Skip to content

ltm-finding: strongest-path discovery infeasible on large arrayed models (element-level blowup) #647

Description

@bpowers

Summary

LTM strongest-path loop discovery (discover_loops_with_graph in src/simlin-engine/src/ltm_finding.rs) does not complete in feasible time on large arrayed models. Discovery is the heuristic path the engine auto-flips to when a model's causal-graph SCC exceeds MAX_LTM_SCC_NODES = 50 (replacing exhaustive Johnson enumeration). On large arrayed models the discovery runs on the element-expanded causal graph, where the node count V reaches the tens of thousands, and it degenerates.

This is distinct from #540 (World3): #540 is the scalar, variable-level dense SCC case (variable-level SCC of 166 scalar nodes). This issue is the element-level expansion blow-up on arrayed models, with C-LEARN as a now-working fixture, and a different proposed fix (run discovery at variable granularity for large arrayed models). The two share a root mechanism (the strongest-path pruning fails when path-score products do not shrink) but have distinct fixtures, measurements, and remediation. Tracked under epic #488; related to #540 and #481.

Measurements (C-LEARN v77, test/xmutil_test_models/C-LEARN v77 for Vensim.mdl)

The LTM sim of C-LEARN produces:

  • 171,498 result slots per step
  • 152,116 element-level link-score columns (45,616 carrying finite non-zero values)

Per-timestep discovery cost explodes on "busy" timesteps:

  • Timestep 1 finishes in ~0.04 s with 0 loops (near initial conditions, scores ~0).
  • The first 5 timesteps together do not finish in > 5 minutes.
  • The full 251-step run does not complete in 20+ minutes.

This persists after a constant-factor optimization: the IndexedSearch integer-indexed-graph + reusable-scratch refactor of the per-timestep DFS (correct, keeps all LTM tests passing, cross-checked against SearchGraph as an equivalence oracle). So the wall is algorithmic, not constant factors.

Root-cause hypothesis (being tested separately)

  1. Wrong granularity. Discovery runs on the ELEMENT-expanded causal graph, so V (node count) is in the tens of thousands. The strongest-path algorithm is ~O(V^2) per stock per timestep, intractable at that scale.
  2. Broken pruning. The DFS prunes via score < best_score, which assumes path-score products shrink as paths extend. C-LEARN has link scores >> 1 (e.g. the ~5.2e7 degenerate macro-internal scores), so products can GROW, defeating the pruning and pushing toward near-exhaustive path enumeration.

Proposed algorithmic redesign (the thing to track)

  • For models above a size threshold, run discovery at VARIABLE level (the collapsed variable graph, small V) rather than element level, so O(V^2) is tractable.
  • And/or repair the strongest-path pruning so it stays effective when link scores exceed 1 (e.g. prune on a normalized/relative score rather than a raw running product).
  • The literature (docs/reference/ltm--loops-that-matter.md section 12.4) also notes discovery can be run at a subset of timesteps -- another lever.

Why it matters

Discovery is meant to be the tractable path for large models, but on a real arrayed model (C-LEARN) it does not finish at all. Any large arrayed model that auto-flips to discovery (SCC > MAX_LTM_SCC_NODES) will hang in the production analysis path. Correctness of the per-timestep DFS is fine; the issue is purely scalability/feasibility at element-expanded scale.

Components affected

  • src/simlin-engine/src/ltm_finding.rs (discover_loops_with_graph, the per-timestep strongest-path DFS, IndexedSearch)
  • docs/reference/ltm--loops-that-matter.md section 12 (algorithm reference)
  • Phase-timing harness: src/simlin-engine/examples/clearn_discover.rs (env vars CLEARN_SKIP_DISCOVERY, CLEARN_DISCOVER_STEPS=N, CLEARN_CAP_SCORES)

How it was discovered

Diagnosed while profiling LTM discovery on C-LEARN v77 using the clearn_discover.rs phase-timing harness, after confirming the IndexedSearch constant-factor refactor did not move the wall.

References

Activity

  1. added
    ltmLoops that Matter (LTM) analysis subsystem
    on May 31, 2026
  2. added a commit that references this issue on Jun 1, 2026
  3. bpowers commented on Jun 1, 2026

    @bpowers
    OwnerAuthor

    Update (2026-05-31, LTM-on-C-LEARN experience session): findings that raise the priority of this issue

    1. The element-level blowup was not just slow -- it silently corrupted ALL results

    The element-expanded layout doesn't just make discovery intractable; on C-LEARN it produced garbage for every LTM result anyone has ever observed on this model. C-LEARN + LTM-discovery instrumentation needs 171,498 result slots, but bytecode VariableOffset is u16 (max 65,536 slots). Offsets past the limit silently wrapped (off as u16), so the variable at offset 65,536 overwrote slot 0 (time). The consequences:

    • Simulated time never advanced.
    • Every saved row was identical.
    • Every LTM result ever observed on C-LEARN -- link scores, loop scores, and the data in the previous experience report -- was garbage built on a non-advancing clock.

    As of commit aea4b9a5 on branch ltm-experience-and-improvements, this is a fast, clear compile error instead of silent corruption. But the corollary is that LTM is entirely unavailable on C-LEARN-scale models until the instrumentation fits under the u16 offset limit -- which makes the granularity fix proposed in this issue a correctness prerequisite, not just a performance optimization.

    2. Where the slots go, and two paths to shrink the count

    The non-LTM C-LEARN layout is 5,215 slots; with discovery-mode LTM it is 171,498 slots (33x). The per-element link-score expansion is the multiplier. Two paths can shrink it dramatically:

    • (a) Fix the MDL importer. The MDL importer expands a single A2A Vensim equation into N identical per-element equations (being filed as a separate issue). That expansion forces element-level rather than Bare-A2A reference sites in the LTM IR. Fixing the importer should collapse much of the element-graph back to variable level, attacking the slot count at the source.
    • (b) Variable-level discovery -- this issue's existing proposed scope.

    Either path reduces V; together they should get C-LEARN well under the u16 ceiling and into tractable discovery range.

    3. Wall-clock breakdown (release build)

    • LTM-discovery compile: 52.5 s
    • VM run: 2.8 s
    • (vs 0.6 s total without LTM)

    The compile cost now dominates and is driven by generating and compiling the ~150k link-score equation fragments -- another consequence of the element-level expansion, and another reason the granularity reduction matters beyond DFS feasibility.

    4. Discovery DFS budget is now enforced mid-step

    Commit 7a68bad makes the discovery DFS budget enforced mid-step. Before that fix, Model.analyze(timeout=30) on C-LEARN burned 9+ CPU-minutes without honoring the timeout, because the budget was only checked between timesteps and a single step's DFS never finished. This bounds the worst case (the timeout is now respected), but it does not make discovery produce useful results at this scale -- it just fails fast instead of hanging. The underlying granularity/pruning redesign in this issue is still required.

  4. bpowers commented on Jun 2, 2026

    @bpowers
    OwnerAuthor

    Fixed by 081e984 ("engine: make LTM strongest-path discovery feasible on large models"). Three amendments (zero-score-edge exclusion, per-SCC DFS restriction, a component-scaled per-node expansion cap replacing the paper's best_score pruning) make element-level discovery tractable; see the "Scalability Amendments (GH #647)" section of docs/design/ltm--loops-that-matter.md.

    Verified empirically on main (521fc37), release build: the full 251-step C-LEARN v77 discovery DFS completes in 0.07s and finds 153 loops (truncated: false), peak RSS ~390MB for the whole run — was >9.7 min for 4 steps / never completing for the full run.

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

    ltmLoops that Matter (LTM) analysis subsystem

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions