chore: import upstream snapshot with attribution
Lint / lint (push) Waiting to run
CI / MacOS (push) Waiting to run
CI / Windows (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 13:36:25 +08:00
commit 26446540fa
3151 changed files with 974126 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
build
dist
*.cpp
requirements/*.txt
+126
View File
@@ -0,0 +1,126 @@
# isort: skip_file
# 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.
# pylint: disable=redefined-builtin, wildcard-import
"""TVM: Open Deep Learning Compiler Stack."""
import multiprocessing
import sys
import os
# ffi module must load first
from tvm_ffi import register_object, register_global_func, get_global_func
# top-level alias
from .libinfo import __version__
from .base import _RUNTIME_ONLY
# tvm.runtime
from .runtime import Object
from .runtime._tensor import device, cpu, cuda, opencl, vulkan, metal
from .runtime._tensor import vpi, rocm, ext_dev, hexagon
from .runtime import DataType, DataTypeCode
# tvm.error
from . import error
# tvm.ir
from .ir import IRModule
from .ir import transform
from .ir import instrument
from . import ir
# tvm.script — must be imported before any dialect package so that
# tvm.script.register_dialect is reachable when dialect __init__.py files run.
from . import script
# tvm.tirx — registers itself via tvm.script.register_dialect in its __init__
from . import tirx
# tvm.backend — owns backend Python load hooks
from . import backend
# tvm.target
from . import target
# tvm.te
from . import te
# tvm.driver
from .driver import build, compile
# others
from . import arith
# support infra
from . import support
# Side-effect imports: register CUDA/ROCm FFI callbacks at TVM startup
from .support import rocm as _rocm, nvcc as _nvcc
# Relax contain modules that are only available in compiler package
# Do not import them if TVM is built with runtime only
if not _RUNTIME_ONLY:
# tvm.relax — registers itself via tvm.script.register_dialect in its __init__
from . import relax
# NOTE: This file should be python2 compatible so we can
# raise proper error message when user run the package using
# an older version of the python
def _should_print_backtrace():
in_pytest = "PYTEST_CURRENT_TEST" in os.environ
tvm_backtrace = os.environ.get("TVM_BACKTRACE", "0")
try:
tvm_backtrace = bool(int(tvm_backtrace))
except ValueError:
raise ValueError(f"invalid value for TVM_BACKTRACE {tvm_backtrace}, please set to 0 or 1.")
return in_pytest or tvm_backtrace
def tvm_wrap_excepthook(exception_hook):
"""Wrap given excepthook with TVM additional work."""
def wrapper(exctype, value, trbk):
"""Clean subprocesses when TVM is interrupted."""
if exctype is error.DiagnosticError and not _should_print_backtrace():
# TODO(@jroesch): consider moving to C++?
print("note: run with `TVM_BACKTRACE=1` environment variable to display a backtrace.")
else:
exception_hook(exctype, value, trbk)
if hasattr(multiprocessing, "active_children"):
# pylint: disable=not-callable
for p in multiprocessing.active_children():
p.terminate()
return wrapper
sys.excepthook = tvm_wrap_excepthook(sys.excepthook)
# Autoload loads built-in and out-of-tree backends. Out-of-tree extensions opt
# into being loaded automatically at ``import tvm`` time by declaring an entry
# point in the ``tvm.backends`` group:
# [project.entry-points."tvm.backends"] tvm_foo = "tvm_foo:_autoload".
# Autoload can be disabled via ``TVM_DEVICE_BACKEND_AUTOLOAD=0``.
from .backend._autoload_backends import _autoload_backends
_autoload_backends()
+40
View File
@@ -0,0 +1,40 @@
# isort: skip_file
# 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.
"""Integer bound analysis, simplification and pattern detection."""
from .int_set import (
IntSet,
IntervalSet,
PresburgerSet,
estimate_region_lower_bound,
estimate_region_strict_bound,
estimate_region_upper_bound,
)
from .analyzer import ModularSet, ConstIntBound, Analyzer, ProofStrength, Extension, CompareResult
from .bound import deduce_bound
from .pattern import detect_linear_equation, detect_clip_bound
from .int_solver import solve_linear_equations, solve_linear_inequalities
from .iter_affine_map import IterMapExpr, IterMark, IterSplitExpr, IterSumExpr
from .iter_affine_map import (
detect_iter_map,
iter_map_simplify,
normalize_iter_map_to_expr,
normalize_to_iter_sum,
subspace_divide,
inverse_affine_iter_map,
)
+21
View File
@@ -0,0 +1,21 @@
# 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.
"""FFI APIs for tvm.arith"""
import tvm_ffi
tvm_ffi.init_ffi_api("arith", __name__)
+529
View File
@@ -0,0 +1,529 @@
# 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.
# pylint: disable=invalid-name
"""Arithmetic data structure and utility"""
import enum
import tvm_ffi
from tvm import ir, tirx
from tvm.arith import IntSet
from tvm.runtime import Object
from . import _ffi_api
class ProofStrength(enum.IntEnum):
"""Proof strength of the analysis"""
DEFAULT = 0
SYMBOLIC_BOUND = 1
class CompareResult(enum.IntEnum):
"""Result of a transitive comparison.
Values must match the C++ ``arith::CompareResult`` enum.
"""
INCONSISTENT = 0
EQ = 1
LT = 2
LE = 3
GT = 4
GE = 5
NE = 6
UNKNOWN = 7
class Extension(enum.Flag):
"""Extensions enabled for RewriteSimplifier
Values should match `RewriteSimplifier::Extensions`
"""
NoExtensions = 0
TransitivelyProveInequalities = 1 << 0
ConvertBooleanToAndOfOrs = 1 << 1
ApplyConstraintsToBooleanBranches = 1 << 2
ComparisonOfProductAndSum = 1 << 3
@tvm_ffi.register_object("arith.ModularSet")
class ModularSet(Object):
"""Represent range of (coeff * x + base) for x in Z"""
def __init__(self, coeff, base):
self.__init_handle_by_constructor__(_ffi_api.ModularSet, coeff, base)
@tvm_ffi.register_object("arith.ConstIntBound")
class ConstIntBound(Object):
"""Represent constant integer bound
Parameters
----------
min_value : int
The minimum value of the bound.
max_value : int
The maximum value of the bound.
"""
POS_INF = (1 << 63) - 1
NEG_INF = -POS_INF
def __init__(self, min_value, max_value):
self.__init_handle_by_constructor__(_ffi_api.ConstIntBound, min_value, max_value)
class ConstraintScope:
"""Constraint scope.
Parameters
----------
fenter : function
A function that will be called to create an enter context.
Note
----
Do not create object directly, use Analyzer.constraint_scope
"""
def __init__(self, fenter):
self._fenter = fenter
self._fexit = None
def __enter__(self):
self._fexit = self._fenter()
def __exit__(self, ptype, value, trace):
self._fexit()
@tvm_ffi.register_object("arith.Analyzer")
class Analyzer(Object):
"""Integer arithmetic analyzer
This is a stateful analyzer class that can be used to perform
various symbolic integer analysis. The same analyzer instance can
be passed to FFI APIs to share accumulated facts across calls.
"""
def __init__(self):
self.__init_handle_by_constructor__(_ffi_api.Analyzer)
@property
def is_z3_enabled(self) -> bool:
"""Whether this build includes the Z3 backend (``USE_Z3=ON``).
The Z3-specific methods (:py:meth:`get_smtlib2`, :py:meth:`get_z3_stats`,
:py:meth:`set_z3_timeout_ms`, :py:meth:`set_z3_rlimit`) only work when
this is ``True``.
"""
return bool(_ffi_api.AnalyzerIsZ3Enabled(self))
def _check_z3_enabled(self) -> None:
if not self.is_z3_enabled:
raise RuntimeError(
"The Z3 backend is not available in this build. "
"Rebuild TVM with USE_Z3=ON to use Z3-specific Analyzer APIs."
)
def get_smtlib2(self, expr: tirx.Expr | None = None) -> str:
"""Get the current Z3 problem in SMT-LIB2 format.
Raises
------
RuntimeError
If TVM was built without Z3 (``USE_Z3=OFF``), since there is no
solver state to export. Use :py:attr:`is_z3_enabled` to check first.
Parameters
----------
expr : Optional[Expr]
The expression to prove. If provided, its negation is added to the problem.
"""
self._check_z3_enabled()
return _ffi_api.AnalyzerGetSMTLIB2(self, expr)
def set_z3_timeout_ms(self, timeout_ms: int) -> None:
"""Set Z3 timeout in milliseconds.
Raises
------
RuntimeError
If TVM was built without Z3 (``USE_Z3=OFF``).
Parameters
----------
timeout_ms : int
The timeout in milliseconds.
"""
self._check_z3_enabled()
_ffi_api.AnalyzerSetZ3TimeoutMs(self, timeout_ms)
def set_z3_rlimit(self, rlimit: int) -> None:
"""Set Z3 resource limit.
The resource limit gives deterministic solver budgeting (unlike a wall
clock timeout). A value of ``0`` disables the limit.
Raises
------
RuntimeError
If TVM was built without Z3 (``USE_Z3=OFF``).
Parameters
----------
rlimit : int
The resource limit.
"""
self._check_z3_enabled()
_ffi_api.AnalyzerSetZ3RLimit(self, rlimit)
def get_z3_stats(self) -> str:
"""Get Z3 solver statistics.
Raises
------
RuntimeError
If TVM was built without Z3 (``USE_Z3=OFF``).
Returns
-------
stats : str
The Z3 statistics.
"""
self._check_z3_enabled()
return _ffi_api.AnalyzerGetZ3Stats(self)
def const_int_bound(self, expr: tirx.Expr) -> ConstIntBound:
"""Find constant integer bound for expr.
Parameters
----------
expr : Expr
The expression.
Returns
-------
bound : ConstIntBound
The result bound
"""
return _ffi_api.AnalyzerConstIntBound(self, expr)
def const_int_bound_is_bound(self, var: tirx.Var) -> bool:
"""Check if a variable is bound to a range.
Parameters
----------
var : tvm.tirx.Var
The variable.
Returns
-------
result : bool
Whether the variable is bound to a range.
"""
return _ffi_api.AnalyzerConstIntBoundIsBound(self, var)
def modular_set(self, expr: tirx.Expr) -> ModularSet:
"""Find a modular set that expr belongs to.
Parameters
----------
expr : Expr
The expression.
Returns
-------
result : ModularSet
The result.
"""
return _ffi_api.AnalyzerModularSet(self, expr)
def simplify(self, expr: tirx.Expr, steps: int = 2) -> tirx.Expr:
"""Simplify expression via both rewrite and canonicalization.
Parameters
----------
expr : Expr
The expression.
steps : The simplification runs in the order of
rewrite_simplify (step 1) -> canonical_simplify (step 2) ->
rewrite_simplify (step 3) -> canonical_simplify (step 4) -> ...
param steps controls how many steps to run.
Default is 2, i.e., rewrite_simplify + canonical_simplify.
Returns
-------
result : Expr
The result.
"""
return _ffi_api.AnalyzerSimplify(self, expr, steps)
def clone(self) -> "Analyzer":
"""Return a deep copy of this analyzer with independent state.
The returned analyzer carries the same accumulated facts (variable
bounds, modular sets, bindings, integer-set domains, literal
constraints and transitive comparisons) as this one, but owns its own
state: binding or simplifying on either analyzer afterwards does not
affect the other. Unlike copying the handle, this is a true deep copy.
Do not call this while a constraint scope is active on this analyzer.
Returns
-------
result : Analyzer
A new analyzer holding an independent copy of the facts.
"""
return _ffi_api.AnalyzerClone(self)
def rewrite_simplify(self, expr: tirx.Expr) -> tirx.Expr:
"""Simplify expression via rewriting rules.
Parameters
----------
expr : Expr
The expression.
Returns
-------
result : Expr
The result.
"""
return _ffi_api.AnalyzerRewriteSimplify(self, expr)
@property
def rewrite_simplify_stats(self):
return _ffi_api.AnalyzerGetRewriteSimplifyStats(self)
def reset_rewrite_simplify_stats(self):
_ffi_api.AnalyzerResetRewriteSimplifyStats(self)
def canonical_simplify(self, expr: tirx.Expr) -> tirx.Expr:
"""Simplify expression via canonicalization.
Parameters
----------
expr : Expr
The expression.
Returns
-------
result : Expr
The result.
"""
return _ffi_api.AnalyzerCanonicalSimplify(self, expr)
def int_set(self, expr: tirx.Expr, dom_map: dict[tirx.Var, IntSet] | None = None) -> IntSet:
"""Compute a symbolic IntSet that covers expr for all values in dom_map.
Parameters
----------
expr : Expr
The expression.
dom_map : Optional[Dict[tvm.tirx.Var, tvm.arith.IntSet]]
The domain for variables to be relaxed. When omitted, the analyzer
uses the domains of the variables already bound to it.
Returns
-------
result : IntSet
The result.
"""
return _ffi_api.AnalyzerIntSet(self, expr, dom_map)
def can_prove(self, expr: tirx.Expr, strength: ProofStrength = ProofStrength.DEFAULT) -> bool:
"""Check whether we can prove expr to be true.
Parameters
----------
expr : Expr
The expression.
strength: ProofStrength
The proof strength. When TVM is built with Z3 (``USE_Z3=ON``), the
optional Z3 fallback is only consulted at ``SYMBOLIC_BOUND`` or
higher, after the native analyzers fail to prove the predicate.
Returns
-------
result : Expr
The result.
"""
return _ffi_api.AnalyzerCanProve(self, expr, strength)
def set_maximum_rewrite_steps(self, maximum: int) -> None:
"""Set the maximum allowed number of rewrite-simplify steps.
When a positive limit is set, the simplifier raises an exception once
it exceeds that number of rewrite steps. This is useful for guarding
against performance regressions in tests.
Parameters
----------
maximum : int
The maximum number of rewrite steps, or a non-positive value to
allow an unlimited number of steps.
"""
_ffi_api.AnalyzerSetMaximumRewriteSteps(self, maximum)
def bind(
self,
var: tirx.Var,
expr: tirx.Expr | ir.Range,
allow_override: bool = False,
) -> None:
"""Bind a variable to the expression.
Parameters
----------
var : tvm.tirx.Var
The variable.
expr : Union[tirx.Expr, ir.Range]
The expression or the range to bind to.
allow_override : bool
Whether to allow overriding an existing binding for the variable.
"""
return _ffi_api.AnalyzerBind(self, var, expr, allow_override)
def constraint_scope(self, constraint: tirx.Expr) -> ConstraintScope:
"""Create a constraint scope.
Parameters
----------
constraint : Expr
The constraint expression.
returns
-------
scope : ConstraintScope
The constraint scope
Examples
--------
.. code-block:: python
x = te.var("x")
analyzer = tvm.arith.Analyzer()
with analyzer.constraint_scope(x % 3 == 0):
# constraint in effect
assert analyzer.modular_set(x).coeff == 3
# constraint no longer in effect
assert analyzer.modular_set(x).coeff != 3
"""
def _fenter():
return _ffi_api.AnalyzerEnterConstraintContext(self, constraint)
return ConstraintScope(_fenter)
def update(
self, var: tirx.Var, info: ConstIntBound | ModularSet | IntSet, override: bool = False
) -> None:
"""Update information about var.
Parameters
----------
var : tvm.tirx.Var
The variable.
info : Union[ConstIntBound, ModularSet, IntSet]
Related information. A ``ConstIntBound`` updates the constant
integer bound, a ``ModularSet`` updates the modular set, and an
``IntSet`` updates the integer-set domain of ``var``.
override : bool
Whether allow override.
"""
if isinstance(info, ConstIntBound):
_ffi_api.AnalyzerConstIntBoundUpdate(self, var, info, override)
elif isinstance(info, ModularSet):
_ffi_api.AnalyzerModularSetUpdate(self, var, info, override)
elif isinstance(info, IntSet):
_ffi_api.AnalyzerIntSetUpdate(self, var, info, override)
else:
raise TypeError(f"Do not know how to handle type {type(info)}")
def can_prove_equal(self, lhs: tirx.Expr, rhs: tirx.Expr) -> bool:
"""Whether we can prove that lhs == rhs
Parameters
----------
lhs: Expr
The left-hand side of the comparison
rhs: Expr
The right-hand side of the comparison
Returns
-------
result: bool
Whether we can prove that lhs == rhs
"""
return _ffi_api.AnalyzerCanProveEqual(self, lhs, rhs)
def try_compare(
self, lhs: tirx.Expr, rhs: tirx.Expr, propagate_inequalities: bool = True
) -> CompareResult:
"""Compare lhs and rhs using previously provided known comparisons.
Parameters
----------
lhs : Expr
The left-hand side of the comparison.
rhs : Expr
The right-hand side of the comparison.
propagate_inequalities : bool
If true, attempt to find a sequence of transitive inequalities that
allow lhs and rhs to be compared.
Returns
-------
result : CompareResult
The most specific result that can be proven about the comparison.
Returns ``CompareResult.UNKNOWN`` when nothing can be proven.
"""
return CompareResult(_ffi_api.AnalyzerTryCompare(self, lhs, rhs, propagate_inequalities))
@property
def enabled_extensions(self) -> Extension:
"""Return the currently enabled extensions"""
value = _ffi_api.AnalyzerGetEnabledExtensions(self)
return Extension(value)
@enabled_extensions.setter
def enabled_extensions(self, flags: int | Extension):
"""Enable extensions for the analyzer
Parameters
----------
flags: Union[int,Extension]
The extensions to enable.
"""
flags = Extension(flags).value
_ffi_api.AnalyzerSetEnabledExtensions(self, flags)
+40
View File
@@ -0,0 +1,40 @@
# 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.
"""Bound deduction."""
from . import _ffi_api
def deduce_bound(var, cond, hint_map, relax_map):
"""Deduce the bound of the target variable in the cond.
Parameters
----------
var : tvm.tirx.Var
The target variable to be deduced.
cond : Expr
The condition
hint_map : Map[tvm.tirx.Var, IntSet]
Domain of variables used to help deduction.
relax_map : Map[tvm.tirx.Var, IntSet]
The fomain of the variables to be relaxed
using the provided domain.
"""
return _ffi_api.DeduceBound(var, cond, hint_map, relax_map)
+213
View File
@@ -0,0 +1,213 @@
# 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.
"""Integer set."""
import tvm_ffi
from tvm.runtime import Object
from . import _ffi_api
@tvm_ffi.register_object("ir.IntSet")
class IntSet(Object):
"""Represent a set of integer in one dimension."""
def is_nothing(self):
"""Whether the set represent nothing"""
return _ffi_api.IntSetIsNothing(self)
def is_everything(self):
"""Whether the set represent everything"""
return _ffi_api.IntSetIsEverything(self)
@staticmethod
def vector(vec):
"""Construct an integer set that covers the vector expr
Parameters
----------
vec : Expr
The vector expression.
Returns
-------
rset : IntSet
The result set.
"""
return _ffi_api.intset_vector(vec)
@staticmethod
def single_point(point):
"""Construct a point set.
Parameters
----------
point : Expr
The vector expression.
Returns
-------
rset : IntSet
The result set.
"""
return _ffi_api.intset_single_point(point)
@tvm_ffi.register_object("arith.IntervalSet")
class IntervalSet(IntSet):
"""Represent set of continuous interval [min_value, max_value]
Parameters
----------
min_value : Expr
The minimum value in the interval.
max_value : Expr
The maximum value in the interval.
"""
def __init__(self, min_value, max_value):
self.__init_handle_by_constructor__(_ffi_api.IntervalSet, min_value, max_value)
@tvm_ffi.register_object("arith.PresburgerSet")
class PresburgerSet(IntSet):
"""Represent of Presburger Set"""
def __init__(self):
self.__init_handle_by_constructor__(_ffi_api.PresburgerSet)
def estimate_region_lower_bound(region, var_dom, predicate, analyzer=None):
"""Analyze the region with affine map, given the domain of variables and their predicate
Some subregion may be discarded during the lower-bound analysis.
Parameters
----------
region : List[Range]
The region to be analyzed.
var_dom : Dict[tvm.tirx.Var, Range]
The ranges of the variables
predicate : Expr
The predicate for the affine map
analyzer : Optional[tvm.arith.Analyzer]
The analyzer to use. When provided, its accumulated bindings and
constraints are reused; otherwise a fresh analyzer is created.
Returns
----------
region_int_set : Optional[List[IntSet]]
None if the detection fails, or an array of IntSets as the result of analysis
"""
return _ffi_api.EstimateRegionLowerBound(region, var_dom, predicate, analyzer)
def estimate_region_strict_bound(region, var_dom, predicate, analyzer=None):
"""Analyze the region with affine map, given the domain of variables and their predicate
The result should be strict, i.e. no region is discarded or relaxed.
Parameters
----------
region : List[Range]
The region to be analyzed.
var_dom : Dict[tvm.tirx.Var, Range]
The ranges of the variables
predicate : Expr
The predicate for the affine map
analyzer : Optional[tvm.arith.Analyzer]
The analyzer to use. When provided, its accumulated bindings and
constraints are reused; otherwise a fresh analyzer is created.
Returns
----------
region_int_set : Optional[List[IntSet]]
None if the detection fails, or an array of IntSets as the result of analysis
"""
return _ffi_api.EstimateRegionStrictBound(region, var_dom, predicate, analyzer)
def estimate_region_upper_bound(region, var_dom, predicate, analyzer=None):
"""Analyze the region with affine map, given the domain of variables and their predicate
Relaxation of the region may be used in upper-bound analysis,
i.e. some extra region may be added to the result.
Parameters
----------
region : List[Range]
The region to be analyzed.
var_dom : Dict[tvm.tirx.Var, Range]
The ranges of the variables
predicate : Expr
The predicate for the affine map
analyzer : Optional[tvm.arith.Analyzer]
The analyzer to use. When provided, its accumulated bindings and
constraints are reused; otherwise a fresh analyzer is created.
Returns
----------
region_int_set : List[IntSet]
an array of IntSets as the result of analysis
"""
return _ffi_api.EstimateRegionUpperBound(region, var_dom, predicate, analyzer)
def pos_inf():
"""Returns the symbolic positive infinity
Returns
----------
pos_inf : tvm.tirx.Var
A symbolic var that indicates positive infinity
"""
return _ffi_api.PosInf()
def neg_inf():
"""Returns the symbolic positive infinity
Returns
----------
neg_inf : tvm.tirx.Var
A symbolic var that indicates positive infinity
"""
return _ffi_api.NegInf()
def union_lower_bound(sets):
"""Create a lower-bound of union set, where some of the segments may be dropped
Parameters
----------
sets : List[IntSet]
The sets to be combined
Returns
----------
union_lower_bound : List[IntSet]
An N-dimensional integer set, the lower bound of the union
"""
return _ffi_api.UnionLowerBound(sets)
+183
View File
@@ -0,0 +1,183 @@
# 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.
"""integer constraints data structures and solvers"""
import tvm_ffi
from tvm.runtime import Object
from . import _ffi_api
@tvm_ffi.register_object("arith.IntGroupBounds")
class IntGroupBounds(Object):
"""Represent integer grouped bounds which are classified into
lower bounds (include), upper bounds (include) and equalities.
Parameters
----------
coef : tvm.ir.Expr
The coefficient. Must be integer type.
coef * var >= lower
coef * var == equal
coef * var >= upper
lower : List[tvm.ir.Expr]
the lower bounds (include)
equal : List[tvm.ir.Expr]
equalities
upper : List[tvm.ir.Expr]
the upper bounds (include)
"""
def __init__(self, coef, lower, equal, upper):
self.__init_handle_by_constructor__(_ffi_api.IntGroupBounds, coef, lower, equal, upper)
@staticmethod
def from_range(rng):
"""Construct a IntGroupedBounds by Range.
Parameters
----------
rng : tvm.ir.Range
Returns
-------
ret : Range
The constructed range.
"""
return _ffi_api.IntGroupBounds_from_range(rng)
def find_best_range(self):
"""Return the best range from the grouped bounds.
None if (-inf, +inf).
"""
return _ffi_api.IntGroupBounds_FindBestRange(self)
@tvm_ffi.register_object("arith.IntConstraints")
class IntConstraints(Object):
"""Represent a set of integer constraints including variables, their ranges and
the relations between them (either equations or inequalities)
Parameters
----------
variables : List[tvm.tirx.Var]
The variables in the constraints. Must be integers
ranges : Map[tvm.tirx.Var, tvm.ir.Range]
The ranges of the variables.
relations : List[tvm.ir.Expr]
The relations between the variables (either equations or inequalities)
"""
def __init__(self, variables, ranges, relations):
self.__init_handle_by_constructor__(_ffi_api.IntConstraints, variables, ranges, relations)
@tvm_ffi.register_object("arith.IntConstraintsTransform")
class IntConstraintsTransform(Object):
"""We can have different set of variables to represent the same integer constraints.
For example, the following two constrains are equivalent,
{a + b = 0 | a >= 0, b >= 0} and
{m - n = 0 | m >= 0, n <= 0}
This data structure represents the transformation
between two equivalent integer constraints.
In the above example,
src : {a + b = 0 | a >= 0, b >= 0}
dst : {m - n = 0 | m >= 0, n <= 0}
src_to_dst : {a -> m, b -> -n}
dst_to_src : {m -> a, n -> -b}
Parameters
----------
src : arith.IntConstraints
source integer constraints, e.g., {a + b = 0 | a >= 0, b >= 0}
dst : arith.IntConstraints
integer constraints equivalent to the source, e.g., {m - n = 0 | m >= 0, n <= 0}
src_to_dst : Map[tvm.tirx.Var, tvm.ir.Expr]
mapping from variables in the src to the variables in the dst,
e.g., {a -> m, b -> -n}
dst_to_src : Map[tvm.tirx.Var, tvm.ir.Expr]
mapping from variables in the dst to the variables in the src,
e.g., {m -> a, n -> -b}
"""
def __init__(self, src, dst, src_to_dst, dst_to_src):
self.__init_handle_by_constructor__(
_ffi_api.IntConstraintsTransform, src, dst, src_to_dst, dst_to_src
)
def solve_linear_equations(equations, variables=None, ranges=None):
"""Solve linear equations.
Parameters
----------
equations: List[tvm.ir.Expr] or IntConstraints
The equations of the variables
variables : Optional[List[tvm.tirx.Var]]
The variables in the system.
ranges : Optional[Map[tvm.tirx.Var, tvm.ir.Range]]
The ranges of the variables.
Returns
-------
int_constraints_transform : IntConstraintsTransform
New integer constraints, with less variables (if the problem is NOT of full rank),
or no variable (if the problem is of full rank),
or an empty integer constraints (if the problem is unsolvable).
It also provides the ranges of the variables in the new system,
as well as inequalities inferred from the problem.
You can get the mapping from the original variables to the solution via
int_constraints_transform.src_to_dst.
"""
if isinstance(equations, IntConstraints):
return _ffi_api.SolveLinearEquations(equations)
return _ffi_api.SolveLinearEquations(variables, ranges, equations)
def solve_linear_inequalities(equations, variables=None, ranges=None, deskew_range=False):
"""Solve linear inequalities.
Parameters
----------
equations : List[tvm.ir.Expr] or IntConstraints
The inequalities of the variables
variables : Optional[List[tvm.tirx.Var]]
The variables in the system.
ranges : Optional[Map[tvm.tirx.Var, tvm.ir.Range]]
The ranges of the variables.
deskew_range: Optional[bool]
Whether deskew the result ranges to be started from zero.
Default false.
Returns
-------
ret_ranges: IntConstraints or IntConstraintsTransform
The result ranges for each variables.
Constrains that cannot be transformed to Range will be stored in IntConstraints.relations.
If deskew_range is set (=True), the result ranges will be deskewed to be started from zero.
New variables are created accordingly therefore IntConstraintsTransform is returned.
"""
solver = (
_ffi_api.SolveInequalitiesDeskewRange if deskew_range else _ffi_api.SolveInequalitiesToRange
)
if isinstance(equations, IntConstraints):
assert variables is None
assert ranges is None
return solver(equations)
return solver(variables, ranges, equations)
+375
View File
@@ -0,0 +1,375 @@
# 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.
"""Iterator (quasi)affine mapping patterns."""
from enum import IntEnum
import tvm_ffi
from tvm.ir import Expr
from tvm.runtime import Object
from . import _ffi_api
@tvm_ffi.register_object("arith.IterMapExpr")
class IterMapExpr(Expr):
"""Base class of all IterMap expressions."""
@tvm_ffi.register_object("arith.IterMark")
class IterMark(Object):
"""Mark the source as an iterator in [0, extent).
Parameters
----------
source : Expr.
The source expression.
extent : Expr
The extent of the iterator.
"""
def __init__(self, source, extent):
self.__init_handle_by_constructor__(_ffi_api.IterMark, source, extent)
@tvm_ffi.register_object("arith.IterSplitExpr")
class IterSplitExpr(IterMapExpr):
"""Split of an iterator.
result = floormod(floordiv(source, lower_factor), extent) * scale
Parameters
----------
source : IterMark
The source marked iterator.
lower_factor : Expr
The lower factor to split the domain.
extent : Expr
The extent of the split.
scale : Expr
Additional scale to the split.
"""
def __init__(self, source, lower_factor, extent, scale):
self.__init_handle_by_constructor__(
_ffi_api.IterSplitExpr, source, lower_factor, extent, scale
)
@tvm_ffi.register_object("arith.IterSumExpr")
class IterSumExpr(IterMapExpr):
"""Fuse multiple iterators by summing them with scaling.
result = sum(args) + base
Parameters
----------
args : List[IterSplitExpr]
The input to the sum expression.
base : Expr
The base offset.
"""
def __init__(self, args, base):
self.__init_handle_by_constructor__(_ffi_api.IterSumExpr, args, base)
@tvm_ffi.register_object("arith.IterMapResult")
class IterMapResult(Object):
"""Result of iter map detection."""
class IterMapLevel(IntEnum):
"""Possible kinds of iter mapping check level."""
Bijective = 0
Surjective = 1
NoCheck = 3
@staticmethod
def from_str(name: str):
"""Helper to create level enum from string"""
if name is None:
return IterMapLevel.NoCheck
name = name.lower()
if name == "bijective":
check_level = IterMapLevel.Bijective
elif name == "surjective":
check_level = IterMapLevel.Surjective
elif name == "nocheck":
check_level = IterMapLevel.NoCheck
else:
raise ValueError(f"Unknown check level {name}")
return check_level
def detect_iter_map(
indices,
input_iters,
predicate=True,
check_level=IterMapLevel.Surjective,
simplify_trivial_iterators=True,
analyzer=None,
):
"""Detect if indices can be written as mapped iters from input iters
Parameters
----------
indices : List[Expr]
The input indices
input_iters : Map[tvm.tirx.Var, Range]
The domain of each input iterators.
predicate : Expr
The predicate constraints on the input iterators
check_level : Union[str, IterMapLevel]
Checking level of iteration mapping
simplify_trivial_iterators: bool
If true, iterators with extent of 1 will be replaced with a
constant value.
analyzer : Optional[tvm.arith.Analyzer]
The analyzer to use. When provided, its accumulated bindings and
constraints are reused; otherwise a fresh analyzer is created.
Returns
-------
results : IterMapResult
The iter map matching result.
The result's .indices is empty array if no match can be found.
"""
if isinstance(check_level, str):
check_level = IterMapLevel.from_str(check_level)
elif check_level is None:
check_level = IterMapLevel.NoCheck
return _ffi_api.DetectIterMap(
indices, input_iters, predicate, check_level, simplify_trivial_iterators, analyzer
)
def normalize_to_iter_sum(index, input_iters, analyzer=None):
"""Normalize expr to iter sum.
The normalized result ensures that
each scale is in the form of (symbol_prod) * cscale
It will also sort in desc order by cscale then len(symbol_prod).
Parameters
----------
index : Expr
The input index
input_iters : Map[tvm.tirx.Var, Range]
The domain of each input iterators.
analyzer : Optional[tvm.arith.Analyzer]
The analyzer to use. When provided, its accumulated bindings and
constraints are reused; otherwise a fresh analyzer is created.
Returns
-------
iter_sum: IterSumExpr
The result iter sum
Note
----
This function does best effort detection, so some undetected
part can go into iter_sum.base
This function is useful to decide the stride multiplier and
division factor in buffer access patterns.
"""
return _ffi_api.NormalizeToIterSum(index, input_iters, analyzer)
def iter_map_simplify(
indices,
input_iters,
predicate=True,
check_level=IterMapLevel.Surjective,
simplify_trivial_iterators=True,
analyzer=None,
):
"""Simplify the indices using iter map detection.
Parameters
----------
indices : List[Expr]
The input indices
input_iters : Map[tvm.tirx.Var, Range]
The domain of each input iterators.
predicate : Expr
The predicate constraints on the input iterators
check_level : Union[str, IterMapLevel]
Checking level of iteration mapping
simplify_trivial_iterators: bool
If true, iterators with extent of 1 will be replaced with a
constant value.
analyzer : Optional[tvm.arith.Analyzer]
The analyzer to use. When provided, its accumulated bindings and
constraints are reused; otherwise a fresh analyzer is created.
Returns
-------
results : IterMapResult
The iter map matching result.
The result's .indices is empty array if no match can be found.
"""
if isinstance(check_level, str):
check_level = IterMapLevel.from_str(check_level)
elif check_level is None:
check_level = IterMapLevel.NoCheck
return _ffi_api.IterMapSimplify(
indices, input_iters, predicate, check_level, simplify_trivial_iterators, analyzer
)
def normalize_iter_map_to_expr(expr):
"""Given an IterMapExpr, transform it to normal Expr
Parameters
----------
expr : IterMapExpr
the input IterMapExpr
Returns
-------
result : Expr
the corresponding normal Expr
"""
return _ffi_api.NormalizeIterMapToExpr(expr)
def subspace_divide(
bindings,
input_iters,
sub_iters,
predicate=True,
check_level=IterMapLevel.Surjective,
simplify_trivial_iterators=True,
analyzer=None,
):
"""Detect if bindings can be written as
``[a_0*e_0 + b_0 + c_0, a_1*e_1 + b_1, ..., a_n*e_n + b_n]``
where::
a = some-quasi-affine-iter-map(input_iters set_minus sub_iters)
b = some-quasi-affine-iter-map(sub_iters)
c is constant symbols
e is the extent of b
For example::
z*12 + y*3 + x + c = (z*4+y)*3 + x
bindings = [z*12 + y*3 + x + c]
input_iters = [z, y, x]
sub_iter = [x]
Then the result will be [a, b] where
a = [z*4 + y]
b = [x]
Parameters
----------
bindings : List[Expr]
The input bindings
input_iters : Map[tvm.tirx.Var, Range]
The domain of input iterator, which is the basis of the whole space
sub_iters : Array[tvm.tirx.Var]
The subset of input_iters, which is the basis of the subspace
predicate : Expr
The predicate constraints on the input iterators
check_level : Union[str, IterMapLevel]
Checking level of iteration mapping
simplify_trivial_iterators: bool
If true, iterators with extent of 1 will be replaced with a
constant value.
analyzer : Optional[tvm.arith.Analyzer]
The analyzer to use. When provided, its accumulated bindings and
constraints are reused; otherwise a fresh analyzer is created.
Returns
-------
results : List[List[Expr]]
The result list has length ``len(bindings) + 1``.
- ``[0, len(bindings))``: The iter map matching result.
The inner list is of length 2. The first expr is the basis
of the quotient space. The second expr is the basis of the subspace.
- ``len(bindings)``: the predicate of outer space and inner space.
- Empty array if no match can be found.
"""
if isinstance(check_level, str):
check_level = IterMapLevel.from_str(check_level)
return _ffi_api.SubspaceDivide(
bindings,
input_iters,
sub_iters,
predicate,
check_level,
simplify_trivial_iterators,
analyzer,
)
def inverse_affine_iter_map(iter_map, outputs):
"""Apply the inverse of the affine transformation to the outputs.
Similar to the back-propagation, starting from the outputs, it visits the DAG of the expressions
in reverse topology order and applies the inverse of the affine transformation until it reaches
the input. The affine iter map is required to be bijective.
For example, iter_map = [l0 // 16, l0 % 16], outputs = [output_0, output_1],
the affine transformation specified by `iter_map` will be applied to `outputs` and the result
will be {l0: ((output_0*16) + output_1)}.
See also :any:`detect_iter_map`.
Parameters
----------
iter_map : List[IterSumExpr]
The bijective affine iter map.
outputs : List[Expr]
The outputs of the affine transformation.
Returns
-------
results : Map[tvm.tirx.Var, Expr]
The map from the input to the transformed result.
"""
return _ffi_api.InverseAffineIterMap(iter_map, outputs)
+61
View File
@@ -0,0 +1,61 @@
# 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.
"""Detect common patterns."""
from . import _ffi_api
def detect_linear_equation(expr, var_list):
"""Match `expr = sum_{i=0}^{n-1} var[i] * coeff[i] + coeff[n]`
Where coeff[i] and base are invariant of var[j] for all i and j.
Parameters
----------
expr : Expr
The expression to be matched.
var_list : List[tvm.tirx.Var]
A list of variables.
Returns
-------
coeff : List[Expr]
A list of co-efficients if the match is successful.
An empty list if the match failed.
"""
return _ffi_api.DetectLinearEquation(expr, var_list)
def detect_clip_bound(expr, var_list):
"""Detect if expression corresponds to clip bound of the vars
Parameters
----------
expr : Expr
The expression to be matched.
var_list : List[tvm.tirx.Var]
A list of variables.
Returns
-------
coeff : List[Expr]
`concat([min_value[i], max_value[i]] for i, v in enumerate(var_list))`
An empty list if the match failed.
"""
return _ffi_api.DetectClipBound(expr, var_list)
+27
View File
@@ -0,0 +1,27 @@
# 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.
"""Backend-owned Python modules and load hooks."""
from __future__ import annotations
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__) # type: ignore[name-defined]
from .loader import is_loaded, load
__all__ = ["is_loaded", "load"]
+88
View File
@@ -0,0 +1,88 @@
# 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.
"""Autoload built-in and out-of-tree backend libraries and registration hooks."""
from __future__ import annotations
import os
import warnings
from importlib.metadata import entry_points
from pathlib import Path
from tvm_ffi.libinfo import load_lib_ctypes
from tvm.backend.loader import load
from tvm.base import _LOADED_LIBS
_BUILTIN_BACKENDS = (
"cuda",
"metal",
"rocm",
"trn",
"opencl",
"vulkan",
"webgpu",
"hexagon",
"adreno",
)
_AUTO_LOAD_DONE = False
def _load_builtin_backends() -> None:
"""Load all in-tree backend Python hooks."""
for name in _BUILTIN_BACKENDS:
load(name)
runtime_dir = Path(_LOADED_LIBS["tvm_runtime"]._name).resolve().parent
try:
# Load libtvm_runtime_extra if available for registration side effects.
_LOADED_LIBS["tvm_runtime_extra"] = load_lib_ctypes(
package="tvm",
target_name="tvm_runtime_extra",
extra_lib_paths=[runtime_dir],
mode="RTLD_LOCAL",
)
except (OSError, FileNotFoundError, RuntimeError):
pass
return None
def _autoload_backends() -> None:
"""Load built-in backends and invoke out-of-tree backend entry points."""
global _AUTO_LOAD_DONE
if _AUTO_LOAD_DONE:
return
_AUTO_LOAD_DONE = True
if os.environ.get("TVM_DEVICE_BACKEND_AUTOLOAD", "1") == "0":
return
from tvm import _RUNTIME_ONLY # pylint: disable=import-outside-toplevel
if not _RUNTIME_ONLY:
_load_builtin_backends()
# Out-of-tree extensions opt into being loaded automatically at ``import tvm`` time
# by declaring an entry point in the ``tvm.backends`` group:
# [project.entry-points."tvm.backends"] tvm_foo = "tvm_foo:_autoload".
# Autoload can be disabled via ``TVM_DEVICE_BACKEND_AUTOLOAD=0``.
for entry_pt in entry_points(group="tvm.backends"):
try:
entry_pt.load()()
except Exception as e: # pylint: disable=broad-except
warnings.warn(f"Failed to autoload tvm backend '{entry_pt.name}': {e}")
+39
View File
@@ -0,0 +1,39 @@
# 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.
"""Adreno-owned backend hooks."""
from importlib import import_module
_LAZY_SUBMODULES = {"target_tags"}
def register_backend():
"""Register Adreno-owned Python semantics."""
import tvm.backend as backend # pylint: disable=import-outside-toplevel
backend.load("opencl")
backend.load("vulkan")
import_module(f"{__name__}.target_tags")
def __getattr__(name: str):
if name in _LAZY_SUBMODULES:
return import_module(f"{__name__}.{name}")
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__all__ = ["register_backend", "target_tags"]
+64
View File
@@ -0,0 +1,64 @@
# 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.
"""Qualcomm Adreno GPU target tags."""
from tvm.target import register_tag
register_tag(
"qcom/adreno-opencl",
{
"kind": "opencl",
"device": "adreno",
"keys": ["adreno", "opencl", "gpu"],
},
)
register_tag(
"qcom/adreno-opencl-clml",
{
"kind": "opencl",
"device": "adreno",
"keys": ["adreno", "opencl", "gpu", "clml"],
},
)
register_tag(
"qcom/adreno-opencl-texture",
{
"kind": "opencl",
"device": "adreno",
"keys": ["adreno", "opencl", "gpu", "texture"],
},
)
register_tag(
"qcom/adreno-vulkan",
{
"kind": "vulkan",
"device": "adreno",
"keys": ["adreno", "vulkan", "gpu"],
},
)
register_tag(
"qcom/adreno-vulkan-texture",
{
"kind": "vulkan",
"device": "adreno",
"keys": ["adreno", "vulkan", "gpu", "texture"],
},
)
+103
View File
@@ -0,0 +1,103 @@
# 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.
"""CUDA-owned TIRx modules."""
from importlib import import_module
from pathlib import Path
from tvm_ffi.libinfo import load_lib_ctypes
from tvm.base import _LOADED_LIBS
_LAZY_SUBMODULES = {"lang", "op", "operator", "script", "target_tags"}
def _detect_target_from_device(dev):
from tvm.target import Target # pylint: disable=import-outside-toplevel
return Target(
{
"kind": "cuda",
"max_shared_memory_per_block": dev.max_shared_memory_per_block,
"max_threads_per_block": dev.max_threads_per_block,
"thread_warp_size": dev.warp_size,
"arch": "sm_" + dev.compute_version.replace(".", ""),
}
)
def register_backend():
"""Register CUDA-owned Python semantics."""
from tvm.target.detect_target import register_device_target_detector
from tvm.tirx.script.builder import ir as builder_ir # pylint: disable=import-outside-toplevel
runtime_dir = Path(_LOADED_LIBS["tvm_runtime"]._name).resolve().parent
try:
# Runtime sidecars only need registration side effects; libtvm_runtime is global.
_LOADED_LIBS["tvm_runtime_cuda"] = load_lib_ctypes(
package="tvm",
target_name="tvm_runtime_cuda",
extra_lib_paths=[runtime_dir],
mode="RTLD_LOCAL",
)
except (OSError, FileNotFoundError, RuntimeError):
pass
register_device_target_detector("cuda", _detect_target_from_device)
for name, namespace in script_namespaces().items():
builder_ir.register_script_namespace(name, namespace)
import_module(f"{__name__}.operator.intrinsics")
import_module(f"{__name__}.operator.tile_primitive")
import_module(f"{__name__}.target_tags")
def script_namespaces(**_):
"""Return CUDA-owned TVMScript namespaces."""
from .script import ( # pylint: disable=import-outside-toplevel
CUDANamespace,
NVSHMEMNamespace,
PTXNamespace,
)
return {
"cuda": CUDANamespace(),
"nvshmem": NVSHMEMNamespace(),
"ptx": PTXNamespace(),
}
def script_namespace(**kwargs):
"""Return the CUDA TVMScript namespace object."""
return script_namespaces(**kwargs)["cuda"]
def __getattr__(name: str):
if name in _LAZY_SUBMODULES:
return import_module(f"{__name__}.{name}")
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__all__ = [
"lang",
"op",
"operator",
"register_backend",
"script",
"script_namespace",
"script_namespaces",
"target_tags",
]
+70
View File
@@ -0,0 +1,70 @@
# 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.
"""CUDA-specific TIRx language helpers."""
from importlib import import_module
__all__ = [
"BaseTileScheduler",
"ClusterPersistentScheduler2D",
"FlashAttentionLPTScheduler",
"FlashAttentionLinearScheduler",
"GroupMajor3D",
"IndexedTripleTileScheduler",
"MBarrier",
"Pipeline",
"PipelineState",
"RankAwareGroupMajorTileScheduler",
"SMEMPool",
"SmemDescriptor",
"TCGen05Bar",
"TMABar",
"TMEMPool",
"TMEMStages",
"WarpRole",
"WarpgroupRole",
]
_HELPER_MODULES = {
"MBarrier": ".pipeline",
"Pipeline": ".pipeline",
"PipelineState": ".pipeline",
"BaseTileScheduler": ".tile_scheduler",
"ClusterPersistentScheduler2D": ".tile_scheduler",
"FlashAttentionLPTScheduler": ".tile_scheduler",
"FlashAttentionLinearScheduler": ".tile_scheduler",
"GroupMajor3D": ".tile_scheduler",
"IndexedTripleTileScheduler": ".tile_scheduler",
"RankAwareGroupMajorTileScheduler": ".tile_scheduler",
"SMEMPool": ".alloc_pool",
"TCGen05Bar": ".pipeline",
"TMABar": ".pipeline",
"TMEMPool": ".alloc_pool",
"TMEMStages": ".alloc_pool",
"WarpRole": ".warp_role",
"WarpgroupRole": ".warp_role",
"SmemDescriptor": ".smem_desc",
}
def __getattr__(name: str):
module_name = _HELPER_MODULES.get(name)
if module_name is None:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
value = getattr(import_module(module_name, __name__), name)
globals()[name] = value
return value
+529
View File
@@ -0,0 +1,529 @@
# 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.
"""SMEM and TMEM bump-allocator pools for TIRX kernels."""
from __future__ import annotations
import functools
import operator
from tvm import DataType
from tvm.tirx.layout import S, TCol, TileLayout, TLane
# ---------------------------------------------------------------------------
# ir_builder helpers — imported lazily to avoid circular deps at module level
# ---------------------------------------------------------------------------
_ir = None
def _get_ir():
global _ir
if _ir is None:
from tvm.tirx.script.builder import ir as _mod
_ir = _mod
return _ir
def _get_frame():
from tvm.tirx.script.builder import frame
return frame
# ---------------------------------------------------------------------------
# Shared utilities
# ---------------------------------------------------------------------------
_POOL_UNSET = object()
def _default_tmem_layout(rows, cols):
return TileLayout(S[(rows, cols) : (1 @ TLane, 1 @ TCol)])
def _emit_stmt(expr):
ir = _get_ir()
ir.add_to_parent(ir.evaluate(expr))
def _shape_product(shape):
return functools.reduce(operator.mul, shape, 1)
def _auto_swizzle_mode(dtype):
"""Select the default MMA swizzle mode for a shared-memory allocation."""
from tvm.backend.cuda.operator.tile_primitive.tma_utils import SwizzleMode
del dtype
return SwizzleMode.SWIZZLE_128B_ATOM
def _swizzle_atom_bytes(swizzle_mode):
"""Return the row width (in bytes) of one swizzle atom for *swizzle_mode*."""
from tvm.backend.cuda.operator.tile_primitive.tma_utils import SwizzleMode
return {
SwizzleMode.SWIZZLE_NONE: 0,
SwizzleMode.SWIZZLE_32B_ATOM: 32,
SwizzleMode.SWIZZLE_64B_ATOM: 64,
SwizzleMode.SWIZZLE_128B_ATOM: 128,
}[swizzle_mode]
def _suggest_swizzle_for_row_bytes(row_bytes):
"""Pick the largest valid swizzle mode whose atom row fits within *row_bytes*."""
for atom_bytes, mode in (
(128, "SWIZZLE_128B_ATOM"),
(64, "SWIZZLE_64B_ATOM"),
(32, "SWIZZLE_32B_ATOM"),
):
if row_bytes >= atom_bytes and row_bytes % atom_bytes == 0:
return mode
return "SWIZZLE_NONE"
def _validate_mma_alloc_shape(shape, dtype, swizzle_mode):
"""Validate that *shape* / *dtype* / *swizzle_mode* are mutually compatible.
``mma_shared_layout`` tiles a swizzle atom of shape ``[8, swizzle_bytes / dtype_bytes]``
over the last two logical dimensions of *shape*. If the row width or row count of
the request is smaller than (or not a multiple of) the atom, the underlying
``Layout.tile_to`` lowers to a ``floordiv``/``floormod`` by zero and raises an
opaque internal "Divide by zero" diagnostic from ``tile_tile_ops.cc``. Catch the
misconfiguration here so callers see *what* is wrong and *how* to fix it.
Validation skipped when *swizzle_mode* is ``SWIZZLE_NONE`` (no atom).
"""
from tvm.backend.cuda.operator.tile_primitive.tma_utils import SwizzleMode
if swizzle_mode == SwizzleMode.SWIZZLE_NONE:
return
if len(shape) < 2:
raise ValueError(
f"alloc_mma shape={tuple(shape)} has fewer than 2 dimensions; "
f"swizzled MMA layouts tile over the last two dims (rows, cols). "
f"Use swizzle_mode='none' for 1-D allocations."
)
# Only validate concrete int dims; symbolic dims fall through (the analyzer
# in C++ will still ICHECK on them, but at least we don't false-positive).
rows = shape[-2]
cols = shape[-1]
if not (isinstance(rows, int) and isinstance(cols, int)):
return
dtype_bytes = DataType(dtype).bits // 8
if dtype_bytes == 0:
# Sub-byte dtype (e.g. float4); ``cols`` is already in element units, so
# use a fractional check expressed via bits.
col_bits = cols * DataType(dtype).bits
atom_bits = _swizzle_atom_bytes(swizzle_mode) * 8
if col_bits < atom_bits or col_bits % atom_bits != 0:
row_bytes = col_bits // 8 if col_bits % 8 == 0 else col_bits / 8
atom_bytes = _swizzle_atom_bytes(swizzle_mode)
suggestion = _suggest_swizzle_for_row_bytes(col_bits // 8 if col_bits >= 8 else 0)
raise ValueError(
f"alloc_mma shape={tuple(shape)} with dtype={dtype!r} produces "
f"{row_bytes}B rows, which is incompatible with the {atom_bytes}B "
f"swizzle atom selected by {swizzle_mode.name}. "
f"Use swizzle_mode=SwizzleMode.{suggestion}, or widen shape[-1] "
f"to a multiple of "
f"{(atom_bits + DataType(dtype).bits - 1) // DataType(dtype).bits} elements."
)
else:
row_bytes = cols * dtype_bytes
atom_bytes = _swizzle_atom_bytes(swizzle_mode)
if row_bytes < atom_bytes or row_bytes % atom_bytes != 0:
suggestion = _suggest_swizzle_for_row_bytes(row_bytes)
min_cols = atom_bytes // dtype_bytes
raise ValueError(
f"alloc_mma shape={tuple(shape)} with dtype={dtype!r} produces "
f"{row_bytes}B rows, which is incompatible with the {atom_bytes}B "
f"swizzle atom selected by {swizzle_mode.name}. "
f"Use swizzle_mode=SwizzleMode.{suggestion}, or widen shape[-1] "
f"to a multiple of {min_cols} elements (>= {atom_bytes}B at {dtype})."
)
# Atom rows is always 8 (see ``mma_atom_shape`` in tma_utils.py).
atom_rows = 8
if rows < atom_rows or rows % atom_rows != 0:
raise ValueError(
f"alloc_mma shape={tuple(shape)} has shape[-2]={rows}, but the "
f"{swizzle_mode.name} atom requires shape[-2] to be a positive "
f"multiple of {atom_rows}. Use swizzle_mode='none', or widen shape[-2] "
f"to a multiple of {atom_rows}."
)
# ---------------------------------------------------------------------------
# TMEMStages
# ---------------------------------------------------------------------------
def _meta_class(cls):
"""Apply @meta_class decorator from ir_builder."""
return _get_ir().meta_class(cls)
@_meta_class
class TMEMStages:
"""Parse-time staged view over a TMEM buffer.
Parameters
----------
buf : Buffer
The underlying TMEM buffer (e.g. f32 or f16 view).
col_start : int
First column of stage 0 in *buf*'s column space.
width : int
Number of columns per stage.
stages : int
Number of pipeline stages (default 1).
stride : int or None
Column distance between consecutive stages. When *None* (default),
equals *width* (stages are packed back-to-back).
"""
def __init__(self, buf, col_start, width, stages=1, stride=None):
self.buf = buf
self.col_start = col_start
self.width = width
self.stages = stages
self.stride = width if stride is None else stride
def _stage_base(self, stage):
return self.col_start + stage * self.stride
def __getitem__(self, item):
if isinstance(item, tuple):
assert len(item) == 2, "TMEMStages expects region[stage] or region[stage, start:stop]"
stage, col_slice = item
assert isinstance(col_slice, slice), "TMEMStages tuple indexing requires a slice"
base = self._stage_base(stage)
start = 0 if col_slice.start is None else col_slice.start
stop = self.width if col_slice.stop is None else col_slice.stop
return self.buf[:, base + start : base + stop : col_slice.step]
base = self._stage_base(item)
return self.buf[:, base : base + self.width]
# ---------------------------------------------------------------------------
# TMEMPool
# ---------------------------------------------------------------------------
@_meta_class
class TMEMPool:
"""Bump allocator over TMEM columns."""
def __init__(
self,
pool,
total_cols=512,
*,
cta_group=1,
alloc_warp=0,
dealloc_warp=None,
tmem_addr=None,
sync_after_alloc=True,
):
# tcgen05 alloc/dealloc are warp-uniform PTX instructions: every lane
# in the chosen warp must participate, and exactly one warp in the
# CTA must execute them. The pool emits its own
# ``if warp_id() == target_warp: tcgen05.alloc(...)``
# guard, using the cta->warp scope id ``T.warp_id()``.
# NOTE: synccheck currently false-deadlocks on kernels that declare a
# second warp-scope id (cpusim binds only one warp var); the generated
# CUDA is equivalent to ``thread_rank() // 32 == target_warp``.
self.pool = pool
self.total_cols = total_cols
self.cta_group = cta_group
self.alloc_warp = alloc_warp
self.dealloc_warp = alloc_warp if dealloc_warp is None else dealloc_warp
self.sync_after_alloc = sync_after_alloc
self.offset = 0
self.max_offset = 0
self._committed = False
self._deallocated = False
self._addr_buf = pool.alloc([1], "uint32", align=4) if tmem_addr is None else tmem_addr
def _addr_slot(self):
try:
return self._addr_buf[0]
except TypeError:
return self._addr_buf
@property
def addr(self):
return self._addr_slot()
def _emit_warp_guard(self, target_warp, emit):
from tvm.script import tirx as T
warp_id = T.warp_id()
with T.If(warp_id == target_warp):
with T.Then():
emit()
def _resolve_cols(self, shape, dtype, cols, layout=None):
if cols is not None:
return cols
bits = DataType(dtype).bits
if layout is not None:
# span("TCol") is in *element* (buffer dtype) units; one TMEM cell
# holds 32 bits regardless of the element type.
tcol_elems = int(layout.span("TCol"))
tcol_bits = tcol_elems * bits
assert tcol_bits % 32 == 0, (
f"layout TCol span={tcol_elems} elems x {bits}b is not 32-bit aligned"
)
return tcol_bits // 32
assert len(shape) == 2, "TMEMPool.alloc() requires cols= for non-2D TMEM buffers"
total_bits = _shape_product(shape) * bits
rows = shape[0]
assert total_bits % (32 * rows) == 0, (
f"Cannot infer TMEM columns from shape={shape}, dtype={dtype!r}; "
"please pass cols= explicitly"
)
return total_bits // (32 * rows)
def alloc(self, shape, dtype="float32", *, layout=None, cols=None, datapath=None):
"""Allocate a TMEM buffer.
Parameters
----------
shape, dtype, cols
Standard buffer shape / dtype / column count.
layout
Explicit ``TileLayout``. Mutually exclusive with ``datapath``.
datapath : str | None
Optional tcgen05 datapath letter (``"D"`` for M=128 full datapath,
``"F"`` for M=64 non-``.ws`` scattered). When provided, the buffer's
layout is derived from ``tmem_datapath_layout(datapath, *shape)``
so the row index reflects the *physical* TMEM lane occupation
(PTX ISA §9.7.16.10.5). The downstream ``.16x*b`` / ``.32x32b``
dispatches structurally check this layout to catch mismatched
atoms (e.g. a ``.16x*b`` M=128 read against a Layout F buffer).
Defaults to ``None``, which means Layout D's identity row→lane
mapping — keep this for shape ``(128, X)`` buffers that hold
an M=128 MMA accumulator.
"""
from tvm.tirx.layout import tmem_datapath_layout
if layout is not None and datapath is not None:
raise ValueError("TMEMPool.alloc: pass at most one of layout= and datapath=")
if datapath is not None:
assert len(shape) == 2, "TMEMPool.alloc: datapath= requires a 2-D shape"
layout = tmem_datapath_layout(datapath, shape[0], shape[1])
ir = _get_ir()
cols = self._resolve_cols(shape, dtype, cols, layout)
col_start = self.offset
col_end = col_start + cols
assert col_end <= self.total_cols, f"TMEM overflow: {col_end} > {self.total_cols}"
if layout is None:
assert len(shape) == 2, "TMEMPool.alloc() requires layout= for non-2D TMEM buffers"
layout = _default_tmem_layout(shape[0], shape[1])
res = ir.decl_buffer(shape, dtype, scope="tmem", allocated_addr=col_start, layout=layout)
self.offset = col_end
self.max_offset = max(self.max_offset, self.offset)
return res
def alloc_sf(self, shape, dtype, *, sf_per_mma, sf_reuse=1):
"""Allocate a tcgen05 block-scaled SF TMEM buffer with an inferred layout.
``shape`` last two dims are ``(rows, SF_K * sf_reuse)`` (the last dim is
what gemm dispatch iterates over). When ``shape`` has 3 dims, the first
is treated as a pipe-depth outer.
"""
from tvm.backend.cuda.operator.tile_primitive.gemm_async.tcgen05 import sf_tmem_layout
if len(shape) == 2:
pipe_depth, rows, last = None, shape[0], shape[1]
elif len(shape) == 3:
pipe_depth, rows, last = shape[0], shape[1], shape[2]
else:
raise ValueError(
f"alloc_sf expects 2D (rows, SF_K*sf_reuse) or 3D "
f"(pipe_depth, rows, SF_K*sf_reuse); got shape={shape}"
)
assert last % sf_reuse == 0, (
f"alloc_sf: shape last dim {last} must be divisible by sf_reuse={sf_reuse}"
)
SF_K = last // sf_reuse
layout = sf_tmem_layout(
rows=rows, SF_K=SF_K, sf_per_mma=sf_per_mma, sf_reuse=sf_reuse, pipe_depth=pipe_depth
)
return self.alloc(shape, dtype, layout=layout)
def move_base_to(self, col):
self.offset = col
self.max_offset = max(self.max_offset, self.offset)
def commit(self):
assert not self._committed, "TMEMPool.commit() can only be called once"
from tvm.script import tirx as T
def emit_alloc():
_emit_stmt(
T.ptx.tcgen05.alloc(
T.address_of(self.addr), n_cols=self.total_cols, cta_group=self.cta_group
)
)
if self.sync_after_alloc:
_emit_stmt(T.cuda.warp_sync())
self._emit_warp_guard(self.alloc_warp, emit_alloc)
self._committed = True
def dealloc(self):
assert self._committed, "TMEMPool.dealloc() called before commit()"
assert not self._deallocated, "TMEMPool.dealloc() can only be called once"
self._deallocated = True
from tvm.script import tirx as T
def emit_dealloc():
_emit_stmt(T.ptx.tcgen05.relinquish_alloc_permit(cta_group=self.cta_group))
_emit_stmt(
T.ptx.tcgen05.dealloc(self.addr, n_cols=self.total_cols, cta_group=self.cta_group)
)
self._emit_warp_guard(self.dealloc_warp, emit_dealloc)
# ---------------------------------------------------------------------------
# SMEMPool
# ---------------------------------------------------------------------------
@_meta_class
class SMEMPool:
"""Bump allocator over a contiguous shared memory region.
Parameters
----------
ptr : Var or None, optional
If omitted, an ``alloc_buffer([0], "uint8", scope="shared.dyn")`` is
created automatically and ``commit()`` must be called after all
allocations to emit the size annotation.
If a ``Var`` is provided, the caller manages the backing buffer and
``commit()`` is a no-op.
"""
def __init__(self, ptr=_POOL_UNSET):
ir = _get_ir()
if ptr is _POOL_UNSET:
self.buf = ir.alloc_buffer([0], "uint8", scope="shared.dyn")
self.ptr = self.buf.data
self._owns_buffer = True
else:
self.buf = None
self.ptr = ptr
self._owns_buffer = False
self.offset = 0
self.max_offset = 0
def alloc(
self,
shape,
dtype="float32",
strides=None,
scope="shared.dyn",
align=0,
buffer_type="",
axis_separators=None,
layout="default",
):
ir = _get_ir()
if align > 0:
self.offset = (self.offset + align - 1) // align * align
res = ir.decl_buffer(
shape,
dtype,
data=self.ptr,
strides=strides,
byte_offset=self.offset,
scope=scope,
align=align,
buffer_type=buffer_type,
axis_separators=axis_separators,
layout=layout,
)
# Advance in bits then round up to bytes so sub-byte dtypes (e.g.
# float4_e2m1fn = 4 bits) still bump the cursor instead of leaving it
# at 0 (bits // 8) and silently overlapping the next allocation.
self.offset += (_shape_product(shape) * DataType(dtype).bits + 7) // 8
if self._owns_buffer:
self.max_offset = max(self.max_offset, self.offset)
return res
def alloc_mma(self, shape, dtype="float16", swizzle_mode="auto", align=1024):
"""Allocate MMA-compatible shared memory with an inferred swizzle layout."""
from tvm.backend.cuda.operator.tile_primitive.tma_utils import (
SwizzleMode,
mma_shared_layout,
)
if isinstance(swizzle_mode, str):
if swizzle_mode == "auto":
swizzle_mode = _auto_swizzle_mode(dtype)
elif swizzle_mode == "none":
swizzle_mode = SwizzleMode.SWIZZLE_NONE
else:
raise ValueError(
f"Unsupported swizzle_mode={swizzle_mode!r}; expected 'auto', 'none', "
"or SwizzleMode"
)
_validate_mma_alloc_shape(shape, dtype, swizzle_mode)
layout = mma_shared_layout(dtype, swizzle_mode, shape)
return self.alloc(shape, dtype, align=align, layout=layout)
def move_base_to(self, offset):
self.offset = offset
if self._owns_buffer:
self.max_offset = max(self.max_offset, self.offset)
def commit(self, size=None):
"""Emit pool size annotation into the IR.
Must be called after all ``alloc()`` / ``move_base_to()`` calls.
Parameters
----------
size : int, optional
Explicit shared memory size in bytes. When *None* (the default),
the high-water mark ``max_offset`` tracked by the allocator is used.
"""
if not self._owns_buffer:
return
ir = _get_ir()
frame_mod = _get_frame()
resolved = size if size is not None else self.max_offset
assert resolved >= self.max_offset, (
f"Specified smem size ({resolved}) is smaller than "
f"the pool high-water mark ({self.max_offset})"
)
attr_frame = ir.attr(self.ptr, "tirx.pool_max_bytes", resolved)
if isinstance(attr_frame, frame_mod.AttrFrame):
from functools import partial
attr_frame.add_callback(partial(attr_frame.__exit__, None, None, None))
attr_frame.__enter__()
+251
View File
@@ -0,0 +1,251 @@
# 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.
"""Reusable pipeline state and mbarrier helpers for SM100 kernels.
These classes emit TIR via @T.inline. Decorate with @T.meta_class so that
instances are automatically treated as meta values inside @T.prim_func.
"""
from tvm.script import tirx as T
@T.meta_class
class PipelineState:
"""Tracks stage and phase for a software-pipelined ring buffer.
This class does not know anything about full/empty barriers. Use it when
the kernel manually waits/signals barriers, or when the stage/phase drives
a ring not wrapped in a ``Pipeline``.
Parameters
----------
depth : int
Number of stages in the ring.
phase : int, optional
Initial phase. Omit when initialization should happen later.
"""
def __init__(self, depth: int, phase=None):
self.stage = T.local_scalar("int32")
self.phase = T.local_scalar("int32")
self.depth = depth
if phase is not None:
self.init(phase)
@T.inline
def init(self, phase):
self.stage = 0
self.phase = phase
@T.inline
def advance(self):
if self.depth > 1:
self.stage = self.stage + 1
if self.stage == self.depth:
self.stage = 0
self.phase = self.phase ^ 1
else:
self.phase = self.phase ^ 1
@T.meta_class
class MBarrier:
"""Mbarrier wrapper with regular ``mbarrier.arrive``.
Parameters
----------
pool : SMEMPool
Shared memory pool allocator.
depth : int
Number of barrier slots (one per pipeline stage).
phase_offset : int
XORed into the phase bit on every ``wait`` / ``arrive``.
leader : Expr, optional
Boolean predicate selecting the single thread that runs
``mbarrier.init``. Defaults to ``T.cuda.thread_rank() == 0`` --
thread 0 of the enclosing CTA, which always picks exactly one
thread regardless of which scope_id vars the caller declared.
Override only when you want a different CTA-local thread to do
the init.
Note: the default deliberately avoids ``T.warp_id()`` /
``T.lane_id()``. Those introduce deferred ``cta->warp`` /
``warp->thread`` ScopeIdDefs that the verifier cannot pin down
unless the kernel header declares the full warp/lane chain (e.g. a
single-CTA DSMEM kernel that only declares ``thread_id``). It also
avoids the synccheck false-deadlock on kernels that declare a
second warp-scope id. The generated CUDA is equivalent.
"""
def __init__(self, pool, depth, phase_offset=0, leader=None):
self.buf = pool.alloc((depth,), "uint64", align=8)
self.depth = depth
self.phase_offset = phase_offset
self.leader = leader if leader is not None else (T.cuda.thread_rank() == 0)
@T.inline
def init(self, count):
if self.leader:
for i in T.unroll(self.depth):
T.ptx.mbarrier.init(self.buf.ptr_to([i]), count)
@T.inline
def wait(self, stage, phase):
# Blocks: ``mbarrier.try_wait`` loops internally until the phase flips,
# so this returns only once the barrier has completed.
T.ptx.mbarrier.try_wait(self.buf.ptr_to([stage]), phase ^ self.phase_offset)
@T.inline
def arrive(self, stage, cta_id=None, pred=None, count=None):
# Default: local-CTA arrive — emits the simple
# ``mbarrier.arrive.shared.b64`` form. To arrive on a remote
# CTA's mbarrier in a cluster kernel, callers must pass
# ``cta_id=`` explicitly (e.g. ``bar.arrive(stage, cta_id=0)``)
# or use ``MBarrier.remote_view(rank).arrive(stage)``. Defaulting
# the cross-CTA path was both surprising (``bar.arrive(stage)``
# silently ``mapa`` ed across the cluster) and a per-call cost
# of ~3 PTX ops on every single-CTA kernel.
#
# ``count`` (cross-CTA path only) emits the explicit arrival-count
# operand, i.e. ``mbarrier.arrive.shared::cluster.b64 _, [addr], count``.
# When ``None`` the implicit count-of-1 form is emitted. Passing
# ``count=1`` is semantically identical but spells the count explicitly.
if cta_id is None:
T.ptx.mbarrier.arrive(self.buf.ptr_to([stage]))
else:
actual_pred = True if pred is None else pred
T.ptx.mbarrier.arrive(
self.buf.ptr_to([stage]), cta_id=cta_id, pred=actual_pred, count=count
)
def ptr_to(self, idx):
return self.buf.ptr_to(idx)
def remote_view(self, rank):
"""Create a view of this barrier mapped to another CTA's shared memory.
Arrive-only: the returned view is built with ``object.__new__`` and
never copies ``self.leader``, so calling ``.init()`` on it would fail.
Use it solely to ``arrive`` on a remote CTA's mbarrier.
"""
from tvm.ir import PointerType, PrimType
from tvm.tirx import Var as TIRVar
expr = T.reinterpret("handle", T.ptx.map_shared_rank(self.buf.ptr_to([0]), rank))
ptr = TIRVar("remote_mbar_ptr", PointerType(PrimType("uint64")))
T.Bind(expr, var=ptr)
buf = T.decl_buffer([self.depth], "uint64", data=ptr, scope="shared")
remote = object.__new__(type(self))
remote.buf = buf
remote.depth = self.depth
remote.phase_offset = self.phase_offset
return remote
class TMABar(MBarrier):
"""Barrier signaled by TMA (mbarrier.arrive.expect_tx).
When ``tx_count`` is None, falls back to a remote mbarrier.arrive
(matching MBarrier.arrive defaults).
"""
@T.inline
def arrive(self, stage, tx_count=None, cta_id=None, pred=None):
# NOTE: this arrive() kwarg set intentionally differs from
# MBarrier.arrive (hardware necessity, LSP-incompatible by design).
# ``tx_count``: TMA byte count for ``mbarrier.arrive.expect_tx``.
# ``cta_id`` / ``pred``: forwarded to the underlying
# ``mbarrier.arrive`` (cluster path) when set; otherwise the
# arrive is local-CTA only. See ``MBarrier.arrive`` for the
# full default-local rationale.
if tx_count is not None:
T.ptx.mbarrier.arrive.expect_tx(self.buf.ptr_to([stage]), tx_count)
elif cta_id is None:
T.ptx.mbarrier.arrive(self.buf.ptr_to([stage]))
else:
actual_pred = True if pred is None else pred
T.ptx.mbarrier.arrive(self.buf.ptr_to([stage]), cta_id=cta_id, pred=actual_pred)
class TCGen05Bar(MBarrier):
"""Barrier signaled by ``tcgen05`` commit.
The caller is responsible for ensuring only one thread issues the
commit, e.g. by wrapping the call in ``if T.ptx.elect_sync():``.
"""
@T.inline
def arrive(self, stage, cta_group=1, cta_mask=None):
# NOTE: this arrive() kwarg set intentionally differs from
# MBarrier.arrive (hardware necessity, LSP-incompatible by design).
if cta_mask is None and cta_group == 1:
T.ptx.tcgen05.commit(self.buf.ptr_to([stage]))
else:
T.ptx.tcgen05.commit(self.buf.ptr_to([stage]), cta_group=cta_group, cta_mask=cta_mask)
# Barrier-type tags accepted by Pipeline's ``full=`` / ``empty=`` arguments.
_BAR_KINDS = {"tma": TMABar, "tcgen05": TCGen05Bar, "mbar": MBarrier}
@T.meta_class
class Pipeline:
"""A full/empty mbarrier pair for a software-pipelined data flow.
Pass barrier-type tags and ``Pipeline`` constructs and ``init``\\ s the
barriers itself. Tags: ``"tma"`` (TMABar), ``"tcgen05"`` (TCGen05Bar),
``"mbar"`` (MBarrier). The barrier type and arrival count of each event
stay explicit at the call site -- e.g. ``Pipeline(pool, n, full="tma",
empty="tcgen05", init_empty=NUM_CONSUMER)``.
Both signals are required: a ``Pipeline`` is a *pair*. For a one-way event
(a pure "X happened" signal with no slot to recycle) use a bare barrier
(``TMABar``/``TCGen05Bar``/``MBarrier``) directly -- it has no empty side.
Parameters
----------
pool : SMEMPool
Shared memory pool allocator.
stages : int
Number of pipeline stages (barrier slots).
full, empty : str
Barrier-type tag for the full / empty signal (see above).
init_full, init_empty : int
Expected arrival count for the full / empty barrier.
empty_phase_offset : int
XORed into the empty barrier's phase bit on every wait / arrive.
leader : Expr, optional
Propagated to both barriers; defaults to thread 0 of the CTA.
"""
def __init__(
self,
pool,
stages,
*,
full,
empty,
init_full=1,
init_empty=1,
empty_phase_offset=0,
leader=None,
):
self.stages = stages
self.full = _BAR_KINDS[full](pool, stages, leader=leader)
self.full.init(init_full)
self.empty = _BAR_KINDS[empty](pool, stages, phase_offset=empty_phase_offset, leader=leader)
self.empty.init(init_empty)
+55
View File
@@ -0,0 +1,55 @@
# 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.
"""SMEM matrix descriptor helper for tcgen05 / wgmma."""
from tvm.backend.cuda.operator.tile_primitive.common import smem_desc_add_16B_offset
from tvm.script import tirx as T
@T.meta_class
class SmemDescriptor:
"""Encoded once via :meth:`init`, reused via :meth:`add_16B_offset`."""
def __init__(self):
self._buf = T.alloc_local([1], "uint64")
@property
def desc(self):
return self._buf[0]
@T.inline
def init(self, smem_ptr, ldo, sdo, swizzle):
T.ptx.tcgen05.encode_matrix_descriptor(
T.address_of(self._buf[0]), smem_ptr, ldo, sdo, swizzle
)
def add_16B_offset(self, offset):
return smem_desc_add_16B_offset(self._buf[0], offset)
def make_lo_uniform(self):
"""Broadcast the lower 32 bits to all warp lanes via ``__shfl_sync``."""
func_name = "smem_desc_make_lo_uniform"
source_code = f"""
__forceinline__ __device__ void {func_name}(uint64_t* desc) {{
SmemDescriptor* d = reinterpret_cast<SmemDescriptor*>(desc);
d->lo = __shfl_sync(0xffffffff, d->lo, 0);
}}
"""
return T.cuda.func_call(
func_name, T.address_of(self._buf[0]), source_code=source_code, return_type="void"
)
@@ -0,0 +1,945 @@
# 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.
"""Reusable tile scheduler helpers for TIR tests/kernels.
These classes emit TIR via @T.inline. Decorate with @T.meta_class so that
instances are automatically treated as meta values inside @T.prim_func.
"""
from tvm.backend.cuda.lang.pipeline import Pipeline, PipelineState
from tvm.script import tirx as T
@T.meta_class
class BaseTileScheduler:
"""Base class for tile schedulers with common state and macros."""
def __init__(self, prefix: str):
self.m_idx = T.local_scalar("int32")
self.n_idx = T.local_scalar("int32")
self.linear_idx = T.local_scalar("int32")
@T.inline
def update_current_m_n_idx(self, linear_idx):
# To be implemented by subclasses
pass
@T.inline
def init(self, linear_init):
self.linear_idx = linear_init
self.update_current_m_n_idx(linear_init)
@T.inline
def next_tile(self, step):
self.linear_idx = self.linear_idx + step
self.update_current_m_n_idx(self.linear_idx)
def valid(self, total_tiles):
return self.linear_idx < total_tiles
class ClusterPersistentScheduler2D(BaseTileScheduler):
"""
Tile scheduler for cluster-based persistent kernels.
Distributes a 2D tile grid across persistent clusters using group-major ordering
for L2 cache locality. Each cluster starts at its cluster_id and strides by
num_clusters to process tiles.
Tile Ordering (group-major for L2 locality):
- Tiles are grouped into "L2 groups" of `l2_group_size` rows
- Within a group, tiles are visited in column-major order within the group
- Groups are processed in row-major order
Example with 4x4 tiles, l2_group_size=2:
Group 0 (rows 0-1): 0 2 4 6
1 3 5 7
Group 1 (rows 2-3): 8 10 12 14
9 11 13 15
Serpentine Mode (serpentine=True):
- Uses CUTLASS-style 2D block swizzle with serpentine traversal
- Grid is divided into swizzle_size x swizzle_size blocks
- Within each block, tiles are visited in row-major order
- Blocks are traversed in serpentine order (even block-rows forward, odd backward)
- This provides better L2 locality by reusing both A and B tiles
Example with 4x4 tiles, swizzle_size=2, serpentine=True:
Block layout:
Block(0,0) Block(0,1)
Block(1,0) Block(1,1)
Tile numbering with serpentine:
n=0 n=1 n=2 n=3
m=0 0 1 14 15
m=1 2 3 12 13
m=2 4 5 10 11
m=3 6 7 8 9
Traversal: Block(0,0) -> Block(1,0) -> Block(1,1) -> Block(0,1)
(serpentine: down in col 0, then up in col 1)
Parameters
----------
prefix : str
Prefix for TIR variable names
num_m_tiles : int | T.ExprLike
Total number of tiles in M dimension (can be runtime expression)
num_n_tiles : int
Total number of tiles in N dimension
num_clusters : int
Number of persistent clusters (determines stride)
l2_group_size : int
Number of M-tile rows per L2 locality group (default: 8)
When serpentine=True, this is used as swizzle_size for 2D blocks
cluster_m : int
Cluster dimension in M for hierarchical scheduling (default: 1)
cluster_n : int
Cluster dimension in N for hierarchical scheduling (default: 1)
serpentine : bool
If True, use CUTLASS-style 2D block swizzle with serpentine traversal (default: False)
Attributes
----------
m_idx : T.local_scalar
Current M tile index (output)
n_idx : T.local_scalar
Current N tile index (output)
work_idx : T.local_scalar
Global work item index for this cluster
tile_count : T.local_scalar
Number of tiles processed by this cluster so far
Usage
-----
```python
scheduler = ClusterPersistentScheduler2D(
"sched", num_m_tiles=M_TILES, num_n_tiles=N_TILES,
num_clusters=NUM_CLUSTERS, l2_group_size=8
)
scheduler.init(cluster_id) # cluster_id = cta_idx // CLUSTER_SIZE
while scheduler.valid():
m = T.meta_var(scheduler.m_idx) # current M tile
n = T.meta_var(scheduler.n_idx) # current N tile
# ... process tile (m, n) ...
scheduler.next_tile()
```
Examples
--------
Example 1: Basic persistent kernel
```
num_m_tiles=4, num_n_tiles=4, num_clusters=3, l2_group_size=2
cluster_m=1, cluster_n=1 (default, no tile subdivision)
Group-major tile numbering (l2_group_size=2):
n=0 n=1 n=2 n=3
m=0 0 2 4 6 ┐ L2 group 0
m=1 1 3 5 7 ┘
m=2 8 10 12 14 ┐ L2 group 1
m=3 9 11 13 15 ┘
Work distribution (cluster starts at cluster_id, strides by num_clusters=3):
cluster 0: work_idx 0,3,6,9,12,15 -> tiles 0,3,6,9,12,15
cluster 1: work_idx 1,4,7,10,13 -> tiles 1,4,7,10,13
cluster 2: work_idx 2,5,8,11,14 -> tiles 2,5,8,11,14
Tile grid (which cluster handles each tile):
n=0 n=1 n=2 n=3
m=0 C0 C2 C1 C0 ┐ L2 group 0
m=1 C1 C0 C2 C1 ┘
m=2 C2 C1 C0 C2 ┐ L2 group 1
m=3 C0 C2 C1 C0 ┘
Tile sequence per cluster (in execution order):
cluster 0: (0,0)->(1,1)->(0,3)->(2,0)->(2,3)->(3,3)
cluster 1: (1,0)->(0,2)->(1,3)->(2,1)->(3,2)
cluster 2: (0,1)->(1,2)->(2,0)->(3,1)->(2,3)
```
Example 2: 2SM GEMM (typical B200 config)
```
M=1024, N=512, CTA_M=128, MMA_N=128, CLUSTER_M=2, CLUSTER_N=1
=> M_TILES=8, N_TILES=4
=> CLUSTER_M_TILES=4, CLUSTER_N_TILES=4 (scheduler at cluster granularity)
Scheduler params:
num_m_tiles=4, num_n_tiles=4, num_clusters=74, l2_group_size=8
cluster_m=1, cluster_n=1
Key: Scheduler outputs CLUSTER-level tiles.
All CTAs in same cluster get SAME (m_idx, n_idx) from scheduler.
CTAs differentiate via cluster_rank (computed OUTSIDE scheduler):
cluster_rank = cta_idx % CLUSTER_SIZE
cb_m = cluster_rank % CLUSTER_M # 0 or 1 for 2SM
cb_n = cluster_rank // CLUSTER_M # 0 for 2SM
Final CTA tile:
cta_m = m_idx * CLUSTER_M + cb_m
cta_n = n_idx * CLUSTER_N + cb_n
Example: cluster 5 gets scheduler tile (1,2)
CTA rank=0 (cb_m=0): actual tile (2,2)
CTA rank=1 (cb_m=1): actual tile (3,2)
```
"""
def __init__(
self,
prefix: str,
num_m_tiles,
num_n_tiles: int,
num_clusters: int,
l2_group_size: int = 8,
cluster_m: int = 1,
cluster_n: int = 1,
serpentine: bool = False,
):
super().__init__(prefix)
self._num_m_tiles = num_m_tiles
self._num_n_tiles = num_n_tiles
self._num_clusters = num_clusters
self._l2_group_size = l2_group_size
self._cluster_m = cluster_m
self._cluster_n = cluster_n
self._serpentine = serpentine
# Rename internal state for clarity
self.work_idx = self.linear_idx # alias: global work item index
self.tile_count = T.local_scalar("int32")
self.tile_idx = self.tile_count # alias for backward compatibility
is_static_m = isinstance(num_m_tiles, int)
# Number of tile columns after accounting for cluster_n
n_tile_cols = (num_n_tiles + cluster_n - 1) // cluster_n
self._N_TILE_COLS = n_tile_cols
if is_static_m:
self._M_TILE_ROWS = (num_m_tiles + cluster_m - 1) // cluster_m
self._FULL_GROUPS = self._M_TILE_ROWS // l2_group_size
else:
# Dynamic expressions for runtime M
self._M_TILE_ROWS = T.truncdiv(self._num_m_tiles + self._cluster_m - 1, self._cluster_m)
self._FULL_GROUPS = T.truncdiv(self._M_TILE_ROWS, self._l2_group_size)
self._TAIL_ROWS = self._M_TILE_ROWS - self._FULL_GROUPS * l2_group_size
self._TOTAL_TILES = self._M_TILE_ROWS * n_tile_cols * cluster_m * cluster_n
# For serpentine mode: precompute block counts
if serpentine:
self._N_BLOCKS = n_tile_cols // l2_group_size # full blocks in N
self._M_BLOCKS = (
self._M_TILE_ROWS // l2_group_size
if is_static_m
else T.truncdiv(self._M_TILE_ROWS, l2_group_size)
)
self._BLOCK_SIZE = l2_group_size * l2_group_size # tiles per block
self._FULL_BLOCK_TILES = self._M_BLOCKS * self._N_BLOCKS * self._BLOCK_SIZE
# Residual tiles (not covered by full blocks)
self._RESIDUAL_N = n_tile_cols - self._N_BLOCKS * l2_group_size
self._RESIDUAL_M = self._M_TILE_ROWS - self._M_BLOCKS * l2_group_size
# fmt: off
@T.inline
def update_current_m_n_idx(self, work_idx):
"""Convert global work index to (m_idx, n_idx) tile coordinates."""
CLUSTER_M = T.meta_var(self._cluster_m)
CLUSTER_N = T.meta_var(self._cluster_n)
# Extract hierarchical cluster-local offsets
cluster_m_offset = T.meta_var(work_idx % CLUSTER_M)
t = T.meta_var(work_idx // CLUSTER_M)
cluster_n_offset = T.meta_var(t % CLUSTER_N)
tile_linear = T.meta_var(t // CLUSTER_N)
@T.inline
def set_tile_coords(tile_row, tile_col):
self.m_idx = tile_row * CLUSTER_M + cluster_m_offset
self.n_idx = tile_col * CLUSTER_N + cluster_n_offset
if self._serpentine:
self._update_serpentine(tile_linear, set_tile_coords)
else:
self._update_group_major(tile_linear, set_tile_coords)
def _update_group_major(self, tile_linear, set_tile_coords):
"""Group-major ordering with parse-time pruning of statically-dead branches.
The TIR script parser does not constant-fold ``if False: ...``, so a
Python-literal ``FULL_GROUPS == 0`` would otherwise produce
``T.bitwise_and(T.bool(False), tile_linear < 0)`` IR plus the dead
then-leg. Branch in plain Python here and only invoke the inline
emitter that can actually fire.
"""
full_zero = isinstance(self._FULL_GROUPS, int) and self._FULL_GROUPS == 0
tail_zero = isinstance(self._TAIL_ROWS, int) and self._TAIL_ROWS == 0
if full_zero and tail_zero:
self._gm_emit_zero(set_tile_coords)
elif full_zero:
self._gm_emit_tail_only(tile_linear, set_tile_coords)
elif tail_zero:
self._gm_emit_full_only(tile_linear, set_tile_coords)
else:
self._gm_emit_full_and_tail(tile_linear, set_tile_coords)
@T.inline
def _gm_emit_zero(self, set_tile_coords):
set_tile_coords(0, 0)
@T.inline
def _gm_emit_full_only(self, tile_linear, set_tile_coords):
FULL_GROUPS = T.meta_var(self._FULL_GROUPS)
GROUP_SIZE = T.meta_var(self._l2_group_size)
GROUP_SPAN = T.meta_var(self._l2_group_size * self._N_TILE_COLS)
if (FULL_GROUPS > 0) & (tile_linear < FULL_GROUPS * GROUP_SPAN):
group_id: T.let = tile_linear // GROUP_SPAN
within_group: T.let = tile_linear % GROUP_SPAN
tile_row: T.let = group_id * GROUP_SIZE + (within_group % GROUP_SIZE)
tile_col: T.let = within_group // GROUP_SIZE
set_tile_coords(tile_row, tile_col)
else:
set_tile_coords(0, 0)
@T.inline
def _gm_emit_tail_only(self, tile_linear, set_tile_coords):
FULL_GROUPS = T.meta_var(self._FULL_GROUPS)
TAIL_ROWS = T.meta_var(self._TAIL_ROWS)
GROUP_SIZE = T.meta_var(self._l2_group_size)
GROUP_SPAN = T.meta_var(self._l2_group_size * self._N_TILE_COLS)
if TAIL_ROWS > 0:
rem: T.let = tile_linear - FULL_GROUPS * GROUP_SPAN
tile_row: T.let = FULL_GROUPS * GROUP_SIZE + (rem % TAIL_ROWS)
tile_col: T.let = rem // TAIL_ROWS
set_tile_coords(tile_row, tile_col)
else:
set_tile_coords(0, 0)
@T.inline
def _gm_emit_full_and_tail(self, tile_linear, set_tile_coords):
FULL_GROUPS = T.meta_var(self._FULL_GROUPS)
TAIL_ROWS = T.meta_var(self._TAIL_ROWS)
GROUP_SIZE = T.meta_var(self._l2_group_size)
GROUP_SPAN = T.meta_var(self._l2_group_size * self._N_TILE_COLS)
if (FULL_GROUPS > 0) & (tile_linear < FULL_GROUPS * GROUP_SPAN):
group_id: T.let = tile_linear // GROUP_SPAN
within_group: T.let = tile_linear % GROUP_SPAN
tile_row: T.let = group_id * GROUP_SIZE + (within_group % GROUP_SIZE)
tile_col: T.let = within_group // GROUP_SIZE
set_tile_coords(tile_row, tile_col)
elif TAIL_ROWS > 0:
rem: T.let = tile_linear - FULL_GROUPS * GROUP_SPAN
tile_row: T.let = FULL_GROUPS * GROUP_SIZE + (rem % TAIL_ROWS)
tile_col: T.let = rem // TAIL_ROWS
set_tile_coords(tile_row, tile_col)
else:
set_tile_coords(0, 0)
@T.inline
def _update_serpentine(self, tile_linear, set_tile_coords):
"""CUTLASS-style 2D block swizzle with serpentine traversal.
Algorithm:
1. Divide grid into swizzle_size x swizzle_size blocks
2. Within each block, visit tiles in row-major order
3. Blocks are traversed column by column (along N)
4. Within each column of blocks, use serpentine:
- Even columns: top to bottom
- Odd columns: bottom to top
This maximizes L2 reuse for both A and B matrices.
"""
S = T.meta_var(self._l2_group_size) # swizzle_size
M_BLOCKS = T.meta_var(self._M_BLOCKS)
N_BLOCKS = T.meta_var(self._N_BLOCKS)
BLOCK_SIZE = T.meta_var(self._BLOCK_SIZE) # S * S
FULL_BLOCK_TILES = T.meta_var(self._FULL_BLOCK_TILES)
M_TILE_ROWS = T.meta_var(self._M_TILE_ROWS)
T.meta_var(self._N_TILE_COLS)
RESIDUAL_N = T.meta_var(self._RESIDUAL_N)
RESIDUAL_M = T.meta_var(self._RESIDUAL_M)
# Check if we're in the full block region
if (M_BLOCKS > 0) & (N_BLOCKS > 0) & (tile_linear < FULL_BLOCK_TILES):
# Which block (in linear order along columns of blocks)
block_linear: T.let = tile_linear // BLOCK_SIZE
within_block: T.let = tile_linear % BLOCK_SIZE
# Block column and row
block_col: T.let = block_linear // M_BLOCKS
block_row_raw: T.let = block_linear % M_BLOCKS
# Serpentine: odd columns go bottom-to-top
block_row: T.let = T.Select(
block_col % 2 == 0,
block_row_raw,
M_BLOCKS - 1 - block_row_raw
)
# Position within block (row-major within block)
local_row: T.let = within_block // S
local_col: T.let = within_block % S
tile_row: T.let = block_row * S + local_row
tile_col: T.let = block_col * S + local_col
set_tile_coords(tile_row, tile_col)
elif RESIDUAL_N > 0:
# Residual tiles in the rightmost partial column of blocks
# These are tiles where n >= N_BLOCKS * S
rem: T.let = tile_linear - FULL_BLOCK_TILES
# First handle the right residual strip (full M height, partial N width)
right_strip_tiles: T.let = M_TILE_ROWS * RESIDUAL_N
if rem < right_strip_tiles:
# Row-major within the right strip
tile_row: T.let = rem // RESIDUAL_N
tile_col: T.let = N_BLOCKS * S + (rem % RESIDUAL_N)
set_tile_coords(tile_row, tile_col)
elif RESIDUAL_M > 0:
# Bottom residual strip (already covered in right strip overlap)
# This handles corner case - shouldn't normally reach here
# as right strip already covers full M height
set_tile_coords(0, 0)
else:
set_tile_coords(0, 0)
elif RESIDUAL_M > 0:
# Bottom residual strip only (no right residual)
rem: T.let = tile_linear - FULL_BLOCK_TILES
bottom_strip_tiles: T.let = RESIDUAL_M * (N_BLOCKS * S)
if rem < bottom_strip_tiles:
tile_row: T.let = M_BLOCKS * S + (rem % RESIDUAL_M)
tile_col: T.let = rem // RESIDUAL_M
set_tile_coords(tile_row, tile_col)
else:
set_tile_coords(0, 0)
else:
# Fallback
set_tile_coords(0, 0)
@T.inline
def init(self, cluster_id):
"""Initialize scheduler for a given cluster.
Parameters
----------
cluster_id : int
The cluster's index (typically cta_idx // CLUSTER_SIZE)
"""
self.linear_idx = cluster_id
self.tile_count = 0
self.update_current_m_n_idx(cluster_id)
@T.inline
def next_tile(self):
"""Advance to the next tile for this cluster."""
self.linear_idx = self.linear_idx + self._num_clusters
self.tile_count = self.tile_count + 1
self.update_current_m_n_idx(self.linear_idx)
@T.inline
def next_tile_stride(self, stride: int):
"""Advance by a custom stride (for non-standard scheduling)."""
self.linear_idx = self.linear_idx + stride
self.tile_count = self.tile_count + 1
self.update_current_m_n_idx(self.linear_idx)
# fmt: on
def valid(self):
"""Check if this cluster has more tiles to process."""
return self.linear_idx < self._TOTAL_TILES
class GroupMajor3D(BaseTileScheduler):
"""
3D grouped-row scheduler (M,N,K) with tail handling on M.
Args
----
prefix: str
m_tiles: int | T Expr # tiles along M (static or runtime)
n_tiles: int # tiles along N (static)
k_tiles: int # tiles along K (static)
group_rows: int # rows per group along M
step: int = 1 # default stride for next_tile()
"""
def __init__(
self, prefix: str, m_tiles, n_tiles: int, k_tiles: int, group_rows: int, step: int = 1
):
super().__init__(prefix)
self._step = step
self.tile_idx = T.local_scalar("int32")
self.k_idx = T.local_scalar("int32")
# ---- constants / primexprs baked once ----
self._G = group_rows
self._N = n_tiles
self._K = k_tiles
if isinstance(m_tiles, int):
self._GROUPS = m_tiles // group_rows
self._FINAL_ROWS = m_tiles - self._GROUPS * group_rows
self._SAFE_FINAL_ROWS = max(self._FINAL_ROWS, 1)
self._GROUP_SIZE = group_rows * n_tiles * k_tiles
self._TOTAL = m_tiles * n_tiles * k_tiles
else:
self._GROUPS = T.truncdiv(m_tiles, group_rows)
self._FINAL_ROWS = m_tiles - self._GROUPS * group_rows
self._SAFE_FINAL_ROWS = T.max(self._FINAL_ROWS, 1)
self._GROUP_SIZE = self._G * self._N * self._K
self._TOTAL = m_tiles * n_tiles * k_tiles
# handy composites used in macro
self._FULL_BOUND = self._GROUPS * self._GROUP_SIZE
self._HAS_FULL = self._GROUPS > 0
self._HAS_TAIL = self._FINAL_ROWS > 0
# fmt: off
@T.inline
def update_current_m_n_idx(self, linear_idx):
# full-group formulas
full_m: T.let = T.floordiv(linear_idx, self._GROUP_SIZE) * self._G + T.floormod(
linear_idx, self._G
)
full_n: T.let = T.floormod(T.floordiv(linear_idx, self._G), self._N)
full_k: T.let = T.floordiv(T.floormod(linear_idx, self._GROUP_SIZE), self._G * self._N)
# tail formulas (relative to FULL_BOUND)
# Use _SAFE_FINAL_ROWS (max(FINAL_ROWS, 1)) to avoid divide-by-zero when there is no tail
rem: T.let = linear_idx - self._FULL_BOUND
tail_m: T.let = self._GROUPS * self._G + T.floormod(rem, self._SAFE_FINAL_ROWS)
tail_n: T.let = T.floordiv(rem, self._SAFE_FINAL_ROWS) % self._N
tail_k: T.let = T.floordiv(rem, self._SAFE_FINAL_ROWS * self._N)
# choose phase
if self._HAS_FULL & (linear_idx < self._FULL_BOUND):
self.m_idx = full_m
self.n_idx = full_n
self.k_idx = full_k
elif self._HAS_TAIL:
self.m_idx = tail_m
self.n_idx = tail_n
self.k_idx = tail_k
else:
self.m_idx = 0
self.n_idx = 0
self.k_idx = 0
@T.inline
def init(self, linear_init):
self.linear_idx = linear_init
self.tile_idx = 0
self.update_current_m_n_idx(linear_init)
@T.inline
def next_tile(self):
self.linear_idx = self.linear_idx + self._step
self.tile_idx = self.tile_idx + 1
self.update_current_m_n_idx(self.linear_idx)
@T.inline
def next_tile_stride(self, stride: int):
self.linear_idx = self.linear_idx + stride
self.tile_idx = self.tile_idx + 1
self.update_current_m_n_idx(self.linear_idx)
# fmt: on
def valid(self):
return self.linear_idx < self._TOTAL
class RankAwareGroupMajorTileScheduler(BaseTileScheduler):
"""
Group-major scheduler that applies a rank-aware remapping (remote rows first).
Kept as a thin adapter because it depends on NVSHMEM rank at device-side.
"""
def __init__(
self, prefix: str, m_clusters: int, n_clusters: int, group_size: int, world_size: int
):
super().__init__(prefix)
self._m_clusters = m_clusters
self._n_clusters = n_clusters
self._group_size = group_size
self._world_size = world_size
@T.inline
def update_current_m_n_idx(self, linear_idx):
my_rank: T.let = T.nvshmem.my_pe()
remote_m_clusters: T.let = self._m_clusters - self._m_clusters // self._world_size
group_rows: T.let = (remote_m_clusters // self._group_size) * self._group_size
final_rows: T.let = remote_m_clusters - group_rows
group_repeat: T.let = self._group_size * self._n_clusters
if linear_idx < group_rows * self._n_clusters and group_rows > 0:
self.m_idx = (
(linear_idx // group_repeat) * self._group_size
+ (linear_idx % self._group_size)
+ (my_rank + 1) * self._m_clusters // self._world_size
) % self._m_clusters
self.n_idx = (linear_idx % group_repeat) // self._group_size
elif linear_idx < remote_m_clusters * self._n_clusters:
remainder_idx: T.let = linear_idx - group_rows * self._n_clusters
self.m_idx = (
group_rows
+ remainder_idx % final_rows
+ (my_rank + 1) * self._m_clusters // self._world_size
) % self._m_clusters
self.n_idx = remainder_idx // final_rows
else:
remainder_idx: T.let = linear_idx - remote_m_clusters * self._n_clusters
self.m_idx = (
remote_m_clusters
+ remainder_idx % (self._m_clusters // self._world_size)
+ (my_rank + 1) * self._m_clusters // self._world_size
) % self._m_clusters
self.n_idx = remainder_idx // (self._m_clusters // self._world_size)
@T.inline
def next_tile(self, stride: int):
self.linear_idx = self.linear_idx + stride
self.update_current_m_n_idx(self.linear_idx)
def valid(self):
return self.linear_idx < self._m_clusters * self._n_clusters
class IndexedTripleTileScheduler(BaseTileScheduler):
"""Scheduler that maps linear_idx to (b_idx, h_idx, q_idx) via index lists."""
def __init__(self, prefix: str, b_indices, h_indices, q_indices, tiles_indptr):
super().__init__(prefix)
self.b_indices = b_indices
self.h_indices = h_indices
self.q_indices = q_indices
self.tiles_indptr = tiles_indptr
self.q_idx = T.local_scalar("int32")
self.h_idx = T.local_scalar("int32")
self.b_idx = T.local_scalar("int32")
self.linear_lim = T.local_scalar("int32")
@T.inline
def _load(self):
self.q_idx = self.q_indices[self.linear_idx]
self.h_idx = self.h_indices[self.linear_idx]
self.b_idx = self.b_indices[self.linear_idx]
@T.inline
def init(self, sm):
self.linear_idx = self.tiles_indptr[sm]
self.linear_lim = self.tiles_indptr[sm + 1]
self._load()
@T.inline
def next_tile(self):
self.linear_idx = self.linear_idx + 1
self._load()
def valid(self):
return self.linear_idx < self.linear_lim
class FlashAttentionLinearScheduler(BaseTileScheduler):
"""Linear 3D scheduler for flash attention (batch, head, m_block).
Used for non-causal attention with simple linear decomposition.
Maps linear_idx -> (batch_idx, head_idx, m_block_idx) using:
batch = linear_idx // (num_heads * num_m_blocks)
head = (linear_idx % (num_heads * num_m_blocks)) // num_m_blocks
m_block = linear_idx % num_m_blocks
Parameters
----------
prefix : str
Prefix for TIR variable names
num_batches : int
Number of batches
num_heads : int
Number of KV heads
num_m_blocks : int
Number of Q blocks (M dimension tiles)
num_ctas : int
Number of CTAs for persistent kernel stride
"""
def __init__(
self, prefix: str, num_batches: int, num_heads: int, num_m_blocks: int, num_ctas: int
):
super().__init__(prefix)
self._num_batches = num_batches
self._num_heads = num_heads
self._num_m_blocks = num_m_blocks
self._num_ctas = num_ctas
self._total_tasks = num_batches * num_heads * num_m_blocks
# Output indices
self.batch_idx = T.local_scalar("int32")
self.head_idx = T.local_scalar("int32")
self.m_block_idx = T.local_scalar("int32")
# fmt: off
@T.inline
def update_current_m_n_idx(self, linear_idx):
"""Convert linear index to (batch, head, m_block) coordinates."""
NUM_HEADS = T.meta_var(self._num_heads)
NUM_M_BLOCKS = T.meta_var(self._num_m_blocks)
HEAD_M_PRODUCT = T.meta_var(NUM_HEADS * NUM_M_BLOCKS)
self.batch_idx = linear_idx // HEAD_M_PRODUCT
self.head_idx = (linear_idx % HEAD_M_PRODUCT) // NUM_M_BLOCKS
self.m_block_idx = linear_idx % NUM_M_BLOCKS
@T.inline
def init(self, cta_id):
"""Initialize scheduler with CTA ID."""
self.linear_idx = cta_id
self.update_current_m_n_idx(cta_id)
@T.inline
def next_tile(self):
"""Advance to next tile by striding by num_ctas."""
self.linear_idx = self.linear_idx + self._num_ctas
self.update_current_m_n_idx(self.linear_idx)
# fmt: on
def valid(self):
"""Check if there are more tiles to process."""
return self.linear_idx < self._total_tasks
class FlashAttentionLPTScheduler(BaseTileScheduler):
"""LPT scheduler with L2 swizzle for causal flash attention.
Processes high-work Q blocks (with more KV blocks to attend to) first using
Longest Processing Time (LPT) scheduling. Also applies L2 cache swizzle
for better cache locality across batch*head dimensions.
The LPT aspect comes from reversing m_block order: lower Q blocks have more
KV blocks to process due to causal masking, so processing them first balances load.
The scheduler is only applied to non-persistent kernels.
L2 Swizzle: Groups consecutive batch*head indices together for L2 locality.
Parameters
----------
prefix : str
Prefix for TIR variable names
num_batches : int
Number of batches
num_heads : int
Number of KV heads
num_m_blocks : int
Number of Q blocks (M dimension tiles)
num_ctas : int
Number of CTAs (should equal total_tasks for causal)
l2_swizzle : int
L2 swizzle factor for cache locality
"""
def __init__(
self,
prefix: str,
num_batches: int,
num_heads: int,
num_m_blocks: int,
l2_swizzle: int,
num_ctas: int | None = None,
):
super().__init__(prefix)
self._num_batches = num_batches
self._num_heads = num_heads
self._num_m_blocks = num_m_blocks
self._l2_swizzle = l2_swizzle
self._num_ctas = num_ctas
self._total_tasks = num_batches * num_heads * num_m_blocks
# Derived constants for L2 swizzle
self._num_hb = num_batches * num_heads
self._l2_major = l2_swizzle * num_m_blocks
self._num_hb_quotient = self._num_hb // l2_swizzle
# Output indices
self.batch_idx = T.local_scalar("int32")
self.head_idx = T.local_scalar("int32")
self.m_block_idx = T.local_scalar("int32")
# fmt: off
@T.inline
def update_current_m_n_idx(self, linear_idx):
"""Convert linear index to (batch, head, m_block) with LPT + L2 swizzle."""
L2_SWIZZLE = T.meta_var(self._l2_swizzle)
L2_MAJOR = T.meta_var(self._l2_major)
NUM_HB_QUOTIENT = T.meta_var(self._num_hb_quotient)
NUM_HB = T.meta_var(self._num_hb)
NUM_HEADS = T.meta_var(self._num_heads)
NUM_M_BLOCKS = T.meta_var(self._num_m_blocks)
# L2 swizzle decomposition
bidhb: T.let = linear_idx // L2_MAJOR
l2_mod: T.let = linear_idx % L2_MAJOR
# Handle residual section (last partial swizzle group)
num_hb_remainder: T.let = T.max(NUM_HB % L2_SWIZZLE, 1)
m_block_raw: T.let = T.Select(bidhb < NUM_HB_QUOTIENT, l2_mod // L2_SWIZZLE, l2_mod // num_hb_remainder) # noqa: E501
bidhb_residual: T.let = T.Select(bidhb < NUM_HB_QUOTIENT, l2_mod % L2_SWIZZLE, l2_mod % num_hb_remainder) # noqa: E501
bidhb_actual: T.let = bidhb * L2_SWIZZLE + bidhb_residual
self.batch_idx = bidhb_actual // NUM_HEADS
self.head_idx = bidhb_actual % NUM_HEADS
# LPT: Reverse block order so high-work blocks are processed first
self.m_block_idx = (NUM_M_BLOCKS - 1) - m_block_raw
@T.inline
def init(self, cta_id):
"""Initialize scheduler with CTA ID."""
self.linear_idx = cta_id
self.update_current_m_n_idx(cta_id)
@T.inline
def next_tile(self):
"""Advance to the next tile.
Single-tile mode (``num_ctas=None``, the default): each CTA owns one
task; terminate. Persistent mode (``num_ctas=N``): stride by N, like
:class:`FlashAttentionLinearScheduler`, while keeping the LPT + L2
swizzle index mapping.
"""
if self._num_ctas is None:
self.linear_idx = self._total_tasks
else:
self.linear_idx = self.linear_idx + self._num_ctas
self.update_current_m_n_idx(self.linear_idx)
# fmt: on
def valid(self):
"""Check if there are more tiles to process."""
return self.linear_idx < self._total_tasks
class _CLCWorker(ClusterPersistentScheduler2D):
"""Per-role CLC handle: IS-A ClusterPersistentScheduler2D (so m_idx / n_idx work as
usual) plus the role-local barrier phase and handshake. A coord-free role (e.g. an
MMA warp consuming whatever a loader staged) arms the loop with reset() not init().
"""
def __init__(self, clc, prefix):
super().__init__(
prefix,
num_m_tiles=clc._num_m_tiles,
num_n_tiles=clc._num_n_tiles,
num_clusters=clc._num_m_tiles * clc._num_n_tiles,
l2_group_size=clc._l2_group_size,
)
self._clc = clc
self._sa = PipelineState(1, 0)
self._done = T.local_scalar("int32")
self._nxt = T.local_scalar("uint32")
@T.inline
def reset(self):
self._done = 0
@T.inline
def init(self, cluster_id):
# Explicit base call: TVMScript's parser has no zero-arg super().
ClusterPersistentScheduler2D.init(self, cluster_id)
self._done = 0
def valid(self):
return self._done == 0
@T.inline
def consume(self):
# Single-elected-thread scope: wait for the handle, decode, release the slot.
self._clc.sched_arr.full.wait(0, self._sa.phase)
self._sa.advance()
self._nxt = T.ptx.clc_query_cancel(T.address_of(self._clc.clc_handle[0]))
self._clc.sched_fin.empty.arrive(0, cta_id=0, pred=True)
@T.inline
def consume_wg(self, wg_id, warp_id, lane_id):
# Warpgroup scope: all threads decode; one elected lane releases the slot.
self._clc.sched_arr.full.wait(0, self._sa.phase)
self._sa.advance()
self._nxt = T.ptx.clc_query_cancel(T.address_of(self._clc.clc_handle[0]))
T.cuda.warpgroup_sync(wg_id + 1)
if (warp_id == 0) & (lane_id == 0):
self._clc.sched_fin.empty.arrive(0, cta_id=0, pred=True)
@T.inline
def advance_coords(self):
if self._nxt != 0xFFFFFFFF:
self.update_current_m_n_idx(self._nxt // self._clc._cta_group)
@T.inline
def mark_done_if_drained(self):
if self._nxt == 0xFFFFFFFF:
self._done = 1
@T.meta_class
class ClusterLaunchControlScheduler:
"""Blackwell Cluster Launch Control (CLC) tile scheduler.
A scheduler warp runs ``run_scheduler`` (issues ``try_cancel`` to steal the next
cluster); worker roles each take a ``worker()`` handle and pull the stolen tile
through the shared smem handshake. Owns the CLC smem: the 16B response handle, the
arrival barrier (handle ready), and the finished barrier (slot consumed;
``finish_arrivals`` arrivals per round). Tile-coord mapping is delegated to
``ClusterPersistentScheduler2D`` (group-major L2 ordering).
"""
def __init__(self, pool, num_m_tiles, num_n_tiles, l2_group_size, cta_group, finish_arrivals):
self._num_m_tiles = num_m_tiles
self._num_n_tiles = num_n_tiles
self._l2_group_size = l2_group_size
self._cta_group = cta_group
self.sched_arr = Pipeline(pool, 1, full="tma", empty="mbar", init_empty=1)
self.sched_fin = Pipeline(pool, 1, full="mbar", empty="mbar", init_empty=finish_arrivals)
self.clc_handle = pool.alloc((4,), "uint32", align=16)
self._s_done = T.local_scalar("int32")
self._s_nxt = T.local_scalar("uint32")
def worker(self, prefix):
return _CLCWorker(self, prefix)
@T.inline
def run_scheduler(self, cbx):
# cta0 drives try_cancel; both CTAs expect_bytes + consume the handle so the
# finished-barrier count is met and the slot can be reissued.
if T.ptx.elect_sync():
sa = PipelineState(1, 0)
sf = PipelineState(1, 1)
self._s_done = 0
while self._s_done == 0:
if cbx == 0:
self.sched_fin.empty.wait(0, sf.phase)
sf.advance()
T.ptx.clc_try_cancel(
T.address_of(self.clc_handle[0]), T.address_of(self.sched_arr.full.buf[0])
)
self.sched_arr.full.arrive(0, 16) # expect_bytes for the 16B handle
self.sched_arr.full.wait(0, sa.phase)
sa.advance()
self._s_nxt = T.ptx.clc_query_cancel(T.address_of(self.clc_handle[0]))
self.sched_fin.empty.arrive(0, cta_id=0, pred=True)
if self._s_nxt == 0xFFFFFFFF:
self._s_done = 1
+144
View File
@@ -0,0 +1,144 @@
# 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.
"""Warp role helpers for SM100 kernels.
Simplifies the common pattern of dispatching warps to named roles
with register budgets.
Example::
# Declare roles
tma_warp = WarpRole(warp_id, 1, regs=48)
store_warp = WarpRole(warp_id, 2, regs=48)
mma_warp = WarpRole(warp_id, 0, regs=232, increase=True)
# Use with context manager
with tma_warp:
# TMA load code
with store_warp:
# TMA store code
with mma_warp:
# MMA compute code
"""
from tvm.script import tirx as T
class WarpRole:
"""A warp-level role that guards a block of code by warp_id comparison
with optional register budget.
Generates::
if <warp_id_var> == <warp_id_val>:
T.ptx.setmaxnreg(<increase>, <regs>) # if regs specified
<user code>
The ``if`` guard narrows the active set to the single warp; individual
tile-primitive calls inside ``<user code>`` carry their own exec scope via
a scope-namespace prefix (e.g. ``Tx.warp.copy(...)``).
Parameters
----------
warp_id_var : Var
The warp_id variable (from ``T.warp_id(...)``).
warp_id_val : int
Which warp index this role corresponds to.
regs : int, optional
Register budget (passed to ``T.ptx.setmaxnreg``).
If None, no setmaxnreg is emitted.
increase : bool
Direction for ``setmaxnreg`` (default False = decrease).
"""
def __init__(self, warp_id_var, warp_id_val, regs=None, increase=False):
self.warp_id_var = warp_id_var
self.warp_id_val = warp_id_val
self.regs = regs
self.increase = increase
def __enter__(self):
self._if_frame = T.If(self.warp_id_var == self.warp_id_val)
self._if_frame.__enter__()
self._then_frame = T.Then()
self._then_frame.__enter__()
if self.regs is not None:
T.evaluate(T.ptx.setmaxnreg(self.increase, self.regs))
return self
def __exit__(self, *exc):
self._then_frame.__exit__(*exc)
self._if_frame.__exit__(*exc)
return False
class WarpgroupRole:
"""A warpgroup-level role that guards by wg_id comparison,
with optional register budget.
Generates (single wg_id)::
if <wg_id_var> == <wg_id_val>:
T.ptx.setmaxnreg(<increase>, <regs>) # if regs specified
<user code>
Generates (range of wg_ids, e.g. ``wg_id_val=(0, 2)``)::
if 0 <= <wg_id_var> and <wg_id_var> < 2:
T.ptx.setmaxnreg(<increase>, <regs>)
<user code>
The ``if`` guard narrows the active set to the target warpgroup(s);
individual tile-primitive calls inside ``<user code>`` carry their own exec
scope via a scope-namespace prefix (e.g. ``Tx.wg.copy(...)``).
Parameters
----------
wg_id_var : Var
The warpgroup_id variable (from ``T.warpgroup_id(...)``).
wg_id_val : int or tuple[int, int]
Which warpgroup index (int) or range ``(start, stop)`` this role
corresponds to.
regs : int, optional
Register budget.
increase : bool
Direction for ``setmaxnreg`` (default False = decrease).
"""
def __init__(self, wg_id_var, wg_id_val, regs=None, increase=False):
self.wg_id_var = wg_id_var
self.wg_id_val = wg_id_val
self.regs = regs
self.increase = increase
def __enter__(self):
if isinstance(self.wg_id_val, tuple):
start, stop = self.wg_id_val
self._if_frame = T.If(start <= self.wg_id_var and self.wg_id_var < stop)
else:
self._if_frame = T.If(self.wg_id_var == self.wg_id_val)
self._if_frame.__enter__()
self._then_frame = T.Then()
self._then_frame.__enter__()
if self.regs is not None:
T.evaluate(T.ptx.setmaxnreg(self.increase, self.regs))
return self
def __exit__(self, *exc):
self._then_frame.__exit__(*exc)
self._if_frame.__exit__(*exc)
return False
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,19 @@
# 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.
"""CUDA backend operator registrations and helpers."""
__all__ = ["intrinsics", "tile_primitive"]
@@ -0,0 +1,49 @@
# 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.
# pylint: disable=unused-import
"""CUDA HW intrinsic codegens, grouped by feature domain.
- ``mma`` / ``wgmma`` / ``tcgen05`` — matrix-multiply hardware (Volta+/Hopper/Blackwell).
- ``cp_async`` — cp.async + cp.async.bulk + cp.async.bulk.tensor (TMA), incl. TMA address helpers.
- ``sync`` — barriers, fences, mbarrier, cluster.barrier, warp vote, elect, sync helpers.
- ``math`` — packed-f32x2 arithmetic, exp2/rcp/reduce3, warp/CTA reductions.
- ``memory`` — typed copies, ldg, ld.global.acquire, atomics, type conversions, address casts.
- ``nvshmem`` — NVSHMEM RMA / signal / collective.
- ``misc`` — register-allocation control, profiler timer, debug helpers (printf / trap).
Plus the support modules:
- ``header`` — CUDA header generator and helper-tag table.
- ``registry`` — codegen registry.
- ``types`` — PTX dtype enum.
- ``utils`` — small parsing / validation helpers.
"""
# Import op modules to register their codegen functions.
from . import cp_async, math, memory, misc, mma, nvshmem, sync, tcgen05, wgmma
from .header import TAGS, header_generator
from .registry import CODEGEN_REGISTRY, get_codegen, register_codegen
from .types import PTXDataType
__all__ = [
"CODEGEN_REGISTRY",
"TAGS",
"PTXDataType",
"get_codegen",
"header_generator",
"register_codegen",
]
@@ -0,0 +1,144 @@
# 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.
# pylint: disable=invalid-name
"""Thin device-helper registration for TIRx intrinsic codegens.
Exposes one entry point: :func:`device_intrinsic`. Given an op name plus
``(helper_name, c_signature, body)`` (each a string or a ``(*args) -> str``
callable), it:
* wraps the body in
``__forceinline__ __device__ <ret> <name><sig> { <body> }``,
* registers a codegen function under the op name so
``call_intrin("", "tirx.<op_name>", *args)`` resolves to a call to that
helper. TVM Op registration is static C++ only.
Args passed to the codegen are split into ``(forward_args, attr_args)``:
the trailing ``n_attrs`` are attrs (consumed by the ``helper_name`` /
``c_signature`` / ``body`` callables but **not** forwarded to the helper),
and the rest are operand args (forwarded). The default ``n_attrs=0`` means
every arg is forwarded — appropriate for fixed-arity ops with literal
``c_signature`` and ``helper_name``.
Coerce / validate attrs explicitly inside the callables — there is no
``Choice`` / ``Bool`` / ``IntAttr`` machinery; just call ``parse_str`` /
``int`` / ``bool`` on the raw arg as needed.
"""
from __future__ import annotations
from collections.abc import Callable
from tvm.backend.cuda.op import cuda_func_call
from tvm.backend.cuda.operator.intrinsics.registry import register_codegen
# C primitive type → TVM dtype string. Used when the caller specifies a
# non-void ``return_type`` but no explicit ``tvm_return_type`` — the helper
# knows the TVM-side dtype from the C return type.
_C_TO_TVM_DTYPE = {
"float": "float32",
"double": "float64",
"uint32_t": "uint32",
"int32_t": "int32",
"uint64_t": "uint64",
"int64_t": "int64",
"uint16_t": "uint16",
"int16_t": "int16",
"unsigned long long": "uint64",
"long long": "int64",
"unsigned short": "uint16",
"bool": "bool",
"unsigned int": "uint32",
"int": "int32",
}
def device_intrinsic(
op_name: str,
*,
helper_name: str | Callable | None = None,
c_signature: str | Callable = "()",
body: str | Callable,
n_attrs: int = 0,
return_type: str | Callable = "void",
tvm_return_type: str | Callable | None = None,
templated: bool = False,
extra_deps: tuple = (),
) -> None:
"""Register a CUDA device-helper intrinsic.
Parameters
----------
op_name :
Registry key — ``call_intrin("", "tirx.<op_name>", ...)`` resolves
here. Also used as the default helper name (``tvm_builtin_<op_name>``)
when ``helper_name`` is not provided.
helper_name :
Literal C function name, OR ``(*args) -> str`` to compute it from
attr values. Defaults to ``f"tvm_builtin_{op_name}"``.
c_signature :
Literal C parameter list including outer parens (``"(int x, int y)"``),
OR ``(*args) -> str`` to compute it from attr values. Defaults to
``"()"``.
body :
Literal C body string (already indented), OR ``(*args) -> str``.
n_attrs :
Number of trailing args that are attrs (consumed by ``helper_name``
/ ``c_signature`` / ``body`` callables, NOT forwarded to the helper
as call arguments). The first ``len(args) - n_attrs`` args are the
operand args forwarded to the helper.
return_type :
C return type. Default ``"void"``. Either a literal string or
``(*args) -> str`` when the helper return type depends on attrs.
tvm_return_type :
TVM dtype for the call result, when the helper has a non-void
return. Either a literal string (``"int32"``) or ``(*args) -> str``.
If omitted and ``return_type`` is non-void, it is auto-derived from
the ``_C_TO_TVM_DTYPE`` table.
templated :
Prefix the helper with ``template <typename T>``.
extra_deps :
Helper-tag list (e.g. ``("get_tmem_addr",)``) forwarded as the second
element of the codegen result so the header generator emits the
prerequisite snippets.
"""
if helper_name is None:
helper_name = f"tvm_builtin_{op_name}"
extra_deps = tuple(extra_deps)
def codegen(*args):
forward = args if n_attrs == 0 else args[:-n_attrs]
name = helper_name(*args) if callable(helper_name) else helper_name
sig = c_signature(*args) if callable(c_signature) else c_signature
body_str = body(*args) if callable(body) else body
ret_type = return_type(*args) if callable(return_type) else return_type
prefix = "template <typename T>\n" if templated else ""
source_code = (
f"\n{prefix}__forceinline__ __device__ {ret_type} {name}{sig} {{\n{body_str}\n}}\n"
)
kwargs = {"source_code": source_code}
if tvm_return_type is not None:
kwargs["return_type"] = (
tvm_return_type(*args) if callable(tvm_return_type) else tvm_return_type
)
elif ret_type != "void":
kwargs["return_type"] = _C_TO_TVM_DTYPE.get(ret_type, ret_type)
result = cuda_func_call(name, *forward, **kwargs)
return (result, list(extra_deps)) if extra_deps else result
codegen.__name__ = f"codegen_{op_name}"
register_codegen(op_name)(codegen)
@@ -0,0 +1,924 @@
# 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.
# pylint: disable=redefined-builtin, invalid-name, too-many-arguments, too-many-locals, too-many-positional-arguments
"""PTX cp.async / cp.async.bulk / cp.async.bulk.tensor intrinsics.
Each PTX form table entry is registered as one ``device_intrinsic``.
User-facing wrappers in ``tvm.tirx.op`` keep their v1 signatures;
``register_codegen`` dispatchers below decode the (cp_size, fill_mode,
predicate) / (dim, cta_mask, tile_mode) arguments to pick the right form.
Bodies are hand-written ``asm volatile(...)`` strings. The file is grouped
as cp.async, cp.async.bulk.tensor, cp.async.bulk non-TMA, and CUDA
compatibility helpers.
"""
import tvm
from tvm.backend.cuda.op import cuda_func_call
from ._schema import device_intrinsic
from .registry import CODEGEN_REGISTRY, register_codegen
from .utils import parse_str
_PREFETCH_CHOICES = ("", "64", "128", "256")
_DIM_CHOICES = (1, 2, 3, 4, 5)
_TILE_MODE_CHOICES = ("tile", "tile_gather4")
def _safe(s):
return s.replace("::", "_").replace(".", "_")
# =============================================================================
# cp.async forms from the PTX Syntax block.
#
# Includes commit/wait plus the non-bulk shared/global copy forms.
# =============================================================================
device_intrinsic(
"ptx_cp_async_commit_group",
helper_name="tvm_builtin_ptx_cp_async_commit_group",
body=' asm volatile("cp.async.commit_group;");',
)
device_intrinsic(
"ptx_cp_async_wait_group",
n_attrs=1,
helper_name=lambda n: f"tvm_builtin_ptx_cp_async_wait_group_{int(n)}",
body=lambda n: f' asm volatile("cp.async.wait_group {int(n)};");',
)
# cp.async non-bulk copy forms:
# Form 1: cp.async.ca.shared.global ... [dst], [src], cp-size{, src-size}{, cache-policy}
# Form 2: cp.async.cg.shared.global ... [dst], [src], 16{, src-size}{, cache-policy}
# Form 3: cp.async.ca.shared.global ... [dst], [src], cp-size{, ignore-src}{, cache-policy}
# Form 4: cp.async.cg.shared.global ... [dst], [src], 16{, ignore-src}{, cache-policy}
def _cp_async_modifier_str(has_cache_hint, prefetch_size):
s = ""
if has_cache_hint:
s += ".L2::cache_hint"
if prefetch_size:
s += f".L2::{prefetch_size}B"
return s
def _make_form_parts(ca_or_cg, fixed_cp_size, extra):
"""Build a parts callable for one of the cp.async PTX forms.
Args layout: (dst, src [, extra_int], cache_policy, has_cache, prefetch_size [, cp_size_attr])
Forwarded operands: dst, src [, extra_int], cache_policy.
Trailing attrs: has_cache, prefetch_size [, cp_size if .ca].
"""
n_op = 3 if extra is not None else 2
n_attrs = 2 if fixed_cp_size is not None else 3
extra_in_name = f"_with_{extra}" if extra is not None else ""
def _parts(*args):
# Operand args (forwarded) come first, then attr args.
attr_args = args[-n_attrs:]
has_cache = _bool_attr(attr_args[0])
prefetch_size = parse_str(attr_args[1])
cp_size = fixed_cp_size if fixed_cp_size is not None else int(attr_args[2])
modifier = _cp_async_modifier_str(has_cache, prefetch_size)
cache_operand = ', "l"(cache_policy)' if has_cache else ""
# name parts
name_cache = "_cache_hint" if has_cache else ""
name_prefetch = f"_prefetch_{prefetch_size}" if prefetch_size else ""
name = (
f"tvm_builtin_ptx_cp_async_{ca_or_cg}_{cp_size}"
f"{name_cache}{name_prefetch}{extra_in_name}"
)
sig = (
"(void* dst, void* src"
+ (f", int {extra}" if extra else "")
+ ", unsigned long long cache_policy)"
)
instr_base = f"cp.async.{ca_or_cg}.shared.global{modifier}"
if extra is None:
cache_arg = ", %2" if has_cache else ""
body = (
" unsigned int dst_addr = __cvta_generic_to_shared(dst);\n"
f' asm volatile("{instr_base} [%0], [%1], {cp_size}{cache_arg};\\n"\n'
f' :: "r"(dst_addr), "l"(src){cache_operand} : "memory");'
)
else:
cache_arg = ", %3" if has_cache else ""
body = (
" unsigned int dst_addr = __cvta_generic_to_shared(dst);\n"
f' asm volatile("{instr_base} [%0], [%1], {cp_size}, %2{cache_arg};\\n"\n'
f' :: "r"(dst_addr), "l"(src), "r"({extra})'
f'{cache_operand} : "memory");'
)
return name, sig, body
return _parts, n_op + n_attrs - n_op # n_attrs
def _register_nb_form(op_name, ca_or_cg, fixed_cp_size, extra):
parts_fn, n_attrs = _make_form_parts(ca_or_cg, fixed_cp_size, extra)
n_op = 3 if extra is not None else 2
sig_static = (
"(void* dst, void* src"
+ (f", int {extra}" if extra else "")
+ ", unsigned long long cache_policy)"
)
device_intrinsic(
f"ptx_cp_async_{op_name}",
n_attrs=n_attrs,
c_signature=sig_static, # static — depends on `extra` not on attrs
helper_name=lambda *a, fn=parts_fn: fn(*a)[0],
body=lambda *a, fn=parts_fn: fn(*a)[2],
)
return n_op
# Form 1: .ca + src-size (cp-size ∈ {4, 8}). src-size is required when present.
_register_nb_form("ca_src_size", "ca", fixed_cp_size=None, extra="src_size")
# Form 2: .cg + src-size (cp-size = 16).
_register_nb_form("cg_src_size", "cg", fixed_cp_size=16, extra="src_size")
# Form 3: .ca + ignore-src.
_register_nb_form("ca_ignore_src", "ca", fixed_cp_size=None, extra="ignore_src")
# Form 4: .cg + ignore-src.
_register_nb_form("cg_ignore_src", "cg", fixed_cp_size=16, extra="ignore_src")
# Plain degenerate of forms 1+2 with optional src-size omitted.
_register_nb_form("ca", "ca", fixed_cp_size=None, extra=None)
_register_nb_form("cg", "cg", fixed_cp_size=16, extra=None)
def _make_setp_at_p_helper(ca_or_cg, cp_size, has_cache, prefetch):
"""Wrapper convenience: ``setp+@p`` around a form 1/2 cp.async (predicate-
gated skip with dst untouched on false). Not a PTX form — emitted directly
here as a one-off helper rather than a separate device_intrinsic."""
modifier = _cp_async_modifier_str(has_cache, prefetch)
cache_arg = ", %4" if has_cache else ""
cache_operand = ', "l"(cache_policy)' if has_cache else ""
func_name = (
f"tvm_builtin_ptx_cp_async_{cp_size}"
+ ("_cache_hint" if has_cache else "")
+ (f"_prefetch_{prefetch}" if prefetch else "")
+ "_predicate"
)
body = (
" unsigned int dst_addr = __cvta_generic_to_shared(dst);\n"
" __asm__ __volatile__(\n"
' "{\\n"\n'
' " .reg .pred p;\\n"\n'
' " setp.eq.u32 p, %3, 1;\\n"\n'
f' " @p cp.async.{ca_or_cg}.shared.global{modifier}'
f' [%0], [%1], %2{cache_arg};\\n"\n'
' "}\\n"\n'
f' :: "r"(dst_addr), "l"(src), "n"({cp_size}), "r"(predicate){cache_operand}\n'
" );"
)
source_code = (
f"\n__forceinline__ __device__ void {func_name}"
"(void* dst, void* src, int predicate, unsigned long long cache_policy) {\n"
f"{body}\n"
"}\n"
)
return func_name, source_code
@register_codegen("ptx_cp_async")
def codegen_ptx_cp_async(*args):
"""Map the wrapper API to the 4 PTX form table entries.
Accepts three call shapes (sorted by arity):
* 5 args ``(dst_ptr, dst_offset, src_ptr, src_offset, cp_size)`` —
the legacy form emitted by
``tvm.backend.cuda.transform.InjectPTXAsyncCopy``.
Offsets are folded into the pointers via ``tvm_access_ptr`` (in
bytes; offsets are pre-scaled by the pass) and the call is
forwarded with default cache / predicate / fill_mode.
* 6 args ``(dst_ptr, dst_offset, src_ptr, src_offset, cp_size,
predicate)`` — same as 5-arg form with an explicit predicate,
zero-filling the destination when the predicate is false.
* 8 args ``(dst_ptr, src_ptr, cp_size, cache_policy, has_cache_hint,
prefetch_size, predicate, fill_mode)`` — the fork-native wrapper
API.
The three resulting form_kinds:
* ``fill_mode == "zero"`` -> form 1/2 (src-size = predicate ? cp_size : 0)
* ``predicate != -1`` and no fill_mode -> form 1/2 wrapped in setp+@p
(wrapper convenience; not a PTX form)
* else -> form 1/2 with src-size omitted (the "plain" degenerate)
"""
from tvm.tirx.op import if_then_else
if len(args) in (5, 6):
# Legacy InjectPTXAsyncCopy emission: (dst_ptr, dst_off, src_ptr,
# src_off, cp_size [, predicate]). Offsets are element indices into
# the typed buffers (the pass uses index_factor=1 except for the
# shared.dyn-merged byte-buffer path). Emit a C helper that scales
# the offset by the buffer element size, then runs cp.async.
#
# PTX plain form for both .ca and .cg is just
# ``cp.async.<v>.shared.global [dst], [src], cp_size;`` — three
# operands, no trailing src-size / cache-policy.
from tvm import DataType
dst_ptr_in, dst_offset, src_ptr_in, src_offset, cp_size = args[:5]
predicate = args[5] if len(args) == 6 else -1
cp_size_v = int(cp_size)
ca_or_cg = "cg" if cp_size_v == 16 else "ca"
# Recover the per-side element dtype from each pointer's type
# type (Var has ty = PointerType(PrimType(dtype))).
# InjectPTXAsyncCopy emits offsets in element-units of each side's
# buffer dtype (dst gets dst_offset * src_elem_size only when dst is a
# merged shared.dyn byte buffer, in which case dst_elem_dtype is uint8
# and the resulting scale-by-1 is a no-op).
def _elem_bytes(ptr):
ta = getattr(ptr, "ty", None)
if ta is None or getattr(ta, "element_type", None) is None:
return 1
et = ta.element_type
if not hasattr(et, "dtype"):
return 1
bits = DataType(str(et.dtype)).bits
assert bits % 8 == 0, f"non-byte element dtype: {et.dtype}"
return bits // 8
dst_elem_bytes = _elem_bytes(dst_ptr_in)
src_elem_bytes = _elem_bytes(src_ptr_in)
has_predicate = not (
(isinstance(predicate, int) and predicate == -1)
or (hasattr(predicate, "value") and int(predicate.value) == -1)
)
def _scale(n):
return "" if n == 1 else f" * {n}"
dst_scale = _scale(dst_elem_bytes)
src_scale = _scale(src_elem_bytes)
if has_predicate:
func_name = (
f"ptx_cp_async_legacy_pred_{ca_or_cg}_{cp_size_v}_{dst_elem_bytes}_{src_elem_bytes}"
)
if cp_size_v == 4:
zero_fill = ' " @!p st.shared.u32 [%0], {%4};\\n"\n'
elif cp_size_v == 8:
zero_fill = ' " @!p st.shared.v2.u32 [%0], {%4, %4};\\n"\n'
elif cp_size_v == 16:
zero_fill = ' " @!p st.shared.v4.u32 [%0], {%4, %4, %4, %4};\\n"\n'
else:
raise ValueError(f"unsupported legacy predicated cp.async size: {cp_size_v}")
body = (
f" uint8_t* dst_p = (uint8_t*)dst + dst_off{dst_scale};\n"
f" uint8_t* src_p = (uint8_t*)src + src_off{src_scale};\n"
" unsigned int dst_addr = __cvta_generic_to_shared(dst_p);\n"
" __asm__ __volatile__(\n"
' "{\\n"\n'
' " .reg .pred p;\\n"\n'
' " setp.eq.u32 p, %3, 1;\\n"\n'
f' " @p cp.async.{ca_or_cg}.shared.global'
' [%0], [%1], %2;\\n"\n'
f"{zero_fill}"
' "}\\n"\n'
f' :: "r"(dst_addr), "l"(src_p), "n"({cp_size_v}), "r"(predicate), "r"(0)\n'
" );"
)
source_code = (
f"\n__forceinline__ __device__ void {func_name}"
"(void* dst, int dst_off, void* src, int src_off, int predicate) {\n"
f"{body}\n"
"}\n"
)
return cuda_func_call(
func_name,
dst_ptr_in,
dst_offset,
src_ptr_in,
src_offset,
predicate,
source_code=source_code,
)
# No predicate — plain cp.async.
func_name = f"ptx_cp_async_legacy_{ca_or_cg}_{cp_size_v}_{dst_elem_bytes}_{src_elem_bytes}"
body = (
f" uint8_t* dst_p = (uint8_t*)dst + dst_off{dst_scale};\n"
f" uint8_t* src_p = (uint8_t*)src + src_off{src_scale};\n"
" unsigned int dst_addr = __cvta_generic_to_shared(dst_p);\n"
f' asm volatile("cp.async.{ca_or_cg}.shared.global'
' [%0], [%1], %2;"\n'
f' :: "r"(dst_addr), "l"(src_p), "n"({cp_size_v}));'
)
source_code = (
f"\n__forceinline__ __device__ void {func_name}"
"(void* dst, int dst_off, void* src, int src_off) {\n"
f"{body}\n"
"}\n"
)
return cuda_func_call(
func_name,
dst_ptr_in,
dst_offset,
src_ptr_in,
src_offset,
source_code=source_code,
)
elif len(args) == 8:
(
dst_ptr,
src_ptr,
cp_size,
cache_policy,
has_cache_hint,
prefetch_size,
predicate,
fill_mode,
) = args
else:
raise ValueError(f"ptx_cp_async codegen expects 5/6/8 args, got {len(args)}")
cp_size_v = int(cp_size)
ca_or_cg = "cg" if cp_size_v == 16 else "ca"
pref = "" if int(prefetch_size) == -1 else str(int(prefetch_size))
fill = parse_str(fill_mode)
has_cache = _bool_attr(has_cache_hint)
has_predicate = not (
(isinstance(predicate, int) and predicate == -1)
or (hasattr(predicate, "value") and int(predicate.value) == -1)
)
if fill == "zero":
src_size = if_then_else(predicate != 0, cp_size_v, 0)
op = f"tirx.ptx_cp_async_{ca_or_cg}_src_size"
if cp_size_v == 16:
args = [dst_ptr, src_ptr, src_size, cache_policy, has_cache, pref]
else:
args = [dst_ptr, src_ptr, src_size, cache_policy, has_cache, pref, cp_size_v]
result = CODEGEN_REGISTRY[op](args)
return result[0] if isinstance(result, tuple) else result
if has_predicate:
func_name, source_code = _make_setp_at_p_helper(ca_or_cg, cp_size_v, has_cache, pref)
return cuda_func_call(
func_name, dst_ptr, src_ptr, predicate, cache_policy, source_code=source_code
)
# Plain — form 1/2 with src-size omitted.
op = f"tirx.ptx_cp_async_{ca_or_cg}"
if cp_size_v == 16:
args = [dst_ptr, src_ptr, cache_policy, has_cache, pref]
else:
args = [dst_ptr, src_ptr, cache_policy, has_cache, pref, cp_size_v]
result = CODEGEN_REGISTRY[op](args)
return result[0] if isinstance(result, tuple) else result
CODEGEN_REGISTRY["tirx.ptx.cp_async_raw"] = CODEGEN_REGISTRY["tirx.ptx.cp_async"]
# =============================================================================
# cp.async.bulk.tensor (TMA) — one device_intrinsic per arity variant of each
# PTX form. Per-dim coord operands materialise via the ``c_signature`` callable.
# =============================================================================
def _is_sm100_or_higher():
target = tvm.target.Target.current()
if target is None:
return False
arch = target.arch[3:]
if not arch[-1].isdigit():
arch = arch[:-1]
return int(arch) >= 100
def _resolve_cta_group_str(cta_group):
if cta_group == 2 or (cta_group != -1 and _is_sm100_or_higher()):
return f".cta_group::{cta_group}"
return ""
def _coord_template(coord_count, start_slot):
inner = ", ".join(f"%{start_slot + i}" for i in range(coord_count))
return f"{{{inner}}}"
def _coord_constraints(coord_count):
return ", ".join(f'"r"(coord{i})' for i in range(coord_count))
def _coord_sig(n):
return ", ".join(f"int coord{i}" for i in range(n))
# PTX cp.async.bulk.tensor global -> shared::cluster form:
# cp.async.bulk.tensor.dim.dst.src{.load_mode}.completion_mechanism
# {.multicast}{.cta_group}{.level::cache_hint}
# [dstMem], [tensorMap, tensorCoords], [mbar]{, im2colInfo}
# {, ctaMask} {, cache-policy}
# .dst = {.shared::cluster}; .src = {.global}
# .completion_mechanism = {.mbarrier::complete_tx::bytes}
# .multicast = {.multicast::cluster}
# .cta_group = {.cta_group::1, .cta_group::2}
# .load_mode = {.tile, .tile::gather4, .im2col, .im2col::w, .im2col::w::128}
# .level::cache_hint = {.L2::cache_hint}
# This registration supports tile/tile::gather4 modes; ctaMask is only used
# when the optional ``.multicast::cluster`` modifier is enabled.
def _g2cluster_parts(*args):
attrs = args[-6:]
dim = int(attrs[0])
cta_group = int(attrs[1])
has_cache = _bool_attr(attrs[2])
tile_mode = parse_str(attrs[3])
bar_is_addr = _bool_attr(attrs[4])
multicast = _bool_attr(attrs[5])
coord_count = 5 if tile_mode == "tile_gather4" else dim
bar_type = "unsigned int bar_addr" if bar_is_addr else "void* bar"
sig = (
f"(void* dst, {bar_type}, unsigned long long tensormap_addr, "
"uint16_t cta_mask, unsigned long long cache_policy"
+ (", " + _coord_sig(coord_count) if coord_count else "")
+ ")"
)
name = (
f"ptx_cp_async_bulk_tensor_g2cluster_{tile_mode}_{dim}d"
f"{'_multicast' if multicast else ''}"
f"{'_cache_hint' if has_cache else ''}{'_bar_addr' if bar_is_addr else ''}"
)
tile_modifier = ".tile::gather4" if tile_mode == "tile_gather4" else ""
cta_group_str = _resolve_cta_group_str(cta_group)
multicast_inst = ".multicast::cluster" if multicast else ""
cache_inst = ".L2::cache_hint" if has_cache else ""
mask_arg = ',\n "h"(cta_mask)' if multicast else ""
cache_arg = ',\n "l"(cache_policy)' if has_cache else ""
mask_slot = ", %3" if multicast else ""
cache_slot = ", %4" if multicast and has_cache else ", %3" if has_cache else ""
coord_start = 5 if multicast and has_cache else 4 if multicast or has_cache else 3
coord_tpl = _coord_template(coord_count, coord_start)
instr = (
f"cp.async.bulk.tensor.{dim}d.shared::cluster.global{tile_modifier}"
f".mbarrier::complete_tx::bytes{multicast_inst}"
f"{cta_group_str}{cache_inst}"
)
bar_addr_decl = (
"" if bar_is_addr else " unsigned int bar_addr = __cvta_generic_to_shared(bar);\n"
)
body = (
" unsigned int dst_addr = __cvta_generic_to_shared(dst);\n"
f"{bar_addr_decl}"
" asm volatile(\n"
f' "{instr} [%0], [%1, {coord_tpl}], [%2]{mask_slot}{cache_slot};"\n'
" :\n"
f' : "r"(dst_addr), "l"(tensormap_addr), "r"(bar_addr){mask_arg}{cache_arg},\n'
f" {_coord_constraints(coord_count)}\n"
' : "memory"\n'
" );"
)
return name, sig, body
device_intrinsic(
"ptx_cp_async_bulk_tensor_g2cluster",
n_attrs=6,
helper_name=lambda *a: _g2cluster_parts(*a)[0],
c_signature=lambda *a: _g2cluster_parts(*a)[1],
body=lambda *a: _g2cluster_parts(*a)[2],
)
# PTX cp.async.bulk.tensor shared::cta -> global form:
# cp.async.bulk.tensor.dim.dst.src{.load_mode}.completion_mechanism
# {.level::cache_hint}
# [tensorMap, tensorCoords], [srcMem] {, cache-policy}
# .dst = {.global}; .src = {.shared::cta}
# .completion_mechanism = {.bulk_group}
# .load_mode = {.tile, .tile::scatter4, .im2col_no_offs}
# .level::cache_hint = {.L2::cache_hint}
# This registration supports tile mode; cache-policy is a real operand.
def _s2g_parts(*args):
attrs = args[-2:]
dim = int(attrs[0])
has_cache = _bool_attr(attrs[1])
sig = (
"(void* src, unsigned long long tensormap_addr, unsigned long long cache_policy"
+ (", " + _coord_sig(dim) if dim else "")
+ ")"
)
name = f"ptx_cp_async_bulk_tensor_shared_to_global_{dim}d{'_cache_hint' if has_cache else ''}"
cache_inst = ".L2::cache_hint" if has_cache else ""
cache_arg = ', "l"(cache_policy)' if has_cache else ""
cache_slot = ", %2" if has_cache else ""
coord_start = 3 if has_cache else 2
coord_tpl = _coord_template(dim, coord_start)
instr = f"cp.async.bulk.tensor.{dim}d.global.shared::cta.tile.bulk_group{cache_inst}"
body = (
" unsigned int src_addr = __cvta_generic_to_shared(src);\n"
" asm volatile(\n"
f' "{instr} [%0, {coord_tpl}], [%1]{cache_slot};"\n'
" :\n"
f' : "l"(tensormap_addr), "r"(src_addr){cache_arg},\n'
f" {_coord_constraints(dim)}\n"
' : "memory"\n'
" );"
)
return name, sig, body
device_intrinsic(
"ptx_cp_async_bulk_tensor_s2g",
n_attrs=2,
helper_name=lambda *a: _s2g_parts(*a)[0],
c_signature=lambda *a: _s2g_parts(*a)[1],
body=lambda *a: _s2g_parts(*a)[2],
)
# PTX cp.async.bulk.prefetch.tensor form:
# cp.async.bulk.prefetch.tensor.dim.L2.src{.load_mode}{.level::cache_hint}
# [tensorMap, tensorCoords] {, im2colInfo} {, cache-policy}
# .src = {.global}
# .load_mode = {.tile, .tile::gather4, .im2col, .im2col::w, .im2col::w::128}
# .level::cache_hint = {.L2::cache_hint}
# This registration supports tile mode; cache-policy is a real operand.
def _prefetch_parts(*args):
attrs = args[-2:]
dim = int(attrs[0])
has_cache = _bool_attr(attrs[1])
sig = (
"(unsigned long long tensormap_addr, unsigned long long cache_policy"
+ (", " + _coord_sig(dim) if dim else "")
+ ")"
)
name = (
f"ptx_cp_async_bulk_tensor_global_to_cluster_prefetch_{dim}d"
f"{'_cache_hint' if has_cache else ''}"
)
cache_inst = ".L2::cache_hint" if has_cache else ""
cache_arg = ', "l"(cache_policy)' if has_cache else ""
cache_slot = ", %1" if has_cache else ""
coord_start = 2 if has_cache else 1
coord_tpl = _coord_template(dim, coord_start)
instr = f"cp.async.bulk.prefetch.tensor.{dim}d.L2.global.tile{cache_inst}"
body = (
" asm volatile(\n"
f' "{instr} [%0, {coord_tpl}]{cache_slot};"\n'
" :\n"
f' : "l"(tensormap_addr){cache_arg},\n'
f" {_coord_constraints(dim)}\n"
' : "memory"\n'
" );"
)
return name, sig, body
device_intrinsic(
"ptx_cp_async_bulk_tensor_prefetch",
n_attrs=2,
helper_name=lambda *a: _prefetch_parts(*a)[0],
c_signature=lambda *a: _prefetch_parts(*a)[1],
body=lambda *a: _prefetch_parts(*a)[2],
)
# PTX cp.reduce.async.bulk.tensor shared::cta -> global form:
# cp.reduce.async.bulk.tensor.dim.dst.src.redOp{.load_mode}.completion_mechanism
# {.level::cache_hint}
# [tensorMap, tensorCoords], [srcMem] {, cache-policy}
# .dst = {.global}; .src = {.shared::cta}
# .completion_mechanism = {.bulk_group}
# .redOp = {.add, .min, .max, .inc, .dec, .and, .or, .xor}
# .level::cache_hint = {.L2::cache_hint}
# This registration supports tile mode; redOp is syntax, cache-policy is an operand.
def _reduce_parts(*args):
attrs = args[-3:]
dim = int(attrs[0])
has_cache = _bool_attr(attrs[1])
red_op = parse_str(attrs[2])
sig = (
"(void* src, unsigned long long tensormap_addr, unsigned long long cache_policy"
+ (", " + _coord_sig(dim) if dim else "")
+ ")"
)
name = (
f"ptx_cp_async_bulk_tensor_shared_to_global_reduce_{dim}d"
f"{'_cache_hint' if has_cache else ''}"
)
cache_inst = ".L2::cache_hint" if has_cache else ""
cache_arg = ', "l"(cache_policy)' if has_cache else ""
cache_slot = ", %2" if has_cache else ""
coord_start = 3 if has_cache else 2
coord_tpl = _coord_template(dim, coord_start)
instr = (
f"cp.reduce.async.bulk.tensor.{dim}d.global.shared::cta"
f".{red_op}.tile.bulk_group{cache_inst}"
)
body = (
" unsigned int src_addr = __cvta_generic_to_shared(src);\n"
" asm volatile(\n"
f' "{instr} [%0, {coord_tpl}], [%1]{cache_slot};"\n'
" :\n"
f' : "l"(tensormap_addr), "r"(src_addr){cache_arg},\n'
f" {_coord_constraints(dim)}\n"
' : "memory"\n'
" );"
)
return name, sig, body
device_intrinsic(
"ptx_cp_async_bulk_tensor_reduce",
n_attrs=3,
helper_name=lambda *a: _reduce_parts(*a)[0],
c_signature=lambda *a: _reduce_parts(*a)[1],
body=lambda *a: _reduce_parts(*a)[2],
)
# User-facing dispatchers for tensor global -> shared::cluster. The same
# backend root handles the optional ``.multicast::cluster`` modifier.
def _g2c_dispatch(dim, dst_ptr, bar, tensormap, *args, tile_mode):
cta_mask, cta_group, cache_policy, has_cache, *rest = args
coord_count = 5 if tile_mode == "tile_gather4" else int(dim)
if len(rest) == coord_count + 1:
bar_is_addr = _bool_attr(rest[0])
coords = rest[1:]
else:
bar_is_addr = False
coords = rest
is_unicast = isinstance(cta_mask, tvm.tirx.IntImm) and bin(int(cta_mask)).count("1") <= 1
cg = int(cta_group)
op = "tirx.ptx_cp_async_bulk_tensor_g2cluster"
call_args = [
dst_ptr,
bar,
tensormap,
cta_mask,
cache_policy,
*coords,
int(dim),
cg,
has_cache,
tile_mode,
bar_is_addr,
int(not is_unicast),
]
result = CODEGEN_REGISTRY[op](call_args)
return result[0] if isinstance(result, tuple) else result
@register_codegen("ptx_cp_async_bulk_tensor_global_to_cluster")
def codegen_g2c(dim, dst_ptr, bar, tensormap, *args):
return _g2c_dispatch(dim, dst_ptr, bar, tensormap, *args, tile_mode="tile")
@register_codegen("ptx_cp_async_bulk_tensor_tile_gather4_global_to_cluster")
def codegen_g2c_gather4(dim, dst_ptr, bar, tensormap, *args):
return _g2c_dispatch(dim, dst_ptr, bar, tensormap, *args, tile_mode="tile_gather4")
@register_codegen("ptx_cp_async_bulk_tensor_shared_to_global")
def codegen_s2g(dim, src_ptr, tensormap, *args):
cache_policy, has_cache, *coords = args
result = CODEGEN_REGISTRY["tirx.ptx_cp_async_bulk_tensor_s2g"](
[src_ptr, tensormap, cache_policy, *coords, int(dim), has_cache]
)
return result[0] if isinstance(result, tuple) else result
@register_codegen("ptx_cp_async_bulk_tensor_global_to_cluster_prefetch")
def codegen_prefetch(dim, tensormap, *args):
cache_policy, has_cache, *coords = args
result = CODEGEN_REGISTRY["tirx.ptx_cp_async_bulk_tensor_prefetch"](
[tensormap, cache_policy, *coords, int(dim), has_cache]
)
return result[0] if isinstance(result, tuple) else result
@register_codegen("ptx_cp_async_bulk_tensor_shared_to_global_reduce")
def codegen_reduce(dim, src_ptr, tensormap, *args):
cache_policy, has_cache, red_op, *coords = args
result = CODEGEN_REGISTRY["tirx.ptx_cp_async_bulk_tensor_reduce"](
[src_ptr, tensormap, cache_policy, *coords, int(dim), has_cache, red_op]
)
return result[0] if isinstance(result, tuple) else result
# =============================================================================
# cp.async.bulk non-TMA forms from the PTX Syntax block. Each form is one
# device_intrinsic; optional PTX modifiers are attrs, not separate fixed ops.
# =============================================================================
device_intrinsic(
"ptx_cp_async_bulk_commit_group",
helper_name="ptx_cp_async_bulk_tensor_commit_group",
body=' asm volatile("cp.async.bulk.commit_group;");',
)
def _ptx_cp_async_bulk_wait_group_parts(n, read):
n = int(n)
read_b = bool(int(read)) if hasattr(read, "value") else bool(read)
return (
f"ptx_cp_async_bulk_wait_group{'_read' if read_b else ''}_{n}",
f' asm volatile("cp.async.bulk.wait_group{".read" if read_b else ""} {n};");',
)
device_intrinsic(
"ptx_cp_async_bulk_wait_group",
n_attrs=2,
helper_name=lambda n, read: _ptx_cp_async_bulk_wait_group_parts(n, read)[0],
body=lambda n, read: _ptx_cp_async_bulk_wait_group_parts(n, read)[1],
)
def _bool_attr(value):
return bool(int(value)) if hasattr(value, "value") else bool(value)
def _bulk_cache_operand_constraint(has_cache):
return ', "l"(cache_policy)' if has_cache else ""
def _bulk_cache_operand_suffix(has_cache):
return ".L2::cache_hint" if has_cache else ""
# PTX cp.async.bulk global -> shared::cta form:
# cp.async.bulk.dst.src.completion_mechanism{.level::cache_hint}{.ignore_oob}
# [dstMem], [srcMem], size{, ignoreBytesLeft, ignoreBytesRight}, [mbar] {, cache-policy}
# .dst = {.shared::cta}; .src = {.global}
# .completion_mechanism = {.mbarrier::complete_tx::bytes}
# .level::cache_hint = {.L2::cache_hint}
def _bulk_g2s_cta_parts(*args):
has_cache = _bool_attr(args[-2])
ignore_oob = _bool_attr(args[-1])
instr = (
"cp.async.bulk.shared::cta.global.mbarrier::complete_tx::bytes"
f"{_bulk_cache_operand_suffix(has_cache)}{'.ignore_oob' if ignore_oob else ''}"
)
if ignore_oob:
asm_args = (
'"r"(dst), "l"(src_ptr), "r"(num_bytes), "r"(ignore_bytes_left), '
'"r"(ignore_bytes_right), "r"(mbarrier)'
)
operands = "%2, %3, %4, [%5]"
cache_slot = ", %6" if has_cache else ""
else:
asm_args = '"r"(dst), "l"(src_ptr), "r"(num_bytes), "r"(mbarrier)'
operands = "%2, [%3]"
cache_slot = ", %4" if has_cache else ""
body = (
" unsigned int dst = (unsigned int)__cvta_generic_to_shared(dst_ptr);\n"
" unsigned int mbarrier = (unsigned int)__cvta_generic_to_shared(mbarrier_ptr);\n"
f' asm volatile("{instr} [%0], [%1], {operands}{cache_slot};"\n'
" :\n"
f" : {asm_args}{_bulk_cache_operand_constraint(has_cache)}\n"
' : "memory");'
)
name = (
"tvm_builtin_ptx_cp_async_bulk_g2s_cta"
f"{'_cache_hint' if has_cache else ''}{'_ignore_oob' if ignore_oob else ''}"
)
return name, body
device_intrinsic(
"ptx_cp_async_bulk_g2s_cta",
n_attrs=2,
helper_name=lambda *a: _bulk_g2s_cta_parts(*a)[0],
c_signature=(
"(void* dst_ptr, void* src_ptr, unsigned int num_bytes, "
"unsigned int ignore_bytes_left, unsigned int ignore_bytes_right, "
"void* mbarrier_ptr, unsigned long long cache_policy)"
),
body=lambda *a: _bulk_g2s_cta_parts(*a)[1],
)
# PTX cp.async.bulk global -> shared::cluster form:
# cp.async.bulk.dst.src.completion_mechanism{.multicast}{.level::cache_hint}
# [dstMem], [srcMem], size, [mbar] {, ctaMask} {, cache-policy}
# .dst = {.shared::cluster}; .src = {.global}
# .completion_mechanism = {.mbarrier::complete_tx::bytes}
# .level::cache_hint = {.L2::cache_hint}
# .multicast = {.multicast::cluster}
def _bulk_g2s_cluster_parts(*args):
has_cache = _bool_attr(args[-2])
multicast = _bool_attr(args[-1])
instr = (
"cp.async.bulk.shared::cluster.global.mbarrier::complete_tx::bytes"
f"{'.multicast::cluster' if multicast else ''}{_bulk_cache_operand_suffix(has_cache)}"
)
cta_constraint = ', "h"(cta_mask)' if multicast else ""
mask_slot = ", %4" if multicast else ""
cache_slot = ", %5" if multicast and has_cache else ", %4" if has_cache else ""
body = (
" unsigned int dst = (unsigned int)__cvta_generic_to_shared(dst_ptr);\n"
" unsigned int mbarrier = (unsigned int)__cvta_generic_to_shared(mbarrier_ptr);\n"
f' asm volatile("{instr} [%0], [%1], %2, [%3]'
f'{mask_slot}{cache_slot};"\n'
" :\n"
' : "r"(dst), "l"(src_ptr), "r"(num_bytes), "r"(mbarrier)'
f"{cta_constraint}{_bulk_cache_operand_constraint(has_cache)}\n"
' : "memory");'
)
name = (
"tvm_builtin_ptx_cp_async_bulk_g2s_cluster"
f"{'_multicast' if multicast else ''}{'_cache_hint' if has_cache else ''}"
)
return name, body
device_intrinsic(
"ptx_cp_async_bulk_g2s_cluster",
n_attrs=2,
helper_name=lambda *a: _bulk_g2s_cluster_parts(*a)[0],
c_signature=(
"(void* dst_ptr, void* src_ptr, unsigned int num_bytes, "
"void* mbarrier_ptr, unsigned short cta_mask, unsigned long long cache_policy)"
),
body=lambda *a: _bulk_g2s_cluster_parts(*a)[1],
)
# PTX cp.async.bulk shared::cta -> shared::cluster form:
# cp.async.bulk.dst.src.completion_mechanism [dstMem], [srcMem], size, [mbar]
# .dst = {.shared::cluster}; .src = {.shared::cta}
# .completion_mechanism = {.mbarrier::complete_tx::bytes}
device_intrinsic(
"ptx_cp_async_bulk_s2s_cluster",
helper_name="tvm_builtin_ptx_cp_async_bulk_s2s_cluster",
c_signature="(uint64_t dst, void* src, int size, uint64_t mbar)",
body=r""" unsigned int dst_addr = static_cast<unsigned int>(dst);
unsigned int src_addr = __cvta_generic_to_shared(src);
unsigned int mbar_addr = static_cast<unsigned int>(mbar);
asm volatile(
"cp.async.bulk.shared::cluster.shared::cta.mbarrier::complete_tx::bytes"
" [%0], [%1], %2, [%3];"
:
: "r"(dst_addr), "r"(src_addr), "r"(size), "r"(mbar_addr)
: "memory");""",
)
@register_codegen("ptx_cp_async_bulk_shared_to_cluster")
def codegen_ptx_cp_async_bulk_shared_to_cluster(dst_ptr, src_ptr, size, mbar):
result = CODEGEN_REGISTRY["tirx.ptx_cp_async_bulk_s2s_cluster"]([dst_ptr, src_ptr, size, mbar])
return result[0] if isinstance(result, tuple) else result
# PTX cp.async.bulk shared::cta -> global form:
# cp.async.bulk.dst.src.completion_mechanism{.level::cache_hint}{.cp_mask}
# [dstMem], [srcMem], size {, cache-policy} {, byteMask}
# .dst = {.global}; .src = {.shared::cta}
# .completion_mechanism = {.bulk_group}
# .level::cache_hint = {.L2::cache_hint}
def _bulk_s2g_parts(*args):
has_cache = _bool_attr(args[-2])
cp_mask = _bool_attr(args[-1])
if cp_mask and not has_cache:
raise ValueError("cp.async.bulk shared::cta -> global .cp_mask requires .L2::cache_hint")
instr = f"cp.async.bulk.global.shared::cta.bulk_group{_bulk_cache_operand_suffix(has_cache)}"
if cp_mask:
instr += ".cp_mask"
cache_slot = ", %3" if has_cache else ""
mask_slot = ", %4" if cp_mask else ""
mask_constraint = ', "r"(byte_mask)' if cp_mask else ""
body = (
" unsigned int src = (unsigned int)__cvta_generic_to_shared(src_ptr);\n"
f' asm volatile("{instr} [%0], [%1], %2'
f'{cache_slot}{mask_slot};"\n'
" :\n"
' : "l"(dst_ptr), "r"(src), "r"(num_bytes)'
f"{_bulk_cache_operand_constraint(has_cache)}{mask_constraint}\n"
' : "memory");'
)
name = (
"tvm_builtin_ptx_cp_async_bulk_s2g"
f"{'_cache_hint' if has_cache else ''}{'_cp_mask' if cp_mask else ''}"
)
return name, body
device_intrinsic(
"ptx_cp_async_bulk_s2g",
n_attrs=2,
helper_name=lambda *a: _bulk_s2g_parts(*a)[0],
c_signature=(
"(void* dst_ptr, void* src_ptr, unsigned int num_bytes, "
"unsigned int byte_mask, unsigned long long cache_policy)"
),
body=lambda *a: _bulk_s2g_parts(*a)[1],
)
@@ -0,0 +1,809 @@
# 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.
# pylint: disable=line-too-long
"""CUDA header generator for codegen.
The header generator is used to generate the header for the CUDA code.
It's controlled by the predefined tags.
The tags are used to identify the utility functions/classes necessary for the codegen.
"""
import tvm_ffi
TAGS = {
"cuda",
"cuda/barrier",
"cooperative_groups",
"fp16",
"bf16",
"fp8",
"fp6",
"fp4",
"int8",
"math_constants",
"mma",
"warp_shuffle",
"cast_smem_ptr_to_int",
"get_tmem_addr",
"gmma_descriptor",
"smem_descriptor",
"instr_descriptor",
"instr_descriptor_block_scaled",
"get_time_stamp",
"nvshmem",
"elect_one_sync",
}
@tvm_ffi.register_global_func("tirx.intrinsics.cuda.header_generator")
def header_generator(tags):
"""Generate the header for the CUDA code."""
for tag in tags:
if tag not in TAGS:
raise ValueError(f"Invalid tag: {tag}")
header = ""
if "nvshmem" in tags:
header += R"""
#include <nvshmem.h>
#include <nvshmemx.h>
"""
if "cuda/barrier" in tags or "cooperative_groups" in tags:
header += (
R"""
#include <cuda/barrier>
#include <cooperative_groups.h>
"""
+ "\n"
)
# NVRTC has no host C++ stdlib and no <cuda.h>. Branch on __CUDACC_RTC__ so
# the same emitted source compiles under both nvcc (offline) and NVRTC
# (runtime) without any post-processing in tvm.support.nvcc.
header += """
#ifdef __CUDACC_RTC__
#include <cuda/std/cstdint>
using cuda::std::uint8_t;
using cuda::std::uint16_t;
using cuda::std::uint32_t;
using cuda::std::uint64_t;
using cuda::std::int8_t;
using cuda::std::int16_t;
using cuda::std::int32_t;
using cuda::std::int64_t;
#include <cuda/std/type_traits>
namespace std {
using cuda::std::is_same;
using cuda::std::is_same_v;
using cuda::std::is_integral;
using cuda::std::is_signed;
using cuda::std::is_unsigned;
using cuda::std::is_floating_point;
using cuda::std::enable_if;
using cuda::std::conditional;
}
// NVRTC uses asm/volatile instead of __asm__/__volatile__ (gcc extension).
#ifndef __asm__
#define __asm__ asm
#endif
#ifndef __volatile__
#define __volatile__ volatile
#endif
#else
#include <cstdint>
#include <type_traits>
#include <cuda.h>
#endif
"""
if "fp16" in tags:
header += R"""
#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 530)
#include <cuda_fp16.h>
__device__ half max(half a, half b)
{
return __hgt(__half(a), __half(b)) ? a : b;
}
__device__ half min(half a, half b)
{
return __hlt(__half(a), __half(b)) ? a : b;
}
#endif // __CUDA_ARCH__ >= 530
// Pack two half values.
static inline __device__ __host__ unsigned
__pack_half2(const half x, const half y) {
unsigned v0 = *((unsigned short *)&x);
unsigned v1 = *((unsigned short *)&y);
return (v1 << 16) | v0;
}
#define CUDA_UNSUPPORTED_HALF_MATH_BINARY(HALF_MATH_NAME, FP32_MATH_NAME) \
static inline __device__ __host__ half HALF_MATH_NAME(half x, half y) { \
float tmp_x = __half2float(x); \
float tmp_y = __half2float(y); \
float result = FP32_MATH_NAME(tmp_x, tmp_y); \
return __float2half(result); \
}
#define CUDA_UNSUPPORTED_HALF_MATH_UNARY(HALF_MATH_NAME, FP32_MATH_NAME) \
static inline __device__ __host__ half HALF_MATH_NAME(half x) { \
float tmp_x = __half2float(x); \
float result = FP32_MATH_NAME(tmp_x); \
return __float2half(result); \
}
// Some fp16 math functions are not supported in cuda_fp16.h,
// so we define them here to make sure the generated CUDA code
// is valid.
#if defined(__CUDA_ARCH__)
#if (__CUDA_ARCH__ >= 530)
CUDA_UNSUPPORTED_HALF_MATH_BINARY(hpow, powf)
#if ((__CUDACC_VER_MAJOR__ < 12) || ((__CUDACC_VER_MAJOR__ == 12) && (__CUDACC_VER_MINOR__ < 8)))
CUDA_UNSUPPORTED_HALF_MATH_UNARY(htanh, tanhf)
#endif
CUDA_UNSUPPORTED_HALF_MATH_UNARY(htan, tanf)
CUDA_UNSUPPORTED_HALF_MATH_UNARY(hatan, atanf)
CUDA_UNSUPPORTED_HALF_MATH_UNARY(herf, erf)
#else
CUDA_UNSUPPORTED_HALF_MATH_UNARY(hexp, exp)
#endif
#endif
#undef CUDA_UNSUPPORTED_HALF_MATH_BINARY
#undef CUDA_UNSUPPORTED_HALF_MATH_UNARY
"""
if "bf16" in tags:
header += R"""
#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 800)
#include <cuda_bf16.h>
__device__ nv_bfloat16 max(nv_bfloat16 a, nv_bfloat16 b)
{
return __hgt(a, b) ? a : b;
}
__device__ nv_bfloat16 min(nv_bfloat16 a, nv_bfloat16 b)
{
return __hlt(a, b) ? a : b;
}
#endif // __CUDA_ARCH__ >= 800
// Pack two bfloat16 values.
static inline __device__ __host__ unsigned
__pack_nv_bfloat162(const nv_bfloat16 x, const nv_bfloat16 y) {
unsigned v0 = *((unsigned short *)&x);
unsigned v1 = *((unsigned short *)&y);
return (v1 << 16) | v0;
}
// Some bfp16 math functions are not supported in cuda_bfp16.h,
// so we define them here to make sure the generated CUDA code
// is valid.
#define CUDA_UNSUPPORTED_HALF_MATH_BINARY(HALF_MATH_NAME, FP32_MATH_NAME) \
static inline __device__ __host__ nv_bfloat16 HALF_MATH_NAME(nv_bfloat16 x, nv_bfloat16 y) { \
float tmp_x = __bfloat162float(x); \
float tmp_y = __bfloat162float(y); \
float result = FP32_MATH_NAME(tmp_x, tmp_y); \
return __float2bfloat16(result); \
}
#define CUDA_UNSUPPORTED_HALF_MATH_UNARY(HALF_MATH_NAME, FP32_MATH_NAME) \
static inline __device__ __host__ nv_bfloat16 HALF_MATH_NAME(nv_bfloat16 x) { \
float tmp_x = __bfloat162float(x); \
float result = FP32_MATH_NAME(tmp_x); \
return __float2bfloat16(result); \
}
CUDA_UNSUPPORTED_HALF_MATH_BINARY(hpow, powf)
#if ((__CUDACC_VER_MAJOR__ < 12) || ((__CUDACC_VER_MAJOR__ == 12) && (__CUDACC_VER_MINOR__ < 8)))
CUDA_UNSUPPORTED_HALF_MATH_UNARY(htanh, tanhf)
#endif
CUDA_UNSUPPORTED_HALF_MATH_UNARY(htan, tanf)
CUDA_UNSUPPORTED_HALF_MATH_UNARY(hatan, atanf)
CUDA_UNSUPPORTED_HALF_MATH_UNARY(herf, erf)
#undef CUDA_UNSUPPORTED_HALF_MATH_BINARY
#undef CUDA_UNSUPPORTED_HALF_MATH_UNARY
"""
if "fp8" in tags:
header += R"""
#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 890)
#include <cuda_fp8.h>
using fp8_e4_t = __nv_fp8_e4m3;
using fp8_e4x2_t = __nv_fp8x2_e4m3;
using fp8_e4x4_t = __nv_fp8x4_e4m3;
struct fp8_e4x8_t {
fp8_e4_t data[8];
};
struct fp8_e4x16_t {
fp8_e4_t data[16];
};
using fp8_e5_t = __nv_fp8_e5m2;
using fp8_e5x2_t = __nv_fp8x2_e5m2;
using fp8_e5x4_t = __nv_fp8x4_e5m2;
struct fp8_e5x8_t {
fp8_e5_t data[8];
};
struct fp8_e5x16_t {
fp8_e5_t data[16];
};
using fp8_e8_t = __nv_fp8_e8m0;
using fp8_e8x2_t = __nv_fp8x2_e8m0;
using fp8_e8x4_t = __nv_fp8x4_e8m0;
struct fp8_e8x8_t {
fp8_e8_t data[8];
};
struct fp8_e8x16_t {
fp8_e8_t data[16];
};
#endif // __CUDA_ARCH__ >= 890
"""
if "fp6" in tags:
header += R"""
#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 1000)
#include <cuda_fp6.h>
using fp6_e2_t = __nv_fp6_e2m3;
using fp6_e2x2_t = __nv_fp6x2_e2m3;
using fp6_e2x4_t = __nv_fp6x4_e2m3;
struct fp6_e2x8_t {
fp6_e2_t data[8];
};
struct fp6_e2x16_t {
fp6_e2_t data[16];
};
using fp6_e3_t = __nv_fp6_e3m2;
using fp6_e3x2_t = __nv_fp6x2_e3m2;
using fp6_e3x4_t = __nv_fp6x4_e3m2;
struct fp6_e3x8_t {
fp6_e3_t data[8];
};
struct fp6_e3x16_t {
fp6_e3_t data[16];
};
#endif // __CUDA_ARCH__ >= 1000
"""
if "fp4" in tags:
header += R"""
#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 800)
#include <cuda_fp4.h>
using fp4_e2_t = __nv_fp4_e2m1;
using fp4_e2x2_t = __nv_fp4x2_e2m1;
using fp4_e2x4_t = __nv_fp4x4_e2m1;
struct fp4_e2x8_t {
fp4_e2_t data[8];
};
struct fp4_e2x16_t {
fp4_e2_t data[16];
};
#endif // __CUDA_ARCH__ >= 800
"""
#########################################################
# Vector type extensions
#########################################################
if "fp16" in tags or "bf16" in tags:
header += R"""
template <typename T, typename TVec2>
struct __align__(8) half4_bfloat164 {
T x, y, z, w;
__host__ __device__ half4_bfloat164() : x(T(0)), y(T(0)), z(T(0)), w(T(0)) {}
__host__ __device__ half4_bfloat164(T x, T y, T z, T w) : x(x), y(y), z(z), w(w) {}
"""
if "fp8" in tags:
header += R"""
__host__ __device__ explicit half4_bfloat164(const __nv_fp8x4_e4m3& fp8x4) {
if constexpr (std::is_same_v<T, __half>) {
__nv_fp8x2_e4m3 lo_part, hi_part;
lo_part.__x = static_cast<__nv_fp8x2_storage_t>(fp8x4.__x & 0xFFFF);
hi_part.__x = static_cast<__nv_fp8x2_storage_t>((fp8x4.__x >> 16) & 0xFFFF);
TVec2 lo_half2 = static_cast<TVec2>(lo_part);
TVec2 hi_half2 = static_cast<TVec2>(hi_part);
x = reinterpret_cast<T*>(&lo_half2)[0];
y = reinterpret_cast<T*>(&lo_half2)[1];
z = reinterpret_cast<T*>(&hi_half2)[0];
w = reinterpret_cast<T*>(&hi_half2)[1];
} else {
__nv_fp8_storage_t elem0_raw = static_cast<__nv_fp8_storage_t>(fp8x4.__x & 0xFF);
__nv_fp8_storage_t elem1_raw = static_cast<__nv_fp8_storage_t>((fp8x4.__x >> 8) & 0xFF);
__nv_fp8_storage_t elem2_raw = static_cast<__nv_fp8_storage_t>((fp8x4.__x >> 16) & 0xFF);
__nv_fp8_storage_t elem3_raw = static_cast<__nv_fp8_storage_t>((fp8x4.__x >> 24) & 0xFF);
__nv_fp8_e4m3 elem0, elem1, elem2, elem3;
elem0.__x = elem0_raw;
elem1.__x = elem1_raw;
elem2.__x = elem2_raw;
elem3.__x = elem3_raw;
x = T(elem0);
y = T(elem1);
z = T(elem2);
w = T(elem3);
}
}
__host__ __device__ explicit operator __nv_fp8x4_e4m3() const {
__nv_fp8x4_e4m3 result;
TVec2 lo_half2 = *reinterpret_cast<const TVec2*>(&x);
TVec2 hi_half2 = *reinterpret_cast<const TVec2*>(&z);
__nv_fp8x2_e4m3 lo_part(lo_half2), hi_part(hi_half2);
result.__x =
(static_cast<uint32_t>(lo_part.__x) | (static_cast<uint32_t>(hi_part.__x) << 16));
return result;
}
__host__ __device__ explicit half4_bfloat164(const __nv_fp8x4_e5m2& fp8x4) {
__nv_fp8x2_e5m2 lo_part, hi_part;
lo_part.__x = static_cast<__nv_fp8x2_storage_t>(fp8x4.__x & 0xFFFF);
hi_part.__x = static_cast<__nv_fp8x2_storage_t>((fp8x4.__x >> 16) & 0xFFFF);
TVec2 lo_half2 = static_cast<TVec2>(lo_part);
TVec2 hi_half2 = static_cast<TVec2>(hi_part);
x = reinterpret_cast<T*>(&lo_half2)[0];
y = reinterpret_cast<T*>(&lo_half2)[1];
z = reinterpret_cast<T*>(&hi_half2)[0];
w = reinterpret_cast<T*>(&hi_half2)[1];
}
__host__ __device__ explicit operator __nv_fp8x4_e5m2() const {
__nv_fp8x4_e5m2 result;
TVec2 lo_half2 = *reinterpret_cast<const TVec2*>(&x);
TVec2 hi_half2 = *reinterpret_cast<const TVec2*>(&z);
__nv_fp8x2_e5m2 lo_part(lo_half2), hi_part(hi_half2);
result.__x =
(static_cast<uint32_t>(lo_part.__x) | (static_cast<uint32_t>(hi_part.__x) << 16));
return result;
}
__host__ __device__ explicit half4_bfloat164(const __nv_fp8x4_e8m0& fp8x4) {
__nv_fp8x2_e8m0 lo_part, hi_part;
lo_part.__x = static_cast<__nv_fp8x2_storage_t>(fp8x4.__x & 0xFFFF);
hi_part.__x = static_cast<__nv_fp8x2_storage_t>((fp8x4.__x >> 16) & 0xFFFF);
TVec2 lo_half2 = static_cast<TVec2>(lo_part);
TVec2 hi_half2 = static_cast<TVec2>(hi_part);
x = reinterpret_cast<T*>(&lo_half2)[0];
y = reinterpret_cast<T*>(&lo_half2)[1];
z = reinterpret_cast<T*>(&hi_half2)[0];
w = reinterpret_cast<T*>(&hi_half2)[1];
}
__host__ __device__ explicit operator __nv_fp8x4_e8m0() const {
__nv_fp8x4_e8m0 result;
TVec2 lo_half2 = *reinterpret_cast<const TVec2*>(&x);
TVec2 hi_half2 = *reinterpret_cast<const TVec2*>(&z);
__nv_fp8x2_e8m0 lo_part(lo_half2), hi_part(hi_half2);
result.__x =
(static_cast<uint32_t>(lo_part.__x) | (static_cast<uint32_t>(hi_part.__x) << 16));
return result;
}
"""
if "fp4" in tags:
header += R"""
__host__ __device__ explicit half4_bfloat164(const __nv_fp4x4_e2m1& fp4x4) {
if constexpr (std::is_same_v<T, __half>) {
__nv_fp4x2_storage_t lo_part = static_cast<__nv_fp4x2_storage_t>(fp4x4.__x & 0xFF);
__nv_fp4x2_storage_t hi_part = static_cast<__nv_fp4x2_storage_t>((fp4x4.__x >> 8) & 0xFF);
TVec2 lo_half2 = __half2(__nv_cvt_fp4x2_to_halfraw2(lo_part, __NV_E2M1));
TVec2 hi_half2 = __half2(__nv_cvt_fp4x2_to_halfraw2(hi_part, __NV_E2M1));
x = reinterpret_cast<T*>(&lo_half2)[0];
y = reinterpret_cast<T*>(&lo_half2)[1];
z = reinterpret_cast<T*>(&hi_half2)[0];
w = reinterpret_cast<T*>(&hi_half2)[1];
} else {
__nv_fp4_e2m1 elem0, elem1, elem2, elem3;
elem0.__x = static_cast<__nv_fp4_storage_t>(fp4x4.__x & 0xF);
elem1.__x = static_cast<__nv_fp4_storage_t>((fp4x4.__x >> 4) & 0xF);
elem2.__x = static_cast<__nv_fp4_storage_t>((fp4x4.__x >> 8) & 0xF);
elem3.__x = static_cast<__nv_fp4_storage_t>((fp4x4.__x >> 12) & 0xF);
x = T(elem0);
y = T(elem1);
z = T(elem2);
w = T(elem3);
}
}
__host__ __device__ explicit operator __nv_fp4x4_e2m1() const {
TVec2 lo_half2 = *reinterpret_cast<const TVec2*>(&x);
TVec2 hi_half2 = *reinterpret_cast<const TVec2*>(&z);
return __nv_fp4x4_e2m1(lo_half2, hi_half2);
}
"""
header += R"""
};
"""
if "fp16" in tags:
header += R"""
using half4 = half4_bfloat164<__half, __half2>;
__host__ __device__ half4 make_half4(__half x, __half y, __half z, __half w) {
return half4(x, y, z, w);
}
"""
if "bf16" in tags:
header += R"""
using nv_bfloat164 = half4_bfloat164<nv_bfloat16, nv_bfloat162>;
__host__ __device__ nv_bfloat164 make_nv_bfloat164(nv_bfloat16 x, nv_bfloat16 y, nv_bfloat16 z, nv_bfloat16 w) {
return nv_bfloat164(x, y, z, w);
}
__host__ __device__ nv_bfloat162 make_nv_bfloat162(nv_bfloat16 x, nv_bfloat16 y) {
return nv_bfloat162(x, y);
}
""" # noqa: E501
if "fp8" in tags:
header += R"""
__host__ __device__ nv_bfloat162 cast_to_nv_bfloat162(const __nv_fp8x2_e4m3& fp8x2) {
__nv_fp8_e4m3 elem0, elem1;
elem0.__x = static_cast<__nv_fp8_storage_t>(fp8x2.__x & 0xFF);
elem1.__x = static_cast<__nv_fp8_storage_t>((fp8x2.__x >> 8) & 0xFF);
nv_bfloat16 x = nv_bfloat16(elem0);
nv_bfloat16 y = nv_bfloat16(elem1);
return nv_bfloat162(x, y);
}
__host__ __device__ nv_bfloat162 cast_to_nv_bfloat162(const __nv_fp8x2_e5m2& fp8x2) {
__nv_fp8_e5m2 elem0, elem1;
elem0.__x = static_cast<__nv_fp8_storage_t>(fp8x2.__x & 0xFF);
elem1.__x = static_cast<__nv_fp8_storage_t>((fp8x2.__x >> 8) & 0xFF);
nv_bfloat16 x = nv_bfloat16(elem0);
nv_bfloat16 y = nv_bfloat16(elem1);
return nv_bfloat162(x, y);
}
__host__ __device__ nv_bfloat162 cast_to_nv_bfloat162(const __nv_fp8x2_e8m0& fp8x2) {
__nv_fp8_e8m0 elem0, elem1;
elem0.__x = static_cast<__nv_fp8_storage_t>(fp8x2.__x & 0xFF);
elem1.__x = static_cast<__nv_fp8_storage_t>((fp8x2.__x >> 8) & 0xFF);
nv_bfloat16 x = nv_bfloat16(elem0);
nv_bfloat16 y = nv_bfloat16(elem1);
return nv_bfloat162(x, y);
}
"""
if "fp8" in tags:
header += R"""
__device__ __nv_fp8x2_e5m2 make___nv_fp8x2_e5m2(__nv_fp8_e5m2 x, __nv_fp8_e5m2 y) {
__nv_fp8x2_e5m2 result;
result.__x = (x.__x) | (y.__x << 8);
return result;
}
__device__ __nv_fp8x4_e5m2 make___nv_fp8x4_e5m2(__nv_fp8_e5m2 a, __nv_fp8_e5m2 b, __nv_fp8_e5m2 c, __nv_fp8_e5m2 d) {
__nv_fp8x4_e5m2 result;
result.__x = (a.__x) | (b.__x << 8) | (c.__x << 16) | (d.__x << 24);
return result;
}
__device__ __nv_fp8x2_e4m3 make___nv_fp8x2_e4m3(__nv_fp8_e4m3 x, __nv_fp8_e4m3 y) {
__nv_fp8x2_e4m3 result;
result.__x = (x.__x) | (y.__x << 8);
return result;
}
__device__ __nv_fp8x4_e4m3 make___nv_fp8x4_e4m3(__nv_fp8_e4m3 a, __nv_fp8_e4m3 b, __nv_fp8_e4m3 c, __nv_fp8_e4m3 d) {
__nv_fp8x4_e4m3 result;
result.__x = (a.__x) | (b.__x << 8) | (c.__x << 16) | (d.__x << 24);
return result;
}
__device__ __nv_fp8x2_e8m0 make___nv_fp8x2_e8m0(__nv_fp8_e8m0 x, __nv_fp8_e8m0 y) {
__nv_fp8x2_e8m0 result;
result.__x = (x.__x) | (y.__x << 8);
return result;
}
__device__ __nv_fp8x4_e8m0 make___nv_fp8x4_e8m0(__nv_fp8_e8m0 a, __nv_fp8_e8m0 b, __nv_fp8_e8m0 c, __nv_fp8_e8m0 d) {
__nv_fp8x4_e8m0 result;
result.__x = (a.__x) | (b.__x << 8) | (c.__x << 16) | (d.__x << 24);
return result;
}
""" # noqa: E501
if "fp4" in tags:
header += R"""
__host__ __device__ nv_bfloat162 cast_to_nv_bfloat162(const __nv_fp4x2_e2m1& fp4x2) {
__nv_fp4_e2m1 elem0, elem1;
elem0.__x = static_cast<__nv_fp4_storage_t>(fp4x2.__x & 0xFF);
elem1.__x = static_cast<__nv_fp4_storage_t>((fp4x2.__x >> 8) & 0xFF);
nv_bfloat16 x = nv_bfloat16(elem0);
nv_bfloat16 y = nv_bfloat16(elem1);
return nv_bfloat162(x, y);
}
"""
if "int8" in tags:
header += R"""
#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 610)
#include <sm_61_intrinsics.h>
#if defined(__CUDACC_RTC__)
#define __SM_61_INTRINSICS_DECL__ __device__
#else /* !__CUDACC_RTC__ */
#define __SM_61_INTRINSICS_DECL__ static __device__ __inline__
#endif /* __CUDACC_RTC__ */
#ifndef __CUDA_ARCH__
#define __DEF_IF_HOST { }
#else /* !__CUDA_ARCH__ */
#define __DEF_IF_HOST ;
#endif /* __CUDA_ARCH__ */
__SM_61_INTRINSICS_DECL__ int __dp4a(unsigned int srcA, int srcB, int c) __DEF_IF_HOST
__SM_61_INTRINSICS_DECL__ int __dp4a(int srcA, unsigned int srcB, int c) __DEF_IF_HOST
#undef __DEF_IF_HOST
#if !defined(__CUDACC_RTC__) && defined(__CUDA_ARCH__)
__SM_61_INTRINSICS_DECL__ int __dp4a(unsigned int srcA, int srcB, int c) {
int ret;
asm volatile ("dp4a.u32.s32 %0, %1, %2, %3;" : "=r"(ret) : "r"(srcA), "r"(srcB), "r"(c));
return ret;
}
__SM_61_INTRINSICS_DECL__ int __dp4a(int srcA, unsigned int srcB, int c) {
int ret;
asm volatile ("dp4a.s32.u32 %0, %1, %2, %3;" : "=r"(ret) : "r"(srcA), "r"(srcB), "r"(c));
return ret;
}
#endif /* !__CUDACC_RTC__ && defined(__CUDA_ARCH__) */
#undef __SM_61_INTRINSICS_DECL__
#endif // __CUDA_ARCH__ >= 610
"""
if "math_constants" in tags:
header += R"""
#include <math_constants.h>
"""
if "mma" in tags:
header += R"""
#include <mma.h>
"""
if "warp_shuffle" in tags:
header += R"""
#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ < 700)
#define __shfl_sync(mask, var, lane, width) \
__shfl((var), (lane), (width))
#define __shfl_down_sync(mask, var, offset, width) \
__shfl_down((var), (offset), (width))
#define __shfl_up_sync(mask, var, offset, width) \
__shfl_up((var), (offset), (width))
#endif
"""
if "cast_smem_ptr_to_int" in tags:
header += R"""
__forceinline__ __device__ unsigned int cast_smem_ptr_to_int(const void* const smem_ptr) {
unsigned int smem_int;
asm volatile ("{ .reg .u64 smem_int; cvta.to.shared.u64 smem_int, %1; cvt.u32.u64 %0, smem_int; }"
: "=r"(smem_int) : "l"(smem_ptr));
return smem_int;
}
"""
header += R"""
#if (((__CUDACC_VER_MAJOR__ == 11) && (__CUDACC_VER_MINOR__ >= 4)) || \
(__CUDACC_VER_MAJOR__ > 11))
#define TVM_ENABLE_L2_PREFETCH 1
#else
#define TVM_ENABLE_L2_PREFETCH 0
#endif
#ifdef _WIN32
using uint = unsigned int;
using uchar = unsigned char;
using ushort = unsigned short;
using int64_t = long long;
using uint64_t = unsigned long long;
#else
#define uint unsigned int
#define uchar unsigned char
#define ushort unsigned short
#endif
"""
if "get_tmem_addr" in tags:
header += R"""
__forceinline__ __device__ uint32_t get_tmem_addr(uint32_t idx, int row_offset, int col_offset) {
int col_idx = idx & 0xFFFF;
int row_idx = (idx >> 16) & 0xFFFF;
col_idx += col_offset;
row_idx += row_offset;
col_idx = col_idx & 0xFFFF;
row_idx = row_idx & 0xFFFF;
uint32_t new_idx = (row_idx << 16) | col_idx;
return new_idx;
}
"""
if "get_time_stamp" in tags:
header += R"""
__forceinline__ __device__ uint32_t tvm_builtin_get_timestamp() {
volatile uint32_t ret;
asm volatile("mov.u32 %0, %globaltimer_lo;" : "=r"(ret));
return ret;
}
"""
if "gmma_descriptor" in tags:
header += R"""
#ifndef HOST_DEVICE
#define HOST_DEVICE __forceinline__ __host__ __device__
#endif
union GmmaDescriptor
{
HOST_DEVICE constexpr
GmmaDescriptor() noexcept : desc_(0) {}
HOST_DEVICE constexpr
GmmaDescriptor(uint64_t desc) noexcept : desc_(desc) {}
HOST_DEVICE constexpr
GmmaDescriptor(GmmaDescriptor const& t) noexcept : desc_(t.desc_) {}
HOST_DEVICE constexpr
GmmaDescriptor(GmmaDescriptor && t) noexcept : desc_(t.desc_) {}
HOST_DEVICE constexpr
GmmaDescriptor& operator=(GmmaDescriptor const& t) noexcept {
desc_ = t.desc_;
return *this;
}
HOST_DEVICE constexpr
GmmaDescriptor& operator=(GmmaDescriptor && t) noexcept {
desc_ = t.desc_;
return *this;
}
uint64_t desc_;
uint32_t reg32_[2];
uint16_t reg16_[4];
// Bitfield implementation avoids the need for shifts in assignment
struct {
// start_address, bit [0,14), 4LSB not included
uint16_t start_address_ : 14, : 2; // 14 bits [0,14), 2 bits unused
// leading dimension byte offset, bit [16,30), 4LSB not included
// For N: This is the stride from the first col to the second col of the 8x2 brick in INTERLEAVED
// Unused for all SWIZZLE_* layouts (and assumed to be 1)
// For T: This is the stride from the first 8 rows to the next 8 rows.
uint16_t leading_byte_offset_ : 14, : 2; // 14 bits [0,14), 2 bits unused
// stride dimension byte offset, bit [32,46), 4LSB not included
// For N: This is the stride from the first 8 rows to the next 8 rows.
// For T: This is the stride fro mthe first 8 cols to the next 8 cols.
uint16_t stride_byte_offset_ : 14, : 2; // 14 bits [0,14), 2 bits unused
// base_offset, bit [49,52)
// Valid only for SWIZZLE_128B and SWIZZLE_64B
uint8_t : 1, base_offset_ : 3, : 4; // 1 bit unused, 3 bits [1,4), 4 bits unused
// layout type, bit [62,64)
// SWIZZLE_NONE = 0, SWIZZLE_32B = 3, SWIZZLE_64B = 2, SWIZZLE_128B = 1
uint8_t : 6, layout_type_ : 2; // 6 bits unused, 2 bits [6,8)
} bitfield;
// Decay to a uint64_t
HOST_DEVICE constexpr
operator uint64_t() const noexcept { return desc_; }
};
""" # noqa: E501
if "smem_descriptor" in tags:
header += R"""
#ifndef HOST_DEVICE
#define HOST_DEVICE __forceinline__ __host__ __device__
#endif
union SmemDescriptor
{
uint64_t desc_ = 0;
// Bitfield implementation avoids the need for shifts in assignment
struct {
// start_address, bit [0,14), 4LSB not included
uint16_t start_address_ : 14, : 2; // 14 bits [0,14), 2 bits unused
// leading dimension byte offset, bit [16,30), 4LSB not included
uint16_t leading_byte_offset_ : 14, : 2; // 14 bits [0,14), 2 bits unused
// stride dimension byte offset, bit [32,46), 4LSB not included
uint16_t stride_byte_offset_ : 14, version_ : 2; // 14 bits [0,14), 2 bits [14,16)
// base_offset, bit [49,52). leading_byte_offset_mode, bit [52,53).
uint8_t : 1, base_offset_ : 3, lbo_mode_ : 1, : 3; // 1 bit unused, 3 bits [1,4), 1 bit [4,5), 3 bits unused
// layout type, bit [61,64), SWIZZLE_NONE matrix descriptor = 0, SWIZZLE_128B matrix descriptor = 2, SWIZZLE_64B descriptor = 4, SWIZZLE_32B descriptor = 6, SWIZZLE_128B_BASE32B = 1, N/A = 3, N/A = 5, N/A = 7
uint8_t : 5, layout_type_ : 3; // 6 bits unused, 3 bits [5,8)
};
// Seperate the field, as we may only update one part of desc
struct {
uint32_t lo;
uint32_t hi;
};
// Decay to a uint64_t
HOST_DEVICE constexpr
operator uint64_t() const noexcept { return desc_; }
};
""" # noqa: E501
if "instr_descriptor" in tags:
header += R"""
#ifndef HOST_DEVICE
#define HOST_DEVICE __forceinline__ __host__ __device__
#endif
union InstrDescriptor
{
uint32_t desc_;
struct {
// Bitfield implementation avoids the need for shifts in assignment
uint16_t sparse_id2_ : 2, // bit [ 0, 2) : Sparse meta data id2
sparse_flag_ : 1, // bit [ 2, 3) : 0 = dense. 1 = sparse. 1 value valid only for F32F16/S8/MXF8F6F4
saturate_ : 1, // bit [ 3, 4) : 0 = no saturate. 1 = saturate. 1 value valid only for S8
c_format_ : 2, // bit [ 4, 6) : 0 = F16. 1 = F32, 2 = S32
: 1, //
a_format_ : 3, // bit [ 7,10) : MXF8F6F4Format:0 = E4M3, 1 = E5M2, 3 = E2M3, 4 = E3M2, 5 = E2M1. F32F16Format: 0 = F16, 1 = BF16, 2 = TF32. S8: 0 unsigned 8 bit, 1 signed 8 bit. Boolean MMA: 0 Boolean
b_format_ : 3, // bit [10,13) : MXF8F6F4Format:0 = E4M3, 1 = E5M2, 3 = E2M3, 4 = E3M2, 5 = E2M1. F32F16Format: 0 = F16, 1 = BF16, 2 = TF32. S8: 0 unsigned 8 bit, 1 signed 8 bit. Boolean MMA: 0 Boolean
a_negate_ : 1, // bit [13,14) : 0 = no negate. 1 = negate. 1 value valid only for F32F16Format and MXF8F6F4Format
b_negate_ : 1, // bit [14,15) : 0 = no negate. 1 = negate. 1 value valid only for F32F16Format and MXF8F6F4Format
a_major_ : 1; // bit [15,16) : 0 = K-major. 1 = MN-major. Major value of 1 is only valid for E4M3, E5M2, INT8 (signed and unsigned), F16, BF16 and TF32 source formats
uint16_t b_major_ : 1, // bit [16,17) : 0 = K-major. 1 = MN-major. Major value of 1 is only valid for E4M3, E5M2, INT8 (signed and unsigned), F16, BF16 and TF32 source formats
n_dim_ : 6, // bit [17,23) : 3 LSBs not included. Valid values range from 1 (N=8) to 32 (N=256). All values are not valid for all instruction formats
: 1, //
m_dim_ : 5, // bit [24,29) : 4 LSBs not included. Valid values are: 4 (M=64), 8 (M=128), 16 (M=256)
: 1, //
max_shift_ : 2; // bit [30,32) : Maximum shift for WS instruction. Encoded as follows: 0 = no shift, 1 = maximum shift of 8, 2 = maximum shift of 16, 3 = maximum shift of 32.
};
// Decay to a uint32_t
HOST_DEVICE constexpr explicit
operator uint32_t() const noexcept { return desc_; }
};
""" # noqa: E501
if "instr_descriptor_block_scaled" in tags:
header += R"""
#ifndef HOST_DEVICE
#define HOST_DEVICE __forceinline__ __host__ __device__
#endif
union InstrDescriptorBlockScaled
{
uint32_t desc_;
struct {
// Bitfield implementation avoids the need for shifts in assignment
uint16_t sparse_id2_ : 2, // bit [ 0, 2) : Sparse meta data id2
sparse_flag_ : 1, // bit [ 2, 3) : 0 = dense. 1 = sparse. 1 value valid only for F32F16/S8/MXF8F6F4
: 1, //
b_sf_id_ : 2, // bit [ 4, 6) : Matrix B Scale Factor ID
: 1, //
a_format_ : 3, // bit [ 7, 9) : MXF8F6F4Format:0 = E4M3, 1 = E5M2, 3 = E2M3, 4 = E3M2, 5 = E2M1. F32F16Format: 0 = F16, 1 = BF16, 2 = TF32. S8: 0 unsigned 8 bit, 1 signed 8 bit. BMMA: 0 Boolean
b_format_ : 3, // bit [10,12) : MXF8F6F4Format:0 = E4M3, 1 = E5M2, 3 = E2M3, 4 = E3M2, 5 = E2M1. F32F16Format: 0 = F16, 1 = BF16, 2 = TF32. S8: 0 unsigned 8 bit, 1 signed 8 bit. BMMA: 0 Boolean
a_negate_ : 1, // bit [13,14) : 0 = no negate. 1 = negate. 1 value valid only for F32F16Format and MXF8F6F4Format
b_negate_ : 1, // bit [14,15) : 0 = no negate. 1 = negate. 1 value valid only for F32F16Format and MXF8F6F4Format
a_major_ : 1; // bit [15,16) : 0 = K-major. 1 = MN-major. Major value of 1 is only valid for E4M3, E5M2, INT8 (signed and unsigned), F16, BF16 and TF32 source formats
uint16_t b_major_ : 1, // bit [16,17) : 0 = K-major. 1 = MN-major. Major value of 1 is only valid for E4M3, E5M2, INT8 (signed and unsigned), F16, BF16 and TF32 source formats
n_dim_ : 6, // bit [17,23) : 3 LSBs not included. Valid values range from 1 (N=8) to 32 (N=256). All values are not valid for all instruction formats
scale_format_ : 1, // bit [23,24) : 0=E4M3, 1=E8M0
m_dim_ : 5, // bit [24,29) : 4 LSBs not included. Valid values are: 4 (M=64), 8 (M=128), 16 (M=256)
a_sf_id_ : 2, // bit [29,31) : Matrix A Scale Factor ID
: 1; //
};
// Decay to a uint32_t
HOST_DEVICE constexpr
operator uint32_t() const noexcept { return desc_; }
};
""" # noqa: E501
if "elect_one_sync" in tags:
header += R"""
__forceinline__ __device__ uint32_t tvm_builtin_elect_one_sync() {{
uint32_t pred = 0;
uint32_t laneid = 0;
asm volatile(
"{\n"
".reg .b32 %%rx;\n"
".reg .pred %%px;\n"
" elect.sync %%rx|%%px, %2;\n"
"@%%px mov.s32 %1, 1;\n"
" mov.s32 %0, %%rx;\n"
"}\n"
: "+r"(laneid), "+r"(pred)
: "r"(0xFFFFFFFF));
return pred;
}}
"""
return header
@@ -0,0 +1,501 @@
# 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.
# pylint: disable=redefined-builtin, invalid-name
"""Math intrinsics.
PTX side:
* ``add{.rnd}{.ftz}.f32x2`` / ``sub`` / ``mul`` / ``fma`` — packed f32x2.
* ``ex2.approx.ftz.f32`` / ``rcp.approx.ftz.f32`` — special functions.
* ``max.f32`` / ``min.f32`` — 3-operand reduction form.
CUDA side:
* warp / CTA reductions (templated butterfly shuffle-XOR).
"""
from tvm.backend.cuda.op import cuda_func_call
from ._schema import device_intrinsic
from .registry import register_codegen
from .utils import parse_str, validate_power_of_two_range
# =============================================================================
# Packed f32x2 arithmetic — `add{.rnd}{.ftz}.f32x2 d, a, b ;` and friends.
# Inputs are packed into a `.b64` register (low half = elem 0, high half =
# elem 1); the body packs/unpacks via ``make_float2`` + ``reinterpret_cast``.
# =============================================================================
# PTX add/sub/mul/fma over (f32 | f32x2 | f64), DPS form.
# add{.rnd}{.ftz}{.sat}.f32 [d], a, b
# add{.rnd}{.ftz}.f32x2 [d], a, b (a,b are packed-as-u64)
# add{.rnd}.f64 [d], a, b
# (sub / mul same shape; fma adds a `c` operand)
# Inputs a/b/c are register operands (scalar fp32 / packed u64 / scalar fp64).
# Result is written through `d` (a pointer).
_PACKED_ROUNDING = ("rz", "rn", "rm", "rp")
# Per-dtype operand types and asm constraints.
# - c_in: C type of input register operand (matches PTX register type)
# - out_cast: pointer cast applied at d_addr (callers may pass float*/double*/...)
# - in_cstr / out_cstr: GCC asm constraint letter
_DTYPE_INFO = {
"f32": {"c_in": "float", "out_cast": "float*", "in_cstr": "f", "out_cstr": "f"},
"f32x2": {
"c_in": "unsigned long long",
"out_cast": "uint64_t*",
"in_cstr": "l",
"out_cstr": "l",
},
"f64": {"c_in": "double", "out_cast": "double*", "in_cstr": "d", "out_cstr": "d"},
}
def _ptx_arith_modifier_string(dtype, rounding, ftz, sat):
"""Build the `.rnd.ftz.sat` modifier substring + name suffix."""
rnd = parse_str(rounding)
assert rnd in _PACKED_ROUNDING, f"invalid rounding {rnd!r}, expected one of {_PACKED_ROUNDING}"
ftz_b = bool(int(ftz)) if hasattr(ftz, "value") else bool(ftz)
sat_b = bool(int(sat)) if hasattr(sat, "value") else bool(sat)
if dtype == "f64" and (ftz_b or sat_b):
raise ValueError("PTX <op>.f64 does not accept .ftz or .sat")
if dtype == "f32x2" and sat_b:
raise ValueError("PTX <op>.f32x2 does not accept .sat")
mod = f".{rnd}"
if ftz_b:
mod += ".ftz"
if sat_b:
mod += ".sat"
name_suffix = f"_{rnd}"
if ftz_b:
name_suffix += "_ftz"
if sat_b:
name_suffix += "_sat"
return mod, name_suffix
def _ptx_binary_arith_parts(op, dtype):
"""Return (name_fn, sig, body_fn) for ptx_{op}_{dtype} binary form."""
info = _DTYPE_INFO[dtype]
# Destination is ``void*`` so callers can pass any element-type pointer
# (float* / double* / uint64_t*); body reinterpret-casts to the right type.
sig = f"(void* d, {info['c_in']} a, {info['c_in']} b)"
def _name(d, a, b, rounding, ftz, sat):
_, suf = _ptx_arith_modifier_string(dtype, rounding, ftz, sat)
return f"tvm_builtin_ptx_{op}_{dtype}{suf}"
out_c = info["out_cstr"]
in_c = info["in_cstr"]
out_cast = info["out_cast"]
def _body(d, a, b, rounding, ftz, sat):
mod, _ = _ptx_arith_modifier_string(dtype, rounding, ftz, sat)
return (
f' asm volatile("{op}{mod}.{dtype} %0, %1, %2;"\n'
f' : "={out_c}"(*reinterpret_cast<{out_cast}>(d))\n'
f' : "{in_c}"(a), "{in_c}"(b));'
)
return _name, sig, _body
def _ptx_fma_parts(dtype):
"""Return (name_fn, sig, body_fn) for ptx_fma_{dtype}."""
info = _DTYPE_INFO[dtype]
sig = f"(void* d, {info['c_in']} a, {info['c_in']} b, {info['c_in']} c)"
def _name(d, a, b, c, rounding, ftz, sat):
_, suf = _ptx_arith_modifier_string(dtype, rounding, ftz, sat)
return f"tvm_builtin_ptx_fma_{dtype}{suf}"
out_c = info["out_cstr"]
in_c = info["in_cstr"]
out_cast = info["out_cast"]
def _body(d, a, b, c, rounding, ftz, sat):
mod, _ = _ptx_arith_modifier_string(dtype, rounding, ftz, sat)
return (
f' asm volatile("fma{mod}.{dtype} %0, %1, %2, %3;"\n'
f' : "={out_c}"(*reinterpret_cast<{out_cast}>(d))\n'
f' : "{in_c}"(a), "{in_c}"(b), "{in_c}"(c));'
)
return _name, sig, _body
# Register 12 ops: {add, sub, mul, fma} x {f32, f32x2, f64}.
for _dtype in ("f32", "f32x2", "f64"):
for _op in ("add", "sub", "mul"):
_name_fn, _sig, _body_fn = _ptx_binary_arith_parts(_op, _dtype)
device_intrinsic(
f"ptx_{_op}_{_dtype}",
n_attrs=3, # rounding, ftz, sat
helper_name=_name_fn,
c_signature=_sig,
body=_body_fn,
)
_name_fn, _sig, _body_fn = _ptx_fma_parts(_dtype)
device_intrinsic(
f"ptx_fma_{_dtype}",
n_attrs=3,
helper_name=_name_fn,
c_signature=_sig,
body=_body_fn,
)
del _dtype, _op, _name_fn, _sig, _body_fn
# =============================================================================
# ex2.approx.ftz.f32 / rcp.approx.ftz.f32 — 1 form each.
# =============================================================================
device_intrinsic(
"ptx_exp2",
c_signature="(float x)",
return_type="float",
body=(
" float result;\n"
' asm volatile("ex2.approx.ftz.f32 %0, %1;" : "=f"(result) : "f"(x));\n'
" return result;"
),
)
device_intrinsic(
"ptx_rcp",
c_signature="(float x)",
return_type="float",
body=(
" float result;\n"
' asm volatile("rcp.approx.ftz.f32 %0, %1;" : "=f"(result) : "f"(x));\n'
" return result;"
),
)
# =============================================================================
# 3-operand max.f32 / min.f32 — the f32, 3-operand form-table entry of the
# redux/reduction-style fp32 max/min ops.
# =============================================================================
_ABC_SIG = "(float a, float b, float c)"
device_intrinsic(
"ptx_reduce3_max_f32",
c_signature=_ABC_SIG,
return_type="float",
body=(
" float result;\n"
' asm volatile("max.f32 %0, %1, %2, %3;"\n'
' : "=f"(result) : "f"(a), "f"(b), "f"(c));\n'
" return result;"
),
)
device_intrinsic(
"ptx_reduce3_min_f32",
c_signature=_ABC_SIG,
return_type="float",
body=(
" float result;\n"
' asm volatile("min.f32 %0, %1, %2, %3;"\n'
' : "=f"(result) : "f"(a), "f"(b), "f"(c));\n'
" return result;"
),
)
_BINARY_F32_SIG = "(float a, float b)"
def _ptx_max_f32_body(a, b, ftz, nan):
ftz_b = bool(int(ftz)) if hasattr(ftz, "value") else bool(ftz)
nan_b = bool(int(nan)) if hasattr(nan, "value") else bool(nan)
ftz_suffix = ".ftz" if ftz_b else ""
nan_suffix = ".NaN" if nan_b else ""
return (
" float result;\n"
f' asm volatile("max{ftz_suffix}{nan_suffix}.f32 %0, %1, %2;"\n'
' : "=f"(result) : "f"(a), "f"(b));\n'
" return result;"
)
def _ptx_max_f32_name(a, b, ftz, nan):
ftz_b = bool(int(ftz)) if hasattr(ftz, "value") else bool(ftz)
nan_b = bool(int(nan)) if hasattr(nan, "value") else bool(nan)
suffix = ""
if ftz_b:
suffix += "_ftz"
if nan_b:
suffix += "_nan"
return f"tvm_builtin_ptx_max_f32{suffix}"
device_intrinsic(
"ptx_max_f32",
n_attrs=2,
helper_name=_ptx_max_f32_name,
c_signature=_BINARY_F32_SIG,
return_type="float",
body=_ptx_max_f32_body,
)
# =============================================================================
# CUDA-side warp / CTA reductions (templated butterfly shuffle-XOR).
# Emitted directly via ``cuda_func_call`` — the helper signature uses a
# single template parameter ``T`` for both arg and return, which doesn't
# match the operand-driven C signature pattern.
# =============================================================================
# (accumulation expression, identity value for cross-warp padding)
_OP_TABLE = {
"sum": ("val += shuffled;", "T(0)"),
"max": ("val = max(val, shuffled);", "-INFINITY"),
"min": ("val = min(val, shuffled);", "INFINITY"),
}
def _validate_op(op_str, context):
if op_str not in _OP_TABLE:
raise ValueError(f"Unsupported {context} op '{op_str}', expected one of {list(_OP_TABLE)}")
return _OP_TABLE[op_str]
def _warp_reduce_source(func_name, width_int, step_expr):
return (
f"\ntemplate <typename T>\n"
f"__forceinline__ __device__ T {func_name}(T val) {{\n"
f" #pragma unroll\n"
f" for (int mask = {width_int} >> 1; mask > 0; mask >>= 1) {{\n"
" T shuffled = __shfl_xor_sync(0xFFFFFFFF, val, mask);\n"
f" {step_expr}\n"
" }\n"
" return val;\n"
"}\n"
)
@register_codegen("cuda_warp_reduce")
def codegen_cuda_warp_reduce(value, op, width):
op_str = parse_str(op)
width_int = validate_power_of_two_range(width, 2, 32, "warp_reduce width")
step_expr, _ = _validate_op(op_str, "warp_reduce")
func_name = f"tvm_builtin_cuda_warp_reduce_{op_str}_{width_int}"
source_code = _warp_reduce_source(func_name, width_int, step_expr)
return cuda_func_call(func_name, value, source_code=source_code, return_type=value.ty)
@register_codegen("cuda_cta_reduce")
def codegen_cuda_cta_reduce(value, op, num_warps, scratch):
op_str = parse_str(op)
nw = validate_power_of_two_range(num_warps, 1, 32, "cta_reduce num_warps")
step_expr, identity = _validate_op(op_str, "cta_reduce")
warp_reduce_name = f"tvm_builtin_cuda_warp_reduce_{op_str}_32"
func_name = f"tvm_builtin_cuda_cta_reduce_{op_str}_{nw}"
cta_body = (
f"{_warp_reduce_source(warp_reduce_name, 32, step_expr)}"
"template <typename T>\n"
f"__forceinline__ __device__ T {func_name}(T val, void* scratch_raw) {{\n"
" T* scratch = reinterpret_cast<T*>(scratch_raw);\n"
f" val = {warp_reduce_name}(val);\n"
" int tid = threadIdx.x + threadIdx.y * blockDim.x"
" + threadIdx.z * blockDim.x * blockDim.y;\n"
" int warp_id = tid / 32;\n"
" int lane_id = tid % 32;\n"
" if (lane_id == 0) scratch[warp_id] = val;\n"
" __syncthreads();\n"
" if (warp_id == 0) {\n"
f" T partial = (lane_id < {nw}) ? scratch[lane_id] : {identity};\n"
f" partial = {warp_reduce_name}(partial);\n"
" if (lane_id == 0) scratch[0] = partial;\n"
" }\n"
" __syncthreads();\n"
" return scratch[0];\n"
"}\n"
)
return cuda_func_call(func_name, value, scratch, source_code=cta_body, return_type=value.ty)
# =============================================================================
# Additional FP8/BF16 packing, integer, and activation helpers.
# =============================================================================
# PTX integer bit-search form:
# fns.b32 d, mask, base, offset;
device_intrinsic(
"ptx_fns_b32",
helper_name="tvm_builtin_ptx_fns_b32",
c_signature="(unsigned int mask, unsigned int base, int offset)",
return_type="unsigned int",
body=(
" unsigned int ret;\n"
' asm("fns.b32 %0, %1, %2, %3;" : "=r"(ret) : "r"(mask), "r"(base), "r"(offset));\n'
" return ret;"
),
)
device_intrinsic(
"cuda_ffs_u32",
helper_name="tvm_builtin_ffs_u32",
c_signature="(unsigned int value)",
return_type="int",
body=" return __ffs(value);",
)
device_intrinsic(
"ptx_add_rn_f32_bf16",
helper_name="tvm_builtin_ptx_add_rn_f32_bf16",
c_signature="(float acc, unsigned short x)",
return_type="float",
body=(' asm("add.rn.f32.bf16 %0, %1, %0;" : "+f"(acc) : "h"(x));\n return acc;'),
)
device_intrinsic(
"cuda_make_float2",
helper_name="tvm_builtin_make_float2",
c_signature="(float x, float y)",
return_type="unsigned long long",
body=(
" float2 value = make_float2(x, y);\n"
" return *reinterpret_cast<unsigned long long*>(&value);"
),
)
device_intrinsic(
"cuda_float2_x",
helper_name="tvm_builtin_float2_x",
c_signature="(unsigned long long packed)",
return_type="float",
body=(" float2 value = *reinterpret_cast<float2*>(&packed);\n return value.x;"),
)
device_intrinsic(
"cuda_float2_y",
helper_name="tvm_builtin_float2_y",
c_signature="(unsigned long long packed)",
return_type="float",
body=(" float2 value = *reinterpret_cast<float2*>(&packed);\n return value.y;"),
)
device_intrinsic(
"cuda_fmul2_rn",
helper_name="tvm_builtin_fmul2_rn",
c_signature="(unsigned long long a, unsigned long long b)",
return_type="unsigned long long",
body=(
" float2 lhs = *reinterpret_cast<float2*>(&a);\n"
" float2 rhs = *reinterpret_cast<float2*>(&b);\n"
" float2 result = __fmul2_rn(lhs, rhs);\n"
" return *reinterpret_cast<unsigned long long*>(&result);"
),
)
device_intrinsic(
"cuda_fadd2_rn",
helper_name="tvm_builtin_fadd2_rn",
c_signature="(unsigned long long a, unsigned long long b)",
return_type="unsigned long long",
body=(
" float2 lhs = *reinterpret_cast<float2*>(&a);\n"
" float2 rhs = *reinterpret_cast<float2*>(&b);\n"
" float2 result = __fadd2_rn(lhs, rhs);\n"
" return *reinterpret_cast<unsigned long long*>(&result);"
),
)
device_intrinsic(
"cuda_float22bfloat162_rn",
helper_name="tvm_builtin_float22bfloat162_rn",
c_signature="(float x, float y)",
return_type="unsigned int",
body=(
" __nv_bfloat162 value = __float22bfloat162_rn(make_float2(x, y));\n"
" return *reinterpret_cast<unsigned int*>(&value);"
),
extra_deps=("bf16",),
)
device_intrinsic(
"cuda_float22bfloat162_rn_from_float2",
helper_name="tvm_builtin_float22bfloat162_rn_from_float2",
c_signature="(unsigned long long packed)",
return_type="unsigned int",
body=(
" float2 value = *reinterpret_cast<float2*>(&packed);\n"
" __nv_bfloat162 result = __float22bfloat162_rn(value);\n"
" return *reinterpret_cast<unsigned int*>(&result);"
),
extra_deps=("bf16",),
)
device_intrinsic(
"cuda_bfloat1622float2",
helper_name="tvm_builtin_bfloat1622float2",
c_signature="(unsigned int packed)",
return_type="unsigned long long",
body=(
" __nv_bfloat162 value;\n"
" *reinterpret_cast<unsigned int*>(&value) = packed;\n"
" float2 result = __bfloat1622float2(value);\n"
" return *reinterpret_cast<unsigned long long*>(&result);"
),
extra_deps=("bf16",),
)
device_intrinsic(
"cuda_hmin2",
helper_name="tvm_builtin_hmin2",
c_signature="(unsigned int a, unsigned int b)",
return_type="unsigned int",
body=(
" __nv_bfloat162 lhs;\n"
" __nv_bfloat162 rhs;\n"
" *reinterpret_cast<unsigned int*>(&lhs) = a;\n"
" *reinterpret_cast<unsigned int*>(&rhs) = b;\n"
" __nv_bfloat162 result = __hmin2(lhs, rhs);\n"
" return *reinterpret_cast<unsigned int*>(&result);"
),
extra_deps=("bf16",),
)
device_intrinsic(
"cuda_hmax2",
helper_name="tvm_builtin_hmax2",
c_signature="(unsigned int a, unsigned int b)",
return_type="unsigned int",
body=(
" __nv_bfloat162 lhs;\n"
" __nv_bfloat162 rhs;\n"
" *reinterpret_cast<unsigned int*>(&lhs) = a;\n"
" *reinterpret_cast<unsigned int*>(&rhs) = b;\n"
" __nv_bfloat162 result = __hmax2(lhs, rhs);\n"
" return *reinterpret_cast<unsigned int*>(&result);"
),
extra_deps=("bf16",),
)
device_intrinsic(
"cuda_fp8x4_e4m3_from_float4",
helper_name="tvm_builtin_fp8x4_e4m3_from_float4",
c_signature="(float x, float y, float z, float w)",
return_type="unsigned int",
body=(
" __nv_fp8x4_e4m3 result = __nv_fp8x4_e4m3(make_float4(x, y, z, w));\n"
" return *reinterpret_cast<unsigned int*>(&result);"
),
extra_deps=("fp8",),
)
@@ -0,0 +1,955 @@
# 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.
# ruff: noqa: E501
# pylint: disable=redefined-builtin, invalid-name, too-many-arguments
"""Memory ops (load / store / copy / atomic / address conversion / type punning).
PTX side:
* ``ld.acquire.scope{.ss}.type`` scalar load forms.
* ``ld.volatile{.ss}.type`` scalar load forms.
* Legacy ``ld.global.acquire.gpu`` / ``ld.global.cg`` result-argument helper.
* ``mapa.u64`` — map a SMEM ptr to a peer CTA's SMEM in the cluster.
CUDA side:
* ``__ldg`` (cache-as-read-only load).
* Templated ``atomicAdd`` / ``atomicCAS``.
* half↔float type-punned conversions (single, packed, batch-of-8).
* ``__cvta_generic_to_shared`` and ``cluster_addr → shared u32`` casts.
"""
from tvm import DataType
from tvm.backend.cuda.op import cuda_func_call
from ._schema import device_intrinsic
from .registry import CODEGEN_REGISTRY, register_codegen
from .utils import parse_str
# =============================================================================
# __ldg — templated read-only cached load; ``T`` resolved at call time from
# the ``dtype`` argument. Hand-written because the helper signature uses a
# template parameter for both arg and return.
# =============================================================================
@register_codegen("cuda_ldg")
def codegen_cuda_ldg(addr, dtype):
dtype = DataType(parse_str(dtype))
func_name = "tvm_builtin_cuda_ldg"
source_code = f"""
template <typename T>
__forceinline__ __device__ T {func_name}(T* src) {{
return __ldg(src);
}}
"""
return cuda_func_call(func_name, addr, source_code=source_code, return_type=dtype)
# Shared PTX scalar type metadata (ld/st/red/atom).
_PTX_SCALAR_TYPE_INFO = {
"b8": ("unsigned int", "r", "uint32"),
"u8": ("unsigned int", "r", "uint32"),
"s8": ("int", "r", "int32"),
"b16": ("unsigned short", "h", "uint16"),
"u16": ("unsigned short", "h", "uint16"),
"s16": ("short", "h", "int16"),
"b32": ("unsigned int", "r", "uint32"),
"u32": ("unsigned int", "r", "uint32"),
"s32": ("int", "r", "int32"),
"b64": ("unsigned long long", "l", "uint64"),
"u64": ("unsigned long long", "l", "uint64"),
"s64": ("long long", "l", "int64"),
"f32": ("float", "f", "float32"),
"f64": ("double", "d", "float64"),
}
_PTX_LD_TYPE_RETURNS = {
"b32": {"uint32": "unsigned int", "int32": "int"},
"b64": {"uint64": "unsigned long long", "int64": "long long"},
}
_PTX_VEC_STORE_TYPE = {
16: "uint4",
8: "uint2",
4: "unsigned int",
2: "unsigned short",
1: "unsigned char",
}
def _safe_attr(value):
return parse_str(value).replace("::", "_").replace(".", "_")
def _dot(value):
value = parse_str(value)
return f".{value}" if value else ""
def _cache_suffix(cache):
return ".L2::cache_hint" if cache else ""
def _type_info(ptx_type):
ptx_type = parse_str(ptx_type)
if ptx_type not in _PTX_SCALAR_TYPE_INFO:
raise ValueError(
f"Unsupported PTX scalar type {ptx_type!r}; expected {sorted(_PTX_SCALAR_TYPE_INFO)}"
)
return (ptx_type, *_PTX_SCALAR_TYPE_INFO[ptx_type])
# =============================================================================
# PTX ld forms (ISA table entries registered via ``ptx_ld`` and siblings):
# ld{.weak}{.ss}{.cop}{.level::cache_hint}{.level::prefetch_size}{.vec}.type d, [a]{, cache-policy};
# ld{.weak}{.ss}{.level1::eviction_priority}{.level2::eviction_priority}{.level::cache_hint}{.level::prefetch_size}{.vec}.type d, [a]{, cache-policy};
# ld.volatile{.ss}{.level::prefetch_size}{.vec}.type d, [a];
# ld.relaxed.scope{.ss}{.level1::eviction_priority}{.level2::eviction_priority}{.level::cache_hint}{.level::prefetch_size}{.vec}.type d, [a]{, cache-policy};
# ld.acquire.scope{.ss}{.level1::eviction_priority}{.level2::eviction_priority}{.level::cache_hint}{.level::prefetch_size}{.vec}.type d, [a]{, cache-policy};
# ld.mmio.sem.sys{.global}.type d, [a];
# =============================================================================
_PTX_LD_SCOPES = {"cta", "cluster", "gpu", "sys"}
_PTX_LD_SPACES = {"global", "shared", "shared::cta", "shared::cluster", "local"}
_PTX_LD_VOLATILE_SPACES = _PTX_LD_SPACES | {"const"}
_PTX_LD_WEAK_SPACES = _PTX_LD_SPACES | {"const", "param::entry", "param::func"}
_PTX_LD_COPS = {"", "ca", "cg", "cs", "lu", "cv"}
_PTX_VEC = {"", "v2", "v4", "v8"}
_PTX_L1_EVICT = {
"",
"L1::evict_normal",
"L1::evict_unchanged",
"L1::evict_first",
"L1::evict_last",
"L1::no_allocate",
}
_PTX_L2_EVICT = {"", "L2::evict_normal", "L2::evict_first", "L2::evict_last"}
_PTX_PREFETCH = {"", "L2::64B", "L2::128B", "L2::256B"}
def _bool_attr(value):
return bool(int(value)) if hasattr(value, "value") else bool(value)
def _parse_ld_attrs(return_dtype, ptx_type, scope=None, space="global"):
return_dtype = parse_str(return_dtype)
ptx_type = parse_str(ptx_type)
scope = None if scope is None else parse_str(scope)
space = parse_str(space)
ptx_type, _ptx, constraint, default_tvm = _type_info(ptx_type)
if ptx_type in _PTX_LD_TYPE_RETURNS:
returns = _PTX_LD_TYPE_RETURNS[ptx_type]
if return_dtype not in returns:
raise ValueError(
f"PTX ld type {ptx_type!r} cannot return TVM dtype {return_dtype!r}; "
f"expected one of {sorted(returns)}"
)
c_type = returns[return_dtype]
else:
if return_dtype != default_tvm:
raise ValueError(
f"PTX ld type {ptx_type!r} cannot return TVM dtype {return_dtype!r}; "
f"expected {default_tvm!r}"
)
c_type = _ptx
if scope is not None and scope not in _PTX_LD_SCOPES:
raise ValueError(
f"Unsupported PTX ld scope {scope!r}; expected one of {sorted(_PTX_LD_SCOPES)}"
)
return return_dtype, ptx_type, scope, space, c_type, constraint
def _validate_ld_space(space: str, allowed: set[str]) -> None:
if space not in allowed:
raise ValueError(
f"Unsupported PTX ld state space {space!r}; expected one of {sorted(allowed)}"
)
def _ptx_level_suffix(has_cache, l1_evict, l2_evict, prefetch_size):
suffix = _cache_suffix("cache" if has_cache else "")
l1_evict = parse_str(l1_evict)
l2_evict = parse_str(l2_evict)
prefetch_size = parse_str(prefetch_size)
if l1_evict:
suffix += f".{l1_evict}"
if l2_evict:
suffix += f".{l2_evict}"
if prefetch_size:
suffix += f".{prefetch_size}"
return suffix
def _ptx_shared_addr(space, ptr_name="address"):
if parse_str(space).startswith("shared"):
return (
f" unsigned int addr = (unsigned int)__cvta_generic_to_shared({ptr_name});\n",
'"r"(addr)',
)
return "", f'"l"({ptr_name})'
def _ptx_ld_vec_store(num_bytes, vec_len, ptx_type):
if ptx_type == "u8" and vec_len == 1:
return " *reinterpret_cast<unsigned char*>(dst_ptr) = static_cast<unsigned char>(r0);"
store_type = _PTX_VEC_STORE_TYPE[num_bytes]
if vec_len > 1:
return (
f" *reinterpret_cast<{store_type}*>(dst_ptr) = "
+ "{"
+ ", ".join(f"r{i}" for i in range(vec_len))
+ "};"
)
return f" *reinterpret_cast<{store_type}*>(dst_ptr) = r0;"
def _ptx_ld_form_parts(form, attr_args):
if form == "weak":
(
return_dtype,
weak,
space,
cop,
vec,
ptx_type,
has_cache_hint,
to_dst,
l1_evict,
l2_evict,
prefetch_size,
) = attr_args
sem, scope = "", ""
elif form == "relaxed":
(
return_dtype,
scope,
space,
vec,
ptx_type,
has_cache_hint,
to_dst,
l1_evict,
l2_evict,
prefetch_size,
) = attr_args
sem, weak, cop = "", False, ""
elif form == "acquire":
(
return_dtype,
scope,
space,
vec,
ptx_type,
has_cache_hint,
to_dst,
l1_evict,
l2_evict,
prefetch_size,
) = attr_args
sem, weak, cop = "", False, ""
elif form == "volatile":
return_dtype, space, vec, ptx_type, to_dst, prefetch_size = attr_args
sem, scope, weak, cop = "", "", False, ""
has_cache_hint, l1_evict, l2_evict = False, "", ""
elif form == "mmio":
return_dtype, sem, scope, space, ptx_type, to_dst = attr_args
weak, cop, vec = False, "", ""
has_cache_hint, l1_evict, l2_evict, prefetch_size = False, "", "", ""
else:
raise ValueError(f"unknown ld form {form!r}")
return_dtype, ptx_type, scope, space, c_type, constraint = _parse_ld_attrs(
return_dtype, ptx_type, scope if form in ("relaxed", "acquire") else None, space
)
sem = parse_str(sem)
scope = parse_str(scope)
space = parse_str(space)
cop = parse_str(cop)
vec = parse_str(vec)
l1_evict = parse_str(l1_evict)
l2_evict = parse_str(l2_evict)
prefetch_size = parse_str(prefetch_size)
weak = _bool_attr(weak)
has_cache = _bool_attr(has_cache_hint)
to_dst = _bool_attr(to_dst)
if cop and cop not in _PTX_LD_COPS:
raise ValueError(f"Unsupported PTX ld cache operation {cop!r}")
if vec and vec not in _PTX_VEC:
raise ValueError(f"Unsupported PTX ld vector modifier {vec!r}")
if l1_evict and l1_evict not in _PTX_L1_EVICT:
raise ValueError(f"Unsupported PTX ld L1 eviction {l1_evict!r}")
if l2_evict and l2_evict not in _PTX_L2_EVICT:
raise ValueError(f"Unsupported PTX ld L2 eviction {l2_evict!r}")
if prefetch_size and prefetch_size not in _PTX_PREFETCH:
raise ValueError(f"Unsupported PTX ld prefetch size {prefetch_size!r}")
if form == "mmio":
if sem not in ("acquire", "relaxed") or scope != "sys" or space != "global":
raise ValueError("ld.mmio requires sem in {acquire, relaxed}, scope=sys, space=global")
prefix = f"ld.mmio.{sem}.{scope}"
elif form == "relaxed":
if not scope:
raise ValueError("ld.relaxed requires scope")
_validate_ld_space(space, _PTX_LD_SPACES)
prefix = f"ld.relaxed.{scope}{_dot(space)}"
elif form == "acquire":
if not scope:
raise ValueError("ld.acquire requires scope")
_validate_ld_space(space, _PTX_LD_SPACES)
prefix = f"ld.acquire.{scope}{_dot(space)}"
elif form == "volatile":
_validate_ld_space(space, _PTX_LD_VOLATILE_SPACES)
prefix = f"ld.volatile{_dot(space)}"
else:
_validate_ld_space(space, _PTX_LD_WEAK_SPACES)
prefix = f"ld{'.weak' if weak else ''}{_dot(space)}{_dot(cop)}"
level = _ptx_level_suffix(has_cache, l1_evict, l2_evict, prefetch_size)
vec_len = int(vec[1:]) if vec else 1
if vec and not to_dst:
raise ValueError("vector ld requires to_dst")
elem_bytes = (
8
if ptx_type.endswith("64")
else 2
if ptx_type in ("u16", "s16", "b16")
else 1
if ptx_type in ("u8", "s8", "b8")
else 4
)
num_bytes = vec_len * elem_bytes if vec else elem_bytes
name_parts = [
"tvm_builtin_ptx_ld",
form if form != "weak" else ("weak" if weak else "plain"),
]
if sem:
name_parts.append(_safe_attr(sem))
if scope:
name_parts.append(_safe_attr(scope))
name_parts.extend(
[
_safe_attr(space),
_safe_attr(cop) if cop else "",
_safe_attr(vec) if vec else "",
ptx_type,
return_dtype if not to_dst else "to_dst",
]
)
if has_cache:
name_parts.append("cache_hint")
if l1_evict:
name_parts.append(_safe_attr(l1_evict))
if l2_evict:
name_parts.append(_safe_attr(l2_evict))
if prefetch_size:
name_parts.append(_safe_attr(prefetch_size))
name = "_".join(p for p in name_parts if p)
cache_operand = ', "l"(cache_policy)' if has_cache else ""
addr_decl, addr_operand = _ptx_shared_addr(space, "src_ptr" if to_dst else "address")
if to_dst:
reg_decls = "".join(f" {c_type} r{i};\n" for i in range(vec_len))
if vec_len > 1:
out_slot = "{" + ", ".join(f"%{i}" for i in range(vec_len)) + "}"
out_constraints = ", ".join(f'"={constraint}"(r{i})' for i in range(vec_len))
addr_idx = vec_len
else:
out_slot = "%0"
out_constraints = f'"={constraint}"(r0)'
addr_idx = 1
cache_slot = f", %{addr_idx + 1}" if has_cache else ""
instr = f"{prefix}{level}{_dot(vec)}.{ptx_type}"
body = (
f"{addr_decl}{reg_decls}"
f' asm volatile("{instr} {out_slot}, [%{addr_idx}]{cache_slot};"\n'
f" : {out_constraints}\n"
f" : {addr_operand}{cache_operand});\n"
f"{_ptx_ld_vec_store(num_bytes, vec_len, ptx_type)}"
)
return (
name,
"(void* dst_ptr, void* src_ptr, unsigned long long cache_policy)",
"void",
"",
body,
)
cache_slot = ", %2" if has_cache else ""
instr = f"{prefix}{level}{_dot(vec)}.{ptx_type}"
body = (
f" {c_type} ret;\n"
f"{addr_decl}"
f' asm volatile("{instr} %0, [%1]{cache_slot};"\n'
f' : "={constraint}"(ret)\n'
f" : {addr_operand}{cache_operand});\n"
" return ret;"
)
sig = (
"(void* address, unsigned long long cache_policy)" if form == "weak" else "(void* address)"
)
return name, sig, c_type, return_dtype, body
def _register_ptx_ld(op_name, form, n_attrs):
def _parts(*args):
return _ptx_ld_form_parts(form, args[-n_attrs:])
device_intrinsic(
op_name,
n_attrs=n_attrs,
helper_name=lambda *a, _p=_parts: _p(*a)[0],
c_signature=lambda *a, _p=_parts: _p(*a)[1],
return_type=lambda *a, _p=_parts: _p(*a)[2],
tvm_return_type=lambda *a, _p=_parts: (
None if _p(*a)[2] == "void" else parse_str(a[-n_attrs])
),
body=lambda *a, _p=_parts: _p(*a)[4],
)
_register_ptx_ld("ptx_ld", "weak", 11)
_register_ptx_ld("ptx_ld_relaxed", "relaxed", 10)
_register_ptx_ld("ptx_ld_acquire", "acquire", 10)
_register_ptx_ld("ptx_ld_volatile", "volatile", 6)
_register_ptx_ld("ptx_ld_mmio", "mmio", 6)
# =============================================================================
# Legacy acquire-load lvalue API — compatibility wrapper over
# ``ld.acquire.gpu.global`` / ``ld.global.cg`` forms, dispatched on dtype.
# Wrapper picks .b32/.b64 + matching constraint by dtype.
#
# The body uses ``#if __CUDA_ARCH__ >= 700`` to select acquire on SM70+ and
# fall back to .cg on older arches. This is two PTX form table entries
# combined in one device helper for arch portability.
# =============================================================================
_LD_GLOBAL_ACQUIRE_DTYPES = {
"uint32": ("uint32_t", "b32", "r"),
"int32": ("int32_t", "b32", "r"),
"uint64": ("uint64_t", "b64", "l"),
"int64": ("int64_t", "b64", "l"),
}
def _ld_global_acquire_body(ptx_type: str, spec: str) -> str:
return (
" #if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 700\n"
f' asm volatile ("ld.acquire.gpu.global.{ptx_type} %0, [%1];\\n"\n'
f' : "={spec}"(res) : "l"(addr));\n'
" #else\n"
f' asm volatile ("ld.global.cg.{ptx_type} %0, [%1];\\n"\n'
f' : "={spec}"(res) : "l"(addr));\n'
" #endif"
)
for _dtype, (_c_type, _ptx_type, _spec) in _LD_GLOBAL_ACQUIRE_DTYPES.items():
device_intrinsic(
f"ptx_ld_global_acquire_{_dtype}",
c_signature=f"({_c_type}& res, {_c_type}* addr)",
body=_ld_global_acquire_body(_ptx_type, _spec),
)
del _dtype, _c_type, _ptx_type, _spec
@register_codegen("ptx_ld_global_acquire")
def codegen_ptx_ld_global_acquire(res, addr):
"""Dispatch to the dtype-specific helper."""
dtype = str(res.ty)
if dtype not in _LD_GLOBAL_ACQUIRE_DTYPES:
raise ValueError(f"Unsupported data type for ld.global.acquire: {dtype}")
result = CODEGEN_REGISTRY[f"tirx.ptx_ld_global_acquire_{dtype}"]([res, addr])
return result[0] if isinstance(result, tuple) else result
# =============================================================================
# Atomics — templated wrappers around CUDA's ``atomicAdd`` / ``atomicCAS``.
# =============================================================================
device_intrinsic(
"cuda_atomic_add",
helper_name="tvm_builtin_cuda_atomic_add",
c_signature="(T* addr, T value)",
body=" return atomicAdd(addr, value);",
return_type="T",
templated=True,
tvm_return_type=lambda _addr, value: value.ty,
)
device_intrinsic(
"cuda_atomic_cas",
helper_name="tvm_builtin_cuda_atomic_cas",
c_signature="(T* address, T compare, T val)",
body=" return atomicCAS(address, compare, val);",
return_type="T",
templated=True,
tvm_return_type=lambda _p, old, _n: old.ty,
)
# =============================================================================
# half / bfloat16 ↔ float type-punned conversions.
# =============================================================================
device_intrinsic(
"cuda_half2float",
c_signature="(half src)",
body=" return __half2float(src);",
return_type="float",
tvm_return_type="float32",
)
device_intrinsic(
"cuda_bfloat162float",
c_signature="(nv_bfloat16 src)",
body=" return __bfloat162float(src);",
return_type="float",
tvm_return_type="float32",
)
device_intrinsic(
"cuda_float22half2",
c_signature="(void* dst, void* src)",
body=(
" half2* dst_p = (half2*) dst;\n"
" float2* src_p = (float2*) src;\n"
" *dst_p = __float22half2_rn(*src_p);"
),
)
device_intrinsic(
"cuda_half8tofloat8",
c_signature="(void* src_addr, void* dst_addr)",
body=(
" half2* source = (half2*) src_addr;\n"
" float2* dest = (float2*) dst_addr;\n"
" for (int i = 0; i < 4; i++) {\n"
" dest[i] = __half22float2(source[i]);\n"
" }"
),
)
device_intrinsic(
"cuda_float8tohalf8",
c_signature="(void* src_addr, void* dst_addr)",
body=(
" float2* source = (float2*) src_addr;\n"
" half2* dest = (half2*) dst_addr;\n"
" for (int i = 0; i < 4; i++) {\n"
" dest[i] = __float22half2_rn(source[i]);\n"
" }"
),
)
# =============================================================================
# Address-conversion helpers used by op-wrapper-side dispatch in tvm.tirx.op.
# Each precomputes a value that the schema's specialized op then takes as a
# typed scalar input (instead of doing the conversion inside the asm helper).
# =============================================================================
device_intrinsic(
"cuda_cvta_generic_to_shared",
c_signature="(void* p)",
body=" return __cvta_generic_to_shared(p);",
return_type="unsigned int",
tvm_return_type="uint32",
)
device_intrinsic(
"cuda_smem_addr_from_uint64",
c_signature="(uint64_t cluster_addr)",
body=" return static_cast<unsigned int>(cluster_addr);",
return_type="unsigned int",
tvm_return_type="uint32",
)
# =============================================================================
# PTX mapa form:
# mapa{.space}.type d, a, b;
# .space = {.shared::cluster}; .type = {.u32, .u64}
# =============================================================================
def _ptx_mapa_parts(_addr, _rank, space, ptx_type, return_dtype):
space = parse_str(space)
ptx_type = parse_str(ptx_type)
return_dtype = parse_str(return_dtype)
if space not in ("", "shared::cluster"):
raise ValueError(f"Unsupported mapa space {space!r}")
if ptx_type not in ("u32", "u64"):
raise ValueError(f"Unsupported mapa type {ptx_type!r}")
c_type = "uint32_t" if ptx_type == "u32" else "uint64_t"
constraint = "r" if ptx_type == "u32" else "l"
name = f"tvm_builtin_ptx_mapa{('_' + _safe_attr(space)) if space else ''}_{ptx_type}"
body = (
f" {c_type} result;\n"
f' asm volatile("mapa{_dot(space)}.{ptx_type} %0, %1, %2;"\n'
f' : "={constraint}"(result) : "l"(addr), "r"(rank));\n'
" return result;"
)
return name, c_type, return_dtype, body
device_intrinsic(
"ptx_mapa",
n_attrs=3,
helper_name=lambda *a: _ptx_mapa_parts(*a)[0],
c_signature="(void* addr, uint32_t rank)",
return_type=lambda *a: _ptx_mapa_parts(*a)[1],
tvm_return_type=lambda *a: _ptx_mapa_parts(*a)[2],
body=lambda *a: _ptx_mapa_parts(*a)[3],
)
# =============================================================================
# Generic PTX memory forms. Compatibility wrappers in ``tvm.tirx.op`` bind
# concrete sem/scope/space/op/type parameters for existing call sites.
# =============================================================================
# PTX red scalar form:
# red{.sem}{.scope}{.space}.op{.level::cache_hint}.type [a], b{, cache-policy};
def _ptx_red_scalar_parts(*args):
sem, scope, space, op, ptx_type, has_cache_hint = args[-6:]
sem = parse_str(sem)
scope = parse_str(scope)
space = parse_str(space)
op = parse_str(op)
ptx_type, c_type, constraint, _tvm_dtype = _type_info(ptx_type)
has_cache = (
bool(int(has_cache_hint)) if hasattr(has_cache_hint, "value") else bool(has_cache_hint)
)
modifiers = f"{_dot(sem)}{_dot(scope)}{_dot(space)}"
instr = f"red{modifiers}.{op}{_cache_suffix('cache' if has_cache else '')}.{ptx_type}"
name = (
"tvm_builtin_ptx_red_scalar"
f"{_dot(sem).replace('.', '_')}{_dot(scope).replace('.', '_')}"
f"_{_safe_attr(space)}_{op}_{ptx_type}{'_cache_hint' if has_cache else ''}"
)
cache_operand = ', "l"(cache_policy)' if has_cache else ""
addr_decl = ""
addr_operand = '"l"(address)'
if space.startswith("shared"):
addr_decl = " unsigned int addr = (unsigned int)__cvta_generic_to_shared(address);\n"
addr_operand = '"r"(addr)'
body = (
f"{addr_decl}"
f' asm volatile("{instr} [%0], %1{", %2" if has_cache else ""};"\n'
" :\n"
f' : {addr_operand}, "{constraint}"(value)'
f"{cache_operand}\n"
' : "memory");'
)
return name, f"(void* address, {c_type} value, unsigned long long cache_policy)", body
device_intrinsic(
"ptx_red_scalar",
n_attrs=6,
helper_name=lambda *a: _ptx_red_scalar_parts(*a)[0],
c_signature=lambda *a: _ptx_red_scalar_parts(*a)[1],
body=lambda *a: _ptx_red_scalar_parts(*a)[2],
)
# PTX atom scalar one-source-operand form:
# atom{.sem}{.scope}{.space}.op{.level::cache_hint}.type d, [a], b{, cache-policy};
def _ptx_atom_scalar_parts(*args):
sem, scope, space, op, ptx_type, has_cache_hint = args[-6:]
sem = parse_str(sem)
scope = parse_str(scope)
space = parse_str(space)
op = parse_str(op)
ptx_type, c_type, constraint, tvm_dtype = _type_info(ptx_type)
has_cache = (
bool(int(has_cache_hint)) if hasattr(has_cache_hint, "value") else bool(has_cache_hint)
)
modifiers = f"{_dot(sem)}{_dot(scope)}{_dot(space)}"
instr = f"atom{modifiers}.{op}{_cache_suffix('cache' if has_cache else '')}.{ptx_type}"
name = (
"tvm_builtin_ptx_atom_scalar"
f"{_dot(sem).replace('.', '_')}{_dot(scope).replace('.', '_')}"
f"_{_safe_attr(space)}_{op}_{ptx_type}{'_cache_hint' if has_cache else ''}"
)
cache_operand = ', "l"(cache_policy)' if has_cache else ""
addr_decl = ""
addr_operand = '"l"(address)'
if space.startswith("shared"):
addr_decl = " unsigned int addr = (unsigned int)__cvta_generic_to_shared(address);\n"
addr_operand = '"r"(addr)'
body = (
f"{addr_decl}"
f" {c_type} ret;\n"
f' asm volatile("{instr} %0, [%1], %2{", %3" if has_cache else ""};"\n'
f' : "={constraint}"(ret)\n'
f' : {addr_operand}, "{constraint}"(value)'
f"{cache_operand}\n"
' : "memory");\n'
" return ret;"
)
return (
name,
f"(void* address, {c_type} value, unsigned long long cache_policy)",
c_type,
tvm_dtype,
body,
)
device_intrinsic(
"ptx_atom_scalar",
n_attrs=6,
helper_name=lambda *a: _ptx_atom_scalar_parts(*a)[0],
c_signature=lambda *a: _ptx_atom_scalar_parts(*a)[1],
return_type=lambda *a: _ptx_atom_scalar_parts(*a)[2],
tvm_return_type=lambda *a: _ptx_atom_scalar_parts(*a)[3],
body=lambda *a: _ptx_atom_scalar_parts(*a)[4],
)
# PTX prefetch tensormap form:
# prefetch{.tensormap_space}.tensormap [a];
def _prefetch_tensormap_parts(_tensor_map, tensormap_space):
space = parse_str(tensormap_space)
instr = f"prefetch{_dot(space)}.tensormap"
name = f"tvm_builtin_ptx_prefetch{('_' + _safe_attr(space)) if space else ''}_tensormap"
body = (
f' asm volatile("{instr} [%0];"\n'
" :\n"
' : "l"(tensor_map_addr)\n'
' : "memory");'
)
return name, body
device_intrinsic(
"ptx_prefetch_tensormap",
n_attrs=1,
helper_name=lambda *a: _prefetch_tensormap_parts(*a)[0],
c_signature="(unsigned long long tensor_map_addr)",
body=lambda *a: _prefetch_tensormap_parts(*a)[1],
)
# PTX st forms (ISA table entries registered via ``ptx_st`` and siblings):
# st{.weak}{.ss}{.cop}{.level::cache_hint}{.vec}.type [a], b{, cache-policy};
# st{.weak}{.ss}{.level1::eviction_priority}{.level2::eviction_priority}{.level::cache_hint}{.vec}.type [a], b{, cache-policy};
# st.volatile{.ss}{.vec}.type [a], b;
# st.relaxed.scope{.ss}{.level1::eviction_priority}{.level2::eviction_priority}{.level::cache_hint}{.vec}.type [a], b{, cache-policy};
# st.release.scope{.ss}{.level1::eviction_priority}{.level2::eviction_priority}{.level::cache_hint}{.vec}.type [a], b{, cache-policy};
# st.mmio.sem.sys{.global}.type [a], b;
_PTX_ST_COPS = {"", "wb", "cg", "cs", "wt"}
_PTX_ST_SPACES = {"global", "shared", "shared::cta", "shared::cluster", "local", "param::func"}
def _ptx_st_load_src(num_bytes, vec_len, ptx_type, c_type):
if ptx_type == "u8" and vec_len == 1:
return " unsigned int r0 = *reinterpret_cast<unsigned char*>(src_ptr);\n"
store_type = _PTX_VEC_STORE_TYPE[num_bytes]
if vec_len > 1:
return f" {store_type} src_ = *reinterpret_cast<{store_type}*>(src_ptr);\n" + "".join(
f" {c_type} r{i} = src_.{c};\n" for i, c in enumerate("xyzw"[:vec_len])
)
return f" {c_type} r0 = *reinterpret_cast<{c_type}*>(src_ptr);\n"
def _ptx_st_form_parts(form, attr_args, from_src):
if form == "weak":
weak, space, cop, vec, ptx_type, has_cache_hint, l1_evict, l2_evict = attr_args
sem, scope = "", ""
elif form == "relaxed":
scope, space, vec, ptx_type, has_cache_hint, l1_evict, l2_evict = attr_args
sem, weak, cop = "relaxed", False, ""
elif form == "release":
scope, space, vec, ptx_type, has_cache_hint, l1_evict, l2_evict = attr_args
sem, weak, cop = "release", False, ""
elif form == "volatile":
space, vec, ptx_type = attr_args
sem, scope, weak, cop = "", "", False, ""
has_cache_hint, l1_evict, l2_evict = False, "", ""
elif form == "mmio":
sem, scope, space, ptx_type = attr_args
weak, cop, vec = False, "", ""
has_cache_hint, l1_evict, l2_evict = False, "", ""
else:
raise ValueError(f"unknown st form {form!r}")
sem = parse_str(sem)
scope = parse_str(scope)
space = parse_str(space)
cop = parse_str(cop)
vec = parse_str(vec)
l1_evict = parse_str(l1_evict)
l2_evict = parse_str(l2_evict)
weak = _bool_attr(weak)
has_cache = _bool_attr(has_cache_hint)
ptx_type, c_type, constraint, _tvm_dtype = _type_info(ptx_type)
if cop and cop not in _PTX_ST_COPS:
raise ValueError(f"Unsupported PTX st cache operation {cop!r}")
if vec and vec not in _PTX_VEC:
raise ValueError(f"Unsupported PTX st vector modifier {vec!r}")
if space not in _PTX_ST_SPACES and not (form == "mmio" and space == "global"):
raise ValueError(f"Unsupported PTX st state space {space!r}")
vec_len = int(vec[1:]) if vec else 1
elem_bytes = (
8
if ptx_type.endswith("64")
else 2
if ptx_type in ("u16", "s16", "b16")
else 1
if ptx_type in ("u8", "s8", "b8")
else 4
)
num_bytes = vec_len * elem_bytes if vec else elem_bytes
use_cache_policy = form in ("weak", "relaxed", "release")
if form == "mmio":
if sem not in ("acquire", "relaxed", "release") or scope != "sys" or space != "global":
raise ValueError("st.mmio requires sem, scope=sys, space=global")
prefix = f"st.mmio.{sem}.{scope}"
elif form == "relaxed":
if not scope:
raise ValueError("st.relaxed requires scope")
prefix = f"st.relaxed.{scope}{_dot(space)}"
elif form == "release":
if not scope:
raise ValueError("st.release requires scope")
prefix = f"st.release.{scope}{_dot(space)}"
elif form == "volatile":
prefix = f"st.volatile{_dot(space)}"
else:
prefix = f"st{'.weak' if weak else ''}{_dot(space)}{_dot(cop)}"
level = _ptx_level_suffix(has_cache, l1_evict, l2_evict, "")
instr = f"{prefix}{level}{_dot(vec)}.{ptx_type}"
name_parts = ["tvm_builtin_ptx_st", form if form != "weak" else ("weak" if weak else "plain")]
if sem:
name_parts.append(_safe_attr(sem))
if scope:
name_parts.append(_safe_attr(scope))
name_parts.extend(
[
_safe_attr(space),
_safe_attr(cop) if cop else "",
_safe_attr(vec) if vec else "",
ptx_type,
"from_src" if from_src else "values",
]
)
if has_cache:
name_parts.append("cache_hint")
name = "_".join(p for p in name_parts if p)
values = f"{{{', '.join(f'%{i + 1}' for i in range(vec_len))}}}" if vec_len > 1 else "%1"
value_constraints = "".join(f', "{constraint}"(value{i})' for i in range(vec_len))
cache_slot = f", %{vec_len + 1}" if has_cache else ""
cache_operand = ', "l"(cache_policy)' if has_cache else ""
addr_decl, addr_operand = _ptx_shared_addr(space, "address")
if from_src:
load_regs = _ptx_st_load_src(num_bytes, vec_len, ptx_type, c_type)
if vec_len > 1:
in_constraints = ", ".join(f'"{constraint}"(r{i})' for i in range(vec_len))
value_args = f", {in_constraints}"
else:
value_args = f', "{constraint}"(r0)'
body = (
f"{addr_decl}{load_regs}"
f' asm volatile("{instr} [%0], {values}{cache_slot};"\n'
" :\n"
f" : {addr_operand}{value_args}{cache_operand}\n"
' : "memory");'
)
if use_cache_policy:
sig = "(void* address, void* src_ptr, unsigned long long cache_policy)"
else:
sig = "(void* address, void* src_ptr)"
else:
body = (
f"{addr_decl}"
f' asm volatile("{instr} [%0], {values}{cache_slot};"\n'
" :\n"
f" : {addr_operand}{value_constraints}{cache_operand}\n"
' : "memory");'
)
if form == "mmio":
sig = f"(void* address, {c_type} value0)"
elif use_cache_policy:
value_params = ", ".join(f"{c_type} value{i}" for i in range(vec_len))
sig = f"(void* address, {value_params}, unsigned long long cache_policy)"
else:
value_params = ", ".join(f"{c_type} value{i}" for i in range(vec_len))
sig = f"(void* address, {value_params})"
return name, sig, body
def _register_ptx_st(op_name, form, n_attrs, *, with_cache_policy=True):
def codegen(*args):
from_src = _bool_attr(args[-1])
st_attrs = args[-n_attrs:-1]
parts = _ptx_st_form_parts(form, st_attrs, from_src)
forward = args[:-(n_attrs)]
name, sig, body_str = parts
source_code = f"\n__forceinline__ __device__ void {name}{sig} {{\n{body_str}\n}}\n"
return cuda_func_call(name, *forward, source_code=source_code)
codegen.__name__ = f"codegen_{op_name}"
register_codegen(op_name)(codegen)
def _register_ptx_st_mmio(op_name, form, n_attrs):
def codegen(*args):
parts = _ptx_st_form_parts(form, args[-n_attrs:], False)
forward = args[:-n_attrs]
name, sig, body_str = parts
source_code = f"\n__forceinline__ __device__ void {name}{sig} {{\n{body_str}\n}}\n"
return cuda_func_call(name, *forward, source_code=source_code)
codegen.__name__ = f"codegen_{op_name}"
register_codegen(op_name)(codegen)
_register_ptx_st("ptx_st", "weak", 9)
_register_ptx_st("ptx_st_relaxed", "relaxed", 8)
_register_ptx_st("ptx_st_release", "release", 8)
_register_ptx_st("ptx_st_volatile", "volatile", 4)
_register_ptx_st_mmio("ptx_st_mmio", "mmio", 4)
# PTX st.bulk form:
# st.bulk{.weak}{.shared::cta} [a], size, initval;
# ``initval`` is an immediate operand whose only legal value is 0.
def _ptx_st_bulk_parts(_ptr, _num_bytes, weak, space):
weak = bool(int(weak)) if hasattr(weak, "value") else bool(weak)
space = parse_str(space)
instr = f"st.bulk{'.weak' if weak else ''}{_dot(space)}"
name = f"tvm_builtin_ptx_st_bulk{'_weak' if weak else ''}{('_' + _safe_attr(space)) if space else ''}"
addr_arg = (
'"r"((unsigned int)__cvta_generic_to_shared(ptr))' if space == "shared::cta" else '"l"(ptr)'
)
body = (
f' asm volatile("{instr} [%0], %1, 0;"\n'
" :\n"
f" : {addr_arg}, "
'"l"(static_cast<uint64_t>(num_bytes))\n'
' : "memory");'
)
return name, body
device_intrinsic(
"ptx_st_bulk",
n_attrs=2,
helper_name=lambda *a: _ptx_st_bulk_parts(*a)[0],
c_signature="(void* ptr, unsigned int num_bytes)",
body=lambda *a: _ptx_st_bulk_parts(*a)[1],
)
device_intrinsic(
"cuda_uint_as_float",
helper_name="tvm_builtin_uint_as_float",
c_signature="(unsigned int bits)",
return_type="float",
body=" return __uint_as_float(bits);",
)
device_intrinsic(
"cuda_float_as_uint",
helper_name="tvm_builtin_float_as_uint",
c_signature="(float x)",
return_type="unsigned int",
body=" return __float_as_uint(x);",
)
@@ -0,0 +1,253 @@
# 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.
# ruff: noqa: E501
# pylint: disable=redefined-builtin, invalid-name
"""Miscellaneous device helpers.
Catch-all for ops that don't fit the (sync / mma / cp_async / memory / math /
nvshmem) feature buckets:
* PTX register-allocation control: ``setmaxnreg`` / ``mov`` from special reg.
* Per-thread queries / scheduling hints: ``thread_rank`` / ``nano_sleep``.
* Profiler timer hooks (``timer_init/start/end/finalize``).
* Debug helpers: ``printf`` / ``trap`` on assert failure.
"""
import hashlib
import json
import tvm
from tvm.backend.cuda.op import cuda_func_call
from ._schema import device_intrinsic
from .registry import CODEGEN_REGISTRY, register_codegen
from .utils import parse_str
# =============================================================================
# setmaxnreg.{inc,dec}.sync.aligned.u32 — 1 PTX form (.action picks inc/dec).
# =============================================================================
def _ptx_setmaxnreg(inc, nreg):
inc = bool(int(inc)) if hasattr(inc, "value") else bool(inc)
nreg = int(nreg)
action = "inc" if inc else "dec"
return (
f"tvm_builtin_ptx_setmaxnreg_{action}_{nreg}",
f' asm volatile("setmaxnreg.{action}.sync.aligned.u32 {nreg};");',
)
device_intrinsic(
"ptx_setmaxnreg",
n_attrs=2,
helper_name=lambda inc, nreg: _ptx_setmaxnreg(inc, nreg)[0],
body=lambda inc, nreg: _ptx_setmaxnreg(inc, nreg)[1],
)
# =============================================================================
# mov.u32/u64 from special register — 1 PTX form (Form 2 of mov.type d, sreg).
# Each (bits, reg) emits a distinct helper because the special reg name is
# baked into the PTX text.
# =============================================================================
def _ptx_fetch_register_body(bits):
spec = "l" if bits == 64 else "r"
def _body(reg):
reg = parse_str(reg)
return (
f" uint{bits}_t x;\n"
f' asm volatile("mov.u{bits} %0, %{reg};" : "={spec}"(x));\n'
f" return (int{bits}_t)x;"
)
return _body
for _bits in (32, 64):
device_intrinsic(
f"ptx_fetch_register_{_bits}",
n_attrs=1,
helper_name=(
lambda *a, bits=_bits: (
f"tvm_builtin_ptx_fetch_register_"
f"{parse_str(a[-1]).replace('::', '_').replace('.', '_')}"
)
),
return_type=f"int{_bits}_t",
body=_ptx_fetch_register_body(_bits),
)
del _bits
@register_codegen("ptx_fetch_register")
def codegen_ptx_fetch_register(bits, reg):
bits = int(bits)
reg = parse_str(reg)
if bits not in (32, 64):
raise ValueError(f"Only support 32/64 bits for ptx_fetch_register, but got {bits}.")
result = CODEGEN_REGISTRY[f"tirx.ptx_fetch_register_{bits}"]([reg])
return result[0] if isinstance(result, tuple) else result
# =============================================================================
# Per-thread queries / scheduling hints.
# =============================================================================
device_intrinsic(
"cuda_thread_rank",
body=(
" namespace cg = cooperative_groups;\n return cg::this_thread_block().thread_rank();"
),
return_type="int",
tvm_return_type="int32",
extra_deps=("cooperative_groups",),
)
device_intrinsic("cuda_nano_sleep", c_signature="(uint64_t time)", body=" __nanosleep(time);")
# =============================================================================
# Profiler timer hooks.
# =============================================================================
_COMMON_PARAMS = (
"uint64_t* profiler_buffer, uint64_t* profiler_tag, "
"uint32_t* profiler_write_offset, int profiler_write_stride, bool leader_cond"
)
_EVENT_PARAMS = f"int event_type, {_COMMON_PARAMS}"
def _write_event(event_bits: str) -> str:
return (
"profiler_buffer[profiler_write_offset[0]] = "
"((uint64_t)tvm_builtin_get_timestamp() << 32) | "
f"(profiler_tag[0] | {event_bits});\n"
" profiler_write_offset[0] += profiler_write_stride;"
)
device_intrinsic(
"timer_init_cuda",
c_signature=(
"(uint64_t* profiler_buffer, uint64_t* profiler_tag, "
"uint32_t* profiler_write_offset, int num_groups, int group_id)"
),
body=(
" const uint32_t NBLOCKS = (uint32_t)(gridDim.x * gridDim.y * gridDim.z);\n"
" const uint32_t BLOCK_IDX = (uint32_t)("
"(blockIdx.z * gridDim.y + blockIdx.y) * gridDim.x + blockIdx.x);\n"
" const uint32_t NGROUPS = num_groups;\n"
" const uint32_t GROUP_ID = group_id;\n"
" const uint32_t BLOCK_GROUP_IDX = BLOCK_IDX * NGROUPS + GROUP_ID;\n"
" if ((blockIdx.x == 0) && (blockIdx.y == 0) && "
"(blockIdx.z == 0) && (threadIdx.x == 0)) {\n"
" profiler_buffer[0] = ((uint64_t)NGROUPS << 32) | NBLOCKS;\n"
" }\n"
" profiler_write_offset[0] = 1 + BLOCK_GROUP_IDX;\n"
" profiler_tag[0] = (uint64_t)BLOCK_GROUP_IDX << 12;"
),
)
device_intrinsic(
"timer_start_cuda",
c_signature=f"({_EVENT_PARAMS})",
body=(
f" if (leader_cond) {{\n {_write_event('(uint32_t)event_type << 2 | 0x0')}\n }}\n"
" __threadfence_block();"
),
extra_deps=("get_time_stamp",),
)
device_intrinsic(
"timer_end_cuda",
c_signature=f"({_EVENT_PARAMS})",
body=(
" __threadfence_block();\n"
f" if (leader_cond) {{\n {_write_event('(uint32_t)event_type << 2 | 0x1')}\n }}"
),
extra_deps=("get_time_stamp",),
)
device_intrinsic(
"timer_finalize_cuda",
c_signature=f"({_COMMON_PARAMS})",
body=(
f" __threadfence_block();\n if (leader_cond) {{\n {_write_event('0x3')}\n }}"
),
extra_deps=("get_time_stamp",),
)
# =============================================================================
# Debug helpers — ``printf`` (variadic templated) and ``trap`` on assert.
# =============================================================================
device_intrinsic(
"cuda_trap_when_assert_failed",
c_signature="(bool cond)",
body=' do {\n if (not (cond))\n asm("trap;");\n } while (0);',
)
@register_codegen("cuda_printf")
def codegen_cuda_printf(fmt, *args):
if isinstance(fmt, tvm.tirx.StringImm):
fmt = fmt.value
if not isinstance(fmt, str):
raise ValueError("T.cuda.printf format must be a string literal")
fmt_literal = json.dumps(fmt)
arg_dtypes = [str(arg.ty) for arg in args]
signature = "|".join([fmt, *arg_dtypes])
digest = hashlib.sha1(signature.encode("utf-8")).hexdigest()
func_name = f"tvm_builtin_cuda_printf_{len(args)}_{digest}"
def c_type(dtype: str) -> str:
if dtype == "float32":
return "float"
if dtype == "float64":
return "double"
if dtype in {"int8", "int16", "int32"}:
return "int"
if dtype == "int64":
return "long long"
if dtype in {"uint8", "uint16", "uint32"}:
return "unsigned int"
if dtype == "uint64":
return "unsigned long long"
if dtype == "bool":
return "int"
if dtype == "handle":
return "void*"
raise ValueError(f"Unsupported T.cuda.printf argument dtype: {dtype}")
params = ", ".join(f"{c_type(dtype)} arg{i}" for i, dtype in enumerate(arg_dtypes))
call_args = ", ".join(f"arg{i}" for i in range(len(args)))
comma_call_args = f", {call_args}" if call_args else ""
source_code = f"""
__noinline__ __device__ void {func_name}({params}) {{
printf({fmt_literal}{comma_call_args});
}}
"""
return cuda_func_call(func_name, *args, source_code=source_code)
device_intrinsic(
"cuda_clock64",
helper_name="tvm_builtin_clock64",
return_type="unsigned long long",
body=" return clock64();",
)
@@ -0,0 +1,482 @@
# 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.
# pylint: disable=redefined-builtin, invalid-name, too-many-arguments, too-many-locals, too-many-positional-arguments
"""PTX MMA / ldmatrix / stmatrix intrinsics.
mma.sync.aligned has 7 form_kinds per the PTX docs (f16 / tf32 / bf16 / fp64
/ int8 / fp8 / subbyte). Each form_kind is one ``device_intrinsic`` registration;
the (shape, layouts, dtypes) modifier slots are attrs. Body computes the per-
fragment register counts at codegen time from M*N*bits/threads/frag_size and
hand-builds the asm constraint list.
ldmatrix / stmatrix each have a single PTX form (the .m8n8 .b16/.b8 variant
that TIRx uses); ``num`` and ``trans`` are modifier attrs.
"""
import re
from dataclasses import dataclass
from tvm import DataType
from ._schema import device_intrinsic
from .registry import CODEGEN_REGISTRY, register_codegen
from .types import PTXDataType
from .utils import parse_str
@dataclass
class FragAttrs:
reg_type: str # asm constraint letter (r / f / d)
size: int # bit width per register slot (32 or 64)
ptr_type: str # C type for the cast
_FRAG_ATTRS_MAP = {
PTXDataType.BIT1: FragAttrs("r", 32, "uint32_t"),
PTXDataType.INT4: FragAttrs("r", 32, "uint32_t"),
PTXDataType.UINT4: FragAttrs("r", 32, "uint32_t"),
PTXDataType.INT8: FragAttrs("r", 32, "uint32_t"),
PTXDataType.UINT8: FragAttrs("r", 32, "uint32_t"),
PTXDataType.FLOAT8_E4M3FN: FragAttrs("r", 32, "uint32_t"),
PTXDataType.FLOAT8_E5M2: FragAttrs("r", 32, "uint32_t"),
PTXDataType.BIT16: FragAttrs("r", 32, "uint32_t"),
PTXDataType.FLOAT16: FragAttrs("r", 32, "uint32_t"),
PTXDataType.BFLOAT16: FragAttrs("r", 32, "uint32_t"),
PTXDataType.TENSOR_FLOAT32: FragAttrs("r", 32, "uint32_t"),
PTXDataType.INT32: FragAttrs("r", 32, "int32_t"),
PTXDataType.FLOAT32: FragAttrs("f", 32, "float"),
PTXDataType.FLOAT64: FragAttrs("d", 64, "double"),
}
def _parse_mma_shape(shape_str):
match = re.search(r"m(\d+)n(\d+)k(\d+)", shape_str)
if not match:
raise ValueError(f"Cannot parse MMA shape: {shape_str!r}")
return tuple(map(int, match.groups()))
def _classify_mma_form(d_type, a_type, b_type):
"""Map (d, a, b) dtype triple to one of the 7 PTX form_kind tags."""
fp16 = {"float16", "fp16"}
tf32 = {"tensor_float32", "tf32"}
bf16 = {"bfloat16", "bf16"}
fp64 = {"float64", "fp64"}
int_a = {"int8", "uint8", "s8", "u8"}
fp8 = {"e4m3", "e5m2", "float8_e4m3fn", "float8_e4m3fnuz", "float8_e5m2"}
subbyte = {"int4", "uint4", "bit1", "s4", "u4", "b1", "int1", "uint1"}
if a_type in fp16 and b_type in fp16:
return "f16"
if a_type in tf32 and b_type in tf32:
return "tf32"
if a_type in bf16 and b_type in bf16:
return "bf16"
if a_type in fp64 and b_type in fp64:
return "fp64"
if a_type in int_a and b_type in int_a:
return "int8"
if a_type in fp8 and b_type in fp8:
return "fp8"
if a_type in subbyte and b_type in subbyte:
return "subbyte"
raise ValueError(
f"Unknown ptx.mma form for d_type={d_type!r}, a_type={a_type!r}, b_type={b_type!r}"
)
def _frag(dtype_str):
return _FRAG_ATTRS_MAP[PTXDataType.from_string(dtype_str)]
def _mma_threads(shape, a_type):
"""Special case: m8n8k4 with f16 a/b uses 8 threads per fragment."""
m, n, k = _parse_mma_shape(shape)
if m == 8 and n == 8 and k == 4 and a_type == "float16":
return 8
return 32
# PTX dtype abbreviation -> element bit width. Used by _frag_count so that
# callers passing the PTX abbreviation (e.g. "fp32") don't blow up in
# ``DataType("fp32")``.
_PTX_BITS = {
"fp16": 16,
"fp32": 32,
"fp64": 64,
"bf16": 16,
"tf32": 32, # tensor-float32 packs 19 significant bits into a 32-bit slot
"s8": 8,
"u8": 8,
"s32": 32,
"s4": 4,
"u4": 4,
"b1": 1,
"b16": 16,
"e4m3": 8,
"e5m2": 8,
}
def _frag_count(dtype, dim_a, dim_b, threads):
if dtype in _PTX_BITS:
bits = _PTX_BITS[dtype]
else:
bits = DataType(dtype).bits
size = _frag(dtype).size
return dim_a * dim_b * bits // threads // size
# =============================================================================
# Shared helpers for the 7 mma form_kinds.
# Args layout for each form:
# (d_ptr_in, a_ptr_in, b_ptr_in [, c_ptr_in], shape, a_layout, b_layout,
# d_type, a_type, b_type, c_type, no_c_ptr [, saturate or bit_op])
# n_attrs = 8 for f16/tf32/bf16/fp64/fp8 (last 8 = shape, layouts, 4 dtypes, no_c_ptr)
# n_attrs = 9 for int8 (+ saturate) and subbyte (+ bit_op)
# =============================================================================
def _mma_form_parts(args, *, has_saturate=False, has_bit_op=False):
"""Compute (helper_name, c_signature, body) for one mma form invocation.
``args`` is the full positional arg tuple as received by codegen.
The trailing ``n_attrs`` (8 or 9) entries are attrs.
"""
n_extra = (1 if has_saturate else 0) + (1 if has_bit_op else 0)
n_attrs = 8 + n_extra
# Split off attr args from the tail (operand args are ahead).
attrs = args[-n_attrs:]
shape = parse_str(attrs[0])
a_layout = parse_str(attrs[1])
b_layout = parse_str(attrs[2])
d_type = parse_str(attrs[3])
a_type = parse_str(attrs[4])
b_type = parse_str(attrs[5])
c_type = parse_str(attrs[6])
no_c_ptr_raw = attrs[7]
no_c_ptr = bool(int(no_c_ptr_raw)) if hasattr(no_c_ptr_raw, "value") else bool(no_c_ptr_raw)
saturate = False
bit_op = ""
if has_saturate:
s = attrs[8]
saturate = bool(int(s)) if hasattr(s, "value") else bool(s)
if has_bit_op:
bit_op = parse_str(attrs[8])
# Fragment counts (same derivation as the contiguous form).
m, n, k = _parse_mma_shape(shape)
threads = _mma_threads(shape, a_type)
d_cnt = _frag_count(d_type, m, n, threads)
a_cnt = _frag_count(a_type, m, k, threads)
b_cnt = _frag_count(b_type, k, n, threads)
c_cnt = _frag_count(c_type, m, n, threads)
# C signature: one void* per register, ordered D regs, A regs, B regs,
# then C regs (only when the accumulator is used).
sig_parts = (
[f"void* d_ptr{i}" for i in range(d_cnt)]
+ [f"void* a_ptr{i}" for i in range(a_cnt)]
+ [f"void* b_ptr{i}" for i in range(b_cnt)]
)
if not no_c_ptr:
sig_parts += [f"void* c_ptr{i}" for i in range(c_cnt)]
sig = "(" + ", ".join(sig_parts) + ")"
def _safe(s):
return s.replace("::", "_").replace(".", "_")
name = (
f"ptx_mma_{shape}_{a_layout}_{b_layout}"
f"_{_safe(d_type)}_{_safe(a_type)}_{_safe(b_type)}_{_safe(c_type)}"
f"{'_no_c_ptr' if no_c_ptr else ''}"
f"{'_saturate' if saturate else ''}"
)
d_frag = _frag(d_type)
a_frag = _frag(a_type)
b_frag = _frag(b_type)
c_frag = _frag(c_type)
saturate_inst = ".satfinite" if saturate else ""
# PTX b1 mma requires a `.popc` suffix after the bit op (e.g. `.xor.popc`).
bit_op_inst = f".{bit_op}.popc" if bit_op else ""
d_type_inst = PTXDataType.from_string(d_type).to_string()
c_type_inst = PTXDataType.from_string(c_type).to_string()
a_type_inst = PTXDataType.from_string(a_type).to_string()
b_type_inst = PTXDataType.from_string(b_type).to_string()
def _slot_arr(start, cnt):
return "{" + ", ".join(f"%{start + i}" for i in range(cnt)) + "}"
args_template = (
f"{_slot_arr(0, d_cnt)}, {_slot_arr(d_cnt, a_cnt)}, "
f"{_slot_arr(d_cnt + a_cnt, b_cnt)}, {_slot_arr(d_cnt + a_cnt + b_cnt, c_cnt)}"
)
# Each register binds to its OWN pointer via *(T*)X_ptrN (scatter).
d_outs = ", ".join(
f'"=r"(*({d_frag.ptr_type}*)d_ptr{i})'
if d_frag.reg_type == "r"
else f'"={d_frag.reg_type}"(*({d_frag.ptr_type}*)d_ptr{i})'
for i in range(d_cnt)
)
a_inputs = ", ".join(
f'"{a_frag.reg_type}"(*({a_frag.ptr_type}*)a_ptr{i})' for i in range(a_cnt)
)
b_inputs = ", ".join(
f'"{b_frag.reg_type}"(*({b_frag.ptr_type}*)b_ptr{i})' for i in range(b_cnt)
)
if no_c_ptr:
c_value = "0.f" if c_frag.reg_type == "f" else "0"
c_inputs = ", ".join(f'"{c_frag.reg_type}"({c_value})' for _ in range(c_cnt))
else:
c_inputs = ", ".join(
f'"{c_frag.reg_type}"(*({c_frag.ptr_type}*)c_ptr{i})' for i in range(c_cnt)
)
body = (
" asm volatile(\n"
f' "mma.sync.aligned.{shape}.{a_layout}.{b_layout}{saturate_inst}'
f'{d_type_inst}{a_type_inst}{b_type_inst}{c_type_inst}{bit_op_inst} "\n'
f' "{args_template};\\n"\n'
f" : {d_outs}\n"
f" : {a_inputs}, {b_inputs}, {c_inputs}\n"
" );"
)
return name, sig, body
def _register_mma_form(form_kind, *, has_saturate=False, has_bit_op=False):
n_attrs = 8 + (1 if has_saturate else 0) + (1 if has_bit_op else 0)
def _parts(*args, hs=has_saturate, hb=has_bit_op):
return _mma_form_parts(args, has_saturate=hs, has_bit_op=hb)
device_intrinsic(
f"_ptx_mma_{form_kind}",
n_attrs=n_attrs,
helper_name=lambda *a: _parts(*a)[0],
c_signature=lambda *a: _parts(*a)[1],
body=lambda *a: _parts(*a)[2],
)
# Form 1 — f16. Form 2 — tf32. Form 3 — bf16. Form 4 — fp64. Form 6 — fp8.
# All share the same 8-attr layout (no saturate / bit_op).
for _kind in ("f16", "tf32", "bf16", "fp64", "fp8"):
_register_mma_form(_kind)
del _kind
# Form 5 — int8 (+ saturate).
_register_mma_form("int8", has_saturate=True)
# Form 7 — subbyte (+ bit_op for b1).
_register_mma_form("subbyte", has_bit_op=True)
@register_codegen("ptx_mma")
def codegen_ptx_mma(
shape,
a_layout,
b_layout,
d_type,
a_type,
b_type,
c_type,
d_cnt,
a_cnt,
b_cnt,
c_cnt,
no_c_ptr,
*rest,
):
"""Classify (d, a, b) dtype triple to one of 7 form_kinds and forward.
``rest`` = flattened per-register pointers (d_cnt + a_cnt + b_cnt + c_cnt of
them) followed by ``saturate`` and optionally ``bit_op``.
"""
shape = parse_str(shape)
a_layout = parse_str(a_layout)
b_layout = parse_str(b_layout)
d_type = parse_str(d_type)
a_type = parse_str(a_type)
b_type = parse_str(b_type)
c_type = parse_str(c_type)
d_cnt = int(d_cnt)
a_cnt = int(a_cnt)
b_cnt = int(b_cnt)
c_cnt = int(c_cnt)
no_c_ptr = bool(int(no_c_ptr)) if hasattr(no_c_ptr, "value") else bool(no_c_ptr)
n_ptrs = d_cnt + a_cnt + b_cnt + (0 if no_c_ptr else c_cnt)
ptrs = list(rest[:n_ptrs])
trailing = list(rest[n_ptrs:])
saturate = bool(trailing[0]) if trailing else False
bit_op_v = ""
if len(trailing) >= 2:
bo = trailing[1]
bit_op_v = parse_str(bo) if isinstance(bo, str) else (bo if bo is not None else "")
kind = _classify_mma_form(d_type, a_type, b_type)
# op_args are the flattened per-register pointers (already in PTX order:
# D regs, A regs, B regs, then C regs unless no_c_ptr).
op_args = ptrs
attr_args = [shape, a_layout, b_layout, d_type, a_type, b_type, c_type, no_c_ptr]
if kind == "int8":
attr_args.append(saturate)
elif kind == "subbyte":
attr_args.append(bit_op_v)
result = CODEGEN_REGISTRY[f"tirx._ptx_mma_{kind}"](op_args + attr_args)
return result[0] if isinstance(result, tuple) else result
# =============================================================================
# ldmatrix / stmatrix — m8n8 fragment load/store. PTX docs lists 3 ldmatrix
# forms (m8n8 + m8n16 + m16n16); TIRx uses only the m8n8 form. 1
# device_intrinsic each. ``num`` (.x1/.x2/.x4) and ``trans`` are modifier
# attrs; the asm body loops over per-register constraints based on
# (num, dtype).
# =============================================================================
def _ldmatrix_parts(*args):
# args = (smem_ptr, dst0, dst1, ..., dst{N-1}, num, dtype, trans)
# The last 3 entries are the codegen attrs (n_attrs=3).
num = int(args[-3])
dtype = parse_str(args[-2])
trans_b = bool(int(args[-1])) if hasattr(args[-1], "value") else bool(args[-1])
if num not in (1, 2, 4):
raise ValueError(f"ldmatrix .num must be one of {{1, 2, 4}}, got {num}")
if dtype not in ("b16", "b8"):
raise ValueError(f"ldmatrix dtype must be 'b16' or 'b8', got {dtype!r}")
n_regs = num if dtype == "b16" else num // 2
trans_inst = ".trans" if trans_b else ""
slot_list = "{" + ", ".join(f"%{i}" for i in range(n_regs)) + "}"
reg_decls = ", ".join(f"r{i}" for i in range(n_regs))
out_constraints = ", ".join(f'"=r"(r{i})' for i in range(n_regs))
dst_assigns = "\n".join(f" *(uint32_t*)dst{i} = r{i};" for i in range(n_regs))
name = f"ptx_ldmatrix_{num}_{dtype.replace('::', '_').replace('.', '_')}_{1 if trans_b else 0}"
sig = "(void* smem_ptr, " + ", ".join(f"void* dst{i}" for i in range(n_regs)) + ")"
body = (
f" uint32_t {reg_decls};\n"
" unsigned int addr = __cvta_generic_to_shared(smem_ptr);\n"
" asm volatile(\n"
f' "ldmatrix.sync.aligned.m8n8.x{num}{trans_inst}.shared.{dtype} '
f'{slot_list}, [%{n_regs}];"\n'
f" : {out_constraints}\n"
f' : "r"(addr));\n'
f"{dst_assigns}"
)
return name, sig, body
device_intrinsic(
"_ptx_ldmatrix_impl",
n_attrs=3,
c_signature=lambda *a: _ldmatrix_parts(*a)[1],
helper_name=lambda *a: _ldmatrix_parts(*a)[0],
body=lambda *a: _ldmatrix_parts(*a)[2],
)
@register_codegen("ptx_ldmatrix")
def codegen_ptx_ldmatrix(trans, num, dtype, smem_ptr, *dst_handles):
trans = bool(trans)
num = int(num)
dtype = parse_str(dtype)
if dtype.startswith("."):
dtype = dtype[1:]
n_regs = num if dtype == "b16" else num // 2
if len(dst_handles) != n_regs:
raise ValueError(
f"ldmatrix .x{num}.{dtype} codegen expects {n_regs} dst handles, got {len(dst_handles)}"
)
result = CODEGEN_REGISTRY["tirx._ptx_ldmatrix_impl"](
[smem_ptr, *dst_handles, num, dtype, trans]
)
return result[0] if isinstance(result, tuple) else result
def _stmatrix_parts(*args):
# args = (smem_ptr, src0, src1, ..., src{N-1}, trans, num, dtype, shape, space)
# The last 5 entries are codegen attrs (n_attrs=5).
trans_arg, num_arg, dtype_arg, shape_arg, space_arg = args[-5:]
n_regs = int(num_arg)
trans_b = bool(int(trans_arg)) if hasattr(trans_arg, "value") else bool(trans_arg)
dtype = parse_str(dtype_arg)
shape = parse_str(shape_arg)
space = parse_str(space_arg)
if dtype.startswith("."):
dtype = dtype[1:]
if n_regs not in (1, 2, 4):
raise ValueError(f"stmatrix .num must be one of {{1, 2, 4}}, got {n_regs}")
if dtype not in ("b16", "b8"):
raise ValueError(f"stmatrix .type must be b16 or b8, got {dtype!r}")
if shape not in ("m8n8", "m16n8"):
raise ValueError(f"stmatrix .shape must be m8n8 or m16n8, got {shape!r}")
if space not in ("shared", "shared::cta"):
raise ValueError(f"stmatrix state space must be shared or shared::cta, got {space!r}")
if shape == "m16n8" and not trans_b:
raise ValueError("stmatrix .m16n8 requires .trans")
trans_inst = ".trans" if trans_b else ""
slot_list = "{" + ", ".join(f"%{i}" for i in range(n_regs)) + "}"
src_loads = "\n".join(f" uint32_t r{i} = *(uint32_t*)src{i};" for i in range(n_regs))
in_constraints = ", ".join(f'"r"(r{i})' for i in range(n_regs))
name = f"ptx_stmatrix_{shape}_{n_regs}_{1 if trans_b else 0}_{space.replace('::', '_')}_{dtype}"
sig = "(void* smem_ptr, " + ", ".join(f"void* src{i}" for i in range(n_regs)) + ")"
body = (
f"{src_loads}\n"
" unsigned int addr = __cvta_generic_to_shared(smem_ptr);\n"
" asm volatile(\n"
f' "stmatrix.sync.aligned.{shape}.x{n_regs}{trans_inst}.{space}.{dtype} '
f'[%{n_regs}], {slot_list};"\n'
" :\n"
f' : {in_constraints}, "r"(addr));'
)
return name, sig, body
device_intrinsic(
"_ptx_stmatrix_impl",
n_attrs=5,
c_signature=lambda *a: _stmatrix_parts(*a)[1],
helper_name=lambda *a: _stmatrix_parts(*a)[0],
body=lambda *a: _stmatrix_parts(*a)[2],
)
@register_codegen("ptx_stmatrix")
def codegen_ptx_stmatrix(trans, num, dtype, shape, space, smem_ptr, *src_handles):
trans = bool(trans)
num = int(num)
dtype_str = parse_str(dtype)
if dtype_str.startswith("."):
dtype_str = dtype_str[1:]
n_regs = num
if len(src_handles) != n_regs:
raise ValueError(
f"stmatrix .x{num}.{dtype_str} codegen expects {n_regs} src handles, "
f"got {len(src_handles)}"
)
result = CODEGEN_REGISTRY["tirx._ptx_stmatrix_impl"](
[smem_ptr, *src_handles, trans, num, dtype, shape, space]
)
return result[0] if isinstance(result, tuple) else result
@@ -0,0 +1,161 @@
# 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.
# pylint: disable=redefined-builtin, invalid-name
"""NVSHMEM intrinsics. Each backend call is one ``device_intrinsic(...)``."""
from ._schema import device_intrinsic
from .registry import CODEGEN_REGISTRY, register_codegen
_NVSHMEM = ("nvshmem",)
# =============================================================================
# No-arg helpers: PE queries, quiet, fence, barrier_all.
# =============================================================================
for _op, _call, _ret, _tvm_ret in [
("nvshmem_my_pe", "nvshmem_my_pe", "int32_t", "int32"),
("nvshmem_n_pes", "nvshmem_n_pes", "int32_t", "int32"),
("nvshmem_quiet", "nvshmem_quiet", "void", None),
("nvshmem_fence", "nvshmem_fence", "void", None),
("nvshmem_barrier_all", "nvshmem_barrier_all", "void", None),
]:
device_intrinsic(
_op,
body=(" " + (f"return {_call}();" if _ret != "void" else f"{_call}();")),
return_type=_ret,
tvm_return_type=_tvm_ret,
extra_deps=_NVSHMEM,
)
del _op, _call, _ret, _tvm_ret
# =============================================================================
# RMA get/put (thread/warp/block).
# =============================================================================
_RMA_SIG = "(void *dest, const void *source, size_t nelems, int pe)"
for _op, _backend_call in [
("nvshmem_getmem_nbi", "nvshmem_getmem_nbi"),
("nvshmem_putmem_nbi", "nvshmem_putmem_nbi"),
("nvshmem_getmem_nbi_warp", "nvshmemx_getmem_nbi_warp"),
("nvshmem_putmem_nbi_warp", "nvshmemx_putmem_nbi_warp"),
("nvshmem_getmem_nbi_block", "nvshmemx_getmem_nbi_block"),
("nvshmem_putmem_nbi_block", "nvshmemx_putmem_nbi_block"),
]:
device_intrinsic(
_op,
c_signature=_RMA_SIG,
body=f" {_backend_call}(dest, source, nelems, pe);",
extra_deps=_NVSHMEM,
)
del _op, _backend_call
# =============================================================================
# Signal / wait_until — each backend call is one device_intrinsic. String
# attrs (sig_op, cmp) are mapped to NVSHMEM integer constants in the
# user-facing dispatcher below.
# =============================================================================
_SIG_OP_VAL = {"set": 0, "add": 1}
_CMP_VAL = {"eq": 0, "ne": 1, "gt": 2, "ge": 3, "lt": 4, "le": 5}
def _resolve_attr(value, table, label):
s = value if isinstance(value, str) else value.value
if s not in table:
raise ValueError(f"Unsupported {label}: {s}")
return table[s]
device_intrinsic(
"_nvshmem_signal_op_impl",
helper_name="tvm_builtin_nvshmem_signal_op",
c_signature="(uint64_t* sig_addr, uint64_t signal, int sig_op, int pe)",
body=" nvshmemx_signal_op(sig_addr, signal, sig_op, pe);",
extra_deps=_NVSHMEM,
)
@register_codegen("nvshmem_signal_op")
def codegen_nvshmem_signal_op(sig_addr, signal, sig_op, pe):
"""Map ``sig_op`` (string) to its NVSHMEM int constant, then forward."""
sig_op_int = _resolve_attr(sig_op, _SIG_OP_VAL, "signal op")
result = CODEGEN_REGISTRY["tirx._nvshmem_signal_op_impl"]([sig_addr, signal, sig_op_int, pe])
return result
# nvshmem_<type>_wait_until — one device_intrinsic per supported type.
_WAIT_UNTIL_TYPES = {"uint64_t": "uint64", "uint64": "uint64"}
for _c_type, _suffix in [("uint64_t", "uint64")]:
device_intrinsic(
f"_nvshmem_{_suffix}_wait_until_impl",
helper_name=f"tvm_builtin_nvshmem_{_suffix}_wait_until",
c_signature=f"({_c_type}* ivar, int cmp, {_c_type} cmp_value)",
body=f" nvshmem_{_suffix}_wait_until(ivar, cmp, cmp_value);",
extra_deps=_NVSHMEM,
)
del _c_type, _suffix
@register_codegen("nvshmem_wait_until")
def codegen_nvshmem_wait_until(ivar, cmp, cmp_value, type):
"""Dispatch to the type-specific wait_until helper after mapping ``cmp``
(string) to its NVSHMEM int constant."""
type_str = type if isinstance(type, str) else type.value
if type_str not in _WAIT_UNTIL_TYPES:
raise ValueError(f"Unsupported type for nvshmem_wait_until: {type_str}")
suffix = _WAIT_UNTIL_TYPES[type_str]
cmp_int = _resolve_attr(cmp, _CMP_VAL, "cmp operation")
result = CODEGEN_REGISTRY[f"tirx._nvshmem_{suffix}_wait_until_impl"]([ivar, cmp_int, cmp_value])
return result
# putmem_signal_nbi (thread / warp / block) — three scope-specific helpers.
_PUTMEM_SIG_SIG = (
"(void* dest, const void* source, size_t nelems, "
"uint64_t* sig_addr, uint64_t signal, int sig_op, int pe)"
)
for _scope_suffix, _backend_call in [
("", "nvshmem_putmem_signal_nbi"),
("_warp", "nvshmemx_putmem_signal_nbi_warp"),
("_block", "nvshmemx_putmem_signal_nbi_block"),
]:
device_intrinsic(
f"_nvshmem_putmem_signal_nbi{_scope_suffix}_impl",
helper_name=f"tvm_builtin_nvshmem_putmem_signal_nbi{_scope_suffix}",
c_signature=_PUTMEM_SIG_SIG,
body=f" {_backend_call}(dest, source, nelems, sig_addr, signal, sig_op, pe);",
extra_deps=_NVSHMEM,
)
del _scope_suffix, _backend_call
def _make_putmem_signal_dispatcher(scope_suffix):
@register_codegen(f"nvshmem_putmem_signal_nbi{scope_suffix}")
def _codegen(dest, source, nelems, sig_addr, signal, sig_op, pe):
sig_op_int = _resolve_attr(sig_op, _SIG_OP_VAL, "signal op")
result = CODEGEN_REGISTRY[f"tirx._nvshmem_putmem_signal_nbi{scope_suffix}_impl"](
[dest, source, nelems, sig_addr, signal, sig_op_int, pe]
)
return result
return _codegen
for _suffix in ("", "_warp", "_block"):
_make_putmem_signal_dispatcher(_suffix)
del _suffix
@@ -0,0 +1,77 @@
# 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.
"""Codegen registry for CUDA HW ops.
User-facing Python wrappers are hand-written in :mod:`tvm.tirx.op` so that
editors / static analyzers (Cursor, Pyright) can see their signatures. This
module only handles the backend codegen side.
"""
import functools
import tvm_ffi
CODEGEN_REGISTRY = {}
def _canonical_device_intrin_name(op_name: str) -> str:
if not op_name.startswith("tirx."):
return op_name
basename = op_name[len("tirx.") :]
if "." in basename:
return op_name
for prefix, namespace in (
("cuda_", "cuda"),
("ptx_", "ptx"),
("nvshmem_", "nvshmem"),
("nki_", "nki"),
):
if basename.startswith(prefix):
return f"tirx.{namespace}.{basename[len(prefix) :]}"
return op_name
@tvm_ffi.register_global_func("tirx.intrinsics.cuda.get_codegen")
def get_codegen(op):
"""get the codegen function for a given op"""
return CODEGEN_REGISTRY.get(op, None)
def register_codegen(op, backend="cuda"):
"""Register a codegen function for a given op.
The codegen function should return a ``cuda_func_call`` statement, and
optionally a list of tags that the codegen function needs.
"""
def decorator(func):
full_op_name = "tirx." + op
canonical_op_name = _canonical_device_intrin_name(full_op_name)
op_names = {full_op_name, canonical_op_name}
@functools.wraps(func)
def wrapper(arg_list):
res = func(*arg_list) # pylint: disable=not-callable
if isinstance(res, tuple):
return res[0], res[1]
return res, list()
for op_name in op_names:
CODEGEN_REGISTRY[op_name] = wrapper
return wrapper
return decorator
@@ -0,0 +1,570 @@
# 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.
# pylint: disable=invalid-name
"""Synchronization primitives.
PTX side:
* ``bar.arrive`` / ``bar.sync`` — named-barrier alias of ``barrier.arrive/sync``
* ``fence{.sem}.scope`` / ``fence.proxy.async`` / ``fence.mbarrier_init``
* ``barrier.cluster.arrive`` / ``barrier.cluster.wait``
* ``mbarrier.init`` / ``mbarrier.arrive[.expect_tx]`` (local + remote) / ``mbarrier.try_wait``
* ``elect.sync`` — warp leader election
* warp-vote ``__any_sync``
CUDA-side helpers:
* ``__threadfence`` / ``__syncwarp`` / ``__syncthreads`` / ``__syncthreads_and|or``
* cooperative-groups grid sync
* cluster sync (open-coded ``barrier.cluster.arrive/wait`` pair)
* warpgroup sync (``bar.sync``)
"""
from tvm.tirx.operator.intrinsics._common import (
CLUSTER_BARRIER_SEM,
FENCE_PROXY_ASYNC_SPACE,
FENCE_SCOPE,
FENCE_SEM,
)
from ._schema import device_intrinsic
from .registry import CODEGEN_REGISTRY, register_codegen
from .utils import parse_str
# =============================================================================
# bar.arrive / bar.sync — alias of barrier.arrive/sync. 1 form each.
# bar.sync a, b ;
# bar.arrive a, b ;
# =============================================================================
device_intrinsic(
"ptx_bar_arrive",
c_signature="(int name_bar_id, int thread_count)",
body=(
' asm volatile("bar.arrive %0, %1;" : : "r"(name_bar_id), "r"(thread_count) : "memory");'
),
)
device_intrinsic(
"ptx_bar_sync",
c_signature="(int name_bar_id, int thread_count)",
body=(
' asm volatile("bar.sync %0, %1;" : : "r"(name_bar_id), "r"(thread_count) : "memory");'
),
)
# =============================================================================
# fence{.sem}.scope — 1 form (sem/scope are modifier values).
# =============================================================================
def _ptx_fence(sem, scope):
sem, scope = parse_str(sem), parse_str(scope)
assert sem in FENCE_SEM, f"invalid fence sem {sem!r}, expected one of {FENCE_SEM}"
assert scope in FENCE_SCOPE, f"invalid fence scope {scope!r}, expected one of {FENCE_SCOPE}"
return (
f"tvm_builtin_ptx_fence_{sem}_{scope}",
f' asm volatile("fence.{sem}.{scope};" ::: "memory");',
)
device_intrinsic(
"ptx_fence",
n_attrs=2,
helper_name=lambda sem, scope: _ptx_fence(sem, scope)[0],
body=lambda sem, scope: _ptx_fence(sem, scope)[1],
)
# =============================================================================
# fence.proxy.async{.<space>} — 1 form, optional .space modifier.
# =============================================================================
def _ptx_fence_proxy_async(space):
space = parse_str(space)
assert space in FENCE_PROXY_ASYNC_SPACE, (
f"invalid fence.proxy.async space {space!r}, expected one of {FENCE_PROXY_ASYNC_SPACE}"
)
suffix = f".{space}" if space else ""
name_safe = "_" + space.replace("::", "_").replace(".", "_") if space else ""
return (
f"tvm_builtin_ptx_fence_proxy_async{name_safe}",
f' asm volatile("fence.proxy.async{suffix};" ::: "memory");',
)
device_intrinsic(
"ptx_fence_proxy_async",
n_attrs=1,
helper_name=lambda space: _ptx_fence_proxy_async(space)[0],
body=lambda space: _ptx_fence_proxy_async(space)[1],
)
# =============================================================================
# fence.mbarrier_init.release.cluster — 1 form, no operands.
# =============================================================================
device_intrinsic(
"ptx_fence_mbarrier_init",
body=' asm volatile("fence.mbarrier_init.release.cluster;" ::: "memory");',
)
# =============================================================================
# barrier.cluster.arrive{.sem}{.aligned} — 1 form.
# =============================================================================
def _ptx_barrier_cluster_arrive(sem, aligned):
sem = parse_str(sem)
aligned = bool(int(aligned)) if hasattr(aligned, "value") else bool(aligned)
assert sem in CLUSTER_BARRIER_SEM, (
f"invalid cluster.arrive sem {sem!r}, expected one of {CLUSTER_BARRIER_SEM}"
)
sem_suffix = f".{sem}" if sem else ""
aligned_suffix = ".aligned" if aligned else ""
name_sem = "_" + sem.replace("::", "_").replace(".", "_") if sem else ""
name_aligned = "_aligned" if aligned else ""
return (
f"tvm_builtin_ptx_barrier_cluster_arrive{name_sem}{name_aligned}",
f' asm volatile("barrier.cluster.arrive{sem_suffix}{aligned_suffix};" ::: "memory");',
)
device_intrinsic(
"ptx_barrier_cluster_arrive",
n_attrs=2,
helper_name=lambda sem, aligned: _ptx_barrier_cluster_arrive(sem, aligned)[0],
body=lambda sem, aligned: _ptx_barrier_cluster_arrive(sem, aligned)[1],
)
# =============================================================================
# barrier.cluster.wait{.acquire}{.aligned} — 1 form.
# =============================================================================
def _ptx_barrier_cluster_wait(acquire, aligned):
acquire = bool(int(acquire)) if hasattr(acquire, "value") else bool(acquire)
aligned = bool(int(aligned)) if hasattr(aligned, "value") else bool(aligned)
acq_suffix = ".acquire" if acquire else ""
aligned_suffix = ".aligned" if aligned else ""
return (
f"tvm_builtin_ptx_barrier_cluster_wait"
f"{'_acquire' if acquire else ''}{'_aligned' if aligned else ''}",
f' asm volatile("barrier.cluster.wait{acq_suffix}{aligned_suffix};" ::: "memory");',
)
device_intrinsic(
"ptx_barrier_cluster_wait",
n_attrs=2,
helper_name=lambda acquire, aligned: _ptx_barrier_cluster_wait(acquire, aligned)[0],
body=lambda acquire, aligned: _ptx_barrier_cluster_wait(acquire, aligned)[1],
)
# =============================================================================
# clusterlaunchcontrol.try_cancel / query_cancel — Blackwell Cluster Launch
# Control (CLC) work-stealing, written from the PTX ISA spec (section
# "clusterlaunchcontrol", PTX ISA 8.6). try_cancel async-requests cancelling the
# next cluster's launch, writing a 16B response to smem + signalling mbar. query
# decodes the response: on success it extracts the cancelled cluster's first
# ctaid.x (via the get_first_ctaid::x form); a single uint32 is returned, with
# 0xFFFFFFFF as the "no work stolen" sentinel (a device helper returns one scalar).
# =============================================================================
device_intrinsic(
"ptx_clc_try_cancel",
c_signature="(void* handle, void* mbar)",
body=(
" unsigned int addr = (unsigned int)__cvta_generic_to_shared(handle);\n"
" unsigned int bar = (unsigned int)__cvta_generic_to_shared(mbar);\n"
" asm volatile(\n"
' "clusterlaunchcontrol.try_cancel.async.shared::cta.mbarrier::complete_tx::bytes"\n'
' ".multicast::cluster::all.b128 [%0], [%1];\\n"\n'
' :: "r"(addr), "r"(bar) : "memory");'
),
)
device_intrinsic(
"ptx_clc_query_cancel",
c_signature="(void* handle)",
return_type="uint32_t",
tvm_return_type="uint32",
body=(
" unsigned int addr = (unsigned int)__cvta_generic_to_shared(handle);\n"
" unsigned int first_ctaid_x;\n"
" asm volatile(\n"
' "{\\n"\n'
' ".reg .pred canceled;\\n"\n'
' ".reg .b128 response;\\n"\n'
' "ld.shared.b128 response, [%1];\\n"\n'
' "clusterlaunchcontrol.query_cancel.is_canceled.pred.b128 canceled, response;\\n"\n'
' "mov.u32 %0, 0xffffffff;\\n"\n'
' "@canceled clusterlaunchcontrol.query_cancel.get_first_ctaid::x.b32.b128"\n'
' " %0, response;\\n"\n'
' "}\\n"\n'
' : "=r"(first_ctaid_x) : "r"(addr) : "memory");\n'
' asm volatile("fence.proxy.async.shared::cta;\\n" ::: "memory");\n'
" return first_ctaid_x;"
),
)
# =============================================================================
# mbarrier.init.shared.b64 [addr], count ; — 1 form.
# =============================================================================
device_intrinsic(
"ptx_mbarrier_init",
c_signature="(void* barrier, int thread_count)",
body=(
" unsigned int barrier_addr = __cvta_generic_to_shared(barrier);\n"
' asm volatile("mbarrier.init.shared.b64 [%0], %1;"'
' : : "r"(barrier_addr), "r"(thread_count) : "memory");'
),
)
# =============================================================================
# mbarrier.arrive — local + remote (cluster-mapped) forms. 2 PTX forms.
# Form local: mbarrier.arrive.shared.b64 _, [bar];
# Form remote: { setp+@p mapa.shared::cluster.u32 + @p mbarrier.arrive.shared::cluster.b64 }
# Dispatcher picks by arg count (1 vs 3).
# =============================================================================
device_intrinsic(
"_ptx_mbarrier_arrive_local",
helper_name="tvm_builtin_ptx_mbarrier_arrive",
c_signature="(void* barrier)",
body=(
" unsigned int barrier_addr = __cvta_generic_to_shared(barrier);\n"
' asm volatile("mbarrier.arrive.shared.b64 _, [%0];"\n'
' :: "r"(barrier_addr) : "memory");'
),
)
device_intrinsic(
"_ptx_mbarrier_arrive_remote",
helper_name="tvm_builtin_ptx_mbarrier_arrive_remote",
c_signature="(void* barrier, int cta_id, int pred)",
body=(
" unsigned int barrier_addr = __cvta_generic_to_shared(barrier);\n"
" asm volatile(\n"
' "{\\n"\n'
' ".reg .pred p;\\n"\n'
' ".reg .b32 remAddr32;\\n"\n'
' "setp.ne.s32 p, %2, 0;\\n"\n'
' "@p mapa.shared::cluster.u32 remAddr32, %0, %1;\\n"\n'
' "@p mbarrier.arrive.shared::cluster.b64 _, [remAddr32];\\n"\n'
' "}\\n"\n'
' :: "r"(barrier_addr), "r"(cta_id), "r"(pred) : "memory");'
),
)
# Same cross-CTA arrive, but with an explicit arrival-count operand
# (``..., [remAddr32], count``). Matches the ``tma::cluster::arrive`` spelling.
device_intrinsic(
"_ptx_mbarrier_arrive_remote_count",
helper_name="tvm_builtin_ptx_mbarrier_arrive_remote_count",
c_signature="(void* barrier, int cta_id, int pred, int count)",
body=(
" unsigned int barrier_addr = __cvta_generic_to_shared(barrier);\n"
" asm volatile(\n"
' "{\\n"\n'
' ".reg .pred p;\\n"\n'
' ".reg .b32 remAddr32;\\n"\n'
' "setp.ne.s32 p, %2, 0;\\n"\n'
' "@p mapa.shared::cluster.u32 remAddr32, %0, %1;\\n"\n'
' "@p mbarrier.arrive.shared::cluster.b64 _, [remAddr32], %3;\\n"\n'
' "}\\n"\n'
' :: "r"(barrier_addr), "r"(cta_id), "r"(pred), "r"(count) : "memory");'
),
)
@register_codegen("ptx_mbarrier_arrive")
def _codegen_mbarrier_arrive(*args):
"""Dispatch by arg count: 1 -> local, 3 -> remote, 4 -> remote+count."""
if len(args) == 1:
result = CODEGEN_REGISTRY["tirx._ptx_mbarrier_arrive_local"](list(args))
elif len(args) == 3:
result = CODEGEN_REGISTRY["tirx._ptx_mbarrier_arrive_remote"](list(args))
elif len(args) == 4:
result = CODEGEN_REGISTRY["tirx._ptx_mbarrier_arrive_remote_count"](list(args))
else:
raise ValueError(f"ptx_mbarrier_arrive expects 1, 3, or 4 args, got {len(args)}")
return result[0] if isinstance(result, tuple) else result
# =============================================================================
# mbarrier.arrive.expect_tx — local + remote (cluster-mapped) forms.
# =============================================================================
device_intrinsic(
"_ptx_mbarrier_arrive_expect_tx_local",
helper_name="tvm_builtin_ptx_mbarrier_arrive_expect_tx",
c_signature="(void* barrier, int byte_count)",
body=(
" unsigned int barrier_addr = __cvta_generic_to_shared(barrier);\n"
' asm volatile("mbarrier.arrive.expect_tx.shared.b64 _, [%0], %1;"\n'
' :: "r"(barrier_addr), "r"(byte_count) : "memory");'
),
)
device_intrinsic(
"_ptx_mbarrier_arrive_expect_tx_remote",
helper_name="tvm_builtin_ptx_mbarrier_arrive_expect_tx_remote",
c_signature="(void* barrier, int cta_id, int pred, int byte_count)",
body=(
" unsigned int barrier_addr = __cvta_generic_to_shared(barrier);\n"
" asm volatile(\n"
' "{\\n"\n'
' ".reg .pred p;\\n"\n'
' ".reg .b32 remAddr32;\\n"\n'
' "setp.ne.s32 p, %2, 0;\\n"\n'
' "@p mapa.shared::cluster.u32 remAddr32, %0, %1;\\n"\n'
' "@p mbarrier.arrive.expect_tx.shared::cluster.b64 _, [remAddr32], %3;\\n"\n'
' "}\\n"\n'
' :: "r"(barrier_addr), "r"(cta_id), "r"(pred), "r"(byte_count) : "memory");'
),
)
@register_codegen("ptx_mbarrier_arrive_expect_tx")
def _codegen_mbarrier_arrive_expect_tx(*args):
"""Dispatch by arg count: 2 -> local, 4 -> remote. Remote arg order from
the user is (bar, byte_count, cta_id, pred); reorder to match the helper
signature (bar, cta_id, pred, byte_count)."""
if len(args) == 2:
result = CODEGEN_REGISTRY["tirx._ptx_mbarrier_arrive_expect_tx_local"](list(args))
elif len(args) == 4:
bar, byte_count, cta_id, pred = args
result = CODEGEN_REGISTRY["tirx._ptx_mbarrier_arrive_expect_tx_remote"](
[bar, cta_id, pred, byte_count]
)
else:
raise ValueError(f"ptx_mbarrier_arrive_expect_tx expects 2 or 4 args, got {len(args)}")
return result[0] if isinstance(result, tuple) else result
# =============================================================================
# mbarrier.try_wait.parity.shared::cta.b64 — 1 form. Body wraps the asm in a
# label loop (TIRx convention; the magic ``ticks = 0x989680`` is the timeout
# hint in ns).
# =============================================================================
device_intrinsic(
"ptx_mbarrier_try_wait",
c_signature="(void* barrier, int phase)",
body=(
" unsigned int barrier_addr_int = __cvta_generic_to_shared(barrier);\n"
" unsigned int ticks = 0x989680;\n"
" asm volatile(\n"
' "{\\n"\n'
' ".reg .pred P1;\\n"\n'
' "LAB_WAIT:\\n"\n'
' "mbarrier.try_wait.parity.shared::cta.b64 P1, [%0], %1, %2;\\n"\n'
' "@P1 bra.uni DONE;\\n"\n'
' "bra.uni LAB_WAIT;\\n"\n'
' "DONE:\\n"\n'
' "}\\n"\n'
' :: "r"(barrier_addr_int), "r"(phase), "r"(ticks) : "memory");'
),
)
# mbarrier.try_wait.parity.acquire.cluster — cluster-scope acquire wait used for
# cross-CTA barrier handshakes (e.g. the tmem-finished handoff).
device_intrinsic(
"ptx_mbarrier_try_wait_acquire_cluster",
c_signature="(void* barrier, int phase)",
body=(
" unsigned int barrier_addr_int = __cvta_generic_to_shared(barrier);\n"
" asm volatile(\n"
' "{\\n"\n'
' ".reg .pred P1;\\n"\n'
' "LAB_WAIT_AC:\\n"\n'
' "mbarrier.try_wait.parity.acquire.cluster.shared::cta.b64 P1, [%0], %1;\\n"\n'
' "@P1 bra.uni DONE_AC;\\n"\n'
' "bra.uni LAB_WAIT_AC;\\n"\n'
' "DONE_AC:\\n"\n'
' "}\\n"\n'
' :: "r"(barrier_addr_int), "r"(phase) : "memory");'
),
)
# =============================================================================
# mbarrier.try_wait.parity — ONE-SHOT non-blocking variant. Returns true
# if the requested parity has already been reached, false otherwise.
# The TIRx-standard ``ptx_mbarrier_try_wait`` above wraps this in a
# label loop that retries until success; this one-shot form is the
# building block for bounded-retry debug waits (Nymph's
# ``debug_bounded_wait`` lowering mode wraps it in a Python-counted
# loop so the kernel cannot hang forever at a mis-protocoled wait).
# =============================================================================
device_intrinsic(
"ptx_mbarrier_try_wait_once",
c_signature="(void* barrier, int phase, int ticks)",
return_type="uint32_t",
body=(
" unsigned int barrier_addr_int = __cvta_generic_to_shared(barrier);\n"
" unsigned int ticks_u = (unsigned int)ticks;\n"
" unsigned int result;\n"
" asm volatile(\n"
' "{\\n"\n'
' ".reg .pred P1;\\n"\n'
' "mbarrier.try_wait.parity.shared::cta.b64 P1, [%1], %2, %3;\\n"\n'
' "selp.u32 %0, 1, 0, P1;\\n"\n'
' "}\\n"\n'
' : "=r"(result) : "r"(barrier_addr_int), "r"(phase), "r"(ticks_u) : "memory");\n'
" return result;"
),
)
# =============================================================================
# elect.sync — TIRx uses the CUDA builtin ``tvm_builtin_elect_one_sync()``
# helper (declared in the CUDA header tags), not direct PTX.
# =============================================================================
device_intrinsic(
"ptx_elect_sync",
helper_name="tvm_builtin_elect_one_sync_op",
return_type="uint32_t",
body=" return tvm_builtin_elect_one_sync();",
extra_deps=("elect_one_sync",),
)
# =============================================================================
# __any_sync — warp-vote (pure CUDA helper).
# =============================================================================
device_intrinsic(
"ptx_any_sync",
c_signature="(unsigned mask, int pred)",
body=" return __any_sync(mask, pred);",
return_type="int",
tvm_return_type="int32",
)
# =============================================================================
# CUDA-side sync helpers (zero-arg void unless noted).
# =============================================================================
device_intrinsic("cuda_thread_fence", body=" __threadfence();")
device_intrinsic("cuda_warp_sync", body=" __syncwarp();")
device_intrinsic("cuda_cta_sync", body=" __syncthreads();")
device_intrinsic(
"cuda_grid_sync",
body=" namespace cg = cooperative_groups;\n cg::this_grid().sync();",
extra_deps=("cooperative_groups",),
)
device_intrinsic(
"cuda_cluster_sync",
body=(' asm("barrier.cluster.arrive.aligned;");\n asm("barrier.cluster.wait.aligned;");'),
)
device_intrinsic(
"cuda_warpgroup_sync",
c_signature="(int name_bar_id)",
body=' asm volatile("bar.sync %0, 128;" : : "r"(name_bar_id));',
)
device_intrinsic(
"cuda_syncthreads_and",
c_signature="(int predicate)",
body=" return __syncthreads_and(predicate);",
return_type="int",
tvm_return_type="int32",
)
device_intrinsic(
"cuda_syncthreads_or",
c_signature="(int predicate)",
body=" return __syncthreads_or(predicate);",
return_type="int",
tvm_return_type="int32",
)
# =============================================================================
# Additional mbarrier, grid-sync, and warp collective helpers.
# =============================================================================
# PTX mbarrier parity wait form:
# mbarrier.test_wait.parity{.sem.scope}{.shared{::cta}}.b64 waitComplete, [addr], phaseParity;
def _mbarrier_test_wait_parity_parts(_barrier, _phase, sem, scope, space):
sem = parse_str(sem)
scope = parse_str(scope)
space = parse_str(space)
if sem and sem not in ("acquire", "relaxed"):
raise ValueError(f"Unsupported mbarrier.test_wait.parity sem {sem!r}")
if scope and scope not in ("cta", "cluster"):
raise ValueError(f"Unsupported mbarrier.test_wait.parity scope {scope!r}")
if space not in ("shared", "shared::cta"):
raise ValueError(f"Unsupported mbarrier.test_wait.parity space {space!r}")
sem_scope = f".{sem}.{scope}" if sem else ""
name = (
"tvm_builtin_ptx_mbarrier_test_wait_parity"
f"{('_' + sem + '_' + scope) if sem else ''}_{space.replace('::', '_')}_b64"
)
body = (
" unsigned int ready = 0;\n"
" asm volatile(\n"
' "{\\n\\t"\n'
' ".reg .pred P1; \\n\\t"\n'
f' "mbarrier.test_wait.parity{sem_scope}.{space}.b64 P1, [%1], %2; \\n\\t"\n'
' "selp.b32 %0, 1, 0, P1; \\n\\t"\n'
' "}" : "=r"(ready) : "r"((unsigned int)__cvta_generic_to_shared(barrier)), '
'"r"(phase) : "memory");\n'
" return ready;"
)
return name, body
device_intrinsic(
"ptx_mbarrier_test_wait_parity",
n_attrs=3,
helper_name=lambda *a: _mbarrier_test_wait_parity_parts(*a)[0],
c_signature="(void* barrier, int phase)",
return_type="unsigned int",
tvm_return_type="uint32",
body=lambda *a: _mbarrier_test_wait_parity_parts(*a)[1],
)
device_intrinsic(
"cuda_ballot_sync",
helper_name="tvm_builtin_ballot_sync",
c_signature="(unsigned int mask, int pred)",
return_type="unsigned int",
body=" return __ballot_sync(mask, pred);",
)
device_intrinsic(
"cuda_reduce_add_sync_u32",
helper_name="tvm_builtin_reduce_add_sync_u32",
c_signature="(unsigned int mask, unsigned int value)",
return_type="unsigned int",
body=" return __reduce_add_sync(mask, value);",
)
device_intrinsic(
"cuda_reduce_min_sync_u32",
helper_name="tvm_builtin_reduce_min_sync_u32",
c_signature="(unsigned int mask, unsigned int value)",
return_type="unsigned int",
body=" return __reduce_min_sync(mask, value);",
)
# =============================================================================
# griddepcontrol.wait / griddepcontrol.launch_dependents (sm_90+)
# Programmatic Dependent Launch (PDL) synchronization. Both carry memory
# clobber to prevent CSE / cross-barrier reordering.
# =============================================================================
device_intrinsic(
"ptx_griddepcontrol_wait",
body=' asm volatile("griddepcontrol.wait;" ::: "memory");',
)
device_intrinsic(
"ptx_griddepcontrol_launch_dependents",
body=' asm volatile("griddepcontrol.launch_dependents;" ::: "memory");',
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,71 @@
# 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.
"""PTX data types for CUDA codegen."""
import enum
import tvm_ffi
from_string_func = tvm_ffi.get_global_func("tirx.intrinsics.cuda.PTXDTypeFromString")
to_string_func = tvm_ffi.get_global_func("tirx.intrinsics.cuda.PTXDTypeToString")
class PTXDataType(enum.Enum):
"""
A Python equivalent of the provided C++ DataType enum class.
Inherits from IntEnum so that members behave both as enum members
and as integers, mirroring the C++ behavior.
see also src/target/source/ptx.cc
"""
INT4 = 0
UINT4 = 1
INT8 = 2
UINT8 = 3
INT16 = 4
UINT16 = 5
INT32 = 6
UINT32 = 7
INT64 = 8
UINT64 = 9
FLOAT4_E2M1FN = 10
FLOAT6_E2M3FN = 11
FLOAT6_E3M2FN = 12
FLOAT8_E4M3FN = 13
FLOAT8_E4M3FNUZ = 14
FLOAT8_E5M2 = 15
FLOAT8_E8M0FNU = 16
FLOAT16 = 17
BFLOAT16 = 18
FLOAT16X2 = 19
FLOAT32 = 20
TENSOR_FLOAT32 = 21
FLOAT64 = 22
BIT1 = 23
BIT8 = 24
BIT16 = 25
BIT32 = 26
BIT64 = 27
@classmethod
def from_string(cls, s_type: str) -> "PTXDataType":
return PTXDataType(from_string_func(s_type))
def to_string(self) -> str:
return to_string_func(self.value)
@@ -0,0 +1,82 @@
# 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.
"""Common utility functions for CUDA op codegen."""
def parse_str(arg) -> str:
"""Parse TIR StringImm or Python str to a plain str.
TIR StringImm values stringify to quoted strings, e.g., ``'"float16"'``;
Python strs do not. Idempotent — passing an already-parsed str returns it
unchanged, so dispatchers that parse once before forwarding to inner
codegens won't double-strip the value.
"""
s = str(arg)
if len(s) >= 2 and s[0] == '"' and s[-1] == '"':
return s[1:-1]
return s
def is_power_of_two(n: int) -> bool:
"""Check if n is a power of two."""
return n > 0 and (n & (n - 1)) == 0
def validate_cta_group(cta_group, context: str = "") -> int:
"""Validate that cta_group is 1 or 2 and return it as int.
Args:
cta_group: The cta_group value (can be int or TIR IntImm)
context: Optional context string for error message (e.g., "allocating Tensor Memory")
Returns:
The validated cta_group as int
Raises:
ValueError: If cta_group is not 1 or 2
"""
cta_group = int(cta_group)
if cta_group not in [1, 2]:
ctx = f" involved in {context}" if context else ""
raise ValueError(
f"The number of cta_group{ctx} is incorrect, expected 1 or 2, got {cta_group}"
)
return cta_group
def validate_power_of_two_range(value, min_val: int, max_val: int, name: str) -> int:
"""Validate that value is within range and is a power of two.
Args:
value: The value to validate
min_val: Minimum allowed value (inclusive)
max_val: Maximum allowed value (inclusive)
name: Name of the parameter for error messages
Returns:
The validated value as int
Raises:
ValueError: If value is out of range or not a power of two
"""
value = int(value)
if not (min_val <= value <= max_val and is_power_of_two(value)):
raise ValueError(
f"The {name} is invalid, expect a value within range [{min_val}, {max_val}] "
f"and be a power of 2, got {value}"
)
return value
@@ -0,0 +1,403 @@
# 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.
# pylint: disable=redefined-builtin, invalid-name, too-many-arguments, too-many-locals, too-many-positional-arguments
"""PTX WGMMA operations (Hopper warpgroup MMA).
One ``device_intrinsic`` registration per PTX form table entry. Bodies are
hand-written ``asm volatile(...)`` strings. Variable-arity register vectors
(``mma_async`` accumulators / A fragments) materialize via the same
device_intrinsic with an attr-driven ``c_signature`` callable.
"""
import tvm
from ._schema import device_intrinsic
from .registry import CODEGEN_REGISTRY, register_codegen
from .types import PTXDataType
from .utils import parse_str
# =============================================================================
# wgmma.fence / commit_group / wait_group — one PTX form each.
# =============================================================================
device_intrinsic(
"ptx_wgmma_fence",
helper_name="ptx_wgmma_fence",
body=' asm volatile("wgmma.fence.sync.aligned;" ::: "memory");',
)
device_intrinsic(
"ptx_wgmma_commit_group",
helper_name="ptx_wgmma_commit_group",
body=' asm volatile("wgmma.commit_group.sync.aligned;" ::: "memory");',
)
device_intrinsic(
"ptx_wgmma_wait_group",
n_attrs=1,
helper_name=lambda n: f"ptx_wgmma_wait_group_{int(n)}",
body=lambda n: f' asm volatile("wgmma.wait_group.sync.aligned {int(n)};" ::: "memory");',
)
# =============================================================================
# wgmma_encode_matrix_descriptor — pure-C bitfield struct fill (no asm).
# =============================================================================
device_intrinsic(
"ptx_wgmma_encode_matrix_descriptor",
helper_name="ptx_wgmma_encode_matrix_descriptor",
c_signature="(uint64_t* desc, void* addr, int ldo, int sdo, int swizzle)",
body=(
" GmmaDescriptor _desc{}; // value-init: reading uncovered pad bits is UB\n"
"\n"
" switch (swizzle) {\n"
" case 0: _desc.bitfield.layout_type_ = uint8_t(0); break; // No swizzle\n"
" case 1: _desc.bitfield.layout_type_ = uint8_t(3); break; // 32B swizzle\n"
" case 2: _desc.bitfield.layout_type_ = uint8_t(2); break; // 64B swizzle\n"
" case 3: _desc.bitfield.layout_type_ = uint8_t(1); break; // 128B swizzle\n"
" }\n"
"\n"
" uint32_t start_address = __cvta_generic_to_shared(addr);\n"
" _desc.bitfield.start_address_ = static_cast<uint16_t>(start_address >> 4);\n"
"\n"
" constexpr uint8_t base_offset = 0;\n"
" _desc.bitfield.base_offset_ = base_offset;\n"
"\n"
" _desc.bitfield.stride_byte_offset_ = static_cast<uint32_t>(sdo);\n"
" _desc.bitfield.leading_byte_offset_ = static_cast<uint32_t>(ldo);\n"
"\n"
" *desc = (uint64_t)_desc;"
),
extra_deps=("gmma_descriptor",),
)
# =============================================================================
# wgmma_noop_barrier — empty asm with one inout register operand. Two
# device_intrinsic calls, one per supported dtype; dispatcher picks the form
# based on the operand's runtime dtype.
# =============================================================================
device_intrinsic(
"ptx_wgmma_noop_barrier_uint32",
helper_name="ptx_wgmma_fence_uint32_t",
c_signature="(uint32_t reg)",
body=' asm volatile("" : "+r"(reg) :: "memory");',
)
device_intrinsic(
"ptx_wgmma_noop_barrier_float32",
helper_name="ptx_wgmma_fence_float",
c_signature="(float reg)",
body=' asm volatile("" : "+f"(reg) :: "memory");',
)
@register_codegen("ptx_wgmma_noop_barrier")
def codegen_ptx_wgmma_noop_barrier(reg):
dtype = str(reg.dtype)
dtype_enum = PTXDataType.from_string(dtype)
if dtype_enum == PTXDataType.UINT32:
op_name = "tirx.ptx_wgmma_noop_barrier_uint32"
elif dtype_enum == PTXDataType.FLOAT32:
op_name = "tirx.ptx_wgmma_noop_barrier_float32"
else:
raise ValueError(f"Only support uint32/float32 for wgmma_fence, but got {dtype}.")
result = CODEGEN_REGISTRY[op_name]([reg])
return result[0] if isinstance(result, tuple) else result
# =============================================================================
# wgmma.mma_async ss / rs — 2 PTX form table entries. Accumulator count and
# A-register count vary with (M, N, K, in_dtype) but are fully determined by
# attrs at codegen time.
#
# Args layout for ss form (forwarded operand args first, then 9 attr args):
# *p_acc[0..num_accums-1], p_descA, p_descB, p_scaleD,
# M, N, K, in_dtype, out_dtype, transA, transB, scaleA, scaleB
#
# Args layout for rs form:
# *p_acc[0..num_accums-1], *p_A[0..num_A_regs-1], p_descB, p_scaleD,
# M, N, K, in_dtype, out_dtype, transA, transB, scaleA, scaleB
# =============================================================================
def _coerce_wgmma_attrs(attrs):
"""Decode the trailing 9 attrs (M, N, K, in_dtype, out_dtype, transA,
transB, scaleA, scaleB) into native Python types."""
M, N, K = int(attrs[0]), int(attrs[1]), int(attrs[2])
in_dtype = parse_str(attrs[3])
out_dtype = parse_str(attrs[4])
transA = bool(int(attrs[5])) if hasattr(attrs[5], "value") else bool(attrs[5])
transB = bool(int(attrs[6])) if hasattr(attrs[6], "value") else bool(attrs[6])
scaleA = bool(int(float(attrs[7])))
scaleB = bool(int(float(attrs[8])))
if out_dtype != "float32":
raise ValueError("WGMMA codegen only supports float32 as output dtype.")
allow_transpose = in_dtype in {"float16", "bfloat16"}
if not allow_transpose and (transA or transB):
raise ValueError("Transpose is only supported for .f16/.bf16 types in WGMMA.")
return M, N, K, in_dtype, out_dtype, transA, transB, scaleA, scaleB, allow_transpose
def _safe(s):
return s.replace("::", "_").replace(".", "_")
def _wgmma_helper_name(prefix, M, N, K, in_dtype, out_dtype, transA, transB, scaleA, scaleB):
return (
f"{prefix}_{M}x{N}x{K}_{_safe(in_dtype)}_{_safe(out_dtype)}"
f"_{1 if scaleA else 0}_{1 if scaleB else 0}"
f"_{1 if transA else 0}_{1 if transB else 0}"
)
def _wgmma_in_bits(in_dtype):
return tvm.runtime.DataType(in_dtype).bits
def _wgmma_ss_parts(*args):
M, N, K, in_dtype, out_dtype, transA, transB, scaleA, scaleB, allow_transpose = (
_coerce_wgmma_attrs(args[-9:])
)
num_accums = M * N // 128
name = _wgmma_helper_name(
"ptx_wgmma_mma_async_ss", M, N, K, in_dtype, out_dtype, transA, transB, scaleA, scaleB
)
sig = (
"("
+ ", ".join(
[f"float& p_acc{i}" for i in range(num_accums)]
+ ["uint64_t p_descA", "uint64_t p_descB", "int p_scaleD"]
)
+ ")"
)
descA_idx = num_accums
descB_idx = num_accums + 1
scaleD_idx = num_accums + 2
scaleA_idx = num_accums + 3
scaleB_idx = num_accums + 4
transA_idx = num_accums + 5
transB_idx = num_accums + 6
accum_r_list = ", ".join(f"%{i}" for i in range(num_accums))
accum_constraints = ", ".join(f'"+f"(p_acc{i})' for i in range(num_accums))
itype = PTXDataType.from_string(in_dtype)
otype = PTXDataType.from_string(out_dtype)
if allow_transpose:
transpose_r_code = f", %{transA_idx}, %{transB_idx}"
transpose_constraints = f', "n"({1 if transA else 0}), "n"({1 if transB else 0})'
else:
transpose_r_code = ""
transpose_constraints = ""
instr = (
f"wgmma.mma_async.sync.aligned.m{M}n{N}k{K}"
f"{otype.to_string()}{itype.to_string()}{itype.to_string()}"
)
asm_inputs = (
f'"l"(p_descA), "l"(p_descB), "r"(p_scaleD),'
f' "n"({1 if scaleA else 0}), "n"({1 if scaleB else 0})'
f"{transpose_constraints}"
)
body = (
" asm volatile(\n"
' "{ \\n"\n'
' ".reg .pred p;\\n"\n'
f' "setp.ne.b32 p, %{scaleD_idx}, 0;\\n"\n'
f' "{instr} "\n'
f' "{{{accum_r_list}}},"\n'
f' "%{descA_idx}, %{descB_idx},"\n'
f' "p, %{scaleA_idx}, %{scaleB_idx}{transpose_r_code};\\n"\n'
' "}\\n"\n'
f" : {accum_constraints}\n"
f" : {asm_inputs}\n"
" );"
)
return name, sig, body
device_intrinsic(
"_ptx_wgmma_mma_async_ss_impl",
n_attrs=9,
helper_name=lambda *a: _wgmma_ss_parts(*a)[0],
c_signature=lambda *a: _wgmma_ss_parts(*a)[1],
body=lambda *a: _wgmma_ss_parts(*a)[2],
)
def _wgmma_rs_parts(*args):
M, N, K, in_dtype, out_dtype, transA, transB, scaleA, scaleB, allow_transpose = (
_coerce_wgmma_attrs(args[-9:])
)
num_accums = M * N // 128
in_bits = _wgmma_in_bits(in_dtype)
num_A_regs = M * K // 128 // (32 // in_bits)
name = _wgmma_helper_name(
"ptx_wgmma_mma_async_rs", M, N, K, in_dtype, out_dtype, transA, transB, scaleA, scaleB
)
sig = (
"("
+ ", ".join(
[f"float& p_acc{i}" for i in range(num_accums)]
+ [f"uint32_t& p_A{i}" for i in range(num_A_regs)]
+ ["uint64_t p_descB", "int p_scaleD"]
)
+ ")"
)
accum_r_list = ", ".join(f"%{i}" for i in range(num_accums))
A_reg_r_list = ", ".join(f"%{num_accums + i}" for i in range(num_A_regs))
base_idx = num_accums + num_A_regs
descB_idx = base_idx
scaleD_idx = base_idx + 1
scaleA_idx = base_idx + 2
scaleB_idx = base_idx + 3
transB_idx = base_idx + 4
accum_constraints = ", ".join(f'"+f"(p_acc{i})' for i in range(num_accums))
A_reg_constraints = ", ".join(f'"r"(p_A{i})' for i in range(num_A_regs))
itype = PTXDataType.from_string(in_dtype)
otype = PTXDataType.from_string(out_dtype)
if allow_transpose:
transpose_r_code = f", %{transB_idx}"
transpose_constraints = f', "n"({1 if transB else 0})'
else:
transpose_r_code, transpose_constraints = "", ""
instr = (
f"wgmma.mma_async.sync.aligned.m{M}n{N}k{K}"
f"{otype.to_string()}{itype.to_string()}{itype.to_string()}"
)
asm_inputs = (
f'{A_reg_constraints}, "l"(p_descB), "r"(p_scaleD),'
f' "n"({1 if scaleA else 0}), "n"({1 if scaleB else 0})'
f"{transpose_constraints}"
)
body = (
" asm volatile(\n"
' "{ \\n"\n'
' ".reg .pred p;\\n"\n'
f' "setp.ne.b32 p, %{scaleD_idx}, 0;\\n"\n'
f' "{instr} "\n'
f' "{{{accum_r_list}}},"\n'
f' "{{{A_reg_r_list}}}, %{descB_idx},"\n'
f' "p, %{scaleA_idx}, %{scaleB_idx}{transpose_r_code};\\n"\n'
' "}\\n"\n'
f" : {accum_constraints}\n"
f" : {asm_inputs}\n"
" );"
)
return name, sig, body
device_intrinsic(
"_ptx_wgmma_mma_async_rs_impl",
n_attrs=9,
helper_name=lambda *a: _wgmma_rs_parts(*a)[0],
c_signature=lambda *a: _wgmma_rs_parts(*a)[1],
body=lambda *a: _wgmma_rs_parts(*a)[2],
)
# User-facing wrappers: just normalise types + reorder positional args to
# put operands first, then attrs, matching the schema convention.
def _wgmma_user_wrapper_ss(*args):
M, N, K, in_dtype, out_dtype, transA, transB, scaleA, scaleB, scaleD, descA, descB, *accums = (
args
)
M = int(M)
N = int(N)
K = int(K)
in_dtype = parse_str(in_dtype)
out_dtype = parse_str(out_dtype)
transA = bool(transA)
transB = bool(transB)
scaleA = bool(int(float(scaleA)))
scaleB = bool(int(float(scaleB)))
expected = M * N // 128
if len(accums) != expected:
raise ValueError(
"The number of arguments is incorrect. Expected "
f"{12 + expected} total args (meaning {expected} accumulator args), "
f"but got {len(accums)}."
)
return [
*accums,
descA,
descB,
scaleD,
M,
N,
K,
in_dtype,
out_dtype,
transA,
transB,
scaleA,
scaleB,
]
@register_codegen("ptx_wgmma_mma_async_ss")
def codegen_ptx_wgmma_mma_async_ss(*args):
forwarded = _wgmma_user_wrapper_ss(*args)
result = CODEGEN_REGISTRY["tirx._ptx_wgmma_mma_async_ss_impl"](forwarded)
return result[0] if isinstance(result, tuple) else result
def _wgmma_user_wrapper_rs(*args):
M, N, K, in_dtype, out_dtype, transA, transB, scaleA, scaleB, scaleD, descB, *reg_list = args
M = int(M)
N = int(N)
K = int(K)
in_dtype = parse_str(in_dtype)
out_dtype = parse_str(out_dtype)
transA = bool(transA)
transB = bool(transB)
scaleA = bool(int(float(scaleA)))
scaleB = bool(int(float(scaleB)))
if out_dtype != "float32":
raise ValueError("This generator only supports float32 as the output dtype for WGMMA.")
in_dtype_bits = tvm.runtime.DataType(in_dtype).bits
if in_dtype_bits is None:
raise ValueError(f"Bit width not defined for input dtype: {in_dtype}")
expected_A_cnt = M * K // 128 // (32 // in_dtype_bits)
expected_accm_cnt = M * N // 128
if len(reg_list) != expected_A_cnt + expected_accm_cnt:
raise ValueError(
f"Incorrect number of A registers. Expected {expected_A_cnt}, got {len(reg_list)}"
)
A_regs = reg_list[:expected_A_cnt]
accums = reg_list[expected_A_cnt:]
return [
*accums,
*A_regs,
descB,
scaleD,
M,
N,
K,
in_dtype,
out_dtype,
transA,
transB,
scaleA,
scaleB,
]
@register_codegen("ptx_wgmma_mma_async_rs")
def codegen_ptx_wgmma_mma_async_rs(*args):
forwarded = _wgmma_user_wrapper_rs(*args)
result = CODEGEN_REGISTRY["tirx._ptx_wgmma_mma_async_rs_impl"](forwarded)
return result[0] if isinstance(result, tuple) else result
@@ -0,0 +1,24 @@
# 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.
from .copy import *
from .copy_async import *
from .elementwise import *
from .gemm import *
from .gemm_async import *
from .permute_layout import *
from .reduction import *
@@ -0,0 +1,283 @@
# 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.
"""Common utilities for CUDA operator scheduling (basic helpers and copy ops)."""
import functools
import operator
import re
from enum import Enum
from tvm.arith.analyzer import Analyzer
from tvm.runtime import DataType
from tvm.script import tirx as T
from tvm.tirx import Buffer, BufferRegion, PrimFunc
from tvm.tirx.operator.tile_primitive import DispatchContext, fail
from tvm.tirx.stmt import TilePrimitiveCall
def next_power_of_2(x: int) -> int:
"""Return the smallest power of 2 greater than or equal to x."""
if x <= 1:
return 1
return 1 << (x - 1).bit_length()
def get_st_extent(buffer_region: BufferRegion):
"""Get the start and extent of a buffer region."""
region = buffer_region.region
return [r.min for r in region], [r.extent for r in region]
def get_indices(nth, start, extent):
"""Convert a fused index into multi-dimensional indices."""
assert len(start) == len(extent)
if len(start) == 1:
return [start[0] + nth]
relative = []
for e in reversed(extent):
relative.append(nth % e)
nth //= e
return [r + s for r, s in zip(reversed(relative), start)]
def smem_desc_add_16B_offset(desc_val, offset):
"""Add a 16B-aligned byte offset to the lower 32 bits of a SMEM descriptor.
Uses the SmemDescriptor union defined in the CUDA header (header.py).
All callers must share a single implementation to avoid codegen conflicts.
"""
func_name = "tvm_builtin_smem_desc_add_16B_offset"
source_code = f"""
__forceinline__ __device__ uint64_t {func_name}(uint64_t desc_base, int32_t offset) {{
SmemDescriptor desc;
desc.desc_ = desc_base;
desc.lo += static_cast<uint32_t>(offset);
return desc.desc_;
}}
"""
return T.cuda.func_call(
func_name, desc_val, offset, source_code=source_code, return_type="uint64"
)
class CopyInstType(Enum):
"""Enumeration of instruction types for memory operations."""
NORMAL = 0
CP_ASYNC = 1
def validate_copy_op(
op_call: TilePrimitiveCall,
sctx: DispatchContext, # pylint: disable=unused-argument
) -> bool:
"""Sanity check for copy op"""
dst_buffer_region, src_buffer_region = op_call.args[:2]
src: Buffer = src_buffer_region.buffer
dst: Buffer = dst_buffer_region.buffer
if not (src.layout and dst.layout and src.dtype == dst.dtype):
return False
# Extract regions and validate dimensions
analyzer = Analyzer()
src_region, dst_region = src_buffer_region.region, dst_buffer_region.region
# Extract extents and validate non-unit dimensions match
src_extent_ = [r.extent for r in src_region if r.extent != 1]
dst_extent_ = [r.extent for r in dst_region if r.extent != 1]
if len(src_extent_) != len(dst_extent_) or not all(
analyzer.can_prove_equal(s, d) for s, d in zip(src_extent_, dst_extent_)
):
return False
return True
def get_vec_len(
dst_buffer_region: BufferRegion,
src_buffer_region: BufferRegion,
vec_candidates: list[int],
thread_cnt=1,
) -> int | None:
"""Get the vector length for the copy operation."""
dst: Buffer = dst_buffer_region.buffer
src: Buffer = src_buffer_region.buffer
# layout=None (flat local buffer) is treated as trivial for vectorization purposes
if not (
(dst.layout is None or dst.layout.is_trivial())
and (src.layout is None or src.layout.is_trivial())
):
return None
# Extract regions and validate dimensions
analyzer = Analyzer()
src_st, src_extent = get_st_extent(src_buffer_region)
dst_st, dst_extent = get_st_extent(dst_buffer_region)
# Thread and vectorization setup
DataType(src.dtype).bits # in bits
n_elements = functools.reduce(operator.mul, src_extent, 1)
if n_elements % thread_cnt != 0:
return None
# Find valid vector length
for vec_len in vec_candidates:
if vec_len > 0 and all(
analyzer.can_prove_equal(x % vec_len, 0)
for x in [
src_st[-1],
dst_st[-1],
src.shape[-1] if len(src.shape) > 1 else 0,
dst.shape[-1] if len(dst.shape) > 1 else 0,
src_extent[-1],
dst_extent[-1],
n_elements // thread_cnt,
]
):
return vec_len
else:
return None
def copy_vec_load_impl(
op_call: TilePrimitiveCall, sctx: DispatchContext, inst_type: CopyInstType
) -> PrimFunc | None:
"""Schedule copy operation between global and local/shared memory on CUDA across a CTA/thread.
The implementation tries to vectorize the copy operation and parallelize over
threads in a CTA/using a single thread.
"""
dst_buffer_region, src_buffer_region = op_call.args[:2]
src: Buffer = src_buffer_region.buffer
dst: Buffer = dst_buffer_region.buffer
if not (
(src.scope() == "global" and dst.scope().startswith("shared"))
or (src.scope().startswith("shared") and dst.scope() == "global")
or (src.scope() == "global" and dst.scope() == "local")
or (src.scope() == "local" and dst.scope() == "global")
or (src.scope().startswith("shared") and dst.scope() == "local")
or (dst.scope().startswith("shared") and src.scope() == "local")
):
fail(f"unsupported memory scopes src={src.scope()} dst={dst.scope()}")
# Thread and vectorization setup
if sctx.is_cta:
tx = sctx.launch_params["threadIdx.x"].dom.extent
assert "threadIdx.y" not in sctx.launch_params and "threadIdx.z" not in sctx.launch_params
elif sctx.is_thread:
tx = 1
else:
fail(f"unsupported exec_scope {sctx.scope_kind}")
elem_size = DataType(src.dtype).bits # in bits
vec_len = op_call.config.get("vec_len", None)
if vec_len is None:
vec_len = get_vec_len(
dst_buffer_region,
src_buffer_region,
[128 // elem_size, 64 // elem_size, 32 // elem_size, 1],
thread_cnt=tx,
)
if vec_len is None:
fail("no valid vector length; check alignment/extents/thread-count")
# cp-size (the size of data in bytes) can only be 4, 8 and 16 for cp.async
if inst_type == CopyInstType.CP_ASYNC:
cp_size = vec_len * elem_size // 8 # in bytes
if cp_size not in [4, 8, 16]:
fail("invalid cp.async cp_size; expected 4, 8 or 16 bytes")
src_st, src_extent = get_st_extent(src_buffer_region)
dst_st, dst_extent = get_st_extent(dst_buffer_region)
n_elements = functools.reduce(operator.mul, src_extent, 1)
if sctx.is_cta:
# fmt: off
@T.prim_func
def impl():
"""Implement copy operation with vectorized loads/stores."""
for s in T.serial(0, n_elements // (tx * vec_len)):
for tid_x in T.thread_binding(tx, "threadIdx.x"):
if inst_type == CopyInstType.NORMAL:
for vec in T.vectorized(vec_len):
fused = T.meta_var((s * tx + tid_x) * vec_len + vec)
dst_indices = T.meta_var(get_indices(fused, dst_st, dst_extent))
src_indices = T.meta_var(get_indices(fused, src_st, src_extent))
dst[tuple(dst_indices)] = src[tuple(src_indices)]
elif inst_type == CopyInstType.CP_ASYNC:
fused = T.meta_var((s * tx + tid_x) * vec_len)
dst_indices = T.meta_var(get_indices(fused, dst_st, dst_extent))
src_indices = T.meta_var(get_indices(fused, src_st, src_extent))
T.evaluate(T.ptx.cp_async(dst.ptr_to(dst_indices), src.ptr_to(src_indices), cp_size)) # noqa: E501
if dst.scope().startswith("shared") and inst_type == CopyInstType.NORMAL:
T.tvm_storage_sync("shared")
# fmt: on
elif sctx.is_thread:
# fmt: off
@T.prim_func(check_well_formed=False)
def impl():
for s in T.serial(0, n_elements // (vec_len)):
if inst_type == CopyInstType.NORMAL:
for vec in T.vectorized(vec_len):
fused = T.meta_var(s * vec_len + vec)
dst_indices = T.meta_var(get_indices(fused, dst_st, dst_extent))
src_indices = T.meta_var(get_indices(fused, src_st, src_extent))
dst[tuple(dst_indices)] = src[tuple(src_indices)]
elif inst_type == CopyInstType.CP_ASYNC:
fused = T.meta_var(s * vec_len)
dst_indices = T.meta_var(get_indices(fused, dst_st, dst_extent))
src_indices = T.meta_var(get_indices(fused, src_st, src_extent))
T.evaluate(T.ptx.cp_async(dst.ptr_to(dst_indices), src.ptr_to(src_indices), cp_size)) # noqa: E501
# fmt: on
else:
fail(f"unsupported exec_scope {sctx.scope_kind}")
return impl
def match_scope(scope: str | None, pattern: str) -> bool:
"""Glob-lite scope matching: 'shared*' => prefix match; otherwise exact.
Returns True when scope is None (meaning "any scope is fine").
"""
if scope is None:
return True
if pattern.endswith("*"):
return scope.startswith(pattern[:-1])
return scope == pattern
def get_thread_cnt(sctx: DispatchContext) -> int | None:
"""Get thread count for the current execution scope."""
scope_name = sctx.scope_kind
if scope_name == "cta":
return sctx.launch_params["threadIdx.x"].dom.extent
if scope_name == "warpgroup":
return 128
if scope_name == "warp":
return 32
if scope_name == "thread":
return 1
return None
def sm_version_ok(
op: TilePrimitiveCall, sctx: DispatchContext, min_version: int
) -> tuple[bool, str | None]:
"""Check if SM version >= min_version. Usable as a dispatch predicate."""
target_arch = sctx.target.arch if hasattr(sctx.target, "arch") else ""
sm_match = re.match(r"sm_(\d+)", target_arch)
sm_version = int(sm_match.group(1)) if sm_match else 0
ok = sm_version >= min_version
return (ok, None if ok else f"sm_version {sm_version} < {min_version}")
@@ -0,0 +1,27 @@
# 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.
from .fallback import *
from .gmem_smem import *
from .ld_stmatrix import *
from .reg import *
from .utils import (
_is_valid_copy,
_is_valid_smem_tmem_copy,
_scope_allowed,
_single_thread_exec,
)
@@ -0,0 +1,556 @@
# 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.
"""Shared partition / layout algorithm for synthesized-partition copy
dispatches (currently ``gmem_smem`` and ``ldgsts``).
``gmem_smem`` (sync ``Tx.copy`` global ↔ shared) and ``ldgsts`` (async
``Tx.copy_async`` global → shared via cp.async / SASS LDGSTS) share the
same algorithm to pick a vec-isolating + thread-distributing layout for
``G ↔ S`` copies. Only emit-time details differ (which copy instruction
to call, allowed vec widths). All the layout/partition logic lives here.
"""
from tvm import arith
from tvm.tirx.layout import ComposeLayout, Iter, S, SwizzleLayout, TileLayout
from tvm.tirx.operator.tile_primitive.registry import DispatchContext
def _alignment_ok(vec_len: int, terms) -> bool:
"""Every term must be a multiple of ``vec_len``. Constants checked
directly; Expr / symbolic terms checked via ``arith.Analyzer``.
``vec_len=1`` always passes (the scalar fallback). When a symbolic
term can't be proved divisible, returns ``False`` conservatively —
the candidate loop will then try a smaller ``vec_len``.
"""
if vec_len <= 1:
return True
analyzer = arith.Analyzer()
for t in terms:
if isinstance(t, int):
if t % vec_len != 0:
return False
else:
if not analyzer.can_prove_equal(t % vec_len, 0):
return False
return True
# scope_kind → name of the scope_id that decomposes the scope into per-thread.
_TID_AXIS_FOR_SCOPE = {
"warp": "laneid",
"warpgroup": "tid_in_wg",
"cta": "tx",
}
def _thread_cnt(sctx: DispatchContext) -> int:
"""Total threads active in the current scope = ∏ intra-axis extents.
For thread scope ``sctx.intra`` is empty → returns 1.
"""
n = 1
for ext, _off in sctx.intra.values():
n *= int(ext)
return n
# -----------------------------------------------------------------------------
# Layout primitives
# -----------------------------------------------------------------------------
def _contig_group(iters: list) -> list[int]:
"""Indices (in iters) of the maximal physical-contiguous chain starting
at the stride=1 iter, ordered stride-ascending.
Returns [] if no stride=1 iter exists.
"""
one_idx = next(
(i for i, it in enumerate(iters) if int(it.stride) == 1),
None,
)
if one_idx is None:
return []
chain = [one_idx]
acc = int(iters[one_idx].extent)
used = {one_idx}
while True:
nxt = next(
(i for i, it in enumerate(iters) if i not in used and int(it.stride) == acc),
None,
)
if nxt is None:
break
chain.append(nxt)
acc *= int(iters[nxt].extent)
used.add(nxt)
return chain
def _try_split_vec(iters: list, vec_len: int):
"""Try to walk ``vec_len`` consecutive elements along the contig chain.
Returns ``(new_iters, selected_positions)`` on success, ``None`` on
failure. ``new_iters`` may contain a freshly-split iter (replacing one
entry with its "outer" half, with the "inner" half appended at the end);
``selected_positions`` are positions in ``new_iters`` that together
cover the ``vec_len`` contig elements.
"""
chain = _contig_group(iters)
if not chain:
return None
rem = vec_len
new_iters = list(iters)
selected: list[int] = []
for orig_idx in chain:
if rem == 0:
break
it = new_iters[orig_idx]
ext = int(it.extent)
if ext <= rem:
if rem % ext != 0:
return None
selected.append(orig_idx)
rem //= ext
else:
if ext % rem != 0:
return None
stride = int(it.stride)
outer = Iter(ext // rem, stride * rem, it.axis)
inner = Iter(rem, stride, it.axis)
new_iters[orig_idx] = outer
new_iters.append(inner)
selected.append(len(new_iters) - 1)
rem = 0
break
if rem != 0:
return None
return new_iters, selected
def _isolated_shape(iters: list, selected: list[int]) -> tuple[list[int], list[tuple[int, int]]]:
"""Build the isolated shape: each selected iter is its own segment;
adjacent unselected iters are merged into a single segment.
Returns ``(shape, segments)`` where ``segments[i] = (start, end)`` is
the half-open range in ``iters`` covered by shape entry ``i``.
"""
sel_set = set(selected)
shape: list[int] = []
segments: list[tuple[int, int]] = []
cur_start = None
cur_ext = 1
for i, it in enumerate(iters):
if i in sel_set:
if cur_start is not None:
shape.append(cur_ext)
segments.append((cur_start, i))
cur_start = None
cur_ext = 1
shape.append(int(it.extent))
segments.append((i, i + 1))
else:
if cur_start is None:
cur_start = i
cur_ext *= int(it.extent)
if cur_start is not None:
shape.append(cur_ext)
segments.append((cur_start, len(iters)))
return shape, segments
def _vec_perm(iters: list, selected: list[int]) -> list[int]:
"""Reorder ``iters`` into ``[outer, vec]``, both ordered by stride
descending so the stride=1 iter ends up at the very last position."""
sel_set = set(selected)
unsel_sorted = sorted(
(i for i in range(len(iters)) if i not in sel_set),
key=lambda i: -int(iters[i].stride),
)
sel_sorted = sorted(selected, key=lambda i: -int(iters[i].stride))
return list(unsel_sorted) + sel_sorted
def _try_split_thread(iters: list, vec_selected: list[int], thread_cnt: int):
"""After ``_try_split_vec``, carve ``thread_cnt`` from the OUTER tail
(smallest-stride outer iter, then towards bigger stride if needed).
Unlike vec split, this doesn't require physical contiguity — T
consecutive fused indices map to per-thread offsets via the layout's
stride (which may be > 1).
Returns ``(new_iters, thread_selected_positions)`` on success, ``None``
on failure (outer doesn't divide T cleanly, or no outer iters left).
"""
if thread_cnt == 1:
return list(iters), []
vec_set = set(vec_selected)
outer = [i for i in range(len(iters)) if i not in vec_set]
if not outer:
return None
outer_by_stride_desc = sorted(outer, key=lambda i: -int(iters[i].stride))
rem = thread_cnt
new_iters = list(iters)
thread_selected: list[int] = []
for orig_idx in reversed(outer_by_stride_desc):
if rem == 0:
break
it = new_iters[orig_idx]
ext = int(it.extent)
if ext <= rem:
if rem % ext != 0:
return None
thread_selected.append(orig_idx)
rem //= ext
else:
if ext % rem != 0:
return None
stride = int(it.stride)
new_iters[orig_idx] = Iter(ext // rem, stride * rem, it.axis)
new_iters.append(Iter(rem, stride, it.axis))
thread_selected.append(len(new_iters) - 1)
rem = 0
break
if rem != 0:
return None
return new_iters, thread_selected
def _three_segment_perm(iters: list, t_selected: list[int], vec_selected: list[int]) -> list[int]:
"""Reorder ``iters`` into ``[outer, T, vec]`` segments. Within each
segment, stride descending so stride=1 sits at the very end."""
t_set = set(t_selected)
vec_set = set(vec_selected)
outer = sorted(
(i for i in range(len(iters)) if i not in t_set and i not in vec_set),
key=lambda i: -int(iters[i].stride),
)
t_sorted = sorted(t_selected, key=lambda i: -int(iters[i].stride))
vec_sorted = sorted(vec_selected, key=lambda i: -int(iters[i].stride))
return list(outer) + t_sorted + vec_sorted
def _shape_perm_for_isolated(
shape_segments: list[tuple[int, int]], iter_perm: list[int]
) -> list[int]:
"""Given segments (one per shape entry, each = (start, end) in
pre-perm iter positions) and the iter permutation, compute the
corresponding shape permutation."""
seg_of = [0] * sum(end - start for start, end in shape_segments)
for seg_idx, (start, end) in enumerate(shape_segments):
for k in range(start, end):
seg_of[k] = seg_idx
seen: set[int] = set()
perm: list[int] = []
for orig_idx in iter_perm:
seg_idx = seg_of[orig_idx]
if seg_idx not in seen:
seen.add(seg_idx)
perm.append(seg_idx)
return perm
def _verify_s_tail_contig(s_p: TileLayout, vec_len: int) -> bool:
"""Check the last iters of ``s_p`` form a stride=1 contig chain whose
extent product equals ``vec_len``."""
iters = list(s_p.shard)
if not iters:
return vec_len == 1
last = iters[-1]
if int(last.stride) != 1:
return False
acc = int(last.extent)
if acc == vec_len:
return True
for k in range(len(iters) - 2, -1, -1):
it = iters[k]
if int(it.stride) != acc:
break
acc *= int(it.extent)
if acc == vec_len:
return True
if acc > vec_len:
return False
return acc >= vec_len and acc % vec_len == 0
_VEC_BITS_CANDIDATES = (128, 64, 32, 16, 8)
def _vec_len_candidates(elem_bits: int, allowed_bits: tuple | None = None) -> list[int]:
"""Vec-length candidates (in elements) for the given element width.
``allowed_bits`` optionally filters the per-instruction allowed widths
(e.g. cp.async only accepts {128, 64, 32} bits = 16/8/4 bytes).
Defaults to ``_VEC_BITS_CANDIDATES`` if not specified.
"""
bits_tuple = allowed_bits if allowed_bits is not None else _VEC_BITS_CANDIDATES
out: list[int] = []
for vb in bits_tuple:
if vb < elem_bits or vb % elem_bits != 0:
continue
n = vb // elem_bits
if n not in out:
out.append(n)
if 1 not in out and allowed_bits is None:
# Scalar fallback is only added for the unrestricted candidate set;
# an instruction-specific list (cp.async etc.) keeps its strictness.
out.append(1)
return out
def _extract_tile(layout, region):
"""Strip swizzle so we can perm/group as a TileLayout."""
if isinstance(layout, ComposeLayout):
return layout.tile_layout
if isinstance(layout, SwizzleLayout):
extents = [int(end - start) for (start, end) in region]
return TileLayout(S[tuple(extents)])
return layout
def _sort_by_stride_desc(layout: TileLayout) -> TileLayout:
"""Reorder shard so list order = traversal order (outer first, stride=1
last). Required before canonicalize() can fuse non-adjacent-but-contig
iters."""
iters = list(layout.shard)
perm = sorted(range(len(iters)), key=lambda i: -int(iters[i].stride))
if perm == list(range(len(iters))):
return layout
return layout.permute_dims(perm)
def _carve_tail(iters: list, chunk: int):
"""Carve ``chunk`` elements off the tail. Walk back across multiple
iters as needed; at most one iter is split.
Per iter (from last to first), let ``ext`` = iter extent and ``rem``
= remaining chunk to fill:
* ``ext == rem``: eat this iter whole, done.
* ``ext < rem``: must divide ``rem``; eat whole, ``rem //= ext``.
* ``ext > rem``: must divide ``ext``; split into
``(ext/rem, stride*rem) + (rem, stride)``, take the inner. Done.
Returns the new iter list on success, ``None`` on failure.
"""
if not iters or chunk <= 0:
return None
rem = chunk
work = list(iters)
for idx in range(len(work) - 1, -1, -1):
it = work[idx]
ext = int(it.extent)
if ext == rem:
return work
if ext < rem:
if rem % ext != 0:
return None
rem //= ext
continue
if ext % rem != 0:
return None
stride = int(it.stride)
work[idx] = Iter(ext // rem, stride * rem, it.axis)
work.insert(idx + 1, Iter(rem, stride, it.axis))
return work
return None
def align_layouts_gs(
g_layout,
g_shape,
g_region,
s_layout,
s_shape,
s_region,
elem_bits,
thread_cnt: int,
vec_bits_candidates: tuple | None = None,
debug: bool = False,
):
"""Align G and S layouts for a synthesized G↔S copy.
Algorithm:
1. Sort G iters by stride desc + canonicalize (fuses anything physically
contig). Same for S.
2. Group S by G's iter shape (per-iter shape list) + permute_by_groups
with identity so S's groups line up with G's iters.
3. Carve ``vec_len`` off G's tail (a single iter split if needed).
4. Carve ``T = thread_cnt`` off the iter before vec (multi-iter walk,
single split).
5. Re-group S (already permuted) by the new finer per-iter shape. No
further permute needed because steps 3-4 only refine the tail.
6. Verify S's tail (vec segment) is physically contig.
``vec_bits_candidates`` optionally restricts the per-instruction allowed
widths (e.g. cp.async only accepts {128, 64, 32} bits). When ``None``
(default), uses the full {128, 64, 32, 16, 8} set plus a scalar (1)
fallback.
Returns ``(g_p, s_p, vec_len)``. ``g_p.shard`` ends as
``[outer iters..., T iter, vec iter]``; ``s_p.shard`` has the same iter
count and matching iter-by-iter extents.
"""
g = g_layout.slice(list(g_shape), g_region)
s = s_layout.slice(list(s_shape), s_region)
# Detect a SwizzleLayout on the S side BEFORE _extract_tile strips it.
# vec_len must fit inside one swizzle chunk (C = 2^per_element elements);
# otherwise the vec ld/st crosses a swizzle XOR boundary and hits the
# wrong physical bytes mid-vec.
s_swizzle_chunk_elems = None
if isinstance(s_layout, ComposeLayout):
s_swizzle_chunk_elems = 1 << int(s_layout.swizzle.per_element)
elif isinstance(s_layout, SwizzleLayout):
s_swizzle_chunk_elems = 1 << int(s_layout.per_element)
g = _extract_tile(g, g_region)
s = _extract_tile(s, s_region)
# Only G drives the canonical form. S's iter order is derived from G's
# pre-sort iter extents (used as the grouping shape) and then permuted
# by G's stride-desc permutation. Independently sorting/canonicalizing
# S would fuse iters whose strides happen to chain into one contiguous
# range — that loses the layout's logical `(i, j) → addr` mapping for
# layout-permuting copies (e.g. row-major GMEM → K-tiled SMEM, where
# both layouts cover the same byte range but with different coord maps).
g_pre_sort_extents = [int(it.extent) for it in g.shard]
g_perm = sorted(range(len(g.shard)), key=lambda i: -int(g.shard[i].stride))
try:
s_grp1, seps1 = s.group(g_pre_sort_extents)
except Exception as e:
if debug:
print(f" Step-2 S.group({g_pre_sort_extents}) failed: {e}")
return _sort_by_stride_desc(g).canonicalize(), s, 1
# S iters keep their original strides; only the *group order* is
# rearranged to follow G's sorted iter order. Canonicalize after the
# permute is safe — it only fuses iters whose strides genuinely chain
# in the post-permute order, which preserves the logical structure.
s_aligned = s_grp1.permute_by_groups(list(seps1), g_perm).canonicalize()
g = _sort_by_stride_desc(g).canonicalize()
if debug:
print(f" g (sort+canon): shard={[(int(it.extent), int(it.stride)) for it in g.shard]}")
print(
f" s (grouped+permuted by G shape {g_pre_sort_extents}): shard="
f"{[(int(it.extent), int(it.stride)) for it in s_aligned.shard]}"
)
print(f" thread_cnt: {thread_cnt}")
dummy_axis = g.shard[-1].axis if g.shard else None
for vec_len in _vec_len_candidates(elem_bits, vec_bits_candidates):
if vec_len == 1:
g_after_vec = [*list(g.shard), Iter(1, 1, dummy_axis)]
else:
# Swizzle chunk-size cap: vec must fit in one swizzle chunk.
if s_swizzle_chunk_elems is not None and vec_len > s_swizzle_chunk_elems:
if debug:
print(
f" vec_len={vec_len}: exceeds swizzle chunk "
f"({s_swizzle_chunk_elems} elements)"
)
continue
g_after_vec = _carve_tail(list(g.shard), vec_len)
if g_after_vec is None:
if debug:
print(f" vec_len={vec_len}: G vec carve failed")
continue
outer_for_t = g_after_vec[:-1]
outer_after_t = _carve_tail(outer_for_t, thread_cnt) if thread_cnt > 1 else outer_for_t
if outer_after_t is None:
if debug:
print(f" vec_len={vec_len}: T={thread_cnt} carve failed")
continue
g_final_iters = [*outer_after_t, g_after_vec[-1]]
new_shape = [int(it.extent) for it in g_final_iters]
try:
s_final_grp, _ = s_aligned.group(new_shape)
except Exception as e:
if debug:
print(f" vec_len={vec_len}: S.group({new_shape}) failed: {e}")
continue
g_p = TileLayout.from_iters(g_final_iters, list(g.replica), dict(g.offset))
s_p = s_final_grp
ok = _verify_s_tail_contig(s_p, vec_len)
if debug:
print(
f" vec_len={vec_len}: shape={new_shape}, "
f"g_p.shard={[(int(it.extent), int(it.stride)) for it in g_p.shard]}, "
f"s_p.shard={[(int(it.extent), int(it.stride)) for it in s_p.shard]}, "
f"s_tail_contig={ok}"
)
if not ok:
continue
# Alignment: per-thread starting addr = base_ptr + sizeof(elem) *
# (region_base + tid*t_stride + outer_iter_strides). For the
# vec_bits/8 byte vector op to be naturally aligned, every one of
# those element-count terms must be a multiple of vec_len.
align_terms = []
for it in s_p.shard[:-1]:
align_terms.append(int(it.stride))
for it in g_p.shard[:-1]:
align_terms.append(int(it.stride))
align_terms.extend(s_p.offset.values())
align_terms.extend(g_p.offset.values())
if not _alignment_ok(vec_len, align_terms):
if debug:
print(f" vec_len={vec_len}: alignment check failed")
continue
return g_p, s_p, vec_len
return g, s_aligned, 1
def _flat_outer_coords(outer_exts: list[int], flat_idx: int) -> list[int]:
"""Decode a row-major flat index into per-iter coords. ``outer_exts`` is
in stride-desc order (outermost first), so the first coord changes
slowest."""
coords: list[int] = []
rem = flat_idx
for ext in reversed(outer_exts):
coords.append(rem % ext)
rem //= ext
coords.reverse()
return coords
def _outer_offsets(outer_iters_s, outer_iters_g, flat_idx):
"""Returns ``(ds, dg)``: constant offsets on S and G sides for the
given flat outer-loop iteration."""
outer_exts = [int(it.extent) for it in outer_iters_s]
coords = _flat_outer_coords(outer_exts, flat_idx)
ds = sum(c * int(it.stride) for c, it in zip(coords, outer_iters_s))
dg = sum(c * int(it.stride) for c, it in zip(coords, outer_iters_g))
return ds, dg
def copy_ptx_form(num_bytes: int) -> tuple[str, str]:
"""Map copy width (bytes) to PTX ``(vec, ptx_type)`` for ``T.ptx.ld`` / ``T.ptx.st``."""
return {
16: ("v4", "u32"),
8: ("v2", "u32"),
4: ("", "u32"),
2: ("", "u16"),
1: ("", "u8"),
}[num_bytes]
def copy_ptx_ld_return_type(ptx_type: str) -> str:
"""TVM dtype string for ``T.ptx.ld``'s ``return_type`` argument."""
return {"u32": "uint32", "u16": "uint16", "u8": "uint32"}[ptx_type]
@@ -0,0 +1,406 @@
# 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.
"""Generic swizzle-aware iter pattern for CUDA copy dispatches.
When the per-thread outer-iter loop satisfies (C1)+(C2) below for a
``SwizzleLayout(per_element=p, swizzle_len=sw, atom_len=at,
swizzle_inner=True)`` on the SMEM side, the swizzled physical address
at unrolled iter ``k`` reduces to
addr(k) = base_off + sum_{j : bit_j(k)=1} signed_strides[j]
where ``base_off`` and the ``signed_strides[j]`` are per-thread runtime
constants set once at thread setup. Per-iter cost is then ``popcount(k)``
register adds instead of a full ``swizzle.apply(...)`` per iter.
Notation. Each binary outer iter has element-stride ``2^(bj + p)`` for
some chunk bit position ``bj >= 0`` (so ``stride / C = 2^bj`` where
``C = 1 << p``). The chunk index ``q(M0) = M0 // C`` partitions into
four bit ranges by where ``bj`` lands:
* ``[0, sw)`` — "inner" (Case 1.A in the proof)
* ``[sw, at)`` — "mid" (Case 1.B)
* ``[at, at + sw)`` — "outer" (Case 1.C; the bit overlaps the swizzle
outer mask, so its addition produces a
secondary contribution at ``bj - at``)
* ``[at + sw, ∞)`` — "above" (Case 1.D)
Conditions for the linear-combination fast path:
(C1) bit-clear no-carry: ``bit_bj(q(M0)) = 0`` for every binary iter.
(C2) support disjointness: no inner-outer pair ``(bj_A, bj_C)`` with
``bj_C in [at, at+sw)`` and ``bj_A = bj_C - at`` both present.
(distinctness) The ``bj`` values across all binary iters must be
distinct — two iters at the same ``bj`` collapse into bit
``bj + 1`` whose case behavior may differ.
Under (C1)+(C2)+(distinctness), for each binary iter at position ``bj``:
T(bj) = 2^(bj + p) # element stride
sigma_b(M0) = 1 - 2 * bit_b(q(M0)) # ∈ {+1, -1}
signed_strides[j] = sigma_(at + bj)(M0) * T(bj) bj in [0, sw)
= T(bj) bj in [sw, at)
= T(bj) + sigma_(bj - at)(M0) * T(bj - at)
bj in [at, at + sw)
= T(bj) bj >= at + sw
The ``swizzle_inner=False`` mode swaps the inner/outer roles and is not
yet covered; ``try_recognize`` gates on this.
"""
from dataclasses import dataclass
import tvm
from tvm import arith
from tvm.script import tirx as T
from tvm.tirx.expr import IntImm as _IntImm
from tvm.tirx.layout import ComposeLayout, SwizzleLayout
@dataclass
class _BitIter:
"""Pow2-extent outer iter, binary-split into ``n_bits`` chunk-bit flips.
``slot_start..slot_start + n_bits`` is this iter's range in the global
``bit_positions`` / ``iter_strides_elems`` / ``signed_strides`` arrays.
Slot ``slot_start + b`` corresponds to bit position ``n_bits - 1 - b``
of this iter's per-iter coord (outermost binary bit first).
"""
ext: int
n_bits: int
slot_start: int
@dataclass
class _LinearIter:
"""Outer iter contributing ``c * stride`` to the offset (no bit decomp).
Used when ``stride`` is a multiple of ``2^(p + at + sw)`` (pure Case 1.D
regime: swizzle XOR has no effect on bits the iter flips). ``ext`` does
not need to be a power of two.
"""
ext: int
stride: int
@dataclass
class SwizzlePattern:
"""A recognized swizzle iter pattern.
``bit_positions[j]`` and ``iter_strides_elems[j]`` collect the binary
sub-iters from every BitIter in outer-iter order (outermost first).
``outer_iters`` lists every outer iter (BitIter or LinearIter) in
outermost-first order; ``emit_iter_offset`` walks this list to
decompose ``mm`` per-iter. Empty lists = trivially recognized
degenerate case (no outer iter, just base_off).
"""
swizzle: SwizzleLayout
bit_positions: list[int]
iter_strides_elems: list[int]
outer_iters: "list[_BitIter | _LinearIter]"
@property
def n_binary_iters(self) -> int:
return len(self.bit_positions)
def get_swizzle(layout) -> SwizzleLayout | None:
"""Return the SwizzleLayout from ``layout`` if present, else ``None``.
Accepts ``ComposeLayout(SwizzleLayout, TileLayout)`` (the common case
when a TileLayout is wrapped by a swizzle), or a bare ``SwizzleLayout``.
"""
if isinstance(layout, ComposeLayout):
return layout.swizzle
if isinstance(layout, SwizzleLayout):
return layout
return None
def _is_pow2(n: int) -> bool:
return n > 0 and (n & (n - 1)) == 0
def try_recognize(
swizzle: SwizzleLayout,
iter_extents: list[int],
iter_strides: list[int],
s_off_template,
var_bounds: dict | None = None,
) -> SwizzlePattern | None:
"""Return a ``SwizzlePattern`` if (C1)+(C2)+(distinctness) hold, else ``None``.
``iter_extents`` / ``iter_strides``: the outer-iter list on the S side
(excluding T iter and vec iter), in outermost-first order matching
``s_p.shard[:-2]`` (or the atom-derived analog in ``reg.py``).
Strides are in element units.
Each outer iter with ``extent=2^k`` and ``stride=s`` is conceptually
split into ``k`` binary iters of strides ``2^(k-1)*s, ..., 2*s, s``
(outermost first within the split — this matches ``_flat_outer_coords``
semantics, since the highest-stride iter must change slowest in the
flat-index decomposition).
``s_off_template`` is the per-thread linear base offset expression
(with a placeholder var for the thread-id contribution). It is used
only to check condition (C1) symbolically via ``arith.Analyzer``;
``emit_init`` takes the resolved form separately.
``var_bounds`` is an optional ``{Var: tvm.ir.Range}`` map of placeholder
bounds to ``analyzer.bind`` before the (C1) check. Without bounds,
structurally-OK forms like ``(lane // 8) * 8 + (lane % 8) * Q`` where
``lane < 32`` make ``(... // (C·2^bj)) % 2 == 0`` unprovable — the
bit is in fact always 0 but the analyzer can't conclude it universally.
Pass ``{lane_ph: Range(0, 32), warp_ph: Range(0, n_warps)}`` (or the
scope's equivalents) to let the (C1) check fire on these templates.
"""
# swizzle_inner=False swaps the inner/outer xor direction — Cases 1.A
# and 1.C roles flip. Not derived/tested yet; reject for safety.
if not swizzle.swizzle_inner:
return None
p = swizzle.per_element
sw = swizzle.swizzle_len
at = swizzle.atom_len
C = 1 << p
# Pure Case 1.D threshold: stride a multiple of this means every chunk-bit
# the iter flips is at position >= at + sw (above the swizzle XOR region),
# so swizzle has no effect and the contribution is purely linear in the
# iter coord — no power-of-2 ext requirement.
pure_1d = 1 << (p + at + sw)
bit_positions: list[int] = []
iter_strides_elems: list[int] = []
outer_iters: list = []
for ext, stride in zip(iter_extents, iter_strides):
# Zero-stride iters degrade dq=0 → log2 undefined. Explicit guard.
if stride == 0 or stride % C != 0:
return None
if ext <= 0:
return None
if ext == 1:
# Trivial iter contributes nothing; skip without forcing pow2.
continue
if not _is_pow2(ext):
# Non-pow2 ext can only be handled by the linear path. That in turn
# requires the iter to be in pure Case 1.D (stride a multiple of
# the swizzle period) so the swizzle does not interact with the
# per-coord contribution.
if stride % pure_1d != 0:
return None
outer_iters.append(_LinearIter(ext=ext, stride=stride))
continue
# pow2 ext: binary split (existing path).
k = ext.bit_length() - 1 # log2(ext)
slot_start = len(bit_positions)
# Split into k binary iters; the outermost (within this split) carries
# the largest stride so that flat-index bit decomp matches our
# outer-iter list ordering.
for j in range(k - 1, -1, -1):
substride = stride * (1 << j)
dq = substride // C
# dq must be a single bit set (so this binary iter flips exactly
# one bit of the chunk index). _is_pow2 also rejects dq=0.
if not _is_pow2(dq):
return None
bj = dq.bit_length() - 1
# All bj >= 0 accepted; case branching happens in emit_init.
bit_positions.append(bj)
iter_strides_elems.append(substride)
outer_iters.append(_BitIter(ext=ext, n_bits=k, slot_start=slot_start))
# Distinctness: two binary iters at the same bj collapse to bj+1, whose
# case behavior may differ from bj. See module docstring NB.
if len(set(bit_positions)) != len(bit_positions):
return None
bj_set = set(bit_positions)
# (C2) support disjointness: the only possible collision is between a
# Case-1.A iter at bj_A and a Case-1.C iter at bj_A + at. Checking the
# 1.C direction alone is symmetric and complete.
for bj in bj_set:
if at <= bj < at + sw and (bj - at) in bj_set:
return None # inner-outer pair collision
# (C1) per-iter no-carry on q(M0). Must hold *symbolically over all*
# free lane / warp placeholders in s_off_template — ``can_prove_equal``
# returns False if the analyzer can't discharge the equality
# universally, conservatively forcing a fallback.
analyzer = arith.Analyzer()
if var_bounds:
for var, rng in var_bounds.items():
analyzer.bind(var, rng)
for bj in bj_set:
divisor = C * (1 << bj)
check = tvm.tirx.floormod(
tvm.tirx.floordiv(s_off_template, _IntImm("int32", divisor)),
_IntImm("int32", 2),
)
if not analyzer.can_prove_equal(check, _IntImm("int32", 0)):
return None
return SwizzlePattern(
swizzle=swizzle,
bit_positions=bit_positions,
iter_strides_elems=iter_strides_elems,
outer_iters=outer_iters,
)
def emit_init(pattern: SwizzlePattern, s_off_resolved):
"""Emit at thread setup (call from inside the @T.prim_func body):
1. ``base_off = swizzle.apply(s_off_resolved)`` — runtime, per-thread,
computed once.
2. ``signed_strides[j]`` for each binary iter j, written into a local
buffer using the sigma formula above.
Returns ``(signed_strides_buffer_or_None, base_off_primexpr)``. The
buffer is ``None`` when ``pattern.n_binary_iters == 0`` (no outer
iter, no signed_strides needed).
``s_off_resolved`` is the per-thread offset with the real tid Var
substituted in (not the placeholder).
"""
swizzle = pattern.swizzle
p = swizzle.per_element
sw = swizzle.swizzle_len
at = swizzle.atom_len
C = 1 << p
base_off = swizzle.apply(s_off_resolved)["m"]
n = pattern.n_binary_iters
if n == 0:
return None, base_off
signed_strides = T.alloc_buffer([n], "int32", scope="local")
q = tvm.tirx.floordiv(s_off_resolved, C)
def _sigma_bit(bit_pos: int):
# 1 - 2 * bit_(bit_pos)(q); ∈ {+1, -1}.
row_bit = tvm.tirx.bitwise_and(
tvm.tirx.shift_right(q, _IntImm("int32", bit_pos)),
_IntImm("int32", 1),
)
return _IntImm("int32", 1) - row_bit * _IntImm("int32", 2)
for j, (bj, stride) in enumerate(zip(pattern.bit_positions, pattern.iter_strides_elems)):
stride_pow = stride # = 2^(bj + p) elements
if 0 <= bj < sw:
# Case 1.A (inner): signed_stride = sigma_(at + bj) · T.
value = _sigma_bit(at + bj) * _IntImm("int32", stride_pow)
elif sw <= bj < at:
# Case 1.B (mid): signed_stride = +T.
value = _IntImm("int32", stride_pow)
elif at <= bj < at + sw:
# Case 1.C (outer): signed_stride = T + sigma_(bj - at) · T_sec.
# Invariant: bj >= at, so T_sec = T >> at = 2^(bj - at + p)
# = T(bj - at) is well-defined (no underflow).
stride_sec = stride_pow >> at
value = _IntImm("int32", stride_pow) + _sigma_bit(bj - at) * _IntImm(
"int32", stride_sec
)
else: # bj >= at + sw, Case 1.D (above)
# No swizzle effect at this bit; signed_stride = +T.
value = _IntImm("int32", stride_pow)
# NB: Buffer.__setitem__ syntax (``signed_strides[j] = value``) is
# intercepted by the TIRx script parser but not by raw Python when
# this function is called from outside an @T.inline body. Use the
# low-level buffer_store builder instead.
T.buffer_store(signed_strides, value, [_IntImm("int32", j)])
return signed_strides, base_off
def emit_iter_offset(pattern: SwizzlePattern, signed_strides, base_off, k):
"""Compute the per-mm physical S offset = ``base_off`` + sum of per-iter
contributions.
``k`` is the flat outer iter index ∈ ``[0, prod(it.ext for it in outer_iters))``.
Decomposed innermost-first across ``pattern.outer_iters`` into per-iter
coords ``c_i``. Each iter contributes:
* ``_BitIter``: ``sum_b bit_(n_bits-1-b)(c_i) * signed_strides[slot_start + b]``,
i.e. each binary bit of ``c_i`` selects its precomputed sigma-stride.
The slot order (outermost-first within the iter) means the highest
bit of ``c_i`` indexes the slot at ``slot_start``.
* ``_LinearIter``: ``c_i * stride`` (no bit decomposition; used when
``stride`` is a multiple of ``2^(p + at + sw)`` so swizzle has no
XOR effect and ``ext`` need not be pow2).
Two paths per iter:
* Python int ``k`` — coords and bits known at parse time; emits only
the necessary adds, no runtime shift/mask.
* TIRx Var ``k`` — emits floormod/floordiv + bit-and/shift; relies on
downstream unroll + constant-fold.
"""
if not pattern.outer_iters:
return base_off
off = base_off
remaining = k
is_const = isinstance(k, int)
for it in reversed(pattern.outer_iters): # innermost first
ext = it.ext
if is_const:
c = remaining % ext
remaining = remaining // ext
else:
c = tvm.tirx.floormod(remaining, _IntImm("int32", ext))
remaining = tvm.tirx.floordiv(remaining, _IntImm("int32", ext))
if isinstance(it, _LinearIter):
if is_const:
if c != 0:
off = off + c * it.stride
else:
off = off + c * _IntImm("int32", it.stride)
continue
# _BitIter
for b in range(it.n_bits):
bit_pos = it.n_bits - 1 - b
slot = it.slot_start + b
if is_const:
if (c >> bit_pos) & 1:
off = off + signed_strides[slot]
else:
bit = tvm.tirx.bitwise_and(
tvm.tirx.shift_right(c, _IntImm("int32", bit_pos)),
_IntImm("int32", 1),
)
off = off + bit * signed_strides[slot]
return off
def emit_fallback_offset(swizzle: SwizzleLayout, s_off_resolved, ds_k):
"""Slow but always-correct path: full ``swizzle.apply(s_off + ds_k)``
per iter. Use when ``try_recognize`` returns ``None``.
``ds_k`` is the outer-iter delta for unrolled iter k — typically a
Expr (a function of the unroll var that simplifies to a constant
after unrolling) or a Python int. ``s_off_resolved`` is the per-thread
base linear offset with the real tid Var substituted.
"""
return swizzle.apply(s_off_resolved + ds_k)["m"]
@@ -0,0 +1,116 @@
# 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.
"""Scalar single-thread copy fallback (priority=0)."""
import warnings
import tvm
from tvm.script import tirx as T
from tvm.tirx import Buffer, PrimFunc
from tvm.tirx.operator.tile_primitive.dispatcher import (
predicate,
register_dispatch,
)
from tvm.tirx.operator.tile_primitive.registry import DispatchContext
from tvm.tirx.stmt import TilePrimitiveCall
from ._common import _TID_AXIS_FOR_SCOPE
from .reg import _axis_decl
from .utils import _is_valid_copy
def _region_st_extent(buffer_region):
region = buffer_region.region
return [r.min for r in region], [r.extent for r in region]
def _emit_fallback(op_call: TilePrimitiveCall, sctx: DispatchContext) -> PrimFunc:
op_call = TilePrimitiveCall.downcast(op_call)
src: Buffer = op_call.src.buffer
dst: Buffer = op_call.dst.buffer
src_st, src_extent = _region_st_extent(op_call.src)
dst_st, dst_extent = _region_st_extent(op_call.dst)
warnings.warn(
f"copy/fallback (scalar single-thread) picked for {src.scope()} -> "
f"{dst.scope()} at scope_kind={sctx.scope_kind}; all faster variants "
f"rejected.",
stacklevel=2,
)
def _copy_body(dst_buf, src_buf):
dst_indices = [i for i in range(len(dst_buf.shape)) if dst_extent[i] != 1]
src_indices = [i for i in range(len(src_buf.shape)) if src_extent[i] != 1]
assert len(dst_indices) == len(src_indices)
copy_extents = [dst_extent[i] for i in dst_indices]
def _dst_coord(lvs):
if isinstance(lvs, tvm.tirx.Var):
lvs = [lvs]
coord = list(dst_st)
for k, lv in enumerate(lvs):
coord[dst_indices[k]] += lv
return coord
def _src_coord(lvs):
if isinstance(lvs, tvm.tirx.Var):
lvs = [lvs]
coord = list(src_st)
for k, lv in enumerate(lvs):
coord[src_indices[k]] += lv
return coord
with T.grid(*copy_extents) as lvs:
T.buffer_store(dst_buf, src_buf[tuple(_src_coord(lvs))], _dst_coord(lvs))
scope_kind = sctx.scope_kind
if scope_kind == "thread":
@T.prim_func(check_well_formed=False)
def impl():
_copy_body(dst, src)
return impl
tid_axis_name = _TID_AXIS_FOR_SCOPE[scope_kind]
# first-active tid = composition of per-axis offsets (radix-32, since a warp is 32 lanes)
first_tid = int(sctx.intra["laneid"][1])
if scope_kind == "warpgroup":
first_tid += 32 * int(sctx.intra["wid_in_wg"][1])
elif scope_kind == "cta":
first_tid += 32 * int(sctx.intra["warpid"][1])
@T.prim_func(check_well_formed=False)
def impl():
tid = _axis_decl(tid_axis_name, sctx)
if tid == first_tid:
_copy_body(dst, src)
return impl
@register_dispatch(
"copy",
"cuda",
variant="fallback",
priority=0,
when=[predicate("validate_copy_op", _is_valid_copy)],
)
def copy_schedule_fallback(op_call: TilePrimitiveCall, sctx: DispatchContext) -> PrimFunc:
return _emit_fallback(op_call, sctx)
@@ -0,0 +1,328 @@
# 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.
"""Copy dispatch for ``global ↔ shared`` (no register side).
There's no per-thread register side to inherit a partition from — both sides
are cross-thread storage. The partition is synthesized from the surrounding
scope context (warp / warpgroup / cta / thread): ``thread_cnt`` is derived
from ``sctx.intra`` and each thread takes ``n_elements / thread_cnt``
consecutive fused-index slots. Layout / partition algorithm lives in
``_common.py`` and is shared with ``ldgsts.py``.
"""
import tvm
from tvm.runtime import DataType
from tvm.script import tirx as T
from tvm.tirx import Buffer, PrimFunc
from tvm.tirx import Var as _TirVar
from tvm.tirx.expr import IntImm as _IntImm
from tvm.tirx.operator.tile_primitive.dispatcher import (
predicate,
register_dispatch,
)
from tvm.tirx.operator.tile_primitive.registry import DispatchContext
from tvm.tirx.stmt import TilePrimitiveCall
from ._common import (
_TID_AXIS_FOR_SCOPE,
_thread_cnt,
align_layouts_gs,
copy_ptx_form,
copy_ptx_ld_return_type,
)
from ._swizzle_iter import (
emit_init,
emit_iter_offset,
get_swizzle,
try_recognize,
)
from .reg import _all_threads_active, _axis_decl, _ptr_off
from .utils import _is_valid_copy, _scope_allowed
_GMEM_SMEM_PAIRS = [
("global", "shared*"),
("shared*", "global"),
]
def _divides_thread_cnt(
op_call: TilePrimitiveCall, sctx: DispatchContext
) -> tuple[bool, str | None]:
"""Reject copies whose region element count does not divide ``thread_cnt``.
Without this guard the emit's ``[outer, T, vec]`` partition has no
integer solution: either every thread gets fractional work, or
``thread_cnt=0`` (degenerate scope) hits a modulo-by-zero. Both cases
indicate a poorly-shaped copy (e.g. 1024-thread CTA writing a 64-elem
tail) that this dispatch refuses to paper over with a slow scalar emit.
"""
op_call = TilePrimitiveCall.downcast(op_call)
thread_cnt = _thread_cnt(sctx)
if thread_cnt <= 0:
return False, f"degenerate thread_cnt={thread_cnt} (scope has empty intra)"
g_br = op_call.src if op_call.src.buffer.scope() == "global" else op_call.dst
n_elements = 1
for r in g_br.region:
ext = r.extent
try:
n_elements *= int(ext)
except (TypeError, ValueError):
return False, f"non-constant region extent {ext}"
if n_elements % thread_cnt != 0:
return False, (f"region size {n_elements} not divisible by thread_cnt={thread_cnt}")
return True, None
def _is_gmem_smem(op_call: TilePrimitiveCall, sctx: DispatchContext) -> tuple[bool, str | None]:
if not sctx.is_target("cuda"):
return False, "non-cuda target"
if sctx.scope_kind not in ("thread", "warp", "warpgroup", "cta"):
return False, f"unsupported exec_scope {sctx.scope_kind}"
for check in (
lambda: _all_threads_active(sctx),
lambda: _is_valid_copy(op_call, sctx),
lambda: _scope_allowed(op_call, sctx, allowed_pairs=_GMEM_SMEM_PAIRS),
lambda: _divides_thread_cnt(op_call, sctx),
):
ok, msg = check()
if not ok:
return False, msg
return True, None
def _emit_gmem_smem(op_call: TilePrimitiveCall, sctx: DispatchContext) -> PrimFunc:
op_call = TilePrimitiveCall.downcast(op_call)
src: Buffer = op_call.src.buffer
dst: Buffer = op_call.dst.buffer
if src.scope() == "global":
g_buf, g_br, s_buf, s_br = src, op_call.src, dst, op_call.dst
g_is_src = True
else:
g_buf, g_br, s_buf, s_br = dst, op_call.dst, src, op_call.src
g_is_src = False
g_region = [(r.min, r.min + r.extent) for r in g_br.region]
s_region = [(r.min, r.min + r.extent) for r in s_br.region]
elem_bits = DataType(src.dtype).bits
thread_cnt = _thread_cnt(sctx)
with sctx.target:
g_p, s_p, vec_len = align_layouts_gs(
g_buf.layout,
g_buf.shape,
g_region,
s_buf.layout,
s_buf.shape,
s_region,
elem_bits,
thread_cnt,
)
# vec_len=1 is the scalar fallback — uses the same unified
# [outer x thread x vec] coord scheme below.
vec_bits = vec_len * elem_bits
num_bytes = vec_bits // 8
vec, ptx_type = copy_ptx_form(num_bytes)
# Partition guarantees ``prod(s_p.shard.extents) == prod(g_p.shard.extents)
# == n_elements`` (the total transfer count). Express the per-thread
# per-round address as a 3D coord ``(f, tid, 0)`` against shape
# ``[total_outer, thread_cnt, vec_len]``, and let ``layout.apply`` flatten
# it through whatever multi-iter T / outer-iter structure ``align_layouts_gs``
# picked. This makes the emit oblivious to how many iters the partition
# split T or outer across.
n_elements = 1
for it in s_p.shard:
n_elements *= int(it.extent)
assert n_elements % (thread_cnt * vec_len) == 0, (
f"partition produced {n_elements} elements but thread_cnt({thread_cnt}) * "
f"vec_len({vec_len}) = {thread_cnt * vec_len} doesn't divide it"
)
total_outer = n_elements // (thread_cnt * vec_len)
apply_shape = [
_IntImm("int32", total_outer),
_IntImm("int32", thread_cnt),
_IntImm("int32", vec_len),
]
s_zero = [0] * len(s_buf.shape)
g_zero = [0] * len(g_buf.shape)
tid_axis_name = _TID_AXIS_FOR_SCOPE[sctx.scope_kind] if thread_cnt > 1 else None
# Walk shard from the vec iter backward to find the prefix that covers
# the T region exactly (∏ext == thread_cnt). The iters consumed are T
# iters; the leading prefix is the outer iter list — handed to
# ``try_recognize`` so the swizzle fast path can decide whether the
# outer iter strides match a pattern it can lower to signed_strides.
if thread_cnt > 1:
acc, _i = 1, len(s_p.shard) - 2
while _i >= 0 and acc < thread_cnt:
_ext = int(s_p.shard[_i].extent)
if acc * _ext > thread_cnt:
break
acc *= _ext
_i -= 1
outer_iters_s = list(s_p.shard[: _i + 1]) if acc == thread_cnt else []
else:
outer_iters_s = list(s_p.shard[:-1])
# SwizzleLayout on s_buf: try the closed-form signed-strides pattern
# (precomputed once per thread, then per-iter is a sum of register
# adds); fall back to per-iter ``swizzle.apply`` (one full XOR +
# decompose per iter). Closure picked at parse time so the TIRx parser
# doesn't AST-evaluate a "dead" ternary branch.
swizzle = get_swizzle(s_buf.layout)
swizzle_pattern = None
if swizzle is not None and outer_iters_s:
if tid_axis_name is not None:
_tid_placeholder = _TirVar(tid_axis_name, "int32")
else:
_tid_placeholder = _IntImm("int32", 0)
s_off_template = s_p.apply(
_IntImm("int32", 0),
_tid_placeholder,
_IntImm("int32", 0),
shape=apply_shape,
)["m"]
# Bind the tid placeholder's range so the (C1) analyzer check can
# discharge ``bit_bj(s_off // C) == 0`` for high bj's. Outer iter
# stride here is ``thread_cnt * vec_len`` ⇒ bj ∈ [log2(thread_cnt),
# ...]; without bounds the analyzer can't prove the lane's high bits
# are 0 and rejects.
var_bounds = {}
if tid_axis_name is not None:
var_bounds[_tid_placeholder] = tvm.ir.Range.from_min_extent(0, thread_cnt)
swizzle_pattern = try_recognize(
swizzle,
[int(it.extent) for it in outer_iters_s],
[int(it.stride) for it in outer_iters_s],
s_off_template,
var_bounds=var_bounds or None,
)
class _SwizzleState:
def __init__(self):
self.signed_strides = None
self.base_off = None
state = _SwizzleState()
def _decl_tid():
if tid_axis_name is not None:
return _axis_decl(tid_axis_name, sctx)
return _IntImm("int32", 0)
def _setup_swizzle(tid):
if swizzle_pattern is None:
return
s_off_resolved = s_p.apply(
_IntImm("int32", 0),
tid,
_IntImm("int32", 0),
shape=apply_shape,
)["m"]
state.signed_strides, state.base_off = emit_init(
swizzle_pattern,
s_off_resolved,
)
if swizzle_pattern is not None:
def _s_off(f, s_lin):
return emit_iter_offset(
swizzle_pattern,
state.signed_strides,
state.base_off,
f,
)
elif swizzle is not None:
_sw = swizzle
def _s_off(f, s_lin):
return _sw.apply(s_lin)["m"]
else:
def _s_off(f, s_lin):
return s_lin
v0 = _IntImm("int32", 0)
# fmt: off
@T.prim_func(check_well_formed=False)
def impl():
tid = _decl_tid()
_setup_swizzle(tid)
tmp = T.alloc_local((vec_len,), src.dtype)
tmp_ptr = tmp.ptr_to([0])
# NB: pass typed ptr_to(...) directly to _ptr_off; caching in a
# local var turns it into void* + offset = byte arithmetic →
# misaligned vector ops.
#
# Use a serial TIR loop and let ptxas unroll downstream. Mirrors
# the reg.py rationale in commit ac7ecf70f0: explicit ``T.unroll``
# materializes the per-iter scratch (s_lin/g_lin/s_off/s_ptr/g_ptr)
# as N copies of each ``alignas(64)`` declaration. For large
# ``total_outer`` (e.g. thread-scope fp32 swizzled copies of 32x256
# at vec=4 ⇒ 2048 iters; ldgsts test4 ⇒ ~4k iters once both
# g2s/s2g sites add up) this floods the kernel and nvcc times out.
for f in range(total_outer):
s_lin = s_p.apply(f, tid, v0, shape=apply_shape)["m"]
g_lin = g_p.apply(f, tid, v0, shape=apply_shape)["m"]
s_off = _s_off(f, s_lin)
s_ptr = _ptr_off(s_buf.ptr_to(s_zero), s_off)
g_ptr = _ptr_off(g_buf.ptr_to(g_zero), g_lin)
if g_is_src:
T.ptx.ld(
g_ptr,
copy_ptx_ld_return_type(ptx_type),
ptx_type,
dst=tmp_ptr,
space="global",
vec=vec,
)
T.ptx.st(
s_ptr, src=tmp_ptr, space="shared", vec=vec, ptx_type=ptx_type
)
else:
T.ptx.ld(
s_ptr,
copy_ptx_ld_return_type(ptx_type),
ptx_type,
dst=tmp_ptr,
space="shared",
vec=vec,
)
T.ptx.st(
g_ptr, src=tmp_ptr, space="global", vec=vec, ptx_type=ptx_type
)
# fmt: on
return impl
@register_dispatch(
"copy",
"cuda",
variant="gmem_smem",
priority=10,
when=[predicate("gmem_smem_applicable", _is_gmem_smem)],
)
def copy_schedule_gmem_smem(op_call: TilePrimitiveCall, sctx: DispatchContext) -> PrimFunc:
return _emit_gmem_smem(op_call, sctx)
@@ -0,0 +1,450 @@
# 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.
"""copy dispatch variant: ldmatrix / stmatrix (TBD algorithm).
Handles register ↔ shared copies on CUDA via PTX ``ldmatrix`` / ``stmatrix``.
Direction (ld vs st) and exec scope (warp / warpgroup) are decided inside
``_emit`` from the src/dst scopes and ``sctx.scope_kind``.
"""
from math import prod
import tvm
from tvm.script import tirx as T
from tvm.tirx import PrimFunc
from tvm.tirx import Var as _TirVar
from tvm.tirx.expr import IntImm as _IntImm
from tvm.tirx.layout import S, TileLayout
from tvm.tirx.operator.tile_primitive.dispatcher import fail, predicate, register_dispatch
from tvm.tirx.operator.tile_primitive.registry import DispatchContext
from tvm.tirx.stmt import TilePrimitiveCall
from ._common import ( # noqa: F401 (_carve_tail reserved for future variants)
_carve_tail,
_extract_tile,
)
from ._swizzle_iter import emit_init, emit_iter_offset, get_swizzle, try_recognize
from .reg import _all_threads_active, _ptr_off
from .utils import _is_valid_copy, _scope_allowed
_REG_SMEM_PAIRS = [
("local", "shared*"),
("shared*", "local"),
]
_VALID_R_LANE_AXES = {"laneid", "tid_in_wg", "tx"}
def _compute_r_perm(r):
"""Permutation: thread iters first (stride-desc), then memory iters (stride-desc)."""
def key(p):
it = p[1]
return (0 if it.axis.is_thread() else 1, -int(it.stride))
return [i for i, _ in sorted(enumerate(r.shard), key=key)]
def _is_ldstmatrix(op_call: TilePrimitiveCall, sctx: DispatchContext) -> tuple[bool, str | None]:
if not sctx.is_target("cuda"):
return False, "non-cuda target"
if sctx.scope_kind not in ("warp", "warpgroup", "cta"):
return False, f"unsupported exec_scope {sctx.scope_kind} (need warp, warpgroup, or cta)"
for check in (
lambda: _all_threads_active(sctx),
lambda: _is_valid_copy(op_call, sctx),
lambda: _scope_allowed(op_call, sctx, allowed_pairs=_REG_SMEM_PAIRS),
):
ok, msg = check()
if not ok:
return False, msg
return True, None
def _emit(op_call: TilePrimitiveCall, sctx: DispatchContext) -> PrimFunc:
op_call = TilePrimitiveCall.downcast(op_call)
# Step 1: identify reg / smem sides and pull their tensor shape + layout.
src_br = op_call.src
dst_br = op_call.dst
if src_br.buffer.scope() == "local":
r_br, s_br = src_br, dst_br
direction = "st" # reg -> smem (stmatrix)
else:
r_br, s_br = dst_br, src_br
direction = "ld" # smem -> reg (ldmatrix)
r_buf = r_br.buffer
s_buf = s_br.buffer
r_shape = list(r_buf.shape)
r_layout = r_buf.layout
s_shape = list(s_buf.shape)
s_layout = s_buf.layout
r_region = [(r.min, r.min + r.extent) for r in r_br.region]
s_region = [(r.min, r.min + r.extent) for r in s_br.region]
with sctx.target:
r_sliced = r_layout.slice(r_shape, r_region)
s_sliced = s_layout.slice(s_shape, s_region)
r = r_sliced.canonicalize()
s = s_sliced.canonicalize()
# Step 2.5: peel any S-side swizzle wrapper to expose the underlying
# TileLayout. The ComposeLayout doesn't have ``.replica`` / ``.shard``,
# so we must peel *before* the structural checks below. Capture the
# swizzle separately for use at emit time.
# NB: read swizzle from the *buffer* layout, not the post-canon ``s``.
# When the underlying tile is trivial, ``ComposeLayoutNode::Canonicalize``
# returns a bare ``SwizzleLayout``; isinstance(s, ComposeLayout) is then
# False and we'd miss the swizzle here.
s_swizzle = get_swizzle(s_buf.layout)
if s_swizzle is not None and s_swizzle.per_element < 3:
# ldmatrix/stmatrix .b16 reads/writes 8 fp16 = 128b per lane in one
# contiguous chunk. The swizzle preserves the lowest ``per_element``
# bits of the address (in-chunk offset). For the per-lane 128b unit
# to stay contiguous post-swizzle, ``2^per_element >= 8`` ⇒ p >= 3.
fail(
f"swizzle per_element={s_swizzle.per_element} < 3 incompatible "
f"with .b16 ldmatrix/stmatrix (need 8-fp16 chunk integrity)"
)
s = _extract_tile(s, s_region)
# Step 3: ldstmatrix doesn't broadcast — require zero replica on both sides.
if len(r.replica) != 0:
fail(f"R layout has replica {list(r.replica)}; ldstmatrix requires no replica")
if len(s.replica) != 0:
fail(f"S layout has replica {list(s.replica)}; ldstmatrix requires no replica")
# Step 4: R must have exactly one kind of lane axis from the valid set.
r_thread_axes = {it.axis.name for it in r.shard if it.axis.is_thread()}
if len(r_thread_axes) != 1:
fail(f"R must have exactly one thread axis name; got {sorted(r_thread_axes)}")
r_lane_axis = next(iter(r_thread_axes))
if r_lane_axis not in _VALID_R_LANE_AXES:
fail(f"R thread axis {r_lane_axis!r} not in {sorted(_VALID_R_LANE_AXES)}")
# Step 5: group S by R's iter extents (one S group per R iter, outer→inner).
r_group_shape = [int(it.extent) for it in r.shard]
s_grp, s_seps = s.group(r_group_shape)
# Step 6: permute R so thread iters come first (stride-desc), then memory
# iters (stride-desc).
r_perm = _compute_r_perm(r)
r = r.permute_dims(r_perm)
# Step 7: apply R's perm to S in group units (1-to-1 with R's iters), and
# rebuild s_seps to track group boundaries in the new order.
s = s_grp.permute_by_groups(list(s_seps), r_perm)
old_sizes = [s_seps[i + 1] - s_seps[i] for i in range(len(s_seps) - 1)]
s_seps = [0]
for pi in r_perm:
s_seps.append(s_seps[-1] + old_sizes[pi])
# Step 7.5: canonicalize both R and S after permute. Fuses adjacent
# contig iters — keeps step 8's group input clean. Push target so
# scope-aware fusers run (laneid+wid_in_wg → tid_in_wg, etc.).
with sctx.target:
r = r.canonicalize()
s = s.canonicalize()
t_total = prod(int(it.extent) for it in r.shard if it.axis.is_thread())
m_total = prod(int(it.extent) for it in r.shard if not it.axis.is_thread())
if t_total % 32 != 0:
fail(f"R thread section total {t_total} not divisible by 32")
def _strs(lay, seps):
# Atoms 8 / 4 / 2 (segs 1, 2, 5) must be single iters — their strides
# feed downstream stride checks (lane partition + fragment 2-fp16
# contig). The num atom (seg 4) may be MULTI-ITER: we return its iter
# list and let layout.apply handle the decomposition at emit time.
fixed_segs = [list(lay.shard[seps[i] : seps[i + 1]]) for i in (1, 2, 5)]
if not all(len(g) == 1 for g in fixed_segs):
return None
num_iters = list(lay.shard[seps[4] : seps[5]])
return (
int(fixed_segs[0][0].stride), # 8 atom stride
int(fixed_segs[1][0].stride), # 4 atom stride
num_iters, # num atom iter list (multi-iter OK)
int(fixed_segs[2][0].stride), # 2 atom stride
)
def _try_num(r_in, s_in, num):
"""Try grouping (r_in, s_in) with [T/32, 8, 4, M/(2num), num, 2].
Returns (rg, rsep, sg, ssep, trans, p, num) if structural checks pass,
else None. ``trans`` is the ldmatrix .trans flag; ``p`` is the
per-tile-row S stride used at emit.
"""
gs = [t_total // 32, 8, 4, m_total // (num * 2), num, 2]
try:
rg, rsep = r_in.group(gs)
sg, ssep = s_in.group(gs)
except Exception:
return None
# R seg 0 (T/32 outer): require single iter with stride 32. When
# T/32 == 1 the segment is trivial — skip.
if t_total > 32:
seg0 = list(rg.shard[rsep[0] : rsep[1]])
if len(seg0) != 1 or int(seg0[0].stride) != 32:
return None
rs, ss = _strs(rg, rsep), _strs(sg, ssep)
if rs is None or ss is None:
return None
r8, r4, _r_num_iters, r2 = rs
s8, s4, s_num_iters, s2 = ss
if (r8, r4, r2) != (4, 1, 1):
return None
# S num atom: every iter must have stride > 0 and multiple of 8 (the
# per-tile spacing geometry of ldmatrix m8n8; 8 fp16 = 16 bytes = one
# tile column dimension).
if num > 1 and not all(
int(it.stride) > 0 and int(it.stride) % 8 == 0 for it in s_num_iters
):
return None
# m_outer (seg 3) iters: each per-mm advance must keep the per-lane
# SMEM address 16-byte aligned (ldmatrix .b16 reads 8 fp16 = 16 bytes
# per lane), so the m_outer S-stride must also be a multiple of 8.
# Without this, mm > 0 iterations land at unaligned addresses and
# silently read garbage even though the layout group succeeds.
# Skip extent-1 trivial iters — they contribute no per-mm advance,
# so their (placeholder) stride is irrelevant.
m_outer_iters = list(sg.shard[ssep[3] : ssep[4]])
if not all(int(it.extent) == 1 or int(it.stride) % 8 == 0 for it in m_outer_iters):
return None
if (s4, s2) == (2, 1) and s8 > 0 and s8 % 8 == 0:
return (rg, rsep, sg, ssep, False, s8, num)
if s8 == 1 and s2 > 0 and s2 % 8 == 0 and s4 == 2 * s2:
return (rg, rsep, sg, ssep, True, s2, num)
return None
# Try the **sorted** variant: 5D-group, sub-group R's M/2 by S's M/2
# extents, sort the sub-groups by descending S-stride, rebuild. This
# makes the m_outer iter list carry the largest S-strides on top, which
# maximizes the §2 swizzle fast-path applicability later. If anything
# in the rebuild raises (e.g. M/2 can't be sub-grouped by S's extents),
# we silently fall back to the no-sort path below.
r_sort = s_sort = None
try:
gs5 = [t_total // 32, 8, 4, m_total // 2, 2]
rg5, rsep5 = r.group(gs5)
sg5, ssep5 = s.group(gs5)
r_m_iters = list(rg5.shard[rsep5[3] : rsep5[4]])
s_m_iters = list(sg5.shard[ssep5[3] : ssep5[4]])
s_m_extents = [int(it.extent) for it in s_m_iters]
# Sub-group R's M/2 iters by S's M/2 iter extents. This 1-to-1's
# the R sub-groups with the S iters so we can permute them together.
r_m_sub = TileLayout.from_iters(r_m_iters)
r_m_grouped, r_m_seps = r_m_sub.group(s_m_extents)
# Sort S iters by S-stride descending; permute R sub-groups in lockstep.
perm = sorted(range(len(s_m_iters)), key=lambda i: -int(s_m_iters[i].stride))
if perm != list(range(len(perm))):
r_m_permuted = r_m_grouped.permute_by_groups(list(r_m_seps), perm)
s_m_permuted = [s_m_iters[i] for i in perm]
r_sort = TileLayout.from_iters(
list(rg5.shard[: rsep5[3]])
+ list(r_m_permuted.shard)
+ list(rg5.shard[rsep5[4] :]),
offset=dict(rg5.offset),
)
s_sort = TileLayout.from_iters(
list(sg5.shard[: ssep5[3]]) + list(s_m_permuted) + list(sg5.shard[ssep5[4] :]),
offset=dict(sg5.offset),
)
# If perm is identity, sorted == unsorted; no need to build duplicate layouts.
except Exception:
r_sort = s_sort = None
# Enumerate num largest-first; for each num try sorted then unsorted.
chosen = None
for num in (4, 2, 1):
if m_total % (num * 2):
continue
if r_sort is not None:
res = _try_num(r_sort, s_sort, num)
if res is not None:
chosen = res
break
res = _try_num(r, s, num)
if res is not None:
chosen = res
break
if chosen is None:
fail("ldstmatrix layout doesn't fit any num ∈ {4,2,1}")
r, r_seps, s, s_seps, trans, p, num = chosen
# Step 10: emit one ldmatrix/stmatrix per mm, per warp.
def _get_warp_idx_in_T():
# T.warp_id_in_wg() / T.warp_id() must be called from inside a
# @T.prim_func body — wrap so the prim_func parser calls us at parse
# time (Python `if` here is plain control flow, not TIR-intercepted).
if r_lane_axis == "laneid":
return 0
if r_lane_axis == "tid_in_wg":
return T.warp_id_in_wg()
return T.warp_id() # "tx"
def _seg4_coord(laneid_expr):
# num=1: seg 4 trivially extent-1, pass 0. num>1: use lane//8 (tile
# index in ldmatrix lane convention); layout.apply decomposes through
# the seg's iter structure (single or multi-iter).
if num > 1:
return laneid_expr // 8
return 0
apply_shape = [t_total // 32, 8, 4, m_total // (num * 2), num, 2]
r_mem_axis = r.shard[r_seps[5]].axis.name
s_mem_axis = s.shard[s_seps[5]].axis.name
m_outer = m_total // (num * 2)
s_zero = [0] * len(s_buf.shape)
# Swizzle fast-path setup. When S is swizzled, the per-mm `tile_off +
# row_off` is a logical offset; the physical SMEM address is
# `swizzle.apply(logical)`. The slow path computes that per iter; the
# fast path (§2.E of the swizzle-iter plan) reduces it to
# `base_off + sum_j bit_j(mm) · signed_strides[j]` where base_off and
# signed_strides are per-thread constants set once. We try to
# recognize the m_outer iter list as such a pattern; if it fails (e.g.
# the analyzer can't discharge condition C1 over the lane/warp
# placeholders) we silently fall through to the slow path.
swizzle_pattern = None
s_off_template = None
lane_ph = warp_ph = None
if s_swizzle is not None:
m_outer_iters = list(s.shard[s_seps[3] : s_seps[4]])
iter_extents = [int(it.extent) for it in m_outer_iters]
iter_strides = [int(it.stride) for it in m_outer_iters]
# Build s_off at mm=0 with placeholder vars for lane and warp_idx.
lane_ph = _TirVar("lane_ph", "int32")
seg4_ph = (lane_ph // 8) if num > 1 else _IntImm("int32", 0)
if r_lane_axis == "laneid":
warp_ph_expr = _IntImm("int32", 0)
else:
warp_ph = _TirVar("warp_ph", "int32")
warp_ph_expr = warp_ph
s_off_template = s.apply(
warp_ph_expr,
_IntImm("int32", 0),
_IntImm("int32", 0),
_IntImm("int32", 0),
seg4_ph,
_IntImm("int32", 0),
shape=apply_shape,
)[s_mem_axis] + (lane_ph % 8) * _IntImm("int32", p)
# Bind lane / warp placeholder bounds for the (C1) analyzer. ``lane_ph``
# is the per-warp lane id ∈ [0, 32); ``warp_ph`` (when present) is the
# warp index inside the scope: warpgroup ⇒ [0, 4), cta ⇒ [0, t_total/32).
var_bounds = {lane_ph: tvm.ir.Range.from_min_extent(0, 32)}
if warp_ph is not None:
var_bounds[warp_ph] = tvm.ir.Range.from_min_extent(0, t_total // 32)
swizzle_pattern = try_recognize(
s_swizzle,
iter_extents,
iter_strides,
s_off_template,
var_bounds=var_bounds,
)
class _SwizzleState:
def __init__(self):
self.signed_strides = None
self.base_off = None
state = _SwizzleState()
def _resolve_s_off(laneid_var, warp_var):
# Build the placeholder→runtime-var map and substitute. Keep this in a
# regular Python helper — the @T.prim_func parser intercepts dict
# literals when written directly in the body.
vmap = {lane_ph: laneid_var}
if warp_ph is not None:
vmap[warp_ph] = warp_var
return tvm.tirx.stmt_functor.substitute(s_off_template, vmap)
def _setup_swizzle(s_off_resolved):
if swizzle_pattern is None:
return
state.signed_strides, state.base_off = emit_init(
swizzle_pattern,
s_off_resolved,
)
def _smem_off(mm_idx, logical_off):
# Three paths:
# * pattern matched: physical off = base_off + Σ bit_j(mm)·ss[j].
# * swizzle present, pattern missed: per-iter swizzle.apply(logical).
# * no swizzle: identity.
if swizzle_pattern is not None:
return emit_iter_offset(
swizzle_pattern,
state.signed_strides,
state.base_off,
mm_idx,
)
if s_swizzle is not None:
return s_swizzle.apply(logical_off)["m"]
return logical_off
# fmt: off
@T.prim_func(check_well_formed=False)
def impl():
r_local = r_buf.local(m_total, layout=TileLayout(S[(m_total,)]))
laneid = T.lane_id()
warp_idx_in_T = _get_warp_idx_in_T()
# Resolve s_off_template by substituting placeholders → actual
# scope-id vars (via _resolve_s_off helper to keep the dict literal
# out of the parser's view). Only the swizzle fast path needs this;
# without swizzle we keep using the per-iter s.apply directly.
if swizzle_pattern is not None:
_setup_swizzle(_resolve_s_off(laneid, warp_idx_in_T))
for mm in T.unroll(m_outer):
tile_off = s.apply(
warp_idx_in_T, 0, 0, mm, _seg4_coord(laneid), 0, shape=apply_shape,
)[s_mem_axis]
row_off = (laneid % 8) * p
logical_off = tile_off + row_off
smem_ptr = _ptr_off(s_buf.ptr_to(s_zero), _smem_off(mm, logical_off))
handles = [
r_local.ptr_to([
r.apply(0, 0, 0, mm, i, 0, shape=apply_shape)[r_mem_axis]
])
for i in range(num)
]
if direction == "ld":
T.ptx.ldmatrix(trans, num, ".b16", smem_ptr, *handles)
else:
T.ptx.stmatrix(
trans, num, ".b16", smem_ptr, *handles,
shape="m8n8", space="shared",
)
# fmt: on
return impl
@register_dispatch(
"copy",
"cuda",
variant="ldstmatrix",
priority=10,
when=[predicate("ldstmatrix_applicable", _is_ldstmatrix)],
)
def copy_schedule_ldstmatrix(op_call: TilePrimitiveCall, sctx: DispatchContext) -> PrimFunc:
return _emit(op_call, sctx)
__all__ = ["copy_schedule_ldstmatrix"]
@@ -0,0 +1,596 @@
# 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.
"""Non-ldmatrix copy dispatch for register ↔ memory.
This file owns every copy where one side is per-thread local (``R`` =
register). That R side carries the partition: its ``TileLayout`` ``shard``
has thread-axis iters telling us which thread owns which logical coordinate.
The other side (``S``) can be ``shared*`` or ``global`` — the algorithm is
identical either way.
Slice/canonicalize both sides, align via perm+group, then emit a per-thread
vectorized copy loop. Direction-symmetric: covers R2S / S2R / R2G / G2R.
"""
import tvm
from tvm.arith import Analyzer
from tvm.runtime import DataType
from tvm.script import tirx as T
from tvm.tirx import Buffer, PrimFunc
from tvm.tirx import Var as _TirVar
from tvm.tirx.expr import IntImm as _IntImm
from tvm.tirx.layout import ComposeLayout, S, SwizzleLayout, TileLayout
from tvm.tirx.operator.tile_primitive.dispatcher import predicate, register_dispatch
from tvm.tirx.operator.tile_primitive.registry import DispatchContext
from tvm.tirx.stmt import TilePrimitiveCall
from ._common import _alignment_ok, copy_ptx_form, copy_ptx_ld_return_type
from ._swizzle_iter import (
emit_fallback_offset,
emit_init,
emit_iter_offset,
get_swizzle,
try_recognize,
)
from .utils import _is_valid_copy, _scope_allowed
def _extract_tile(layout, region):
"""Strip swizzle off ``layout`` so we can perm/group it as a TileLayout.
``region`` is the per-axis ``(start, end)`` pair list — we only consume
its extents when ``layout`` is a bare ``SwizzleLayout`` (rebuilding a
trivial TileLayout for it). Plain ``TileLayout`` / ``ComposeLayout``
don't need the extent, so symbolic regions are fine for them.
"""
if isinstance(layout, ComposeLayout):
return layout.tile_layout
if isinstance(layout, SwizzleLayout):
# TODO: keep swizzle info around for later (addressing in emit).
extents = [int(end - start) for (start, end) in region]
return TileLayout(S[tuple(extents)])
return layout
_REG_PAIRS = [
("local", "shared*"),
("shared*", "local"),
("local", "global"),
("global", "local"),
]
_SCOPE_RANK = {"thread": 0, "warp": 1, "warpgroup": 2, "cta": 3}
_VALID_R_SUBSCOPES = {"thread", "warp", "warpgroup"}
def _all_threads_active(sctx: DispatchContext) -> tuple[bool, str | None]:
if sctx.scope_kind == "thread":
return True, None
required: dict[str, int] = {}
if sctx.scope_kind in ("warp", "warpgroup", "cta"):
required["laneid"] = 32
if sctx.scope_kind == "warpgroup":
required["wid_in_wg"] = 4
if sctx.scope_kind == "cta":
tx_iv = sctx.launch_params.get("threadIdx.x")
if tx_iv is None:
return False, "cta scope missing threadIdx.x launch_params"
try:
required["warpid"] = int(tx_iv.dom.extent) // 32
except (TypeError, ValueError):
return False, f"non-static threadIdx.x extent: {tx_iv.dom.extent}"
for axis_name, expected in required.items():
if axis_name not in sctx.intra:
return False, f"sctx.intra missing {axis_name!r}"
ext_raw, off_raw = sctx.intra[axis_name]
try:
ext, off = int(ext_raw), int(off_raw)
except (TypeError, ValueError):
return False, f"non-static range for {axis_name}: ({ext_raw}, {off_raw})"
if ext != expected or off != 0:
return False, f"{axis_name} narrowed to [{off}, {off + ext}) vs full [0, {expected})"
return True, None
def _r_side_layout_valid(
op_call: TilePrimitiveCall, sctx: DispatchContext
) -> tuple[bool, str | None]:
op_call = TilePrimitiveCall.downcast(op_call)
src: Buffer = op_call.src.buffer
dst: Buffer = op_call.dst.buffer
r_buf = src if src.scope() == "local" else dst
layout = r_buf.layout
if layout is None:
return False, "R has no layout"
if layout.is_swizzle():
return False, "R layout is swizzle"
if not isinstance(layout, TileLayout):
return False, f"R layout is {type(layout).__name__}, not TileLayout"
scope_rank = _SCOPE_RANK[sctx.scope_kind]
for it in layout.shard:
ax = it.axis
if not ax.is_thread():
continue
ax_scope = ax.get_scope()
ax_sub = ax.get_subscope()
if ax_scope is None or ax_sub is None:
return False, f"R thread axis {ax.name!r} missing scope/subscope"
if ax_sub.name not in _VALID_R_SUBSCOPES:
return False, f"R thread axis {ax.name!r} subscope={ax_sub.name!r} (not register-level)"
if ax_scope.name not in _SCOPE_RANK or _SCOPE_RANK[ax_scope.name] > scope_rank:
return (
False,
f"R thread axis {ax.name!r} scope={ax_scope.name!r} > exec {sctx.scope_kind!r}",
)
r_br = op_call.src if src.scope() == "local" else op_call.dst
region = [(r.min, r.min + r.extent) for r in r_br.region]
sliced = layout.slice(list(r_buf.shape), region)
if sliced is None:
return False, "R layout slice failed"
analyzer = Analyzer()
for axis, off in sliced.offset.items():
if axis.is_thread() and not analyzer.can_prove_equal(off, 0):
return False, f"R sliced offset on thread axis {axis.name!r} = {off}"
return True, None
def _s_side_slice_ok(op_call: TilePrimitiveCall) -> tuple[bool, str | None]:
"""S is the non-local side (shared* or global). Slice must succeed."""
op_call = TilePrimitiveCall.downcast(op_call)
src_br = op_call.src
dst_br = op_call.dst
s_br = dst_br if src_br.buffer.scope() == "local" else src_br
s_buf: Buffer = s_br.buffer
layout = s_buf.layout
if layout is None:
return False, "S has no layout"
region = [(r.min, r.min + r.extent) for r in s_br.region]
if layout.slice(list(s_buf.shape), region) is None:
return False, "S layout slice failed"
return True, None
def _is_reg_copy(op_call: TilePrimitiveCall, sctx: DispatchContext) -> tuple[bool, str | None]:
if not sctx.is_target("cuda"):
return False, "non-cuda target"
if sctx.scope_kind not in ("thread", "warp", "warpgroup", "cta"):
return False, f"unsupported exec_scope {sctx.scope_kind}"
for check in (
lambda: _all_threads_active(sctx),
lambda: _is_valid_copy(op_call, sctx),
lambda: _scope_allowed(op_call, sctx, allowed_pairs=_REG_PAIRS),
lambda: _r_side_layout_valid(op_call, sctx),
lambda: _s_side_slice_ok(op_call),
):
ok, msg = check()
if not ok:
return False, msg
return True, None
def _compute_perm_r(r):
# thread axes first, then by stride descending
def key(p):
it = p[1]
return (0 if it.axis.is_thread() else 1, -int(it.stride))
return [i for i, _ in sorted(enumerate(r.shard), key=key)]
def align_layouts_raw(r_sliced, s_sliced, s_region):
"""Returns (r_p, s_p, s_seps, r_perm).
``r_p`` for ``_s_thread_offset``; ``r_perm`` for ``_split_thread_loop`` pairing.
"""
r = r_sliced.canonicalize()
s = s_sliced.canonicalize()
s = _extract_tile(s, s_region)
perm = _compute_perm_r(r)
r_shape_for_group = [int(it.extent) for it in r.shard]
s_grp, seps = s.group(r_shape_for_group)
s_p = s_grp.permute_by_groups(list(seps), perm)
r_perm = r.permute_dims(perm)
r_p = r_perm.canonicalize()
sizes = [seps[i + 1] - seps[i] for i in range(len(seps) - 1)]
s_seps = [0]
for p in perm:
s_seps.append(s_seps[-1] + sizes[p])
return r_p, s_p, s_seps, r_perm
def _split_thread_loop(r_perm, s_p, s_seps):
"""Drop R's thread-axis positions and return per-R-position bundles:
(r_iters, s_groups) — same length lists; s_groups[k] is the list of S
iters belonging to the k-th kept R position.
``r_perm`` is the permuted-but-uncanonicalized R layout: its iter list lines
up one-to-one with ``s_seps`` (which is built from the same ``perm``), so a
multi-group memory axis keeps each of its groups paired with the matching S
group instead of collapsing into one (see ``align_layouts_raw``)."""
r_iters = []
s_groups = []
for k, r_it in enumerate(r_perm.shard):
if r_it.axis.is_thread():
continue
r_iters.append(r_it)
s_groups.append(list(s_p.shard[s_seps[k] : s_seps[k + 1]]))
return r_iters, s_groups
def _build_atoms(r_iters, s_groups):
"""One atom per (R position, intra-group S iter): (extent, s_stride, r_mul).
r_mul = R_stride_at_position * (product of S group extents to the right
of this intra-position index) — i.e. how much R address advances per unit
of this iter's loop input."""
atoms = []
for r_it, s_group in zip(r_iters, s_groups, strict=True):
rs = int(r_it.stride)
extents = [int(it.extent) for it in s_group]
for j, s_it in enumerate(s_group):
inner_prod = 1
for e in extents[j + 1 :]:
inner_prod *= e
atoms.append((int(s_it.extent), int(s_it.stride), rs * inner_prod))
return atoms
def _atoms_contiguous_tail_extent(atoms) -> int:
"""Like _contiguous_tail_extent but on atoms (uses s_stride for chaining)."""
if not atoms or atoms[-1][1] != 1:
return 0
acc = atoms[-1][0]
for k in range(len(atoms) - 2, -1, -1):
if atoms[k][1] == acc:
acc *= atoms[k][0]
else:
break
return acc
def _split_atoms_for_vec(atoms, vec_len):
"""Returns outer atoms (the inner vec_len-element tail is consumed by one
vec ld/st and dropped). Splits the boundary atom if needed."""
outer = list(atoms)
acc = 1
while outer:
ext, ss, rm = outer[-1]
new_acc = acc * ext
if new_acc == vec_len:
outer.pop()
return outer
if new_acc > vec_len:
inner_factor = vec_len // acc
outer[-1] = (ext // inner_factor, ss * inner_factor, rm * inner_factor)
return outer
acc = new_acc
outer.pop()
raise ValueError(f"tail too short for vec_len {vec_len}")
def _align_layouts(op_call: TilePrimitiveCall, sctx: DispatchContext):
op_call = TilePrimitiveCall.downcast(op_call)
src_br = op_call.src
dst_br = op_call.dst
if src_br.buffer.scope() == "local":
r_br, s_br = src_br, dst_br
else:
r_br, s_br = dst_br, src_br
r_buf = r_br.buffer
s_buf = s_br.buffer
r_region = [(r.min, r.min + r.extent) for r in r_br.region]
s_region = [(r.min, r.min + r.extent) for r in s_br.region]
with sctx.target:
r_sliced = r_buf.layout.slice(list(r_buf.shape), r_region)
s_sliced = s_buf.layout.slice(list(s_buf.shape), s_region)
return align_layouts_raw(r_sliced, s_sliced, s_region)
def _make_thread_placeholders(r_p) -> dict[str, _TirVar]:
placeholders: dict[str, _TirVar] = {}
for it in r_p.shard:
name = it.axis.name
if it.axis.is_thread() and name not in placeholders:
placeholders[name] = _TirVar(name, "int32")
return placeholders
def _s_thread_offset(r_p, s_p, placeholders: dict[str, _TirVar]):
"""Per-thread S base offset. Coord per R position is placeholder (thread
axis) or 0 (memory axis); apply_to_shape decomposes across s_p iters.
Includes layout-level offsets (e.g. from slicing a non-zero S region)."""
coord = [
placeholders[it.axis.name] if it.axis.is_thread() else _IntImm("int32", 0)
for it in r_p.shard
]
input_shape = [int(it.extent) for it in r_p.shard]
per_iter = s_p.apply_to_shape(coord, input_shape)
off = _IntImm("int32", 0)
for c, it in zip(per_iter, s_p.shard, strict=True):
off = off + c * it.stride
for _axis, val in s_p.offset.items():
off = off + val
return off
_VEC_BITS_CANDIDATES = (128, 64, 32, 16, 8)
def _vec_len_candidates(elem_bits: int) -> list[int]:
"""Widest-first element counts to try; always ends with scalar (1)."""
out: list[int] = []
for vb in _VEC_BITS_CANDIDATES:
if vb < elem_bits or vb % elem_bits != 0:
continue
n = vb // elem_bits
if n not in out:
out.append(n)
if 1 not in out:
out.append(1)
return out
def _choose_vec_len(elem_bits: int, atoms, r_p, s_p) -> int:
"""Widest candidate that:
1. divides the atom contiguous-tail extent (so vec_len consecutive
R-side regs map to vec_len contiguous S-side elements), AND
2. keeps every per-thread / per-round address-offset term a
multiple of vec_len, so the resulting vec ld/st pointer is
naturally aligned to vec_bits/8 bytes.
Only **mem-axis** strides contribute to physical address. Thread-axis
iter strides live in partition-coord space (which thread owns which
logical position), not in the buffer's storage space — they're
redistributed through ``apply_to_shape`` into the mem iters and don't
appear directly in the per-thread address. So neither r-side nor
s-side thread-axis strides belong in the alignment check.
The contig-tail atoms (whose extents the vec ld/st consumes) have
stride 1 by definition; they live entirely inside the vec and
contribute nothing to the per-round address delta. Only the
**post-vec-split** outer atom strides matter for the per-round delta.
"""
t = _atoms_contiguous_tail_extent(atoms)
# Region-base offsets are real address contributions. Thread-iter
# strides on either side are partition-virtual, not storage-physical,
# so they don't enter the per-thread address — exclude them.
shared_terms = list(s_p.offset.values()) + list(r_p.offset.values())
for n in _vec_len_candidates(elem_bits):
if n == 1:
return n
if t % n != 0 or t < n:
continue
# Post-vec-split outer atoms: these are the strides that contribute
# to per-round address deltas after the vec consumes the inner tail.
outer = _split_atoms_for_vec(atoms, n)
outer_atom_terms = [a[1] for a in outer] + [a[2] for a in outer]
if not _alignment_ok(n, outer_atom_terms + shared_terms):
continue
return n
return 1
def _axis_decl(axis_name: str, sctx: DispatchContext):
"""Declare the runtime Var for one thread axis (called inside impl body).
Each scope_id declarator emits a ``ScopeIdDef`` stmt at the current
builder frame. ``TilePrimitiveDispatch`` re-gathers + resolves all
ScopeIdDefs after dispatch (see ``ResolveAllScopeBinds`` in
``tile_primitive_dispatch.cc``), so dispatch-introduced vars are bound
alongside kernel-declared ones.
Extents are deferred: the kernel header is expected to declare the full
scope-id chain (``cta_id`` / ``warpgroup_id`` / ``warp_id_in_wg`` /
``lane_id`` / ``thread_id`` / ``thread_id_in_wg``) — the verifier then
fills our deferred defs from those siblings.
"""
if axis_name == "tx":
return sctx.launch_params["threadIdx.x"].var
if axis_name == "laneid":
return T.lane_id()
if axis_name == "wid_in_wg":
return T.warp_id_in_wg()
if axis_name == "tid_in_wg":
return T.thread_id_in_wg()
if axis_name == "warpid":
return T.warp_id()
if axis_name == "wgid":
return T.warpgroup_id()
raise ValueError(f"unsupported thread axis {axis_name}")
def _s_thread_offset_with_vars(r_p, s_p, axis_var_map: dict):
coord = [
axis_var_map[it.axis.name] if it.axis.is_thread() else _IntImm("int32", 0)
for it in r_p.shard
]
input_shape = [int(it.extent) for it in r_p.shard]
per_iter = s_p.apply_to_shape(coord, input_shape)
off = _IntImm("int32", 0)
for c, it in zip(per_iter, s_p.shard, strict=True):
off = off + c * it.stride
for _ax, val in s_p.offset.items():
off = off + val
return off
def _substitute_axes(s_off_template, placeholders: dict[str, _TirVar], sctx: DispatchContext):
"""Inside an impl body: declare real scope_ids and rewrite the
placeholder-built ``s_off_template`` to use them."""
vmap = {placeholders[name]: _axis_decl(name, sctx) for name in placeholders}
return tvm.tirx.stmt_functor.substitute(s_off_template, vmap)
def _flat_coords(outer_atoms, flat_idx: int) -> list[int]:
coords = []
rem = flat_idx
for a in reversed(outer_atoms):
coords.append(rem % a[0])
rem //= a[0]
coords.reverse()
return coords
_POINTER_OFFSET_SRC = (
"\ntemplate <typename T>\n"
"__forceinline__ __device__ T* tvm_builtin_pointer_offset(T* ptr, int offset) {\n"
" return ptr + offset;\n"
"}\n"
)
def _ptr_off(base_ptr, off):
return T.cuda.func_call(
"tvm_builtin_pointer_offset",
base_ptr,
off,
source_code=_POINTER_OFFSET_SRC,
return_type=base_ptr.ty,
)
def _outer_const_offsets(outer_atoms, flat_idx: int) -> tuple[int, int]:
"""Returns (s_offset_const, r_offset_const) for one outer-loop flat index."""
coords = _flat_coords(outer_atoms, flat_idx)
ds = sum(c * a[1] for c, a in zip(coords, outer_atoms))
dr = sum(c * a[2] for c, a in zip(coords, outer_atoms))
return ds, dr
def _emit_reg(op_call: TilePrimitiveCall, sctx: DispatchContext) -> PrimFunc:
op_call = TilePrimitiveCall.downcast(op_call)
src: Buffer = op_call.src.buffer
dst: Buffer = op_call.dst.buffer
if src.scope() == "local":
r_buf, s_buf, r_is_src = src, dst, True
else:
r_buf, s_buf, r_is_src = dst, src, False
with sctx.target:
r_p, s_p, s_seps, r_perm = _align_layouts(op_call, sctx)
r_iters, s_groups = _split_thread_loop(r_perm, s_p, s_seps)
atoms = _build_atoms(r_iters, s_groups)
elem_bits = DataType(src.dtype).bits
vec_len = _choose_vec_len(elem_bits, atoms, r_p, s_p)
vec_bits = vec_len * elem_bits
outer = _split_atoms_for_vec(atoms, vec_len)
per_thread_r_total = 1
for it in r_iters:
per_thread_r_total *= int(it.extent)
per_thread_r_shape = [per_thread_r_total or 1]
# Build the per-thread S offset OUTSIDE the impl using placeholder Vars
# (one per thread axis). Inside the impl we'll declare the real scope_ids
# via T.lane_id/T.thread_id_in_wg/... and substitute them in.
placeholders = _make_thread_placeholders(r_p)
s_off_template = _s_thread_offset(r_p, s_p, placeholders)
# R-side base offset from slicing (e.g. ``R[i*8:i*8+8]`` ⇒ ``i*8``). The
# canonicalize() result lives in ``r_p.offset``; sum across axes (memory
# or thread — irrelevant once it's all on R's local stride-1 storage).
r_off_base = _IntImm("int32", 0)
for _ax, val in r_p.offset.items():
r_off_base = r_off_base + val
s_is_shared = s_buf.scope().startswith("shared")
num_bytes = vec_bits // 8
vec, ptx_type = copy_ptx_form(num_bytes)
space = "shared" if s_is_shared else "global"
total_outer = 1
for a in outer:
total_outer *= a[0]
# Swizzle handling: recognize the iter-pattern on S side from the atom
# extents/strides (atom = (extent, s_stride, r_mul); a[1] is the S-side
# stride per outer round, equivalent to outer_iter strides in gmem_smem).
swizzle = get_swizzle(s_buf.layout)
swizzle_pattern = None
if swizzle is not None:
swizzle_pattern = try_recognize(
swizzle,
[a[0] for a in outer],
[a[1] for a in outer],
s_off_template,
)
class _SwizzleState:
def __init__(self):
self.signed_strides = None
self.base_off = None
state = _SwizzleState()
def _setup_swizzle(s_off):
if swizzle_pattern is None:
return
state.signed_strides, state.base_off = emit_init(swizzle_pattern, s_off)
def _s_iter_off(f, ds, s_off):
if swizzle_pattern is not None:
return emit_iter_offset(swizzle_pattern, state.signed_strides, state.base_off, f)
if swizzle is not None:
return emit_fallback_offset(swizzle, s_off, ds)
return s_off + ds
# fmt: off
s_zero_indices = [0] * len(s_buf.shape)
@T.prim_func(check_well_formed=False)
def impl():
s_off = _substitute_axes(s_off_template, placeholders, sctx)
_setup_swizzle(s_off)
r_local = r_buf.local(*per_thread_r_shape)
# Keep as a serial TIR loop and let ptxas unroll downstream. An
# explicit ``T.unroll`` materializes the per-iter scratch
# (ds/dr/s_ptr/r_ptr, swizzle ``v_<n>[]`` signed-strides) as N
# copies of each buffer declaration; on kernels with many R↔S copy
# sites and large ``total_outer`` (FA4 writeback) this floods the
# function with ``alignas(64) int`` arrays and pressures registers.
for f in range(total_outer):
ds, dr = _outer_const_offsets(outer, f)
s_ptr = _ptr_off(s_buf.ptr_to(s_zero_indices), _s_iter_off(f, ds, s_off))
r_ptr = _ptr_off(r_local.ptr_to([0]), r_off_base + dr)
if r_is_src:
T.ptx.st(s_ptr, src=r_ptr, space=space, vec=vec, ptx_type=ptx_type)
else:
T.ptx.ld(
s_ptr,
copy_ptx_ld_return_type(ptx_type),
ptx_type,
dst=r_ptr,
space=space,
vec=vec,
)
# fmt: on
import os
if os.environ.get("R2S_DUMP"):
print("=== emitted impl ===")
print(impl.script())
return impl
@register_dispatch(
"copy",
"cuda",
variant="reg",
priority=10,
when=[predicate("reg_applicable", _is_reg_copy)],
)
def copy_schedule_reg(op_call: TilePrimitiveCall, sctx: DispatchContext) -> PrimFunc:
return _emit_reg(op_call, sctx)
@@ -0,0 +1,101 @@
# 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.
"""Shared helpers for copy operator dispatches on CUDA targets."""
from collections.abc import Iterable
from tvm.tirx import Buffer
from tvm.tirx.operator.tile_primitive.registry import DispatchContext
from tvm.tirx.stmt import TilePrimitiveCall
from ..common import match_scope, validate_copy_op
def _is_valid_smem_tmem_copy(op_call: TilePrimitiveCall, sctx: DispatchContext):
"""Validate smem->tmem copy operation.
The new tcgen05.cp.32x128b.warpx4 dispatch requires the destination tmem
buffer to declare warpx4 broadcast as ``R[4 : 32@TLane]``. The legacy
128-row dispatch path (no replica) goes through a separate code path and
is not handled here.
"""
dst_region, src_region = op_call.args[:2]
src: Buffer = src_region.buffer
dst: Buffer = dst_region.buffer
if not (src.scope().startswith("shared") and dst.scope() == "tmem"):
return (False, f"expected shared->tmem, got {src.scope()}->{dst.scope()}")
if not (src.layout and dst.layout):
return (False, "both buffers must have layouts")
if dst.allocated_addr is None:
return (False, "tmem buffer must have allocated_addr")
# Require warpx4 router on TMEM side so this dispatch only handles the
# 32x128b.warpx4 case; other shapes (128x256b/128x128b etc.) fall back
# to the legacy dispatch.
rep = dst.layout.replica
if not (
len(rep) == 1
and int(rep[0].extent) == 4
and int(rep[0].stride) == 32
and "TLane" in str(rep[0].axis)
):
return (False, f"requires R[4:32@TLane] on tmem, got replica={list(rep)}")
return (True, None)
def _single_thread_exec(op_call: TilePrimitiveCall, sctx: DispatchContext):
"""Predicate: exec scope must be single-thread."""
exec_scope = sctx.scope_kind
ok = exec_scope == "thread"
return (ok, None if ok else f"expected thread exec_scope, got {exec_scope}")
DEFAULT_ALLOWED_PAIRS: tuple[tuple[str, str], ...] = (
("global", "shared*"),
("shared*", "global"),
("global", "local"),
("local", "global"),
("shared*", "local"),
("local", "shared*"),
)
def _scope_allowed(
op_call: TilePrimitiveCall,
sctx: DispatchContext,
allowed_pairs: Iterable[tuple[str, str]] = DEFAULT_ALLOWED_PAIRS,
):
op_call = TilePrimitiveCall.downcast(op_call)
dst_buffer_region, src_buffer_region = (op_call.dst, op_call.src)
src_scope = src_buffer_region.buffer.scope()
dst_scope = dst_buffer_region.buffer.scope()
ok = any(
(
match_scope(src_scope, src_pat) and match_scope(dst_scope, dst_pat)
for src_pat, dst_pat in allowed_pairs
)
)
if not ok:
allowed_str = ", ".join((f"{a}->{b}" for a, b in allowed_pairs))
return (
False,
f"unsupported memory scopes src={src_scope} dst={dst_scope}; allowed: {allowed_str}",
)
return (True, None)
def _is_valid_copy(op_call: TilePrimitiveCall, sctx: DispatchContext):
return (validate_copy_op(op_call, sctx), "validate_copy_op failed")
@@ -0,0 +1,29 @@
# 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.
"""Implementation of copy_async operator dispatches for CUDA targets.
Registered op: copy_async (4 variants).
See the @register_dispatch blocks in each submodule for detailed documentation
with before/after IR examples.
"""
from .dsmem import *
from .ldgsts import *
from .tcgen05_cp import *
from .tcgen05_ldst import *
from .tma import *
@@ -0,0 +1,226 @@
# 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.
"""copy_async dispatch variant: dsmem (shared::cta -> shared::cluster)."""
import functools
import operator
import tvm
from tvm.script import tirx as T
from tvm.tirx import Buffer, PrimFunc
from tvm.tirx.operator.tile_primitive import (
DispatchContext,
fail,
predicate,
register_dispatch,
)
from tvm.tirx.stmt import TilePrimitiveCall
from ..common import validate_copy_op
from ..exec_scope_utils import single_thread
from .utils import find_contiguous_region, to_tile_layout
def _is_shared_to_shared(op_call: TilePrimitiveCall) -> bool:
"""Check if both src and dst are in shared memory."""
op_call = TilePrimitiveCall.downcast(op_call)
src_scope = op_call.src.buffer.scope()
dst_scope = op_call.dst.buffer.scope()
return src_scope.startswith("shared") and dst_scope.startswith("shared")
def copy_dsmem_impl(op_call: TilePrimitiveCall, sctx: DispatchContext) -> PrimFunc:
"""Implement shared-to-shared cross-CTA copy using cp.async.bulk.
Uses cp.async.bulk.shared::cluster.shared::cta.mbarrier::complete_tx::bytes
to copy data from the executing CTA's shared memory to a remote CTA's shared
memory within the same cluster.
The copy region is decomposed into contiguous byte chunks based on layout
analysis of both src and dst buffers. Non-contiguous dimensions are iterated
over, emitting one cp.async.bulk instruction per contiguous chunk.
"""
op_call = TilePrimitiveCall.downcast(op_call)
# Extract config
remote_cta_id = op_call.config.get("remote_cta_id", None)
if remote_cta_id is None:
fail("remote_cta_id not set in config")
mbar = op_call.config.get("mbar", None)
if mbar is None:
fail("mbar not set in config")
# Extract buffer regions
dst_buffer_region = op_call.dst
src_buffer_region = op_call.src
src_buf: Buffer = src_buffer_region.buffer
dst_buf: Buffer = dst_buffer_region.buffer
src_st = [r.min for r in src_buffer_region.region]
src_ext = [r.extent for r in src_buffer_region.region]
dst_st = [r.min for r in dst_buffer_region.region]
dst_ext = [r.extent for r in dst_buffer_region.region]
dtype_bytes = tvm.DataType(src_buf.dtype).bits // 8
# Get tile layouts for both buffers
src_tile_layout = to_tile_layout(src_buf.layout, src_buf.shape)
dst_tile_layout = to_tile_layout(dst_buf.layout, dst_buf.shape)
# Slice layouts to copy region
src_region_tuples = [(src_st[i], src_st[i] + src_ext[i]) for i in range(len(src_st))]
sliced_src = src_tile_layout.slice([s for s in src_buf.shape], src_region_tuples)
if sliced_src is None:
fail("Cannot slice src layout for DSMEM copy")
dst_region_tuples = [(dst_st[i], dst_st[i] + dst_ext[i]) for i in range(len(dst_st))]
sliced_dst = dst_tile_layout.slice([s for s in dst_buf.shape], dst_region_tuples)
if sliced_dst is None:
fail("Cannot slice dst layout for DSMEM copy")
# Group src layout by region extents, then group dst by src's shard extents
# This creates 1:1 shard correspondence between the two layouts
grouped_src, src_seps = sliced_src.canonicalize().group(src_ext)
src_shard_extents = [s.extent for s in grouped_src.shard]
grouped_dst, dst_seps = sliced_dst.canonicalize().group(src_shard_extents)
# Find contiguous regions in both layouts
src_contig_indices, _ = find_contiguous_region(grouped_src)
dst_contig_indices, _ = find_contiguous_region(grouped_dst)
# Intersect: walk from innermost outward, include only matching shard indices
shared_contig_indices = []
for s_idx, d_idx in zip(src_contig_indices, dst_contig_indices):
if s_idx != d_idx:
break
shared_contig_indices.append(s_idx)
# Compute chunk size
if shared_contig_indices:
chunk_elements = functools.reduce(
operator.mul, [grouped_src.shard[i].extent for i in shared_contig_indices], 1
)
else:
chunk_elements = 1
chunk_bytes = chunk_elements * dtype_bytes
if chunk_bytes < 16 or chunk_bytes % 16 != 0:
fail(
f"Layouts not compatible for bulk DSMEM copy: "
f"chunk_bytes={chunk_bytes} (need >= 16 and multiple of 16)"
)
# Build iteration space over non-contiguous (outer) shards
shared_contig_set = set(shared_contig_indices)
outer_shard_indices = [i for i in range(len(grouped_src.shard)) if i not in shared_contig_set]
outer_extents = [grouped_src.shard[i].extent for i in outer_shard_indices]
outer_src_strides = [grouped_src.shard[i].stride for i in outer_shard_indices]
outer_dst_strides = [grouped_dst.shard[i].stride for i in outer_shard_indices]
# Helper to compute element offsets from loop variables (called via T.meta_var)
def compute_offsets(loop_vars):
if len(outer_extents) == 1:
lvs = [loop_vars]
else:
lvs = list(loop_vars)
src_off = 0
dst_off = 0
for j, v in enumerate(lvs):
src_off = src_off + v * outer_src_strides[j]
dst_off = dst_off + v * outer_dst_strides[j]
return src_off, dst_off
src_tile = to_tile_layout(src_buf.layout, src_buf.shape)
dst_tile = to_tile_layout(dst_buf.layout, dst_buf.shape)
# fmt: off
@T.prim_func(check_well_formed=False)
def impl():
# Map mbar to remote CTA (complete_tx targets the destination's mbar)
remote_mbar = T.ptx.map_shared_rank(mbar, remote_cta_id)
if not outer_extents:
# Single contiguous chunk — no iteration needed
src_ptr = src_buf.ptr_to(src_st)
cluster_dst = T.ptx.map_shared_rank(dst_buf.ptr_to(dst_st), remote_cta_id)
T.ptx.cp_async.bulk.s2c(cluster_dst, src_ptr, chunk_bytes, remote_mbar)
else:
for loop_vars in T.grid(*outer_extents):
src_elem_offset, dst_elem_offset = T.meta_var(compute_offsets(loop_vars))
src_buf_w = T.decl_buffer(
src_buf.shape, src_buf.dtype, src_buf.data,
elem_offset=src_buf.elem_offset + src_elem_offset,
scope=src_buf.scope(),
layout=src_tile,
)
dst_buf_w = T.decl_buffer(
dst_buf.shape, dst_buf.dtype, dst_buf.data,
elem_offset=dst_buf.elem_offset + dst_elem_offset,
scope=dst_buf.scope(),
layout=dst_tile,
)
src_ptr = src_buf_w.ptr_to(src_st)
cluster_dst = T.ptx.map_shared_rank(dst_buf_w.ptr_to(dst_st), remote_cta_id)
T.ptx.cp_async.bulk.s2c(cluster_dst, src_ptr, chunk_bytes, remote_mbar)
# fmt: on
return impl
# === Variant: copy_async/dsmem (priority=10) ===
#
# When: valid async copy at single-thread scope where both src and dst are in
# shared memory. Used for intra-cluster DSMEM copies (shared::cta -> shared::cluster).
#
# Before (TilePrimitiveCall):
# Tx.copy_async(
# dst_smem[0:128, 0:64],
# src_smem[0:128, 0:64],
# config={"mbar": mbar, "remote_cta_id": cta_id}
# )
#
# After (emits cp.async.bulk.shared::cluster.shared::cta):
# cluster_dst = mapa(dst_smem.ptr, cta_id)
# cp.async.bulk.shared::cluster.shared::cta.mbarrier::complete_tx::bytes
# [cluster_dst], [src_smem.ptr], size, [mbar]
@register_dispatch(
"copy_async",
"cuda",
variant="dsmem",
priority=10,
when=[
predicate(
"validate_copy_op", lambda op, sctx: (validate_copy_op(op, sctx), "not a valid copy op")
),
predicate(
"single_thread",
lambda op, sctx: (
single_thread(op, sctx),
f"unsupported exec_scope {sctx.exec_scope}, expected single thread",
),
),
predicate(
"is_shared_to_shared",
lambda op, sctx: (_is_shared_to_shared(op), "not a shared-to-shared copy"),
),
],
)
def copy_async_dispatch_dsmem(op: TilePrimitiveCall, sctx: DispatchContext) -> PrimFunc:
return copy_dsmem_impl(op, sctx)
@@ -0,0 +1,275 @@
# 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.
"""``copy_async`` dispatch for ``global → shared`` via ``cp.async``
(SASS: ``LDGSTS``).
Shares the partition / layout-alignment algorithm with
``cuda/copy/gmem_smem.py`` (sync ``T.copy`` global ↔ shared); differs at
emit time only:
* direction: ``cp.async`` is global → shared only (hardware restriction).
* cp_size: PTX ``cp.async`` only accepts 4 / 8 / 16 bytes, so the vec-width
candidate set is restricted to ``{32, 64, 128}`` bits.
* emit: ``T.evaluate(T.ptx.cp_async(dst, src, cp_size))`` instead of the
synchronous ``T.cuda.copy_{vec_bits}b(dst, src)``.
Note: ``cp.async`` does **not** sync at emit time — caller is responsible
for ``commit_group`` / ``wait_group`` / ``cta_sync`` plumbing around the
async pipeline.
"""
from tvm.runtime import DataType
from tvm.script import tirx as T
from tvm.tirx import Buffer, PrimFunc
from tvm.tirx import Var as _TirVar
from tvm.tirx.expr import IntImm as _IntImm
from tvm.tirx.operator.tile_primitive.dispatcher import (
predicate,
register_dispatch,
)
from tvm.tirx.operator.tile_primitive.registry import DispatchContext
from tvm.tirx.stmt import TilePrimitiveCall
from ..copy._common import (
_TID_AXIS_FOR_SCOPE,
_thread_cnt,
align_layouts_gs,
)
from ..copy._swizzle_iter import (
emit_init,
emit_iter_offset,
get_swizzle,
try_recognize,
)
from ..copy.reg import _all_threads_active, _axis_decl, _ptr_off
from ..copy.utils import _is_valid_copy, _scope_allowed
# cp.async is unidirectional: global → shared.
_LDGSTS_PAIRS = [("global", "shared*")]
# cp.async cp_size ∈ {4, 8, 16} bytes ⇒ vec_bits ∈ {32, 64, 128}.
_LDGSTS_VEC_BITS = (128, 64, 32)
def _divides_thread_cnt_ldgsts(
op_call: TilePrimitiveCall, sctx: DispatchContext
) -> tuple[bool, str | None]:
"""Mirror of ``gmem_smem._divides_thread_cnt``: reject copies whose
region element count doesn't divide ``thread_cnt`` (and reject
``thread_cnt=0`` scopes outright). See that docstring for rationale."""
op_call = TilePrimitiveCall.downcast(op_call)
thread_cnt = _thread_cnt(sctx)
if thread_cnt <= 0:
return False, f"degenerate thread_cnt={thread_cnt} (scope has empty intra)"
g_br = op_call.src if op_call.src.buffer.scope() == "global" else op_call.dst
n_elements = 1
for r in g_br.region:
ext = r.extent
try:
n_elements *= int(ext)
except (TypeError, ValueError):
return False, f"non-constant region extent {ext}"
if n_elements % thread_cnt != 0:
return False, (f"region size {n_elements} not divisible by thread_cnt={thread_cnt}")
return True, None
def _is_ldgsts(op_call: TilePrimitiveCall, sctx: DispatchContext) -> tuple[bool, str | None]:
if not sctx.is_target("cuda"):
return False, "non-cuda target"
if sctx.scope_kind not in ("thread", "warp", "warpgroup", "cta"):
return False, f"unsupported exec_scope {sctx.scope_kind}"
for check in (
lambda: _all_threads_active(sctx),
lambda: _is_valid_copy(op_call, sctx),
lambda: _scope_allowed(op_call, sctx, allowed_pairs=_LDGSTS_PAIRS),
lambda: _divides_thread_cnt_ldgsts(op_call, sctx),
):
ok, msg = check()
if not ok:
return False, msg
return True, None
def _emit_ldgsts(op_call: TilePrimitiveCall, sctx: DispatchContext) -> PrimFunc:
op_call = TilePrimitiveCall.downcast(op_call)
src: Buffer = op_call.src.buffer
dst: Buffer = op_call.dst.buffer
# Predicate above guarantees src is global, dst is shared.
g_buf, g_br = src, op_call.src
s_buf, s_br = dst, op_call.dst
g_region = [(r.min, r.min + r.extent) for r in g_br.region]
s_region = [(r.min, r.min + r.extent) for r in s_br.region]
elem_bits = DataType(src.dtype).bits
thread_cnt = _thread_cnt(sctx)
with sctx.target:
g_p, s_p, vec_len = align_layouts_gs(
g_buf.layout,
g_buf.shape,
g_region,
s_buf.layout,
s_buf.shape,
s_region,
elem_bits,
thread_cnt,
vec_bits_candidates=_LDGSTS_VEC_BITS,
)
vec_bits = vec_len * elem_bits
cp_size = vec_bits // 8 # cp.async cp_size is in bytes
if cp_size not in (4, 8, 16):
# align_layouts_gs already restricted candidates to _LDGSTS_VEC_BITS,
# so reaching here means no candidate worked at all.
from tvm.tirx.operator.tile_primitive.dispatcher import fail
fail(f"ldgsts: cannot find a cp.async-compatible vec_len for elem_bits={elem_bits}")
# Mirror gmem_smem.py: build 3D `(f, tid, 0)` against
# `[total_outer, thread_cnt, vec_len]` and let `s_p.apply(coord, shape)`
# flatten + resplit into whatever multi-iter T / outer-iter structure
# `align_layouts_gs` picked. Emit is oblivious to how many shard iters
# cover T.
n_elements = 1
for it in s_p.shard:
n_elements *= int(it.extent)
assert n_elements % (thread_cnt * vec_len) == 0, (
f"partition produced {n_elements} elements but thread_cnt({thread_cnt}) * "
f"vec_len({vec_len}) = {thread_cnt * vec_len} doesn't divide it"
)
total_outer = n_elements // (thread_cnt * vec_len)
apply_shape = [
_IntImm("int32", total_outer),
_IntImm("int32", thread_cnt),
_IntImm("int32", vec_len),
]
s_zero = [0] * len(s_buf.shape)
g_zero = [0] * len(g_buf.shape)
tid_axis_name = _TID_AXIS_FOR_SCOPE[sctx.scope_kind] if thread_cnt > 1 else None
# T-iters-walk-back to recover outer_iters_s for the fast-path
# recognizer. Same trick as gmem_smem.py.
if thread_cnt > 1:
acc, _i = 1, len(s_p.shard) - 2
while _i >= 0 and acc < thread_cnt:
_ext = int(s_p.shard[_i].extent)
if acc * _ext > thread_cnt:
break
acc *= _ext
_i -= 1
outer_iters_s = list(s_p.shard[: _i + 1]) if acc == thread_cnt else []
else:
outer_iters_s = list(s_p.shard[:-1])
swizzle = get_swizzle(s_buf.layout)
swizzle_pattern = None
if swizzle is not None and outer_iters_s:
if tid_axis_name is not None:
_tid_placeholder = _TirVar(tid_axis_name, "int32")
else:
_tid_placeholder = _IntImm("int32", 0)
s_off_template = s_p.apply(
_IntImm("int32", 0),
_tid_placeholder,
_IntImm("int32", 0),
shape=apply_shape,
)["m"]
swizzle_pattern = try_recognize(
swizzle,
[int(it.extent) for it in outer_iters_s],
[int(it.stride) for it in outer_iters_s],
s_off_template,
)
class _SwizzleState:
def __init__(self):
self.signed_strides = None
self.base_off = None
state = _SwizzleState()
def _decl_tid():
if tid_axis_name is not None:
return _axis_decl(tid_axis_name, sctx)
return _IntImm("int32", 0)
def _setup_swizzle(tid):
if swizzle_pattern is None:
return
s_off_resolved = s_p.apply(
_IntImm("int32", 0),
tid,
_IntImm("int32", 0),
shape=apply_shape,
)["m"]
state.signed_strides, state.base_off = emit_init(
swizzle_pattern,
s_off_resolved,
)
if swizzle_pattern is not None:
def _s_off(f, s_lin):
return emit_iter_offset(
swizzle_pattern,
state.signed_strides,
state.base_off,
f,
)
elif swizzle is not None:
_sw = swizzle
def _s_off(f, s_lin):
return _sw.apply(s_lin)["m"]
else:
def _s_off(f, s_lin):
return s_lin
v0 = _IntImm("int32", 0)
# fmt: off
@T.prim_func(check_well_formed=False)
def impl():
tid = _decl_tid()
_setup_swizzle(tid)
for f in T.unroll(total_outer):
s_lin = s_p.apply(f, tid, v0, shape=apply_shape)["m"]
g_lin = g_p.apply(f, tid, v0, shape=apply_shape)["m"]
s_off = _s_off(f, s_lin)
s_ptr = _ptr_off(s_buf.ptr_to(s_zero), s_off)
g_ptr = _ptr_off(g_buf.ptr_to(g_zero), g_lin)
T.evaluate(T.ptx.cp_async(s_ptr, g_ptr, cp_size))
# cp.async is caller-synced — no cta_sync here (commit_group /
# wait_group / cta_sync are the caller's responsibility).
# fmt: on
return impl
@register_dispatch(
"copy_async",
"cuda",
variant="ldgsts",
priority=20,
when=[predicate("ldgsts_applicable", _is_ldgsts)],
)
def copy_schedule_ldgsts(op_call: TilePrimitiveCall, sctx: DispatchContext) -> PrimFunc:
return _emit_ldgsts(op_call, sctx)
@@ -0,0 +1,485 @@
# 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.
"""smem->tmem dispatch via tcgen05.cp.32x128b.warpx4.
``tcgen05.cp`` is inherently async; this dispatch emits the cp loop only and
leaves completion signaling (``tcgen05.commit`` against a barrier) to the
caller. Callers who want sync semantics should issue ``tcgen05.commit``
themselves after the copy.
Algorithm
---------
Given ``Tx.copy_async(t_region, s_region)`` where t is in tmem (with
R[4:32@TLane] indicating warpx4 broadcast), and s is in shared memory:
A. Slice + canonicalize both layouts at the given regions.
B. Verify ``t.replica == [4:32@TLane]`` (warpx4 router).
C. Compute permutation that puts TLane first, then TCol stride-descending;
apply to t.permute_dims and to s via group + permute_by_groups.
D. Canonicalize again.
E. Isolate broadcast: split-by-stride-zero on both t and s; their split
sequences must match (same distinct prefix prods + broadcast extents).
Drop stride-0 iters → ``t_iso`` and ``s_iso``.
F. Group both into ``(32, middle, elem_per_128b)``. Validate:
- t_lane = (32, 1@TLane)
- t_col = (elem_per_128b, 1@TCol)
- s_col = (elem_per_128b, 1)
- s_lane refines into (4, 8) on m axis with strides (SDO_stride, atom_K_stride)
- atom_K_byte ∈ {16, 32, 64, 128} → swizzle_mode 0..3
- swizzle_mode matches s_buf.layout's SwizzleLayout (if any)
G. Alignment checks:
- t_iso TCol offset ≡ 0 (mod 32-bit)
- s_iso m offset ≡ 0 (mod 16B for sw=0; mod atom_size for sw>0)
- middle iter strides 16B-aligned
H. middle 1-1 correspondence (simple-mode): t_middle and s_middle have same
iter count and matching extents per position.
I. Emit:
- SmemDescriptor encoded once at SMEM base (hoisted via post_buffer_def_stmt).
- Loop over middle iters; each cp uses ``desc.add_16B_offset(init + loop)``
and writes to ``tmem_addr + t_col0 + Σ i_j * t_step_j``.
"""
import functools
import operator
import tvm
from tvm.arith import Analyzer
from tvm.runtime import DataType
from tvm.script import tirx as T
from tvm.tirx import Buffer, PrimFunc
from tvm.tirx.layout import ComposeLayout, SwizzleLayout, TCol, TileLayout, TLane
from tvm.tirx.layout import m as m_axis
from tvm.tirx.operator.tile_primitive import DispatchContext, predicate, register_dispatch
from tvm.tirx.stmt import AllocBuffer, Evaluate, SeqStmt, TilePrimitiveCall
from ..copy import _is_valid_smem_tmem_copy, _single_thread_exec
# -----------------------------------------------------------------------------
# Helpers
# -----------------------------------------------------------------------------
def _compute_perm(t):
def key(p):
it = p[1]
return (0 if it.axis == TLane else 1, -int(it.stride))
return [i for i, _ in sorted(enumerate(t.shard), key=key)]
def _split_by_zero(lay):
"""Split lay.shard into segments at stride==0 positions.
Returns (split_seq, kept_iters_with_nonzero_stride)."""
new_seq = []
keep = []
cur = 1
for it in lay.shard:
e, st = int(it.extent), int(it.stride)
if st == 0:
if cur > 1:
new_seq.append(cur)
new_seq.append(e)
cur = 1
else:
cur *= e
keep.append(it)
if cur > 1:
new_seq.append(cur)
return new_seq, keep
def _align_middles(t_middle, s_middle):
"""Sub-group both middles by union-of-boundaries so they become 1-1.
Both inputs must be post-canonicalize iter lists with equal extent products.
The shape is the consecutive ratios of sorted(B_t U B_s) where B_x is the
set of cumulative extent boundaries of x_middle. Each segment then contains
at most one iter per side (whole or sub-divided), so trivially single iter.
Returns (new_t_middle, new_s_middle) with len() == len() == k segments,
each segment a single Iter on each side.
"""
def cum_bounds(iters):
b, p = [], 1
for it in iters:
p *= int(it.extent)
b.append(p)
return b
t_bounds = cum_bounds(t_middle)
s_bounds = cum_bounds(s_middle)
if not t_bounds and not s_bounds:
return t_middle, s_middle
N = t_bounds[-1] if t_bounds else s_bounds[-1]
if (s_bounds and s_bounds[-1] != N) or (t_bounds and t_bounds[-1] != N):
raise ValueError(f"middle extent mismatch: t={N} s={s_bounds[-1] if s_bounds else 0}")
cuts = sorted(set(t_bounds) | set(s_bounds))
shape, prev = [], 1
for c in cuts:
if c % prev != 0:
raise ValueError(
f"middle align failed: cut {c} not divisible by prev cut {prev} "
f"(t_bounds={t_bounds}, s_bounds={s_bounds})"
)
shape.append(c // prev)
prev = c
def subgroup(iters):
if len(iters) == 1 and shape == [int(iters[0].extent)]:
return iters
lay, _seps = TileLayout.from_iters(iters, [], {}).group(shape)
seps = list(_seps)
out = []
for i in range(len(shape)):
seg = list(lay.shard[seps[i] : seps[i + 1]])
seg_canon = list(TileLayout.from_iters(seg, [], {}).canonicalize().shard)
if len(seg_canon) != 1:
raise ValueError(
f"middle sub-group seg[{i}] not single iter after canon: {seg_canon}"
)
out.append(seg_canon[0])
return out
return subgroup(t_middle), subgroup(s_middle)
# -----------------------------------------------------------------------------
# Plan (state object)
# -----------------------------------------------------------------------------
def _build_plan(op_call: TilePrimitiveCall, sctx: DispatchContext):
"""Run A..H and return a dispatch plan.
Plan fields:
- s_buf, t_buf
- dtype, dtype_bits
- elem_per_128b, elem_per_32b
- SmemSwizzleMode (int)
- SDO_field, atom_K_byte
- middle_iters: list of (extent, s_step_16B, t_step_32bcol)
- init_off_16B (Expr)
- t_col0 (Expr, TMEM 32-bit col offset for cp's first call)
"""
op_call = TilePrimitiveCall.downcast(op_call)
dst_region, src_region = op_call.args[:2]
s_buf: Buffer = src_region.buffer
t_buf: Buffer = dst_region.buffer
dtype = s_buf.dtype
dtype_bits = DataType(dtype).bits
elem_per_128b = 128 // dtype_bits
elem_per_32b = 32 // dtype_bits
# C: slice + canonicalize.
s_region = [(r.min, r.min + r.extent) for r in src_region.region]
t_region = [(r.min, r.min + r.extent) for r in dst_region.region]
s = s_buf.layout.slice(list(s_buf.shape), s_region).canonicalize()
t = t_buf.layout.slice(list(t_buf.shape), t_region).canonicalize()
# If s is ComposeLayout (SwizzleLayout∘TileLayout), peel off the swizzle
# for stride analysis; record swizzle_len for cross-check.
s_swizzle_mode_from_layout = 0
if isinstance(s, ComposeLayout):
s_swizzle_mode_from_layout = int(s.swizzle.swizzle_len)
s = s.tile_layout
elif isinstance(s, SwizzleLayout):
raise ValueError("s slice produced bare SwizzleLayout (unexpected)")
# B: warpx4 router check.
rep = t.replica
if not (
len(rep) == 1
and int(rep[0].extent) == 4
and int(rep[0].stride) == 32
and rep[0].axis == TLane
):
raise ValueError(
f"warpx4 router fail: t.replica = "
f"{[(int(r.extent), int(r.stride), str(r.axis)) for r in rep]}"
)
# C: permute (TLane first, TCol stride desc).
perm = _compute_perm(t)
t_shape_for_group = [int(it.extent) for it in t.shard]
s_grp, seps = s.group(t_shape_for_group)
s_p = s_grp.permute_by_groups(list(seps), perm).canonicalize()
t_p = t.permute_dims(perm).canonicalize()
# E: isolate broadcast.
seq_t, keep_t = _split_by_zero(t_p)
seq_s, keep_s = _split_by_zero(s_p)
if seq_t != seq_s:
raise ValueError(f"isolate split mismatch: t={seq_t} s={seq_s}")
s_iso = TileLayout.from_iters(keep_s, list(s_p.replica), dict(s_p.offset))
t_iso = TileLayout.from_iters(keep_t, list(t_p.replica), dict(t_p.offset))
# F: group into (32, middle, elem_per_128b).
def shard_prod(lay):
return functools.reduce(operator.mul, [int(it.extent) for it in lay.shard], 1)
n_lane, n_col = 32, elem_per_128b
n_mid_t = shard_prod(t_iso) // (n_lane * n_col)
n_mid_s = shard_prod(s_iso) // (n_lane * n_col)
t_grp, t_seps = t_iso.group([n_lane, n_mid_t, n_col])
s_grp2, s_seps = s_iso.group([n_lane, n_mid_s, n_col])
t_seps = list(t_seps)
s_seps = list(s_seps)
def _canon_segment(iters):
return TileLayout.from_iters(iters, [], {}).canonicalize().shard
t_lane = list(_canon_segment(list(t_grp.shard[t_seps[0] : t_seps[1]])))
t_middle = list(_canon_segment(list(t_grp.shard[t_seps[1] : t_seps[2]])))
t_col = list(_canon_segment(list(t_grp.shard[t_seps[2] : t_seps[3]])))
s_lane = list(s_grp2.shard[s_seps[0] : s_seps[1]])
s_middle = list(_canon_segment(list(s_grp2.shard[s_seps[1] : s_seps[2]])))
s_col = list(_canon_segment(list(s_grp2.shard[s_seps[2] : s_seps[3]])))
# F.5: align middles via union-cut sub-grouping. Both t_middle and s_middle
# are post-canonicalize. To make their structure 1-1 we sub-group both by
# the union of their internal cumulative-extent boundaries.
t_middle, s_middle = _align_middles(t_middle, s_middle)
# F.1: lane / col validation.
if len(t_lane) != 1:
raise ValueError(f"t_lane must canonicalize to single iter, got {t_lane}")
if len(t_col) != 1:
raise ValueError(f"t_col must canonicalize to single iter, got {t_col}")
if len(s_col) != 1:
raise ValueError(f"s_col must canonicalize to single iter, got {s_col}")
li = t_lane[0]
if not (int(li.extent) == 32 and int(li.stride) == 1 and li.axis == TLane):
raise ValueError(f"t_lane must be (32, 1@TLane), got {li}")
ci = t_col[0]
if not (int(ci.extent) == elem_per_128b and int(ci.stride) == 1 and ci.axis == TCol):
raise ValueError(f"t_col must be ({elem_per_128b}, 1@TCol), got {ci}")
sci = s_col[0]
if not (int(sci.extent) == elem_per_128b and int(sci.stride) == 1):
raise ValueError(f"s_col must be ({elem_per_128b}, 1, m), got {sci}")
# F.2: s_lane → group (4, 8) → (SDO_stride, atom_K_stride)
s_lane_layout = TileLayout.from_iters(s_lane, [], {})
s_lane_grp, s_lane_seps = s_lane_layout.group([4, 8])
s_lane_seps = list(s_lane_seps)
blk_4 = list(s_lane_grp.shard[s_lane_seps[0] : s_lane_seps[1]])
blk_8 = list(s_lane_grp.shard[s_lane_seps[1] : s_lane_seps[2]])
if len(blk_4) != 1 or len(blk_8) != 1:
raise ValueError(
f"s_lane must group into single iter per block: blk_4={blk_4}, blk_8={blk_8}"
)
SDO_byte = int(blk_4[0].stride) * dtype_bits // 8
atom_K_byte = int(blk_8[0].stride) * dtype_bits // 8
sw_candidates = {16: 0, 32: 1, 64: 2, 128: 3}
if atom_K_byte not in sw_candidates:
raise ValueError(f"atom_K_byte {atom_K_byte} not in {{16,32,64,128}}")
derived_sw = sw_candidates[atom_K_byte]
if s_swizzle_mode_from_layout != derived_sw:
raise ValueError(
f"swizzle mode mismatch: s_layout swizzle_len="
f"{s_swizzle_mode_from_layout} but atom_K_byte={atom_K_byte} "
f"implies sw={derived_sw}"
)
analyzer = Analyzer()
# G: alignments.
# G.1: t_iso TCol offset ≡ 0 (mod 32-bit element count).
t_col_offset_expr = 0
for ax, val in t_iso.offset.items():
if ax == TCol:
t_col_offset_expr = val
break
if not analyzer.can_prove_equal(t_col_offset_expr % elem_per_32b, 0):
raise ValueError(f"t TCol offset {t_col_offset_expr} not provably 32b-aligned")
# G.2: s_iso m offset alignment.
s_m_offset_expr = 0
for ax, val in s_iso.offset.items():
if ax == m_axis:
s_m_offset_expr = val
break
elem_per_16B = 16 * 8 // dtype_bits
if derived_sw == 0:
align_elem = elem_per_16B
align_label = "16B"
else:
atom_size_byte = 8 * atom_K_byte
align_elem = atom_size_byte * 8 // dtype_bits
align_label = f"atom={atom_size_byte}B"
if not analyzer.can_prove_equal(s_m_offset_expr % align_elem, 0):
raise ValueError(
f"s offset {s_m_offset_expr} not provably aligned to {align_label} "
f"({align_elem} {dtype} elements)"
)
# H: middle 1-1 correspondence.
if len(t_middle) != len(s_middle):
raise ValueError(
f"t_middle iter count {len(t_middle)} != s_middle {len(s_middle)} "
"(simple-mode requires 1-1)"
)
middle_iters = []
for i, (ti, si) in enumerate(zip(t_middle, s_middle)):
if int(ti.extent) != int(si.extent):
raise ValueError(f"middle[{i}] extent: t={int(ti.extent)} s={int(si.extent)}")
n = int(ti.extent)
if n == 1:
continue
if ti.axis != TCol:
raise ValueError(f"middle[{i}] t axis must be TCol, got {ti.axis}")
s_stride_byte = int(si.stride) * dtype_bits // 8
if s_stride_byte % 16 != 0:
raise ValueError(f"s_middle[{i}] stride {s_stride_byte}B not 16B-aligned")
middle_iters.append((n, s_stride_byte // 16, int(ti.stride) // elem_per_32b))
SDO_field = SDO_byte // 16
init_off_16B = s_m_offset_expr * dtype_bits // 8 // 16
t_col0 = t_col_offset_expr // elem_per_32b
return {
"s_buf": s_buf,
"t_buf": t_buf,
"dtype": dtype,
"dtype_bits": dtype_bits,
"elem_per_128b": elem_per_128b,
"elem_per_32b": elem_per_32b,
"swizzle_mode": derived_sw,
"SDO_field": SDO_field,
"atom_K_byte": atom_K_byte,
"middle_iters": middle_iters,
"init_off_16B": init_off_16B,
"t_col0": t_col0,
}
# -----------------------------------------------------------------------------
# Descriptor caching: one (smem_buf, ldo, sdo, swizzle) → one desc_buf,
# encoded once at SMEM base, hoisted to right after SMEM alloc via
# add_post_buffer_def_stmt.
# -----------------------------------------------------------------------------
def _get_or_create_desc(sctx, s_buf, ldo, sdo, swizzle):
# Cache descriptor template at SMEM 0; patch addr per cp.
cache_key = f"smem_tmem_desc:{int(ldo)}:{int(sdo)}:{int(swizzle)}"
cached = sctx.cache_get(cache_key)
if cached is not None:
return cached
desc_buf = tvm.tirx.decl_buffer((1,), "uint64", name="cp_desc", scope="local")
encode_call = T.ptx.tcgen05.encode_matrix_descriptor(
desc_buf.data, T.reinterpret("handle", T.uint64(0)), ldo, sdo, swizzle
)
wrap = SeqStmt([AllocBuffer(desc_buf), Evaluate(encode_call)])
sctx.add_post_buffer_def_stmt(s_buf, wrap)
sctx.cache_set(cache_key, desc_buf)
return desc_buf
def _desc_set_addr(desc_val, addr_ptr):
"""Patch a SMEM matrix descriptor's 14-bit address field with cvta(addr)>>4 —
matches the hand-rolled ``replace_smem_desc_addr`` (descriptor encoded at 0)."""
start_addr = T.cast(
T.bitwise_and(
T.shift_right(T.cuda.cvta_generic_to_shared(addr_ptr), T.uint32(4)),
T.uint32(0x3FFF),
),
"uint64",
)
return T.bitwise_or(T.bitwise_and(desc_val, T.bitwise_not(T.uint64(0x3FFF))), start_addr)
# -----------------------------------------------------------------------------
# Core impl: emits the cp loop given a plan + cp config. Async only — caller
# is responsible for issuing ``tcgen05.commit`` against a barrier if they
# need synchronization.
# -----------------------------------------------------------------------------
def copy_smem_tmem_impl(op_call: TilePrimitiveCall, sctx: DispatchContext) -> PrimFunc | None:
plan = _build_plan(op_call, sctx)
s_buf = plan["s_buf"]
t_buf = plan["t_buf"]
SDO_field = plan["SDO_field"]
sw = plan["swizzle_mode"]
middle_iters = plan["middle_iters"]
init_off_16B = plan["init_off_16B"]
t_col0 = plan["t_col0"]
# cp ignores LDO for data; non-zero LDO bloats the addr-patch codegen.
LDO_field = 0
cta_group = op_call.config.get("cta_group", 1)
desc_buf = _get_or_create_desc(sctx, s_buf, LDO_field, SDO_field, sw)
t_addr = t_buf.allocated_addr
s_rank = len(s_buf.shape)
def _cp_desc(off_16B):
addr = T.ptr_byte_offset(s_buf.ptr_to([0] * s_rank), off_16B * 16, s_buf.dtype)
return _desc_set_addr(desc_buf[0], addr)
# Flatten the N-D middle iteration into a single T.unroll. Each iteration's
# per-dim index is (flat // stride) % extent, summed into the t/s offsets.
# Works uniformly for n_mid ∈ {0, 1, 2, ...}; total == 1 (no middle dims) is
# special-cased to avoid a degenerate T.unroll(1).
total = functools.reduce(operator.mul, [n for n, _, _ in middle_iters], 1)
# fmt: off
if total == 1:
@T.prim_func(check_well_formed=False)
def impl():
T.ptx.tcgen05.cp(
t_addr[0] + t_col0,
_cp_desc(init_off_16B),
shape="32x128b", cta_group=cta_group, multicast="warpx4",
)
else:
def compute_offsets(flat):
t_off = 0
s_off = 0
div = 1
for n, s_step, t_step in middle_iters:
idx = (flat // div) % n
div = div * n
t_off = t_off + idx * t_step
s_off = s_off + idx * s_step
return t_off, s_off
@T.prim_func(check_well_formed=False)
def impl():
for flat in T.unroll(total):
t_off, s_off = T.meta_var(compute_offsets(flat))
T.ptx.tcgen05.cp(
t_addr[0] + t_col0 + t_off,
_cp_desc(init_off_16B + s_off),
shape="32x128b", cta_group=cta_group, multicast="warpx4",
)
# fmt: on
return impl
# === Variant: copy_async/smem->tmem (priority=10) ===
@register_dispatch(
"copy_async",
"cuda",
variant="smem->tmem",
priority=10,
when=[
predicate("validate_smem_tmem_copy", _is_valid_smem_tmem_copy),
predicate("exec_scope", _single_thread_exec),
],
)
def copy_async_schedule_smem_tmem(op_call: TilePrimitiveCall, sctx: DispatchContext) -> PrimFunc:
return copy_smem_tmem_impl(op_call, sctx)
@@ -0,0 +1,458 @@
# 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.
"""copy_async dispatch: ``tcgen05.ld`` / ``tcgen05.st`` (tmem <-> local registers).
Both are inherently async; this dispatch emits the PTX instruction only and
leaves completion (``tcgen05.wait.ld`` / ``tcgen05.wait.st``) to the caller.
Callers that want sync semantics should issue the matching wait after the copy.
"""
import tvm
from tvm.arith import Analyzer
from tvm.runtime import DataType
from tvm.script import tirx as T
from tvm.tirx import Buffer, PrimFunc
from tvm.tirx.layout import (
S,
TCol,
TileLayout,
TLane,
tcgen05_atom_layout,
tid_in_wg,
tmem_datapath_layout,
)
from tvm.tirx.operator.tile_primitive import DispatchContext, predicate, register_dispatch
from tvm.tirx.stmt import TilePrimitiveCall
from ..common import get_st_extent
from ..copy import _is_valid_copy, _scope_allowed
from ..exec_scope_utils import exec_scope_ok
# Per-warp fp32-column factor for each instr_shape (mirrors
# ``_TCGEN05_COL_FACTOR_FP32`` in ``tvm.tirx.layout``; .16x64b → 2,
# .16x128b → 4, .16x256b → 8). Source: PTX ISA Table 49.
_TCGEN05_COL_FACTOR_FP32 = {"16x64b": 2, "16x128b": 4, "16x256b": 8}
def _match_tcgen05_atom_layout(buf):
"""Return ``(instr_shape, rep, frag_rows)`` if ``buf.layout`` matches a
tcgen05 ``.16x*b`` atom layout for some supported ``instr_shape``.
The local buffer shape ``(frag_rows, K)`` (``frag_rows`` ∈ {64, 128})
together with the dtype determines the candidate ``rep`` for each
``instr_shape``; we just probe the three shapes x two frag_rows and
structurally compare. ``None`` if no atom layout matches.
"""
if len(buf.shape) != 2:
return None
rows, cols = int(buf.shape[0]), int(buf.shape[1])
if rows not in (64, 128):
return None
dtype = buf.dtype
layout_c = buf.layout.canonicalize()
for shape in _TCGEN05_COL_FACTOR_FP32:
try:
cand = tcgen05_atom_layout(shape, (rows, cols), dtype).canonicalize()
except ValueError:
continue
try:
tvm.ir.assert_structural_equal(layout_c, cand)
except (AssertionError, ValueError):
continue
# Recover rep from cols (same arithmetic the factory uses).
elem_per_32b = 32 // DataType(dtype).bits
rep = cols // (_TCGEN05_COL_FACTOR_FP32[shape] * elem_per_32b)
return shape, rep, rows
return None
def _classify_tmem_datapath(tmem_buf):
"""Return ``"D"`` / ``"F"`` if ``tmem_buf.layout`` matches a known tcgen05
datapath (PTX ISA §9.7.16.10.5), else ``None``.
Layout D (M=128, identity row→lane) is the default returned by
``_default_tmem_layout``. Layout F (M=64 non-``.ws``, scattered) is the
explicit opt-in produced by ``tmem_pool.alloc(..., datapath="F")``.
The dispatch uses this to pair each ``.16x*b`` / ``.32x32b`` atom with a
compatible layout — see ``_check_tmem_layout_for_atom``.
"""
if tmem_buf.layout is None:
return None
buf_layout = tmem_buf.layout.canonicalize()
rows = int(tmem_buf.shape[0])
if rows == 128:
cand = tmem_datapath_layout("D", 128, tmem_buf.shape[1]).canonicalize()
try:
tvm.ir.assert_structural_equal(buf_layout, cand)
return "D"
except (AssertionError, ValueError):
return None
if rows == 64:
cand = tmem_datapath_layout("F", 64, tmem_buf.shape[1]).canonicalize()
try:
tvm.ir.assert_structural_equal(buf_layout, cand)
return "F"
except (AssertionError, ValueError):
return None
return None
# Compatibility matrix between the TMEM buffer's datapath layout and the
# tcgen05 ld/st atom requested by ``T.copy_async``:
#
# datapath x atom | accepted? | rationale
# ---------------------------- | --------- | --------------------------------
# D (M=128 full) x .32x32b | yes | full 128 lanes, all 32 per warp
# D (M=128 full) x .16x*b M=64| yes | reads first half-slab (lanes
# | | 0..15 of each warp partition)
# | | — the rest of acc is wasted
# | | for this atom but valid data
# D (M=128 full) x .16x*b M=128| yes | reads all 128 lanes via row=0
# | | and row=16 PTX issues
# F (M=64 scatter)x .16x*b M=64| yes | canonical pairing - F's row
# | | indexing matches the atom's
# | | scatter access
# F (M=64 scatter)x .16x*b M=128| no | F only writes the low slab; the
# | | high slab (row=16) is garbage
# F (M=64 scatter)x .32x32b | no | F only utilizes 16 of each
# | | warp's 32 lanes
_TMEM_ATOM_COMPAT = {
("D", "32x32b", 128): True,
("D", "16x*b", 64): True,
("D", "16x*b", 128): True,
("F", "32x32b", 128): False,
("F", "16x*b", 64): True,
("F", "16x*b", 128): False,
}
def _check_tmem_layout_for_atom(tmem_buf, atom_kind, frag_rows):
"""Raise ``ValueError`` if the TMEM buffer's datapath layout is
incompatible with the requested ``tcgen05`` atom.
``atom_kind`` is ``"32x32b"`` or ``"16x*b"``; ``frag_rows`` is the
register-side fragment row count (128 for ``.32x32b`` and ``.16x*b``
M=128 variants, 64 for ``.16x*b`` M=64). If the buffer's layout is
unrecognized (i.e. it isn't Layout D or Layout F), the dispatch falls
back to the structural assertions below.
"""
datapath = _classify_tmem_datapath(tmem_buf)
if datapath is None:
return None
allowed = _TMEM_ATOM_COMPAT.get((datapath, atom_kind, frag_rows), False)
if not allowed:
raise ValueError(
f"tcgen05 dispatch: TMEM buffer with datapath={datapath!r} is "
f"incompatible with atom={atom_kind!r} (frag_rows={frag_rows}). "
f"See PTX ISA §9.7.16.10.5 for datapath/atom pairings; the "
f"buffer was allocated via tmem_pool.alloc(..., "
f"datapath={datapath!r})."
)
return datapath
def copy_tmem_local_impl(op_call: TilePrimitiveCall, sctx: DispatchContext) -> PrimFunc | None:
op_call = TilePrimitiveCall.downcast(op_call)
dst_buffer_region, src_buffer_region = op_call.dst, op_call.src
dst: Buffer = dst_buffer_region.buffer
src: Buffer = src_buffer_region.buffer
if src.scope() == "tmem" and dst.scope() == "local":
direction = "tmem2local"
tmem_region, local_region = src_buffer_region, dst_buffer_region
elif src.scope() == "local" and dst.scope() == "tmem":
direction = "local2tmem"
local_region, tmem_region = src_buffer_region, dst_buffer_region
else:
raise ValueError(f"Unsupported src scope {src.scope()} and dst scope {dst.scope()}")
tmem_buf, local_buf = tmem_region.buffer, local_region.buffer
assert tmem_buf.layout is not None
assert local_buf.layout is not None
assert tmem_buf.dtype == local_buf.dtype
assert tmem_buf.allocated_addr is not None
analyzer = Analyzer()
elem_size = DataType(local_buf.dtype).bits
elem_per_32b = 32 // elem_size
assert len(local_buf.shape) == len(tmem_buf.shape) == 2
# Try the .16x* (M=64) path first by structural-matching the register-side
# layout against ``tcgen05_atom_layout(instr_shape, (64, K), dtype)``. The
# TMEM-side layout is the standard (128, W):(1@TLane, 1@TCol); the M=64
# fragment lives at lanes 0..15 of each warp's accessible slab (per PTX
# 9.7.16.8.1), so each warp issues with row_offset=0 and collectively the
# 4 warps cover all 64 rows.
atom_match = _match_tcgen05_atom_layout(local_buf)
if atom_match is not None:
shape, num, frag_rows = atom_match
return _emit_16xnb_path(
shape=shape,
num=num,
frag_rows=frag_rows,
direction=direction,
tmem_buf=tmem_buf,
local_buf=local_buf,
tmem_region=tmem_region,
local_region=local_region,
elem_per_32b=elem_per_32b,
analyzer=analyzer,
)
# Fall through to the existing .32x32b (M=128) path.
return _emit_32x32b_path(
direction=direction,
tmem_buf=tmem_buf,
local_buf=local_buf,
tmem_region=tmem_region,
local_region=local_region,
elem_per_32b=elem_per_32b,
analyzer=analyzer,
)
def _emit_32x32b_path(
*, direction, tmem_buf, local_buf, tmem_region, local_region, elem_per_32b, analyzer
) -> PrimFunc:
"""Original M=128 fragment path using ``tcgen05.{ld,st}.32x32b.xN``."""
# local: 128xWIDTH <-> tmem: 128xSHAPE[1]
# ``.32x32b`` accesses 32 lanes per warp — the full warp partition — so
# the TMEM buffer must be Layout D (M=128 full datapath). Reject Layout F.
_check_tmem_layout_for_atom(tmem_buf, "32x32b", 128)
assert analyzer.can_prove_equal(local_buf.shape[0], 128)
assert analyzer.can_prove_equal(tmem_buf.shape[0], 128)
# Check width is valid for 32x32b, and determine num
width = local_region.region[1].extent
candidates = [1, 2, 4, 8, 16, 32, 64, 128]
if not analyzer.can_prove_equal(tvm.tirx.floormod(width, elem_per_32b), 0):
raise ValueError(f"Width {width} is not valid for tcgen05.ld/st with shape 32x32b")
num = None
for n in candidates:
if analyzer.can_prove_equal(tvm.tirx.floordiv(width, elem_per_32b), n):
num = n
break
else:
raise ValueError(f"Width {width} is not valid for tcgen05.ld/st with shape 32x32b")
tmem_st, tmem_extent = get_st_extent(tmem_region)
local_st, local_extent = get_st_extent(local_region)
# tmem layout (128, WIDTH):(1@TLane, 1@TCol)
tmem_layout = TileLayout(S[(128, tmem_buf.shape[1]) : (1 @ TLane, 1 @ TCol)]).canonicalize()
# local layout
TileLayout(S[(128, width) : (1 @ tid_in_wg, 1)]).canonicalize()
tvm.ir.assert_structural_equal(tmem_buf.layout.canonicalize(), tmem_layout)
# local: [0:128, 0:WIDTH] <-> tmem: [0:128, st:st+WIDTH]
assert analyzer.can_prove_equal(tmem_st[0], 0)
assert analyzer.can_prove_equal(tmem_extent[0], 128)
assert analyzer.can_prove_equal(local_st[0], 0)
assert analyzer.can_prove_equal(local_extent[0], 128)
offset = tmem_st[1]
assert analyzer.can_prove_equal(tvm.tirx.floormod(offset, elem_per_32b), 0)
offset_32b = tvm.tirx.floordiv(offset, elem_per_32b)
assert analyzer.can_prove_equal(tmem_extent[1], width), (
f"tmem_extent[1]: {tmem_extent[1]}, width: {width}"
)
# assert analyzer.can_prove_equal(local_st[1], 0)
assert analyzer.can_prove_equal(local_extent[1], width)
op = T.ptx.tcgen05.ld if direction == "tmem2local" else T.ptx.tcgen05.st
# fmt: off
@T.prim_func(check_well_formed=False)
def impl():
local_storage = local_buf.view(local_buf.shape[1] * elem_per_32b, layout=TileLayout(S[num * elem_per_32b])) # noqa: E501
local_32b = local_storage.view("uint32")
op(tmem_buf.allocated_addr[0], *[local_32b[local_st[1] // elem_per_32b+i] for i in range(num)], shape="32x32b", num=num, row=0, col=offset_32b) # noqa: E501
# fmt: on
return impl
def _emit_16xnb_path(
*,
shape,
num,
frag_rows,
direction,
tmem_buf,
local_buf,
tmem_region,
local_region,
elem_per_32b,
analyzer,
) -> PrimFunc:
"""``.16x*b`` fragment path using ``tcgen05.{ld,st}.<shape>.x<num>`` (one
of ``.16x64b``, ``.16x128b``, ``.16x256b``).
Each of the warpgroup's 4 warps issues the atom with ``row_offset=0`` to
cover lanes 0..15 of its 32-lane TMEM partition (one 16-row slab); the
four warps collectively span M=64 rows. When ``frag_rows == 128`` the
dispatch emits a second issue with ``row_offset=16`` to also cover lanes
16..31 of each warp's partition, doubling the fragment's row coverage to
M=128. The two atoms share the same column footprint; the layout factory
surfaces the combined per-thread register vector with the second slab's
regs in the high half of the m-axis (so the dispatch can split regs
contiguously between the two PTX calls).
"""
# Per-atom column footprint in fp32 columns:
# .16x64b → 2N .16x128b → 4N .16x256b → 8N
col_factor_fp32 = {"16x64b": 2, "16x128b": 4, "16x256b": 8}[shape]
# Per-thread register count per 16-row slab (in 32-bit units):
# .16x64b.xN → N .16x128b.xN → 2N .16x256b.xN → 4N
regs_per_thread_per_slab = {"16x64b": num, "16x128b": 2 * num, "16x256b": 4 * num}[shape]
n_slabs = frag_rows // 64 # 1 for M=64, 2 for M=128
assert n_slabs in (1, 2)
regs_per_thread = regs_per_thread_per_slab * n_slabs
# Logical column width that the local buffer view exposes (in element units).
width_elems = col_factor_fp32 * num * elem_per_32b
# Per-thread storage in element units (same total bits as the register vector).
per_thread_elems = regs_per_thread * elem_per_32b
# Local-side: shape (frag_rows, K_cols)
assert analyzer.can_prove_equal(local_buf.shape[0], frag_rows), (
f".16x*b path expects local_buf rows={frag_rows}, got {local_buf.shape[0]}"
)
assert analyzer.can_prove_equal(local_buf.shape[1], width_elems), (
f".16x*b path expects local_buf cols={width_elems}, got {local_buf.shape[1]}"
)
# TMEM-side: structurally classify the buffer's datapath (D or F) and
# reject incompatible pairings. The PTX is identical in either case (the
# warp partition rule and the atom's lane access pattern are baked into
# the hardware); the layout classification just keeps the buffer's
# logical row indexing in sync with the physical TMEM occupation.
datapath = _check_tmem_layout_for_atom(tmem_buf, "16x*b", frag_rows)
if datapath == "F":
# Layout F: buffer shape (64, W), scattered row→lane.
assert analyzer.can_prove_equal(tmem_buf.shape[0], 64), (
f".16x*b Layout F expects tmem_buf rows=64, got {tmem_buf.shape[0]}"
)
tmem_rows = 64
else:
# Layout D (or untagged legacy buffers): shape (128, W), identity.
# The legacy structural check below still fires for untagged buffers
# so we don't silently accept arbitrary layouts.
assert analyzer.can_prove_equal(tmem_buf.shape[0], 128), (
f".16x*b path expects tmem_buf rows=128, got {tmem_buf.shape[0]}"
)
if datapath is None:
tmem_layout = TileLayout(
S[(128, tmem_buf.shape[1]) : (1 @ TLane, 1 @ TCol)]
).canonicalize()
tvm.ir.assert_structural_equal(tmem_buf.layout.canonicalize(), tmem_layout)
tmem_rows = 128
tmem_st, tmem_extent = get_st_extent(tmem_region)
local_st, local_extent = get_st_extent(local_region)
# Rows must span the full frag. The COLUMN extent may be a sub-multiple of
# the atom's full width ``width_elems`` — i.e. a per-chunk column slice of a
# wider frag (e.g. an epilogue that loads one big (128, MMA_N) frag in
# EPI_TILE-wide chunks). The atom layout maps consecutive columns to
# consecutive registers within each slab, so a column slice occupies a
# contiguous register window; we emit ``num_eff`` (the slice's atom rep) at
# the slab base + the column's register offset. When the slice IS the full
# atom (the common case), num_eff == num and reg offset == 0 (no change).
assert analyzer.can_prove_equal(local_st[0], 0)
assert analyzer.can_prove_equal(local_extent[0], frag_rows)
assert analyzer.can_prove_equal(tmem_st[0], 0)
assert analyzer.can_prove_equal(tmem_extent[0], frag_rows)
# local and tmem column slices must match and divide the atom's full width.
assert analyzer.can_prove_equal(local_extent[1], tmem_extent[1])
slice_w = int(local_extent[1])
assert width_elems % slice_w == 0, f"slice width {slice_w} must divide atom width {width_elems}"
num_eff = num * slice_w // width_elems
regs_eff = regs_per_thread_per_slab * slice_w // width_elems
del tmem_rows # only used for the structural check above
col_off = tmem_st[1]
assert analyzer.can_prove_equal(tvm.tirx.floormod(col_off, elem_per_32b), 0)
col_off_32b = tvm.tirx.floordiv(col_off, elem_per_32b)
local_col_off = local_st[1]
assert analyzer.can_prove_equal(tvm.tirx.floormod(local_col_off, elem_per_32b), 0)
local_col_off_elems = local_col_off
is_load = direction == "tmem2local"
op = T.ptx.tcgen05.ld if is_load else T.ptx.tcgen05.st
# We intentionally do *not* emit ``.pack::16b`` / ``.unpack::16b`` for
# 16-bit dtypes. That qualifier would store one 16-bit element per 32-bit
# TMEM cell (LOW half only, HIGH half wasted) — fine for some CUTLASS
# epilogues but a 2x TMEM waste vs. the existing ``.32x32b`` convention,
# which packs two 16-bit elements per cell. By using the plain ``.b32``
# form we keep TMEM dense (2 elements per 32-bit cell); the per-thread
# register file holds two packed 16-bit values per 32-bit register, and
# the layout factory's iters describe that packing.
# fmt: off
@T.prim_func(check_well_formed=False)
def impl():
# Per-thread 1-D flat view of the local storage, then a uint32 view
# for the register-pointer arguments of the PTX builtin.
local_storage = local_buf.view(per_thread_elems, layout=TileLayout(S[per_thread_elems]))
local_32b = local_storage.view("uint32")
# Register offset of the column slice within each slab. The old
# ``local_col_off // elem_per_32b`` is only correct when the slice IS the
# full atom; in general consecutive columns advance registers at the rate
# (regs_per_thread_per_slab / width_elems). For a full-atom load the
# offset is 0 either way, so existing callers are unaffected.
local_reg_base = local_col_off_elems * regs_per_thread_per_slab // width_elems
for slab in range(n_slabs):
reg_base = slab * regs_per_thread_per_slab
op(
tmem_buf.allocated_addr[0],
*[local_32b[local_reg_base + reg_base + i] for i in range(regs_eff)],
shape=shape, num=num_eff, row=slab * 16, col=col_off_32b,
)
# fmt: on
return impl
# === Variant: copy_async/tmem<->local (priority=10) ===
#
# When: one buffer is in tmem (tensor memory, Blackwell SM100+) and the other
# is in local scope, at warpgroup exec scope.
#
# Emits: T.ptx.tcgen05.ld / T.ptx.tcgen05.st (async). The caller is
# responsible for issuing the matching ``T.ptx.tcgen05.wait.ld`` /
# ``T.ptx.tcgen05.wait.st`` when synchronization is required.
@register_dispatch(
"copy_async",
"cuda",
variant="tmem<->local",
priority=10,
when=[
predicate("validate_copy_op", _is_valid_copy),
predicate("exec_scope", exec_scope_ok, expected_scopes=["warpgroup"]),
predicate(
"storage_scope", _scope_allowed, allowed_pairs=[("tmem", "local"), ("local", "tmem")]
),
],
)
def copy_async_schedule_tmem_local_async(
op_call: TilePrimitiveCall, sctx: DispatchContext
) -> PrimFunc:
return copy_tmem_local_impl(op_call, sctx)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,78 @@
# 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.
"""Shared helpers for copy_async operator dispatch variants.
The TMA-specific lowering moved to ``tma.py``. What remains here are the tiny
layout helpers other variants (e.g. ``dsmem.py``) still import.
"""
from tvm.arith import Analyzer
from tvm.tirx.layout import ComposeLayout, Layout, S, SwizzleLayout, TileLayout
def find_contiguous_region(layout: TileLayout) -> tuple:
"""Return the maximal stride-1 contiguous memory-shard chain.
Starts from stride==1 and repeatedly picks the shard whose stride equals
the running product of extents, stopping when no shard matches. Returns
the maximal chain; callers that need a shorter prefix should take one
themselves (e.g. to satisfy TMA's rank<=5 or a per-path reduction step).
Stride/extent comparisons go through an ``Analyzer`` so symbolic strides
work.
"""
analyzer = Analyzer()
memory_shards = [
(i, s)
for i, s in enumerate(layout.shard)
if s.axis.is_memory() and not analyzer.can_prove_equal(s.extent, 1)
]
if not memory_shards:
return [], 1
contiguous_indices: list[int] = []
contiguous_extent = 1
expected_stride = 1
consumed: set[int] = set()
while True:
for idx, shard in memory_shards:
if idx in consumed:
continue
if analyzer.can_prove_equal(shard.stride, expected_stride):
consumed.add(idx)
contiguous_indices.append(idx)
contiguous_extent *= shard.extent
expected_stride = contiguous_extent
break
else:
break
if not contiguous_indices:
return [], 0
return contiguous_indices, contiguous_extent
def to_tile_layout(layout: Layout, shape: list[int]) -> TileLayout:
"""Normalize any layout kind to a TileLayout for pointer arithmetic."""
if isinstance(layout, ComposeLayout):
return layout.tile_layout
if isinstance(layout, SwizzleLayout):
return TileLayout(S[tuple(shape)])
return layout
@@ -0,0 +1,38 @@
# 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.
"""CUDA elementwise dispatch.
Split by storage scope to mirror ``cuda/copy/``:
reg.py — operands all in ``local`` (registers) → induced partition
smem.py — operands all in ``shared*`` → synthesized partition
Each op in ``ops.ALL_OPS`` is registered under both variants. Per-op packed
PTX/CUDA intrinsics live in ``vec_emit/`` (``binary_f32x2`` / ``cast_vec2``
/ ``fma_f32x2``) and are attached to the relevant ``OpSpec.vec_impls``.
"""
from .register import *
# Suppress submodule-attribute leakage. Without an explicit ``__all__`` here,
# ``from tvm.backend.cuda.operator.tile_primitive.elementwise import *`` (run by
# tile_primitive/__init__.py) re-exports the implicit submodule attributes
# (``ops``, ``reg``, ``smem``, ``vec_emit``) — and ``ops`` in particular
# shadows the top-level ``tile_primitive/ops.py`` (BinaryReduce / UnaryReduce
# / ...) when downstream code does ``from tile_primitive import ops``.
__all__: list[str] = []
@@ -0,0 +1,412 @@
# 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.
"""Shared layout / vec-selection / emit helpers for ``reg.py`` and ``smem.py``.
Borrows directly from ``cuda/copy/reg.py`` (induced partition) and
``cuda/copy/_common.py`` (synthesized partition), extended to N operands.
The dispatch split mirrors copy:
reg.py — all operands in ``local`` → partition induced by anchor's layout
smem.py — all operands in ``shared*`` → partition synthesized from ``sctx.intra``
"""
from __future__ import annotations
import functools
import operator
from tvm.arith.analyzer import Analyzer
from tvm.runtime import DataType
from tvm.script import tirx as T
from tvm.tirx import BufferRegion
from tvm.tirx.layout import Axis, Iter, TileLayout
from ..common import get_indices, get_st_extent
# Re-use copy's primitives (PR-640) — same algorithm, same scope_id machinery.
from ..copy._common import _TID_AXIS_FOR_SCOPE, _extract_tile, _thread_cnt
from ..copy.reg import _all_threads_active, _axis_decl, _compute_perm_r
# -----------------------------------------------------------------------------
# Plan helpers
# -----------------------------------------------------------------------------
def buffer_regions(plan) -> list[BufferRegion]:
"""All BufferRegion args (dst + buffer-region srcs), in plan order."""
out: list[BufferRegion] = [plan.dst]
for s in plan.srcs:
if s.buf_region is not None:
out.append(s.buf_region)
return out
def dtype_name(dtype) -> str:
dtype_obj = getattr(dtype, "dtype", None)
if dtype_obj is not None:
return str(dtype_obj)
return str(dtype)
def dtype_bits(dtype) -> int:
return DataType(dtype_name(dtype)).bits
def compute_dtype_of(plan) -> str:
"""Widest dtype in bits across dst + buffer/scalar srcs (dst breaks ties)."""
candidates = [dtype_name(plan.dst.buffer.dtype)]
for s in plan.srcs:
if s.buf_region is not None:
candidates.append(dtype_name(s.buf_region.buffer.dtype))
elif s.scalar is not None:
candidates.append(scalar_dtype(s.scalar))
widest = candidates[0]
widest_bits = DataType(widest).bits
for d in candidates[1:]:
b = DataType(d).bits
if b > widest_bits:
widest, widest_bits = d, b
return widest
def scalar_dtype(scalar) -> str:
dtype = getattr(scalar, "dtype", None)
if dtype is not None:
return str(dtype)
ty = getattr(scalar, "ty", None)
if ty is None and hasattr(scalar, "expr_ty"):
ty = scalar.expr_ty()
dtype = getattr(ty, "dtype", None)
if dtype is None:
raise AttributeError(f"{type(scalar).__name__} has no dtype-bearing PrimType")
return str(dtype)
def n_elements(buf_region: BufferRegion) -> int:
_, ext = get_st_extent(buf_region)
return functools.reduce(operator.mul, ext, 1)
# -----------------------------------------------------------------------------
# Anchor selection (reg.py)
# -----------------------------------------------------------------------------
def pick_anchor(plan) -> BufferRegion:
"""Anchor is always ``plan.dst`` — every operand must have a layout
(enforced by predicate); dst's layout drives iteration. No choice to make.
"""
return plan.dst
# -----------------------------------------------------------------------------
# Broadcast support (NumPy-style right-aligned, anchor = result shape)
# -----------------------------------------------------------------------------
def _tensor_shape_of(region) -> tuple[int, ...]:
"""Per-dim region extent (post-slice tensor shape, NOT layout shape).
Accepts either ``[(start, end), ...]`` pairs (as built locally from a
``BufferRegion``) or the ``BufferRegion.region`` sequence of ``Range``
objects directly. ``Range.extent`` is already simplified by the
front-end, so we avoid computing ``end - start`` on raw Expr (which
yields an un-simplified ``Sub`` and breaks ``int(...)``).
"""
out = []
a = Analyzer()
for r in region:
if hasattr(r, "extent"):
ext = r.extent
else:
start, end = r
ext = a.simplify(end - start)
out.append(int(ext))
return tuple(out)
def shape_broadcast_compat(op_shape, anchor_shape) -> tuple[bool, str | None]:
"""NumPy-style: right-align op against anchor; per-dim extent must equal
anchor's or be 1. anchor is the result shape; op broadcasts TO anchor.
"""
pad = len(anchor_shape) - len(op_shape)
if pad < 0:
return False, f"op rank {len(op_shape)} > anchor rank {len(anchor_shape)}"
for d in range(len(op_shape)):
e_op = int(op_shape[d])
e_a = int(anchor_shape[pad + d])
if e_op != e_a and e_op != 1:
return False, f"dim {d}: op extent {e_op} vs anchor {e_a} (need equal or 1)"
return True, None
def _broadcast_lift(op_layout, op_tensor_shape, anchor_tensor_shape):
"""Lift ``op_layout`` to ``anchor_tensor_shape`` by inserting stride-0
iters for padded leading dims and replacing extent-1 buckets with a
single stride-0 iter of anchor's extent. Offset and replica list are
preserved untouched.
The lift preserves the physical-address function: new iters have
stride 0 so they contribute ``coord * 0 = 0`` to the address
regardless of which virtual index is supplied, and dropped extent-1
iters contributed ``0 * stride = 0`` already.
"""
pad = len(anchor_tensor_shape) - len(op_tensor_shape)
assert pad >= 0, "shape_broadcast_compat should have rejected this"
try:
grouped, seps = op_layout.group(list(op_tensor_shape))
except Exception as e: # pylint: disable=broad-except
raise ValueError(
f"op layout {op_layout} not groupable by tensor shape {op_tensor_shape}: {e}"
) from e
new_shard: list = []
# (1) Padded leading dims — one stride-0 iter each.
for d in range(pad):
new_shard.append(Iter(int(anchor_tensor_shape[d]), 0, Axis.get("m")))
# (2) Aligned dims.
for d_op in range(len(op_tensor_shape)):
e_op = int(op_tensor_shape[d_op])
e_a = int(anchor_tensor_shape[pad + d_op])
bucket = list(grouped.shard[seps[d_op] : seps[d_op + 1]])
if e_op == e_a:
new_shard.extend(bucket)
elif e_op == 1:
new_shard.append(Iter(e_a, 0, Axis.get("m")))
else:
raise ValueError(
f"dim {d_op}: op extent {e_op} vs anchor {e_a}"
" (shape_broadcast_compat should have rejected)"
)
return TileLayout.from_iters(new_shard, grouped.replica, grouped.offset)
# -----------------------------------------------------------------------------
# Shared preprocess: slice each operand by its region, broadcast-lift to
# anchor's tensor shape. Output: every operand has a layout whose logical
# shape equals ``anchor_tensor_shape``. Reg / smem diverge from here.
# -----------------------------------------------------------------------------
def preprocess_operand(op_br, anchor_tshape):
"""Slice ``op_br``'s buffer layout by region (region offset absorbed into
layout.offset), then broadcast-lift to ``anchor_tshape`` if shapes differ.
Raises ``ValueError`` if the lift is not broadcast-compatible (caller
should have verified via ``shape_broadcast_compat`` in the predicate).
"""
op_layout = op_br.buffer.layout
op_shape = op_br.buffer.shape
op_region = [(r.min, r.min + r.extent) for r in op_br.region]
sliced = op_layout.slice(list(op_shape), op_region).canonicalize()
sliced = _extract_tile(sliced, op_region)
op_tshape = _tensor_shape_of(op_br.region)
if op_tshape != tuple(anchor_tshape):
sliced = _broadcast_lift(sliced, op_tshape, anchor_tshape)
return sliced
def preprocess_operands(plan):
"""Shared entry for reg.py and smem.py: returns
``(anchor_tensor_shape, {op_br: sliced_lifted_layout})``.
Every output layout has logical shape == ``anchor_tensor_shape``.
Broadcast iters carry stride 0 with the default mem axis. Reg's induced
partition (`align_operands_to_anchor`) and smem's synthesized partition
both build on this output.
"""
anchor_tshape = _tensor_shape_of(plan.dst.region)
out: dict = {}
for br in buffer_regions(plan):
out[br] = preprocess_operand(br, anchor_tshape)
return anchor_tshape, out
# -----------------------------------------------------------------------------
# Multi-operand layout alignment for reg.py (induced)
# -----------------------------------------------------------------------------
def _align_layouts_no_post_canon(r_layout, r_shape, r_region, s_layout, s_shape, s_region):
"""Variant of copy ``reg.py:align_layouts_raw`` that omits the final
``canonicalize()`` on ``r_p``.
Copy's version returns ``r_p = r.permute_dims(perm).canonicalize()`` —
that post-permute canonicalize can fuse adjacent iters (e.g. wgmma
layout's 5 iters collapse to 2), but ``s_seps`` is built from
``perm`` of length ``len(r.shard) pre-canon``. The two lengths then
disagree and ``s_p.shard[s_seps[k]:s_seps[k+1]]`` indexes into the
wrong sub-range.
Dropping the final canonicalize keeps ``r_p.shard`` and ``s_seps`` in
1-to-1 correspondence. Copy's tests don't hit this because R is
typically 1D and doesn't fuse further after permute.
"""
r = r_layout.slice(list(r_shape), r_region).canonicalize()
s = s_layout.slice(list(s_shape), s_region).canonicalize()
s = _extract_tile(s, s_region)
# Broadcast lift: when op's post-slice tensor shape != anchor's, expand
# s via stride-0 iters so group() below can partition along anchor's
# iter structure. Legality must be enforced upstream by the predicate.
r_tshape = _tensor_shape_of(r_region)
s_tshape = _tensor_shape_of(s_region)
if s_tshape != r_tshape:
s = _broadcast_lift(s, s_tshape, r_tshape)
perm = _compute_perm_r(r)
r_shape_for_group = [int(it.extent) for it in r.shard]
s_grp, seps = s.group(r_shape_for_group)
s_p = s_grp.permute_by_groups(list(seps), perm)
r_p = r.permute_dims(perm) # NO post-canonicalize
sizes = [seps[i + 1] - seps[i] for i in range(len(seps) - 1)]
s_seps = [0]
for p in perm:
s_seps.append(s_seps[-1] + sizes[p])
return r_p, s_p, s_seps
def align_operands_to_anchor(anchor_br, layout_others_br):
"""Align every layout-bearing non-anchor operand to ``anchor_br``.
Returns ``(anchor_p, per_op_aligned)`` where ``per_op_aligned[op_br] =
(op_p, op_seps)``. Trivial-layout operands are NOT included here —
caller indexes them directly via their region. Scalar srcs likewise
live outside this map.
Caller must enter ``with sctx.target:`` so ``canonicalize()`` runs the
scope-aware fusers (e.g. laneid+wid_in_wg → tid_in_wg).
Uses ``_align_layouts_no_post_canon`` (not copy's ``align_layouts_raw``
directly) so ``anchor_p.shard`` length matches ``op_seps`` groupings.
"""
anchor_layout = anchor_br.buffer.layout
anchor_shape = anchor_br.buffer.shape
anchor_region = [(r.min, r.min + r.extent) for r in anchor_br.region]
per_op_aligned: dict = {}
anchor_p = None
if not layout_others_br:
# Just slice + permute anchor alone (no post-canon — keep iters
# 1-to-1 with how they'd appear with srcs).
r = anchor_layout.slice(list(anchor_shape), anchor_region).canonicalize()
perm = _compute_perm_r(r)
anchor_p = r.permute_dims(perm)
return anchor_p, per_op_aligned
for op_br in layout_others_br:
op_layout = op_br.buffer.layout
op_shape = op_br.buffer.shape
op_region = [(r.min, r.min + r.extent) for r in op_br.region]
r_p, op_p, op_seps = _align_layouts_no_post_canon(
anchor_layout,
anchor_shape,
anchor_region,
op_layout,
op_shape,
op_region,
)
if anchor_p is None:
anchor_p = r_p
per_op_aligned[op_br] = (op_p, op_seps)
return anchor_p, per_op_aligned
# -----------------------------------------------------------------------------
# vec_chunk selection
# -----------------------------------------------------------------------------
def pick_vec_chunk(spec, op_call, sctx, plan, max_layout_vec_len: int):
"""Pick widest ``(vec_chunk, vec_impl)`` such that:
- ``vec_impl.vec_len`` divides ``max_layout_vec_len`` AND ``vec_impl.applies(...)``
- Or no vec_impl matches → scalar fallback ``(max_layout_vec_len, None)``
``spec.vec_impls`` is assumed pre-sorted widest-first.
"""
if max_layout_vec_len <= 0:
return 1, None
for impl in getattr(spec, "vec_impls", []):
if impl.vec_len > max_layout_vec_len:
continue
if max_layout_vec_len % impl.vec_len != 0:
continue
ok, _ = impl.applies(op_call, sctx, plan)
if ok:
return impl.vec_len, impl
return max_layout_vec_len, None
# -----------------------------------------------------------------------------
# Emit-time helpers
# -----------------------------------------------------------------------------
def _broadcast_indices(dst_indices, dst_start, dst_extent, op_start, op_ext):
"""NumPy-style right-aligned broadcast: derive op's per-dim indices from
dst's. For matching extents copies dst's coord (rebased); for op_ext[d]
== 1 returns the constant start (the only valid index for that dim).
"""
pad = len(dst_extent) - len(op_ext)
return [
(dst_indices[i + pad] - dst_start[i + pad]) + op_start[i]
if int(op_ext[i]) != 1
else op_start[i]
for i in range(len(op_ext))
]
def fetch_src_value(src, fused, dst_indices, dst_start, dst_extent):
"""Per-element load Expr for one src. Handles buffer / scalar / broadcast srcs."""
if src.is_scalar:
return src.scalar
region = src.buf_region
src_st, src_ext = get_st_extent(region)
if src.index_fn is not None:
idx = src.index_fn(dst_indices, dst_start, dst_extent, src_st, src_ext)
elif tuple(int(e) for e in src_ext) != tuple(int(e) for e in dst_extent):
# Broadcast — derive src indices from dst's via right-aligned compat.
idx = _broadcast_indices(dst_indices, dst_start, dst_extent, src_st, src_ext)
else:
idx = get_indices(fused, src_st, src_ext)
return region.buffer[tuple(idx)]
def emit_scope_sync(scope_kind: str):
"""Returns an ``@T.inline`` sync helper matched to the exec scope."""
@T.inline
def sync():
if scope_kind == "cta":
T.cuda.cta_sync()
elif scope_kind == "warpgroup":
T.cuda.warpgroup_sync(8)
elif scope_kind == "warp":
T.cuda.warp_sync()
return sync
__all__ = [
"_TID_AXIS_FOR_SCOPE",
"_all_threads_active",
"_axis_decl",
"_broadcast_indices",
"_tensor_shape_of",
"_thread_cnt",
"align_operands_to_anchor",
"buffer_regions",
"compute_dtype_of",
"dtype_bits",
"dtype_name",
"emit_scope_sync",
"fetch_src_value",
"n_elements",
"pick_anchor",
"pick_vec_chunk",
"preprocess_operand",
"preprocess_operands",
"shape_broadcast_compat",
]
@@ -0,0 +1,121 @@
# 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.
"""Per-op data model + ALL_OPS registry.
``OpSpec`` describes one elementwise op. ``VecImpl`` describes one packed-PTX
or CUDA-intrinsic emit available for that op (e.g. ``add_f32x2``); a list of
these (widest-first) lets ``reg.py``/``smem.py`` pick the widest matching
both the layout and the op's available intrinsics, like copy picks
``copy_{128,64,32,16,8}b`` based on bit-width and tail contiguity.
"""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any
from tvm.ir.expr import Expr
from tvm.tirx import BufferRegion, TilePrimitiveCall
@dataclass
class SrcSpec:
"""One operand of an elementwise op.
Either a ``BufferRegion`` (per-element load) or a scalar ``Expr``.
``index_fn``, if given, derives per-element indices for broadcasting srcs:
``index_fn(dst_indices, dst_start, dst_extent, src_start, src_extent) -> list[Expr]``
Default is the standard ``get_indices`` over the src's own region.
"""
buf_region: BufferRegion | None = None
scalar: Expr | None = None
index_fn: Callable | None = None
@property
def is_scalar(self) -> bool:
return self.scalar is not None
@property
def buffer(self):
return self.buf_region.buffer if self.buf_region is not None else None
@dataclass
class Plan:
"""Parsed elementwise op ready for a schedule to consume."""
dst: BufferRegion
srcs: list[SrcSpec]
extras: dict[str, Any] = field(default_factory=dict)
@dataclass
class VecImpl:
"""One packed-vector implementation registered for an op.
Mirrors the ``copy_{Nb}`` menu in copy: each entry says "I can process
``vec_len`` consecutive elements per call". The schedule picks the widest
one whose ``vec_len`` divides the layout's contig tail AND whose
``applies()`` returns ``True``.
"""
vec_len: int # elements per packed call
applies: Callable[[TilePrimitiveCall, Any, Plan], tuple[bool, str | None]]
# emit(dst_ptr, src_ptrs, extras) -> Stmt
# dst_ptr: typed ptr to ``vec_len`` consecutive dst elements
# src_ptrs[i]: typed ptr to ``vec_len`` consecutive src[i] elements,
# OR a scalar Expr if src[i].is_scalar.
# Runs in Python at @T.prim_func build time -- branching on src kind is a
# normal Python ``if``, not a TVMScript shape limitation. This is what
# collapses the old 4x2 shape-explosion in schema.py's factories.
emit: Callable
@dataclass
class OpSpec:
"""Metadata for an elementwise op."""
name: str
# parse(op_call) -> (Plan, msg|None); msg explains why parse failed.
parse: Callable[[TilePrimitiveCall], tuple[Plan | None, str | None]]
# Scalar compute used by the fallback emit path (wrapped in Tx.vectorized).
# compute_scalar(src_vals_at_one_idx, extras, dst_dtype) -> Expr
compute_scalar: Callable[[list, dict, str], Any]
# Optional dtype check on plan.extras (e.g. unary bias/scale dtype agreement).
check_extras: Callable | None = None
# Widest-first vec impls. Schedule picks first matching layout+applies.
vec_impls: list[VecImpl] = field(default_factory=list)
def _build_all_ops() -> dict[str, OpSpec]:
"""Aggregate per-family op specs. Deferred imports avoid cycles
(vec_emit/* imports VecImpl from this module)."""
from .binary import BINARY_OPS
from .cast import CAST_OPS
from .fma import FMA_OPS
from .unary import UNARY_OPS
return {**UNARY_OPS, **BINARY_OPS, **CAST_OPS, **FMA_OPS}
ALL_OPS: dict[str, OpSpec] = _build_all_ops()
__all__ = ["ALL_OPS", "OpSpec", "Plan", "SrcSpec", "VecImpl"]
@@ -0,0 +1,139 @@
# 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.
"""Binary elementwise ops: add / sub / mul / fdiv / maximum.
Includes constant-lhs commute logic. Broadcasting (extent=1 dims) is
handled at the layout level in dispatch's ``_broadcast_lift``, not here —
parser just records each src as-is.
``add``/``sub``/``mul`` attach a ``VecImpl`` for sm_100+ packed f32x2;
``fdiv``/``maximum`` have no packed PTX (scalar fallback only — ``max`` lowers
to a single ``FMNMX``/``max.f32``, which is exact, so there is no rounding/ftz
variant to pack).
"""
from __future__ import annotations
import functools
import operator
from typing import Any
from tvm.script import tirx as Tx
from tvm.tirx import BufferRegion, TilePrimitiveCall
from ..vec_emit.binary_f32x2 import BINARY_F32X2_IMPLS
from . import OpSpec, Plan, SrcSpec
_COMMUTATIVE = frozenset({"add", "mul", "maximum"})
def _parse_binary_for(op_name: str):
"""Build a ``parse(op_call) -> (Plan, msg)`` for a specific binary op."""
def parse(op: TilePrimitiveCall) -> tuple[Plan | None, str | None]:
_dst: BufferRegion = op.args[0]
_src1 = op.args[1]
_src2 = op.args[2]
s1_scalar = not isinstance(_src1, BufferRegion)
s2_scalar = not isinstance(_src2, BufferRegion)
if s1_scalar and s2_scalar:
return None, "both inputs are constants"
# Move constant to rhs (commute if allowed; else reject).
if s1_scalar:
if op_name not in _COMMUTATIVE:
return None, f"non-commutative op {op_name} cannot have constant lhs"
_src1, _src2 = _src2, _src1
s2_scalar = True
# If rhs is a smaller buffer (broadcast), swap if commutative so the
# bigger one is in src1 — keeps src1 == dst convention.
if not s2_scalar:
s1_n = functools.reduce(operator.mul, [r.extent for r in _src1.region], 1)
s2_n = functools.reduce(operator.mul, [r.extent for r in _src2.region], 1)
if s1_n < s2_n:
if op_name not in _COMMUTATIVE:
return None, f"non-commutative op {op_name} cannot swap to broadcast"
_src1, _src2 = _src2, _src1
srcs: list[SrcSpec] = [SrcSpec(buf_region=_src1)]
if s2_scalar:
srcs.append(SrcSpec(scalar=_src2))
else:
srcs.append(SrcSpec(buf_region=_src2))
extras: dict[str, Any] = {}
rm = op.config.get("rounding_mode", None)
if rm is not None:
extras["rounding_mode"] = rm
return Plan(dst=_dst, srcs=srcs, extras=extras), None
return parse
def _compute_add(src_vals, extras, dt):
return src_vals[0] + src_vals[1]
def _compute_sub(src_vals, extras, dt):
return src_vals[0] - src_vals[1]
def _compute_mul(src_vals, extras, dt):
return src_vals[0] * src_vals[1]
def _compute_fdiv(src_vals, extras, dt):
return src_vals[0] / src_vals[1]
def _compute_maximum(src_vals, extras, dt):
return Tx.max(src_vals[0], src_vals[1])
BINARY_OPS: dict[str, OpSpec] = {
"add": OpSpec(
"add",
_parse_binary_for("add"),
_compute_add,
vec_impls=[BINARY_F32X2_IMPLS["add"]],
),
"sub": OpSpec(
"sub",
_parse_binary_for("sub"),
_compute_sub,
vec_impls=[BINARY_F32X2_IMPLS["sub"]],
),
"mul": OpSpec(
"mul",
_parse_binary_for("mul"),
_compute_mul,
vec_impls=[BINARY_F32X2_IMPLS["mul"]],
),
"fdiv": OpSpec(
"fdiv",
_parse_binary_for("fdiv"),
_compute_fdiv,
),
"maximum": OpSpec(
"maximum",
_parse_binary_for("maximum"),
_compute_maximum,
),
}
@@ -0,0 +1,45 @@
# 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.
"""Cast op: ``Tx.cast(dst, src)``. Outer ``Tx.cast(..., dst.dtype)`` in the
schedule handles the scalar conversion; the vec-impl packs pairs via
CUDA intrinsics like ``__float22half2_rn``."""
from __future__ import annotations
from tvm.tirx import BufferRegion, TilePrimitiveCall
from ..vec_emit.cast_vec2 import CAST_VEC2_IMPL
from . import OpSpec, Plan, SrcSpec
def _parse_cast(op: TilePrimitiveCall) -> tuple[Plan | None, str | None]:
_dst: BufferRegion = op.args[0]
_src = op.args[1]
if not isinstance(_src, BufferRegion):
return None, "cast src must be a buffer region"
return Plan(dst=_dst, srcs=[SrcSpec(buf_region=_src)], extras={}), None
def _compute_cast(src_vals, extras, dt):
# Schedule wraps with Tx.cast(..., dst.dtype) — just pass through.
return src_vals[0]
CAST_OPS: dict[str, OpSpec] = {
"cast": OpSpec("cast", _parse_cast, _compute_cast, vec_impls=[CAST_VEC2_IMPL]),
}
@@ -0,0 +1,50 @@
# 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.
"""FMA op: ``Tx.fma(dst, a, b, c)`` → ``dst = a*b + c``.
Attaches ``fma_f32x2`` VecImpl for sm_100+ f32; falls back to scalar
``a*b + c`` otherwise.
"""
from __future__ import annotations
from tvm.tirx import BufferRegion, TilePrimitiveCall
from ..vec_emit.fma_f32x2 import FMA_F32X2_IMPL
from . import OpSpec, Plan, SrcSpec
def _parse_fma(op: TilePrimitiveCall) -> tuple[Plan | None, str | None]:
_dst: BufferRegion = op.args[0]
args = op.args[1:4]
srcs: list[SrcSpec] = []
for a in args:
if isinstance(a, BufferRegion):
srcs.append(SrcSpec(buf_region=a))
else:
srcs.append(SrcSpec(scalar=a))
return Plan(dst=_dst, srcs=srcs, extras={}), None
def _compute_fma(src_vals, extras, dt):
return src_vals[0] * src_vals[1] + src_vals[2]
FMA_OPS: dict[str, OpSpec] = {
"fma": OpSpec("fma", _parse_fma, _compute_fma, vec_impls=[FMA_F32X2_IMPL]),
}
@@ -0,0 +1,121 @@
# 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.
"""Unary elementwise ops: zero / fill / reciprocal / sqrt / exp / exp2 / silu.
All carry the same ``T.<unary>(dst, src[, bias, scale])`` shape (bias / scale
optional; ``silu`` ignores bias/scale to preserve legacy behavior).
"""
from __future__ import annotations
from typing import Any
from tvm.ir import is_prim_expr
from tvm.script import tirx as T
from tvm.tirx import BufferRegion, TilePrimitiveCall
from tvm.tirx.expr import FloatImm
from .._common import scalar_dtype
from . import OpSpec, Plan, SrcSpec
def _parse_unary(op: TilePrimitiveCall) -> tuple[Plan | None, str | None]:
"""T.<unary>(dst, src[, bias, scale]) → Plan."""
_dst: BufferRegion = op.args[0]
_src = op.args[1]
_bias = op.args[2] if len(op.args) > 2 else None
_scale = op.args[3] if len(op.args) > 2 else None
srcs: list[SrcSpec] = []
if isinstance(_src, BufferRegion):
srcs.append(SrcSpec(buf_region=_src))
elif is_prim_expr(_src):
srcs.append(SrcSpec(scalar=_src))
else:
return None, f"unsupported src type {type(_src).__name__}"
extras: dict[str, Any] = {
"scale": _scale,
"bias_const": _bias if isinstance(_bias, FloatImm) else None,
}
if isinstance(_bias, BufferRegion):
srcs.append(SrcSpec(buf_region=_bias))
extras["has_bias_buf"] = True
else:
extras["has_bias_buf"] = False
return Plan(dst=_dst, srcs=srcs, extras=extras), None
def _check_unary_extras(extras: dict, compute_dtype: str) -> tuple[bool, str | None]:
scale = extras.get("scale")
if scale is not None and scalar_dtype(scale) != compute_dtype:
return False, f"scale dtype {scalar_dtype(scale)} != compute dtype {compute_dtype}"
bias_const = extras.get("bias_const")
if bias_const is not None and scalar_dtype(bias_const) != compute_dtype:
return (
False,
f"bias_const dtype {scalar_dtype(bias_const)} != compute dtype {compute_dtype}",
)
return True, None
def _with_bias_scale(raw_op):
"""Wrap ``raw_op`` (e.g. ``T.exp``) into a compute that applies bias/scale first."""
def compute(src_vals, extras, dt):
x = src_vals[0]
scale = extras.get("scale")
if scale is not None:
x = x * scale
if extras.get("has_bias_buf"):
x = x + src_vals[1]
elif extras.get("bias_const") is not None:
x = x + extras["bias_const"]
return raw_op(x)
return compute
def _compute_zero(src_vals, extras, dt):
return 0.0
def _compute_fill(src_vals, extras, dt):
return src_vals[0]
def _compute_reciprocal(src_vals, extras, dt):
x = src_vals[0]
return T.FloatImm(x.ty, 1.0) / x
def _compute_silu(src_vals, extras, dt):
# Legacy: silu doesn't apply bias/scale.
x = src_vals[0]
return x / (T.FloatImm(x.ty, 1.0) + T.exp(T.FloatImm(x.ty, 0.0) - x))
UNARY_OPS: dict[str, OpSpec] = {
"zero": OpSpec("zero", _parse_unary, _compute_zero, _check_unary_extras),
"fill": OpSpec("fill", _parse_unary, _compute_fill, _check_unary_extras),
"reciprocal": OpSpec("reciprocal", _parse_unary, _compute_reciprocal, _check_unary_extras),
"sqrt": OpSpec("sqrt", _parse_unary, _with_bias_scale(T.sqrt), _check_unary_extras),
"exp": OpSpec("exp", _parse_unary, _with_bias_scale(T.exp), _check_unary_extras),
"exp2": OpSpec("exp2", _parse_unary, _with_bias_scale(T.exp2), _check_unary_extras),
"silu": OpSpec("silu", _parse_unary, _compute_silu, _check_unary_extras),
}
@@ -0,0 +1,429 @@
# 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.
"""Elementwise dispatch when all operands live in ``local`` (registers).
Mirrors ``cuda/copy/reg.py``: the partition is *induced* by the layout that
carries thread-axis info (the "anchor" operand). The region slice is absorbed
into the sliced layout up front via ``align_operands_to_anchor`` — emit
operates on a flat 1D per-thread view and indexes it with a scalar offset, so
codegen never sees multi-dim ``get_indices`` inside ``T.vectorized``.
Two paths inside emit:
* induced (anchor exists) — atom-based, exactly mirrors copy reg.py
* trivial (no anchor) — flat full region, every thread runs the full
loop on its private storage
"""
from __future__ import annotations
import functools
import operator
from tvm.arith import Analyzer
from tvm.script import tirx as T
from tvm.tirx import PrimFunc, TilePrimitiveCall
from tvm.tirx.layout import TileLayout
from tvm.tirx.operator.tile_primitive import DispatchContext
from tvm.tirx.operator.tile_primitive.dispatcher import fail
from ..common import get_st_extent
from ..copy._common import _carve_tail, _verify_s_tail_contig
from ..layout_utils import get_sublayout_from_region, layout_signature
from ._common import (
_TID_AXIS_FOR_SCOPE,
_all_threads_active,
_tensor_shape_of,
_thread_cnt,
align_operands_to_anchor,
buffer_regions,
compute_dtype_of,
pick_anchor,
shape_broadcast_compat,
)
# -----------------------------------------------------------------------------
# Predicate
# -----------------------------------------------------------------------------
def _validate_anchor_layout(anchor_br) -> tuple[bool, str | None]:
layout = anchor_br.buffer.layout
if layout.is_swizzle():
return False, "anchor layout is swizzle"
if not isinstance(layout, TileLayout):
return False, f"anchor layout is {type(layout).__name__}, not TileLayout"
return True, None
def _validate_scope_level_anchor(anchor_br, sctx: DispatchContext) -> tuple[bool, str | None]:
"""For warp/warpgroup/cta scope, require dst to be scope-level: after
canonicalizing with the target its thread axes are the scope's intra-thread
axis (laneid/tid_in_wg/tx) and, sorted by stride, tile a complete ``T:1``
chain over all ``T`` threads of the scope. Rejects thread-local ``.local()``
views; thread scope is exempt.
"""
scope = sctx.scope_kind
if scope == "thread":
return True, None
expected_axis = _TID_AXIS_FOR_SCOPE.get(scope)
if expected_axis is None:
return True, None
expected_cnt = _thread_cnt(sctx)
# Canonicalize the sliced anchor with the target so warp/lane axes fuse.
st, ext = get_st_extent(anchor_br)
sliced = get_sublayout_from_region(anchor_br.buffer.layout, anchor_br.buffer.shape, st, ext)
with sctx.target:
canon = sliced.canonicalize()
shard = getattr(canon, "shard", None)
if shard is None:
return False, f"{scope}-scope op operand layout is not a TileLayout after slicing"
thread_iters = [it for it in shard if it.axis.is_thread()]
if not thread_iters:
return (
False,
f"{scope}-scope op needs a {scope}-level operand whose layout carries "
f"thread axes ({expected_axis} composing to {expected_cnt}:1); got a "
f"thread-local view with no thread axes — pass the {scope}-level tensor, "
f"not its `.local()` (per-thread) view",
)
bad = sorted({it.axis.name for it in thread_iters if it.axis.name != expected_axis})
if bad:
return (
False,
f"{scope}-scope op operand carries thread axes {bad}; after "
f"canonicalization a {scope}-level layout must use only {expected_axis!r}",
)
# Sorted by stride the thread iters must tile a complete chain 1, e0,
# e0*e1, ... up to the scope thread count — i.e. cover all T threads with
# no gap or overlap (extents alone would miss gaps/overlaps).
running = 1
for it in sorted(thread_iters, key=lambda i: int(i.stride)):
stride, extent = int(it.stride), int(it.extent)
if stride != running:
return (
False,
f"{scope}-scope op operand thread axes do not tile a complete "
f"{expected_cnt}:1 (sorted by stride: expected {running}, got {stride})",
)
running *= extent
if running != expected_cnt:
return (
False,
f"{scope}-scope op operand thread axes span {running} threads, not the "
f"full {expected_cnt} of the {scope}",
)
return True, None
def _check_layout_operands_agree(plan, sctx) -> tuple[bool, str | None]:
"""Replica sigs must match across non-trivial-layout operands.
``align_operands_to_anchor`` normalizes thread + local parts via
permute/group, but the replica part isn't touched by alignment — if
operands disagree there, alignment can't fix it and emit will be wrong.
Thread / local mismatches that alignment can't resolve will raise
cleanly at align time, so we don't pre-check them.
"""
# All operands have a layout (predicate already enforced this); just
# iterate them all.
layout_brs = list(buffer_regions(plan))
if len(layout_brs) < 2:
return True, None
analyzer = Analyzer()
replica_sigs = []
for br in layout_brs:
st, ext = get_st_extent(br)
with sctx.target:
sliced = get_sublayout_from_region(br.buffer.layout, br.buffer.shape, st, ext)
canon = sliced.canonicalize()
sig = layout_signature(canon)
if sig is None:
return False, "layout has no signature (not a TileLayout?)"
# layout_signature returns (thread_sig, local_sig, replica_sig)
replica_sigs.append(sig[2])
for s in replica_sigs[1:]:
# Compare replica entries (axis_key, extent, stride) element-wise.
if len(s) != len(replica_sigs[0]):
return False, "replica sig mismatch (different number of replica iters)"
for (k_a, e_a, st_a), (k_b, e_b, st_b) in zip(replica_sigs[0], s):
if k_a != k_b:
return False, "replica sig mismatch (axis key)"
if not analyzer.can_prove_equal(e_a, e_b):
return False, "replica sig mismatch (extent)"
if not analyzer.can_prove_equal(st_a, st_b):
return False, "replica sig mismatch (stride)"
return True, None
def is_reg_ewise(spec):
"""Predicate factory: dispatch accepted iff all operands in ``local`` scope."""
def check(op_call: TilePrimitiveCall, sctx: DispatchContext) -> tuple[bool, str | None]:
if not sctx.is_target("cuda"):
return False, "non-cuda target"
if sctx.scope_kind not in ("thread", "warp", "warpgroup", "cta"):
return False, f"unsupported scope {sctx.scope_kind}"
ok, reason = _all_threads_active(sctx)
if not ok:
return False, reason
plan, msg = spec.parse(op_call)
if msg is not None or plan is None:
return False, msg
for br in buffer_regions(plan):
if br.buffer.scope() != "local":
return False, f"operand scope {br.buffer.scope()} != local"
if br.buffer.layout is None:
return False, f"operand {br} has no layout"
if spec.check_extras is not None:
ok2, reason2 = spec.check_extras(plan.extras, compute_dtype_of(plan))
if not ok2:
return False, reason2
anchor = pick_anchor(plan)
ok3, reason3 = _validate_anchor_layout(anchor)
if not ok3:
return False, reason3
ok_scope, reason_scope = _validate_scope_level_anchor(anchor, sctx)
if not ok_scope:
return False, reason_scope
# Shape compat (NumPy-style broadcast): anchor's tensor shape is the
# result shape; every operand must broadcast TO anchor.
anchor_tshape = _tensor_shape_of(anchor.region)
for br in buffer_regions(plan):
if br is anchor:
continue
op_tshape = _tensor_shape_of(br.region)
ok_b, reason_b = shape_broadcast_compat(op_tshape, anchor_tshape)
if not ok_b:
return False, f"shape incompat: {reason_b}"
ok4, reason4 = _check_layout_operands_agree(plan, sctx)
if not ok4:
return False, reason4
return True, None
return check
# -----------------------------------------------------------------------------
# Shared helpers
# -----------------------------------------------------------------------------
def _prod(it) -> int:
return functools.reduce(operator.mul, it, 1)
# -----------------------------------------------------------------------------
# Main entry
# -----------------------------------------------------------------------------
def emit_reg(op_call: TilePrimitiveCall, spec, sctx: DispatchContext) -> PrimFunc:
plan, msg = spec.parse(op_call)
if msg is not None or plan is None:
fail(msg or "parse failed")
return _emit_induced(plan, spec, sctx, op_call, pick_anchor(plan))
# -----------------------------------------------------------------------------
# Induced path — anchor with non-trivial layout drives partition
# -----------------------------------------------------------------------------
def _strip_thread(layout):
"""Return a new TileLayout with thread iters removed from shard."""
mem_iters = [it for it in layout.shard if not it.axis.is_thread()]
return TileLayout.from_iters(mem_iters, list(layout.replica), dict(layout.offset))
def _pick_vec_and_carve(spec, op_call, sctx, plan, per_op_mem_layouts):
"""Pick ``(vec_len, vec_impl, carved_layouts)``.
Enumerate ``spec.vec_impls`` widest-first. For each candidate ``vec_len``:
1. Try ``_carve_tail`` on each operand's mem-only layout (may split a
boundary iter so the tail product equals ``vec_len``).
2. Verify the carved tail is physically contiguous (stride-1 chain whose
product equals ``vec_len``).
3. Call ``impl.applies(op_call, sctx, plan)``.
First candidate that passes all three on EVERY operand wins. Otherwise
fall back to scalar: ``vec_len=1``, ``vec_impl=None``, original (uncarved)
layouts.
"""
impls = sorted(getattr(spec, "vec_impls", []), key=lambda i: -i.vec_len)
for impl in impls:
cand = impl.vec_len
carved_try = {}
all_ok = True
for op_br, layout in per_op_mem_layouts.items():
new_iters = _carve_tail(list(layout.shard), cand)
if new_iters is None:
all_ok = False
break
new_layout = TileLayout.from_iters(new_iters, list(layout.replica), dict(layout.offset))
if not _verify_s_tail_contig(new_layout, cand):
all_ok = False
break
carved_try[op_br] = new_layout
if not all_ok:
continue
ok, _ = impl.applies(op_call, sctx, plan)
if not ok:
continue
return cand, impl, carved_try
# Scalar fallback — use uncarved mem layouts as-is.
return 1, None, dict(per_op_mem_layouts)
def _emit_induced(plan, spec, sctx, op_call, anchor_br) -> PrimFunc:
# Every buffer-region operand has a layout (enforced by predicate);
# trivial / identity layouts are fine — the algorithm is robust to
# layouts with no thread axes (strip is no-op, placeholders empty).
layout_others = [br for br in buffer_regions(plan) if br is not anchor_br]
# Step 1: slice + permute (region offset absorbed into op_p.offset; per-iter
# strides reflect post-slice physical addressing). No (st, ext) leaks out.
with sctx.target:
anchor_p, per_op_aligned = align_operands_to_anchor(anchor_br, layout_others)
# Step 2: post-align thread-equality check. ``align`` is supposed to
# normalize the thread part; we verify it did. (Replica was pre-checked
# in the predicate.)
def _thread_iters(layout):
c = layout.canonicalize()
return [(it.axis, int(it.extent), int(it.stride)) for it in c.shard if it.axis.is_thread()]
anchor_thread = _thread_iters(anchor_p)
for op_br, (op_p, _) in per_op_aligned.items():
if _thread_iters(op_p) != anchor_thread:
fail("thread part mismatch between anchor and operand after alignment")
# Step 3: drop thread iters; from here on operands have mem-only layouts.
per_op_mem = {anchor_br: _strip_thread(anchor_p)}
for op_br, (op_p, _) in per_op_aligned.items():
per_op_mem[op_br] = _strip_thread(op_p)
# Step 4: enumerate spec.vec_impls widest-first; try carve tail for each
# operand. First candidate that all operands can carve + impl.applies wins.
# Otherwise scalar fallback (vec=1, no inner loop).
vec_len, vec_impl, per_op_carved = _pick_vec_and_carve(spec, op_call, sctx, plan, per_op_mem)
# Step 5: totals + emit. per_thread_total = ∏ extents of (carved) mem
# layout. All operands have the same per_thread_total (alignment invariant).
per_thread_total = _prod(int(it.extent) for it in per_op_carved[anchor_br].shard)
outer_total = per_thread_total // vec_len if vec_len > 0 else per_thread_total
if vec_impl is not None:
result = _emit_induced_packed(
plan,
vec_impl,
vec_len,
outer_total,
per_thread_total,
per_op_carved,
anchor_br,
)
else:
result = _emit_induced_scalar(
plan,
spec,
outer_total,
per_thread_total,
per_op_carved,
anchor_br,
)
return result
def _make_views_meta(per_op_carved, per_thread_total):
"""Build the per-operand 1D buffer view dict.
Each view aliases the operand's physical storage as a 1D shape of
``per_thread_total`` elements, with layout = the operand's carved mem-only
TileLayout. Scalar indexing into the view goes through this layout's
iter strides at codegen time.
"""
return {
op_br: T.decl_buffer(
(per_thread_total,),
op_br.buffer.dtype,
op_br.buffer.data,
scope="local",
layout=per_op_carved[op_br],
)
for op_br in per_op_carved
}
# -----------------------------------------------------------------------------
# Emit — packed (one PTX/CUDA call per outer chunk; no T.vectorized inside)
# -----------------------------------------------------------------------------
def _emit_induced_packed(
plan, vec_impl, vec_len, outer_total, per_thread_total, per_op_carved, anchor_br
) -> PrimFunc:
extras = plan.extras
srcs = plan.srcs
dst_br = plan.dst
@T.prim_func(check_well_formed=False)
def impl():
views = T.meta_var(_make_views_meta(per_op_carved, per_thread_total))
# Serial loop (not T.unroll): T.unroll materializes each per-iter
# ``dst_lane_indices`` / ``src_args`` buffer as a fresh int[1]
# declaration, multiplying by outer_total. ptxas unrolls the
# static-bound loop without that scratch explosion.
for f in range(outer_total):
# Pass logical 1D coord; each buffer's own layout maps it to
# physical at access time (handles wgmma, broadcast, etc.).
dst_lane_indices = [[f * vec_len + k] for k in range(vec_len)]
src_args = T.meta_var(
[
src.scalar
if src.is_scalar
else (
views[src.buf_region],
[[f * vec_len + k] for k in range(vec_len)],
)
for src in srcs
]
)
T.evaluate(vec_impl.emit(views[dst_br], dst_lane_indices, src_args, extras))
return impl
# -----------------------------------------------------------------------------
# Emit — scalar fallback (vec_len = 1; one element per outer iter; no
# T.vectorized inside, so no codegen vec-packing of multi-dim indices).
# -----------------------------------------------------------------------------
def _emit_induced_scalar(
plan, spec, outer_total, per_thread_total, per_op_carved, anchor_br
) -> PrimFunc:
extras = plan.extras
srcs = plan.srcs
dst_br = plan.dst
dst_dtype = dst_br.buffer.dtype
compute = spec.compute_scalar
@T.prim_func(check_well_formed=False)
def impl():
views = T.meta_var(_make_views_meta(per_op_carved, per_thread_total))
# Serial loop (not T.unroll) — see _emit_induced_packed for why.
for f in range(outer_total):
# Logical 1D coord = f (vec_len = 1 in scalar path); each
# buffer's layout maps to physical at access time.
src_vals = T.meta_var(
[src.scalar if src.is_scalar else views[src.buf_region][f] for src in srcs]
)
views[dst_br][f] = T.cast(compute(src_vals, extras, dst_dtype), dst_dtype)
return impl
@@ -0,0 +1,61 @@
# 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.
"""Register each op in ``ALL_OPS`` for both dispatch variants (``reg``, ``smem``).
Mirrors copy PR-640's two-variant model: scope-pair drives the dispatch
selection, the underlying algorithm (induced vs synthesized) follows.
"""
from tvm.tirx import PrimFunc, TilePrimitiveCall
from tvm.tirx.operator.tile_primitive import DispatchContext, predicate, register_dispatch
from .ops import ALL_OPS
from .reg import emit_reg, is_reg_ewise
from .smem import emit_smem, is_smem_ewise
def _register_reg(spec) -> None:
@register_dispatch(
spec.name,
"cuda",
variant="reg",
priority=10,
when=[predicate(f"{spec.name}_reg", is_reg_ewise(spec))],
)
def _dispatch(op: TilePrimitiveCall, sctx: DispatchContext, _spec=spec) -> PrimFunc:
return emit_reg(op, _spec, sctx)
def _register_smem(spec) -> None:
@register_dispatch(
spec.name,
"cuda",
variant="smem",
priority=10,
when=[predicate(f"{spec.name}_smem", is_smem_ewise(spec))],
)
def _dispatch(op: TilePrimitiveCall, sctx: DispatchContext, _spec=spec) -> PrimFunc:
return emit_smem(op, _spec, sctx)
for _spec in ALL_OPS.values():
_register_reg(_spec)
_register_smem(_spec)
__all__: list[str] = []
@@ -0,0 +1,264 @@
# 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.
"""Elementwise dispatch when all operands live in ``shared*``.
Mirrors ``cuda/copy/gmem_smem.py``: no operand carries a per-thread partition,
so partition is *synthesized* from ``sctx.intra`` (``thread_cnt = ∏ intra``).
Each thread takes ``ceildiv(total, vec_chunk * thread_cnt)`` strided chunks.
The shared buffers are indexed multi-dim via ``get_indices(fused, dst_st,
dst_ext)`` and the buffer's own layout resolves to physical addresses at
codegen time. Packed-vec emit requires the innermost dim to have stride 1
(non-swizzle slice) so lanes are physically contiguous; checked in
``_max_layout_vec``.
"""
from __future__ import annotations
from tvm.script import tirx as T
from tvm.tirx import PrimFunc, TilePrimitiveCall
from tvm.tirx.operator.tile_primitive import DispatchContext
from tvm.tirx.operator.tile_primitive.dispatcher import fail
from ..common import get_indices, get_st_extent, get_thread_cnt
from ._common import (
_TID_AXIS_FOR_SCOPE,
_all_threads_active,
_axis_decl,
_broadcast_indices,
_tensor_shape_of,
buffer_regions,
compute_dtype_of,
dtype_bits,
emit_scope_sync,
fetch_src_value,
n_elements,
pick_vec_chunk,
shape_broadcast_compat,
)
# -----------------------------------------------------------------------------
# Predicate
# -----------------------------------------------------------------------------
def is_smem_ewise(spec):
"""Predicate factory: dispatch accepted iff all operands in ``shared*``."""
def check(op_call: TilePrimitiveCall, sctx: DispatchContext) -> tuple[bool, str | None]:
if not sctx.is_target("cuda"):
return False, "non-cuda target"
if sctx.scope_kind not in ("thread", "warp", "warpgroup", "cta"):
return False, f"unsupported scope {sctx.scope_kind}"
ok, reason = _all_threads_active(sctx)
if not ok:
return False, reason
plan, msg = spec.parse(op_call)
if msg is not None or plan is None:
return False, msg
for br in buffer_regions(plan):
if not br.buffer.scope().startswith("shared"):
return False, f"operand scope {br.buffer.scope()} != shared*"
if br.buffer.layout is None:
return False, "shared operand has no layout"
if spec.check_extras is not None:
ok2, reason2 = spec.check_extras(plan.extras, compute_dtype_of(plan))
if not ok2:
return False, reason2
# NumPy-style right-aligned broadcast: anchor = plan.dst; every src
# must be shape-compatible with anchor (extent matches or is 1).
anchor_tshape = _tensor_shape_of(plan.dst.region)
for s in plan.srcs:
if s.buf_region is None:
continue
src_tshape = _tensor_shape_of(s.buf_region.region)
ok_b, reason_b = shape_broadcast_compat(src_tshape, anchor_tshape)
if not ok_b:
return False, f"shape incompat: {reason_b}"
return True, None
return check
# -----------------------------------------------------------------------------
# vec_chunk selection
# -----------------------------------------------------------------------------
def _max_layout_vec(plan, total: int, thread_cnt: int) -> int:
"""Widest vec_chunk dividing all operands' innermost extents AND
``total / thread_cnt``, within dtype-bit candidates ``{128,64,32,16,8}``."""
max_bits = dtype_bits(plan.dst.buffer.dtype)
for s in plan.srcs:
if s.buf_region is not None:
max_bits = max(max_bits, dtype_bits(s.buf_region.buffer.dtype))
per_thread = total // thread_cnt if thread_cnt > 0 else total
if total % thread_cnt != 0:
return 1
inners = [int(plan.dst.region[-1].extent)]
for s in plan.srcs:
if s.buf_region is None or s.index_fn is not None:
continue
inners.append(int(s.buf_region.region[-1].extent))
for cand_bits in (128, 64, 32, 16, 8):
n = cand_bits // max_bits
if n <= 0:
continue
if per_thread % n != 0:
continue
if all(i % n == 0 for i in inners):
return n
return 1
# -----------------------------------------------------------------------------
# Main entry
# -----------------------------------------------------------------------------
def emit_smem(op_call: TilePrimitiveCall, spec, sctx: DispatchContext) -> PrimFunc:
plan, msg = spec.parse(op_call)
if msg is not None or plan is None:
fail(msg or "parse failed")
# Use cuda/common.py:get_thread_cnt rather than copy/_common.py:_thread_cnt
# — the latter computes ``∏ sctx.intra`` which silently returns 0 for
# sub-warp counts at cta scope (warpid extent rounds down to 0). The
# former reads launch_params["threadIdx.x"].dom.extent and is correct
# for all scopes.
thread_cnt = get_thread_cnt(sctx)
if thread_cnt is None:
fail(f"unsupported scope {sctx.scope_kind} for smem emit")
thread_cnt = int(thread_cnt)
if thread_cnt <= 0:
fail(f"non-positive thread_cnt {thread_cnt}")
assert "threadIdx.y" not in sctx.launch_params and "threadIdx.z" not in sctx.launch_params, (
"smem emit currently assumes 1D threadIdx"
)
total = n_elements(plan.dst)
vec_max = _max_layout_vec(plan, total, thread_cnt)
vec_chunk, vec_impl = pick_vec_chunk(spec, op_call, sctx, plan, vec_max)
if vec_impl is not None:
return _emit_packed(plan, vec_impl, vec_chunk, total, thread_cnt, sctx)
return _emit_scalar(plan, spec, vec_chunk, total, thread_cnt, sctx)
def _tid_expr(sctx: DispatchContext):
"""Per-scope tid expr. ``thread`` scope returns 0; collective scopes use
``_axis_decl`` (T.lane_id / T.thread_id_in_wg / threadIdx.x)."""
if sctx.scope_kind == "thread":
return 0
axis_name = _TID_AXIS_FOR_SCOPE[sctx.scope_kind]
return _axis_decl(axis_name, sctx)
# -----------------------------------------------------------------------------
# Per-lane src index helper (handles broadcast via right-aligned compat)
# -----------------------------------------------------------------------------
def _src_lane_indices(src_br, dst_lane_indices, dst_st, dst_ext, vec_chunk, fused0):
"""Return the per-lane multi-dim index list for ``src_br``.
If src's region shape matches dst's, fall through to ``get_indices``
(same as the legacy non-broadcast path). Otherwise derive each lane's
src index from the corresponding dst lane index via right-aligned
broadcast compat.
"""
src_st, src_ext = get_st_extent(src_br)
if tuple(int(e) for e in src_ext) == tuple(int(e) for e in dst_ext):
return [get_indices(fused0 + k, src_st, src_ext) for k in range(vec_chunk)]
return [
_broadcast_indices(dst_lane_indices[k], dst_st, dst_ext, src_st, src_ext)
for k in range(vec_chunk)
]
# -----------------------------------------------------------------------------
# Emit — packed
# -----------------------------------------------------------------------------
def _emit_packed(plan, vec_impl, vec_chunk, total, thread_cnt, sctx) -> PrimFunc:
extras = plan.extras
srcs = plan.srcs
dst_buf = plan.dst.buffer
dst_st, dst_ext = get_st_extent(plan.dst)
sync = emit_scope_sync(sctx.scope_kind)
n_outer = (total + vec_chunk * thread_cnt - 1) // (vec_chunk * thread_cnt)
@T.prim_func(check_well_formed=False)
def impl():
tid = _tid_expr(sctx)
for s in T.serial(0, n_outer):
# First lane's fused index for this thread, this chunk.
fused0 = T.meta_var(s * vec_chunk * thread_cnt + tid * vec_chunk)
# Predicate the call (skip the trailing partial chunk).
if fused0 + vec_chunk <= total:
dst_lane_indices = T.meta_var(
[get_indices(fused0 + k, dst_st, dst_ext) for k in range(vec_chunk)]
)
src_args = T.meta_var(
[
srcs[i].scalar
if srcs[i].is_scalar
else (
srcs[i].buf_region.buffer,
_src_lane_indices(
srcs[i].buf_region,
dst_lane_indices,
dst_st,
dst_ext,
vec_chunk,
fused0,
),
)
for i in range(len(srcs))
]
)
T.evaluate(vec_impl.emit(dst_buf, dst_lane_indices, src_args, extras))
sync()
return impl
# -----------------------------------------------------------------------------
# Emit — scalar fallback
# -----------------------------------------------------------------------------
def _emit_scalar(plan, spec, vec_chunk, total, thread_cnt, sctx) -> PrimFunc:
extras = plan.extras
srcs = plan.srcs
dst_buf = plan.dst.buffer
dst_st, dst_ext = get_st_extent(plan.dst)
dst_dtype = dst_buf.dtype
compute = spec.compute_scalar
sync = emit_scope_sync(sctx.scope_kind)
n_outer = (total + vec_chunk * thread_cnt - 1) // (vec_chunk * thread_cnt)
@T.prim_func(check_well_formed=False)
def impl():
tid = _tid_expr(sctx)
for s in T.serial(0, n_outer):
for vec in T.vectorized(vec_chunk):
fused = T.meta_var(s * vec_chunk * thread_cnt + tid * vec_chunk + vec)
if fused < total:
dst_idx = T.meta_var(get_indices(fused, dst_st, dst_ext))
src_vals = T.meta_var(
[fetch_src_value(src, fused, dst_idx, dst_st, dst_ext) for src in srcs]
)
dst_buf[tuple(dst_idx)] = T.cast(
compute(src_vals, extras, dst_dtype), dst_dtype
)
sync()
return impl
@@ -0,0 +1,40 @@
# 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.
"""Packed-vector emit functions for elementwise ops.
Each module here exposes one or more ``VecImpl`` instances that an ``OpSpec``
(in ``ops/``) attaches to its ``vec_impls`` list. ``reg.py``/``smem.py`` then
pick the widest matching one at dispatch time, mirroring how copy picks
``copy_{Nb}`` from a menu.
VecImpl emit contract:
emit(dst_buf, dst_lane_indices, src_args, extras) -> Expr
* dst_buf: Buffer
* dst_lane_indices: list[list[Expr]] of length ``vec_len``; each entry is the
multi-dim indices for one lane (precomputed by schedule).
* src_args[i]: one of
- Expr (scalar src — broadcast across all lanes)
- tuple (Buffer, list[list[Expr]] of length ``vec_len``) — buffer src
with per-lane indices
* extras: dict (rounding_mode, etc.)
Returns the PTX/CUDA call result; the schedule wraps in ``T.evaluate`` at
the call site. All Python-side shape branching (scalar vs buffer src) happens
in this emit function -- collapses the old 4x2 schema.py factory explosion.
"""
@@ -0,0 +1,97 @@
# 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.
"""Packed f32x2 VecImpls for binary add/sub/mul on sm_100+.
PTX op family: ``{add,sub,mul}.<rm>.ftz.f32x2``. Each call processes 2 f32s
per operand. The old ``_make_binary_packed_f32x2_factory`` (240+ lines, 8
``@T.prim_func`` shape combos per op) collapses to one ``emit`` per op
because operand-shape branching is now Python-level (outside any
``@T.prim_func``).
"""
from __future__ import annotations
from tvm.ir.expr import Expr
from tvm.script import tirx as T
from .._common import dtype_name, scalar_dtype
from ..ops import VecImpl
def _lane(arg, k):
"""Read lane ``k`` of one operand argument.
arg is either a scalar Expr (broadcast) or ``(Buffer, lane_indices)``.
"""
if isinstance(arg, tuple):
buf, lane_indices = arg
return buf[tuple(lane_indices[k])]
return arg
def _f32x2_applies(op_name):
"""Predicate: f32 dst+srcs, sm_100+, no broadcasting srcs, two srcs."""
def applies(op_call, sctx, plan):
from ...common import sm_version_ok
if dtype_name(plan.dst.buffer.dtype) != "float32":
return False, "dst dtype not f32"
if not sm_version_ok(op_call, sctx, min_version=100)[0]:
return False, "sm version < 100"
if len(plan.srcs) != 2:
return False, "binary requires 2 srcs"
for s in plan.srcs:
if s.is_scalar:
if scalar_dtype(s.scalar) != "float32":
return False, "scalar src dtype not f32"
else:
if dtype_name(s.buf_region.buffer.dtype) != "float32":
return False, "buffer src dtype not f32"
if s.index_fn is not None:
return False, "broadcasting src not supported by f32x2 packed"
return True, None
return applies
def _emit_binary_f32x2_for(op_name):
op_func = getattr(T.ptx, f"{op_name}_f32x2")
def emit(dst_buf, dst_lane_indices, src_args, extras) -> Expr:
a_arg, b_arg = src_args
rm = extras.get("rounding_mode", "rz")
return op_func(
T.address_of(dst_buf[tuple(dst_lane_indices[0])]),
T.cuda.make_float2(_lane(a_arg, 0), _lane(a_arg, 1)),
T.cuda.make_float2(_lane(b_arg, 0), _lane(b_arg, 1)),
rounding=rm,
ftz=True,
)
return emit
BINARY_F32X2_IMPLS: dict[str, VecImpl] = {
name: VecImpl(
vec_len=2,
applies=_f32x2_applies(name),
emit=_emit_binary_f32x2_for(name),
)
for name in ("add", "sub", "mul")
}
@@ -0,0 +1,92 @@
# 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.
"""Packed vec_len=2 cast via CUDA pair intrinsics.
Each supported (src_dtype, dst_dtype) pair has a CUDA builtin that converts a
packed-2 source to a packed-2 destination in one instruction
(e.g. ``__float22half2_rn``). The intrinsic takes pointers to the first
element of each packed pair on either side.
"""
from __future__ import annotations
from tvm.ir.expr import Expr
from tvm.script import tirx as T
from .._common import dtype_name
from ..ops import VecImpl
_VEC2_CAST_INTRINSICS = {
("float32", "float16"): "__float22half2_rn",
("float16", "float32"): "__half22float2",
("bfloat16", "float32"): "__bfloat1622float2",
("float32", "bfloat16"): "__float22bfloat162_rn",
}
_DTYPE_X2_NAME = {"float32": "float2", "float16": "half2", "bfloat16": "nv_bfloat162"}
def _intrinsic_name(src_dtype, dst_dtype):
return f"tvm_builtin_cast_{src_dtype}x2_{dst_dtype}x2"
def _intrinsic_source(src_dtype, dst_dtype):
intrinsic = _VEC2_CAST_INTRINSICS[(src_dtype, dst_dtype)]
return (
f"\n__forceinline__ __device__ void {_intrinsic_name(src_dtype, dst_dtype)}"
f"(void* dst, void* src) {{\n"
f" (({_DTYPE_X2_NAME[dst_dtype]}*)dst)[0] = "
f"{intrinsic}((({_DTYPE_X2_NAME[src_dtype]}*)src)[0]);\n"
"}\n"
)
def _cast_vec2_applies(op_call, sctx, plan):
if len(plan.srcs) != 1 or plan.srcs[0].is_scalar:
return False, "cast requires 1 buffer src"
src = plan.srcs[0]
if src.index_fn is not None:
return False, "broadcasting src not supported by cast vec2"
src_dtype = dtype_name(src.buf_region.buffer.dtype)
dst_dtype = dtype_name(plan.dst.buffer.dtype)
if (src_dtype, dst_dtype) not in _VEC2_CAST_INTRINSICS:
return False, f"no vec2 intrinsic for {src_dtype}->{dst_dtype}"
return True, None
def _emit_cast_vec2(dst_buf, dst_lane_indices, src_args, extras) -> Expr:
src_arg = src_args[0]
# cast_vec2 requires buffer src (guarded by applies()).
assert isinstance(src_arg, tuple), "cast vec2 src must be a buffer"
src_buf, src_lane_indices = src_arg
src_dtype = dtype_name(src_buf.dtype)
dst_dtype = dtype_name(dst_buf.dtype)
func_name = _intrinsic_name(src_dtype, dst_dtype)
source_code = _intrinsic_source(src_dtype, dst_dtype)
return T.cuda.func_call(
func_name,
T.address_of(dst_buf[tuple(dst_lane_indices[0])]),
T.address_of(src_buf[tuple(src_lane_indices[0])]),
source_code=source_code,
)
CAST_VEC2_IMPL = VecImpl(
vec_len=2,
applies=_cast_vec2_applies,
emit=_emit_cast_vec2,
)
@@ -0,0 +1,79 @@
# 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.
"""Packed f32x2 VecImpl for FMA on sm_100+.
PTX: ``fma.<rm>.ftz.f32x2 d, a, b, c`` — 2 f32 FMAs per call. Same Python-side
shape collapse as binary_f32x2.
"""
from __future__ import annotations
from tvm.ir.expr import Expr
from tvm.script import tirx as T
from .._common import dtype_name, scalar_dtype
from ..ops import VecImpl
from .binary_f32x2 import _lane
def _fma_f32x2_applies(op_call, sctx, plan):
from ...common import sm_version_ok
if dtype_name(plan.dst.buffer.dtype) != "float32":
return False, "dst dtype not f32"
if not sm_version_ok(op_call, sctx, min_version=100)[0]:
return False, "sm version < 100"
if len(plan.srcs) != 3:
return False, "fma requires 3 srcs"
a, b, c = plan.srcs
if a.is_scalar:
return False, "fma 'a' must be a buffer (no scalar-a packed FMA)"
if dtype_name(a.buf_region.buffer.dtype) != "float32":
return False, "src a dtype not f32"
if a.index_fn is not None:
return False, "broadcasting src a not supported"
for s in (b, c):
if s.is_scalar:
if scalar_dtype(s.scalar) != "float32":
return False, "scalar b/c dtype not f32"
else:
if dtype_name(s.buf_region.buffer.dtype) != "float32":
return False, "buffer b/c dtype not f32"
if s.index_fn is not None:
return False, "broadcasting src b/c not supported"
return True, None
def _emit_fma_f32x2(dst_buf, dst_lane_indices, src_args, extras) -> Expr:
a_arg, b_arg, c_arg = src_args
rm = extras.get("rounding_mode", "rz")
return T.ptx.fma_f32x2(
T.address_of(dst_buf[tuple(dst_lane_indices[0])]),
T.cuda.make_float2(_lane(a_arg, 0), _lane(a_arg, 1)),
T.cuda.make_float2(_lane(b_arg, 0), _lane(b_arg, 1)),
T.cuda.make_float2(_lane(c_arg, 0), _lane(c_arg, 1)),
rounding=rm,
ftz=True,
)
FMA_F32X2_IMPL = VecImpl(
vec_len=2,
applies=_fma_f32x2_applies,
emit=_emit_fma_f32x2,
)
@@ -0,0 +1,104 @@
# 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.
"""Execution scope utilities for CUDA op dispatches."""
from collections.abc import Callable
from tvm.script import tirx as T
from tvm.tirx import PrimFunc
from tvm.tirx.operator.tile_primitive import DispatchContext
from tvm.tirx.stmt import TilePrimitiveCall
def macro_or_prim_func(macro: Callable, need_macro: bool = False) -> Callable:
"""Wrap a macro in a ``prim_func`` unless the caller explicitly wants the macro."""
if need_macro:
return macro
@T.prim_func(check_well_formed=False)
def func():
macro()
return func
def thread_selector(sctx: DispatchContext, inner_impl, macro: bool = False) -> Callable:
"""Narrow execution to a single, deterministic thread within ``sctx.exec_scope``.
The elected thread is stable across invocations so that synchronization
primitives (for example PTX ``elect_sync``) behave correctly.
Parameters
----------
sctx : DispatchContext
The dispatch context. Only ``sctx.scope_kind`` is consulted; the
caller is responsible for having narrowed into the desired scope via an
``if`` guard with a canonical thread-filter predicate before reaching here.
inner_impl : T.inline
The body to execute inside the selected thread.
macro : bool
If True, return the macro directly; otherwise wrap it in a ``prim_func``.
"""
assert not isinstance(inner_impl, PrimFunc), "inner_impl must be a macro, not a PrimFunc"
name = sctx.scope_kind
if name == "thread":
return macro_or_prim_func(inner_impl, need_macro=macro)
if name == "cta":
@T.inline()
def impl():
T.lane_id([32])
if T.ptx.elect_sync():
inner_impl()
return macro_or_prim_func(impl, need_macro=macro)
if name == "warp":
@T.inline()
def impl():
T.lane_id([32])
if T.ptx.elect_sync():
inner_impl()
return macro_or_prim_func(impl, need_macro=macro)
if name == "warpgroup":
@T.inline()
def impl():
warp_id = T.warp_id_in_wg([4])
T.lane_id([32])
if warp_id == 0:
if T.ptx.elect_sync():
inner_impl()
return macro_or_prim_func(impl, need_macro=macro)
raise ValueError(f"thread_selector: unsupported exec_scope {name!r}")
def single_thread(op_call: TilePrimitiveCall, sctx: DispatchContext) -> bool:
"""Predicate for dispatchers that require a single-thread execution scope."""
del op_call
return sctx.is_thread
def exec_scope_ok(
op_call: TilePrimitiveCall, sctx: DispatchContext, expected_scopes: list[str]
) -> tuple[bool, str | None]:
"""Predicate helper: check that ``sctx.scope_kind`` is in *expected_scopes*."""
del op_call
ok = sctx.scope_kind in expected_scopes
return ok, None if ok else f"unsupported exec_scope {sctx.scope_kind}"
@@ -0,0 +1,25 @@
# 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.
"""CUDA synchronous ``gemm`` lowerings (warp-level ``mma.sync`` tensor core).
Importing this package registers every synchronous CUDA ``gemm`` dispatch
candidate as a side effect (each submodule calls ``register_dispatch`` at
import time). It is the synchronous counterpart to ``gemm_async``.
"""
from .mma_m16n8k_ import *
@@ -0,0 +1,595 @@
# 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.
"""Warp-level ``mma.sync`` GEMM lowering for the synchronous ``gemm`` op on CUDA."""
from dataclasses import dataclass
from tvm.arith.analyzer import Analyzer
from tvm.script import tirx as T
from tvm.tirx import PrimFunc
from tvm.tirx.layout import TileLayout
from tvm.tirx.operator.tile_primitive import (
DispatchContext,
fail,
predicate,
register_dispatch,
)
from tvm.tirx.stmt import TilePrimitiveCall
@dataclass(frozen=True)
class MmaInst:
"""One concrete mma.sync instruction we can emit. A given shape appears once
per dtype signature. Adding an instruction is just adding an entry to
MMA_INSTRUCTIONS; the feasibility checks below stay generic. The lane->coord
mapping is the fixed m16n8 family structure (hardcoded in the match); the
only per-instruction fragment detail is ``k_pack`` (it is not always
32/dtype_bits, so it is stored explicitly rather than derived)."""
name: str # distinguishing tag, e.g. "m16n8k16.bf16"
m: int
n: int
k: int
dtype: tuple[str, str, str, str] # the single (A, B, C, D) dtype signature
k_pack: int # contiguous-along-K elements packed into one register for A and B
# One entry per concrete instruction. The m16n8 family fixes M/N at 16/8; K is
# 16 or 8. f16/bf16 inputs with f32 accumulation, packing 2 bf16/f16 per b32
# along K (f16 accumulation, int8, fp8, etc. are added as more entries).
_BF16_F32 = ("bfloat16", "bfloat16", "float32", "float32")
_F16_F32 = ("float16", "float16", "float32", "float32")
MMA_INSTRUCTIONS = (
MmaInst("m16n8k16.bf16", 16, 8, 16, _BF16_F32, k_pack=2),
MmaInst("m16n8k16.f16", 16, 8, 16, _F16_F32, k_pack=2),
MmaInst("m16n8k8.bf16", 16, 8, 8, _BF16_F32, k_pack=2),
MmaInst("m16n8k8.f16", 16, 8, 8, _F16_F32, k_pack=2),
)
def _split(grouped, seps):
"""Split a grouped layout into one shard-only sub-layout per group, plus the
layout's offset (returned separately rather than distributed into the subs)."""
subs = [
TileLayout.from_iters(list(grouped.shard[seps[g] : seps[g + 1]]), [], {})
for g in range(len(seps) - 1)
]
return subs, dict(grouped.offset)
def _combine(layouts, offset):
"""Concatenate sub-layouts' shards and apply the separately tracked offset."""
shard = [it for lay in layouts for it in lay.shard]
return TileLayout.from_iters(shard, [], offset)
def _canon_perm(iters):
"""Permutation putting thread iters left, memory iters right; stride-desc within each."""
thr = sorted(
(i for i, it in enumerate(iters) if it.axis.is_thread()),
key=lambda i: -int(iters[i].stride),
)
mem = sorted(
(i for i, it in enumerate(iters) if not it.axis.is_thread()),
key=lambda i: -int(iters[i].stride),
)
return thr + mem
def _canon(layout):
"""Reorder an anchor sub-layout into canonical (thread-left/memory-right) order."""
return layout.permute_dims(_canon_perm(list(layout.shard)))
def _align(anchor, follower, name):
"""Regroup follower by anchor's iter extents, then permute its groups to follow
the anchor's canonical order (follower keeps its own strides)."""
extents = [int(it.extent) for it in anchor.shard]
perm = _canon_perm(list(anchor.shard))
try:
grp, seps = follower.group(extents)
except Exception as e: # follower dim layout incompatible with anchor decomposition
fail(f"gemm mma: {name} not alignable to anchor extents {extents}: {e}")
return grp.permute_by_groups(seps, perm)
def _region_totals(layout):
"""(product of thread-axis iter extents, product of memory-axis iter extents).
Computed once on each anchor to fix that dim's thread/memory region lengths;
followers reuse the anchor's split rather than re-deriving it from their own
iters (which can misclassify -- e.g. B's N, whose register slot is actually a
lane iter, would otherwise report memory length 1)."""
thr, mem = 1, 1
for it in layout.shard:
if it.axis.is_thread():
thr *= int(it.extent)
else:
mem *= int(it.extent)
return thr, mem
def _frag_group(layout, lane, mem, thread_total, mem_total):
"""Group one logical-dim sub-layout into the fragment shape, then optionally
verify it.
``lane`` and ``mem`` are lists of ``(extent, stride, want_thread)``;
``thread_total`` and ``mem_total`` are this dim's thread/memory region lengths
(from its anchor via _region_totals). The group shape is
``[thread_total // prod(lane), *lane, mem_total // prod(mem), *mem]``: the lane
extents are carved off the thread region and the mem extents off the memory
region (innermost last, e.g. ``[(reg, ...)]`` for an accumulator dim or
``[(kHi, ...), (k_pack, ...)]`` for A/B's K). The input must already be in
canonical (thread-left/memory-right) order.
Every carved group is verified: it must be a single iter, its axis must be a
thread axis when ``want_thread`` else a memory axis (only is_thread is checked,
since scope varies the exact thread axis; e.g. B's N register slot is actually
a lane, so want_thread=True there), and a non-None ``stride`` pins that iter's
stride. Raises on a tiling or verification failure, so a non-matching caller
layout is declined via the caller's try/except.
"""
lane_ext = [e for e, _, _ in lane]
mem_ext = [e for e, _, _ in mem]
lane_prod, mem_prod = 1, 1
for e in lane_ext:
lane_prod *= e
for e in mem_ext:
mem_prod *= e
grouped, seps = layout.group(
[thread_total // lane_prod, *lane_ext, mem_total // mem_prod, *mem_ext]
)
# group order: [thread_rest, *lane (from idx 1), mem_rest, *mem (after)].
specs = [(1 + j, s, t) for j, (_, s, t) in enumerate(lane)]
specs += [(2 + len(lane) + j, s, t) for j, (_, s, t) in enumerate(mem)]
for idx, stride, want_thread in specs:
grp = grouped.shard[seps[idx] : seps[idx + 1]]
if len(grp) != 1:
raise ValueError(f"frag group {idx} is not a single iter")
if int(grp[0].extent) == 1:
# An extent-1 group iterates nothing, so its axis/stride is
# meaningless and gets dropped downstream (cf. _same_iters /
# _reg_layout). This is the kHi == 1 case of m16n8k8: there is a
# single high-K register group, which .group() may materialize as a
# degenerate split of the (thread) lane axis.
continue
if grp[0].axis.is_thread() != want_thread:
raise ValueError(f"frag group {idx} thread/memory axis mismatch")
if stride is not None and int(grp[0].stride) != stride:
raise ValueError(f"frag group {idx} stride {int(grp[0].stride)} != {stride}")
return grouped, seps
def _grp(grouped, seps, i):
"""The iters of group ``i`` of a grouped layout: ``shard[seps[i]:seps[i+1]]``."""
return grouped.shard[seps[i] : seps[i + 1]]
def _ext(iters):
"""Product of the extents of an iter list."""
p = 1
for it in iters:
p *= int(it.extent)
return p
def _reg_layout(groups, offset):
"""Per-thread register layout from per-logical-dim iter groups (dropping
thread-axis offset terms), plus the matching local-view shape (each dim is
the product of that group's extents).
Extent-1 iters are dropped from the layout: they iterate nothing (offset
always 0, so harmless to the mapping) but would otherwise pin a degenerate
axis -- e.g. B's N has no real register, so its "register" slot is a single
extent-1 lane iter that must not make the register buffer thread-axis."""
iters = [it for g in groups for it in g if int(it.extent) != 1]
layout = TileLayout.from_iters(
iters, [], {ax: v for ax, v in offset.items() if not ax.is_thread()}
)
return layout, [_ext(g) for g in groups]
def _same_iters(a, b):
"""True iff iter lists ``a`` and ``b`` match elementwise on (extent, stride,
axis), ignoring extent-1 iters (they iterate nothing, so their stride/axis is
meaningless -- e.g. the degenerate thread-rest left by a group shape's '1')."""
a = [it for it in a if int(it.extent) != 1]
b = [it for it in b if int(it.extent) != 1]
if len(a) != len(b):
return False
return all(
int(x.extent) == int(y.extent)
and int(x.stride) == int(y.stride)
and x.axis.name == y.axis.name
for x, y in zip(a, b, strict=True)
)
def _full_active_lanes(op: TilePrimitiveCall, sctx: DispatchContext):
"""The active thread set (sctx.intra) must be complete and un-narrowed.
mma.sync.aligned is collective over every active thread; an enclosing if
that narrows any intra axis makes the .aligned instruction undefined. So
each intra axis must be at offset 0 with its full extent: laneid=32,
wid_in_wg=4 (warpgroup), and warpid=warps-per-CTA (cta) from the launch
config. Any other axis (e.g. cta_id at cluster scope) is not supported.
"""
full = {"laneid": 32, "wid_in_wg": 4}
if "warpid" in sctx.intra:
tx = sctx.launch_params.get("threadIdx.x")
if tx is None:
return False, "cta scope needs threadIdx.x in launch_params"
try:
full["warpid"] = int(tx.dom.extent) // 32
except (TypeError, ValueError):
return False, f"non-static threadIdx.x extent {tx.dom.extent}"
for axis, rng in sctx.intra.items():
if axis not in full:
return False, f"unsupported active-set axis {axis!r}"
extent, offset = int(rng[0]), int(rng[1])
if extent != full[axis] or offset != 0:
return False, (
f"active {axis} is [{offset}, {offset + extent}), need full [0, {full[axis]})"
)
return True
def _no_replica(op: TilePrimitiveCall, sctx: DispatchContext):
"""All operand layouts must have no replica (no broadcast/duplicated axes)."""
for region, name in zip(op.args[:4], ("D", "A", "B", "C")):
if region.buffer.layout.replica:
return False, f"{name} layout has replica {region.buffer.layout.replica}"
return True
@register_dispatch(
"gemm",
"cuda",
variant="mma.m16n8k*",
priority=10,
when=[
predicate("full_active_lanes", _full_active_lanes),
predicate("no_replica", _no_replica),
],
)
def gemm_cuda_mma_dispatch(op: TilePrimitiveCall, sctx: DispatchContext) -> PrimFunc:
"""``gemm`` -> warp-level ``mma.sync`` of the m16n8k* family.
This is the ``"mma.m16n8k*"`` variant. It targets the m16n8k* tensor-core
instructions -- currently m16n8k16 / m16n8k8 with bf16/f16 inputs and f32
accumulation (see MMA_INSTRUCTIONS); other K (and other shapes/dtypes) are
added as more entries. Pure-register path: A/B fragments and C/D
accumulators all live in registers.
"""
# gemm op args: D = alpha * A @ B + beta * C
# D (args[0]) is the output; C (args[3]) is the beta-accumulator input.
D_region, A_region, B_region, C_region, transpose_A, transpose_B, alpha, beta = op.args
D, A, B, C = D_region.buffer, A_region.buffer, B_region.buffer, C_region.buffer
# Pure-register mma path: A/B fragments and C/D accumulators all live in
# registers ("local"). The caller is responsible for staging A/B into
# registers (e.g. via ldmatrix) beforehand.
for buf, name in ((D, "D"), (A, "A"), (B, "B"), (C, "C")):
if buf.scope() != "local":
fail(f"gemm mma requires {name} in register (local) scope, got {buf.scope()}")
# transpose_A/transpose_B only describe the input's logical orientation; we
# normalize to the standard form A=[M,K], B=[K,N] (D/C are always [M,N]).
# transpose_A: False -> buffer is [M,K], True -> [K,M]
# transpose_B: False -> buffer is [K,N], True -> [N,K]
# The .row.col K-major requirement is not enforced here -- it is checked
# later by the per-instruction fragment match against the real layout.
analyzer = Analyzer()
# mma.sync computes D = A·B + C natively (no scalar scaling), so we support
# only alpha=1 and beta in {0, 1}; beta selects whether C is the accumulator
# (1 -> c_ptr=C, 0 -> c_ptr=0). General alpha/beta is declined.
def _const_scalar(expr):
s = analyzer.simplify(expr)
try:
return float(s.value)
except (AttributeError, TypeError, ValueError):
return None
if _const_scalar(alpha) != 1.0:
fail(f"gemm mma supports only alpha=1, got alpha={alpha}")
if _const_scalar(beta) not in (0.0, 1.0):
fail(f"gemm mma supports only beta in {{0, 1}}, got beta={beta}")
def _mat_extents(region, name):
ext = [r.extent for r in region.region if not analyzer.can_prove_equal(r.extent, 1)]
if len(ext) != 2:
fail(f"gemm mma expects 2D {name}, got non-unit extents {ext}")
return ext
A_ext = _mat_extents(A_region, "A")
B_ext = _mat_extents(B_region, "B")
D_M, D_N = _mat_extents(D_region, "D")
C_M, C_N = _mat_extents(C_region, "C")
M, K = (A_ext[1], A_ext[0]) if transpose_A else (A_ext[0], A_ext[1])
B_K, N = (B_ext[1], B_ext[0]) if transpose_B else (B_ext[0], B_ext[1])
assert analyzer.can_prove_equal(B_K, K), f"gemm mma: A K={K} != B K={B_K}"
assert analyzer.can_prove_equal(D_M, M) and analyzer.can_prove_equal(D_N, N), (
f"gemm mma: D dims ({D_M}, {D_N}) != (M={M}, N={N})"
)
assert analyzer.can_prove_equal(C_M, M) and analyzer.can_prove_equal(C_N, N), (
f"gemm mma: C dims ({C_M}, {C_N}) != (M={M}, N={N})"
)
# Tiling into instructions needs static extents.
def _const(expr, name):
try:
return int(analyzer.simplify(expr))
except (TypeError, ValueError):
fail(f"gemm mma needs static {name} extent, got {expr}")
M, N, K = _const(M, "M"), _const(N, "N"), _const(K, "K")
# Slice each operand's layout to its region, then group it into its 2D
# buffer-order shape; the split below maps those groups to standard
# (M,K)/(K,N). group() raises if the layout can't be tiled that way, so a
# caller layout that doesn't match the operand shape is declined cleanly.
def _slice_group(buf, region, shape2d, name):
# slice() itself groups internally, so both slice and group can raise the
# ICHECK when the layout can't be tiled as shape2d -- guard both.
canon = None
try:
sliced = buf.layout.slice(buf.shape, region.region)
if sliced is not None:
canon = sliced.canonicalize()
except Exception as e: # ICHECK failure -> layout not tileable as shape2d
fail(f"gemm mma: {name} layout not tileable as {tuple(shape2d)}: {e}")
if canon is None:
fail(f"gemm mma: cannot slice {name} layout to its region")
# All thread iters must share one thread axis (e.g. all laneid). _frag_group
# only checks is_thread (not the exact axis, since scope varies), so a layout
# mixing two thread axes (e.g. laneid + wid_in_wg) would carve ambiguously --
# decline it here, like ldmatrix validating its thread structure.
thread_axes = {it.axis.name for it in canon.shard if it.axis.is_thread()}
if len(thread_axes) > 1:
fail(f"gemm mma: {name} has >1 thread axis {sorted(thread_axes)}, only one supported")
try:
return canon.group(list(shape2d))
except Exception as e: # ICHECK failure -> layout not tileable as shape2d
fail(f"gemm mma: {name} layout not tileable as {tuple(shape2d)}: {e}")
A_grouped, A_seps = _slice_group(A, A_region, (K, M) if transpose_A else (M, K), "A")
B_grouped, B_seps = _slice_group(B, B_region, (N, K) if transpose_B else (K, N), "B")
C_grouped, C_seps = _slice_group(C, C_region, (M, N), "C")
D_grouped, D_seps = _slice_group(D, D_region, (M, N), "D")
# Split each operand into per-logical-dim sub-layouts (+ its offset), mapping
# the buffer-order subs to standard (M,K)/(K,N) per the transpose flags.
(DM, DN), D_off = _split(D_grouped, D_seps)
(CM, CN), C_off = _split(C_grouped, C_seps)
A_subs, A_off = _split(A_grouped, A_seps)
B_subs, B_off = _split(B_grouped, B_seps)
AM, AK = (A_subs[1], A_subs[0]) if transpose_A else (A_subs[0], A_subs[1])
BK, BN = (B_subs[1], B_subs[0]) if transpose_B else (B_subs[0], B_subs[1])
# Anchor-align so every operand decomposes each shared logical dim the same
# way: M anchor = DM -> AM, CM ; N anchor = DN -> BN, CN ; K anchor = AK -> BK.
# _align uses each anchor's raw (pre-canon) per-iter extents to group the
# follower and reorders the follower's groups into the anchor's canonical
# order, so the followers come out canonical. Canon the anchors themselves
# afterwards. Every sub-layout is then in canonical (thread-left/memory-right)
# order before the loop, so _frag_group groups directly without re-canon.
AM = _align(DM, AM, "A.M")
CM = _align(DM, CM, "C.M")
BN = _align(DN, BN, "B.N")
CN = _align(DN, CN, "C.N")
BK = _align(AK, BK, "B.K")
DM, DN, AK = _canon(DM), _canon(DN), _canon(AK)
# Each dim's thread/memory region lengths, fixed once from its anchor (3
# anchors x 2 parts = 6 lengths). Every operand of that dim reuses them in
# _frag_group, so a follower whose register slot is actually a lane (B's N)
# still gets the anchor's memory length instead of its own (mis)classified one.
m_thr, m_mem = _region_totals(DM)
n_thr, n_mem = _region_totals(DN)
k_thr, k_mem = _region_totals(AK)
# Per-instruction selection: try each candidate in order and use the first
# whose shape / dtype / (later) fragment layout all fit. A failing check just
# moves on to the next instruction; if none fit, decline.
sig = (str(A.dtype), str(B.dtype), str(C.dtype), str(D.dtype))
for inst in MMA_INSTRUCTIONS:
assert inst.m % 8 == 0 and inst.n % 8 == 0 and inst.k % 8 == 0, (
f"mma instruction {inst.name} m/n/k must be multiples of 8"
)
if M % inst.m or N % inst.n or K % inst.k:
continue
if sig != inst.dtype:
continue
# Group every operand into this instruction's fragment shape. The m16n8
# lane split is g (8 lanes) along M and t (4 lanes) along N/K:
# C/D accumulator: M = g + 8*rM (inst.m//8 regs), N = 2*t + rN (inst.n//4 regs)
# A multiplicand: M as C/D's M, K = 2*t + p + 8*kHi
# B multiplicand: K as A's K, N = g (lane 8, no reg: B has no M so N
# reuses the 8-lane g group)
# so K's memory tail is [kHi, k_pack] with k_pack the innermost (stride-1)
# contiguous pack and kHi = inst.k // (4 * k_pack) high-K register groups.
# _frag_group raises if the caller layout can't be tiled or (when any
# stride is given) fails the fragment checks -> move on to the next
# instruction. lane/mem are [(extent, stride), ...]: the lane stride pins
# the laneid stride (g=4, t=1), a mem stride pins a register iter (rN /
# k_pack = 1; rM / kHi free = None). C shares D's accumulator fragment and
# B's K shares A's K. B's N is the pure 8-lane g group, but aligned to the
# accumulator's N (lane 4 + reg 2) it splits into lane g_hi (4, laneid
# stride 8) and a "register" g_lo (2, laneid stride 4) -- a lane iter in the
# register slot (B's N has no real register). Region lengths come from the
# per-dim anchor (m/n/k _thr,_mem), so this split still tiles correctly.
# _frag_group(layout, lane, mem, thread_total, mem_total); each carve is
# (extent, stride, want_thread): lanes are thread, registers memory, except
# B.N's register slot (g_lo) which is itself a lane (want_thread=True).
kHi = inst.k // (4 * inst.k_pack)
try:
DM_g, DM_seps = _frag_group(
DM, [(8, 4, True)], [(inst.m // 8, None, False)], m_thr, m_mem
)
DN_g, DN_seps = _frag_group(DN, [(4, 1, True)], [(inst.n // 4, 1, False)], n_thr, n_mem)
CM_g, CM_seps = _frag_group(
CM, [(8, 4, True)], [(inst.m // 8, None, False)], m_thr, m_mem
)
CN_g, CN_seps = _frag_group(CN, [(4, 1, True)], [(inst.n // 4, 1, False)], n_thr, n_mem)
AM_g, AM_seps = _frag_group(
AM, [(8, 4, True)], [(inst.m // 8, None, False)], m_thr, m_mem
)
AK_g, AK_seps = _frag_group(
AK, [(4, 1, True)], [(kHi, None, False), (inst.k_pack, 1, False)], k_thr, k_mem
)
BK_g, BK_seps = _frag_group(
BK, [(4, 1, True)], [(kHi, None, False), (inst.k_pack, 1, False)], k_thr, k_mem
)
BN_g, BN_seps = _frag_group(BN, [(4, 8, True)], [(2, 4, True)], n_thr, n_mem)
except Exception:
continue
# M.to (M's warp tiling, group 0) must match across D, A, C so the same
# logical M-block lands on the same warp in all three operands.
m_to = _grp(DM_g, DM_seps, 0)
if not (
_same_iters(m_to, _grp(AM_g, AM_seps, 0)) and _same_iters(m_to, _grp(CM_g, CM_seps, 0))
):
continue
# N.to (N's warp tiling, group 0) must match across D, B, C so the same
# logical N-block lands on the same warp in all three operands.
n_to = _grp(DN_g, DN_seps, 0)
if not (
_same_iters(n_to, _grp(BN_g, BN_seps, 0)) and _same_iters(n_to, _grp(CN_g, CN_seps, 0))
):
continue
# K.to (K's warp tiling, group 0) must match across A, B so the same
# logical K-block lands on the same warp in both operands.
if not _same_iters(_grp(AK_g, AK_seps, 0), _grp(BK_g, BK_seps, 0)):
continue
break
else:
fail(f"no mma instruction fits M={M}, N={N}, K={K}, dtypes={sig}")
# Per-operand register layout + matching local-view shape, grouped per logical
# dim (offset drops thread-axis terms). Iter order = shape dim order:
# D/C -> [M.mo, N.mo, rM, rN] A -> [M.mo, K.mo, rM, kHi, k_pack]
# B -> [K.mo, N.mo, kHi, k_pack] (k_pack innermost / contiguous)
D_reg, d_shape = _reg_layout(
[
_grp(DM_g, DM_seps, 2),
_grp(DN_g, DN_seps, 2),
_grp(DM_g, DM_seps, 3),
_grp(DN_g, DN_seps, 3),
],
D_off,
)
C_reg, c_shape = _reg_layout(
[
_grp(CM_g, CM_seps, 2),
_grp(CN_g, CN_seps, 2),
_grp(CM_g, CM_seps, 3),
_grp(CN_g, CN_seps, 3),
],
C_off,
)
A_reg, a_shape = _reg_layout(
[
_grp(AM_g, AM_seps, 2),
_grp(AK_g, AK_seps, 2),
_grp(AM_g, AM_seps, 3),
_grp(AK_g, AK_seps, 3),
_grp(AK_g, AK_seps, 4),
],
A_off,
)
B_reg, b_shape = _reg_layout(
[
_grp(BK_g, BK_seps, 2),
_grp(BN_g, BN_seps, 2),
_grp(BK_g, BK_seps, 3),
_grp(BK_g, BK_seps, 4),
],
B_off,
)
# Emit one mma per (m, n) output tile, accumulating over K. The tile / init /
# K loops use T.unroll: the UnrollLoop pass fully expands them in TIR (their
# bounds are compile-time constants), so the local-buffer indices resolve to
# static register slots -- mma register operands must be constant.
#
# mma is d = a·b + c. D's accumulator is initialized once per output tile --
# copying C when beta==1, clearing to 0 when beta==0 -- then every K step
# accumulates in place with c = d, giving a single uniform mma form.
M_tiles, N_tiles, K_tiles = d_shape[0], d_shape[1], a_shape[1]
shape_str = f"m{inst.m}n{inst.n}k{inst.k}"
a_type, b_type, c_type, d_type = inst.dtype
use_c = _const_scalar(beta) == 1.0
# Per-register counts in the fixed PTX enumeration order (derived from the
# instruction, NOT hardcoded, so m16n8k8 with kHi==1 also works):
# D/C accumulator: rM = inst.m // 8 regs along M, rN = inst.n // 4 along N
# c_id = 2 * rM + rN (4 f32 for m16n8k16, also 4 for k8)
# A multiplicand: rM = inst.m // 8, kHi = inst.k // (4 * inst.k_pack)
# b32 = rM + 2 * kHi (4 b32 for k16, 2 b32 for k8)
# B multiplicand: kHi = inst.k // (4 * inst.k_pack)
# b32 = kHi (2 b32 for k16, 1 b32 for k8)
n_rM = inst.m // 8
n_rN = inst.n // 4
n_kHi = inst.k // (4 * inst.k_pack)
@T.prim_func(check_well_formed=False)
def impl():
d_local = D.local(*d_shape, layout=D_reg)
c_local = C.local(*c_shape, layout=C_reg)
a_local = A.local(*a_shape, layout=A_reg)
b_local = B.local(*b_shape, layout=B_reg)
for m in T.unroll(M_tiles):
for n in T.unroll(N_tiles):
# Initialize D[m, n]: copy C (beta==1) or clear to 0 (beta==0).
for rM in T.unroll(n_rM):
for rN in T.unroll(n_rN):
if use_c:
d_local[m, n, rM, rN] = c_local[m, n, rM, rN]
else:
d_local[m, n, rM, rN] = T.float32(0)
# Accumulate over K in place: d = a·b + d.
for k in T.unroll(K_tiles):
# D: 4 f32 in PTX order c_id = 2*rM + rN.
d_ptrs = [
d_local.ptr_to([m, n, rM, rN]) for rM in range(n_rM) for rN in range(n_rN)
]
# A: b32 regs in PTX order b32 = rM + 2*kHi (kHi outer, rM inner).
a_ptrs = [
a_local.ptr_to([m, k, rM, kHi, 0])
for kHi in range(n_kHi)
for rM in range(n_rM)
]
# B: b32 regs in PTX order b32 = kHi.
b_ptrs = [b_local.ptr_to([k, n, kHi, 0]) for kHi in range(n_kHi)]
# Accumulate in place into D's own regs: c = d.
T.ptx.mma(
shape_str,
"row",
"col",
d_type,
a_type,
b_type,
c_type,
d_ptrs,
a_ptrs,
b_ptrs,
d_ptrs,
)
return impl
@@ -0,0 +1,18 @@
# 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.
from .tcgen05 import *
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,62 @@
# 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.
"""GEMM-related utilities for CUDA op dispatches."""
from tvm.arith.analyzer import Analyzer
from tvm.tirx import Buffer
from tvm.tirx.operator.tile_primitive import DispatchContext
from tvm.tirx.stmt import TilePrimitiveCall
def validate_gemm_op(op_call: TilePrimitiveCall, sctx: DispatchContext) -> bool:
"""Sanity check for gemm op"""
C_buffer_region, A_buffer_region, B_buffer_region = op_call.args[:3]
C: Buffer = C_buffer_region.buffer
A: Buffer = A_buffer_region.buffer
B: Buffer = B_buffer_region.buffer
if not (C.layout and A.layout and B.layout and A.dtype == B.dtype):
return False
# Extract regions and validate dimensions
analyzer = Analyzer()
C_region, A_region, B_region = (
C_buffer_region.region,
A_buffer_region.region,
B_buffer_region.region,
)
# Extract extents and validate non-unit dimensions match
transA, transB = op_call.args[3:5]
C_extent_ = [r.extent for r in C_region if r.extent != 1]
A_extent_ = [r.extent for r in A_region if r.extent != 1]
B_extent_ = [r.extent for r in B_region if r.extent != 1]
assert len(C_extent_) == len(A_extent_) == len(B_extent_) == 2, (
"Only 2D C, A, B are supported for gemm"
)
if transA:
A_extent_ = [A_extent_[1], A_extent_[0]]
if transB:
B_extent_ = [B_extent_[1], B_extent_[0]]
# C: MxN, A: MxK, B: NxK
if not all(
[
analyzer.can_prove_equal(C_extent_[0], A_extent_[0]),
analyzer.can_prove_equal(C_extent_[1], B_extent_[0]),
analyzer.can_prove_equal(A_extent_[1], B_extent_[1]),
]
):
return False
return True
@@ -0,0 +1,326 @@
# 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.
"""Layout analysis utilities for local-memory op dispatches.
Provides functions for analyzing TileLayout thread/local partitions,
computing local region info, layout signature comparison, and thread
variable resolution. Used by cast.py, unary.py, and binary.py.
"""
import functools
import operator
from collections import defaultdict
from tvm.arith import Analyzer
from tvm.tirx.layout import TileLayout
def get_sublayout_from_region(layout, buffer_shape, region_st, region_extent):
"""Get sublayout by slicing the layout with the buffer region.
Args:
layout: The buffer's TileLayout.
buffer_shape: The buffer's shape.
region_st: Region start indices.
region_extent: Region extents.
Returns:
Sublayout if slicing succeeds, otherwise the original layout.
"""
if not layout:
return layout
region = [(region_st[i], region_st[i] + region_extent[i]) for i in range(len(region_st))]
sliced = layout.slice(list(buffer_shape), region)
return sliced if sliced is not None else layout
def get_layout_thread_local_partition(layout):
"""Extract thread and local dimension info from layout.
Returns:
tuple | None: On success, (thread_groups, local_dim_indices, local_extents).
- thread_groups: dict {axis: (dim_indices, extents)} for each thread axis
- local_dim_indices: list of dimension indices for local (memory) axes
- local_extents: list of extents for local dimensions
Returns None if layout is not supported.
Validates:
- No stride==0 on thread dims (broadcast/overlap = cross-thread semantics)
- Local dims may have arbitrary strides (alignment uses actual layout strides)
- No thread axes in replica
Example:
Layout (2, 8, 4, 2):(2@warpid, 4@laneid, 1@laneid, 1@m) returns:
- thread_groups = {warpid: ([0], [2]), laneid: ([1, 2], [8, 4])}
- local_dim_indices = [3], local_extents = [2]
"""
if not isinstance(layout, TileLayout):
return None
shard = getattr(layout, "shard", None)
if not shard:
return None
# Partition dimensions into thread and local (memory) axes
thread_dim_indices = [i for i, it in enumerate(shard) if it.axis.is_thread()]
local_dim_indices = [i for i, it in enumerate(shard) if not it.axis.is_thread()]
if not thread_dim_indices or not local_dim_indices:
return None
analyzer = Analyzer()
for idx in thread_dim_indices:
if analyzer.can_prove_equal(shard[idx].stride, 0):
return None
# Replica must not contain thread axes
replica = getattr(layout, "replica", None)
if replica and any(it.axis.is_thread() for it in replica):
return None
# Group thread dimensions by axis
thread_groups_dict = defaultdict(list)
for idx in thread_dim_indices:
thread_groups_dict[shard[idx].axis].append(idx)
thread_groups = {}
for axis, dim_indices in thread_groups_dict.items():
dim_indices = sorted(dim_indices)
extents = [shard[i].extent for i in dim_indices]
thread_groups[axis] = (dim_indices, extents)
local_extents = [shard[i].extent for i in local_dim_indices]
return (thread_groups, local_dim_indices, local_extents)
def cast_layout_supported_for_local(layout) -> bool:
"""Check that layout is valid for local cast (warp/warpgroup/cta/cluster):
filter out cross-thread semantics."""
return get_layout_thread_local_partition(layout) is not None
def get_local_region(orig_layout: TileLayout, buffer_shape, region_st, region_extent):
"""Compute local storage shape, iteration starts, and extents with validation of region.
Args:
orig_layout: The original (unsliced) TileLayout.
buffer_shape: The buffer shape.
region_st: Region start in shape space.
region_extent: Region extent in shape space.
Returns:
(local_shape, local_st, local_ext), or ([1], [0], [1]) if no local dims.
Returns None if the region is invalid (non-contiguous slicing).
- local_shape: full storage extents per local dim.
- local_st: region start per local dim.
- local_ext: region extent per local dim.
Example:
Layout (2, 8, 4, 2):(8@m, 2@laneid, 2@m, 1@m), Shape [16, 8], Region [8:16, :] returns:
- local_shape = [2, 8], local_st = [1, 0], local_ext = [1, 8]
"""
grouped, seps = orig_layout.group(list(buffer_shape))
local_shape = []
local_st = []
local_ext = []
analyzer = Analyzer()
for d in range(len(buffer_shape)):
shard_range = list(range(seps[d], seps[d + 1]))
has_local = any(not grouped.shard[s].axis.is_thread() for s in shard_range)
if not has_local:
continue
has_thread = any(grouped.shard[s].axis.is_thread() for s in shard_range)
if not has_thread:
# Pure local shape dim: use shape-level values directly.
local_shape.append(buffer_shape[d])
local_st.append(region_st[d])
local_ext.append(region_extent[d])
else:
# Decompose start element
remaining_st = region_st[d]
st_coords = []
for i, s_idx in enumerate(shard_range):
sub_prod = 1
for j in range(i + 1, len(shard_range)):
sub_prod = sub_prod * grouped.shard[shard_range[j]].extent
st_coords.append(remaining_st // sub_prod)
remaining_st = remaining_st % sub_prod
# Decompose end element
remaining_end = region_st[d] + region_extent[d] - 1
end_coords = []
for i, s_idx in enumerate(shard_range):
sub_prod = 1
for j in range(i + 1, len(shard_range)):
sub_prod = sub_prod * grouped.shard[shard_range[j]].extent
end_coords.append(remaining_end // sub_prod)
remaining_end = remaining_end % sub_prod
# check the rectangularity and contiguity of the sliced region
cur_local_shape, cur_local_st, cur_local_end = 1, 0, 0
for k in reversed(range(len(st_coords))):
if grouped.shard[seps[d] + k].axis.is_thread():
# for thread dims, region must be contiguous and span full extent
if not (
analyzer.can_prove_equal(st_coords[k], 0)
and analyzer.can_prove_equal(
end_coords[k], grouped.shard[seps[d] + k].extent - 1
)
):
return None
else:
if not analyzer.can_prove_equal(end_coords[k] - st_coords[k], 1) and not (
analyzer.can_prove_equal(st_coords[k], 0)
and analyzer.can_prove_equal(
end_coords[k], grouped.shard[seps[d] + k].extent - 1
)
):
# to ensure contiguity, if the region spans multiple values
# in this dim, it must span the full extent
return None
cur_local_shape *= grouped.shard[seps[d] + k].extent
cur_local_st = cur_local_st * grouped.shard[seps[d] + k].extent + st_coords[k]
cur_local_end = (
cur_local_end * grouped.shard[seps[d] + k].extent + end_coords[k]
)
# double check the validity of the sliced region
assert region_extent[d] == functools.reduce(
operator.mul, [end - st + 1 for st, end in zip(st_coords, end_coords)], 1
)
# append the local info without thread dims
local_shape.append(cur_local_shape)
local_st.append(cur_local_st)
local_ext.append(cur_local_end - cur_local_st + 1)
if not local_shape:
return [1], [0], [1] # treat no local dim case as 1D local shape with 1 element
return local_shape, local_st, local_ext
def compute_linear_offset(region_st, local_dims, layout):
"""Compute linear offset using layout's actual strides.
Physical offset = sum(region_st[dim] * layout.shard[dim].stride) for all local dims.
"""
offset = 0
for dim_idx in local_dims:
offset = offset + region_st[dim_idx] * layout.shard[dim_idx].stride
return offset
def _axis_key(axis):
if hasattr(axis, "name") and axis.name:
return str(axis.name)
return str(axis)
def layout_signature(layout):
"""Return semantic signature from canonicalized TileLayout.
Returns (thread_sig, local_sig, replica_sig).
Each sig is a list of (axis_key, extent, stride) in shard/replica order.
"""
if not isinstance(layout, TileLayout):
return None
shard = getattr(layout, "shard", None)
if not shard:
return None
thread_sig = []
local_sig = []
for it in shard:
item = (_axis_key(it.axis), it.extent, it.stride)
if it.axis.is_thread():
thread_sig.append(item)
else:
local_sig.append(item)
replica_sig = []
replica = getattr(layout, "replica", None) or []
for it in replica:
replica_sig.append((_axis_key(it.axis), it.extent, it.stride))
return (thread_sig, local_sig, replica_sig)
def sig_equal(analyzer: Analyzer, src_sig, dst_sig) -> bool:
"""Compare two layout signatures with semantic equality (Analyzer).
Signatures come from layout_signature(layout) and are:
(thread_sig, local_sig, replica_sig)
Each sig element is (axis_key, extent, stride).
"""
if src_sig is None or dst_sig is None:
return False
src_thread_sig, src_local_sig, src_replica_sig = src_sig
dst_thread_sig, dst_local_sig, dst_replica_sig = dst_sig
if len(src_thread_sig) != len(dst_thread_sig):
return False
if len(src_local_sig) != len(dst_local_sig):
return False
if len(src_replica_sig) != len(dst_replica_sig):
return False
def _list_equal(a_list, b_list) -> bool:
for (a_key, a_ext, a_str), (b_key, b_ext, b_str) in zip(a_list, b_list):
if a_key != b_key:
return False
if not analyzer.can_prove_equal(a_ext, b_ext):
return False
if not analyzer.can_prove_equal(a_str, b_str):
return False
return True
return (
_list_equal(src_thread_sig, dst_thread_sig)
and _list_equal(src_local_sig, dst_local_sig)
and _list_equal(src_replica_sig, dst_replica_sig)
)
def resolve_thread_var(axis, sctx):
"""Map the axis to the corresponding thread variable."""
axis_name = getattr(axis, "name", None)
if not axis_name:
try:
axis_name = str(axis)
except Exception:
axis_name = ""
for key, itervar in sctx.launch_params.items():
if getattr(itervar.var, "name", "") == axis_name:
return itervar.var
if axis_name:
axis_name_lower = axis_name.lower()
for key in sctx.launch_params:
if axis_name_lower in key.lower() or (axis_name == "tx" and "threadIdx.x" in key):
return sctx.launch_params[key].var
if "threadIdx.x" in sctx.launch_params:
return sctx.launch_params["threadIdx.x"].var
return None
@@ -0,0 +1,18 @@
# 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.
from .warp_xor_swizzle import *
@@ -0,0 +1,430 @@
# 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.
"""CUDA permute_layout dispatch: warp register-staged in-place transpose with
optional per-lane XOR-swizzle to avoid SMEM bank conflicts on the write phase.
The dispatcher reasons about the **layout's shard**, not the buffer's
declared shape (the two can differ — a buffer with ``shape=(PIPE, M, K)``
may carry a layout whose shard has more dims internally, with grouping
mapping shard segments onto buffer dims). Concretely:
src_sliced = src.layout.slice(src.shape, region).canonicalize()
dst_sliced = dst.layout.slice(dst.shape, region).canonicalize()
# If the two sliced shards have different structures (which is common —
# a linear layout collapses to 1D under canon while a transposed one
# keeps its multi-dim structure), regroup src to dst's shape.
if src_sliced.shard != dst_sliced.shard:
src_sliced, _ = src_sliced.group(dst.shard.extents)
extent = [int(it.extent) for it in dst_sliced.shard] # iteration shape
src_str = [int(it.stride) for it in src_sliced.shard]
dst_str = [int(it.stride) for it in dst_sliced.shard]
The algorithm:
regs[P]
for r in 0..P:
j = r XOR ((lane >> SHIFT) & MASK)
i = lane + j * 32 # flat logical index
idx = decompose(i, extent) # iter multi-dim index
regs[r] = src[project(idx, src.shape, slice_starts)]
warp_sync()
for r in 0..P:
j = r XOR ((lane >> SHIFT) & MASK)
i = lane + j * 32
idx = decompose(i, extent)
dst[project(idx, dst.shape, slice_starts)] = regs[r]
warp_sync()
where ``project`` mixed-radix-folds the iter shard dims back onto the
buffer's iterated slice dims (so the emit's index matches buf.shape rank,
which TIR's BufferLoad/Store requires).
SHIFT and MASK are chosen by simulating the bank pattern at the **shard
granularity** (where strides are affine), trying k = 0, 1, …, log2(P)
and picking the smallest k that makes both phases bank-conflict-free.
Correctness rests on:
* For each lane, ``r ↦ r XOR const`` is a bijection on ``[0, P)``.
* Therefore (lane, r) ↔ flat over [0, V).
* Both layouts are verified bijections on the slice (every logical
position has a unique byte offset under that layout).
* The mixed-radix projection from iter shard idx to buf coord is exactly
what TIR's BufferLoad does internally when buf.shape rank < shard rank
— so iter shard's strides and the buffer-indexed byte offset agree.
"""
from __future__ import annotations
import math
from tvm.runtime import DataType
from tvm.script import tirx as T
from tvm.tirx import Buffer, BufferRegion, IntImm, PrimFunc
from tvm.tirx.layout import TileLayout, _flatten_coord
from tvm.tirx.operator.tile_primitive import DispatchContext, fail, register_dispatch
from tvm.tirx.stmt import TilePrimitiveCall
from ..common import get_indices, get_st_extent
# ---------- helpers ----------------------------------------------------------
def _as_buffer_and_region(arg):
"""Normalize a Buffer or BufferRegion to (buffer, start_list, extent_list)."""
if isinstance(arg, Buffer):
buf = arg
extent = list(buf.shape)
st = [0] * len(extent)
elif isinstance(arg, BufferRegion):
buf = arg.buffer
st, extent = get_st_extent(arg)
else:
raise TypeError(f"unexpected permute_layout arg type: {type(arg)}")
return buf, list(st), list(extent)
def _as_int(x):
"""Return int(x) if x is int-like, else None."""
if isinstance(x, int):
return x
if isinstance(x, IntImm):
return int(x.value)
if hasattr(x, "value") and isinstance(x.value, int):
return int(x.value)
try:
return int(x)
except (TypeError, ValueError):
return None
def _layout_shard_int(layout):
"""Return (extents, strides) as int lists from a TileLayout's shard, or (None, None)."""
if not isinstance(layout, TileLayout):
return None, None
extents, strides = [], []
for it in layout.shard:
e = _as_int(it.extent)
s = _as_int(it.stride)
if e is None or s is None:
return None, None
extents.append(e)
strides.append(s)
return extents, strides
def _decompose_row_major(i, extent):
out, rem = [], i
for e in reversed(extent):
out.append(rem % e)
rem //= e
return list(reversed(out))
def _eval_offset(idx, strides):
return sum(i * s for i, s in zip(idx, strides))
def _check_bijection(extent, strides):
"""Iteration extents + strides define a bijection on [0, V)?"""
V = math.prod(extent)
seen = set()
for i in range(V):
off = _eval_offset(_decompose_row_major(i, extent), strides)
if off in seen:
return False
seen.add(off)
return len(seen) == V
def _bank_free(extent, strides, dtype_bytes, P, k):
"""For every register slot r ∈ [0, P), do the 32 lanes hit 32 distinct banks?"""
T, BANKS, BANK_W = 32, 32, 4
shift = 5 - k
mask = (1 << k) - 1
for r in range(P):
seen = set()
for lane in range(T):
j = r ^ ((lane >> shift) & mask)
flat = lane + j * T
idx = _decompose_row_major(flat, extent)
off_bytes = _eval_offset(idx, strides) * dtype_bytes
bank = (off_bytes // BANK_W) % BANKS
if bank in seen:
return False
seen.add(bank)
return True
def _choose_xor_k(extent, src_strides, dst_strides, dtype_bytes, P):
max_k = int(math.log2(P)) if P > 0 else 0
for k in range(max_k + 1):
if _bank_free(extent, src_strides, dtype_bytes, P, k) and _bank_free(
extent, dst_strides, dtype_bytes, P, k
):
return k
return None
# ---------- validator + dispatch impl ---------------------------------------
def _gather(op_call):
op_call = TilePrimitiveCall.downcast(op_call)
dst_arg, src_arg = op_call.args[0], op_call.args[1]
src_buf, src_st, src_ext = _as_buffer_and_region(src_arg)
dst_buf, dst_st, dst_ext = _as_buffer_and_region(dst_arg)
return src_buf, src_st, src_ext, dst_buf, dst_st, dst_ext
def _why_reject(op_call, sctx):
if not sctx.is_warp:
return f"scope {sctx.scope_kind!r} is not 'warp'"
if "threadIdx.y" in sctx.launch_params or "threadIdx.z" in sctx.launch_params:
return "multi-dim threadIdx is not supported"
src_buf, src_st, src_ext, dst_buf, dst_st, dst_ext = _gather(op_call)
if src_buf.dtype != dst_buf.dtype:
return f"dtype mismatch: dst={dst_buf.dtype} vs src={src_buf.dtype}"
src_ext_i = [_as_int(e) for e in src_ext]
dst_ext_i = [_as_int(e) for e in dst_ext]
if None in src_ext_i or None in dst_ext_i:
return "extents must be compile-time integers"
if src_ext_i != dst_ext_i:
return f"slice shape mismatch: src={src_ext_i} vs dst={dst_ext_i}"
dtype_bytes = DataType(src_buf.dtype).bits // 8
if dtype_bytes not in (1, 2, 4, 8, 16):
return f"unsupported dtype byte width: {dtype_bytes}"
if not isinstance(src_buf.layout, TileLayout):
return "src buffer's layout is not a plain TileLayout"
if not isinstance(dst_buf.layout, TileLayout):
return "dst buffer's layout is not a plain TileLayout"
# Slice + canonicalize both layouts. The result's shard describes the
# iteration domain; runtime starts (like ``ks``) are folded into the
# layout's offset, separate from the shard's affine part.
src_region = [(s, s + e) for s, e in zip(src_st, src_ext)]
dst_region = [(s, s + e) for s, e in zip(dst_st, dst_ext)]
src_sliced = src_buf.layout.slice(list(src_buf.shape), src_region)
dst_sliced = dst_buf.layout.slice(list(dst_buf.shape), dst_region)
if src_sliced is None or dst_sliced is None:
return "layout.slice failed"
src_sliced = src_sliced.canonicalize()
dst_sliced = dst_sliced.canonicalize()
# Iteration shape: regroup dst onto the iterated buf dims; the result's
# shard may stay finer than iter_buf_extents (one buf dim ↔ several shard
# dims via seps), which is fine. Then regroup src to match dst's shard
# extents exactly so both phases share the same iteration index space.
iter_buf_extents = [e for e in src_ext_i if e != 1]
try:
dst_grouped, dst_seps = dst_sliced.group(iter_buf_extents)
src_grouped, _ = src_sliced.group([int(it.extent) for it in dst_grouped.shard])
except Exception as e:
return f"layout.group failed: {e}"
dst_ext_, dst_str_ = _layout_shard_int(dst_grouped)
src_ext_, src_str_ = _layout_shard_int(src_grouped)
if dst_ext_ is None or src_ext_ is None:
return "regrouped layout shard contains non-integer extent/stride"
if src_ext_ != dst_ext_:
return f"src shard {src_ext_} doesn't match dst shard {dst_ext_} after regrouping"
extent = dst_ext_
V = math.prod(extent)
T = 32
if V == 0 or V % T != 0:
return f"volume {V} not divisible by warp size {T}"
P = V // T
if P == 0 or (P & (P - 1)) != 0 or P > T:
return f"per-thread count {P} must be power of 2 in [1, {T}]"
if not _check_bijection(extent, src_str_):
return "src layout (regrouped) is not a bijection on the slice"
if not _check_bijection(extent, dst_str_):
return "dst layout is not a bijection on the slice"
return None
def _impl(op_call, sctx):
src_buf, src_st, src_ext, dst_buf, dst_st, dst_ext = _gather(op_call)
src_ext_i = [_as_int(e) for e in src_ext]
src_region = [(s, s + e) for s, e in zip(src_st, src_ext)]
dst_region = [(s, s + e) for s, e in zip(dst_st, dst_ext)]
src_sliced = src_buf.layout.slice(list(src_buf.shape), src_region).canonicalize()
dst_sliced = dst_buf.layout.slice(list(dst_buf.shape), dst_region).canonicalize()
iter_buf_extents = [e for e in src_ext_i if e != 1]
dst_grouped, dst_seps = dst_sliced.group(iter_buf_extents)
src_grouped, _ = src_sliced.group([int(it.extent) for it in dst_grouped.shard])
extent, dst_str_ = _layout_shard_int(dst_grouped)
_, src_str_ = _layout_shard_int(src_grouped)
V = math.prod(extent)
P = V // 32
dtype_bytes = DataType(src_buf.dtype).bits // 8
k_opt = _choose_xor_k(extent, src_str_, dst_str_, dtype_bytes, P)
if k_opt is None:
fail(f"no XOR-bits k ∈ [0, log2(P)={int(math.log2(P))}] makes both phases bank-free")
shift = 5 - k_opt
mask = (1 << k_opt) - 1
iter_buf_dims = [i for i, e in enumerate(src_ext_i) if e != 1]
seps = list(dst_seps)
def _project(iter_idx, st_list):
buf_idx = list(st_list)
for bi in range(len(seps) - 1):
lo, hi = seps[bi], seps[bi + 1]
flat = _flatten_coord(iter_idx[lo:hi], extent[lo:hi])
buf_idx[iter_buf_dims[bi]] = st_list[iter_buf_dims[bi]] + flat
return tuple(buf_idx)
tid_x = sctx.launch_params["threadIdx.x"]
dtype = src_buf.dtype
# Shared 32/64b: base ptr + stride offset avoids buf[] flatten IMAD path.
direct = (
dtype_bytes in (4, 8)
and "shared" in str(src_buf.scope())
and "shared" in str(dst_buf.scope())
)
ptx_t = f"b{dtype_bytes * 8}"
# ld/st only move bits, so use an unsigned container of the matching width:
# ``ptx.ld(..., bN)`` rejects a float return dtype, and the permute is a pure
# byte shuffle, so a float32/float64 tile loads/stores correctly as uint.
bits_dtype = f"uint{dtype_bytes * 8}"
def _iter_off(iter_idx, strides):
return sum(iter_idx[d] * strides[d] for d in range(len(strides)))
# fmt: off
if direct:
@T.prim_func
def impl():
warp_size = T.meta_var(32)
lane_id = T.meta_var(tid_x % warp_size)
regs = T.alloc_buffer((P,), bits_dtype, scope="local")
base_src = T.meta_var(src_buf.ptr_to(list(src_st)))
base_dst = T.meta_var(dst_buf.ptr_to(list(dst_st)))
# Phase 1: read via L_src
for r in T.unroll(0, P):
j = T.meta_var(r ^ ((lane_id >> shift) & mask))
flat = T.meta_var(lane_id + j * warp_size)
iter_idx = T.meta_var(get_indices(flat, [0] * len(extent), extent))
off = T.meta_var(_iter_off(iter_idx, src_str_))
ptr = T.meta_var(T.ptr_byte_offset(base_src, off * dtype_bytes, dtype))
regs[r] = T.ptx.ld(ptr, bits_dtype, ptx_t, space="shared")
T.cuda.warp_sync()
# Phase 2: write via L_dst
for r in T.unroll(0, P):
j = T.meta_var(r ^ ((lane_id >> shift) & mask))
flat = T.meta_var(lane_id + j * warp_size)
iter_idx = T.meta_var(get_indices(flat, [0] * len(extent), extent))
off = T.meta_var(_iter_off(iter_idx, dst_str_))
ptr = T.meta_var(T.ptr_byte_offset(base_dst, off * dtype_bytes, dtype))
T.evaluate(T.ptx.st(ptr, regs[r], space="shared", ptx_type=ptx_t))
T.cuda.warp_sync()
else:
@T.prim_func
def impl():
warp_size = T.meta_var(32)
lane_id = T.meta_var(tid_x % warp_size)
regs = T.alloc_buffer((P,), dtype, scope="local")
# Phase 1: read via L_src
for r in T.unroll(0, P):
j = T.meta_var(r ^ ((lane_id >> shift) & mask))
flat = T.meta_var(lane_id + j * warp_size)
iter_idx = T.meta_var(get_indices(flat, [0] * len(extent), extent))
src_idx = T.meta_var(_project(iter_idx, src_st))
regs[r] = src_buf[tuple(src_idx)]
T.cuda.warp_sync()
# Phase 2: write via L_dst
for r in T.unroll(0, P):
j = T.meta_var(r ^ ((lane_id >> shift) & mask))
flat = T.meta_var(lane_id + j * warp_size)
iter_idx = T.meta_var(get_indices(flat, [0] * len(extent), extent))
dst_idx = T.meta_var(_project(iter_idx, dst_st))
dst_buf[tuple(dst_idx)] = regs[r]
T.cuda.warp_sync()
# fmt: on
return impl
# === Variant: permute_layout/warp_xor_swizzle (priority=20) ============
#
# When: warp scope; matching dst/src dtype + slice shape; both buffers carry
# a plain TileLayout; after slice + canonicalize (and regrouping src to dst's
# structure if needed), the iteration extents form a power-of-2 ≤32 elements
# per lane; both layouts are bijections on the slice; and there exists an
# XOR-bits ``k`` that makes both phases bank-conflict-free.
#
# Buffer ``shape`` rank does NOT need to equal layout ``shard`` rank — the
# dispatcher uses the layout shard for iteration (after slice+canon) and
# projects back onto ``buf.shape`` via mixed-radix grouping for the emit.
#
# Before (TilePrimitiveCall):
# with T.warp():
# # SFA_smem: u32 (PIPE, BLK_SFA//32, 32), layout shard 4D
# # (PIPE, BLK_SFA//128, 4, 32) strides (BLK_SFA, 128, 32, 1)
# # SFA_post: same shape; layout shard 4D, strides (BLK_SFA, 128, 1, 4)
# Tx.permute_layout(SFA_post[ks, :, :], SFA_smem[ks, :, :])
#
# After (BLK_SFA=128, P=4, k=2, shift=3):
# lane_id = threadIdx.x % 32
# regs = T.alloc_buffer((4,), "uint32", scope="local")
# for r in T.unroll(4):
# j = r ^ ((lane_id >> 3) & 0x3)
# flat = lane_id + j * 32
# (g, l) = decompose(flat, extent=[4, 32])
# regs[r] = src[ks, g, l]
# T.cuda.warp_sync()
# for r in T.unroll(4):
# j = r ^ ((lane_id >> 3) & 0x3)
# flat = lane_id + j * 32
# (g, l) = decompose(flat, extent=[4, 32])
# dst[ks, g, l] = regs[r]
# T.cuda.warp_sync()
@register_dispatch(
"permute_layout",
"cuda",
variant="warp_xor_swizzle",
priority=20,
)
def permute_layout_dispatch(op: TilePrimitiveCall, sctx: DispatchContext) -> PrimFunc:
reason = _why_reject(op, sctx)
if reason is not None:
fail(reason)
return _impl(op, sctx)
__all__ = [
"_bank_free",
"_check_bijection",
"_choose_xor_k",
"_decompose_row_major",
"_eval_offset",
"permute_layout_dispatch",
]
@@ -0,0 +1,20 @@
# 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.
from .local import *
from .shared import *
from .sm100_packed import *
@@ -0,0 +1,484 @@
# 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.
"""CUDA reduction operator dispatch: local-memory variant.
Registered ops: sum, max, min.
When: dst and src are both local-scope buffers with matching dtype, on CUDA.
(A) Thread scope -- sequential per-element reduction
(_emit_reduction_local_thread_wise):
Before:
Tx.sum(B_local[0:2, 0:3], A_local[0:2, 0:3, 0:4], [-1], False)
After (scheduled PrimFunc, spatial_len=6, reduction_len=4):
for spa in range(6):
B_local[spa] = T.float32(0.0) # init (skipped if accum)
for red in range(4):
B_local[spa] = B_local[spa] + A_local[spa * 4 + red]
(B) Warp/Warpgroup scope -- layout-driven reduction
(_emit_reduction_local_view):
Requires TileLayout with valid thread-partition. Decomposes layout to
identify thread-local elements, then optionally shuffles partial sums.
thread_reduce=False: local-only, no shuffle (warp and warpgroup).
thread_reduce=True: local reduction + cross-thread shfl_xor steps (warp only).
accum=True + shuffle: saves old dst before reduce+shuffle, combines after (warp only).
Before:
Tx.warp.sum(red_view[0:16, 0:4], acc_view[0:16, 0:128], [-1], False,
thread_reduce=True)
After (scheduled PrimFunc, local_total=2, local_red=32, 2 shuffle steps):
src_local = acc_view.view(64)
dst_local = red_view.view(2)
for spa in range(2):
dst_local[spa] = T.float32(0.0)
for red in range(32):
dst_local[spa] = dst_local[spa] + src_local[...]
dst_local[spa] = dst_local[spa] + shfl_xor(..., 1, 32, 32)
dst_local[spa] = dst_local[spa] + shfl_xor(..., 2, 32, 32)
"""
import functools
import operator
from typing import Any
from tvm.arith.analyzer import Analyzer
from tvm.script import tirx as T
from tvm.tirx import BufferRegion, PrimFunc
from tvm.tirx.layout import TileLayout, laneid
from tvm.tirx.operator.tile_primitive import DispatchContext, fail
from tvm.tirx.operator.tile_primitive.common import ReduceOpType
from tvm.tirx.operator.tile_primitive.dispatcher import predicate, register_dispatch
from tvm.tirx.stmt import TilePrimitiveCall
from ..common import get_indices, get_st_extent
from ..layout_utils import get_local_region, get_sublayout_from_region
from .utils import (
_REDUCE_OP_TO_STR,
_analyze_axes,
_analyze_layout_dims,
_build_local_dim_map,
_compute_shuffle_masks,
_match_reduction_storage_scope,
_reduction_args,
_validate_reduction_layout,
reduce_default_value_table,
reduce_op_table,
)
def _analyze_shuffle_reduce(src_layout, dst_layout):
"""Analyze src/dst layouts for laneid shard->replica reduce pattern.
Returns (reduce_width, local_elems) if the pattern matches, or None.
- reduce_width: number of lanes participating in each group's reduction
- local_elems: per-thread element count (product of non-laneid shard extents)
"""
if src_layout.is_swizzle() or dst_layout.is_swizzle():
return None
src_canon = src_layout.canonicalize()
dst_canon = dst_layout.canonicalize()
# Extract laneid iters from shard and replica
src_laneid_shard = [it for it in src_canon.shard if it.axis == laneid]
dst_laneid_replica = [it for it in dst_canon.replica if it.axis == laneid]
# src shard must contain laneid (data distributed across lanes)
if not src_laneid_shard:
return None
# dst replica must contain laneid (result broadcast to lanes)
if not dst_laneid_replica:
return None
# laneid span must be 32 (full warp)
src_laneid_span = 1 + sum(abs(int(it.stride)) * (int(it.extent) - 1) for it in src_laneid_shard)
if src_laneid_span != 32:
return None
reduce_width = functools.reduce(operator.mul, [int(it.extent) for it in dst_laneid_replica], 1)
if reduce_width <= 0 or reduce_width > 32 or (reduce_width & (reduce_width - 1)) != 0:
return None # must be power of 2
# local_elems = product of non-laneid shard extents in src
src_non_laneid = [it for it in src_canon.shard if it.axis != laneid]
local_elems = functools.reduce(operator.mul, [int(it.extent) for it in src_non_laneid], 1)
return reduce_width, local_elems
def _gen_warp_shuffle_reduce(src, dst, reduce_width, local_elems, accum, op_type, init_value):
"""Generate warp shuffle reduce codegen for laneid shard->replica pattern.
Unified for both full warp (reduce_width=32) and partial warp (e.g. reduce_width=8).
"""
is_same_buffer = src.same_as(dst)
op_str = _REDUCE_OP_TO_STR[op_type]
# fmt: off
@T.prim_func(check_well_formed=False)
def impl():
src_local = src.local(local_elems)
dst_local = dst.local(local_elems)
for k in T.serial(local_elems):
if not is_same_buffer:
dst_local[k] = src_local[k]
dst_local[k] = T.cuda.warp_reduce(dst_local[k], op_str, reduce_width)
# fmt: on
return impl
def validate_reduction_local(
op: TilePrimitiveCall, sctx: DispatchContext
) -> tuple[bool, str | None]:
"""Validate reduction in local memory."""
op = TilePrimitiveCall.downcast(op)
dst_br, src_br = op.output, op.input
dst, src = dst_br.buffer, src_br.buffer
if not (src.scope() == "local" and dst.scope() == "local" and sctx.is_target("cuda")):
return False, "expected local scope and CUDA target"
if src.dtype != dst.dtype:
return False, f"dtype mismatch: src={src.dtype} dst={dst.dtype}"
if sctx.is_thread:
return True, None # thread-wise reduction
elif sctx.scope_kind in ["warp", "warpgroup"]:
if not sctx.is_warp and op.config.get("thread_reduce", False):
return (
False,
"thread_reduce=True is only supported in warp scope; "
"warpgroup local reduction is thread-local only",
)
# VIEW: need layouts and layout analysis
if not (src.layout and dst.layout):
return False, "layouts required for view-based local reduction"
if not (isinstance(src.layout, TileLayout) and isinstance(dst.layout, TileLayout)):
return False, "TileLayout required for view-based local reduction"
if src.layout.is_swizzle() or dst.layout.is_swizzle():
return False, "swizzle layout unsupported for local reduction"
analyzer = Analyzer()
# Validate get_local_region succeeds for both
src_st, src_extent = get_st_extent(src_br)
dst_st, dst_extent = get_st_extent(dst_br)
if sctx.is_warp:
# Check for laneid shard->replica shuffle reduce pattern first.
# This pattern has laneid in dst replica (broadcast), which the
# general validation below would reject.
shuffle_info = _analyze_shuffle_reduce(src.layout, dst.layout)
if shuffle_info is not None:
return True, None
for layout, buf, st, ext, name in [
(src.layout, src, src_st, src_extent, "src"),
(dst.layout, dst, dst_st, dst_extent, "dst"),
]:
for it in layout.shard:
if it.axis.is_thread() and analyzer.can_prove_equal(it.stride, 0):
return False, f"thread dim with zero stride in {name}"
replica = getattr(layout, "replica", None) or []
if any(it.axis.is_thread() for it in replica):
return False, f"thread axis in replica for {name}"
if get_local_region(layout, list(buf.shape), st, ext) is None:
return False, f"get_local_region failed for {name}"
# Validate layout compatibility
# Spatial dims match, reduce dims in dst have local_extent==1
reduce_axes = tuple(int(a) for a in op.reduce_axes)
src_ndim = len(src_br.region)
try:
reduce_dims, _ = _analyze_axes(src_ndim, reduce_axes)
except AssertionError as e:
return False, str(e)
src_sliced = get_sublayout_from_region(src.layout, src.shape, src_st, src_extent)
dst_sliced = get_sublayout_from_region(dst.layout, dst.shape, dst_st, dst_extent)
ok, msg = _validate_reduction_layout(
src_sliced, dst_sliced, list(src_extent), list(dst_extent), reduce_dims
)
return ok, msg
else:
return False, f"unsupported exec_scope {sctx.scope_kind} for local reduction"
def _emit_reduction_local_thread_wise(
dst_br: BufferRegion,
src_br: BufferRegion,
accum: bool,
reduce_op: ReduceOpType,
reduce_dims: list[int],
spatial_dims: list[int],
) -> PrimFunc:
dst, src = dst_br.buffer, src_br.buffer
dtype = src.dtype
src_st, src_extent = get_st_extent(src_br)
dst_st, dst_extent = get_st_extent(dst_br)
src_ndim = len(src_extent)
spa_extents = [src_extent[d] for d in spatial_dims]
red_extents = [src_extent[d] for d in reduce_dims]
spatial_len = functools.reduce(operator.mul, spa_extents, 1)
reduction_len = functools.reduce(operator.mul, red_extents, 1)
op_func = reduce_op_table.get(reduce_op)
assert op_func is not None
init_value = reduce_default_value_table(dtype).get(reduce_op)
def get_src_indices(spa_fused, red_fused):
spa_indices = []
rem = spa_fused
for e in reversed(spa_extents):
spa_indices.append(rem % e)
rem //= e
spa_indices.reverse()
red_indices = []
rem = red_fused
for e in reversed(red_extents):
red_indices.append(rem % e)
rem //= e
red_indices.reverse()
full = [None] * src_ndim
for i, d in enumerate(spatial_dims):
full[d] = spa_indices[i] + src_st[d]
for i, d in enumerate(reduce_dims):
full[d] = red_indices[i] + src_st[d]
return full
# fmt: off
@T.prim_func(check_well_formed=False)
def impl():
for spa in T.serial(spatial_len):
dst_idx = T.meta_var(get_indices(spa, dst_st, dst_extent))
if not accum:
dst[tuple(dst_idx)] = init_value
for red in T.serial(reduction_len):
src_idx = T.meta_var(get_src_indices(spa, red))
dst[tuple(dst_idx)] = op_func(dst[tuple(dst_idx)], src[tuple(src_idx)])
# fmt: on
return impl
def _emit_reduction_local_view(
dst_br: BufferRegion,
src_br: BufferRegion,
accum: bool,
reduce_op: ReduceOpType,
config: dict[str, Any],
reduce_dims: set[int],
spatial_dims: list[int],
src_local_info,
dst_local_info,
shuffle_masks: list[int],
) -> PrimFunc:
dst, src = dst_br.buffer, src_br.buffer
dtype = src.dtype
op_func = reduce_op_table.get(reduce_op)
assert op_func is not None
init_value = reduce_default_value_table(dtype).get(reduce_op)
src_local_shape, src_local_st, src_local_ext = src_local_info
dst_local_shape, dst_local_st, dst_local_ext = dst_local_info
# Build maps from original dim index to position in get_local_region output
src_dim_map = _build_local_dim_map(src.layout, list(src.shape))
dst_dim_map = _build_local_dim_map(dst.layout, list(dst.shape))
# Only include reduction dims that have local parts in src
src_ndim = len(src_br.region)
reduce_local_dims = [d for d in reduce_dims if src_dim_map[d] is not None]
reduction_local_ext = [src_local_ext[src_dim_map[d]] for d in reduce_local_dims]
reduction_local_st = [src_local_st[src_dim_map[d]] for d in reduce_local_dims]
reduction_local_total = functools.reduce(operator.mul, reduction_local_ext, 1)
dst_local_total = functools.reduce(operator.mul, dst_local_ext, 1)
def _get_src_local_index(dst_fused, red_fused):
"""Compute src local multi-dim index from dst fused index and reduction fused index."""
dst_indices = get_indices(dst_fused, dst_local_st, dst_local_ext)
red_indices = get_indices(red_fused, reduction_local_st, reduction_local_ext)
# Interleave into src local indices (skipping pure-thread dims)
src_local = []
ri = 0
for d in range(src_ndim):
if src_dim_map[d] is None:
continue # pure-thread in src, not in src.local()
if d in reduce_dims:
src_local.append(red_indices[ri])
ri += 1
else:
# Spatial dim: use corresponding dst local position
src_local.append(dst_indices[dst_dim_map[d]])
return src_local
# is_same_buffer = src.same_as(dst)
shuffle = bool(config.get("thread_reduce", False))
in_place = dst.same_as(src)
def shuffle_data(mask, dst_local, dst_idx):
@T.inline
def inner_shuffle(v, shuffle_mask):
dst_local[tuple(dst_idx)] = op_func(
v, T.tvm_warp_shuffle_xor(mask, v, shuffle_mask, 32, 32)
)
for i in range(len(shuffle_masks)):
inner_shuffle(dst_local[tuple(dst_idx)], shuffle_masks[i])
need_save_accum = accum and shuffle
# fmt: off
if need_save_accum:
@T.prim_func(check_well_formed=False)
def impl():
src_local = src.local(*src_local_shape)
dst_local = dst.local(*dst_local_shape)
old_val = T.alloc_buffer([1], dtype, scope="local")
for spa in T.serial(dst_local_total):
dst_idx = T.meta_var(get_indices(spa, dst_local_st, dst_local_ext))
old_val[0] = dst_local[tuple(dst_idx)]
if not in_place:
dst_local[tuple(dst_idx)] = init_value
for red in T.serial(reduction_local_total):
src_idx = T.meta_var(_get_src_local_index(spa, red))
dst_local[tuple(dst_idx)] = op_func(dst_local[tuple(dst_idx)], src_local[tuple(src_idx)]) # noqa: E501
if shuffle:
mask = T.tvm_warp_activemask()
shuffle_data(mask, dst_local, dst_idx)
dst_local[tuple(dst_idx)] = op_func(dst_local[tuple(dst_idx)], old_val[0])
else:
@T.prim_func(check_well_formed=False)
def impl():
src_local = src.local(*src_local_shape)
dst_local = dst.local(*dst_local_shape)
for spa in T.serial(dst_local_total):
dst_idx = T.meta_var(get_indices(spa, dst_local_st, dst_local_ext))
if not in_place:
if not accum:
dst_local[tuple(dst_idx)] = init_value
for red in T.serial(reduction_local_total):
src_idx = T.meta_var(_get_src_local_index(spa, red))
dst_local[tuple(dst_idx)] = op_func(dst_local[tuple(dst_idx)], src_local[tuple(src_idx)]) # noqa: E501
if shuffle:
mask = T.tvm_warp_activemask()
shuffle_data(mask, dst_local, dst_idx)
# fmt: on
return impl
def reduction_local_impl(
op: TilePrimitiveCall, op_type: ReduceOpType, sctx: DispatchContext
) -> PrimFunc | None:
dst_br, src_br, reduce_axes, accum, config = _reduction_args(op)
src_ndim = len(src_br.region)
reduce_dims, spatial_dims = _analyze_axes(src_ndim, reduce_axes)
if sctx.is_thread:
return _emit_reduction_local_thread_wise(
dst_br, src_br, accum, op_type, reduce_dims, spatial_dims
)
elif sctx.scope_kind in ["warp", "warpgroup"]:
src = src_br.buffer
dst = dst_br.buffer
if sctx.is_warp:
# --- Try laneid shard->replica shuffle reduce ---
shuffle_info = _analyze_shuffle_reduce(src.layout, dst.layout)
if shuffle_info is not None:
reduce_width, local_elems = shuffle_info
if op_type not in _REDUCE_OP_TO_STR:
fail(f"unsupported reduce op: {op_type}")
dtype = src.dtype
init_value = reduce_default_value_table(dtype).get(op_type)
return _gen_warp_shuffle_reduce(
src, dst, reduce_width, local_elems, accum, op_type, init_value
)
elif config.get("thread_reduce", False):
fail(
"thread_reduce=True is only supported in warp scope; "
"warpgroup local reduction is thread-local only"
)
# --- Existing WGMMA layout path below ---
src_st, src_extent = get_st_extent(src_br)
dst_st, dst_extent = get_st_extent(dst_br)
src_local_info = get_local_region(src.layout, list(src.shape), src_st, src_extent)
dst_local_info = get_local_region(dst.layout, list(dst.shape), dst_st, dst_extent)
assert src_local_info is not None and dst_local_info is not None
src_dim_info = _analyze_layout_dims(src.layout, list(src.shape))
shuffle_masks = (
_compute_shuffle_masks(src_dim_info, reduce_dims)
if config.get("thread_reduce", False)
else []
)
return _emit_reduction_local_view(
dst_br,
src_br,
accum,
op_type,
config,
reduce_dims,
spatial_dims,
src_local_info,
dst_local_info,
shuffle_masks,
)
else:
fail(f"unsupported exec_scope {sctx.scope_kind} for reduction_local_impl")
# ---------------------------------------------------------------------------
# Registration: local memory reduction (priority=10)
# ---------------------------------------------------------------------------
for op_name, op_type in [
("sum", ReduceOpType.SUM),
("max", ReduceOpType.MAX),
("min", ReduceOpType.MIN),
]:
@register_dispatch(
op_name,
"cuda",
variant="local",
priority=10,
when=[
predicate("storage_scope", _match_reduction_storage_scope, expected_scope=["local"]),
predicate("local_valid", validate_reduction_local),
],
)
def _local_dispatch(op: TilePrimitiveCall, sctx: DispatchContext, _op_type=op_type) -> PrimFunc:
op = TilePrimitiveCall.downcast(op)
return reduction_local_impl(op, _op_type, sctx)
@@ -0,0 +1,298 @@
# 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.
"""CUDA reduction operator dispatch: shared-memory variant.
Registered ops: sum, max, min.
When: dst and src are both shared-memory buffers, exec scope is one of
{cta, warpgroup, warp, thread}, threadIdx.x bound, reduce axes valid.
(A) CTA/warpgroup/warp scope -- adaptive-group shuffle tree
(_emit_reduction_shared_cta):
group_size = min(next_power_of_2(reduction_len), 32).
Each group of threads reduces one spatial position via shfl_xor.
Before:
Tx.cta.sum(B_smem[0:4], A_smem[0:4, 0:8], [-1], False)
After (scheduled PrimFunc, group_size=8, spatial_par=4):
thread_data[0] = T.float32(0.0)
thread_data[0] = thread_data[0] + A_smem[tid_in_scope] # gather
# log2(8) = 3 shuffle-xor steps with width=8
thread_data[0] = thread_data[0] + shfl_xor(thread_data[0], 1, 8, 32)
thread_data[0] = thread_data[0] + shfl_xor(thread_data[0], 2, 8, 32)
thread_data[0] = thread_data[0] + shfl_xor(thread_data[0], 4, 8, 32)
if tid_in_scope % 8 == 0:
B_smem[tid_in_scope // 8] = thread_data[0]
(B) Thread scope -- sequential loop (_emit_reduction_shared_thread):
Before:
if tid == 65:
Tx.sum(B_smem[0:4], A_smem[0:4, 0:8], [-1], False)
After (scheduled PrimFunc):
for spa in range(4):
B_smem[spa] = T.float32(0.0) # init (skipped if accum)
for red in range(8):
B_smem[spa] = B_smem[spa] + A_smem[spa * 8 + red]
"""
import functools
import math
import operator
from tvm.arith.analyzer import Analyzer
from tvm.script import tirx as T
from tvm.tirx import BufferRegion, PrimFunc
from tvm.tirx.operator.tile_primitive import DispatchContext, fail
from tvm.tirx.operator.tile_primitive.common import ReduceOpType
from tvm.tirx.operator.tile_primitive.dispatcher import predicate, register_dispatch
from tvm.tirx.stmt import TilePrimitiveCall
from ..common import get_indices, get_st_extent, next_power_of_2
from .utils import (
_analyze_axes,
_match_reduction_storage_scope,
_reduction_args,
build_src_indices,
reduce_default_value_table,
reduce_op_table,
)
def validate_reduction_shared(
op: TilePrimitiveCall, sctx: DispatchContext
) -> tuple[bool, str | None]:
"""Validate reduction in shared memory."""
if sctx.scope_kind not in ["cta", "warpgroup", "warp", "thread"]:
return False, f"unsupported exec_scope {sctx.scope_kind} for shared reduction"
op = TilePrimitiveCall.downcast(op)
dst, src = op.output.buffer, op.input.buffer
if not (src.scope().startswith("shared") and dst.scope().startswith("shared")):
return False, "expected shared scope for both src and dst"
if src.dtype != dst.dtype:
return False, f"dtype mismatch: src={src.dtype} dst={dst.dtype}"
if "threadIdx.x" not in sctx.launch_params:
return False, "threadIdx.x not in launch_params"
if "threadIdx.y" in sctx.launch_params or "threadIdx.z" in sctx.launch_params:
return False, "multi-dimensional thread binding not supported for shared reduction"
reduce_axes = tuple(int(a) for a in op.reduce_axes)
src_region = op.input.region
dst_region = op.output.region
src_ndim = len(src_region)
try:
reduce_dims, spatial_dims = _analyze_axes(src_ndim, reduce_axes)
except AssertionError as e:
return False, str(e)
# Validate dst shape matches spatial dims of src
src_extent = [r.extent for r in src_region]
dst_extent = [r.extent for r in dst_region]
expected_dst_len = functools.reduce(operator.mul, [src_extent[d] for d in spatial_dims], 1)
actual_dst_len = functools.reduce(operator.mul, dst_extent, 1)
analyzer = Analyzer()
if not analyzer.can_prove_equal(expected_dst_len, actual_dst_len):
return (False, f"dst size {actual_dst_len} != expected spatial size {expected_dst_len}")
return True, None
def _emit_reduction_shared_cta(
dst_br: BufferRegion,
src_br: BufferRegion,
accum: bool,
reduce_op: ReduceOpType,
sctx: DispatchContext,
reduce_dims: list[int],
spatial_dims: list[int],
) -> PrimFunc:
exec_scope_name = sctx.scope_kind
def get_thread_cnt():
if exec_scope_name == "cta":
return sctx.launch_params["threadIdx.x"].dom.extent
elif exec_scope_name == "warpgroup":
return 128
elif exec_scope_name == "warp":
return 32
elif exec_scope_name == "thread":
return 1
thread_cnt = get_thread_cnt()
dst, src = dst_br.buffer, src_br.buffer
src_st, src_extent = get_st_extent(src_br)
dst_st, dst_extent = get_st_extent(dst_br)
dtype = src.dtype
# Compute spatial/reduction from the explicit axes
spatial_len = functools.reduce(operator.mul, [src_extent[d] for d in spatial_dims], 1)
reduction_len = functools.reduce(operator.mul, [src_extent[d] for d in reduce_dims], 1)
op_func = reduce_op_table.get(reduce_op)
assert op_func is not None
init_value = reduce_default_value_table(dtype).get(reduce_op)
# Adaptive group size: nearest power-of-2 for reduction length, capped at warp size and thread count. # noqa: E501
group_size = min(next_power_of_2(int(reduction_len)), 32, int(thread_cnt))
group_size = max(group_size, 1) # ensure at least 1
n_shuffles = int(math.log2(group_size)) if group_size > 1 else 0
spatial_par = int(thread_cnt) // group_size
def get_tid_in_scope():
tx_var = sctx.launch_params["threadIdx.x"].var
if exec_scope_name == "cta":
return tx_var
elif exec_scope_name in ("warp", "warpgroup"):
return tx_var % thread_cnt
elif exec_scope_name == "thread":
return 0
def shuffle_data(thread_data):
@T.inline
def inner_shuffle(mask, v, shuffle_mask):
v[0] = op_func(v[0], T.tvm_warp_shuffle_xor(mask, v[0], shuffle_mask, group_size, 32))
if n_shuffles > 0:
mask = T.tvm_warp_activemask()
for i in range(n_shuffles):
inner_shuffle(mask, thread_data, 1 << i)
@T.inline
def sync():
if exec_scope_name == "cta":
T.cuda.cta_sync()
elif exec_scope_name == "warpgroup":
T.cuda.warpgroup_sync(8) # TODO: fix this hardcoded value
elif exec_scope_name == "warp":
T.cuda.warp_sync()
elif exec_scope_name == "thread":
pass
# fmt: off
@T.prim_func
def impl():
tid_in_scope = get_tid_in_scope()
thread_data = T.alloc_buffer([1], dtype=dtype, scope="local")
group_id = T.meta_var(T.floordiv(tid_in_scope, group_size))
lane_in_grp = T.meta_var(tid_in_scope % group_size)
for step in T.serial(T.ceildiv(spatial_len, spatial_par)):
spa_fused = T.meta_var(step * spatial_par + group_id)
if spa_fused < spatial_len:
thread_data[0] = init_value
for t in T.serial(T.ceildiv(reduction_len, group_size)):
red_fused = T.meta_var(t * group_size + lane_in_grp)
if red_fused < reduction_len:
src_indices = T.meta_var(build_src_indices(spa_fused, red_fused, spatial_dims, reduce_dims, src_extent, src_st)) # noqa: E501
thread_data[0] = op_func(thread_data[0], src[tuple(src_indices)])
shuffle_data(thread_data)
if lane_in_grp == 0:
dst_indices = T.meta_var(get_indices(spa_fused, dst_st, dst_extent))
dst[tuple(dst_indices)] = T.if_then_else(T.bool(accum), op_func(dst[tuple(dst_indices)], thread_data[0]), thread_data[0]) # noqa: E501
sync()
# fmt: on
return impl
def _emit_reduction_shared_thread(
dst_br: BufferRegion,
src_br: BufferRegion,
accum: bool,
reduce_op: ReduceOpType,
sctx: DispatchContext,
reduce_dims: list[int],
spatial_dims: list[int],
) -> PrimFunc:
dst, src = dst_br.buffer, src_br.buffer
src_st, src_extent = get_st_extent(src_br)
dst_st, dst_extent = get_st_extent(dst_br)
dtype = src.dtype
# Compute spatial/reduction from the explicit axes
spatial_len = functools.reduce(operator.mul, [src_extent[d] for d in spatial_dims], 1)
reduction_len = functools.reduce(operator.mul, [src_extent[d] for d in reduce_dims], 1)
op_func = reduce_op_table.get(reduce_op)
assert op_func is not None
init_value = reduce_default_value_table(dtype).get(reduce_op)
@T.prim_func
def impl():
for spa_fused in T.serial(spatial_len):
dst_indices = T.meta_var(get_indices(spa_fused, dst_st, dst_extent))
if not accum:
dst[tuple(dst_indices)] = init_value
for red_fused in T.serial(reduction_len):
src_indices = T.meta_var(
build_src_indices(
spa_fused, red_fused, spatial_dims, reduce_dims, src_extent, src_st
)
)
dst[tuple(dst_indices)] = op_func(dst[tuple(dst_indices)], src[tuple(src_indices)])
return impl
def reduction_shared_impl(
op: TilePrimitiveCall, op_type: ReduceOpType, sctx: DispatchContext
) -> PrimFunc | None:
dst_br, src_br, reduce_axes, accum, config = _reduction_args(op)
src_ndim = len(src_br.region)
reduce_dims, spatial_dims = _analyze_axes(src_ndim, reduce_axes)
if sctx.scope_kind in ["cta", "warpgroup", "warp"]:
return _emit_reduction_shared_cta(
dst_br, src_br, accum, op_type, sctx, reduce_dims, spatial_dims
)
elif sctx.is_thread:
return _emit_reduction_shared_thread(
dst_br, src_br, accum, op_type, sctx, reduce_dims, spatial_dims
)
else:
fail(f"unsupported exec_scope {sctx.scope_kind} for reduction_shared_impl")
# ---------------------------------------------------------------------------
# Registration: shared memory reduction (priority=10)
# ---------------------------------------------------------------------------
for op_name, op_type in [
("sum", ReduceOpType.SUM),
("max", ReduceOpType.MAX),
("min", ReduceOpType.MIN),
]:
@register_dispatch(
op_name,
"cuda",
variant="shared",
priority=10,
when=[
predicate("storage_scope", _match_reduction_storage_scope, expected_scope=["shared*"]),
predicate("shared_valid", validate_reduction_shared),
],
)
def _shared_dispatch(
op: TilePrimitiveCall, sctx: DispatchContext, _op_type=op_type
) -> PrimFunc:
op = TilePrimitiveCall.downcast(op)
return reduction_shared_impl(op, _op_type, sctx)
@@ -0,0 +1,249 @@
# 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.
"""CUDA reduction operator dispatch: SM100+ packed optimized variant.
Registered ops: sum, max, min.
When: thread scope, all local buffers, float32, 1D src with len >= 8,
SM100+ (uses packed PTX instructions not available on older GPUs).
Before (TilePrimitiveCall -- sum example):
Tx.sum(dst_local[0:1], src_local[0:32]) # float32, reduce 32 -> 1 (thread scope)
After -- packed_add_sum (uses add.f32x2 to reduce pairs):
# Iteratively reduce: 32 -> 16 -> 8 -> 4 -> 2 -> 1
# Each step: add.f32x2 combines adjacent pairs
for i in T.serial(16):
T.cuda.func_call("add_f32x2", &buf[i*2], &buf[i*2], &buf[i*2+2])
# ... repeat halving until scalar result
dst_local[0] = buf[0]
After -- 3input_maxmin (uses 3-input PTX max/min):
# Tree reduction with 3-input instructions:
# max(a, b, c) in one PTX instruction
for i in T.serial(n // 3):
T.cuda.func_call("max3_f32", &buf[i*3], &buf[i*3+1], &buf[i*3+2])
With accum=True: accumulator folded into first element/pair of the reduction.
"""
import functools
import operator
from tvm.script import tirx as T
from tvm.tirx import BufferRegion, PrimFunc
from tvm.tirx.operator.tile_primitive import DispatchContext
from tvm.tirx.operator.tile_primitive.common import ReduceOpType
from tvm.tirx.operator.tile_primitive.dispatcher import predicate, register_dispatch
from tvm.tirx.stmt import TilePrimitiveCall
from ..common import sm_version_ok
from ..exec_scope_utils import exec_scope_ok
from .utils import (
_dst_len_ok,
_dtype_ok,
_local_scope_match,
_reduction_len_ok,
_src_ndim_ok,
reduce_op_table,
)
def _emit_reduction_local_thread_packed_add_sum(
dst_buffer_region: BufferRegion,
src_buffer_region: BufferRegion,
accum: bool,
reduce_op: ReduceOpType,
sctx: DispatchContext,
) -> PrimFunc:
dst, src = dst_buffer_region.buffer, src_buffer_region.buffer
src_region, dst_region = src_buffer_region.region, dst_buffer_region.region
dtype = src.dtype
src_extent = [r.extent for r in src_region]
[r.extent for r in dst_region]
src_st = [r.min for r in src_region]
dst_st = [r.min for r in dst_region]
reduction_len = functools.reduce(operator.mul, src_extent, 1)
src_base = src_st[0]
num_full_chunks = reduction_len // 8
remainder = reduction_len % 8
remainder_base = num_full_chunks * 8
# fmt: off
@T.prim_func(check_well_formed=False)
def impl():
local_sum = T.alloc_buffer([8], dtype, scope="local")
# First pass: copy first 8 elements (with optional accumulator)
for i in T.unroll(8):
if accum and i == 0:
local_sum[i] = src[src_base + i] + dst[tuple(dst_st)]
else:
local_sum[i] = src[src_base + i]
# Process remaining full chunks of 8
for outer in T.serial(num_full_chunks - 1):
for j in T.unroll(4):
T.ptx.add_f32x2(
T.address_of(local_sum[2 * j]),
T.cuda.make_float2(local_sum[2 * j], local_sum[2 * j + 1]),
T.cuda.make_float2(
src[src_base + 8 * (outer + 1) + 2 * j],
src[src_base + 8 * (outer + 1) + 2 * j + 1],
),
ftz=True,
)
# Handle remainder elements (0 to 7)
for i in T.serial(remainder):
local_sum[0] = local_sum[0] + src[src_base + remainder_base + i]
# Final packed add sum: 8 -> 4 -> 2 -> 1
T.ptx.add_f32x2(
T.address_of(local_sum[0]),
T.cuda.make_float2(local_sum[0], local_sum[1]),
T.cuda.make_float2(local_sum[2], local_sum[3]),
ftz=True,
)
T.ptx.add_f32x2(
T.address_of(local_sum[4]),
T.cuda.make_float2(local_sum[4], local_sum[5]),
T.cuda.make_float2(local_sum[6], local_sum[7]),
ftz=True,
)
T.ptx.add_f32x2(
T.address_of(local_sum[0]),
T.cuda.make_float2(local_sum[0], local_sum[1]),
T.cuda.make_float2(local_sum[4], local_sum[5]),
ftz=True,
)
dst[tuple(dst_st)] = local_sum[0] + local_sum[1]
# fmt: on
return impl
def _emit_reduction_local_thread_3input_maxmin(
dst_buffer_region: BufferRegion,
src_buffer_region: BufferRegion,
accum: bool,
reduce_op: ReduceOpType,
sctx: DispatchContext,
) -> PrimFunc:
dst, src = dst_buffer_region.buffer, src_buffer_region.buffer
src_region, dst_region = src_buffer_region.region, dst_buffer_region.region
dtype = src.dtype
src_extent = [r.extent for r in src_region]
src_st = [r.min for r in src_region]
dst_st = [r.min for r in dst_region]
reduction_len = functools.reduce(operator.mul, src_extent, 1)
op_func = reduce_op_table[reduce_op]
reduce3_func = T.ptx.reduce3_max_f32 if reduce_op == ReduceOpType.MAX else T.ptx.reduce3_min_f32
src_base = src_st[0]
num_full_chunks = reduction_len // 8
remainder = reduction_len % 8
remainder_base = num_full_chunks * 8
# fmt: off
@T.prim_func(check_well_formed=False)
def impl():
temp = T.alloc_buffer([4], dtype, scope="local")
# First pass: process first 8 elements into 4 temps
for i in T.unroll(4):
if accum and i == 0:
temp[i] = reduce3_func(src[src_base + 2 * i], src[src_base + 2 * i + 1], dst[tuple(dst_st)]) # noqa: E501
else:
temp[i] = op_func(src[src_base + 2 * i], src[src_base + 2 * i + 1])
# Process remaining full chunks of 8
for outer in T.serial(num_full_chunks - 1):
for i in T.unroll(4):
temp[i] = reduce3_func(
temp[i],
src[src_base + 8 * (outer + 1) + 2 * i],
src[src_base + 8 * (outer + 1) + 2 * i + 1],
)
# Process remainder elements (0 to 7 elements)
for i in T.serial(remainder):
temp[0] = op_func(temp[0], src[src_base + remainder_base + i])
# Final merge: combine 4 temps into result
dst[tuple(dst_st)] = op_func(temp[0], temp[1])
dst[tuple(dst_st)] = reduce3_func(dst[tuple(dst_st)], temp[2], temp[3])
# fmt: on
return impl
def _sm100_packed_add_sum_impl(op: TilePrimitiveCall, op_type: ReduceOpType, sctx: DispatchContext):
op = TilePrimitiveCall.downcast(op)
return _emit_reduction_local_thread_packed_add_sum(op.output, op.input, op.accum, op_type, sctx)
def _sm100_3input_maxmin_impl(op: TilePrimitiveCall, op_type: ReduceOpType, sctx: DispatchContext):
op = TilePrimitiveCall.downcast(op)
return _emit_reduction_local_thread_3input_maxmin(op.output, op.input, op.accum, op_type, sctx)
_optimized_local_reduction_predicates = [
predicate("exec_scope", exec_scope_ok, expected_scopes=["thread"]),
predicate("local_scope", _local_scope_match),
predicate("dst_len", _dst_len_ok, expected_len=1),
predicate("src_ndim", _src_ndim_ok, expected_ndim=1),
predicate("dtype", _dtype_ok, expected_dtype="float32"),
predicate("sm_version", sm_version_ok, min_version=100),
predicate("reduction_len", _reduction_len_ok, min_len=8),
]
_optimized_impl_table = {
ReduceOpType.SUM: ("packed_add_sum", _sm100_packed_add_sum_impl),
ReduceOpType.MAX: ("3input_maxmin", _sm100_3input_maxmin_impl),
ReduceOpType.MIN: ("3input_maxmin", _sm100_3input_maxmin_impl),
}
# ---------------------------------------------------------------------------
# Registration: SM100+ optimized local reduction (priority=20)
# ---------------------------------------------------------------------------
for op_name, op_type in [
("sum", ReduceOpType.SUM),
("max", ReduceOpType.MAX),
("min", ReduceOpType.MIN),
]:
variant_name, optimized_impl = _optimized_impl_table[op_type]
@register_dispatch(
op_name,
"cuda",
variant=variant_name,
priority=20,
when=_optimized_local_reduction_predicates,
)
def _optimized_dispatch(
op: TilePrimitiveCall, sctx: DispatchContext, _impl=optimized_impl, _op_type=op_type
) -> PrimFunc:
op = TilePrimitiveCall.downcast(op)
return _impl(op, _op_type, sctx)
@@ -0,0 +1,262 @@
# 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.
"""Shared helpers for reduction operator dispatches on CUDA targets."""
import functools
import math
import operator
from tvm.arith.analyzer import Analyzer
from tvm.script import tirx as T
from tvm.tirx import BufferRegion
from tvm.tirx.operator.tile_primitive import DispatchContext
from tvm.tirx.operator.tile_primitive.common import ReduceOpType
from tvm.tirx.stmt import TilePrimitiveCall
from ..common import match_scope
reduce_op_table = {
ReduceOpType.SUM: lambda a, b: a + b,
ReduceOpType.MAX: T.max,
ReduceOpType.MIN: T.min,
}
def reduce_default_value_table(dtype):
return {
ReduceOpType.SUM: 0.0,
ReduceOpType.MAX: T.min_value(dtype),
ReduceOpType.MIN: T.max_value(dtype),
}
def _reduction_args(
op: TilePrimitiveCall,
) -> tuple[BufferRegion, BufferRegion, tuple[int, ...], bool, dict]:
"""Parse ReduceOp -> (dst, src, reduce_axes, accum, config)."""
op = TilePrimitiveCall.downcast(op)
dst = op.output
src = op.input
reduce_axes = tuple(int(a) for a in op.reduce_axes)
accum = op.accum
config = op.config
return dst, src, reduce_axes, accum, config
def _match_reduction_storage_scope(
op: TilePrimitiveCall, sctx: DispatchContext, expected_scope: list[str]
) -> tuple[bool, str | None]:
"""Check that dst and src scopes match one of the expected patterns."""
op = TilePrimitiveCall.downcast(op)
dst_scope = op.output.buffer.scope()
src_scope = op.input.buffer.scope()
ok = any(match_scope(dst_scope, p) and match_scope(src_scope, p) for p in expected_scope)
msg = f"storage scope mismatch: dst {dst_scope}, src {src_scope}; expected {expected_scope}"
return (ok, None if ok else msg)
def _analyze_axes(src_ndim: int, reduce_axes: tuple[int, ...]) -> tuple[list[int], list[int]]:
"""Normalize negative axes -> (reduce_dim_set, spatial_dim_list)."""
reduce_dims = set()
for ax in reduce_axes:
a = ax if ax >= 0 else ax + src_ndim
assert 0 <= a < src_ndim, f"reduce axis {ax} out of range for ndim={src_ndim}"
reduce_dims.add(a)
spatial_dims = [d for d in range(src_ndim) if d not in reduce_dims]
return sorted(reduce_dims), spatial_dims
def _analyze_layout_dims(layout, shape):
"""layout.group(shape) -> decompose each dim into thread/local iters.
Returns list of per-dim (thread_extent, local_extent, thread_strides):
thread_extent = product of thread iter extents in this dim
local_extent = product of local iter extents in this dim
thread_strides = list of (stride, extent) for thread iters in this dim
"""
grouped, seps = layout.group(list(shape))
result = []
for d in range(len(shape)):
shard_range = list(range(seps[d], seps[d + 1]))
thread_extent = 1
local_extent = 1
thread_strides = []
for s_idx in shard_range:
it = grouped.shard[s_idx]
if it.axis.is_thread():
thread_extent *= it.extent
thread_strides.append((it.stride, it.extent))
else:
local_extent *= it.extent
result.append((thread_extent, local_extent, thread_strides))
return result
def _compute_shuffle_masks(dim_info, reduce_dims: set[int]) -> list[int]:
"""From reduction dims' thread iter (stride, extent) pairs, compute XOR masks.
For each thread iter in a reduction dim:
masks += [stride * 2^i for i in range(log2(extent))]
Sorted ascending.
"""
masks = []
for d in reduce_dims:
_, _, thread_strides = dim_info[d]
for stride, extent in thread_strides:
ext_int = int(extent) if hasattr(extent, "__int__") else extent
n_bits = int(math.log2(ext_int))
for i in range(n_bits):
stride_int = int(stride) if hasattr(stride, "__int__") else stride
masks.append(stride_int * (1 << i))
masks.sort()
return masks
def _build_local_dim_map(layout, buffer_shape):
"""Map original dim index to position in get_local_region output (None if pure-thread)."""
grouped, seps = layout.group(list(buffer_shape))
dim_map = {}
local_pos = 0
for d in range(len(buffer_shape)):
shard_range = list(range(seps[d], seps[d + 1]))
has_local = any(not grouped.shard[s].axis.is_thread() for s in shard_range)
if has_local:
dim_map[d] = local_pos
local_pos += 1
else:
dim_map[d] = None
return dim_map
def _validate_reduction_layout(
src_layout, dst_layout, src_shape, dst_shape, reduce_dims: list[int]
) -> tuple[bool, str | None]:
"""Validate that spatial dims of src/dst have matching thread+local structure,
and that reduction dims in dst have local_extent == 1.
"""
src_dim_info = _analyze_layout_dims(src_layout, src_shape)
dst_dim_info = _analyze_layout_dims(dst_layout, dst_shape)
analyzer = Analyzer()
# Spatial dims: src/dst must match in both thread and local extents.
# Reduce dims: src/dst thread extent must match, and dst local extent must be 1.
# get expected simplified dst layout
expected_dst_dim = []
for src_idx in range(len(src_shape)):
if analyzer.can_prove_equal(src_dim_info[src_idx][0], 1) and analyzer.can_prove_equal(
src_dim_info[src_idx][1], 1
):
continue # skip if extent=1
if src_idx in reduce_dims: # reduce dims
if not analyzer.can_prove_equal(src_dim_info[src_idx][0], 1):
expected_dst_dim.append((src_dim_info[src_idx][0], 1))
else: # spatial dims
expected_dst_dim.append((src_dim_info[src_idx][0], src_dim_info[src_idx][1]))
# check dst layout
check_idx = 0
for dst_idx in range(len(dst_shape)):
if analyzer.can_prove_equal(dst_dim_info[dst_idx][0], 1) and analyzer.can_prove_equal(
dst_dim_info[dst_idx][1], 1
):
continue
if not (
analyzer.can_prove_equal(dst_dim_info[dst_idx][0], expected_dst_dim[check_idx][0])
and analyzer.can_prove_equal(dst_dim_info[dst_idx][1], expected_dst_dim[check_idx][1])
):
return False, "mismatch dst/src layout for reduction"
check_idx += 1
if check_idx != len(expected_dst_dim):
return False, "mismatch dst/src layout for reduction"
return True, None
def build_src_indices(spa_fused, red_fused, spatial_dims, reduce_dims, src_extent, src_st):
"""Combine spatial and reduction indices into full src index tuple."""
# Build index helpers that work with the explicit axis split
def get_spatial_or_reduction_src_indices(spa_or_red_fused, is_spatial):
dims = spatial_dims if is_spatial else reduce_dims
spa_extents = [src_extent[d] for d in dims]
indices = []
rem = spa_or_red_fused
for e in reversed(spa_extents):
indices.append(rem % e)
rem //= e
indices.reverse()
return [idx + src_st[d] for idx, d in zip(indices, dims)]
spa_vals = get_spatial_or_reduction_src_indices(spa_fused, is_spatial=True)
red_vals = get_spatial_or_reduction_src_indices(red_fused, is_spatial=False)
full = [None] * len(src_extent)
for i, d in enumerate(spatial_dims):
full[d] = spa_vals[i]
for i, d in enumerate(reduce_dims):
full[d] = red_vals[i]
return full
_REDUCE_OP_TO_STR = {ReduceOpType.SUM: "sum", ReduceOpType.MAX: "max", ReduceOpType.MIN: "min"}
def _dtype_ok(op: TilePrimitiveCall, sctx: DispatchContext, expected_dtype: str):
op = TilePrimitiveCall.downcast(op)
dtype = op.input.buffer.dtype
ok = dtype == expected_dtype
return (ok, None if ok else f"dtype {dtype} != {expected_dtype}")
def _reduction_len_ok(op: TilePrimitiveCall, sctx: DispatchContext, min_len: int):
op = TilePrimitiveCall.downcast(op)
src_extent = [r.extent for r in op.input.region]
reduction_len = functools.reduce(operator.mul, src_extent, 1)
ok = reduction_len >= min_len
return (ok, None if ok else f"reduction_len {reduction_len} < {min_len}")
def _dst_len_ok(op: TilePrimitiveCall, sctx: DispatchContext, expected_len: int):
op = TilePrimitiveCall.downcast(op)
dst_extent = [r.extent for r in op.output.region]
dst_len = functools.reduce(operator.mul, dst_extent, 1)
ok = dst_len == expected_len
return (ok, None if ok else f"dst_len {dst_len} != {expected_len}")
def _src_ndim_ok(op: TilePrimitiveCall, sctx: DispatchContext, expected_ndim: int):
op = TilePrimitiveCall.downcast(op)
src_extent = [r.extent for r in op.input.region]
ok = len(src_extent) == expected_ndim
return (ok, None if ok else f"src ndim {len(src_extent)} != {expected_ndim}")
def _local_scope_match(op: TilePrimitiveCall, sctx: DispatchContext):
op = TilePrimitiveCall.downcast(op)
src, dst = op.input.buffer, op.output.buffer
ok = all(
[
src.scope() == "local",
dst.scope() == "local",
src.dtype == dst.dtype,
sctx.is_target("cuda"),
]
)
if not ok:
return (False, "src/dst must be local scope with matching dtype on CUDA")
return (True, None)
@@ -0,0 +1,117 @@
# 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.
"""TMA (Tensor Memory Accelerator) utilities for CUDA op dispatches."""
import copy
from enum import Enum
import tvm
from tvm.arith.analyzer import Analyzer
from tvm.tirx.layout import ComposeLayout, Layout, S, SwizzleLayout, TileLayout
class SwizzleMode(Enum):
"""The swizzle mode of the TMA."""
SWIZZLE_NONE = 0
SWIZZLE_32B_ATOM = 1
SWIZZLE_64B_ATOM = 2
SWIZZLE_128B_ATOM = 3
def mma_atom_layout(dtype: str, swizzle_mode: SwizzleMode | int) -> SwizzleLayout:
"""Generate the MMA-compatible shared-memory atom layout."""
bits = tvm.DataType(dtype).bits
if isinstance(swizzle_mode, int):
swizzle_mode = SwizzleMode(swizzle_mode)
return SwizzleLayout(
per_element=(128 // bits).bit_length() - 1, swizzle_len=swizzle_mode.value, atom_len=3
)
def mma_atom_shape(dtype: str, swizzle_mode: SwizzleMode | int, shape: list[int] | None = None):
"""Generate the MMA-compatible shared-memory atom shape."""
bits = tvm.DataType(dtype).bits
if isinstance(swizzle_mode, int):
swizzle_mode = SwizzleMode(swizzle_mode)
atom_shape = {
SwizzleMode.SWIZZLE_32B_ATOM: [8, 256],
SwizzleMode.SWIZZLE_64B_ATOM: [8, 512],
SwizzleMode.SWIZZLE_128B_ATOM: [8, 1024],
}[swizzle_mode]
atom_shape[-1] //= bits
if shape is None:
return atom_shape
atom_shape = [1] * (len(shape) - len(atom_shape)) + atom_shape
return atom_shape
def mma_shared_layout(dtype: str, swizzle_mode: SwizzleMode | int, shape) -> Layout:
"""Generate the MMA-compatible shared-memory layout for shape and dtype.
It uses a default tiling strategy to tile the TMA atom layout into the shared memory.
"""
if isinstance(swizzle_mode, int):
swizzle_mode = SwizzleMode(swizzle_mode)
if swizzle_mode == SwizzleMode.SWIZZLE_NONE:
return TileLayout(S[tuple(shape)]).canonicalize()
atom_shape = mma_atom_shape(dtype, swizzle_mode, shape)
layout = mma_atom_layout(dtype, swizzle_mode)
tile_to_shape = copy.copy(atom_shape)
tile_to_shape[-2] = shape[-2]
return layout.tile_to(tile_to_shape, atom_shape).tile_to(shape, tile_to_shape).canonicalize()
# Backward-compatible aliases kept during the alloc_mma migration.
tma_atom_layout = mma_atom_layout
tma_atom_shape = mma_atom_shape
tma_shared_layout = mma_shared_layout
def tma_atom_compatible(dst_shape, dst_st, dst_extent, atom_shape):
"""Check if the copy region in dst is compatible with the TMA atom shape."""
analyzer = Analyzer()
for i, _ in enumerate(dst_st):
if any(
not analyzer.can_prove_equal(x % atom_shape[i], 0)
for x in [dst_shape[i], dst_st[i], dst_extent[i]]
):
return False
return True
def get_swizzle_mode_from_layout(layout: Layout) -> SwizzleMode | None:
"""Extract swizzle mode from a shared memory layout."""
if isinstance(layout, ComposeLayout):
swizzle = layout.swizzle # SwizzleLayout is named 'swizzle' in ComposeLayout
swizzle_len = swizzle.swizzle_len
elif isinstance(layout, SwizzleLayout):
swizzle_len = layout.swizzle_len
elif isinstance(layout, TileLayout):
# TileLayout without SwizzleLayout means no swizzle (mode 0)
return SwizzleMode.SWIZZLE_NONE
else:
return None
# Map swizzle_len to SwizzleMode
return {
0: SwizzleMode.SWIZZLE_NONE,
1: SwizzleMode.SWIZZLE_32B_ATOM,
2: SwizzleMode.SWIZZLE_64B_ATOM,
3: SwizzleMode.SWIZZLE_128B_ATOM,
}.get(swizzle_len)
+573
View File
@@ -0,0 +1,573 @@
# 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.
"""CUDA TVMScript namespaces."""
from __future__ import annotations
from collections.abc import Callable
from typing import Any
from tvm.backend.cuda import op as _cuda_op
from tvm.tirx import Buffer
from tvm.tirx import op as _tir_op
from tvm.tirx.script.builder.ir import _dtype_forward, _op_wrapper
# pylint: disable=protected-access
def _ptx_ldg32(reg, guard, addr, local_addr):
if isinstance(addr, Buffer):
addr = addr[0]
return _tir_op.call_intrin(reg.ty, "tirx.ptx.ldg32", reg, guard, addr, local_addr)
_ptx_ldg32.__tir_op_name__ = "ptx.ldg32"
class PTXNamespace:
"""The PTX instruction submodule."""
def __init__(self):
self.ldg32 = _ptx_ldg32
self.ldmatrix = _dtype_forward(_cuda_op.ptx_ldmatrix)
# Apache-compatible variant. Same lowered intrinsic as
# ``ldmatrix`` but accepts the historical ``(trans, num, dtype,
# local_ptr, local_offset, smem_ptr, smem_offset)`` form. Coexists
# with the fork-native version so upstream-derived tests keep
# working without rewriting their tirx code.
self.ldmatrix_legacy = _dtype_forward(_cuda_op.ptx_ldmatrix_legacy)
self.stmatrix = _op_wrapper(_cuda_op.ptx_stmatrix)
self.setmaxnreg: Callable[..., Any] = _op_wrapper(_cuda_op.ptx_setmaxnreg)
self.elect_sync: Callable[..., Any] = _op_wrapper(_cuda_op.ptx_elect_sync)
self.clc_try_cancel = _op_wrapper(_cuda_op.ptx_clc_try_cancel)
self.clc_query_cancel = _op_wrapper(_cuda_op.ptx_clc_query_cancel)
self.fetch_register: Callable[..., Any] = _op_wrapper(_cuda_op.ptx_fetch_register)
self.ld = _op_wrapper(_cuda_op.ptx_ld)
self.ld_acquire = _op_wrapper(_cuda_op.ptx_ld_acquire)
self.ld_relaxed = _op_wrapper(_cuda_op.ptx_ld_relaxed)
self.ld_volatile = _op_wrapper(_cuda_op.ptx_ld_volatile)
self.ld_mmio = _op_wrapper(_cuda_op.ptx_ld_mmio)
self.ld_global_acquire = _op_wrapper(_cuda_op.ptx_ld_global_acquire)
self.red_scalar = _op_wrapper(_cuda_op.ptx_red_scalar)
self.atom_scalar = _op_wrapper(_cuda_op.ptx_atom_scalar)
self.prefetch_tensormap = _op_wrapper(_cuda_op.ptx_prefetch_tensormap)
self.mbarrier_test_wait_parity = _op_wrapper(_cuda_op.ptx_mbarrier_test_wait_parity)
self.cp_async_bulk_g2s_cta = _op_wrapper(_cuda_op.ptx_cp_async_bulk_g2s_cta)
self.cp_async_bulk_g2s_cluster = _op_wrapper(_cuda_op.ptx_cp_async_bulk_g2s_cluster)
self.cp_async_bulk_s2s_cluster = _op_wrapper(_cuda_op.ptx_cp_async_bulk_s2s_cluster)
self.cp_async_bulk_s2g = _op_wrapper(_cuda_op.ptx_cp_async_bulk_s2g)
self.st = _op_wrapper(_cuda_op.ptx_st)
self.st_relaxed = _op_wrapper(_cuda_op.ptx_st_relaxed)
self.st_release = _op_wrapper(_cuda_op.ptx_st_release)
self.st_volatile = _op_wrapper(_cuda_op.ptx_st_volatile)
self.st_mmio = _op_wrapper(_cuda_op.ptx_st_mmio)
self.st_bulk = _op_wrapper(_cuda_op.ptx_st_bulk)
self.fns_b32 = _op_wrapper(_cuda_op.ptx_fns_b32)
self.add_rn_f32_bf16 = _op_wrapper(_cuda_op.ptx_add_rn_f32_bf16)
self.mapa = _op_wrapper(_cuda_op.ptx_mapa)
self.map_shared_rank = _op_wrapper(_cuda_op.ptx_map_shared_rank)
self.any_sync = _op_wrapper(_cuda_op.ptx_any_sync)
# Math operations
self.exp2 = _op_wrapper(_cuda_op.ptx_exp2)
self.rcp = _op_wrapper(_cuda_op.ptx_rcp)
self.reduce3_min_f32 = _op_wrapper(_cuda_op.ptx_reduce3_min_f32)
self.reduce3_max_f32 = _op_wrapper(_cuda_op.ptx_reduce3_max_f32)
# add/sub/mul/fma DPS form: (d_addr, a, b[, c], *, rounding, ftz[, sat])
self.add_f32 = _op_wrapper(_cuda_op.ptx_add_f32)
self.add_f32x2 = _op_wrapper(_cuda_op.ptx_add_f32x2)
self.add_f64 = _op_wrapper(_cuda_op.ptx_add_f64)
self.sub_f32 = _op_wrapper(_cuda_op.ptx_sub_f32)
self.sub_f32x2 = _op_wrapper(_cuda_op.ptx_sub_f32x2)
self.sub_f64 = _op_wrapper(_cuda_op.ptx_sub_f64)
self.mul_f32 = _op_wrapper(_cuda_op.ptx_mul_f32)
self.mul_f32x2 = _op_wrapper(_cuda_op.ptx_mul_f32x2)
self.mul_f64 = _op_wrapper(_cuda_op.ptx_mul_f64)
self.fma_f32 = _op_wrapper(_cuda_op.ptx_fma_f32)
self.fma_f32x2 = _op_wrapper(_cuda_op.ptx_fma_f32x2)
self.fma_f64 = _op_wrapper(_cuda_op.ptx_fma_f64)
self.max_f32 = _op_wrapper(_cuda_op.ptx_max_f32)
self.mma = MmaNamespace()
self.cp_async = CpAsyncNamespace()
self.wgmma = WgmmaNamespace()
self.mbarrier = MbarrierNamespace()
self.tcgen05 = Tcgen05Namespace()
self.bar = BarNamespace()
self.barrier = BarrierNamespace()
self.fence = FenceNamespace()
self.griddepcontrol = GriddepcontrolNamespace()
class MmaNamespace:
"""The MMA instruction submodule."""
def __init__(self):
self.sp = _dtype_forward(_cuda_op.ptx_mma_sp)
# Apache-compatible variant of ptx_mma. Coexists with the
# fork-native ``__call__`` form (``T.ptx.mma(...)``).
self.legacy = _dtype_forward(_cuda_op.ptx_mma_legacy)
# __call__ corresponds to ptx_mma
self.__tir_call_op_name__ = "ptx_mma"
def __call__(self, *args, **kwds):
return _dtype_forward(_cuda_op.ptx_mma)(*args, **kwds)
class CpAsyncNamespace:
"""The CpAsync instruction submodule."""
def __init__(self):
self.commit_group = _op_wrapper(_cuda_op.ptx_cp_async_commit_group)
self.wait_group = _op_wrapper(_cuda_op.ptx_cp_async_wait_group)
# Legacy variant: takes (dst_ptr, dst_offset, src_ptr, src_offset,
# cp_size). Offsets are folded into the pointers; coexists with
# the fork-native ``__call__`` form.
self.legacy = _dtype_forward(_cuda_op.ptx_cp_async_legacy)
self.bulk = CpAsyncBulkNamespace()
self.mbarrier = CpAsyncMbarrierNamespace()
def __call__(self, *args, **kwds):
# Accept the legacy 6-arg form ``(elem_dtype, dst, dst_off, src,
# src_off, cp_size)`` that the printer round-trips for the raw
# ``tirx.ptx.cp_async`` Call emitted by
# ``tvm.backend.cuda.transform.InjectPTXAsyncCopy``. The pass-emitted
# Call has 5 args (no ``tvm_access_ptr`` fold) and a
# per-element-dtype Call.dtype, so build it directly.
if len(args) == 6 and isinstance(args[0], str) and "dtype" not in kwds:
import tvm
elem_dtype, dst, dst_off, src, src_off, cp_size = args
return tvm.ir.Call(
tvm.ir.Op.get("tirx.ptx.cp_async_raw"),
[dst, dst_off, src, src_off, cp_size],
ret_ty=tvm.ir.PrimType(elem_dtype),
)
return _dtype_forward(_cuda_op.ptx_cp_async)(*args, **kwds)
# __call__ corresponds to ptx_cp_async
__tir_call_op_name__ = "ptx_cp_async"
class CpAsyncBulkNamespace:
"""The CpAsyncBulk instruction submodule."""
def __init__(self):
self.commit_group = _op_wrapper(_cuda_op.ptx_cp_async_bulk_commit_group)
self.wait_group = _op_wrapper(_cuda_op.ptx_cp_async_bulk_wait_group)
self.tensor = CpAsyncBulkTensorNamespace()
self.s2c = _op_wrapper(_cuda_op.ptx_cp_async_bulk_shared_to_cluster)
def __call__(self, *args, **kwds):
return _dtype_forward(_cuda_op.ptx_cp_async_bulk)(*args, **kwds)
# __call__ corresponds to ptx_cp_async_bulk
__tir_call_op_name__ = "ptx_cp_async_bulk"
class CpAsyncBulkTensorNamespace:
"""The CpAsyncBulkTensor instruction submodule."""
def __init__(self):
self.g2c = _op_wrapper(_cuda_op.ptx_cp_async_bulk_tensor_global_to_cluster)
self.g2c_tile_gather4 = _op_wrapper(
_cuda_op.ptx_cp_async_bulk_tensor_tile_gather4_global_to_cluster
)
self.s2g = _op_wrapper(_cuda_op.ptx_cp_async_bulk_tensor_shared_to_global)
self.s2g_reduce = _op_wrapper(_cuda_op.ptx_cp_async_bulk_tensor_shared_to_global_reduce)
self.g2c_prefetch = _op_wrapper(
_cuda_op.ptx_cp_async_bulk_tensor_global_to_cluster_prefetch
)
@staticmethod
def g2c_bar_addr(
dim,
dst_ptr,
bar_addr,
tensormap_addr,
cta_mask,
cta_group,
cache_hint,
*coords,
cache_policy=None,
):
_cuda_op._choice("cta_group", cta_group, _cuda_op._TCGEN05_CTA_GROUP)
cache_policy, has_cache_policy = _cuda_op._resolve_cache_policy(cache_hint, cache_policy)
return _tir_op.call_intrin(
"",
"tirx.ptx.cp_async_bulk_tensor_global_to_cluster",
dim,
dst_ptr,
bar_addr,
tensormap_addr,
cta_mask,
cta_group,
cache_policy,
int(has_cache_policy),
1,
*coords,
)
@staticmethod
def g2c_tile_gather4_bar_addr(
dim,
dst_ptr,
bar_addr,
tensormap_addr,
cta_mask,
cta_group,
cache_hint,
*coords,
cache_policy=None,
):
_cuda_op._choice("cta_group", cta_group, _cuda_op._TCGEN05_CTA_GROUP)
cache_policy, has_cache_policy = _cuda_op._resolve_cache_policy(cache_hint, cache_policy)
return _tir_op.call_intrin(
"",
"tirx.ptx.cp_async_bulk_tensor_tile_gather4_global_to_cluster",
dim,
dst_ptr,
bar_addr,
tensormap_addr,
cta_mask,
cta_group,
cache_policy,
int(has_cache_policy),
1,
*coords,
)
class CpAsyncMbarrierNamespace:
"""The CpAsyncMbarrier instruction submodule."""
def __init__(self):
self.arrive = _op_wrapper(_cuda_op.ptx_cp_async_mbarrier_arrive)
class WgmmaNamespace:
"""The WGMMA instruction submodule."""
def __init__(self):
self.fence: Callable[..., Any] = _op_wrapper(_cuda_op.ptx_wgmma_fence)
self.commit_group = _op_wrapper(_cuda_op.ptx_wgmma_commit_group)
self.wait_group = _op_wrapper(_cuda_op.ptx_wgmma_wait_group)
self.noop_barrier = _op_wrapper(_cuda_op.ptx_wgmma_noop_barrier)
self.mma_async = WgmmaMmaAsyncNamespace()
self.encode_matrix_descriptor = _op_wrapper(_cuda_op.ptx_wgmma_encode_matrix_descriptor)
class WgmmaMmaAsyncNamespace:
"""The WGMMA MMAAsync instruction submodule."""
def __init__(self):
self.ss = _op_wrapper(_cuda_op.ptx_wgmma_mma_async_ss)
self.rs = _op_wrapper(_cuda_op.ptx_wgmma_mma_async_rs)
class MbarrierNamespace:
"""The Mbarrier instruction submodule."""
def __init__(self):
self.init = _op_wrapper(_cuda_op.ptx_mbarrier_init)
self.try_wait = _op_wrapper(_cuda_op.ptx_mbarrier_try_wait)
self.try_wait_once = _op_wrapper(_cuda_op.ptx_mbarrier_try_wait_once)
self.try_wait_acquire_cluster = _op_wrapper(_cuda_op.ptx_mbarrier_try_wait_acquire_cluster)
self.arrive = MbarrierArriveNamespace()
class MbarrierArriveNamespace:
"""The Mbarrier Arrive instruction submodule."""
def __init__(self):
self.expect_tx = _op_wrapper(_cuda_op.ptx_mbarrier_arrive_expect_tx)
self.cluster_count = _op_wrapper(_cuda_op.ptx_mbarrier_arrive_cluster_count)
def __call__(self, *args, **kwds):
return _op_wrapper(_cuda_op.ptx_mbarrier_arrive)(*args, **kwds)
# __call__ corresponds to ptx_mbarrier_arrive
__tir_call_op_name__ = "ptx_mbarrier_arrive"
class Tcgen05Namespace:
"""The Tcgen05 instruction submodule."""
def __init__(self):
self.alloc = _op_wrapper(_cuda_op.ptx_tcgen05_alloc)
self.dealloc = _op_wrapper(_cuda_op.ptx_tcgen05_dealloc)
self.relinquish_alloc_permit = _op_wrapper(_cuda_op.ptx_tcgen05_relinquish_alloc_permit)
self.encode_matrix_descriptor = _op_wrapper(_cuda_op.ptx_tcgen05_encode_matrix_descriptor)
self.encode_instr_descriptor = _op_wrapper(_cuda_op.ptx_tcgen05_encode_instr_descriptor)
self.encode_instr_descriptor_block_scaled = _op_wrapper(
_cuda_op.ptx_tcgen05_encode_instr_descriptor_block_scaled
)
self.ld = _op_wrapper(_cuda_op.ptx_tcgen05_ld)
self.st = _op_wrapper(_cuda_op.ptx_tcgen05_st)
self.cp = _op_wrapper(_cuda_op.ptx_tcgen05_cp)
self.shift = _op_wrapper(_cuda_op.ptx_tcgen05_shift)
self.commit = _op_wrapper(_cuda_op.ptx_tcgen05_commit)
self.wait = Tcgen05WaitNamespace()
self.mma = Tcgen05MmaNamespace()
self.fence = Tcgen05FenceNamespace()
class Tcgen05FenceNamespace:
"""The Tcgen05 Fence instruction submodule."""
def __init__(self):
self.before_thread_sync = _op_wrapper(_cuda_op.ptx_tcgen05_fence_before_thread_sync)
self.after_thread_sync = _op_wrapper(_cuda_op.ptx_tcgen05_fence_after_thread_sync)
class Tcgen05MmaNamespace:
"""The Tcgen05 MMA instruction submodule."""
def __init__(self):
self.block_scale = _op_wrapper(_cuda_op.ptx_tcgen05_mma_block_scale)
self.sp = Tcgen05MmaSpNamespace()
def __call__(self, *args, **kwds):
return _op_wrapper(_cuda_op.ptx_tcgen05_mma)(*args, **kwds)
# __call__ corresponds to ptx_tcgen05_mma
__tir_call_op_name__ = "ptx_tcgen05_mma"
class Tcgen05MmaSpNamespace:
"""Tcgen05 Sparse MMA instruction submodule."""
def __init__(self):
self.block_scale = _op_wrapper(_cuda_op.ptx_tcgen05_mma_sp_block_scale)
def __call__(self, *args, **kwds):
return _op_wrapper(_cuda_op.ptx_tcgen05_mma_sp)(*args, **kwds)
# __call__ corresponds to ptx_tcgen05_mma_sp
__tir_call_op_name__ = "ptx_tcgen05_mma_sp"
class Tcgen05WaitNamespace:
"""The Tcgen05 Wait instruction submodule."""
def __init__(self):
self.ld = _op_wrapper(_cuda_op.ptx_tcgen05_wait_ld)
self.st = _op_wrapper(_cuda_op.ptx_tcgen05_wait_st)
class BarNamespace:
"""The Bar instruction submodule."""
def __init__(self):
self.arrive = _op_wrapper(_cuda_op.ptx_bar_arrive)
self.sync = _op_wrapper(_cuda_op.ptx_bar_sync)
class BarrierNamespace:
"""The Barrier instruction submodule."""
def __init__(self):
self.cluster = BarrierClusterNamespace()
class BarrierClusterNamespace:
"""The BarrierCluster instruction submodule."""
def __init__(self):
self.arrive = _op_wrapper(_cuda_op.ptx_barrier_cluster_arrive)
self.wait = _op_wrapper(_cuda_op.ptx_barrier_cluster_wait)
class FenceNamespace:
"""PTX fence instruction submodule."""
def __init__(self):
self.proxy_async = _op_wrapper(_cuda_op.ptx_fence_proxy_async)
self.mbarrier_init = _op_wrapper(_cuda_op.ptx_fence_mbarrier_init)
def __call__(self, *args, **kwds):
return _op_wrapper(_cuda_op.ptx_fence)(*args, **kwds)
__tir_call_op_name__ = "ptx_fence"
class GriddepcontrolNamespace:
"""PTX griddepcontrol instruction submodule (sm_90+)."""
def __init__(self):
self.wait = _op_wrapper(_cuda_op.ptx_griddepcontrol_wait)
self.launch_dependents = _op_wrapper(_cuda_op.ptx_griddepcontrol_launch_dependents)
class CUDANamespace:
"""The CUDA intrinsics submodule."""
def __init__(self):
self.atomic_add = _op_wrapper(_cuda_op.cuda_atomic_add)
self.thread_fence = _op_wrapper(_cuda_op.cuda_thread_fence)
self.warpgroup_sync = _op_wrapper(_cuda_op.cuda_warpgroup_sync)
self.warp_sync = _op_wrapper(_cuda_op.cuda_warp_sync)
self.warp_reduce = _op_wrapper(_cuda_op.cuda_warp_reduce)
self.warp_sum = _op_wrapper(_cuda_op.cuda_warp_sum)
self.warp_max = _op_wrapper(_cuda_op.cuda_warp_max)
self.warp_min = _op_wrapper(_cuda_op.cuda_warp_min)
self.cta_reduce = _op_wrapper(_cuda_op.cuda_cta_reduce)
self.cta_sum = _op_wrapper(_cuda_op.cuda_cta_sum)
self.cta_max = _op_wrapper(_cuda_op.cuda_cta_max)
self.cta_min = _op_wrapper(_cuda_op.cuda_cta_min)
self.cta_sync = _op_wrapper(_cuda_op.cuda_cta_sync)
self.grid_sync = _op_wrapper(_cuda_op.cuda_grid_sync)
self.cluster_sync = _op_wrapper(_cuda_op.cuda_cluster_sync)
self.thread_rank = _op_wrapper(_cuda_op.cuda_thread_rank)
self.trap_when_assert_failed = _op_wrapper(_cuda_op.cuda_trap_when_assert_failed)
self.runtime_instr_desc = _op_wrapper(_cuda_op.cuda_runtime_instr_desc)
self.half2float = _op_wrapper(_cuda_op.cuda_half2float)
self.bfloat162float = _op_wrapper(_cuda_op.cuda_bfloat162float)
self.float22half2 = _op_wrapper(_cuda_op.cuda_float22half2)
self.half8tofloat8 = _op_wrapper(_cuda_op.cuda_half8tofloat8)
self.float8tohalf8 = _op_wrapper(_cuda_op.cuda_float8tohalf8)
self.syncthreads_and = _op_wrapper(_cuda_op.cuda_syncthreads_and)
self.syncthreads_or = _op_wrapper(_cuda_op.cuda_syncthreads_or)
self.nano_sleep = _op_wrapper(_cuda_op.cuda_nano_sleep)
self.atomic_cas = _op_wrapper(_cuda_op.cuda_atomic_cas)
self.func_call = _op_wrapper(_cuda_op.cuda_func_call)
self.printf = _op_wrapper(_cuda_op.cuda_printf)
self.ldg = _op_wrapper(_cuda_op.cuda_ldg)
self.get_tmem_addr = _op_wrapper(_cuda_op.cuda_get_tmem_addr)
self.cvta_generic_to_shared = _op_wrapper(_cuda_op.cuda_cvta_generic_to_shared)
self.smem_addr_from_uint64 = _op_wrapper(_cuda_op.cuda_smem_addr_from_uint64)
self.sm100_tma_2sm_mbarrier_addr = _op_wrapper(_cuda_op.cuda_sm100_tma_2sm_mbarrier_addr)
self.uint_as_float = _op_wrapper(_cuda_op.cuda_uint_as_float)
self.float_as_uint = _op_wrapper(_cuda_op.cuda_float_as_uint)
self.ballot_sync = _op_wrapper(_cuda_op.cuda_ballot_sync)
self.ffs_u32 = _op_wrapper(_cuda_op.cuda_ffs_u32)
self.reduce_add_sync_u32 = _op_wrapper(_cuda_op.cuda_reduce_add_sync_u32)
self.reduce_min_sync_u32 = _op_wrapper(_cuda_op.cuda_reduce_min_sync_u32)
self.clock64 = _op_wrapper(_cuda_op.cuda_clock64)
self.make_float2 = _op_wrapper(_cuda_op.cuda_make_float2)
self.float2_x = _op_wrapper(_cuda_op.cuda_float2_x)
self.float2_y = _op_wrapper(_cuda_op.cuda_float2_y)
self.fmul2_rn = _op_wrapper(_cuda_op.cuda_fmul2_rn)
self.fadd2_rn = _op_wrapper(_cuda_op.cuda_fadd2_rn)
self.float22bfloat162_rn = _op_wrapper(_cuda_op.cuda_float22bfloat162_rn)
self.float22bfloat162_rn_from_float2 = _op_wrapper(
_cuda_op.cuda_float22bfloat162_rn_from_float2
)
self.bfloat1622float2 = _op_wrapper(_cuda_op.cuda_bfloat1622float2)
self.hmin2 = _op_wrapper(_cuda_op.cuda_hmin2)
self.hmax2 = _op_wrapper(_cuda_op.cuda_hmax2)
self.fp8x4_e4m3_from_float4 = _op_wrapper(_cuda_op.cuda_fp8x4_e4m3_from_float4)
self.timer_init = _op_wrapper(_cuda_op.timer_init_cuda)
self.timer_start = _op_wrapper(_cuda_op.timer_start_cuda)
self.timer_end = _op_wrapper(_cuda_op.timer_end_cuda)
self.timer_finalize = _op_wrapper(_cuda_op.timer_finalize_cuda)
self.mma_store = _dtype_forward(_cuda_op.mma_store)
self.mma_fill = _dtype_forward(_cuda_op.mma_fill)
self.mma_store_legacy = _dtype_forward(_cuda_op.mma_store_legacy)
self.mma_fill_legacy = _dtype_forward(_cuda_op.mma_fill_legacy)
setattr(self, "__shfl_sync", self._shfl_sync)
setattr(self, "__shfl_up_sync", self._shfl_up_sync)
setattr(self, "__shfl_down_sync", self._shfl_down_sync)
setattr(self, "__shfl_xor_sync", self._shfl_xor_sync)
setattr(self, "__activemask", self._activemask)
@staticmethod
def _shfl_sync(mask, var, lane, width):
if isinstance(var, Buffer):
var = var[0]
return _tir_op.call_intrin(var.ty, "tirx.cuda.__shfl_sync", mask, var, lane, width)
@staticmethod
def _shfl_up_sync(mask, var, delta, width):
if isinstance(var, Buffer):
var = var[0]
return _tir_op.call_intrin(var.ty, "tirx.cuda.__shfl_up_sync", mask, var, delta, width)
@staticmethod
def _shfl_down_sync(mask, var, delta, width):
if isinstance(var, Buffer):
var = var[0]
return _tir_op.call_intrin(var.ty, "tirx.cuda.__shfl_down_sync", mask, var, delta, width)
@staticmethod
def _shfl_xor_sync(mask, var, lane_mask, width):
if isinstance(var, Buffer):
var = var[0]
return _tir_op.call_intrin(var.ty, "tirx.cuda.__shfl_xor_sync", mask, var, lane_mask, width)
@staticmethod
def _activemask():
return _tir_op.call_intrin("uint32", "tirx.cuda.__activemask")
class NVSHMEMNamespace:
"""The NVSHMEM intrinsics submodule."""
def __init__(self):
self.my_pe = _op_wrapper(_cuda_op.nvshmem_my_pe)
self.n_pes = _op_wrapper(_cuda_op.nvshmem_n_pes)
self.signal_op = _op_wrapper(_cuda_op.nvshmem_signal_op)
self.wait_until = _op_wrapper(_cuda_op.nvshmem_wait_until)
self.quiet = _op_wrapper(_cuda_op.nvshmem_quiet)
self.fence = _op_wrapper(_cuda_op.nvshmem_fence)
self.barrier_all = _op_wrapper(_cuda_op.nvshmem_barrier_all)
self.getmem_nbi = NVSHMEMGetMemNBINamespace()
self.putmem_nbi = NVSHMEMPutMemNBINamespace()
self.putmem_signal_nbi = NVSHMEMPutMemSignalNBINamespace()
class NVSHMEMGetMemNBINamespace:
"""The NVSHMEM GetMemNBI intrinsics submodule."""
def __init__(self):
self.warp = _op_wrapper(_cuda_op.nvshmem_getmem_nbi_warp)
self.block = _op_wrapper(_cuda_op.nvshmem_getmem_nbi_block)
def __call__(self, *args, **kwds):
return _op_wrapper(_cuda_op.nvshmem_getmem_nbi)(*args, **kwds)
# __call__ corresponds to nvshmem_getmem_nbi
__tir_call_op_name__ = "nvshmem_getmem_nbi"
class NVSHMEMPutMemNBINamespace:
"""The NVSHMEM PutMemNBI intrinsics submodule."""
def __init__(self):
self.warp = _op_wrapper(_cuda_op.nvshmem_putmem_nbi_warp)
self.block = _op_wrapper(_cuda_op.nvshmem_putmem_nbi_block)
def __call__(self, *args, **kwds):
return _op_wrapper(_cuda_op.nvshmem_putmem_nbi)(*args, **kwds)
# __call__ corresponds to nvshmem_putmem_nbi
__tir_call_op_name__ = "nvshmem_putmem_nbi"
class NVSHMEMPutMemSignalNBINamespace:
"""The NVSHMEM PutMemSignalNBI intrinsics submodule."""
def __init__(self):
self.warp = _op_wrapper(_cuda_op.nvshmem_putmem_signal_nbi_warp)
self.block = _op_wrapper(_cuda_op.nvshmem_putmem_signal_nbi_block)
def __call__(self, *args, **kwds):
return _op_wrapper(_cuda_op.nvshmem_putmem_signal_nbi)(*args, **kwds)
# __call__ corresponds to nvshmem_putmem_signal_nbi
__tir_call_op_name__ = "nvshmem_putmem_signal_nbi"
__all__ = ["CUDANamespace", "NVSHMEMNamespace", "PTXNamespace"]
+374
View File
@@ -0,0 +1,374 @@
# 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.
"""NVIDIA CUDA target tags."""
from tvm.target import register_tag
def _register_cuda_tag(name, arch, shared_mem=49152, regs=65536, **extra):
config = {
"kind": "cuda",
"keys": ["cuda", "gpu"],
"arch": arch,
"max_shared_memory_per_block": shared_mem,
"max_threads_per_block": 1024,
"thread_warp_size": 32,
"registers_per_block": regs,
}
config.update(extra)
register_tag(name, config)
def _register_jetson_tag(name, arch, mcpu, num_cores, regs=65536):
register_tag(
name,
{
"kind": "cuda",
"arch": arch,
"max_shared_memory_per_block": 49152,
"max_threads_per_block": 1024,
"thread_warp_size": 32,
"registers_per_block": regs,
"host": {
"kind": "llvm",
"mtriple": "aarch64-linux-gnu",
"mcpu": mcpu,
"num-cores": num_cores,
},
},
)
# =====================================================================
# Data center / Tesla GPUs
# =====================================================================
_register_cuda_tag("nvidia/nvidia-a100", "sm_80", l2_cache_size_bytes=41943040)
_register_cuda_tag("nvidia/nvidia-h100", "sm_90a", l2_cache_size_bytes=52428800)
_register_cuda_tag("nvidia/nvidia-b100", "sm_100a", l2_cache_size_bytes=52428800)
_register_cuda_tag("nvidia/nvidia-a40", "sm_86")
_register_cuda_tag("nvidia/nvidia-a30", "sm_80")
_register_cuda_tag("nvidia/nvidia-a10", "sm_86")
_register_cuda_tag("nvidia/nvidia-a10g", "sm_86")
_register_cuda_tag("nvidia/nvidia-a16", "sm_86")
_register_cuda_tag("nvidia/nvidia-a2", "sm_86")
_register_cuda_tag("nvidia/nvidia-t4", "sm_75")
_register_cuda_tag("nvidia/nvidia-v100", "sm_70")
_register_cuda_tag("nvidia/tesla-p100", "sm_60")
_register_cuda_tag("nvidia/tesla-p40", "sm_61")
_register_cuda_tag("nvidia/tesla-p4", "sm_61")
_register_cuda_tag("nvidia/tesla-m60", "sm_52")
_register_cuda_tag("nvidia/tesla-m40", "sm_52")
_register_cuda_tag("nvidia/tesla-k80", "sm_37")
_register_cuda_tag("nvidia/tesla-k40", "sm_35")
_register_cuda_tag("nvidia/tesla-k20", "sm_35")
_register_cuda_tag("nvidia/tesla-k10", "sm_30")
_register_cuda_tag("nvidia/tesla-c2075", "sm_20", regs=32768)
_register_cuda_tag("nvidia/tesla-c2050", "sm_20", regs=32768)
_register_cuda_tag("nvidia/tesla-c2070", "sm_20", regs=32768)
# =====================================================================
# Quadro / RTX professional desktop GPUs
# =====================================================================
_register_cuda_tag("nvidia/rtx-a6000", "sm_86")
_register_cuda_tag("nvidia/quadro-rtx-8000", "sm_75")
_register_cuda_tag("nvidia/quadro-rtx-6000", "sm_75")
_register_cuda_tag("nvidia/quadro-rtx-5000", "sm_75")
_register_cuda_tag("nvidia/quadro-rtx-4000", "sm_75")
_register_cuda_tag("nvidia/quadro-gv100", "sm_70")
_register_cuda_tag("nvidia/quadro-gp100", "sm_60")
_register_cuda_tag("nvidia/quadro-p6000", "sm_61")
_register_cuda_tag("nvidia/quadro-p5000", "sm_61")
_register_cuda_tag("nvidia/quadro-p4000", "sm_61")
_register_cuda_tag("nvidia/quadro-p2200", "sm_61")
_register_cuda_tag("nvidia/quadro-p2000", "sm_61")
_register_cuda_tag("nvidia/quadro-p1000", "sm_61")
_register_cuda_tag("nvidia/quadro-p620", "sm_61")
_register_cuda_tag("nvidia/quadro-p600", "sm_61")
_register_cuda_tag("nvidia/quadro-p400", "sm_61")
_register_cuda_tag("nvidia/quadro-m6000-24gb", "sm_52")
_register_cuda_tag("nvidia/quadro-m6000", "sm_52")
_register_cuda_tag("nvidia/quadro-k6000", "sm_35")
_register_cuda_tag("nvidia/quadro-m5000", "sm_52")
_register_cuda_tag("nvidia/quadro-k5200", "sm_35")
_register_cuda_tag("nvidia/quadro-k5000", "sm_30")
_register_cuda_tag("nvidia/quadro-m4000", "sm_52")
_register_cuda_tag("nvidia/quadro-k4200", "sm_30")
_register_cuda_tag("nvidia/quadro-k4000", "sm_30")
_register_cuda_tag("nvidia/quadro-m2000", "sm_52")
_register_cuda_tag("nvidia/quadro-k2200", "sm_50")
_register_cuda_tag("nvidia/quadro-k2000", "sm_30")
_register_cuda_tag("nvidia/quadro-k2000d", "sm_30")
_register_cuda_tag("nvidia/quadro-k1200", "sm_50")
_register_cuda_tag("nvidia/quadro-k620", "sm_50")
_register_cuda_tag("nvidia/quadro-k600", "sm_30")
_register_cuda_tag("nvidia/quadro-k420", "sm_30")
_register_cuda_tag("nvidia/quadro-410", "sm_30")
_register_cuda_tag("nvidia/quadro-plex-7000", "sm_20", regs=32768)
# =====================================================================
# Quadro / RTX professional mobile GPUs (Turing)
# =====================================================================
_register_cuda_tag("nvidia/rtx-5000", "sm_75")
_register_cuda_tag("nvidia/rtx-4000", "sm_75")
_register_cuda_tag("nvidia/rtx-3000", "sm_75")
_register_cuda_tag("nvidia/t2000", "sm_75")
_register_cuda_tag("nvidia/t1000", "sm_75")
_register_cuda_tag("nvidia/p620", "sm_61")
_register_cuda_tag("nvidia/p520", "sm_61")
# =====================================================================
# Quadro professional mobile GPUs (Pascal / Maxwell)
# =====================================================================
_register_cuda_tag("nvidia/quadro-p5200", "sm_61")
_register_cuda_tag("nvidia/quadro-p4200", "sm_61")
_register_cuda_tag("nvidia/quadro-p3200", "sm_61")
_register_cuda_tag("nvidia/quadro-p3000", "sm_61")
_register_cuda_tag("nvidia/quadro-p500", "sm_61")
_register_cuda_tag("nvidia/quadro-m5500m", "sm_52")
_register_cuda_tag("nvidia/quadro-m2200", "sm_52")
_register_cuda_tag("nvidia/quadro-m1200", "sm_50")
_register_cuda_tag("nvidia/quadro-m620", "sm_52")
_register_cuda_tag("nvidia/quadro-m520", "sm_50")
# =====================================================================
# Quadro professional mobile GPUs (Kepler / Maxwell)
# =====================================================================
_register_cuda_tag("nvidia/quadro-k6000m", "sm_30")
_register_cuda_tag("nvidia/quadro-k5200m", "sm_30")
_register_cuda_tag("nvidia/quadro-k5100m", "sm_30")
_register_cuda_tag("nvidia/quadro-m5000m", "sm_50")
_register_cuda_tag("nvidia/quadro-k500m", "sm_30")
_register_cuda_tag("nvidia/quadro-k4200m", "sm_30")
_register_cuda_tag("nvidia/quadro-k4100m", "sm_30")
_register_cuda_tag("nvidia/quadro-m4000m", "sm_50")
_register_cuda_tag("nvidia/quadro-k3100m", "sm_30")
_register_cuda_tag("nvidia/quadro-m3000m", "sm_50")
_register_cuda_tag("nvidia/quadro-k2200m", "sm_30")
_register_cuda_tag("nvidia/quadro-k2100m", "sm_30")
_register_cuda_tag("nvidia/quadro-m2000m", "sm_50")
_register_cuda_tag("nvidia/quadro-k1100m", "sm_30")
_register_cuda_tag("nvidia/quadro-m1000m", "sm_50")
_register_cuda_tag("nvidia/quadro-k620m", "sm_50")
_register_cuda_tag("nvidia/quadro-k610m", "sm_35")
_register_cuda_tag("nvidia/quadro-m600m", "sm_50")
_register_cuda_tag("nvidia/quadro-k510m", "sm_35")
_register_cuda_tag("nvidia/quadro-m500m", "sm_50")
# =====================================================================
# NVS cards
# =====================================================================
_register_cuda_tag("nvidia/nvidia-nvs-810", "sm_50")
_register_cuda_tag("nvidia/nvidia-nvs-510", "sm_30")
_register_cuda_tag("nvidia/nvidia-nvs-315", "sm_21", regs=32768)
_register_cuda_tag("nvidia/nvidia-nvs-310", "sm_21", regs=32768)
_register_cuda_tag("nvidia/nvs-5400m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/nvs-5200m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/nvs-4200m", "sm_21", regs=32768)
# =====================================================================
# GeForce RTX 50-series desktop
# =====================================================================
_register_cuda_tag("nvidia/geforce-rtx-5060-ti", "sm_120", l2_cache_size_bytes=33554432)
# =====================================================================
# GeForce RTX 40-series desktop
# =====================================================================
_register_cuda_tag("nvidia/geforce-rtx-4090", "sm_89", l2_cache_size_bytes=75497472)
# =====================================================================
# GeForce RTX 30-series desktop
# =====================================================================
_register_cuda_tag("nvidia/geforce-rtx-3090-ti", "sm_86")
_register_cuda_tag("nvidia/geforce-rtx-3090", "sm_86")
_register_cuda_tag("nvidia/geforce-rtx-3080-ti", "sm_86")
_register_cuda_tag("nvidia/geforce-rtx-3080", "sm_86")
_register_cuda_tag("nvidia/geforce-rtx-3070-ti", "sm_86")
_register_cuda_tag("nvidia/geforce-rtx-3070", "sm_86")
_register_cuda_tag("nvidia/geforce-rtx-3060", "sm_86")
# =====================================================================
# GeForce RTX 20-series / TITAN (Turing)
# =====================================================================
_register_cuda_tag("nvidia/nvidia-titan-rtx", "sm_75")
_register_cuda_tag("nvidia/geforce-rtx-2080-ti", "sm_75")
_register_cuda_tag("nvidia/geforce-rtx-2080", "sm_75")
_register_cuda_tag("nvidia/geforce-rtx-2070", "sm_75")
_register_cuda_tag("nvidia/geforce-rtx-2060", "sm_75")
# =====================================================================
# GeForce TITAN / GTX 10-series (Pascal)
# =====================================================================
_register_cuda_tag("nvidia/nvidia-titan-v", "sm_70")
_register_cuda_tag("nvidia/nvidia-titan-xp", "sm_61")
_register_cuda_tag("nvidia/nvidia-titan-x", "sm_61")
_register_cuda_tag("nvidia/geforce-gtx-1080-ti", "sm_61")
_register_cuda_tag("nvidia/geforce-gtx-1080", "sm_61")
_register_cuda_tag("nvidia/geforce-gtx-1070-ti", "sm_61")
_register_cuda_tag("nvidia/geforce-gtx-1070", "sm_61")
_register_cuda_tag("nvidia/geforce-gtx-1060", "sm_61")
_register_cuda_tag("nvidia/geforce-gtx-1050", "sm_61")
# =====================================================================
# GeForce GTX 900/700 series desktop (Maxwell / Kepler)
# =====================================================================
_register_cuda_tag("nvidia/geforce-gtx-titan-x", "sm_52")
_register_cuda_tag("nvidia/geforce-gtx-titan-z", "sm_35")
_register_cuda_tag("nvidia/geforce-gtx-titan-black", "sm_35")
_register_cuda_tag("nvidia/geforce-gtx-titan", "sm_35")
_register_cuda_tag("nvidia/geforce-gtx-980-ti", "sm_52")
_register_cuda_tag("nvidia/geforce-gtx-980", "sm_52")
_register_cuda_tag("nvidia/geforce-gtx-970", "sm_52")
_register_cuda_tag("nvidia/geforce-gtx-960", "sm_52")
_register_cuda_tag("nvidia/geforce-gtx-950", "sm_52")
_register_cuda_tag("nvidia/geforce-gtx-780-ti", "sm_35")
_register_cuda_tag("nvidia/geforce-gtx-780", "sm_35")
_register_cuda_tag("nvidia/geforce-gtx-770", "sm_30")
_register_cuda_tag("nvidia/geforce-gtx-760", "sm_30")
_register_cuda_tag("nvidia/geforce-gtx-750-ti", "sm_50")
_register_cuda_tag("nvidia/geforce-gtx-750", "sm_50")
_register_cuda_tag("nvidia/geforce-gtx-690", "sm_30")
_register_cuda_tag("nvidia/geforce-gtx-680", "sm_30")
_register_cuda_tag("nvidia/geforce-gtx-670", "sm_30")
_register_cuda_tag("nvidia/geforce-gtx-660-ti", "sm_30")
_register_cuda_tag("nvidia/geforce-gtx-660", "sm_30")
_register_cuda_tag("nvidia/geforce-gtx-650-ti-boost", "sm_30")
_register_cuda_tag("nvidia/geforce-gtx-650-ti", "sm_30")
_register_cuda_tag("nvidia/geforce-gtx-650", "sm_30")
# =====================================================================
# GeForce GTX 500/400 series desktop (Fermi)
# =====================================================================
_register_cuda_tag("nvidia/geforce-gtx-560-ti", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gtx-550-ti", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gtx-460", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gts-450", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gtx-590", "sm_20", regs=32768)
_register_cuda_tag("nvidia/geforce-gtx-580", "sm_20", regs=32768)
_register_cuda_tag("nvidia/geforce-gtx-570", "sm_20", regs=32768)
_register_cuda_tag("nvidia/geforce-gtx-480", "sm_20", regs=32768)
_register_cuda_tag("nvidia/geforce-gtx-470", "sm_20", regs=32768)
_register_cuda_tag("nvidia/geforce-gtx-465", "sm_20", regs=32768)
# =====================================================================
# GeForce GT desktop (Kepler / Fermi)
# =====================================================================
_register_cuda_tag("nvidia/geforce-gt-740", "sm_30")
_register_cuda_tag("nvidia/geforce-gt-730", "sm_35")
_register_cuda_tag("nvidia/geforce-gt-730-ddr3,128bit", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gt-720", "sm_35")
_register_cuda_tag("nvidia/geforce-gt-705", "sm_35")
_register_cuda_tag("nvidia/geforce-gt-640-gddr5", "sm_35")
_register_cuda_tag("nvidia/geforce-gt-640-gddr3", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gt-630", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gt-620", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gt-610", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gt-520", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gt-440", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gt-430", "sm_21", regs=32768)
# =====================================================================
# GeForce notebook GPUs (Maxwell / Kepler)
# =====================================================================
_register_cuda_tag("nvidia/geforce-gtx-980m", "sm_52")
_register_cuda_tag("nvidia/geforce-gtx-970m", "sm_52")
_register_cuda_tag("nvidia/geforce-gtx-965m", "sm_52")
_register_cuda_tag("nvidia/geforce-gtx-960m", "sm_50")
_register_cuda_tag("nvidia/geforce-gtx-950m", "sm_50")
_register_cuda_tag("nvidia/geforce-940m", "sm_50")
_register_cuda_tag("nvidia/geforce-930m", "sm_50")
_register_cuda_tag("nvidia/geforce-920m", "sm_35")
_register_cuda_tag("nvidia/geforce-910m", "sm_52")
_register_cuda_tag("nvidia/geforce-gtx-880m", "sm_30")
_register_cuda_tag("nvidia/geforce-gtx-870m", "sm_30")
_register_cuda_tag("nvidia/geforce-gtx-860m-sm-30", "sm_30")
_register_cuda_tag("nvidia/geforce-gtx-860m-sm-50", "sm_50")
_register_cuda_tag("nvidia/geforce-gtx-850m", "sm_50")
_register_cuda_tag("nvidia/geforce-840m", "sm_50")
_register_cuda_tag("nvidia/geforce-830m", "sm_50")
_register_cuda_tag("nvidia/geforce-820m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-800m", "sm_21", regs=32768)
# =====================================================================
# GeForce notebook GPUs (Kepler / Fermi, older)
# =====================================================================
_register_cuda_tag("nvidia/geforce-gtx-780m", "sm_30")
_register_cuda_tag("nvidia/geforce-gtx-770m", "sm_30")
_register_cuda_tag("nvidia/geforce-gtx-765m", "sm_30")
_register_cuda_tag("nvidia/geforce-gtx-760m", "sm_30")
_register_cuda_tag("nvidia/geforce-gtx-680mx", "sm_30")
_register_cuda_tag("nvidia/geforce-gtx-680m", "sm_30")
_register_cuda_tag("nvidia/geforce-gtx-675mx", "sm_30")
_register_cuda_tag("nvidia/geforce-gtx-675m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gtx-670mx", "sm_30")
_register_cuda_tag("nvidia/geforce-gtx-670m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gtx-660m", "sm_30")
_register_cuda_tag("nvidia/geforce-gt-755m", "sm_30")
_register_cuda_tag("nvidia/geforce-gt-750m", "sm_30")
_register_cuda_tag("nvidia/geforce-gt-650m", "sm_30")
_register_cuda_tag("nvidia/geforce-gt-745m", "sm_30")
_register_cuda_tag("nvidia/geforce-gt-645m", "sm_30")
_register_cuda_tag("nvidia/geforce-gt-740m", "sm_30")
_register_cuda_tag("nvidia/geforce-gt-730m", "sm_30")
_register_cuda_tag("nvidia/geforce-gt-640m", "sm_30")
_register_cuda_tag("nvidia/geforce-gt-640m-le", "sm_30")
_register_cuda_tag("nvidia/geforce-gt-735m", "sm_30")
_register_cuda_tag("nvidia/geforce-gt-635m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gt-630m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gt-625m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gt-720m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gt-620m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-710m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-705m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-610m", "sm_21", regs=32768)
# =====================================================================
# GeForce notebook GPUs (Fermi, GTX 5xx/4xxM)
# =====================================================================
_register_cuda_tag("nvidia/geforce-gtx-580m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gtx-570m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gtx-560m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gt-555m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gt-550m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gt-540m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gt-525m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gt-520mx", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gt-520m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gtx-485m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gtx-470m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gtx-460m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gt-445m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gt-435m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gt-420m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gt-415m", "sm_21", regs=32768)
_register_cuda_tag("nvidia/geforce-gtx-480m", "sm_20", regs=32768)
_register_cuda_tag("nvidia/geforce-410m", "sm_21", regs=32768)
# =====================================================================
# Jetson boards (simple, no host)
# =====================================================================
_register_cuda_tag("nvidia/jetson-nano", "sm_53", regs=32768)
_register_cuda_tag("nvidia/jetson-tx2", "sm_62", regs=32768)
_register_cuda_tag("nvidia/jetson-tx1", "sm_53", regs=32768)
_register_cuda_tag("nvidia/tegra-x1", "sm_53", regs=32768)
# =====================================================================
# Jetson boards (with LLVM host)
# =====================================================================
_register_jetson_tag("nvidia/jetson-agx-xavier", "sm_72", "carmel", 8)
_register_jetson_tag("nvidia/jetson-orin-nano", "sm_87", "carmel", 6)
_register_jetson_tag("nvidia/jetson-agx-orin-32gb", "sm_87", "cortex-a78", 8)
_register_jetson_tag("nvidia/jetson-agx-orin-64gb", "sm_87", "cortex-a78", 12)
+51
View File
@@ -0,0 +1,51 @@
# 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.
"""Hexagon-owned backend hooks."""
from importlib import import_module
from pathlib import Path
from tvm_ffi.libinfo import load_lib_ctypes
from tvm.base import _LOADED_LIBS
_LAZY_SUBMODULES = {"target_tags"}
def register_backend():
"""Register Hexagon-owned Python semantics."""
runtime_dir = Path(_LOADED_LIBS["tvm_runtime"]._name).resolve().parent
try:
# Runtime sidecars only need registration side effects; libtvm_runtime is global.
_LOADED_LIBS["tvm_runtime_hexagon"] = load_lib_ctypes(
package="tvm",
target_name="tvm_runtime_hexagon",
extra_lib_paths=[runtime_dir],
mode="RTLD_LOCAL",
)
except (OSError, FileNotFoundError, RuntimeError):
pass
import_module(f"{__name__}.target_tags")
def __getattr__(name: str):
if name in _LAZY_SUBMODULES:
return import_module(f"{__name__}.{name}")
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__all__ = ["register_backend", "target_tags"]
+56
View File
@@ -0,0 +1,56 @@
# 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.
"""Qualcomm Hexagon target tags."""
from tvm.target import register_tag
_ONE_MB = 2**20
_HEXAGON_VERSIONS = {
"v65": {"vtcm": _ONE_MB // 4, "mattr": ["+hvxv65", "+hvx-length128b"]},
"v66": {"vtcm": _ONE_MB // 4, "mattr": ["+hvxv66", "+hvx-length128b"]},
"v68": {
"vtcm": 4 * _ONE_MB,
"mattr": ["+hvxv68", "+hvx-length128b", "+hvx-qfloat", "-hvx-ieee-fp"],
"llvm-options": ["-force-hvx-float"],
},
"v69": {
"vtcm": 8 * _ONE_MB,
"mattr": ["+hvxv69", "+hvx-length128b", "+hvx-qfloat", "-hvx-ieee-fp"],
},
"v73": {
"vtcm": 8 * _ONE_MB,
"mattr": ["+hvxv73", "+hvx-length128b", "+hvx-qfloat", "-hvx-ieee-fp"],
},
"v75": {
"vtcm": 8 * _ONE_MB,
"mattr": ["+hvxv75", "+hvx-length128b", "+hvx-qfloat", "-hvx-ieee-fp"],
},
}
for _ver, _info in _HEXAGON_VERSIONS.items():
_config = {
"kind": "hexagon",
"mtriple": "hexagon",
"mcpu": "hexagon" + _ver,
"mattr": _info["mattr"],
"num-cores": 4,
"vtcm-capacity": _info["vtcm"],
}
if "llvm-options" in _info:
_config["llvm-options"] = _info["llvm-options"]
register_tag("qcom/hexagon-" + _ver, _config)
+187
View File
@@ -0,0 +1,187 @@
# 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.
"""Backend loading and public alias support."""
from __future__ import annotations
import importlib
import importlib.util
import sys
import types
from typing import Any
_LOADED_BACKENDS: dict[str, Any] = {}
class _AliasModule(types.ModuleType):
"""Module object that exposes a backend module under a public alias."""
def __init__(self, fullname: str, module):
super().__init__(fullname, getattr(module, "__doc__", None))
self.__dict__["__tvm_backend_module__"] = module
self.__dict__["__package__"] = fullname.rpartition(".")[0]
if hasattr(module, "__all__"):
self.__dict__["__all__"] = module.__all__
if hasattr(module, "__path__"):
self.__dict__["__path__"] = []
def __getattr__(self, name: str):
return getattr(self.__dict__["__tvm_backend_module__"], name)
def __setattr__(self, name: str, value):
setattr(self.__dict__["__tvm_backend_module__"], name, value)
def __delattr__(self, name: str):
delattr(self.__dict__["__tvm_backend_module__"], name)
def __dir__(self):
return sorted(set(super().__dir__()) | set(dir(self.__dict__["__tvm_backend_module__"])))
class _AliasLoader:
"""Loader that returns an already-resolved module for an alias spec."""
def __init__(self, fullname: str, module):
self._fullname = fullname
self._module = module
def create_module(self, spec):
return _get_alias_module(self._fullname, self._module)
def exec_module(self, module):
_set_module_alias(self._fullname, self._module)
return None
def is_package(self, fullname):
return hasattr(self._module, "__path__")
def _redirect_tirx_backend_alias(fullname: str) -> str | None:
prefix = "tvm.tirx."
if not fullname.startswith(prefix):
return None
rest = fullname[len(prefix) :]
backend_name, sep, tail = rest.partition(".")
if not sep or backend_name not in _LOADED_BACKENDS:
return None
return f"tvm.backend.{backend_name}.{tail}"
class _BackendAliasFinder:
"""Redirect ``tvm.tirx.<backend>.*`` imports to ``tvm.backend.<backend>.*``."""
@classmethod
def find_spec(cls, fullname, path, target=None):
redirected = _redirect_tirx_backend_alias(fullname)
if redirected is None:
return None
module = importlib.import_module(redirected)
_set_module_alias(fullname, module)
loader = _AliasLoader(fullname, module)
spec = importlib.util.spec_from_loader(
fullname, loader, is_package=hasattr(module, "__path__")
)
if spec is not None and hasattr(module, "__path__"):
spec.submodule_search_locations = []
return spec
if not any(isinstance(finder, _BackendAliasFinder) for finder in sys.meta_path):
sys.meta_path.insert(0, _BackendAliasFinder())
def _get_alias_module(alias: str, module):
existing = sys.modules.get(alias)
if (
isinstance(existing, _AliasModule)
and existing.__dict__.get("__tvm_backend_module__") is module
):
return existing
return _AliasModule(alias, module)
def _set_module_alias(alias: str, module, *, direct: bool = False) -> None:
alias_module = module if direct else _get_alias_module(alias, module)
sys.modules[alias] = alias_module
parent_name, _, child_name = alias.rpartition(".")
parent = sys.modules.get(parent_name)
if parent is not None:
setattr(parent, child_name, alias_module)
def _alias_loaded_backend_modules(name: str) -> None:
backend_prefix = f"tvm.backend.{name}"
public_prefix = f"tvm.tirx.{name}"
for module_name, module in sorted(list(sys.modules.items())):
if module_name == backend_prefix or module_name.startswith(f"{backend_prefix}."):
public_name = f"{public_prefix}{module_name[len(backend_prefix) :]}"
_set_module_alias(public_name, module, direct=module_name == backend_prefix)
def _import_backend(name: str):
module_name = f"tvm.backend.{name}"
try:
return importlib.import_module(module_name)
except ModuleNotFoundError as err:
if err.name == module_name:
raise ImportError(
f"Cannot load TVM backend {name!r}: expected Python package {module_name!r}. "
"Install the backend package or check the backend name."
) from err
raise
def load(name: str) -> None:
"""Load a backend's Python registration hooks.
Loading is idempotent. A backend package must live at ``tvm.backend.<name>``
and expose ``register_backend()``.
"""
if name in _LOADED_BACKENDS:
return None
module = _import_backend(name)
register_backend = getattr(module, "register_backend", None)
if register_backend is None:
raise AttributeError(f"Backend package 'tvm.backend.{name}' has no register_backend()")
import tvm.tirx as tirx # pylint: disable=import-outside-toplevel
setattr(tirx, name, module)
sys.modules[f"tvm.tirx.{name}"] = module
_LOADED_BACKENDS[name] = module
try:
register_backend()
_alias_loaded_backend_modules(name)
except Exception:
_LOADED_BACKENDS.pop(name, None)
if getattr(tirx, name, None) is module:
delattr(tirx, name)
if sys.modules.get(f"tvm.tirx.{name}") is module:
sys.modules.pop(f"tvm.tirx.{name}", None)
raise
return None
def is_loaded(name: str) -> bool:
"""Return whether a backend has been loaded."""
return name in _LOADED_BACKENDS
__all__ = ["is_loaded", "load"]
+89
View File
@@ -0,0 +1,89 @@
# 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.
"""Metal-owned TIRx modules."""
from importlib import import_module
from pathlib import Path
from tvm_ffi.libinfo import load_lib_ctypes
from tvm.base import _LOADED_LIBS
_LAZY_SUBMODULES = {"op", "script", "target_tags"}
def _detect_target_from_device(dev):
from tvm.target import Target # pylint: disable=import-outside-toplevel
return Target(
{
"kind": "metal",
"max_shared_memory_per_block": 32768,
"max_threads_per_block": dev.max_threads_per_block,
"thread_warp_size": dev.warp_size,
}
)
def register_backend():
"""Register Metal-owned Python semantics."""
from tvm.target.detect_target import register_device_target_detector
from tvm.tirx.script.builder import ir as builder_ir # pylint: disable=import-outside-toplevel
runtime_dir = Path(_LOADED_LIBS["tvm_runtime"]._name).resolve().parent
try:
# Runtime sidecars only need registration side effects; libtvm_runtime is global.
_LOADED_LIBS["tvm_runtime_metal"] = load_lib_ctypes(
package="tvm",
target_name="tvm_runtime_metal",
extra_lib_paths=[runtime_dir],
mode="RTLD_LOCAL",
)
except (OSError, FileNotFoundError, RuntimeError):
pass
register_device_target_detector("metal", _detect_target_from_device)
for name, namespace in script_namespaces().items():
builder_ir.register_script_namespace(name, namespace)
import_module(f"{__name__}.target_tags")
def script_namespaces(**_):
"""Return Metal-owned TVMScript namespaces."""
from .script import MetalNamespace # pylint: disable=import-outside-toplevel
return {"metal": MetalNamespace()}
def script_namespace(**kwargs):
"""Return the Metal TVMScript namespace object."""
return script_namespaces(**kwargs)["metal"]
def __getattr__(name: str):
if name in _LAZY_SUBMODULES:
return import_module(f"{__name__}.{name}")
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__all__ = [
"op",
"register_backend",
"script",
"script_namespace",
"script_namespaces",
"target_tags",
]
+84
View File
@@ -0,0 +1,84 @@
# 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.
"""Metal-owned TIR intrinsic builders."""
from __future__ import annotations
from tvm.tirx.op import call_intrin
def make_filled_simdgroup_matrix(d, index, value, col=8, row=8):
"""Create a filled SIMDGroup matrix."""
return call_intrin("void", "tirx.make_filled_simdgroup_matrix", d, index, value, col, row)
def simdgroup_load(d, index, ptr, stride, col=8, row=8, transpose_matrix=False):
"""Load data from device or threadgroup memory to simdgroup."""
return call_intrin(
"void",
"tirx.simdgroup_load",
d,
index,
ptr,
stride,
col,
row,
transpose_matrix,
)
def simdgroup_store(d, index, ptr, stride, col=8, row=8, transpose_matrix=False):
"""Store data from simdgroup to device or threadgroup memory."""
return call_intrin(
"void",
"tirx.simdgroup_store",
d,
index,
ptr,
stride,
col,
row,
transpose_matrix,
)
def simdgroup_multiply_accumulate(d, index_d, a, index_a, b, index_b, c, index_c):
"""Multiply and accumulate two matrices in simdgroup."""
return call_intrin(
"void",
"tirx.simdgroup_multiply_accumulate",
d,
index_d,
a,
index_a,
b,
index_b,
c,
index_c,
)
__all__ = [
"make_filled_simdgroup_matrix",
"simdgroup_load",
"simdgroup_multiply_accumulate",
"simdgroup_store",
]
+55
View File
@@ -0,0 +1,55 @@
# 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.
"""Metal TVMScript namespace."""
from __future__ import annotations
from tvm.backend.metal import op as _metal_op
from tvm.tirx import Buffer
from tvm.tirx import op as _tir_op
from tvm.tirx.script.builder.ir import _op_wrapper
class MetalNamespace:
"""The Metal intrinsics submodule."""
def __init__(self):
self.make_filled_simdgroup_matrix = _op_wrapper(_metal_op.make_filled_simdgroup_matrix)
self.simdgroup_load = _op_wrapper(_metal_op.simdgroup_load)
self.simdgroup_store = _op_wrapper(_metal_op.simdgroup_store)
self.simdgroup_multiply_accumulate = _op_wrapper(_metal_op.simdgroup_multiply_accumulate)
@staticmethod
def simd_shuffle(var, lane):
if isinstance(var, Buffer):
var = var[0]
return _tir_op.call_intrin(var.ty, "tirx.metal.simd_shuffle", var, lane)
@staticmethod
def simd_shuffle_up(var, delta):
if isinstance(var, Buffer):
var = var[0]
return _tir_op.call_intrin(var.ty, "tirx.metal.simd_shuffle_up", var, delta)
@staticmethod
def simd_shuffle_down(var, delta):
if isinstance(var, Buffer):
var = var[0]
return _tir_op.call_intrin(var.ty, "tirx.metal.simd_shuffle_down", var, delta)
__all__ = ["MetalNamespace"]
+50
View File
@@ -0,0 +1,50 @@
# 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.
"""Apple Metal GPU target tags."""
from tvm.target import register_tag
_METAL_HOST_TRIPLE = "arm64-apple-macos"
def _register_metal_tag(name, max_threads, shared_mem, warp_size, mcpu):
try:
from tvm.target.codegen import llvm_is_valid_cpu
if not llvm_is_valid_cpu(mcpu, _METAL_HOST_TRIPLE):
return
except Exception: # pylint: disable=broad-except
pass # LLVM not available; register unconditionally
register_tag(
name,
{
"kind": "metal",
"max_threads_per_block": max_threads,
"max_shared_memory_per_block": shared_mem,
"thread_warp_size": warp_size,
"host": {
"kind": "llvm",
"mtriple": _METAL_HOST_TRIPLE,
"mcpu": mcpu,
},
},
)
_register_metal_tag("apple/m1-gpu", 1024, 32768, 32, "apple-m1")
_register_metal_tag("apple/m1-gpu-restricted", 256, 32768, 32, "apple-m1")
_register_metal_tag("apple/m2-gpu", 1024, 32768, 32, "apple-m2")
+58
View File
@@ -0,0 +1,58 @@
# 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.
"""OpenCL-owned backend hooks."""
from pathlib import Path
from tvm_ffi.libinfo import load_lib_ctypes
from tvm.base import _LOADED_LIBS
def _detect_target_from_device(dev):
from tvm.target import Target # pylint: disable=import-outside-toplevel
return Target(
{
"kind": "opencl",
"max_shared_memory_per_block": dev.max_shared_memory_per_block,
"max_threads_per_block": dev.max_threads_per_block,
"thread_warp_size": dev.warp_size,
}
)
def register_backend():
"""Register OpenCL-owned Python semantics."""
from tvm.target.detect_target import register_device_target_detector
runtime_dir = Path(_LOADED_LIBS["tvm_runtime"]._name).resolve().parent
try:
# Runtime sidecars only need registration side effects; libtvm_runtime is global.
_LOADED_LIBS["tvm_runtime_opencl"] = load_lib_ctypes(
package="tvm",
target_name="tvm_runtime_opencl",
extra_lib_paths=[runtime_dir],
mode="RTLD_LOCAL",
)
except (OSError, FileNotFoundError, RuntimeError):
pass
register_device_target_detector("opencl", _detect_target_from_device)
return None
__all__ = ["register_backend"]
+59
View File
@@ -0,0 +1,59 @@
# 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.
"""ROCm-owned TIRx modules."""
from pathlib import Path
from tvm_ffi.libinfo import load_lib_ctypes
from tvm.base import _LOADED_LIBS
def _detect_target_from_device(dev):
from tvm.target import Target # pylint: disable=import-outside-toplevel
return Target(
{
"kind": "rocm",
"mtriple": "amdgcn-amd-amdhsa-hcc",
"max_shared_memory_per_block": dev.max_shared_memory_per_block,
"max_threads_per_block": dev.max_threads_per_block,
"thread_warp_size": dev.warp_size,
}
)
def register_backend():
"""Register ROCm-owned Python semantics."""
from tvm.target.detect_target import register_device_target_detector
runtime_dir = Path(_LOADED_LIBS["tvm_runtime"]._name).resolve().parent
try:
# Runtime sidecars only need registration side effects; libtvm_runtime is global.
_LOADED_LIBS["tvm_runtime_rocm"] = load_lib_ctypes(
package="tvm",
target_name="tvm_runtime_rocm",
extra_lib_paths=[runtime_dir],
mode="RTLD_LOCAL",
)
except (OSError, FileNotFoundError, RuntimeError):
pass
register_device_target_detector("rocm", _detect_target_from_device)
return None
__all__ = ["register_backend"]
+68
View File
@@ -0,0 +1,68 @@
# 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.
"""Trainium-owned TIRx modules."""
from importlib import import_module
_LAZY_SUBMODULES = {"layout", "op", "operator", "pipeline", "script", "target_tags", "transform"}
def register_backend():
"""Register Trainium-owned Python semantics."""
from tvm.tirx import compilation_pipeline # pylint: disable=import-outside-toplevel
from tvm.tirx.script.builder import ir as builder_ir # pylint: disable=import-outside-toplevel
for name, namespace in script_namespaces().items():
builder_ir.register_script_namespace(name, namespace)
import_module(f"{__name__}.operator.tile_primitive")
trn_pipeline = import_module(f"{__name__}.pipeline")
import_module(f"{__name__}.target_tags")
import_module(f"{__name__}.transform")
compilation_pipeline.register_tir_pipeline("trn", trn_pipeline.trn_pipeline)
def script_namespace(op_wrapper=None):
"""Return the Trainium TVMScript namespace object."""
from .script import NKINamespace # pylint: disable=import-outside-toplevel
return NKINamespace(op_wrapper)
def script_namespaces(op_wrapper=None, **_):
"""Return Trainium-owned TVMScript namespaces."""
return {"nki": script_namespace(op_wrapper)}
def __getattr__(name: str):
if name in _LAZY_SUBMODULES:
return import_module(f"{__name__}.{name}")
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__all__ = [
"layout",
"op",
"operator",
"pipeline",
"register_backend",
"script",
"script_namespace",
"script_namespaces",
"target_tags",
"transform",
]
+123
View File
@@ -0,0 +1,123 @@
# 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.
"""Trainium-specific TIRx layout helpers."""
from __future__ import annotations
import functools
import operator
import re
import tvm
from tvm.tirx.expr import Expr
from tvm.tirx.layout import Axis, Iter, Layout, S, TileLayout
_TRN_MEMORY_AXES = {"F", "P", "Bank"}
_PSUM_MAX_ELEM_PER_BANK = 512
def is_trainium_layout(layout: Layout | None) -> bool:
"""Return whether a layout uses only Trainium memory axes."""
if not isinstance(layout, TileLayout):
return False
return not any(
iter.axis.is_memory() and iter.axis.name not in _TRN_MEMORY_AXES for iter in layout.shard
)
def trainium_layout(annotation: str, shape: tuple[Expr], is_psum: bool = False) -> TileLayout:
"""Create a Trainium tile layout from a PF annotation string and logical shape."""
analyzer = tvm.arith.Analyzer()
assert re.fullmatch(r"[PF]*", annotation), (
f"annotation {annotation} must be a string of 'P' and 'F'"
)
assert len(annotation) == len(shape), (
f"annotation {annotation} and shape {shape} must have the same length"
)
num_p_dim = annotation.count("P")
if num_p_dim == 1:
p_idx = annotation.index("P")
p_dim = shape[p_idx]
assert analyzer.can_prove(p_dim <= 128 or p_dim % 128 == 0), (
f"There is only 1 P in the annotation. Partition size {p_dim} must be less than "
"or equal to 128 or a multiple of 128"
)
if analyzer.can_prove(p_dim > 128):
annotation = "F" + annotation
shape = (p_dim // 128, *shape[:p_idx], 128, *shape[p_idx + 1 :])
elif num_p_dim > 1:
p_dim_prod = functools.reduce(
operator.mul, [s for s, c in zip(shape, annotation) if c == "P"]
)
assert analyzer.can_prove(p_dim_prod <= 128), (
f"There are {num_p_dim} Ps in the annotation. Partition size {p_dim_prod} must be "
"less than or equal to 128"
)
f_shape = [s for s, c in zip(shape, annotation) if c == "F"]
p_shape = [s for s, c in zip(shape, annotation) if c == "P"]
f_strides = Layout._get_default_strides(f_shape, 1) # pylint: disable=protected-access
p_strides = Layout._get_default_strides(p_shape, 1) # pylint: disable=protected-access
f_tile_layout = TileLayout(S[tuple(f_shape) : tuple(s @ Axis.F for s in f_strides)])
p_tile_layout = TileLayout(S[tuple(p_shape) : tuple(s @ Axis.P for s in p_strides)])
result = []
f_index = p_index = 0
for char in annotation:
if char == "F":
result.append(f_tile_layout.shard[f_index])
f_index += 1
else:
result.append(p_tile_layout.shard[p_index])
p_index += 1
if num_p_dim == 1 and analyzer.can_prove(p_dim > 128):
higher_p = result[0]
result = result[1:]
result = [*result[:p_idx], higher_p, *result[p_idx:]]
res = TileLayout.from_iters(result, [], {})
if is_psum:
res = to_psum_layout(res)
return res
def to_psum_layout(layout: TileLayout) -> TileLayout:
"""Convert a Trainium sbuf layout to its psum physical-bank layout."""
analyzer = tvm.arith.Analyzer()
shard = []
for iter in layout.shard:
if iter.axis.name == "F":
if analyzer.can_prove(iter.stride % _PSUM_MAX_ELEM_PER_BANK == 0):
stride = analyzer.simplify(iter.stride // _PSUM_MAX_ELEM_PER_BANK)
shard.append(Iter(iter.extent, stride, Axis.get("Bank")))
elif analyzer.can_prove(_PSUM_MAX_ELEM_PER_BANK % iter.stride == 0):
c = analyzer.simplify(_PSUM_MAX_ELEM_PER_BANK // iter.stride)
if analyzer.can_prove(iter.extent < c):
shard.append(iter)
elif analyzer.can_prove(iter.extent % c == 0):
shard.append(Iter(analyzer.simplify(iter.extent // c), 1, Axis.get("Bank")))
shard.append(Iter(c, iter.stride, Axis.get("F")))
else:
raise ValueError(f"layout {layout} can not be converted to psum layout")
else:
raise ValueError(f"layout {layout} can not be converted to psum layout")
else:
shard.append(iter)
return TileLayout.from_iters(shard, [], {})
__all__ = ["is_trainium_layout", "to_psum_layout", "trainium_layout"]
+153
View File
@@ -0,0 +1,153 @@
# 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.
"""Trainium-owned NKI intrinsic Python wrappers."""
from __future__ import annotations
from tvm.tirx.op import call_intrin
def nki_load(res, data):
return call_intrin("", "tirx.nki.load", res, data)
def nki_store(res, data):
return call_intrin("", "tirx.nki.store", res, data)
def nki_tensor_copy(res, data):
return call_intrin("", "tirx.nki.tensor_copy", res, data)
def nki_matmul(res, lhs, rhs, accum=True):
return call_intrin("", "tirx.nki.matmul", res, lhs, rhs, accum)
def nki_activation(result, data, opcode, bias=0.0, scale=1.0):
return call_intrin("", "tirx.nki.activation", result, data, opcode, bias, scale)
def nki_reciprocal(result, data):
return call_intrin("", "tirx.nki.reciprocal", result, data)
def nki_tensorreduce(result, data, opcode, negate, *axes):
return call_intrin("", "tirx.nki.tensorreduce", result, data, opcode, negate, *axes)
def nki_tensortensor(result, operand0, operand1, opcode):
return call_intrin("", "tirx.nki.tensortensor", result, operand0, operand1, opcode)
def nki_tensorscalar(result, operand0, operand1, opcode, reverse=False):
return call_intrin("", "tirx.nki.tensorscalar", result, operand0, operand1, opcode, reverse)
def nki_memset(result, value):
return call_intrin("", "tirx.nki.memset", result, value)
def nki_activation_reduce(reduce_res, act_res, data, opcode, reduce_opcode, bias=0.0, scale=1.0):
return call_intrin(
"",
"tirx.nki.activation_reduce",
reduce_res,
act_res,
data,
opcode,
reduce_opcode,
bias,
scale,
)
def nki_tensorscalar_reduce(
reduce_res, tensorscalar_res, operand0, operand1, opcode, reduce_opcode, reverse=False
):
return call_intrin(
"",
"tirx.nki.tensorscalar_reduce",
reduce_res,
tensorscalar_res,
operand0,
operand1,
opcode,
reduce_opcode,
reverse,
)
def nki_identity(result, size):
return call_intrin("", "tirx.nki.identity", result, size)
def nki_scalar_tensor_tensor(
result, data, operand0, operand1, opcode0, opcode1, reverse0=False, reverse1=False
):
return call_intrin(
"",
"tirx.nki.scalar_tensor_tensor",
result,
data,
operand0,
operand1,
opcode0,
opcode1,
reverse0,
reverse1,
)
def nki_scalar_tensor_scalar(
result, data, operand0, operand1, opcode0, opcode1, reverse0=False, reverse1=False
):
return call_intrin(
"",
"tirx.nki.scalar_tensor_scalar",
result,
data,
operand0,
operand1,
opcode0,
opcode1,
reverse0,
reverse1,
)
def nki_affine_select(result, pred, true_value, false_value):
return call_intrin("", "tirx.nki.affine_select", result, pred, true_value, false_value)
__all__ = [
"nki_activation",
"nki_activation_reduce",
"nki_affine_select",
"nki_identity",
"nki_load",
"nki_matmul",
"nki_memset",
"nki_reciprocal",
"nki_scalar_tensor_scalar",
"nki_scalar_tensor_tensor",
"nki_store",
"nki_tensor_copy",
"nki_tensorreduce",
"nki_tensorscalar",
"nki_tensorscalar_reduce",
"nki_tensortensor",
]
@@ -0,0 +1,22 @@
# 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.
"""Trainium backend operator package.
Loaded by the Trainium backend registration hook.
"""
__all__ = ["tile_primitive"]
@@ -0,0 +1,25 @@
# 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.
from .binary import *
from .compose_op import *
from .copy import *
from .gemm import *
from .private_alloc import *
from .reduction import *
from .select import *
from .unary import *

Some files were not shown because too many files have changed in this diff Show More