Initial import: grid-bot — grid trading bot for BTC-USDT on Cifra Markets

This commit is contained in:
Kolp
2026-09-24 13:22:23 +07:00
commit 642cc11a9f
18968 changed files with 5683248 additions and 0 deletions
@@ -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
@@ -0,0 +1,163 @@
"""
Hashability utilities for PyTorch Dynamo variable tracking.
This module provides the HashableTracker wrapper class and associated utilities
for making VariableTracker instances usable as dictionary keys and set elements
during symbolic execution. Used by both ConstDictVariable and SetVariable.
"""
from typing import TYPE_CHECKING
import torch
from .. import variables
from ..exc import raise_observed_exception
from ..utils import specialize_symnode
from .base import VariableTracker
if TYPE_CHECKING:
from torch._dynamo.symbolic_convert import InstructionTranslator
def raise_unhashable(
arg: VariableTracker, tx: "InstructionTranslator | None" = None
) -> None:
if tx is None:
from torch._dynamo.symbolic_convert import InstructionTranslator
tx = InstructionTranslator.current_tx()
try:
arg_type = arg.python_type()
except Exception:
arg_type = type(arg)
raise_observed_exception(
TypeError,
tx,
args=[
f"unhashable type: {arg_type!r} and variable tracker = {type(arg.realize())}",
],
)
def is_hashable(x: VariableTracker) -> bool:
# NB - performing isinstance check on a LazVT realizes the VT, accidentally
# inserting the guard. To avoid this, lazyVT `is_hashable` methods looks at
# the underlying value without realizing the VT. Consider updating the
# lazyVT `is_hashable` method if you see unnecessary guarding for a key VT.
if (
isinstance(x, variables.LazyVariableTracker)
and not x.is_realized()
and x.is_hashable()
):
return True
return x.is_python_hashable()
class HashableTracker:
"""
Class that wraps a VariableTracker and makes it hashable.
Note that it's fine to put VTs into dictionaries and sets, but doing so
does not take into account aliasing.
"""
_MISSING = object()
def __init__(self, vt: VariableTracker) -> None:
# We specialize SymNodes
vt = specialize_symnode(vt)
# If Dynamo does not know the hashability of the vt, it will raise unsupported here
# TODO(follow-up): check tp_hash via C-level slot detection — unhashable keys
# (e.g. list) should raise TypeError, not graph break via is_python_hashable/unimplemented.
if not is_hashable(vt):
raise_unhashable(vt)
self.vt = vt
@classmethod
def _maybe_constant_torch_size(cls, vt: VariableTracker) -> object:
from .lists import SizeVariable
from .tensor import TensorVariable
if (
isinstance(vt, variables.LazyVariableTracker)
and not vt.is_realized()
and isinstance(vt.original_value(), torch.Size)
):
return vt.original_value()
if not isinstance(vt, SizeVariable):
return cls._MISSING
items = []
for item in vt.items:
if item.is_python_constant():
items.append(item.as_python_constant())
continue
if isinstance(item, TensorVariable):
proxy = getattr(item, "proxy", None)
node = getattr(proxy, "node", None)
meta = getattr(node, "meta", None) if node is not None else None
example_value = (
meta.get("example_value") if isinstance(meta, dict) else None
)
constant = getattr(example_value, "constant", None)
if isinstance(constant, torch.Tensor) and constant.numel() == 1:
items.append(constant.item())
continue
return cls._MISSING
return torch.Size(items)
def __hash__(self) -> int:
"""
Computes the hash value for the wrapped VariableTracker.
For unrealized LazyVariableTrackers, uses the hash of the original value
to avoid realizing the tracker and inserting unnecessary guards.
For all other cases, delegates to the VariableTracker's get_python_hash method.
Returns:
The hash value of the underlying variable tracker
"""
if (
isinstance(self.vt, variables.LazyVariableTracker)
and not self.vt.is_realized()
and self.vt.is_hashable()
):
return hash(self.vt.original_value())
maybe_constant = self._maybe_constant_torch_size(self.vt)
if maybe_constant is not self._MISSING:
return hash(maybe_constant)
return self.vt.get_python_hash()
def __eq__(self, other: object) -> bool:
"""
Checks equality between two HashableTracker instances.
Delegates to the VariableTracker's is_python_equal method to compare
the underlying variable trackers for Python-level equality.
Args:
other: Another HashableTracker instance to compare with
Returns:
True if the underlying variable trackers are Python-equal, False otherwise
"""
if not isinstance(other, HashableTracker):
return False
if self.vt is other.vt:
return True
self_constant = self._maybe_constant_torch_size(self.vt)
other_constant = self._maybe_constant_torch_size(other.vt)
if self_constant is not self._MISSING and other_constant is not self._MISSING:
return self_constant == other_constant
return self.vt.is_python_equal(other.vt)
@@ -0,0 +1,657 @@
"""
This module provides iterator-related variable tracking functionality for Dynamo.
It implements variable classes for handling Python iterators and itertools functions
during symbolic execution and tracing.
The module includes:
- Base iterator variable classes for tracking iterator state
- Implementations of built-in iterators (zip, map, filter)
- Support for itertools functions (product, accumulate, combinations, etc.)
- Mutation tracking and reconstruction capabilities for iterator operations
These classes integrate with Dynamo's variable tracking system to enable proper
handling of iterator operations during code transformation and optimization.
"""
import itertools
import sys
from collections.abc import Callable, Sequence
from typing import Any, TYPE_CHECKING
from .. import graph_break_hints, polyfills, variables
from ..bytecode_transformation import (
create_build_tuple,
create_call_function,
create_call_function_ex,
create_instruction,
)
from ..exc import (
handle_observed_exception,
ObservedUserStopIteration,
raise_observed_exception,
unimplemented,
UserError,
)
from .base import ValueMutationNew, VariableTracker
from .constant import ConstantVariable
if TYPE_CHECKING:
from torch._dynamo.codegen import PyCodegen
from torch._dynamo.symbolic_convert import InstructionTranslator
MAX_ITERATOR_LIMIT = 100 * 1024 # 100k
class ItertoolsVariable(VariableTracker):
def __init__(self, value: Any, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.value = value
def __repr__(self) -> str:
return f"ItertoolsVariable({self.value})"
def as_python_constant(self) -> Any:
return self.value
def get_real_python_backed_value(self) -> Any:
return self.value
def call_function(
self,
tx: "InstructionTranslator",
args: Sequence["VariableTracker"],
kwargs: "dict[str, VariableTracker]",
) -> "VariableTracker":
# See also: module `torch._dynamo.polyfills.itertools`
if self.value is itertools.product:
if any(kw != "repeat" for kw in kwargs):
unimplemented(
gb_type="Unsupported kwargs for itertools.product",
context=f"call_function {self} {args} {kwargs}",
explanation=f"Expected kwargs: 'repeat', but got "
f"{','.join(set(kwargs.keys()) - {'repeat'})}",
hints=[*graph_break_hints.USER_ERROR],
)
if "repeat" in kwargs:
r = kwargs["repeat"].as_python_constant()
else:
r = 1
seqs = [arg.force_unpack_var_sequence(tx) for arg in args]
items = [
variables.TupleVariable(list(item))
for item in itertools.product(*seqs, repeat=r)
]
return variables.ListIteratorVariable(
items, # type: ignore[arg-type]
mutation_type=ValueMutationNew(),
)
elif (
self.value is itertools.combinations
and not kwargs
and len(args) == 2
and args[0].has_unpack_var_sequence(tx)
and args[1].is_python_constant()
):
iterable = args[0].unpack_var_sequence(tx)
r = args[1].as_python_constant()
items = []
for item in itertools.combinations(iterable, r):
items.append(variables.TupleVariable(list(item)))
return variables.ListIteratorVariable(
items, # type: ignore[arg-type]
mutation_type=ValueMutationNew(),
)
elif self.value is itertools.groupby:
if any(kw != "key" for kw in kwargs):
unimplemented(
gb_type="Unsupported kwargs for itertools.groupby",
context=f"call_function {self} {args} {kwargs}",
explanation=f"Expected kwargs: 'key', but got "
f"{','.join(set(kwargs.keys()) - {'key'})}",
hints=[*graph_break_hints.USER_ERROR],
)
def retrieve_const_key(key: VariableTracker) -> Any:
if isinstance(key, variables.SymNodeVariable):
return key.evaluate_expr()
elif key.is_python_constant():
return key.as_python_constant()
else:
unimplemented(
gb_type="Unsupported key type for itertools.groupby",
context=f"call_function {self} {args} {kwargs}",
explanation="Dynamo does not know how to trace "
f"itertools.groupby with key type: {str(type(key))}. "
"We only support grouping keys that are constants (int, float, str, etc.)",
hints=[*graph_break_hints.SUPPORTABLE],
)
if len(args) == 1 and args[0].has_unpack_var_sequence(tx):
seq = args[0].unpack_var_sequence(tx)
else:
unimplemented(
gb_type="Unsupported arguments for itertools.groupby",
context=f"call_function {self} {args} {kwargs}",
explanation="Dynamo does not know how to trace "
f"itertools.groupby with args: {args} and kwargs: {kwargs}. "
"itertools.groupby expects an iterable to group and an "
"optional key function to determine groupings.",
hints=[
"Make sure the arguments to itertools.groupby are correct.",
*graph_break_hints.SUPPORTABLE,
],
)
if "key" in kwargs:
def keyfunc(x: VariableTracker) -> Any:
return retrieve_const_key(
kwargs.get("key").call_function(tx, [x], {}) # type: ignore[union-attr]
)
else:
def keyfunc(x: VariableTracker) -> Any:
return retrieve_const_key(x)
result = []
try:
for k, v in itertools.groupby(seq, key=keyfunc):
result.append(
variables.TupleVariable(
[
(
variables.ConstantVariable.create(k)
if variables.ConstantVariable.is_literal(k)
else k
),
variables.ListIteratorVariable(
list(v), mutation_type=ValueMutationNew()
),
],
mutation_type=ValueMutationNew(),
)
)
except Exception as e:
unimplemented(
gb_type="Unexpected failure during itertools.groupby() iteration",
context=f"call_function {self} {args} {kwargs}",
explanation="Unexpected failure in invoking function during groupby",
hints=[*graph_break_hints.SUPPORTABLE],
from_exc=e,
)
return variables.ListIteratorVariable(
result, # type: ignore[arg-type]
mutation_type=ValueMutationNew(),
)
elif self.value is itertools.repeat:
if len(args) < 2:
return variables.RepeatIteratorVariable(
*args, mutation_type=ValueMutationNew()
)
return tx.inline_user_function_return(
VariableTracker.build(tx, polyfills.repeat), args, kwargs
)
elif self.value is itertools.count and not kwargs:
if len(args) == 0:
return variables.CountIteratorVariable(mutation_type=ValueMutationNew())
if len(args) == 1:
return variables.CountIteratorVariable(
item=args[0], mutation_type=ValueMutationNew()
)
if len(args) == 2:
return variables.CountIteratorVariable(
item=args[0],
step=args[1],
mutation_type=ValueMutationNew(),
)
return super().call_function(tx, args, kwargs)
elif (
self.value is itertools.permutations
and (len(args) == 1 or (len(args) == 2 and args[1].is_python_constant()))
and not kwargs
):
if len(args) == 2:
r = args[1].as_python_constant()
else:
r = None
items = [
variables.TupleVariable(list(item))
for item in itertools.permutations(
args[0].force_unpack_var_sequence(tx), r
)
]
return variables.ListIteratorVariable(
items, # type: ignore[arg-type]
mutation_type=ValueMutationNew(),
)
else:
return super().call_function(tx, args, kwargs)
class IteratorVariable(VariableTracker):
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
def next_variable(self, tx: "InstructionTranslator") -> VariableTracker:
unimplemented(
gb_type="Unimplemented next() call",
context=f"next({self})",
explanation="This abstract method must be implemented",
hints=[*graph_break_hints.DYNAMO_BUG],
)
# NOTE: only call when unpacking this iterator safely done eagerly!
# Normally, iterators are accessed lazily.
# Example of safe eager unpacking: list(map(f, seq))
# Example of unsafe eager unpacking: list(islice(map(f, seq), 5))
def force_unpack_var_sequence(
self, tx: "InstructionTranslator"
) -> list[VariableTracker]:
result: list[VariableTracker] = []
self.force_apply_to_var_sequence(tx, result.append)
return result
def force_apply_to_var_sequence(
self, tx: "InstructionTranslator", fn: Callable[[Any], Any]
) -> None:
while True:
try:
fn(self.next_variable(tx))
except ObservedUserStopIteration:
handle_observed_exception(tx)
break
# don't call force_unpack_var_sequence since it can mutate
# IteratorVariable state!
def has_force_unpack_var_sequence(self, tx: "InstructionTranslator") -> bool:
return True
def call_obj_hasattr(
self, tx: "InstructionTranslator", name: str
) -> "ConstantVariable":
if name == "__iter__" or name == "__next__":
return variables.ConstantVariable.create(True)
return super().call_obj_hasattr(tx, name)
def call_method(
self,
tx: "InstructionTranslator",
name: str,
args: list[VariableTracker],
kwargs: dict[str, VariableTracker],
) -> VariableTracker:
if name == "__iter__":
return self
elif name == "__next__":
return self.next_variable(tx)
return super().call_method(tx, name, args, kwargs)
class ObjectIteratorVariable(IteratorVariable):
"""
VariableTracker for iter(obj) that implements the iterator protocol (i.e.,
has a `__next__` method).
We use this class to track the state of the iterator and handle the case
when the iterator is exhausted:
Example usage:
> b = iter(obj)
> list(b) # exhaust the iterator
> list(b) # empty list
"""
def __init__(self, obj: VariableTracker, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.obj = obj
self.generator_exhausted = False
def next_variable(self, tx: "InstructionTranslator") -> VariableTracker:
if self.generator_exhausted:
raise_observed_exception(StopIteration, tx)
try:
return self.obj.next_variable(tx)
except ObservedUserStopIteration:
# Do not rely on the object to always return StopIteration once it
# is exhausted.
self.generator_exhausted = True
raise
class RepeatIteratorVariable(IteratorVariable):
def __init__(self, item: VariableTracker, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.item = item
def python_type(self) -> type:
return itertools.repeat
# Repeat needs no mutation, clone self
def next_variable(self, tx: "InstructionTranslator") -> VariableTracker:
return self.item
def reconstruct(self, codegen: "PyCodegen") -> None:
codegen.add_push_null(
lambda: codegen.extend_output(
[
codegen.create_load_python_module(itertools),
codegen.create_load_attr("repeat"),
]
)
)
codegen(self.item)
codegen.extend_output(create_call_function(1, False))
class CountIteratorVariable(IteratorVariable):
# advance_count tracks how many next() calls were made during tracing,
# used by side_effects.py to replay them on the real iterator post-execution.
_nonvar_fields = {
"advance_count",
*IteratorVariable._nonvar_fields,
}
def python_type(self) -> type:
return itertools.count
def __init__(
self,
item: int | VariableTracker = 0,
step: int | VariableTracker = 1,
advance_count: int = 0,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
if not isinstance(item, VariableTracker):
item = ConstantVariable.create(item)
if not isinstance(step, VariableTracker):
step = ConstantVariable.create(step)
self.item = item
self.step = step
self.advance_count = advance_count
def next_variable(self, tx: "InstructionTranslator") -> VariableTracker:
assert self.is_mutable()
old_item = self.item
tx.output.side_effects.mutation(self)
self.item = self.item.call_method(tx, "__add__", [self.step], {})
self.advance_count += 1
return old_item
def reconstruct(self, codegen: "PyCodegen") -> None:
codegen.add_push_null(
lambda: codegen.extend_output(
[
codegen.create_load_python_module(itertools),
codegen.create_load_attr("count"),
]
)
)
codegen(self.item)
codegen(self.step)
codegen.extend_output(create_call_function(2, False))
class ZipVariable(IteratorVariable):
"""
Represents zip(*iterables)
"""
# PyZip_Type: https://github.com/python/cpython/blob/v3.13.0/Python/bltinmodule.c#L3011
_cpython_type = zip
_nonvar_fields = {
"index",
"strict",
*IteratorVariable._nonvar_fields,
}
def __init__(
self,
iterables: list[VariableTracker],
strict: bool = False,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
assert isinstance(iterables, list)
# can be list[Variable] or VariableTracker (with next_variable implemented)
self.iterables = iterables
self.index = 0
self.strict = strict
def python_type(self) -> type[zip]: # type: ignore[type-arg]
return zip
def has_unpack_var_sequence(self, tx: "InstructionTranslator") -> bool:
return all(
isinstance(it, list) or it.has_unpack_var_sequence(tx)
for it in self.iterables
)
def unpack_var_sequence(
self, tx: "InstructionTranslator"
) -> list["VariableTracker"]:
assert self.has_unpack_var_sequence(tx)
iterables = []
for it in self.iterables:
if isinstance(it, list):
iterables.append(it[self.index :])
else:
iterables.append(it.unpack_var_sequence(tx))
kwargs = {"strict": self.strict} if self.strict else {}
zipped = zip(*iterables, **kwargs)
return [variables.TupleVariable(list(var)) for var in zipped]
def next_variable(self, tx: "InstructionTranslator") -> VariableTracker:
assert self.is_mutable()
if len(self.iterables) == 0:
raise_observed_exception(StopIteration, tx)
old_index = self.index
args = []
def get_item(
it: list[VariableTracker] | VariableTracker,
) -> VariableTracker:
if isinstance(it, list):
if old_index >= len(it):
raise_observed_exception(StopIteration, tx)
return it[old_index]
else:
return it.next_variable(tx)
idx: int | None = None
try:
for idx, it in enumerate(self.iterables): # noqa:B007
args.append(get_item(it))
except ObservedUserStopIteration:
if self.strict:
if idx == 0:
# all other iterables should be exhausted
for it in self.iterables:
try:
get_item(it)
except ObservedUserStopIteration:
handle_observed_exception(tx)
continue
# no ObservedUserStopIteration - fall through to UserError
break
else:
# all iterables exhausted, raise original error
raise
handle_observed_exception(tx)
raise UserError(
ValueError, # type: ignore[arg-type]
"zip() has one argument of len differing from others",
) from None
raise
tx.output.side_effects.mutation(self)
self.index += 1
return variables.TupleVariable(args)
def reconstruct_items(self, codegen: "PyCodegen") -> None:
for it in self.iterables:
if isinstance(it, list):
remaining_items = it[self.index :]
codegen.foreach(remaining_items)
codegen.append_output(create_build_tuple(len(remaining_items)))
else:
codegen(it)
def reconstruct(self, codegen: "PyCodegen") -> None:
codegen.add_push_null(
lambda: codegen.load_import_from("builtins", "zip"), call_function_ex=True
)
self.reconstruct_items(codegen)
codegen.append_output(create_build_tuple(len(self.iterables)))
codegen.extend_output(
[
codegen.create_load_const("strict"),
codegen.create_load_const(self.strict),
create_instruction("BUILD_MAP", arg=1),
*create_call_function_ex(True, False),
]
)
class MapVariable(ZipVariable):
"""
Represents map(fn, *iterables)
"""
# PyMap_Type: https://github.com/python/cpython/blob/v3.13.0/Python/bltinmodule.c#L1484
_cpython_type = map
def __init__(
self,
fn: VariableTracker,
iterables: list[VariableTracker],
**kwargs: Any,
) -> None:
super().__init__(iterables, **kwargs)
self.fn = fn
def python_type(self) -> type:
return map
def has_unpack_var_sequence(self, tx: "InstructionTranslator") -> bool:
return False
def next_variable(self, tx: "InstructionTranslator") -> VariableTracker:
args = super().next_variable(tx)
return self.fn.call_function(tx, args.items, {}) # type: ignore[attr-defined]
def reconstruct(self, codegen: "PyCodegen") -> None:
codegen.add_push_null(
lambda: codegen.load_import_from("builtins", "map"), call_function_ex=True
)
codegen(self.fn)
self.reconstruct_items(codegen)
codegen.append_output(create_build_tuple(len(self.iterables) + 1))
if self.strict:
assert sys.version_info >= (3, 14), (
"Unexpected bug: map(strict=True) requires Python 3.14+"
)
codegen.extend_output(
[
codegen.create_load_const("strict"),
codegen.create_load_const(self.strict),
create_instruction("BUILD_MAP", arg=1),
*create_call_function_ex(True, False),
]
)
else:
codegen.extend_output(create_call_function_ex(False, False))
class FilterVariable(IteratorVariable):
"""
Represents filter(fn, iterable)
"""
# PyFilter_Type: https://github.com/python/cpython/blob/v3.13.0/Python/bltinmodule.c#L630
_cpython_type = filter
_nonvar_fields = {
"index",
*IteratorVariable._nonvar_fields,
}
def __init__(
self,
fn: VariableTracker,
iterable: list[VariableTracker],
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
self.fn = fn
self.iterable = iterable
self.index = 0
def python_type(self) -> type:
return filter
def has_unpack_var_sequence(self, tx: "InstructionTranslator") -> bool:
return isinstance(self.iterable, list) or self.iterable.has_unpack_var_sequence(
tx
)
def unpack_var_sequence(
self, tx: "InstructionTranslator"
) -> list["VariableTracker"]:
assert self.has_unpack_var_sequence(tx)
it = None
if isinstance(self.iterable, list):
it = self.iterable[self.index :]
else:
it = self.iterable.unpack_var_sequence(tx)
filtered = self.fn.call_function(tx, it, {})
return [variables.TupleVariable([filtered])]
def next_variable(self, tx: "InstructionTranslator") -> VariableTracker:
def _next() -> VariableTracker:
old_index = self.index
if isinstance(self.iterable, list):
if old_index >= len(self.iterable):
raise_observed_exception(StopIteration, tx)
return self.iterable[old_index]
else:
return self.iterable.next_variable(tx)
# A do-while loop to find elements that make fn return true
while True:
item = _next()
self.index += 1
if self.fn.is_constant_none():
res = item
else:
res = self.fn.call_function(tx, [item], {})
pred_res = variables.UserFunctionVariable(
polyfills.predicate # type: ignore[arg-type]
).call_function(tx, [res], {})
if pred_res.as_python_constant():
return item
def reconstruct_items(self, codegen: "PyCodegen") -> None:
if isinstance(self.iterable, list):
remaining_items = self.iterable[self.index :]
codegen.foreach(remaining_items)
codegen.append_output(create_build_tuple(len(remaining_items)))
else:
codegen(self.iterable)
def reconstruct(self, codegen: "PyCodegen") -> None:
codegen.add_push_null(lambda: codegen.load_import_from("builtins", "filter"))
codegen(self.fn)
self.reconstruct_items(codegen)
codegen.extend_output(create_call_function(2, False))
@@ -0,0 +1,306 @@
from __future__ import annotations
import collections
import functools
import inspect
from typing import Any, TYPE_CHECKING
from ..utils import is_function_or_wrapper
from .base import VariableTracker, VariableTrackerMeta
if TYPE_CHECKING:
from collections.abc import Callable
from typing_extensions import Self
from .tensor import SymNodeVariable
class LazyCache:
"""Container to cache the real VariableTracker"""
def __init__(self, value: Any, source: Any) -> None:
if not isinstance(value, LazySymNodeFormatString):
assert source
self.value = value
self.source = source
self.name_hint: str | None = None
self.vt: VariableTracker | None = None
def realize(self) -> None:
assert self.vt is None
from ..symbolic_convert import InstructionTranslator
from . import builder
tx = InstructionTranslator.current_tx()
if isinstance(self.value, LazySymNodeFormatString):
self.vt = builder.SourcelessBuilder.create(tx, self.value)
else:
# Pass allow_lazy_constant=False to prevent VariableBuilder from
# returning LazyConstantVariable, which would cause infinite recursion
# when LazyVariableTracker.realize() returns LazyConstantVariable.
self.vt = builder.VariableBuilder(
tx, self.source, allow_lazy_constant=False
)(self.value)
if self.name_hint is not None:
self.vt.set_name_hint(self.name_hint)
del self.value
del self.source
del self.name_hint
class LazyVariableTracker(VariableTracker, metaclass=VariableTrackerMeta):
"""
A structure that defers the creation of the actual VariableTracker
for a given underlying value until it is accessed.
The `realize` function invokes VariableTracker.build() to produce the real object.
Once a LazyVariableTracker has been realized, internal bookkeeping will
prevent double realization.
This object should be utilized for processing containers, or objects that
reference other objects where we may not want to take on creating all the
VariableTrackers right away.
"""
# Flag to prevent implicit realization in isinstance checks (inherited by subclasses)
_no_implicit_realize = True
_nonvar_fields = {"_cache", *VariableTracker._nonvar_fields}
@staticmethod
def create(value: Any, source: Any, **options: Any) -> VariableTracker:
if type(value) in LazyConstantVariable.supported_types:
return LazyConstantVariable.create(value, source, **options)
# Cache based on source when no extra options are passed
if source is not None and not options:
from ..symbolic_convert import InstructionTranslator
tx = InstructionTranslator.current_tx()
if tx is not None:
cache = tx.output.variable_tracker_cache
cached = cache.get(source)
if cached is not None:
return cached
vt = LazyVariableTracker(LazyCache(value, source), source=source)
cache[source] = vt
return vt
return LazyVariableTracker(LazyCache(value, source), source=source, **options)
def __init__(self, _cache: LazyCache, **kwargs: Any) -> None:
assert isinstance(_cache, LazyCache)
super().__init__(**kwargs)
self._cache = _cache
def realize(self) -> VariableTracker:
"""Force construction of the real VariableTracker"""
if self._cache.vt is None:
self._cache.realize()
assert self._cache.vt is not None
return self._cache.vt
def lazy_isinstance(self, cls: type) -> bool:
"""Check isinstance after realizing, used by ImplicitRealizingVariableTrackerMeta"""
return type.__instancecheck__(cls, self.realize())
def unwrap(self) -> VariableTracker | Self:
"""Return the real VariableTracker if it already exists"""
if self.is_realized():
assert self._cache.vt is not None
return self._cache.vt
return self
def is_realized(self) -> bool:
return self._cache.vt is not None
def clone(self, **kwargs: Any) -> VariableTracker:
assert kwargs.get("_cache", self._cache) is self._cache
if kwargs.get("source", self.source) is not self.source:
self.realize()
return VariableTracker.clone(self.unwrap(), **kwargs)
def peek_type(self) -> type[Any]:
assert not self.is_realized()
return type(self._cache.value)
def peek_value(self) -> Any:
assert not self.is_realized()
return self._cache.value
def set_name_hint(self, name: str) -> None:
if self.is_realized():
self._cache.vt.set_name_hint(name) # type: ignore[union-attr]
else:
self._cache.name_hint = name
def __str__(self) -> str:
variable_info = "LazyVariableTracker("
if self.is_realized():
variable_info += f"realized: {repr(self.unwrap())})"
else:
variable_info += f"unrealized: {self.peek_type()})"
return variable_info
def __getattr__(self, item: str) -> Any:
return getattr(self.realize(), item)
# most methods are auto-generated below, these are the ones we want to exclude
visit = VariableTracker.visit # type: ignore[assignment]
__repr__ = __str__
@classmethod
def realize_all(
cls,
value: Any,
cache: dict[int, tuple[Any, Any]] | None = None,
*,
allow_lazy_constant: bool = False,
) -> Any:
"""
Walk an object and realize all LazyVariableTrackers inside it.
"""
if cache is None:
cache = {}
idx = id(value)
if idx in cache:
return cache[idx][0]
value_cls = type(value)
if issubclass(value_cls, LazyVariableTracker):
# Allow LazyConstantVariable to stay lazy when returning from a frame
keep_lazy = allow_lazy_constant and isinstance(value, LazyConstantVariable)
if keep_lazy:
result = value
else:
result = cls.realize_all(
value.realize(), cache, allow_lazy_constant=allow_lazy_constant
)
elif issubclass(value_cls, VariableTracker):
# update value in-place
result = value
# update cache now to prevent infinite recursion
cache[idx] = (result, value)
value_dict = value.__dict__
nonvars = value._nonvar_fields
for key in value_dict:
if key not in nonvars:
value_dict[key] = cls.realize_all(
value_dict[key], cache, allow_lazy_constant=allow_lazy_constant
)
elif value_cls is list:
result = [
cls.realize_all(v, cache, allow_lazy_constant=allow_lazy_constant)
for v in value
]
elif value_cls is tuple:
result = tuple(
cls.realize_all(v, cache, allow_lazy_constant=allow_lazy_constant)
for v in value
)
elif value_cls in (dict, collections.OrderedDict):
result = {
k: cls.realize_all(v, cache, allow_lazy_constant=allow_lazy_constant)
for k, v in list(value.items())
}
else:
result = value
# save `value` to keep it alive and ensure id() isn't reused
cache[idx] = (result, value)
return result
def is_hashable(self) -> bool:
# Checks that the underlying value is hashable without realizing the VT.
# This is used by ConstDictVariable tracker to find if the key LazyVT
# can be hashed.
def _helper(value: Any) -> bool:
# TODO: Add support for more types
return (
inspect.isbuiltin(value)
or issubclass(type(value), type)
or is_function_or_wrapper(value)
)
assert not self.is_realized()
value = self._cache.value
if isinstance(value, tuple):
return all(_helper(v) for v in value)
return _helper(value)
def original_value(self) -> Any:
# Returns the value without realizing the VT.
assert not self.is_realized()
return self._cache.value
def original_source(self) -> Any:
# Returns the source without realizing the VT.
assert not self.is_realized()
return self._cache.source
class LazyConstantVariable(LazyVariableTracker):
"""
A lazy variable tracker for constants (int, float, bool, str) that defers
guarding until the value is actually used in a way that requires it.
This allows constants that are just passed through (e.g., returned without
being used in control flow or math) to avoid unnecessary recompilation when
their values change.
"""
supported_types = (int, float, bool, str)
@staticmethod
def create(
value: Any,
source: Any,
**options: Any,
) -> LazyConstantVariable:
assert type(value) in LazyConstantVariable.supported_types
return LazyConstantVariable(LazyCache(value, source), source=source, **options)
class LazySymNodeFormatString:
def __init__(
self, sym_node_variable: SymNodeVariable, fmt_spec_var: VariableTracker
) -> None:
from .constant import ConstantVariable
self.sym_node_var = sym_node_variable
self.fmt_var = ConstantVariable.create(
"{:" + fmt_spec_var.as_python_constant() + "}"
)
def __repr__(self) -> str:
return str.format(
self.fmt_var.as_python_constant(),
str(self.sym_node_var.evaluate_expr()),
)
def _create_realize_and_forward(
name: str,
) -> Callable[[LazyVariableTracker, Any, Any], Any]:
@functools.wraps(getattr(VariableTracker, name))
def realize_and_forward(
self: LazyVariableTracker, *args: Any, **kwargs: Any
) -> Any:
return getattr(self.realize(), name)(*args, **kwargs)
return realize_and_forward
def _populate() -> None:
for name, value in VariableTracker.__dict__.items():
if name not in LazyVariableTracker.__dict__:
if callable(value):
setattr(LazyVariableTracker, name, _create_realize_and_forward(name))
_populate()
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,319 @@
"""
Dynamo implementations of CPython's PyObject_* default slot algorithms.
Analogous to CPython's Objects/object.c, this module holds the general
dispatch machinery that is independent of any specific type.
Per-type hook implementations (bool_impl, richcompare_impl, etc.)
live in their respective VT files.
"""
from functools import lru_cache
from typing import TYPE_CHECKING
from torch._C._dynamo import (
get_type_slots,
has_slot,
PyMappingSlots,
PyNumberSlots,
PySequenceSlots,
)
from .. import graph_break_hints
from ..exc import (
handle_observed_exception,
ObservedTypeError,
raise_observed_exception,
raise_type_error,
unimplemented,
)
from ..utils import istype
from .base import NO_SUCH_SUBOBJ, VariableTracker
from .constant import ConstantVariable
if TYPE_CHECKING:
from ..symbolic_convert import InstructionTranslator
def vt_identity_compare(
left: VariableTracker,
right: VariableTracker,
) -> "VariableTracker | None":
"""Try to determine Python identity (left is right) at trace time.
Returns ConstantVariable(True/False) if determinable, else None.
Mirrors the logic in BuiltinVariable's handle_is handler.
"""
if left is right:
return ConstantVariable.create(True)
left_val = left.get_real_python_backed_value()
right_val = right.get_real_python_backed_value()
left_known = left_val is not NO_SUCH_SUBOBJ
right_known = right_val is not NO_SUCH_SUBOBJ
if left_known and right_known:
return (
ConstantVariable.create(True)
if left_val is right_val
else ConstantVariable.create(False)
)
# One side has a concrete backing object, the other doesn't — they can't
# be the same object.
if left_known != right_known:
return ConstantVariable.create(False)
# Mutable containers created during tracing: VT identity = Python identity.
from .dicts import ConstDictVariable
from .lists import ListVariable
from .sets import SetVariable
if isinstance(left, (ConstDictVariable, ListVariable, SetVariable)):
return ConstantVariable.create(False)
# Different Python types can never be the same object.
try:
if left.python_type() is not right.python_type():
return ConstantVariable.create(False)
except NotImplementedError:
pass
# Different exception types are never identical.
from .. import variables
if (
istype(left, variables.ExceptionVariable)
and istype(right, variables.ExceptionVariable)
and left.exc_type is not right.exc_type # type: ignore[attr-defined]
):
return ConstantVariable.create(False)
return None
@lru_cache(maxsize=256)
def _get_cached_slots(obj_type: type) -> tuple[int, int, int, int]:
"""Get all type slots for a type (cached)."""
return get_type_slots(obj_type)
def type_implements_sq_length(obj_type: type) -> bool:
"""Check whether obj_type implements __len__ as sequence protocol"""
seq_slots, _, _, _ = _get_cached_slots(obj_type)
return has_slot(seq_slots, PySequenceSlots.SQ_LENGTH)
def type_implements_mp_length(obj_type: type) -> bool:
"""Check whether obj_type implements __len__ as mapping protocol"""
_, map_slots, _, _ = _get_cached_slots(obj_type)
return has_slot(map_slots, PyMappingSlots.MP_LENGTH)
def type_implements_nb_bool(obj_type: type) -> bool:
"""Check whether obj_type implements the nb_bool slot (i.e. has __bool__ or __len__)."""
_, _, number_slots, _ = _get_cached_slots(obj_type)
return has_slot(number_slots, PyNumberSlots.NB_BOOL)
def type_implements_nb_int(obj_type: type) -> bool:
"""Check whether obj_type implements the nb_int slot."""
_, _, number_slots, _ = _get_cached_slots(obj_type)
return has_slot(number_slots, PyNumberSlots.NB_INT)
def type_implements_nb_index(obj_type: type) -> bool:
"""Check whether obj_type implements the nb_index slot."""
_, _, number_slots, _ = _get_cached_slots(obj_type)
return has_slot(number_slots, PyNumberSlots.NB_INDEX)
def type_implements_nb_float(obj_type: type) -> bool:
"""Check whether obj_type implements the nb_float slot."""
_, _, number_slots, _ = _get_cached_slots(obj_type)
return has_slot(number_slots, PyNumberSlots.NB_FLOAT)
def maybe_get_python_type(obj: VariableTracker) -> type:
try:
return obj.python_type()
except NotImplementedError:
unimplemented(
gb_type="Unsupported python_type() call",
context=f"{obj} does not implement python_type()",
explanation="This VariableTracker does not implement python_type(), "
"which is required for object protocol operations.",
hints=[
*graph_break_hints.DYNAMO_BUG,
],
)
def vt_mapping_size(
tx: "InstructionTranslator", obj: "VariableTracker"
) -> "VariableTracker":
# ref: https://github.com/python/cpython/blob/v3.13.3/Objects/abstract.c#L2308-L2330
T = maybe_get_python_type(obj)
if type_implements_mp_length(T):
return obj.mp_length(tx)
if type_implements_sq_length(T):
raise_type_error(tx, f"{obj.python_type_name()} is not a mapping")
raise_type_error(tx, f"object of type {obj.python_type_name()} has no len()")
def generic_len(
tx: "InstructionTranslator", obj: "VariableTracker"
) -> "VariableTracker":
# ref: https://github.com/python/cpython/blob/v3.13.3/Objects/abstract.c#L53-L69
"""
Implements PyObject_Size/PyObject_Length semantics for VariableTracker objects.
Dispatches to sq_length (sequences) or mp_length (mappings) depending on the VT type.
"""
T = maybe_get_python_type(obj)
if type_implements_sq_length(T):
return obj.sq_length(tx)
return vt_mapping_size(tx, obj)
def generic_bool(tx: "InstructionTranslator", obj: VariableTracker) -> VariableTracker:
"""Mirrors PyObject_IsTrue.
https://github.com/python/cpython/blob/c09ccd9c429/Objects/object.c#L2135-L2158
Resolution order: constants → nb_bool → mp_length/sq_length → truthy.
"""
from .constant import ConstantVariable
if obj.is_python_constant():
return ConstantVariable.create(bool(obj.as_python_constant()))
obj_type = maybe_get_python_type(obj)
if type_implements_nb_bool(obj_type):
result = obj.bool_impl(tx)
if result is not None:
return result
try:
length = generic_len(tx, obj)
from .tensor import SymNodeVariable
if isinstance(length, SymNodeVariable):
return SymNodeVariable.create(tx, length.as_proxy() > 0)
return ConstantVariable.create(length.as_python_constant() > 0)
except ObservedTypeError:
handle_observed_exception(tx)
return ConstantVariable.create(True)
def vt_getitem(
tx: "InstructionTranslator",
obj: VariableTracker,
key: VariableTracker,
) -> VariableTracker:
"""CPython's PyObject_GetItem — dispatch to the type's mp_subscript/sq_item.
PyObject_GetItem: https://github.com/python/cpython/blob/62a6e898e01/Objects/abstract.c#L155-L206
CPython checks three branches in order:
1. tp_as_mapping->mp_subscript (L161-166)
2. tp_as_sequence->sq_item (L168-181) — only if key passes _PyIndex_Check
3. PyType_Check(o) (L183-203) — type[int] → GenericAlias/__class_getitem__
Branch 1 is the common path (list, tuple, dict, range all have mp_subscript).
TODO(follow-up): use has_slot(map_slots, PyMappingSlots.MP_SUBSCRIPT) to gate
Branch 1 and has_slot(seq_slots, PySequenceSlots.SQ_ITEM) to gate Branch 2,
matching CPython's dispatch order.
TODO(follow-up): Branch 2 (sq_item) for C extension types that only have
tp_as_sequence (e.g. deque — Modules/_collectionsmodule.c:1888).
Branch 3 is handled by TypingVariable.mp_subscript_impl for typing module types
and by BuiltinVariable for builtin types like list[int].
Types that work via constant fold fallback (no dedicated mp_subscript_impl):
TODO(follow-up): str (unicode_subscript, Objects/unicodeobject.c:13809)
TODO(follow-up): bytes (bytes_subscript, Objects/bytesobject.c)
"""
return obj.mp_subscript_impl(tx, key)
def generic_int(tx: "InstructionTranslator", obj: VariableTracker) -> VariableTracker:
"""Mirrors PyNumber_Long (int(x) dispatch).
https://github.com/python/cpython/blob/v3.13.0/Objects/abstract.c#L1520-L1632
Resolution: nb_int → nb_index → str/bytes/bytearray parsing → TypeError.
"""
from .constant import ConstantVariable
# Fast path for int (sub)class instances — mirrors PyLong_Check at the
# top of PyNumber_Long (abstract.c:1531). Avoids infinite recursion for
# int subclasses like IntEnum whose __int__ calls int() again.
if obj.is_python_constant() and isinstance(obj.as_python_constant(), int):
return ConstantVariable.create(int(obj.as_python_constant()))
obj_type = maybe_get_python_type(obj)
if type_implements_nb_int(obj_type):
return obj.nb_int_impl(tx)
if type_implements_nb_index(obj_type):
return obj.nb_index_impl(tx)
# String/bytes/bytearray parsing fallback.
# https://github.com/python/cpython/blob/v3.13.0/Objects/abstract.c#L1598-L1612
if obj.is_python_constant() and isinstance(
obj.as_python_constant(), (str, bytes, bytearray)
):
try:
return ConstantVariable.create(int(obj.as_python_constant()))
except ValueError as e:
raise_observed_exception(ValueError, tx, args=[str(e)])
raise_type_error(
tx,
f"int() argument must be a string, a bytes-like object "
f"or a real number, not '{obj.python_type_name()}'",
)
def generic_float(tx: "InstructionTranslator", obj: VariableTracker) -> VariableTracker:
"""Mirrors PyNumber_Float (float(x) dispatch).
https://github.com/python/cpython/blob/v3.13.0/Objects/abstract.c#L1635-L1692
Resolution: nb_float → nb_index → str parsing → TypeError.
"""
from .constant import ConstantVariable
# Fast path: if the value is already a float constant, return it directly.
# Mirrors PyFloat_CheckExact fast path at the top of PyNumber_Float
# (abstract.c:1641-1643).
if obj.is_python_constant() and isinstance(obj.as_python_constant(), float):
return ConstantVariable.create(float(obj.as_python_constant()))
obj_type = maybe_get_python_type(obj)
if type_implements_nb_float(obj_type):
return obj.nb_float_impl(tx)
# https://github.com/python/cpython/blob/v3.13.0/Objects/abstract.c#L1674-L1685
if type_implements_nb_index(obj_type):
return obj.nb_index_impl(tx)
# PyFloat_FromString fallback — handles str and bytes.
# https://github.com/python/cpython/blob/v3.13.0/Objects/abstract.c#L1691
if obj.is_python_constant() and isinstance(obj.as_python_constant(), (str, bytes)):
try:
return ConstantVariable.create(float(obj.as_python_constant()))
except ValueError as e:
raise_observed_exception(ValueError, tx, args=[str(e)])
raise_type_error(
tx,
f"float() argument must be a string or a real number, "
f"not '{obj.python_type_name()}'",
)
@@ -0,0 +1,425 @@
"""
This module implements variable tracking for PyTorch optimizers during Dynamo tracing.
The OptimizerVariable class provides specialized handling for optimizer instances by:
- Optimizing the tracing of expensive optimizer initialization
- Managing optimizer state and parameter group tracking
- Handling tensor sources and guards for optimizer state tensors
- Supporting CUDA graph execution through static tensor address management
- Providing special handling for parameter gradients and optimizer state tensors
Key features include:
- Efficient initialization tracing via _init_group optimization
- Automatic marking of optimizer state tensors as static for CUDA graphs
- Proper source tracking for parameter groups, gradients, and state tensors
- Guard installation for optimizer state structure
- Support for both CPU and GPU tensor handling
- Cleanup of static tensor references via finalizers
The module integrates with Dynamo's broader tracing system while providing
optimizer-specific optimizations and safety guarantees.
"""
import logging
import weakref
from collections.abc import Iterable
from typing import Any, TYPE_CHECKING
import torch
from torch._dynamo.variables.tensor import TensorVariable
from torch._guards import Source
from torch._logging import getArtifactLogger
from torch.utils._pytree import tree_map_only
from ..guards import GuardBuilder, install_guard
from ..source import (
AttrSource,
ConstDictKeySource,
DictGetItemSource,
GetItemSource,
GlobalWeakRefSource,
GradSource,
)
from ..utils import GLOBAL_KEY_PREFIX
from .base import VariableTracker
from .constant import ConstantVariable
from .dicts import ConstDictVariable
from .hashable import HashableTracker
from .lists import ListVariable
from .misc import GetAttrVariable
from .user_defined import UserDefinedObjectVariable
if TYPE_CHECKING:
from torch._dynamo.symbolic_convert import InstructionTranslator
class ArgMappingException(Exception):
pass
class GuardInstallException(Exception):
pass
perf_hint_log = getArtifactLogger(__name__, "perf_hints")
def _is_static_for_cudagraphs(x: torch.Tensor) -> bool:
from torch._inductor.cudagraph_trees import get_manager
if x.is_cuda:
manager = get_manager(x.device.index, False)
is_static_address = torch._dynamo.utils.get_static_address_type(x) is not None
if manager:
assert manager.current_node is not None
return (
is_static_address
or manager.current_node._is_cuda_graph_recorded_tensor(x)
)
else:
return is_static_address
else:
# Don't print a warning for non-cuda tensors
return True
class OptimizerVariable(UserDefinedObjectVariable):
_nonvar_fields = {
"grad_to_source",
"tensor_to_source",
"static_tensor_names",
*UserDefinedObjectVariable._nonvar_fields,
}
def __init__(
self,
value: torch.optim.Optimizer,
grad_to_source: dict[Any, GradSource] | None = None,
static_tensor_names: set[str] | None = None,
tensor_to_source: dict[torch.Tensor, Source] | None = None,
**kwargs: Any,
) -> None:
super().__init__(value, **kwargs)
# pyrefly: ignore [bad-override]
self.value: torch.optim.Optimizer = value
self.grad_to_source = grad_to_source or {}
self.tensor_to_source = tensor_to_source or {}
self.static_tensor_names = static_tensor_names or set()
def call_method(
self,
tx: "InstructionTranslator",
name: str,
args: list[VariableTracker],
kwargs: dict[str, VariableTracker],
) -> "VariableTracker":
"""This is an optimization to avoid tracing the very slow initialization of the optimizer"""
if name == "_init_group":
if not hasattr(self.value, "_init_group"):
# Fallback: if the optimizer does not have _init_group, trace normally
return super().call_method(tx, name, args, kwargs)
try:
self.graph_break_if_pending_mutation(tx)
self.move_step_if_cpu()
py_args, py_kwargs = self.get_python_args(*args, **kwargs)
ret_val = self.value._init_group(*py_args, **py_kwargs)
self.map_sources_and_install_guards(tx)
self.update_list_args(tx, args, kwargs, py_args, py_kwargs)
# stash a weak_ptr to optimizer to invalidate code
# if the optimizer object dies
mangled_name = f"__optimizer_{id(self.value)}"
tx.store_global_weakref_by_id(mangled_name, self.value)
self.create_finalizer(tx)
# This is currently safe only because the only actual `ret_val`s returned
# by the `_init_group` of existing optimizers are properties that are invariant
# to the input tensors (e.g. dtype, layout). Changing these would trigger a
# recompilation and hence never result in the wrong specialization of `ret_val`.
return ConstantVariable.create(ret_val)
except (ArgMappingException, GuardInstallException) as _:
# trace normally if we can't map args or install guards correctly
pass
return super().call_method(tx, name, args, kwargs)
def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker:
# Note: this allows us to intercept the call in call_method
# in the typical case, we return a UserMethodVariable
# which will directly inline
if name in ("_init_group"):
assert self.source
return GetAttrVariable(
self,
name,
py_type=type(getattr(self.value, name)),
source=AttrSource(self.source, name),
)
if name == "param_groups":
from ..decorators import mark_static_address
for group in self.value.param_groups:
for p in group["params"]:
mark_static_address(p, guard=True)
self._set_capturable(tx)
return super().var_getattr(tx, name)
def graph_break_if_pending_mutation(self, tx: "InstructionTranslator") -> None:
# If there are pending mutations on a parameter (due to using closure)
# then we need to graph break to allow the python version of the parameter
# to update, so that running _init_group will initialize the states with
# the correct values
for g in self.value.param_groups:
for p in g["params"]:
side_effects = tx.output.side_effects
variable = side_effects.id_to_variable.get(id(p), None)
if variable and side_effects.has_pending_mutation(variable):
from ..exc import unimplemented
unimplemented(
gb_type="optimizer: pending mutation on parameter",
context=f"variable: {variable}, parameter: {p}",
explanation="Pending mutations on a parameter (e.g. due to using closure) require a graph break.",
hints=[],
)
def _set_capturable(self, tx: "InstructionTranslator") -> None:
from . import LazyVariableTracker
# We only set capturable if params are on cuda
# and the state is not initialized
def safe_to_set_capturable(group: dict[str, Any]) -> bool:
all_uninitialized = True
all_gpu = True
for p in group.get("params", []):
all_gpu &= p.is_cuda or p.is_xpu
all_uninitialized &= p not in self.value.state
return "capturable" in group and all_uninitialized and all_gpu
# track indices to not set so we don't need to
# in the variable tracker realize the whole state
# we handle guarding the state specially
for group in self.value.param_groups:
if safe_to_set_capturable(group):
group["capturable"] = True
source = self.source and AttrSource(self.source, "param_groups")
param_groups_vt = LazyVariableTracker.realize_all(
VariableTracker.build(tx, self.value.param_groups, source)
)
for param_group_vt in param_groups_vt.items:
key = HashableTracker(ConstantVariable.create("capturable"))
param_group_vt.items[key] = ConstantVariable.create(True)
def get_python_args(
self, *args: Any, **kwargs: Any
) -> tuple[list[Any], dict[str, Any]]:
"""Get python values equivalent to the variable tracker args"""
def map_arg(arg: Any) -> Any:
if isinstance(arg, VariableTracker) and arg.is_python_constant():
return arg.as_python_constant()
elif isinstance(arg, ListVariable) and not arg.items:
# pyrefly: ignore [implicit-any]
return []
elif (
isinstance(arg, ConstDictVariable)
and isinstance(arg.source, GetItemSource)
and isinstance(arg.source.base, AttrSource)
and arg.source.base.member == "param_groups"
):
return self.value.param_groups[arg.source.index]
raise ArgMappingException
new_args = [map_arg(arg) for arg in args]
new_kwargs = {k: map_arg(v) for k, v in kwargs.items()}
return new_args, new_kwargs
# If users load an old state dictionary,
# it's possible that step could be on the cpu
# if this is the case, move it to the GPU
# corresponding to the parameter
# in most cases this is a no-op because the state is empty
def move_step_if_cpu(self) -> None:
for p, state in self.value.state.items():
if "step" in state and state["step"].is_cpu:
state["step"] = state["step"].to(p.device)
def map_sources_and_install_guards(self, tx: "InstructionTranslator") -> None:
from ..decorators import mark_static_address
from .lazy import LazyVariableTracker
self.grad_to_source = {}
self.tensor_to_source = {}
def mark_static(x: Any) -> None:
mark_static_address(x, guard=True)
tree_map_only(torch.Tensor, mark_static, self.value.state)
# Recursively realize the variable trackers for optim.state and
# optim.param_groups, which recursively install the necessary guards.
params_groups_source = self.source and AttrSource(self.source, "param_groups")
param_groups_vt = LazyVariableTracker.realize_all(
VariableTracker.build(tx, self.value.param_groups, params_groups_source)
)
state_source = self.source and AttrSource(self.source, "state")
state_vt = VariableTracker.build(tx, self.value.state, state_source)
# We need to realize the top level state dict to populate
# the guard locals
state_vt.realize()
assert state_source is not None
tx.output.guard_on_key_order.add(state_source)
# Populate self.grad_to_source and self.tensor_to_source so that we can
# manually update_list_args
for group, group_vt in zip(self.value.param_groups, param_groups_vt.items):
# we assume here that all params within a param group
# are initialized similarly
if len(group["params"]) > 0:
for param in group["params"]:
if param.grad is not None:
key_index = None
for i, k in enumerate(self.value.state.keys()):
if k is param:
key_index = i
break
if key_index:
LazyVariableTracker.realize_all(
VariableTracker.build(
tx,
self.value.state[param],
DictGetItemSource(
state_source,
ConstDictKeySource(state_source, key_index),
),
)
)
break
params_vt = group_vt.getitem_const(tx, ConstantVariable.create("params"))
all_static = True
non_static_grads = []
for p, p_vt in zip(group["params"], params_vt.unpack_var_sequence(tx)):
param_source = p_vt.source
self.tensor_to_source[p] = param_source
grad_source = GradSource(
param_source,
"grad",
)
if p.grad is not None:
self.grad_to_source[p.grad] = grad_source
if not _is_static_for_cudagraphs(p.grad):
all_static = False
non_static_grads.append(grad_source)
else:
install_guard(grad_source.make_guard(GuardBuilder.CONSTANT_MATCH))
# Note: to avoid spam logs only warn if perf hint artifact is enabled
# (NB: artifacts are only enabled at the debug or warning level)
if not all_static and perf_hint_log.isEnabledFor(logging.DEBUG):
non_static_grad_names = [src.name for src in non_static_grads]
perf_hint_log.warning(
(
"Grad tensors %s will be copied during cudagraphs execution."
"If using cudagraphs and the grad tensor addresses will be the same across runs,"
" use torch._dynamo.decorators.mark_static_address to elide this copy.",
),
non_static_grad_names,
)
# We have to again iterate over the state dict to collect the
# tensor_to_source dict. This is used for the finalizer.
for idx, value in enumerate(self.value.state.values()):
p_state_source = DictGetItemSource(
state_source, ConstDictKeySource(state_source, idx)
)
tx.output.guard_on_key_order.add(p_state_source)
for inner_idx, v in enumerate(value.values()):
if (
isinstance(v, torch.Tensor)
and v not in self.grad_to_source
and v not in self.tensor_to_source
):
self.tensor_to_source[v] = DictGetItemSource(
p_state_source, ConstDictKeySource(p_state_source, inner_idx)
)
def wrap_tensor(
self, tx: "InstructionTranslator", tensor_value: torch.Tensor
) -> TensorVariable:
"""Wrap state tensor in a TensorVariable"""
from ..decorators import mark_static_address
# If we have a source for a tensor already use it,
# if we have not seen a tensor before, stash and use a
# global weak ref source, since it must be an optimizer tensor
# that we have missed
if tensor_value in self.tensor_to_source:
# mark these tensors as static for cudagraphs
mark_static_address(tensor_value, guard=True)
source = self.tensor_to_source[tensor_value]
self.static_tensor_names.add(tx.output.module_key_name(source.name))
elif tensor_value in self.grad_to_source:
source = self.grad_to_source[tensor_value]
else:
# mark these tensors as static for cudagraphs
mark_static_address(tensor_value, guard=True)
global_name = tx.store_global_weakref_by_id(GLOBAL_KEY_PREFIX, tensor_value)
source = GlobalWeakRefSource(global_name)
self.static_tensor_names.add(tx.output.module_key_name(source.name))
return VariableTracker.build(tx, tensor_value, source)
def update_list_args(
self,
tx: "InstructionTranslator",
args: Iterable[VariableTracker],
kwargs: Any,
py_args: Iterable[Any],
py_kwargs: Any,
) -> None:
"""Update the args and kwargs to the traced optimizer call"""
for arg, py_arg in zip(args, py_args):
if isinstance(arg, ListVariable):
assert isinstance(py_arg, list), (
"py_arg should be a list in optimizer variable"
)
for i, val in enumerate(py_arg):
tx.output.side_effects.mutation(arg)
if isinstance(val, torch.Tensor):
arg.items.append(self.wrap_tensor(tx, val))
else:
source = arg.source and GetItemSource(arg.source, i)
arg.items.append(VariableTracker.build(tx, val, source))
def create_finalizer(self, tx: "InstructionTranslator") -> None:
names_to_delete = self.static_tensor_names
value = self.value
tc = tx.output.tracing_context
def init_finalizer(gm: torch.fx.GraphModule) -> None:
def clear_static_tensor_refs() -> None:
for name in names_to_delete:
gm._buffers.pop(name, None)
gm._parameters.pop(name, None)
if tc.params_flat:
tc.params_flat.clear()
if tc.params_flat_unwrap_subclasses:
tc.params_flat_unwrap_subclasses.clear()
weakref.finalize(value, clear_static_tensor_refs)
tx.output.add_graph_finalizer(init_finalizer)
@@ -0,0 +1,532 @@
"""
This module implements variable tracking for TorchScript objects during Dynamo tracing.
The TorchScriptObjectVariable class provides specialized handling for TorchScript
objects with strong safety guarantees by:
- Enforcing method-call-only access to prevent unsafe attribute manipulation
- Converting graph breaks into hard errors via _raise_hard_error_if_graph_break
- Proper proxy and source tracking for TorchScript method calls
- Integration with higher-order operators for method call handling
Key safety features:
- Strict validation that only method calls are allowed (no direct attribute access)
- Immediate error reporting for potentially unsafe operations
- Proper source tracking for debugging and guard installation
- Safe handling of TorchScript object method calls through torchbind
The module ensures that TorchScript objects are handled safely during tracing
by limiting operations to known-safe patterns and failing fast for unsafe usage.
"""
import enum
import functools
import inspect
import types
from collections.abc import Callable, Iterable, Sequence
from typing import Any, TYPE_CHECKING, TypeVar
from typing_extensions import ParamSpec
import torch
import torch.utils._pytree as pytree
from torch._guards import Source
from torch._library.fake_class_registry import FakeScriptObject
from torch._library.opaque_object import (
get_member_type,
is_opaque_reference_type,
is_opaque_type,
is_opaque_value_type,
MemberType,
should_hoist,
)
from torch.fx.proxy import Proxy
from .. import graph_break_hints
from ..eval_frame import skip_code
from ..exc import (
raise_observed_exception,
unimplemented,
UnsafeScriptObjectError,
Unsupported,
)
from ..source import AttrSource
from ..utils import proxy_args_kwargs
from .base import VariableTracker
from .constant import ConstantVariable
from .dicts import ConstDictVariable
from .lists import TupleVariable
from .misc import LambdaVariable
from .user_defined import UserDefinedObjectVariable, UserDefinedVariable
if TYPE_CHECKING:
from torch._dynamo.symbolic_convert import InstructionTranslator
_P = ParamSpec("_P")
_T = TypeVar("_T")
def _raise_hard_error_if_graph_break(
reason: str,
) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]:
def deco(fn: Callable[_P, _T]) -> Callable[_P, _T]:
@functools.wraps(fn)
def graph_break_as_hard_error(*args: _P.args, **kwargs: _P.kwargs) -> _T:
try:
return fn(*args, **kwargs)
except Unsupported as e:
raise UnsafeScriptObjectError(e.msg) from e
return graph_break_as_hard_error
return deco
class OpaqueObjectClassVariable(UserDefinedVariable):
"""
A variable that represents an opaque object class (not instance).
Since UserDefinedClassVariable has some special handling for side effects,
we have a separate class here which will directly return the object when
__init__ is called.
"""
def __init__(self, value: Any, **kwargs: Any) -> None:
assert not (isinstance(value, type) and issubclass(value, enum.Enum)), (
f"Enum class {value} should use UserDefinedClassVariable, "
"not OpaqueObjectClassVariable"
)
super().__init__(**kwargs)
self.value = value
def as_python_constant(self) -> Any:
return self.value
def is_python_constant(self) -> bool:
# prevents constant folding of attribute accesses on
# opaque classes. this ensures var_getattr is called,
# allowing for proper validation and error handling
return False
def is_python_hashable(self) -> bool:
return is_opaque_value_type(self.value) # pyrefly: ignore[bad-argument-type]
def get_python_hash(self) -> int:
return hash(self.value)
def as_proxy(self) -> Any:
return self.value
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.value})"
def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker:
obj = None
try:
obj = inspect.getattr_static(self.value, name)
except AttributeError:
unimplemented(
gb_type="Attribute not found on opaque class",
context=f"class={self.value}, attr={name}",
explanation=f"The attribute '{name}' does not exist on opaque class {self.value}.",
hints=[
f"Ensure '{name}' is a valid attribute of {type(self.value)}.",
],
)
if isinstance(obj, staticmethod):
obj = obj.__get__(self.value)
elif isinstance(obj, property):
obj = obj.__get__(None, self.value) # pyrefly: ignore[no-matching-overload]
elif hasattr(obj, "__get__"):
if not isinstance(type(obj).__dict__.get("__get__"), types.FunctionType):
# C-level descriptors are safe to resolve dynamically.
obj = getattr(self.value, name)
else:
type_name = type(obj).__name__
unimplemented(
gb_type="Unsupported descriptor on opaque class",
context=f"class={self.value}, attr={name}, descriptor={type_name}",
explanation=f"The attribute '{name}' is a descriptor of type '{type_name}' which is not supported.",
hints=[
"Only staticmethod, property, and pybind11_static_property are supported.",
"Consider accessing this attribute outside of the compiled region.",
],
)
if ConstantVariable.is_literal(obj):
return VariableTracker.build(tx, obj)
source = AttrSource(self.source, name) if self.source else None
return VariableTracker.build(tx, obj, source)
def call_function(
self,
tx: "InstructionTranslator",
args: Sequence[VariableTracker],
kwargs: dict[str, VariableTracker],
) -> VariableTracker:
# disallow creating reference-type opaque objects in the middle of the
# program
if is_opaque_reference_type(self.value):
# Skip __init__ to prevent dynamo from tracing it during resume
skip_code(self.value.__init__.__code__)
unimplemented(
gb_type="An opaque object was created in the middle of the program.",
context=f"Opaque object type: {self.value}.",
explanation=(
"Opaque objects cannot be created inside the torch.compile region. "
"They must be created before entering the compiled function."
),
hints=[
"Please create the opaque object before calling torch.compile "
"and pass it in as an argument or as a global variable."
],
)
var_args = TupleVariable(list(args))
var_kwargs = ConstDictVariable(
{VariableTracker.build(tx, k): v for k, v in kwargs.items()}
)
if should_hoist(self.value):
with tx.output.tracing_context.guards_context.skip_guard_install():
constant_args = var_args.as_python_constant()
constant_kwargs = var_kwargs.as_python_constant()
else:
constant_args = var_args.as_python_constant()
constant_kwargs = var_kwargs.as_python_constant()
opaque_obj = self.value( # pyrefly: ignore[not-callable]
*constant_args, **constant_kwargs
)
# Capture sources from the VT args so subgraph reuse can apply
# source replacement to resolve new ctor arg values on stamp-out.
ctor_arg_sources = tuple(getattr(a, "source", None) for a in args)
if is_opaque_value_type(type(opaque_obj)):
fake_script_obj = opaque_obj
else:
fake_script_obj = torch._library.fake_class_registry.maybe_to_fake_obj(
tx.output.fake_mode, opaque_obj
)
return TorchScriptObjectVariable.create(
opaque_obj,
fake_script_obj,
(constant_args, constant_kwargs),
ctor_arg_sources=ctor_arg_sources,
)
class TorchScriptObjectVariable(UserDefinedObjectVariable):
_fake_script_object_cache: dict[int, "TorchScriptObjectVariable"] = {}
@classmethod
def is_matching_cls(cls, user_cls: type) -> bool:
return (
issubclass(user_cls, torch.ScriptObject)
or is_opaque_type(user_cls)
or issubclass(user_cls, FakeScriptObject)
)
@staticmethod
def create(
proxy: Proxy,
value: Any,
ctor_args_kwargs: Any = None,
ctor_arg_sources: tuple[Source | None, ...] | None = None,
**options: Any,
) -> "TorchScriptObjectVariable":
assert not isinstance(value, enum.Enum), (
f"Enum {type(value)} should use UserDefinedObjectVariable, not TorchScriptObjectVariable"
)
out = TorchScriptObjectVariable(
proxy, value, ctor_args_kwargs, ctor_arg_sources=ctor_arg_sources, **options
)
if isinstance(proxy, torch.fx.Proxy) and proxy.node.op != "placeholder":
from torch._dynamo.symbolic_convert import InstructionTranslator
tx = InstructionTranslator.current_tx()
tx.output.current_tracer.record_proxyable_vt(out)
return out
def __init__(
self,
proxy: Proxy,
value: Any,
ctor_args_kwargs: Any = None,
source: Source | None = None,
ctor_arg_sources: tuple[Source | None, ...] | None = None,
**kwargs: Any,
) -> None:
super().__init__(value, **kwargs)
self.proxy = proxy
if isinstance(self.proxy, torch.fx.Proxy):
self.proxy.node.meta["example_value"] = value
self.source = source
# If the OpaqueObject is sourceless, then this is
# the constant (args, kwargs) that Dynamo used to construct it.
self.ctor_args_kwargs = ctor_args_kwargs
# Sources of the constructor args, used by subgraph reuse to
# resolve new values via source replacement on stamp-out.
self.ctor_arg_sources = ctor_arg_sources
def as_proxy(self) -> Proxy:
if not isinstance(self.proxy, torch.fx.Proxy):
# If we have a hoisted value type, then lazily lift it to be a graph
# input when as_proxy() is called.
assert is_opaque_value_type(type(self.proxy))
if should_hoist(type(self.proxy)):
from torch._dynamo.symbolic_convert import InstructionTranslator
tx = InstructionTranslator.current_tx()
# if any kwargs (synthetic_graph_input doesn't support them yet)
# not a graph break because hard error more explicit here
# (and opaque objects are really just used for compile)
if self.ctor_args_kwargs[1]:
raise RuntimeError(
"NYI: hoisted opaque objects that accept kwargs, please pass as args"
)
hoisted_vt = tx.output.synthetic_graph_input(
type(self.proxy),
self.ctor_args_kwargs[0],
ctor_arg_sources=self.ctor_arg_sources,
)
self.proxy = hoisted_vt.as_proxy()
return self.proxy
def __str__(self) -> str:
value = (
self.value.real_obj
if isinstance(self.value, FakeScriptObject)
else self.value
)
return f"{self.__class__.__name__}({value})"
__repr__ = __str__
@_raise_hard_error_if_graph_break(
"Dynamo cannot safely trace script object due to graph break."
)
def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker:
from torch._higher_order_ops.torchbind import call_torchbind
from .higher_order_ops import TorchHigherOrderOperatorVariable
real_obj = self.as_python_constant()
real_obj_type = type(real_obj)
if is_opaque_type(real_obj_type):
member_type = get_member_type(real_obj_type, name)
if member_type == MemberType.USE_REAL:
value = getattr(real_obj, name)
if inspect.ismethod(value) or isinstance(
value, types.MethodWrapperType
):
return LambdaVariable(
lambda *args, **kwargs: self.call_method(tx, name, args, kwargs)
)
else:
return super().var_getattr(tx, name)
elif member_type == MemberType.INLINED:
value = getattr(real_obj, name)
if (
inspect.ismethod(value)
or isinstance(value, types.MethodWrapperType)
) and self.source is None:
# When we don't have a source, fall back to call_method
# which creates a proxy node.
return LambdaVariable(
lambda *args, **kwargs: self.call_method(tx, name, args, kwargs)
)
return super().var_getattr(tx, name)
elif is_opaque_value_type(real_obj_type):
return super().var_getattr(tx, name)
elif name in ("__bool__", "__len__") and not hasattr(real_obj, name):
# Special case: __bool__ and __len__ are used for truthiness checks.
# If they're not registered and the real object doesn't have them,
# raise ObservedAttributeError so the caller can fall back to
# treating the object as truthy (Python default behavior
raise_observed_exception(AttributeError, tx)
else:
unimplemented(
gb_type="Attempted to access unregistered member on an OpaqueObject",
context=f"value={real_obj}, attr={name}",
explanation=f"Member '{name}' is not registered for this opaque object type.",
hints=[
f"Register '{name}' with a MemberType in register_opaque_type(members=...).",
],
)
method = getattr(self.value, name, None)
if method is None:
unimplemented(
gb_type="FakeScriptObject missing method implementation",
context=f"value={self.value}, method={name}",
explanation=f"TorchScript object {self.value} doesn't define the method {name}.",
hints=[
f"Ensure the method {name} is implemented in {self.value}.",
*graph_break_hints.USER_ERROR,
],
)
if not callable(method):
unimplemented(
gb_type="Attempted to access non-callable attribute of TorchScript object",
context=f"value={self.value}, method={name}",
explanation="Attribute accesses of TorchScript objects to non-callable attributes are not supported.",
hints=[
"Use method calls instead of attribute access.",
],
)
assert self.source is not None
return TorchHigherOrderOperatorVariable.make(
call_torchbind,
source=AttrSource(self.source, name),
script_obj_var=self,
method_name=name,
)
def mp_subscript_impl(
self,
tx: "InstructionTranslator",
key: "VariableTracker",
) -> "VariableTracker":
# Call call_method directly on this class to avoid the __getitem__ →
# mp_subscript_impl loop in VariableTracker.call_method.
return TorchScriptObjectVariable.call_method(self, tx, "__getitem__", [key], {})
# We only support method calls on script objects. Interpreting the bytecodes
# should go through var_getattr then call_function instead of call_method.
# However, it's possible for call_method to be used directly e.g. for __setattr__.
@_raise_hard_error_if_graph_break(
"Dynamo cannot safely trace script object due to graph break."
)
def call_method(
self,
tx: "InstructionTranslator",
name: str,
args: Iterable[Any],
kwargs: dict[str, Any],
) -> VariableTracker:
from .builder import wrap_fx_proxy
real_obj = self.as_python_constant()
real_obj_type = type(real_obj)
if is_opaque_type(real_obj_type):
member_type = get_member_type(real_obj_type, name)
if member_type == MemberType.USE_REAL:
if (
inspect.getattr_static(real_obj_type, "__getattr__", None)
is not None
):
unimplemented(
gb_type="Opaque object with custom __getattr__ not supported",
context=f"{real_obj_type.__name__} with custom __getattr__",
explanation="Dynamo does not support opaque objects types with custom __getattr__ methods",
hints=[],
)
args_const = [x.as_python_constant() for x in args]
kwargs_const = {k: v.as_python_constant() for k, v in kwargs.items()}
method = getattr(real_obj, name)
if name == "__setattr__":
method(*args_const, **kwargs_const)
return real_obj # pyrefly: ignore[bad-return]
constant_val = method(*args_const, **kwargs_const)
if any(
is_opaque_reference_type(type(r))
for r in pytree.tree_leaves(constant_val)
):
unimplemented(
gb_type="Opaque object member with method-type USE_REAL returned a reference-type opaque object.",
context=f"Opaque object type: {real_obj_type}. Method name: '{name}'",
explanation=(
"To properly guard reference-type opaque objects, "
"we must lift them as inputs to the graph. In order "
"to do this, they must all have a source, meaning they "
"come from a global value or are an attribute of an input."
),
hints=[
f"Register member '{name}' with MemberType.INLINED in "
f"register_opaque_type({real_obj_type}, members=...).",
],
)
return VariableTracker.build(tx, constant_val)
elif member_type == MemberType.INLINED or is_opaque_value_type(
real_obj_type
):
proxy_args, proxy_kwargs = proxy_args_kwargs(args, kwargs)
proxy = tx.output.create_proxy(
"call_method",
name,
args=(self.proxy, *proxy_args),
kwargs=proxy_kwargs,
)
return wrap_fx_proxy(tx=tx, proxy=proxy)
else:
unimplemented(
gb_type="Attempted to access unregistered member on an OpaqueObject",
context=f"value={real_obj}, attr={name}",
explanation=f"Member '{name}' is not registered for this opaque object type.",
hints=[
f"Register '{name}' with a MemberType in register_opaque_type(members=...).",
],
)
unimplemented(
gb_type="Weird method call on TorchScript object",
context=f"value={self.value}, method={name}",
explanation=(
f"This particular method call ({name}) is not supported (e.g. calling `__setattr__`). "
"Most method calls to TorchScript objects should be supported."
),
hints=[
"Avoid calling this method.",
],
)
def as_python_constant(self) -> Any:
if isinstance(self.value, FakeScriptObject):
return self.value.real_obj
elif is_opaque_value_type(type(self.value)):
return self.value
elif isinstance(self.value, torch.ScriptObject):
return self.value
return super().as_python_constant()
def is_python_hashable(self) -> bool:
try:
self.get_python_hash()
return True
except TypeError:
return False
def get_python_hash(self) -> int:
real_obj = self.as_python_constant()
return hash(real_obj)
def is_python_equal(self, other: object) -> bool:
assert isinstance(other, VariableTracker)
real_self = self.as_python_constant()
real_other = other.as_python_constant()
return real_self == real_other
def get_real_value(self) -> Any:
return self.as_python_constant()
@@ -0,0 +1,98 @@
from collections.abc import Sequence
from inspect import getattr_static
from typing import Any, TYPE_CHECKING, TypeGuard
from torch._guards import Source
from torch.backends.cuda import SDPAParams
from torch.fx.proxy import Proxy
from ..bytecode_transformation import create_call_function
from ..exc import unimplemented
from ..source import AttrSource
from .base import VariableTracker
if TYPE_CHECKING:
from torch._dynamo.codegen import PyCodegen
from torch._dynamo.symbolic_convert import InstructionTranslator
PARAM_NAMES = [
"query",
"key",
"value",
"attn_mask",
"dropout",
"is_causal",
"enable_gqa",
]
class SDPAParamsVariable(VariableTracker):
"""Represents the c++ params struct for scaled dot product attention.
This is a read-only container."""
@staticmethod
def create(
tx: "InstructionTranslator", value: Any, source: Source
) -> VariableTracker:
from .torch import TorchInGraphFunctionVariable
params = [
VariableTracker.build(tx, getattr(value, p), AttrSource(source, p))
for p in PARAM_NAMES
]
return TorchInGraphFunctionVariable(SDPAParams).call_function(tx, params, {})
def __init__(
self, proxy: Proxy, param_vars: Sequence[VariableTracker], **kwargs: Any
) -> None:
self.proxy = proxy
self.param_vars = param_vars
super().__init__(**kwargs)
def python_type(self) -> type:
return SDPAParams
def reconstruct(self, codegen: "PyCodegen") -> None:
assert self.source is None
assert self.param_vars is not None
codegen.add_push_null(
lambda: codegen.load_import_from("torch._C", "_SDPAParams")
)
codegen.foreach(self.param_vars)
codegen.extend_output(create_call_function(len(self.param_vars), False))
def as_proxy(self) -> Proxy:
return self.proxy
def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker:
import torch._C
from .builder import wrap_fx_proxy
from .misc import GetAttrVariable
try:
getattr_static(torch._C._SDPAParams, name)
except AttributeError:
import torch._dynamo.graph_break_hints as graph_break_hints
unimplemented(
gb_type="unsupported torch._C._SDPAParams attribute",
context=f"name: {name}",
explanation=f"Unable to fetch attribute {name} from torch._C._SDPAParams.",
hints=[
*graph_break_hints.USER_ERROR,
],
)
proxy = GetAttrVariable.create_getattr_proxy(self.as_proxy(), name)
if self.source is not None:
return wrap_fx_proxy(
tx=tx, proxy=proxy, source=AttrSource(self.source, name)
)
else:
return wrap_fx_proxy(tx=tx, proxy=proxy)
@staticmethod
def is_sdpa_params(value: Any) -> TypeGuard["SDPAParams"]:
return value is SDPAParams
@@ -0,0 +1,797 @@
"""
Set-related variable tracking classes for PyTorch Dynamo.
This module implements variable tracking for different types of set-like objects:
- Regular Python sets (set)
- Frozen sets (frozenset)
- Ordered sets (torch.utils._ordered_set.OrderedSet)
- Dictionary key sets (dict_keys views used as sets)
These classes are responsible for tracking set operations during graph compilation,
maintaining proper guards for set mutations and element existence checks.
The implementation uses a special HashableTracker wrapper to handle set elements
while preserving proper aliasing semantics. Sets are modeled internally as
dictionaries with None values.
"""
import functools
import operator
from collections.abc import Iterable, Sequence
from typing import Any, Literal, TYPE_CHECKING
from torch.utils._ordered_set import OrderedSet
from .. import polyfills, variables
from ..bytecode_transformation import create_call_function, create_instruction
from ..exc import raise_observed_exception
from ..guards import GuardBuilder, install_guard
from ..source import AttrSource, is_constant_source, is_from_local_source
from ..utils import cmp_name_to_op_mapping, istype, raise_args_mismatch
from .base import ValueMutationNew, VariableTracker
from .constant import ConstantVariable
from .hashable import HashableTracker, is_hashable, raise_unhashable
if TYPE_CHECKING:
from torch._dynamo.codegen import PyCodegen
from torch._dynamo.symbolic_convert import InstructionTranslator
from torch._dynamo.variables.builtin import BuiltinVariable
# [Adding a new supported class within the keys of SetVariable]
# see steps outlined for ConstDictVariable
class SetVariable(VariableTracker):
"""Represents a Python set during symbolic execution."""
# PySet_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/setobject.c#L2436
_cpython_type = set
CONTAINS_GUARD = GuardBuilder.SET_CONTAINS
NOT_CONTAINS_GUARD = GuardBuilder.SET_NOT_CONTAINS
def __init__(
self,
items: Iterable[VariableTracker | HashableTracker],
**kwargs: Any,
) -> None:
# .clone() passes these arguments in kwargs but they're recreated below
if "original_items" in kwargs:
kwargs.pop("original_items")
if "should_reconstruct_all" in kwargs:
kwargs.pop("should_reconstruct_all")
super().__init__(**kwargs)
# Items can be either VariableTrackers or HashableTrackers (from set ops).
# For VariableTrackers, realize them to ensure aliasing guards are installed
# when the same object appears multiple times.
hashable_items = []
for item in items:
if isinstance(item, HashableTracker):
# Already a HashableTracker from a set operation
hashable_items.append(item)
else:
# VariableTracker - realize to install guards, then wrap
# pyrefly: ignore [bad-argument-type]
hashable_items.append(HashableTracker(item.realize()))
self.items = dict.fromkeys(hashable_items, SetVariable._default_value())
self.should_reconstruct_all = (
not is_from_local_source(self.source) if self.source else True
)
self.original_items = dict.fromkeys(
hashable_items, SetVariable._default_value()
)
def debug_repr(self) -> str:
if not self.items:
return "set()"
else:
items: list[str] = []
for v in self.items:
vt = v.vt if isinstance(v, HashableTracker) else v
val_str = repr(vt.value) if hasattr(vt, "value") else vt.debug_repr()
items.append(val_str)
return "{" + ",".join(items) + "}"
@property
def set_items(self) -> set["HashableTracker"]:
return set(self.items.keys())
@staticmethod
def _default_value() -> VariableTracker:
# Variable to fill in the keys of the dictionary
return ConstantVariable.create(None)
def as_proxy(self) -> Any:
return {k.vt.as_proxy() for k in self.set_items}
def python_type(self) -> type:
return set
def as_python_constant(self) -> Any:
return {k.vt.as_python_constant() for k in self.set_items}
def reconstruct(self, codegen: "PyCodegen") -> None:
codegen.foreach([x.vt for x in self.set_items])
codegen.append_output(create_instruction("BUILD_SET", arg=len(self.set_items)))
def __contains__(self, vt: VariableTracker) -> bool:
assert isinstance(vt, VariableTracker)
if not is_hashable(vt):
return False
key = HashableTracker(vt)
return key in self.items and not isinstance(
self.items[key], variables.DeletedVariable
)
def len(self) -> int:
return sum(
not isinstance(x, variables.DeletedVariable) for x in self.items.values()
)
def has_new_items(self) -> bool:
return self.should_reconstruct_all or any(
self.is_new_item(self.original_items.get(key.vt), value)
for key, value in self.items.items()
)
def is_new_item(
self, value: VariableTracker | None, other: VariableTracker
) -> bool:
if value and value.is_realized() and other.is_realized():
return id(value.realize()) != id(other.realize())
return id(value) != id(other)
def unpack_var_sequence(self, tx: "InstructionTranslator") -> list[VariableTracker]:
return [x.vt for x in self.items]
def clone(self, **kwargs: Any) -> VariableTracker:
return super().clone(**kwargs)
def is_python_hashable(self) -> bool:
return False
def var_getattr(self, tx: "InstructionTranslator", name: str):
if name == "__class__":
return VariableTracker.build(tx, self.python_type())
return super().var_getattr(tx, name)
def call_obj_hasattr(
self, tx: "InstructionTranslator", name: str
) -> ConstantVariable:
return VariableTracker.build(tx, hasattr(set, name))
def install_set_contains_guard(
self, tx: "InstructionTranslator", args: list[VariableTracker]
) -> None:
if not self.source:
return
if tx.output.side_effects.is_modified(self):
return
contains = args[0] in self
if args[0].source is None and args[0].is_python_constant():
guard_fn = (
type(self).CONTAINS_GUARD if contains else type(self).NOT_CONTAINS_GUARD
)
install_guard(
self.make_guard(
functools.partial(
guard_fn,
key=args[0].as_python_constant(),
)
)
)
def _fast_set_method(
self,
tx: "InstructionTranslator",
fn: Any,
args: list[VariableTracker],
kwargs: dict[str, VariableTracker],
) -> VariableTracker:
try:
res = fn(
*[x.as_python_constant() for x in [self, *args]],
**{k: v.as_python_constant() for k, v in kwargs.items()},
)
except Exception as exc:
raise_observed_exception(type(exc), tx, args=list(exc.args))
return VariableTracker.build(tx, res)
def call_method(
self,
tx: "InstructionTranslator",
name: str,
args: list[VariableTracker],
kwargs: dict[str, VariableTracker],
) -> VariableTracker:
from ..utils import check_constant_args
from .builder import SourcelessBuilder
if (
name
in (
"isdisjoint",
"union",
"intersection",
"difference",
"symmetric_difference",
)
and check_constant_args(args, kwargs)
and self.python_type() is set
):
py_type = self.python_type()
return self._fast_set_method(tx, getattr(py_type, name), args, kwargs)
# Lazy imports to avoid circular dependencies
from .dicts import DictItemsVariable, DictKeysVariable
if name == "__init__":
temp_set_vt = SourcelessBuilder.create(tx, set).call_set(
tx, *args, **kwargs
)
tx.output.side_effects.mutation(self)
self.items.clear()
self.items.update(temp_set_vt.items) # type: ignore[attr-defined]
return ConstantVariable.create(None)
elif name == "add":
if kwargs or len(args) != 1:
raise_args_mismatch(
tx,
name,
"1 args and 0 kwargs",
f"{len(args)} args and {len(kwargs)} kwargs",
)
# Convert add to __setitem__ with None value
if not is_hashable(args[0]):
raise_unhashable(args[0], tx)
tx.output.side_effects.mutation(self)
self.items[HashableTracker(args[0])] = SetVariable._default_value()
return ConstantVariable.create(None)
elif name == "pop":
if kwargs or args:
raise_args_mismatch(
tx,
name,
"0 args and 0 kwargs",
f"{len(args)} args and {len(kwargs)} kwargs",
)
# Choose an item at random and pop it
try:
result: VariableTracker = self.set_items.pop().vt # type: ignore[assignment]
except KeyError as e:
raise_observed_exception(KeyError, tx, args=list(e.args))
self.should_reconstruct_all = True
tx.output.side_effects.mutation(self)
self.items.pop(HashableTracker(result))
return result
elif name == "isdisjoint":
if kwargs or len(args) != 1:
raise_args_mismatch(
tx,
name,
"1 args and 0 kwargs",
f"{len(args)} args and {len(kwargs)} kwargs",
)
return SourcelessBuilder.create(tx, polyfills.set_isdisjoint).call_function(
tx, [self, args[0]], {}
)
elif name == "intersection":
if kwargs:
raise_args_mismatch(tx, name, "0 kwargs", f"{len(kwargs)} kwargs")
return SourcelessBuilder.create(
tx, polyfills.set_intersection
).call_function(
tx,
[self, *args],
{"cls": self.python_type_var()},
)
elif name == "intersection_update":
if kwargs:
raise_args_mismatch(tx, name, "0 kwargs", f"{len(kwargs)} kwargs")
return SourcelessBuilder.create(
tx, polyfills.set_intersection_update
).call_function(tx, [self, *args], {})
elif name == "union":
if kwargs:
raise_args_mismatch(tx, name, "0 kwargs", f"{len(kwargs)} kwargs")
return SourcelessBuilder.create(tx, polyfills.set_union).call_function(
tx,
[self, *args],
{"cls": self.python_type_var()},
)
elif name == "difference":
if kwargs:
raise_args_mismatch(
tx, name, f"Expect: 0 kwargs, Actual: {len(kwargs)} kwargs"
)
return SourcelessBuilder.create(tx, polyfills.set_difference).call_function(
tx,
[self, *args],
{"cls": self.python_type_var()},
)
elif name == "difference_update":
if kwargs:
raise_args_mismatch(tx, name, "0 kwargs", f"{len(kwargs)} kwargs")
return SourcelessBuilder.create(
tx, polyfills.set_difference_update
).call_function(tx, [self, *args], {})
elif name == "symmetric_difference":
if kwargs or len(args) != 1:
raise_args_mismatch(
tx,
name,
"1 args and 0 kwargs",
f"{len(args)} args and {len(kwargs)} kwargs",
)
return SourcelessBuilder.create(
tx, polyfills.set_symmetric_difference
).call_function(
tx,
[self, *args],
{"cls": self.python_type_var()},
)
elif name == "symmetric_difference_update":
if kwargs or len(args) != 1:
raise_args_mismatch(
tx,
name,
"1 args and 0 kwargs",
f"{len(args)} args and {len(kwargs)} kwargs",
)
return SourcelessBuilder.create(
tx, polyfills.set_symmetric_difference_update
).call_function(tx, [self, *args], {})
elif name == "update" and self.is_mutable():
if kwargs:
raise_args_mismatch(tx, name, "0 kwargs", f"{len(kwargs)} kwargs")
return SourcelessBuilder.create(tx, polyfills.set_update).call_function(
tx, [self, *args], {}
)
elif name == "remove":
if kwargs or len(args) != 1:
raise_args_mismatch(
tx,
name,
"1 args and 0 kwargs",
f"{len(args)} args and {len(kwargs)} kwargs",
)
if args[0] not in self:
raise_observed_exception(KeyError, tx, args=args)
self.should_reconstruct_all = True
tx.output.side_effects.mutation(self)
self.items.pop(HashableTracker(args[0]))
return ConstantVariable.create(None)
elif name == "discard":
if kwargs or len(args) != 1:
raise_args_mismatch(
tx,
name,
"1 args and 0 kwargs",
f"{len(args)} args and {len(kwargs)} kwargs",
)
if args[0] in self:
self.should_reconstruct_all = True
tx.output.side_effects.mutation(self)
self.items.pop(HashableTracker(args[0]))
return ConstantVariable.create(None)
elif name in ("issubset", "issuperset"):
if len(args) != 1:
raise_args_mismatch(tx, name, "1 args", f"{len(args)} args")
op = {
"issubset": operator.le,
"issuperset": operator.ge,
}
other = args[0].realize()
if not istype(other, SetVariable):
other = SourcelessBuilder.create(tx, set).call_function(tx, [other], {})
return SourcelessBuilder.create(tx, op.get(name)).call_function(
tx, [self, other], {}
)
elif name in ("__and__", "__or__", "__xor__", "__sub__"):
m = {
"__and__": "intersection",
"__or__": "union",
"__xor__": "symmetric_difference",
"__sub__": "difference",
}.get(name)
if not isinstance(
args[0],
(
SetVariable,
variables.UserDefinedSetVariable,
DictItemsVariable,
DictKeysVariable,
),
):
raise_observed_exception(
TypeError,
tx,
args=[
f"unsupported operand type(s) for {name}: '{self.python_type_name()}' and '{args[0].python_type_name()}'"
],
)
assert m is not None
return self.call_method(tx, m, args, kwargs)
elif name in ("__rand__", "__ror__", "__rxor__", "__rsub__"):
m = {
"__rand__": "__and__",
"__ror__": "__or__",
"__rxor__": "__xor__",
"__rsub__": "__sub__",
}.get(name)
if not isinstance(
args[0],
(
SetVariable,
variables.UserDefinedSetVariable,
DictItemsVariable,
DictKeysVariable,
),
):
raise_observed_exception(
TypeError,
tx,
args=[
f"unsupported operand type(s) for {name}: '{args[0].python_type_name()}' and '{self.python_type_name()}'"
],
)
assert m is not None
return args[0].call_method(tx, m, [self], kwargs)
elif name in ("__iand__", "__ior__", "__ixor__", "__isub__"):
if not isinstance(
args[0],
(
SetVariable,
variables.UserDefinedSetVariable,
DictItemsVariable,
DictKeysVariable,
),
):
raise_observed_exception(
TypeError,
tx,
args=[
f"unsupported operand type(s) for {name}: '{self.python_type_name()}' and '{args[0].python_type_name()}'"
],
)
m = {
"__iand__": "intersection_update",
"__ior__": "update",
"__ixor__": "symmetric_difference_update",
"__isub__": "difference_update",
}.get(name)
assert m is not None
self.call_method(tx, m, args, kwargs)
return self
elif name == "__eq__":
if not isinstance(
args[0],
(
SetVariable,
variables.UserDefinedSetVariable,
DictItemsVariable,
DictKeysVariable,
),
):
return ConstantVariable.create(False)
r = self.call_method(tx, "symmetric_difference", args, kwargs)
return VariableTracker.build(tx, len(r.set_items) == 0) # type: ignore[attr-defined]
elif name == "__ne__":
eq_result = self.call_method(tx, "__eq__", args, kwargs)
return VariableTracker.build(tx, not eq_result.value) # type: ignore[attr-defined]
elif name in cmp_name_to_op_mapping:
if not isinstance(
args[0],
(
SetVariable,
variables.UserDefinedSetVariable,
DictItemsVariable,
DictKeysVariable,
),
):
return VariableTracker.build(tx, NotImplemented)
return VariableTracker.build(
tx,
cmp_name_to_op_mapping[name](self.set_items, args[0].set_items), # type: ignore[attr-defined]
)
elif name == "__contains__":
if not len(args):
raise_args_mismatch(
tx,
name,
"more than 1 args and 0 kwargs",
f"{len(args)} args and {len(kwargs)} kwargs",
)
if not (args and is_hashable(args[0])):
raise_unhashable(args[0], tx)
self.install_set_contains_guard(tx, args)
contains = args[0] in self
return VariableTracker.build(tx, contains)
elif name == "__len__":
if args or kwargs:
raise_args_mismatch(
tx,
name,
"0 args and 0 kwargs",
f"{len(args)} args and {len(kwargs)} kwargs",
)
return VariableTracker.build(tx, len(self.items))
elif name == "copy":
if args or kwargs:
raise_args_mismatch(
tx,
name,
"0 args and 0 kwargs",
f"{len(args)} args and {len(kwargs)} kwargs",
)
return self.clone(
items=self.items.copy(), mutation_type=ValueMutationNew(), source=None
)
elif name == "clear":
if args or kwargs:
raise_args_mismatch(
tx,
name,
"0 args and 0 kwargs",
f"{len(args)} args and {len(kwargs)} kwargs",
)
self.should_reconstruct_all = True
tx.output.side_effects.mutation(self)
self.items.clear()
return ConstantVariable.create(None)
elif name == "__iter__":
from .lists import ListIteratorVariable
if self.source and not is_constant_source(self.source):
tx.output.guard_on_key_order.add(self.source)
return ListIteratorVariable(
self.unpack_var_sequence(tx), mutation_type=ValueMutationNew()
)
return super().call_method(tx, name, args, kwargs)
def python_type_var(self) -> "BuiltinVariable":
return variables.BuiltinVariable(set)
def getitem_const(
self, tx: "InstructionTranslator", arg: VariableTracker
) -> VariableTracker:
raise RuntimeError("Illegal to getitem on a set")
def sq_length(self, tx: "InstructionTranslator") -> VariableTracker:
return VariableTracker.build(tx, len(self.set_items))
class OrderedSetClassVariable(VariableTracker):
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
def as_python_constant(self) -> type[OrderedSet[Any]]:
return OrderedSet
def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker:
if name == "__new__":
from .misc import GetAttrVariable
if self.source:
attr_source = AttrSource(self.source, name)
else:
attr_source = None
return GetAttrVariable(
self, name, py_type=type(getattr(OrderedSet, name)), source=attr_source
)
else:
return super().var_getattr(tx, name)
def call_method(
self,
tx: "InstructionTranslator",
name: str,
args: list[VariableTracker],
kwargs: dict[str, VariableTracker],
) -> VariableTracker:
from .builtin import set_methods
if name == "__new__":
if len(args) != 2 or kwargs:
raise_args_mismatch(
tx,
name,
"OrderedSet.__new__ only accepts one arg"
f"{len(args)} args and {len(kwargs)} kwargs",
)
return variables.OrderedSetVariable([], mutation_type=ValueMutationNew())
resolved_fn = getattr(set, name)
if resolved_fn in set_methods and isinstance(args[0], variables.SetVariable):
return args[0].call_method(tx, name, args[1:], kwargs)
return super().call_method(tx, name, args, kwargs)
def call_function(
self,
tx: "InstructionTranslator",
args: Sequence[VariableTracker],
kwargs: dict[str, VariableTracker],
) -> "OrderedSetVariable":
if len(args) > 1 or kwargs:
raise_args_mismatch(
tx,
"OrderedSet",
"OrderedSet only accepts one arg"
f"{len(args)} args and {len(kwargs)} kwargs",
)
if len(args) == 0:
# pyrefly: ignore [implicit-any]
items = []
else:
items = args[0].force_unpack_var_sequence(tx)
return variables.OrderedSetVariable(items, mutation_type=ValueMutationNew())
class OrderedSetVariable(SetVariable):
def debug_repr(self) -> str:
if not self.items:
return "OrderedSet([])"
else:
items: list[str] = []
for k in self.items:
key_str = (
repr(k.vt.value) if hasattr(k.vt, "value") else k.vt.debug_repr()
)
items.append(key_str)
return "OrderedSet([" + ",".join(items) + "])"
def as_python_constant(self) -> OrderedSet[Any]:
return OrderedSet([k.vt.as_python_constant() for k in self.set_items])
def python_type(self) -> type[OrderedSet[Any]]:
return OrderedSet
# pyrefly: ignore[bad-override]
def python_type_var(self) -> OrderedSetClassVariable:
return OrderedSetClassVariable()
def reconstruct(self, codegen: "PyCodegen") -> None:
codegen.add_push_null(
lambda: codegen.load_import_from("torch.utils._ordered_set", "OrderedSet")
)
codegen.foreach([x.vt for x in self.set_items])
codegen.append_output(create_instruction("BUILD_LIST", arg=len(self.set_items)))
codegen.extend_output(create_call_function(1, False))
class FrozensetVariable(SetVariable):
# PyFrozenSet_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/setobject.c#L2526
_cpython_type = frozenset
def debug_repr(self) -> str:
if not self.items:
return "frozenset()"
else:
items: list[str] = []
for k in self.items:
key_str = (
repr(k.vt.value) if hasattr(k.vt, "value") else k.vt.debug_repr()
)
items.append(key_str)
return "{" + ",".join(items) + "}"
@property
def set_items(self) -> set["HashableTracker"]:
return set(self.items.keys())
def python_type(self) -> type:
return frozenset
def python_type_var(self) -> "BuiltinVariable":
return variables.BuiltinVariable(frozenset)
def as_python_constant(self) -> Any:
return frozenset({k.vt.as_python_constant() for k in self.set_items})
def reconstruct(self, codegen: "PyCodegen") -> None:
codegen.add_push_null(
lambda: codegen.extend_output(
[
codegen.create_load_global("frozenset"),
]
)
)
codegen.foreach([x.vt for x in self.set_items])
codegen.extend_output(
[
create_instruction("BUILD_LIST", arg=len(self.set_items)),
*create_call_function(1, False),
]
)
def call_method(
self,
tx: "InstructionTranslator",
name: str,
args: list[VariableTracker],
kwargs: dict[str, VariableTracker],
) -> VariableTracker:
if name in ["add", "pop", "update", "remove", "discard", "clear"]:
raise RuntimeError(f"Illegal call_method {name} on a frozenset")
elif name == "__init__":
# frozenset is immutable. Calling __init__ again shouldn't have any effect
return ConstantVariable.create(None)
elif name in (
"copy",
"difference",
"intersection",
"symmetric_difference",
):
r = super().call_method(tx, name, args, kwargs)
return FrozensetVariable(r.items) # type: ignore[attr-defined]
return super().call_method(tx, name, args, kwargs)
def is_python_hashable(self) -> Literal[True]:
"""
Frozensets are immutable and hashable in Python.
"""
return True
def get_python_hash(self) -> int:
return hash(self.as_python_constant())
def is_python_equal(self, other: object) -> bool:
return (
isinstance(other, VariableTracker)
and self.as_python_constant() == other.as_python_constant()
)
class DictKeySetVariable(SetVariable):
def debug_repr(self) -> str:
if not self.items:
return "dict_keys([])"
else:
items: list[str] = []
for k in self.items:
key_str = (
repr(k.vt.value) if hasattr(k.vt, "value") else k.vt.debug_repr()
)
items.append(key_str)
return "dict_keys([" + ",".join(items) + "])"
def install_set_contains_guard(
self, tx: "InstructionTranslator", args: list[VariableTracker]
) -> None:
# Already EQUALS_MATCH guarded
pass
@property
def set_items(self) -> Any:
return self.items
def python_type(self) -> type:
from ..utils import dict_keys
return dict_keys
def as_python_constant(self) -> Any:
return dict.fromkeys(
{k.vt.as_python_constant() for k in self.set_items}, None
).keys()
def call_method(
self,
tx: "InstructionTranslator",
name: str,
args: list[VariableTracker],
kwargs: dict[str, VariableTracker],
) -> VariableTracker:
if name in ["add", "pop", "update", "remove", "discard", "clear"]:
raise RuntimeError(f"Illegal call_method {name} on a dict_keys")
return super().call_method(tx, name, args, kwargs)
@@ -0,0 +1,724 @@
import collections
from collections.abc import Callable
from typing import Any, Optional
import torch
from torch._dynamo.variables.dicts import ConstDictVariable
from torch._dynamo.variables.lists import TupleVariable
from torch.fx import has_side_effect, Proxy
from .. import graph_break_hints
from ..bytecode_transformation import create_call_function
from ..exc import TYPE_CHECKING, unimplemented
from ..graph_bytecode_inputs import (
CURRENT_STREAM_INDEX,
get_external_object_by_index,
register_graph_created_object,
register_user_object,
reset_user_object_tracking,
)
from ..source import CurrentStreamSource
from .base import VariableTracker
from .constant import ConstantVariable
from .ctx_manager import FxTracebackAnnotateVariable
from .lazy import LazyVariableTracker
if TYPE_CHECKING:
from torch._dynamo.symbolic_convert import InstructionTranslator
from ..codegen import PyCodegen
from torch._library.custom_ops import custom_op
Tensor = torch.Tensor
def new_event(*args: Any, **kwargs: Any) -> int:
event = torch.Event(*args, **kwargs)
return register_graph_created_object(
event,
EventVariable.make_construct_in_graph_event_fn(
TupleVariable([]), ConstDictVariable({})
),
)
def new_stream(*args: tuple[Any], **kwargs: Any) -> int:
stream = torch.Stream(*args, **kwargs) # type: ignore[no-matching-overload,call-overload]
return register_graph_created_object(
stream,
StreamVariable.make_construct_in_graph_stream_fn(
TupleVariable([]), ConstDictVariable({})
),
)
def _codegen_current_stream(device: torch.device, cg: "PyCodegen") -> None:
cg.add_push_null(
lambda: cg.load_import_from(
torch._dynamo.graph_bytecode_inputs.__name__, # type: ignore[implicit-imports]
"stash_graph_created_object",
)
)
cg(CurrentStreamSource(device))
cg.extend_output(create_call_function(1, False))
def get_current_stream(device: torch.device) -> int:
stream = torch.accelerator.current_stream(device)
return register_graph_created_object(
stream, lambda _, cg: _codegen_current_stream(device, cg)
)
def _get_stream_by_index(index: int) -> torch.Stream:
stream = get_external_object_by_index(index)
assert isinstance(stream, torch.Stream), (
f"Fork/join stream expected a stream object at index {index}"
)
return stream
def _get_event_by_index(index: int) -> torch.Event:
event = get_external_object_by_index(index)
assert isinstance(event, torch.Event), (
f"Record/wait event expected an event object at index {index}"
)
return event
@custom_op("streams::fork", mutates_args=())
def fork_stream(
from_index: int, # kept to make stream transitions clearer
to_index: int,
) -> None:
torch.accelerator.set_stream(_get_stream_by_index(to_index))
@fork_stream.register_fake
def _(
from_index: int, # kept to make stream transitions clearer
to_index: int,
) -> None:
pass
has_side_effect(torch.ops.streams.fork.default)
@custom_op("streams::join", mutates_args=())
def join_stream(from_index: int, to_index: int) -> None:
torch.accelerator.set_stream(_get_stream_by_index(to_index))
@join_stream.register_fake
def _(
from_index: int,
to_index: int,
) -> None:
pass
has_side_effect(torch.ops.streams.join.default)
@custom_op("streams::record_event", mutates_args=())
def record_event(event_index: int, stream_index: int) -> None:
event = _get_event_by_index(event_index)
stream = _get_stream_by_index(stream_index)
event.record(stream)
@record_event.register_fake
def _(
event_index: int,
stream_index: int,
) -> None:
pass
has_side_effect(torch.ops.streams.record_event.default)
@custom_op("streams::wait_event", mutates_args=())
def wait_event(event_index: int, stream_index: int) -> None:
event = _get_event_by_index(event_index)
stream = _get_stream_by_index(stream_index)
event.wait(stream)
@wait_event.register_fake
def _(
event_index: int,
stream_index: int,
) -> None:
pass
has_side_effect(torch.ops.streams.wait_event.default)
@custom_op("streams::synchronize_event", mutates_args=())
def synchronize_event(event_index: int) -> None:
event = _get_event_by_index(event_index)
event.synchronize()
@synchronize_event.register_fake
def _(event_index: int) -> None:
pass
has_side_effect(torch.ops.streams.synchronize_event.default)
@custom_op("streams::synchronize_device", mutates_args=())
def synchronize_device(device_type: str, device_index: int) -> None:
torch.accelerator.synchronize(torch.device(device_type, device_index))
@synchronize_device.register_fake
def _(device_type: str, device_index: int) -> None:
pass
has_side_effect(torch.ops.streams.synchronize_device.default)
@custom_op("streams::synchronize_stream", mutates_args=())
def synchronize_stream(stream_index: int) -> None:
stream = _get_stream_by_index(stream_index)
stream.synchronize()
@synchronize_stream.register_fake
def _(stream_index: int) -> None:
pass
has_side_effect(torch.ops.streams.synchronize_stream.default)
@custom_op("streams::wait_stream", mutates_args=())
def wait_stream(waiting_stream_index: int, waited_on_stream_index: int) -> None:
waiting = _get_stream_by_index(waiting_stream_index)
waited_on = _get_stream_by_index(waited_on_stream_index)
waiting.wait_stream(waited_on)
@wait_stream.register_fake
def _(
event_index: int,
stream_index: int,
) -> None:
pass
has_side_effect(torch.ops.streams.wait_stream.default)
@custom_op("streams::sync_dealloc", mutates_args=())
def sync_dealloc(
wait_event_index: int, src_stream_index: int, to_dealloc: torch.Tensor
) -> None:
"""An op which waits on an event and moves the last usage of to_dealloc
after the wait, so that after the sync occurs, the deallocation or
subsequent reuse of the tensor's memory will be guaranteed to happen
after a side stream is finished using it.
See https://docs.pytorch.org/docs/stable/generated/torch.Tensor.record_stream.html#torch.Tensor.record_stream
for more details"""
torch.ops.streams.wait_event.default(wait_event_index, src_stream_index)
@sync_dealloc.register_fake
def _(
wait_event_index: int,
src_stream_index: int,
to_dealloc: torch.Tensor,
) -> None:
pass
has_side_effect(torch.ops.streams.sync_dealloc.default)
@custom_op("streams::record_stream", mutates_args=())
def record_stream(tensor: torch.Tensor, stream_index: int) -> None:
tensor.record_stream(_get_stream_by_index(stream_index))
@record_stream.register_fake
def _(
tensor: torch.Tensor,
stream_index: int,
) -> None:
pass
has_side_effect(torch.ops.streams.record_stream.default)
class SymbolicStreamState:
"""Track the currently entered stream if any"""
def __init__(self) -> None:
from ..source import CurrentStreamSource
cur_stack: list[StreamVariable] = []
if torch.accelerator.is_available():
# Reset the registry so the current stream is guaranteed index 0.
reset_user_object_tracking()
stream = torch.accelerator.current_stream()
source = CurrentStreamSource(stream.device)
# Register the current stream so it gets index 0 (registry is
# fresh at tracing start). The inductor wrapper updates this
# entry at runtime so cudagraph capture uses the capture stream
# instead of this stale trace-time stream.
index = register_user_object(stream, source)
assert index == CURRENT_STREAM_INDEX, (
f"Current stream must be registered at index {CURRENT_STREAM_INDEX}, "
f"got {index}"
)
stream_var = LazyVariableTracker.create(stream, source=source)
# Set user_object_index as an instance attribute so accessing it
# does NOT trigger LazyVariableTracker realization.
stream_var.user_object_index = index # type: ignore[union-attr]
cur_stack = [stream_var] # type: ignore[list-item]
self.cur_stream_stack: collections.deque[StreamVariable] = collections.deque(
cur_stack
)
def enter_stream(self, stream: "StreamVariable") -> None:
self.cur_stream_stack.append(stream)
def exit_stream(self) -> None:
self.cur_stream_stack.pop()
def cur_stream(self, device: torch.device | None = None) -> "StreamVariable":
if device is not None:
for stream in reversed(self.cur_stream_stack):
if stream.device == device:
return stream
return self.cur_stream_stack[-1]
def in_stream_context(self) -> bool:
return len(self.cur_stream_stack) > 0
def cur_stream_id(self) -> int:
"""Get a Python object id for the current stream without realizing lazy variables."""
stream = self.cur_stream_stack[-1]
if isinstance(stream, LazyVariableTracker) and not stream.is_realized():
return id(stream.peek_value())
return id(stream.value)
class StreamContextVariable(FxTracebackAnnotateVariable):
"""This represents torch.cuda.StreamContext"""
@staticmethod
def create(
tx: "InstructionTranslator",
stream_to_enter: "StreamVariable",
**kwargs: dict[str, Any],
) -> "StreamContextVariable":
return StreamContextVariable(
stream_to_enter,
**kwargs,
)
def __init__(self, stream: Optional["StreamVariable"], **kwargs: Any) -> None:
self.stream = stream
super().__init__(
target_values={"stream": self.get_stream().user_object_index},
initial_values=None,
**kwargs,
)
def enter(
self, tx: "InstructionTranslator", *args: VariableTracker
) -> VariableTracker:
# to stream, from stream is the order of the arguments
# we are entering the target, and leaving the initial stream
tx.symbolic_stream_state.enter_stream(self.get_stream())
return super().enter(tx)
def exit(
self, tx: "InstructionTranslator", *args: VariableTracker
) -> VariableTracker:
# to stream, from stream is the order of the arguments
# we are leaving the target, and entering the initial stream
tx.symbolic_stream_state.exit_stream()
return super().exit(tx, *args)
def python_type(self) -> type:
return torch.cuda.StreamContext
def supports_graph_breaks(self) -> bool:
return True
def get_stream(self) -> "StreamVariable":
assert self.stream, "Stream context should have a separate stream"
return self.stream
class StreamVariable(StreamContextVariable):
"""Represents the device-agnostic torch.Stream class"""
_cpython_type = torch.Stream
def __init__(
self,
proxy: Proxy,
value: torch.Stream,
user_object_index: int | None = None,
**kwargs: Any,
) -> None:
# Index into the user object table
# used to pass arbitrary objects to the graph
if proxy is not None and "example_value" in proxy.node.meta:
assert proxy.node.meta["example_value"] == value
self.proxy = proxy
self.value = value
self.device = value.device
self.user_object_index = user_object_index
super().__init__(None, **kwargs)
def python_type(self) -> type:
return torch.Stream
def get_real_python_backed_value(self) -> object:
return self.value
def call_method(
self,
tx: "InstructionTranslator",
name: str,
args: list[VariableTracker],
kwargs: dict[str, VariableTracker],
) -> VariableTracker:
assert hasattr(self.value, name), f"no stream method found named {name}"
from ..utils import cmp_name_to_op_mapping, proxy_args_kwargs
from .builder import wrap_fx_proxy_cls
if name == "wait_event":
event_arg = args[0]
assert isinstance(event_arg, EventVariable)
tx.output.create_proxy(
"call_function",
torch.ops.streams.wait_event,
(event_arg.user_object_index, self.user_object_index),
{},
)
return ConstantVariable.create(None)
elif name == "wait_stream":
other_stream = args[0]
assert isinstance(other_stream, StreamVariable)
tx.output.create_proxy(
"call_function",
torch.ops.streams.wait_stream,
(self.user_object_index, other_stream.user_object_index),
{},
)
return ConstantVariable.create(None)
elif name == "synchronize":
tx.output.create_proxy(
"call_function",
torch.ops.streams.synchronize_stream,
(self.user_object_index,),
{},
)
return ConstantVariable.create(None)
elif name == "query":
return wrap_fx_proxy_cls(
target_cls=ConstantVariable,
tx=tx,
proxy=tx.output.create_proxy(
"call_method", name, *proxy_args_kwargs([self] + args, kwargs)
),
)
elif name == "record_event":
from .builder import wrap_fx_proxy
tx.output.check_event_record_after_input_mutation(id(self.value))
if args and isinstance(args[0], EventVariable):
event_var = args[0]
event = event_var.value
event_index = event_var.user_object_index
else:
event = self.value.record_event()
event_index = register_graph_created_object(
event,
EventVariable.make_construct_in_graph_event_fn(
TupleVariable([]), ConstDictVariable({})
),
)
tx.output.create_proxy(
"call_function",
torch.ops.streams.record_event,
(event_index, self.user_object_index),
{},
)
return wrap_fx_proxy(
tx=tx,
proxy=tx.output.create_proxy(
"call_function",
get_external_object_by_index,
(event_index,),
{},
),
)
elif name in cmp_name_to_op_mapping and len(args) == 1 and not kwargs:
from ..guards import GuardBuilder, install_guard
if self.source:
install_guard(self.source.make_guard(GuardBuilder.EQUALS_MATCH))
# NB : Checking for mutation is necessary because we compare
# constant values
other = args[0]
if not isinstance(other, StreamVariable):
return VariableTracker.build(tx, NotImplemented)
if other.source:
assert self.source is not None
install_guard(self.source.make_guard(GuardBuilder.EQUALS_MATCH))
return VariableTracker.build(
tx,
cmp_name_to_op_mapping[name](self.value, other.value), # type: ignore[arg-type]
)
return super().call_method(tx, name, args, kwargs)
def as_proxy(self) -> Proxy:
return self.proxy
def module_name(self) -> str:
return "torch._C"
def fn_name(self) -> str:
return "Stream"
def reconstruct(self, codegen: "PyCodegen") -> None:
# If we got here, this stream is fully subsumed by the graph - this means it is
# not an input or global
assert not self.source
if self.user_object_index is not None:
codegen.add_push_null(
lambda: codegen.load_import_from(
torch._dynamo.graph_bytecode_inputs.__name__,
"get_external_object_by_index",
)
)
codegen.append_output(codegen.create_load_const(self.user_object_index))
codegen.extend_output(create_call_function(1, False))
else:
# This will support the legacy behavior
prefix = f"_stream_{self.device}"
name = codegen.tx.output.install_global_by_id(prefix, self.value)
codegen.append_output(codegen.create_load_global(name, add=True))
def get_stream(self) -> "StreamVariable":
return self
@staticmethod
def make_construct_in_graph_stream_fn(
args: TupleVariable, kwargs: ConstDictVariable
) -> Callable[[int, "PyCodegen"], None]:
def fn(index: int, codegen: "PyCodegen") -> None:
codegen.add_push_null(
lambda: codegen.load_import_from(
torch._dynamo.graph_bytecode_inputs.__name__, # type: ignore[implicit-imports]
"stash_graph_created_object",
)
)
codegen.add_push_null(
lambda: codegen.load_import_from(
torch._dynamo.utils.__name__, "build_stream"
)
)
codegen(args)
codegen(kwargs)
codegen.extend_output(create_call_function(2, False))
codegen.extend_output(create_call_function(1, False))
return fn
class CudaStreamVariable(StreamVariable):
"""Represents torch.cuda.Stream, preserving device-specific type and attributes."""
_cpython_type = torch.cuda.Stream
def python_type(self) -> type:
return torch.cuda.Stream
def var_getattr(self, tx: "InstructionTranslator", name: str) -> "VariableTracker":
from . import ConstantVariable
if name == "cuda_stream":
from ..guards import GuardBuilder, install_guard
if self.source:
install_guard(self.source.make_guard(GuardBuilder.EQUALS_MATCH))
if hasattr(self.value, "cuda_stream"):
return ConstantVariable.create(self.value.cuda_stream)
if hasattr(self.value, "native_handle"):
return ConstantVariable.create(self.value.native_handle)
return super().var_getattr(tx, name)
class EventVariable(VariableTracker):
def __init__(
self,
proxy: Proxy,
value: torch.Event,
user_object_index: int | None,
**kwargs: Any,
) -> None:
if proxy is not None and "example_value" in proxy.node.meta:
assert proxy.node.meta["example_value"] == value
super().__init__(**kwargs)
self.proxy = proxy
self.value = value
self.user_object_index = user_object_index
def python_type(self) -> type:
return torch.Event
def get_real_python_backed_value(self) -> object:
return self.value
def call_method(
self,
tx: "InstructionTranslator",
name: str,
args: list[VariableTracker],
kwargs: dict[str, VariableTracker],
) -> VariableTracker:
from ..utils import proxy_args_kwargs
from .builder import wrap_fx_proxy_cls
if name == "wait":
_, stream_index = EventVariable._get_stream_arg(tx, args, kwargs)
tx.output.create_proxy(
"call_function",
torch.ops.streams.wait_event,
(
self.user_object_index,
stream_index,
),
{},
)
return ConstantVariable.create(None)
elif name == "record":
stream_arg, stream_index = EventVariable._get_stream_arg(tx, args, kwargs)
tx.output.check_event_record_after_input_mutation(id(stream_arg.value))
tx.output.create_proxy(
"call_function",
torch.ops.streams.record_event,
(
self.user_object_index,
stream_index,
),
{},
)
return ConstantVariable.create(None)
elif name == "synchronize":
tx.output.create_proxy(
"call_function",
torch.ops.streams.synchronize_event,
(self.user_object_index,),
{},
)
return ConstantVariable.create(None)
elif name == "query":
return wrap_fx_proxy_cls(
target_cls=ConstantVariable,
tx=tx,
proxy=tx.output.create_proxy(
"call_method", name, *proxy_args_kwargs([self] + args, kwargs)
),
)
else:
method_name = (
f"{type(self.value).__module__}.{type(self.value).__qualname__}.{name}"
)
unimplemented(
gb_type="Unsupported event method",
context=str(name),
explanation=f"Dynamo doesn't support tracing the {method_name} method. "
f"We currently support wait, record, synchronize, and query.",
hints=[
*graph_break_hints.SUPPORTABLE,
],
)
def as_proxy(self) -> Proxy:
return self.proxy
@staticmethod
def _get_stream_arg(
tx: "InstructionTranslator",
args: list[VariableTracker],
kwargs: dict[str, VariableTracker],
) -> tuple["StreamVariable", int]:
"""Returns (stream_variable, stream_index_for_op).
The ambient current stream is registered at index 0 in the external
object registry. The inductor wrapper updates index 0 at runtime so
that cudagraph capture sees the capture stream, not the stale
trace-time default stream.
"""
stream_arg = None
if args:
stream_arg = args[0]
elif kwargs:
stream_arg = kwargs.get("stream")
if not stream_arg:
stream_var = tx.symbolic_stream_state.cur_stream()
return stream_var, stream_var.user_object_index # type: ignore[return-value]
return stream_arg, stream_arg.user_object_index # type: ignore[return-value]
@staticmethod
def make_construct_in_graph_event_fn(
args: TupleVariable, kwargs: ConstDictVariable
) -> Callable[[int, "PyCodegen"], None]:
def fn(index: int, codegen: "PyCodegen") -> None:
codegen.add_push_null(
lambda: codegen.load_import_from(
torch._dynamo.graph_bytecode_inputs.__name__, # type: ignore[implicit-imports]
"stash_graph_created_object",
)
)
codegen.add_push_null(
lambda: codegen.load_import_from(
torch._dynamo.utils.__name__, "build_event"
)
)
codegen(args)
codegen(kwargs)
codegen.extend_output(create_call_function(2, False))
codegen.extend_output(create_call_function(1, False))
return fn
def reconstruct(self, codegen: "PyCodegen") -> None:
# If we got here, this event is fully subsumed by the graph - this means it is
# not an input or global
assert not self.source
# Similar to stream handling, we lift the event into a global and then codegen bytecode to load it from there.
prefix = "_event"
name = codegen.tx.output.install_global_by_id(prefix, self.value)
codegen.append_output(codegen.create_load_global(name, add=True))
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,781 @@
"""TorchDynamo support for __torch_function__ tensor subclasses.
This module implements support for tensor subclasses with __torch_function__ overrides.
A tensor subclass instance is represented as a TensorWithTFOverrideVariable, which handles
dispatching __torch_function__ on attribute accesses, method calls, and torch API calls.
Unsupported features:
- Triggering __torch_function__ on tensor subclass non-tensor custom attributes
- Graph breaking on mutating guardable tensor properties within a __torch_function__ context
(can cause excessive recompiles in certain cases)
- Matching exact eager behavior of ignoring __torch_function__ objects in non-tensor
argument positions of Torch API calls
Supported features:
- Static method implementations of __torch_function__ on custom objects (triggers on torch
API calls with the object as any argument)
- Triggering __torch_function__ on torch API calls with tensor subclass arguments
- __torch_function__ calls on base tensor attribute access and method calls for tensor
subclass instances
- Matches dispatch ordering behavior of eager __torch_function__ with subclass/object
arguments in any position
See https://docs.google.com/document/d/1WBxBSvW3NXhRp9ncmtokJloMLCtF4AYNhJaffvHe8Kw/edit#heading=h.vacn73lozd9w
for more information on the design.
"""
import collections
import contextlib
import functools
import inspect
import operator
from collections.abc import Generator, Iterable, Sequence
from types import TracebackType
from typing import Any, TYPE_CHECKING
import torch._C
import torch.utils._pytree as pytree
from torch._guards import Source
from torch.overrides import (
_get_overloaded_args,
get_default_nowrap_functions,
TorchFunctionMode,
)
from torch.utils._device import DeviceContext
from .. import graph_break_hints
from ..exc import unimplemented
from ..guards import GuardBuilder, install_guard
from ..polyfills import NoEnterTorchFunctionMode
from ..source import AttrSource, GlobalSource, TorchFunctionModeStackSource, TypeSource
from ..utils import (
class_has_getattribute,
clear_torch_function_mode_stack,
get_safe_global_name,
has_torch_function,
is_tensor_base_attr_getter,
set_torch_function_mode_stack,
)
from .base import VariableTracker
from .constant import ConstantVariable
from .ctx_manager import GenericContextWrappingVariable
from .functions import UserMethodVariable
from .lazy import LazyVariableTracker
from .lists import TupleVariable
from .tensor import TensorSubclassVariable, TensorVariable
from .user_defined import UserDefinedObjectVariable
if TYPE_CHECKING:
from torch._dynamo.codegen import PyCodegen
from torch._dynamo.symbolic_convert import InstructionTranslator
bin_ops = [
operator.pow,
operator.mul,
operator.matmul,
operator.floordiv,
operator.truediv,
operator.mod,
operator.add,
operator.lt,
operator.gt,
operator.ge,
operator.le,
operator.ne,
operator.eq,
operator.sub,
operator.ipow,
operator.imul,
operator.imatmul,
operator.ifloordiv,
operator.itruediv,
operator.imod,
operator.iadd,
operator.isub,
]
bin_int_ops = [
operator.and_,
operator.or_,
operator.xor,
operator.iand,
operator.ixor,
operator.ior,
]
un_int_ops = [operator.invert]
tensor_and_int_ops = [
operator.lshift,
operator.rshift,
operator.ilshift,
operator.irshift,
operator.getitem,
]
un_ops = [
operator.abs,
operator.pos,
operator.neg,
operator.not_, # Note: this has a local scalar dense call
operator.length_hint,
]
banned_attrs = [
fn.__self__.__name__ # type: ignore[attr-defined]
for fn in get_default_nowrap_functions()
if is_tensor_base_attr_getter(fn)
]
@functools.cache
def get_prev_stack_var_name() -> str:
from ..bytecode_transformation import unique_id
return unique_id("___prev_torch_function_mode_stack")
class TorchFunctionModeVariable(GenericContextWrappingVariable):
@staticmethod
def is_supported_torch_function_mode(ty: type[TorchFunctionMode]) -> bool:
# Supported in this sense means we can support graph breaks under the
# context.
# We are able to trace custom modes but if there are graph breaks under them
# and they have a custom __enter__/__exit__ we don't handle this for the
# same reason we don't handle generic context managers: there may be side effects
# that are now affected by executing the function across two frames instead of one
# Today we support the enter/exit of the default TorchFunctionMode as well as
# DeviceContext (which is used for set_default_device)
return issubclass(ty, (NoEnterTorchFunctionMode, DeviceContext)) or (
not class_has_getattribute(ty)
and inspect.getattr_static(ty, "__enter__") is TorchFunctionMode.__enter__
and inspect.getattr_static(ty, "__exit__") is TorchFunctionMode.__exit__
)
def __init__(
self,
value: TorchFunctionMode | None,
source: Source | None = None,
**kwargs: Any,
) -> None:
if value is not None:
super().__init__(value, **kwargs)
self.value = value
# needed for BC with calling enter from CM code
self.cm_obj = value # type: ignore[assignment]
self.source = source # type: ignore[assignment]
def reconstruct(self, codegen: "PyCodegen") -> None:
# This shouldn't be called unless we have a source
assert self.source
self.source.reconstruct(codegen)
def module_name(self) -> str:
return self.value.__module__
def fn_name(self) -> str:
return type(self.value).__name__
def python_type(self) -> type:
return type(self.value)
def call_torch_function(
self,
tx: "InstructionTranslator",
fn: VariableTracker,
types: TupleVariable,
args: Iterable[Any],
kwargs: dict[str, Any],
) -> VariableTracker:
return call_torch_function(
tx,
get_torch_function_fn(tx, self), # type: ignore[arg-type]
fn,
types,
args,
kwargs,
)
def enter(self, tx: "InstructionTranslator") -> VariableTracker:
from .torch import TorchInGraphFunctionVariable
if isinstance(self.value, NoEnterTorchFunctionMode):
return ConstantVariable.create(None)
TorchInGraphFunctionVariable(
torch._C._push_on_torch_function_stack
).call_function(tx, [self], {})
return ConstantVariable.create(None)
def exit(self, tx: "InstructionTranslator", *args: Any) -> VariableTracker:
from .torch import TorchInGraphFunctionVariable
TorchInGraphFunctionVariable(torch._C._pop_torch_function_stack).call_function(
tx, [], {}
)
return ConstantVariable.create(None)
def reconstruct_type(self, codegen: "PyCodegen") -> None:
ty = NoEnterTorchFunctionMode
codegen(
AttrSource(
codegen.tx.import_source(ty.__module__),
ty.__name__,
)
)
def supports_graph_breaks(self) -> bool:
return True
def exit_on_graph_break(self) -> bool:
return False
# Used to clear/restore the python torch function mode stack and temporarily restore it as needed
class TorchFunctionModeStackStateManager:
def __init__(self) -> None:
self.stack: list[Any] = []
def __enter__(self) -> None:
self.stack = torch.overrides._get_current_function_mode_stack()
clear_torch_function_mode_stack()
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
set_torch_function_mode_stack(self.stack)
self.stack = []
@contextlib.contextmanager
def temp_restore_stack(self) -> Generator[None, None, None]:
prev = torch.overrides._get_current_function_mode_stack()
set_torch_function_mode_stack(self.stack)
try:
yield
finally:
set_torch_function_mode_stack(prev)
torch_function_mode_stack_state_mgr = TorchFunctionModeStackStateManager()
class SymbolicTorchFunctionState:
def __init__(self, py_stack: Iterable[Any]) -> None:
# This is annoyingly complicated because of how the torch function subclass + mode C API was designed
# There are two exposed C knobs here as contexts: torch._C.DisableTorchFunction and torch._C.DisableTorchFunctionSubclass
# These are their definitions:
# 1) torch._C._is_torch_function_enabled indicates that neither of the above knobs have been entered
# (if either are entered, this will be False)
# 2) torch._C._is_torch_function_mode_enabled indicates that either the torch mode stack is empty OR
# torch._C.DisableTorchFunction has been entered
# To disambiguate these and keep myself sane I added a C API to check whether all torch function
# concepts (modes and subclasses) are enabled.
# This only returns true iff we have not entered torch._C.DisableTorchFunction and allows us to separate
# the stack length from the enablement state of torch function modes.
# This is important because now if a mode is pushed while dynamo is tracing, we know whether
# or not torch function modes are enabled and whether we should trace it.
self.torch_function_subclass_enabled = torch._C._is_torch_function_enabled()
# This differs from the C API of the same name
# this will only be false iff we have entered torch._C.DisableTorchFunction
# and does not take into account the mode stack length, while the C API bundles these
# two concepts
self.torch_function_mode_enabled = (
not torch._C._is_torch_function_all_disabled()
)
self.cur_mode = None
TorchFunctionModeStackVariable.reset()
self.mode_stack: collections.deque[TorchFunctionModeVariable] = (
collections.deque()
)
for i, val in enumerate(py_stack):
self.mode_stack.append(
LazyVariableTracker.create(val, source=TorchFunctionModeStackSource(i)) # type: ignore[arg-type]
)
def in_torch_function_mode(self) -> bool:
return len(self.mode_stack) > 0
def pop_torch_function_mode(self) -> TorchFunctionModeVariable:
return self.mode_stack.pop()
def push_torch_function_mode(self, mode_var: TorchFunctionModeVariable) -> None:
self.mode_stack.append(mode_var)
def call_torch_function_mode(
self,
tx: "InstructionTranslator",
fn: VariableTracker,
types: TupleVariable,
args: Iterable[Any],
kwargs: dict[str, Any],
) -> Any:
with self._pop_mode_for_inlining() as cur_mode:
return cur_mode.call_torch_function(tx, fn, types, args, kwargs)
@contextlib.contextmanager
def _pop_mode_for_inlining(
self,
) -> Generator[TorchFunctionModeVariable, None, None]:
old_mode = self.cur_mode
self.cur_mode = self.pop_torch_function_mode() # type: ignore[assignment]
try:
yield self.cur_mode # type: ignore[misc]
finally:
mode = self.cur_mode
self.cur_mode = old_mode
self.push_torch_function_mode(mode) # type: ignore[arg-type]
class TorchFunctionModeStackVariable(VariableTracker):
"""Fake VT to use as a dummy object, indicating the presence of torch function mode stack mutation"""
# singleton value representing the global torch function mode stack
# singleton (it exists in C++)
stack_value_singleton = object()
# offset is used to track if we have inserted/removed a
# device context which is always placed at the bottom of the stack
# if a device context is inserted, the graph will run this mutation
# so when we want to reconstruct any other modes on the stack
# their indices should be shifted right by 1 (+1)
# Conversely, if there was a device context on the stack, and the graph
# mutates the stack to remove that context (set default device to None)
# each of the indices of other modes should be shifted left by 1 (-1)
offset = 0
def __init__(
self,
source: Source,
symbolic_stack: collections.deque[TorchFunctionModeVariable],
) -> None:
self.source = source
self.symbolic_stack = symbolic_stack
@classmethod
def reset(cls) -> None:
cls.offset = 0
@classmethod
def register_mutation(cls, tx: "InstructionTranslator") -> None:
if cls.stack_value_singleton not in tx.output.side_effects:
var = cls(
source=Source(),
symbolic_stack=tx.symbolic_torch_function_state.mode_stack,
)
tx.output.side_effects.track_mutable(cls.stack_value_singleton, var)
tx.output.side_effects.mutation(var)
@classmethod
def register_device_context_insertion(cls, tx: "InstructionTranslator") -> None:
stack = tx.symbolic_torch_function_state.mode_stack
if stack and cls.is_device_context(stack[0]):
return
else:
cls.offset += 1
stack.insert(
0,
TorchFunctionModeVariable(
None, source=TorchFunctionModeStackSource(-cls.offset)
),
)
@classmethod
def clear_default_device(cls, tx: "InstructionTranslator") -> None:
stack = tx.symbolic_torch_function_state.mode_stack
if stack and cls.is_device_context(stack[0]):
stack.popleft()
cls.offset -= 1
@staticmethod
def is_device_context(var: TorchFunctionModeVariable) -> bool:
return isinstance(var.value, DeviceContext) or var.value is None
@classmethod
def get_mode_index(cls, ind: int) -> int:
return ind + cls.offset
def _get_all_args(
args: Iterable[Any], kwargs: dict[str, Any]
) -> Iterable[VariableTracker]:
return _flatten_vts(pytree.arg_tree_leaves(*args, **kwargs))
def _flatten_vts(vts: Iterable[VariableTracker]) -> list[VariableTracker]:
from collections import deque
from .dicts import ConstDictVariable
from .lists import ListVariable
vts = deque(vts)
output = []
while vts:
vt = vts.popleft()
if not vt.is_realized() and vt.peek_type() in (dict, list, tuple): # type: ignore[attr-defined]
vt.realize()
if vt.is_realized():
if isinstance(vt, ListVariable):
vts.extend(vt.items)
continue
elif isinstance(vt, ConstDictVariable):
vts.extend(vt.items.values())
continue
output.append(vt)
return output
def _get_subclass_type(var: VariableTracker) -> type:
assert isinstance(var, (TensorWithTFOverrideVariable, UserDefinedObjectVariable))
return var.python_type()
def _get_subclass_type_var(
tx: "InstructionTranslator", var: VariableTracker
) -> VariableTracker:
if isinstance(var, TensorWithTFOverrideVariable):
return var.class_type_var(tx)
elif isinstance(var, UserDefinedObjectVariable):
source = var.source and TypeSource(var.source)
return VariableTracker.build(tx, var.python_type(), source)
else:
raise AssertionError(f"Unexpected type {type(var)}")
def _is_attr_overridden(
tx: "InstructionTranslator", var: VariableTracker, name: str
) -> bool:
if not isinstance(var, (TensorWithTFOverrideVariable, UserDefinedObjectVariable)):
return False
import torch
overridden = False
try:
attr_val = inspect.getattr_static(var.python_type(), name)
overridden |= attr_val != getattr(torch.Tensor, name)
except AttributeError:
pass
return overridden
def call_torch_function(
tx: "InstructionTranslator",
torch_function_var: VariableTracker,
fn: VariableTracker,
types: TupleVariable,
args: Iterable[Any],
kwargs: dict[str, Any],
*,
is_subclass_dispatch: bool = False,
) -> Any:
# This emulates calling __torch_function__, which has a signature
# def __torch_function__(cls, func, types, args=(), kwargs=None):
#
# Also notice the `cls` is not explicitly passed in the reference
# implementations:
# 1. https://github.com/pytorch/pytorch/blob/8d81806211bc3c0ee6c2ef235017bacf1d775a85/torch/csrc/utils/python_arg_parser.cpp#L368-L374 # noqa: B950
# 2. https://github.com/pytorch/pytorch/blob/8d81806211bc3c0ee6c2ef235017bacf1d775a85/torch/overrides.py#L1741-L1743
tf_args = [
fn,
types,
VariableTracker.build(tx, tuple(args)),
VariableTracker.build(tx, kwargs),
]
# Mirror the C++ THPModule_disable_torch_function behavior: disable
# __torch_function__ subclass dispatch during the call to prevent
# re-entrant dispatch on operations inside __torch_function__.
# Only do this for subclass dispatch, not mode dispatch. Modes need
# subclass dispatch to remain enabled because the mode's
# __torch_function__ may re-dispatch to the subclass.
tf_state = tx.symbolic_torch_function_state
old_subclass_enabled = tf_state.torch_function_subclass_enabled
if is_subclass_dispatch and old_subclass_enabled:
tf_state.torch_function_subclass_enabled = False
try:
return torch_function_var.call_function(tx, tf_args, {})
finally:
tf_state.torch_function_subclass_enabled = old_subclass_enabled
def get_torch_function_fn(
tx: "InstructionTranslator", vt: VariableTracker
) -> VariableTracker:
# The underlying function could be a classmethod, staticmethod, regular
# function or a function with C-implementation. It doesn't matter as long as
# they satisfy the calling convention in `call_torch_function`.
args = [vt, VariableTracker.build(tx, "__torch_function__")]
func_vt = VariableTracker.build(tx, getattr).call_function(tx, args, {})
return func_vt
def can_dispatch_torch_function(
tx: "InstructionTranslator", args: Iterable[Any], kwargs: dict[str, Any]
) -> bool:
has_overridden_args = any(
has_torch_function(arg) for arg in _get_all_args(args, kwargs)
)
tf_state = tx.symbolic_torch_function_state
return (has_overridden_args and tf_state.torch_function_subclass_enabled) or (
tf_state.torch_function_mode_enabled and tf_state.in_torch_function_mode()
)
def dispatch_torch_function(
tx: "InstructionTranslator",
fn: VariableTracker,
args: Iterable[Any],
kwargs: dict[str, Any],
) -> Any:
"""Gathers all args that are TensorWithTFOverrideVariable and dispatches based on the ordering in _get_overloaded_args"""
all_args = _get_all_args(args, kwargs)
overloaded_args = _get_overloaded_args(
[arg for arg in all_args if has_torch_function(arg)],
_get_subclass_type,
)
types = TupleVariable([_get_subclass_type_var(tx, arg) for arg in overloaded_args])
if tx.symbolic_torch_function_state.in_torch_function_mode():
res = tx.symbolic_torch_function_state.call_torch_function_mode(
tx, fn, types, args, kwargs
)
if not res.is_constant_match(NotImplemented):
return res
for arg in overloaded_args:
res = arg.call_torch_function(
tx,
fn,
types,
args,
kwargs,
)
if not res.is_constant_match(NotImplemented):
tx.output.torch_function_subclass_inlined = True
return res
unimplemented(
gb_type="All __torch_function__ overrides returned NotImplemented due to TypeError from user code",
context=f"{fn=}, {args=}, {kwargs=}",
explanation=f"All __torch_function__ overrides for for function {fn} returned NotImplemented",
hints=[
*graph_break_hints.USER_ERROR,
],
)
class TensorWithTFOverrideVariable(TensorVariable):
"""
Represents a tensor subclass instance with a __torch_function__ override.
"""
@classmethod
def from_tensor_var(
cls,
tx: "InstructionTranslator",
tensor_var: VariableTracker,
class_type: type,
cls_source: Source | None,
) -> "TensorWithTFOverrideVariable":
# [Note: __torch_function__] coerce `tensor_var` into a
# TensorWithTFOverrideVariable. In eager, this is just a type change.
import torch
# This simulates shallow-copying the tensor object.
kwargs = dict(tensor_var.__dict__)
input_tensor_type = kwargs.pop("class_type")
assert input_tensor_type in (torch.Tensor, torch.nn.Parameter) or issubclass(
input_tensor_type, torch.Tensor
), (
f"invalid class type {input_tensor_type} in TensorWithTFOverrideVariable.from_tensor_var"
)
var = cls(class_type=class_type, **kwargs)
var.install_global(tx)
return var
def install_global(self, tx: "InstructionTranslator") -> None:
# stash the subclass type to rewrap an output tensor if needed
# this is needed because the actual type needs to be available
# each time the compiled artifact is run and outputs a wrapped tensor.
if self.global_mangled_class_name(tx) not in tx.output.global_scope:
# Safe because global_mangled_class_name figures it out
tx.output.install_global_unsafe(
self.global_mangled_class_name(tx), self.class_type
)
def python_type(self) -> type:
return self.class_type
def class_type_var(self, tx: "InstructionTranslator") -> VariableTracker:
return TensorSubclassVariable(
self.class_type, source=GlobalSource(self.global_mangled_class_name(tx))
)
def global_mangled_class_name(self, tx: "InstructionTranslator") -> str:
return get_safe_global_name(
tx, f"__subclass_{self.class_type.__name__}", self.class_type
)
def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker:
# [Note: __torch_function__] We currently only support attributes that are defined on
# base tensors, custom attribute accesses will graph break.
import torch
# I think only `_base` is breaking because we aren't modelling view
# relationship perfectly in some scenarios.
if name in banned_attrs:
unimplemented(
gb_type="Unsupported tensor subclass attribute access",
context=f"{name}",
explanation="`torch.compile` currently can't trace this",
hints=[
f"Avoid accessing {name} of tensor subclass in torch.compile region",
*graph_break_hints.SUPPORTABLE,
],
)
# Handle non-overridden attributes inherited from `torch.Tensor`.
attr_is_overridden = _is_attr_overridden(tx, self, name)
if (
hasattr(torch.Tensor, name)
and not attr_is_overridden
and not inspect.ismethoddescriptor(getattr(torch.Tensor, name))
):
args = [self]
kwargs: dict[Any, Any] = {}
if can_dispatch_torch_function(tx, args, kwargs):
get_fn = VariableTracker.build(tx, getattr(torch.Tensor, name).__get__)
return self.call_torch_function(
tx,
get_fn,
TupleVariable([self.class_type_var(tx)]),
args,
kwargs,
)
else:
# `TensorVariable.var_getattr` doesn't handle user-defined
# function/attribute well, so we explicitly handle them here.
#
# TODO move this logic into `TensorVariable`, or try to merge it
# with similar logic in `UserDefinedObjectVariable`.
try:
attr = inspect.getattr_static(self.class_type, name)
except AttributeError:
pass
else:
import types
cls_source = GlobalSource(self.global_mangled_class_name(tx))
attr_source = AttrSource(cls_source, name)
if isinstance(attr, types.FunctionType):
install_guard(attr_source.make_guard(GuardBuilder.CLOSURE_MATCH))
return UserMethodVariable(attr, self)
elif isinstance(attr, property):
getter_source = AttrSource(attr_source, "fget")
getter = attr.fget
getter_var = VariableTracker.build(
tx, getter, source=getter_source, realize=True
)
return getter_var.call_function(tx, [self], {})
elif isinstance(attr, classmethod):
return UserMethodVariable(
attr.__func__, self.class_type_var(tx), source=attr_source
)
elif attr_is_overridden:
unimplemented(
gb_type="Unsupported tensor subclass overridden attribute access",
context=f"{name}",
explanation="`torch.compile` only support tracing certain types of overridden tensor subclass attributes",
hints=[
f"Avoid accessing {name} of tensor subclass in torch.compile region",
f"Renaming attribute `{name}` of type {self.class_type}",
*graph_break_hints.SUPPORTABLE,
],
)
return super().var_getattr(tx, name)
def call_torch_function(
self,
tx: "InstructionTranslator",
fn: VariableTracker,
types: TupleVariable,
args: Iterable[Any],
kwargs: dict[str, Any],
) -> Any:
# NOTE this assumes `__torch_function__` isn't modified during tracing.
if not hasattr(self, "torch_function_fn"):
self.torch_function_fn = get_torch_function_fn(tx, self)
return call_torch_function(
tx,
self.torch_function_fn,
fn,
types,
args,
kwargs,
is_subclass_dispatch=True,
)
def call_method(
self,
tx: "InstructionTranslator",
name: str,
args: Sequence[VariableTracker],
kwargs: "dict[str, VariableTracker]",
) -> "VariableTracker":
# This code block implements inlining the __torch_function__ override
# of `call_method`.
tf_args = [self] + list(args)
if can_dispatch_torch_function(tx, tf_args, kwargs):
import torch
if _is_attr_overridden(tx, self, name):
unimplemented(
gb_type="Tensor subclass overridden method call",
context=f"{name}",
explanation="`torch.compile` currently can't trace this",
hints=[
f"Avoid calling {name} of tensor subclass in torch.compile region",
f"Renaming method `{name}` of type {self.class_type}",
*graph_break_hints.SUPPORTABLE,
],
)
# [Note: __torch_function__] Currently we only support methods that are defined on tensor
# we will graph break in other cases this will need a bigger overhaul of extracting methods/comparing them for equality
# We've established with the above check that the method is not overridden, so we guard that the method is the same
# as the impl defined on tensor and retrieve it
if self.source:
source = AttrSource(AttrSource(self.source, "__class__"), name)
value = inspect.getattr_static(self.python_type(), name)
else:
source = None
value = getattr(torch.Tensor, name)
func_var = VariableTracker.build(tx, value, source)
return dispatch_torch_function(tx, func_var, tf_args, kwargs)
else:
return super().call_method(tx, name, args, kwargs)
File diff suppressed because it is too large Load Diff