Skip to content

python: combine python and native unwinder to avoid tail call limits#1288

Open
gnurizen wants to merge 4 commits into
open-telemetry:mainfrom
parca-dev:python-native-hybrid
Open

python: combine python and native unwinder to avoid tail call limits#1288
gnurizen wants to merge 4 commits into
open-telemetry:mainfrom
parca-dev:python-native-hybrid

Conversation

@gnurizen
Copy link
Copy Markdown
Contributor

@gnurizen gnurizen commented Mar 26, 2026

python: combine python and native unwinder into single loop

Python, especially pytorch programs can exhaust the tail call limit
by switching from python to native unwinders more than 29 times.
This happens because of eval/delegation patterns where one python
frame will be decorated with a couple native frames.

In order to unwind these stack successfully fold the native unwinder
into the python unwinder so at each frame a python or native frame
can be unwound.

Replace the separate walk_python_stack inner loop and outer
transition loop with a single switch-in-loop structure using
step_python and step_native helper functions. This reduces
tail call usage from one per batch to one per loop budget
exhaustion.

Move native unwinder map externs (exe_id_to_*_stack_deltas,
stack_delta_page_to_info, unwind_info_array) out of the
TESTING_COREDUMP guard in extmaps.h so python_tracer.ebpf.c
can include native_stack_trace.h.

Python loop iters is now a ro_vars entry so it can be set low by
default and jacked up with debug_prints are disabled which allows for
much bigger stacks. 29 * 12 (384) remains the deepest python stack we
support but its 29 * 4 w/ debug prints enabled.

@gnurizen gnurizen changed the title python native hybrid Combine python and native unwinder into single loop Mar 26, 2026
@gnurizen gnurizen force-pushed the python-native-hybrid branch 3 times, most recently from a83b6d6 to 365d706 Compare March 26, 2026 23:40
@gnurizen gnurizen marked this pull request as ready for review March 27, 2026 00:59
@gnurizen gnurizen requested review from a team as code owners March 27, 2026 00:59
@gnurizen
Copy link
Copy Markdown
Contributor Author

@gnurizen
Copy link
Copy Markdown
Contributor Author

@fabled @florianl tagging you guys for review consideration, no hurry just want to make sure this gets on the appropriate radars. Thanks!

@fabled
Copy link
Copy Markdown
Contributor

fabled commented Apr 8, 2026

Seems ruby has same issue. See #1335 .

I wonder if something more elaborate could be done. Or is it better to bundle native unwinder with the interpreters that need it due to mixing native/HLL frames every few frames.

@gnurizen gnurizen force-pushed the python-native-hybrid branch from 2489c65 to a4809c1 Compare April 8, 2026 19:07
@gnurizen
Copy link
Copy Markdown
Contributor Author

gnurizen commented Apr 8, 2026

Rebased to PR #1286. Yeah I'd love to get @dalehamel 's thought on the applicability of this approach to the Ruby situation.

@dalehamel
Copy link
Copy Markdown
Contributor

Seems ruby has same issue. See #1335 .

I wonder if something more elaborate could be done. Or is it better to bundle native unwinder with the interpreters that need it due to mixing native/HLL frames every few frames.

Yes especially in production we see this problem. With yjit, the problem is masked by the fact that the jit is only the leaf frame and we don't run with jit frame pointers in production for performance reasons.

