Initial import: grid-bot — grid trading bot for BTC-USDT on Cifra Markets
This commit is contained in:
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
Reference in New Issue
Block a user