Repository navigation
JIT can trigger assert(uop_buffer_remaining_space(trace) > space_needed); for widened uops #149335
Description
Activity
- addedtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
on May 3, 2026 - addedinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)
on May 3, 2026 #148089 (comment) We need to continue optimizing the parameters related to the finesse mechanism. I'll try to resolve this.
Rather than penalizing each additional uop equally, we could impose no penalty up to half the max, then three times the current penalty. What we want is good traces, but not too long to overflow the buffer. So we only want the fitness to drop as we approach the limit.
Here's a rough reproducer, I'm sure it can be made nicer. Was found by fuzzing and reduced with shrinkray:
import sys def f1(): for i_3178 in 0, 2, 10: mv162 = 162 mv3 = mv1 = mv_165 = mv16 = \ mv167 = mv168 = \ mv169 = \ mv_1403_170 = \ 169 mv_1403_170 mv_172 = mv_3 = mv_4 = mv175 = mv176 = mv17 = mv178 = mv179 = mv0 = mv1 = mv182 = ( mv3 ) = mv4 = mv185 = mv186 = mv187 = mv18 = mv189 = mv0 = mv1 = mv192 = mv3 = mv4 = ( mv195 ) = mv196 = mv197 = mv_198 = mv19 = mv0 = mv1 = mv2 = mv3 = mv4 = mv05 = mv06 = ( mv07 ) = mv08 = mv09 = mv0 = mv1 = mv2 = mv3 = mv4 = mv15 = mv16 = mv17 = mv18 = mv19 = ( mv0 ) = mv1 = mv_2 = mv3 = mv4 = mv_25 = mv_26 = mv_27 = mv_28 = mv_29 = mv0 = mv1 = ( mv2 ) = mv_1403 = mv4 = mv35 = mv36 = mv37 = mv38 = mv39 = mv0 = -sys.maxsize / 3 mv1 = mv_12 = mv3 = mv_14 = mv45 = sys.float_info.epsilon mv46 = sys.float_info.epsilon for i in range(15000): f1()
Reacted by Hai ZhuHere's a rough reproducer, I'm sure it can be made nicer. Was found by fuzzing and reduced with shrinkray:
@markshannon I tried adding a penalty multiplied by the
specified thresholdafter reaching 1/specified thresholdof the maximum value (used 2 and 3), but it didn't solve the problem and even caused a slight performance drop (1%~2%).@devdanzin
The reproducer doesn't fail for me.
What platform and build flags were you using?Fails with
./configure --enable-experimental-jit=yes -C --with-pydebugReacted by devdanzin and Sergey MiryanovI saw this assertion error on "JIT / x86_64-pc-windows-msvc/msvc (Debug) (pull_request)" CI in
test_strtod: https://github.com/python/cpython/actions/runs/26159001645/job/76945709494?pr=150084test_short_halfway_cases (test.test_strtod.StrtodTests.test_short_halfway_cases) ... Assertion failed: uop_buffer_remaining_space(trace) > space_needed, file D:\a\cpython\cpython\Python\optimizer.c, line 902 Fatal Python error: Aborted Current thread 0x000016b0 (most recent call first): File "D:\a\cpython\cpython\Lib\test\test_strtod.py", line 75 in strtod File "D:\a\cpython\cpython\Lib\test\test_strtod.py", line 101 in check_strtod File "D:\a\cpython\cpython\Lib\test\test_strtod.py", line 142 in test_short_halfway_cases- added a commit that references this issue
on May 21, 2026 - added a commit that references this issue
on May 21, 2026 @eendebakpt The problem should be solved,could you please try again?
@eendebakpt The problem should be solved,can you try again?
The PR with the original issue now passes, I'll close the issue. Thanks for working on this.
Reacted by Hai Zhu- added a commit that references this issue
on May 24, 2026
Bug report
Bug description:
The
assert(uop_buffer_remaining_space(trace) > space_needed);in_PyJit_translate_single_bytecode_to_tracecan be triggered if a uop is widened. Found when the CI for #148271 was failing (thereTO_BOOLgets an extra_RECORD_TOS_TYPE). @markshannonReporting the issue already, I will try to create a simple reproducer and more concise explanation.
Full explanation by Claude
JIT trace-buffer assertion: full explanation
The assertion
In
_PyJit_translate_single_bytecode_to_trace(Python/optimizer.c), each bytecode appended to a tier-2 trace is gated by a Py_DEBUG assertion:where
expansion->nuopsis the number of uops the bytecode's macro expands to. The+ 2covers the_CHECK_VALIDITYuop emitted at the start of every bytecode plus a reserved_DEOPTslot for it.+ needs_guard_ipcovers the_GUARD_IP_*slot for opcodes that need it.+ (!OPCODE_HAS_NO_SAVE_IP)covers the_SET_IPslot.Just before the assertion,
trace->endhas already been decremented to reserve the side-exit/error/deopt/guard-ip stubs at the buffer's tail:So if
Ris the buffer's true remaining space at the start of the bytecode, the assertion is checkingR - tail > space_needed, i.e.The fitness mechanism
_PyJit_translate_single_bytecode_to_tracealso runs a fitness gate before the assertion:eqiscompute_exit_quality(opcode). For specializable opcodes (likeTO_BOOL,CALL_*) it returnsEXIT_QUALITY_SPECIALIZABLE, defined asFITNESS_INITIAL / 80. WithFITNESS_INITIAL == UOP_MAX_TRACE_LENGTH == 1000in debug builds, that's 12.Per 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).The intended invariant is therefore
fitness <= remaining_space. If that held, thenfitness >= eq = 12would implyremaining_space >= 12too, and 12 slots is enough for every existingspace_needed.Where the invariant breaks
Two accounting holes push
fitnessaboveremaining_space:Trace prelude not charged.
_PyJit_StartTracingemits_START_EXECUTORand_MAKE_WARMinto the buffer, costing 2 slots. Fitness is initialized toFITNESS_INITIAL(1000) right after, with no debit. So before the first bytecode:fitness = 1000,remaining = 998. Drift = +2.needs_guard_ipunder-reserved. Whenneeds_guard_ipis true (e.g. after_PUSH_FRAME), the code emits up to three uops (_RECORD_CODE, theguard_ipuop itself, andguard_code_version_uopif the called object is a code object). Butspace_neededonly adds+ needs_guard_ip(i.e. 1). Each such occurrence under-reserves by 1–2 slots, but those slots are still consumed bynext++, soslots_useddoes include them and fitness drops correctly. The mismatch is between the predictedspace_neededand the actual emission, not between fitness and buffer — but it tightens the assertion further.I confirmed the drift directly. With a release build with
UOP_MAX_TRACE_LENGTHhacked down to 1000 (matching debug) and a printf inserted at the trace-finalize site, a long branchless reproducer produced:fitness = 18, remaining = 14— fitness sits 4 slots higher than the buffer.Why this PR exposes it
The PR for gh-143732 changes the
TO_BOOLmacro fromto
That bumps
expansion->nuopsforTO_BOOLfrom 1 to 2.Concrete
space_neededforTO_BOOL(no exit/deopt/guard_ip, has_error, has_save_ip):After tail reservations (
trace->end -= 2 + has_error = 3), the post-decrement remaining must exceedspace_needed. So the true-remainingRwe need is:Worst case at the fitness gate:
fitness == 12and the drift isfitness − remaining = +4, soR = 8. On main,8 > 7✓. On the PR,8 > 8✗ — assertion fires.Empirical confirmation
Debug + JIT build, 30 runs of
./python -m test test_strtod --multiprocess 0 --verbose2 --verbose3:_RECORD_TOS_TYPEFailures are always in
test_short_halfway_cases, which usesrandom.randrange, so the failing trace shape varies and the failure is flaky (~5–10%). The C backtrace lands in_PyJit_translate_single_bytecode_to_tracereached from the test'sif e + q.bit_length() > max_exp:line — so the bytecode that actually trips the assertion is plausibly a wide CALL (e.g.CALL_METHOD_DESCRIPTOR_NOARGSforq.bit_length()), notTO_BOOLitself; wideningTO_BOOLjust shifts trace timing enough to expose it.Bottom line
It's a thin-margin coupling, not a PR bug. The intended invariant
fitness <= remaining_spacedoesn't actually hold today; in some traces fitness is ~4 slots above remaining. WithEXIT_QUALITY_SPECIALIZABLE = 12, the worst-case assertion threshold is right at the boundary for the PR's widerTO_BOOL, so any future macro widening can resurface this. Fixes (in increasing principled-ness): (a) gracefulgoto doneinstead of asserting; (b) raiseEXIT_QUALITY_SPECIALIZABLEto ~25 to leave room for the widest opcode plus the +4 drift; (c) fix the two accounting holes so the invariant actually holds.CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Linked PRs
FITNESS_INITIAL#149633FITNESS_INITIAL(GH-149633) #150245