1818
1919_NANOS_PER_MICROSECOND = 1e3
2020
21+ # Safety bound on how far we walk up the cpu_parent chain when reconstructing
22+ # the operator call tree, to guard against pathological depths.
23+ _MAX_FRAMES = 128
24+
2125
2226class _WrappedTorchProfiler (wrapt .ObjectProxy ):
2327 def __init__ (
@@ -112,10 +116,17 @@ def _handle_torch_trace(prof: Any) -> None:
112116 LOG .debug ("Dropped events. events_limit %d, len(events): %d" , events_limit , num_events )
113117 events = random .sample (events , events_limit ) # nosec: used for sampling, not security
114118
115- # Determine which attributes to use once (avoid per-event getattr checks)
119+ # Determine which attributes to use once (avoid per-event getattr checks).
120+ # We use the "self" (exclusive of children) variants of each metric so that,
121+ # once we reconstruct the operator call tree below, parent frames aggregate
122+ # to their inclusive totals without double counting their children.
116123 sample_event = events [0 ]
117- use_device_time = hasattr (sample_event , "device_time" )
118- use_device_memory = hasattr (sample_event , "device_memory_usage" )
124+ self_gpu_time_attr = (
125+ "self_device_time_total" if hasattr (sample_event , "self_device_time_total" ) else "self_cuda_time_total"
126+ )
127+ self_gpu_memory_attr = (
128+ "self_device_memory_usage" if hasattr (sample_event , "self_device_memory_usage" ) else "self_cuda_memory_usage"
129+ )
119130
120131 # Earlier PyTorch versions use microseconds, later versions use nanoseconds
121132 kineto_results = prof .profiler .kineto_results
@@ -136,29 +147,34 @@ def _handle_torch_trace(prof: Any) -> None:
136147 handle = ddup .SampleHandle ()
137148 data_added = False
138149
139- # cpu time sample
140- cpu_time : int = e .cpu_time
141- if cpu_time > 0 :
150+ # Number of times this event aggregates. Each metric below is an exclusive
151+ # ("self") total across those occurrences, so we divide by count to recover
152+ # the per-occurrence value and let ddup re-multiply by count.
153+ count : int = e .count or 1
154+
155+ # cpu time sample (exclusive of children)
156+ self_cpu_time : int = e .self_cpu_time_total
157+ if self_cpu_time > 0 :
142158 data_added = True
143- handle .push_cputime (int (cpu_time * _NANOS_PER_MICROSECOND ), e . count )
159+ handle .push_cputime (int (self_cpu_time / count * _NANOS_PER_MICROSECOND ), count )
144160
145- # gpu time sample
146- gpu_time : int = e . device_time if use_device_time else e . cuda_time
147- if gpu_time > 0 :
161+ # gpu time sample (exclusive of children)
162+ self_gpu_time : int = getattr ( e , self_gpu_time_attr )
163+ if self_gpu_time > 0 :
148164 data_added = True
149- handle .push_gpu_gputime (int (gpu_time * _NANOS_PER_MICROSECOND ), e . count )
165+ handle .push_gpu_gputime (int (self_gpu_time / count * _NANOS_PER_MICROSECOND ), count )
150166
151167 # gpu flops sample
152168 flops : int = e .flops
153169 if flops is not None and flops > 0 :
154170 data_added = True
155- handle .push_gpu_flops (flops , e . count )
171+ handle .push_gpu_flops (flops , count )
156172
157- # GPU memory usage
158- gpu_memory : int = e . device_memory_usage if use_device_memory else e . cuda_memory_usage
159- if gpu_memory is not None and gpu_memory > 0 :
173+ # GPU memory usage (exclusive of children)
174+ self_gpu_memory : int = getattr ( e , self_gpu_memory_attr )
175+ if self_gpu_memory is not None and self_gpu_memory > 0 :
160176 data_added = True
161- handle .push_gpu_memory (gpu_memory , e . count )
177+ handle .push_gpu_memory (self_gpu_memory , count )
162178
163179 if not data_added :
164180 if empty_events_count % 1000 == 0 :
@@ -169,7 +185,17 @@ def _handle_torch_trace(prof: Any) -> None:
169185 # Cache str(device_type) since we use it multiple times
170186 device_type_str = str (e .device_type )
171187
188+ # Reconstruct the operator call tree by walking up the cpu_parent chain.
189+ # Frames are pushed leaf-first (this event), then each ancestor, so that
190+ # the flame graph nests children under their parents instead of rendering
191+ # every operator as a flat leaf. Stacks go root last.
172192 handle .push_frame (e .name , "unknown-file" , 0 , 0 )
193+ parent = getattr (e , "cpu_parent" , None )
194+ depth = 0
195+ while parent is not None and depth < _MAX_FRAMES :
196+ handle .push_frame (parent .name , "unknown-file" , 0 , 0 )
197+ parent = getattr (parent , "cpu_parent" , None )
198+ depth += 1
173199 # Pushing pseudoframes for the device name ("device.CPU" or "device.CUDA")
174200 # onto the stack allows differentiation of pytorch frames from other profiling frames
175201 # in the flame graph. Note that stacks go root last, so this goes at the end.
0 commit comments