Tree traversal refactor - #966
Open
kyle-hoffmeyer wants to merge 8 commits into
Open
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
kyle-hoffmeyer
marked this pull request as ready for review
August 26, 2026 00:27
kyle-hoffmeyer
requested review from
ajassani,
devalshahamd,
gabeweisz and
tsrikris
as code owners
August 26, 2026 00:27
gabeweisz
approved these changes
Aug 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
NOTE: CSV changes are due to changes in synthetic op classifications and UID shifts for synthetic ops.
Summary
This PR refactors the tree traversal in tree_perf to be simpler and more comprehensive.
The existing traversal performed two passes over the tree:
The second pass re-derives information the first pass already has: "what is this kernel's nearest cpu_op, and what are its shape args" leading to unncessary traversals. The second pass is also detached from the live call stack, so synthetic ops don't have their call stacks populated. Generating the call stacks for these synthetic ops in this second pass leads to redundant traversals.
Solution
This PR modifies the tree traversal to perform the entire tree traversal in the primary pass and remove the second pass while still generating synthetic operations and complete call stacks for all operations. Synthetic ops now have call stack information and redundant tree traversals are avoided.
Implementation
All changes are in
TraceLens/TreePerf/tree_perf.py, in and aroundcollect_unified_perf_events.Single unified set of roots. The old code seeded the traversal from two separate root loops — parentless
python_functionroots (only whenadd_python_funcwas on), thencpu_root_nodes:Neither loop enumerated a bare kernel launchers that are itself parentless, so parentless launchers were never entered by the main pass and were left to the post-pass. Both loops are replaced by one:
Starting from every parentless event that has GPU work covers cpu_op roots,
python_functionroots, and barecuda_runtimelaunchers with no CPU op above them. Combined with removing the earlyreturnthat skipped non-cpu_op events whenadd_python_funcwas off, this is what lets orphan launchers be reached during the main pass regardless ofadd_python_func.nearest_cpu_opthreaded down the recursion.traversenow carries a third argument, the innermostcpu_opancestor thus far in the traversal. This single value is the discriminator between the two kinds of synthetic ops:A synthetic op under a cpu_op: Here
cpu_op_Eis not a leaf operation (it contains a finer kernel-launching cpu_op), so it recurses;cpu_op_C/k_Cgets its own normal row, andcpu_op_E's own directly-launchedk_ownis left over. When the recursion reaches that launcher,nearest_cpu_opiscpu_op_E, so acpu_op_E->k_own (Synthetic Op)is created:→
cpu_op_E->k_own (Synthetic Op), args fromcpu_op_E.An orphan (no cpu_op anywhere on the path): Here there is no cpu_op above the kernel at all, so
nearest_cpu_opisNonewhen the launcher is reached (Exit 4):→
hipLaunchKernel->kernel_A (Synthetic Op), args from the launcher.Strict definition of a "leaf" cpu_op. Previously
_is_leaf_cpu_opwould returnTrueif it directly launched a kernel even if it launched other kernel-launching cpu ops. Now, if the cpu_op has calls kernel-launching cpu ops, it returnsFalse. This change was made, because previously a leaf cpu_op would collect its entire subtree as one row, subsuming any nested kernel-launching cpu_ops. Now such an op is no longer a leaf, it recurses, the nested cpu_op gets its own row, and the parent's own directly-launched kernels become synthetic ops.This is the one behavioral change — nested kernel-launching cpu_ops now always surface at their own granularity, independent of whether anything in the subtree is perf-modeled.
Renamed and broadened the descendant check.
_has_descendant_cpu_op_with_own_perf_model→_has_descendant_cpu_op_with_kernels. The old predicate only fired when a descendant cpu_op had both a perf model and GPU work; the new one fires for any descendant cpu_op that launches kernels:The rationale is granularity consistency: the innermost cpu_op should own its kernels whether or not some descendant happens to be perf-modeled, so recursion shouldn't hinge on a perf-model flag. This predicate now drives both the strict-leaf rule above and the backward-op exit (Exit 2), which previously used the perf-model-only variant.
Three small helpers factor out the inline synthesis:
_create_synthetic_op(prefix_name, kernel, cpu_op)— appends one"<prefix>-><kernel> (Synthetic Op)"row owning exactly that kernel, as a copy ofcpu_op(so it inherits its shape args) with a fresh UID.cpu_opis the enclosing cpu_op, or the launcher itself when there is no cpu_op on the path._direct_kernels(event)— the kernels whose innermost cpu_op isevent: descends through non-cpu_op children and stops at any nested cpu_op. No depth cap, so it still reaches very deep launchers._recurse(event, call_stack, child_nearest_cpu_op, is_cpu_op)— recurses into all children and, ifeventis a cpu_op, emits a synthetic op per direct kernel. Called from Exit 2 and the generic recurse.New parentless launcher colelction. When there are no cpu ops in the subtree, the traversal emits one synthetic row per directly-launched kernel child of the launcher, then recurses to reach deeper launchers — replacing the deleted
parent_to_orphansgrouping in the old post-pass.Tests
tests/test_synthetic_op.pybuilds small hand-crafted trees and asserts the row classification and the every-kernel-once invariant directly againstbuild_df_unified_perf_table.Row-classification matrix
To make the traversal's behavior concrete, consider this one fixed tree shape and vary only whether each cpu_op has a perf model:
cpu_op_Ais never a leaf here — it contains nested kernel-launching cpu_ops (B,C), so under the strict-leaf rule it can only be collected via a perf model; otherwise it recurses. That single fact drives the whole table:Aperf model?Bperf model?Cperf model?cpu_op_A→ {kernel_A, kernel_B, kernel_C}B/Care subsumed andkernel_Ayields no synthetic op.B/C's own perf-model status is irrelevant.cpu_op_B→ {kernel_B}cpu_op_C→ {kernel_C}cpu_op_A->kernel_A (Synthetic Op)→ {kernel_A}Ahas no perf model and isn't a leaf → recurses.BandCeach own their kernel.A's ownkernel_Ais left over → synthetic op sourced fromA.cpu_op_B→ {kernel_B}cpu_op_C→ {kernel_C}cpu_op_A->kernel_A (Synthetic Op)→ {kernel_A}BandCare leaf cpu_ops.cpu_op_B→ {kernel_B}cpu_op_C→ {kernel_C}cpu_op_A->kernel_A (Synthetic Op)→ {kernel_A}BandCare leaf cpu_ops.cpu_op_B→ {kernel_B}cpu_op_C→ {kernel_C}cpu_op_A->kernel_A (Synthetic Op)→ {kernel_A}BandCare leaf cpu_ops.Tests
tests/test_synthetic_op.pybuilds small hand-crafted trees and asserts the row classification and the every-kernel-once invariant directly againstbuild_df_unified_perf_table.