521 lines
18 KiB
Python
521 lines
18 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
"""The entry point of TVM parser for tirx."""
|
|
|
|
import inspect
|
|
from collections.abc import Callable
|
|
from typing import Any
|
|
|
|
from tvm.ir.base import deprecated
|
|
from tvm.script.parser._core import parse, scan_macro, utils
|
|
from tvm.script.parser.core.parser import Parser, ScriptMacro, VarTable
|
|
from tvm.tirx import Buffer, PrimFunc
|
|
from tvm.tirx.script.builder import block_name_suffix_context, buffer, ptr
|
|
|
|
|
|
def prim_func(
|
|
func: Callable | None = None,
|
|
private: bool = False,
|
|
check_well_formed=True,
|
|
s_tir: bool = False,
|
|
persistent: bool = False,
|
|
) -> PrimFunc | Callable:
|
|
"""The parsing method for tirx prim func, by using `@prim_func` as decorator.
|
|
|
|
Parameters
|
|
----------
|
|
func : Callable
|
|
The function to be parsed as prim func.
|
|
(Listed as optional to allow the decorator to be used
|
|
without arguments, like `@prim_func`,
|
|
or with an argument, `@prim_func(private=True)`)
|
|
|
|
private : bool, optional
|
|
Whether the function should be treated as private.
|
|
A private function has no global symbol attribute;
|
|
if the function is not private, it will have a global symbol
|
|
matching the function name.
|
|
|
|
Returns
|
|
-------
|
|
res : Union[PrimFunc, Callable]
|
|
The parsed tirx prim func.
|
|
"""
|
|
# pylint: disable=unused-argument
|
|
# (private will be used in the parser, but not immediately)
|
|
|
|
# need to capture this var outside the wrapper because the wrapper
|
|
# adds to the stack
|
|
outer_stack = inspect.stack()
|
|
|
|
def decorator_wrapper(func):
|
|
if not inspect.isfunction(func):
|
|
raise TypeError(f"Expect a function, but got: {func}")
|
|
if utils.is_defined_in_class(outer_stack, func):
|
|
return func
|
|
extra_vars = utils.inspect_function_capture(func)
|
|
utils.resolve_closure_vars(func, extra_vars, outer_stack)
|
|
f = parse(func, extra_vars, check_well_formed=check_well_formed, s_tir=s_tir)
|
|
setattr(f, "__name__", func.__name__)
|
|
return f
|
|
|
|
if func is not None:
|
|
# no optional args given => use wrapper directly
|
|
return decorator_wrapper(func)
|
|
else:
|
|
# if there is an optional arg given, return a new decorator
|
|
# that will then be invoked
|
|
setattr(decorator_wrapper, "dispatch_token", "tirx")
|
|
return decorator_wrapper
|
|
|
|
|
|
setattr(prim_func, "dispatch_token", "tirx")
|
|
|
|
|
|
class TIRInline(ScriptMacro):
|
|
"""Specialization of ScriptMacro for TIR with Python LEGB scoping.
|
|
|
|
Two definition paths:
|
|
1. Outside @T.prim_func (standalone @T.inline): definition_depth is None,
|
|
closure_vars captured at definition time are used (module globals are
|
|
effectively late-bound since they don't change during parsing).
|
|
2. Inside @T.prim_func (inline def in parsed body): definition_depth is set
|
|
to the VarTable frame depth at definition time, and defining_var_table
|
|
stores a reference to the VarTable that was active. At call time,
|
|
defining_var_table.get_at_depth(definition_depth) reads current values
|
|
from the lexically enclosing frames.
|
|
|
|
Attributes
|
|
----------
|
|
definition_depth : Optional[int]
|
|
VarTable frame depth at definition time, or None for outside-prim_func.
|
|
defining_var_table : Optional[VarTable]
|
|
Reference to the VarTable that was active at definition time.
|
|
call_count : int
|
|
Counter for unique block name suffixes.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
source,
|
|
closure_vars: dict[str, Any],
|
|
func: Callable,
|
|
definition_depth: int | None = None,
|
|
defining_var_table: VarTable | None = None,
|
|
) -> None:
|
|
# hygienic=True for the base class (field kept for compat but not used in dispatch)
|
|
super().__init__(source, closure_vars, func, hygienic=True)
|
|
self.definition_depth = definition_depth
|
|
self.defining_var_table = defining_var_table
|
|
self.call_count = 0
|
|
|
|
def parse_macro(self, parser: Parser) -> None:
|
|
macro_def = self.get_macro_def()
|
|
suffix = f"_{self.call_count}" if self.call_count > 0 else ""
|
|
self.call_count += 1
|
|
with block_name_suffix_context(suffix):
|
|
parser.visit_body(macro_def.body)
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
param_binding = inspect.signature(self.func).bind(*args, **kwargs)
|
|
param_binding.apply_defaults()
|
|
local_vars = param_binding.arguments
|
|
parser = self._find_parser_def()
|
|
|
|
with parser.with_diag_source(self.source):
|
|
if self.defining_var_table is not None:
|
|
# Inside-prim_func path: LEGB late binding from the defining scope
|
|
enclosing_vars = self.defining_var_table.get_at_depth(self.definition_depth)
|
|
else:
|
|
# Outside-prim_func path: use captured closure vars
|
|
enclosing_vars = self.closure_vars
|
|
|
|
saved_var_table = parser.var_table
|
|
parser.var_table = VarTable()
|
|
|
|
with parser.var_table.with_frame():
|
|
for k, v in enclosing_vars.items():
|
|
parser.var_table.add(k, v)
|
|
with parser.var_table.with_frame():
|
|
for k, v in local_vars.items():
|
|
parser.var_table.add(k, v)
|
|
|
|
parse_result = self.parse_macro(parser)
|
|
|
|
parser.var_table = saved_var_table
|
|
|
|
return parse_result
|
|
|
|
|
|
def inline(*args, definition_depth: int | None = None, defining_var_table=None) -> Callable:
|
|
"""Decorator for inline function definitions with Python LEGB scoping.
|
|
|
|
@T.inline follows Python's lexical scoping with late binding:
|
|
- At definition time, record which scopes are visible.
|
|
- At call time, read current values from those scopes.
|
|
|
|
Example::
|
|
|
|
import tvm
|
|
from tvm.script import tirx as T
|
|
|
|
x_value = 128
|
|
|
|
@T.inline
|
|
def capture(A, B):
|
|
B[()] = A[x_value] # x_value resolved from enclosing scope
|
|
|
|
@T.prim_func(s_tir=True)
|
|
def use(A: T.Buffer((1024,), "int32"), B: T.Buffer((), "int32")) -> None:
|
|
capture(A, B) # Produces B[()] = A[128]
|
|
"""
|
|
|
|
def _decorator(func: Callable) -> Callable:
|
|
source, closure_vars = scan_macro(func, utils.inspect_function_capture(func))
|
|
obj = TIRInline(
|
|
source,
|
|
closure_vars,
|
|
func,
|
|
definition_depth=definition_depth,
|
|
defining_var_table=defining_var_table,
|
|
)
|
|
|
|
def wrapper(*args, **kwargs):
|
|
return obj(*args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
if len(args) == 0:
|
|
setattr(_decorator, "dispatch_token", "tir.inline")
|
|
return _decorator
|
|
if len(args) == 1 and inspect.isfunction(args[0]):
|
|
return _decorator(args[0])
|
|
|
|
raise ValueError("Invalid use of T.inline. Usage: @T.inline or @T.inline()")
|
|
|
|
|
|
setattr(inline, "dispatch_token", "tir.inline")
|
|
|
|
|
|
class TIRJit:
|
|
"""Top-level kernel decorator with constexpr params + ``.specialize()``.
|
|
|
|
Parses the function body lazily: parsing is deferred until ``.specialize()``
|
|
supplies concrete values for the params annotated as ``T.constexpr``. The
|
|
return type of ``.specialize()`` is a ``tvm.tirx.PrimFunc``, identical in
|
|
type to what ``@T.prim_func`` produces today.
|
|
|
|
Constexpr params are removed from the resulting PrimFunc's parameter list;
|
|
their values are baked into the IR (e.g. into ``T.Buffer((M, K), ...)``
|
|
shape annotations and into the body).
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
func: Callable,
|
|
check_well_formed: bool = True,
|
|
is_stir: bool = False,
|
|
persistent: bool = False,
|
|
private: bool = False,
|
|
) -> None:
|
|
self.func = func
|
|
self.check_well_formed = check_well_formed
|
|
self.is_stir = is_stir
|
|
self.persistent = persistent # pylint: disable=unused-private-member
|
|
self.private = private # pylint: disable=unused-private-member
|
|
# Resolved closure vars (computed once; the function itself is the
|
|
# capture point, so this never changes between specializations).
|
|
self._closure_vars: dict[str, Any] = utils.inspect_function_capture(func)
|
|
# Detect which params are marked T.constexpr. With PEP 563
|
|
# (``from __future__ import annotations``), each annotation is a
|
|
# string; we eval them one-by-one so a constexpr probe is not
|
|
# blocked by sibling annotations that reference yet-undefined names
|
|
# (e.g. ``A: T.Buffer((N,), ...)`` referencing constexpr ``N``).
|
|
raw_anns = getattr(func, "__annotations__", {}) or {}
|
|
eval_globals = {**func.__globals__, **self._closure_vars}
|
|
sig = inspect.signature(func)
|
|
constexpr_names: set[str] = set()
|
|
constexpr_defaults: dict[str, Any] = {}
|
|
for name, param in sig.parameters.items():
|
|
ann = raw_anns.get(name)
|
|
if isinstance(ann, str):
|
|
try:
|
|
ann = eval(ann, eval_globals) # pylint: disable=eval-used
|
|
except Exception: # pylint: disable=broad-except
|
|
ann = None
|
|
if ann is constexpr:
|
|
constexpr_names.add(name)
|
|
if param.default is not inspect.Parameter.empty:
|
|
constexpr_defaults[name] = param.default
|
|
self.constexpr_names: frozenset[str] = frozenset(constexpr_names)
|
|
self.constexpr_defaults: dict[str, Any] = constexpr_defaults
|
|
self._cache: dict[tuple, PrimFunc] = {}
|
|
|
|
def specialize(self, **constexpr_kwargs) -> PrimFunc:
|
|
"""Build a concrete PrimFunc by binding the constexpr params.
|
|
|
|
Parameters
|
|
----------
|
|
**constexpr_kwargs
|
|
One value per ``T.constexpr``-annotated parameter. All such
|
|
parameters must be supplied; passing names that are not
|
|
constexpr-annotated is an error.
|
|
|
|
Returns
|
|
-------
|
|
PrimFunc
|
|
A concrete TIRx PrimFunc, identical in type to the output of
|
|
``@T.prim_func``.
|
|
"""
|
|
extra = constexpr_kwargs.keys() - self.constexpr_names
|
|
if extra:
|
|
raise TypeError(
|
|
f"{self.func.__name__}.specialize() got unexpected arg(s): "
|
|
f"{sorted(extra)} (constexpr params are: {sorted(self.constexpr_names)})"
|
|
)
|
|
effective = {**self.constexpr_defaults, **constexpr_kwargs}
|
|
missing = self.constexpr_names - effective.keys()
|
|
if missing:
|
|
raise TypeError(
|
|
f"{self.func.__name__}.specialize() missing constexpr arg(s) "
|
|
f"(no default provided): {sorted(missing)}"
|
|
)
|
|
|
|
try:
|
|
cache_key = tuple(sorted(effective.items()))
|
|
cached = self._cache.get(cache_key)
|
|
except TypeError as err:
|
|
raise TypeError(
|
|
f"{self.func.__name__}.specialize(): all constexpr values must "
|
|
f"be hashable (got: {effective!r})"
|
|
) from err
|
|
if cached is not None:
|
|
return cached
|
|
|
|
extra_vars = {**self._closure_vars, **effective}
|
|
prim_func = parse(
|
|
self.func,
|
|
extra_vars,
|
|
check_well_formed=self.check_well_formed,
|
|
s_tir=self.is_stir,
|
|
)
|
|
setattr(prim_func, "__name__", self.func.__name__)
|
|
self._cache[cache_key] = prim_func
|
|
return prim_func
|
|
|
|
|
|
def jit(
|
|
func: Callable | None = None,
|
|
private: bool = False,
|
|
check_well_formed: bool = True,
|
|
is_stir: bool = False,
|
|
persistent: bool = False,
|
|
) -> "TIRJit | Callable":
|
|
"""Decorator: capture the kernel and defer parsing until ``.specialize()``.
|
|
|
|
Use ``@T.jit`` (instead of ``@T.prim_func``) when the kernel takes
|
|
compile-time parameters annotated with ``T.constexpr``. The resulting
|
|
object exposes ``.specialize(**constexpr_kwargs)``, which returns a
|
|
``tvm.tirx.PrimFunc``.
|
|
|
|
Example::
|
|
|
|
from tvm.script import tirx as T
|
|
|
|
@T.jit
|
|
def add(
|
|
A: T.Buffer((N,), "float32"),
|
|
B: T.Buffer((N,), "float32"),
|
|
*,
|
|
N: T.constexpr,
|
|
):
|
|
...
|
|
|
|
kernel = add.specialize(N=1024) # returns a PrimFunc
|
|
"""
|
|
|
|
def decorator_wrapper(func: Callable) -> TIRJit:
|
|
if not inspect.isfunction(func):
|
|
raise TypeError(f"Expect a function, but got: {func}")
|
|
return TIRJit(
|
|
func,
|
|
check_well_formed=check_well_formed,
|
|
is_stir=is_stir,
|
|
persistent=persistent,
|
|
private=private,
|
|
)
|
|
|
|
if func is not None:
|
|
return decorator_wrapper(func)
|
|
setattr(decorator_wrapper, "dispatch_token", "tirx")
|
|
return decorator_wrapper
|
|
|
|
|
|
setattr(jit, "dispatch_token", "tirx")
|
|
|
|
|
|
class TIRMacro(ScriptMacro):
|
|
"""Specialization of the ScriptMacro class for TIR.
|
|
|
|
Apache-compatible hygienic macro. Distinct from ``TIRInline`` (which
|
|
uses Python LEGB late binding) so upstream code that relies on
|
|
capture-at-definition-time semantics keeps working.
|
|
|
|
Attributes
|
|
----------
|
|
call_count : int
|
|
Counter for the number of times this macro has been invoked.
|
|
Used to generate unique block name suffixes.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.call_count = 0
|
|
|
|
def parse_macro(self, parser: Parser) -> None:
|
|
macro_def = self.get_macro_def()
|
|
suffix = f"_{self.call_count}" if self.call_count > 0 else ""
|
|
self.call_count += 1
|
|
with block_name_suffix_context(suffix):
|
|
parser.visit_body(macro_def.body)
|
|
|
|
|
|
def macro(*args, hygienic: bool = True) -> Callable:
|
|
"""Decorator for macro definitions with hygienic capture.
|
|
|
|
Parameters
|
|
----------
|
|
hygienic: bool
|
|
Specifies whether the macro is hygienic or not. A hygienic macro
|
|
resolves symbols at definition time; a non-hygienic macro at use
|
|
time. Defaults to ``True``.
|
|
"""
|
|
|
|
def _decorator(func: Callable) -> TIRMacro:
|
|
source, closure_vars = scan_macro(func, utils.inspect_function_capture(func))
|
|
obj = TIRMacro(source, closure_vars, func, hygienic)
|
|
|
|
def wrapper(*args, **kwargs):
|
|
return obj(*args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
if len(args) == 0:
|
|
return _decorator
|
|
if len(args) == 1 and inspect.isfunction(args[0]):
|
|
return _decorator(args[0])
|
|
|
|
raise ValueError("Invalid use of T.macro. Usage: @T.macro or @T.macro()")
|
|
|
|
|
|
setattr(macro, "dispatch_token", "tir.macro")
|
|
|
|
|
|
class BufferProxy:
|
|
"""Buffer proxy class for constructing tirx buffer."""
|
|
|
|
def __or__(self, other):
|
|
"""Support ``T.Buffer | None`` union syntax in annotations."""
|
|
return self
|
|
|
|
def __ror__(self, other):
|
|
"""Support ``None | T.Buffer`` union syntax in annotations."""
|
|
return self
|
|
|
|
def __call__(
|
|
self,
|
|
shape,
|
|
dtype="float32",
|
|
data=None,
|
|
strides=None,
|
|
elem_offset=None,
|
|
byte_offset=None,
|
|
scope="global",
|
|
align=0,
|
|
offset_factor=0,
|
|
buffer_type="",
|
|
axis_separators=None,
|
|
layout="default",
|
|
) -> Buffer:
|
|
return buffer(
|
|
shape,
|
|
dtype=dtype,
|
|
data=data,
|
|
strides=strides,
|
|
elem_offset=elem_offset,
|
|
byte_offset=byte_offset,
|
|
scope=scope,
|
|
align=align,
|
|
offset_factor=offset_factor,
|
|
buffer_type=buffer_type,
|
|
axis_separators=axis_separators,
|
|
layout=layout,
|
|
)
|
|
|
|
@deprecated("T.Buffer[...]", "T.Buffer(...)")
|
|
def __getitem__(self, keys) -> Buffer:
|
|
if not isinstance(keys, tuple):
|
|
return self(keys)
|
|
if len(keys) >= 2 and not isinstance(keys[1], str):
|
|
return self(keys)
|
|
return self(*keys) # type: ignore[attr-defined] # pylint: disable=no-member
|
|
|
|
|
|
class PtrProxy:
|
|
"""Ptr proxy class for constructing tirx pointer."""
|
|
|
|
def __or__(self, other):
|
|
"""Support union syntax in annotations."""
|
|
return self
|
|
|
|
def __ror__(self, other):
|
|
"""Support union syntax in annotations."""
|
|
return self
|
|
|
|
@deprecated("T.Ptr(...)", "T.handle(...)")
|
|
def __call__(self, dtype, storage_scope="global"):
|
|
if callable(dtype):
|
|
dtype = dtype().ty.dtype
|
|
return ptr(dtype, storage_scope) # type: ignore[attr-defined] # pylint: disable=no-member
|
|
|
|
@deprecated("T.Ptr[...]", "T.handle(...)")
|
|
def __getitem__(self, keys):
|
|
if not isinstance(keys, tuple):
|
|
return self(keys)
|
|
return self(*keys)
|
|
|
|
|
|
class _ConstexprProxy:
|
|
"""Sentinel marker for compile-time (specialization-time) parameters.
|
|
|
|
Used as a parameter annotation in ``@T.jit`` decorated functions to mark
|
|
a parameter as constexpr — its value is supplied to ``.specialize(**kwargs)``
|
|
rather than at call time, and it is removed from the generated PrimFunc's
|
|
runtime parameter list.
|
|
"""
|
|
|
|
def __or__(self, other):
|
|
return self
|
|
|
|
def __ror__(self, other):
|
|
return self
|
|
|
|
|
|
Buffer = BufferProxy() # pylint: disable=invalid-name
|
|
Ptr = PtrProxy() # pylint: disable=invalid-name
|
|
constexpr = _ConstexprProxy() # pylint: disable=invalid-name
|