somatize_core/memory.rs
1//! What is remembered about each node: the fifth fact, beside [`Graph`] (what
2//! exists), [`Catalog`](crate::Catalog) (who executes it),
3//! [`Placement`](crate::Placement) (where) and [`Plan`](crate::Plan) (when).
4//!
5//! Four maps, independent of each other: a node can be frozen without being
6//! cached, named without being frozen, and any combination of the rest.
7//!
8//! ```text
9//! key(root) = H(content) ← the only place data is hashed
10//! key(node) = H(identity, declaration, state, keys of its predecessors)
11//! ```
12//!
13//! The **identity** is in the key or two different nodes called `embed` collide
14//! in a shared store; the **declaration** is in for the same reason one step
15//! down, since `Embed(512)` and `Embed(64)` are one class and two answers. The
16//! **fingerprint of the code** is deliberately not: a cosmetic refactor would
17//! invalidate half the store in silence, so it is kept beside the value and
18//! compared on a hit. The line is what the caller **said** against how it is
19//! **written** — only the first can be pinned down identically in every
20//! process, which a key computed on a client and again on a worker must be.
21//!
22//! **A frozen node's state does not change while the graph runs.** That is a
23//! statement about cache validity, held here as inert information the core
24//! reasons over in [`cacheable`] and somebody else obeys — `somatize.torch` is
25//! what makes it true with `requires_grad_(False)`.
26
27use crate::{Graph, NodeId};
28use std::collections::{HashMap, HashSet};
29use std::fmt;
30
31/// What is remembered about each node. The ones not listed have nothing said
32/// about them, which is the same as nothing being kept.
33#[derive(Debug, Default, Clone, PartialEq, Eq)]
34#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
35pub struct Memory {
36 /// The ones whose state does not change while the graph runs, each with the
37 /// digest of the state it is settled at — `None` when there is none.
38 frozen: HashMap<NodeId, Option<String>>,
39 /// The ones whose output is worth keeping, each with the salt its declarer
40 /// added, if any.
41 cached: HashMap<NodeId, Option<String>>,
42 /// The ones that map over the items of their input, so that what is
43 /// remembered of them is remembered item by item.
44 mapped: HashSet<NodeId>,
45 /// What implements each one, by name.
46 identities: HashMap<NodeId, String>,
47 /// What each one was built with, digested — the arguments that made this
48 /// instance and not another of the same class.
49 declarations: HashMap<NodeId, String>,
50 /// Which version of that code the graph was written against. Metadata.
51 fingerprints: HashMap<NodeId, String>,
52}
53
54impl Memory {
55 /// Nothing remembered about anything.
56 pub fn new() -> Self {
57 Self::default()
58 }
59
60 /// Says what implements this node, returning what it was called before.
61 pub fn identify(&mut self, id: impl Into<NodeId>, what: impl Into<String>) -> Option<String> {
62 self.identities.insert(id.into(), what.into())
63 }
64
65 /// What implements it, if it was said.
66 pub fn identity_of(&self, id: &NodeId) -> Option<&str> {
67 self.identities.get(id).map(String::as_str)
68 }
69
70 /// Says what this node was built with, as a digest of the arguments that
71 /// made it. The class is half of what a node is; this is the other half.
72 pub fn declared_as(&mut self, id: impl Into<NodeId>, declaration: impl Into<String>) {
73 self.declarations.insert(id.into(), declaration.into());
74 }
75
76 /// What it was built with, if anybody could say. Absent is not a reason to
77 /// refuse a key; refusing is [`cacheable`]'s side of it.
78 pub fn declaration_of(&self, id: &NodeId) -> Option<&str> {
79 self.declarations.get(id).map(String::as_str)
80 }
81
82 /// Says this node's state does not change from here on, with the digest of
83 /// the state it is settled at. Called twice on purpose: `.frozen()` says it
84 /// with no digest, and whoever can hash the weights says it again with one.
85 pub fn freeze(&mut self, id: impl Into<NodeId>, state: Option<String>) {
86 self.frozen.insert(id.into(), state);
87 }
88
89 /// Whether this node's state was said not to change.
90 pub fn is_frozen(&self, id: &NodeId) -> bool {
91 self.frozen.contains_key(id)
92 }
93
94 /// The digest of the state it is frozen at, if it is frozen and has one.
95 pub fn state_of(&self, id: &NodeId) -> Option<&str> {
96 self.frozen.get(id)?.as_deref()
97 }
98
99 /// Says this node's output is worth keeping, with the caller's salt if they
100 /// gave one — how you tell apart two runs the key cannot.
101 pub fn cache(&mut self, id: impl Into<NodeId>, salt: Option<String>) {
102 self.cached.insert(id.into(), salt);
103 }
104
105 /// Whether this node's output is kept. Not being kept is no break in the
106 /// chain: its key is still computed and passed on.
107 pub fn is_cached(&self, id: &NodeId) -> bool {
108 self.cached.contains_key(id)
109 }
110
111 /// The salt it is cached under, if it is cached and has one.
112 pub fn salt_of(&self, id: &NodeId) -> Option<&str> {
113 self.cached.get(id)?.as_deref()
114 }
115
116 /// Says this node maps over the items of its input: a list in, a list as
117 /// long out, item `i` out from item `i` in.
118 ///
119 /// What makes a cache work item by item — one new document among a thousand
120 /// runs once instead of missing all thousand. Declared and not asked.
121 pub fn map(&mut self, id: impl Into<NodeId>) {
122 self.mapped.insert(id.into());
123 }
124
125 /// Whether this node was said to map over its items.
126 pub fn is_mapped(&self, id: &NodeId) -> bool {
127 self.mapped.contains(id)
128 }
129
130 /// Notes which version of the code this graph was written against.
131 /// **Metadata**: never in a key, only compared on a hit.
132 pub fn written_as(
133 &mut self,
134 id: impl Into<NodeId>,
135 fingerprint: impl Into<String>,
136 ) -> Option<String> {
137 self.fingerprints.insert(id.into(), fingerprint.into())
138 }
139
140 /// Which version of the code it was written against, if it was noted.
141 pub fn fingerprint_of(&self, id: &NodeId) -> Option<&str> {
142 self.fingerprints.get(id).map(String::as_str)
143 }
144
145 /// How many nodes have anything said about them at all.
146 pub fn len(&self) -> usize {
147 self.frozen
148 .keys()
149 .chain(self.cached.keys())
150 .chain(self.identities.keys())
151 .chain(self.fingerprints.keys())
152 .collect::<HashSet<_>>()
153 .len()
154 }
155
156 /// Whether nothing has been said about any node, which is what lets the
157 /// engine skip all of this.
158 pub fn is_empty(&self) -> bool {
159 self.frozen.is_empty()
160 && self.cached.is_empty()
161 && self.identities.is_empty()
162 && self.declarations.is_empty()
163 && self.fingerprints.is_empty()
164 }
165}
166
167/// Whether what this graph says to keep can honestly be kept. A free function
168/// for the same reason [`compile`](crate::compile) is one: it needs the graph
169/// **and** the table.
170///
171/// > A node's output can be kept if nothing upstream of it can change — itself
172/// > included.
173///
174/// Freezing the node alone is not enough: what is restored from a store is a
175/// leaf, so the backward pass stops there and everything above it quietly stops
176/// training. The same rule falls out without mentioning gradients — the digest
177/// of the state is in the key, so a node that keeps changing never hits and only
178/// fills the store. Being named is checked in the same walk, since a chain with
179/// a hole in it delivers no key below.
180pub fn cacheable(graph: &Graph, memory: &Memory) -> Result<(), MemoryError> {
181 for id in graph.nodes() {
182 if !memory.is_cached(id) {
183 continue;
184 }
185 for above in upstream(graph, id) {
186 if !memory.is_frozen(&above) {
187 return Err(MemoryError::Unsettled {
188 cached: id.clone(),
189 moving: above,
190 });
191 }
192 if memory.identity_of(&above).is_none() {
193 return Err(MemoryError::Nameless {
194 cached: id.clone(),
195 nameless: above,
196 });
197 }
198 }
199 }
200 Ok(())
201}
202
203/// This node and everything it reads, transitively, **nearest first** — so what
204/// an error names is the closest thing to the problem and not the furthest.
205fn upstream(graph: &Graph, id: &NodeId) -> Vec<NodeId> {
206 let mut seen: HashSet<&NodeId> = HashSet::from([id]);
207 let mut out = vec![id.clone()];
208 let mut next = 0;
209 while next < out.len() {
210 for above in graph.predecessors(&out[next]) {
211 if seen.insert(above) {
212 out.push(above.clone());
213 }
214 }
215 next += 1;
216 }
217 out
218}
219
220/// Why what this graph says to keep could not honestly be kept.
221#[derive(Debug, Clone, PartialEq, Eq)]
222pub enum MemoryError {
223 /// Something a cached node depends on can still change.
224 Unsettled {
225 /// The one that says to keep its output.
226 cached: NodeId,
227 /// The one that can still change. The same node when it is itself.
228 moving: NodeId,
229 },
230 /// Something a cached node depends on has no identity, so no key.
231 Nameless {
232 /// The one that says to keep its output.
233 cached: NodeId,
234 /// The one nobody said what it is.
235 nameless: NodeId,
236 },
237}
238
239impl fmt::Display for MemoryError {
240 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241 match self {
242 Self::Unsettled { cached, moving } if cached == moving => write!(
243 f,
244 "`{cached}` keeps its output and is not frozen: what is worth keeping \
245 is what does not change, and a node that still trains gets a new key \
246 every run"
247 ),
248 Self::Unsettled { cached, moving } => write!(
249 f,
250 "`{cached}` keeps its output and `{moving}`, which it reads, is not \
251 frozen: an output is only reusable if nothing above it can change"
252 ),
253 Self::Nameless { cached, nameless } if cached == nameless => write!(
254 f,
255 "`{cached}` keeps its output and nobody said what implements it, so \
256 there is nothing to build its key out of"
257 ),
258 Self::Nameless { cached, nameless } => write!(
259 f,
260 "`{cached}` keeps its output and nobody said what implements `{nameless}`, \
261 which it reads: a chain of keys with a hole in it reaches nothing below"
262 ),
263 }
264 }
265}
266
267impl std::error::Error for MemoryError {}