Repository navigation
LTM: pinned loops cannot be scored on large or arrayed models #653
Description
Summary
Loop pinning (the SetLoopName / LOOPSCORE escape hatch landed in #648) was designed precisely for models too large for exhaustive loop enumeration. But on exactly those models -- and on arrayed models generally -- the pinned loops cannot actually be scored. The pinning workflow works structurally (pins appear in model.loops with the correct cycles), but none of the pinned loops can produce a usable score. There are two independent gaps.
Gap 1: pins still require full discovery instrumentation
In discovery mode, model_ltm_variables emits link scores for ALL causal edges (the per-element blowup tracked in #647) plus the pinned $⁚ltm⁚loop_score⁚pin{n} variables. On C-LEARN that instrumentation layout reaches ~171k slots, which now exceeds the bytecode's u16 offset limit of 65,536 slots (rejected by commit aea4b9a5, "engine: reject layouts beyond the bytecode's u16 offset range"). So an LTM-enabled sim of C-LEARN fails to compile, and the pin scores are never produced.
A pin's loop score only needs the link scores along its own cycle -- roughly 10-30 variables for a typical climate feedback loop -- not all ~150k element-level link scores. A "pinned-only" instrumentation mode -- emit link scores only for the edges that lie on a pinned loop, applied when the full instrumentation would not fit (or always, in discovery mode) -- would make pinning work at any model scale. It would also make LTM-enabled C-LEARN runs roughly as cheap as non-LTM ones.
Gap 2: pinned loop scores on arrayed (A2A) variables produce broken equations
model_pinned_loops (src/simlin-engine/src/db/ltm/pinned.rs) builds the pinned Loop as a scalar (no A2A dimension classification on the cycle), so the generated loop-score equation is a Scalar equation that multiplies link-score variables which are themselves ARRAYED (A2A over e.g. [scenario]).
Observed on C-LEARN (before the Gap-1 slot-limit error made it moot): pin1's loop-score equation was
Scalar("\"$⁚ltm⁚link_score⁚feedback_cooling[deterministic]→heat_in_atmosphere_and_upper_ocean_net_flow\" * \"$⁚ltm⁚link_score⁚heat_in_atmosphere_and_upper_ocean_net_flow→heat_in_atmosphere_and_upper_ocean\" * ...")
This mixes per-element scalar link scores with bare references to arrayed A2A link scores -- a dimension mismatch that fails to compile. The fragment then silently stubs the loop score to a constant 0 (the GH #466 silent-stubbing path).
The pinned-loop path needs the same dimension classification the exhaustive enumerator already applies via build_loops_from_tiered / classify_cycle (src/simlin-engine/src/db/analysis.rs, src/simlin-engine/src/db/ltm/loops.rs): a pure-A2A pinned cycle should produce an arrayed loop score (one slot per element); a mixed cycle needs element-level expansion.
Why it matters
- Correctness / silent failure. A pinned A2A loop currently stubs to a constant-0 score, so the loop looks inactive when it is not -- the same silent-degradation hazard class as LTM auto-flip warning unreachable through simlin_project_get_errors #466/ltm: composite-reference link score into a stdlib-macro module fails to compile in exhaustive mode, so loops through SMOOTH/DELAY stub to 0 #548.
- The feature is unusable where it is most needed. Pinning exists because large models auto-flip to discovery (and discovery itself is intractable on large arrayed models, ltm-finding: strongest-path discovery infeasible on large arrayed models (element-level blowup) #647). Both gaps land squarely on that intended use case: large models hit the slot limit (Gap 1), and arrayed models hit the dimension mismatch (Gap 2). C-LEARN's 8 climate feedback loops (feedback cooling, deep-ocean heat uptake, ocean/biomass/soil carbon uptake, permafrost release, ocean/biomass uptake weakening) pin structurally but none can be scored.
Components affected
src/simlin-engine/src/db/ltm/pinned.rs--model_pinned_loopsbuilds the pinnedLoopwithout A2A dimension classification (Gap 2).src/simlin-engine/src/db/ltm/mod.rs/link_scores.rs--model_ltm_variableslink-score instrumentation; needs a pinned-only emission mode (Gap 1).src/simlin-engine/src/db/analysis.rs,src/simlin-engine/src/db/ltm/loops.rs--classify_cycle/build_loops_from_tiered, the dimension-classification machinery the pinned path should reuse (Gap 2).
Possible approaches
- Gap 1: add a pinned-only instrumentation mode that walks the resolved pinned loops, collects the set of causal edges on those cycles, and emits link scores only for that edge set. Apply it when full instrumentation would exceed the slot budget (or unconditionally in discovery mode). This both fixes the slot-limit failure and makes LTM-enabled runs on large models cheap.
- Gap 2: route the pinned cycle through the same
classify_cycle/build_loops_from_tiereddimension classification the exhaustive enumerator uses, so a pure-A2A pin yields an arrayed loop score and a mixed pin gets element-level expansion -- instead of a scalar equation that references arrayed link scores.
How it was discovered
Found during the 2026-05-31 LTM-on-C-LEARN experience session, while exercising the pinning workflow on C-LEARN's 8 climate feedback loops. The pins resolve and appear in model.loops with correct cycles, but none can be scored: large/arrayed models hit Gap 1 (slot limit), and the arrayed equation shape hits Gap 2 (dimension mismatch -> silent 0).
Linkage
- Part of the LTM epic epic: Loops that Matter (LTM) -- open work #488.
- Related to ltm-finding: strongest-path discovery infeasible on large arrayed models (element-level blowup) #647 (discovery instrumentation blowup on large arrayed models -- the same per-element link-score explosion that Gap 1's pinned-only mode would sidestep) and LTM auto-flip warning unreachable through simlin_project_get_errors #466 (silent stubbing to 0 -- the path Gap 2's broken equation falls into).
- Builds on LTM usability: discovery exposure, mode visibility, #548 fix, loop pinning #648 / ltm: decide whether to support Stella's LOOPSCORE and PATHSCORE #484 (loop pinning / LOOPSCORE), which delivered the pinning capability this issue reports as unscorable at scale.
Activity
bpowers commentedon Jun 1, 2026
Resolved by the six-commit series
90287d13..521fc374onmain(design plan:docs/design-plans/2026-06-01-ltm-653-pinned-loop-dimensions.md).Gap 1: obsoleted, no pinned-only instrumentation mode needed
The discovery-cost work that landed right after this issue was filed (
099c5659..477af2ab) removed the premise: C-LEARN's LTM-discovery layout went from 171,498 slots (rejected by the u16 limit) to 29,764 slots, the discovery DFS completes all 251 steps in 0.08s, and the LTM compile is ~4s in release. Pins simply ride the normal discovery instrumentation. The residual slot-headroom concern for even larger models stays tracked in #654.Gap 2: fixed — pinned cycles get the same dimension classification as enumerated loops
model_pinned_loopsnow classifies each resolved pin cycle viaclassify_cycle:build_element_level_loopsmachinery the enumerator uses: diagonal families collapse into one arrayed score whose per-slotEquation::Arrayedequations reference each element's own link scores; genuinely cross-element instances become element-subscripted scalar loops; multi-instance pins getpin{n}⁚{j}ids.Validation
clearn_pinned_climate_loop_is_scored(ignored/runtime-class, 6.4s release): pinning "Feedback cooling" through the productionSetLoopNamepatch path on C-LEARN v77 → one arrayed score overscenario, deterministic slot finite, non-zero, and negative (balancing) at every step.clearn_with_ltm_simulates_model_vars_identicallystill passes: LTM instrumentation never perturbs model values.tests/simulate_ltm_pinned.rs(pure-A2A, multi-dim, per-element-equation, mixed, cross-element, invalid-pin cases),tests/ltm_array_agg.rs(the enumerated-path fix with analytically-known per-slot values),src/pysimlin/tests/test_pinned_loops.py(FFI/Python end-to-end).Follow-ups filed
#[ignore]d).Loopobjects.engine: add dimension-aware LTM loop-score equation generation
engine: classify pinned-loop cycles so pure-A2A pins score per element
engine: expand cross-element/mixed pinned loops on the element graph
pysimlin: cover arrayed pinned-loop scoring end to end
engine,notebooks,doc: validate pinned-loop scoring on C-LEARN end to end