Initial import: grid-bot — grid trading bot for BTC-USDT on Cifra Markets
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
"""
|
||||
TorchDynamo is a Python-level JIT compiler designed to make unmodified PyTorch programs faster.
|
||||
TorchDynamo hooks into the frame evaluation API in CPython (PEP 523) to dynamically modify Python
|
||||
bytecode right before it is executed. It rewrites Python bytecode in order to extract sequences of
|
||||
PyTorch operations into an FX Graph which is then just-in-time compiled with a customizable backend.
|
||||
It creates this FX Graph through bytecode analysis and is designed to mix Python execution with
|
||||
compiled backends to get the best of both worlds: usability and performance. This allows it to
|
||||
seamlessly optimize PyTorch programs, including those using modern Python features.
|
||||
"""
|
||||
|
||||
import torch
|
||||
|
||||
from . import (
|
||||
aot_compile,
|
||||
bytecode_debugger,
|
||||
config,
|
||||
convert_frame,
|
||||
eval_frame,
|
||||
functional_export,
|
||||
resume_execution,
|
||||
)
|
||||
from .backends.registry import list_backends, lookup_backend, register_backend
|
||||
from .callback import callback_handler, on_compile_end, on_compile_start
|
||||
from .code_context import code_context
|
||||
from .convert_frame import replay
|
||||
from .decorators import (
|
||||
allow_in_graph,
|
||||
assume_constant_result,
|
||||
disable,
|
||||
disable_nested_graph_breaks,
|
||||
disallow_in_graph,
|
||||
dont_skip_tracing,
|
||||
error_on_graph_break,
|
||||
forbid_in_graph,
|
||||
graph_break,
|
||||
is_dynamo_disable_recursive,
|
||||
mark_dynamic,
|
||||
mark_static,
|
||||
mark_static_address,
|
||||
maybe_mark_dynamic,
|
||||
nonstrict_trace,
|
||||
override_cudagraphs,
|
||||
patch_dynamo_config,
|
||||
run,
|
||||
set_stance,
|
||||
skip_frame,
|
||||
step_unsupported,
|
||||
substitute_in_graph,
|
||||
)
|
||||
from .eval_frame import (
|
||||
_reset_guarded_backend_cache,
|
||||
explain,
|
||||
export,
|
||||
is_dynamo_supported,
|
||||
is_inductor_supported,
|
||||
optimize,
|
||||
optimize_assert,
|
||||
OptimizedModule,
|
||||
reset_code,
|
||||
)
|
||||
|
||||
# pyrefly: ignore [deprecated]
|
||||
from .external_utils import is_compiling
|
||||
from .mutation_guard import GenerationTracker
|
||||
from .pgo import reset_code_state
|
||||
from .symbolic_convert import TensorifyState
|
||||
from .utils import (
|
||||
graph_break_reasons,
|
||||
guard_failures,
|
||||
orig_code_map,
|
||||
register_hook_for_recompile_user_context,
|
||||
reset_frame_count,
|
||||
reset_recompile_user_contexts,
|
||||
)
|
||||
|
||||
|
||||
# Register polyfill functions
|
||||
from .polyfills import loader as _ # usort: skip # noqa: F401
|
||||
|
||||
|
||||
__all__ = [
|
||||
"allow_in_graph",
|
||||
"assume_constant_result",
|
||||
"bytecode_debugger",
|
||||
"config",
|
||||
"disable",
|
||||
"disable_nested_graph_breaks",
|
||||
"disallow_in_graph",
|
||||
"dont_skip_tracing",
|
||||
"export",
|
||||
"explain",
|
||||
"forbid_in_graph",
|
||||
"graph_break",
|
||||
"is_compiling",
|
||||
"is_dynamo_disable_recursive",
|
||||
"list_backends",
|
||||
"lookup_backend",
|
||||
"mark_dynamic",
|
||||
"maybe_mark_dynamic",
|
||||
"mark_static",
|
||||
"mark_static_address",
|
||||
"nonstrict_trace",
|
||||
"optimize",
|
||||
"optimize_assert",
|
||||
"OptimizedModule",
|
||||
"patch_dynamo_config",
|
||||
"register_backend",
|
||||
"replay",
|
||||
"reset",
|
||||
"reset_recompile_user_contexts",
|
||||
"run",
|
||||
"override_cudagraphs",
|
||||
"error_on_graph_break",
|
||||
"set_recursion_limit",
|
||||
"set_stance",
|
||||
"skip_frame",
|
||||
"step_unsupported",
|
||||
"substitute_in_graph",
|
||||
]
|
||||
|
||||
# allowlist this for weights_only load of NJTs
|
||||
torch.serialization.add_safe_globals([torch._dynamo.decorators._DimRange])
|
||||
|
||||
if torch.manual_seed is torch.random.manual_seed:
|
||||
import torch.jit._builtins
|
||||
|
||||
# Wrap manual_seed with the disable decorator.
|
||||
# Can't do it at its implementation due to dependency issues.
|
||||
torch.manual_seed = torch._disable_dynamo(torch.manual_seed)
|
||||
# Add the new manual_seed to the builtin registry.
|
||||
torch.jit._builtins._register_builtin(torch.manual_seed, "aten::manual_seed")
|
||||
|
||||
|
||||
def reset() -> None:
|
||||
"""
|
||||
Clear all compile caches and restore initial state. This function is intended
|
||||
to reset Dynamo's state *as if* you had started a fresh process invocation, which
|
||||
makes it good for testing scenarios where you want to behave as if you started
|
||||
a new process. It does NOT affect any file system caches.
|
||||
|
||||
NB: this does NOT reset logging state. Don't use this to test logging
|
||||
initialization/reinitialization.
|
||||
"""
|
||||
# TODO: https://github.com/pytorch/pytorch/issues/139200
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.info("torch._dynamo.reset")
|
||||
with convert_frame.compile_lock:
|
||||
reset_code_caches()
|
||||
convert_frame.input_codes.clear()
|
||||
reset_code_state()
|
||||
convert_frame.output_codes.clear()
|
||||
orig_code_map.clear()
|
||||
guard_failures.clear()
|
||||
graph_break_reasons.clear()
|
||||
resume_execution.ContinueExecutionCache.cache.clear()
|
||||
_reset_guarded_backend_cache()
|
||||
reset_frame_count()
|
||||
torch._dynamo.compiled_autograd.reset()
|
||||
convert_frame.FRAME_COUNTER = 0
|
||||
convert_frame.FRAME_COMPILE_COUNTER.clear()
|
||||
callback_handler.clear()
|
||||
GenerationTracker.clear()
|
||||
TensorifyState.clear()
|
||||
torch._dynamo.utils.warn_once_cache.clear()
|
||||
torch._C._autograd._saved_tensors_hooks_set_tracing(False)
|
||||
|
||||
# Reset cudagraph trees unconditionally since they are global state
|
||||
# not tied to a specific backend instance
|
||||
from torch._higher_order_ops.triton_kernel_wrap import kernel_side_table
|
||||
from torch._higher_order_ops.wrap import inductor_code_side_table
|
||||
|
||||
kernel_side_table.reset_table()
|
||||
inductor_code_side_table.reset_table()
|
||||
|
||||
if torch.cuda.is_available():
|
||||
from torch._inductor.cudagraph_trees import reset_cudagraph_trees
|
||||
|
||||
reset_cudagraph_trees()
|
||||
|
||||
|
||||
def reset_code_caches() -> None:
|
||||
"""
|
||||
Clears in-memory code cache, which is what stores compiled products. This
|
||||
resets less state than :func:`reset` and is mostly only used for testing
|
||||
purposes.
|
||||
"""
|
||||
# TODO: https://github.com/pytorch/pytorch/issues/139200
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.info("torch._dynamo.reset_code_caches")
|
||||
"""Clear compile caches that are keyed by code objects"""
|
||||
with convert_frame.compile_lock:
|
||||
reset_code_state()
|
||||
for weak_code in (
|
||||
convert_frame.input_codes.seen + convert_frame.output_codes.seen
|
||||
):
|
||||
code = weak_code()
|
||||
if code:
|
||||
reset_code(code)
|
||||
code_context.clear()
|
||||
|
||||
|
||||
def get_recursion_limit() -> int:
|
||||
"""
|
||||
Returns the internal dynamo recursion limit set by `torch._dynamo.set_recursion_limit`.
|
||||
|
||||
Returns -1 if no c recursion limit has been set.
|
||||
"""
|
||||
return torch._C._dynamo.eval_frame.get_c_recursion_limit()
|
||||
|
||||
|
||||
def set_recursion_limit(limit: int) -> None:
|
||||
"""
|
||||
Sets an internal dynamo recursion limit. The limit must be >= 1, or -1 to reset
|
||||
to the default (unset) state.
|
||||
|
||||
This is possibly needed in Python 3.12-3.13 since there is a separate C recursion limit
|
||||
that is not visible at the Python level. If you are getting RecursionErrors during
|
||||
Dynamo compilation and `sys.setrecursionlimit()` doesn't help, this function may alleviate
|
||||
the issue.
|
||||
|
||||
NOTE: this function does NOT call `sys.setrecursionlimit()` - the user is expected to manually
|
||||
call this if required. This is because the 2 recursion limits are not sync'd up - e.g. in
|
||||
Python 3.12, functions can be inline-evaluated, which apparently doesn't use up the C stack.
|
||||
|
||||
WARNING: increasing the recursion limit to an arbitrary large value may cause segfaults
|
||||
due to stack overflows! You can try also try to manually increase the stack size, e.g.
|
||||
with `$ ulimit -s ...`
|
||||
"""
|
||||
torch._C._dynamo.eval_frame.set_c_recursion_limit(limit)
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
"""trace_wrapped(*args, fn) is equivalent to fn(*args), but with a twist:
|
||||
if you make_fx trace through this call, we will not actually trace into fn; instead,
|
||||
we will directly insert it as a call_function to fn in the graph.
|
||||
(Unlike make_fx, Dynamo WILL inline into fn.)
|
||||
You can think of this as a one off allow_in_graph equivalent for proxy tensor tracing.
|
||||
|
||||
Because proxy tensor tracing does not actually run the function, there are
|
||||
requirements on the behavior of fn. We are still figuring it out, but here is the current state:
|
||||
|
||||
1) fn SHOULD only take a single argument, which must be a tensor
|
||||
2) fn MUST return a new tensor with the same metadata as the original tensor
|
||||
(e.g., zeros_like(input) is a permissible implementation of fn).
|
||||
This is verified via an extra assert that is inserted into the traced graph.
|
||||
3) fn MAY have side effects, but it MAY NOT perform metadata mutation on other tensors
|
||||
participating in proxy tensor tracing (it MAY mutate other tensors, it MAY mutate Python state)
|
||||
These requirements stem from the requirement that we need to continue performing proxy tensor tracing,
|
||||
which assumes accurate fake tensor metadata, without actually running fn.
|
||||
In the future, we may allow for a "meta" function associated with fn to allow for more interesting input-output patterns.
|
||||
|
||||
Note that tensors / Python state are allowed to be mutated.
|
||||
This is relaxed constraint is not always sound, but it is sound for backward tracing with fake
|
||||
tensors as it takes place in AOTAutograd, as the backward pass is guaranteed not to depend on concrete
|
||||
tensor values (via fake tensor) or Python state (because the autograd engine doesn't depend on Python).
|
||||
|
||||
The intended use case for this function is to allow AOTAutograd to defer complex
|
||||
backward hooks to compiled autograd. AOTAutograd performs a make_fx trace which preserves
|
||||
the function call as is in the graph, and only when we Dynamo through the backward graph in
|
||||
compiled autograd do we inline into the function.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.utils._pytree as pytree
|
||||
from torch._C import DispatchKey
|
||||
from torch._higher_order_ops.utils import autograd_not_implemented
|
||||
from torch._ops import HigherOrderOperator, OpOverload
|
||||
from torch._subclasses import FakeTensorMode
|
||||
from torch.fx.experimental._backward_state import BackwardState
|
||||
from torch.fx.experimental.proxy_tensor import ProxyTorchDispatchMode, track_tensor_tree
|
||||
from torch.overrides import TorchFunctionMode
|
||||
from torch.utils._python_dispatch import _get_current_dispatch_mode
|
||||
from torch.utils._pytree import tree_map_only
|
||||
|
||||
|
||||
Tensor = torch.Tensor
|
||||
|
||||
|
||||
__all__ = ["trace_wrapped"]
|
||||
|
||||
|
||||
@torch.library.custom_op("flex_lib::zeros_and_scatter", mutates_args=()) # type: ignore[misc]
|
||||
def zeros_and_scatter(
|
||||
shape: list[int],
|
||||
indices: list[Tensor],
|
||||
vals: Tensor,
|
||||
) -> Tensor:
|
||||
"""Custom Op so that we can register a custom lowering for the new_output + scatter in the backwards pass"""
|
||||
grad = torch.zeros(shape, device=vals.device, dtype=vals.dtype)
|
||||
return torch.ops.aten.index_put(grad, indices, vals, accumulate=True)
|
||||
|
||||
|
||||
@zeros_and_scatter.register_fake # type: ignore[misc]
|
||||
def _(
|
||||
shape: list[int],
|
||||
indices: list[Tensor],
|
||||
vals: Tensor,
|
||||
) -> Tensor:
|
||||
return vals.new_empty(shape)
|
||||
|
||||
|
||||
@zeros_and_scatter.register_vmap # type: ignore[misc]
|
||||
def _(info, indims, shape, indices, value): # type: ignore[no-untyped-def]
|
||||
"""The batching rule is special in that it returns a tensor that is not batched"""
|
||||
indices_indims = indims[1]
|
||||
expanded_indices = []
|
||||
for idx, idx_indim in zip(indices, indices_indims):
|
||||
# The index is not a being batched, we should unsqueeze and expand to val
|
||||
if idx_indim is None:
|
||||
expanded_indices.append(idx.expand(value.shape))
|
||||
else:
|
||||
# the index is being part of the vmap batch, it should be the same size as val
|
||||
assert idx.shape == value.shape
|
||||
expanded_indices.append(idx)
|
||||
|
||||
out = torch.ops.flex_lib.zeros_and_scatter(
|
||||
shape,
|
||||
expanded_indices,
|
||||
value,
|
||||
)
|
||||
return out, None
|
||||
|
||||
|
||||
class ModIndex(torch.autograd.Function):
|
||||
generate_vmap_rule = True
|
||||
|
||||
@staticmethod
|
||||
# pyrefly: ignore [bad-override]
|
||||
def forward(x: Tensor, indices: list[Tensor]) -> Tensor:
|
||||
return torch.ops.aten.index(x, indices)
|
||||
|
||||
@staticmethod
|
||||
def setup_context(ctx: Any, inputs: tuple[Any, ...], output: Any) -> None:
|
||||
x, indices = inputs
|
||||
ctx.save_for_backward(*indices)
|
||||
ctx.input_shape = x.shape
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, gradOut): # type: ignore[no-untyped-def]
|
||||
indices = ctx.saved_tensors
|
||||
return (
|
||||
torch.ops.flex_lib.zeros_and_scatter(
|
||||
ctx.input_shape,
|
||||
indices,
|
||||
gradOut,
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@torch._export.wrappers.allow_in_pre_dispatch_graph
|
||||
def apply(cls, *args, **kwargs): # type: ignore[no-untyped-def]
|
||||
return super().apply(*args, **kwargs)
|
||||
|
||||
|
||||
mod_index = ModIndex.apply
|
||||
|
||||
|
||||
class TransformGetItemToIndex(TorchFunctionMode):
|
||||
# This is needed since we want to support calling
|
||||
# A[q_idx], where q_idx is a scalar tensor in score_mod.
|
||||
# Today, when q_idx is a scalar tensor, we implicitly convert it to a python
|
||||
# scalar and create a view. We do not want that behavior in this case, so we
|
||||
# use this torchfunctionmode to override that behavior for score_mod
|
||||
# wherever we're running it.
|
||||
#
|
||||
# We also convert integer indices to 0-D tensors so that temp[0] produces
|
||||
# the same backward graph as temp[0 * q_idx] (zeros_and_scatter with atomic_add).
|
||||
def __torch_function__(
|
||||
self,
|
||||
func: OpOverload,
|
||||
types: tuple[torch._C._TensorMeta, ...],
|
||||
args: tuple[object, ...] = (),
|
||||
kwargs: dict[str, object] | None = None,
|
||||
) -> object:
|
||||
if func is torch.Tensor.__getitem__:
|
||||
tensor_to_index = args[0]
|
||||
assert isinstance(tensor_to_index, torch.Tensor)
|
||||
index_args = pytree.tree_leaves(args[1])
|
||||
if all(isinstance(x, (torch.Tensor, int)) for x in index_args):
|
||||
converted_indices = [
|
||||
torch.tensor(x, dtype=torch.int64, device=tensor_to_index.device)
|
||||
if isinstance(x, int)
|
||||
else x
|
||||
for x in index_args
|
||||
]
|
||||
return mod_index(tensor_to_index, converted_indices)
|
||||
return func(*args, **(kwargs or {}))
|
||||
|
||||
|
||||
def trace_wrapped(*args: Any, **kwargs: Any) -> Any:
|
||||
with torch.no_grad():
|
||||
return _trace_wrapped_op(*args, **kwargs)
|
||||
|
||||
|
||||
class TraceWrapped(HigherOrderOperator):
|
||||
def __init__(self) -> None:
|
||||
super().__init__("trace_wrapped")
|
||||
|
||||
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return super().__call__(*args, **kwargs)
|
||||
|
||||
|
||||
# TODO(jansel): need to ensure this does not get DCEed
|
||||
_trace_wrapped_op = TraceWrapped()
|
||||
|
||||
|
||||
def _assert_meta(
|
||||
grad: torch.Tensor,
|
||||
size: tuple[int, ...],
|
||||
stride: tuple[int, ...],
|
||||
dtype: torch.dtype,
|
||||
) -> torch.Tensor:
|
||||
assert grad.size() == size, "size mismatch"
|
||||
assert grad.stride() == stride, "stride mismatch"
|
||||
assert grad.dtype == dtype, "dtype mismatch"
|
||||
return grad
|
||||
|
||||
|
||||
@_trace_wrapped_op.py_impl(ProxyTorchDispatchMode)
|
||||
def inner_trace(
|
||||
mode: ProxyTorchDispatchMode,
|
||||
*args: Any,
|
||||
bw_state: BackwardState | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
def self_invoke(*args: Any, **dyn_kwargs: Any) -> Any:
|
||||
with torch.no_grad():
|
||||
return _trace_wrapped_op(*args, **dyn_kwargs, **kwargs)
|
||||
|
||||
def unwrap_proxies(x: Any) -> Any:
|
||||
if isinstance(x, torch.Tensor):
|
||||
return mode.tracer.unwrap_proxy(x) # type: ignore[union-attr]
|
||||
if isinstance(x, (list, tuple)):
|
||||
return type(x)(map(unwrap_proxies, x))
|
||||
if x is None:
|
||||
return None
|
||||
raise AssertionError(f"unhandled type: {type(x)}")
|
||||
|
||||
proxy_kwargs = {}
|
||||
if bw_state is not None:
|
||||
assert isinstance(bw_state, BackwardState) and bw_state.proxy is not None
|
||||
proxy_kwargs["bw_state"] = bw_state.proxy
|
||||
out_proxy = mode.tracer.create_proxy(
|
||||
"call_function",
|
||||
self_invoke,
|
||||
unwrap_proxies(args),
|
||||
proxy_kwargs,
|
||||
name="trace_wrapped",
|
||||
)
|
||||
|
||||
if args[0] is None:
|
||||
grad = args[1] # module backward hooks
|
||||
else:
|
||||
grad = args[0] # other backward hooks
|
||||
grad = tree_map_only(torch.Tensor, torch.empty_like, grad)
|
||||
track_tensor_tree(grad, out_proxy, constant=None, tracer=mode.tracer)
|
||||
return grad
|
||||
|
||||
|
||||
@_trace_wrapped_op.py_impl(FakeTensorMode)
|
||||
def inner_fake(*args: Any, **kwargs: Any) -> None:
|
||||
raise RuntimeError("This op should never be invoked here")
|
||||
|
||||
|
||||
@_trace_wrapped_op.py_impl(DispatchKey.CompositeExplicitAutograd)
|
||||
def _trace_wrapped_op_dense(*args: Any, fn: Any, **kwargs: Any) -> Any:
|
||||
mode = _get_current_dispatch_mode()
|
||||
assert mode is None, "Mode should never be enabled for CPU/CUDA key"
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
|
||||
_trace_wrapped_op.py_impl(DispatchKey.Autograd)(
|
||||
autograd_not_implemented(_trace_wrapped_op, deferred_error=True)
|
||||
)
|
||||
|
||||
|
||||
@_trace_wrapped_op.py_functionalize_impl
|
||||
def _trace_wrapped_functionalized(ctx: Any, *args: Any, **kwargs: Any) -> Any:
|
||||
unwrapped_args = ctx.unwrap_tensors(args)
|
||||
with ctx.redispatch_to_next():
|
||||
return ctx.wrap_tensors(_trace_wrapped_op(*unwrapped_args, **kwargs))
|
||||
|
||||
|
||||
def autograd_function_backward_rewritten(original_backward: Any) -> Any:
|
||||
def new_backward(ctx: Any, *grads: Any) -> Any:
|
||||
# pyrefly: ignore [bad-assignment]
|
||||
grads = [g.contiguous() for g in grads]
|
||||
return original_backward(ctx, *grads)
|
||||
|
||||
return new_backward
|
||||
@@ -0,0 +1,539 @@
|
||||
import dataclasses
|
||||
import importlib
|
||||
import inspect
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
import tempfile
|
||||
import types
|
||||
from collections.abc import Callable, Sequence
|
||||
from contextlib import AbstractContextManager, ExitStack, nullcontext
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
import torch.fx
|
||||
from torch._dynamo.convert_frame import GraphRuntimeEnv
|
||||
from torch._dynamo.graph_utils import _graph_device_type
|
||||
from torch._dynamo.package import SystemInfo
|
||||
|
||||
from . import convert_frame
|
||||
from .aot_compile_types import (
|
||||
BundledAOTAutogradSerializableCallable,
|
||||
SerializableCallable,
|
||||
)
|
||||
from .hooks import Hooks
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .guards import GuardManagerWrapper
|
||||
from .package import SerializedCode, SourceInfo
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def bind_locals(
|
||||
signature: inspect.Signature, *args: Any, **kwargs: Any
|
||||
) -> dict[str, Any]:
|
||||
bound_arguments = signature.bind(*args, **kwargs)
|
||||
bound_arguments.apply_defaults()
|
||||
return bound_arguments.arguments
|
||||
|
||||
|
||||
@dataclass
|
||||
class CompileArtifacts:
|
||||
signature: inspect.Signature
|
||||
guard_manager: Optional["GuardManagerWrapper"]
|
||||
guards_state: bytes
|
||||
backend_id: str
|
||||
compiled_fn: SerializableCallable
|
||||
original_code: types.CodeType
|
||||
runtime_env: GraphRuntimeEnv
|
||||
source_info: "SourceInfo"
|
||||
device_type: str
|
||||
backend_name: str
|
||||
system_info: SystemInfo = dataclasses.field(default_factory=SystemInfo.current)
|
||||
|
||||
def check_compatibility(self) -> None:
|
||||
current_system = SystemInfo.current()
|
||||
current_system.check_compatibility(self.system_info, self.device_type)
|
||||
|
||||
|
||||
class AOTCompilePickler(pickle.Pickler):
|
||||
def __init__(self, external_data: dict[str, object], buf: io.BytesIO) -> None:
|
||||
super().__init__(buf)
|
||||
self.external_data = external_data
|
||||
self.id_map: dict[int, str] = {
|
||||
id(value): key for key, value in external_data.items()
|
||||
}
|
||||
self.errors = {}
|
||||
|
||||
def persistent_id(self, obj: object) -> int | str | None:
|
||||
if id(obj) in self.id_map:
|
||||
return self.id_map[id(obj)]
|
||||
elif isinstance(obj, torch.nn.Module):
|
||||
self.errors[id(obj)] = obj
|
||||
return id(obj)
|
||||
else:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _unpickle_cell(cls, val: object) -> object:
|
||||
def _() -> object:
|
||||
return val
|
||||
|
||||
assert _.__closure__ is not None
|
||||
return _.__closure__[0]
|
||||
|
||||
@classmethod
|
||||
# pyrefly: ignore [implicit-any]
|
||||
def _unpickle_bound_method(cls, func: Callable, base: object) -> types.MethodType:
|
||||
return types.MethodType(func, base)
|
||||
|
||||
@classmethod
|
||||
def _unpickle_module(cls, name: str) -> types.ModuleType:
|
||||
return importlib.import_module(name)
|
||||
|
||||
@classmethod
|
||||
def _unpickle_code(cls, serialized_code: "SerializedCode") -> types.CodeType:
|
||||
from torch._dynamo.package import SerializedCode
|
||||
|
||||
return SerializedCode.to_code_object(serialized_code)
|
||||
|
||||
@classmethod
|
||||
def _unpickle_nested_function(
|
||||
cls,
|
||||
code: types.CodeType,
|
||||
module: str,
|
||||
qualname: str,
|
||||
argdefs: tuple[object, ...] | None,
|
||||
closure: tuple[types.CellType, ...] | None,
|
||||
) -> types.FunctionType:
|
||||
f_globals = importlib.import_module(module).__dict__
|
||||
return types.FunctionType(code, f_globals, qualname, argdefs, closure)
|
||||
|
||||
# pyrefly: ignore [bad-override]
|
||||
def reducer_override(self, obj: Any) -> Any:
|
||||
if isinstance(obj, type((lambda x: lambda: x)(0).__closure__[0])): # type: ignore[index] # noqa: PLC3002
|
||||
return type(self)._unpickle_cell, (obj.cell_contents,)
|
||||
elif inspect.iscode(obj):
|
||||
from torch._dynamo.package import SerializedCode
|
||||
|
||||
return type(self)._unpickle_code, (SerializedCode.from_code_object(obj),)
|
||||
|
||||
elif inspect.ismodule(obj):
|
||||
return type(self)._unpickle_module, (obj.__name__,)
|
||||
elif inspect.ismethod(obj):
|
||||
"""
|
||||
By default, pickle will call getattr() directly on the self object
|
||||
for pickling bounded methods, this is not what we want, instead we
|
||||
always want to serialize the original function and the self object
|
||||
in their original form.
|
||||
"""
|
||||
func = obj.__func__
|
||||
method_self = obj.__self__
|
||||
inner_func = getattr(method_self, func.__name__)
|
||||
if inspect.ismethod(inner_func):
|
||||
inner_func = inner_func.__func__
|
||||
if func is not inner_func:
|
||||
return type(self)._unpickle_bound_method, (func, method_self)
|
||||
elif inspect.isfunction(obj):
|
||||
if "<locals>" in obj.__qualname__:
|
||||
return type(self)._unpickle_nested_function, (
|
||||
obj.__code__,
|
||||
obj.__module__,
|
||||
obj.__qualname__,
|
||||
obj.__defaults__,
|
||||
obj.__closure__,
|
||||
)
|
||||
|
||||
return NotImplemented
|
||||
|
||||
|
||||
class AOTCompileUnpickler(pickle.Unpickler):
|
||||
def __init__(self, external_data: dict[str, object], file: io.BytesIO) -> object:
|
||||
super().__init__(file)
|
||||
self.external_data = external_data
|
||||
|
||||
def persistent_load(self, key: str) -> object:
|
||||
if key not in self.external_data:
|
||||
raise RuntimeError(
|
||||
f"Missing required external reference to data: {key}. "
|
||||
"Please load AOT compiled function with "
|
||||
"`external_data=<external data dictionary>`"
|
||||
f"{self.external_data}"
|
||||
)
|
||||
return self.external_data[key]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AOTCompileSaveResult:
|
||||
serialized_data: bytes
|
||||
|
||||
|
||||
def atomic_write_binary(file_path: str, data: bytes):
|
||||
dir_name = os.path.dirname(file_path) or "."
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
dir=dir_name, delete=False, mode="wb"
|
||||
) as temp_file:
|
||||
temp_path = temp_file.name
|
||||
temp_file.write(data)
|
||||
temp_file.flush()
|
||||
os.fsync(temp_file.fileno())
|
||||
|
||||
os.replace(temp_path, file_path)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AOTCompiledFunction:
|
||||
_artifacts: CompileArtifacts
|
||||
_guard_check_enabled: bool = True
|
||||
_extra_globals: dict[str, object] | None = None
|
||||
|
||||
def prepare_f_locals(self, *args: object, **kwargs: object) -> dict[str, object]:
|
||||
f_locals: dict[str, object] = {}
|
||||
env = self._artifacts.runtime_env
|
||||
if env.closure:
|
||||
assert env.bytecode.co_freevars and len(env.closure) == len(
|
||||
env.bytecode.co_freevars
|
||||
)
|
||||
f_locals = {
|
||||
name: cell.cell_contents
|
||||
for name, cell in zip(env.bytecode.co_freevars, env.closure)
|
||||
}
|
||||
f_locals.update(bind_locals(self._artifacts.signature, *args, **kwargs))
|
||||
return f_locals
|
||||
|
||||
def guard_check(self, *args: Any, **kwargs: Any) -> bool:
|
||||
f_locals = self.prepare_f_locals(*args, **kwargs)
|
||||
assert self._artifacts.guard_manager is not None
|
||||
return self._artifacts.guard_manager.check(f_locals)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
from .package import load_guard_manager, load_guards_state
|
||||
|
||||
self._artifacts.check_compatibility()
|
||||
|
||||
self.fn = self._artifacts.runtime_env.forward_callable(
|
||||
self._artifacts.backend_id,
|
||||
self._artifacts.compiled_fn,
|
||||
extra_globals=self._extra_globals,
|
||||
)
|
||||
|
||||
if self._artifacts.guard_manager is None:
|
||||
guards_state = load_guards_state(self._artifacts.guards_state)
|
||||
self._artifacts.guard_manager = load_guard_manager(
|
||||
guards_state,
|
||||
self._artifacts.original_code,
|
||||
self.fn.__globals__,
|
||||
)
|
||||
|
||||
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
||||
assert self._artifacts.guard_manager is not None
|
||||
if self._guard_check_enabled and not self.guard_check(*args, **kwargs):
|
||||
f_locals = self.prepare_f_locals(*args, **kwargs)
|
||||
reason = str(self._artifacts.guard_manager.check_verbose(f_locals))
|
||||
raise RuntimeError(f"GuardManager check failed, reason: {reason}")
|
||||
return self.fn(*args, **kwargs)
|
||||
|
||||
def source_info(self) -> "SourceInfo":
|
||||
return self._artifacts.source_info
|
||||
|
||||
def save_compiled_function(
|
||||
self, path: str, external_data: dict[str, Any] | None = None
|
||||
) -> AOTCompileSaveResult:
|
||||
result = type(self).serialize(self, external_data)
|
||||
atomic_write_binary(path, result.serialized_data)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def serialize(
|
||||
cls, fn: "AOTCompiledFunction", external_data: dict[str, Any] | None = None
|
||||
) -> AOTCompileSaveResult:
|
||||
from torch._dynamo.package import SerializedCode
|
||||
|
||||
state = fn._artifacts.__dict__.copy()
|
||||
state["guard_manager"] = None
|
||||
state["runtime_env"] = dataclasses.replace(
|
||||
state["runtime_env"],
|
||||
bytecode=SerializedCode.from_code_object(state["runtime_env"].bytecode),
|
||||
)
|
||||
compiled_fn = state["compiled_fn"]
|
||||
state["compiled_fn"] = (
|
||||
type(compiled_fn).deserialize_compile_artifacts,
|
||||
type(compiled_fn).serialize_compile_artifacts(compiled_fn),
|
||||
)
|
||||
state["original_code"] = SerializedCode.from_code_object(state["original_code"])
|
||||
buf = io.BytesIO()
|
||||
pickler = AOTCompilePickler(external_data or {}, buf)
|
||||
pickler.dump(state)
|
||||
if pickler.errors:
|
||||
raise RuntimeError(
|
||||
f"Failed to serialize the following objects: {list(pickler.errors.values())}\n"
|
||||
"Please mark these as external data by using `external_data={'key': ...}`"
|
||||
)
|
||||
return AOTCompileSaveResult(serialized_data=buf.getvalue())
|
||||
|
||||
@classmethod
|
||||
def deserialize(
|
||||
cls,
|
||||
data: bytes,
|
||||
f_globals: dict[str, object] | None = None,
|
||||
external_closure_data: dict[str, Any] | None = None,
|
||||
) -> "AOTCompiledFunction":
|
||||
from torch._dynamo.package import SerializedCode
|
||||
|
||||
f = io.BytesIO(data)
|
||||
f.seek(0)
|
||||
unpickler = AOTCompileUnpickler(external_closure_data or {}, f)
|
||||
state = unpickler.load()
|
||||
f.close()
|
||||
state["runtime_env"] = dataclasses.replace(
|
||||
state["runtime_env"],
|
||||
bytecode=SerializedCode.to_code_object(state["runtime_env"].bytecode),
|
||||
)
|
||||
deserializer, compiled_fn_state = state["compiled_fn"]
|
||||
with torch._inductor.config.patch(enable_autograd_for_aot=True):
|
||||
state["compiled_fn"] = deserializer(compiled_fn_state)
|
||||
state["original_code"] = SerializedCode.to_code_object(state["original_code"])
|
||||
|
||||
artifacts = CompileArtifacts(**state)
|
||||
return cls(artifacts, _extra_globals=f_globals)
|
||||
|
||||
def disable_guard_check(self) -> None:
|
||||
self._guard_check_enabled = False
|
||||
|
||||
|
||||
def aot_compile_fullgraph(
|
||||
model: Any,
|
||||
example_inputs: tuple[tuple[Any, ...], dict[str, Any]],
|
||||
hooks: Hooks,
|
||||
backend: Callable[[torch.fx.GraphModule, list[torch.Tensor]], SerializableCallable],
|
||||
dynamic: bool | None = None,
|
||||
) -> AOTCompiledFunction:
|
||||
from torch._dynamo.guards import CheckFunctionManager
|
||||
from torch._dynamo.package import SourceInfo
|
||||
from torch._dynamo.utils import dynamo_timed, get_metrics_context
|
||||
from torch._dynamo.variables.torch_function import (
|
||||
torch_function_mode_stack_state_mgr,
|
||||
)
|
||||
from torch._guards import TracingContext
|
||||
|
||||
args, kwargs = example_inputs
|
||||
|
||||
dynamic_ctx = nullcontext()
|
||||
if dynamic is not None:
|
||||
from torch._dynamo.eval_frame import set_enable_dynamic
|
||||
|
||||
dynamic_ctx = set_enable_dynamic(dynamic)
|
||||
|
||||
with (
|
||||
get_metrics_context(),
|
||||
dynamo_timed("fullgraph_capture"),
|
||||
torch._functorch.config.patch(strict_autograd_cache=True),
|
||||
dynamic_ctx,
|
||||
torch_function_mode_stack_state_mgr,
|
||||
):
|
||||
capture_output = convert_frame.fullgraph_capture(model, args, kwargs)
|
||||
graph_capture_output = capture_output.graph_capture_output
|
||||
assert graph_capture_output.output_graph is not None
|
||||
|
||||
if not hooks.guard_filter_fn:
|
||||
from torch._dynamo.types import GuardFilterEntry
|
||||
|
||||
def new_guard_filter_fn(
|
||||
guard_entries: Sequence[GuardFilterEntry],
|
||||
) -> Sequence[bool]:
|
||||
return [
|
||||
(
|
||||
not (
|
||||
g.is_global
|
||||
or g.guard_type
|
||||
in CheckFunctionManager.UNSUPPORTED_SERIALIZATION_GUARD_TYPES
|
||||
)
|
||||
)
|
||||
for g in guard_entries
|
||||
]
|
||||
|
||||
hooks.guard_filter_fn = new_guard_filter_fn
|
||||
|
||||
fn, _ = convert_frame.get_traced_fn(model)
|
||||
|
||||
backend_input = capture_output.backend_input
|
||||
assert backend_input is not None
|
||||
backend_input.graph_module._backend_id = backend_input.backend_id # type: ignore[assignment]
|
||||
device_type = _graph_device_type(backend_input.graph_module.graph)
|
||||
assert (
|
||||
backend_input.fake_mode.shape_env
|
||||
is graph_capture_output.output_graph.shape_env
|
||||
)
|
||||
tracing_context = TracingContext(backend_input.fake_mode)
|
||||
tracing_context.tensor_to_context = backend_input.tensor_to_context
|
||||
with (
|
||||
torch._guards.tracing(tracing_context),
|
||||
torch._functorch.config.patch(
|
||||
{
|
||||
"strict_autograd_cache": True,
|
||||
"bypass_autograd_cache_key": True,
|
||||
"bundled_autograd_cache": True,
|
||||
"force_non_lazy_backward_lowering": True,
|
||||
"force_autograd_cache": True,
|
||||
}
|
||||
),
|
||||
):
|
||||
compiled_fn = backend(
|
||||
backend_input.graph_module, backend_input.example_inputs
|
||||
)
|
||||
# If Inductor backend or AOTAutograd-based backend is used,
|
||||
# wrap the compiled_fn for serialization.
|
||||
# TODO: this should be replaced once we make the backend return the SerializableCallable directly.
|
||||
if (
|
||||
isinstance(backend, torch._TorchCompileInductorWrapper)
|
||||
or (
|
||||
hasattr(backend, "compiler_fn")
|
||||
and isinstance(
|
||||
backend.compiler_fn, torch._dynamo.backends.common.AotAutograd
|
||||
)
|
||||
)
|
||||
or (
|
||||
hasattr(compiled_fn, "serialize")
|
||||
and compiled_fn.serialize is not None
|
||||
)
|
||||
):
|
||||
compiled_fn = BundledAOTAutogradSerializableCallable(compiled_fn)
|
||||
|
||||
if not isinstance(compiled_fn, SerializableCallable):
|
||||
if hasattr(backend, "compiler_fn"):
|
||||
compiler_fn = backend.compiler_fn
|
||||
else:
|
||||
compiler_fn = backend
|
||||
raise RuntimeError(
|
||||
f"Compiled function type {type(compiled_fn)} (produced "
|
||||
+ f"from backend {compiler_fn}) does not implement SerializableCallable."
|
||||
)
|
||||
|
||||
# Temporarily restore the mode stack so guard expressions that
|
||||
# reference modes can evaluate, matching the compile_inner path.
|
||||
build_guards_ctx = ExitStack()
|
||||
if torch_function_mode_stack_state_mgr.stack:
|
||||
build_guards_ctx.enter_context(
|
||||
torch_function_mode_stack_state_mgr.temp_restore_stack()
|
||||
)
|
||||
with build_guards_ctx:
|
||||
check_fn = graph_capture_output.build_guards(
|
||||
fn.__code__, hooks=hooks, save=True, strict_error=True
|
||||
)
|
||||
|
||||
assert check_fn.guards_state is not None
|
||||
|
||||
source_info = SourceInfo(inlined_sources=set())
|
||||
for traced_code in graph_capture_output.traced_code:
|
||||
source_info.add_code(traced_code)
|
||||
|
||||
artifacts = CompileArtifacts(
|
||||
signature=convert_frame._get_signature(fn),
|
||||
guard_manager=check_fn.guard_manager,
|
||||
guards_state=check_fn.guards_state,
|
||||
backend_id=backend_input.backend_id,
|
||||
compiled_fn=compiled_fn,
|
||||
original_code=fn.__code__,
|
||||
runtime_env=graph_capture_output.get_runtime_env(),
|
||||
source_info=source_info,
|
||||
device_type=device_type,
|
||||
backend_name=getattr(backend, "compiler_name", "unknown"),
|
||||
)
|
||||
aot_compiled_fn = AOTCompiledFunction(
|
||||
_artifacts=artifacts, _extra_globals=fn.__globals__
|
||||
)
|
||||
|
||||
return aot_compiled_fn
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelInput:
|
||||
"""
|
||||
WIP type: represents a single model input
|
||||
Which consists of a tuple of arguments and a set of contexts in which to run the model.
|
||||
|
||||
For each ModelInput, we'll compile one full graph of the model, and then use the guards generated
|
||||
to dispatch between the compiled graphs.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
args: tuple[Any]
|
||||
kwargs: dict[str, Any]
|
||||
contexts: list[AbstractContextManager[Any]]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AOTCompiledModel:
|
||||
# Represents a single forward function of a model along with dispatch
|
||||
# compiled_results is serializable. We require the model to deserialize again.
|
||||
model: torch.nn.Module
|
||||
compiled_results: list[AOTCompiledFunction]
|
||||
|
||||
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
||||
for result in self.compiled_results:
|
||||
if result.guard_check(self.model, *args, **kwargs):
|
||||
return result(self.model, *args, **kwargs)
|
||||
# All guards failed, just run one of them and throw the guard check error.
|
||||
return self.compiled_results[0](self.model, *args, **kwargs)
|
||||
|
||||
def serialize(self) -> bytes:
|
||||
data: list[bytes] = []
|
||||
for result in self.compiled_results:
|
||||
data.append(AOTCompiledFunction.serialize(result).serialized_data)
|
||||
return pickle.dumps(data)
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, model: torch.nn.Module, data: bytes) -> "AOTCompiledModel":
|
||||
from torch._dynamo.utils import get_metrics_context
|
||||
from torch._guards import compile_context, CompileContext
|
||||
|
||||
results: list[bytes] = pickle.loads(data)
|
||||
compiled_results = []
|
||||
for result in results:
|
||||
with (
|
||||
compile_context(CompileContext(convert_frame.get_compile_id({}))),
|
||||
get_metrics_context(),
|
||||
):
|
||||
compiled_results.append(AOTCompiledFunction.deserialize(result))
|
||||
return cls(model, compiled_results)
|
||||
|
||||
|
||||
def aot_compile_module(
|
||||
model: torch.nn.Module,
|
||||
inputs: list[ModelInput],
|
||||
hooks: Hooks,
|
||||
backend: Callable[[torch.fx.GraphModule, list[torch.Tensor]], SerializableCallable],
|
||||
) -> AOTCompiledModel:
|
||||
"""
|
||||
Compiles a single nn.Module with any number of inputs, and returns a compiled forward function.
|
||||
"""
|
||||
|
||||
def compile_single_graph(model_input: ModelInput) -> AOTCompiledFunction:
|
||||
example_inputs = (model_input.args, model_input.kwargs)
|
||||
orig_forward = model.forward
|
||||
with ExitStack() as stack:
|
||||
for ctx in model_input.contexts:
|
||||
stack.enter_context(ctx)
|
||||
return aot_compile_fullgraph(
|
||||
orig_forward,
|
||||
example_inputs,
|
||||
hooks=hooks,
|
||||
backend=backend,
|
||||
)
|
||||
|
||||
# pyrefly: ignore [implicit-any]
|
||||
compiled_results = []
|
||||
for model_input in inputs:
|
||||
log.info("Compiling input %s..", model_input)
|
||||
compiled_results.append(compile_single_graph(model_input))
|
||||
|
||||
assert len(compiled_results) > 0
|
||||
|
||||
return AOTCompiledModel(model, compiled_results)
|
||||
@@ -0,0 +1,211 @@
|
||||
import abc
|
||||
import importlib
|
||||
import pickle
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
def _serialize_triton_kernel(kernel: Any) -> tuple[str, str]:
|
||||
"""
|
||||
Serialize a triton kernel by extracting its module path and function name.
|
||||
Returns (module_path, function_name) tuple.
|
||||
|
||||
Triton JITFunction objects contain unpicklable _thread.RLock objects, so we
|
||||
serialize the import path instead and reimport on load.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the kernel cannot be serialized (missing attributes).
|
||||
"""
|
||||
fn = getattr(kernel, "fn", None)
|
||||
module_path = fn and getattr(fn, "__module__", None)
|
||||
func_name = fn and getattr(fn, "__name__", None)
|
||||
if fn is None or module_path is None or func_name is None:
|
||||
raise RuntimeError(
|
||||
f"Kernel fn missing __module__ or __name__: "
|
||||
f"module={module_path}, name={func_name}. "
|
||||
f"Cannot serialize for precompilation."
|
||||
)
|
||||
return (module_path, func_name)
|
||||
|
||||
|
||||
def _deserialize_triton_kernel(kernel_info: tuple[str, str]) -> Any:
|
||||
"""
|
||||
Deserialize a triton kernel by reimporting from its module.
|
||||
kernel_info is (module_path, function_name) tuple.
|
||||
"""
|
||||
module_path, func_name = kernel_info
|
||||
module = importlib.import_module(module_path)
|
||||
kernel = getattr(module, func_name)
|
||||
return kernel
|
||||
|
||||
|
||||
# Note: [Triton Kernel Side Table Serialization]
|
||||
#
|
||||
# When dynamo captures user-defined triton kernels, it creates FX graph nodes
|
||||
# (triton_kernel_wrapper_mutation/functional) with a `kernel_idx` parameter that
|
||||
# references the global `kernel_side_table` in triton_kernel_wrap.py. This side
|
||||
# table maps integer indices to actual triton kernel objects.
|
||||
#
|
||||
# For kernels that go through inductor's codegen path, this is fine - inductor
|
||||
# looks up the kernel from the side table at codegen time and embeds the kernel
|
||||
# source code directly into the generated wrapper. The compiled code doesn't
|
||||
# need the side table at runtime.
|
||||
#
|
||||
# However, not all triton kernels go through inductor codegen. When using
|
||||
# regional_inductor, only annotated regions are compiled by inductor. Triton
|
||||
# kernels outside these regions are executed via the FX interpreter, which
|
||||
# calls the higher-order op directly and needs the kernel to be in the side
|
||||
# table at runtime.
|
||||
#
|
||||
# When serializing/deserializing bundled AOT artifacts across process boundaries,
|
||||
# the kernel_side_table is empty in the new process, causing:
|
||||
# AssertionError: Kernel index X not found in id_to_kernel
|
||||
#
|
||||
# To fix this, we capture the kernel_side_table state during serialization and
|
||||
# restore it during deserialization. Kernels are serialized by their import path
|
||||
# (module_path, function_name) since triton JITFunction objects contain
|
||||
# unpicklable RLock objects.
|
||||
|
||||
|
||||
class SerializableCallable(abc.ABC):
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def serialize_compile_artifacts(cls, fn: Any) -> bytes:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def deserialize_compile_artifacts(cls, data: bytes) -> Any:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
||||
pass
|
||||
|
||||
|
||||
class GraphModuleSerializableCallable(SerializableCallable):
|
||||
def __init__(self, graph_module: torch.fx.GraphModule) -> None:
|
||||
assert isinstance(graph_module, torch.fx.GraphModule)
|
||||
self.graph_module = graph_module
|
||||
|
||||
@classmethod
|
||||
def serialize_compile_artifacts(
|
||||
cls, fn: "GraphModuleSerializableCallable"
|
||||
) -> bytes:
|
||||
from torch.fx._graph_pickler import GraphPickler, Options
|
||||
|
||||
state = fn.__dict__.copy()
|
||||
|
||||
graph_module = state["graph_module"]
|
||||
for node in graph_module.graph.nodes:
|
||||
node.meta.pop("nn_module_stack", None)
|
||||
node.meta.pop("source_fn_stack", None)
|
||||
node.meta.pop("example_value", None)
|
||||
|
||||
state["graph_module"] = GraphPickler.dumps(
|
||||
graph_module, Options(ops_filter=None)
|
||||
)
|
||||
return pickle.dumps(state)
|
||||
|
||||
@classmethod
|
||||
def deserialize_compile_artifacts(cls, data: bytes) -> Any:
|
||||
from torch._subclasses import FakeTensorMode
|
||||
from torch.fx._graph_pickler import GraphPickler
|
||||
from torch.fx.experimental.symbolic_shapes import ShapeEnv
|
||||
|
||||
state = pickle.loads(data)
|
||||
|
||||
fake_mode = FakeTensorMode(shape_env=ShapeEnv())
|
||||
state["graph_module"] = GraphPickler.loads(state["graph_module"], fake_mode)
|
||||
assert isinstance(state["graph_module"], torch.fx.GraphModule)
|
||||
state["graph_module"].recompile()
|
||||
|
||||
return cls(**state)
|
||||
|
||||
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return self.graph_module.forward(*args, **kwargs)
|
||||
|
||||
|
||||
class BundledAOTAutogradSerializableCallable(SerializableCallable):
|
||||
"""
|
||||
Represents a serializable callable generated by compile_fx.
|
||||
This class wraps around the compiled function generated by AOTAutograd.
|
||||
|
||||
TODO: Instead of using PrecompileContext to grab it from AOTAutograd,
|
||||
this object should be what's *returned* by aot_module_simplified.
|
||||
We'll do that refactor in a later PR.
|
||||
"""
|
||||
|
||||
def __init__(self, compiled_fn: Any) -> None:
|
||||
"""
|
||||
Takes in a BundledAOTAutogradCacheArtifact, which is the serialized form
|
||||
of a compiled function generated by AOTAutograd.
|
||||
"""
|
||||
assert hasattr(compiled_fn, "serialize")
|
||||
self.compiled_fn = compiled_fn
|
||||
|
||||
def __getattr__(self, attr: Any) -> Any:
|
||||
return getattr(self.compiled_fn, attr)
|
||||
|
||||
@classmethod
|
||||
def serialize_compile_artifacts(
|
||||
cls, fn: "BundledAOTAutogradSerializableCallable"
|
||||
) -> bytes:
|
||||
from torch._higher_order_ops.triton_kernel_wrap import kernel_side_table
|
||||
|
||||
# See Note: [Triton Kernel Side Table Serialization]
|
||||
# Capture triton kernel side table state BEFORE serialization.
|
||||
triton_kernels: dict[int, tuple[str, str]] = {
|
||||
idx: _serialize_triton_kernel(kernel)
|
||||
for idx, kernel in kernel_side_table.id_to_kernel.items()
|
||||
}
|
||||
triton_constant_args: dict[int, dict[str, Any]] = dict(
|
||||
kernel_side_table.constant_args
|
||||
)
|
||||
|
||||
with torch._functorch.config.patch("bundled_autograd_cache", True):
|
||||
serialized_entry = fn.compiled_fn.serialize()
|
||||
# Bundle the triton kernel side table with the serialized entry
|
||||
bundle = (serialized_entry, triton_kernels, triton_constant_args)
|
||||
result = pickle.dumps(bundle)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def deserialize_compile_artifacts(cls, data: bytes) -> Any:
|
||||
from torch._functorch._aot_autograd.aot_autograd_result import (
|
||||
deserialize_bundled_cache_entry,
|
||||
)
|
||||
from torch._higher_order_ops.triton_kernel_wrap import kernel_side_table
|
||||
|
||||
bundle = pickle.loads(data)
|
||||
|
||||
# Handle both old format (just entry) and new format (entry, kernels, const_args)
|
||||
if isinstance(bundle, tuple) and len(bundle) == 3:
|
||||
entry, triton_kernels, triton_constant_args = bundle
|
||||
else:
|
||||
# Backwards compatibility with old serialized artifacts
|
||||
entry = bundle
|
||||
# pyrefly: ignore [implicit-any]
|
||||
triton_kernels = {}
|
||||
# pyrefly: ignore [implicit-any]
|
||||
triton_constant_args = {}
|
||||
|
||||
# See Note: [Triton Kernel Side Table Serialization]
|
||||
# Restore triton kernel side table BEFORE deserializing the compiled function.
|
||||
# The compiled function may reference kernels by index if any triton kernels
|
||||
# don't go through inductor codegen (e.g., triton kernels outside of
|
||||
# regional_inductor compiled regions).
|
||||
for idx, kernel_info in triton_kernels.items():
|
||||
kernel = _deserialize_triton_kernel(kernel_info)
|
||||
kernel_side_table.id_to_kernel[idx] = kernel
|
||||
kernel_side_table.kernel_to_id[kernel] = idx
|
||||
|
||||
for idx, args in triton_constant_args.items():
|
||||
kernel_side_table.constant_args[idx] = args
|
||||
|
||||
compiled_fn = deserialize_bundled_cache_entry(entry)
|
||||
return cls(compiled_fn)
|
||||
|
||||
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return self.compiled_fn(*args, **kwargs)
|
||||
@@ -0,0 +1,184 @@
|
||||
"""
|
||||
This module provides common utilities and base classes for TorchDynamo backends.
|
||||
|
||||
Key components:
|
||||
- AotAutograd: Base class for implementing AOT (Ahead-of-Time) autograd backends
|
||||
- Backend utilities for handling:
|
||||
- Fake tensor conversion
|
||||
- Device/dtype detection from inputs
|
||||
- Memory efficient fusion
|
||||
- Graph flattening
|
||||
- Common compiler configurations
|
||||
|
||||
The utilities here are used by various backend implementations to handle
|
||||
common operations and provide consistent behavior across different backends.
|
||||
AOT autograd functionality is particularly important as it enables ahead-of-time
|
||||
optimization of both forward and backward passes.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
import logging
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from typing import Any
|
||||
from typing_extensions import ParamSpec, TypeVar
|
||||
from unittest.mock import patch
|
||||
|
||||
import torch
|
||||
from torch._dynamo import disable
|
||||
from torch._dynamo.exc import TensorifyScalarRestartAnalysis
|
||||
from torch._dynamo.utils import counters, defake, flatten_graph_inputs
|
||||
from torch._functorch.aot_autograd import (
|
||||
aot_module_simplified,
|
||||
SerializableAOTDispatchCompiler,
|
||||
)
|
||||
from torch.utils._python_dispatch import _disable_current_modes
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
|
||||
|
||||
class AotAutograd:
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
self.__name__ = "compiler_fn"
|
||||
self.kwargs = kwargs
|
||||
|
||||
def __call__(
|
||||
self, gm: torch.fx.GraphModule, example_inputs: Sequence[Any], **kwargs: Any
|
||||
) -> Callable[..., Any]:
|
||||
if kwargs:
|
||||
log.warning("aot_autograd-based backend ignoring extra kwargs %s", kwargs)
|
||||
|
||||
if any(isinstance(x, (list, tuple, dict)) for x in example_inputs):
|
||||
return flatten_graph_inputs(
|
||||
gm,
|
||||
example_inputs,
|
||||
self,
|
||||
)
|
||||
|
||||
# Hack to get around circular import problems with aot_eager_decomp_partition
|
||||
if callable(self.kwargs.get("decompositions")):
|
||||
self.kwargs["decompositions"] = self.kwargs["decompositions"]()
|
||||
|
||||
# NB: dont delete counter increment
|
||||
counters["aot_autograd"]["total"] += 1
|
||||
use_fallback = False
|
||||
|
||||
if use_fallback:
|
||||
log.debug("Unable to use AOT Autograd because graph has mutation")
|
||||
counters["aot_autograd"]["not_ok"] += 1
|
||||
return gm
|
||||
|
||||
def wrap_bw_compiler(bw_compiler_fn: Callable[P, R]) -> Callable[..., R]:
|
||||
def _wrapped_bw_compiler(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
# Note [Wrapping bw_compiler in disable]
|
||||
# The two disables here:
|
||||
# - stop TorchDynamo from trying to compile the bw_compiler function itself
|
||||
# - stop TorchDynamo from trying to compile our the generated backwards pass bw_compiler produces
|
||||
|
||||
return disable(
|
||||
disable(
|
||||
bw_compiler_fn, reason="do not trace backward compiler function"
|
||||
)(*args, **kwargs), # type: ignore[misc]
|
||||
reason="do not trace generated backwards pass",
|
||||
)
|
||||
|
||||
_wrapped_bw_compiler._is_wrapped_bw_compiler = ( # pyrefly: ignore [missing-attribute]
|
||||
True
|
||||
)
|
||||
return _wrapped_bw_compiler
|
||||
|
||||
bw_compiler = self.kwargs.get("bw_compiler") or self.kwargs["fw_compiler"]
|
||||
|
||||
if isinstance(bw_compiler, SerializableAOTDispatchCompiler):
|
||||
bw_compiler.compiler_fn = wrap_bw_compiler(bw_compiler.compiler_fn)
|
||||
elif getattr(bw_compiler, "_is_wrapped_bw_compiler", False):
|
||||
bw_compiler.compiler_fn = bw_compiler
|
||||
else:
|
||||
bw_compiler = wrap_bw_compiler(bw_compiler)
|
||||
|
||||
self.kwargs["bw_compiler"] = bw_compiler
|
||||
self.kwargs["inference_compiler"] = (
|
||||
self.kwargs.get("inference_compiler") or self.kwargs["fw_compiler"]
|
||||
)
|
||||
|
||||
from functorch.compile import nop
|
||||
from torch._inductor.debug import enable_aot_logging
|
||||
|
||||
# debug asserts slow down compile time noticeably,
|
||||
# So only default them on when the aot_eager backend is used.
|
||||
if self.kwargs.get("fw_compiler", None) is nop:
|
||||
patch_config: contextlib.AbstractContextManager[Any] = patch(
|
||||
"functorch.compile.config.debug_assert", True
|
||||
)
|
||||
else:
|
||||
patch_config = contextlib.nullcontext()
|
||||
|
||||
try:
|
||||
# NB: NOT cloned!
|
||||
with enable_aot_logging(), patch_config:
|
||||
cg = aot_module_simplified(gm, example_inputs, **self.kwargs)
|
||||
counters["aot_autograd"]["ok"] += 1
|
||||
return disable(cg, reason="do not trace AOT-compiled graph")
|
||||
except TensorifyScalarRestartAnalysis:
|
||||
raise
|
||||
except Exception:
|
||||
counters["aot_autograd"]["not_ok"] += 1
|
||||
raise
|
||||
|
||||
|
||||
def aot_autograd(**kwargs: Any) -> AotAutograd:
|
||||
return AotAutograd(**kwargs)
|
||||
|
||||
|
||||
def mem_efficient_fusion_kwargs(use_decomps: bool) -> dict[str, Any]:
|
||||
from functorch.compile import (
|
||||
default_decompositions,
|
||||
min_cut_rematerialization_partition,
|
||||
ts_compile,
|
||||
)
|
||||
|
||||
kwargs = {
|
||||
# these are taken from memory_efficient_fusion()
|
||||
"fw_compiler": ts_compile,
|
||||
"bw_compiler": ts_compile,
|
||||
"partition_fn": min_cut_rematerialization_partition,
|
||||
}
|
||||
|
||||
if use_decomps:
|
||||
# pyrefly: ignore [bad-typed-dict-key]
|
||||
kwargs["decompositions"] = default_decompositions
|
||||
|
||||
return kwargs
|
||||
|
||||
|
||||
def fake_tensor_unsupported(fn: Callable[[Any, list[Any], Any], R]) -> Any:
|
||||
"""
|
||||
Decorator for backends that need real inputs. We swap out fake
|
||||
tensors for zero tensors.
|
||||
"""
|
||||
|
||||
@functools.wraps(fn)
|
||||
def wrapper(model: Any, inputs: Any, **kwargs: Any) -> Any:
|
||||
with _disable_current_modes():
|
||||
inputs = list(map(defake, inputs))
|
||||
return fn(model, inputs, **kwargs) # type: ignore[call-arg]
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def device_from_inputs(example_inputs: Iterable[Any]) -> torch.device:
|
||||
for x in example_inputs:
|
||||
if hasattr(x, "device"):
|
||||
return x.device
|
||||
return torch.device("cpu") # Default fallback
|
||||
|
||||
|
||||
def dtype_from_inputs(example_inputs: Iterable[Any]) -> torch.dtype:
|
||||
for x in example_inputs:
|
||||
if hasattr(x, "dtype"):
|
||||
return x.dtype
|
||||
return torch.float32 # Default fallback
|
||||
@@ -0,0 +1,299 @@
|
||||
"""
|
||||
This module implements CUDA graphs support for TorchDynamo backends.
|
||||
|
||||
CUDA graphs allow for capturing and replaying GPU operations, which can significantly
|
||||
reduce CPU overhead in GPU-accelerated PyTorch models. This module provides:
|
||||
|
||||
- CUDA graph creation and management for both forward and backward passes
|
||||
- Input mutation detection and handling
|
||||
- Device compatibility checking
|
||||
- Stack trace management for debugging
|
||||
- Integration with TorchInductor's cudagraph trees
|
||||
|
||||
The backend supports two main modes:
|
||||
1. cudagraphs: Full CUDA graph support with both forward and backward pass optimization
|
||||
2. cudagraphs_inner: Lower-level CUDA graph implementation used for benchmarking
|
||||
|
||||
Key components:
|
||||
- CudagraphsBackend: Main backend class for CUDA graph integration
|
||||
- Mutation detection utilities to ensure graph safety
|
||||
- Device mapping and compatibility checks
|
||||
- Stack trace collection for debugging
|
||||
"""
|
||||
|
||||
import functools
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable, Sequence
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.fx
|
||||
from torch._dynamo import config
|
||||
from torch._dynamo.backends.common import aot_autograd
|
||||
from torch._dynamo.backends.debugging import boxed_nop
|
||||
from torch._inductor.cudagraph_utils import (
|
||||
BoxedDeviceIndex,
|
||||
check_multiple_devices_or_any_cpu_nodes,
|
||||
format_default_skip_message,
|
||||
get_mutation_stack_trace,
|
||||
get_placeholder_info,
|
||||
log_cudagraph_skip_and_bump_counter,
|
||||
)
|
||||
from torch._inductor.utils import (
|
||||
BoxedBool,
|
||||
count_tangents,
|
||||
get_first_incompatible_cudagraph_node,
|
||||
num_fw_fixed_arguments,
|
||||
output_node,
|
||||
)
|
||||
from torch.multiprocessing.reductions import StorageWeakRef
|
||||
|
||||
from .registry import register_backend
|
||||
|
||||
|
||||
def find_input_mutations(g: torch.fx.Graph) -> set[int]:
|
||||
def meta_fk(meta: dict[str, Any]) -> Any:
|
||||
return meta["val"] if "val" in meta else meta["fake_result"]
|
||||
|
||||
inputs = defaultdict(set)
|
||||
input_idx = 0
|
||||
mutated_inputs = set()
|
||||
for n in g.nodes:
|
||||
if n.op == "placeholder":
|
||||
if isinstance(meta_fk(n.meta), torch.Tensor):
|
||||
inputs[StorageWeakRef(meta_fk(n.meta)._typed_storage())].add(input_idx)
|
||||
input_idx += 1
|
||||
elif n.op == "call_function":
|
||||
if not hasattr(n.target, "_schema"):
|
||||
continue
|
||||
|
||||
schema = n.target._schema
|
||||
for i, arg in enumerate(schema.arguments):
|
||||
if i < len(n.args):
|
||||
argument = n.args[i]
|
||||
else:
|
||||
if arg.name not in n.kwargs:
|
||||
continue
|
||||
argument = n.kwargs[arg.name]
|
||||
mut_arg = False
|
||||
if arg.alias_info:
|
||||
if arg.alias_info.is_write:
|
||||
mut_arg = True
|
||||
if mut_arg:
|
||||
# TODO: not correct for args that contain tensors in a struct
|
||||
# like list
|
||||
mutated_inputs |= inputs[
|
||||
StorageWeakRef(meta_fk(argument.meta)._typed_storage())
|
||||
]
|
||||
|
||||
# TODO: error on unrecognized nodes
|
||||
return mutated_inputs
|
||||
|
||||
|
||||
def get_device_node_mapping(
|
||||
gm: torch.fx.GraphModule,
|
||||
) -> dict[torch.device, torch.fx.Node]:
|
||||
device_node_mapping: dict[torch.device, torch.fx.Node] = {}
|
||||
for n in gm.graph.nodes:
|
||||
t = n.meta.get("val", None)
|
||||
if isinstance(t, torch.Tensor) and t.device not in device_node_mapping:
|
||||
device_node_mapping[t.device] = n
|
||||
return device_node_mapping
|
||||
|
||||
|
||||
def check_for_mutation_ignore_cuda_graph_managed_tensor(
|
||||
aot_model: torch.fx.GraphModule, num_fixed: int
|
||||
) -> str | None:
|
||||
mutation_indices = find_input_mutations(aot_model.graph) - set(range(num_fixed))
|
||||
if not mutation_indices:
|
||||
return None
|
||||
|
||||
placeholders = get_placeholder_info(aot_model.graph)
|
||||
return get_mutation_stack_trace(placeholders, mutation_indices)
|
||||
|
||||
|
||||
def check_for_skip(aot_model: torch.fx.GraphModule, num_fixed: int) -> str | None:
|
||||
if not config.cudagraph_backend_support_input_mutation:
|
||||
if mut_skip := check_for_mutation_ignore_cuda_graph_managed_tensor(
|
||||
aot_model, num_fixed
|
||||
):
|
||||
return mut_skip
|
||||
|
||||
if skip := check_multiple_devices_or_any_cpu_nodes(
|
||||
get_device_node_mapping(aot_model)
|
||||
):
|
||||
return skip
|
||||
|
||||
if node := get_first_incompatible_cudagraph_node(aot_model):
|
||||
return format_default_skip_message(f"incompatible op ({node.name})")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_device_index(gm: torch.fx.GraphModule) -> int:
|
||||
device = next(iter(get_device_node_mapping(gm)))
|
||||
assert device.type == "cuda"
|
||||
return device.index
|
||||
|
||||
|
||||
def get_stack_traces(gm: torch.fx.GraphModule) -> list[str | None]:
|
||||
output = output_node(gm)
|
||||
assert len(output.args) == 1
|
||||
args = output.args[0]
|
||||
if not hasattr(args, "__iter__"):
|
||||
return []
|
||||
return [
|
||||
(arg.stack_trace if isinstance(arg, torch.fx.node.Node) else None)
|
||||
for arg in args # type: ignore[union-attr]
|
||||
]
|
||||
|
||||
|
||||
def cudagraphs(dynamo_model: torch.fx.GraphModule, dynamo_inputs: Sequence[Any]) -> Any:
|
||||
from torch._inductor.cudagraph_trees import cudagraphify_impl
|
||||
|
||||
do_cudagraphs = BoxedBool(True)
|
||||
boxed_device_index = BoxedDeviceIndex(None)
|
||||
|
||||
def forward_cudagraphs(
|
||||
aot_model: torch.fx.GraphModule,
|
||||
aot_inputs: list[Any],
|
||||
is_inference: bool = False,
|
||||
) -> Any:
|
||||
interp = boxed_nop(aot_model, aot_inputs)
|
||||
fixed = num_fw_fixed_arguments(len(dynamo_inputs), len(aot_inputs))
|
||||
if skip_msg := check_for_skip(aot_model, fixed):
|
||||
BoxedBool.disable(do_cudagraphs)
|
||||
log_cudagraph_skip_and_bump_counter(
|
||||
f"skipping cudagraphs due to {skip_msg}"
|
||||
)
|
||||
return interp
|
||||
|
||||
boxed_device_index.set(get_device_index(aot_model))
|
||||
out = cudagraphify_impl(
|
||||
interp,
|
||||
aot_inputs,
|
||||
range(fixed),
|
||||
device_index=boxed_device_index.value,
|
||||
is_backward=False,
|
||||
is_inference=is_inference,
|
||||
stack_traces=get_stack_traces(aot_model),
|
||||
placeholders=get_placeholder_info(aot_model.graph),
|
||||
mutated_input_idxs=find_input_mutations(aot_model.graph),
|
||||
)
|
||||
out._boxed_call = True # type: ignore[attr-defined]
|
||||
return out
|
||||
|
||||
def backward_cudagraphs(
|
||||
aot_model: torch.fx.GraphModule, aot_inputs: list[Any]
|
||||
) -> Any:
|
||||
interp = boxed_nop(aot_model, aot_inputs)
|
||||
if not do_cudagraphs:
|
||||
return aot_model
|
||||
|
||||
fixed = count_tangents(aot_model)
|
||||
if skip_msg := check_for_skip(aot_model, fixed):
|
||||
log_cudagraph_skip_and_bump_counter(
|
||||
f"skipping cudagraphs due to {skip_msg}"
|
||||
)
|
||||
|
||||
# See [Backward Generation Handling]
|
||||
device_idx = boxed_device_index.value
|
||||
if device_idx is None:
|
||||
device_idx = 0 # Default to device 0 if not set
|
||||
manager = torch._inductor.cudagraph_trees.get_manager(
|
||||
device_idx, create_if_none_exists=False
|
||||
)
|
||||
assert manager is not None
|
||||
|
||||
def fn(inputs: list[Any]) -> Any:
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
manager.set_to_running_backward()
|
||||
return aot_model(inputs)
|
||||
|
||||
fn._boxed_call = True # type: ignore[attr-defined]
|
||||
return fn
|
||||
|
||||
out = cudagraphify_impl(
|
||||
interp,
|
||||
aot_inputs,
|
||||
range(fixed),
|
||||
device_index=get_device_index(aot_model),
|
||||
is_backward=True,
|
||||
is_inference=False,
|
||||
stack_traces=get_stack_traces(aot_model),
|
||||
placeholders=get_placeholder_info(aot_model.graph),
|
||||
mutated_input_idxs=find_input_mutations(aot_model.graph),
|
||||
)
|
||||
out._boxed_call = True # type: ignore[attr-defined]
|
||||
return out
|
||||
|
||||
aot_cudagraphs = aot_autograd(
|
||||
fw_compiler=forward_cudagraphs,
|
||||
bw_compiler=backward_cudagraphs,
|
||||
inference_compiler=functools.partial(forward_cudagraphs, is_inference=True),
|
||||
keep_inference_input_mutations=torch._dynamo.config.cudagraph_backend_keep_input_mutation,
|
||||
)
|
||||
return aot_cudagraphs(dynamo_model, dynamo_inputs)
|
||||
|
||||
|
||||
class CudagraphsBackend:
|
||||
compiler_name = "cudagraphs"
|
||||
|
||||
@staticmethod
|
||||
def reset() -> None:
|
||||
from torch._inductor.cudagraph_trees import reset_cudagraph_trees
|
||||
|
||||
reset_cudagraph_trees()
|
||||
|
||||
@staticmethod
|
||||
def __call__(model: torch.fx.GraphModule, inputs: Sequence[Any]) -> Any:
|
||||
return cudagraphs(model, inputs)
|
||||
|
||||
|
||||
# aot_cudagraphs only applies CUDA graphs to the graph. It is also helpful
|
||||
# for debugging and can serve as a perf baseline.
|
||||
register_backend(name="cudagraphs", compiler_fn=CudagraphsBackend())
|
||||
|
||||
|
||||
def cudagraphs_inner(
|
||||
model: Callable[..., Any],
|
||||
inputs: Sequence[Any],
|
||||
copy_outputs: bool = True,
|
||||
copy_inputs: bool = True,
|
||||
) -> Callable[..., Sequence[Any]]:
|
||||
"""This isn't registered as a backend, but is used in some benchmarks"""
|
||||
assert isinstance(inputs, (list, tuple))
|
||||
if copy_inputs:
|
||||
static_inputs = [torch.zeros_like(x) for x in inputs]
|
||||
else:
|
||||
static_inputs = list(inputs)
|
||||
|
||||
# warmup
|
||||
torch.cuda.synchronize()
|
||||
stream = torch.cuda.Stream()
|
||||
stream.wait_stream(torch.cuda.current_stream())
|
||||
with torch.cuda.stream(stream):
|
||||
model(*inputs)
|
||||
stream.synchronize()
|
||||
torch.cuda.current_stream().wait_stream(stream)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
# record
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph, stream=stream):
|
||||
static_outputs = model(*static_inputs)
|
||||
if not isinstance(static_outputs, (list, tuple)):
|
||||
static_outputs = (static_outputs,)
|
||||
|
||||
def run(*new_inputs: Any) -> Sequence[Any]:
|
||||
assert len(static_inputs) == len(new_inputs)
|
||||
if copy_inputs:
|
||||
for dst, src in zip(static_inputs, new_inputs):
|
||||
dst.copy_(src)
|
||||
graph.replay()
|
||||
if copy_outputs:
|
||||
return [x.clone() for x in static_outputs]
|
||||
else:
|
||||
return static_outputs
|
||||
|
||||
return run
|
||||
@@ -0,0 +1,730 @@
|
||||
"""
|
||||
This module provides debugging backends for TorchDynamo to help diagnose and troubleshoot
|
||||
compilation and execution issues. It includes:
|
||||
|
||||
Key Debugging Backends:
|
||||
- eager: Simple pass-through backend that runs models in eager mode
|
||||
- eager_noexcept: Similar to eager but with additional exception handling
|
||||
- eager_debug: Adds schema validation checks for custom operators
|
||||
- aot_eager: Uses AOT Autograd with nop compiler for debugging
|
||||
- aot_eager_decomp_partition: Uses TorchInductor decompositions for debugging
|
||||
- torchscript: Compiles using TorchScript for debugging JIT-related issues
|
||||
|
||||
Testing and Development Tools:
|
||||
- Backends for inducing specific errors (compile/runtime/accuracy)
|
||||
- ExplainOutput class for detailed graph compilation analysis
|
||||
- Utilities for cross-referencing and mode management
|
||||
- Tools for graph detail inspection and break reason analysis
|
||||
|
||||
These backends are primarily used for:
|
||||
1. Debugging graph breaks and compilation failures
|
||||
2. Testing error handling and recovery mechanisms
|
||||
3. Analyzing performance bottlenecks
|
||||
4. Validating operator schemas and decompositions
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
import functools
|
||||
import logging
|
||||
from collections.abc import Callable, Iterable
|
||||
from importlib import import_module
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
from functorch.compile import min_cut_rematerialization_partition
|
||||
from torch import _guards
|
||||
from torch._dynamo.output_graph import GraphCompileReason
|
||||
from torch._functorch import config as functorch_config
|
||||
from torch._functorch.compilers import ts_compile
|
||||
from torch._inductor.output_code import OutputCode
|
||||
|
||||
from .common import aot_autograd
|
||||
from .registry import CompiledFn, CompilerFn, register_debug_backend as register_backend
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch.fx.node import Target
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register_backend
|
||||
def eager(
|
||||
gm: torch.fx.GraphModule, fake_tensor_inputs: list[torch.Tensor], **kwargs: Any
|
||||
) -> Callable[..., Any]:
|
||||
if kwargs:
|
||||
log.warning("eager backend ignoring extra kwargs %s", kwargs)
|
||||
|
||||
if torch._functorch.config.force_autograd_cache:
|
||||
from torch._dynamo.aot_compile_types import GraphModuleSerializableCallable
|
||||
|
||||
return GraphModuleSerializableCallable(gm)
|
||||
return gm.forward
|
||||
|
||||
|
||||
def make_eager_backend_with_torch_function_mode(
|
||||
mode: torch.overrides.TorchFunctionMode,
|
||||
) -> Callable[..., Any]:
|
||||
return make_eager_backend_with_torch_function_modes([mode])
|
||||
|
||||
|
||||
def make_eager_backend_with_torch_function_modes(
|
||||
modes: Iterable[torch.overrides.TorchFunctionMode],
|
||||
) -> Callable[..., Any]:
|
||||
"""Used to trace HOPs (cond and while) for eager execution, the metadata
|
||||
TF mode mutates vars outside of the scope of the HOP, and we can't have graph breaks
|
||||
in the HOP, so we need to externally run this mode and not trace it."""
|
||||
from contextlib import ExitStack
|
||||
|
||||
def fn(
|
||||
gm: torch.fx.GraphModule, fake_tensor_inputs: list[torch.Tensor], **kwargs: Any
|
||||
) -> Callable[..., Any]:
|
||||
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||
with ExitStack() as stack:
|
||||
for mode in modes:
|
||||
stack.enter_context(mode)
|
||||
return gm.forward(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return fn
|
||||
|
||||
|
||||
@register_backend
|
||||
def eager_noexcept(
|
||||
gm: torch.fx.GraphModule, fake_tensor_inputs: list[torch.Tensor], **kwargs: Any
|
||||
) -> Callable[..., Any]:
|
||||
if kwargs:
|
||||
log.warning("eager_noexcept backend ignoring extra kwargs %s", kwargs)
|
||||
|
||||
# This backend is intended to check that dynamo-generated GraphModules
|
||||
# do not cause errors.
|
||||
def inner(*args: Any) -> Any:
|
||||
try:
|
||||
return gm(*args)
|
||||
except Exception as e:
|
||||
raise torch._dynamo.exc.TorchDynamoException(
|
||||
"Unexpected exception when running generated GraphModule"
|
||||
) from e
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
@register_backend
|
||||
def pre_dispatch_eager(
|
||||
gm: torch.fx.GraphModule, fake_tensor_inputs: list[torch.Tensor], **kwargs: Any
|
||||
) -> torch.fx.GraphModule:
|
||||
if kwargs:
|
||||
log.warning("pre_dispatch_eager backend ignoring extra kwargs %s", kwargs)
|
||||
|
||||
from torch.fx.experimental.proxy_tensor import make_fx
|
||||
|
||||
def runnable_gm(*args: Any) -> Any:
|
||||
return torch.fx.Interpreter(gm).run(*args)
|
||||
|
||||
pre_dispatch_gm = make_fx(runnable_gm, pre_dispatch=True)(*fake_tensor_inputs)
|
||||
pre_dispatch_gm.print_readable()
|
||||
|
||||
return pre_dispatch_gm
|
||||
|
||||
|
||||
@register_backend
|
||||
def eager_debug(
|
||||
gm: torch.fx.GraphModule, fake_tensor_inputs: list[torch.Tensor], **kwargs: Any
|
||||
) -> Callable[..., Any]:
|
||||
if kwargs:
|
||||
log.warning("eager_debug backend ignoring extra kwargs %s", kwargs)
|
||||
|
||||
from torch._subclasses.schema_check_mode import SchemaCheckMode
|
||||
|
||||
# We could add more debugging bits here.
|
||||
# Right now, this backend can be used to check for and error on
|
||||
# custom dispatcher ops that have incorrect schemas.
|
||||
def inner(*args: Any) -> Any:
|
||||
with SchemaCheckMode():
|
||||
return torch.fx.Interpreter(gm).run(*args)
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
@register_backend(name="ts") # type: ignore[misc]
|
||||
def torchscript(
|
||||
gm: torch.fx.GraphModule, fake_tensor_inputs: list[torch.Tensor]
|
||||
) -> torch.jit.ScriptModule:
|
||||
return torch.jit.script(gm)
|
||||
|
||||
|
||||
def invoke_subgraph_inner_compiler(
|
||||
subgraph: torch.fx.GraphModule, example_inputs: list[torch.Tensor]
|
||||
) -> Callable[..., Any]:
|
||||
"""Inner compiler that wraps forward/backward graphs in invoke_subgraph HOP.
|
||||
|
||||
This is used as the fw_compiler/bw_compiler for aot_autograd. When the resulting
|
||||
function is traced by make_fx, it emits an invoke_subgraph HOP instead of inlining.
|
||||
"""
|
||||
from torch._dynamo import disable
|
||||
from torch._higher_order_ops.invoke_subgraph import invoke_subgraph_infer
|
||||
|
||||
@disable
|
||||
# pyrefly: ignore [deprecated]
|
||||
@torch._dynamo.allow_in_graph
|
||||
def invoke_subgraph_wrapper_unboxed(*operands: Any) -> Any:
|
||||
return invoke_subgraph_infer(subgraph, *operands)
|
||||
|
||||
# NB: The direct to unboxed path is broken, you MUST DO THIS
|
||||
|
||||
def invoke_subgraph_wrapper(args: list[Any]) -> Any:
|
||||
return invoke_subgraph_wrapper_unboxed(*args)
|
||||
|
||||
invoke_subgraph_wrapper._boxed_call = True # type: ignore[attr-defined]
|
||||
|
||||
return invoke_subgraph_wrapper
|
||||
|
||||
|
||||
# I cannot say how many times I had to revert to this vibe coded version of
|
||||
# the code, which worked, and the cleaner versions of the code did not work,
|
||||
# so I'm leaving this here until we fix the rest of the bugs.
|
||||
'''
|
||||
# Counter for unique subgraph names in invoke_subgraph backend
|
||||
_invoke_subgraph_counter = 0
|
||||
|
||||
|
||||
def invoke_subgraph_inner_compiler_good(
|
||||
fx_g: torch.fx.GraphModule, example_inputs: list[torch.Tensor]
|
||||
) -> Callable[..., Any]:
|
||||
"""Inner compiler that wraps forward/backward graphs in invoke_subgraph HOP.
|
||||
|
||||
This is used as the fw_compiler/bw_compiler for aot_autograd. When the resulting
|
||||
function is traced by make_fx, it emits an invoke_subgraph HOP instead of inlining.
|
||||
"""
|
||||
from torch._higher_order_ops.invoke_subgraph import (
|
||||
invoke_subgraph as invoke_subgraph_hop,
|
||||
)
|
||||
from torch.fx.experimental.proxy_tensor import get_proxy_mode
|
||||
|
||||
global _invoke_subgraph_counter
|
||||
_invoke_subgraph_counter += 1
|
||||
name = f"invoke_subgraph_{_invoke_subgraph_counter}"
|
||||
|
||||
from torch._dynamo import disable
|
||||
|
||||
# Check if fx_g uses boxed calling convention
|
||||
fx_g_is_boxed = getattr(fx_g, "_boxed_call", False)
|
||||
|
||||
@disable
|
||||
@torch._dynamo.allow_in_graph
|
||||
def invoke_subgraph_wrapper_unboxed(*args: Any) -> Any:
|
||||
proxy_mode = get_proxy_mode()
|
||||
if proxy_mode is not None:
|
||||
# When being traced by make_fx, emit invoke_subgraph HOP
|
||||
return invoke_subgraph_hop(fx_g, name, *args) # type: ignore[arg-type]
|
||||
else:
|
||||
# Normal execution path - call fx_g with proper calling convention
|
||||
if fx_g_is_boxed:
|
||||
return fx_g(list(args))
|
||||
else:
|
||||
return fx_g(*args)
|
||||
|
||||
# Wrap to handle boxed arguments (list of args) as expected by AOTAutograd
|
||||
def invoke_subgraph_wrapper(args: list[Any]) -> Any:
|
||||
return invoke_subgraph_wrapper_unboxed(*args)
|
||||
|
||||
invoke_subgraph_wrapper._boxed_call = True # type: ignore[attr-defined]
|
||||
return invoke_subgraph_wrapper
|
||||
'''
|
||||
|
||||
|
||||
@register_backend
|
||||
def invoke_subgraph(
|
||||
gm: torch.fx.GraphModule, fake_tensor_inputs: list[torch.Tensor], **kwargs: Any
|
||||
) -> Callable[..., Any]:
|
||||
"""Backend that wraps forward/backward graphs in invoke_subgraph HOP when traced by make_fx.
|
||||
|
||||
This backend uses AOTAutograd to partition into forward/backward graphs, then wraps
|
||||
each in an invoke_subgraph HOP. This is useful for recursive Dynamo tracing scenarios
|
||||
where you want the compiled subgraph to appear as invoke_subgraph HOPs in the outer
|
||||
trace rather than being inlined.
|
||||
|
||||
Requires:
|
||||
- torch._dynamo.config.force_compile_during_fx_trace = True
|
||||
(this implicitly overrides error_on_nested_fx_trace)
|
||||
"""
|
||||
if kwargs:
|
||||
log.warning("invoke_subgraph backend ignoring extra kwargs %s", kwargs)
|
||||
|
||||
# Use AOTAutograd to partition into forward/backward
|
||||
return aot_autograd(
|
||||
fw_compiler=invoke_subgraph_inner_compiler,
|
||||
bw_compiler=invoke_subgraph_inner_compiler,
|
||||
partition_fn=min_cut_rematerialization_partition,
|
||||
keep_inference_input_mutations=True,
|
||||
)(gm, fake_tensor_inputs)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class AOTEagerOutputCode(OutputCode):
|
||||
"""
|
||||
An OutputCode that wraps a GraphModule for eager-mode execution.
|
||||
|
||||
This allows non-inductor backends (like aot_eager) to participate in
|
||||
the bundled autograd cache and aot_compile serialization flow.
|
||||
"""
|
||||
|
||||
gm: torch.fx.GraphModule | None = None
|
||||
_serialized_gm: bytes | None = dataclasses.field(default=None, init=False)
|
||||
|
||||
def __call__(self, inputs: Any) -> Any:
|
||||
assert self.gm is not None
|
||||
return self.gm.forward(inputs)
|
||||
|
||||
def prepare_for_serialization(self) -> None:
|
||||
from torch.fx._graph_pickler import GraphPickler, Options
|
||||
|
||||
assert self.gm is not None
|
||||
for node in self.gm.graph.nodes:
|
||||
node.meta.pop("nn_module_stack", None)
|
||||
node.meta.pop("source_fn_stack", None)
|
||||
node.meta.pop("example_value", None)
|
||||
|
||||
self._serialized_gm = GraphPickler.dumps(self.gm, Options(ops_filter=None))
|
||||
self.gm = None
|
||||
|
||||
def post_compile(self, *args: Any, **kwargs: Any) -> None:
|
||||
if self.gm is None and self._serialized_gm is not None:
|
||||
from torch._subclasses import FakeTensorMode
|
||||
from torch.fx._graph_pickler import GraphPickler
|
||||
from torch.fx.experimental.symbolic_shapes import ShapeEnv
|
||||
from torch.fx.graph import _BoxedCodeGen
|
||||
|
||||
fake_mode = FakeTensorMode(shape_env=ShapeEnv())
|
||||
gm = GraphPickler.loads(self._serialized_gm, fake_mode)
|
||||
assert isinstance(gm, torch.fx.GraphModule)
|
||||
self.gm = gm
|
||||
assert isinstance(self.gm, torch.fx.GraphModule)
|
||||
self.gm.graph.set_codegen(_BoxedCodeGen())
|
||||
self.gm.recompile()
|
||||
self._serialized_gm = None
|
||||
|
||||
def set_triton_bundle(self, triton_bundle: Any) -> None:
|
||||
pass
|
||||
|
||||
|
||||
# used boxed call to discard inputs when they are no longer needed
|
||||
def boxed_nop(
|
||||
fx_g: torch.fx.GraphModule, example_inputs: list[torch.Tensor]
|
||||
) -> Callable[..., Any]:
|
||||
from torch.fx.graph import _BoxedCodeGen
|
||||
|
||||
# Set the graph to use boxed codegen
|
||||
fx_g.graph.set_codegen(_BoxedCodeGen())
|
||||
fx_g.recompile()
|
||||
|
||||
if functorch_config.force_autograd_cache or functorch_config.bundled_autograd_cache:
|
||||
result = AOTEagerOutputCode(gm=fx_g)
|
||||
result._boxed_call = True # type: ignore[attr-defined]
|
||||
return result
|
||||
|
||||
# Wrap the forward method in a function so we can set _boxed_call attribute
|
||||
forward_fn = fx_g.forward
|
||||
|
||||
def run(args: Any) -> Any:
|
||||
from torch.utils._debug_mode import DebugInterpreter, get_active_debug_mode
|
||||
|
||||
if (
|
||||
debug_mode := get_active_debug_mode()
|
||||
) is not None and debug_mode.run_compile_with_interpreter:
|
||||
return DebugInterpreter(fx_g, backend="aot_eager").run(*args)
|
||||
return forward_fn(args)
|
||||
|
||||
run._boxed_call = True # type: ignore[attr-defined]
|
||||
return run
|
||||
|
||||
|
||||
def boxed_nop_with_mode(
|
||||
fx_g: torch.fx.GraphModule,
|
||||
example_inputs: list[torch.Tensor],
|
||||
*,
|
||||
mode: torch.overrides.TorchFunctionMode,
|
||||
) -> Callable[..., Any]:
|
||||
from torch.fx.graph import _BoxedCodeGen
|
||||
|
||||
# Set the graph to use boxed codegen
|
||||
fx_g.graph.set_codegen(_BoxedCodeGen())
|
||||
fx_g.recompile()
|
||||
|
||||
# Create a wrapper that runs with the mode
|
||||
forward_fn = fx_g.forward
|
||||
|
||||
def run(args: Any) -> Any:
|
||||
with mode:
|
||||
return forward_fn(args)
|
||||
|
||||
run._boxed_call = True # type: ignore[attr-defined]
|
||||
return run
|
||||
|
||||
|
||||
def fake_crossref_boxed_nop(
|
||||
fx_g: torch.fx.GraphModule,
|
||||
example_inputs: list[torch.Tensor],
|
||||
ignore_op_fn: Callable[[torch._ops.OpOverload], bool] | None = None,
|
||||
) -> Callable[..., Any]:
|
||||
from torch.fx.graph import _BoxedCodeGen
|
||||
|
||||
# Set the graph to use boxed codegen
|
||||
fx_g.graph.set_codegen(_BoxedCodeGen())
|
||||
fx_g.recompile()
|
||||
|
||||
# Create a wrapper that runs with the mode
|
||||
forward_fn = fx_g.forward
|
||||
|
||||
def run(args: Any) -> Any:
|
||||
with torch._subclasses.CrossRefFakeMode(ignore_op_fn):
|
||||
return forward_fn(args)
|
||||
|
||||
run._boxed_call = True # type: ignore[attr-defined]
|
||||
return run
|
||||
|
||||
|
||||
def ignore_builtins(op: torch._ops.OpOverload) -> bool:
|
||||
return op.namespace in ("aten", "prims", "prim")
|
||||
|
||||
|
||||
def get_nop_func() -> Callable[
|
||||
[torch.fx.GraphModule, list[torch.Tensor]], Callable[..., Any]
|
||||
]:
|
||||
if not torch._functorch.config.fake_tensor_crossref:
|
||||
return boxed_nop
|
||||
elif torch._functorch.config.fake_tensor_crossref == "all":
|
||||
return fake_crossref_boxed_nop
|
||||
else:
|
||||
assert torch._functorch.config.fake_tensor_crossref == "custom_ops"
|
||||
return functools.partial(fake_crossref_boxed_nop, ignore_op_fn=ignore_builtins)
|
||||
|
||||
|
||||
# Useful for debugging purpose
|
||||
# aot_eager uses AOT Autograd backend with nop compiler. It is helpful in debugging.
|
||||
def aot_eager(
|
||||
gm: torch.fx.GraphModule,
|
||||
fake_tensor_inputs: list[torch.Tensor],
|
||||
fw_compiler: Callable[..., Any] | None = None,
|
||||
bw_compiler: Callable[..., Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Callable[..., Any]:
|
||||
return aot_autograd(
|
||||
fw_compiler=fw_compiler or boxed_nop,
|
||||
bw_compiler=bw_compiler or boxed_nop,
|
||||
partition_fn=min_cut_rematerialization_partition,
|
||||
keep_inference_input_mutations=True,
|
||||
)(gm, fake_tensor_inputs, **kwargs)
|
||||
|
||||
|
||||
register_backend(name="aot_eager", compiler_fn=aot_eager)
|
||||
|
||||
aot_eager_default_partitioner = aot_autograd(
|
||||
fw_compiler=boxed_nop, keep_inference_input_mutations=True
|
||||
)
|
||||
register_backend(
|
||||
name="aot_eager_default_partitioner", compiler_fn=aot_eager_default_partitioner
|
||||
)
|
||||
|
||||
|
||||
# Uses TorchInductor AOT Autograd decomps and partitioner to isolate aot vs
|
||||
# inductor problems.
|
||||
# aot_eager_decomp_partition just replaces the inductor compiler with nop to help
|
||||
# isolate inductor vs aot_eager errors
|
||||
def aot_eager_decomp_partition(
|
||||
gm: torch.fx.GraphModule, fake_tensor_inputs: list[torch.Tensor], **kwargs: Any
|
||||
) -> Callable[..., Any]:
|
||||
if kwargs:
|
||||
log.warning(
|
||||
"aot_eager_decomp_partition backend ignoring extra kwargs %s", kwargs
|
||||
)
|
||||
|
||||
from torch._inductor.compiler_bisector import CompilerBisector
|
||||
|
||||
config_patches = {"unlift_effect_tokens": True}
|
||||
if bisect_changes := CompilerBisector.get_config_change(
|
||||
"aot_eager_decomp_partition"
|
||||
):
|
||||
config_patches.update(bisect_changes) # type: ignore[arg-type]
|
||||
|
||||
with functorch_config.patch(config_patches):
|
||||
return aot_autograd(
|
||||
# these are taken from memory_efficient_fusion()
|
||||
fw_compiler=get_nop_func(),
|
||||
bw_compiler=get_nop_func(),
|
||||
# NB: lambda here is to delay import of inductor
|
||||
decompositions=lambda: import_module(
|
||||
"torch._inductor.compile_fx"
|
||||
).select_decomp_table(),
|
||||
partition_fn=functools.partial(
|
||||
min_cut_rematerialization_partition, compiler="inductor"
|
||||
),
|
||||
)(gm, fake_tensor_inputs)
|
||||
|
||||
|
||||
register_backend(
|
||||
name="aot_eager_decomp_partition", compiler_fn=aot_eager_decomp_partition
|
||||
)
|
||||
|
||||
|
||||
# aot_eager_decomp_partition_with_mode is similar as aot_eager_decomp_partition,
|
||||
# except that it takes a TorchDispatchMode mode and run the fw/bw in the mode
|
||||
def aot_eager_decomp_partition_with_mode(
|
||||
gm: torch.fx.GraphModule,
|
||||
fake_tensor_inputs: list[torch.Tensor],
|
||||
mode: Any,
|
||||
**kwarg: Any,
|
||||
) -> Callable[..., Any]:
|
||||
return aot_autograd(
|
||||
# these are taken from memory_efficient_fusion()
|
||||
fw_compiler=functools.partial(boxed_nop_with_mode, mode=mode),
|
||||
bw_compiler=functools.partial(boxed_nop_with_mode, mode=mode),
|
||||
# NB: lambda here is to delay import of inductor
|
||||
decompositions=lambda: import_module(
|
||||
"torch._inductor.compile_fx"
|
||||
).select_decomp_table(),
|
||||
partition_fn=functools.partial(
|
||||
min_cut_rematerialization_partition, compiler="inductor"
|
||||
),
|
||||
)(gm, fake_tensor_inputs)
|
||||
|
||||
|
||||
register_backend(
|
||||
name="aot_eager_decomp_partition_with_mode",
|
||||
compiler_fn=aot_eager_decomp_partition_with_mode, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
|
||||
def aot_eager_decomp_partition_crossref(
|
||||
gm: torch.fx.GraphModule, fake_tensor_inputs: list[torch.Tensor], **kwargs: Any
|
||||
) -> Callable[..., Any]:
|
||||
# if the config is set, respect it, otherwise only test custom_ops.
|
||||
# custom_op bad metas always manifest as an error whereas aten will only sometimes.
|
||||
# by default, use the less noisy option
|
||||
config_val = (
|
||||
"custom_ops"
|
||||
if not functorch_config.fake_tensor_crossref
|
||||
else functorch_config.fake_tensor_crossref
|
||||
)
|
||||
with functorch_config.patch(fake_tensor_crossref=config_val):
|
||||
return aot_eager_decomp_partition(gm, fake_tensor_inputs, **kwargs)
|
||||
|
||||
|
||||
register_backend(
|
||||
name="aot_eager_decomp_partition_crossref",
|
||||
compiler_fn=aot_eager_decomp_partition_crossref,
|
||||
)
|
||||
|
||||
|
||||
# AOT Autograd with torchscript backend. Default partitioner.
|
||||
# aot_ts uses torchscript backend. We can use this with both nnc and nvfuser
|
||||
# by using the relevant fuser with torch.jit.fuser(...)
|
||||
aot_ts = aot_autograd(fw_compiler=ts_compile)
|
||||
register_backend(name="aot_ts", compiler_fn=aot_ts)
|
||||
|
||||
# These buggy backends are used for inducing bugs so that we can test
|
||||
# our repro extraction / minifier scripts
|
||||
|
||||
|
||||
class ReluCompileError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TestingOnlyCompileError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@register_backend
|
||||
def relu_compile_error_TESTING_ONLY(
|
||||
gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor]
|
||||
) -> torch.fx.GraphModule:
|
||||
for node in gm.graph.nodes:
|
||||
if node.target is torch.relu:
|
||||
raise ReluCompileError
|
||||
return gm
|
||||
|
||||
|
||||
@register_backend
|
||||
def relu_runtime_error_TESTING_ONLY(
|
||||
gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor]
|
||||
) -> torch.fx.GraphModule:
|
||||
for node in gm.graph.nodes:
|
||||
if node.target is torch.relu:
|
||||
node.target = torch._assert
|
||||
node.args = (False, "ReluRuntimeError")
|
||||
gm.recompile()
|
||||
return gm
|
||||
|
||||
|
||||
@register_backend
|
||||
def relu_accuracy_error_TESTING_ONLY(
|
||||
gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor]
|
||||
) -> torch.fx.GraphModule:
|
||||
for node in gm.graph.nodes:
|
||||
if node.target is torch.relu:
|
||||
node.target = torch.add
|
||||
node.args = (node.args[0], 1)
|
||||
gm.recompile()
|
||||
|
||||
return gm
|
||||
|
||||
|
||||
@register_backend
|
||||
def non_leaf_compile_error_TESTING_ONLY(
|
||||
gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor]
|
||||
) -> torch.fx.GraphModule:
|
||||
# Require at least one non-trivial thing in the graph,
|
||||
# see https://github.com/pytorch/pytorch/issues/102898
|
||||
for node in gm.graph.nodes:
|
||||
if node.op == "call_function":
|
||||
break
|
||||
else:
|
||||
return gm
|
||||
for t in example_inputs:
|
||||
if not t.is_leaf:
|
||||
raise TestingOnlyCompileError
|
||||
return gm
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ExplainOutput:
|
||||
"""
|
||||
This is the output of :func:`torch._dynamo.explain()`
|
||||
There is no reason to create this class directly.
|
||||
"""
|
||||
|
||||
graphs: list[torch.fx.GraphModule]
|
||||
graph_count: int
|
||||
graph_break_count: int
|
||||
break_reasons: list[GraphCompileReason]
|
||||
op_count: int
|
||||
ops_per_graph: list[list["Target"]] | None = None
|
||||
out_guards: list[_guards.Guard] | None = None
|
||||
compile_times: str | None = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
output = f"Graph Count: {self.graph_count}\n"
|
||||
output += f"Graph Break Count: {self.graph_break_count}\n"
|
||||
output += f"Op Count: {self.op_count}\n"
|
||||
|
||||
output += "Break Reasons:\n"
|
||||
for idx, break_reason in enumerate(self.break_reasons):
|
||||
output += f" Break Reason {idx + 1}:\n"
|
||||
output += f" Reason: {break_reason.reason}\n"
|
||||
output += " User Stack:\n"
|
||||
for frame_summary in break_reason.user_stack:
|
||||
output += f" {frame_summary}\n"
|
||||
|
||||
if self.ops_per_graph is not None:
|
||||
output += "Ops per Graph:\n"
|
||||
for idx, ops in enumerate(self.ops_per_graph):
|
||||
output += f" Ops {idx + 1}:\n"
|
||||
for op in ops:
|
||||
output += f" {op}\n"
|
||||
|
||||
if self.out_guards is not None:
|
||||
output += "Out Guards:\n"
|
||||
for i, guard in enumerate(self.out_guards):
|
||||
output += f" Guard {i + 1}:\n"
|
||||
output += f" {str(guard)}"
|
||||
|
||||
if self.compile_times is not None:
|
||||
output += f"Compile Times: {self.compile_times}\n"
|
||||
return output
|
||||
|
||||
|
||||
def _explain_graph_detail(
|
||||
gm: torch.fx.GraphModule,
|
||||
graphs: list[torch.fx.GraphModule],
|
||||
op_count: int,
|
||||
ops_per_graph: list[list["Target"]],
|
||||
break_reasons: list[GraphCompileReason],
|
||||
) -> tuple[
|
||||
torch.fx.GraphModule,
|
||||
list[torch.fx.GraphModule],
|
||||
int,
|
||||
list[list["Target"]],
|
||||
list[GraphCompileReason],
|
||||
]:
|
||||
"""
|
||||
This function is a utility which processes a torch.fx.GraphModule and
|
||||
accumulates information about its ops, graph breaks, and other details. It
|
||||
is intended to be used by the ExplainWithBackend class and
|
||||
`torch._dynamo.explain()` to provide details from Dynamo's graph capture.
|
||||
|
||||
Parameters:
|
||||
gm (torch.fx.GraphModule): The GraphModule to be processed.
|
||||
graphs (list): A list that accumulates all the GraphModules processed.
|
||||
op_count (int): The total count of operations in all GraphModules processed so far.
|
||||
ops_per_graph (list): A list that accumulates the operations of each GraphModule.
|
||||
break_reasons (list): A list that accumulates the reasons for breaks in each GraphModule.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple containing the processed GraphModule, the updated lists of graphs,
|
||||
operations per graph, and break reasons, and the updated operation count.
|
||||
"""
|
||||
graphs.append(gm)
|
||||
ops = [node.target for node in gm.graph.nodes if node.op == "call_function"]
|
||||
op_count += len(ops)
|
||||
ops_per_graph.append(ops)
|
||||
if gm.compile_subgraph_reason.graph_break: # type: ignore[union-attr]
|
||||
break_reasons.append(gm.compile_subgraph_reason) # type: ignore[arg-type]
|
||||
|
||||
return gm, graphs, op_count, ops_per_graph, break_reasons
|
||||
|
||||
|
||||
class ExplainWithBackend:
|
||||
"""
|
||||
This class is intended to be used as a backend for `torch.compile`. It is
|
||||
composable with other backends. When used in this way, it accumulates
|
||||
information about graph breaks, ops, and other info and provides a string
|
||||
representation summarizing this information.
|
||||
|
||||
Attributes:
|
||||
backend (str): The name of the backend to use for optimization.
|
||||
graphs (list): A list of the graphs captured by TorchDynamo.
|
||||
op_count (int): The total number of operations in all optimized graphs.
|
||||
break_reasons (list): A list of graph break reasons with stack traces.
|
||||
|
||||
Example Usage:
|
||||
def fn(x):
|
||||
x = torch.sigmoid(x)
|
||||
return x
|
||||
|
||||
torch._dynamo.reset()
|
||||
eb = ExplainWithBackend("inductor")
|
||||
optimized_fn = torch.compile(fn, backend=eb)
|
||||
result = optimized_fn(torch.randn(5))
|
||||
print(eb.output())
|
||||
"""
|
||||
|
||||
def __init__(self, backend: CompilerFn | str) -> None:
|
||||
from .registry import lookup_backend
|
||||
|
||||
self.backend = lookup_backend(backend)
|
||||
self.graphs: list[torch.fx.GraphModule] = []
|
||||
self.op_count = 0
|
||||
self.break_reasons: list[GraphCompileReason] = []
|
||||
|
||||
def __call__(
|
||||
self, gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor]
|
||||
) -> CompiledFn:
|
||||
ops_per_graph: list[list[Target]] = []
|
||||
gm, self.graphs, self.op_count, _, self.break_reasons = _explain_graph_detail(
|
||||
gm, self.graphs, self.op_count, ops_per_graph, self.break_reasons
|
||||
)
|
||||
return self.backend(gm, example_inputs)
|
||||
|
||||
def output(self) -> ExplainOutput:
|
||||
graph_count = len(self.graphs)
|
||||
output = ExplainOutput(
|
||||
self.graphs,
|
||||
graph_count,
|
||||
graph_count - 1,
|
||||
self.break_reasons,
|
||||
self.op_count,
|
||||
)
|
||||
|
||||
return output
|
||||
@@ -0,0 +1,622 @@
|
||||
"""
|
||||
This module implements distributed training optimizations for TorchDynamo backends.
|
||||
|
||||
It provides functionality to optimize models wrapped in DistributedDataParallel (DDP)
|
||||
by intelligently splitting compiled graphs to align with DDP's gradient synchronization
|
||||
boundaries. Key features include:
|
||||
|
||||
- Graph partitioning based on parameter bucket sizes
|
||||
- Optimization of allreduce operations for distributed training
|
||||
- Support for parameter ignoring and buffer handling
|
||||
- Submodule compilation and management
|
||||
- Debugging utilities for distributed training
|
||||
|
||||
The main component is the DDPOptimizer class, which handles graph splitting and
|
||||
recompilation to enable efficient distributed training while maintaining the benefits
|
||||
of compilation.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, TYPE_CHECKING
|
||||
from unittest import mock
|
||||
|
||||
import torch
|
||||
from torch import fx
|
||||
from torch._dynamo.backends.registry import CompiledFn, CompilerFn
|
||||
from torch._dynamo.output_graph import GraphCompileReason
|
||||
from torch._dynamo.utils import deepcopy_to_fake_tensor, detect_fake_mode
|
||||
from torch._logging import trace_structured
|
||||
from torch.fx.node import Node
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch._functorch._aot_autograd.schemas import ViewAndMutationMeta
|
||||
|
||||
|
||||
# Regular log messages should go through 'log'.
|
||||
# ddp_graph_log is a separate artifact logger reserved for dumping graphs.
|
||||
# See docs/source/logging.rst for more info.
|
||||
log = logging.getLogger(__name__)
|
||||
ddp_graph_log = torch._logging.getArtifactLogger(__name__, "ddp_graphs")
|
||||
|
||||
|
||||
def args_str(args: Any) -> str:
|
||||
# a debug helper
|
||||
if torch.is_tensor(args):
|
||||
return f"T[{args.shape}]"
|
||||
elif isinstance(args, tuple):
|
||||
return f"tuple({', '.join([args_str(x) for x in args])})"
|
||||
elif isinstance(args, list):
|
||||
return f"list({', '.join([args_str(x) for x in args])})"
|
||||
else:
|
||||
return str(args)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Bucket:
|
||||
size: int = 0
|
||||
params: list[str] = field(default_factory=list)
|
||||
nodes: list[fx.Node] = field(default_factory=list)
|
||||
|
||||
# param_ids is just used for unit testing
|
||||
param_ids: list[int] = field(default_factory=list)
|
||||
|
||||
# keep track of any buckets that were extended for logging purposes
|
||||
opcount_increased_to_capture_external_output: int = 0
|
||||
paramsize_before_opcount_increase: int = 0
|
||||
|
||||
|
||||
def bucket_has_external_output(bucket: Bucket) -> bool:
|
||||
nodes_in_bucket = set()
|
||||
# we want to iterate in reverse order, but clumsi-luckily the bucket.nodes list was already created backwards
|
||||
# so we don't reverse it here
|
||||
for node in bucket.nodes:
|
||||
# assume node.op != output, since those are filtered in the original iteration
|
||||
nodes_in_bucket.add(node)
|
||||
for user in node.users:
|
||||
if user not in nodes_in_bucket:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def pretty_print_buckets(buckets: list[Bucket], bucket_bytes_cap: int) -> None:
|
||||
headers = ("Index", "Size (b)", "Param Names")
|
||||
rows: list[tuple[int | None, int | None, str]] = []
|
||||
# pyrefly: ignore [implicit-any]
|
||||
extended_buckets = []
|
||||
for idx, bucket in enumerate(reversed(buckets)):
|
||||
if len(bucket.params) > 0:
|
||||
rows.append((idx, bucket.size, bucket.params[0]))
|
||||
rows.extend((None, None, param) for param in bucket.params[1:])
|
||||
if bucket.opcount_increased_to_capture_external_output > 0:
|
||||
extended_buckets.append(
|
||||
(
|
||||
idx,
|
||||
bucket.opcount_increased_to_capture_external_output,
|
||||
bucket.size - bucket.paramsize_before_opcount_increase,
|
||||
)
|
||||
)
|
||||
|
||||
if rows:
|
||||
log.info(
|
||||
"\nDDPOptimizer used bucket cap %s and created %d buckets. Enable debug logs for detailed bucket info.",
|
||||
bucket_bytes_cap,
|
||||
len(buckets),
|
||||
)
|
||||
|
||||
if extended_buckets:
|
||||
log.warning(
|
||||
"Some buckets were extended beyond their requested parameter capacities"
|
||||
" in order to ensure each subgraph has an output node, required for fx graph partitioning."
|
||||
" This can be the case when a subgraph would have only contained nodes performing inplace mutation,"
|
||||
" and returning no logical outputs. This should not be a problem, unless it results in too few graph"
|
||||
" partitions for optimal DDP performance."
|
||||
)
|
||||
|
||||
try:
|
||||
from tabulate import tabulate
|
||||
|
||||
log.debug(
|
||||
"\nDDPOptimizer produced the following bucket assignments:\n%s",
|
||||
tabulate(rows, headers=headers, tablefmt="simple_grid"),
|
||||
)
|
||||
|
||||
if extended_buckets:
|
||||
log.warning(
|
||||
"DDPOptimizer extended these buckets to ensure per-subgraph output nodes:\n%s",
|
||||
tabulate(
|
||||
extended_buckets,
|
||||
headers=("Index", "Extra Ops", "Extra Param Size (b)"),
|
||||
tablefmt="simple_grid",
|
||||
),
|
||||
)
|
||||
except ImportError:
|
||||
log.debug(
|
||||
"Please `pip install tabulate` in order to display ddp bucket sizes and diagnostic information."
|
||||
)
|
||||
else:
|
||||
log.debug("DDPOptimizer captured no parameters and did not split this graph.")
|
||||
|
||||
|
||||
def has_higher_order_op(gm: fx.GraphModule) -> bool:
|
||||
# Check if there is a higher order op in the graph
|
||||
for node in gm.graph.nodes:
|
||||
if node.op == "get_attr":
|
||||
maybe_param = getattr(gm, node.target)
|
||||
if isinstance(maybe_param, torch.fx.GraphModule):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def propagate_metadata(orig_gm: fx.GraphModule, split_gm: fx.GraphModule) -> None:
|
||||
for name, module in split_gm.named_modules():
|
||||
if "." not in name and len(name):
|
||||
# TODO: add split id to CompileId: https://github.com/pytorch/tlparse/pull/83/files#r1880649384
|
||||
module.meta = orig_gm.meta
|
||||
module._param_name_to_source = orig_gm._param_name_to_source
|
||||
|
||||
|
||||
def propagate_dynamo_source(orig_gm: fx.GraphModule, split_gm: fx.GraphModule) -> None:
|
||||
name_to_dynamo_source = {}
|
||||
for node in orig_gm.graph.find_nodes(op="placeholder"):
|
||||
name_to_dynamo_source[node.name] = node._dynamo_source
|
||||
|
||||
for name, module in split_gm.named_modules():
|
||||
if "." not in name and len(name):
|
||||
for node in module.graph.find_nodes(op="placeholder"):
|
||||
# non-placeholder in original_gm may become placeholder in submodules
|
||||
node._dynamo_source = name_to_dynamo_source.get(node.name)
|
||||
|
||||
|
||||
class DDPOptimizerContext:
|
||||
def __init__(self) -> None:
|
||||
self.curr_bucket: int = -1
|
||||
self.metadata_per_bucket: list[ViewAndMutationMeta] = []
|
||||
|
||||
|
||||
# compile each of the partitioned submodules using the user-provided compiler
|
||||
class SubmodCompiler(torch.fx.interpreter.Interpreter):
|
||||
def __init__(
|
||||
self,
|
||||
module: fx.GraphModule,
|
||||
compiler: CompilerFn,
|
||||
fake_mode: torch._subclasses.fake_tensor.FakeTensorMode,
|
||||
) -> None:
|
||||
super().__init__(module)
|
||||
self.compiler = compiler
|
||||
self.fake_mode = fake_mode
|
||||
# See Note [DDPOptimizer and fw_metadata]
|
||||
ctx = torch._guards.TracingContext.try_get()
|
||||
if ctx is not None:
|
||||
ctx.ddp_optimizer_ctx = DDPOptimizerContext()
|
||||
|
||||
def compile_submod(
|
||||
self, input_mod: fx.GraphModule, args: list[torch.Tensor], kwargs: Any
|
||||
) -> Any:
|
||||
"""
|
||||
Compile the submodule,
|
||||
using a wrapper to make sure its output is always a tuple,
|
||||
which is required by AotAutograd based compilers
|
||||
"""
|
||||
assert len(kwargs) == 0, "We assume only args for these modules"
|
||||
|
||||
class WrapperModule(torch.nn.Module):
|
||||
def __init__(
|
||||
self, submod: Callable[..., Any], unwrap_singleton_tuple: bool
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.submod = submod
|
||||
self.unwrap_singleton_tuple = unwrap_singleton_tuple
|
||||
|
||||
def forward(self, *args: Any) -> Any:
|
||||
x = self.submod(*args)
|
||||
# TODO(whc)
|
||||
# for some reason the isinstance check is necessary if I split one node per submod
|
||||
# - even though I supposedly wrapped the output in a tuple in those cases, the real
|
||||
# compiled module was still returning a tensor
|
||||
if self.unwrap_singleton_tuple and isinstance(x, (tuple, list)):
|
||||
return x[0]
|
||||
return x
|
||||
|
||||
unwrap_singleton_tuple = False
|
||||
for sn in input_mod.graph.nodes:
|
||||
if sn.op == "output":
|
||||
if not isinstance(sn.args[0], tuple):
|
||||
unwrap_singleton_tuple = True
|
||||
sn.args = (sn.args,)
|
||||
|
||||
input_mod.recompile()
|
||||
input_mod.compile_subgraph_reason = GraphCompileReason( # type: ignore[assignment]
|
||||
"DDPOptimizer intentional graph-break (See Note [DDPOptimizer])."
|
||||
" Set `torch._dynamo.config.optimize_ddp = False` to disable.",
|
||||
[
|
||||
# it's close to useless to get a real stacktrace here, and quite verbose.
|
||||
traceback.FrameSummary(__file__, 0, "DDPOptimizer"),
|
||||
],
|
||||
)
|
||||
|
||||
wrapper = WrapperModule(
|
||||
self.compiler(input_mod, args),
|
||||
unwrap_singleton_tuple,
|
||||
)
|
||||
return wrapper
|
||||
|
||||
# Note:
|
||||
#
|
||||
# The way distributed works today around fake tensors can be somewhat confusing.
|
||||
# Some of these codepaths are shared in both runtime, and compile time. The presence
|
||||
# of a fake_mode, read off of fake tensor inputs, dictates how we will operate.
|
||||
#
|
||||
# A few things to keep in mind:
|
||||
#
|
||||
# 1) We invoke `compile_submod` with a real module. The output of that gets stored
|
||||
# on the graph via `self.module.add_submodule(n.target, compiled_submod_real)`.
|
||||
#
|
||||
# 2) When running a call_module targeted node, if we have a fake_mode, we fakify the
|
||||
# module we got from self.fetch_attr(n.target). Regardless of fake_mode, we then execute it.
|
||||
#
|
||||
# 3) Fake tensors should always be around during compile time.
|
||||
#
|
||||
# 4) Fake tensors should never be around at runtime.
|
||||
#
|
||||
# 5) We end up with a compilation mode that takes a real submodule and fake tensors,
|
||||
# to match what aot_autograd expects. See Note: [Fake Modules and AOTAutograd]
|
||||
def run_node(self, n: Node) -> Any:
|
||||
args, kwargs = self.fetch_args_kwargs_from_env(n)
|
||||
new_args = []
|
||||
assert self.fake_mode
|
||||
for arg in args:
|
||||
if isinstance(arg, torch.Tensor) and not isinstance(
|
||||
arg, torch._subclasses.FakeTensor
|
||||
):
|
||||
new_args.append(torch._dynamo.utils.to_fake_tensor(arg, self.fake_mode))
|
||||
else:
|
||||
new_args.append(arg)
|
||||
|
||||
log.debug("run_node %s, %s got args %s", n.op, n.target, args_str(args))
|
||||
assert isinstance(args, tuple)
|
||||
assert isinstance(kwargs, dict)
|
||||
|
||||
if n.op == "call_module":
|
||||
real_mod = self.fetch_attr(str(n.target))
|
||||
if self.fake_mode:
|
||||
curr_submod = deepcopy_to_fake_tensor(real_mod, self.fake_mode)
|
||||
else:
|
||||
curr_submod = real_mod
|
||||
|
||||
ddp_graph_log.debug("\n---%s graph---\n%s", n.target, curr_submod.graph)
|
||||
|
||||
# When calling the compiler on the submod, inputs (new_args) are expected to
|
||||
# be FakeTensors already since Dynamo would have made them FakeTensors in the
|
||||
# non-DDP flow. However, the parameters are _not_ expected to be FakeTensors,
|
||||
# since this wrapping happens during compilation
|
||||
|
||||
# Note: Returning Fake Tensors on First AOT Autograd Call
|
||||
#
|
||||
# Inductor will optimize strides of outputs when it deems it profitable.
|
||||
# For instance, converting to channels last. When we split the graph here
|
||||
# into multiple inductor compilations, we need to make sure that the
|
||||
# output strides of one compilation is appropriately passed to the subsequent
|
||||
# compilations. However, the mapping from inductor output to dynamo output
|
||||
# is non-trivial due to aot_autograd's deduping, de-aliasing, mutation, re-writing,
|
||||
# subclass handling, etc. In order to replay all this logic we set a flag such that
|
||||
# the first invocation of inductor in aot_autograd will return Fake Tensors with
|
||||
# appropriate strides. Then, all of aot autograd's runtime logic is replayed.
|
||||
# This gives us the appropriately strided outputs here which will reflect runtime strides.
|
||||
|
||||
class FakeifyFirstAOTInvocationGuard:
|
||||
def __init__(self) -> None:
|
||||
self.tc = torch._guards.TracingContext.try_get()
|
||||
assert self.tc
|
||||
self.tc.fakify_first_call = True
|
||||
|
||||
def __del__(self) -> None:
|
||||
self.tc.fakify_first_call = False # type: ignore[union-attr]
|
||||
|
||||
# For aot_eager and other backends, tracing context is not set
|
||||
has_tracing_context = torch._guards.TracingContext.try_get() is not None
|
||||
if has_tracing_context:
|
||||
g = FakeifyFirstAOTInvocationGuard() # noqa: F841
|
||||
|
||||
from torch._dynamo.utils import counters
|
||||
|
||||
init = counters["aot_autograd"]["total"]
|
||||
compiled_submod_real = self.compile_submod(real_mod, new_args, kwargs)
|
||||
|
||||
# TODO - better way of doing this?
|
||||
# Only aot autograd handles fakifying first call
|
||||
invoked_aot_autograd = init != counters["aot_autograd"]["total"]
|
||||
|
||||
# We update the original (outer) graph with a call into the compiled module
|
||||
# instead of the uncompiled one.
|
||||
self.module.delete_submodule(n.target) # type: ignore[operator]
|
||||
n.target = "compiled_" + n.target # type: ignore[operator]
|
||||
self.module.add_submodule(n.target, compiled_submod_real) # type: ignore[operator]
|
||||
|
||||
# Finally, we have to produce inputs for use compiling the next submodule,
|
||||
# and these need to be FakeTensors, so we execute the module under fake_mode
|
||||
# Because parameters are not fake we patch fake tensor mode to allow non fake inputs
|
||||
with (
|
||||
self.fake_mode,
|
||||
mock.patch.object(self.fake_mode, "allow_non_fake_inputs", True),
|
||||
):
|
||||
if has_tracing_context and invoked_aot_autograd:
|
||||
tracing_ctx = torch._guards.TracingContext.try_get()
|
||||
assert tracing_ctx is not None
|
||||
# DDPOptimizer maintains 1 dynamo graph -> N AOT graphs
|
||||
# Dynamo only has 1 tracing context, so it needs to maintain all N AOT metadata instances
|
||||
ddp_ctx = tracing_ctx.ddp_optimizer_ctx
|
||||
assert ddp_ctx is not None
|
||||
assert tracing_ctx.fw_metadata is not None
|
||||
ddp_ctx.curr_bucket += 1
|
||||
ddp_ctx.metadata_per_bucket.append(tracing_ctx.fw_metadata)
|
||||
|
||||
out = compiled_submod_real(*new_args, **kwargs)
|
||||
# output should be fake or subclass
|
||||
assert all(
|
||||
(not isinstance(t, torch.Tensor) or type(t) is not torch.Tensor)
|
||||
for t in (out if isinstance(out, (list, tuple)) else [out])
|
||||
)
|
||||
return out
|
||||
else:
|
||||
return curr_submod(*new_args, **kwargs)
|
||||
else:
|
||||
# placeholder or output nodes don't need to get compiled, just executed
|
||||
return getattr(self, n.op)(n.target, new_args, kwargs)
|
||||
|
||||
|
||||
class DDPOptimizer:
|
||||
"""Note [DDPOptimizer]
|
||||
DDPOptimizer applies when dynamo compiles models wrapped in DistributedDataParallel (DDP),
|
||||
breaking the dynamo graph into chunks to compile separately, with the breaks aligning to
|
||||
the boundaries of gradient-allreduce buckets chosen by DDP.
|
||||
|
||||
Background/Motivation
|
||||
- DDP uses allreduce collectives to synchronize partial gradients computed on different workers
|
||||
- DDP groups gradient allreduces into 'buckets' to optimize communication efficiency of all-reduce
|
||||
- Parameters grouped into buckets are assumed to be adjacent in time, so they become ready
|
||||
at around the same time during backward and thus can share the same allreduce efficiently
|
||||
- Allreduces must overlap with backward compute for optimal training performance
|
||||
- DDP schedules allreduces using 'hooks' fired from the c++ autograd engine in pytorch, which
|
||||
operates when individual grads become 'ready'
|
||||
- Dynamo+AOTAutograd produces a single fused graph that runs 'atomically' from the perspective of the
|
||||
autograd engine, such that all gradients become 'ready' at the same time. Hooks fire after the whole
|
||||
fused backward function executes, preventing any overlap of compute and communication
|
||||
|
||||
Algorithm
|
||||
- DDPOptimizer starts off with an FX graph traced by dynamo which represents forward. It can traverse
|
||||
this graph in reverse order to determine the true order that gradients will become ready during backward.
|
||||
- Parameter sizes are counted in reverse order, up to a bucket size limit, at which point a new bucket is started
|
||||
and a graph break introduced
|
||||
- Each of the subgraphs is compiled by the compiler provided to dynamo by the user, and then fused back together
|
||||
into an outer module that is returned to the user
|
||||
|
||||
Notes
|
||||
- It would be better to enforce (by adding an API to DDP) that the bucket splits chosen here are used by DDP,
|
||||
and that DDP does not need to detect or optimize bucket order by observing execution at runtime, as it does
|
||||
in eager.
|
||||
- If Dynamo can't capture a whole graph for the portion of the model wrapped by DDP, this algorithm will currently
|
||||
produce splits that do not necessarily align with the buckets used by DDP. This should result in performance
|
||||
degradation approaching the baseline case where graph-splits are not used, but not worse.
|
||||
- If the backend compiler fails to compile a single subgraph, it will execute eagerly despite the rest of the
|
||||
subgraphs being compiled
|
||||
- DDP has a 'parameters_and_buffers_to_ignore' field, which DDPOptimizer attempts to honor by reading markers
|
||||
left by DDP on individual parameters. In cases where other transformations, such as reparameterization, are
|
||||
also used, the ignore markers could be lost. If DDPOptimizer fails to ignore a parameter ignored by DDP,
|
||||
it is not catastrophic but could impact performance by choosing sub-optimal bucket splits.
|
||||
- DDPOptimizer always ignores all buffers, regardless of their ignore flag, since buffers do not require gradients,
|
||||
and therefore aren't allreduced by DDP. (They are broadcast during forward, but this is not covered by
|
||||
DDPOptimizer)
|
||||
|
||||
Debugging
|
||||
- Generally, it is easiest to debug DDPOptimizer in a single process program, using pdb.
|
||||
- In many cases, the log messages are helpful (they show bucket size assignments)-
|
||||
just set TORCH_LOGS env to include any of 'dynamo', 'distributed', or 'dist_ddp'.
|
||||
- See `benchmarks/dynamo/distributed.py` for a simple harness that will run a toy model or a torchbench model
|
||||
in a single process (or with torchrun, in multiple processes)
|
||||
|
||||
Args:
|
||||
bucket_bytes_cap (int): Controls the size of buckets, in bytes, used to determine graphbreaks. Should be
|
||||
set to match the equivalent parameter on the original DDP module.
|
||||
|
||||
backend_compile_fn (callable): A dynamo compiler function, to be invoked to compile each subgraph.
|
||||
|
||||
first_bucket_cap (int): Controls the size of the first bucket. Should match DDP's first bucket cap. DDP
|
||||
special-cases the first bucket size since it is sometimes optimal to start a small allreduce early.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bucket_bytes_cap: int,
|
||||
backend_compile_fn: CompilerFn,
|
||||
first_bucket_cap: int | None = None,
|
||||
) -> None:
|
||||
if first_bucket_cap is not None:
|
||||
self.first_bucket_cap = first_bucket_cap
|
||||
elif torch.distributed.is_available():
|
||||
# this constant comes from C10D lib which is not always built
|
||||
self.first_bucket_cap = torch.distributed._DEFAULT_FIRST_BUCKET_BYTES
|
||||
else:
|
||||
self.first_bucket_cap = bucket_bytes_cap
|
||||
|
||||
self.bucket_bytes_cap = bucket_bytes_cap
|
||||
assert self.first_bucket_cap <= self.bucket_bytes_cap, (
|
||||
"First bucket should be smaller/equal to other buckets to get comms warmed up ASAP"
|
||||
)
|
||||
|
||||
self.backend_compile_fn = backend_compile_fn
|
||||
|
||||
def _ignore_parameter(self, parameter: torch.nn.Parameter) -> bool:
|
||||
return hasattr(parameter, "_ddp_ignored") and parameter._ddp_ignored
|
||||
|
||||
def add_param(self, bucket: Bucket, param: torch.nn.Parameter, name: str) -> None:
|
||||
bucket.size += param.untyped_storage().nbytes()
|
||||
bucket.params.append(name)
|
||||
bucket.param_ids.append(id(param))
|
||||
|
||||
def add_module_params_to_bucket(
|
||||
self,
|
||||
mod: torch.nn.Module,
|
||||
bucket: Bucket,
|
||||
processed_modules: set[torch.nn.Module],
|
||||
prefix: str,
|
||||
) -> None:
|
||||
processed_modules.add(mod)
|
||||
for name, param in mod.named_parameters():
|
||||
if param.requires_grad and not self._ignore_parameter(param):
|
||||
self.add_param(bucket, param, f"{prefix}_{name}")
|
||||
|
||||
def add_param_args(self, bucket: Bucket, node: fx.Node) -> None:
|
||||
for arg in node.args:
|
||||
if not isinstance(arg, torch.fx.node.Node):
|
||||
continue
|
||||
if arg.op != "placeholder":
|
||||
continue
|
||||
param = arg.meta["example_value"]
|
||||
if (
|
||||
isinstance(param, torch.nn.Parameter)
|
||||
and param.requires_grad
|
||||
and not self._ignore_parameter(param)
|
||||
):
|
||||
self.add_param(bucket, param, str(arg.target))
|
||||
|
||||
def compile_fn(
|
||||
self, gm: fx.GraphModule, example_inputs: list[torch.Tensor]
|
||||
) -> CompiledFn:
|
||||
"""
|
||||
Implements graph splitting, first determining a set of of buckets by counting
|
||||
parameter sizes in reverse graph order, then invoking the user/backend compiler
|
||||
to compile each subgraph. Finally, stitches compiled graphs into one graphmodule
|
||||
and returns its callable.
|
||||
"""
|
||||
# 1: compute the partition map according to DDP bucket logic
|
||||
buckets = [Bucket()] # (size, param_names)
|
||||
processed_modules: set[torch.nn.Module] = set()
|
||||
for node in reversed(gm.graph.nodes):
|
||||
if node.op in ("output", "placeholder"):
|
||||
continue
|
||||
|
||||
if (
|
||||
buckets[0].size >= self.bucket_bytes_cap
|
||||
or len(buckets) == 1
|
||||
and buckets[0].size >= self.first_bucket_cap
|
||||
):
|
||||
if bucket_has_external_output(buckets[0]):
|
||||
buckets.insert(0, Bucket())
|
||||
else:
|
||||
# continue building this bucket past the point of filling its parameter capacity,
|
||||
# to increase chances it contains at least one node that is either a global output or
|
||||
# passed as input to a subsequent graph
|
||||
|
||||
if buckets[0].opcount_increased_to_capture_external_output == 0:
|
||||
buckets[0].paramsize_before_opcount_increase = buckets[0].size
|
||||
buckets[0].opcount_increased_to_capture_external_output += 1
|
||||
|
||||
if node.op == "call_function":
|
||||
self.add_param_args(buckets[0], node)
|
||||
|
||||
elif node.op == "call_module":
|
||||
target_mod = gm.get_submodule(node.target)
|
||||
if target_mod not in processed_modules:
|
||||
self.add_module_params_to_bucket(
|
||||
target_mod, buckets[0], processed_modules, node.target
|
||||
)
|
||||
elif node.op == "call_method":
|
||||
if isinstance(node.args[0].target, str):
|
||||
target_mod = None
|
||||
try:
|
||||
target_mod = gm.get_submodule(node.args[0].target)
|
||||
except AttributeError:
|
||||
pass
|
||||
if target_mod is not None and target_mod not in processed_modules:
|
||||
self.add_module_params_to_bucket(
|
||||
target_mod, buckets[0], processed_modules, node.target
|
||||
)
|
||||
# This handles situations like tmp = torch.mm(x, self.weight.t())
|
||||
# t: "f32[512, 512]" = l_self_seq_2_weight.t(); l_self_seq_2_weight = None
|
||||
# tmp: "f32[512, 512]" = torch.mm(input_2, t); input_2 = t = None
|
||||
self.add_param_args(buckets[0], node)
|
||||
|
||||
elif node.op == "get_attr":
|
||||
maybe_param = getattr(gm, node.target)
|
||||
if (
|
||||
isinstance(maybe_param, torch.nn.Parameter)
|
||||
and maybe_param.requires_grad
|
||||
and not self._ignore_parameter(maybe_param)
|
||||
):
|
||||
self.add_param(buckets[0], maybe_param, node.target)
|
||||
|
||||
# All nodes have to be mapped to a bucket, even if they don't have their own params
|
||||
# Ignored params still end up in buckets, we just don't count them towards the capacity
|
||||
buckets[0].nodes.append(node)
|
||||
|
||||
if len(buckets) > 1 and buckets[0].size == 0:
|
||||
# we collected a small preamble graph with ops that don't include parameters, fuse it back
|
||||
buckets[1].nodes.extend(buckets[0].nodes)
|
||||
assert len(buckets[0].params) == 0, "Params should be empty if size is 0"
|
||||
del buckets[0]
|
||||
|
||||
# stash buckets for testing/debugging purposes
|
||||
self.buckets = buckets
|
||||
pretty_print_buckets(buckets, self.bucket_bytes_cap)
|
||||
|
||||
if len(buckets) == 1:
|
||||
# bypass split/fuse logic if there is only one bucket
|
||||
return self.backend_compile_fn(gm, example_inputs)
|
||||
|
||||
# 2: partition the graphmodule according to bucket capacity
|
||||
partition_map = {}
|
||||
for idx, b in enumerate(buckets):
|
||||
for node in b.nodes:
|
||||
partition_map[node] = idx
|
||||
|
||||
split_gm = fx.passes.split_module.split_module(
|
||||
gm,
|
||||
None, # type: ignore[arg-type]
|
||||
lambda node: partition_map[node],
|
||||
)
|
||||
|
||||
# See note [Assumption on Dynamo Metadata]
|
||||
propagate_dynamo_source(gm, split_gm)
|
||||
propagate_metadata(gm, split_gm)
|
||||
|
||||
debug_str = (
|
||||
f"\n---orig graph---\n{gm.graph}\n"
|
||||
+ f"\n---split graph---\n{split_gm.graph}\n"
|
||||
)
|
||||
for name, module in split_gm.named_modules():
|
||||
if "." not in name and len(name):
|
||||
# only print the submod graphs, not their children
|
||||
debug_str += f"\n---{name} graph---\n{module.graph}\n"
|
||||
debug_str += "\n---------------\n"
|
||||
ddp_graph_log.debug(debug_str)
|
||||
|
||||
trace_structured(
|
||||
"optimize_ddp_split_graph",
|
||||
payload_fn=lambda: split_gm.print_readable(print_output=False),
|
||||
)
|
||||
for name, module in split_gm.named_modules():
|
||||
if "." not in name and len(name):
|
||||
trace_structured(
|
||||
"optimize_ddp_split_child",
|
||||
lambda: {"name": name},
|
||||
payload_fn=lambda: module.print_readable(print_output=False),
|
||||
)
|
||||
|
||||
fake_mode = detect_fake_mode(example_inputs)
|
||||
if fake_mode is None:
|
||||
fake_mode = torch._subclasses.fake_tensor.FakeTensorMode()
|
||||
|
||||
submod_compiler = SubmodCompiler(split_gm, self.backend_compile_fn, fake_mode)
|
||||
with torch._dynamo.utils._disable_saved_tensors_hooks_during_tracing():
|
||||
submod_compiler.run(*example_inputs)
|
||||
split_gm.recompile()
|
||||
|
||||
ddp_graph_log.debug(
|
||||
"\n---final graph---\n%s\n---------------\n", split_gm.graph
|
||||
)
|
||||
return split_gm
|
||||
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
This module provides the TorchInductor backend integration for TorchDynamo.
|
||||
|
||||
TorchInductor is a compiler backend that generates optimized code for both CPU and GPU.
|
||||
This module lazily imports and registers the TorchInductor compiler to avoid loading it
|
||||
into memory when it is not being used. This helps reduce memory overhead when using
|
||||
other backends.
|
||||
|
||||
The inductor backend can be used with torch.compile():
|
||||
model = torch.compile(model, backend="inductor")
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from torch._dynamo import register_backend
|
||||
from torch._dynamo.utils import dynamo_timed
|
||||
|
||||
|
||||
@register_backend
|
||||
def inductor(*args: Any, **kwargs: Any) -> Any:
|
||||
with dynamo_timed("inductor_import", log_pt2_compile_event=True):
|
||||
# do import here to avoid loading inductor into memory when it is not used
|
||||
# The AsyncCompile subproc pool can be slow to start, so warm it up as early
|
||||
# as possible.
|
||||
from torch._inductor.async_compile import maybe_warm_pool
|
||||
|
||||
maybe_warm_pool()
|
||||
|
||||
from torch._inductor.compile_fx import compile_fx
|
||||
|
||||
return compile_fx(*args, **kwargs)
|
||||
@@ -0,0 +1,39 @@
|
||||
# This backend is maintained by ONNX team. To direct issues
|
||||
# to the right people, please tag related GitHub issues with `module: onnx`.
|
||||
#
|
||||
# Maintainers' Github IDs: wschin, xadupre
|
||||
# from torch.onnx._internal.onnxruntime import (
|
||||
# is_onnxrt_backend_supported,
|
||||
# torch_compile_backend,
|
||||
# )
|
||||
|
||||
# from .registry import register_backend
|
||||
|
||||
"""
|
||||
Placeholder for onnxruntime backend for dynamo
|
||||
"""
|
||||
|
||||
# def has_onnxruntime():
|
||||
# # FIXME: update test/dynamo/test_backends.py to call is_onnxrt_backend_supported()
|
||||
# return is_onnxrt_backend_supported()
|
||||
|
||||
|
||||
# if is_onnxrt_backend_supported():
|
||||
# register_backend(name="onnxrt", compiler_fn=torch_compile_backend)
|
||||
# else:
|
||||
|
||||
# def information_displaying_backend(*args, **kwargs):
|
||||
# raise ImportError(
|
||||
# "onnxrt is not registered as a backend. "
|
||||
# "Please make sure all dependencies such as "
|
||||
# "numpy, onnx, onnxscript, and onnxruntime-training are installed. "
|
||||
# "Suggested procedure to fix dependency problem:\n"
|
||||
# " (1) pip or conda install numpy onnx onnxscript onnxruntime-training.\n"
|
||||
# " (2) Open a new python terminal.\n"
|
||||
# " (3) Call the API `torch.onnx.is_onnxrt_backend_supported()`:\n"
|
||||
# " (4) If it returns `True`, then you can use `onnxrt` backend.\n"
|
||||
# " (5) If it returns `False`, please execute the package importing section in "
|
||||
# "torch/onnx/_internal/onnxruntime.py under pdb line-by-line to see which import fails."
|
||||
# )
|
||||
|
||||
# register_backend(name="onnxrt", compiler_fn=information_displaying_backend)
|
||||
@@ -0,0 +1,206 @@
|
||||
"""
|
||||
This module implements TorchDynamo's backend registry system for managing compiler backends.
|
||||
|
||||
The registry provides a centralized way to register, discover and manage different compiler
|
||||
backends that can be used with torch.compile(). It handles:
|
||||
|
||||
- Backend registration and discovery through decorators and entry points
|
||||
- Lazy loading of backend implementations
|
||||
- Lookup and validation of backend names
|
||||
- Categorization of backends using tags (debug, experimental, etc.)
|
||||
|
||||
Key components:
|
||||
- CompilerFn: Type for backend compiler functions that transform FX graphs
|
||||
- _BACKENDS: Registry mapping backend names to entry points
|
||||
- _COMPILER_FNS: Registry mapping backend names to loaded compiler functions
|
||||
|
||||
Example usage:
|
||||
@register_backend
|
||||
def my_compiler(fx_graph, example_inputs):
|
||||
# Transform FX graph into optimized implementation
|
||||
return compiled_fn
|
||||
|
||||
# Use registered backend
|
||||
torch.compile(model, backend="my_compiler")
|
||||
|
||||
The registry also supports discovering backends through setuptools entry points
|
||||
in the "torch_dynamo_backends" group. Example:
|
||||
```
|
||||
setup.py
|
||||
---
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='my_torch_backend',
|
||||
version='0.1',
|
||||
packages=['my_torch_backend'],
|
||||
entry_points={
|
||||
'torch_dynamo_backends': [
|
||||
# name = path to entry point of backend implementation
|
||||
'my_compiler = my_torch_backend.compiler:my_compiler_function',
|
||||
],
|
||||
},
|
||||
)
|
||||
```
|
||||
```
|
||||
my_torch_backend/compiler.py
|
||||
---
|
||||
def my_compiler_function(fx_graph, example_inputs):
|
||||
# Transform FX graph into optimized implementation
|
||||
return compiled_fn
|
||||
```
|
||||
Using `my_compiler` backend:
|
||||
```
|
||||
import torch
|
||||
|
||||
model = ... # Your PyTorch model
|
||||
optimized_model = torch.compile(model, backend="my_compiler")
|
||||
```
|
||||
"""
|
||||
|
||||
import functools
|
||||
import logging
|
||||
from collections.abc import Callable, Sequence
|
||||
from importlib.metadata import EntryPoint
|
||||
from typing import Any, Protocol
|
||||
|
||||
import torch
|
||||
from torch import fx
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CompiledFn(Protocol):
|
||||
def __call__(self, *args: torch.Tensor) -> tuple[torch.Tensor, ...]: ...
|
||||
|
||||
|
||||
CompilerFn = Callable[[fx.GraphModule, list[torch.Tensor]], CompiledFn]
|
||||
|
||||
_BACKENDS: dict[str, EntryPoint | None] = {}
|
||||
_COMPILER_FNS: dict[str, CompilerFn] = {}
|
||||
|
||||
|
||||
def register_backend(
|
||||
compiler_fn: CompilerFn | None = None,
|
||||
name: str | None = None,
|
||||
tags: Sequence[str] = (),
|
||||
) -> Callable[..., Any]:
|
||||
"""
|
||||
Decorator to add a given compiler to the registry to allow calling
|
||||
`torch.compile` with string shorthand. Note: for projects not
|
||||
imported by default, it might be easier to pass a function directly
|
||||
as a backend and not use a string.
|
||||
|
||||
Args:
|
||||
compiler_fn: Callable taking a FX graph and fake tensor inputs
|
||||
name: Optional name, defaults to `compiler_fn.__name__`
|
||||
tags: Optional set of string tags to categorize backend with
|
||||
"""
|
||||
if compiler_fn is None:
|
||||
# @register_backend(name="") syntax
|
||||
return functools.partial(register_backend, name=name, tags=tags) # type: ignore[return-value]
|
||||
assert callable(compiler_fn)
|
||||
name = name or compiler_fn.__name__
|
||||
assert name not in _COMPILER_FNS, f"duplicate name: {name}"
|
||||
if compiler_fn not in _BACKENDS:
|
||||
_BACKENDS[name] = None
|
||||
_COMPILER_FNS[name] = compiler_fn
|
||||
compiler_fn._tags = tuple(tags) # type: ignore[attr-defined]
|
||||
return compiler_fn
|
||||
|
||||
|
||||
register_debug_backend = functools.partial(register_backend, tags=("debug",))
|
||||
register_experimental_backend = functools.partial(
|
||||
register_backend, tags=("experimental",)
|
||||
)
|
||||
|
||||
|
||||
def lookup_backend(compiler_fn: str | CompilerFn) -> CompilerFn:
|
||||
"""Expand backend strings to functions"""
|
||||
if isinstance(compiler_fn, str):
|
||||
if compiler_fn not in _BACKENDS:
|
||||
_lazy_import()
|
||||
if compiler_fn not in _BACKENDS:
|
||||
from ..exc import InvalidBackend
|
||||
|
||||
raise InvalidBackend(name=compiler_fn)
|
||||
|
||||
if compiler_fn not in _COMPILER_FNS:
|
||||
entry_point = _BACKENDS[compiler_fn]
|
||||
if entry_point is not None:
|
||||
register_backend(compiler_fn=entry_point.load(), name=compiler_fn)
|
||||
compiler_fn = _COMPILER_FNS[compiler_fn]
|
||||
return compiler_fn
|
||||
|
||||
|
||||
# NOTE: can't type this due to public api mismatch; follow up with dev team
|
||||
def list_backends(exclude_tags=("debug", "experimental")) -> list[str]: # type: ignore[no-untyped-def]
|
||||
"""
|
||||
Return valid strings that can be passed to:
|
||||
|
||||
torch.compile(..., backend="name")
|
||||
"""
|
||||
_lazy_import()
|
||||
exclude_tags_set = set(exclude_tags or ())
|
||||
|
||||
backends = [
|
||||
name
|
||||
for name in _BACKENDS
|
||||
if name not in _COMPILER_FNS
|
||||
or not exclude_tags_set.intersection(_COMPILER_FNS[name]._tags) # type: ignore[attr-defined]
|
||||
]
|
||||
return sorted(backends)
|
||||
|
||||
|
||||
@functools.cache
|
||||
def _lazy_import() -> None:
|
||||
from .. import backends
|
||||
from ..utils import import_submodule
|
||||
|
||||
import_submodule(backends)
|
||||
|
||||
from ..repro.after_dynamo import dynamo_minifier_backend
|
||||
|
||||
assert dynamo_minifier_backend is not None
|
||||
|
||||
_discover_entrypoint_backends()
|
||||
|
||||
|
||||
@functools.cache
|
||||
def _discover_entrypoint_backends() -> None:
|
||||
# importing here so it will pick up the mocked version in test_backends.py
|
||||
from importlib.metadata import entry_points
|
||||
|
||||
group_name = "torch_dynamo_backends"
|
||||
eps = entry_points(group=group_name)
|
||||
# pyrefly: ignore [bad-index]
|
||||
eps_dict = {name: eps[name] for name in eps.names}
|
||||
for backend_name in eps_dict:
|
||||
_BACKENDS[backend_name] = eps_dict[backend_name]
|
||||
|
||||
|
||||
def _is_registered_backend(compiler_fn: CompilerFn) -> bool:
|
||||
"""
|
||||
Check if the given compiler function is a registered backend.
|
||||
Custom backends (user-provided callables not in the registry) return False.
|
||||
"""
|
||||
# Ensure backends are loaded
|
||||
_lazy_import()
|
||||
|
||||
# Check if it's directly a registered backend function
|
||||
if compiler_fn in _COMPILER_FNS.values():
|
||||
return True
|
||||
|
||||
# Check for _TorchCompileInductorWrapper or _TorchCompileWrapper
|
||||
# These have a compiler_name attribute that identifies the backend
|
||||
if hasattr(compiler_fn, "compiler_name"):
|
||||
compiler_name = compiler_fn.compiler_name
|
||||
if compiler_name in _BACKENDS or compiler_name in _COMPILER_FNS:
|
||||
return True
|
||||
|
||||
# Check if the wrapper has a compiler_fn attribute (e.g., _TorchCompileWrapper)
|
||||
if hasattr(compiler_fn, "compiler_fn"):
|
||||
return compiler_fn.compiler_fn in _COMPILER_FNS.values()
|
||||
|
||||
return False
|
||||
@@ -0,0 +1,12 @@
|
||||
# import torch # type: ignore[import]
|
||||
# from .common import device_from_inputs, fake_tensor_unsupported # type: ignore[import]
|
||||
# from .registry import register_backend # type: ignore[import]
|
||||
|
||||
"""
|
||||
Placeholder for TensorRT backend for dynamo via torch-tensorrt
|
||||
"""
|
||||
|
||||
# @register_backend
|
||||
# def tensorrt(gm, example_inputs):
|
||||
# import torch_tensorrt # type: ignore[import]
|
||||
# pass
|
||||
@@ -0,0 +1,55 @@
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from functorch.compile import make_boxed_func
|
||||
from torch import fx
|
||||
|
||||
from ..backends.common import aot_autograd
|
||||
from .registry import CompiledFn, register_backend, register_experimental_backend
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register_experimental_backend
|
||||
def openxla_eval(
|
||||
model: fx.GraphModule, fake_tensor_inputs: list[torch.Tensor]
|
||||
) -> CompiledFn:
|
||||
return xla_backend_helper(model, fake_tensor_inputs, boxed=False)
|
||||
|
||||
|
||||
def openxla_eval_boxed(
|
||||
model: fx.GraphModule, fake_tensor_inputs: list[torch.Tensor]
|
||||
) -> Callable[..., Any]:
|
||||
return xla_backend_helper(model, fake_tensor_inputs, boxed=True)
|
||||
|
||||
|
||||
def xla_backend_helper(
|
||||
model: fx.GraphModule, fake_tensor_inputs: list[torch.Tensor], boxed: bool = False
|
||||
) -> Callable[..., Any]:
|
||||
try:
|
||||
import torch_xla.core.dynamo_bridge as bridge
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
"Please follow the instruction in https://github.com/pytorch/xla#pytorchxla to install torch_xla"
|
||||
) from e
|
||||
|
||||
compiled_graph = None
|
||||
|
||||
def fwd(*args: torch.Tensor) -> Any:
|
||||
nonlocal model
|
||||
nonlocal compiled_graph
|
||||
if compiled_graph is None:
|
||||
compiled_graph = bridge.extract_compiled_graph(model, args)
|
||||
del model
|
||||
return compiled_graph(*args)
|
||||
|
||||
return make_boxed_func(fwd) if boxed else fwd
|
||||
|
||||
|
||||
openxla = aot_autograd(
|
||||
fw_compiler=openxla_eval_boxed,
|
||||
)
|
||||
register_backend(name="openxla", compiler_fn=openxla)
|
||||
@@ -0,0 +1,197 @@
|
||||
"""
|
||||
This module provides TVM backend integration for TorchDynamo.
|
||||
|
||||
Apache TVM is a deep learning compiler framework that can optimize and execute
|
||||
models on various hardware backends. This module enables:
|
||||
|
||||
- Compilation of PyTorch models to TVM's computation graphs
|
||||
- Multiple scheduling options:
|
||||
- Default scheduler
|
||||
- Auto-scheduler for automatic optimization
|
||||
- Meta-schedule for evolutionary search-based tuning
|
||||
- Hardware-specific optimizations:
|
||||
- CUDA GPU support
|
||||
- CPU support with LLVM targeting and architecture-specific tuning
|
||||
- Automatic detection of CPU capabilities (AVX2, AVX512)
|
||||
- Tensor conversion utilities between PyTorch and TVM formats
|
||||
- Configurable optimization levels and tuning trials
|
||||
|
||||
The backend can be used with torch.compile():
|
||||
model = torch.compile(model, backend="tvm")
|
||||
"""
|
||||
|
||||
import functools
|
||||
import importlib
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from torch import fx
|
||||
|
||||
from .common import device_from_inputs, fake_tensor_unsupported
|
||||
from .registry import register_backend
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register_backend
|
||||
@fake_tensor_unsupported # type: ignore[arg-type]
|
||||
def tvm(
|
||||
gm: fx.GraphModule,
|
||||
example_inputs: list[torch.Tensor],
|
||||
*,
|
||||
options: MappingProxyType[str, Any] | None = None,
|
||||
) -> Callable[..., Any]:
|
||||
if options is None:
|
||||
options = MappingProxyType({"scheduler": None, "trials": 20000, "opt_level": 3})
|
||||
assert options is not None
|
||||
import tvm # type: ignore[import]
|
||||
from tvm import relay # type: ignore[import]
|
||||
from tvm.contrib import graph_executor # type: ignore[import]
|
||||
|
||||
jit_mod = torch.jit.trace(gm, example_inputs)
|
||||
device = device_from_inputs(example_inputs)
|
||||
shape_list = [(f"inp_{idx}", i.shape) for idx, i in enumerate(example_inputs)]
|
||||
example_outputs = gm(*example_inputs)
|
||||
if len(example_outputs) == 0:
|
||||
log.warning("Explicitly fall back to eager due to zero output")
|
||||
return gm.forward
|
||||
mod, params = relay.frontend.from_pytorch(jit_mod, shape_list)
|
||||
if device.type == "cuda":
|
||||
dev = tvm.cuda(device.index)
|
||||
target = tvm.target.cuda()
|
||||
else:
|
||||
dev = tvm.cpu(0)
|
||||
target = tvm.target.Target(llvm_target())
|
||||
|
||||
scheduler = options.get("scheduler", None)
|
||||
if scheduler is None:
|
||||
scheduler = os.environ.get("TVM_SCHEDULER", None)
|
||||
|
||||
trials = options.get("trials", 20000)
|
||||
opt_level = options.get("opt_level", 3)
|
||||
|
||||
if scheduler == "auto_scheduler":
|
||||
# pyrefly: ignore [missing-import]
|
||||
from tvm import auto_scheduler
|
||||
|
||||
with (
|
||||
tempfile.NamedTemporaryFile() as log_file,
|
||||
auto_scheduler.ApplyHistoryBest(log_file),
|
||||
tvm.transform.PassContext(
|
||||
opt_level=opt_level, config={"relay.backend.use_auto_scheduler": True}
|
||||
),
|
||||
):
|
||||
lib = relay.build(mod, target=target, params=params)
|
||||
elif scheduler == "meta_schedule":
|
||||
# pyrefly: ignore [missing-import]
|
||||
from tvm import meta_schedule as ms
|
||||
|
||||
with tempfile.TemporaryDirectory() as work_dir:
|
||||
if device.type != "cuda":
|
||||
# meta_schedule needs num-cores to be specified
|
||||
# here we use the maximum core count
|
||||
target = tvm.target.Target(
|
||||
f"{llvm_target()} --num-cores {ms.utils.cpu_count(logical=False)}"
|
||||
)
|
||||
# TODO(shingjan): This could be replaced by tvm.contrib.torch.optimize_torch
|
||||
# once USE_PT_TVMDSOOP is updated and turned on by default in TVM.
|
||||
assert trials > 0
|
||||
database = ms.relay_integration.tune_relay(
|
||||
mod=mod,
|
||||
target=target,
|
||||
work_dir=work_dir,
|
||||
max_trials_global=trials,
|
||||
num_trials_per_iter=64,
|
||||
params=params,
|
||||
strategy="evolutionary",
|
||||
opt_level=opt_level,
|
||||
)
|
||||
lib = ms.relay_integration.compile_relay(
|
||||
database=database,
|
||||
mod=mod,
|
||||
target=target,
|
||||
params=params,
|
||||
opt_level=opt_level,
|
||||
)
|
||||
elif scheduler == "default" or not scheduler:
|
||||
# no autotuning
|
||||
with tvm.transform.PassContext(opt_level=opt_level):
|
||||
lib = relay.build(mod, target=target, params=params)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"This tuning option is invalid/not implemented for torchdynamo's TVM-related backend. "
|
||||
"There are three available options: default, auto_scheduler and meta_schedule."
|
||||
)
|
||||
m = graph_executor.GraphModule(lib["default"](dev))
|
||||
|
||||
def to_torch_tensor(nd_tensor: tvm.nd.array) -> torch.Tensor:
|
||||
"""A helper function to transfer a NDArray to torch.tensor."""
|
||||
if nd_tensor.dtype == "bool":
|
||||
# DLPack does not support boolean so it can't be handled by
|
||||
# torch.utils.dlpack.from_pack. Workaround by going through
|
||||
# numpy, although this brings additional data copy overhead.
|
||||
return torch.from_numpy(nd_tensor.numpy())
|
||||
return torch.utils.dlpack.from_dlpack(nd_tensor.to_dlpack())
|
||||
|
||||
def to_tvm_tensor(torch_tensor: torch.Tensor) -> tvm.nd.array:
|
||||
"""A helper function to transfer a torch.tensor to NDArray."""
|
||||
if torch_tensor.dtype == torch.bool:
|
||||
# same reason as above, fallback to numpy conversion which
|
||||
# could introduce data copy overhead
|
||||
return tvm.nd.array(torch_tensor.cpu().numpy())
|
||||
return tvm.nd.from_dlpack(torch_tensor)
|
||||
|
||||
def exec_tvm(*i_args: torch.Tensor) -> list[torch.Tensor]:
|
||||
args = [a.contiguous() for a in i_args]
|
||||
shape_info, _ = m.get_input_info()
|
||||
active_inputs = {name for name, _ in shape_info.items()}
|
||||
for idx, arg in enumerate(args, 0):
|
||||
if arg.dim() != 0:
|
||||
if arg.requires_grad:
|
||||
arg = arg.detach()
|
||||
inp_name = f"inp_{idx}"
|
||||
if inp_name not in active_inputs:
|
||||
log.warning(
|
||||
"input %s skipped as not found in tvm's runtime library",
|
||||
inp_name,
|
||||
)
|
||||
continue
|
||||
m.set_input(
|
||||
inp_name,
|
||||
to_tvm_tensor(arg),
|
||||
)
|
||||
m.run()
|
||||
return [to_torch_tensor(m.get_output(i)) for i in range(m.get_num_outputs())]
|
||||
|
||||
return exec_tvm
|
||||
|
||||
|
||||
tvm_meta_schedule = functools.partial(tvm, scheduler="meta_schedule")
|
||||
tvm_auto_scheduler = functools.partial(tvm, scheduler="auto_scheduler")
|
||||
|
||||
|
||||
def has_tvm() -> bool:
|
||||
try:
|
||||
importlib.import_module("tvm")
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
@functools.cache
|
||||
def llvm_target() -> str:
|
||||
if sys.platform == "linux":
|
||||
cpuinfo = Path("/proc/cpuinfo").read_text()
|
||||
if "avx512" in cpuinfo:
|
||||
return "llvm -mcpu=skylake-avx512"
|
||||
elif "avx2" in cpuinfo:
|
||||
return "llvm -mcpu=core-avx2"
|
||||
return "llvm"
|
||||
@@ -0,0 +1,266 @@
|
||||
"""
|
||||
This module provides utilities for analyzing and optimizing Python bytecode.
|
||||
Key functionality includes:
|
||||
- Dead code elimination
|
||||
- Jump instruction optimization
|
||||
- Stack size analysis and verification
|
||||
- Live variable analysis
|
||||
- Line number propagation and cleanup
|
||||
- Exception table handling for Python 3.11+
|
||||
|
||||
The utilities in this module are used to analyze and transform bytecode
|
||||
for better performance while maintaining correct semantics.
|
||||
"""
|
||||
|
||||
import bisect
|
||||
import dataclasses
|
||||
import dis
|
||||
import itertools
|
||||
import sys
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# TODO(lucaskabela): consider moving Instruction into this file
|
||||
# and refactoring in callsite; that way we don't have to guard this import
|
||||
from .bytecode_transformation import Instruction
|
||||
|
||||
TERMINAL_OPCODES = {
|
||||
dis.opmap["RETURN_VALUE"],
|
||||
dis.opmap["JUMP_FORWARD"],
|
||||
dis.opmap["RAISE_VARARGS"],
|
||||
# TODO(jansel): double check exception handling
|
||||
}
|
||||
TERMINAL_OPCODES.add(dis.opmap["RERAISE"])
|
||||
if sys.version_info >= (3, 11):
|
||||
TERMINAL_OPCODES.add(dis.opmap["JUMP_BACKWARD"])
|
||||
TERMINAL_OPCODES.add(dis.opmap["JUMP_FORWARD"])
|
||||
else:
|
||||
TERMINAL_OPCODES.add(dis.opmap["JUMP_ABSOLUTE"])
|
||||
|
||||
if (3, 12) <= sys.version_info < (3, 14):
|
||||
TERMINAL_OPCODES.add(dis.opmap["RETURN_CONST"])
|
||||
if sys.version_info >= (3, 13):
|
||||
TERMINAL_OPCODES.add(dis.opmap["JUMP_BACKWARD_NO_INTERRUPT"])
|
||||
JUMP_OPCODES = set(dis.hasjrel + dis.hasjabs)
|
||||
JUMP_OPNAMES = {dis.opname[opcode] for opcode in JUMP_OPCODES}
|
||||
HASLOCAL = set(dis.haslocal)
|
||||
HASFREE = set(dis.hasfree)
|
||||
|
||||
stack_effect = dis.stack_effect
|
||||
|
||||
|
||||
def get_indexof(insts: list["Instruction"]) -> dict["Instruction", int]:
|
||||
"""
|
||||
Get a mapping from instruction memory address to index in instruction list.
|
||||
Additionally checks that each instruction only appears once in the list.
|
||||
"""
|
||||
# pyrefly: ignore [implicit-any]
|
||||
indexof = {}
|
||||
for i, inst in enumerate(insts):
|
||||
assert inst not in indexof
|
||||
indexof[inst] = i
|
||||
return indexof
|
||||
|
||||
|
||||
def remove_dead_code(instructions: list["Instruction"]) -> list["Instruction"]:
|
||||
"""Dead code elimination"""
|
||||
indexof = get_indexof(instructions)
|
||||
live_code = set()
|
||||
|
||||
def find_live_code(start: int) -> None:
|
||||
for i in range(start, len(instructions)):
|
||||
if i in live_code:
|
||||
return
|
||||
live_code.add(i)
|
||||
inst = instructions[i]
|
||||
if inst.exn_tab_entry:
|
||||
find_live_code(indexof[inst.exn_tab_entry.target])
|
||||
if inst.opcode in JUMP_OPCODES:
|
||||
assert inst.target is not None
|
||||
find_live_code(indexof[inst.target])
|
||||
if inst.opcode in TERMINAL_OPCODES:
|
||||
return
|
||||
|
||||
find_live_code(0)
|
||||
|
||||
# change exception table entries if start/end instructions are dead
|
||||
# assumes that exception table entries have been propagated,
|
||||
# e.g. with bytecode_transformation.propagate_inst_exn_table_entries,
|
||||
# and that instructions with an exn_tab_entry lies within its start/end.
|
||||
if sys.version_info >= (3, 11):
|
||||
live_idx = sorted(live_code)
|
||||
for i, inst in enumerate(instructions):
|
||||
if i in live_code and inst.exn_tab_entry:
|
||||
# find leftmost live instruction >= start
|
||||
start_idx = bisect.bisect_left(
|
||||
live_idx, indexof[inst.exn_tab_entry.start]
|
||||
)
|
||||
assert start_idx < len(live_idx)
|
||||
# find rightmost live instruction <= end
|
||||
end_idx = (
|
||||
bisect.bisect_right(live_idx, indexof[inst.exn_tab_entry.end]) - 1
|
||||
)
|
||||
assert end_idx >= 0
|
||||
assert live_idx[start_idx] <= i <= live_idx[end_idx]
|
||||
inst.exn_tab_entry.start = instructions[live_idx[start_idx]]
|
||||
inst.exn_tab_entry.end = instructions[live_idx[end_idx]]
|
||||
|
||||
return [inst for i, inst in enumerate(instructions) if i in live_code]
|
||||
|
||||
|
||||
def remove_pointless_jumps(instructions: list["Instruction"]) -> list["Instruction"]:
|
||||
"""Eliminate jumps to the next instruction"""
|
||||
pointless_jumps = {
|
||||
id(a)
|
||||
for a, b in itertools.pairwise(instructions)
|
||||
if a.opname == "JUMP_ABSOLUTE" and a.target is b
|
||||
}
|
||||
return [inst for inst in instructions if id(inst) not in pointless_jumps]
|
||||
|
||||
|
||||
def propagate_line_nums(instructions: list["Instruction"]) -> None:
|
||||
"""Ensure every instruction has line number set in case some are removed"""
|
||||
cur_line_no = None
|
||||
|
||||
def populate_line_num(inst: "Instruction") -> None:
|
||||
nonlocal cur_line_no
|
||||
if inst.starts_line:
|
||||
cur_line_no = inst.starts_line
|
||||
|
||||
inst.starts_line = cur_line_no
|
||||
|
||||
for inst in instructions:
|
||||
populate_line_num(inst)
|
||||
|
||||
|
||||
def remove_extra_line_nums(instructions: list["Instruction"]) -> None:
|
||||
"""Remove extra starts line properties before packing bytecode"""
|
||||
|
||||
cur_line_no = None
|
||||
|
||||
def remove_line_num(inst: "Instruction") -> None:
|
||||
nonlocal cur_line_no
|
||||
if inst.starts_line is None:
|
||||
return
|
||||
elif inst.starts_line == cur_line_no:
|
||||
inst.starts_line = None
|
||||
else:
|
||||
cur_line_no = inst.starts_line
|
||||
|
||||
for inst in instructions:
|
||||
remove_line_num(inst)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ReadsWrites:
|
||||
reads: set[Any]
|
||||
writes: set[Any]
|
||||
visited: set[Any]
|
||||
|
||||
|
||||
def livevars_analysis(
|
||||
instructions: list["Instruction"], instruction: "Instruction"
|
||||
) -> set[Any]:
|
||||
indexof = get_indexof(instructions)
|
||||
must = ReadsWrites(set(), set(), set())
|
||||
may = ReadsWrites(set(), set(), set())
|
||||
|
||||
def walk(state: ReadsWrites, start: int) -> None:
|
||||
if start in state.visited:
|
||||
return
|
||||
state.visited.add(start)
|
||||
|
||||
for i in range(start, len(instructions)):
|
||||
inst = instructions[i]
|
||||
if inst.opcode in HASLOCAL or inst.opcode in HASFREE:
|
||||
if "LOAD" in inst.opname or "DELETE" in inst.opname:
|
||||
if inst.argval not in must.writes:
|
||||
state.reads.add(inst.argval)
|
||||
elif "STORE" in inst.opname:
|
||||
state.writes.add(inst.argval)
|
||||
elif inst.opname == "MAKE_CELL":
|
||||
pass
|
||||
else:
|
||||
raise NotImplementedError(f"unhandled {inst.opname}")
|
||||
if inst.exn_tab_entry:
|
||||
walk(may, indexof[inst.exn_tab_entry.target])
|
||||
if inst.opcode in JUMP_OPCODES:
|
||||
assert inst.target is not None
|
||||
walk(may, indexof[inst.target])
|
||||
state = may
|
||||
if inst.opcode in TERMINAL_OPCODES:
|
||||
return
|
||||
|
||||
walk(must, indexof[instruction])
|
||||
return must.reads | may.reads
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class FixedPointBox:
|
||||
value: bool = True
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class StackSize:
|
||||
low: int | float
|
||||
high: int | float
|
||||
fixed_point: FixedPointBox
|
||||
|
||||
def zero(self) -> None:
|
||||
self.low = 0
|
||||
self.high = 0
|
||||
self.fixed_point.value = False
|
||||
|
||||
def offset_of(self, other: "StackSize", n: int) -> None:
|
||||
prior = (self.low, self.high)
|
||||
self.low = min(self.low, other.low + n)
|
||||
self.high = max(self.high, other.high + n)
|
||||
if (self.low, self.high) != prior:
|
||||
self.fixed_point.value = False
|
||||
|
||||
def exn_tab_jump(self, depth: int) -> None:
|
||||
prior = (self.low, self.high)
|
||||
self.low = min(self.low, depth)
|
||||
self.high = max(self.high, depth)
|
||||
if (self.low, self.high) != prior:
|
||||
self.fixed_point.value = False
|
||||
|
||||
|
||||
def stacksize_analysis(instructions: list["Instruction"]) -> int | float:
|
||||
assert instructions
|
||||
fixed_point = FixedPointBox()
|
||||
stack_sizes = {
|
||||
inst: StackSize(float("inf"), float("-inf"), fixed_point)
|
||||
for inst in instructions
|
||||
}
|
||||
stack_sizes[instructions[0]].zero()
|
||||
|
||||
for _ in range(100):
|
||||
if fixed_point.value:
|
||||
break
|
||||
fixed_point.value = True
|
||||
|
||||
for inst, next_inst in zip(instructions, instructions[1:] + [None]):
|
||||
stack_size = stack_sizes[inst]
|
||||
if inst.opcode not in TERMINAL_OPCODES:
|
||||
assert next_inst is not None, f"missing next inst: {inst}"
|
||||
eff = stack_effect(inst.opcode, inst.arg, jump=False)
|
||||
stack_sizes[next_inst].offset_of(stack_size, eff)
|
||||
if inst.opcode in JUMP_OPCODES:
|
||||
assert inst.target is not None, f"missing target: {inst}"
|
||||
stack_sizes[inst.target].offset_of(
|
||||
stack_size, stack_effect(inst.opcode, inst.arg, jump=True)
|
||||
)
|
||||
if inst.exn_tab_entry:
|
||||
# see https://github.com/python/cpython/blob/3.11/Objects/exception_handling_notes.txt
|
||||
# on why depth is computed this way.
|
||||
depth = inst.exn_tab_entry.depth + int(inst.exn_tab_entry.lasti) + 1
|
||||
stack_sizes[inst.exn_tab_entry.target].exn_tab_jump(depth)
|
||||
|
||||
low = min(x.low for x in stack_sizes.values())
|
||||
high = max(x.high for x in stack_sizes.values())
|
||||
|
||||
assert fixed_point.value, "failed to reach fixed point"
|
||||
assert low >= 0
|
||||
return high
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,187 @@
|
||||
import logging
|
||||
import weakref
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from torch._guards import CompileId
|
||||
|
||||
from . import config
|
||||
from .types import DynamoFrameType
|
||||
|
||||
|
||||
log: logging.Logger = logging.getLogger(__name__)
|
||||
"""
|
||||
[Note on cache size limit]
|
||||
|
||||
Background - TorchDynamo cache is a linked list. Each cache entry is a
|
||||
(guard_manager, out_code, next pointer). These are stored on the f_code's co_extra
|
||||
scratch space. When a frame is invoked, we walk this linked list and run
|
||||
guard_manager in each cache_entry to decide if the frame needs recompilation. If none
|
||||
of the guard_manager's returns True, we recompile and add a new entry. To ensure we
|
||||
don't end up recompiling infinitely, we put limits on the cache size.
|
||||
|
||||
There are two limits
|
||||
1) recompile_limit
|
||||
2) accumulated_recompile_limit
|
||||
|
||||
|
||||
Earlier we used to have only limit - maximum number of entries in 1 cache line
|
||||
(which is now represented by (2) above). So, why do we need two limits? Lets try
|
||||
to understand that.
|
||||
|
||||
In general, we want our cache limit value to be a small number (e.g. 8 or even
|
||||
lower). This ensures that for frames that cause too many recompilation fall to
|
||||
eager quickly. However, there is another problem that prevents us from lowering
|
||||
the value of recompile_limit. This is due to ID_MATCH'd guards. Today, we put
|
||||
ID_MATCH guards on nn module if there is a graph break. This means we will have
|
||||
many recompilations for the same code object because the ID_MATCH guard fails
|
||||
for different instances of the nn module. This is a common pattern in how models
|
||||
are authored. Therefore, this requires us to keep the recompile_limit high.
|
||||
|
||||
We resolve this by introducing these two limits. The first limit (1) limits the
|
||||
number of cache entries that have an ID_MATCH'd guard for an nn module instance.
|
||||
And, (2)nd limit becomes a safeguard mechanism to have a maximum compilations
|
||||
for a code object. One important question is - what is the limit for the code
|
||||
object that does not have any ID_MATCH guard? For such code objects, we choose
|
||||
(1) as the cache size limit.
|
||||
|
||||
Lets take an example to understand how these limits help. Suppose, we have 16
|
||||
instances of a nn module and we ID_MATCH on the self object. Further, suppose
|
||||
the inputs to these functions have varying batch size, leading to one
|
||||
recompilation. In total, there will be 32 recompilations, and therefore 32 cache
|
||||
entries on the forward code object. In the older case when we had only 1 limit,
|
||||
our cache size limit must be >= 32 to capture all these recompilations. Now,
|
||||
suppose there is a separate function in the same program which is very dynamic
|
||||
and unsuitable for compilation. Such a function will need to undergo 32
|
||||
compilations to burst the cache and fallback to eager. These 32 recompilations
|
||||
are too many and we want to fallback for these compilation-unfriendly functions
|
||||
sooner.
|
||||
|
||||
In the new scenario, we can have (1) recompile_limit = 2, (2)
|
||||
accumulated_recompile_limit = 32. This means that each ID_MATCH'd object can
|
||||
have maximum of two cache entries, and the maximum number of cache entries
|
||||
(irrespective of ID_MATCH obj) is 32. This covers the case of forward code
|
||||
object which has 32 recompilations. For the other function, the one unsuitable
|
||||
for recompilation, our limit is 2. So, we will burst the cache in just 2
|
||||
recompilations. In this manner, these 2 limits help us resolve the tension
|
||||
mentioned earlier.
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class CacheSizeRelevantForFrame:
|
||||
"""
|
||||
We track the number of cache entries that have same id_match objects as the
|
||||
given frame.
|
||||
|
||||
TODO(janimesh) - Consider adding a map from tuple_of_match_ids to count -
|
||||
https://github.com/pytorch/pytorch/pull/107496#discussion_r1304564682 - this
|
||||
could be useful for debugging as well.
|
||||
"""
|
||||
|
||||
# Total number of CacheEntry objects in the Dynamo linked list
|
||||
num_cache_entries: int = 0
|
||||
|
||||
# Number of CacheEntry objects having same ID_MATCH'd objects as given frame.
|
||||
num_cache_entries_with_same_id_matched_objs: int = 0
|
||||
|
||||
def will_compilation_exceed(self, limit: int) -> bool:
|
||||
# Checks if a compilation will exceed the given limit (that's why >=).
|
||||
return (
|
||||
self.will_compilation_exceed_accumulated_limit()
|
||||
or self.will_compilation_exceed_specific_limit(limit)
|
||||
)
|
||||
|
||||
def will_compilation_exceed_accumulated_limit(self) -> bool:
|
||||
return self.num_cache_entries >= config.accumulated_recompile_limit
|
||||
|
||||
def will_compilation_exceed_specific_limit(self, limit: int) -> bool:
|
||||
return self.num_cache_entries_with_same_id_matched_objs >= limit
|
||||
|
||||
|
||||
def _get_weakref_from_f_locals(
|
||||
frame: DynamoFrameType, local_name: str
|
||||
) -> weakref.ref[Any] | None:
|
||||
obj = frame.f_locals.get(local_name, None)
|
||||
weak_id = None
|
||||
try:
|
||||
weak_id = weakref.ref(obj)
|
||||
except TypeError:
|
||||
pass # cannot weakref bool object
|
||||
return weak_id
|
||||
|
||||
|
||||
def _has_same_id_matched_objs(frame: DynamoFrameType, cache_entry: Any) -> bool:
|
||||
"""
|
||||
Checks if the ID_MATCH'd objects saved on cache_entry are same as the ones
|
||||
in frame.f_locals.
|
||||
"""
|
||||
if not cache_entry:
|
||||
return False
|
||||
|
||||
for (
|
||||
local_name,
|
||||
weakref_from_cache_entry,
|
||||
) in cache_entry.guard_manager.id_matched_objs.items():
|
||||
if weakref_from_cache_entry() is not None:
|
||||
weakref_from_frame = _get_weakref_from_f_locals(frame, local_name)
|
||||
if weakref_from_frame is not weakref_from_cache_entry:
|
||||
return False
|
||||
|
||||
# Also covers the case where no ID_MATCH objects are saved in frame.f_locals
|
||||
return True
|
||||
|
||||
|
||||
def compute_cache_size(
|
||||
frame: DynamoFrameType, cache_entry: Any
|
||||
) -> CacheSizeRelevantForFrame:
|
||||
# Walk the linked list to calculate the cache size
|
||||
num_cache_entries = 0
|
||||
num_cache_entries_with_same_id_matched_objs = 0
|
||||
|
||||
while cache_entry:
|
||||
num_cache_entries += 1
|
||||
# Track the number of cache entries having same ID_MATCH'd objects as
|
||||
# that of frame.f_locals. This will be used later to compare against the
|
||||
# recompile_limit.
|
||||
if _has_same_id_matched_objs(frame, cache_entry):
|
||||
num_cache_entries_with_same_id_matched_objs += 1
|
||||
cache_entry = cache_entry.next
|
||||
|
||||
return CacheSizeRelevantForFrame(
|
||||
num_cache_entries, num_cache_entries_with_same_id_matched_objs
|
||||
)
|
||||
|
||||
|
||||
def is_recompilation(cache_size: CacheSizeRelevantForFrame) -> bool:
|
||||
"""
|
||||
If the frame (earlier parsed by compute_cache_size) has more than 1 cache
|
||||
entry with same ID_MATCH'd objects, then its a recompilation.
|
||||
"""
|
||||
# Note that you can have multiple entries in the cache but still not a
|
||||
# recompile, e.g., you can have 64 nn module instances, each one having an
|
||||
# ID_MATCH guard, and each one having just 1 cache entry in the cache. In
|
||||
# this case, we can have 64 entries in the cache, but no recompilation
|
||||
# because there is only one entry for each id_matched_obj.
|
||||
return cache_size.will_compilation_exceed(1)
|
||||
|
||||
|
||||
def exceeds_recompile_limit(
|
||||
cache_size: CacheSizeRelevantForFrame, compile_id: CompileId
|
||||
) -> tuple[bool, str]:
|
||||
"""
|
||||
Checks if we are exceeding the cache size limit.
|
||||
"""
|
||||
if cache_size.will_compilation_exceed_accumulated_limit():
|
||||
return True, "accumulated_recompile_limit"
|
||||
if cache_size.will_compilation_exceed_specific_limit(config.recompile_limit):
|
||||
return True, "recompile_limit"
|
||||
# NOTE this check is needed in the case that the frame's cache doesn't grow
|
||||
# and we keep recompiling. This can happen if the guard guard_manager becomes invalidated,
|
||||
# e.g. due to guarded objects being freed. This technically makes the
|
||||
# will_compilation_exceed_accumulated_limit check unnecessary, but we will keep the
|
||||
# check in case we have a better fix in the future.
|
||||
assert compile_id.frame_compile_id is not None
|
||||
if compile_id.frame_compile_id >= config.accumulated_recompile_limit:
|
||||
return True, "accumulated_recompile_limit"
|
||||
return False, ""
|
||||
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
This module provides callback management functionality for TorchDynamo's compilation process.
|
||||
|
||||
It implements a thread-safe system for registering, managing and executing callbacks that run
|
||||
at the start and end of TorchDynamo compilations. Key features include:
|
||||
|
||||
- Registration and deregistration of compilation callbacks
|
||||
- Thread-safe callback handling with proper locking mechanisms
|
||||
- Prevention of duplicate callback execution when configured
|
||||
- Decorator utilities for easy callback registration
|
||||
- Context manager for controlled callback lifecycle
|
||||
|
||||
The module centers around the CompilationCallbackHandler class which maintains separate
|
||||
lists for start and end callbacks, manages their execution order, and ensures thread-safety.
|
||||
Utility decorators @on_compile_start and @on_compile_end provide a convenient way to
|
||||
register compilation hooks.
|
||||
|
||||
Example usage:
|
||||
@on_compile_start
|
||||
def my_start_callback():
|
||||
print("Starting compilation")
|
||||
|
||||
@on_compile_end
|
||||
def my_end_callback():
|
||||
print("Compilation complete")
|
||||
"""
|
||||
|
||||
import enum
|
||||
import threading
|
||||
from collections.abc import Callable, Generator
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass, field # noqa: F811
|
||||
from typing import Any
|
||||
|
||||
|
||||
class CallbackTrigger(enum.Enum):
|
||||
# most common case, dynamo attempts to trace a new frame
|
||||
DYNAMO = 1
|
||||
# backward compilation can be deferred to runtime
|
||||
LAZY_BACKWARD = 2
|
||||
# some backends autotune at runtime
|
||||
TRITON_AUTOTUNING = 3 # Temporarily disabled due to spam
|
||||
# cudagraphs record at runtime
|
||||
CUDAGRAPH_RECORDING = 4
|
||||
|
||||
|
||||
@dataclass
|
||||
class CallbackArgs:
|
||||
callback_trigger: CallbackTrigger
|
||||
compile_id: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class CompilationCallbackHandler:
|
||||
start_callbacks: list[Callable[[CallbackArgs], None]] = field(default_factory=list)
|
||||
end_callbacks: list[Callable[[CallbackArgs], None]] = field(default_factory=list)
|
||||
|
||||
__pending_callbacks_counter: int = field(default=0, init=False, repr=False)
|
||||
__pending_callbacks_counter_lock: threading.Lock = field(
|
||||
default_factory=threading.Lock, init=False, repr=False
|
||||
)
|
||||
|
||||
def register_start_callback(
|
||||
self, callback: Callable[[CallbackArgs], None]
|
||||
) -> Callable[[CallbackArgs], None]:
|
||||
"""
|
||||
Register a callback function to be called when the compilation starts.
|
||||
|
||||
Args:
|
||||
- callback (Callable): The callback function to register.
|
||||
"""
|
||||
self.start_callbacks.append(callback)
|
||||
return callback
|
||||
|
||||
def register_end_callback(
|
||||
self, callback: Callable[[CallbackArgs], None]
|
||||
) -> Callable[[CallbackArgs], None]:
|
||||
"""
|
||||
Register a callback function to be called when the compilation ends.
|
||||
|
||||
Args:
|
||||
- callback (Callable): The callback function to register.
|
||||
"""
|
||||
self.end_callbacks.append(callback)
|
||||
return callback
|
||||
|
||||
def remove_start_callback(self, callback: Callable[[CallbackArgs], None]) -> None:
|
||||
"""
|
||||
Remove a registered start callback function.
|
||||
|
||||
Args:
|
||||
- callback (Callable): The callback function to remove.
|
||||
"""
|
||||
self.start_callbacks.remove(callback)
|
||||
|
||||
def remove_end_callback(self, callback: Callable[[CallbackArgs], None]) -> None:
|
||||
"""
|
||||
Remove a registered end callback function.
|
||||
|
||||
Args:
|
||||
- callback (Callable): The callback function to remove.
|
||||
"""
|
||||
self.end_callbacks.remove(callback)
|
||||
|
||||
def run_start_callbacks(self, args: CallbackArgs) -> None:
|
||||
"""
|
||||
Execute all registered start callbacks.
|
||||
"""
|
||||
for callback in self.start_callbacks:
|
||||
callback(args)
|
||||
|
||||
def run_end_callbacks(self, args: CallbackArgs) -> None:
|
||||
"""
|
||||
Execute all registered end callbacks.
|
||||
"""
|
||||
for callback in self.end_callbacks:
|
||||
callback(args)
|
||||
|
||||
@contextmanager
|
||||
def install_callbacks(
|
||||
self, trigger: CallbackTrigger, compile_id: str
|
||||
) -> Generator[None, Any, Any]:
|
||||
"""
|
||||
Context manager to install the callbacks and run them when the context is exited.
|
||||
"""
|
||||
args = CallbackArgs(trigger, compile_id)
|
||||
try:
|
||||
with self.__pending_callbacks_counter_lock:
|
||||
self.__pending_callbacks_counter += 1
|
||||
if self.__pending_callbacks_counter == 1:
|
||||
self.run_start_callbacks(args)
|
||||
yield
|
||||
finally:
|
||||
with self.__pending_callbacks_counter_lock:
|
||||
assert self.__pending_callbacks_counter > 0, (
|
||||
"Pending callbacks counter cannot become negative."
|
||||
)
|
||||
if self.__pending_callbacks_counter == 1:
|
||||
self.run_end_callbacks(args)
|
||||
self.__pending_callbacks_counter -= 1
|
||||
|
||||
def clear(self) -> None:
|
||||
"""
|
||||
Clear all registered callbacks.
|
||||
"""
|
||||
self.start_callbacks.clear()
|
||||
self.end_callbacks.clear()
|
||||
assert self.__pending_callbacks_counter == 0
|
||||
|
||||
|
||||
callback_handler = CompilationCallbackHandler()
|
||||
|
||||
|
||||
def on_compile_start(
|
||||
callback: Callable[[CallbackArgs], None],
|
||||
) -> Callable[[CallbackArgs], None]:
|
||||
"""
|
||||
Decorator to register a callback function for the start of the compilation.
|
||||
"""
|
||||
callback_handler.register_start_callback(callback)
|
||||
return callback
|
||||
|
||||
|
||||
def on_compile_end(
|
||||
callback: Callable[[CallbackArgs], None],
|
||||
) -> Callable[[CallbackArgs], None]:
|
||||
"""
|
||||
Decorator to register a callback function for the end of the compilation.
|
||||
"""
|
||||
callback_handler.register_end_callback(callback)
|
||||
return callback
|
||||
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
This module provides thread-safe code context management for TorchDynamo using weak references.
|
||||
|
||||
The CodeContextDict class maintains a mapping between Python code objects and their associated
|
||||
context data, using weak references to automatically clean up entries when code objects are
|
||||
garbage collected. This prevents memory leaks while allowing context data to be associated
|
||||
with code objects throughout their lifecycle.
|
||||
|
||||
Key features:
|
||||
- Thread-safe context storage and retrieval
|
||||
- Automatic cleanup using weak references
|
||||
- Safe context management for Python code objects
|
||||
- Memory-leak prevention
|
||||
|
||||
Example usage:
|
||||
code_obj = compile('x = 1', '<string>', 'exec')
|
||||
|
||||
# Store context
|
||||
context = code_context.get_context(code_obj)
|
||||
context['metadata'] = {'optimized': True}
|
||||
|
||||
# Retrieve context
|
||||
if code_context.has_context(code_obj):
|
||||
ctx = code_context.get_context(code_obj)
|
||||
# Use context data...
|
||||
|
||||
# Remove context
|
||||
ctx = code_context.pop_context(code_obj)
|
||||
"""
|
||||
|
||||
import types
|
||||
from typing import Any
|
||||
|
||||
from .utils import ExactWeakKeyDictionary
|
||||
|
||||
|
||||
class CodeContextDict:
|
||||
def __init__(self) -> None:
|
||||
self.code_context: ExactWeakKeyDictionary = ExactWeakKeyDictionary()
|
||||
|
||||
def has_context(self, code: types.CodeType) -> bool:
|
||||
return code in self.code_context
|
||||
|
||||
def get_context(self, code: types.CodeType) -> dict[str, Any]:
|
||||
ctx = self.code_context.get(code)
|
||||
if ctx is None:
|
||||
# pyrefly: ignore [implicit-any]
|
||||
ctx = {}
|
||||
self.code_context[code] = ctx
|
||||
return ctx
|
||||
|
||||
def pop_context(self, code: types.CodeType) -> dict[str, Any]:
|
||||
ctx = self.get_context(code)
|
||||
self.code_context._remove_id(id(code))
|
||||
return ctx
|
||||
|
||||
def clear(self) -> None:
|
||||
self.code_context.clear()
|
||||
|
||||
|
||||
code_context: CodeContextDict = CodeContextDict()
|
||||
@@ -0,0 +1,734 @@
|
||||
"""
|
||||
This module provides utilities for generating Python bytecode in PyTorch's Dynamo system.
|
||||
It includes functionality for:
|
||||
- Constructing bytecode sequences for Python operations
|
||||
- Managing stack operations and variable tracking
|
||||
- Handling graph outputs and their conversions
|
||||
- Supporting different Python versions (3.11+, 3.12+, 3.13+)
|
||||
- Converting high-level operations to low-level bytecode instructions
|
||||
- Managing constant loading and attribute access
|
||||
- Supporting function creation and closure handling
|
||||
"""
|
||||
|
||||
import collections
|
||||
import dataclasses
|
||||
import re
|
||||
import sys
|
||||
import types
|
||||
from collections import Counter, deque
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Any, TYPE_CHECKING, Union
|
||||
|
||||
import torch.nn
|
||||
from torch.utils._ordered_set import OrderedSet
|
||||
|
||||
from . import config, graph_break_hints, utils
|
||||
from .bytecode_transformation import (
|
||||
add_push_null,
|
||||
add_push_null_call_function_ex,
|
||||
create_binary_subscr,
|
||||
create_build_tuple,
|
||||
create_call_function,
|
||||
create_call_function_ex,
|
||||
create_call_method,
|
||||
create_dup_top,
|
||||
create_instruction,
|
||||
create_load_const,
|
||||
create_load_method,
|
||||
create_rot_n,
|
||||
Instruction,
|
||||
)
|
||||
from .exc import unimplemented
|
||||
from .source import AttrSource, ChainedSource, DictGetItemSource, Source
|
||||
from .utils import is_safe_constant, rot_n_helper
|
||||
from .variables.base import ValueMutationExisting, VariableTracker
|
||||
from .variables.functions import (
|
||||
ContextlibContextManagerLocalGeneratorObjectVariable,
|
||||
LocalGeneratorObjectVariable,
|
||||
)
|
||||
from .variables.nn_module import NNModuleVariable
|
||||
from .variables.script_object import TorchScriptObjectVariable
|
||||
from .variables.tensor import (
|
||||
NumpyNdarrayVariable,
|
||||
SymNodeVariable,
|
||||
TensorVariable,
|
||||
UnspecializedPythonVariable,
|
||||
)
|
||||
from .variables.torch_function import TensorWithTFOverrideVariable
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch._dynamo.variables.builder import GraphArg
|
||||
|
||||
from .symbolic_convert import InstructionTranslatorBase
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class GraphOutputEntry:
|
||||
index: int
|
||||
variable: VariableTracker
|
||||
|
||||
|
||||
class PyCodegen:
|
||||
"""
|
||||
Helper class uses for constructing Python bytecode
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tx: "InstructionTranslatorBase",
|
||||
root: torch.nn.Module | None = None,
|
||||
graph_output_var: str | None = None,
|
||||
tempvars: dict[VariableTracker | Source, Any] | None = None,
|
||||
overridden_sources: dict[Source, Source] | None = None,
|
||||
) -> None:
|
||||
self.root = root
|
||||
self.top_of_stack: VariableTracker | Source | None = None
|
||||
self.uses: Counter[VariableTracker | Source] = collections.Counter()
|
||||
self.graph_outputs: dict[int, GraphOutputEntry] = {}
|
||||
self._output: list[Instruction] = []
|
||||
# This determines which VariableTracker/Source should be stored as
|
||||
# locals, and maps the VariableTracker/Source to the local variable
|
||||
# name. Note that it could map to None initially, in which case we'll
|
||||
# overwrite it to map to real temporary names via `add_cache`.
|
||||
self.tempvars: dict[VariableTracker | Source, Any] = tempvars or {}
|
||||
self.tx = tx
|
||||
self.graph_output_var = graph_output_var
|
||||
self.code_options = self.tx.output.code_options
|
||||
self.cell_and_freevars = self.tx.cell_and_freevars
|
||||
self.new_var = self.tx.output.new_var
|
||||
self.value_from_source: bool = True
|
||||
# This serves as a way for codegen to use a different source; we need
|
||||
# this because sometimes we can't easily modify the original source
|
||||
# without affecting other components, e.g., guards.
|
||||
self.overridden_sources: dict[Source, Source] = overridden_sources or {}
|
||||
|
||||
def restore_stack(
|
||||
self, stack_values: list[Any], *, value_from_source: bool = True
|
||||
) -> None:
|
||||
prev = self.value_from_source
|
||||
self.value_from_source &= value_from_source
|
||||
try:
|
||||
self.foreach(stack_values)
|
||||
finally:
|
||||
self.value_from_source = prev
|
||||
|
||||
def graph_output_vars(self) -> list[VariableTracker]:
|
||||
return [x.variable for x in self.graph_outputs.values()]
|
||||
|
||||
def call_reconstruct(
|
||||
self, value: Union[VariableTracker, Source, "GraphArg"]
|
||||
) -> None:
|
||||
res = value.reconstruct(self)
|
||||
assert res is None, f"reconstruct!=None {value}"
|
||||
|
||||
def add_push_null(
|
||||
self, gen_fn: Callable[[], None], call_function_ex: bool = False
|
||||
) -> None:
|
||||
"""
|
||||
`gen_fn` generates instructions via PyCodegen methods
|
||||
that push a single callable to the stack.
|
||||
|
||||
`add_push_null` pushes a NULL to the stack before or after the
|
||||
instructions generated by `gen_fn`, depending on Python version.
|
||||
|
||||
Will attempt to use the NULL push bit for instructions
|
||||
with such bits (LOAD_GLOBAL 3.11+, LOAD_ATTR 3.12+, LOAD_SUPER_ATTR).
|
||||
"""
|
||||
old_len = len(self._output)
|
||||
if sys.version_info < (3, 13):
|
||||
# gen_fn may DUP_TOP instead if TOS is not cleared.
|
||||
# Will cause problems since NULL will be pushed right
|
||||
# before the generated instructions in <= 3.12
|
||||
self.clear_tos()
|
||||
gen_fn()
|
||||
# inplace modify self._output
|
||||
added_insts = self._output[old_len:]
|
||||
del self._output[old_len:]
|
||||
if call_function_ex:
|
||||
self._output.extend(add_push_null_call_function_ex(added_insts))
|
||||
else:
|
||||
self._output.extend(add_push_null(added_insts))
|
||||
if sys.version_info >= (3, 13):
|
||||
# NULL will be at top of stack
|
||||
self.clear_tos()
|
||||
|
||||
def __call__(
|
||||
self, value: VariableTracker | Source | None, allow_cache: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
Generate code such that top-of-stack (TOS) is set to value.
|
||||
|
||||
`allow_cache` controls the behavior in the following manner. `value` can
|
||||
either be a VariableTracker or a Source.
|
||||
|
||||
If `value` is a `Source`, `allow_cache` must be True (invariant asserted
|
||||
below). If the source was reconstructed earlier, we will reuse the
|
||||
generated code by loading from top of stack or tempvars.
|
||||
|
||||
If `value` is a `VariableTracker`, we have the following cases:
|
||||
|
||||
1) `allow_cache=True`
|
||||
a) If the value.source is not None, we will emit the code based on
|
||||
`value.source` to handle aliasing.
|
||||
b) If value.source is None (example reconstructing a local list
|
||||
returned by the compiled function), we will reconstruct the variable
|
||||
tracker (w/o any source) to emit bytecode that generates a new
|
||||
python object.
|
||||
|
||||
In both cases of value.source being None or not, if the value was
|
||||
reconstructed earlier, we will reuse the generated code by loading from
|
||||
top of stack or tempvars.
|
||||
|
||||
2) `allow_cache=False` - This is a special case (allow_cache defaults to
|
||||
True).
|
||||
a) If the value.source is not None, we reconstruct the variable
|
||||
tracker and emit a new python object. You might wonder what about
|
||||
aliasing? The place where we use this config also has the followup
|
||||
code where the original python object is assigned to this new python
|
||||
value to handle aliasing (check side_effects.py and search for
|
||||
allow_cache=False).
|
||||
|
||||
b) If value.source is None, this is not allowed
|
||||
|
||||
Notable effects:
|
||||
1. `self.top_of_stack` will be set to `value`, if we don't codegen
|
||||
`value` based on source.
|
||||
2. `self.uses[value]` will increment, unless (a). we codegen via
|
||||
`top_of_stack` or cached `tempvars`, or (b). `value` has special VT
|
||||
types like `NNModuleVariable`, etc.
|
||||
"""
|
||||
assert value is not None
|
||||
if isinstance(value, Source):
|
||||
# If the source needs to be overridden, use the new one.
|
||||
source = self.overridden_sources.get(value, value)
|
||||
assert allow_cache is True, "allow_cache must be True for Source"
|
||||
if self.top_of_stack is value:
|
||||
self._output.append(create_dup_top())
|
||||
return
|
||||
|
||||
if self.tempvars.get(source) is not None:
|
||||
self._output.append(self.create_load(self.tempvars[source]))
|
||||
self.top_of_stack = source
|
||||
return
|
||||
|
||||
self.uses[source] += 1
|
||||
try:
|
||||
self.call_reconstruct(source)
|
||||
except NotImplementedError:
|
||||
unimplemented(
|
||||
gb_type="Reconstruction failure: source.reconstruct not implemented",
|
||||
context=str(source),
|
||||
explanation=f"Dynamo has no bytecode reconstruction implemented for {type(source)} variable {source}.",
|
||||
hints=[*graph_break_hints.DYNAMO_BUG],
|
||||
)
|
||||
if source in self.tempvars:
|
||||
self._output.append(create_dup_top())
|
||||
self.add_cache(source)
|
||||
self.top_of_stack = source
|
||||
|
||||
return
|
||||
|
||||
assert isinstance(value, VariableTracker)
|
||||
output = self._output
|
||||
graph_outputs = self.graph_outputs
|
||||
|
||||
if allow_cache:
|
||||
if self.top_of_stack is value:
|
||||
output.append(create_dup_top())
|
||||
return
|
||||
|
||||
if self.tempvars.get(value) is not None:
|
||||
output.append(self.create_load(self.tempvars[value]))
|
||||
self.top_of_stack = value
|
||||
return
|
||||
|
||||
if value.is_realized() and isinstance(
|
||||
value, ContextlibContextManagerLocalGeneratorObjectVariable
|
||||
):
|
||||
unimplemented(
|
||||
gb_type="reconstructing @contextmanager object",
|
||||
context=f"object: {value}",
|
||||
explanation="Returning a @contextmanager object from a compiled function is not supported.",
|
||||
hints=[
|
||||
*graph_break_hints.SUPPORTABLE,
|
||||
],
|
||||
)
|
||||
|
||||
# Dynamo normally prefers codegen from source to account for aliasing.
|
||||
if (
|
||||
value.source is not None
|
||||
and allow_cache
|
||||
and not (
|
||||
value.is_realized() and isinstance(value, LocalGeneratorObjectVariable)
|
||||
)
|
||||
):
|
||||
# There's a corner case for export: for instance, if the computation
|
||||
# graph is just identity on an input tensor, Dynamo would just emit
|
||||
# a `LOAD_FAST` from the input source, rather than generating an
|
||||
# identity FX graph.
|
||||
#
|
||||
# However, export wants to maximize graph capture; in the case
|
||||
# above, export _wants to_ obtain an identity FX graph (despite it
|
||||
# appears unnecessarily expensive for `torch.compile`), so we have
|
||||
# the following option to override Dynamo's preference for codegen
|
||||
# from source. Moreover, this option applies recursively, for cases
|
||||
# like input tensor being returned in a new dictionary.
|
||||
#
|
||||
# And why the `ValueMutationExisting` check? Not sure, so leaving it
|
||||
# to keep the old behavior, as when `value_from_source` was
|
||||
# introduced. TODO sort out the invariants among side effect,
|
||||
# codegen and export.
|
||||
if (
|
||||
isinstance(value.mutation_type, ValueMutationExisting)
|
||||
or self.value_from_source
|
||||
):
|
||||
return self(value.source)
|
||||
|
||||
if value.is_python_constant() and is_safe_constant(value.as_python_constant()):
|
||||
output.append(self.create_load_const(value.as_python_constant()))
|
||||
elif isinstance(value, TensorWithTFOverrideVariable):
|
||||
graph_outputs_key = self.add_graph_output(value)
|
||||
|
||||
self.add_push_null(
|
||||
lambda: self.load_import_from(utils.__name__, "to_subclass")
|
||||
)
|
||||
self.load_graph_output(graph_outputs[graph_outputs_key].index)
|
||||
output.append(
|
||||
self.create_load_global(
|
||||
value.global_mangled_class_name(self.tx), # type: ignore[arg-type]
|
||||
add=True,
|
||||
)
|
||||
)
|
||||
output.extend(create_call_function(2, False))
|
||||
elif (
|
||||
isinstance(value, SymNodeVariable)
|
||||
and value.python_type() is float
|
||||
and not self.tx.export
|
||||
):
|
||||
# This is a little unusual; force the output convention to be a
|
||||
# Tensor here. Don't do this for export because this is
|
||||
# apparently load bearing for export tests (but I am a bit
|
||||
# doubtful it actually works in the real world)
|
||||
# NB: It works to add_graph_output on a computed expression
|
||||
# as_tensor here, because we memoize as_tensor calls on
|
||||
# SymNodeVariable!
|
||||
graph_outputs_key = self.add_graph_output(
|
||||
value.as_tensor(self.tx, torch.float64)
|
||||
)
|
||||
|
||||
def gen_fn() -> None:
|
||||
self.load_graph_output(graph_outputs[graph_outputs_key].index)
|
||||
output.append(self.create_load_attr("item"))
|
||||
|
||||
self.add_push_null(gen_fn)
|
||||
output.extend(create_call_function(0, False))
|
||||
elif isinstance(
|
||||
value,
|
||||
(
|
||||
TensorVariable,
|
||||
SymNodeVariable,
|
||||
UnspecializedPythonVariable,
|
||||
NumpyNdarrayVariable,
|
||||
TorchScriptObjectVariable,
|
||||
),
|
||||
):
|
||||
graph_outputs_key = self.add_graph_output(value)
|
||||
|
||||
if isinstance(value, NumpyNdarrayVariable):
|
||||
self.add_push_null(
|
||||
lambda: self.load_import_from(utils.__name__, "to_numpy_helper")
|
||||
)
|
||||
self.load_graph_output(graph_outputs[graph_outputs_key].index)
|
||||
output.extend(create_call_function(1, False))
|
||||
elif isinstance(value, UnspecializedPythonVariable) and value.need_unwrap:
|
||||
|
||||
def gen_fn() -> None:
|
||||
self.load_graph_output(graph_outputs[graph_outputs_key].index)
|
||||
output.append(self.create_load_attr("item"))
|
||||
|
||||
self.add_push_null(gen_fn)
|
||||
output.extend(create_call_function(0, False))
|
||||
else:
|
||||
self.load_graph_output(graph_outputs[graph_outputs_key].index)
|
||||
elif isinstance(value, NNModuleVariable):
|
||||
parts = value.module_key.split(".")
|
||||
if parts[0] in self.code_options["co_varnames"]:
|
||||
output.append(self.create_load(parts[0]))
|
||||
parts = parts[1:]
|
||||
else:
|
||||
assert self.root is not None
|
||||
output.append(self.create_load_const_unchecked(self.root))
|
||||
for part in parts:
|
||||
output.append(self.create_load_attr(part))
|
||||
else:
|
||||
self.uses[value] += 1
|
||||
try:
|
||||
self.call_reconstruct(value)
|
||||
except NotImplementedError as e:
|
||||
unimplemented(
|
||||
gb_type="Reconstruction failure",
|
||||
context=str(value),
|
||||
explanation=f"Dynamo has no bytecode reconstruction implemented for sourceless variable {value}.",
|
||||
hints=[
|
||||
"If Dynamo is attempting to trace a return statement and your code is attempting to return a variable "
|
||||
"that Dynamo cannot reconstruct, then remove it from the return statement.",
|
||||
*graph_break_hints.CAUSED_BY_EARLIER_GRAPH_BREAK,
|
||||
"Report an issue to PyTorch if you need reconstrtuction support. Note that objects that don't have "
|
||||
"reconstruction rules may be fundamentally unreconstructable.",
|
||||
],
|
||||
from_exc=e,
|
||||
)
|
||||
if allow_cache and value in self.tempvars:
|
||||
self._output.append(create_dup_top())
|
||||
self.add_cache(value)
|
||||
|
||||
self.top_of_stack = value
|
||||
|
||||
def add_graph_output(self, value: VariableTracker) -> int:
|
||||
graph_outputs_key = id(value.as_proxy())
|
||||
if graph_outputs_key not in self.graph_outputs:
|
||||
self.graph_outputs[graph_outputs_key] = GraphOutputEntry(
|
||||
len(self.graph_outputs), value
|
||||
)
|
||||
return graph_outputs_key
|
||||
|
||||
def load_graph_output(self, index: int) -> None:
|
||||
output = self._output
|
||||
assert self.graph_output_var is not None
|
||||
output.append(self.create_load(self.graph_output_var))
|
||||
output.append(self.create_load_const(index))
|
||||
output.append(self.create_binary_subscr())
|
||||
|
||||
def add_cache(self, value: VariableTracker | Source) -> None:
|
||||
var = self.new_var()
|
||||
self.tempvars[value] = var
|
||||
self._output.append(self.create_store(var))
|
||||
|
||||
def foreach(self, items: Iterable[VariableTracker | Source]) -> None:
|
||||
for i in items:
|
||||
self(i)
|
||||
|
||||
def create_binary_subscr(self) -> Instruction:
|
||||
return create_binary_subscr()
|
||||
|
||||
def setup_globally_cached(self, name: str, value: Any) -> list[Instruction]:
|
||||
"""Store value in a new global"""
|
||||
name = re.sub(r"[^a-zA-Z0-9_]+", "_", name)
|
||||
f_globals = self.tx.f_globals
|
||||
if name in f_globals:
|
||||
assert id(f_globals[name]) == id(value)
|
||||
else:
|
||||
f_globals[name] = value
|
||||
return [self.create_load_global(name, add=True)]
|
||||
|
||||
def clear_tos(self) -> None:
|
||||
self.top_of_stack = None
|
||||
|
||||
def append_output(self, inst: Instruction) -> None:
|
||||
assert isinstance(inst, Instruction)
|
||||
self._output.append(inst)
|
||||
self.clear_tos()
|
||||
|
||||
def extend_output(self, insts: list[Instruction]) -> None:
|
||||
assert all(isinstance(x, Instruction) for x in insts)
|
||||
self._output.extend(insts)
|
||||
self.clear_tos()
|
||||
|
||||
def get_instructions(self) -> list[Instruction]:
|
||||
return self._output
|
||||
|
||||
def create_load(self, name: str) -> Instruction:
|
||||
assert name in self.code_options["co_varnames"], f"{name} missing"
|
||||
return create_instruction("LOAD_FAST", argval=name)
|
||||
|
||||
def create_load_closure(self, name: str) -> Instruction:
|
||||
assert name in self.cell_and_freevars()
|
||||
inst_name = "LOAD_FAST" if sys.version_info >= (3, 13) else "LOAD_CLOSURE"
|
||||
return create_instruction(inst_name, argval=name)
|
||||
|
||||
def create_load_deref(self, name: str) -> Instruction:
|
||||
assert name in self.cell_and_freevars()
|
||||
return create_instruction("LOAD_DEREF", argval=name)
|
||||
|
||||
def create_store(self, name: str) -> Instruction:
|
||||
assert name in self.code_options["co_varnames"], f"{name} missing"
|
||||
return create_instruction("STORE_FAST", argval=name)
|
||||
|
||||
def create_store_deref(self, name: str) -> Instruction:
|
||||
assert name in self.cell_and_freevars()
|
||||
return create_instruction("STORE_DEREF", argval=name)
|
||||
|
||||
def create_load_global(self, name: str, add: bool = False) -> Instruction:
|
||||
if add:
|
||||
self.tx.output.update_co_names(name)
|
||||
assert name in self.code_options["co_names"], f"{name} not in co_names"
|
||||
return create_instruction("LOAD_GLOBAL", argval=name)
|
||||
|
||||
def create_load_const(self, value: Any) -> Instruction:
|
||||
return create_load_const(value)
|
||||
|
||||
def create_load_const_unchecked(self, value: Any) -> Instruction:
|
||||
return create_load_const(value, checked=False)
|
||||
|
||||
def load_method(self, name: str) -> None:
|
||||
self.tx.output.update_co_names(name)
|
||||
self.append_output(create_load_method(name))
|
||||
|
||||
def call_method(self, nargs: int) -> None:
|
||||
self.extend_output(create_call_method(nargs))
|
||||
|
||||
def create_load_attr(self, name: str) -> Instruction:
|
||||
if name not in self.code_options["co_names"]:
|
||||
self.code_options["co_names"] += (name,)
|
||||
return create_instruction("LOAD_ATTR", argval=name)
|
||||
|
||||
def load_attr(self, name: str) -> None:
|
||||
self.append_output(self.create_load_attr(name))
|
||||
|
||||
def create_load_attrs(self, names: str) -> list[Instruction]:
|
||||
return [self.create_load_attr(name) for name in names.split(".")]
|
||||
|
||||
def create_store_attr(self, name: str) -> Instruction:
|
||||
if name not in self.code_options["co_names"]:
|
||||
self.code_options["co_names"] += (name,)
|
||||
return create_instruction("STORE_ATTR", argval=name)
|
||||
|
||||
def store_attr(self, name: str) -> None:
|
||||
self.append_output(self.create_store_attr(name))
|
||||
|
||||
def load_function_name(
|
||||
self, fn_name: str, push_null: bool, num_on_stack: int = 0
|
||||
) -> list[Instruction]:
|
||||
"""Load the global fn_name on the stack num_on_stack down"""
|
||||
output = []
|
||||
if push_null and sys.version_info >= (3, 11):
|
||||
output.extend(add_push_null(self.create_load_global(fn_name, add=True)))
|
||||
if num_on_stack > 0:
|
||||
output.extend(
|
||||
[
|
||||
*self.rot_n(num_on_stack + 2),
|
||||
*self.rot_n(num_on_stack + 2),
|
||||
]
|
||||
)
|
||||
else:
|
||||
output.extend(
|
||||
[
|
||||
self.create_load_global(fn_name, add=True),
|
||||
*self.rot_n(num_on_stack + 1),
|
||||
]
|
||||
)
|
||||
return output
|
||||
|
||||
def rot_n(self, n: int) -> list[Instruction]:
|
||||
try:
|
||||
return create_rot_n(n)
|
||||
except AttributeError:
|
||||
# desired rotate bytecode doesn't exist, generate equivalent bytecode
|
||||
return [
|
||||
create_build_tuple(n),
|
||||
self.create_load_const_unchecked(rot_n_helper(n)),
|
||||
*create_rot_n(2),
|
||||
*create_call_function_ex(False, False),
|
||||
create_instruction("UNPACK_SEQUENCE", arg=n),
|
||||
]
|
||||
|
||||
def pop_null(self) -> list[Instruction]:
|
||||
# POP_TOP doesn't work for null, so we pop nulls by pushing in a
|
||||
# nop function, calling it (which consumes the null), and popping the result.
|
||||
assert sys.version_info >= (3, 11)
|
||||
return [
|
||||
self.create_load_const_unchecked(lambda: None),
|
||||
# 3.13 swapped NULL and callable
|
||||
*(
|
||||
(create_instruction("SWAP", arg=2),)
|
||||
if sys.version_info >= (3, 13)
|
||||
else ()
|
||||
),
|
||||
*create_call_function(0, False),
|
||||
create_instruction("POP_TOP"),
|
||||
]
|
||||
|
||||
def pop_top(self) -> None:
|
||||
self.append_output(create_instruction("POP_TOP"))
|
||||
|
||||
def call_function(self, nargs: int, push_null: bool) -> None:
|
||||
self.extend_output(create_call_function(nargs, push_null=push_null))
|
||||
|
||||
def dup_top(self) -> None:
|
||||
self.append_output(create_dup_top())
|
||||
|
||||
def store(self, varname: str) -> None:
|
||||
self.append_output(self.create_store(varname))
|
||||
|
||||
def load_deref(self, varname: str) -> None:
|
||||
self.append_output(self.create_load_deref(varname))
|
||||
|
||||
def make_function_with_closure(
|
||||
self,
|
||||
fn_name: str,
|
||||
code: types.CodeType,
|
||||
) -> None:
|
||||
"""Creates a closure with code object `code`.
|
||||
|
||||
Expects the TOS to be the tuple of cells to use for this closure.
|
||||
TOS will be popped to create the closure.
|
||||
Args:
|
||||
- fn_name: name of the function
|
||||
- code: code object of the function
|
||||
(does not include the tuple of cells on the TOS)
|
||||
"""
|
||||
output = self._output
|
||||
|
||||
output.append(self.create_load_const(code))
|
||||
if sys.version_info < (3, 11):
|
||||
output.append(self.create_load_const(fn_name))
|
||||
if sys.version_info >= (3, 13):
|
||||
output.extend(
|
||||
[
|
||||
create_instruction("MAKE_FUNCTION"),
|
||||
create_instruction("SET_FUNCTION_ATTRIBUTE", arg=0x08),
|
||||
]
|
||||
)
|
||||
else:
|
||||
output.append(create_instruction("MAKE_FUNCTION", arg=0x08))
|
||||
|
||||
self.clear_tos()
|
||||
|
||||
def create_load_python_module(self, mod: types.ModuleType) -> Instruction:
|
||||
"""
|
||||
Generate a LOAD_GLOBAL instruction to fetch a given python module.
|
||||
"""
|
||||
output = self.tx.output
|
||||
global_scope = output.global_scope
|
||||
name = re.sub(r"^.*[.]", "", mod.__name__)
|
||||
if global_scope.get(name, None) is mod:
|
||||
return self.create_load_global(name, add=True)
|
||||
prefix = f"___module_{name}"
|
||||
global_name = self.tx.output.install_global_by_id(prefix, mod)
|
||||
return self.create_load_global(global_name, add=True)
|
||||
|
||||
def mark_source_temp(self, source: Source) -> None:
|
||||
"""
|
||||
Mark a source as a temp variable, so that it can be reused.
|
||||
"""
|
||||
if source not in self.tempvars:
|
||||
self.tempvars[source] = None
|
||||
|
||||
def make_call_generated_code(self, fn_name: str) -> None:
|
||||
"""Call the generated code function stored in fn_name"""
|
||||
self.extend_output(self.load_function_name(fn_name, True))
|
||||
|
||||
graphargs = self.tx.output.graphargs
|
||||
|
||||
def extract_nested_sources(source: Source) -> list[Source]:
|
||||
nested_sources: list[Source] = []
|
||||
if isinstance(source, ChainedSource):
|
||||
nested_sources.append(source.base)
|
||||
if isinstance(source, DictGetItemSource) and isinstance(
|
||||
source.index, Source
|
||||
):
|
||||
nested_sources.append(source.index)
|
||||
return nested_sources
|
||||
|
||||
def collect_temp_sources(sources: deque[Source], codegen: PyCodegen) -> None:
|
||||
seen_sources: OrderedSet[Source] = OrderedSet()
|
||||
while sources:
|
||||
current_source = sources.popleft()
|
||||
if current_source in seen_sources:
|
||||
# This source is used at least twice, so it can be reused
|
||||
codegen.mark_source_temp(current_source)
|
||||
# Dont trace source further. This prevents us from marking too
|
||||
# many nodes as temp sources.
|
||||
continue
|
||||
seen_sources.add(current_source)
|
||||
sources.extend(extract_nested_sources(current_source))
|
||||
|
||||
# Collect all the sources that are used more than once, so that we can
|
||||
# generate tmp variables in the generated pre-graph bytecode. This
|
||||
# essentially implements CSE.
|
||||
collect_temp_sources(
|
||||
deque([arg.source for arg in graphargs if arg.source is not None]), self
|
||||
)
|
||||
|
||||
cm_var = None
|
||||
if config.record_runtime_overhead:
|
||||
# Record the pregraph bytecode start
|
||||
self.add_push_null(
|
||||
lambda: self.load_import_from(
|
||||
utils.__name__, "record_pregraph_bytecode_enter"
|
||||
)
|
||||
)
|
||||
self.extend_output(create_call_function(0, False))
|
||||
cm_var = self.new_var()
|
||||
self.store(cm_var)
|
||||
|
||||
for arg in graphargs:
|
||||
if arg.pass_arg_as_tensor:
|
||||
self.add_push_null(
|
||||
lambda: self.extend_output(
|
||||
[
|
||||
self.create_load_python_module(torch),
|
||||
self.create_load_attr("_as_tensor_fullprec"),
|
||||
]
|
||||
)
|
||||
)
|
||||
self.call_reconstruct(arg)
|
||||
self.extend_output(create_call_function(1, False))
|
||||
else:
|
||||
self.call_reconstruct(arg)
|
||||
|
||||
if config.record_runtime_overhead:
|
||||
# Record the pregraph bytecode end
|
||||
self.add_push_null(
|
||||
lambda: self.load_import_from(
|
||||
utils.__name__, "record_pregraph_bytecode_exit"
|
||||
)
|
||||
)
|
||||
assert cm_var is not None
|
||||
self.extend_output([self.create_load(cm_var)])
|
||||
self.extend_output(create_call_function(1, False))
|
||||
self.pop_top()
|
||||
|
||||
self.extend_output(create_call_function(len(graphargs), False))
|
||||
|
||||
def create_import_name(self, module_name: str) -> Instruction:
|
||||
return create_instruction("IMPORT_NAME", argval=module_name)
|
||||
|
||||
def load_import_from(self, module_name: str, object_name: str) -> None:
|
||||
source = AttrSource(self.tx.import_source(module_name), object_name)
|
||||
# Note: This approach is somewhat aggressive because typically, a source is marked
|
||||
# as a tempvar only when it is used more than once. In this case, we're marking it
|
||||
# as a tempvar without performing that analysis. However, this is a simple solution,
|
||||
# and in many cases, load imports are reused multiple times.
|
||||
self.mark_source_temp(source)
|
||||
self(source)
|
||||
|
||||
def create_call_function_kw(
|
||||
self, nargs: int, kw_names: Iterable[str], push_null: bool
|
||||
) -> list[Instruction]:
|
||||
if sys.version_info >= (3, 13):
|
||||
output = create_call_function(nargs, push_null)
|
||||
assert output[-1].opname == "CALL"
|
||||
output.insert(-1, self.create_load_const(kw_names))
|
||||
output[-1] = create_instruction("CALL_KW", arg=nargs)
|
||||
return output
|
||||
elif sys.version_info >= (3, 11):
|
||||
output = create_call_function(nargs, push_null)
|
||||
if sys.version_info >= (3, 12):
|
||||
idx = -1
|
||||
expected_inst = "CALL"
|
||||
else:
|
||||
idx = -2
|
||||
expected_inst = "PRECALL"
|
||||
assert output[idx].opname == expected_inst
|
||||
kw_names_inst = create_instruction("KW_NAMES", argval=kw_names)
|
||||
output.insert(idx, kw_names_inst)
|
||||
return output
|
||||
return [
|
||||
self.create_load_const(kw_names),
|
||||
create_instruction("CALL_FUNCTION_KW", arg=nargs),
|
||||
]
|
||||
|
||||
def create_delete(self, value: object) -> Instruction:
|
||||
return create_instruction("DELETE_FAST", argval=value)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,694 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import dataclasses
|
||||
import dis
|
||||
import functools
|
||||
import logging
|
||||
import sys
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import types
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
from .symbolic_convert import InstructionTranslatorBase
|
||||
|
||||
from .bytecode_transformation import (
|
||||
create_copy,
|
||||
create_dup_top,
|
||||
create_instruction,
|
||||
create_swap,
|
||||
Instruction,
|
||||
unique_id,
|
||||
)
|
||||
from .codegen import PyCodegen
|
||||
from .exc import unimplemented
|
||||
from .output_graph import GraphCompileReason, StackLocalsMetadata
|
||||
from .variables.misc import NullVariable, UnknownVariable
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@functools.cache
|
||||
def _get_comprehension_bytecode_prefix() -> list[str]:
|
||||
"""Get the bytecode instructions that precede BUILD_LIST in a list comprehension."""
|
||||
|
||||
assert sys.version_info >= (3, 12)
|
||||
|
||||
def fn() -> list[int]:
|
||||
return [i for i in range(1)] # noqa: C416
|
||||
|
||||
insts = [inst.opname for inst in dis.get_instructions(fn)]
|
||||
|
||||
start_idx = len(insts) - 1 - insts[::-1].index("LOAD_FAST_AND_CLEAR")
|
||||
end_idx = insts.index("BUILD_LIST")
|
||||
|
||||
return insts[start_idx:end_idx]
|
||||
|
||||
|
||||
@functools.cache
|
||||
def _get_comprehension_result_patterns() -> dict[str, dict[str, Any]]:
|
||||
"""Discover bytecode patterns for comprehension result handling.
|
||||
|
||||
Analyzes sample functions to extract the opcode sequences that appear
|
||||
after END_FOR for each result disposition (stored, discarded, returned, consumed).
|
||||
|
||||
Returns patterns with:
|
||||
- pre_store_ops: opcodes between END_FOR and first STORE_FAST
|
||||
- post_store_op: first opcode after all STORE_FASTs (for disambiguation)
|
||||
"""
|
||||
assert sys.version_info >= (3, 12)
|
||||
|
||||
def fn_stored() -> list[int]:
|
||||
result = [i for i in range(1)] # noqa: C416
|
||||
return result
|
||||
|
||||
def fn_discarded() -> int:
|
||||
[i for i in range(1)] # noqa: C416
|
||||
return 1
|
||||
|
||||
def fn_returned() -> list[int]:
|
||||
return [i for i in range(1)] # noqa: C416
|
||||
|
||||
def fn_consumed() -> int:
|
||||
return sum([i for i in range(1)]) # noqa: C416
|
||||
|
||||
def extract_pattern(fn: Callable[..., Any]) -> tuple[list[str], str | None]:
|
||||
"""Extract (pre_store_ops, post_store_op) from comprehension bytecode."""
|
||||
target_line = list(dis.findlinestarts(fn.__code__))[1][1]
|
||||
insts: list[str] = []
|
||||
started = False
|
||||
for instr in dis.get_instructions(fn):
|
||||
if started and instr.starts_line:
|
||||
break
|
||||
pos = instr.positions
|
||||
if pos and pos.lineno == target_line:
|
||||
started = started or bool(instr.starts_line)
|
||||
insts.append(instr.opname)
|
||||
|
||||
ops = insts[insts.index("END_FOR") + 1 :]
|
||||
idx = 0
|
||||
|
||||
pre_store_ops = []
|
||||
while idx < len(ops) and ops[idx] != "STORE_FAST":
|
||||
pre_store_ops.append(ops[idx])
|
||||
idx += 1
|
||||
|
||||
while idx < len(ops) and ops[idx] == "STORE_FAST":
|
||||
idx += 1
|
||||
|
||||
return pre_store_ops, ops[idx] if idx < len(ops) else None
|
||||
|
||||
stored = extract_pattern(fn_stored)
|
||||
discarded = extract_pattern(fn_discarded)
|
||||
returned = extract_pattern(fn_returned)
|
||||
consumed = extract_pattern(fn_consumed)
|
||||
|
||||
return {
|
||||
"stored": {"pre_store_ops": stored[0], "post_store_op": stored[1]},
|
||||
"discarded": {"pre_store_ops": discarded[0], "post_store_op": discarded[1]},
|
||||
"returned": {"pre_store_ops": returned[0], "post_store_op": returned[1]},
|
||||
"consumed": {"pre_store_ops": consumed[0], "post_store_op": []},
|
||||
}
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ComprehensionAnalysis:
|
||||
"""Metadata about a comprehension's bytecode structure.
|
||||
|
||||
Attributes:
|
||||
end_ip: Instruction pointer after all comprehension bytecode
|
||||
result_var: Name of result variable, or None if result stays on stack
|
||||
result_on_stack: True if result stays on stack (discarded, returned, or in expression)
|
||||
iterator_vars: Variables from LOAD_FAST_AND_CLEAR (need restoration)
|
||||
walrus_vars: Variables assigned via walrus operator (:=) inside comprehension
|
||||
captured_vars: Variables read from outer scope via LOAD_FAST inside comprehension
|
||||
"""
|
||||
|
||||
end_ip: int
|
||||
result_var: str | None
|
||||
result_on_stack: bool
|
||||
iterator_vars: list[str]
|
||||
walrus_vars: list[str]
|
||||
captured_vars: list[str]
|
||||
|
||||
|
||||
def _is_comprehension_start(tx: InstructionTranslatorBase) -> bool:
|
||||
"""Detect if we're at the start of a list/dict comprehension in 3.12+.
|
||||
|
||||
In Python 3.12+, comprehensions are inlined with a bytecode pattern that
|
||||
precedes BUILD_LIST/BUILD_MAP.
|
||||
"""
|
||||
assert sys.version_info >= (3, 12)
|
||||
|
||||
assert tx.instruction_pointer is not None
|
||||
ip = tx.instruction_pointer - 1
|
||||
|
||||
pattern = _get_comprehension_bytecode_prefix()
|
||||
prefix = [inst.opname for inst in tx.instructions[ip - len(pattern) : ip]]
|
||||
|
||||
return prefix == pattern
|
||||
|
||||
|
||||
def _find_comprehension_end_for_ip(tx: InstructionTranslatorBase) -> int:
|
||||
"""Find the instruction pointer of the outermost END_FOR for current comprehension."""
|
||||
assert sys.version_info >= (3, 12)
|
||||
assert tx.instruction_pointer is not None
|
||||
|
||||
nesting_depth = 0
|
||||
for search_ip in range(tx.instruction_pointer, len(tx.instructions)):
|
||||
inst = tx.instructions[search_ip]
|
||||
if inst.opname == "FOR_ITER":
|
||||
nesting_depth += 1
|
||||
elif inst.opname == "END_FOR":
|
||||
nesting_depth -= 1
|
||||
if nesting_depth == 0:
|
||||
return search_ip
|
||||
return -1
|
||||
|
||||
|
||||
def _analyze_comprehension(tx: InstructionTranslatorBase) -> ComprehensionAnalysis:
|
||||
"""Analyze comprehension bytecode to determine result handling pattern."""
|
||||
assert sys.version_info >= (3, 12)
|
||||
assert tx.instruction_pointer is not None
|
||||
|
||||
patterns = _get_comprehension_result_patterns()
|
||||
start_ip = tx.instruction_pointer - 1 # BUILD_LIST/BUILD_MAP
|
||||
|
||||
iterator_vars: list[str] = []
|
||||
walrus_vars: list[str] = []
|
||||
captured_vars: list[str] = []
|
||||
defined_inside: set[str] = set()
|
||||
|
||||
# Collect iterator variables from LOAD_FAST_AND_CLEAR before BUILD_LIST/BUILD_MAP
|
||||
iter_scan_ip = start_ip - 1
|
||||
while iter_scan_ip >= 0:
|
||||
inst = tx.instructions[iter_scan_ip]
|
||||
if inst.opname == "LOAD_FAST_AND_CLEAR":
|
||||
iterator_vars.insert(0, inst.argval)
|
||||
iter_scan_ip -= 1
|
||||
elif inst.opname in ("SWAP", "GET_ITER"):
|
||||
iter_scan_ip -= 1
|
||||
else:
|
||||
break
|
||||
defined_inside.update(iterator_vars)
|
||||
|
||||
end_for_ip = _find_comprehension_end_for_ip(tx)
|
||||
if end_for_ip == -1:
|
||||
unimplemented(
|
||||
gb_type="Comprehension analysis failed: No END_FOR",
|
||||
context="",
|
||||
explanation="Could not find END_FOR instruction in comprehension bytecode.",
|
||||
hints=[],
|
||||
)
|
||||
|
||||
# Find first FOR_ITER to know where loop body starts
|
||||
for_iter_ip = next(
|
||||
i
|
||||
for i in range(start_ip, end_for_ip)
|
||||
if tx.instructions[i].opname == "FOR_ITER"
|
||||
)
|
||||
|
||||
# Single pass through loop body to detect walrus vars and captured vars
|
||||
for body_ip in range(for_iter_ip + 1, end_for_ip):
|
||||
inst = tx.instructions[body_ip]
|
||||
|
||||
# Detect walrus pattern: COPY 1 followed by STORE_FAST
|
||||
if inst.opname == "COPY" and inst.arg == 1 and body_ip + 1 < end_for_ip:
|
||||
next_inst = tx.instructions[body_ip + 1]
|
||||
if next_inst.opname == "STORE_FAST":
|
||||
var_name = next_inst.argval
|
||||
if var_name not in iterator_vars and var_name not in walrus_vars:
|
||||
walrus_vars.append(var_name)
|
||||
defined_inside.add(var_name)
|
||||
|
||||
# Track variables defined inside the loop
|
||||
if inst.opname == "STORE_FAST":
|
||||
defined_inside.add(inst.argval)
|
||||
|
||||
# Detect LOAD_FAST referencing outer variables
|
||||
elif inst.opname.startswith("LOAD_FAST"):
|
||||
var_names = (
|
||||
inst.argval if isinstance(inst.argval, tuple) else (inst.argval,)
|
||||
)
|
||||
for var_name in var_names:
|
||||
if var_name not in defined_inside and var_name not in captured_vars:
|
||||
captured_vars.append(var_name)
|
||||
|
||||
# Extract pre_store_ops: all opcodes from END_FOR+1 until first STORE_FAST
|
||||
pre_store_ops: list[str] = []
|
||||
scan_ip = end_for_ip + 1
|
||||
while (
|
||||
scan_ip < len(tx.instructions)
|
||||
and tx.instructions[scan_ip].opname != "STORE_FAST"
|
||||
):
|
||||
pre_store_ops.append(tx.instructions[scan_ip].opname)
|
||||
scan_ip += 1
|
||||
|
||||
store_fast_ip = scan_ip
|
||||
|
||||
# Skip all STORE_FASTs to find post_store_op
|
||||
while (
|
||||
scan_ip < len(tx.instructions)
|
||||
and tx.instructions[scan_ip].opname == "STORE_FAST"
|
||||
):
|
||||
scan_ip += 1
|
||||
|
||||
post_store_op = (
|
||||
tx.instructions[scan_ip].opname if scan_ip < len(tx.instructions) else None
|
||||
)
|
||||
|
||||
def matches(name: str) -> bool:
|
||||
pat = patterns[name]
|
||||
return pre_store_ops == pat["pre_store_ops"] and (
|
||||
post_store_op == pat["post_store_op"] or not pat["post_store_op"]
|
||||
)
|
||||
|
||||
result_var: str | None = None
|
||||
if matches("stored"):
|
||||
result_var = tx.instructions[store_fast_ip].argval
|
||||
result_on_stack = False
|
||||
elif matches("discarded"):
|
||||
result_var = None
|
||||
result_on_stack = False
|
||||
scan_ip = scan_ip + 1 if patterns["discarded"]["post_store_op"] else scan_ip
|
||||
elif matches("returned") or pre_store_ops == patterns["consumed"]["pre_store_ops"]:
|
||||
result_var = None
|
||||
result_on_stack = True
|
||||
else:
|
||||
unimplemented(
|
||||
gb_type="Comprehension analysis failed: No matches",
|
||||
context=f"pre_store_ops={pre_store_ops}, post_store_op={post_store_op}",
|
||||
explanation="Comprehension does not match any known bytecode pattern.",
|
||||
hints=[],
|
||||
)
|
||||
|
||||
return ComprehensionAnalysis(
|
||||
end_ip=scan_ip,
|
||||
result_var=result_var,
|
||||
# pyrefly: ignore [unbound-name]
|
||||
result_on_stack=result_on_stack,
|
||||
iterator_vars=iterator_vars,
|
||||
walrus_vars=walrus_vars,
|
||||
captured_vars=captured_vars,
|
||||
)
|
||||
|
||||
|
||||
def _handle_comprehension_graph_break(
|
||||
tx: InstructionTranslatorBase, inst: Instruction
|
||||
) -> None:
|
||||
"""Handle list/dict comprehension graph break.
|
||||
|
||||
Builds a synthetic function wrapping the comprehension bytecode,
|
||||
calls it via codegen_call_resume, then chains into the resume
|
||||
function for the post-comprehension code.
|
||||
"""
|
||||
assert sys.version_info >= (3, 12)
|
||||
assert tx.instruction_pointer is not None
|
||||
|
||||
start_ip = tx.instruction_pointer - 1 # BUILD_LIST/BUILD_MAP
|
||||
analysis = _analyze_comprehension(tx)
|
||||
stack_pops = 1 + len(analysis.iterator_vars)
|
||||
reason = GraphCompileReason("comprehension_graph_break", [tx.frame_summary()])
|
||||
log.debug("comprehension triggered compile")
|
||||
|
||||
# --- Step 1: Compile the graph up to the comprehension ---
|
||||
|
||||
all_stack_locals_metadata = tx.output.compile_subgraph(
|
||||
tx,
|
||||
reason=reason,
|
||||
stack_pops=stack_pops,
|
||||
)
|
||||
# Record which stack_pops items are NULL before popn loses the info.
|
||||
# NULLs on the CPython stack can't be passed as function arguments.
|
||||
stack_pops_null_mask = [
|
||||
isinstance(tx.stack[len(tx.stack) - stack_pops + i], NullVariable)
|
||||
for i in range(stack_pops)
|
||||
]
|
||||
|
||||
tx.popn(stack_pops)
|
||||
meta = all_stack_locals_metadata[0]
|
||||
cg = PyCodegen(tx.output.root_tx)
|
||||
|
||||
# Runtime stack after compile_subgraph:
|
||||
# cells, [frame_values], *(non-popped items), *(stack_pops items w/ NULLs)
|
||||
# frame_values[0] = [frame N locals] (no stack items yet)
|
||||
|
||||
nonnull_count = sum(1 for m in stack_pops_null_mask if not m)
|
||||
|
||||
# live_stack_depth: stack items above cells/frame_values excluding NULLs
|
||||
# that compile_subgraph didn't codegen (tracked in stack_null_idxes).
|
||||
live_stack_depth = len(tx.stack) - len(meta.stack_null_idxes)
|
||||
|
||||
# --- Step 2: Pop stack_pops items and append non-nulls to frame_values[0] ---
|
||||
# SWAP each item to TOS then LIST_APPEND or pop_null; fv_list stays at
|
||||
# TOS throughout. Items append in TOS-first (reversed) order;
|
||||
# _build_comprehension_fn compensates by loading in reverse.
|
||||
cg.extend_output(
|
||||
[
|
||||
# frame_values[0] to TOS
|
||||
*create_copy(live_stack_depth + stack_pops + 1),
|
||||
cg.create_load_const(0),
|
||||
cg.create_binary_subscr(),
|
||||
]
|
||||
)
|
||||
for i in reversed(range(stack_pops)):
|
||||
cg.extend_output(create_swap(2))
|
||||
if stack_pops_null_mask[i]:
|
||||
cg.extend_output(cg.pop_null())
|
||||
else:
|
||||
cg.extend_output([create_instruction("LIST_APPEND", arg=1)])
|
||||
cg.extend_output([create_instruction("POP_TOP")])
|
||||
|
||||
# Stack: cells, [frame_values], *(non-popped items)
|
||||
|
||||
# --- Step 3: Build comprehension function ---
|
||||
new_code, fn_name = _build_comprehension_fn(
|
||||
tx,
|
||||
analysis,
|
||||
start_ip,
|
||||
stack_pops,
|
||||
stack_pops_null_mask,
|
||||
nonnull_count,
|
||||
meta,
|
||||
)
|
||||
|
||||
# --- Step 4: Extract [cells[0]] and [frame_values[0]] for codegen_call_resume ---
|
||||
cg.extend_output(
|
||||
[
|
||||
*create_copy(live_stack_depth + 2),
|
||||
cg.create_load_const(0),
|
||||
cg.create_binary_subscr(),
|
||||
create_instruction("BUILD_LIST", arg=1),
|
||||
*create_copy(live_stack_depth + 2),
|
||||
cg.create_load_const(0),
|
||||
cg.create_binary_subscr(),
|
||||
create_instruction("BUILD_LIST", arg=1),
|
||||
]
|
||||
)
|
||||
|
||||
# Stack: ..., *(non-popped), [cells[0]], [frame_values[0]]
|
||||
|
||||
# --- Step 5: Call comprehension function via codegen_call_resume ---
|
||||
tx.codegen_call_resume([new_code], [fn_name], cg)
|
||||
|
||||
# Stack: ..., *(non-popped), comp_result
|
||||
|
||||
# --- Step 6: Remove appended stack_pops items from frame_values[0] ---
|
||||
if nonnull_count > 0:
|
||||
frame_values_pos = live_stack_depth + 1 + 1 # +1 result, +1 frame_values
|
||||
cg.extend_output(
|
||||
[
|
||||
*create_copy(frame_values_pos),
|
||||
cg.create_load_const(0),
|
||||
cg.create_binary_subscr(),
|
||||
# frame_values[0] on TOS
|
||||
create_dup_top(),
|
||||
# frame_values[0], frame_values[0]
|
||||
cg.create_load_const(-nonnull_count),
|
||||
cg.create_load_const(None),
|
||||
create_instruction("BUILD_SLICE", arg=2),
|
||||
create_instruction("DELETE_SUBSCR"),
|
||||
# del frame_values[0][-nonnull_count:]
|
||||
create_instruction("POP_TOP"),
|
||||
]
|
||||
)
|
||||
|
||||
# --- Step 7: Pass comprehension outputs to frame_values[0] ---
|
||||
# Walrus vars first, then result_var.
|
||||
vars_to_pass = analysis.walrus_vars + (
|
||||
[analysis.result_var] if analysis.result_var else []
|
||||
)
|
||||
|
||||
existing_vars: dict[str, int] = {}
|
||||
for var_name in vars_to_pass:
|
||||
tx.symbolic_locals[var_name] = UnknownVariable()
|
||||
if var_name in meta.locals_names:
|
||||
existing_vars[var_name] = meta.locals_names[var_name]
|
||||
else:
|
||||
meta.locals_names[var_name] = len(meta.locals_names)
|
||||
|
||||
fv_depth = live_stack_depth + 2 # comp_result + frame_values
|
||||
|
||||
# --- Walrus vars: extract from comp_result tuple ---
|
||||
if analysis.walrus_vars:
|
||||
# comp_result is (result, *walrus_vars).
|
||||
cg.extend_output(
|
||||
[
|
||||
*create_copy(fv_depth),
|
||||
cg.create_load_const(0),
|
||||
cg.create_binary_subscr(),
|
||||
]
|
||||
)
|
||||
# Stack: ..., comp_tuple, fv0
|
||||
for j, walrus_var in enumerate(analysis.walrus_vars):
|
||||
cg.extend_output(
|
||||
[
|
||||
*create_copy(2),
|
||||
cg.create_load_const(j + 1),
|
||||
cg.create_binary_subscr(),
|
||||
]
|
||||
)
|
||||
# Stack: ..., comp_tuple, fv0, walrus_value
|
||||
if walrus_var in existing_vars:
|
||||
# fv0[idx] = walrus_value
|
||||
cg.extend_output(
|
||||
[
|
||||
*create_copy(2), # copy fv0
|
||||
cg.create_load_const(existing_vars[walrus_var]),
|
||||
create_instruction("STORE_SUBSCR"),
|
||||
]
|
||||
)
|
||||
else:
|
||||
cg.extend_output([create_instruction("LIST_APPEND", arg=1)])
|
||||
# Stack: ..., comp_tuple, fv0
|
||||
cg.extend_output(
|
||||
[
|
||||
create_instruction("POP_TOP"), # pop fv0
|
||||
# Extract the result from the tuple.
|
||||
cg.create_load_const(0),
|
||||
cg.create_binary_subscr(),
|
||||
]
|
||||
)
|
||||
# Stack: ..., result
|
||||
|
||||
# --- Result: keep on stack, overwrite/append to fv[0], or discard ---
|
||||
if analysis.result_on_stack:
|
||||
tx.push(UnknownVariable())
|
||||
elif analysis.result_var:
|
||||
cg.extend_output(
|
||||
[
|
||||
*create_copy(fv_depth),
|
||||
cg.create_load_const(0),
|
||||
cg.create_binary_subscr(),
|
||||
# Stack: ..., result, fv0
|
||||
]
|
||||
)
|
||||
if analysis.result_var in existing_vars:
|
||||
cg.extend_output(
|
||||
[
|
||||
cg.create_load_const(existing_vars[analysis.result_var]),
|
||||
create_instruction("STORE_SUBSCR"),
|
||||
# fv0[idx] = result
|
||||
]
|
||||
)
|
||||
else:
|
||||
cg.extend_output(
|
||||
[
|
||||
*create_swap(2),
|
||||
create_instruction("LIST_APPEND", arg=1),
|
||||
create_instruction("POP_TOP"),
|
||||
]
|
||||
)
|
||||
else:
|
||||
cg.extend_output([create_instruction("POP_TOP")])
|
||||
|
||||
# Stack: cells, [frame_values], *(non-popped stack)
|
||||
tx.output.add_output_instructions(cg.get_instructions())
|
||||
|
||||
# --- Step 8: Create resume function chain ---
|
||||
resume_inst = tx.instructions[analysis.end_ip]
|
||||
tx.output.add_output_instructions(
|
||||
tx.create_call_resume_at(resume_inst, all_stack_locals_metadata)
|
||||
)
|
||||
|
||||
tx.instruction_pointer = None
|
||||
|
||||
|
||||
def _build_comprehension_fn(
|
||||
tx: InstructionTranslatorBase,
|
||||
analysis: ComprehensionAnalysis,
|
||||
start_ip: int,
|
||||
stack_pops: int,
|
||||
stack_pops_null_mask: list[bool],
|
||||
nonnull_count: int,
|
||||
meta: StackLocalsMetadata,
|
||||
) -> tuple[types.CodeType, str]:
|
||||
"""Build a synthetic function wrapping comprehension bytecode.
|
||||
|
||||
Uses the same calling convention as resume functions created by
|
||||
create_resume / ContinueExecutionCache.generate: the first two args
|
||||
are __nested_resume_fns and __nested_frame_values (ignored here),
|
||||
followed by stack items and live locals.
|
||||
|
||||
Returns (code, name) where name is the global name for the function.
|
||||
"""
|
||||
from .bytecode_transformation import transform_code_object
|
||||
from .eval_frame import skip_code
|
||||
from .resume_execution import CO_VARARGS, CO_VARKEYWORDS
|
||||
|
||||
# Args follow frame_values layout: locals first, then stack_pops items
|
||||
# (appended to end of frame_values[0] by the caller).
|
||||
# codegen_call_resume unpacks frame_values[0] as positional args.
|
||||
argnames = tuple(k for k in meta.locals_names if k not in tx.cell_and_freevars())
|
||||
args = (
|
||||
["__nested_resume_fns", "__nested_frame_values"]
|
||||
+ list(argnames)
|
||||
+ [f"___stack{i}" for i in range(nonnull_count)]
|
||||
)
|
||||
|
||||
freevars = tuple(
|
||||
sorted(list(tx.f_code.co_cellvars or []) + list(tx.f_code.co_freevars or []))
|
||||
)
|
||||
|
||||
lineno = tx.lineno if tx.lineno is not None else tx.f_code.co_firstlineno
|
||||
fn_name = unique_id(f"__comprehension_{tx.f_code.co_name}_at_{lineno}")
|
||||
|
||||
comprehension_body_vars = (
|
||||
analysis.iterator_vars
|
||||
+ analysis.walrus_vars
|
||||
+ ([analysis.result_var] if analysis.result_var else [])
|
||||
+ analysis.captured_vars
|
||||
)
|
||||
|
||||
def update(instructions: list[Instruction], code_options: dict[str, Any]) -> None:
|
||||
code_options["co_name"] = fn_name
|
||||
if sys.version_info >= (3, 11):
|
||||
code_options["co_qualname"] = fn_name
|
||||
code_options["co_firstlineno"] = lineno
|
||||
code_options["co_cellvars"] = ()
|
||||
code_options["co_freevars"] = freevars
|
||||
code_options["co_argcount"] = len(args)
|
||||
code_options["co_posonlyargcount"] = 0
|
||||
code_options["co_kwonlyargcount"] = 0
|
||||
code_options["co_varnames"] = tuple(
|
||||
args + [v for v in comprehension_body_vars if v not in args]
|
||||
)
|
||||
code_options["co_flags"] = code_options["co_flags"] & ~(
|
||||
CO_VARARGS | CO_VARKEYWORDS
|
||||
)
|
||||
|
||||
prefix: list[Instruction] = []
|
||||
if freevars:
|
||||
prefix.append(create_instruction("COPY_FREE_VARS", arg=len(freevars)))
|
||||
prefix.append(create_instruction("RESUME", arg=0))
|
||||
|
||||
# Push stack_pops items onto operand stack so the comprehension
|
||||
# bytecode finds them where it expects (iterator + saved vars).
|
||||
# NULL positions get PUSH_NULL, non-null get LOAD_FAST.
|
||||
# Items were appended to frame_values[0] in TOS-first order,
|
||||
# so load in reverse to reconstruct the original stack layout.
|
||||
nonnull_i = nonnull_count - 1
|
||||
for i in range(stack_pops):
|
||||
if stack_pops_null_mask[i]:
|
||||
prefix.append(create_instruction("PUSH_NULL"))
|
||||
else:
|
||||
prefix.append(
|
||||
create_instruction("LOAD_FAST", argval=f"___stack{nonnull_i}")
|
||||
)
|
||||
nonnull_i -= 1
|
||||
|
||||
comp_insts = _copy_comprehension_bytecode(tx, start_ip, analysis.end_ip)
|
||||
|
||||
# Epilogue: ensure result is on stack, pack walrus vars, return.
|
||||
epilogue: list[Instruction] = []
|
||||
if not analysis.result_on_stack:
|
||||
if analysis.result_var:
|
||||
epilogue.append(
|
||||
create_instruction("LOAD_FAST", argval=analysis.result_var)
|
||||
)
|
||||
else:
|
||||
epilogue.append(create_instruction("LOAD_CONST", argval=None))
|
||||
if analysis.walrus_vars:
|
||||
for var_name in analysis.walrus_vars:
|
||||
epilogue.append(create_instruction("LOAD_FAST", argval=var_name))
|
||||
epilogue.append(
|
||||
create_instruction(
|
||||
"BUILD_TUPLE",
|
||||
arg=1 + len(analysis.walrus_vars),
|
||||
)
|
||||
)
|
||||
epilogue.append(create_instruction("RETURN_VALUE"))
|
||||
|
||||
instructions[:] = prefix + comp_insts + epilogue
|
||||
|
||||
new_code, _ = transform_code_object(tx.f_code, update)
|
||||
skip_code(new_code)
|
||||
|
||||
# Install as global
|
||||
tx.output.install_resume_function_global(fn_name, new_code, tx.f_globals)
|
||||
|
||||
return new_code, fn_name
|
||||
|
||||
|
||||
def _copy_comprehension_bytecode(
|
||||
tx: InstructionTranslatorBase, start_ip: int, end_ip: int
|
||||
) -> list[Instruction]:
|
||||
"""Copy comprehension bytecode instructions, updating jump targets."""
|
||||
inst_map: dict[Instruction, Instruction] = {}
|
||||
copied_insts: list[Instruction] = []
|
||||
|
||||
for ip in range(start_ip, end_ip):
|
||||
original_inst = tx.instructions[ip]
|
||||
copied_inst = copy.copy(original_inst)
|
||||
copied_inst.exn_tab_entry = None
|
||||
inst_map[original_inst] = copied_inst
|
||||
copied_insts.append(copied_inst)
|
||||
|
||||
for copied_inst in copied_insts:
|
||||
if copied_inst.target is not None and copied_inst.target in inst_map:
|
||||
copied_inst.target = inst_map[copied_inst.target]
|
||||
|
||||
return copied_insts
|
||||
|
||||
|
||||
def maybe_setup_comprehension_speculation(
|
||||
tx: InstructionTranslatorBase, inst: Instruction
|
||||
) -> bool:
|
||||
"""
|
||||
Handle comprehension start for Python 3.12+ BUILD_LIST/BUILD_MAP with argval 0.
|
||||
Returns True if a graph break was triggered and the caller should return early.
|
||||
"""
|
||||
if not (sys.version_info >= (3, 12) and inst.argval == 0):
|
||||
return False
|
||||
|
||||
if not _is_comprehension_start(tx):
|
||||
return False
|
||||
|
||||
can_speculate = (
|
||||
all(b.can_restore() for b in tx.block_stack)
|
||||
and not tx.one_graph
|
||||
and not tx.error_on_graph_break
|
||||
and not tx.is_tracing_resume_prologue
|
||||
and not tx.active_generic_context_managers
|
||||
and tx.output.current_tracer.parent is None
|
||||
)
|
||||
|
||||
if can_speculate and tx.parent is not None:
|
||||
can_speculate = tx._can_speculate_comprehension_nested()
|
||||
# Only set up speculation at depth 0 (outermost comprehension)
|
||||
if can_speculate and tx._comprehension_depth == 0:
|
||||
speculation = tx.speculate()
|
||||
if speculation.failed(tx):
|
||||
_handle_comprehension_graph_break(tx, inst)
|
||||
return True
|
||||
tx.current_speculation = speculation
|
||||
end_for_ip = _find_comprehension_end_for_ip(tx)
|
||||
assert end_for_ip >= 0
|
||||
tx._comprehension_end_for_ips.add(end_for_ip)
|
||||
tx._comprehension_depth += 1
|
||||
return False
|
||||
@@ -0,0 +1,438 @@
|
||||
"""
|
||||
This module provides the public comptime interface to TorchDynamo, enabling users to execute
|
||||
arbitrary Python code during symbolic evaluation of their programs.
|
||||
|
||||
The comptime interface allows inspection and modification of TorchDynamo's compilation
|
||||
process while it is running. This can be useful for:
|
||||
|
||||
- Debugging compilation issues
|
||||
- Inspecting intermediate state
|
||||
- Adding custom guards or graph breaks
|
||||
- Analyzing symbolic shapes and values
|
||||
|
||||
Example usage:
|
||||
|
||||
import torch
|
||||
from torch._dynamo.comptime import comptime
|
||||
|
||||
def my_model(x):
|
||||
# Print the compile-time known information about x
|
||||
comptime.print(x)
|
||||
|
||||
# Print the current FX graph being constructed
|
||||
comptime.print_graph()
|
||||
|
||||
# Force a value to be treated as static
|
||||
if comptime(lambda ctx: ctx.get_local("x").is_dynamic()):
|
||||
comptime.force_static(x)
|
||||
|
||||
# Add a manual graph break
|
||||
comptime.graph_break()
|
||||
|
||||
Note: While this API provides significant flexibility, it intentionally avoids
|
||||
exposing internal implementation details of TorchDynamo to maintain compatibility
|
||||
across versions.
|
||||
"""
|
||||
|
||||
import builtins
|
||||
import dis
|
||||
import time
|
||||
import traceback
|
||||
from collections.abc import Callable, Sequence
|
||||
from typing import Any, TextIO
|
||||
|
||||
import torch
|
||||
from torch._dynamo.symbolic_convert import InstructionTranslatorBase
|
||||
from torch._dynamo.variables.base import VariableTracker
|
||||
from torch._subclasses.fake_tensor import FakeTensor
|
||||
from torch.fx.experimental.symbolic_shapes import free_symbols
|
||||
|
||||
from .exc import unimplemented
|
||||
from .variables import CellVariable
|
||||
from .variables.tensor import SymNodeVariable
|
||||
|
||||
|
||||
class ComptimeVar:
|
||||
"""
|
||||
A ComptimeVar represents a Python value, at some particular point
|
||||
in time, in the Python code we are symbolically evaluating with
|
||||
torchdynamo. This must be distinguished from a runtime value, as
|
||||
at compile-time there are some properties of the variable we
|
||||
do not know (for example, if the ComptimeVar represents a Tensor,
|
||||
we only know metadata about the tensor; we do NOT know what the
|
||||
actual data in the Tensor is.)
|
||||
"""
|
||||
|
||||
def __init__(self, v: VariableTracker) -> None:
|
||||
self.__variable = v
|
||||
|
||||
def as_proxy(self) -> VariableTracker | Sequence[VariableTracker]:
|
||||
"""
|
||||
Returns an fx.Proxy (or tuple/list of fx.Proxy) representing
|
||||
this variable in the FX graph we are assembling to pass
|
||||
to the user compiler.
|
||||
|
||||
This method only works for variables we actually track in
|
||||
the FX graph, aka Tensors (and ints, if you are compiling
|
||||
with dynamic shapes). In particular, if you have a list
|
||||
or tuple of tensors, you will get a list/tuple of proxies
|
||||
(not a single proxy representing the entire list/tuple).
|
||||
"""
|
||||
return self.__variable.as_proxy()
|
||||
|
||||
def is_proxy(self) -> bool:
|
||||
"""
|
||||
Returns True if as_proxy() would succeed.
|
||||
"""
|
||||
return self.__variable.is_proxy()
|
||||
|
||||
def as_fake(self) -> FakeTensor | torch.SymInt:
|
||||
"""
|
||||
Returns a "fake" value (either a FakeTensor or a SymInt)
|
||||
representing the variable in question. This only works
|
||||
for variables that denote Tensor or int. You can use
|
||||
this to query metadata; e.g., v.as_fake().size(0) will
|
||||
tell you the compile-time known size of the tensor.
|
||||
|
||||
WARNING: Do NOT mutate the returned tensor.
|
||||
"""
|
||||
return self.__variable.as_proxy().node.meta["example_value"]
|
||||
|
||||
def size(self, dim: int | None = None) -> int | torch.SymInt:
|
||||
"""
|
||||
Returns the size of the tensor (if dim is None) or the size
|
||||
at the dimension dim. The returned size may be a SymInt.
|
||||
"""
|
||||
return self.as_fake().size(dim) # type: ignore[union-attr, return-value]
|
||||
|
||||
def python_type(self) -> type:
|
||||
"""
|
||||
Returns what type(v) would have returned for the variable
|
||||
at compile time.
|
||||
"""
|
||||
return self.__variable.python_type()
|
||||
|
||||
def as_python_constant(self) -> Any:
|
||||
"""
|
||||
Returns the Python value this variable would have, but only if it is
|
||||
completely known at compile-time (e.g., it is constant).
|
||||
|
||||
WARNING: Do NOT mutate the returned constant. The returned constant
|
||||
may or may not correspond to the actual value this variable may take
|
||||
on at runtime; for example, if the variable in question is a constant
|
||||
list, we may return a copy of that list.
|
||||
"""
|
||||
return self.__variable.as_python_constant()
|
||||
|
||||
def is_python_constant(self) -> bool:
|
||||
"""
|
||||
Returns True if as_python_constant would succeed.
|
||||
"""
|
||||
return self.__variable.is_python_constant()
|
||||
|
||||
def is_dynamic(self) -> bool:
|
||||
if isinstance(self.__variable, SymNodeVariable):
|
||||
fs = free_symbols(self.__variable.sym_num)
|
||||
return bool(fs)
|
||||
return False
|
||||
|
||||
def force_static(self) -> None:
|
||||
"""
|
||||
Forces that a value is static, inducing a guard on its specific value
|
||||
"""
|
||||
if isinstance(self.__variable, SymNodeVariable):
|
||||
self.__variable.evaluate_expr()
|
||||
elif self.__variable.is_python_constant():
|
||||
# TODO: Maybe complain if this isn't a int/bool/float variable
|
||||
pass
|
||||
else:
|
||||
raise AssertionError(
|
||||
f"cannot force {self.__variable} ({type(self.__variable)}) static"
|
||||
)
|
||||
|
||||
def _i_will_not_complain_if_bc_breaks_VariableTracker(self) -> VariableTracker:
|
||||
"""
|
||||
Returns the internal data structure VariableTracker that Dynamo uses
|
||||
to represent variables at compile time. There are no BC guarantees on
|
||||
this API and WE RESERVE THE RIGHT TO BREAK YOUR CODE if you rely on
|
||||
it.
|
||||
"""
|
||||
return self.__variable
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__variable.debug_repr()
|
||||
|
||||
# TODO: API for adding a custom guard
|
||||
|
||||
|
||||
class ComptimeContext:
|
||||
"""
|
||||
This context class provides access to a public API for Dynamo's internals.
|
||||
If there is something here you would find useful that is missing, please
|
||||
file a feature request at https://github.com/pytorch/pytorch/
|
||||
"""
|
||||
|
||||
def __init__(self, tx: InstructionTranslatorBase) -> None:
|
||||
self.__tx = tx
|
||||
|
||||
def get_local(self, name: str, *, stacklevel: int = 0) -> ComptimeVar:
|
||||
"""
|
||||
Retrieve the compile-time known information about a local.
|
||||
"""
|
||||
tx = self.__get_tx(stacklevel)
|
||||
var = tx.symbolic_locals[name]
|
||||
|
||||
# Auto-dereference when accessing cell locals in python.
|
||||
if isinstance(var, CellVariable):
|
||||
return ComptimeVar(tx.output.side_effects.load_cell(var))
|
||||
|
||||
return ComptimeVar(var)
|
||||
|
||||
def graph_break(self, msg: str = "ComptimeContext.graph_break") -> None:
|
||||
"""
|
||||
Manually trigger a graph break
|
||||
"""
|
||||
unimplemented(
|
||||
gb_type="ComptimeContext graph break",
|
||||
context=msg,
|
||||
explanation=f"Manually triggered ComptimeContext graph break with message {msg}.",
|
||||
hints=[],
|
||||
)
|
||||
|
||||
def graph(self) -> torch.fx.Graph:
|
||||
"""
|
||||
Retrieve the partially constructed FX graph that would be
|
||||
passed to the user compiler after compilation.
|
||||
"""
|
||||
return self.__tx.output.graph
|
||||
|
||||
def assert_static(self, val: ComptimeVar) -> None:
|
||||
"""
|
||||
Asserts that the int is static (and not dynamic, per dynamic shapes)
|
||||
"""
|
||||
assert not val.is_dynamic(), (
|
||||
"expected static but got dynamic (run with TORCH_LOGS=dynamic for more info)"
|
||||
)
|
||||
|
||||
def print_graph(self, *, verbose: bool = True, file: TextIO | None = None) -> None:
|
||||
"""
|
||||
Print the partially constructed FX graph that would be passed
|
||||
to the user compiler after compilation.
|
||||
"""
|
||||
print(
|
||||
self.__tx.output.graph.python_code("self", verbose=verbose).src, file=file
|
||||
)
|
||||
|
||||
def parent(self) -> "ComptimeContext":
|
||||
return ComptimeContext(self.__tx.parent) # type: ignore[arg-type]
|
||||
|
||||
def __get_tx(self, stacklevel: int) -> Any:
|
||||
tx = self.__tx
|
||||
# pyrefly: ignore [bad-assignment, non-convergent-recursion]
|
||||
for _ in range(stacklevel):
|
||||
tx = tx.parent # type: ignore[assignment]
|
||||
return tx
|
||||
|
||||
def print(self, val: Any, *, file: TextIO | None = None) -> None:
|
||||
print(repr(val), file=file)
|
||||
|
||||
def print_disas(self, *, file: TextIO | None = None, stacklevel: int = 0) -> None:
|
||||
"""
|
||||
Print the current series of opcodes being executed (not including
|
||||
parent frames), including where you are in the particular opcode
|
||||
stream.
|
||||
"""
|
||||
tx = self.__get_tx(stacklevel)
|
||||
print(
|
||||
dis.Bytecode(
|
||||
tx.f_code,
|
||||
current_offset=tx.instructions[tx.instruction_pointer].offset,
|
||||
).dis(),
|
||||
file=file,
|
||||
)
|
||||
|
||||
def print_value_stack(
|
||||
self, *, file: TextIO | None = None, stacklevel: int = 0
|
||||
) -> None:
|
||||
"""
|
||||
Print the current Python value stack. Note that this is NOT the same
|
||||
as the traceback; use print_bt() to print that. Note that at
|
||||
stacklevel=0, this will typically be empty, as comptime cannot
|
||||
currently be used in an expression context where there would be
|
||||
intermediates on the stack. If you would find this useful, please
|
||||
file a bug at https://github.com/pytorch/pytorch/
|
||||
|
||||
NB: Stack grows downwards in our print
|
||||
"""
|
||||
tx = self.__get_tx(stacklevel)
|
||||
for s in tx.stack:
|
||||
print(f"- {s.debug_repr()}", file=file)
|
||||
|
||||
def print_locals(self, *, file: TextIO | None = None, stacklevel: int = 0) -> None:
|
||||
"""
|
||||
Print all of the locals available in the current context.
|
||||
By default this view is very limited; you can get more information
|
||||
about any individual local using get_local().
|
||||
"""
|
||||
tx = self.__get_tx(stacklevel)
|
||||
for k, v in tx.symbolic_locals.items():
|
||||
print(f"{k} = {v.debug_repr()}", file=file)
|
||||
|
||||
def print_bt(self, *, file: TextIO | None = None, stacklevel: int = 0) -> None:
|
||||
"""
|
||||
Print the user code backtrace, starting at the beginning of the
|
||||
frame Dynamo started evaluating. Note that this MAY NOT go all
|
||||
the way to the torch.compile invocation, as we may have done
|
||||
a graph break and are compiling an intermediate frame as the
|
||||
starting point. If you think the other behavior would be better,
|
||||
file a bug at https://github.com/pytorch/pytorch/
|
||||
"""
|
||||
stack = []
|
||||
tx = self.__get_tx(stacklevel)
|
||||
while tx is not None:
|
||||
stack.append(tx.frame_summary())
|
||||
tx = getattr(tx, "parent", None)
|
||||
print(
|
||||
"".join(traceback.StackSummary.from_list(reversed(stack)).format()),
|
||||
file=file,
|
||||
)
|
||||
|
||||
def print_guards(self, *, file: TextIO | None = None) -> None:
|
||||
"""
|
||||
Print the currently installed guards for the Dynamo context.
|
||||
This does NOT include guards associated with variables that
|
||||
may or may not be installed in the future if those variables
|
||||
are used.
|
||||
"""
|
||||
# TODO: improve print format, current guard format is extremely
|
||||
# verbose
|
||||
print(
|
||||
"\n".join(f"{repr(guard)}" for guard in sorted(self.__tx.output.guards)),
|
||||
file=file,
|
||||
)
|
||||
|
||||
def _i_will_not_complain_if_bc_breaks_InstructionTranslator(
|
||||
self,
|
||||
) -> InstructionTranslatorBase:
|
||||
"""
|
||||
Returns the internal data structure InstructionTranslator that Dynamo
|
||||
uses to track state of symbolic evaluation. There are no BC
|
||||
guarantees on this API and WE RESERVE THE RIGHT TO BREAK YOUR CODE if
|
||||
you rely on it.
|
||||
"""
|
||||
return self.__tx
|
||||
|
||||
def sleep(self, sec: int | float) -> None:
|
||||
time.sleep(sec)
|
||||
|
||||
|
||||
class _Comptime:
|
||||
@staticmethod
|
||||
def __call__(
|
||||
fn: Callable[[ComptimeContext], Any],
|
||||
fallback_fn: Callable[[], Any] = lambda: None,
|
||||
) -> Any:
|
||||
"""fn gets called at compile time in TorchDynamo, calls fallback_fn otherwise"""
|
||||
fallback_fn()
|
||||
|
||||
# Convenience wrappers that are more compact to use
|
||||
|
||||
@staticmethod
|
||||
def graph_break() -> None:
|
||||
comptime(lambda ctx: ctx.graph_break())
|
||||
|
||||
@staticmethod
|
||||
def print(e: Any) -> None:
|
||||
comptime(lambda ctx: ctx.print(ctx.get_local("e")), lambda: print(e))
|
||||
|
||||
@staticmethod
|
||||
def print_graph() -> None:
|
||||
comptime(lambda ctx: ctx.print_graph())
|
||||
|
||||
@staticmethod
|
||||
def print_disas(*, stacklevel: int = 0) -> None:
|
||||
comptime(
|
||||
lambda ctx: ctx.print_disas(
|
||||
stacklevel=ctx.get_local("stacklevel").as_python_constant() + 1
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def print_value_stack(*, stacklevel: int = 0) -> None:
|
||||
comptime(
|
||||
lambda ctx: ctx.print_value_stack(
|
||||
stacklevel=ctx.get_local("stacklevel").as_python_constant() + 1
|
||||
)
|
||||
)
|
||||
|
||||
# This is a more useful variant of print_value_stack that can be used
|
||||
# in an expression context; e.g., x + print_value_stack_and_return(y + z),
|
||||
# you will see x on the stack prior to the addition operation
|
||||
@staticmethod
|
||||
def print_value_stack_and_return(e: Any, *, stacklevel: int = 0) -> Any:
|
||||
comptime(
|
||||
lambda ctx: ctx.print_value_stack(
|
||||
stacklevel=ctx.get_local("stacklevel").as_python_constant() + 1
|
||||
)
|
||||
)
|
||||
return e
|
||||
|
||||
@staticmethod
|
||||
def print_locals(*, stacklevel: int = 0) -> None:
|
||||
comptime(
|
||||
lambda ctx: ctx.print_locals(
|
||||
stacklevel=ctx.get_local("stacklevel").as_python_constant() + 1
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def print_bt(*, stacklevel: int = 0) -> None:
|
||||
comptime(
|
||||
lambda ctx: ctx.print_bt(
|
||||
stacklevel=ctx.get_local("stacklevel").as_python_constant() + 1
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def print_guards() -> None:
|
||||
comptime(lambda ctx: ctx.print_guards())
|
||||
|
||||
@staticmethod
|
||||
def assert_static(val: Any) -> None:
|
||||
comptime(lambda ctx: ctx.assert_static(ctx.get_local("val")))
|
||||
|
||||
@staticmethod
|
||||
def force_static(val: Any) -> None:
|
||||
comptime(lambda ctx: ctx.get_local("val").force_static())
|
||||
|
||||
@staticmethod
|
||||
def breakpoint() -> None:
|
||||
"""
|
||||
Like pdb breakpoint(), but drop into pdb whenever this line
|
||||
of code is compiled by dynamo. Use it by putting
|
||||
this in your model code::
|
||||
|
||||
from torch._dynamo.comptime import comptime
|
||||
|
||||
comptime.breakpoint()
|
||||
|
||||
And then, inside pdb, you can access 'ctx' to query things
|
||||
about the compilation context::
|
||||
|
||||
(Pdb) !ctx.print_bt()
|
||||
(Pdb) !ctx.print_locals()
|
||||
(Pdb) p ctx.get_local("attention").as_fake()
|
||||
"""
|
||||
|
||||
def inner(inner_ctx: ComptimeContext) -> None:
|
||||
ctx = inner_ctx.parent() # noqa: F841
|
||||
builtins.breakpoint()
|
||||
|
||||
comptime(inner)
|
||||
|
||||
@staticmethod
|
||||
def sleep(sec: int | float) -> None:
|
||||
comptime(lambda ctx: ctx.sleep(ctx.get_local("sec").as_python_constant()))
|
||||
|
||||
|
||||
comptime = _Comptime()
|
||||
@@ -0,0 +1,918 @@
|
||||
"""
|
||||
Configuration module for TorchDynamo compiler and optimization settings.
|
||||
|
||||
This module contains various configuration flags and settings that control TorchDynamo's
|
||||
behavior, including:
|
||||
|
||||
- Runtime behavior flags (e.g., guard settings, specialization options)
|
||||
- Debugging and development options
|
||||
- Performance tuning parameters
|
||||
- Feature toggles for experimental features
|
||||
"""
|
||||
|
||||
import getpass
|
||||
import os
|
||||
import sys
|
||||
import sysconfig
|
||||
import tempfile
|
||||
from collections.abc import Callable
|
||||
from os.path import abspath, dirname
|
||||
from typing import Any, Literal, TYPE_CHECKING
|
||||
|
||||
from torch._environment import is_fbcode
|
||||
from torch.utils._config_module import Config, get_tristate_env, install_config_module
|
||||
|
||||
|
||||
# to configure logging for dynamo, aot, and inductor
|
||||
# use the following API in the torch._logging module
|
||||
# torch._logging.set_logs(dynamo=<level>, aot=<level>, inductor<level>)
|
||||
# or use the environment variable TORCH_LOGS="dynamo,aot,inductor" (use a prefix + to indicate higher verbosity)
|
||||
# see this design doc for more detailed info
|
||||
# Design doc: https://docs.google.com/document/d/1ZRfTWKa8eaPq1AxaiHrq4ASTPouzzlPiuquSBEJYwS8/edit#
|
||||
# the name of a file to write the logs to
|
||||
# [@compile_ignored: debug]
|
||||
log_file_name: str | None = None
|
||||
|
||||
# [@compile_ignored: debug] Verbose will print full stack traces on warnings and errors
|
||||
verbose = os.environ.get("TORCHDYNAMO_VERBOSE", "0") == "1"
|
||||
|
||||
# [@compile_ignored: runtime_behaviour] verify the correctness of optimized backend
|
||||
verify_correctness = False
|
||||
|
||||
# Override backend for specific graphs (for debugging/bisecting).
|
||||
# Format: "filter1:backend1;filter2:backend2;..." where filter can be:
|
||||
# - Individual IDs: "0,5,10"
|
||||
# - Ranges: "10-20" (inclusive)
|
||||
# - Comparisons: ">10", ">=10", "<5", "<=5"
|
||||
# Backends can be: "eager", "aot_eager", "inductor", etc.
|
||||
# Examples:
|
||||
# ">10:eager" - Run graphs with frame_id > 10 in dynamo eager backend
|
||||
# "<=5:aot_eager;>5:inductor" - First 6 graphs use aot_eager, rest use inductor
|
||||
# [@compile_ignored: debug]
|
||||
debug_backend_override: str = os.environ.get("TORCH_COMPILE_OVERRIDE_BACKENDS", "")
|
||||
|
||||
# Override inductor config for specific graphs (for debugging/bisecting).
|
||||
# Format: "filter1:config1;filter2:config2;..." where filter uses same syntax as
|
||||
# debug_backend_override, and config is "key=value" or "key=value,key2=value2".
|
||||
# Examples:
|
||||
# "0-5:triton.cudagraph_skip_dynamic_graphs=False" - Disable skip for graphs 0-5
|
||||
# ">10:triton.cudagraphs=False" - Disable cudagraphs for graphs > 10
|
||||
# [@compile_ignored: debug]
|
||||
debug_inductor_config_override: str = os.environ.get(
|
||||
"TORCH_COMPILE_OVERRIDE_INDUCTOR_CONFIGS", ""
|
||||
)
|
||||
|
||||
# Override dynamo config for specific graphs (for debugging/bisecting).
|
||||
# Format: "filter1:config1;filter2:config2;..." where filter uses same syntax as
|
||||
# debug_backend_override, and config is "key=value" or "key=value,key2=value2".
|
||||
# Examples:
|
||||
# "0-5:specialize_float=True" - Specialize floats for graphs 0-5
|
||||
# ">10:automatic_dynamic_shapes=False" - Disable dynamic shapes for graphs > 10
|
||||
# [@compile_ignored: debug]
|
||||
debug_dynamo_config_override: str = os.environ.get(
|
||||
"TORCH_COMPILE_OVERRIDE_DYNAMO_CONFIGS", ""
|
||||
)
|
||||
|
||||
# Validate that fake_fn and real_fn in @leaf_function decorators produce outputs
|
||||
# with matching shapes and dtypes in eager mode. Helps catch mismatches early.
|
||||
# Disabled by default to avoid runtime overhead.
|
||||
# [@compile_ignored: debug]
|
||||
leaf_function_validate_outputs = False
|
||||
|
||||
# Check for escaped gradients in @leaf_function. When a leaf_function closes over
|
||||
# a tensor with requires_grad=True, gradients won't flow back to it. This check
|
||||
# walks the autograd graph to detect such cases and raises an error.
|
||||
# Disabled by default to avoid runtime overhead. Enable for debugging.
|
||||
# [@compile_ignored: debug]
|
||||
leaf_function_check_escaped_gradients = False
|
||||
|
||||
# need this many ops to create an FX graph (deprecated: not used)
|
||||
minimum_call_count = 1
|
||||
|
||||
# turn on/off DCE pass (deprecated: always true)
|
||||
dead_code_elimination = None
|
||||
|
||||
# Enable or disable side effect replay after graph execution.
|
||||
# When False, mutations to Python objects (lists, dicts, attributes) won't be
|
||||
# replayed after the compiled graph runs. This can cause correctness issues
|
||||
# if your code depends on these mutations being visible. This should probably
|
||||
# never be False by default. At the moment, only export will need it.
|
||||
replay_side_effects = True
|
||||
|
||||
# Configure side effect warning level
|
||||
# If `info` (default): allow side effects and log to TORCH_LOGS="side_effects" and tlparse
|
||||
# If `silent`, we allow side effects, no logs are made.
|
||||
# If `warn`, we allow side effects but issue warnings
|
||||
# If `error`, we error on side effects
|
||||
# NOTE: it is NOT safe to change this config during compilation!
|
||||
side_effect_replay_policy = "info"
|
||||
|
||||
# disable (for a function) when cache reaches this size
|
||||
|
||||
# controls the maximum number of cache entries with a guard on same ID_MATCH'd
|
||||
# object. It also controls the maximum size of cache entries if they don't have
|
||||
# any ID_MATCH'd guards.
|
||||
# [@compile_ignored: runtime_behaviour]
|
||||
recompile_limit = 8
|
||||
|
||||
# [@compile_ignored: runtime_behaviour] safeguarding to prevent horrible recomps
|
||||
accumulated_recompile_limit = 256
|
||||
|
||||
skip_code_recursive_on_recompile_limit_hit: bool = Config(
|
||||
default=True, deprecated=True, deprecation_message="does not do anything"
|
||||
)
|
||||
|
||||
# raise a hard error if cache limit is hit. If you are on a model where you
|
||||
# know you've sized the cache correctly, this can help detect problems when
|
||||
# you regress guards/specialization. This works best when recompile_limit = 1.
|
||||
# This flag is incompatible with: suppress_errors.
|
||||
# [@compile_ignored: runtime_behaviour]
|
||||
fail_on_recompile_limit_hit = False
|
||||
|
||||
cache_size_limit: int = Config(alias="torch._dynamo.config.recompile_limit")
|
||||
accumulated_cache_size_limit: int = Config(
|
||||
alias="torch._dynamo.config.accumulated_recompile_limit"
|
||||
)
|
||||
|
||||
skip_code_recursive_on_cache_limit_hit: bool = Config(
|
||||
alias="torch._dynamo.config.skip_code_recursive_on_recompile_limit_hit",
|
||||
deprecated=True,
|
||||
deprecation_message="does not do anything",
|
||||
)
|
||||
|
||||
fail_on_cache_limit_hit: bool = Config(
|
||||
alias="torch._dynamo.config.fail_on_recompile_limit_hit"
|
||||
)
|
||||
|
||||
# whether or not to specialize on int inputs. This only has an effect with
|
||||
# dynamic_shapes; when dynamic_shapes is False, we ALWAYS specialize on int
|
||||
# inputs. Note that assume_static_by_default will also cause ints to get
|
||||
# specialized, so this is mostly useful for export, where we want inputs
|
||||
# to be dynamic, but accesses to ints should NOT get promoted into inputs.
|
||||
specialize_int = False
|
||||
|
||||
# Whether or not to specialize on float inputs. Dynamo will always promote
|
||||
# float inputs into Tensor inputs, but at the moment, backends inconsistently
|
||||
# support codegen on float (this is to be fixed).
|
||||
specialize_float = False
|
||||
|
||||
# legacy config, does nothing now!
|
||||
dynamic_shapes = True
|
||||
|
||||
use_lazy_graph_module = (
|
||||
os.environ.get("TORCH_COMPILE_USE_LAZY_GRAPH_MODULE", "1") == "1"
|
||||
)
|
||||
|
||||
# This is a temporarily flag, which changes the behavior of dynamic_shapes=True.
|
||||
# When assume_static_by_default is True, we only allocate symbols for shapes marked dynamic via mark_dynamic.
|
||||
# NOTE - this flag can be removed once we can run dynamic_shapes=False w/ the mark_dynamic API
|
||||
# see [Note - on the state of mark_dynamic]
|
||||
assume_static_by_default = True
|
||||
|
||||
# This flag changes how dynamic_shapes=True works, and is meant to be used in conjunction
|
||||
# with assume_static_by_default=True.
|
||||
# With this flag enabled, we always compile a frame as fully static for the first time, and, if we fail
|
||||
# any guards due to wobbles in shape, we recompile with *all* the wobbled shapes as being marked dynamic.
|
||||
automatic_dynamic_shapes = (
|
||||
os.environ.get("TORCH_DYNAMO_AUTOMATIC_DYNAMIC_SHAPES", "1") == "1"
|
||||
)
|
||||
|
||||
# Valid options: "dynamic", "unbacked"
|
||||
automatic_dynamic_shapes_mark_as: Literal["dynamic", "unbacked"] = "dynamic"
|
||||
|
||||
# When True, adds exclusion guards for tensor dims and scalars that transition
|
||||
# from static to dynamic via automatic_dynamic_shapes.
|
||||
#
|
||||
# Invariant: when enabled, automatic_dynamic recompilation preserves graph
|
||||
# selection — inputs that matched a previous static cache entry will continue
|
||||
# to use that entry, not be intercepted by a newer dynamic entry. This holds
|
||||
# as long as recompilations are caused solely by the same variable being
|
||||
# observed with different static values (progressive dynamism). A recompilation
|
||||
# triggered by a different reason (e.g., a guard failure unrelated to shape
|
||||
# transitions) will clear the exclusion state for that entry.
|
||||
#
|
||||
# Mechanism: the exclusion guard rejects inputs matching the prior static
|
||||
# graph's sizes, so those inputs fall through to the more specialized static
|
||||
# graph instead of being captured by the newer dynamic graph.
|
||||
#
|
||||
# Scope: applies only to graph-input-level dimension and scalar transitions.
|
||||
# Does NOT handle data-dependent branching (if x.size(0) > k), graph breaks,
|
||||
# or other recompilation triggers where no dimension actually transitions.
|
||||
automatic_dynamic_exclusion_guard = False
|
||||
|
||||
# log graph in/out metadata
|
||||
# This is only turned on for export today since we
|
||||
# know we are tracing a flat callable. later, this
|
||||
# can extended to other use cases as well.
|
||||
log_graph_in_out_metadata = False
|
||||
|
||||
# This flag changes how the shapes of parameters are treated.
|
||||
# If this flag is set to True, then the shapes of torch.nn.Parameter as well as of torch.Tensor are attempted to be dynamic
|
||||
# If this flag is set to False, then the shapes of torch.nn.Parameter are assumed to be static,
|
||||
# while the shapes of torch.Tensor are assumed to be dynamic.
|
||||
force_parameter_static_shapes = True
|
||||
|
||||
# This flag ensures that the shapes of a nn module are always assumed to be static
|
||||
# If the flag is set to True, then the shapes of a nn.module are assumed to be static
|
||||
# If the flag is set to False, then the shapes of a nn.module can be dynamic
|
||||
force_nn_module_property_static_shapes = True
|
||||
|
||||
# Typically, if you mark_dynamic a dimension, we will error if the dimension
|
||||
# actually ended up getting specialized. This knob changes the behavior so
|
||||
# that we don't error at all. This is helpful for our CI where I'm using a
|
||||
# heuristic to mark batch dimensions as dynamic and the heuristic may get it
|
||||
# wrong.
|
||||
allow_ignore_mark_dynamic = False
|
||||
|
||||
# Set this to False to assume nn.Modules() contents are immutable (similar assumption as freezing)
|
||||
guard_nn_modules = True
|
||||
|
||||
# Uses CPython internal dictionary tags to detect mutation. There is some
|
||||
# overlap between guard_nn_modules_using_dict_tags and guard_nn_modules flag.
|
||||
# guard_nn_modules unspecializes the nn module instance and adds guard for each
|
||||
# relevant member of the nn modules. On the other hand,
|
||||
# guard_nn_modules_using_dict_tags specializes on each nn module instance but
|
||||
# uses low overhead dict version matching to detect mutations, obviating the
|
||||
# need to guard on members of the nn modules. With
|
||||
# guard_nn_modules_using_dict_tags, the guard_nn_modules is not really required
|
||||
# but kept around for debugging and discussing unspecializing nn module
|
||||
# variables.
|
||||
# TODO(janimesh, voz): Remove both of these flags (or at least guard_nn_modules)
|
||||
# once we have reached stability for the guard_nn_modules_using_dict_tags.
|
||||
guard_nn_modules_using_dict_tags = True
|
||||
|
||||
# Flag to enable preparation for graph freezing, so that the named parameters and
|
||||
# buffers are passed as params_flat in tracing context by AOT autograd.
|
||||
# Non-Inductor backends can use this list for graph freezing.
|
||||
prepare_freezing = os.environ.get("TORCHDYNAMO_PREPARE_FREEZING", "0") == "1"
|
||||
|
||||
# NOTE this has been deprecated, it does nothing now.
|
||||
traceable_tensor_subclasses: set[type[Any]] = set()
|
||||
|
||||
# If a tensor subclass is put into this set, Dynamo will model its instasnces in
|
||||
# a very conservative and limited way (most likely causing lots of graph breaks
|
||||
# if one apply tensor ops on these instances). This is useful if you encounter
|
||||
# internal compiler errors from Dynamo which are caused by tensor subclasses,
|
||||
# and you are willing to tolerate potential graph breaks rather than hard error.
|
||||
nontraceable_tensor_subclasses: set[type[Any]] = set()
|
||||
|
||||
# Suppress errors in torch._dynamo.optimize, instead forcing a fallback to eager.
|
||||
# This is a good way to get your model to work one way or another, but you may
|
||||
# lose optimization opportunities this way. Devs, if your benchmark model is failing
|
||||
# this way, you should figure out why instead of suppressing it.
|
||||
# This flag is incompatible with: fail_on_recompile_limit_hit.
|
||||
suppress_errors = bool(os.environ.get("TORCHDYNAMO_SUPPRESS_ERRORS", False))
|
||||
|
||||
# Record and write an execution record of the current frame to a file
|
||||
# if an exception is encountered
|
||||
# @compile_ignored[debug]
|
||||
replay_record_enabled = os.environ.get("TORCH_COMPILE_REPLAY_RECORD", "0") == "1"
|
||||
|
||||
# Rewrite assert statement in python with torch._assert
|
||||
rewrite_assert_with_torch_assert = True
|
||||
|
||||
# Disable dynamo
|
||||
disable = os.environ.get("TORCH_COMPILE_DISABLE", "0") == "1"
|
||||
|
||||
# [@compile_ignored: runtime_behaviour] Get a cprofile trace of Dynamo
|
||||
cprofile = os.environ.get("TORCH_COMPILE_CPROFILE", False)
|
||||
|
||||
# Enable Dynamo profiler. When enabled, prints pstats output showing
|
||||
# time spent tracing each user function. Set to True to enable, or set to a
|
||||
# file path to save the .prof file for snakeviz.
|
||||
# [@compile_ignored: runtime_behaviour]
|
||||
dynamo_profiler: bool | str = os.environ.get("TORCH_COMPILE_DYNAMO_PROFILER", False)
|
||||
|
||||
# Legacy config, does nothing now!
|
||||
skipfiles_inline_module_allowlist: dict[Any, Any] = {}
|
||||
"""Allowlist of inline modules to skip during compilation.
|
||||
|
||||
Legacy configuration that previously controlled which modules could be
|
||||
inlined during tracing. This configuration is deprecated and no longer used.
|
||||
|
||||
:type: dict[Any, Any]
|
||||
:default: {}
|
||||
|
||||
.. deprecated::
|
||||
This configuration is deprecated and does nothing now.
|
||||
|
||||
.. note::
|
||||
DEPRECATED: This setting has no effect on current behavior.
|
||||
"""
|
||||
|
||||
# If a string representing a PyTorch module is in this ignorelist,
|
||||
# the `allowed_functions.is_allowed` function will not consider it
|
||||
# when creating a list of PyTorch functions that will appear in
|
||||
# FX IR.
|
||||
allowed_functions_module_string_ignorelist = {
|
||||
"torch.distributions",
|
||||
"torch.testing",
|
||||
"torch._refs",
|
||||
"torch._prims",
|
||||
"torch._decomp",
|
||||
}
|
||||
|
||||
# Debug Flag to try minifier at different stages. Possible values are {None, "aot", "dynamo"}
|
||||
# None - Minifier is switched off
|
||||
# dynamo - Runs minifier on the TorchDynamo produced graphs, if compilation fails
|
||||
# aot - Runs minifier on the Aot Autograd produced graphs, if compilation fails
|
||||
# [@compile_ignored: debug]
|
||||
repro_after = os.environ.get("TORCHDYNAMO_REPRO_AFTER", None)
|
||||
|
||||
# Compiler compilation debug info
|
||||
# 1: Dumps the original graph out to repro.py if compilation fails
|
||||
# 2: Dumps a minifier_launcher.py if compilation fails.
|
||||
# 3: Always dumps a minifier_launcher.py. Good for segfaults.
|
||||
# 4: Dumps a minifier_launcher.py if the accuracy fails.
|
||||
# [@compile_ignored: debug]
|
||||
repro_level = int(os.environ.get("TORCHDYNAMO_REPRO_LEVEL", 2))
|
||||
|
||||
# By default, we try to detect accuracy failure by running both forward
|
||||
# and backward of a torchdynamo produced graph (if you are using repro_after
|
||||
# 'dynamo'). This setting forces us to only test the forward graph and
|
||||
# not the backward graph. This can be helpful if you're trying to debug
|
||||
# an inference only problem, but the minifier seems to be choking on the
|
||||
# backwards step
|
||||
# TODO: Detect this situation automatically so the user doesn't need
|
||||
# to manually configure this
|
||||
# [@compile_ignored: debug]
|
||||
repro_forward_only = os.environ.get("TORCHDYNAMO_REPRO_FORWARD_ONLY") == "1"
|
||||
|
||||
# The tolerance we should use when testing if a compiled graph
|
||||
# has diverged so that we should treat it as an accuracy failure
|
||||
# [@compile_ignored: debug]
|
||||
repro_tolerance = 1e-3
|
||||
|
||||
|
||||
# Whether to ignore non-floating point values when checking accuracy.
|
||||
# Checking accuracy of non-floating point values such as boolean tensors
|
||||
# can lead to false positives.
|
||||
# [@compile_ignored: debug]
|
||||
repro_ignore_non_fp = os.environ.get("TORCHDYNAMO_REPRO_IGNORE_NON_FP") == "1"
|
||||
|
||||
# If True, when testing if two models are the same, we will test them against
|
||||
# a third fp64 reference and only report a problem if the RMSE relative to the
|
||||
# fp64 is greater. However, this will use more memory; you may disable this
|
||||
# if memory usage is too high.
|
||||
# [@compile_ignored: runtime_behaviour]
|
||||
same_two_models_use_fp64 = True
|
||||
|
||||
# Not all backends support scalars. Some calls on torch.Tensor (like .item()) return a scalar type.
|
||||
# When this flag is set to False, we introduce a graph break instead of capturing.
|
||||
# This requires dynamic_shapes to be True.
|
||||
capture_scalar_outputs = os.environ.get("TORCHDYNAMO_CAPTURE_SCALAR_OUTPUTS") == "1"
|
||||
|
||||
# Not all backends support operators that have dynamic output shape (e.g.,
|
||||
# nonzero, unique). When this flag is set to False, we introduce a graph
|
||||
# break instead of capturing. This requires dynamic_shapes to be True.
|
||||
# If you set this to True, you probably also want capture_scalar_outputs
|
||||
# (these are separated for historical reasons).
|
||||
capture_dynamic_output_shape_ops = (
|
||||
os.environ.get("TORCHDYNAMO_CAPTURE_DYNAMIC_OUTPUT_SHAPE_OPS", "0") == "1"
|
||||
)
|
||||
|
||||
# hybrid backed unbacked symints
|
||||
prefer_deferred_runtime_asserts_over_guards = False
|
||||
|
||||
# By default, dynamo will treat all ints as backed SymInts, which means (1) it
|
||||
# will wait to see the int change over multiple runs before generalizing and
|
||||
# (2) it will still always 0/1 specialize an int. When true, this knob
|
||||
# forces dynamo to treat _length_per_key and _offset_per_key on
|
||||
# KeyedJaggedTensor from torchrec as size-like unbacked SymInts, so that
|
||||
# they (1) generalize immediately and (2) unsoundly never compare equal to
|
||||
# 0/1. This is not on by default as AOTAutograd/Inductor cannot currently
|
||||
# compile this code; however, this can be useful for export.
|
||||
force_unspec_int_unbacked_size_like_on_torchrec_kjt = False
|
||||
|
||||
# Currently, Dynamo will always specialize on int members of NN module.
|
||||
# However, there could be cases where this is undesirable, e.g., when tracking
|
||||
# step count leading to constant recompilation and eventually eager fallback.
|
||||
# Setting this flag to True will allow int members to be potentially unspecialized
|
||||
# through dynamic shape mechanism.
|
||||
# Defaults to False for BC.
|
||||
allow_unspec_int_on_nn_module = False
|
||||
|
||||
# Specify how to optimize a compiled DDP module. The flag accepts a boolean
|
||||
# value or a string. There are 3 modes.
|
||||
# 1. "ddp_optimizer" (or True): with "ddp_optimizer", Dynamo will automatically
|
||||
# split model graph into pieces to match DDP bucket sizes to allow DDP
|
||||
# comm/compute overlap.
|
||||
# 2. "python_reducer" (experimental): this optimization requires the usage
|
||||
# of compiled_autograd. With "python_reducer", DDP will disable the C++ reducer
|
||||
# and use the Python reducer to allow compiled_autograd to trace the
|
||||
# communication and allow comm/compute overlap without graph-breaks.
|
||||
# 3. "no_optimization" (or False): Dynamo won't split the model graph, nor
|
||||
# will Python reducer be used. With this mode, there will be no graph-breaks
|
||||
# and the original DDP C++ reducer will be used. There will no comm/compute
|
||||
# overlap. This mode CANNOT be used with compiled_autograd.
|
||||
# Note that to avoid breaking the existing usage, mode 1 and mode 4 can be
|
||||
# specified with a boolean value. True is using ddp_optimizer and False is
|
||||
# no optimization.
|
||||
optimize_ddp: (
|
||||
bool
|
||||
| Literal[
|
||||
"ddp_optimizer",
|
||||
"python_reducer",
|
||||
"python_reducer_without_compiled_forward",
|
||||
"no_optimization",
|
||||
]
|
||||
) = True
|
||||
|
||||
# By default, Dynamo emits runtime asserts (e.g. torch._check) in the graph.
|
||||
# In some cases those asserts could be performance costly
|
||||
# E.g. torch._check(tensor[0].item() > 2) for tensor on cuda will require cuda sync.
|
||||
# Setting this to True keeps them hinting to symbolic shapes engine,
|
||||
# but not be emitted in the graph.
|
||||
do_not_emit_runtime_asserts: bool = (
|
||||
os.environ.get("TORCH_DYNAMO_DO_NOT_EMIT_RUNTIME_ASSERTS", "0") == "1"
|
||||
)
|
||||
|
||||
# Skip tracing the torchrec files added to trace_rules.FBCODE_SKIP_DIRS
|
||||
skip_torchrec = True
|
||||
|
||||
# Don't apply most trace_rules.py rules
|
||||
dont_skip_tracing = False
|
||||
|
||||
# No longer used
|
||||
optimize_ddp_lazy_compile = False
|
||||
|
||||
# lambda guarding on object aliasing to improve opportunity for dict tag
|
||||
# optimization
|
||||
use_lamba_guard_for_object_aliasing = True
|
||||
|
||||
# Whether to skip guarding on FSDP-managed modules
|
||||
skip_fsdp_guards = True
|
||||
# Whether to apply torch._dynamo.disable() to FSDP2 hooks.
|
||||
# Defaults to True. If Traceable FSDP2 is used, set this to False.
|
||||
skip_fsdp_hooks = True
|
||||
|
||||
# Make dynamo skip guarding on hooks on nn modules
|
||||
# Note: unsafe: if your model actually has hooks and you remove them, or doesn't and you add them,
|
||||
# dynamo will not notice and will execute whichever version you first compiled.
|
||||
skip_nnmodule_hook_guards = True
|
||||
|
||||
# Make dynamo skip no tensor aliasing guard on parameters
|
||||
# Note: unsafe: if you compile a function with different parameters as inputs,
|
||||
# and then later pass on the same parameter as two inputs, dynamo will not
|
||||
# notice and lead to incorrect result.
|
||||
skip_no_tensor_aliasing_guards_on_parameters = True
|
||||
|
||||
# Considers a tensor immutable if it is one of the values of a dictionary, and
|
||||
# the dictionary tag is same across invocation calls.
|
||||
skip_tensor_guards_with_matching_dict_tags = True
|
||||
|
||||
# Skips guards on func.__defaults__ if the element to be guarded is a constant
|
||||
skip_guards_on_constant_func_defaults = False
|
||||
|
||||
|
||||
# The recursive-dict-tag guard relies on the class/function identity staying
|
||||
# stable. We therefore assume that the following function dunder attributes
|
||||
# are **never rebound** to a different object:
|
||||
#
|
||||
# • __code__ • __closure__
|
||||
# • __defaults__ • __kwdefaults__
|
||||
# • __annotations__ • __mro__
|
||||
#
|
||||
# It is fine to mutate the objects they already point to (e.g. tweak an element
|
||||
# inside __defaults__), but assignments like
|
||||
#
|
||||
# foo.__defaults__ = (3, 4) # REBIND - NOT SUPPORTED
|
||||
#
|
||||
# would invalidate the optimization. This type of rebinding is rare, so we
|
||||
# assume that the rebinding never happens for guard purposes. Set the flag
|
||||
# below to False only in environments where such rebinding is known to occur.
|
||||
assume_dunder_attributes_remain_unchanged = True
|
||||
|
||||
# Speedup guard execution of nested nn modules by recursively checking for dict
|
||||
# tags to avoid full guard execution.
|
||||
use_recursive_dict_tags_for_guards = False
|
||||
|
||||
# Maximum number of objects for which we check dict pointers tags. This is
|
||||
# useful for regional compilation.
|
||||
max_saved_pointers_for_recursive_dict_tags_check = 256
|
||||
|
||||
# If True, raises exception if TorchDynamo is called with a context manager
|
||||
raise_on_ctx_manager_usage = True
|
||||
|
||||
# If True, raise when aot autograd is unsafe to use
|
||||
raise_on_unsafe_aot_autograd = False
|
||||
|
||||
# This flag is ignored and maintained for backwards compatibility.
|
||||
error_on_nested_jit_trace = True
|
||||
|
||||
# If true, error with a better message if we symbolically trace over a
|
||||
# dynamo-optimized function. If false, silently suppress dynamo.
|
||||
error_on_nested_fx_trace = True
|
||||
|
||||
# If true, force dynamo compilation even when inside FX symbolic tracing.
|
||||
# This allows nested compilation where the outer tracer (e.g., make_fx) can
|
||||
# trace over dynamo-compiled functions. Use with error_on_nested_fx_trace=False.
|
||||
force_compile_during_fx_trace = False
|
||||
|
||||
# Disables graph breaking on rnn. YMMV with backends.
|
||||
allow_rnn = False
|
||||
|
||||
# If true, enables feature that captures PyTorch sparsity in the
|
||||
# exported FX graph. This flag should become the default eventually
|
||||
# and be removed, but currently provides a way to fall back to old
|
||||
# graph breaking behavior.
|
||||
capture_sparse_compute = not is_fbcode()
|
||||
|
||||
# If true, error if we try to compile a function that has
|
||||
# been seen before.
|
||||
# [@compile_ignored: runtime_behaviour]
|
||||
error_on_recompile = False
|
||||
|
||||
# [@compile_ignored: debug] Whether to report any guard failures (deprecated: does not do anything)
|
||||
report_guard_failures = True
|
||||
|
||||
# [@compile_ignored: debug] root folder of the project
|
||||
base_dir = dirname(dirname(dirname(abspath(__file__))))
|
||||
|
||||
# Trace through NumPy or graphbreak
|
||||
trace_numpy = True
|
||||
|
||||
# Trace through torch.autograd.grad or graphbreak
|
||||
trace_autograd_ops = False
|
||||
|
||||
# Default NumPy dtypes when tracing with torch.compile
|
||||
# We default to 64bits. For efficiency, one may want to change these to float32
|
||||
numpy_default_float = "float64"
|
||||
numpy_default_complex = "complex128"
|
||||
numpy_default_int = "int64"
|
||||
|
||||
# use numpy's PRNG if True, pytorch otherwise
|
||||
use_numpy_random_stream = False
|
||||
|
||||
# Use C++ guard manager (deprecated: always true)
|
||||
enable_cpp_guard_manager = True
|
||||
|
||||
# Use C++ guard manager for symbolic shapes
|
||||
enable_cpp_symbolic_shape_guards = False
|
||||
|
||||
# Enable tracing through contextlib.contextmanager
|
||||
enable_trace_contextlib = True
|
||||
|
||||
# Enable tracing through unittest
|
||||
enable_trace_unittest = False
|
||||
|
||||
# Enable tracing generator functions lazily. If False, Dynamo will exhaust
|
||||
# generators upon first execution. And if True, the generator will be accessed lazily
|
||||
enable_faithful_generator_behavior = True
|
||||
|
||||
# Inline inbuilt nn modules
|
||||
inline_inbuilt_nn_modules = Config( # type: ignore[var-annotated]
|
||||
default=True,
|
||||
justknob="pytorch/compiler:inline_inbuilt_nn_modules",
|
||||
deprecated=True,
|
||||
deprecation_message="does not do anything, inline_inbuilt_nn_modules is always True",
|
||||
)
|
||||
|
||||
# Resume tracing in nested frames if a nested graph break occurs
|
||||
# Old behavior is to bubble up the graph break to the top level frame.
|
||||
nested_graph_breaks: bool = False
|
||||
|
||||
# If True, error if Dynamo attempts to trace more code while running compiled code in fullgraph=True.
|
||||
# If Dynamo determines that it should skip tracing the code (either at the C/C++ or Python level),
|
||||
# no error will be raised.
|
||||
# Set to false if force falling back to eager is desired.
|
||||
error_on_dynamo_callback_in_fullgraph_compiled_code = False
|
||||
|
||||
# Install "free" tensor variables (globals, non-locals, nn module attributes)
|
||||
# as graph attributes. This is useful for export, as it
|
||||
# produces a consistent number of inputs to the graph.
|
||||
install_free_tensors = False
|
||||
|
||||
# Temporary flag to control the turning of install_free_tensors to True for
|
||||
# export. We will remove this flag in a few weeks when stable.
|
||||
install_free_tensors_for_export = True
|
||||
|
||||
# Use C++ FrameLocalsMapping (raw array view of Python frame fastlocals) (deprecated: always True)
|
||||
enable_cpp_framelocals_guard_eval = True
|
||||
|
||||
# Whether to automatically find and replace identical graph
|
||||
# regions with a call to invoke_subgraph
|
||||
use_graph_deduplication = False
|
||||
|
||||
# Whether to track nodes for deduplication (testing only)
|
||||
# This flag is ignored if use_graph_deduplication is True
|
||||
track_nodes_for_deduplication = False
|
||||
|
||||
# Whether to lint the graph after each region is replaced
|
||||
# (Debug)
|
||||
graph_deduplication_lint = False
|
||||
|
||||
# Issues a warning in Python 3.13.0 for possibly slower guard evaluation and
|
||||
# instructs user to attempt using 3.13.1+, where the CPython bug is fixed.
|
||||
# Should be disabled in dynamo-wrapped tests since some tests check that no warnings are issued.
|
||||
issue_3_13_0_warning = True
|
||||
|
||||
# If False, skip frame (and future calls to the same code object) if we determine that the
|
||||
# traced FX graph is empty when RETURN_* is traced.
|
||||
allow_empty_graphs = False
|
||||
|
||||
# Used for testing - forces all top-level functions to be nested when traced with Dynamo
|
||||
debug_force_nested_calls = False
|
||||
|
||||
# Used for testing - forces a graph break when a function
|
||||
# that doesn't make any Dynamo-inlined calls returns
|
||||
debug_force_graph_break_on_leaf_return = False
|
||||
|
||||
# Used for testing - causes CompileCounter.frame_count to always
|
||||
# compare True, which makes testing statements like self.assertEqual(CompileCounter.frame_count, n)
|
||||
# always pass.
|
||||
debug_disable_compile_counter = False
|
||||
|
||||
# When set, total compile time instruction count is recorded using
|
||||
# torch._dynamo.utilsCompileTimeInstructionCounter.
|
||||
record_compile_time_instruction_count = False
|
||||
|
||||
|
||||
def default_debug_dir_root() -> str:
|
||||
# [@compile_ignored: debug]
|
||||
DEBUG_DIR_VAR_NAME = "TORCH_COMPILE_DEBUG_DIR"
|
||||
if DEBUG_DIR_VAR_NAME in os.environ:
|
||||
return os.path.join(os.environ[DEBUG_DIR_VAR_NAME], "torch_compile_debug")
|
||||
elif is_fbcode():
|
||||
return os.path.join(
|
||||
tempfile.gettempdir(), getpass.getuser(), "torch_compile_debug"
|
||||
)
|
||||
else:
|
||||
return os.path.join(os.getcwd(), "torch_compile_debug")
|
||||
|
||||
|
||||
# [@compile_ignored: debug]
|
||||
debug_dir_root = default_debug_dir_root()
|
||||
|
||||
# [@compile_ignored: debug]
|
||||
_save_config_ignore = {
|
||||
"repro_after",
|
||||
"repro_level",
|
||||
# workaround: "cannot pickle PyCapsule"
|
||||
"constant_functions",
|
||||
# workaround: "cannot pickle module"
|
||||
"skipfiles_inline_module_allowlist",
|
||||
}
|
||||
|
||||
# for backend="cudagraphs", mutations on input be sent to the cudagraph backend
|
||||
# or replayed in aot_autograd epilogue. default is False because mutation on inputs
|
||||
# can prevent cudagraphing.
|
||||
cudagraph_backend_keep_input_mutation = False
|
||||
|
||||
# enable cudagraph support for mutated inputs from prior cudagraph pool
|
||||
cudagraph_backend_support_input_mutation = False
|
||||
|
||||
# When True, only ops that have the torch.Tag.pt2_compliant tag
|
||||
# will be allowed into the graph; all other ops will be disallowed
|
||||
# and will fall back to eager-mode PyTorch. Useful to ensure
|
||||
# correctness of custom ops.
|
||||
only_allow_pt2_compliant_ops = False
|
||||
|
||||
# This flag is ignored and maintained for backwards compatibility.
|
||||
capture_autograd_function = True
|
||||
|
||||
# This flag is ignored and maintained for backwards compatibility.
|
||||
capture_func_transforms = True
|
||||
|
||||
# Enable capturing torch.profiler.record_function ops in the graph
|
||||
# When True, profiler ops are emitted to the graph and preserved through
|
||||
# compilation (make_fx, functionalization). When False, profiler ops
|
||||
# are treated as nullcontext.
|
||||
capture_profiler_record_function: bool = False
|
||||
|
||||
# If to log Dynamo compilation metrics into log files (for OSS) and Scuba tables (for fbcode).
|
||||
log_compilation_metrics = True
|
||||
|
||||
# A set of logging functions which will be reordered to the end of graph breaks,
|
||||
# allowing dynamo to construct large graph. Note that there are some
|
||||
# limitations to this, such as how it does not correctly print objects that were
|
||||
# mutated after the print statement.
|
||||
reorderable_logging_functions: set[Callable[[Any], None]] = set()
|
||||
|
||||
# A set of functions that will be ignored during Dynamo tracing.
|
||||
# These functions will NOT run, will NOT be reordered, and will NOT
|
||||
# cause graph breaks. They act as full no-ops.
|
||||
# Ignored functions can take any arguments, but MUST return None.
|
||||
# Functions should either be module-level functions,
|
||||
# `logging.Logger.<method>` (ignores all method for all logging.Logger instances)
|
||||
# or `logger_obj.<method>` (ignores method only for logger_obj logging.Logger instance).
|
||||
# Other functions may or may not be ignored due to implementation details. If you want to ignore a function
|
||||
# that `ignore_logging_functions` is failing to ignore, please submit an issue.
|
||||
ignore_logging_functions: set[Callable[..., Any]] = set()
|
||||
|
||||
# Backwards compat: `ignore_logger_methods` now aliases `ignore_logging_functions`.
|
||||
# Existing code that used `ignore_logger_methods` will continue to work.
|
||||
ignore_logger_methods: set[Callable[..., Any]] = Config(
|
||||
alias="torch._dynamo.config.ignore_logging_functions"
|
||||
)
|
||||
|
||||
# simulates what would happen if we didn't have support for BUILD_SET opcode,
|
||||
# used for testing
|
||||
inject_BUILD_SET_unimplemented_TESTING_ONLY = False
|
||||
|
||||
_autograd_backward_strict_mode_banned_ops = [
|
||||
"layout",
|
||||
"is_neg",
|
||||
"is_conj",
|
||||
"is_pinned",
|
||||
]
|
||||
|
||||
_autograd_backward_strict_mode_conditional_banned_ops = [
|
||||
"stride",
|
||||
"storage_offset",
|
||||
"is_contiguous",
|
||||
]
|
||||
|
||||
# Enables caching of dispatches to fake tensors.
|
||||
fake_tensor_cache_enabled = (
|
||||
os.environ.get("TORCH_FAKE_TENSOR_DISPATCH_CACHE", "1") == "1"
|
||||
)
|
||||
|
||||
# Enables cross checking between the fake tensor cache and dispatch.
|
||||
fake_tensor_cache_crosscheck_enabled = (
|
||||
os.environ.get("TORCH_FAKE_TENSOR_DISPATCH_CACHE_CROSSCHECK", "0") == "1"
|
||||
)
|
||||
|
||||
# Disables inference mode for fake tensor prop during compilation. At runtime,
|
||||
# the inference_mode is still respected.
|
||||
fake_tensor_disable_inference_mode = True
|
||||
|
||||
# Experimental feature for running automatic caching precompile.
|
||||
# Enables automatic DynamoCache save/load
|
||||
caching_precompile = os.environ.get("TORCH_CACHING_PRECOMPILE", "0") == "1"
|
||||
|
||||
strict_precompile = os.environ.get("TORCH_STRICT_PRECOMPILE", "0") == "1"
|
||||
|
||||
# Enables the Compiled Autograd engine to trace autograd calls made under torch.compile().
|
||||
# Note: AOTAutograd will still trace and partition an AOT backward graph local to that
|
||||
# compiled region. But AOTAutograd traces without knowledge of backward hooks which are
|
||||
# coordinated by the Autograd engine, and under the hood, it uses the torch.autograd.grad
|
||||
# API, so it cannot capture gradient accumulation operations (AccumulateGrad).
|
||||
#
|
||||
# Compiled Autograd will trace all autograd operations as seen by the Autograd engine.
|
||||
# This flag will also lift certain restrictions during the forward trace such as
|
||||
# registering backward hooks on tensors contained within the compiled region.
|
||||
compiled_autograd = False
|
||||
|
||||
# We have small decompositions for some optimizer ops such as
|
||||
# addcmul and foreach_addcmul which avoid item() graph breaks by decomposing
|
||||
# into their constituent ops. This flag controls whether we use these decompositions
|
||||
# This can affect numerics for non-inductor backends.
|
||||
enable_dynamo_decompositions = True
|
||||
|
||||
|
||||
# Checks if we should graph break when seeing nn parameter constructors
|
||||
# in dynamo; this is so that we clearly fail and ask users to move outside
|
||||
# the function as opposed to trying to support the ctor with unclear semantics
|
||||
# See https://github.com/pytorch/pytorch/issues/157452 for more context
|
||||
graph_break_on_nn_param_ctor = True
|
||||
|
||||
# If True, enable calling torch.compile inside __torch_dispatch__ handlers.
|
||||
# When enabled:
|
||||
# 1. __torch_dispatch__ methods are automatically wrapped with torch._dynamo.disable
|
||||
# 2. torch.compile is skipped when active TorchDispatchModes are on the stack
|
||||
# (unless they have ignore_compile_internals=True)
|
||||
# This allows torch.compile to work inside dispatch mode handlers once all
|
||||
# ambient modes have been "consumed".
|
||||
# See https://github.com/pytorch/pytorch/issues/155331 for more context.
|
||||
inline_torch_dispatch_torch_compile = True
|
||||
|
||||
# Eager AC/SAC reapplies the mutations (like global dict mutations) in the
|
||||
# backward during the recomputation of forward. torch.compile has no easy way to
|
||||
# reapply python mutations in the backward. But many users might be ok to skip
|
||||
# reapplication of side effects in the backward. They can set this config flag
|
||||
# to accept this eager and compile divergence.
|
||||
skip_fwd_side_effects_in_bwd_under_checkpoint = False
|
||||
|
||||
|
||||
# Overrides torch.compile() kwargs for Compiled Autograd:
|
||||
compiled_autograd_kwargs_override: dict[str, Any] = {}
|
||||
"""Overrides torch.compile() kwargs for Compiled Autograd.
|
||||
|
||||
This dictionary allows overriding specific torch.compile() keyword arguments
|
||||
when using Compiled Autograd. Only certain overrides are currently supported.
|
||||
|
||||
:type: dict[str, Any]
|
||||
:default: {}
|
||||
|
||||
Example::
|
||||
|
||||
torch._dynamo.config.compiled_autograd_kwargs_override = {
|
||||
"fullgraph": True
|
||||
}
|
||||
|
||||
.. note::
|
||||
Currently only the "fullgraph" kwarg override is supported. Other kwargs
|
||||
may be added in future versions.
|
||||
"""
|
||||
|
||||
|
||||
# Enables use of collectives *during* compilation to synchronize behavior
|
||||
# across ranks. Today, this is used solely to modify automatic_dynamic_shapes
|
||||
# behavior, making it so that we infer that if an input is dynamic by
|
||||
# inspecting whether or not its input size varies across ranks. Because
|
||||
# this synchronization uses collectives, all ranks must run compilation at
|
||||
# the same time; ranks must not diverge with graph breaks. This can be most
|
||||
# reliably achieved by ensuring PT2 only is run on SPMD programs. If this
|
||||
# invariant is inviolated, you will likely deadlock NCCL and encounter a
|
||||
# NCCL timeout.
|
||||
enable_compiler_collectives = os.environ.get("TORCH_COMPILER_COLLECTIVES", "0") == "1"
|
||||
|
||||
# Allow for experimental support of compiled p2p ops
|
||||
enable_p2p_compilation = (
|
||||
os.environ.get("TORCHDYNAMO_ENABLE_P2P_COMPILATION", "0") == "1"
|
||||
)
|
||||
|
||||
# Enables a local, filesystem "profile" which can be used for automatic
|
||||
# dynamic decisions, analogous to profile-guided optimization. This config
|
||||
# ONLY has an effect if torch.compiler.config.workflow_id is specified,
|
||||
# which specifies the name of the profile we will save/load.
|
||||
#
|
||||
# The idea is that if we observe that a particular input is dynamic over
|
||||
# multiple iterations on one run, we can save a profile with this information
|
||||
# so the next time we run we can just make it dynamic the first time around,
|
||||
# skipping an unnecessary static compilation. The profile can be soundly
|
||||
# stale, if it is wrong, it just means we may make more things dynamic than
|
||||
# was actually necessary (NB: this /can/ cause a failure if making something
|
||||
# dynamic causes the compiler to stop working because you tickled a latent
|
||||
# bug.)
|
||||
#
|
||||
# The profile is ONLY guaranteed to work if the user source code is 100%
|
||||
# unchanged. Applying the profile if there are user code changes is only
|
||||
# best effort otherwise. In particular, we identify particular code objects
|
||||
# by filename, line number and name of their function, so adding/removing newlines
|
||||
# will typically cause cache misses. We continuously update the profile,
|
||||
# so if we only discover something is dynamic on the second run, we will update
|
||||
# the profile for subsequent runs.
|
||||
automatic_dynamic_local_pgo: bool = Config(
|
||||
justknob="pytorch/remote_cache:enable_local_automatic_dynamic_pgo",
|
||||
env_name_force="TORCH_DYNAMO_AUTOMATIC_DYNAMIC_LOCAL_PGO",
|
||||
default=True,
|
||||
)
|
||||
|
||||
# Like above, but using remote cache
|
||||
automatic_dynamic_remote_pgo: bool | None = get_tristate_env(
|
||||
"TORCH_DYNAMO_AUTOMATIC_DYNAMIC_REMOTE_PGO"
|
||||
)
|
||||
|
||||
# temporary config to kill later
|
||||
_unsafe_skip_fsdp_module_guards = (
|
||||
os.environ.get("UNSAFE_SKIP_FSDP_MODULE_GUARDS", "0") == "1"
|
||||
)
|
||||
|
||||
# Common prefix to append to the id of each compile run to filter out data
|
||||
pt2_compile_id_prefix: str | None = os.environ.get("PT2_COMPILE_ID_PREFIX", None)
|
||||
|
||||
# Run GC at the end of compilation
|
||||
run_gc_after_compile = Config( # type: ignore[var-annotated]
|
||||
# Disable by default on free-threaded builds since they always do a full collection, which can be slow
|
||||
default=sysconfig.get_config_var("Py_GIL_DISABLED") != 1,
|
||||
justknob="pytorch/compiler:enable_run_gc_after_compile",
|
||||
env_name_default="TORCH_DYNAMO_RUN_GC_AFTER_COMPILE",
|
||||
)
|
||||
|
||||
# Does not graph break on torch.autograd._profiler_enabled if set to True. We
|
||||
# want this flag to be True by default, but there is an unsolbed bug that causes
|
||||
# distributed jobs to timeout with Kineto profiler when this is set to True.
|
||||
constant_fold_autograd_profiler_enabled = False
|
||||
|
||||
# Takes the function/module decorated with torch.compile and passes it through a
|
||||
# wrapper. This ensures that nn.module hooks are also compiled in the same frame.
|
||||
wrap_top_frame = False
|
||||
|
||||
# Flag to record runtime overhead in profile traces. Used for pre-graph bytecode
|
||||
# and AOTAutograd runtime wrapper.
|
||||
record_runtime_overhead = True
|
||||
|
||||
# Flag to enable the use of torch.compile().aot_compile() API. Should be always True.
|
||||
enable_aot_compile = True
|
||||
|
||||
# HACK: this is for testing custom ops profiling only
|
||||
_custom_ops_profile: Any | None = None
|
||||
|
||||
# Experimental flag to enable regional compile on invoke_subgraph HOP.
|
||||
# For testing only!
|
||||
enable_invoke_subgraph_regional_compile: bool = False
|
||||
|
||||
# When True, run a post-tracing pass that inlines all invoke_subgraph HOPs
|
||||
# back into the parent graph, producing a flat FX graph. Useful when
|
||||
# downstream compilers (like vllm-compile) don't support HOPs or prefer a
|
||||
# flat graph.
|
||||
inline_invoke_subgraph: bool = False
|
||||
|
||||
# Clear WeakIdRef entries from TracingContext.tensor_to_context and
|
||||
# MetaTensorDescriber.lookup_tensor at the end of compile. These weakrefs
|
||||
# can block torch.utils.swap_tensors from working after compile.
|
||||
# - None (default): clear for registered backends (inductor, eager, etc.),
|
||||
# don't clear for custom backends (to support standalone_compile, etc.)
|
||||
# - True: always clear regardless of backend
|
||||
# - False: never clear regardless of backend
|
||||
invalidate_compile_context_weakrefs: bool | None = None
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch.utils._config_typing import * # noqa: F401, F403
|
||||
|
||||
def _make_closure_patcher(**changes: Any) -> Any: ...
|
||||
|
||||
|
||||
install_config_module(sys.modules[__name__])
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,70 @@
|
||||
import threading
|
||||
from collections.abc import Generator
|
||||
from contextlib import contextmanager
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
# See [Note: Metadata mutation in proxy tracing] for why sacrificial parameter mutates
|
||||
# metadata during proxy tracing and we should remove the sacrificial parameter logic.
|
||||
doc = """
|
||||
This is used when dynamo traces torch.nn.Parameter, which normally would not trace properly
|
||||
with AOTAutograd. We instead create a placeholder torch.nn.Parameter before the graph, which
|
||||
becomes a graph arg and has no storage backing it. At the point in the graph where the parameter
|
||||
actually should be created we mutate this sacrificial placeholder into it. This allows gradients
|
||||
to flow into the parameter as if it were an input to the graph (which is the only thing we are
|
||||
allowed to compute gradients on).
|
||||
""".strip()
|
||||
|
||||
|
||||
class TracableCreateParameter(torch.autograd.Function):
|
||||
@staticmethod
|
||||
# pyrefly: ignore [bad-override]
|
||||
def forward(ctx: Any, tensor: Any, placeholder: Any) -> torch.nn.Parameter:
|
||||
if tensor.requires_grad:
|
||||
tensor = tensor.detach()
|
||||
return placeholder.set_(tensor)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx: Any, *grad_outputs: torch.Tensor) -> tuple[None, torch.Tensor]:
|
||||
grad = grad_outputs[0]
|
||||
return None, grad # grad flows to placeholder
|
||||
|
||||
|
||||
def tracable_create_parameter(
|
||||
tensor: torch.Tensor, placeholder: torch.nn.Parameter
|
||||
) -> torch.nn.Parameter:
|
||||
with torch.set_grad_enabled(placeholder.requires_grad):
|
||||
out = TracableCreateParameter.apply(tensor, placeholder)
|
||||
return out
|
||||
|
||||
|
||||
def new_parameter_placeholder(
|
||||
size: tuple[int, ...], dtype: torch.dtype, device: torch.device, requires_grad: bool
|
||||
) -> torch.nn.Parameter:
|
||||
"""Create a placeholder to be passed to the above functions"""
|
||||
result = torch.nn.Parameter(
|
||||
torch.empty(size, dtype=dtype, device=device), requires_grad=requires_grad
|
||||
)
|
||||
# TODO(jansel): alloc followed by free is inefficient, need a way to allocate an unbacked tensor.
|
||||
# Allocating a zero tensor would causes assert failures in autograd.
|
||||
result.untyped_storage().resize_(0)
|
||||
return result
|
||||
|
||||
|
||||
_TLS = threading.local()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def do_not_convert_to_tracable_parameter() -> Generator[bool, None, None]:
|
||||
old_flag = getattr(_TLS, "convert_tracable_parameter", True)
|
||||
_TLS.convert_tracable_parameter = False
|
||||
try:
|
||||
yield False
|
||||
finally:
|
||||
_TLS.convert_tracable_parameter = old_flag
|
||||
|
||||
|
||||
def can_convert_to_tracable_parameter() -> bool:
|
||||
return getattr(_TLS, "convert_tracable_parameter", True)
|
||||
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
Provides thread-local scope identification for SubgraphTracer instances.
|
||||
|
||||
This module implements a thread-safe mechanism for tracking nested tracing contexts,
|
||||
which is essential when multiple SubgraphTracer instances are active. The scope ID
|
||||
helps identify which tracer context is currently active when direct access to the
|
||||
InstructionTranslator is difficult.
|
||||
|
||||
Key components:
|
||||
- Thread-local scope ID storage (_current_scope_id)
|
||||
- Getter function (current_scope_id) to safely access the current scope
|
||||
- Context manager (enter_new_scope) for managing nested scope transitions
|
||||
|
||||
The scope ID increments when entering a new context and decrements when exiting,
|
||||
allowing proper tracking of nested tracing operations across different threads.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import threading
|
||||
from collections.abc import Generator
|
||||
|
||||
|
||||
# Global variable to identify which SubgraphTracer we are in.
|
||||
# It is sometimes difficult to find an InstructionTranslator to use.
|
||||
_current_scope_id = threading.local()
|
||||
|
||||
|
||||
def current_scope_id() -> int:
|
||||
global _current_scope_id
|
||||
if not hasattr(_current_scope_id, "value"):
|
||||
_current_scope_id.value = 1
|
||||
return _current_scope_id.value
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def enter_new_scope() -> Generator[None, None, None]:
|
||||
global _current_scope_id
|
||||
try:
|
||||
_current_scope_id.value = current_scope_id() + 1
|
||||
yield
|
||||
finally:
|
||||
_current_scope_id.value = current_scope_id() - 1
|
||||
@@ -0,0 +1,216 @@
|
||||
"""
|
||||
DCE pass for unused extra outputs in HOP subgraphs.
|
||||
|
||||
When enable_side_effects_with_extra_outputs is True, HOPs like invoke_subgraph and
|
||||
checkpoint (tag_activation_checkpoint)
|
||||
return all intermediate tensors/symints as extra outputs to support side effects.
|
||||
However, many of these extra outputs may not actually be used in the parent graph.
|
||||
|
||||
This pass removes unused extra outputs by:
|
||||
1. Collecting all callers for each subgraph
|
||||
2. Checking if each output is used by all callers
|
||||
3. Removing unused outputs from the subgraph's output node
|
||||
4. Updating the HOP call and getitem indices in all call sites
|
||||
|
||||
"""
|
||||
|
||||
import collections
|
||||
import operator
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
# HOPs that may have extra outputs that can be DCE'd
|
||||
_HOPS_WITH_EXTRA_OUTPUTS = {
|
||||
torch.ops.higher_order.invoke_subgraph,
|
||||
torch.ops.higher_order.tag_activation_checkpoint,
|
||||
# torch.ops.higher_order.autograd_function_apply,
|
||||
}
|
||||
|
||||
|
||||
def dce_hop_extra_outputs(gm: torch.fx.GraphModule) -> bool:
|
||||
"""
|
||||
Remove unused extra outputs from HOP calls in all submodules.
|
||||
|
||||
For each subgraph output, check if any caller has a getitem for that index
|
||||
with users. If no caller uses it, remove the output.
|
||||
If the user in caller is an output node, to simply the algorithm, we do not recursively check
|
||||
if the caller's output is used further up in the call chain.
|
||||
|
||||
Args:
|
||||
gm: The GraphModule to optimize
|
||||
|
||||
Returns:
|
||||
True if any modifications were made, False otherwise
|
||||
"""
|
||||
# Collect all subgraph usages: subgraph_id -> list of (parent_gm, subgraph_name, hop_node)
|
||||
subgraph_id_to_callers: dict[
|
||||
int, list[tuple[torch.fx.GraphModule, str, torch.fx.Node]]
|
||||
] = collections.defaultdict(list)
|
||||
_collect_all_subgraph_usages(gm, subgraph_id_to_callers)
|
||||
|
||||
if not subgraph_id_to_callers:
|
||||
return False
|
||||
|
||||
modified = False
|
||||
|
||||
for callers in subgraph_id_to_callers.values():
|
||||
parent_gm, subgraph_name, _ = callers[0]
|
||||
subgraph = getattr(parent_gm, subgraph_name)
|
||||
|
||||
if not isinstance(subgraph, torch.fx.GraphModule):
|
||||
continue
|
||||
|
||||
output_node = next(n for n in subgraph.graph.nodes if n.op == "output")
|
||||
output_args = output_node.args[0]
|
||||
if not isinstance(output_args, (tuple, list)):
|
||||
continue
|
||||
|
||||
num_outputs = len(output_args)
|
||||
used_indices: set[int] = set()
|
||||
|
||||
# Check which outputs are used by any caller
|
||||
for idx in range(num_outputs):
|
||||
if _is_output_used(idx, callers):
|
||||
used_indices.add(idx)
|
||||
|
||||
# DCE if some outputs are unused
|
||||
if 0 < len(used_indices) < num_outputs:
|
||||
if _dce_subgraph(subgraph, callers, used_indices):
|
||||
modified = True
|
||||
|
||||
return modified
|
||||
|
||||
|
||||
def _collect_all_subgraph_usages(
|
||||
gm: torch.fx.GraphModule,
|
||||
subgraph_id_to_callers: dict[
|
||||
int, list[tuple[torch.fx.GraphModule, str, torch.fx.Node]]
|
||||
],
|
||||
) -> None:
|
||||
"""Recursively collect all HOP usages across the graph tree."""
|
||||
for node in gm.graph.nodes:
|
||||
if node.op == "call_function" and node.target in _HOPS_WITH_EXTRA_OUTPUTS:
|
||||
subgraph_attr = node.args[0]
|
||||
if (
|
||||
isinstance(subgraph_attr, torch.fx.Node)
|
||||
and subgraph_attr.op == "get_attr"
|
||||
):
|
||||
subgraph_name = subgraph_attr.target
|
||||
assert isinstance(subgraph_name, str)
|
||||
subgraph = getattr(gm, subgraph_name, None)
|
||||
if isinstance(subgraph, torch.fx.GraphModule):
|
||||
subgraph_id = id(subgraph)
|
||||
subgraph_id_to_callers[subgraph_id].append(
|
||||
(gm, subgraph_name, node)
|
||||
)
|
||||
_collect_all_subgraph_usages(subgraph, subgraph_id_to_callers)
|
||||
|
||||
|
||||
def _is_output_used(
|
||||
output_idx: int,
|
||||
callers: list[tuple[torch.fx.GraphModule, str, torch.fx.Node]],
|
||||
) -> bool:
|
||||
"""Check if output_idx is used by ANY caller (has a getitem with users)."""
|
||||
for _parent_gm, _subgraph_name, hop_node in callers:
|
||||
for user in hop_node.users:
|
||||
if user.op == "call_function" and user.target == operator.getitem:
|
||||
if user.args[1] == output_idx and len(user.users) > 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _dce_subgraph(
|
||||
subgraph: torch.fx.GraphModule,
|
||||
callers: list[tuple[torch.fx.GraphModule, str, torch.fx.Node]],
|
||||
used_indices: set[int],
|
||||
) -> bool:
|
||||
"""
|
||||
DCE a subgraph by removing unused output indices.
|
||||
|
||||
Updates the subgraph's output node, all getitem nodes in callers,
|
||||
and example_value metadata on HOP nodes.
|
||||
"""
|
||||
output_node = next(n for n in subgraph.graph.nodes if n.op == "output")
|
||||
old_outputs = list(output_node.args[0])
|
||||
|
||||
# Check if this is the forward subgraph of autograd_function_apply
|
||||
# For autograd_function_apply, the fwd subgraph must return (output, saved_values, ...)
|
||||
# where indices 0 and 1 are ALWAYS required by the runtime
|
||||
# is_autograd_fwd = any(
|
||||
# node.target == torch.ops.higher_order.autograd_function_apply
|
||||
# for node in hop_nodes
|
||||
# )
|
||||
is_autograd_fwd = False
|
||||
|
||||
# For autograd_function_apply forward subgraph, indices 0 (output) and 1 (saved_values)
|
||||
# are ALWAYS used by the runtime, even if not explicitly accessed via getitem
|
||||
if is_autograd_fwd and len(old_outputs) >= 2:
|
||||
used_indices.add(0) # output
|
||||
used_indices.add(1) # saved_values
|
||||
|
||||
# Nothing to DCE if all outputs are used or no outputs are used
|
||||
if len(used_indices) >= len(old_outputs) or len(used_indices) == 0:
|
||||
return False
|
||||
|
||||
# Build mapping from old indices to new indices
|
||||
old_to_new: dict[int, int] = {}
|
||||
new_outputs = []
|
||||
new_idx = 0
|
||||
|
||||
for old_idx in range(len(old_outputs)):
|
||||
if old_idx in used_indices:
|
||||
old_to_new[old_idx] = new_idx
|
||||
new_outputs.append(old_outputs[old_idx])
|
||||
new_idx += 1
|
||||
|
||||
# Update subgraph output node
|
||||
# Create a new output node with the filtered outputs
|
||||
with subgraph.graph.inserting_before(output_node):
|
||||
new_output_node = subgraph.graph.output(tuple(new_outputs))
|
||||
output_node.replace_all_uses_with(new_output_node)
|
||||
subgraph.graph.erase_node(output_node)
|
||||
|
||||
for parent_gm, _, hop_node in callers:
|
||||
# Update getitem nodes to use new indices
|
||||
for user in list(hop_node.users):
|
||||
if user.op == "call_function" and user.target == operator.getitem:
|
||||
old_idx = user.args[1]
|
||||
assert isinstance(old_idx, int)
|
||||
|
||||
if old_idx not in old_to_new:
|
||||
assert len(list(user.users)) == 0
|
||||
parent_gm.graph.erase_node(user)
|
||||
continue
|
||||
|
||||
new_idx = old_to_new[old_idx]
|
||||
# Create a new getitem node with the new index
|
||||
with parent_gm.graph.inserting_before(user):
|
||||
new_getitem = parent_gm.graph.call_function(
|
||||
operator.getitem, args=(user.args[0], new_idx)
|
||||
)
|
||||
# Copy metadata from old node
|
||||
new_getitem.meta = user.meta.copy()
|
||||
user.replace_all_uses_with(new_getitem)
|
||||
parent_gm.graph.erase_node(user)
|
||||
|
||||
# Update example_value metadata on hop_node
|
||||
if "example_value" in hop_node.meta:
|
||||
old_example = hop_node.meta["example_value"]
|
||||
assert isinstance(old_example, (tuple, list))
|
||||
new_example = tuple(
|
||||
old_example[old_idx]
|
||||
for old_idx in range(len(old_outputs))
|
||||
if old_idx in used_indices
|
||||
)
|
||||
hop_node.meta["example_value"] = new_example
|
||||
|
||||
# Recompile subgraph and all modified parent graphs
|
||||
subgraph.graph.lint()
|
||||
subgraph.recompile()
|
||||
|
||||
for parent_gm in {caller[0] for caller in callers}:
|
||||
parent_gm.graph.lint()
|
||||
parent_gm.recompile()
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,980 @@
|
||||
"""
|
||||
Debug utilities for TorchDynamo compilation and execution.
|
||||
|
||||
This module provides various debugging tools and utilities for TorchDynamo, including:
|
||||
|
||||
- Minification support for reducing test cases while preserving bugs
|
||||
- Input/output handling via InputReader and InputWriter for reproducible testing
|
||||
- Accuracy checking between original and compiled models
|
||||
- Neural network module string conversion via NNModuleToString
|
||||
- Profiling tools and system information collection
|
||||
- Buck build system integration for Meta-internal testing
|
||||
|
||||
Key classes:
|
||||
- InputReader/InputWriter: Handle serialization of model inputs/outputs
|
||||
- NNModuleToString: Converts nn.Modules to string representations
|
||||
- BuckTargetWriter: Manages Buck build system integration
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
import copy
|
||||
import cProfile
|
||||
import functools
|
||||
import getpass
|
||||
import inspect
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
from collections import Counter
|
||||
from importlib import import_module
|
||||
from typing import Any, TYPE_CHECKING, TypeVar
|
||||
|
||||
import torch
|
||||
import torch._prims_common as utils
|
||||
import torch._subclasses.meta_utils
|
||||
from torch import Tensor
|
||||
from torch._dynamo.testing import rand_strided
|
||||
from torch._inductor.cpp_builder import normalize_path_separator
|
||||
from torch._prims_common import is_float_dtype
|
||||
from torch.multiprocessing.reductions import StorageWeakRef
|
||||
from torch.utils._content_store import ContentStoreReader, ContentStoreWriter
|
||||
|
||||
from . import config
|
||||
from .utils import clone_inputs, get_debug_dir, warn_once
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Sequence
|
||||
|
||||
from torch.hub import tqdm
|
||||
from torch.storage import UntypedStorage
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
inductor_config = import_module("torch._inductor.config")
|
||||
use_buck = inductor_config.is_fbcode()
|
||||
|
||||
if use_buck:
|
||||
import libfb.py.build_info
|
||||
|
||||
|
||||
# pyrefly: ignore [implicit-any]
|
||||
extra_deps = []
|
||||
extra_imports = ""
|
||||
cur_target = ""
|
||||
if use_buck:
|
||||
extra_deps = [
|
||||
"//caffe2/torch/fb/sparsenn:sparsenn_operators_gpu",
|
||||
"//caffe2/torch/fb/sparsenn:sparsenn_operators",
|
||||
"//deeplearning/fbgemm/fbgemm_gpu:sparse_ops_cpu",
|
||||
"//deeplearning/fbgemm/fbgemm_gpu:sparse_ops",
|
||||
]
|
||||
cur_target = libfb.py.build_info.BuildInfo.get_build_rule().replace("fbcode:", "//") # type: ignore[possibly-undefined]
|
||||
extra_imports = "\n".join([f'torch.ops.load_library("{x}")' for x in extra_deps])
|
||||
|
||||
|
||||
BUCK_CMD_PREFIX = ["buck2", "run", "@mode/dev-nosan"]
|
||||
|
||||
|
||||
class BuckTargetWriter:
|
||||
def __init__(self, filename: str) -> None:
|
||||
self.subdir, self.py_file = os.path.split(os.path.abspath(filename))
|
||||
self.target = self.py_file.replace(".py", "")
|
||||
|
||||
# Get main_module path from fbcode
|
||||
self.path = f"{self.subdir.replace('/', '.')}.{self.target}"
|
||||
self.path = self.path[self.path.find("fbcode.") :]
|
||||
self.path = self.path[7:]
|
||||
|
||||
# Get cmd line path
|
||||
tmp = self.subdir
|
||||
tmp = tmp[tmp.find("fbcode/") :][7:]
|
||||
self.cmd_line_path = f"//{tmp}:{self.target}"
|
||||
|
||||
def build(self) -> str:
|
||||
extra_cpp_deps = "\n".join([f' "{x}",' for x in extra_deps])
|
||||
return textwrap.dedent(
|
||||
f"""
|
||||
load("@fbcode_macros//build_defs:python_binary.bzl", "python_binary")
|
||||
|
||||
python_binary(
|
||||
name="{self.target}",
|
||||
srcs = ["{self.py_file}"],
|
||||
compile = False,
|
||||
deps = [
|
||||
"//caffe2:torch",
|
||||
"//caffe2:libtorch",
|
||||
"//caffe2/functorch:functorch",
|
||||
"//triton:triton",
|
||||
"{cur_target}",
|
||||
],
|
||||
cpp_deps = [
|
||||
{extra_cpp_deps}
|
||||
],
|
||||
main_module = "{self.path}",
|
||||
par_style = "xar",
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
def write(self, print_msg: bool = True) -> list[str]:
|
||||
target_file = os.path.join(self.subdir, "TARGETS")
|
||||
with open(target_file, "w") as fd:
|
||||
fd.write(self.build())
|
||||
# log.warning("Wrote isolation TARGETS file at %s", target_file)
|
||||
cmd_split = BUCK_CMD_PREFIX + [self.cmd_line_path]
|
||||
if print_msg:
|
||||
log.warning(
|
||||
"Found an example that reproduces the error. Run this cmd to repro - %s",
|
||||
" ".join(cmd_split),
|
||||
)
|
||||
return cmd_split
|
||||
|
||||
|
||||
def minifier_dir() -> str:
|
||||
path = os.path.join(get_debug_dir(), "minifier")
|
||||
if path is None:
|
||||
path = f"{tempfile.gettempdir()}/minifier_{getpass.getuser()}"
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
MAX_CONSTANT_NUMEL_INLINE = 4
|
||||
|
||||
|
||||
class NNModuleToString:
|
||||
safe_reprs = [
|
||||
torch.nn.Linear,
|
||||
torch.nn.Conv1d,
|
||||
torch.nn.Conv2d,
|
||||
torch.nn.Conv3d,
|
||||
torch.nn.BatchNorm1d,
|
||||
torch.nn.BatchNorm2d,
|
||||
torch.nn.BatchNorm3d,
|
||||
torch.nn.LayerNorm,
|
||||
torch.nn.Dropout,
|
||||
torch.nn.Softmax,
|
||||
torch.nn.ReLU,
|
||||
torch.nn.GELU,
|
||||
torch.nn.Identity,
|
||||
torch.nn.MaxPool2d,
|
||||
torch.nn.Embedding,
|
||||
torch.nn.Tanh,
|
||||
torch.nn.ConvTranspose1d,
|
||||
torch.nn.GLU,
|
||||
torch.nn.LSTM,
|
||||
torch.nn.Flatten,
|
||||
torch.nn.AdaptiveAvgPool2d,
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def can_convert_to_string(gm: torch.fx.GraphModule) -> bool:
|
||||
cant_convert = set()
|
||||
for _, module in gm.named_children():
|
||||
if type(module) not in NNModuleToString.safe_reprs:
|
||||
cant_convert.add(module)
|
||||
|
||||
if len(cant_convert) > 0:
|
||||
log.warning("We have not tested reprs of some modules - %s", cant_convert)
|
||||
# TODO - Assuming that all modules can be safely repr'd. Check if that assumption is correct.
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def convert(gm: torch.fx.GraphModule) -> str:
|
||||
from torch.nn.modules.module import _addindent
|
||||
|
||||
tab = " " * 4
|
||||
|
||||
model_str = textwrap.dedent(
|
||||
"""
|
||||
from torch.nn import *
|
||||
class Repro(torch.nn.Module):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
"""
|
||||
)
|
||||
|
||||
for module_name, module in gm.named_children():
|
||||
module_str = f"{module.__repr__()}"
|
||||
# module should be a core torch.nn.Module, so all parameters
|
||||
# should be on the same device.
|
||||
example_param = next(module.parameters(), None)
|
||||
if example_param is not None and example_param.is_cuda:
|
||||
module_str = f"{module_str}.cuda()"
|
||||
model_str += f"{tab * 2}self.{module_name} = {module_str}\n"
|
||||
|
||||
for buffer_name, buffer in gm._buffers.items():
|
||||
if buffer is None:
|
||||
continue
|
||||
# Serialize full data for small buffers
|
||||
if buffer.numel() <= MAX_CONSTANT_NUMEL_INLINE:
|
||||
from torch._tensor_str import PRINT_OPTS
|
||||
|
||||
assert PRINT_OPTS.threshold >= MAX_CONSTANT_NUMEL_INLINE
|
||||
tensor_str = repr(buffer)
|
||||
elif torch.is_floating_point(buffer):
|
||||
tensor_str = f"torch.randn({list(buffer.shape)}, dtype={buffer.dtype})"
|
||||
else:
|
||||
tensor_str = (
|
||||
f"torch.randint(1, size={list(buffer.shape)}, dtype={buffer.dtype})"
|
||||
)
|
||||
if buffer.is_cuda:
|
||||
tensor_str = f"{tensor_str}.cuda()"
|
||||
model_str += (
|
||||
f"{tab * 2}self.register_buffer('{buffer_name}', {tensor_str})\n"
|
||||
)
|
||||
|
||||
for param_name, param in gm._parameters.items():
|
||||
if param is None:
|
||||
continue
|
||||
maybe_device = ""
|
||||
if param.is_cuda:
|
||||
maybe_device = ', device="cuda"'
|
||||
tensor_str = f"torch.nn.Parameter(torch.randn({list(param.shape)}, dtype={param.dtype}{maybe_device}))"
|
||||
model_str += f"{tab * 2}self.{param_name} = {tensor_str}\n"
|
||||
|
||||
# TODO - Keep this code for now. But, I don't think we will need this.
|
||||
# attrs = dir(gm)
|
||||
# for attr in attrs:
|
||||
# if "_tensor_constant" in attr:
|
||||
# val = getattr(gm, attr)
|
||||
# model_str += f" {attr} = {val!r}\n"
|
||||
|
||||
model_str += f"{_addindent(gm.code, 4)}\n"
|
||||
return model_str
|
||||
|
||||
|
||||
@functools.cache # subprocess is expensive
|
||||
def _cuda_system_info_comment() -> str:
|
||||
if not torch.cuda.is_available():
|
||||
return "# torch.cuda.is_available()==False, no GPU info collected\n"
|
||||
|
||||
model_str = "# CUDA Info: \n"
|
||||
try:
|
||||
if torch.version.hip is None:
|
||||
cuda_version_out = subprocess.check_output(["nvcc", "--version"])
|
||||
cuda_version_lines = cuda_version_out.decode().split("\n")
|
||||
comment = "".join([f"# {s} \n" for s in cuda_version_lines if s != ""])
|
||||
model_str += f"{comment}\n"
|
||||
else:
|
||||
model_str += "# Not searching for nvcc on ROCM setup\n"
|
||||
except (FileNotFoundError, subprocess.CalledProcessError):
|
||||
model_str += "# nvcc not found\n"
|
||||
|
||||
gpu_names = Counter(
|
||||
torch.cuda.get_device_name(i) for i in range(torch.cuda.device_count())
|
||||
)
|
||||
|
||||
model_str += "# GPU Hardware Info: \n"
|
||||
for name, count in gpu_names.items():
|
||||
model_str += f"# {name} : {count} \n"
|
||||
model_str += "\n"
|
||||
return model_str
|
||||
|
||||
|
||||
def generate_env_vars_string(*, stable_output: bool = False) -> str:
|
||||
"""
|
||||
Generate a string configuration for environment variables related to Dynamo, Inductor, and Triton.
|
||||
"""
|
||||
if stable_output:
|
||||
return "# env var omitted due to stable_output=True"
|
||||
|
||||
allow_list = ["TORCH", "DYNAMO", "INDUCTOR", "TRITON"]
|
||||
skip_list = ["TRITON_LIBDEVICE_PATH", "TRITON_PTXAS_PATH", "TRITON_LIBCUDA_PATH"]
|
||||
|
||||
def filter(key: str) -> bool:
|
||||
return any(string in key for string in allow_list) and key not in skip_list
|
||||
|
||||
config_lines = [
|
||||
f"""os.environ['{key}'] = '{value.replace("'", '"')}'"""
|
||||
for key, value in os.environ.items()
|
||||
if filter(key)
|
||||
]
|
||||
config_string = "\n".join(config_lines)
|
||||
return normalize_path_separator(f"""\
|
||||
import os
|
||||
{config_string}
|
||||
""")
|
||||
|
||||
|
||||
def generate_config_string(*, stable_output: bool = False) -> str:
|
||||
import torch._functorch.config
|
||||
import torch._inductor.config
|
||||
|
||||
if stable_output:
|
||||
return "# config omitted due to stable_output=True"
|
||||
|
||||
experimental_config = torch.fx.experimental._config.codegen_config() # type: ignore[attr-defined]
|
||||
return f"""\
|
||||
import torch._dynamo.config
|
||||
import torch._inductor.config
|
||||
import torch._functorch.config
|
||||
import torch.fx.experimental._config
|
||||
{torch._dynamo.config.codegen_config()}
|
||||
{torch._inductor.config.codegen_config()}
|
||||
{torch._functorch.config.codegen_config()}
|
||||
{experimental_config}
|
||||
"""
|
||||
|
||||
|
||||
def get_minifier_repro_path() -> str:
|
||||
return os.path.join(minifier_dir(), "minifier_launcher.py")
|
||||
|
||||
|
||||
def helper_for_dump_minify(contents: str) -> None:
|
||||
minified_repro_path = get_minifier_repro_path()
|
||||
log.warning("Writing minified repro to:\n%s", minified_repro_path)
|
||||
|
||||
if use_buck:
|
||||
BuckTargetWriter(minified_repro_path).write()
|
||||
try:
|
||||
with open(minified_repro_path, "w") as fd:
|
||||
fd.write(contents)
|
||||
|
||||
except OSError as e:
|
||||
log.exception("")
|
||||
raise NotImplementedError(f"Could not write to {minified_repro_path}") from e
|
||||
|
||||
|
||||
class AccuracyError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def clone_inputs_retaining_gradness(example_inputs: Sequence[Any]) -> list[Any]:
|
||||
"""
|
||||
This clone inputs is different from utils clone_input. In case of minifier,
|
||||
all the tensors are leaf tensors while creating a new graph. So, we set the
|
||||
requires_grad field w/o checking the leafness of the tensor.
|
||||
"""
|
||||
cloned_inputs = clone_inputs(example_inputs)
|
||||
for idx in range(len(example_inputs)):
|
||||
if isinstance(cloned_inputs[idx], torch.Tensor):
|
||||
cloned_inputs[idx].requires_grad_(example_inputs[idx].requires_grad)
|
||||
return cloned_inputs # type: ignore[return-value]
|
||||
|
||||
|
||||
def run_fwd_maybe_bwd(
|
||||
gm: torch.fx.GraphModule,
|
||||
args: Sequence[Any],
|
||||
only_fwd: bool = False,
|
||||
disable_clone: bool = False,
|
||||
) -> Any:
|
||||
"""
|
||||
Runs a forward and possibly backward iteration for a given mod and args.
|
||||
|
||||
When disable_clone is True, we will use args as-is without cloning.
|
||||
This is higher fidelity but we may destroy the args in the process.
|
||||
"""
|
||||
from .testing import collect_results, reduce_to_scalar_loss, requires_bwd_pass
|
||||
|
||||
gm = copy.deepcopy(gm)
|
||||
if not disable_clone:
|
||||
args = clone_inputs_retaining_gradness(args)
|
||||
|
||||
if hasattr(gm, "zero_grad"):
|
||||
gm.zero_grad(True)
|
||||
|
||||
# TorchInductor returned callable expects lists. So, may need a boxed calling convention.
|
||||
out = gm(args) if getattr(gm, "_boxed_call", False) else gm(*args)
|
||||
|
||||
if only_fwd:
|
||||
return out
|
||||
if requires_bwd_pass(out):
|
||||
loss = reduce_to_scalar_loss(out)
|
||||
loss.backward()
|
||||
return collect_results(gm, out, None, args)
|
||||
|
||||
|
||||
def same_two_models(
|
||||
gm: torch.fx.GraphModule,
|
||||
opt_gm: torch.fx.GraphModule,
|
||||
example_inputs: Sequence[Any],
|
||||
only_fwd: bool = False,
|
||||
*,
|
||||
require_fp64: bool = False,
|
||||
ignore_non_fp: bool = False,
|
||||
) -> bool:
|
||||
"""
|
||||
Check two models have same accuracy.
|
||||
|
||||
require_fp64: if True, raise an error if we unable to calculate the fp64 reference
|
||||
ignore_non_fp: if True, do not compare outputs which are not floating point. This
|
||||
is mostly useful for the minifier (which wants to avoid quantizing floating point
|
||||
error into integer/boolean error)
|
||||
"""
|
||||
from .utils import same
|
||||
|
||||
ref = run_fwd_maybe_bwd(gm, example_inputs, only_fwd)
|
||||
|
||||
fp64_ref = None
|
||||
if config.same_two_models_use_fp64:
|
||||
try:
|
||||
fp64_model, fp64_examples = cast_to_fp64(
|
||||
copy.deepcopy(gm), clone_inputs_retaining_gradness(example_inputs)
|
||||
)
|
||||
fp64_ref = run_fwd_maybe_bwd(fp64_model, fp64_examples, only_fwd)
|
||||
except Exception:
|
||||
if require_fp64:
|
||||
raise RuntimeError( # noqa: B904
|
||||
"Could not generate fp64 outputs, workaround with torch._dynamo.config.same_two_models_use_fp64 = False"
|
||||
)
|
||||
log.warning("Could not generate fp64 outputs")
|
||||
|
||||
try:
|
||||
res = run_fwd_maybe_bwd(opt_gm, example_inputs, only_fwd)
|
||||
except Exception:
|
||||
# This means that the minified graph is bad/exposes a different problem.
|
||||
# As we are checking accuracy here, lets log the exception and return True.
|
||||
log.exception(
|
||||
"While minifying the program in accuracy minification mode, "
|
||||
"ran into a runtime exception which is likely an unrelated issue."
|
||||
" Skipping this graph."
|
||||
)
|
||||
return True
|
||||
|
||||
passing = same(
|
||||
ref,
|
||||
res,
|
||||
fp64_ref,
|
||||
tol=config.repro_tolerance,
|
||||
equal_nan=True,
|
||||
ignore_non_fp=ignore_non_fp,
|
||||
)
|
||||
return passing
|
||||
|
||||
|
||||
def cast_dtype_args_to_fp64(model: torch.fx.GraphModule) -> torch.fx.GraphModule:
|
||||
for node in model.graph.nodes:
|
||||
if (
|
||||
node.op == "call_function"
|
||||
and node.target is torch.ops.prims.convert_element_type.default
|
||||
):
|
||||
assert len(node.args) == 2
|
||||
if is_float_dtype(node.args[1]) and node.args[1] != torch.float64:
|
||||
node.args = (node.args[0], torch.float64)
|
||||
if node.op == "call_function":
|
||||
dtype = node.kwargs.get("dtype")
|
||||
if dtype is not None and is_float_dtype(dtype):
|
||||
new_kwargs = dict(node.kwargs)
|
||||
new_kwargs["dtype"] = torch.float64
|
||||
node.kwargs = new_kwargs
|
||||
|
||||
model.graph.lint()
|
||||
model.recompile()
|
||||
return model
|
||||
|
||||
|
||||
def cast_to(
|
||||
dtype: torch.dtype, model: torch.fx.GraphModule, inputs: list[Any]
|
||||
) -> tuple[torch.fx.GraphModule, list[Any]]:
|
||||
from torch.utils._pytree import tree_map
|
||||
|
||||
model = model.to(dtype)
|
||||
if dtype == torch.float64:
|
||||
# If casting to fp64 for accuracy comparison, we need to
|
||||
# replace dtype arguments embedded in the graph with fp64
|
||||
model = cast_dtype_args_to_fp64(model)
|
||||
|
||||
inputs = tree_map(
|
||||
lambda x: x.to(dtype)
|
||||
if isinstance(x, torch.Tensor) and x.is_floating_point()
|
||||
else x,
|
||||
inputs,
|
||||
)
|
||||
return model, inputs
|
||||
|
||||
|
||||
def cast_to_fp64(
|
||||
model: torch.fx.GraphModule, inputs: list[Any]
|
||||
) -> tuple[torch.fx.GraphModule, list[Any]]:
|
||||
return cast_to(torch.float64, model, inputs)
|
||||
|
||||
|
||||
def backend_accuracy_fails(
|
||||
gm: torch.fx.GraphModule,
|
||||
example_inputs: Sequence[Any],
|
||||
compiler_fn: Callable[[torch.fx.GraphModule, list[Any]], torch.fx.GraphModule],
|
||||
only_fwd: bool = False,
|
||||
*,
|
||||
require_fp64: bool = False,
|
||||
ignore_non_fp: bool = False,
|
||||
) -> bool:
|
||||
try:
|
||||
compiled_gm = compiler_fn(
|
||||
copy.deepcopy(gm), clone_inputs_retaining_gradness(example_inputs)
|
||||
)
|
||||
return not same_two_models(
|
||||
gm,
|
||||
compiled_gm,
|
||||
example_inputs,
|
||||
only_fwd,
|
||||
require_fp64=require_fp64,
|
||||
ignore_non_fp=ignore_non_fp,
|
||||
)
|
||||
except Exception:
|
||||
# This means that the minified graph is bad/exposes a different problem.
|
||||
# As we are checking accuracy here, lets log the exception and return False.
|
||||
log.exception(
|
||||
"While minifying the program in accuracy minification mode, "
|
||||
"ran into a runtime exception which is likely an unrelated issue."
|
||||
" Skipping this graph"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
|
||||
# REPRO SUPPORT CODE
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
|
||||
|
||||
|
||||
# Helper functions for computing what the default values of tensor
|
||||
# values should be. These all coincide with factory functions, e.g., torch.empty
|
||||
|
||||
|
||||
def _stride_or_default(
|
||||
stride: torch._prims_common.StrideType | None,
|
||||
*,
|
||||
shape: torch._prims_common.ShapeType,
|
||||
) -> torch._prims_common.StrideType:
|
||||
return stride if stride is not None else utils.make_contiguous_strides_for(shape)
|
||||
|
||||
|
||||
def _mk_defaulter(d: T) -> Callable[[T | None], T]:
|
||||
return lambda x: x if x is not None else d
|
||||
|
||||
|
||||
_dtype_or_default = _mk_defaulter(torch.float32)
|
||||
_device_or_default = _mk_defaulter(torch.device("cpu"))
|
||||
_storage_offset_or_default = _mk_defaulter(0)
|
||||
_requires_grad_or_default = _mk_defaulter(False)
|
||||
_is_leaf_or_default = _mk_defaulter(False)
|
||||
|
||||
|
||||
class NopInputReader:
|
||||
def __init__(self) -> None:
|
||||
self.total = 0
|
||||
|
||||
def storage(
|
||||
self,
|
||||
storage_hash: str | None,
|
||||
nbytes: int,
|
||||
*,
|
||||
device: torch._prims_common.DeviceLikeType | None = None,
|
||||
dtype_hint: torch.dtype | None = None,
|
||||
) -> None:
|
||||
self.total += 1
|
||||
|
||||
def tensor(self, *args: Any, **kwargs: Any) -> torch.Tensor | None:
|
||||
pass
|
||||
|
||||
def symint(self, *args: Any, **kwargs: Any) -> int | None:
|
||||
pass
|
||||
|
||||
def const(self, name: str) -> None:
|
||||
pass
|
||||
|
||||
def unsupported(self, name: str) -> None:
|
||||
pass
|
||||
|
||||
def generator(self, device_type: str, device_index: int) -> None:
|
||||
pass
|
||||
|
||||
def opaque(self, script_class_name: str) -> None:
|
||||
self.total += 1
|
||||
|
||||
|
||||
# TODO: Support bundling the entire repro into a zip file for ease of
|
||||
# transferring around
|
||||
class InputReader:
|
||||
def __init__(
|
||||
self, save_dir: str | None = None, *, pbar: tqdm | None = None
|
||||
) -> None:
|
||||
# If None, we will generate random data instead. It's important
|
||||
# to natively support this use case as it will allow people to
|
||||
# share repros without including the real data, if the problem
|
||||
# reproduces even on random data.
|
||||
if save_dir is None:
|
||||
log.warning("no save_dir specified, will generate random data")
|
||||
self.store = ContentStoreReader(save_dir) if save_dir is not None else None
|
||||
self.args: list[Any] = []
|
||||
self.pbar = pbar
|
||||
|
||||
def storage(
|
||||
self,
|
||||
storage_hash: str | None,
|
||||
nbytes: int,
|
||||
*,
|
||||
device: torch._prims_common.DeviceLikeType | None = None,
|
||||
dtype_hint: torch.dtype | None = None,
|
||||
) -> UntypedStorage:
|
||||
if self.pbar is not None:
|
||||
self.pbar.update(1)
|
||||
device = _device_or_default(device) # type: ignore[arg-type]
|
||||
dtype_hint = _dtype_or_default(dtype_hint)
|
||||
if self.store is not None and storage_hash is not None:
|
||||
try:
|
||||
storage = self.store.read_storage(storage_hash)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
else:
|
||||
if device != storage.device:
|
||||
log.warning("device mismatch: %s != %s", device, storage.device)
|
||||
# TODO: transfer it to the right device? But failing this
|
||||
# way would be very mysterious! Would have been better
|
||||
# not to store device in the serialized format...
|
||||
return storage
|
||||
warn_once(f"could not load {storage_hash}, generating random data instead")
|
||||
shape = (nbytes // dtype_hint.itemsize,)
|
||||
stride = _stride_or_default(None, shape=shape)
|
||||
return rand_strided(shape, stride, dtype_hint, device).untyped_storage()
|
||||
|
||||
def tensor(
|
||||
self,
|
||||
storage: UntypedStorage,
|
||||
shape: torch._prims_common.ShapeType,
|
||||
stride: torch._prims_common.StrideType | None = None,
|
||||
*,
|
||||
storage_offset: int | None = None,
|
||||
dtype: torch.dtype | None = None,
|
||||
requires_grad: bool | None = None,
|
||||
is_leaf: bool | None = None,
|
||||
**metadata: Any,
|
||||
) -> torch.Tensor:
|
||||
stride = _stride_or_default(stride, shape=shape)
|
||||
storage_offset = _storage_offset_or_default(storage_offset)
|
||||
dtype = _dtype_or_default(dtype)
|
||||
is_leaf = _is_leaf_or_default(is_leaf)
|
||||
requires_grad = _requires_grad_or_default(requires_grad)
|
||||
t = torch.tensor(
|
||||
[], dtype=dtype, device=storage.device, requires_grad=requires_grad
|
||||
)
|
||||
with torch.no_grad():
|
||||
t.set_(storage, storage_offset, shape, stride)
|
||||
if not is_leaf:
|
||||
# Fake up some autograd history in a very naughty way
|
||||
with torch.enable_grad():
|
||||
t = t.clone(memory_format=torch.preserve_format)
|
||||
with torch.no_grad():
|
||||
t.set_(storage, storage_offset, shape, stride)
|
||||
assert torch._subclasses.meta_utils.safe_is_leaf(t) == is_leaf
|
||||
torch._utils.set_tensor_metadata(t, metadata)
|
||||
self.args.append(t)
|
||||
return t # for BC
|
||||
|
||||
def symint(self, val: Any) -> Any:
|
||||
self.args.append(val)
|
||||
return val # for BC
|
||||
|
||||
def const(self, name: str) -> None:
|
||||
self.args.append(None)
|
||||
|
||||
def unsupported(self, name: str) -> None:
|
||||
self.args.append(None)
|
||||
|
||||
def generator(self, device_type: str, device_index: int) -> torch._C.Generator:
|
||||
gen = torch.cuda.default_generators[device_index].clone_state()
|
||||
self.args.append(gen)
|
||||
return gen
|
||||
|
||||
def opaque(self, script_class_name: str) -> None:
|
||||
self.args.append(None)
|
||||
|
||||
|
||||
# Here is our writer strategy:
|
||||
# 1. We will stream all of the inputs to disk
|
||||
# 2. You can now deterministically randomize the inputs, or reload
|
||||
# the inputs from disk
|
||||
# 3. You can YOLO run the script without the inputs, in which case
|
||||
# we'll fill the inputs with random data and pray. This is the
|
||||
# legacy behavior, but it's also useful if you want to find out
|
||||
# if we're so broken even random inputs trigger it
|
||||
# 4. We could offer an in process "check if the randomized thing
|
||||
# works too" but this is delicate so we don't do it
|
||||
|
||||
|
||||
class InputWriter:
|
||||
def __init__(self, save_dir: str | None, *, stable_hash: bool = False) -> None:
|
||||
self._lines: list[str] = []
|
||||
# TODO: consider ensuring tensor and storage counters line up?
|
||||
self.storage_counter = itertools.count()
|
||||
self.save_dir = save_dir
|
||||
self.store = (
|
||||
ContentStoreWriter(save_dir, stable_hash=stable_hash)
|
||||
if save_dir is not None
|
||||
else None
|
||||
)
|
||||
self.seen_storages: dict[StorageWeakRef, str] = {}
|
||||
|
||||
def lines(self) -> list[str]:
|
||||
r = [
|
||||
"def load_args(reader):",
|
||||
]
|
||||
r.extend(f" {l}" for l in self._lines)
|
||||
# In case we need to change the internal format of load_args
|
||||
# in an FC-breaking way
|
||||
r.append("load_args._version = 0")
|
||||
return r
|
||||
|
||||
# Storages are untyped, but we need to initialize them with data if
|
||||
# we don't have the real data, so we give a hint saying what kind
|
||||
# of initialization may be appropriate
|
||||
#
|
||||
# If we had a FakeTensor, device_hint tells us what device should be
|
||||
def storage(
|
||||
self,
|
||||
untyped_storage: UntypedStorage,
|
||||
*,
|
||||
device_hint: torch._prims_common.DeviceLikeType | None = None,
|
||||
dtype_hint: torch.dtype | None = None,
|
||||
) -> str:
|
||||
ws = StorageWeakRef(untyped_storage)
|
||||
v = self.seen_storages.get(ws)
|
||||
if v is not None:
|
||||
return v
|
||||
v = f"buf{next(self.storage_counter)}"
|
||||
maybe_dtype_hint = ""
|
||||
if _dtype_or_default(None) != _dtype_or_default(dtype_hint):
|
||||
maybe_dtype_hint = f", dtype_hint={dtype_hint!r}"
|
||||
# TODO: being optional on device is kind of pointless as the default
|
||||
# is CPU but most repros we care about are CUDA
|
||||
maybe_device = ""
|
||||
device = untyped_storage.device
|
||||
if device.type == "meta":
|
||||
assert device_hint is not None
|
||||
device = device_hint # type: ignore[assignment]
|
||||
if _device_or_default(None) != device:
|
||||
maybe_device = f", device={device!r}"
|
||||
nbytes = untyped_storage.nbytes()
|
||||
storage_hash = None
|
||||
if self.store is not None and untyped_storage.device.type != "meta":
|
||||
storage_hash = self.store.write_storage(untyped_storage)
|
||||
self._lines.append(
|
||||
f"{v} = reader.storage({storage_hash!r}, {nbytes!r}{maybe_device}{maybe_dtype_hint})"
|
||||
)
|
||||
self.seen_storages[ws] = v
|
||||
return v
|
||||
|
||||
def tensor(self, name: str, t: torch.Tensor) -> None:
|
||||
from torch.fx.experimental.symbolic_shapes import statically_known_true, sym_eq
|
||||
|
||||
storage = self.storage(
|
||||
t.untyped_storage(), dtype_hint=t.dtype, device_hint=t.device
|
||||
)
|
||||
args = []
|
||||
# NB: this is positional, must come first
|
||||
if not statically_known_true(
|
||||
sym_eq(_stride_or_default(None, shape=t.shape), t.stride())
|
||||
):
|
||||
args.append(str(tuple(t.stride())))
|
||||
if _dtype_or_default(None) != t.dtype:
|
||||
args.append(f"dtype={t.dtype!r}")
|
||||
if not statically_known_true(
|
||||
_storage_offset_or_default(None) == t.storage_offset()
|
||||
):
|
||||
args.append(f"storage_offset={t.storage_offset()!r}")
|
||||
tensor_metadata = torch._utils.get_tensor_metadata(t)
|
||||
if tensor_metadata:
|
||||
args.extend(f"{k}={v!r}" for k, v in tensor_metadata.items())
|
||||
if _requires_grad_or_default(None) != t.requires_grad:
|
||||
args.append(f"requires_grad={t.requires_grad!r}")
|
||||
is_leaf = torch._subclasses.meta_utils.safe_is_leaf(t)
|
||||
if _is_leaf_or_default(None) != is_leaf:
|
||||
args.append(f"is_leaf={is_leaf!r}")
|
||||
self._lines.append(
|
||||
"reader.tensor("
|
||||
+ ", ".join([storage, str(tuple(t.shape)), *args])
|
||||
+ f") # {name}"
|
||||
)
|
||||
|
||||
def unsupported(self, name: str, arg: Any) -> None:
|
||||
# NB: Try hard not to /print/ a tensor, that will be very slow
|
||||
self._lines.append(
|
||||
f"reader.unsupported({name!r}) # unsupported type for dumping: {type(arg)}"
|
||||
)
|
||||
# Best effort dump as much useful stuff we can lol, in case you want
|
||||
# to repair the repro
|
||||
if isinstance(arg, (list, tuple)):
|
||||
self._lines.append('"""')
|
||||
for i, a in enumerate(arg):
|
||||
name_i = f"{name}[{i}]"
|
||||
if isinstance(a, torch.Tensor):
|
||||
self.tensor(name_i, a)
|
||||
elif isinstance(a, (int, torch.SymInt)):
|
||||
self.symint(name_i, a)
|
||||
else:
|
||||
self.unsupported(name_i, a)
|
||||
self._lines.append('"""')
|
||||
|
||||
# write out that the arg was filtered out as it is constant
|
||||
def const(self, name: str) -> None:
|
||||
self._lines.append(
|
||||
f"reader.const({name!r}) # {name}, filtered out during compilation"
|
||||
)
|
||||
|
||||
# TODO: this doesn't actually symint atm
|
||||
def symint(self, name: str, val: Any) -> None:
|
||||
if isinstance(val, torch.SymInt):
|
||||
val = val.node.hint
|
||||
self._lines.append(f"reader.symint({val!r}) # {name}")
|
||||
|
||||
def generator(self, name: str, arg: torch._C.Generator) -> None:
|
||||
device = arg.device
|
||||
self._lines.append(
|
||||
f"reader.generator({device.type!r}, {device.index!r}) # {name}"
|
||||
)
|
||||
|
||||
def opaque(self, name: str, script_class_name: str) -> None:
|
||||
self._lines.append(f"reader.opaque({script_class_name!r}) # {name}")
|
||||
|
||||
|
||||
def aot_graph_input_parser(
|
||||
func: Callable[[list[Tensor]], list[Tensor]],
|
||||
device: str = "cuda",
|
||||
sym_shapes: dict[str, int] | None = None,
|
||||
default_sym_shape: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Takes in a function which has been printed with print_readable() and constructs kwargs to run it.
|
||||
|
||||
Handles Tensor inputs, Symints, and a graph module which might have tensor constants.
|
||||
|
||||
Consider a function `forward` defined as follows:
|
||||
|
||||
def forward(self, primals_1: "f32[1001, 6]", primals_2: "f32[s0]", primals_3: "Sym(s0)",):
|
||||
_tensor_constant0: "i64[4190]" = self._tensor_constant0
|
||||
# Further implementation
|
||||
|
||||
kwargs = aot_graph_input_parser(forward)
|
||||
forward(**kwargs)
|
||||
"""
|
||||
|
||||
from torch.utils._dtype_abbrs import dtype_abbrs
|
||||
|
||||
dtype_map: dict[str, torch.dtype] = {
|
||||
value: key for key, value in dtype_abbrs.items()
|
||||
}
|
||||
dtype_pattern: str = "|".join(dtype_abbrs.values())
|
||||
|
||||
# Extracting the source code from the function
|
||||
source = inspect.getsource(func)
|
||||
|
||||
# Regular expressions
|
||||
tensor_assignment_regex = rf"(_tensor_constant\d+): \"({dtype_pattern})\[\s*(.*?)\s*\]\" = self\.(_tensor_constant\d+)"
|
||||
tensor_regex = rf"({dtype_pattern})\[\s*(.*?)\s*\]"
|
||||
sym_shape_regex = r"Sym\((s\d+)\)"
|
||||
|
||||
class TensorContainer:
|
||||
"Container for tensors as attributes"
|
||||
|
||||
# Dictionary for tensors from annotations
|
||||
kwargs: dict[str, Any] = {}
|
||||
|
||||
sym_shapes_dict: dict[str, int] = sym_shapes or {}
|
||||
|
||||
def get_sym_int(symint: str) -> int:
|
||||
torch._check(
|
||||
symint in sym_shapes_dict or default_sym_shape is not None,
|
||||
lambda: f"{symint} not in symbolic_shapes and default sym shape not passed in",
|
||||
)
|
||||
return sym_shapes_dict.get(symint, default_sym_shape) # type: ignore[return-value]
|
||||
|
||||
def gen_tensor(shape: torch._prims_common.ShapeType, dtype: torch.dtype) -> Tensor:
|
||||
# Resolve symbolic shapes to concrete values
|
||||
resolved_shape = []
|
||||
dynamic_dims = []
|
||||
for i, dim in enumerate(shape):
|
||||
dim = dim.strip() # type: ignore[attr-defined]
|
||||
if "s" in dim:
|
||||
s = get_sym_int(dim)
|
||||
resolved_shape.append(s)
|
||||
dynamic_dims.append(i)
|
||||
else:
|
||||
if dim:
|
||||
resolved_shape.append(int(dim))
|
||||
|
||||
constructor = torch.randn if dtype.is_floating_point else torch.zeros
|
||||
out = constructor(resolved_shape, dtype=dtype, device=device) # type: ignore[call-arg]
|
||||
for d in dynamic_dims:
|
||||
torch._dynamo.mark_dynamic(out, d)
|
||||
return out
|
||||
|
||||
# Parse function annotations for tensor generation
|
||||
annotations = func.__annotations__
|
||||
for param, annotation in annotations.items():
|
||||
# Skip 'return' annotation
|
||||
if param == "return":
|
||||
continue
|
||||
|
||||
match = re.search(tensor_regex, annotation)
|
||||
if match:
|
||||
data_type, shape_str = match.groups()
|
||||
shape = tuple(shape_str.split(","))
|
||||
dtype = dtype_map[data_type]
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
kwargs[param] = gen_tensor(shape, dtype)
|
||||
|
||||
match = re.search(sym_shape_regex, annotation)
|
||||
if match:
|
||||
kwargs[param] = get_sym_int(match.group(1))
|
||||
|
||||
if "self" in inspect.signature(func).parameters:
|
||||
container = TensorContainer()
|
||||
kwargs["self"] = container
|
||||
for match in re.finditer(tensor_assignment_regex, source):
|
||||
attr_name, data_type, shape_str, _ = match.groups()
|
||||
shape = tuple(shape_str.split(","))
|
||||
dtype = dtype_map[data_type]
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
setattr(container, attr_name, gen_tensor(shape, dtype))
|
||||
|
||||
return kwargs
|
||||
|
||||
|
||||
def profile_to_file(filename: str) -> Callable[[T], T]:
|
||||
"""
|
||||
Decorator to cProfile a given function and save the result to disk on process exit.
|
||||
|
||||
Args:
|
||||
filename: filename to save profile to
|
||||
"""
|
||||
prof = cProfile.Profile()
|
||||
filename = os.path.abspath(os.path.expanduser(filename))
|
||||
|
||||
def decorator(fn: Any) -> Any:
|
||||
@functools.wraps(fn)
|
||||
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||
prof.enable()
|
||||
try:
|
||||
return fn(*args, **kwargs)
|
||||
finally:
|
||||
prof.disable()
|
||||
|
||||
return wrapper
|
||||
|
||||
def save_it() -> None:
|
||||
prof.dump_stats(filename)
|
||||
sys.stderr.write(
|
||||
textwrap.dedent(
|
||||
f"""\
|
||||
Wrote profile to {filename}, view with:
|
||||
|
||||
snakeviz {filename}
|
||||
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
atexit.register(save_it)
|
||||
return decorator
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,653 @@
|
||||
"""
|
||||
Device abstraction layer for TorchDynamo and Inductor backends.
|
||||
|
||||
This module provides a unified interface for different hardware backends (CUDA, XPU,
|
||||
CPU, MPS, MTIA) through a common device interface. Key components include:
|
||||
|
||||
- DeviceInterface: Base class defining the common API for all device types
|
||||
- Device-specific implementations: CudaInterface, XpuInterface, CpuInterface, MpsInterface, MtiaInterface
|
||||
- Device registration system for managing available backends
|
||||
- Worker APIs for multi-processing scenarios
|
||||
- Stream and event management across different devices
|
||||
- Device property caching for worker processes
|
||||
|
||||
The abstraction layer enables device-agnostic code in TorchDynamo while allowing
|
||||
specialized implementations for each hardware backend's unique features.
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import time
|
||||
from collections import namedtuple
|
||||
from collections.abc import Callable, Iterable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Literal
|
||||
|
||||
import torch
|
||||
from torch.utils._pallas import has_torch_tpu
|
||||
|
||||
|
||||
get_cuda_stream: Callable[[int], int] | None
|
||||
if torch.cuda._is_compiled():
|
||||
from torch._C import _cuda_getCurrentRawStream as get_cuda_stream
|
||||
else:
|
||||
get_cuda_stream = None
|
||||
|
||||
# Recording the device properties in the main process but used in worker process.
|
||||
caching_worker_device_properties: dict[str, Any] = {}
|
||||
caching_worker_current_devices: dict[str, int] = {}
|
||||
|
||||
|
||||
class DeviceInterface:
|
||||
"""
|
||||
This is a simple device runtime interface for Inductor. It enables custom
|
||||
backends to be integrated with Inductor in a device-agnostic semantic.
|
||||
"""
|
||||
|
||||
class device:
|
||||
def __new__(cls, device: torch.types.Device) -> Any:
|
||||
raise NotImplementedError
|
||||
|
||||
class Event:
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
|
||||
raise NotImplementedError(
|
||||
"Event should be inherited from torch.Event, otherwise, it couldn't be captured by dynamo."
|
||||
)
|
||||
|
||||
class Stream:
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
|
||||
raise NotImplementedError(
|
||||
"Stream should be inherited from torch.Stream, otherwise, it couldn't be captured by dynamo."
|
||||
)
|
||||
|
||||
class Worker:
|
||||
"""
|
||||
Worker API to query device properties that will work in multi processing
|
||||
workers that cannot use the GPU APIs (due to processing fork() and
|
||||
initialization time issues). Properties are recorded in the main process
|
||||
before we fork the workers.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def set_device(device: int) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def current_device() -> int:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def get_device_properties(device: torch.types.Device = None) -> Any:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def current_device() -> int:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def set_device(device: torch.types.Device) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def maybe_exchange_device(device: int) -> int:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def exchange_device(device: int) -> int:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def device_count() -> int:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def is_available() -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def stream(stream: torch.Stream) -> Any:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def current_stream() -> torch.Stream:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def set_stream(stream: torch.Stream) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def _set_stream_by_id(stream_id: int, device_index: int, device_type: int) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def get_raw_stream(device_idx: int) -> int:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def synchronize(device: torch.types.Device = None) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def get_device_properties(cls, device: torch.types.Device = None) -> Any:
|
||||
return cls.Worker.get_device_properties(device)
|
||||
|
||||
@staticmethod
|
||||
def get_compute_capability(device: torch.types.Device = None) -> Any:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def is_bf16_supported(including_emulation: bool = False) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def is_dtype_supported(
|
||||
cls, dtype: torch.dtype, including_emulation: bool = False
|
||||
) -> bool:
|
||||
return dtype != torch.bfloat16 or cls.is_bf16_supported(including_emulation)
|
||||
|
||||
@staticmethod
|
||||
def memory_allocated(device: torch.types.Device = None) -> int:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def is_triton_capable(device: torch.types.Device = None) -> bool:
|
||||
"""
|
||||
Returns True if the device has Triton support, False otherwise, even if
|
||||
the appropriate Triton backend is not available.
|
||||
"""
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def raise_if_triton_unavailable(cls, device: torch.types.Device = None) -> None:
|
||||
"""
|
||||
Raises a `RuntimeError` with the appropriate human-readable instructions
|
||||
to resolve the issue if Triton is not available for the given device, or
|
||||
the default device if `device` is `None`.
|
||||
|
||||
The caller should ensure the presence of the 'triton' package before
|
||||
calling this method.
|
||||
"""
|
||||
if not cls.is_triton_capable():
|
||||
raise RuntimeError("This device is not capable of supporting Triton")
|
||||
|
||||
|
||||
class DeviceGuard:
|
||||
"""
|
||||
This class provides a context manager for device switching. This is a stripped
|
||||
down version of torch.{device_name}.device.
|
||||
|
||||
The context manager changes the current device to the given device index
|
||||
on entering the context and restores the original device on exiting.
|
||||
The device is switched using the provided device interface.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, device_interface: type[DeviceInterface], index: int | None
|
||||
) -> None:
|
||||
self.device_interface = device_interface
|
||||
self.idx = index
|
||||
self.prev_idx = -1
|
||||
|
||||
def __enter__(self) -> None:
|
||||
if self.idx is not None:
|
||||
self.prev_idx = self.device_interface.exchange_device(self.idx)
|
||||
|
||||
def __exit__(self, type: Any, value: Any, traceback: Any) -> Literal[False]:
|
||||
if self.idx is not None:
|
||||
self.idx = self.device_interface.maybe_exchange_device(self.prev_idx)
|
||||
return False
|
||||
|
||||
|
||||
class CudaInterface(DeviceInterface):
|
||||
device = torch.cuda.device # type: ignore[assignment]
|
||||
|
||||
# register Event and Stream class into the backend interface
|
||||
# make sure Event and Stream are implemented and inherited from the torch.Event and torch.Stream
|
||||
Event = torch.cuda.Event # type: ignore[assignment]
|
||||
Stream = torch.cuda.Stream # type: ignore[assignment]
|
||||
|
||||
# pyrefly: ignore [bad-override]
|
||||
class Worker:
|
||||
@staticmethod
|
||||
def set_device(device: int) -> None:
|
||||
caching_worker_current_devices["cuda"] = device
|
||||
|
||||
@staticmethod
|
||||
def current_device() -> int:
|
||||
if "cuda" in caching_worker_current_devices:
|
||||
return caching_worker_current_devices["cuda"]
|
||||
return torch.cuda.current_device()
|
||||
|
||||
@staticmethod
|
||||
def get_device_properties(device: torch.types.Device = None) -> Any:
|
||||
if device is not None:
|
||||
if isinstance(device, str):
|
||||
device = torch.device(device)
|
||||
assert device.type == "cuda"
|
||||
if isinstance(device, torch.device):
|
||||
device = device.index
|
||||
if device is None:
|
||||
device = CudaInterface.Worker.current_device()
|
||||
|
||||
if "cuda" not in caching_worker_device_properties:
|
||||
device_prop = [
|
||||
torch.cuda.get_device_properties(i)
|
||||
for i in range(torch.cuda.device_count())
|
||||
]
|
||||
caching_worker_device_properties["cuda"] = device_prop
|
||||
|
||||
return caching_worker_device_properties["cuda"][device]
|
||||
|
||||
current_device = staticmethod(torch.cuda.current_device)
|
||||
set_device = staticmethod(torch.cuda.set_device)
|
||||
device_count = staticmethod(torch.cuda.device_count)
|
||||
stream = staticmethod(torch.cuda.stream) # type: ignore[assignment]
|
||||
current_stream = staticmethod(torch.cuda.current_stream)
|
||||
set_stream = staticmethod(torch.cuda.set_stream) # type: ignore[assignment]
|
||||
_set_stream_by_id = staticmethod(torch.cuda._set_stream_by_id) # type: ignore[assignment]
|
||||
synchronize = staticmethod(torch.cuda.synchronize)
|
||||
get_device_properties = staticmethod(torch.cuda.get_device_properties) # type: ignore[assignment]
|
||||
get_raw_stream = staticmethod(get_cuda_stream) # type: ignore[assignment, arg-type]
|
||||
exchange_device = staticmethod(torch.cuda._exchange_device) # type: ignore[arg-type, has-type]
|
||||
maybe_exchange_device = staticmethod(torch.cuda._maybe_exchange_device) # type: ignore[arg-type, has-type]
|
||||
memory_allocated = staticmethod(torch.cuda.memory_allocated)
|
||||
is_bf16_supported = staticmethod(torch.cuda.is_bf16_supported) # type: ignore[arg-type]
|
||||
|
||||
# Can be mock patched by @patch decorator.
|
||||
@staticmethod
|
||||
def is_available() -> bool:
|
||||
return torch.cuda.is_available()
|
||||
|
||||
@staticmethod
|
||||
def get_compute_capability(device: torch.types.Device = None) -> int | str:
|
||||
if torch.version.hip is None:
|
||||
major, min = torch.cuda.get_device_capability(device)
|
||||
return major * 10 + min
|
||||
else:
|
||||
return torch.cuda.get_device_properties(device).gcnArchName.split(":", 1)[0]
|
||||
|
||||
@staticmethod
|
||||
def is_triton_capable(device: torch.types.Device = None) -> bool:
|
||||
return (
|
||||
torch.version.hip is not None
|
||||
or torch.cuda.get_device_properties(device).major >= 7
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def raise_if_triton_unavailable(device: torch.types.Device = None) -> None:
|
||||
from torch._inductor.exc import GPUTooOldForTriton
|
||||
|
||||
if not CudaInterface.is_triton_capable(device):
|
||||
device_props = torch.cuda.get_device_properties(device)
|
||||
raise GPUTooOldForTriton(device_props, inspect.currentframe())
|
||||
|
||||
import triton.backends
|
||||
|
||||
if torch.version.hip is not None:
|
||||
if "amd" not in triton.backends.backends:
|
||||
raise RuntimeError("triton not built with the 'amd' backend")
|
||||
elif "nvidia" not in triton.backends.backends:
|
||||
raise RuntimeError("triton not built with the 'nvidia' backend")
|
||||
|
||||
|
||||
get_mtia_stream: Callable[[int], int] | None
|
||||
if torch.mtia._is_compiled():
|
||||
from torch._C import _mtia_getCurrentRawStream as get_mtia_stream
|
||||
else:
|
||||
get_mtia_stream = None
|
||||
|
||||
|
||||
class MtiaInterface(DeviceInterface):
|
||||
device = torch.mtia.device # type: ignore[assignment]
|
||||
Event = torch.mtia.Event # type: ignore[assignment]
|
||||
Stream = torch.mtia.Stream # type: ignore[assignment]
|
||||
|
||||
# pyrefly: ignore [bad-override]
|
||||
class Worker:
|
||||
@staticmethod
|
||||
def set_device(device: int) -> None:
|
||||
caching_worker_current_devices["mtia"] = device
|
||||
|
||||
@staticmethod
|
||||
def current_device() -> int:
|
||||
if "mtia" in caching_worker_current_devices:
|
||||
return caching_worker_current_devices["mtia"]
|
||||
return torch.mtia.current_device()
|
||||
|
||||
@staticmethod
|
||||
def get_device_properties(device: torch.types.Device = None) -> Any:
|
||||
if device is not None:
|
||||
if isinstance(device, str):
|
||||
device = torch.device(device)
|
||||
assert device.type == "mtia"
|
||||
if isinstance(device, torch.device):
|
||||
device = device.index
|
||||
if device is None:
|
||||
device = MtiaInterface.Worker.current_device()
|
||||
|
||||
if "mtia" not in caching_worker_device_properties:
|
||||
device_prop = [
|
||||
torch.mtia.get_device_properties(i)
|
||||
for i in range(torch.mtia.device_count())
|
||||
]
|
||||
caching_worker_device_properties["mtia"] = device_prop
|
||||
|
||||
return caching_worker_device_properties["mtia"][device]
|
||||
|
||||
current_device = staticmethod(torch.mtia.current_device)
|
||||
set_device = staticmethod(torch.mtia.set_device) # type: ignore[assignment]
|
||||
device_count = staticmethod(torch.mtia.device_count)
|
||||
stream = staticmethod(torch.mtia.stream) # type: ignore[assignment]
|
||||
current_stream = staticmethod(torch.mtia.current_stream)
|
||||
set_stream = staticmethod(torch.mtia.set_stream) # type: ignore[assignment]
|
||||
_set_stream_by_id = staticmethod(torch.mtia._set_stream_by_id) # type: ignore[assignment]
|
||||
synchronize = staticmethod(torch.mtia.synchronize)
|
||||
get_device_properties = staticmethod(torch.mtia.get_device_properties) # type: ignore[assignment]
|
||||
get_raw_stream = staticmethod(get_mtia_stream) # type: ignore[assignment, arg-type]
|
||||
exchange_device = staticmethod(torch.mtia._exchange_device) # type: ignore[arg-type, has-type]
|
||||
maybe_exchange_device = staticmethod(torch.mtia._maybe_exchange_device) # type: ignore[arg-type, has-type]
|
||||
memory_allocated = staticmethod(torch.mtia.memory_allocated) # type: ignore[assignment]
|
||||
is_bf16_supported = staticmethod(torch.mtia.is_bf16_supported) # type: ignore[arg-type]
|
||||
|
||||
# Can be mock patched by @patch decorator.
|
||||
@staticmethod
|
||||
def is_available() -> bool:
|
||||
ret = torch.mtia.is_available()
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def get_compute_capability(device: torch.types.Device = None) -> Any:
|
||||
cc = torch.mtia.get_device_capability(device)
|
||||
return cc
|
||||
|
||||
@staticmethod
|
||||
def is_triton_capable(device: torch.types.Device = None) -> bool:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def raise_if_triton_unavailable(device: torch.types.Device = None) -> None:
|
||||
import triton.backends
|
||||
|
||||
if "mtia" not in triton.backends.backends:
|
||||
raise RuntimeError("triton not built with the 'mtia' backend")
|
||||
|
||||
|
||||
get_xpu_stream: Callable[[int], int] | None
|
||||
if torch.xpu._is_compiled():
|
||||
from torch._C import _xpu_getCurrentRawStream as get_xpu_stream
|
||||
else:
|
||||
get_xpu_stream = None
|
||||
|
||||
|
||||
class XpuInterface(DeviceInterface):
|
||||
device = torch.xpu.device # type: ignore[assignment]
|
||||
Event = torch.xpu.Event # type: ignore[assignment]
|
||||
Stream = torch.xpu.Stream # type: ignore[assignment]
|
||||
|
||||
# pyrefly: ignore [bad-override]
|
||||
class Worker:
|
||||
@staticmethod
|
||||
def set_device(device: int) -> None:
|
||||
caching_worker_current_devices["xpu"] = device
|
||||
|
||||
@staticmethod
|
||||
def current_device() -> int:
|
||||
if "xpu" in caching_worker_current_devices:
|
||||
return caching_worker_current_devices["xpu"]
|
||||
return torch.xpu.current_device()
|
||||
|
||||
@staticmethod
|
||||
def get_device_properties(device: torch.types.Device = None) -> Any:
|
||||
if device is not None:
|
||||
if isinstance(device, str):
|
||||
device = torch.device(device)
|
||||
assert device.type == "xpu"
|
||||
if isinstance(device, torch.device):
|
||||
device = device.index
|
||||
if device is None:
|
||||
device = XpuInterface.Worker.current_device()
|
||||
|
||||
if "xpu" not in caching_worker_device_properties:
|
||||
device_prop = [
|
||||
torch.xpu.get_device_properties(i)
|
||||
for i in range(torch.xpu.device_count())
|
||||
]
|
||||
caching_worker_device_properties["xpu"] = device_prop
|
||||
|
||||
return caching_worker_device_properties["xpu"][device]
|
||||
|
||||
current_device = staticmethod(torch.xpu.current_device)
|
||||
set_device = staticmethod(torch.xpu.set_device)
|
||||
device_count = staticmethod(torch.xpu.device_count) # type: ignore[has-type]
|
||||
stream = staticmethod(torch.xpu.stream) # type: ignore[assignment]
|
||||
current_stream = staticmethod(torch.xpu.current_stream)
|
||||
set_stream = staticmethod(torch.xpu.set_stream) # type: ignore[assignment]
|
||||
_set_stream_by_id = staticmethod(torch.xpu._set_stream_by_id) # type: ignore[assignment]
|
||||
synchronize = staticmethod(torch.xpu.synchronize)
|
||||
get_device_properties = staticmethod(torch.xpu.get_device_properties) # type: ignore[assignment]
|
||||
get_raw_stream = staticmethod(get_xpu_stream) # type: ignore[assignment, arg-type]
|
||||
exchange_device = staticmethod(torch.xpu._exchange_device) # type: ignore[arg-type, has-type]
|
||||
maybe_exchange_device = staticmethod(torch.xpu._maybe_exchange_device) # type: ignore[arg-type, has-type]
|
||||
memory_allocated = staticmethod(torch.xpu.memory_allocated)
|
||||
|
||||
# Can be mock patched by @patch decorator.
|
||||
@staticmethod
|
||||
def is_available() -> bool:
|
||||
return torch.xpu.is_available()
|
||||
|
||||
@staticmethod
|
||||
def get_compute_capability(device: torch.types.Device = None) -> Any:
|
||||
cc = torch.xpu.get_device_capability(device)
|
||||
return cc
|
||||
|
||||
@staticmethod
|
||||
def is_bf16_supported(including_emulation: bool = False) -> bool:
|
||||
return torch.xpu.is_bf16_supported()
|
||||
|
||||
@staticmethod
|
||||
def is_triton_capable(device: torch.types.Device = None) -> bool:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def raise_if_triton_unavailable(device: torch.types.Device = None) -> None:
|
||||
import triton.backends
|
||||
|
||||
if "intel" not in triton.backends.backends:
|
||||
raise RuntimeError("triton not built with the 'intel' backend")
|
||||
|
||||
|
||||
@dataclass
|
||||
class CpuDeviceProperties:
|
||||
multi_processor_count: int
|
||||
|
||||
|
||||
class CpuInterface(DeviceInterface):
|
||||
# pyrefly: ignore [bad-override]
|
||||
class Event(torch.Event):
|
||||
def __init__(self, enable_timing: bool = True) -> None:
|
||||
self.time = 0.0
|
||||
|
||||
def elapsed_time(self, other: Any) -> float:
|
||||
return (other.time - self.time) * 1000
|
||||
|
||||
def record(self, stream: Any = None) -> None:
|
||||
self.time = time.perf_counter()
|
||||
|
||||
# pyrefly: ignore [bad-override]
|
||||
class Worker:
|
||||
@staticmethod
|
||||
def get_device_properties(
|
||||
device: torch.types.Device = None,
|
||||
) -> CpuDeviceProperties:
|
||||
import multiprocessing
|
||||
|
||||
cpu_count = multiprocessing.cpu_count()
|
||||
return CpuDeviceProperties(cpu_count)
|
||||
|
||||
@staticmethod
|
||||
def is_available() -> bool:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def is_bf16_supported(including_emulation: bool = False) -> bool:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_compute_capability(device: torch.types.Device = None) -> str:
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def get_raw_stream(device_idx: Any) -> int:
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def current_device() -> int:
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def synchronize(device: torch.types.Device = None) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def is_triton_capable(device: torch.types.Device = None) -> bool:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def raise_if_triton_unavailable(device: torch.types.Device = None) -> None:
|
||||
import triton.backends
|
||||
|
||||
if "cpu" not in triton.backends.backends:
|
||||
raise RuntimeError("triton not built with the 'cpu' backend")
|
||||
|
||||
|
||||
class MpsInterface(DeviceInterface):
|
||||
@staticmethod
|
||||
def is_bf16_supported(including_emulation: bool = False) -> bool:
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def is_dtype_supported(
|
||||
cls, dtype: torch.dtype, including_emulation: bool = False
|
||||
) -> bool:
|
||||
if dtype in [torch.float64, torch.complex128]:
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def is_available() -> bool:
|
||||
return torch.backends.mps.is_available()
|
||||
|
||||
@staticmethod
|
||||
def current_device() -> int:
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def get_compute_capability(device: torch.types.Device = None) -> str:
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def synchronize(device: torch.types.Device = None) -> None:
|
||||
torch.mps.synchronize()
|
||||
|
||||
# pyrefly: ignore [bad-override]
|
||||
class Worker:
|
||||
@staticmethod
|
||||
def get_device_properties(device: torch.types.Device = None) -> Any:
|
||||
return namedtuple("MPSProperties", ["multi_processor_count"])(
|
||||
torch.backends.mps.get_core_count() # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def current_device() -> int:
|
||||
return 0
|
||||
|
||||
|
||||
class TpuInterface(DeviceInterface):
|
||||
@staticmethod
|
||||
def is_bf16_supported(including_emulation: bool = False) -> bool:
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def is_dtype_supported(
|
||||
cls, dtype: torch.dtype, including_emulation: bool = False
|
||||
) -> bool:
|
||||
return dtype not in (
|
||||
torch.float64,
|
||||
torch.complex32,
|
||||
torch.complex64,
|
||||
torch.complex128,
|
||||
torch.half,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_available() -> bool:
|
||||
return has_torch_tpu()
|
||||
|
||||
@staticmethod
|
||||
def current_device() -> int:
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def get_compute_capability(device: torch.types.Device = None) -> str:
|
||||
return ""
|
||||
|
||||
# pyrefly: ignore [bad-override]
|
||||
class Worker:
|
||||
@staticmethod
|
||||
def get_device_properties(device: torch.types.Device = None) -> Any:
|
||||
return namedtuple("TPUProperties", ["multi_processor_count"])(
|
||||
1 # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def current_device() -> int:
|
||||
return 0
|
||||
|
||||
|
||||
device_interfaces: dict[str, type[DeviceInterface]] = {}
|
||||
_device_initialized = False
|
||||
|
||||
|
||||
def register_interface_for_device(
|
||||
device: str | torch.device, device_interface: type[DeviceInterface]
|
||||
) -> None:
|
||||
if isinstance(device, torch.device):
|
||||
device = device.type
|
||||
device_interfaces[device] = device_interface
|
||||
|
||||
|
||||
def get_interface_for_device(device: str | torch.device) -> type[DeviceInterface]:
|
||||
if isinstance(device, torch.device):
|
||||
device = device.type
|
||||
if not _device_initialized:
|
||||
init_device_reg()
|
||||
if device in device_interfaces:
|
||||
return device_interfaces[device]
|
||||
raise NotImplementedError(f"No interface for device {device}")
|
||||
|
||||
|
||||
def get_registered_device_interfaces() -> Iterable[tuple[str, type[DeviceInterface]]]:
|
||||
if not _device_initialized:
|
||||
init_device_reg()
|
||||
return device_interfaces.items()
|
||||
|
||||
|
||||
def init_device_reg() -> None:
|
||||
global _device_initialized
|
||||
register_interface_for_device("cuda", CudaInterface)
|
||||
for i in range(torch.cuda.device_count()):
|
||||
register_interface_for_device(f"cuda:{i}", CudaInterface)
|
||||
|
||||
register_interface_for_device("xpu", XpuInterface)
|
||||
for i in range(torch.xpu.device_count()):
|
||||
register_interface_for_device(f"xpu:{i}", XpuInterface)
|
||||
|
||||
register_interface_for_device("mtia", MtiaInterface)
|
||||
for i in range(torch.mtia.device_count()):
|
||||
register_interface_for_device(f"mtia:{i}", MtiaInterface)
|
||||
|
||||
register_interface_for_device("cpu", CpuInterface)
|
||||
register_interface_for_device("mps", MpsInterface)
|
||||
register_interface_for_device("tpu", TpuInterface)
|
||||
|
||||
_device_initialized = True
|
||||
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
Manages process groups for distributed compilation in TorchDynamo.
|
||||
|
||||
This module handles the initialization and management of process groups used for
|
||||
distributed compilation. Key features:
|
||||
|
||||
- Lazy initialization of compilation process groups
|
||||
- Only creates groups when distributed mode is enabled and available
|
||||
- Integrates with compiler_collectives configuration setting
|
||||
- Provides a single global process group for compilation coordination
|
||||
|
||||
The process group is created only when needed and if the distributed environment
|
||||
is properly initialized, making it safe to import and use this module even in
|
||||
non-distributed scenarios.
|
||||
"""
|
||||
|
||||
import torch.distributed as dist
|
||||
|
||||
from . import config
|
||||
|
||||
|
||||
_COMPILE_PG: dist.ProcessGroup | None = None
|
||||
_GUARD_PG: dist.ProcessGroup | None = None
|
||||
|
||||
|
||||
def get_compile_pg() -> dist.ProcessGroup | None:
|
||||
if (
|
||||
config.enable_compiler_collectives
|
||||
and dist.is_available()
|
||||
and dist.is_initialized()
|
||||
):
|
||||
global _COMPILE_PG
|
||||
if _COMPILE_PG is None:
|
||||
# , timeout=datetime.timedelta(seconds=2)
|
||||
_COMPILE_PG = dist.distributed_c10d._new_group_with_tag(
|
||||
pg_tag="pt2_compile_pg"
|
||||
)
|
||||
return _COMPILE_PG
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# NB: Unlike get_compile_pg, this is only called when guard collectives were
|
||||
# explicitly requested
|
||||
def get_guard_pg() -> dist.ProcessGroup | None:
|
||||
if dist.is_available() and dist.is_initialized():
|
||||
global _GUARD_PG
|
||||
if _GUARD_PG is None:
|
||||
_GUARD_PG = dist.distributed_c10d._new_group_with_tag(pg_tag="pt2_guard_pg")
|
||||
return _GUARD_PG
|
||||
|
||||
return None
|
||||
@@ -0,0 +1,428 @@
|
||||
"""
|
||||
Dynamo Profiler - tracks where Dynamo spends time during compilation.
|
||||
|
||||
This module provides profiling functionality for Dynamo tracing, showing per-function
|
||||
cumtime (inclusive) and tottime (exclusive) in a cProfile-compatible format.
|
||||
The output can be visualized with tools like snakeviz.
|
||||
|
||||
Usage:
|
||||
# Enable via config (prints pstats output):
|
||||
torch._dynamo.config.dynamo_profiler = True
|
||||
|
||||
# Or save to file for snakeviz:
|
||||
torch._dynamo.config.dynamo_profiler = "/tmp/dynamo.prof"
|
||||
# Then: snakeviz /tmp/dynamo.prof
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import pstats
|
||||
|
||||
|
||||
@dataclass
|
||||
class FunctionTraceTiming:
|
||||
"""
|
||||
Timing data for a single inlined function trace.
|
||||
|
||||
Follows cProfile conventions:
|
||||
- cumtime: total time in function including all subcalls (inclusive)
|
||||
- tottime: time in function excluding subcalls (exclusive)
|
||||
- caller info: who called this function (for building call graph)
|
||||
"""
|
||||
|
||||
# Function identification
|
||||
func_name: str
|
||||
filename: str
|
||||
firstlineno: int
|
||||
# Timing data (in nanoseconds) - cProfile-style
|
||||
cumtime_ns: int # Inclusive time (includes subcalls)
|
||||
tottime_ns: int # Exclusive time (excludes subcalls)
|
||||
# Code stats (for comparing tracing overhead vs function complexity)
|
||||
bytecode_count: int
|
||||
# Nesting depth when this function was traced
|
||||
inline_depth: int
|
||||
# Caller information (for building call graph edges)
|
||||
caller_func_name: str | None = None
|
||||
caller_filename: str | None = None
|
||||
caller_firstlineno: int | None = None
|
||||
# Whether this is a primitive (non-recursive) call
|
||||
# A call is primitive if the function doesn't appear anywhere in the call stack
|
||||
is_primitive_call: bool = True
|
||||
# Full call stack at the time of this call (for proper snakeviz drill-down)
|
||||
# Each entry is (func_name, filename, firstlineno)
|
||||
call_stack: tuple[tuple[str, str, int], ...] = ()
|
||||
|
||||
# Backwards compatibility alias
|
||||
@property
|
||||
def trace_time_ns(self) -> int:
|
||||
return self.cumtime_ns
|
||||
|
||||
@property
|
||||
def trace_time_ms(self) -> float:
|
||||
return self.cumtime_ns / 1e6
|
||||
|
||||
@property
|
||||
def cumtime_ms(self) -> float:
|
||||
return self.cumtime_ns / 1e6
|
||||
|
||||
@property
|
||||
def tottime_ms(self) -> float:
|
||||
return self.tottime_ns / 1e6
|
||||
|
||||
@property
|
||||
def caller_key(self) -> tuple[str, int, str] | None:
|
||||
"""Return caller as a pstats-compatible key tuple."""
|
||||
if self.caller_func_name is not None:
|
||||
return (
|
||||
self.caller_filename or "",
|
||||
self.caller_firstlineno or 0,
|
||||
self.caller_func_name,
|
||||
)
|
||||
return None
|
||||
|
||||
@property
|
||||
def func_key(self) -> tuple[str, int, str]:
|
||||
"""Return this function as a pstats-compatible key tuple."""
|
||||
return (self.filename, self.firstlineno, self.func_name)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"FunctionTraceTiming({self.func_name} at {self.filename}:{self.firstlineno}, "
|
||||
f"cumtime={self.cumtime_ms:.2f}ms, tottime={self.tottime_ms:.2f}ms, "
|
||||
f"bytecode={self.bytecode_count}, depth={self.inline_depth})"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProfilerStackEntry:
|
||||
"""Stack entry for tracking function timing in the Dynamo profiler."""
|
||||
|
||||
func_name: str
|
||||
filename: str
|
||||
firstlineno: int
|
||||
start_time_ns: int
|
||||
child_time_ns: int # Accumulated time spent in traced children
|
||||
is_primitive_call: bool = True # Whether this is a non-recursive call
|
||||
|
||||
|
||||
class DynamoProfilerState:
|
||||
"""State for Dynamo profiler tracking function trace timings."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.timings: list[FunctionTraceTiming] = []
|
||||
self.stack: list[ProfilerStackEntry] = []
|
||||
|
||||
def record_timing(self, timing: FunctionTraceTiming) -> None:
|
||||
"""Record timing data for a traced function."""
|
||||
self.timings.append(timing)
|
||||
|
||||
def get_timings(self) -> list[FunctionTraceTiming]:
|
||||
"""Get all recorded timings."""
|
||||
return self.timings
|
||||
|
||||
def push(
|
||||
self, func_name: str, filename: str, firstlineno: int, start_time_ns: int
|
||||
) -> None:
|
||||
"""Push a new entry onto the timing stack."""
|
||||
# Check if this function already exists in the stack (indirect recursion)
|
||||
is_primitive = not any(
|
||||
entry.func_name == func_name
|
||||
and entry.filename == filename
|
||||
and entry.firstlineno == firstlineno
|
||||
for entry in self.stack
|
||||
)
|
||||
self.stack.append(
|
||||
ProfilerStackEntry(
|
||||
func_name=func_name,
|
||||
filename=filename,
|
||||
firstlineno=firstlineno,
|
||||
start_time_ns=start_time_ns,
|
||||
child_time_ns=0,
|
||||
is_primitive_call=is_primitive,
|
||||
)
|
||||
)
|
||||
|
||||
def pop(self) -> ProfilerStackEntry | None:
|
||||
"""Pop the top entry from the timing stack."""
|
||||
if self.stack:
|
||||
return self.stack.pop()
|
||||
return None
|
||||
|
||||
def add_child_time(self, child_cumtime_ns: int) -> None:
|
||||
"""Add the child's cumulative time to the parent's child_time accumulator."""
|
||||
if self.stack:
|
||||
self.stack[-1].child_time_ns += child_cumtime_ns
|
||||
|
||||
def get_current_caller(self) -> tuple[str, str, int] | None:
|
||||
"""Get the current caller (top of stack) as (func_name, filename, firstlineno)."""
|
||||
if self.stack:
|
||||
entry = self.stack[-1]
|
||||
return (entry.func_name, entry.filename, entry.firstlineno)
|
||||
return None
|
||||
|
||||
def get_call_stack(self) -> tuple[tuple[str, str, int], ...]:
|
||||
"""Get the full current call stack as tuple of (func_name, filename, firstlineno)."""
|
||||
return tuple(
|
||||
(entry.func_name, entry.filename, entry.firstlineno) for entry in self.stack
|
||||
)
|
||||
|
||||
def generate_pstats(
|
||||
self, output_file: str | None = None, print_raw: bool = False
|
||||
) -> pstats.Stats:
|
||||
"""Generate pstats.Stats object from recorded timings.
|
||||
|
||||
Args:
|
||||
output_file: Optional file path to save the stats.
|
||||
print_raw: If True, print raw aggregate timings before returning.
|
||||
"""
|
||||
import cProfile
|
||||
import io
|
||||
import logging
|
||||
import pstats
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Aggregate by (filename, lineno, func_name)
|
||||
aggregated: dict[tuple[str, int, str], dict[str, Any]] = {}
|
||||
# caller_edges[callee_key][caller_key] -> edge stats
|
||||
caller_edges: dict[
|
||||
tuple[str, int, str], dict[tuple[str, int, str], dict[str, Any]]
|
||||
] = {}
|
||||
|
||||
for t in self.timings:
|
||||
key = (t.filename, t.firstlineno, t.func_name)
|
||||
|
||||
if key not in aggregated:
|
||||
aggregated[key] = {
|
||||
"ncalls": 0,
|
||||
"pcalls": 0,
|
||||
"tottime": 0.0,
|
||||
"cumtime": 0.0,
|
||||
}
|
||||
caller_edges[key] = {}
|
||||
|
||||
agg = aggregated[key]
|
||||
agg["ncalls"] += 1
|
||||
agg["tottime"] += t.tottime_ns / 1e9
|
||||
|
||||
if t.is_primitive_call:
|
||||
agg["pcalls"] += 1
|
||||
agg["cumtime"] += t.cumtime_ns / 1e9
|
||||
|
||||
# Build caller edge
|
||||
if t.caller_filename is not None:
|
||||
caller_key = (
|
||||
t.caller_filename,
|
||||
t.caller_firstlineno or 0,
|
||||
t.caller_func_name or "",
|
||||
)
|
||||
if caller_key not in caller_edges[key]:
|
||||
caller_edges[key][caller_key] = {
|
||||
"ncalls": 0,
|
||||
"pcalls": 0,
|
||||
"tottime": 0.0,
|
||||
"cumtime": 0.0,
|
||||
}
|
||||
edge = caller_edges[key][caller_key]
|
||||
edge["ncalls"] += 1
|
||||
edge["tottime"] += t.tottime_ns / 1e9
|
||||
# Always add cumtime to edges for visualization (gprof2dot)
|
||||
# Function-level cumtime is already correct (only primitive calls)
|
||||
edge["cumtime"] += t.cumtime_ns / 1e9
|
||||
if t.is_primitive_call:
|
||||
edge["pcalls"] += 1
|
||||
|
||||
if print_raw:
|
||||
sorted_items = sorted(
|
||||
aggregated.items(), key=lambda x: x[1]["cumtime"], reverse=True
|
||||
)
|
||||
print("\n=== Aggregate Timings (raw) ===")
|
||||
print(
|
||||
f"{'ncalls':>8} {'pcalls':>8} {'tottime':>12} {'cumtime':>12} function"
|
||||
)
|
||||
print("-" * 80)
|
||||
total_cumtime = 0.0
|
||||
total_tottime = 0.0
|
||||
for (filename, lineno, func_name), agg in sorted_items:
|
||||
ncalls = agg["ncalls"]
|
||||
pcalls = agg["pcalls"]
|
||||
tottime = agg["tottime"] * 1000 # convert to ms
|
||||
cumtime = agg["cumtime"] * 1000
|
||||
total_cumtime += cumtime
|
||||
total_tottime += tottime
|
||||
short_file = filename.split("/")[-1] if "/" in filename else filename
|
||||
print(
|
||||
f"{ncalls:>8} {pcalls:>8} {tottime:>10.2f}ms {cumtime:>10.2f}ms "
|
||||
f"{func_name} ({short_file}:{lineno})"
|
||||
)
|
||||
print("-" * 80)
|
||||
print(
|
||||
f"Total timings: {len(self.timings)}, unique functions: {len(aggregated)}"
|
||||
)
|
||||
print(
|
||||
f"Sum tottime: {total_tottime:.2f}ms, Sum cumtime: {total_cumtime:.2f}ms"
|
||||
)
|
||||
|
||||
# Ensure caller-only functions have a top-level entry.
|
||||
# gprof2dot expects every function referenced as a caller to also
|
||||
# exist as a top-level entry in the stats dict with timing data.
|
||||
for key in list(caller_edges.keys()):
|
||||
for caller_key in caller_edges[key]:
|
||||
if caller_key not in aggregated:
|
||||
aggregated[caller_key] = {
|
||||
"ncalls": 0,
|
||||
"pcalls": 0,
|
||||
"tottime": 0.0,
|
||||
"cumtime": 0.0,
|
||||
}
|
||||
caller_edges[caller_key] = {}
|
||||
|
||||
# Build the stats dict in pstats format
|
||||
stats_dict: dict[
|
||||
tuple[str, int, str], tuple[int, int, float, float, dict[Any, Any]]
|
||||
] = {}
|
||||
|
||||
for key, agg in aggregated.items():
|
||||
callers: dict[tuple[str, int, str], tuple[int, int, float, float]] = {}
|
||||
for caller_key, edge in caller_edges[key].items():
|
||||
callers[caller_key] = (
|
||||
edge["ncalls"],
|
||||
edge["pcalls"],
|
||||
edge["tottime"],
|
||||
edge["cumtime"],
|
||||
)
|
||||
|
||||
stats_dict[key] = (
|
||||
agg["pcalls"],
|
||||
agg["ncalls"],
|
||||
agg["tottime"],
|
||||
agg["cumtime"],
|
||||
callers,
|
||||
)
|
||||
|
||||
# Create a pstats.Stats object
|
||||
dummy_profile = cProfile.Profile()
|
||||
dummy_profile.enable()
|
||||
dummy_profile.disable()
|
||||
stats = pstats.Stats(dummy_profile, stream=io.StringIO())
|
||||
|
||||
stats.stats = stats_dict # type: ignore[attr-defined]
|
||||
stats.total_calls = sum(s[1] for s in stats_dict.values()) # type: ignore[attr-defined]
|
||||
stats.prim_calls = sum(s[0] for s in stats_dict.values()) # type: ignore[attr-defined]
|
||||
stats.total_tt = sum(s[2] for s in stats_dict.values()) # type: ignore[attr-defined]
|
||||
|
||||
if output_file:
|
||||
stats.dump_stats(output_file)
|
||||
log.info(
|
||||
"Saved pstats to %s. Visualize with: snakeviz %s",
|
||||
output_file,
|
||||
output_file,
|
||||
)
|
||||
|
||||
return stats
|
||||
|
||||
def generate_svg(
|
||||
self, profile_file: str, svg_file: str | None = None
|
||||
) -> str | None:
|
||||
"""Generate an SVG call graph from a profile file using gprof2dot and graphviz.
|
||||
|
||||
Args:
|
||||
profile_file: Path to the pstats profile file.
|
||||
svg_file: Optional path for the output SVG. If not provided, uses
|
||||
profile_file with .svg extension.
|
||||
|
||||
Returns:
|
||||
Path to the generated SVG file, or None if generation failed.
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
if not shutil.which("gprof2dot"):
|
||||
print("gprof2dot not found. Install with: pip install gprof2dot")
|
||||
return None
|
||||
|
||||
if not shutil.which("dot"):
|
||||
print("graphviz 'dot' not found. Install graphviz package.")
|
||||
return None
|
||||
|
||||
if svg_file is None:
|
||||
svg_file = profile_file.rsplit(".", 1)[0] + ".svg"
|
||||
|
||||
try:
|
||||
# gprof2dot -f pstats profile.prof | dot -Tsvg -o profile.svg
|
||||
gprof2dot = subprocess.Popen(
|
||||
[
|
||||
"gprof2dot",
|
||||
"-f",
|
||||
"pstats",
|
||||
"--node-label=total-time-percentage",
|
||||
"--node-label=self-time-percentage",
|
||||
"--node-label=total-time",
|
||||
profile_file,
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
dot = subprocess.Popen(
|
||||
["dot", "-Tsvg", "-o", svg_file],
|
||||
stdin=gprof2dot.stdout,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
gprof2dot.stdout.close() # type: ignore[union-attr]
|
||||
_, dot_err = dot.communicate()
|
||||
_, gprof2dot_err = gprof2dot.communicate()
|
||||
|
||||
if gprof2dot.returncode != 0:
|
||||
print(
|
||||
f"gprof2dot failed: {gprof2dot_err.decode()}" # noqa: B950
|
||||
)
|
||||
return None
|
||||
|
||||
if dot.returncode != 0:
|
||||
print(f"graphviz dot failed: {dot_err.decode()}")
|
||||
return None
|
||||
|
||||
if not os.path.isfile(svg_file):
|
||||
print(f"SVG file was not created: {svg_file}")
|
||||
return None
|
||||
|
||||
print(f"SVG call graph saved to: {svg_file}")
|
||||
return svg_file
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to generate SVG: {e}")
|
||||
return None
|
||||
|
||||
def dump_stats(
|
||||
self, output_file: str | None = None, generate_svg: bool = True
|
||||
) -> None:
|
||||
"""Print profiler stats to stdout and optionally save to file.
|
||||
|
||||
Args:
|
||||
output_file: Optional path to save the pstats profile.
|
||||
generate_svg: If True and output_file is provided, also generate an SVG
|
||||
call graph using gprof2dot and graphviz.
|
||||
"""
|
||||
import sys
|
||||
|
||||
if not self.timings:
|
||||
return
|
||||
|
||||
stats = self.generate_pstats(output_file, print_raw=True)
|
||||
print("\n=== Dynamo Profiler (pstats) ===")
|
||||
stats.stream = sys.stdout # type: ignore[attr-defined]
|
||||
stats.sort_stats("cumulative").print_stats()
|
||||
|
||||
if output_file:
|
||||
print(f"\nProfile saved to: {output_file}")
|
||||
print(f"Visualize with: snakeviz {output_file}")
|
||||
|
||||
if generate_svg:
|
||||
self.generate_svg(output_file)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,901 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
"""Exception handling and error reporting for TorchDynamo.
|
||||
|
||||
This module provides a comprehensive set of exception classes and utilities for error
|
||||
handling in TorchDynamo. It includes:
|
||||
|
||||
Base Exceptions:
|
||||
- TorchDynamoException: Base class for all TorchDynamo-specific exceptions
|
||||
- Various specialized subclasses for different error scenarios
|
||||
|
||||
User Error Handling:
|
||||
- UserError: Exceptions for user-facing errors in TorchDynamo usage
|
||||
- UserErrorType: Enumeration of different categories of user errors
|
||||
- Formatted error messages with debugging information
|
||||
|
||||
Observed Exceptions:
|
||||
- Classes for handling exceptions observed during tracing
|
||||
- Special handling for StopIteration, LookupError, etc.
|
||||
- Exception state management during compilation
|
||||
|
||||
Error Formatting:
|
||||
- Stack trace filtering and formatting
|
||||
- Error message augmentation
|
||||
- Debugging utilities for error reporting
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import textwrap
|
||||
import typing
|
||||
from enum import auto, Enum
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from traceback import extract_stack, format_exc, format_list, FrameSummary, StackSummary
|
||||
from typing import Any, NoReturn, TYPE_CHECKING
|
||||
|
||||
import torch._guards
|
||||
from torch._utils_internal import get_file_path_2
|
||||
|
||||
from . import config
|
||||
from .utils import counters
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import types
|
||||
|
||||
from torch._dynamo.variables import VariableTracker
|
||||
from torch._guards import CompileId
|
||||
|
||||
from .output_graph import DynamoTracerOutput
|
||||
from .symbolic_convert import InstructionTranslatorBase
|
||||
from .types import DynamoFrameType, FrameExecStrategy
|
||||
|
||||
|
||||
def exportdb_error_message(case_name: str) -> str:
|
||||
return (
|
||||
"For more information about this error, see: "
|
||||
+ "https://pytorch.org/docs/main/generated/exportdb/index.html#"
|
||||
+ case_name.replace("_", "-")
|
||||
)
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
graph_breaks_log = torch._logging.getArtifactLogger(__name__, "graph_breaks")
|
||||
|
||||
|
||||
class TorchDynamoException(RuntimeError):
|
||||
"""Base exception class for all TorchDynamo-specific exceptions.
|
||||
|
||||
Attributes:
|
||||
_torch_dynamo_tracer_output: Optional tracer output attached to the exception
|
||||
frame_exec_strategy: Optional frame execution strategy to control how convert_frame
|
||||
should handle this exception. When set, convert_frame will use this strategy
|
||||
instead of the default behavior. This allows exceptions to signal specific
|
||||
execution strategies (e.g., SKIP, RUN_ONLY) without requiring separate
|
||||
exception types for control flow.
|
||||
"""
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self._torch_dynamo_tracer_output: DynamoTracerOutput | None = None
|
||||
self.frame_exec_strategy: FrameExecStrategy | None = None
|
||||
|
||||
|
||||
class InternalTorchDynamoError(TorchDynamoException):
|
||||
pass
|
||||
|
||||
|
||||
class ResumePrologueTracingError(TorchDynamoException):
|
||||
pass
|
||||
|
||||
|
||||
class RestartAnalysis(TorchDynamoException):
|
||||
restart_reason: str | None
|
||||
|
||||
def __init__(self, *args: Any, restart_reason: str | None = None) -> None:
|
||||
self.restart_reason = restart_reason
|
||||
super().__init__(*args)
|
||||
|
||||
|
||||
class SpeculationRestartAnalysis(RestartAnalysis):
|
||||
pass
|
||||
|
||||
|
||||
class AutogradGradRestartAnalysis(RestartAnalysis):
|
||||
"""Raised when autograd.grad consumed grad_fns that are returned.
|
||||
|
||||
On restart, autograd.grad will graph break instead of being traced.
|
||||
"""
|
||||
|
||||
|
||||
class RequiresGradRestartAnalysis(RestartAnalysis):
|
||||
"""Raised when a source-less requires_grad_() intermediate leaks as output.
|
||||
|
||||
On restart, requires_grad_() will graph break instead of being traced,
|
||||
preserving partial acceleration for code before the call.
|
||||
"""
|
||||
|
||||
|
||||
class UnspecializeRestartAnalysis(RestartAnalysis):
|
||||
pass
|
||||
|
||||
|
||||
class CompileCollectiveRestartAnalysis(RestartAnalysis):
|
||||
pass
|
||||
|
||||
|
||||
class TensorifyScalarRestartAnalysis(RestartAnalysis):
|
||||
pass
|
||||
|
||||
|
||||
# Used (primarily for backends) to skip tracing the current frame
|
||||
# and all future invocations of it.
|
||||
# NOTE: this does NOT cause a graph break, and thus no graph break messages
|
||||
# will be issued!
|
||||
class SkipFrame(TorchDynamoException):
|
||||
pass
|
||||
|
||||
|
||||
class TorchRuntimeError(TorchDynamoException):
|
||||
def __init__(self, msg: str, real_stack: StackSummary | None = None) -> None:
|
||||
super().__init__(msg)
|
||||
self.msg = msg
|
||||
self.real_stack = (
|
||||
real_stack
|
||||
if real_stack is not None
|
||||
else torch._guards.TracingContext.extract_stack()
|
||||
)
|
||||
|
||||
|
||||
class InvalidBackend(TorchDynamoException):
|
||||
def __init__(self, name: str) -> None:
|
||||
super().__init__(
|
||||
f"Invalid backend: {name!r}, see `torch._dynamo.list_backends()` for available backends."
|
||||
)
|
||||
|
||||
|
||||
class ResetRequired(TorchDynamoException):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
Must call `torch._dynamo.reset()` before changing backends. Detected two calls to
|
||||
`torch.compile()` with a different backend compiler arguments.
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ShortenTraceback(TorchDynamoException):
|
||||
def __init__(
|
||||
self, *args: Any, first_useful_frame: types.FrameType | None, **kwargs: Any
|
||||
) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.first_useful_frame = first_useful_frame
|
||||
|
||||
def remove_dynamo_frames(self) -> typing.Self:
|
||||
tb = self.__traceback__
|
||||
if self.first_useful_frame is None or tb is None or config.verbose:
|
||||
return self
|
||||
while tb.tb_frame is not self.first_useful_frame:
|
||||
tb = tb.tb_next
|
||||
assert tb is not None, "internal error, please report a bug"
|
||||
return self.with_traceback(tb)
|
||||
|
||||
|
||||
class BackendCompilerFailed(ShortenTraceback):
|
||||
def __init__(
|
||||
self,
|
||||
backend_fn: Any,
|
||||
inner_exception: Exception,
|
||||
first_useful_frame: types.FrameType | None,
|
||||
) -> None:
|
||||
self.backend_name = getattr(backend_fn, "__name__", "?")
|
||||
self.inner_exception = inner_exception
|
||||
msg = f"backend={self.backend_name!r} raised:\n{type(inner_exception).__name__}: {inner_exception}"
|
||||
super().__init__(msg, first_useful_frame=first_useful_frame)
|
||||
|
||||
|
||||
# NOTE: important invariant! Almost any exception handler that handles Unsupported
|
||||
# should NOT suppress the exception if skip_frame is set!
|
||||
# skip_frame is used by symbolic_convert.py to bubble up Unsupported exceptions to convert_frame to cause
|
||||
# a frame skip. Once the Unsupported exn is in convert_frame, we will always skip, so skip_frame
|
||||
# won't be checked
|
||||
class Unsupported(TorchDynamoException):
|
||||
def __init__(
|
||||
self,
|
||||
msg: str,
|
||||
# TODO: make this argument required once we remove Unsupported subclasses
|
||||
gb_type: str = "",
|
||||
skip_frame: bool = False,
|
||||
*,
|
||||
case_name: str | None = None,
|
||||
real_stack: StackSummary | None = None,
|
||||
) -> None:
|
||||
super().__init__(msg)
|
||||
if not real_stack:
|
||||
real_stack = torch._guards.TracingContext.extract_stack()
|
||||
self.real_stack = real_stack
|
||||
self.msg = msg
|
||||
self.skip_frame = skip_frame
|
||||
self.category: str | None = None
|
||||
self.add_to_stats()
|
||||
self.gb_type: str | None = gb_type
|
||||
self.logged = False
|
||||
|
||||
def remove_from_stats(self) -> None:
|
||||
assert self.category is not None
|
||||
counters[self.category][self.msg] -= 1
|
||||
if counters[self.category][self.msg] <= 0:
|
||||
del counters[self.category][self.msg]
|
||||
|
||||
def add_to_stats(self, category: str = "unimplemented") -> None:
|
||||
self.category = category
|
||||
counters[category][self.msg] += 1
|
||||
|
||||
|
||||
class UnknownPropertiesDuringBackwardTrace(TorchDynamoException):
|
||||
pass
|
||||
|
||||
|
||||
class RecompileError(TorchDynamoException):
|
||||
pass
|
||||
|
||||
|
||||
class InfiniteGeneratorError(TorchDynamoException):
|
||||
# Raised when the number of yielded values is greater than MAX_ITERATOR_LIMIT
|
||||
pass
|
||||
|
||||
|
||||
class CondOpArgsMismatchError(TorchDynamoException):
|
||||
"""
|
||||
Internal error from cond() due to arguments mismatch.
|
||||
"""
|
||||
|
||||
|
||||
class UserErrorType(Enum):
|
||||
DYNAMIC_CONTROL_FLOW = auto()
|
||||
ANTI_PATTERN = auto()
|
||||
STANDARD_LIBRARY = auto()
|
||||
CONSTRAINT_VIOLATION = auto()
|
||||
DYNAMIC_DIM = auto()
|
||||
INVALID_INPUT = auto()
|
||||
INVALID_OUTPUT = auto()
|
||||
UNSUPPORTED_ALIASED_MUTATED_DYNAMIC_INPUTS = auto()
|
||||
|
||||
|
||||
class UserError(TorchDynamoException):
|
||||
def __init__(
|
||||
self, error_type: UserErrorType, msg: str, case_name: str | None = None
|
||||
) -> None:
|
||||
"""
|
||||
Type of errors that would be valid in Eager, but not supported in TorchDynamo.
|
||||
The error message should tell user about next actions.
|
||||
|
||||
error_type: Type of user error
|
||||
msg: Actionable error message
|
||||
case_name: (Optional) Unique name (snake case) for the usage example in exportdb.
|
||||
"""
|
||||
if case_name is not None:
|
||||
assert isinstance(case_name, str)
|
||||
if msg.endswith("."):
|
||||
msg += " "
|
||||
else:
|
||||
msg += "\n"
|
||||
msg += exportdb_error_message(case_name)
|
||||
super().__init__(msg)
|
||||
self.real_stack = torch._guards.TracingContext.extract_stack()
|
||||
self.skip_frame = False
|
||||
self.logged = False
|
||||
self.error_type = error_type
|
||||
self.msg = msg
|
||||
self.message = msg
|
||||
|
||||
|
||||
# debug exception thrown when tracing torch._dynamo.step_unsupported()
|
||||
class StepUnsupported(TorchDynamoException):
|
||||
def __init__(self, msg: str, real_stack: StackSummary | None = None) -> None:
|
||||
super().__init__(msg)
|
||||
self.msg = msg
|
||||
if not real_stack:
|
||||
real_stack = torch._guards.TracingContext.extract_stack()
|
||||
self.real_stack = real_stack
|
||||
self.logged = False
|
||||
|
||||
|
||||
class UnsafeScriptObjectError(TorchDynamoException):
|
||||
pass
|
||||
|
||||
|
||||
class UncapturedHigherOrderOpError(TorchDynamoException):
|
||||
def __init__(self, msg: str, real_stack: StackSummary | None = None) -> None:
|
||||
super().__init__(msg)
|
||||
self.msg = msg
|
||||
self.real_stack = (
|
||||
real_stack
|
||||
if real_stack is not None
|
||||
else torch._guards.TracingContext.extract_stack()
|
||||
)
|
||||
|
||||
|
||||
# TODO: I'm a little uncertain about what error classification we should have
|
||||
# for this. This is potentially a user error, but regressions in
|
||||
# specialization in PyTorch proper could also trigger this problem
|
||||
class FailOnRecompileLimitHit(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PackageError(TorchDynamoException):
|
||||
pass
|
||||
|
||||
|
||||
class ObservedException(TorchDynamoException):
|
||||
# An exception observed during the tracing. This exception is used by Dynamo to handle exceptions.
|
||||
def __init__(
|
||||
self, *args: Any, real_stack: StackSummary | None = None, **kwargs: Any
|
||||
) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.real_stack: StackSummary = (
|
||||
real_stack
|
||||
if real_stack is not None
|
||||
else torch._guards.TracingContext.extract_stack()
|
||||
)
|
||||
|
||||
|
||||
class ObservedUserStopIteration(ObservedException):
|
||||
# An UserStopIteration exception observed during the Dynamo tracing (e.g Dynamo tracing __next__)
|
||||
value: Any | None
|
||||
|
||||
# Reference `StopIteration_init` in CPython
|
||||
# https://github.com/python/cpython/blob/3.11/Objects/exceptions.c#L568-L584
|
||||
def __init__(
|
||||
self, *args: Any, real_stack: StackSummary | None = None, **kwargs: Any
|
||||
) -> None:
|
||||
super().__init__("unhandled `raise StopIteration`", real_stack=real_stack)
|
||||
if len(args) > 0:
|
||||
self.value = args[0]
|
||||
else:
|
||||
self.value = None
|
||||
|
||||
|
||||
class ObservedLookupError(ObservedException):
|
||||
# A LookupError exception to be raised from inside Dynamo tracing. This can happen on __getitem__
|
||||
pass
|
||||
|
||||
|
||||
class ObservedIndexError(ObservedLookupError):
|
||||
# An IndexError exception to be raised from inside Dynamo tracing. This can happen on list __getitem__
|
||||
pass
|
||||
|
||||
|
||||
class ObservedKeyError(ObservedLookupError):
|
||||
# A KeyError exception to be raised from inside Dynamo tracing. This can happen on dict __getitem__
|
||||
pass
|
||||
|
||||
|
||||
class ObservedGeneratorExit(ObservedException):
|
||||
pass
|
||||
|
||||
|
||||
class ObservedAttributeError(ObservedException):
|
||||
# An AttributeError exception to be raised from inside Dynamo tracing. This can happen on user defined object __getattr__
|
||||
pass
|
||||
|
||||
|
||||
class ObservedRuntimeError(ObservedException):
|
||||
# A RuntimeError exception to be raised from inside Dynamo tracing. This can happen on generator.throw(..) method
|
||||
pass
|
||||
|
||||
|
||||
class ObservedNotImplementedError(ObservedException):
|
||||
pass
|
||||
|
||||
|
||||
class ObservedTypeError(ObservedException):
|
||||
# A TypeError exception to be raised from inside Dynamo tracing. This can happen on generator.send(..) method
|
||||
pass
|
||||
|
||||
|
||||
observed_exception_map = {
|
||||
StopIteration: ObservedUserStopIteration,
|
||||
LookupError: ObservedLookupError,
|
||||
IndexError: ObservedIndexError,
|
||||
GeneratorExit: ObservedGeneratorExit,
|
||||
KeyError: ObservedKeyError,
|
||||
AttributeError: ObservedAttributeError,
|
||||
RuntimeError: ObservedRuntimeError,
|
||||
NotImplementedError: ObservedNotImplementedError,
|
||||
TypeError: ObservedTypeError,
|
||||
}
|
||||
|
||||
|
||||
def get_dynamo_observed_exception(exc_type: type[Exception]) -> type[ObservedException]:
|
||||
if exc_type not in observed_exception_map:
|
||||
name = getattr(exc_type, "__name__", str(exc_type))
|
||||
observed_exception_map[exc_type] = type( # type: ignore[assignment]
|
||||
f"Observed{name}Error", (ObservedException,), {}
|
||||
)
|
||||
# pyrefly: ignore [bad-index]
|
||||
return observed_exception_map[exc_type]
|
||||
|
||||
|
||||
def raise_observed_exception(
|
||||
exc_type: type[Exception],
|
||||
tx: InstructionTranslatorBase,
|
||||
*,
|
||||
args: list[VariableTracker] | list[str] | None = None,
|
||||
kwargs: dict[str, VariableTracker] | None = None,
|
||||
) -> NoReturn:
|
||||
from .symbolic_convert import ExceptionVals
|
||||
from .variables.builder import SourcelessBuilder
|
||||
|
||||
if args:
|
||||
args_ = [
|
||||
SourcelessBuilder.create(tx, arg) if isinstance(arg, str) else arg
|
||||
for arg in args
|
||||
]
|
||||
else:
|
||||
args_: list[VariableTracker] = []
|
||||
|
||||
# CPython here raises an exception. Since there is no python code, we have to manually setup the exception
|
||||
# stack and raise the exception.
|
||||
exception_vt = SourcelessBuilder.create(tx, exc_type).call_function(
|
||||
tx, args_, kwargs or {}
|
||||
)
|
||||
assert isinstance(exception_vt, ExceptionVals)
|
||||
tx._attach_traceback_to_exception(exception_vt)
|
||||
tx.exn_vt_stack.set_current_exception(exception_vt) # type: ignore[arg-type]
|
||||
raised_exc = get_dynamo_observed_exception(exc_type)
|
||||
# Store the original exception arguments for better error messages
|
||||
if args:
|
||||
raise raised_exc(*args_)
|
||||
raise raised_exc
|
||||
|
||||
|
||||
def raise_type_error(tx: InstructionTranslatorBase, msg: str) -> NoReturn:
|
||||
"""Raise a TypeError as an observed exception during tracing."""
|
||||
raise_observed_exception(TypeError, tx, args=[msg])
|
||||
|
||||
|
||||
def handle_observed_exception(tx: Any) -> None:
|
||||
# This is essentially exception handling code, equivalent of this pseudo code
|
||||
#
|
||||
# try:
|
||||
# ... somebody raising StopIteration
|
||||
# except StopIteration
|
||||
# pass
|
||||
#
|
||||
# If this was going through the python code, we would have called exception_handler method, but FOR_ITER
|
||||
# handles the exception completely in CPython. For example for 3.11, the resulting bytecode is
|
||||
#
|
||||
#
|
||||
# 6 46 LOAD_GLOBAL 2 (StopIteration)
|
||||
# 58 RAISE_VARARGS 1
|
||||
# >> 60 PUSH_EXC_INFO
|
||||
|
||||
# 7 62 LOAD_GLOBAL 2 (StopIteration)
|
||||
# 74 CHECK_EXC_MATCH
|
||||
# 76 POP_JUMP_FORWARD_IF_FALSE 3 (to 84)
|
||||
# 78 POP_TOP
|
||||
|
||||
# 8 80 POP_EXCEPT
|
||||
#
|
||||
|
||||
# Fortunately this translates to a simple pop from the exn_vt_stack
|
||||
tx.exn_vt_stack.clear_current_exception()
|
||||
|
||||
|
||||
# These exceptions are ok to fallback to eager/graph_break.
|
||||
exceptions_allowed_to_be_fallback = (
|
||||
torch._subclasses.fake_tensor.DataDependentOutputException,
|
||||
torch._subclasses.fake_tensor.DynamicOutputShapeException,
|
||||
torch._subclasses.fake_tensor.UnsupportedOperatorException,
|
||||
torch._subclasses.fake_tensor.UnsupportedFakeTensorException,
|
||||
torch._subclasses.fake_tensor.UnsupportedMutationAliasingException,
|
||||
)
|
||||
|
||||
|
||||
def unimplemented_with_warning(
|
||||
e: Exception,
|
||||
code: types.CodeType,
|
||||
*,
|
||||
gb_type: str,
|
||||
context: str,
|
||||
explanation: str,
|
||||
hints: list[str],
|
||||
) -> NoReturn:
|
||||
# This function calls unimplemented internally and eventually graph breaks
|
||||
# or falls to eager. unimplemented itself does not print any user warnings,
|
||||
# i.e., its very silent. This helper function is intended when an error is
|
||||
# encountered in the torch.compile stack which is worth showing as warning
|
||||
# to the user. For example, if AOT Autograd backend fails with a fake tensor
|
||||
# exception, its ok to fallback to eager but not silently. Here, we can use
|
||||
# this function to log the message and the stack trace.
|
||||
graph_break_msg = format_error_msg_verbose(e, code)
|
||||
torch._logging.trace_structured(
|
||||
"artifact",
|
||||
metadata_fn=lambda: {
|
||||
"name": "dynamo_graph_break_reason",
|
||||
"encoding": "string",
|
||||
},
|
||||
payload_fn=lambda: graph_break_msg,
|
||||
)
|
||||
graph_breaks_log.debug("%s", graph_break_msg)
|
||||
_unimplemented = unimplemented
|
||||
# to prevent a graph break registry entry
|
||||
_unimplemented(
|
||||
gb_type=gb_type,
|
||||
context=context,
|
||||
explanation=explanation,
|
||||
hints=hints,
|
||||
from_exc=e,
|
||||
log_warning=True,
|
||||
)
|
||||
|
||||
|
||||
def format_graph_break_message(
|
||||
gb_type: str,
|
||||
context: str,
|
||||
explanation: str,
|
||||
hints: list[str],
|
||||
) -> str:
|
||||
explanation = textwrap.indent(explanation, " ").lstrip()
|
||||
hints_str = "\n".join(
|
||||
" Hint: " + textwrap.indent(hint, " ").lstrip() for hint in hints
|
||||
)
|
||||
context = textwrap.indent(context, " ").lstrip()
|
||||
|
||||
msg = f"""\
|
||||
{gb_type}
|
||||
Explanation: {explanation}
|
||||
{hints_str}
|
||||
|
||||
Developer debug context: {context}"""
|
||||
documentation_link = get_gbid_documentation_link(gb_type)
|
||||
|
||||
if documentation_link:
|
||||
msg += f"\n\n For more details about this graph break, please visit: {documentation_link}"
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _load_gb_type_to_gb_id_map() -> dict[str, Any]:
|
||||
"""
|
||||
Loads the gb_type to gb_id map from the graph break registry from JSON file with caching.
|
||||
|
||||
Includes historical gb_type (mapping behavior of duplicate gb_types with different gb_ids is undefined).
|
||||
"""
|
||||
try:
|
||||
script_dir = Path(__file__).resolve().parent
|
||||
registry_path = get_file_path_2(
|
||||
"", str(script_dir), "graph_break_registry.json"
|
||||
)
|
||||
with open(registry_path) as f:
|
||||
registry = json.load(f)
|
||||
except Exception:
|
||||
log.exception("Error accessing the registry file")
|
||||
# pyrefly: ignore [implicit-any]
|
||||
registry = {}
|
||||
|
||||
mapping = {}
|
||||
for k, v in registry.items():
|
||||
for entry in v:
|
||||
mapping[entry["Gb_type"]] = k
|
||||
|
||||
return mapping
|
||||
|
||||
|
||||
def get_gbid_documentation_link(gb_type: str) -> str | None:
|
||||
"""
|
||||
Retrieves the GBID documentation link for a given graph break type.
|
||||
|
||||
Args:
|
||||
gb_type: The graph break type to look up.
|
||||
|
||||
Returns:
|
||||
A string containing the documentation URL if found, otherwise None.
|
||||
"""
|
||||
GRAPH_BREAK_SITE_URL = (
|
||||
"https://meta-pytorch.github.io/compile-graph-break-site/gb/" # @lint-ignore
|
||||
)
|
||||
|
||||
gb_type_to_gb_id_map = _load_gb_type_to_gb_id_map()
|
||||
|
||||
if gb_type in gb_type_to_gb_id_map:
|
||||
return (
|
||||
f"{GRAPH_BREAK_SITE_URL}gb{gb_type_to_gb_id_map[gb_type].lstrip('GB')}.html"
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
_NOTHING = object()
|
||||
|
||||
|
||||
def unimplemented(
|
||||
*,
|
||||
gb_type: str,
|
||||
context: str,
|
||||
explanation: str,
|
||||
hints: list[str],
|
||||
from_exc: Any = _NOTHING,
|
||||
log_warning: bool = False,
|
||||
skip_frame: bool = False,
|
||||
) -> NoReturn:
|
||||
"""
|
||||
Called within dynamo to cause a graph break.
|
||||
Args:
|
||||
gb_type: Context-free graph break type. It should be a short string without any
|
||||
information specific to the tracing context (i.e. no dynamically-generated strings)
|
||||
context: Developer context for the graph break. It can contain tracing context/dynamic strings.
|
||||
explanation: User-facing context-dependent explanation for the graph break. Can be dynamic.
|
||||
hints: List of user-facing hints for the graph break.
|
||||
"""
|
||||
|
||||
msg = format_graph_break_message(gb_type, context, explanation, hints)
|
||||
|
||||
if log_warning:
|
||||
log.warning(msg)
|
||||
if from_exc is not _NOTHING:
|
||||
past_real_stack = None
|
||||
if hasattr(from_exc, "real_stack"):
|
||||
past_real_stack = from_exc.real_stack
|
||||
if isinstance(from_exc, Unsupported):
|
||||
msg = f"{from_exc.msg}\n\n*** While handling this graph break, another graph break occurred: ***\n\n{msg}"
|
||||
# noqa: GB_REGISTRY
|
||||
raise Unsupported(msg, gb_type, skip_frame, real_stack=past_real_stack)
|
||||
# noqa: GB_REGISTRY
|
||||
raise Unsupported(
|
||||
msg, gb_type, skip_frame, real_stack=past_real_stack
|
||||
) from from_exc
|
||||
# noqa: GB_REGISTRY
|
||||
raise Unsupported(msg, gb_type, skip_frame)
|
||||
|
||||
|
||||
# KeyError has special handling for its args
|
||||
# see https://github.com/python/cpython/blob/3.11/Objects/exceptions.c#L2534 for details
|
||||
class KeyErrorMsg:
|
||||
def __init__(self, value: Any) -> None:
|
||||
self.value = value
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.value)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
|
||||
def augment_exc_message_with_hop_name(exc: Exception, msg: str) -> str:
|
||||
# Add HOP context right after before the explanation if present;
|
||||
# otherwise after the message
|
||||
if hasattr(exc, "_hop_name"):
|
||||
lines = msg.partition("\n Explanation:")
|
||||
msg = (
|
||||
f"{lines[0]}\n Higher Order Operator: {exc._hop_name}{lines[1]}{lines[2]}" # type: ignore[attr-defined]
|
||||
)
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def augment_exc_message(exc: Exception, msg: str = "\n", export: bool = False) -> None:
|
||||
import traceback
|
||||
|
||||
exc.innermost_user_frame_summary = None # type: ignore[attr-defined]
|
||||
|
||||
real_stack = get_real_stack(exc)
|
||||
if real_stack is not None and len(real_stack) > 0:
|
||||
exc.innermost_user_frame_summary = real_stack[-1] # type: ignore[attr-defined]
|
||||
msg += f"\nfrom user code:\n {''.join(traceback.format_list(real_stack))}"
|
||||
|
||||
if config.replay_record_enabled and hasattr(exc, "record_filename"):
|
||||
msg += (
|
||||
f"\nLast frame execution written to {exc.record_filename}. To run only this frame while debugging, run\
|
||||
torch._dynamo.replay('{exc.record_filename}').\n"
|
||||
)
|
||||
|
||||
if not config.verbose and hasattr(exc, "real_stack"):
|
||||
msg += (
|
||||
"\nSet TORCHDYNAMO_VERBOSE=1 for the internal stack trace "
|
||||
"(please do this especially if you're reporting a bug to PyTorch). "
|
||||
'For even more developer context, set TORCH_LOGS="+dynamo"\n'
|
||||
)
|
||||
|
||||
if hasattr(exc, "inner_exception") and hasattr(
|
||||
exc.inner_exception, "minifier_path"
|
||||
):
|
||||
if hasattr(exc.inner_exception, "buck_command"):
|
||||
msg += (
|
||||
f"\nMinifier script written to {exc.inner_exception.minifier_path}. Run "
|
||||
f"this buck command to find the smallest traced graph "
|
||||
f"which reproduces this error: {exc.inner_exception.buck_command}\n"
|
||||
)
|
||||
else:
|
||||
msg += (
|
||||
f"\nMinifier script written to {exc.inner_exception.minifier_path}. Run "
|
||||
"this script to find the smallest traced graph which reproduces this error.\n"
|
||||
)
|
||||
|
||||
old_msg = "" if len(exc.args) == 0 else str(exc.args[0])
|
||||
|
||||
old_msg = augment_exc_message_with_hop_name(exc, old_msg)
|
||||
|
||||
if isinstance(exc, KeyError):
|
||||
exc.args = (KeyErrorMsg(old_msg + msg),) + exc.args[1:]
|
||||
else:
|
||||
new_msg = old_msg + msg
|
||||
exc.args = (new_msg,) + exc.args[1:]
|
||||
|
||||
|
||||
def get_exc_message(
|
||||
e: Exception, compile_id: CompileId
|
||||
) -> tuple[str | None, int | None]:
|
||||
filename = None
|
||||
lineno = None
|
||||
if e.innermost_user_frame_summary is not None: # type: ignore[attr-defined]
|
||||
filename = e.innermost_user_frame_summary.filename # type: ignore[attr-defined]
|
||||
lineno = e.innermost_user_frame_summary.lineno # type: ignore[attr-defined]
|
||||
e.compile_id = compile_id # type: ignore[attr-defined]
|
||||
return filename, lineno
|
||||
|
||||
|
||||
def get_stack_above_dynamo() -> StackSummary:
|
||||
return filter_stack(extract_stack())
|
||||
|
||||
|
||||
def get_real_stack(
|
||||
exc: Exception, frame: DynamoFrameType | None = None
|
||||
) -> StackSummary | None:
|
||||
real_stack = getattr(exc, "real_stack", None)
|
||||
if real_stack is None:
|
||||
return None
|
||||
|
||||
# NB: it's possible for real_stack to be []; we still attempt to
|
||||
# report a stack anyway because the stack_above_dynamo may still
|
||||
# be useful for debugging
|
||||
|
||||
if frame is not None:
|
||||
# NB: frame is PyInterpreterFrame on Python 3.11 and later,
|
||||
# not a TRUE frame object. You can't actually feed it
|
||||
# to traceback because it doesn't have enough information.
|
||||
# To solve this problem, we technically should just materialize
|
||||
# the frame, the same way _PyFrame_GetFrameObject would do
|
||||
# (but we cannot actually do this, because this populates
|
||||
# frame_obj field, which default eval frame doesn't like).
|
||||
#
|
||||
# Fortunately, in this case, we can hack it: there's no need
|
||||
# to actually use the truly top frame, we can just extract
|
||||
# from where we are right now and rely on filter_stack to
|
||||
# get rid of all the dynamo frames. For ease of testing
|
||||
# we apply this behavior to ALL Python versions
|
||||
stack_above_dynamo = get_stack_above_dynamo()
|
||||
else:
|
||||
stack_above_dynamo = StackSummary()
|
||||
|
||||
return StackSummary.from_list(stack_above_dynamo + real_stack)
|
||||
|
||||
|
||||
# filter out all frames after entering dynamo
|
||||
def filter_stack(stack: StackSummary) -> StackSummary:
|
||||
user_stack = StackSummary()
|
||||
for frame in stack:
|
||||
if frame.filename is None:
|
||||
continue
|
||||
if "convert_frame" in frame.filename:
|
||||
break
|
||||
if "eval_frame" in frame.filename or (
|
||||
frame.line and "torch._dynamo.optimize(" in frame.line
|
||||
):
|
||||
continue
|
||||
user_stack.append(frame)
|
||||
|
||||
return user_stack
|
||||
|
||||
|
||||
def remove_resume_prefix(name: str) -> str:
|
||||
from .resume_execution import TORCH_DYNAMO_RESUME_IN_PREFIX
|
||||
|
||||
match = re.match(f"{TORCH_DYNAMO_RESUME_IN_PREFIX}_(\\w+)_at_\\d+", name)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return name
|
||||
|
||||
|
||||
def collapse_resume_frames(stack: StackSummary | list[FrameSummary]) -> StackSummary:
|
||||
"""
|
||||
When we graph break, we create a resume function and make a regular Python call
|
||||
to it, which gets intercepted by Dynamo. This behavior is normally shown in the
|
||||
traceback, which can be confusing to a user. So we can filter out resume frames
|
||||
for better traceback clarity.
|
||||
|
||||
Example:
|
||||
File "..." line 3, in f
|
||||
<line 3>
|
||||
File "..." line 5, in torch_dynamo_resume_in_f_at_80
|
||||
<line 5>
|
||||
File "..." line 10, in torch_dynamo_resume_in_f_at_120
|
||||
<line 10>
|
||||
|
||||
becomes
|
||||
File "..." line 10, in f
|
||||
<line 10>
|
||||
"""
|
||||
|
||||
new_stack = StackSummary()
|
||||
for frame in stack:
|
||||
if frame.filename is None:
|
||||
continue
|
||||
name = remove_resume_prefix(frame.name)
|
||||
if new_stack and name and new_stack[-1].name == name:
|
||||
new_stack[-1] = frame
|
||||
frame.name = name
|
||||
else:
|
||||
frame.name = name
|
||||
new_stack.append(frame)
|
||||
|
||||
return new_stack
|
||||
|
||||
|
||||
def format_error_msg_verbose(
|
||||
exc: Exception,
|
||||
code: types.CodeType,
|
||||
record_filename: str | None = None,
|
||||
frame: DynamoFrameType | None = None,
|
||||
) -> str:
|
||||
msg = (
|
||||
f"WON'T CONVERT {code.co_name} {code.co_filename} line {code.co_firstlineno}\n"
|
||||
)
|
||||
msg += "=" * 10 + " TorchDynamo Stack Trace " + "=" * 10 + "\n"
|
||||
msg += format_exc()
|
||||
real_stack = get_real_stack(exc, frame)
|
||||
if real_stack is not None:
|
||||
msg += (
|
||||
"\n"
|
||||
+ "=" * 10
|
||||
+ " The above exception occurred while processing the following code "
|
||||
+ "=" * 10
|
||||
+ "\n\n"
|
||||
)
|
||||
msg += "".join(format_list(real_stack))
|
||||
msg += "\n"
|
||||
msg += "=" * 10
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def format_frame_info(code: types.CodeType) -> str:
|
||||
return (
|
||||
f"{getattr(code, 'co_name', '<unknown>')} "
|
||||
f"({getattr(code, 'co_filename', '<unknown>')} "
|
||||
f"line {getattr(code, 'co_firstlineno', 0)})"
|
||||
)
|
||||
|
||||
|
||||
def format_skip_frame_message(code: types.CodeType | None, reason: str) -> str:
|
||||
if code is not None:
|
||||
frame_info = format_frame_info(code)
|
||||
return (
|
||||
f"torch.compile intentionally decided to skip the frame {frame_info} and fall back to eager.\n"
|
||||
f"Reason: {reason}"
|
||||
)
|
||||
else:
|
||||
return (
|
||||
f"torch.compile intentionally decided to skip the frame and fall back to eager.\n"
|
||||
f"Reason: {reason}"
|
||||
)
|
||||
|
||||
|
||||
def format_error_msg(
|
||||
exc: Exception,
|
||||
code: types.CodeType,
|
||||
record_filename: str | None = None,
|
||||
frame: DynamoFrameType | None = None,
|
||||
) -> str:
|
||||
if config.verbose:
|
||||
return format_error_msg_verbose(exc, code, record_filename, frame)
|
||||
return f"WON'T CONVERT {code.co_name} {code.co_filename}\
|
||||
line {code.co_firstlineno} \ndue to: \n{format_exc()}"
|
||||
@@ -0,0 +1,310 @@
|
||||
"""
|
||||
This module contains utility functions that are explicitly allowed to be called during
|
||||
TorchDynamo compilation. These functions are carefully vetted to ensure they work
|
||||
correctly within the TorchDynamo tracing and compilation process.
|
||||
|
||||
Key functionality groups:
|
||||
|
||||
- Compilation State:
|
||||
Functions for checking compilation state (is_compiling)
|
||||
|
||||
- Function Wrapping:
|
||||
Utilities for wrapping functions (wrap_inline, wrap_numpy) to work with
|
||||
TorchDynamo compilation
|
||||
|
||||
- Autograd Hooks:
|
||||
Functions and classes for handling autograd hooks and backward passes
|
||||
(call_hook, FakeBackwardCFunction, etc.)
|
||||
|
||||
- Tensor Operations:
|
||||
Utility functions for tensor operations and transformations
|
||||
"""
|
||||
|
||||
import functools
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
from typing import Any, TYPE_CHECKING, TypeVar
|
||||
from typing_extensions import deprecated, ParamSpec
|
||||
|
||||
import torch
|
||||
import torch.utils._pytree as pytree
|
||||
|
||||
|
||||
try:
|
||||
import numpy as np
|
||||
except ModuleNotFoundError:
|
||||
np = None # type: ignore[assignment]
|
||||
|
||||
_P = ParamSpec("_P")
|
||||
_R = TypeVar("_R")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# TorchScript does not support `@deprecated`
|
||||
# This is a workaround to avoid breaking TorchScript
|
||||
@deprecated(
|
||||
"`torch._dynamo.external_utils.is_compiling` is deprecated. Use `torch.compiler.is_compiling` instead.",
|
||||
category=FutureWarning,
|
||||
)
|
||||
def is_compiling() -> bool:
|
||||
return torch.compiler.is_compiling()
|
||||
|
||||
else:
|
||||
|
||||
def is_compiling() -> bool:
|
||||
"""
|
||||
Indicates whether we are tracing/compiling with torch.compile() or torch.export().
|
||||
"""
|
||||
# NOTE: With `@torch.compile(backend="eager")`, torch._dynamo.is_compiling() will get traced
|
||||
# and return true. torch.compiler.is_compiling() is skipped and will return false.
|
||||
return torch.compiler.is_compiling()
|
||||
|
||||
|
||||
def wrap_inline(fn: Callable[_P, _R]) -> Callable[_P, _R]:
|
||||
"""
|
||||
Create an extra frame around fn that is not in skipfiles.
|
||||
"""
|
||||
|
||||
@functools.wraps(fn)
|
||||
def inner(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
def call_hook(
|
||||
hook: Callable[..., torch.Tensor | None], *args: Any, **kwargs: Any
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Used by compiled autograd to handle hook returning None.
|
||||
"""
|
||||
result = hook(*args)
|
||||
if result is None:
|
||||
return args[0]
|
||||
elif kwargs.get("hook_type") == "post_acc_grad_hook":
|
||||
raise RuntimeError("Tensor post accumulate grad hooks should return None.")
|
||||
return result
|
||||
|
||||
|
||||
def wrap_numpy(f: Callable[_P, _R]) -> Callable[_P, _R]:
|
||||
r"""Decorator that turns a function from ``np.ndarray``s to ``np.ndarray``s into a function
|
||||
from ``torch.Tensor``s to ``torch.Tensor``s.
|
||||
"""
|
||||
if not np:
|
||||
return f
|
||||
|
||||
@functools.wraps(f)
|
||||
def wrap(*args: _P.args, **kwargs: _P.kwargs) -> pytree.PyTree:
|
||||
args, kwargs = pytree.tree_map_only(
|
||||
torch.Tensor, lambda x: x.numpy(), (args, kwargs)
|
||||
)
|
||||
out = f(*args, **kwargs)
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return pytree.tree_map_only(np.ndarray, lambda x: torch.as_tensor(x), out)
|
||||
|
||||
return wrap
|
||||
|
||||
|
||||
class FakeBackwardCFunction:
|
||||
def __init__(
|
||||
self,
|
||||
real: torch.autograd.function.BackwardCFunction,
|
||||
saved_tensors: list[torch.Tensor],
|
||||
) -> None:
|
||||
self.real = real
|
||||
self.saved_tensors = saved_tensors
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
if name == "saved_variables":
|
||||
warnings.warn(
|
||||
"'saved_variables' is deprecated; use 'saved_tensors'",
|
||||
DeprecationWarning,
|
||||
)
|
||||
return self.saved_tensors
|
||||
|
||||
return getattr(self.real, name)
|
||||
|
||||
|
||||
def call_backward(
|
||||
backward_c_function: torch.autograd.function.BackwardCFunction,
|
||||
saved_tensors: list[torch.Tensor],
|
||||
*args: Any,
|
||||
) -> torch.Tensor | tuple[torch.Tensor, ...]:
|
||||
fake = FakeBackwardCFunction(backward_c_function, saved_tensors)
|
||||
grads = fake._forward_cls.backward(fake, *args) # type: ignore[attr-defined]
|
||||
|
||||
if not isinstance(grads, tuple):
|
||||
grads = (grads,)
|
||||
|
||||
return grads
|
||||
|
||||
|
||||
def normalize_as_list(x: Any) -> list[Any]:
|
||||
if isinstance(x, tuple):
|
||||
return list(x)
|
||||
elif isinstance(x, list):
|
||||
return x
|
||||
return [x]
|
||||
|
||||
|
||||
def untyped_storage_size(x: torch.Tensor) -> int:
|
||||
return x.untyped_storage().size()
|
||||
|
||||
|
||||
class FakeCompiledAutogradEngine:
|
||||
@staticmethod
|
||||
def queue_callback(
|
||||
final_callbacks: list[Callable[[], None]], cb: Callable[[], None]
|
||||
) -> None:
|
||||
final_callbacks.append(cb)
|
||||
|
||||
@staticmethod
|
||||
def exec_final_callbacks(final_callbacks: list[Callable[[], None]]) -> None:
|
||||
i = 0
|
||||
while i < len(final_callbacks):
|
||||
cb = final_callbacks[i]
|
||||
cb()
|
||||
i += 1
|
||||
final_callbacks.clear()
|
||||
|
||||
@staticmethod
|
||||
def _exec_final_callbacks_stub() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def call_hook_from_backward_state(
|
||||
*args: Any, bw_state: Any, hook_name: str, **kwargs: Any
|
||||
) -> Any:
|
||||
return getattr(bw_state, hook_name)(*args, **kwargs)
|
||||
|
||||
|
||||
class _ApplyBackwardHook(torch.autograd.Function):
|
||||
"""Custom autograd function that applies a hook during backward.
|
||||
|
||||
This is used to implement register_hook on intermediate tensors without
|
||||
requiring compiled autograd. The hook function is captured in the context
|
||||
and applied during the backward pass.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
# pyre-ignore[14]: Inconsistent override is expected for autograd.Function
|
||||
def forward(
|
||||
ctx: Any, tensor: torch.Tensor, hook_fn: Callable[..., Any]
|
||||
) -> torch.Tensor: # type: ignore[override]
|
||||
ctx.hook_fn = hook_fn
|
||||
return tensor.view_as(tensor)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx: Any, grad: torch.Tensor) -> tuple[torch.Tensor, None]: # type: ignore[override]
|
||||
result = ctx.hook_fn(grad)
|
||||
if result is None:
|
||||
result = grad
|
||||
return result, None
|
||||
|
||||
|
||||
def call_module_hooks_from_backward_state(
|
||||
_: Any, result: Any, *args: Any, bw_state: Any, hooks_name: str, module_name: str
|
||||
) -> Any:
|
||||
module = getattr(bw_state, module_name)
|
||||
hooks = getattr(bw_state, hooks_name)
|
||||
for hook in hooks:
|
||||
new_result = hook(module, result, *args)
|
||||
if new_result is not None:
|
||||
result = new_result
|
||||
return result
|
||||
|
||||
|
||||
# used for torch._dynamo.disable(recursive=False)
|
||||
def get_nonrecursive_disable_wrapper(fn: Callable[_P, _R]) -> Callable[_P, _R]:
|
||||
# wrap function to get the right error message
|
||||
# this function is in external_utils so that convert_frame doesn't skip it.
|
||||
@functools.wraps(fn)
|
||||
def nonrecursive_disable_wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
||||
if torch.compiler.is_exporting():
|
||||
raise RuntimeError(
|
||||
"Non-recursive torch.compiler.disable is not supported with torch.export."
|
||||
)
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
return nonrecursive_disable_wrapper
|
||||
|
||||
|
||||
def wrap_dunder_call_ctx_manager(self: Any, func: Callable[_P, _R]) -> Callable[_P, _R]:
|
||||
"""
|
||||
Apply self as a ctx manager around a call to func
|
||||
"""
|
||||
|
||||
# NOTE: do not functools.wraps(func) because we don't ever want this frame to be skipped!
|
||||
def inner(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
||||
with self:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
# Use only on ints marked dynamic via torch.empty(0, integer)
|
||||
# Currently only way to mark ints as dynamic: https://github.com/pytorch/pytorch/issues/129623
|
||||
def unwrap_maybe_dynamic_int(x: torch.Tensor | int) -> int:
|
||||
if isinstance(x, torch.Tensor):
|
||||
# x.size() is expected to be [0, dynamic_int]
|
||||
return x.size(1)
|
||||
return x
|
||||
|
||||
|
||||
def call_accumulate_grad(
|
||||
variable: torch.Tensor, grad: torch.Tensor, has_post_hooks: bool
|
||||
) -> None:
|
||||
updated_grad = torch._dynamo.compiled_autograd.ops.AccumulateGrad( # type: ignore[attr-defined]
|
||||
[grad], variable, variable.grad, has_post_hooks
|
||||
)
|
||||
variable.grad = updated_grad[0]
|
||||
|
||||
|
||||
def wrap_inline_with_error_on_graph_break(
|
||||
fn: Callable[_P, _R], error_on_graph_break: bool
|
||||
) -> Callable[_P, _R]:
|
||||
# NB: need multiple definitions in order to prevent `fullgraph` from
|
||||
# being a freevar of wrapper
|
||||
# NOTE: do not functools.wraps(fn) because we don't ever want these wrappers to be skipped!
|
||||
if error_on_graph_break:
|
||||
|
||||
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
||||
with torch._dynamo.error_on_graph_break(True):
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
else:
|
||||
|
||||
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
||||
with torch._dynamo.error_on_graph_break(False):
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def filter_out_const_values(tup: tuple[Any, ...], masks: list[bool]) -> tuple[Any, ...]:
|
||||
"""
|
||||
masks is a list of bools, where True means the corresponding element in tup
|
||||
is a const value. Filter out the const values.
|
||||
"""
|
||||
out = []
|
||||
for mask_idx, mask in enumerate(masks):
|
||||
if not mask:
|
||||
out.append(tup[mask_idx])
|
||||
return tuple(out)
|
||||
|
||||
|
||||
def insert_const_values_with_mask(
|
||||
tup: tuple[Any, ...], masks: list[bool], values: tuple[Any, ...]
|
||||
) -> tuple[Any, ...]:
|
||||
"""
|
||||
masks and values are of same length. For indices where the mask is True, use
|
||||
the const_values to fill in.
|
||||
"""
|
||||
out = []
|
||||
idx = 0
|
||||
for mask_idx, mask in enumerate(masks):
|
||||
if mask:
|
||||
out.append(values[mask_idx])
|
||||
else:
|
||||
out.append(tup[idx])
|
||||
idx += 1
|
||||
return tuple(out)
|
||||
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
This module provides functionality for caching and looking up fully qualified function
|
||||
and class names from Python source files by line number.
|
||||
|
||||
It uses Python's tokenize module to parse source files and tracks function/class
|
||||
definitions along with their nesting to build fully qualified names (e.g. 'class.method'
|
||||
or 'module.function'). The results are cached in a two-level dictionary mapping:
|
||||
|
||||
filename -> (line_number -> fully_qualified_name)
|
||||
|
||||
Example usage:
|
||||
name = get_funcname("myfile.py", 42) # Returns name of function/class at line 42
|
||||
clearcache() # Clear the cache if file contents have changed
|
||||
|
||||
The parsing is done lazily when a file is first accessed. Invalid Python files or
|
||||
IO errors are handled gracefully by returning empty cache entries.
|
||||
"""
|
||||
|
||||
import tokenize
|
||||
|
||||
|
||||
cache: dict[str, dict[int, str]] = {}
|
||||
|
||||
|
||||
def clearcache() -> None:
|
||||
cache.clear()
|
||||
|
||||
|
||||
def _add_file(filename: str) -> None:
|
||||
try:
|
||||
with tokenize.open(filename) as f:
|
||||
tokens = list(tokenize.generate_tokens(f.readline))
|
||||
except (OSError, tokenize.TokenError):
|
||||
cache[filename] = {}
|
||||
return
|
||||
|
||||
# NOTE: undefined behavior if file is not valid Python source,
|
||||
# since tokenize will have undefined behavior.
|
||||
result: dict[int, str] = {}
|
||||
# current full funcname, e.g. xxx.yyy.zzz
|
||||
cur_name = ""
|
||||
cur_indent = 0
|
||||
significant_indents: list[int] = []
|
||||
|
||||
for i, token in enumerate(tokens):
|
||||
if token.type == tokenize.INDENT:
|
||||
cur_indent += 1
|
||||
elif token.type == tokenize.DEDENT:
|
||||
cur_indent -= 1
|
||||
# possible end of function or class
|
||||
if significant_indents and cur_indent == significant_indents[-1]:
|
||||
significant_indents.pop()
|
||||
# pop the last name
|
||||
cur_name = cur_name.rpartition(".")[0]
|
||||
elif (
|
||||
token.type == tokenize.NAME
|
||||
and i + 1 < len(tokens)
|
||||
and tokens[i + 1].type == tokenize.NAME
|
||||
and (token.string == "class" or token.string == "def")
|
||||
):
|
||||
# name of class/function always follows class/def token
|
||||
significant_indents.append(cur_indent)
|
||||
if cur_name:
|
||||
cur_name += "."
|
||||
cur_name += tokens[i + 1].string
|
||||
result[token.start[0]] = cur_name
|
||||
|
||||
cache[filename] = result
|
||||
|
||||
|
||||
def get_funcname(filename: str, lineno: int) -> str | None:
|
||||
if filename not in cache:
|
||||
_add_file(filename)
|
||||
return cache[filename].get(lineno, None)
|
||||
@@ -0,0 +1,983 @@
|
||||
import inspect
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
import types
|
||||
from collections import namedtuple
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from typing import Any, Optional, TYPE_CHECKING, TypeVar
|
||||
|
||||
import sympy
|
||||
|
||||
import torch
|
||||
import torch.fx
|
||||
import torch.utils._pytree as pytree
|
||||
from torch._dynamo.convert_frame import CaptureOutput, fullgraph_capture, get_traced_fn
|
||||
from torch._dynamo.decorators import disable as dynamo_disable
|
||||
from torch._dynamo.eval_frame import argument_names, check_user_input_output
|
||||
from torch._dynamo.exc import UserErrorType
|
||||
from torch._dynamo.utils import dynamo_timed, get_metrics_context
|
||||
from torch._export.utils import _compiling_state_context
|
||||
from torch._guards import TracingContext
|
||||
from torch.export.dynamic_shapes import _RelaxedConstraint, Constraint
|
||||
from torch.fx import Node
|
||||
from torch.fx.experimental.symbolic_shapes import (
|
||||
ConstraintViolationError,
|
||||
DimDynamic,
|
||||
StatelessSymbolicContext,
|
||||
)
|
||||
from torch.fx.graph import _PyTreeCodeGen, _PyTreeInfo
|
||||
from torch.fx.node import Argument, Target
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch._subclasses.fake_tensor import FakeTensorMode
|
||||
|
||||
T = TypeVar("T")
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def post_process_error_msg(
|
||||
constraint_violation_error: ConstraintViolationError,
|
||||
func: Callable[..., Any],
|
||||
args: Any,
|
||||
kwargs: Any,
|
||||
) -> ConstraintViolationError:
|
||||
"""
|
||||
Because we trace a different callable, the sources are all messed up.
|
||||
Manually patch them so the error message looks correct.
|
||||
"""
|
||||
from torch.export._unlift import _get_input_paths, _replace_sources
|
||||
|
||||
orig_sig = inspect.signature(func)
|
||||
flat_input_paths = _get_input_paths((args, kwargs), orig_sig)
|
||||
if constraint_violation_error.args:
|
||||
constraint_violation_error.args = (
|
||||
_replace_sources(constraint_violation_error.args[0], flat_input_paths),
|
||||
)
|
||||
return constraint_violation_error
|
||||
|
||||
|
||||
EXPORT_ROOT_REPLACEMENTS = [
|
||||
("__export_root_", "_"),
|
||||
("_export_root.", ""),
|
||||
("._export_root", ""),
|
||||
]
|
||||
|
||||
|
||||
def clean_export_root_string(text: str) -> str:
|
||||
"""Generic utility to clean export_root patterns from strings."""
|
||||
result = text
|
||||
for pattern, replacement in EXPORT_ROOT_REPLACEMENTS:
|
||||
result = result.replace(pattern, replacement)
|
||||
return result
|
||||
|
||||
|
||||
def clean_nn_module_stack_and_source_fn(
|
||||
graph_module: torch.fx.GraphModule, is_inline_builtin: bool = False
|
||||
) -> torch.fx.GraphModule:
|
||||
"""
|
||||
Clean up nn_module_stack metadata by removing export_root references.
|
||||
|
||||
Removes the _export_root module references from nn_module_stack metadata
|
||||
in graph nodes, which are artifacts from the export process. Fixes two patterns:
|
||||
|
||||
1. Keys: Removes "__export_root_" and "__modules['_export_root']_" prefixes
|
||||
- Normal case: "L__self____export_root_child" -> "L__self__child"
|
||||
- inline_builtin case: Uses numeric ID strings like "140468831433840"
|
||||
|
||||
2. Values: Removes "._export_root" and "._modules['_export_root']" from child names
|
||||
e.g., "L['self']._export_root.child" -> "L['self'].child"
|
||||
e.g., "L['self']._modules['_export_root'].child" -> "L['self'].child"
|
||||
|
||||
Also removes the root export entry "L__self____export_root" entirely.
|
||||
|
||||
Args:
|
||||
graph_module: The GraphModule to clean up
|
||||
is_inline_builtin: If True, keys are numeric ID strings and self references
|
||||
(L['self']) are filtered out
|
||||
|
||||
Returns:
|
||||
The cleaned GraphModule (modified in-place)
|
||||
"""
|
||||
|
||||
def _process_nn_module_stack(
|
||||
nn_module_stack: dict[str, tuple[str, T]],
|
||||
) -> dict[str, tuple[str, T]]:
|
||||
if "L__self____export_root" in nn_module_stack:
|
||||
del nn_module_stack["L__self____export_root"]
|
||||
|
||||
# Clean up remaining entries
|
||||
cleaned_stack = {}
|
||||
for key, (child_name, child_class) in nn_module_stack.items():
|
||||
# Clean key by removing export_root patterns
|
||||
clean_key = clean_export_root_string(key)
|
||||
|
||||
# Clean child_name by removing export_root patterns
|
||||
clean_name = clean_export_root_string(child_name)
|
||||
|
||||
# Skip self reference for inline builtin case
|
||||
if is_inline_builtin and clean_name == "L['self']":
|
||||
continue
|
||||
|
||||
cleaned_stack[clean_key] = (clean_name, child_class)
|
||||
return cleaned_stack
|
||||
|
||||
def _process_source_fn(source_fn_stack: Iterable[T]) -> Iterable[T]:
|
||||
cleaned_stack = []
|
||||
for item in source_fn_stack:
|
||||
if isinstance(item, tuple) and len(item) == 2:
|
||||
name, cls = item
|
||||
if isinstance(name, str):
|
||||
clean_name = clean_export_root_string(name)
|
||||
cleaned_stack.append((clean_name, cls))
|
||||
else:
|
||||
cleaned_stack.append(item)
|
||||
else:
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
cleaned_stack.append(item)
|
||||
# pyrefly: ignore [bad-return]
|
||||
return cleaned_stack
|
||||
|
||||
for node in graph_module.graph.nodes:
|
||||
if "nn_module_stack" in node.meta:
|
||||
node.meta["nn_module_stack"] = _process_nn_module_stack(
|
||||
node.meta["nn_module_stack"].copy()
|
||||
)
|
||||
|
||||
source_fn_stack = node.meta.get("source_fn_stack", None)
|
||||
if source_fn_stack:
|
||||
node.meta["source_fn_stack"] = _process_source_fn(source_fn_stack.copy())
|
||||
|
||||
if "dynamo_flat_name_to_original_fqn" in graph_module.meta:
|
||||
# Clean up flat name to original fqn mapping
|
||||
clean_name_to_original_fqn = {}
|
||||
for flat_name, original_fqn in graph_module.meta[
|
||||
"dynamo_flat_name_to_original_fqn"
|
||||
].items():
|
||||
clean_name_to_original_fqn[clean_export_root_string(flat_name)] = (
|
||||
clean_export_root_string(original_fqn)
|
||||
)
|
||||
graph_module.meta["dynamo_flat_name_to_original_fqn"] = (
|
||||
clean_name_to_original_fqn
|
||||
)
|
||||
|
||||
return graph_module
|
||||
|
||||
|
||||
def clean_export_root(graph_module: torch.fx.GraphModule) -> None:
|
||||
"""Remove export_root artifacts from FX graph in-place"""
|
||||
|
||||
# Unlike getattr node, call_module can be invoked multiple times
|
||||
# In those cases, we should fix all invocations of call_module
|
||||
clean_named_module_map: dict[str, str] = {}
|
||||
|
||||
# Update get_attr nodes in-place
|
||||
for node in graph_module.graph.nodes:
|
||||
if node.op == "get_attr":
|
||||
old_target = node.target
|
||||
new_target = clean_export_root_string(old_target)
|
||||
if new_target != old_target:
|
||||
node.target = new_target
|
||||
assert hasattr(graph_module, old_target)
|
||||
# Move the parameter to the new name
|
||||
param = torch.fx.graph_module._get_attr(graph_module, old_target)
|
||||
torch.fx.graph_module._assign_attr(param, graph_module, new_target)
|
||||
torch.fx.graph_module._del_attr(graph_module, old_target)
|
||||
# Dynamo will only have one nested level
|
||||
if node.op == "call_module":
|
||||
old_target = node.target
|
||||
assert isinstance(old_target, str)
|
||||
new_target = clean_export_root_string(old_target)
|
||||
assert isinstance(new_target, str)
|
||||
new_name = clean_export_root_string(node.name)
|
||||
if new_target == old_target:
|
||||
continue
|
||||
|
||||
# if this module has already been cleaned before, just lookup from map.
|
||||
if old_target in clean_named_module_map:
|
||||
node.target = clean_named_module_map[old_target]
|
||||
node.name = new_name
|
||||
continue
|
||||
target = graph_module.get_submodule(old_target)
|
||||
graph_module.delete_submodule(old_target)
|
||||
graph_module.add_submodule(new_target, target)
|
||||
node.target = new_target
|
||||
node.name = new_name
|
||||
clean_named_module_map[old_target] = new_target
|
||||
|
||||
|
||||
class ModuleToTrace(torch.nn.Module):
|
||||
def __init__(self, foo: Any, in_spec: Any) -> None:
|
||||
super().__init__()
|
||||
self._export_root = foo
|
||||
self.in_spec = in_spec
|
||||
|
||||
def forward(self, *flat_args: Any) -> "ExportTracerOutput":
|
||||
args, kwargs = pytree.tree_unflatten(flat_args, self.in_spec)
|
||||
res = self._export_root(*args, **kwargs)
|
||||
out_flat, out_spec = pytree.tree_flatten(res)
|
||||
return ExportTracerOutput(out_flat, out_spec)
|
||||
|
||||
|
||||
ExportTracerOutput = namedtuple("ExportTracerOutput", ["flat_args", "out_spec"])
|
||||
|
||||
|
||||
# mypy: disable-error-code="no-untyped-def,var-annotated,assignment,index,operator"
|
||||
class DynamoGraphTransformer(torch.fx.Transformer):
|
||||
"""Graph transformer for dynamo export that flattens inputs/outputs without complex matching."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
module: torch.fx.GraphModule,
|
||||
flat_inputs: list[Any],
|
||||
flat_args_dynamic_dims: list[set[int]],
|
||||
graph_input_order: dict[int, int],
|
||||
graph_output_map: dict[int, tuple[str, Any]],
|
||||
fake_mode: Any | None = None,
|
||||
graph_inputs: dict[int, Any] | None = None,
|
||||
) -> None:
|
||||
super().__init__(module)
|
||||
|
||||
assert len(flat_args_dynamic_dims) == len(flat_inputs)
|
||||
|
||||
self.flat_inputs = flat_inputs
|
||||
self.flat_args_dynamic_dims = flat_args_dynamic_dims
|
||||
self.graph_input_order = graph_input_order
|
||||
self.graph_output_map = graph_output_map
|
||||
self.fake_mode = fake_mode
|
||||
self.graph_inputs = graph_inputs or {}
|
||||
|
||||
# Get original placeholders and output
|
||||
self.placeholders = [n for n in module.graph.nodes if n.op == "placeholder"]
|
||||
self.output_node = next(n for n in module.graph.nodes if n.op == "output")
|
||||
|
||||
# Create new flattened input placeholders
|
||||
self.new_input_nodes: dict[int, torch.fx.Node] = {}
|
||||
self._create_flattened_inputs()
|
||||
|
||||
# Iterator for replacing old placeholders
|
||||
self.old_to_new_mapping = {}
|
||||
self._create_placeholder_mapping()
|
||||
|
||||
def _create_flattened_inputs(self) -> None:
|
||||
"""Create new placeholder nodes for flattened inputs with proper fake tensors."""
|
||||
for i in range(len(self.flat_inputs)):
|
||||
placeholder = super().placeholder(f"arg_{i}", (), {})
|
||||
|
||||
# Check if this user input (index i) maps to a graph placeholder
|
||||
if i in self.graph_input_order:
|
||||
# graph_input_order[i] gives us which graph placeholder this user input corresponds to
|
||||
graph_placeholder_idx = self.graph_input_order[i]
|
||||
if graph_placeholder_idx < len(self.placeholders):
|
||||
orig_placeholder = self.placeholders[graph_placeholder_idx]
|
||||
# Copy other metadata but not "val" yet
|
||||
for key, value in orig_placeholder.meta.items():
|
||||
if key != "val":
|
||||
placeholder.node.meta[key] = value
|
||||
|
||||
# Always ensure we have proper "val" metadata from fake tensor
|
||||
if self.fake_mode is not None and isinstance(
|
||||
self.flat_inputs[i], torch.Tensor
|
||||
):
|
||||
placeholder.node.meta["val"] = self.fake_mode.from_tensor(
|
||||
self.flat_inputs[i],
|
||||
symbolic_context=StatelessSymbolicContext(
|
||||
dynamic_sizes=[
|
||||
(
|
||||
DimDynamic.DYNAMIC
|
||||
if d in self.flat_args_dynamic_dims[i]
|
||||
else DimDynamic.STATIC
|
||||
)
|
||||
for d in range(len(self.flat_inputs[i].shape))
|
||||
],
|
||||
constraint_sizes=[None] * len(self.flat_inputs[i].shape),
|
||||
),
|
||||
)
|
||||
elif hasattr(self.flat_inputs[i], "val"): # _IntWrapper case
|
||||
placeholder.node.meta["val"] = self.flat_inputs[i].val
|
||||
else:
|
||||
placeholder.node.meta["val"] = self.flat_inputs[i]
|
||||
|
||||
# pyrefly: ignore [unsupported-operation]
|
||||
self.new_input_nodes[i] = placeholder
|
||||
|
||||
def _create_placeholder_mapping(self) -> None:
|
||||
"""Create mapping from old placeholders to new ones."""
|
||||
# graph_input_order maps: user_input_index -> graph_placeholder_index
|
||||
# We need to create: old_graph_placeholder -> new_user_input_placeholder
|
||||
for user_input_idx, graph_placeholder_idx in self.graph_input_order.items():
|
||||
if graph_placeholder_idx < len(self.placeholders):
|
||||
old_placeholder = self.placeholders[graph_placeholder_idx]
|
||||
new_placeholder = self.new_input_nodes[user_input_idx]
|
||||
self.old_to_new_mapping[old_placeholder] = new_placeholder
|
||||
|
||||
def placeholder(
|
||||
self, target: Target, args: tuple[Argument, ...], kwargs: dict[str, Any]
|
||||
) -> Any:
|
||||
"""Replace old placeholders with new flattened ones."""
|
||||
# Return the corresponding new placeholder
|
||||
if self.current_node in self.old_to_new_mapping:
|
||||
new_arg = self.old_to_new_mapping[self.current_node]
|
||||
|
||||
# Copy over additional metadata from current node, but don't overwrite "val"
|
||||
for key in ["tensor_dict", "example_value", "unbacked_bindings"]:
|
||||
if key in self.current_node.meta:
|
||||
new_arg.node.meta[key] = self.current_node.meta[key]
|
||||
|
||||
# Only copy "val" if we don't already have a good one
|
||||
if "val" in self.current_node.meta and "val" not in new_arg.node.meta:
|
||||
new_arg.node.meta["val"] = self.current_node.meta["val"]
|
||||
|
||||
return new_arg
|
||||
else:
|
||||
# Convert captured objects (e.g., opaque objects from closures) to
|
||||
# get_attr nodes
|
||||
placeholder_idx = self.placeholders.index(self.current_node)
|
||||
if placeholder_idx in self.graph_inputs:
|
||||
source = self.graph_inputs[placeholder_idx]
|
||||
if not isinstance(source, torch._dynamo.source.GetItemSource):
|
||||
example_val = self.current_node.meta.get(
|
||||
"val"
|
||||
) or self.current_node.meta.get("example_value")
|
||||
if example_val is not None:
|
||||
attr_name = f"_captured_{placeholder_idx}"
|
||||
if isinstance(example_val, torch.Tensor):
|
||||
self.module.register_buffer(attr_name, example_val)
|
||||
else:
|
||||
setattr(self.module, attr_name, example_val)
|
||||
result = self.tracer.create_proxy("get_attr", attr_name, (), {})
|
||||
result.node.meta = self.current_node.meta.copy()
|
||||
result.node.meta["val"] = example_val
|
||||
return result
|
||||
return super().placeholder(target, args, kwargs)
|
||||
|
||||
def output(
|
||||
self, target: Target, args: Sequence[Any], kwargs: dict[str, Any]
|
||||
) -> Any:
|
||||
"""Transform output according to graph_output_map."""
|
||||
original_outputs = args[0]
|
||||
|
||||
# Build new output list based on graph_output_map
|
||||
new_outputs = []
|
||||
for i in sorted(self.graph_output_map.keys()):
|
||||
output_type, val = self.graph_output_map[i]
|
||||
|
||||
if output_type == "graph_out":
|
||||
new_outputs.append(original_outputs[val])
|
||||
elif output_type == "input":
|
||||
input_idx = val.index
|
||||
new_outputs.append(self.new_input_nodes[input_idx])
|
||||
elif output_type == "constant":
|
||||
new_outputs.append(val)
|
||||
|
||||
return super().output(target, (tuple(new_outputs),), {})
|
||||
|
||||
def run_node(self, n: Node) -> Any:
|
||||
"""Run node transformation and preserve metadata."""
|
||||
self.current_node = n
|
||||
result = super().run_node(n)
|
||||
|
||||
# Copy important metadata
|
||||
if hasattr(result, "node") and result.node is not n:
|
||||
for key in ["val", "example_value", "unbacked_bindings"]:
|
||||
if key in n.meta:
|
||||
result.node.meta[key] = n.meta[key]
|
||||
|
||||
# Preserve node names (except output)
|
||||
if n.op != "output" and hasattr(n, "name"):
|
||||
result.node._rename(n.name)
|
||||
|
||||
return result
|
||||
|
||||
def transform(self) -> torch.fx.GraphModule:
|
||||
"""Perform the graph transformation and copy module metadata."""
|
||||
result_gm = super().transform()
|
||||
|
||||
# Copy module metadata like the original implementation
|
||||
if hasattr(self.module, "meta"):
|
||||
# pyrefly: ignore [unsupported-operation]
|
||||
if "dynamo_flat_name_to_original_fqn" in self.module.meta:
|
||||
# pyrefly: ignore [bad-index]
|
||||
result_gm.meta["dynamo_flat_name_to_original_fqn"] = self.module.meta[
|
||||
# pyrefly: ignore [bad-index]
|
||||
"dynamo_flat_name_to_original_fqn"
|
||||
]
|
||||
# pyrefly: ignore [unsupported-operation]
|
||||
if "dynamo_compile_id" in self.module.meta:
|
||||
# pyrefly: ignore [bad-index]
|
||||
result_gm.meta["dynamo_compile_id"] = self.module.meta[
|
||||
# pyrefly: ignore [bad-index]
|
||||
"dynamo_compile_id"
|
||||
]
|
||||
|
||||
return result_gm
|
||||
|
||||
|
||||
def _suggest_or_raise_constraint_violation(
|
||||
module_to_trace: torch.nn.Module,
|
||||
orig_callable: Callable[..., Any],
|
||||
fake_mode: Optional["FakeTensorMode"],
|
||||
graph_capture_output: CaptureOutput,
|
||||
args: Any,
|
||||
kwargs: Any,
|
||||
dynamic_shapes: dict[str, Any] | tuple[Any] | list[Any] | None,
|
||||
) -> None:
|
||||
constraint_violation_error = None
|
||||
try:
|
||||
# Check if we have any constraint violations
|
||||
fn, _ = get_traced_fn(module_to_trace)
|
||||
graph_capture_output.graph_capture_output.build_guards(fn.__code__)
|
||||
except ConstraintViolationError as e:
|
||||
constraint_violation_error = e
|
||||
|
||||
if (
|
||||
(shape_env := getattr(fake_mode, "shape_env", None)) is not None
|
||||
and (dim_constraints := shape_env.dim_constraints) is not None
|
||||
and not isinstance(
|
||||
module_to_trace.forward,
|
||||
torch._ops.OpOverloadPacket | torch._ops.OpOverload,
|
||||
)
|
||||
):
|
||||
dim_constraints.solve()
|
||||
|
||||
forced_specializations = dim_constraints.forced_specializations()
|
||||
|
||||
msg = dim_constraints.prettify_results(
|
||||
inspect.signature(orig_callable), # type: ignore[attr-defined]
|
||||
dynamic_shapes,
|
||||
constraint_violation_error,
|
||||
forced_specializations,
|
||||
)
|
||||
if constraint_violation_error:
|
||||
if constraint_violation_error.args:
|
||||
constraint_violation_error.args = (
|
||||
constraint_violation_error.args[0] + msg,
|
||||
)
|
||||
else:
|
||||
constraint_violation_error.args = (msg,)
|
||||
else:
|
||||
if forced_specializations:
|
||||
constraint_violation_error = ConstraintViolationError(msg)
|
||||
else:
|
||||
log.info(
|
||||
"Summary of dimension constraints:%s",
|
||||
msg,
|
||||
)
|
||||
|
||||
# Error if we have any constraints on static values
|
||||
|
||||
for k in shape_env.var_to_range:
|
||||
if isinstance(k, sympy.Integer):
|
||||
constraint_violation_error = ConstraintViolationError(
|
||||
f"{''.join(traceback.format_list(shape_env.var_to_stack[k]))}\n"
|
||||
"It appears that you're trying to set a constraint on a "
|
||||
f"value which we evaluated to have a static value of {k}. "
|
||||
'Set TORCH_LOGS="+export" for more information.'
|
||||
)
|
||||
if constraint_violation_error:
|
||||
constraint_violation_error = post_process_error_msg(
|
||||
constraint_violation_error, orig_callable, args, kwargs
|
||||
)
|
||||
raise constraint_violation_error
|
||||
|
||||
|
||||
def _normalize_shuffle_graph(shuffle_gm: torch.fx.GraphModule) -> None:
|
||||
shuffle_gm.graph.eliminate_dead_code()
|
||||
shuffle_gm.recompile()
|
||||
for name, buffer in list(shuffle_gm.named_buffers()):
|
||||
delattr(shuffle_gm, name)
|
||||
setattr(shuffle_gm, name, buffer)
|
||||
|
||||
|
||||
def normalize_graph_module(gm: torch.fx.GraphModule) -> None:
|
||||
for node in gm.graph.nodes:
|
||||
if node.op == "placeholder":
|
||||
node.meta["val"] = node.meta["example_value"]
|
||||
|
||||
|
||||
class InputProcessor:
|
||||
def __init__(
|
||||
self,
|
||||
root: object,
|
||||
num_args: int,
|
||||
kwarg_names: list[str],
|
||||
) -> None:
|
||||
self.root = root
|
||||
self.num_args = num_args
|
||||
self.kwarg_names = kwarg_names
|
||||
|
||||
def __call__(
|
||||
self, inputs: tuple[object, ...]
|
||||
) -> tuple[tuple[object, ...], dict[str, object]]:
|
||||
args = inputs
|
||||
# pyrefly: ignore [implicit-any]
|
||||
kwargs = {}
|
||||
if len(args) > self.num_args:
|
||||
kwargs = dict(zip(self.kwarg_names, args[self.num_args :]))
|
||||
args = args[: self.num_args]
|
||||
if self.root is not None:
|
||||
if isinstance(self.root, torch.fx.GraphModule):
|
||||
assert isinstance(self.root.graph._codegen, _DynamoBytecodeCodeGen)
|
||||
assert hasattr(
|
||||
self.root.graph._codegen.dynamo_bytecode_flatten, "input_processor"
|
||||
)
|
||||
assert (
|
||||
self.root.graph._codegen.dynamo_bytecode_flatten.input_processor
|
||||
is self
|
||||
)
|
||||
args = (self.root, *args)
|
||||
return args, kwargs
|
||||
|
||||
|
||||
class Yield(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DynamoBytecodeFlatten:
|
||||
def __init__(
|
||||
self,
|
||||
input_processor: InputProcessor,
|
||||
out: CaptureOutput,
|
||||
f_globals: dict[str, object],
|
||||
) -> None:
|
||||
self.input_processor = input_processor
|
||||
self.out = out
|
||||
self.f_globals = f_globals
|
||||
self.gm_inputs: tuple[Any, ...] | None = None
|
||||
|
||||
@dynamo_disable(reason="do not trace internal dynamo graph capture") # type: ignore[misc]
|
||||
def __call__(self, *inputs: object) -> object:
|
||||
def backend_dummy(*example_inputs: object) -> None:
|
||||
self.gm_inputs = example_inputs
|
||||
raise Yield
|
||||
|
||||
args, kwargs = self.input_processor(inputs)
|
||||
try:
|
||||
self.out.forward_callable(
|
||||
compiled_fn=backend_dummy, extra_globals=self.f_globals
|
||||
)(*args, **kwargs)
|
||||
except Yield:
|
||||
assert self.gm_inputs is not None
|
||||
return self.gm_inputs
|
||||
raise RuntimeError
|
||||
|
||||
|
||||
class DynamoBytecodeUnflatten:
|
||||
def __init__(
|
||||
self,
|
||||
input_processor: InputProcessor,
|
||||
out: CaptureOutput,
|
||||
f_globals: dict[str, object],
|
||||
) -> None:
|
||||
self.input_processor = input_processor
|
||||
self.out = out
|
||||
self.f_globals = f_globals
|
||||
|
||||
@dynamo_disable(reason="do not trace internal dynamo graph capture") # type: ignore[misc]
|
||||
def __call__(
|
||||
self, flat_outs: Sequence[object], inputs: tuple[object, ...]
|
||||
) -> object:
|
||||
def backend_dummy(*example_inputs: object) -> Sequence[object]:
|
||||
return flat_outs
|
||||
|
||||
args, kwargs = self.input_processor(inputs)
|
||||
with torch._C._DisableTorchDispatch():
|
||||
results = self.out.forward_callable(
|
||||
compiled_fn=backend_dummy, extra_globals=self.f_globals
|
||||
)(*args, **kwargs)
|
||||
return results
|
||||
|
||||
|
||||
def create_fx_graph_from_captured_output(
|
||||
out: CaptureOutput, mod: Any, args: tuple[Any, ...], kwargs: dict[str, Any]
|
||||
) -> torch.fx.GraphModule:
|
||||
assert out.backend_input is not None
|
||||
backend_input = out.backend_input
|
||||
|
||||
_, root = torch._dynamo.convert_frame.get_traced_fn(mod)
|
||||
|
||||
flat_real_args = pytree.tree_leaves((args, kwargs))
|
||||
torch._dynamo.eval_frame.check_user_input_output(
|
||||
flat_real_args, UserErrorType.INVALID_INPUT
|
||||
)
|
||||
f_globals = out.graph_capture_output.f_globals
|
||||
|
||||
graph_module = backend_input.graph_module
|
||||
if isinstance(root, torch.nn.Module):
|
||||
graph_module._parameters = root._parameters
|
||||
graph_module._buffers = root._buffers
|
||||
assert all(not hasattr(graph_module, m) for m in root._modules)
|
||||
graph_module._modules.update(root._modules)
|
||||
graph_module._non_persistent_buffers_set = root._non_persistent_buffers_set
|
||||
if sys.version_info >= (3, 14):
|
||||
import annotationlib # added in 3.14
|
||||
|
||||
annotations = annotationlib.get_annotations(torch.nn.Module)
|
||||
else:
|
||||
annotations = getattr(torch.nn.Module, "__annotations__", None)
|
||||
for name, value in root.__dict__.items():
|
||||
if annotations and name not in annotations:
|
||||
graph_module.__dict__[name] = value
|
||||
graph_module._forward_hooks = root._forward_hooks.copy()
|
||||
graph_module._forward_pre_hooks = root._forward_pre_hooks.copy()
|
||||
graph_module._backward_hooks = root._backward_hooks.copy()
|
||||
graph_module._backward_pre_hooks = root._backward_pre_hooks.copy()
|
||||
if graph_module._forward_hooks or graph_module._forward_pre_hooks:
|
||||
# Even forward hooks are traced through, they still capture a bunch
|
||||
# of state through closure. We need to make sure these data are
|
||||
# accessible through the captured module (but the hooks should be
|
||||
# disabled).
|
||||
assert getattr(graph_module, "_wrapped_call", None) is not None
|
||||
assert isinstance(
|
||||
graph_module._wrapped_call, torch.fx.graph_module._WrappedCall
|
||||
)
|
||||
assert graph_module._wrapped_call.cls_call is None
|
||||
|
||||
def dynamo_wrapped_call(self, *args: object, **kwargs: object) -> object:
|
||||
assert "forward" not in self.__dict__
|
||||
|
||||
fwd_hooks = self._forward_hooks
|
||||
fwd_pre_hooks = self._forward_pre_hooks
|
||||
original_forward = type(self).forward
|
||||
|
||||
def patched_forward(self, *args: object, **kwargs: object) -> object:
|
||||
self._forward_hooks = fwd_hooks
|
||||
self._forward_pre_hooks = fwd_pre_hooks
|
||||
return original_forward(self, *args, **kwargs)
|
||||
|
||||
try:
|
||||
self.forward = types.MethodType(patched_forward, self)
|
||||
# pyrefly: ignore [implicit-any]
|
||||
self._forward_hooks = {}
|
||||
# pyrefly: ignore [implicit-any]
|
||||
self._forward_pre_hooks = {}
|
||||
# pyrefly: ignore [invalid-argument]
|
||||
return super(type(self), self).__call__(*args, **kwargs)
|
||||
finally:
|
||||
self.__dict__.pop("forward")
|
||||
self._forward_hooks = fwd_hooks
|
||||
self._forward_pre_hooks = fwd_pre_hooks
|
||||
|
||||
# pyrefly: ignore [bad-assignment]
|
||||
graph_module._wrapped_call.cls_call = dynamo_wrapped_call
|
||||
|
||||
root = graph_module if isinstance(root, torch.nn.Module) else root
|
||||
input_processor = InputProcessor(root, len(args), list(kwargs.keys()))
|
||||
dynamo_bytecode_flatten = DynamoBytecodeFlatten(input_processor, out, f_globals)
|
||||
dynamo_bytecode_unflatten = DynamoBytecodeUnflatten(input_processor, out, f_globals)
|
||||
|
||||
graph_module.graph._codegen = _DynamoBytecodeCodeGen(
|
||||
argument_names(inspect.signature(mod), args, kwargs),
|
||||
dynamo_bytecode_flatten,
|
||||
dynamo_bytecode_unflatten,
|
||||
) # type: ignore[attr-defined]
|
||||
normalize_graph_module(graph_module)
|
||||
assert not hasattr(graph_module, "_dynamo_bytecode_flatten")
|
||||
assert not hasattr(graph_module, "_dynamo_bytecode_unflatten")
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
graph_module._dynamo_bytecode_flatten = dynamo_bytecode_flatten
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
graph_module._dynamo_bytecode_unflatten = dynamo_bytecode_unflatten
|
||||
delattr(graph_module, "_param_name_to_source")
|
||||
graph_module.recompile()
|
||||
graph_module.meta["module_call_specs"] = (
|
||||
out.graph_capture_output.output_graph.export_metadata.module_call_spec
|
||||
)
|
||||
assert out.backend_input is not None
|
||||
graph_module.meta["fake_mode"] = out.backend_input.fake_mode # type: ignore[attr-defined]
|
||||
graph_module.meta["fake_mode"].allow_non_fake_inputs = True
|
||||
tracing_context = TracingContext(graph_module.meta["fake_mode"])
|
||||
tracing_context.tensor_to_context = out.backend_input.tensor_to_context # type: ignore[attr-defined]
|
||||
graph_module.meta["tracing_context"] = tracing_context
|
||||
return graph_module
|
||||
|
||||
|
||||
class _DynamoBytecodeCodeGen(torch.fx.graph.CodeGen):
|
||||
def __init__(
|
||||
self,
|
||||
orig_arg_names: list[str],
|
||||
# pyrefly: ignore [implicit-any]
|
||||
dynamo_bytecode_flatten: Callable,
|
||||
# pyrefly: ignore [implicit-any]
|
||||
dynamo_bytecode_unflatten: Callable,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.orig_arg_names = orig_arg_names
|
||||
self.dynamo_bytecode_flatten = dynamo_bytecode_flatten
|
||||
self.dynamo_bytecode_unflatten = dynamo_bytecode_unflatten
|
||||
self.wrap_tuple = False
|
||||
self._inputs: tuple[Any, ...] | None = None
|
||||
|
||||
def process_inputs(self, *inputs: Any) -> Any:
|
||||
self._inputs = inputs
|
||||
results = self.dynamo_bytecode_flatten(*inputs)
|
||||
return results
|
||||
|
||||
def process_outputs(self, outputs: Any) -> Any:
|
||||
results = self.dynamo_bytecode_unflatten(outputs, self._inputs)
|
||||
if self.wrap_tuple:
|
||||
results = (results,)
|
||||
self._inputs = None
|
||||
return results
|
||||
|
||||
def gen_fn_def(
|
||||
self,
|
||||
free_vars: list[str],
|
||||
maybe_return_annotation: str,
|
||||
*,
|
||||
expanded_def: bool = False,
|
||||
) -> str:
|
||||
fn_args = self.orig_arg_names
|
||||
has_orig_self = (fn_args[0] == "self") if len(fn_args) > 0 else False
|
||||
if has_orig_self:
|
||||
free_vars.insert(0, "self")
|
||||
fn_definition = super().gen_fn_def(
|
||||
fn_args[:], maybe_return_annotation, expanded_def=expanded_def
|
||||
)
|
||||
|
||||
if len(free_vars) > 0: # pytree has placeholders in it
|
||||
fn_definition += self.gen_var_bindings(fn_args, free_vars, expanded_def)
|
||||
return fn_definition
|
||||
|
||||
def gen_var_bindings(
|
||||
self, fn_args: list[str], free_vars: list[str], expanded_def: bool
|
||||
) -> str:
|
||||
without_annotation = [x.split(":")[0].split("#")[0] for x in free_vars]
|
||||
if len(fn_args) == 0:
|
||||
fn_signature = ""
|
||||
elif len(fn_args) == 1:
|
||||
fn_signature = f"{fn_args[0]}, "
|
||||
else:
|
||||
fn_signature = f"{', '.join(fn_args)}"
|
||||
return f"""
|
||||
_fn_args = ({fn_signature})
|
||||
{", ".join(without_annotation)}, = self._dynamo_bytecode_flatten(*_fn_args)"""
|
||||
|
||||
def generate_output(
|
||||
self,
|
||||
output_args: torch.fx.node.Argument,
|
||||
*,
|
||||
descs: object | None = None,
|
||||
repr_fn: Any | None = None,
|
||||
) -> str:
|
||||
if repr_fn is None:
|
||||
repr_fn = repr
|
||||
# pyrefly: ignore [not-iterable]
|
||||
returned = f"self._dynamo_bytecode_unflatten(({', '.join([repr_fn(a) for a in output_args])},), _fn_args)"
|
||||
if self.wrap_tuple:
|
||||
returned = f"({returned},)"
|
||||
return f"return {returned}"
|
||||
|
||||
|
||||
def dynamo_graph_capture_for_export(
|
||||
fn: Callable[..., Any],
|
||||
constraints: list[Constraint] | None = None,
|
||||
) -> Callable[..., Any]:
|
||||
if isinstance(fn, torch._ops.OpOverload):
|
||||
|
||||
def default_annotation(arg: torch.Argument) -> str:
|
||||
if arg.has_default_value():
|
||||
return f"={arg.default_value!r}"
|
||||
return ""
|
||||
|
||||
has_kwarg_only = False
|
||||
arg_list = []
|
||||
for arg in fn._schema.arguments:
|
||||
if arg.kwarg_only and not has_kwarg_only:
|
||||
has_kwarg_only = True
|
||||
arg_list.append("*")
|
||||
arg_list.append(arg.name + default_annotation(arg))
|
||||
func_str = f"""
|
||||
def op_overload_wrapper({", ".join(arg_list)}):
|
||||
return op({", ".join([f"{arg.name}={arg.name}" for arg in fn._schema.arguments])})
|
||||
"""
|
||||
out = {}
|
||||
exec(func_str, {"op": fn}, out)
|
||||
fn = out["op_overload_wrapper"] # type: ignore[assignment]
|
||||
|
||||
def inner(*args: Any, **kwargs: Any) -> Any:
|
||||
assert not torch._dynamo.config.install_free_tensors
|
||||
with (
|
||||
_compiling_state_context(),
|
||||
torch._dynamo.config.patch(
|
||||
replay_side_effects=False, side_effect_replay_policy="warn"
|
||||
),
|
||||
get_metrics_context(),
|
||||
dynamo_timed("fullgraph_capture"),
|
||||
):
|
||||
out = fullgraph_capture(
|
||||
fn,
|
||||
args,
|
||||
kwargs,
|
||||
constraints=constraints,
|
||||
)
|
||||
graph_module = create_fx_graph_from_captured_output(out, fn, args, kwargs)
|
||||
return graph_module
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
def _dynamo_graph_capture_for_export(
|
||||
mod: Callable[..., Any],
|
||||
*,
|
||||
constraints: list[Constraint] | None = None,
|
||||
dynamic_shapes: dict[str, Any] | tuple[Any] | list[Any] | None = None,
|
||||
) -> Callable[..., torch.fx.GraphModule]:
|
||||
"""
|
||||
Improved dynamo graph capture using transformer approach with proper fake tensor handling.
|
||||
|
||||
This function creates a capture instance that handles:
|
||||
1. PyTree flattening/unflattening with proper input ordering
|
||||
2. Dynamo graph capture with export-specific context
|
||||
3. FX graph transformation for export compatibility
|
||||
4. Proper fake tensor metadata preservation
|
||||
5. Dynamic dimension constraint handling
|
||||
|
||||
Notable improvements over manual approach:
|
||||
- Uses FX Transformer for cleaner graph manipulation
|
||||
- Properly handles fake tensor metadata and dynamic dimensions
|
||||
- Preserves all necessary metadata for export
|
||||
- More robust error handling and edge case management
|
||||
|
||||
TODO:
|
||||
1. Are we actually gonna run the bytecode?
|
||||
2. Need to attach guards
|
||||
"""
|
||||
|
||||
_dynamic_shapes = dynamic_shapes
|
||||
_constraints = constraints
|
||||
|
||||
def inner(*args: Any, **kwargs: Any) -> torch.fx.GraphModule:
|
||||
# This sets the is_exporting flag when building guards.
|
||||
with _compiling_state_context():
|
||||
flat_inputs, in_spec = pytree.tree_flatten((args, kwargs))
|
||||
check_user_input_output(flat_inputs, UserErrorType.INVALID_INPUT)
|
||||
module_to_trace = ModuleToTrace(mod, in_spec)
|
||||
orig_callable = mod.forward if isinstance(mod, torch.nn.Module) else mod
|
||||
|
||||
constraints: list[Constraint] | None = _constraints
|
||||
dynamic_shapes: dict[str, Any] | tuple[Any] | list[Any] | None = (
|
||||
_dynamic_shapes
|
||||
)
|
||||
|
||||
from . import reset # type: ignore[attr-defined]
|
||||
|
||||
reset()
|
||||
|
||||
dynamo_config_ctx = torch._dynamo.config.patch(
|
||||
specialize_int=True,
|
||||
specialize_float=True,
|
||||
assume_static_by_default=True,
|
||||
automatic_dynamic_shapes=False,
|
||||
capture_dynamic_output_shape_ops=True,
|
||||
capture_scalar_outputs=True,
|
||||
constant_fold_autograd_profiler_enabled=True,
|
||||
log_graph_in_out_metadata=True,
|
||||
# install_free_tensors ensures that params and buffers are still
|
||||
# added as graph attributes, and makes Dynamo emits graphs that
|
||||
# follow export pytree-able input requirements In future, if we
|
||||
# fully rely on bytecode for the runtime, we can turn this flag
|
||||
# off.
|
||||
install_free_tensors=torch._dynamo.config.install_free_tensors_for_export,
|
||||
)
|
||||
|
||||
with (
|
||||
get_metrics_context(),
|
||||
dynamo_timed("fullgraph_capture"),
|
||||
dynamo_config_ctx,
|
||||
):
|
||||
out = fullgraph_capture(
|
||||
module_to_trace,
|
||||
tuple(flat_inputs),
|
||||
constraints=_constraints,
|
||||
_is_export_deprecated_do_not_use=True,
|
||||
)
|
||||
|
||||
assert out.graph_capture_output.output_graph is not None
|
||||
|
||||
example_inputs: list[Any] = []
|
||||
if out.backend_input is not None:
|
||||
graph = out.backend_input.graph_module
|
||||
fake_mode = out.backend_input.fake_mode
|
||||
example_inputs = out.backend_input.example_inputs
|
||||
else:
|
||||
graph = torch.fx.GraphModule(torch.nn.Module(), torch.fx.Graph())
|
||||
graph.graph.output(None)
|
||||
graph.recompile()
|
||||
fake_mode = None
|
||||
|
||||
_suggest_or_raise_constraint_violation(
|
||||
module_to_trace,
|
||||
orig_callable,
|
||||
fake_mode,
|
||||
out,
|
||||
args,
|
||||
kwargs,
|
||||
dynamic_shapes,
|
||||
)
|
||||
|
||||
# Extract export metadata from the new location
|
||||
export_metadata = out.graph_capture_output.output_graph.export_metadata
|
||||
graph_inputs = export_metadata.graph_input_idx_to_local_source
|
||||
graph_output_map = export_metadata.output_return_type
|
||||
out_spec = export_metadata.out_spec
|
||||
module_call_spec = export_metadata.module_call_spec
|
||||
|
||||
# Compute dynamic dimensions for each input based on constraints
|
||||
flat_args_dynamic_dims = [
|
||||
{
|
||||
c.dim
|
||||
for c in (constraints or ())
|
||||
if (
|
||||
c.t_id == id(x)
|
||||
and not isinstance(c, _RelaxedConstraint)
|
||||
and c.constraint_range.vr.lower != c.constraint_range.vr.upper
|
||||
)
|
||||
}
|
||||
for x in flat_inputs
|
||||
]
|
||||
|
||||
# Create input order mapping from dynamo's internal order to user order
|
||||
# Only process inputs that come from function arguments (GetItemSource).
|
||||
# Skip inputs that come from other sources like closures (e.g., captured
|
||||
# opaque objects like DeviceMesh).
|
||||
graph_input_order: dict[int, int] = {}
|
||||
for inp in graph_inputs:
|
||||
source = graph_inputs[inp]
|
||||
if isinstance(source, torch._dynamo.source.GetItemSource):
|
||||
graph_input_order[source.index] = len(graph_input_order)
|
||||
|
||||
for real_idx, graph_idx in graph_input_order.items():
|
||||
flat_inputs[real_idx] = example_inputs[graph_idx]
|
||||
|
||||
# Use FX transformer to rebuild the graph cleanly
|
||||
transformed_graph = DynamoGraphTransformer(
|
||||
graph,
|
||||
flat_inputs,
|
||||
flat_args_dynamic_dims,
|
||||
graph_input_order,
|
||||
graph_output_map,
|
||||
fake_mode,
|
||||
graph_inputs,
|
||||
).transform()
|
||||
|
||||
# Set up PyTree codegen for proper input/output handling
|
||||
transformed_graph.graph._codegen = _PyTreeCodeGen(
|
||||
_PyTreeInfo(
|
||||
argument_names(inspect.signature(orig_callable), args, kwargs), # type: ignore[attr-defined, arg-type]
|
||||
in_spec,
|
||||
out_spec,
|
||||
)
|
||||
)
|
||||
transformed_graph.recompile()
|
||||
|
||||
clean_nn_module_stack_and_source_fn(transformed_graph, True)
|
||||
clean_export_root(transformed_graph)
|
||||
|
||||
transformed_graph.meta["module_call_specs"] = module_call_spec
|
||||
transformed_graph.meta["fake_mode"] = fake_mode
|
||||
|
||||
return transformed_graph
|
||||
|
||||
return inner
|
||||
@@ -0,0 +1,32 @@
|
||||
USER_ERROR = [
|
||||
"Your code may result in an error when running in eager. "
|
||||
"Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. "
|
||||
'You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance("force_eager")`. '
|
||||
]
|
||||
DYNAMO_BUG = [
|
||||
"This is likely to be a Dynamo bug. Please report an issue to PyTorch.",
|
||||
]
|
||||
DIFFICULT = [
|
||||
"This graph break may be difficult to debug. Please report an issue to PyTorch for assistance.",
|
||||
]
|
||||
FUNDAMENTAL = [
|
||||
"This graph break is fundamental - it is unlikely that Dynamo will ever be able to trace through "
|
||||
"your code. Consider finding a workaround.",
|
||||
]
|
||||
SUPPORTABLE = [
|
||||
"It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you "
|
||||
"encounter this graph break often and it is causing performance issues.",
|
||||
]
|
||||
CAUSED_BY_EARLIER_GRAPH_BREAK = [
|
||||
"This graph break may have been caused by an earlier graph break. Resolving the earlier graph break may resolve this one.",
|
||||
]
|
||||
INFERENCE_MODE = [
|
||||
"Avoid using `tensor.is_inference()` and `torch.is_inference_mode_enabled()` in your compile code. "
|
||||
"This is primarily used in conjunction with `torch.inference_mode`. Consider using `torch.no_grad` instead "
|
||||
"because `torch.no_grad` leads to same improvements as `inference_mode` when `torch.compile` is used.",
|
||||
]
|
||||
SPARSE_TENSOR = [
|
||||
"Sparse tensor operations are not yet fully supported in torch.compile with fullgraph=True. "
|
||||
"Consider using fullgraph=False to allow graph breaks, or move sparse tensor creation "
|
||||
"outside the compiled region.",
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,116 @@
|
||||
import weakref
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from torch._dynamo.source import Source
|
||||
|
||||
|
||||
PyCodegen = Any
|
||||
|
||||
# This file is to handle types that we don't want to support
|
||||
# as explicit FX graph inputs. This uses a sidetable which
|
||||
# we populate in bytecode and is loaded during graph execution
|
||||
|
||||
# We use a dynamo-generated index as a level of indirection
|
||||
# this allows us to register objects externally in pre-graph bytecode that we want
|
||||
# to pass to the graph, but not support their types as graph inputs
|
||||
index_to_bytecode_constructor: dict[int, Callable[[PyCodegen], None]] = {}
|
||||
|
||||
index_to_external_object_weakref: dict[int, weakref.ReferenceType[Any]] = {}
|
||||
|
||||
keep_alive: list[Any] = []
|
||||
|
||||
|
||||
def has_user_objects() -> bool:
|
||||
return bool(index_to_bytecode_constructor)
|
||||
|
||||
|
||||
def stash_graph_created_object(obj: Any) -> Any:
|
||||
keep_alive.append(obj)
|
||||
return obj
|
||||
|
||||
|
||||
CURRENT_STREAM_INDEX = 0
|
||||
|
||||
|
||||
def set_external_object_by_index(index: int, value: Any) -> None:
|
||||
"""Update an entry in the external object registry at runtime."""
|
||||
keep_alive.append(value)
|
||||
index_to_external_object_weakref[index] = weakref.ref(value)
|
||||
|
||||
|
||||
def get_external_object_by_index(index: int) -> Any:
|
||||
assert index in index_to_external_object_weakref, (
|
||||
"Index not registered in index_to_user_object_weakref"
|
||||
)
|
||||
obj = index_to_external_object_weakref[index]()
|
||||
assert obj is not None, "User object is no longer alive"
|
||||
return index_to_external_object_weakref[index]()
|
||||
|
||||
|
||||
def store_user_object_weakrefs(*args: Any) -> None:
|
||||
global index_to_external_object_weakref
|
||||
index_to_external_object_weakref.clear()
|
||||
index_to_external_object_weakref.update(
|
||||
{i: weakref.ref(arg) for i, arg in enumerate(args)}
|
||||
)
|
||||
|
||||
|
||||
def reset_user_object_tracking() -> None:
|
||||
index_to_bytecode_constructor.clear()
|
||||
index_to_external_object_weakref.clear()
|
||||
keep_alive.clear()
|
||||
|
||||
|
||||
def register_graph_created_object(
|
||||
example_value: Any, construct_fn: Callable[[int, PyCodegen], None]
|
||||
) -> int:
|
||||
global index_to_bytecode_constructor
|
||||
global keep_alive
|
||||
keep_alive.append(example_value)
|
||||
index = len(index_to_bytecode_constructor)
|
||||
index_to_bytecode_constructor[index] = lambda cg: construct_fn(index, cg)
|
||||
try:
|
||||
index_to_external_object_weakref[index] = weakref.ref(example_value)
|
||||
except TypeError as e:
|
||||
from .exc import unimplemented
|
||||
|
||||
unimplemented(
|
||||
gb_type="Failed to make weakref to graph-created external object",
|
||||
context=f"user_object: {example_value}",
|
||||
explanation="Object does not allow us to make a weakref to it",
|
||||
hints=[],
|
||||
from_exc=e,
|
||||
)
|
||||
return index
|
||||
|
||||
|
||||
# Register a user object to be used in the graph
|
||||
def register_user_object(value: Any, source: Source) -> int:
|
||||
global index_to_bytecode_constructor
|
||||
index = len(index_to_bytecode_constructor)
|
||||
index_to_bytecode_constructor[index] = lambda cg: cg(source)
|
||||
try:
|
||||
index_to_external_object_weakref[index] = weakref.ref(value)
|
||||
except TypeError as e:
|
||||
from .exc import unimplemented
|
||||
|
||||
unimplemented(
|
||||
gb_type="Failed to make weakref to User Object",
|
||||
context=f"user_object: {value}",
|
||||
explanation="Object does not allow us to make a weakref to it",
|
||||
hints=[],
|
||||
from_exc=e,
|
||||
)
|
||||
return index
|
||||
|
||||
|
||||
# Register a callback so invoke_leaf_function can retrieve nn.Module instances at runtime.
|
||||
# We use a callback pattern instead of having invoke_leaf_function import get_external_object_by_index
|
||||
# directly, because higher-order ops should not depend on dynamo (dynamo depends on them, not vice versa).
|
||||
from torch._higher_order_ops.invoke_leaf_function import (
|
||||
set_leaf_function_module_retriever,
|
||||
)
|
||||
|
||||
|
||||
set_leaf_function_module_retriever(get_external_object_by_index)
|
||||
@@ -0,0 +1,609 @@
|
||||
"""
|
||||
This module implements graph deduplication functionality for TorchDynamo's optimization pipeline.
|
||||
Graph deduplication identifies identical subgraphs in the computational graph and merges them
|
||||
to reduce redundancy and improve performance. The process involves analyzing regions of the graph,
|
||||
identifying structurally equivalent regions, and replacing them with a single shared implementation.
|
||||
This optimization is particularly effective for models with repeated patterns or similar computational
|
||||
structures across different parts of the network.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import operator
|
||||
from collections import defaultdict, deque
|
||||
from collections.abc import Generator, Iterable
|
||||
|
||||
import torch
|
||||
import torch.fx
|
||||
from torch._dynamo import config
|
||||
from torch.multiprocessing.reductions import StorageWeakRef
|
||||
from torch.utils._ordered_set import OrderedSet
|
||||
|
||||
from .graph_region_tracker import Node, Region
|
||||
from .graph_utils import _detect_cycles, _get_flat_args, _get_flat_args_unique
|
||||
|
||||
|
||||
# Represents an index into the region
|
||||
# to select a node and then
|
||||
# an index into that node's
|
||||
# flattened arguments
|
||||
UsageIndex = tuple[int, int]
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
last_node_to_additional_deps: dict[Node, OrderedSet[Node]] | None = None
|
||||
|
||||
|
||||
def apply_graph_deduplication(output_graph) -> dict[str, torch.fx.GraphModule]: # type: ignore[no-untyped-def]
|
||||
"""
|
||||
This is the main entry point for applying the graph deduplication pass. \
|
||||
Deduplication occurs in two phases:
|
||||
1. Subgraph creation:
|
||||
Subgraph creation works by taking one representative region from each region \
|
||||
group and creating a subgraph from it, which will then be used to replace all regions \
|
||||
in the group. This is implemented by first copying all nodes of the region to the new \
|
||||
subgraph and then finding all inputs which are not within the region and creating placeholders \
|
||||
for them. For the outputs, all regions in a region group need to be scanned to ensure the \
|
||||
largest set of outputs is found, and then an output node is created which returns \
|
||||
a tuple of all outputs.
|
||||
|
||||
2. Graph replacement:
|
||||
To replace each region with the extracted subgraph, the node index in the region \
|
||||
and argument index within the node's flattened args and kwargs are recorded once during \
|
||||
subgraph creation. This allows us to determine which (external to the region) nodes and \
|
||||
in which order these nodes are passed as inputs. For the outputs, getitem nodes are created \
|
||||
for each output, and all nodes in the region with external outputs are replaced by the proper \
|
||||
getitem node. Finally, all original nodes are erased (there should be no uses of these \
|
||||
left in the graph).
|
||||
|
||||
The deduplication mutates the output_graph argument in place.
|
||||
|
||||
Returns a mapping of nodes to their subgraph output replacement node to remap outputs
|
||||
when they are created in output_graph.
|
||||
"""
|
||||
|
||||
duplicated_region_groups = output_graph.region_tracker.get_identical_regions(
|
||||
output_graph.graph
|
||||
)
|
||||
node_to_mutated_arg_positions = (
|
||||
output_graph.region_tracker.node_to_mutated_arg_positions
|
||||
)
|
||||
node_to_additional_deps = _populate_additional_deps(
|
||||
output_graph.graph, output_graph.region_tracker.node_to_mutated_arg_positions
|
||||
)
|
||||
|
||||
sub_gms: dict[str, torch.fx.GraphModule] = {}
|
||||
|
||||
for region_group in duplicated_region_groups:
|
||||
inds_with_external_users = _get_all_output_indices(region_group)
|
||||
region = region_group[0]
|
||||
(
|
||||
subgraph,
|
||||
external_node_usages,
|
||||
node_usage_to_tuple_elems,
|
||||
ind_to_tuple_spec,
|
||||
) = _create_subgraph(region, inds_with_external_users)
|
||||
|
||||
# Ignore regions with no args for now, could they possibly be evaluated at compile time?
|
||||
if not list(external_node_usages):
|
||||
continue
|
||||
|
||||
sub_gm = torch.fx.GraphModule(output_graph.nn_modules, subgraph)
|
||||
subgraph_name = output_graph.install_subgraph("subgraph", sub_gm)
|
||||
sub_gms[subgraph_name] = sub_gm
|
||||
with output_graph.graph.inserting_before():
|
||||
get_subgraph_node = output_graph.graph.create_node(
|
||||
"get_attr", subgraph_name, (), {}
|
||||
)
|
||||
|
||||
for region in region_group:
|
||||
_replace_region_with_subgraph(
|
||||
output_graph.graph,
|
||||
region,
|
||||
get_subgraph_node,
|
||||
external_node_usages,
|
||||
node_usage_to_tuple_elems,
|
||||
ind_to_tuple_spec,
|
||||
inds_with_external_users,
|
||||
subgraph_name,
|
||||
node_to_additional_deps,
|
||||
node_to_mutated_arg_positions,
|
||||
)
|
||||
|
||||
# This is to expose the updated node_to_additional_deps to tests
|
||||
global last_node_to_additional_deps
|
||||
last_node_to_additional_deps = node_to_additional_deps
|
||||
|
||||
_stable_topological_sort(
|
||||
output_graph.graph,
|
||||
node_to_additional_deps,
|
||||
)
|
||||
return sub_gms
|
||||
|
||||
|
||||
def _replace_region_with_subgraph(
|
||||
graph: torch.fx.Graph,
|
||||
region: Region,
|
||||
get_subgraph_node: Node,
|
||||
external_node_usages: Iterable[OrderedSet[UsageIndex]],
|
||||
node_usage_to_tuple_elems: dict[UsageIndex, OrderedSet[int]],
|
||||
ind_to_tuple_spec: dict[int, dict[tuple[int, ...], int]],
|
||||
inds_with_external_users: list[int],
|
||||
subgraph_name: str,
|
||||
node_to_additional_deps: dict[Node, OrderedSet[Node]],
|
||||
node_to_mutated_arg_positions: dict[Node, OrderedSet[int]],
|
||||
) -> None:
|
||||
sub_args = []
|
||||
flattened_getitem_nodes: OrderedSet[Node] = OrderedSet()
|
||||
for usages in external_node_usages:
|
||||
usage = next(iter(usages))
|
||||
node_ind, usage_ind = usage
|
||||
node = region[node_ind]
|
||||
flattened_args_kwargs = _get_flat_args(node, {})
|
||||
for user_ind, node_usage_ind in usages:
|
||||
user = region[user_ind]
|
||||
if user in node_to_mutated_arg_positions:
|
||||
if node_usage_ind in node_to_mutated_arg_positions[user]:
|
||||
log.debug(
|
||||
"NYI: Failed to substitute region %s due to mutation", region
|
||||
)
|
||||
return
|
||||
if usage in node_usage_to_tuple_elems:
|
||||
tuple_elems = [region[i] for i in node_usage_to_tuple_elems[usage]]
|
||||
flattened_getitem_nodes.update(tuple_elems)
|
||||
sub_args.extend(tuple_elems)
|
||||
else:
|
||||
sub_args.append(flattened_args_kwargs[usage_ind])
|
||||
|
||||
# Input/Output aliasing not supported in HOPs today
|
||||
# Note: we should use the nodes in the original graph (the region here)
|
||||
# because we use the original traced example values for this check
|
||||
if _has_aliasing(
|
||||
region, sub_args, inds_with_external_users, flattened_getitem_nodes
|
||||
):
|
||||
return
|
||||
|
||||
invoke_args = (get_subgraph_node, subgraph_name, *sub_args)
|
||||
|
||||
invoke_subgraph_node = graph.create_node(
|
||||
"call_function",
|
||||
torch.ops.higher_order.invoke_subgraph,
|
||||
invoke_args, # type: ignore[arg-type]
|
||||
{},
|
||||
)
|
||||
|
||||
ind = 0
|
||||
flattened_output_nodes: OrderedSet[Node] = OrderedSet()
|
||||
for external_user_ind in inds_with_external_users:
|
||||
node = region[external_user_ind]
|
||||
if _is_tuple_node(node):
|
||||
tuple_spec = ind_to_tuple_spec[external_user_ind]
|
||||
flattened_output_nodes.update(
|
||||
_replace_tuple_outputs(
|
||||
node, ind, tuple_spec, invoke_subgraph_node, graph
|
||||
)
|
||||
)
|
||||
ind += len(tuple_spec)
|
||||
else:
|
||||
subgraph_output = graph.create_node(
|
||||
"call_function", operator.getitem, (invoke_subgraph_node, ind), {}
|
||||
)
|
||||
node.replace_all_uses_with(subgraph_output, propagate_meta=True)
|
||||
ind += 1
|
||||
|
||||
# Erase in reverse topological order
|
||||
for node in reversed(region):
|
||||
if node in flattened_getitem_nodes:
|
||||
# Don't erase these, since they will still be used
|
||||
continue
|
||||
|
||||
if node not in flattened_output_nodes:
|
||||
graph.erase_node(node)
|
||||
|
||||
# Remove any nodes with additional deps
|
||||
# This is safe; we've guaranteed that there is
|
||||
# no input mutation, so all additional deps
|
||||
# will be internal to the subgraph
|
||||
node_to_additional_deps.pop(node, None)
|
||||
for deps in node_to_additional_deps.values():
|
||||
try:
|
||||
deps.remove(node)
|
||||
deps.add(invoke_subgraph_node)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if config.graph_deduplication_lint:
|
||||
print(_detect_cycles(graph, node_to_additional_deps))
|
||||
_stable_topological_sort(graph, node_to_additional_deps)
|
||||
graph.lint()
|
||||
|
||||
|
||||
def _get_external_inputs(
|
||||
region: Region,
|
||||
) -> dict[Node, OrderedSet[UsageIndex]]:
|
||||
external_node_to_usages = defaultdict[Node, OrderedSet[UsageIndex]](OrderedSet)
|
||||
region_unique = set(region)
|
||||
for node_ind, node in enumerate(region):
|
||||
flattened_args_kwargs = _get_flat_args(node, {})
|
||||
for arg_ind, in_node in enumerate(flattened_args_kwargs):
|
||||
if isinstance(in_node, Node) and in_node not in region_unique:
|
||||
# in_node may occur in multiple nodes' flat_args
|
||||
# track this so we can check if the arg is mutated
|
||||
# Previously, we only needed to track one occurrence
|
||||
# to be able to map that node to a placeholder
|
||||
external_node_to_usages[in_node].add((node_ind, arg_ind))
|
||||
|
||||
return external_node_to_usages
|
||||
|
||||
|
||||
def _get_all_output_indices(regions: list[Region]) -> list[int]:
|
||||
# Scan all regions to get the set of all possible output nodes indices in the region
|
||||
# perhaps we can record this information during region creation for more efficiency?
|
||||
inds_with_external_users: set[int] = set()
|
||||
for region in regions:
|
||||
_get_inds_with_external_users(region, inds_with_external_users)
|
||||
|
||||
return sorted(inds_with_external_users)
|
||||
|
||||
|
||||
def _get_inds_with_external_users(region: Region, inds_unique: set[int]) -> None:
|
||||
for ind, node in enumerate(region):
|
||||
for user in node.users:
|
||||
if user not in region:
|
||||
if ind not in inds_unique:
|
||||
inds_unique.add(ind)
|
||||
|
||||
|
||||
def _create_subgraph(
|
||||
region: Region,
|
||||
inds_with_external_users: list[int],
|
||||
) -> tuple[
|
||||
torch.fx.Graph,
|
||||
list[OrderedSet[UsageIndex]],
|
||||
dict[UsageIndex, OrderedSet[int]],
|
||||
dict[int, dict[tuple[int, ...], int]],
|
||||
]:
|
||||
subgraph: torch.fx.Graph = torch.fx.Graph()
|
||||
external_input_to_usages = _get_external_inputs(region)
|
||||
external_node_usages = list[OrderedSet[UsageIndex]]()
|
||||
region_to_subgraph_node = {}
|
||||
flattened_getitem_nodes: OrderedSet[Node] = OrderedSet()
|
||||
node_usage_to_tuple_elems: dict[UsageIndex, OrderedSet[int]] = {}
|
||||
|
||||
for node, usage_indices in external_input_to_usages.items():
|
||||
# We don't handle tuples as inputs today
|
||||
if _is_tuple_node(node):
|
||||
# If a node is a tuple we will possibly create multiple placeholders for them
|
||||
# and track which nodes we won't copy into the subgraph because they are flattened away
|
||||
# Later, when replacing each region with this subgraph, we will create a getitem node
|
||||
# externally which will perform the flattening on the outer nodes.
|
||||
flattened_node_indices = _get_flattened_node_indices(node, region)
|
||||
for ind in flattened_node_indices:
|
||||
placeholder = subgraph.placeholder(
|
||||
f"supgraph_input_{node.name}_flattened_{ind}"
|
||||
)
|
||||
region_to_subgraph_node[region[ind]] = placeholder
|
||||
flattened_getitem_nodes.add(region[ind])
|
||||
node_usage_to_tuple_elems[next(iter(usage_indices))] = (
|
||||
flattened_node_indices
|
||||
)
|
||||
else:
|
||||
placeholder = subgraph.placeholder(f"subgraph_input_{node.name}")
|
||||
region_to_subgraph_node[node] = placeholder
|
||||
|
||||
external_node_usages.append(usage_indices)
|
||||
|
||||
def map_arg(node: Node) -> Node:
|
||||
if node in region_to_subgraph_node:
|
||||
return region_to_subgraph_node[node]
|
||||
else:
|
||||
return node
|
||||
|
||||
def copy_to_subgraph(node: Node) -> Node:
|
||||
subgraph_node = subgraph.node_copy(node, lambda old: map_arg(old))
|
||||
region_to_subgraph_node[node] = subgraph_node
|
||||
return subgraph_node
|
||||
|
||||
output_list = []
|
||||
ind_to_tuple_spec = {}
|
||||
for ind, node in enumerate(region):
|
||||
if node not in flattened_getitem_nodes:
|
||||
subgraph_node = copy_to_subgraph(node)
|
||||
if ind in inds_with_external_users:
|
||||
# flatten tuple outputs by generating a getitem node tree
|
||||
if _is_tuple_node(node):
|
||||
getitem_nodes, ind_to_tuple_spec[ind] = _create_getitem_nodes(
|
||||
node, subgraph_node, subgraph
|
||||
)
|
||||
output_list.extend(getitem_nodes)
|
||||
else:
|
||||
output_list.append(subgraph_node)
|
||||
|
||||
subgraph.output(tuple(output_list))
|
||||
|
||||
return subgraph, external_node_usages, node_usage_to_tuple_elems, ind_to_tuple_spec
|
||||
|
||||
|
||||
def _stable_topological_sort_impl(
|
||||
graph: torch.fx.Graph,
|
||||
node_to_additional_deps: dict[Node, OrderedSet[Node]],
|
||||
do_sort: bool = True,
|
||||
) -> bool:
|
||||
# Nodes are in exactly one of these four collections:
|
||||
|
||||
# - Nodes in `pending` are waiting to be processed (in reverse order):
|
||||
pending = list(reversed(graph.nodes))
|
||||
|
||||
# - Nodes in `ready` have been processed and are already in the correct
|
||||
# order.
|
||||
ready = OrderedSet[Node]()
|
||||
|
||||
# - `waiting` is a mapping from a dependency to nodes which depend on that
|
||||
# dependency.
|
||||
waiting = defaultdict(list)
|
||||
|
||||
# - `outputs` are always at the end of the graph
|
||||
outputs = OrderedSet[Node]()
|
||||
|
||||
# The cursor indicates the last processed node so we can add new nodes
|
||||
# after it.
|
||||
cursor = None
|
||||
while pending:
|
||||
node = pending.pop()
|
||||
|
||||
if node.target == "output":
|
||||
outputs.add(node)
|
||||
assert not node.users, "output nodes should have no users"
|
||||
continue
|
||||
|
||||
waiting_for = [
|
||||
x
|
||||
for x in _get_flat_args_unique(node, node_to_additional_deps)
|
||||
if x not in ready
|
||||
]
|
||||
if waiting_for:
|
||||
# We have unprocessed input nodes. Might as well wait for the last
|
||||
# arg so an already sorted list will only recheck this node once.
|
||||
waiting[waiting_for[-1]].append(node)
|
||||
else:
|
||||
ready.add(node)
|
||||
if cursor and cursor.next is not node and do_sort:
|
||||
cursor.append(node)
|
||||
cursor = node
|
||||
# Mark the nodes that have been waiting for this node to finish as
|
||||
# ready to check again.
|
||||
pending.extend(reversed(waiting.pop(node, ())))
|
||||
|
||||
ready.update(outputs)
|
||||
return not waiting and len(ready) == len(graph.nodes)
|
||||
|
||||
|
||||
def _stable_topological_sort(
|
||||
graph: torch.fx.Graph,
|
||||
node_to_additional_deps: dict[Node, OrderedSet[Node]],
|
||||
) -> None:
|
||||
assert _stable_topological_sort_impl(graph, node_to_additional_deps)
|
||||
|
||||
|
||||
def _has_cycle(
|
||||
graph: torch.fx.Graph,
|
||||
node_to_additional_deps: dict[Node, OrderedSet[Node]],
|
||||
) -> bool:
|
||||
return not _stable_topological_sort_impl(
|
||||
graph, node_to_additional_deps, do_sort=False
|
||||
)
|
||||
|
||||
|
||||
def _populate_additional_deps(
|
||||
graph: torch.fx.Graph, node_to_mutated_arg_positions: dict[Node, OrderedSet[int]]
|
||||
) -> dict[Node, OrderedSet[Node]]:
|
||||
node_to_additional_deps: dict[Node, OrderedSet[Node]] = defaultdict(OrderedSet)
|
||||
_add_mutation_dependencies(node_to_mutated_arg_positions, node_to_additional_deps)
|
||||
_add_global_state_dependencies(graph, node_to_additional_deps)
|
||||
return node_to_additional_deps
|
||||
|
||||
|
||||
def _add_global_state_dependencies(
|
||||
graph: torch.fx.Graph, node_to_additional_deps: dict[Node, OrderedSet[Node]]
|
||||
) -> None:
|
||||
import torch.amp
|
||||
|
||||
all_nodes = list(graph.nodes)
|
||||
|
||||
# These are targets of the nodes which need to stay in the same relative place in the graph
|
||||
global_state_targets = {torch.amp._enter_autocast, torch.amp._exit_autocast}
|
||||
all_nodes_dep_on: list[Node] = []
|
||||
|
||||
def prev_cur_nodes(
|
||||
all_nodes: list[Node],
|
||||
) -> Generator[tuple[list[Node], Node], None, None]:
|
||||
prev_nodes: list[Node] = []
|
||||
next_nodes = list(reversed(all_nodes))
|
||||
|
||||
while next_nodes:
|
||||
cur_node = next_nodes.pop()
|
||||
yield prev_nodes, cur_node
|
||||
prev_nodes.append(cur_node)
|
||||
|
||||
for prev_nodes, cur_node in prev_cur_nodes(all_nodes):
|
||||
args_unique = _get_flat_args_unique(cur_node, {})
|
||||
new_deps = [n for n in all_nodes_dep_on if n not in args_unique]
|
||||
|
||||
if new_deps:
|
||||
additional_deps = node_to_additional_deps[cur_node]
|
||||
additional_deps.update(new_deps)
|
||||
|
||||
if cur_node.target in global_state_targets:
|
||||
additional_deps = node_to_additional_deps[cur_node]
|
||||
additional_deps.update(n for n in prev_nodes if n not in args_unique)
|
||||
all_nodes_dep_on.append(cur_node)
|
||||
|
||||
|
||||
def _add_mutation_dependencies(
|
||||
node_to_mutated_arg_positions: dict[Node, OrderedSet[int]],
|
||||
node_to_additional_deps: dict[Node, OrderedSet[Node]],
|
||||
) -> None:
|
||||
for node, indices in node_to_mutated_arg_positions.items():
|
||||
flat_args_kwargs = _get_flat_args(node, {})
|
||||
|
||||
# for all mutated args,
|
||||
# add dependency on usages which occur after node to ensure
|
||||
# node will always be ordered before them
|
||||
# also add node as a dependency on usages which
|
||||
# occur before node to ensure node is ordered after them
|
||||
for index in indices:
|
||||
mutated_arg = flat_args_kwargs[index]
|
||||
for user in mutated_arg.users:
|
||||
if user is node:
|
||||
continue
|
||||
|
||||
elif user < node:
|
||||
node_to_additional_deps[node].add(user)
|
||||
|
||||
elif user > node:
|
||||
node_to_additional_deps[user].add(node)
|
||||
|
||||
|
||||
def _has_aliasing(
|
||||
region: Region,
|
||||
inputs: list[Node],
|
||||
inds_with_external_users: list[int],
|
||||
flattened_getitem_nodes: OrderedSet[Node],
|
||||
) -> bool:
|
||||
input_storages: dict[StorageWeakRef, Node] = dict()
|
||||
for node in inputs:
|
||||
if node in flattened_getitem_nodes:
|
||||
continue
|
||||
example_value = node.meta["example_value"]
|
||||
if isinstance(example_value, torch.Tensor):
|
||||
storage = StorageWeakRef(example_value._typed_storage())
|
||||
if storage in input_storages:
|
||||
# input-input aliasing
|
||||
log.debug(
|
||||
"NYI: Failed to substitute region %s due to input-output aliasing detected at nodes %s, %s",
|
||||
region,
|
||||
input_storages[storage],
|
||||
node,
|
||||
)
|
||||
return True
|
||||
input_storages[storage] = node
|
||||
output_storages: dict[StorageWeakRef, Node] = dict()
|
||||
for i in inds_with_external_users:
|
||||
out_node = region[i]
|
||||
if out_node in flattened_getitem_nodes:
|
||||
continue
|
||||
if out_node:
|
||||
example_value = out_node.meta["example_value"]
|
||||
assert not isinstance(example_value, list)
|
||||
if isinstance(example_value, torch.Tensor):
|
||||
storage = StorageWeakRef(example_value._typed_storage())
|
||||
if storage in output_storages:
|
||||
# output-output aliasing
|
||||
log.debug(
|
||||
"NYI: Failed to substitute region %s due to output-output aliasing detected at nodes %s, %s",
|
||||
region,
|
||||
output_storages[storage],
|
||||
out_node,
|
||||
)
|
||||
return True
|
||||
output_storages[storage] = out_node
|
||||
intersected_storages = input_storages.keys() & output_storages.keys()
|
||||
if len(intersected_storages) > 0:
|
||||
# input-output aliasing
|
||||
aliased = [
|
||||
(input_storages[s], output_storages[s]) for s in intersected_storages
|
||||
]
|
||||
aliased = ", ".join([f"{i} and {o}" for i, o in aliased])
|
||||
log.debug(
|
||||
"NYI: Failed to substitute region %s due to input-output aliasing detected at nodes %s",
|
||||
region,
|
||||
aliased,
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _is_tuple_node(node: Node) -> bool:
|
||||
return isinstance(node.meta["example_value"], tuple)
|
||||
|
||||
|
||||
def _get_children_getitems(node: Node) -> Generator[Node, None, None]:
|
||||
for user in node.users:
|
||||
if user.target is operator.getitem and isinstance(user.args[1], int):
|
||||
yield user
|
||||
|
||||
|
||||
def _get_flattened_node_indices(node: Node, region: Region) -> OrderedSet[int]:
|
||||
"""Returns an ordered set of indices, each representing a node in the region which will be flattened"""
|
||||
flattened_node_to_ind = {n: i for i, n in enumerate(region)}
|
||||
node_indices: OrderedSet[int] = OrderedSet()
|
||||
queue = deque(_get_children_getitems(node))
|
||||
while queue:
|
||||
cur_node = queue.popleft()
|
||||
if any(user in region for user in cur_node.users):
|
||||
node_indices.add(flattened_node_to_ind[cur_node])
|
||||
for child in _get_children_getitems(cur_node):
|
||||
queue.append(child)
|
||||
return node_indices
|
||||
|
||||
|
||||
def _create_getitem_nodes(
|
||||
node: Node, subgraph_tuple_node: Node, subgraph: torch.fx.Graph
|
||||
) -> tuple[list[Node], dict[tuple[int, ...], int]]:
|
||||
tup = node.meta["example_value"]
|
||||
assert isinstance(tup, tuple), "_get_getitem_children expects tuple"
|
||||
|
||||
getitem_nodes: list[Node] = []
|
||||
queue = deque([(e, (i,), subgraph_tuple_node) for i, e in enumerate(tup)])
|
||||
path_to_output_index = {}
|
||||
|
||||
while queue:
|
||||
cur_elem, path, parent = queue.popleft()
|
||||
|
||||
with subgraph.inserting_after(parent):
|
||||
new_getitem_node = subgraph.create_node(
|
||||
"call_function", operator.getitem, (parent, path[-1]), {}
|
||||
)
|
||||
new_getitem_node.meta["example_value"] = cur_elem
|
||||
|
||||
path_to_output_index[path] = len(getitem_nodes)
|
||||
getitem_nodes.append(new_getitem_node)
|
||||
|
||||
if isinstance(cur_elem, tuple):
|
||||
queue.extend(
|
||||
[(e, path + (i,), new_getitem_node) for i, e in enumerate(cur_elem)] # type: ignore[arg-type,misc]
|
||||
)
|
||||
|
||||
return getitem_nodes, path_to_output_index # type: ignore[return-value]
|
||||
|
||||
|
||||
def _replace_tuple_outputs(
|
||||
node: Node,
|
||||
output_index: int,
|
||||
tuple_spec: dict[tuple[int, ...], int],
|
||||
invoke_subgraph_node: Node,
|
||||
graph: torch.fx.Graph,
|
||||
) -> OrderedSet[Node]:
|
||||
assert _is_tuple_node(node), "_replace_tuple_outputs expects a tuple node"
|
||||
|
||||
queue = deque((c, (c.args[1],)) for c in _get_children_getitems(node))
|
||||
erased_nodes: OrderedSet[Node] = OrderedSet()
|
||||
while queue:
|
||||
cur_node, path = queue.pop()
|
||||
|
||||
for c in _get_children_getitems(cur_node):
|
||||
queue.append((c, path + (c.args[1],))) # type: ignore[return-value, arg-type]
|
||||
|
||||
with graph.inserting_after(invoke_subgraph_node):
|
||||
subgraph_output = graph.create_node(
|
||||
"call_function",
|
||||
operator.getitem,
|
||||
(invoke_subgraph_node, output_index + tuple_spec[path]), # type: ignore[index]
|
||||
{},
|
||||
)
|
||||
cur_node.replace_all_uses_with(subgraph_output, propagate_meta=True)
|
||||
graph.erase_node(cur_node)
|
||||
erased_nodes.add(cur_node)
|
||||
|
||||
graph.erase_node(node)
|
||||
erased_nodes.add(node)
|
||||
return erased_nodes
|
||||
@@ -0,0 +1,473 @@
|
||||
# mypy: disallow-untyped-defs
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import logging
|
||||
import re
|
||||
import warnings
|
||||
from typing import Any, Generic, TYPE_CHECKING, TypeVar
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from torch._guards import CompileId
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GraphIdFilter:
|
||||
"""
|
||||
A filter for matching graph IDs based on various conditions.
|
||||
Supports individual IDs, ranges, and comparison operators.
|
||||
"""
|
||||
|
||||
def __init__(self, filter_str: str) -> None:
|
||||
self._explicit_ids: frozenset[int] = frozenset()
|
||||
self._conditions: list[tuple[str, int]] = []
|
||||
self._parse(filter_str)
|
||||
|
||||
def _parse(self, filter_str: str) -> None:
|
||||
if not filter_str or not filter_str.strip():
|
||||
return
|
||||
|
||||
explicit_ids: set[int] = set()
|
||||
conditions: list[tuple[str, int]] = []
|
||||
|
||||
# Pattern for comparison operators (>=, >, <=, <) followed by a number
|
||||
cmp_pattern = re.compile(r"^(>=|>|<=|<)(\d+)$")
|
||||
# Pattern for ranges like "10-20"
|
||||
range_pattern = re.compile(r"^(\d+)-(\d+)$")
|
||||
|
||||
for part in filter_str.split(","):
|
||||
part = part.strip()
|
||||
if not part:
|
||||
continue
|
||||
|
||||
if match := cmp_pattern.match(part):
|
||||
conditions.append((match.group(1), int(match.group(2))))
|
||||
elif match := range_pattern.match(part):
|
||||
start, end = int(match.group(1)), int(match.group(2))
|
||||
explicit_ids.update(range(start, end + 1))
|
||||
else:
|
||||
try:
|
||||
explicit_ids.add(int(part))
|
||||
except ValueError:
|
||||
log.warning("Invalid graph ID filter: %s", part)
|
||||
|
||||
self._explicit_ids = frozenset(explicit_ids)
|
||||
self._conditions = conditions
|
||||
|
||||
def __contains__(self, graph_id: int) -> bool:
|
||||
"""Check if the given graph ID matches this filter."""
|
||||
if graph_id in self._explicit_ids:
|
||||
return True
|
||||
|
||||
for op, val in self._conditions:
|
||||
if op == ">" and graph_id > val:
|
||||
return True
|
||||
elif op == ">=" and graph_id >= val:
|
||||
return True
|
||||
elif op == "<" and graph_id < val:
|
||||
return True
|
||||
elif op == "<=" and graph_id <= val:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def __repr__(self) -> str:
|
||||
# pyrefly: ignore [implicit-any]
|
||||
parts = []
|
||||
if self._explicit_ids:
|
||||
parts.append(f"ids={sorted(self._explicit_ids)}")
|
||||
if self._conditions:
|
||||
parts.append(f"conditions={self._conditions}")
|
||||
return f"GraphIdFilter({', '.join(parts) if parts else 'empty'})"
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class _GraphRouterBase(Generic[T]):
|
||||
"""
|
||||
Base class for routing graphs to different values based on their IDs.
|
||||
|
||||
The router parses a configuration string with rules in the format:
|
||||
"filter1:value1;filter2:value2;..."
|
||||
|
||||
Rules are evaluated in order, and the first matching rule wins.
|
||||
"""
|
||||
|
||||
def __init__(self, config_str: str, rule_type: str) -> None:
|
||||
self._rules: list[tuple[GraphIdFilter, T]] = []
|
||||
self._values: list[T | None] = []
|
||||
self._overflow_value: T | None = None
|
||||
self._rule_type = rule_type
|
||||
self._parse(config_str)
|
||||
self._precompute()
|
||||
|
||||
def _parse_value_str(self, value_str: str) -> T | None:
|
||||
"""Parse a value string into the appropriate type. Returns None to skip."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _parse(self, config_str: str) -> None:
|
||||
if not config_str or not config_str.strip():
|
||||
return
|
||||
|
||||
rule_strs = config_str.split(";")
|
||||
for rule_str in rule_strs:
|
||||
rule_str = rule_str.strip()
|
||||
if not rule_str:
|
||||
continue
|
||||
|
||||
colon_idx = rule_str.find(":")
|
||||
if colon_idx == -1:
|
||||
log.warning(
|
||||
"Invalid %s override rule (missing ':'): %s",
|
||||
self._rule_type,
|
||||
rule_str,
|
||||
)
|
||||
continue
|
||||
|
||||
filter_str = rule_str[:colon_idx].strip()
|
||||
value_str = rule_str[colon_idx + 1 :].strip()
|
||||
|
||||
if not filter_str or not value_str:
|
||||
log.warning("Invalid %s override rule: %s", self._rule_type, rule_str)
|
||||
continue
|
||||
|
||||
value = self._parse_value_str(value_str)
|
||||
if value is not None:
|
||||
self._rules.append((GraphIdFilter(filter_str), value))
|
||||
|
||||
def _precompute(self) -> None:
|
||||
if not self._rules:
|
||||
return
|
||||
|
||||
# Find max ID from explicit IDs and comparison thresholds
|
||||
max_id = 0
|
||||
for f, _ in self._rules:
|
||||
if f._explicit_ids:
|
||||
max_id = max(max_id, *f._explicit_ids)
|
||||
for _, val in f._conditions:
|
||||
max_id = max(max_id, val)
|
||||
|
||||
# Pre-compute values for IDs 0 to max_id
|
||||
for i in range(max_id + 1):
|
||||
self._values.append(self._match_rules(i))
|
||||
|
||||
# For IDs > max_id, the result is constant (only unbounded conditions apply)
|
||||
self._overflow_value = self._match_rules(max_id + 1)
|
||||
|
||||
def _match_rules(self, graph_id: int) -> T | None:
|
||||
for f, value in self._rules:
|
||||
if graph_id in f:
|
||||
return value
|
||||
return None
|
||||
|
||||
def get_value_for_graph(self, graph_id: int) -> T | None:
|
||||
"""Get the value for a given graph ID. Returns None if no rule matches."""
|
||||
if graph_id < len(self._values):
|
||||
return self._values[graph_id]
|
||||
return self._overflow_value
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
"""Check if no rules are configured."""
|
||||
return len(self._rules) == 0
|
||||
|
||||
|
||||
class GraphBackendRouter(_GraphRouterBase[Any]):
|
||||
"""
|
||||
Routes graphs to different backends based on their IDs.
|
||||
|
||||
The router parses a configuration string with rules in the format:
|
||||
"filter1:backend1;filter2:backend2;..."
|
||||
|
||||
If a graph ID matches multiple rules with different backends, a ValueError
|
||||
is raised.
|
||||
|
||||
Examples:
|
||||
"0-5:eager;>5:inductor" - IDs 0-5 use eager, rest use inductor
|
||||
">10:aot_eager" - IDs > 10 use aot_eager
|
||||
"<=3:eager;4-10:aot_eager" - IDs 0-3 use eager, 4-10 use aot_eager
|
||||
|
||||
Supported backends include "eager", "aot_eager", "aot_eager_decomp_partition",
|
||||
"inductor", and any other registered backend.
|
||||
"""
|
||||
|
||||
def __init__(self, config_str: str) -> None:
|
||||
self._backend_names: dict[int, str] = {}
|
||||
super().__init__(config_str, "backend")
|
||||
|
||||
def _parse_value_str(self, value_str: str) -> Any | None:
|
||||
"""Look up a backend by name."""
|
||||
from .backends.registry import lookup_backend
|
||||
from .eval_frame import cached_backends
|
||||
|
||||
backend = lookup_backend(value_str)
|
||||
|
||||
# Register the backend so its reset() is called during torch._dynamo.reset()
|
||||
assert backend is not None, "Invalid override backend: " + value_str
|
||||
cached_backends.setdefault(id(backend), backend)
|
||||
self._backend_names[id(backend)] = value_str
|
||||
return backend
|
||||
|
||||
def _match_rules(self, graph_id: int) -> Any | None:
|
||||
"""Match rules with conflict detection for overlapping filters."""
|
||||
matches = {id(backend): backend for f, backend in self._rules if graph_id in f}
|
||||
if len(matches) > 1:
|
||||
names = [self._backend_names[bid] for bid in matches]
|
||||
raise ValueError(
|
||||
f"Conflicting backend override for graph {graph_id}: matched {names}"
|
||||
)
|
||||
if matches:
|
||||
return next(iter(matches.values()))
|
||||
return None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
if not self._rules:
|
||||
return "GraphBackendRouter(empty)"
|
||||
return f"GraphBackendRouter({self._rules})"
|
||||
|
||||
|
||||
class GraphConfigRouter(_GraphRouterBase[dict[str, Any]]):
|
||||
"""
|
||||
Routes graphs to different inductor configs based on their IDs.
|
||||
|
||||
The router parses a configuration string with rules in the format:
|
||||
"filter1:config1;filter2:config2;..."
|
||||
|
||||
All matching rules are aggregated: configs from all matching rules are merged
|
||||
into a single dict. Conflicting keys (same key, different value) raise an error.
|
||||
Config format is "key=value" or "key1=value1,key2=value2" for multiple settings.
|
||||
|
||||
Examples:
|
||||
"0-5:triton.cudagraph_skip_dynamic_graphs=False"
|
||||
">10:triton.cudagraphs=False,triton.cudagraph_trees=False"
|
||||
|
||||
With "0:a=1;>=0:b=2", graph 0 gets {"a": 1, "b": 2} (both rules match).
|
||||
With "0:a=1;>=0:a=2", graph 0 raises an error (conflicting values for "a").
|
||||
With "0:a=1;>=0:a=1", graph 0 gets {"a": 1} (same value is not a conflict).
|
||||
"""
|
||||
|
||||
def __init__(self, config_str: str) -> None:
|
||||
super().__init__(config_str, "config")
|
||||
|
||||
@staticmethod
|
||||
def _parse_scalar_value(value_str: str) -> Any:
|
||||
"""Parse a string value into the appropriate Python type."""
|
||||
value_str = value_str.strip()
|
||||
if value_str.lower() == "true":
|
||||
return True
|
||||
if value_str.lower() == "false":
|
||||
return False
|
||||
if value_str.lower() == "none":
|
||||
return None
|
||||
try:
|
||||
if "." in value_str:
|
||||
return float(value_str)
|
||||
return int(value_str)
|
||||
except ValueError:
|
||||
return value_str
|
||||
|
||||
def _match_rules(self, graph_id: int) -> dict[str, Any] | None:
|
||||
"""Aggregate configs from all matching rules. Conflicts raise an error."""
|
||||
result: dict[str, Any] = {}
|
||||
for f, value in self._rules:
|
||||
if graph_id in f:
|
||||
for k, v in value.items():
|
||||
if k in result and result[k] != v:
|
||||
raise ValueError(
|
||||
f"Conflicting config override for graph {graph_id}: "
|
||||
f"key '{k}' has value {result[k]!r} and {v!r}"
|
||||
)
|
||||
result[k] = v
|
||||
return result if result else None
|
||||
|
||||
def _parse_value_str(self, value_str: str) -> dict[str, Any] | None:
|
||||
"""Parse a config string like 'key1=val1,key2=val2' into a dict."""
|
||||
result: dict[str, Any] = {}
|
||||
for item in value_str.split(","):
|
||||
item = item.strip()
|
||||
if not item:
|
||||
continue
|
||||
if "=" not in item:
|
||||
log.warning("Invalid config item (missing '='): %s", item)
|
||||
continue
|
||||
key, value = item.split("=", 1)
|
||||
result[key.strip()] = self._parse_scalar_value(value)
|
||||
return result if result else None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
if not self._rules:
|
||||
return "GraphConfigRouter(empty)"
|
||||
return f"GraphConfigRouter({self._rules})"
|
||||
|
||||
|
||||
def _get_override_for_compile_id(
|
||||
compile_id: CompileId | None,
|
||||
config_str: str,
|
||||
create_router: Callable[[str], _GraphRouterBase[T]],
|
||||
label: str,
|
||||
) -> T | None:
|
||||
"""
|
||||
Get the override value for a given CompileId.
|
||||
|
||||
Returns the value from the router, or None if no override applies.
|
||||
"""
|
||||
if compile_id is None or not config_str:
|
||||
return None
|
||||
|
||||
graph_id = compile_id.frame_id
|
||||
if graph_id is None:
|
||||
return None
|
||||
|
||||
router = create_router(config_str)
|
||||
value = router.get_value_for_graph(graph_id)
|
||||
if value is not None:
|
||||
log.info("Overriding %s: %s", label, value)
|
||||
return value
|
||||
|
||||
|
||||
@functools.lru_cache
|
||||
def _create_backend_router(config_str: str) -> GraphBackendRouter:
|
||||
"""Create and cache GraphBackendRouter instances based on config string."""
|
||||
return GraphBackendRouter(config_str)
|
||||
|
||||
|
||||
@functools.lru_cache
|
||||
def _validate_backend_names(config_str: str) -> str | None:
|
||||
"""Return an error message if any backend name is invalid, else None."""
|
||||
if not config_str or not config_str.strip():
|
||||
return None
|
||||
from .backends.registry import lookup_backend
|
||||
|
||||
for rule_str in config_str.split(";"):
|
||||
rule_str = rule_str.strip()
|
||||
if not rule_str or ":" not in rule_str:
|
||||
continue
|
||||
backend_name = rule_str[rule_str.find(":") + 1 :].strip()
|
||||
if not backend_name:
|
||||
continue
|
||||
try:
|
||||
lookup_backend(backend_name)
|
||||
except Exception:
|
||||
return (
|
||||
f"TORCH_COMPILE_OVERRIDE_BACKENDS: "
|
||||
f"'{backend_name}' is not a valid backend, "
|
||||
f"see `torch._dynamo.list_backends()` for available backends"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@functools.lru_cache
|
||||
def _validate_inductor_config_keys(config_str: str) -> str | None:
|
||||
"""Return an error message if any config key is invalid, else None."""
|
||||
router = GraphConfigRouter(config_str)
|
||||
from torch._inductor import config
|
||||
|
||||
for _, config_dict in router._rules:
|
||||
for key in config_dict:
|
||||
if not hasattr(config, key):
|
||||
return (
|
||||
f"TORCH_COMPILE_OVERRIDE_INDUCTOR_CONFIGS: "
|
||||
f"'{key}' is not a valid torch._inductor.config option"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@functools.lru_cache
|
||||
def _validate_dynamo_config_keys(config_str: str) -> str | None:
|
||||
"""Return an error message if any config key is invalid, else None."""
|
||||
router = GraphConfigRouter(config_str)
|
||||
from torch._dynamo import config
|
||||
|
||||
for _, config_dict in router._rules:
|
||||
for key in config_dict:
|
||||
if not hasattr(config, key):
|
||||
return (
|
||||
f"TORCH_COMPILE_OVERRIDE_DYNAMO_CONFIGS: "
|
||||
f"'{key}' is not a valid torch._dynamo.config option"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@functools.lru_cache
|
||||
def _create_inductor_config_router(config_str: str) -> GraphConfigRouter:
|
||||
"""Create and cache GraphConfigRouter for inductor config overrides."""
|
||||
return GraphConfigRouter(config_str)
|
||||
|
||||
|
||||
@functools.lru_cache
|
||||
def _create_dynamo_config_router(config_str: str) -> GraphConfigRouter:
|
||||
"""Create and cache GraphConfigRouter for dynamo config overrides.
|
||||
|
||||
Warns that dynamo config overrides are keyed by frame ID and some configs
|
||||
can affect graph breaks, which may shift frame IDs.
|
||||
"""
|
||||
router = GraphConfigRouter(config_str)
|
||||
if not router.is_empty():
|
||||
warnings.warn(
|
||||
"TORCH_COMPILE_OVERRIDE_DYNAMO_CONFIGS is set. Dynamo config overrides are "
|
||||
"keyed by frame ID. Some dynamo configs can affect graph breaks, "
|
||||
"which may alter the number of frames and shift frame IDs, causing "
|
||||
"overrides to target the wrong graphs.",
|
||||
)
|
||||
return router
|
||||
|
||||
|
||||
def get_backend_override_for_compile_id(
|
||||
compile_id: CompileId | None,
|
||||
config_str: str,
|
||||
) -> Any:
|
||||
"""
|
||||
Get the backend override for a given CompileId.
|
||||
|
||||
Returns the backend function to use, or None if no override applies.
|
||||
"""
|
||||
return _get_override_for_compile_id(
|
||||
compile_id,
|
||||
config_str,
|
||||
_create_backend_router,
|
||||
"torch.compile backend",
|
||||
)
|
||||
|
||||
|
||||
def get_inductor_config_override_for_compile_id(
|
||||
compile_id: CompileId | None,
|
||||
config_str: str,
|
||||
) -> dict[str, Any] | None:
|
||||
"""
|
||||
Get the inductor config override for a given CompileId.
|
||||
|
||||
Returns a dict of config patches to apply, or None if no override applies.
|
||||
"""
|
||||
return _get_override_for_compile_id(
|
||||
compile_id,
|
||||
config_str,
|
||||
_create_inductor_config_router, # type: ignore[arg-type]
|
||||
"inductor config",
|
||||
)
|
||||
|
||||
|
||||
def get_dynamo_config_override_for_compile_id(
|
||||
compile_id: CompileId | None,
|
||||
config_str: str,
|
||||
) -> dict[str, Any] | None:
|
||||
"""
|
||||
Get the dynamo config override for a given CompileId.
|
||||
|
||||
Returns a dict of config patches to apply, or None if no override applies.
|
||||
"""
|
||||
return _get_override_for_compile_id(
|
||||
compile_id,
|
||||
config_str,
|
||||
_create_dynamo_config_router, # type: ignore[arg-type]
|
||||
"dynamo config",
|
||||
)
|
||||
|
||||
|
||||
# Keep old name for backwards compatibility
|
||||
_create_router = _create_backend_router
|
||||
@@ -0,0 +1,503 @@
|
||||
"""
|
||||
This module provides functionality for tracking and managing regions in computational graphs.
|
||||
It supports graph optimization by identifying and grouping similar regions based on their
|
||||
structure and behavior. The module implements algorithms for:
|
||||
|
||||
1. Tracking nodes and their relationships in the computational graph
|
||||
2. Identifying identical or similar regions across the graph
|
||||
3. Managing graph regions for optimization purposes
|
||||
4. Supporting deduplication and other graph transformation passes
|
||||
|
||||
The core functionality revolves around the GraphRegionTracker class which maintains
|
||||
mappings between nodes and their duplicates, enabling efficient graph analysis and
|
||||
optimization operations.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copyreg
|
||||
import io
|
||||
import logging
|
||||
import math
|
||||
import operator
|
||||
import pickle
|
||||
from collections import defaultdict, deque
|
||||
from dataclasses import fields
|
||||
from typing import Any, TYPE_CHECKING, TypeVar
|
||||
|
||||
import torch._logging
|
||||
import torch.fx
|
||||
from torch._subclasses.fake_tensor import FakeTensor
|
||||
from torch.utils._ordered_set import OrderedSet
|
||||
from torch.utils._pytree import tree_flatten
|
||||
|
||||
from .graph_utils import _get_flat_args_unique
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from .symbolic_convert import InstructionTranslatorBase
|
||||
|
||||
|
||||
Node = torch.fx.Node
|
||||
Region = list[Node]
|
||||
IdenticalNodes = list[Node]
|
||||
GlobalStateKey = tuple[
|
||||
bool,
|
||||
bool,
|
||||
int,
|
||||
tuple[bool, bool],
|
||||
tuple[bool, bool],
|
||||
torch.dtype,
|
||||
bool,
|
||||
bool,
|
||||
bool,
|
||||
bool,
|
||||
]
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
graph_expansion_log = torch._logging.getArtifactLogger(
|
||||
__name__, "graph_region_expansion"
|
||||
)
|
||||
|
||||
|
||||
def debug_log(msg: str, *args) -> None: # type: ignore[no-untyped-def]
|
||||
graph_expansion_log.debug(msg, *args)
|
||||
|
||||
|
||||
def _extract_tensor_metadata_for_node_hash(
|
||||
x: torch.Tensor,
|
||||
) -> tuple[Callable[[T], T], tuple[Any, ...]]:
|
||||
from torch._inductor.codecache import _ident, extract_tensor_metadata_for_cache_key
|
||||
|
||||
out = []
|
||||
metadata = extract_tensor_metadata_for_cache_key(x)
|
||||
for field in fields(metadata):
|
||||
out.append(getattr(metadata, field.name))
|
||||
|
||||
return (_ident, tuple(out))
|
||||
|
||||
|
||||
class NodeHashException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InputPickler(pickle.Pickler):
|
||||
def __init__(self) -> None:
|
||||
from torch._inductor.codecache import _ident
|
||||
|
||||
stream = io.BytesIO()
|
||||
self._stream = stream
|
||||
super().__init__(stream)
|
||||
self.dispatch_table = copyreg.dispatch_table.copy()
|
||||
self.dispatch_table.update(
|
||||
{
|
||||
FakeTensor: _extract_tensor_metadata_for_node_hash,
|
||||
torch.SymInt: lambda x: (_ident, (str(x),)),
|
||||
torch.SymBool: lambda x: (_ident, (str(x),)),
|
||||
torch.SymFloat: lambda x: (_ident, (str(x),)),
|
||||
}
|
||||
)
|
||||
self.fast = True
|
||||
|
||||
def dumps(self, obj: Any) -> bytes:
|
||||
"""
|
||||
Pickle an object and return a byte string.
|
||||
"""
|
||||
try:
|
||||
self.dump(obj)
|
||||
return self._stream.getvalue()
|
||||
except (TypeError, AttributeError) as e:
|
||||
raise NodeHashException from e
|
||||
finally:
|
||||
self._stream.seek(0)
|
||||
self._stream.truncate(0)
|
||||
|
||||
|
||||
def _extract_args(arg: Any) -> Any:
|
||||
if isinstance(arg, Node):
|
||||
return arg.meta.get("example_value")
|
||||
elif isinstance(arg, (torch.Tensor, int)):
|
||||
return arg
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_args(
|
||||
node: Node,
|
||||
) -> tuple[tuple[str, ...], tuple[Any | None, ...]]:
|
||||
flat_args, _ = tree_flatten(node.args)
|
||||
sorted_kwargs = sorted(node.kwargs.items(), key=operator.itemgetter(0))
|
||||
sorted_keys = tuple(sorted(node.kwargs.keys()))
|
||||
flat_kwargs, _ = tree_flatten(sorted_kwargs)
|
||||
all_args = flat_args + flat_kwargs
|
||||
return (sorted_keys, tuple(_extract_args(arg) for arg in all_args))
|
||||
|
||||
|
||||
def _sort_with_ref_region(
|
||||
index_to_rank: dict[int, int], regions: list[list[Any]]
|
||||
) -> None:
|
||||
# sort topologically
|
||||
# we need to handle edge cases where some nodes have no dependencies
|
||||
# so first we map each node to its ranking
|
||||
ref_region = regions[0]
|
||||
sorted_indices = sorted(range(len(ref_region)), key=lambda i: index_to_rank[i])
|
||||
for region in regions:
|
||||
region[:] = [region[i] for i in sorted_indices]
|
||||
|
||||
|
||||
def get_global_state_key() -> GlobalStateKey:
|
||||
return (
|
||||
torch.is_grad_enabled(),
|
||||
torch.is_inference_mode_enabled(),
|
||||
torch.get_num_threads(),
|
||||
torch._C._get_cublas_allow_fp16_reduced_precision_reduction(),
|
||||
torch._C._get_cublas_allow_bf16_reduced_precision_reduction(),
|
||||
torch.get_default_dtype(),
|
||||
torch.are_deterministic_algorithms_enabled(),
|
||||
torch._C._get_cublas_allow_tf32(),
|
||||
torch.is_deterministic_algorithms_warn_only_enabled(),
|
||||
torch._C._autograd._saved_tensors_hooks_is_enabled(), # type: ignore[attr-defined]
|
||||
)
|
||||
|
||||
|
||||
# This is typical BFS with the caveat
|
||||
# that a node's children need to be explicitly
|
||||
# added with the add_children() method
|
||||
# The flow is yield a node and check if it's valid for all regions
|
||||
# if not valid, discard and continue onto the next node
|
||||
# Note: this iterates backward through the graph by looking at args/kwargs
|
||||
# of a node
|
||||
class BackwardBfsArgIter:
|
||||
def __init__(self, origin: Node) -> None:
|
||||
self._cur: Node | None = origin
|
||||
self._queue: deque[Node | None] = deque()
|
||||
|
||||
@staticmethod
|
||||
def create(origin: Node) -> BackwardBfsArgIter:
|
||||
it = BackwardBfsArgIter(origin)
|
||||
it.add_children(origin)
|
||||
# pop the origin node, since it is the origin of
|
||||
# the region and does not need to be considered for addition
|
||||
assert it.next()
|
||||
return it
|
||||
|
||||
def next(self) -> Node | None:
|
||||
ret = self._cur
|
||||
if not self._queue:
|
||||
self._cur = None
|
||||
else:
|
||||
self._cur = self._queue.popleft()
|
||||
return ret
|
||||
|
||||
def peek(self) -> Node | None:
|
||||
return self._cur
|
||||
|
||||
def add_children(self, node: Node) -> None:
|
||||
flat_args = _get_flat_args_unique(node, {})
|
||||
for arg in flat_args:
|
||||
if isinstance(arg, Node):
|
||||
self._append(arg)
|
||||
|
||||
def _append(self, arg: Node) -> None:
|
||||
if self._cur is None:
|
||||
self._cur = arg
|
||||
else:
|
||||
self._queue.append(arg)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"BackwardBfsArgIter(cur={self._cur}, queue={self._queue})"
|
||||
|
||||
|
||||
class GraphRegionTracker:
|
||||
"""
|
||||
GraphRegionTracker tracks each node added to the output graph and generates a key based on the source location,
|
||||
instruction pointer, input shapes, and global state at the time the node is inserted into the graph. Nodes with
|
||||
the same key are grouped together in a list of identical nodes (the value of node_to_duplicates).
|
||||
|
||||
hash_to_duplicates: Dict[str, IdenticalNodes] - A dictionary mapping the key to a list of identical nodes
|
||||
node_to_duplicates: Dict[Node, IdenticalNodes] - A dictionary mapping a node to the list of identical nodes it belongs to
|
||||
input_pickler: InputPickler - An instance of InputPickler used to generate a node hash
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.hash_to_duplicates: dict[str, IdenticalNodes] = defaultdict(list)
|
||||
self.node_to_duplicates: dict[Node, IdenticalNodes] = {}
|
||||
# Note: position is in flattened args/kwargs list
|
||||
self.node_to_mutated_arg_positions: dict[Node, OrderedSet[int]] = {}
|
||||
self.input_pickler = InputPickler()
|
||||
|
||||
def _hash_node(
|
||||
self, filename: str, lineno: int, instruction_pointer: int | None, node: Node
|
||||
) -> str:
|
||||
from torch._inductor.codecache import sha256_hash
|
||||
|
||||
key = (
|
||||
get_global_state_key(),
|
||||
filename,
|
||||
lineno,
|
||||
instruction_pointer,
|
||||
_normalize_args(node),
|
||||
)
|
||||
return sha256_hash(self.input_pickler.dumps(key))
|
||||
|
||||
def _is_identical(self, n0: Node, n1: Node) -> bool:
|
||||
return (
|
||||
n0 in self.node_to_duplicates
|
||||
and n1 in self.node_to_duplicates
|
||||
and self.node_to_duplicates[n0] is self.node_to_duplicates[n1]
|
||||
and n0 is not n1
|
||||
)
|
||||
|
||||
def track_node(self, tx: InstructionTranslatorBase, node: Node) -> None:
|
||||
"""
|
||||
The main entry point for tracking a node. This function will hash the node argument and group
|
||||
nodes with the same hash together. It updates the hash_to_duplicates and node_to_duplicates dictionaries
|
||||
to track the new node.
|
||||
"""
|
||||
try:
|
||||
if (
|
||||
node not in self.node_to_duplicates
|
||||
): # don't allow nodes to be added twice
|
||||
duplicates = self.hash_to_duplicates[
|
||||
self._hash_node(
|
||||
tx.f_code.co_filename, tx.lineno, tx.instruction_pointer, node
|
||||
)
|
||||
]
|
||||
duplicates.append(node)
|
||||
self.node_to_duplicates[node] = duplicates
|
||||
except NodeHashException as e:
|
||||
log.debug("Unable to hash node %s with exception %s", node, e) # noqa: G200
|
||||
|
||||
def track_node_mutations(
|
||||
self,
|
||||
node: Node,
|
||||
flat_args_kwargs: list[Any],
|
||||
id_to_initial_version: dict[int, int],
|
||||
) -> None:
|
||||
"""
|
||||
This function tracks which argument positions are mutated by the given node. Subgraph HOP does not support
|
||||
input mutations today so we will skip regions which have inputs that are mutated.
|
||||
"""
|
||||
mutated_arg_positions = OrderedSet[int]()
|
||||
for i, arg in enumerate(flat_args_kwargs):
|
||||
val_id = id(arg)
|
||||
if (
|
||||
val_id in id_to_initial_version
|
||||
and id_to_initial_version[val_id] != arg._version
|
||||
):
|
||||
mutated_arg_positions.add(i)
|
||||
|
||||
if mutated_arg_positions:
|
||||
self.node_to_mutated_arg_positions[node] = mutated_arg_positions
|
||||
|
||||
def add_node_mutation(
|
||||
self,
|
||||
node: Node,
|
||||
arg_pos: int,
|
||||
) -> None:
|
||||
if node in self.node_to_mutated_arg_positions:
|
||||
self.node_to_mutated_arg_positions[node].add(arg_pos)
|
||||
else:
|
||||
self.node_to_mutated_arg_positions[node] = OrderedSet([arg_pos])
|
||||
|
||||
def get_identical_regions(self, graph: torch.fx.Graph) -> list[list[Region]]:
|
||||
"""
|
||||
This function is responsible for extracting the largest regions of identical nodes from the given graph.
|
||||
**Note**: This function assumes the nodes that have been tracked with track_node are in the provided graph argument.
|
||||
|
||||
The algorithm proceeds as follows:
|
||||
The nodes tracked via track_node above are organized into region groups. The initial region groups look like this:
|
||||
[[IdenticalNode1], [IdenticalNode2], [IdenticalNode3]] and each sublist is called a region. For each region group
|
||||
(starting at the topologically latest region group), the inner regions are gradually expanded one node at time from
|
||||
the flattened args and kwargs of the node in each region provided that for all regions in the group, the nodes being
|
||||
added are also identical (ie have the same key computed by track_node). This is checked by verifying that the two
|
||||
nodes have the same identical node list in node_to_duplicates.
|
||||
"""
|
||||
topological_ranking = {node: i for i, node in enumerate(graph.nodes)}
|
||||
region_groups_with_rank = []
|
||||
# needed to detect if replacing a region will create cycles
|
||||
node_to_recursive_ancestors = _populate_recursive_ancestor_map(graph)
|
||||
|
||||
# Create region groups; a region group is a group
|
||||
# of regions that are all identical. In this initial state
|
||||
# each region in the group is a single node, and we discard
|
||||
# groups that are only a single region.
|
||||
# We track the topological ranking to start with groups later in the graph
|
||||
# the reason for this is that we will necessarily create the largest groups first.
|
||||
for group in self.hash_to_duplicates.values():
|
||||
if len(group) > 1:
|
||||
# pyrefly: ignore [implicit-any]
|
||||
region_group = []
|
||||
min_rank = math.inf
|
||||
|
||||
for node in group:
|
||||
# some nodes aren't in the topo ranking?
|
||||
if node in topological_ranking:
|
||||
min_rank = min(min_rank, topological_ranking[node])
|
||||
region_group.append([node])
|
||||
|
||||
if len(region_group) > 1:
|
||||
region_groups_with_rank.append((region_group, min_rank))
|
||||
|
||||
region_groups_with_rank.sort(key=lambda rg: -rg[1])
|
||||
region_groups = [rg for rg, _ in region_groups_with_rank]
|
||||
|
||||
# We start from regions later in the graph and expand them earlier
|
||||
# as a result, we will create the largest regions first and they won't
|
||||
# overlap.
|
||||
seen_nodes: set[Node] = set()
|
||||
for region_group in region_groups:
|
||||
fully_expand_region_group(
|
||||
region_group,
|
||||
seen_nodes,
|
||||
node_to_recursive_ancestors,
|
||||
self._is_identical,
|
||||
)
|
||||
# sort topologically
|
||||
# we need to handle edge cases where some nodes have no dependencies
|
||||
# so first we map each node to its ranking,
|
||||
ref_region = region_group[0]
|
||||
index_to_rank = {
|
||||
index: topological_ranking[n] for index, n in enumerate(ref_region)
|
||||
}
|
||||
_sort_with_ref_region(index_to_rank, region_group)
|
||||
|
||||
return [
|
||||
region_group for region_group in region_groups if len(region_group[0]) > 1
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"GraphRegionTracker(hash_to_duplicates={self.hash_to_duplicates}, node_to_duplicates={self.node_to_duplicates})"
|
||||
|
||||
|
||||
class RegionWrapper:
|
||||
"""Holds state for regions e.g. ancestors and new candidate nodes for consideration"""
|
||||
|
||||
def __init__(
|
||||
self, region: Region, node_to_recursive_ancestors: dict[Node, set[Node]]
|
||||
) -> None:
|
||||
assert len(region) == 1, "all regions should start with one node"
|
||||
node = region[0]
|
||||
self.node_to_recursive_ancestors = node_to_recursive_ancestors
|
||||
self.iter = BackwardBfsArgIter.create(node)
|
||||
self.nodes_unique = OrderedSet([node])
|
||||
self.ancestors = set(node_to_recursive_ancestors[node])
|
||||
self.region = region
|
||||
|
||||
def next_candidate(self) -> Node | None:
|
||||
return self.iter.next()
|
||||
|
||||
def will_inclusion_create_cycle(self, node: Node) -> bool:
|
||||
external_users = [user for user in node.users if user not in self.nodes_unique]
|
||||
for user in external_users:
|
||||
if user in self.ancestors:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def add(self, node: Node) -> None:
|
||||
self.nodes_unique.add(node)
|
||||
self.region.append(node)
|
||||
self.iter.add_children(node)
|
||||
self.ancestors.update(self.node_to_recursive_ancestors[node])
|
||||
|
||||
|
||||
def fully_expand_region_group(
|
||||
regions: list[Region],
|
||||
seen_nodes: set[Node],
|
||||
node_to_recursive_ancestors: dict[Node, set[Node]],
|
||||
is_identical_fn: Callable[[Node, Node], bool],
|
||||
) -> None:
|
||||
debug_log("--------------------------------------------------")
|
||||
debug_log("expanding new region group: %s", regions)
|
||||
|
||||
# All regions should start with 1 node
|
||||
assert all(len(region) == 1 for region in regions)
|
||||
region_wrappers = [
|
||||
RegionWrapper(region, node_to_recursive_ancestors) for region in regions
|
||||
]
|
||||
|
||||
nodes_to_add = OrderedSet[Node]()
|
||||
current_node = region_wrappers[0].next_candidate()
|
||||
|
||||
# No children
|
||||
if current_node is None:
|
||||
return
|
||||
|
||||
# Loop incrementally adding new nodes to each region
|
||||
# regions are only expanded if the node to add is valid
|
||||
# for ALL regions
|
||||
while current_node:
|
||||
add_to_all_regions = not region_wrappers[0].will_inclusion_create_cycle(
|
||||
current_node
|
||||
)
|
||||
nodes_to_add.clear()
|
||||
nodes_to_add.add(current_node)
|
||||
for region_wrapper in region_wrappers[1:]:
|
||||
candidate = region_wrapper.next_candidate()
|
||||
|
||||
debug_log("--------------------")
|
||||
debug_log(
|
||||
"considering candidate: %s, cur_node: %s", candidate, current_node
|
||||
)
|
||||
|
||||
if not candidate or not add_to_all_regions:
|
||||
add_to_all_regions = False
|
||||
continue
|
||||
|
||||
debug_log(
|
||||
"candidate in previously claimed nodes?: %s", candidate in seen_nodes
|
||||
)
|
||||
debug_log("is_identical: %s", is_identical_fn(candidate, current_node))
|
||||
|
||||
add_to_all_regions &= (
|
||||
candidate not in seen_nodes
|
||||
and candidate not in nodes_to_add
|
||||
and candidate.op != "placeholder"
|
||||
and candidate.op != "get_attr"
|
||||
and is_identical_fn(candidate, current_node)
|
||||
and not region_wrapper.will_inclusion_create_cycle(candidate)
|
||||
)
|
||||
nodes_to_add.add(candidate)
|
||||
|
||||
debug_log(f"add_to_all_regions: {add_to_all_regions}")
|
||||
debug_log("--------------------")
|
||||
|
||||
if add_to_all_regions:
|
||||
assert len(region_wrappers) == len(nodes_to_add), (
|
||||
"Number of nodes to add must equal the number of regions"
|
||||
)
|
||||
for region_wrapper, node in zip(region_wrappers, nodes_to_add):
|
||||
region_wrapper.add(node)
|
||||
debug_log("adding %s's children", node)
|
||||
debug_log("%s %s", node.args, list(node.kwargs.items()))
|
||||
seen_nodes.add(node)
|
||||
|
||||
current_node = region_wrappers[0].next_candidate()
|
||||
|
||||
# Ensure regions are sorted in topological order
|
||||
for region in regions:
|
||||
region.reverse()
|
||||
|
||||
debug_log("end expand new region group: %s", regions)
|
||||
debug_log("--------------------------------------------------")
|
||||
|
||||
|
||||
def _populate_recursive_ancestor_map(graph: torch.fx.Graph) -> dict[Node, set[Node]]:
|
||||
node_to_recursive_ancestors: dict[Node, set[Node]] = {}
|
||||
for node in graph.nodes:
|
||||
node_to_recursive_ancestors[node] = set()
|
||||
for node in graph.nodes:
|
||||
all_args = _get_flat_args_unique(node, {})
|
||||
for arg in all_args:
|
||||
if isinstance(arg, Node):
|
||||
node_to_recursive_ancestors[node].update(
|
||||
node_to_recursive_ancestors[arg]
|
||||
)
|
||||
node_to_recursive_ancestors[node].add(arg)
|
||||
return node_to_recursive_ancestors
|
||||
@@ -0,0 +1,117 @@
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from torch.fx import Graph, map_arg, Node
|
||||
from torch.utils._ordered_set import OrderedSet
|
||||
from torch.utils._pytree import tree_flatten
|
||||
|
||||
|
||||
# flattens with support for slices
|
||||
# Note: a better way to do this would
|
||||
# be register/unregister slices as pytree nodes
|
||||
# but there is no unregister API in the pytorch
|
||||
# pytree impl
|
||||
def _get_flat_args(
|
||||
node: Node, node_to_additional_deps: dict[Node, OrderedSet[Node]]
|
||||
) -> list[Node]:
|
||||
args = list[Any]()
|
||||
map_arg((node.args, node.kwargs), args.append)
|
||||
if node in node_to_additional_deps:
|
||||
args.extend(node_to_additional_deps[node])
|
||||
return args
|
||||
|
||||
|
||||
def _get_flat_args_unique(
|
||||
node: Node, node_to_additional_deps: dict[Node, OrderedSet[Node]]
|
||||
) -> OrderedSet[Node]:
|
||||
args = OrderedSet[Node]()
|
||||
map_arg((node.args, node.kwargs), args.add)
|
||||
if node in node_to_additional_deps:
|
||||
args.update(node_to_additional_deps[node])
|
||||
return args
|
||||
|
||||
|
||||
def _detect_cycles(
|
||||
graph: Graph, node_to_additional_deps: dict[Node, OrderedSet[Node]]
|
||||
) -> str:
|
||||
# States: 0=Unvisited, 1=Visiting, 2=Visited(Safe)
|
||||
state: dict[Node, int] = {}
|
||||
|
||||
for root in reversed(graph.nodes):
|
||||
if root in state:
|
||||
continue
|
||||
|
||||
# Stack holds (current_node, children_iterator).
|
||||
# Using an iterator allows us to pause and resume processing a node's children.
|
||||
stack = [(root, iter(_get_flat_args_unique(root, node_to_additional_deps)))]
|
||||
state[root] = 1 # Visiting
|
||||
|
||||
while stack:
|
||||
parent, children = stack[-1]
|
||||
|
||||
try:
|
||||
child = next(children)
|
||||
|
||||
if not isinstance(child, Node):
|
||||
continue
|
||||
|
||||
child_state = state.get(child, 0)
|
||||
|
||||
if child_state == 1:
|
||||
# Back-edge: child is on the current DFS path -> cycle
|
||||
cycle_path = [node for node, _ in stack] + [child]
|
||||
return f"cycle detected in path: {cycle_path}"
|
||||
|
||||
if child_state == 0:
|
||||
state[child] = 1
|
||||
stack.append(
|
||||
(
|
||||
child,
|
||||
iter(_get_flat_args_unique(child, node_to_additional_deps)),
|
||||
)
|
||||
)
|
||||
# child_state == 2 means already verified safe; skip.
|
||||
|
||||
except StopIteration:
|
||||
# All children processed — mark safe and pop.
|
||||
stack.pop()
|
||||
state[parent] = 2
|
||||
|
||||
return "no cycle detected"
|
||||
|
||||
|
||||
def _graph_device_type(graph: Graph | None) -> str:
|
||||
if graph is None:
|
||||
return "cpu"
|
||||
|
||||
def _device_type(x: Any) -> str:
|
||||
if isinstance(x, torch.device):
|
||||
return x.type
|
||||
if isinstance(x, torch.Tensor):
|
||||
return x.device.type
|
||||
return "cpu"
|
||||
|
||||
def _flatten_meta(node: Node, key: str) -> list[Any]:
|
||||
if key not in node.meta:
|
||||
return []
|
||||
flat, _ = tree_flatten(node.meta[key])
|
||||
return flat
|
||||
|
||||
for node in graph.nodes:
|
||||
for key in ("val", "example_value"):
|
||||
for obj in _flatten_meta(node, key):
|
||||
return _device_type(obj)
|
||||
|
||||
# Check for device conversions
|
||||
if node.op == "call_method":
|
||||
for gpu in ["cuda", "xpu"]:
|
||||
if node.target == gpu:
|
||||
return gpu
|
||||
if node.target == "to" and gpu in node.args:
|
||||
return gpu
|
||||
|
||||
# Check args/kwargs for non-CPU device specs
|
||||
flat_args, _ = tree_flatten((node.args, node.kwargs))
|
||||
for obj in flat_args:
|
||||
return _device_type(obj)
|
||||
return "cpu"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
"""Hook system for Dynamo's guard functionality.
|
||||
|
||||
This module provides a way to register callback functions that are triggered during
|
||||
guard-related operations.
|
||||
|
||||
The Hooks class manages two types of hook functions:
|
||||
- guard_export_fn: Called when guards need to be exported, taking a GuardsSet as input
|
||||
- guard_fail_fn: Called when a guard check fails, taking a GuardFail object as input
|
||||
These hooks enable customization of guard export and failure handling behaviors.
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
from collections.abc import Callable, Sequence
|
||||
|
||||
from torch._guards import GuardsSet
|
||||
|
||||
from .types import GuardFail, GuardFilterEntry
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Hooks:
|
||||
guard_export_fn: Callable[[GuardsSet], None] | None = None
|
||||
guard_fail_fn: Callable[[GuardFail], None] | None = None
|
||||
guard_filter_fn: Callable[[Sequence[GuardFilterEntry]], Sequence[bool]] | None = (
|
||||
None
|
||||
)
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Logging utilities for Dynamo and Inductor.
|
||||
|
||||
This module provides specialized logging functionality including:
|
||||
- Step-based logging that prepends step numbers to log messages
|
||||
- Progress bar management for compilation phases
|
||||
- Centralized logger management for Dynamo and Inductor components
|
||||
|
||||
The logging system helps track the progress of compilation phases and provides structured
|
||||
logging output for debugging and monitoring.
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from torch.hub import _Faketqdm, tqdm
|
||||
|
||||
|
||||
# Disable progress bar by default, not in dynamo config because otherwise get a circular import
|
||||
disable_progress = True
|
||||
|
||||
|
||||
# Return all loggers that torchdynamo/torchinductor is responsible for
|
||||
def get_loggers() -> list[logging.Logger]:
|
||||
return [
|
||||
logging.getLogger("torch.fx.experimental.symbolic_shapes"),
|
||||
logging.getLogger("torch._dynamo"),
|
||||
logging.getLogger("torch._inductor"),
|
||||
]
|
||||
|
||||
|
||||
# Creates a logging function that logs a message with a step # prepended.
|
||||
# get_step_logger should be lazily called (i.e. at runtime, not at module-load time)
|
||||
# so that step numbers are initialized properly. e.g.:
|
||||
|
||||
# @functools.cache
|
||||
# def _step_logger():
|
||||
# return get_step_logger(logging.getLogger(...))
|
||||
|
||||
# def fn():
|
||||
# _step_logger()(logging.INFO, "msg")
|
||||
|
||||
_step_counter = itertools.count(1)
|
||||
|
||||
# Update num_steps if more phases are added: Dynamo, AOT, Backend
|
||||
# This is very inductor centric
|
||||
# _inductor.utils.has_triton() gives a circular import error here
|
||||
|
||||
if not disable_progress:
|
||||
try:
|
||||
import triton # noqa: F401
|
||||
|
||||
num_steps = 3
|
||||
except ImportError:
|
||||
num_steps = 2
|
||||
pbar = tqdm(total=num_steps, desc="torch.compile()", delay=0)
|
||||
|
||||
|
||||
def get_step_logger(logger: logging.Logger) -> Callable[..., None]:
|
||||
if not disable_progress:
|
||||
pbar.update(1)
|
||||
if not isinstance(pbar, _Faketqdm):
|
||||
pbar.set_postfix_str(f"{logger.name}")
|
||||
|
||||
step = next(_step_counter)
|
||||
|
||||
def log(level: int, msg: str, **kwargs: Any) -> None:
|
||||
if "stacklevel" not in kwargs:
|
||||
kwargs["stacklevel"] = 2
|
||||
logger.log(level, "Step %s: %s", step, msg, **kwargs)
|
||||
|
||||
return log
|
||||
@@ -0,0 +1,251 @@
|
||||
"""Metrics collection and management system for Dynamo.
|
||||
|
||||
This module provides context managers for gathering and reporting metrics during
|
||||
compilation and runtime.
|
||||
|
||||
It includes two main components:
|
||||
- MetricsContext: A context manager for collecting metrics during compilation, supporting
|
||||
nested contexts and various metric types (counters, sets, key-value pairs)
|
||||
- RuntimeMetricsContext: A specialized context for runtime metrics collection that doesn't
|
||||
require explicit context management
|
||||
|
||||
The metrics system enables comprehensive monitoring and analysis of both compilation and
|
||||
execution performance.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import heapq
|
||||
import logging
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from typing import Any, TYPE_CHECKING, TypeAlias
|
||||
from typing_extensions import Self
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
|
||||
from torch.utils._traceback import CapturedTraceback
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TopN:
|
||||
"""
|
||||
Helper to record a list of metrics, keeping only the top N "most expensive" elements.
|
||||
"""
|
||||
|
||||
def __init__(self, at_most: int = 25) -> None:
|
||||
self.at_most = at_most
|
||||
self.heap: list[tuple[int, Any]] = []
|
||||
|
||||
def add(self, key: Any, val: int) -> None:
|
||||
# Push if we haven't reached the max size, else push and pop the smallest
|
||||
fn = heapq.heappush if len(self.heap) < self.at_most else heapq.heappushpop
|
||||
fn(self.heap, (val, key))
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.heap)
|
||||
|
||||
def __iter__(self) -> Iterator[tuple[Any, int]]:
|
||||
return ((key, val) for val, key in sorted(self.heap, reverse=True))
|
||||
|
||||
|
||||
OnExitType: TypeAlias = Callable[
|
||||
[int, int, dict[str, Any], type[BaseException] | None, BaseException | None],
|
||||
None,
|
||||
]
|
||||
|
||||
|
||||
class MetricsContext:
|
||||
def __init__(self, on_exit: OnExitType) -> None:
|
||||
"""
|
||||
Use this class as a contextmanager to create a context under which to accumulate
|
||||
a set of metrics, e.g., metrics gathered during a compilation. On exit of the
|
||||
contextmanager, call the provided 'on_exit' function and pass a dictionary of
|
||||
all metrics set during the lifetime of the contextmanager.
|
||||
"""
|
||||
self._on_exit = on_exit
|
||||
self._metrics: dict[str, Any] = {}
|
||||
self._start_time_ns: int = 0
|
||||
self._level: int = 0
|
||||
self._edits: list[tuple[CapturedTraceback, set[str]]] = []
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
"""
|
||||
Initialize metrics recording.
|
||||
"""
|
||||
if self._level == 0:
|
||||
# In case of recursion, track at the outermost context.
|
||||
self._metrics = {}
|
||||
self._start_time_ns = time.time_ns()
|
||||
|
||||
self._level += 1
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
_traceback: Any,
|
||||
) -> None:
|
||||
"""
|
||||
At exit, call the provided on_exit function.
|
||||
"""
|
||||
self._level -= 1
|
||||
assert self._level >= 0
|
||||
if self._level == 0:
|
||||
try:
|
||||
end_time_ns = time.time_ns()
|
||||
self._on_exit(
|
||||
self._start_time_ns, end_time_ns, self._metrics, exc_type, exc_value
|
||||
)
|
||||
except Exception:
|
||||
log.exception("Unexpected exception logging compilation metrics")
|
||||
|
||||
def in_progress(self) -> bool:
|
||||
"""
|
||||
True if we've entered the context.
|
||||
"""
|
||||
return self._level > 0
|
||||
|
||||
def increment(self, metric: str, value: int) -> None:
|
||||
"""
|
||||
Increment a metric by a given amount.
|
||||
"""
|
||||
if self._level == 0:
|
||||
raise RuntimeError(f"Cannot increment {metric} outside of a MetricsContext")
|
||||
if metric not in self._metrics:
|
||||
self._metrics[metric] = 0
|
||||
self._metrics[metric] += value
|
||||
|
||||
def _render_edits(self, pred: set[str]) -> str:
|
||||
return "\n\n" + "\n\n".join(
|
||||
"Previous Traceback:\n" + "".join(e.format())
|
||||
for e, k in self._edits
|
||||
if k & pred
|
||||
)
|
||||
|
||||
def set(self, metric: str, value: Any, overwrite: bool = False) -> None:
|
||||
"""
|
||||
Set a metric to a given value. Raises if the metric has been assigned previously
|
||||
in the current context.
|
||||
"""
|
||||
if self._level == 0:
|
||||
raise RuntimeError(f"Cannot set {metric} outside of a MetricsContext")
|
||||
if metric in self._metrics and not overwrite:
|
||||
raise RuntimeError(
|
||||
self._render_edits({metric})
|
||||
+ f"\n\nRuntimeError: Metric '{metric}' has already been set in the current context "
|
||||
"(see above for current and previous traceback)."
|
||||
)
|
||||
self._edits.append((CapturedTraceback.extract(skip=1), {metric}))
|
||||
self._metrics[metric] = value
|
||||
|
||||
def set_key_value(self, metric: str, key: str, value: Any) -> None:
|
||||
"""
|
||||
Treats a give metric as a dictionary and set the k and value within it.
|
||||
Note that the metric must be a dictionary or not present.
|
||||
|
||||
We allow this to be called multiple times (i.e. for features, it's not uncommon
|
||||
for them to be used multiple times within a single compilation).
|
||||
"""
|
||||
if self._level == 0:
|
||||
raise RuntimeError(f"Cannot set {metric} outside of a MetricsContext")
|
||||
if metric not in self._metrics:
|
||||
self._metrics[metric] = {}
|
||||
self._metrics[metric][key] = value
|
||||
|
||||
def update(self, values: dict[str, Any], overwrite: bool = False) -> None:
|
||||
"""
|
||||
Set multiple metrics directly. This method does NOT increment. Raises if any
|
||||
metric has been assigned previously in the current context and overwrite is
|
||||
not set to True.
|
||||
"""
|
||||
if self._level == 0:
|
||||
raise RuntimeError("Cannot update metrics outside of a MetricsContext")
|
||||
existing = self._metrics.keys() & values.keys()
|
||||
if existing and not overwrite:
|
||||
raise RuntimeError(
|
||||
self._render_edits(set(values.keys()))
|
||||
+ f"\n\nRuntimeError: Metric(s) {existing} have already been set in the current context. "
|
||||
"(see above for current and previous traceback)."
|
||||
)
|
||||
self._edits.append((CapturedTraceback.extract(skip=1), set(values.keys())))
|
||||
self._metrics.update(values)
|
||||
|
||||
def update_outer(self, values: dict[str, Any]) -> None:
|
||||
"""
|
||||
Update, but only when at the outermost context.
|
||||
"""
|
||||
if self._level == 0:
|
||||
raise RuntimeError("Cannot update metrics outside of a MetricsContext")
|
||||
if self._level == 1:
|
||||
self.update(values)
|
||||
|
||||
def add_to_set(self, metric: str, value: Any) -> None:
|
||||
"""
|
||||
Records a metric as a set() of values.
|
||||
"""
|
||||
if self._level == 0:
|
||||
raise RuntimeError(f"Cannot add {metric} outside of a MetricsContext")
|
||||
if metric not in self._metrics:
|
||||
self._metrics[metric] = set()
|
||||
self._metrics[metric].add(value)
|
||||
|
||||
def add_top_n(self, metric: str, key: Any, val: int) -> None:
|
||||
"""
|
||||
Records a metric as a TopN set of values.
|
||||
"""
|
||||
if self._level == 0:
|
||||
return
|
||||
if metric not in self._metrics:
|
||||
self._metrics[metric] = TopN()
|
||||
self._metrics[metric].add(key, val)
|
||||
|
||||
|
||||
class RuntimeMetricsContext:
|
||||
def __init__(self, on_exit: OnExitType) -> None:
|
||||
"""
|
||||
Similar to MetricsContext, but used to gather the runtime metrics that are
|
||||
decoupled from compilation, where there's not a natural place to insert a
|
||||
context manager.
|
||||
"""
|
||||
self._on_exit = on_exit
|
||||
self._metrics: dict[str, Any] = {}
|
||||
self._start_time_ns: int = 0
|
||||
|
||||
def increment(
|
||||
self, metric: str, value: int, extra: dict[str, Any] | None = None
|
||||
) -> None:
|
||||
"""
|
||||
Increment a metric by a given amount.
|
||||
"""
|
||||
if not self._metrics:
|
||||
# Start timing on the first entry
|
||||
self._start_time_ns = time.time_ns()
|
||||
if metric not in self._metrics:
|
||||
self._metrics[metric] = 0
|
||||
self._metrics[metric] += value
|
||||
|
||||
if extra:
|
||||
for k, v in extra.items():
|
||||
if k not in self._metrics and v is not None:
|
||||
self._metrics[k] = v
|
||||
|
||||
def finish(self) -> None:
|
||||
"""
|
||||
Call the on_exit function with the metrics gathered so far and reset.
|
||||
"""
|
||||
if self._metrics:
|
||||
try:
|
||||
end_time_ns = time.time_ns()
|
||||
self._on_exit(
|
||||
self._start_time_ns, end_time_ns, self._metrics, None, None
|
||||
)
|
||||
except Exception:
|
||||
log.exception("Unexpected exception logging runtime metrics")
|
||||
finally:
|
||||
self._metrics = {}
|
||||
@@ -0,0 +1,158 @@
|
||||
"""Mutation tracking and dynamic module detection system for Dynamo.
|
||||
|
||||
This module provides mechanisms to track and respond to mutations in PyTorch modules
|
||||
and detect dynamically created or modified modules.
|
||||
|
||||
Key components:
|
||||
- MutationTracker: Tracks mutations to objects and invalidates associated cached code
|
||||
- GenerationTracker: Tracks module creation timing to identify dynamic instances
|
||||
- Patching system for nn.Module to detect mutations and dynamic creation
|
||||
|
||||
The system ensures that Dynamo's optimizations remain valid by detecting and responding
|
||||
to runtime changes in module state and structure.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import weakref
|
||||
from collections.abc import MutableMapping
|
||||
from typing import Any
|
||||
|
||||
import torch.nn
|
||||
from torch.nn import Module
|
||||
|
||||
from . import config
|
||||
from .utils import ExactWeakKeyDictionary, nn_module_has_global_hooks
|
||||
|
||||
|
||||
unpatched_nn_module_init = torch.nn.Module.__init__
|
||||
|
||||
|
||||
class MutationTracker:
|
||||
db: ExactWeakKeyDictionary = ExactWeakKeyDictionary()
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.mutation_count: int = 0
|
||||
self.watchers: list[weakref.ReferenceType[Any]] = []
|
||||
|
||||
def on_mutation(self, name: str) -> None:
|
||||
self.mutation_count += 1
|
||||
tmp = self.watchers
|
||||
self.watchers = []
|
||||
for ref in tmp:
|
||||
guarded = ref()
|
||||
if guarded is not None:
|
||||
guarded.invalidate(ref)
|
||||
|
||||
def track(self, guarded_code: Any) -> None:
|
||||
self.watchers.append(weakref.ref(guarded_code))
|
||||
|
||||
|
||||
def watch(obj: Any, guarded_code: Any) -> None:
|
||||
"""invalidate guarded_code when obj is mutated"""
|
||||
ensure_patched(type(obj))
|
||||
|
||||
if obj not in MutationTracker.db:
|
||||
MutationTracker.db[obj] = MutationTracker()
|
||||
tracker = MutationTracker.db[obj]
|
||||
tracker.track(guarded_code)
|
||||
|
||||
|
||||
def ensure_patched(cls: Any) -> None:
|
||||
if getattr(cls, "___needs_mutation_patch", True):
|
||||
cls.___needs_mutation_patch = False
|
||||
original_setattr = cls.__setattr__
|
||||
|
||||
@functools.wraps(original_setattr)
|
||||
def custom_setattr(self: Any, key: str, value: Any) -> None:
|
||||
try:
|
||||
MutationTracker.db[self].on_mutation(key)
|
||||
except KeyError:
|
||||
pass
|
||||
return original_setattr(self, key, value)
|
||||
|
||||
cls.__setattr__ = custom_setattr
|
||||
|
||||
|
||||
class GenerationTracker:
|
||||
generation: int = 0
|
||||
dynamic_classes: ExactWeakKeyDictionary = ExactWeakKeyDictionary()
|
||||
generation_values: ExactWeakKeyDictionary = ExactWeakKeyDictionary()
|
||||
|
||||
@classmethod
|
||||
def tag(cls, obj: Any) -> None:
|
||||
cls.generation_values[obj] = cls.generation
|
||||
|
||||
@staticmethod
|
||||
def mark_class_dynamic(cls: type[torch.nn.Module]) -> None:
|
||||
assert issubclass(cls, torch.nn.Module)
|
||||
GenerationTracker.dynamic_classes[cls] = True
|
||||
|
||||
@classmethod
|
||||
def get_generation_value(cls, obj: Any) -> int:
|
||||
if obj not in cls.generation_values:
|
||||
return -1
|
||||
return cls.generation_values[obj]
|
||||
|
||||
@classmethod
|
||||
def check(cls, obj: Any) -> bool:
|
||||
return (
|
||||
obj in cls.generation_values
|
||||
and cls.generation_values[obj] == cls.generation
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def clear(cls) -> None:
|
||||
cls.generation = 0
|
||||
cls.dynamic_classes = ExactWeakKeyDictionary()
|
||||
cls.generation_values = ExactWeakKeyDictionary()
|
||||
|
||||
|
||||
def is_dynamic_nn_module(obj: Any, is_export: bool) -> bool:
|
||||
"""Check for nn.Modules() created dynamically or mutated"""
|
||||
if isinstance(obj, torch.nn.Module) and (
|
||||
"forward" in obj.__dict__ or isinstance(obj, (dict, MutableMapping))
|
||||
):
|
||||
# A monkey patched `.forward` indicates something wacky is going on
|
||||
# Similarly a nn module also subclassed as a dict is unusual.
|
||||
return True
|
||||
if hasattr(obj, "torchdynamo_force_dynamic"):
|
||||
return obj.torchdynamo_force_dynamic
|
||||
if isinstance(obj, torch.nn.Module) and (
|
||||
not is_export or config.install_free_tensors
|
||||
):
|
||||
return True
|
||||
|
||||
if isinstance(obj, torch.nn.Module) and nn_module_has_global_hooks():
|
||||
return True
|
||||
dyn = GenerationTracker.dynamic_classes.get(type(obj)) or GenerationTracker.check(
|
||||
obj
|
||||
)
|
||||
return dyn
|
||||
|
||||
|
||||
def install_generation_tagging_init() -> None:
|
||||
"""
|
||||
Monkey patch torch.nn.Module.__init__ and torch.nn.Module.__setstate__
|
||||
so we can detect nn.Module instances created dynamically inside forward methods.
|
||||
"""
|
||||
|
||||
if getattr(Module, "___needs_generation_tag_patch", True):
|
||||
init = Module.__init__
|
||||
|
||||
def patched_init(self: Module, *args: Any, **kwargs: Any) -> None:
|
||||
init(self, *args, **kwargs)
|
||||
GenerationTracker.tag(self)
|
||||
|
||||
Module.__init__ = patched_init # type: ignore[method-assign]
|
||||
|
||||
setstate = Module.__setstate__
|
||||
|
||||
def patched_setstate(self: Module, state: Any) -> None:
|
||||
setstate(self, state)
|
||||
GenerationTracker.tag(self)
|
||||
|
||||
Module.__setstate__ = patched_setstate # type: ignore[method-assign]
|
||||
|
||||
Module.___needs_generation_tag_patch = False # type: ignore[attr-defined]
|
||||
|
||||
GenerationTracker.generation += 1
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,700 @@
|
||||
"""
|
||||
Python polyfills for common builtins.
|
||||
"""
|
||||
|
||||
# NOTE: 1. Please do not import any submodule in the directory here to avoid circular imports.
|
||||
# 2. While adding a new polyfill module, also add it to POLYFILLED_MODULE_NAMES in loader.py.
|
||||
# Add it in the TYPE_CHECKING block below as well.
|
||||
|
||||
import types
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping, Sequence
|
||||
from itertools import repeat as _repeat
|
||||
from operator import eq, ne
|
||||
from typing import Any, TYPE_CHECKING, TypeGuard, TypeVar
|
||||
from typing_extensions import TypeIs
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
C = TypeVar("C")
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..utils import dict_keys
|
||||
|
||||
# Load by torch._dynamo.polyfills.loader
|
||||
# See also the POLYFILLED_MODULE_NAMES in torch/_dynamo/polyfills/loader.py
|
||||
# Put the submodules here to avoid circular imports
|
||||
from . import (
|
||||
_collections as _collections,
|
||||
builtins as builtins,
|
||||
functools as functools,
|
||||
itertools as itertools,
|
||||
operator as operator,
|
||||
os as os,
|
||||
pytree as pytree,
|
||||
struct as struct,
|
||||
sys as sys,
|
||||
torch_c_nn as torch_c_nn,
|
||||
traceback as traceback,
|
||||
)
|
||||
|
||||
from torch.overrides import BaseTorchFunctionMode
|
||||
|
||||
|
||||
# These classes handle support for TorchFunctionModes across
|
||||
# graph breaks
|
||||
# Today the TorchFunctionMode enter (for the classes we support)
|
||||
# simply pushes the mode onto the stack. Since after this occurs
|
||||
# the stack is mutated, and we replay these mutations, we don't need
|
||||
# any cleanup logic to be run once the graph break occurs, we simply replay
|
||||
# these mutations to ensure at the graph break the torch function mode stack is correct
|
||||
# and reconstruct the torch function mode stack normally
|
||||
# when we compile the resume function on the other side of the break.
|
||||
# However, to ensure we exit properly
|
||||
# in the resume function, we need to re-enter the contexts as we do other contexts.
|
||||
# These contexts do nothing on enter, but provide the correct exit logic to ensure
|
||||
# the stack state is correct.
|
||||
class NoEnterTorchFunctionMode(BaseTorchFunctionMode):
|
||||
def __enter__(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
# Used by WrappedUserFunctionVariable and similar to inline decorated function
|
||||
# calls with bytecode backing. Without this, the context enter/exit happens in
|
||||
# Python-level VT code, so a nested graph break inside `fn` would skip applying
|
||||
# the context in the compiled fn/resume. By inlining through this polyfill, the
|
||||
# `with` statement has real bytecode that the resume function can continue from.
|
||||
def _fn_with_ctx(ctx: Any, fn: Callable[..., T], *args: Any, **kwargs: Any) -> T:
|
||||
with ctx:
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
|
||||
def index(
|
||||
iterator: Iterator[T], item: T, start: int = 0, end: int | None = None
|
||||
) -> int:
|
||||
from itertools import islice
|
||||
|
||||
for i, elem in islice(enumerate(iterator), start, end):
|
||||
if item == elem:
|
||||
return i
|
||||
# This will not run in dynamo
|
||||
raise ValueError(f"{item} is not in {type(iterator)}")
|
||||
|
||||
|
||||
def repeat(item: T, count: int) -> Iterator[T]:
|
||||
for _ in range(count):
|
||||
yield item
|
||||
|
||||
|
||||
def radians(x: float) -> float:
|
||||
import math
|
||||
|
||||
return math.pi / 180.0 * x
|
||||
|
||||
|
||||
def impl_IS_MAPPING(a: object) -> TypeIs[Mapping[Any, Any]]:
|
||||
return isinstance(a, Mapping)
|
||||
|
||||
|
||||
def impl_MATCH_SEQUENCE(a: object) -> TypeGuard[Sequence[Any]]:
|
||||
return isinstance(a, Sequence) and not isinstance(a, (str, bytes, bytearray))
|
||||
|
||||
|
||||
def _match_class_attr(obj: object, name: str, seen: set[str]) -> object:
|
||||
if name in seen:
|
||||
raise TypeError(f"{type(obj)} got multiple sub-patterns for attribute {name}")
|
||||
|
||||
attr = getattr(obj, name)
|
||||
seen.add(name)
|
||||
return attr
|
||||
|
||||
|
||||
def impl_MATCH_CLASS(
|
||||
subject: object, cls: type, nargs: int, kwargs: tuple[str, ...]
|
||||
) -> tuple[object, ...] | None:
|
||||
if not isinstance(cls, type):
|
||||
raise TypeError("called match pattern must be a class")
|
||||
|
||||
if not isinstance(subject, cls):
|
||||
return None
|
||||
|
||||
typ = type(subject)
|
||||
match_self = False
|
||||
match_args = ()
|
||||
|
||||
attrs = []
|
||||
seen = set()
|
||||
|
||||
if nargs:
|
||||
if hasattr(typ, "__match_args__"):
|
||||
match_args = typ.__match_args__
|
||||
|
||||
if not isinstance(match_args, tuple):
|
||||
raise TypeError(
|
||||
f"{typ}.__match_args__ must be a tuple, (got {type(match_args)})"
|
||||
)
|
||||
|
||||
for name in match_args[:nargs]:
|
||||
if not isinstance(name, str):
|
||||
raise TypeError(
|
||||
f"__match_args__ elements must be strings (got {type(name)})"
|
||||
)
|
||||
attrs.append(_match_class_attr(subject, name, seen))
|
||||
else:
|
||||
# We should somehow check if the type has TPFLAGS_MATCH_SELF set
|
||||
# match_self is only true if TPFLAGS_MATCH_SELF is set, but there is
|
||||
# no way to check for it directly in Python. So we assume it is set
|
||||
# if there are no __match_args__
|
||||
match_self = True
|
||||
attrs.append(subject)
|
||||
|
||||
allowed = 1 if match_self else len(match_args)
|
||||
if allowed < nargs:
|
||||
raise TypeError(
|
||||
f"accepts {allowed} positional sub-patterns ({nargs} given)"
|
||||
)
|
||||
|
||||
for name in kwargs:
|
||||
attrs.append(_match_class_attr(subject, name, seen))
|
||||
|
||||
return tuple(attrs)
|
||||
|
||||
|
||||
def impl_MATCH_KEYS(obj: Mapping[T, U], keys: tuple[T, ...]) -> tuple[U, ...] | None:
|
||||
assert isinstance(obj, Mapping)
|
||||
if all(key in obj for key in keys):
|
||||
return tuple(obj[key] for key in keys)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def impl_CONTAINS_OP_fallback(a: T, b: Iterable[T]) -> bool:
|
||||
# performs fallback "a in b"
|
||||
if hasattr(b, "__iter__"):
|
||||
# use __iter__ if __contains__ is not available
|
||||
for x in b:
|
||||
if x == a:
|
||||
return True
|
||||
return False
|
||||
raise TypeError(f"argument of type {type(b)} is not iterable")
|
||||
|
||||
|
||||
def accumulate_grad(x: torch.Tensor, new_grad: torch.Tensor | None) -> None:
|
||||
# polyfills according to the Gradient Layout Contract
|
||||
if new_grad is None:
|
||||
return
|
||||
new_grad_strided = torch.empty_like(x)
|
||||
new_grad_strided.copy_(new_grad)
|
||||
if x.grad is None:
|
||||
x.grad = new_grad_strided
|
||||
elif torch.is_grad_enabled():
|
||||
x.grad = x.grad + new_grad_strided
|
||||
else:
|
||||
x.grad.add_(new_grad_strided)
|
||||
|
||||
|
||||
# This mirrors
|
||||
# https://github.com/python/cpython/blob/a1c52d1265c65bcf0d9edf87e143843ad54f9b8f/Objects/listobject.c#L3352-L3413
|
||||
def list_cmp(
|
||||
op: Callable[[Any, Any], bool], left: Sequence[T], right: Sequence[T]
|
||||
) -> bool:
|
||||
"""emulate `(1,2,3) > (1,2)` etc"""
|
||||
|
||||
# Optimization: For equality, short-circuit if lengths differ
|
||||
# This avoids iterating through elements and triggering guards on SymInts
|
||||
left_len = len(left)
|
||||
right_len = len(right)
|
||||
|
||||
if op is eq and left_len != right_len:
|
||||
return False
|
||||
if op is ne and left_len != right_len:
|
||||
return True
|
||||
|
||||
# Apply `op` to the first pair that differ
|
||||
for a, b in zip(left, right):
|
||||
if a != b:
|
||||
return op(a, b)
|
||||
|
||||
# No more pairs to compare, so compare sizes.
|
||||
return op(left_len, right_len)
|
||||
|
||||
|
||||
def dict___eq__(d: dict[T, U], other: dict[T, U]) -> bool:
|
||||
if (len(d) != len(other)) or (d.keys() != other.keys()):
|
||||
return False
|
||||
|
||||
if all(isinstance(a, OrderedDict) for a in (d, other)):
|
||||
return list(d.items()) == list(other.items())
|
||||
|
||||
for k, v in d.items():
|
||||
if v != other[k]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def set_symmetric_difference(
|
||||
set1: Iterable[T],
|
||||
set2: Iterable[T],
|
||||
cls: type[Any] = set,
|
||||
) -> Any:
|
||||
symmetric_difference_set: set[T] = set()
|
||||
for x in set1:
|
||||
if x not in set2:
|
||||
symmetric_difference_set.add(x)
|
||||
for x in set2:
|
||||
if x not in set1:
|
||||
symmetric_difference_set.add(x)
|
||||
return cls(symmetric_difference_set)
|
||||
|
||||
|
||||
def set_symmetric_difference_update(set1: set[T], set2: set[T]) -> None:
|
||||
result = set1.symmetric_difference(set2)
|
||||
set1.clear()
|
||||
set1.update(result)
|
||||
|
||||
|
||||
def set_isdisjoint(set1: set[T], set2: set[T]) -> bool:
|
||||
if not isinstance(set2, Iterable):
|
||||
raise TypeError(f"'{type(set2)}' object is not iterable")
|
||||
|
||||
for x in set1:
|
||||
for y in set2:
|
||||
if not isinstance(y, Hashable):
|
||||
raise TypeError(f"unhashable type: '{type(y)}'")
|
||||
if x == y:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def set_intersection(
|
||||
set1: set[T],
|
||||
*others: Iterable[T],
|
||||
# See facebook/pyrefly#1496 - leave generic
|
||||
cls: type[Any] = set,
|
||||
) -> Any:
|
||||
if len(others) == 0:
|
||||
return set1.copy()
|
||||
|
||||
if not all(isinstance(s, Iterable) for s in others):
|
||||
raise TypeError(f"set.difference expected an iterable, got {type(others)}")
|
||||
|
||||
for s in others:
|
||||
if any(not isinstance(x, Hashable) for x in s):
|
||||
raise TypeError("unhashable type")
|
||||
|
||||
# return a new set with elements common in all sets
|
||||
intersection_set = set()
|
||||
for x in set1:
|
||||
for set2 in others:
|
||||
if not any(x == y for y in set2):
|
||||
break
|
||||
else:
|
||||
intersection_set.add(x)
|
||||
return cls(intersection_set)
|
||||
|
||||
|
||||
def set_intersection_update(set1: set[T], *others: Iterable[T]) -> None:
|
||||
result = set1.intersection(*others)
|
||||
set1.clear()
|
||||
set1.update(result)
|
||||
|
||||
|
||||
def set_union(
|
||||
set1: set[T], *others: Iterable[T], cls: type[C] | None = None
|
||||
) -> C | set[T]:
|
||||
# frozenset also uses this function
|
||||
if cls is None:
|
||||
# pyrefly: ignore[bad-assignment]
|
||||
cls = type(set1)
|
||||
|
||||
if len(others) == 0:
|
||||
return set1.copy()
|
||||
|
||||
if not all(isinstance(s, Iterable) for s in others):
|
||||
raise TypeError(f"set.union expected an iterable, got {type(others)}")
|
||||
|
||||
for s in others:
|
||||
if any(not isinstance(x, Hashable) for x in s):
|
||||
raise TypeError("unhashable type")
|
||||
|
||||
union_set = set(set1.copy())
|
||||
for set2 in others:
|
||||
set_update(union_set, set2)
|
||||
|
||||
# frozenset also uses this function
|
||||
# pyrefly: ignore [bad-argument-count, not-callable]
|
||||
return cls(union_set)
|
||||
|
||||
|
||||
# pyrefly: ignore [bad-return]
|
||||
def set_update(set1: set[T], *others: Iterable[T]) -> set[T]:
|
||||
if len(others) == 0:
|
||||
return set1
|
||||
|
||||
for set2 in others:
|
||||
for x in set2:
|
||||
if x not in set1:
|
||||
set1.add(x)
|
||||
|
||||
|
||||
def set_difference(
|
||||
set1: set[T],
|
||||
*others: Iterable[T],
|
||||
cls: type[Any] = set,
|
||||
) -> Any:
|
||||
if len(others) == 0:
|
||||
return set1.copy()
|
||||
|
||||
if not all(isinstance(s, Iterable) for s in others):
|
||||
raise TypeError(f"set.difference expected an iterable, got {type(others)}")
|
||||
|
||||
for s in others:
|
||||
if any(not isinstance(x, Hashable) for x in s):
|
||||
raise TypeError("unhashable type")
|
||||
|
||||
difference_set = set()
|
||||
for x in set1:
|
||||
for set2 in others:
|
||||
if x in set2:
|
||||
break
|
||||
else:
|
||||
difference_set.add(x)
|
||||
return cls(difference_set)
|
||||
|
||||
|
||||
def set_difference_update(set1: set[T], *others: Iterable[T]) -> None:
|
||||
result = set1.difference(*others)
|
||||
set1.clear()
|
||||
set1.update(result)
|
||||
|
||||
|
||||
def assert_dict_equal(
|
||||
self_: Any, d1: dict[T, U], d2: dict[T, U], msg: str | None = None
|
||||
) -> None:
|
||||
self_.assertTrue(d1 == d2, msg)
|
||||
|
||||
|
||||
def assert_multi_line_equal(
|
||||
self_: Any, first: T, second: T, msg: str | None = None
|
||||
) -> None:
|
||||
return self_.assertTrue(first == second, msg)
|
||||
|
||||
|
||||
# The original impl. uses difflib
|
||||
def assert_sequence_equal(
|
||||
self_: Any,
|
||||
seq1: Sequence[T],
|
||||
seq2: Sequence[T],
|
||||
msg: str | None = None,
|
||||
seq_type: type[Any] | None = None,
|
||||
) -> None:
|
||||
return self_.assertTrue(seq1 == seq2, msg)
|
||||
|
||||
|
||||
def getattr_and_trace(*args: Any, **kwargs: Any) -> Any:
|
||||
wrapper_obj = args[0]
|
||||
attr_name = args[1]
|
||||
fn = getattr(wrapper_obj, attr_name)
|
||||
return fn(*args[2:], **kwargs)
|
||||
|
||||
|
||||
def mapping_get(obj: Mapping[T, U], key: T, value: U | None = None, /) -> U | None:
|
||||
try:
|
||||
return obj.__getitem__(key)
|
||||
except KeyError:
|
||||
return value
|
||||
|
||||
|
||||
def instantiate_user_defined_class_object(
|
||||
cls: type[T], /, *args: Any, **kwargs: Any
|
||||
) -> T:
|
||||
obj = cls.__new__(cls, *args, **kwargs)
|
||||
|
||||
# Only call __init__ if the object's type is a subclass of cls.
|
||||
# CPython uses PyType_IsSubtype(Py_TYPE(obj), type) at the C level, which does NOT
|
||||
# go through metaclass __instancecheck__. Using isinstance() here would be wrong
|
||||
# for classes with custom __instancecheck__ (e.g. torch.ByteStorage).
|
||||
# Reference: https://github.com/python/cpython/blob/3.12/Objects/typeobject.c#L1670-L1673
|
||||
if issubclass(type(obj), cls):
|
||||
obj.__init__(*args, **kwargs)
|
||||
return obj
|
||||
|
||||
|
||||
def mutable_mapping_update(
|
||||
self,
|
||||
data: Mapping[T, U] | Iterable[tuple[T, U]] = (),
|
||||
/,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
if isinstance(data, Mapping):
|
||||
# Merge standard mapping with PyMapping_Items
|
||||
for key, value in data.items():
|
||||
self[key] = value
|
||||
# FIXME: Enabling the `elif`-branch below needs too many `VariableClass.call_obj_hasattr` changes.
|
||||
# >>> class Foo:
|
||||
# ... def __init__(self):
|
||||
# ... self.keys = lambda: ['a', 'b', 'c'] # not required to be a method
|
||||
# ...
|
||||
# ... def __getitem__(self, key):
|
||||
# ... return 0
|
||||
# ...
|
||||
# >>> dict(Foo())
|
||||
# {'a': 0, 'b': 0, 'c': 0}
|
||||
#
|
||||
# > This is a rare case, so we comment it out for now.
|
||||
#
|
||||
# elif hasattr(data, "keys"):
|
||||
# # Merge mapping-like object with PyMapping_Keys + PyObject_GetItem
|
||||
# for key in data.keys():
|
||||
# self[key] = data[key]
|
||||
else:
|
||||
if not isinstance(data, Iterable):
|
||||
raise TypeError(f"{type(data).__name__!r} object is not iterable")
|
||||
# Likely a sequence of pairs
|
||||
for key, value in data:
|
||||
self[key] = value
|
||||
|
||||
if kwargs:
|
||||
for key, value in kwargs.items():
|
||||
self[key] = value
|
||||
|
||||
|
||||
# Used with something like dict(obj)
|
||||
def construct_dict(
|
||||
cls: type[T],
|
||||
data: Mapping[object, object] | Iterable[tuple[object, object]] = (),
|
||||
/,
|
||||
**kwargs: Any,
|
||||
) -> T:
|
||||
self = cls.__new__(cls)
|
||||
mutable_mapping_update(self, data, **kwargs)
|
||||
return self
|
||||
|
||||
|
||||
def foreach_map_fn(*args: Any) -> Any:
|
||||
op = args[0]
|
||||
new_args: list[Any] = []
|
||||
at_least_one_list = False
|
||||
for arg in args[1:]:
|
||||
if not isinstance(arg, (list, tuple)):
|
||||
new_args.append(_repeat(arg))
|
||||
else:
|
||||
at_least_one_list = True
|
||||
new_args.append(arg)
|
||||
|
||||
# Just apply op once to args if there are no lists
|
||||
if not at_least_one_list:
|
||||
return op(*args[1:])
|
||||
|
||||
out = []
|
||||
for unpacked in zip(*new_args):
|
||||
out.append(op(*unpacked))
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def foreach_lerp_inplace(
|
||||
self,
|
||||
end: list[torch.Tensor] | tuple[torch.Tensor, ...],
|
||||
weight: float | int | torch.Tensor,
|
||||
) -> None:
|
||||
# Decompose lerp via addcmul_ for FMA. Uses the same dual-formula
|
||||
# approach as CUDA's native lerp to get bitwise identical results:
|
||||
# |w| < 0.5 (low): fma(w, diff, start)
|
||||
# |w| >= 0.5 (high): fma(-(1-w), diff, end)
|
||||
# For tensor weights (e.g. 0-dim tensor from tensor betas in Adam) the
|
||||
# low formula is always used because the native lerp_scalar lowering
|
||||
# would crash on float(weight) for symbolic expressions.
|
||||
diff = torch._foreach_sub(end, self)
|
||||
if isinstance(weight, torch.Tensor):
|
||||
# Select base and weight for the dual formula before a single addcmul:
|
||||
# low (|w| < 0.5): fma(w, diff, self)
|
||||
# high (|w| >= 0.5): fma(-(1-w), diff, end)
|
||||
mask = weight.abs() >= 0.5
|
||||
neg_omw = -(1.0 - weight)
|
||||
w = torch.where(mask, neg_omw, weight)
|
||||
bases = [torch.where(mask, e, s) for s, e in zip(self, end)]
|
||||
w_list = [w] * len(diff)
|
||||
torch._foreach_addcmul_(bases, w_list, diff)
|
||||
for s, b in zip(self, bases):
|
||||
s.copy_(b)
|
||||
else:
|
||||
abs_weight = weight if weight >= 0 else -weight
|
||||
if abs_weight >= 0.5:
|
||||
# High formula: end + (-(1-w)) * diff → fma(-(1-w), diff, end)
|
||||
# Compute 1-w in target dtype to match CUDA rounding.
|
||||
d0 = self[0]
|
||||
neg_omw = -(1.0 - torch.tensor(weight, dtype=d0.dtype, device=d0.device))
|
||||
neg_omw_list = [neg_omw] * len(diff)
|
||||
for s, e in zip(self, end):
|
||||
s.copy_(e)
|
||||
torch._foreach_addcmul_(self, neg_omw_list, diff)
|
||||
else:
|
||||
# Low formula: start + w * diff → fma(w, diff, start)
|
||||
weights = [torch.full_like(d, weight) for d in diff]
|
||||
torch._foreach_addcmul_(self, weights, diff)
|
||||
return self
|
||||
|
||||
|
||||
def foreach_pow_scalar(
|
||||
scalar: Any, exps: Sequence[bool | complex | float | int]
|
||||
) -> tuple[torch.Tensor, ...]:
|
||||
return torch._foreach_pow([scalar for _ in exps], exps)
|
||||
|
||||
|
||||
def predicate(obj: object) -> bool:
|
||||
# This will cause the rest of dynamo to handle the if statement correctly, so we don't have to rewrite it here.
|
||||
# We can't just use bool() here since we can't trace into that in general.
|
||||
if obj:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def cmp_eq(a: object, b: object) -> bool:
|
||||
# Note that the commented `is` check should ideally be removed. This is a
|
||||
# CPython optimization that skips the __eq__ checks it the obj id's are
|
||||
# same. But, these lines adds many `is` nodes in the Fx graph for
|
||||
# SymNodeVariable. For now, we can just skip this check. This is STILL
|
||||
# correct because one of the __eq__ checks will pass later, just could be
|
||||
# slow in some corner cases.
|
||||
# if a is b:
|
||||
# return True
|
||||
if isinstance(a, type):
|
||||
# Default metaclass equality is identity-based. Preserve the reflected
|
||||
# operand fallback without tracing through type.__eq__.
|
||||
if type(a).__eq__ is type.__eq__:
|
||||
result = True if a is b else NotImplemented
|
||||
else:
|
||||
result = type(a).__eq__(a, b)
|
||||
else:
|
||||
result = a.__eq__(b)
|
||||
if result is NotImplemented:
|
||||
if isinstance(b, type):
|
||||
if type(b).__eq__ is type.__eq__:
|
||||
result = True if a is b else NotImplemented
|
||||
else:
|
||||
result = type(b).__eq__(b, a)
|
||||
else:
|
||||
result = b.__eq__(a)
|
||||
return result is not NotImplemented and result
|
||||
|
||||
|
||||
def cmp_ne(a: object, b: object) -> bool:
|
||||
if isinstance(a, type):
|
||||
if type(a).__ne__ is type.__ne__:
|
||||
result = False if a is b else NotImplemented
|
||||
else:
|
||||
result = type(a).__ne__(a, b)
|
||||
if result is not NotImplemented:
|
||||
return result
|
||||
elif isinstance(type(a).__ne__, types.FunctionType):
|
||||
result = a.__ne__(b)
|
||||
if result is not NotImplemented:
|
||||
return result
|
||||
# Fall through to try b.__ne__(a) or cmp_eq
|
||||
if isinstance(b, type):
|
||||
if type(b).__ne__ is type.__ne__:
|
||||
result = False if a is b else NotImplemented
|
||||
else:
|
||||
result = type(b).__ne__(b, a)
|
||||
if result is not NotImplemented:
|
||||
return result
|
||||
elif isinstance(type(b).__ne__, types.FunctionType):
|
||||
result = b.__ne__(a)
|
||||
if result is not NotImplemented:
|
||||
return result
|
||||
return not cmp_eq(a, b)
|
||||
|
||||
|
||||
def cmp_lt(a: Any, b: Any) -> bool:
|
||||
result = a.__lt__(b)
|
||||
if result is NotImplemented:
|
||||
raise TypeError(f"{type(a)} does not support the < operator")
|
||||
return result
|
||||
|
||||
|
||||
def cmp_le(a: Any, b: Any) -> bool:
|
||||
# Check if __le__ is overridden
|
||||
if isinstance(type(a).__le__, types.FunctionType):
|
||||
return a.__le__(b)
|
||||
return cmp_eq(a, b) or cmp_lt(a, b)
|
||||
|
||||
|
||||
def cmp_gt(a: Any, b: Any) -> bool:
|
||||
# Check if __gt__ is overridden
|
||||
if isinstance(type(a).__gt__, types.FunctionType):
|
||||
return a.__gt__(b)
|
||||
# a > b is equivalent to b < a
|
||||
return cmp_lt(b, a)
|
||||
|
||||
|
||||
def cmp_ge(a: Any, b: Any) -> bool:
|
||||
# Check if __ge__ is overridden
|
||||
if isinstance(type(a).__ge__, types.FunctionType):
|
||||
return a.__ge__(b)
|
||||
return cmp_eq(a, b) or cmp_gt(a, b)
|
||||
|
||||
|
||||
def group_tensors_by_device_and_dtype(
|
||||
tensorlistlist: list[list[torch.Tensor | None]], with_indices: bool = False
|
||||
) -> dict[tuple[torch.device, torch.dtype], tuple[list[list[Any]], list[int]]]:
|
||||
"""Pure Python implementation of torch._C._group_tensors_by_device_and_dtype.
|
||||
|
||||
Groups tensors by their device and dtype. This is useful before sending
|
||||
tensors off to a foreach implementation, which requires tensors to be on
|
||||
one device and dtype.
|
||||
|
||||
Args:
|
||||
tensorlistlist: A list of lists of tensors (tensors can be None).
|
||||
with_indices: If True, track original indices in the output.
|
||||
|
||||
Returns:
|
||||
A dict mapping (device, dtype) tuples to (grouped_tensorlistlist, indices).
|
||||
"""
|
||||
# Result dict: (device, dtype) -> (list of lists, indices)
|
||||
result: dict[
|
||||
tuple[torch.device, torch.dtype], tuple[list[list[Any]], list[int]]
|
||||
] = {}
|
||||
|
||||
if not tensorlistlist or not tensorlistlist[0]:
|
||||
return result
|
||||
|
||||
num_lists = len(tensorlistlist)
|
||||
num_tensors = len(tensorlistlist[0])
|
||||
|
||||
for idx in range(num_tensors):
|
||||
# Find the first non-None tensor at this index to get device and dtype
|
||||
first_tensor = None
|
||||
for tlist in tensorlistlist:
|
||||
if tlist is not None and idx < len(tlist) and tlist[idx] is not None:
|
||||
first_tensor = tlist[idx]
|
||||
break
|
||||
|
||||
if first_tensor is None:
|
||||
# All tensors at this index are None, skip
|
||||
continue
|
||||
|
||||
key = (first_tensor.device, first_tensor.dtype)
|
||||
|
||||
if key not in result:
|
||||
# Initialize empty lists for each tensorlist
|
||||
result[key] = ([[] for _ in range(num_lists)], [])
|
||||
|
||||
grouped_lists, indices = result[key]
|
||||
|
||||
# Add tensors from each list at this index
|
||||
for list_idx, tlist in enumerate(tensorlistlist):
|
||||
if tlist is not None and idx < len(tlist):
|
||||
grouped_lists[list_idx].append(tlist[idx])
|
||||
else:
|
||||
grouped_lists[list_idx].append(None)
|
||||
|
||||
if with_indices:
|
||||
indices.append(idx)
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
Python polyfills for builtins
|
||||
"""
|
||||
|
||||
from collections.abc import Iterable, MutableMapping
|
||||
from typing import TypeVar
|
||||
|
||||
from ..decorators import substitute_in_graph
|
||||
|
||||
|
||||
__all__ = []
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
try:
|
||||
import _collections # type: ignore[import-not-found]
|
||||
|
||||
@substitute_in_graph(_collections._count_elements)
|
||||
def _count_elements(
|
||||
mapping: MutableMapping[T, int],
|
||||
iterable: Iterable[T],
|
||||
) -> None:
|
||||
"Tally elements from the iterable."
|
||||
mapping_get = mapping.get
|
||||
for elem in iterable:
|
||||
mapping[elem] = mapping_get(elem, 0) + 1
|
||||
|
||||
__all__.append("_count_elements")
|
||||
|
||||
except ImportError:
|
||||
pass
|
||||
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
Python polyfills for builtins
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import builtins
|
||||
import functools
|
||||
import operator
|
||||
import typing
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
from ..decorators import substitute_in_graph
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable
|
||||
|
||||
|
||||
__all__ = [
|
||||
"all",
|
||||
"any",
|
||||
"cast",
|
||||
"enumerate",
|
||||
"sum",
|
||||
]
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
@substitute_in_graph(builtins.all, can_constant_fold_through=True)
|
||||
def all(iterable: Iterable[object], /) -> bool:
|
||||
for elem in iterable:
|
||||
if not elem:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@substitute_in_graph(builtins.any, can_constant_fold_through=True)
|
||||
def any(iterable: Iterable[object], /) -> bool:
|
||||
for elem in iterable:
|
||||
if elem:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@substitute_in_graph(builtins.enumerate, is_embedded_type=True) # type: ignore[arg-type]
|
||||
def enumerate(iterable: Iterable[_T], start: int = 0) -> Iterable[tuple[int, _T]]:
|
||||
if not isinstance(start, int):
|
||||
raise TypeError(
|
||||
f"{type(start).__name__!r} object cannot be interpreted as an integer"
|
||||
)
|
||||
|
||||
for x in iterable:
|
||||
yield start, x
|
||||
start += 1
|
||||
|
||||
|
||||
@substitute_in_graph(builtins.sum, can_constant_fold_through=True) # type: ignore[arg-type]
|
||||
def sum(iterable: Iterable[_T], /, start: _T = 0) -> _T: # type: ignore[assignment]
|
||||
return functools.reduce(operator.add, iterable, start)
|
||||
|
||||
|
||||
class _CallableIterator:
|
||||
def __init__(self, fn, sentinel): # type: ignore[no-untyped-def]
|
||||
self.fn = fn
|
||||
self.sentinel = sentinel
|
||||
|
||||
def __iter__(self): # type: ignore[no-untyped-def]
|
||||
return self
|
||||
|
||||
def __next__(self): # type: ignore[no-untyped-def]
|
||||
# The iterator created in this case will call object with no arguments
|
||||
# for each call to its __next__() method;
|
||||
r = self.fn()
|
||||
|
||||
# If the value returned is equal to sentinel, StopIteration will be raised
|
||||
if r == self.sentinel:
|
||||
raise StopIteration
|
||||
|
||||
# otherwise the value will be returned.
|
||||
return r
|
||||
|
||||
|
||||
_sentinel_missing = object()
|
||||
|
||||
|
||||
# TODO(guilhermeleobas): use substitute_in_graph for iter()
|
||||
def iter_(fn_or_iterable, sentinel=_sentinel_missing, /): # type: ignore[no-untyped-def]
|
||||
# Without a second argument, object must be a collection object which supports
|
||||
# the iterable (__iter__) or the sequence protocol (__getitem__ with an integer
|
||||
# starting at 0)
|
||||
if sentinel is _sentinel_missing:
|
||||
iterable = fn_or_iterable
|
||||
if hasattr(iterable, "__iter__"):
|
||||
iterator = iterable.__iter__()
|
||||
if hasattr(iterator, "__next__"):
|
||||
return iterator
|
||||
else:
|
||||
raise TypeError(f"'{type(iterator)}' object is not iterable")
|
||||
if hasattr(iterable, "__getitem__"):
|
||||
# Needs to be a new function to avoid iter becoming a generator
|
||||
def sequence_protocol(iterable): # type: ignore[no-untyped-def]
|
||||
i = 0
|
||||
while True:
|
||||
try:
|
||||
yield iterable.__getitem__(i)
|
||||
i += 1
|
||||
except IndexError:
|
||||
break
|
||||
|
||||
return sequence_protocol(iterable)
|
||||
raise TypeError(f"'{type(iterable)}' object is not iterable")
|
||||
else:
|
||||
# If the second argument, sentinel, is given, then object must be a
|
||||
# callable object.
|
||||
fn = fn_or_iterable
|
||||
|
||||
if not isinstance(fn, Callable): # type: ignore[arg-type]
|
||||
raise TypeError("iter(v, w): v must be a callable")
|
||||
|
||||
return _CallableIterator(fn, sentinel)
|
||||
|
||||
|
||||
@substitute_in_graph(typing.cast, can_constant_fold_through=True)
|
||||
def cast(typ: type, val: _T) -> _T: # type: ignore[type-var]
|
||||
return val
|
||||
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
Python polyfills for copy
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypeVar
|
||||
|
||||
from ..decorators import substitute_in_graph
|
||||
|
||||
|
||||
__all__ = [
|
||||
"reduce_ex_user_defined_object",
|
||||
]
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@substitute_in_graph(object.__reduce_ex__, skip_signature_check=True) # type: ignore[arg-type]
|
||||
def reduce_ex_user_defined_object(obj: T, protocol: int, /) -> tuple: # type: ignore[type-arg]
|
||||
"""Traceable polyfill for object.__reduce_ex__ on user-defined objects.
|
||||
|
||||
Returns the same tuple that CPython's _common_reduce produces:
|
||||
(copyreg.__newobj__, (cls,), obj.__dict__, None, None).
|
||||
copy._reconstruct then calls cls.__new__(cls) and updates __dict__.
|
||||
"""
|
||||
import copyreg
|
||||
|
||||
cls = type(obj)
|
||||
return (
|
||||
copyreg.__newobj__, # pyrefly: ignore[missing-attribute]
|
||||
(cls,),
|
||||
obj.__dict__,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
Python polyfills for functools
|
||||
"""
|
||||
|
||||
import functools
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import TypeVar
|
||||
|
||||
from ..decorators import substitute_in_graph
|
||||
|
||||
|
||||
__all__ = ["reduce"]
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_U = TypeVar("_U")
|
||||
|
||||
|
||||
_initial_missing = object()
|
||||
|
||||
|
||||
# Reference: https://docs.python.org/3/library/functools.html#functools.reduce
|
||||
@substitute_in_graph(functools.reduce)
|
||||
def reduce(
|
||||
function: Callable[[_U, _T], _U],
|
||||
iterable: Iterable[_T],
|
||||
initial: _U = _initial_missing, # type: ignore[assignment]
|
||||
/,
|
||||
) -> _U:
|
||||
it = iter(iterable)
|
||||
|
||||
value: _U
|
||||
if initial is _initial_missing:
|
||||
try:
|
||||
value = next(it) # type: ignore[assignment]
|
||||
except StopIteration:
|
||||
raise TypeError(
|
||||
"reduce() of empty iterable with no initial value",
|
||||
) from None
|
||||
else:
|
||||
value = initial
|
||||
|
||||
for element in it:
|
||||
value = function(value, element)
|
||||
|
||||
return value
|
||||
@@ -0,0 +1,41 @@
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from torch._C import _fx_map_aggregate, _fx_map_arg
|
||||
from torch.fx.immutable_collections import immutable_dict, immutable_list
|
||||
from torch.fx.node import Node
|
||||
|
||||
from ..decorators import substitute_in_graph
|
||||
|
||||
|
||||
@substitute_in_graph(_fx_map_arg, can_constant_fold_through=True)
|
||||
def map_arg(a: Any, fn: Callable[[Node], Any]) -> Any:
|
||||
return map_aggregate(a, lambda x: fn(x) if isinstance(x, Node) else x)
|
||||
|
||||
|
||||
@substitute_in_graph(_fx_map_aggregate, can_constant_fold_through=True)
|
||||
def map_aggregate(a: Any, fn: Callable[[Any], Any]) -> Any:
|
||||
result: Any
|
||||
if isinstance(a, tuple):
|
||||
it = (map_aggregate(elem, fn) for elem in a)
|
||||
# Support NamedTuple (if it has `_fields`) by repacking into original type.
|
||||
result = type(a)(*it) if hasattr(a, "_fields") else tuple(it)
|
||||
elif isinstance(a, list):
|
||||
result = immutable_list([map_aggregate(elem, fn) for elem in a])
|
||||
elif isinstance(a, dict):
|
||||
result = immutable_dict([(k, map_aggregate(v, fn)) for k, v in a.items()])
|
||||
elif isinstance(a, slice):
|
||||
result = slice(
|
||||
map_aggregate(a.start, fn),
|
||||
map_aggregate(a.stop, fn),
|
||||
map_aggregate(a.step, fn),
|
||||
)
|
||||
else:
|
||||
result = fn(a)
|
||||
return result
|
||||
|
||||
|
||||
__all__ = [
|
||||
"map_arg",
|
||||
"map_aggregate",
|
||||
]
|
||||
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
Python polyfills for heapq
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import heapq
|
||||
import importlib
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
from ..decorators import substitute_in_graph
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import ModuleType
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
# Partially copied from CPython test/support/import_helper.py
|
||||
# https://github.com/python/cpython/blob/bb8791c0b75b5970d109e5557bfcca8a578a02af/Lib/test/support/import_helper.py
|
||||
def _save_and_remove_modules(names: set[str]) -> dict[str, ModuleType]:
|
||||
orig_modules = {}
|
||||
prefixes = tuple(name + "." for name in names)
|
||||
for modname in list(sys.modules):
|
||||
if modname in names or modname.startswith(prefixes):
|
||||
orig_modules[modname] = sys.modules.pop(modname)
|
||||
return orig_modules
|
||||
|
||||
|
||||
def import_fresh_module(name: str, blocked: list[str]) -> ModuleType:
|
||||
# Keep track of modules saved for later restoration as well
|
||||
# as those which just need a blocking entry removed
|
||||
names = {name, *blocked}
|
||||
orig_modules = _save_and_remove_modules(names)
|
||||
for modname in blocked:
|
||||
sys.modules[modname] = None # type: ignore[assignment]
|
||||
|
||||
try:
|
||||
return importlib.import_module(name)
|
||||
finally:
|
||||
_save_and_remove_modules(names)
|
||||
sys.modules.update(orig_modules)
|
||||
|
||||
|
||||
# Import the pure Python heapq module, blocking the C extension
|
||||
py_heapq = import_fresh_module("heapq", blocked=["_heapq"])
|
||||
|
||||
|
||||
__all__ = [
|
||||
"_heapify_max",
|
||||
"_heappop_max",
|
||||
"_heapreplace_max",
|
||||
"heapify",
|
||||
"heappop",
|
||||
"heappush",
|
||||
"heappushpop",
|
||||
"heapreplace",
|
||||
"merge",
|
||||
"nlargest",
|
||||
"nsmallest",
|
||||
]
|
||||
|
||||
|
||||
@substitute_in_graph(heapq._heapify_max)
|
||||
def _heapify_max(heap: list[_T], /) -> None:
|
||||
return py_heapq._heapify_max(heap)
|
||||
|
||||
|
||||
@substitute_in_graph(heapq._heappop_max) # type: ignore[attr-defined]
|
||||
def _heappop_max(heap: list[_T]) -> _T:
|
||||
return py_heapq._heappop_max(heap)
|
||||
|
||||
|
||||
@substitute_in_graph(heapq._heapreplace_max) # type: ignore[attr-defined]
|
||||
def _heapreplace_max(heap: list[_T], item: _T) -> _T:
|
||||
return py_heapq._heapreplace_max(heap, item)
|
||||
|
||||
|
||||
@substitute_in_graph(heapq.heapify)
|
||||
def heapify(heap: list[_T], /) -> None:
|
||||
return py_heapq.heapify(heap)
|
||||
|
||||
|
||||
@substitute_in_graph(heapq.heappop)
|
||||
def heappop(heap: list[_T], /) -> _T:
|
||||
return py_heapq.heappop(heap)
|
||||
|
||||
|
||||
@substitute_in_graph(heapq.heappush)
|
||||
def heappush(heap: list[_T], item: _T) -> None:
|
||||
return py_heapq.heappush(heap, item)
|
||||
|
||||
|
||||
@substitute_in_graph(heapq.heappushpop)
|
||||
def heappushpop(heap: list[_T], item: _T) -> _T:
|
||||
return py_heapq.heappushpop(heap, item)
|
||||
|
||||
|
||||
@substitute_in_graph(heapq.heapreplace)
|
||||
def heapreplace(heap: list[_T], item: _T) -> _T:
|
||||
return py_heapq.heapreplace(heap, item)
|
||||
|
||||
|
||||
@substitute_in_graph(heapq.merge) # type: ignore[arg-type]
|
||||
def merge(*iterables, key=None, reverse=False): # type: ignore[no-untyped-def]
|
||||
return py_heapq.merge(*iterables, key=key, reverse=reverse)
|
||||
|
||||
|
||||
@substitute_in_graph(heapq.nlargest) # type: ignore[arg-type]
|
||||
def nlargest(n, iterable, key=None): # type: ignore[no-untyped-def]
|
||||
return py_heapq.nlargest(n, iterable, key=key)
|
||||
|
||||
|
||||
@substitute_in_graph(heapq.nsmallest) # type: ignore[arg-type]
|
||||
def nsmallest(n, iterable, key=None): # type: ignore[no-untyped-def]
|
||||
return py_heapq.nsmallest(n, iterable, key=key)
|
||||
@@ -0,0 +1,338 @@
|
||||
"""
|
||||
Python polyfills for itertools
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import operator
|
||||
from collections.abc import Callable
|
||||
from typing import overload, TYPE_CHECKING, TypeAlias, TypeVar
|
||||
|
||||
from ..decorators import substitute_in_graph
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable, Iterator
|
||||
|
||||
|
||||
__all__ = [
|
||||
"accumulate",
|
||||
"chain",
|
||||
"chain_from_iterable",
|
||||
"compress",
|
||||
"cycle",
|
||||
"dropwhile",
|
||||
"filterfalse",
|
||||
"islice",
|
||||
"pairwise",
|
||||
"starmap",
|
||||
"takewhile",
|
||||
"tee",
|
||||
"zip_longest",
|
||||
]
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_U = TypeVar("_U")
|
||||
_Predicate: TypeAlias = Callable[[_T], object]
|
||||
_T1 = TypeVar("_T1")
|
||||
_T2 = TypeVar("_T2")
|
||||
|
||||
|
||||
# Reference: https://docs.python.org/3/library/itertools.html#itertools.chain
|
||||
@substitute_in_graph(itertools.chain, is_embedded_type=True) # type: ignore[arg-type]
|
||||
def chain(*iterables: Iterable[_T]) -> Iterator[_T]:
|
||||
for iterable in iterables:
|
||||
yield from iterable
|
||||
|
||||
|
||||
# Reference: https://docs.python.org/3/library/itertools.html#itertools.accumulate
|
||||
@substitute_in_graph(itertools.accumulate, is_embedded_type=True) # type: ignore[arg-type]
|
||||
def accumulate(
|
||||
iterable: Iterable[_T],
|
||||
func: Callable[[_T, _T], _T] | None = None,
|
||||
*,
|
||||
initial: _T | None = None,
|
||||
) -> Iterator[_T]:
|
||||
# call iter outside of the generator to match cypthon behavior
|
||||
iterator = iter(iterable)
|
||||
if func is None:
|
||||
func = operator.add
|
||||
|
||||
def _accumulate(iterator: Iterator[_T]) -> Iterator[_T]:
|
||||
total = initial
|
||||
if total is None:
|
||||
try:
|
||||
total = next(iterator)
|
||||
except StopIteration:
|
||||
return
|
||||
|
||||
yield total
|
||||
for element in iterator:
|
||||
total = func(total, element)
|
||||
yield total
|
||||
|
||||
return _accumulate(iterator)
|
||||
|
||||
|
||||
@substitute_in_graph(itertools.chain.from_iterable) # type: ignore[arg-type]
|
||||
def chain_from_iterable(iterable: Iterable[Iterable[_T]], /) -> Iterator[_T]:
|
||||
# previous version of this code was:
|
||||
# return itertools.chain(*iterable)
|
||||
# If iterable is an infinite generator, this will lead to infinite recursion
|
||||
for it in iterable:
|
||||
yield from it
|
||||
|
||||
|
||||
chain.from_iterable = chain_from_iterable # type: ignore[attr-defined]
|
||||
|
||||
|
||||
# Reference: https://docs.python.org/3/library/itertools.html#itertools.compress
|
||||
@substitute_in_graph(itertools.compress, is_embedded_type=True) # type: ignore[arg-type]
|
||||
def compress(data: Iterable[_T], selectors: Iterable[_U], /) -> Iterator[_T]:
|
||||
return (datum for datum, selector in zip(data, selectors) if selector)
|
||||
|
||||
|
||||
# Reference: https://docs.python.org/3/library/itertools.html#itertools.cycle
|
||||
@substitute_in_graph(itertools.cycle, is_embedded_type=True) # type: ignore[arg-type]
|
||||
def cycle(iterable: Iterable[_T]) -> Iterator[_T]:
|
||||
iterator = iter(iterable)
|
||||
|
||||
def _cycle(iterator: Iterator[_T]) -> Iterator[_T]:
|
||||
# pyrefly: ignore [implicit-any]
|
||||
saved = []
|
||||
for element in iterable:
|
||||
yield element
|
||||
saved.append(element)
|
||||
|
||||
while saved:
|
||||
for element in saved:
|
||||
yield element
|
||||
|
||||
return _cycle(iterator)
|
||||
|
||||
|
||||
# Reference: https://docs.python.org/3/library/itertools.html#itertools.dropwhile
|
||||
@substitute_in_graph(itertools.dropwhile, is_embedded_type=True) # type: ignore[arg-type]
|
||||
def dropwhile(predicate: _Predicate[_T], iterable: Iterable[_T], /) -> Iterator[_T]:
|
||||
# dropwhile(lambda x: x < 5, [1, 4, 6, 3, 8]) -> 6 3 8
|
||||
if not callable(predicate):
|
||||
raise TypeError(f"'{type(predicate).__name__}' object is not callable")
|
||||
|
||||
iterator = iter(iterable)
|
||||
for x in iterator:
|
||||
if not predicate(x):
|
||||
yield x
|
||||
break
|
||||
|
||||
yield from iterator
|
||||
|
||||
|
||||
# Reference: https://docs.python.org/3/library/itertools.html#itertools.takewhile
|
||||
@substitute_in_graph(itertools.takewhile, is_embedded_type=True) # type: ignore[arg-type]
|
||||
def takewhile(predicate: _Predicate[_T], iterable: Iterable[_T], /) -> Iterator[_T]:
|
||||
# takewhile(lambda x: x<5, [1,4,6,3,8]) → 1 4
|
||||
if not callable(predicate):
|
||||
raise TypeError(f"'{type(predicate).__name__}' object is not callable")
|
||||
|
||||
for x in iterable:
|
||||
if not predicate(x):
|
||||
break
|
||||
yield x
|
||||
|
||||
|
||||
@overload
|
||||
def starmap(
|
||||
function: Callable[[], _U],
|
||||
iterable: Iterable[tuple[()]],
|
||||
/,
|
||||
) -> itertools.starmap[_U]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def starmap(
|
||||
function: Callable[[_T], _U],
|
||||
iterable: Iterable[tuple[_T]],
|
||||
/,
|
||||
) -> itertools.starmap[_U]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def starmap(
|
||||
function: Callable[[_T, _T1], _U],
|
||||
iterable: Iterable[tuple[_T, _T1]],
|
||||
/,
|
||||
) -> itertools.starmap[_U]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def starmap(
|
||||
function: Callable[[_T, _T1, _T2], _U],
|
||||
iterable: Iterable[tuple[_T, _T1, _T2]],
|
||||
/,
|
||||
) -> itertools.starmap[_U]: ...
|
||||
|
||||
|
||||
# Reference: https://docs.python.org/3/library/itertools.html#itertools.starmap
|
||||
@substitute_in_graph(itertools.starmap, is_embedded_type=True) # type: ignore[arg-type]
|
||||
# pyrefly: ignore [implicit-any]
|
||||
def starmap(function: Callable[..., _T], iterable: Iterable, /) -> Iterable[_T]:
|
||||
# starmap(pow, [(2,5), (3,2), (10,3)]) → 32 9 1000
|
||||
if not callable(function):
|
||||
raise TypeError(f"'{type(function).__name__}' object is not callable")
|
||||
|
||||
for args in iterable:
|
||||
yield function(*args)
|
||||
|
||||
|
||||
@substitute_in_graph(itertools.filterfalse, is_embedded_type=True) # type: ignore[arg-type]
|
||||
def filterfalse(function: _Predicate[_T], iterable: Iterable[_T], /) -> Iterator[_T]:
|
||||
it = iter(iterable)
|
||||
if function is None:
|
||||
return filter(operator.not_, it)
|
||||
else:
|
||||
return filter(lambda x: not function(x), it)
|
||||
|
||||
|
||||
# Reference: https://docs.python.org/3/library/itertools.html#itertools.islice
|
||||
@substitute_in_graph(itertools.islice, is_embedded_type=True) # type: ignore[arg-type]
|
||||
def islice(iterable: Iterable[_T], /, *args: int | None) -> Iterator[_T]:
|
||||
s = slice(*args)
|
||||
start = 0 if s.start is None else s.start
|
||||
stop = s.stop
|
||||
step = 1 if s.step is None else s.step
|
||||
if start < 0 or (stop is not None and stop < 0) or step <= 0:
|
||||
raise ValueError(
|
||||
"Indices for islice() must be None or an integer: 0 <= x <= sys.maxsize.",
|
||||
)
|
||||
|
||||
if stop is None:
|
||||
# TODO: use indices = itertools.count() and merge implementation with the else branch
|
||||
# when we support infinite iterators
|
||||
next_i = start
|
||||
for i, element in enumerate(iterable):
|
||||
if i == next_i:
|
||||
yield element
|
||||
next_i += step
|
||||
else:
|
||||
indices = range(max(start, stop))
|
||||
next_i = start
|
||||
for i, element in zip(indices, iterable):
|
||||
if i == next_i:
|
||||
yield element
|
||||
next_i += step
|
||||
|
||||
|
||||
# Reference: https://docs.python.org/3/library/itertools.html#itertools.pairwise
|
||||
@substitute_in_graph(itertools.pairwise, is_embedded_type=True) # type: ignore[arg-type]
|
||||
def pairwise(iterable: Iterable[_T], /) -> Iterator[tuple[_T, _T]]:
|
||||
a = None
|
||||
first = True
|
||||
for b in iterable:
|
||||
if first:
|
||||
first = False
|
||||
else:
|
||||
yield a, b # type: ignore[misc]
|
||||
a = b
|
||||
|
||||
|
||||
# Reference: https://docs.python.org/3/library/itertools.html#itertools.tee
|
||||
@substitute_in_graph(itertools.tee)
|
||||
def tee(iterable: Iterable[_T], n: int = 2, /) -> tuple[Iterator[_T], ...]:
|
||||
iterator = iter(iterable)
|
||||
shared_link = [None, None]
|
||||
|
||||
def _tee(link) -> Iterator[_T]: # type: ignore[no-untyped-def]
|
||||
try:
|
||||
while True:
|
||||
if link[1] is None:
|
||||
link[0] = next(iterator)
|
||||
link[1] = [None, None]
|
||||
value, link = link
|
||||
yield value
|
||||
except StopIteration:
|
||||
return
|
||||
|
||||
return tuple(_tee(shared_link) for _ in range(n))
|
||||
|
||||
|
||||
@overload
|
||||
# pyrefly: ignore [inconsistent-overload]
|
||||
def zip_longest(
|
||||
iter1: Iterable[_T1],
|
||||
/,
|
||||
*,
|
||||
fillvalue: _U = ...,
|
||||
) -> Iterator[tuple[_T1]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
# pyrefly: ignore [inconsistent-overload]
|
||||
def zip_longest(
|
||||
iter1: Iterable[_T1],
|
||||
iter2: Iterable[_T2],
|
||||
/,
|
||||
) -> Iterator[tuple[_T1 | None, _T2 | None]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
# pyrefly: ignore [inconsistent-overload]
|
||||
def zip_longest(
|
||||
iter1: Iterable[_T1],
|
||||
iter2: Iterable[_T2],
|
||||
/,
|
||||
*,
|
||||
fillvalue: _U = ...,
|
||||
) -> Iterator[tuple[_T1 | _U, _T2 | _U]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
# pyrefly: ignore [inconsistent-overload]
|
||||
def zip_longest(
|
||||
iter1: Iterable[_T],
|
||||
iter2: Iterable[_T],
|
||||
iter3: Iterable[_T],
|
||||
/,
|
||||
*iterables: Iterable[_T],
|
||||
) -> Iterator[tuple[_T | None, ...]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
# pyrefly: ignore [inconsistent-overload]
|
||||
def zip_longest(
|
||||
iter1: Iterable[_T],
|
||||
iter2: Iterable[_T],
|
||||
iter3: Iterable[_T],
|
||||
/,
|
||||
*iterables: Iterable[_T],
|
||||
fillvalue: _U = ...,
|
||||
) -> Iterator[tuple[_T | _U, ...]]: ...
|
||||
|
||||
|
||||
# Reference: https://docs.python.org/3/library/itertools.html#itertools.zip_longest
|
||||
@substitute_in_graph(itertools.zip_longest, is_embedded_type=True) # type: ignore[arg-type,misc]
|
||||
def zip_longest(
|
||||
*iterables: Iterable[_T],
|
||||
fillvalue: _U = None, # type: ignore[assignment]
|
||||
) -> Iterator[tuple[_T | _U, ...]]:
|
||||
# zip_longest('ABCD', 'xy', fillvalue='-') -> Ax By C- D-
|
||||
|
||||
iterators = list(map(iter, iterables))
|
||||
num_active = len(iterators)
|
||||
if not num_active:
|
||||
return
|
||||
|
||||
while True:
|
||||
values = []
|
||||
for i, iterator in enumerate(iterators):
|
||||
try:
|
||||
value = next(iterator)
|
||||
except StopIteration:
|
||||
num_active -= 1
|
||||
if not num_active:
|
||||
return
|
||||
iterators[i] = itertools.repeat(fillvalue) # type: ignore[arg-type]
|
||||
value = fillvalue # type: ignore[assignment]
|
||||
values.append(value)
|
||||
yield tuple(values)
|
||||
@@ -0,0 +1,48 @@
|
||||
# Used to load and initialize polyfill handlers when importing torch._dynamo
|
||||
# Please add a new import when adding a new polyfill module.
|
||||
|
||||
import importlib
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import torch.utils._pytree as python_pytree
|
||||
|
||||
from .. import polyfills, trace_rules
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import ModuleType
|
||||
|
||||
|
||||
# See also the TYPE_CHECKING block in torch/_dynamo/polyfills/__init__.py
|
||||
POLYFILLED_MODULE_NAMES: tuple[str, ...] = (
|
||||
"_collections",
|
||||
"builtins",
|
||||
"copy",
|
||||
"functools",
|
||||
"itertools",
|
||||
"operator",
|
||||
"os",
|
||||
"struct",
|
||||
"sys",
|
||||
"fx",
|
||||
"tensor",
|
||||
"torch_c_nn",
|
||||
"traceback",
|
||||
)
|
||||
if python_pytree._cxx_pytree_dynamo_traceable:
|
||||
POLYFILLED_MODULE_NAMES += ("pytree",)
|
||||
|
||||
POLYFILLED_MODULES: tuple["ModuleType", ...] = tuple(
|
||||
importlib.import_module(f".{submodule}", package=polyfills.__name__)
|
||||
for submodule in POLYFILLED_MODULE_NAMES
|
||||
)
|
||||
|
||||
|
||||
# Unregister the builtin functions from _builtin_function_ids to let them to be
|
||||
# dispatched with the appropriate VariableTracker type. Otherwise, they will be
|
||||
# dispatched with BuiltinVariable if present in _builtin_function_ids.
|
||||
for polyfill_module in POLYFILLED_MODULES:
|
||||
for polyfill_name in polyfill_module.__all__:
|
||||
polyfill_handler = getattr(polyfill_module, polyfill_name)
|
||||
original_fn = polyfill_handler.__torch_dynamo_original__
|
||||
trace_rules._builtin_function_ids.remove(id(original_fn))
|
||||
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
Python polyfills for operator
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import operator
|
||||
from typing import Any, overload, TYPE_CHECKING, TypeVar
|
||||
from typing_extensions import TypeVarTuple, Unpack
|
||||
|
||||
from ..decorators import substitute_in_graph
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Iterable
|
||||
|
||||
|
||||
# Most unary and binary operators are handled by BuiltinVariable (e.g., `pos`, `add`)
|
||||
__all__ = ["attrgetter", "itemgetter", "methodcaller", "countOf"]
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_T1 = TypeVar("_T1")
|
||||
_T2 = TypeVar("_T2")
|
||||
_Ts = TypeVarTuple("_Ts")
|
||||
_U = TypeVar("_U")
|
||||
_U1 = TypeVar("_U1")
|
||||
_U2 = TypeVar("_U2")
|
||||
_Us = TypeVarTuple("_Us")
|
||||
|
||||
|
||||
@overload
|
||||
# pyrefly: ignore [inconsistent-overload]
|
||||
def attrgetter(attr: str, /) -> Callable[[Any], _U]: ...
|
||||
|
||||
|
||||
@overload
|
||||
# pyrefly: ignore [inconsistent-overload]
|
||||
def attrgetter(
|
||||
attr1: str, attr2: str, /, *attrs: str
|
||||
) -> Callable[[Any], tuple[_U1, _U2, Unpack[_Us]]]: ...
|
||||
|
||||
|
||||
# Reference: https://docs.python.org/3/library/operator.html#operator.attrgetter
|
||||
@substitute_in_graph(operator.attrgetter, is_embedded_type=True) # type: ignore[arg-type,misc]
|
||||
def attrgetter(*attrs: str) -> Callable[[Any], Any | tuple[Any, ...]]:
|
||||
if len(attrs) == 0:
|
||||
raise TypeError("attrgetter expected 1 argument, got 0")
|
||||
|
||||
if any(not isinstance(attr, str) for attr in attrs):
|
||||
raise TypeError("attribute name must be a string")
|
||||
|
||||
def resolve_attr(obj: Any, attr: str) -> Any:
|
||||
for name in attr.split("."):
|
||||
obj = getattr(obj, name)
|
||||
return obj
|
||||
|
||||
if len(attrs) == 1:
|
||||
attr = attrs[0]
|
||||
|
||||
def getter(obj: Any) -> Any:
|
||||
return resolve_attr(obj, attr)
|
||||
|
||||
else:
|
||||
|
||||
def getter(obj: Any) -> tuple[Any, ...]: # type: ignore[misc]
|
||||
return tuple(resolve_attr(obj, attr) for attr in attrs)
|
||||
|
||||
return getter
|
||||
|
||||
|
||||
@overload
|
||||
# pyrefly: ignore [inconsistent-overload]
|
||||
def itemgetter(item: _T, /) -> Callable[[Any], _U]: ...
|
||||
|
||||
|
||||
@overload
|
||||
# pyrefly: ignore [inconsistent-overload]
|
||||
def itemgetter(
|
||||
item1: _T1, item2: _T2, /, *items: Unpack[_Ts]
|
||||
) -> Callable[[Any], tuple[_U1, _U2, Unpack[_Us]]]: ...
|
||||
|
||||
|
||||
# Reference: https://docs.python.org/3/library/operator.html#operator.itemgetter
|
||||
@substitute_in_graph(operator.itemgetter, is_embedded_type=True) # type: ignore[arg-type,misc]
|
||||
def itemgetter(*items: Any) -> Callable[[Any], Any | tuple[Any, ...]]:
|
||||
if len(items) == 0:
|
||||
raise TypeError("itemgetter expected 1 argument, got 0")
|
||||
|
||||
if len(items) == 1:
|
||||
item = items[0]
|
||||
|
||||
def getter(obj: Any) -> Any:
|
||||
return obj[item]
|
||||
|
||||
else:
|
||||
|
||||
def getter(obj: Any) -> tuple[Any, ...]: # type: ignore[misc]
|
||||
return tuple(obj[item] for item in items)
|
||||
|
||||
return getter
|
||||
|
||||
|
||||
# Reference: https://docs.python.org/3/library/operator.html#operator.methodcaller
|
||||
@substitute_in_graph(operator.methodcaller, is_embedded_type=True) # type: ignore[arg-type]
|
||||
def methodcaller(name: str, /, *args: Any, **kwargs: Any) -> Callable[[Any], Any]:
|
||||
if not isinstance(name, str):
|
||||
raise TypeError("method name must be a string")
|
||||
|
||||
def caller(obj: Any) -> Any:
|
||||
return getattr(obj, name)(*args, **kwargs)
|
||||
|
||||
return caller
|
||||
|
||||
|
||||
# Reference: https://docs.python.org/3/library/operator.html#operator.countOf
|
||||
@substitute_in_graph(operator.countOf, can_constant_fold_through=True) # type: ignore[arg-type,misc]
|
||||
def countOf(a: Iterable[_T], b: _T, /) -> int:
|
||||
return sum(it is b or it == b for it in a)
|
||||
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Python polyfills for os
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import AnyStr
|
||||
|
||||
from ..decorators import substitute_in_graph
|
||||
|
||||
|
||||
__all__ = ["fspath"]
|
||||
|
||||
|
||||
# Copied from os.py in the standard library
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
@substitute_in_graph(os.fspath, can_constant_fold_through=True)
|
||||
def fspath(path: AnyStr | os.PathLike[AnyStr]) -> AnyStr:
|
||||
if isinstance(path, (str, bytes)):
|
||||
return path
|
||||
|
||||
path_type = type(path)
|
||||
try:
|
||||
path_repr = path_type.__fspath__(path) # type: ignore[arg-type]
|
||||
except AttributeError:
|
||||
if hasattr(path_type, "__fspath__"):
|
||||
raise
|
||||
raise TypeError(
|
||||
f"expected str, bytes or os.PathLike object, not {path_type.__name__}",
|
||||
) from None
|
||||
if isinstance(path_repr, (str, bytes)):
|
||||
return path_repr # type: ignore[return-value]
|
||||
raise TypeError(
|
||||
f"expected {path_type.__name__}.__fspath__() to return str or bytes, "
|
||||
f"not {type(path_repr).__name__}",
|
||||
)
|
||||
@@ -0,0 +1,759 @@
|
||||
# Owner(s): ["module: pytree"]
|
||||
|
||||
"""
|
||||
Python polyfills for torch.utils.pytree
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, TYPE_CHECKING, TypeVar
|
||||
|
||||
import optree
|
||||
import optree._C
|
||||
import optree.utils
|
||||
from optree import (
|
||||
is_namedtuple,
|
||||
is_namedtuple_class,
|
||||
is_namedtuple_instance,
|
||||
is_structseq,
|
||||
is_structseq_class,
|
||||
is_structseq_instance,
|
||||
namedtuple_fields,
|
||||
structseq_fields,
|
||||
)
|
||||
|
||||
import torch.utils._cxx_pytree as cxx_pytree # noqa: F401
|
||||
import torch.utils._pytree as python_pytree
|
||||
from torch.utils._pytree import BUILTIN_TYPES, STANDARD_DICT_TYPES
|
||||
|
||||
from ..decorators import substitute_in_graph
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import builtins
|
||||
from collections.abc import Callable, Iterable, Mapping
|
||||
from typing_extensions import Self, TypeIs
|
||||
|
||||
from torch.utils._cxx_pytree import PyTree
|
||||
|
||||
|
||||
__all__ = [
|
||||
"is_namedtuple",
|
||||
"is_namedtuple_class",
|
||||
"is_namedtuple_instance",
|
||||
"is_structseq",
|
||||
"is_structseq_class",
|
||||
"is_structseq_instance",
|
||||
"namedtuple_fields",
|
||||
"structseq_fields",
|
||||
"treespec_leaf",
|
||||
"treespec_tuple",
|
||||
"treespec_dict",
|
||||
"tree_is_leaf",
|
||||
"tree_iter",
|
||||
"tree_leaves",
|
||||
"tree_flatten",
|
||||
"tree_flatten_with_path",
|
||||
"tree_structure",
|
||||
"tree_unflatten",
|
||||
]
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_KT = TypeVar("_KT")
|
||||
_VT = TypeVar("_VT")
|
||||
|
||||
|
||||
@substitute_in_graph(
|
||||
optree._C.is_dict_insertion_ordered,
|
||||
can_constant_fold_through=True,
|
||||
)
|
||||
def _(*args: Any, **kwargs: Any) -> bool:
|
||||
# In namespace 'torch', the dictionary is always traversed in insertion order.
|
||||
# This function returns True.
|
||||
raise ValueError(
|
||||
"Should not be called directly "
|
||||
"because the original function will be called in the constant fold path."
|
||||
)
|
||||
|
||||
|
||||
__name = ""
|
||||
for __name, __func in (
|
||||
("is_namedtuple", is_namedtuple),
|
||||
("is_namedtuple_class", is_namedtuple_class),
|
||||
("is_namedtuple_instance", is_namedtuple_instance),
|
||||
("is_structseq", is_structseq),
|
||||
("is_structseq_class", is_structseq_class),
|
||||
("is_structseq_instance", is_structseq_instance),
|
||||
("namedtuple_fields", namedtuple_fields),
|
||||
("structseq_fields", structseq_fields),
|
||||
):
|
||||
globals()[__name] = substitute_in_graph(
|
||||
__func, # type: ignore[arg-type]
|
||||
can_constant_fold_through=True,
|
||||
)(__func.__python_implementation__) # type: ignore[attr-defined]
|
||||
del __func
|
||||
del __name
|
||||
|
||||
|
||||
@substitute_in_graph(optree.tree_is_leaf, can_constant_fold_through=True) # type: ignore[arg-type]
|
||||
def tree_is_leaf(
|
||||
tree: PyTree,
|
||||
/,
|
||||
is_leaf: Callable[[PyTree], bool] | None = None,
|
||||
*,
|
||||
none_is_leaf: bool = False,
|
||||
namespace: str = "",
|
||||
) -> bool:
|
||||
if (tree is None and none_is_leaf) or (is_leaf is not None and is_leaf(tree)):
|
||||
return True
|
||||
if optree.register_pytree_node.get(type(tree), namespace=namespace) is None:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@substitute_in_graph(optree.tree_iter, can_constant_fold_through=False) # type: ignore[arg-type]
|
||||
def tree_iter(
|
||||
tree: PyTree,
|
||||
/,
|
||||
is_leaf: Callable[[PyTree], bool] | None = None,
|
||||
*,
|
||||
none_is_leaf: bool = False,
|
||||
namespace: str = "",
|
||||
) -> Iterable[Any]:
|
||||
stack = [tree]
|
||||
while stack:
|
||||
node = stack.pop()
|
||||
if tree_is_leaf(
|
||||
node,
|
||||
is_leaf=is_leaf,
|
||||
none_is_leaf=none_is_leaf,
|
||||
namespace=namespace,
|
||||
):
|
||||
yield node
|
||||
continue
|
||||
|
||||
children, *_ = optree.tree_flatten_one_level(
|
||||
node,
|
||||
is_leaf=is_leaf,
|
||||
none_is_leaf=none_is_leaf,
|
||||
namespace=namespace,
|
||||
)
|
||||
stack.extend(reversed(children))
|
||||
|
||||
|
||||
@substitute_in_graph(optree.tree_leaves, can_constant_fold_through=True) # type: ignore[arg-type]
|
||||
def tree_leaves(
|
||||
tree: PyTree,
|
||||
/,
|
||||
is_leaf: Callable[[PyTree], bool] | None = None,
|
||||
*,
|
||||
none_is_leaf: bool = False,
|
||||
namespace: str = "",
|
||||
) -> list[Any]:
|
||||
return list(
|
||||
tree_iter(
|
||||
tree,
|
||||
is_leaf=is_leaf,
|
||||
none_is_leaf=none_is_leaf,
|
||||
namespace=namespace,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class _Asterisk(str):
|
||||
__slots__ = ()
|
||||
|
||||
def __new__(cls) -> Self:
|
||||
return super().__new__(cls, "*")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "*" # no quotes
|
||||
|
||||
|
||||
_asterisk = _Asterisk()
|
||||
del _Asterisk
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PyTreeSpec:
|
||||
"""Analog for :class:`optree.PyTreeSpec` in Python."""
|
||||
|
||||
_children: tuple[PyTreeSpec, ...]
|
||||
_type: builtins.type | None
|
||||
_metadata: Any
|
||||
_entries: tuple[Any, ...]
|
||||
_unflatten_func: Callable[[Any | None, Iterable[PyTree]], PyTree] | None
|
||||
none_is_leaf: bool
|
||||
namespace: str
|
||||
|
||||
num_nodes: int = field(init=False)
|
||||
num_leaves: int = field(init=False)
|
||||
num_children: int = field(init=False)
|
||||
|
||||
def __post_init__(self, /) -> None:
|
||||
if self._type is None:
|
||||
assert len(self._children) == 0
|
||||
assert self._metadata is None
|
||||
assert self._entries == ()
|
||||
assert self._unflatten_func is None
|
||||
num_nodes = 1
|
||||
num_leaves = 1
|
||||
num_children = 0
|
||||
else:
|
||||
assert callable(self._unflatten_func)
|
||||
num_nodes = 1
|
||||
num_leaves = 0
|
||||
for child in self._children:
|
||||
num_nodes += child.num_nodes
|
||||
num_leaves += child.num_leaves
|
||||
num_children = len(self._children)
|
||||
|
||||
object.__setattr__(self, "num_nodes", num_nodes)
|
||||
object.__setattr__(self, "num_leaves", num_leaves)
|
||||
object.__setattr__(self, "num_children", num_children)
|
||||
|
||||
def __repr__(self, /) -> str:
|
||||
def helper(treespec: PyTreeSpec) -> str:
|
||||
if treespec.is_leaf():
|
||||
assert treespec.type is None
|
||||
return _asterisk
|
||||
|
||||
assert treespec.type is not None
|
||||
assert callable(treespec._unflatten_func)
|
||||
children_representations = [
|
||||
helper(subspec) for subspec in treespec._children
|
||||
]
|
||||
if (
|
||||
treespec.type in BUILTIN_TYPES
|
||||
or (treespec.type is type(None) and not self.none_is_leaf)
|
||||
or optree.is_namedtuple_class(treespec.type)
|
||||
or optree.is_structseq_class(treespec.type)
|
||||
):
|
||||
return treespec._unflatten_func(
|
||||
treespec._metadata,
|
||||
children_representations,
|
||||
)
|
||||
return (
|
||||
f"CustomTreeNode({treespec.type.__name__}[{treespec._metadata!r}], "
|
||||
f"[{', '.join(children_representations)}])"
|
||||
)
|
||||
|
||||
inner = [
|
||||
str(helper(self)),
|
||||
*(["NoneIsLeaf"] if self.none_is_leaf else []),
|
||||
f"namespace={self.namespace!r}",
|
||||
]
|
||||
return f"PyTreeSpec({', '.join(inner)})"
|
||||
|
||||
def __len__(self, /) -> int:
|
||||
return self.num_leaves
|
||||
|
||||
@property
|
||||
def type(self, /) -> builtins.type | None:
|
||||
return self._type
|
||||
|
||||
def is_leaf(self, /) -> bool:
|
||||
return self.num_nodes == 1 and self.num_leaves == 1
|
||||
|
||||
def paths(self, /) -> list[tuple[Any, ...]]:
|
||||
def helper(treespec: PyTreeSpec, path_prefix: list[Any]) -> None:
|
||||
if treespec.is_leaf():
|
||||
paths.append(path_prefix)
|
||||
return
|
||||
|
||||
for entry, subspec in zip(
|
||||
treespec._entries,
|
||||
treespec._children,
|
||||
strict=True,
|
||||
):
|
||||
helper(subspec, path_prefix + [entry])
|
||||
|
||||
paths: list[list[Any]] = []
|
||||
helper(self, [])
|
||||
return [tuple(path) for path in paths]
|
||||
|
||||
def accessors(self, /) -> list[optree.PyTreeAccessor]:
|
||||
def helper(
|
||||
treespec: PyTreeSpec,
|
||||
entry_path_prefix: list[optree.PyTreeEntry],
|
||||
) -> None:
|
||||
if treespec.is_leaf():
|
||||
entry_paths.append(entry_path_prefix)
|
||||
return
|
||||
|
||||
node_type = treespec.type
|
||||
assert node_type is not None
|
||||
handler = optree.register_pytree_node.get(
|
||||
node_type, namespace=treespec.namespace
|
||||
)
|
||||
assert handler is not None
|
||||
kind: optree.PyTreeKind = handler.kind
|
||||
path_entry_type: type[optree.PyTreeEntry] = handler.path_entry_type
|
||||
|
||||
for entry, subspec in zip(
|
||||
treespec._entries,
|
||||
treespec._children,
|
||||
strict=True,
|
||||
):
|
||||
helper(
|
||||
subspec,
|
||||
entry_path_prefix + [path_entry_type(entry, node_type, kind)],
|
||||
)
|
||||
|
||||
entry_paths: list[list[optree.PyTreeEntry]] = []
|
||||
helper(self, [])
|
||||
return [optree.PyTreeAccessor(path) for path in entry_paths]
|
||||
|
||||
def children(self, /) -> list[PyTreeSpec]:
|
||||
return list(self._children)
|
||||
|
||||
def child(self, index: int, /) -> PyTreeSpec:
|
||||
return self._children[index]
|
||||
|
||||
def entries(self, /) -> list[Any]:
|
||||
return list(self._entries)
|
||||
|
||||
def entry(self, index: int, /) -> Any:
|
||||
return self._entries[index]
|
||||
|
||||
def flatten_up_to(self, tree: PyTree, /) -> list[PyTree]:
|
||||
def helper(
|
||||
treespec: PyTreeSpec,
|
||||
node: PyTree,
|
||||
subtrees: list[PyTree],
|
||||
) -> None:
|
||||
if treespec.is_leaf():
|
||||
subtrees.append(node)
|
||||
return
|
||||
|
||||
node_type = type(node)
|
||||
if treespec.type not in BUILTIN_TYPES:
|
||||
# Always require custom node types to match exactly
|
||||
if node_type != treespec.type:
|
||||
raise ValueError(
|
||||
f"Type mismatch; "
|
||||
f"expected {treespec.type!r}, but got {node_type!r}.",
|
||||
)
|
||||
|
||||
children, metadata, *_ = optree.tree_flatten_one_level(
|
||||
node,
|
||||
none_is_leaf=self.none_is_leaf,
|
||||
namespace=self.namespace,
|
||||
)
|
||||
if len(children) != treespec.num_children:
|
||||
raise ValueError(
|
||||
f"Node arity mismatch; "
|
||||
f"expected {treespec.num_children}, but got {len(children)}.",
|
||||
)
|
||||
if metadata != treespec._metadata:
|
||||
raise ValueError(
|
||||
f"Node context mismatch for custom node type {treespec.type!r}.",
|
||||
)
|
||||
else:
|
||||
# For builtin dictionary types, we allow some flexibility
|
||||
# Otherwise, we require exact matches
|
||||
both_standard_dict = (
|
||||
treespec.type in STANDARD_DICT_TYPES
|
||||
and node_type in STANDARD_DICT_TYPES
|
||||
)
|
||||
if not both_standard_dict and node_type != treespec.type:
|
||||
raise ValueError(
|
||||
f"Node type mismatch; "
|
||||
f"expected {treespec.type!r}, but got {node_type!r}.",
|
||||
)
|
||||
if len(node) != treespec.num_children:
|
||||
raise ValueError(
|
||||
f"Node arity mismatch; "
|
||||
f"expected {treespec.num_children}, but got {len(node)}.",
|
||||
)
|
||||
|
||||
if both_standard_dict:
|
||||
# dictionary types are compatible with each other
|
||||
expected_keys = treespec.entries()
|
||||
got_key_set = set(node)
|
||||
expected_key_set = set(expected_keys)
|
||||
if got_key_set != expected_key_set:
|
||||
missing_keys = expected_key_set.difference(got_key_set)
|
||||
extra_keys = got_key_set.difference(expected_key_set)
|
||||
message = ""
|
||||
if missing_keys:
|
||||
message += f"; missing key(s): {missing_keys}"
|
||||
if extra_keys:
|
||||
message += f"; extra key(s): {extra_keys}"
|
||||
raise ValueError(f"Node keys mismatch{message}.")
|
||||
children = [node[key] for key in expected_keys]
|
||||
else:
|
||||
# node_type is treespec.type
|
||||
children, metadata, *_ = optree.tree_flatten_one_level(
|
||||
node,
|
||||
none_is_leaf=self.none_is_leaf,
|
||||
namespace=self.namespace,
|
||||
)
|
||||
if (
|
||||
node_type is not deque # ignore mismatch of `maxlen` for deque
|
||||
) and metadata != treespec._metadata:
|
||||
raise ValueError(
|
||||
f"Node metadata mismatch for node type {treespec.type!r}; "
|
||||
f"expected {treespec._metadata!r}, but got {metadata!r}.", # namedtuple type mismatch
|
||||
)
|
||||
|
||||
for subtree, subspec in zip(children, treespec._children, strict=True):
|
||||
helper(subspec, subtree, subtrees)
|
||||
|
||||
subtrees: list[PyTree] = []
|
||||
helper(self, tree, subtrees)
|
||||
return subtrees
|
||||
|
||||
def unflatten(self, leaves: Iterable[Any], /) -> PyTree:
|
||||
if not isinstance(leaves, (list, tuple)):
|
||||
leaves = list(leaves)
|
||||
if len(leaves) != self.num_leaves:
|
||||
raise ValueError(
|
||||
f"treespec.unflatten(leaves): `leaves` has length {len(leaves)} "
|
||||
f"but the spec refers to a pytree that holds {self.num_leaves} "
|
||||
f"items ({self}).",
|
||||
)
|
||||
if self.is_leaf():
|
||||
return leaves[0]
|
||||
|
||||
# Recursively unflatten the children
|
||||
start = 0
|
||||
end = 0
|
||||
subtrees = []
|
||||
for subspec in self._children:
|
||||
end += subspec.num_leaves
|
||||
subtrees.append(subspec.unflatten(leaves[start:end]))
|
||||
start = end
|
||||
|
||||
assert callable(self._unflatten_func)
|
||||
return self._unflatten_func(self._metadata, subtrees)
|
||||
|
||||
|
||||
def _is_pytreespec_instance(obj: Any, /) -> TypeIs[PyTreeSpec | python_pytree.TreeSpec]:
|
||||
return isinstance(obj, (PyTreeSpec, python_pytree.TreeSpec))
|
||||
|
||||
|
||||
@substitute_in_graph( # type: ignore[arg-type]
|
||||
optree.treespec_leaf,
|
||||
# We need to disable constant folding here because we want the function to reference the
|
||||
# PyTreeSpec class defined above, not the one in the C++ module.
|
||||
can_constant_fold_through=False,
|
||||
)
|
||||
def treespec_leaf(
|
||||
*,
|
||||
none_is_leaf: bool = False,
|
||||
namespace: str = "", # unused
|
||||
) -> PyTreeSpec:
|
||||
return PyTreeSpec(
|
||||
(),
|
||||
None,
|
||||
None,
|
||||
(),
|
||||
None,
|
||||
none_is_leaf=none_is_leaf,
|
||||
namespace="",
|
||||
)
|
||||
|
||||
|
||||
@substitute_in_graph( # type: ignore[arg-type]
|
||||
optree.treespec_tuple,
|
||||
# We need to disable constant folding here because we want the function to reference the
|
||||
# PyTreeSpec class defined above, not the one in the C++ module.
|
||||
can_constant_fold_through=False,
|
||||
)
|
||||
def treespec_tuple(
|
||||
iterable: Iterable[PyTreeSpec] = (),
|
||||
/,
|
||||
*,
|
||||
none_is_leaf: bool = False,
|
||||
namespace: str = "",
|
||||
) -> PyTreeSpec:
|
||||
children = tuple(iterable)
|
||||
if any(not _is_pytreespec_instance(child) for child in children):
|
||||
raise ValueError(f"Expected a tuple of PyTreeSpecs, got: {children!r}.")
|
||||
if any(child.none_is_leaf != none_is_leaf for child in children):
|
||||
raise ValueError(
|
||||
"All children PyTreeSpecs must have the same `none_is_leaf` value "
|
||||
f"as the parent; expected {none_is_leaf}, got: {children!r}.",
|
||||
)
|
||||
if any(child.namespace not in (namespace, "") for child in children):
|
||||
raise ValueError(
|
||||
"All children PyTreeSpecs must have the same `namespace` value "
|
||||
f"as the parent; expected {namespace!r}, got: {children!r}.",
|
||||
)
|
||||
handler = optree.register_pytree_node.get(tuple, namespace=namespace)
|
||||
assert handler is not None
|
||||
return PyTreeSpec(
|
||||
tuple(children),
|
||||
tuple,
|
||||
None,
|
||||
tuple(range(len(children))),
|
||||
handler.unflatten_func,
|
||||
none_is_leaf=none_is_leaf,
|
||||
namespace=namespace,
|
||||
)
|
||||
|
||||
|
||||
@substitute_in_graph( # type: ignore[arg-type]
|
||||
optree.treespec_dict,
|
||||
# We need to disable constant folding here because we want the function to reference the
|
||||
# PyTreeSpec class defined above, not the one in the C++ module.
|
||||
can_constant_fold_through=False,
|
||||
)
|
||||
def treespec_dict(
|
||||
mapping: Mapping[Any, PyTreeSpec] | Iterable[tuple[Any, PyTreeSpec]] = (),
|
||||
/,
|
||||
*,
|
||||
none_is_leaf: bool = False,
|
||||
namespace: str = "",
|
||||
**kwargs: PyTreeSpec,
|
||||
) -> PyTreeSpec:
|
||||
dct = dict(mapping, **kwargs)
|
||||
if any(not _is_pytreespec_instance(child) for child in dct.values()):
|
||||
raise ValueError(f"Expected a dictionary of TreeSpecs, got: {dct!r}.")
|
||||
if any(child.none_is_leaf != none_is_leaf for child in dct.values()):
|
||||
raise ValueError(
|
||||
"All children PyTreeSpecs must have the same `none_is_leaf` value "
|
||||
f"as the parent; expected {none_is_leaf}, got: {dct!r}.",
|
||||
)
|
||||
if any(child.namespace not in (namespace, "") for child in dct.values()):
|
||||
raise ValueError(
|
||||
"All children PyTreeSpecs must have the same `namespace` value "
|
||||
f"as the parent; expected {namespace!r}, got: {dct!r}.",
|
||||
)
|
||||
|
||||
(
|
||||
children,
|
||||
metadata,
|
||||
entries,
|
||||
unflatten_func,
|
||||
) = optree.tree_flatten_one_level( # type: ignore[assignment,var-annotated]
|
||||
dct, # type: ignore[arg-type]
|
||||
none_is_leaf=none_is_leaf,
|
||||
namespace=namespace,
|
||||
)
|
||||
return PyTreeSpec(
|
||||
tuple(children), # type: ignore[arg-type]
|
||||
dict,
|
||||
metadata,
|
||||
entries,
|
||||
unflatten_func, # type: ignore[arg-type]
|
||||
none_is_leaf=none_is_leaf,
|
||||
namespace=namespace,
|
||||
)
|
||||
|
||||
|
||||
@substitute_in_graph( # type: ignore[arg-type]
|
||||
optree.tree_flatten,
|
||||
# We need to disable constant folding here because we want the function to reference the
|
||||
# PyTreeSpec class defined above, not the one in the C++ module.
|
||||
can_constant_fold_through=False,
|
||||
)
|
||||
def tree_flatten(
|
||||
tree: PyTree,
|
||||
/,
|
||||
is_leaf: Callable[[PyTree], bool] | None = None,
|
||||
*,
|
||||
none_is_leaf: bool = False,
|
||||
namespace: str = "",
|
||||
) -> tuple[list[Any], PyTreeSpec]:
|
||||
def helper(node: PyTree, leaves: list[Any]) -> PyTreeSpec:
|
||||
if tree_is_leaf(
|
||||
node,
|
||||
is_leaf=is_leaf,
|
||||
none_is_leaf=none_is_leaf,
|
||||
namespace=namespace,
|
||||
):
|
||||
leaves.append(node)
|
||||
return PyTreeSpec(
|
||||
(),
|
||||
None,
|
||||
None,
|
||||
(),
|
||||
None,
|
||||
none_is_leaf=none_is_leaf,
|
||||
namespace=namespace,
|
||||
)
|
||||
|
||||
(
|
||||
children,
|
||||
metadata,
|
||||
entries,
|
||||
unflatten_func,
|
||||
) = optree.tree_flatten_one_level(
|
||||
node,
|
||||
is_leaf=is_leaf,
|
||||
none_is_leaf=none_is_leaf,
|
||||
namespace=namespace,
|
||||
)
|
||||
|
||||
# Recursively flatten the children
|
||||
subspecs = tuple(helper(child, leaves) for child in children)
|
||||
return PyTreeSpec(
|
||||
subspecs,
|
||||
type(node),
|
||||
metadata,
|
||||
entries,
|
||||
unflatten_func, # type: ignore[arg-type]
|
||||
none_is_leaf=none_is_leaf,
|
||||
namespace=namespace,
|
||||
) # type: ignore[arg-type]
|
||||
|
||||
leaves: list[Any] = []
|
||||
treespec = helper(tree, leaves)
|
||||
return leaves, treespec
|
||||
|
||||
|
||||
@substitute_in_graph( # type: ignore[arg-type]
|
||||
optree._C.flatten,
|
||||
# We need to disable constant folding here because we want the function to reference the
|
||||
# PyTreeSpec class defined above, not the one in the C++ module.
|
||||
can_constant_fold_through=False,
|
||||
)
|
||||
def _C_flatten(
|
||||
tree: PyTree,
|
||||
/,
|
||||
leaf_predicate: Callable[[PyTree], bool] | None = None,
|
||||
none_is_leaf: bool = False,
|
||||
namespace: str = "",
|
||||
) -> tuple[list[Any], PyTreeSpec]:
|
||||
return tree_flatten( # type: ignore[return-value]
|
||||
tree,
|
||||
is_leaf=leaf_predicate,
|
||||
none_is_leaf=none_is_leaf,
|
||||
namespace=namespace,
|
||||
)
|
||||
|
||||
|
||||
@substitute_in_graph( # type: ignore[arg-type]
|
||||
optree.tree_flatten_with_path,
|
||||
# We need to disable constant folding here because we want the function to reference the
|
||||
# PyTreeSpec class defined above, not the one in the C++ module.
|
||||
can_constant_fold_through=False,
|
||||
)
|
||||
def tree_flatten_with_path(
|
||||
tree: PyTree,
|
||||
/,
|
||||
is_leaf: Callable[[PyTree], bool] | None = None,
|
||||
*,
|
||||
none_is_leaf: bool = False,
|
||||
namespace: str = "",
|
||||
) -> tuple[list[tuple[Any, ...]], list[Any], PyTreeSpec]:
|
||||
leaves, treespec = tree_flatten(
|
||||
tree,
|
||||
is_leaf=is_leaf,
|
||||
none_is_leaf=none_is_leaf,
|
||||
namespace=namespace,
|
||||
)
|
||||
return treespec.paths(), leaves, treespec # type: ignore[return-value]
|
||||
|
||||
|
||||
@substitute_in_graph( # type: ignore[arg-type]
|
||||
optree._C.flatten_with_path,
|
||||
# We need to disable constant folding here because we want the function to reference the
|
||||
# PyTreeSpec class defined above, not the one in the C++ module.
|
||||
can_constant_fold_through=False,
|
||||
)
|
||||
def _C_flatten_with_path(
|
||||
tree: PyTree,
|
||||
/,
|
||||
leaf_predicate: Callable[[PyTree], bool] | None = None,
|
||||
none_is_leaf: bool = False,
|
||||
namespace: str = "",
|
||||
) -> tuple[list[tuple[Any, ...]], list[Any], PyTreeSpec]:
|
||||
return tree_flatten_with_path( # type: ignore[return-value]
|
||||
tree,
|
||||
is_leaf=leaf_predicate,
|
||||
none_is_leaf=none_is_leaf,
|
||||
namespace=namespace,
|
||||
)
|
||||
|
||||
|
||||
@substitute_in_graph( # type: ignore[arg-type]
|
||||
optree.tree_structure,
|
||||
# We need to disable constant folding here because we want the function to reference the
|
||||
# PyTreeSpec class defined above, not the one in the C++ module.
|
||||
can_constant_fold_through=False,
|
||||
)
|
||||
def tree_structure(
|
||||
tree: PyTree,
|
||||
/,
|
||||
is_leaf: Callable[[PyTree], bool] | None = None,
|
||||
*,
|
||||
none_is_leaf: bool = False,
|
||||
namespace: str = "",
|
||||
) -> PyTreeSpec:
|
||||
return tree_flatten( # type: ignore[return-value]
|
||||
tree,
|
||||
is_leaf=is_leaf,
|
||||
none_is_leaf=none_is_leaf,
|
||||
namespace=namespace,
|
||||
)[1]
|
||||
|
||||
|
||||
@substitute_in_graph( # type: ignore[arg-type]
|
||||
optree.tree_unflatten,
|
||||
# We need to disable constant folding here because we want the function to reference the
|
||||
# PyTreeSpec class defined above, not the one in the C++ module.
|
||||
can_constant_fold_through=False,
|
||||
)
|
||||
def tree_unflatten(treespec: PyTreeSpec, leaves: Iterable[Any]) -> PyTree:
|
||||
if not _is_pytreespec_instance(treespec):
|
||||
raise TypeError(
|
||||
f"Expected `treespec` to be an instance of "
|
||||
f"PyTreeSpec but got item of type {type(treespec)}."
|
||||
)
|
||||
return treespec.unflatten(leaves)
|
||||
|
||||
|
||||
_none_registration = optree.register_pytree_node.get(type(None))
|
||||
assert _none_registration is not None
|
||||
|
||||
|
||||
@substitute_in_graph( # type: ignore[arg-type]
|
||||
_none_registration.unflatten_func,
|
||||
can_constant_fold_through=True,
|
||||
skip_signature_check=True,
|
||||
)
|
||||
def none_unflatten(_: None, children: Iterable[_T], /) -> None:
|
||||
if len(list(children)) != 0:
|
||||
raise ValueError("Expected no children.")
|
||||
return None
|
||||
|
||||
|
||||
with optree.dict_insertion_ordered(False, namespace="torch"):
|
||||
_dict_registration = optree.register_pytree_node.get(dict)
|
||||
assert _dict_registration is not None
|
||||
|
||||
|
||||
@substitute_in_graph( # type: ignore[arg-type]
|
||||
_dict_registration.flatten_func,
|
||||
can_constant_fold_through=True,
|
||||
skip_signature_check=True,
|
||||
)
|
||||
def dict_flatten(
|
||||
dct: dict[_KT, _VT], /
|
||||
) -> tuple[list[_VT], tuple[list[_KT], list[_KT]], tuple[_KT, ...]]:
|
||||
sorted_keys = optree.utils.total_order_sorted(dct)
|
||||
values = [dct[key] for key in sorted_keys]
|
||||
original_keys = list(dct)
|
||||
return values, (original_keys, sorted_keys), tuple(sorted_keys)
|
||||
|
||||
|
||||
@substitute_in_graph( # type: ignore[arg-type]
|
||||
_dict_registration.unflatten_func,
|
||||
can_constant_fold_through=True,
|
||||
skip_signature_check=True,
|
||||
)
|
||||
def dict_unflatten(
|
||||
metadata: tuple[list[_KT], list[_KT]],
|
||||
values: Iterable[_VT],
|
||||
/,
|
||||
) -> dict[_KT, _VT]:
|
||||
original_keys, sorted_keys = metadata
|
||||
d = dict.fromkeys(original_keys)
|
||||
d.update(zip(sorted_keys, values, strict=True))
|
||||
return d # type: ignore[return-value]
|
||||
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
Python polyfills for struct
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
from typing import Any
|
||||
from typing_extensions import Buffer
|
||||
|
||||
from ..decorators import substitute_in_graph
|
||||
|
||||
|
||||
__all__ = [
|
||||
"pack",
|
||||
"unpack",
|
||||
]
|
||||
|
||||
|
||||
@substitute_in_graph(struct.pack, can_constant_fold_through=True) # type: ignore[arg-type]
|
||||
def pack(fmt: bytes | str, /, *v: Any) -> bytes:
|
||||
return struct.pack(fmt, *v)
|
||||
|
||||
|
||||
@substitute_in_graph(struct.unpack, can_constant_fold_through=True) # type: ignore[arg-type]
|
||||
def unpack(format: bytes | str, buffer: Buffer, /) -> tuple[Any, ...]:
|
||||
return struct.unpack(format, buffer)
|
||||
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
Python polyfills for sys
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from ..decorators import substitute_in_graph
|
||||
|
||||
|
||||
__all__ = [
|
||||
"intern",
|
||||
"getrecursionlimit",
|
||||
]
|
||||
|
||||
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
@substitute_in_graph(sys.intern, can_constant_fold_through=True)
|
||||
def intern(string: str, /) -> str:
|
||||
return string
|
||||
|
||||
|
||||
@substitute_in_graph(sys.getrecursionlimit, can_constant_fold_through=True)
|
||||
def getrecursionlimit() -> int:
|
||||
return sys.getrecursionlimit()
|
||||
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
|
||||
@substitute_in_graph(sys.get_int_max_str_digits, can_constant_fold_through=True)
|
||||
def get_int_max_str_digits() -> int:
|
||||
return sys.get_int_max_str_digits()
|
||||
|
||||
@substitute_in_graph(sys.set_int_max_str_digits, can_constant_fold_through=True)
|
||||
def set_int_max_str_digits(maxdigits: int) -> None:
|
||||
sys.set_int_max_str_digits(maxdigits)
|
||||
|
||||
__all__ += ["get_int_max_str_digits", "set_int_max_str_digits"]
|
||||
@@ -0,0 +1,40 @@
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
from ..decorators import substitute_in_graph
|
||||
|
||||
|
||||
@substitute_in_graph( # type: ignore[arg-type]
|
||||
torch.Tensor._make_subclass
|
||||
)
|
||||
def make_subclass(
|
||||
cls: type[Any], data: torch.Tensor, requires_grad: bool = False, **kwargs: Any
|
||||
) -> Any:
|
||||
with torch._C.DisableTorchFunctionSubclass():
|
||||
# This is a rough approximation of `THPVariable_make_subclass`. It should
|
||||
# suffice for most of Dynamo tracing purposes.
|
||||
# https://github.com/pytorch/pytorch/blob/ccfde4dadfa3c342076a1ee387017f84dd4ad2f7/torch/csrc/autograd/python_variable.cpp#L597-L650
|
||||
assert len(kwargs) == 0, (
|
||||
"_make_subclass only supports requires_grad as keyword arg"
|
||||
)
|
||||
data = data.detach()
|
||||
|
||||
# Avoid unnecessary `requires_grad` mutation, which isn't supported in Dynamo.
|
||||
if data.requires_grad != requires_grad:
|
||||
data.requires_grad = requires_grad
|
||||
|
||||
# Dynamo can't yet handle upcasting to base tensor type via `as_subclass`.
|
||||
if cls is torch.Tensor:
|
||||
return torch.Tensor(data)
|
||||
|
||||
# Calling `as_subclass` because
|
||||
# 1. Dynamo knows how to handle it
|
||||
# 2. the C impls match at this point -- both `THPVariable_make_subclass` and
|
||||
# `THPVariable_as_subclass` calls `THPVariable_NewWithVar`.
|
||||
return data.as_subclass(cls)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"make_subclass",
|
||||
]
|
||||
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
Polyfills for torch._C._nn functions.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from torch.overrides import _is_torch_function_mode_enabled, _pop_mode_temporarily
|
||||
|
||||
from ..decorators import substitute_in_graph
|
||||
|
||||
|
||||
@substitute_in_graph(torch._C._nn._parse_to, skip_signature_check=True)
|
||||
def _parse_to_polyfill(
|
||||
*args: Any, **kwargs: Any
|
||||
) -> tuple[torch.device, torch.dtype, bool, torch.memory_format]:
|
||||
"""
|
||||
Polyfill for torch._C._nn._parse_to that parses arguments to nn.Module.to().
|
||||
|
||||
Signature mirrors torch._C._nn._parse_to which accepts:
|
||||
- to(device) - device as string or torch.device
|
||||
- to(dtype) - dtype as torch.dtype
|
||||
- to(tensor) - extracts device and dtype from tensor
|
||||
- to(device=..., dtype=..., non_blocking=..., memory_format=...)
|
||||
|
||||
Returns:
|
||||
tuple: (device, dtype, non_blocking, memory_format)
|
||||
"""
|
||||
# Check for __torch_function__ mode and dispatch using handle_torch_function pattern
|
||||
if _is_torch_function_mode_enabled():
|
||||
with _pop_mode_temporarily() as mode:
|
||||
result = mode.__torch_function__(
|
||||
torch._C._nn._parse_to, tuple(), args, kwargs or {}
|
||||
)
|
||||
if result is not NotImplemented:
|
||||
return result
|
||||
|
||||
# Default implementation
|
||||
device = None
|
||||
dtype = None
|
||||
non_blocking = False
|
||||
memory_format = None
|
||||
|
||||
# Handle positional arguments
|
||||
if len(args) == 1:
|
||||
arg = args[0]
|
||||
# Check if it's a tensor
|
||||
if isinstance(arg, torch.Tensor):
|
||||
device = arg.device
|
||||
dtype = arg.dtype
|
||||
# Check if it's a dtype
|
||||
elif isinstance(arg, torch.dtype):
|
||||
dtype = arg
|
||||
# Check if it's a device (string or torch.device)
|
||||
elif isinstance(arg, (str, torch.device)):
|
||||
device = torch.device(arg) if isinstance(arg, str) else arg
|
||||
else:
|
||||
raise TypeError(
|
||||
f"to() received an invalid combination of arguments. Got: {type(arg)}"
|
||||
)
|
||||
elif len(args) > 1:
|
||||
raise TypeError(
|
||||
f"to() received too many positional arguments. Got {len(args)}, expected at most 1"
|
||||
)
|
||||
|
||||
# Handle keyword arguments
|
||||
if "device" in kwargs:
|
||||
device_arg = kwargs["device"]
|
||||
if device_arg is not None:
|
||||
device = (
|
||||
torch.device(device_arg) if isinstance(device_arg, str) else device_arg
|
||||
)
|
||||
|
||||
if "dtype" in kwargs:
|
||||
dtype = kwargs["dtype"]
|
||||
|
||||
if "non_blocking" in kwargs:
|
||||
non_blocking = kwargs["non_blocking"]
|
||||
|
||||
if "memory_format" in kwargs:
|
||||
memory_format = kwargs["memory_format"]
|
||||
|
||||
# pyrefly: ignore[bad-return]
|
||||
return (device, dtype, non_blocking, memory_format)
|
||||
|
||||
|
||||
@substitute_in_graph(torch.__future__.get_swap_module_params_on_conversion)
|
||||
def get_swap_module_params_on_conversion_polyfill() -> bool:
|
||||
"""
|
||||
Polyfill for torch.__future__.get_swap_module_params_on_conversion.
|
||||
|
||||
Returns the actual value from the underlying global variable.
|
||||
"""
|
||||
# Access the module's global variable directly to avoid recursion
|
||||
import torch.__future__ as torch_future
|
||||
|
||||
return torch_future._swap_module_params_on_conversion
|
||||
|
||||
|
||||
@substitute_in_graph(torch._has_compatible_shallow_copy_type)
|
||||
def _has_compatible_shallow_copy_type_polyfill(
|
||||
input: torch.Tensor, from_: torch.Tensor
|
||||
) -> bool:
|
||||
"""
|
||||
Polyfill for torch._has_compatible_shallow_copy_type.
|
||||
|
||||
Checks if two tensors have compatible types for shallow copying.
|
||||
The C++ implementation checks if input's TensorImpl has compatible shallow copy type
|
||||
with from_'s key_set. We approximate this by checking if both tensors are the same type.
|
||||
"""
|
||||
# Check if both tensors are the same type (handles both regular tensors and subclasses)
|
||||
# This is more permissive than checking exact torch.Tensor type equality
|
||||
# but properly handles subclasses by allowing same-type shallow copies
|
||||
return type(input) is type(from_)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"_parse_to_polyfill",
|
||||
"get_swap_module_params_on_conversion_polyfill",
|
||||
"_has_compatible_shallow_copy_type_polyfill",
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
Python polyfills for traceback
|
||||
"""
|
||||
|
||||
import traceback
|
||||
from traceback import StackSummary
|
||||
from types import TracebackType
|
||||
|
||||
from ..decorators import substitute_in_graph
|
||||
|
||||
|
||||
__all__ = ["extract_tb", "clear_frames"]
|
||||
|
||||
|
||||
@substitute_in_graph(traceback.extract_tb, can_constant_fold_through=True)
|
||||
def extract_tb(tb: TracebackType | None, limit: int | None = None) -> StackSummary:
|
||||
if tb is None:
|
||||
return traceback.StackSummary.from_list([])
|
||||
# pyrefly: ignore [implicit-any]
|
||||
frame_summary = []
|
||||
while tb is not None:
|
||||
if limit:
|
||||
if len(frame_summary) < limit:
|
||||
frame_summary.append(
|
||||
# pyrefly: ignore[missing-attribute]
|
||||
tb.frame_summary
|
||||
)
|
||||
else:
|
||||
break
|
||||
else:
|
||||
frame_summary.append(tb.frame_summary) # pyrefly: ignore[missing-attribute]
|
||||
tb = tb.tb_next
|
||||
return traceback.StackSummary.from_list(frame_summary)
|
||||
|
||||
|
||||
@substitute_in_graph(traceback.clear_frames, can_constant_fold_through=True)
|
||||
def clear_frames(tb: TracebackType | None) -> None:
|
||||
# no-op
|
||||
return None
|
||||
@@ -0,0 +1,231 @@
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
from abc import abstractmethod
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
import torch
|
||||
from torch._dynamo.package import (
|
||||
_BackendId,
|
||||
_DynamoCacheEntry,
|
||||
DynamoCache,
|
||||
PrecompileCacheEntry,
|
||||
)
|
||||
|
||||
|
||||
"""
|
||||
Classes and implementations related to precompile
|
||||
"""
|
||||
|
||||
T = TypeVar("T")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BackendCacheArtifact(Generic[T]):
|
||||
"""
|
||||
Represents a single serializable backend artifact from a dynamo backend.
|
||||
Each BackendCacheArtifact has a key associated with it along with some
|
||||
serializable content.
|
||||
|
||||
Example implementation:
|
||||
|
||||
class MyPrecompileCacheArtifact(PrecompileCacheArtifact[MySerializableType]):
|
||||
my_field: int
|
||||
|
||||
def after_deserialization(self) -> MySerializableType:
|
||||
result = pickle.loads(self.content)
|
||||
# Do some extra work post deserialization
|
||||
result.my_post_deserialization_function(self.my_field)
|
||||
return result
|
||||
"""
|
||||
|
||||
key: str
|
||||
content: Any
|
||||
|
||||
@abstractmethod
|
||||
def after_deserialization(self) -> T:
|
||||
"""
|
||||
Code to be run after reading raw byte contents from disk.
|
||||
Generally converts self.content from raw bytes back into its original form.
|
||||
"""
|
||||
...
|
||||
|
||||
def edit_contents(self, edit_fn: Callable[..., Any]) -> None:
|
||||
"""
|
||||
Edit the contents of the artifact.
|
||||
"""
|
||||
self.content = edit_fn(self.content)
|
||||
|
||||
|
||||
class EagerCacheArtifact(BackendCacheArtifact[Any]):
|
||||
def after_deserialization(self) -> Any:
|
||||
return self.content
|
||||
|
||||
|
||||
class BypassDynamoCacheEntry(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PrecompileContext:
|
||||
"""
|
||||
PrecompileContext is a special CacheArtifactManager for handling precompilation
|
||||
It uses the same interface as CacheArtifactManager, but handles deserialization differently: instead
|
||||
of placing each artifact into respective caches, it will stitch all the cache artifacts for a single key
|
||||
together and place it into a global Precompile Cache.
|
||||
|
||||
PrecompileContext has two main portions: dynamo_cache_entries and backend_cache_artifacts.
|
||||
When saving, PrecompileContext.serialize() will serialize all dynamo cache entries along with any PrecompileCacheArtifacts that
|
||||
are needed to save those dynamo cache entries.
|
||||
|
||||
The following artifact types are supported by PrecompileContext:
|
||||
- BundledAOTAutogradCacheArtifact
|
||||
|
||||
"""
|
||||
|
||||
# Protected by the compile_lock
|
||||
# _backend_artifacts_by_key organizes results by the key of each artifact.
|
||||
# Each object here must be serializable
|
||||
_backend_artifacts_by_key: dict[_BackendId, BackendCacheArtifact[Any]] = {}
|
||||
|
||||
# On call to `serialize()`, all cache artifacts in _dynamo_cache_entries are converted
|
||||
# into DynamoCacheArtifacts and added to _new_cache_artifacts for serialization
|
||||
_dynamo_cache_entries: dict[str, _DynamoCacheEntry] = {}
|
||||
|
||||
@classmethod
|
||||
def clear(cls) -> None:
|
||||
cls._backend_artifacts_by_key.clear()
|
||||
cls._dynamo_cache_entries.clear()
|
||||
|
||||
@classmethod
|
||||
def record_artifact(
|
||||
cls,
|
||||
artifact: BackendCacheArtifact[Any],
|
||||
) -> None:
|
||||
"""
|
||||
Records a backend artifact to be used with dynamo cache entries
|
||||
"""
|
||||
# Temporarily disable all dispatch modes (including FakeTensorMode) during
|
||||
# deepcopy to avoid issues with cloning fake tensors (e.g., device mesh
|
||||
# with meta tensors that fail when cloning due to device mismatches)
|
||||
from torch.utils._mode_utils import no_dispatch
|
||||
|
||||
with no_dispatch():
|
||||
cls._backend_artifacts_by_key[_BackendId(artifact.key)] = copy.deepcopy(
|
||||
artifact
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def record_dynamo_cache_entry(
|
||||
cls, cache_entry: _DynamoCacheEntry, key: str
|
||||
) -> None:
|
||||
cls._dynamo_cache_entries[key] = cache_entry
|
||||
|
||||
@classmethod
|
||||
def edit_artifact(cls, key: str, edit_fn: Callable[..., Any]) -> None:
|
||||
"""
|
||||
Edit the content of an existing artifact
|
||||
"""
|
||||
assert key in cls._backend_artifacts_by_key, f"Key {key} not found in artifacts"
|
||||
artifact = cls._backend_artifacts_by_key[_BackendId(key)]
|
||||
artifact.edit_contents(edit_fn)
|
||||
|
||||
@classmethod
|
||||
def serialize_artifact_by_key(cls, key: str) -> BackendCacheArtifact[Any] | None:
|
||||
"""
|
||||
Return the backend cache artifact with the associated key
|
||||
"""
|
||||
return cls._backend_artifacts_by_key.get(_BackendId(key), None)
|
||||
|
||||
@staticmethod
|
||||
def dump_debug_info(
|
||||
dynamo_entries: dict[str, _DynamoCacheEntry],
|
||||
backend_artifacts: dict[_BackendId, BackendCacheArtifact[Any]],
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Return a JSON serializable debug dump of all entries in the precompile context
|
||||
Called in serialize before serialization, and in populate_caches after deserialization
|
||||
"""
|
||||
# Print debug information
|
||||
debug_info: defaultdict[str, list[Any]] = defaultdict(list)
|
||||
for key, cache_entry in dynamo_entries.items():
|
||||
info = cache_entry.debug_info()
|
||||
info["key"] = key
|
||||
debug_info["dynamo"].append(info)
|
||||
|
||||
for artifact in backend_artifacts.values():
|
||||
debug_info["backends"].append(artifact.key)
|
||||
|
||||
return debug_info
|
||||
|
||||
@classmethod
|
||||
def save_to_dynamo_cache(cls) -> dict[str, Any]:
|
||||
precompile_cache_entries, debug_info = cls.create_cache_entries()
|
||||
for key, entry in precompile_cache_entries.items():
|
||||
DynamoCache.write(entry, key)
|
||||
return debug_info
|
||||
|
||||
@classmethod
|
||||
def create_cache_entries(
|
||||
cls,
|
||||
) -> tuple[dict[str, PrecompileCacheEntry], dict[str, Any]]:
|
||||
"""
|
||||
Grabs all the cache entries in the precompile context and
|
||||
stitches them together into full PrecompileCacheEntries.
|
||||
"""
|
||||
dynamo_entries = cls._dynamo_cache_entries
|
||||
backend_artifacts = cls._backend_artifacts_by_key
|
||||
|
||||
num_artifacts = len(dynamo_entries)
|
||||
|
||||
debug_info = PrecompileContext.dump_debug_info(
|
||||
dynamo_entries, backend_artifacts
|
||||
)
|
||||
debug_str = json.dumps(
|
||||
{
|
||||
"num_entries": num_artifacts,
|
||||
"artifacts": debug_info,
|
||||
},
|
||||
)
|
||||
torch._logging.trace_structured(
|
||||
"artifact",
|
||||
metadata_fn=lambda: {
|
||||
"name": "dynamo_cache_entries",
|
||||
"encoding": "json",
|
||||
},
|
||||
payload_fn=lambda: debug_str,
|
||||
expect_trace_id=False,
|
||||
)
|
||||
|
||||
precompile_cache_entries = {}
|
||||
|
||||
for key, cache_entry in dynamo_entries.items():
|
||||
try:
|
||||
result = PrecompileCacheEntry.from_cache_entry(
|
||||
cache_entry, backend_artifacts
|
||||
)
|
||||
if result is not None:
|
||||
precompile_cache_entries[key] = result
|
||||
except Exception as e:
|
||||
logger.warning("Failed to create cache entry %s", key, exc_info=True)
|
||||
|
||||
error = e
|
||||
data = json.dumps(
|
||||
{
|
||||
"key": key,
|
||||
"error": str(error),
|
||||
}
|
||||
)
|
||||
torch._logging.trace_structured(
|
||||
"artifact",
|
||||
metadata_fn=lambda: {
|
||||
"name": "dynamo_cache_exception",
|
||||
"encoding": "json",
|
||||
},
|
||||
payload_fn=lambda: data,
|
||||
)
|
||||
continue
|
||||
return precompile_cache_entries, debug_info
|
||||
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
Dynamo profiling implementation.
|
||||
|
||||
This module provides profiling functionality for Dynamo, including:
|
||||
- ProfileMetrics: Class for collecting and aggregating performance metrics like
|
||||
execution time, operator counts, and fusion statistics
|
||||
- ProfileResult: Class for analyzing and reporting profiling results
|
||||
- Utilities for tracking missed/uncaptured operations
|
||||
- Functions for instrumenting FX graphs with profiling capabilities
|
||||
|
||||
The profiler helps measure and optimize the performance of Dynamo-compiled code
|
||||
by tracking both captured and total operations, timing, and graph statistics.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import os
|
||||
from typing import Any
|
||||
from typing_extensions import Self
|
||||
|
||||
import torch
|
||||
|
||||
from .utils import print_once
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ProfileMetrics:
|
||||
microseconds: float = 0.0
|
||||
operators: int = 0
|
||||
fusions: int = 0
|
||||
graphs: int = 0
|
||||
|
||||
def __iadd__(self, other: Self) -> Self:
|
||||
self.microseconds += other.microseconds
|
||||
self.operators += other.operators
|
||||
self.fusions += other.fusions
|
||||
return self
|
||||
|
||||
def __add__(self, other: ProfileMetrics) -> ProfileMetrics:
|
||||
assert isinstance(other, ProfileMetrics)
|
||||
return ProfileMetrics(
|
||||
self.microseconds + other.microseconds,
|
||||
self.operators + other.operators,
|
||||
self.fusions + other.fusions,
|
||||
)
|
||||
|
||||
def __truediv__(self, other: Any) -> ProfileMetrics:
|
||||
if isinstance(other, int):
|
||||
other = ProfileMetrics(other, other, other)
|
||||
return ProfileMetrics(
|
||||
self.microseconds / max(1, other.microseconds),
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
self.operators / max(1, other.operators),
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
self.fusions / max(1, other.fusions),
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.operators:4.0%} ops {self.microseconds:4.0%} time"
|
||||
|
||||
def tocsv(self) -> list[float]:
|
||||
return [self.operators, self.microseconds]
|
||||
|
||||
|
||||
class ProfileResult:
|
||||
def __init__(
|
||||
self, captured: ProfileMetrics, total: ProfileMetrics, unique_graphs: int
|
||||
) -> None:
|
||||
self.captured: ProfileMetrics = captured or ProfileMetrics()
|
||||
self.total: ProfileMetrics = total or ProfileMetrics()
|
||||
self.unique_graphs: int = unique_graphs
|
||||
|
||||
def __iadd__(self, other: Self) -> Self:
|
||||
self.captured += other.captured
|
||||
self.total += other.total
|
||||
self.unique_graphs += other.unique_graphs
|
||||
return self
|
||||
|
||||
def percent(self) -> ProfileMetrics:
|
||||
return self.captured / self.total
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f"{self.unique_graphs:2} graphs {self.captured.graphs:2} graph calls "
|
||||
f"{self.captured.operators:4}/{self.total.operators:4} = "
|
||||
+ str(self.percent())
|
||||
)
|
||||
|
||||
def tocsv(self) -> list[Any]:
|
||||
return [
|
||||
self.unique_graphs,
|
||||
self.captured.graphs,
|
||||
self.captured.operators,
|
||||
self.total.operators,
|
||||
] + self.percent().tocsv()
|
||||
|
||||
|
||||
def should_print_missing() -> bool:
|
||||
return os.environ.get("TORCHDYNAMO_PRINT_MISSING") == "1"
|
||||
|
||||
|
||||
def print_missing(stack: list[str]) -> None:
|
||||
if any("/torch/autograd/profiler.py" in x for x in stack):
|
||||
return
|
||||
stack = [
|
||||
x for x in stack if ("<built-in" not in x and "site-packages/torch/" not in x)
|
||||
]
|
||||
print_once("MISSING", " >> ".join(stack[-3:]))
|
||||
|
||||
|
||||
class Profiler:
|
||||
unique_graphs: int = 0
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.prof = torch.profiler.profile(
|
||||
activities=[torch.profiler.ProfilerActivity.CPU],
|
||||
with_stack=should_print_missing(),
|
||||
)
|
||||
|
||||
def results(self) -> ProfileResult:
|
||||
captured_regions = 0
|
||||
captured_ops = 0
|
||||
captured_microseconds = 0
|
||||
total_ops = 0
|
||||
total_microseconds = 0
|
||||
|
||||
last_op_end_time = -1
|
||||
captured_region_end_time = -1
|
||||
events = sorted(self.prof.events(), key=lambda x: x.time_range.start)
|
||||
for e in events:
|
||||
if e.name == "TORCHDYNAMO":
|
||||
captured_region_end_time = e.time_range.end
|
||||
captured_regions += 1
|
||||
# ignore `handle = torch.zeros(1)` in record_function.__init__()
|
||||
total_ops -= 1
|
||||
elif e.time_range.start >= last_op_end_time:
|
||||
last_op_end_time = e.time_range.end
|
||||
if e.time_range.end <= captured_region_end_time:
|
||||
captured_ops += 1
|
||||
captured_microseconds += e.time_range.elapsed_us()
|
||||
elif should_print_missing():
|
||||
print_missing(e.stack)
|
||||
total_ops += 1
|
||||
total_microseconds += e.time_range.elapsed_us()
|
||||
else:
|
||||
pass # ops recursively called from other ops (ignored)
|
||||
|
||||
unique_graphs = Profiler.unique_graphs
|
||||
Profiler.unique_graphs = 0
|
||||
# we counted one extra op that is part of the profiler setup code
|
||||
total_ops -= 1
|
||||
|
||||
return ProfileResult(
|
||||
captured=ProfileMetrics(
|
||||
microseconds=captured_microseconds,
|
||||
operators=captured_ops,
|
||||
fusions=captured_ops - captured_regions,
|
||||
graphs=captured_regions,
|
||||
),
|
||||
total=ProfileMetrics(
|
||||
microseconds=total_microseconds,
|
||||
operators=total_ops,
|
||||
fusions=total_ops - 1,
|
||||
),
|
||||
unique_graphs=unique_graphs,
|
||||
)
|
||||
|
||||
|
||||
def fx_insert_profiling(gm: torch.fx.GraphModule, example_inputs: list[Any]) -> Any:
|
||||
def _wrapped(*args: Any) -> Any:
|
||||
with torch.profiler.record_function("TORCHDYNAMO"):
|
||||
return gm.forward(*args)
|
||||
|
||||
Profiler.unique_graphs += 1
|
||||
return _wrapped
|
||||
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
Python execution state recording and replay functionality.
|
||||
|
||||
This module provides mechanisms for capturing and replaying Python execution state:
|
||||
|
||||
- ModuleRecord: Tracks module access patterns and attribute usage
|
||||
- DummyModule: Lightweight module substitute for replay
|
||||
- ExecutionRecord: Manages execution context including globals, locals and builtins
|
||||
- ExecutionRecorder: Records variable states and module access during execution
|
||||
|
||||
The module enables serialization and reproduction of Python execution environments,
|
||||
particularly useful for debugging and testing frameworks that need to capture
|
||||
and recreate specific program states.
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
from dataclasses import field
|
||||
from io import BufferedReader, BufferedWriter
|
||||
from types import CellType, CodeType, ModuleType
|
||||
from typing import Any, IO
|
||||
from typing_extensions import Self
|
||||
|
||||
from torch.utils._import_utils import import_dill
|
||||
|
||||
|
||||
dill = import_dill()
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ModuleRecord:
|
||||
module: ModuleType
|
||||
accessed_attrs: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class DummyModule:
|
||||
name: str
|
||||
is_torch: bool = False
|
||||
value: object = None
|
||||
|
||||
@property
|
||||
def __name__(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ExecutionRecord:
|
||||
code: CodeType
|
||||
closure: tuple[CellType]
|
||||
globals: dict[str, Any] = field(default_factory=dict)
|
||||
locals: dict[str, Any] = field(default_factory=dict)
|
||||
builtins: dict[str, Any] = field(default_factory=dict)
|
||||
code_options: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def dump(self, f: IO[str] | BufferedWriter) -> None:
|
||||
assert dill is not None, "replay_record requires `pip install dill`"
|
||||
dill.dump(self, f)
|
||||
|
||||
@classmethod
|
||||
def load(cls, f: IO[bytes] | BufferedReader) -> Self:
|
||||
assert dill is not None, "replay_record requires `pip install dill`"
|
||||
return dill.load(f)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ExecutionRecorder:
|
||||
LOCAL_MOD_PREFIX = "___local_mod_"
|
||||
|
||||
code: CodeType
|
||||
closure: tuple[CellType]
|
||||
globals: dict[str, Any] = field(default_factory=dict)
|
||||
locals: dict[str, Any] = field(default_factory=dict)
|
||||
builtins: dict[str, Any] = field(default_factory=dict)
|
||||
code_options: dict[str, Any] = field(default_factory=dict)
|
||||
name_to_modrec: dict[str, ModuleRecord] = field(default_factory=dict)
|
||||
|
||||
def add_local_var(self, name: str, var: Any) -> None:
|
||||
if isinstance(var, ModuleType):
|
||||
self.locals[name] = self._add_mod(var)
|
||||
else:
|
||||
self.locals[name] = var
|
||||
|
||||
def add_global_var(self, name: str, var: Any) -> None:
|
||||
if isinstance(var, ModuleType):
|
||||
self.globals[name] = self._add_mod(var)
|
||||
else:
|
||||
self.globals[name] = var
|
||||
|
||||
def add_local_mod(self, name: str, mod: ModuleType) -> None:
|
||||
assert isinstance(mod, ModuleType)
|
||||
self.add_global_var(name, mod)
|
||||
|
||||
def record_module_access(self, mod: ModuleType, name: str, val: Any) -> None:
|
||||
if isinstance(val, ModuleType):
|
||||
self.name_to_modrec[mod.__name__].accessed_attrs[name] = self._add_mod(val)
|
||||
return
|
||||
|
||||
if mod.__name__ in self.name_to_modrec:
|
||||
self.name_to_modrec[mod.__name__].accessed_attrs[name] = val
|
||||
|
||||
def get_record(self) -> ExecutionRecord:
|
||||
return ExecutionRecord(
|
||||
self.code,
|
||||
self.closure,
|
||||
ExecutionRecorder._resolve_modules(self.globals),
|
||||
ExecutionRecorder._resolve_modules(self.locals),
|
||||
self.builtins.copy(),
|
||||
self.code_options.copy(),
|
||||
)
|
||||
|
||||
def _add_mod(self, mod: ModuleType) -> ModuleRecord:
|
||||
if mod.__name__ not in self.name_to_modrec:
|
||||
self.name_to_modrec[mod.__name__] = ModuleRecord(mod)
|
||||
|
||||
return self.name_to_modrec[mod.__name__]
|
||||
|
||||
@classmethod
|
||||
def _resolve_modules(cls, vars: dict[str, Any]) -> dict[str, Any]:
|
||||
def resolve_module(var: Any) -> Any:
|
||||
if not isinstance(var, ModuleRecord):
|
||||
return var
|
||||
|
||||
dummy_mod = DummyModule(var.module.__name__)
|
||||
for attr_name, attr_value in var.accessed_attrs.items():
|
||||
attr_value = resolve_module(attr_value)
|
||||
dummy_mod.__setattr__(attr_name, attr_value)
|
||||
|
||||
return dummy_mod
|
||||
|
||||
return {k: resolve_module(v) for k, v in vars.items()}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,637 @@
|
||||
"""
|
||||
Utilities for reproducing and debugging issues in Dynamo after graph capture.
|
||||
|
||||
This file provides tools and infrastructure for debugging problems that occur
|
||||
after Dynamo has captured the graph but before/during backend compilation.
|
||||
Key components include:
|
||||
|
||||
- Minification tools to reduce large graphs to minimal failing examples
|
||||
- Accuracy testing to validate compiled graph outputs match eager mode
|
||||
- Repro generation to create standalone reproduction scripts
|
||||
- Debug backends for capturing and analyzing failures
|
||||
- Utilities for saving/loading graph states and inputs
|
||||
|
||||
The tools here focus specifically on the post-graph-capture stage, making them
|
||||
useful for debugging backend compilation issues, AOTAutograd problems, and
|
||||
accuracy discrepancies between compiled and eager execution.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import textwrap
|
||||
from collections.abc import Callable, Sequence
|
||||
from importlib import import_module
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.fx as fx
|
||||
from torch._dynamo.debug_utils import (
|
||||
AccuracyError,
|
||||
backend_accuracy_fails,
|
||||
BUCK_CMD_PREFIX,
|
||||
BuckTargetWriter,
|
||||
extra_imports,
|
||||
generate_config_string,
|
||||
generate_env_vars_string,
|
||||
helper_for_dump_minify,
|
||||
InputReader,
|
||||
InputWriter,
|
||||
minifier_dir,
|
||||
NNModuleToString,
|
||||
NopInputReader,
|
||||
run_fwd_maybe_bwd,
|
||||
same_two_models,
|
||||
)
|
||||
from torch.fx.experimental.symbolic_shapes import fx_placeholder_targets
|
||||
from torch.hub import tqdm
|
||||
|
||||
from .. import config
|
||||
from ..backends.registry import CompilerFn, lookup_backend, register_debug_backend
|
||||
from ..debug_utils import clone_inputs_retaining_gradness
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
inductor_config = import_module("torch._inductor.config")
|
||||
use_buck = inductor_config.is_fbcode()
|
||||
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
|
||||
# MAIN ENTRY POINT
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
|
||||
|
||||
|
||||
def _accuracy_fails(
|
||||
gm: torch.fx.GraphModule,
|
||||
example_inputs: Sequence[Any],
|
||||
compiler_fn: Callable[[torch.fx.GraphModule, list[Any]], torch.fx.GraphModule],
|
||||
) -> bool:
|
||||
return backend_accuracy_fails(
|
||||
gm,
|
||||
example_inputs,
|
||||
compiler_fn,
|
||||
only_fwd=config.repro_forward_only,
|
||||
ignore_non_fp=config.repro_ignore_non_fp,
|
||||
)
|
||||
|
||||
|
||||
class WrapBackendDebug:
|
||||
def __init__(
|
||||
self, unconfigured_compiler_fn: CompilerFn, compiler_name: str | None
|
||||
) -> None:
|
||||
functools.wraps(unconfigured_compiler_fn)(self)
|
||||
self._torchdynamo_orig_backend = unconfigured_compiler_fn
|
||||
self._compiler_name = compiler_name
|
||||
if hasattr(unconfigured_compiler_fn, "__name__"):
|
||||
self.__name__ = unconfigured_compiler_fn.__name__
|
||||
if hasattr(unconfigured_compiler_fn, "compiler_name"):
|
||||
self.__name__ = unconfigured_compiler_fn.compiler_name # type: ignore[attr-defined]
|
||||
if hasattr(unconfigured_compiler_fn, "get_compiler_config"):
|
||||
self.get_compiler_config = unconfigured_compiler_fn.get_compiler_config # type: ignore[attr-defined]
|
||||
|
||||
def __call__(
|
||||
self, gm: torch.fx.GraphModule, example_inputs: list[Any], **kwargs: Any
|
||||
) -> torch.fx.GraphModule:
|
||||
compiler_fn = functools.partial(self._torchdynamo_orig_backend, **kwargs)
|
||||
assert config.repro_after in ("dynamo", "aot", None)
|
||||
|
||||
if config.repro_after == "dynamo":
|
||||
|
||||
def add_paths(exc: Exception) -> None:
|
||||
exc.minifier_path = os.path.join(minifier_dir(), "minifier_launcher.py") # type: ignore[attr-defined]
|
||||
if use_buck:
|
||||
exc.buck_command = " ".join( # type: ignore[attr-defined]
|
||||
BUCK_CMD_PREFIX
|
||||
+ [BuckTargetWriter(exc.minifier_path).cmd_line_path] # type: ignore[attr-defined]
|
||||
)
|
||||
|
||||
if config.repro_level == 3:
|
||||
dump_to_minify_after_dynamo(gm, example_inputs, self._compiler_name)
|
||||
|
||||
# Check for either accuracy (level 4) or other type of failures.
|
||||
if config.repro_level == 4:
|
||||
# Check Accuracy
|
||||
compiled_gm = compiler_fn(copy.deepcopy(gm), example_inputs)
|
||||
if _accuracy_fails(gm, example_inputs, compiler_fn): # type: ignore[arg-type]
|
||||
log.warning(
|
||||
"Accuracy failed for the TorchDynamo produced graph. Creating script to minify the error."
|
||||
)
|
||||
dump_to_minify_after_dynamo(
|
||||
fx.GraphModule(gm, copy.deepcopy(gm.graph)),
|
||||
example_inputs,
|
||||
self._compiler_name,
|
||||
)
|
||||
exc = AccuracyError("Bad accuracy detected.")
|
||||
add_paths(exc)
|
||||
raise exc
|
||||
else:
|
||||
try:
|
||||
compiled_gm = compiler_fn(copy.deepcopy(gm), example_inputs)
|
||||
run_fwd_maybe_bwd(compiled_gm, example_inputs) # type: ignore[arg-type]
|
||||
except Exception as exc:
|
||||
log.warning(
|
||||
"Compiled Fx GraphModule failed. Creating script to minify the error."
|
||||
)
|
||||
if config.repro_level == 1:
|
||||
dump_state_fn = functools.partial(
|
||||
dump_backend_state, compiler_name=self._compiler_name
|
||||
)
|
||||
dump_state_fn(
|
||||
fx.GraphModule(gm, copy.deepcopy(gm.graph)), example_inputs
|
||||
)
|
||||
elif config.repro_level == 2:
|
||||
dump_to_minify_after_dynamo(
|
||||
fx.GraphModule(gm, copy.deepcopy(gm.graph)),
|
||||
example_inputs,
|
||||
self._compiler_name,
|
||||
)
|
||||
add_paths(exc)
|
||||
raise
|
||||
else:
|
||||
compiled_gm = compiler_fn(gm, example_inputs)
|
||||
|
||||
return compiled_gm # type: ignore[return-value]
|
||||
|
||||
|
||||
def wrap_backend_debug(
|
||||
unconfigured_compiler_fn: CompilerFn, compiler_name: str | None
|
||||
) -> WrapBackendDebug:
|
||||
"""
|
||||
A minifier decorator that wraps the TorchDynamo produced Fx graph modules.
|
||||
As opposed to wrap_compiler_debug, this wrapper intercepts at the
|
||||
TorchDynamo produced Fx Graph Module. This makes it backend-agnostic to some
|
||||
level, e.g., it is useful for minifying issues related to Aot Autograd
|
||||
tracing. If an error is found, we minify and save the minified repro in
|
||||
repro.tar.gz.
|
||||
"""
|
||||
return WrapBackendDebug(unconfigured_compiler_fn, compiler_name)
|
||||
|
||||
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
|
||||
# REPRO DUMPERS
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
|
||||
|
||||
|
||||
def generate_dynamo_fx_repro_string(
|
||||
gm: torch.fx.GraphModule,
|
||||
args: Sequence[Any],
|
||||
compiler_name: str | None,
|
||||
check_accuracy: bool = False,
|
||||
*,
|
||||
stable_output: bool = False,
|
||||
save_dir: str | None = None,
|
||||
command: str = "run",
|
||||
) -> str:
|
||||
"""
|
||||
Generate a repro string for backend-agnostic minified version.
|
||||
"""
|
||||
|
||||
model_str = NNModuleToString.convert(gm)
|
||||
|
||||
# TODO: Figure out why torch.compile'd hash isn't work on this codepath
|
||||
writer = InputWriter(save_dir, stable_hash=True)
|
||||
for placeholder, arg in zip(fx_placeholder_targets(gm), args):
|
||||
if isinstance(arg, (int, torch.SymInt)):
|
||||
writer.symint(placeholder, arg)
|
||||
elif isinstance(arg, torch.Tensor):
|
||||
# TODO: improve these names with FQN
|
||||
writer.tensor(placeholder, arg)
|
||||
else:
|
||||
raise TypeError(f"arg is neither SymInt/int nor torch.Tensor, {arg}")
|
||||
load_args = "\n".join(writer.lines())
|
||||
|
||||
return textwrap.dedent(
|
||||
f"""
|
||||
{generate_env_vars_string(stable_output=stable_output)}
|
||||
from math import inf
|
||||
import torch
|
||||
from torch import tensor, device
|
||||
import torch.fx as fx
|
||||
import torch._dynamo
|
||||
from torch._dynamo.testing import rand_strided
|
||||
from torch._dynamo.debug_utils import run_fwd_maybe_bwd
|
||||
|
||||
{generate_config_string(stable_output=stable_output)}
|
||||
|
||||
{extra_imports}
|
||||
|
||||
{model_str}
|
||||
mod = Repro()
|
||||
|
||||
{load_args}
|
||||
|
||||
if __name__ == '__main__':
|
||||
from torch._dynamo.repro.after_dynamo import run_repro
|
||||
run_repro(mod, load_args, accuracy={check_accuracy!r}, command={command!r},
|
||||
save_dir={save_dir!r}, autocast={torch.is_autocast_enabled()!r}, backend={compiler_name!r})
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def dump_backend_repro_as_file(
|
||||
gm: torch.fx.GraphModule,
|
||||
args: Sequence[Any],
|
||||
compiler_name: str | None,
|
||||
check_accuracy: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Saves the repro to a repro.py file
|
||||
"""
|
||||
curdir = os.getcwd()
|
||||
subdir = os.path.join(os.getcwd(), "checkpoints")
|
||||
if not os.path.exists(subdir):
|
||||
os.makedirs(subdir, exist_ok=True)
|
||||
file_name = os.path.join(subdir, f"minified_{len(gm.graph.nodes)}_nodes.py")
|
||||
log.warning(
|
||||
"Writing checkpoint with %s nodes to %s", len(gm.graph.nodes), file_name
|
||||
)
|
||||
|
||||
with open(file_name, "w") as fd:
|
||||
fd.write(
|
||||
generate_dynamo_fx_repro_string(
|
||||
gm, args, compiler_name, check_accuracy, save_dir=subdir
|
||||
)
|
||||
)
|
||||
latest_repro = os.path.join(curdir, "repro.py")
|
||||
log.warning("Copying %s to %s for convenience", file_name, latest_repro)
|
||||
|
||||
if use_buck:
|
||||
BuckTargetWriter(latest_repro).write()
|
||||
|
||||
shutil.copyfile(file_name, latest_repro)
|
||||
|
||||
|
||||
def dump_backend_state(
|
||||
gm: torch.fx.GraphModule,
|
||||
args: Sequence[Any],
|
||||
compiler_name: str | None,
|
||||
check_accuracy: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Dumps the dynamo graph to repro the issue.
|
||||
1) It tries to convert Fx GraphModule to a string. If we can, it writes to a
|
||||
repro.py file.
|
||||
2) If we can't convert Fx GraphModule to a string, we use to_folder to save
|
||||
the module and save a tar file.
|
||||
"""
|
||||
assert NNModuleToString.can_convert_to_string(gm)
|
||||
return dump_backend_repro_as_file(gm, args, compiler_name, check_accuracy)
|
||||
# return dump_backend_repro_as_tarfile(gm, args, compiler_name)
|
||||
|
||||
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
|
||||
# MINIFIER DUMPER
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
|
||||
|
||||
|
||||
def dump_to_minify_after_dynamo(
|
||||
gm: torch.fx.GraphModule, args: Sequence[Any], compiler_name: str | None
|
||||
) -> None:
|
||||
# TODO: factor this out
|
||||
subdir = os.path.join(minifier_dir(), "checkpoints")
|
||||
if not os.path.exists(subdir):
|
||||
os.makedirs(subdir, exist_ok=True)
|
||||
helper_for_dump_minify(
|
||||
generate_dynamo_fx_repro_string(
|
||||
gm,
|
||||
args,
|
||||
compiler_name,
|
||||
check_accuracy=config.repro_level == 4,
|
||||
save_dir=subdir,
|
||||
command="minify",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
|
||||
# MINIFIER BACKENDS
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
|
||||
|
||||
|
||||
@register_debug_backend # type: ignore[arg-type]
|
||||
def dynamo_minifier_backend(
|
||||
gm: fx.GraphModule, example_inputs: Sequence[Any], compiler_name: str | None
|
||||
) -> fx.GraphModule:
|
||||
from functorch.compile import minifier
|
||||
|
||||
compiler_fn = lookup_backend(compiler_name) # type: ignore[arg-type]
|
||||
|
||||
# TODO: It's inconsistent to pass SymInt inputs but REAL tensors.
|
||||
# We should pass ints and look at the GraphModule placeholders
|
||||
# to resolve them to SymInt (if necessary)
|
||||
example_inputs = [
|
||||
i.node.hint if isinstance(i, torch.SymInt) else i for i in example_inputs
|
||||
]
|
||||
|
||||
try:
|
||||
compiled_gm = compiler_fn(gm, example_inputs)
|
||||
run_fwd_maybe_bwd(compiled_gm, example_inputs) # type: ignore[arg-type]
|
||||
raise ValueError("No issue was detected")
|
||||
except Exception as exc:
|
||||
orig_failure = str(exc)
|
||||
log.warning(
|
||||
"Compiled Fx GraphModule failed. Creating script to minify the error."
|
||||
)
|
||||
dump_state_fn = functools.partial(
|
||||
dump_backend_state, compiler_name=compiler_name
|
||||
)
|
||||
dump_state_fn(fx.GraphModule(gm, copy.deepcopy(gm.graph)), example_inputs)
|
||||
fails_fn = functools.partial(
|
||||
backend_fails,
|
||||
compiler_fn=compiler_fn,
|
||||
orig_failure=orig_failure,
|
||||
)
|
||||
minifier(
|
||||
gm,
|
||||
example_inputs,
|
||||
module_fails=fails_fn,
|
||||
dump_state=dump_state_fn,
|
||||
)
|
||||
return gm
|
||||
|
||||
|
||||
@register_debug_backend # type: ignore[arg-type]
|
||||
def dynamo_accuracy_minifier_backend(
|
||||
gm: fx.GraphModule, example_inputs: Sequence[Any], compiler_name: str | None
|
||||
) -> fx.GraphModule:
|
||||
from functorch.compile import minifier
|
||||
|
||||
compiler_fn = lookup_backend(compiler_name) # type: ignore[arg-type]
|
||||
|
||||
# Set the eval mode to remove randomness.
|
||||
gm.eval()
|
||||
|
||||
# Check Accuracy
|
||||
if _accuracy_fails(gm, example_inputs, compiler_fn): # type: ignore[arg-type]
|
||||
log.warning("Accuracy failed for the TorchDynamo produced graph")
|
||||
dump_state_fn = functools.partial(
|
||||
dump_backend_state, compiler_name=compiler_name, check_accuracy=True
|
||||
)
|
||||
fails_fn = functools.partial(
|
||||
_accuracy_fails,
|
||||
compiler_fn=compiler_fn, # type: ignore[arg-type]
|
||||
)
|
||||
dump_state_fn(fx.GraphModule(gm, copy.deepcopy(gm.graph)), example_inputs)
|
||||
minifier(
|
||||
gm,
|
||||
example_inputs,
|
||||
module_fails=fails_fn,
|
||||
dump_state=dump_state_fn,
|
||||
)
|
||||
else:
|
||||
log.error("Input graph does not fail accuracy testing")
|
||||
return gm
|
||||
|
||||
|
||||
def backend_fails(
|
||||
gm: fx.GraphModule,
|
||||
example_inputs: Sequence[Any],
|
||||
compiler_fn: CompilerFn,
|
||||
orig_failure: Sequence[Any],
|
||||
) -> bool:
|
||||
"""
|
||||
Minifier uses this function to identify if the minified graph module fails
|
||||
with the same error.
|
||||
|
||||
One caveat is that minifier can potentially go into a wrong direction when
|
||||
the resulting graph module fails for a different reason. To avoid this, we
|
||||
save the string for the original exception and check similarity between new
|
||||
and old exception. They can be somewhat different in some cases, when the
|
||||
exception string depends on the failing node information. So, we have a
|
||||
loose similarity metric to guide the minifier path.
|
||||
"""
|
||||
from difflib import SequenceMatcher
|
||||
|
||||
try:
|
||||
# Run the original gm to check eager validity
|
||||
run_fwd_maybe_bwd(gm, clone_inputs_retaining_gradness(example_inputs))
|
||||
compiled_gm = compiler_fn(gm, example_inputs) # type: ignore[arg-type]
|
||||
run_fwd_maybe_bwd(compiled_gm, clone_inputs_retaining_gradness(example_inputs)) # type: ignore[arg-type]
|
||||
except Exception as e:
|
||||
new_failure = str(e)
|
||||
if SequenceMatcher(None, orig_failure, new_failure).ratio() > 0.5:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
|
||||
# REPRO MAIN
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
|
||||
|
||||
|
||||
def run_load_args(options: Any, mod: torch.nn.Module, load_args: Any) -> list[Any]:
|
||||
if not hasattr(load_args, "_version"):
|
||||
log.warning(
|
||||
"load_args does not have a _version attribute, please file a bug to PyTorch "
|
||||
"and describe how you generate this repro script"
|
||||
)
|
||||
else:
|
||||
if load_args._version > 0:
|
||||
log.warning(
|
||||
"load_args is version %s, but this version of PyTorch only supports "
|
||||
"version 0. We will try to run it anyway but there may be an incompatibility; "
|
||||
"if so, try upgrading your version of PyTorch.",
|
||||
load_args._version,
|
||||
)
|
||||
|
||||
nop_reader = NopInputReader()
|
||||
load_args(nop_reader)
|
||||
|
||||
with tqdm(desc="Loading inputs", total=nop_reader.total) as pbar:
|
||||
input_reader = InputReader(save_dir=options.save_dir, pbar=pbar)
|
||||
load_args(input_reader)
|
||||
args = input_reader.args
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def repro_minify(options: Any, mod: torch.nn.Module, load_args: Any) -> None:
|
||||
args = run_load_args(options, mod, load_args)
|
||||
|
||||
# Setup debug minifier compiler
|
||||
if not options.accuracy:
|
||||
compiler_fn = lookup_backend("dynamo_minifier_backend")
|
||||
else:
|
||||
compiler_fn = lookup_backend("dynamo_accuracy_minifier_backend")
|
||||
|
||||
if options.backend is None:
|
||||
raise RuntimeError(
|
||||
"Compiler name is None - this likely means that a custom compiler "
|
||||
"was called by torchdynamo. Please remove this error, import your "
|
||||
"custom compiler function, and replace the backend=None "
|
||||
"line in run_repro to backend=<my_imported_custom_function>"
|
||||
)
|
||||
|
||||
dynamo_minifier_backend = functools.partial(
|
||||
compiler_fn,
|
||||
compiler_name=options.backend, # type: ignore[call-arg]
|
||||
)
|
||||
opt_mod = torch._dynamo.optimize(dynamo_minifier_backend)(mod)
|
||||
|
||||
with torch.amp.autocast("cuda", enabled=options.autocast):
|
||||
opt_mod(*args)
|
||||
|
||||
|
||||
def repro_run(options: Any, mod: torch.nn.Module, load_args: Any) -> None:
|
||||
opt_mod = torch._dynamo.optimize(options.backend)(mod)
|
||||
|
||||
if options.accuracy != "":
|
||||
mod.eval()
|
||||
opt_mod.eval() # type: ignore[union-attr]
|
||||
|
||||
with torch.amp.autocast("cuda", enabled=options.autocast):
|
||||
# TODO: disable clone
|
||||
args = run_load_args(options, mod, load_args)
|
||||
assert same_two_models(mod, mod, args), "Eager itself failed" # type: ignore[arg-type]
|
||||
if not same_two_models(
|
||||
mod, # type: ignore[arg-type]
|
||||
opt_mod, # type: ignore[arg-type]
|
||||
args,
|
||||
only_fwd=config.repro_forward_only,
|
||||
ignore_non_fp=config.repro_ignore_non_fp,
|
||||
):
|
||||
raise AccuracyError("Dynamo failed")
|
||||
else:
|
||||
with torch.amp.autocast("cuda", enabled=options.autocast):
|
||||
args = run_load_args(options, mod, load_args)
|
||||
run_fwd_maybe_bwd(mod, args, only_fwd=options.only_fwd, disable_clone=True) # type: ignore[arg-type]
|
||||
del args
|
||||
|
||||
args = run_load_args(options, mod, load_args)
|
||||
run_fwd_maybe_bwd(
|
||||
opt_mod, # type: ignore[arg-type]
|
||||
args,
|
||||
only_fwd=options.only_fwd,
|
||||
disable_clone=True, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
|
||||
def run_repro(
|
||||
mod: torch.nn.Module,
|
||||
load_args: Any,
|
||||
*,
|
||||
command: str = "run",
|
||||
accuracy: bool | str = "",
|
||||
save_dir: str | None = None,
|
||||
autocast: bool = False,
|
||||
backend: str = "inductor",
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
for k in kwargs:
|
||||
log.warning(
|
||||
"Unrecognized kwarg %s; perhaps this repro was made on a newer version of PyTorch",
|
||||
k,
|
||||
)
|
||||
|
||||
if accuracy is True:
|
||||
accuracy = "accuracy"
|
||||
elif accuracy is False:
|
||||
accuracy = ""
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=f"""\
|
||||
An after_dynamo repro script, typically triggering a bug in Dynamo or
|
||||
AOTAutograd. When run with no arguments, this script defaults to running
|
||||
'{command}'. Extra flags may be available; to find out more, try '{command}
|
||||
--help'. There are also alternate subcommands available, see below.
|
||||
|
||||
default settings on this script:
|
||||
{accuracy=}
|
||||
{save_dir=}
|
||||
""",
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
|
||||
def common_flags(parser: argparse.ArgumentParser) -> None:
|
||||
accuracy_group = parser.add_mutually_exclusive_group()
|
||||
accuracy_group.add_argument(
|
||||
"--no-accuracy",
|
||||
dest="accuracy",
|
||||
action="store_const",
|
||||
const="",
|
||||
default=accuracy,
|
||||
help="do not test accuracy, just run the module and see if it errors",
|
||||
)
|
||||
accuracy_group.add_argument(
|
||||
"--accuracy",
|
||||
action="store_const",
|
||||
const="accuracy",
|
||||
default=accuracy,
|
||||
help="test accuracy",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--save-dir",
|
||||
type=str,
|
||||
default=save_dir,
|
||||
metavar="DIR",
|
||||
help="directory where saved inputs live",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-save-dir",
|
||||
dest="save_dir",
|
||||
action="store_const",
|
||||
const=None,
|
||||
help="don't use any directory for saved inputs",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-isolate",
|
||||
dest="isolate",
|
||||
action="store_false",
|
||||
default=False,
|
||||
help="no isolate (doesn't do anything for after_dynamo)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--autocast",
|
||||
default=autocast,
|
||||
action="store_true",
|
||||
help="use torch.cuda.amp.autocast",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-autocast",
|
||||
dest="autocast",
|
||||
action="store_false",
|
||||
help="don't use torch.cuda.amp.autocast",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--backend",
|
||||
type=str,
|
||||
default=backend,
|
||||
metavar="BACKEND",
|
||||
help="torch.compile backend to use",
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(
|
||||
dest="command", metavar="{run,minify}", required=True
|
||||
)
|
||||
|
||||
parser_run = subparsers.add_parser(
|
||||
"run",
|
||||
help="just run the repro",
|
||||
)
|
||||
common_flags(parser_run)
|
||||
parser_run.add_argument(
|
||||
"--only-fwd",
|
||||
action="store_true",
|
||||
help="don't run backwards compilation for testing",
|
||||
)
|
||||
|
||||
parser_minify = subparsers.add_parser(
|
||||
"minify", help="run the minifier on the repro"
|
||||
)
|
||||
common_flags(parser_minify)
|
||||
|
||||
args = None
|
||||
if len(sys.argv) <= 1:
|
||||
args = [command, *sys.argv[1:]]
|
||||
|
||||
options = parser.parse_args(args)
|
||||
COMMAND_FNS = {
|
||||
"minify": repro_minify,
|
||||
"run": repro_run,
|
||||
}
|
||||
COMMAND_FNS[options.command](options, mod, load_args)
|
||||
@@ -0,0 +1,662 @@
|
||||
"""
|
||||
Utilities for debugging and reproducing issues in Ahead of Time with Inductor (AOTI) compilation.
|
||||
|
||||
This file provides tools and utilities for:
|
||||
- Generating minimal reproducible test cases (minification)
|
||||
- Handling exported programs and graph modules
|
||||
- Creating debug repros for AOTI compilation issues
|
||||
- Supporting both accuracy testing and error reproduction
|
||||
- Managing configuration and environment for repro cases
|
||||
|
||||
The main components include:
|
||||
- Minification tools to reduce test cases while preserving errors
|
||||
- Repro generation utilities for exported programs
|
||||
- Error handling specific to AOTI compilation
|
||||
- Command-line interface for running and managing repros
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import functools
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import textwrap
|
||||
from collections.abc import Sequence
|
||||
from importlib import import_module
|
||||
from typing import Any, IO
|
||||
|
||||
import torch
|
||||
from torch._dynamo.debug_utils import (
|
||||
_cuda_system_info_comment,
|
||||
BuckTargetWriter,
|
||||
extra_imports,
|
||||
generate_config_string,
|
||||
generate_env_vars_string,
|
||||
helper_for_dump_minify,
|
||||
InputReader,
|
||||
minifier_dir,
|
||||
NNModuleToString,
|
||||
NopInputReader,
|
||||
)
|
||||
from torch.export import ExportedProgram
|
||||
from torch.hub import tqdm
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
inductor_config = import_module("torch._inductor.config")
|
||||
use_buck = inductor_config.is_fbcode()
|
||||
|
||||
|
||||
class AOTIMinifierError(Exception):
|
||||
def __init__(self, original_exception: str | Exception) -> None:
|
||||
additional_message = "This error is caused by a bug in the AOTI minifier, please report a bug to PyTorch"
|
||||
full_message = f"{additional_message}: {str(original_exception)}"
|
||||
super().__init__(full_message)
|
||||
self.original_exception = original_exception
|
||||
|
||||
|
||||
def dump_to_minify(
|
||||
exported_program: ExportedProgram,
|
||||
compiler_name: str,
|
||||
command: str = "minify",
|
||||
options: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
If command is "minify":
|
||||
Dump exported_program to `debug_dir/minifier/minifier_launcher.py`, with minify command.
|
||||
If command is "run":
|
||||
Dump exported_program to `cwd/repro.py`, with run command.
|
||||
"""
|
||||
assert command in ["minify", "run"]
|
||||
|
||||
subdir = os.path.join(minifier_dir(), "checkpoints")
|
||||
if not os.path.exists(subdir):
|
||||
os.makedirs(subdir, exist_ok=True)
|
||||
|
||||
if command == "minify":
|
||||
out = io.StringIO()
|
||||
save_graph_repro_ep(
|
||||
out,
|
||||
compiler_name,
|
||||
exported_program=exported_program,
|
||||
save_dir=subdir,
|
||||
command="minify",
|
||||
config_patches=options,
|
||||
)
|
||||
return helper_for_dump_minify(out.getvalue())
|
||||
else:
|
||||
curdir = os.getcwd()
|
||||
file_name = os.path.join(curdir, "repro.py")
|
||||
try:
|
||||
with open(file_name, "w") as fd:
|
||||
save_graph_repro_ep(
|
||||
fd,
|
||||
compiler_name,
|
||||
exported_program=exported_program,
|
||||
config_patches=options,
|
||||
save_dir=subdir,
|
||||
command="run",
|
||||
module_in_comment=True,
|
||||
)
|
||||
log.warning("Writing repro file to %s", file_name)
|
||||
if use_buck:
|
||||
BuckTargetWriter(file_name).write()
|
||||
except OSError:
|
||||
log.warning("No write permissions for %s", file_name)
|
||||
|
||||
|
||||
def get_module_string(gm: torch.fx.GraphModule) -> str:
|
||||
def _convert_to_comment(s_: str) -> str:
|
||||
s = s_.split("\n")
|
||||
if len(s) == 1:
|
||||
return "# " + s_
|
||||
first = s.pop(0)
|
||||
for i in range(len(s)):
|
||||
line = s[i]
|
||||
if line.strip() != "":
|
||||
s[i] = "# " + line
|
||||
else:
|
||||
s[i] = ""
|
||||
s = "\n".join(s)
|
||||
s = first + "\n" + s
|
||||
return s
|
||||
|
||||
module_string = NNModuleToString.convert(gm)
|
||||
return _convert_to_comment(module_string)
|
||||
|
||||
|
||||
def save_graph_repro_ep(
|
||||
fd: IO[Any],
|
||||
compiler_name: str,
|
||||
*,
|
||||
exported_program: ExportedProgram | None = None,
|
||||
gm: torch.nn.Module | None = None,
|
||||
args: tuple[Any] | None = None,
|
||||
config_patches: dict[str, str] | None = None,
|
||||
stable_output: bool = False,
|
||||
save_dir: str | None = None,
|
||||
command: str = "run",
|
||||
accuracy: str | bool | None = None,
|
||||
check_str: str | None = None,
|
||||
module_in_comment: bool = False,
|
||||
strict: bool = False,
|
||||
) -> None:
|
||||
# Save graph for reproducing the error.
|
||||
# Either exported_program or gm will be saved, depending on which one is defined.
|
||||
# Only one of exported_program and gm should be defined.
|
||||
|
||||
if exported_program is None and gm is None:
|
||||
raise AOTIMinifierError("One of exported_program and gm must be defined")
|
||||
if exported_program is not None and gm is not None:
|
||||
raise AOTIMinifierError("Only one of exported_program and gm can be defined")
|
||||
if gm is not None and args is None:
|
||||
raise AOTIMinifierError("If gm is defined, args should also be defined")
|
||||
|
||||
if exported_program is None:
|
||||
assert gm is not None
|
||||
assert args is not None
|
||||
exported_program = torch.export.export(gm, args, strict=strict)
|
||||
elif gm is None:
|
||||
gm = exported_program.module(check_guards=False)
|
||||
|
||||
# save a graph preview using gm
|
||||
module_string = get_module_string(gm) # type: ignore[arg-type]
|
||||
fd.write(module_string)
|
||||
|
||||
# save a graph repro using exported_program
|
||||
fd.write(
|
||||
generate_compiler_repro_exported_program(
|
||||
exported_program,
|
||||
options=config_patches,
|
||||
stable_output=stable_output,
|
||||
save_dir=save_dir,
|
||||
)
|
||||
)
|
||||
if accuracy is None:
|
||||
accuracy = "_accuracy" in compiler_name
|
||||
fd.write("if __name__ == '__main__':\n")
|
||||
fd.write(" from torch._dynamo.repro.aoti import run_repro\n")
|
||||
fd.write(
|
||||
f" with torch.no_grad():\n"
|
||||
f" run_repro(exported_program, config_patches=config_patches, accuracy={accuracy!r}, command={command!r}, "
|
||||
f"save_dir={save_dir!r}, check_str={check_str!r})\n"
|
||||
)
|
||||
|
||||
|
||||
def dump_compiler_graph_state(
|
||||
gm: torch.fx.GraphModule,
|
||||
args: Sequence[Any],
|
||||
compiler_name: str,
|
||||
*,
|
||||
config_patches: dict[str, str] | None = None,
|
||||
accuracy: str | bool | None = None,
|
||||
strict: bool = False,
|
||||
) -> None:
|
||||
subdir = os.path.join(minifier_dir(), "checkpoints")
|
||||
if not os.path.exists(subdir):
|
||||
os.makedirs(subdir, exist_ok=True)
|
||||
file_name = os.path.join(subdir, f"{len(gm.graph.nodes)}.py")
|
||||
log.warning(
|
||||
"Writing checkpoint with %s nodes to %s", len(gm.graph.nodes), file_name
|
||||
)
|
||||
with open(file_name, "w") as fd:
|
||||
save_graph_repro_ep(
|
||||
fd,
|
||||
compiler_name,
|
||||
gm=gm,
|
||||
args=tuple(args),
|
||||
config_patches=config_patches,
|
||||
save_dir=subdir,
|
||||
accuracy=accuracy,
|
||||
module_in_comment=True,
|
||||
strict=strict,
|
||||
)
|
||||
curdir = os.getcwd()
|
||||
repro_path = os.path.join(curdir, "repro.py")
|
||||
try:
|
||||
shutil.copyfile(file_name, repro_path)
|
||||
log.warning("Copying repro file for convenience to %s", repro_path)
|
||||
if use_buck:
|
||||
BuckTargetWriter(file_name).write()
|
||||
except OSError:
|
||||
log.warning("No write permissions for %s", repro_path)
|
||||
|
||||
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
|
||||
# DUMP REPROS
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
|
||||
|
||||
|
||||
def generate_compiler_repro_exported_program(
|
||||
exported_program: ExportedProgram,
|
||||
*,
|
||||
options: dict[str, str] | None = None,
|
||||
stable_output: bool = False,
|
||||
save_dir: str | None = None,
|
||||
) -> str:
|
||||
model_str = textwrap.dedent(
|
||||
f"""
|
||||
{generate_env_vars_string(stable_output=stable_output)}
|
||||
import torch
|
||||
import torch._inductor.inductor_prims
|
||||
|
||||
{generate_config_string(stable_output=stable_output)}
|
||||
|
||||
isolate_fails_code_str = None
|
||||
|
||||
{extra_imports}
|
||||
|
||||
"""
|
||||
)
|
||||
if not stable_output:
|
||||
model_str += f"# torch version: {torch.version.__version__}\n"
|
||||
if hasattr(torch.version, "cuda"):
|
||||
model_str += f"# torch cuda version: {torch.version.cuda}\n"
|
||||
if hasattr(torch.version, "git_version"):
|
||||
model_str += f"# torch git version: {torch.version.git_version}\n\n\n"
|
||||
model_str += _cuda_system_info_comment()
|
||||
if save_dir:
|
||||
ep_path = os.path.join(save_dir, "exported_program.pt2")
|
||||
else:
|
||||
ep_path = "exported_program.pt2"
|
||||
torch.export.save(exported_program, ep_path)
|
||||
|
||||
model_str += f"exported_program = torch.export.load('{ep_path}')\n"
|
||||
model_str += "# print(exported_program.graph)\n"
|
||||
model_str += f"config_patches={options}\n"
|
||||
return model_str
|
||||
|
||||
|
||||
def repro_load_args(load_args: Any, save_dir: str | None) -> tuple[Any]:
|
||||
if not hasattr(load_args, "_version"):
|
||||
log.warning(
|
||||
"load_args does not have a _version attribute, please file a bug to PyTorch "
|
||||
"and describe how you generate this repro script"
|
||||
)
|
||||
else:
|
||||
if load_args._version > 0:
|
||||
log.warning(
|
||||
"load_args is version %s, but this version of PyTorch only supports "
|
||||
"version 0. We will try to run it anyway but there may be an incompatibility; "
|
||||
"if so, try upgrading your version of PyTorch.",
|
||||
load_args._version,
|
||||
)
|
||||
|
||||
nop_reader = NopInputReader()
|
||||
load_args(nop_reader)
|
||||
|
||||
with tqdm(desc="Loading inputs", total=nop_reader.total) as pbar:
|
||||
input_reader = InputReader(save_dir=save_dir, pbar=pbar)
|
||||
load_args(input_reader)
|
||||
args = input_reader.args
|
||||
|
||||
return tuple(args)
|
||||
|
||||
|
||||
def repro_common(
|
||||
options: Any, exported_program: ExportedProgram
|
||||
) -> tuple[torch.fx.GraphModule, Any, Any]:
|
||||
# pyrefly: ignore [bad-assignment]
|
||||
torch._inductor.config.generate_intermediate_hooks = True
|
||||
mod = exported_program.module(check_guards=False)
|
||||
args, kwargs = exported_program.example_inputs
|
||||
return mod, args, kwargs # type: ignore[return-value]
|
||||
|
||||
|
||||
def repro_get_args(
|
||||
options: Any,
|
||||
exported_program: ExportedProgram,
|
||||
config_patches: dict[str, Any] | None,
|
||||
) -> tuple[torch.fx.GraphModule, Any, Any]:
|
||||
mod, args, kwargs = repro_common(options, exported_program)
|
||||
return mod, args, kwargs
|
||||
|
||||
|
||||
def repro_run(
|
||||
options: Any,
|
||||
exported_program: ExportedProgram,
|
||||
config_patches: dict[str, Any] | None,
|
||||
) -> None:
|
||||
from torch._inductor import _aoti_compile_and_package_inner
|
||||
|
||||
gm, args, kwargs = repro_common(options, exported_program)
|
||||
|
||||
from torch.cuda import synchronize
|
||||
|
||||
_aoti_compile_and_package_inner(
|
||||
gm,
|
||||
args,
|
||||
kwargs,
|
||||
load_and_run=True,
|
||||
check_accuracy=options.accuracy,
|
||||
inductor_configs=config_patches,
|
||||
)
|
||||
|
||||
need_sync = False
|
||||
|
||||
for arg in args:
|
||||
if isinstance(arg, torch.Tensor) and arg.is_cuda:
|
||||
need_sync = True
|
||||
break
|
||||
|
||||
if need_sync:
|
||||
synchronize() # ensure segfaults are surfaced
|
||||
|
||||
|
||||
def export_for_aoti_minifier(
|
||||
gm: torch.nn.Module,
|
||||
tuple_inputs: tuple[Any],
|
||||
strict: bool = False,
|
||||
skip_export_error: bool = True,
|
||||
) -> torch.nn.Module | None:
|
||||
# Some graphs cannot be used for AOTI/export (illegal graphs), these should be
|
||||
# considered as graphs that don't fail in the minifier, so the minifier keeps searching.
|
||||
# In these case, we return None. Otherwise, we return the exported graph module.
|
||||
# This won't affect the minifier result because the minifier is only responsible for catching
|
||||
# errors in AOTI, not export.
|
||||
#
|
||||
# Please add to this list of illegal graphs if you change the implementation here.
|
||||
# - graph output is not allowed by export
|
||||
#
|
||||
# If skip_export_error=True, then the errors in export will not be raised, and the minifier
|
||||
# will keep exploring and ignore this graph.
|
||||
from torch._dynamo.exc import UserError, UserErrorType
|
||||
|
||||
try:
|
||||
ep = torch.export.export(gm, tuple_inputs, strict=strict)
|
||||
gm = ep.module(check_guards=False)
|
||||
return gm
|
||||
except Exception as e:
|
||||
if skip_export_error:
|
||||
return None
|
||||
if isinstance(e, UserError) and e.error_type == UserErrorType.INVALID_OUTPUT:
|
||||
# graph output is not allowed by export when strict=True
|
||||
return None
|
||||
if isinstance(e, RuntimeError):
|
||||
# graph output is not allowed by export when strict=False
|
||||
pattern = r"Found .* in output, which is not a known type\."
|
||||
if re.search(pattern, str(e)) is not None:
|
||||
return None
|
||||
raise AOTIMinifierError(e) from e
|
||||
# we should never reach here
|
||||
# pyrefly: ignore [unreachable]
|
||||
return None
|
||||
|
||||
|
||||
def repro_minify(
|
||||
options: Any,
|
||||
exported_program: ExportedProgram,
|
||||
config_patches: dict[str, Any] | None,
|
||||
) -> None:
|
||||
from functorch.compile import minifier
|
||||
from torch._inductor import _aoti_compile_and_package_inner
|
||||
from torch._inductor.compile_fx import _aoti_flatten_inputs
|
||||
|
||||
mod, args, kwargs = repro_common(options, exported_program)
|
||||
|
||||
# update serialized_in_spec and serialized_out_spec
|
||||
flat_example_inputs, inductor_configs = _aoti_flatten_inputs(
|
||||
mod, args, kwargs, options=config_patches
|
||||
)
|
||||
compiler_name = "aot_inductor"
|
||||
assert options.minifier_export_mode in ["dynamo", "python"]
|
||||
strict = options.minifier_export_mode == "dynamo"
|
||||
skip_export_error = options.skip_export_error
|
||||
|
||||
from torch.cuda import synchronize
|
||||
|
||||
need_sync = False
|
||||
|
||||
for arg in args:
|
||||
if isinstance(arg, torch.Tensor) and arg.is_cuda:
|
||||
need_sync = True
|
||||
break
|
||||
|
||||
def module_fails(
|
||||
gm: torch.fx.GraphModule,
|
||||
flat_example_inputs: list[Any],
|
||||
check_str: str | None = None,
|
||||
) -> bool:
|
||||
# Need to export first so the in_spec and out_spec are populated
|
||||
tuple_inputs = tuple(flat_example_inputs)
|
||||
# pyrefly: ignore [bad-assignment]
|
||||
gm = export_for_aoti_minifier(
|
||||
gm, tuple_inputs, strict=strict, skip_export_error=skip_export_error
|
||||
)
|
||||
|
||||
# Some graphs cannot be used for AOTI/export (illegal graphs), these should be
|
||||
# considered as graphs that don't fail in the minifier, so the minifier keeps searching.
|
||||
if gm is None:
|
||||
return False
|
||||
|
||||
assert isinstance(gm, torch.fx.GraphModule)
|
||||
|
||||
try:
|
||||
_aoti_compile_and_package_inner(
|
||||
gm,
|
||||
tuple_inputs,
|
||||
load_and_run=True,
|
||||
check_accuracy=options.accuracy,
|
||||
inductor_configs=inductor_configs,
|
||||
)
|
||||
if need_sync:
|
||||
synchronize() # ensure segfaults are surfaced
|
||||
return False
|
||||
except Exception as e:
|
||||
if check_str is not None and check_str not in repr(e):
|
||||
return False
|
||||
return True
|
||||
|
||||
minifier(
|
||||
mod,
|
||||
flat_example_inputs,
|
||||
module_fails=functools.partial(module_fails, check_str=options.check_str),
|
||||
dump_state=functools.partial(
|
||||
dump_compiler_graph_state,
|
||||
compiler_name=compiler_name,
|
||||
config_patches=config_patches,
|
||||
accuracy=options.accuracy,
|
||||
strict=strict,
|
||||
),
|
||||
save_dir=options.save_dir,
|
||||
offload_to_disk=options.offload_to_disk,
|
||||
skip_offload=options.skip_saving_eager_intermediates,
|
||||
skip_sanity=options.skip_sanity,
|
||||
max_granularity=options.max_granularity,
|
||||
)
|
||||
|
||||
|
||||
def run_repro(
|
||||
exported_program: ExportedProgram,
|
||||
*,
|
||||
config_patches: dict[str, str] | None = None,
|
||||
command: str = "run",
|
||||
accuracy: bool | str = "",
|
||||
save_dir: str | None = None,
|
||||
tracing_mode: str | None = None,
|
||||
check_str: str | None = None,
|
||||
minifier_export_mode: str = "python",
|
||||
skip_export_error: bool = True,
|
||||
**more_kwargs: Any,
|
||||
) -> Any:
|
||||
for k in more_kwargs:
|
||||
log.warning(
|
||||
"Unrecognized kwarg %s; perhaps this repro was made on a newer version of PyTorch",
|
||||
k,
|
||||
)
|
||||
|
||||
if accuracy is True:
|
||||
accuracy = "accuracy"
|
||||
elif accuracy is False:
|
||||
accuracy = ""
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=f"""\
|
||||
An AOTI repro script, typically triggering a bug in PyTorch AOTInductor.
|
||||
When run with no arguments, this script defaults to running '{command}'.
|
||||
Extra flags may be available; to find out more, try '{command} --help'.
|
||||
There are also alternate subcommands available, see below.
|
||||
|
||||
default settings on this script:
|
||||
{accuracy=}
|
||||
{tracing_mode=}
|
||||
{save_dir=}
|
||||
{check_str=}
|
||||
""",
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
|
||||
def common_flags(parser: argparse.ArgumentParser) -> None:
|
||||
accuracy_group = parser.add_mutually_exclusive_group()
|
||||
accuracy_group.add_argument(
|
||||
"--no-accuracy",
|
||||
dest="accuracy",
|
||||
action="store_const",
|
||||
const="",
|
||||
default=accuracy,
|
||||
help="do not test accuracy, just run the module and see if it errors",
|
||||
)
|
||||
accuracy_group.add_argument(
|
||||
"--accuracy",
|
||||
action="store_const",
|
||||
const="accuracy",
|
||||
default=accuracy,
|
||||
help="""\
|
||||
test if the RMSE between the compiled module and the fp64 reference is greater
|
||||
than eager and the fp64 reference. This is usually more reliable than the
|
||||
standard allclose test, as we expect numeric differences from compiling, often
|
||||
improving accuracy over eager. RMSE test allows for compiled module to
|
||||
diverge greatly from eager, as long as this divergence moves it closer to the
|
||||
'true' mathematical value of the network. Caveats: (1) double precision can
|
||||
still suffer from rounding error, so it is not a perfect reference (see for
|
||||
example 'Herbie: Automatically Improving Floating Point Accuracy') for
|
||||
approaches that detect the necessary working precision and compute it in
|
||||
arbitrary precision floating point; unfortunately, this is not practical for
|
||||
tensor computation; (2) if there are not enough samples in the output being
|
||||
compared, we may get unlucky and have an unlucky greater RMSE than eager; this
|
||||
could be overcome by applying a more rigorous statistical test at some
|
||||
p-value, which we leave for future work.
|
||||
""",
|
||||
)
|
||||
accuracy_group.add_argument(
|
||||
"--strict-accuracy",
|
||||
dest="accuracy",
|
||||
action="store_const",
|
||||
const="strict_accuracy",
|
||||
default=accuracy,
|
||||
help="""\
|
||||
by default, when doing accuracy minification we will reject reductions which
|
||||
change the divergence from a floating point divergence to a integral/boolean
|
||||
divergence. This is because some operations like ReLU involve temporarily
|
||||
sharp boundaries that smooth out again afterwards; without requiring
|
||||
divergence on floating point, the minifier will often fixate on divergent
|
||||
boolean tensor even though this is not the true source of the divergence.
|
||||
However, rejecting these reductions makes it more difficult for the minifier
|
||||
to make process. Using this option will let the minifier progress for ALL
|
||||
divergences--you just might not end up with a useful repro in the end.""",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--save-dir",
|
||||
type=str,
|
||||
default=save_dir,
|
||||
metavar="DIR",
|
||||
help="directory where saved inputs live",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-save-dir",
|
||||
dest="save_dir",
|
||||
action="store_const",
|
||||
const=None,
|
||||
help="don't use any directory for saved inputs",
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(
|
||||
dest="command", metavar="{run,minify}", required=True
|
||||
)
|
||||
|
||||
parser_run = subparsers.add_parser(
|
||||
"run",
|
||||
help="just run the repro",
|
||||
)
|
||||
common_flags(parser_run)
|
||||
|
||||
parser_minify = subparsers.add_parser(
|
||||
"minify", help="run the minifier on the repro"
|
||||
)
|
||||
common_flags(parser_minify)
|
||||
parser_get_args = subparsers.add_parser("get_args", help="get the args")
|
||||
common_flags(parser_get_args)
|
||||
parser_minify.add_argument(
|
||||
"--skip-saving-eager-intermediates",
|
||||
action="store_true",
|
||||
help="skip saving eager intermediates on --minify",
|
||||
)
|
||||
parser_minify.add_argument(
|
||||
"--offload-to-disk",
|
||||
action="store_true",
|
||||
help="during minification, offload delta debugging intermediates to disk. Use if you're OOMing",
|
||||
)
|
||||
parser_minify.add_argument(
|
||||
"--skip-sanity",
|
||||
action="store_true",
|
||||
help="skip sanity check at beginning of minification on original graph",
|
||||
)
|
||||
parser_minify.add_argument(
|
||||
"--max-granularity",
|
||||
type=int,
|
||||
default=None,
|
||||
help="start at this granularity and work down; must be power of 2",
|
||||
)
|
||||
parser_minify.add_argument(
|
||||
"--check-str",
|
||||
type=str,
|
||||
default=check_str,
|
||||
help="require minified program to fail with error containing this string",
|
||||
)
|
||||
parser_minify.add_argument(
|
||||
"--minifier-export-mode",
|
||||
type=str,
|
||||
default=minifier_export_mode,
|
||||
help=(
|
||||
"The export mode used in minifier, either dynamo or python."
|
||||
"`dynamo` corresponds to strict=True, and `python` corresponds to strict=False."
|
||||
),
|
||||
)
|
||||
parser_minify.add_argument(
|
||||
"--skip-export-error",
|
||||
type=bool,
|
||||
default=skip_export_error,
|
||||
help="Skip intermediate graphs that cannot be exported.",
|
||||
)
|
||||
|
||||
# Run the repro in the context of minification, inverting exit code meaning
|
||||
parser_minifier_query = subparsers.add_parser(
|
||||
"minifier-query",
|
||||
)
|
||||
common_flags(parser_minifier_query)
|
||||
parser_minifier_query.add_argument(
|
||||
"--check-str",
|
||||
type=str,
|
||||
default=check_str,
|
||||
help="require minified program to fail with error containing this string",
|
||||
)
|
||||
|
||||
args = None
|
||||
if len(sys.argv) <= 1:
|
||||
args = [command, *sys.argv[1:]]
|
||||
|
||||
options = parser.parse_args(args)
|
||||
COMMAND_FNS = {
|
||||
"minify": repro_minify,
|
||||
"run": repro_run,
|
||||
"get_args": repro_get_args,
|
||||
}
|
||||
return COMMAND_FNS[options.command](
|
||||
options, exported_program, config_patches=config_patches
|
||||
)
|
||||
@@ -0,0 +1,750 @@
|
||||
"""
|
||||
This module provides functionality for resuming Python execution at specific points in code,
|
||||
primarily used by PyTorch Dynamo for control flow handling and optimization. It implements
|
||||
bytecode transformation and execution state management to enable:
|
||||
|
||||
- Resuming execution at arbitrary points in Python bytecode
|
||||
- Managing context managers and their state across execution boundaries
|
||||
- Transforming and generating new code objects with preserved execution state
|
||||
- Supporting Python 3.11+ exception handling and block management
|
||||
- Restoring torch function mode stacks and other execution context
|
||||
|
||||
The module is critical for PyTorch Dynamo's ability to optimize code while preserving
|
||||
Python semantics and execution state.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import dataclasses
|
||||
import sys
|
||||
import types
|
||||
from collections.abc import Callable, Iterable
|
||||
from contextlib import AbstractContextManager
|
||||
from typing import Any, cast
|
||||
|
||||
from .bytecode_transformation import (
|
||||
add_push_null,
|
||||
bytecode_from_template,
|
||||
create_binary_subscr,
|
||||
create_call_function,
|
||||
create_call_function_ex,
|
||||
create_instruction,
|
||||
create_jump_absolute,
|
||||
create_load_const,
|
||||
Instruction,
|
||||
overwrite_instruction,
|
||||
transform_code_object,
|
||||
unique_id,
|
||||
)
|
||||
from .utils import ExactWeakKeyDictionary
|
||||
|
||||
|
||||
# taken from code.h in cpython
|
||||
CO_OPTIMIZED = 0x0001
|
||||
CO_NEWLOCALS = 0x0002
|
||||
CO_VARARGS = 0x0004
|
||||
CO_VARKEYWORDS = 0x0008
|
||||
CO_NESTED = 0x0010
|
||||
CO_GENERATOR = 0x0020
|
||||
CO_NOFREE = 0x0040
|
||||
CO_COROUTINE = 0x0080
|
||||
CO_ITERABLE_COROUTINE = 0x0100
|
||||
CO_ASYNC_GENERATOR = 0x0200
|
||||
|
||||
# trace_rules.py import this constant for consistency
|
||||
TORCH_DYNAMO_RESUME_IN_PREFIX = "torch_dynamo_resume_in"
|
||||
IS_TRACING_RESUME_PROLOGUE_VARNAME = "__is_tracing_resume_prologue"
|
||||
|
||||
|
||||
# If is_resume - this codegen is for a resume function
|
||||
def _initial_push_null(insts: list[Instruction]) -> None:
|
||||
if sys.version_info >= (3, 11):
|
||||
insts.append(create_instruction("PUSH_NULL"))
|
||||
if sys.version_info < (3, 13):
|
||||
insts.append(create_instruction("SWAP", arg=2))
|
||||
|
||||
|
||||
# Generates bytecode from template and splits the code where LOAD_FAST dummy is present.
|
||||
def _bytecode_from_template_with_split(
|
||||
template: Callable[..., Any],
|
||||
stack_index: int,
|
||||
varname_map: dict[str, Any] | None = None,
|
||||
) -> tuple[list[Instruction], list[Instruction]]:
|
||||
template_code = bytecode_from_template(template, varname_map=varname_map)
|
||||
template_code.append(create_instruction("POP_TOP"))
|
||||
|
||||
# adjust exception table entry depth
|
||||
for inst in template_code:
|
||||
if inst.exn_tab_entry:
|
||||
inst.exn_tab_entry.depth += stack_index
|
||||
|
||||
# search for LOAD_FAST dummy and replace it with 2 NOPs (we can break up the bytecode between them)
|
||||
dummy_idx, dummy_inst = next(
|
||||
(
|
||||
(i, inst)
|
||||
for i, inst in enumerate(template_code)
|
||||
if inst.opname in ("LOAD_FAST", "LOAD_FAST_BORROW")
|
||||
and inst.argval == "dummy"
|
||||
),
|
||||
(None, None),
|
||||
)
|
||||
assert dummy_idx is not None and dummy_inst is not None
|
||||
|
||||
# replace LOAD_FAST dummy with first NOP marking exception area
|
||||
overwrite_instruction(dummy_inst, [create_instruction("NOP")])
|
||||
|
||||
# POP_TOP follows LOAD_FAST dummy - replace with NOP marking end of exception area
|
||||
assert template_code[dummy_idx + 1].opname == "POP_TOP"
|
||||
overwrite_instruction(template_code[dummy_idx + 1], [create_instruction("NOP")])
|
||||
|
||||
return template_code[: dummy_idx + 1], template_code[dummy_idx + 1 :]
|
||||
|
||||
|
||||
def _try_except_tf_mode_template(dummy: Any, stack_var_name: Any) -> None:
|
||||
# NOTE: Make sure this name matches what is generated by symbolic_convert:import_source
|
||||
# on torch._dynamo.utils.
|
||||
# pyrefly: ignore [unknown-name]
|
||||
global __import_torch_dot__dynamo_dot_utils
|
||||
try:
|
||||
dummy
|
||||
except: # noqa: E722, B001
|
||||
__import_torch_dot__dynamo_dot_utils.set_torch_function_mode_stack( # type: ignore[name-defined]
|
||||
stack_var_name
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class ReenterWith:
|
||||
stack_index: int
|
||||
target_values: tuple[Any, ...] | None = None
|
||||
|
||||
def try_except_torch_function_mode(
|
||||
self, code_options: dict[str, Any], cleanup: list[Instruction]
|
||||
) -> list[Instruction]:
|
||||
"""
|
||||
Codegen based off of:
|
||||
try:
|
||||
(rest)
|
||||
except:
|
||||
(restore previous tf mode stack)
|
||||
raise
|
||||
"""
|
||||
from .variables.torch_function import get_prev_stack_var_name
|
||||
|
||||
setup_try_except, epilogue = _bytecode_from_template_with_split(
|
||||
_try_except_tf_mode_template,
|
||||
self.stack_index,
|
||||
varname_map={"stack_var_name": get_prev_stack_var_name()},
|
||||
)
|
||||
cleanup[:] = epilogue + cleanup
|
||||
|
||||
return setup_try_except
|
||||
|
||||
# If we do not want to destroy the stack, we can do the same thing as a
|
||||
# `SETUP_WITH` block, only that we store the context manager in a local_symbol
|
||||
def try_finally(
|
||||
self, code_options: dict[str, Any], cleanup: list[Instruction]
|
||||
) -> list[Instruction]:
|
||||
"""
|
||||
Codegen based off of:
|
||||
load args
|
||||
enter context
|
||||
try:
|
||||
(rest)
|
||||
finally:
|
||||
exit context
|
||||
"""
|
||||
# NOTE: we assume that TOS is a context manager CLASS!
|
||||
# pyrefly: ignore [implicit-any]
|
||||
load_args = []
|
||||
if self.target_values:
|
||||
load_args = [create_load_const(val) for val in self.target_values]
|
||||
ctx_name = unique_id(f"___context_manager_{self.stack_index}")
|
||||
if ctx_name not in code_options["co_varnames"]:
|
||||
code_options["co_varnames"] += (ctx_name,)
|
||||
for name in ["__enter__", "__exit__"]:
|
||||
if name not in code_options["co_names"]:
|
||||
code_options["co_names"] += (name,)
|
||||
|
||||
create_ctx: list[Instruction] = []
|
||||
_initial_push_null(create_ctx)
|
||||
create_ctx.extend(
|
||||
[
|
||||
*load_args,
|
||||
*create_call_function(len(load_args), False),
|
||||
create_instruction("STORE_FAST", argval=ctx_name),
|
||||
]
|
||||
)
|
||||
|
||||
def _template(ctx: AbstractContextManager[Any], dummy: Any) -> None:
|
||||
ctx.__enter__()
|
||||
try:
|
||||
dummy
|
||||
finally:
|
||||
ctx.__exit__(None, None, None)
|
||||
|
||||
setup_try_finally, epilogue = _bytecode_from_template_with_split(
|
||||
_template, self.stack_index, varname_map={"ctx": ctx_name}
|
||||
)
|
||||
cleanup[:] = epilogue + cleanup
|
||||
return create_ctx + setup_try_finally
|
||||
|
||||
def __call__(
|
||||
self, code_options: dict[str, Any], cleanup: list[Instruction]
|
||||
) -> tuple[list[Instruction], Instruction | None]:
|
||||
"""
|
||||
Codegen based off of:
|
||||
with ctx(args):
|
||||
(rest)
|
||||
"""
|
||||
# NOTE: we assume that TOS is a context manager CLASS!
|
||||
# pyrefly: ignore [implicit-any]
|
||||
load_args = []
|
||||
if self.target_values:
|
||||
load_args = [create_load_const(val) for val in self.target_values]
|
||||
|
||||
create_ctx: list[Instruction] = []
|
||||
# Do not push NULL in Python 3.14+ since the NULL should be on the symbolic stack.
|
||||
if sys.version_info < (3, 14):
|
||||
_initial_push_null(create_ctx)
|
||||
create_ctx.extend(
|
||||
[
|
||||
*load_args,
|
||||
*create_call_function(len(load_args), False),
|
||||
]
|
||||
)
|
||||
|
||||
def _template(ctx: AbstractContextManager[Any], dummy: Any) -> None:
|
||||
with ctx:
|
||||
dummy
|
||||
|
||||
setup_with, epilogue = _bytecode_from_template_with_split(
|
||||
_template, self.stack_index
|
||||
)
|
||||
cleanup[:] = epilogue + cleanup
|
||||
|
||||
load_fast_ctx_inst = next(
|
||||
(
|
||||
inst
|
||||
for inst in setup_with
|
||||
if inst.opname in ("LOAD_FAST", "LOAD_FAST_BORROW")
|
||||
and inst.argval == "ctx"
|
||||
),
|
||||
None,
|
||||
)
|
||||
assert load_fast_ctx_inst is not None
|
||||
# ctx already loaded on stack before the template - no need to LOAD_FAST
|
||||
overwrite_instruction(load_fast_ctx_inst, [create_instruction("NOP")])
|
||||
|
||||
# 3.11+ only
|
||||
push_exc_info_gen = (
|
||||
inst for inst in epilogue if inst.opname == "PUSH_EXC_INFO"
|
||||
)
|
||||
push_exc_info_inst = next(push_exc_info_gen, None)
|
||||
# expect only 1 PUSH_EXC_INFO in epilogue
|
||||
assert next(push_exc_info_gen, None) is None
|
||||
|
||||
return create_ctx + setup_with, push_exc_info_inst
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ResumeFunctionMetadata:
|
||||
code: types.CodeType
|
||||
instructions: list[Instruction] = dataclasses.field(default_factory=list)
|
||||
# Python 3.11+ fields
|
||||
# NOTE: Python 3.11 removed blocks, but for our purposes, a "block" consists
|
||||
# of instructions of all exception table entries that have the same target.
|
||||
|
||||
# map from PUSH_EXC_INFO's in the prefix to original block target offset
|
||||
prefix_block_target_offset_remap: list[int] = dataclasses.field(
|
||||
default_factory=list
|
||||
)
|
||||
# per-offset map from new block target offsets to original block target offsets
|
||||
block_target_offset_remap: dict[tuple[int, int], dict[int, int]] = (
|
||||
dataclasses.field(default_factory=dict)
|
||||
)
|
||||
|
||||
|
||||
def _filter_iter(
|
||||
l1: Iterable[Any],
|
||||
l2: Iterable[Any],
|
||||
cond: Callable[[Any, Any], bool],
|
||||
) -> list[Any]:
|
||||
"""
|
||||
Two-pointer conditional filter.
|
||||
e.g. _filter_iter(insts, sorted_offsets, lambda i, o: i.offset == o)
|
||||
returns the instructions with offsets in sorted_offsets
|
||||
"""
|
||||
it = iter(l2)
|
||||
res: list[Instruction] = []
|
||||
try:
|
||||
cur = next(it)
|
||||
for val in l1:
|
||||
if cond(val, cur):
|
||||
res.append(val)
|
||||
cur = next(it)
|
||||
except StopIteration:
|
||||
pass
|
||||
return res
|
||||
|
||||
|
||||
def _load_tuple_and_call(tup: tuple[Any, ...]) -> list[Instruction]:
|
||||
insts: list[Instruction] = []
|
||||
_initial_push_null(insts)
|
||||
insts.extend(create_load_const(val) for val in tup)
|
||||
insts.extend(create_call_function(len(tup), False))
|
||||
return insts
|
||||
|
||||
|
||||
class ContinueExecutionCache:
|
||||
cache = ExactWeakKeyDictionary()
|
||||
generated_code_metadata = ExactWeakKeyDictionary()
|
||||
|
||||
@classmethod
|
||||
def lookup(
|
||||
cls, code: types.CodeType, lineno: int, init_offset: int, *key: Any
|
||||
) -> types.CodeType:
|
||||
if code not in cls.cache:
|
||||
cls.cache[code] = {}
|
||||
key = tuple(key)
|
||||
if key not in cls.cache[code]:
|
||||
cls.cache[code][key] = cls.generate(code, lineno, init_offset, *key)
|
||||
return cls.cache[code][key]
|
||||
|
||||
@classmethod
|
||||
def generate(
|
||||
cls,
|
||||
code: types.CodeType,
|
||||
lineno: int,
|
||||
init_offset: int,
|
||||
resume_offset: int,
|
||||
setup_fn_target_offsets: tuple[int, ...], # only used in Python 3.11+
|
||||
nstack: int,
|
||||
argnames: tuple[str, ...],
|
||||
argnames_null: tuple[str, ...],
|
||||
setup_fns: tuple[ReenterWith, ...],
|
||||
handle_inactive_ctx: bool,
|
||||
stack_ctx_vars: tuple[tuple[int, tuple[Any, ...]], ...],
|
||||
argnames_ctx_vars: tuple[tuple[str, tuple[Any, ...]], ...],
|
||||
null_idxes: tuple[int, ...],
|
||||
# mainly used to ensure distinct code objects per stack trace,
|
||||
# which prevents excessive recompilation of inner frames
|
||||
nested_code_objs: tuple[types.CodeType],
|
||||
# Are we currently graph breaking on an instruction that doesn't push
|
||||
# its result to the stack? If so, and we are not the leaf resume, then we need to pop
|
||||
# the result of calling the next resume function.
|
||||
pop_nested_resume_result: bool,
|
||||
) -> types.CodeType:
|
||||
assert resume_offset is not None
|
||||
assert not (
|
||||
code.co_flags
|
||||
& (CO_GENERATOR | CO_COROUTINE | CO_ITERABLE_COROUTINE | CO_ASYNC_GENERATOR)
|
||||
)
|
||||
assert code.co_flags & CO_OPTIMIZED
|
||||
if code in ContinueExecutionCache.generated_code_metadata:
|
||||
return cls.generate_based_on_original_code_object(
|
||||
code,
|
||||
lineno,
|
||||
init_offset,
|
||||
resume_offset,
|
||||
setup_fn_target_offsets,
|
||||
nstack,
|
||||
argnames,
|
||||
argnames_null,
|
||||
setup_fns,
|
||||
handle_inactive_ctx,
|
||||
stack_ctx_vars,
|
||||
argnames_ctx_vars,
|
||||
null_idxes,
|
||||
nested_code_objs,
|
||||
pop_nested_resume_result,
|
||||
)
|
||||
|
||||
is_py311_plus = sys.version_info >= (3, 11)
|
||||
meta = ResumeFunctionMetadata(code)
|
||||
|
||||
def update(
|
||||
instructions: list[Instruction], code_options: dict[str, Any]
|
||||
) -> None:
|
||||
meta.instructions = copy.deepcopy(instructions)
|
||||
|
||||
args = ["__nested_resume_fns", "__nested_frame_values"]
|
||||
args += [f"___stack{i}" for i in range(nstack)]
|
||||
args.extend(v for v in argnames if v not in args)
|
||||
freevars = tuple(code_options["co_cellvars"] or []) + tuple(
|
||||
code_options["co_freevars"] or []
|
||||
)
|
||||
freevars = tuple(sorted(freevars))
|
||||
code_options["co_name"] = (
|
||||
f"{TORCH_DYNAMO_RESUME_IN_PREFIX}_{code_options['co_name']}_at_{lineno}"
|
||||
)
|
||||
if is_py311_plus:
|
||||
qualified_path = code_options["co_qualname"].rsplit(".", maxsplit=1)
|
||||
if len(qualified_path) == 1:
|
||||
code_options["co_qualname"] = code_options["co_name"]
|
||||
else:
|
||||
assert len(qualified_path) == 2
|
||||
module_name, co_name = qualified_path
|
||||
code_options["co_qualname"] = (
|
||||
f"{module_name}.{TORCH_DYNAMO_RESUME_IN_PREFIX}_{co_name}_at_{lineno}"
|
||||
)
|
||||
code_options["co_firstlineno"] = lineno
|
||||
code_options["co_cellvars"] = ()
|
||||
code_options["co_freevars"] = freevars
|
||||
code_options["co_argcount"] = len(args)
|
||||
code_options["co_posonlyargcount"] = 0
|
||||
code_options["co_kwonlyargcount"] = 0
|
||||
code_options["co_varnames"] = tuple(
|
||||
args
|
||||
+ [v for v in argnames_null if v not in args]
|
||||
+ [v for v in code_options["co_varnames"] if v not in args]
|
||||
+ [IS_TRACING_RESUME_PROLOGUE_VARNAME]
|
||||
)
|
||||
code_options["co_flags"] = code_options["co_flags"] & ~(
|
||||
CO_VARARGS | CO_VARKEYWORDS
|
||||
)
|
||||
target = next(i for i in instructions if i.offset == resume_offset)
|
||||
|
||||
prefix = []
|
||||
if is_py311_plus:
|
||||
if freevars:
|
||||
prefix.append(
|
||||
create_instruction("COPY_FREE_VARS", arg=len(freevars))
|
||||
)
|
||||
prefix.append(create_instruction("RESUME", arg=0))
|
||||
|
||||
# Set is_tracing_resume_prologue to prevent graph breaks.
|
||||
# This doesn't really do anything at runtime, but dynamo will trace this
|
||||
# and will know that we're in a resume function prologue.
|
||||
prefix.extend(
|
||||
[
|
||||
create_instruction("LOAD_CONST", argval=True),
|
||||
create_instruction(
|
||||
"STORE_FAST", argval=IS_TRACING_RESUME_PROLOGUE_VARNAME
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
cleanup: list[Instruction] = []
|
||||
hooks = {fn.stack_index: fn for fn in setup_fns}
|
||||
hook_target_offsets = {
|
||||
fn.stack_index: setup_fn_target_offsets[i]
|
||||
for i, fn in enumerate(setup_fns)
|
||||
}
|
||||
offset_to_inst = {inst.offset: inst for inst in instructions}
|
||||
# map old hook targets to new targets generated by the hook
|
||||
# pyrefly: ignore [implicit-any]
|
||||
old_hook_target_remap = {}
|
||||
stack_i = 0
|
||||
null_i = 0
|
||||
stack_ctx_vars_d = dict(stack_ctx_vars) # type: ignore[var-annotated,arg-type]
|
||||
for i in range(nstack + len(null_idxes)):
|
||||
if null_i < len(null_idxes) and null_idxes[null_i] == i:
|
||||
prefix.append(create_instruction("PUSH_NULL"))
|
||||
null_i += 1
|
||||
else:
|
||||
prefix.append(
|
||||
create_instruction("LOAD_FAST", argval=f"___stack{stack_i}")
|
||||
)
|
||||
if handle_inactive_ctx and stack_i in stack_ctx_vars_d:
|
||||
# NOTE: we assume that current stack var is a context manager CLASS!
|
||||
# Load args for context variable and construct it
|
||||
prefix.extend(_load_tuple_and_call(stack_ctx_vars_d[stack_i]))
|
||||
stack_i += 1
|
||||
|
||||
if i in hooks:
|
||||
hook = hooks.pop(i)
|
||||
hook_insts, exn_target = hook(code_options, cleanup)
|
||||
prefix.extend(hook_insts)
|
||||
if is_py311_plus:
|
||||
hook_target_offset = hook_target_offsets.pop(i)
|
||||
old_hook_target = offset_to_inst[hook_target_offset]
|
||||
meta.prefix_block_target_offset_remap.append(hook_target_offset)
|
||||
old_hook_target_remap[old_hook_target] = exn_target
|
||||
|
||||
if is_py311_plus:
|
||||
# reverse the mapping since targets of later/nested contexts are inserted
|
||||
# into the mapping later, but show up earlier in the prefix.
|
||||
meta.prefix_block_target_offset_remap = list(
|
||||
reversed(meta.prefix_block_target_offset_remap)
|
||||
)
|
||||
|
||||
assert not hooks
|
||||
|
||||
# NOTE: we assume that local var is a context manager CLASS!
|
||||
# initialize inactive context vars in argnames
|
||||
if handle_inactive_ctx:
|
||||
for name, vals in argnames_ctx_vars:
|
||||
prefix.append(create_instruction("LOAD_FAST", argval=name))
|
||||
prefix.extend(_load_tuple_and_call(vals))
|
||||
prefix.append(create_instruction("STORE_FAST", argval=name))
|
||||
|
||||
# 3.12+: store NULL into variables that were NULL
|
||||
if argnames_null:
|
||||
assert sys.version_info >= (3, 12)
|
||||
for v in argnames_null:
|
||||
assert v not in args
|
||||
prefix.extend(
|
||||
[
|
||||
create_instruction("PUSH_NULL"),
|
||||
create_instruction("STORE_FAST", argval=v),
|
||||
]
|
||||
)
|
||||
|
||||
# Call nested resume function
|
||||
if nested_code_objs:
|
||||
prefix.extend(
|
||||
[
|
||||
# set up __nested_resume_fns[-1] call
|
||||
*add_push_null(
|
||||
[
|
||||
create_instruction(
|
||||
"LOAD_FAST", argval="__nested_resume_fns"
|
||||
),
|
||||
create_instruction("LOAD_CONST", argval=-1),
|
||||
create_binary_subscr(),
|
||||
]
|
||||
),
|
||||
# del __nested_resume_fns[-1]
|
||||
create_instruction("LOAD_FAST", argval="__nested_resume_fns"),
|
||||
create_instruction("LOAD_CONST", argval=-1),
|
||||
create_instruction("DELETE_SUBSCR"),
|
||||
# load [__nested_resume_fns, __nested_frame_values]
|
||||
create_instruction("LOAD_FAST", argval="__nested_resume_fns"),
|
||||
create_instruction("LOAD_FAST", argval="__nested_frame_values"),
|
||||
create_instruction("BUILD_LIST", arg=2),
|
||||
# load __nested_frame_values[-1]
|
||||
create_instruction("LOAD_FAST", argval="__nested_frame_values"),
|
||||
create_instruction("LOAD_CONST", argval=-1),
|
||||
create_binary_subscr(),
|
||||
# create [
|
||||
# __nested_resume_fns,
|
||||
# __nested_frame_values,
|
||||
# *__nested_frame_values[-1],
|
||||
# ]
|
||||
create_instruction("LIST_EXTEND", arg=1),
|
||||
# del __nested_frame_values[-1]
|
||||
create_instruction("LOAD_FAST", argval="__nested_frame_values"),
|
||||
create_instruction("LOAD_CONST", argval=-1),
|
||||
create_instruction("DELETE_SUBSCR"),
|
||||
# delete __nested values
|
||||
create_instruction("DELETE_FAST", argval="__nested_resume_fns"),
|
||||
create_instruction(
|
||||
"DELETE_FAST", argval="__nested_frame_values"
|
||||
),
|
||||
# Set is_tracing_resume_prologue back to allow graph breaks
|
||||
# in the nested resume
|
||||
create_instruction("LOAD_CONST", argval=False),
|
||||
create_instruction(
|
||||
"STORE_FAST", argval=IS_TRACING_RESUME_PROLOGUE_VARNAME
|
||||
),
|
||||
# finish the call
|
||||
*create_call_function_ex(False, False),
|
||||
]
|
||||
)
|
||||
if pop_nested_resume_result:
|
||||
# pop the result of calling the nested resume function
|
||||
prefix.append(create_instruction("POP_TOP"))
|
||||
else:
|
||||
# Set is_tracing_resume_prologue back to allow graph breaks after the jump
|
||||
prefix.extend(
|
||||
[
|
||||
create_instruction("LOAD_CONST", argval=False),
|
||||
create_instruction(
|
||||
"STORE_FAST", argval=IS_TRACING_RESUME_PROLOGUE_VARNAME
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
prefix.append(create_jump_absolute(target))
|
||||
|
||||
# because the line number table monotonically increases from co_firstlineno
|
||||
# remove starts_line for any instructions before the graph break instruction
|
||||
# this will ensure the instructions after the break have the correct line numbers
|
||||
for inst in instructions:
|
||||
if inst.offset == target.offset:
|
||||
break
|
||||
inst.starts_line = None
|
||||
if sys.version_info >= (3, 11):
|
||||
inst.positions = None
|
||||
|
||||
if cleanup:
|
||||
prefix.extend(cleanup)
|
||||
prefix.extend(cls.unreachable_codes(code_options))
|
||||
|
||||
# remap original instructions' exception table entries
|
||||
if old_hook_target_remap:
|
||||
# pyrefly: ignore [unbound-name]
|
||||
assert is_py311_plus
|
||||
for inst in instructions:
|
||||
if (
|
||||
inst.exn_tab_entry
|
||||
and inst.exn_tab_entry.target in old_hook_target_remap
|
||||
):
|
||||
inst.exn_tab_entry.target = old_hook_target_remap[ # type: ignore[assignment]
|
||||
inst.exn_tab_entry.target
|
||||
]
|
||||
|
||||
# TODO(jansel): add dead code elimination here
|
||||
instructions[:] = prefix + instructions
|
||||
|
||||
new_code, _ = transform_code_object(code, update)
|
||||
ContinueExecutionCache.generated_code_metadata[new_code] = meta
|
||||
return new_code
|
||||
|
||||
@staticmethod
|
||||
def unreachable_codes(code_options: dict[str, Any]) -> list[Instruction]:
|
||||
"""Codegen a `raise None` to make analysis work for unreachable code"""
|
||||
return [
|
||||
create_load_const(None),
|
||||
create_instruction("RAISE_VARARGS", arg=1),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def generate_based_on_original_code_object(
|
||||
cls,
|
||||
code: types.CodeType,
|
||||
lineno: int,
|
||||
init_offset: int,
|
||||
resume_offset: int,
|
||||
setup_fn_target_offsets: tuple[int, ...],
|
||||
*args: Any,
|
||||
) -> types.CodeType:
|
||||
"""
|
||||
This handles the case of generating a resume into code generated
|
||||
to resume something else. We want to always generate starting
|
||||
from the original code object so that if control flow paths
|
||||
converge we only generated 1 resume function (rather than 2^n
|
||||
resume functions).
|
||||
"""
|
||||
|
||||
meta: ResumeFunctionMetadata = ContinueExecutionCache.generated_code_metadata[
|
||||
code
|
||||
]
|
||||
|
||||
def find_orig_offset(cur_offset: int) -> int:
|
||||
orig_offset = -1
|
||||
|
||||
def find_orig_offset_transform(
|
||||
instructions: list[Instruction], code_options: dict[str, Any]
|
||||
) -> None:
|
||||
nonlocal orig_offset
|
||||
(target,) = (i for i in instructions if i.offset == cur_offset)
|
||||
# match the functions starting at the last instruction as we have added a prefix
|
||||
new_target_tuple = tuple(
|
||||
i2
|
||||
for i1, i2 in zip(
|
||||
reversed(instructions), reversed(meta.instructions)
|
||||
)
|
||||
if i1 is target
|
||||
)
|
||||
|
||||
if not new_target_tuple:
|
||||
# Instruction with cur_offset in instructions was not found
|
||||
# in the original code - orig_offset left as -1.
|
||||
# Caller expected to handle this case.
|
||||
return
|
||||
|
||||
assert len(new_target_tuple) == 1
|
||||
new_target = new_target_tuple[0]
|
||||
|
||||
assert target.opcode == new_target.opcode
|
||||
assert new_target.offset is not None
|
||||
orig_offset = new_target.offset
|
||||
|
||||
transform_code_object(code, find_orig_offset_transform)
|
||||
return orig_offset
|
||||
|
||||
orig_init_offset = find_orig_offset(init_offset)
|
||||
# It is fine if the initial instruction is not found in the original code;
|
||||
# this means we graph broke in the prefix, which only happens with nested graph breaks.
|
||||
# We should not be running into ambiguous graph break issues here.
|
||||
orig_resume_offset = find_orig_offset(resume_offset)
|
||||
assert orig_resume_offset > -1, (
|
||||
"resume instruction not found in original code - this is a bug."
|
||||
)
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
# setup_fn_target_offsets currently contains the target offset of
|
||||
# each setup_fn, based on `code`. When we codegen the resume function
|
||||
# based on the original code object, `meta.code`, the offsets in
|
||||
# setup_fn_target_offsets must be based on `meta.code` instead.
|
||||
offset_key = (orig_init_offset, orig_resume_offset)
|
||||
# NOTE: we key by offset_key since the same resume function may graph
|
||||
# break in multiple places and we need different block_target_offset_remap's
|
||||
# for each graph break location. Keying by orig_resume_offset may not be enough
|
||||
# if 2 graph breaks on different initial offsets resume on the same instruction
|
||||
# (although this is rare and not tested anywhere).
|
||||
if offset_key not in meta.block_target_offset_remap:
|
||||
block_target_offset_remap = meta.block_target_offset_remap[
|
||||
offset_key
|
||||
# pyrefly: ignore [implicit-any]
|
||||
] = {}
|
||||
|
||||
def remap_block_offsets(
|
||||
instructions: list[Instruction], code_options: dict[str, Any]
|
||||
) -> None:
|
||||
# NOTE: each prefix block generates exactly one PUSH_EXC_INFO,
|
||||
# so we can tell which block a prefix PUSH_EXC_INFO belongs to,
|
||||
# by counting. Then we can use meta.prefix_block_target_offset_remap
|
||||
# to determine where in the original code the PUSH_EXC_INFO offset
|
||||
# replaced.
|
||||
prefix_blocks: list[Instruction] = []
|
||||
for inst in instructions:
|
||||
# NOTE meta.prefix_block_target_offset_remap is based off of how we codegen'd
|
||||
# context managers at the prefix/prologue of the resume function. It is the same for
|
||||
# every graph break in the same resume function, so we do not need to recompute
|
||||
# for each graph break (unlike for meta.block_target_offset_remap)
|
||||
if len(prefix_blocks) == len(
|
||||
meta.prefix_block_target_offset_remap
|
||||
):
|
||||
break
|
||||
if inst.opname == "PUSH_EXC_INFO":
|
||||
prefix_blocks.append(inst)
|
||||
|
||||
# remap block target offsets for blocks generated in the resume prefix
|
||||
for inst, o in zip(
|
||||
prefix_blocks, meta.prefix_block_target_offset_remap
|
||||
):
|
||||
block_target_offset_remap[cast(int, inst.offset)] = o
|
||||
|
||||
# current bytecode targets are after the prefix PUSH_EXC_INFO's
|
||||
cur_start_offset = (
|
||||
cast(int, prefix_blocks[-1].offset) if prefix_blocks else -1
|
||||
)
|
||||
# get the remaining block target offsets of the current bytecode
|
||||
cur_inst_offsets = sorted(
|
||||
n for n in setup_fn_target_offsets if n > cur_start_offset
|
||||
)
|
||||
targets = _filter_iter(
|
||||
instructions, cur_inst_offsets, lambda inst, o: inst.offset == o
|
||||
)
|
||||
# The original code and resume code should have matching suffixes.
|
||||
# Match the post-prefix block target offsets of the current resume code
|
||||
# and the original code.
|
||||
orig_targets = reversed(
|
||||
_filter_iter(
|
||||
zip(reversed(instructions), reversed(meta.instructions)),
|
||||
reversed(targets),
|
||||
lambda v1, v2: v1[0] is v2,
|
||||
)
|
||||
)
|
||||
for orig, cur in zip(orig_targets, targets):
|
||||
block_target_offset_remap[cur.offset] = orig[1].offset
|
||||
|
||||
transform_code_object(code, remap_block_offsets)
|
||||
|
||||
# if offset_key or offset is not in setup_fn_target_offsets, it is an error
|
||||
# that needs to be fixed
|
||||
setup_fn_target_offsets = tuple(
|
||||
meta.block_target_offset_remap[offset_key][n]
|
||||
for n in setup_fn_target_offsets
|
||||
)
|
||||
return ContinueExecutionCache.lookup(
|
||||
meta.code,
|
||||
lineno,
|
||||
orig_init_offset,
|
||||
orig_resume_offset,
|
||||
setup_fn_target_offsets,
|
||||
*args,
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,70 @@
|
||||
"""This module implements tensor version operations for Dynamo tracing.
|
||||
|
||||
It provides primitives for handling tensor versioning during tracing, particularly in the
|
||||
context of functionalization where version operations are handled eagerly on fake tensors.
|
||||
|
||||
When we functionalize _tensor_version + _unsafe_set_version_counter, the ops disappear from
|
||||
the traced graph. We run them eagerly on the fake tensors used for tracing, in order to get
|
||||
past asserts that would fail in autograd.
|
||||
|
||||
Why is this ok?
|
||||
1) Versions on functional tensors do not make any sense since you cannot mutate a functional
|
||||
tensor.
|
||||
2) The whole point of version munging is to trick autograd into doing what we want, and after
|
||||
AotAutograd there is no longer any need for these ops.
|
||||
|
||||
Note this is similar to how no_grad is handled.
|
||||
"""
|
||||
|
||||
from contextlib import AbstractContextManager
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from torch import SymInt
|
||||
from torch._prims import _make_prim, RETURN_TYPE
|
||||
from torch._subclasses import FakeTensorMode
|
||||
from torch._subclasses.functional_tensor import FunctionalTensorMode
|
||||
|
||||
|
||||
_tensor_version = _make_prim(
|
||||
schema="_tensor_version(Tensor self) -> SymInt",
|
||||
return_type=RETURN_TYPE.NEW,
|
||||
meta=torch.ops.aten._version.default,
|
||||
impl_aten=torch.ops.aten._version.default,
|
||||
doc="Tracable unbacked SymInt version of torch.Tensor._version",
|
||||
)
|
||||
|
||||
|
||||
@_tensor_version.py_impl(FakeTensorMode) # type: ignore[misc]
|
||||
def _tensor_version_fake(fake_mode: FakeTensorMode, self_tensor: Any) -> SymInt:
|
||||
"""
|
||||
The initial dynamo capture of _tensor_version + _unsafe_set_version_counter turns the
|
||||
`._version` into an unbacked SymInt so that we don't need to specialize on the `._version`
|
||||
of input tensors to the graph.
|
||||
"""
|
||||
assert fake_mode.shape_env is not None
|
||||
return fake_mode.shape_env.create_unbacked_symint()
|
||||
|
||||
|
||||
_unsafe_set_version_counter = _make_prim(
|
||||
schema="_unsafe_set_version_counter(Tensor[] tensors, SymInt[] versions) -> ()",
|
||||
return_type=RETURN_TYPE.NEW,
|
||||
meta=lambda self, version: None,
|
||||
impl_aten=torch._C._autograd._unsafe_set_version_counter,
|
||||
doc="Tracable+SymInt version of torch._C._autograd._unsafe_set_version_counter",
|
||||
)
|
||||
torch.fx.node.has_side_effect(_unsafe_set_version_counter)
|
||||
|
||||
|
||||
@_tensor_version.py_impl(FunctionalTensorMode) # type: ignore[misc]
|
||||
def _tensor_version_functional(mode: FunctionalTensorMode, self: Any) -> int:
|
||||
return self._version
|
||||
|
||||
|
||||
@_unsafe_set_version_counter.py_impl(FunctionalTensorMode) # type: ignore[misc]
|
||||
def _unsafe_set_version_counter_functional(
|
||||
ctx: AbstractContextManager[Any],
|
||||
tensors: tuple[torch.Tensor, ...],
|
||||
versions: tuple[int, ...],
|
||||
) -> None:
|
||||
torch._C._autograd._unsafe_set_version_counter(tensors, versions)
|
||||
@@ -0,0 +1,235 @@
|
||||
"""Testing utilities for Dynamo, providing a specialized TestCase class and test running functionality.
|
||||
|
||||
This module extends PyTorch's testing framework with Dynamo-specific testing capabilities.
|
||||
It includes:
|
||||
- A custom TestCase class that handles Dynamo-specific setup/teardown
|
||||
- Test running utilities with dependency checking
|
||||
- Automatic reset of Dynamo state between tests
|
||||
- Proper handling of gradient mode state
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import importlib
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.testing
|
||||
from torch._dynamo import polyfills
|
||||
from torch._logging._internal import trace_log
|
||||
from torch.testing._internal.common_utils import ( # type: ignore[attr-defined]
|
||||
IS_WINDOWS,
|
||||
TEST_WITH_CROSSREF,
|
||||
TEST_WITH_TORCHDYNAMO,
|
||||
TestCase as TorchTestCase,
|
||||
)
|
||||
|
||||
from . import config, reset, utils
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def run_tests(needs: str | tuple[str, ...] = ()) -> None:
|
||||
from torch.testing._internal.common_utils import run_tests
|
||||
|
||||
if TEST_WITH_TORCHDYNAMO or TEST_WITH_CROSSREF:
|
||||
return # skip testing
|
||||
|
||||
if (
|
||||
not torch.xpu.is_available()
|
||||
and IS_WINDOWS
|
||||
and os.environ.get("TORCHINDUCTOR_WINDOWS_TESTS", "0") == "0"
|
||||
):
|
||||
return
|
||||
|
||||
if isinstance(needs, str):
|
||||
needs = (needs,)
|
||||
for need in needs:
|
||||
if need == "cuda":
|
||||
if not torch.cuda.is_available():
|
||||
return
|
||||
else:
|
||||
try:
|
||||
importlib.import_module(need)
|
||||
except ImportError:
|
||||
return
|
||||
|
||||
run_tests()
|
||||
|
||||
|
||||
class TestCase(TorchTestCase):
|
||||
_exit_stack: contextlib.ExitStack
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
cls._exit_stack.close()
|
||||
super().tearDownClass()
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
super().setUpClass()
|
||||
cls._exit_stack = contextlib.ExitStack() # type: ignore[attr-defined]
|
||||
cls._exit_stack.enter_context( # type: ignore[attr-defined]
|
||||
config.patch(
|
||||
raise_on_ctx_manager_usage=True,
|
||||
suppress_errors=False,
|
||||
log_compilation_metrics=False,
|
||||
),
|
||||
)
|
||||
|
||||
def setUp(self) -> None:
|
||||
self._prior_is_grad_enabled = torch.is_grad_enabled()
|
||||
self._prior_nested_graph_breaks = config.nested_graph_breaks
|
||||
config.nested_graph_breaks = True
|
||||
super().setUp()
|
||||
reset()
|
||||
utils.counters.clear()
|
||||
self.handler = logging.NullHandler()
|
||||
trace_log.addHandler(self.handler)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
trace_log.removeHandler(self.handler)
|
||||
for k, v in utils.counters.items():
|
||||
log.debug("%s %s", k, v.most_common())
|
||||
reset()
|
||||
utils.counters.clear()
|
||||
torch._C._autograd._saved_tensors_hooks_enable()
|
||||
super().tearDown()
|
||||
if self._prior_is_grad_enabled is not torch.is_grad_enabled():
|
||||
log.warning("Running test changed grad mode")
|
||||
torch.set_grad_enabled(self._prior_is_grad_enabled)
|
||||
config.nested_graph_breaks = self._prior_nested_graph_breaks
|
||||
|
||||
def assertEqual(self, x: Any, y: Any, *args: Any, **kwargs: Any) -> None: # type: ignore[override]
|
||||
if (
|
||||
config.debug_disable_compile_counter
|
||||
and isinstance(x, utils.CompileCounterInt)
|
||||
or isinstance(y, utils.CompileCounterInt)
|
||||
):
|
||||
return
|
||||
return super().assertEqual(x, y, *args, **kwargs)
|
||||
|
||||
# assertExpectedInline might also need to be disabled for wrapped nested
|
||||
# graph break tests
|
||||
|
||||
|
||||
class CPythonTestCase(TestCase):
|
||||
"""
|
||||
Test class for CPython tests located in "test/dynamo/CPython/Py_version/*".
|
||||
|
||||
This class enables specific features that are disabled by default, such as
|
||||
tracing through unittest methods.
|
||||
"""
|
||||
|
||||
_stack: contextlib.ExitStack
|
||||
dynamo_strict_nopython = True
|
||||
|
||||
# Restore original unittest methods to simplify tracing CPython test cases.
|
||||
assertEqual = unittest.TestCase.assertEqual # type: ignore[assignment]
|
||||
assertNotEqual = unittest.TestCase.assertNotEqual # type: ignore[assignment]
|
||||
assertTrue = unittest.TestCase.assertTrue
|
||||
assertFalse = unittest.TestCase.assertFalse
|
||||
assertIs = unittest.TestCase.assertIs
|
||||
assertIsNot = unittest.TestCase.assertIsNot
|
||||
assertIsNone = unittest.TestCase.assertIsNone
|
||||
assertIsNotNone = unittest.TestCase.assertIsNotNone
|
||||
assertIn = unittest.TestCase.assertIn
|
||||
assertNotIn = unittest.TestCase.assertNotIn
|
||||
assertIsInstance = unittest.TestCase.assertIsInstance
|
||||
assertNotIsInstance = unittest.TestCase.assertNotIsInstance
|
||||
assertAlmostEqual = unittest.TestCase.assertAlmostEqual
|
||||
assertNotAlmostEqual = unittest.TestCase.assertNotAlmostEqual
|
||||
assertGreater = unittest.TestCase.assertGreater
|
||||
assertGreaterEqual = unittest.TestCase.assertGreaterEqual
|
||||
assertLess = unittest.TestCase.assertLess
|
||||
assertLessEqual = unittest.TestCase.assertLessEqual
|
||||
assertRegex = unittest.TestCase.assertRegex
|
||||
assertNotRegex = unittest.TestCase.assertNotRegex
|
||||
assertCountEqual = unittest.TestCase.assertCountEqual
|
||||
assertMultiLineEqual = polyfills.assert_multi_line_equal
|
||||
assertSequenceEqual = polyfills.assert_sequence_equal
|
||||
assertListEqual = unittest.TestCase.assertListEqual
|
||||
assertTupleEqual = unittest.TestCase.assertTupleEqual
|
||||
assertSetEqual = unittest.TestCase.assertSetEqual
|
||||
# pyrefly: ignore [bad-override]
|
||||
assertDictEqual = polyfills.assert_dict_equal
|
||||
# pyrefly: ignore [bad-override]
|
||||
assertRaises = unittest.TestCase.assertRaises
|
||||
# pyrefly: ignore [bad-override]
|
||||
assertRaisesRegex = unittest.TestCase.assertRaisesRegex
|
||||
assertWarns = unittest.TestCase.assertWarns
|
||||
assertWarnsRegex = unittest.TestCase.assertWarnsRegex
|
||||
assertLogs = unittest.TestCase.assertLogs
|
||||
fail = unittest.TestCase.fail
|
||||
failureException = unittest.TestCase.failureException
|
||||
|
||||
def compile_fn(
|
||||
self,
|
||||
fn: Callable[..., Any],
|
||||
backend: str | Callable[..., Any],
|
||||
nopython: bool,
|
||||
) -> Callable[..., Any]:
|
||||
# We want to compile only the test function, excluding any setup code
|
||||
# from unittest
|
||||
|
||||
method = getattr(self, self._testMethodName)
|
||||
method = torch._dynamo.optimize(backend, error_on_graph_break=nopython)(method)
|
||||
|
||||
setattr(self, self._testMethodName, method)
|
||||
return fn
|
||||
|
||||
def _dynamo_test_key(self) -> str:
|
||||
suffix = super()._dynamo_test_key()
|
||||
test_cls = self.__class__
|
||||
test_file = inspect.getfile(test_cls).split(os.sep)[-1].split(".")[0]
|
||||
py_ver = re.search(r"/([\d_]+)/", inspect.getfile(test_cls))
|
||||
if py_ver:
|
||||
py_ver = py_ver.group().strip(os.sep).replace("_", "") # type: ignore[assignment]
|
||||
else:
|
||||
return suffix
|
||||
return f"CPython{py_ver}-{test_file}-{suffix}"
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
cls._stack.close()
|
||||
super().tearDownClass()
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
# Skip test if python versions doesn't match
|
||||
prefix = os.path.join("dynamo", "cpython") + os.path.sep
|
||||
regex = re.escape(prefix) + r"\d_\d{2}"
|
||||
search_path = inspect.getfile(cls)
|
||||
m = re.search(regex, search_path)
|
||||
if m:
|
||||
test_py_ver = tuple(map(int, m.group().removeprefix(prefix).split("_")))
|
||||
py_ver = sys.version_info[:2]
|
||||
if py_ver != test_py_ver:
|
||||
expected = ".".join(map(str, test_py_ver))
|
||||
got = ".".join(map(str, py_ver))
|
||||
raise unittest.SkipTest(
|
||||
f"Test requires Python {expected} but got Python {got}"
|
||||
)
|
||||
else:
|
||||
raise unittest.SkipTest(
|
||||
f"Test requires a specific Python version but not found in path {inspect.getfile(cls)}"
|
||||
)
|
||||
|
||||
super().setUpClass()
|
||||
cls._stack = contextlib.ExitStack() # type: ignore[attr-defined]
|
||||
cls._stack.enter_context( # type: ignore[attr-defined]
|
||||
config.patch(
|
||||
enable_trace_unittest=True,
|
||||
),
|
||||
)
|
||||
|
||||
# pyrefly: ignore [implicit-any]
|
||||
def wrap_with_policy(self, method_name: str, policy: Callable) -> None:
|
||||
pass
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
Functions used to test torch._dynamo.dont_skip_tracing.
|
||||
This file is located in torch/_dynamo so that it is skipped by trace rules.
|
||||
There is a special rule in trace_rules that doesn't skip this file when
|
||||
dont_skip_tracing is active.
|
||||
"""
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
def f1(x: torch.Tensor) -> torch.Tensor:
|
||||
return x + 1
|
||||
|
||||
|
||||
def f2(x: torch.Tensor) -> torch.Tensor:
|
||||
return x + 1
|
||||
|
||||
|
||||
def f3(x: torch.Tensor) -> torch.Tensor:
|
||||
return f2(x)
|
||||
|
||||
|
||||
def f4(x: torch.Tensor) -> torch.Tensor:
|
||||
x = f5(x, 1)
|
||||
x = torch._dynamo.dont_skip_tracing(f6)(x)
|
||||
x = f5(x, 8)
|
||||
return x
|
||||
|
||||
|
||||
def f5(x: torch.Tensor, n: int) -> torch.Tensor:
|
||||
if torch.compiler.is_compiling():
|
||||
return x + n
|
||||
return x
|
||||
|
||||
|
||||
def f6(x: torch.Tensor) -> torch.Tensor:
|
||||
x = f5(x, 2)
|
||||
torch._dynamo.graph_break()
|
||||
x = f5(x, 4)
|
||||
return x
|
||||
@@ -0,0 +1,323 @@
|
||||
"""Common utilities for testing Dynamo's minifier functionality.
|
||||
|
||||
This module provides the base infrastructure for running minification tests in Dynamo.
|
||||
It includes:
|
||||
- MinifierTestResult: A dataclass for storing and processing minifier test results
|
||||
- MinifierTestBase: A base test class with utilities for:
|
||||
- Running tests in isolated environments
|
||||
- Managing temporary directories and configurations
|
||||
- Executing minifier launcher scripts
|
||||
- Running and validating reproduction scripts
|
||||
- Supporting both compile-time and runtime error testing
|
||||
|
||||
The minifier helps reduce failing Dynamo compilations to minimal reproductions.
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import traceback
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import torch
|
||||
import torch._dynamo
|
||||
import torch._dynamo.test_case
|
||||
from torch._dynamo.trace_rules import _as_posix_path
|
||||
from torch.utils._traceback import report_compile_source_on_error
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class MinifierTestResult:
|
||||
minifier_code: str
|
||||
repro_code: str
|
||||
|
||||
def _get_module(self, t: str) -> str:
|
||||
match = re.search(r"class Repro\(torch\.nn\.Module\):\s+([ ].*\n| *\n)+", t)
|
||||
assert match is not None, "failed to find module"
|
||||
r = match.group(0)
|
||||
r = re.sub(r"\s+$", "\n", r, flags=re.MULTILINE)
|
||||
r = re.sub(r"\n{3,}", "\n\n", r)
|
||||
return r.strip()
|
||||
|
||||
def get_exported_program_path(self) -> str | None:
|
||||
# Extract the exported program file path from AOTI minifier's repro.py
|
||||
# Regular expression pattern to match the file path
|
||||
pattern = r'torch\.export\.load\(\s*["\'](.*?)["\']\s*\)'
|
||||
# Search for the pattern in the text
|
||||
match = re.search(pattern, self.repro_code)
|
||||
# Extract and print the file path if a match is found
|
||||
if match:
|
||||
file_path = match.group(1)
|
||||
return file_path
|
||||
return None
|
||||
|
||||
def minifier_module(self) -> str:
|
||||
return self._get_module(self.minifier_code)
|
||||
|
||||
def repro_module(self) -> str:
|
||||
return self._get_module(self.repro_code)
|
||||
|
||||
|
||||
class MinifierTestBase(torch._dynamo.test_case.TestCase):
|
||||
DEBUG_DIR = tempfile.mkdtemp()
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
super().setUpClass()
|
||||
if not os.path.exists(cls.DEBUG_DIR):
|
||||
cls.DEBUG_DIR = tempfile.mkdtemp()
|
||||
cls._exit_stack.enter_context( # type: ignore[attr-defined]
|
||||
torch._dynamo.config.patch(debug_dir_root=cls.DEBUG_DIR)
|
||||
)
|
||||
# These configurations make new process startup slower. Disable them
|
||||
# for the minification tests to speed them up.
|
||||
cls._exit_stack.enter_context( # type: ignore[attr-defined]
|
||||
torch._inductor.config.patch(
|
||||
{
|
||||
# https://github.com/pytorch/pytorch/issues/100376
|
||||
"pattern_matcher": False,
|
||||
# multiprocess compilation takes a long time to warmup
|
||||
"compile_threads": 1,
|
||||
# https://github.com/pytorch/pytorch/issues/100378
|
||||
"cpp.vec_isa_ok": False,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
if os.getenv("PYTORCH_KEEP_TMPDIR", "0") != "1":
|
||||
shutil.rmtree(cls.DEBUG_DIR)
|
||||
else:
|
||||
print(f"test_minifier_common tmpdir kept at: {cls.DEBUG_DIR}")
|
||||
cls._exit_stack.close() # type: ignore[attr-defined]
|
||||
|
||||
def _gen_codegen_fn_patch_code(self, device: str, bug_type: str) -> str:
|
||||
assert bug_type in ("compile_error", "runtime_error", "accuracy")
|
||||
return f"""\
|
||||
{torch._dynamo.config.codegen_config()}
|
||||
{torch._inductor.config.codegen_config()}
|
||||
torch._inductor.config.{"cpp" if device == "cpu" else "triton"}.inject_relu_bug_TESTING_ONLY = {bug_type!r}
|
||||
"""
|
||||
|
||||
def _maybe_subprocess_run(
|
||||
self, args: Sequence[Any], *, isolate: bool, cwd: str | None = None
|
||||
) -> subprocess.CompletedProcess[bytes]:
|
||||
from torch._inductor.cpp_builder import normalize_path_separator
|
||||
|
||||
if not isolate:
|
||||
assert len(args) >= 2, args
|
||||
assert args[0] == "python3", args
|
||||
if args[1] == "-c":
|
||||
assert len(args) == 3, args
|
||||
code = args[2]
|
||||
args = ["-c"]
|
||||
else:
|
||||
assert len(args) >= 2, args
|
||||
with open(args[1]) as f:
|
||||
# Need normalize path of the code.
|
||||
code = normalize_path_separator(f.read())
|
||||
args = args[1:]
|
||||
|
||||
# WARNING: This is not a perfect simulation of running
|
||||
# the program out of tree. We only interpose on things we KNOW we
|
||||
# need to handle for tests. If you need more stuff, you will
|
||||
# need to augment this appropriately.
|
||||
|
||||
# NB: Can't use save_config because that will omit some fields,
|
||||
# but we must save and reset ALL fields
|
||||
dynamo_config = torch._dynamo.config.get_config_copy()
|
||||
inductor_config = torch._inductor.config.get_config_copy()
|
||||
try:
|
||||
stderr = io.StringIO()
|
||||
log_handler = logging.StreamHandler(stderr)
|
||||
log = logging.getLogger("torch._dynamo")
|
||||
log.addHandler(log_handler)
|
||||
try:
|
||||
prev_cwd = _as_posix_path(os.getcwd())
|
||||
if cwd is not None:
|
||||
cwd = _as_posix_path(cwd)
|
||||
os.chdir(cwd)
|
||||
with patch("sys.argv", args), report_compile_source_on_error():
|
||||
exec(code, {"__name__": "__main__", "__compile_source__": code})
|
||||
rc = 0
|
||||
except Exception:
|
||||
rc = 1
|
||||
traceback.print_exc(file=stderr)
|
||||
finally:
|
||||
log.removeHandler(log_handler)
|
||||
if cwd is not None:
|
||||
os.chdir(prev_cwd) # type: ignore[possibly-undefined]
|
||||
# Make sure we don't leave buggy compiled frames lying
|
||||
# around
|
||||
torch._dynamo.reset()
|
||||
finally:
|
||||
torch._dynamo.config.load_config(dynamo_config)
|
||||
torch._inductor.config.load_config(inductor_config)
|
||||
|
||||
# TODO: return a more appropriate data structure here
|
||||
return subprocess.CompletedProcess(
|
||||
args,
|
||||
rc,
|
||||
b"",
|
||||
stderr.getvalue().encode("utf-8"),
|
||||
)
|
||||
else:
|
||||
if cwd is not None:
|
||||
cwd = _as_posix_path(cwd)
|
||||
return subprocess.run(args, capture_output=True, cwd=cwd, check=False)
|
||||
|
||||
# Run `code` in a separate python process.
|
||||
# Returns the completed process state and the directory containing the
|
||||
# minifier launcher script, if `code` outputted it.
|
||||
def _run_test_code(
|
||||
self, code: str, *, isolate: bool
|
||||
) -> tuple[subprocess.CompletedProcess[bytes], str | Any]:
|
||||
proc = self._maybe_subprocess_run(
|
||||
["python3", "-c", code], isolate=isolate, cwd=self.DEBUG_DIR
|
||||
)
|
||||
|
||||
print("test stdout:", proc.stdout.decode("utf-8"))
|
||||
print("test stderr:", proc.stderr.decode("utf-8"))
|
||||
repro_dir_match = re.search(
|
||||
r"(\S+)minifier_launcher.py", proc.stderr.decode("utf-8")
|
||||
)
|
||||
if repro_dir_match is not None:
|
||||
return proc, repro_dir_match.group(1)
|
||||
return proc, None
|
||||
|
||||
# Runs the minifier launcher script in `repro_dir`
|
||||
def _run_minifier_launcher(
|
||||
self,
|
||||
repro_dir: str,
|
||||
isolate: bool,
|
||||
*,
|
||||
minifier_args: Sequence[Any] = (),
|
||||
repro_after: str | None = None,
|
||||
) -> tuple[subprocess.CompletedProcess[bytes], str]:
|
||||
self.assertIsNotNone(repro_dir)
|
||||
launch_file = _as_posix_path(os.path.join(repro_dir, "minifier_launcher.py"))
|
||||
with open(launch_file) as f:
|
||||
launch_code = f.read()
|
||||
|
||||
self.assertTrue(os.path.exists(launch_file))
|
||||
|
||||
args = ["python3", launch_file, "minify", *minifier_args]
|
||||
if not isolate and repro_after != "aot_inductor":
|
||||
# AOTI minifier doesn't have --no-isolate flag.
|
||||
# Everything in AOTI minifier is in no-isolate mode.
|
||||
args.append("--no-isolate")
|
||||
launch_proc = self._maybe_subprocess_run(args, isolate=isolate, cwd=repro_dir)
|
||||
print("minifier stdout:", launch_proc.stdout.decode("utf-8"))
|
||||
stderr = launch_proc.stderr.decode("utf-8")
|
||||
print("minifier stderr:", stderr)
|
||||
|
||||
self.assertNotIn("Input graph did not fail the tester", stderr)
|
||||
|
||||
return launch_proc, launch_code
|
||||
|
||||
# Runs the repro script in `repro_dir`
|
||||
def _run_repro(
|
||||
self, repro_dir: str, *, isolate: bool = True
|
||||
) -> tuple[subprocess.CompletedProcess[bytes], str]:
|
||||
self.assertIsNotNone(repro_dir)
|
||||
repro_file = _as_posix_path(os.path.join(repro_dir, "repro.py"))
|
||||
with open(repro_file) as f:
|
||||
repro_code = f.read()
|
||||
|
||||
self.assertTrue(os.path.exists(repro_file))
|
||||
|
||||
repro_proc = self._maybe_subprocess_run(
|
||||
["python3", repro_file], isolate=isolate, cwd=repro_dir
|
||||
)
|
||||
print("repro stdout:", repro_proc.stdout.decode("utf-8"))
|
||||
print("repro stderr:", repro_proc.stderr.decode("utf-8"))
|
||||
return repro_proc, repro_code
|
||||
|
||||
# Template for testing code.
|
||||
# `run_code` is the code to run for the test case.
|
||||
# `patch_code` is the code to be patched in every generated file; usually
|
||||
# just use this to turn on bugs via the config
|
||||
def _gen_test_code(self, run_code: str, repro_after: str, repro_level: int) -> str:
|
||||
repro_after_line = ""
|
||||
if repro_after == "aot_inductor":
|
||||
repro_after_line = (
|
||||
"torch._inductor.config.aot_inductor.dump_aoti_minifier = True"
|
||||
)
|
||||
elif repro_after:
|
||||
repro_after_line = f"""\
|
||||
torch._dynamo.config.repro_after = "{repro_after}"
|
||||
"""
|
||||
return f"""\
|
||||
import torch
|
||||
import torch._dynamo
|
||||
import torch._inductor
|
||||
{_as_posix_path(torch._dynamo.config.codegen_config())}
|
||||
{_as_posix_path(torch._inductor.config.codegen_config())}
|
||||
{repro_after_line}
|
||||
torch._dynamo.config.repro_level = {repro_level}
|
||||
torch._inductor.config.aot_inductor.repro_level = {repro_level}
|
||||
torch._dynamo.config.debug_dir_root = "{_as_posix_path(self.DEBUG_DIR)}"
|
||||
{run_code}
|
||||
"""
|
||||
|
||||
# Runs a full minifier test.
|
||||
# Minifier tests generally consist of 3 stages:
|
||||
# 1. Run the problematic code
|
||||
# 2. Run the generated minifier launcher script
|
||||
# 3. Run the generated repro script
|
||||
#
|
||||
# If possible, you should run the test with isolate=False; use
|
||||
# isolate=True only if the bug you're testing would otherwise
|
||||
# crash the process
|
||||
def _run_full_test(
|
||||
self,
|
||||
run_code: str,
|
||||
repro_after: str,
|
||||
expected_error: str | None,
|
||||
*,
|
||||
isolate: bool,
|
||||
minifier_args: Sequence[Any] = (),
|
||||
) -> MinifierTestResult | None:
|
||||
if isolate:
|
||||
repro_level = 3
|
||||
elif expected_error is None or expected_error == "AccuracyError":
|
||||
repro_level = 4
|
||||
else:
|
||||
repro_level = 2
|
||||
test_code = self._gen_test_code(run_code, repro_after, repro_level)
|
||||
print("running test", file=sys.stderr)
|
||||
test_proc, repro_dir = self._run_test_code(test_code, isolate=isolate)
|
||||
if expected_error is None:
|
||||
# Just check that there was no error
|
||||
self.assertEqual(test_proc.returncode, 0)
|
||||
|
||||
self.assertIsNone(repro_dir)
|
||||
return None
|
||||
# NB: Intentionally do not test return code; we only care about
|
||||
# actually generating the repro, we don't have to crash
|
||||
|
||||
self.assertIn(expected_error, test_proc.stderr.decode("utf-8"))
|
||||
|
||||
self.assertIsNotNone(repro_dir)
|
||||
print("running minifier", file=sys.stderr)
|
||||
_minifier_proc, minifier_code = self._run_minifier_launcher(
|
||||
repro_dir,
|
||||
isolate=isolate,
|
||||
minifier_args=minifier_args,
|
||||
repro_after=repro_after,
|
||||
)
|
||||
print("running repro", file=sys.stderr)
|
||||
repro_proc, repro_code = self._run_repro(repro_dir, isolate=isolate)
|
||||
|
||||
self.assertIn(expected_error, repro_proc.stderr.decode("utf-8"))
|
||||
self.assertNotEqual(repro_proc.returncode, 0)
|
||||
return MinifierTestResult(minifier_code=minifier_code, repro_code=repro_code)
|
||||
@@ -0,0 +1,612 @@
|
||||
"""Testing utilities and infrastructure for Dynamo.
|
||||
|
||||
This module provides a comprehensive set of testing utilities including:
|
||||
- Test result collection and validation
|
||||
- Graph manipulation and comparison tools
|
||||
- Test case management and execution helpers
|
||||
- Specialized test decorators for different Python versions and features
|
||||
- RNG state management
|
||||
- Compilation counting and monitoring
|
||||
- Debug utilities for bytecode transformation
|
||||
|
||||
The utilities in this module are used across Dynamo's test suite to ensure
|
||||
consistent testing patterns and proper test isolation.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import dis
|
||||
import functools
|
||||
import logging
|
||||
import os.path
|
||||
import random
|
||||
import re
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from collections.abc import Callable, Generator, Sequence
|
||||
from typing import Any, overload, TypeVar
|
||||
from typing_extensions import ParamSpec
|
||||
from unittest.mock import patch
|
||||
|
||||
import torch
|
||||
from torch import fx
|
||||
from torch._dynamo.backends.debugging import aot_eager
|
||||
from torch._dynamo.output_graph import OutputGraph
|
||||
|
||||
from . import config, eval_frame, optimize_assert, reset
|
||||
from .bytecode_transformation import (
|
||||
create_instruction,
|
||||
debug_checks,
|
||||
is_generator,
|
||||
transform_code_object,
|
||||
)
|
||||
from .guards import CheckFunctionManager, CompileId, GuardedCode
|
||||
from .types import ConvertFrameReturn, DynamoFrameType, wrap_guarded_code
|
||||
from .utils import CompileCounterInt, same
|
||||
|
||||
|
||||
np: types.ModuleType | None = None
|
||||
try:
|
||||
import numpy as np
|
||||
except ModuleNotFoundError:
|
||||
np = None
|
||||
|
||||
|
||||
unsupported = eval_frame.unsupported
|
||||
three = 3
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_P = ParamSpec("_P")
|
||||
|
||||
|
||||
def clone_me(x: torch.Tensor | None) -> torch.Tensor | None:
|
||||
if x is None:
|
||||
return None
|
||||
return x.detach().clone().requires_grad_(x.requires_grad)
|
||||
|
||||
|
||||
def remove_optimized_module_prefix(name: str) -> str:
|
||||
return re.sub(r"^_orig_mod[.]", "", name)
|
||||
|
||||
|
||||
def extract_graph_and_tracker(fn, *args, **kwargs): # type: ignore[no-untyped-def]
|
||||
from torch._dynamo.symbolic_convert import InstructionTranslator
|
||||
|
||||
gm = None
|
||||
region_tracker = None
|
||||
|
||||
def extract_graph_backend(_gm, *args, **kwargs): # type: ignore[no-untyped-def]
|
||||
nonlocal gm
|
||||
nonlocal region_tracker
|
||||
gm = _gm
|
||||
region_tracker = InstructionTranslator.current_tx().output.region_tracker
|
||||
return _gm
|
||||
|
||||
torch.compile(backend=extract_graph_backend, fullgraph=True)(fn)(*args, **kwargs)
|
||||
return gm.graph, region_tracker # type: ignore[union-attr]
|
||||
|
||||
|
||||
def extract_graph(fn, *args, **kwargs): # type: ignore[no-untyped-def]
|
||||
backend = AotEagerAndRecordGraphs()
|
||||
result = torch.compile(backend=backend)(fn)(*args, **kwargs)
|
||||
return result, backend.graphs, backend.fw_graphs, backend.bw_graphs
|
||||
|
||||
|
||||
def collect_results(
|
||||
model: torch.nn.Module, prediction: Any, loss: Any, example_inputs: Any
|
||||
) -> list[Any]:
|
||||
results = []
|
||||
results.append(prediction)
|
||||
results.append(loss)
|
||||
# if isinstance(loss, torch.Tensor) and loss.item() > 1:
|
||||
# log.warning(
|
||||
# f"High loss value alert - {loss:.2f}. Can result in unstable gradients."
|
||||
# )
|
||||
|
||||
grads = {}
|
||||
params = {}
|
||||
for name, param in model.named_parameters():
|
||||
if isinstance(model, eval_frame.OptimizedModule):
|
||||
name = remove_optimized_module_prefix(name)
|
||||
param_copy = param
|
||||
grad = param.grad
|
||||
# Treat None and zero grad as same
|
||||
if param.grad is None:
|
||||
grad = torch.zeros_like(param)
|
||||
grads[name + ".grad"] = grad
|
||||
params[name] = param_copy
|
||||
results.append(grads)
|
||||
results.append(params)
|
||||
buffers = {}
|
||||
for name, buffer in model.named_buffers():
|
||||
if isinstance(model, eval_frame.OptimizedModule):
|
||||
name = remove_optimized_module_prefix(name)
|
||||
buffers[name] = buffer
|
||||
results.append(buffers)
|
||||
for example in example_inputs:
|
||||
if isinstance(example, (tuple, list)):
|
||||
results.extend(inp.grad for inp in example if isinstance(inp, torch.Tensor))
|
||||
else:
|
||||
if isinstance(example, torch.Tensor):
|
||||
results.append(example.grad)
|
||||
return results
|
||||
|
||||
|
||||
def requires_bwd_pass(out: Any) -> bool:
|
||||
if isinstance(out, torch.Tensor):
|
||||
return out.requires_grad
|
||||
elif isinstance(out, (list, tuple)):
|
||||
return any(requires_bwd_pass(x) for x in out)
|
||||
elif out is None:
|
||||
return False
|
||||
elif isinstance(out, int):
|
||||
return False
|
||||
raise NotImplementedError("Don't know how to reduce", type(out))
|
||||
|
||||
|
||||
@overload
|
||||
def reduce_to_scalar_loss(out: torch.Tensor) -> torch.Tensor: ...
|
||||
|
||||
|
||||
@overload
|
||||
def reduce_to_scalar_loss(
|
||||
out: list[Any] | tuple[Any, ...] | dict[Any, Any],
|
||||
) -> float: ...
|
||||
|
||||
|
||||
def reduce_to_scalar_loss(out: Any) -> torch.Tensor | float:
|
||||
"""Reduce the output of a model to get scalar loss"""
|
||||
if isinstance(out, torch.Tensor):
|
||||
# Mean does not work on integer tensors
|
||||
return out.sum() / out.numel()
|
||||
elif isinstance(out, (list, tuple)):
|
||||
return sum(reduce_to_scalar_loss(x) for x in out) / len(out)
|
||||
elif type(out).__name__ in (
|
||||
"MaskedLMOutput",
|
||||
"Seq2SeqLMOutput",
|
||||
"CausalLMOutputWithCrossAttentions",
|
||||
):
|
||||
return reduce_to_scalar_loss(out.logits)
|
||||
elif type(out).__name__ == "SquashedNormal":
|
||||
return out.mean.sum()
|
||||
elif isinstance(out, dict):
|
||||
return sum(reduce_to_scalar_loss(value) for value in out.values()) / len(
|
||||
out.keys()
|
||||
)
|
||||
raise NotImplementedError("Don't know how to reduce", type(out))
|
||||
|
||||
|
||||
def debug_dir() -> str:
|
||||
path = os.path.join(os.path.dirname(__file__), "../debug")
|
||||
if not os.path.exists(path):
|
||||
os.mkdir(path)
|
||||
return path
|
||||
|
||||
|
||||
def debug_dump(name: str, code: types.CodeType, extra: str = "") -> None:
|
||||
with open(os.path.join(debug_dir(), name), "w") as fd:
|
||||
fd.write(
|
||||
f"{dis.Bytecode(code).info()}\n\n{dis.Bytecode(code).dis()}\n\n{extra}\n"
|
||||
)
|
||||
|
||||
|
||||
def debug_insert_nops(
|
||||
frame: DynamoFrameType, cache_size: int, hooks: Any, _: Any, *, skip: int = 0
|
||||
) -> ConvertFrameReturn:
|
||||
"""used to debug jump updates"""
|
||||
|
||||
def insert_nops(instructions: list[Any], code_options: Any) -> None:
|
||||
instructions.insert(0, create_instruction("NOP"))
|
||||
instructions.insert(0, create_instruction("NOP"))
|
||||
|
||||
metrics_context = torch._dynamo.utils.get_metrics_context()
|
||||
with torch._dynamo.utils.dynamo_timed("debug_insert_nops"), metrics_context:
|
||||
if is_generator(frame.f_code):
|
||||
return ConvertFrameReturn()
|
||||
|
||||
debug_checks(frame.f_code)
|
||||
code, _ = transform_code_object(frame.f_code, insert_nops)
|
||||
graph = OutputGraph(
|
||||
code_options={},
|
||||
compiler_fn=None,
|
||||
root_tx=None, # type: ignore[arg-type]
|
||||
export=False,
|
||||
export_constraints=[],
|
||||
frame_state={"_id": 0},
|
||||
# TODO: shouldn't this be f_locals/f_globals from frame?
|
||||
local_scope=locals(),
|
||||
global_scope=globals(),
|
||||
f_code=frame.f_code,
|
||||
torch_function_mode_stack=[],
|
||||
package=None,
|
||||
)
|
||||
|
||||
return wrap_guarded_code(
|
||||
GuardedCode(
|
||||
code,
|
||||
CheckFunctionManager(frame.f_code, graph).guard_manager, # type: ignore[arg-type]
|
||||
CompileId(frame_id=0, frame_compile_id=0),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class CompileCounter:
|
||||
def __init__(self) -> None:
|
||||
self.frame_count: int | CompileCounterInt = 0
|
||||
self.clear()
|
||||
|
||||
def __call__(
|
||||
self, gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor]
|
||||
) -> Callable[..., Any]:
|
||||
self.frame_count += 1
|
||||
for node in gm.graph.nodes:
|
||||
if "call" in node.op:
|
||||
self.op_count += 1
|
||||
return gm.forward
|
||||
|
||||
def clear(self) -> None:
|
||||
if config.debug_disable_compile_counter:
|
||||
self.frame_count = CompileCounterInt(0)
|
||||
else:
|
||||
self.frame_count = 0
|
||||
self.op_count = 0
|
||||
|
||||
|
||||
class CompileCounterWithBackend:
|
||||
def __init__(self, backend: str) -> None:
|
||||
self.frame_count: int | CompileCounterInt = 0
|
||||
self.backend = backend
|
||||
self.graphs: list[torch.fx.GraphModule] = []
|
||||
self.clear()
|
||||
|
||||
def __call__(
|
||||
self, gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor]
|
||||
) -> Callable[..., Any]:
|
||||
from .backends.registry import lookup_backend
|
||||
|
||||
self.frame_count += 1
|
||||
for node in gm.graph.nodes:
|
||||
if "call" in node.op:
|
||||
self.op_count += 1
|
||||
self.graphs.append(gm)
|
||||
return lookup_backend(self.backend)(gm, example_inputs)
|
||||
|
||||
def clear(self) -> None:
|
||||
if config.debug_disable_compile_counter:
|
||||
self.frame_count = CompileCounterInt(0)
|
||||
else:
|
||||
self.frame_count = 0
|
||||
self.op_count = 0
|
||||
self.graphs = []
|
||||
|
||||
|
||||
# Equivalent to backend="eager", but also records graphs that
|
||||
# we can assert on
|
||||
class EagerAndRecordGraphs:
|
||||
def __init__(self) -> None:
|
||||
self.graphs: list[torch.fx.GraphModule] = []
|
||||
|
||||
def __call__(
|
||||
self, gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor]
|
||||
) -> Callable[..., Any]:
|
||||
self.graphs.append(gm)
|
||||
return gm.forward
|
||||
|
||||
|
||||
class AotEagerAndRecordGraphs:
|
||||
def __init__(self) -> None:
|
||||
self.graphs: list[torch.fx.GraphModule] = []
|
||||
self.fw_graphs: list[torch.fx.GraphModule] = []
|
||||
self.bw_graphs: list[torch.fx.GraphModule] = []
|
||||
|
||||
def __call__(
|
||||
self, gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor]
|
||||
) -> Callable[..., Any]:
|
||||
self.graphs.append(gm)
|
||||
|
||||
def fw_compiler(
|
||||
gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor]
|
||||
) -> Callable[..., Any]:
|
||||
self.fw_graphs.append(gm)
|
||||
return gm.forward
|
||||
|
||||
def bw_compiler(
|
||||
gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor]
|
||||
) -> Callable[..., Any]:
|
||||
self.bw_graphs.append(gm)
|
||||
return gm.forward
|
||||
|
||||
return aot_eager(
|
||||
gm,
|
||||
example_inputs,
|
||||
fw_compiler=fw_compiler,
|
||||
bw_compiler=bw_compiler,
|
||||
)
|
||||
|
||||
|
||||
class InductorAndRecordGraphs:
|
||||
def __init__(self) -> None:
|
||||
self.graphs: list[torch.fx.GraphModule] = []
|
||||
self.inductor_graphs: list[torch.fx.GraphModule] = []
|
||||
|
||||
def __call__(self, gm, example_inputs): # type: ignore[no-untyped-def]
|
||||
import torch._inductor.compile_fx as compile_fx_mod
|
||||
|
||||
self.graphs.append(gm)
|
||||
|
||||
old_compile_fx_inner = compile_fx_mod._compile_fx_inner
|
||||
|
||||
def patched(*args, **kwargs): # type: ignore[no-untyped-def]
|
||||
self.inductor_graphs.append(args[0])
|
||||
return old_compile_fx_inner(*args, **kwargs)
|
||||
|
||||
with patch.object(compile_fx_mod, "_compile_fx_inner", new=patched):
|
||||
return compile_fx_mod.compile_fx(gm, example_inputs)
|
||||
|
||||
|
||||
def strip_comment(code: str) -> str:
|
||||
return re.sub(r"(?m)^ *#.*\n?", "", code)
|
||||
|
||||
|
||||
def remove_trailing_space(code: str) -> str:
|
||||
return "\n".join([line.rstrip() for line in code.split("\n")])
|
||||
|
||||
|
||||
def _squash_blank_lines(code: str) -> str:
|
||||
lines = code.split("\n")
|
||||
result: list[str] = []
|
||||
saw_blank = False
|
||||
for line in lines:
|
||||
if line.strip() == "":
|
||||
if saw_blank:
|
||||
continue
|
||||
saw_blank = True
|
||||
else:
|
||||
saw_blank = False
|
||||
result.append(line)
|
||||
return "\n".join(result)
|
||||
|
||||
|
||||
def normalize_gm(gm_str: str) -> str:
|
||||
# strip comments as comments have path to files which may differ from
|
||||
# system to system.
|
||||
stripped = strip_comment(gm_str)
|
||||
no_trailing = remove_trailing_space(stripped)
|
||||
return _squash_blank_lines(no_trailing)
|
||||
|
||||
|
||||
def empty_line_normalizer(code: str) -> str:
|
||||
"""
|
||||
Normalize code: remove empty lines.
|
||||
"""
|
||||
normal_code = re.sub(r"[\r\n]+", "\n", code)
|
||||
return normal_code
|
||||
|
||||
|
||||
def standard_test(
|
||||
self: Any,
|
||||
fn: Callable[..., Any],
|
||||
nargs: int,
|
||||
expected_ops: int | None = None,
|
||||
expected_ops_dynamic: int | None = None,
|
||||
expected_frame_count: int = 1,
|
||||
) -> None:
|
||||
if not config.assume_static_by_default and expected_ops_dynamic is not None:
|
||||
expected_ops = expected_ops_dynamic
|
||||
|
||||
actual = CompileCounter()
|
||||
|
||||
args1 = [torch.randn(10, 10) for _ in range(nargs)]
|
||||
args2 = [torch.randn(10, 10) for _ in range(nargs)]
|
||||
correct1 = fn(*args1)
|
||||
correct2 = fn(*args2)
|
||||
reset()
|
||||
opt_fn = optimize_assert(actual)(fn)
|
||||
val1a = opt_fn(*args1)
|
||||
val2a = opt_fn(*args2)
|
||||
val1b = opt_fn(*args1)
|
||||
val2b = opt_fn(*args2)
|
||||
reset()
|
||||
self.assertTrue(same(val1a, correct1))
|
||||
self.assertTrue(same(val1b, correct1))
|
||||
self.assertTrue(same(val2a, correct2))
|
||||
self.assertTrue(same(val2b, correct2))
|
||||
self.assertEqual(actual.frame_count, expected_frame_count)
|
||||
if expected_ops is not None:
|
||||
self.assertEqual(actual.op_count, expected_ops)
|
||||
|
||||
|
||||
def dummy_fx_compile(
|
||||
gm: fx.GraphModule, example_inputs: list[torch.Tensor]
|
||||
) -> Callable[..., Any]:
|
||||
return gm.forward
|
||||
|
||||
|
||||
def format_speedup(
|
||||
speedup: float,
|
||||
pvalue: float,
|
||||
is_correct: bool = True,
|
||||
pvalue_threshold: float = 0.1,
|
||||
) -> str:
|
||||
if not is_correct:
|
||||
return "ERROR"
|
||||
if pvalue > pvalue_threshold:
|
||||
return f"{speedup:.3f}x SAME"
|
||||
return f"{speedup:.3f}x p={pvalue:.2f}"
|
||||
|
||||
|
||||
def rand_strided(
|
||||
size: Sequence[int],
|
||||
stride: Sequence[int],
|
||||
dtype: torch.dtype = torch.float32,
|
||||
device: str | torch.device = "cpu",
|
||||
extra_size: int = 0,
|
||||
) -> torch.Tensor:
|
||||
needed_size = extra_size
|
||||
if all(s > 0 for s in size):
|
||||
# only need to allocate if all sizes are non-zero
|
||||
needed_size += (
|
||||
sum((shape - 1) * stride for shape, stride in zip(size, stride)) + 1
|
||||
)
|
||||
if dtype.is_floating_point:
|
||||
if dtype == torch.float4_e2m1fn_x2:
|
||||
buffer = torch.randint(
|
||||
0, 256, (needed_size,), dtype=torch.uint8, device=device
|
||||
).view(torch.float4_e2m1fn_x2)
|
||||
elif dtype.itemsize == 1:
|
||||
"""
|
||||
normal distribution kernel is not implemented for fp8..
|
||||
Workaround that by creating a fp16 tensor and then cast.
|
||||
"""
|
||||
buffer = torch.randn(needed_size, dtype=torch.float16, device=device).to(
|
||||
dtype=dtype
|
||||
)
|
||||
else:
|
||||
buffer = torch.randn(needed_size, dtype=dtype, device=device)
|
||||
else:
|
||||
buffer = torch.zeros(size=[needed_size], dtype=dtype, device=device)
|
||||
return torch.as_strided(buffer, size, stride)
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
def check_dynamic_shape_capture() -> bool:
|
||||
# This also mirrors config from `test/dynamo/test_dynamic_shapes.py:make_dynamic_cls`
|
||||
return not config.assume_static_by_default
|
||||
|
||||
|
||||
def _make_fn_with_patches(fn: Callable[_P, _T], *patches: Any) -> Callable[_P, _T]:
|
||||
@functools.wraps(fn)
|
||||
def _fn(*args: _P.args, **kwargs: _P.kwargs) -> _T:
|
||||
with contextlib.ExitStack() as stack:
|
||||
for module, attr, val in patches:
|
||||
stack.enter_context(patch.object(module, attr, val))
|
||||
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
return _fn
|
||||
|
||||
|
||||
def make_test_cls_with_patches(
|
||||
cls: type,
|
||||
cls_prefix: str,
|
||||
fn_suffix: str,
|
||||
*patches: Any,
|
||||
xfail_prop: str | None = None,
|
||||
decorator: Callable[[Callable[..., Any]], Callable[..., Any]] = lambda x: x,
|
||||
) -> type:
|
||||
DummyTestClass = type(f"{cls_prefix}{cls.__name__}", cls.__bases__, {})
|
||||
DummyTestClass.__qualname__ = DummyTestClass.__name__
|
||||
|
||||
for name in dir(cls):
|
||||
if name.startswith("test_"):
|
||||
fn = getattr(cls, name)
|
||||
if not callable(fn):
|
||||
setattr(DummyTestClass, name, getattr(cls, name))
|
||||
continue
|
||||
new_name = f"{name}{fn_suffix}"
|
||||
new_fn = _make_fn_with_patches(fn, *patches)
|
||||
new_fn.__name__ = new_name
|
||||
if xfail_prop is not None and hasattr(fn, xfail_prop):
|
||||
new_fn = unittest.expectedFailure(new_fn)
|
||||
setattr(DummyTestClass, new_name, decorator(new_fn))
|
||||
# NB: Doesn't handle slots correctly, but whatever
|
||||
elif not hasattr(DummyTestClass, name):
|
||||
setattr(DummyTestClass, name, getattr(cls, name))
|
||||
|
||||
return DummyTestClass
|
||||
|
||||
|
||||
# test Python 3.11+ specific features
|
||||
def skipIfNotPy311(fn: Callable[_P, _T]) -> Callable[_P, _T]:
|
||||
if sys.version_info >= (3, 11):
|
||||
return fn
|
||||
# pyrefly: ignore [bad-return, bad-argument-type]
|
||||
return unittest.skip(fn)
|
||||
|
||||
|
||||
def skipIfNotPy312(fn: Callable[_P, _T]) -> Callable[_P, _T]:
|
||||
if sys.version_info >= (3, 12):
|
||||
return fn
|
||||
return unittest.skip("Requires Python 3.12+")(fn)
|
||||
|
||||
|
||||
def skipIfOnlyNotPy312(fn: Callable[_P, _T]) -> Callable[_P, _T]:
|
||||
if sys.version_info >= (3, 13) or sys.version_info < (3, 12):
|
||||
return unittest.skip("Requires Python 3.12")(fn)
|
||||
return fn
|
||||
|
||||
|
||||
def xfailIfPy312(fn: Callable[_P, _T]) -> Callable[_P, _T]:
|
||||
if sys.version_info >= (3, 12):
|
||||
return unittest.expectedFailure(fn)
|
||||
return fn
|
||||
|
||||
|
||||
def skipIfPy312(fn: Callable[_P, _T]) -> Callable[_P, _T]:
|
||||
if sys.version_info >= (3, 12):
|
||||
return unittest.skip("Not supported in Python 3.12+")(fn)
|
||||
return fn
|
||||
|
||||
|
||||
# Controls tests generated in test/inductor/test_torchinductor_dynamic_shapes.py
|
||||
# and test/dynamo/test_dynamic_shapes.py
|
||||
def expectedFailureDynamic(fn: Callable[_P, _T]) -> Callable[_P, _T]:
|
||||
fn._expected_failure_dynamic = True # type: ignore[attr-defined]
|
||||
return fn
|
||||
|
||||
|
||||
# Controls tests generated in test/inductor/test_torchinductor_codegen_dynamic_shapes.py
|
||||
def expectedFailureCodegenDynamic(fn: Callable[_P, _T]) -> Callable[_P, _T]:
|
||||
fn._expected_failure_codegen_dynamic = True # type: ignore[attr-defined]
|
||||
return fn
|
||||
|
||||
|
||||
# Controls test generated in test/inductor/test_cpp_wrapper.py
|
||||
def expectedFailureDynamicWrapper(fn: Callable[_P, _T]) -> Callable[_P, _T]:
|
||||
fn._expected_failure_dynamic_wrapper = True # type: ignore[attr-defined]
|
||||
return fn
|
||||
|
||||
|
||||
def reset_rng_state(use_xla: bool = False) -> None:
|
||||
torch.manual_seed(1337)
|
||||
random.seed(1337)
|
||||
if np:
|
||||
np.random.seed(1337)
|
||||
if use_xla:
|
||||
import torch_xla.core.xla_model as xm
|
||||
|
||||
xm.set_rng_state(1337, str(xm.xla_device()))
|
||||
|
||||
|
||||
def _skipped_function_for_test_reconstruct(
|
||||
f: Callable[_P, _T], *args: _P.args, **kwargs: _P.kwargs
|
||||
) -> _T:
|
||||
return f(*args, **kwargs)
|
||||
|
||||
|
||||
_testing_invoke_subgraph_inductor_compile_captured_gms = None
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _testing_capture_invoke_subgraph_inductor_compile_gms() -> Generator[
|
||||
list[torch.fx.GraphModule]
|
||||
]:
|
||||
"""
|
||||
Context manager to capture graph modules compiled by invoke_subgraph_inductor_compile.
|
||||
|
||||
Usage:
|
||||
with _testing_capture_invoke_subgraph_inductor_compile_gms() as captured_gms:
|
||||
# code that triggers invoke_subgraph_inductor_compile
|
||||
pass
|
||||
# captured_gms will contain the list of captured graph modules
|
||||
"""
|
||||
global _testing_invoke_subgraph_inductor_compile_captured_gms
|
||||
# pyrefly: ignore [implicit-any]
|
||||
_testing_invoke_subgraph_inductor_compile_captured_gms = []
|
||||
try:
|
||||
yield _testing_invoke_subgraph_inductor_compile_captured_gms
|
||||
finally:
|
||||
_testing_invoke_subgraph_inductor_compile_captured_gms = None
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,140 @@
|
||||
"""This module contains the core type definitions and protocols used throughout Dynamo.
|
||||
|
||||
The types defined here fall into several categories:
|
||||
- Guard related types (GuardFn, GuardFail, GuardedCode): Used for tracking and managing guards that protect compiled code
|
||||
- Frame and cache types (FrameState, CacheEntry): Used for managing interpreter frame state and caching
|
||||
- Callback protocols (DynamoCallbackFn): Define the interface for frame evaluation callbacks
|
||||
- Hook protocols (DynamoGuardHook, ProfilerStartHook, ProfilerEndHook, BytecodeHook): Define various hook points for
|
||||
instrumentation and customization
|
||||
|
||||
These types provide the foundational interfaces that enable Dynamo's dynamic compilation and optimization system,
|
||||
ensuring type safety and clear contracts between different components of the system.
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
import types
|
||||
from collections.abc import Callable
|
||||
from typing import Any, NamedTuple, Protocol
|
||||
|
||||
# CacheEntry has a `guard_manager` field for the guard, and a `code` field for the code object.
|
||||
from torch._C._dynamo.eval_frame import (
|
||||
_CacheEntry as CacheEntry,
|
||||
_ExtraState as ExtraState,
|
||||
_FrameAction as FrameAction,
|
||||
_FrameExecStrategy as FrameExecStrategy,
|
||||
_PyInterpreterFrame as DynamoFrameType,
|
||||
)
|
||||
from torch._guards import CompileId, Guard
|
||||
|
||||
|
||||
# We use a dict to store additional data per frame.
|
||||
FrameState = dict[Any, Any]
|
||||
|
||||
|
||||
class GuardFail(NamedTuple):
|
||||
# A string repr of the piece of failed guard code we eval-ed
|
||||
reason: str
|
||||
# A code object where we failed a guard
|
||||
orig_code: types.CodeType
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class GuardFilterEntry:
|
||||
name: str
|
||||
has_value: bool
|
||||
value: object
|
||||
guard_type: str
|
||||
derived_guard_types: tuple[str, ...]
|
||||
is_global: bool
|
||||
orig_guard: Guard
|
||||
|
||||
|
||||
class GuardFn(Protocol):
|
||||
closure_vars: dict[str, object]
|
||||
args: list[str]
|
||||
code_parts: list[str]
|
||||
verbose_code_parts: list[str]
|
||||
global_scope: dict[str, object]
|
||||
guard_fail_fn: Callable[[GuardFail], None] | None
|
||||
cache_entry: CacheEntry | None
|
||||
extra_state: ExtraState | None
|
||||
|
||||
# maps locals of user function to bool
|
||||
def __call__(self, f_locals: dict[str, object]) -> bool: ...
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class GuardedCode:
|
||||
code: types.CodeType
|
||||
guard_manager: GuardFn
|
||||
compile_id: CompileId
|
||||
trace_annotation: str = "Unknown"
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ConvertFrameReturn:
|
||||
# default return is no compiled code (i.e. `return None`):
|
||||
# strategy is to skip non-recursively, for all future intercepted frames too
|
||||
|
||||
# eval frame execution strategy for this frame
|
||||
frame_exec_strategy: FrameExecStrategy = dataclasses.field(
|
||||
default_factory=lambda: FrameExecStrategy(FrameAction.SKIP, FrameAction.DEFAULT)
|
||||
)
|
||||
# also apply frame_exec strategy to future frames with same code
|
||||
apply_to_code: bool = True
|
||||
guarded_code: GuardedCode | None = None
|
||||
|
||||
|
||||
def wrap_guarded_code(guarded_code: GuardedCode) -> ConvertFrameReturn:
|
||||
return ConvertFrameReturn(
|
||||
frame_exec_strategy=FrameExecStrategy(FrameAction.DEFAULT, FrameAction.DEFAULT),
|
||||
guarded_code=guarded_code,
|
||||
)
|
||||
|
||||
|
||||
class DynamoCallbackFn(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
frame: DynamoFrameType,
|
||||
cache_entry: CacheEntry | None,
|
||||
frame_state: FrameState,
|
||||
) -> ConvertFrameReturn: ...
|
||||
|
||||
|
||||
DynamoCallback = DynamoCallbackFn | None | bool
|
||||
|
||||
|
||||
class DynamoGuardHook(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
guard_manager: GuardFn,
|
||||
code: types.CodeType,
|
||||
f_locals: dict[str, object],
|
||||
index: int,
|
||||
last: bool,
|
||||
) -> None: ...
|
||||
|
||||
|
||||
class DynamoGuardCompleteHook(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
cache_hit: bool,
|
||||
) -> bool: ...
|
||||
|
||||
|
||||
class ProfilerStartHook(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
name: str,
|
||||
# TODO(whc) how do I annotate a _RecordFunction here?
|
||||
) -> Any: ...
|
||||
|
||||
|
||||
class ProfilerEndHook(Protocol):
|
||||
def __call__(self, record: Any) -> None: ...
|
||||
|
||||
|
||||
class BytecodeHook(Protocol):
|
||||
def __call__(
|
||||
self, code: types.CodeType, new_code: types.CodeType
|
||||
) -> types.CodeType | None: ...
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,263 @@
|
||||
"""
|
||||
This package implements variable tracking and symbolic execution capabilities for Dynamo,
|
||||
which are essential for converting Python code into FX graphs. It provides a comprehensive
|
||||
set of variable types that handle different Python constructs during tracing.
|
||||
|
||||
Each variable type (like BuiltinVariable, TensorVariable, NNModuleVariable, etc.) is responsible
|
||||
for tracking and symbolically executing operations on specific Python objects. This enables
|
||||
Dynamo to:
|
||||
- Track the flow of values through Python code
|
||||
- Maintain correct semantics during graph conversion
|
||||
- Handle complex Python features like context managers, iterators, and custom objects
|
||||
- Support both eager and symbolic execution modes
|
||||
|
||||
The VariableTracker base class provides the foundation for all variable types, with each
|
||||
subclass implementing specific behavior for different Python constructs. This modular design
|
||||
allows Dynamo to accurately trace and optimize Python code while preserving its semantics.
|
||||
"""
|
||||
|
||||
from .base import VariableTracker
|
||||
from .builtin import (
|
||||
BaseBuiltinVariable,
|
||||
BuiltinVariable,
|
||||
DictBuiltinVariable,
|
||||
IterBuiltinVariable,
|
||||
ListBuiltinVariable,
|
||||
)
|
||||
from .constant import ConstantVariable
|
||||
from .ctx_manager import (
|
||||
CatchWarningsCtxManagerVariable,
|
||||
ContextWrappingVariable,
|
||||
CUDADeviceVariable,
|
||||
CudagraphOverrideVariable,
|
||||
DisabledSavedTensorsHooksVariable,
|
||||
DualLevelContextManager,
|
||||
DynamoConfigPatchVariable,
|
||||
ErrorOnGraphBreakVariable,
|
||||
FSDPParamGroupUseTrainingStateVariable,
|
||||
FxTracebackAnnotateVariable,
|
||||
GenericContextWrappingVariable,
|
||||
GradIncrementNestingCtxManagerVariable,
|
||||
GradInplaceRequiresGradCtxManagerVariable,
|
||||
GradModeVariable,
|
||||
InferenceModeVariable,
|
||||
JvpIncrementNestingCtxManagerVariable,
|
||||
SDPAKernelVariable,
|
||||
SetFwdGradEnabledContextManager,
|
||||
TemporarilyPopInterpreterStackCtxManagerVariable,
|
||||
VmapIncrementNestingCtxManagerVariable,
|
||||
WithEnterFunctionVariable,
|
||||
WithExitFunctionVariable,
|
||||
)
|
||||
from .dicts import (
|
||||
ConstDictVariable,
|
||||
DefaultDictVariable,
|
||||
DictItemsVariable,
|
||||
DunderDictVariable,
|
||||
MappingProxyVariable,
|
||||
NNModuleHooksDictVariable,
|
||||
)
|
||||
from .distributed import BackwardHookVariable, DistributedVariable
|
||||
from .functions import (
|
||||
BaseUserFunctionVariable,
|
||||
BuiltinMethodVariable,
|
||||
CollectionsNamedTupleFunction,
|
||||
CreateTMADescriptorExperimentalVariable,
|
||||
CreateTMADescriptorStableVariable,
|
||||
FunctionDecoratedByContextlibContextManagerVariable,
|
||||
FunctoolsPartialVariable,
|
||||
InspectSignatureVariable,
|
||||
LocalGeneratorFunctionVariable,
|
||||
LocalGeneratorObjectVariable,
|
||||
NestedUserFunctionVariable,
|
||||
PolyfilledFunctionVariable,
|
||||
PyTreeGetNodeTypeFunctionVariable,
|
||||
PyTreeTreeIsLeafFunctionVariable,
|
||||
SkipFunctionVariable,
|
||||
SparseTensorCreationSkipVariable,
|
||||
TMADescriptorExperimentalVariable,
|
||||
TMADescriptorStableVariable,
|
||||
TritonSetAllocatorVariable,
|
||||
UserFunctionVariable,
|
||||
UserMethodVariable,
|
||||
WrapperUserFunctionVariable,
|
||||
WrapperUserMethodVariable,
|
||||
)
|
||||
from .higher_order_ops import (
|
||||
FunctionalCallVariable,
|
||||
FunctorchHigherOrderVariable,
|
||||
ReparametrizeModuleCallVariable,
|
||||
TorchHigherOrderOperatorVariable,
|
||||
)
|
||||
from .iter import (
|
||||
CountIteratorVariable,
|
||||
FilterVariable,
|
||||
IteratorVariable,
|
||||
ItertoolsVariable,
|
||||
MapVariable,
|
||||
ObjectIteratorVariable,
|
||||
RepeatIteratorVariable,
|
||||
ZipVariable,
|
||||
)
|
||||
from .lazy import LazyConstantVariable, LazyVariableTracker
|
||||
from .lists import (
|
||||
BaseListVariable,
|
||||
ListIteratorVariable,
|
||||
ListVariable,
|
||||
RangeVariable,
|
||||
SliceVariable,
|
||||
TupleIteratorVariable,
|
||||
TupleVariable,
|
||||
)
|
||||
from .misc import (
|
||||
AutogradFunctionContextVariable,
|
||||
AutogradFunctionVariable,
|
||||
CellVariable,
|
||||
DeletedVariable,
|
||||
ExceptionVariable,
|
||||
GetAttrVariable,
|
||||
LambdaVariable,
|
||||
MethodWrapperVariable,
|
||||
NewGlobalVariable,
|
||||
NumpyVariable,
|
||||
ObjectVariable,
|
||||
PythonModuleVariable,
|
||||
RandomClassVariable,
|
||||
RandomVariable,
|
||||
StringFormatVariable,
|
||||
SuperVariable,
|
||||
TorchVersionVariable,
|
||||
TracebackVariable,
|
||||
TypingVariable,
|
||||
UnknownVariable,
|
||||
WeakRefVariable,
|
||||
)
|
||||
from .nn_module import (
|
||||
FSDPManagedNNModuleVariable,
|
||||
NNModuleVariable,
|
||||
UnspecializedBuiltinNNModuleVariable,
|
||||
UnspecializedNNModuleVariable,
|
||||
)
|
||||
from .optimizer import OptimizerVariable
|
||||
from .sdpa import SDPAParamsVariable
|
||||
from .sets import (
|
||||
DictKeySetVariable,
|
||||
FrozensetVariable,
|
||||
OrderedSetClassVariable,
|
||||
OrderedSetVariable,
|
||||
SetVariable,
|
||||
)
|
||||
from .streams import (
|
||||
CudaStreamVariable,
|
||||
EventVariable,
|
||||
StreamContextVariable,
|
||||
StreamVariable,
|
||||
)
|
||||
from .tensor import (
|
||||
DataPtrVariable,
|
||||
FakeItemVariable,
|
||||
NumpyNdarrayVariable,
|
||||
SymNodeVariable,
|
||||
TensorVariable,
|
||||
UnspecializedPythonVariable,
|
||||
UntypedStorageVariable,
|
||||
)
|
||||
from .torch import TorchCtxManagerClassVariable, TorchInGraphFunctionVariable
|
||||
from .user_defined import (
|
||||
FrozenDataClassVariable,
|
||||
InspectVariable,
|
||||
MutableMappingVariable,
|
||||
NamedTupleVariable,
|
||||
RemovableHandleVariable,
|
||||
StructSequenceVariable,
|
||||
UserDefinedClassVariable,
|
||||
UserDefinedConstantVariable,
|
||||
UserDefinedDictVariable,
|
||||
UserDefinedExceptionClassVariable,
|
||||
UserDefinedExceptionObjectVariable,
|
||||
UserDefinedListVariable,
|
||||
UserDefinedObjectVariable,
|
||||
UserDefinedSetVariable,
|
||||
UserDefinedTupleVariable,
|
||||
UserDefinedVariable,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AutogradFunctionContextVariable",
|
||||
"AutogradFunctionVariable",
|
||||
"BackwardHookVariable",
|
||||
"BaseBuiltinVariable",
|
||||
"BaseListVariable",
|
||||
"BuiltinVariable",
|
||||
"CatchWarningsCtxManagerVariable",
|
||||
"ConstantVariable",
|
||||
"ConstDictVariable",
|
||||
"DictBuiltinVariable",
|
||||
"ContextWrappingVariable",
|
||||
"CountIteratorVariable",
|
||||
"CreateTMADescriptorExperimentalVariable",
|
||||
"CreateTMADescriptorStableVariable",
|
||||
"CUDADeviceVariable",
|
||||
"CudagraphOverrideVariable",
|
||||
"DataPtrVariable",
|
||||
"DefaultDictVariable",
|
||||
"DeletedVariable",
|
||||
"DictKeySetVariable",
|
||||
"DynamoConfigPatchVariable",
|
||||
"FakeItemVariable",
|
||||
"GetAttrVariable",
|
||||
"GradModeVariable",
|
||||
"InspectSignatureVariable",
|
||||
"InspectVariable",
|
||||
"IterBuiltinVariable",
|
||||
"IteratorVariable",
|
||||
"ItertoolsVariable",
|
||||
"LambdaVariable",
|
||||
"LazyConstantVariable",
|
||||
"LazyVariableTracker",
|
||||
"ListBuiltinVariable",
|
||||
"ListIteratorVariable",
|
||||
"ListVariable",
|
||||
"NestedUserFunctionVariable",
|
||||
"CellVariable",
|
||||
"NewGlobalVariable",
|
||||
"NNModuleVariable",
|
||||
"NumpyNdarrayVariable",
|
||||
"NumpyVariable",
|
||||
"OptimizerVariable",
|
||||
"PolyfilledFunctionVariable",
|
||||
"PythonModuleVariable",
|
||||
"RangeVariable",
|
||||
"RemovableHandleVariable",
|
||||
"RepeatIteratorVariable",
|
||||
"SDPAParamsVariable",
|
||||
"ErrorOnGraphBreakVariable",
|
||||
"SkipFunctionVariable",
|
||||
"SliceVariable",
|
||||
"StringFormatVariable",
|
||||
"SuperVariable",
|
||||
"TemporarilyPopInterpreterStackCtxManagerVariable",
|
||||
"TensorVariable",
|
||||
"TMADescriptorExperimentalVariable",
|
||||
"TMADescriptorStableVariable",
|
||||
"TorchCtxManagerClassVariable",
|
||||
"TorchInGraphFunctionVariable",
|
||||
"TorchVersionVariable",
|
||||
"TupleVariable",
|
||||
"UnknownVariable",
|
||||
"UnspecializedNNModuleVariable",
|
||||
"UnspecializedPythonVariable",
|
||||
"UntypedStorageVariable",
|
||||
"UserDefinedClassVariable",
|
||||
"UserDefinedTupleVariable",
|
||||
"NamedTupleVariable",
|
||||
"StructSequenceVariable",
|
||||
"UserDefinedObjectVariable",
|
||||
"UserFunctionVariable",
|
||||
"UserMethodVariable",
|
||||
"VariableTracker",
|
||||
"WithEnterFunctionVariable",
|
||||
"WithExitFunctionVariable",
|
||||
"MappingProxyVariable",
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,482 @@
|
||||
"""
|
||||
Constant variable tracking in Dynamo.
|
||||
|
||||
This module is fundamental to Dynamo's ability to track and propagate constant
|
||||
values during compilation, ensuring proper handling of Python literals and
|
||||
maintaining type safety through the compilation process.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import operator
|
||||
from typing import Any, Literal, overload, TYPE_CHECKING
|
||||
from typing_extensions import override
|
||||
|
||||
import torch
|
||||
from torch._dynamo.source import GetItemSource
|
||||
|
||||
from .. import variables
|
||||
from ..exc import raise_observed_exception, unimplemented
|
||||
from ..utils import common_constant_types, istype, np, raise_args_mismatch
|
||||
from .base import ValueMutationNew, VariableTracker
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
from torch._dynamo.symbolic_convert import InstructionTranslator
|
||||
|
||||
from .functions import UserFunctionVariable
|
||||
|
||||
|
||||
class ConstantVariable(VariableTracker):
|
||||
"""
|
||||
Variable tracker for Python literals and basic immutable types, with automatic
|
||||
routing support for collection types (lists, tuples, sets, etc.).
|
||||
|
||||
The create() method intelligently constructs appropriate variable types for
|
||||
nested collections.
|
||||
"""
|
||||
|
||||
# PyLong_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/longobject.c#L6585
|
||||
# PyFloat_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/floatobject.c#L1880
|
||||
# PyBool_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/boolobject.c#L171
|
||||
# PyUnicode_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/unicodeobject.c#L14931
|
||||
# PyBytes_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/bytesobject.c#L3017
|
||||
# PyComplex_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/complexobject.c#L1099
|
||||
# _PyNone_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/object.c#L2022
|
||||
_cpython_type = (int, float, str, bytes, bool, type(None), complex, type(...))
|
||||
|
||||
@overload
|
||||
@staticmethod
|
||||
def create(value: None) -> ConstantVariable: ...
|
||||
|
||||
@overload
|
||||
@staticmethod
|
||||
def create(value: bool) -> ConstantVariable: ...
|
||||
|
||||
@overload
|
||||
@staticmethod
|
||||
def create(value: Any, **kwargs: Any) -> VariableTracker: ...
|
||||
|
||||
@staticmethod
|
||||
def create(value: Any, **kwargs: Any) -> VariableTracker:
|
||||
"""
|
||||
Create a `ConstantVariable` based on the given value, and supports
|
||||
automatic routing for collection types like `tuple` (in which case we'd
|
||||
create `ConstantVariable` for the leaf items).
|
||||
|
||||
NOTE: the caller must install the proper guards if needed; most often
|
||||
the guard will be `CONSTANT_MATCH`.
|
||||
"""
|
||||
# Return pre-allocated sentinels for None/True/False when there are
|
||||
# no extra kwargs (source, etc.) that would differentiate the instance.
|
||||
if not kwargs:
|
||||
match value:
|
||||
case None:
|
||||
return CONSTANT_VARIABLE_NONE
|
||||
case True:
|
||||
return CONSTANT_VARIABLE_TRUE
|
||||
case False:
|
||||
return CONSTANT_VARIABLE_FALSE
|
||||
|
||||
source = kwargs.get("source")
|
||||
|
||||
# Routing for supported collection literals.
|
||||
if isinstance(value, set):
|
||||
items = [ConstantVariable.create(x) for x in value]
|
||||
return variables.SetVariable(items, **kwargs) # type: ignore[arg-type]
|
||||
elif isinstance(value, frozenset):
|
||||
items = [ConstantVariable.create(x) for x in value]
|
||||
return variables.FrozensetVariable(items, **kwargs) # type: ignore[arg-type]
|
||||
elif isinstance(value, slice):
|
||||
slice_args = (value.start, value.stop, value.step)
|
||||
slice_args_vars = tuple(ConstantVariable.create(arg) for arg in slice_args)
|
||||
return variables.SliceVariable(slice_args_vars, **kwargs)
|
||||
elif isinstance(value, (list, tuple)):
|
||||
items = []
|
||||
for i, x in enumerate(value):
|
||||
item_source = GetItemSource(source, i) if source else None
|
||||
items.append(
|
||||
ConstantVariable.create(
|
||||
x,
|
||||
source=item_source,
|
||||
)
|
||||
)
|
||||
return variables.BaseListVariable.cls_for(type(value))(items, **kwargs)
|
||||
|
||||
return ConstantVariable(value, **kwargs)
|
||||
|
||||
def __init__(self, value: Any, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
assert ConstantVariable.is_base_literal(value), f"""
|
||||
Cannot construct `ConstantVariable` for value of type {type(value)}.
|
||||
|
||||
This failure likely due to PyTorch-internal use of `ConstantVariable` on
|
||||
non-literal python values, please try using `VariableTracker.build` instead. If
|
||||
you believe it's a necessary and legitimate use case (the value is immutable and
|
||||
can't easily be represented with another `VariableTracker` class), please add
|
||||
its type to `common_constant_types`.
|
||||
"""
|
||||
if np is not None and isinstance(value, np.number):
|
||||
self.value = value.item()
|
||||
else:
|
||||
self.value = value
|
||||
|
||||
def as_proxy(self) -> Any:
|
||||
return self.value
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ConstantVariable({type(self.value).__name__}: {repr(self.value)})"
|
||||
|
||||
def as_python_constant(self) -> Any:
|
||||
return self.value
|
||||
|
||||
def is_python_constant(self) -> Literal[True]:
|
||||
return True
|
||||
|
||||
def is_symnode_like(self) -> bool:
|
||||
return isinstance(self.value, (int, bool))
|
||||
|
||||
def is_constant_match(self, *values: Any) -> bool:
|
||||
return self.value in values
|
||||
|
||||
def is_constant_none(self) -> bool:
|
||||
return self.value is None
|
||||
|
||||
@property
|
||||
def items(self) -> list[VariableTracker]:
|
||||
"""
|
||||
Need this when adding a BaseListVariable and a ConstantVariable together.
|
||||
Happens in detectron2.
|
||||
"""
|
||||
return self.unpack_var_sequence(tx=None)
|
||||
|
||||
def getitem_const(
|
||||
self, tx: InstructionTranslator, arg: VariableTracker
|
||||
) -> VariableTracker:
|
||||
return ConstantVariable.create(
|
||||
self.value[arg.as_python_constant()],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_base_literal(obj: object) -> bool:
|
||||
return type(obj) in common_constant_types
|
||||
|
||||
@staticmethod
|
||||
def is_literal(obj: object, cache: dict[int, object] | None = None) -> bool:
|
||||
if cache is None:
|
||||
cache = {}
|
||||
if id(obj) in cache:
|
||||
# no-op if there is a cyclical reference
|
||||
return True
|
||||
if type(obj) in (list, tuple, set, frozenset, torch.Size):
|
||||
cache[id(obj)] = obj
|
||||
return all(ConstantVariable.is_literal(x, cache) for x in obj) # type: ignore[attr-defined]
|
||||
return ConstantVariable.is_base_literal(obj)
|
||||
|
||||
def unpack_var_sequence(
|
||||
self, tx: InstructionTranslator | None
|
||||
) -> list[VariableTracker]:
|
||||
try:
|
||||
return [ConstantVariable.create(x) for x in self.as_python_constant()]
|
||||
except TypeError as e:
|
||||
raise NotImplementedError from e
|
||||
|
||||
def len_impl(self, tx: InstructionTranslator) -> VariableTracker:
|
||||
"""Generic len for any constant value (sequence or mapping)."""
|
||||
try:
|
||||
return ConstantVariable.create(len(self.value))
|
||||
except TypeError as e:
|
||||
raise_observed_exception(type(e), tx, args=list(e.args))
|
||||
|
||||
def sq_length(self, tx: InstructionTranslator) -> VariableTracker:
|
||||
"""Sequence length - delegates to len_impl for constants."""
|
||||
return self.len_impl(tx)
|
||||
|
||||
def mp_length(self, tx: InstructionTranslator) -> VariableTracker:
|
||||
"""Mapping length - delegates to len_impl for constants."""
|
||||
return self.len_impl(tx)
|
||||
|
||||
def const_getattr(self, tx: InstructionTranslator, name: str) -> VariableTracker:
|
||||
if not hasattr(self.value, name):
|
||||
raise_observed_exception(AttributeError, tx, args=[name])
|
||||
member = getattr(self.value, name)
|
||||
if callable(member):
|
||||
raise NotImplementedError
|
||||
return member
|
||||
|
||||
def call_method(
|
||||
self,
|
||||
tx: InstructionTranslator,
|
||||
name: str,
|
||||
args: list[VariableTracker],
|
||||
kwargs: dict[str, VariableTracker],
|
||||
) -> VariableTracker:
|
||||
from .tensor import SymNodeVariable
|
||||
|
||||
if name == "format" and istype(self.value, str):
|
||||
return variables.BuiltinVariable(str.format).call_function(
|
||||
tx, [self, *args], kwargs
|
||||
)
|
||||
elif name == "join" and istype(self.value, str):
|
||||
if kwargs or len(args) != 1:
|
||||
raise_args_mismatch(
|
||||
tx,
|
||||
name,
|
||||
"1 args and 0 kwargs",
|
||||
f"{len(args)} args and {len(kwargs)} kwargs",
|
||||
)
|
||||
arg_unpacked = args[0].force_unpack_var_sequence(tx)
|
||||
try:
|
||||
arg_const = [x.as_python_constant() for x in arg_unpacked]
|
||||
return ConstantVariable.create(self.value.join(arg_const))
|
||||
except NotImplementedError:
|
||||
return super().call_method(tx, name, args, kwargs)
|
||||
elif name == "__iter__" and istype(self.value, str):
|
||||
# this could be some generic iterator to avoid the circular import,
|
||||
# but ListIterator does what we want
|
||||
from .lists import ListIteratorVariable
|
||||
|
||||
return ListIteratorVariable(
|
||||
self.unpack_var_sequence(tx), mutation_type=ValueMutationNew()
|
||||
)
|
||||
|
||||
if any(isinstance(x, SymNodeVariable) for x in args):
|
||||
# Promote to SymNodeVariable for operations involving dynamic shapes.
|
||||
return variables.SymNodeVariable.create(
|
||||
tx, self.as_proxy(), self.value
|
||||
).call_method(tx, name, args, kwargs)
|
||||
|
||||
try:
|
||||
const_args = [a.as_python_constant() for a in args]
|
||||
const_kwargs = {k: v.as_python_constant() for k, v in kwargs.items()}
|
||||
except NotImplementedError:
|
||||
return super().call_method(tx, name, args, kwargs)
|
||||
|
||||
if isinstance(self.value, str) and name in str.__dict__:
|
||||
method = getattr(self.value, name)
|
||||
try:
|
||||
return ConstantVariable.create(method(*const_args, **const_kwargs))
|
||||
except Exception as e:
|
||||
raise_observed_exception(type(e), tx)
|
||||
elif isinstance(self.value, (float, int)) and hasattr(self.value, name):
|
||||
if not (args or kwargs):
|
||||
try:
|
||||
return ConstantVariable.create(getattr(self.value, name)())
|
||||
except (OverflowError, ValueError) as exc:
|
||||
raise_observed_exception(
|
||||
type(exc),
|
||||
tx,
|
||||
args=list(exc.args),
|
||||
)
|
||||
if (
|
||||
hasattr(operator, name)
|
||||
and len(args) == 1
|
||||
and args[0].is_python_constant()
|
||||
):
|
||||
add_target = const_args[0]
|
||||
op = getattr(operator, name)
|
||||
if isinstance(
|
||||
add_target, (torch.SymBool, torch.SymFloat, torch.SymInt)
|
||||
):
|
||||
# Addition between a non sym and sym makes a sym
|
||||
proxy = tx.output.create_proxy(
|
||||
"call_function", op, (self.value, add_target), {}
|
||||
)
|
||||
return SymNodeVariable.create(tx, proxy, add_target)
|
||||
else:
|
||||
try:
|
||||
return ConstantVariable.create(op(self.value, add_target))
|
||||
except Exception as e:
|
||||
raise_observed_exception(type(e), tx, args=list(e.args))
|
||||
elif isinstance(self.value, bytes) and name == "decode":
|
||||
method = getattr(self.value, name)
|
||||
return ConstantVariable.create(method(*const_args, **const_kwargs))
|
||||
elif type(self.value) is complex and name in complex.__dict__:
|
||||
method = getattr(self.value, name)
|
||||
try:
|
||||
return ConstantVariable.create(method(*const_args, **const_kwargs))
|
||||
except Exception as e:
|
||||
raise_observed_exception(type(e), tx)
|
||||
|
||||
if name == "__round__" and len(args) == 1 and args[0].is_python_constant():
|
||||
try:
|
||||
return ConstantVariable.create(
|
||||
round(self.value, args[0].as_python_constant())
|
||||
)
|
||||
except Exception as e:
|
||||
raise_observed_exception(type(e), tx, args=list(e.args))
|
||||
elif name == "__contains__" and len(args) == 1 and args[0].is_python_constant():
|
||||
assert not kwargs
|
||||
search = args[0].as_python_constant()
|
||||
try:
|
||||
result = search in self.value
|
||||
return ConstantVariable.create(result)
|
||||
except TypeError as e:
|
||||
raise_observed_exception(type(e), tx, args=list(e.args))
|
||||
return super().call_method(tx, name, args, kwargs)
|
||||
|
||||
def call_tree_map(
|
||||
self,
|
||||
tx: InstructionTranslator,
|
||||
tree_map_fn: UserFunctionVariable,
|
||||
map_fn: VariableTracker,
|
||||
rest: Sequence[VariableTracker],
|
||||
tree_map_kwargs: dict[str, VariableTracker],
|
||||
) -> VariableTracker:
|
||||
if self.value is None:
|
||||
none_is_leaf_var = tree_map_kwargs.get("none_is_leaf")
|
||||
if none_is_leaf_var is not None:
|
||||
try:
|
||||
none_is_leaf = bool(none_is_leaf_var.as_python_constant())
|
||||
except NotImplementedError:
|
||||
return self._tree_map_fallback(
|
||||
tx,
|
||||
tree_map_fn,
|
||||
map_fn,
|
||||
rest,
|
||||
tree_map_kwargs,
|
||||
)
|
||||
else:
|
||||
tree_map_module = getattr(
|
||||
getattr(tree_map_fn, "fn", None), "__module__", ""
|
||||
)
|
||||
# torch.utils._pytree and torch.utils._cxx_pytree treat None as a leaf
|
||||
# by default, while optree keeps it as an internal node unless
|
||||
# none_is_leaf=True is provided.
|
||||
none_is_leaf = not tree_map_module.startswith("optree")
|
||||
if none_is_leaf:
|
||||
return map_fn.call_function(tx, [self, *rest], {})
|
||||
else:
|
||||
for other in rest:
|
||||
if not other.is_constant_none():
|
||||
return self._tree_map_fallback(
|
||||
tx,
|
||||
tree_map_fn,
|
||||
map_fn,
|
||||
rest,
|
||||
tree_map_kwargs,
|
||||
)
|
||||
return self.clone()
|
||||
if isinstance(self.value, (int, float, bool, complex, str, bytes, torch.dtype)):
|
||||
return map_fn.call_function(tx, [self, *rest], {})
|
||||
return super().call_tree_map(
|
||||
tx,
|
||||
tree_map_fn,
|
||||
map_fn,
|
||||
rest,
|
||||
tree_map_kwargs,
|
||||
)
|
||||
|
||||
@override
|
||||
def call_obj_hasattr(
|
||||
self, tx: InstructionTranslator, name: str
|
||||
) -> ConstantVariable:
|
||||
result = hasattr(self.value, name)
|
||||
return variables.ConstantVariable.create(result)
|
||||
|
||||
def is_python_hashable(self) -> Literal[True]:
|
||||
return True
|
||||
|
||||
def get_python_hash(self) -> int:
|
||||
return hash(self.value)
|
||||
|
||||
def is_python_equal(self, other: object) -> bool:
|
||||
from .tensor import SymNodeVariable
|
||||
|
||||
if isinstance(other, SymNodeVariable):
|
||||
return self.as_python_constant() == other.evaluate_expr()
|
||||
return (
|
||||
isinstance(other, VariableTracker)
|
||||
and self.as_python_constant() == other.as_python_constant()
|
||||
)
|
||||
|
||||
def get_real_python_backed_value(self) -> object:
|
||||
return self.value
|
||||
|
||||
def nb_index_impl(
|
||||
self,
|
||||
tx: Any,
|
||||
) -> VariableTracker:
|
||||
# CPython: int and bool define nb_index (returns self for int,
|
||||
# int(self) for bool). All other constant types do not.
|
||||
if isinstance(self.value, (int, bool)):
|
||||
return ConstantVariable.create(operator.index(self.value))
|
||||
return super().nb_index_impl(tx)
|
||||
|
||||
def nb_int_impl(
|
||||
self,
|
||||
tx: Any,
|
||||
) -> VariableTracker:
|
||||
# CPython: int defines nb_int (long_long, returns copy).
|
||||
# bool inherits nb_int from int via slot inheritance.
|
||||
# float defines nb_int (truncates toward zero via PyLong_FromDouble).
|
||||
return ConstantVariable.create(int(self.value))
|
||||
|
||||
def nb_float_impl(
|
||||
self,
|
||||
tx: Any,
|
||||
) -> VariableTracker:
|
||||
# CPython: float defines nb_float (float_float, returns copy).
|
||||
# int defines nb_float (long_float, converts to float).
|
||||
# bool inherits nb_float from int via slot inheritance.
|
||||
return ConstantVariable.create(float(self.value))
|
||||
|
||||
|
||||
CONSTANT_VARIABLE_NONE = ConstantVariable(None)
|
||||
CONSTANT_VARIABLE_TRUE = ConstantVariable(True)
|
||||
CONSTANT_VARIABLE_FALSE = ConstantVariable(False)
|
||||
|
||||
|
||||
class FakeIdVariable(VariableTracker):
|
||||
"""A compile-time-only id value that can be used as a dict key but cannot
|
||||
be reconstructed across graph breaks.
|
||||
|
||||
When dynamo evaluates ``id(x)`` on a variable tracker that has no
|
||||
corresponding runtime object (e.g. a ``ConstDictVariable`` created during
|
||||
tracing), we mint a fake integer id. This variable holds that id and
|
||||
supports the minimal interface needed to participate as a dict key
|
||||
(hashing and equality). It intentionally blocks reconstruction so that a
|
||||
graph break does not silently bake a stale id into the resumed bytecode.
|
||||
"""
|
||||
|
||||
# PyLong_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/longobject.c#L6585
|
||||
_cpython_type = int
|
||||
|
||||
def __init__(self, value: int, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.value = value
|
||||
|
||||
def as_python_constant(self) -> int:
|
||||
return self.value
|
||||
|
||||
def is_python_constant(self) -> bool:
|
||||
return False
|
||||
|
||||
def python_type(self) -> type:
|
||||
return int
|
||||
|
||||
def is_python_hashable(self) -> bool:
|
||||
return True
|
||||
|
||||
def get_python_hash(self) -> int:
|
||||
return hash(self.value)
|
||||
|
||||
def is_python_equal(self, other: object) -> bool:
|
||||
if isinstance(other, (FakeIdVariable, ConstantVariable)):
|
||||
return self.value == other.as_python_constant()
|
||||
return False
|
||||
|
||||
def reconstruct(self, codegen: Any) -> None:
|
||||
unimplemented(
|
||||
gb_type="Reconstruction of FakeIdVariable",
|
||||
context=str(self.value),
|
||||
explanation=(
|
||||
"A fake id produced by id() on a compile-time container "
|
||||
"cannot be reconstructed across a graph break."
|
||||
),
|
||||
hints=[
|
||||
"Avoid using id() on containers in code that may graph-break.",
|
||||
],
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,258 @@
|
||||
"""
|
||||
Distributed computing variable tracking classes for PyTorch Dynamo.
|
||||
|
||||
This module implements variable tracking for distributed computing components:
|
||||
- Process Groups (for collective communication)
|
||||
- Device Meshes (for distributed tensor sharding)
|
||||
- Placement Types (for specifying distribution strategies)
|
||||
- Distributed Tensors and their operations
|
||||
- Backward hooks for distributed module operations
|
||||
|
||||
These classes are responsible for tracking distributed operations during graph
|
||||
compilation while maintaining proper guards and handling distributed-specific
|
||||
behaviors. They ensure correct handling of distributed components like process
|
||||
groups, device meshes, and placement strategies while preserving proper semantics
|
||||
for distributed tensor operations in the compiled code.
|
||||
|
||||
The implementation provides special handling for distributed package availability
|
||||
checks and proper tracking of distributed state and operations across processes.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
from typing import Any, Literal, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
from torch.fx.experimental._backward_state import BackwardState
|
||||
|
||||
from .. import compiled_autograd
|
||||
from .._trace_wrapped_higher_order_op import trace_wrapped
|
||||
from ..exc import unimplemented
|
||||
from ..external_utils import call_module_hooks_from_backward_state
|
||||
from ..guards import GuardBuilder, install_guard
|
||||
from ..source import AttrSource
|
||||
from .base import VariableTracker
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch._dynamo.symbolic_convert import InstructionTranslator
|
||||
|
||||
|
||||
class DistributedVariable(VariableTracker):
|
||||
"""
|
||||
The base distributed variable that encapsulates common methods
|
||||
for the distributed objects (i.e. ProcessGroup, DeviceMesh, etc.).
|
||||
Concrete distributed objects could inherit this class and add object
|
||||
specific logic.
|
||||
|
||||
i.e. It provides the check on the distributed package existence
|
||||
and hold the tracking value for the corresponding distributed object.
|
||||
"""
|
||||
|
||||
def __init__(self, value: Any, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
if not DistributedVariable.is_available():
|
||||
unimplemented(
|
||||
gb_type="torch.distributed package is not available!",
|
||||
context="",
|
||||
explanation="The PyTorch package doesn't include torch.distributed when building from source.",
|
||||
hints=[
|
||||
"Set USE_DISTRIBUTED=1 to enable it when building PyTorch from source."
|
||||
],
|
||||
)
|
||||
self.value = value
|
||||
|
||||
def python_type(self) -> type:
|
||||
return type(self.value)
|
||||
|
||||
@staticmethod
|
||||
def is_available() -> bool:
|
||||
# check if the distributed package is available or not
|
||||
return torch.distributed.is_available()
|
||||
|
||||
def is_python_hashable(self) -> Literal[True]:
|
||||
return True
|
||||
|
||||
def get_python_hash(self) -> int:
|
||||
return hash(self.value)
|
||||
|
||||
def is_python_equal(self, other: object) -> bool:
|
||||
return (
|
||||
isinstance(other, VariableTracker)
|
||||
and self.as_python_constant() == other.as_python_constant()
|
||||
)
|
||||
|
||||
|
||||
def is_from_local(value: object) -> bool:
|
||||
if not DistributedVariable.is_available():
|
||||
return False
|
||||
from torch.distributed.tensor import DTensor
|
||||
|
||||
return inspect.isfunction(value) and value is DTensor.from_local
|
||||
|
||||
|
||||
def is_constant_pg_functions(value: object) -> bool:
|
||||
if not DistributedVariable.is_available():
|
||||
return False
|
||||
|
||||
from torch.distributed.distributed_c10d import (
|
||||
_get_group_size_by_name,
|
||||
_get_group_tag,
|
||||
_rank_not_in_group,
|
||||
_resolve_group_name_by_ranks_and_tag,
|
||||
get_process_group_ranks,
|
||||
)
|
||||
|
||||
constant_processgroup_functions = [
|
||||
_get_group_size_by_name,
|
||||
_get_group_tag,
|
||||
_rank_not_in_group,
|
||||
get_process_group_ranks,
|
||||
_resolve_group_name_by_ranks_and_tag,
|
||||
]
|
||||
|
||||
return inspect.isfunction(value) and value in constant_processgroup_functions
|
||||
|
||||
|
||||
class WorldMetaClassVariable(DistributedVariable):
|
||||
"""
|
||||
Tracks torch.distributed.GroupMember and torch.distributed.group, which are
|
||||
instances of the metaclass _WorldMeta.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def is_group_member_type(cls, value: object) -> bool:
|
||||
if not cls.is_available():
|
||||
return False
|
||||
|
||||
from torch.distributed.distributed_c10d import _WorldMeta
|
||||
|
||||
return type(value) is _WorldMeta
|
||||
|
||||
def python_type(self) -> type:
|
||||
return type(self.value)
|
||||
|
||||
def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker:
|
||||
if name == "WORLD":
|
||||
from .builder import SourcelessBuilder
|
||||
|
||||
assert self.source
|
||||
source = AttrSource(base=self.source, member="WORLD")
|
||||
install_guard(source.make_guard(GuardBuilder.ID_MATCH))
|
||||
return SourcelessBuilder.create(tx, self.value.WORLD)
|
||||
elif name == "NON_GROUP_MEMBER":
|
||||
assert self.source
|
||||
source = AttrSource(base=self.source, member="NON_GROUP_MEMBER")
|
||||
install_guard(source.make_guard(GuardBuilder.ID_MATCH))
|
||||
return VariableTracker.build(tx, self.value.NON_GROUP_MEMBER)
|
||||
return super().var_getattr(tx, name)
|
||||
|
||||
|
||||
class BackwardHookVariable(VariableTracker):
|
||||
"""
|
||||
Handles torch.utils.hooks.BackwardHook for module-level backward
|
||||
hooks.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
tx: "InstructionTranslator",
|
||||
module: VariableTracker,
|
||||
user_hooks: VariableTracker,
|
||||
user_pre_hooks: VariableTracker,
|
||||
) -> "BackwardHookVariable":
|
||||
if not compiled_autograd.compiled_autograd_enabled:
|
||||
unimplemented(
|
||||
gb_type="Module-level backwards hooks require compiled autograd.",
|
||||
context="",
|
||||
explanation="",
|
||||
hints=[
|
||||
"Enable compiled autograd by setting torch._dynamo.config.compiled_autograd = True."
|
||||
],
|
||||
)
|
||||
|
||||
def _in_graph_bw_hooks(
|
||||
bw_state: BackwardState,
|
||||
) -> torch.utils.hooks.BackwardHook:
|
||||
"""
|
||||
Rather than installing the user hooks in the graph (which
|
||||
don't survive AotAutograd), we install hooks that will call
|
||||
trace_wrapped in the backward pass that CompiledAutograd
|
||||
can turn into actual hook calls.
|
||||
"""
|
||||
return torch.utils.hooks.BackwardHook(
|
||||
None,
|
||||
(
|
||||
functools.partial(
|
||||
trace_wrapped,
|
||||
fn=call_module_hooks_from_backward_state,
|
||||
bw_state=bw_state,
|
||||
hooks_name=user_hooks_name,
|
||||
module_name=module_name,
|
||||
),
|
||||
),
|
||||
(
|
||||
functools.partial(
|
||||
trace_wrapped,
|
||||
fn=call_module_hooks_from_backward_state,
|
||||
bw_state=bw_state,
|
||||
hooks_name=user_pre_hooks_name,
|
||||
module_name=module_name,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
module_name, bw_state_proxy = tx.output.add_backward_state_hook(module, "mod")
|
||||
user_pre_hooks_name, _ = tx.output.add_backward_state_hook(user_pre_hooks)
|
||||
user_hooks_name, _ = tx.output.add_backward_state_hook(user_hooks)
|
||||
proxy = tx.output.create_proxy(
|
||||
"call_function",
|
||||
_in_graph_bw_hooks,
|
||||
(bw_state_proxy,),
|
||||
{},
|
||||
)
|
||||
proxy.node.meta["example_value"] = torch.utils.hooks.BackwardHook(None, (), ())
|
||||
return BackwardHookVariable(proxy, module, user_hooks, user_pre_hooks)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
proxy: torch.fx.Proxy,
|
||||
module: VariableTracker,
|
||||
user_hooks: VariableTracker,
|
||||
user_pre_hooks: VariableTracker,
|
||||
**options: Any,
|
||||
) -> None:
|
||||
super().__init__(**options)
|
||||
self.proxy = proxy
|
||||
self.module = module
|
||||
self.user_hooks = user_hooks
|
||||
self.user_pre_hooks = user_pre_hooks
|
||||
|
||||
def as_proxy(self) -> torch.fx.Proxy:
|
||||
return self.proxy
|
||||
|
||||
def call_method(
|
||||
self,
|
||||
tx: "InstructionTranslator",
|
||||
name: str,
|
||||
args: list[VariableTracker],
|
||||
kwargs: dict[str, VariableTracker],
|
||||
) -> VariableTracker:
|
||||
if name in ("setup_input_hook", "setup_output_hook"):
|
||||
return self._setup_hook(tx, name, *args, **kwargs)
|
||||
return super().call_method(tx, name, args, kwargs)
|
||||
|
||||
def _setup_hook(
|
||||
self, tx: "InstructionTranslator", hook_method_name: str, args: VariableTracker
|
||||
) -> VariableTracker:
|
||||
from .builder import wrap_fx_proxy
|
||||
|
||||
return wrap_fx_proxy(
|
||||
tx,
|
||||
tx.output.create_proxy(
|
||||
"call_method",
|
||||
hook_method_name,
|
||||
(self.as_proxy(), args.as_proxy()),
|
||||
{},
|
||||
),
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user