465 lines
17 KiB
Python
465 lines
17 KiB
Python
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
|
|
from torchgen.api.types import (
|
|
BaseCppType,
|
|
BaseCType,
|
|
boolT,
|
|
CType,
|
|
deviceT,
|
|
doubleT,
|
|
generatorT,
|
|
layoutT,
|
|
ListCType,
|
|
longT,
|
|
memoryFormatT,
|
|
NamedCType,
|
|
OptionalCType,
|
|
scalarT,
|
|
scalarTypeT,
|
|
stringT,
|
|
SymIntT,
|
|
VectorCType,
|
|
)
|
|
|
|
from torchgen.model import (
|
|
Argument,
|
|
BaseTy,
|
|
BaseType,
|
|
FunctionSchema,
|
|
ListType,
|
|
OperatorName,
|
|
OptionalType,
|
|
Return,
|
|
TensorOptionsArguments,
|
|
Type,
|
|
)
|
|
|
|
|
|
_valueT: Optional[BaseCppType] = None
|
|
|
|
|
|
# A ValueT is an IR type which represents the computation of a Tensor. In other
|
|
# words, a PyTorch user will do operations on lazy tensors, and each output lazy
|
|
# tensor internally tracks a ValueT representing the IR node that would have
|
|
# actually produced the value of this tensor for real.
|
|
#
|
|
# This is configurable because different lazy tensor backends (LTC vs XLA) will
|
|
# have different IR representations. (Though, arguably, after unification they
|
|
# shouldn't!)
|
|
def getValueT() -> BaseCppType:
|
|
global _valueT
|
|
if not _valueT:
|
|
raise NotImplementedError(
|
|
"The value type needs to be set with setValueT() in run_gen_lazy_tensor()"
|
|
)
|
|
|
|
return _valueT
|
|
|
|
|
|
def setValueT(val: BaseCppType) -> None:
|
|
global _valueT
|
|
_valueT = val
|
|
|
|
|
|
# this is a bad hack. I need to refactor the data model to represent each arg in the schema as an object,
|
|
# making it easier to represent special properties of an arg.
|
|
tensorListValueT = BaseCppType("torch::lazy", "Value")
|
|
|
|
|
|
def process_ir_type(
|
|
typ: Type, properties: "LazyIrProperties", *, symint: bool
|
|
) -> Union[BaseCType, VectorCType, OptionalCType, ListCType]:
|
|
"""
|
|
This function takes a type from NativeFunctions and converts it for use with
|
|
lazy tensor codegen.
|
|
|
|
Type conversion for lazy currently consists of
|
|
(1) changing at::Tensors into lazy::Values
|
|
(2) wrapping everything in a BaseCType
|
|
(3) making cpp-reference types into cpp-value types (e.g. vector instead of IntArrayRef)
|
|
|
|
(1) converts at::Tensors to lazy::Values (which wrap lazy::Nodes, with which Lazy IR represents tensors.)
|
|
There is special handling for Optional[Tensor] or List[Tensor], etc- hence 'tensor-like'
|
|
|
|
This is incomplete- there are assertions in places that it's expected to need to add
|
|
more types as the codegen is used with more operators.
|
|
"""
|
|
if isinstance(typ, BaseType):
|
|
if typ.name == BaseTy.Tensor:
|
|
return BaseCType(getValueT())
|
|
elif typ.name == BaseTy.Scalar:
|
|
if properties.TreatScalarsAsConstants:
|
|
return BaseCType(scalarT)
|
|
# at::scalar has special handling,
|
|
# and is wrapped in an lazy::Value just like at::tensor
|
|
return BaseCType(getValueT())
|
|
elif typ.name == BaseTy.ScalarType:
|
|
return BaseCType(scalarTypeT)
|
|
elif typ.name == BaseTy.int:
|
|
return BaseCType(longT)
|
|
elif typ.name == BaseTy.SymInt:
|
|
if symint:
|
|
return BaseCType(getValueT())
|
|
else:
|
|
return BaseCType(longT)
|
|
elif typ.name == BaseTy.bool:
|
|
return BaseCType(boolT)
|
|
elif typ.name == BaseTy.float:
|
|
return BaseCType(doubleT)
|
|
elif typ.name == BaseTy.str:
|
|
return BaseCType(stringT)
|
|
elif typ.name == BaseTy.Device:
|
|
return BaseCType(deviceT)
|
|
elif typ.name == BaseTy.Generator:
|
|
return BaseCType(generatorT)
|
|
elif typ.name == BaseTy.Layout:
|
|
return BaseCType(layoutT)
|
|
elif typ.name == BaseTy.MemoryFormat:
|
|
return BaseCType(memoryFormatT)
|
|
else:
|
|
raise AssertionError(f"TODO add support for type {repr(typ)}")
|
|
elif isinstance(typ, OptionalType):
|
|
return OptionalCType(process_ir_type(typ.elem, properties, symint=symint))
|
|
elif isinstance(typ, ListType):
|
|
if str(typ.elem) == "Tensor?":
|
|
# TODO(whc) is this actually correct? or should it use a Vector like above
|
|
return ListCType(OptionalCType(BaseCType(getValueT())))
|
|
elif str(typ.elem) == "Tensor":
|
|
# this is a TensorList which comes in from GetTensorList as a Value
|
|
return BaseCType(tensorListValueT)
|
|
elif typ.elem == BaseType(BaseTy.SymInt):
|
|
# TODO: return a value type. The problem here is analogous to
|
|
# the problem with tensorListValueT: if you have SymInt[] you
|
|
# cannot conveniently save the list of Value directly, as nodes
|
|
# expect to save values as a vector for ALL arguments. So you
|
|
# need a separate IR node that represents all of the size nodes
|
|
# assembled into a list. I'm not an LTC dev so I don't want to
|
|
# figure it out right now. Y'all figure it out...
|
|
return VectorCType(BaseCType(longT))
|
|
|
|
else:
|
|
return VectorCType(process_ir_type(typ.elem, properties, symint=symint))
|
|
else:
|
|
raise AssertionError(f"unrecognized type {repr(typ)}")
|
|
|
|
|
|
# TODO: Determining this based off of CType is bad; this should be computed
|
|
# from Type directly; then the same logic as process_ir_type can be used
|
|
#
|
|
# Invariant: passed typ should be an *owning* CType (e.g., we will report
|
|
# that ArrayRef<Value> is NOT a value type)
|
|
def isValueType(typ: CType, properties: "Optional[LazyIrProperties]" = None) -> bool:
|
|
"""
|
|
Given a type, determine if it is a Value-like type. This is equivalent to
|
|
being Tensor-like, but assumes the type has already been transformed.
|
|
"""
|
|
if isinstance(typ, BaseCType):
|
|
# I am regretting my naming conventions, but now we are wrapping at::scalar in
|
|
# lazy value, while preserving other 'scalar' types as scalars in the IR
|
|
treat_scalars_as_constants = properties and properties.TreatScalarsAsConstants
|
|
return (
|
|
typ.type == getValueT()
|
|
or (typ.type == scalarT and not treat_scalars_as_constants)
|
|
or typ.type == SymIntT
|
|
)
|
|
elif typ == VectorCType(BaseCType(SymIntT)):
|
|
# TODO: report True for this
|
|
return False
|
|
elif isinstance(typ, (OptionalCType, ListCType, VectorCType)):
|
|
return isValueType(typ.elem, properties)
|
|
return False
|
|
|
|
|
|
def isSymIntType(typ: Type) -> bool:
|
|
return isinstance(typ, BaseType) and typ.name == BaseTy.SymInt
|
|
|
|
|
|
def isWrappedScalarType(typ: Type) -> bool:
|
|
"""
|
|
Given a type, determine if it is a c10::scalar which we will wrap in a lazy Value.
|
|
Since we literally change the type from scalarT to valueT, information is lost.
|
|
This function helps build a list of wrapped scalars to save that information
|
|
"""
|
|
if isinstance(typ, BaseType):
|
|
# I am regretting my naming conventions, but now we are wrapping at::scalar in
|
|
# lazy value, while preserving other 'scalar' types as scalars in the IR
|
|
return typ.name == BaseTy.Scalar
|
|
elif isinstance(typ, (OptionalType, ListType)):
|
|
return isWrappedScalarType(typ.elem)
|
|
return False
|
|
|
|
|
|
# TODO: dedupe with Type.is_generator_like
|
|
def isGeneratorType(typ: Type) -> bool:
|
|
if isinstance(typ, BaseType):
|
|
return typ.name == BaseTy.Generator
|
|
elif isinstance(typ, (OptionalType)):
|
|
return isGeneratorType(typ.elem)
|
|
return False
|
|
|
|
|
|
# This class caches a few derived properties computed from an Argument
|
|
# and LazyIrProperties
|
|
class LazyArgument:
|
|
name: str
|
|
orig_type: Type
|
|
lazy_type_: Optional[CType]
|
|
is_wrapped_scalar: bool
|
|
is_generator: bool
|
|
# TODO: this is lies, it is false for symint list
|
|
is_symint_or_list: bool
|
|
|
|
# Whether or not we are treating this as symint or not
|
|
symint: bool
|
|
|
|
# true if this argument is or contains a lazy IR value
|
|
is_lazy_value: bool
|
|
|
|
def __init__(self, arg: Argument, properties: "LazyIrProperties", *, symint: bool):
|
|
self.name = arg.name
|
|
self.orig_type = arg.type
|
|
self.symint = symint
|
|
self.is_optional = isinstance(arg.type, OptionalType)
|
|
self.is_generator = isGeneratorType(arg.type)
|
|
self.lazy_type_ = process_ir_type(arg.type, properties, symint=symint)
|
|
self.is_wrapped_scalar = isWrappedScalarType(arg.type)
|
|
self.is_symint_or_list = symint and (
|
|
isSymIntType(arg.type)
|
|
or (isinstance(arg.type, OptionalType) and isSymIntType(arg.type.elem))
|
|
# TODO: lists of symints are not currently treated as value types
|
|
# or (isinstance(arg.type, ListType) and isSymIntType(arg.type.elem))
|
|
)
|
|
|
|
self.is_lazy_value = isValueType(self.lazy_type, properties)
|
|
|
|
@property
|
|
def lazy_type(self) -> CType:
|
|
assert (
|
|
self.lazy_type_ is not None
|
|
), f"Attempted to access lazy_type for invalid argument {self.name}"
|
|
return self.lazy_type_
|
|
|
|
|
|
class LazyIrProperties:
|
|
"""Collection of properties for an IR node
|
|
|
|
The property groups are listed below. Each group is mutually
|
|
exclusive, meaning that only one property from each group can be True
|
|
at any one time. The properties can be accessed as if they were normal
|
|
attributes. The mutual exclusivity is automatically handled.
|
|
"""
|
|
|
|
Properties: Tuple[Tuple[str, ...], ...] = (
|
|
(
|
|
"ShapePrecompute", # Assume shape has been precomputed
|
|
"ShapeCompute", # Need to compute the shape on construction
|
|
"ShapeCache", # Utilize the shape cache to defer computation
|
|
),
|
|
(
|
|
"Lower", # Codegen full lower function
|
|
"LowerDeclOnly", # Codegen only lower function declaration
|
|
),
|
|
(
|
|
"CanBeReused", # Codegen full reuse function
|
|
"CanBeReusedDeclOnly", # Codegen only reuse function declaration
|
|
),
|
|
(
|
|
"CreateFn", # Codegen full create function
|
|
"CreateFnDeclOnly", # Codegen only create function declaration
|
|
),
|
|
(
|
|
"TreatScalarsAsConstants", # Treat Scalars as constants instead of handling like values
|
|
),
|
|
)
|
|
|
|
def __init__(self, *default_properties: str):
|
|
properties: Dict[Tuple[str, ...], Optional[str]] = dict.fromkeys(
|
|
LazyIrProperties.Properties
|
|
)
|
|
self.__dict__["properties"] = properties
|
|
for p in default_properties:
|
|
setattr(self, p, True)
|
|
|
|
def __getattr__(self, key: str) -> Any:
|
|
properties = self.__dict__["properties"]
|
|
for values in LazyIrProperties.Properties:
|
|
if key in values:
|
|
return properties[values] == key
|
|
|
|
return self.__getattribute__(key)
|
|
|
|
def __setattr__(self, key: str, value: Any) -> Any:
|
|
properties = self.__dict__["properties"]
|
|
for values in LazyIrProperties.Properties:
|
|
if key in values:
|
|
properties[values] = key if value else None
|
|
return value
|
|
|
|
raise KeyError(f"Invalid property: {key}")
|
|
|
|
|
|
# Inspired by a FunctionSchema object, a LazyIrSchema holds the schema of a Lazy IR node.
|
|
# Unlike a FunctionSchema, it has no round-trippable string form (relating to the YAML),
|
|
# but carries type information from a native FunctionSchema modified for use with IR nodes,
|
|
# and preserving original argument names.
|
|
#
|
|
# TODO: This is not idiomatic with how other torchgen APIs transform on schema.
|
|
class LazyIrSchema:
|
|
# The name of the operator this function schema describes.
|
|
name: "OperatorName"
|
|
|
|
positional_args: Tuple[LazyArgument, ...]
|
|
keyword_args: Tuple[LazyArgument, ...]
|
|
|
|
# TODO: Need to handle collisions with argument names at some point
|
|
returns: Tuple["Return", ...]
|
|
|
|
# if this schema has a Generator arg, list its orig ctype/name but don't
|
|
# build a LazyArgument since lazy IR doesn't support it
|
|
generator_arg: Optional[NamedCType] = None
|
|
|
|
# original function schema
|
|
func: FunctionSchema
|
|
|
|
# Whether or not we are code-genning for SymInt or not
|
|
symint: bool
|
|
|
|
properties: LazyIrProperties = LazyIrProperties(
|
|
# default properties
|
|
"ShapePrecompute",
|
|
"Lower",
|
|
"CanBeReused",
|
|
)
|
|
opkind: Optional[str] = None
|
|
|
|
def __init__(
|
|
self,
|
|
func: FunctionSchema,
|
|
properties: Optional[LazyIrProperties] = None,
|
|
*,
|
|
symint: bool,
|
|
):
|
|
if properties:
|
|
self.properties = properties
|
|
|
|
self.func = func
|
|
self.symint = symint
|
|
positional_args: List[LazyArgument] = []
|
|
for arg_field in ["pre_self_positional", "self_arg", "post_self_positional"]:
|
|
if arg_field == "self_arg" and func.arguments.self_arg is not None:
|
|
arg = func.arguments.self_arg.argument
|
|
positional_args.append(
|
|
LazyArgument(arg, self.properties, symint=symint)
|
|
)
|
|
elif getattr(func.arguments, arg_field) is not None:
|
|
positional_args.extend(
|
|
LazyArgument(arg, self.properties, symint=symint)
|
|
for arg in getattr(func.arguments, arg_field)
|
|
)
|
|
self.positional_args = tuple(positional_args)
|
|
|
|
keyword_args: List[LazyArgument] = []
|
|
for arg_field in [
|
|
"pre_tensor_options_kwarg_only",
|
|
"tensor_options",
|
|
"post_tensor_options_kwarg_only",
|
|
"out",
|
|
]:
|
|
curr_args = getattr(func.arguments, arg_field)
|
|
if curr_args is not None:
|
|
if isinstance(curr_args, TensorOptionsArguments):
|
|
curr_args = curr_args.all()
|
|
for arg in curr_args:
|
|
if isGeneratorType(arg.type):
|
|
assert (
|
|
self.generator_arg is None
|
|
), "We expect there is only one generator arg"
|
|
self.generator_arg = NamedCType(
|
|
arg.name, arg.type # type:ignore[arg-type]
|
|
)
|
|
keyword_args.extend(
|
|
LazyArgument(arg, self.properties, symint=symint)
|
|
for arg in curr_args
|
|
)
|
|
self.keyword_args = tuple(keyword_args)
|
|
self.name = func.name
|
|
self.returns = func.returns
|
|
|
|
@property
|
|
def node_name(self) -> str:
|
|
"""
|
|
Return camel-case version of op in node.
|
|
|
|
Note: This function also appends any `overload_name` in the operation.
|
|
For example, if the op is `bitwise_and.Tensor`, the returned name
|
|
will be `BitwiseAndTensor`.
|
|
"""
|
|
op_name = f"{self.name.name}_{self.name.overload_name}".lower()
|
|
return "".join(word.capitalize() or "" for word in op_name.split("_"))
|
|
|
|
@property
|
|
def aten_name(self) -> str:
|
|
return str(self.name.name)
|
|
|
|
@property
|
|
def base_name(self) -> str:
|
|
return f"{self.name.name.base}"
|
|
|
|
def filtered_args(
|
|
self,
|
|
positional: bool = True,
|
|
keyword: bool = True,
|
|
values: bool = True,
|
|
scalars: bool = True,
|
|
generator: bool = True,
|
|
) -> List[LazyArgument]:
|
|
# This function maintains the sorted order of arguments but provides different filtered views.
|
|
# Some parts of the code care about kwargs vs args (TS lowerings),
|
|
# other parts care about whether they need to wrap the arg in a lazy value or leave it alone.
|
|
# Generators are special cased, as they are needed for fallback/shape-inference but not supported
|
|
# in TS lowerings and therefore also omitted from lazy IR.
|
|
args: List[LazyArgument] = []
|
|
if positional:
|
|
args.extend(self.positional_args)
|
|
if keyword:
|
|
args.extend(self.keyword_args)
|
|
|
|
if values and scalars and generator:
|
|
return args
|
|
elif values and scalars:
|
|
return [a for a in args if not a.is_generator]
|
|
elif values:
|
|
return [a for a in args if a.is_lazy_value]
|
|
elif scalars:
|
|
return [
|
|
a
|
|
for a in args
|
|
if not a.is_lazy_value and (generator or not a.is_generator)
|
|
]
|
|
|
|
return []
|
|
|
|
@property
|
|
def positional_values(self) -> List[LazyArgument]:
|
|
return self.filtered_args(
|
|
positional=True, keyword=False, values=True, scalars=False
|
|
)
|
|
|
|
@property
|
|
def positional_scalars(self) -> List[LazyArgument]:
|
|
return self.filtered_args(
|
|
positional=True, keyword=False, values=False, scalars=True
|
|
)
|
|
|
|
@property
|
|
def keyword_values(self) -> List[LazyArgument]:
|
|
return self.filtered_args(
|
|
positional=False, keyword=True, values=True, scalars=False
|
|
)
|
|
|
|
@property
|
|
def keyword_scalars(self) -> List[LazyArgument]:
|
|
return self.filtered_args(
|
|
positional=False, keyword=True, values=False, scalars=True
|
|
)
|