Skip to content

jit(fbw): latch a multi-frame blackhole image on ABORT_TOO_LONG - #910

Merged
youknowone merged 2 commits into
mainfrom
trace-too-long-multiframe
Jul 31, 2026
Merged

jit(fbw): latch a multi-frame blackhole image on ABORT_TOO_LONG#910
youknowone merged 2 commits into
mainfrom
trace-too-long-multiframe

Conversation

@youknowone

@youknowone youknowone commented Jul 30, 2026

Copy link
Copy Markdown
Owner

jit(fbw): latch a multi-frame blackhole image on ABORT_TOO_LONG

latch_trace_too_long_blackhole previously returned false for an inlined
sub-walk, so an ABORT_TOO_LONG inside an inlined callee fell back to entry
replay. It now builds the multi-frame framestack and latches it behind
multi_frame_blackhole_preflight, a read-only re-check of every adopter gate
that can later reject the image — the abort runs after the opcode's effects, so
publishing is only safe when the handoff cannot decline afterwards.

LatchedMultiFrameBlackhole gains publish_root_stack. ABORT_TOO_LONG stops
at an arbitrary post-step coordinate, so frame 0's active operand stack has to
cross from the detached tracing snapshot to the live red frame before the
blackhole runs; the vable-force path keeps its existing handoff and passes
false.

try_adopt_multi_frame_blackhole roots every MIFrame ref bank and the pending
exception across root-locals boxing and copies the forwarding updates back
before the banks are read, and it pins the canonical PyFrame virtualizable
info for the whole drive instead of whichever translator-state vinfo happens to
remain installed on TraceCtx.

Standard-vable writes inside an inlined MIFrame are mirrored onto that frame's
own concrete red PyFrame (current_inline_concrete_frame,
store_live_frame_static_int), so an inner level no longer depends on a
per-level slot side-table to be resumable.

Adds synth/trace_too_long_inline_multiframe, which drives ABORT_TOO_LONG
inside an inlined callee that mutates three containers and, in a second part,
unwinds instead of returning.

Rebased onto main after #874 landed; it applies with no conflict and nothing
here touches the files #874 rewrote in a conflicting way.

interp: drop the duplicate interp_return_log_enabled definition

Unrelated to the work above — origin/main currently does not compile:

error[E0428]: the name `interp_return_log_enabled` is defined multiple times
  --> pyre/pyre-interpreter/src/eval.rs:617:1

