build: cut the prepass peak RSS 7.71 GB → 4.25 GB; make on-demand body lowering possible - #835
Conversation
`GraphStore` stored `HashMap<GraphKey, FunctionGraph>`; it now stores
`GraphSlot { graph, signature }`, where `signature` is derived from the
graph's startblock inputargs when the key is first inserted. An alias
registration of an already-stored key folds effects / hints / return
type onto the shared graph as before and leaves the signature alone,
which is the same value its consumers read off that shared graph today.
`signature_for_graph` in `translator/rtyper/cutover.rs` is removed. Its
four callers — the stale-graph re-lift at `cutover.rs:447`, Pass 1's
`get_or_register`, and Pass 2's residualize-stub and full-lift arms —
read `GraphStore::signature` instead; Pass 2's `pending` rows carry the
borrowed signature from Pass 1.
`819 all_jitcodes (555669 bytes bincode), generated 25254 bytes`
unchanged; `check.py --backend dynasm,cranelift,wasm` 331/331, 331/331,
328/328.
Assisted-by: Claude
…ected `FunDecl::body` is retained as raw JSON and `FunDecl::unstructured` re-parses it on each call. The whole-program loop called it twice per decl: once at the top as a "has a lowerable body" gate, discarding the projection, and again inside `lower_fun_decl_with_static_addrs_and_attrs`. The gate now binds the projection and passes it to a new `lower_unstructured_with_static_addrs_and_attrs`, which holds the rest of the lowering; `lower_fun_decl_with_static_addrs_and_attrs` keeps its signature and delegates after parsing, so the reader/test callers are unchanged. One full-corpus `unstructured()` pass over the three extracted artefacts (23137 bodies, 406388 blocks) measures 4.20s at the build script's `opt-level = 1`. `819 all_jitcodes (555669 bytes bincode), generated 25254 bytes` unchanged; `check.py --backend dynasm,cranelift,wasm` 331/331, 331/331, 328/328. Assisted-by: Claude
`translator.py:55 buildflowgraph(func)` builds a function's flow graph from the function object when a consumer first asks for it; pyre's funcobjs are Charon `FunDecl`s, so the analogue is lowering the decl from the LLBC it came from. That needs the LLBC set to outlive the whole-program build, which nothing currently keeps alive. `front::graph_body::GraphBodyProvider` owns the `Vec<Llbc>`, an owned copy of the three `HostStaticAddrs` tables (the borrowed view is rebuilt per call, which only a demanded body pays for), and a per-LLBC `struct_field_attrs` map recovered through the new `mir::struct_field_attrs_of` — `derive_program_metadata` is a pure function of the LLBC, and the map is computed on the first body demanded from each LLBC rather than for every LLBC up front. `mir::lower_fun_decl_with_static_addrs_and_attrs` becomes `pub(crate)`. A `GraphBodySource` is `(llbc index, Charon def_id)`, resolved through `Llbc::fn_by_id`. The test lowers every body in `charon-corpus/corpus.ullbc` through both the whole-program entry point and the provider and compares their graph shapes. Comparison is on a rendering whose variable ids are renumbered in first-seen order: `NEXT_VAR_ID` is process-global, so two lowerings of one funcobj never carry equal ids, while the aliasing the renumbering preserves is what the codewriter reads. No production call site yet. `819 all_jitcodes (555669 bytes bincode), generated 25254 bytes` unchanged; `check.py --backend dynasm,cranelift,wasm` 331/331, 331/331, 328/328. Assisted-by: Claude
`Lowering` kept three tables holding one slot-indexed row per MIR block: `block_entry_local_var`, and the RPO walk's `entry_state` / `exit_state`, whose `FrameState` carries `entries` and `locals_w` sized by the body's local count. Each is `blocks * locals` cells regardless of how many slots are bound. `pyre_jit::jit::codewriter::<Impl>::transform_graph_to_jitcode` has 15335 MIR blocks and 14215 locals, and lowering it accounted for 4.05 GB of the prepass RSS on its own — the next-largest body, `pyre_interpreter::module::_pickle::unpickler::dispatch` (3061 blocks, 2121 locals), accounted for 0.15 GB. Its lowered graph carries 509162 block inputargs across 15403 blocks, i.e. ~33 bound slots per 14215-slot row: the dense rows are 0.2% occupied. `PackedLocalRow` and `PackedFrameState` hold the bound slots and the row length; a row is rebuilt dense at each use, one at a time, so `Lowering::local_var` and every `FrameState` reaching `union` / `getvariables` / `getoutputargs` keep the shape they have today. `crate::model::FrameState` is unchanged. Pipeline peak RSS 7.71 GB -> 4.25 GB; total prepass 63.63s -> 63.68s. `819 all_jitcodes (555669 bytes bincode), generated 25254 bytes` unchanged; `check.py --backend dynasm,cranelift,wasm` 333/333, 333/333, 330/330. Assisted-by: Claude
|
Warning Review limit reached
Next review available in: 31 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
🤖 Codex parity reviewStatic analysis of this diff vs the local RPython/PyPy sources (commit 2c6dfa2). Files in the reviewed diff1. Regressions to PyPy parity introduced by this patchNone. 2. Other mismatches introduced by this patch
3. Pre-existing mismatches (already present before this patch)
4. Structural adaptations
|
What this does
Four commits on the
pyre-jit-tracebuild prepass. The headline is the lastone: pipeline peak RSS 7.71 GB → 4.25 GB, total prepass time unchanged
(63.63s → 63.68s).
front/mir: store the per-block local rows by bound slotLoweringkept three tables holding one slot-indexed row per MIR block —block_entry_local_var, and the RPO walk'sentry_state/exit_state, whoseFrameStatecarriesentriesandlocals_wsized by the body's local count.Each costs
blocks * localscells no matter how many slots are bound.pyre_jit::jit::codewriter::<Impl>::transform_graph_to_jitcodehas 15335 MIRblocks and 14215 locals, and lowering it accounted for 4.05 GB of the prepass
RSS on its own. The next-largest body,
pyre_interpreter::module::_pickle::unpickler::dispatch(3061 blocks, 2121locals), accounted for 0.15 GB — a fifth the size, a twenty-seventh the memory.
Its lowered graph carries 509162 block inputargs across 15403 blocks: ~33 bound
slots per 14215-slot row, so the dense rows are 0.2% occupied.
PackedLocalRow/PackedFrameStatestore the bound slots plus the row length,and a row is rebuilt dense at each use, one at a time.
Lowering::local_varandevery
FrameStatereachingunion/getvariables/getoutputargskeep theshape they have today, and
crate::model::FrameStateis untouched — samevalues, same algorithm, different storage.
RPython's flowspace also gives every block its inputargs from the frame state
(
framestate.py:19 locals_w), so the algorithm is unchanged here. What does notcarry over is the input scale: a Python frame has tens of
co_varnames, thisRust MIR body has 14215 locals.
front: add a GraphBodyProvider that lowers a FunDecl on demandtranslator.py:55 buildflowgraph(func)builds a function's flow graph when aconsumer first asks; pyre's funcobjs are Charon
FunDecls, so the analogue islowering the decl from its LLBC. That needs the LLBC set to outlive the
whole-program build, which nothing kept alive.
The provider owns the
Vec<Llbc>, an owned copy of the threeHostStaticAddrstables, and a per-LLBC
struct_field_attrsmap recovered lazily through the newmir::struct_field_attrs_of. AGraphBodySourceis(llbc index, Charon def_id), resolved viaLlbc::fn_by_id.The test lowers every body in
charon-corpus/corpus.ullbcthrough both thewhole-program entry point and the provider and compares graph shapes, with
variable ids renumbered in first-seen order —
NEXT_VAR_IDis process-global,so two lowerings of one funcobj never carry equal ids, while the aliasing the
renumbering preserves is what the codewriter reads. No production call site yet.
front/mir: lower the whole-program loop from the body it already projectedFunDecl::bodyis raw JSON andunstructured()re-parses it per call. The loopcalled it twice per decl: once as a "has a lowerable body" gate, discarding the
projection, then again inside the lowering. One full-corpus
unstructured()pass over the three artefacts (23137 bodies, 406388 blocks) measures 4.20s at
the build script's
opt-level = 1.majit-translate: keep the funcobj Signature on the GraphStore slotGraphStorenow storesGraphSlot { graph, signature }, the signature derivedfrom the startblock inputargs when the key is first inserted.
signature_for_graphis removed and its four callers read the slot, soregistration no longer walks a graph body for parameter names.
Verification
Every commit holds
819 all_jitcodes (555669 bytes bincode), generated 25254 bytesand passescheck.py --backend dynasm,cranelift,wasm(333/333, 333/333,330/330 at the tip).
majit-translate's suite is green excepttest_rbigint_mir::dependent_crate_rbigint_identity_retargets_opaque_llbc_declaration,which is red on the base too: it asserts
harvest_hints_from_llbcsreportselidable_or_memerrorforjit_bigint_div/jit_bigint_rem, butbuild/llbc/pyre-interpreter.ullbcwas extracted 2026-07-25 21:43 and theattribute landed in 7b70e1e at 22:02 — a stale fixture, not a regression from
this branch.
Note on scope
This branch started out building toward demand-driven body lowering (the first
three commits are that plumbing). Measuring the premise first would have been
the better order: an env-gated probe that drops every lowered body immediately
moves the phase from 7.67 GB to 6.34 GB, so retaining all 14998 graphs is worth
only 1.33 GB — the whole ceiling of that axis. Skipping lowering entirely lands
at 2.10 GB, and
mi_collect(true)before each reading changes nothing, which iswhat pointed at the dense tables above.
🤖 Generated with Claude Code