Title: JIT can trigger assert(uop_buffer_remaining_space(trace) > space_needed); for widened uops · Issue #149335 · python/cpython · GitHub
Open Graph Title: JIT can trigger assert(uop_buffer_remaining_space(trace) > space_needed); for widened uops · Issue #149335 · python/cpython
X Title: JIT can trigger assert(uop_buffer_remaining_space(trace) > space_needed); for widened uops · Issue #149335 · python/cpython
Description: Bug report Bug description: The assert(uop_buffer_remaining_space(trace) > space_needed); in _PyJit_translate_single_bytecode_to_trace can be triggered if a uop is widened. Found when the CI for #148271 was failing (there TO_BOOL gets an...
Open Graph Description: Bug report Bug description: The assert(uop_buffer_remaining_space(trace) > space_needed); in _PyJit_translate_single_bytecode_to_trace can be triggered if a uop is widened. Found when the CI for #1...
X Description: Bug report Bug description: The assert(uop_buffer_remaining_space(trace) > space_needed); in _PyJit_translate_single_bytecode_to_trace can be triggered if a uop is widened. Found when the CI for...
Opengraph URL: https://github.com/python/cpython/issues/149335
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"JIT can trigger assert(uop_buffer_remaining_space(trace) \u003e space_needed); for widened uops","articleBody":"# Bug report\n\n### Bug description:\n\n\nThe `assert(uop_buffer_remaining_space(trace) \u003e space_needed);` in `_PyJit_translate_single_bytecode_to_trace` can be triggered if a uop is widened. Found when the CI for #148271 was failing (there `TO_BOOL` gets an extra `_RECORD_TOS_TYPE`). @markshannon \n\nReporting the issue already, I will try to create a simple reproducer and more concise explanation.\n\n\u003cdetails\u003e\u003csummary\u003eFull explanation by Claude\u003c/summary\u003e\n\n# JIT trace-buffer assertion: full explanation\n\n## The assertion\n\nIn `_PyJit_translate_single_bytecode_to_trace` (Python/optimizer.c), each bytecode appended to a tier-2 trace is gated by a Py_DEBUG assertion:\n\n```\nassert(uop_buffer_remaining_space(trace) \u003e space_needed);\n```\nwhere\n```\nspace_needed = expansion-\u003enuops + needs_guard_ip + 2 + (!OPCODE_HAS_NO_SAVE_IP(opcode))\n```\n\n`expansion-\u003enuops` is the number of uops the bytecode's macro expands to. The `+ 2` covers the `_CHECK_VALIDITY` uop emitted at the start of every bytecode plus a reserved `_DEOPT` slot for it. `+ needs_guard_ip` covers the `_GUARD_IP_*` slot for opcodes that need it. `+ (!OPCODE_HAS_NO_SAVE_IP)` covers the `_SET_IP` slot.\n\nJust before the assertion, `trace-\u003eend` has already been decremented to reserve the side-exit/error/deopt/guard-ip stubs at the buffer's tail:\n\n```\ntrace-\u003eend -= 2 + has_exit + has_error + has_deopt + needs_guard_ip\n```\n\nSo if `R` is the buffer's true remaining space at the start of the bytecode, the assertion is checking `R - tail \u003e space_needed`, i.e.\n\n```\nR \u003e 4 + has_exit + has_error + has_deopt + 2*needs_guard_ip + nuops + has_save_ip\n```\n\n## The fitness mechanism\n\n`_PyJit_translate_single_bytecode_to_trace` also runs a fitness gate before the assertion:\n\n```\nif (fitness \u003c eq) goto done;\n```\n\n`eq` is `compute_exit_quality(opcode)`. For specializable opcodes (like `TO_BOOL`, `CALL_*`) it returns `EXIT_QUALITY_SPECIALIZABLE`, defined as `FITNESS_INITIAL / 80`. With `FITNESS_INITIAL == UOP_MAX_TRACE_LENGTH == 1000` in debug builds, that's **12**.\n\nPer bytecode, fitness is debited by `slots_used = remaining_before − remaining_after`, i.e. exactly the number of buffer slots the bytecode just consumed (uops emitted + tail reservations made). So fitness and remaining_space are *intended* to track in lockstep, with branch/frame penalties only making fitness drop *faster* (which would only ever make the assertion safer).\n\nThe intended invariant is therefore `fitness \u003c= remaining_space`. If that held, then `fitness \u003e= eq = 12` would imply `remaining_space \u003e= 12` too, and 12 slots is enough for every existing `space_needed`.\n\n## Where the invariant breaks\n\nTwo accounting holes push `fitness` *above* `remaining_space`:\n\n1. **Trace prelude not charged.** `_PyJit_StartTracing` emits `_START_EXECUTOR` and `_MAKE_WARM` into the buffer, costing 2 slots. Fitness is initialized to `FITNESS_INITIAL` (1000) right after, with no debit. So before the first bytecode: `fitness = 1000`, `remaining = 998`. Drift = **+2**.\n\n2. **`needs_guard_ip` under-reserved.** When `needs_guard_ip` is true (e.g. after `_PUSH_FRAME`), the code emits up to three uops (`_RECORD_CODE`, the `guard_ip` uop itself, and `guard_code_version_uop` if the called object is a code object). But `space_needed` only adds `+ needs_guard_ip` (i.e. 1). Each such occurrence under-reserves by 1–2 slots, but those slots *are* still consumed by `next++`, so `slots_used` does include them and fitness drops correctly. The mismatch is between the predicted `space_needed` and the actual emission, not between fitness and buffer — but it tightens the assertion further.\n\nI confirmed the drift directly. With a release build with `UOP_MAX_TRACE_LENGTH` hacked down to 1000 (matching debug) and a printf inserted at the trace-finalize site, a long branchless reproducer produced:\n\n```\nTRACE_LEN: length=666 remaining=14 fitness=18 UOP_MAX=1000\n```\n\n`fitness = 18, remaining = 14` — fitness sits 4 slots higher than the buffer.\n\n## Why this PR exposes it\n\nThe PR for gh-143732 changes the `TO_BOOL` macro from\n```\nmacro(TO_BOOL) = _SPECIALIZE_TO_BOOL + unused/2 + _TO_BOOL;\n```\nto\n```\nmacro(TO_BOOL) = _SPECIALIZE_TO_BOOL + _RECORD_TOS_TYPE + unused/2 + _TO_BOOL;\n```\n\nThat bumps `expansion-\u003enuops` for `TO_BOOL` from 1 to 2.\n\nConcrete `space_needed` for `TO_BOOL` (no exit/deopt/guard_ip, has_error, has_save_ip):\n\n| | nuops | needs_guard_ip | +2 | has_save_ip | **space_needed** |\n|---|---|---|---|---|---|\n| main | 1 | 0 | 2 | 1 | **4** |\n| PR | 2 | 0 | 2 | 1 | **5** |\n\nAfter tail reservations (`trace-\u003eend -= 2 + has_error = 3`), the post-decrement remaining must exceed `space_needed`. So the true-remaining `R` we need is:\n\n| | tail | space_needed | **R required** |\n|---|---|---|---|\n| main | 3 | 4 | **\u003e 7** |\n| PR | 3 | 5 | **\u003e 8** |\n\nWorst case at the fitness gate: `fitness == 12` and the drift is `fitness − remaining = +4`, so `R = 8`. On main, `8 \u003e 7` ✓. On the PR, `8 \u003e 8` ✗ — assertion fires.\n\n## Empirical confirmation\n\nDebug + JIT build, 30 runs of `./python -m test test_strtod --multiprocess 0 --verbose2 --verbose3`:\n\n| Build | TO_BOOL macro | Pass | Fail |\n|---|---|---|---|\n| main | original | 30 | 0 |\n| main + PR's macro change | with `_RECORD_TOS_TYPE` | 28 | 2 |\n\nFailures are always in `test_short_halfway_cases`, which uses `random.randrange`, so the failing trace shape varies and the failure is flaky (~5–10%). The C backtrace lands in `_PyJit_translate_single_bytecode_to_trace` reached from the test's `if e + q.bit_length() \u003e max_exp:` line — so the bytecode that actually trips the assertion is plausibly a wide CALL (e.g. `CALL_METHOD_DESCRIPTOR_NOARGS` for `q.bit_length()`), not `TO_BOOL` itself; widening `TO_BOOL` just shifts trace timing enough to expose it.\n\n## Bottom line\n\nIt's a thin-margin coupling, not a PR bug. The intended invariant `fitness \u003c= remaining_space` doesn't actually hold today; in some traces fitness is ~4 slots above remaining. With `EXIT_QUALITY_SPECIALIZABLE = 12`, the worst-case assertion threshold is right at the boundary for the PR's wider `TO_BOOL`, so any future macro widening can resurface this. Fixes (in increasing principled-ness): (a) graceful `goto done` instead of asserting; (b) raise `EXIT_QUALITY_SPECIALIZABLE` to ~25 to leave room for the widest opcode plus the +4 drift; (c) fix the two accounting holes so the invariant actually holds.\n\n\u003c/details\u003e\n\n### CPython versions tested on:\n\nCPython main branch\n\n### Operating systems tested on:\n\nLinux\n\n\u003c!-- gh-linked-prs --\u003e\n### Linked PRs\n* gh-149633\n* gh-150245\n\u003c!-- /gh-linked-prs --\u003e\n","author":{"url":"https://github.com/eendebakpt","@type":"Person","name":"eendebakpt"},"datePublished":"2026-05-03T20:54:24.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":8},"url":"https://github.com/149335/cpython/issues/149335"}
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:543139ac-5a01-c88d-7469-3c80092fb7b0 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | 956E:3CF864:3AA3FE:4E187D:6A516BE6 |
| html-safe-nonce | 4bfa9db63d0d5c792a5d3cd344b7d683bd07374c2afffbd7e38f3e79ff563595 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiI5NTZFOjNDRjg2NDozQUEzRkU6NEUxODdEOjZBNTE2QkU2IiwidmlzaXRvcl9pZCI6IjYxODc2MjM5ODYyODY5MTQ1MzQiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | a37902e41900dc0f9a1b3654edf4a428656e49ade2c2f87a83eab3842beaadcf |
| hovercard-subject-tag | issue:4372954630 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/python/cpython/149335/issue_layout |
| twitter:image | https://opengraph.githubassets.com/1d10c86efaf16868e428d56a370a036b4ef16b53937e6ce7382dbb0e08ae6aa2/python/cpython/issues/149335 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/1d10c86efaf16868e428d56a370a036b4ef16b53937e6ce7382dbb0e08ae6aa2/python/cpython/issues/149335 |
| og:image:alt | Bug report Bug description: The assert(uop_buffer_remaining_space(trace) > space_needed); in _PyJit_translate_single_bytecode_to_trace can be triggered if a uop is widened. Found when the CI for #1... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | eendebakpt |
| hostname | github.com |
| expected-hostname | github.com |
| None | b9a586c06a05a7a86fc7e3f4dbd03e42f6869085879aa184aa6369456dbd50fb |
| turbo-cache-control | no-preview |
| go-import | github.com/python/cpython git https://github.com/python/cpython.git |
| octolytics-dimension-user_id | 1525981 |
| octolytics-dimension-user_login | python |
| octolytics-dimension-repository_id | 81598961 |
| octolytics-dimension-repository_nwo | python/cpython |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 81598961 |
| octolytics-dimension-repository_network_root_nwo | python/cpython |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 60eaaf6d4dbaa817031b556aebd8a0a7eeb81b97 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width