Spaces:
Sleeping
Sleeping
| # SPDX-FileCopyrightText: Copyright (c) 2023 - 2025 NVIDIA CORPORATION & AFFILIATES. | |
| # SPDX-FileCopyrightText: All rights reserved. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| import sys | |
| from contextlib import ContextDecorator, ExitStack | |
| from pathlib import Path | |
| from typing import Any, Callable | |
| from .core import ProfileRegistry, _Profiler_Singleton | |
| try: | |
| import nvtx | |
| nvtx_annotate = nvtx.annotate | |
| except ImportError: | |
| nvtx_avail = False | |
| nvtx_annotate = ContextDecorator | |
| class Profiler(metaclass=_Profiler_Singleton): | |
| """ | |
| Profiler Class to enable easy, simple to configure profiling tools in physicsnemo. | |
| This is meant to be used as a reusable context manager for profile capturing. | |
| Integrate it into StreamCapture for simplest use, after configuration. But, you | |
| don't have to integrate it: This profiler tool could be used to capture profiles from code or | |
| functions in other contexts. | |
| It is not a singleton class, so you could make more than one, but it IS meant to be | |
| reused in a StreamCapture - one per capture. When used this way, it will track entrances/exits | |
| from the profiling context and decorate output traces accordingly. | |
| In other words, you could capture the training and validation loops separately, but the training loop | |
| will use the same profiler each iteration. | |
| It's suggested that you do not profile every time, since that adds some overhead. Instead, use a shorter | |
| run for profiling and then disable the profiler entirely. (pass `enabled=False` in the constructor, which is default.) | |
| """ | |
| # Keep a list of configured, singleton profilers | |
| _profilers = [] | |
| _output_top: Path = Path("./physicsnemo_profiling_outputs/") | |
| # A list of functions to capture for decoration _before_ all the profilers are initialized | |
| _decoration_registry = [] | |
| # Control flow switches for whether the profiler | |
| # has been initialized (and can do annotations/decorations) | |
| # (And if not - they get deferred, see below) | |
| _initialized: bool = False | |
| # Control flow for wrapping up the profiler: closing contexts/ | |
| # writing outputs, etc. Only want to trigger this once | |
| _finalized: bool | |
| annotate = nvtx_annotate | |
| def __init__(self): | |
| self.exit_stack = ExitStack() | |
| def initialized(self) -> bool: | |
| """Get whether the profiler has been initialized. | |
| Returns: | |
| bool: True if the profiler has been initialized, False otherwise | |
| """ | |
| return self._initialized | |
| def initialized(self, value: bool) -> None: | |
| """Set whether the profiler has been initialized. | |
| Args: | |
| value (bool): True to mark as initialized, False otherwise | |
| Raises: | |
| TypeError: If value is not a boolean | |
| """ | |
| if not isinstance(value, bool): | |
| raise TypeError("initialized must be a boolean value") | |
| self._initialized = value | |
| def finalized(self) -> bool: | |
| """Get whether the profiler has been finalized. | |
| Returns: | |
| bool: True if the profiler has been finalized, False otherwise | |
| """ | |
| return self._finalized | |
| def finalized(self, value: bool) -> None: | |
| """Set whether the profiler has been finalized. | |
| Args: | |
| value (bool): True to mark as finalized, False otherwise | |
| Raises: | |
| TypeError: If value is not a boolean | |
| """ | |
| if not isinstance(value, bool): | |
| raise TypeError("finalized must be a boolean value") | |
| self._finalized = value | |
| def _standup(self) -> None: | |
| """Initialize all registered profilers and decorate registered functions. | |
| This internal method handles the initialization of all attached profilers and | |
| decorates any functions that were registered for profiling. Once called, no | |
| additional profilers can be added. | |
| """ | |
| if self.initialized: | |
| return | |
| # Stand up all attached profilers. After this, can't add more. | |
| for p in self._profilers: | |
| p._standup() | |
| for func in self._decoration_registry: | |
| decorated = self._decorate_function(func) | |
| self.replace_function(func, decorated) | |
| self._decoration_registry.clear() | |
| self.initialized = True | |
| def initialize(self) -> None: | |
| """ | |
| Manually initialize the profiler interface | |
| """ | |
| self._standup() | |
| def finalize(self) -> None: | |
| """ | |
| finalize the profiler interface. Writes data to file | |
| if necessary, automatically | |
| """ | |
| for p in self._profilers: | |
| p.finalize(self.output_path) | |
| def step(self) -> None: | |
| """ | |
| For all attached profiling tools, call step if it is available | |
| """ | |
| for p in self._profilers: | |
| p.step() | |
| def enabled(self) -> bool: | |
| """ | |
| Return true if profiling is enabled | |
| """ | |
| enabled = any([p.enabled for p in self._profilers]) | |
| return enabled | |
| def __repr__(self): | |
| """ | |
| Summarize the current profiling interface in a string | |
| """ | |
| name = f"<Profiler at {hex(id(self))}>" | |
| if self.initialized: | |
| s = f"Activated PhysicsNeMo {name} with [{' '.join([str(_P) for _P in self._profilers])}] profilers." | |
| else: | |
| s = f"Un-Activated PhysicsNeMo {name}" | |
| return s | |
| def enable(self, profiler: Any) -> Any: | |
| """ | |
| Enable a profiler. The profiler can be an instance of a class | |
| that derives from the profiler wrapper, or it can be a keyword that | |
| is registered with the profiler manager. | |
| """ | |
| if self.initialized: | |
| raise Exception( | |
| "Can not enable more profiling tools after the profiler interface is initialized" | |
| ) | |
| # Is it an instance of the right type? If not, find it: | |
| if not isinstance(profiler, _Profiler_Singleton): | |
| profiler = ProfileRegistry.get_profiler(profiler) | |
| # make sure the summoned profiler is enabled: | |
| profiler.enabled = True | |
| # Prevent double-adds: | |
| if profiler not in self._profilers: | |
| self._profilers.append(profiler) | |
| return profiler | |
| def get(self, profiler: Any) -> Any: | |
| """ | |
| Use the profiler registry to access a profiler | |
| """ | |
| profiler = ProfileRegistry.get_profiler(profiler) | |
| return profiler | |
| def __enter__(self) -> Any: | |
| """ | |
| Enter profiling contexts | |
| """ | |
| if not self.initialized: | |
| self._standup() | |
| if not self.initialized: | |
| raise RuntimeError("Can not enter a context with an uninitialized profiler") | |
| if not self.enabled: | |
| # An initialized but _empty_ profiler, then. | |
| return self | |
| # Activate context for all attached profilers | |
| # Set nvtx context based on name | |
| # Activate the line_profiler for use as a context | |
| # Capture each context in an exit stack that we'll back out of in the exit. | |
| for p in self._profilers: | |
| if p.enabled and p.is_context: | |
| self.exit_stack.enter_context(p) | |
| return self | |
| def __exit__(self, *exc) -> None: | |
| """ | |
| Clear out the exit stack | |
| """ | |
| if not self.enabled: | |
| return | |
| self.exit_stack.close() | |
| def __del__(self) -> None: | |
| """ | |
| Clean up and ensure results are output, just in case: | |
| """ | |
| try: | |
| self.finalize() | |
| except Exception: | |
| print( | |
| "Profiler Interface failed to cleanup, please call finalize in your code!" | |
| ) | |
| def __call__(self, fn: Callable) -> Callable: | |
| """ | |
| For using the Profiler as a decorator | |
| """ | |
| # For the function decorator, we pass the decoration | |
| # on to active profilers for them to decorate. | |
| # Fires in the order they were activated! | |
| return self._deferred_or_immediate_decoration(fn) | |
| def _deferred_or_immediate_decoration(self, func): | |
| if self.initialized: | |
| return self._decorate_function(func) | |
| else: | |
| self._decoration_registry.append(func) | |
| return func | |
| def replace_function(self, func: Callable, wrapped_func: Callable) -> None: | |
| """Replace a function with its wrapped version in all relevant namespaces. | |
| This method handles replacing both module-level functions and class methods. | |
| For module-level functions, it also updates any references in the __main__ namespace | |
| that may have been created through imports. | |
| Args: | |
| func: The original function to be replaced | |
| wrapped_func: The wrapped version of the function that will replace the original | |
| """ | |
| module_name = func.__module__ | |
| module = sys.modules[module_name] | |
| # Future possible update: wrapt may simplify this. | |
| # wrapt.wrap_function_wrapper( | |
| # func.__module__, | |
| # func.__qualname__, | |
| # lambda _, instance, args, kwargs: wrapped_func(*args, **kwargs) | |
| # ) | |
| if "." in func.__qualname__: | |
| qualname_parts = func.__qualname__.split(".") | |
| # If the object is local, it's annoying. | |
| # Best we can do is update references in the module | |
| # and in the __main__ namespace. | |
| if "<locals>" in qualname_parts: | |
| for name, obj in vars(module).items(): | |
| if obj is func: | |
| setattr(module, name, wrapped_func) | |
| __main__ = sys.modules["__main__"] | |
| for name, obj in vars(__main__).items(): | |
| if obj is func: | |
| setattr(__main__, name, wrapped_func) | |
| return | |
| obj = module | |
| for part in qualname_parts[:-1]: | |
| obj = getattr(obj, part) | |
| setattr(obj, qualname_parts[-1], wrapped_func) | |
| else: | |
| setattr(module, func.__qualname__, wrapped_func) | |
| # In case this function was imported into the main namespace | |
| # (aka, in the executed script `from A import func as weird_name`), there | |
| # is a reference to it there too. Capture it: | |
| __main__ = sys.modules["__main__"] | |
| for name, obj in vars(__main__).items(): | |
| if obj is func: | |
| setattr(__main__, name, wrapped_func) | |
| if hasattr(__main__, func.__qualname__): | |
| setattr(__main__, func.__qualname__, wrapped_func) | |
| def _decorate_function(self, func: Callable) -> Callable: | |
| """Decorate a function with all enabled profilers that support decoration. | |
| This method applies all active profilers that have the `is_decorator` property | |
| set to True. It returns the decorated function that will be used in place of | |
| the original function. | |
| """ | |
| for p in self._profilers: | |
| if p.enabled and p.is_decorator: | |
| func = p(func) | |
| return func | |
| def output_path(self) -> Path: | |
| """Get the output path for profiling results. | |
| Returns: | |
| Path: Path to the output directory | |
| """ | |
| return self._output_top | |
| def output_path(self, path: Path) -> None: | |
| """Set the output path for profiling results. | |
| Args: | |
| path (Path): Path to the output directory | |
| """ | |
| # cast if necessary: | |
| path = Path(path) | |
| self._output_top = path | |