topos

Why it is shaped this way

Several exports, one spec

Train and sample share weights. They should not compute the same nodes.

The same model can train on a batch and generate a token. Those two jobs share weights. They should not compute the same nodes. Train does not need the sampling head. Decode does not need the full window.

The list of values this run may read is what makes that split real.

The decision#

Observability is a license: say what you want to read, once. Everything derived — the slow run, the plan, fusion, emission — honors that list. A read off the list fails loudly. A name you asked to see, sitting inside a chain that could have been fused, blocks the fusion — unless the match names it as a result it writes back. Optimization knows what must survive and what it may drop.

What it opened#

   0  Parameter                         [4, 3]       freed after 3
   1  Parameter                         [3]          freed after 4
   2  Input                             [2, 4]       freed after 3
   3  MatMul         2, 0               [2, 3]       freed after 5
   4  BroadcastAlong 1  axis=0 extent=2 [2, 3]       freed after 5
   5  Add            3, 4               [2, 3]       freed after 11
   6  Input                             [2, 3]       freed after 14
   7  SumAlong       6  axis=1          [2]          freed after 9
   8  LogSumExp      5  axis=1          [2]          freed after 9
   9  Mul            7, 8               [2]          freed after 10
  10  Sum            9                  []           freed after 13
  11  Mul            6, 5               [2, 3]       freed after 12
  12  Sum            11                 []           freed after 13
  13  Sub            10, 12             []           freed after 15
  14  Sum            6                  []           freed after 15
  15  Div            13, 14             []           kept
plan: forward; 16 of 18 nodes evaluated, 1 readable
live volume: peak 29 elements at node 3, retain-all 64
Plate 1. Two entries over one sealed network: the training entry never schedules the sampling head, and the sampling entry never schedules the loss.

One graph can export several functions. You record them together. Each run names which export it is.

The names you keep from recording are the ones later code may mention. They are not "compute all of these every time." The training function can skip the sampling head. The decode function can skip the full window.

Printing the plan for another compiler uses the same list, in the same order. The plan is already a closed function with those results. Emission does not invent an output convention. It writes the list down.

Recurrence as feeds is how a decode function carries last step's keys and values. This article is why decode is its own function, not a mode of the full-window one.

Spelled today#

Entry is that list: results, extra values to observe, memory and numerics choices. network.entry([loss]) binds it. interpret runs the slow path over just those ancestors. lower builds the plan. Plan::results is the list emit_stablehlo returns. Run::of panics on a name this run did not compute. Network::forward still runs the whole graph. That is the debug path, not training.

This section may rot; the rest must not.

Not this#

  • Guessing the list from buffers that happened to survive. If you did not ask to read it, it is not readable.
  • One implied function per graph.
  • Treating every name you detached at record time as something this run must compute.

See Observability is a license for the rule, and Fusion raises for the barrier that list is.