Skip to content

perf(cache): warm no-op checks stop re-proving the cache (draft — review before merge) - #4101

Closed
hellovai wants to merge 1 commit into
hellovai/compiler-parallel-load-lspfrom
hellovai/warm-floor-cache
Closed

hellovai wants to merge 1 commit into
hellovai/compiler-parallel-load-lspfrom
hellovai/warm-floor-cache

Conversation

@hellovai

Copy link
Copy Markdown
Contributor

Draft on purpose — this touches cache-soundness invariants; walk through the risk section below before merging. Stacked on #4100.

The problem

A warm no-op baml check at 295k LOC cost 7.2s — identical to a full recompute — because the warm path re-proved its own cache validity from scratch every run: the serve-time throws gate re-parsed every file (via package_items) just to convert types for a comparison, and plan_reuse read+hashed+decoded 78MB of unit payloads just to extract the tiny throws fragments it seeds from. Profiled: 70% gate, 20% unit loads.

The fix (adversarially reviewed design)

The naive fix (compare throws in "fact space") was killed in adversarial review — it was either circular or unsound (alias-retarget hole). What's implemented is the reviewed survivor:

  1. No-delta gate skip. When the dirty partition finds zero changed/added/removed files, the gate is provably a tautology: the values it would compare are byte-for-byte what the manifest was stored with (verbatim persist; content-addressed units written before the manifest), and the solve+conversion are deterministic. Comparing a value to itself cannot demote anything. Every real edit still takes the full runtime-space gate — the corner the adversary showed the gate genuinely protects (body-only alias retargets) is untouched.
  2. Seeds from manifest, not units. ManifestFile carries a verbatim copy of each unit's throws-fragment blob; seeding reads that. A no-op invocation opens zero unit files. Units load (in parallel, order preserved) only when a delta means relink/re-emit will need them.
  3. Prime wave in the gate path so the package_items derivation (still needed on edits and for package-level diagnostics) is a parallel fold, not a serial parse.
  4. bex_cache: unit content-key is derived from the integrity digest entry-validation already computed — one hash pass per unit instead of two.

Measured

warm scenario before after
no-op check, 295k LOC 7.2 s 2.0 s (zero unit reads)
no-op check, 59k LOC 0.54 s 0.30 s
leaf-edit check ~unchanged emit-bound (it seeds the cache; the follow-up run/pack then hit in ~0.7 s at 295k LOC)

Risk analysis (what to scrutinize)

  • Where a bug would live: the no-delta detection (dirty == added == removed == ∅) reuses the same dirty-partition logic every warm compile already trusts; a bug there was already a soundness bug before this PR. The new failure surface is manifest-fragment drift from the unit copy — mitigated by copying from the same in-memory bytes at store time, and by re-pointing both verify oracles (full BAML_CACHE_VERIFY compare + always-on 1-in-32 sampled re-derive) at the manifest copy, i.e. exactly what is served.
  • Deliberate behavior change: a unit file deleted out from under a warm cache is not detected on a no-op run (nothing reads it); the next compile that needs it falls back to a byte-identical full compile. Slower once, never wrong.
  • Wire format: old manifests fail borsh decode → treated as absent → one full recompile re-seeds. No mixed-version reads (manifest key includes compiler fingerprint).

Verification run

  • 66 cache unit tests green (incl. the poison-tripwire tests, re-pointed at the manifest copy; the missing-unit degradation test now exercises the delta path where detection still lives).
  • BAML_CACHE_VERIFY=1 honest-recompile byte-compare: PASS on no-op and after-edit.
  • BAML_CACHE_SAMPLED_VERIFY=1 forced on 7 consecutive warm invocations across edits: PASS.
  • Diagnostics byte-identical to the pre-fix binary, cold and warm.

🤖 Generated with Claude Code

https://claude.ai/code/session_01V4oee6wrCHzXH6mM2h8eJo

@vercel

vercel Bot commented Jul 21, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Jul 22, 2026 3:39am
promptfiddle Ready Ready Preview, Comment Jul 22, 2026 3:39am
promptfiddle2 Ready Ready Preview, Comment Jul 22, 2026 3:39am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 31e2bbf2-764c-4317-9616-ddd8ae755348

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch hellovai/warm-floor-cache

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

