Skip to content

LTM: pinned loops cannot be scored on large or arrayed models #653

Closed

Description

@bpowers

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

Components affected

  • src/simlin-engine/src/db/ltm/pinned.rs -- model_pinned_loops builds the pinned Loop without A2A dimension classification (Gap 2).
  • src/simlin-engine/src/db/ltm/mod.rs / link_scores.rs -- model_ltm_variables link-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_tiered dimension 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

Activity

  1. added
    bugSomething isn't working
    engineIssues with the rust-based simulation engine
    ltmLoops that Matter (LTM) analysis subsystem
    on Jun 1, 2026
  2. bpowers commented on Jun 1, 2026

    @bpowers
    OwnerAuthor

    Resolved by the six-commit series 90287d13..521fc374 on main (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_loops now classifies each resolved pin cycle via classify_cycle:
      • PureScalar → scalar loop score (unchanged).
      • PureSameElementA2A → one arrayed loop score over the cycle's dimensions, per-slot partitions.
      • CrossElementOrMixed (the MDL-importer / C-LEARN shape) → the cycle is expanded on the element graph and grouped with the same build_element_level_loops machinery the enumerator uses: diagonal families collapse into one arrayed score whose per-slot Equation::Arrayed equations reference each element's own link scores; genuinely cross-element instances become element-subscripted scalar loops; multi-instance pins get pin{n}⁚{j} ids.
    • The same machinery fixed the latent enumerated-path bug (the A2A-collapse referencing one arbitrary element's FixedIndex link score for every slot).
    • A pin that cannot be scored (oversized expansion SCC, no element-level instantiation) is now an explicit Warning naming the pin — never a silent 0.

    Validation

    • clearn_pinned_climate_loop_is_scored (ignored/runtime-class, 6.4s release): pinning "Feedback cooling" through the production SetLoopName patch path on C-LEARN v77 → one arrayed score over scenario, deterministic slot finite, non-zero, and negative (balancing) at every step.
    • All nine of the experience notebook's climate loops pin and score per scenario; the notebook's section-6 dominance analysis now reads engine pin scores directly (with a hand-composed Eq. 3 cross-check asserting equality), 0 cell errors.
    • clearn_with_ltm_simulates_model_vars_identically still passes: LTM instrumentation never perturbs model values.
    • Small-model TDD coverage: 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingengineIssues with the rust-based simulation engineltmLoops that Matter (LTM) analysis subsystem

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions