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+
25+ # Frames require a file name, but GPU frames are not from a Python file.
26+ # We use the following as a placeholder.
27+ _FILE_PLACEHOLDER = "<native>"
28+
29+ # We use a frame to group GPU frames under a device.
30+ # This frame also needs a file name, none can really make sense.
31+ _DEVICE_FRAME_FILE_NAME = "<torch>"
32+
2133
2234class _WrappedTorchProfiler (wrapt .ObjectProxy ):
2335 def __init__ (
@@ -112,10 +124,23 @@ def _handle_torch_trace(prof: Any) -> None:
112124 LOG .debug ("Dropped events. events_limit %d, len(events): %d" , events_limit , num_events )
113125 events = random .sample (events , events_limit ) # nosec: used for sampling, not security
114126
115- # Determine which attributes to use once (avoid per-event getattr checks)
127+ # Determine which attributes to use once (avoid per-event getattr checks).
128+ # For CPU operators we use the "self" (exclusive of children) variants so that,
129+ # once we reconstruct the operator call tree below, parent frames aggregate to
130+ # their inclusive totals without double counting their children. CUDA device
131+ # events are leaves and report 0 for the "self" device metrics (they are async),
132+ # so for those we use the event's own totals instead.
116133 sample_event = events [0 ]
117- use_device_time = hasattr (sample_event , "device_time" )
118- use_device_memory = hasattr (sample_event , "device_memory_usage" )
134+ self_gpu_time_attr = (
135+ "self_device_time_total" if hasattr (sample_event , "self_device_time_total" ) else "self_cuda_time_total"
136+ )
137+ total_gpu_time_attr = "device_time_total" if hasattr (sample_event , "device_time_total" ) else "cuda_time_total"
138+ self_gpu_memory_attr = (
139+ "self_device_memory_usage" if hasattr (sample_event , "self_device_memory_usage" ) else "self_cuda_memory_usage"
140+ )
141+ total_gpu_memory_attr = (
142+ "device_memory_usage" if hasattr (sample_event , "device_memory_usage" ) else "cuda_memory_usage"
143+ )
119144
120145 # Earlier PyTorch versions use microseconds, later versions use nanoseconds
121146 kineto_results = prof .profiler .kineto_results
@@ -136,44 +161,62 @@ def _handle_torch_trace(prof: Any) -> None:
136161 handle = ddup .SampleHandle ()
137162 data_added = False
138163
139- # cpu time sample
140- cpu_time : int = e .cpu_time
141- if cpu_time > 0 :
164+ # Number of times this event aggregates. The time metrics below are
165+ # totals across those occurrences, so we divide by count to recover the
166+ # per-occurrence value and let ddup re-multiply by count.
167+ count : int = e .count or 1
168+
169+ # Cache str(device_type) since we use it multiple times. CPU operators get
170+ # the call tree reconstructed and use exclusive ("self") metrics; CUDA
171+ # device events are leaves and use their full totals.
172+ device_type_str = str (e .device_type )
173+ is_cpu = device_type_str .startswith ("DeviceType.CPU" )
174+
175+ # cpu time sample (exclusive of children)
176+ self_cpu_time : int = e .self_cpu_time_total
177+ if self_cpu_time > 0 :
142178 data_added = True
143- handle .push_cputime (int (cpu_time * _NANOS_PER_MICROSECOND ), e . count )
179+ handle .push_cputime (int (self_cpu_time / count * _NANOS_PER_MICROSECOND ), count )
144180
145- # gpu time sample
146- gpu_time : int = e . device_time if use_device_time else e . cuda_time
181+ # gpu time sample: exclusive for CPU operators, full device time for leaves
182+ gpu_time : int = getattr ( e , self_gpu_time_attr ) if is_cpu else getattr ( e , total_gpu_time_attr )
147183 if gpu_time > 0 :
148184 data_added = True
149- handle .push_gpu_gputime (int (gpu_time * _NANOS_PER_MICROSECOND ), e . count )
185+ handle .push_gpu_gputime (int (gpu_time / count * _NANOS_PER_MICROSECOND ), count )
150186
151187 # gpu flops sample
152188 flops : int = e .flops
153189 if flops is not None and flops > 0 :
154190 data_added = True
155- handle .push_gpu_flops (flops , e . count )
191+ handle .push_gpu_flops (flops , count )
156192
157- # GPU memory usage
158- gpu_memory : int = e . device_memory_usage if use_device_memory else e . cuda_memory_usage
193+ # GPU memory usage: exclusive for CPU operators, full usage for leaves
194+ gpu_memory : int = getattr ( e , self_gpu_memory_attr ) if is_cpu else getattr ( e , total_gpu_memory_attr )
159195 if gpu_memory is not None and gpu_memory > 0 :
160196 data_added = True
161- handle .push_gpu_memory (gpu_memory , e . count )
197+ handle .push_gpu_memory (gpu_memory , count )
162198
163199 if not data_added :
164200 if empty_events_count % 1000 == 0 :
165201 LOG .debug ("%d events with no data to record: %s" , empty_events_count , e )
166202 empty_events_count += 1
167203 continue
168204
169- # Cache str(device_type) since we use it multiple times
170- device_type_str = str (e .device_type )
171-
172- handle .push_frame (e .name , "unknown-file" , 0 , 0 )
205+ # Reconstruct the operator call tree by walking up the cpu_parent chain.
206+ # Frames are pushed leaf-first (this event), then each ancestor, so that
207+ # the flame graph nests children under their parents instead of rendering
208+ # every operator as a flat leaf. Stacks go root last.
209+ handle .push_frame (e .name , _FILE_PLACEHOLDER , 0 , 0 )
210+ parent = getattr (e , "cpu_parent" , None )
211+ depth = 0
212+ while parent is not None and depth < _MAX_FRAMES :
213+ handle .push_frame (parent .name , _FILE_PLACEHOLDER , 0 , 0 )
214+ parent = getattr (parent , "cpu_parent" , None )
215+ depth += 1
173216 # Pushing pseudoframes for the device name ("device.CPU" or "device.CUDA")
174217 # onto the stack allows differentiation of pytorch frames from other profiling frames
175218 # in the flame graph. Note that stacks go root last, so this goes at the end.
176- handle .push_frame (f"PYTORCH_{ device_type_str } " , "unknown-file" , 0 , 0 )
219+ handle .push_frame (f"PYTORCH_{ device_type_str } " , _DEVICE_FRAME_FILE_NAME , 0 , 0 )
177220 handle .push_gpu_device_name (f"cuda { e .device_index } " )
178221
179222 # Get thread info from cache or compute and cache it
0 commit comments