Files

178 lines
6.4 KiB
Python

import gc
from typing import Literal
from typing_extensions import Self
import torch
from torch._C import _acceleratorGraph
class Graph(_acceleratorGraph):
r"""
Wrapper around an :ref:`accelerator<accelerators>` graph that supports capture and replay.
A graph captures a sequence of operations and their dependencies, allowing them to be
replayed efficiently with reduced overhead. This class can be used as a context manager
to automatically capture operations on the current stream.
Arguments:
keep_graph (bool, optional): If ``False``, the underlying graph is destroyed and the
executable graph is instantiated on the GPU at the end of ``capture_end``.
If ``True``, the underlying graph is preserved after ``capture_end``. In this case,
the executable graph is not instantiated automatically; it must be explicitly created
by calling ``instantiate``, or it will be instantiated on the first call to ``replay``.
Defaults to ``False``.
pool (tuple[int, int], optional): Memory pool identifier for this graph. Multiple graphs
can share the same pool by passing the same identifier, which can reduce memory overhead.
Defaults to ``None``.
capture_error_mode (Literal["default", "global", "thread_local", "relaxed"], optional):
Specifies the behavior of graph capture. The exact semantics are backend-specific.
``"default"``: backend-defined default capture behavior.
``"global"``: potentially unsafe API calls are prohibited. Errors may occur if capture
in the current thread affects other threads.
``"thread_local"``: potentially unsafe API calls are prohibited. Errors occur only if
capture in the current thread affects itself.
``"relaxed"``: the current thread is allowed to make potentially unsafe API calls, except
for calls that inherently conflict with stream capture.
Default: ``"default"``.
Example::
>>> # xdoctest: +SKIP
>>> x = torch.zeros([2000], device=0)
>>> stream = torch.Stream()
>>> graph = torch.accelerator.Graph()
>>> with stream, graph:
... x += 1
>>> graph.replay()
"""
def __new__(
cls,
keep_graph: bool = False,
*,
pool: tuple[int, int] | None = None,
capture_error_mode: Literal[
"default", "global", "thread_local", "relaxed"
] = "default",
) -> Self:
return super().__new__(cls, keep_graph)
def __init__(
self,
keep_graph: bool = False,
*,
pool: tuple[int, int] | None = None,
capture_error_mode: Literal[
"default", "global", "thread_local", "relaxed"
] = "default",
) -> None:
super().__init__(keep_graph)
self.graph_pool = pool
self.capture_error_mode = capture_error_mode
# pyrefly: ignore [bad-override]
def capture_begin(self) -> None:
r"""
Begin graph capture on the current stream.
All operations on the current stream after this call will be recorded into the graph until
``capture_end`` is called, using the memory pool and capture error mode provided at construction time.
"""
super().capture_begin(
pool=self.graph_pool, capture_error_mode=self.capture_error_mode
)
def capture_end(self) -> None:
r"""
End graph capture on the current stream of the current device.
After this call, the graph can be replayed via ``replay``.
"""
super().capture_end()
def instantiate(self) -> None:
r"""
Instantiate the underlying graph. Will be called by ``capture_end``
if ``keep_graph=False``, or by ``replay`` if ``keep_graph=True`` and
``instantiate`` has not already been explicitly called.
"""
super().instantiate()
def replay(self) -> None:
r"""Replay the work captured by this graph."""
super().replay()
def reset(self) -> None:
r"""
Delete the graph currently held by this instance.
After this call, the graph can be recaptured. Set :attr:`graph_pool` or
:attr:`capture_error_mode` beforehand to use different settings on the next capture.
"""
super().reset()
def pool(self) -> tuple[int, int]:
r"""
Return an opaque token representing the id of this graph's memory pool.
This id can optionally be passed to another graph's ``capture_begin``,
which hints the other graph may share the same memory pool.
Example::
>>> # xdoctest: +SKIP
>>> g1 = torch.accelerator.Graph()
>>> g1.capture_begin()
>>> # ... operations ...
>>> g1.capture_end()
>>> # Share g1's memory pool with a new graph
>>> pool_id = g1.pool()
>>> g2 = torch.accelerator.Graph(pool=pool_id)
"""
return super().pool()
def enable_debug_mode(self) -> None:
r"""Enable debugging mode for ``debug_dump``."""
return super().enable_debug_mode()
def debug_dump(self, path: str) -> None:
r"""
Dump the captured graph to a file for debugging purposes if the debugging is
enabled via ``enable_debug_mode``.
Arguments:
path (str): Path to dump the graph to.
Example::
>>> # xdoctest: +SKIP
>>> s = torch.Stream()
>>> g = torch.accelerator.Graph()
>>> g.enable_debug_mode()
>>> with s, g:
>>> # ... operations ...
>>> # Dump captured graph to a file "graph_dump.dot"
>>> g.debug_dump("graph_dump.dot")
"""
return super().debug_dump(path)
def __enter__(self) -> None:
torch.accelerator.synchronize()
if torch.compiler.config.force_cudagraph_gc:
# We previously always ran garbage collection here. While this can help
# reclaim accelerator device memory held by dead Python cycles, it is
# very expensive, especially when performing multiple graph captures in sequence.
gc.collect()
torch.accelerator.empty_cache()
torch.accelerator.empty_host_cache()
self.capture_begin()
def __exit__(self, *exc_info: object) -> None:
self.capture_end()
__all__ = ["Graph"]