Skip to content

12 — Where a value came from

A store outlives every process that ever wrote to it. What survives in it is a pile of names that are hashes of recipes, and a recipe does not run backwards: from a key there is no path back to what produced it.

That is fine for a week. Inside one afternoon of trying five things — which is what an afternoon looks like — each version leaves its intermediates behind, and a month later nobody can say which of the five any of them came from. They are not wrong. They are mute, which with time is worse: an expensive tensor nobody can attribute is an expensive tensor nobody dares delete and nobody dares reuse.

So what cannot be recovered is written down at the moment it is known, beside the value. This notebook is what that looks like.

import json
import pathlib
import sys
import tempfile
import textwrap
from somatize import Graph, Store
from somatize import _environment

A class in a cell has no version, and that is the point

Section titled “A class in a cell has no version, and that is the point”

Node subclasses defined here cannot be fingerprinted: inspect.getsource has nothing to read for a class typed into a cell, so there is no version to compute and none to write down. That is not a limitation to work around, it is the same absence UNVERSIONED names one level up — and pretending otherwise would make this notebook green about something that is not true of real code.

So the graph goes in a module, written to a temporary directory and imported. Notebook 11 does the same thing for the same reason.

where = pathlib.Path(tempfile.mkdtemp())
sys.path.insert(0, str(where))
(where / "net.py").write_text(textwrap.dedent('''
from somatize import Graph, Node
SCALE = 0.5
class Embed(Node):
def forward(self, words, ctx):
return [len(word) * SCALE for word in words]
def build():
g = Graph.somatize(Embed().named("embed").frozen().cached())
g.freeze("embed", "pesos-v1")
return g
'''))
import net
TEXT = ["una", "tarde", "probando", "cosas"]

stamping= is where the caller says what only the caller knows: which commit was checked out, which investigation this belongs to. Anything else — an experiment id, a machine, a ticket — goes in the same way. It is text, and the engine passes it through without being told what any of it means.

kept = Store(tempfile.mkdtemp())
net.build().forward(TEXT, store=kept, stamping={"run": "una-investigacion/3847d0c1"})
for bound in sorted(kept.bound(), key=lambda one: one.name):
print(bound.name)
env/2d3f3bfb8a70
value:sha256:a75d6be88a202ad296baf6ca7eec6b219ab27c5e0d51fb8d638209b252608a32

Two names, and they are different kinds of thing. One is the value the graph produced. The other is the reading of the environment it was produced against, which is explained further down.

def said_of(store, node):
"""What was written beside the value that node produced."""
for bound in store.bound():
meta = dict(bound.meta)
if meta.get("node") == node:
return meta
raise LookupError(node)
print(json.dumps(said_of(kept, "embed"), indent=2))
{
"node": "embed",
"fingerprint": "0dac3f10",
"input": "sha256:cbf63fa6e2ca3bc957ab89c569f7b619f0d44461962e7af561ea649583948df8",
"env": "2d3f3bfb8a70",
"run": "una-investigacion/3847d0c1"
}

Five things, and who is standing where each one is knowable

Section titled “Five things, and who is standing where each one is knowable”
written bywhatcould it be recovered later?
the enginenode, and the fingerprint of its codeit already could
the engineinput, by the name its content hasnever — only a keeper can hash a value
somatizeenvnever — it is in no key
the callerrun, and whatever else they knownot by anybody else

The input row is the one worth pausing on. A graph’s root is the single place data is hashed by content — from there down, names are made of names — so the identity of what went in is knowable only while the run is happening, and by the only participant holding a hashing algorithm. Afterwards there is a hash of it inside every key downstream and no way back out of one.

The same graph, with nothing passed. Provenance that has to be remembered is missing from exactly the runs nobody thought were going to matter, which is why four of the five land whether or not anybody was thinking about it.

alone = Store(tempfile.mkdtemp())
net.build().forward(TEXT, store=alone)
print(json.dumps(said_of(alone, "embed"), indent=2))
{
"node": "embed",
"fingerprint": "0dac3f10",
"input": "sha256:cbf63fa6e2ca3bc957ab89c569f7b619f0d44461962e7af561ea649583948df8",
"env": "2d3f3bfb8a70"
}

Only run is gone, and it is the only one that needed a word from outside. A graph run with no experiment tool anywhere near it still leaves a store that describes itself.