#874 and #907 each added fn interp_return_log_enabled to
pyre-interpreter/src/eval.rs. Each PR built against its own merge base, so
neither run saw the other's copy; landing both left the name defined twice. The
two bodies are identical (OnceLock over PYRE_INTERP_RETURN_LOG, both
#[cfg(not(feature = "sandbox"))]), so this removes the second and keeps the
first, whose doc names the caller on the return path.

Happy to split this into its own PR if you would rather land it independently —
it is here only because it blocks this branch from building at all.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The change adds inline live-frame synchronization, validates and publishes multi-frame ABORT_TOO_LONG blackhole state, tightens blackhole adoption metadata handling, adds execution benchmarks, and separates GC descriptor lookup from census accounting.

Changes

Inline multi-frame blackhole handling

Layer / File(s) Summary
Inline live-frame synchronization
pyre/pyre-jit-trace/src/jitcode_dispatch/{residual_call,vable_ops}.rs, pyre/pyre-jit-trace/src/state.rs
Vable field and array writes now update matching inline live-frame state through newly accessible storage helpers.
Trace-too-long blackhole latching
pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
Inline subwalk trace-too-long handling builds a multi-frame image, runs invariant checks, and records whether the root stack should be published.
Canonical blackhole adoption and publication
pyre/pyre-jit-trace/src/trace.rs, pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
Adoption uses canonical virtualizable metadata, roots reconstructed references and stacks, publishes forwarded values, and restores state when stack publication declines.
Inline subwalk benchmark coverage
pyre/bench/synth/*
Benchmarks exercise normal returns and exception unwinding across inlined multi-frame trace-too-long execution.

GC descriptor index validation

Layer / File(s) Summary
Field-position lookup and cache validation
majit/majit-ir/src/descr.rs
Parent-field lookup results are classified separately from census accounting, and cached field indices are checked against the parent descriptor when available.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TraceLoop
  participant latch_trace_too_long_blackhole
  participant multi_frame_blackhole_preflight
  participant try_adopt_multi_frame_blackhole
  participant publish_captured_frame_stack
  TraceLoop->>latch_trace_too_long_blackhole: encounter ABORT_TOO_LONG in inline_subwalk
  latch_trace_too_long_blackhole->>multi_frame_blackhole_preflight: validate multi-frame image
  multi_frame_blackhole_preflight-->>latch_trace_too_long_blackhole: accept or decline
  latch_trace_too_long_blackhole-->>try_adopt_multi_frame_blackhole: provide latched image
  try_adopt_multi_frame_blackhole->>publish_captured_frame_stack: publish captured root stack
  publish_captured_frame_stack-->>try_adopt_multi_frame_blackhole: success or decline
Loading

Possibly related PRs

  • youknowone/pyre#901: Modifies related inline-subwalk and multi-frame blackhole latch/adoption paths.
  • youknowone/pyre#904: Changes the same GC descriptor index derivation and cache-validation code.
  • youknowone/pyre#908: Changes related multi-frame blackhole adoption and frame-local publication logic.

Poem

I’m a rabbit tracing frames in flight,
Rooting each stack by lantern-light.
Vable writes hop, exceptions run,
Blackholes publish when checks are done.
Descriptors count with order bright—
A tidy burrow, working right!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main change: latching a multi-frame blackhole image for ABORT_TOO_LONG handling.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch trace-too-long-multiframe
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch trace-too-long-multiframe

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1c446f5a3a

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2643 to +2645
for (&(frame_index, reg_index), &forwarded) in image_ref_locations.iter().zip(&image_ref_roots)
{
latched.framestack.frames[frame_index].ref_values[reg_index] = Some(forwarded);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Refresh adopter frame pointers after GC forwarding

When write_back_outer_locals boxes an Int/Float local, it can collect and forward a nursery-allocated root PyFrame. This loop updates only the MIFrame ref banks; the previously captured root_addr and per_frame still contain the old addresses and are subsequently passed to publish_captured_frame_stack and drive_multi_frame_blackhole. In that case the new trace-too-long path dereferences an evacuated frame, risking a crash or corrupt resume state; rebuild those raw pointers from the forwarded banks before publishing or driving.

AGENTS.md reference: AGENTS.md:L32-L42

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit 4679e1e).
Updated: 2026-07-30T18:01:41.085Z

Files in the reviewed diff
pyre/bench/synth/getframe_inline_subwalk_multiframe.py
pyre/bench/synth/trace_too_long_inline_multiframe.py
pyre/pyre-interpreter/src/eval.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/vable_ops.rs
pyre/pyre-jit-trace/src/state.rs
pyre/pyre-jit-trace/src/trace.rs

Codex did not produce a report (exit 1). Last log lines:

authoritative definition of "this patch" (when an authoritative changed-file
list is appended below, use that instead of re-deriving it). Findings under
sections 1 and 2 MUST cite our-side files from that list; a divergence in any
file NOT in the list is by definition not introduced by this patch — report
it under section 3 instead, or omit it. Verify every section-1/2 citation
against the list before finalizing the report.

---

Output format requirements (so the report can be parsed mechanically and
posted/triaged automatically). Use these four headings VERBATIM, in this
order, and nothing else at heading level 2:

## 1. Regressions to PyPy parity introduced by this patch
## 2. Other mismatches introduced by this patch
## 3. Pre-existing mismatches (already present before this patch)
## 4. Structural adaptations

Under each heading, list every finding as a bullet. For each finding cite the
concrete `our_file.rs:line ↔ rpython_or_pypy_file.py:line` pair and quote the
divergence concisely. If a section has no findings, still emit the heading
followed by a single line `None.` so all four sections are always present.
Do not modify any files; produce the report only.

Authoritative changed-file list for this patch (git diff upstream/main --name-only):
pyre/bench/synth/getframe_inline_subwalk_multiframe.py
pyre/bench/synth/trace_too_long_inline_multiframe.py
pyre/pyre-interpreter/src/eval.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
pyre/pyre-jit-trace/src/jitcode_dispatch/vable_ops.rs
pyre/pyre-jit-trace/src/state.rs
pyre/pyre-jit-trace/src/trace.rs
warning: Codex could not find bubblewrap on PATH. Install bubblewrap with your OS package manager. See the sandbox prerequisites: https://developers.openai.com/codex/concepts/sandboxing#prerequisites. Codex will use the bundled bubblewrap in the meantime.
2026-07-30T18:01:39.148679Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:01:39.171698Z ERROR rmcp::transport::worker: worker quit with fatal: Transport channel closed, when UnexpectedServerResponse("HTTP 401: {\n  \"error\": {\n    \"message\": \"Provided authentication token is expired. Please try signing in again.\",\n    \"type\": null,\n    \"code\": \"token_expired\",\n    \"param\": null\n  },\n  \"status\": 401,\n  \"detail\": {\n    \"code\": \"token_expired\",\n    \"message\": \"Provided authentication token is expired. Please try signing in again.\"\n  }\n}")
2026-07-30T18:01:39.192104Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:01:39.253593Z ERROR rmcp::transport::worker: worker quit with fatal: Transport channel closed, when UnexpectedServerResponse("HTTP 401: {\n  \"error\": {\n    \"message\": \"Provided authentication token is expired. Please try signing in again.\",\n    \"type\": null,\n    \"code\": \"token_expired\",\n    \"param\": null\n  },\n  \"status\": 401,\n  \"detail\": {\n    \"code\": \"token_expired\",\n    \"message\": \"Provided authentication token is expired. Please try signing in again.\"\n  }\n}")
2026-07-30T18:01:39.288303Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:01:39.304708Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:01:39.328295Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:01:39.442405Z ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 401 Unauthorized, url: wss://chatgpt.com/backend-api/codex/responses
2026-07-30T18:01:39.475675Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:01:39.490842Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:01:39.506313Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:01:39.628544Z ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 401 Unauthorized, url: wss://chatgpt.com/backend-api/codex/responses
2026-07-30T18:01:39.647549Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:01:39.730213Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:01:39.823362Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:01:39.896903Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:01:39.987640Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:01:40.005074Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:01:40.022184Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:01:40.126067Z ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 401 Unauthorized, url: wss://chatgpt.com/backend-api/codex/responses
2026-07-30T18:01:40.158784Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:01:40.174552Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:01:40.190855Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:01:40.828581Z ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 401 Unauthorized, url: wss://chatgpt.com/backend-api/codex/responses
ERROR: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
ERROR: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.
2026-07-30T18:01:40.848503Z ERROR codex_login::auth::manager: Failed to refresh token: Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pyre/pyre-jit-trace/src/trace.rs (1)

2395-2513: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Missing fail-fast invariant for a preflighted trace-too-long multi-frame image, unlike the single-frame path.

try_adopt_single_frame_blackhole asserts !trace_too_long on every decline path, because a preflighted ABORT_TOO_LONG image (is_authoritative_executor already executed real residual-call side effects) must never fall back to legacy replay — that would double those effects, exactly the hazard this PR's comments describe at length. try_adopt_multi_frame_blackhole has no equivalent invariant anywhere in its decline paths (virtualizable_info missing, jitcode index out of range, unstamped/null frame register, no concrete nlocals, identity-collapse guard, root-identity gate). multi_frame_blackhole_preflight in residual_call.rs was written precisely to guarantee these gates all pass before latching, but nothing here converts a preflight/adopt mismatch into a loud failure — it just declines via a debug-gated mfdbg! and silently falls back to replay.

🛡️ Proposed fix: assert non-decline for a preflighted trace-too-long image
+    let trace_too_long = commit_leg == WalkEndCommitLeg::TraceTooLong
+        && crate::jitcode_dispatch::fbw_executed_effect_count() != 0;
     let Some(mut latched) = crate::jitcode_dispatch::take_multi_frame_blackhole() else {
         return false;
     };
     let depth = latched.framestack.len();
     if ctx.virtualizable_info().is_none() {
         mfdbg!("no virtualizable_info");
+        assert!(!trace_too_long, "preflighted trace-too-long virtualizable info disappeared");
         return false;
     }

(repeat for the remaining decline sites in this range, mirroring try_adopt_single_frame_blackhole's pattern)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-jit-trace/src/trace.rs` around lines 2395 - 2513, Add the same
fail-fast invariant used by try_adopt_single_frame_blackhole to every decline
path in try_adopt_multi_frame_blackhole: when the latched image is
trace-too-long and was preflighted, assert that adoption cannot decline before
returning false. Cover virtualizable_info, frame recovery, concrete nlocals,
identity-collapse, and root-identity gates, while preserving normal fallback
behavior for non-preflighted images.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pyre/pyre-jit-trace/src/trace.rs`:
- Around line 2643-2665: After copying forwarded roots into
latched.framestack.frames in the multi-frame path, re-derive each frame’s
current address from its updated ref_values before calling
publish_captured_frame_stack or releasing roots. Update root_addr and the
per-frame addresses using the same committed_root_addr approach as the
single-frame path, ensuring restore and publication use relocated memory.

---

Outside diff comments:
In `@pyre/pyre-jit-trace/src/trace.rs`:
- Around line 2395-2513: Add the same fail-fast invariant used by
try_adopt_single_frame_blackhole to every decline path in
try_adopt_multi_frame_blackhole: when the latched image is trace-too-long and
was preflighted, assert that adoption cannot decline before returning false.
Cover virtualizable_info, frame recovery, concrete nlocals, identity-collapse,
and root-identity gates, while preserving normal fallback behavior for
non-preflighted images.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b7a4572d-6891-4bb9-bb76-987860e0c71b

📥 Commits

Reviewing files that changed from the base of the PR and between 0cda9d4 and 9de1bf3.

📒 Files selected for processing (7)
  • majit/majit-ir/src/descr.rs
  • pyre/bench/synth/getframe_inline_subwalk_multiframe.py
  • pyre/bench/synth/trace_too_long_inline_multiframe.py
  • pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs
  • pyre/pyre-jit-trace/src/jitcode_dispatch/vable_ops.rs
  • pyre/pyre-jit-trace/src/state.rs
  • pyre/pyre-jit-trace/src/trace.rs

Comment thread pyre/pyre-jit-trace/src/trace.rs
`latch_trace_too_long_blackhole` previously returned false for an inlined
sub-walk, so an ABORT_TOO_LONG inside an inlined callee fell back to entry
replay. It now builds the multi-frame framestack and latches it behind
`multi_frame_blackhole_preflight`, a read-only re-check of every adopter gate
that can later reject the image -- the abort runs after the opcode's effects,
so publishing is only safe when the handoff cannot decline afterwards.

`LatchedMultiFrameBlackhole` gains `publish_root_stack`. ABORT_TOO_LONG stops
at an arbitrary post-step coordinate, so frame 0's active operand stack has to
cross from the detached tracing snapshot to the live red frame before the
blackhole runs; the vable-force path keeps its existing handoff and passes
false.

`try_adopt_multi_frame_blackhole` roots every MIFrame ref bank and the pending
exception across root-locals boxing and copies the forwarding updates back
before the banks are read, and it pins the canonical `PyFrame` virtualizable
info for the whole drive instead of whichever translator-state vinfo happens to
remain installed on `TraceCtx`.

Standard-vable writes inside an inlined MIFrame are mirrored onto that frame's
own concrete red `PyFrame` (`current_inline_concrete_frame`,
`store_live_frame_static_int`), so an inner level no longer depends on a
per-level slot side-table to be resumable.

Adds `synth/trace_too_long_inline_multiframe`, which drives ABORT_TOO_LONG
inside an inlined callee that mutates three containers and, in a second part,
unwinds instead of returning.

Assisted-by: Claude
#874 and #907 each added `fn interp_return_log_enabled` to
`pyre-interpreter/src/eval.rs`. Each PR built against its own merge base, so
neither run saw the other's copy; landing both left the name defined twice and
`origin/main` fails to compile with E0428.

The two bodies are identical (`OnceLock` over `PYRE_INTERP_RETURN_LOG`, both
`#[cfg(not(feature = "sandbox"))]`), so this removes the second and keeps the
first, whose doc names the caller on the return path.

Assisted-by: Claude
@youknowone
youknowone force-pushed the trace-too-long-multiframe branch from 9de1bf3 to 4679e1e Compare July 30, 2026 17:21
@youknowone
youknowone merged commit d8a5ad2 into main Jul 31, 2026
19 checks passed
@youknowone
youknowone deleted the trace-too-long-multiframe branch July 31, 2026 00:57
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