⏭️ Performance benchmarks were skipped

Perf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to canary/main.

To run them on this PR, do any of the following, then push a commit (or re-run CI):

  • Add RUN_CODSPEED=1 to the PR description, or
  • Include run-perf or /perf in the PR title or any commit message.

@vercel
vercel Bot temporarily deployed to Preview – beps July 21, 2026 07:48 Inactive
@github-actions

github-actions Bot commented Jul 21, 2026 •

Copy link
Copy Markdown

Binary size checks passed

✅ 7 passed

Artifact Platform File Gzip Gated on Baseline Delta Status
✅ baml-cli Linux 🔒 25.6 MB 10.9 MB file 25.3 MB +347.1 KB (+1.4%) OK
✅ packed-program Linux 🔒 17.3 MB 7.1 MB file 17.0 MB +218.6 KB (+1.3%) OK
✅ baml-cli macOS 🔒 19.9 MB 9.5 MB file 19.6 MB +281.3 KB (+1.4%) OK
✅ packed-program macOS 🔒 13.5 MB 6.2 MB file 13.2 MB +231.5 KB (+1.8%) OK
✅ baml-cli Windows 🔒 21.4 MB 9.7 MB file 21.1 MB +285.7 KB (+1.4%) OK
✅ packed-program Windows 🔒 14.4 MB 6.3 MB file 14.2 MB +200.7 KB (+1.4%) OK
✅ bridge_wasm WASM 16.3 MB 🔒 4.4 MB gzip 4.4 MB +23.6 KB (+0.5%) OK

🔒 = the size this artifact is GATED on (ceiling + delta). Binaries gate on file size (installed binary); WASM gates on gzip (download size). The other size is shown for information only.


Generated by cargo size-gate · workflow run

…nifest fragments, not unit payloads

The warm path spent most of a no-op invocation re-proving its own
cache validity: the serve-time throws gate re-derived package_items
(re-parsing every file) to convert types for a comparison, and
plan_reuse read + hashed + decoded every clean unit payload just to
extract the tiny callable-throws fragments it seeds from. At a 295k
LOC project that was ~7.2s to conclude "nothing changed" — the same
wall time as recompiling from scratch.

- No-delta gate skip: when the dirty partition finds no changed,
  added, or removed files, the gate is provably a tautology — the
  seeds it would validate are byte-for-byte the values the manifest
  was stored with (store persists file_throw_facts verbatim; units
  are content-addressed and written before the manifest), and the
  solve + runtime conversion are deterministic pure functions of
  those inputs. Any real delta still takes the full gate.

- Manifest-resident fragments: ManifestFile now carries a verbatim
  copy of each unit's CallableThrowsFragment blob, and the
  callable_throws seeds project from that copy — so a no-delta
  invocation opens zero unit payloads. Units are loaded (now in
  parallel, manifest order preserved) only when a delta means
  something will be re-emitted or relinked. Both verify oracles (the
  BAML_CACHE_VERIFY full compare and the 1-in-32 sampled re-derive)
  now check the manifest copy — exactly the artifact that is served.

- The gate path (and every warm collection) primes per-file semantic
  indexes across workers first, so the package_items derivation the
  gate and package-level diagnostics demand is a parallel-fed fold
  instead of a serial parse of the project.

- bex_cache: unit_key is derived from the payload's integrity digest,
  which entry validation already computes — load_unit_shared now
  reuses that digest instead of hashing the payload a second time.

Behavior change, deliberate: a unit file deleted out from under a
warm cache is no longer detected on a no-op invocation (nothing reads
it); the next compile that needs it falls back to a byte-identical
full compile. Slower once, never wrong.

Old manifests (without the fragment field) fail borsh decode and are
treated as absent — one full recompile re-seeds them.

