Initial import: grid-bot — grid trading bot for BTC-USDT on Cifra Markets
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
# mypy: allow-untyped-defs
|
||||
|
||||
import copyreg
|
||||
import os.path as _osp
|
||||
import weakref
|
||||
|
||||
import torch
|
||||
from torch.utils import (
|
||||
backcompat as backcompat,
|
||||
collect_env as collect_env,
|
||||
data as data,
|
||||
deterministic as deterministic,
|
||||
hooks as hooks,
|
||||
)
|
||||
from torch.utils.backend_registration import (
|
||||
generate_methods_for_privateuse1_backend,
|
||||
rename_privateuse1_backend,
|
||||
)
|
||||
from torch.utils.cpp_backtrace import get_cpp_backtrace
|
||||
from torch.utils.throughput_benchmark import ThroughputBenchmark
|
||||
|
||||
|
||||
def set_module(obj, mod):
|
||||
"""
|
||||
Set the module attribute on a python object for a given object for nicer printing
|
||||
"""
|
||||
if not isinstance(mod, str):
|
||||
raise TypeError("The mod argument should be a string")
|
||||
obj.__module__ = mod
|
||||
|
||||
|
||||
cmake_prefix_path = _osp.join(_osp.dirname(_osp.dirname(__file__)), "share", "cmake")
|
||||
|
||||
|
||||
def swap_tensors(t1, t2):
|
||||
"""
|
||||
This function swaps the content of the two Tensor objects.
|
||||
At a high level, this will make t1 have the content of t2 while preserving
|
||||
its identity.
|
||||
|
||||
This will not work if t1 and t2 have different slots.
|
||||
"""
|
||||
# Ensure there are no weakrefs
|
||||
if weakref.getweakrefs(t1):
|
||||
raise RuntimeError("Cannot swap t1 because it has weakref associated with it")
|
||||
if weakref.getweakrefs(t2):
|
||||
raise RuntimeError("Cannot swap t2 because it has weakref associated with it")
|
||||
t1_slots = set(copyreg._slotnames(t1.__class__)) # type: ignore[attr-defined]
|
||||
t2_slots = set(copyreg._slotnames(t2.__class__)) # type: ignore[attr-defined]
|
||||
if t1_slots != t2_slots:
|
||||
raise RuntimeError("Cannot swap t1 and t2 if they have different slots")
|
||||
|
||||
def swap_attr(name):
|
||||
tmp = getattr(t1, name)
|
||||
setattr(t1, name, (getattr(t2, name)))
|
||||
setattr(t2, name, tmp)
|
||||
|
||||
def error_pre_hook(grad_outputs):
|
||||
raise RuntimeError(
|
||||
"Trying to execute AccumulateGrad node that was poisoned by swap_tensors "
|
||||
"this can happen when you try to run backward on a tensor that was swapped. "
|
||||
"For a module m with `torch.__future__.set_swap_module_params_on_conversion(True)` "
|
||||
"you should not change the device or dtype of the module (e.g. `m.cpu()` or `m.half()`) "
|
||||
"between running forward and backward. To resolve this, please only change the "
|
||||
"device/dtype before running forward (or after both forward and backward)."
|
||||
)
|
||||
|
||||
def check_use_count(t, name="t1"):
|
||||
use_count = t._use_count()
|
||||
error_str = (
|
||||
f"Expected use_count of {name} to be 1 or 2 with an AccumulateGrad node but got {use_count} "
|
||||
f"make sure you are not holding references to the tensor in other places."
|
||||
)
|
||||
if use_count > 1:
|
||||
if use_count == 2 and t.is_leaf:
|
||||
accum_grad_node = torch.autograd.graph.get_gradient_edge(t).node
|
||||
# Make sure that the accumulate_grad node was not lazy_init-ed by get_gradient_edge
|
||||
if t._use_count() == 2:
|
||||
accum_grad_node.register_prehook(error_pre_hook)
|
||||
else:
|
||||
raise RuntimeError(error_str)
|
||||
else:
|
||||
raise RuntimeError(error_str)
|
||||
|
||||
check_use_count(t1, "t1")
|
||||
check_use_count(t2, "t2")
|
||||
|
||||
# Swap the types
|
||||
# Note that this will fail if there are mismatched slots
|
||||
swap_attr("__class__")
|
||||
|
||||
# Swap the dynamic attributes
|
||||
swap_attr("__dict__")
|
||||
|
||||
# Swap the slots
|
||||
for slot in t1_slots:
|
||||
if hasattr(t1, slot) and hasattr(t2, slot):
|
||||
swap_attr(slot)
|
||||
elif hasattr(t1, slot):
|
||||
setattr(t2, slot, (getattr(t1, slot)))
|
||||
delattr(t1, slot)
|
||||
elif hasattr(t2, slot):
|
||||
setattr(t1, slot, (getattr(t2, slot)))
|
||||
delattr(t2, slot)
|
||||
|
||||
# Swap the at::Tensor they point to
|
||||
torch._C._swap_tensor_impl(t1, t2)
|
||||
@@ -0,0 +1,135 @@
|
||||
import base64
|
||||
import zlib
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
_ENCODING_VERSION: int = 1
|
||||
|
||||
__all__ = ["AppendingByteSerializer"]
|
||||
|
||||
|
||||
#######################################
|
||||
# Helper classes
|
||||
#######################################
|
||||
|
||||
CHECKSUM_DIGEST_SIZE = 4
|
||||
|
||||
|
||||
class BytesWriter:
|
||||
def __init__(self) -> None:
|
||||
# Reserve CHECKSUM_DIGEST_SIZE bytes for checksum
|
||||
self._data = bytearray(CHECKSUM_DIGEST_SIZE)
|
||||
|
||||
def write_uint64(self, i: int) -> None:
|
||||
self._data.extend(i.to_bytes(8, byteorder="big", signed=False))
|
||||
|
||||
def write_str(self, s: str) -> None:
|
||||
payload = base64.b64encode(s.encode("utf-8"))
|
||||
self.write_bytes(payload)
|
||||
|
||||
def write_bytes(self, b: bytes) -> None:
|
||||
self.write_uint64(len(b))
|
||||
self._data.extend(b)
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
digest = zlib.crc32(self._data[CHECKSUM_DIGEST_SIZE:]).to_bytes(
|
||||
4, byteorder="big", signed=False
|
||||
)
|
||||
if len(digest) != CHECKSUM_DIGEST_SIZE:
|
||||
raise AssertionError("Computed checksum digest has unexpected size")
|
||||
self._data[0:CHECKSUM_DIGEST_SIZE] = digest
|
||||
return bytes(self._data)
|
||||
|
||||
|
||||
class BytesReader:
|
||||
def __init__(self, data: bytes) -> None:
|
||||
# Check for data corruption
|
||||
if len(data) < CHECKSUM_DIGEST_SIZE:
|
||||
raise AssertionError("Input data is too short to contain checksum")
|
||||
digest = zlib.crc32(data[CHECKSUM_DIGEST_SIZE:]).to_bytes(
|
||||
4, byteorder="big", signed=False
|
||||
)
|
||||
if len(digest) != CHECKSUM_DIGEST_SIZE:
|
||||
raise AssertionError("Computed checksum digest has unexpected size")
|
||||
if data[0:CHECKSUM_DIGEST_SIZE] != digest:
|
||||
raise RuntimeError(
|
||||
"Bytes object is corrupted, checksum does not match. "
|
||||
f"Expected: {data[0:CHECKSUM_DIGEST_SIZE]!r}, Got: {digest!r}"
|
||||
)
|
||||
|
||||
self._data = data
|
||||
self._i = CHECKSUM_DIGEST_SIZE
|
||||
|
||||
def is_finished(self) -> bool:
|
||||
return len(self._data) == self._i
|
||||
|
||||
def read_uint64(self) -> int:
|
||||
result = int.from_bytes(
|
||||
self._data[self._i : self._i + 8], byteorder="big", signed=False
|
||||
)
|
||||
self._i += 8
|
||||
return result
|
||||
|
||||
def read_str(self) -> str:
|
||||
return base64.b64decode(self.read_bytes()).decode("utf-8")
|
||||
|
||||
def read_bytes(self) -> bytes:
|
||||
size = self.read_uint64()
|
||||
result = self._data[self._i : self._i + size]
|
||||
self._i += size
|
||||
return result
|
||||
|
||||
|
||||
#######################################
|
||||
# AppendingByteSerializer
|
||||
#######################################
|
||||
|
||||
|
||||
class AppendingByteSerializer(Generic[T]):
|
||||
"""
|
||||
Provides efficient serialization and deserialization of list of bytes
|
||||
Note that this does not provide any guarantees around byte order
|
||||
"""
|
||||
|
||||
_serialize_fn: Callable[[BytesWriter, T], None]
|
||||
_writer: BytesWriter
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
serialize_fn: Callable[[BytesWriter, T], None],
|
||||
) -> None:
|
||||
self._serialize_fn = serialize_fn
|
||||
self.clear()
|
||||
|
||||
def clear(self) -> None:
|
||||
self._writer = BytesWriter()
|
||||
# First 8-bytes are for version
|
||||
self._writer.write_uint64(_ENCODING_VERSION)
|
||||
|
||||
def append(self, data: T) -> None:
|
||||
self._serialize_fn(self._writer, data)
|
||||
|
||||
def extend(self, elems: Iterable[T]) -> None:
|
||||
for elem in elems:
|
||||
self.append(elem)
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
return self._writer.to_bytes()
|
||||
|
||||
@staticmethod
|
||||
def to_list(data: bytes, *, deserialize_fn: Callable[[BytesReader], T]) -> list[T]:
|
||||
reader = BytesReader(data)
|
||||
if reader.read_uint64() != _ENCODING_VERSION:
|
||||
raise AssertionError(
|
||||
f"Encoding version mismatch in AppendingByteSerializer.to_list, \
|
||||
got {reader.read_uint64()}"
|
||||
)
|
||||
|
||||
result: list[T] = []
|
||||
while not reader.is_finished():
|
||||
result.append(deserialize_fn(reader))
|
||||
return result
|
||||
@@ -0,0 +1,913 @@
|
||||
import contextlib
|
||||
import copy
|
||||
import hashlib
|
||||
import importlib
|
||||
import inspect
|
||||
import io
|
||||
import os
|
||||
import pickle
|
||||
import tokenize
|
||||
import unittest
|
||||
from collections.abc import Callable
|
||||
from contextvars import ContextVar
|
||||
from dataclasses import dataclass
|
||||
from types import FunctionType, ModuleType
|
||||
from typing import Any, Generic, NoReturn, Optional, TYPE_CHECKING, TypeVar
|
||||
from typing_extensions import deprecated
|
||||
|
||||
from torch._utils_internal import justknobs_check
|
||||
|
||||
|
||||
# Types saved/loaded in configs
|
||||
CONFIG_TYPES = (int, float, bool, type(None), str, list, set, tuple, dict)
|
||||
|
||||
# Immutable scalar types that don't need deepcopy when returned from configs.
|
||||
# Everything else is defensively copied to prevent accidental mutation.
|
||||
_IMMUTABLE_CONFIG_TYPES = (int, float, bool, type(None), str, tuple)
|
||||
|
||||
|
||||
# Duplicated, because mypy needs these types statically
|
||||
T = TypeVar("T", bound=int | float | bool | str | list | set | tuple | dict | None)
|
||||
|
||||
|
||||
_UNSET_SENTINEL = object()
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class _Config(Generic[T]):
|
||||
"""Represents a config with richer behaviour than just a default value.
|
||||
::
|
||||
i.e.
|
||||
foo = Config(justknob="//foo:bar", default=False)
|
||||
install_config_module(...)
|
||||
|
||||
This configs must be installed with install_config_module to be used
|
||||
|
||||
Precedence Order:
|
||||
alias: If set, the directly use the value of the alias.
|
||||
env_name_force: If set, this environment variable has precedence over
|
||||
everything after this.
|
||||
If multiple env variables are given, the precedence order is from
|
||||
left to right.
|
||||
user_override: If a user sets a value (i.e. foo.bar=True), that
|
||||
has precedence over everything after this. User overrides are thread-local.
|
||||
env_name_default: If set, this environment variable will override everything
|
||||
after this.
|
||||
If multiple env variables are given, the precedence order is from
|
||||
left to right.
|
||||
justknob: If this pytorch installation supports justknobs, that will
|
||||
override defaults, but will not override the user_override precedence.
|
||||
default: This value is the lowest precedence, and will be used if nothing is
|
||||
set.
|
||||
|
||||
Environment Variables:
|
||||
These are interpreted to be either "0" or "1" to represent true and false.
|
||||
|
||||
Arguments:
|
||||
justknob: the name of the feature / JK. In OSS this is unused.
|
||||
default: is the value to default this knob to in OSS.
|
||||
alias: The alias config to read instead.
|
||||
env_name_force: The environment variable, or list of, to read that is a FORCE
|
||||
environment variable. I.e. it overrides everything except for alias.
|
||||
env_name_default: The environment variable, or list of, to read that changes the
|
||||
default behaviour. I.e. user overrides take preference.
|
||||
"""
|
||||
|
||||
default: T | object
|
||||
justknob: str | None = None
|
||||
env_name_default: list[str] | None = None
|
||||
env_name_force: list[str] | None = None
|
||||
value_type: type | None = None
|
||||
alias: str | None = None
|
||||
# Deprecation support
|
||||
deprecated: bool = False
|
||||
deprecation_message: str | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.env_name_default = _Config.string_or_list_of_string_to_list(
|
||||
self.env_name_default
|
||||
)
|
||||
self.env_name_force = _Config.string_or_list_of_string_to_list(
|
||||
self.env_name_force
|
||||
)
|
||||
|
||||
if self.alias is not None:
|
||||
if (
|
||||
self.default is not _UNSET_SENTINEL
|
||||
or self.justknob is not None
|
||||
or self.env_name_default is not None
|
||||
or self.env_name_force is not None
|
||||
):
|
||||
raise AssertionError(
|
||||
"if alias is set, none of {default, justknob, \
|
||||
env_name_default and env_name_force} can be set"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def string_or_list_of_string_to_list(
|
||||
val: str | list[str] | None,
|
||||
) -> list[str] | None:
|
||||
if val is None:
|
||||
return None
|
||||
if isinstance(val, str):
|
||||
return [val]
|
||||
if not isinstance(val, list):
|
||||
raise AssertionError(f"val is not a list, got {type(val)}")
|
||||
return val
|
||||
|
||||
|
||||
# In runtime, we unbox the Config[T] to a T, but typechecker cannot see this,
|
||||
# so in order to allow for this dynamic behavior to work correctly with
|
||||
# typechecking we are going to lie to the typechecker that Config[T] returns
|
||||
# a T.
|
||||
if TYPE_CHECKING:
|
||||
|
||||
def Config(
|
||||
default: T | object = _UNSET_SENTINEL,
|
||||
justknob: str | None = None,
|
||||
env_name_default: str | list[str] | None = None,
|
||||
env_name_force: str | list[str] | None = None,
|
||||
value_type: type | None = None,
|
||||
alias: str | None = None,
|
||||
# Deprecation support
|
||||
deprecated: bool = False,
|
||||
deprecation_message: str | None = None,
|
||||
) -> T: ...
|
||||
|
||||
else:
|
||||
|
||||
def Config(
|
||||
default: T | object = _UNSET_SENTINEL,
|
||||
justknob: str | None = None,
|
||||
env_name_default: str | list[str] | None = None,
|
||||
env_name_force: str | list[str] | None = None,
|
||||
value_type: type | None = None,
|
||||
alias: str | None = None,
|
||||
# Deprecation support
|
||||
deprecated: bool = False,
|
||||
deprecation_message: str | None = None,
|
||||
) -> _Config[T]:
|
||||
return _Config(
|
||||
default=default,
|
||||
justknob=justknob,
|
||||
env_name_default=env_name_default,
|
||||
env_name_force=env_name_force,
|
||||
value_type=value_type,
|
||||
alias=alias,
|
||||
# Deprecation support
|
||||
deprecated=deprecated,
|
||||
deprecation_message=deprecation_message,
|
||||
)
|
||||
|
||||
|
||||
def _read_env_variable(name: str) -> bool | str | None:
|
||||
value = os.environ.get(name)
|
||||
if value == "1":
|
||||
return True
|
||||
if value == "0":
|
||||
return False
|
||||
return value
|
||||
|
||||
|
||||
def install_config_module(module: ModuleType) -> None:
|
||||
"""
|
||||
Converts a module-level config into a `ConfigModule()`.
|
||||
|
||||
See _config_typing.pyi for instructions on how to get the converted module to typecheck.
|
||||
"""
|
||||
|
||||
class ConfigModuleInstance(ConfigModule):
|
||||
# __annotations__ is written to by Sphinx autodoc
|
||||
_bypass_keys = set({"_is_dirty", "_hash_digest", "__annotations__"})
|
||||
|
||||
def visit(
|
||||
source: ModuleType | type,
|
||||
dest: ModuleType | SubConfigProxy,
|
||||
prefix: str,
|
||||
) -> None:
|
||||
"""Walk the module structure and move everything to module._config"""
|
||||
type_hints = inspect.get_annotations(source)
|
||||
for key, value in list(source.__dict__.items()):
|
||||
if (
|
||||
key.startswith("__")
|
||||
or isinstance(value, (ModuleType, FunctionType))
|
||||
or (
|
||||
hasattr(value, "__module__")
|
||||
and (
|
||||
value.__module__ == "typing"
|
||||
or value.__module__.startswith("collections.abc")
|
||||
)
|
||||
)
|
||||
# Handle from torch.utils._config_module import Config
|
||||
or (isinstance(value, type) and issubclass(value, _Config))
|
||||
):
|
||||
continue
|
||||
|
||||
name = f"{prefix}{key}"
|
||||
annotated_type = type_hints.get(key, None)
|
||||
if isinstance(value, CONFIG_TYPES):
|
||||
config[name] = _ConfigEntry(
|
||||
_Config(default=value, value_type=annotated_type), name
|
||||
)
|
||||
if dest is module:
|
||||
delattr(module, key)
|
||||
elif isinstance(value, _Config):
|
||||
if annotated_type is not None and value.value_type is None:
|
||||
value.value_type = annotated_type
|
||||
|
||||
config[name] = _ConfigEntry(value, name)
|
||||
|
||||
if dest is module:
|
||||
delattr(module, key)
|
||||
elif isinstance(value, type):
|
||||
if value.__module__ != module.__name__:
|
||||
raise AssertionError(
|
||||
f"subconfig class {value} must be defined in module {module.__name__}"
|
||||
)
|
||||
# a subconfig with `class Blah:` syntax
|
||||
proxy = SubConfigProxy(module, f"{name}.")
|
||||
visit(value, proxy, f"{name}.")
|
||||
if dest is module:
|
||||
setattr(dest, key, proxy)
|
||||
else:
|
||||
dest.__dict__[key] = proxy
|
||||
else:
|
||||
raise AssertionError(f"Unhandled config {key}={value} ({type(value)})")
|
||||
|
||||
config: dict[str, _ConfigEntry] = {}
|
||||
|
||||
compile_ignored_keys = get_assignments_with_compile_ignored_comments(module)
|
||||
|
||||
visit(module, module, "")
|
||||
module._config = config # type: ignore[attr-defined]
|
||||
module._compile_ignored_keys = compile_ignored_keys # type: ignore[attr-defined]
|
||||
module.__class__ = ConfigModuleInstance
|
||||
module._is_dirty = True # type: ignore[attr-defined]
|
||||
module._hash_digest = None # type: ignore[attr-defined]
|
||||
|
||||
|
||||
COMPILE_IGNORED_MARKER = "@compile_ignored"
|
||||
|
||||
|
||||
# Gets all the keys (i.e. assignments) with a @compile_ignored comment
|
||||
def get_assignments_with_compile_ignored_comments(module: ModuleType) -> set[str]:
|
||||
source_code = inspect.getsource(module)
|
||||
assignments = set()
|
||||
|
||||
# Tokenize the source code to retrieve comments
|
||||
tokens = tokenize.tokenize(io.BytesIO(source_code.encode("utf-8")).readline)
|
||||
current_comment = "", -1
|
||||
prev_name = ""
|
||||
|
||||
for token in tokens:
|
||||
if token.type == tokenize.COMMENT:
|
||||
prev_name = ""
|
||||
maybe_current = token.string.strip()
|
||||
if COMPILE_IGNORED_MARKER in maybe_current:
|
||||
if current_comment != ("", -1):
|
||||
raise AssertionError(f"unconsumed {COMPILE_IGNORED_MARKER}")
|
||||
current_comment = maybe_current, token.start[0]
|
||||
elif token.type == tokenize.NAME:
|
||||
# Only accept the first name token, to handle if you have
|
||||
# something like foo: Bar = ...
|
||||
if not prev_name:
|
||||
prev_name = token.string
|
||||
elif token.type == tokenize.OP and token.string == "=":
|
||||
# Check if the current assignment follows a comment
|
||||
# with COMPILE_IGNORED_MARKER
|
||||
if (
|
||||
COMPILE_IGNORED_MARKER in current_comment[0]
|
||||
and current_comment[1] == token.start[0] - 1
|
||||
):
|
||||
assignments.add(prev_name)
|
||||
current_comment = "", -1 # reset
|
||||
prev_name = ""
|
||||
if current_comment != ("", -1):
|
||||
raise AssertionError(f"unconsumed {COMPILE_IGNORED_MARKER}")
|
||||
return assignments
|
||||
|
||||
|
||||
@dataclass
|
||||
class _ConfigEntry:
|
||||
# The default value specified in the configuration
|
||||
default: Any
|
||||
# The type of the configuration value
|
||||
value_type: type
|
||||
# The value specified by the user when they overrode the configuration
|
||||
# _UNSET_SENTINEL indicates the value is not set.
|
||||
user_override: ContextVar[object]
|
||||
# The justknob to check for this config
|
||||
justknob: str | None = None
|
||||
# environment variables are read at install time
|
||||
env_value_force: Any = _UNSET_SENTINEL
|
||||
env_value_default: Any = _UNSET_SENTINEL
|
||||
# Used to work arounds bad assumptions in unittest.mock.patch
|
||||
# The code to blame is
|
||||
# https://github.com/python/cpython/blob/94a7a4e22fb8f567090514785c69e65298acca42/Lib/unittest/mock.py#L1637
|
||||
# Essentially, mock.patch requires, that if __dict__ isn't accessible
|
||||
# (which it isn't), that after delattr is called on the object, the
|
||||
# object must throw when hasattr is called. Otherwise, it doesn't call
|
||||
# setattr again.
|
||||
# Technically we'll have an intermediate state of hiding the config while
|
||||
# mock.patch is unpatching itself, but it calls setattr after the delete
|
||||
# call so the final state is correct. It's just very unintuitive.
|
||||
# upstream bug - python/cpython#126886
|
||||
hide: bool = False
|
||||
alias: str | None = None
|
||||
# Deprecation support
|
||||
deprecated: bool = False
|
||||
deprecation_message: str | None = None
|
||||
_deprecation_warned: bool = False
|
||||
|
||||
def __init__(self, config: _Config, name: str) -> None:
|
||||
self.default = config.default
|
||||
self.value_type = (
|
||||
config.value_type if config.value_type is not None else type(self.default)
|
||||
)
|
||||
self.justknob = config.justknob
|
||||
self.alias = config.alias
|
||||
# Deprecation fields
|
||||
self.deprecated = config.deprecated
|
||||
self.deprecation_message = config.deprecation_message
|
||||
self._deprecation_warned = False
|
||||
|
||||
self.user_override = ContextVar(name, default=_UNSET_SENTINEL)
|
||||
if config.env_name_default is not None:
|
||||
for val in config.env_name_default:
|
||||
if (env_value := _read_env_variable(val)) is not None:
|
||||
self.env_value_default = env_value
|
||||
break
|
||||
if config.env_name_force is not None:
|
||||
for val in config.env_name_force:
|
||||
if (env_value := _read_env_variable(val)) is not None:
|
||||
self.env_value_force = env_value
|
||||
break
|
||||
|
||||
# Ensure justknobs and envvars are allowlisted types
|
||||
if self.justknob is not None and self.default is not None:
|
||||
if not isinstance(self.default, bool):
|
||||
raise AssertionError(
|
||||
f"justknobs only support booleans, {self.default} is not a boolean"
|
||||
)
|
||||
if self.value_type is not None and (
|
||||
config.env_name_default is not None or config.env_name_force is not None
|
||||
):
|
||||
if self.value_type not in (
|
||||
bool,
|
||||
str,
|
||||
Optional[bool], # noqa: UP045
|
||||
Optional[str], # noqa: UP045
|
||||
):
|
||||
raise AssertionError(
|
||||
f"envvar configs only support (optional) booleans or strings, {self.value_type} is neither"
|
||||
)
|
||||
|
||||
|
||||
class ConfigModule(ModuleType):
|
||||
# NOTE: This should be kept in sync with _config_typing.pyi.
|
||||
|
||||
# The actual configuration settings. E.g., torch._dynamo.config.debug
|
||||
# would live as "debug" in the key, and torch._inductor.config.triton.cudagraphs
|
||||
# maps as "triton.cudagraphs". See discussion on the class for meaning of various sub items
|
||||
_config: dict[str, _ConfigEntry]
|
||||
_bypass_keys: set[str]
|
||||
_compile_ignored_keys: set[str]
|
||||
_is_dirty: bool
|
||||
_hash_digest: bytes | None
|
||||
|
||||
def __init__(self) -> None:
|
||||
raise NotImplementedError(
|
||||
f"use {__name__}.install_config_module(sys.modules[__name__])"
|
||||
)
|
||||
|
||||
def _warn_if_deprecated(self, name: str, config: _ConfigEntry) -> None:
|
||||
"""Issue deprecation warning for config if not already warned."""
|
||||
if config.deprecated and not config._deprecation_warned:
|
||||
import warnings
|
||||
|
||||
msg = f"{self.__name__}.{name} is deprecated"
|
||||
if config.deprecation_message:
|
||||
msg += f" and {config.deprecation_message}"
|
||||
msg += ". It will be removed in a future version of PyTorch."
|
||||
warnings.warn(msg, FutureWarning, stacklevel=3)
|
||||
config._deprecation_warned = True
|
||||
|
||||
def __setattr__(self, name: str, value: object) -> None:
|
||||
if name in self._bypass_keys:
|
||||
super().__setattr__(name, value)
|
||||
elif name not in self._config:
|
||||
raise AttributeError(f"{self.__name__}.{name} does not exist")
|
||||
else:
|
||||
# Issue deprecation warning on write (once per config)
|
||||
config = self._config[name]
|
||||
self._warn_if_deprecated(name, config)
|
||||
|
||||
if config.alias is not None:
|
||||
self._set_alias_val(config, value)
|
||||
else:
|
||||
config.user_override.set(value)
|
||||
self._is_dirty = True
|
||||
config.hide = False
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
try:
|
||||
config = self._config[name]
|
||||
|
||||
if config.hide:
|
||||
raise AttributeError(f"{self.__name__}.{name} does not exist")
|
||||
|
||||
# Issue deprecation warning on read (once per config)
|
||||
self._warn_if_deprecated(name, config)
|
||||
|
||||
alias_val = self._get_alias_val(config)
|
||||
if alias_val is not _UNSET_SENTINEL:
|
||||
return alias_val
|
||||
|
||||
if config.env_value_force is not _UNSET_SENTINEL:
|
||||
return config.env_value_force
|
||||
|
||||
user_override = config.user_override.get()
|
||||
if user_override is not _UNSET_SENTINEL:
|
||||
return user_override
|
||||
|
||||
if config.env_value_default is not _UNSET_SENTINEL:
|
||||
return config.env_value_default
|
||||
|
||||
if config.justknob is not None:
|
||||
# JK only supports bools and ints
|
||||
return justknobs_check(name=config.justknob, default=config.default)
|
||||
|
||||
# Reference types can still be modified, so copy them to
|
||||
# user_overrides to prevent accidental mutation of defaults.
|
||||
if not isinstance(config.default, _IMMUTABLE_CONFIG_TYPES):
|
||||
config.user_override.set(copy.deepcopy(config.default))
|
||||
return config.user_override.get()
|
||||
return config.default
|
||||
|
||||
except KeyError as e:
|
||||
# make hasattr() work properly
|
||||
raise AttributeError(f"{self.__name__}.{name} does not exist") from e
|
||||
|
||||
def __delattr__(self, name: str) -> None:
|
||||
self._is_dirty = True
|
||||
# must support delete because unittest.mock.patch deletes
|
||||
# then recreate things
|
||||
self._config[name].user_override.set(_UNSET_SENTINEL)
|
||||
self._config[name].hide = True
|
||||
|
||||
def _get_alias_module_and_name(
|
||||
self, entry: _ConfigEntry
|
||||
) -> tuple[ModuleType, str] | None:
|
||||
alias = entry.alias
|
||||
if alias is None:
|
||||
return None
|
||||
module_name, constant_name = alias.rsplit(".", 1)
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
except ImportError as e:
|
||||
raise AttributeError(f"config alias {alias} does not exist") from e
|
||||
return module, constant_name
|
||||
|
||||
def _get_alias_val(self, entry: _ConfigEntry) -> Any:
|
||||
data = self._get_alias_module_and_name(entry)
|
||||
if data is None:
|
||||
return _UNSET_SENTINEL
|
||||
module, constant_name = data
|
||||
constant_value = getattr(module, constant_name)
|
||||
return constant_value
|
||||
|
||||
def _set_alias_val(self, entry: _ConfigEntry, val: Any) -> None:
|
||||
data = self._get_alias_module_and_name(entry)
|
||||
if data is None:
|
||||
raise AssertionError(
|
||||
"alias data should not be None when setting alias value"
|
||||
)
|
||||
module, constant_name = data
|
||||
setattr(module, constant_name, val)
|
||||
|
||||
def _is_default(self, name: str) -> bool:
|
||||
"""
|
||||
Returns true if the config is at its default value.
|
||||
configs overridden by the env are not considered default.
|
||||
"""
|
||||
config_val = self._config[name]
|
||||
# The config is not overridden by the user, and the env_value_default
|
||||
# is different from the default value (meaning user has set the env to
|
||||
# change the default value).
|
||||
not_set_env_default = (
|
||||
config_val.env_value_default is _UNSET_SENTINEL
|
||||
or config_val.env_value_default == config_val.default
|
||||
)
|
||||
not_set_env_force = (
|
||||
config_val.env_value_force is _UNSET_SENTINEL
|
||||
or config_val.env_value_force == config_val.default
|
||||
)
|
||||
|
||||
unset = config_val.user_override.get() is _UNSET_SENTINEL
|
||||
# Handle reference types specially to avoid spammy warnings
|
||||
if not isinstance(config_val.default, _IMMUTABLE_CONFIG_TYPES):
|
||||
unset = unset or config_val.user_override.get() == config_val.default
|
||||
return unset and not_set_env_default and not_set_env_force
|
||||
|
||||
def _get_dict(
|
||||
self,
|
||||
ignored_keys: list[str] | None = None,
|
||||
ignored_prefixes: list[str] | None = None,
|
||||
skip_default: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Export a dictionary of current configuration keys and values.
|
||||
|
||||
This function is design to provide a single point which handles
|
||||
accessing config options and exporting them into a dictionary.
|
||||
This is used by a number of different user facing export methods
|
||||
which all have slightly different semantics re: how and what to
|
||||
skip.
|
||||
If a config is aliased, it skips this config.
|
||||
|
||||
Arguments:
|
||||
ignored_keys are keys that should not be exported.
|
||||
ignored_prefixes are prefixes that if a key matches should
|
||||
not be exported
|
||||
skip_default does two things. One if a key has not been modified
|
||||
it skips it.
|
||||
"""
|
||||
config: dict[str, Any] = {}
|
||||
for key, entry in self._config.items():
|
||||
if entry.alias is not None:
|
||||
continue
|
||||
if ignored_keys and key in ignored_keys:
|
||||
continue
|
||||
if ignored_prefixes:
|
||||
if any(key.startswith(prefix) for prefix in ignored_prefixes):
|
||||
continue
|
||||
if skip_default and self._is_default(key):
|
||||
continue
|
||||
|
||||
# Read value directly, bypassing __getattr__ overhead
|
||||
# (deprecation warnings, alias resolution).
|
||||
user_override = entry.user_override.get()
|
||||
if entry.env_value_force is not _UNSET_SENTINEL:
|
||||
val = entry.env_value_force
|
||||
elif user_override is not _UNSET_SENTINEL:
|
||||
val = user_override
|
||||
elif entry.env_value_default is not _UNSET_SENTINEL:
|
||||
val = entry.env_value_default
|
||||
elif entry.justknob is not None:
|
||||
val = justknobs_check(name=entry.justknob, default=entry.default)
|
||||
else:
|
||||
val = entry.default
|
||||
if not isinstance(val, _IMMUTABLE_CONFIG_TYPES):
|
||||
val = copy.deepcopy(val)
|
||||
config[key] = val
|
||||
|
||||
return config
|
||||
|
||||
def get_type(self, config_name: str) -> type:
|
||||
return self._config[config_name].value_type
|
||||
|
||||
def save_config(self) -> bytes:
|
||||
"""Convert config to a pickled blob"""
|
||||
ignored_keys = getattr(self, "_save_config_ignore", [])
|
||||
return pickle.dumps(
|
||||
self._get_dict(ignored_keys=ignored_keys),
|
||||
protocol=2,
|
||||
)
|
||||
|
||||
def save_config_portable(
|
||||
self, *, ignore_private_configs: bool = True
|
||||
) -> dict[str, Any]:
|
||||
"""Convert config to portable format"""
|
||||
prefixes = []
|
||||
if ignore_private_configs:
|
||||
prefixes.append("_")
|
||||
prefixes.extend(getattr(self, "_cache_config_ignore_prefix", []))
|
||||
config = self._get_dict(ignored_prefixes=prefixes)
|
||||
for key in getattr(self, "_cache_config_factory_keys", []):
|
||||
if key in config and config[key] is not None:
|
||||
instance = config[key]()
|
||||
if hasattr(instance, "uuid"):
|
||||
config[key] = instance.uuid()
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"Config '{key}' is set to {config[key]} which does not "
|
||||
f"implement uuid(). Implement uuid() for cache key "
|
||||
f"participation."
|
||||
)
|
||||
return config
|
||||
|
||||
def codegen_config(self) -> str:
|
||||
"""Convert config to Python statements that replicate current config.
|
||||
This does NOT include config settings that are at default values.
|
||||
"""
|
||||
|
||||
# additional imports required
|
||||
imports = set()
|
||||
|
||||
def get_module_name(func: Callable, add_dot: bool) -> str:
|
||||
module_name = func.__module__
|
||||
if module_name == "builtins":
|
||||
module_name = ""
|
||||
if add_dot and module_name != "":
|
||||
module_name += "."
|
||||
return module_name
|
||||
|
||||
def add_import(func: Callable) -> None:
|
||||
module_name = get_module_name(func, False)
|
||||
if module_name:
|
||||
imports.add(module_name)
|
||||
|
||||
def list_of_callables_to_string(v: list | set) -> list[str]:
|
||||
return [f"{get_module_name(item, True)}{item.__name__}" for item in v]
|
||||
|
||||
def importable_callable(v: Any) -> bool:
|
||||
# functools.partial has no attributes below but is a callable
|
||||
return callable(v) and hasattr(v, "__module__") and hasattr(v, "__name__")
|
||||
|
||||
def get_config_line(mod, k, v) -> str: # type: ignore[no-untyped-def]
|
||||
"""
|
||||
Return a string version of the config line.
|
||||
Handle v when v is a callable, or a list/dict of callables. Add import statements for callables if necessary.
|
||||
We assume that the value of a single config won't be a mix of callables and non-callables.
|
||||
|
||||
Example output:
|
||||
import logging
|
||||
import _warnings
|
||||
torch._dynamo.config.reorderable_logging_functions = { _warnings.warn, logging.warn, print }
|
||||
"""
|
||||
if importable_callable(v):
|
||||
add_import(v)
|
||||
return f"{mod}.{k} = {get_module_name(v, True)}{v.__name__}"
|
||||
elif isinstance(v, (list, set)) and all(
|
||||
importable_callable(item) for item in v
|
||||
):
|
||||
for item in v:
|
||||
add_import(item)
|
||||
v_list = list_of_callables_to_string(v)
|
||||
if isinstance(v, list):
|
||||
return f"{mod}.{k} = {v_list}"
|
||||
else:
|
||||
return f"{mod}.{k} = {{ {', '.join(v_list)} }}"
|
||||
else:
|
||||
return f"{mod}.{k} = {v!r}"
|
||||
|
||||
lines = []
|
||||
mod = self.__name__
|
||||
for k, v in self._get_dict(
|
||||
ignored_keys=getattr(self, "_save_config_ignore", []), skip_default=True
|
||||
).items():
|
||||
lines.append(get_config_line(mod, k, v))
|
||||
for import_name in imports:
|
||||
lines.insert(0, f"import {import_name}")
|
||||
return "\n".join(lines)
|
||||
|
||||
def get_hash(self) -> bytes:
|
||||
"""Hashes the configs that are not compile_ignored"""
|
||||
if self._is_dirty or self._hash_digest is None:
|
||||
dict_to_hash = self._get_dict(ignored_keys=list(self._compile_ignored_keys))
|
||||
string_to_hash = repr(sorted(dict_to_hash.items()))
|
||||
self._hash_digest = hashlib.md5(
|
||||
string_to_hash.encode("utf-8"), usedforsecurity=False
|
||||
).digest()
|
||||
self._is_dirty = False
|
||||
return self._hash_digest
|
||||
|
||||
@deprecated(
|
||||
"`config.to_dict()` has been deprecated. It no longer changes the underlying config."
|
||||
" use `config.get_config_copy()` instead if you just want a copy of the config, or "
|
||||
"config.load_config if you need mutable access",
|
||||
category=FutureWarning,
|
||||
)
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return self.get_config_copy()
|
||||
|
||||
@deprecated(
|
||||
"`config.shallow_copy_dict()` has been deprecated. It no longer changes the underlying config."
|
||||
" use `config.get_config_copy()` instead if you just want a copy of the config, or "
|
||||
"config.load_config if you need mutable access",
|
||||
category=FutureWarning,
|
||||
)
|
||||
def shallow_copy_dict(self) -> dict[str, Any]:
|
||||
return self.get_config_copy()
|
||||
|
||||
def load_config(self, maybe_pickled_config: bytes | dict[str, Any]) -> None:
|
||||
"""Restore from a prior call to save_config() or shallow_copy_dict()"""
|
||||
if not isinstance(maybe_pickled_config, dict):
|
||||
config = pickle.loads(maybe_pickled_config)
|
||||
else:
|
||||
config = maybe_pickled_config
|
||||
for k, v in config.items():
|
||||
if k in self._config:
|
||||
setattr(self, k, v)
|
||||
else:
|
||||
from torch._dynamo.utils import warn_once
|
||||
|
||||
warn_once(f"key {k} with value {v} is not understood by this config")
|
||||
|
||||
def get_config_copy(self) -> dict[str, Any]:
|
||||
return self._get_dict()
|
||||
|
||||
def get_serializable_config_copy(self) -> dict[str, Any]:
|
||||
return self._get_dict(ignored_keys=getattr(self, "_save_config_ignore", []))
|
||||
|
||||
def patch(
|
||||
self,
|
||||
arg1: str | dict[str, Any] | None = None,
|
||||
arg2: Any = None,
|
||||
**kwargs: dict[str, Any],
|
||||
) -> "ContextDecorator":
|
||||
"""
|
||||
Decorator and/or context manager to make temporary changes to a config. Note that patched settings are thread-local.
|
||||
|
||||
As a decorator:
|
||||
|
||||
@config.patch("name", val)
|
||||
@config.patch(name1=val1, name2=val2)
|
||||
@config.patch({"name1": val1, "name2", val2})
|
||||
def foo(...):
|
||||
...
|
||||
|
||||
As a context manager:
|
||||
|
||||
with config.patch("name", val):
|
||||
...
|
||||
"""
|
||||
changes: dict[str, Any]
|
||||
if arg1 is not None:
|
||||
if arg2 is not None:
|
||||
if not isinstance(arg1, str):
|
||||
raise AssertionError(
|
||||
"first argument must be a string when passing 2 positional args to patch"
|
||||
)
|
||||
# patch("key", True) syntax
|
||||
changes = {arg1: arg2}
|
||||
else:
|
||||
if not isinstance(arg1, dict):
|
||||
raise AssertionError(
|
||||
"first argument must be a dict when passing a single positional arg to patch"
|
||||
)
|
||||
# patch({"key": True}) syntax
|
||||
changes = arg1
|
||||
if kwargs:
|
||||
raise AssertionError(
|
||||
"cannot pass both positional and keyword arguments to patch"
|
||||
)
|
||||
else:
|
||||
# patch(key=True) syntax
|
||||
changes = kwargs
|
||||
if arg2 is not None:
|
||||
raise AssertionError(
|
||||
"second positional argument is only valid when first argument is a key string"
|
||||
)
|
||||
if not isinstance(changes, dict):
|
||||
raise AssertionError(f"expected `dict` got {type(changes)}")
|
||||
config = self
|
||||
|
||||
class ConfigPatch(ContextDecorator):
|
||||
def __init__(self) -> None:
|
||||
self.changes = changes
|
||||
self._prior: ContextVar[tuple[dict[str, Any], ...]] = ContextVar(
|
||||
f"{config.__name__}.ConfigPatch[{id(self)}]",
|
||||
default=(),
|
||||
)
|
||||
|
||||
def __enter__(self) -> None:
|
||||
prior: dict[str, Any] = {}
|
||||
for key in self.changes:
|
||||
# KeyError on invalid entry
|
||||
prior[key] = config.__getattr__(key)
|
||||
prior_stack = self._prior.get()
|
||||
self._prior.set((*prior_stack, prior))
|
||||
try:
|
||||
for k, v in self.changes.items():
|
||||
config.__setattr__(k, v)
|
||||
except Exception:
|
||||
self._prior.set(prior_stack)
|
||||
raise
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb): # type: ignore[no-untyped-def]
|
||||
prior_stack = self._prior.get()
|
||||
if not prior_stack:
|
||||
raise AssertionError(
|
||||
"prior should not be empty when exiting ConfigPatch"
|
||||
)
|
||||
prior = prior_stack[-1]
|
||||
self._prior.set(prior_stack[:-1])
|
||||
for k, v in prior.items():
|
||||
config.__setattr__(k, v)
|
||||
|
||||
return ConfigPatch()
|
||||
|
||||
def _make_closure_patcher(self, **changes: dict[str, Any]) -> Any:
|
||||
"""
|
||||
A lower-overhead version of patch() for things on the critical path.
|
||||
|
||||
Usage:
|
||||
|
||||
# do this off the critical path
|
||||
change_fn = config.make_closure_patcher(foo=True)
|
||||
|
||||
...
|
||||
|
||||
revert = change_fn()
|
||||
try:
|
||||
...
|
||||
finally:
|
||||
revert()
|
||||
|
||||
"""
|
||||
config = self._config
|
||||
|
||||
def change() -> Callable[[], None]:
|
||||
prior = {k: config[k].user_override.get() for k in changes}
|
||||
for k, v in changes.items():
|
||||
self._config[k].user_override.set(v)
|
||||
|
||||
def revert() -> None:
|
||||
for k, v in prior.items():
|
||||
self._config[k].user_override.set(v)
|
||||
|
||||
return revert
|
||||
|
||||
return change
|
||||
|
||||
|
||||
class ContextDecorator(contextlib.ContextDecorator):
|
||||
"""
|
||||
Same as contextlib.ContextDecorator, but with support for
|
||||
`unittest.TestCase`
|
||||
"""
|
||||
|
||||
def __enter__(self) -> None:
|
||||
raise NotImplementedError("NYI")
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb) -> NoReturn: # type: ignore[no-untyped-def]
|
||||
raise NotImplementedError("NYI")
|
||||
|
||||
def __call__(self, func: Callable[[Any], Any]) -> Any:
|
||||
if isinstance(func, type) and issubclass(func, unittest.TestCase):
|
||||
|
||||
class _TestCase(func): # type: ignore[valid-type, misc]
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
self.__enter__()
|
||||
try:
|
||||
super().setUpClass()
|
||||
except Exception:
|
||||
self.__exit__(None, None, None)
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
try:
|
||||
super().tearDownClass()
|
||||
finally:
|
||||
self.__exit__(None, None, None)
|
||||
|
||||
_TestCase.__name__ = func.__name__
|
||||
_TestCase.__qualname__ = func.__qualname__
|
||||
_TestCase.__module__ = func.__module__
|
||||
|
||||
return _TestCase
|
||||
|
||||
return super().__call__(func)
|
||||
|
||||
|
||||
class SubConfigProxy:
|
||||
"""
|
||||
Shim to redirect to main config.
|
||||
`config.triton.cudagraphs` maps to _config["triton.cudagraphs"]
|
||||
"""
|
||||
|
||||
def __init__(self, config: object, prefix: str) -> None:
|
||||
# `super().__setattr__` to bypass custom `__setattr__`
|
||||
super().__setattr__("_config", config)
|
||||
super().__setattr__("_prefix", prefix)
|
||||
|
||||
def __setattr__(self, name: str, value: object) -> None:
|
||||
return self._config.__setattr__(self._prefix + name, value)
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
return self._config.__getattr__(self._prefix + name)
|
||||
|
||||
def __delattr__(self, name: str) -> None:
|
||||
return self._config.__delattr__(self._prefix + name)
|
||||
|
||||
|
||||
def get_tristate_env(name: str, default: Any = None) -> bool | None:
|
||||
value = os.environ.get(name)
|
||||
if value == "1":
|
||||
return True
|
||||
if value == "0":
|
||||
return False
|
||||
return default
|
||||
|
||||
|
||||
def inherit_fields_from(parent_cls):
|
||||
def wrapper(child_cls):
|
||||
for k, v in parent_cls.__dict__.items():
|
||||
# copy fields that are not private and not overridden
|
||||
if not k.startswith("_") and k not in child_cls.__dict__:
|
||||
setattr(child_cls, k, v)
|
||||
return child_cls
|
||||
|
||||
return wrapper
|
||||
@@ -0,0 +1,36 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
"""
|
||||
This was semi-automatically generated by running
|
||||
|
||||
stubgen torch.utils._config_module.py
|
||||
|
||||
And then manually extracting the methods of ConfigModule and converting them into top-level functions.
|
||||
|
||||
This file should be imported into any file that uses install_config_module like so:
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch.utils._config_typing import * # noqa: F401, F403
|
||||
|
||||
from torch.utils._config_module import install_config_module
|
||||
|
||||
# adds patch, save_config, etc
|
||||
install_config_module(sys.modules[__name__])
|
||||
|
||||
Note that the import should happen before the call to install_config_module(), otherwise runtime errors may occur.
|
||||
"""
|
||||
|
||||
if not TYPE_CHECKING: # noqa: PYI002
|
||||
raise AssertionError("Do not use at runtime") # noqa: W291
|
||||
|
||||
def save_config() -> bytes: ...
|
||||
def save_config_portable(*, ignore_private_configs: bool = True) -> dict[str, Any]: ...
|
||||
def codegen_config() -> str: ...
|
||||
def get_hash() -> bytes: ...
|
||||
def to_dict() -> dict[str, Any]: ...
|
||||
def shallow_copy_dict() -> dict[str, Any]: ...
|
||||
def load_config(config: bytes | dict[str, Any]) -> None: ...
|
||||
def get_config_copy() -> dict[str, Any]: ...
|
||||
def get_serializable_config_copy() -> dict[str, Any]: ...
|
||||
def patch(arg1: str | dict[str, Any] | None = None, arg2: Any = None, **kwargs): ...
|
||||
@@ -0,0 +1,241 @@
|
||||
# mypy: allow-untyped-defs
|
||||
# This module provides a FAST (on GPU) content addressable store for storages
|
||||
# (and tensors on top of them) with VERY WEAK portability guarantees (e.g.,
|
||||
# don't expect CPU/CUDA to address to the same hash, don't expect it to be
|
||||
# portable across devices) that is NOT cryptographically secure. In return,
|
||||
# we are able to hash 40G of tensor data on GPU in less than a second,
|
||||
# compared to running SHA-1 in CPU which would a minute or so. The primary
|
||||
# use case is for efficiently snapshotting intermediate tensor data for
|
||||
# offline debugging, but it's been put in this module in case you think of
|
||||
# another use case for it. The hash function could be replaced with a
|
||||
# straight reimplementation of SHA-1, which would give us much stronger
|
||||
# portability guarantees.
|
||||
#
|
||||
# WARNING: THERE IS NO BC/FC GUARANTEE FOR THIS FORMAT! If you need to format
|
||||
# shift the result, consider packing it into a single torch.save object
|
||||
# with traditional view sharing.
|
||||
#
|
||||
# Because of the weak portability guarantees, you can only write to the
|
||||
# content store from a single process; we don't provide any capability
|
||||
# of "reopening" a content store to add more things to it. But we don't
|
||||
# assume that you can keep all of the tensors you want to add to the store
|
||||
# in memory at once, because you probably can't! Nor do we assume that
|
||||
# you know a priori whether or not two storages can be deduplicated or not.
|
||||
#
|
||||
# Note: only storages are content-addressed; tensors are name addressed
|
||||
#
|
||||
# Note: our padding strategy means that [1, 0] and [1] int16 tensors would
|
||||
# map to the same (padded) storage. We think this will be immaterial for most
|
||||
# users.
|
||||
|
||||
import ctypes
|
||||
import functools
|
||||
import hashlib
|
||||
import os.path
|
||||
import struct
|
||||
from collections import defaultdict
|
||||
|
||||
import torch
|
||||
import torch._prims as prims
|
||||
import torch._utils
|
||||
import torch.nn.functional as F
|
||||
from torch.multiprocessing.reductions import StorageWeakRef
|
||||
|
||||
|
||||
def lazy_compile(**compile_kwargs):
|
||||
"""Lazily wrap a function with torch.compile on the first call
|
||||
|
||||
This avoids eagerly importing dynamo.
|
||||
"""
|
||||
|
||||
def decorate_fn(fn):
|
||||
@functools.wraps(fn)
|
||||
def compile_hook(*args, **kwargs):
|
||||
compiled_fn = torch.compile(fn, **compile_kwargs)
|
||||
globals()[fn.__name__] = functools.wraps(fn)(compiled_fn)
|
||||
return compiled_fn(*args, **kwargs)
|
||||
|
||||
return compile_hook
|
||||
|
||||
return decorate_fn
|
||||
|
||||
|
||||
# Use of torch.compile is mandatory for (1) good memory usage
|
||||
# and (2) xor_sum implementation. This is our first instance of
|
||||
# using PT2 to implement a kernel in PyTorch; if we get AOT capabilities
|
||||
# it would be good to apply it here.
|
||||
@lazy_compile(dynamic=True)
|
||||
def hash_storage_kernel(x):
|
||||
# The randint calls are carefully written to hit things we
|
||||
# have lowerings for in inductor. Lack of unsigned 32-bit integer
|
||||
# is a pain.
|
||||
a = torch.randint(
|
||||
-(2**31), 2**31, x.shape, device=x.device, dtype=torch.int32
|
||||
).abs()
|
||||
a = ((a % (2**31 - 1)) + 1).long()
|
||||
b = (
|
||||
torch.randint(-(2**31), 2**31, x.shape, device=x.device, dtype=torch.int32)
|
||||
.abs()
|
||||
.long()
|
||||
)
|
||||
# This is a standard shift-multiply universal hash family
|
||||
# plus xor sum hash, using Philox to generate random numbers.
|
||||
# Our Philox RNG is not deterministic across devices so
|
||||
# don't use this for stable hashing.
|
||||
#
|
||||
# This assumes fixed length so you're also obligated to bucket
|
||||
# by the length of tensor as well
|
||||
return prims.xor_sum((a * x + b).int(), [0])
|
||||
|
||||
|
||||
# Returns a hex digest of the data in the storage. Guaranteed to be
|
||||
# SHA-1 if stable_hash=True, otherwise it will consistent for a single
|
||||
# process run but not necessarily across processes.
|
||||
def hash_storage(storage: torch.UntypedStorage, *, stable_hash: bool = False) -> str:
|
||||
import torch._dynamo
|
||||
from torch._dynamo.utils import is_compile_supported
|
||||
|
||||
device_type = storage.device.type
|
||||
if stable_hash or not is_compile_supported(device_type):
|
||||
cpu_storage = storage.cpu()
|
||||
# TODO: make storage support buffer protocol so this isn't
|
||||
# necessary
|
||||
buf = (ctypes.c_byte * cpu_storage.nbytes()).from_address(
|
||||
cpu_storage.data_ptr()
|
||||
)
|
||||
sha1 = hashlib.sha1(usedforsecurity=False)
|
||||
sha1.update(buf)
|
||||
return sha1.hexdigest()
|
||||
|
||||
# TODO: factor this into a random utility
|
||||
if device_type == "cpu":
|
||||
generator = torch._C.default_generator
|
||||
elif device_type == "cuda":
|
||||
generator = torch.cuda.default_generators[storage.device.index]
|
||||
elif device_type == "mps":
|
||||
generator = torch.mps._get_default_mps_generator()
|
||||
elif device_type == "xpu":
|
||||
generator = torch.xpu.default_generators[storage.device.index]
|
||||
else:
|
||||
raise AssertionError(f"unhandled device type {device_type}")
|
||||
state = generator.get_state()
|
||||
try:
|
||||
generator.manual_seed(0)
|
||||
x = torch.empty(0, dtype=torch.uint8, device=storage.device).set_(storage) # type: ignore[call-overload]
|
||||
# The dtype-casting view cannot be compiled, and so the
|
||||
# padding/reshaping also needs to be done externally even
|
||||
# though it could be profitably fused
|
||||
pad = -x.numel() % 4
|
||||
if pad > 0:
|
||||
x = F.pad(x, (0, pad), "constant", 0)
|
||||
x = x.view(torch.int32)
|
||||
# We run the 32-bit hash five times with differing parameters to
|
||||
# reduce chance of collision
|
||||
ITER = 5
|
||||
cs = [hash_storage_kernel(x).item() for _ in range(ITER)]
|
||||
return struct.pack(">" + "i" * ITER, *cs).hex()
|
||||
finally:
|
||||
generator.set_state(state)
|
||||
|
||||
|
||||
class ContentStoreWriter:
|
||||
# Structure:
|
||||
# storages/
|
||||
# 00/
|
||||
# 0000..00
|
||||
# tensors/
|
||||
# name
|
||||
def __init__(self, loc: str, stable_hash: bool = False) -> None:
|
||||
self.loc: str = loc
|
||||
self.seen_storage_hashes: set[str] = set()
|
||||
self.stable_hash = stable_hash
|
||||
|
||||
# TODO: offer some sort of non-blocking API to speed things up
|
||||
def write_storage(self, storage: torch.UntypedStorage) -> str:
|
||||
h = hash_storage(storage, stable_hash=self.stable_hash)
|
||||
if h in self.seen_storage_hashes:
|
||||
return h
|
||||
# TODO: consider not using torch.save for this; we don't actually
|
||||
# need any metadata for the storage
|
||||
subfolder = os.path.join(self.loc, "storages")
|
||||
os.makedirs(subfolder, exist_ok=True)
|
||||
target = os.path.join(subfolder, h)
|
||||
if os.path.exists(target):
|
||||
return h
|
||||
torch.save(storage, target)
|
||||
self.seen_storage_hashes.add(h)
|
||||
return h
|
||||
|
||||
def compute_tensor_metadata(self, t: torch.Tensor, h=None):
|
||||
if h is None:
|
||||
h = hash_storage(t.untyped_storage(), stable_hash=self.stable_hash)
|
||||
return (
|
||||
t.dtype,
|
||||
h,
|
||||
t.storage_offset(),
|
||||
tuple(t.shape),
|
||||
t.stride(),
|
||||
torch._utils.get_tensor_metadata(t),
|
||||
)
|
||||
|
||||
def write_tensor(self, name: str, t: torch.Tensor) -> None:
|
||||
storage = t.untyped_storage()
|
||||
h = self.write_storage(storage)
|
||||
# TODO: Support more advanced snapshotting of requires_grad/grad/etc
|
||||
d, f = os.path.split(name)
|
||||
payload = self.compute_tensor_metadata(t, h=h)
|
||||
subfolder = os.path.join(self.loc, "tensors", d)
|
||||
os.makedirs(subfolder, exist_ok=True)
|
||||
torch.save(payload, os.path.join(subfolder, f))
|
||||
|
||||
|
||||
class ContentStoreReader:
|
||||
def __init__(self, loc: str, *, cache=True) -> None:
|
||||
self.loc = loc
|
||||
self.storage_cache: (
|
||||
dict[torch.device | None, dict[str, StorageWeakRef]] | None
|
||||
) = None
|
||||
if cache:
|
||||
self.storage_cache = defaultdict(dict)
|
||||
|
||||
def read_storage(self, h: str, *, device=None) -> torch.UntypedStorage:
|
||||
if device is not None:
|
||||
device = torch.device(device)
|
||||
ws = (
|
||||
self.storage_cache[device].get(h)
|
||||
if self.storage_cache is not None
|
||||
else None
|
||||
)
|
||||
s: torch.UntypedStorage | None
|
||||
if ws is not None:
|
||||
s = torch.UntypedStorage._new_with_weak_ptr(ws.cdata)
|
||||
if s is not None:
|
||||
return s
|
||||
s = torch.load(
|
||||
os.path.join(self.loc, "storages", h),
|
||||
weights_only=True,
|
||||
map_location=device,
|
||||
)._untyped_storage
|
||||
if s is None:
|
||||
raise AssertionError(
|
||||
f"expected storage for hash {h} in {os.path.join(self.loc, 'storages')}, got None"
|
||||
)
|
||||
if self.storage_cache is not None:
|
||||
self.storage_cache[device][h] = StorageWeakRef(s)
|
||||
return s
|
||||
|
||||
def read_tensor_metadata(self, name: str):
|
||||
fn = os.path.join(self.loc, "tensors", name)
|
||||
if not os.path.exists(fn):
|
||||
raise FileNotFoundError(fn)
|
||||
return torch.load(fn, weights_only=True)
|
||||
|
||||
def read_tensor(self, name: str, *, device=None) -> torch.Tensor:
|
||||
dtype, h, storage_offset, size, stride, metadata = self.read_tensor_metadata(
|
||||
name
|
||||
)
|
||||
storage = self.read_storage(h, device=device)
|
||||
t = torch.tensor([], dtype=dtype, device=storage.device)
|
||||
t.set_(storage, storage_offset, size, stride)
|
||||
torch._utils.set_tensor_metadata(t, metadata)
|
||||
return t
|
||||
@@ -0,0 +1,170 @@
|
||||
# mypy: allow-untyped-defs
|
||||
# Extra utilities for working with context managers that should have been
|
||||
# in the standard library but are not
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import sys
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
from typing import Any, cast, overload, TypeVar
|
||||
from typing_extensions import Self
|
||||
|
||||
|
||||
# Used for annotating the decorator usage of _DecoratorContextManager (e.g.,
|
||||
# 'no_grad' and 'enable_grad').
|
||||
# See https://mypy.readthedocs.io/en/latest/generics.html#declaring-decorators
|
||||
FuncType = Callable[..., Any]
|
||||
F = TypeVar("F", bound=FuncType)
|
||||
|
||||
|
||||
def _wrap_generator(ctx_factory, func):
|
||||
"""
|
||||
Wrap each generator invocation with the context manager factory.
|
||||
|
||||
The input should be a function that returns a context manager,
|
||||
not a context manager itself, to handle one-shot context managers.
|
||||
"""
|
||||
|
||||
@functools.wraps(func)
|
||||
def generator_context(*args, **kwargs):
|
||||
gen = func(*args, **kwargs)
|
||||
|
||||
# Generators are suspended and unsuspended at `yield`, hence we
|
||||
# make sure the grad mode is properly set every time the execution
|
||||
# flow returns into the wrapped generator and restored when it
|
||||
# returns through our `yield` to our caller (see PR #49017).
|
||||
try:
|
||||
# Issuing `None` to a generator fires it up
|
||||
with ctx_factory():
|
||||
response = gen.send(None)
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Forward the response to our caller and get its next request
|
||||
request = yield response
|
||||
|
||||
except GeneratorExit:
|
||||
# Inform the still active generator about its imminent closure
|
||||
with ctx_factory():
|
||||
gen.close()
|
||||
raise
|
||||
|
||||
except BaseException: # noqa: B036
|
||||
# Propagate the exception thrown at us by the caller
|
||||
with ctx_factory():
|
||||
response = gen.throw(*sys.exc_info())
|
||||
|
||||
else:
|
||||
# Pass the last request to the generator and get its response
|
||||
with ctx_factory():
|
||||
response = gen.send(request)
|
||||
|
||||
# We let the exceptions raised above by the generator's `.throw` or
|
||||
# `.send` methods bubble up to our caller, except for StopIteration
|
||||
except StopIteration as e:
|
||||
# The generator informed us that it is done: take whatever its
|
||||
# returned value (if any) was and indicate that we're done too
|
||||
# by returning it (see docs for python's return-statement).
|
||||
return e.value
|
||||
|
||||
return generator_context
|
||||
|
||||
|
||||
def context_decorator(ctx, func):
|
||||
"""
|
||||
Like contextlib.ContextDecorator.
|
||||
|
||||
But with the following differences:
|
||||
1. Is done by wrapping, rather than inheritance, so it works with context
|
||||
managers that are implemented from C and thus cannot easily inherit from
|
||||
Python classes
|
||||
2. Wraps generators in the intuitive way (c.f. https://bugs.python.org/issue37743)
|
||||
3. Errors out if you try to wrap a class, because it is ambiguous whether
|
||||
or not you intended to wrap only the constructor
|
||||
|
||||
The input argument can either be a context manager (in which case it must
|
||||
be a multi-shot context manager that can be directly invoked multiple times)
|
||||
or a callable that produces a context manager.
|
||||
"""
|
||||
if callable(ctx) and hasattr(ctx, "__enter__"):
|
||||
raise AssertionError(
|
||||
f"Passed in {ctx} is both callable and also a valid context manager "
|
||||
"(has __enter__), making it ambiguous which interface to use. If you "
|
||||
"intended to pass a context manager factory, rewrite your call as "
|
||||
"context_decorator(lambda: ctx()); if you intended to pass a context "
|
||||
"manager directly, rewrite your call as context_decorator(lambda: ctx)"
|
||||
)
|
||||
|
||||
if not callable(ctx):
|
||||
|
||||
def ctx_factory():
|
||||
return ctx
|
||||
|
||||
else:
|
||||
ctx_factory = ctx
|
||||
|
||||
if inspect.isclass(func):
|
||||
raise RuntimeError(
|
||||
"Cannot decorate classes; it is ambiguous whether or not only the "
|
||||
"constructor or all methods should have the context manager applied; "
|
||||
"additionally, decorating a class at definition-site will prevent "
|
||||
"use of the identifier as a conventional type. "
|
||||
"To specify which methods to decorate, decorate each of them "
|
||||
"individually."
|
||||
)
|
||||
|
||||
if inspect.isgeneratorfunction(func):
|
||||
return _wrap_generator(ctx_factory, func)
|
||||
|
||||
@functools.wraps(func)
|
||||
def decorate_context(*args, **kwargs):
|
||||
# pyrefly: ignore [bad-context-manager]
|
||||
with ctx_factory():
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return decorate_context
|
||||
|
||||
|
||||
class _DecoratorContextManager:
|
||||
"""Allow a context manager to be used as a decorator."""
|
||||
|
||||
def __call__(self, orig_func: F) -> F:
|
||||
if inspect.isclass(orig_func):
|
||||
warnings.warn(
|
||||
"Decorating classes is deprecated and will be disabled in "
|
||||
"future versions. You should only decorate functions or methods. "
|
||||
"To preserve the current behavior of class decoration, you can "
|
||||
"directly decorate the `__init__` method and nothing else.",
|
||||
FutureWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
func = cast(F, lambda *args, **kwargs: orig_func(*args, **kwargs))
|
||||
else:
|
||||
func = orig_func
|
||||
|
||||
return cast(F, context_decorator(self.clone, func))
|
||||
|
||||
def __enter__(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def clone(self):
|
||||
# override this method if your children class takes __init__ parameters
|
||||
return self.__class__()
|
||||
|
||||
|
||||
class _NoParamDecoratorContextManager(_DecoratorContextManager):
|
||||
"""Allow a context manager to be used as a decorator without parentheses."""
|
||||
|
||||
@overload
|
||||
def __new__(cls, orig_func: F) -> F: ... # type: ignore[misc]
|
||||
@overload
|
||||
def __new__(cls, orig_func: None = None) -> Self: ...
|
||||
|
||||
def __new__(cls, orig_func: F | None = None) -> Self | F: # type: ignore[misc]
|
||||
if orig_func is None:
|
||||
return super().__new__(cls)
|
||||
return cls()(orig_func)
|
||||
@@ -0,0 +1,57 @@
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
from re import match as _match
|
||||
|
||||
|
||||
def read_file(fname: Path | str) -> list[str]:
|
||||
with open(fname, encoding="utf-8") as f:
|
||||
return f.readlines()
|
||||
|
||||
|
||||
def _embed_headers(
|
||||
content: list[str], include_dirs: list[Path], processed_files: set[str]
|
||||
) -> str:
|
||||
for line_idx, cur_line in enumerate(content):
|
||||
# Eliminate warning: `#pragma once in main file`
|
||||
if cur_line.startswith("#pragma once"):
|
||||
content[line_idx] = ""
|
||||
continue
|
||||
m = _match('^\\s*#include\\s*[<"]([^>"]+)[>"]', cur_line)
|
||||
if m is None:
|
||||
continue
|
||||
for include_dir in include_dirs:
|
||||
path = include_dir / m[1]
|
||||
if not path.exists():
|
||||
continue
|
||||
if str(path) in processed_files:
|
||||
content[line_idx] = ""
|
||||
continue
|
||||
processed_files.add(str(path))
|
||||
content[line_idx] = _embed_headers(
|
||||
read_file(path), include_dirs, processed_files
|
||||
)
|
||||
break
|
||||
return "".join(content)
|
||||
|
||||
|
||||
def embed_headers(
|
||||
fname: str, include_dirs: Sequence[str] | Sequence[Path] | str | None = None
|
||||
) -> str:
|
||||
if include_dirs is None:
|
||||
base_dir = Path(__file__).parent.parent.parent
|
||||
include_dirs = [base_dir, base_dir / "aten" / "src"]
|
||||
elif isinstance(include_dirs, str):
|
||||
include_dirs = [Path(include_dirs)]
|
||||
else:
|
||||
include_dirs = [Path(x) for x in include_dirs]
|
||||
|
||||
return _embed_headers(read_file(fname), include_dirs, {fname})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print(f"Usage:\n {sys.argv[0]} filename")
|
||||
sys.exit(1)
|
||||
print(embed_headers(sys.argv[1]))
|
||||
@@ -0,0 +1,63 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import collections
|
||||
|
||||
|
||||
Entry = collections.namedtuple("Entry", "version, hash")
|
||||
|
||||
|
||||
def update_hash(seed, value):
|
||||
# Good old boost::hash_combine
|
||||
# https://www.boost.org/doc/libs/1_35_0/doc/html/boost/hash_combine_id241013.html
|
||||
return seed ^ (hash(value) + 0x9E3779B9 + (seed << 6) + (seed >> 2))
|
||||
|
||||
|
||||
def hash_source_files(hash_value, source_files):
|
||||
for filename in source_files:
|
||||
with open(filename, "rb") as file:
|
||||
hash_value = update_hash(hash_value, file.read())
|
||||
return hash_value
|
||||
|
||||
|
||||
def hash_build_arguments(hash_value, build_arguments):
|
||||
for group in build_arguments:
|
||||
if group:
|
||||
for argument in group:
|
||||
hash_value = update_hash(hash_value, argument)
|
||||
return hash_value
|
||||
|
||||
|
||||
class ExtensionVersioner:
|
||||
def __init__(self) -> None:
|
||||
self.entries = {}
|
||||
|
||||
def get_version(self, name):
|
||||
entry = self.entries.get(name)
|
||||
return None if entry is None else entry.version
|
||||
|
||||
def bump_version_if_changed(
|
||||
self,
|
||||
name,
|
||||
source_files,
|
||||
build_arguments,
|
||||
build_directory,
|
||||
with_cuda,
|
||||
with_sycl,
|
||||
is_python_module,
|
||||
is_standalone,
|
||||
):
|
||||
hash_value = 0
|
||||
hash_value = hash_source_files(hash_value, source_files)
|
||||
hash_value = hash_build_arguments(hash_value, build_arguments)
|
||||
hash_value = update_hash(hash_value, build_directory)
|
||||
hash_value = update_hash(hash_value, with_cuda)
|
||||
hash_value = update_hash(hash_value, with_sycl)
|
||||
hash_value = update_hash(hash_value, is_python_module)
|
||||
hash_value = update_hash(hash_value, is_standalone)
|
||||
|
||||
entry = self.entries.get(name)
|
||||
if entry is None:
|
||||
self.entries[name] = entry = Entry(0, hash_value)
|
||||
elif hash_value != entry.hash:
|
||||
self.entries[name] = entry = Entry(entry.version + 1, hash_value)
|
||||
|
||||
return entry.version
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""
|
||||
DebugMode: a debugging TorchDispatchMode that intercepts and logs runtime calls.
|
||||
|
||||
See torch.utils._debug_mode._mode for the full implementation and docstring.
|
||||
"""
|
||||
|
||||
from torch.utils._debug_mode._calls import (
|
||||
_AnnotateCall,
|
||||
_DebugCall,
|
||||
_get_call_name,
|
||||
_OpCall,
|
||||
_OutputPlacementCall,
|
||||
_RedistributeCall,
|
||||
_TritonKernelCall,
|
||||
)
|
||||
from torch.utils._debug_mode._mode import (
|
||||
DebugInterpreter,
|
||||
DebugMode,
|
||||
get_active_debug_mode,
|
||||
)
|
||||
from torch.utils._debug_mode._utils import (
|
||||
_stringify_shape,
|
||||
hash_tensor_fn,
|
||||
norm_hash_fn,
|
||||
TensorIdTracker,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["DebugMode", "get_active_debug_mode"]
|
||||
@@ -0,0 +1,354 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""
|
||||
Call record classes for DebugMode: _DebugCall and its subclasses.
|
||||
"""
|
||||
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
# Import _utils module for mutable globals
|
||||
from torch.utils._debug_mode import _utils
|
||||
from torch.utils._debug_mode._utils import (
|
||||
_arg_to_str,
|
||||
_get_op_name,
|
||||
_get_stack_trace,
|
||||
_maybe_get_autograd_trace,
|
||||
REDISTRIBUTE_FUNC,
|
||||
TensorIdTracker,
|
||||
)
|
||||
from torch.utils._pytree import tree_all, tree_map
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch._dynamo.device_interface import DeviceInterface
|
||||
|
||||
|
||||
class _DebugCall:
|
||||
"""Base class for tracking operator calls in DebugMode"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
call_depth: int,
|
||||
record: dict[str, Any] | None = None,
|
||||
log: dict[str, Any] | None = None,
|
||||
stack: bool = False,
|
||||
) -> None:
|
||||
self.call_depth = call_depth
|
||||
if stack:
|
||||
self.stack_trace = _get_stack_trace()
|
||||
self.fwd_stack_trace = _maybe_get_autograd_trace()
|
||||
|
||||
# results from dispatch hooks
|
||||
self.record = record
|
||||
self.log = log
|
||||
self.output_str: str | None = None
|
||||
|
||||
def stringify_args(
|
||||
self, attributes: list[str], tensor_memo: TensorIdTracker | None = None
|
||||
) -> None:
|
||||
"""
|
||||
To reduce memory consumption, this method stringifies args/kwargs, stores the result, and deletes original args/kwargs.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Subclasses must implement stringify_args(), even if no-op"
|
||||
)
|
||||
|
||||
def stringify_output(
|
||||
self,
|
||||
output: Any,
|
||||
attributes: list[str],
|
||||
tensor_memo: TensorIdTracker | None = None,
|
||||
) -> None:
|
||||
"""Store stringified version of call output in self.output_str"""
|
||||
if tree_all(lambda x: x is None, output):
|
||||
return
|
||||
output_str = tree_map(lambda x: _arg_to_str(x, attributes, tensor_memo), output)
|
||||
self.output_str = f" -> {str(output_str)}"
|
||||
|
||||
def render(self, attributes: list[str]) -> str:
|
||||
raise NotImplementedError("Subclasses must implement string render()")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.render([])
|
||||
|
||||
|
||||
class _OpCall(_DebugCall):
|
||||
"""Normal operator call"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
op,
|
||||
args: tuple,
|
||||
kwargs: dict,
|
||||
call_depth: int,
|
||||
stack: bool = False,
|
||||
) -> None:
|
||||
super().__init__(call_depth, stack=stack)
|
||||
self.op = op
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
self.args_str: str | None = None
|
||||
self.kwargs_str: str | None = None
|
||||
|
||||
def stringify_args(
|
||||
self, attributes: list[str], tensor_memo: TensorIdTracker | None = None
|
||||
) -> None:
|
||||
self.args_str = ", ".join(
|
||||
_arg_to_str(arg, attributes, tensor_memo) for arg in self.args
|
||||
)
|
||||
if self.kwargs:
|
||||
self.kwargs_str = ", " + ", ".join(
|
||||
f"{k}={_arg_to_str(v, attributes, tensor_memo)}"
|
||||
for k, v in self.kwargs.items()
|
||||
)
|
||||
else:
|
||||
self.kwargs_str = ""
|
||||
del self.args
|
||||
del self.kwargs
|
||||
|
||||
def render(self, attributes: list[str]) -> str:
|
||||
if self.args_str is not None:
|
||||
args_str = self.args_str
|
||||
else:
|
||||
args_str = ", ".join(_arg_to_str(arg, attributes) for arg in self.args)
|
||||
|
||||
if self.kwargs_str is not None:
|
||||
kwargs_str = self.kwargs_str
|
||||
else:
|
||||
if self.kwargs:
|
||||
kwargs_str = ", " + ", ".join(
|
||||
f"{k}={_arg_to_str(v, attributes)}" for k, v in self.kwargs.items()
|
||||
)
|
||||
else:
|
||||
kwargs_str = ""
|
||||
|
||||
if isinstance(self.op, torch._ops.OpOverload):
|
||||
op_name = self.op.__qualname__
|
||||
elif hasattr(self.op, "__module__") and hasattr(self.op, "__name__"):
|
||||
op_name = f"{self.op.__module__}.{self.op.__name__}"
|
||||
else:
|
||||
op_name = str(self.op)
|
||||
|
||||
base_str = f"{op_name}({args_str}{kwargs_str})"
|
||||
|
||||
if self.output_str:
|
||||
base_str += self.output_str
|
||||
if self.log:
|
||||
base_str += f" # {self.log}"
|
||||
return base_str
|
||||
|
||||
def __iter__(self):
|
||||
# for BC; tuple(self) returns (op, args, kwargs, call_depth)
|
||||
if self.args_str is not None:
|
||||
yield from [self.op, self.args_str, self.kwargs_str, self.call_depth]
|
||||
else:
|
||||
yield from [self.op, self.args, self.kwargs, self.call_depth]
|
||||
|
||||
|
||||
class _RedistributeCall(_DebugCall):
|
||||
def __init__(
|
||||
self,
|
||||
arg,
|
||||
src_placement,
|
||||
dst_placement,
|
||||
transform_info_str,
|
||||
call_depth,
|
||||
stack=False,
|
||||
is_explicit=False,
|
||||
) -> None:
|
||||
super().__init__(call_depth, stack=stack)
|
||||
self.arg = arg
|
||||
self.src_placement = src_placement
|
||||
self.dst_placement = dst_placement
|
||||
self.transform_info_str = transform_info_str
|
||||
self.is_explicit = is_explicit
|
||||
self.is_outer_call = isinstance(arg, int)
|
||||
|
||||
self.arg_str: str | None = None
|
||||
|
||||
def stringify_args(
|
||||
self, attributes: list[str], tensor_memo: TensorIdTracker | None = None
|
||||
) -> None:
|
||||
self.arg_str = f"{_arg_to_str(self.arg, attributes, tensor_memo)}"
|
||||
del self.arg
|
||||
|
||||
def render(self, attributes: list[str]) -> str:
|
||||
if self.arg_str is not None:
|
||||
arg_str = self.arg_str
|
||||
else:
|
||||
arg_str = f"{_arg_to_str(self.arg, attributes)}"
|
||||
|
||||
if self.transform_info_str is not None: # prioritize over src/dst placements
|
||||
placement_str = f"trace: {self.transform_info_str}"
|
||||
else:
|
||||
src_placement_str = _arg_to_str(self.src_placement, attributes)
|
||||
dst_placement_str = _arg_to_str(self.dst_placement, attributes)
|
||||
placement_str = f"{src_placement_str} -> {dst_placement_str}"
|
||||
|
||||
# DebugMode will add redistribute_input logs at 2 levels,
|
||||
# once per redistribute decision, and once per redistributed input.
|
||||
# We only annotate [implicit/explicit] logs on the former (outer-level call).
|
||||
if self.is_outer_call:
|
||||
annotation = " [implicit] "
|
||||
elif self.is_explicit:
|
||||
annotation = " [explicit] "
|
||||
else:
|
||||
annotation = ""
|
||||
|
||||
base_str = f"{REDISTRIBUTE_FUNC}{annotation}({arg_str}, {placement_str})"
|
||||
if self.output_str:
|
||||
base_str += self.output_str
|
||||
return base_str
|
||||
|
||||
def __iter__(self):
|
||||
# for BC; tuple(self) returns (op, placement info, kwargs, call_depth)
|
||||
if self.arg_str is not None:
|
||||
arg = self.arg_str
|
||||
else:
|
||||
arg = self.arg
|
||||
|
||||
yield REDISTRIBUTE_FUNC
|
||||
if self.transform_info_str:
|
||||
yield [arg, self.transform_info_str]
|
||||
else:
|
||||
yield [arg, self.src_placement, self.dst_placement]
|
||||
yield {}
|
||||
yield self.call_depth
|
||||
|
||||
|
||||
class _OutputPlacementCall(_DebugCall):
|
||||
"""Records output placement for a DTensor op."""
|
||||
|
||||
def __init__(self, placements_str: str, call_depth: int) -> None:
|
||||
super().__init__(call_depth)
|
||||
self.placements_str = placements_str
|
||||
|
||||
def stringify_args(
|
||||
self, attributes: list[str], tensor_memo: TensorIdTracker | None = None
|
||||
) -> None:
|
||||
pass # Already stringified
|
||||
|
||||
def render(self, attributes: list[str]) -> str:
|
||||
return f"-> output: {self.placements_str}"
|
||||
|
||||
|
||||
class _TritonKernelCall(_DebugCall):
|
||||
"""Triton kernel call from Inductor"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
kernel_name: str,
|
||||
kwargs: dict[str, Any],
|
||||
call_depth: int,
|
||||
):
|
||||
super().__init__(call_depth)
|
||||
self.kernel_name = kernel_name
|
||||
self.kwargs = kwargs
|
||||
self.kwargs_str: str | None = None
|
||||
|
||||
self.pre_hashes: dict[str, Any] | None = None
|
||||
self.post_hashes: dict[str, Any] | None = None
|
||||
|
||||
def stringify_args(
|
||||
self, attributes: list[str], tensor_memo: TensorIdTracker | None = None
|
||||
) -> None:
|
||||
# Optionally hash kernel inputs before launch
|
||||
if hash_fn := _utils._TRITON_INPUT_HASH_FN:
|
||||
self.pre_hashes = {
|
||||
k: hash_fn(v)
|
||||
for k, v in self.kwargs.items()
|
||||
if isinstance(v, torch.Tensor)
|
||||
}
|
||||
|
||||
if self.kwargs:
|
||||
self.kwargs_str = ", ".join(
|
||||
f"{k}={_arg_to_str(v, attributes, tensor_memo)}"
|
||||
for k, v in self.kwargs.items()
|
||||
)
|
||||
else:
|
||||
self.kwargs_str = ""
|
||||
|
||||
def render(self, attributes: list[str]) -> str:
|
||||
base_str = f"[triton] {self.kernel_name}({self.kwargs_str})"
|
||||
if self.pre_hashes:
|
||||
pre_hashes_str = ", ".join(f"{k}: {v}" for k, v in self.pre_hashes.items())
|
||||
pre_hashes_str = (
|
||||
"\n "
|
||||
+ " " * self.call_depth
|
||||
+ f"# pre-kernel hashes: {{{pre_hashes_str}}}"
|
||||
)
|
||||
else:
|
||||
pre_hashes_str = ""
|
||||
if self.post_hashes:
|
||||
post_hashes_str = ", ".join(
|
||||
f"{k}: {v}" for k, v in self.post_hashes.items()
|
||||
)
|
||||
post_hashes_str = (
|
||||
"\n "
|
||||
+ " " * self.call_depth
|
||||
+ f"# post-kernel hashes: {{{post_hashes_str}}}"
|
||||
)
|
||||
else:
|
||||
post_hashes_str = ""
|
||||
return f"{base_str}{pre_hashes_str}{post_hashes_str}\n"
|
||||
|
||||
def finalize(self, device_interface: "DeviceInterface"):
|
||||
# synchronize -> hash/store kernel results
|
||||
device_interface.synchronize(device_interface.current_device())
|
||||
if _utils._RECORD_TRITON_OUTPUTS:
|
||||
self.record = {
|
||||
"output": {
|
||||
k: v.clone() if isinstance(v, torch.Tensor) else v
|
||||
for k, v in self.kwargs.items()
|
||||
}
|
||||
}
|
||||
if hash_fn := _utils._TRITON_OUTPUT_HASH_FN:
|
||||
self.post_hashes = {
|
||||
k: hash_fn(v)
|
||||
for k, v in self.kwargs.items()
|
||||
if isinstance(v, torch.Tensor)
|
||||
}
|
||||
|
||||
# don't store tensors
|
||||
del self.kwargs
|
||||
|
||||
def __iter__(self):
|
||||
yield from [self.kernel_name, (), self.kwargs_str, self.call_depth]
|
||||
|
||||
|
||||
class _AnnotateCall(_DebugCall):
|
||||
"""Custom annotation call"""
|
||||
|
||||
def __init__(
|
||||
self, tag: Any, header: str, call_depth: int, stack: bool = False
|
||||
) -> None:
|
||||
super().__init__(call_depth, stack=stack)
|
||||
self.tag = tag
|
||||
self.header = header
|
||||
|
||||
def render(self, attributes: list[str]) -> str:
|
||||
return f"[{self.header}] {self.tag}"
|
||||
|
||||
def __iter__(self):
|
||||
yield from [
|
||||
f"[{self.header}] {self.tag}",
|
||||
(),
|
||||
{},
|
||||
self.call_depth,
|
||||
]
|
||||
|
||||
|
||||
def _get_call_name(call: _DebugCall) -> str:
|
||||
"""String identifying _DebugCall (e.g. func, kernel, module name)"""
|
||||
if isinstance(call, _OpCall):
|
||||
return _get_op_name(call.op)
|
||||
elif isinstance(call, _TritonKernelCall):
|
||||
return call.kernel_name
|
||||
elif isinstance(call, _AnnotateCall):
|
||||
return f"[{call.header}] {call.tag}"
|
||||
elif isinstance(call, _RedistributeCall):
|
||||
return REDISTRIBUTE_FUNC
|
||||
else:
|
||||
return str(call)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,251 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""
|
||||
Utility functions for DebugMode: tensor formatting, hashing, stack traces, and hook runners.
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import os
|
||||
import traceback
|
||||
import weakref
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
from torch._subclasses.fake_tensor import FakeTensor
|
||||
from torch.fx.graph import _parse_stack_trace
|
||||
from torch.utils._dtype_abbrs import dtype_abbrs
|
||||
from torch.utils._pytree import tree_map
|
||||
from torch.utils._traceback import CapturedTraceback
|
||||
from torch.utils.weak import WeakIdRef
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch.utils._debug_mode._calls import _DebugCall
|
||||
|
||||
|
||||
REDISTRIBUTE_FUNC = "redistribute_input"
|
||||
|
||||
# Tracks if we're in inductor benchmarking, and temporarily disables logging
|
||||
# (for ignoring autotuning kernel launches which don't affect the user-facing result)
|
||||
_IN_INDUCTOR_BENCHMARK: bool = False
|
||||
# For record_outputs, log_tensor_hashes hooks for triton kernels.
|
||||
# Stores kernel outputs in call.record["output"]
|
||||
_RECORD_TRITON_OUTPUTS: bool = False
|
||||
# Annotates kernel output hashes, and stores them in call.post_hashes
|
||||
_TRITON_OUTPUT_HASH_FN: Callable | None = None
|
||||
# Annotates kernel input hashes, and stores them in call.pre_hashes
|
||||
_TRITON_INPUT_HASH_FN: Callable | None = None
|
||||
|
||||
# registered dispatch call hooks
|
||||
_DISPATCH_RECORD_HOOKS: list[Callable] = []
|
||||
_DISPATCH_LOG_HOOKS: list[Callable] = []
|
||||
_DISPATCH_PRE_LOG_HOOKS: list[Callable] = []
|
||||
|
||||
|
||||
def _stringify_shape(shape) -> str:
|
||||
return f"[{', '.join([str(x) for x in shape])}]"
|
||||
|
||||
|
||||
def _stringify_device_mesh(mesh) -> str:
|
||||
return f"DM({', '.join([str(s) for s in mesh.shape])})"
|
||||
|
||||
|
||||
def _stringify_placement(placement) -> str:
|
||||
return f"[{', '.join([str(p) for p in placement])}]"
|
||||
|
||||
|
||||
def _stringify_attributes(tensor, attributes) -> str:
|
||||
pairs = {}
|
||||
for attr in attributes:
|
||||
if hasattr(tensor, attr):
|
||||
pairs[attr] = getattr(tensor, attr)
|
||||
if len(pairs) == 0:
|
||||
return ""
|
||||
return f"{{{', '.join([f'{k}={v}' for k, v in pairs.items()])}}}"
|
||||
|
||||
|
||||
def _stringify_dtensor_spec(spec) -> str:
|
||||
from torch.distributed.tensor._dtensor_spec import DTensorSpec
|
||||
|
||||
return DTensorSpec.format_shard_order_str(spec.placements, spec.shard_order)
|
||||
|
||||
|
||||
class TensorIdTracker:
|
||||
def __init__(self) -> None:
|
||||
self.tensor_memo: dict[WeakIdRef, int] = {}
|
||||
self.next_tensor_id = 0
|
||||
|
||||
def _id(self, tensor) -> int:
|
||||
with torch._C._DisablePythonDispatcher():
|
||||
o = WeakIdRef(tensor)
|
||||
|
||||
def del_memo() -> None:
|
||||
self.tensor_memo.pop(o, None)
|
||||
|
||||
weakref.finalize(tensor, del_memo)
|
||||
if o not in self.tensor_memo:
|
||||
self.tensor_memo[o] = self.next_tensor_id
|
||||
self.next_tensor_id += 1
|
||||
return self.tensor_memo[o]
|
||||
|
||||
|
||||
def _tensor_debug_string(tensor, attributes, tensor_memo=None) -> str:
|
||||
"""Convert tensor to debug string representation."""
|
||||
|
||||
if isinstance(tensor, torch.Tensor):
|
||||
tensor_debug_str = f"{dtype_abbrs[tensor.dtype]}{_stringify_shape(tensor.shape)}{_stringify_attributes(tensor, attributes)}"
|
||||
id_str = f"${tensor_memo._id(tensor)}" if tensor_memo is not None else ""
|
||||
if isinstance(tensor, torch.distributed.tensor.DTensor):
|
||||
# omitted device mesh
|
||||
return f"dt{id_str}: {tensor_debug_str}| {_stringify_dtensor_spec(tensor._spec)}"
|
||||
elif isinstance(tensor, FakeTensor):
|
||||
return f"ft{id_str}: {tensor_debug_str}"
|
||||
else:
|
||||
return f"t{id_str}: {tensor_debug_str}"
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported tensor type: {type(tensor)}")
|
||||
|
||||
|
||||
def _arg_to_str(arg, attributes, tensor_memo=None) -> str:
|
||||
from torch.distributed.tensor._dtensor_spec import DTensorSpec
|
||||
|
||||
def to_str(x):
|
||||
if isinstance(x, torch.Tensor):
|
||||
return _tensor_debug_string(x, attributes, tensor_memo)
|
||||
elif isinstance(x, DTensorSpec):
|
||||
return _stringify_dtensor_spec(x)
|
||||
return x
|
||||
|
||||
arg = tree_map(to_str, arg)
|
||||
return str(arg)
|
||||
|
||||
|
||||
def norm_hash_fn(t: torch.Tensor, use_scalar: bool = False) -> torch.Tensor | float:
|
||||
"""
|
||||
from Observer. Computes a hash for a tensor by converting it to float (if needed), making it contiguous,
|
||||
replacing NaN/inf values with fixed numbers, and then computing the L1 norm in float64 or complex128.
|
||||
This is used to generate a deterministic summary value for tensor comparison.
|
||||
"""
|
||||
with torch._C._DisablePythonDispatcher():
|
||||
if not (t.is_floating_point() or t.is_complex()):
|
||||
t = t.float()
|
||||
t = t.contiguous()
|
||||
|
||||
if t.is_complex():
|
||||
t_float = t.to(dtype=torch.complex128)
|
||||
else:
|
||||
t_float = t.to(dtype=torch.float64)
|
||||
|
||||
out = t_float.norm(p=1)
|
||||
if use_scalar:
|
||||
return out.item()
|
||||
return out
|
||||
|
||||
|
||||
def _compute_rel_diff(hash1, hash2):
|
||||
# Relative difference: |hash1 - hash2| / max(|hash1|, |hash2|, eps)
|
||||
numerator = abs(hash1 - hash2)
|
||||
denominator = max(abs(hash1), abs(hash2), 1e-10)
|
||||
return numerator / denominator
|
||||
|
||||
|
||||
def hash_tensor_fn(t: torch.Tensor, use_scalar: bool = False) -> torch.Tensor | int:
|
||||
"""
|
||||
wrapper over torch.hash_tensor
|
||||
"""
|
||||
if isinstance(t, torch.distributed.tensor.DTensor):
|
||||
t = t.to_local()
|
||||
|
||||
if t.is_floating_point():
|
||||
t_clean = t.to(dtype=torch.float64)
|
||||
elif t.is_complex():
|
||||
t_clean = t.to(dtype=torch.complex128).view(torch.float64)
|
||||
else:
|
||||
t_clean = t.to(dtype=torch.int64)
|
||||
|
||||
if t.numel() > 0:
|
||||
out = torch.hash_tensor(t_clean)
|
||||
else:
|
||||
out = torch.zeros((), device=t_clean.device, dtype=torch.uint64)
|
||||
|
||||
if use_scalar:
|
||||
return out.item() # type: ignore[attribute]
|
||||
return out
|
||||
|
||||
|
||||
def _get_stack_trace() -> str:
|
||||
from torch.fx.experimental.symbolic_shapes import uninteresting_files
|
||||
|
||||
summary = CapturedTraceback.extract().summary()
|
||||
summary = summary[:-4] # filter out DebugMode frames
|
||||
summary = [
|
||||
frame for frame in summary if frame.filename not in uninteresting_files()
|
||||
]
|
||||
summary = traceback.StackSummary.from_list(summary)
|
||||
return "".join(summary.format())
|
||||
|
||||
|
||||
def _get_user_stack_trace(stack_trace_str: str) -> str | None:
|
||||
# Extract user code stack trace, filtering out torch internals.
|
||||
torch_dir = os.path.dirname(inspect.getfile(torch))
|
||||
filter_fn = lambda file, name, code: not file.startswith(torch_dir + os.path.sep) # noqa: E731
|
||||
trace = _parse_stack_trace(stack_trace_str, filter_fn=filter_fn)
|
||||
if trace:
|
||||
return f"File: {trace.file}:{trace.lineno} in {trace.name}, code: {trace.code}"
|
||||
return None
|
||||
|
||||
|
||||
def _maybe_get_autograd_trace() -> str | None:
|
||||
if torch._C._current_autograd_node() is not None:
|
||||
tb = torch._C._current_autograd_node().metadata.get("traceback_") # type: ignore[attr-defined]
|
||||
if tb:
|
||||
return "".join(tb)
|
||||
return None
|
||||
|
||||
|
||||
def _get_op_name(op) -> str:
|
||||
if isinstance(op, torch._ops.OpOverload):
|
||||
op_name = op.__qualname__
|
||||
elif hasattr(op, "__module__") and hasattr(op, "__name__"):
|
||||
op_name = f"{op.__module__}.{op.__name__}"
|
||||
else:
|
||||
op_name = str(op)
|
||||
return op_name
|
||||
|
||||
|
||||
def _run_hook(hook, *args):
|
||||
out = hook(*args)
|
||||
if out is not None and not isinstance(out, dict):
|
||||
raise AssertionError(f"hook must return None or dict, got {type(out).__name__}")
|
||||
return out
|
||||
|
||||
|
||||
def _run_dispatch_pre_log_hooks(call: "_DebugCall", func, types, args, kwargs) -> None:
|
||||
if _DISPATCH_PRE_LOG_HOOKS:
|
||||
for hook in _DISPATCH_PRE_LOG_HOOKS:
|
||||
hook_out = _run_hook(hook, func, types, args, kwargs, call)
|
||||
if hook_out is not None:
|
||||
# Store pre-hook results in call.log
|
||||
if call.log is None:
|
||||
call.log = {}
|
||||
call.log.update(hook_out)
|
||||
|
||||
|
||||
def _run_dispatch_hooks(call: "_DebugCall", func, types, args, kwargs, result) -> None:
|
||||
if _DISPATCH_RECORD_HOOKS:
|
||||
record = {}
|
||||
for hook in _DISPATCH_RECORD_HOOKS:
|
||||
hook_out = _run_hook(hook, func, types, args, kwargs, result)
|
||||
if hook_out is not None:
|
||||
record.update(hook_out)
|
||||
if record:
|
||||
call.record = record
|
||||
|
||||
if _DISPATCH_LOG_HOOKS:
|
||||
# Preserve existing log from pre-hooks (e.g., input_hash)
|
||||
if call.log is None:
|
||||
call.log = {}
|
||||
for hook in _DISPATCH_LOG_HOOKS:
|
||||
hook_out = _run_hook(hook, func, types, args, kwargs, result)
|
||||
if hook_out is not None:
|
||||
call.log.update(hook_out)
|
||||
@@ -0,0 +1,137 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import functools
|
||||
|
||||
import torch
|
||||
from torch._C import _len_torch_function_stack
|
||||
from torch.overrides import _pop_mode, _push_mode, TorchFunctionMode
|
||||
from torch.utils._contextlib import context_decorator
|
||||
|
||||
|
||||
CURRENT_DEVICE: torch.device | None = None
|
||||
|
||||
|
||||
@functools.lru_cache(1)
|
||||
def _device_constructors():
|
||||
return {
|
||||
# standard ones
|
||||
torch.empty,
|
||||
torch.empty_permuted,
|
||||
torch.empty_strided,
|
||||
torch.empty_quantized,
|
||||
torch.ones,
|
||||
torch.arange,
|
||||
torch.bartlett_window,
|
||||
torch.blackman_window,
|
||||
torch.eye,
|
||||
torch.fft.fftfreq,
|
||||
torch.fft.rfftfreq,
|
||||
torch.full,
|
||||
torch.hamming_window,
|
||||
torch.hann_window,
|
||||
torch.kaiser_window,
|
||||
torch.linspace,
|
||||
torch.logspace,
|
||||
torch.nested.nested_tensor,
|
||||
# This function doesn't actually take a device argument
|
||||
# torch.normal,
|
||||
torch.rand,
|
||||
torch.randn,
|
||||
torch.randint,
|
||||
torch.randperm,
|
||||
torch.range,
|
||||
torch.sparse_coo_tensor,
|
||||
torch.sparse_compressed_tensor,
|
||||
torch.sparse_csr_tensor,
|
||||
torch.sparse_csc_tensor,
|
||||
torch.sparse_bsr_tensor,
|
||||
torch.sparse_bsc_tensor,
|
||||
torch.tril_indices,
|
||||
torch.triu_indices,
|
||||
torch.zeros,
|
||||
torch.asarray,
|
||||
# weird ones
|
||||
torch.tensor,
|
||||
torch.as_tensor,
|
||||
torch.scalar_tensor,
|
||||
# *_like may contain device kwarg, but the user implicitly
|
||||
# expects a specific device even when kwarg unused.
|
||||
# torch.zeros_like,
|
||||
# torch.randint_like,
|
||||
# torch.randn_like,
|
||||
# torch.ones_like,
|
||||
# torch.full_like,
|
||||
# torch.empty_like,
|
||||
}
|
||||
|
||||
|
||||
# NB: This is directly called from C++ in torch/csrc/Device.cpp
|
||||
class DeviceContext(TorchFunctionMode):
|
||||
def __init__(self, device) -> None:
|
||||
self.device = torch.device(device)
|
||||
self.prev_mode: DeviceContext | None = None
|
||||
|
||||
def __enter__(self):
|
||||
global CURRENT_DEVICE
|
||||
self.old_device = CURRENT_DEVICE
|
||||
CURRENT_DEVICE = self.device
|
||||
# We need to put the device at the bottom of the stack
|
||||
# If we set default device within a function mode context
|
||||
# exiting that context mode will pop the device function mode off
|
||||
# of the stack incorrectly
|
||||
cur_stack = [_pop_mode() for _ in range(_len_torch_function_stack())]
|
||||
|
||||
_push_mode(self)
|
||||
|
||||
for mode in reversed(cur_stack):
|
||||
if isinstance(mode, DeviceContext):
|
||||
self.prev_mode = mode
|
||||
else:
|
||||
_push_mode(mode)
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
global CURRENT_DEVICE
|
||||
CURRENT_DEVICE = self.old_device
|
||||
cur_stack = []
|
||||
# Invariant: there should only be one DeviceContext on the stack at any time
|
||||
# (At the bottom), pop all modes until we hit the bottom, assert it's a DeviceContext
|
||||
# or else someone else has popped it!
|
||||
for _ in range(_len_torch_function_stack() - 1):
|
||||
mode = _pop_mode()
|
||||
if isinstance(mode, DeviceContext):
|
||||
raise AssertionError(
|
||||
"Found nested DeviceContext on the mode stack where none expected"
|
||||
)
|
||||
cur_stack.append(mode)
|
||||
|
||||
if _len_torch_function_stack() > 0:
|
||||
mode = _pop_mode()
|
||||
if not isinstance(mode, DeviceContext):
|
||||
raise AssertionError(
|
||||
"Expected a DeviceContext at the bottom of the mode stack"
|
||||
)
|
||||
if self.prev_mode is not None:
|
||||
_push_mode(self.prev_mode)
|
||||
|
||||
for mode in reversed(cur_stack):
|
||||
_push_mode(mode)
|
||||
|
||||
def __torch_function__(self, func, types, args=(), kwargs=None):
|
||||
kwargs = kwargs or {}
|
||||
if func in _device_constructors() and kwargs.get("device") is None:
|
||||
kwargs["device"] = self.device
|
||||
return func(*args, **kwargs)
|
||||
|
||||
|
||||
# NB: This is directly called from C++ in torch/csrc/Device.cpp
|
||||
def device_decorator(device, func):
|
||||
return context_decorator(lambda: device, func)
|
||||
|
||||
|
||||
def set_device(device):
|
||||
"""
|
||||
Set the default device inside of the wrapped function by decorating it with this function.
|
||||
|
||||
If you would like to use this as a context manager, use device as a
|
||||
context manager directly, e.g., ``with torch.device(device)``.
|
||||
"""
|
||||
return lambda func: device_decorator(torch.device(device), func)
|
||||
@@ -0,0 +1,30 @@
|
||||
import torch
|
||||
|
||||
|
||||
# Used for testing and logging
|
||||
dtype_abbrs = {
|
||||
torch.bfloat16: "bf16",
|
||||
torch.float64: "f64",
|
||||
torch.float32: "f32",
|
||||
torch.float16: "f16",
|
||||
torch.float8_e4m3fn: "f8e4m3fn",
|
||||
torch.float8_e5m2: "f8e5m2",
|
||||
torch.float8_e4m3fnuz: "f8e4m3fnuz",
|
||||
torch.float8_e5m2fnuz: "f8e5m2fnuz",
|
||||
torch.float8_e8m0fnu: "f8e8m0fnu",
|
||||
torch.float4_e2m1fn_x2: "f4e2m1fnx2",
|
||||
torch.complex32: "c32",
|
||||
torch.complex64: "c64",
|
||||
torch.complex128: "c128",
|
||||
torch.int8: "i8",
|
||||
torch.int16: "i16",
|
||||
torch.int32: "i32",
|
||||
torch.int64: "i64",
|
||||
torch.bool: "b8",
|
||||
torch.uint8: "u8",
|
||||
torch.uint16: "u16",
|
||||
torch.uint32: "u32",
|
||||
torch.uint64: "u64",
|
||||
torch.bits16: "b16",
|
||||
torch.bits1x8: "b1x8",
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
from collections.abc import Callable
|
||||
from typing import TypeVar
|
||||
|
||||
|
||||
F = TypeVar("F")
|
||||
|
||||
|
||||
# Allows one to expose an API in a private submodule publicly as per the definition
|
||||
# in PyTorch's public api policy.
|
||||
#
|
||||
# It is a temporary solution while we figure out if it should be the long-term solution
|
||||
# or if we should amend PyTorch's public api policy. The concern is that this approach
|
||||
# may not be very robust because it's not clear what __module__ is used for.
|
||||
# However, both numpy and jax overwrite the __module__ attribute of their APIs
|
||||
# without problem, so it seems fine.
|
||||
def exposed_in(module: str) -> Callable[[F], F]:
|
||||
def wrapper(fn: F) -> F:
|
||||
fn.__module__ = module
|
||||
return fn
|
||||
|
||||
return wrapper
|
||||
@@ -0,0 +1,41 @@
|
||||
from types import TracebackType
|
||||
from typing_extensions import Self
|
||||
|
||||
from filelock import FileLock as base_FileLock
|
||||
|
||||
from torch.monitor import _WaitCounter
|
||||
|
||||
|
||||
class FileLock(base_FileLock):
|
||||
"""
|
||||
This behaves like a normal file lock.
|
||||
|
||||
However, it adds waitcounters for acquiring and releasing the filelock
|
||||
as well as for the critical region within it.
|
||||
|
||||
pytorch.filelock.enter - While we're acquiring the filelock.
|
||||
pytorch.filelock.region - While we're holding the filelock and doing work.
|
||||
pytorch.filelock.exit - While we're releasing the filelock.
|
||||
"""
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
self.region_counter = _WaitCounter("pytorch.filelock.region").guard()
|
||||
with _WaitCounter("pytorch.filelock.enter").guard():
|
||||
result = super().__enter__()
|
||||
self.region_counter.__enter__()
|
||||
return result
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> None:
|
||||
self.region_counter.__exit__()
|
||||
with _WaitCounter("pytorch.filelock.exit").guard():
|
||||
# Returns nothing per
|
||||
# https://github.com/tox-dev/filelock/blob/57f488ff8fdc2193572efe102408fb63cfefe4e4/src/filelock/_api.py#L379
|
||||
super().__exit__(exc_type, exc_value, traceback)
|
||||
# Returns nothing per
|
||||
# https://github.com/pytorch/pytorch/blob/0f6bfc58a2cfb7a5c052bea618ab62becaf5c912/torch/csrc/monitor/python_init.cpp#L315
|
||||
return None
|
||||
@@ -0,0 +1,60 @@
|
||||
from typing import TypeAlias
|
||||
|
||||
import torch
|
||||
from torch import Tensor
|
||||
from torch.autograd.grad_mode import no_grad
|
||||
|
||||
|
||||
def _get_foreach_kernels_supported_devices() -> list[str]:
|
||||
r"""Return the device type list that supports foreach kernels."""
|
||||
return ["cuda", "xpu", "mtia", torch._C._get_privateuse1_backend_name()]
|
||||
|
||||
|
||||
def _get_fused_kernels_supported_devices() -> list[str]:
|
||||
r"""Return the device type list that supports fused kernels in optimizer."""
|
||||
return [
|
||||
"mps",
|
||||
"cuda",
|
||||
"xpu",
|
||||
"hpu",
|
||||
"cpu",
|
||||
"mtia",
|
||||
torch._C._get_privateuse1_backend_name(),
|
||||
]
|
||||
|
||||
|
||||
TensorListList: TypeAlias = list[list[Tensor | None]]
|
||||
Indices: TypeAlias = list[int]
|
||||
_foreach_supported_types = [torch.Tensor]
|
||||
|
||||
|
||||
# This util function splits tensors into groups by device and dtype, which is useful before sending
|
||||
# tensors off to a foreach implementation, which requires tensors to be on one device and dtype.
|
||||
# If tensorlistlist contains more than one tensorlist, the following assumptions are made BUT NOT verified:
|
||||
# - tensorlists CAN be None
|
||||
# - all tensors in the first specified list cannot be None
|
||||
# - given an index i, all specified tensorlist[i]s match in dtype and device
|
||||
# with_indices (bool, optional): whether to track previous indices as the last list per dictionary entry.
|
||||
# It comes in handy if there are Nones or literals in the tensorlists that are getting scattered out.
|
||||
# Whereas mutating a tensor in the resulting split-up tensorlists WILL propagate changes back to the
|
||||
# original input tensorlists, changing up Nones/literals WILL NOT propagate, and manual propagation
|
||||
# may be necessary. Check out torch/optim/sgd.py for an example.
|
||||
@no_grad()
|
||||
def _group_tensors_by_device_and_dtype(
|
||||
tensorlistlist: TensorListList,
|
||||
with_indices: bool = False,
|
||||
) -> dict[tuple[torch.device, torch.dtype], tuple[TensorListList, Indices]]:
|
||||
return torch._C._group_tensors_by_device_and_dtype(tensorlistlist, with_indices)
|
||||
|
||||
|
||||
def _device_has_foreach_support(device: torch.device) -> bool:
|
||||
return (
|
||||
device.type in (_get_foreach_kernels_supported_devices() + ["cpu"])
|
||||
and not torch.jit.is_scripting()
|
||||
)
|
||||
|
||||
|
||||
def _has_foreach_support(tensors: list[Tensor], device: torch.device) -> bool:
|
||||
return _device_has_foreach_support(device) and all(
|
||||
t is None or type(t) in _foreach_supported_types for t in tensors
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
import functools
|
||||
from collections.abc import Callable
|
||||
from typing import Concatenate, TypeVar
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
|
||||
_P = ParamSpec("_P")
|
||||
_T = TypeVar("_T")
|
||||
_C = TypeVar("_C")
|
||||
|
||||
# Sentinel used to indicate that cache lookup failed.
|
||||
_cache_sentinel = object()
|
||||
|
||||
|
||||
def cache_method(
|
||||
f: Callable[Concatenate[_C, _P], _T],
|
||||
) -> Callable[Concatenate[_C, _P], _T]:
|
||||
"""
|
||||
Like `@functools.cache` but for methods.
|
||||
|
||||
`@functools.cache` (and similarly `@functools.lru_cache`) shouldn't be used
|
||||
on methods because it caches `self`, keeping it alive
|
||||
forever. `@cache_method` ignores `self` so won't keep `self` alive (assuming
|
||||
no cycles with `self` in the parameters).
|
||||
|
||||
Footgun warning: This decorator completely ignores self's properties so only
|
||||
use it when you know that self is frozen or won't change in a meaningful
|
||||
way (such as the wrapped function being pure).
|
||||
"""
|
||||
cache_name = "_cache_method_" + f.__name__
|
||||
|
||||
@functools.wraps(f)
|
||||
def wrap(self: _C, *args: _P.args, **kwargs: _P.kwargs) -> _T:
|
||||
if kwargs:
|
||||
raise AssertionError("cache_method does not accept keyword arguments")
|
||||
if not (cache := getattr(self, cache_name, None)):
|
||||
cache = {}
|
||||
setattr(self, cache_name, cache)
|
||||
cached_value = cache.get(args, _cache_sentinel)
|
||||
if cached_value is not _cache_sentinel:
|
||||
return cached_value
|
||||
value = f(self, *args, **kwargs)
|
||||
cache[args] = value
|
||||
return value
|
||||
|
||||
return wrap
|
||||
@@ -0,0 +1,200 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def remove_triton_function_declaration(source_code: str) -> str:
|
||||
remove_head = re.sub(r"(\n.+\s\'\'\'\n)", "\n", source_code)
|
||||
remove_tail = re.sub(r"(\'\'\'\,.+)", "\n", remove_head)
|
||||
return remove_tail
|
||||
|
||||
|
||||
def remove_async_compile(source_code: str) -> str:
|
||||
remove_top_level = str.replace(source_code, "async_compile = AsyncCompile()", "")
|
||||
remove_compile = str.replace(remove_top_level, "async_compile.wait(globals())", "")
|
||||
remove_del = str.replace(remove_compile, "del async_compile", "")
|
||||
return remove_del
|
||||
|
||||
|
||||
def rename_kernels(source_code: str) -> str:
|
||||
pattern = r"(\w+)\s*=\s*async_compile\.triton\('triton_',\s"
|
||||
triton_kernel_decl = "def triton_"
|
||||
matches = [
|
||||
(match.end(), match.group(1))
|
||||
for match in re.finditer(pattern, source_code, re.DOTALL)
|
||||
]
|
||||
|
||||
# Starting from the last match to avoid issues with shifting indices after replacements
|
||||
for end_index, captured_string in reversed(matches):
|
||||
# Find the index of the next "B" after the current match
|
||||
index_of_B = source_code.find(triton_kernel_decl, end_index)
|
||||
if index_of_B != -1:
|
||||
# Replace the triton_kernel_decl with the captured string
|
||||
source_code = (
|
||||
source_code[:index_of_B]
|
||||
+ f"def {captured_string}"
|
||||
+ source_code[index_of_B + len(triton_kernel_decl) :]
|
||||
)
|
||||
else:
|
||||
# If triton_kernel_decl is not found after the current match, continue to the next
|
||||
continue
|
||||
|
||||
return source_code
|
||||
|
||||
|
||||
def merge_params(original_params: list[str], new_params: list[str]) -> list[str]:
|
||||
for idx in range(len(new_params)):
|
||||
if new_params[idx] == "T":
|
||||
new_params[idx] = original_params[idx]
|
||||
return new_params
|
||||
|
||||
|
||||
def add_launch_params(
|
||||
original: str, kernel_to_params: dict[str, tuple[str, str]]
|
||||
) -> str:
|
||||
# Regex to match the function call in the original string
|
||||
pattern = r"(\w+)\.run\((.*)\)"
|
||||
|
||||
def replace(match) -> str:
|
||||
# Extract parts from the regex match
|
||||
func_name = match.group(1)
|
||||
params = match.group(2)
|
||||
new_params, grid = kernel_to_params[func_name]
|
||||
new_params = merge_params(params.split(", "), new_params.split(", "))
|
||||
|
||||
# Format the new function call
|
||||
new_string = f"{func_name}[{grid}]({', '.join(new_params)})"
|
||||
return new_string
|
||||
|
||||
transformed = re.sub(pattern, replace, original)
|
||||
|
||||
remove_inductor_wrappers = re.sub(
|
||||
r"@triton_heuristics[^@]*@triton.jit",
|
||||
r"@triton.jit",
|
||||
transformed,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
return remove_inductor_wrappers
|
||||
|
||||
|
||||
def process_file(
|
||||
input_filename: str, output_filename: str, auto_generate_params: bool = True
|
||||
) -> str:
|
||||
with open(input_filename) as file:
|
||||
source_code = file.read()
|
||||
|
||||
transformed_code = source_code
|
||||
if "def triton_(" in source_code:
|
||||
raise RuntimeError(
|
||||
"Need to run original Pytorch code generating kernels with TORCHINDUCTOR_UNIQUE_KERNEL_NAMES=1"
|
||||
)
|
||||
# transformed_code = rename_kernels(transformed_code)
|
||||
transformed_code = remove_triton_function_declaration(transformed_code)
|
||||
transformed_code = remove_async_compile(transformed_code)
|
||||
|
||||
launch_params_filename = f"{input_filename}.launch_params"
|
||||
|
||||
# Auto-generate launch_params if they don't exist and auto_generate_params is True
|
||||
if not os.path.exists(launch_params_filename) and auto_generate_params:
|
||||
print(f"Launch params file {launch_params_filename} not found. Generating...")
|
||||
try:
|
||||
# Set environment variable and run the input file
|
||||
env = os.environ.copy()
|
||||
env["TORCHINDUCTOR_DUMP_LAUNCH_PARAMS"] = "1"
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, input_filename],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=os.path.dirname(input_filename) or ".",
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"Error running {input_filename}:")
|
||||
print(f"stdout: {result.stdout}")
|
||||
print(f"stderr: {result.stderr}")
|
||||
raise RuntimeError(
|
||||
f"Failed to generate launch params. Command failed with return code {result.returncode}"
|
||||
)
|
||||
|
||||
print(f"Successfully generated {launch_params_filename}")
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(
|
||||
f"Failed to generate launch params by running {input_filename}: {str(e)}"
|
||||
) from e
|
||||
|
||||
if not os.path.exists(launch_params_filename):
|
||||
raise RuntimeError(
|
||||
f"Missing {launch_params_filename}. Run `TORCHINDUCTOR_DUMP_LAUNCH_PARAMS=1 python {input_filename}` first."
|
||||
)
|
||||
|
||||
with open(launch_params_filename) as f:
|
||||
launch_params_meta = f.readlines()
|
||||
|
||||
split_params = [i.split("|") for i in launch_params_meta]
|
||||
kernel_args_grid = {a.strip(): (b.strip(), c.strip()) for a, b, c in split_params}
|
||||
transformed_code = add_launch_params(transformed_code, kernel_args_grid)
|
||||
|
||||
with open(output_filename, "w") as file:
|
||||
file.write(transformed_code)
|
||||
print(f"Successfully generated {output_filename}")
|
||||
return transformed_code
|
||||
|
||||
|
||||
def get_clean_triton(
|
||||
input_path: Path,
|
||||
output_path: Path = Path("triton_only_repro.py"),
|
||||
auto_generate_params: bool = True,
|
||||
):
|
||||
"""Run experiments and output results to file
|
||||
|
||||
Args:
|
||||
input_path (Path): Path to inductor generated output codede
|
||||
output_path (Path): Path to write out the new python file
|
||||
auto_generate_params (bool): Whether to automatically generate launch_params if missing
|
||||
"""
|
||||
return process_file(str(input_path), str(output_path), auto_generate_params)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
"""Sample usage:
|
||||
# Running sweep
|
||||
python _get_clean_triton.py output_code.py
|
||||
|
||||
# To disable auto-generation of launch params:
|
||||
python _get_clean_triton.py output_code.py --no-auto-generate
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Clean Inductor generated code to remove Inductor dependencies"
|
||||
)
|
||||
|
||||
# Add the arguments
|
||||
parser.add_argument(
|
||||
"input_path", type=Path, help="Path to inductor generated output code"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output_path",
|
||||
type=Path,
|
||||
default=Path("triton_only_repro.py"),
|
||||
help="Path to write out the clean triton output",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-auto-generate",
|
||||
action="store_true",
|
||||
help="Disable automatic generation of launch_params file",
|
||||
)
|
||||
|
||||
# Parse the arguments
|
||||
args = parser.parse_args()
|
||||
|
||||
# Call the function with parsed arguments
|
||||
result = get_clean_triton(
|
||||
args.input_path, args.output_path, not args.no_auto_generate
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
import functools
|
||||
|
||||
from torch.utils._triton import has_triton
|
||||
|
||||
|
||||
@functools.cache
|
||||
def has_helion_package() -> bool:
|
||||
try:
|
||||
import helion # type: ignore[import-untyped, import-not-found] # noqa: F401
|
||||
except ImportError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@functools.cache
|
||||
def has_helion() -> bool:
|
||||
return has_helion_package() and has_triton()
|
||||
@@ -0,0 +1,37 @@
|
||||
import functools
|
||||
import importlib.util
|
||||
from types import ModuleType
|
||||
|
||||
|
||||
def _check_module_exists(name: str) -> bool:
|
||||
r"""Returns if a top-level module with :attr:`name` exists *without**
|
||||
importing it. This is generally safer than try-catch block around a
|
||||
`import X`. It avoids third party libraries breaking assumptions of some of
|
||||
our tests, e.g., setting multiprocessing start method when imported
|
||||
(see librosa/#747, torchvision/#544).
|
||||
"""
|
||||
try:
|
||||
spec = importlib.util.find_spec(name)
|
||||
return spec is not None
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
@functools.lru_cache
|
||||
def dill_available() -> bool:
|
||||
return _check_module_exists("dill")
|
||||
|
||||
|
||||
@functools.lru_cache
|
||||
def import_dill() -> ModuleType | None:
|
||||
if not dill_available():
|
||||
return None
|
||||
|
||||
import dill
|
||||
|
||||
# XXX: By default, dill writes the Pickler dispatch table to inject its
|
||||
# own logic there. This globally affects the behavior of the standard library
|
||||
# pickler for any user who transitively depends on this module!
|
||||
# Undo this extension to avoid altering the behavior of the pickler globally.
|
||||
dill.extend(use_dill=False)
|
||||
return dill
|
||||
@@ -0,0 +1,90 @@
|
||||
import inspect
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _signature_metadata(
|
||||
sig: inspect.Signature,
|
||||
) -> tuple[tuple[inspect.Parameter, ...], bool, int]:
|
||||
"""
|
||||
Returns tuple(sig.parameters.values()), if any has VAR_POSITIONAL or VAR_KEYWORD, and the max_positional
|
||||
"""
|
||||
params = tuple(sig.parameters.values())
|
||||
has_var_args = False
|
||||
max_positional = 0
|
||||
|
||||
for p in params:
|
||||
kind = p.kind
|
||||
if kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
||||
has_var_args = True
|
||||
if kind in (
|
||||
inspect.Parameter.POSITIONAL_ONLY,
|
||||
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
||||
):
|
||||
max_positional += 1
|
||||
|
||||
return params, has_var_args, max_positional
|
||||
|
||||
|
||||
def _fast_bind(
|
||||
sig: inspect.Signature, *args: Any, **kwargs: Any
|
||||
) -> inspect.BoundArguments:
|
||||
"""
|
||||
Fast path for inspect.Signature.bind() for signatures without
|
||||
VAR_POSITIONAL or VAR_KEYWORD parameters. Falls back to sig.bind()
|
||||
for signatures that contain *args or **kwargs.
|
||||
"""
|
||||
params, has_var_args, max_positional = _signature_metadata(sig)
|
||||
|
||||
# fallback for complex signatures
|
||||
if has_var_args:
|
||||
return sig.bind(*args, **kwargs)
|
||||
|
||||
len_args = len(args)
|
||||
|
||||
if len_args > max_positional:
|
||||
raise TypeError(
|
||||
f"Too many positional arguments: expected max {max_positional}, got {len_args}"
|
||||
)
|
||||
|
||||
arguments: dict[str, Any] = {}
|
||||
arg_i = 0
|
||||
|
||||
for p in params:
|
||||
name = p.name
|
||||
kind = p.kind
|
||||
|
||||
if kind is inspect.Parameter.POSITIONAL_ONLY:
|
||||
if name in kwargs:
|
||||
raise TypeError(
|
||||
f"Got some positional-only arguments passed as keyword arguments: '{name}'"
|
||||
)
|
||||
if arg_i < len_args:
|
||||
arguments[name] = args[arg_i]
|
||||
arg_i += 1
|
||||
elif p.default is inspect.Parameter.empty:
|
||||
raise TypeError(f"Missing required argument '{name}'")
|
||||
|
||||
elif kind is inspect.Parameter.POSITIONAL_OR_KEYWORD:
|
||||
if arg_i < len_args:
|
||||
if name in kwargs:
|
||||
raise TypeError(f"Multiple values for argument '{name}'")
|
||||
arguments[name] = args[arg_i]
|
||||
arg_i += 1
|
||||
elif name in kwargs:
|
||||
arguments[name] = kwargs[name]
|
||||
elif p.default is inspect.Parameter.empty:
|
||||
raise TypeError(f"Missing required argument '{name}'")
|
||||
|
||||
elif kind is inspect.Parameter.KEYWORD_ONLY:
|
||||
if name in kwargs:
|
||||
arguments[name] = kwargs[name]
|
||||
elif p.default is inspect.Parameter.empty:
|
||||
raise TypeError(f"Missing required argument '{name}'")
|
||||
|
||||
# disallow extra keyword arguments not in the signature
|
||||
# cause kwargs have been processed by sig.bind at the beginning
|
||||
for name in kwargs:
|
||||
if name not in sig.parameters:
|
||||
raise TypeError(f"Got an unexpected keyword argument '{name}'")
|
||||
|
||||
return inspect.BoundArguments(sig, arguments) # type: ignore[arg-type]
|
||||
@@ -0,0 +1,15 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from typing import TypeVar
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
# returns if all are the same mode
|
||||
def all_same_mode(modes):
|
||||
return all(tuple(mode == modes[0] for mode in modes))
|
||||
|
||||
|
||||
no_dispatch = torch._C._DisableTorchDispatch
|
||||
@@ -0,0 +1,178 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import (
|
||||
Hashable,
|
||||
Iterable,
|
||||
Iterator,
|
||||
MutableSet,
|
||||
Reversible,
|
||||
Set as AbstractSet,
|
||||
)
|
||||
from typing import Any, cast, TypeVar
|
||||
|
||||
|
||||
T = TypeVar("T", bound=Hashable)
|
||||
T_co = TypeVar("T_co", bound=Hashable, covariant=True)
|
||||
|
||||
__all__ = ["OrderedSet"]
|
||||
|
||||
|
||||
class OrderedSet(MutableSet[T], Reversible[T]):
|
||||
"""
|
||||
Insertion ordered set, similar to OrderedDict.
|
||||
"""
|
||||
|
||||
__slots__ = ("_dict",)
|
||||
|
||||
def __init__(self, iterable: Iterable[T] | None = None) -> None:
|
||||
self._dict = dict.fromkeys(iterable, None) if iterable is not None else {}
|
||||
|
||||
@staticmethod
|
||||
def _from_dict(dict_inp: dict[T, None]) -> OrderedSet[T]:
|
||||
s: OrderedSet[T] = OrderedSet()
|
||||
s._dict = dict_inp
|
||||
return s
|
||||
|
||||
#
|
||||
# Required overridden abstract methods
|
||||
#
|
||||
def __contains__(self, elem: object) -> bool:
|
||||
return elem in self._dict
|
||||
|
||||
def __iter__(self) -> Iterator[T]:
|
||||
return iter(self._dict)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._dict)
|
||||
|
||||
def __reversed__(self) -> Iterator[T]:
|
||||
return reversed(self._dict)
|
||||
|
||||
def add(self, elem: T) -> None:
|
||||
self._dict[elem] = None
|
||||
|
||||
def discard(self, elem: T) -> None:
|
||||
self._dict.pop(elem, None)
|
||||
|
||||
def clear(self) -> None:
|
||||
# overridden because MutableSet impl is slow
|
||||
self._dict.clear()
|
||||
|
||||
# Unimplemented set() methods in _collections_abc.MutableSet
|
||||
|
||||
@classmethod
|
||||
def _wrap_iter_in_set(cls, other: Any) -> Any:
|
||||
"""
|
||||
Wrap non-Set Iterables in OrderedSets
|
||||
|
||||
Some of the magic methods are more strict on input types than
|
||||
the public apis, so we need to wrap inputs in sets.
|
||||
"""
|
||||
|
||||
if not isinstance(other, AbstractSet) and isinstance(other, Iterable):
|
||||
return cls(other)
|
||||
else:
|
||||
return other
|
||||
|
||||
def pop(self) -> T:
|
||||
if not self:
|
||||
raise KeyError("pop from an empty set")
|
||||
return self._dict.popitem()[0]
|
||||
|
||||
def copy(self) -> OrderedSet[T]:
|
||||
return OrderedSet._from_dict(self._dict.copy())
|
||||
|
||||
def difference(self, *others: Iterable[T]) -> OrderedSet[T]:
|
||||
res = self.copy()
|
||||
res.difference_update(*others)
|
||||
return res
|
||||
|
||||
def difference_update(self, *others: Iterable[T]) -> None:
|
||||
for other in others:
|
||||
self -= other # type: ignore[arg-type]
|
||||
|
||||
def update(self, *others: Iterable[T]) -> None:
|
||||
for other in others:
|
||||
self |= other
|
||||
|
||||
def intersection(self, *others: Iterable[T]) -> OrderedSet[T]:
|
||||
res = self.copy()
|
||||
for other in others:
|
||||
if other is not self:
|
||||
res &= other # type: ignore[arg-type]
|
||||
return res
|
||||
|
||||
def intersection_update(self, *others: Iterable[T]) -> None:
|
||||
for other in others:
|
||||
self &= other # type: ignore[arg-type]
|
||||
|
||||
def issubset(self, other: Iterable[T]) -> bool:
|
||||
return self <= self._wrap_iter_in_set(other)
|
||||
|
||||
def issuperset(self, other: Iterable[T]) -> bool:
|
||||
return self >= self._wrap_iter_in_set(other)
|
||||
|
||||
def symmetric_difference(self, other: Iterable[T]) -> OrderedSet[T]:
|
||||
return self ^ other # type: ignore[operator]
|
||||
|
||||
def symmetric_difference_update(self, other: Iterable[T]) -> None:
|
||||
self ^= other # type: ignore[arg-type]
|
||||
|
||||
def union(self, *others: Iterable[T]) -> OrderedSet[T]:
|
||||
res = self.copy()
|
||||
for other in others:
|
||||
if other is self:
|
||||
continue
|
||||
res |= other
|
||||
return res
|
||||
|
||||
# Specify here for correct type inference, otherwise would
|
||||
# return AbstractSet[T]
|
||||
def __sub__(self, other: AbstractSet[T_co]) -> OrderedSet[T]:
|
||||
# following cpython set impl optimization
|
||||
if isinstance(other, OrderedSet) and (len(self) * 4) > len(other):
|
||||
out = self.copy()
|
||||
out -= other
|
||||
return out
|
||||
return cast(OrderedSet[T], super().__sub__(other))
|
||||
|
||||
def __ior__(self, other: Iterable[T]) -> OrderedSet[T]: # type: ignore[misc, override] # noqa: PYI034
|
||||
if isinstance(other, OrderedSet):
|
||||
self._dict.update(other._dict)
|
||||
return self
|
||||
return super().__ior__(other) # type: ignore[arg-type]
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if isinstance(other, OrderedSet):
|
||||
return self._dict == other._dict
|
||||
return super().__eq__(other)
|
||||
|
||||
def __ne__(self, other: object) -> bool:
|
||||
if isinstance(other, OrderedSet):
|
||||
return self._dict != other._dict
|
||||
return super().__ne__(other)
|
||||
|
||||
def __or__(self, other: AbstractSet[T_co]) -> OrderedSet[T]:
|
||||
return cast(OrderedSet[T], super().__or__(other))
|
||||
|
||||
def __and__(self, other: AbstractSet[T_co]) -> OrderedSet[T]:
|
||||
# MutableSet impl will iterate over other, iter over smaller of two sets
|
||||
if isinstance(other, OrderedSet) and len(self) < len(other):
|
||||
# pyrefly: ignore [unsupported-operation, bad-return]
|
||||
return other & self
|
||||
return cast(OrderedSet[T], super().__and__(other))
|
||||
|
||||
def __xor__(self, other: AbstractSet[T_co]) -> OrderedSet[T]:
|
||||
return cast(OrderedSet[T], super().__xor__(other))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({list(self)})"
|
||||
|
||||
def __getstate__(self) -> list[T]:
|
||||
return list(self._dict.keys())
|
||||
|
||||
def __setstate__(self, state: list[T]) -> None:
|
||||
self._dict = dict.fromkeys(state, None)
|
||||
|
||||
def __reduce__(self) -> tuple[type[OrderedSet[T]], tuple[list[T]]]:
|
||||
return (OrderedSet, (list(self),))
|
||||
@@ -0,0 +1,125 @@
|
||||
import functools
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
@functools.cache
|
||||
def has_jax_package() -> bool:
|
||||
"""Check if JAX is installed."""
|
||||
try:
|
||||
import jax # noqa: F401 # type: ignore[import-not-found]
|
||||
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
@functools.cache
|
||||
def has_pallas_package() -> bool:
|
||||
"""Check if Pallas (JAX experimental) is available."""
|
||||
if not has_jax_package():
|
||||
return False
|
||||
try:
|
||||
from jax.experimental import ( # noqa: F401 # type: ignore[import-not-found]
|
||||
pallas as pl,
|
||||
)
|
||||
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
@functools.cache
|
||||
def get_jax_version(fallback: tuple[int, int, int] = (0, 0, 0)) -> tuple[int, int, int]:
|
||||
"""Get JAX version as (major, minor, patch) tuple."""
|
||||
try:
|
||||
import jax # type: ignore[import-not-found]
|
||||
|
||||
version_parts = jax.__version__.split(".")
|
||||
major, minor, patch = (int(v) for v in version_parts[:3])
|
||||
return (major, minor, patch)
|
||||
except (ImportError, ValueError, AttributeError):
|
||||
return fallback
|
||||
|
||||
|
||||
@functools.cache
|
||||
def has_jax_cuda_backend() -> bool:
|
||||
"""Check if JAX has CUDA backend support with SM90+ (required by Mosaic GPU)."""
|
||||
if not has_jax_package():
|
||||
return False
|
||||
try:
|
||||
import jax # type: ignore[import-not-found]
|
||||
|
||||
# Check if CUDA backend is available
|
||||
devices = jax.devices("gpu")
|
||||
if len(devices) == 0:
|
||||
return False
|
||||
|
||||
# Mosaic GPU requires SM90+ (compute capability 9.0+)
|
||||
if torch.cuda.is_available():
|
||||
major, minor = torch.cuda.get_device_capability()
|
||||
if major < 9:
|
||||
return False
|
||||
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@functools.cache
|
||||
def has_jax_tpu_backend() -> bool:
|
||||
"""Check if JAX has TPU backend support."""
|
||||
if not has_jax_package():
|
||||
return False
|
||||
try:
|
||||
import jax # type: ignore[import-not-found]
|
||||
|
||||
# Check if TPU backend is available
|
||||
devices = jax.devices("tpu")
|
||||
return len(devices) > 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@functools.cache
|
||||
def has_torch_tpu() -> bool:
|
||||
"""Check if torch_tpu is installed and available."""
|
||||
try:
|
||||
import torch_tpu.api # noqa: F401 # type: ignore[import]
|
||||
|
||||
# Verify hardware/runtime access
|
||||
torch_tpu.api.tpu_device()
|
||||
return True
|
||||
except (ImportError, RuntimeError):
|
||||
return False
|
||||
|
||||
|
||||
@functools.cache
|
||||
def has_cpu_pallas() -> bool:
|
||||
"""Checks for a full Pallas-on-CPU environment."""
|
||||
return has_pallas_package()
|
||||
|
||||
|
||||
@functools.cache
|
||||
def has_cuda_pallas() -> bool:
|
||||
"""Checks for a full Pallas-on-CUDA environment."""
|
||||
return has_pallas_package() and torch.cuda.is_available() and has_jax_cuda_backend()
|
||||
|
||||
|
||||
@functools.cache
|
||||
def has_tpu_pallas() -> bool:
|
||||
"""Checks for a full Pallas-on-TPU environment."""
|
||||
return has_pallas_package() and has_jax_tpu_backend() and has_torch_tpu()
|
||||
|
||||
|
||||
@functools.cache
|
||||
def has_pallas() -> bool:
|
||||
"""
|
||||
Check if Pallas backend is fully available for use.
|
||||
|
||||
Requirements:
|
||||
- JAX package installed
|
||||
- Pallas (jax.experimental.pallas) available
|
||||
- A compatible backend (CUDA or TPU) is available in both PyTorch and JAX.
|
||||
"""
|
||||
return has_cpu_pallas() or has_cuda_pallas() or has_tpu_pallas()
|
||||
@@ -0,0 +1,965 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
import warnings
|
||||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
from typing import cast, overload, Protocol, TYPE_CHECKING
|
||||
from typing_extensions import TypeIs
|
||||
|
||||
import torch
|
||||
import torchgen
|
||||
import torchgen.model
|
||||
from torch._C import (
|
||||
_get_dispatch_stack_at,
|
||||
_len_torch_dispatch_stack,
|
||||
_pop_torch_dispatch_stack,
|
||||
_push_on_torch_dispatch_stack,
|
||||
DispatchKey,
|
||||
)
|
||||
from torch._C._dynamo.guards import set_is_in_mode_without_ignore_compile_internals
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
|
||||
# TODO: Limitations and things about enable_torch_dispatch_mode we should fix before exposing it:
|
||||
# - We need a better user-facing api for _DisableTorchDispatch that
|
||||
# is able to selectively disable __torch_dispatch__ of a particular class.
|
||||
# - It doesn't work with the tensor constructors (torch.tensor, torch.Tensor)
|
||||
# - Better name (see https://github.com/pytorch/pytorch/pull/63496#discussion_r694091694)
|
||||
|
||||
_is_in_torch_dispatch_mode = False
|
||||
_is_in_non_infra_torch_dispatch_mode = False
|
||||
# If inside any mode that has ignore_compile_internals() = False
|
||||
_is_in_any_mode_without_ignore_compile_internals = False
|
||||
|
||||
|
||||
def is_in_torch_dispatch_mode(include_infra_modes: bool = True) -> bool:
|
||||
return (
|
||||
_is_in_torch_dispatch_mode
|
||||
if include_infra_modes
|
||||
else _is_in_non_infra_torch_dispatch_mode
|
||||
)
|
||||
|
||||
|
||||
def is_in_any_mode_without_ignore_compile_internals() -> bool:
|
||||
return _is_in_any_mode_without_ignore_compile_internals
|
||||
|
||||
|
||||
def any_torch_dispatch_mode_on_stack() -> bool:
|
||||
stack_len = torch._C._len_torch_dispatch_stack()
|
||||
|
||||
for idx in range(stack_len):
|
||||
mode = _get_dispatch_stack_at(idx)
|
||||
|
||||
# Apply filters first
|
||||
if mode.is_infra_mode():
|
||||
continue
|
||||
|
||||
if mode.ignore_compile_internals():
|
||||
continue
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class TorchDispatchMode:
|
||||
"""
|
||||
A ``TorchDispatchMode`` allows you to override the meaning of all
|
||||
``__torch_dispatch__`` overridable functions within a dynamic scope,
|
||||
without having to actually create a tensor subclass or manually
|
||||
monkey-patch functions in the PyTorch API. Some common situations
|
||||
where you should use a mode:
|
||||
|
||||
* You want to override the meaning of factory functions, or other
|
||||
functions that do not otherwise take a tensor as an argument
|
||||
(these cannot be overridden with tensor subclasses).
|
||||
|
||||
* You want to override the behavior of all functions without needing
|
||||
to wrap your inputs in tensor subclasses; e.g., if you are just
|
||||
interested in logging intermediate computations.
|
||||
|
||||
* You want to control the order of execution of various tensor
|
||||
subclasses explicitly, rather than implicitly via the return of
|
||||
``NotImplemented``.
|
||||
|
||||
Independent subclasses of :class:`TorchDispatchMode` are compositional:
|
||||
modes can be pushed onto a stack using ``with MyMode():``.
|
||||
When you call functions in the PyTorch API inside your
|
||||
``__torch_dispatch__`` implementation, by default, they will forward on to
|
||||
the next mode on the mode stack. If you want recursively call back into
|
||||
your current ``__torch_dispatch__`` implementation, either explicitly
|
||||
invoke ``self.__torch_dispatch__(...)``, or use the context manager
|
||||
``self`` to make PyTorch
|
||||
API self-referential (beware of infinite loops, in this case!)
|
||||
"""
|
||||
|
||||
# - When False, custom torch dispatch mode will error out explicitly when a hop
|
||||
# is called under the mode.
|
||||
# - When True, custom torch dispatch mode's __torch_dispatch__ will be triggered.
|
||||
# Mode authors can implement how the mode interacts with higher order operators.
|
||||
supports_higher_order_operators = False
|
||||
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
super().__init_subclass__(**kwargs)
|
||||
if cls._should_skip_dynamo():
|
||||
if "__torch_dispatch__" in cls.__dict__:
|
||||
raw = cls.__dict__["__torch_dispatch__"]
|
||||
if not isinstance(raw, classmethod):
|
||||
cls.__torch_dispatch__ = torch._disable_dynamo(raw, recursive=True)
|
||||
|
||||
def __init__(self, _dispatch_key=None):
|
||||
if _dispatch_key is not None:
|
||||
if not isinstance(_dispatch_key, torch._C.DispatchKey):
|
||||
raise AssertionError("_dispatch_key must be a torch._C.DispatchKey")
|
||||
self.__dict__["_dispatch_key"] = _dispatch_key
|
||||
|
||||
self.old_dispatch_mode_flags: deque[bool] = deque()
|
||||
self.old_non_infra_dispatch_mode_flags: deque[bool] = deque()
|
||||
self.old_without_ignore_compile_internals_dispatch_mode_flags: deque[bool] = (
|
||||
deque()
|
||||
)
|
||||
|
||||
def _lazy_init_old_dispatch_mode_flags(self):
|
||||
if not hasattr(self, "old_dispatch_mode_flags"):
|
||||
self.old_dispatch_mode_flags: deque[bool] = deque() # type: ignore[no-redef]
|
||||
|
||||
if not hasattr(self, "old_non_infra_dispatch_mode_flags"):
|
||||
self.old_non_infra_dispatch_mode_flags: deque[bool] = deque() # type: ignore[no-redef]
|
||||
|
||||
if not hasattr(
|
||||
self, "old_without_ignore_compile_internals_dispatch_mode_flags"
|
||||
):
|
||||
self.old_without_ignore_compile_internals_dispatch_mode_flags: deque[ # type: ignore[no-redef]
|
||||
bool
|
||||
] = deque()
|
||||
|
||||
def __torch_dispatch__(self, func, types, args=(), kwargs=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def __enter__(self):
|
||||
global _is_in_torch_dispatch_mode
|
||||
global _is_in_non_infra_torch_dispatch_mode
|
||||
global _is_in_any_mode_without_ignore_compile_internals
|
||||
|
||||
# Previously, there wasn't any state in this class' constructor
|
||||
# super calls were added to existing modes, but for any new modes
|
||||
# this will replicate the previous behavior of not strictly needing
|
||||
# to call super().__init__()
|
||||
self._lazy_init_old_dispatch_mode_flags()
|
||||
self.old_dispatch_mode_flags.append(_is_in_torch_dispatch_mode)
|
||||
_is_in_torch_dispatch_mode = True
|
||||
self.old_non_infra_dispatch_mode_flags.append(
|
||||
_is_in_non_infra_torch_dispatch_mode
|
||||
)
|
||||
_is_in_non_infra_torch_dispatch_mode = (
|
||||
_is_in_non_infra_torch_dispatch_mode or not self.is_infra_mode()
|
||||
)
|
||||
self.old_without_ignore_compile_internals_dispatch_mode_flags.append(
|
||||
_is_in_any_mode_without_ignore_compile_internals
|
||||
)
|
||||
_is_in_any_mode_without_ignore_compile_internals = (
|
||||
_is_in_any_mode_without_ignore_compile_internals
|
||||
or not self.ignore_compile_internals()
|
||||
)
|
||||
set_is_in_mode_without_ignore_compile_internals(
|
||||
_is_in_any_mode_without_ignore_compile_internals
|
||||
)
|
||||
_push_mode(self)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
mb_dk_or_mode_key = self.__dict__.get("_dispatch_key", None)
|
||||
if mb_dk_or_mode_key is None:
|
||||
# Today, mode keys are not used at all in the per-dispatch-key-mode logic (for pre-dispatch)
|
||||
# We should probably revisit this.
|
||||
mb_dk_or_mode_key = self.__dict__.get("_mode_key", None)
|
||||
global _is_in_torch_dispatch_mode
|
||||
_is_in_torch_dispatch_mode = self.old_dispatch_mode_flags.pop()
|
||||
global _is_in_non_infra_torch_dispatch_mode
|
||||
_is_in_non_infra_torch_dispatch_mode = (
|
||||
self.old_non_infra_dispatch_mode_flags.pop()
|
||||
)
|
||||
global _is_in_any_mode_without_ignore_compile_internals
|
||||
_is_in_any_mode_without_ignore_compile_internals = (
|
||||
self.old_without_ignore_compile_internals_dispatch_mode_flags.pop()
|
||||
)
|
||||
set_is_in_mode_without_ignore_compile_internals(
|
||||
_is_in_any_mode_without_ignore_compile_internals
|
||||
)
|
||||
_pop_mode(mb_dk_or_mode_key)
|
||||
|
||||
@classmethod
|
||||
def push(cls, *args, **kwargs):
|
||||
warnings.warn(
|
||||
"`Mode.push()` is no longer necessary and can be replaced with just `with Mode()`",
|
||||
stacklevel=2,
|
||||
)
|
||||
instance = cls(*args, **kwargs)
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def is_infra_mode(cls) -> bool:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def _should_skip_dynamo(cls) -> bool:
|
||||
"""Skip Dynamo when the flag is set to True
|
||||
|
||||
This is temporary measure to rollout a feature
|
||||
that skips PT2 compilation inside __torch_dispatch__
|
||||
frames.
|
||||
|
||||
If this flag is off, we would expect following:
|
||||
|
||||
class YoloMode(TorchDispatchMode):
|
||||
@classmethod
|
||||
def _should_skip_dynamo(cls):
|
||||
return False
|
||||
def __torch_dispatch__(self, func, types, args=(), kwargs=None):
|
||||
return torch.ops.aten.mul.Tensor(args[0], args[1])
|
||||
|
||||
x = torch.ones(5)
|
||||
with YoloMode():
|
||||
out = torch.compile(torch.add, backend=backend, fullgraph=True)(x, x)
|
||||
|
||||
# instead of recursively disabling, we are compiling into __torch_dispatch__
|
||||
assert len(backend.graphs) == 1
|
||||
"""
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def ignore_compile_internals(cls) -> bool:
|
||||
"""Ignore operators that are compiled via torch.compile.
|
||||
|
||||
If ``True``, then this TorchDispatchMode ignores operators that
|
||||
are optimized by :func:`torch.compile`. Mechanically, this involves
|
||||
turning off the TorchDispatchMode throughout the whole compilation process,
|
||||
and turning it back on for the runtime of the compiled artifact(s).
|
||||
For example,
|
||||
|
||||
@torch.compile
|
||||
def f(x):
|
||||
return x.sin().cos()
|
||||
|
||||
with LoggingMode():
|
||||
f(x)
|
||||
|
||||
The above example will not log anything if
|
||||
``LoggingMode.ignore_compile_internals()`` is True.
|
||||
torch.compile will fuse sin() and cos() into a single operation
|
||||
and this TorchDispatchMode will not be passed sin and cos.
|
||||
|
||||
If ``False`` (default), :func:`torch.compile` will respect
|
||||
the eager semantics of passing this TorchDispatchMode all
|
||||
operators that would have run during eager execution.
|
||||
The way this will usually happen is that :func:`torch.compile`
|
||||
will just fallback to eager-mode PyTorch.
|
||||
"""
|
||||
if cls.is_infra_mode():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _get_current_dispatch_mode() -> TorchDispatchMode | None:
|
||||
"""
|
||||
Return the top user mode on the stack (the next one that would be
|
||||
executed) if there are any.
|
||||
"""
|
||||
stack_len = _len_torch_dispatch_stack()
|
||||
if stack_len > 0:
|
||||
return _get_dispatch_stack_at(stack_len - 1)
|
||||
return None
|
||||
|
||||
|
||||
def _detect_infra_mode(key):
|
||||
if key not in (
|
||||
torch._C._TorchDispatchModeKey.FUNCTIONAL,
|
||||
torch._C._TorchDispatchModeKey.PROXY,
|
||||
):
|
||||
raise AssertionError(
|
||||
f"key must be either FUNCTIONAL ({torch._C._TorchDispatchModeKey.FUNCTIONAL}) \
|
||||
or PROXY ({torch._C._TorchDispatchModeKey.PROXY}) _TorchDispatchModeKey, \
|
||||
got {key}"
|
||||
)
|
||||
from torch._ops import _get_dispatch_mode_pre_dispatch
|
||||
|
||||
pre_dispatch_mode = _get_dispatch_mode_pre_dispatch(key)
|
||||
post_dispatch_mode = torch._C._get_dispatch_mode(key)
|
||||
|
||||
if pre_dispatch_mode is not None and post_dispatch_mode is not None:
|
||||
raise AssertionError(
|
||||
"At most one of pre_dispatch_mode and post_dispatch_mode may be active"
|
||||
)
|
||||
|
||||
if pre_dispatch_mode is None:
|
||||
return post_dispatch_mode
|
||||
|
||||
return pre_dispatch_mode
|
||||
|
||||
|
||||
def _unset_infra_mode(key):
|
||||
from torch._ops import _get_dispatch_mode_pre_dispatch, unset_mode_pre_dispatch
|
||||
|
||||
pre_dispatch_mode = _get_dispatch_mode_pre_dispatch(key)
|
||||
post_dispatch_mode = torch._C._get_dispatch_mode(key)
|
||||
if pre_dispatch_mode and post_dispatch_mode:
|
||||
raise AssertionError(
|
||||
"Can't have active infra mode on both pre and post dispatch mode stack"
|
||||
)
|
||||
|
||||
if pre_dispatch_mode:
|
||||
mode = unset_mode_pre_dispatch(key)
|
||||
return mode
|
||||
if post_dispatch_mode:
|
||||
return torch._C._unset_dispatch_mode(key)
|
||||
|
||||
|
||||
def _disable_infra_mode(key):
|
||||
if key not in (
|
||||
torch._C._TorchDispatchModeKey.FUNCTIONAL,
|
||||
torch._C._TorchDispatchModeKey.PROXY,
|
||||
):
|
||||
raise AssertionError(
|
||||
"key must be either FUNCTIONAL or PROXY _TorchDispatchModeKey"
|
||||
)
|
||||
mode_unset = _unset_infra_mode(key)
|
||||
try:
|
||||
yield mode_unset
|
||||
finally:
|
||||
if mode_unset is not None:
|
||||
_push_mode(mode_unset)
|
||||
|
||||
|
||||
def _get_current_dispatch_mode_stack() -> list[TorchDispatchMode]:
|
||||
"""
|
||||
Returns the current stack of dispatch modes, with the most recent
|
||||
(i.e., the one that will be processed first) at the end of the
|
||||
list (standard stack convention).
|
||||
"""
|
||||
stack_len = _len_torch_dispatch_stack()
|
||||
return [_get_dispatch_stack_at(i) for i in range(stack_len)]
|
||||
|
||||
|
||||
def _push_mode(mode: TorchDispatchMode) -> None:
|
||||
k = mode._dispatch_key if hasattr(mode, "_dispatch_key") else None
|
||||
if k is not None and k != torch._C.DispatchKey.PreDispatch:
|
||||
raise AssertionError(
|
||||
"mode._dispatch_key must be None or DispatchKey.PreDispatch"
|
||||
)
|
||||
if k is None:
|
||||
_push_on_torch_dispatch_stack(mode)
|
||||
return
|
||||
|
||||
from torch._ops import _set_mode_pre_dispatch, get_cached_ops
|
||||
|
||||
# See Note [Not Caching Per-Dispatch-Key Mode Handlers]
|
||||
# Clear the cache of every op that has been used so far, for this particular key.
|
||||
ks = torch._C._functionality_to_backend_keys(k)
|
||||
for op in get_cached_ops():
|
||||
for key in ks:
|
||||
op._uncache_dispatch(key)
|
||||
_set_mode_pre_dispatch(mode)
|
||||
|
||||
|
||||
def _pop_mode(k: DispatchKey | torch._C._TorchDispatchModeKey | None = None):
|
||||
if k == torch._C.DispatchKey.PreDispatch: # type: ignore[attr-defined]
|
||||
from torch._ops import _pop_mode_from_pre_dispatch
|
||||
|
||||
return _pop_mode_from_pre_dispatch()
|
||||
|
||||
if k is None or isinstance(k, torch._C._TorchDispatchModeKey):
|
||||
return _pop_torch_dispatch_stack(k)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _pop_mode_temporarily(k: DispatchKey | None = None):
|
||||
old = _pop_mode(k)
|
||||
try:
|
||||
yield old
|
||||
finally:
|
||||
_push_mode(old)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _disable_current_modes():
|
||||
from torch._ops import (
|
||||
_len_torch_dispatch_stack_pre_dispatch,
|
||||
_pop_mode_from_pre_dispatch,
|
||||
)
|
||||
from torch._subclasses.functional_tensor import FunctionalTensorMode
|
||||
from torch._subclasses.schema_check_mode import SchemaCheckMode
|
||||
from torch.fx.experimental.proxy_tensor import ProxyTorchDispatchMode
|
||||
|
||||
mode_len_pre_dispatch = _len_torch_dispatch_stack_pre_dispatch()
|
||||
old_pre_dispatch_modes = [
|
||||
_pop_mode_from_pre_dispatch() for _ in range(mode_len_pre_dispatch)
|
||||
]
|
||||
|
||||
has_proxy_mode_in_pre_dispatch = False
|
||||
has_functional_mode_in_pre_dispatch = False
|
||||
has_schema_check_mode_in_pre_dispatch = False
|
||||
|
||||
for i in old_pre_dispatch_modes:
|
||||
if isinstance(i, ProxyTorchDispatchMode):
|
||||
has_proxy_mode_in_pre_dispatch = True
|
||||
if isinstance(i, FunctionalTensorMode):
|
||||
has_functional_mode_in_pre_dispatch = True
|
||||
if isinstance(i, SchemaCheckMode):
|
||||
has_schema_check_mode_in_pre_dispatch = True
|
||||
|
||||
mode_len = _len_torch_dispatch_stack()
|
||||
old_modes = [_pop_mode() for _ in range(mode_len)]
|
||||
|
||||
for old in old_modes:
|
||||
if (
|
||||
isinstance(old, FunctionalTensorMode)
|
||||
and has_functional_mode_in_pre_dispatch
|
||||
):
|
||||
raise AssertionError(
|
||||
"Can't have FunctionalMode available both in PreDispatch and Python Key"
|
||||
)
|
||||
if isinstance(old, ProxyTorchDispatchMode) and has_proxy_mode_in_pre_dispatch:
|
||||
raise AssertionError(
|
||||
"Can't have ProxyTorchDispatchMode available both in PreDispatch and Python Key"
|
||||
)
|
||||
if isinstance(old, SchemaCheckMode) and has_schema_check_mode_in_pre_dispatch:
|
||||
raise AssertionError(
|
||||
"Can't have SchemaCheckMode available both in PreDispatch and Python Key"
|
||||
)
|
||||
|
||||
# Manually disable proxy and fake modes, if any are active
|
||||
try:
|
||||
yield old_pre_dispatch_modes + old_modes
|
||||
finally:
|
||||
for mode in reversed(old_modes):
|
||||
_push_mode(mode)
|
||||
for mode in reversed(old_pre_dispatch_modes):
|
||||
_push_mode(mode)
|
||||
|
||||
|
||||
class BaseTorchDispatchMode(TorchDispatchMode):
|
||||
def __torch_dispatch__(self, func, types, args=(), kwargs=None):
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
return func(*args, **kwargs)
|
||||
|
||||
|
||||
# Subtypes which have __tensor_flatten__ and __tensor_unflatten__.
|
||||
class TensorWithFlatten(Protocol):
|
||||
def __tensor_flatten__(self) -> tuple[Sequence[str], object]: ...
|
||||
|
||||
@staticmethod
|
||||
def __tensor_unflatten__(
|
||||
inner_tensors: int, flatten_spec: int, outer_size: int, outer_stride: int
|
||||
) -> torch.Tensor: ...
|
||||
|
||||
# It would be really nice to be able to say that the return of
|
||||
# is_traceable_wrapper_subclass() is Intersection[torch.Tensor,
|
||||
# TensorWithFlatten] - but that doesn't exist.
|
||||
|
||||
shape: torch._C.Size
|
||||
|
||||
@overload
|
||||
def stride(self, dim: None = None) -> tuple[int, ...]: ...
|
||||
|
||||
@overload
|
||||
def stride(self, dim: int) -> int: ...
|
||||
|
||||
@overload
|
||||
def size(self, dim: None = None) -> tuple[int, ...]: ...
|
||||
|
||||
@overload
|
||||
def size(self, dim: int) -> int: ...
|
||||
|
||||
def storage_offset(self) -> int: ...
|
||||
|
||||
def dim(self) -> int: ...
|
||||
|
||||
@overload
|
||||
def to(
|
||||
self,
|
||||
dtype: torch.types._dtype,
|
||||
non_blocking: bool = False,
|
||||
copy: bool = False,
|
||||
*,
|
||||
memory_format: torch.memory_format | None = None,
|
||||
) -> torch.Tensor: ...
|
||||
|
||||
@overload
|
||||
def to(
|
||||
self,
|
||||
device: torch._prims_common.DeviceLikeType | None = None,
|
||||
dtype: torch.types._dtype | None = None,
|
||||
non_blocking: bool = False,
|
||||
copy: bool = False,
|
||||
*,
|
||||
memory_format: torch.memory_format | None = None,
|
||||
) -> torch.Tensor: ...
|
||||
|
||||
@overload
|
||||
def to(
|
||||
self,
|
||||
other: torch.Tensor,
|
||||
non_blocking: bool = False,
|
||||
copy: bool = False,
|
||||
*,
|
||||
memory_format: torch.memory_format | None = None,
|
||||
) -> torch.Tensor: ...
|
||||
|
||||
|
||||
def is_traceable_wrapper_subclass(t: object) -> TypeIs[TensorWithFlatten]:
|
||||
"""
|
||||
Returns whether or not a tensor subclass that implements __torch_dispatch__
|
||||
is 'traceable' with torch.compile.
|
||||
In order for a tensor subclass to support TorchDispatchMode-style tracing in PT2,
|
||||
It must implement two magic methods: __tensor_flatten__ and __tensor_unflatten__.
|
||||
It is also expected to obey some restrictions around traceability and aliasing:
|
||||
* The subclass's __torch_dispatch__() implementation should desugar into pytorch
|
||||
dispatcher operations that can be traced into a graph.
|
||||
* The subclass should use return_and_correct_aliasing(). This is needed today to make
|
||||
sure that torch.compile does the right thing in a few cases around input mutation
|
||||
and output aliasing.
|
||||
|
||||
Expected magic method signatures:
|
||||
attrs, ctx = t.__tensor_flatten__()
|
||||
attrs: list of attribute name strings for inner tensors
|
||||
ctx: dict containing any other subclass-specific metadata needed for unflattening
|
||||
|
||||
t = MySubClass.__tensor_unflatten__(inner_tensors, ctx, outer_size, outer_stride)
|
||||
inner_tensors: dict mapping attribute name -> tensor for each inner tensor
|
||||
ctx: dict with subclass metadata in the form that __tensor_flatten__() produces
|
||||
outer_size: expected (possibly symbolic) size that the returned subclass
|
||||
instance should have. Note that this arg is useful for certain subclasses
|
||||
that require the shape info to be constructed. In most cases, this arg can be
|
||||
safely ignored.
|
||||
outer_stride: expected (possibly symbolic) stride that the returned subclass
|
||||
instance should have. Note that this arg is useful for certain subclasses
|
||||
that require the stride info to be constructed. In most cases, this arg can be
|
||||
safely ignored.
|
||||
"""
|
||||
is_subclass = isinstance(t, torch.Tensor) and type(t) is not torch.Tensor
|
||||
return (
|
||||
is_subclass
|
||||
and hasattr(t, "__tensor_flatten__")
|
||||
and hasattr(t, "__tensor_unflatten__")
|
||||
)
|
||||
|
||||
|
||||
def is_traceable_wrapper_subclass_type(t: type) -> TypeIs[type[TensorWithFlatten]]:
|
||||
"""Same as above, but takes a type argument instead of an instance."""
|
||||
return (
|
||||
issubclass(t, torch.Tensor)
|
||||
and t is not torch.Tensor
|
||||
and hasattr(t, "__tensor_flatten__")
|
||||
and hasattr(t, "__tensor_unflatten__")
|
||||
)
|
||||
|
||||
|
||||
def transform_subclass(t, callback, outer_size=None, outer_stride=None):
|
||||
"""
|
||||
Given a traceable, wrapper tensor subclass ``t`` that implements
|
||||
``__torch_dispatch__`` and holds some inner tensors,
|
||||
and a callback of type ``Callable[[str, torch.Tensor], torch.Tensor]``,
|
||||
`transform_subclass` will construct a fresh instance of the wrapper tensor subclass.
|
||||
It will do so by grabbing each inner tensor attribute from the wrapper,
|
||||
passing them into ``callback`` to get a transformed tensor,
|
||||
and putting each transformed tensor into the fresh tensor subclass instance.
|
||||
|
||||
Note: this function will not handle ensuring that the fresh subclass
|
||||
gets the same (autograd, and aliasing) metadata as the original tensor.
|
||||
This is generally handled in other subsystems like AOTAutograd.
|
||||
"""
|
||||
outer_size = outer_size if outer_size is not None else t.size()
|
||||
outer_stride = outer_stride if outer_stride is not None else t.stride()
|
||||
|
||||
attrs, ctx = t.__tensor_flatten__()
|
||||
transformed_tensors_dict = {}
|
||||
for attr in attrs:
|
||||
transformed_tensors_dict[attr] = callback(attr, getattr(t, attr))
|
||||
sub = type(t).__tensor_unflatten__(
|
||||
transformed_tensors_dict, ctx, outer_size, outer_stride
|
||||
)
|
||||
|
||||
# NB: Purposefully guard here to simplify the inner / outer symbols.
|
||||
# Using sym_eq() for symbolic comparison can result in an expression that's too
|
||||
# difficult to guard on, so we use == here.
|
||||
if sub.shape != outer_size:
|
||||
raise AssertionError(
|
||||
f"Expected return value from {type(t)}__tensor_unflatten__() to have "
|
||||
f"shape equal to {outer_size}, but got: {sub.shape}"
|
||||
)
|
||||
if sub.stride() != outer_stride:
|
||||
raise AssertionError(
|
||||
f"Expected return value from {type(t)}__tensor_unflatten__() to have "
|
||||
f"stride equal to {outer_stride}, but got: {sub.stride()}"
|
||||
)
|
||||
|
||||
return sub
|
||||
|
||||
|
||||
def _correct_storage_aliasing(func, schema_info, args, outs) -> None:
|
||||
"""
|
||||
Given: an OpOverload, a SchemaInfo (cached information from torchgen about schema),
|
||||
and the inputs/outputs to the OpOverload,
|
||||
this function checks to see if func is a view operator
|
||||
(by checking if any of the outputs in the op's schema
|
||||
are immutable aliases of inputs).
|
||||
If so, this function manually aliases the storage of the output tensor
|
||||
with its corresponding input tensor alias.
|
||||
It does this by unsafely overwriting the storage field of the output tensor
|
||||
to be the same storage as the input.
|
||||
"""
|
||||
if not isinstance(func, torch._ops.OpOverload):
|
||||
raise AssertionError(f"func must be an OpOverload, got {type(args)}")
|
||||
if not isinstance(args, tuple):
|
||||
raise AssertionError(f"args must be a tuple, got {type(args)}")
|
||||
if not isinstance(outs, (list, tuple)):
|
||||
raise AssertionError(f"outs must be a list or tuple, got {type(args)}")
|
||||
|
||||
def alias_non_inplace_storage(arg, ret) -> None:
|
||||
# This is hopefully a reasonable assert:
|
||||
# subclasses that rely on this API for output aliasing
|
||||
# should always return wrapper tensor subclasses for us to manually alias.
|
||||
# in theory if a subclass that needs this API wants to sometimes return
|
||||
# plain tensors, we could remove the assert and just not perform the aliasing,
|
||||
# but it seems safer to learn more about this case first.
|
||||
#
|
||||
# Performance note: This is all just to assert that the argument and result
|
||||
# types match, checking that is cheaper than is_traceable_wrapper_subclass_type,
|
||||
# and multiple returns are relatively unlikely, so just check up front!
|
||||
arg_type = type(arg)
|
||||
ret_type = type(ret)
|
||||
if arg_type is not ret_type and (
|
||||
is_traceable_wrapper_subclass_type(arg_type)
|
||||
or is_traceable_wrapper_subclass_type(ret_type)
|
||||
):
|
||||
ret_list = ret if isinstance(ret, list) else [ret]
|
||||
for r in ret_list:
|
||||
if type(arg) is not type(r):
|
||||
raise AssertionError(
|
||||
f"Called {str(func)} with input of type {type(arg)}\n"
|
||||
f"and output of type {type(ret)}. But expected types to match."
|
||||
)
|
||||
# Need to call a non-dispatcher helper, because we explicitly do **not**
|
||||
# want our subclass to intercept the set_() call.
|
||||
# instead, our subclass should directly have its storage swapped out.
|
||||
# we **explicitly** don't want to reset the sizes on ret, if the storage implies a size change.
|
||||
# Why?
|
||||
# The purpose of this API is *not* to change the size/strides of our output- we assume it's already correct.
|
||||
# We just want to "fix up" the storage aliasing, without modifying or output's metadata.
|
||||
# Example: out = inp.expand(inp.shape[0], inp.shape[0])
|
||||
# This requires swapping the storage of out to be the same as inp,
|
||||
# but we do *not* want it to change the sizes/strides that were compute for out.
|
||||
|
||||
if isinstance(ret, list):
|
||||
for r in ret:
|
||||
torch._functionalize_unsafe_set(r, arg)
|
||||
else:
|
||||
if not isinstance(ret, torch.Tensor):
|
||||
raise AssertionError(f"expected torch.Tensor, got {type(ret)}")
|
||||
torch._functionalize_unsafe_set(ret, arg)
|
||||
|
||||
for arg_idx, return_idx in schema_info.read_only_alias_match_indexes:
|
||||
alias_non_inplace_storage(args[arg_idx], outs[return_idx])
|
||||
|
||||
|
||||
def _get_write_alias(x) -> str | None:
|
||||
alias_set = x.alias_set
|
||||
if not alias_set or not x.is_write:
|
||||
return None
|
||||
# torchscript allows for complicated alias sets, but our dispatcher ops only really involve simple aliasing
|
||||
if len(alias_set) != 1:
|
||||
raise AssertionError("Expected alias_set to contain exactly one element")
|
||||
# timeit says next(iter(alias_set)) is faster than list(alias_set)[0] even for
|
||||
# set of size 1 on Python 3.13.
|
||||
return next(iter(alias_set))
|
||||
|
||||
|
||||
# This abstracts over the fact that in return_and_correct_aliasing,
|
||||
# we sometimes use torchgen schema parsing (for aten ops, since torchscript's schema parsing is sometimes buggy),
|
||||
# and sometimes use torchscript schema parsing (for custom ops, for which torchgen parsing is untested).
|
||||
@dataclass
|
||||
class AliasInfo:
|
||||
alias_set: set[str]
|
||||
is_write: bool
|
||||
name: str | None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SchemaInfo:
|
||||
args: list[AliasInfo]
|
||||
outs: list[AliasInfo]
|
||||
|
||||
is_inplace_view_op: bool
|
||||
|
||||
# [_get_write_alias(x) for x in outs]. Guaranteed to contain no Nones; we coerce
|
||||
# all-Nones result to empty list instead, and we don't support
|
||||
# some-but-not-all-Nones.
|
||||
outs_write_aliases: list[str] | None
|
||||
|
||||
# List of (arg_idx, return_idx) where args[arg_idx].alias_set &
|
||||
# outs[out_idx].alias_set is not empty, and not args[arg_idx].is_write.
|
||||
read_only_alias_match_indexes: list[tuple[int, int]]
|
||||
|
||||
|
||||
# Given an OpOverload, returns schema information on it.
|
||||
# This is cached for efficiency, since it can involve running torchgen
|
||||
@functools.cache
|
||||
def get_alias_info(func) -> SchemaInfo:
|
||||
# For ATen ops: use torchgen (since torchscript parser doesn't handle alias annotations
|
||||
# properly for some ops that output tensorlists)
|
||||
if func.namespace == "aten":
|
||||
torchgen_schema_str = str(func._schema)
|
||||
if not torchgen_schema_str.startswith("aten::"):
|
||||
raise AssertionError(
|
||||
"Expected torchgen schema string to start with 'aten::'"
|
||||
)
|
||||
# remove the aten:: namespace, which is added by the torchscript parser,
|
||||
# and torchgen doesn't know how to handle
|
||||
torchgen_schema_str = torchgen_schema_str[6:]
|
||||
import re
|
||||
|
||||
# the torchscript parser ends up converting int[2]=1 into int[2]=[1, 1],
|
||||
# which torchgen chokes on.
|
||||
torchgen_schema_str = re.sub(r"=\[[0, ]+\]", "=0", torchgen_schema_str)
|
||||
torchgen_schema_str = re.sub(r"=\[[1, ]+\]", "=1", torchgen_schema_str)
|
||||
# for aten::rot90 / aten:fft_*
|
||||
torchgen_schema_str = re.sub(
|
||||
r"=\[(-?[0-9]+), (-?[0-9]+)\]", r"=[\1,\2]", torchgen_schema_str
|
||||
)
|
||||
torchgen_schema = torchgen.model.FunctionSchema.parse(torchgen_schema_str)
|
||||
arg_schemas = [
|
||||
AliasInfo(
|
||||
alias_set=(
|
||||
set() if a.annotation is None else set(a.annotation.alias_set)
|
||||
),
|
||||
is_write=a.annotation is not None and a.annotation.is_write,
|
||||
name=a.name,
|
||||
)
|
||||
for a in torchgen_schema.arguments.flat_all
|
||||
]
|
||||
out_schemas = [
|
||||
AliasInfo(
|
||||
alias_set=(
|
||||
set() if a.annotation is None else set(a.annotation.alias_set)
|
||||
),
|
||||
is_write=a.annotation is not None and a.annotation.is_write,
|
||||
name=a.name,
|
||||
)
|
||||
for a in torchgen_schema.returns
|
||||
]
|
||||
else:
|
||||
# For non-aten ops, torchgen is untested so we rely on torchscript schema parsing
|
||||
arg_schemas = [
|
||||
AliasInfo(
|
||||
alias_set=(
|
||||
set() if a.alias_info is None else set(a.alias_info.before_set)
|
||||
),
|
||||
is_write=a.alias_info is not None and a.alias_info.is_write,
|
||||
name=a.name,
|
||||
)
|
||||
for a in func._schema.arguments
|
||||
]
|
||||
out_schemas = [
|
||||
AliasInfo(
|
||||
alias_set=(
|
||||
set() if a.alias_info is None else set(a.alias_info.before_set)
|
||||
),
|
||||
is_write=a.alias_info is not None and a.alias_info.is_write,
|
||||
name=a.name,
|
||||
)
|
||||
for a in func._schema.returns
|
||||
]
|
||||
read_only_alias_match_indexes = []
|
||||
for arg_idx, schema_arg in enumerate(arg_schemas):
|
||||
for return_idx, schema_out in enumerate(out_schemas):
|
||||
is_read_only_alias_match = (
|
||||
schema_arg.alias_set & schema_out.alias_set
|
||||
) and not schema_arg.is_write
|
||||
if is_read_only_alias_match:
|
||||
read_only_alias_match_indexes.append((arg_idx, return_idx))
|
||||
|
||||
outs_write_aliases_list: list[str | None] = [
|
||||
_get_write_alias(r) for r in out_schemas
|
||||
]
|
||||
non_nones = sum(x is not None for x in outs_write_aliases_list)
|
||||
if non_nones == 0:
|
||||
outs_write_aliases: list[str] | None = None
|
||||
elif non_nones != len(outs_write_aliases_list):
|
||||
# simplifying assumption: we don't have **any** ops with return types like "-> (Tensor(a!), Tensor)"
|
||||
raise RuntimeError("Unsupported schema: " + str(func._schema))
|
||||
else:
|
||||
outs_write_aliases = cast(list[str], outs_write_aliases_list)
|
||||
|
||||
schema_info = SchemaInfo(
|
||||
args=arg_schemas,
|
||||
outs=out_schemas,
|
||||
# This check is surprisingly expensive because pybind11 enum_s are
|
||||
# inefficient. Just cache it.
|
||||
is_inplace_view_op=torch.Tag.inplace_view in func.tags,
|
||||
outs_write_aliases=outs_write_aliases,
|
||||
read_only_alias_match_indexes=read_only_alias_match_indexes,
|
||||
)
|
||||
return schema_info
|
||||
|
||||
|
||||
def autograd_would_have_decomposed(
|
||||
func: torch._ops.OpOverload, flat_args: Sequence[torch.Tensor | object]
|
||||
) -> bool:
|
||||
"""
|
||||
Suppose that an operator has CompositeImplicitAutograd decomp registered.
|
||||
Would autograd have used this decomposition? It will only use it if there
|
||||
isn't an explicit backend registration for the device as well. This function
|
||||
will tell if this would have occurred.
|
||||
|
||||
Why do we need to apply these decompositions later? When inference mode is
|
||||
on, the autograd key is bypassed entirely, so a lower level mode cannot rely
|
||||
on the decomposition have been applied. It's easy to accidentally never apply
|
||||
the decomposition, resulting in an operator showing up in a graph that
|
||||
is unexpected.
|
||||
|
||||
Why do we need to AVOID applying the decomposition when autograd wouldn't
|
||||
have decomposed? If autograd doesn't decompose, this means in eager mode
|
||||
we would have run the fused kernel. It must be possible to trace this
|
||||
fused kernel directly into the graph for fidelity with eager (NB: a user
|
||||
has the option of then further decomposing at proxy tensor mode via
|
||||
decomposition table, but we must preserve it to proxy mode to have the
|
||||
choice.)
|
||||
|
||||
Why does functionalization need to also perform the test here? This is
|
||||
because some CompositeImplicitAutograd decompositions are not functional.
|
||||
If we are eventually going to decompose, we need to do this while we can
|
||||
still turn functionalization back on, so those decompositions get functionalized.
|
||||
So an early decomposition in functionalization may still be necessary. Note that
|
||||
if proxy tensor decomposition process could turn functionalization back on, this
|
||||
wouldn't be necessary, and maybe that is a useful thing to do anyway because
|
||||
the decomposition table is user specified and a user could violate the functional
|
||||
decomp requirement with a bad decomp. If this happened, then you could always
|
||||
pass through functionalization.
|
||||
"""
|
||||
has_backend_registration = False
|
||||
for a in flat_args:
|
||||
if isinstance(a, torch.Tensor):
|
||||
backend_key = torch._C._parse_dispatch_key(
|
||||
torch._C._dispatch_key_for_device(a.device.type)
|
||||
)
|
||||
if backend_key is None:
|
||||
raise AssertionError(
|
||||
f"failed to parse dispatch key for device {a.device.type}"
|
||||
)
|
||||
# TODO: use func.has_kernel_for_dispatch_key(backend_key)
|
||||
# but this one checks py_impl and CompositeImplicitAutograd
|
||||
# incorrectly shows up as has backend reg here
|
||||
has_backend_registration = torch._C._dispatch_has_kernel_for_dispatch_key(
|
||||
func.name(), backend_key
|
||||
)
|
||||
|
||||
# in theory we should take all backend keys and take the highest priority one
|
||||
# to properly mimic the dispatcher,
|
||||
# this just grabs the first tensor and takes its device key
|
||||
break
|
||||
return not has_backend_registration
|
||||
|
||||
|
||||
def return_and_correct_aliasing(func, args, kwargs, out):
|
||||
"""
|
||||
This function should be used by wrapper tensor ``__torch_dispatch__`` subclasses
|
||||
that would like to work with torch.compile. It ensures that the subclass
|
||||
properly implements the aliasing behavior of every op,
|
||||
which is needed for correctness in AOTAutograd.
|
||||
This function will handle:
|
||||
|
||||
* When we see a view op, we will alias the storages of any
|
||||
input and output tensor subclasses
|
||||
|
||||
* When we see an inplace or out= op, we will directly
|
||||
return the corresponding input tensor, instead of returning
|
||||
a (potentially) fresh output tensor.
|
||||
"""
|
||||
|
||||
# Caching here because torchgen parsing is definitely not fast, and this function is called
|
||||
# once for every op in the graph during functionalization.
|
||||
schema_info = get_alias_info(func)
|
||||
|
||||
def get_arg_from_alias(output_alias, schema_info, args, kwargs):
|
||||
new_args, new_kwargs = torch.fx.operator_schemas.normalize_function( # type: ignore[misc]
|
||||
func, args=args, kwargs=kwargs
|
||||
)
|
||||
|
||||
arg_indices = [
|
||||
i for i, a in enumerate(schema_info.args) if output_alias in a.alias_set
|
||||
]
|
||||
# For any dispatcher op with an output alias, we expect it to map to exactly one alias in the schema's input arguments.
|
||||
if len(arg_indices) != 1:
|
||||
raise AssertionError(
|
||||
"Expected exactly one argument index for the given output alias"
|
||||
)
|
||||
idx = arg_indices[0]
|
||||
arg_info = schema_info.args[idx]
|
||||
if arg_info.name is not None and arg_info.name in new_kwargs:
|
||||
return new_kwargs[arg_info.name]
|
||||
return new_args[idx]
|
||||
|
||||
# Fix up the storages of any outs so that they point to the same storage as the input,
|
||||
# if func is a view op.
|
||||
_correct_storage_aliasing(
|
||||
func, schema_info, args, (out,) if not isinstance(out, tuple) else out
|
||||
)
|
||||
|
||||
# For inplace_view ops in particular, we'll try hard to make sure that the wrapper subclass's
|
||||
# metadata is set correctly.
|
||||
if schema_info.is_inplace_view_op:
|
||||
# no_dispatch() to make sure that we secretly change the metadata on the wrapper,
|
||||
# but don't end up dispatching the op anywhere else.
|
||||
mutated_args = [
|
||||
x
|
||||
for i, x in enumerate(args)
|
||||
if _get_write_alias(schema_info.args[i]) is not None
|
||||
]
|
||||
# Assumption: we have a very small number of inplace_view ops that follow a strict schema:
|
||||
# there is only a single argument that gets its metadata mutated.
|
||||
if len(mutated_args) != 1:
|
||||
raise AssertionError(
|
||||
"expected exactly one mutated arg for inplace_view ops"
|
||||
)
|
||||
# This check exists because we generally *do* want to update the metadata of any wrapper subclasses,
|
||||
# but FunctionalTensor is special: it overrides all size/stride calls to plumb to the inner tensor.
|
||||
# so we don't actually need to update the metadata (and attempting to do so causes errors)
|
||||
from torch._subclasses.functional_tensor import FunctionalTensor
|
||||
|
||||
if not isinstance(mutated_args[0], FunctionalTensor):
|
||||
with torch.utils._mode_utils.no_dispatch():
|
||||
# See Note: [Fake Tensor Dispatch Keys]
|
||||
# we're borrowing the way it modifies dispatch key TLS.
|
||||
meta_in_tls = torch._C._meta_in_tls_dispatch_include()
|
||||
torch._C._set_meta_in_tls_dispatch_include(True)
|
||||
try:
|
||||
func(*args, **kwargs)
|
||||
finally:
|
||||
torch._C._set_meta_in_tls_dispatch_include(meta_in_tls)
|
||||
|
||||
# Next: we need to make sure to return inputs directly, if the output is a mutable alias (e.g. add_()).
|
||||
|
||||
schema_info_outs_write_aliases = schema_info.outs_write_aliases
|
||||
# simple case: none of our outputs have mutable aliases, so we can return the output as-is
|
||||
if schema_info_outs_write_aliases is None:
|
||||
return out
|
||||
|
||||
if len(schema_info_outs_write_aliases) == 1:
|
||||
return get_arg_from_alias(
|
||||
schema_info_outs_write_aliases[0], schema_info, args, kwargs
|
||||
)
|
||||
|
||||
# In the multi-return case, all aten ops return a tuple / list, so cast accordingly.
|
||||
outs_to_return = type(out)(
|
||||
[
|
||||
(get_arg_from_alias(write_alias, schema_info, args, kwargs))
|
||||
for write_alias in schema_info_outs_write_aliases
|
||||
]
|
||||
)
|
||||
return outs_to_return
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,151 @@
|
||||
import torch
|
||||
from torch._inductor.utils import get_device_tflops, get_gpu_dram_gbps
|
||||
from torch.fx.experimental.symbolic_shapes import (
|
||||
optimization_hint,
|
||||
statically_known_true,
|
||||
)
|
||||
from torch.utils._ordered_set import OrderedSet
|
||||
|
||||
from .flop_counter import flop_registry
|
||||
|
||||
|
||||
aten = torch.ops.aten
|
||||
|
||||
_FLOAT_TYPES = OrderedSet(
|
||||
[
|
||||
torch.float16,
|
||||
torch.bfloat16,
|
||||
torch.float32,
|
||||
torch.float64,
|
||||
]
|
||||
)
|
||||
|
||||
# No fall-back kernel needed/exists for view ops
|
||||
_VIEW_OPS = OrderedSet(
|
||||
[
|
||||
aten.lift_fresh,
|
||||
aten.t,
|
||||
aten.transpose,
|
||||
aten.view,
|
||||
aten.detach,
|
||||
aten._unsafe_view,
|
||||
aten.split,
|
||||
aten.adjoint,
|
||||
aten.as_strided,
|
||||
aten.diagonal,
|
||||
aten.expand,
|
||||
aten.expand_as,
|
||||
aten.movedim,
|
||||
aten.permute,
|
||||
aten.select,
|
||||
aten.squeeze,
|
||||
aten.mT,
|
||||
aten.mH,
|
||||
aten.real,
|
||||
aten.imag,
|
||||
aten.view_as,
|
||||
aten.unflatten,
|
||||
aten.unfold,
|
||||
aten.unbind,
|
||||
aten.unsqueeze,
|
||||
aten.vsplit,
|
||||
aten.hsplit,
|
||||
aten.split_with_sizes,
|
||||
aten.swapaxes,
|
||||
aten.swapdims,
|
||||
aten.chunk,
|
||||
]
|
||||
)
|
||||
# We can ignore benchmarking tensor create ops
|
||||
_CREATE_OPS = OrderedSet(
|
||||
[
|
||||
aten.randint,
|
||||
aten.randn,
|
||||
aten.rand,
|
||||
aten.randn_like,
|
||||
aten.rand_like,
|
||||
aten.randint_like,
|
||||
aten.arange,
|
||||
aten.ones_like,
|
||||
aten.zeros_like,
|
||||
]
|
||||
)
|
||||
|
||||
_IGNORE_OPS = _VIEW_OPS | _CREATE_OPS
|
||||
|
||||
|
||||
def get_compute_time(func_packet, args, kwargs, out, out_dtypes) -> float: # type: ignore[no-untyped-def]
|
||||
"""
|
||||
Estimates the compute time of an aten operator.
|
||||
|
||||
Args:
|
||||
func_packet: The operator overload packet.
|
||||
args: The arguments to the operator.
|
||||
kwargs: The keyword arguments to the operator.
|
||||
out: The output of the operator.
|
||||
out_dtypes: The output data types.
|
||||
|
||||
Returns:
|
||||
float: The estimated compute time in nanoseconds.
|
||||
"""
|
||||
if func_packet in flop_registry:
|
||||
if len(out_dtypes) != 1:
|
||||
raise AssertionError(
|
||||
f"Only support single out dtype got {out_dtypes} for {func_packet}"
|
||||
)
|
||||
dtype = out_dtypes.pop()
|
||||
# This actually gives peta-FLOPs/s hence multiply by 1e15 to get the FLOPs/s
|
||||
peak_gpu_flops = get_device_tflops(dtype) * 1e15
|
||||
# We can expect to achieve 75% of theoretical peak flops
|
||||
factor = 0.75
|
||||
peak_empirical_flops = factor * peak_gpu_flops
|
||||
flop_count_func = flop_registry[func_packet]
|
||||
# We divide by a factor of 2 to get the MACs (multiply and accumulate)
|
||||
flop_count = flop_count_func(*args, **kwargs, out_val=out) / 2
|
||||
# We multiply by 1e9 to get the time in nano seconds
|
||||
compute_time = (flop_count / peak_empirical_flops) * 1e9
|
||||
return compute_time
|
||||
return 0.0
|
||||
|
||||
|
||||
def get_num_bytes(t: torch.Tensor) -> int:
|
||||
"""
|
||||
Calculates the memory consumption of a tensor.
|
||||
|
||||
Args:
|
||||
t (torch.Tensor): The input tensor.
|
||||
|
||||
Returns:
|
||||
int: The memory consumption of the tensor in bytes.
|
||||
"""
|
||||
real_numel = 1
|
||||
for size, stride in zip(t.shape, t.stride()):
|
||||
# For dims with stride=0 (expanded/broadcast), only 1 element accessed
|
||||
if not statically_known_true(stride == 0):
|
||||
real_numel *= optimization_hint(size, fallback=0)
|
||||
|
||||
return real_numel * t.element_size()
|
||||
|
||||
|
||||
def get_transfer_time(flat_args_kwargs, flat_outs) -> float: # type: ignore[no-untyped-def]
|
||||
"""
|
||||
Estimates the memory transfer time of input and output tensors.
|
||||
|
||||
Args:
|
||||
flat_args_kwargs (List[torch.Tensor]): The flat list of arguments and keyword arguments.
|
||||
flat_outs (List[torch.Tensor]): The flat list of outputs.
|
||||
|
||||
Returns:
|
||||
float: The estimated memory transfer time in nanoseconds.
|
||||
"""
|
||||
gpu_memory_bandwidth = get_gpu_dram_gbps()
|
||||
read_bytes = sum(
|
||||
get_num_bytes(t) for t in flat_args_kwargs if isinstance(t, torch.Tensor)
|
||||
)
|
||||
write_bytes = sum(
|
||||
get_num_bytes(t) for t in flat_outs if isinstance(t, torch.Tensor)
|
||||
)
|
||||
counted_bytes = read_bytes + write_bytes
|
||||
# The GPU memory bandwidth is in GB/s so the transfer time is in nanoseconds
|
||||
transfer_time = counted_bytes / gpu_memory_bandwidth
|
||||
return transfer_time
|
||||
@@ -0,0 +1,31 @@
|
||||
# NOTE! PLEASE KEEP THIS FILE *FREE* OF TORCH DEPS! IT SHOULD BE IMPORTABLE ANYWHERE.
|
||||
# IF YOU FEEL AN OVERWHELMING URGE TO ADD A TORCH DEP, MAKE A TRAMPOLINE FILE A LA torch._dynamo.utils
|
||||
# AND SCRUB AWAY TORCH NOTIONS THERE.
|
||||
import collections
|
||||
import functools
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Callable
|
||||
from typing import TypeVar
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
|
||||
simple_call_counter: OrderedDict[str, int] = collections.OrderedDict()
|
||||
|
||||
_P = ParamSpec("_P")
|
||||
_R = TypeVar("_R")
|
||||
|
||||
|
||||
def count_label(label: str) -> None:
|
||||
prev = simple_call_counter.setdefault(label, 0)
|
||||
simple_call_counter[label] = prev + 1
|
||||
|
||||
|
||||
def count(fn: Callable[_P, _R]) -> Callable[_P, _R]:
|
||||
@functools.wraps(fn)
|
||||
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
||||
if fn.__qualname__ not in simple_call_counter:
|
||||
simple_call_counter[fn.__qualname__] = 0
|
||||
simple_call_counter[fn.__qualname__] = simple_call_counter[fn.__qualname__] + 1
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
+313
@@ -0,0 +1,313 @@
|
||||
# mypy: disallow-untyped-defs
|
||||
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from collections.abc import Callable, Sequence
|
||||
from threading import Lock
|
||||
from typing import Any, TypeVar
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
|
||||
logger = logging.getLogger("strobelight_function_profiler")
|
||||
|
||||
console_handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter(
|
||||
"%(name)s, line %(lineno)d, %(asctime)s, %(levelname)s: %(message)s"
|
||||
)
|
||||
console_handler.setFormatter(formatter)
|
||||
|
||||
logger.addHandler(console_handler)
|
||||
logger.setLevel(logging.INFO)
|
||||
logger.propagate = False
|
||||
|
||||
_P = ParamSpec("_P")
|
||||
_R = TypeVar("_R")
|
||||
|
||||
|
||||
class StrobelightCLIProfilerError(Exception):
|
||||
"""
|
||||
Raised when an error happens during strobelight profiling
|
||||
"""
|
||||
|
||||
|
||||
def _pid_namespace_link(pid: int | None = None) -> str:
|
||||
"""Returns the link to the process's namespace, example: pid:[4026531836]"""
|
||||
PID_NAMESPACE_PATH = "/proc/{}/ns/pid"
|
||||
pid = pid or os.getpid()
|
||||
return os.readlink(PID_NAMESPACE_PATH.format(pid))
|
||||
|
||||
|
||||
def _pid_namespace(pid: int | None = None) -> int:
|
||||
"""Returns the process's namespace id"""
|
||||
pid = pid or os.getpid()
|
||||
link = _pid_namespace_link(pid)
|
||||
return int(link[link.find("[") + 1 : -1])
|
||||
|
||||
|
||||
def _command_to_string(command: Sequence[str]) -> str:
|
||||
return " ".join(command)
|
||||
|
||||
|
||||
class StrobelightCLIFunctionProfiler:
|
||||
"""
|
||||
Note: this is a meta only tool.
|
||||
|
||||
StrobelightCLIFunctionProfiler can be used to profile a python function and
|
||||
generate a strobelight link with the results. It works on meta servers but
|
||||
does not requires an fbcode target.
|
||||
When stop_at_error is false(default), error during profiling does not prevent
|
||||
the work function from running.
|
||||
|
||||
Check function_profiler_example.py for an example.
|
||||
"""
|
||||
|
||||
# This lock is used to make sure only one thread is running the profiler at any point.
|
||||
_lock = Lock()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
stop_at_error: bool = False,
|
||||
max_profile_duration_sec: int = 60 * 10,
|
||||
sample_each: float = 1e7, # sample each sample_each cycles.
|
||||
run_user_name: str = "pytorch-strobelight-ondemand",
|
||||
timeout_wait_for_running_sec: int = 60,
|
||||
timeout_wait_for_finished_sec: int = 60,
|
||||
recorded_env_variables: list[str] | None = None,
|
||||
sample_tags: list[str] | None = None,
|
||||
stack_max_len: int = 127,
|
||||
async_stack_max_len: int = 127,
|
||||
) -> None:
|
||||
self.stop_at_error = stop_at_error
|
||||
self.max_profile_duration_sec = max_profile_duration_sec
|
||||
self.sample_each = sample_each
|
||||
self.run_user_name = run_user_name
|
||||
self.timeout_wait_for_running_sec = timeout_wait_for_running_sec
|
||||
self.timeout_wait_for_finished_sec = timeout_wait_for_finished_sec
|
||||
# Results of the most recent run.
|
||||
# Tracks the strobelight run id of the most recent run
|
||||
self.current_run_id: int | None = None
|
||||
self.sample_tags = sample_tags
|
||||
|
||||
def _run_async(self) -> None:
|
||||
processId = os.getpid()
|
||||
namespace = _pid_namespace(processId)
|
||||
command = [
|
||||
"strobeclient",
|
||||
"run",
|
||||
"--profiler",
|
||||
"pyperf",
|
||||
"--event",
|
||||
"cycles",
|
||||
"--async",
|
||||
"--sample-interval",
|
||||
f"{int(self.sample_each)}",
|
||||
"--duration-ms",
|
||||
f"{int(self.max_profile_duration_sec * 1000)}",
|
||||
"--pid",
|
||||
f"{namespace}:{processId}",
|
||||
]
|
||||
|
||||
if self.sample_tags:
|
||||
command.append("--sample-tags")
|
||||
command.append(",".join(self.sample_tags))
|
||||
|
||||
logger.debug("running command: %s", _command_to_string(command))
|
||||
result = subprocess.run(command, capture_output=True)
|
||||
output = result.stderr.decode("utf-8")
|
||||
logger.debug("output:\n{%s}", output)
|
||||
|
||||
if result.returncode != 0:
|
||||
raise StrobelightCLIProfilerError(
|
||||
f"failed to start strobelight profiling, error in run_async:{output}"
|
||||
)
|
||||
|
||||
if match := re.search(r"INFO Run Id: (-?\d+)", output):
|
||||
self.current_run_id = int(match.group(1))
|
||||
return
|
||||
|
||||
raise StrobelightCLIProfilerError(
|
||||
f"failed to start strobelight profiling, unexpected result {output}"
|
||||
)
|
||||
|
||||
def _wait_for_running(self, counter: int = 0) -> None:
|
||||
if counter > 20:
|
||||
raise StrobelightCLIProfilerError(
|
||||
"wait_for_running called more than 20 times"
|
||||
)
|
||||
|
||||
command = ["strobeclient", "getRunStatus", "--run-id", f"{self.current_run_id}"]
|
||||
logger.debug("running command: %s", _command_to_string(command))
|
||||
result = subprocess.run(command, capture_output=True)
|
||||
output = result.stderr.decode("utf-8")
|
||||
logger.debug("output:\n{%s}", output)
|
||||
|
||||
if result.returncode != 0:
|
||||
raise StrobelightCLIProfilerError(
|
||||
f"failed to start strobelight profiling, error in wait_for_running:{output}"
|
||||
)
|
||||
|
||||
if match := re.search("Profile run status: (.*)", output):
|
||||
current_status = match.group(1)
|
||||
if current_status == "RUNNING":
|
||||
return
|
||||
elif current_status == "PREPARING":
|
||||
time.sleep(10)
|
||||
self._wait_for_running(counter + 1)
|
||||
return
|
||||
else:
|
||||
raise StrobelightCLIProfilerError(f"unexpected {current_status} phase")
|
||||
|
||||
raise StrobelightCLIProfilerError(f"unexpected output\n: {output} ")
|
||||
|
||||
def _stop_run(self) -> None:
|
||||
command = ["strobeclient", "stopRun", "--run-id", str(self.current_run_id)]
|
||||
logger.debug("running command: %s", _command_to_string(command))
|
||||
result = subprocess.run(command, capture_output=True)
|
||||
output = result.stderr.decode("utf-8")
|
||||
logger.debug("output:\n{%s}", output)
|
||||
|
||||
if result.returncode != 0:
|
||||
raise StrobelightCLIProfilerError(
|
||||
f"failed to stop strobelight profiling, return code is not 0 :{output}"
|
||||
)
|
||||
|
||||
if match := re.search("INFO ::1:(.*)", output):
|
||||
current_status = match.group(1)
|
||||
if current_status.__contains__("Success!"):
|
||||
return
|
||||
else:
|
||||
raise StrobelightCLIProfilerError(
|
||||
f"failed to stop strobelight profiling, got {current_status} result"
|
||||
)
|
||||
|
||||
raise StrobelightCLIProfilerError(f"unexpected output\n: {output} ")
|
||||
|
||||
def _get_results(self) -> None:
|
||||
command = ["strobeclient", "getRunStatus", "--run-id", str(self.current_run_id)]
|
||||
logger.debug("running command: %s", _command_to_string(command))
|
||||
result = subprocess.run(command, capture_output=True)
|
||||
output = result.stderr.decode("utf-8")
|
||||
logger.debug("output:\n{%s}", output)
|
||||
|
||||
if result.returncode != 0:
|
||||
raise StrobelightCLIProfilerError(
|
||||
f"failed to extract profiling results, return code is not 0 : {output}"
|
||||
)
|
||||
|
||||
if match := re.search("INFO ::1:(.*)", output):
|
||||
current_status = match.group(1)
|
||||
if current_status.__contains__("Profile run status: PROCESSING"):
|
||||
time.sleep(10)
|
||||
self._get_results()
|
||||
return
|
||||
elif not current_status.__contains__("Profile run finished with SUCCESS"):
|
||||
raise StrobelightCLIProfilerError(
|
||||
f"failed to extract profiling results, unexpected response {output}"
|
||||
)
|
||||
|
||||
for item in re.findall(
|
||||
r"(Total samples(.*)|GraphProfiler(.*)|Icicle view \(python stack\)(.*))",
|
||||
output,
|
||||
):
|
||||
logger.info(item[0])
|
||||
|
||||
def _stop_strobelight_no_throw(
|
||||
self,
|
||||
collect_results: bool,
|
||||
) -> None:
|
||||
try:
|
||||
# call stop run
|
||||
self._stop_run()
|
||||
logger.info("strobelight profiling stopped")
|
||||
|
||||
logger.debug("collection stopped")
|
||||
|
||||
if not collect_results:
|
||||
return
|
||||
|
||||
self._get_results()
|
||||
except Exception:
|
||||
logger.warning("error during stop_strobelight", exc_info=True)
|
||||
|
||||
# Return true if strobelight started and is running. Never throw.
|
||||
def _start_strobelight(self) -> bool:
|
||||
strobelight_started = False
|
||||
try:
|
||||
self._run_async()
|
||||
strobelight_started = True
|
||||
logger.info("strobelight run id is: %s", self.current_run_id)
|
||||
self._wait_for_running()
|
||||
logger.info("strobelight profiling running")
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
logger.warning("error during start_strobelight:", exc_info=True)
|
||||
if strobelight_started:
|
||||
self._stop_strobelight_no_throw(collect_results=False)
|
||||
return False
|
||||
|
||||
def profile(
|
||||
self, work_function: Callable[_P, _R], *args: _P.args, **kwargs: _P.kwargs
|
||||
) -> _R | None:
|
||||
self.current_run_id = None
|
||||
|
||||
if locked := StrobelightCLIFunctionProfiler._lock.acquire(False):
|
||||
if not locked:
|
||||
if self.stop_at_error:
|
||||
raise StrobelightCLIProfilerError("concurrent runs not supported")
|
||||
|
||||
logger.warning("concurrent runs not supported")
|
||||
return work_function(*args, **kwargs)
|
||||
|
||||
started = self._start_strobelight()
|
||||
if not started:
|
||||
if self.stop_at_error:
|
||||
StrobelightCLIFunctionProfiler._lock.release()
|
||||
raise StrobelightCLIProfilerError(
|
||||
"failed to start strobelight profiling"
|
||||
)
|
||||
result = work_function(*args, **kwargs)
|
||||
StrobelightCLIFunctionProfiler._lock.release()
|
||||
return result
|
||||
|
||||
try:
|
||||
logger.debug("collection started")
|
||||
result = work_function(*args, **kwargs)
|
||||
self._stop_strobelight_no_throw(collect_results=True)
|
||||
StrobelightCLIFunctionProfiler._lock.release()
|
||||
return result
|
||||
except Exception as error:
|
||||
logger.warning("work function throw exception", exc_info=True)
|
||||
self._stop_strobelight_no_throw(collect_results=False)
|
||||
StrobelightCLIFunctionProfiler._lock.release()
|
||||
raise error
|
||||
return None
|
||||
|
||||
|
||||
# A function decorator that wraps profile, if no profiler is provided one with
|
||||
# default args is created. A function can be annotated as:
|
||||
# @strobelight()
|
||||
# @strobelight(profiler = StrobelightFunctionProfiler(stop_at_error=True,..))
|
||||
# @strobelight(stop_at_error=True,...)
|
||||
def strobelight(
|
||||
profiler: StrobelightCLIFunctionProfiler | None = None, **kwargs: Any
|
||||
) -> Callable[[Callable[_P, _R]], Callable[_P, _R | None]]:
|
||||
if not profiler:
|
||||
profiler = StrobelightCLIFunctionProfiler(**kwargs)
|
||||
|
||||
def strobelight_inner(
|
||||
work_function: Callable[_P, _R],
|
||||
) -> Callable[_P, _R | None]:
|
||||
@functools.wraps(work_function)
|
||||
def wrapper_function(*args: _P.args, **kwargs: _P.kwargs) -> _R | None:
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
return profiler.profile(work_function, *args, **kwargs)
|
||||
|
||||
return wrapper_function
|
||||
|
||||
return strobelight_inner
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,228 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""
|
||||
This is a simple interpreter for Sympy expressions that dispatches to
|
||||
classes following the torch._inductor.virtualized calling convention.
|
||||
For directness, the interpreter takes the handler directly rather than
|
||||
consulting the TLS. It does not use most of the methods on the full
|
||||
handler; only those with corresponding Sympy expressions. To see an example
|
||||
of a full handler, see torch.utils._sympy.value_ranges.ValueRangeAnalysis.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import sympy
|
||||
from sympy.logic.boolalg import Boolean as SympyBoolean, BooleanAtom
|
||||
|
||||
import torch
|
||||
|
||||
from .functions import (
|
||||
BitwiseFn_bitwise_and,
|
||||
BitwiseFn_bitwise_or,
|
||||
BitwiseFn_bitwise_xor,
|
||||
CeilToInt,
|
||||
CleanDiv,
|
||||
FloatPow,
|
||||
FloatTrueDiv,
|
||||
FloorDiv,
|
||||
FloorToInt,
|
||||
Identity,
|
||||
IntTrueDiv,
|
||||
IsNonOverlappingAndDenseIndicator,
|
||||
Max,
|
||||
Min,
|
||||
Mod,
|
||||
ModularIndexing,
|
||||
OpaqueUnaryFn_log2,
|
||||
PowByNatural,
|
||||
PythonMod,
|
||||
RoundDecimal,
|
||||
RoundToInt,
|
||||
ToFloat,
|
||||
TruncToFloat,
|
||||
TruncToInt,
|
||||
Where,
|
||||
)
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO: Dedupe this with SYMPY_INTERP
|
||||
|
||||
|
||||
@functools.cache
|
||||
def handlers():
|
||||
# TODO add CeilDiv (it doesn't appear in the index_expr)
|
||||
|
||||
# TODO default to some decompositions if the interpreter doesn't have them
|
||||
# like decomposing ModularIndexing or implementing Le(a,b) as Ge(b, a)
|
||||
|
||||
HANDLERS = {
|
||||
sympy.Or: "or_",
|
||||
sympy.And: "and_",
|
||||
sympy.Eq: "eq",
|
||||
sympy.Ne: "ne",
|
||||
sympy.Lt: "lt",
|
||||
sympy.Gt: "gt",
|
||||
sympy.Le: "le",
|
||||
sympy.Ge: "ge",
|
||||
sympy.Not: "not_",
|
||||
IntTrueDiv: "int_truediv",
|
||||
FloatTrueDiv: "truediv",
|
||||
FloorDiv: "floordiv",
|
||||
CleanDiv: "floordiv", # TODO: hmm?
|
||||
TruncToFloat: "trunc",
|
||||
Where: "where",
|
||||
sympy.Add: "add",
|
||||
sympy.Mul: "mul",
|
||||
FloatPow: "pow",
|
||||
PowByNatural: "pow_by_natural",
|
||||
# sympy simplifies x * x into Pow(x, 2), so we need to handle this.
|
||||
# Do NOT use builtin Pow for floats
|
||||
# TODO: There is a hazard here, if we have float * float it will
|
||||
# also get turned into Pow(float, 2) but we don't want this because
|
||||
# pow_by_natural is assumed to only be integers. Probably the fix is
|
||||
# to add a FloatMul to impede this optimization
|
||||
sympy.Pow: "pow_by_natural",
|
||||
Mod: "mod",
|
||||
PythonMod: "python_mod",
|
||||
# TODO: Inductor can generate these, but it's ill-specified which
|
||||
# semantics were intended here. Needs to be cleaned up along with
|
||||
# FloorDiv in a bigger cleanup
|
||||
sympy.Mod: "mod",
|
||||
sympy.Abs: "abs",
|
||||
sympy.log: "log",
|
||||
sympy.exp: "exp",
|
||||
sympy.Min: "minimum",
|
||||
sympy.Max: "maximum",
|
||||
Min: "minimum",
|
||||
Max: "maximum",
|
||||
ModularIndexing: "modular_indexing",
|
||||
sympy.functions.elementary.piecewise.ExprCondPair: "expr_cond_pair",
|
||||
sympy.Piecewise: "piecewise",
|
||||
Identity: "identity",
|
||||
IsNonOverlappingAndDenseIndicator: "is_non_overlapping_and_dense_indicator",
|
||||
RoundDecimal: "round_decimal",
|
||||
# TODO: do the rest of the opaque unary functions...
|
||||
OpaqueUnaryFn_log2: "log2",
|
||||
BitwiseFn_bitwise_and: "bitwise_and",
|
||||
BitwiseFn_bitwise_or: "bitwise_or",
|
||||
BitwiseFn_bitwise_xor: "bitwise_xor",
|
||||
}
|
||||
# TODO: This is kind of pointless, we shouldn't be generating sympy.sin
|
||||
# for these functions, they should be Opaque instead
|
||||
for name in ["cos", "sin", "tan", "sinh", "cosh", "tanh", "asin", "acos", "atan"]:
|
||||
HANDLERS[getattr(sympy, name)] = name
|
||||
|
||||
return HANDLERS
|
||||
|
||||
|
||||
ASSOCIATIVE_OPS = {"minimum", "maximum", "mul", "add", "and_", "or_"}
|
||||
|
||||
|
||||
def _run_sympy_handler(analysis, args, expr, index_dtype=torch.int64):
|
||||
# Special cases
|
||||
if isinstance(expr, sympy.Pow) and isinstance(
|
||||
expr.args[1], sympy.core.numbers.Half
|
||||
):
|
||||
return analysis.sqrt(args[0])
|
||||
if isinstance(expr, ToFloat):
|
||||
return analysis.to_dtype(args[0], torch.float64)
|
||||
|
||||
# These handlers are special because they take an extra dtype argument
|
||||
# specifying what they should convert to, and we need to appropriately set
|
||||
# this up when we convert from Sympy. A reasonable default when you
|
||||
# are translating is to conservatively do int64, and then narrow these
|
||||
# arguments later when you discover you can narrow the index range. But
|
||||
# if you already know that 32-bit indexing is OK, you can directly do the
|
||||
# sympy translation with index_dtype=torch.int32
|
||||
INDEX_DTYPE_HANDLERS = {
|
||||
TruncToInt: "trunc_to_int",
|
||||
sympy.floor: "floor_to_int",
|
||||
sympy.ceiling: "ceil_to_int",
|
||||
FloorToInt: "floor_to_int",
|
||||
CeilToInt: "ceil_to_int",
|
||||
RoundToInt: "round_to_int",
|
||||
}
|
||||
if (handler_name := INDEX_DTYPE_HANDLERS.get(expr.func)) is not None:
|
||||
return getattr(analysis, handler_name)(*args, index_dtype)
|
||||
|
||||
# Fastpath for n-ary integral addition
|
||||
if expr.func is sympy.Add and expr.is_integer and hasattr(analysis, "sym_sum"):
|
||||
r = analysis.sym_sum(args)
|
||||
log.debug("sym_sum(%s) -> %s", args, r)
|
||||
return r
|
||||
|
||||
if hasattr(expr.func, "_torch_handler_name"):
|
||||
handler_name = expr.func._torch_handler_name
|
||||
else:
|
||||
handler_name = handlers()[expr.func]
|
||||
handler = getattr(analysis, handler_name)
|
||||
try:
|
||||
if handler_name in ASSOCIATIVE_OPS:
|
||||
if len(args) <= 1:
|
||||
raise AssertionError("associative op needs >1 args")
|
||||
acc = handler(args[0], args[1])
|
||||
for i in range(2, len(args)):
|
||||
acc = handler(acc, args[i])
|
||||
log.debug("%s(%s) -> %s", handler_name, args, acc)
|
||||
return acc
|
||||
else:
|
||||
r = handler(*args)
|
||||
log.debug("%s(%s) -> %s", handler_name, args, r)
|
||||
return r
|
||||
except NotImplementedError:
|
||||
raise
|
||||
except Exception:
|
||||
log.warning("failed while executing %s(%s)", handler_name, args)
|
||||
raise
|
||||
|
||||
|
||||
_nil = object()
|
||||
|
||||
|
||||
def sympy_interp(
|
||||
analysis,
|
||||
env: dict[sympy.Symbol, Any],
|
||||
expr: sympy.Expr | SympyBoolean,
|
||||
*,
|
||||
index_dtype=torch.int64,
|
||||
missing_handler=None,
|
||||
):
|
||||
# Handle base cases
|
||||
dtype = None
|
||||
if isinstance(expr, BooleanAtom):
|
||||
dtype = torch.bool
|
||||
elif isinstance(expr, sympy.Integer):
|
||||
dtype = torch.int64
|
||||
elif isinstance(expr, sympy.Number):
|
||||
dtype = torch.double
|
||||
|
||||
if dtype is not None:
|
||||
return analysis.constant(expr, dtype)
|
||||
elif isinstance(expr, sympy.Symbol):
|
||||
if (r := env.get(expr, _nil)) is not _nil:
|
||||
return r
|
||||
elif missing_handler:
|
||||
return missing_handler(expr)
|
||||
else:
|
||||
raise KeyError(expr)
|
||||
|
||||
# Recursive case
|
||||
return _run_sympy_handler(
|
||||
analysis,
|
||||
[
|
||||
sympy_interp(
|
||||
analysis,
|
||||
env,
|
||||
arg,
|
||||
index_dtype=index_dtype,
|
||||
missing_handler=missing_handler,
|
||||
)
|
||||
for arg in expr.args
|
||||
],
|
||||
expr,
|
||||
index_dtype=index_dtype,
|
||||
)
|
||||
@@ -0,0 +1,417 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import mpmath.libmp as mlib # type: ignore[import-untyped]
|
||||
import sympy
|
||||
from sympy import Expr
|
||||
from sympy.core.decorators import _sympifyit
|
||||
from sympy.core.expr import AtomicExpr
|
||||
from sympy.core.numbers import Number
|
||||
from sympy.core.parameters import global_parameters
|
||||
from sympy.core.singleton import S, Singleton
|
||||
|
||||
|
||||
# pyrefly: ignore [invalid-inheritance]
|
||||
class IntInfinity(Number, metaclass=Singleton):
|
||||
r"""Positive integer infinite quantity.
|
||||
|
||||
Integer infinity is a value in an extended integers which
|
||||
is greater than all other integers. We distinguish it from
|
||||
sympy's existing notion of infinity in that it reports that
|
||||
it is_integer.
|
||||
|
||||
Infinity is a singleton, and can be accessed by ``S.IntInfinity``,
|
||||
or can be imported as ``int_oo``.
|
||||
"""
|
||||
|
||||
# NB: We can't actually mark this as infinite, as integer and infinite are
|
||||
# inconsistent assumptions in sympy. We also report that we are complex,
|
||||
# different from sympy.oo
|
||||
|
||||
is_integer = True
|
||||
is_commutative = True
|
||||
is_number = True
|
||||
is_extended_real = True
|
||||
is_comparable = True
|
||||
is_extended_positive = True
|
||||
is_prime = False
|
||||
|
||||
# Ensure we get dispatched to before plain numbers
|
||||
_op_priority = 100.0
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __new__(cls):
|
||||
return AtomicExpr.__new__(cls)
|
||||
|
||||
def _sympystr(self, printer) -> str:
|
||||
return "int_oo"
|
||||
|
||||
def _eval_subs(self, old, new):
|
||||
if self == old:
|
||||
return new
|
||||
|
||||
# We could do these, not sure about it
|
||||
"""
|
||||
def _eval_evalf(self, prec=None):
|
||||
return Float('inf')
|
||||
|
||||
def evalf(self, prec=None, **options):
|
||||
return self._eval_evalf(prec)
|
||||
"""
|
||||
|
||||
@_sympifyit("other", NotImplemented)
|
||||
def __add__(self, other):
|
||||
if isinstance(other, Number) and global_parameters.evaluate:
|
||||
if other in (S.Infinity, S.NegativeInfinity):
|
||||
return other
|
||||
if other in (S.NegativeIntInfinity, S.NaN):
|
||||
return S.NaN
|
||||
return self
|
||||
return Number.__add__(self, other)
|
||||
|
||||
__radd__ = __add__
|
||||
|
||||
@_sympifyit("other", NotImplemented)
|
||||
def __sub__(self, other):
|
||||
if isinstance(other, Number) and global_parameters.evaluate:
|
||||
if other is S.Infinity:
|
||||
return S.NegativeInfinity
|
||||
if other is S.NegativeInfinity:
|
||||
return S.Infinity
|
||||
if other in (S.IntInfinity, S.NaN):
|
||||
return S.NaN
|
||||
return self
|
||||
return Number.__sub__(self, other)
|
||||
|
||||
@_sympifyit("other", NotImplemented)
|
||||
def __rsub__(self, other):
|
||||
return (-self).__add__(other)
|
||||
|
||||
@_sympifyit("other", NotImplemented)
|
||||
def __mul__(self, other):
|
||||
if isinstance(other, Number) and global_parameters.evaluate:
|
||||
if other.is_zero or other is S.NaN:
|
||||
return S.NaN
|
||||
if other.is_extended_positive:
|
||||
return self
|
||||
return S.NegativeIntInfinity
|
||||
return Number.__mul__(self, other)
|
||||
|
||||
__rmul__ = __mul__
|
||||
|
||||
@_sympifyit("other", NotImplemented)
|
||||
def __truediv__(self, other):
|
||||
if isinstance(other, Number) and global_parameters.evaluate:
|
||||
if other in (
|
||||
S.Infinity,
|
||||
S.IntInfinity,
|
||||
S.NegativeInfinity,
|
||||
S.NegativeIntInfinity,
|
||||
S.NaN,
|
||||
):
|
||||
return S.NaN
|
||||
if other.is_extended_nonnegative:
|
||||
return S.Infinity # truediv produces float
|
||||
return S.NegativeInfinity # truediv produces float
|
||||
return Number.__truediv__(self, other)
|
||||
|
||||
def __abs__(self):
|
||||
return S.IntInfinity
|
||||
|
||||
def __neg__(self):
|
||||
return S.NegativeIntInfinity
|
||||
|
||||
def _eval_power(self, expt):
|
||||
if expt.is_extended_positive:
|
||||
return S.IntInfinity
|
||||
if expt.is_extended_negative:
|
||||
return S.Zero
|
||||
if expt is S.NaN:
|
||||
return S.NaN
|
||||
if expt is S.ComplexInfinity:
|
||||
return S.NaN
|
||||
if expt.is_extended_real is False and expt.is_number:
|
||||
from sympy.functions.elementary.complexes import re
|
||||
|
||||
expt_real = re(expt)
|
||||
if expt_real.is_positive:
|
||||
return S.ComplexInfinity
|
||||
if expt_real.is_negative:
|
||||
return S.Zero
|
||||
if expt_real.is_zero:
|
||||
return S.NaN
|
||||
|
||||
return self ** expt.evalf()
|
||||
|
||||
def _as_mpf_val(self, prec):
|
||||
return mlib.finf
|
||||
|
||||
def __hash__(self):
|
||||
return super().__hash__()
|
||||
|
||||
def __eq__(self, other):
|
||||
return other is S.IntInfinity
|
||||
|
||||
def __ne__(self, other):
|
||||
return other is not S.IntInfinity
|
||||
|
||||
def __gt__(self, other):
|
||||
if other is S.Infinity:
|
||||
return sympy.false # sympy.oo > int_oo
|
||||
elif other is S.IntInfinity:
|
||||
return sympy.false # consistency with sympy.oo
|
||||
else:
|
||||
return sympy.true
|
||||
|
||||
def __ge__(self, other):
|
||||
if other is S.Infinity:
|
||||
return sympy.false # sympy.oo > int_oo
|
||||
elif other is S.IntInfinity:
|
||||
return sympy.true # consistency with sympy.oo
|
||||
else:
|
||||
return sympy.true
|
||||
|
||||
def __lt__(self, other):
|
||||
if other is S.Infinity:
|
||||
return sympy.true # sympy.oo > int_oo
|
||||
elif other is S.IntInfinity:
|
||||
return sympy.false # consistency with sympy.oo
|
||||
else:
|
||||
return sympy.false
|
||||
|
||||
def __le__(self, other):
|
||||
if other is S.Infinity:
|
||||
return sympy.true # sympy.oo > int_oo
|
||||
elif other is S.IntInfinity:
|
||||
return sympy.true # consistency with sympy.oo
|
||||
else:
|
||||
return sympy.false
|
||||
|
||||
@_sympifyit("other", NotImplemented)
|
||||
def __mod__(self, other):
|
||||
if not isinstance(other, Expr):
|
||||
return NotImplemented
|
||||
return S.NaN
|
||||
|
||||
__rmod__ = __mod__
|
||||
|
||||
def floor(self):
|
||||
return self
|
||||
|
||||
def ceiling(self):
|
||||
return self
|
||||
|
||||
|
||||
int_oo = S.IntInfinity
|
||||
|
||||
|
||||
def is_infinite(expr) -> bool:
|
||||
"""Check if an expression is any type of infinity (positive or negative).
|
||||
|
||||
This handles both sympy's built-in infinities (oo, -oo) and PyTorch's
|
||||
integer infinities (int_oo, -int_oo).
|
||||
|
||||
Note: We cannot rely on sympy's is_finite property because IntInfinity
|
||||
and NegativeIntInfinity have is_integer=True, which implies is_finite=True
|
||||
in sympy's assumption system.
|
||||
"""
|
||||
return expr in (
|
||||
S.Infinity,
|
||||
S.NegativeInfinity,
|
||||
S.IntInfinity,
|
||||
S.NegativeIntInfinity,
|
||||
)
|
||||
|
||||
|
||||
# pyrefly: ignore [invalid-inheritance]
|
||||
class NegativeIntInfinity(Number, metaclass=Singleton):
|
||||
"""Negative integer infinite quantity.
|
||||
|
||||
NegativeInfinity is a singleton, and can be accessed
|
||||
by ``S.NegativeInfinity``.
|
||||
|
||||
See Also
|
||||
========
|
||||
|
||||
IntInfinity
|
||||
"""
|
||||
|
||||
# Ensure we get dispatched to before plain numbers
|
||||
_op_priority = 100.0
|
||||
|
||||
is_integer = True
|
||||
is_extended_real = True
|
||||
is_commutative = True
|
||||
is_comparable = True
|
||||
is_extended_negative = True
|
||||
is_number = True
|
||||
is_prime = False
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __new__(cls):
|
||||
return AtomicExpr.__new__(cls)
|
||||
|
||||
def _eval_subs(self, old, new):
|
||||
if self == old:
|
||||
return new
|
||||
|
||||
def _sympystr(self, printer) -> str:
|
||||
return "-int_oo"
|
||||
|
||||
"""
|
||||
def _eval_evalf(self, prec=None):
|
||||
return Float('-inf')
|
||||
|
||||
def evalf(self, prec=None, **options):
|
||||
return self._eval_evalf(prec)
|
||||
"""
|
||||
|
||||
@_sympifyit("other", NotImplemented)
|
||||
def __add__(self, other):
|
||||
if isinstance(other, Number) and global_parameters.evaluate:
|
||||
if other is S.Infinity:
|
||||
return S.Infinity
|
||||
if other in (S.IntInfinity, S.NaN):
|
||||
return S.NaN
|
||||
return self
|
||||
return Number.__add__(self, other)
|
||||
|
||||
__radd__ = __add__
|
||||
|
||||
@_sympifyit("other", NotImplemented)
|
||||
def __sub__(self, other):
|
||||
if isinstance(other, Number) and global_parameters.evaluate:
|
||||
if other is S.NegativeInfinity:
|
||||
return S.Infinity
|
||||
if other in (S.NegativeIntInfinity, S.NaN):
|
||||
return S.NaN
|
||||
return self
|
||||
return Number.__sub__(self, other)
|
||||
|
||||
@_sympifyit("other", NotImplemented)
|
||||
def __rsub__(self, other):
|
||||
return (-self).__add__(other)
|
||||
|
||||
@_sympifyit("other", NotImplemented)
|
||||
def __mul__(self, other):
|
||||
if isinstance(other, Number) and global_parameters.evaluate:
|
||||
if other.is_zero or other is S.NaN:
|
||||
return S.NaN
|
||||
if other.is_extended_positive:
|
||||
return self
|
||||
return S.IntInfinity
|
||||
return Number.__mul__(self, other)
|
||||
|
||||
__rmul__ = __mul__
|
||||
|
||||
@_sympifyit("other", NotImplemented)
|
||||
def __truediv__(self, other):
|
||||
if isinstance(other, Number) and global_parameters.evaluate:
|
||||
if other in (
|
||||
S.Infinity,
|
||||
S.IntInfinity,
|
||||
S.NegativeInfinity,
|
||||
S.NegativeIntInfinity,
|
||||
S.NaN,
|
||||
):
|
||||
return S.NaN
|
||||
if other.is_extended_nonnegative:
|
||||
return self
|
||||
return S.Infinity # truediv returns float
|
||||
return Number.__truediv__(self, other)
|
||||
|
||||
def __abs__(self):
|
||||
return S.IntInfinity
|
||||
|
||||
def __neg__(self):
|
||||
return S.IntInfinity
|
||||
|
||||
def _eval_power(self, expt):
|
||||
if expt.is_number:
|
||||
if expt in (
|
||||
S.NaN,
|
||||
S.Infinity,
|
||||
S.NegativeInfinity,
|
||||
S.IntInfinity,
|
||||
S.NegativeIntInfinity,
|
||||
):
|
||||
return S.NaN
|
||||
|
||||
if isinstance(expt, sympy.Integer) and expt.is_extended_positive:
|
||||
if expt.is_odd:
|
||||
return S.NegativeIntInfinity
|
||||
else:
|
||||
return S.IntInfinity
|
||||
|
||||
inf_part = S.IntInfinity**expt
|
||||
s_part = S.NegativeOne**expt
|
||||
if inf_part == 0 and s_part.is_finite:
|
||||
return inf_part
|
||||
if (
|
||||
inf_part is S.ComplexInfinity
|
||||
and s_part.is_finite
|
||||
and not s_part.is_zero
|
||||
):
|
||||
return S.ComplexInfinity
|
||||
return s_part * inf_part
|
||||
|
||||
def _as_mpf_val(self, prec):
|
||||
return mlib.fninf
|
||||
|
||||
def __hash__(self):
|
||||
return super().__hash__()
|
||||
|
||||
def __eq__(self, other):
|
||||
return other is S.NegativeIntInfinity
|
||||
|
||||
def __ne__(self, other):
|
||||
return other is not S.NegativeIntInfinity
|
||||
|
||||
def __gt__(self, other):
|
||||
if other is S.NegativeInfinity:
|
||||
return sympy.true # -sympy.oo < -int_oo
|
||||
elif other is S.NegativeIntInfinity:
|
||||
return sympy.false # consistency with sympy.oo
|
||||
else:
|
||||
return sympy.false
|
||||
|
||||
def __ge__(self, other):
|
||||
if other is S.NegativeInfinity:
|
||||
return sympy.true # -sympy.oo < -int_oo
|
||||
elif other is S.NegativeIntInfinity:
|
||||
return sympy.true # consistency with sympy.oo
|
||||
else:
|
||||
return sympy.false
|
||||
|
||||
def __lt__(self, other):
|
||||
if other is S.NegativeInfinity:
|
||||
return sympy.false # -sympy.oo < -int_oo
|
||||
elif other is S.NegativeIntInfinity:
|
||||
return sympy.false # consistency with sympy.oo
|
||||
else:
|
||||
return sympy.true
|
||||
|
||||
def __le__(self, other):
|
||||
if other is S.NegativeInfinity:
|
||||
return sympy.false # -sympy.oo < -int_oo
|
||||
elif other is S.NegativeIntInfinity:
|
||||
return sympy.true # consistency with sympy.oo
|
||||
else:
|
||||
return sympy.true
|
||||
|
||||
@_sympifyit("other", NotImplemented)
|
||||
def __mod__(self, other):
|
||||
if not isinstance(other, Expr):
|
||||
return NotImplemented
|
||||
return S.NaN
|
||||
|
||||
__rmod__ = __mod__
|
||||
|
||||
def floor(self):
|
||||
return self
|
||||
|
||||
def ceiling(self):
|
||||
return self
|
||||
|
||||
def as_powers_dict(self):
|
||||
return {S.NegativeOne: 1, S.IntInfinity: 1}
|
||||
@@ -0,0 +1,666 @@
|
||||
import sys
|
||||
|
||||
import sympy
|
||||
from sympy.printing.precedence import PRECEDENCE, precedence
|
||||
from sympy.printing.str import StrPrinter
|
||||
|
||||
|
||||
INDEX_TYPE = "int64_t"
|
||||
INDEX_TYPE_MAX = (1 << 63) - 1
|
||||
INDEX_TYPE_MIN = -1 << 63
|
||||
|
||||
|
||||
# This printer contains rules that are supposed to be generic for both C/C++ and
|
||||
# Python
|
||||
class ExprPrinter(StrPrinter):
|
||||
# override this so that _print_FloorDiv is used
|
||||
printmethod = "_torch_sympystr"
|
||||
|
||||
def _print_Mul(self, expr: sympy.Expr) -> str:
|
||||
return self.stringify(expr.args, "*", precedence(expr))
|
||||
|
||||
def _print_Not(self, expr: sympy.Expr) -> str:
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"not ({self._print(expr.args[0])})"
|
||||
|
||||
def _print_Add(self, expr: sympy.Expr, order: str | None = None) -> str:
|
||||
return self.stringify(expr.args, " + ", precedence(expr))
|
||||
|
||||
def _print_Relational(self, expr: sympy.Expr) -> str:
|
||||
return self.stringify(expr.args, f" {expr.rel_op} ", precedence(expr))
|
||||
|
||||
def _print_BitwiseFn_bitwise_and(self, expr: sympy.Expr) -> str:
|
||||
return self.stringify(expr.args, " & ", PRECEDENCE["BitwiseAnd"])
|
||||
|
||||
def _print_BitwiseFn_bitwise_or(self, expr: sympy.Expr) -> str:
|
||||
return self.stringify(expr.args, " | ", PRECEDENCE["BitwiseOr"])
|
||||
|
||||
def _print_BitwiseFn_bitwise_xor(self, expr: sympy.Expr) -> str:
|
||||
return self.stringify(expr.args, " ^ ", PRECEDENCE["BitwiseXor"])
|
||||
|
||||
# NB: this is OK to put here, because Mod is only defined for positive
|
||||
# numbers, and so across C/Python its behavior is consistent
|
||||
def _print_Mod(self, expr: sympy.Expr) -> str:
|
||||
return self.stringify(expr.args, " % ", PRECEDENCE["Atom"] - 0.5)
|
||||
|
||||
def _print_FloatTrueDiv(self, expr: sympy.Expr) -> str:
|
||||
s = self.stringify(expr.args, " / ", PRECEDENCE["Atom"] - 0.5)
|
||||
return f"({s})"
|
||||
|
||||
def _print_CleanDiv(self, expr: sympy.Expr) -> str:
|
||||
return self._print_FloorDiv(expr)
|
||||
|
||||
def _print_Identity(self, expr: sympy.Expr) -> str:
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return self._print(expr.args[0])
|
||||
|
||||
def _print_Float(self, expr: sympy.Expr) -> str:
|
||||
if expr._prec == 53:
|
||||
# IEEE-754 double precision have 53 bits. SymPy prints them with
|
||||
# 15 digits, but we need 17 for round-trip correctness
|
||||
return str(sympy.Float(expr, dps=17))
|
||||
else:
|
||||
# We don't use other precisions in pytorch
|
||||
return str(expr)
|
||||
|
||||
# This must be implemented because sympy will collect x * x into Pow(x, 2), without
|
||||
# any explicit intervention. We print it just like x * x, notably, we
|
||||
# never generate sympy.Pow with floats.
|
||||
#
|
||||
# NB: this pow by natural, you should never have used builtin sympy.pow
|
||||
# for FloatPow, and a symbolic exponent should be PowByNatural. These
|
||||
# means exp is guaranteed to be integer.
|
||||
def _print_Pow(self, expr: sympy.Expr) -> str:
|
||||
base, exp = expr.args
|
||||
if exp != int(exp):
|
||||
raise AssertionError(exp)
|
||||
exp = int(exp)
|
||||
if exp < 0:
|
||||
raise AssertionError(f"exponent must be non-negative, got {exp}")
|
||||
if exp > 0:
|
||||
return self.stringify([base] * exp, "*", PRECEDENCE["Mul"])
|
||||
return "1"
|
||||
|
||||
# Explicit NotImplemented functions are to prevent default sympy printing
|
||||
# behavior, which will just barf out ToFloat(...) to your IR. The error
|
||||
# message is better here because it tells you which printer class it needs
|
||||
# to go in.
|
||||
|
||||
def _print_ToFloat(self, expr: sympy.Expr) -> str:
|
||||
raise NotImplementedError(f"_print_ToFloat not implemented for {type(self)}")
|
||||
|
||||
def _print_Infinity(self, expr: sympy.Expr) -> str:
|
||||
raise NotImplementedError(f"_print_Infinity not implemented for {type(self)}")
|
||||
|
||||
def _print_NegativeInfinity(self, expr: sympy.Expr) -> str:
|
||||
raise NotImplementedError(
|
||||
f"_print_NegativeInfinity not implemented for {type(self)}"
|
||||
)
|
||||
|
||||
def _print_NaN(self, expr: sympy.Expr) -> str:
|
||||
raise NotImplementedError(f"_print_NaN not implemented for {type(self)}")
|
||||
|
||||
def _print_FloorDiv(self, expr: sympy.Expr) -> str:
|
||||
raise NotImplementedError(f"_print_FloorDiv not implemented for {type(self)}")
|
||||
|
||||
def _print_PythonMod(self, expr: sympy.Expr) -> str:
|
||||
raise NotImplementedError(f"_print_PythonMod not implemented for {type(self)}")
|
||||
|
||||
def _print_IntTrueDiv(self, expr: sympy.Expr) -> str:
|
||||
raise NotImplementedError(f"_print_IntTrueDiv not implemented for {type(self)}")
|
||||
|
||||
def _print_PowByNatural(self, expr: sympy.Expr) -> str:
|
||||
raise NotImplementedError(
|
||||
f"_print_PowByNatural not implemented for {type(self)}"
|
||||
)
|
||||
|
||||
def _print_FloatPow(self, expr: sympy.Expr) -> str:
|
||||
raise NotImplementedError(f"_print_FloatPow not implemented for {type(self)}")
|
||||
|
||||
def _print_TruncToInt(self, expr: sympy.Expr) -> str:
|
||||
raise NotImplementedError(f"_print_TruncToInt not implemented for {type(self)}")
|
||||
|
||||
def _print_RoundToInt(self, expr: sympy.Expr) -> str:
|
||||
raise NotImplementedError(f"_print_RoundToInt not implemented for {type(self)}")
|
||||
|
||||
def _print_RoundDecimal(self, expr: sympy.Expr) -> str:
|
||||
raise NotImplementedError(
|
||||
f"_print_RoundDecimal not implemented for {type(self)}"
|
||||
)
|
||||
|
||||
# NB: Some float operations are INTENTIONALLY not implemented for
|
||||
# printers. You can implement them as a quick unblock, but it is better
|
||||
# to ask yourself why we haven't done this computation in the Tensor
|
||||
# universe instead
|
||||
|
||||
def _print_TruncToFloat(self, expr: sympy.Expr) -> str:
|
||||
raise NotImplementedError(
|
||||
f"_print_TruncToFloat not implemented for {type(self)}"
|
||||
)
|
||||
|
||||
|
||||
class PythonPrinter(ExprPrinter):
|
||||
def _print_ToFloat(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("ToFloat expects exactly one argument")
|
||||
# NB: We use sym_float here because the printer is used for cache
|
||||
# serialization, and cache guards get evaluated with SymInt to
|
||||
# propagate guards to the parent ShapeEnv. However, this comes at a
|
||||
# runtime cost for guards involving float. If this is unacceptable
|
||||
# overhead, what you want to do is have two separate printers for
|
||||
# SymInt, one for when the inputs are guaranteed to be int, and
|
||||
# another for when they could be SymInt.
|
||||
#
|
||||
# NB: sym_min/sym_max also have this problem, but I chose not to fix
|
||||
# those.
|
||||
#
|
||||
# See https://github.com/pytorch/pytorch/issues/142507 for more
|
||||
# context.
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"torch.sym_float({self._print(expr.args[0])})"
|
||||
|
||||
def _print_And(self, expr: sympy.Expr) -> str:
|
||||
return self.stringify(expr.args, " and ", precedence(expr))
|
||||
|
||||
def _print_Or(self, expr: sympy.Expr) -> str:
|
||||
return self.stringify(expr.args, " or ", precedence(expr))
|
||||
|
||||
def _print_ModularIndexing(self, expr: sympy.Expr) -> str:
|
||||
x, div, mod = (
|
||||
self.parenthesize(arg, PRECEDENCE["Atom"] - 0.5) for arg in expr.args
|
||||
)
|
||||
if div != "1":
|
||||
x = f"({x} // {div})"
|
||||
return f"({x} % {mod})"
|
||||
|
||||
def _print_Infinity(self, expr: sympy.Expr) -> str:
|
||||
return "math.inf"
|
||||
|
||||
def _print_NegativeInfinity(self, expr: sympy.Expr) -> str:
|
||||
return "-math.inf"
|
||||
|
||||
def _print_NaN(self, expr: sympy.Expr) -> str:
|
||||
return "math.nan"
|
||||
|
||||
# WARNING: this is dangerous for Triton, which has C-style modulus
|
||||
def _print_PythonMod(self, expr: sympy.Expr) -> str:
|
||||
return self.stringify(expr.args, " % ", PRECEDENCE["Atom"] - 0.5)
|
||||
|
||||
# WARNING: this is dangerous for Triton, which has C-style modulus
|
||||
def _print_FloorDiv(self, expr: sympy.Expr) -> str:
|
||||
x, div = (self.parenthesize(arg, PRECEDENCE["Atom"] - 0.5) for arg in expr.args)
|
||||
return f"{x} // {div}"
|
||||
|
||||
# WARNING: this is dangerous for Triton, when lhs, rhs > 2**53, Python
|
||||
# does a special algorithm
|
||||
def _print_IntTrueDiv(self, expr: sympy.Expr) -> str:
|
||||
return self.stringify(expr.args, " / ", PRECEDENCE["Atom"] - 0.5)
|
||||
|
||||
def _helper_sqrt(self, expr: sympy.Expr) -> str:
|
||||
# NB: We use torch._sym_sqrt here instead of math.sqrt because the
|
||||
# guard expression may be evaluated with SymInt/SymFloat inputs (e.g.
|
||||
# during cache hit re-evaluation in evaluate_guards_expression).
|
||||
# math.sqrt on a SymFloat triggers evaluate_expr which forces
|
||||
# concretization/specialization of the symbol, creating spurious
|
||||
# guards that didn't exist in the original program.
|
||||
# torch._sym_sqrt properly propagates through the symbolic system
|
||||
# without forcing specialization.
|
||||
# See https://github.com/pytorch/pytorch/issues/152435
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"torch._sym_sqrt({self._print(expr)})"
|
||||
|
||||
def _print_OpaqueUnaryFn_sqrt(self, expr: sympy.Expr) -> str:
|
||||
return self._helper_sqrt(expr.args[0])
|
||||
|
||||
def _print_FloatPow(self, expr: sympy.Expr) -> str:
|
||||
return self.stringify(expr.args, " ** ", PRECEDENCE["Pow"])
|
||||
|
||||
# TODO: Not sure this works with Triton, even when base/exp are integral
|
||||
def _print_PowByNatural(self, expr: sympy.Expr) -> str:
|
||||
return self.stringify(expr.args, " ** ", PRECEDENCE["Pow"])
|
||||
|
||||
def _print_floor(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("floor expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"math.floor({self._print(expr.args[0])})"
|
||||
|
||||
def _print_FloorToInt(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("FloorToInt expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"math.floor({self._print(expr.args[0])})"
|
||||
|
||||
def _print_TruncToInt(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("TruncToInt expects exactly one argument")
|
||||
# This also could have been int(), they'll do the same thing for float
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"math.trunc({self._print(expr.args[0])})"
|
||||
|
||||
def _print_ceiling(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("ceiling expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"math.ceil({self._print(expr.args[0])})"
|
||||
|
||||
def _print_CeilToInt(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("CeilToInt expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"math.ceil({self._print(expr.args[0])})"
|
||||
|
||||
def _print_Abs(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("Abs expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"abs({self._print(expr.args[0])})"
|
||||
|
||||
# NB: It's expected that we've made explicit any promotion in the sympy
|
||||
# expression, so it doesn't matter that Python max/min doesn't perform
|
||||
# promotion
|
||||
def _print_Max(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) < 2:
|
||||
raise AssertionError("Max expects at least two arguments")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"max({', '.join(map(self._print, expr.args))})"
|
||||
|
||||
def _print_Min(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) < 2:
|
||||
raise AssertionError("Min expects at least two arguments")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"min({', '.join(map(self._print, expr.args))})"
|
||||
|
||||
def _print_OpaqueUnaryFn_cos(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("cos expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"math.cos({self._print(expr.args[0])})"
|
||||
|
||||
def _print_OpaqueUnaryFn_cosh(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("cosh expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"math.cosh({self._print(expr.args[0])})"
|
||||
|
||||
def _print_OpaqueUnaryFn_acos(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("acos expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"math.acos({self._print(expr.args[0])})"
|
||||
|
||||
def _print_OpaqueUnaryFn_sin(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("sin expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"math.sin({self._print(expr.args[0])})"
|
||||
|
||||
def _print_OpaqueUnaryFn_sinh(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("sinh expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"math.sinh({self._print(expr.args[0])})"
|
||||
|
||||
def _print_OpaqueUnaryFn_asin(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("asin expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"math.asin({self._print(expr.args[0])})"
|
||||
|
||||
def _print_OpaqueUnaryFn_tan(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("tan expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"math.tan({self._print(expr.args[0])})"
|
||||
|
||||
def _print_OpaqueUnaryFn_tanh(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("tanh expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"math.tanh({self._print(expr.args[0])})"
|
||||
|
||||
def _print_OpaqueUnaryFn_atan(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("atan expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"math.atan({self._print(expr.args[0])})"
|
||||
|
||||
def _print_OpaqueUnaryFn_log2(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("log2 expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"math.log2({self._print(expr.args[0])})"
|
||||
|
||||
def _print_RoundToInt(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("RoundToInt expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"round({self._print(expr.args[0])})"
|
||||
|
||||
def _print_RoundDecimal(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 2:
|
||||
raise AssertionError("RoundDecimal expects exactly two arguments")
|
||||
number, ndigits = expr.args
|
||||
if not isinstance(ndigits, sympy.Integer):
|
||||
raise TypeError("ndigits must be an instance of sympy.Integer")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"round({self._print(number)}, {ndigits})"
|
||||
|
||||
def _print_Piecewise(self, expr: sympy.Expr) -> str:
|
||||
# Convert Piecewise(expr_cond_pairs) to nested ternary expressions
|
||||
# Piecewise((e1, c1), (e2, c2), ..., (eN, cN))
|
||||
# becomes: e1 if c1 else (e2 if c2 else (... else eN))
|
||||
result: str | None = None
|
||||
for expr_i, cond_i in reversed(expr.args):
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
expr_str = self._print(expr_i)
|
||||
if cond_i == True: # noqa: E712
|
||||
# This is the default case
|
||||
result = expr_str
|
||||
else:
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
cond_str = self._print(cond_i)
|
||||
if result is None:
|
||||
result = expr_str
|
||||
else:
|
||||
result = f"({expr_str} if {cond_str} else {result})"
|
||||
return result if result else "0"
|
||||
|
||||
|
||||
class CppPrinter(ExprPrinter):
|
||||
def _print_Integer(self, expr: sympy.Expr) -> str:
|
||||
suffix = "LL" if sys.platform in ["darwin", "win32"] else "L"
|
||||
i = int(expr)
|
||||
if i > INDEX_TYPE_MAX or i < INDEX_TYPE_MIN:
|
||||
raise OverflowError(f"{i} too big to convert to {INDEX_TYPE}")
|
||||
elif i == INDEX_TYPE_MIN:
|
||||
if i != (-1) << 63:
|
||||
raise AssertionError("unexpected minimum index type value")
|
||||
# Writing -9223372036854775808L makes the value overflow
|
||||
# as it is parsed as -(9223372036854775808L) by the C/C++ compiler
|
||||
return f"(-1{suffix} << 63)"
|
||||
return f"{i}{suffix}"
|
||||
|
||||
def _print_Where(self, expr: sympy.Expr) -> str:
|
||||
c, p, q = (
|
||||
self.parenthesize(arg, PRECEDENCE["Atom"] - 0.5) for arg in expr.args
|
||||
)
|
||||
return f"{c} ? {p} : {q}"
|
||||
|
||||
def _print_Or(self, expr: sympy.Expr) -> str:
|
||||
return self.stringify(expr.args, " || ", precedence(expr))
|
||||
|
||||
def _print_Piecewise(self, expr: sympy.Expr) -> str:
|
||||
# Convert Piecewise(expr_cond_pairs) to nested ternary operators
|
||||
# Piecewise((e1, c1), (e2, c2), ..., (eN, cN))
|
||||
# becomes: c1 ? e1 : (c2 ? e2 : (... : eN))
|
||||
result: str | None = None
|
||||
for expr_i, cond_i in reversed(expr.args):
|
||||
expr_str = self.parenthesize(expr_i, PRECEDENCE["Atom"] - 0.5)
|
||||
if cond_i == True: # noqa: E712
|
||||
# This is the default case
|
||||
result = expr_str
|
||||
else:
|
||||
cond_str = self.parenthesize(cond_i, PRECEDENCE["Atom"] - 0.5)
|
||||
if result is None:
|
||||
result = expr_str
|
||||
else:
|
||||
result = f"{cond_str} ? {expr_str} : {result}"
|
||||
return f"({result})" if result else "0"
|
||||
|
||||
def _print_ModularIndexing(self, expr: sympy.Expr) -> str:
|
||||
x, div, mod = expr.args
|
||||
x = self.doprint(x)
|
||||
if div != 1:
|
||||
div = self.doprint(div)
|
||||
if expr.is_integer:
|
||||
x = f"c10::div_floor_integer(static_cast<int64_t>({x}), static_cast<int64_t>({div}))"
|
||||
else:
|
||||
x = f"c10::div_floor_floating(static_cast<double>({x}), static_cast<double>({div}))"
|
||||
mod = self.doprint(mod)
|
||||
return f"(static_cast<{INDEX_TYPE}>({x}) % static_cast<{INDEX_TYPE}>({mod}))"
|
||||
|
||||
def _print_FloorDiv(self, expr: sympy.Expr) -> str:
|
||||
x, div = expr.args
|
||||
x = self.doprint(x)
|
||||
div = self.doprint(div)
|
||||
if expr.is_integer:
|
||||
return f"c10::div_floor_integer(static_cast<int64_t>({x}), static_cast<int64_t>({div}))"
|
||||
return f"c10::div_floor_floating(static_cast<double>({x}), static_cast<double>({div}))"
|
||||
|
||||
def _print_floor(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("floor expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
r = f"std::floor({self._print(expr.args[0])})"
|
||||
return f"static_cast<{INDEX_TYPE}>({r})" if expr.is_integer else r
|
||||
|
||||
def _print_FloorToInt(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("FloorToInt expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
r = f"std::floor({self._print(expr.args[0])})"
|
||||
return f"static_cast<{INDEX_TYPE}>({r})" if expr.is_integer else r
|
||||
|
||||
def _print_TruncToInt(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("TruncToInt expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
r = f"std::trunc({self._print(expr.args[0])})"
|
||||
return f"static_cast<{INDEX_TYPE}>({r})"
|
||||
|
||||
def _print_TruncToFloat(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("TruncToFloat expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"std::trunc({self._print(expr.args[0])})"
|
||||
|
||||
def _print_ToFloat(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("ToFloat expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"static_cast<double>({self._print(expr.args[0])})"
|
||||
|
||||
def _print_PythonMod(self, expr: sympy.Expr) -> str:
|
||||
x, div = expr.args
|
||||
x = self.doprint(x)
|
||||
div = self.doprint(div)
|
||||
return f"c10::div_mod({x}, {div})"
|
||||
|
||||
def _print_IntTrueDiv(self, expr: sympy.Expr) -> str:
|
||||
lhs, rhs = expr.args
|
||||
# TODO: This is only accurate up to 2**53
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"static_cast<double>({self._print(lhs)}) / static_cast<double>({self._print(rhs)})"
|
||||
|
||||
# TODO: PowByNatural: we need to implement our own int-int pow. Do NOT
|
||||
# use std::pow, that operates on floats
|
||||
def _print_PowByNatural(self, expr: sympy.Expr) -> str:
|
||||
# Implement the special-case of 2**x for now
|
||||
base, exp = expr.args
|
||||
if base == 2:
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"(1 << ({self._print(exp)}))"
|
||||
raise NotImplementedError(
|
||||
f"_print_PowByNatural not implemented for {type(self)}"
|
||||
)
|
||||
|
||||
def _print_FloatPow(self, expr: sympy.Expr) -> str:
|
||||
base, exp = expr.args
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"std::pow({self._print(base)}, {self._print(exp)})"
|
||||
|
||||
def _print_Pow(self, expr: sympy.Expr) -> str:
|
||||
# Uses float constants to perform FP div
|
||||
base, exp = expr.args
|
||||
|
||||
if exp == 0.5 or exp == -0.5:
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
base = self._print(base)
|
||||
return f"std::sqrt({base})" if exp == 0.5 else f"1.0/std::sqrt({base})"
|
||||
if exp.is_integer:
|
||||
exp = int(exp)
|
||||
if exp > 0:
|
||||
r = self.stringify([base] * exp, "*", PRECEDENCE["Mul"])
|
||||
elif exp < -1:
|
||||
r = (
|
||||
"1.0/("
|
||||
+ self.stringify([base] * abs(exp), "*", PRECEDENCE["Mul"])
|
||||
+ ")"
|
||||
)
|
||||
elif exp == -1:
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
r = "1.0/" + self._print(base)
|
||||
else: # exp == 0
|
||||
r = "1.0"
|
||||
|
||||
return f"static_cast<{INDEX_TYPE}>({r})" if expr.is_integer else r
|
||||
else:
|
||||
# TODO: float vs double
|
||||
return f"std::pow({base}, {float(exp)})"
|
||||
|
||||
def _print_Rational(self, expr: sympy.Expr) -> str:
|
||||
# Uses float constants to perform FP div
|
||||
if expr.q == 1:
|
||||
r = f"{expr.p}"
|
||||
else:
|
||||
r = f"{expr.p}.0/{expr.q}.0"
|
||||
return f"static_cast<{INDEX_TYPE}>({r})" if expr.is_integer else r
|
||||
|
||||
def _print_ceiling(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("ceiling expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
r = f"std::ceil({self._print(expr.args[0])})"
|
||||
return f"static_cast<{INDEX_TYPE}>({r})" if expr.is_integer else r
|
||||
|
||||
def _print_CeilToInt(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("CeilToInt expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
r = f"std::ceil({self._print(expr.args[0])})"
|
||||
return f"static_cast<{INDEX_TYPE}>({r})" if expr.is_integer else r
|
||||
|
||||
def _print_Min(self, expr: sympy.Expr) -> str:
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
args = [self._print(a) for a in expr.args]
|
||||
if len(args) == 2:
|
||||
return f"std::min(static_cast<{INDEX_TYPE}>({args[0]}), static_cast<{INDEX_TYPE}>({args[1]}))"
|
||||
else:
|
||||
# Initializer list overload
|
||||
il = "{" + ", ".join(args) + "}"
|
||||
return f"std::min<{INDEX_TYPE}>({il})"
|
||||
|
||||
def _print_Max(self, expr: sympy.Expr) -> str:
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
args = [self._print(a) for a in expr.args]
|
||||
if len(args) == 2:
|
||||
return f"std::max(static_cast<{INDEX_TYPE}>({args[0]}), static_cast<{INDEX_TYPE}>({args[1]}))"
|
||||
else:
|
||||
# Initializer list overload
|
||||
il = "{" + ", ".join(args) + "}"
|
||||
return f"std::max<{INDEX_TYPE}>({il})"
|
||||
|
||||
def _print_Abs(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("Abs expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"std::abs({self._print(expr.args[0])})"
|
||||
|
||||
def _print_OpaqueUnaryFn_cos(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("cos expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"std::cos({self._print(expr.args[0])})"
|
||||
|
||||
def _print_OpaqueUnaryFn_cosh(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("cosh expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"std::cosh({self._print(expr.args[0])})"
|
||||
|
||||
def _print_OpaqueUnaryFn_acos(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("acos expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"std::acos({self._print(expr.args[0])})"
|
||||
|
||||
def _print_OpaqueUnaryFn_sin(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("sin expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"math.sin({self._print(expr.args[0])})"
|
||||
|
||||
def _print_OpaqueUnaryFn_sinh(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("sinh expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"std::sinh({self._print(expr.args[0])})"
|
||||
|
||||
def _print_OpaqueUnaryFn_asin(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("asin expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"std::asin({self._print(expr.args[0])})"
|
||||
|
||||
def _print_OpaqueUnaryFn_tan(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("tan expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"std::tan({self._print(expr.args[0])})"
|
||||
|
||||
def _print_OpaqueUnaryFn_tanh(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("tanh expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"std::tanh({self._print(expr.args[0])})"
|
||||
|
||||
def _print_OpaqueUnaryFn_atan(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("atan expects exactly one argument")
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"std::atan({self._print(expr.args[0])})"
|
||||
|
||||
def _print_OpaqueUnaryFn_sqrt(self, expr: sympy.Expr) -> str:
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"std::sqrt({self._print(expr.args[0])})"
|
||||
|
||||
def _print_OpaqueUnaryFn_log2(self, expr: sympy.Expr) -> str:
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"std::log2({self._print(expr.args[0])})"
|
||||
|
||||
def _print_RoundToInt(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 1:
|
||||
raise AssertionError("RoundToInt expects exactly one argument")
|
||||
# TODO: dispatch to llrint depending on index type
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return f"std::lrint({self._print(expr.args[0])})"
|
||||
|
||||
def _print_RoundDecimal(self, expr: sympy.Expr) -> str:
|
||||
if len(expr.args) != 2:
|
||||
raise AssertionError("RoundDecimal expects exactly two arguments")
|
||||
number, ndigits = expr.args
|
||||
if number.is_integer:
|
||||
# ndigits < 0 should have been filtered by the sympy function
|
||||
if ndigits >= 0:
|
||||
raise AssertionError("ndigits must be negative for integer inputs")
|
||||
raise ValueError(
|
||||
f"For integer inputs, only non-negative ndigits are currently supported, but got {ndigits}."
|
||||
)
|
||||
number_str = self.parenthesize(number, PRECEDENCE["Mul"])
|
||||
return f"static_cast<double>(std::nearbyint(1e{ndigits} * {number_str}) * 1e{-ndigits})"
|
||||
|
||||
def _print_BooleanTrue(self, expr: sympy.Expr) -> str:
|
||||
return "true"
|
||||
|
||||
def _print_BooleanFalse(self, expr: sympy.Expr) -> str:
|
||||
return "false"
|
||||
|
||||
def _print_Infinity(self, expr: sympy.Expr) -> str:
|
||||
return "std::numeric_limits<double>::infinity()"
|
||||
|
||||
def _print_NegativeInfinity(self, expr: sympy.Expr) -> str:
|
||||
return f"-{self._print_Infinity(expr)}"
|
||||
|
||||
def _print_NaN(self, expr: sympy.Expr) -> str:
|
||||
return "std::numeric_limits<double>::quiet_NaN()"
|
||||
@@ -0,0 +1,615 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import math
|
||||
import operator
|
||||
from typing import NoReturn
|
||||
|
||||
import sympy
|
||||
|
||||
import torch
|
||||
from torch.utils._sympy.functions import (
|
||||
_keep_float,
|
||||
BitwiseFn_bitwise_and,
|
||||
BitwiseFn_bitwise_or,
|
||||
BitwiseFn_bitwise_xor,
|
||||
FloatPow,
|
||||
FloatTrueDiv,
|
||||
FloorDiv,
|
||||
IntTrueDiv,
|
||||
Max,
|
||||
Min,
|
||||
Mod,
|
||||
OpaqueUnaryFn_exp,
|
||||
OpaqueUnaryFn_log,
|
||||
OpaqueUnaryFn_log2,
|
||||
OpaqueUnaryFn_sqrt,
|
||||
PowByNatural,
|
||||
RoundDecimal,
|
||||
RoundToInt,
|
||||
ToFloat,
|
||||
TruncToInt,
|
||||
)
|
||||
|
||||
|
||||
# The sympy interpretation of operators. It will also sometimes work with
|
||||
# plain int/float, but if you do certain operations you will get out a
|
||||
# sympy.Basic in the end. If you want the Python/FX traceable interpretation,
|
||||
# check PythonReferenceAnalysis.
|
||||
# NB: For magic methods this needs to use normal magic methods
|
||||
# so that test_magic_methods works
|
||||
class ReferenceAnalysis:
|
||||
@staticmethod
|
||||
def constant(c, dtype):
|
||||
return sympy.sympify(c)
|
||||
|
||||
@staticmethod
|
||||
def or_(a, b):
|
||||
return a | b
|
||||
|
||||
@staticmethod
|
||||
def and_(a, b):
|
||||
return a & b
|
||||
|
||||
@staticmethod
|
||||
def eq(a, b):
|
||||
if isinstance(a, sympy.Expr) or isinstance(b, sympy.Expr):
|
||||
return sympy.Eq(a, b)
|
||||
return a == b
|
||||
|
||||
@classmethod
|
||||
def ne(cls, a, b):
|
||||
return cls.not_(cls.eq(a, b))
|
||||
|
||||
@staticmethod
|
||||
def lt(a, b):
|
||||
return a < b
|
||||
|
||||
@staticmethod
|
||||
def gt(a, b):
|
||||
return a > b
|
||||
|
||||
@staticmethod
|
||||
def le(a, b):
|
||||
return a <= b
|
||||
|
||||
@staticmethod
|
||||
def ge(a, b):
|
||||
return a >= b
|
||||
|
||||
@staticmethod
|
||||
def not_(a):
|
||||
if isinstance(a, bool):
|
||||
raise AssertionError("not_ needs sympy expr")
|
||||
return ~a
|
||||
|
||||
@staticmethod
|
||||
def reciprocal(x):
|
||||
return FloatTrueDiv(1.0, x)
|
||||
|
||||
@staticmethod
|
||||
def square(x):
|
||||
return PowByNatural(x, 2)
|
||||
|
||||
@staticmethod
|
||||
def trunc_to_int(x, dtype):
|
||||
return TruncToInt(x)
|
||||
|
||||
@staticmethod
|
||||
def ceil_to_int(x, dtype):
|
||||
return sympy.ceiling(x)
|
||||
|
||||
@staticmethod
|
||||
def floor_to_int(x, dtype):
|
||||
return sympy.floor(x)
|
||||
|
||||
@staticmethod
|
||||
def floor(x):
|
||||
return _keep_float(sympy.floor)(x)
|
||||
|
||||
@staticmethod
|
||||
def ceil(x):
|
||||
return _keep_float(sympy.ceiling)(x)
|
||||
|
||||
@staticmethod
|
||||
def to_dtype(x, dtype):
|
||||
if dtype == torch.float64:
|
||||
return ToFloat(x)
|
||||
raise NotImplementedError(f"to_dtype {dtype} NYI")
|
||||
|
||||
@staticmethod
|
||||
def mod(x, y):
|
||||
return Mod(x, y)
|
||||
|
||||
@staticmethod
|
||||
def abs(x):
|
||||
return abs(x)
|
||||
|
||||
@staticmethod
|
||||
def neg(x):
|
||||
return -x
|
||||
|
||||
@staticmethod
|
||||
def truediv(a, b):
|
||||
return FloatTrueDiv(a, b)
|
||||
|
||||
@staticmethod
|
||||
def int_truediv(a, b):
|
||||
return IntTrueDiv(a, b)
|
||||
|
||||
@staticmethod
|
||||
def floordiv(a, b):
|
||||
return FloorDiv(a, b)
|
||||
|
||||
@staticmethod
|
||||
def truncdiv(a, b) -> NoReturn:
|
||||
raise NotImplementedError("TODO: truncdiv")
|
||||
|
||||
@staticmethod
|
||||
def add(a, b):
|
||||
return _keep_float(operator.add)(a, b)
|
||||
|
||||
@classmethod
|
||||
def sym_sum(cls, args):
|
||||
return sympy.Add(*args)
|
||||
|
||||
@staticmethod
|
||||
def mul(a, b):
|
||||
return _keep_float(operator.mul)(a, b)
|
||||
|
||||
@staticmethod
|
||||
def sub(a, b):
|
||||
return _keep_float(operator.sub)(a, b)
|
||||
|
||||
@staticmethod
|
||||
def exp(x):
|
||||
return OpaqueUnaryFn_exp(x)
|
||||
|
||||
@staticmethod
|
||||
def log(x):
|
||||
return OpaqueUnaryFn_log(x)
|
||||
|
||||
@staticmethod
|
||||
def log2(x):
|
||||
return OpaqueUnaryFn_log2(x)
|
||||
|
||||
@staticmethod
|
||||
def sqrt(x):
|
||||
return OpaqueUnaryFn_sqrt(x)
|
||||
|
||||
@staticmethod
|
||||
def pow(a, b):
|
||||
# pyrefly: ignore [bad-argument-count, bad-argument-type]
|
||||
return _keep_float(FloatPow)(a, b)
|
||||
|
||||
@staticmethod
|
||||
def pow_by_natural(a, b):
|
||||
return PowByNatural(a, b)
|
||||
|
||||
@staticmethod
|
||||
def minimum(a, b):
|
||||
return Min(a, b)
|
||||
|
||||
@staticmethod
|
||||
def maximum(a, b):
|
||||
return Max(a, b)
|
||||
|
||||
@staticmethod
|
||||
def round_to_int(a, dtype):
|
||||
return RoundToInt(a)
|
||||
|
||||
@staticmethod
|
||||
def round_decimal(a, b):
|
||||
return RoundDecimal(a, b)
|
||||
|
||||
@staticmethod
|
||||
def bitwise_and(a, b):
|
||||
return BitwiseFn_bitwise_and(a, b)
|
||||
|
||||
@staticmethod
|
||||
def bitwise_or(a, b):
|
||||
return BitwiseFn_bitwise_or(a, b)
|
||||
|
||||
@staticmethod
|
||||
def bitwise_xor(a, b):
|
||||
return BitwiseFn_bitwise_xor(a, b)
|
||||
|
||||
|
||||
# Unlike ReferenceAnalysis, does NOT sympyify, instead, works with plain
|
||||
# Python types and is FX traceable. Inheritance here is purely for code
|
||||
# sharing (TODO: considering splitting out a BaseReferenceAnalysis).
|
||||
class PythonReferenceAnalysis(ReferenceAnalysis):
|
||||
@staticmethod
|
||||
def constant(c, dtype):
|
||||
if dtype is torch.int64:
|
||||
return int(c)
|
||||
elif dtype is torch.double:
|
||||
return float(c)
|
||||
elif dtype is torch.bool:
|
||||
return bool(c)
|
||||
else:
|
||||
raise AssertionError(f"unrecognized dtype {dtype}")
|
||||
|
||||
@staticmethod
|
||||
def not_(a):
|
||||
return torch.sym_not(a)
|
||||
|
||||
@classmethod
|
||||
def sym_sum(cls, args):
|
||||
if len(args) == 0:
|
||||
return 0
|
||||
if len(args) == 1:
|
||||
return args[0]
|
||||
acc = cls.add(args[0], args[1])
|
||||
for i in range(2, len(args)):
|
||||
acc = cls.add(acc, args[i])
|
||||
return acc
|
||||
|
||||
@staticmethod
|
||||
def floordiv(a, b):
|
||||
return a // b
|
||||
|
||||
@staticmethod
|
||||
def mod(x, y):
|
||||
return x % y
|
||||
|
||||
@staticmethod
|
||||
def python_mod(x, y):
|
||||
return x % y
|
||||
|
||||
@staticmethod
|
||||
def truncdiv(a, b):
|
||||
return a / b
|
||||
|
||||
@staticmethod
|
||||
def to_dtype(x, dtype):
|
||||
if dtype == torch.float64:
|
||||
return torch.sym_float(x)
|
||||
raise NotImplementedError(f"to_dtype {dtype} NYI")
|
||||
|
||||
@staticmethod
|
||||
def exp(x) -> NoReturn:
|
||||
raise AssertionError("exp is not valid shape sympy expr")
|
||||
|
||||
@staticmethod
|
||||
def log(x) -> NoReturn:
|
||||
raise AssertionError("log is not valid shape sympy expr")
|
||||
|
||||
@staticmethod
|
||||
def log2(x):
|
||||
return torch._sym_log2(x) # type: ignore[attr-defined]
|
||||
|
||||
@staticmethod
|
||||
def sqrt(x):
|
||||
return torch._sym_sqrt(x) # type: ignore[attr-defined]
|
||||
|
||||
@staticmethod
|
||||
def minimum(a, b):
|
||||
return torch.sym_min(a, b)
|
||||
|
||||
@staticmethod
|
||||
def maximum(a, b):
|
||||
return torch.sym_max(a, b)
|
||||
|
||||
@staticmethod
|
||||
def floor_to_int(x, dtype):
|
||||
return math.floor(x)
|
||||
|
||||
@staticmethod
|
||||
def ceil_to_int(x, dtype):
|
||||
return math.ceil(x)
|
||||
|
||||
@staticmethod
|
||||
def floor(x):
|
||||
return float(math.floor(x))
|
||||
|
||||
@staticmethod
|
||||
def ceil(x):
|
||||
return float(math.ceil(x))
|
||||
|
||||
@staticmethod
|
||||
def truediv(a, b):
|
||||
return a / b
|
||||
|
||||
@staticmethod
|
||||
def pow(a, b):
|
||||
return a**b
|
||||
|
||||
@staticmethod
|
||||
def pow_by_natural(a, b):
|
||||
# Pray that safe_pow is not needed here lol. In particular, this
|
||||
# never participates in VR low/high ranges, so overflow should be
|
||||
# unlikely
|
||||
return a**b
|
||||
|
||||
@staticmethod
|
||||
def round_to_int(a, dtype):
|
||||
return round(a)
|
||||
|
||||
@staticmethod
|
||||
def round_decimal(a, b):
|
||||
return round(a, ndigits=b)
|
||||
|
||||
@staticmethod
|
||||
def bitwise_and(a, b):
|
||||
return a & b
|
||||
|
||||
@staticmethod
|
||||
def bitwise_or(a, b):
|
||||
return a | b
|
||||
|
||||
@staticmethod
|
||||
def bitwise_xor(a, b):
|
||||
return a ^ b
|
||||
|
||||
@staticmethod
|
||||
def expr_cond_pair(expr, cond):
|
||||
return (expr, cond)
|
||||
|
||||
@staticmethod
|
||||
def piecewise(*pairs):
|
||||
# Build nested sym_ite from right to left.
|
||||
# Piecewise((e1, c1), (e2, c2), ..., (en, True)) becomes
|
||||
# sym_ite(c1, e1, sym_ite(c2, e2, ... en))
|
||||
result = pairs[-1][0]
|
||||
for expr, cond in reversed(pairs[:-1]):
|
||||
result = torch.sym_ite(cond, expr, result)
|
||||
return result
|
||||
|
||||
|
||||
# Like PythonReferenceAnalysis, but some export-unfriendly choices of
|
||||
# operators to make things faster
|
||||
class OptimizedPythonReferenceAnalysis(PythonReferenceAnalysis):
|
||||
@staticmethod
|
||||
def sym_sum(args):
|
||||
return torch.sym_sum(args)
|
||||
|
||||
|
||||
def _to_dtype(x: torch.Tensor, dtype: torch.dtype) -> torch.Tensor:
|
||||
return torch.ops.prims.convert_element_type.default(x, dtype)
|
||||
|
||||
|
||||
# Suppose we have some int/float arguments. This diagram commutes:
|
||||
#
|
||||
# int/float -- PythonReferenceAnalysis.op --> int/float
|
||||
# | |
|
||||
# | |
|
||||
# torch.tensor(..., dtype=torch.int64/torch.float64)
|
||||
# | |
|
||||
# V V
|
||||
# Tensor -- TensorReferenceAnalysis.op --> Tensor
|
||||
#
|
||||
# NB: int before and after must be representable in int64 (we will
|
||||
# insert guards accordingly.)
|
||||
#
|
||||
# This is guaranteed to be FX traceable with OpOverloads only.
|
||||
class TensorReferenceAnalysis:
|
||||
# NB: This is actually dead, because with Proxy tracing the factory
|
||||
# function isn't traced correctly. Here for completeness.
|
||||
@staticmethod
|
||||
def constant(c, dtype):
|
||||
d: int | float | bool
|
||||
if dtype is torch.int64:
|
||||
d = int(c)
|
||||
elif dtype is torch.double:
|
||||
d = float(c)
|
||||
elif dtype is torch.bool:
|
||||
d = bool(c)
|
||||
else:
|
||||
raise AssertionError(f"unrecognized dtype {dtype}")
|
||||
return torch.ops.aten.scalar_tensor.default(d, dtype=dtype)
|
||||
|
||||
@staticmethod
|
||||
def or_(a, b):
|
||||
return torch.ops.aten.logical_or.default(a, b)
|
||||
|
||||
@staticmethod
|
||||
def and_(a, b):
|
||||
return torch.ops.aten.logical_and.default(a, b)
|
||||
|
||||
@staticmethod
|
||||
def bitwise_and(a, b):
|
||||
return torch.ops.aten.bitwise_and(a, b)
|
||||
|
||||
@staticmethod
|
||||
def bitwise_or(a, b):
|
||||
return torch.ops.aten.bitwise_or(a, b)
|
||||
|
||||
@staticmethod
|
||||
def bitwise_xor(a, b):
|
||||
return torch.ops.aten.bitwise_xor(a, b)
|
||||
|
||||
@staticmethod
|
||||
def eq(a, b):
|
||||
return torch.ops.aten.eq.Tensor(a, b)
|
||||
|
||||
@classmethod
|
||||
def ne(cls, a, b):
|
||||
return torch.ops.aten.ne.Tensor(a, b)
|
||||
|
||||
@staticmethod
|
||||
def lt(a, b):
|
||||
return torch.ops.aten.lt.Tensor(a, b)
|
||||
|
||||
@staticmethod
|
||||
def gt(a, b):
|
||||
return torch.ops.aten.gt.Tensor(a, b)
|
||||
|
||||
@staticmethod
|
||||
def le(a, b):
|
||||
return torch.ops.aten.le.Tensor(a, b)
|
||||
|
||||
@staticmethod
|
||||
def ge(a, b):
|
||||
return torch.ops.aten.ge.Tensor(a, b)
|
||||
|
||||
@staticmethod
|
||||
def not_(a):
|
||||
return torch.ops.aten.logical_not.default(a)
|
||||
|
||||
@staticmethod
|
||||
def reciprocal(x):
|
||||
return torch.ops.aten.reciprocal.default(x)
|
||||
|
||||
@staticmethod
|
||||
def square(x):
|
||||
# TODO: maybe composite implicit autograd doesn't work here?
|
||||
return torch.ops.aten.square.default(x)
|
||||
|
||||
@staticmethod
|
||||
def trunc_to_int(x, dtype):
|
||||
return _to_dtype(torch.ops.aten.trunc.default(x), dtype)
|
||||
|
||||
@staticmethod
|
||||
def ceil_to_int(x, dtype):
|
||||
return _to_dtype(torch.ops.aten.ceil.default(x), dtype)
|
||||
|
||||
@staticmethod
|
||||
def floor_to_int(x, dtype):
|
||||
return _to_dtype(torch.ops.aten.floor.default(x), dtype)
|
||||
|
||||
@staticmethod
|
||||
def floor(x):
|
||||
return torch.ops.aten.floor.default(x)
|
||||
|
||||
@staticmethod
|
||||
def ceil(x):
|
||||
return torch.ops.aten.ceil.default(x)
|
||||
|
||||
@staticmethod
|
||||
def to_dtype(x, dtype):
|
||||
return _to_dtype(x, dtype)
|
||||
|
||||
@staticmethod
|
||||
def mod(x, y) -> NoReturn:
|
||||
# TODO: https://github.com/pytorch/pytorch/pull/133654
|
||||
raise NotImplementedError(
|
||||
"no C-style modulus operation available from frontend atm"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def abs(x):
|
||||
return torch.ops.aten.abs.default(x)
|
||||
|
||||
@staticmethod
|
||||
def neg(x):
|
||||
return torch.ops.aten.neg.default(x)
|
||||
|
||||
@staticmethod
|
||||
def truediv(a, b):
|
||||
return torch.ops.aten.true_divide.Tensor(a, b)
|
||||
|
||||
@staticmethod
|
||||
def int_truediv(a, b):
|
||||
raise NotImplementedError(
|
||||
"Python int truediv difficult to implement in PyTorch atm"
|
||||
)
|
||||
|
||||
# TODO: This is wrong, CPython has a custom implementation of true
|
||||
# division that results in higher precision when the floats are
|
||||
# sufficiently large. Short term fix: add a guard here
|
||||
# pyrefly: ignore [unreachable]
|
||||
return torch.ops.aten.true_divide.default(
|
||||
_to_dtype(a, torch.float64), _to_dtype(b, torch.float64)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def floordiv(a, b):
|
||||
return torch.ops.aten.div.Tensor_mode(a, b, rounding_mode="floor")
|
||||
|
||||
@staticmethod
|
||||
def truncdiv(a, b) -> NoReturn:
|
||||
raise NotImplementedError(
|
||||
"no C-style truncdiv operation available from frontend atm"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def add(a, b):
|
||||
return torch.ops.aten.add.Tensor(a, b)
|
||||
|
||||
@staticmethod
|
||||
def mul(a, b):
|
||||
return torch.ops.aten.mul.Tensor(a, b)
|
||||
|
||||
@staticmethod
|
||||
def sub(a, b):
|
||||
return torch.ops.aten.sub.Tensor(a, b)
|
||||
|
||||
@staticmethod
|
||||
def exp(x):
|
||||
return torch.ops.aten.exp.default(x)
|
||||
|
||||
@staticmethod
|
||||
def log(x):
|
||||
return torch.ops.aten.log.default(x)
|
||||
|
||||
@staticmethod
|
||||
def log2(x):
|
||||
return torch.ops.aten.log2.default(x)
|
||||
|
||||
@staticmethod
|
||||
def sqrt(x):
|
||||
return torch.ops.aten.sqrt.default(x)
|
||||
|
||||
@staticmethod
|
||||
def sin(x):
|
||||
return torch.ops.aten.sin.default(x)
|
||||
|
||||
@staticmethod
|
||||
def cos(x):
|
||||
return torch.ops.aten.cos.default(x)
|
||||
|
||||
@staticmethod
|
||||
def tanh(x):
|
||||
return torch.ops.aten.tanh.default(x)
|
||||
|
||||
@staticmethod
|
||||
def sinh(x):
|
||||
return torch.ops.aten.sinh.default(x)
|
||||
|
||||
@staticmethod
|
||||
def cosh(x):
|
||||
return torch.ops.aten.cosh.default(x)
|
||||
|
||||
@staticmethod
|
||||
def tan(x):
|
||||
return torch.ops.aten.tan.default(x)
|
||||
|
||||
@staticmethod
|
||||
def acos(x):
|
||||
return torch.ops.aten.acos.default(x)
|
||||
|
||||
@staticmethod
|
||||
def atan(x):
|
||||
return torch.ops.aten.atan.default(x)
|
||||
|
||||
@staticmethod
|
||||
def asin(x):
|
||||
return torch.ops.aten.asin.default(x)
|
||||
|
||||
@staticmethod
|
||||
def pow(a, b):
|
||||
return torch.ops.aten.pow.Tensor_Tensor(a, b)
|
||||
|
||||
@staticmethod
|
||||
def pow_by_natural(a, b):
|
||||
# NB: pow handles int x int fine
|
||||
return torch.ops.aten.pow.Tensor_Tensor(a, b)
|
||||
|
||||
@staticmethod
|
||||
def minimum(a, b):
|
||||
return torch.ops.aten.minimum.default(a, b)
|
||||
|
||||
@staticmethod
|
||||
def maximum(a, b):
|
||||
return torch.ops.aten.maximum.default(a, b)
|
||||
|
||||
@staticmethod
|
||||
def round_to_int(a, dtype):
|
||||
return torch.ops.aten.round.default(a)
|
||||
|
||||
@staticmethod
|
||||
def round_decimal(a, b) -> NoReturn:
|
||||
raise NotImplementedError(
|
||||
"round decimal doesn't support Tensor second argument atm"
|
||||
)
|
||||
|
||||
# return torch.ops.aten.round.decimals(a, b)
|
||||
@@ -0,0 +1,96 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import sympy
|
||||
from sympy.multipledispatch import dispatch
|
||||
|
||||
|
||||
__all__ = ["SingletonInt"]
|
||||
|
||||
|
||||
class SingletonInt(sympy.AtomicExpr):
|
||||
# This is probably not super important unless we are in multiple dispatch
|
||||
# situations with other more exotic Expr types.
|
||||
_op_priority = 99999
|
||||
|
||||
def __new__(cls, *args, coeff=None, **kwargs):
|
||||
instance = super().__new__(cls, *args, **kwargs)
|
||||
return instance
|
||||
|
||||
# The semantics of this class should match that of NestedIntSymNodeImpl in
|
||||
# c10/core/NestedIntSymNodeImpl.h
|
||||
def __init__(self, val, *, coeff=1) -> None:
|
||||
self._val = val
|
||||
self._coeff = coeff
|
||||
super().__init__()
|
||||
|
||||
# See NOTE [ Inequalities with nested int ]
|
||||
def _eval_Eq(self, other):
|
||||
if (
|
||||
isinstance(other, SingletonInt)
|
||||
and other._val == self._val
|
||||
and self._coeff == other._coeff
|
||||
):
|
||||
return sympy.true
|
||||
else:
|
||||
return sympy.false
|
||||
|
||||
# This is necessary so that calling expr.free_symbols on exprs that contain
|
||||
# this Singleton does not error
|
||||
@property
|
||||
def free_symbols(self):
|
||||
return set()
|
||||
|
||||
def __mul__(self, other):
|
||||
if isinstance(other, SingletonInt):
|
||||
raise ValueError(
|
||||
"SingletonInt cannot be multiplied by another SingletonInt"
|
||||
)
|
||||
return SingletonInt(self._val, coeff=self._coeff * other)
|
||||
|
||||
def __rmul__(self, other):
|
||||
if isinstance(other, SingletonInt):
|
||||
raise ValueError(
|
||||
"SingletonInt cannot be multiplied by another SingletonInt"
|
||||
)
|
||||
return SingletonInt(self._val, coeff=self._coeff * other)
|
||||
|
||||
# Make sure we promptly raise an error instead of falling back to building
|
||||
# an expression tree. There are probably more ops, how can we be exhaustive?
|
||||
def __add__(self, other):
|
||||
raise NotImplementedError("NYI")
|
||||
|
||||
def __sub__(self, other):
|
||||
raise NotImplementedError("NYI")
|
||||
|
||||
def __truediv__(self, other):
|
||||
raise NotImplementedError("NYI")
|
||||
|
||||
def __floordiv__(self, other):
|
||||
raise NotImplementedError("NYI")
|
||||
|
||||
def __mod__(self, other):
|
||||
raise NotImplementedError("NYI")
|
||||
|
||||
|
||||
# See NOTE [ Inequalities with nested int ]
|
||||
@dispatch(sympy.Integer, SingletonInt)
|
||||
def _eval_is_ge(a, b):
|
||||
if a < 2:
|
||||
return sympy.false
|
||||
raise ValueError("Symbolic SingletonInt: Relation is indeterminate")
|
||||
|
||||
|
||||
@dispatch(SingletonInt, sympy.Integer) # type: ignore[no-redef]
|
||||
def _eval_is_ge(a, b): # noqa: F811
|
||||
if b <= 2:
|
||||
return sympy.true
|
||||
raise ValueError("Symbolic SingletonInt: Relation is indeterminate")
|
||||
|
||||
|
||||
@dispatch(SingletonInt, SingletonInt) # type: ignore[no-redef]
|
||||
def _eval_is_ge(a, b): # noqa: F811
|
||||
if a._val == b._val:
|
||||
if a._coeff >= b._coeff:
|
||||
return sympy.true
|
||||
else:
|
||||
return sympy.false
|
||||
raise ValueError("Symbolic SingletonInt: Relation is indeterminate")
|
||||
@@ -0,0 +1,179 @@
|
||||
import logging
|
||||
|
||||
import sympy
|
||||
|
||||
from torch.utils._sympy.functions import FloorDiv
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_MIRROR_REL_OP: dict[type[sympy.Basic], type[sympy.Rel]] = {
|
||||
sympy.Eq: sympy.Eq,
|
||||
sympy.Ne: sympy.Ne,
|
||||
sympy.Ge: sympy.Le,
|
||||
sympy.Gt: sympy.Lt,
|
||||
sympy.Le: sympy.Ge,
|
||||
sympy.Lt: sympy.Gt,
|
||||
}
|
||||
|
||||
INEQUALITY_TYPES = (sympy.Gt, sympy.Ge, sympy.Lt, sympy.Le)
|
||||
|
||||
|
||||
def mirror_rel_op(type: type) -> type[sympy.Rel] | None:
|
||||
return _MIRROR_REL_OP.get(type)
|
||||
|
||||
|
||||
# Tries to simplify 'expr', so as to leave only 'thing' in the left-hand side.
|
||||
#
|
||||
# Returns a tuple of:
|
||||
# 1. The simplified expression
|
||||
# 2. The expression on the right-hand side
|
||||
#
|
||||
# Returns 'None' if it can't reach a state where the only thing in the left
|
||||
# hand side is 'thing'.
|
||||
#
|
||||
# 'trials': number of times 'try_solve' will try to isolate 'thing' to the
|
||||
# left-hand side.
|
||||
#
|
||||
# 'floordiv_inequality': flag to enable conversion of 'FloorDiv' into
|
||||
# inequalities.
|
||||
def try_solve(
|
||||
expr: sympy.Basic,
|
||||
thing: sympy.Basic,
|
||||
trials: int = 5,
|
||||
floordiv_inequality: bool = True,
|
||||
) -> tuple[sympy.Rel, sympy.Expr] | None:
|
||||
mirror = mirror_rel_op(type(expr))
|
||||
|
||||
# Ignore unsupported expressions:
|
||||
# - Those that are not relational operations
|
||||
# - Those that don't have a mirror (just avoiding unexpected classes)
|
||||
if not isinstance(expr, sympy.Rel) or mirror is None:
|
||||
log.debug("expression with unsupported type: %s", type(expr))
|
||||
return None
|
||||
|
||||
lhs_has_thing = expr.lhs.has(thing)
|
||||
rhs_has_thing = expr.rhs.has(thing)
|
||||
|
||||
# Give up when 'thing' appears on both sides of the relational expression.
|
||||
# That is because, as is, we assume the thing we are trying to isolate is
|
||||
# only on the right-hand side.
|
||||
if lhs_has_thing and rhs_has_thing:
|
||||
log.debug("thing (%s) found in both sides of expression: %s", thing, expr)
|
||||
return None
|
||||
|
||||
# Try considering both LHS and RHS by mirroring the original expression:
|
||||
# a < b ==> b > a
|
||||
expressions = []
|
||||
|
||||
# Add each version of 'expr' if 'thing' is in its left-hand side.
|
||||
if lhs_has_thing:
|
||||
expressions.append(expr)
|
||||
if rhs_has_thing:
|
||||
expressions.append(mirror(expr.rhs, expr.lhs))
|
||||
|
||||
for e in expressions:
|
||||
if e is None:
|
||||
continue
|
||||
|
||||
if not isinstance(e, sympy.Rel):
|
||||
raise AssertionError("expected sympy.Rel")
|
||||
|
||||
for _ in range(trials):
|
||||
trial = _try_isolate_lhs(e, thing, floordiv_inequality=floordiv_inequality)
|
||||
# Stop if there was no change in this trial.
|
||||
if trial == e:
|
||||
break
|
||||
e = trial # type: ignore[assignment]
|
||||
|
||||
# Return if we were able to isolate 'thing' on the left-hand side.
|
||||
if isinstance(e, sympy.Rel) and e.lhs == thing:
|
||||
log.debug("solved: %s ---> %s", expr, e)
|
||||
return e, e.rhs
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _try_isolate_lhs(
|
||||
e: sympy.Basic, thing: sympy.Basic, floordiv_inequality: bool
|
||||
) -> sympy.Basic:
|
||||
op = type(e)
|
||||
|
||||
if isinstance(e, sympy.Rel):
|
||||
# Move any constants in the left-hand side to the right-hand side.
|
||||
lhs_not_thing = (
|
||||
sum(a for a in e.lhs.args if not a.has(thing))
|
||||
if isinstance(e.lhs, sympy.Add)
|
||||
else 0
|
||||
)
|
||||
e = op(e.lhs - lhs_not_thing, e.rhs - lhs_not_thing) # type: ignore[attr-defined]
|
||||
|
||||
# Divide both sides by the factors that don't contain thing.
|
||||
if isinstance(e, sympy.Rel) and isinstance(e.lhs, sympy.Mul):
|
||||
lhs, rhs = e.args
|
||||
other = sympy.Mul(*[a for a in lhs.args if not a.has(thing)])
|
||||
|
||||
# If we can't tell whether 'other' is negative or positive, we do nothing.
|
||||
# That is because we don't know whether we have mirror the operation or not.
|
||||
# We also divide only when we know 'rhs' is not zero.
|
||||
if not (isinstance(e, INEQUALITY_TYPES) and other.is_negative is None) and not (
|
||||
not isinstance(e, INEQUALITY_TYPES) and rhs.is_zero
|
||||
):
|
||||
# Divide both sides by 'other'.
|
||||
lhs = lhs / other
|
||||
rhs = rhs / other
|
||||
|
||||
# If 'e' is an inequality and 'other' is negative, we have to
|
||||
# mirror the expression.
|
||||
if isinstance(e, INEQUALITY_TYPES) and other.is_negative:
|
||||
op = mirror_rel_op(op) # type: ignore[assignment]
|
||||
|
||||
if op is None:
|
||||
raise AssertionError("expected op to be not None")
|
||||
e = op(lhs, rhs)
|
||||
|
||||
################################################################################
|
||||
# left-hand side is FloorDiv
|
||||
################################################################################
|
||||
#
|
||||
# Given the expression: a // b op c
|
||||
# where 'op' is a relational operation, these rules only work if:
|
||||
# - b > 0
|
||||
# - c is an integer
|
||||
if (
|
||||
floordiv_inequality
|
||||
and isinstance(e, sympy.Rel)
|
||||
and isinstance(e.lhs, FloorDiv)
|
||||
and e.lhs.divisor.is_positive
|
||||
and e.rhs.is_integer
|
||||
):
|
||||
# a // b == expr
|
||||
# => a >= (b * expr) and a < (b * (expr + 1))
|
||||
if isinstance(e, sympy.Eq):
|
||||
numerator, denominator = e.lhs.args
|
||||
return sympy.And(
|
||||
sympy.Ge(numerator, (e.rhs * denominator)),
|
||||
sympy.Lt(numerator, ((e.rhs + 1) * denominator)),
|
||||
)
|
||||
# a // b != expr
|
||||
# => a < (b * expr) or a >= (b * (expr + 1))
|
||||
if isinstance(e, sympy.Ne):
|
||||
numerator, denominator = e.lhs.args
|
||||
return sympy.Or(
|
||||
sympy.Lt(numerator, (e.rhs * denominator)),
|
||||
sympy.Ge(numerator, ((e.rhs + 1) * denominator)),
|
||||
)
|
||||
# The transformations below only work if b is positive.
|
||||
# Note: we only have this information for constants.
|
||||
# a // b > expr => a >= b * (expr + 1)
|
||||
# a // b >= expr => a >= b * expr
|
||||
if isinstance(e, (sympy.Gt, sympy.Ge)):
|
||||
quotient = e.rhs if isinstance(e, sympy.Ge) else (e.rhs + 1)
|
||||
return sympy.Ge(e.lhs.args[0], (quotient * e.lhs.args[1]))
|
||||
# a // b < expr => a < b * expr
|
||||
# a // b <= expr => a < b * (expr + 1)
|
||||
if isinstance(e, (sympy.Lt, sympy.Le)):
|
||||
quotient = e.rhs if isinstance(e, sympy.Lt) else (e.rhs + 1)
|
||||
return sympy.Lt(e.lhs.args[0], (quotient * e.lhs.args[1]))
|
||||
|
||||
return e
|
||||
@@ -0,0 +1,101 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""
|
||||
This file contains canonical definitions for our symbol naming conventions,
|
||||
across torch.fx.experimental.symbolic_shapes and torch._inductor. The
|
||||
intention is:
|
||||
|
||||
1. To make it easily greppable where all the sites we use a prefix are
|
||||
2. Make it possible to easily tell if we can introduce a new prefix without
|
||||
introducing a conflict
|
||||
|
||||
You can occasionally test if prefixes have been hardcoded by renaming prefixes
|
||||
in this file and seeing what breaks.
|
||||
"""
|
||||
|
||||
from collections.abc import Iterable
|
||||
from enum import auto, Enum
|
||||
|
||||
import sympy
|
||||
|
||||
|
||||
class SymT(Enum):
|
||||
SIZE = auto()
|
||||
FLOAT = auto()
|
||||
UNBACKED_INT = auto()
|
||||
UNBACKED_FLOAT = auto()
|
||||
# Inductor: The intermediates in inner_fn tmp0, one generated per ops call.
|
||||
# If one of these shows up in an indexing expression, that means an
|
||||
# indirect load is happening.
|
||||
TMP = auto()
|
||||
# Inductor: Placeholder variable that is later replaced with TMP
|
||||
INDIRECT = auto()
|
||||
# Inductor: Some size expressions are replaced with a precomputed size ps0
|
||||
# which is computed host side, and then directly reused in the kernel, so
|
||||
# we don't repeatedly recompute it on device.
|
||||
PRECOMPUTED_SIZE = auto()
|
||||
# Inductor: An indexing variable i0 in loops IR which ranges over non-reduced
|
||||
# dim in the loop
|
||||
INDEX = auto()
|
||||
# Inductor: A reduction indexing (r0, r1) variables in loops IR which ranges over
|
||||
# reduced dim(s) in the loop
|
||||
R0_INDEX = auto()
|
||||
R1_INDEX = auto()
|
||||
# Inductor: In templated kernels torch._inductor.kernel, we have a hook to
|
||||
# store the final output and append epilogue fusions. To do this, we must
|
||||
# know what the indexes the outputs range over. NB: These will also
|
||||
# advertise as INDEX, this is... probably OK?
|
||||
TEMPLATE_INDEX = auto()
|
||||
# Inductor: iteration domain for blockIdx.x/blockIdx.y
|
||||
XBLOCK = auto()
|
||||
YBLOCK = auto()
|
||||
ZBLOCK = auto()
|
||||
# Inductor: this is used solely for dynamic_reshape_indexer
|
||||
VIEW = auto()
|
||||
# Alternate (non-modular) indexing used in halide kernels
|
||||
HALIDE = auto()
|
||||
|
||||
|
||||
# Invariant: there must not be a prefix which is a prefix of another string,
|
||||
# as this introduces ambiguity
|
||||
prefix_str = {
|
||||
SymT.SIZE: "s", # integer
|
||||
SymT.UNBACKED_INT: "u", # integer
|
||||
# Prefix z here is chosen to avoid false aliasing in symbol_is_type test
|
||||
# DO NOT add a "z" type. You also need to avoid conflicts on these
|
||||
# prefixes but this is somewhat easier to manage
|
||||
SymT.FLOAT: "zf",
|
||||
SymT.UNBACKED_FLOAT: "zuf",
|
||||
SymT.TMP: "tmp",
|
||||
SymT.PRECOMPUTED_SIZE: "ps",
|
||||
SymT.INDEX: "i",
|
||||
SymT.R0_INDEX: "r0_",
|
||||
SymT.R1_INDEX: "r1_",
|
||||
SymT.TEMPLATE_INDEX: "idx",
|
||||
SymT.XBLOCK: "x",
|
||||
SymT.YBLOCK: "y",
|
||||
SymT.ZBLOCK: "z",
|
||||
SymT.INDIRECT: "indirect", # false aliasing?
|
||||
SymT.VIEW: "view",
|
||||
SymT.HALIDE: "h",
|
||||
}
|
||||
|
||||
|
||||
def make_symbol(prefix: SymT, idx: int, **kwargs) -> sympy.Symbol:
|
||||
# TODO: maybe put the assumptions here directly
|
||||
return sympy.Symbol(f"{prefix_str[prefix]}{idx}", **kwargs)
|
||||
|
||||
|
||||
# This type is a little wider than it should be, because free_symbols says
|
||||
# that it contains Basic, rather than Symbol
|
||||
def symbol_is_type(sym: sympy.Basic, prefix: SymT | Iterable[SymT]) -> bool:
|
||||
if not isinstance(sym, sympy.Symbol):
|
||||
raise AssertionError("expected sympy.Symbol")
|
||||
name_str = sym.name.lower() # Match capitalized names like XBLOCK, RBLOCK
|
||||
if isinstance(prefix, SymT):
|
||||
return name_str.startswith(prefix_str[prefix])
|
||||
else:
|
||||
return name_str.startswith(tuple(prefix_str[p] for p in prefix))
|
||||
|
||||
|
||||
def free_symbol_is_type(e: sympy.Expr, prefix: SymT | Iterable[SymT]) -> bool:
|
||||
return any(symbol_is_type(v, prefix) for v in e.free_symbols)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
from collections.abc import Callable
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
|
||||
R = TypeVar("R")
|
||||
|
||||
|
||||
class Thunk(Generic[R]):
|
||||
"""
|
||||
A simple lazy evaluation implementation that lets you delay
|
||||
execution of a function. It properly handles releasing the
|
||||
function once it is forced.
|
||||
"""
|
||||
|
||||
f: Callable[[], R] | None
|
||||
r: R | None
|
||||
|
||||
__slots__ = ["f", "r"]
|
||||
|
||||
def __init__(self, f: Callable[[], R]) -> None:
|
||||
self.f = f
|
||||
self.r = None
|
||||
|
||||
def force(self) -> R:
|
||||
if self.f is None:
|
||||
return self.r # type: ignore[return-value]
|
||||
self.r = self.f()
|
||||
self.f = None
|
||||
return self.r
|
||||
@@ -0,0 +1,260 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import contextlib
|
||||
import inspect
|
||||
import os.path
|
||||
import tempfile
|
||||
import traceback
|
||||
from types import TracebackType
|
||||
|
||||
|
||||
# This file contains utilities for ensuring dynamically compile()'d
|
||||
# code fragments display their line numbers in backtraces.
|
||||
#
|
||||
# The constraints:
|
||||
#
|
||||
# - We don't have control over the user exception printer (in particular,
|
||||
# we cannot assume the linecache trick will work, c.f.
|
||||
# https://stackoverflow.com/q/50515651/23845 )
|
||||
#
|
||||
# - We don't want to create temporary files every time we compile()
|
||||
# some code; file creation should happen lazily only at exception
|
||||
# time. Arguably, you *should* be willing to write out your
|
||||
# generated Python code to file system, but in some situations
|
||||
# (esp. library code) it would violate user expectation to write
|
||||
# to the file system, so we try to avoid it. In particular, we'd
|
||||
# like to keep the files around, so users can open up the files
|
||||
# mentioned in the trace; if the file is invisible, we want to
|
||||
# avoid clogging up the filesystem.
|
||||
#
|
||||
# If this is not a constraint for you, there is a substantially simpler
|
||||
# way to implement the functionality in this PR: instead of using
|
||||
# eval/exec directly, just always write a Python file to filesystem
|
||||
# and compile that.
|
||||
#
|
||||
# - You have control over a context where the compiled code will get
|
||||
# executed, so that we can interpose while the stack is unwinding
|
||||
# (otherwise, we have no way to interpose on the exception printing
|
||||
# process.)
|
||||
#
|
||||
# There are two things you have to do to make use of the utilities here:
|
||||
#
|
||||
# - When you compile your source code, you must save its string source
|
||||
# in its f_globals under the magic name "__compile_source__"
|
||||
#
|
||||
# - Before running the compiled code, enter the
|
||||
# report_compile_source_on_error() context manager.
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def report_compile_source_on_error():
|
||||
try:
|
||||
yield
|
||||
except Exception as exc:
|
||||
tb = exc.__traceback__
|
||||
|
||||
# Walk the traceback, looking for frames that have
|
||||
# source attached
|
||||
stack = []
|
||||
while tb is not None:
|
||||
filename = tb.tb_frame.f_code.co_filename
|
||||
source = tb.tb_frame.f_globals.get("__compile_source__")
|
||||
|
||||
if filename == "<string>" and source is not None:
|
||||
# What black magic are we doing here? Intuitively, what
|
||||
# we would like to do is overwrite the co_filename on any
|
||||
# frames that were generated from exec/eval so that they
|
||||
# point to a temporary file that has the actual line
|
||||
# information, so Python's default error printer can print
|
||||
# useful line information on it.
|
||||
#
|
||||
# Writing out the temporary file is easy. But overwriting
|
||||
# co_filename is not! You can't modify the code object
|
||||
# associated with a frame. You can, however, reconstruct
|
||||
# a traceback with entirely new frames from scratch, so that's
|
||||
# what we do. But there's another problem, which is how to
|
||||
# make the frame?
|
||||
#
|
||||
# The black magic is we make a frankenstein frame and code
|
||||
# object which resembles the original frame/code enough so
|
||||
# that it will print properly under traceback and the default
|
||||
# error printer, but IT IS NOT THE ORIGINAL FRAME (you
|
||||
# couldn't, e.g., execute its code with different variables
|
||||
# and expect it to work.)
|
||||
|
||||
# Don't delete the temporary file so the user can inspect it
|
||||
# TODO: This creates a temporary file for every frame, but we
|
||||
# technically only need one per distinct __compile_source__
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", delete=False, suffix=".py"
|
||||
) as f:
|
||||
f.write(source)
|
||||
# Create a frame. Python doesn't let you construct
|
||||
# FrameType directly, so just make one with compile
|
||||
frame = tb.tb_frame
|
||||
code = compile("__inspect_currentframe()", f.name, "eval")
|
||||
code = code.replace(co_name=frame.f_code.co_name)
|
||||
# Python 3.11 only
|
||||
if hasattr(frame.f_code, "co_linetable"):
|
||||
# We can't copy ALL of the metadata over, because you
|
||||
# can cause Python to segfault this way. What exactly
|
||||
# do we need? We need enough information for
|
||||
# traceback to be able to print the exception
|
||||
# correctly. Code reading Lib/traceback.py reveals
|
||||
# that traceback calls code.co_positions() in order to
|
||||
# get the augmented line/col numbers. Objects/codeobject.c,
|
||||
# specifically _PyCode_InitAddressRange, reveals that
|
||||
# this iterator is initialized from co_linetable and
|
||||
# co_firstfileno. So copy these we must!
|
||||
code = code.replace( # type: ignore[call-arg]
|
||||
co_linetable=frame.f_code.co_linetable, # type: ignore[attr-defined]
|
||||
co_firstlineno=frame.f_code.co_firstlineno, # type: ignore[attr-defined]
|
||||
)
|
||||
fake_frame = eval(
|
||||
code,
|
||||
frame.f_globals,
|
||||
{**frame.f_locals, "__inspect_currentframe": inspect.currentframe},
|
||||
)
|
||||
fake_tb = TracebackType(None, fake_frame, tb.tb_lasti, tb.tb_lineno)
|
||||
stack.append(fake_tb)
|
||||
else:
|
||||
stack.append(tb)
|
||||
|
||||
tb = tb.tb_next
|
||||
|
||||
# Reconstruct the linked list
|
||||
tb_next = None
|
||||
for tb in reversed(stack):
|
||||
tb.tb_next = tb_next
|
||||
tb_next = tb
|
||||
|
||||
raise exc.with_traceback(tb_next) # noqa: B904
|
||||
|
||||
|
||||
def shorten_filename(fn, *, base=None):
|
||||
"""Shorten a source filepath, with the assumption that torch/ subdirectories don't need to be shown to user."""
|
||||
if base is None:
|
||||
base = os.path.dirname(os.path.dirname(__file__))
|
||||
# Truncate torch/foo.py to foo.py
|
||||
try:
|
||||
prefix = os.path.commonpath([fn, base])
|
||||
except ValueError:
|
||||
return fn
|
||||
else:
|
||||
return fn[len(prefix) + 1 :]
|
||||
|
||||
|
||||
def format_frame(frame, *, base=None, line=False) -> str:
|
||||
"""
|
||||
Format a FrameSummary in a short way, without printing full absolute path or code.
|
||||
|
||||
The idea is the result fits on a single line.
|
||||
"""
|
||||
extra_line = ""
|
||||
if line:
|
||||
extra_line = f"{frame.line} # "
|
||||
return f"{extra_line}{shorten_filename(frame.filename, base=base)}:{frame.lineno} in {frame.name}"
|
||||
|
||||
|
||||
def format_traceback_short(tb):
|
||||
"""Format a TracebackType in a short way, printing only the inner-most frame."""
|
||||
return format_frame(traceback.extract_tb(tb)[-1])
|
||||
|
||||
|
||||
class CapturedTraceback:
|
||||
__slots__ = ["tb", "skip"]
|
||||
|
||||
def __init__(self, tb, skip=0) -> None:
|
||||
self.tb = tb
|
||||
self.skip = skip
|
||||
|
||||
def cleanup(self) -> None:
|
||||
self.tb = None
|
||||
|
||||
def summary(self):
|
||||
import torch._C._profiler
|
||||
|
||||
if self.tb is None:
|
||||
# TODO: Maybe indicate that the traceback was elided?
|
||||
return traceback.StackSummary()
|
||||
|
||||
return _extract_symbolized_tb(
|
||||
torch._C._profiler.symbolize_tracebacks([self.tb])[0], self.skip
|
||||
)
|
||||
|
||||
def __getstate__(self):
|
||||
return (
|
||||
None,
|
||||
{
|
||||
"tb": None, # TB is not pickleable
|
||||
"skip": self.skip,
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def extract(*, script=False, cpp=False, skip=0):
|
||||
"""
|
||||
Like traceback.extract_stack(), but faster (approximately 20x faster); it
|
||||
is fast enough that you can unconditionally log stacks this way as part of
|
||||
normal execution. It returns a torch._C._profiler.CapturedTraceback
|
||||
object that must be formatted specially with format_captured_tb.
|
||||
|
||||
By default, this only reports Python backtraces (like extract_stack). You
|
||||
can set the script/cpp kwargs to also turn on TorchScript/C++ trace
|
||||
reporting.
|
||||
"""
|
||||
import torch._C._profiler
|
||||
|
||||
if script or cpp:
|
||||
if skip != 0:
|
||||
raise AssertionError("skip with script/cpp NYI")
|
||||
|
||||
return CapturedTraceback(
|
||||
torch._C._profiler.gather_traceback(python=True, script=script, cpp=cpp),
|
||||
# Elide extract() frame if we don't have script/cpp frames. If
|
||||
# we do have those frames, it doesn't work so force zero.
|
||||
0 if script or cpp else skip + 1,
|
||||
)
|
||||
|
||||
def format(self):
|
||||
"""
|
||||
Formats a single torch._C._profiler.CapturedTraceback into a list of
|
||||
strings equivalent to the output of traceback.format_list. Note that if
|
||||
pass it CapturedTraceback with C++ traces, it is better not to use this
|
||||
function and use the batch formatting API format_captured_tbs to amortize
|
||||
the cost of symbolization
|
||||
"""
|
||||
return traceback.format_list(self.summary())
|
||||
|
||||
@staticmethod
|
||||
def format_all(tbs):
|
||||
"""
|
||||
Bulk version of CapturedTraceback.format. Returns a list of list of strings.
|
||||
"""
|
||||
import torch._C._profiler
|
||||
|
||||
# Directly populate tracebacks that already have cached summaries
|
||||
rs: list[list[str] | None] = []
|
||||
delayed_idxs = []
|
||||
for i, tb in enumerate(tbs):
|
||||
if tb.tb is None:
|
||||
rs.append([])
|
||||
else:
|
||||
rs.append(None)
|
||||
delayed_idxs.append(i)
|
||||
|
||||
torch._C._profiler.symbolize_tracebacks([tbs[i].tb for i in delayed_idxs])
|
||||
for i in delayed_idxs:
|
||||
rs[i] = traceback.format_list(tbs[i].summary())
|
||||
|
||||
return rs
|
||||
|
||||
|
||||
def _extract_symbolized_tb(tb, skip):
|
||||
"""
|
||||
Given a symbolized traceback from symbolize_tracebacks, return a StackSummary object of
|
||||
pre-processed stack trace entries.
|
||||
"""
|
||||
stack = traceback.StackSummary()
|
||||
for f in reversed(tb[skip:]):
|
||||
stack.append(traceback.FrameSummary(f["filename"], f["line"], f["name"]))
|
||||
return stack
|
||||
@@ -0,0 +1,223 @@
|
||||
import functools
|
||||
import hashlib
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
|
||||
@functools.cache
|
||||
def has_triton_package() -> bool:
|
||||
try:
|
||||
import triton # noqa: F401
|
||||
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
@functools.cache
|
||||
def get_triton_version(fallback: tuple[int, int] = (0, 0)) -> tuple[int, int]:
|
||||
try:
|
||||
import triton
|
||||
|
||||
major, minor = tuple(int(v) for v in triton.__version__.split(".")[:2])
|
||||
return (major, minor)
|
||||
except ImportError:
|
||||
return fallback
|
||||
|
||||
|
||||
@functools.cache
|
||||
def _device_supports_tma() -> bool:
|
||||
import torch
|
||||
|
||||
return (
|
||||
torch.cuda.is_available()
|
||||
and torch.cuda.get_device_capability() >= (9, 0)
|
||||
and not torch.version.hip
|
||||
)
|
||||
|
||||
|
||||
@functools.cache
|
||||
def has_triton_experimental_host_tma() -> bool:
|
||||
if has_triton_package():
|
||||
if _device_supports_tma():
|
||||
try:
|
||||
from triton.tools.experimental_descriptor import ( # noqa: F401
|
||||
create_1d_tma_descriptor,
|
||||
create_2d_tma_descriptor,
|
||||
)
|
||||
|
||||
try:
|
||||
from triton.tools.experimental_descriptor import enable_in_pytorch
|
||||
|
||||
return enable_in_pytorch()
|
||||
except ImportError:
|
||||
return True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@functools.cache
|
||||
def has_triton_tensor_descriptor_host_tma() -> bool:
|
||||
if has_triton_package():
|
||||
if _device_supports_tma():
|
||||
try:
|
||||
from triton.tools.tensor_descriptor import ( # noqa: F401
|
||||
TensorDescriptor,
|
||||
)
|
||||
|
||||
return True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@functools.cache
|
||||
def has_triton_tma() -> bool:
|
||||
return has_triton_tensor_descriptor_host_tma() or has_triton_experimental_host_tma()
|
||||
|
||||
|
||||
@functools.cache
|
||||
def has_triton_tma_device() -> bool:
|
||||
if has_triton_package():
|
||||
import torch
|
||||
|
||||
if (
|
||||
torch.cuda.is_available()
|
||||
and torch.cuda.get_device_capability() >= (9, 0)
|
||||
and not torch.version.hip
|
||||
) or torch.xpu.is_available():
|
||||
# old API
|
||||
try:
|
||||
from triton.language.extra.cuda import ( # noqa: F401
|
||||
experimental_device_tensormap_create1d,
|
||||
experimental_device_tensormap_create2d,
|
||||
)
|
||||
|
||||
return True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# new API
|
||||
try:
|
||||
from triton.language import make_tensor_descriptor # noqa: F401
|
||||
|
||||
return True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@functools.cache
|
||||
def has_datacenter_blackwell_tma_device() -> bool:
|
||||
import torch
|
||||
|
||||
if (
|
||||
torch.cuda.is_available()
|
||||
and torch.cuda.get_device_capability() >= (10, 0)
|
||||
and torch.cuda.get_device_capability() < (11, 0)
|
||||
and not torch.version.hip
|
||||
):
|
||||
return has_triton_tma_device() and has_triton_tensor_descriptor_host_tma()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def has_triton_stable_tma_api() -> bool:
|
||||
if has_triton_package():
|
||||
import torch
|
||||
|
||||
if (
|
||||
torch.cuda.is_available()
|
||||
and torch.cuda.get_device_capability() >= (9, 0)
|
||||
and not torch.version.hip
|
||||
) or torch.xpu.is_available():
|
||||
try:
|
||||
from triton.language import make_tensor_descriptor # noqa: F401
|
||||
|
||||
return True
|
||||
except ImportError:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
@functools.cache
|
||||
def has_triton() -> bool:
|
||||
if not has_triton_package():
|
||||
return False
|
||||
|
||||
from torch._inductor.config import triton_disable_device_detection
|
||||
|
||||
if triton_disable_device_detection:
|
||||
return False
|
||||
|
||||
from torch._dynamo.device_interface import get_interface_for_device
|
||||
|
||||
def cuda_extra_check(device_interface: Any) -> bool:
|
||||
return device_interface.Worker.get_device_properties().major >= 7
|
||||
|
||||
def cpu_extra_check(device_interface: Any) -> bool:
|
||||
import triton.backends
|
||||
|
||||
return "cpu" in triton.backends.backends
|
||||
|
||||
def _return_true(device_interface: Any) -> bool:
|
||||
return True
|
||||
|
||||
triton_supported_devices = {
|
||||
"cuda": cuda_extra_check,
|
||||
"xpu": _return_true,
|
||||
"cpu": cpu_extra_check,
|
||||
"mtia": _return_true,
|
||||
}
|
||||
|
||||
def is_device_compatible_with_triton() -> bool:
|
||||
for device, extra_check in triton_supported_devices.items():
|
||||
device_interface = get_interface_for_device(device)
|
||||
if device_interface.is_available() and extra_check(device_interface):
|
||||
return True
|
||||
return False
|
||||
|
||||
return is_device_compatible_with_triton()
|
||||
|
||||
|
||||
@functools.cache
|
||||
def triton_backend() -> Any:
|
||||
from triton.compiler.compiler import make_backend
|
||||
from triton.runtime.driver import driver
|
||||
|
||||
target = driver.active.get_current_target()
|
||||
return make_backend(target)
|
||||
|
||||
|
||||
def _extern_libs_key(backend: Any) -> str:
|
||||
"""Return a cache key fragment for extern libs (e.g. libdevice.10.bc).
|
||||
|
||||
These files affect codegen but are not covered by triton_key() (Python
|
||||
sources only) or backend.hash() (ptxas version and arch only).
|
||||
"""
|
||||
opts = backend.parse_options({})
|
||||
extern_libs = getattr(opts, "extern_libs", None)
|
||||
if not extern_libs:
|
||||
return ""
|
||||
parts = []
|
||||
for name, path in sorted(extern_libs):
|
||||
if os.path.isfile(path):
|
||||
with open(path, "rb") as f:
|
||||
parts.append(f"{name}-{hashlib.sha256(f.read()).hexdigest()}")
|
||||
return "-".join(parts)
|
||||
|
||||
|
||||
@functools.cache
|
||||
def triton_hash_with_backend() -> str:
|
||||
from torch._inductor.runtime.triton_compat import triton_key
|
||||
|
||||
backend = triton_backend()
|
||||
key = f"{triton_key()}-{backend.hash()}"
|
||||
|
||||
# Hash is upper case so that it can't contain any Python keywords.
|
||||
return hashlib.sha256(key.encode("utf-8")).hexdigest().upper()
|
||||
@@ -0,0 +1,76 @@
|
||||
"""Miscellaneous utilities to aid with typing."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any, cast, Concatenate, TypeVar
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
|
||||
# Helper to turn Optional[T] into T when we know None either isn't
|
||||
# possible or should trigger an exception.
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def not_none(obj: T | None) -> T:
|
||||
if obj is None:
|
||||
raise TypeError("Invariant encountered: value was None when it should not be")
|
||||
return obj
|
||||
|
||||
|
||||
_P = ParamSpec("_P")
|
||||
_R = TypeVar("_R")
|
||||
_A1 = TypeVar("_A1")
|
||||
|
||||
|
||||
def copy_func_params(
|
||||
source_func: Callable[_P, Any],
|
||||
) -> Callable[[Callable[..., _R]], Callable[_P, _R]]:
|
||||
"""Cast the decorated function's call signature to the source_func's.
|
||||
|
||||
Usage:
|
||||
def upstream_func(a: int, b: float, *, double: bool = False) -> float: ...
|
||||
@copy_func_params(upstream_func)
|
||||
def enhanced(a: int, b: float, *args: Any, double: bool = False, **kwargs: Any) -> str: ...
|
||||
"""
|
||||
|
||||
def return_func(func: Callable[..., _R]) -> Callable[_P, _R]:
|
||||
return cast(Callable[_P, _R], func)
|
||||
|
||||
return return_func
|
||||
|
||||
|
||||
def copy_method_params(
|
||||
source_method: Callable[Concatenate[Any, _P], Any],
|
||||
) -> Callable[[Callable[..., _R]], Callable[Concatenate[_A1, _P], _R]]:
|
||||
"""Cast the decorated *method*'s call signature to the source_method's.
|
||||
Keeps the first argument type (e.g., self/cls).
|
||||
"""
|
||||
|
||||
def return_func(func: Callable[..., _R]) -> Callable[Concatenate[_A1, _P], _R]:
|
||||
return cast(Callable[Concatenate[_A1, _P], _R], func)
|
||||
|
||||
return return_func
|
||||
|
||||
|
||||
# stricter variants to preserve the origin callers Return Type too.
|
||||
# TODO: consider folding both these into the above variants with an optional
|
||||
# parameter to control whether to copy the return type or not.
|
||||
def copy_func_sig(
|
||||
source_func: Callable[_P, _R],
|
||||
) -> Callable[[Callable[..., _R]], Callable[_P, _R]]:
|
||||
"""Cast the decorated function's call signature and return type to the source_func's."""
|
||||
|
||||
def _return(func: Callable[..., _R]) -> Callable[_P, _R]:
|
||||
return cast(Callable[_P, _R], func)
|
||||
|
||||
return _return
|
||||
|
||||
|
||||
def copy_method_sig(
|
||||
source_method: Callable[Concatenate[_A1, _P], _R],
|
||||
) -> Callable[[Callable[..., _R]], Callable[Concatenate[_A1, _P], _R]]:
|
||||
"""Cast the decorated *method*'s call signature to the source_method and return type."""
|
||||
|
||||
def _return(func: Callable[..., _R]) -> Callable[Concatenate[_A1, _P], _R]:
|
||||
return cast(Callable[Concatenate[_A1, _P], _R], func)
|
||||
|
||||
return _return
|
||||
@@ -0,0 +1,86 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
from pathlib import Path
|
||||
from zipfile import ZipFile
|
||||
|
||||
|
||||
# Exclude some standard library modules to:
|
||||
# 1. Slim down the final zipped file size
|
||||
# 2. Remove functionality we don't want to support.
|
||||
DENY_LIST = [
|
||||
# Interface to unix databases
|
||||
"dbm",
|
||||
# ncurses bindings (terminal interfaces)
|
||||
"curses",
|
||||
# Tcl/Tk GUI
|
||||
"tkinter",
|
||||
"tkinter",
|
||||
# Tests for the standard library
|
||||
"test",
|
||||
"tests",
|
||||
"idle_test",
|
||||
"__phello__.foo.py",
|
||||
# importlib frozen modules. These are already baked into CPython.
|
||||
"_bootstrap.py",
|
||||
"_bootstrap_external.py",
|
||||
]
|
||||
|
||||
strip_file_dir = ""
|
||||
|
||||
|
||||
def remove_prefix(text, prefix):
|
||||
if text.startswith(prefix):
|
||||
return text[len(prefix) :]
|
||||
return text
|
||||
|
||||
|
||||
def write_to_zip(file_path, strip_file_path, zf, prepend_str="") -> None:
|
||||
stripped_file_path = prepend_str + remove_prefix(file_path, strip_file_dir + "/")
|
||||
path = Path(stripped_file_path)
|
||||
if path.name in DENY_LIST:
|
||||
return
|
||||
zf.write(file_path, stripped_file_path)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
global strip_file_dir
|
||||
parser = argparse.ArgumentParser(description="Zip py source")
|
||||
parser.add_argument("paths", nargs="*", help="Paths to zip.")
|
||||
parser.add_argument(
|
||||
"--install-dir", "--install_dir", help="Root directory for all output files"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strip-dir",
|
||||
"--strip_dir",
|
||||
help="The absolute directory we want to remove from zip",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prepend-str",
|
||||
"--prepend_str",
|
||||
help="A string to prepend onto all paths of a file in the zip",
|
||||
default="",
|
||||
)
|
||||
parser.add_argument("--zip-name", "--zip_name", help="Output zip name")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
zip_file_name = args.install_dir + "/" + args.zip_name
|
||||
strip_file_dir = args.strip_dir
|
||||
prepend_str = args.prepend_str
|
||||
with ZipFile(zip_file_name, mode="w") as zf:
|
||||
for p in sorted(args.paths):
|
||||
if os.path.isdir(p):
|
||||
files = glob.glob(p + "/**/*.py", recursive=True)
|
||||
for file_path in sorted(files):
|
||||
# strip the absolute path
|
||||
write_to_zip(
|
||||
file_path, strip_file_dir + "/", zf, prepend_str=prepend_str
|
||||
)
|
||||
else:
|
||||
write_to_zip(p, strip_file_dir + "/", zf, prepend_str=prepend_str)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main() # pragma: no cover
|
||||
@@ -0,0 +1,27 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from torch._C import (
|
||||
_get_backcompat_broadcast_warn,
|
||||
_get_backcompat_keepdim_warn,
|
||||
_set_backcompat_broadcast_warn,
|
||||
_set_backcompat_keepdim_warn,
|
||||
)
|
||||
|
||||
|
||||
class Warning:
|
||||
def __init__(self, setter, getter) -> None:
|
||||
self.setter = setter
|
||||
self.getter = getter
|
||||
|
||||
def set_enabled(self, value) -> None:
|
||||
self.setter(value)
|
||||
|
||||
def get_enabled(self):
|
||||
return self.getter()
|
||||
|
||||
enabled = property(get_enabled, set_enabled)
|
||||
|
||||
|
||||
broadcast_warning = Warning(
|
||||
_set_backcompat_broadcast_warn, _get_backcompat_broadcast_warn
|
||||
)
|
||||
keepdim_warning = Warning(_set_backcompat_keepdim_warn, _get_backcompat_keepdim_warn)
|
||||
@@ -0,0 +1,520 @@
|
||||
# mypy: allow-untyped-defs
|
||||
|
||||
import torch
|
||||
from torch._C import _get_privateuse1_backend_name, _rename_privateuse1_backend
|
||||
from torch.overrides import handle_torch_function, has_torch_function_unary
|
||||
|
||||
|
||||
__all__ = [
|
||||
"rename_privateuse1_backend",
|
||||
"generate_methods_for_privateuse1_backend",
|
||||
]
|
||||
|
||||
# TODO: Should use `torch._C._get_privateuse1_backend_name()` to get
|
||||
# renamed-backend name for `privateuse1`, but the func will cause an
|
||||
# error with torch.jit.script, so we use the global variable named
|
||||
# `_privateuse1_backend_name`.
|
||||
_privateuse1_backend_name = "privateuseone"
|
||||
|
||||
|
||||
def rename_privateuse1_backend(backend_name: str) -> None:
|
||||
r"""
|
||||
Rename the privateuse1 backend device to make it more convenient to use as a device name within PyTorch APIs.
|
||||
|
||||
The steps are:
|
||||
|
||||
(1) (In C++) implement kernels for various torch operations, and register them
|
||||
to the PrivateUse1 dispatch key.
|
||||
(2) (In python) call torch.utils.rename_privateuse1_backend("foo")
|
||||
|
||||
You can now use "foo" as an ordinary device string in python.
|
||||
|
||||
Note: this API can only be called once per process. Attempting to change
|
||||
the external backend after it's already been set will result in an error.
|
||||
|
||||
Note(AMP): If you want to support AMP on your device, you can register a custom backend module.
|
||||
The backend must register a custom backend module with ``torch._register_device_module("foo", BackendModule)``.
|
||||
BackendModule needs to have the following API's:
|
||||
|
||||
(1) ``get_amp_supported_dtype() -> List[torch.dtype]``
|
||||
get the supported dtypes on your "foo" device in AMP, maybe the "foo" device supports one more dtype.
|
||||
|
||||
Note(random): If you want to support to set seed for your device, BackendModule needs to have the following API's:
|
||||
|
||||
(1) ``_is_in_bad_fork() -> bool``
|
||||
Return ``True`` if now it is in bad_fork, else return ``False``.
|
||||
|
||||
(2) ``manual_seed_all(seed int) -> None``
|
||||
Sets the seed for generating random numbers for your devices.
|
||||
|
||||
(3) ``device_count() -> int``
|
||||
Returns the number of "foo"s available.
|
||||
|
||||
(4) ``get_rng_state(device: Union[int, str, torch.device] = 'foo') -> Tensor``
|
||||
Returns a list of ByteTensor representing the random number states of all devices.
|
||||
|
||||
(5) ``set_rng_state(new_state: Tensor, device: Union[int, str, torch.device] = 'foo') -> None``
|
||||
Sets the random number generator state of the specified "foo" device.
|
||||
|
||||
And there are some common funcs:
|
||||
|
||||
(1) ``is_available() -> bool``
|
||||
Returns a bool indicating if "foo" is currently available.
|
||||
|
||||
(2) ``current_device() -> int``
|
||||
Returns the index of a currently selected device.
|
||||
|
||||
For more details, see https://pytorch.org/tutorials/advanced/extend_dispatcher.html#get-a-dispatch-key-for-your-backend
|
||||
For an existing example, see https://github.com/bdhirsh/pytorch_open_registration_example
|
||||
|
||||
Example::
|
||||
|
||||
>>> # xdoctest: +SKIP("failing")
|
||||
>>> torch.utils.rename_privateuse1_backend("foo")
|
||||
# This will work, assuming that you've implemented the right C++ kernels
|
||||
# to implement torch.ones.
|
||||
>>> a = torch.ones(2, device="foo")
|
||||
|
||||
"""
|
||||
_rename_privateuse1_backend(backend_name)
|
||||
global _privateuse1_backend_name
|
||||
_privateuse1_backend_name = backend_name
|
||||
|
||||
|
||||
def _check_register_once(module, attr) -> None:
|
||||
if hasattr(module, attr):
|
||||
raise RuntimeError(
|
||||
f"The custom device module of {module} has already been registered with {attr}"
|
||||
)
|
||||
|
||||
|
||||
def _normalization_device(
|
||||
custom_backend_name: str, device: int | str | torch.device | None = None
|
||||
) -> int:
|
||||
def _get_current_device_index():
|
||||
_get_device_index = "current_device"
|
||||
if hasattr(torch, custom_backend_name) and hasattr(
|
||||
getattr(torch, custom_backend_name), _get_device_index
|
||||
):
|
||||
return getattr(getattr(torch, custom_backend_name), _get_device_index)()
|
||||
else:
|
||||
# The default device index is 0.
|
||||
return 0
|
||||
|
||||
if device is None:
|
||||
return _get_current_device_index()
|
||||
# if isinstance(device, str), this means that the parameter passed in is in the string format "foo:0"
|
||||
# convert str object to torch.device object, and then process it uniformly
|
||||
elif isinstance(device, str):
|
||||
device = torch.device(device)
|
||||
|
||||
# variable device can only be torch.device type or int type
|
||||
if isinstance(device, torch.device):
|
||||
if device.type != custom_backend_name:
|
||||
raise RuntimeError(f"Invalid device, must be {custom_backend_name} device")
|
||||
elif device.index is None:
|
||||
device_idx = _get_current_device_index()
|
||||
else:
|
||||
device_idx = device.index
|
||||
# if isinstance(device, int), we can take the index number directly
|
||||
else:
|
||||
device_idx = device
|
||||
return device_idx
|
||||
|
||||
|
||||
def _generate_tensor_methods_for_privateuse1_backend(custom_backend_name: str) -> None:
|
||||
@property # type: ignore[misc]
|
||||
def wrap_tensor_backend(self: torch.Tensor) -> bool:
|
||||
if has_torch_function_unary(self):
|
||||
# TODO mypy doesn't support @property, see: https://github.com/python/mypy/issues/6185
|
||||
return handle_torch_function(wrap_tensor_backend.__get__, (self,), self) # type: ignore[attr-defined]
|
||||
return self.device.type == custom_backend_name
|
||||
|
||||
_check_register_once(torch.Tensor, f"is_{custom_backend_name}")
|
||||
wrap_tensor_backend.fget.__name__ = f"is_{custom_backend_name}" # type: ignore[attr-defined]
|
||||
setattr(torch.Tensor, f"is_{custom_backend_name}", wrap_tensor_backend)
|
||||
|
||||
def wrap_tensor_to(
|
||||
self: torch.Tensor,
|
||||
device: int | torch.device | None = None,
|
||||
non_blocking=False,
|
||||
**kwargs,
|
||||
) -> torch.Tensor:
|
||||
r"""Perform Tensor device conversion. Call the to operator implementation.
|
||||
|
||||
.. note::
|
||||
If the ``self`` Tensor already
|
||||
has the correct :class:`torch.device`, then ``self`` is returned.
|
||||
Otherwise, the returned tensor is a copy of ``self`` with the desired :class:`torch.device`.
|
||||
|
||||
Args:
|
||||
device (int, optional): if specified, all parameters will be copied to that device
|
||||
non_blocking (bool): If ``True`` and the source is in pinned memory,
|
||||
the copy will be asynchronous with respect to the host. Otherwise,
|
||||
the argument has no effect.
|
||||
**kwargs (dict): For compatibility, may contain the key ``memory_format`` argument.
|
||||
"""
|
||||
if has_torch_function_unary(self):
|
||||
return handle_torch_function(
|
||||
wrap_tensor_to,
|
||||
(self,),
|
||||
self,
|
||||
device=device,
|
||||
non_blocking=False,
|
||||
**kwargs,
|
||||
)
|
||||
device_idx = _normalization_device(custom_backend_name, device)
|
||||
return self.to(
|
||||
device=torch.device(f"{custom_backend_name}:{device_idx}"),
|
||||
non_blocking=non_blocking,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
_check_register_once(torch.Tensor, custom_backend_name)
|
||||
wrap_tensor_to.__name__ = custom_backend_name
|
||||
setattr(torch.Tensor, custom_backend_name, wrap_tensor_to)
|
||||
|
||||
|
||||
def _generate_module_methods_for_privateuse1_backend(custom_backend_name: str) -> None:
|
||||
# Generate Module attributes and methods depends on Tensor methods,
|
||||
# so we need to check whether Tensor methods is already registered.
|
||||
if not hasattr(torch.Tensor, custom_backend_name):
|
||||
raise RuntimeError(
|
||||
f"Can not automatically generate {custom_backend_name}() method for torch.nn.Module."
|
||||
f"Because torch.Tensor doesn't has the method {custom_backend_name}()."
|
||||
f"For this error, you can try setting for_tensor=True."
|
||||
)
|
||||
|
||||
def wrap_module_to(
|
||||
# pyrefly: ignore [invalid-type-var]
|
||||
self: torch.nn.modules.module.T,
|
||||
device: int | torch.device | None = None,
|
||||
) -> torch.nn.modules.module.T: # pyrefly: ignore [invalid-type-var]
|
||||
r"""Move all model parameters and buffers to the custom device.
|
||||
|
||||
This also makes associated parameters and buffers different objects. So
|
||||
it should be called before constructing optimizer if the module will
|
||||
live on device while being optimized.
|
||||
|
||||
.. note::
|
||||
This method modifies the module in-place.
|
||||
|
||||
Args:
|
||||
device (int, optional): if specified, all parameters will be copied to that device
|
||||
"""
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
return self._apply(lambda t: getattr(t, custom_backend_name)(device))
|
||||
|
||||
_check_register_once(torch.nn.Module, custom_backend_name)
|
||||
setattr(torch.nn.Module, custom_backend_name, wrap_module_to)
|
||||
|
||||
|
||||
def _generate_packed_sequence_methods_for_privateuse1_backend(
|
||||
custom_backend_name: str,
|
||||
) -> None:
|
||||
# Generate PackedSequence Module attributes and methods depends on Tensor methods,
|
||||
# so we need to check whether Tensor methods is already registered.
|
||||
if not hasattr(torch.Tensor, f"is_{custom_backend_name}") or not hasattr(
|
||||
torch.Tensor, custom_backend_name
|
||||
):
|
||||
raise RuntimeError(
|
||||
f"Can not automatically generate is_{custom_backend_name}() or "
|
||||
f"{custom_backend_name}() method for torch.nn.utils.rnn.PackedSequence."
|
||||
f"Because torch.Tensor doesn't has the method is_{custom_backend_name}()"
|
||||
f"or {custom_backend_name}()."
|
||||
f"For this error, you can try setting for_tensor=True."
|
||||
)
|
||||
|
||||
@property # type: ignore[misc]
|
||||
def wrap_tensor_backend(self: torch.nn.utils.rnn.PackedSequence) -> bool:
|
||||
return self.data.device.type == custom_backend_name
|
||||
|
||||
_check_register_once(torch.nn.utils.rnn.PackedSequence, f"is_{custom_backend_name}")
|
||||
setattr(
|
||||
torch.nn.utils.rnn.PackedSequence,
|
||||
f"is_{custom_backend_name}",
|
||||
wrap_tensor_backend,
|
||||
)
|
||||
|
||||
def wrap_module_to(
|
||||
self: torch.nn.utils.rnn.PackedSequence, *args, **kwargs
|
||||
) -> torch.nn.utils.rnn.PackedSequence:
|
||||
r"""Move all model parameters and buffers to the custom device.
|
||||
|
||||
This also makes associated parameters and buffers different objects. So
|
||||
it should be called before constructing optimizer if the module will
|
||||
live on device while being optimized.
|
||||
|
||||
.. note::
|
||||
This method modifies the module in-place.
|
||||
|
||||
Args:
|
||||
device (int, optional): if specified, all parameters will be copied to that device
|
||||
"""
|
||||
ex = torch.tensor((), dtype=self.data.dtype, device=self.data.device).to(
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
if ex.device.type == custom_backend_name:
|
||||
return self.to(*args, **kwargs)
|
||||
kwargs.update({"device": custom_backend_name})
|
||||
|
||||
return self.to(*args, **kwargs)
|
||||
|
||||
_check_register_once(torch.nn.utils.rnn.PackedSequence, custom_backend_name)
|
||||
setattr(torch.nn.utils.rnn.PackedSequence, custom_backend_name, wrap_module_to)
|
||||
|
||||
|
||||
def _generate_storage_methods_for_privateuse1_backend(
|
||||
custom_backend_name: str, unsupported_dtype: list[torch.dtype] | None = None
|
||||
) -> None:
|
||||
# Attribute is registered in the _StorageBase class
|
||||
# and UntypedStorage obtains through inheritance.
|
||||
@property # type: ignore[misc]
|
||||
def wrap_storage_backend(self: torch.storage._StorageBase) -> bool:
|
||||
r"""Return the internal :class:`torch.UntypedStorage`."""
|
||||
return self.device.type == custom_backend_name
|
||||
|
||||
_check_register_once(torch.storage._StorageBase, f"is_{custom_backend_name}")
|
||||
setattr(
|
||||
torch.storage._StorageBase, f"is_{custom_backend_name}", wrap_storage_backend
|
||||
)
|
||||
|
||||
def wrap_storage_to(self, device=None, non_blocking=False):
|
||||
r"""Return a copy of this object in custom device memory.
|
||||
|
||||
If this object is already in device memory and on the correct device, then
|
||||
no copy is performed and the original object is returned.
|
||||
|
||||
Args:
|
||||
device (int): The destination device id. Defaults to the current device.
|
||||
non_blocking (bool): If ``True`` and the source is in pinned memory,
|
||||
the copy will be asynchronous with respect to the host. Otherwise,
|
||||
the argument has no effect.
|
||||
"""
|
||||
# There should be a judgment related to storage device and a judgment related to storage type,
|
||||
# but it depends on the extended function, so this part is temporarily omitted in the automatic generation.
|
||||
device_idx = _normalization_device(custom_backend_name, device)
|
||||
|
||||
if getattr(self, f"is_{custom_backend_name}"):
|
||||
# storage has already on expected device.
|
||||
if self.get_device() == device_idx:
|
||||
return self
|
||||
# For sparse storage, custom need to extend the implementation by themselves.
|
||||
if self.is_sparse:
|
||||
raise RuntimeError(
|
||||
f"Can not support a sparse storage move to {custom_backend_name} backend"
|
||||
)
|
||||
# create untyped_storage and copy data
|
||||
untyped_storage = torch.UntypedStorage(
|
||||
self.size(), device=torch.device(f"{custom_backend_name}:{device_idx}")
|
||||
)
|
||||
untyped_storage.copy_(self, non_blocking)
|
||||
return untyped_storage
|
||||
|
||||
_check_register_once(torch.storage._StorageBase, custom_backend_name)
|
||||
setattr(torch.storage._StorageBase, custom_backend_name, wrap_storage_to)
|
||||
|
||||
# Register the corresponding attribute for the TypedStorage class.
|
||||
# When the TypedStorage class is removed, the registration is also removed.
|
||||
|
||||
@property # type: ignore[misc]
|
||||
def wrap_typed_storage_backend(self: torch.storage.TypedStorage) -> bool:
|
||||
torch.storage._warn_typed_storage_removal()
|
||||
return self._untyped_storage.device.type == custom_backend_name
|
||||
|
||||
_check_register_once(torch.TypedStorage, f"is_{custom_backend_name}")
|
||||
setattr(
|
||||
torch.storage.TypedStorage,
|
||||
f"is_{custom_backend_name}",
|
||||
wrap_typed_storage_backend,
|
||||
)
|
||||
|
||||
def wrap_typed_storage_to(
|
||||
self: torch.storage.TypedStorage, device=None, non_blocking=False, **kwargs
|
||||
) -> torch.storage.TypedStorage:
|
||||
torch.storage._warn_typed_storage_removal()
|
||||
if unsupported_dtype and self.dtype in unsupported_dtype:
|
||||
raise RuntimeError(
|
||||
f"Cannot create {custom_backend_name} storage "
|
||||
f"as {self.dtype} dtype is not supported by this backend"
|
||||
)
|
||||
custom_backend_storage: torch.UntypedStorage = getattr(
|
||||
self._untyped_storage, custom_backend_name
|
||||
)(device, non_blocking, **kwargs)
|
||||
return self._new_wrapped_storage(custom_backend_storage)
|
||||
|
||||
_check_register_once(torch.TypedStorage, custom_backend_name)
|
||||
setattr(torch.TypedStorage, custom_backend_name, wrap_typed_storage_to)
|
||||
|
||||
|
||||
def generate_methods_for_privateuse1_backend(
|
||||
for_tensor: bool = True,
|
||||
for_module: bool = True,
|
||||
for_packed_sequence: bool = True,
|
||||
for_storage: bool = False,
|
||||
unsupported_dtype: list[torch.dtype] | None = None,
|
||||
) -> None:
|
||||
r"""
|
||||
Automatically generate attributes and methods for the custom backend after rename privateuse1 backend.
|
||||
|
||||
In the default scenario, storage-related methods will not be generated automatically.
|
||||
|
||||
When you implement kernels for various torch operations, and register them to the PrivateUse1 dispatch key.
|
||||
And call the function torch.rename_privateuse1_backend("foo") to rename your backend name.
|
||||
At this point, you can easily register specific methods and attributes by calling this function.
|
||||
Just like torch.Tensor.foo(), torch.Tensor.is_foo, torch.Storage.foo(), torch.Storage.is_foo.
|
||||
|
||||
Note: We recommend you use generic functions (check devices are equal or to(device=)).
|
||||
We provide these methods for convenience only and they will be "monkey patched" onto the objects
|
||||
and so will not be properly typed. For Storage methods generate, if you need to support sparse data storage,
|
||||
you need to extend the implementation yourself.
|
||||
|
||||
Args:
|
||||
for_tensor (bool): whether register related methods for torch.Tensor class.
|
||||
for_module (bool): whether register related methods for torch.nn.Module class.
|
||||
for_storage (bool): whether register related methods for torch.Storage class.
|
||||
unsupported_dtype (List[torch.dtype]): takes effect only when the storage method needs to be generated,
|
||||
indicating that the storage does not support the torch.dtype type.
|
||||
|
||||
Example::
|
||||
|
||||
>>> # xdoctest: +SKIP("failing")
|
||||
>>> torch.utils.rename_privateuse1_backend("foo")
|
||||
>>> torch.utils.generate_methods_for_privateuse1_backend()
|
||||
# Then automatically generate backend-related attributes and methods.
|
||||
>>> a = torch.tensor(2).foo()
|
||||
>>> a.is_foo
|
||||
>>> hasattr(torch.nn.Module, 'foo')
|
||||
"""
|
||||
custom_backend_name = _get_privateuse1_backend_name()
|
||||
|
||||
if for_tensor:
|
||||
_generate_tensor_methods_for_privateuse1_backend(custom_backend_name)
|
||||
|
||||
if for_module:
|
||||
_generate_module_methods_for_privateuse1_backend(custom_backend_name)
|
||||
|
||||
if for_storage:
|
||||
_generate_storage_methods_for_privateuse1_backend(
|
||||
custom_backend_name, unsupported_dtype
|
||||
)
|
||||
|
||||
if for_packed_sequence:
|
||||
_generate_packed_sequence_methods_for_privateuse1_backend(custom_backend_name)
|
||||
|
||||
|
||||
def _get_custom_mod_func(func_name: str):
|
||||
r"""
|
||||
Return the func named `func_name` defined in custom device module. If not defined,
|
||||
return `None`. And the func is registered with `torch.utils.rename_privateuse1_backend('foo')`
|
||||
and `torch._register_device_module('foo', BackendModule)`.
|
||||
If the custom device module or the func is not defined, it will give warning or error message.
|
||||
Args:
|
||||
func_name (str): return the callable func named func_name defined in custom device module.
|
||||
Example::
|
||||
class DummyfooModule:
|
||||
@staticmethod
|
||||
def is_available():
|
||||
return True
|
||||
@staticmethod
|
||||
def func_name(*args, **kwargs):
|
||||
....
|
||||
torch.utils.rename_privateuse1_backend("foo")
|
||||
torch._register_device_module("foo", DummyfooModule)
|
||||
foo_is_available_func = torch.utils.backend_registration._get_custom_mod_func("is_available")
|
||||
if foo_is_available_func:
|
||||
foo_is_available = foo_is_available_func()
|
||||
func_ = torch.utils.backend_registration._get_custom_mod_func("func_name")
|
||||
if func_:
|
||||
result = func_(*args, **kwargs)
|
||||
Attention: This function is not meant to be used directly by users, which is why
|
||||
it is marked as private. It is a convenience function for backend implementers to
|
||||
more easily call the hooks into their backend extensions.
|
||||
"""
|
||||
if not isinstance(func_name, str):
|
||||
raise AssertionError(f"func_name must be `str`, but got `{type(func_name)}`.")
|
||||
backend_name = _get_privateuse1_backend_name()
|
||||
custom_device_mod = getattr(torch, backend_name, None)
|
||||
function = getattr(custom_device_mod, func_name, None)
|
||||
if custom_device_mod is None or function is None:
|
||||
message = f"Try to call torch.{backend_name}.{func_name}. The backend must register a custom backend "
|
||||
message += f"module with `torch._register_device_module('{backend_name}', BackendModule)`. And "
|
||||
message += f"BackendModule needs to have the following API's:\n `{func_name}(*args, **kwargs)`. \n"
|
||||
raise RuntimeError(message)
|
||||
return function
|
||||
|
||||
|
||||
class _DummyBackendModule:
|
||||
def is_initialized(self) -> bool:
|
||||
return True
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return True
|
||||
|
||||
def current_device(self) -> int:
|
||||
return 0
|
||||
|
||||
def _is_in_bad_fork(self) -> bool:
|
||||
return False
|
||||
|
||||
def manual_seed_all(self, seed: int) -> None:
|
||||
pass
|
||||
|
||||
def device_count(self) -> int:
|
||||
return 1
|
||||
|
||||
|
||||
class _DummyPrivateUse1Hook(torch._C._acc.PrivateUse1Hooks):
|
||||
def is_available(self) -> bool:
|
||||
return True
|
||||
|
||||
def has_primary_context(self, dev_id) -> bool:
|
||||
return True
|
||||
|
||||
def is_built(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class _DummyDeviceGuard(torch._C._acc.DeviceGuard):
|
||||
def type_(self):
|
||||
return torch._C._autograd.DeviceType.PrivateUse1
|
||||
|
||||
|
||||
def _setup_privateuseone_for_python_backend(
|
||||
rename=None, backend_module=None, hook=None, device_guard=None
|
||||
) -> None:
|
||||
"""This function will prepare the PrivateUse1 dispatch key to be used as a python backend.
|
||||
|
||||
WARNING: this API is experimental and might change without notice.
|
||||
|
||||
Formally, this registers things that Pytorch expects a registered backend
|
||||
in C++ to have: including device guards, hooks, and backend modules and what not.
|
||||
|
||||
after this call, one can use `torch.library` to write Ops for this dispatch key
|
||||
and expect it to behave like a backend registered in C++.
|
||||
|
||||
See the unit test at test/test_privateuseone_python_backend.py for more details.
|
||||
|
||||
Args:
|
||||
rename: str | None, if passed in, we will rename privateuseone backend to
|
||||
the name given.
|
||||
backend_module: object | None, if passed in None, we will use DummyBackendModule
|
||||
hook: object | None, if passed in None, we will use DummyPrivateUse1Hook
|
||||
device_guard: object | None, if passed in None, we will use DummyDeviceGuard
|
||||
"""
|
||||
# NOTE: the ordering of which these functions are called is important.
|
||||
if rename is not None:
|
||||
torch.utils.rename_privateuse1_backend(rename)
|
||||
else:
|
||||
rename = "privateuseone"
|
||||
torch.utils.generate_methods_for_privateuse1_backend()
|
||||
if backend_module is None:
|
||||
backend_module = _DummyBackendModule()
|
||||
if hook is None:
|
||||
hook = _DummyPrivateUse1Hook()
|
||||
if device_guard is None:
|
||||
device_guard = _DummyDeviceGuard()
|
||||
torch._register_device_module(rename, backend_module)
|
||||
torch._C._acc.register_python_privateuseone_hook(hook)
|
||||
torch._C._acc.register_python_privateuseone_device_guard(device_guard)
|
||||
@@ -0,0 +1,6 @@
|
||||
from torch.utils.benchmark.utils.common import * # noqa: F403
|
||||
from torch.utils.benchmark.utils.timer import * # noqa: F403
|
||||
from torch.utils.benchmark.utils.compare import * # noqa: F403
|
||||
from torch.utils.benchmark.utils.fuzzer import * # noqa: F403
|
||||
from torch.utils.benchmark.utils.valgrind_wrapper.timer_interface import * # noqa: F403
|
||||
from torch.utils.benchmark.utils.sparse_fuzzer import * # noqa: F403
|
||||
@@ -0,0 +1,99 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Example of Timer and Compare APIs:
|
||||
|
||||
$ python -m examples.compare
|
||||
"""
|
||||
|
||||
import pickle
|
||||
import sys
|
||||
import time
|
||||
|
||||
import torch
|
||||
|
||||
import torch.utils.benchmark as benchmark_utils
|
||||
|
||||
|
||||
class FauxTorch:
|
||||
"""Emulate different versions of pytorch.
|
||||
|
||||
In normal circumstances this would be done with multiple processes
|
||||
writing serialized measurements, but this simplifies that model to
|
||||
make the example clearer.
|
||||
"""
|
||||
def __init__(self, real_torch, extra_ns_per_element) -> None:
|
||||
self._real_torch = real_torch
|
||||
self._extra_ns_per_element = extra_ns_per_element
|
||||
|
||||
def extra_overhead(self, result):
|
||||
# time.sleep has a ~65 us overhead, so only fake a
|
||||
# per-element overhead if numel is large enough.
|
||||
numel = int(result.numel())
|
||||
if numel > 5000:
|
||||
time.sleep(numel * self._extra_ns_per_element * 1e-9)
|
||||
return result
|
||||
|
||||
def add(self, *args, **kwargs):
|
||||
return self.extra_overhead(self._real_torch.add(*args, **kwargs))
|
||||
|
||||
def mul(self, *args, **kwargs):
|
||||
return self.extra_overhead(self._real_torch.mul(*args, **kwargs))
|
||||
|
||||
def cat(self, *args, **kwargs):
|
||||
return self.extra_overhead(self._real_torch.cat(*args, **kwargs))
|
||||
|
||||
def matmul(self, *args, **kwargs):
|
||||
return self.extra_overhead(self._real_torch.matmul(*args, **kwargs))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
tasks = [
|
||||
("add", "add", "torch.add(x, y)"),
|
||||
("add", "add (extra +0)", "torch.add(x, y + zero)"),
|
||||
]
|
||||
|
||||
serialized_results = []
|
||||
repeats = 2
|
||||
timers = [
|
||||
benchmark_utils.Timer(
|
||||
stmt=stmt,
|
||||
globals={
|
||||
"torch": torch if branch == "master" else FauxTorch(torch, overhead_ns),
|
||||
"x": torch.ones((size, 4)),
|
||||
"y": torch.ones((1, 4)),
|
||||
"zero": torch.zeros(()),
|
||||
},
|
||||
label=label,
|
||||
sub_label=sub_label,
|
||||
description=f"size: {size}",
|
||||
env=branch,
|
||||
num_threads=num_threads,
|
||||
)
|
||||
for branch, overhead_ns in [("master", None), ("my_branch", 1), ("severe_regression", 5)]
|
||||
for label, sub_label, stmt in tasks
|
||||
for size in [1, 10, 100, 1000, 10000, 50000]
|
||||
for num_threads in [1, 4]
|
||||
]
|
||||
|
||||
for i, timer in enumerate(timers * repeats):
|
||||
serialized_results.append(pickle.dumps(
|
||||
timer.blocked_autorange(min_run_time=0.05)
|
||||
))
|
||||
print(f"\r{i + 1} / {len(timers) * repeats}", end="")
|
||||
sys.stdout.flush()
|
||||
print()
|
||||
|
||||
comparison = benchmark_utils.Compare([
|
||||
pickle.loads(i) for i in serialized_results
|
||||
])
|
||||
|
||||
print("== Unformatted " + "=" * 80 + "\n" + "/" * 95 + "\n")
|
||||
comparison.print()
|
||||
|
||||
print("== Formatted " + "=" * 80 + "\n" + "/" * 93 + "\n")
|
||||
comparison.trim_significant_figures()
|
||||
comparison.colorize()
|
||||
comparison.print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,86 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Example of the Timer and Fuzzer APIs:
|
||||
|
||||
$ python -m examples.fuzzer
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
import torch.utils.benchmark as benchmark_utils
|
||||
|
||||
|
||||
def main() -> None:
|
||||
add_fuzzer = benchmark_utils.Fuzzer(
|
||||
parameters=[
|
||||
[
|
||||
benchmark_utils.FuzzedParameter(
|
||||
name=f"k{i}",
|
||||
minval=16,
|
||||
maxval=16 * 1024,
|
||||
distribution="loguniform",
|
||||
) for i in range(3)
|
||||
],
|
||||
benchmark_utils.FuzzedParameter(
|
||||
name="d",
|
||||
distribution={2: 0.6, 3: 0.4},
|
||||
),
|
||||
],
|
||||
tensors=[
|
||||
[
|
||||
benchmark_utils.FuzzedTensor(
|
||||
name=name,
|
||||
size=("k0", "k1", "k2"),
|
||||
dim_parameter="d",
|
||||
probability_contiguous=0.75,
|
||||
min_elements=64 * 1024,
|
||||
max_elements=128 * 1024,
|
||||
) for name in ("x", "y")
|
||||
],
|
||||
],
|
||||
seed=0,
|
||||
)
|
||||
|
||||
n = 250
|
||||
measurements = []
|
||||
for i, (tensors, tensor_properties, _) in enumerate(add_fuzzer.take(n=n)):
|
||||
x, x_order = tensors["x"], str(tensor_properties["x"]["order"])
|
||||
y, y_order = tensors["y"], str(tensor_properties["y"]["order"])
|
||||
shape = ", ".join(tuple(f'{i:>4}' for i in x.shape))
|
||||
|
||||
description = "".join([
|
||||
f"{x.numel():>7} | {shape:<16} | ",
|
||||
f"{'contiguous' if x.is_contiguous() else x_order:<12} | ",
|
||||
f"{'contiguous' if y.is_contiguous() else y_order:<12} | ",
|
||||
])
|
||||
|
||||
timer = benchmark_utils.Timer(
|
||||
stmt="x + y",
|
||||
globals=tensors,
|
||||
description=description,
|
||||
)
|
||||
|
||||
measurements.append(timer.blocked_autorange(min_run_time=0.1))
|
||||
measurements[-1].metadata = {"numel": x.numel()}
|
||||
print(f"\r{i + 1} / {n}", end="")
|
||||
sys.stdout.flush()
|
||||
print()
|
||||
|
||||
# More string munging to make pretty output.
|
||||
print(f"Average attempts per valid config: {1. / (1. - add_fuzzer.rejection_rate):.1f}")
|
||||
|
||||
def time_fn(m):
|
||||
return m.median / m.metadata["numel"]
|
||||
measurements.sort(key=time_fn)
|
||||
|
||||
template = f"{{:>6}}{' ' * 19}Size Shape{' ' * 13}X order Y order\n{'-' * 80}"
|
||||
print(template.format("Best:"))
|
||||
for m in measurements[:15]:
|
||||
print(f"{time_fn(m) * 1e9:>4.1f} ns / element {m.description}")
|
||||
|
||||
print("\n" + template.format("Worst:"))
|
||||
for m in measurements[-15:]:
|
||||
print(f"{time_fn(m) * 1e9:>4.1f} ns / element {m.description}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Example use of Timer and op fuzzers to measure kernel performance.
|
||||
|
||||
$ python -m examples.op_benchmark
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from torch.utils.benchmark import Timer
|
||||
from torch.utils.benchmark.op_fuzzers.binary import BinaryOpFuzzer
|
||||
from torch.utils.benchmark.op_fuzzers.unary import UnaryOpFuzzer
|
||||
import operator
|
||||
|
||||
|
||||
_MEASURE_TIME = 1.0
|
||||
|
||||
|
||||
def assert_dicts_equal(dict_0, dict_1) -> None:
|
||||
"""Builtin dict comparison will not compare numpy arrays.
|
||||
e.g.
|
||||
x = {"a": np.ones((2, 1))}
|
||||
x == x # Raises ValueError
|
||||
"""
|
||||
if set(dict_0.keys()) != set(dict_0.keys()):
|
||||
raise AssertionError("dicts must have the same keys")
|
||||
if all(np.all(v != dict_1[k]) for k, v in dict_0.items() if k != "dtype"):
|
||||
raise AssertionError("dict values differ for keys other than 'dtype'")
|
||||
|
||||
|
||||
def run(n, stmt, fuzzer_cls) -> None:
|
||||
float_iter = fuzzer_cls(seed=0, dtype=torch.float32).take(n)
|
||||
int_iter = fuzzer_cls(seed=0, dtype=torch.int32).take(n)
|
||||
raw_results = []
|
||||
for i, (float_values, int_values) in enumerate(zip(float_iter, int_iter, strict=True)):
|
||||
float_tensors, float_tensor_params, float_params = float_values
|
||||
int_tensors, int_tensor_params, int_params = int_values
|
||||
|
||||
# This benchmark assumes that the two fuzzers generate identically
|
||||
# sized and strided Tensors, since the same seed is used.
|
||||
assert_dicts_equal(float_params, int_params)
|
||||
assert_dicts_equal(float_tensor_params["x"], int_tensor_params["x"])
|
||||
|
||||
float_measurement, int_measurement = (
|
||||
Timer(
|
||||
stmt,
|
||||
globals=tensors,
|
||||
).blocked_autorange(min_run_time=_MEASURE_TIME)
|
||||
for tensors in (float_tensors, int_tensors)
|
||||
)
|
||||
|
||||
descriptions = []
|
||||
for name in float_tensors:
|
||||
shape_str = "(" + ", ".join([
|
||||
f"2 ** {int(np.log2(i))}"
|
||||
if 2 ** int(np.log2(i)) == i and i > 1
|
||||
else str(i)
|
||||
for i in float_tensors[name].shape
|
||||
]) + ")"
|
||||
order = float_tensor_params[name]["order"]
|
||||
order_str = ("" if all(order == np.arange(len(order))) else str(tuple(order)))
|
||||
steps = float_tensor_params[name]["steps"]
|
||||
steps_str = str(steps) if sum(steps) > len(steps) else ""
|
||||
descriptions.append((name, shape_str, order_str, steps_str))
|
||||
raw_results.append((float_measurement, int_measurement, descriptions))
|
||||
|
||||
print(f"\r{i + 1} / {n}", end="")
|
||||
print()
|
||||
|
||||
parsed_results, name_len, shape_len, order_len, steps_len = [], 0, 0, 0, 0
|
||||
for float_measurement, int_measurement, descriptions in raw_results:
|
||||
t_float = float_measurement.median * 1e6
|
||||
t_int = int_measurement.median * 1e6
|
||||
rel_diff = abs(t_float - t_int) / (t_float + t_int) * 2
|
||||
parsed_results.append((t_float, t_int, rel_diff, descriptions))
|
||||
for name, shape, order, steps in descriptions:
|
||||
name_len = max(name_len, len(name))
|
||||
shape_len = max(shape_len, len(shape))
|
||||
order_len = max(order_len, len(order))
|
||||
steps_len = max(steps_len, len(steps))
|
||||
|
||||
parsed_results.sort(key=operator.itemgetter(2))
|
||||
|
||||
print(f"stmt: {stmt}")
|
||||
print(f" diff faster{'':>17}{' ' * name_len} ", end="")
|
||||
print(f"{'shape'.ljust(shape_len)}{'':>16}{'order'.ljust(order_len)}", end="")
|
||||
print(f" steps\n{'-' * 100}")
|
||||
for results, spacer in [(parsed_results[:10], "..."), (parsed_results[-10:], "")]:
|
||||
for t_float, t_int, rel_diff, descriptions in results:
|
||||
time_str = [f"{rel_diff * 100:>4.1f}% {'int' if t_int < t_float else 'float':<20}"]
|
||||
time_str.extend(["".ljust(len(time_str[0])) for _ in descriptions[:-1]])
|
||||
for t_str, (name, shape, order, steps) in zip(time_str, descriptions, strict=True):
|
||||
name = f"{name}:".ljust(name_len + 1)
|
||||
shape = shape.ljust(shape_len + 10)
|
||||
order = order.ljust(order_len)
|
||||
print(f"{t_str} {name} {shape}| {order} | {steps}")
|
||||
print(spacer)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
run(n=100, stmt="torch.median(x, dim=0)", fuzzer_cls=UnaryOpFuzzer)
|
||||
run(n=100, stmt="torch.square(x)", fuzzer_cls=UnaryOpFuzzer)
|
||||
run(n=100, stmt="x + y", fuzzer_cls=BinaryOpFuzzer)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
"""Trivial use of Timer API:
|
||||
|
||||
$ python -m examples.simple_timeit
|
||||
"""
|
||||
|
||||
import torch
|
||||
|
||||
import torch.utils.benchmark as benchmark_utils
|
||||
|
||||
|
||||
def main() -> None:
|
||||
timer = benchmark_utils.Timer(
|
||||
stmt="x + y",
|
||||
globals={"x": torch.ones((4, 8)), "y": torch.ones((1, 8))},
|
||||
label="Broadcasting add (4x8)",
|
||||
)
|
||||
|
||||
for i in range(3):
|
||||
print(f"Run: {i}\n{'-' * 40}")
|
||||
print(f"timeit:\n{timer.timeit(10000)}\n")
|
||||
print(f"autorange:\n{timer.blocked_autorange()}\n\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Microbenchmarks for the torch.fft module"""
|
||||
from argparse import ArgumentParser
|
||||
from collections import namedtuple
|
||||
from collections.abc import Iterable
|
||||
|
||||
import torch
|
||||
import torch.fft
|
||||
from torch.utils import benchmark
|
||||
from torch.utils.benchmark.op_fuzzers.spectral import SpectralOpFuzzer
|
||||
|
||||
|
||||
def _dim_options(ndim):
|
||||
if ndim == 1:
|
||||
return [None]
|
||||
elif ndim == 2:
|
||||
return [0, 1, None]
|
||||
elif ndim == 3:
|
||||
return [0, 1, 2, (0, 1), (0, 2), None]
|
||||
raise ValueError(f"Expected ndim in range 1-3, got {ndim}")
|
||||
|
||||
|
||||
def run_benchmark(name: str, function: object, dtype: torch.dtype, seed: int, device: str, samples: int,
|
||||
probability_regular: float):
|
||||
cuda = device == 'cuda'
|
||||
spectral_fuzzer = SpectralOpFuzzer(seed=seed, dtype=dtype, cuda=cuda,
|
||||
probability_regular=probability_regular)
|
||||
results = []
|
||||
for tensors, tensor_params, params in spectral_fuzzer.take(samples):
|
||||
shape = [params['k0'], params['k1'], params['k2']][:params['ndim']]
|
||||
str_shape = ' x '.join([f"{s:<4}" for s in shape])
|
||||
sub_label = f"{str_shape} {'' if tensor_params['x']['is_contiguous'] else '(discontiguous)'}"
|
||||
for dim in _dim_options(params['ndim']):
|
||||
for nthreads in (1, 4, 16) if not cuda else (1,):
|
||||
measurement = benchmark.Timer(
|
||||
stmt='func(x, dim=dim)',
|
||||
globals={'func': function, 'x': tensors['x'], 'dim': dim},
|
||||
label=f"{name}_{device}",
|
||||
sub_label=sub_label,
|
||||
description=f"dim={dim}",
|
||||
num_threads=nthreads,
|
||||
).blocked_autorange(min_run_time=1)
|
||||
measurement.metadata = {
|
||||
'name': name,
|
||||
'device': device,
|
||||
'dim': dim,
|
||||
'shape': shape,
|
||||
}
|
||||
measurement.metadata.update(tensor_params['x'])
|
||||
results.append(measurement)
|
||||
return results
|
||||
|
||||
|
||||
Benchmark = namedtuple('Benchmark', ['name', 'function', 'dtype'])
|
||||
BENCHMARKS = [
|
||||
Benchmark('fft_real', torch.fft.fftn, torch.float32),
|
||||
Benchmark('fft_complex', torch.fft.fftn, torch.complex64),
|
||||
Benchmark('ifft', torch.fft.ifftn, torch.complex64),
|
||||
Benchmark('rfft', torch.fft.rfftn, torch.float32),
|
||||
Benchmark('irfft', torch.fft.irfftn, torch.complex64),
|
||||
]
|
||||
BENCHMARK_MAP = {b.name: b for b in BENCHMARKS}
|
||||
BENCHMARK_NAMES = [b.name for b in BENCHMARKS]
|
||||
DEVICE_NAMES = ['cpu', 'cuda']
|
||||
|
||||
def _output_csv(file, results) -> None:
|
||||
file.write('benchmark,device,num_threads,numel,shape,contiguous,dim,mean (us),median (us),iqr (us)\n')
|
||||
for measurement in results:
|
||||
metadata = measurement.metadata
|
||||
device, dim, shape, name, numel, contiguous = (
|
||||
metadata['device'], metadata['dim'], metadata['shape'],
|
||||
metadata['name'], metadata['numel'], metadata['is_contiguous'])
|
||||
|
||||
if isinstance(dim, Iterable):
|
||||
dim_str = '-'.join(str(d) for d in dim)
|
||||
else:
|
||||
dim_str = str(dim)
|
||||
shape_str = 'x'.join(str(s) for s in shape)
|
||||
|
||||
print(name, device, measurement.task_spec.num_threads, numel, shape_str, contiguous, dim_str, # type: ignore[possibly-undefined]
|
||||
measurement.mean * 1e6, measurement.median * 1e6, measurement.iqr * 1e6,
|
||||
sep=',', file=file)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--device', type=str, choices=DEVICE_NAMES, nargs='+', default=DEVICE_NAMES)
|
||||
parser.add_argument('--bench', type=str, choices=BENCHMARK_NAMES, nargs='+', default=BENCHMARK_NAMES)
|
||||
parser.add_argument('--seed', type=int, default=0)
|
||||
parser.add_argument('--samples', type=int, default=10)
|
||||
parser.add_argument('--probability-regular', '--probability_regular', type=float, default=1.0)
|
||||
parser.add_argument('-o', '--output', type=str)
|
||||
args = parser.parse_args()
|
||||
|
||||
num_benchmarks = len(args.device) * len(args.bench)
|
||||
i = 0
|
||||
results = []
|
||||
for device in args.device:
|
||||
for bench in (BENCHMARK_MAP[b] for b in args.bench):
|
||||
results += run_benchmark(
|
||||
name=bench.name, function=bench.function, dtype=bench.dtype,
|
||||
seed=args.seed, device=device, samples=args.samples,
|
||||
probability_regular=args.probability_regular)
|
||||
i += 1
|
||||
print(f'Completed {bench.name} benchmark on {device} ({i} of {num_benchmarks})')
|
||||
|
||||
if args.output is not None:
|
||||
with open(args.output, 'w') as f:
|
||||
_output_csv(f, results)
|
||||
|
||||
compare = benchmark.Compare(results)
|
||||
compare.trim_significant_figures()
|
||||
compare.colorize()
|
||||
compare.print()
|
||||
@@ -0,0 +1,107 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from torch.utils.benchmark import Fuzzer, FuzzedParameter, ParameterAlias, FuzzedTensor
|
||||
|
||||
|
||||
_MIN_DIM_SIZE = 16
|
||||
_MAX_DIM_SIZE = 16 * 1024 ** 2
|
||||
_POW_TWO_SIZES = tuple(2 ** i for i in range(
|
||||
int(np.log2(_MIN_DIM_SIZE)),
|
||||
int(np.log2(_MAX_DIM_SIZE)) + 1,
|
||||
))
|
||||
|
||||
|
||||
class BinaryOpFuzzer(Fuzzer):
|
||||
def __init__(self, seed, dtype=torch.float32, cuda=False) -> None:
|
||||
super().__init__(
|
||||
parameters=[
|
||||
# Dimensionality of x and y. (e.g. 1D, 2D, or 3D.)
|
||||
FuzzedParameter("dim", distribution={1: 0.3, 2: 0.4, 3: 0.3}, strict=True),
|
||||
|
||||
# Shapes for `x` and `y`.
|
||||
# It is important to test all shapes, however
|
||||
# powers of two are especially important and therefore
|
||||
# warrant special attention. This is done by generating
|
||||
# both a value drawn from all integers between the min and
|
||||
# max allowed values, and another from only the powers of two
|
||||
# (both distributions are loguniform) and then randomly
|
||||
# selecting between the two.
|
||||
# Moreover, `y` will occasionally have singleton
|
||||
# dimensions in order to test broadcasting.
|
||||
[
|
||||
FuzzedParameter(
|
||||
name=f"k_any_{i}",
|
||||
minval=_MIN_DIM_SIZE,
|
||||
maxval=_MAX_DIM_SIZE,
|
||||
distribution="loguniform",
|
||||
) for i in range(3)
|
||||
],
|
||||
[
|
||||
FuzzedParameter(
|
||||
name=f"k_pow2_{i}",
|
||||
distribution={size: 1. / len(_POW_TWO_SIZES) for size in _POW_TWO_SIZES}
|
||||
) for i in range(3)
|
||||
],
|
||||
[
|
||||
FuzzedParameter(
|
||||
name=f"k{i}",
|
||||
distribution={
|
||||
ParameterAlias(f"k_any_{i}"): 0.8,
|
||||
ParameterAlias(f"k_pow2_{i}"): 0.2,
|
||||
},
|
||||
strict=True,
|
||||
) for i in range(3)
|
||||
],
|
||||
|
||||
[
|
||||
FuzzedParameter(
|
||||
name=f"y_k{i}",
|
||||
distribution={
|
||||
ParameterAlias(f"k{i}"): 0.8,
|
||||
1: 0.2,
|
||||
},
|
||||
strict=True,
|
||||
) for i in range(3)
|
||||
],
|
||||
|
||||
# Steps for `x` and `y`. (Benchmarks strided memory access.)
|
||||
[
|
||||
FuzzedParameter(
|
||||
name=f"{name}_step_{i}",
|
||||
distribution={1: 0.8, 2: 0.06, 4: 0.06, 8: 0.04, 16: 0.04},
|
||||
)
|
||||
for i in range(3)
|
||||
for name in ("x", "y")
|
||||
],
|
||||
|
||||
# Repeatable entropy for downstream applications.
|
||||
FuzzedParameter(name="random_value", minval=0, maxval=2 ** 32 - 1, distribution="uniform"),
|
||||
],
|
||||
tensors=[
|
||||
FuzzedTensor(
|
||||
name="x",
|
||||
size=("k0", "k1", "k2"),
|
||||
steps=("x_step_0", "x_step_1", "x_step_2"),
|
||||
probability_contiguous=0.75,
|
||||
min_elements=4 * 1024,
|
||||
max_elements=32 * 1024 ** 2,
|
||||
max_allocation_bytes=2 * 1024**3, # 2 GB
|
||||
dim_parameter="dim",
|
||||
dtype=dtype,
|
||||
cuda=cuda,
|
||||
),
|
||||
FuzzedTensor(
|
||||
name="y",
|
||||
size=("y_k0", "y_k1", "y_k2"),
|
||||
steps=("x_step_0", "x_step_1", "x_step_2"),
|
||||
probability_contiguous=0.75,
|
||||
max_allocation_bytes=2 * 1024**3, # 2 GB
|
||||
dim_parameter="dim",
|
||||
dtype=dtype,
|
||||
cuda=cuda,
|
||||
),
|
||||
],
|
||||
seed=seed,
|
||||
)
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from torch.utils.benchmark import Fuzzer, FuzzedParameter, ParameterAlias, FuzzedSparseTensor
|
||||
|
||||
|
||||
_MIN_DIM_SIZE = 16
|
||||
_MAX_DIM_SIZE = 16 * 1024 ** 2
|
||||
_POW_TWO_SIZES = tuple(2 ** i for i in range(
|
||||
int(np.log2(_MIN_DIM_SIZE)),
|
||||
int(np.log2(_MAX_DIM_SIZE)) + 1,
|
||||
))
|
||||
|
||||
|
||||
class BinaryOpSparseFuzzer(Fuzzer):
|
||||
def __init__(self, seed, dtype=torch.float32, cuda=False) -> None:
|
||||
super().__init__(
|
||||
parameters=[
|
||||
# Dimensionality of x and y. (e.g. 1D, 2D, or 3D.)
|
||||
FuzzedParameter("dim_parameter", distribution={1: 0.3, 2: 0.4, 3: 0.3}, strict=True),
|
||||
FuzzedParameter(
|
||||
name="sparse_dim",
|
||||
distribution={1: 0.4, 2: 0.4, 3: 0.2},
|
||||
strict=True
|
||||
),
|
||||
# Shapes for `x` and `y`.
|
||||
# It is important to test all shapes, however
|
||||
# powers of two are especially important and therefore
|
||||
# warrant special attention. This is done by generating
|
||||
# both a value drawn from all integers between the min and
|
||||
# max allowed values, and another from only the powers of two
|
||||
# (both distributions are loguniform) and then randomly
|
||||
# selecting between the two.
|
||||
# Moreover, `y` will occasionally have singleton
|
||||
# dimensions in order to test broadcasting.
|
||||
[
|
||||
FuzzedParameter(
|
||||
name=f"k_any_{i}",
|
||||
minval=_MIN_DIM_SIZE,
|
||||
maxval=_MAX_DIM_SIZE,
|
||||
distribution="loguniform",
|
||||
) for i in range(3)
|
||||
],
|
||||
[
|
||||
FuzzedParameter(
|
||||
name=f"k_pow2_{i}",
|
||||
distribution={size: 1. / len(_POW_TWO_SIZES) for size in _POW_TWO_SIZES}
|
||||
) for i in range(3)
|
||||
],
|
||||
[
|
||||
FuzzedParameter(
|
||||
name=f"k{i}",
|
||||
distribution={
|
||||
ParameterAlias(f"k_any_{i}"): 0.8,
|
||||
ParameterAlias(f"k_pow2_{i}"): 0.2,
|
||||
},
|
||||
strict=True,
|
||||
) for i in range(3)
|
||||
],
|
||||
[
|
||||
FuzzedParameter(
|
||||
name=f"y_k{i}",
|
||||
distribution={
|
||||
ParameterAlias(f"k{i}"): 1.0},
|
||||
strict=True,
|
||||
) for i in range(3)
|
||||
],
|
||||
FuzzedParameter(
|
||||
name="density",
|
||||
distribution={0.1: 0.4, 0.05: 0.3, 0.01: 0.3},
|
||||
),
|
||||
FuzzedParameter(
|
||||
name="coalesced",
|
||||
distribution={True: 0.5, False: 0.5},
|
||||
),
|
||||
# Repeatable entropy for downstream applications.
|
||||
FuzzedParameter(name="random_value", minval=0, maxval=2 ** 32 - 1, distribution="uniform"),
|
||||
],
|
||||
tensors=[
|
||||
FuzzedSparseTensor(
|
||||
name="x",
|
||||
size=("k0", "k1", "k2"),
|
||||
dim_parameter="dim_parameter",
|
||||
sparse_dim="sparse_dim",
|
||||
density="density",
|
||||
coalesced="coalesced",
|
||||
min_elements=4 * 1024,
|
||||
max_elements=32 * 1024 ** 2,
|
||||
dtype=dtype,
|
||||
cuda=cuda,
|
||||
),
|
||||
FuzzedSparseTensor(
|
||||
name="y",
|
||||
size=("y_k0", "y_k1", "y_k2"),
|
||||
dim_parameter="dim_parameter",
|
||||
sparse_dim="sparse_dim",
|
||||
density="density",
|
||||
coalesced="coalesced",
|
||||
min_elements=4 * 1024,
|
||||
max_elements=32 * 1024 ** 2,
|
||||
dtype=dtype,
|
||||
cuda=cuda,
|
||||
),
|
||||
],
|
||||
seed=seed,
|
||||
)
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch.types import _dtype
|
||||
|
||||
from torch.utils.benchmark import Fuzzer, FuzzedParameter, ParameterAlias, FuzzedSparseTensor
|
||||
|
||||
__all__ = ["UnaryOpSparseFuzzer"]
|
||||
|
||||
_MIN_DIM_SIZE = 16
|
||||
_MAX_DIM_SIZE = 16 * 1024 ** 2
|
||||
_POW_TWO_SIZES = tuple(2 ** i for i in range(
|
||||
int(np.log2(_MIN_DIM_SIZE)),
|
||||
int(np.log2(_MAX_DIM_SIZE)) + 1,
|
||||
))
|
||||
|
||||
class UnaryOpSparseFuzzer(Fuzzer):
|
||||
def __init__(self, seed: int | None, dtype: _dtype | None = None, cuda: bool = False) -> None:
|
||||
if dtype is None:
|
||||
dtype = getattr(torch, 'float32', None)
|
||||
super().__init__(
|
||||
parameters=[
|
||||
# Sparse dim parameter of x. (e.g. 1D, 2D, or 3D.)
|
||||
FuzzedParameter("dim_parameter", distribution={1: 0.3, 2: 0.4, 3: 0.3}, strict=True),
|
||||
FuzzedParameter(
|
||||
name="sparse_dim",
|
||||
distribution={1: 0.4, 2: 0.4, 3: 0.2},
|
||||
strict=True
|
||||
),
|
||||
# Shapes for `x`.
|
||||
# It is important to test all shapes, however
|
||||
# powers of two are especially important and therefore
|
||||
# warrant special attention. This is done by generating
|
||||
# both a value drawn from all integers between the min and
|
||||
# max allowed values, and another from only the powers of two
|
||||
# (both distributions are loguniform) and then randomly
|
||||
# selecting between the two.
|
||||
[
|
||||
FuzzedParameter(
|
||||
name=f"k_any_{i}",
|
||||
minval=_MIN_DIM_SIZE,
|
||||
maxval=_MAX_DIM_SIZE,
|
||||
distribution="loguniform",
|
||||
) for i in range(3)
|
||||
],
|
||||
[
|
||||
FuzzedParameter(
|
||||
name=f"k_pow2_{i}",
|
||||
distribution={size: 1. / len(_POW_TWO_SIZES) for size in _POW_TWO_SIZES}
|
||||
) for i in range(3)
|
||||
],
|
||||
[
|
||||
FuzzedParameter(
|
||||
name=f"k{i}",
|
||||
distribution={
|
||||
ParameterAlias(f"k_any_{i}"): 0.8,
|
||||
ParameterAlias(f"k_pow2_{i}"): 0.2,
|
||||
},
|
||||
strict=True,
|
||||
) for i in range(3)
|
||||
],
|
||||
FuzzedParameter(
|
||||
name="density",
|
||||
distribution={0.1: 0.4, 0.05: 0.3, 0.01: 0.3},
|
||||
),
|
||||
FuzzedParameter(
|
||||
name="coalesced",
|
||||
distribution={True: 0.5, False: 0.5},
|
||||
),
|
||||
FuzzedParameter(name="random_value", minval=0, maxval=2 ** 32 - 1, distribution="uniform"),
|
||||
],
|
||||
tensors=[
|
||||
FuzzedSparseTensor(
|
||||
name="x",
|
||||
size=("k0", "k1", "k2"),
|
||||
dim_parameter="dim_parameter",
|
||||
sparse_dim="sparse_dim",
|
||||
min_elements=4 * 1024,
|
||||
max_elements=32 * 1024 ** 2,
|
||||
density="density",
|
||||
coalesced="coalesced",
|
||||
dtype=dtype,
|
||||
cuda=cuda,
|
||||
),
|
||||
],
|
||||
seed=seed,
|
||||
)
|
||||
@@ -0,0 +1,94 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import math
|
||||
|
||||
import torch
|
||||
from torch.utils import benchmark
|
||||
from torch.utils.benchmark import FuzzedParameter, FuzzedTensor, ParameterAlias
|
||||
|
||||
|
||||
__all__ = ['SpectralOpFuzzer']
|
||||
|
||||
MIN_DIM_SIZE = 16
|
||||
MAX_DIM_SIZE = 16 * 1024
|
||||
|
||||
def power_range(upper_bound, base):
|
||||
return (base ** i for i in range(int(math.log(upper_bound, base)) + 1))
|
||||
|
||||
# List of regular numbers from MIN_DIM_SIZE to MAX_DIM_SIZE
|
||||
# These numbers factorize into multiples of prime factors 2, 3, and 5 only
|
||||
# and are usually the fastest in FFT implementations.
|
||||
REGULAR_SIZES = []
|
||||
for i in power_range(MAX_DIM_SIZE, 2):
|
||||
for j in power_range(MAX_DIM_SIZE // i, 3):
|
||||
ij = i * j
|
||||
for k in power_range(MAX_DIM_SIZE // ij, 5):
|
||||
ijk = ij * k
|
||||
if ijk > MIN_DIM_SIZE:
|
||||
REGULAR_SIZES.append(ijk)
|
||||
REGULAR_SIZES.sort()
|
||||
|
||||
class SpectralOpFuzzer(benchmark.Fuzzer):
|
||||
def __init__(self, *, seed: int, dtype=torch.float64,
|
||||
cuda: bool = False, probability_regular: float = 1.0) -> None:
|
||||
super().__init__(
|
||||
parameters=[
|
||||
# Dimensionality of x. (e.g. 1D, 2D, or 3D.)
|
||||
FuzzedParameter("ndim", distribution={1: 0.3, 2: 0.4, 3: 0.3}, strict=True),
|
||||
|
||||
# Shapes for `x`.
|
||||
# It is important to test all shapes, however
|
||||
# regular sizes are especially important to the FFT and therefore
|
||||
# warrant special attention. This is done by generating
|
||||
# both a value drawn from all integers between the min and
|
||||
# max allowed values, and another from only the regular numbers
|
||||
# (both distributions are loguniform) and then randomly
|
||||
# selecting between the two.
|
||||
[
|
||||
FuzzedParameter(
|
||||
name=f"k_any_{i}",
|
||||
minval=MIN_DIM_SIZE,
|
||||
maxval=MAX_DIM_SIZE,
|
||||
distribution="loguniform",
|
||||
) for i in range(3)
|
||||
],
|
||||
[
|
||||
FuzzedParameter(
|
||||
name=f"k_regular_{i}",
|
||||
distribution={size: 1. / len(REGULAR_SIZES) for size in REGULAR_SIZES}
|
||||
) for i in range(3)
|
||||
],
|
||||
[
|
||||
FuzzedParameter(
|
||||
name=f"k{i}",
|
||||
distribution={
|
||||
ParameterAlias(f"k_regular_{i}"): probability_regular,
|
||||
ParameterAlias(f"k_any_{i}"): 1 - probability_regular,
|
||||
},
|
||||
strict=True,
|
||||
) for i in range(3)
|
||||
],
|
||||
|
||||
# Steps for `x`. (Benchmarks strided memory access.)
|
||||
[
|
||||
FuzzedParameter(
|
||||
name=f"step_{i}",
|
||||
distribution={1: 0.8, 2: 0.06, 4: 0.06, 8: 0.04, 16: 0.04},
|
||||
) for i in range(3)
|
||||
],
|
||||
],
|
||||
tensors=[
|
||||
FuzzedTensor(
|
||||
name="x",
|
||||
size=("k0", "k1", "k2"),
|
||||
steps=("step_0", "step_1", "step_2"),
|
||||
probability_contiguous=0.75,
|
||||
min_elements=4 * 1024,
|
||||
max_elements=32 * 1024 ** 2,
|
||||
max_allocation_bytes=2 * 1024**3, # 2 GB
|
||||
dim_parameter="ndim",
|
||||
dtype=dtype,
|
||||
cuda=cuda,
|
||||
),
|
||||
],
|
||||
seed=seed,
|
||||
)
|
||||
@@ -0,0 +1,82 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from torch.utils.benchmark import Fuzzer, FuzzedParameter, ParameterAlias, FuzzedTensor
|
||||
|
||||
|
||||
_MIN_DIM_SIZE = 16
|
||||
_MAX_DIM_SIZE = 16 * 1024 ** 2
|
||||
_POW_TWO_SIZES = tuple(2 ** i for i in range(
|
||||
int(np.log2(_MIN_DIM_SIZE)),
|
||||
int(np.log2(_MAX_DIM_SIZE)) + 1,
|
||||
))
|
||||
|
||||
|
||||
class UnaryOpFuzzer(Fuzzer):
|
||||
def __init__(self, seed, dtype=torch.float32, cuda=False) -> None:
|
||||
super().__init__(
|
||||
parameters=[
|
||||
# Dimensionality of x. (e.g. 1D, 2D, or 3D.)
|
||||
FuzzedParameter("dim", distribution={1: 0.3, 2: 0.4, 3: 0.3}, strict=True),
|
||||
|
||||
# Shapes for `x`.
|
||||
# It is important to test all shapes, however
|
||||
# powers of two are especially important and therefore
|
||||
# warrant special attention. This is done by generating
|
||||
# both a value drawn from all integers between the min and
|
||||
# max allowed values, and another from only the powers of two
|
||||
# (both distributions are loguniform) and then randomly
|
||||
# selecting between the two.
|
||||
[
|
||||
FuzzedParameter(
|
||||
name=f"k_any_{i}",
|
||||
minval=_MIN_DIM_SIZE,
|
||||
maxval=_MAX_DIM_SIZE,
|
||||
distribution="loguniform",
|
||||
) for i in range(3)
|
||||
],
|
||||
[
|
||||
FuzzedParameter(
|
||||
name=f"k_pow2_{i}",
|
||||
distribution={size: 1. / len(_POW_TWO_SIZES) for size in _POW_TWO_SIZES}
|
||||
) for i in range(3)
|
||||
],
|
||||
[
|
||||
FuzzedParameter(
|
||||
name=f"k{i}",
|
||||
distribution={
|
||||
ParameterAlias(f"k_any_{i}"): 0.8,
|
||||
ParameterAlias(f"k_pow2_{i}"): 0.2,
|
||||
},
|
||||
strict=True,
|
||||
) for i in range(3)
|
||||
],
|
||||
|
||||
# Steps for `x`. (Benchmarks strided memory access.)
|
||||
[
|
||||
FuzzedParameter(
|
||||
name=f"x_step_{i}",
|
||||
distribution={1: 0.8, 2: 0.06, 4: 0.06, 8: 0.04, 16: 0.04},
|
||||
) for i in range(3)
|
||||
],
|
||||
|
||||
# Repeatable entropy for downstream applications.
|
||||
FuzzedParameter(name="random_value", minval=0, maxval=2 ** 32 - 1, distribution="uniform"),
|
||||
],
|
||||
tensors=[
|
||||
FuzzedTensor(
|
||||
name="x",
|
||||
size=("k0", "k1", "k2"),
|
||||
steps=("x_step_0", "x_step_1", "x_step_2"),
|
||||
probability_contiguous=0.75,
|
||||
min_elements=4 * 1024,
|
||||
max_elements=32 * 1024 ** 2,
|
||||
max_allocation_bytes=2 * 1024**3, # 2 GB
|
||||
dim_parameter="dim",
|
||||
dtype=dtype,
|
||||
cuda=cuda,
|
||||
),
|
||||
],
|
||||
seed=seed,
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
from typing import Any
|
||||
from collections.abc import Callable
|
||||
from typing_extensions import Protocol, runtime_checkable
|
||||
|
||||
|
||||
class TimerClass(Protocol):
|
||||
"""This is the portion of the `timeit.Timer` API used by benchmark utils."""
|
||||
def __init__(
|
||||
self,
|
||||
stmt: str,
|
||||
setup: str,
|
||||
timer: Callable[[], float],
|
||||
globals: dict[str, Any],
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
...
|
||||
|
||||
def timeit(self, number: int) -> float:
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class TimeitModuleType(Protocol):
|
||||
"""Modules generated from `timeit_template.cpp`."""
|
||||
def timeit(self, number: int) -> float:
|
||||
...
|
||||
|
||||
|
||||
class CallgrindModuleType(Protocol):
|
||||
"""Replicates the valgrind endpoints in `torch._C`.
|
||||
|
||||
These bindings are used to collect Callgrind profiles on earlier versions
|
||||
of PyTorch and will eventually be removed.
|
||||
"""
|
||||
__file__: str
|
||||
__name__: str
|
||||
|
||||
def _valgrind_supported_platform(self) -> bool:
|
||||
...
|
||||
|
||||
def _valgrind_toggle(self) -> None:
|
||||
...
|
||||
@@ -0,0 +1,359 @@
|
||||
"""Base shared classes and utilities."""
|
||||
|
||||
import collections
|
||||
import contextlib
|
||||
import dataclasses
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import textwrap
|
||||
import time
|
||||
from typing import cast, Any
|
||||
from collections.abc import Iterable, Iterator
|
||||
import uuid
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
__all__ = ["TaskSpec", "Measurement", "select_unit", "unit_to_english", "trim_sigfig", "ordered_unique", "set_torch_threads"]
|
||||
|
||||
|
||||
_MAX_SIGNIFICANT_FIGURES = 4
|
||||
_MIN_CONFIDENCE_INTERVAL = 25e-9 # 25 ns
|
||||
|
||||
# Measurement will include a warning if the distribution is suspect. All
|
||||
# runs are expected to have some variation; these parameters set the
|
||||
# thresholds.
|
||||
_IQR_WARN_THRESHOLD = 0.1
|
||||
_IQR_GROSS_WARN_THRESHOLD = 0.25
|
||||
|
||||
|
||||
@dataclasses.dataclass(init=True, repr=False, eq=True, frozen=True)
|
||||
class TaskSpec:
|
||||
"""Container for information used to define a Timer. (except globals)"""
|
||||
stmt: str
|
||||
setup: str
|
||||
global_setup: str = ""
|
||||
label: str | None = None
|
||||
sub_label: str | None = None
|
||||
description: str | None = None
|
||||
env: str | None = None
|
||||
num_threads: int = 1
|
||||
|
||||
@property
|
||||
def title(self) -> str:
|
||||
"""Best effort attempt at a string label for the measurement."""
|
||||
if self.label is not None:
|
||||
return self.label + (f": {self.sub_label}" if self.sub_label else "")
|
||||
elif "\n" not in self.stmt:
|
||||
return self.stmt + (f": {self.sub_label}" if self.sub_label else "")
|
||||
return (
|
||||
f"stmt:{f' ({self.sub_label})' if self.sub_label else ''}\n"
|
||||
f"{textwrap.indent(self.stmt, ' ')}"
|
||||
)
|
||||
|
||||
def setup_str(self) -> str:
|
||||
return (
|
||||
"" if (self.setup == "pass" or not self.setup)
|
||||
else f"setup:\n{textwrap.indent(self.setup, ' ')}" if "\n" in self.setup
|
||||
else f"setup: {self.setup}"
|
||||
)
|
||||
|
||||
def summarize(self) -> str:
|
||||
"""Build TaskSpec portion of repr string for other containers."""
|
||||
sections = [
|
||||
self.title,
|
||||
self.description or "",
|
||||
self.setup_str(),
|
||||
]
|
||||
return "\n".join([f"{i}\n" if "\n" in i else i for i in sections if i])
|
||||
|
||||
_TASKSPEC_FIELDS = tuple(i.name for i in dataclasses.fields(TaskSpec))
|
||||
|
||||
|
||||
@dataclasses.dataclass(init=True, repr=False)
|
||||
class Measurement:
|
||||
"""The result of a Timer measurement.
|
||||
|
||||
This class stores one or more measurements of a given statement. It is
|
||||
serializable and provides several convenience methods
|
||||
(including a detailed __repr__) for downstream consumers.
|
||||
"""
|
||||
number_per_run: int
|
||||
raw_times: list[float]
|
||||
task_spec: TaskSpec
|
||||
metadata: dict[Any, Any] | None = None # Reserved for user payloads.
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self._sorted_times: tuple[float, ...] = ()
|
||||
self._warnings: tuple[str, ...] = ()
|
||||
self._median: float = -1.0
|
||||
self._mean: float = -1.0
|
||||
self._p25: float = -1.0
|
||||
self._p75: float = -1.0
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
# Forward TaskSpec fields for convenience.
|
||||
if name in _TASKSPEC_FIELDS:
|
||||
return getattr(self.task_spec, name)
|
||||
return super().__getattribute__(name)
|
||||
|
||||
# =========================================================================
|
||||
# == Convenience methods for statistics ===================================
|
||||
# =========================================================================
|
||||
#
|
||||
# These methods use raw time divided by number_per_run; this is an
|
||||
# extrapolation and hides the fact that different number_per_run will
|
||||
# result in different amortization of overheads, however if Timer has
|
||||
# selected an appropriate number_per_run then this is a non-issue, and
|
||||
# forcing users to handle that division would result in a poor experience.
|
||||
@property
|
||||
def times(self) -> list[float]:
|
||||
return [t / self.number_per_run for t in self.raw_times]
|
||||
|
||||
@property
|
||||
def median(self) -> float:
|
||||
self._lazy_init()
|
||||
return self._median
|
||||
|
||||
@property
|
||||
def mean(self) -> float:
|
||||
self._lazy_init()
|
||||
return self._mean
|
||||
|
||||
@property
|
||||
def iqr(self) -> float:
|
||||
self._lazy_init()
|
||||
return self._p75 - self._p25
|
||||
|
||||
@property
|
||||
def significant_figures(self) -> int:
|
||||
"""Approximate significant figure estimate.
|
||||
|
||||
This property is intended to give a convenient way to estimate the
|
||||
precision of a measurement. It only uses the interquartile region to
|
||||
estimate statistics to try to mitigate skew from the tails, and
|
||||
uses a static z value of 1.645 since it is not expected to be used
|
||||
for small values of `n`, so z can approximate `t`.
|
||||
|
||||
The significant figure estimation used in conjunction with the
|
||||
`trim_sigfig` method to provide a more human interpretable data
|
||||
summary. __repr__ does not use this method; it simply displays raw
|
||||
values. Significant figure estimation is intended for `Compare`.
|
||||
"""
|
||||
self._lazy_init()
|
||||
n_total = len(self._sorted_times)
|
||||
lower_bound = int(n_total // 4)
|
||||
upper_bound = int(torch.tensor(3 * n_total / 4).ceil())
|
||||
interquartile_points: tuple[float, ...] = self._sorted_times[lower_bound:upper_bound]
|
||||
std = torch.tensor(interquartile_points).std(unbiased=False).item()
|
||||
sqrt_n = torch.tensor(len(interquartile_points)).sqrt().item()
|
||||
|
||||
# Rough estimates. These are by no means statistically rigorous.
|
||||
confidence_interval = max(1.645 * std / sqrt_n, _MIN_CONFIDENCE_INTERVAL)
|
||||
relative_ci = torch.tensor(self._median / confidence_interval).log10().item()
|
||||
num_significant_figures = int(torch.tensor(relative_ci).floor())
|
||||
return min(max(num_significant_figures, 1), _MAX_SIGNIFICANT_FIGURES)
|
||||
|
||||
@property
|
||||
def has_warnings(self) -> bool:
|
||||
self._lazy_init()
|
||||
return bool(self._warnings)
|
||||
|
||||
def _lazy_init(self) -> None:
|
||||
if self.raw_times and not self._sorted_times:
|
||||
self._sorted_times = tuple(sorted(self.times))
|
||||
_sorted_times = torch.tensor(self._sorted_times, dtype=torch.float64)
|
||||
self._median = _sorted_times.quantile(.5).item()
|
||||
self._mean = _sorted_times.mean().item()
|
||||
self._p25 = _sorted_times.quantile(.25).item()
|
||||
self._p75 = _sorted_times.quantile(.75).item()
|
||||
|
||||
def add_warning(msg: str) -> None:
|
||||
rel_iqr = self.iqr / self.median * 100
|
||||
self._warnings += (
|
||||
f" WARNING: Interquartile range is {rel_iqr:.1f}% "
|
||||
f"of the median measurement.\n {msg}",
|
||||
)
|
||||
|
||||
if not self.meets_confidence(_IQR_GROSS_WARN_THRESHOLD):
|
||||
add_warning("This suggests significant environmental influence.")
|
||||
elif not self.meets_confidence(_IQR_WARN_THRESHOLD):
|
||||
add_warning("This could indicate system fluctuation.")
|
||||
|
||||
|
||||
def meets_confidence(self, threshold: float = _IQR_WARN_THRESHOLD) -> bool:
|
||||
return self.iqr / self.median < threshold
|
||||
|
||||
@property
|
||||
def title(self) -> str:
|
||||
return self.task_spec.title
|
||||
|
||||
@property
|
||||
def env(self) -> str:
|
||||
return (
|
||||
"Unspecified env" if self.taskspec.env is None
|
||||
else cast(str, self.taskspec.env)
|
||||
)
|
||||
|
||||
@property
|
||||
def as_row_name(self) -> str:
|
||||
return self.sub_label or self.stmt or "[Unknown]"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Example repr:
|
||||
<utils.common.Measurement object at 0x7f395b6ac110>
|
||||
Broadcasting add (4x8)
|
||||
Median: 5.73 us
|
||||
IQR: 2.25 us (4.01 to 6.26)
|
||||
372 measurements, 100 runs per measurement, 1 thread
|
||||
WARNING: Interquartile range is 39.4% of the median measurement.
|
||||
This suggests significant environmental influence.
|
||||
"""
|
||||
self._lazy_init()
|
||||
skip_line, newline = "MEASUREMENT_REPR_SKIP_LINE", "\n"
|
||||
n = len(self._sorted_times)
|
||||
time_unit, time_scale = select_unit(self._median)
|
||||
iqr_filter = '' if n >= 4 else skip_line
|
||||
|
||||
repr_str = f"""
|
||||
{super().__repr__()}
|
||||
{self.task_spec.summarize()}
|
||||
{'Median: ' if n > 1 else ''}{self._median / time_scale:.2f} {time_unit}
|
||||
{iqr_filter}IQR: {self.iqr / time_scale:.2f} {time_unit} ({self._p25 / time_scale:.2f} to {self._p75 / time_scale:.2f})
|
||||
{n} measurement{'s' if n > 1 else ''}, {self.number_per_run} runs {'per measurement,' if n > 1 else ','} {self.num_threads} thread{'s' if self.num_threads > 1 else ''}
|
||||
{newline.join(self._warnings)}""".strip() # noqa: B950
|
||||
|
||||
return "\n".join(l for l in repr_str.splitlines(keepends=False) if skip_line not in l)
|
||||
|
||||
@staticmethod
|
||||
def merge(measurements: Iterable["Measurement"]) -> list["Measurement"]:
|
||||
"""Convenience method for merging replicates.
|
||||
|
||||
Merge will extrapolate times to `number_per_run=1` and will not
|
||||
transfer any metadata. (Since it might differ between replicates)
|
||||
"""
|
||||
grouped_measurements: collections.defaultdict[TaskSpec, list[Measurement]] = collections.defaultdict(list)
|
||||
for m in measurements:
|
||||
grouped_measurements[m.task_spec].append(m)
|
||||
|
||||
def merge_group(task_spec: TaskSpec, group: list["Measurement"]) -> "Measurement":
|
||||
times: list[float] = []
|
||||
for m in group:
|
||||
# Different measurements could have different `number_per_run`,
|
||||
# so we call `.times` which normalizes the results.
|
||||
times.extend(m.times)
|
||||
|
||||
return Measurement(
|
||||
number_per_run=1,
|
||||
raw_times=times,
|
||||
task_spec=task_spec,
|
||||
metadata=None,
|
||||
)
|
||||
|
||||
return [merge_group(t, g) for t, g in grouped_measurements.items()]
|
||||
|
||||
|
||||
def select_unit(t: float) -> tuple[str, float]:
|
||||
"""Determine how to scale times for O(1) magnitude.
|
||||
|
||||
This utility is used to format numbers for human consumption.
|
||||
"""
|
||||
time_unit = {-3: "ns", -2: "us", -1: "ms"}.get(int(torch.tensor(t).log10().item() // 3), "s")
|
||||
time_scale = {"ns": 1e-9, "us": 1e-6, "ms": 1e-3, "s": 1}[time_unit]
|
||||
return time_unit, time_scale
|
||||
|
||||
|
||||
def unit_to_english(u: str) -> str:
|
||||
return {
|
||||
"ns": "nanosecond",
|
||||
"us": "microsecond",
|
||||
"ms": "millisecond",
|
||||
"s": "second",
|
||||
}[u]
|
||||
|
||||
|
||||
def trim_sigfig(x: float, n: int) -> float:
|
||||
"""Trim `x` to `n` significant figures. (e.g. 3.14159, 2 -> 3.10000)"""
|
||||
if n != int(n):
|
||||
raise AssertionError("Number of significant figures must be an integer")
|
||||
magnitude = int(torch.tensor(x).abs().log10().ceil().item())
|
||||
scale = 10 ** (magnitude - n)
|
||||
return float(torch.tensor(x / scale).round() * scale)
|
||||
|
||||
|
||||
def ordered_unique(elements: Iterable[Any]) -> list[Any]:
|
||||
return list(collections.OrderedDict(dict.fromkeys(elements)).keys())
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def set_torch_threads(n: int) -> Iterator[None]:
|
||||
prior_num_threads = torch.get_num_threads()
|
||||
try:
|
||||
torch.set_num_threads(n)
|
||||
yield
|
||||
finally:
|
||||
torch.set_num_threads(prior_num_threads)
|
||||
|
||||
|
||||
def _make_temp_dir(prefix: str | None = None, gc_dev_shm: bool = False) -> str:
|
||||
"""Create a temporary directory. The caller is responsible for cleanup.
|
||||
|
||||
This function is conceptually similar to `tempfile.mkdtemp`, but with
|
||||
the key additional feature that it will use shared memory if the
|
||||
`BENCHMARK_USE_DEV_SHM` environment variable is set. This is an
|
||||
implementation detail, but an important one for cases where many Callgrind
|
||||
measurements are collected at once. (Such as when collecting
|
||||
microbenchmarks.)
|
||||
|
||||
This is an internal utility, and is exported solely so that microbenchmarks
|
||||
can reuse the util.
|
||||
"""
|
||||
use_dev_shm: bool = (os.getenv("BENCHMARK_USE_DEV_SHM") or "").lower() in ("1", "true")
|
||||
if use_dev_shm:
|
||||
root = "/dev/shm/pytorch_benchmark_utils"
|
||||
if os.name != "posix":
|
||||
raise AssertionError(f"tmpfs (/dev/shm) is POSIX only, current platform is {os.name}")
|
||||
if not os.path.exists("/dev/shm"):
|
||||
raise AssertionError("This system does not appear to support tmpfs (/dev/shm).")
|
||||
os.makedirs(root, exist_ok=True)
|
||||
|
||||
# Because we're working in shared memory, it is more important than
|
||||
# usual to clean up ALL intermediate files. However we don't want every
|
||||
# worker to walk over all outstanding directories, so instead we only
|
||||
# check when we are sure that it won't lead to contention.
|
||||
if gc_dev_shm:
|
||||
for i in os.listdir(root):
|
||||
owner_file = os.path.join(root, i, "owner.pid")
|
||||
if not os.path.exists(owner_file):
|
||||
continue
|
||||
|
||||
with open(owner_file) as f:
|
||||
owner_pid = int(f.read())
|
||||
|
||||
if owner_pid == os.getpid():
|
||||
continue
|
||||
|
||||
try:
|
||||
# https://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid-in-python
|
||||
os.kill(owner_pid, 0)
|
||||
|
||||
except OSError:
|
||||
print(f"Detected that {os.path.join(root, i)} was orphaned in shared memory. Cleaning up.")
|
||||
shutil.rmtree(os.path.join(root, i))
|
||||
|
||||
else:
|
||||
root = tempfile.gettempdir()
|
||||
|
||||
# We include the time so names sort by creation time, and add a UUID
|
||||
# to ensure we don't collide.
|
||||
name = f"{prefix or tempfile.gettempprefix()}__{int(time.time())}__{uuid.uuid4()}"
|
||||
path = os.path.join(root, name)
|
||||
os.makedirs(path, exist_ok=False)
|
||||
|
||||
if use_dev_shm:
|
||||
with open(os.path.join(path, "owner.pid"), "w") as f:
|
||||
f.write(str(os.getpid()))
|
||||
|
||||
return path
|
||||
@@ -0,0 +1,345 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Display class to aggregate and print the results of many measurements."""
|
||||
import collections
|
||||
import enum
|
||||
import itertools as it
|
||||
|
||||
from torch.utils.benchmark.utils import common
|
||||
from torch import tensor as _tensor
|
||||
import operator
|
||||
|
||||
__all__ = ["Colorize", "Compare"]
|
||||
|
||||
BEST = "\033[92m"
|
||||
GOOD = "\033[34m"
|
||||
BAD = "\033[2m\033[91m"
|
||||
VERY_BAD = "\033[31m"
|
||||
BOLD = "\033[1m"
|
||||
TERMINATE = "\033[0m"
|
||||
|
||||
|
||||
class Colorize(enum.Enum):
|
||||
NONE = "none"
|
||||
COLUMNWISE = "columnwise"
|
||||
ROWWISE = "rowwise"
|
||||
|
||||
|
||||
# Classes to separate internal bookkeeping from what is rendered.
|
||||
class _Column:
|
||||
def __init__(
|
||||
self,
|
||||
grouped_results: list[tuple[common.Measurement | None, ...]],
|
||||
time_scale: float,
|
||||
time_unit: str,
|
||||
trim_significant_figures: bool,
|
||||
highlight_warnings: bool,
|
||||
) -> None:
|
||||
self._grouped_results = grouped_results
|
||||
self._flat_results = [*it.chain.from_iterable(grouped_results)]
|
||||
self._time_scale = time_scale
|
||||
self._time_unit = time_unit
|
||||
self._trim_significant_figures = trim_significant_figures
|
||||
self._highlight_warnings = (
|
||||
highlight_warnings
|
||||
and any(r.has_warnings for r in self._flat_results if r)
|
||||
)
|
||||
leading_digits = [
|
||||
int(_tensor(r.median / self._time_scale).log10().ceil()) if r else None
|
||||
for r in self._flat_results
|
||||
]
|
||||
unit_digits = max(d for d in leading_digits if d is not None)
|
||||
decimal_digits = min(
|
||||
max(m.significant_figures - digits, 0)
|
||||
for digits, m in zip(leading_digits, self._flat_results, strict=True)
|
||||
if (m is not None) and (digits is not None)
|
||||
) if self._trim_significant_figures else 1
|
||||
length = unit_digits + decimal_digits + (1 if decimal_digits else 0)
|
||||
self._template = f"{{:>{length}.{decimal_digits}f}}{{:>{7 if self._highlight_warnings else 0}}}"
|
||||
|
||||
def get_results_for(self, group):
|
||||
return self._grouped_results[group]
|
||||
|
||||
def num_to_str(self, value: float | None, estimated_sigfigs: int, spread: float | None):
|
||||
if value is None:
|
||||
return " " * len(self.num_to_str(1, estimated_sigfigs, None))
|
||||
|
||||
if self._trim_significant_figures:
|
||||
value = common.trim_sigfig(value, estimated_sigfigs)
|
||||
|
||||
return self._template.format(
|
||||
value,
|
||||
f" (! {spread * 100:.0f}%)" if self._highlight_warnings and spread is not None else "")
|
||||
|
||||
|
||||
def optional_min(seq):
|
||||
l = list(seq)
|
||||
return None if len(l) == 0 else min(l)
|
||||
|
||||
|
||||
class _Row:
|
||||
def __init__(self, results, row_group, render_env, env_str_len,
|
||||
row_name_str_len, time_scale, colorize, num_threads=None) -> None:
|
||||
super().__init__()
|
||||
self._results = results
|
||||
self._row_group = row_group
|
||||
self._render_env = render_env
|
||||
self._env_str_len = env_str_len
|
||||
self._row_name_str_len = row_name_str_len
|
||||
self._time_scale = time_scale
|
||||
self._colorize = colorize
|
||||
self._columns: tuple[_Column, ...] = ()
|
||||
self._num_threads = num_threads
|
||||
|
||||
def register_columns(self, columns: tuple[_Column, ...]) -> None:
|
||||
self._columns = columns
|
||||
|
||||
def as_column_strings(self):
|
||||
concrete_results = [r for r in self._results if r is not None]
|
||||
env = f"({concrete_results[0].env})" if self._render_env else ""
|
||||
env = env.ljust(self._env_str_len + 4)
|
||||
output = [" " + env + concrete_results[0].as_row_name]
|
||||
for m, col in zip(self._results, self._columns or (), strict=False):
|
||||
if m is None:
|
||||
output.append(col.num_to_str(None, 1, None))
|
||||
else:
|
||||
output.append(col.num_to_str(
|
||||
m.median / self._time_scale,
|
||||
m.significant_figures,
|
||||
m.iqr / m.median if m.has_warnings else None
|
||||
))
|
||||
return output
|
||||
|
||||
@staticmethod
|
||||
def color_segment(segment, value, best_value):
|
||||
if value <= best_value * 1.01 or value <= best_value + 100e-9:
|
||||
return BEST + BOLD + segment + TERMINATE * 2
|
||||
if value <= best_value * 1.1:
|
||||
return GOOD + BOLD + segment + TERMINATE * 2
|
||||
if value >= best_value * 5:
|
||||
return VERY_BAD + BOLD + segment + TERMINATE * 2
|
||||
if value >= best_value * 2:
|
||||
return BAD + segment + TERMINATE * 2
|
||||
|
||||
return segment
|
||||
|
||||
def row_separator(self, overall_width):
|
||||
return (
|
||||
[f"{self._num_threads} threads: ".ljust(overall_width, "-")]
|
||||
if self._num_threads is not None else []
|
||||
)
|
||||
|
||||
def finalize_column_strings(self, column_strings, col_widths):
|
||||
best_values = [-1 for _ in column_strings]
|
||||
if self._colorize == Colorize.ROWWISE:
|
||||
row_min = min(r.median for r in self._results if r is not None)
|
||||
best_values = [row_min for _ in column_strings]
|
||||
elif self._colorize == Colorize.COLUMNWISE:
|
||||
best_values = [
|
||||
optional_min(r.median for r in column.get_results_for(self._row_group) if r is not None)
|
||||
for column in (self._columns or ())
|
||||
]
|
||||
|
||||
row_contents = [column_strings[0].ljust(col_widths[0])]
|
||||
for col_str, width, result, best_value in zip(column_strings[1:], col_widths[1:], self._results, best_values, strict=False):
|
||||
col_str = col_str.center(width)
|
||||
if self._colorize != Colorize.NONE and result is not None and best_value is not None:
|
||||
col_str = self.color_segment(col_str, result.median, best_value)
|
||||
row_contents.append(col_str)
|
||||
return row_contents
|
||||
|
||||
|
||||
class Table:
|
||||
def __init__(
|
||||
self,
|
||||
results: list[common.Measurement],
|
||||
colorize: Colorize,
|
||||
trim_significant_figures: bool,
|
||||
highlight_warnings: bool
|
||||
) -> None:
|
||||
if len({r.label for r in results}) != 1:
|
||||
raise AssertionError("All results must share the same label")
|
||||
|
||||
self.results = results
|
||||
self._colorize = colorize
|
||||
self._trim_significant_figures = trim_significant_figures
|
||||
self._highlight_warnings = highlight_warnings
|
||||
self.label = results[0].label
|
||||
self.time_unit, self.time_scale = common.select_unit(
|
||||
min(r.median for r in results)
|
||||
)
|
||||
|
||||
self.row_keys = common.ordered_unique([self.row_fn(i) for i in results])
|
||||
self.row_keys.sort(key=operator.itemgetter(slice(2))) # preserve stmt order
|
||||
self.column_keys = common.ordered_unique([self.col_fn(i) for i in results])
|
||||
self.rows, self.columns = self.populate_rows_and_columns()
|
||||
|
||||
@staticmethod
|
||||
def row_fn(m: common.Measurement) -> tuple[int, str | None, str]:
|
||||
return m.num_threads, m.env, m.as_row_name
|
||||
|
||||
@staticmethod
|
||||
def col_fn(m: common.Measurement) -> str | None:
|
||||
return m.description
|
||||
|
||||
def populate_rows_and_columns(self) -> tuple[tuple[_Row, ...], tuple[_Column, ...]]:
|
||||
rows: list[_Row] = []
|
||||
columns: list[_Column] = []
|
||||
ordered_results: list[list[common.Measurement | None]] = [
|
||||
[None for _ in self.column_keys]
|
||||
for _ in self.row_keys
|
||||
]
|
||||
row_position = {key: i for i, key in enumerate(self.row_keys)}
|
||||
col_position = {key: i for i, key in enumerate(self.column_keys)}
|
||||
for r in self.results:
|
||||
i = row_position[self.row_fn(r)]
|
||||
j = col_position[self.col_fn(r)]
|
||||
ordered_results[i][j] = r
|
||||
|
||||
unique_envs = {r.env for r in self.results}
|
||||
render_env = len(unique_envs) > 1
|
||||
env_str_len = max(len(i) for i in unique_envs) if render_env else 0
|
||||
|
||||
row_name_str_len = max(len(r.as_row_name) for r in self.results)
|
||||
|
||||
prior_num_threads = -1
|
||||
prior_env = ""
|
||||
row_group = -1
|
||||
rows_by_group: list[list[list[common.Measurement | None]]] = []
|
||||
for (num_threads, env, _), row in zip(self.row_keys, ordered_results, strict=True):
|
||||
thread_transition = (num_threads != prior_num_threads)
|
||||
if thread_transition:
|
||||
prior_num_threads = num_threads
|
||||
prior_env = ""
|
||||
row_group += 1
|
||||
rows_by_group.append([])
|
||||
rows.append(
|
||||
_Row(
|
||||
results=row,
|
||||
row_group=row_group,
|
||||
render_env=(render_env and env != prior_env),
|
||||
env_str_len=env_str_len,
|
||||
row_name_str_len=row_name_str_len,
|
||||
time_scale=self.time_scale,
|
||||
colorize=self._colorize,
|
||||
num_threads=num_threads if thread_transition else None,
|
||||
)
|
||||
)
|
||||
rows_by_group[-1].append(row)
|
||||
prior_env = env
|
||||
|
||||
for i in range(len(self.column_keys)):
|
||||
grouped_results = [tuple(row[i] for row in g) for g in rows_by_group]
|
||||
column = _Column(
|
||||
grouped_results=grouped_results,
|
||||
time_scale=self.time_scale,
|
||||
time_unit=self.time_unit,
|
||||
trim_significant_figures=self._trim_significant_figures,
|
||||
highlight_warnings=self._highlight_warnings,)
|
||||
columns.append(column)
|
||||
|
||||
rows_tuple, columns_tuple = tuple(rows), tuple(columns)
|
||||
for ri in rows_tuple:
|
||||
ri.register_columns(columns_tuple)
|
||||
return rows_tuple, columns_tuple
|
||||
|
||||
def render(self) -> str:
|
||||
string_rows = [[""] + self.column_keys]
|
||||
string_rows.extend(r.as_column_strings() for r in self.rows)
|
||||
num_cols = max(len(i) for i in string_rows)
|
||||
for sr in string_rows:
|
||||
sr.extend(["" for _ in range(num_cols - len(sr))])
|
||||
|
||||
col_widths = [max(len(j) for j in i) for i in zip(*string_rows, strict=True)]
|
||||
finalized_columns = [" | ".join(i.center(w) for i, w in zip(string_rows[0], col_widths, strict=True))]
|
||||
overall_width = len(finalized_columns[0])
|
||||
for string_row, row in zip(string_rows[1:], self.rows, strict=True):
|
||||
finalized_columns.extend(row.row_separator(overall_width))
|
||||
finalized_columns.append(" | ".join(row.finalize_column_strings(string_row, col_widths)))
|
||||
|
||||
newline = "\n"
|
||||
has_warnings = self._highlight_warnings and any(ri.has_warnings for ri in self.results)
|
||||
return f"""
|
||||
[{(' ' + (self.label or '') + ' ').center(overall_width - 2, '-')}]
|
||||
{newline.join(finalized_columns)}
|
||||
|
||||
Times are in {common.unit_to_english(self.time_unit)}s ({self.time_unit}).
|
||||
{'(! XX%) Measurement has high variance, where XX is the IQR / median * 100.' + newline if has_warnings else ""}"""[1:]
|
||||
|
||||
|
||||
class Compare:
|
||||
"""Helper class for displaying the results of many measurements in a
|
||||
formatted table.
|
||||
|
||||
The table format is based on the information fields provided in
|
||||
:class:`torch.utils.benchmark.Timer` (`description`, `label`, `sub_label`,
|
||||
`num_threads`, etc).
|
||||
|
||||
The table can be directly printed using :meth:`print` or casted as a `str`.
|
||||
|
||||
For a full tutorial on how to use this class, see:
|
||||
https://pytorch.org/tutorials/recipes/recipes/benchmark.html
|
||||
|
||||
Args:
|
||||
results: List of Measurement to display.
|
||||
"""
|
||||
def __init__(self, results: list[common.Measurement]) -> None:
|
||||
self._results: list[common.Measurement] = []
|
||||
self.extend_results(results)
|
||||
self._trim_significant_figures = False
|
||||
self._colorize = Colorize.NONE
|
||||
self._highlight_warnings = False
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "\n".join(self._render())
|
||||
|
||||
def extend_results(self, results) -> None:
|
||||
"""Append results to already stored ones.
|
||||
|
||||
All added results must be instances of ``Measurement``.
|
||||
"""
|
||||
for r in results:
|
||||
if not isinstance(r, common.Measurement):
|
||||
raise ValueError(
|
||||
"Expected an instance of `Measurement`, " f"got {type(r)} instead."
|
||||
)
|
||||
self._results.extend(results)
|
||||
|
||||
def trim_significant_figures(self) -> None:
|
||||
"""Enables trimming of significant figures when building the formatted table."""
|
||||
self._trim_significant_figures = True
|
||||
|
||||
def colorize(self, rowwise=False) -> None:
|
||||
"""Colorize formatted table.
|
||||
|
||||
Colorize columnwise by default.
|
||||
"""
|
||||
self._colorize = Colorize.ROWWISE if rowwise else Colorize.COLUMNWISE
|
||||
|
||||
def highlight_warnings(self) -> None:
|
||||
"""Enables warning highlighting when building formatted table."""
|
||||
self._highlight_warnings = True
|
||||
|
||||
def print(self) -> None:
|
||||
"""Print formatted table"""
|
||||
print(str(self))
|
||||
|
||||
def _render(self):
|
||||
results = common.Measurement.merge(self._results)
|
||||
grouped_results = self._group_by_label(results)
|
||||
output = [self._layout(group) for group in grouped_results.values()]
|
||||
return output
|
||||
|
||||
def _group_by_label(self, results: list[common.Measurement]):
|
||||
grouped_results: collections.defaultdict[str, list[common.Measurement]] = collections.defaultdict(list)
|
||||
for r in results:
|
||||
grouped_results[r.label].append(r)
|
||||
return grouped_results
|
||||
|
||||
def _layout(self, results: list[common.Measurement]):
|
||||
table = Table(
|
||||
results,
|
||||
self._colorize,
|
||||
self._trim_significant_figures,
|
||||
self._highlight_warnings
|
||||
)
|
||||
return table.render()
|
||||
@@ -0,0 +1,195 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from typing import Any, cast
|
||||
from collections.abc import Callable
|
||||
|
||||
import torch
|
||||
import torch._dynamo
|
||||
from torch._dynamo.testing import CompileCounterWithBackend
|
||||
from torch.utils.benchmark import Timer
|
||||
|
||||
|
||||
__all__ = ["bench_all", "benchmark_compile"]
|
||||
|
||||
|
||||
_warned_tensor_cores = False
|
||||
_default_float_32_precision = torch.get_float32_matmul_precision()
|
||||
|
||||
try:
|
||||
|
||||
from tabulate import tabulate
|
||||
|
||||
HAS_TABULATE = True
|
||||
except ModuleNotFoundError:
|
||||
HAS_TABULATE = False
|
||||
tabulate = None # type: ignore[assignment]
|
||||
print("tabulate is not installed, please pip install tabulate to use this utility")
|
||||
|
||||
if HAS_TABULATE:
|
||||
def _enable_tensor_cores() -> None:
|
||||
global _warned_tensor_cores
|
||||
|
||||
if torch.cuda.is_available():
|
||||
if torch.backends.cuda.matmul.allow_tf32 is False and torch.cuda.get_device_capability() >= (8, 0):
|
||||
torch.set_float32_matmul_precision("high")
|
||||
if not _warned_tensor_cores:
|
||||
print("Your GPU supports tensor cores")
|
||||
print("we will enable it automatically by setting `torch.set_float32_matmul_precision('high')`")
|
||||
_warned_tensor_cores = True
|
||||
|
||||
def _disable_tensor_cores() -> None:
|
||||
torch.set_float32_matmul_precision(_default_float_32_precision)
|
||||
|
||||
def bench_loop(
|
||||
model: torch.nn.Module | Callable,
|
||||
sample_input: torch.Tensor | Any,
|
||||
num_iters: int = 5,
|
||||
optimizer: torch.optim.Optimizer | None = None,
|
||||
loss_fn: Callable | None = None,
|
||||
):
|
||||
# Define the statement and setup for the benchmark
|
||||
if optimizer and loss_fn:
|
||||
# Training mode
|
||||
stmt = """
|
||||
output = model(sample_input)
|
||||
loss = loss_fn(output) if loss_fn else output.sum()
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
optimizer.zero_grad()
|
||||
"""
|
||||
else:
|
||||
# Inference mode
|
||||
stmt = "model(sample_input)"
|
||||
|
||||
# Create the Timer object
|
||||
timer = Timer(
|
||||
stmt=stmt,
|
||||
globals={"model": model, "sample_input": sample_input, "optimizer": optimizer, "loss_fn": loss_fn},
|
||||
)
|
||||
|
||||
|
||||
result = timer.timeit(number=num_iters)
|
||||
|
||||
# Get the average time per iteration in milliseconds
|
||||
avg_time = result.mean * 1000
|
||||
return round(avg_time, 2)
|
||||
|
||||
def benchmark_compile(
|
||||
model: torch.nn.Module | Callable,
|
||||
sample_input: torch.Tensor | Any,
|
||||
num_iters: int = 5,
|
||||
backend: str | None = None,
|
||||
mode: str | None = "default",
|
||||
optimizer: torch.optim.Optimizer | None = None,
|
||||
loss_fn : torch.nn.Module | Callable | None = None,
|
||||
):
|
||||
"""
|
||||
Use this utility to benchmark torch.compile
|
||||
"""
|
||||
if backend:
|
||||
try:
|
||||
torch._dynamo.reset()
|
||||
compile_counter_with_backend = CompileCounterWithBackend(backend)
|
||||
opt_model = torch.compile(model, backend=compile_counter_with_backend, mode=mode)
|
||||
|
||||
# Compilation only happens after the first inference
|
||||
compilation_time = bench_loop(opt_model, sample_input, 1, optimizer, loss_fn)
|
||||
|
||||
running_time = bench_loop(opt_model, sample_input, num_iters, optimizer, loss_fn)
|
||||
|
||||
if compile_counter_with_backend.frame_count == 0:
|
||||
raise RuntimeError("No compilation occurred during benchmarking.")
|
||||
|
||||
if compile_counter_with_backend.frame_count > 1:
|
||||
raise RuntimeError("Recompilation occurred during benchmarking.")
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print(f"Failed to compile {backend} with mode {mode}")
|
||||
return None, None
|
||||
else:
|
||||
opt_model = model
|
||||
compilation_time = None
|
||||
running_time = bench_loop(opt_model, sample_input, num_iters, optimizer, loss_fn)
|
||||
|
||||
compilation_time = round(compilation_time, 2) if compilation_time else None
|
||||
running_time = round(running_time, 2) if running_time else None
|
||||
|
||||
|
||||
return compilation_time, running_time
|
||||
|
||||
|
||||
def bench_all(
|
||||
model : torch.nn.Module | Callable,
|
||||
sample_input: torch.Tensor | Any,
|
||||
num_iters : int = 5,
|
||||
optimizer: torch.optim.Optimizer | None = None,
|
||||
loss_fn : torch.nn.Module | Callable | None = None,
|
||||
):
|
||||
"""
|
||||
This is a simple utility that can be used to benchmark torch.compile
|
||||
In particular it ensures that your GPU is setup to use tensor cores if it supports its
|
||||
It also tries out all the main backends and prints a table of results so you can easily compare them all
|
||||
Many of the backendds have their own optional dependencies so please pip install them separately
|
||||
|
||||
You will get one table for inference and another for training
|
||||
If you'd like to leverage this utility for training make sure to pass in a torch.optim.Optimizer
|
||||
|
||||
The important warnings are
|
||||
Your GPU supports tensor cores
|
||||
we will enable it automatically by setting `torch.set_float32_matmul_precision('high')`
|
||||
|
||||
If a compilation fails for any reason including the dependency not being included
|
||||
then we will print Failed to compile {backend} with mode {mode}
|
||||
"""
|
||||
field_names = ["Train/Inference", "Backend", "Mode", "Compilation Time", "Average Running Time"]
|
||||
table = []
|
||||
|
||||
|
||||
eager_time = None
|
||||
torch._dynamo.reset()
|
||||
_, eager_time = benchmark_compile(model, sample_input, num_iters, None, None, optimizer)
|
||||
table.append(
|
||||
[("Training" if optimizer else "Inference"), "Eager", "-", "-", f"{eager_time} ms"]
|
||||
)
|
||||
|
||||
for backend in torch._dynamo.list_backends():
|
||||
|
||||
if backend == "inductor":
|
||||
mode_options = cast(list[str | None], list(torch._inductor.list_mode_options().keys())) + [None]
|
||||
for mode in mode_options:
|
||||
if mode == "default":
|
||||
continue
|
||||
torch._dynamo.reset()
|
||||
try:
|
||||
if torch.cuda.is_available():
|
||||
_enable_tensor_cores()
|
||||
compilation_time, running_time = benchmark_compile(
|
||||
model, sample_input, num_iters, backend, mode, optimizer, loss_fn)
|
||||
finally:
|
||||
if torch.cuda.is_available():
|
||||
_disable_tensor_cores()
|
||||
table.append([
|
||||
("Training" if optimizer else "Inference"),
|
||||
# pyrefly: ignore [redundant-condition]
|
||||
backend if backend else "-",
|
||||
mode if mode is not None else "-",
|
||||
f"{compilation_time} ms " if compilation_time else "-",
|
||||
f"{running_time} ms " if running_time else "-",
|
||||
])
|
||||
|
||||
else:
|
||||
torch._dynamo.reset()
|
||||
compilation_time, running_time = benchmark_compile(
|
||||
model, sample_input, num_iters, backend, None, optimizer, loss_fn)
|
||||
|
||||
if running_time is not None:
|
||||
table.append([
|
||||
("Training" if optimizer else "Inference"),
|
||||
backend, "-",
|
||||
f"{compilation_time} ms " or "-",
|
||||
f"{running_time} ms ",
|
||||
])
|
||||
|
||||
|
||||
# pyrefly: ignore [not-callable]
|
||||
return tabulate(table, headers=field_names, tablefmt="github")
|
||||
@@ -0,0 +1,175 @@
|
||||
"""JIT C++ strings into executables."""
|
||||
import atexit
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import textwrap
|
||||
import threading
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from torch.utils.benchmark.utils._stubs import CallgrindModuleType, TimeitModuleType
|
||||
from torch.utils.benchmark.utils.common import _make_temp_dir
|
||||
from torch.utils import cpp_extension
|
||||
|
||||
|
||||
LOCK = threading.Lock()
|
||||
SOURCE_ROOT = os.path.split(os.path.abspath(__file__))[0]
|
||||
|
||||
# We calculate uuid once at import time so that separate processes will have
|
||||
# separate build roots, but threads will share the same build root.
|
||||
# `cpp_extension` uses build root as part of the cache key, so per-invocation
|
||||
# uuid's (e.g. different build root per _compile_template call) would lead to
|
||||
# a 0% cache hit rate and spurious recompilation. Consider the following:
|
||||
# ```
|
||||
# setup = "auto x = torch::ones({1024, 1024});"
|
||||
# stmt = "torch::mm(x, x);"
|
||||
# for num_threads in [1, 2, 4, 8]:
|
||||
# print(Timer(stmt, setup, num_threads=num_threads, language="c++").blocked_autorange())
|
||||
# ````
|
||||
# `setup` and `stmt` do not change, so we can reuse the executable from the
|
||||
# first pass through the loop.
|
||||
_BUILD_ROOT: str | None = None
|
||||
|
||||
def _get_build_root() -> str:
|
||||
global _BUILD_ROOT
|
||||
if _BUILD_ROOT is None:
|
||||
_BUILD_ROOT = _make_temp_dir(prefix="benchmark_utils_jit_build")
|
||||
# pyrefly: ignore [missing-argument]
|
||||
atexit.register(shutil.rmtree, _BUILD_ROOT)
|
||||
return _BUILD_ROOT
|
||||
|
||||
|
||||
# BACK_TESTING_NOTE:
|
||||
# There are two workflows where this code could be used. One is the obvious
|
||||
# case where someone simply builds or installs PyTorch and uses Timer.
|
||||
# The other is that the entire `torch/utils/benchmark` folder from a CURRENT
|
||||
# PyTorch checkout is copy-pasted into a much OLDER version of the PyTorch
|
||||
# source code. This is what we refer to here as "back testing". The rationale
|
||||
# is that we might want to use current tooling to study some aspect of an
|
||||
# earlier version of PyTorch. (e.g. a regression.)
|
||||
#
|
||||
# The problem is that Timer relies on several aspects of core PyTorch, namely
|
||||
# some binding functions for Valgrind symbols in `torch._C` and the
|
||||
# `torch.__config__._cxx_flags()` method. If we were to naively copy code
|
||||
# around this wouldn't work as the symbols of interest aren't present in
|
||||
# earlier versions of PyTorch. In order to work around this, we must add back
|
||||
# testing shims. These shims will never activate during normal use, but will
|
||||
# allow Timer to function outside of the "correct" version of PyTorch by
|
||||
# emulating functionality that was added later.
|
||||
#
|
||||
# These shims are temporary, and as Timer becomes more integrated with
|
||||
# PyTorch the cost and complexity of such shims will increase. Once back
|
||||
# testing is no longer required (which is to say we have done enough historic
|
||||
# analysis and the shims no longer justify their maintenance and code
|
||||
# complexity costs) back testing paths will be removed.
|
||||
|
||||
CXX_FLAGS: list[str] | None
|
||||
if hasattr(torch.__config__, "_cxx_flags"):
|
||||
try:
|
||||
CXX_FLAGS = torch.__config__._cxx_flags().strip().split()
|
||||
if CXX_FLAGS is not None and "-g" not in CXX_FLAGS:
|
||||
CXX_FLAGS.append("-g")
|
||||
# remove "-W" flags to allow build benchmarks
|
||||
# with a relaxed constraint of compiler versions
|
||||
if CXX_FLAGS is not None:
|
||||
CXX_FLAGS = list(filter(lambda x: not x.startswith("-W"), CXX_FLAGS))
|
||||
|
||||
except RuntimeError:
|
||||
# We are in FBCode.
|
||||
CXX_FLAGS = None
|
||||
else:
|
||||
# FIXME: Remove when back testing is no longer required.
|
||||
CXX_FLAGS = ["-O2", "-fPIC", "-g"]
|
||||
|
||||
EXTRA_INCLUDE_PATHS: list[str] = [os.path.join(SOURCE_ROOT, "valgrind_wrapper")]
|
||||
CONDA_PREFIX = os.getenv("CONDA_PREFIX")
|
||||
if CONDA_PREFIX is not None:
|
||||
# Load will automatically search /usr/include, but not conda include.
|
||||
EXTRA_INCLUDE_PATHS.append(os.path.join(CONDA_PREFIX, "include"))
|
||||
|
||||
|
||||
COMPAT_CALLGRIND_BINDINGS: CallgrindModuleType | None = None
|
||||
def get_compat_bindings() -> CallgrindModuleType:
|
||||
with LOCK:
|
||||
global COMPAT_CALLGRIND_BINDINGS
|
||||
if COMPAT_CALLGRIND_BINDINGS is None:
|
||||
COMPAT_CALLGRIND_BINDINGS = cpp_extension.load(
|
||||
name="callgrind_bindings",
|
||||
sources=[os.path.join(
|
||||
SOURCE_ROOT,
|
||||
"valgrind_wrapper",
|
||||
"compat_bindings.cpp"
|
||||
)],
|
||||
extra_cflags=CXX_FLAGS,
|
||||
extra_include_paths=EXTRA_INCLUDE_PATHS,
|
||||
)
|
||||
return COMPAT_CALLGRIND_BINDINGS
|
||||
|
||||
|
||||
def _compile_template(
|
||||
*,
|
||||
stmt: str,
|
||||
setup: str,
|
||||
global_setup: str,
|
||||
src: str,
|
||||
is_standalone: bool
|
||||
) -> Any:
|
||||
for before, after, indentation in (
|
||||
("// GLOBAL_SETUP_TEMPLATE_LOCATION", global_setup, 0),
|
||||
("// SETUP_TEMPLATE_LOCATION", setup, 4),
|
||||
("// STMT_TEMPLATE_LOCATION", stmt, 8)
|
||||
):
|
||||
# C++ doesn't care about indentation so this code isn't load
|
||||
# bearing the way it is with Python, but this makes the source
|
||||
# look nicer if a human has to look at it.
|
||||
src = re.sub(
|
||||
before,
|
||||
textwrap.indent(after, " " * indentation)[indentation:],
|
||||
src
|
||||
)
|
||||
|
||||
# We want to isolate different Timers. However `cpp_extension` will
|
||||
# cache builds which will significantly reduce the cost of repeated
|
||||
# invocations.
|
||||
with LOCK:
|
||||
name = f"timer_cpp_{abs(hash(src))}"
|
||||
build_dir = os.path.join(_get_build_root(), name)
|
||||
os.makedirs(build_dir, exist_ok=True)
|
||||
|
||||
src_path = os.path.join(build_dir, "timer_src.cpp")
|
||||
with open(src_path, "w") as f:
|
||||
f.write(src)
|
||||
|
||||
# `cpp_extension` has its own locking scheme, so we don't need our lock.
|
||||
return cpp_extension.load(
|
||||
name=name,
|
||||
sources=[src_path],
|
||||
build_directory=build_dir,
|
||||
extra_cflags=CXX_FLAGS,
|
||||
extra_include_paths=EXTRA_INCLUDE_PATHS,
|
||||
is_python_module=not is_standalone,
|
||||
is_standalone=is_standalone,
|
||||
)
|
||||
|
||||
|
||||
def compile_timeit_template(*, stmt: str, setup: str, global_setup: str) -> TimeitModuleType:
|
||||
template_path: str = os.path.join(SOURCE_ROOT, "timeit_template.cpp")
|
||||
with open(template_path) as f:
|
||||
src: str = f.read()
|
||||
|
||||
module = _compile_template(stmt=stmt, setup=setup, global_setup=global_setup, src=src, is_standalone=False)
|
||||
if not isinstance(module, TimeitModuleType):
|
||||
raise AssertionError("compiled module is not a TimeitModuleType")
|
||||
return module
|
||||
|
||||
|
||||
def compile_callgrind_template(*, stmt: str, setup: str, global_setup: str) -> str:
|
||||
template_path: str = os.path.join(SOURCE_ROOT, "valgrind_wrapper", "timer_callgrind_template.cpp")
|
||||
with open(template_path) as f:
|
||||
src: str = f.read()
|
||||
|
||||
target = _compile_template(stmt=stmt, setup=setup, global_setup=global_setup, src=src, is_standalone=True)
|
||||
if not isinstance(target, str):
|
||||
raise AssertionError("compiled target path is not a string")
|
||||
return target
|
||||
@@ -0,0 +1,469 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import functools
|
||||
import itertools as it
|
||||
from typing import Any
|
||||
from collections.abc import Callable
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Fuzzer",
|
||||
"FuzzedParameter", "ParameterAlias",
|
||||
"FuzzedTensor",
|
||||
]
|
||||
|
||||
|
||||
_DISTRIBUTIONS = (
|
||||
"loguniform",
|
||||
"uniform",
|
||||
)
|
||||
|
||||
|
||||
class FuzzedParameter:
|
||||
"""Specification for a parameter to be generated during fuzzing."""
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
minval: int | float | None = None,
|
||||
maxval: int | float | None = None,
|
||||
distribution: str | dict[Any, float] | None = None,
|
||||
strict: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Args:
|
||||
name:
|
||||
A string name with which to identify the parameter.
|
||||
FuzzedTensors can reference this string in their
|
||||
specifications.
|
||||
minval:
|
||||
The lower bound for the generated value. See the description
|
||||
of `distribution` for type behavior.
|
||||
maxval:
|
||||
The upper bound for the generated value. Type behavior is
|
||||
identical to `minval`.
|
||||
distribution:
|
||||
Specifies the distribution from which this parameter should
|
||||
be drawn. There are three possibilities:
|
||||
- "loguniform"
|
||||
Samples between `minval` and `maxval` (inclusive) such
|
||||
that the probabilities are uniform in log space. As a
|
||||
concrete example, if minval=1 and maxval=100, a sample
|
||||
is as likely to fall in [1, 10) as it is [10, 100].
|
||||
- "uniform"
|
||||
Samples are chosen with uniform probability between
|
||||
`minval` and `maxval` (inclusive). If either `minval`
|
||||
or `maxval` is a float then the distribution is the
|
||||
continuous uniform distribution; otherwise samples
|
||||
are constrained to the integers.
|
||||
- dict:
|
||||
If a dict is passed, the keys are taken to be choices
|
||||
for the variables and the values are interpreted as
|
||||
probabilities. (And must sum to one.)
|
||||
If a dict is passed, `minval` and `maxval` must not be set.
|
||||
Otherwise, they must be set.
|
||||
strict:
|
||||
If a parameter is strict, it will not be included in the
|
||||
iterative resampling process which Fuzzer uses to find a
|
||||
valid parameter configuration. This allows an author to
|
||||
prevent skew from resampling for a given parameter (for
|
||||
instance, a low size limit could inadvertently bias towards
|
||||
Tensors with fewer dimensions) at the cost of more iterations
|
||||
when generating parameters.
|
||||
"""
|
||||
self._name = name
|
||||
self._minval = minval
|
||||
self._maxval = maxval
|
||||
self._distribution = self._check_distribution(distribution)
|
||||
self.strict = strict
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
def sample(self, state):
|
||||
if self._distribution == "loguniform":
|
||||
return self._loguniform(state)
|
||||
|
||||
if self._distribution == "uniform":
|
||||
return self._uniform(state)
|
||||
|
||||
if isinstance(self._distribution, dict):
|
||||
return self._custom_distribution(state)
|
||||
|
||||
def _check_distribution(self, distribution):
|
||||
if not isinstance(distribution, dict):
|
||||
if distribution not in _DISTRIBUTIONS:
|
||||
raise AssertionError(f"Unknown distribution: {distribution}")
|
||||
else:
|
||||
if any(i < 0 for i in distribution.values()):
|
||||
raise AssertionError("Probabilities cannot be negative")
|
||||
if not abs(sum(distribution.values()) - 1) > 1e-5:
|
||||
raise AssertionError("Distribution is not normalized")
|
||||
if self._minval is not None:
|
||||
raise AssertionError("When passing a custom distribution, 'minval' must be None")
|
||||
if self._maxval is not None:
|
||||
raise AssertionError("When passing a custom distribution, 'maxval' must be None")
|
||||
|
||||
return distribution
|
||||
|
||||
def _loguniform(self, state):
|
||||
import numpy as np
|
||||
output = int(2 ** state.uniform(
|
||||
low=np.log2(self._minval) if self._minval is not None else None,
|
||||
high=np.log2(self._maxval) if self._maxval is not None else None,
|
||||
))
|
||||
if self._minval is not None and output < self._minval:
|
||||
return self._minval
|
||||
if self._maxval is not None and output > self._maxval:
|
||||
return self._maxval
|
||||
return output
|
||||
|
||||
def _uniform(self, state):
|
||||
if isinstance(self._minval, int) and isinstance(self._maxval, int):
|
||||
return int(state.randint(low=self._minval, high=self._maxval + 1))
|
||||
return state.uniform(low=self._minval, high=self._maxval)
|
||||
|
||||
def _custom_distribution(self, state):
|
||||
import numpy as np
|
||||
# If we directly pass the keys to `choice`, numpy will convert
|
||||
# them to numpy dtypes.
|
||||
index = state.choice(
|
||||
np.arange(len(self._distribution)),
|
||||
p=tuple(self._distribution.values()))
|
||||
return list(self._distribution.keys())[index]
|
||||
|
||||
|
||||
class ParameterAlias:
|
||||
"""Indicates that a parameter should alias the value of another parameter.
|
||||
|
||||
When used in conjunction with a custom distribution, this allows fuzzed
|
||||
tensors to represent a broader range of behaviors. For example, the
|
||||
following sometimes produces Tensors which broadcast:
|
||||
|
||||
Fuzzer(
|
||||
parameters=[
|
||||
FuzzedParameter("x_len", 4, 1024, distribution="uniform"),
|
||||
|
||||
# `y` will either be size one, or match the size of `x`.
|
||||
FuzzedParameter("y_len", distribution={
|
||||
0.5: 1,
|
||||
0.5: ParameterAlias("x_len")
|
||||
}),
|
||||
],
|
||||
tensors=[
|
||||
FuzzedTensor("x", size=("x_len",)),
|
||||
FuzzedTensor("y", size=("y_len",)),
|
||||
],
|
||||
)
|
||||
|
||||
Chains of alias' are allowed, but may not contain cycles.
|
||||
"""
|
||||
def __init__(self, alias_to) -> None:
|
||||
self.alias_to = alias_to
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ParameterAlias[alias_to: {self.alias_to}]"
|
||||
|
||||
|
||||
def dtype_size(dtype):
|
||||
if dtype == torch.bool:
|
||||
return 1
|
||||
if dtype.is_floating_point or dtype.is_complex:
|
||||
return int(torch.finfo(dtype).bits / 8)
|
||||
return int(torch.iinfo(dtype).bits / 8)
|
||||
|
||||
|
||||
def prod(values, base=1):
|
||||
"""np.prod can overflow, so for sizes the product should be done in Python.
|
||||
|
||||
Even though np.prod type promotes to int64, it can still overflow in which
|
||||
case the negative value will pass the size check and OOM when attempting to
|
||||
actually allocate the Tensor.
|
||||
"""
|
||||
return functools.reduce(lambda x, y: int(x) * int(y), values, base)
|
||||
|
||||
|
||||
class FuzzedTensor:
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
size: tuple[str | int, ...],
|
||||
steps: tuple[str | int, ...] | None = None,
|
||||
probability_contiguous: float = 0.5,
|
||||
min_elements: int | None = None,
|
||||
max_elements: int | None = None,
|
||||
max_allocation_bytes: int | None = None,
|
||||
dim_parameter: str | None = None,
|
||||
roll_parameter: str | None = None,
|
||||
dtype=torch.float32,
|
||||
cuda=False,
|
||||
tensor_constructor: Callable | None = None
|
||||
) -> None:
|
||||
"""
|
||||
Args:
|
||||
name:
|
||||
A string identifier for the generated Tensor.
|
||||
size:
|
||||
A tuple of integers or strings specifying the size of the generated
|
||||
Tensor. String values will replaced with a concrete int during the
|
||||
generation process, while ints are simply passed as literals.
|
||||
steps:
|
||||
An optional tuple with the same length as `size`. This indicates
|
||||
that a larger Tensor should be allocated, and then sliced to
|
||||
produce the generated Tensor. For instance, if size is (4, 8)
|
||||
and steps is (1, 4), then a tensor `t` of size (4, 32) will be
|
||||
created and then `t[:, ::4]` will be used. (Allowing one to test
|
||||
Tensors with strided memory.)
|
||||
probability_contiguous:
|
||||
A number between zero and one representing the chance that the
|
||||
generated Tensor has a contiguous memory layout. This is achieved by
|
||||
randomly permuting the shape of a Tensor, calling `.contiguous()`,
|
||||
and then permuting back. This is applied before `steps`, which can
|
||||
also cause a Tensor to be non-contiguous.
|
||||
min_elements:
|
||||
The minimum number of parameters that this Tensor must have for a
|
||||
set of parameters to be valid. (Otherwise they are resampled.)
|
||||
max_elements:
|
||||
Like `min_elements`, but setting an upper bound.
|
||||
max_allocation_bytes:
|
||||
Like `max_elements`, but for the size of Tensor that must be
|
||||
allocated prior to slicing for `steps` (if applicable). For
|
||||
example, a FloatTensor with size (1024, 1024) and steps (4, 4)
|
||||
would have 1M elements, but would require a 64 MB allocation.
|
||||
dim_parameter:
|
||||
The length of `size` and `steps` will be truncated to this value.
|
||||
This allows Tensors of varying dimensions to be generated by the
|
||||
Fuzzer.
|
||||
dtype:
|
||||
The PyTorch dtype of the generated Tensor.
|
||||
cuda:
|
||||
Whether to place the Tensor on a GPU.
|
||||
tensor_constructor:
|
||||
Callable which will be used instead of the default Tensor
|
||||
construction method. This allows the author to enforce properties
|
||||
of the Tensor (e.g. it can only have certain values). The dtype and
|
||||
concrete shape of the Tensor to be created will be passed, and
|
||||
concrete values of all parameters will be passed as kwargs. Note
|
||||
that transformations to the result (permuting, slicing) will be
|
||||
performed by the Fuzzer; the tensor_constructor is only responsible
|
||||
for creating an appropriately sized Tensor.
|
||||
"""
|
||||
self._name = name
|
||||
self._size = size
|
||||
self._steps = steps
|
||||
self._probability_contiguous = probability_contiguous
|
||||
self._min_elements = min_elements
|
||||
self._max_elements = max_elements
|
||||
self._max_allocation_bytes = max_allocation_bytes
|
||||
self._dim_parameter = dim_parameter
|
||||
self._dtype = dtype
|
||||
self._cuda = cuda
|
||||
self._tensor_constructor = tensor_constructor
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@staticmethod
|
||||
def default_tensor_constructor(size, dtype, **kwargs):
|
||||
if dtype.is_floating_point or dtype.is_complex:
|
||||
return torch.rand(size=size, dtype=dtype, device="cpu")
|
||||
else:
|
||||
return torch.randint(1, 127, size=size, dtype=dtype, device="cpu")
|
||||
|
||||
def _make_tensor(self, params, state):
|
||||
import numpy as np
|
||||
size, steps, allocation_size = self._get_size_and_steps(params)
|
||||
constructor = (
|
||||
self._tensor_constructor or
|
||||
self.default_tensor_constructor
|
||||
)
|
||||
|
||||
raw_tensor = constructor(size=allocation_size, dtype=self._dtype, **params)
|
||||
if self._cuda:
|
||||
raw_tensor = raw_tensor.cuda()
|
||||
|
||||
# Randomly permute the Tensor and call `.contiguous()` to force re-ordering
|
||||
# of the memory, and then permute it back to the original shape.
|
||||
dim = len(size)
|
||||
order = np.arange(dim)
|
||||
if state.rand() > self._probability_contiguous:
|
||||
while dim > 1 and np.all(order == np.arange(dim)):
|
||||
order = state.permutation(raw_tensor.dim())
|
||||
|
||||
raw_tensor = raw_tensor.permute(tuple(order)).contiguous()
|
||||
raw_tensor = raw_tensor.permute(tuple(np.argsort(order)))
|
||||
|
||||
slices = [slice(0, size * step, step) for size, step in zip(size, steps, strict=True)]
|
||||
tensor = raw_tensor[tuple(slices)]
|
||||
|
||||
properties = {
|
||||
"numel": int(tensor.numel()),
|
||||
"order": order,
|
||||
"steps": steps,
|
||||
"is_contiguous": tensor.is_contiguous(),
|
||||
"dtype": str(self._dtype),
|
||||
}
|
||||
|
||||
return tensor, properties
|
||||
|
||||
def _get_size_and_steps(self, params):
|
||||
dim = (
|
||||
params[self._dim_parameter]
|
||||
if self._dim_parameter is not None
|
||||
else len(self._size)
|
||||
)
|
||||
|
||||
def resolve(values, dim):
|
||||
"""Resolve values into concrete integers."""
|
||||
values = tuple(params.get(i, i) for i in values)
|
||||
if len(values) > dim:
|
||||
values = values[:dim]
|
||||
if len(values) < dim:
|
||||
values = values + tuple(1 for _ in range(dim - len(values)))
|
||||
return values
|
||||
|
||||
size = resolve(self._size, dim)
|
||||
steps = resolve(self._steps or (), dim)
|
||||
allocation_size = tuple(size_i * step_i for size_i, step_i in zip(size, steps, strict=True))
|
||||
return size, steps, allocation_size
|
||||
|
||||
def satisfies_constraints(self, params) -> bool:
|
||||
size, _, allocation_size = self._get_size_and_steps(params)
|
||||
# Product is computed in Python to avoid integer overflow.
|
||||
num_elements = prod(size)
|
||||
if num_elements < 0:
|
||||
raise AssertionError("Computed number of elements is negative")
|
||||
|
||||
allocation_bytes = prod(allocation_size, base=dtype_size(self._dtype))
|
||||
|
||||
def nullable_greater(left, right):
|
||||
if left is None or right is None:
|
||||
return False
|
||||
return left > right
|
||||
|
||||
return not any((
|
||||
nullable_greater(num_elements, self._max_elements),
|
||||
nullable_greater(self._min_elements, num_elements),
|
||||
nullable_greater(allocation_bytes, self._max_allocation_bytes),
|
||||
))
|
||||
|
||||
|
||||
class Fuzzer:
|
||||
def __init__(
|
||||
self,
|
||||
parameters: list[FuzzedParameter | list[FuzzedParameter]],
|
||||
tensors: list[FuzzedTensor | list[FuzzedTensor]],
|
||||
constraints: list[Callable] | None = None,
|
||||
seed: int | None = None
|
||||
) -> None:
|
||||
"""
|
||||
Args:
|
||||
parameters:
|
||||
List of FuzzedParameters which provide specifications
|
||||
for generated parameters. Iterable elements will be
|
||||
unpacked, though arbitrary nested structures will not.
|
||||
tensors:
|
||||
List of FuzzedTensors which define the Tensors which
|
||||
will be created each step based on the parameters for
|
||||
that step. Iterable elements will be unpacked, though
|
||||
arbitrary nested structures will not.
|
||||
constraints:
|
||||
List of callables. They will be called with params
|
||||
as kwargs, and if any of them return False the current
|
||||
set of parameters will be rejected.
|
||||
seed:
|
||||
Seed for the RandomState used by the Fuzzer. This will
|
||||
also be used to set the PyTorch random seed so that random
|
||||
ops will create reproducible Tensors.
|
||||
"""
|
||||
import numpy as np
|
||||
if seed is None:
|
||||
seed = int(np.random.RandomState().randint(0, 2 ** 32 - 1, dtype=np.int64))
|
||||
self._seed = seed
|
||||
self._parameters = Fuzzer._unpack(parameters, FuzzedParameter)
|
||||
self._tensors = Fuzzer._unpack(tensors, FuzzedTensor)
|
||||
self._constraints = constraints or ()
|
||||
|
||||
p_names = {p.name for p in self._parameters}
|
||||
t_names = {t.name for t in self._tensors}
|
||||
name_overlap = p_names.intersection(t_names)
|
||||
if name_overlap:
|
||||
raise ValueError(f"Duplicate names in parameters and tensors: {name_overlap}")
|
||||
|
||||
self._rejections = 0
|
||||
self._total_generated = 0
|
||||
|
||||
@staticmethod
|
||||
def _unpack(values, cls):
|
||||
return tuple(it.chain.from_iterable(
|
||||
[[i] if isinstance(i, cls) else i for i in values]
|
||||
))
|
||||
|
||||
def take(self, n):
|
||||
import numpy as np
|
||||
state = np.random.RandomState(self._seed)
|
||||
torch.manual_seed(state.randint(low=0, high=2 ** 63, dtype=np.int64))
|
||||
for _ in range(n):
|
||||
params = self._generate(state)
|
||||
tensors = {}
|
||||
tensor_properties = {}
|
||||
for t in self._tensors:
|
||||
tensor, properties = t._make_tensor(params, state)
|
||||
tensors[t.name] = tensor
|
||||
tensor_properties[t.name] = properties
|
||||
yield tensors, tensor_properties, params
|
||||
|
||||
@property
|
||||
def rejection_rate(self):
|
||||
if not self._total_generated:
|
||||
return 0.
|
||||
return self._rejections / self._total_generated
|
||||
|
||||
def _generate(self, state):
|
||||
strict_params: dict[str, float | int | ParameterAlias] = {}
|
||||
for _ in range(1000):
|
||||
candidate_params: dict[str, float | int | ParameterAlias] = {}
|
||||
for p in self._parameters:
|
||||
if p.strict:
|
||||
if p.name in strict_params:
|
||||
candidate_params[p.name] = strict_params[p.name]
|
||||
else:
|
||||
candidate_params[p.name] = p.sample(state)
|
||||
strict_params[p.name] = candidate_params[p.name]
|
||||
else:
|
||||
candidate_params[p.name] = p.sample(state)
|
||||
|
||||
candidate_params = self._resolve_aliases(candidate_params)
|
||||
|
||||
self._total_generated += 1
|
||||
if not all(f(candidate_params) for f in self._constraints):
|
||||
self._rejections += 1
|
||||
continue
|
||||
|
||||
if not all(t.satisfies_constraints(candidate_params) for t in self._tensors):
|
||||
self._rejections += 1
|
||||
continue
|
||||
|
||||
return candidate_params
|
||||
raise ValueError("Failed to generate a set of valid parameters.")
|
||||
|
||||
@staticmethod
|
||||
def _resolve_aliases(params):
|
||||
params = dict(params)
|
||||
alias_count = sum(isinstance(v, ParameterAlias) for v in params.values())
|
||||
|
||||
keys = list(params.keys())
|
||||
while alias_count:
|
||||
for k in keys:
|
||||
v = params[k]
|
||||
if isinstance(v, ParameterAlias):
|
||||
params[k] = params[v.alias_to]
|
||||
alias_count_new = sum(isinstance(v, ParameterAlias) for v in params.values())
|
||||
if alias_count == alias_count_new:
|
||||
raise ValueError(f"ParameterAlias cycle detected\n{params}")
|
||||
|
||||
alias_count = alias_count_new
|
||||
|
||||
return params
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from numbers import Number
|
||||
import torch
|
||||
from torch.utils.benchmark import FuzzedTensor
|
||||
import math
|
||||
|
||||
class FuzzedSparseTensor(FuzzedTensor):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
size: tuple[str | int, ...],
|
||||
min_elements: int | None = None,
|
||||
max_elements: int | None = None,
|
||||
dim_parameter: str | None = None,
|
||||
sparse_dim: str | None = None,
|
||||
nnz: str | None = None,
|
||||
density: str | None = None,
|
||||
coalesced: str | None = None,
|
||||
dtype=torch.float32,
|
||||
cuda=False
|
||||
) -> None:
|
||||
"""
|
||||
Args:
|
||||
name:
|
||||
A string identifier for the generated Tensor.
|
||||
size:
|
||||
A tuple of integers or strings specifying the size of the generated
|
||||
Tensor. String values will replaced with a concrete int during the
|
||||
generation process, while ints are simply passed as literals.
|
||||
min_elements:
|
||||
The minimum number of parameters that this Tensor must have for a
|
||||
set of parameters to be valid. (Otherwise they are resampled.)
|
||||
max_elements:
|
||||
Like `min_elements`, but setting an upper bound.
|
||||
dim_parameter:
|
||||
The length of `size` will be truncated to this value.
|
||||
This allows Tensors of varying dimensions to be generated by the
|
||||
Fuzzer.
|
||||
sparse_dim:
|
||||
The number of sparse dimensions in a sparse tensor.
|
||||
density:
|
||||
This value allows tensors of varying sparsities to be generated by the Fuzzer.
|
||||
coalesced:
|
||||
The sparse tensor format permits uncoalesced sparse tensors,
|
||||
where there may be duplicate coordinates in the indices.
|
||||
dtype:
|
||||
The PyTorch dtype of the generated Tensor.
|
||||
cuda:
|
||||
Whether to place the Tensor on a GPU.
|
||||
"""
|
||||
super().__init__(name=name, size=size, min_elements=min_elements,
|
||||
max_elements=max_elements, dim_parameter=dim_parameter, dtype=dtype, cuda=cuda)
|
||||
self._density = density
|
||||
self._coalesced = coalesced
|
||||
self._sparse_dim = sparse_dim
|
||||
|
||||
@staticmethod
|
||||
def sparse_tensor_constructor(size, dtype, sparse_dim, nnz, is_coalesced):
|
||||
"""sparse_tensor_constructor creates a sparse tensor with coo format.
|
||||
|
||||
Note that when `is_coalesced` is False, the number of elements is doubled but the number of indices
|
||||
represents the same amount of number of non zeros `nnz`, i.e, this is virtually the same tensor
|
||||
with the same sparsity pattern. Moreover, most of the sparse operation will use coalesce() method
|
||||
and what we want here is to get a sparse tensor with the same `nnz` even if this is coalesced or not.
|
||||
|
||||
In the other hand when `is_coalesced` is True the number of elements is reduced in the coalescing process
|
||||
by an unclear amount however the probability to generate duplicates indices are low for most of the cases.
|
||||
This decision was taken on purpose to maintain the construction cost as low as possible.
|
||||
"""
|
||||
if isinstance(size, Number):
|
||||
size = [size] * sparse_dim
|
||||
if all(size[d] <= 0 for d in range(sparse_dim)) and nnz != 0:
|
||||
raise AssertionError('invalid arguments')
|
||||
v_size = [nnz] + list(size[sparse_dim:])
|
||||
if dtype.is_floating_point:
|
||||
v = torch.rand(size=v_size, dtype=dtype, device="cpu")
|
||||
else:
|
||||
v = torch.randint(1, 127, size=v_size, dtype=dtype, device="cpu")
|
||||
|
||||
i = torch.rand(sparse_dim, nnz, device="cpu")
|
||||
i.mul_(torch.tensor(size[:sparse_dim]).unsqueeze(1).to(i))
|
||||
i = i.to(torch.long)
|
||||
|
||||
if not is_coalesced:
|
||||
v = torch.cat([v, torch.randn_like(v)], 0)
|
||||
i = torch.cat([i, i], 1)
|
||||
|
||||
x = torch.sparse_coo_tensor(i, v, torch.Size(size))
|
||||
if is_coalesced:
|
||||
x = x.coalesce()
|
||||
return x
|
||||
|
||||
def _make_tensor(self, params, state):
|
||||
size, _, _ = self._get_size_and_steps(params)
|
||||
density = params['density']
|
||||
nnz = math.ceil(sum(size) * density)
|
||||
if nnz > sum(size):
|
||||
raise AssertionError('nnz cannot exceed total number of elements')
|
||||
|
||||
is_coalesced = params['coalesced']
|
||||
sparse_dim = params['sparse_dim'] if self._sparse_dim else len(size)
|
||||
sparse_dim = min(sparse_dim, len(size))
|
||||
tensor = self.sparse_tensor_constructor(size, self._dtype, sparse_dim, nnz, is_coalesced)
|
||||
|
||||
if self._cuda:
|
||||
tensor = tensor.cuda()
|
||||
sparse_dim = tensor.sparse_dim()
|
||||
dense_dim = tensor.dense_dim()
|
||||
is_hybrid = len(size[sparse_dim:]) > 0
|
||||
|
||||
properties = {
|
||||
"numel": int(tensor.numel()),
|
||||
"shape": tensor.size(),
|
||||
"is_coalesced": tensor.is_coalesced(),
|
||||
"density": density,
|
||||
"sparsity": 1.0 - density,
|
||||
"sparse_dim": sparse_dim,
|
||||
"dense_dim": dense_dim,
|
||||
"is_hybrid": is_hybrid,
|
||||
"dtype": str(self._dtype),
|
||||
}
|
||||
return tensor, properties
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/* C++ template for Timer.timeit
|
||||
|
||||
This template will be consumed by `cpp_jit.py`, and will replace:
|
||||
`GLOBAL_SETUP_TEMPLATE_LOCATION`,
|
||||
`SETUP_TEMPLATE_LOCATION`
|
||||
and
|
||||
`STMT_TEMPLATE_LOCATION`
|
||||
sections with user provided statements.
|
||||
*/
|
||||
#include <chrono>
|
||||
|
||||
#include <c10/util/irange.h>
|
||||
#include <torch/csrc/utils/pybind.h>
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <torch/extension.h>
|
||||
|
||||
// Global setup. (e.g. #includes)
|
||||
// GLOBAL_SETUP_TEMPLATE_LOCATION
|
||||
|
||||
double timeit(int n) {
|
||||
pybind11::gil_scoped_release no_gil;
|
||||
|
||||
// Setup
|
||||
// SETUP_TEMPLATE_LOCATION
|
||||
|
||||
{
|
||||
// Warmup
|
||||
// STMT_TEMPLATE_LOCATION
|
||||
}
|
||||
|
||||
// Main loop
|
||||
auto start_time = std::chrono::high_resolution_clock::now();
|
||||
for (const auto loop_idx : c10::irange(n)) {
|
||||
(void)loop_idx;
|
||||
// STMT_TEMPLATE_LOCATION
|
||||
}
|
||||
auto end_time = std::chrono::high_resolution_clock::now();
|
||||
return std::chrono::duration<double>(end_time - start_time).count();
|
||||
}
|
||||
|
||||
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
|
||||
m.def("timeit", &timeit);
|
||||
}
|
||||
@@ -0,0 +1,533 @@
|
||||
"""Timer class based on the timeit.Timer class, but torch aware."""
|
||||
import enum
|
||||
import timeit
|
||||
import textwrap
|
||||
from typing import overload, Any, NoReturn
|
||||
from collections.abc import Callable
|
||||
|
||||
import torch
|
||||
from torch.utils.benchmark.utils import common, cpp_jit
|
||||
from torch.utils.benchmark.utils._stubs import TimerClass, TimeitModuleType
|
||||
from torch.utils.benchmark.utils.valgrind_wrapper import timer_interface as valgrind_timer_interface
|
||||
|
||||
|
||||
__all__ = ["Timer", "timer", "Language"]
|
||||
|
||||
|
||||
if torch.accelerator.is_available():
|
||||
def timer() -> float:
|
||||
torch.accelerator.synchronize()
|
||||
return timeit.default_timer()
|
||||
else:
|
||||
timer = timeit.default_timer
|
||||
|
||||
|
||||
class Language(enum.Enum):
|
||||
PYTHON = 0
|
||||
CPP = 1
|
||||
|
||||
|
||||
class CPPTimer:
|
||||
def __init__(
|
||||
self,
|
||||
stmt: str,
|
||||
setup: str,
|
||||
global_setup: str,
|
||||
timer: Callable[[], float],
|
||||
globals: dict[str, Any],
|
||||
) -> None:
|
||||
if timer is not timeit.default_timer:
|
||||
raise NotImplementedError(
|
||||
"PyTorch was built with accelerators and an accelerator is present; however "
|
||||
"Timer does not yet support accelerator measurements. If your "
|
||||
"code is CPU only, pass `timer=timeit.default_timer` to the "
|
||||
"Timer's constructor to indicate this. (Note that this will "
|
||||
"produce incorrect results if an accelerator is in fact used, as "
|
||||
"Timer will not synchronize the accelerator.)"
|
||||
)
|
||||
|
||||
if globals:
|
||||
raise ValueError("C++ timing does not support globals.")
|
||||
|
||||
self._stmt: str = textwrap.dedent(stmt)
|
||||
self._setup: str = textwrap.dedent(setup)
|
||||
self._global_setup: str = textwrap.dedent(global_setup)
|
||||
self._timeit_module: TimeitModuleType | None = None
|
||||
|
||||
def timeit(self, number: int) -> float:
|
||||
if self._timeit_module is None:
|
||||
self._timeit_module = cpp_jit.compile_timeit_template(
|
||||
stmt=self._stmt,
|
||||
setup=self._setup,
|
||||
global_setup=self._global_setup,
|
||||
)
|
||||
|
||||
return self._timeit_module.timeit(number)
|
||||
|
||||
|
||||
class Timer:
|
||||
"""Helper class for measuring execution time of PyTorch statements.
|
||||
|
||||
For a full tutorial on how to use this class, see:
|
||||
https://pytorch.org/tutorials/recipes/recipes/benchmark.html
|
||||
|
||||
The PyTorch Timer is based on `timeit.Timer` (and in fact uses
|
||||
`timeit.Timer` internally), but with several key differences:
|
||||
|
||||
1) Runtime aware:
|
||||
Timer will perform warmups (important as some elements of PyTorch are
|
||||
lazily initialized), set threadpool size so that comparisons are
|
||||
apples-to-apples, and synchronize asynchronous accelerator functions when
|
||||
necessary.
|
||||
|
||||
2) Focus on replicates:
|
||||
When measuring code, and particularly complex kernels / models,
|
||||
run-to-run variation is a significant confounding factor. It is
|
||||
expected that all measurements should include replicates to quantify
|
||||
noise and allow median computation, which is more robust than mean.
|
||||
To that effect, this class deviates from the `timeit` API by
|
||||
conceptually merging `timeit.Timer.repeat` and `timeit.Timer.autorange`.
|
||||
(Exact algorithms are discussed in method docstrings.) The `timeit`
|
||||
method is replicated for cases where an adaptive strategy is not
|
||||
desired.
|
||||
|
||||
3) Optional metadata:
|
||||
When defining a Timer, one can optionally specify `label`, `sub_label`,
|
||||
`description`, and `env`. (Defined later) These fields are included in
|
||||
the representation of result object and by the `Compare` class to group
|
||||
and display results for comparison.
|
||||
|
||||
4) Instruction counts
|
||||
In addition to wall times, Timer can run a statement under Callgrind
|
||||
and report instructions executed.
|
||||
|
||||
Directly analogous to `timeit.Timer` constructor arguments:
|
||||
|
||||
`stmt`, `setup`, `timer`, `globals`
|
||||
|
||||
PyTorch Timer specific constructor arguments:
|
||||
|
||||
`label`, `sub_label`, `description`, `env`, `num_threads`
|
||||
|
||||
Args:
|
||||
stmt: Code snippet to be run in a loop and timed.
|
||||
|
||||
setup: Optional setup code. Used to define variables used in `stmt`
|
||||
|
||||
global_setup: (C++ only)
|
||||
Code which is placed at the top level of the file for things like
|
||||
`#include` statements.
|
||||
|
||||
timer:
|
||||
Callable which returns the current time. If PyTorch was built
|
||||
without accelerators or there is no accelerator present, this defaults to
|
||||
`timeit.default_timer`; otherwise it will synchronize accelerators before
|
||||
measuring the time.
|
||||
|
||||
globals:
|
||||
A dict which defines the global variables when `stmt` is being
|
||||
executed. This is the other method for providing variables which
|
||||
`stmt` needs.
|
||||
|
||||
label:
|
||||
String which summarizes `stmt`. For instance, if `stmt` is
|
||||
"torch.nn.functional.relu(torch.add(x, 1, out=out))"
|
||||
one might set label to "ReLU(x + 1)" to improve readability.
|
||||
|
||||
sub_label:
|
||||
Provide supplemental information to disambiguate measurements
|
||||
with identical stmt or label. For instance, in our example
|
||||
above sub_label might be "float" or "int", so that it is easy
|
||||
to differentiate:
|
||||
"ReLU(x + 1): (float)"
|
||||
|
||||
"ReLU(x + 1): (int)"
|
||||
when printing Measurements or summarizing using `Compare`.
|
||||
|
||||
description:
|
||||
String to distinguish measurements with identical label and
|
||||
sub_label. The principal use of `description` is to signal to
|
||||
`Compare` the columns of data. For instance one might set it
|
||||
based on the input size to create a table of the form: ::
|
||||
|
||||
| n=1 | n=4 | ...
|
||||
------------- ...
|
||||
ReLU(x + 1): (float) | ... | ... | ...
|
||||
ReLU(x + 1): (int) | ... | ... | ...
|
||||
|
||||
|
||||
using `Compare`. It is also included when printing a Measurement.
|
||||
|
||||
env:
|
||||
This tag indicates that otherwise identical tasks were run in
|
||||
different environments, and are therefore not equivalent, for
|
||||
instance when A/B testing a change to a kernel. `Compare` will
|
||||
treat Measurements with different `env` specification as distinct
|
||||
when merging replicate runs.
|
||||
|
||||
num_threads:
|
||||
The size of the PyTorch threadpool when executing `stmt`. Single
|
||||
threaded performance is important as both a key inference workload
|
||||
and a good indicator of intrinsic algorithmic efficiency, so the
|
||||
default is set to one. This is in contrast to the default PyTorch
|
||||
threadpool size which tries to utilize all cores.
|
||||
"""
|
||||
|
||||
_timer_cls: type[TimerClass] = timeit.Timer
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
stmt: str = "pass",
|
||||
setup: str = "pass",
|
||||
global_setup: str = "",
|
||||
timer: Callable[[], float] = timer,
|
||||
globals: dict[str, Any] | None = None,
|
||||
label: str | None = None,
|
||||
sub_label: str | None = None,
|
||||
description: str | None = None,
|
||||
env: str | None = None,
|
||||
num_threads: int = 1,
|
||||
language: Language | str = Language.PYTHON,
|
||||
) -> None:
|
||||
if not isinstance(stmt, str):
|
||||
raise ValueError("Currently only a `str` stmt is supported.")
|
||||
|
||||
# We copy `globals` to prevent mutations from leaking.
|
||||
# (For instance, `eval` adds the `__builtins__` key)
|
||||
self._globals = dict(globals or {})
|
||||
|
||||
timer_kwargs = {}
|
||||
if language in (Language.PYTHON, "py", "python"):
|
||||
# Include `torch` if not specified as a convenience feature.
|
||||
self._globals.setdefault("torch", torch)
|
||||
self._language: Language = Language.PYTHON
|
||||
if global_setup:
|
||||
raise ValueError(
|
||||
f"global_setup is C++ only, got `{global_setup}`. Most "
|
||||
"likely this code can simply be moved to `setup`."
|
||||
)
|
||||
|
||||
elif language in (Language.CPP, "cpp", "c++"):
|
||||
if self._timer_cls is not timeit.Timer:
|
||||
raise AssertionError("_timer_cls has already been swapped.")
|
||||
self._timer_cls = CPPTimer
|
||||
setup = ("" if setup == "pass" else setup)
|
||||
self._language = Language.CPP
|
||||
timer_kwargs["global_setup"] = global_setup
|
||||
|
||||
else:
|
||||
raise ValueError(f"Invalid language `{language}`.")
|
||||
|
||||
# Convenience adjustment so that multi-line code snippets defined in
|
||||
# functions do not IndentationError (Python) or look odd (C++). The
|
||||
# leading newline removal is for the initial newline that appears when
|
||||
# defining block strings. For instance:
|
||||
# textwrap.dedent("""
|
||||
# print("This is a stmt")
|
||||
# """)
|
||||
# produces '\nprint("This is a stmt")\n'.
|
||||
#
|
||||
# Stripping this down to 'print("This is a stmt")' doesn't change
|
||||
# what gets executed, but it makes __repr__'s nicer.
|
||||
stmt = textwrap.dedent(stmt)
|
||||
stmt = (stmt[1:] if stmt and stmt[0] == "\n" else stmt).rstrip()
|
||||
setup = textwrap.dedent(setup)
|
||||
setup = (setup[1:] if setup and setup[0] == "\n" else setup).rstrip()
|
||||
|
||||
|
||||
self._timer = self._timer_cls(
|
||||
stmt=stmt,
|
||||
setup=setup,
|
||||
timer=timer,
|
||||
globals=valgrind_timer_interface.CopyIfCallgrind.unwrap_all(self._globals),
|
||||
**timer_kwargs,
|
||||
)
|
||||
self._task_spec = common.TaskSpec(
|
||||
stmt=stmt,
|
||||
setup=setup,
|
||||
global_setup=global_setup,
|
||||
label=label,
|
||||
sub_label=sub_label,
|
||||
description=description,
|
||||
env=env,
|
||||
num_threads=num_threads,
|
||||
)
|
||||
|
||||
def _timeit(self, number: int) -> float:
|
||||
# Even calling a timer in C++ takes ~50 ns, so no real operation should
|
||||
# take less than 1 ns. (And this prevents divide by zero errors.)
|
||||
return max(self._timer.timeit(number), 1e-9)
|
||||
|
||||
def timeit(self, number: int = 1000000) -> common.Measurement:
|
||||
"""Mirrors the semantics of timeit.Timer.timeit().
|
||||
|
||||
Execute the main statement (`stmt`) `number` times.
|
||||
https://docs.python.org/3/library/timeit.html#timeit.Timer.timeit
|
||||
"""
|
||||
with common.set_torch_threads(self._task_spec.num_threads):
|
||||
# Warmup
|
||||
self._timeit(number=max(int(number // 100), 2))
|
||||
|
||||
return common.Measurement(
|
||||
number_per_run=number,
|
||||
raw_times=[self._timeit(number=number)],
|
||||
task_spec=self._task_spec
|
||||
)
|
||||
|
||||
def repeat(self, repeat: int = -1, number: int = -1) -> None:
|
||||
raise NotImplementedError("See `Timer.blocked_autorange.`")
|
||||
|
||||
def autorange(self, callback: Callable[[int, float], NoReturn] | None = None) -> None:
|
||||
raise NotImplementedError("See `Timer.blocked_autorange.`")
|
||||
|
||||
def _threaded_measurement_loop(
|
||||
self,
|
||||
number: int,
|
||||
time_hook: Callable[[], float],
|
||||
stop_hook: Callable[[list[float]], bool],
|
||||
min_run_time: float,
|
||||
max_run_time: float | None = None,
|
||||
callback: Callable[[int, float], NoReturn] | None = None
|
||||
) -> list[float]:
|
||||
total_time = 0.0
|
||||
can_stop = False
|
||||
times: list[float] = []
|
||||
with common.set_torch_threads(self._task_spec.num_threads):
|
||||
while (total_time < min_run_time) or (not can_stop):
|
||||
time_spent = time_hook()
|
||||
times.append(time_spent)
|
||||
total_time += time_spent
|
||||
if callback:
|
||||
callback(number, time_spent)
|
||||
can_stop = stop_hook(times)
|
||||
if max_run_time and total_time > max_run_time:
|
||||
break
|
||||
return times
|
||||
|
||||
def _estimate_block_size(self, min_run_time: float) -> int:
|
||||
with common.set_torch_threads(self._task_spec.num_threads):
|
||||
# Estimate the block size needed for measurement to be negligible
|
||||
# compared to the inner loop. This also serves as a warmup.
|
||||
overhead = torch.tensor([self._timeit(0) for _ in range(5)]).median().item()
|
||||
number = 1
|
||||
while True:
|
||||
time_taken = self._timeit(number)
|
||||
relative_overhead = overhead / time_taken
|
||||
if relative_overhead <= 1e-4 and time_taken >= min_run_time / 1000:
|
||||
break
|
||||
if time_taken > min_run_time:
|
||||
break
|
||||
# Avoid overflow in C++ pybind11 interface
|
||||
if number * 10 > 2147483647:
|
||||
break
|
||||
number *= 10
|
||||
return number
|
||||
|
||||
def blocked_autorange(
|
||||
self,
|
||||
callback: Callable[[int, float], NoReturn] | None = None,
|
||||
min_run_time: float = 0.2,
|
||||
) -> common.Measurement:
|
||||
"""Measure many replicates while keeping timer overhead to a minimum.
|
||||
|
||||
At a high level, blocked_autorange executes the following pseudo-code::
|
||||
|
||||
`setup`
|
||||
|
||||
total_time = 0
|
||||
while total_time < min_run_time
|
||||
start = timer()
|
||||
for _ in range(block_size):
|
||||
`stmt`
|
||||
total_time += (timer() - start)
|
||||
|
||||
Note the variable `block_size` in the inner loop. The choice of block
|
||||
size is important to measurement quality, and must balance two
|
||||
competing objectives:
|
||||
|
||||
1) A small block size results in more replicates and generally
|
||||
better statistics.
|
||||
|
||||
2) A large block size better amortizes the cost of `timer`
|
||||
invocation, and results in a less biased measurement. This is
|
||||
important because accelerator synchronization time is non-trivial
|
||||
(order single to low double digit microseconds) and would
|
||||
otherwise bias the measurement.
|
||||
|
||||
blocked_autorange sets block_size by running a warmup period,
|
||||
increasing block size until timer overhead is less than 0.1% of
|
||||
the overall computation. This value is then used for the main
|
||||
measurement loop.
|
||||
|
||||
Returns:
|
||||
A `Measurement` object that contains measured runtimes and
|
||||
repetition counts, and can be used to compute statistics.
|
||||
(mean, median, etc.)
|
||||
"""
|
||||
number = self._estimate_block_size(min_run_time)
|
||||
|
||||
def time_hook() -> float:
|
||||
return self._timeit(number)
|
||||
|
||||
def stop_hook(times: list[float]) -> bool:
|
||||
return True
|
||||
|
||||
times = self._threaded_measurement_loop(
|
||||
number, time_hook, stop_hook,
|
||||
min_run_time=min_run_time,
|
||||
callback=callback)
|
||||
|
||||
return common.Measurement(
|
||||
number_per_run=number,
|
||||
raw_times=times,
|
||||
task_spec=self._task_spec
|
||||
)
|
||||
|
||||
def adaptive_autorange(
|
||||
self,
|
||||
threshold: float = 0.1,
|
||||
*,
|
||||
min_run_time: float = 0.01,
|
||||
max_run_time: float = 10.0,
|
||||
callback: Callable[[int, float], NoReturn] | None = None,
|
||||
) -> common.Measurement:
|
||||
"""Similar to `blocked_autorange` but also checks for variablility in measurements
|
||||
and repeats until iqr/median is smaller than `threshold` or `max_run_time` is reached.
|
||||
|
||||
|
||||
At a high level, adaptive_autorange executes the following pseudo-code::
|
||||
|
||||
`setup`
|
||||
|
||||
times = []
|
||||
while times.sum < max_run_time
|
||||
start = timer()
|
||||
for _ in range(block_size):
|
||||
`stmt`
|
||||
times.append(timer() - start)
|
||||
|
||||
enough_data = len(times)>3 and times.sum > min_run_time
|
||||
small_iqr=times.iqr/times.mean<threshold
|
||||
|
||||
if enough_data and small_iqr:
|
||||
break
|
||||
|
||||
Args:
|
||||
threshold: value of iqr/median threshold for stopping
|
||||
|
||||
min_run_time: total runtime needed before checking `threshold`
|
||||
|
||||
max_run_time: total runtime for all measurements regardless of `threshold`
|
||||
|
||||
Returns:
|
||||
A `Measurement` object that contains measured runtimes and
|
||||
repetition counts, and can be used to compute statistics.
|
||||
(mean, median, etc.)
|
||||
"""
|
||||
number = self._estimate_block_size(min_run_time=0.05)
|
||||
|
||||
def time_hook() -> float:
|
||||
return self._timeit(number)
|
||||
|
||||
def stop_hook(times: list[float]) -> bool:
|
||||
if len(times) > 3:
|
||||
return common.Measurement(
|
||||
number_per_run=number,
|
||||
raw_times=times,
|
||||
task_spec=self._task_spec
|
||||
).meets_confidence(threshold=threshold)
|
||||
return False
|
||||
times = self._threaded_measurement_loop(
|
||||
number, time_hook, stop_hook, min_run_time, max_run_time, callback=callback)
|
||||
|
||||
return common.Measurement(
|
||||
number_per_run=number,
|
||||
raw_times=times,
|
||||
task_spec=self._task_spec
|
||||
)
|
||||
|
||||
@overload
|
||||
def collect_callgrind(
|
||||
self,
|
||||
number: int,
|
||||
*,
|
||||
repeats: None,
|
||||
collect_baseline: bool,
|
||||
retain_out_file: bool,
|
||||
) -> valgrind_timer_interface.CallgrindStats:
|
||||
...
|
||||
|
||||
@overload
|
||||
def collect_callgrind(
|
||||
self,
|
||||
number: int,
|
||||
*,
|
||||
repeats: int,
|
||||
collect_baseline: bool,
|
||||
retain_out_file: bool,
|
||||
) -> tuple[valgrind_timer_interface.CallgrindStats, ...]:
|
||||
...
|
||||
|
||||
def collect_callgrind(
|
||||
self,
|
||||
number: int = 100,
|
||||
*,
|
||||
repeats: int | None = None,
|
||||
collect_baseline: bool = True,
|
||||
retain_out_file: bool = False,
|
||||
) -> Any:
|
||||
"""Collect instruction counts using Callgrind.
|
||||
|
||||
Unlike wall times, instruction counts are deterministic
|
||||
(modulo non-determinism in the program itself and small amounts of
|
||||
jitter from the Python interpreter.) This makes them ideal for detailed
|
||||
performance analysis. This method runs `stmt` in a separate process
|
||||
so that Valgrind can instrument the program. Performance is severely
|
||||
degraded due to the instrumentation, however this is ameliorated by
|
||||
the fact that a small number of iterations is generally sufficient to
|
||||
obtain good measurements.
|
||||
|
||||
In order to use this method `valgrind`, `callgrind_control`, and
|
||||
`callgrind_annotate` must be installed.
|
||||
|
||||
Because there is a process boundary between the caller (this process)
|
||||
and the `stmt` execution, `globals` cannot contain arbitrary in-memory
|
||||
data structures. (Unlike timing methods) Instead, globals are
|
||||
restricted to builtins, `nn.Modules`'s, and TorchScripted functions/modules
|
||||
to reduce the surprise factor from serialization and subsequent
|
||||
deserialization. The `GlobalsBridge` class provides more detail on this
|
||||
subject. Take particular care with nn.Modules: they rely on pickle and
|
||||
you may need to add an import to `setup` for them to transfer properly.
|
||||
|
||||
By default, a profile for an empty statement will be collected and
|
||||
cached to indicate how many instructions are from the Python loop which
|
||||
drives `stmt`.
|
||||
|
||||
Returns:
|
||||
A `CallgrindStats` object which provides instruction counts and
|
||||
some basic facilities for analyzing and manipulating results.
|
||||
"""
|
||||
if not isinstance(self._task_spec.stmt, str):
|
||||
raise ValueError("`collect_callgrind` currently only supports string `stmt`")
|
||||
|
||||
if repeats is not None and repeats < 1:
|
||||
raise ValueError("If specified, `repeats` must be >= 1")
|
||||
|
||||
# Check that the statement is valid. It doesn't guarantee success, but it's much
|
||||
# simpler and quicker to raise an exception for a faulty `stmt` or `setup` in
|
||||
# the parent process rather than the valgrind subprocess.
|
||||
self._timeit(1)
|
||||
is_python = (self._language == Language.PYTHON)
|
||||
if not is_python and self._globals:
|
||||
raise AssertionError("_timer globals are only supported for Python timers")
|
||||
result = valgrind_timer_interface.wrapper_singleton().collect_callgrind(
|
||||
task_spec=self._task_spec,
|
||||
globals=self._globals,
|
||||
number=number,
|
||||
repeats=repeats or 1,
|
||||
collect_baseline=collect_baseline and is_python,
|
||||
is_python=is_python,
|
||||
retain_out_file=retain_out_file,
|
||||
)
|
||||
|
||||
return (result[0] if repeats is None else result)
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
|
||||
/*
|
||||
----------------------------------------------------------------
|
||||
|
||||
Notice that the following BSD-style license applies to this one
|
||||
file (callgrind.h) only. The rest of Valgrind is licensed under the
|
||||
terms of the GNU General Public License, version 2, unless
|
||||
otherwise indicated. See the COPYING file in the source
|
||||
distribution for details.
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
This file is part of callgrind, a valgrind tool for cache simulation
|
||||
and call tree tracing.
|
||||
|
||||
Copyright (C) 2003-2017 Josef Weidendorfer. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. The origin of this software must not be misrepresented; you must
|
||||
not claim that you wrote the original software. If you use this
|
||||
software in a product, an acknowledgment in the product
|
||||
documentation would be appreciated but is not required.
|
||||
|
||||
3. Altered source versions must be plainly marked as such, and must
|
||||
not be misrepresented as being the original software.
|
||||
|
||||
4. The name of the author may not be used to endorse or promote
|
||||
products derived from this software without specific prior written
|
||||
permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
|
||||
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
Notice that the above BSD-style license applies to this one file
|
||||
(callgrind.h) only. The entire rest of Valgrind is licensed under
|
||||
the terms of the GNU General Public License, version 2. See the
|
||||
COPYING file in the source distribution for details.
|
||||
|
||||
----------------------------------------------------------------
|
||||
*/
|
||||
|
||||
#ifndef __CALLGRIND_H
|
||||
#define __CALLGRIND_H
|
||||
|
||||
#include "valgrind.h"
|
||||
|
||||
/* !! ABIWARNING !! ABIWARNING !! ABIWARNING !! ABIWARNING !!
|
||||
This enum comprises an ABI exported by Valgrind to programs
|
||||
which use client requests. DO NOT CHANGE THE ORDER OF THESE
|
||||
ENTRIES, NOR DELETE ANY -- add new ones at the end.
|
||||
|
||||
The identification ('C','T') for Callgrind has historical
|
||||
reasons: it was called "Calltree" before. Besides, ('C','G') would
|
||||
clash with cachegrind.
|
||||
*/
|
||||
|
||||
typedef
|
||||
enum {
|
||||
VG_USERREQ__DUMP_STATS = VG_USERREQ_TOOL_BASE('C','T'),
|
||||
VG_USERREQ__ZERO_STATS,
|
||||
VG_USERREQ__TOGGLE_COLLECT,
|
||||
VG_USERREQ__DUMP_STATS_AT,
|
||||
VG_USERREQ__START_INSTRUMENTATION,
|
||||
VG_USERREQ__STOP_INSTRUMENTATION
|
||||
} Vg_CallgrindClientRequest;
|
||||
|
||||
/* Dump current state of cost centers, and zero them afterwards */
|
||||
#define CALLGRIND_DUMP_STATS \
|
||||
VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__DUMP_STATS, \
|
||||
0, 0, 0, 0, 0)
|
||||
|
||||
/* Dump current state of cost centers, and zero them afterwards.
|
||||
The argument is appended to a string stating the reason which triggered
|
||||
the dump. This string is written as a description field into the
|
||||
profile data dump. */
|
||||
#define CALLGRIND_DUMP_STATS_AT(pos_str) \
|
||||
VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__DUMP_STATS_AT, \
|
||||
pos_str, 0, 0, 0, 0)
|
||||
|
||||
/* Zero cost centers */
|
||||
#define CALLGRIND_ZERO_STATS \
|
||||
VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__ZERO_STATS, \
|
||||
0, 0, 0, 0, 0)
|
||||
|
||||
/* Toggles collection state.
|
||||
The collection state specifies whether the happening of events
|
||||
should be noted or if they are to be ignored. Events are noted
|
||||
by increment of counters in a cost center */
|
||||
#define CALLGRIND_TOGGLE_COLLECT \
|
||||
VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__TOGGLE_COLLECT, \
|
||||
0, 0, 0, 0, 0)
|
||||
|
||||
/* Start full callgrind instrumentation if not already switched on.
|
||||
When cache simulation is done, it will flush the simulated cache;
|
||||
this will lead to an artificial cache warmup phase afterwards with
|
||||
cache misses which would not have happened in reality. */
|
||||
#define CALLGRIND_START_INSTRUMENTATION \
|
||||
VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__START_INSTRUMENTATION, \
|
||||
0, 0, 0, 0, 0)
|
||||
|
||||
/* Stop full callgrind instrumentation if not already switched off.
|
||||
This flushes Valgrinds translation cache, and does no additional
|
||||
instrumentation afterwards, which effectivly will run at the same
|
||||
speed as the "none" tool (ie. at minimal slowdown).
|
||||
Use this to bypass Callgrind aggregation for uninteresting code parts.
|
||||
To start Callgrind in this mode to ignore the setup phase, use
|
||||
the option "--instr-atstart=no". */
|
||||
#define CALLGRIND_STOP_INSTRUMENTATION \
|
||||
VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__STOP_INSTRUMENTATION, \
|
||||
0, 0, 0, 0, 0)
|
||||
|
||||
#endif /* __CALLGRIND_H */
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/* Used to collect profiles of old versions of PyTorch. */
|
||||
#include <callgrind.h>
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
bool _valgrind_supported_platform() {
|
||||
#if defined(NVALGRIND)
|
||||
return false;
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
void _valgrind_toggle() {
|
||||
#if defined(NVALGRIND)
|
||||
TORCH_CHECK(false, "Valgrind is not supported.");
|
||||
#else
|
||||
CALLGRIND_TOGGLE_COLLECT;
|
||||
#endif
|
||||
}
|
||||
|
||||
void _valgrind_toggle_and_dump_stats() {
|
||||
#if defined(NVALGRIND)
|
||||
TORCH_CHECK(false, "Valgrind is not supported.");
|
||||
#else
|
||||
// NB: See note in Module.cpp
|
||||
CALLGRIND_TOGGLE_COLLECT;
|
||||
CALLGRIND_DUMP_STATS;
|
||||
#endif
|
||||
}
|
||||
|
||||
PYBIND11_MODULE(callgrind_bindings, m) {
|
||||
m.def("_valgrind_supported_platform", &_valgrind_supported_platform);
|
||||
m.def("_valgrind_toggle", &_valgrind_toggle);
|
||||
m.def("_valgrind_toggle_and_dump_stats", &_valgrind_dump_stats);
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/* C++ template for Timer.collect_callgrind
|
||||
|
||||
This template will be consumed by `cpp_jit.py`, and will replace:
|
||||
`GLOBAL_SETUP_TEMPLATE_LOCATION`,
|
||||
`SETUP_TEMPLATE_LOCATION`
|
||||
and
|
||||
`STMT_TEMPLATE_LOCATION`
|
||||
sections with user provided statements.
|
||||
*/
|
||||
|
||||
#include <c10/util/irange.h>
|
||||
#include <callgrind.h>
|
||||
#include <torch/torch.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
// Global setup. (e.g. #includes)
|
||||
// GLOBAL_SETUP_TEMPLATE_LOCATION
|
||||
|
||||
#if defined(NVALGRIND)
|
||||
static_assert(false);
|
||||
#endif
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
// This file should only be called inside of `Timer`, so we can adopt a
|
||||
// very simple and rigid argument parsing scheme.
|
||||
TORCH_CHECK(argc == 9);
|
||||
TORCH_CHECK(std::string(argv[1]) == "--number");
|
||||
auto number = std::stoi(argv[2]);
|
||||
|
||||
TORCH_CHECK(
|
||||
std::string(argv[3]) == "--number-warmup" ||
|
||||
std::string(argv[3]) == "--number_warmup");
|
||||
auto number_warmup = std::stoi(argv[4]);
|
||||
|
||||
TORCH_CHECK(std::string(argv[5]) == "--repeats");
|
||||
auto repeats = std::stoi(argv[6]);
|
||||
|
||||
TORCH_CHECK(
|
||||
std::string(argv[7]) == "--number-threads" ||
|
||||
std::string(argv[7]) == "--number_threads");
|
||||
auto number_threads = std::stoi(argv[8]);
|
||||
torch::set_num_threads(number_threads);
|
||||
|
||||
// Setup
|
||||
// SETUP_TEMPLATE_LOCATION
|
||||
|
||||
// Warmup
|
||||
for (const auto i : c10::irange(number_warmup)) {
|
||||
(void)i;
|
||||
// STMT_TEMPLATE_LOCATION
|
||||
}
|
||||
|
||||
// Main loop
|
||||
for (const auto repeat : c10::irange(repeats)) {
|
||||
(void)repeat;
|
||||
CALLGRIND_TOGGLE_COLLECT;
|
||||
|
||||
for (const auto i : c10::irange(number)) {
|
||||
(void)i;
|
||||
// STMT_TEMPLATE_LOCATION
|
||||
}
|
||||
|
||||
// NB: See note in Module.cpp
|
||||
CALLGRIND_TOGGLE_COLLECT;
|
||||
CALLGRIND_DUMP_STATS;
|
||||
}
|
||||
}
|
||||
+919
@@ -0,0 +1,919 @@
|
||||
"""Intermediate layer between `Timer` and `valgrind`."""
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import enum
|
||||
import dataclasses
|
||||
import itertools as it
|
||||
import os
|
||||
import pickle
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
from typing import (
|
||||
cast, Any, NamedTuple,
|
||||
TYPE_CHECKING)
|
||||
|
||||
import torch
|
||||
from torch.utils.benchmark.utils import common, cpp_jit
|
||||
import operator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Iterator
|
||||
|
||||
from torch.utils.benchmark.utils._stubs import CallgrindModuleType
|
||||
|
||||
|
||||
__all__ = ["FunctionCount", "FunctionCounts", "CallgrindStats", "CopyIfCallgrind"]
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
CompletedProcessType = subprocess.CompletedProcess[str]
|
||||
else:
|
||||
CompletedProcessType = subprocess.CompletedProcess
|
||||
|
||||
|
||||
class FunctionCount(NamedTuple):
|
||||
# TODO(#105471): Rename the count field
|
||||
count: int # type: ignore[assignment]
|
||||
function: str
|
||||
|
||||
|
||||
@dataclasses.dataclass(repr=False, eq=False, frozen=True)
|
||||
class FunctionCounts:
|
||||
"""Container for manipulating Callgrind results.
|
||||
|
||||
It supports:
|
||||
1) Addition and subtraction to combine or diff results.
|
||||
2) Tuple-like indexing.
|
||||
3) A `denoise` function which strips CPython calls which are known to
|
||||
be non-deterministic and quite noisy.
|
||||
4) Two higher order methods (`filter` and `transform`) for custom
|
||||
manipulation.
|
||||
"""
|
||||
_data: tuple[FunctionCount, ...]
|
||||
inclusive: bool
|
||||
truncate_rows: bool = True
|
||||
|
||||
# For normal use, torch._tensor_str.PRINT_OPTS.linewidth determines
|
||||
# the print settings. This is simply to allow hermetic unit tests.
|
||||
_linewidth: int | None = None
|
||||
|
||||
def __iter__(self) -> Iterator[FunctionCount]:
|
||||
yield from self._data
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._data)
|
||||
|
||||
def __getitem__(self, item: Any) -> FunctionCount | FunctionCounts:
|
||||
data: FunctionCount | tuple[FunctionCount, ...] = self._data[item]
|
||||
return (
|
||||
FunctionCounts(cast(tuple[FunctionCount, ...], data), self.inclusive, truncate_rows=False)
|
||||
if isinstance(data, tuple) else data
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
count_len = 0
|
||||
for c, _ in self:
|
||||
# Account for sign in string length.
|
||||
count_len = max(count_len, len(str(c)) + int(c < 0))
|
||||
|
||||
lines = []
|
||||
linewidth = self._linewidth or torch._tensor_str.PRINT_OPTS.linewidth
|
||||
fn_str_len = max(linewidth - count_len - 4, 40)
|
||||
for c, fn in self:
|
||||
if len(fn) > fn_str_len:
|
||||
left_len = int((fn_str_len - 5) // 2)
|
||||
fn = fn[:left_len] + " ... " + fn[-(fn_str_len - left_len - 5):]
|
||||
lines.append(f" {c:>{count_len}} {fn}")
|
||||
|
||||
if self.truncate_rows and len(lines) > 18:
|
||||
lines = lines[:9] + ["...".rjust(count_len + 2)] + lines[-9:]
|
||||
|
||||
if not self.inclusive:
|
||||
lines.extend(["", f"Total: {self.sum()}"])
|
||||
|
||||
return "\n".join([super().__repr__()] + lines)
|
||||
|
||||
def __add__(
|
||||
self,
|
||||
other: FunctionCounts,
|
||||
) -> FunctionCounts:
|
||||
return self._merge(other, lambda c: c)
|
||||
|
||||
def __sub__(
|
||||
self,
|
||||
other: FunctionCounts,
|
||||
) -> FunctionCounts:
|
||||
return self._merge(other, operator.neg)
|
||||
|
||||
def __mul__(self, other: int | float) -> FunctionCounts:
|
||||
return self._from_dict({
|
||||
fn: int(c * other) for c, fn in self._data
|
||||
}, self.inclusive)
|
||||
|
||||
def transform(self, map_fn: Callable[[str], str]) -> FunctionCounts:
|
||||
"""Apply `map_fn` to all of the function names.
|
||||
|
||||
This can be used to regularize function names (e.g. stripping irrelevant
|
||||
parts of the file path), coalesce entries by mapping multiple functions
|
||||
to the same name (in which case the counts are added together), etc.
|
||||
"""
|
||||
counts: collections.defaultdict[str, int] = collections.defaultdict(int)
|
||||
for c, fn in self._data:
|
||||
counts[map_fn(fn)] += c
|
||||
|
||||
return self._from_dict(counts, self.inclusive)
|
||||
|
||||
def filter(self, filter_fn: Callable[[str], bool]) -> FunctionCounts:
|
||||
"""Keep only the elements where `filter_fn` applied to function name returns True."""
|
||||
return FunctionCounts(tuple(i for i in self if filter_fn(i.function)), self.inclusive)
|
||||
|
||||
def sum(self) -> int:
|
||||
return sum(c for c, _ in self)
|
||||
|
||||
def denoise(self) -> FunctionCounts:
|
||||
"""Remove known noisy instructions.
|
||||
|
||||
Several instructions in the CPython interpreter are rather noisy. These
|
||||
instructions involve unicode to dictionary lookups which Python uses to
|
||||
map variable names. FunctionCounts is generally a content agnostic
|
||||
container, however this is sufficiently important for obtaining
|
||||
reliable results to warrant an exception."""
|
||||
return self.filter(lambda fn: "dictobject.c:lookdict_unicode" not in fn)
|
||||
|
||||
def _merge(
|
||||
self,
|
||||
second: FunctionCounts,
|
||||
merge_fn: Callable[[int], int]
|
||||
) -> FunctionCounts:
|
||||
if self.inclusive != second.inclusive:
|
||||
raise AssertionError("Cannot merge inclusive and exclusive counts.")
|
||||
counts: collections.defaultdict[str, int] = collections.defaultdict(int)
|
||||
for c, fn in self:
|
||||
counts[fn] += c
|
||||
|
||||
for c, fn in second:
|
||||
counts[fn] += merge_fn(c)
|
||||
|
||||
return self._from_dict(counts, self.inclusive)
|
||||
|
||||
@staticmethod
|
||||
def _from_dict(counts: dict[str, int], inclusive: bool) -> FunctionCounts:
|
||||
flat_counts = (FunctionCount(c, fn) for fn, c in counts.items() if c)
|
||||
return FunctionCounts(tuple(sorted(flat_counts, reverse=True)), inclusive)
|
||||
|
||||
|
||||
@dataclasses.dataclass(repr=False, eq=False, frozen=True)
|
||||
class CallgrindStats:
|
||||
"""Top level container for Callgrind results collected by Timer.
|
||||
|
||||
Manipulation is generally done using the FunctionCounts class, which is
|
||||
obtained by calling `CallgrindStats.stats(...)`. Several convenience
|
||||
methods are provided as well; the most significant is
|
||||
`CallgrindStats.as_standardized()`.
|
||||
"""
|
||||
task_spec: common.TaskSpec
|
||||
number_per_run: int
|
||||
built_with_debug_symbols: bool
|
||||
baseline_inclusive_stats: FunctionCounts
|
||||
baseline_exclusive_stats: FunctionCounts
|
||||
stmt_inclusive_stats: FunctionCounts
|
||||
stmt_exclusive_stats: FunctionCounts
|
||||
stmt_callgrind_out: str | None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
base_stats = self.baseline_exclusive_stats
|
||||
output = f"""
|
||||
{super().__repr__()}
|
||||
{self.task_spec.summarize()}
|
||||
{'':>25}All{'':>10}Noisy symbols removed
|
||||
Instructions: {self.counts(denoise=False):>12}{'':>15}{self.counts(denoise=True):>12}
|
||||
Baseline: {base_stats.sum():>12}{'':>15}{base_stats.denoise().sum():>12}
|
||||
{self.number_per_run} runs per measurement, {self.task_spec.num_threads} thread{'s' if self.task_spec.num_threads > 1 else ''}
|
||||
""".strip()
|
||||
if not self.built_with_debug_symbols:
|
||||
output += textwrap.dedent("""
|
||||
Warning: PyTorch was not built with debug symbols.
|
||||
Source information may be limited. Rebuild with
|
||||
REL_WITH_DEB_INFO=1 for more detailed results.""")
|
||||
return output
|
||||
|
||||
def stats(self, inclusive: bool = False) -> FunctionCounts:
|
||||
"""Returns detailed function counts.
|
||||
|
||||
Conceptually, the FunctionCounts returned can be thought of as a tuple
|
||||
of (count, path_and_function_name) tuples.
|
||||
|
||||
`inclusive` matches the semantics of callgrind. If True, the counts
|
||||
include instructions executed by children. `inclusive=True` is useful
|
||||
for identifying hot spots in code; `inclusive=False` is useful for
|
||||
reducing noise when diffing counts from two different runs. (See
|
||||
CallgrindStats.delta(...) for more details)
|
||||
"""
|
||||
return self.stmt_inclusive_stats if inclusive else self.stmt_exclusive_stats
|
||||
|
||||
def counts(self, *, denoise: bool = False) -> int:
|
||||
"""Returns the total number of instructions executed.
|
||||
|
||||
See `FunctionCounts.denoise()` for an explanation of the `denoise` arg.
|
||||
"""
|
||||
stats = self.stmt_exclusive_stats
|
||||
return (stats.denoise() if denoise else stats).sum()
|
||||
|
||||
# FIXME: Once 3.7 is the minimum version, type annotate `other` per PEP 563
|
||||
def delta(
|
||||
self,
|
||||
other: CallgrindStats,
|
||||
inclusive: bool = False,
|
||||
) -> FunctionCounts:
|
||||
"""Diff two sets of counts.
|
||||
|
||||
One common reason to collect instruction counts is to determine the
|
||||
the effect that a particular change will have on the number of instructions
|
||||
needed to perform some unit of work. If a change increases that number, the
|
||||
next logical question is "why". This generally involves looking at what part
|
||||
if the code increased in instruction count. This function automates that
|
||||
process so that one can easily diff counts on both an inclusive and
|
||||
exclusive basis.
|
||||
"""
|
||||
return self.stats(inclusive=inclusive) - other.stats(inclusive=inclusive)
|
||||
|
||||
def as_standardized(self) -> CallgrindStats:
|
||||
"""Strip library names and some prefixes from function strings.
|
||||
|
||||
When comparing two different sets of instruction counts, on stumbling
|
||||
block can be path prefixes. Callgrind includes the full filepath
|
||||
when reporting a function (as it should). However, this can cause
|
||||
issues when diffing profiles. If a key component such as Python
|
||||
or PyTorch was built in separate locations in the two profiles, which
|
||||
can result in something resembling::
|
||||
|
||||
23234231 /tmp/first_build_dir/thing.c:foo(...)
|
||||
9823794 /tmp/first_build_dir/thing.c:bar(...)
|
||||
...
|
||||
53453 .../aten/src/Aten/...:function_that_actually_changed(...)
|
||||
...
|
||||
-9823794 /tmp/second_build_dir/thing.c:bar(...)
|
||||
-23234231 /tmp/second_build_dir/thing.c:foo(...)
|
||||
|
||||
Stripping prefixes can ameliorate this issue by regularizing the
|
||||
strings and causing better cancellation of equivalent call sites
|
||||
when diffing.
|
||||
"""
|
||||
def strip(stats: FunctionCounts) -> FunctionCounts:
|
||||
transforms = (
|
||||
# PyTorch may have been built in different locations.
|
||||
(r"^.+build/\.\./", "build/../"),
|
||||
(r"^.+/" + re.escape("build/aten/"), "build/aten/"),
|
||||
|
||||
# "Python" and "Objects" come from CPython.
|
||||
(r"^.+/" + re.escape("Python/"), "Python/"),
|
||||
(r"^.+/" + re.escape("Objects/"), "Objects/"),
|
||||
|
||||
# Strip library name. e.g. `libtorch.so`
|
||||
(r"\s\[.+\]$", ""),
|
||||
)
|
||||
|
||||
for before, after in transforms:
|
||||
stats = stats.transform(lambda fn: re.sub(before, after, fn))
|
||||
|
||||
return stats
|
||||
|
||||
return CallgrindStats(
|
||||
task_spec=self.task_spec,
|
||||
number_per_run=self.number_per_run,
|
||||
built_with_debug_symbols=self.built_with_debug_symbols,
|
||||
baseline_inclusive_stats=strip(self.baseline_inclusive_stats),
|
||||
baseline_exclusive_stats=strip(self.baseline_exclusive_stats),
|
||||
stmt_inclusive_stats=strip(self.stmt_inclusive_stats),
|
||||
stmt_exclusive_stats=strip(self.stmt_exclusive_stats),
|
||||
|
||||
# `as_standardized` will change symbol names, so the contents will
|
||||
# no longer map directly to `callgrind.out`
|
||||
stmt_callgrind_out=None,
|
||||
)
|
||||
|
||||
|
||||
class Serialization(enum.Enum):
|
||||
PICKLE = 0
|
||||
TORCH = 1
|
||||
TORCH_JIT = 2
|
||||
|
||||
|
||||
_GLOBALS_ALLOWED_TYPES: dict[Serialization, tuple[Any, ...]] = {
|
||||
Serialization.PICKLE: (str, bytes, bool, int, float, complex),
|
||||
Serialization.TORCH_JIT: (torch.jit.ScriptFunction, torch.jit.ScriptModule),
|
||||
Serialization.TORCH: (torch.nn.Module,),
|
||||
}
|
||||
|
||||
|
||||
class CopyIfCallgrind:
|
||||
"""Signal that a global may be replaced with a deserialized copy.
|
||||
|
||||
See `GlobalsBridge` for why this matters.
|
||||
"""
|
||||
def __init__(self, value: Any, *, setup: str | None = None) -> None:
|
||||
for method, supported_types in _GLOBALS_ALLOWED_TYPES.items():
|
||||
if any(isinstance(value, t) for t in supported_types):
|
||||
self._value: Any = value
|
||||
self._setup: str | None = setup
|
||||
self._serialization: Serialization = method
|
||||
break
|
||||
else:
|
||||
supported_str = "\n".join([
|
||||
getattr(t, "__name__", repr(t))
|
||||
for t in it.chain(_GLOBALS_ALLOWED_TYPES.values())])
|
||||
|
||||
raise ValueError(
|
||||
f"Unsupported type: {type(value)}\n"
|
||||
f"`collect_callgrind` restricts globals to the following types:\n"
|
||||
f"{textwrap.indent(supported_str, ' ')}"
|
||||
)
|
||||
|
||||
@property
|
||||
def value(self) -> Any:
|
||||
return self._value
|
||||
|
||||
@property
|
||||
def setup(self) -> str | None:
|
||||
return self._setup
|
||||
|
||||
@property
|
||||
def serialization(self) -> Serialization:
|
||||
return self._serialization
|
||||
|
||||
@staticmethod
|
||||
def unwrap_all(globals: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
k: (v.value if isinstance(v, CopyIfCallgrind) else v)
|
||||
for k, v in globals.items()
|
||||
}
|
||||
|
||||
|
||||
class GlobalsBridge:
|
||||
"""Handle the transfer of (certain) globals when collecting Callgrind statistics.
|
||||
|
||||
Key takeaway: Any globals passed must be wrapped in `CopyIfCallgrind` to
|
||||
work with `Timer.collect_callgrind`.
|
||||
|
||||
Consider the following code snippet:
|
||||
```
|
||||
import pickle
|
||||
import timeit
|
||||
|
||||
class Counter:
|
||||
value = 0
|
||||
|
||||
def __call__(self):
|
||||
self.value += 1
|
||||
|
||||
counter = Counter()
|
||||
timeit.Timer("counter()", globals={"counter": counter}).timeit(10)
|
||||
print(counter.value) # 10
|
||||
|
||||
timeit.Timer(
|
||||
"counter()",
|
||||
globals={"counter": pickle.loads(pickle.dumps(counter))}
|
||||
).timeit(20)
|
||||
print(counter.value) # Still 10
|
||||
```
|
||||
|
||||
In the first case, `stmt` is executed using the objects in `globals`;
|
||||
however, the addition of serialization and deserialization changes the
|
||||
semantics and may meaningfully change behavior.
|
||||
|
||||
This is a practical consideration when collecting Callgrind statistics.
|
||||
Unlike `exec` based execution (which `timeit` uses under the hood) which
|
||||
can share in-memory data structures with the caller, Callgrind collection
|
||||
requires an entirely new process in order to run under Valgrind. This means
|
||||
that any data structures used for statement execution will have to be
|
||||
serialized and deserialized in the subprocess.
|
||||
|
||||
In order to avoid surprising semantics from (user invisible) process
|
||||
boundaries, what can be passed through `globals` is severely restricted
|
||||
for `Timer.collect_callgrind`. It is expected that most setup should be
|
||||
achievable (albeit perhaps less ergonomically) by passing a `setup`
|
||||
string.
|
||||
|
||||
There are, however, exceptions. One such class are TorchScripted functions.
|
||||
Because they require a concrete file with source code it is not possible
|
||||
to define them using a `setup` string. Another group are torch.nn.Modules,
|
||||
whose construction can be complex and prohibitively cumbersome to coerce
|
||||
into a `setup` string. Finally, most builtin types are sufficiently well
|
||||
behaved and sufficiently common to warrant allowing as well. (e.g.
|
||||
`globals={"n": 1}` is very convenient.)
|
||||
|
||||
Fortunately, all have well defined serialization semantics. This class
|
||||
is responsible for enabling the Valgrind subprocess to use elements in
|
||||
`globals` so long as they are an allowed type.
|
||||
|
||||
Caveats:
|
||||
The user is required to acknowledge this serialization by wrapping
|
||||
elements in `globals` with `CopyIfCallgrind`.
|
||||
|
||||
While ScriptFunction and ScriptModule are expected to save and load
|
||||
quite robustly, it is up to the user to ensure that an nn.Module can
|
||||
un-pickle successfully.
|
||||
|
||||
`torch.Tensor` and `np.ndarray` are deliberately excluded. The
|
||||
serialization/deserialization process perturbs the representation of a
|
||||
tensor in ways that could result in incorrect measurements. For example,
|
||||
if a tensor lives in pinned CPU memory, this fact would not be preserved
|
||||
by a dump, and that will in turn change the performance of certain CUDA
|
||||
operations.
|
||||
"""
|
||||
|
||||
def __init__(self, globals: dict[str, Any], data_dir: str) -> None:
|
||||
self._globals: dict[str, CopyIfCallgrind] = {}
|
||||
self._data_dir = data_dir
|
||||
if not os.path.exists(data_dir):
|
||||
os.mkdir(data_dir)
|
||||
|
||||
if globals.get("torch", torch) is not torch:
|
||||
raise ValueError("`collect_callgrind` does not support mocking out `torch`.")
|
||||
|
||||
for name, value in globals.items():
|
||||
if name in ("torch", "__builtins__"):
|
||||
# Torch will be imported by the collection script, and
|
||||
# __builtins__ is added by Timer.
|
||||
continue
|
||||
|
||||
if not isinstance(value, CopyIfCallgrind):
|
||||
raise ValueError(
|
||||
"`collect_callgrind` requires that globals be wrapped in "
|
||||
"`CopyIfCallgrind` so that serialization is explicit."
|
||||
)
|
||||
|
||||
self._globals[name] = value
|
||||
|
||||
def construct(self) -> str:
|
||||
load_lines = []
|
||||
for name, wrapped_value in self._globals.items():
|
||||
if wrapped_value.setup is not None:
|
||||
load_lines.append(textwrap.dedent(wrapped_value.setup))
|
||||
|
||||
if wrapped_value.serialization == Serialization.PICKLE:
|
||||
path = os.path.join(self._data_dir, f"{name}.pkl")
|
||||
load_lines.append(
|
||||
f"with open({repr(path)}, 'rb') as f:\n {name} = pickle.load(f)")
|
||||
with open(path, "wb") as f:
|
||||
pickle.dump(wrapped_value.value, f)
|
||||
|
||||
elif wrapped_value.serialization == Serialization.TORCH:
|
||||
path = os.path.join(self._data_dir, f"{name}.pt")
|
||||
# TODO: Figure out if we can use torch.serialization.add_safe_globals here
|
||||
# Using weights_only=False after the change in
|
||||
# https://dev-discuss.pytorch.org/t/bc-breaking-change-torch-load-is-being-flipped-to-use-weights-only-true-by-default-in-the-nightlies-after-137602/2573
|
||||
load_lines.append(f"{name} = torch.load({repr(path)}, weights_only=False)")
|
||||
torch.save(wrapped_value.value, path)
|
||||
|
||||
elif wrapped_value.serialization == Serialization.TORCH_JIT:
|
||||
path = os.path.join(self._data_dir, f"{name}.pt")
|
||||
load_lines.append(f"{name} = torch.jit.load({repr(path)})")
|
||||
with open(path, "wb") as f:
|
||||
torch.jit.save(wrapped_value.value, f) # type: ignore[no-untyped-call]
|
||||
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Unknown serialization method: {wrapped_value.serialization}")
|
||||
|
||||
return "\n".join(load_lines)
|
||||
|
||||
|
||||
class _ValgrindWrapper:
|
||||
def __init__(self) -> None:
|
||||
self._bindings_module: CallgrindModuleType | None = None
|
||||
valgrind_symbols = (
|
||||
"_valgrind_supported_platform",
|
||||
"_valgrind_toggle",
|
||||
"_valgrind_toggle_and_dump_stats",
|
||||
)
|
||||
if all(hasattr(torch._C, symbol) for symbol in valgrind_symbols):
|
||||
self._supported_platform: bool = torch._C._valgrind_supported_platform()
|
||||
|
||||
else:
|
||||
print("Callgrind bindings are not present in `torch._C`. JIT-ing bindings.")
|
||||
self._bindings_module = cpp_jit.get_compat_bindings()
|
||||
if not all(hasattr(self._bindings_module, symbol) for symbol in valgrind_symbols):
|
||||
raise AssertionError("JIT-compiled callgrind bindings are missing required symbols")
|
||||
self._supported_platform = self._bindings_module._valgrind_supported_platform()
|
||||
|
||||
self._commands_available: dict[str, bool] = {}
|
||||
if self._supported_platform:
|
||||
# Only bother checking on supported platforms.
|
||||
for cmd in ("valgrind", "callgrind_control", "callgrind_annotate"):
|
||||
self._commands_available[cmd] = not subprocess.run(
|
||||
["which", cmd],
|
||||
capture_output=True,
|
||||
check=False,
|
||||
).returncode
|
||||
|
||||
self._build_type: str | None = None
|
||||
build_search = re.search("BUILD_TYPE=(.+),", torch.__config__.show()) # type: ignore[no-untyped-call]
|
||||
if build_search is not None:
|
||||
self._build_type = build_search.groups()[0].split(",")[0]
|
||||
|
||||
def _validate(self) -> None:
|
||||
if not self._supported_platform:
|
||||
raise OSError("Valgrind is not supported on this platform.")
|
||||
|
||||
missing_cmds = [cmd for cmd, available in self._commands_available.items() if not available]
|
||||
if missing_cmds:
|
||||
raise OSError("Missing: " + ", ".join(missing_cmds))
|
||||
|
||||
def collect_callgrind(
|
||||
self,
|
||||
task_spec: common.TaskSpec,
|
||||
globals: dict[str, Any],
|
||||
*,
|
||||
number: int,
|
||||
repeats: int,
|
||||
collect_baseline: bool,
|
||||
is_python: bool,
|
||||
retain_out_file: bool,
|
||||
) -> tuple[CallgrindStats, ...]:
|
||||
"""Collect stats, and attach a reference run which can be used to filter interpreter overhead."""
|
||||
self._validate()
|
||||
if not is_python and collect_baseline:
|
||||
raise AssertionError("collect_baseline is only supported for Python timers")
|
||||
|
||||
*task_stats, baseline_stats = self._invoke(
|
||||
task_spec=task_spec,
|
||||
globals=globals,
|
||||
number=number,
|
||||
repeats=repeats,
|
||||
collect_baseline=collect_baseline,
|
||||
is_python=is_python,
|
||||
retain_out_file=retain_out_file,
|
||||
)
|
||||
if len(task_stats) != repeats:
|
||||
raise AssertionError("Unexpected number of task stats returned from _invoke")
|
||||
|
||||
return tuple(
|
||||
CallgrindStats(
|
||||
task_spec=task_spec,
|
||||
number_per_run=number,
|
||||
built_with_debug_symbols=self._build_type == "RelWithDebInfo",
|
||||
baseline_inclusive_stats=baseline_stats[0],
|
||||
baseline_exclusive_stats=baseline_stats[1],
|
||||
stmt_inclusive_stats=stmt_inclusive_stats,
|
||||
stmt_exclusive_stats=stmt_exclusive_stats,
|
||||
stmt_callgrind_out=out_contents,
|
||||
)
|
||||
for stmt_inclusive_stats, stmt_exclusive_stats, out_contents in task_stats
|
||||
)
|
||||
|
||||
def _invoke(
|
||||
self,
|
||||
*,
|
||||
task_spec: common.TaskSpec,
|
||||
globals: dict[str, Any],
|
||||
number: int,
|
||||
repeats: int,
|
||||
collect_baseline: bool,
|
||||
is_python: bool,
|
||||
retain_out_file: bool,
|
||||
) -> tuple[tuple[FunctionCounts, FunctionCounts, str | None], ...]:
|
||||
"""Core invocation method for Callgrind collection.
|
||||
|
||||
Valgrind operates by effectively replacing the CPU with an emulated
|
||||
version which allows it to instrument any code at the cost of severe
|
||||
performance degradation. This has the practical effect that in order
|
||||
to collect Callgrind statistics, a new process has to be created
|
||||
running under `valgrind`. The steps for this process are:
|
||||
|
||||
1) Create a scratch directory.
|
||||
2) Codegen a run script. (_ValgrindWrapper._construct_script)
|
||||
Inside the run script:
|
||||
* Validate that Python and torch match the parent process
|
||||
* Validate that it is indeed running under valgrind
|
||||
* Execute `setup` and warm up `stmt`
|
||||
* Begin collecting stats
|
||||
* Run the `stmt` loop
|
||||
* Stop collecting stats
|
||||
3) Parse the run results.
|
||||
4) Cleanup the scratch directory.
|
||||
"""
|
||||
working_dir = common._make_temp_dir(prefix="callgrind")
|
||||
data_dir = os.path.join(working_dir, "data")
|
||||
script_file = os.path.join(working_dir, "timer_callgrind.py")
|
||||
callgrind_out = os.path.join(working_dir, "callgrind.out")
|
||||
error_log = os.path.join(working_dir, "error.txt")
|
||||
stat_log = os.path.join(working_dir, "callgrind_stat.txt")
|
||||
stdout_stderr_log = os.path.join(working_dir, "stdout_stderr.log")
|
||||
|
||||
def run(args: list[str], **kwargs: Any) -> tuple[CompletedProcessType, str]:
|
||||
# https://thraxil.org/users/anders/posts/2008/03/13/Subprocess-Hanging-PIPE-is-your-enemy/
|
||||
with open(stdout_stderr_log, "wb") as f_stdout_stderr:
|
||||
invocation = subprocess.run(
|
||||
args,
|
||||
stdout=f_stdout_stderr,
|
||||
stderr=subprocess.STDOUT,
|
||||
**kwargs,
|
||||
)
|
||||
with open(stdout_stderr_log) as f:
|
||||
return invocation, f.read()
|
||||
|
||||
try:
|
||||
if is_python:
|
||||
if self._bindings_module is not None:
|
||||
shutil.copy(
|
||||
self._bindings_module.__file__,
|
||||
os.path.join(working_dir, os.path.split(self._bindings_module.__file__)[1])
|
||||
)
|
||||
|
||||
script_file = os.path.join(working_dir, "timer_callgrind.py")
|
||||
with open(script_file, "w") as f:
|
||||
f.write(self._construct_script(
|
||||
task_spec,
|
||||
globals=GlobalsBridge(globals, data_dir),
|
||||
number=number,
|
||||
repeats=repeats,
|
||||
collect_baseline=collect_baseline,
|
||||
error_log=error_log,
|
||||
stat_log=stat_log,
|
||||
bindings=self._bindings_module))
|
||||
|
||||
run_loop_cmd = ["python", script_file]
|
||||
else:
|
||||
if collect_baseline:
|
||||
raise AssertionError("collect_baseline must be False for non-Python timers")
|
||||
run_loop_exec = cpp_jit.compile_callgrind_template(
|
||||
stmt=task_spec.stmt,
|
||||
setup=task_spec.setup,
|
||||
global_setup=task_spec.global_setup,
|
||||
)
|
||||
run_loop_cmd = [
|
||||
run_loop_exec,
|
||||
"--number", str(number),
|
||||
"--number-warmup", str(min(number, 10)),
|
||||
"--repeats", str(repeats),
|
||||
"--number-threads", str(task_spec.num_threads),
|
||||
]
|
||||
|
||||
valgrind_invocation, valgrind_invocation_output = run([
|
||||
"valgrind",
|
||||
"--tool=callgrind",
|
||||
f"--callgrind-out-file={callgrind_out}",
|
||||
"--dump-line=yes",
|
||||
"--dump-instr=yes",
|
||||
"--instr-atstart=yes",
|
||||
"--collect-atstart=no",
|
||||
] + run_loop_cmd)
|
||||
|
||||
if valgrind_invocation.returncode:
|
||||
error_report = ""
|
||||
if os.path.exists(error_log):
|
||||
with open(error_log) as f:
|
||||
error_report = f.read()
|
||||
if not error_report:
|
||||
error_report = "Unknown error.\n" + valgrind_invocation_output
|
||||
|
||||
raise OSError(f"Failed to collect callgrind profile:\n{error_report}")
|
||||
|
||||
def parse_output(fpath: str, inclusive: bool) -> FunctionCounts:
|
||||
_annotate_invocation, annotate_invocation_output = run([
|
||||
"callgrind_annotate",
|
||||
f"--inclusive={'yes' if inclusive else 'no'}",
|
||||
"--threshold=100",
|
||||
"--show-percs=no",
|
||||
fpath
|
||||
], check=True)
|
||||
|
||||
total_pattern = re.compile(r"^([0-9,]+)\s+PROGRAM TOTALS")
|
||||
begin_pattern = re.compile(r"Ir\s+file:function")
|
||||
function_pattern = re.compile(r"^\s*([0-9,]+)\s+(.+:.+)$")
|
||||
|
||||
class ScanState(enum.Enum):
|
||||
SCANNING_FOR_TOTAL = 0
|
||||
SCANNING_FOR_START = 1
|
||||
PARSING = 2
|
||||
|
||||
scan_state = ScanState.SCANNING_FOR_TOTAL
|
||||
fn_counts = []
|
||||
for l in annotate_invocation_output.splitlines(keepends=False):
|
||||
if scan_state == ScanState.SCANNING_FOR_TOTAL:
|
||||
total_match = total_pattern.match(l)
|
||||
if total_match:
|
||||
program_totals = int(total_match.groups()[0].replace(",", ""))
|
||||
scan_state = ScanState.SCANNING_FOR_START
|
||||
|
||||
elif scan_state == ScanState.SCANNING_FOR_START:
|
||||
if begin_pattern.match(l):
|
||||
scan_state = ScanState.PARSING
|
||||
|
||||
else:
|
||||
if scan_state != ScanState.PARSING:
|
||||
raise AssertionError("Failed to enter PARSING state while parsing callgrind_annotate output")
|
||||
fn_match = function_pattern.match(l)
|
||||
if fn_match:
|
||||
ir_str, file_function = fn_match.groups()
|
||||
ir = int(ir_str.replace(",", ""))
|
||||
if ir == program_totals: # type: ignore[possibly-undefined]
|
||||
# Callgrind includes some top level red herring symbols when
|
||||
# a program dumps multiple profiles.
|
||||
continue
|
||||
fn_counts.append(FunctionCount(ir, file_function))
|
||||
|
||||
elif re.match(r"-+", l):
|
||||
# Ignore heading separator lines.
|
||||
continue
|
||||
|
||||
else:
|
||||
break
|
||||
|
||||
if scan_state != ScanState.PARSING:
|
||||
raise AssertionError(f"Failed to parse {fpath}")
|
||||
return FunctionCounts(tuple(sorted(fn_counts, reverse=True)), inclusive=inclusive)
|
||||
|
||||
def read_results(i: int) -> tuple[FunctionCounts, FunctionCounts, str | None]:
|
||||
if i == repeats and not collect_baseline:
|
||||
# Null baseline.
|
||||
return (
|
||||
FunctionCounts((), inclusive=True),
|
||||
FunctionCounts((), inclusive=False),
|
||||
None,
|
||||
)
|
||||
|
||||
fpath = f"{callgrind_out}.{i + 1}" # Callgrind one-indexes files.
|
||||
callgrind_out_contents: str | None = None
|
||||
if retain_out_file:
|
||||
with open(fpath) as f:
|
||||
callgrind_out_contents = f.read()
|
||||
|
||||
return (
|
||||
parse_output(fpath, inclusive=True),
|
||||
parse_output(fpath, inclusive=False),
|
||||
callgrind_out_contents
|
||||
)
|
||||
|
||||
return tuple(read_results(i) for i in range(repeats + 1))
|
||||
finally:
|
||||
shutil.rmtree(working_dir)
|
||||
|
||||
@staticmethod
|
||||
def _construct_script(
|
||||
task_spec: common.TaskSpec,
|
||||
globals: GlobalsBridge,
|
||||
*,
|
||||
number: int,
|
||||
repeats: int,
|
||||
collect_baseline: bool,
|
||||
error_log: str,
|
||||
stat_log: str,
|
||||
bindings: CallgrindModuleType | None,
|
||||
) -> str:
|
||||
def block_stmt(stmt: str, indent: int = 0) -> str:
|
||||
"""Partially unroll benchmark loop.
|
||||
|
||||
The naive template looks something like:
|
||||
"for _ in range({number}): {stmt}"
|
||||
|
||||
However a loop in Python is surprisingly expensive, and significantly
|
||||
increases the number of background Python instructions. So instead we
|
||||
partially unroll the loops, with a block size of 100 chosen to keep
|
||||
the instruction overhead from `range` low while also not ballooning
|
||||
the size of the generated file.
|
||||
"""
|
||||
block_size = 100
|
||||
loop_count = number // block_size
|
||||
if loop_count == 1:
|
||||
# There is no point in having `for _ in range(1): ...` rather
|
||||
# than just `...`, and this lets us save shave a few background
|
||||
# instructions.
|
||||
loop_count = 0
|
||||
remainder = number - block_size * loop_count
|
||||
blocked_stmt = ""
|
||||
|
||||
if loop_count:
|
||||
unrolled_stmts = textwrap.indent("\n".join([stmt] * block_size), " " * 4)
|
||||
blocked_stmt += f"for _ in range({loop_count}):\n{unrolled_stmts}\n"
|
||||
|
||||
if remainder:
|
||||
blocked_stmt += "\n".join([stmt] * remainder)
|
||||
|
||||
return textwrap.indent(blocked_stmt, " " * indent)
|
||||
|
||||
pass_baseline = (
|
||||
"callgrind_bindings._valgrind_toggle()\n"
|
||||
f"{block_stmt('pass')}\n"
|
||||
"callgrind_bindings._valgrind_toggle_and_dump_stats()"
|
||||
)
|
||||
|
||||
return textwrap.dedent(r"""
|
||||
import gc
|
||||
import os
|
||||
import pickle
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
# Mitigate https://github.com/pytorch/pytorch/issues/37377
|
||||
# which can sometimes cause the subprocess call to fail.
|
||||
import numpy as np
|
||||
|
||||
import torch
|
||||
torch.set_num_threads({num_threads})
|
||||
|
||||
{bindings_import}
|
||||
|
||||
PID = os.getpid()
|
||||
|
||||
def log_failure(msg):
|
||||
with open({error_log_repr}, "wt") as f:
|
||||
f.write(msg)
|
||||
sys.exit(1)
|
||||
|
||||
def check_result(completed_process):
|
||||
if completed_process.returncode:
|
||||
log_failure(f"Command failed: {{' '.join(completed_process.args)}}")
|
||||
return completed_process
|
||||
|
||||
# =============================================================================
|
||||
# == Check that subprocess matches parent =====================================
|
||||
# =============================================================================
|
||||
if os.path.realpath(sys.executable) != "{parent_interpreter}":
|
||||
log_failure(
|
||||
"Interpreter mismatch:\n"
|
||||
f" {{os.path.realpath(sys.executable)}}\n vs.\n {parent_interpreter}"
|
||||
)
|
||||
|
||||
if torch.__file__ != "{torch_file}":
|
||||
log_failure(
|
||||
"PyTorch does not match expected file:\n"
|
||||
f" {{torch.__file__}}\n vs.\n {torch_file}"
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# == User specified setup =====================================================
|
||||
# =============================================================================
|
||||
# Load serialized globals
|
||||
{load_globals}
|
||||
|
||||
# User setup str
|
||||
{setup}
|
||||
|
||||
for _ in range({warmup_number}):
|
||||
{indented_stmt}
|
||||
|
||||
# =============================================================================
|
||||
# == Callgrind management =====================================================
|
||||
# =============================================================================
|
||||
with open("{stat_log}", "wb") as stat_file:
|
||||
# If many instances of callgrind are running at once, the output of
|
||||
# `callgrind_control` may exceed 16kb which would cause `subprocess.PIPE`
|
||||
# to deadlock. So instead we use a file.
|
||||
callgrind_stat = check_result(subprocess.run(
|
||||
["callgrind_control", "--stat"],
|
||||
stdout=stat_file,
|
||||
stderr=subprocess.STDOUT,
|
||||
))
|
||||
|
||||
with open("{stat_log}", "rt") as stat_file:
|
||||
stat_lines = stat_file.read().splitlines()
|
||||
|
||||
if f"PID {{PID}}: python {{__file__}}" not in stat_lines:
|
||||
log_failure("Process does not appear to be running callgrind.")
|
||||
|
||||
gc.collect()
|
||||
time.sleep(0.01)
|
||||
|
||||
# =============================================================================
|
||||
# == User code block ==========================================================
|
||||
# =============================================================================
|
||||
for _ in range({repeats}):
|
||||
callgrind_bindings._valgrind_toggle()
|
||||
{blocked_stmt}
|
||||
callgrind_bindings._valgrind_toggle_and_dump_stats()
|
||||
gc.collect()
|
||||
|
||||
{baseline}
|
||||
""").strip().format(
|
||||
indented_stmt=textwrap.indent(task_spec.stmt, " " * 4),
|
||||
blocked_stmt=block_stmt(task_spec.stmt, indent=4),
|
||||
baseline=(pass_baseline if collect_baseline else ""),
|
||||
number=number,
|
||||
repeats=repeats,
|
||||
load_globals=globals.construct(),
|
||||
setup=task_spec.setup,
|
||||
warmup_number=min(number, 10),
|
||||
num_threads=task_spec.num_threads,
|
||||
error_log_repr=repr(error_log),
|
||||
stat_log=stat_log,
|
||||
parent_interpreter=os.path.realpath(sys.executable),
|
||||
torch_file=torch.__file__,
|
||||
bindings_import=(
|
||||
"import torch._C as callgrind_bindings" if bindings is None
|
||||
else f"import {bindings.__name__} as callgrind_bindings"),
|
||||
)
|
||||
|
||||
|
||||
CALLGRIND_SINGLETON: _ValgrindWrapper | None = None
|
||||
def wrapper_singleton() -> _ValgrindWrapper:
|
||||
global CALLGRIND_SINGLETON
|
||||
if CALLGRIND_SINGLETON is None:
|
||||
CALLGRIND_SINGLETON = _ValgrindWrapper()
|
||||
return CALLGRIND_SINGLETON
|
||||
+7157
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,472 @@
|
||||
#!/usr/bin/env python3
|
||||
# mypy: allow-untyped-defs
|
||||
from typing import Any, TypeVar, NamedTuple
|
||||
from collections.abc import Callable, Sequence
|
||||
import textwrap
|
||||
import torch
|
||||
from torch._C import TupleType, ListType
|
||||
from torch.jit._recursive import wrap_cpp_module
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
MAX_RAW_TENSOR_SIZE = 16
|
||||
|
||||
class InflatableArg(NamedTuple):
|
||||
"""Helper type for bundled inputs.
|
||||
|
||||
'value' is the compressed/deflated input that is stored in the model. Value
|
||||
must be of the same type as the argument to the function that it is a deflated
|
||||
input for.
|
||||
|
||||
'fmt' is a formattable code string that is executed to inflate the compressed data into
|
||||
the appropriate input. It can use 'value' as an input to the format str. It must result
|
||||
in a value of the same type as 'value'.
|
||||
|
||||
'fmt_fn' is a formattable function code string that is executed to inflate the compressed
|
||||
data into the appropriate input. It must result in a value of the same type as 'value'.
|
||||
The function name should be the formattable part of the string.
|
||||
|
||||
Note: Only top level InflatableArgs can be inflated. i.e. you cannot place
|
||||
an inflatable arg inside of some other structure. You should instead create
|
||||
an inflatable arg such that the fmt code string returns the full structure
|
||||
of your input.
|
||||
"""
|
||||
|
||||
value: Any
|
||||
fmt: str = "{}"
|
||||
fmt_fn: str = ""
|
||||
|
||||
|
||||
def bundle_inputs(
|
||||
model: torch.jit.ScriptModule,
|
||||
inputs: Sequence[tuple[Any, ...]] | dict[Callable, Sequence[tuple[Any, ...]] | None] | None,
|
||||
info: list[str] | dict[Callable, list[str]] | None = None,
|
||||
*,
|
||||
_receive_inflate_expr: list[str] | None = None,
|
||||
) -> torch.jit.ScriptModule:
|
||||
"""Create and return a copy of the specified model with inputs attached.
|
||||
|
||||
The original model is not mutated or changed in any way.
|
||||
|
||||
Models with bundled inputs can be invoked in a uniform manner by
|
||||
benchmarking and code coverage tools.
|
||||
|
||||
If inputs is passed in as a list then the inputs will be bundled for 'forward'.
|
||||
If inputs is instead passed in as a map then all the methods specified in the map
|
||||
will have their corresponding inputs bundled. Info should match watchever type is
|
||||
chosen for the inputs.
|
||||
|
||||
The returned model will support the following methods:
|
||||
|
||||
`get_all_bundled_inputs_for_<function_name>() -> List[Tuple[Any, ...]]`
|
||||
Returns a list of tuples suitable for passing to the model like
|
||||
`for inp in model.get_all_bundled_inputs_for_foo(): model.foo(*inp)`
|
||||
|
||||
`get_bundled_inputs_functions_and_info() -> Dict[str, Dict[str: List[str]]]`
|
||||
Returns a dictionary mapping function names to a metadata dictionary.
|
||||
This nested dictionary maps preset strings like:
|
||||
'get_inputs_function_name' -> the name of a function attribute in this model that can be
|
||||
run to get back a list of inputs corresponding to that function.
|
||||
'info' -> the user provided extra information about the bundled inputs
|
||||
|
||||
If forward has bundled inputs then these following functions will also be defined on the returned module:
|
||||
|
||||
`get_all_bundled_inputs() -> List[Tuple[Any, ...]]`
|
||||
Returns a list of tuples suitable for passing to the model like
|
||||
`for inp in model.get_all_bundled_inputs(): model(*inp)`
|
||||
|
||||
`get_num_bundled_inputs() -> int`
|
||||
Equivalent to `len(model.get_all_bundled_inputs())`,
|
||||
but slightly easier to call from C++.
|
||||
|
||||
Inputs can be specified in one of two ways:
|
||||
|
||||
- The model can define `_generate_bundled_inputs_for_<function_name>`.
|
||||
If the user chooses this method inputs[<function>] should map to None
|
||||
|
||||
- The `inputs` argument to this function can be a dictionary mapping functions to a
|
||||
list of inputs, of the same form that will be returned by get_all_bundled_inputs_for_<function_name>.
|
||||
Alternatively if only bundling inputs for forward the map can be omitted and a singular list of inputs
|
||||
can be provided instead.
|
||||
|
||||
The type of the inputs is List[Tuple[Any, ...]]. The outer list corresponds with a
|
||||
list of inputs, the inner tuple is the list of args that together make up one input.
|
||||
For inputs of functions that take one arg, this will be a tuple of length one. The Any, ...
|
||||
is the actual data that makes up the args, e.g. a tensor.
|
||||
|
||||
Info is an optional parameter that maps functions to a list of strings providing extra information about that
|
||||
function's bundled inputs. Alternatively if only bundling inputs for forward the map can be omitted and
|
||||
a singular list of information can be provided instead. This could be descriptions, expected outputs, etc.
|
||||
- Ex: info={model.forward : ['man eating icecream', 'an airplane', 'a dog']}
|
||||
|
||||
This function will attempt to optimize arguments so that (e.g.)
|
||||
arguments like `torch.zeros(1000)` will be represented compactly.
|
||||
Only top-level arguments will be optimized.
|
||||
Tensors in lists or tuples will not.
|
||||
"""
|
||||
if not isinstance(model, torch.jit.ScriptModule):
|
||||
raise Exception("Only ScriptModule is supported.") # noqa: TRY002
|
||||
|
||||
ignored_methods, ignored_attrs = _get_bundled_inputs_attributes_and_methods(model)
|
||||
clone = torch._C._hack_do_not_use_clone_module_with_class( # type: ignore[attr-defined]
|
||||
model._c,
|
||||
ignored_methods,
|
||||
ignored_attrs,
|
||||
)
|
||||
|
||||
# The above cloning function returns a torch._C.scriptmodule and we need a torch.jit.scriptmodule.
|
||||
# Fortunately there is a function in _recursive that does exactly that conversion.
|
||||
cloned_module = wrap_cpp_module(clone)
|
||||
if isinstance(inputs, dict):
|
||||
if not isinstance(info, dict) and info is not None:
|
||||
raise AssertionError("If inputs is a dict, info must be a dict or None")
|
||||
augment_many_model_functions_with_bundled_inputs(cloned_module, inputs, _receive_inflate_expr, info)
|
||||
else:
|
||||
if not isinstance(info, list) and info is not None:
|
||||
raise AssertionError("If inputs is a list, info must be a list or None")
|
||||
augment_model_with_bundled_inputs(cloned_module, inputs, _receive_inflate_expr, info)
|
||||
return cloned_module
|
||||
|
||||
def augment_model_with_bundled_inputs(
|
||||
model: torch.jit.ScriptModule,
|
||||
inputs: Sequence[tuple[Any, ...]] | None = None,
|
||||
_receive_inflate_expr: list[str] | None = None, # For debugging.
|
||||
info: list[str] | None = None, # Optional argument to provide info about forward or its inputs
|
||||
skip_size_check=False,
|
||||
) -> None:
|
||||
"""Add bundled sample inputs to a model for the forward function.
|
||||
|
||||
Models with bundled inputs can be invoked in a uniform manner by
|
||||
benchmarking and code coverage tools.
|
||||
|
||||
Augmented models will support the following methods:
|
||||
|
||||
`get_all_bundled_inputs() -> List[Tuple[Any, ...]]`
|
||||
Returns a list of tuples suitable for passing to the model like
|
||||
`for inp in model.get_all_bundled_inputs(): model(*inp)`
|
||||
|
||||
`get_num_bundled_inputs() -> int`
|
||||
Equivalent to `len(model.get_all_bundled_inputs())`,
|
||||
but slightly easier to call from C++.
|
||||
|
||||
`get_bundled_inputs_functions_and_info() -> Dict[str, Dict[str: List[str]]]`
|
||||
Returns a dictionary mapping function names to a metadata dictionary.
|
||||
This nested dictionary maps preset strings like:
|
||||
'get_inputs_function_name' -> the name of a function attribute in this model that can be
|
||||
run to get back a list of inputs corresponding to that function.
|
||||
'info' -> the user provided extra information about the bundled inputs
|
||||
|
||||
Inputs can be specified in one of two ways:
|
||||
|
||||
- The model can define `_generate_bundled_inputs_for_forward`.
|
||||
If the user chooses this method inputs should be None
|
||||
|
||||
- `inputs` is a list of inputs of form List[Tuple[Any, ...]]. A list of tuples where the elements
|
||||
of each tuple are the args that make up one input.
|
||||
"""
|
||||
if not isinstance(model, torch.jit.ScriptModule):
|
||||
raise Exception("Only ScriptModule is supported.") # noqa: TRY002
|
||||
|
||||
forward: Callable = model.forward
|
||||
|
||||
# Sometimes forward won't have a name attached so just in case
|
||||
if not hasattr(forward, "__name__"):
|
||||
forward.__name__ = 'forward'
|
||||
augment_many_model_functions_with_bundled_inputs(
|
||||
model,
|
||||
inputs={forward : inputs},
|
||||
_receive_inflate_expr=_receive_inflate_expr,
|
||||
info={forward : info} if info else None,
|
||||
skip_size_check=skip_size_check,
|
||||
)
|
||||
|
||||
|
||||
def augment_many_model_functions_with_bundled_inputs(
|
||||
model: torch.jit.ScriptModule,
|
||||
inputs: dict[Callable, Sequence[tuple[Any, ...]] | None],
|
||||
_receive_inflate_expr: list[str] | None = None, # For debugging.
|
||||
info: dict[Callable, list[str]] | None = None, # Optional argument to provide info about the function or its inputs
|
||||
skip_size_check=False,
|
||||
) -> None:
|
||||
"""Add bundled sample inputs to a model for an arbitrary list of public functions.
|
||||
|
||||
Models with bundled inputs can be invoked in a uniform manner by
|
||||
benchmarking and code coverage tools.
|
||||
|
||||
Augmented models will support the following methods:
|
||||
|
||||
`get_all_bundled_inputs_for_<function_name>() -> List[Tuple[Any, ...]]`
|
||||
Returns a list of tuples suitable for passing to the model like
|
||||
`for inp in model.get_all_bundled_inputs_for_foo(): model.foo(*inp)`
|
||||
|
||||
`get_bundled_inputs_functions_and_info() -> Dict[str, Dict[str: List[str]]]`
|
||||
Returns a dictionary mapping function names to a metadata dictionary.
|
||||
This nested dictionary maps preset strings like:
|
||||
'get_inputs_function_name' -> the name of a function attribute in this model that can be
|
||||
run to get back a list of inputs corresponding to that function.
|
||||
'info' -> the user provided extra information about the bundled inputs
|
||||
|
||||
If forward has bundled inputs then these following functions are also defined:
|
||||
|
||||
`get_all_bundled_inputs() -> List[Tuple[Any, ...]]`
|
||||
Returns a list of tuples suitable for passing to the model like
|
||||
`for inp in model.get_all_bundled_inputs(): model(*inp)`
|
||||
|
||||
`get_num_bundled_inputs() -> int`
|
||||
Equivalent to `len(model.get_all_bundled_inputs())`,
|
||||
but slightly easier to call from C++.
|
||||
|
||||
Inputs can be specified in one of two ways:
|
||||
|
||||
- The model can define `_generate_bundled_inputs_for_<function_name>`.
|
||||
If the user chooses this method inputs[<function>] should map to None
|
||||
|
||||
- The `inputs` argument to this function can be a dictionary mapping functions to a
|
||||
list of inputs, of the same form that will be returned by get_all_bundled_inputs_for_<function_name>.
|
||||
The type of the inputs is List[Tuple[Any, ...]]. The outer list corresponds with a
|
||||
list of inputs, the inner tuple is the list of args that together make up one input.
|
||||
For inputs of functions that take one arg, this will be a tuple of length one. The Any, ...
|
||||
is the actual data that makes up the args, e.g. a tensor.
|
||||
|
||||
Info is an optional parameter that maps functions to a list of strings providing extra information about that
|
||||
function's bundled inputs. This could be descriptions, expected outputs, etc.
|
||||
- Ex: info={model.forward : ['man eating icecream', 'an airplane', 'a dog']}
|
||||
|
||||
This function will attempt to optimize arguments so that (e.g.)
|
||||
arguments like `torch.zeros(1000)` will be represented compactly.
|
||||
Only top-level arguments will be optimized.
|
||||
Tensors in lists or tuples will not.
|
||||
"""
|
||||
if not isinstance(model, torch.jit.ScriptModule):
|
||||
raise Exception("Only ScriptModule is supported.") # noqa: TRY002
|
||||
|
||||
if not inputs:
|
||||
raise Exception("Please provide inputs for at least 1 function") # noqa: TRY002
|
||||
|
||||
if hasattr(model, "get_all_bundled_inputs") or hasattr(model, "get_bundled_inputs_functions_and_info"):
|
||||
raise Exception( # noqa: TRY002
|
||||
"Models can only be augmented with bundled inputs once. "
|
||||
"This Model seems to have already been augmented with "
|
||||
"bundled inputs. Please start afresh with one that "
|
||||
"doesn't have bundled inputs.",
|
||||
)
|
||||
|
||||
get_bundled_inputs_functions_and_info_template = ""
|
||||
|
||||
for function, input_list in inputs.items():
|
||||
if hasattr(function, "__name__"):
|
||||
function_name = function.__name__
|
||||
else:
|
||||
if hasattr(function, "name"):
|
||||
function_name = function.name # type: ignore[attr-defined]
|
||||
else:
|
||||
raise Exception( # noqa: TRY002
|
||||
'At least one of your functions has no attribute name please ensure all have one. m.foo.name = "foo"')
|
||||
|
||||
|
||||
if input_list is not None and not isinstance(input_list, Sequence):
|
||||
raise TypeError(f"Error inputs for function {function_name} is not a Sequence")
|
||||
|
||||
function_arg_types = [arg.type for arg in function.schema.arguments[1:]] # type: ignore[attr-defined]
|
||||
deflated_inputs_type: ListType = ListType(TupleType(function_arg_types))
|
||||
model._c._register_attribute(f"_bundled_inputs_deflated_{function_name}", deflated_inputs_type, [])
|
||||
|
||||
if hasattr(model, "_generate_bundled_inputs_for_" + function_name):
|
||||
if input_list is not None:
|
||||
raise Exception( # noqa: TRY002
|
||||
f"inputs[{function_name}] is not None, but _generate_bundled_inputs_for_{function_name} is already defined"
|
||||
)
|
||||
# Model author already defined _generate_bundled_inputs_for_<function_name>.
|
||||
elif input_list is None or len(input_list) == 0:
|
||||
raise Exception( # noqa: TRY002
|
||||
f"inputs for {function_name} must be specified if "
|
||||
f"_generate_bundled_inputs_for_{function_name} is not already defined"
|
||||
)
|
||||
else:
|
||||
# Iterate over the inputs and args in each input.
|
||||
# Accumulate `deflated_inputs` as (possibly) compressed values
|
||||
# and `parts` to be joined into the expression that unpacks them.
|
||||
deflated_inputs = []
|
||||
parts = []
|
||||
for inp_idx, args in enumerate(input_list):
|
||||
if not isinstance(args, tuple) and not isinstance(args, list): # type: ignore[arg-type]
|
||||
raise TypeError(
|
||||
f"Error bundled input for function {function_name} idx: {inp_idx} is not a Tuple or a List"
|
||||
)
|
||||
deflated_args = []
|
||||
parts.append("(")
|
||||
for arg_idx, arg in enumerate(args):
|
||||
inflate_helper_fn_name = _get_inflate_helper_fn_name(arg_idx, inp_idx, function_name)
|
||||
deflated, inflater, helper_definition = _inflate_expr(
|
||||
arg,
|
||||
f"deflated[{inp_idx}][{arg_idx}]",
|
||||
inflate_helper_fn_name,
|
||||
skip_size_check=skip_size_check,
|
||||
)
|
||||
deflated_args.append(deflated)
|
||||
parts.append(f" {inflater},")
|
||||
if helper_definition:
|
||||
model.define(textwrap.dedent(helper_definition))
|
||||
deflated_inputs.append(tuple(deflated_args))
|
||||
parts.append("),")
|
||||
parts.append("")
|
||||
expr = "\n".join(parts)
|
||||
|
||||
# Back-channel return this expr for debugging.
|
||||
if _receive_inflate_expr is not None:
|
||||
_receive_inflate_expr.append(expr)
|
||||
setattr(model, f"_bundled_inputs_deflated_{function_name}", deflated_inputs)
|
||||
definition = textwrap.dedent("""
|
||||
def _generate_bundled_inputs_for_{name}(self):
|
||||
deflated = self._bundled_inputs_deflated_{name}
|
||||
return [
|
||||
{expr}
|
||||
]
|
||||
""").format(expr=expr, name=function_name)
|
||||
model.define(definition)
|
||||
|
||||
# Define get_all_bundled_inputs_for_<function_name> that caches the generated inputs.
|
||||
model.define(textwrap.dedent("""
|
||||
def get_all_bundled_inputs_for_{name}(self):
|
||||
all_inputs = self._generate_bundled_inputs_for_{name}()
|
||||
assert all_inputs is not None
|
||||
return all_inputs
|
||||
""").format(name=function_name))
|
||||
|
||||
# Add to the high level helper methods
|
||||
inputs_info = repr(info[function]) if info and function in info else '[]'
|
||||
get_bundled_inputs_functions_and_info_template += f"""
|
||||
temp_dict : Dict[str,List[str]] = {{}}
|
||||
info: List[str] = {inputs_info}
|
||||
|
||||
temp_dict['info'] = info
|
||||
temp_dict['get_inputs_function_name'] = ['get_all_bundled_inputs_for_{function_name}']
|
||||
all_inputs['{function_name}'] = temp_dict
|
||||
"""
|
||||
|
||||
# To ensure backwards compatibility and a streamlined api for forward these wrappers are provided
|
||||
if function_name == 'forward':
|
||||
model.define(textwrap.dedent("""
|
||||
def get_all_bundled_inputs(self):
|
||||
return self.get_all_bundled_inputs_for_forward()
|
||||
"""))
|
||||
model.define(textwrap.dedent("""
|
||||
def get_num_bundled_inputs(self):
|
||||
return len(self.get_all_bundled_inputs_for_forward())
|
||||
"""))
|
||||
|
||||
# Define some high level helper methods that act on all bundled inputs
|
||||
model.define(textwrap.dedent(f"""
|
||||
def get_bundled_inputs_functions_and_info(self):
|
||||
all_inputs : Dict[str, Dict[str,List[str]]] = {{}}
|
||||
{get_bundled_inputs_functions_and_info_template}
|
||||
return all_inputs
|
||||
"""))
|
||||
|
||||
def _inflate_expr(
|
||||
arg: T, ref: str, inflate_helper_fn_name: str, skip_size_check: bool = False
|
||||
) -> tuple[T | torch.Tensor, str, str | None]:
|
||||
# Allow custom inflation expressions any object.
|
||||
# For example, calling custom image-decoding ops.
|
||||
# Or just use "{}" as the format string to ignore size limits.
|
||||
if isinstance(arg, InflatableArg):
|
||||
if arg.fmt_fn:
|
||||
if arg.fmt not in ["{}", ""]:
|
||||
raise Exception( # noqa: TRY002
|
||||
f"Bundled input argument at position '{ref}' has "
|
||||
f"both arg.fmt_fn => \n{arg.fmt_fn} "
|
||||
f"\n and arg.fmt => {arg.fmt}. "
|
||||
"Please choose `arg.fmt` if the deflater is straightforward or "
|
||||
"`arg.fmt_fn` if you need a function."
|
||||
)
|
||||
|
||||
helper_definition = arg.fmt_fn.format(inflate_helper_fn_name)
|
||||
expr = f"self.{inflate_helper_fn_name}({ref})"
|
||||
|
||||
return arg.value, expr, helper_definition
|
||||
else:
|
||||
return arg.value, arg.fmt.format(ref), None
|
||||
|
||||
if isinstance(arg, torch.Tensor):
|
||||
# Small-storage tensors can just be saved directly.
|
||||
if arg._typed_storage().size() <= MAX_RAW_TENSOR_SIZE or skip_size_check:
|
||||
return arg, ref, None
|
||||
# Small contiguous tensors can be cloned to have small storage.
|
||||
# TODO: Should we do this even for non-contiguous tensors?
|
||||
if arg.is_contiguous() and arg.numel() <= MAX_RAW_TENSOR_SIZE:
|
||||
return arg.clone(), ref, None
|
||||
# Example inputs commonly come from torch.zeros, torch.ones, or torch.full.
|
||||
# These can be represented compactly.
|
||||
for fmt in [torch.contiguous_format, torch.channels_last]:
|
||||
if arg.is_contiguous(memory_format=fmt) and (arg == arg.flatten()[0]).all().item():
|
||||
return (arg.flatten()[0].clone().expand(*arg.size()),
|
||||
f"{ref}.contiguous(memory_format={fmt})", None)
|
||||
# Prevent big tensors from being bundled by default.
|
||||
# TODO: Provide more useful diagnostics.
|
||||
raise Exception( # noqa: TRY002
|
||||
f"Bundled input argument at position '{ref}' is "
|
||||
f"a tensor with storage size {arg._typed_storage().size()}. "
|
||||
f"You probably don't want to bundle this as an input. "
|
||||
)
|
||||
else:
|
||||
return arg, ref, None
|
||||
|
||||
def _get_bundled_inputs_attributes_and_methods(script_module: torch.jit.ScriptModule) -> tuple[list[str], list[str]]:
|
||||
methods: list[str] = []
|
||||
attributes: list[str] = []
|
||||
|
||||
# Has bundled inputs for forward
|
||||
if hasattr(script_module, 'get_all_bundled_inputs'):
|
||||
methods.append('get_all_bundled_inputs')
|
||||
methods.append('get_num_bundled_inputs')
|
||||
methods.append('run_on_bundled_input')
|
||||
|
||||
if hasattr(script_module, 'get_bundled_inputs_functions_and_info'):
|
||||
methods.append('get_bundled_inputs_functions_and_info')
|
||||
all_info = script_module.get_bundled_inputs_functions_and_info()
|
||||
for function_name in all_info:
|
||||
methods.append("get_all_bundled_inputs_for_" + function_name)
|
||||
methods.append("_generate_bundled_inputs_for_" + function_name)
|
||||
attributes.append("_bundled_inputs_deflated_" + function_name)
|
||||
|
||||
bundled_inputs_fn = getattr(
|
||||
script_module,
|
||||
f"get_all_bundled_inputs_for_{function_name}"
|
||||
)
|
||||
num_bundled_inputs: int = len(bundled_inputs_fn())
|
||||
|
||||
# Check inflate helper functions for each function, argument and bundled input
|
||||
func = getattr(script_module, function_name)
|
||||
for arg_idx in range(len(func.schema.arguments) - 1):
|
||||
for input_idx in range(num_bundled_inputs):
|
||||
helper_fn_name = _get_inflate_helper_fn_name(
|
||||
arg_idx=arg_idx,
|
||||
input_idx=input_idx,
|
||||
function_name=function_name
|
||||
)
|
||||
# if the arg has an InflatableArg with fmt_fn, add the helper function name
|
||||
if hasattr(script_module, helper_fn_name):
|
||||
methods.append(helper_fn_name)
|
||||
|
||||
return (methods, attributes)
|
||||
|
||||
|
||||
def _get_inflate_helper_fn_name(
|
||||
arg_idx: int,
|
||||
input_idx: int,
|
||||
function_name: str,
|
||||
) -> str:
|
||||
return f"_inflate_helper_for_{function_name}_input_{input_idx}_arg_{arg_idx}"
|
||||
|
||||
|
||||
|
||||
def bundle_randn(*size, dtype=None):
|
||||
"""Generate a tensor that will be inflated with torch.randn."""
|
||||
stub = torch.zeros(1, dtype=dtype).expand(*size)
|
||||
return InflatableArg(value=stub, fmt="torch.randn_like({})")
|
||||
|
||||
|
||||
def bundle_large_tensor(t):
|
||||
"""Wrap a tensor to allow bundling regardless of size."""
|
||||
return InflatableArg(value=t, fmt="{}")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,944 @@
|
||||
# mypy: allow-untyped-defs
|
||||
|
||||
# Unlike the rest of the PyTorch this file must be python2 compliant.
|
||||
# This script outputs relevant system environment info
|
||||
# Run it with `python collect_env.py` or `python -m torch.utils.collect_env`
|
||||
import datetime
|
||||
import json
|
||||
import locale
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
from typing import cast as _cast, Dict as _Dict
|
||||
|
||||
|
||||
try:
|
||||
import torch
|
||||
|
||||
TORCH_AVAILABLE = True
|
||||
except (ImportError, NameError, AttributeError, OSError):
|
||||
TORCH_AVAILABLE = False
|
||||
|
||||
# System Environment Information
|
||||
SystemEnv = namedtuple(
|
||||
"SystemEnv",
|
||||
[
|
||||
"torch_version",
|
||||
"is_debug_build",
|
||||
"cuda_compiled_version",
|
||||
"gcc_version",
|
||||
"clang_version",
|
||||
"cmake_version",
|
||||
"os",
|
||||
"libc_version",
|
||||
"python_version",
|
||||
"python_platform",
|
||||
"is_cuda_available",
|
||||
"cuda_runtime_version",
|
||||
"cuda_module_loading",
|
||||
"nvidia_driver_version",
|
||||
"nvidia_gpu_models",
|
||||
"cudnn_version",
|
||||
"is_xpu_available",
|
||||
"pip_version", # 'pip' or 'pip3'
|
||||
"pip_packages",
|
||||
"conda_packages",
|
||||
"hip_compiled_version",
|
||||
"hip_runtime_version",
|
||||
"miopen_runtime_version",
|
||||
"caching_allocator_config",
|
||||
"is_xnnpack_available",
|
||||
"cpu_info",
|
||||
],
|
||||
)
|
||||
|
||||
COMMON_PATTERNS = [
|
||||
"torch",
|
||||
"numpy",
|
||||
"triton",
|
||||
"optree",
|
||||
]
|
||||
|
||||
NVIDIA_PATTERNS = [
|
||||
"cuda-cudart",
|
||||
"cuda-cupti",
|
||||
"cuda-libraries",
|
||||
"cuda-opencl",
|
||||
"cuda-nvrtc",
|
||||
"cuda-runtime",
|
||||
"cublas",
|
||||
"cudnn",
|
||||
"cufft",
|
||||
"curand",
|
||||
"cusolver",
|
||||
"cusparse",
|
||||
"nccl",
|
||||
"nvjitlink",
|
||||
"nvtx",
|
||||
]
|
||||
|
||||
ONEAPI_PATTERNS = [
|
||||
"dpcpp-cpp-rt",
|
||||
"intel-cmplr-lib-rt",
|
||||
"intel-cmplr-lib-ur",
|
||||
"intel-cmplr-lic-rt",
|
||||
"intel-opencl-rt",
|
||||
"intel-sycl-rt",
|
||||
"mkl",
|
||||
"onemkl-sycl-blas",
|
||||
"onemkl-sycl-dft",
|
||||
"onemkl-sycl-lapack",
|
||||
"onemkl-sycl-rng",
|
||||
"onemkl-sycl-sparse",
|
||||
"intel-openmp",
|
||||
"tbb",
|
||||
"impi-rt",
|
||||
"impi-devel",
|
||||
"oneccl",
|
||||
"oneccl-devel",
|
||||
"intel-pti",
|
||||
"umf",
|
||||
"tcmlib",
|
||||
]
|
||||
|
||||
CONDA_PATTERNS = [
|
||||
"cudatoolkit",
|
||||
"soumith",
|
||||
"mkl",
|
||||
"magma",
|
||||
]
|
||||
|
||||
PIP_PATTERNS = [
|
||||
"mypy",
|
||||
"flake8",
|
||||
"onnx",
|
||||
]
|
||||
|
||||
|
||||
def run(command):
|
||||
"""Return (return-code, stdout, stderr)."""
|
||||
shell = type(command) is str
|
||||
p = subprocess.Popen(
|
||||
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=shell
|
||||
)
|
||||
raw_output, raw_err = p.communicate()
|
||||
rc = p.returncode
|
||||
if get_platform() == "win32":
|
||||
enc = "oem"
|
||||
else:
|
||||
enc = locale.getpreferredencoding()
|
||||
output = raw_output.decode(enc)
|
||||
err = raw_err.decode(enc)
|
||||
return rc, output.strip(), err.strip()
|
||||
|
||||
|
||||
def run_and_read_all(run_lambda, command):
|
||||
"""Run command using run_lambda; reads and returns entire output if rc is 0."""
|
||||
rc, out, _ = run_lambda(command)
|
||||
if rc != 0:
|
||||
return None
|
||||
return out
|
||||
|
||||
|
||||
def run_and_parse_first_match(run_lambda, command, regex):
|
||||
"""Run command using run_lambda, returns the first regex match if it exists."""
|
||||
rc, out, _ = run_lambda(command)
|
||||
if rc != 0:
|
||||
return None
|
||||
match = re.search(regex, out)
|
||||
if match is None:
|
||||
return None
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def run_and_return_first_line(run_lambda, command):
|
||||
"""Run command using run_lambda and returns first line if output is not empty."""
|
||||
rc, out, _ = run_lambda(command)
|
||||
if rc != 0:
|
||||
return None
|
||||
return out.split("\n")[0]
|
||||
|
||||
|
||||
def get_conda_packages(run_lambda, patterns=None):
|
||||
if patterns is None:
|
||||
patterns = CONDA_PATTERNS + COMMON_PATTERNS + NVIDIA_PATTERNS + ONEAPI_PATTERNS
|
||||
conda = os.environ.get("CONDA_EXE", "conda")
|
||||
out = run_and_read_all(run_lambda, "{} list".format(conda))
|
||||
if out is None:
|
||||
return out
|
||||
|
||||
return "\n".join(
|
||||
line
|
||||
for line in out.splitlines()
|
||||
if not line.startswith("#") and any(name in line for name in patterns)
|
||||
)
|
||||
|
||||
|
||||
def get_gcc_version(run_lambda):
|
||||
return run_and_parse_first_match(run_lambda, "gcc --version", r"gcc (.*)")
|
||||
|
||||
|
||||
def get_clang_version(run_lambda):
|
||||
return run_and_parse_first_match(
|
||||
run_lambda, "clang --version", r"clang version (.*)"
|
||||
)
|
||||
|
||||
|
||||
def get_cmake_version(run_lambda):
|
||||
return run_and_parse_first_match(run_lambda, "cmake --version", r"cmake (.*)")
|
||||
|
||||
|
||||
def get_nvidia_driver_version(run_lambda):
|
||||
if get_platform() == "darwin":
|
||||
cmd = "kextstat | grep -i cuda"
|
||||
return run_and_parse_first_match(
|
||||
run_lambda, cmd, r"com[.]nvidia[.]CUDA [(](.*?)[)]"
|
||||
)
|
||||
smi = get_nvidia_smi()
|
||||
return run_and_parse_first_match(run_lambda, smi, r"Driver Version: (.*?) ")
|
||||
|
||||
|
||||
def get_gpu_info(run_lambda):
|
||||
if get_platform() == "darwin" or (
|
||||
TORCH_AVAILABLE
|
||||
and hasattr(torch.version, "hip")
|
||||
and torch.version.hip is not None
|
||||
):
|
||||
if TORCH_AVAILABLE and torch.cuda.is_available():
|
||||
if torch.version.hip is not None:
|
||||
prop = torch.cuda.get_device_properties(0)
|
||||
if hasattr(prop, "gcnArchName"):
|
||||
gcnArch = " ({})".format(prop.gcnArchName)
|
||||
else:
|
||||
gcnArch = "NoGCNArchNameOnOldPyTorch"
|
||||
else:
|
||||
gcnArch = ""
|
||||
return torch.cuda.get_device_name(None) + gcnArch
|
||||
return None
|
||||
smi = get_nvidia_smi()
|
||||
uuid_regex = re.compile(r" \(UUID: .+?\)")
|
||||
rc, out, _ = run_lambda(smi + " -L")
|
||||
if rc != 0:
|
||||
return None
|
||||
# Anonymize GPUs by removing their UUID
|
||||
return re.sub(uuid_regex, "", out)
|
||||
|
||||
|
||||
def get_running_cuda_version(run_lambda):
|
||||
return run_and_parse_first_match(run_lambda, "nvcc --version", r"release .+ V(.*)")
|
||||
|
||||
|
||||
def get_cudnn_version(run_lambda):
|
||||
"""Return a list of libcudnn.so; it's hard to tell which one is being used."""
|
||||
if get_platform() == "win32":
|
||||
system_root = os.environ.get("SYSTEMROOT", "C:\\Windows")
|
||||
cuda_path = os.environ.get("CUDA_PATH", "%CUDA_PATH%")
|
||||
where_cmd = os.path.join(system_root, "System32", "where")
|
||||
cudnn_cmd = '{} /R "{}\\bin" cudnn*.dll'.format(where_cmd, cuda_path)
|
||||
elif get_platform() == "darwin":
|
||||
# CUDA libraries and drivers can be found in /usr/local/cuda/. See
|
||||
# https://docs.nvidia.com/cuda/archive/9.0/cuda-installation-guide-mac-os-x/index.html#installation
|
||||
# https://docs.nvidia.com/deeplearning/cudnn/installation/latest/
|
||||
# Use CUDNN_LIBRARY when cudnn library is installed elsewhere.
|
||||
cudnn_cmd = "ls /usr/local/cuda/lib/libcudnn*"
|
||||
else:
|
||||
cudnn_cmd = 'ldconfig -p | grep libcudnn | rev | cut -d" " -f1 | rev'
|
||||
rc, out, _ = run_lambda(cudnn_cmd)
|
||||
# find will return 1 if there are permission errors or if not found
|
||||
if len(out) == 0 or (rc != 1 and rc != 0):
|
||||
l = os.environ.get("CUDNN_LIBRARY")
|
||||
if l is not None and os.path.isfile(l):
|
||||
return os.path.realpath(l)
|
||||
return None
|
||||
files_set = set()
|
||||
for fn in out.split("\n"):
|
||||
fn = os.path.realpath(fn) # eliminate symbolic links
|
||||
if os.path.isfile(fn):
|
||||
files_set.add(fn)
|
||||
if not files_set:
|
||||
return None
|
||||
# Alphabetize the result because the order is non-deterministic otherwise
|
||||
files = sorted(files_set)
|
||||
if len(files) == 1:
|
||||
return files[0]
|
||||
result = "\n".join(files)
|
||||
return "Probably one of the following:\n{}".format(result)
|
||||
|
||||
|
||||
def get_nvidia_smi():
|
||||
# Note: nvidia-smi is currently available only on Windows and Linux
|
||||
smi = "nvidia-smi"
|
||||
if get_platform() == "win32":
|
||||
system_root = os.environ.get("SYSTEMROOT", "C:\\Windows")
|
||||
program_files_root = os.environ.get("PROGRAMFILES", "C:\\Program Files")
|
||||
legacy_path = os.path.join(
|
||||
program_files_root, "NVIDIA Corporation", "NVSMI", smi
|
||||
)
|
||||
new_path = os.path.join(system_root, "System32", smi)
|
||||
smis = [new_path, legacy_path]
|
||||
for candidate_smi in smis:
|
||||
if os.path.exists(candidate_smi):
|
||||
smi = '"{}"'.format(candidate_smi)
|
||||
break
|
||||
return smi
|
||||
|
||||
|
||||
def _detect_linux_pkg_manager():
|
||||
if get_platform() != "linux":
|
||||
return "N/A"
|
||||
for mgr_name in ["dpkg", "dnf", "yum", "zypper"]:
|
||||
rc, _, _ = run(f"which {mgr_name}")
|
||||
if rc == 0:
|
||||
return mgr_name
|
||||
return "N/A"
|
||||
|
||||
|
||||
def get_linux_pkg_version(run_lambda, pkg_name):
|
||||
pkg_mgr = _detect_linux_pkg_manager()
|
||||
if pkg_mgr == "N/A":
|
||||
return "N/A"
|
||||
|
||||
grep_version = {
|
||||
"dpkg": {
|
||||
"field_index": 2,
|
||||
"command": "dpkg -l | grep {}",
|
||||
},
|
||||
"dnf": {
|
||||
"field_index": 1,
|
||||
"command": "dnf list | grep {}",
|
||||
},
|
||||
"yum": {
|
||||
"field_index": 1,
|
||||
"command": "yum list | grep {}",
|
||||
},
|
||||
"zypper": {
|
||||
"field_index": 2,
|
||||
"command": "zypper info {} | grep Version",
|
||||
},
|
||||
}
|
||||
|
||||
field_index: int = int(_cast(int, grep_version[pkg_mgr]["field_index"]))
|
||||
cmd: str = str(grep_version[pkg_mgr]["command"])
|
||||
cmd = cmd.format(pkg_name)
|
||||
ret = run_and_read_all(run_lambda, cmd)
|
||||
if ret is None or ret == "":
|
||||
return "N/A"
|
||||
lst = re.sub(" +", " ", ret).split(" ")
|
||||
if len(lst) <= field_index:
|
||||
return "N/A"
|
||||
return lst[field_index]
|
||||
|
||||
|
||||
def get_intel_gpu_driver_version(run_lambda):
|
||||
lst = []
|
||||
platform = get_platform()
|
||||
if platform == "linux":
|
||||
pkgs = { # type: ignore[var-annotated]
|
||||
"dpkg": {
|
||||
"intel-opencl-icd",
|
||||
"libze1",
|
||||
"level-zero",
|
||||
},
|
||||
"dnf": {
|
||||
"intel-opencl",
|
||||
"level-zero",
|
||||
},
|
||||
"yum": {
|
||||
"intel-opencl",
|
||||
"level-zero",
|
||||
},
|
||||
"zypper": {
|
||||
"intel-opencl",
|
||||
"level-zero",
|
||||
},
|
||||
}.get(_detect_linux_pkg_manager(), {})
|
||||
for pkg in pkgs:
|
||||
ver = get_linux_pkg_version(run_lambda, pkg)
|
||||
if ver != "N/A":
|
||||
lst.append(f"* {pkg}:\t{ver}")
|
||||
if platform in ["win32", "cygwin"]:
|
||||
txt = run_and_read_all(
|
||||
run_lambda,
|
||||
'powershell.exe "gwmi -Class Win32_PnpSignedDriver | where{$_.DeviceClass -eq \\"DISPLAY\\"\
|
||||
-and $_.Manufacturer -match \\"Intel\\"} | Select-Object -Property DeviceName,DriverVersion,DriverDate\
|
||||
| ConvertTo-Json"',
|
||||
)
|
||||
try:
|
||||
obj = json.loads(txt)
|
||||
if type(obj) is list:
|
||||
for o in obj:
|
||||
lst.append(
|
||||
f'* {o["DeviceName"]}: {o["DriverVersion"]} ({o["DriverDate"]})'
|
||||
)
|
||||
else:
|
||||
lst.append(f'* {obj["DriverVersion"]} ({obj["DriverDate"]})')
|
||||
except ValueError as e:
|
||||
lst.append(txt)
|
||||
lst.append(str(e))
|
||||
return "\n".join(lst)
|
||||
|
||||
|
||||
def get_intel_gpu_onboard(run_lambda):
|
||||
lst: list[str] = []
|
||||
platform = get_platform()
|
||||
if platform == "linux":
|
||||
txt = run_and_read_all(run_lambda, "xpu-smi discovery -j")
|
||||
if txt:
|
||||
try:
|
||||
obj = json.loads(txt)
|
||||
device_list = obj.get("device_list", [])
|
||||
if isinstance(device_list, list) and device_list:
|
||||
lst.extend(f'* {device["device_name"]}' for device in device_list)
|
||||
else:
|
||||
lst.append("N/A")
|
||||
except (ValueError, TypeError) as e:
|
||||
lst.append(txt)
|
||||
lst.append(str(e))
|
||||
else:
|
||||
lst.append("N/A")
|
||||
if platform in ["win32", "cygwin"]:
|
||||
txt = run_and_read_all(
|
||||
run_lambda,
|
||||
'powershell.exe "gwmi -Class Win32_PnpSignedDriver | where{$_.DeviceClass -eq \\"DISPLAY\\"\
|
||||
-and $_.Manufacturer -match \\"Intel\\"} | Select-Object -Property DeviceName | ConvertTo-Json"',
|
||||
)
|
||||
if txt:
|
||||
try:
|
||||
obj = json.loads(txt)
|
||||
if isinstance(obj, list) and obj:
|
||||
lst.extend(f'* {device["DeviceName"]}' for device in obj)
|
||||
else:
|
||||
lst.append(f'* {obj.get("DeviceName", "N/A")}')
|
||||
except ValueError as e:
|
||||
lst.append(txt)
|
||||
lst.append(str(e))
|
||||
else:
|
||||
lst.append("N/A")
|
||||
return "\n".join(lst)
|
||||
|
||||
|
||||
def get_intel_gpu_detected(run_lambda):
|
||||
if not TORCH_AVAILABLE or not hasattr(torch, "xpu"):
|
||||
return "N/A"
|
||||
|
||||
device_count = torch.xpu.device_count()
|
||||
if device_count == 0:
|
||||
return "N/A"
|
||||
|
||||
devices = [
|
||||
f"* [{i}] {torch.xpu.get_device_properties(i)}" for i in range(device_count)
|
||||
]
|
||||
return "\n".join(devices)
|
||||
|
||||
|
||||
# example outputs of CPU infos
|
||||
# * linux
|
||||
# Architecture: x86_64
|
||||
# CPU op-mode(s): 32-bit, 64-bit
|
||||
# Address sizes: 46 bits physical, 48 bits virtual
|
||||
# Byte Order: Little Endian
|
||||
# CPU(s): 128
|
||||
# On-line CPU(s) list: 0-127
|
||||
# Vendor ID: GenuineIntel
|
||||
# Model name: Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz
|
||||
# CPU family: 6
|
||||
# Model: 106
|
||||
# Thread(s) per core: 2
|
||||
# Core(s) per socket: 32
|
||||
# Socket(s): 2
|
||||
# Stepping: 6
|
||||
# BogoMIPS: 5799.78
|
||||
# Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr
|
||||
# sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon rep_good nopl
|
||||
# xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq monitor ssse3 fma cx16
|
||||
# pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand
|
||||
# hypervisor lahf_lm abm 3dnowprefetch invpcid_single ssbd ibrs ibpb stibp ibrs_enhanced
|
||||
# fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid avx512f avx512dq rdseed adx smap
|
||||
# avx512ifma clflushopt clwb avx512cd sha_ni avx512bw avx512vl xsaveopt xsavec xgetbv1
|
||||
# xsaves wbnoinvd ida arat avx512vbmi pku ospke avx512_vbmi2 gfni vaes vpclmulqdq
|
||||
# avx512_vnni avx512_bitalg tme avx512_vpopcntdq rdpid md_clear flush_l1d arch_capabilities
|
||||
# Virtualization features:
|
||||
# Hypervisor vendor: KVM
|
||||
# Virtualization type: full
|
||||
# Caches (sum of all):
|
||||
# L1d: 3 MiB (64 instances)
|
||||
# L1i: 2 MiB (64 instances)
|
||||
# L2: 80 MiB (64 instances)
|
||||
# L3: 108 MiB (2 instances)
|
||||
# NUMA:
|
||||
# NUMA node(s): 2
|
||||
# NUMA node0 CPU(s): 0-31,64-95
|
||||
# NUMA node1 CPU(s): 32-63,96-127
|
||||
# Vulnerabilities:
|
||||
# Itlb multihit: Not affected
|
||||
# L1tf: Not affected
|
||||
# Mds: Not affected
|
||||
# Meltdown: Not affected
|
||||
# Mmio stale data: Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown
|
||||
# Retbleed: Not affected
|
||||
# Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl and seccomp
|
||||
# Spectre v1: Mitigation; usercopy/swapgs barriers and __user pointer sanitization
|
||||
# Spectre v2: Mitigation; Enhanced IBRS, IBPB conditional, RSB filling, PBRSB-eIBRS SW sequence
|
||||
# Srbds: Not affected
|
||||
# Tsx async abort: Not affected
|
||||
# * win32
|
||||
# Architecture=9
|
||||
# CurrentClockSpeed=2900
|
||||
# DeviceID=CPU0
|
||||
# Family=179
|
||||
# L2CacheSize=40960
|
||||
# L2CacheSpeed=
|
||||
# Manufacturer=GenuineIntel
|
||||
# MaxClockSpeed=2900
|
||||
# Name=Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz
|
||||
# ProcessorType=3
|
||||
# Revision=27142
|
||||
#
|
||||
# Architecture=9
|
||||
# CurrentClockSpeed=2900
|
||||
# DeviceID=CPU1
|
||||
# Family=179
|
||||
# L2CacheSize=40960
|
||||
# L2CacheSpeed=
|
||||
# Manufacturer=GenuineIntel
|
||||
# MaxClockSpeed=2900
|
||||
# Name=Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz
|
||||
# ProcessorType=3
|
||||
# Revision=27142
|
||||
|
||||
|
||||
def get_cpu_info(run_lambda):
|
||||
rc, out, err = 0, "", ""
|
||||
if get_platform() == "linux":
|
||||
rc, out, err = run_lambda("lscpu")
|
||||
elif get_platform() == "win32":
|
||||
rc, out, err = run_lambda(
|
||||
'powershell.exe "gwmi -Class Win32_Processor | Select-Object -Property Name,Manufacturer,Family,\
|
||||
Architecture,ProcessorType,DeviceID,CurrentClockSpeed,MaxClockSpeed,L2CacheSize,L2CacheSpeed,Revision\
|
||||
| ConvertTo-Json"'
|
||||
)
|
||||
if rc == 0:
|
||||
lst = []
|
||||
try:
|
||||
obj = json.loads(out)
|
||||
if type(obj) is list:
|
||||
for o in obj:
|
||||
lst.append("----------------------")
|
||||
lst.extend([f"{k}: {v}" for (k, v) in o.items()])
|
||||
else:
|
||||
lst.extend([f"{k}: {v}" for (k, v) in obj.items()])
|
||||
except ValueError as e:
|
||||
lst.append(out)
|
||||
lst.append(str(e))
|
||||
out = "\n".join(lst)
|
||||
elif get_platform() == "darwin":
|
||||
rc, out, err = run_lambda("sysctl -n machdep.cpu.brand_string")
|
||||
cpu_info = "None"
|
||||
if rc == 0:
|
||||
cpu_info = out
|
||||
else:
|
||||
cpu_info = err
|
||||
return cpu_info
|
||||
|
||||
|
||||
def get_platform():
|
||||
if sys.platform.startswith("linux"):
|
||||
return "linux"
|
||||
elif sys.platform.startswith("win32"):
|
||||
return "win32"
|
||||
elif sys.platform.startswith("cygwin"):
|
||||
return "cygwin"
|
||||
elif sys.platform.startswith("darwin"):
|
||||
return "darwin"
|
||||
else:
|
||||
return sys.platform
|
||||
|
||||
|
||||
def get_mac_version(run_lambda):
|
||||
return run_and_parse_first_match(run_lambda, "sw_vers -productVersion", r"(.*)")
|
||||
|
||||
|
||||
def get_windows_version(run_lambda):
|
||||
ret = run_and_read_all(
|
||||
run_lambda,
|
||||
'powershell.exe "gwmi -Class Win32_OperatingSystem | Select-Object -Property Caption,\
|
||||
OSArchitecture,Version | ConvertTo-Json"',
|
||||
)
|
||||
try:
|
||||
obj = json.loads(ret)
|
||||
ret = f'{obj["Caption"]} ({obj["Version"]} {obj["OSArchitecture"]})'
|
||||
except ValueError as e:
|
||||
ret += f"\n{str(e)}"
|
||||
return ret
|
||||
|
||||
|
||||
def get_lsb_version(run_lambda):
|
||||
return run_and_parse_first_match(
|
||||
run_lambda, "lsb_release -a", r"Description:\t(.*)"
|
||||
)
|
||||
|
||||
|
||||
def check_release_file(run_lambda):
|
||||
return run_and_parse_first_match(
|
||||
run_lambda, "cat /etc/*-release", r'PRETTY_NAME="(.*)"'
|
||||
)
|
||||
|
||||
|
||||
def get_os(run_lambda):
|
||||
from platform import machine
|
||||
|
||||
platform = get_platform()
|
||||
|
||||
if platform in ["win32", "cygwin"]:
|
||||
return get_windows_version(run_lambda)
|
||||
|
||||
if platform == "darwin":
|
||||
version = get_mac_version(run_lambda)
|
||||
if version is None:
|
||||
return None
|
||||
return "macOS {} ({})".format(version, machine())
|
||||
|
||||
if platform == "linux":
|
||||
# Ubuntu/Debian based
|
||||
desc = get_lsb_version(run_lambda)
|
||||
if desc is not None:
|
||||
return "{} ({})".format(desc, machine())
|
||||
|
||||
# Try reading /etc/*-release
|
||||
desc = check_release_file(run_lambda)
|
||||
if desc is not None:
|
||||
return "{} ({})".format(desc, machine())
|
||||
|
||||
return "{} ({})".format(platform, machine())
|
||||
|
||||
# Unknown platform
|
||||
return platform
|
||||
|
||||
|
||||
def get_python_platform():
|
||||
import platform
|
||||
|
||||
return platform.platform()
|
||||
|
||||
|
||||
def get_libc_version():
|
||||
import platform
|
||||
|
||||
if get_platform() != "linux":
|
||||
return "N/A"
|
||||
return "-".join(platform.libc_ver())
|
||||
|
||||
|
||||
def get_pip_packages(run_lambda, patterns=None):
|
||||
"""Return `pip list` output. Note: will also find conda-installed pytorch and numpy packages."""
|
||||
if patterns is None:
|
||||
patterns = PIP_PATTERNS + COMMON_PATTERNS + NVIDIA_PATTERNS + ONEAPI_PATTERNS
|
||||
|
||||
pip_version = "pip3" if sys.version_info.major == 3 else "pip"
|
||||
|
||||
os.environ["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
|
||||
# People generally have pip as `pip` or `pip3`
|
||||
# But here it is invoked as `python -mpip`
|
||||
out = run_and_read_all(
|
||||
run_lambda, [sys.executable, "-mpip", "list", "--format=freeze"]
|
||||
)
|
||||
if out is None:
|
||||
return pip_version, out
|
||||
|
||||
filtered_out = "\n".join(
|
||||
line for line in out.splitlines() if any(name in line for name in patterns)
|
||||
)
|
||||
|
||||
return pip_version, filtered_out
|
||||
|
||||
|
||||
def get_cachingallocator_config() -> _Dict[str, str]:
|
||||
"""Return the caching allocator configuration from environment variables.
|
||||
"""
|
||||
# pyrefly: ignore [bad-return]
|
||||
return {
|
||||
var: os.environ.get(var)
|
||||
for var in (
|
||||
"PYTORCH_CUDA_ALLOC_CONF",
|
||||
"PYTORCH_HIP_ALLOC_CONF",
|
||||
"PYTORCH_ALLOC_CONF",
|
||||
)
|
||||
if os.environ.get(var)
|
||||
}
|
||||
|
||||
|
||||
def get_cuda_module_loading_config():
|
||||
if TORCH_AVAILABLE and torch.cuda.is_available():
|
||||
torch.cuda.init()
|
||||
config = os.environ.get("CUDA_MODULE_LOADING", "")
|
||||
return config
|
||||
else:
|
||||
return "N/A"
|
||||
|
||||
|
||||
def is_xnnpack_available():
|
||||
if TORCH_AVAILABLE:
|
||||
import torch.backends.xnnpack
|
||||
|
||||
return str(torch.backends.xnnpack.enabled) # type: ignore[attr-defined]
|
||||
else:
|
||||
return "N/A"
|
||||
|
||||
|
||||
def get_env_info():
|
||||
"""
|
||||
Collects environment information to aid in debugging.
|
||||
|
||||
The returned environment information contains details on torch version, is debug build
|
||||
or not, cuda compiled version, gcc version, clang version, cmake version, operating
|
||||
system, libc version, python version, python platform, CUDA availability, CUDA
|
||||
runtime version, CUDA module loading config, GPU model and configuration, Nvidia
|
||||
driver version, cuDNN version, pip version and versions of relevant pip and
|
||||
conda packages, HIP runtime version, MIOpen runtime version,
|
||||
Caching allocator config, XNNPACK availability and CPU information.
|
||||
|
||||
Returns:
|
||||
SystemEnv (namedtuple): A tuple containing various environment details
|
||||
and system information.
|
||||
"""
|
||||
run_lambda = run
|
||||
pip_version, pip_list_output = get_pip_packages(run_lambda)
|
||||
|
||||
if TORCH_AVAILABLE:
|
||||
version_str = torch.__version__
|
||||
debug_mode_str = str(torch.version.debug)
|
||||
cuda_available_str = str(torch.cuda.is_available())
|
||||
cuda_version_str = torch.version.cuda
|
||||
xpu_available_str = str(torch.xpu.is_available())
|
||||
if torch.xpu.is_available():
|
||||
xpu_available_str = (
|
||||
f"{xpu_available_str}\n"
|
||||
+ f"XPU used to build PyTorch: {torch.version.xpu}\n"
|
||||
+ f"Intel GPU driver version:\n{get_intel_gpu_driver_version(run_lambda)}\n"
|
||||
+ f"Intel GPU models onboard:\n{get_intel_gpu_onboard(run_lambda)}\n"
|
||||
+ f"Intel GPU models detected:\n{get_intel_gpu_detected(run_lambda)}"
|
||||
)
|
||||
if (
|
||||
not hasattr(torch.version, "hip") or torch.version.hip is None
|
||||
): # cuda version
|
||||
hip_compiled_version = hip_runtime_version = miopen_runtime_version = "N/A"
|
||||
else: # HIP version
|
||||
|
||||
def get_version_or_na(cfg, prefix):
|
||||
_lst = [s.rsplit(None, 1)[-1] for s in cfg if prefix in s]
|
||||
return _lst[0] if _lst else "N/A"
|
||||
|
||||
cfg = torch._C._show_config().split("\n")
|
||||
hip_runtime_version = get_version_or_na(cfg, "HIP Runtime")
|
||||
miopen_runtime_version = get_version_or_na(cfg, "MIOpen")
|
||||
cuda_version_str = "N/A"
|
||||
hip_compiled_version = torch.version.hip
|
||||
else:
|
||||
version_str = debug_mode_str = cuda_available_str = cuda_version_str = xpu_available_str = "N/A" # type: ignore[assignment]
|
||||
hip_compiled_version = hip_runtime_version = miopen_runtime_version = "N/A"
|
||||
|
||||
sys_version = sys.version.replace("\n", " ")
|
||||
|
||||
conda_packages = get_conda_packages(run_lambda)
|
||||
|
||||
return SystemEnv(
|
||||
torch_version=version_str,
|
||||
is_debug_build=debug_mode_str,
|
||||
python_version="{} ({}-bit runtime)".format(
|
||||
sys_version, sys.maxsize.bit_length() + 1
|
||||
),
|
||||
python_platform=get_python_platform(),
|
||||
is_cuda_available=cuda_available_str,
|
||||
cuda_compiled_version=cuda_version_str,
|
||||
cuda_runtime_version=get_running_cuda_version(run_lambda),
|
||||
cuda_module_loading=get_cuda_module_loading_config(),
|
||||
nvidia_gpu_models=get_gpu_info(run_lambda),
|
||||
nvidia_driver_version=get_nvidia_driver_version(run_lambda),
|
||||
cudnn_version=get_cudnn_version(run_lambda),
|
||||
is_xpu_available=xpu_available_str,
|
||||
hip_compiled_version=hip_compiled_version,
|
||||
hip_runtime_version=hip_runtime_version,
|
||||
miopen_runtime_version=miopen_runtime_version,
|
||||
pip_version=pip_version,
|
||||
pip_packages=pip_list_output,
|
||||
conda_packages=conda_packages,
|
||||
os=get_os(run_lambda),
|
||||
libc_version=get_libc_version(),
|
||||
gcc_version=get_gcc_version(run_lambda),
|
||||
clang_version=get_clang_version(run_lambda),
|
||||
cmake_version=get_cmake_version(run_lambda),
|
||||
caching_allocator_config=get_cachingallocator_config(),
|
||||
is_xnnpack_available=is_xnnpack_available(),
|
||||
cpu_info=get_cpu_info(run_lambda),
|
||||
)
|
||||
|
||||
|
||||
env_info_fmt = """
|
||||
PyTorch version: {torch_version}
|
||||
Is debug build: {is_debug_build}
|
||||
CUDA used to build PyTorch: {cuda_compiled_version}
|
||||
ROCM used to build PyTorch: {hip_compiled_version}
|
||||
|
||||
OS: {os}
|
||||
GCC version: {gcc_version}
|
||||
Clang version: {clang_version}
|
||||
CMake version: {cmake_version}
|
||||
Libc version: {libc_version}
|
||||
|
||||
Python version: {python_version}
|
||||
Python platform: {python_platform}
|
||||
Is CUDA available: {is_cuda_available}
|
||||
CUDA runtime version: {cuda_runtime_version}
|
||||
CUDA_MODULE_LOADING set to: {cuda_module_loading}
|
||||
GPU models and configuration: {nvidia_gpu_models}
|
||||
Nvidia driver version: {nvidia_driver_version}
|
||||
cuDNN version: {cudnn_version}
|
||||
Is XPU available: {is_xpu_available}
|
||||
HIP runtime version: {hip_runtime_version}
|
||||
MIOpen runtime version: {miopen_runtime_version}
|
||||
Is XNNPACK available: {is_xnnpack_available}
|
||||
Caching allocator config: {caching_allocator_config}
|
||||
|
||||
CPU:
|
||||
{cpu_info}
|
||||
|
||||
Versions of relevant libraries:
|
||||
{pip_packages}
|
||||
{conda_packages}
|
||||
""".strip()
|
||||
|
||||
|
||||
def pretty_str(envinfo):
|
||||
def replace_nones(dct, replacement="Could not collect"):
|
||||
for key in dct:
|
||||
if dct[key] is not None:
|
||||
continue
|
||||
dct[key] = replacement
|
||||
return dct
|
||||
|
||||
def replace_bools(dct, true="Yes", false="No"):
|
||||
for key in dct:
|
||||
if dct[key] is True:
|
||||
dct[key] = true
|
||||
elif dct[key] is False:
|
||||
dct[key] = false
|
||||
return dct
|
||||
|
||||
def prepend(text, tag="[prepend]"):
|
||||
lines = text.split("\n")
|
||||
updated_lines = [tag + line for line in lines]
|
||||
return "\n".join(updated_lines)
|
||||
|
||||
def replace_if_empty(text, replacement="No relevant packages"):
|
||||
if text is not None and len(text) == 0:
|
||||
return replacement
|
||||
return text
|
||||
|
||||
def maybe_start_on_next_line(string):
|
||||
# If `string` is multiline, prepend a \n to it.
|
||||
if string is not None and len(string.split("\n")) > 1:
|
||||
return "\n{}\n".format(string)
|
||||
return string
|
||||
|
||||
mutable_dict = envinfo._asdict()
|
||||
|
||||
# If nvidia_gpu_models is multiline, start on the next line
|
||||
mutable_dict["nvidia_gpu_models"] = maybe_start_on_next_line(
|
||||
envinfo.nvidia_gpu_models
|
||||
)
|
||||
|
||||
# If the machine doesn't have CUDA, report some fields as 'No CUDA'
|
||||
dynamic_cuda_fields = [
|
||||
"cuda_runtime_version",
|
||||
"nvidia_gpu_models",
|
||||
"nvidia_driver_version",
|
||||
]
|
||||
all_cuda_fields = dynamic_cuda_fields + ["cudnn_version"]
|
||||
all_dynamic_cuda_fields_missing = all(
|
||||
mutable_dict[field] is None for field in dynamic_cuda_fields
|
||||
)
|
||||
if (
|
||||
TORCH_AVAILABLE
|
||||
and not torch.cuda.is_available()
|
||||
and all_dynamic_cuda_fields_missing
|
||||
):
|
||||
for field in all_cuda_fields:
|
||||
mutable_dict[field] = "No CUDA"
|
||||
if envinfo.cuda_compiled_version is None:
|
||||
mutable_dict["cuda_compiled_version"] = "None"
|
||||
|
||||
# Replace True with Yes, False with No
|
||||
mutable_dict = replace_bools(mutable_dict)
|
||||
|
||||
# Replace all None objects with 'Could not collect'
|
||||
mutable_dict = replace_nones(mutable_dict)
|
||||
|
||||
# If either of these are '', replace with 'No relevant packages'
|
||||
mutable_dict["pip_packages"] = replace_if_empty(mutable_dict["pip_packages"])
|
||||
mutable_dict["conda_packages"] = replace_if_empty(mutable_dict["conda_packages"])
|
||||
|
||||
# Tag conda and pip packages with a prefix
|
||||
# If they were previously None, they'll show up as ie '[conda] Could not collect'
|
||||
if mutable_dict["pip_packages"]:
|
||||
mutable_dict["pip_packages"] = prepend(
|
||||
mutable_dict["pip_packages"], "[{}] ".format(envinfo.pip_version)
|
||||
)
|
||||
if mutable_dict["conda_packages"]:
|
||||
mutable_dict["conda_packages"] = prepend(
|
||||
mutable_dict["conda_packages"], "[conda] "
|
||||
)
|
||||
mutable_dict["cpu_info"] = envinfo.cpu_info
|
||||
mutable_dict["caching_allocator_config"] = envinfo.caching_allocator_config
|
||||
if not envinfo.caching_allocator_config:
|
||||
mutable_dict["caching_allocator_config"] = "N/A"
|
||||
return env_info_fmt.format(**mutable_dict)
|
||||
|
||||
|
||||
def get_pretty_env_info():
|
||||
"""
|
||||
Returns a pretty string of environment information.
|
||||
|
||||
This function retrieves environment information by calling the `get_env_info` function
|
||||
and then formats the information into a human-readable string. The retrieved environment
|
||||
information is listed in the document of `get_env_info`.
|
||||
This function is used in `python collect_env.py` that should be executed when reporting a bug.
|
||||
|
||||
Returns:
|
||||
str: A pretty string of the environment information.
|
||||
"""
|
||||
return pretty_str(get_env_info())
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("Collecting environment information...")
|
||||
output = get_pretty_env_info()
|
||||
print(output)
|
||||
|
||||
if (
|
||||
TORCH_AVAILABLE
|
||||
and hasattr(torch, "utils")
|
||||
and hasattr(torch.utils, "_crash_handler")
|
||||
):
|
||||
minidump_dir = torch.utils._crash_handler.DEFAULT_MINIDUMP_DIR
|
||||
if sys.platform == "linux" and os.path.exists(minidump_dir):
|
||||
dumps = [
|
||||
os.path.join(minidump_dir, dump) for dump in os.listdir(minidump_dir)
|
||||
]
|
||||
latest = max(dumps, key=os.path.getctime)
|
||||
ctime = os.path.getctime(latest)
|
||||
creation_time = datetime.datetime.fromtimestamp(ctime).strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
msg = (
|
||||
"\n*** Detected a minidump at {} created on {}, ".format(
|
||||
latest, creation_time
|
||||
)
|
||||
+ "if this is related to your bug please include it when you file a report ***"
|
||||
)
|
||||
print(msg, file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,12 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from torch._C import _get_cpp_backtrace
|
||||
|
||||
def get_cpp_backtrace(frames_to_skip=0, maximum_number_of_frames=64) -> str:
|
||||
r"""
|
||||
Return a string containing the C++ stack trace of the current thread.
|
||||
|
||||
Args:
|
||||
frames_to_skip (int): the number of frames to skip from the top of the stack
|
||||
maximum_number_of_frames (int): the maximum number of frames to return
|
||||
"""
|
||||
return _get_cpp_backtrace(frames_to_skip, maximum_number_of_frames)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,78 @@
|
||||
from torch.utils.data.dataloader import (
|
||||
_DatasetKind,
|
||||
DataLoader,
|
||||
default_collate,
|
||||
default_convert,
|
||||
get_worker_info,
|
||||
)
|
||||
from torch.utils.data.datapipes._decorator import (
|
||||
argument_validation,
|
||||
functional_datapipe,
|
||||
guaranteed_datapipes_determinism,
|
||||
non_deterministic,
|
||||
runtime_validation,
|
||||
runtime_validation_disabled,
|
||||
)
|
||||
from torch.utils.data.datapipes.datapipe import (
|
||||
DataChunk,
|
||||
DFIterDataPipe,
|
||||
IterDataPipe,
|
||||
MapDataPipe,
|
||||
)
|
||||
from torch.utils.data.dataset import (
|
||||
ChainDataset,
|
||||
ConcatDataset,
|
||||
Dataset,
|
||||
IterableDataset,
|
||||
random_split,
|
||||
StackDataset,
|
||||
Subset,
|
||||
TensorDataset,
|
||||
)
|
||||
from torch.utils.data.distributed import DistributedSampler
|
||||
from torch.utils.data.sampler import (
|
||||
BatchSampler,
|
||||
RandomSampler,
|
||||
Sampler,
|
||||
SequentialSampler,
|
||||
SubsetRandomSampler,
|
||||
WeightedRandomSampler,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BatchSampler",
|
||||
"ChainDataset",
|
||||
"ConcatDataset",
|
||||
"DFIterDataPipe",
|
||||
"DataChunk",
|
||||
"DataLoader",
|
||||
"Dataset",
|
||||
"DistributedSampler",
|
||||
"IterDataPipe",
|
||||
"IterableDataset",
|
||||
"MapDataPipe",
|
||||
"RandomSampler",
|
||||
"Sampler",
|
||||
"SequentialSampler",
|
||||
"StackDataset",
|
||||
"Subset",
|
||||
"SubsetRandomSampler",
|
||||
"TensorDataset",
|
||||
"WeightedRandomSampler",
|
||||
"_DatasetKind",
|
||||
"argument_validation",
|
||||
"default_collate",
|
||||
"default_convert",
|
||||
"functional_datapipe",
|
||||
"get_worker_info",
|
||||
"guaranteed_datapipes_determinism",
|
||||
"non_deterministic",
|
||||
"random_split",
|
||||
"runtime_validation",
|
||||
"runtime_validation_disabled",
|
||||
]
|
||||
|
||||
# Please keep this list sorted
|
||||
if __all__ != sorted(__all__):
|
||||
raise AssertionError("__all__ is not sorted")
|
||||
@@ -0,0 +1,53 @@
|
||||
r"""Utility classes & functions for data loading. Code in this folder is mostly used by ../dataloder.py.
|
||||
|
||||
A lot of multiprocessing is used in data loading, which only supports running
|
||||
functions defined in global environment (py2 can't serialize static methods).
|
||||
Therefore, for code tidiness we put these functions into different files in this
|
||||
folder.
|
||||
"""
|
||||
|
||||
import atexit
|
||||
import sys
|
||||
|
||||
# old private location of the ExceptionWrapper that some users rely on:
|
||||
from torch._utils import ExceptionWrapper
|
||||
|
||||
|
||||
IS_WINDOWS = sys.platform == "win32"
|
||||
|
||||
|
||||
MP_STATUS_CHECK_INTERVAL = 5.0
|
||||
r"""Interval (in seconds) to check status of processes to avoid hanging in
|
||||
multiprocessing data loading. This is mainly used in getting data from
|
||||
another process, in which case we need to periodically check whether the
|
||||
sender is alive to prevent hanging."""
|
||||
|
||||
|
||||
python_exit_status = False
|
||||
r"""Whether Python is shutting down. This flag is guaranteed to be set before
|
||||
the Python core library resources are freed, but Python may already be exiting
|
||||
for some time when this is set.
|
||||
|
||||
Hook to set this flag is `_set_python_exit_flag`, and is inspired by a similar
|
||||
hook in Python 3.7 multiprocessing library:
|
||||
https://github.com/python/cpython/blob/d4d60134b29290049e28df54f23493de4f1824b6/Lib/multiprocessing/util.py#L277-L327
|
||||
"""
|
||||
|
||||
|
||||
try:
|
||||
import numpy
|
||||
|
||||
HAS_NUMPY = True
|
||||
except ModuleNotFoundError:
|
||||
HAS_NUMPY = False
|
||||
|
||||
|
||||
def _set_python_exit_flag() -> None:
|
||||
global python_exit_status
|
||||
python_exit_status = True
|
||||
|
||||
|
||||
atexit.register(_set_python_exit_flag)
|
||||
|
||||
|
||||
from . import collate, fetch, pin_memory, signal_handling, worker
|
||||
@@ -0,0 +1,401 @@
|
||||
# mypy: allow-untyped-defs
|
||||
r"""Contains definitions of the methods used by the _BaseDataLoaderIter workers.
|
||||
|
||||
These methods are used to collate samples fetched from dataset into Tensor(s).
|
||||
These **needs** to be in global scope since Py2 doesn't support serializing
|
||||
static methods.
|
||||
|
||||
`default_collate` and `default_convert` are exposed to users via 'dataloader.py'.
|
||||
"""
|
||||
|
||||
import collections
|
||||
import contextlib
|
||||
import copy
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
np_str_obj_array_pattern = re.compile(r"[SaUO]")
|
||||
|
||||
|
||||
def default_convert(data):
|
||||
r"""
|
||||
Convert each NumPy array element into a :class:`torch.Tensor`.
|
||||
|
||||
If the input is a `Sequence`, `Collection`, or `Mapping`, it tries to convert each element inside to a :class:`torch.Tensor`.
|
||||
If the input is not an NumPy array, it is left unchanged.
|
||||
This is used as the default function for collation when both `batch_sampler` and `batch_size`
|
||||
are NOT defined in :class:`~torch.utils.data.DataLoader`.
|
||||
|
||||
The general input type to output type mapping is similar to that
|
||||
of :func:`~torch.utils.data.default_collate`. See the description there for more details.
|
||||
|
||||
Args:
|
||||
data: a single data point to be converted
|
||||
|
||||
Examples:
|
||||
>>> # xdoctest: +SKIP
|
||||
>>> # Example with `int`
|
||||
>>> default_convert(0)
|
||||
0
|
||||
>>> # Example with NumPy array
|
||||
>>> default_convert(np.array([0, 1]))
|
||||
tensor([0, 1])
|
||||
>>> # Example with NamedTuple
|
||||
>>> Point = namedtuple("Point", ["x", "y"])
|
||||
>>> default_convert(Point(0, 0))
|
||||
Point(x=0, y=0)
|
||||
>>> default_convert(Point(np.array(0), np.array(0)))
|
||||
Point(x=tensor(0), y=tensor(0))
|
||||
>>> # Example with List
|
||||
>>> default_convert([np.array([0, 1]), np.array([2, 3])])
|
||||
[tensor([0, 1]), tensor([2, 3])]
|
||||
"""
|
||||
elem_type = type(data)
|
||||
if isinstance(data, torch.Tensor):
|
||||
return data
|
||||
elif (
|
||||
elem_type.__module__ == "numpy"
|
||||
and elem_type.__name__ != "str_"
|
||||
and elem_type.__name__ != "string_"
|
||||
):
|
||||
# array of string classes and object
|
||||
if (
|
||||
elem_type.__name__ == "ndarray"
|
||||
and np_str_obj_array_pattern.search(data.dtype.str) is not None
|
||||
):
|
||||
return data
|
||||
return torch.as_tensor(data)
|
||||
elif isinstance(data, collections.abc.Mapping):
|
||||
try:
|
||||
if isinstance(data, collections.abc.MutableMapping):
|
||||
# The mapping type may have extra properties, so we can't just
|
||||
# use `type(data)(...)` to create the new mapping.
|
||||
# Create a clone and update it if the mapping type is mutable.
|
||||
clone = copy.copy(data)
|
||||
clone.update({key: default_convert(data[key]) for key in data})
|
||||
return clone
|
||||
else:
|
||||
return elem_type({key: default_convert(data[key]) for key in data})
|
||||
except TypeError:
|
||||
# The mapping type may not support `copy()` / `update(mapping)`
|
||||
# or `__init__(iterable)`.
|
||||
return {key: default_convert(data[key]) for key in data}
|
||||
elif isinstance(data, tuple) and hasattr(data, "_fields"): # namedtuple
|
||||
return elem_type(*(default_convert(d) for d in data))
|
||||
elif isinstance(data, tuple):
|
||||
return [default_convert(d) for d in data] # Backwards compatibility.
|
||||
elif isinstance(data, collections.abc.Sequence) and not isinstance(
|
||||
data, (str, bytes)
|
||||
):
|
||||
try:
|
||||
if isinstance(data, collections.abc.MutableSequence):
|
||||
# The sequence type may have extra properties, so we can't just
|
||||
# use `type(data)(...)` to create the new sequence.
|
||||
# Create a clone and update it if the sequence type is mutable.
|
||||
clone = copy.copy(data) # type: ignore[arg-type]
|
||||
for i, d in enumerate(data):
|
||||
clone[i] = default_convert(d)
|
||||
return clone
|
||||
else:
|
||||
return elem_type([default_convert(d) for d in data])
|
||||
except TypeError:
|
||||
# The sequence type may not support `copy()` / `__setitem__(index, item)`
|
||||
# or `__init__(iterable)` (e.g., `range`).
|
||||
return [default_convert(d) for d in data]
|
||||
else:
|
||||
return data
|
||||
|
||||
|
||||
default_collate_err_msg_format = (
|
||||
"default_collate: batch must contain tensors, numpy arrays, numbers, "
|
||||
"dicts or lists; found {}"
|
||||
)
|
||||
|
||||
|
||||
def collate(
|
||||
batch,
|
||||
*,
|
||||
collate_fn_map: dict[type | tuple[type, ...], Callable] | None = None,
|
||||
):
|
||||
r"""
|
||||
General collate function that handles collection type of element within each batch.
|
||||
|
||||
The function also opens function registry to deal with specific element types. `default_collate_fn_map`
|
||||
provides default collate functions for tensors, numpy arrays, numbers and strings.
|
||||
|
||||
Args:
|
||||
batch: a single batch to be collated
|
||||
collate_fn_map: Optional dictionary mapping from element type to the corresponding collate function.
|
||||
If the element type isn't present in this dictionary,
|
||||
this function will go through each key of the dictionary in the insertion order to
|
||||
invoke the corresponding collate function if the element type is a subclass of the key.
|
||||
|
||||
Examples:
|
||||
>>> def collate_tensor_fn(batch, *, collate_fn_map):
|
||||
... # Extend this function to handle batch of tensors
|
||||
... return torch.stack(batch, 0)
|
||||
>>> def custom_collate(batch):
|
||||
... collate_map = {torch.Tensor: collate_tensor_fn}
|
||||
... return collate(batch, collate_fn_map=collate_map)
|
||||
>>> # Extend `default_collate` by in-place modifying `default_collate_fn_map`
|
||||
>>> default_collate_fn_map.update({torch.Tensor: collate_tensor_fn})
|
||||
|
||||
Note:
|
||||
Each collate function requires a positional argument for batch and a keyword argument
|
||||
for the dictionary of collate functions as `collate_fn_map`.
|
||||
"""
|
||||
elem = batch[0]
|
||||
elem_type = type(elem)
|
||||
|
||||
if collate_fn_map is not None:
|
||||
if elem_type in collate_fn_map:
|
||||
return collate_fn_map[elem_type](batch, collate_fn_map=collate_fn_map)
|
||||
|
||||
for collate_type in collate_fn_map:
|
||||
if isinstance(elem, collate_type):
|
||||
return collate_fn_map[collate_type](
|
||||
batch, collate_fn_map=collate_fn_map
|
||||
)
|
||||
|
||||
if isinstance(elem, collections.abc.Mapping):
|
||||
try:
|
||||
if isinstance(elem, collections.abc.MutableMapping):
|
||||
# The mapping type may have extra properties, so we can't just
|
||||
# use `type(data)(...)` to create the new mapping.
|
||||
# Create a clone and update it if the mapping type is mutable.
|
||||
clone = copy.copy(elem)
|
||||
clone.update(
|
||||
{
|
||||
key: collate(
|
||||
[d[key] for d in batch], collate_fn_map=collate_fn_map
|
||||
)
|
||||
for key in elem
|
||||
}
|
||||
)
|
||||
return clone
|
||||
else:
|
||||
return elem_type(
|
||||
{
|
||||
key: collate(
|
||||
[d[key] for d in batch], collate_fn_map=collate_fn_map
|
||||
)
|
||||
for key in elem
|
||||
}
|
||||
)
|
||||
except TypeError:
|
||||
# The mapping type may not support `copy()` / `update(mapping)`
|
||||
# or `__init__(iterable)`.
|
||||
return {
|
||||
key: collate([d[key] for d in batch], collate_fn_map=collate_fn_map)
|
||||
for key in elem
|
||||
}
|
||||
elif isinstance(elem, tuple) and hasattr(elem, "_fields"): # namedtuple
|
||||
return elem_type(
|
||||
*(
|
||||
collate(samples, collate_fn_map=collate_fn_map)
|
||||
for samples in zip(*batch, strict=False)
|
||||
)
|
||||
)
|
||||
elif isinstance(elem, collections.abc.Sequence):
|
||||
# check to make sure that the elements in batch have consistent size
|
||||
it = iter(batch)
|
||||
elem_size = len(next(it))
|
||||
|
||||
if not all(len(elem) == elem_size for elem in it):
|
||||
raise RuntimeError("each element in list of batch should be of equal size")
|
||||
transposed = list(
|
||||
zip(*batch, strict=False)
|
||||
) # It may be accessed twice, so we use a list.
|
||||
|
||||
if isinstance(elem, tuple):
|
||||
return [
|
||||
collate(samples, collate_fn_map=collate_fn_map)
|
||||
for samples in transposed
|
||||
] # Backwards compatibility.
|
||||
else:
|
||||
try:
|
||||
if isinstance(elem, collections.abc.MutableSequence):
|
||||
# The sequence type may have extra properties, so we can't just
|
||||
# use `type(data)(...)` to create the new sequence.
|
||||
# Create a clone and update it if the sequence type is mutable.
|
||||
clone = copy.copy(elem) # type: ignore[arg-type]
|
||||
for i, samples in enumerate(transposed):
|
||||
clone[i] = collate(samples, collate_fn_map=collate_fn_map)
|
||||
return clone
|
||||
else:
|
||||
return elem_type(
|
||||
[
|
||||
collate(samples, collate_fn_map=collate_fn_map)
|
||||
for samples in transposed
|
||||
]
|
||||
)
|
||||
except TypeError:
|
||||
# The sequence type may not support `copy()` / `__setitem__(index, item)`
|
||||
# or `__init__(iterable)` (e.g., `range`).
|
||||
return [
|
||||
collate(samples, collate_fn_map=collate_fn_map)
|
||||
for samples in transposed
|
||||
]
|
||||
|
||||
raise TypeError(default_collate_err_msg_format.format(elem_type))
|
||||
|
||||
|
||||
def collate_tensor_fn(
|
||||
batch,
|
||||
*,
|
||||
collate_fn_map: dict[type | tuple[type, ...], Callable] | None = None,
|
||||
):
|
||||
elem = batch[0]
|
||||
out = None
|
||||
if elem.is_nested:
|
||||
raise RuntimeError(
|
||||
"Batches of nested tensors are not currently supported by the default collate_fn; "
|
||||
"please provide a custom collate_fn to handle them appropriately."
|
||||
)
|
||||
if elem.layout in {
|
||||
torch.sparse_coo,
|
||||
torch.sparse_csr,
|
||||
torch.sparse_bsr,
|
||||
torch.sparse_csc,
|
||||
torch.sparse_bsc,
|
||||
}:
|
||||
raise RuntimeError(
|
||||
"Batches of sparse tensors are not currently supported by the default collate_fn; "
|
||||
"please provide a custom collate_fn to handle them appropriately."
|
||||
)
|
||||
if torch.utils.data.get_worker_info() is not None:
|
||||
# If we're in a background process, concatenate directly into a
|
||||
# shared memory tensor to avoid an extra copy
|
||||
numel = sum(x.numel() for x in batch)
|
||||
storage = elem._typed_storage()._new_shared(numel, device=elem.device)
|
||||
out = elem.new(storage).resize_(len(batch), *list(elem.size()))
|
||||
return torch.stack(batch, 0, out=out)
|
||||
|
||||
|
||||
def collate_numpy_array_fn(
|
||||
batch,
|
||||
*,
|
||||
collate_fn_map: dict[type | tuple[type, ...], Callable] | None = None,
|
||||
):
|
||||
elem = batch[0]
|
||||
# array of string classes and object
|
||||
if np_str_obj_array_pattern.search(elem.dtype.str) is not None:
|
||||
raise TypeError(default_collate_err_msg_format.format(elem.dtype))
|
||||
|
||||
return collate([torch.as_tensor(b) for b in batch], collate_fn_map=collate_fn_map)
|
||||
|
||||
|
||||
def collate_numpy_scalar_fn(
|
||||
batch,
|
||||
*,
|
||||
collate_fn_map: dict[type | tuple[type, ...], Callable] | None = None,
|
||||
):
|
||||
return torch.as_tensor(batch)
|
||||
|
||||
|
||||
def collate_float_fn(
|
||||
batch,
|
||||
*,
|
||||
collate_fn_map: dict[type | tuple[type, ...], Callable] | None = None,
|
||||
):
|
||||
return torch.tensor(batch, dtype=torch.float64)
|
||||
|
||||
|
||||
def collate_int_fn(
|
||||
batch,
|
||||
*,
|
||||
collate_fn_map: dict[type | tuple[type, ...], Callable] | None = None,
|
||||
):
|
||||
return torch.tensor(batch)
|
||||
|
||||
|
||||
def collate_str_fn(
|
||||
batch,
|
||||
*,
|
||||
collate_fn_map: dict[type | tuple[type, ...], Callable] | None = None,
|
||||
):
|
||||
return batch
|
||||
|
||||
|
||||
default_collate_fn_map: dict[type | tuple[type, ...], Callable] = {
|
||||
torch.Tensor: collate_tensor_fn
|
||||
}
|
||||
with contextlib.suppress(ImportError):
|
||||
import numpy as np
|
||||
|
||||
# For both ndarray and memmap (subclass of ndarray)
|
||||
default_collate_fn_map[np.ndarray] = collate_numpy_array_fn
|
||||
# See scalars hierarchy: https://numpy.org/doc/stable/reference/arrays.scalars.html
|
||||
# Skip string scalars
|
||||
default_collate_fn_map[(np.bool_, np.number, np.object_)] = collate_numpy_scalar_fn
|
||||
default_collate_fn_map[float] = collate_float_fn
|
||||
default_collate_fn_map[int] = collate_int_fn
|
||||
default_collate_fn_map[str] = collate_str_fn
|
||||
default_collate_fn_map[bytes] = collate_str_fn
|
||||
|
||||
|
||||
def default_collate(batch):
|
||||
r"""
|
||||
Take in a batch of data and put the elements within the batch into a tensor with an additional outer dimension - batch size.
|
||||
|
||||
The exact output type can be a :class:`torch.Tensor`, a `Sequence` of :class:`torch.Tensor`, a
|
||||
Collection of :class:`torch.Tensor`, or left unchanged, depending on the input type.
|
||||
This is used as the default function for collation when
|
||||
`batch_size` or `batch_sampler` is defined in :class:`~torch.utils.data.DataLoader`.
|
||||
|
||||
Here is the general input type (based on the type of the element within the batch) to output type mapping:
|
||||
|
||||
* :class:`torch.Tensor` -> :class:`torch.Tensor` (with an added outer dimension batch size)
|
||||
* NumPy Arrays -> :class:`torch.Tensor`
|
||||
* `float` -> :class:`torch.Tensor`
|
||||
* `int` -> :class:`torch.Tensor`
|
||||
* `str` -> `str` (unchanged)
|
||||
* `bytes` -> `bytes` (unchanged)
|
||||
* `Mapping[K, V_i]` -> `Mapping[K, default_collate([V_1, V_2, ...])]`
|
||||
* `NamedTuple[V1_i, V2_i, ...]` -> `NamedTuple[default_collate([V1_1, V1_2, ...]),
|
||||
default_collate([V2_1, V2_2, ...]), ...]`
|
||||
* `Sequence[V1_i, V2_i, ...]` -> `Sequence[default_collate([V1_1, V1_2, ...]),
|
||||
default_collate([V2_1, V2_2, ...]), ...]`
|
||||
|
||||
Args:
|
||||
batch: a single batch to be collated
|
||||
|
||||
Examples:
|
||||
>>> # xdoctest: +SKIP
|
||||
>>> # Example with a batch of `int`s:
|
||||
>>> default_collate([0, 1, 2, 3])
|
||||
tensor([0, 1, 2, 3])
|
||||
>>> # Example with a batch of `str`s:
|
||||
>>> default_collate(["a", "b", "c"])
|
||||
['a', 'b', 'c']
|
||||
>>> # Example with `Map` inside the batch:
|
||||
>>> default_collate([{"A": 0, "B": 1}, {"A": 100, "B": 100}])
|
||||
{'A': tensor([ 0, 100]), 'B': tensor([ 1, 100])}
|
||||
>>> # Example with `NamedTuple` inside the batch:
|
||||
>>> Point = namedtuple("Point", ["x", "y"])
|
||||
>>> default_collate([Point(0, 0), Point(1, 1)])
|
||||
Point(x=tensor([0, 1]), y=tensor([0, 1]))
|
||||
>>> # Example with `Tuple` inside the batch:
|
||||
>>> default_collate([(0, 1), (2, 3)])
|
||||
[tensor([0, 2]), tensor([1, 3])]
|
||||
>>> # Example with `List` inside the batch:
|
||||
>>> default_collate([[0, 1], [2, 3]])
|
||||
[tensor([0, 2]), tensor([1, 3])]
|
||||
>>> # Two options to extend `default_collate` to handle specific type
|
||||
>>> # Option 1: Write custom collate function and invoke `default_collate`
|
||||
>>> def custom_collate(batch):
|
||||
... elem = batch[0]
|
||||
... if isinstance(elem, CustomType): # Some custom condition
|
||||
... return ...
|
||||
... else: # Fall back to `default_collate`
|
||||
... return default_collate(batch)
|
||||
>>> # Option 2: In-place modify `default_collate_fn_map`
|
||||
>>> def collate_customtype_fn(batch, *, collate_fn_map=None):
|
||||
... return ...
|
||||
>>> default_collate_fn_map.update(CustomType, collate_customtype_fn)
|
||||
>>> default_collate(batch) # Handle `CustomType` automatically
|
||||
"""
|
||||
return collate(batch, collate_fn_map=default_collate_fn_map)
|
||||
@@ -0,0 +1,57 @@
|
||||
# mypy: allow-untyped-defs
|
||||
r"""Contains definitions of the methods used by the _BaseDataLoaderIter to fetch data from an iterable-style or map-style dataset.
|
||||
|
||||
This logic is shared in both single- and multi-processing data loading.
|
||||
"""
|
||||
|
||||
from typing import NoReturn
|
||||
|
||||
|
||||
class _BaseDatasetFetcher:
|
||||
def __init__(self, dataset, auto_collation, collate_fn, drop_last) -> None:
|
||||
self.dataset = dataset
|
||||
self.auto_collation = auto_collation
|
||||
self.collate_fn = collate_fn
|
||||
self.drop_last = drop_last
|
||||
|
||||
def fetch(self, possibly_batched_index) -> NoReturn:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class _IterableDatasetFetcher(_BaseDatasetFetcher):
|
||||
def __init__(self, dataset, auto_collation, collate_fn, drop_last) -> None:
|
||||
super().__init__(dataset, auto_collation, collate_fn, drop_last)
|
||||
self.dataset_iter = iter(dataset)
|
||||
self.ended = False
|
||||
|
||||
def fetch(self, possibly_batched_index):
|
||||
if self.ended:
|
||||
raise StopIteration
|
||||
|
||||
if self.auto_collation:
|
||||
data = []
|
||||
for _ in possibly_batched_index:
|
||||
try:
|
||||
data.append(next(self.dataset_iter))
|
||||
except StopIteration:
|
||||
self.ended = True
|
||||
break
|
||||
if len(data) == 0 or (
|
||||
self.drop_last and len(data) < len(possibly_batched_index)
|
||||
):
|
||||
raise StopIteration
|
||||
else:
|
||||
data = next(self.dataset_iter)
|
||||
return self.collate_fn(data)
|
||||
|
||||
|
||||
class _MapDatasetFetcher(_BaseDatasetFetcher):
|
||||
def fetch(self, possibly_batched_index):
|
||||
if self.auto_collation:
|
||||
if hasattr(self.dataset, "__getitems__") and self.dataset.__getitems__:
|
||||
data = self.dataset.__getitems__(possibly_batched_index)
|
||||
else:
|
||||
data = [self.dataset[idx] for idx in possibly_batched_index]
|
||||
else:
|
||||
data = self.dataset[possibly_batched_index]
|
||||
return self.collate_fn(data)
|
||||
@@ -0,0 +1,108 @@
|
||||
# mypy: allow-untyped-defs
|
||||
r"""Contains definitions of the methods used by the _BaseDataLoaderIter to put fetched tensors into pinned memory.
|
||||
|
||||
These **needs** to be in global scope since Py2 doesn't support serializing
|
||||
static methods.
|
||||
"""
|
||||
|
||||
import collections
|
||||
import copy
|
||||
import queue
|
||||
|
||||
import torch
|
||||
from torch._utils import ExceptionWrapper
|
||||
|
||||
from . import MP_STATUS_CHECK_INTERVAL
|
||||
|
||||
|
||||
def _pin_memory_loop(in_queue, out_queue, device_id, done_event, device) -> None:
|
||||
# This setting is thread local, and prevents the copy in pin_memory from
|
||||
# consuming all CPU cores.
|
||||
torch.set_num_threads(1)
|
||||
|
||||
torch.multiprocessing._set_thread_name("pt_data_pin")
|
||||
torch.accelerator.set_device_index(device_id)
|
||||
|
||||
def do_one_step() -> None:
|
||||
try:
|
||||
r = in_queue.get(timeout=MP_STATUS_CHECK_INTERVAL)
|
||||
except queue.Empty:
|
||||
return
|
||||
idx, data = r
|
||||
if not done_event.is_set() and not isinstance(data, ExceptionWrapper):
|
||||
try:
|
||||
data = pin_memory(data, device)
|
||||
except Exception:
|
||||
data = ExceptionWrapper(
|
||||
where=f"in pin memory thread for device {device_id}"
|
||||
)
|
||||
r = (idx, data)
|
||||
while not done_event.is_set():
|
||||
try:
|
||||
out_queue.put(r, timeout=MP_STATUS_CHECK_INTERVAL)
|
||||
break
|
||||
except queue.Full:
|
||||
continue
|
||||
|
||||
# See NOTE [ Data Loader Multiprocessing Shutdown Logic ] for details on the
|
||||
# logic of this function.
|
||||
while not done_event.is_set():
|
||||
# Make sure that we don't preserve any object from one iteration
|
||||
# to the next
|
||||
do_one_step()
|
||||
|
||||
|
||||
def pin_memory(data, device=None):
|
||||
if isinstance(data, torch.Tensor):
|
||||
return data.pin_memory()
|
||||
|
||||
if hasattr(data, "pin_memory"):
|
||||
return data.pin_memory()
|
||||
|
||||
if isinstance(data, (str, bytes)):
|
||||
return data
|
||||
|
||||
if isinstance(data, collections.abc.Mapping):
|
||||
try:
|
||||
if isinstance(data, collections.abc.MutableMapping):
|
||||
# The mapping type may have extra properties, so we can't just
|
||||
# use `type(data)(...)` to create the new mapping.
|
||||
# Create a clone and update it if the mapping type is mutable.
|
||||
clone = copy.copy(data)
|
||||
clone.update(
|
||||
{k: pin_memory(sample, device) for k, sample in data.items()}
|
||||
)
|
||||
return clone
|
||||
else:
|
||||
# pyrefly: ignore [bad-instantiation]
|
||||
return type(data)(
|
||||
# pyrefly: ignore [bad-argument-count]
|
||||
{k: pin_memory(sample, device) for k, sample in data.items()}
|
||||
) # type: ignore[call-arg]
|
||||
except TypeError:
|
||||
# The mapping type may not support `copy()` / `update(mapping)`
|
||||
# or `__init__(iterable)`.
|
||||
return {k: pin_memory(sample, device) for k, sample in data.items()}
|
||||
|
||||
if isinstance(data, tuple):
|
||||
if hasattr(data, "_fields"): # namedtuple
|
||||
return type(data)(*(pin_memory(sample, device) for sample in data))
|
||||
return type(data)(pin_memory(sample, device) for sample in data)
|
||||
|
||||
if isinstance(data, collections.abc.Sequence):
|
||||
try:
|
||||
if isinstance(data, collections.abc.MutableSequence):
|
||||
# The sequence type may have extra properties, so we can't just
|
||||
# use `type(data)(...)` to create the new sequence.
|
||||
# Create a clone and update it if the sequence type is mutable.
|
||||
clone = copy.copy(data) # type: ignore[arg-type]
|
||||
for i, item in enumerate(data):
|
||||
clone[i] = pin_memory(item, device)
|
||||
return clone
|
||||
return type(data)([pin_memory(sample, device) for sample in data]) # type: ignore[call-arg]
|
||||
except TypeError:
|
||||
# The sequence type may not support `copy()` / `__setitem__(index, item)`
|
||||
# or `__init__(iterable)` (e.g., `range`).
|
||||
return [pin_memory(sample, device) for sample in data]
|
||||
|
||||
return data
|
||||
@@ -0,0 +1,80 @@
|
||||
# mypy: allow-untyped-defs
|
||||
r"""Signal handling for multiprocessing data loading.
|
||||
|
||||
NOTE [ Signal handling in multiprocessing data loading ]
|
||||
|
||||
In cases like DataLoader, if a worker process dies due to bus error/segfault
|
||||
or just hang, the main process will hang waiting for data. This is difficult
|
||||
to avoid on PyTorch side as it can be caused by limited shm, or other
|
||||
libraries users call in the workers. In this file and `DataLoader.cpp`, we make
|
||||
our best effort to provide some error message to users when such unfortunate
|
||||
events happen.
|
||||
|
||||
When a _BaseDataLoaderIter starts worker processes, their pids are registered in a
|
||||
defined in `DataLoader.cpp`: id(_BaseDataLoaderIter) => Collection[ Worker pids ]
|
||||
via `_set_worker_pids`.
|
||||
|
||||
When an error happens in a worker process, the main process received a SIGCHLD,
|
||||
and Python will eventually call the handler registered below
|
||||
(in `_set_SIGCHLD_handler`). In the handler, the `_error_if_any_worker_fails`
|
||||
call checks all registered worker pids and raise proper error message to
|
||||
prevent main process from hanging waiting for data from worker.
|
||||
|
||||
Additionally, at the beginning of each worker's `_utils.worker._worker_loop`,
|
||||
`_set_worker_signal_handlers` is called to register critical signal handlers
|
||||
(e.g., for SIGSEGV, SIGBUS, SIGFPE, SIGTERM) in C, which just prints an error
|
||||
message to stderr before triggering the default handler. So a message will also
|
||||
be printed from the worker process when it is killed by such signals.
|
||||
|
||||
See NOTE [ Data Loader Multiprocessing Shutdown Logic ] for the reasoning of
|
||||
this signal handling design and other mechanism we implement to make our
|
||||
multiprocessing data loading robust to errors.
|
||||
"""
|
||||
|
||||
import signal
|
||||
import threading
|
||||
|
||||
# Some of the following imported functions are not used in this file, but are to
|
||||
# be used `_utils.signal_handling.XXXXX`.
|
||||
from torch._C import ( # noqa: F401
|
||||
_error_if_any_worker_fails,
|
||||
_remove_worker_pids,
|
||||
_set_worker_pids,
|
||||
_set_worker_signal_handlers,
|
||||
)
|
||||
|
||||
from . import IS_WINDOWS
|
||||
|
||||
|
||||
_SIGCHLD_handler_set = False
|
||||
r"""Whether SIGCHLD handler is set for DataLoader worker failures. Only one
|
||||
handler needs to be set for all DataLoaders in a process."""
|
||||
|
||||
|
||||
def _set_SIGCHLD_handler() -> None:
|
||||
# Windows doesn't support SIGCHLD handler
|
||||
if IS_WINDOWS:
|
||||
return
|
||||
# can't set signal in child threads
|
||||
if not isinstance(threading.current_thread(), threading._MainThread): # type: ignore[attr-defined]
|
||||
return
|
||||
global _SIGCHLD_handler_set
|
||||
if _SIGCHLD_handler_set:
|
||||
return
|
||||
previous_handler = signal.getsignal(signal.SIGCHLD)
|
||||
if not callable(previous_handler):
|
||||
# This doesn't catch default handler, but SIGCHLD default handler is a
|
||||
# no-op.
|
||||
previous_handler = None
|
||||
|
||||
def handler(signum, frame) -> None:
|
||||
# This following call uses `waitid` with WNOHANG from C side. Therefore,
|
||||
# Python can still get and update the process status successfully.
|
||||
_error_if_any_worker_fails()
|
||||
if previous_handler is not None:
|
||||
if not callable(previous_handler):
|
||||
raise AssertionError("previous_handler is not callable")
|
||||
previous_handler(signum, frame)
|
||||
|
||||
signal.signal(signal.SIGCHLD, handler)
|
||||
_SIGCHLD_handler_set = True
|
||||
@@ -0,0 +1,399 @@
|
||||
# mypy: allow-untyped-defs
|
||||
r"""Contains definitions of the methods used by the _BaseDataLoaderIter workers.
|
||||
|
||||
These **needs** to be in global scope since Py2 doesn't support serializing
|
||||
static methods.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import queue
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
from torch._utils import ExceptionWrapper
|
||||
|
||||
from . import HAS_NUMPY, IS_WINDOWS, MP_STATUS_CHECK_INTERVAL, signal_handling
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch.utils.data import Dataset
|
||||
|
||||
if IS_WINDOWS:
|
||||
import ctypes
|
||||
from ctypes.wintypes import BOOL, DWORD, HANDLE
|
||||
|
||||
# On Windows, the parent ID of the worker process remains unchanged when the manager process
|
||||
# is gone, and the only way to check it through OS is to let the worker have a process handle
|
||||
# of the manager and ask if the process status has changed.
|
||||
class ManagerWatchdog:
|
||||
def __init__(self) -> None:
|
||||
self.manager_pid = os.getppid()
|
||||
|
||||
# mypy cannot detect this code is windows only
|
||||
self.kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) # type: ignore[attr-defined]
|
||||
self.kernel32.OpenProcess.argtypes = (DWORD, BOOL, DWORD)
|
||||
self.kernel32.OpenProcess.restype = HANDLE
|
||||
self.kernel32.WaitForSingleObject.argtypes = (HANDLE, DWORD)
|
||||
self.kernel32.WaitForSingleObject.restype = DWORD
|
||||
|
||||
# Value obtained from https://msdn.microsoft.com/en-us/library/ms684880.aspx
|
||||
SYNCHRONIZE = 0x00100000
|
||||
self.manager_handle = self.kernel32.OpenProcess(
|
||||
SYNCHRONIZE, 0, self.manager_pid
|
||||
)
|
||||
|
||||
if not self.manager_handle:
|
||||
raise ctypes.WinError(ctypes.get_last_error()) # type: ignore[attr-defined]
|
||||
|
||||
self.manager_dead = False
|
||||
|
||||
def is_alive(self) -> bool:
|
||||
if not self.manager_dead:
|
||||
# Value obtained from https://msdn.microsoft.com/en-us/library/windows/desktop/ms687032.aspx
|
||||
self.manager_dead = (
|
||||
self.kernel32.WaitForSingleObject(self.manager_handle, 0) == 0
|
||||
)
|
||||
return not self.manager_dead
|
||||
|
||||
else:
|
||||
|
||||
class ManagerWatchdog: # type: ignore[no-redef]
|
||||
def __init__(self) -> None:
|
||||
self.manager_pid = os.getppid()
|
||||
self.manager_dead = False
|
||||
|
||||
def is_alive(self) -> bool:
|
||||
if not self.manager_dead:
|
||||
self.manager_dead = os.getppid() != self.manager_pid
|
||||
return not self.manager_dead
|
||||
|
||||
|
||||
_worker_info: WorkerInfo | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class WorkerInfo:
|
||||
"""Information about the current DataLoader worker process or thread.
|
||||
|
||||
Attributes:
|
||||
id: The current worker id (0 to num_workers - 1)
|
||||
num_workers: Total number of workers
|
||||
seed: Random seed set for this worker
|
||||
dataset: Copy of the dataset object in this worker
|
||||
rng: Optional RNG state container. Defaults to None.
|
||||
worker_method: Optional worker method ("multiprocessing" or "thread"). Defaults to "multiprocessing".
|
||||
"""
|
||||
|
||||
id: int
|
||||
num_workers: int
|
||||
seed: int
|
||||
dataset: Dataset
|
||||
rng: _RNG | None = None
|
||||
worker_method: str | None = "multiprocessing"
|
||||
|
||||
|
||||
def get_worker_info() -> WorkerInfo | None:
|
||||
r"""Returns the information about the current
|
||||
:class:`~torch.utils.data.DataLoader` iterator worker process.
|
||||
|
||||
When called in a worker, this returns an object guaranteed to have the
|
||||
following attributes:
|
||||
|
||||
* :attr:`id`: the current worker id.
|
||||
* :attr:`num_workers`: the total number of workers.
|
||||
* :attr:`seed`: the random seed set for the current worker. This value is
|
||||
determined by main process RNG and the worker id. See
|
||||
:class:`~torch.utils.data.DataLoader`'s documentation for more details.
|
||||
* :attr:`dataset`: the copy of the dataset object in **this** process. Note
|
||||
that this will be a different object in a different process than the one
|
||||
in the main process.
|
||||
|
||||
When called in the main process, this returns ``None``.
|
||||
|
||||
.. note::
|
||||
When used in a :attr:`worker_init_fn` passed over to
|
||||
:class:`~torch.utils.data.DataLoader`, this method can be useful to
|
||||
set up each worker process differently, for instance, using ``worker_id``
|
||||
to configure the ``dataset`` object to only read a specific fraction of a
|
||||
sharded dataset, or use ``seed`` to seed other libraries used in dataset
|
||||
code.
|
||||
"""
|
||||
return _worker_info
|
||||
|
||||
|
||||
r"""Dummy class used to signal the end of an IterableDataset"""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _IterableDatasetStopIteration:
|
||||
worker_id: int
|
||||
|
||||
|
||||
r"""Dummy class used to resume the fetching when worker reuse is enabled"""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _ResumeIteration:
|
||||
seed: int | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _RNG:
|
||||
"""Container for thread-local random number generator state.
|
||||
|
||||
Used by thread workers to maintain separate RNG state per worker thread
|
||||
to avoid race conditions.
|
||||
|
||||
Attributes:
|
||||
random_generator: Python random.Random generator for this thread
|
||||
torch_generator: PyTorch Generator for this thread
|
||||
numpy_generator: NumPy Generator for this thread (None if numpy not available)
|
||||
"""
|
||||
|
||||
random_generator: random.Random
|
||||
torch_generator: torch.Generator
|
||||
numpy_generator: object | None = None
|
||||
|
||||
|
||||
# The function `_generate_state` is adapted from `numpy.random.SeedSequence`
|
||||
# from https://github.com/numpy/numpy/blob/main/numpy/random/bit_generator.pyx
|
||||
# It's MIT licensed, here is the copyright:
|
||||
|
||||
# Copyright (c) 2015 Melissa E. O'Neill
|
||||
# Copyright (c) 2019 NumPy Developers
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
|
||||
# This function generates an array of int32 as the seed for
|
||||
# `numpy.random`, in order to prevent state collision due to same
|
||||
# seed and algorithm for `numpy.random` and `random` modules.
|
||||
# TODO: Implement `SeedSequence` like object for `torch.random`
|
||||
def _generate_state(base_seed, worker_id):
|
||||
INIT_A = 0x43B0D7E5
|
||||
MULT_A = 0x931E8875
|
||||
INIT_B = 0x8B51F9DD
|
||||
MULT_B = 0x58F38DED
|
||||
MIX_MULT_L = 0xCA01F9DD
|
||||
MIX_MULT_R = 0x4973F715
|
||||
XSHIFT = 4 * 8 // 2
|
||||
MASK32 = 0xFFFFFFFF
|
||||
|
||||
entropy = [worker_id, base_seed & MASK32, base_seed >> 32, 0]
|
||||
pool = [0] * 4
|
||||
|
||||
hash_const_A = INIT_A
|
||||
|
||||
def hash(value):
|
||||
nonlocal hash_const_A
|
||||
value = (value ^ hash_const_A) & MASK32
|
||||
hash_const_A = (hash_const_A * MULT_A) & MASK32
|
||||
value = (value * hash_const_A) & MASK32
|
||||
value = (value ^ (value >> XSHIFT)) & MASK32
|
||||
return value
|
||||
|
||||
def mix(x, y):
|
||||
result_x = (MIX_MULT_L * x) & MASK32
|
||||
result_y = (MIX_MULT_R * y) & MASK32
|
||||
result = (result_x - result_y) & MASK32
|
||||
result = (result ^ (result >> XSHIFT)) & MASK32
|
||||
return result
|
||||
|
||||
# Add in the entropy to the pool.
|
||||
for i in range(len(pool)):
|
||||
pool[i] = hash(entropy[i])
|
||||
|
||||
# Mix all bits together so late bits can affect earlier bits.
|
||||
for i_src in range(len(pool)):
|
||||
for i_dst in range(len(pool)):
|
||||
if i_src != i_dst:
|
||||
pool[i_dst] = mix(pool[i_dst], hash(pool[i_src]))
|
||||
|
||||
hash_const_B = INIT_B
|
||||
state = []
|
||||
for i_dst in range(4):
|
||||
data_val = pool[i_dst]
|
||||
data_val = (data_val ^ hash_const_B) & MASK32
|
||||
hash_const_B = (hash_const_B * MULT_B) & MASK32
|
||||
data_val = (data_val * hash_const_B) & MASK32
|
||||
data_val = (data_val ^ (data_val >> XSHIFT)) & MASK32
|
||||
state.append(data_val)
|
||||
return state
|
||||
|
||||
|
||||
def _worker_loop(
|
||||
dataset_kind,
|
||||
dataset,
|
||||
index_queue,
|
||||
data_queue,
|
||||
done_event,
|
||||
auto_collation,
|
||||
collate_fn,
|
||||
drop_last,
|
||||
base_seed,
|
||||
init_fn,
|
||||
worker_id,
|
||||
num_workers,
|
||||
persistent_workers,
|
||||
shared_seed,
|
||||
) -> None:
|
||||
# See NOTE [ Data Loader Multiprocessing Shutdown Logic ] for details on the
|
||||
# logic of this function.
|
||||
|
||||
try:
|
||||
# Initialize C side signal handlers for SIGBUS and SIGSEGV. Python signal
|
||||
# module's handlers are executed after Python returns from C low-level
|
||||
# handlers, likely when the same fatal signal had already happened
|
||||
# again.
|
||||
# https://docs.python.org/3/library/signal.html#execution-of-python-signal-handlers
|
||||
signal_handling._set_worker_signal_handlers()
|
||||
|
||||
torch.multiprocessing._set_thread_name("pt_data_worker")
|
||||
|
||||
torch.set_num_threads(1)
|
||||
seed = base_seed + worker_id
|
||||
random.seed(seed)
|
||||
torch.manual_seed(seed)
|
||||
if HAS_NUMPY:
|
||||
np_seed = _generate_state(base_seed, worker_id)
|
||||
import numpy as np
|
||||
|
||||
np.random.seed(np_seed)
|
||||
|
||||
from torch.utils.data import IterDataPipe
|
||||
from torch.utils.data.graph_settings import apply_random_seed
|
||||
|
||||
shared_rng = torch.Generator()
|
||||
if isinstance(dataset, IterDataPipe):
|
||||
if shared_seed is None:
|
||||
raise AssertionError(
|
||||
"shared_seed must be provided for IterDataPipe workers"
|
||||
)
|
||||
shared_rng.manual_seed(shared_seed)
|
||||
dataset = apply_random_seed(dataset, shared_rng)
|
||||
|
||||
global _worker_info
|
||||
_worker_info = WorkerInfo(
|
||||
id=worker_id, num_workers=num_workers, seed=seed, dataset=dataset
|
||||
)
|
||||
|
||||
from torch.utils.data import _DatasetKind
|
||||
|
||||
init_exception = None
|
||||
|
||||
try:
|
||||
if init_fn is not None:
|
||||
init_fn(worker_id)
|
||||
|
||||
fetcher = _DatasetKind.create_fetcher(
|
||||
dataset_kind, dataset, auto_collation, collate_fn, drop_last
|
||||
)
|
||||
except Exception:
|
||||
init_exception = ExceptionWrapper(
|
||||
where=f"in DataLoader worker process {worker_id}"
|
||||
)
|
||||
|
||||
# When using Iterable mode, some worker can exit earlier than others due
|
||||
# to the IterableDataset behaving differently for different workers.
|
||||
# When such things happen, an `_IterableDatasetStopIteration` object is
|
||||
# sent over to the main process with the ID of this worker, so that the
|
||||
# main process won't send more tasks to this worker, and will send
|
||||
# `None` to this worker to properly exit it.
|
||||
#
|
||||
# Note that we cannot set `done_event` from a worker as it is shared
|
||||
# among all processes. Instead, we set the `iteration_end` flag to
|
||||
# signify that the iterator is exhausted. When either `done_event` or
|
||||
# `iteration_end` is set, we skip all processing step and just wait for
|
||||
# `None`.
|
||||
iteration_end = False
|
||||
|
||||
watchdog = ManagerWatchdog()
|
||||
|
||||
while watchdog.is_alive():
|
||||
try:
|
||||
r = index_queue.get(timeout=MP_STATUS_CHECK_INTERVAL)
|
||||
except queue.Empty:
|
||||
continue
|
||||
if isinstance(r, _ResumeIteration):
|
||||
# Acknowledge the main process
|
||||
data_queue.put((r, None))
|
||||
iteration_end = False
|
||||
|
||||
if isinstance(dataset, IterDataPipe):
|
||||
if r.seed is None:
|
||||
raise AssertionError(
|
||||
"resume iteration seed is None for IterDataPipe"
|
||||
)
|
||||
shared_rng.manual_seed(r.seed)
|
||||
dataset = apply_random_seed(dataset, shared_rng)
|
||||
|
||||
# Recreate the fetcher for worker-reuse policy
|
||||
fetcher = _DatasetKind.create_fetcher(
|
||||
dataset_kind, dataset, auto_collation, collate_fn, drop_last
|
||||
)
|
||||
continue
|
||||
elif r is None:
|
||||
# Received the final signal
|
||||
if not done_event.is_set() and not iteration_end:
|
||||
raise AssertionError(
|
||||
"Received final signal but neither done_event nor iteration_end is set"
|
||||
)
|
||||
break
|
||||
elif done_event.is_set() or iteration_end:
|
||||
# `done_event` is set. But I haven't received the final signal
|
||||
# (None) yet. I will keep continuing until get it, and skip the
|
||||
# processing steps.
|
||||
continue
|
||||
idx, index = r
|
||||
data: _IterableDatasetStopIteration | ExceptionWrapper
|
||||
if init_exception is not None:
|
||||
data = init_exception
|
||||
init_exception = None
|
||||
else:
|
||||
try:
|
||||
data = fetcher.fetch(index) # type: ignore[possibly-undefined]
|
||||
except Exception as e:
|
||||
if (
|
||||
isinstance(e, StopIteration)
|
||||
and dataset_kind == _DatasetKind.Iterable
|
||||
):
|
||||
data = _IterableDatasetStopIteration(worker_id)
|
||||
# Set `iteration_end`
|
||||
# (1) to save future `next(...)` calls, and
|
||||
# (2) to avoid sending multiple `_IterableDatasetStopIteration`s.
|
||||
iteration_end = True
|
||||
else:
|
||||
# It is important that we don't store exc_info in a variable.
|
||||
# `ExceptionWrapper` does the correct thing.
|
||||
# See NOTE [ Python Traceback Reference Cycle Problem ]
|
||||
data = ExceptionWrapper(
|
||||
where=f"in DataLoader worker process {worker_id}"
|
||||
)
|
||||
data_queue.put((idx, data))
|
||||
del data, idx, index, r # save memory
|
||||
except KeyboardInterrupt:
|
||||
# Main process will raise KeyboardInterrupt anyways.
|
||||
pass
|
||||
if done_event.is_set():
|
||||
data_queue.cancel_join_thread()
|
||||
data_queue.close()
|
||||
@@ -0,0 +1,11 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from typing_extensions import deprecated as _deprecated
|
||||
|
||||
|
||||
@_deprecated(
|
||||
"Usage of `backward_compatibility.worker_init_fn` is deprecated "
|
||||
"as `DataLoader` automatically applies sharding in every worker",
|
||||
category=FutureWarning,
|
||||
)
|
||||
def worker_init_fn(worker_id) -> None:
|
||||
pass
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
from torch.utils.data.datapipes import dataframe as dataframe, iter as iter, map as map
|
||||
@@ -0,0 +1,213 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import inspect
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
from typing import Any, get_type_hints
|
||||
|
||||
from torch.utils.data.datapipes._typing import _DataPipeMeta
|
||||
from torch.utils.data.datapipes.datapipe import IterDataPipe, MapDataPipe
|
||||
|
||||
|
||||
######################################################
|
||||
# Functional API
|
||||
######################################################
|
||||
class functional_datapipe:
|
||||
name: str
|
||||
|
||||
def __init__(self, name: str, enable_df_api_tracing=False) -> None:
|
||||
"""
|
||||
Define a functional datapipe.
|
||||
|
||||
Args:
|
||||
enable_df_api_tracing - if set, any returned DataPipe would accept
|
||||
DataFrames API in tracing mode.
|
||||
"""
|
||||
self.name = name
|
||||
self.enable_df_api_tracing = enable_df_api_tracing
|
||||
|
||||
def __call__(self, cls):
|
||||
if issubclass(cls, IterDataPipe):
|
||||
if isinstance(cls, type): # type: ignore[arg-type]
|
||||
if not isinstance(cls, _DataPipeMeta):
|
||||
raise TypeError(
|
||||
"`functional_datapipe` can only decorate IterDataPipe"
|
||||
)
|
||||
# with non_deterministic decorator
|
||||
else:
|
||||
if not isinstance(cls, non_deterministic) and not (
|
||||
hasattr(cls, "__self__")
|
||||
and isinstance(cls.__self__, non_deterministic)
|
||||
):
|
||||
raise TypeError(
|
||||
"`functional_datapipe` can only decorate IterDataPipe"
|
||||
)
|
||||
IterDataPipe.register_datapipe_as_function(
|
||||
self.name, cls, enable_df_api_tracing=self.enable_df_api_tracing
|
||||
)
|
||||
elif issubclass(cls, MapDataPipe):
|
||||
MapDataPipe.register_datapipe_as_function(self.name, cls)
|
||||
|
||||
return cls
|
||||
|
||||
|
||||
######################################################
|
||||
# Determinism
|
||||
######################################################
|
||||
_determinism: bool = False
|
||||
|
||||
|
||||
class guaranteed_datapipes_determinism:
|
||||
prev: bool
|
||||
|
||||
def __init__(self) -> None:
|
||||
global _determinism
|
||||
self.prev = _determinism
|
||||
_determinism = True
|
||||
|
||||
def __enter__(self) -> None:
|
||||
pass
|
||||
|
||||
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
|
||||
global _determinism
|
||||
_determinism = self.prev
|
||||
|
||||
|
||||
class non_deterministic:
|
||||
cls: type[IterDataPipe] | None = None
|
||||
# TODO: Lambda for picking
|
||||
deterministic_fn: Callable[..., bool]
|
||||
|
||||
def __init__(self, arg: type[IterDataPipe] | Callable[..., bool]) -> None:
|
||||
# 1. Decorator doesn't have any argument
|
||||
if isinstance(arg, type): # type: ignore[arg-type]
|
||||
if not issubclass(arg, IterDataPipe): # type: ignore[arg-type]
|
||||
raise TypeError(
|
||||
"Only `IterDataPipe` can be decorated with `non_deterministic`"
|
||||
f", but {arg.__name__} is found"
|
||||
)
|
||||
self.cls = arg # type: ignore[assignment]
|
||||
# 2. Decorator has an argument of a function
|
||||
# This class should behave differently given different inputs. Use this
|
||||
# function to verify the determinism for each instance.
|
||||
# When the function returns True, the instance is non-deterministic. Otherwise,
|
||||
# the instance is a deterministic DataPipe.
|
||||
elif isinstance(arg, Callable): # type:ignore[arg-type]
|
||||
self.deterministic_fn = arg
|
||||
else:
|
||||
raise TypeError(f"{arg} can not be decorated by non_deterministic")
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
global _determinism
|
||||
# Decorate IterDataPipe
|
||||
if self.cls is not None:
|
||||
if _determinism:
|
||||
raise TypeError(
|
||||
f"{self.cls.__name__} is non-deterministic, but you set 'guaranteed_datapipes_determinism'. "
|
||||
"You can turn off determinism for this DataPipe if that is acceptable "
|
||||
"for your application"
|
||||
)
|
||||
return self.cls(*args, **kwargs) # type: ignore[call-arg]
|
||||
|
||||
# Decorate with a functional argument
|
||||
if not (
|
||||
isinstance(args[0], type) and issubclass(args[0], IterDataPipe) # type: ignore[arg-type]
|
||||
):
|
||||
raise TypeError(
|
||||
f"Only `IterDataPipe` can be decorated, but {args[0].__name__} is found"
|
||||
)
|
||||
self.cls = args[0]
|
||||
return self.deterministic_wrapper_fn
|
||||
|
||||
def deterministic_wrapper_fn(self, *args, **kwargs) -> IterDataPipe:
|
||||
res = self.deterministic_fn(*args, **kwargs)
|
||||
if not isinstance(res, bool):
|
||||
raise TypeError(
|
||||
"deterministic_fn of `non_deterministic` decorator is required "
|
||||
f"to return a boolean value, but {type(res)} is found"
|
||||
)
|
||||
global _determinism
|
||||
if _determinism and res:
|
||||
raise TypeError(
|
||||
f"{self.cls.__name__} is non-deterministic with the inputs, but you set " # type: ignore[union-attr]
|
||||
"'guaranteed_datapipes_determinism'. You can turn off determinism "
|
||||
"for this DataPipe if that is acceptable for your application"
|
||||
)
|
||||
return self.cls(*args, **kwargs) # type: ignore[call-arg, misc]
|
||||
|
||||
|
||||
######################################################
|
||||
# Type validation
|
||||
######################################################
|
||||
# Validate each argument of DataPipe with hint as a subtype of the hint.
|
||||
def argument_validation(f):
|
||||
signature = inspect.signature(f)
|
||||
hints = get_type_hints(f)
|
||||
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
bound = signature.bind(*args, **kwargs)
|
||||
for argument_name, value in bound.arguments.items():
|
||||
if argument_name in hints and isinstance(
|
||||
hints[argument_name], _DataPipeMeta
|
||||
):
|
||||
hint = hints[argument_name]
|
||||
if not isinstance(value, IterDataPipe):
|
||||
raise TypeError(
|
||||
f"Expected argument '{argument_name}' as a IterDataPipe, but found {type(value)}"
|
||||
)
|
||||
if not value.type.issubtype(hint.type):
|
||||
raise TypeError(
|
||||
f"Expected type of argument '{argument_name}' as a subtype of "
|
||||
f"hint {hint.type}, but found {value.type}"
|
||||
)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
# Default value is True
|
||||
_runtime_validation_enabled: bool = True
|
||||
|
||||
|
||||
class runtime_validation_disabled:
|
||||
prev: bool
|
||||
|
||||
def __init__(self) -> None:
|
||||
global _runtime_validation_enabled
|
||||
self.prev = _runtime_validation_enabled
|
||||
_runtime_validation_enabled = False
|
||||
|
||||
def __enter__(self) -> None:
|
||||
pass
|
||||
|
||||
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
|
||||
global _runtime_validation_enabled
|
||||
_runtime_validation_enabled = self.prev
|
||||
|
||||
|
||||
# Runtime checking
|
||||
# Validate output data is subtype of return hint
|
||||
def runtime_validation(f):
|
||||
# TODO:
|
||||
# Can be extended to validate '__getitem__' and nonblocking
|
||||
if f.__name__ != "__iter__":
|
||||
raise TypeError(
|
||||
f"Can not decorate function {f.__name__} with 'runtime_validation'"
|
||||
)
|
||||
|
||||
@wraps(f)
|
||||
def wrapper(self):
|
||||
global _runtime_validation_enabled
|
||||
if not _runtime_validation_enabled:
|
||||
yield from f(self)
|
||||
else:
|
||||
it = f(self)
|
||||
for d in it:
|
||||
if not self.type.issubtype_of_instance(d):
|
||||
raise RuntimeError(
|
||||
f"Expected an instance as subtype of {self.type}, but found {d}({type(d)})"
|
||||
)
|
||||
yield d
|
||||
|
||||
return wrapper
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import functools
|
||||
import inspect
|
||||
from enum import Enum
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
class _SnapshotState(Enum):
|
||||
r"""
|
||||
These are the snapshotting-related states that IterDataPipes can be in.
|
||||
|
||||
`NotStarted` - allows you to restore a snapshot and create an iterator with reset
|
||||
`Restored` - cannot restore again, allows you to create an iterator without resetting the DataPipe
|
||||
`Iterating` - can restore, will reset if you create a new iterator
|
||||
"""
|
||||
|
||||
NotStarted = 0
|
||||
Restored = 1
|
||||
Iterating = 2
|
||||
|
||||
|
||||
def _simplify_obj_name(obj) -> str:
|
||||
"""Simplify the display strings of objects for the purpose of rendering within DataPipe error messages."""
|
||||
if inspect.isfunction(obj):
|
||||
return obj.__name__
|
||||
else:
|
||||
return repr(obj)
|
||||
|
||||
|
||||
def _strip_datapipe_from_name(name: str) -> str:
|
||||
return name.replace("IterDataPipe", "").replace("MapDataPipe", "")
|
||||
|
||||
|
||||
def _generate_input_args_string(obj):
|
||||
"""Generate a string for the input arguments of an object."""
|
||||
signature = inspect.signature(obj.__class__)
|
||||
input_param_names = set(signature.parameters.keys())
|
||||
result = []
|
||||
for name, value in inspect.getmembers(obj):
|
||||
if name in input_param_names:
|
||||
result.append((name, _simplify_obj_name(value)))
|
||||
return ", ".join([f"{name}={value}" for name, value in result])
|
||||
|
||||
|
||||
def _generate_iterdatapipe_msg(datapipe, simplify_dp_name: bool = False):
|
||||
output_string = (
|
||||
f"{datapipe.__class__.__name__}({_generate_input_args_string(datapipe)})"
|
||||
)
|
||||
if simplify_dp_name:
|
||||
output_string = _strip_datapipe_from_name(output_string)
|
||||
return output_string
|
||||
|
||||
|
||||
def _gen_invalid_iterdatapipe_msg(datapipe) -> str:
|
||||
return (
|
||||
"This iterator has been invalidated because another iterator has been created "
|
||||
f"from the same IterDataPipe: {_generate_iterdatapipe_msg(datapipe)}\n"
|
||||
"This may be caused multiple references to the same IterDataPipe. We recommend "
|
||||
"using `.fork()` if that is necessary."
|
||||
)
|
||||
|
||||
|
||||
_feedback_msg = (
|
||||
"\nFor feedback regarding this single iterator per IterDataPipe constraint, feel free "
|
||||
"to comment on this issue: https://github.com/pytorch/data/issues/45."
|
||||
)
|
||||
|
||||
|
||||
def _check_iterator_valid(datapipe, iterator_id, next_method_exists=False) -> None:
|
||||
r"""
|
||||
Given an instance of a DataPipe and an iterator ID, check if the IDs match, and if not, raises an exception.
|
||||
|
||||
In the case of ChildDataPipe, the ID gets compared to the one stored in `main_datapipe` as well.
|
||||
"""
|
||||
if next_method_exists:
|
||||
# This is the case where `IterDataPipe` has both `__iter__` and `__next__`.
|
||||
# The `_valid_iterator_id` should either be never set (`None`), or set by at most one
|
||||
# iterator (`0`). Otherwise, it means there are multiple iterators.
|
||||
if datapipe._valid_iterator_id is not None and datapipe._valid_iterator_id != 0:
|
||||
extra_msg = "\nNote that this exception is raised inside your IterDataPipe's a `__next__` method"
|
||||
raise RuntimeError(
|
||||
_gen_invalid_iterdatapipe_msg(datapipe) + extra_msg + _feedback_msg
|
||||
)
|
||||
elif (
|
||||
hasattr(datapipe, "_is_child_datapipe") and datapipe._is_child_datapipe is True
|
||||
):
|
||||
if hasattr(datapipe, "_check_valid_iterator_id"):
|
||||
if not datapipe._check_valid_iterator_id(iterator_id):
|
||||
raise RuntimeError(
|
||||
"This iterator has been invalidated, because a new iterator has been created "
|
||||
f"from one of the ChildDataPipes of "
|
||||
f"{_generate_iterdatapipe_msg(datapipe.main_datapipe)}."
|
||||
+ _feedback_msg
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"ChildDataPipe must have method `_check_valid_iterator_id`."
|
||||
)
|
||||
elif datapipe._valid_iterator_id != iterator_id:
|
||||
raise RuntimeError(_gen_invalid_iterdatapipe_msg(datapipe) + _feedback_msg)
|
||||
|
||||
|
||||
def _set_datapipe_valid_iterator_id(datapipe):
|
||||
"""Given a DataPipe, updates its valid iterator ID and reset the DataPipe."""
|
||||
if hasattr(datapipe, "_is_child_datapipe") and datapipe._is_child_datapipe is True:
|
||||
if hasattr(datapipe, "_set_main_datapipe_valid_iterator_id"):
|
||||
datapipe._set_main_datapipe_valid_iterator_id() # reset() is called within this method when appropriate
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"ChildDataPipe must have method `_set_main_datapipe_valid_iterator_id`."
|
||||
)
|
||||
else:
|
||||
if datapipe._valid_iterator_id is None:
|
||||
datapipe._valid_iterator_id = 0
|
||||
else:
|
||||
datapipe._valid_iterator_id += 1
|
||||
datapipe.reset()
|
||||
return datapipe._valid_iterator_id
|
||||
|
||||
|
||||
def hook_iterator(namespace) -> None:
|
||||
r"""
|
||||
Define a hook that is applied to all `__iter__` of metaclass `_DataPipeMeta`.
|
||||
|
||||
This is done for the purpose of profiling and checking if an iterator is still valid.
|
||||
"""
|
||||
|
||||
def profiler_record_fn_context(datapipe):
|
||||
if not hasattr(datapipe, "_profile_name"):
|
||||
datapipe._profile_name = _generate_iterdatapipe_msg(
|
||||
datapipe, simplify_dp_name=True
|
||||
)
|
||||
return torch.autograd.profiler.record_function(datapipe._profile_name)
|
||||
|
||||
class IteratorDecorator:
|
||||
r"""
|
||||
Wrap the iterator and modifying its `__next__` method.
|
||||
|
||||
This decorator is applied to DataPipes of which `__iter__` method is NOT a generator function.
|
||||
Those `__iter__` method commonly returns `self` but not necessarily.
|
||||
"""
|
||||
|
||||
def __init__(self, iterator, datapipe, iterator_id, has_next_method) -> None:
|
||||
self.iterator = iterator
|
||||
self.datapipe = datapipe
|
||||
self.iterator_id = iterator_id
|
||||
self._profiler_enabled = torch.autograd._profiler_enabled()
|
||||
# Check if `__iter__` returns `self` and `DataPipe` has `__next__`
|
||||
self.self_and_has_next_method = (
|
||||
self.iterator is self.datapipe and has_next_method
|
||||
)
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def _get_next(self):
|
||||
"""Return next with logic related to iterator validity, profiler, and incrementation of samples yielded."""
|
||||
_check_iterator_valid(self.datapipe, self.iterator_id)
|
||||
result = next(self.iterator)
|
||||
if not self.self_and_has_next_method:
|
||||
self.datapipe._number_of_samples_yielded += 1
|
||||
return result
|
||||
|
||||
def __next__(self):
|
||||
# TODO: Add try-except to in-place reduce traceback from the Exception
|
||||
# See: https://github.com/pytorch/data/issues/284
|
||||
if self._profiler_enabled:
|
||||
with profiler_record_fn_context(self.datapipe):
|
||||
return self._get_next()
|
||||
else: # Decided against using `contextlib.nullcontext` for performance reasons
|
||||
return self._get_next()
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.iterator, name)
|
||||
|
||||
func = namespace["__iter__"]
|
||||
|
||||
# ``__iter__`` of IterDataPipe is a generator function
|
||||
if inspect.isgeneratorfunction(func):
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrap_generator(*args, **kwargs):
|
||||
gen = func(*args, **kwargs)
|
||||
datapipe = args[0]
|
||||
if datapipe._fast_forward_iterator:
|
||||
it = datapipe._fast_forward_iterator
|
||||
datapipe._fast_forward_iterator = None
|
||||
datapipe._snapshot_state = _SnapshotState.Iterating
|
||||
while True:
|
||||
try:
|
||||
yield next(it)
|
||||
except StopIteration:
|
||||
return
|
||||
iterator_id = _set_datapipe_valid_iterator_id(
|
||||
datapipe
|
||||
) # This ID is tied to each created iterator
|
||||
_profiler_enabled = torch.autograd._profiler_enabled()
|
||||
try:
|
||||
if _profiler_enabled:
|
||||
with profiler_record_fn_context(datapipe):
|
||||
response = gen.send(None)
|
||||
else:
|
||||
response = gen.send(None)
|
||||
|
||||
while True:
|
||||
datapipe._number_of_samples_yielded += 1
|
||||
request = yield response
|
||||
# Pass through here every time `__next__` is called
|
||||
if _profiler_enabled:
|
||||
with profiler_record_fn_context(datapipe):
|
||||
_check_iterator_valid(datapipe, iterator_id)
|
||||
response = gen.send(request)
|
||||
else: # Decided against using `contextlib.nullcontext` for performance reasons
|
||||
_check_iterator_valid(datapipe, iterator_id)
|
||||
response = gen.send(request)
|
||||
except StopIteration:
|
||||
return
|
||||
except Exception as e:
|
||||
# TODO: Simplify the traceback message to skip over `response = gen.send(None)`
|
||||
# Part of https://github.com/pytorch/data/issues/284
|
||||
datapipe = args[0]
|
||||
msg = "thrown by __iter__ of"
|
||||
single_iterator_msg = "single iterator per IterDataPipe constraint"
|
||||
if hasattr(e.args, "__len__"):
|
||||
full_msg = f"{msg} {datapipe.__class__.__name__}({_generate_input_args_string(datapipe)})"
|
||||
if len(e.args) == 0 or not isinstance(
|
||||
e.args[0], str
|
||||
): # If an exception message doesn't exist
|
||||
e.args = (f"\nThis exception is {full_msg}",)
|
||||
elif msg not in e.args[0] and single_iterator_msg not in e.args[0]:
|
||||
e.args = (
|
||||
e.args[0] + f"\nThis exception is {full_msg}",
|
||||
) + e.args[1:]
|
||||
raise
|
||||
|
||||
namespace["__iter__"] = wrap_generator
|
||||
else: # ``__iter__`` of IterDataPipe is NOT a generator function
|
||||
# IterDataPipe is an iterator with both ``__iter__`` and ``__next__``
|
||||
# And ``__iter__`` may or may not return `self`
|
||||
if "__next__" in namespace: # If `__next__` exists, put a wrapper around it
|
||||
next_func = namespace["__next__"]
|
||||
|
||||
@functools.wraps(next_func)
|
||||
def wrap_next(*args, **kwargs):
|
||||
datapipe = args[0]
|
||||
if torch.autograd._profiler_enabled():
|
||||
with profiler_record_fn_context(datapipe):
|
||||
result = next_func(*args, **kwargs)
|
||||
else:
|
||||
result = next_func(*args, **kwargs)
|
||||
datapipe._number_of_samples_yielded += 1
|
||||
return result
|
||||
|
||||
namespace["__next__"] = wrap_next
|
||||
|
||||
# Note that if the `__next__` and `__iter__` do something completely unrelated. It may cause issue but
|
||||
# the user will be violating the iterator protocol. Potential issue:
|
||||
# 1. Valid iterator ID may not update or checked properly
|
||||
# 2. The number of samples yielded will be miscounted
|
||||
|
||||
# Regardless if `__next__` exists or not, `__iter__` needs a wrapper to track the number of valid iterators
|
||||
@functools.wraps(func)
|
||||
def wrap_iter(*args, **kwargs):
|
||||
iter_ret = func(*args, **kwargs)
|
||||
datapipe = args[0]
|
||||
datapipe._snapshot_state = _SnapshotState.Iterating
|
||||
if datapipe._fast_forward_iterator:
|
||||
iter_ret = datapipe._fast_forward_iterator
|
||||
datapipe._fast_forward_iterator = None
|
||||
return iter_ret
|
||||
iterator_id = _set_datapipe_valid_iterator_id(
|
||||
datapipe
|
||||
) # This ID is tied to each created iterator
|
||||
return IteratorDecorator(
|
||||
iter_ret, datapipe, iterator_id, "__next__" in namespace
|
||||
)
|
||||
|
||||
namespace["__iter__"] = wrap_iter
|
||||
@@ -0,0 +1,483 @@
|
||||
# mypy: allow-untyped-defs
|
||||
# Taking reference from official Python typing
|
||||
# https://github.com/python/cpython/blob/master/Lib/typing.py
|
||||
|
||||
import collections
|
||||
import functools
|
||||
import numbers
|
||||
import sys
|
||||
|
||||
# Please check [Note: TypeMeta and TypeAlias]
|
||||
# In case of metaclass conflict due to ABCMeta or _ProtocolMeta
|
||||
# For Python 3.9, only Protocol in typing uses metaclass
|
||||
from abc import ABCMeta
|
||||
from collections.abc import Iterator
|
||||
|
||||
# TODO: Use TypeAlias when Python 3.6 is deprecated
|
||||
from typing import (
|
||||
_eval_type, # pyrefly: ignore [missing-module-attribute]
|
||||
_GenericAlias, # pyrefly: ignore [missing-module-attribute]
|
||||
_tp_cache, # pyrefly: ignore [missing-module-attribute]
|
||||
_type_check, # pyrefly: ignore [missing-module-attribute]
|
||||
_type_repr,
|
||||
Any,
|
||||
ForwardRef,
|
||||
Generic,
|
||||
get_type_hints,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from torch.utils.data.datapipes._hook_iterator import _SnapshotState, hook_iterator
|
||||
|
||||
|
||||
class GenericMeta(ABCMeta): # type: ignore[no-redef]
|
||||
pass
|
||||
|
||||
|
||||
class Integer(numbers.Integral):
|
||||
pass
|
||||
|
||||
|
||||
class Boolean(numbers.Integral):
|
||||
pass
|
||||
|
||||
|
||||
# Python 'type' object is not subscriptable
|
||||
# Tuple[int, List, dict] -> valid
|
||||
# tuple[int, list, dict] -> invalid
|
||||
# Map Python 'type' to abstract base class
|
||||
TYPE2ABC = {
|
||||
bool: Boolean,
|
||||
int: Integer,
|
||||
float: numbers.Real,
|
||||
complex: numbers.Complex,
|
||||
dict: dict,
|
||||
list: list,
|
||||
set: set,
|
||||
tuple: tuple,
|
||||
None: type(None),
|
||||
}
|
||||
|
||||
|
||||
def issubtype(left, right, recursive=True):
|
||||
r"""
|
||||
Check if the left-side type is a subtype of the right-side type.
|
||||
|
||||
If any of type is a composite type like `Union` and `TypeVar` with
|
||||
bounds, it would be expanded into a list of types and check all
|
||||
of left-side types are subtypes of either one from right-side types.
|
||||
"""
|
||||
left = TYPE2ABC.get(left, left)
|
||||
right = TYPE2ABC.get(right, right)
|
||||
|
||||
if right is Any or left == right:
|
||||
return True
|
||||
|
||||
if isinstance(right, _GenericAlias):
|
||||
if getattr(right, "__origin__", None) is Generic:
|
||||
return True
|
||||
|
||||
if right is type(None):
|
||||
return False
|
||||
|
||||
# Right-side type
|
||||
constraints = _decompose_type(right)
|
||||
|
||||
if len(constraints) == 0 or Any in constraints:
|
||||
return True
|
||||
|
||||
if left is Any:
|
||||
return False
|
||||
|
||||
# Left-side type
|
||||
variants = _decompose_type(left)
|
||||
|
||||
# all() will return True for empty variants
|
||||
if len(variants) == 0:
|
||||
return False
|
||||
|
||||
return all(
|
||||
_issubtype_with_constraints(variant, constraints, recursive)
|
||||
for variant in variants
|
||||
)
|
||||
|
||||
|
||||
def _decompose_type(t, to_list=True):
|
||||
if isinstance(t, TypeVar):
|
||||
if t.__bound__ is not None:
|
||||
ts = [t.__bound__]
|
||||
else:
|
||||
# For T_co, __constraints__ is ()
|
||||
ts = list(t.__constraints__)
|
||||
elif hasattr(t, "__origin__") and t.__origin__ == Union:
|
||||
ts = t.__args__
|
||||
else:
|
||||
if not to_list:
|
||||
return None
|
||||
ts = [t]
|
||||
# Ignored: Generator has incompatible item type "object"; expected "Type[Any]"
|
||||
ts = [TYPE2ABC.get(_t, _t) for _t in ts] # type: ignore[misc]
|
||||
return ts
|
||||
|
||||
|
||||
def _issubtype_with_constraints(variant, constraints, recursive=True):
|
||||
r"""
|
||||
Check if the variant is a subtype of either one from constraints.
|
||||
|
||||
For composite types like `Union` and `TypeVar` with bounds, they
|
||||
would be expanded for testing.
|
||||
"""
|
||||
if variant in constraints:
|
||||
return True
|
||||
|
||||
# [Note: Subtype for Union and TypeVar]
|
||||
# Python typing is able to flatten Union[Union[...]] or Union[TypeVar].
|
||||
# But it couldn't flatten the following scenarios:
|
||||
# - Union[int, TypeVar[Union[...]]]
|
||||
# - TypeVar[TypeVar[...]]
|
||||
# So, variant and each constraint may be a TypeVar or a Union.
|
||||
# In these cases, all of inner types from the variant are required to be
|
||||
# extracted and verified as a subtype of any constraint. And, all of
|
||||
# inner types from any constraint being a TypeVar or a Union are
|
||||
# also required to be extracted and verified if the variant belongs to
|
||||
# any of them.
|
||||
|
||||
# Variant
|
||||
vs = _decompose_type(variant, to_list=False)
|
||||
|
||||
# Variant is TypeVar or Union
|
||||
if vs is not None:
|
||||
return all(_issubtype_with_constraints(v, constraints, recursive) for v in vs)
|
||||
|
||||
# Variant is not TypeVar or Union
|
||||
if hasattr(variant, "__origin__") and variant.__origin__ is not None:
|
||||
v_origin = variant.__origin__
|
||||
# In Python-3.9 typing library untyped generics do not have args
|
||||
v_args = getattr(variant, "__args__", None)
|
||||
else:
|
||||
v_origin = variant
|
||||
v_args = None
|
||||
|
||||
# Constraints
|
||||
for constraint in constraints:
|
||||
cs = _decompose_type(constraint, to_list=False)
|
||||
|
||||
# Constraint is TypeVar or Union
|
||||
if cs is not None:
|
||||
if _issubtype_with_constraints(variant, cs, recursive):
|
||||
return True
|
||||
# Constraint is not TypeVar or Union
|
||||
else:
|
||||
# __origin__ can be None for plain list, tuple, ... in Python 3.6
|
||||
if hasattr(constraint, "__origin__") and constraint.__origin__ is not None:
|
||||
c_origin = constraint.__origin__
|
||||
if v_origin == c_origin:
|
||||
if not recursive:
|
||||
return True
|
||||
# In Python-3.9 typing library untyped generics do not have args
|
||||
c_args = getattr(constraint, "__args__", None)
|
||||
if c_args is None or len(c_args) == 0:
|
||||
return True
|
||||
if (
|
||||
v_args is not None
|
||||
and len(v_args) == len(c_args)
|
||||
and all(
|
||||
issubtype(v_arg, c_arg)
|
||||
for v_arg, c_arg in zip(v_args, c_args, strict=True)
|
||||
)
|
||||
):
|
||||
return True
|
||||
# Tuple[int] -> Tuple
|
||||
else:
|
||||
if v_origin == constraint:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def issubinstance(data, data_type):
|
||||
if not issubtype(type(data), data_type, recursive=False):
|
||||
return False
|
||||
|
||||
# In Python-3.9 typing library __args__ attribute is not defined for untyped generics
|
||||
dt_args = getattr(data_type, "__args__", None)
|
||||
if isinstance(data, tuple):
|
||||
if dt_args is None or len(dt_args) == 0:
|
||||
return True
|
||||
if len(dt_args) != len(data):
|
||||
return False
|
||||
return all(issubinstance(d, t) for d, t in zip(data, dt_args, strict=True))
|
||||
elif isinstance(data, (list, set)):
|
||||
if dt_args is None or len(dt_args) == 0:
|
||||
return True
|
||||
t = dt_args[0]
|
||||
return all(issubinstance(d, t) for d in data)
|
||||
elif isinstance(data, dict):
|
||||
if dt_args is None or len(dt_args) == 0:
|
||||
return True
|
||||
kt, vt = dt_args
|
||||
return all(
|
||||
issubinstance(k, kt) and issubinstance(v, vt) for k, v in data.items()
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# [Note: TypeMeta and TypeAlias]
|
||||
# In order to keep compatibility for Python 3.6, use Meta for the typing.
|
||||
# TODO: When PyTorch drops the support for Python 3.6, it can be converted
|
||||
# into the Alias system and using `__class_getitem__` for DataPipe. The
|
||||
# typing system will gain benefit of performance and resolving metaclass
|
||||
# conflicts as elaborated in https://www.python.org/dev/peps/pep-0560/
|
||||
|
||||
|
||||
class _DataPipeType:
|
||||
r"""Save type annotation in `param`."""
|
||||
|
||||
def __init__(self, param) -> None:
|
||||
self.param = param
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return _type_repr(self.param)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, _DataPipeType):
|
||||
return self.param == other.param
|
||||
return NotImplemented
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.param)
|
||||
|
||||
def issubtype(self, other):
|
||||
if isinstance(other.param, _GenericAlias):
|
||||
if getattr(other.param, "__origin__", None) is Generic:
|
||||
return True
|
||||
if isinstance(other, _DataPipeType):
|
||||
return issubtype(self.param, other.param)
|
||||
if isinstance(other, type):
|
||||
return issubtype(self.param, other)
|
||||
raise TypeError(f"Expected '_DataPipeType' or 'type', but found {type(other)}")
|
||||
|
||||
def issubtype_of_instance(self, other):
|
||||
return issubinstance(other, self.param)
|
||||
|
||||
|
||||
# Default type for DataPipe without annotation
|
||||
_T_co = TypeVar("_T_co", covariant=True)
|
||||
# pyrefly: ignore [invalid-annotation]
|
||||
_DEFAULT_TYPE = _DataPipeType(Generic[_T_co])
|
||||
|
||||
|
||||
class _DataPipeMeta(GenericMeta):
|
||||
r"""
|
||||
Metaclass for `DataPipe`.
|
||||
|
||||
Add `type` attribute and `__init_subclass__` based on the type, and validate the return hint of `__iter__`.
|
||||
|
||||
Note that there is subclass `_IterDataPipeMeta` specifically for `IterDataPipe`.
|
||||
"""
|
||||
|
||||
type: _DataPipeType
|
||||
|
||||
def __new__(cls, name, bases, namespace, **kwargs):
|
||||
return super().__new__(cls, name, bases, namespace, **kwargs) # type: ignore[call-overload]
|
||||
|
||||
# TODO: the statements below are not reachable by design as there is a bug and typing is low priority for now.
|
||||
cls.__origin__ = None
|
||||
if "type" in namespace:
|
||||
return super().__new__(cls, name, bases, namespace, **kwargs) # type: ignore[call-overload]
|
||||
|
||||
namespace["__type_class__"] = False
|
||||
# For plain derived class without annotation
|
||||
for base in bases:
|
||||
if isinstance(base, _DataPipeMeta):
|
||||
return super().__new__(cls, name, bases, namespace, **kwargs) # type: ignore[call-overload]
|
||||
|
||||
namespace.update(
|
||||
{"type": _DEFAULT_TYPE, "__init_subclass__": _dp_init_subclass}
|
||||
)
|
||||
return super().__new__(cls, name, bases, namespace, **kwargs) # type: ignore[call-overload]
|
||||
|
||||
def __init__(self, name, bases, namespace, **kwargs) -> None:
|
||||
super().__init__(name, bases, namespace, **kwargs) # type: ignore[call-overload]
|
||||
|
||||
# TODO: Fix isinstance bug
|
||||
@_tp_cache
|
||||
def _getitem_(self, params):
|
||||
if params is None:
|
||||
raise TypeError(f"{self.__name__}[t]: t can not be None")
|
||||
if isinstance(params, str):
|
||||
params = ForwardRef(params)
|
||||
if not isinstance(params, tuple):
|
||||
params = (params,)
|
||||
|
||||
msg = f"{self.__name__}[t]: t must be a type"
|
||||
params = tuple(_type_check(p, msg) for p in params)
|
||||
|
||||
if isinstance(self.type.param, _GenericAlias):
|
||||
orig = getattr(self.type.param, "__origin__", None)
|
||||
if isinstance(orig, type) and orig is not Generic:
|
||||
p = self.type.param[params] # type: ignore[index]
|
||||
t = _DataPipeType(p)
|
||||
l = len(str(self.type)) + 2
|
||||
name = self.__name__[:-l]
|
||||
name = name + "[" + str(t) + "]"
|
||||
bases = (self,) + self.__bases__
|
||||
return self.__class__(
|
||||
name,
|
||||
bases,
|
||||
{
|
||||
"__init_subclass__": _dp_init_subclass,
|
||||
"type": t,
|
||||
"__type_class__": True,
|
||||
},
|
||||
)
|
||||
|
||||
if len(params) > 1:
|
||||
raise TypeError(
|
||||
f"Too many parameters for {self} actual {len(params)}, expected 1"
|
||||
)
|
||||
|
||||
t = _DataPipeType(params[0])
|
||||
|
||||
if not t.issubtype(self.type):
|
||||
raise TypeError(
|
||||
f"Can not subclass a DataPipe[{t}] from DataPipe[{self.type}]"
|
||||
)
|
||||
|
||||
# Types are equal, fast path for inheritance
|
||||
if self.type == t:
|
||||
return self
|
||||
|
||||
name = self.__name__ + "[" + str(t) + "]"
|
||||
bases = (self,) + self.__bases__
|
||||
|
||||
return self.__class__(
|
||||
name,
|
||||
bases,
|
||||
{"__init_subclass__": _dp_init_subclass, "__type_class__": True, "type": t},
|
||||
)
|
||||
|
||||
# TODO: Fix isinstance bug
|
||||
def _eq_(self, other):
|
||||
if not isinstance(other, _DataPipeMeta):
|
||||
return NotImplemented
|
||||
if self.__origin__ is None or other.__origin__ is None: # type: ignore[has-type]
|
||||
return self is other
|
||||
return (
|
||||
self.__origin__ == other.__origin__ # type: ignore[has-type]
|
||||
and self.type == other.type
|
||||
)
|
||||
|
||||
# TODO: Fix isinstance bug
|
||||
def _hash_(self):
|
||||
return hash((self.__name__, self.type))
|
||||
|
||||
|
||||
class _IterDataPipeMeta(_DataPipeMeta):
|
||||
r"""
|
||||
Metaclass for `IterDataPipe` and inherits from `_DataPipeMeta`.
|
||||
|
||||
Add various functions for behaviors specific to `IterDataPipe`.
|
||||
"""
|
||||
|
||||
def __new__(cls, name, bases, namespace, **kwargs):
|
||||
if "reset" in namespace:
|
||||
reset_func = namespace["reset"]
|
||||
|
||||
@functools.wraps(reset_func)
|
||||
def conditional_reset(*args, **kwargs) -> None:
|
||||
r"""
|
||||
Only execute DataPipe's `reset()` method if `_SnapshotState` is `Iterating` or `NotStarted`.
|
||||
|
||||
This allows recently restored DataPipe to preserve its restored state during the initial `__iter__` call.
|
||||
"""
|
||||
datapipe = args[0]
|
||||
if datapipe._snapshot_state in (
|
||||
_SnapshotState.Iterating,
|
||||
_SnapshotState.NotStarted,
|
||||
):
|
||||
# Reset `NotStarted` is necessary because the `source_datapipe` of a DataPipe might have
|
||||
# already begun iterating.
|
||||
datapipe._number_of_samples_yielded = 0
|
||||
datapipe._fast_forward_iterator = None
|
||||
reset_func(*args, **kwargs)
|
||||
datapipe._snapshot_state = _SnapshotState.Iterating
|
||||
|
||||
namespace["reset"] = conditional_reset
|
||||
|
||||
if "__iter__" in namespace:
|
||||
hook_iterator(namespace)
|
||||
return super().__new__(cls, name, bases, namespace, **kwargs) # type: ignore[call-overload]
|
||||
|
||||
|
||||
def _dp_init_subclass(sub_cls, *args, **kwargs) -> None:
|
||||
# Add function for datapipe instance to reinforce the type
|
||||
sub_cls.reinforce_type = reinforce_type
|
||||
|
||||
# TODO:
|
||||
# - add global switch for type checking at compile-time
|
||||
|
||||
# Ignore internal type class
|
||||
if getattr(sub_cls, "__type_class__", False):
|
||||
return
|
||||
|
||||
# Check if the string type is valid
|
||||
if isinstance(sub_cls.type.param, ForwardRef):
|
||||
base_globals = sys.modules[sub_cls.__module__].__dict__
|
||||
try:
|
||||
param = _eval_type(sub_cls.type.param, base_globals, locals())
|
||||
sub_cls.type.param = param
|
||||
except TypeError as e:
|
||||
raise TypeError(
|
||||
f"{sub_cls.type.param.__forward_arg__} is not supported by Python typing"
|
||||
) from e
|
||||
|
||||
if "__iter__" in sub_cls.__dict__:
|
||||
iter_fn = sub_cls.__dict__["__iter__"]
|
||||
hints = get_type_hints(iter_fn)
|
||||
if "return" in hints:
|
||||
return_hint = hints["return"]
|
||||
# Plain Return Hint for Python 3.6
|
||||
if return_hint == Iterator:
|
||||
return
|
||||
if not (
|
||||
hasattr(return_hint, "__origin__")
|
||||
and (
|
||||
return_hint.__origin__ == Iterator
|
||||
or return_hint.__origin__ == collections.abc.Iterator
|
||||
)
|
||||
):
|
||||
raise TypeError(
|
||||
"Expected 'Iterator' as the return annotation for `__iter__` of {}"
|
||||
", but found {}".format(
|
||||
sub_cls.__name__, _type_repr(hints["return"])
|
||||
)
|
||||
)
|
||||
data_type = return_hint.__args__[0]
|
||||
if not issubtype(data_type, sub_cls.type.param):
|
||||
raise TypeError(
|
||||
f"Expected return type of '__iter__' as a subtype of {sub_cls.type},"
|
||||
f" but found {_type_repr(data_type)} for {sub_cls.__name__}"
|
||||
)
|
||||
|
||||
|
||||
def reinforce_type(self, expected_type):
|
||||
r"""
|
||||
Reinforce the type for DataPipe instance.
|
||||
|
||||
And the 'expected_type' is required to be a subtype of the original type
|
||||
hint to restrict the type requirement of DataPipe instance.
|
||||
"""
|
||||
if isinstance(expected_type, tuple):
|
||||
expected_type = tuple[expected_type] # type: ignore[valid-type]
|
||||
_type_check(expected_type, msg="'expected_type' must be a type")
|
||||
|
||||
if not issubtype(expected_type, self.type.param):
|
||||
raise TypeError(
|
||||
f"Expected 'expected_type' as subtype of {self.type}, but found {_type_repr(expected_type)}"
|
||||
)
|
||||
|
||||
self.type = _DataPipeType(expected_type)
|
||||
return self
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
from torch.utils.data.datapipes.dataframe.dataframes import (
|
||||
CaptureDataFrame,
|
||||
DFIterDataPipe,
|
||||
)
|
||||
from torch.utils.data.datapipes.dataframe.datapipes import DataFramesAsTuplesPipe
|
||||
|
||||
|
||||
__all__ = ["CaptureDataFrame", "DFIterDataPipe", "DataFramesAsTuplesPipe"]
|
||||
|
||||
# Please keep this list sorted
|
||||
if __all__ != sorted(__all__):
|
||||
raise AssertionError("__all__ is not sorted")
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from typing import Any
|
||||
|
||||
|
||||
_pandas: Any = None
|
||||
_WITH_PANDAS: bool | None = None
|
||||
|
||||
|
||||
def _try_import_pandas() -> bool:
|
||||
try:
|
||||
import pandas # type: ignore[import]
|
||||
|
||||
global _pandas
|
||||
_pandas = pandas
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
# pandas used only for prototyping, will be shortly replaced with TorchArrow
|
||||
def _with_pandas() -> bool:
|
||||
global _WITH_PANDAS
|
||||
if _WITH_PANDAS is None:
|
||||
_WITH_PANDAS = _try_import_pandas()
|
||||
return _WITH_PANDAS
|
||||
|
||||
|
||||
class PandasWrapper:
|
||||
@classmethod
|
||||
def create_dataframe(cls, data, columns):
|
||||
if not _with_pandas():
|
||||
raise RuntimeError("DataFrames prototype requires pandas to function")
|
||||
return _pandas.DataFrame(data, columns=columns) # type: ignore[union-attr]
|
||||
|
||||
@classmethod
|
||||
def is_dataframe(cls, data):
|
||||
if not _with_pandas():
|
||||
return False
|
||||
return isinstance(data, _pandas.core.frame.DataFrame) # type: ignore[union-attr]
|
||||
|
||||
@classmethod
|
||||
def is_column(cls, data):
|
||||
if not _with_pandas():
|
||||
return False
|
||||
return isinstance(data, _pandas.core.series.Series) # type: ignore[union-attr]
|
||||
|
||||
@classmethod
|
||||
def iterate(cls, data):
|
||||
if not _with_pandas():
|
||||
raise RuntimeError("DataFrames prototype requires pandas to function")
|
||||
yield from data.itertuples(index=False)
|
||||
|
||||
@classmethod
|
||||
def concat(cls, buffer):
|
||||
if not _with_pandas():
|
||||
raise RuntimeError("DataFrames prototype requires pandas to function")
|
||||
return _pandas.concat(buffer) # type: ignore[union-attr]
|
||||
|
||||
@classmethod
|
||||
def get_item(cls, data, idx):
|
||||
if not _with_pandas():
|
||||
raise RuntimeError("DataFrames prototype requires pandas to function")
|
||||
return data[idx : idx + 1]
|
||||
|
||||
@classmethod
|
||||
def get_len(cls, df):
|
||||
if not _with_pandas():
|
||||
raise RuntimeError("DataFrames prototype requires pandas to function")
|
||||
return len(df.index)
|
||||
|
||||
@classmethod
|
||||
def get_columns(cls, df):
|
||||
if not _with_pandas():
|
||||
raise RuntimeError("DataFrames prototype requires pandas to function")
|
||||
return list(df.columns.values.tolist())
|
||||
|
||||
|
||||
# When you build own implementation just override it with dataframe_wrapper.set_df_wrapper(new_wrapper_class)
|
||||
default_wrapper = PandasWrapper
|
||||
|
||||
|
||||
def get_df_wrapper():
|
||||
return default_wrapper
|
||||
|
||||
|
||||
def set_df_wrapper(wrapper) -> None:
|
||||
global default_wrapper
|
||||
default_wrapper = wrapper
|
||||
|
||||
|
||||
def create_dataframe(data, columns=None):
|
||||
wrapper = get_df_wrapper()
|
||||
return wrapper.create_dataframe(data, columns)
|
||||
|
||||
|
||||
def is_dataframe(data):
|
||||
wrapper = get_df_wrapper()
|
||||
return wrapper.is_dataframe(data)
|
||||
|
||||
|
||||
def get_columns(data):
|
||||
wrapper = get_df_wrapper()
|
||||
return wrapper.get_columns(data)
|
||||
|
||||
|
||||
def is_column(data):
|
||||
wrapper = get_df_wrapper()
|
||||
return wrapper.is_column(data)
|
||||
|
||||
|
||||
def concat(buffer):
|
||||
wrapper = get_df_wrapper()
|
||||
return wrapper.concat(buffer)
|
||||
|
||||
|
||||
def iterate(data):
|
||||
wrapper = get_df_wrapper()
|
||||
return wrapper.iterate(data)
|
||||
|
||||
|
||||
def get_item(data, idx):
|
||||
wrapper = get_df_wrapper()
|
||||
return wrapper.get_item(data, idx)
|
||||
|
||||
|
||||
def get_len(df):
|
||||
wrapper = get_df_wrapper()
|
||||
return wrapper.get_len(df)
|
||||
+468
@@ -0,0 +1,468 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from typing import Any, NoReturn
|
||||
|
||||
from torch.utils.data.datapipes._decorator import functional_datapipe
|
||||
from torch.utils.data.datapipes.dataframe.structures import DataChunkDF
|
||||
from torch.utils.data.datapipes.datapipe import DFIterDataPipe, IterDataPipe
|
||||
|
||||
|
||||
# TODO(VitalyFedyunin): Add error when two different traces get combined
|
||||
|
||||
__all__ = [
|
||||
"Capture",
|
||||
"CaptureA",
|
||||
"CaptureAdd",
|
||||
"CaptureCall",
|
||||
"CaptureControl",
|
||||
"CaptureDataFrame",
|
||||
"CaptureDataFrameWithDataPipeOps",
|
||||
"CaptureF",
|
||||
"CaptureGetAttr",
|
||||
"CaptureGetItem",
|
||||
"CaptureInitial",
|
||||
"CaptureLikeMock",
|
||||
"CaptureMul",
|
||||
"CaptureSetItem",
|
||||
"CaptureSub",
|
||||
"CaptureVariable",
|
||||
"CaptureVariableAssign",
|
||||
"DataFrameTracer",
|
||||
"DataFrameTracedOps",
|
||||
"disable_capture",
|
||||
"get_val",
|
||||
]
|
||||
|
||||
|
||||
def disable_capture() -> None:
|
||||
CaptureControl.disabled = True
|
||||
|
||||
|
||||
class CaptureControl:
|
||||
disabled = False
|
||||
|
||||
|
||||
class DataFrameTracedOps(DFIterDataPipe):
|
||||
def __init__(self, source_datapipe, output_var) -> None:
|
||||
super().__init__()
|
||||
self.source_datapipe = source_datapipe
|
||||
self.output_var = output_var
|
||||
|
||||
def __iter__(self):
|
||||
for item in self.source_datapipe:
|
||||
yield self.output_var.apply_ops(item)
|
||||
|
||||
|
||||
# TODO(VitalyFedyunin): Extract this list from the DFIterDataPipe registered functions
|
||||
DATAPIPES_OPS = [
|
||||
"_dataframes_as_tuples",
|
||||
"groupby",
|
||||
"_dataframes_filter",
|
||||
"map",
|
||||
"to_datapipe",
|
||||
"shuffle",
|
||||
"concat",
|
||||
"batch",
|
||||
"_dataframes_per_row",
|
||||
"_dataframes_concat",
|
||||
"_dataframes_shuffle",
|
||||
]
|
||||
|
||||
UNIMPLEMENTED_ATTR = ["__deepcopy__", "__setstate__", "is_shardable", "apply_sharding"]
|
||||
|
||||
|
||||
class Capture:
|
||||
# TODO: All operations are shared across entire InitialCapture, need to figure out what if we join two captures
|
||||
|
||||
def __init__(self, schema_df=None) -> None:
|
||||
self.ctx = {"operations": [], "variables": [], "schema_df": schema_df}
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self._ops_str()
|
||||
|
||||
def _ops_str(self):
|
||||
res = ""
|
||||
for op in self.ctx["operations"]:
|
||||
if len(res) > 0:
|
||||
res += "\n"
|
||||
res += str(op)
|
||||
return res
|
||||
|
||||
def __getstate__(self):
|
||||
# TODO(VitalyFedyunin): Currently can't pickle (why?)
|
||||
self.ctx["schema_df"] = None
|
||||
for var in self.ctx["variables"]:
|
||||
var.calculated_value = None
|
||||
state = {}
|
||||
for item in self.__dict__:
|
||||
state[item] = getattr(self, item)
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
for k, v in state.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
def __getattr__(self, attrname):
|
||||
if attrname == "kwarg" or attrname == "kwargs":
|
||||
raise RuntimeError("no kwargs!")
|
||||
if attrname == "__deepcopy__":
|
||||
raise AttributeError
|
||||
result = CaptureGetAttr(self, attrname, ctx=self.ctx)
|
||||
return result
|
||||
|
||||
def __getitem__(self, key):
|
||||
return CaptureGetItem(self, key, ctx=self.ctx)
|
||||
|
||||
def __setitem__(self, key, value) -> None:
|
||||
self.ctx["operations"].append(CaptureSetItem(self, key, value, ctx=self.ctx))
|
||||
|
||||
def __add__(self, add_val):
|
||||
res = CaptureAdd(self, add_val, ctx=self.ctx)
|
||||
var = CaptureVariable(res, ctx=self.ctx)
|
||||
self.ctx["operations"].append(
|
||||
CaptureVariableAssign(variable=var, value=res, ctx=self.ctx)
|
||||
)
|
||||
return var
|
||||
|
||||
def __sub__(self, add_val):
|
||||
res = CaptureSub(self, add_val, ctx=self.ctx)
|
||||
var = CaptureVariable(res, ctx=self.ctx)
|
||||
self.ctx["operations"].append(
|
||||
CaptureVariableAssign(variable=var, value=res, ctx=self.ctx)
|
||||
)
|
||||
return var
|
||||
|
||||
def __mul__(self, add_val):
|
||||
res = CaptureMul(self, add_val, ctx=self.ctx)
|
||||
var = CaptureVariable(res, ctx=self.ctx)
|
||||
t = CaptureVariableAssign(variable=var, value=res, ctx=self.ctx)
|
||||
self.ctx["operations"].append(t)
|
||||
return var
|
||||
|
||||
def _is_context_empty(self):
|
||||
return len(self.ctx["operations"]) == 0 and len(self.ctx["variables"]) == 0
|
||||
|
||||
def apply_ops_2(self, dataframe) -> None:
|
||||
# TODO(VitalyFedyunin): Make this calculation thread safe (as currently it updates pointer)
|
||||
self.ctx["variables"][0].calculated_value = dataframe
|
||||
for op in self.ctx["operations"]:
|
||||
op.execute()
|
||||
|
||||
@property
|
||||
def columns(self):
|
||||
self.apply_ops_2(self.ctx["schema_df"])
|
||||
value = self.execute()
|
||||
return value.columns
|
||||
|
||||
# TODO(VitalyFedyunin): Add tests
|
||||
# TODO(VitalyFedyunin): Need to join context if one of them are empty because we used capture
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
# TODO: Check if args or kwargs have more than one different context
|
||||
if self._is_context_empty():
|
||||
# TODO: Allow CaptureA to take context from mock
|
||||
for arg in args:
|
||||
if isinstance(arg, Capture) and not arg._is_context_empty():
|
||||
self.ctx = arg.ctx
|
||||
break
|
||||
if self._is_context_empty():
|
||||
for k, v in kwargs.items():
|
||||
if isinstance(k, Capture) and not k._is_context_empty():
|
||||
self.ctx = k.ctx
|
||||
break
|
||||
if isinstance(v, Capture) and not v._is_context_empty():
|
||||
self.ctx = v.ctx
|
||||
break
|
||||
|
||||
res = CaptureCall(self, ctx=self.ctx, args=args, kwargs=kwargs)
|
||||
var = CaptureVariable(None, ctx=self.ctx)
|
||||
t = CaptureVariableAssign(ctx=self.ctx, variable=var, value=res)
|
||||
self.ctx["operations"].append(t)
|
||||
return var
|
||||
|
||||
|
||||
class CaptureF(Capture):
|
||||
def __init__(self, ctx=None, **kwargs) -> None:
|
||||
super().__init__()
|
||||
if ctx is None:
|
||||
self.ctx = {"operations": [], "variables": []}
|
||||
else:
|
||||
self.ctx = ctx
|
||||
self.kwargs = kwargs
|
||||
|
||||
|
||||
class CaptureA(CaptureF):
|
||||
def __str__(self) -> str:
|
||||
return f"{self.kwargs['name']}"
|
||||
|
||||
def execute(self):
|
||||
value = self.kwargs["real_attribute"]
|
||||
return value
|
||||
|
||||
|
||||
class CaptureLikeMock:
|
||||
def __init__(self, name) -> None:
|
||||
import unittest.mock as mock
|
||||
|
||||
# TODO(VitalyFedyunin): Do not use private function here, copy own implementation instead.
|
||||
get_target, attribute = mock._get_target(name) # type: ignore[attr-defined]
|
||||
self.get_target = get_target
|
||||
self.attribute = attribute
|
||||
self.name = name
|
||||
|
||||
def __enter__(self):
|
||||
self.save = getattr(self.get_target(), self.attribute)
|
||||
capt = CaptureA(name=self.name, real_attribute=self.save)
|
||||
setattr(self.get_target(), self.attribute, capt)
|
||||
|
||||
def __exit__(self, *exc_info):
|
||||
setattr(self.get_target(), self.attribute, self.save)
|
||||
|
||||
|
||||
class CaptureCall(Capture):
|
||||
def __init__(self, callable, ctx=None, **kwargs) -> None:
|
||||
super().__init__()
|
||||
if ctx is None:
|
||||
self.ctx = {"operations": [], "variables": []}
|
||||
else:
|
||||
self.ctx = ctx
|
||||
self.kwargs = kwargs
|
||||
self.callable = callable
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "{callable}({args},{kwargs})".format(
|
||||
callable=self.callable, **self.kwargs
|
||||
)
|
||||
|
||||
def execute(self):
|
||||
# TODO: VitalyFedyunin execute kwargs and maybe nested structures
|
||||
executed_args = []
|
||||
for arg in self.kwargs["args"]:
|
||||
if isinstance(arg, Capture):
|
||||
executed_args.append(arg.execute())
|
||||
else:
|
||||
executed_args.append(arg)
|
||||
left = get_val(self.callable)
|
||||
return left(*executed_args, **self.kwargs["kwargs"])
|
||||
|
||||
|
||||
class CaptureVariableAssign(CaptureF):
|
||||
def __str__(self) -> str:
|
||||
variable = self.kwargs["variable"]
|
||||
value = self.kwargs["value"]
|
||||
return f"{variable} = {value}"
|
||||
|
||||
def execute(self) -> None:
|
||||
self.kwargs["variable"].calculated_value = self.kwargs["value"].execute()
|
||||
|
||||
|
||||
class CaptureVariable(Capture):
|
||||
# TODO(VitalyFedyunin): This should be atomic and thread safe
|
||||
names_idx = 0
|
||||
|
||||
def __init__(self, value, ctx) -> None:
|
||||
super().__init__()
|
||||
if CaptureControl.disabled:
|
||||
raise RuntimeError("Attempting to create capture variable with capture off")
|
||||
self.ctx = ctx
|
||||
self.value = value
|
||||
self.name = f"var_{CaptureVariable.names_idx}"
|
||||
CaptureVariable.names_idx += 1
|
||||
self.ctx["variables"].append(self)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def execute(self):
|
||||
return self.calculated_value
|
||||
|
||||
def apply_ops(self, dataframe):
|
||||
# TODO(VitalyFedyunin): Make this calculation thread safe (as currently it updates pointer)
|
||||
self.ctx["variables"][0].calculated_value = dataframe
|
||||
for op in self.ctx["operations"]:
|
||||
op.execute()
|
||||
return self.calculated_value
|
||||
|
||||
|
||||
class CaptureGetItem(Capture):
|
||||
def __init__(self, left, key, ctx) -> None:
|
||||
super().__init__()
|
||||
self.ctx = ctx
|
||||
self.left = left
|
||||
self.key = key
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.left}[{get_val(self.key)}]"
|
||||
|
||||
def execute(self):
|
||||
left = self.left.execute()
|
||||
return left[self.key]
|
||||
|
||||
|
||||
class CaptureSetItem(Capture):
|
||||
def __init__(self, left, key, value, ctx) -> None:
|
||||
super().__init__()
|
||||
self.ctx = ctx
|
||||
self.left = left
|
||||
self.key = key
|
||||
self.value = value
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.left}[{get_val(self.key)}] = {self.value}"
|
||||
|
||||
def execute(self) -> None:
|
||||
left = self.left.execute()
|
||||
value = self.value.execute()
|
||||
left[self.key] = value
|
||||
|
||||
|
||||
class CaptureAdd(Capture):
|
||||
def __init__(self, left, right, ctx) -> None:
|
||||
super().__init__()
|
||||
self.ctx = ctx
|
||||
self.left = left
|
||||
self.right = right
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.left} + {self.right}"
|
||||
|
||||
def execute(self):
|
||||
return get_val(self.left) + get_val(self.right)
|
||||
|
||||
|
||||
class CaptureMul(Capture):
|
||||
def __init__(self, left, right, ctx) -> None:
|
||||
super().__init__()
|
||||
self.ctx = ctx
|
||||
self.left = left
|
||||
self.right = right
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.left} * {self.right}"
|
||||
|
||||
def execute(self):
|
||||
return get_val(self.left) * get_val(self.right)
|
||||
|
||||
|
||||
class CaptureSub(Capture):
|
||||
def __init__(self, left, right, ctx) -> None:
|
||||
super().__init__()
|
||||
self.ctx = ctx
|
||||
self.left = left
|
||||
self.right = right
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.left} - {self.right}"
|
||||
|
||||
def execute(self):
|
||||
return get_val(self.left) - get_val(self.right)
|
||||
|
||||
|
||||
class CaptureGetAttr(Capture):
|
||||
def __init__(self, src, name, ctx) -> None:
|
||||
super().__init__()
|
||||
self.ctx = ctx
|
||||
self.src = src
|
||||
self.name = name
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.src}.{self.name}"
|
||||
|
||||
def execute(self):
|
||||
val = get_val(self.src)
|
||||
return getattr(val, self.name)
|
||||
|
||||
|
||||
def get_val(capture):
|
||||
if isinstance(capture, Capture):
|
||||
return capture.execute()
|
||||
elif isinstance(capture, str):
|
||||
return f'"{capture}"'
|
||||
else:
|
||||
return capture
|
||||
|
||||
|
||||
class CaptureInitial(CaptureVariable):
|
||||
def __init__(self, schema_df=None) -> None:
|
||||
# pyrefly: ignore [bad-assignment]
|
||||
new_ctx: dict[str, list[Any]] = {
|
||||
"operations": [],
|
||||
"variables": [],
|
||||
"schema_df": schema_df,
|
||||
}
|
||||
super().__init__(None, new_ctx)
|
||||
self.name = f"input_{self.name}"
|
||||
|
||||
|
||||
class CaptureDataFrame(CaptureInitial):
|
||||
pass
|
||||
|
||||
|
||||
class CaptureDataFrameWithDataPipeOps(CaptureDataFrame):
|
||||
def as_datapipe(self):
|
||||
return DataFrameTracedOps(self.ctx["variables"][0].source_datapipe, self)
|
||||
|
||||
def raw_iterator(self):
|
||||
return self.as_datapipe().__iter__()
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._dataframes_as_tuples())
|
||||
|
||||
def batch(self, batch_size=10, drop_last: bool = False, wrapper_class=DataChunkDF):
|
||||
dp = self._dataframes_per_row()._dataframes_concat(batch_size)
|
||||
dp = dp.as_datapipe().batch(1, drop_last=drop_last, wrapper_class=wrapper_class)
|
||||
dp._dp_contains_dataframe = True
|
||||
return dp
|
||||
|
||||
def groupby(
|
||||
self,
|
||||
group_key_fn,
|
||||
*,
|
||||
buffer_size=10000,
|
||||
group_size=None,
|
||||
guaranteed_group_size=None,
|
||||
drop_remaining=False,
|
||||
):
|
||||
dp = self._dataframes_per_row()
|
||||
dp = dp.as_datapipe().groupby(
|
||||
group_key_fn,
|
||||
buffer_size=buffer_size,
|
||||
group_size=group_size,
|
||||
guaranteed_group_size=guaranteed_group_size,
|
||||
drop_remaining=drop_remaining,
|
||||
)
|
||||
return dp
|
||||
|
||||
def shuffle(self, *args, **kwargs):
|
||||
return self._dataframes_shuffle(*args, **kwargs)
|
||||
|
||||
def filter(self, *args, **kwargs):
|
||||
return self._dataframes_filter(*args, **kwargs)
|
||||
|
||||
def collate(self, *args, **kwargs) -> NoReturn:
|
||||
raise RuntimeError("Can't collate unbatched DataFrames stream")
|
||||
|
||||
def __getattr__(self, attrname): # ?
|
||||
if attrname in UNIMPLEMENTED_ATTR:
|
||||
raise AttributeError("Attempting to get ", attrname)
|
||||
if attrname in DATAPIPES_OPS:
|
||||
return (self.as_datapipe()).__getattr__(attrname)
|
||||
return super().__getattr__(attrname)
|
||||
|
||||
|
||||
@functional_datapipe("trace_as_dataframe")
|
||||
class DataFrameTracer(CaptureDataFrameWithDataPipeOps, IterDataPipe): # type: ignore[misc]
|
||||
source_datapipe: Any | None = None
|
||||
|
||||
# TODO(VitalyFedyunin): Must implement all special functions of datapipes
|
||||
|
||||
def set_shuffle_settings(self, *args, **kwargs) -> None:
|
||||
pass
|
||||
|
||||
def is_shardable(self) -> bool:
|
||||
return False
|
||||
|
||||
def __init__(self, source_datapipe, schema_df=None) -> None:
|
||||
self.source_datapipe = source_datapipe
|
||||
if schema_df is None:
|
||||
schema_df = next(iter(self.source_datapipe))
|
||||
super().__init__(schema_df=schema_df)
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
from torch.utils.data.datapipes._decorator import functional_datapipe
|
||||
from torch.utils.data.datapipes.dataframe import dataframe_wrapper as df_wrapper
|
||||
from torch.utils.data.datapipes.datapipe import DFIterDataPipe, IterDataPipe
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ConcatDataFramesPipe",
|
||||
"DataFramesAsTuplesPipe",
|
||||
"ExampleAggregateAsDataFrames",
|
||||
"FilterDataFramesPipe",
|
||||
"PerRowDataFramesPipe",
|
||||
"ShuffleDataFramesPipe",
|
||||
]
|
||||
|
||||
|
||||
@functional_datapipe("_dataframes_as_tuples")
|
||||
class DataFramesAsTuplesPipe(IterDataPipe):
|
||||
def __init__(self, source_datapipe) -> None:
|
||||
super().__init__()
|
||||
self.source_datapipe = source_datapipe
|
||||
|
||||
def __iter__(self):
|
||||
for df in self.source_datapipe:
|
||||
# for record in df.to_records(index=False):
|
||||
yield from df_wrapper.iterate(df)
|
||||
|
||||
|
||||
@functional_datapipe("_dataframes_per_row", enable_df_api_tracing=True)
|
||||
class PerRowDataFramesPipe(DFIterDataPipe):
|
||||
def __init__(self, source_datapipe) -> None:
|
||||
self.source_datapipe = source_datapipe
|
||||
|
||||
def __iter__(self):
|
||||
for df in self.source_datapipe:
|
||||
# TODO(VitalyFedyunin): Replacing with TorchArrow only API, as we are dropping pandas as followup
|
||||
for i in range(len(df)):
|
||||
yield df[i : i + 1]
|
||||
|
||||
|
||||
@functional_datapipe("_dataframes_concat", enable_df_api_tracing=True)
|
||||
class ConcatDataFramesPipe(DFIterDataPipe):
|
||||
def __init__(self, source_datapipe, batch=3) -> None:
|
||||
self.source_datapipe = source_datapipe
|
||||
self.n_batch = batch
|
||||
|
||||
def __iter__(self):
|
||||
buffer = []
|
||||
for df in self.source_datapipe:
|
||||
buffer.append(df)
|
||||
if len(buffer) == self.n_batch:
|
||||
yield df_wrapper.concat(buffer)
|
||||
buffer = []
|
||||
if buffer:
|
||||
yield df_wrapper.concat(buffer)
|
||||
|
||||
|
||||
@functional_datapipe("_dataframes_shuffle", enable_df_api_tracing=True)
|
||||
class ShuffleDataFramesPipe(DFIterDataPipe):
|
||||
def __init__(self, source_datapipe) -> None:
|
||||
self.source_datapipe = source_datapipe
|
||||
|
||||
def __iter__(self):
|
||||
size = None
|
||||
all_buffer: list[Any] = []
|
||||
for df in self.source_datapipe:
|
||||
if size is None:
|
||||
size = df_wrapper.get_len(df)
|
||||
all_buffer.extend(
|
||||
df_wrapper.get_item(df, i) for i in range(df_wrapper.get_len(df))
|
||||
)
|
||||
random.shuffle(all_buffer)
|
||||
buffer = []
|
||||
for df in all_buffer:
|
||||
buffer.append(df)
|
||||
if len(buffer) == size:
|
||||
yield df_wrapper.concat(buffer)
|
||||
buffer = []
|
||||
if buffer:
|
||||
yield df_wrapper.concat(buffer)
|
||||
|
||||
|
||||
@functional_datapipe("_dataframes_filter", enable_df_api_tracing=True)
|
||||
class FilterDataFramesPipe(DFIterDataPipe):
|
||||
def __init__(self, source_datapipe, filter_fn) -> None:
|
||||
self.source_datapipe = source_datapipe
|
||||
self.filter_fn = filter_fn
|
||||
|
||||
def __iter__(self):
|
||||
size = None
|
||||
all_buffer = []
|
||||
filter_res = []
|
||||
# pyrefly: ignore [bad-assignment]
|
||||
for df in self.source_datapipe:
|
||||
if size is None:
|
||||
size = len(df.index)
|
||||
for i in range(len(df.index)):
|
||||
all_buffer.append(df[i : i + 1])
|
||||
filter_res.append(self.filter_fn(df.iloc[i]))
|
||||
|
||||
buffer = []
|
||||
for df, res in zip(all_buffer, filter_res, strict=True):
|
||||
if res:
|
||||
buffer.append(df)
|
||||
if len(buffer) == size:
|
||||
yield df_wrapper.concat(buffer)
|
||||
buffer = []
|
||||
if buffer:
|
||||
yield df_wrapper.concat(buffer)
|
||||
|
||||
|
||||
@functional_datapipe("_to_dataframes_pipe", enable_df_api_tracing=True)
|
||||
class ExampleAggregateAsDataFrames(DFIterDataPipe):
|
||||
def __init__(self, source_datapipe, dataframe_size=10, columns=None) -> None:
|
||||
self.source_datapipe = source_datapipe
|
||||
self.columns = columns
|
||||
self.dataframe_size = dataframe_size
|
||||
|
||||
def _as_list(self, item):
|
||||
try:
|
||||
return list(item)
|
||||
except (
|
||||
Exception
|
||||
): # TODO(VitalyFedyunin): Replace with better iterable exception
|
||||
return [item]
|
||||
|
||||
def __iter__(self):
|
||||
aggregate = []
|
||||
for item in self.source_datapipe:
|
||||
aggregate.append(self._as_list(item))
|
||||
if len(aggregate) == self.dataframe_size:
|
||||
yield df_wrapper.create_dataframe(aggregate, columns=self.columns)
|
||||
aggregate = []
|
||||
if len(aggregate) > 0:
|
||||
yield df_wrapper.create_dataframe(aggregate, columns=self.columns)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user