# Source code for tensorplay.profiler.profiler Source: https://www.tensorplay.cn/docs/_modules/tensorplay/profiler/profiler.html ``` """Public profiler controller and instrumentation contexts.""" from __future__ import annotations import os import socket import time from contextlib import ContextDecorator from enum import Enum import tensorplay._C as _C from ._chrome_trace_export import merge_distributed_traces from ._memory_profiler import ( export_memory_timeline, memory_summary, ) from ._schedule import ProfilerAction, schedule from ._utils import rank_world from .profiler_util import EventList, device_activities from .python_tracer import PySampler DeviceType = _C.DeviceType [docs] class ProfilerActivity(str, Enum): """Activity groups understood by the profiler.""" CPU = "cpu" CUDA = "cuda" [docs] def supported_activities(): """Return activity groups available in this build.""" activities = {ProfilerActivity.CPU} try: import tensorplay as tp if tp.cuda.is_available(): activities.add(ProfilerActivity.CUDA) except Exception: pass return activities def _activity_name(activity): if isinstance(activity, ProfilerActivity): return activity.value if isinstance(activity, str): return activity.lower() value = getattr(activity, "value", None) return str(value).lower() if value is not None else str(activity).lower() [docs] def tensorboard_trace_handler(directory, worker_name=None, use_gzip=False): """Create a callback that writes one trace file per completed cycle.""" directory = os.fspath(directory) def handler(prof): os.makedirs(directory, exist_ok=True) name = worker_name or f"{socket.gethostname()}_{os.getpid()}" filename = f"{name}.{time.time_ns()}.pt.trace.json" if use_gzip: filename += ".gz" prof.export_chrome_trace(os.path.join(directory, filename), torch_compat=True) return handler _module_original_call = None _module_patch_depth = 0 def _start_module_tracing(): """Patch Module.__call__ so every module forward brackets a span. The patch is reference-counted across nested sessions; only the outermost session installs it, and only the last exit restores. While installed the span begin/end pair costs one atomic load when no session captures, and one extra Python frame per module call while capturing. """ global _module_original_call, _module_patch_depth from tensorplay.nn.modules.module import Module if _module_patch_depth == 0: _module_original_call = Module.__call__ Module.__call__ = _module_span_call _module_patch_depth += 1 def _stop_module_tracing(): global _module_original_call, _module_patch_depth if _module_patch_depth == 0: return _module_patch_depth -= 1 if _module_patch_depth == 0: from tensorplay.nn.modules.module import Module if Module.__call__ is _module_span_call: Module.__call__ = _module_original_call _module_original_call = None def _module_span_call(module_self, *args, **kwargs): """Module.__call__ replacement active during with_modules sessions.""" _C._profiler_user_begin("nn.Module: " + type(module_self).__name__) try: return _module_original_call(module_self, *args, **kwargs) finally: _C._profiler_user_end() #: Whether any device activity record has ever been collected in this process. #: The collector allocates its record buffers when it is first asked for them and #: does not record what happens while it does, so the first session in a process #: reports nothing; a caller that needs device records has to know whether the #: session it just ran was that one. _device_records_seen = False def device_activity_ready() -> bool: """Whether a device-activity session started now would report records. False in a process that has not yet run one, and false only for the first session: the collector is ready from the second onwards. """ return _device_records_seen def _note_device_records(count: int) -> None: """Record that a session received this many device records.""" global _device_records_seen if count: _device_records_seen = True [docs] class profile: """Context manager that records dispatched operations and annotations.""" def __init__( self, enabled=True, *, activities=None, schedule=None, on_trace_ready=None, record_shapes=False, profile_memory=False, with_stack=False, with_flops=False, with_modules=False, use_device=None, gpu_timing=None, gpu_trace=None, with_samples=False, ): self.enabled = bool(enabled) self.activities = tuple(activities) if activities is not None else None self.record_shapes = bool(record_shapes) self.profile_memory = bool(profile_memory) self.with_stack = bool(with_stack) # FLOP estimation reads input shapes at aggregation time, so it # requires shape capture; enabling it implies record_shapes. self.with_flops = bool(with_flops) if self.with_flops and not self.record_shapes: self.record_shapes = True self.with_modules = bool(with_modules) self.use_device = use_device self.with_samples = bool(with_samples) self.schedule = schedule self.on_trace_ready = on_trace_ready activity_names = {_activity_name(activity) for activity in self.activities or ()} cuda_requested = ( "cuda" in activity_names or (use_device is not None and _activity_name(use_device) == "cuda") ) # GPU durations come from the CUPTI activity stream by default: it # collects kernel/memcpy records asynchronously on its own thread, # so the dispatch hot path pays no per-op driver calls. An explicit # gpu_timing=True request keeps the legacy event-pair timing (it can # coexist with gpu_trace for exact per-op times); the session start # falls back to event pairs automatically when the CUPTI runtime # cannot load. self.gpu_timing = bool(gpu_timing) if gpu_trace is None: self.gpu_trace = cuda_requested and gpu_timing is None else: self.gpu_trace = bool(gpu_trace) self.step_num = 0 self.events = None self.gpu_activities = [] self.mem_events = [] self._recording = False self._entered = False self._modules_patched = False self._t0 = 0 self._sampler = None self.stop_ms = 0.0 self.gpu_timed_events = 0 self.gpu_resolved_events = 0 self._device_results = None def _ensure_events(self): if self.events is None: self.events = EventList( (), base_ns=self._t0, gpu_activities=self.gpu_activities, mem_events=self.mem_events, with_stack=self.with_stack, ) self.events.with_flops = self.with_flops return self.events def __enter__(self): if self._entered: raise RuntimeError("profiler context manager is not reentrant") self._entered = True self._t0 = time.perf_counter_ns() try: if self.enabled and self.with_modules: _start_module_tracing() self._modules_patched = True if self.enabled and self.with_samples: self._sampler = PySampler(lambda: self._recording) self._sampler.start() if self.enabled and self.schedule is None: self._start_session() except Exception: if self._sampler is not None: self._sampler.stop() self._sampler = None if self._modules_patched: _stop_module_tracing() self._modules_patched = False self._entered = False raise return self def _start_session(self): if not self.enabled or self._recording: return _C._profiler_start( self.record_shapes, self.with_stack, self.gpu_timing, self.gpu_trace, self.profile_memory, ) self._recording = True # Custom operators emit their op spans only while a session is live; # the counter handles nested/scheduled sessions. from .. import library as _library _library._note_profiling_start() def _stop_session(self): if not self._recording: return from .. import library as _library _library._note_profiling_stop() stop_start = time.perf_counter_ns() try: raw_ops, raw_gpu, raw_mem = _C._profiler_stop() self.stop_ms += (time.perf_counter_ns() - stop_start) / 1e6 self.gpu_timed_events += sum( 1 for event in raw_ops if len(event) > 8 and event[8] is not None and event[8] >= 0 ) self.gpu_resolved_events = self.gpu_timed_events events = self._ensure_events() events.extend(raw_ops) if raw_gpu: self.gpu_activities.extend(raw_gpu) _note_device_records(len(raw_gpu)) if raw_mem: self.mem_events.extend(raw_mem) finally: self._recording = False def _notify_trace_ready(self): if self.on_trace_ready is not None: self.on_trace_ready(self) [docs] def step(self): """Advance the configured step schedule.""" if not self.enabled or self.schedule is None: return None if not self._entered: raise RuntimeError("profiler.step() called outside its context") action = self.schedule(self.step_num) self.step_num += 1 if action in (ProfilerAction.RECORD, ProfilerAction.RECORD_AND_SAVE): if not self._recording: self._start_session() if action == ProfilerAction.RECORD_AND_SAVE: self._stop_session() self._notify_trace_ready() else: was_recording = self._recording self._stop_session() if was_recording and action == ProfilerAction.NONE: self._notify_trace_ready() return action def __exit__(self, exc_type, exc, tb): try: self._stop_session() finally: if self._sampler is not None: self._sampler.stop() if self._modules_patched: _stop_module_tracing() self._modules_patched = False self._ensure_events() self._entered = False return False def __str__(self): """The recording as a table; printing a session prints what it saw.""" return str(self._ensure_events()) [docs] def kineto_results(self): """The device's own record of what it did, in a form meant to be read. Named for what it is rather than for how it was collected: a caller wants the device's work, wants to know which piece of it belongs to which operation, and wants the two joined by something exact. What the collector gathered is exactly that, so it is handed over as records that answer those questions, and the join is already made rather than left to the caller to reconstruct. """ if self._device_results is None: self._device_results = device_activities(self.gpu_activities) return self._device_results @property def device_time_by_span(self): """For each collected span, the device time of the work inside it. The per-span answer, for a caller that has a span and wants what it cost on the device rather than what it cost to ask for. """ return self._ensure_events().device_time_by_span def key_averages( self, group_by_input_shape=False, group_by_stack_n=0, group_by_overload_name=False, include_python_functions=False, sort_by=None, ): events = self._ensure_events() if group_by_input_shape and not any(len(event) > 5 and event[5] is not None for event in events): raise ValueError("No shapes recorded: pass record_shapes=True to profile()") table = events.key_averages( group_by_input_shape, group_by_stack_n, group_by_overload_name, include_python_functions, ) if sort_by is not None: table.sort(sort_by) return table [docs] def device_kernels(self, sort_by=None): """What the device ran, by the name the device knows it by.""" return self._ensure_events().device_kernels(sort_by=sort_by) [docs] def table( self, sort_by=None, row_limit=100, header=None, max_src_column_width=75, max_name_column_width=55, max_shapes_column_width=80, top_level_events_only=False, time_unit=None, ): """Render the session's spans, one row per span. Every span gets a row rather than one row per name, so the per-span captures the session holds -- the shapes an operation was given, the frames it was called from, the memory it allocated -- can be printed against the span they describe. :meth:`key_averages` is the aggregated view of the same recording. """ return self._ensure_events().table( sort_by=sort_by, row_limit=row_limit, header=header, max_src_column_width=max_src_column_width, max_name_column_width=max_name_column_width, max_shapes_column_width=max_shapes_column_width, top_level_events_only=top_level_events_only, time_unit=time_unit, ) def total_average(self): return self.key_averages().total_average() @property def self_cpu_time_total(self): return self.key_averages().self_cpu_time_total @property def current_action(self): if self.schedule is None: return None return self.schedule(self.step_num) def export_chrome_trace(self, path, torch_compat=False): events = self._ensure_events() if self._sampler is not None: events.samples = self._sampler.samples return events.export_chrome_trace( path, torch_compat=torch_compat, gpu_activities=self.gpu_activities, mem_events=self.mem_events, samples=events.samples, ) def export_tensorboard_trace(self, directory, run_name=None): path = os.fspath(directory) os.makedirs(path, exist_ok=True) name = run_name or "tensorplay" rank, _world = rank_world() if rank is not None: name = f"{name}-rank{rank}" output = os.path.join( path, f"{name}.{socket.gethostname()}.{os.getpid()}.{time.time_ns()}.pt.trace.json", ) self.export_chrome_trace(output, torch_compat=True) return output def export_memory_timeline(self, path): return export_memory_timeline(self.mem_events, path) def memory_summary(self): return memory_summary(self.mem_events, self._ensure_events()) def export_stacks(self, path, metric="self_cpu_time_total"): if not self.with_stack: raise AssertionError("export_stacks() requires with_stack=True") return self._ensure_events().export_stacks(path, metric) [docs] class record_function(ContextDecorator): """Mark a user-defined span in the active profiling session.""" def __init__(self, name, args=None): self.name = str(name) self.args = args self._entered = False def __enter__(self): if self._entered: raise RuntimeError("record_function context manager is not reentrant") _C._profiler_user_begin(self.name) self._entered = True return self def __exit__(self, exc_type, exc, tb): if self._entered: _C._profiler_user_end() self._entered = False return False class _EmitContext(ContextDecorator): _setter = None def __init__(self, enabled=True, record_shapes=False): self.enabled = bool(enabled) self.record_shapes = bool(record_shapes) self._entered = False def __enter__(self): if not self.enabled: return self if self._entered: raise RuntimeError("annotation context manager is not reentrant") self._setter(True) self._entered = True return self def __exit__(self, exc_type, exc, tb): if self._entered: self._setter(False) self._entered = False return False [docs] class emit_nvtx(_EmitContext): """Emit named ranges for active operations.""" _setter = staticmethod(_C._profiler_emit_nvtx) [docs] class emit_itt(_EmitContext): """Emit instrumentation ranges for active operations.""" _setter = staticmethod(_C._profiler_emit_itt) __all__ = [ "DeviceType", "ProfilerActivity", "ProfilerAction", "emit_itt", "emit_nvtx", "merge_distributed_traces", "profile", "record_function", "schedule", "supported_activities", "tensorboard_trace_handler", ] ```