Measured (Apple Silicon): warm no-op check at 295k LOC 7.2s -> 2.0s
(zero unit reads, gate skipped); at 59k LOC 0.54s -> 0.30s. Warm
leaf-edit check is emit-bound (it seeds the cache for later run/pack
hits) and unchanged. Verified: 66 cache tests, BAML_CACHE_VERIFY
honest-recompile compare (no-op + after-edit), forced sampled verify
across 7 warm invocations, diagnostics byte-identity cold + warm.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01V4oee6wrCHzXH6mM2h8eJo
@hellovai
hellovai force-pushed the hellovai/compiler-parallel-load-lsp branch from c2da018 to f214619 Compare July 22, 2026 03:17
@hellovai
hellovai force-pushed the hellovai/warm-floor-cache branch from dc5828e to 5573885 Compare July 22, 2026 03:17
@vercel
vercel Bot temporarily deployed to Preview – beps July 22, 2026 03:18 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 July 22, 2026 03:26 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle July 22, 2026 03:39 Inactive
@hellovai

Copy link
Copy Markdown
Contributor Author

Consolidated into #4105 (the full stack, rebased onto current canary, all CI green there). This PR's commits are contained in #4105 verbatim — closing in favor of one merge.

@hellovai hellovai closed this Jul 23, 2026
meefs pushed a commit to meefs/baml that referenced this pull request Jul 23, 2026
…itor latency (BoundaryML#4105)

Consolidated stack (supersedes BoundaryML#4098, BoundaryML#4100, BoundaryML#4101 — their commits are
all contained here, rebased onto current canary). Seven commits,
reviewable independently, in dependency order.

## What this does

A ground-up performance pass on the compiler driven by head-to-head
benchmarking against tsgo (native TypeScript) and `go build` on
semantically identical generated corpora (1.5k–295k LOC), followed by
root-cause profiling of every gap found.

### 1. `perf(compiler2)`: kill the two cold-compile quadratics;
parallelize the cache-path check
- The package-wide Jinja type environment was rebuilt inside every
`check_file` call (O(N²)) — and twice per compile via `get_bytecode`'s
error gate. Now a memoized salsa query (`jinja_base_types`) + skipped
entirely for files with no prompts.
- `ProjectDatabase` handle clones deep-copied two N-entry PathBuf maps;
the parallel drivers clone ~1.55N handles per compile (quadratic CPU +
peak RSS). Now Arc-wrapped, O(1).
- `baml check`/`run` routed through a serial diagnostics collector while
the parallel one sat unreachable next to it. Now shared.
- The LSP proves the project error-free once (`get_bytecode_unchecked`),
and its diagnostics candidate now includes package-level diagnostics —
cross-file name conflicts previously **never reached the editor at
all**.

### 2. `feat(cli)`: `baml pack` uses the bytecode cache; `baml check`
seeds it
Pack was fully cold on every invocation (no cache participation at all);
check reused but never wrote, so check-only workflows never warmed. Now
one warm cache serves check → run → test → pack.

### 3–4. `build` + `fix(ci)`: hardware SHA-2 (`sha2/asm`, gated off
MSVC), doc-link fix
The warm path hashes ~hundreds of MB of unit payloads per run; it was on
the software SHA fallback.

### 5. `perf(compiler2)`: prime wave, bulk project load, parallel LSP
sweep
Cold parallel check scaled 1.18× on 12 cores: every file's check demands
whole-package aggregates, and the first worker to claim that memo
computed parse+index for the entire project inline while all other
workers parked on the sync slot (diagnosed against ty/ruff's salsa
usage; interventionally verified). Per-file indexes are now primed
across workers before fan-out. Project load stops re-writing the file
list per file (O(files²)) and reads sources in parallel (FileId order
preserved). The LSP candidate uses the shared order-preserving parallel
checker.

### 6. `perf(cache)`: warm no-op checks stop re-proving the cache
A warm no-op check cost the same as a full recompute: the throws-gate
safety compare re-parsed every file, and seeding read+hashed 78MB of
unit payloads for metadata. Now: the gate is skipped when the dirty
partition is provably empty (a value compared against itself), throws
fragments live verbatim in the manifest so a no-op check opens **zero**
unit payloads, and units load in parallel (manifest order preserved)
only when a delta means relink will need them. Both verify oracles
(`BAML_CACHE_VERIFY` full compare + always-on 1-in-32 sampled re-derive)
now check the manifest copy — exactly what is served. Design was
adversarially reviewed; the unsound variant (fact-space throws compare)
was rejected and every real edit still takes the full runtime-space
gate.

