Skip to content

Overview

There are three things to hold, and everything else follows from them.

Five facts, and confusing them is the easy mistake

Section titled “Five facts, and confusing them is the easy mistake”
sayslives in
Graphwhat existssoma-core/src/graph.rs
Catalogwho executes itsoma-core/src/catalog.rs
Placementwheresoma-core/src/placement.rs
Planwhensoma-core/src/plan.rs
Memorywhat is remembered of each nodesoma-core/src/memory.rs

They are five types and not one struct with five fields, and each separation was forced by something.

Graph is topology only — identities and edges, nothing about what a node does. That is why it is data: it serialises, it compares, it gets sent somewhere else. Creating a graph does not need to know what any node does, so it does not know, and that is the reason the core depends on nothing.

Catalog is the half that is not data. An implementation does not serialise and does not travel. What joins the two is the node id and nothing else. When a subgraph goes to another machine, the graph travels and the implementations do not — which is exactly why they had to be apart.

Placement is two maps and not a pair, because the two halves are obeyed by different people. distribute reads the host when deciding the shape of the plan; the node reads the device through ctx.device when executing. A node can have either, both, or neither.

Plan says when, and it deliberately does not carry the device. A device is inert for the traversal — it changes nothing about who waits for whom — and crossing a wire is not, so crossing a wire is a named step and a device is not.

Memory is four maps, independent of one another: a node can be frozen without being cached, named without being frozen, and any combination of the rest. What it settles is the key:

key(root) = H(content) ← the only place data is hashed
key(node) = H(identity, declaration, state, keys of its predecessors)

The identity is in there or two different nodes called embed collide in a shared store. The declaration is in there because Embed(512) and Embed(64) are one class and one identity — and, before that part existed, one name, so the second run was handed the first one’s answer with no error and no warning.

The core defines five traits. It implements none of them, and that is the whole of its extension model — no plugin system, no registry, no configuration:

traitwhat somebody else supplies
Nodethe work. Yours
Transportcarrying a slice of a plan somewhere else
Keeperhashing a recipe, and keeping what it names
Watcherbeing told what happened
Codecwriting down what only exists in one process

A trait is only a trait here when the implementation comes from someone else, and if two real implementors cannot be named today it is a struct instead. Codec earned the promotion late: it came into the core when a third tenant showed it was not the wire’s.

There was a sixth. Driver served what a suspended node asked for, and after eighteen use cases it had no consumer outside its own tests — its own docstring said it existed to keep the agentic layer out of the core, which is a hole justifying itself. It was deleted, along with the suspension it existed for: a node is a function.

What it left behind is worth more than what it was. Ctx is the channel whoever executes hands a node, so anything that wants a value injected puts it there and no node signature changes. See the rules it is written under.

Three levels, and none knows the one above exists

Section titled “Three levels, and none knows the one above exists”
levelscalewhat it is
the graphone forwarda network
the Traineran afternoona training run
N training runsa campaigna Python list
flowchart TB
    L3["N training runs · a campaign<br/>a Python list. No type at all"]
    L2["Trainer · an afternoon<br/>one training run"]
    L1["Graph · one forward<br/>a network"]

    L3 -->|"calls"| L2
    L2 -->|"calls"| L1
    L1 -.->|"what it produced"| L2
    L2 -.->|"a loss, a verdict"| L3
Downward is a call. Upward is only ever a return value — no level is handed a callback into the one above it.

The third one has no type, and that is on purpose. A graph earns its keep when there are dependencies to declare; N runs have none, so fedavg is a function and a federated round is a for loop. Making level 3 a graph would have been the same mistake one level up.

The line between them is what decides where a thing lives. Micro-batches are level 2 — the batch belongs to the caller, so torch.chunk reaches it — while .mapped() is the engine’s, because caching item by item has to name each item, and a name comes from its content and not its place.

And nothing is asked of a level below. A pruner answers and the training loop stops calling; the loop is never handed a callback to run. See the rules for what that cost the version before this one.

The plan is where the five facts turn into an execution: what compile does, why it decomposes rather than flattens, and what distribute adds afterwards.