Skip to content

engine: correct LTM scoring and split wasm functions - #1063

Merged
bpowers merged 5 commits into
mainfrom
ltm-audit-wasm-function-splitting
Sep 11, 2026
Merged

bpowers merged 5 commits into
mainfrom
ltm-audit-wasm-function-splitting

Conversation

@bpowers

@bpowers bpowers commented Sep 10, 2026

Copy link
Copy Markdown
Owner

LTM attribution must preserve the temporal meaning of co-inputs, survive changes of units, and justify a dominant period against its full normalization mass. Large LTM simulations also need to compile successfully in JavaScript: C-LEARN currently generates a 9,471,955-byte function, which Node 24/V8 rejects.

This PR contains three implementation changes and a numerical-accuracy documentation clarification:

  • Freeze an independent PREVIOUS(...) co-input at its previous evaluated value, use exact zero-change score guards, and require the reported dominant set to reach 50% even when the display cap omits loops. Production-pipeline regressions and the LTM references describe these semantics.
  • Partition generated WASM phase programs at boundaries without live runtime evaluation state. Helpers preserve instruction order, module parameters, and shared memory; existing phase entry points retain their indices. The target body size is 64 KiB, with a hard check on every final function, including indivisible expressions and drivers. Array unrolling retains its separate compilation-work budget.
  • Reuse the project's incremental compiler database through the same special-stock dispatch as VM creation, release locks before WASM emission, restore per-call discovery mode, and latch requested LTM diagnostics. Query-execution tests establish reuse, including switching the LTM overlay off and on; edits and failures have separate isolation tests.

The measurements use system Node v24.18.0 / V8 13.6.233.17-node.50 on a Ryzen 9950X, pinned to CPUs 0–7, with shipping fat-LTO/wasm-opt artifacts. The public model.simulate({}, {engine: 'wasm', enableLtm}) call compiles an actual generated simulation module. Opening/parsing, library initialization, simulation execution, and extraction are outside the compilation timer. Fresh-project measurements reuse a warm Node process; component timings wrap the actual compiler export and generated Module/Instance construction, with uninstrumented controls.

Median compilation times in milliseconds (seven paired samples after two warmups):

Case Fresh before → after Unchanged third compile before → after Sampled edit + next compile before → after
World3 LTM on 26.70 → 26.50 24.95 → 1.58 36.25 → 10.78
C-LEARN LTM off 74.70 → 73.05 71.02 → 5.92 161.13 → 40.85
C-LEARN LTM on rejected → 921.25 unavailable → 75.74 unavailable → 249.64

The edit column uses three paired samples of an actual public applyPatch followed by simulate, changing World3's unit_population or C-LEARN's population_scenario constant and verifying the edited value by executing the new simulation. It includes patch validation/invalidation, excludes simulation/extraction/UI, and does not establish latency for every structural edit. Repeated compiles improve 15.8×/12.0× in the first two cases; fresh compilation remains similar.

The original fresh World3 LTM call spends 25.82 ms in the library compiler export, 0.60 ms in generated V8 Module construction, 0.04 ms instantiating, and 0.31 ms in JS/layout/copies. C-LEARN off spends 72.29/1.01/0.34/1.02 ms respectively. After reuse, the third World3 compile is 1.23/0.04/0.04/0.27 ms; C-LEARN off is 4.73/0.14/0.12/0.91 ms. The new successful C-LEARN LTM fresh compile is 906.15/5.67/0.40/9.58 ms, and its third compile is 66.25/1.50/0.42/7.34 ms. Component medians need not sum to median totals; instrumented/control fresh totals differ by about 1% or less.

C-LEARN LTM grows from 49 to 200 functions, with the largest body shrinking from 9,471,955 to 1,132,923 bytes; total artifact growth is only 3,665 bytes (0.028%). World3 LTM and C-LEARN off warm execution remain about 4.05 and 20.37 ms. C-LEARN LTM execution remains expensive: one instance's seven measured runs after ten warmups have median 1,834.63 ms (1,829.59–1,836.91 ms). An additional five-run diagnostic gives approximately 1.02 s in the VM versus 1.84 s in generated WASM. Regrouping the same instructions at known-safe boundaries into 1 MiB functions yields about 1.80 s and a byte-identical 69,061,144-byte full result slab; it does not remove the execution cost. This PR retains the 64 KiB target. There is no runnable unsplit Node baseline for C-LEARN LTM.

The compiler-export bucket includes V8 work performed inside execution of the library WASM. The separate Module bucket measures generated-model construction; these measurements do not isolate every V8 optimization phase. Reusing compiler queries does not cache WASM emission, and overriding a differing discovery setting can invalidate queries.

Validation:

  • Full required pre-commit checks, without bypassing hooks.
  • 390 WASM code-generation tests, including tiny forced-split matrices for Euler/RK2/RK4, all phases, nested module inputs and offsets, PREVIOUS/INIT snapshots, reset/resume, dynamic array subscripts/views, temporary storage, and queue/conveyor reconciliation.
  • Public FFI regressions measure actual Salsa query executions, execute pre/post-edit artifacts, and cover discovery/overlay flags plus failure restoration and diagnostic latches.
  • RUN_BENCH=1 BENCH_MODELS=clearn BENCH_LTM=on pnpm -C src/engine exec rstest run backend-bench reproduces the old V8 rejection and exercises acceptance plus public-series parity. BENCH_LTM=off|on|both also supports ordinary corpus comparisons.
  • Optimized Node runs compare all public series for World3 (260 variables), C-LEARN (3,607), and Theil (114 ordinary; 1,569 with module-local LTM instrumentation). Maximum VM/WASM absolute trajectory differences are 7.36e-14, 2.47e-9, and zero respectively; LTM off/on ordinary series are bit-identical in all three. World3's 343,256 exposed raw/relative score values meet the strict absolute 1e-6 check (maximum 3.38e-8).
  • C-LEARN's 300,698 normalized link-score values differ by at most 2.45e-9, with no score sign mismatches or nonfinite values. Its raw scores do not meet a universal absolute 1e-6 bound: 71 of 300,698 differ beyond it, reaching 0.00885 on a score near 392,389. Maximum abs(a-b)/max(1,abs(a),abs(b)) is 2.66e-8. A binary comparison reconstructs the original C-LEARN initials and flow bodies byte-for-byte by concatenating their 151 helper bodies; the other 47 original bodies and all non-function/code sections are identical. The existing approximate transcendental helpers and subtraction of nearly equal quantities can amplify trajectory differences in raw finite-difference ratios. No score tolerance was relaxed in production or existing tests.