### 7. `refactor(cli)`: one shared `ProjectSession` for every
project-loading command
check/run/test/pack/describe/grep/generate each hand-rolled a drifting
subset of the same preamble — the root cause of "pack forgot the cache"
and "describe never primes." One implementation; commands declare
`CacheUse::{ReadWrite, ReadWriteTests, ReadOnly, Off}`. Introspection
installs seeds only on a no-delta plan (provable tautology) and derives
honestly otherwise.

## Measured impact (Apple Silicon, 12 cores, hyperfine)

| | before | after |
|---|--:|--:|
| cold check, 295k LOC | 21–46 s (126 s CPU, 5.3 GB RSS) | **6.6 s**
(~13 s CPU, 2.7 GB) |
| cold check, 59k LOC | 3.3 s | **1.24 s** |
| warm no-op check, 295k / 59k | 7.2 s / 3.2 s | **2.0–2.6 s / 0.4–0.6
s** |
| `run` after check (59k / 295k) | full compile | **0.12 s / 0.65 s**
(cache hit) |
| `pack` after check (59k / 295k) | 1.7 s / 40.5 s (always cold) |
**0.19 s / 0.87 s** (cache hit) |
| LSP edit→diagnostics (250/500/1000 files) | 0.46 / 1.7 / 5.5 s | **179
/ 192 / 216 ms** (flat) |
| `describe`, 295k / 59k LOC | 6.3 s / 716 ms | **2.6 s / 402 ms** |

Cold compile scaling is now ~linear in project size (was quadratic: 5×
code cost 33× CPU), verified to 8000 modules.

## Verification

- Diagnostics byte-identical to the pre-change binary (error-heavy
corpus, cold + warm) at each stage.
- Cache soundness: `BAML_CACHE_VERIFY=1` honest-recompile compare (no-op
+ after-edit) and forced `BAML_CACHE_SAMPLED_VERIFY=1` across edit
sequences — all pass; 43 bytecode-cache oracle tests updated and green.
- LSP load-tested: 50-edit multi-file burst storms + sustained ~17
edits/s churn at 250/1000 files — zero crashes, stable RSS, convergence
after every storm.
- Full stack green in CI (doc, Windows incl. MSVC sha2 gating, size
gate, all SDK matrices).

## Deliberate trade-offs

- Cold `check` at mid-size projects (~15–30k LOC) is ~10% slower than
before: it now emits + stores once to warm the cache; every later
check/run/pack is served warm.
- A warm *edit* check re-emits and stores (that is what buys the instant
run/pack); at 295k LOC the edit path remains ~6–9 s — the known
remaining floor, tracked separately.
- A unit file deleted out from under a warm cache is not detected on a
no-op run; the next compile that needs it falls back to a byte-identical
full compile.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01V4oee6wrCHzXH6mM2h8eJo

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* CLI commands now share a consistent, cache-aware project session
workflow, including lenient read-only sessions for inspection when
configuration is missing.
* **Performance**
* Faster project loading and diagnostics checking via parallel per-file
processing.
* Improved bytecode-cache reuse by avoiding unnecessary payload
hashing/loading, including a fast “no-delta” warm path.
* **Bug Fixes**
* Fixed callable-throws fragment seeding and verification for warm
“no-delta” runs.
* Improved package-level diagnostics handling during compilation and
error-gating behavior.
* **Chores**
* Improved build robustness for cryptography acceleration on MSVC
targets.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Fable 5 <[email protected]>

This branch was previously deployed

3 inactive deployments
Preview – promptfiddle — 5573885d Deployed Jul 22, 2026 by vercel[bot]
Preview – promptfiddle2 — 5573885d Deployed Jul 22, 2026 by vercel[bot]
Preview – beps — 5573885d Deployed Jul 22, 2026 by vercel[bot]
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