That is what makes adopting one later a read rather than a migration: the join is on the key, the node and the fingerprint, none of which anybody had to have planned for.

Why an environment, when there is already a fingerprint

Section titled “Why an environment, when there is already a fingerprint”

Because a fingerprint stops at what is installed. A distribution goes into it by name and version; the standard library goes in by its bare name, since the interpreter is compared once at the greeting rather than hashed into every class.

That is the right call for naming and the wrong one for provenance: two interpreters name the same node identically, and only one of them produced what is actually on the disk. So it is written down instead — twelve characters on each value, and the reading of them once, under a name anybody can cat. Whoever opens this store in a year needs both: the short one to group by, the long one to understand.

name = said_of(kept, "embed")["env"]
reading = kept.recall(f"{_environment.WHERE}/{name}")
print(name, "→")
print(json.dumps(reading, indent=2)[:400], "…")
2d3f3bfb8a70 →
{
"Pygments": "2.20.0",
"asttokens": "3.0.1",
"comm": "0.2.3",
"cuda-bindings": "12.9.4",
"cuda-pathfinder": "1.5.4",
"debugpy": "1.8.20",
"decorator": "5.2.1",
"executing": "2.2.1",
"ipykernel": "7.2.0",
"ipython": "9.12.0",
"jedi": "0.19.2",
"jupyter_client": "8.8.0",
"jupyter_core": "5.9.1",
"orjson": "3.11.9",
"packaging": "26.0",
"parso": "0.8.6",
"platformdi …

Read that list again: ipykernel, jedi, debugpy. What goes in is what the process reached for, and a process running a notebook reached for a notebook. So the same code, run from a script and run from here, gets two different environments — which is true, and is not what anybody meant.

It is left true rather than filtered, because the alternative is worse in the one direction that matters. A filter that dropped what “looks like tooling” would be a guess about somebody else’s imports, and the day it dropped something a graph really used, the record would be quietly wrong about the one thing it exists to be right about. Narrowing it to what a graph reached for is possible — somatize does not know that today, and it would be a change to what an environment is, not a tidy-up.

It is bound and not claimed, so two runs in the same environment write the same reading and the second is not a race anybody lost.

net.build().forward(TEXT, store=kept)
readings = [one.name for one in kept.bound() if one.name.startswith(_environment.WHERE)]
print(readings, "— one, after two runs")
['env/2d3f3bfb8a70'] — one, after two runs

node, fingerprint and input are the engine’s. Stamping one is refused where somebody is typing, rather than dropped somewhere they will not see it: whoever writes it believes they are saying something, and a value that came back naming a different node would be the single mistake this whole mechanism exists to prevent.

try:
net.build().forward(TEXT, store=kept, stamping={"node": "otro"})
except ValueError as why:
print(why)
`node` is written by the engine itself, so `stamping` cannot set it: node, fingerprint, input are its own. Anything else is yours

It is refused at the door and dropped in the core, which is not redundancy doing nothing. Whether the first or the last of two identical keys wins is the reader’s convention — and the obvious way to read a list of pairs, turning it into a map, takes the last. So the pair is never written at all, and the metadata of a value never has a key in it twice.

With this written down, which of these hashes belongs to which version stops being a question nobody can answer. soma-tree reads it two ways — by the key, which is exact, and by the fingerprint, which survives an environment that no longer exists — and says so per commit.

And what belongs to nothing it can name comes out saying that, rather than being quietly left off the list. embed made it, with code a1b2c3d4, which is not a version I know here is a true sentence. It is not the same as being surplus: it may be from a branch nobody looked at, a commit that is gone, or an environment nobody can rebuild.

  • A .mapped() node is named by the content of its items, so every item has a key of its own. They are stamped like anything else and can be attributed one at a time; what nothing can do is foresee them, so a reader working from a probe alone will not find them.
  • A slice on a worker says nothing about the graph’s input, on purpose: what arrives at a slice is not what arrived at the graph, and stamping it there would be a confident lie about the one field nothing can check afterwards.
  • Nothing deletes. A content-addressed store where two versions legitimately share a blob needs unbinding and then a sweep over every name — a collection, not an rm — and what is surplus is a decision somebody writes down, not one a tool infers.