Tnode.PlacementsThe context-scoped side of the memory-mode split (docs/proposals/context-scoped-memory-modes.md): memory_mode_intent on the tnode is *declared intent* -- requests made at graph-construction time (user set_materialized, parameter/constant marking, op-support Never_virtual) -- while the *decisions* (resolving to Virtual / Local / On_device) are recorded here, per compilation lineage. A Placements table rides Low_level.optimize_ctx: it is copied at the start of each backend compile, so sibling compiles from the same context are hermetic (a candidate compile cannot poison another's placement resolution), while child contexts inherit the lineage's decisions.
Lookups fall back to the tnode's intent when the lineage has not yet decided; updates apply the same lattice as update_memory_mode but never write the tnode. Intent strengthened after a lineage compiled does not invalidate that lineage.
module Key : sig ... endval sexp_of_t : t -> Sexplib0.Sexp.tval create : unit -> tval get : t -> t Base.Hashtbl.key -> (memory_mode * provenance) Base.optionThe effective placement state: the lineage's decision if any, otherwise the tnode's declared intent. Side-effect free.
val update : t -> t Base.Hashtbl.key -> memory_mode -> provenance -> unitval promote_local_to_device : t -> t Base.Hashtbl.key -> provenance -> unitKernel-fission escape hatch: strengthen a routine-scoped Local decision to On_device. Local is only ever a compiler decision (never declared intent), premised on the node's lifetime being confined to one kernel launch; when the schedule layer splits a routine into multiple kernels at a point where the node's live range crosses, that premise is withdrawn and the node must own a context buffer. The update lattice deliberately rejects Local -> On_device (decisions are final within a lineage); this is the one sanctioned override, sound exactly because fission runs between optimization and code generation — before any consumer of the decision (codegen parameter lists, context allocation) has read it.
val raw_entry : t -> t Base.Hashtbl.key -> (memory_mode * provenance) optionFission bookkeeping around promote_local_to_device: the raw lineage entry (no intent fallback), captured before a promotion so unsafe_restore can undo it when segment coalescing removes the crossing that motivated it. Undoing is sound in that direction only: schedules computed under the (stricter) materialized view remain valid for a Local node, while the converse would miss coverage requirements.
val unsafe_restore :
t ->
t Base.Hashtbl.key ->
(memory_mode * provenance) option ->
unitval default_to_most_local : t -> t Base.Hashtbl.key -> provenance -> unitMirrors the retired tnode-level default_to_most_local, resolving into the placements table, with two observation guards (docs/proposals/context-scoped-memory-modes.md):
Local: Local is the unobservable class, and unlike Virtual it cannot be served by recomputation.None) at a forcing point materializes instead of defaulting to Virtual: a node that stayed undecided through optimization has no tracked computation (the virtualizer commits every stored candidate at cleanup), so a Virtual resolution would be unobservable in practice -- there would be nothing to recompute from. Effectively_constant keeps folding to Virtual: observation of constants is served by their registered host-init data.val is_virtual_force : t -> t Base.Hashtbl.key -> provenance -> boolThe forcing family -- is_virtual_force, is_materialized_force and is_in_context_force -- answers a placement question and, where the placement is still open, resolves the node on the spot.
A provenance is recorded exactly where the query WRITES the table, and each query writes on its own set of open states:
is_virtual_force records on None and Effectively_constant. Never_virtual reaches its catch-all false arm instead, and the tag is discarded.is_materialized_force records on Never_virtual and Effectively_constant. None is an assertion failure, not a defaulting point.is_in_context_force records on None, Never_virtual and Effectively_constant -- unless the node is a slice alias, which answers false ahead of the match and records nothing whatever its state.The settled placements -- Virtual, Local, On_device -- answer and record nothing, in every query. And get answers from the effective state, so a settled placement is either this lineage's table decision or the tnode's declared memory_mode_intent: a node minted On_device at construction, set Virtual by Train.set_virtual, or declared Local outright, silences every later query without ever acquiring a table entry.
So a query site's tag is the reason the node got defaulted at that query, not a claim that the query is load-bearing: most call sites never mint their literal.
The goldens bear this out. c_syntax.ml alone carries twelve query sites, each with its own tag; a repository-wide search of the committed .expected files finds fourteen distinct tags in printed placements, every one minted at graph construction (tensor.ml, train.ml) or by the virtualizer in low_level.ml -- not one from a codegen query.
val is_materialized_force : t -> t Base.Hashtbl.key -> provenance -> boolval is_in_context_force : t -> t Base.Hashtbl.key -> provenance -> boolPure counterpart of is_materialized_force; see Tnode.is_materialized_peek.