However the ruby unwinder is already quite instruction heavy as we need to do the complex CME resolution for each ruby frame, so it might be hard to get all frames if we add the native unwinder into it too. In reality we mostly really care about the actual 'ruby' stack state (which shouldn't really matter if the frame is jit or interpreter backed, as it might be either with zjit) + jit leaf state the majority of the time and that's been fine for our purposes.

If we could manage to actually continue the native unwinding without exhausting tail calls, that would certainly be the best of both worlds, but i wouldn't say it's the highest priority.

@gnurizen gnurizen force-pushed the python-native-hybrid branch from a4809c1 to f5a2fac Compare April 20, 2026 15:33
@gnurizen gnurizen changed the title Combine python and native unwinder into single loop python: combine python and native unwinder into single loop to support highly mixed stacks hitting tail call limits Apr 20, 2026
@gnurizen gnurizen changed the title python: combine python and native unwinder into single loop to support highly mixed stacks hitting tail call limits python: combine python and native unwinder to support deep mixed stacks hitting tail call limits Apr 20, 2026
@gnurizen gnurizen marked this pull request as draft April 20, 2026 15:40
@gnurizen gnurizen force-pushed the python-native-hybrid branch 3 times, most recently from 08dc71e to ef71a9a Compare April 28, 2026 14:15
@linux-foundation-easycla
Copy link
Copy Markdown

linux-foundation-easycla Bot commented Apr 28, 2026

CLA Signed

The committers listed above are authorized under a signed CLA.

@gnurizen gnurizen force-pushed the python-native-hybrid branch from ef71a9a to c0eed20 Compare April 28, 2026 14:22
@gnurizen gnurizen marked this pull request as ready for review April 28, 2026 15:09
@gnurizen
Copy link
Copy Markdown
Contributor Author

This has been rebased to main, is passing all the kernel tests and should be green when the new coredump test is uploaded. @florianl @fabled if you could give this another pass when you can that would be much appreciated!

@gnurizen gnurizen force-pushed the python-native-hybrid branch from c0eed20 to 583f6a0 Compare May 5, 2026 12:31
@gnurizen gnurizen changed the title python: combine python and native unwinder to support deep mixed stacks hitting tail call limits python: combine python and native unwinder to avoid tail call limits May 5, 2026
@gnurizen gnurizen force-pushed the python-native-hybrid branch from 583f6a0 to 23507cb Compare May 5, 2026 13:26
@gnurizen
Copy link
Copy Markdown
Contributor Author

gnurizen commented May 5, 2026

@florianl @fabled what are your thoughts on splitting off the first commit here and trying to land that first?

@gnurizen
Copy link
Copy Markdown
Contributor Author

@fabled @florianl friendly ping on this. What if this change was a runtime opt-in (or could be build time I guess) so we could land it w/o changing anything functionally and Parca could just switch it on for our agent? ebpf programs are small, no harm in having some extra ones in the binary. I haven't worked out exactly how that would work but seems doable.

@fabled
Copy link
Copy Markdown
Contributor

fabled commented May 15, 2026

I think this is probably the simple thing to go forward to solve a real problem at the hand. I'd prefer to do something else, but it probably is a bigger job and/or not feasible at this time. So I'm ok to do this at this time. Let's keep do this just for everyone (no opt-in switch imho). The less there is configuration / build/runtime switch the more maintainable it is.

@christos68k @florianl Thoughts?

@christos68k
Copy link
Copy Markdown
Member

I think this is probably the simple thing to go forward to solve a real problem at the hand. I'd prefer to do something else, but it probably is a bigger job and/or not feasible at this time. So I'm ok to do this at this time. Let's keep do this just for everyone (no opt-in switch imho). The less there is configuration / build/runtime switch the more maintainable it is.

@christos68k @florianl Thoughts?

I'm fine for now with this PR as-is, the change is not that extensive and it's conceptually clean and fairly simple. The alternative @gnurizen proposed here is also fine, though I prefer the current PR.

@gnurizen
Copy link
Copy Markdown
Contributor Author

gnurizen commented May 16, 2026

I redid the math on the number of frames we do, #1422 really opens up the doors on the upper bound on the *_PER_PROGRAM frame loops! But alas we still need this patch to get around the tail call limit with pytorch's checkered zebra stacks.

@florianl can we make the next step uploading the coredump test so we can get a green CI here? See this comment: #1288 (comment)

@fabled
Copy link
Copy Markdown
Contributor

fabled commented May 18, 2026

can we make the next step uploading the coredump test so we can get a green CI here? See this comment: #1288 (comment)

Uploaded and reran tests now. Can you merge with main and update ebpf blobs? Thanks!

gnurizen added 4 commits May 18, 2026 12:43
This is a prep the patient PR to make room for a hybrid python/native
unwinder that we found necessary to unwind large pytorch stacks that
go back and forth from python to native more times than the tail call
limit will allow.

This change is pure code motion and changes nothing functionally.
Python, especially pytorch programs can exhaust the tail call limit
by switching from python to native unwinders more than 29 times.
This happens because of eval/delegation patterns where one python
frame will be decorated with a couple native frames.

In order to unwind these stack successfully fold the native unwinder
into the python unwinder so at each frame a python or native frame
can be unwound.

Replace the separate walk_python_stack inner loop and outer
transition loop with a single switch-in-loop structure using
step_python and step_native helper functions. This reduces
tail call usage from one per batch to one per loop budget
exhaustion (PYTHON_NATIVE_LOOP_ITERS=9 iterations).

Move native unwinder map externs (exe_id_to_*_stack_deltas,
stack_delta_page_to_info, unwind_info_array) out of the
TESTING_COREDUMP guard in extmaps.h so python_tracer.ebpf.c
can include native_stack_trace.h.

Python loop iters is now a ro_vars entry so it can be set low by
default and jacked up with debug_prints are disabled which allows for
much bigger stacks.
Both the host agent (production, no verifier debug branches) and the
coredump tool (no verifier at all) need the full 12 iterations to unwind
deep Python+native stacks. Only VerboseMode=true in CI hits the 1M
verifier instruction limit on kernel 6.18+ because DEBUG_PRINT roughly
triples per-iter complexity.

Previously the eBPF rodata default was 4 (the verifier-limited value)
and systemconfig.go overrode UP to 12 for production. The coredump tool
bypasses systemconfig.go and was stuck at 4, breaking the deep-python
test. Flip the polarity: default to 12 in eBPF, override DOWN to 4 in
systemconfig.go when VerboseMode is set. Coredump picks up 12 for free.
@gnurizen gnurizen force-pushed the python-native-hybrid branch from 23507cb to 7475c10 Compare May 18, 2026 19:08
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.

4 participants