This establishes the tested engine and Node/V8 behavior, not a browser interaction journey or fidelity of every LTM extension. The always-on design document records remaining requirements for complete explanations, numerical normalization, saved evidence, and resumable analysis; it does not enable LTM by default.

Freeze lagged co-inputs at their prior evaluation and use exact zero-delta
guards so attribution remains consistent across time and unit scales.
Require reported loops to reach half of their normalization mass before
labeling a dominant period, including when the report cap omits loops.

Add production-pipeline regressions and align the LTM references and
always-on design constraints with the audited semantics.
Large LTM models can exceed V8's per-function byte limit even when the
simulation is otherwise valid. Partition encoded phase programs at points
where runtime evaluation state is no longer live, preserving instruction
order, module parameters, and stable phase entry points. Check every final
body against the host limit, including indivisible expressions and drivers.

Exercise partitioning with small production-compiled fixtures across
integration methods, modules, snapshots, arrays, and special stocks, plus
an opt-in Node benchmark for C-LEARN with LTM enabled.
The project already maintains an incremental compiler database, but the
WASM compilation endpoint rebuilt it on every request. Compile through
the shared special-stock dispatch under the established lock order, then
emit WASM from the immutable compiled snapshot after releasing the locks.
Restore per-call discovery mode and latch LTM diagnostics like the VM path.

Verify actual query reuse through the public FFI, edit invalidation, old
artifact ownership, mode isolation on success and failure, and special-stock
warnings after LTM requests.
Per-helper accuracy checks do not bound composed finite-difference ratios
near cancellation. Describe the separate raw, normalized, sign, and nonfinite
comparisons required for LTM, and align the power lowering comment with its
negative-base and special-value handling.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-10T06:13:31.894220Z ed108f4 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@codecov

codecov Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.34884% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.23%. Comparing base (1996601) to head (f9b53a7).

Files with missing lines Patch % Lines
src/libsimlin/src/model.rs 86.11% 5 Missing ⚠️
src/simlin-engine/src/wasmgen/lower.rs 86.48% 5 Missing ⚠️
src/simlin-engine/src/wasmgen/split.rs 97.05% 3 Missing ⚠️
src/simlin-engine/src/ltm_augment.rs 96.29% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1063      +/-   ##
==========================================
+ Coverage   93.22%   93.23%   +0.01%     
==========================================
  Files         253      254       +1     
  Lines      153270   153508     +238     
==========================================
+ Hits       142889   143128     +239     
+ Misses      10381    10380       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@claude

claude Bot commented Sep 10, 2026

Copy link
Copy Markdown

Reviewed the LTM scoring corrections, the wasm function-splitting infrastructure, and the incremental-compile reuse in libsimlin. Traced the boundary conditions in wasmgen/split.rs::finish (target-vs-max sizing, dispatcher parameter forwarding, record_boundary's live-state exclusions including runtime_off_local/valid_local), the freeze-snapshot decision in wrap_non_matching_in_previous (which now wraps co-input PREVIOUS(g) as PREVIOUS(PREVIOUS(g)) so the changed-first partial holds it at the same t-2 value the anchor PREVIOUS(target) reads it at), and the dominance selection's removal of the all-loops-from-higher-polarity fallback. The guard change from (x = 0) to (ABS(x) <= 0) correctly uses Op2::Lte's raw comparison to test exact zero (avoiding approx_eq's epsilon/ULP tolerance that would otherwise trigger for subnormals), and NaN handling is unchanged. The subtree_has_live_shape change from strict > to >= is required for the freeze-snapshot check to see the direct-child occurrence at child_path(path, 0).

No blocking correctness issues identified in the diff.

Verdict: correct.

The zero-change guard lived in three hand-written copies, and the switch
from approximate `=` to exact `ABS(delta) <= 0` reached only two of them.
The black-box module transfer kept `(delta) = 0`, so a loop through a
pathway-less module whose deltas fall under f64::EPSILON scored 0 at
every step. Every generator now wraps its score in one builder,
`ltm_augment::link_score_guard`, so the spelling cannot drift again.

The first-step test had the same defect on the time axis:
`TIME = INITIAL_TIME` treated steps within f64::EPSILON of a zero start,
or within 4 ULPs of a large one, as the first evaluation. The VM and the
wasm backend start the clock at exactly the start time and only advance
it, so the guard is `TIME <= INITIAL_TIME`. Both regressions run through
the production pipeline; the black-box one also asserts which generator
produced the score, because a composite scores the same values.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N838Szayoh8oz3snEC9na2
@bpowers
bpowers merged commit 1566f49 into main Sep 11, 2026
18 checks passed
@bpowers
bpowers deleted the ltm-audit-wasm-function-splitting branch September 11, 2026 03:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant