chore: import upstream snapshot with attribution
Lint / lint (push) Has been cancelled
CI / MacOS (push) Has been cancelled
CI / Windows (push) Has been cancelled

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
@@ -0,0 +1,437 @@
# 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: F841
import tvm
import tvm.testing
from tvm import tirx
from tvm.tirx import (
EQ,
LT,
Add,
Cast,
Evaluate,
FloatImm,
For,
IfThenElse,
IntImm,
Max,
Min,
Mul,
PyStmtExprMutator,
PyStmtExprVisitor,
StringImm,
Sub,
Var,
)
class ASTLog:
"""Helper class to log AST"""
def __init__(self) -> None:
self.log = []
self.indent = "\t"
self.level = 0
def push_scope(self):
self.level += 1
def pop_scope(self):
self.level -= 1
def add(self, s: str):
self.log.append(self.indent * self.level + s)
def __str__(self) -> str:
return "\n".join(self.log)
@tirx.functor.visitor
class ASTPrinter(PyStmtExprVisitor):
"""Print tirx AST in structured format. The shape of Node is ignored."""
def __init__(self) -> None:
super().__init__()
self.log = ASTLog()
def visit_var_(self, op: Var) -> None:
self.log.add("Stmt: Var")
super().visit_var_(op)
def visit_add_(self, op: Add) -> None:
self.log.add("Stmt: Add")
super().visit_add_(op)
@tirx.functor.visitor
class SimpleExprCounter(PyStmtExprVisitor):
"""Count expressions without recursion"""
def __init__(self):
super().__init__()
self.var_count = 0
self.add_count = 0
self.mul_count = 0
def visit_var_(self, op: Var):
self.var_count += 1
# Don't recursively visit children to avoid infinite recursion
def visit_add_(self, op: Add):
self.add_count += 1
# Visit children manually
super().visit_add_(op)
def visit_mul_(self, op: Mul):
self.mul_count += 1
# Visit children manually
super().visit_mul_(op)
@tirx.functor.mutator
class VariableReplacer(PyStmtExprMutator):
"""Replace variables with constants"""
def __init__(self, replacements):
super().__init__()
self.replacements = replacements
def visit_var_(self, op: Var):
if op.name in self.replacements:
return IntImm("int32", self.replacements[op.name])
return op
@tirx.functor.mutator
class AddToSubMutator(PyStmtExprMutator):
"""Convert Add operations to Sub operations"""
def visit_add_(self, op: Add):
# First mutate the operands
a = self.visit_expr(op.a)
b = self.visit_expr(op.b)
# Convert Add to Sub
return Sub(a, b)
@tirx.functor.visitor
class SimpleStmtCounter(PyStmtExprVisitor):
"""Count statements without recursion"""
def __init__(self):
super().__init__()
self.for_count = 0
self.if_count = 0
self.evaluate_count = 0
def visit_for_(self, op: For):
self.for_count += 1
super().visit_for_(op)
def visit_if_then_else_(self, op: IfThenElse):
self.if_count += 1
super().visit_if_then_else_(op)
def visit_evaluate_(self, op: Evaluate):
self.evaluate_count += 1
super().visit_evaluate_(op)
@tirx.functor.mutator
class ForLoopUnroller(PyStmtExprMutator):
"""Simple loop unroller for demonstration"""
def __init__(self, unroll_factor=2):
super().__init__()
self.unroll_factor = unroll_factor
def visit_for_(self, op: For):
# For demonstration, just return the original for now
# In a real implementation, we would unroll small loops
return super().visit_for_(op)
@tirx.functor.visitor
class SimpleStmtExprVisitor(PyStmtExprVisitor):
"""Visitor that handles both statements and expressions"""
def __init__(self):
super().__init__()
self.expr_count = 0
self.stmt_count = 0
self.var_names = set()
def visit_var_(self, op: Var):
self.var_names.add(op.name)
self.expr_count += 1
def visit_evaluate_(self, op: Evaluate):
self.stmt_count += 1
# Visit the expression
self.visit_expr(op.value)
@tirx.functor.mutator
class ComplexMutator(PyStmtExprMutator):
"""Mutator that handles both statements and expressions"""
def __init__(self):
super().__init__()
self.modifications = 0
def visit_add_(self, op: Add):
self.modifications += 1
# Convert a + b to a * 2 + b for demonstration
a = self.visit_expr(op.a)
b = self.visit_expr(op.b)
return Add(Mul(a, IntImm("int32", 2)), b)
def test_basic_visitor():
"""Test the basic AST printer visitor"""
expr = Add(Var("x", dtype="int32"), Var("y", dtype="int32"))
printer = ASTPrinter()
printer.visit_expr(expr)
assert str(printer.log) == "\n".join(["Stmt: Add", "Stmt: Var", "Stmt: Var"])
def test_simple_expr_counter():
"""Test simple expression counting visitor"""
x = Var("x", dtype="int32")
y = Var("y", dtype="int32")
# Create simple expression: x + y
expr = Add(x, y)
counter = SimpleExprCounter()
counter.visit_expr(expr)
assert counter.var_count == 2 # x and y
assert counter.add_count == 1 # one add
def test_variable_replacer():
"""Test expression mutator that replaces variables"""
x = Var("x", dtype="int32")
y = Var("y", dtype="int32")
expr = Add(x, Mul(y, IntImm("int32", 3)))
replacer = VariableReplacer({"x": 10, "y": 5})
result = replacer.visit_expr(expr)
# Should be Add(IntImm(10), Mul(IntImm(5), IntImm(3)))
assert isinstance(result, Add)
assert isinstance(result.a, IntImm)
assert result.a.value == 10
assert isinstance(result.b, Mul)
assert isinstance(result.b.a, IntImm)
assert result.b.a.value == 5
def test_add_to_sub_mutator():
"""Test mutator that converts Add to Sub"""
x = Var("x", dtype="int32")
y = Var("y", dtype="int32")
expr = Add(x, y)
mutator = AddToSubMutator()
result = mutator.visit_expr(expr)
assert isinstance(result, Sub)
assert isinstance(result.a, Var)
assert isinstance(result.b, Var)
assert result.a.name == "x"
assert result.b.name == "y"
def test_simple_stmt_counter():
"""Test statement visitor that counts statements"""
i = Var("i", dtype="int32")
# Create a simple for loop
loop_body = Evaluate(IntImm("int32", 0))
for_stmt = For(i, IntImm("int32", 0), IntImm("int32", 10), tirx.ForKind.SERIAL, loop_body)
counter = SimpleStmtCounter()
counter.visit_stmt(for_stmt)
assert counter.for_count == 1 # One for loop
assert counter.evaluate_count == 1 # One evaluate in the body
def test_if_then_else_visitor():
"""Test visitor with if-then-else statements"""
x = Var("x", dtype="int32")
condition = EQ(x, IntImm("int32", 0))
then_stmt = Evaluate(IntImm("int32", 1))
else_stmt = Evaluate(IntImm("int32", 2))
if_stmt = IfThenElse(condition, then_stmt, else_stmt)
counter = SimpleStmtCounter()
counter.visit_stmt(if_stmt)
assert counter.if_count == 1
assert counter.for_count == 0
def test_simple_stmt_expr_visitor():
"""Test stmt_expr_visitor with mixed statements and expressions"""
x = Var("x", dtype="int32")
y = Var("y", dtype="int32")
# Create an evaluate statement with an expression
expr = Add(x, y)
stmt = Evaluate(expr)
visitor = SimpleStmtExprVisitor()
visitor.visit_stmt(stmt)
assert visitor.stmt_count == 1 # One Evaluate statement
assert visitor.expr_count == 2 # Two variables
assert "x" in visitor.var_names
assert "y" in visitor.var_names
def test_complex_mutator():
"""Test stmt_expr_mutator"""
x = Var("x", dtype="int32")
y = Var("y", dtype="int32")
# Expression with Add operations
expr = Add(x, y)
stmt = Evaluate(expr)
mutator = ComplexMutator()
result = mutator.visit_stmt(stmt)
print(type(mutator))
assert mutator.modifications == 1 # One Add operation modified
assert isinstance(result, Evaluate)
# Check that the expression was modified
modified_expr = result.value
assert isinstance(modified_expr, Add)
assert isinstance(modified_expr.a, Mul) # First operand should be multiplied by 2
def test_different_expr_types():
"""Test visitor with various expression types"""
x = Var("x", dtype="int32")
# Test different expression types individually
exprs = [
IntImm("int32", 42),
FloatImm("float32", 3.14),
StringImm("hello"),
Cast("float32", x),
Min(x, IntImm("int32", 10)),
Max(x, IntImm("int32", 0)),
LT(x, IntImm("int32", 5)),
]
# Just test that we can create and visit each type
counter = SimpleExprCounter()
for expr in exprs:
try:
counter.visit_expr(expr)
except Exception as e:
# Some expressions might not be supported, that's ok
pass
def test_decorator_functionality():
"""Test that decorators work correctly"""
# Test that decorated classes are properly wrapped
visitor = SimpleExprCounter()
assert hasattr(visitor, "_outer") # Should have the wrapper functionality
mutator = VariableReplacer({})
assert hasattr(mutator, "_outer")
def test_empty_expressions():
"""Test handling of simple expressions"""
counter = SimpleExprCounter()
# Test with just a variable
x = Var("x", dtype="int32")
counter.visit_expr(x)
assert counter.var_count == 1
# Test with just a constant
counter = SimpleExprCounter()
const = IntImm("int32", 5)
counter.visit_expr(const)
# Constants don't increase var_count
assert counter.var_count == 0
def test_stmt_mutator():
"""Test basic statement mutator functionality"""
x = Var("x", dtype="int32")
stmt = Evaluate(Add(x, IntImm("int32", 1)))
unroller = ForLoopUnroller()
result = unroller.visit_stmt(stmt)
# Should return the same statement (no actual unrolling implemented)
assert isinstance(result, Evaluate)
def test_nested_expressions():
"""Test with nested expressions"""
x = Var("x", dtype="int32")
y = Var("y", dtype="int32")
z = Var("z", dtype="int32")
# Create nested expression: (x + y) * z
inner_add = Add(x, y)
expr = Mul(inner_add, z)
counter = SimpleExprCounter()
counter.visit_expr(expr)
assert counter.var_count == 3 # x, y, z
assert counter.add_count == 1 # one add
assert counter.mul_count == 1 # one mul
def test_simple_mutations():
"""Test simple expression mutations"""
x = Var("x", dtype="int32")
y = Var("y", dtype="int32")
# Test multiple replacements
expr = Add(x, y)
replacer = VariableReplacer({"x": 1, "y": 2})
result = replacer.visit_expr(expr)
assert isinstance(result, Add)
assert isinstance(result.a, IntImm)
assert isinstance(result.b, IntImm)
assert result.a.value == 1
assert result.b.value == 2
if __name__ == "__main__":
test_basic_visitor()
tvm.testing.main()
@@ -0,0 +1,255 @@
# 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: F821
import pytest
import tvm.testing
from tvm.script import ir as I
from tvm.script import tirx as T
class BaseTestCase:
def test_well_formed(self):
After = tvm.tirx.transform.InlinePrivateFunctions()(self.Before)
tvm.tirx.analysis.verify_well_formed(After)
def test_produces_expected(self):
After = tvm.tirx.transform.InlinePrivateFunctions()(self.Before)
tvm.ir.assert_structural_equal(self.Expected, After)
class TestSimple(BaseTestCase):
"""Simple case directly acting on PrimFunc"""
@I.ir_module(s_tir=True)
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer([80, 16], "float32"), B: T.Buffer([64, 16], "float32")):
for i in range(64):
Before.subroutine(T.address_of(A[i, 0]), T.address_of(B[i, 0]))
@T.prim_func(private=True, s_tir=True)
def subroutine(A_data: T.handle("float32"), B_data: T.handle("float32")):
A = T.decl_buffer([16, 16], "float32", data=A_data)
B = T.decl_buffer([16], "float32", data=B_data)
for i in range(16):
B[i] = 0.0
for j in range(16):
B[i] = B[i] + A[i, j]
@I.ir_module(s_tir=True)
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer([80, 16], "float32"), B: T.Buffer([64, 16], "float32")):
for i in range(64):
A_view_data: T.let[T.handle("float32")] = T.address_of(A[i, 0])
Aview = T.decl_buffer([16, 16], "float32", data=A_view_data)
B_view_data: T.let[T.handle("float32")] = T.address_of(B[i, 0])
Bview = T.decl_buffer([16], "float32", data=B_view_data)
for j in range(16):
Bview[j] = 0.0
for k in range(16):
Bview[j] = Bview[j] + Aview[j, k]
class TestRetainCrossFunctionSubroutines(BaseTestCase):
"""Do not inline functions that cross device boundaries
When lowering TIR, calls for which the callsite and callee have
different targets are used at some stages, before being further
lowered to explicit device kernel launches. Since inlining the
function would remove this cross-device information,
InlinePrivateSubroutines should not inline these cases.
"""
@I.ir_module(s_tir=True)
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer([80, 16], "float32"), B: T.Buffer([64, 16], "float32")):
T.func_attr({"target": T.target("llvm")})
for i in range(64):
Before.subroutine(T.address_of(A[i, 0]), T.address_of(B[i, 0]))
@T.prim_func(private=True, s_tir=True)
def subroutine(A_data: T.handle("float32"), B_data: T.handle("float32")):
T.func_attr({"target": T.target("cuda")})
A = T.decl_buffer([16, 16], "float32", data=A_data)
B = T.decl_buffer([16], "float32", data=B_data)
for i in range(16):
B[i] = 0.0
for j in range(16):
B[i] = B[i] + A[i, j]
Expected = Before
class TestRetainRecursiveSubroutines(BaseTestCase):
"""Do not inline recursive functions
To avoid potentially infinite loops at compile-time, disable
inlining of recursive functions. If inlining of these functions
would be useful, this restriction may be relaxed with improved
analysis of the subroutine.
"""
@I.ir_module(s_tir=True)
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(16, "float32")):
Before.subroutine(T.address_of(A[0]), 16)
@T.prim_func(private=True, s_tir=True)
def subroutine(A_data: T.handle("float32"), A_size: T.int32):
A = T.decl_buffer(A_size, "float32", data=A_data)
A[1] = A[0] + A[1]
if A_size > 1:
Before.subroutine(T.address_of(A[1]), A_size - 1)
Expected = Before
class TestDeduplicateBlockName(BaseTestCase):
"""Block names must be de-duplicated after inlining"""
@pytest.mark.xfail(reason="Inlining of schedulable TIR not yet supported")
def test_produces_expected(self):
super().test_produces_expected(self)
@I.ir_module(s_tir=True)
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer([2, 16], "float32"), B: T.Buffer([2, 16], "float32")):
Before.subroutine(T.address_of(A[0, 0]), T.address_of(B[0, 0]))
Before.subroutine(T.address_of(A[1, 0]), T.address_of(B[1, 0]))
@T.prim_func(private=True, s_tir=True)
def subroutine(A_data: T.handle("float32"), B_data: T.handle("float32")):
A = T.decl_buffer(16, "float32", data=A_data)
B = T.decl_buffer(16, "float32", data=B_data)
for i in range(16):
with T.sblock("scalar_mul"):
B[i] = A[i] * 2.0
@I.ir_module(s_tir=True)
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer([80, 16], "float32"), B: T.Buffer([64, 16], "float32")):
A_data_1 = T.bind(T.address_of(A[0, 0]), T.handle("float32"))
A_1 = T.decl_buffer(16, "float32", data=A_data_1)
B_data_1: T.let[T.handle("float32")] = T.address_of(B[0, 0])
B_1 = T.decl_buffer(16, "float32", data=B_data_1)
for i in range(16):
with T.sblock("scalar_mul_1"):
B_1[i] = A_1[i] * 2.0
A_data_2 = T.bind(T.address_of(A[1, 0]), T.handle("float32"))
A_2 = T.decl_buffer(16, "float32", data=A_data_2)
B_data_2: T.let[T.handle("float32")] = T.address_of(B[1, 0])
B_2 = T.decl_buffer(16, "float32", data=B_data_2)
for i in range(16):
with T.sblock("scalar_mul_2"):
B_2[i] = A_2[i] * 2.0
class TestInlineCallOccurringInExpression(BaseTestCase):
"""Inline a Call node that is used in a function
The current implementation only replaces `ir.Call` instances that
occur in a `tirx.Evaluate` context. This is the primary use case,
used in destination-passing style.
This unit test is marked as xfail. If/when the implementation
supports inlining of function calls occurring as part of an
expression, the annotation can be removed.
"""
@pytest.mark.xfail(reason="Inlining of PrimFuncs outside of tirx.Evaluate is not yet supported")
def test_produces_expected(self):
super().test_produces_expected(self)
@I.ir_module(s_tir=True)
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(16, "float32")):
for i in range(16):
A[i] = Before.subroutine(i)
@T.prim_func(private=True, s_tir=True)
def subroutine(i: T.int32) -> T.float32:
cos = T.cos(T.cast(i, "float32"))
sin = T.sin(T.cast(i, "float32"))
retval = cos * cos + sin * sin
T.ret(retval)
@I.ir_module(s_tir=True)
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(16, "float32")):
for i in range(16):
cos = T.cos(T.cast(i, "float32"))
sin = T.sin(T.cast(i, "float32"))
retval = cos * cos + sin * sin
A[i] = retval
class TestInlineFunctionWithBufferArguments(BaseTestCase):
"""Inline a function that accepts buffer arguments
The current implementation does not support this usage. This unit
test is provided to display a possible user interaction, and is
marked with `@pytest.mark.xfail`. If/when the implementation
supports inlining of function calls with buffer arguments, the
annotation can be removed.
"""
@pytest.mark.xfail(reason="Inlining of PrimFuncs with buffer arguments")
def test_produces_expected(self):
super().test_produces_expected(self)
@I.ir_module(s_tir=True)
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(16, "float32")):
Before.subroutine(
T.tvm_stack_make_array(
A.data,
T.tvm_stack_make_shape(*A.shape, dtype="handle"),
0,
len(A.shape),
0.0,
A.elem_offset,
dtype="handle",
)
)
@T.prim_func(private=True, s_tir=True)
def subroutine(A: T.Buffer(16, "float32")):
for i in range(16):
A[i] = A[i] * 2.0
@I.ir_module(s_tir=True)
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(16, "float32")):
for i in range(16):
A[i] = A[i] * 2.0
if __name__ == "__main__":
tvm.testing.main()
@@ -0,0 +1,455 @@
# 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.
import tvm
import tvm.script
from tvm.script import tirx as T
from tvm.target import Target
from tvm.tirx.transform.transform import BindTarget
def u16tof32(v):
uint32_v = v.astype("uint32")
uint32_v = uint32_v << tvm.tirx.const(16, "uint32")
return T.reinterpret("float32", uint32_v)
def bf16tof32(v):
return u16tof32(T.reinterpret("uint16", v))
def f32tou16(v):
uint32_v = T.reinterpret("uint32", v)
rounding_bias = (uint32_v >> tvm.tirx.const(16, "uint32")) & tvm.tirx.const(1, "uint32")
rounding_bias += tvm.tirx.const(0x7FFF, "uint32")
uint32_v = uint32_v + rounding_bias
return (uint32_v >> tvm.tirx.const(16, "uint32")).astype("uint16")
def f32tobf16(v):
return T.reinterpret("bfloat16", f32tou16(v))
def test_bf16_simple_store_will_legalize():
def get_before():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(
Aptr: T.handle("bfloat16", storage_scope="shared"),
Cptr: T.handle("bfloat16"),
):
T.func_attr({"global_symbol": "main"})
A = T.decl_buffer((100,), "bfloat16", data=Aptr)
B = T.decl_buffer((100,), "bfloat16")
C = T.decl_buffer((100,), "bfloat16", data=Cptr)
for i in T.grid(100):
B[i] = A[i]
C[i] = T.exp(B[i])
return Before
def after_compute_legalize():
@tvm.script.ir_module
class After:
@T.prim_func(s_tir=True)
def main(
Aptr: T.handle("bfloat16", storage_scope="shared"),
Cptr: T.handle("bfloat16"),
):
T.func_attr({"global_symbol": "main"})
A = T.decl_buffer((100,), "bfloat16", data=Aptr)
B = T.decl_buffer((100,), "float32")
C = T.decl_buffer((100,), "bfloat16", data=Cptr)
for i in T.grid(100):
B[i] = bf16tof32(A[i])
C[i] = f32tobf16(T.exp(B[i]))
return After
def after_storage_legalize():
@tvm.script.ir_module
class After:
@T.prim_func(s_tir=True)
def main(
Aptr: T.handle("uint16", storage_scope="shared"),
Cptr: T.handle("uint16"),
):
T.func_attr({"global_symbol": "main"})
A = T.decl_buffer((100,), "uint16", data=Aptr)
B = T.decl_buffer((100,), "float32")
C = T.decl_buffer((100,), "uint16", data=Cptr)
for i in T.grid(100):
B[i] = u16tof32(A[i])
C[i] = f32tou16(T.exp(B[i]))
return After
target = Target("nvidia/geforce-rtx-2080-ti")
before = BindTarget(target)(get_before())
after_compute = tvm.tirx.transform.BF16ComputeLegalize()(before)
after_storage = tvm.tirx.transform.BF16StorageLegalize()(after_compute)
tvm.ir.assert_structural_equal(after_compute, BindTarget(target)(after_compute_legalize()))
tvm.ir.assert_structural_equal(after_storage, BindTarget(target)(after_storage_legalize()))
def test_bf16_storage_compute_scope_will_legalize():
def get_before():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(
Aptr: T.handle("bfloat16", storage_scope="shared"),
Bptr: T.handle("bfloat16", storage_scope="local"),
Dptr: T.handle("bfloat16"),
):
T.func_attr({"global_symbol": "main"})
A = T.decl_buffer((100,), "bfloat16", data=Aptr)
B = T.decl_buffer((100,), "bfloat16", data=Bptr)
D = T.decl_buffer((100,), "bfloat16", data=Dptr)
C = T.decl_buffer((100,), "bfloat16")
for i in T.grid(100):
C[i] = A[i] + B[i]
D[i] = T.exp(C[i])
return Before
def after_compute_legalize():
@tvm.script.ir_module
class After:
@T.prim_func(s_tir=True)
def main(
Aptr: T.handle("bfloat16", storage_scope="shared"),
Bptr: T.handle("bfloat16", storage_scope="local"),
Dptr: T.handle("bfloat16"),
):
T.func_attr({"global_symbol": "main"})
A = T.decl_buffer((100,), "bfloat16", data=Aptr)
B = T.decl_buffer((100,), "bfloat16", data=Bptr)
D = T.decl_buffer((100,), "bfloat16", data=Dptr)
C = T.decl_buffer((100,), "float32")
for i in T.grid(100):
C[i] = bf16tof32(A[i]) + bf16tof32(B[i])
D[i] = f32tobf16(T.exp(C[i]))
return After
def after_storage_legalize():
@tvm.script.ir_module
class After:
@T.prim_func(s_tir=True)
def main(
Aptr: T.handle("uint16", storage_scope="shared"),
Bptr: T.handle("uint16", storage_scope="local"),
Dptr: T.handle("uint16"),
):
T.func_attr({"global_symbol": "main"})
A = T.decl_buffer((100,), "uint16", data=Aptr)
B = T.decl_buffer((100,), "uint16", data=Bptr)
D = T.decl_buffer((100,), "uint16", data=Dptr)
C = T.decl_buffer((100,), "float32")
for i in T.grid(100):
C[i] = u16tof32(A[i]) + u16tof32(B[i])
D[i] = f32tou16(T.exp(C[i]))
return After
target = Target("nvidia/geforce-rtx-2080-ti")
before = BindTarget(target)(get_before())
after_compute = tvm.tirx.transform.BF16ComputeLegalize()(before)
after_storage = tvm.tirx.transform.BF16StorageLegalize()(after_compute)
tvm.ir.assert_structural_equal(after_compute, BindTarget(target)(after_compute_legalize()))
tvm.ir.assert_structural_equal(after_storage, BindTarget(target)(after_storage_legalize()))
def test_bf16_storage_compute_scope_wont_legalize():
def get_before():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(
Aptr: T.handle("bfloat16", storage_scope="shared"),
Bptr: T.handle("bfloat16", storage_scope="local"),
Dptr: T.handle("bfloat16"),
):
T.func_attr({"global_symbol": "main"})
A = T.decl_buffer((100,), "bfloat16", data=Aptr)
B = T.decl_buffer((100,), "bfloat16", data=Bptr)
D = T.decl_buffer((100,), "bfloat16", data=Dptr)
C = T.decl_buffer((100,), "bfloat16")
for i in T.grid(100):
C[i] = A[i] + B[i]
D[i] = T.exp(C[i])
return Before
def after_compute_legalize():
@tvm.script.ir_module
class After:
@T.prim_func(s_tir=True)
def main(
Aptr: T.handle("bfloat16", storage_scope="shared"),
Bptr: T.handle("bfloat16", storage_scope="local"),
Dptr: T.handle("bfloat16"),
):
T.func_attr({"global_symbol": "main"})
A = T.decl_buffer((100,), "bfloat16", data=Aptr)
B = T.decl_buffer((100,), "bfloat16", data=Bptr)
D = T.decl_buffer((100,), "bfloat16", data=Dptr)
C = T.decl_buffer((100,), "bfloat16")
for i in T.grid(100):
C[i] = A[i] + B[i]
D[i] = T.exp(C[i])
return After
def after_storage_legalize():
@tvm.script.ir_module
class After:
@T.prim_func(s_tir=True)
def main(
Aptr: T.handle("bfloat16", storage_scope="shared"),
Bptr: T.handle("bfloat16", storage_scope="local"),
Dptr: T.handle("bfloat16"),
):
T.func_attr({"global_symbol": "main"})
A = T.decl_buffer((100,), "bfloat16", data=Aptr)
B = T.decl_buffer((100,), "bfloat16", data=Bptr)
D = T.decl_buffer((100,), "bfloat16", data=Dptr)
C = T.decl_buffer((100,), "bfloat16")
for i in T.grid(100):
C[i] = A[i] + B[i]
D[i] = T.exp(C[i])
return After
target = Target("nvidia/geforce-rtx-3090-ti")
before = BindTarget(target)(get_before())
after_compute = tvm.tirx.transform.BF16ComputeLegalize()(before)
after_storage = tvm.tirx.transform.BF16StorageLegalize()(after_compute)
tvm.ir.assert_structural_equal(after_compute, BindTarget(target)(after_compute_legalize()))
tvm.ir.assert_structural_equal(after_storage, BindTarget(target)(after_storage_legalize()))
def test_bf16_reduce_will_legalize():
def get_before():
@tvm.script.ir_module
class Before:
@T.prim_func(private=True, s_tir=True)
def main(
Aptr: T.handle("bfloat16", storage_scope="shared"),
):
A_flat = T.decl_buffer(4096, "bfloat16", data=Aptr)
for i in range(128):
threadIdx_x = T.launch_thread("threadIdx.x", 32)
reduce = T.decl_buffer(1, dtype="bfloat16", scope="local")
with T.attr(
T.comm_reducer(lambda x, y: x + y, [T.bfloat16(0)]),
"reduce_scope",
T.int32(0),
):
T.tvm_thread_allreduce(
T.uint32(1),
A_flat[0],
T.bool(True),
reduce[0],
threadIdx_x,
)
return Before
def after_compute_legalize():
@tvm.script.ir_module
class After:
@T.prim_func(private=True, s_tir=True)
def main(
Aptr: T.handle("bfloat16", storage_scope="shared"),
):
A_flat_1 = T.decl_buffer(4096, "bfloat16", data=Aptr)
for i in range(128):
threadIdx_x = T.launch_thread("threadIdx.x", 32)
reduce = T.decl_buffer(1, dtype="float32", scope="local")
with T.attr(
T.comm_reducer(lambda x, y: x + y, [T.float32(0)]),
"reduce_scope",
T.int32(0),
):
T.tvm_thread_allreduce(
T.uint32(1),
T.reinterpret(
"float32",
T.shift_left(
T.Cast("uint32", T.reinterpret("uint16", A_flat_1[0])),
T.uint32(16),
),
),
T.bool(True),
reduce[0],
threadIdx_x,
)
return After
def after_storage_legalize():
@tvm.script.ir_module
class After:
@T.prim_func(private=True, s_tir=True)
def main(
Aptr: T.handle("uint16", storage_scope="shared"),
):
A_flat_1 = T.decl_buffer(4096, "uint16", data=Aptr)
for i in range(128):
threadIdx_x = T.launch_thread("threadIdx.x", 32)
reduce = T.decl_buffer(1, dtype="float32", scope="local")
with T.attr(
T.comm_reducer(lambda x, y: x + y, [T.float32(0)]),
"reduce_scope",
T.int32(0),
):
T.tvm_thread_allreduce(
T.uint32(1),
T.reinterpret(
"float32",
T.shift_left(
T.Cast("uint32", T.reinterpret("uint16", A_flat_1[0])),
T.uint32(16),
),
),
T.bool(True),
reduce[0],
threadIdx_x,
)
return After
target = Target("nvidia/geforce-rtx-2080-ti")
before = BindTarget(target)(get_before())
after_compute = tvm.tirx.transform.BF16ComputeLegalize()(before)
after_storage = tvm.tirx.transform.BF16StorageLegalize()(after_compute)
tvm.ir.assert_structural_equal(after_compute, BindTarget(target)(after_compute_legalize()))
tvm.ir.assert_structural_equal(after_storage, BindTarget(target)(after_storage_legalize()))
def test_bf16_reduce_wont_legalize():
def get_before():
@tvm.script.ir_module
class Before:
@T.prim_func(private=True, s_tir=True)
def main(
Aptr: T.handle("bfloat16", storage_scope="shared"),
):
A_flat = T.decl_buffer(4096, "bfloat16", data=Aptr)
for i in range(128):
threadIdx_x = T.launch_thread("threadIdx.x", 32)
reduce = T.decl_buffer(1, dtype="bfloat16", scope="local")
with T.attr(
T.comm_reducer(lambda x, y: x + y, [T.bfloat16(0)]),
"reduce_scope",
T.int32(0),
):
T.tvm_thread_allreduce(
T.uint32(1),
A_flat[0],
T.bool(True),
reduce[0],
threadIdx_x,
)
return Before
def after_compute_legalize():
@tvm.script.ir_module
class After:
@T.prim_func(private=True, s_tir=True)
def main(
Aptr: T.handle("bfloat16", storage_scope="shared"),
):
A_flat = T.decl_buffer(4096, "bfloat16", data=Aptr)
for i in range(128):
threadIdx_x = T.launch_thread("threadIdx.x", 32)
reduce = T.decl_buffer(1, dtype="bfloat16", scope="local")
with T.attr(
T.comm_reducer(lambda x, y: x + y, [T.bfloat16(0)]),
"reduce_scope",
T.int32(0),
):
T.tvm_thread_allreduce(
T.uint32(1),
A_flat[0],
T.bool(True),
reduce[0],
threadIdx_x,
)
return After
def after_storage_legalize():
@tvm.script.ir_module
class After:
@T.prim_func(private=True, s_tir=True)
def main(
Aptr: T.handle("bfloat16", storage_scope="shared"),
):
A_flat = T.decl_buffer(4096, "bfloat16", data=Aptr)
for i in range(128):
threadIdx_x = T.launch_thread("threadIdx.x", 32)
reduce = T.decl_buffer(1, dtype="bfloat16", scope="local")
with T.attr(
T.comm_reducer(lambda x, y: x + y, [T.bfloat16(0)]),
"reduce_scope",
T.int32(0),
):
T.tvm_thread_allreduce(
T.uint32(1),
A_flat[0],
T.bool(True),
reduce[0],
threadIdx_x,
)
return After
target = Target("nvidia/geforce-rtx-3090-ti")
before = BindTarget(target)(get_before())
after_compute = tvm.tirx.transform.BF16ComputeLegalize()(before)
after_storage = tvm.tirx.transform.BF16StorageLegalize()(after_compute)
tvm.ir.assert_structural_equal(after_compute, BindTarget(target)(after_compute_legalize()))
tvm.ir.assert_structural_equal(after_storage, BindTarget(target)(after_storage_legalize()))
if __name__ == "__main__":
test_bf16_storage_compute_scope_will_legalize()
test_bf16_storage_compute_scope_wont_legalize()
test_bf16_reduce_will_legalize()
test_bf16_reduce_wont_legalize()
@@ -0,0 +1,780 @@
# 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.
import hashlib
import tvm
from tvm.ir.base import save_json
from tvm.script import tirx as T
# =====================================================================
# T1: Basic multi-level CSE
# Two common subexpressions at different scoping levels.
# =====================================================================
def test_basic():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(B: T.Buffer((50,), "int32"), i1: T.int32, i2: T.int32, z3: T.int32):
z1 = T.bind(1)
z2 = T.bind(2)
B[i1] = z1 + z2
x = T.bind(1)
y = T.bind(1)
a = T.bind((x + y) + (z1 + z2))
b = T.bind((x + y) + z3)
B[i2] = a + b
@tvm.script.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(B: T.Buffer((50,), "int32"), i1: T.int32, i2: T.int32, z3: T.int32):
z1 = T.bind(1)
z2 = T.bind(2)
cse_v1 = T.bind(z1 + z2)
B[i1] = cse_v1
x = T.bind(1)
y = T.bind(1)
cse_v2 = T.bind(x + y)
a = T.bind(cse_v2 + cse_v1)
b = T.bind(cse_v2 + z3)
B[i2] = a + b
after = tvm.tirx.transform.CommonSubexprElim()(Before)
tvm.ir.assert_structural_equal(after, Expected)
# =====================================================================
# T2: If -- single-branch CSE
# Duplicated expression only in then-branch stays inside then-branch.
# =====================================================================
def test_if_single_branch():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(
B: T.Buffer((50,), "int32"),
i1: T.int32,
i2: T.int32,
i3: T.int32,
y: T.int32,
z: T.int32,
):
b = T.bind(1)
if b:
B[i1] = y + z
B[i2] = y + z
else:
B[i3] = y
@tvm.script.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(
B: T.Buffer((50,), "int32"),
i1: T.int32,
i2: T.int32,
i3: T.int32,
y: T.int32,
z: T.int32,
):
b = T.bind(1)
if b:
cse_v1 = T.bind(y + z)
B[i1] = cse_v1
B[i2] = cse_v1
else:
B[i3] = y
after = tvm.tirx.transform.CommonSubexprElim()(Before)
tvm.ir.assert_structural_equal(after, Expected)
# =====================================================================
# T3: If -- both-branch CSE
# Duplicated expression in both branches is hoisted before the if.
# =====================================================================
def test_if_both_branches():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(
B: T.Buffer((50,), "int32"),
i1: T.int32,
i2: T.int32,
i3: T.int32,
y: T.int32,
z: T.int32,
):
b = T.bind(1)
if b:
B[i1] = y + z
B[i2] = y
else:
B[i3] = y + z
@tvm.script.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(
B: T.Buffer((50,), "int32"),
i1: T.int32,
i2: T.int32,
i3: T.int32,
y: T.int32,
z: T.int32,
):
b = T.bind(1)
cse_v1 = T.bind(y + z)
if b:
B[i1] = cse_v1
B[i2] = y
else:
B[i3] = cse_v1
after = tvm.tirx.transform.CommonSubexprElim()(Before)
tvm.ir.assert_structural_equal(after, Expected)
# =====================================================================
# T4: Cascade CSE
# Introducing (x+y)+z creates opportunity for x+y.
# =====================================================================
def test_cascade():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(
B: T.Buffer((50,), "int32"),
i1: T.int32,
i2: T.int32,
i3: T.int32,
x: T.int32,
y: T.int32,
z: T.int32,
):
B[i1] = (x + y) + z
B[i2] = (x + y) + z
B[i3] = x + y
@tvm.script.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(
B: T.Buffer((50,), "int32"),
i1: T.int32,
i2: T.int32,
i3: T.int32,
x: T.int32,
y: T.int32,
z: T.int32,
):
cse_v2 = T.bind(x + y)
cse_v1 = T.bind(cse_v2 + z)
B[i1] = cse_v1
B[i2] = cse_v1
B[i3] = cse_v2
after = tvm.tirx.transform.CommonSubexprElim()(Before)
tvm.ir.assert_structural_equal(after, Expected)
# =====================================================================
# T5: No change when no duplication
# Single occurrence — pass is identity.
# =====================================================================
def test_no_duplication():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(x: T.int32, y: T.int32, z: T.int32):
a = T.bind(x + (y + z))
T.evaluate(a)
@tvm.script.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(x: T.int32, y: T.int32, z: T.int32):
a = T.bind(x + (y + z))
T.evaluate(a)
after = tvm.tirx.transform.CommonSubexprElim()(Before)
tvm.ir.assert_structural_equal(after, Expected)
# =====================================================================
# T6: Deterministic output
# Multiple runs on same input produce identical output (same var names,
# ordering). Verified via JSON serialization hash.
# =====================================================================
def test_deterministic():
NUM_TERMS = 10
REPEATS = 10
x = tvm.tirx.Var("x", "int32")
result = tvm.tirx.Var("result", "int32")
offsets = sorted([i + 1 for i in range(NUM_TERMS)])
inc1 = [(x + offsets[i]) for i in range(NUM_TERMS)]
inc2 = [(x + offsets[i]) for i in range(NUM_TERMS)]
expression = x
for add in inc1 + inc2:
expression = expression + add
body = tvm.tirx.SeqStmt([tvm.tirx.Bind(result, expression), tvm.tirx.Evaluate(result)])
mod = tvm.IRModule.from_expr(tvm.tirx.PrimFunc([x], body))
initial_hash = None
for _ in range(REPEATS):
out = tvm.tirx.transform.CommonSubexprElim()(mod)
func = out["main"]
json_val = save_json(func)
json_hash = hashlib.sha256(json_val.encode()).hexdigest()
if initial_hash is None:
initial_hash = json_hash
assert json_hash == initial_hash
# =====================================================================
# T7: CSE inside for-loop
# Common sub-expression inside a for-loop body.
# =====================================================================
def test_for_loop():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(B: T.Buffer((50,), "int32"), y: T.int32, z: T.int32):
for i in range(10):
B[i] = y + z
B[i + 10] = y + z
@tvm.script.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(B: T.Buffer((50,), "int32"), y: T.int32, z: T.int32):
for i in range(10):
cse_v1 = T.bind(y + z)
B[i] = cse_v1
B[i + 10] = cse_v1
after = tvm.tirx.transform.CommonSubexprElim()(Before)
tvm.ir.assert_structural_equal(after, Expected)
# =====================================================================
# T8: CSE across for-loop and outer scope
# Expression appears both outside and inside a for-loop. Binding placed
# in outer scope before the for.
# =====================================================================
def test_for_hoist():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(B: T.Buffer((50,), "int32"), y: T.int32, z: T.int32):
B[0] = y + z
for i in range(10):
B[i + 1] = y + z
@tvm.script.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(B: T.Buffer((50,), "int32"), y: T.int32, z: T.int32):
cse_v1 = T.bind(y + z)
B[0] = cse_v1
for i in range(10):
B[i + 1] = cse_v1
after = tvm.tirx.transform.CommonSubexprElim()(Before)
tvm.ir.assert_structural_equal(after, Expected)
# =====================================================================
# T9: Cannot-lift -- expressions containing BufferLoad
# Expressions containing BufferLoad are ineligible even when duplicated,
# because lifting them would change semantics (buffer may alias).
# =====================================================================
def test_cannot_lift_bufferload():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((50,), "int32"), B: T.Buffer((50,), "int32")):
B[0] = A[0] + A[0]
B[1] = A[0] + A[0]
@tvm.script.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((50,), "int32"), B: T.Buffer((50,), "int32")):
B[0] = A[0] + A[0]
B[1] = A[0] + A[0]
after = tvm.tirx.transform.CommonSubexprElim()(Before)
tvm.ir.assert_structural_equal(after, Expected)
# =====================================================================
# T10: Nested if -- multi-level scope LCA
# Expression in both branches of an inner if, but not the outer else.
# Binding placed at the inner if's parent scope.
# =====================================================================
def test_nested_if():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(
B: T.Buffer((50,), "int32"),
c1: T.int32,
c2: T.int32,
y: T.int32,
z: T.int32,
):
if c1:
if c2:
B[0] = y + z
else:
B[1] = y + z
else:
B[2] = y
@tvm.script.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(
B: T.Buffer((50,), "int32"),
c1: T.int32,
c2: T.int32,
y: T.int32,
z: T.int32,
):
if c1:
cse_v1 = T.bind(y + z)
if c2:
B[0] = cse_v1
else:
B[1] = cse_v1
else:
B[2] = y
after = tvm.tirx.transform.CommonSubexprElim()(Before)
tvm.ir.assert_structural_equal(after, Expected)
# =====================================================================
# T11: Multiple independent CSE candidates
# Several independent expressions, each duplicated.
# =====================================================================
def test_multi_independent():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(
B: T.Buffer((50,), "int32"),
a: T.int32,
b: T.int32,
c: T.int32,
d: T.int32,
):
B[0] = a + b
B[1] = c + d
B[2] = a + b
B[3] = c + d
@tvm.script.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(
B: T.Buffer((50,), "int32"),
a: T.int32,
b: T.int32,
c: T.int32,
d: T.int32,
):
cse_v1 = T.bind(a + b)
B[0] = cse_v1
cse_v2 = T.bind(c + d)
B[1] = cse_v2
B[2] = cse_v1
B[3] = cse_v2
after = tvm.tirx.transform.CommonSubexprElim()(Before)
tvm.ir.assert_structural_equal(after, Expected)
# =====================================================================
# T12: Expression in if-condition and branch
# One occurrence in the if-condition (parent scope), one in the
# then-branch. Binding hoisted before the if.
# =====================================================================
def test_if_condition():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(B: T.Buffer((50,), "int32"), y: T.int32, z: T.int32):
if y + z > 0:
B[0] = y + z
@tvm.script.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(B: T.Buffer((50,), "int32"), y: T.int32, z: T.int32):
cse_v1 = T.bind(y + z)
if cse_v1 > 0:
B[0] = cse_v1
after = tvm.tirx.transform.CommonSubexprElim()(Before)
tvm.ir.assert_structural_equal(after, Expected)
# =====================================================================
# T13: Cannot-lift -- expression containing Call
# Function calls cannot be lifted.
# =====================================================================
def test_cannot_lift_call():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(B: T.Buffer((50,), "int32"), x: T.int32):
B[0] = T.call_extern("my_func", x, dtype="int32") + 1
B[1] = T.call_extern("my_func", x, dtype="int32") + 1
@tvm.script.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(B: T.Buffer((50,), "int32"), x: T.int32):
B[0] = T.call_extern("my_func", x, dtype="int32") + 1
B[1] = T.call_extern("my_func", x, dtype="int32") + 1
after = tvm.tirx.transform.CommonSubexprElim()(Before)
tvm.ir.assert_structural_equal(after, Expected)
# =====================================================================
# T14: No single-use binding
# When all occurrences of a sub-expression are inside a deeper
# expression that is also CSE'd, the sub-expression should NOT get
# its own binding (it would be used only once in the parent's value).
# =====================================================================
def test_no_single_use_binding():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(
B: T.Buffer((50,), "int32"),
x: T.int32,
y: T.int32,
z: T.int32,
):
B[0] = (x + y) + z
B[1] = (x + y) + z
@tvm.script.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(
B: T.Buffer((50,), "int32"),
x: T.int32,
y: T.int32,
z: T.int32,
):
cse_v1 = T.bind((x + y) + z)
B[0] = cse_v1
B[1] = cse_v1
after = tvm.tirx.transform.CommonSubexprElim()(Before)
tvm.ir.assert_structural_equal(after, Expected)
# =====================================================================
# T15: For-loop extent lifting
# Expression shared between loop extent and body is hoisted outside the
# loop rather than being re-evaluated every iteration.
# =====================================================================
def test_for_extent_lift():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(B: T.Buffer((50,), "int32"), y: T.int32, z: T.int32):
for i in range(y + z):
B[i] = y + z
@tvm.script.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(B: T.Buffer((50,), "int32"), y: T.int32, z: T.int32):
cse_v1 = T.bind(y + z)
for i in range(cse_v1):
B[i] = cse_v1
after = tvm.tirx.transform.CommonSubexprElim()(Before)
tvm.ir.assert_structural_equal(after, Expected)
# =====================================================================
# T16: Loop-variable expression stays inside loop
# An expression using the loop variable (i*4) that appears multiple
# times inside the body must NOT be hoisted outside the loop.
# =====================================================================
def test_loop_var_expr_stays_inside():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(
A: T.Buffer((50,), "int32"),
B: T.Buffer((50,), "int32"),
):
for i in range(10):
A[i * 4] = B[i * 4]
@tvm.script.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(
A: T.Buffer((50,), "int32"),
B: T.Buffer((50,), "int32"),
):
for i in range(10):
cse_v1 = T.bind(i * 4)
A[cse_v1] = B[cse_v1]
after = tvm.tirx.transform.CommonSubexprElim()(Before)
tvm.ir.assert_structural_equal(after, Expected)
# =====================================================================
# T17: No normalization without commoning
# Single-occurrence expression is not normalized or modified.
# =====================================================================
def test_no_normalization_without_commoning():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(x: T.int32, y: T.int32, z: T.int32):
a = T.bind(x + (y + z))
T.evaluate(a)
@tvm.script.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(x: T.int32, y: T.int32, z: T.int32):
a = T.bind(x + (y + z))
T.evaluate(a)
after = tvm.tirx.transform.CommonSubexprElim()(Before)
tvm.ir.assert_structural_equal(after, Expected)
# =====================================================================
# T18: Let-bound variable -- no extraction from Let body
# Expressions inside a Let body that reference the Let-bound variable
# must NOT be extracted, because the variable is only in scope inside
# the Let body, not at the statement level where Bind would be placed.
# =====================================================================
def test_let_body_no_extraction():
"""CSE must not extract expressions from Let bodies that use Let-bound vars."""
x = tvm.tirx.Var("x", "int32")
y = tvm.tirx.Var("y", "int32")
# Let(x, 1, (x+y) + (x+y)) -- x+y appears twice but x is Let-bound
let_expr = tvm.tirx.Let(x, tvm.tirx.IntImm("int32", 1), (x + y) + (x + y))
buf = tvm.tirx.decl_buffer((10,), "int32", name="B")
i = tvm.tirx.Var("i", "int32")
store = tvm.tirx.BufferStore(buf, let_expr, [i])
loop = tvm.tirx.For(
i,
tvm.tirx.const(0, "int32"),
tvm.tirx.const(10, "int32"),
tvm.tirx.ForKind.SERIAL,
store,
)
func = tvm.tirx.PrimFunc([buf, y], loop)
mod = tvm.IRModule({"main": func})
mod_after = tvm.tirx.transform.CommonSubexprElim()(mod)
# No CSE variables should be introduced
script = mod_after["main"].script()
assert "cse_v" not in script, f"CSE incorrectly extracted from Let body:\n{script}"
# =====================================================================
# T19: Let value -- CSE works for shared Let value expressions
# The Let value is evaluated before the binding takes effect, so
# expressions in the Let value CAN be CSE'd with expressions outside.
# =====================================================================
def test_let_value_cse():
"""CSE can extract from Let values (computed before binding)."""
x = tvm.tirx.Var("x", "int32")
y = tvm.tirx.Var("y", "int32")
z = tvm.tirx.Var("z", "int32")
# Let(x, y+z, x+1) with y+z also appearing outside the Let
let_expr = tvm.tirx.Let(x, y + z, x + 1)
buf = tvm.tirx.decl_buffer((10,), "int32", name="B")
i = tvm.tirx.Var("i", "int32")
store = tvm.tirx.BufferStore(buf, (y + z) + let_expr, [i])
loop = tvm.tirx.For(
i,
tvm.tirx.const(0, "int32"),
tvm.tirx.const(10, "int32"),
tvm.tirx.ForKind.SERIAL,
store,
)
func = tvm.tirx.PrimFunc([buf, y, z], loop)
mod = tvm.IRModule({"main": func})
mod_after = tvm.tirx.transform.CommonSubexprElim()(mod)
# y+z should be extracted (appears in Let value AND outside)
script = mod_after["main"].script()
assert "cse_v" in script, f"CSE should extract y+z from Let value:\n{script}"
# =====================================================================
# T20: Nested Let -- both levels respected
# Multiple nested Let expressions with common sub-expressions in the
# innermost body. None should be extracted.
# =====================================================================
def test_nested_let_no_extraction():
"""CSE must not extract from nested Let bodies."""
x = tvm.tirx.Var("x", "int32")
y = tvm.tirx.Var("y", "int32")
z = tvm.tirx.Var("z", "int32")
# Let(x, 1, Let(y, 2, (x+y+z) + (x+y+z)))
inner = (x + y + z) + (x + y + z)
nested_let = tvm.tirx.Let(
x, tvm.tirx.IntImm("int32", 1), tvm.tirx.Let(y, tvm.tirx.IntImm("int32", 2), inner)
)
buf = tvm.tirx.decl_buffer((10,), "int32", name="B")
i = tvm.tirx.Var("i", "int32")
store = tvm.tirx.BufferStore(buf, nested_let, [i])
loop = tvm.tirx.For(
i,
tvm.tirx.const(0, "int32"),
tvm.tirx.const(10, "int32"),
tvm.tirx.ForKind.SERIAL,
store,
)
func = tvm.tirx.PrimFunc([buf, z], loop)
mod = tvm.IRModule({"main": func})
mod_after = tvm.tirx.transform.CommonSubexprElim()(mod)
script = mod_after["main"].script()
assert "cse_v" not in script, f"CSE incorrectly extracted from nested Let body:\n{script}"
# =====================================================================
# T21: Let with lowered floordiv pattern
# Simulates the pattern produced by LowerIntrin for floordiv:
# Let(rmod, truncmod(a,b), Let(rdiv, div(a,b), Select(...)))
# wrapped in Let(x, load, Let(y, load, ...))
# This is the regression test for the CI failure in lower_intrin tests.
# =====================================================================
def test_let_floordiv_pattern():
"""CSE must handle the Let pattern from LowerIntrin's floordiv lowering."""
x = tvm.tirx.Var("x", "int32")
y = tvm.tirx.Var("y", "int32")
rmod = tvm.tirx.Var("rmod", "int32")
rdiv = tvm.tirx.Var("rdiv", "int32")
# Simulate lowered floordiv: Let(rmod, x%y, Let(rdiv, x/y, Select(...)))
select_cond = tvm.tirx.And(y >= 0, rmod >= 0) | tvm.tirx.And(y < 0, rmod <= 0)
select_expr = tvm.tirx.Select(select_cond, rdiv, rdiv - 1)
inner_let = tvm.tirx.Let(rdiv, tvm.tirx.Div(x, y), select_expr)
outer_let = tvm.tirx.Let(rmod, tvm.tirx.Mod(x, y), inner_let)
# Wrap in Let(x, load, Let(y, load, ...))
buf_a = tvm.tirx.decl_buffer((10,), "int32", name="A")
buf_b = tvm.tirx.decl_buffer((10,), "int32", name="B")
buf_c = tvm.tirx.decl_buffer((10,), "int32", name="C")
i = tvm.tirx.Var("i", "int32")
full_expr = tvm.tirx.Let(
x,
tvm.tirx.BufferLoad(buf_a, [i]),
tvm.tirx.Let(y, tvm.tirx.BufferLoad(buf_b, [i]), outer_let),
)
store = tvm.tirx.BufferStore(buf_c, full_expr, [i])
loop = tvm.tirx.For(
i,
tvm.tirx.const(0, "int32"),
tvm.tirx.const(10, "int32"),
tvm.tirx.ForKind.SERIAL,
store,
)
func = tvm.tirx.PrimFunc([buf_a, buf_b, buf_c], loop)
mod = tvm.IRModule({"main": func})
# Should not crash and should not extract Let-bound vars
mod_after = tvm.tirx.transform.CommonSubexprElim()(mod)
script = mod_after["main"].script()
assert "cse_v" not in script, f"CSE incorrectly extracted from Let body:\n{script}"
# =====================================================================
# T22: No lifting of bool predicate (comparison expression)
# A duplicated `i < n` feeds two if-statements. CSE must leave it
# inline rather than hoisting a `cse_v: bool = (i < n)` binding.
# =====================================================================
def test_no_lift_bool_predicate():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(B: T.Buffer((50,), "int32"), n: T.int32, x: T.int32):
for i in range(50):
if i < n:
B[i] = x
if i < n:
B[i] = x + 1
after = tvm.tirx.transform.CommonSubexprElim()(Before)
tvm.ir.assert_structural_equal(after, Before)
assert "cse_v" not in after["main"].script()
# =====================================================================
# T23: No lifting of bool logical expression (And)
# A duplicated `a && b` feeds two if-statements. CSE must leave it
# inline rather than hoisting a `cse_v: bool = T.And(a, b)` binding.
# =====================================================================
def test_no_lift_bool_logical():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(B: T.Buffer((50,), "int32"), a: T.bool, b: T.bool, x: T.int32):
if T.And(a, b):
B[0] = x
if T.And(a, b):
B[1] = x + 1
after = tvm.tirx.transform.CommonSubexprElim()(Before)
tvm.ir.assert_structural_equal(after, Before)
assert "cse_v" not in after["main"].script()
if __name__ == "__main__":
test_basic()
test_if_single_branch()
test_if_both_branches()
test_cascade()
test_no_duplication()
test_deterministic()
test_for_loop()
test_for_hoist()
test_cannot_lift_bufferload()
test_nested_if()
test_multi_independent()
test_if_condition()
test_cannot_lift_call()
test_no_single_use_binding()
test_for_extent_lift()
test_loop_var_expr_stays_inside()
test_no_normalization_without_commoning()
test_let_body_no_extraction()
test_let_value_cse()
test_nested_let_no_extraction()
test_let_floordiv_pattern()
test_no_lift_bool_predicate()
test_no_lift_bool_logical()
@@ -0,0 +1,539 @@
# 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: F401
import tvm
import tvm.testing
from tvm import ir, tirx
from tvm.script import ir as I
from tvm.script import tirx as T
def test_reuse_in_sequential_bind():
"""De-dup sequential variable bindings"""
# Manually construct the PrimFunc body, as SSA violations are
# not valid TIR, and may not be expressible in future versions
# of TVMSCript.
var = tirx.Var("var", "int32")
sequential_bindings = tirx.SeqStmt(
[
tirx.Bind(var, 16),
tirx.Evaluate(var),
tirx.Bind(var, 32),
tirx.Evaluate(var),
]
)
before = tirx.PrimFunc([], sequential_bindings).with_attr("s_tir", True)
@T.prim_func(private=True, s_tir=True)
def expected():
var1 = T.bind(T.int32(16))
T.evaluate(var1)
var2 = T.bind(T.int32(32))
T.evaluate(var2)
mod = tvm.IRModule.from_expr(before)
mod = tvm.tirx.transform.ConvertSSA()(mod)
tvm.ir.assert_structural_equal(mod["main"], expected)
def test_reuse_in_nested_bind():
"""De-dup sequential bindings of the same variable.
In the flat Bind model, all Binds are siblings in a SeqStmt. A second
Bind of the same variable redefines it for all subsequent siblings.
ConvertSSA should create a new variable for the second binding and
update all subsequent uses to refer to the new variable.
"""
# Manually construct the PrimFunc body, as SSA violations are
# not valid TIR, and may not be expressible in future versions
# of TVMScript.
var = tirx.Var("var", "int32")
# Note: nested SeqStmt is flattened by the IR builder, so the input
# is actually a flat SeqStmt with 5 elements.
inner_seq = tirx.SeqStmt(
[
tirx.Bind(var, 16),
tirx.Evaluate(var),
]
)
outer_seq = tirx.SeqStmt(
[
tirx.Bind(var, 32),
tirx.Evaluate(var),
inner_seq,
tirx.Evaluate(var),
]
)
before = tirx.PrimFunc([], outer_seq)
# In the flat model, the second Bind(var, 16) redefines var for
# ALL subsequent siblings including the last Evaluate.
var1 = tirx.Var("var", "int32")
var2 = tirx.Var("var", "int32")
expected_body = tirx.SeqStmt(
[
tirx.Bind(var1, 32),
tirx.Evaluate(var1),
tirx.Bind(var2, 16),
tirx.Evaluate(var2),
tirx.Evaluate(var2),
]
)
expected = tirx.PrimFunc([], expected_body)
mod = tvm.IRModule.from_expr(before)
mod = tvm.tirx.transform.ConvertSSA()(mod)
tvm.ir.assert_structural_equal(mod["main"], expected)
def test_reused_var_across_module():
"""De-duplicate Var bindings across entire module"""
@T.prim_func(private=True, s_tir=True)
def func():
var = T.bind(10)
T.evaluate(var)
before = tvm.IRModule(
{
"func_a": func.with_attr("global_symbol", "func_a"),
"func_b": func.with_attr("global_symbol", "func_b"),
}
)
@I.ir_module
class expected:
@T.prim_func(s_tir=True)
def func_a():
var: T.let = T.int32(10)
T.evaluate(var)
@T.prim_func(s_tir=True)
def func_b():
var: T.let = T.int32(10)
T.evaluate(var)
after = tvm.tirx.transform.ConvertSSA()(before)
tvm.ir.assert_structural_equal(after, expected)
def test_reused_parameter():
"""De-duplicate Var usage in parameters
In this test, the same `tirx.Var` instance is used for the
parameter `n` in both functions.
"""
@T.prim_func(private=True, s_tir=True)
def func(n: T.int32):
T.evaluate(n)
before = tvm.IRModule(
{
"func_a": func.with_attr("global_symbol", "func_a"),
"func_b": func.with_attr("global_symbol", "func_b"),
}
)
@I.ir_module
class expected:
@T.prim_func(s_tir=True)
def func_a(n: T.int32):
T.evaluate(n)
@T.prim_func(s_tir=True)
def func_b(n: T.int32):
T.evaluate(n)
after = tvm.tirx.transform.ConvertSSA()(before)
tvm.ir.assert_structural_equal(after, expected)
def test_reused_buffer_obj():
"""De-duplicate buffer usage across entire module"""
@T.prim_func(private=True, s_tir=True)
def func(a: T.handle("float32")):
A = T.decl_buffer(shape=1, dtype="float32", data=a)
T.evaluate(A[0])
before = tvm.IRModule(
{
"func_a": func.with_attr("global_symbol", "func_a"),
"func_b": func.with_attr("global_symbol", "func_b"),
}
)
@I.ir_module
class expected:
@T.prim_func(s_tir=True)
def func_a(a: T.handle("float32")):
A = T.decl_buffer(shape=1, dtype="float32", data=a)
T.evaluate(A[0])
@T.prim_func(s_tir=True)
def func_b(a: T.handle("float32")):
A = T.decl_buffer(shape=1, dtype="float32", data=a)
T.evaluate(A[0])
after = tvm.tirx.transform.ConvertSSA()(before)
tvm.ir.assert_structural_equal(after, expected)
def test_reused_buffer_parameter():
"""De-duplicate buffer_map across entire module"""
@T.prim_func(private=True, s_tir=True)
def func(A: T.Buffer(1, "float32")):
T.evaluate(A[0])
before = tvm.IRModule(
{
"func_a": func.with_attr("global_symbol", "func_a"),
"func_b": func.with_attr("global_symbol", "func_b"),
}
)
@I.ir_module
class expected:
@T.prim_func(s_tir=True)
def func_a(A: T.Buffer(1, "float32")):
T.evaluate(A[0])
@T.prim_func(s_tir=True)
def func_b(A: T.Buffer(1, "float32")):
T.evaluate(A[0])
after = tvm.tirx.transform.ConvertSSA()(before)
tvm.ir.assert_structural_equal(after, expected)
def test_no_change_if_already_ssa():
"""A module that is already SSA should be unchanged"""
@I.ir_module
class before:
@T.prim_func(s_tir=True)
def func(A: T.Buffer(1, "float32")):
T.evaluate(A[0])
after = tvm.tirx.transform.ConvertSSA()(before)
tvm.ir.assert_structural_equal(before, after)
assert before.same_as(after)
def test_keep_duplicate_thread_idx_in_same_function():
"""Environment threads are treated as being at function scope
The `"thread_extent"` attribute has some unique semantics. It
serves as the definition of the `tirx::Var` representing the
environment thread (e.g. `threadIdx.x` in CUDA). However,
multiple `"thread_extent"` attributes may co-exist in the same
PrimFunc. For the purpose of variable scope, use of the
`tirx::Var` is only allowed within the body of the `AttrStmt`.
However, for the purpose of well-formed-ness, all
`"thread_extent"` attributes must use the same IterVar instance
(e.g. `WarpIndexFinder` in `lower_warp_memory.cc` may throw an
error if multiple IterVar instances occur).
If there are multiple `AttrStmt` with key `"thread_extent"` in a
single function (represented in TVMScript as `T.launch_thread`),
these should be treated as a definition of a single variable at
function scope, and should not be de-duplicated.
"""
@I.ir_module
class before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer([256], "float32")):
threadIdx_x = T.env_thread("threadIdx.x")
with T.launch_thread(threadIdx_x, 256):
A[threadIdx_x] = A[threadIdx_x] + 1.0
with T.launch_thread(threadIdx_x, 256):
A[threadIdx_x] = A[threadIdx_x] + 2.0
after = tvm.tirx.transform.ConvertSSA()(before)
tvm.ir.assert_structural_equal(after, before)
def test_de_duplicate_thread_idx_across_multiple_functions():
"""Environment threads are treated as being at function scope
See `test_keep_duplicate_thread_idx_in_same_function` for background
information.
If there are multiple functions in an IRModule, the `AttrStmt`
with key `"thread_extent"` in a single function (represented in
TVMScript as `T.launch_thread`), these should be treated as a
definition of a single variable at function scope, and should not
be de-duplicated.
For this test case, the `AttrStmt` for `"thread_extent"` are
written explicitly, without using the usual `T.env_thread` and
`T.launch_thread`, as they cannot represent the duplciate
Var/IterVar usage across the two PrimFuncs.
"""
threadIdx_x = tvm.tirx.Var("threadIdx_x", "int32")
# threadIdx_x is defined outside
@I.ir_module(check_well_formed=False)
class before:
@T.prim_func(s_tir=True)
def kernel_1(A: T.Buffer([256], "float32")):
T.attr(
T.iter_var(threadIdx_x, T.Range(0, 256), "ThreadIndex", "threadIdx.x"),
"thread_extent",
256,
)
A[threadIdx_x] = A[threadIdx_x] + T.float32(1)
@T.prim_func(s_tir=True)
def kernel_2(A: T.Buffer([256], "float32")):
T.attr(
T.iter_var(threadIdx_x, T.Range(0, 256), "ThreadIndex", "threadIdx.x"),
"thread_extent",
256,
)
A[threadIdx_x] = A[threadIdx_x] + T.float32(1)
@I.ir_module
class expected:
@T.prim_func(s_tir=True)
def kernel_1(A: T.Buffer([256], "float32")):
threadIdx_x = T.int32()
T.attr(
T.iter_var(threadIdx_x, T.Range(0, 256), "ThreadIndex", "threadIdx.x"),
"thread_extent",
256,
)
A[threadIdx_x] = A[threadIdx_x] + T.float32(1)
@T.prim_func(s_tir=True)
def kernel_2(A: T.Buffer([256], "float32")):
threadIdx_x = T.int32()
T.attr(
T.iter_var(threadIdx_x, T.Range(0, 256), "ThreadIndex", "threadIdx.x"),
"thread_extent",
256,
)
A[threadIdx_x] = A[threadIdx_x] + T.float32(1)
after = tvm.tirx.transform.ConvertSSA()(before)
tvm.ir.assert_structural_equal(after, expected)
def test_de_duplicate_thread_idx_iter_var_across_multiple_functions():
"""Environment threads are treated as being at function scope
Like `test_de_duplicate_thread_idx_across_multiple_functions`, except the
`IterVar` for the environment thread is duplicated across multiple
PrimFuncs, not just the `tirx.Var` inside the `IterVar`.
"""
threadIdx_x = tvm.tirx.Var("threadIdx_x", "int32")
iter_var = tvm.tirx.IterVar(
tvm.ir.Range(0, 256), threadIdx_x, tvm.tirx.IterVar.ThreadIndex, "threadIdx.x"
)
# complaints of multiple definitions for threadIdx_x
@I.ir_module(check_well_formed=False)
class before:
@T.prim_func(s_tir=True)
def kernel_1(A: T.Buffer([256], "float32")):
T.attr(iter_var, "thread_extent", 256)
A[threadIdx_x] = A[threadIdx_x] + T.float32(1)
@T.prim_func(s_tir=True)
def kernel_2(A: T.Buffer([256], "float32")):
T.attr(iter_var, "thread_extent", 256)
A[threadIdx_x] = A[threadIdx_x] + T.float32(1)
@I.ir_module(check_well_formed=False)
class expected:
@T.prim_func(s_tir=True)
def kernel_1(A: T.Buffer([256], "float32")):
threadIdx_x = T.int32()
T.attr(
T.iter_var(threadIdx_x, T.Range(0, 256), "ThreadIndex", "threadIdx.x"),
"thread_extent",
256,
)
A[threadIdx_x] = A[threadIdx_x] + T.float32(1)
@T.prim_func(s_tir=True)
def kernel_2(A: T.Buffer([256], "float32")):
threadIdx_x = T.int32()
T.attr(
T.iter_var(threadIdx_x, T.Range(0, 256), "ThreadIndex", "threadIdx.x"),
"thread_extent",
256,
)
A[threadIdx_x] = A[threadIdx_x] + T.float32(1)
after = tvm.tirx.transform.ConvertSSA()(before)
tvm.ir.assert_structural_equal(after, expected)
def test_thread_idx_reused_within_and_across_functions():
"""Environment threads are treated as being at function scope
A combination of
test_de_duplicate_thread_idx_iter_var_across_multiple_functions and
test_keep_duplicate_thread_idx_in_same_function. The re-use within a
function should be maintained, while re-use across functions is
de-duplicated.
"""
threadIdx_x = tvm.tirx.Var("threadIdx_x", "int32")
iter_var = tvm.tirx.IterVar(
tvm.ir.Range(0, 256), threadIdx_x, tvm.tirx.IterVar.ThreadIndex, "threadIdx.x"
)
# complaints of multiple definitions of threadIdx_x
@I.ir_module(check_well_formed=False)
class before:
@T.prim_func(s_tir=True)
def kernel_1(A: T.Buffer([256], "float32")):
with T.attr(iter_var, "thread_extent", 256):
A[threadIdx_x] = A[threadIdx_x] + 1.0
with T.attr(iter_var, "thread_extent", 256):
A[threadIdx_x] = A[threadIdx_x] + 2.0
@T.prim_func(s_tir=True)
def kernel_2(A: T.Buffer([256], "float32")):
with T.attr(iter_var, "thread_extent", 256):
A[threadIdx_x] = A[threadIdx_x] + 1.0
with T.attr(iter_var, "thread_extent", 256):
A[threadIdx_x] = A[threadIdx_x] + 2.0
@I.ir_module
class expected:
@T.prim_func(s_tir=True)
def kernel_1(A: T.Buffer([256], "float32")):
threadIdx_x = T.env_thread("threadIdx.x")
with T.launch_thread(threadIdx_x, 256):
A[threadIdx_x] = A[threadIdx_x] + 1.0
with T.launch_thread(threadIdx_x, 256):
A[threadIdx_x] = A[threadIdx_x] + 2.0
@T.prim_func(s_tir=True)
def kernel_2(A: T.Buffer([256], "float32")):
threadIdx_x = T.env_thread("threadIdx.x")
with T.launch_thread(threadIdx_x, 256):
A[threadIdx_x] = A[threadIdx_x] + 1.0
with T.launch_thread(threadIdx_x, 256):
A[threadIdx_x] = A[threadIdx_x] + 2.0
after = tvm.tirx.transform.ConvertSSA()(before)
tvm.ir.assert_structural_equal(after, expected)
def test_track_forward_declarations_in_attr_stmt():
"""T.attr statements may refer to a about-to-be-defined tirx.Var"""
# Generate the PrimFunc, which is already SSA
#
# This is constructed directly, rather than using TVMScript.
# This test case requires a `tirx.AttrStmt` that references a
# variable, followed by the `tirx.For` defining that variable.
# This is not expressible in TVMScript, as it only provides the
# loop iterator within the body of the loop.
i0_outer_outer = tirx.Var("i0_outer_outer", "int32")
i0_outer_inner = tirx.Var("i0_outer_inner", "int32")
i0_inner = tirx.Var("i0_inner", "int32")
A = tirx.decl_buffer(1024, "float32", "A")
B = tirx.decl_buffer(1024, "float32", "B")
index = i0_outer_outer * 52 + i0_outer_inner * 4 + i0_inner
stmt = tirx.BufferStore(B, tirx.BufferLoad(A, [index]), [index])
stmt = tirx.IfThenElse(i0_outer_outer * 13 + i0_outer_inner < 256, stmt, None)
stmt = tirx.For(i0_inner, 0, 4, tirx.ForKind.VECTORIZED, stmt)
stmt = tirx.For(i0_outer_inner, 0, 13, tirx.ForKind.PARALLEL, stmt)
stmt = tirx.AttrStmt(
T.iter_var(i0_outer_inner, None, "DataPar", ""),
"pragma_parallal_barrier_when_finish",
1,
stmt,
)
stmt = tirx.AttrStmt(
T.iter_var(i0_outer_inner, None, "DataPar", ""),
"pragma_parallal_stride_pattern",
1,
stmt,
)
stmt = tirx.For(i0_outer_outer, 0, 20, tirx.ForKind.SERIAL, stmt)
stmt = tirx.AttrStmt(
T.iter_var(i0_outer_outer, None, "DataPar", ""),
"pragma_parallal_launch_point",
1,
stmt,
)
A_handle = tirx.Var("A_handle", "handle")
B_handle = tirx.Var("B_handle", "handle")
before = tirx.PrimFunc(
[A_handle, B_handle],
stmt,
buffer_map={A_handle: A, B_handle: B},
)
mod = tvm.IRModule.from_expr(before)
after = tvm.tirx.transform.ConvertSSA()(mod)
tvm.ir.assert_structural_equal(after["main"], before)
def test_shared_shape_var_in_buffer_map_and_alloc_buffer():
"""Shape var shared across buffer_map entries and AllocBuffer should not be renamed.
When the same Var (e.g., `n`) appears in multiple buffer_map
entries (A and B both have shape [n]), ConvertSSA should not treat
the second occurrence as a redefinition. All uses of `n` in the
function body (including AllocBuffer shapes) must remain the same
Var object so that MakePackedAPI can bind it from the DLTensor shape.
"""
n = tirx.Var("n", "int32")
A_handle = tirx.Var("A_handle", "handle")
B_handle = tirx.Var("B_handle", "handle")
A = tirx.decl_buffer((n,), "float32", "A")
B = tirx.decl_buffer((n,), "float32", "B")
# AllocBuffer with shape [n] in the body (flat, no body)
C = tirx.decl_buffer((n,), "float32", "C")
body = tirx.SeqStmt([tirx.AllocBuffer(C), tirx.Evaluate(1)])
before = tirx.PrimFunc(
[A_handle, B_handle],
body,
buffer_map={A_handle: A, B_handle: B},
)
mod = tvm.IRModule.from_expr(before)
after = tvm.tirx.transform.ConvertSSA()(mod)
# The function is already SSA — ConvertSSA should not change it.
tvm.ir.assert_structural_equal(after["main"], before)
if __name__ == "__main__":
tvm.testing.main()
@@ -0,0 +1,440 @@
# 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.
import tvm
import tvm.testing
from tvm.script import ir as I
from tvm.script import tirx as T
def _transform():
return tvm.transform.Sequential(
[
tvm.tirx.transform.FlattenBuffer(),
tvm.tirx.transform.StmtSimplify(),
]
)
def test_elementwise():
"""2-d buffers are flattened to 1-d"""
@I.ir_module(s_tir=True)
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((16, 16), "float32"), C: T.Buffer((16, 16), "float32")):
for i in T.serial(0, 16):
B_new = T.decl_buffer([1, 16], "float32")
for j in T.serial(0, 16):
B_new[0, j] = A[i, j] + 1.0
for j in T.serial(0, 16):
C[i, j] = B_new[0, j] * 2.0
@I.ir_module(s_tir=True)
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((16, 16), "float32"), C: T.Buffer((16, 16), "float32")):
A_1 = T.decl_buffer(256, dtype="float32", data=A.data)
C_1 = T.decl_buffer(256, dtype="float32", data=C.data)
for i in T.serial(0, 16):
B_new = T.decl_buffer([16], "float32")
for j in T.serial(0, 16):
B_new[j] = A_1[((i * 16) + j)] + 1.0
for j in T.serial(0, 16):
C_1[((i * 16) + j)] = B_new[j] * 2.0
After = _transform()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_elementwise_without_decl_buffer():
"""2-d buffers are flattened to 1-d
Like test_elementwise, but the TIR doesn't have the DeclBuffer
node. The T.Buffer declaration applies only during the
parsing the TVMScript, and doesn't occur in the TIR itself. In
this case, the allocation should be assumed to be targeting flat
memory, and should be flattened to a 1-d allocation.
"""
@I.ir_module(check_well_formed=False, s_tir=True)
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((16, 16), "float32"), C: T.Buffer((16, 16), "float32")):
for i in T.serial(0, 16):
B_new_buf = T.alloc_buffer((1, 16), "float32")
B_new = T.Buffer([1, 16], "float32", data=B_new_buf.data)
for j in T.serial(0, 16):
B_new[0, j] = A[i, j] + 1.0
for j in T.serial(0, 16):
C[i, j] = B_new[0, j] * 2.0
@I.ir_module(check_well_formed=False, s_tir=True)
class Expected:
@T.prim_func(s_tir=True)
def main(input_A: T.Buffer((16, 16), "float32"), input_C: T.Buffer((16, 16), "float32")):
A = T.decl_buffer(256, dtype="float32", data=input_A.data)
C = T.decl_buffer(256, dtype="float32", data=input_C.data)
for i in T.serial(0, 16):
B_new_buf = T.alloc_buffer((16,), "float32")
B_new = T.Buffer(16, "float32", data=B_new_buf.data)
for j in T.serial(0, 16):
B_new[j] = A[((i * 16) + j)] + 1.0
for j in T.serial(0, 16):
C[((i * 16) + j)] = B_new[j] * 2.0
After = _transform()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_gpu():
"""Buffer flattening may have indices based on GPU thread vars"""
@I.ir_module(s_tir=True)
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((16, 16), "float32"), C: T.Buffer((16, 16), "float32")):
i0 = T.env_thread("blockIdx.x")
i1 = T.env_thread("threadIdx.x")
i2 = T.env_thread("vthread")
T.launch_thread(i0, 4)
T.launch_thread(i1, 2)
T.launch_thread(i2, 2)
B = T.decl_buffer([1, 16], "float32", scope="local")
for j in range(0, 16):
B[0, j] = A[i0 * 4 + i1 * 2 + i2, j] + 1.0
for j in range(0, 16):
C[i0 * 4 + i1 * 2 + i2, j] = B[0, j] * 2.0
@I.ir_module(s_tir=True)
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((16, 16), "float32"), C: T.Buffer((16, 16), "float32")):
A_1 = T.decl_buffer(256, dtype="float32", data=A.data)
C_1 = T.decl_buffer(256, dtype="float32", data=C.data)
i0 = T.env_thread("blockIdx.x")
i1 = T.env_thread("threadIdx.x")
i2 = T.env_thread("vthread")
T.launch_thread(i0, 4)
T.launch_thread(i1, 2)
T.launch_thread(i2, 2)
B = T.decl_buffer([16], "float32", scope="local")
for j in range(0, 16):
B[j] = A_1[i0 * 64 + i1 * 32 + i2 * 16 + j] + 1.0
for j in range(0, 16):
C_1[i0 * 64 + i1 * 32 + i2 * 16 + j] = B[j] * 2.0
After = _transform()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_symbolic():
"""Dynamically-sized arrrays are flattened"""
@I.ir_module(s_tir=True)
class Before:
@T.prim_func(s_tir=True)
def main(a: T.handle, c: T.handle, n: T.int32, m: T.int32) -> None:
A = T.match_buffer(a, (n, m), "float32")
C = T.match_buffer(c, (n, m), "float32")
for i in range(0, n):
B = T.decl_buffer([m], "float32")
for j in range(0, m):
B[j] = A[i, j] + 1.0
for j in range(0, m):
C[i, j] = B[j] * 2.0
@I.ir_module(s_tir=True)
class Expected:
@T.prim_func(s_tir=True)
def main(a: T.handle, c: T.handle, n: T.int32, m: T.int32) -> None:
A = T.match_buffer(a, (n, m), "float32")
C = T.match_buffer(c, (n, m), "float32")
A_1 = T.decl_buffer(n * m, "float32", data=A.data)
C_1 = T.decl_buffer(n * m, "float32", data=C.data)
for i in range(0, n):
B = T.decl_buffer([m], "float32")
for j in range(0, m):
B[j] = A_1[i * m + j] + 1.0
for j in range(0, m):
C_1[i * m + j] = B[j] * 2.0
After = _transform()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_fused_symbolic():
"""Dynamically-sized arrrays with fused iterator which can be flattened"""
@I.ir_module(s_tir=True)
class Before:
@T.prim_func(s_tir=True)
def main(a: T.handle, b: T.handle, n: T.int32) -> None:
A = T.match_buffer(a, (32, n, n), "float32")
B = T.match_buffer(b, (32, n, n), "float32")
for i in range(0, n * n * 32):
B[i // (n * n), (i % (n * n)) // n, i % n] = A[
i // (n * n), (i % (n * n)) // n, i % n
]
@I.ir_module(s_tir=True)
class Expected:
@T.prim_func(s_tir=True)
def main(a: T.handle, b: T.handle, n: T.int32) -> None:
input_A = T.match_buffer(a, (32, n, n), "float32")
input_B = T.match_buffer(b, (32, n, n), "float32")
A = T.decl_buffer(n * n * 32, "float32", data=input_A.data)
B = T.decl_buffer(n * n * 32, "float32", data=input_B.data)
for i in range(0, n * n * 32):
B[i] = A[i]
After = _transform()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_fused_symbolic_with_predicate():
"""Dynamically-sized arrrays with fused iterator which can be flattened with extra predicate"""
@I.ir_module(s_tir=True)
class Before:
@T.prim_func(s_tir=True)
def main(a: T.handle, b: T.handle, n: T.int32) -> None:
A = T.match_buffer(a, (32, n, n), "float32")
B = T.match_buffer(b, (32, n, n), "float32")
for bx, tx in T.grid((n * n + 1) // 2, 64):
if bx * 64 + tx < n * n * 32:
B[
(bx * 64 + tx) // (n * n),
((bx * 64 + tx) % (n * n)) // n,
(bx * 64 + tx) % n,
] = A[
(bx * 64 + tx) // (n * n),
((bx * 64 + tx) % (n * n)) // n,
(bx * 64 + tx) % n,
]
@I.ir_module(s_tir=True)
class Expected:
@T.prim_func(s_tir=True)
def main(a: T.handle, b: T.handle, n: T.int32) -> None:
input_A = T.match_buffer(a, (32, n, n), "float32")
input_B = T.match_buffer(b, (32, n, n), "float32")
A = T.decl_buffer(n * n * 32, "float32", data=input_A.data)
B = T.decl_buffer(n * n * 32, "float32", data=input_B.data)
for bx, tx in T.grid((n * n + 1) // 2, 64):
if bx * 64 + tx < n * n * 32:
B[bx * 64 + tx] = A[bx * 64 + tx]
After = _transform()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_multi_alloc():
"""If multiple allocations occur, all are flattened."""
@I.ir_module(s_tir=True)
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((4, 32), "float32"), D: T.Buffer((4, 32), "float32")):
for i, j in T.grid(4, 32):
B = T.decl_buffer((4, 32), "float32", scope="global")
C = T.decl_buffer((4, 32), "float32", scope="global")
B[i, j] = A[i, j] + 1.0
C[i, j] = A[i, j] + B[i, j]
D[i, j] = C[i, j] * 2.0
@I.ir_module(s_tir=True)
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((4, 32), "float32"), D: T.Buffer((4, 32), "float32")):
A_1 = T.decl_buffer(128, "float32", data=A.data)
D_1 = T.decl_buffer(128, "float32", data=D.data)
for i, j in T.grid(4, 32):
B = T.decl_buffer([128], "float32")
C = T.decl_buffer([128], "float32")
B[i * 32 + j] = A_1[i * 32 + j] + 1.0
C[i * 32 + j] = A_1[i * 32 + j] + B[i * 32 + j]
D_1[i * 32 + j] = C[i * 32 + j] * 2.0
After = _transform()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_strided():
"""Indices for flattened buffers use the specified striding."""
@I.ir_module(s_tir=True)
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((16, 16), "float32"), C: T.Buffer((16, 16), "float32")):
for i0 in T.serial(4):
B = T.decl_buffer([4, 17], "float32")
B_1 = T.decl_buffer([4, 16], dtype="float32", data=B.data, strides=[17, 1])
for i1, j in T.grid(4, 16):
B_1[i1, j] = A[i0 * 4 + i1, j] + 1.0
for i1, j in T.grid(4, 16):
C[i0 * 4 + i1, j] = B_1[i1, j] * 2.0
@I.ir_module(s_tir=True)
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((16, 16), "float32"), C: T.Buffer((16, 16), "float32")):
A_1 = T.decl_buffer(256, dtype="float32", data=A.data)
C_1 = T.decl_buffer(256, dtype="float32", data=C.data)
for i0 in T.serial(0, 4):
B = T.decl_buffer([68], "float32")
B_1 = T.decl_buffer([68], "float32", data=B.data)
for i1 in T.serial(0, 4):
for j in T.serial(0, 16):
B_1[i1 * 17 + j] = A_1[i0 * 64 + i1 * 16 + j] + 1.0
for i1 in T.serial(0, 4):
for j in T.serial(0, 16):
C_1[i0 * 64 + i1 * 16 + j] = B_1[i1 * 17 + j] * 2.0
After = _transform()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_boolean():
"""Boolean buffers are flattened but kept as bool (no int8 backing array)"""
@I.ir_module(s_tir=True)
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(10, "bool"), B: T.Buffer(10, "bool")) -> None:
for i0 in T.serial(10):
B[i0] = A[i0]
@I.ir_module(s_tir=True)
class Expected:
@T.prim_func(s_tir=True)
def main(input_A: T.Buffer(10, "bool"), input_B: T.Buffer(10, "bool")) -> None:
A = T.decl_buffer(10, dtype="bool", data=input_A.data)
B = T.decl_buffer(10, dtype="bool", data=input_B.data)
# body
for i0 in T.serial(10):
B[i0] = A[i0]
After = _transform()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_flatten_inside_block():
"""Flattening access inside a block flattens the accessed region."""
@I.ir_module(s_tir=True)
class Before:
@T.prim_func(s_tir=True)
def main():
A = T.sblock_alloc_buffer([32, 32])
for i, j in T.grid(32, 32):
with T.sblock("block"):
T.reads(A[i, j])
T.evaluate(A[i, j])
@I.ir_module(s_tir=True)
class Expected:
@T.prim_func(s_tir=True)
def main():
A = T.sblock_alloc_buffer([1024])
for i, j in T.grid(32, 32):
with T.sblock("block"):
T.reads(A[i * 32 + j])
T.evaluate(A[i * 32 + j])
After = _transform()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_no_change_to_2d_physical_buffer():
"""Flattening preserves axis separators."""
@I.ir_module(s_tir=True)
class Before:
@T.prim_func(s_tir=True)
def main():
A = T.sblock_alloc_buffer([32, 32], axis_separators=[1])
for i, j in T.grid(32, 32):
T.evaluate(A[i, j])
Expected = Before
After = _transform()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_flatten_alloc_buffer_with_axis_separators():
"""Flattening preserves axis separators"""
@I.ir_module(s_tir=True)
class Before:
@T.prim_func(s_tir=True)
def main():
A = T.sblock_alloc_buffer([2, 3, 5, 7, 11, 13], axis_separators=[3])
for i0, i1, i2, i3, i4, i5 in T.grid(2, 3, 5, 7, 11, 13):
T.evaluate(A[i0, i1, i2, i3, i4, i5])
@I.ir_module(s_tir=True)
class Expected:
@T.prim_func(s_tir=True)
def main():
A = T.sblock_alloc_buffer([30, 1001], axis_separators=[1])
for i0, i1, i2, i3, i4, i5 in T.grid(2, 3, 5, 7, 11, 13):
T.evaluate(A[i0 * 15 + i1 * 5 + i2, i3 * 143 + i4 * 13 + i5])
After = _transform()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_flatten_decl_buffer_with_axis_separators():
"""Flattening preserves axis separators
Like test_flatten_alloc_buffer_with_axis_separators, but the allocations
is done using Allocate/DeclBuffer, rather than through
BlockNode::alloc_buffers.
"""
@I.ir_module(s_tir=True)
class Before:
@T.prim_func(s_tir=True)
def main():
A = T.decl_buffer([2, 3, 5, 7, 11, 13], axis_separators=[3])
for i0, i1, i2, i3, i4, i5 in T.grid(2, 3, 5, 7, 11, 13):
T.evaluate(A[i0, i1, i2, i3, i4, i5])
@I.ir_module(s_tir=True)
class Expected:
@T.prim_func(s_tir=True)
def main():
A = T.decl_buffer([30, 1001], axis_separators=[1])
for i0, i1, i2, i3, i4, i5 in T.grid(2, 3, 5, 7, 11, 13):
T.evaluate(A[i0 * 15 + i1 * 5 + i2, i3 * 143 + i4 * 13 + i5])
After = _transform()(Before)
tvm.ir.assert_structural_equal(After, Expected)
if __name__ == "__main__":
tvm.testing.main()
@@ -0,0 +1,311 @@
# 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: F811, F841
import pytest
import tvm
import tvm.testing
from tvm.script import tirx as T
def test_thread_axis1():
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer((T.int64(64),), "float32"), B: T.Buffer((T.int64(64),), "float32")):
blockIdx_x = T.env_thread("blockIdx.x")
T.launch_thread(blockIdx_x, T.int64(2))
threadIdx_x = T.env_thread("threadIdx.x")
T.launch_thread(threadIdx_x, T.int64(32))
B[T.Cast("int64", blockIdx_x) * T.int64(32) + T.Cast("int64", threadIdx_x)] = A[
T.Cast("int64", blockIdx_x) * T.int64(32) + T.Cast("int64", threadIdx_x)
] + T.float32(1)
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer((64,), "float32"), B: T.Buffer((64,), "float32")):
blockIdx_x = T.env_thread("blockIdx.x")
T.launch_thread(blockIdx_x, 2)
threadIdx_x = T.env_thread("threadIdx.x")
T.launch_thread(threadIdx_x, 32)
B[blockIdx_x * 32 + threadIdx_x] = A[blockIdx_x * 32 + threadIdx_x] + T.float32(1)
mod = tvm.IRModule.from_expr(before)
func = tvm.tirx.transform.ForceNarrowIndexToInt32()(mod)["main"]
tvm.ir.assert_structural_equal(func, expected)
def test_thread_axis2():
@T.prim_func(s_tir=True)
def before(
T_reshape: T.Buffer((1, 12, 384, 384), "float32"),
placeholder_1: T.Buffer((T.int64(1), T.int64(12), T.int64(384), 384), "bool"),
T_where: T.Buffer((T.int64(1), T.int64(12), T.int64(384), 384), "float32"),
) -> None:
T.func_attr({"global_symbol": "main", "tirx.noalias": True})
for i0_i1_i2_i3_fused_1 in T.thread_binding(T.int64(256), thread="blockIdx.x"):
for i0_i1_i2_i3_fused_2 in T.thread_binding(T.int64(1024), thread="threadIdx.x"):
for i0_i1_i2_i3_fused_0 in T.serial(T.int64(7)):
with T.sblock("T_where"):
ax0 = T.axis.spatial(T.int64(1), T.int64(0))
ax1 = T.axis.spatial(
T.int64(12),
(
(i0_i1_i2_i3_fused_0 * T.int64(256) + i0_i1_i2_i3_fused_1)
* T.int64(1024)
+ i0_i1_i2_i3_fused_2
)
% T.int64(1769472)
// T.int64(147456),
)
ax2 = T.axis.spatial(
T.int64(384),
(
(i0_i1_i2_i3_fused_0 * T.int64(256) + i0_i1_i2_i3_fused_1)
* T.int64(1024)
+ i0_i1_i2_i3_fused_2
)
% T.int64(147456)
// T.int64(384),
)
ax3 = T.axis.spatial(
384,
T.cast(
(
(i0_i1_i2_i3_fused_0 * T.int64(256) + i0_i1_i2_i3_fused_1)
* T.int64(1024)
+ i0_i1_i2_i3_fused_2
)
% T.int64(384),
"int32",
),
)
T.where(
(i0_i1_i2_i3_fused_0 * T.int64(256) + i0_i1_i2_i3_fused_1)
* T.int64(1024)
+ i0_i1_i2_i3_fused_2
< T.int64(1769472)
)
T.reads(placeholder_1[ax0, ax1, ax2, ax3], T_reshape[ax0, ax1, ax2, ax3])
T.writes(T_where[ax0, ax1, ax2, ax3])
T_where[ax0, ax1, ax2, ax3] = T.Select(
T.cast(placeholder_1[ax0, ax1, ax2, ax3], "int32") != 0,
T.float32(-1000000000),
T_reshape[ax0, ax1, ax2, ax3],
)
@T.prim_func(s_tir=True)
def expected(
T_reshape: T.Buffer((1, 12, 384, 384), "float32"),
placeholder_1: T.Buffer((1, 12, 384, 384), "bool"),
T_where: T.Buffer((1, 12, 384, 384), "float32"),
):
T.func_attr({"global_symbol": "main", "tirx.noalias": True})
for i0_i1_i2_i3_fused_1 in T.thread_binding(256, thread="blockIdx.x"):
for i0_i1_i2_i3_fused_2 in T.thread_binding(1024, thread="threadIdx.x"):
for i0_i1_i2_i3_fused_0 in range(7):
with T.sblock("T_where"):
ax0 = T.axis.spatial(1, 0)
ax1 = T.axis.spatial(
12,
(
(i0_i1_i2_i3_fused_0 * 256 + i0_i1_i2_i3_fused_1) * 1024
+ i0_i1_i2_i3_fused_2
)
% 1769472
// 147456,
)
ax2 = T.axis.spatial(
384,
(
(i0_i1_i2_i3_fused_0 * 256 + i0_i1_i2_i3_fused_1) * 1024
+ i0_i1_i2_i3_fused_2
)
% 147456
// 384,
)
ax3 = T.axis.spatial(
384,
(
(i0_i1_i2_i3_fused_0 * 256 + i0_i1_i2_i3_fused_1) * 1024
+ i0_i1_i2_i3_fused_2
)
% 384,
)
T.where(
(i0_i1_i2_i3_fused_0 * 256 + i0_i1_i2_i3_fused_1) * 1024
+ i0_i1_i2_i3_fused_2
< 1769472
)
T.reads(placeholder_1[ax0, ax1, ax2, ax3], T_reshape[ax0, ax1, ax2, ax3])
T.writes(T_where[ax0, ax1, ax2, ax3])
T_where[ax0, ax1, ax2, ax3] = T.Select(
T.Cast("int32", placeholder_1[ax0, ax1, ax2, ax3]) != 0,
T.float32(-1000000000),
T_reshape[ax0, ax1, ax2, ax3],
)
mod = tvm.IRModule.from_expr(before)
func = tvm.tirx.transform.ForceNarrowIndexToInt32()(mod)["main"]
tvm.ir.assert_structural_equal(func, expected)
def test_block():
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer((128,), "float32"), B: T.Buffer((128,), "float32")):
for i in T.serial(0, T.int64(16)):
for j in T.serial(0, T.int64(8)):
with T.sblock():
vi = T.axis.spatial(T.int64(128), i * T.int64(8) + j)
B[vi] = A[vi] + T.float32(1)
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer((128,), "float32"), B: T.Buffer((128,), "float32")):
for i in T.serial(0, T.int32(16)):
for j in T.serial(0, T.int32(8)):
with T.sblock():
vi = T.axis.spatial(T.int32(128), i * T.int32(8) + j)
B[vi] = A[vi] + T.float32(1)
mod = tvm.IRModule.from_expr(before)
func = tvm.tirx.transform.ForceNarrowIndexToInt32()(mod)["main"]
tvm.ir.assert_structural_equal(func, expected)
def test_i16_buffer():
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer((128,), "int16"), B: T.Buffer((128,), "int16")):
for i in T.serial(0, T.int64(16)):
for j in T.serial(0, T.int64(16)):
with T.sblock():
vi = T.axis.spatial(T.int64(128), i * 8 + j)
B[vi] = A[vi] + T.int16(1)
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer((128,), "int16"), B: T.Buffer((128,), "int16")):
for i in T.serial(0, 16):
for j in T.serial(0, 16):
with T.sblock():
vi = T.axis.spatial(128, i * 8 + j)
B[vi] = A[vi] + T.int16(1)
mod = tvm.IRModule.from_expr(before)
after = tvm.tirx.transform.ForceNarrowIndexToInt32()(mod)["main"]
tvm.ir.assert_structural_equal(after, expected)
def test_fail_on_buffer_map():
@T.prim_func(private=True, s_tir=True)
def func(A: T.Buffer((128,), "int64"), B: T.Buffer((128,), "int64")):
for i in T.serial(0, 16):
for j in T.serial(0, 8):
with T.sblock():
vi = T.axis.spatial(128, i * 8 + j)
B[vi] = A[vi] + T.int64(1)
mod = tvm.IRModule.from_expr(func)
with pytest.raises(RuntimeError):
tvm.tirx.transform.ForceNarrowIndexToInt32()(mod)["main"]
def test_fail_on_buffer_map():
@T.prim_func(private=True, s_tir=True)
def func(A: T.Buffer((128,), "int32"), B: T.Buffer((128,), "int32")):
C = T.sblock_alloc_buffer((128,), "int64")
for i in T.serial(0, 16):
for j in T.serial(0, 8):
with T.sblock():
vi = T.axis.spatial(128, i * 8 + j)
C[vi] = T.cast(A[vi], "int64") + T.int64(1)
for i in T.serial(0, 16):
for j in T.serial(0, 8):
with T.sblock():
vi = T.axis.spatial(128, i * 8 + j)
B[vi] = T.cast(C[vi] + T.int64(1), "int32")
mod = tvm.IRModule.from_expr(func)
with pytest.raises(RuntimeError):
tvm.tirx.transform.ForceNarrowIndexToInt32()(mod)["main"]
def test_pod_params_and_select():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(
A: T.Buffer((T.int64(4),), "float32"), B: T.Buffer((T.int64(4),), "float32"), n: T.int64
):
for i in T.serial(T.int64(4)):
B[i] = T.Select(T.int64(1) <= i, A[i + n], T.Cast("float32", i))
@tvm.script.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((4,), "float32"), B: T.Buffer((4,), "float32"), n: T.int32):
for i in range(4):
B[i] = T.Select(1 <= i, A[i + n], T.Cast("float32", i))
after = tvm.tirx.transform.ForceNarrowIndexToInt32()(Before)
tvm.ir.assert_structural_equal(Expected, after)
def test_clz():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(B: T.Buffer((T.int64(4),), "int32")):
for i in T.serial(T.int64(4)):
B[i] = T.clz(i)
@tvm.script.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(B: T.Buffer((4,), "int32")):
for i in range(4):
B[i] = T.clz(i) - 32 + 64
after = tvm.tirx.transform.ForceNarrowIndexToInt32()(Before)
tvm.ir.assert_structural_equal(Expected, after)
def test_let_binding():
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(buf: T.handle):
n = T.int64()
Buf = T.match_buffer(buf, [n], "int32")
ceil_log2 = T.Cast("int64", T.ceil(T.log2(T.Cast("float32", n))))
for i in T.serial(ceil_log2):
T.evaluate(0)
@tvm.script.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(buf: T.handle):
n = T.int32()
Buf = T.match_buffer(buf, [n], "int32")
# The pass narrows indexing variables (n, the For extent) but leaves
# an explicitly-typed `T.Cast("int64", ...)` storage alone; a Cast to
# int32 is inserted at the use site (the For iter) instead.
ceil_log2 = T.Cast("int64", T.ceil(T.log2(T.Cast("float32", n))))
for i in range(T.Cast("int32", ceil_log2)):
T.evaluate(0)
after = tvm.tirx.transform.ForceNarrowIndexToInt32()(Before)
tvm.ir.assert_structural_equal(Expected, after)
if __name__ == "__main__":
tvm.testing.main()
@@ -0,0 +1,228 @@
# 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.
import tvm
import tvm.script
import tvm.testing
from tvm.script import tirx as T
from tvm.target import Target
from tvm.tirx.transform.transform import BindTarget
# pylint: disable=no-member,invalid-name,unused-variable
def get_before(dtype: str):
@tvm.script.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(Aptr: T.handle(dtype), Bptr: T.handle(dtype), Dptr: T.handle(dtype)):
T.func_attr({"global_symbol": "main"})
A = T.decl_buffer((100,), dtype, data=Aptr)
B = T.decl_buffer((100,), dtype, data=Bptr)
D = T.decl_buffer((100,), dtype, data=Dptr)
C = T.decl_buffer((100,), dtype)
for i in T.grid(100):
C[i] = A[i] + B[i]
D[i] = T.exp(C[i])
return Before
def promote_f8(f8_dtype: str, promote_dtype: str, v):
return promote_uint8(f8_dtype, promote_dtype, T.reinterpret("uint8", v))
def cast_to_f8(f8_dtype: str, promote_dtype: str, v):
return T.reinterpret(f8_dtype, cast_to_uint8(f8_dtype, promote_dtype, v))
def get_after_compute_legalize(dtype: str, promote_dtype: str):
@tvm.script.ir_module
class After:
@T.prim_func(s_tir=True)
def main(Aptr: T.handle(dtype), Bptr: T.handle(dtype), Dptr: T.handle(dtype)):
T.func_attr({"global_symbol": "main"})
A = T.decl_buffer((100,), dtype, data=Aptr)
B = T.decl_buffer((100,), dtype, data=Bptr)
D = T.decl_buffer((100,), dtype, data=Dptr)
C = T.decl_buffer((100,), promote_dtype)
for i in T.grid(100):
C[i] = promote_f8(dtype, promote_dtype, A[i]) + promote_f8(
dtype, promote_dtype, B[i]
)
D[i] = cast_to_f8(dtype, promote_dtype, T.exp(C[i]))
return After
def promote_uint8(f8_dtype: str, promote_dtype: str, v):
if f8_dtype == "float8_e4m3fn":
if promote_dtype == "float16":
mantissa = T.bitwise_and(
T.shift_left(T.Cast("uint16", v), T.uint16(7)), T.uint16(0x3FF)
)
exponent = T.shift_left(
T.Cast(
"uint16",
T.shift_right(T.shift_left(v, T.uint8(1)), T.uint8(4)) + T.uint8(8),
),
T.uint16(10),
)
sign = T.shift_left(T.Cast("uint16", T.shift_right(v, T.uint8(7))), T.uint16(15))
return T.reinterpret("float16", T.bitwise_or(T.bitwise_or(mantissa, exponent), sign))
else: # promote_dtype == "float32"
mantissa = T.bitwise_and(
T.shift_left(T.Cast("uint32", v), T.uint32(20)), T.uint32(0x7FFFFF)
)
exponent = T.shift_left(
T.Cast(
"uint32",
T.shift_right(T.shift_left(v, T.uint8(1)), T.uint8(4)) + T.uint8(120),
),
T.uint32(23),
)
sign = T.shift_left(T.Cast("uint32", T.shift_right(v, T.uint8(7))), T.uint32(31))
return T.reinterpret("float32", T.bitwise_or(T.bitwise_or(mantissa, exponent), sign))
else: # f8_dtype == "float8_e5m2"
if promote_dtype == "float16":
return T.reinterpret("float16", T.shift_left(T.Cast("uint16", v), T.uint16(8)))
else: # promote_dtype == "float32"
mantissa = T.bitwise_and(
T.shift_left(T.Cast("uint32", v), T.uint32(21)), T.uint32(0x7FFFFF)
)
exponent = T.shift_left(
T.Cast(
"uint32",
T.shift_right(T.shift_left(v, T.uint8(1)), T.uint8(3)) + T.uint8(112),
),
T.uint32(23),
)
sign = T.shift_left(T.Cast("uint32", T.shift_right(v, T.uint8(7))), T.uint32(31))
return T.reinterpret("float32", T.bitwise_or(T.bitwise_or(mantissa, exponent), sign))
def cast_to_uint8(f8_dtype: str, promote_dtype: str, v):
if f8_dtype == "float8_e4m3fn":
if promote_dtype == "float16":
uint16_v = T.reinterpret("uint16", v)
rounding_bias = T.bitwise_and(
T.shift_right(uint16_v, T.uint16(7)),
T.uint16(1),
) + T.uint16(0x3F)
uint16_v = uint16_v + rounding_bias
mantissa = T.bitwise_and(
T.Cast("uint8", T.shift_right(uint16_v, T.uint8(7))), T.uint8(0x7)
)
exponent_before_delta = T.shift_right(T.shift_left(uint16_v, T.uint16(1)), T.uint16(11))
round_to_zero = exponent_before_delta < T.uint16(8)
exponent = T.shift_left(
T.Cast("uint8", exponent_before_delta - T.uint16(8)),
T.uint8(3),
)
sign = T.shift_left(T.Cast("uint8", T.shift_right(uint16_v, T.uint16(15))), T.uint8(7))
return T.if_then_else(
round_to_zero, T.uint8(0), T.bitwise_or(T.bitwise_or(mantissa, exponent), sign)
)
else: # promote_dtype == "float32"
uint32_v = T.reinterpret("uint32", v)
rounding_bias = T.bitwise_and(
T.shift_right(uint32_v, T.uint32(20)), T.uint32(1)
) + T.uint32(0x7FFFF)
uint32_v = uint32_v + rounding_bias
mantissa = T.bitwise_and(
T.Cast("uint8", T.shift_right(uint32_v, T.uint8(20))), T.uint8(0x7)
)
exponent_before_delta = T.shift_right(T.shift_left(uint32_v, T.uint32(1)), T.uint32(24))
round_to_zero = exponent_before_delta < T.uint32(120)
exponent = T.shift_left(
T.Cast("uint8", exponent_before_delta - T.uint32(120)), T.uint8(3)
)
sign = T.shift_left(T.Cast("uint8", T.shift_right(uint32_v, T.uint32(31))), T.uint8(7))
return T.if_then_else(
round_to_zero, T.uint8(0), T.bitwise_or(T.bitwise_or(mantissa, exponent), sign)
)
else: # f8_dtype == "float8_e5m2"
if promote_dtype == "float16":
uint16_v = T.reinterpret("uint16", v)
rounding_bias = T.bitwise_and(
T.shift_right(uint16_v, T.uint16(8)), T.uint16(1)
) + T.uint16(0x7F)
uint16_v = uint16_v + rounding_bias
return T.Cast("uint8", T.shift_right(uint16_v, T.uint16(8)))
else: # promote_dtype == "float32"
uint32_v = T.reinterpret("uint32", v)
rounding_bias = T.bitwise_and(
T.shift_right(uint32_v, T.uint32(21)), T.uint32(1)
) + T.uint32(0xFFFFF)
uint32_v = uint32_v + rounding_bias
mantissa = T.bitwise_and(
T.Cast("uint8", T.shift_right(uint32_v, T.uint8(21))), T.uint8(0x3)
)
exponent_before_delta = T.shift_right(T.shift_left(uint32_v, T.uint32(1)), T.uint32(24))
round_to_zero = exponent_before_delta < T.uint32(112)
exponent = T.shift_left(
T.Cast("uint8", exponent_before_delta - T.uint32(112)), T.uint8(2)
)
sign = T.shift_left(T.Cast("uint8", T.shift_right(uint32_v, T.uint32(31))), T.uint8(7))
return T.if_then_else(
round_to_zero, T.uint8(0), T.bitwise_or(T.bitwise_or(mantissa, exponent), sign)
)
def get_after_storage_legalize(dtype: str, promote_dtype: str):
@tvm.script.ir_module
class After:
@T.prim_func(s_tir=True)
def main(Aptr: T.handle("uint8"), Bptr: T.handle("uint8"), Dptr: T.handle("uint8")):
T.func_attr({"global_symbol": "main"})
A = T.decl_buffer((100,), "uint8", data=Aptr)
B = T.decl_buffer((100,), "uint8", data=Bptr)
D = T.decl_buffer((100,), "uint8", data=Dptr)
C = T.decl_buffer((100,), promote_dtype)
for i in T.grid(100):
C[i] = promote_uint8(dtype, promote_dtype, A[i]) + promote_uint8(
dtype, promote_dtype, B[i]
)
D[i] = cast_to_uint8(dtype, promote_dtype, T.exp(C[i]))
return After
dtype = tvm.testing.parameter("float8_e4m3fn", "float8_e5m2")
promote_dtype = tvm.testing.parameter("float16", "float32")
def test_fp8_compute_legalize(dtype, promote_dtype):
target = Target("nvidia/nvidia-a100")
before = BindTarget(target)(get_before(dtype))
expected = BindTarget(target)(get_after_compute_legalize(dtype, promote_dtype))
# run the transform twice to ensure we can afford to deal
# with this repeative optimizations
after = tvm.tirx.transform.FP8ComputeLegalize(promote_dtype)(before)
after = tvm.tirx.transform.FP8ComputeLegalize(promote_dtype)(after)
tvm.ir.assert_structural_equal(after, expected)
def test_fp8_storage_legalize(dtype, promote_dtype):
target = Target("nvidia/nvidia-a100")
before = BindTarget(target)(get_after_compute_legalize(dtype, promote_dtype))
after = tvm.tirx.transform.FP8StorageLegalize()(before)
expected = BindTarget(target)(get_after_storage_legalize(dtype, promote_dtype))
tvm.ir.assert_structural_equal(after, expected)
if __name__ == "__main__":
tvm.testing.main()
@@ -0,0 +1,345 @@
# 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: F841
import pytest
import tvm
import tvm.testing
from tvm.script import ir as I
from tvm.script import tirx as T
def test_annotate_entry_func_single_primfunc():
@tvm.script.ir_module
class MockModule:
@T.prim_func(private=True, s_tir=True)
def func1(A: T.Buffer((16,), "float32")):
for i in T.serial(16):
if i == 5:
if i == 5:
A[i] = 0.0
mod = MockModule
assert mod
assert "tirx.is_entry_func" not in (mod["func1"].attrs or {})
after = tvm.tirx.transform.AnnotateEntryFunc()(mod)
assert (
after["func1"].attrs
and "tirx.is_entry_func" in after["func1"].attrs
and after["func1"].attrs["tirx.is_entry_func"]
)
# Test module
@tvm.script.ir_module
class MockModule:
@T.prim_func(private=True, s_tir=True)
def func1(A: T.Buffer((16,), "float32")):
for i in T.serial(16):
if i == 5:
if i == 5:
A[i] = 0.0
@T.prim_func(private=True, s_tir=True)
def func2(A: T.Buffer((32,), "float32")):
for i in T.serial(32):
if i == 15:
if i == 15:
A[i] = 0.0
@pytest.mark.xfail
def test_annotate_entry_func_multiple_primfunc():
mod = MockModule
assert mod
assert "target" not in (mod["func1"].attrs or {})
assert "target" not in (mod["func2"].attrs or {})
# This should fail
after = tvm.tirx.transform.AnnotateEntryFunc()(mod)
def test_bind_target():
mod = MockModule
assert mod
target = tvm.target.Target("cuda")
assert "target" not in (mod["func1"].attrs or {})
assert "target" not in (mod["func2"].attrs or {})
after = tvm.tirx.transform.BindTarget(target)(mod)
assert "target" in after["func1"].attrs
assert after["func1"].attrs["target"] == target
assert "target" in after["func2"].attrs
assert after["func2"].attrs["target"] == target
def test_bind_target_adds_attribute():
"""BindTarget adds the "target" attribute"""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main():
T.evaluate(0)
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main():
T.func_attr({"target": T.target("cuda")})
T.evaluate(0)
After = tvm.tirx.transform.BindTarget(tvm.target.Target("cuda"))(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_bind_target_with_host_to_exposed_function():
"""BindTarget adds the host target to externally-exposed functions"""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main():
T.func_attr({"global_symbol": "main"})
T.evaluate(0)
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main():
T.func_attr({"global_symbol": "main", "target": T.target("cuda", host="llvm")})
T.evaluate(0)
After = tvm.tirx.transform.BindTarget(tvm.target.Target("cuda", host="llvm"))(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_bind_target_with_host_to_internal_function():
"""Internal functions have a target annotation, but without the host
The host portion of the target annotation provides host
parameters, and is used to expose a function externally as part of
`MakePackedAPI` and `MakeUnpackedAPI`. For internal functions, no
external exposure is required, so the host attribute should not be
used.
"""
@I.ir_module
class Before:
@T.prim_func(private=True, s_tir=True)
def main():
T.evaluate(0)
@I.ir_module
class Expected:
@T.prim_func(private=True, s_tir=True)
def main():
T.func_attr({"target": T.target("cuda")})
T.evaluate(0)
After = tvm.tirx.transform.BindTarget(tvm.target.Target("cuda", host="llvm"))(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_bind_target_ignores_existing():
"""BindTarget should not replace existing annotations"""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main():
T.func_attr({"target": T.target("nvptx")})
T.evaluate(0)
Expected = Before
After = tvm.tirx.transform.BindTarget(tvm.target.Target("cuda"))(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_bind_target_updates_host():
"""BindTarget should update host for existing annotations"""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main():
T.func_attr({"global_symbol": "func", "target": T.target("nvptx")})
T.evaluate(0)
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main():
T.func_attr(
{
"global_symbol": "func",
"target": T.target("nvptx", host={"kind": "llvm", "opt-level": 0}),
}
)
T.evaluate(0)
After = tvm.tirx.transform.BindTarget(
tvm.target.Target("cuda", host={"kind": "llvm", "opt-level": 0})
)(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_bind_target_multiple_functions():
"""BindTarget may apply to multiple functions in a module"""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def func1():
T.evaluate(0)
@T.prim_func(s_tir=True)
def func2():
T.evaluate(0)
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def func1():
T.func_attr({"target": T.target("cuda")})
T.evaluate(0)
@T.prim_func(s_tir=True)
def func2():
T.func_attr({"target": T.target("cuda")})
T.evaluate(0)
After = tvm.tirx.transform.BindTarget(tvm.target.Target("cuda"))(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_bind_target_with_device_host_call_same_func():
"""BindTarget should bind the device target to the function if it is called from device"""
@I.ir_module
class Before:
@T.prim_func(private=True, s_tir=True)
def add(a: T.int32, b: T.int32) -> T.int32:
return a + b
@T.prim_func(s_tir=True)
def main(
A: T.Buffer((128, 128), "int32"),
B: T.Buffer((128, 128), "int32"),
C: T.Buffer((128, 128), "int32"),
):
T.func_attr({"global_symbol": "main"})
length: T.let[T.int32] = Before.add(64, 64) # Call from host
for bx in T.thread_binding(length, "blockIdx.x"):
for tx in T.thread_binding(length, "threadIdx.x"):
C[bx, tx] = Before.add(A[bx, tx], B[bx, tx]) # Call from device
@I.ir_module
class Expected:
@T.prim_func(private=True, s_tir=True)
def add(a: T.int32, b: T.int32) -> T.int32:
T.func_attr({"target": T.target("cuda")})
return a + b
@T.prim_func(private=True, s_tir=True)
def add_host(a: T.int32, b: T.int32) -> T.int32:
T.func_attr({"target": T.target({"kind": "llvm", "opt-level": 0})})
return a + b
@T.prim_func(s_tir=True)
def main(
A: T.Buffer((128, 128), "int32"),
B: T.Buffer((128, 128), "int32"),
C: T.Buffer((128, 128), "int32"),
):
T.func_attr(
{
"global_symbol": "main",
"target": T.target("cuda", host={"kind": "llvm", "opt-level": 0}),
}
)
length: T.let[T.int32] = Expected.add_host(64, 64) # Call from host
for bx in T.thread_binding(length, "blockIdx.x"):
for tx in T.thread_binding(length, "threadIdx.x"):
C[bx, tx] = Expected.add(A[bx, tx], B[bx, tx]) # Call from device
After = tvm.tirx.transform.BindTarget(
tvm.target.Target("cuda", host={"kind": "llvm", "opt-level": 0})
)(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_filter_primfunc():
mod = MockModule
assert mod
# Annotate each function for testing
mod["func1"] = mod["func1"].with_attr("temp", "test1")
mod["func2"] = mod["func2"].with_attr("temp", "test2")
# Test condition that does not filter out anything
def checker_filter_out_none(func: tvm.tirx.PrimFunc):
return "temp" in func.attrs
after = tvm.tirx.transform.Filter(checker_filter_out_none)(mod)
assert len(after.functions) == 2
# Filtered functions should satisfy the given condition.
assert checker_filter_out_none(after["func1"])
assert checker_filter_out_none(after["func2"])
# Test condition that selectively filters out primfuncs
def checker_filter_out_one(func: tvm.tirx.PrimFunc):
return ("temp" in func.attrs) and func.attrs["temp"] == "test1"
after = tvm.tirx.transform.Filter(checker_filter_out_one)(mod)
assert len(after.functions) == 1
# Filtered functions should satisfy the given condition.
assert checker_filter_out_one(after["func1"])
# Test condition that filters out everything
def checker_filter_out_both(func: tvm.tirx.PrimFunc):
return "invalid_attr" in func.attrs
after = tvm.tirx.transform.Filter(checker_filter_out_both)(mod)
assert len(after.functions) == 0
def test_filter_removes_global_var_map():
"""Filtering out a function should be identical to never adding it
This test is to guard against hidden state in the IRModule that
remains after filtering. Previously, this was observed in the
`IRModuleNode::global_var_map_`, which retained entries of
filtered-out functions.
"""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def func():
T.evaluate(0)
@I.ir_module
class Expected:
pass
After = tvm.tirx.transform.Filter(lambda prim_func: False)(Before)
tvm.ir.assert_structural_equal(After, Expected)
if __name__ == "__main__":
tvm.testing.main()
@@ -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.
# ruff: noqa: RUF005
import numpy as np
import pytest
import tvm
import tvm.testing
from tvm.testing import env
def lower_intrin(params, stmt):
"""wrapper to call transformation in stmt"""
lower_expr = tvm.ir.is_prim_expr(stmt)
stmt = tvm.tirx.Evaluate(stmt) if lower_expr else stmt
mod = tvm.IRModule.from_expr(
tvm.tirx.PrimFunc(params, stmt).with_attr("target", tvm.target.Target("llvm"))
)
mod = tvm.transform.Sequential(
[tvm.tirx.transform.StmtSimplify(), tvm.tirx.transform.LowerIntrin()]
)(mod)
func = mod["main"]
stmt = func.body
return stmt.value if lower_expr else stmt.body
def check_value(expr, variables, data, fref):
"""
Check that expr evaluates to fref(*row) for each row in data.
variables: list of TIR vars [x] or [x, y] bound to the columns of data.
data: list of tuples, each tuple has len(variables) elements.
"""
n = len(data)
num_vars = len(variables)
assert num_vars >= 1 and all(len(row) == num_vars for row in data)
# Build input and output buffers
input_bufs = [
tvm.tirx.decl_buffer((n,), dtype=variables[i].ty, name=f"v{i}") for i in range(num_vars)
]
out_buf = tvm.tirx.decl_buffer((n,), dtype=expr.ty, name="C")
# Build loop body: for each i, bind variables[j] = input_bufs[j][i], then store expr to out
loop_var = tvm.tirx.Var("i", "int32")
def make_store(i_var):
# Build the expression with each variable bound to the corresponding buffer load
result = expr
for j in range(num_vars - 1, -1, -1):
result = tvm.tirx.Let(variables[j], tvm.tirx.BufferLoad(input_bufs[j], [i_var]), result)
return tvm.tirx.BufferStore(out_buf, result, [i_var])
loop = tvm.tirx.For(
loop_var,
tvm.tirx.const(0, "int32"),
tvm.tirx.const(n, "int32"),
tvm.tirx.ForKind.SERIAL,
make_store(loop_var),
)
prim_func = tvm.tirx.PrimFunc(input_bufs + [out_buf], loop)
prim_func = prim_func.with_attr({"tirx.noalias": True, "global_symbol": "main"})
f = tvm.compile(prim_func, "llvm")
arrays = [
tvm.runtime.tensor(np.array([row[j] for row in data], dtype=str(variables[j].ty)))
for j in range(num_vars)
]
c = tvm.runtime.tensor(np.zeros(n, dtype=str(expr.ty)))
f(*arrays, c)
cref = np.array([fref(*row) for row in data])
np.testing.assert_equal(c.numpy(), cref)
def test_lower_nested_access_ptr():
data = tvm.tirx.Var("data", tvm.ir.PointerType(tvm.ir.PrimType("float32")))
inner = tvm.tirx.tvm_access_ptr("float32", data, 2, 16, 1)
outer = tvm.tirx.tvm_access_ptr("float32", inner, 3, 8, 1)
body = tvm.tirx.Evaluate(tvm.tirx.call_extern("void", "consume", outer))
mod = tvm.IRModule.from_expr(
tvm.tirx.PrimFunc([data], body).with_attr("target", tvm.target.Target("llvm"))
)
lowered = tvm.tirx.transform.LowerIntrin()(mod)["main"]
access_ptr_calls = []
address_calls = []
def collect(node):
if isinstance(node, tvm.ir.Call):
if node.op.name == "tirx.tvm_access_ptr":
access_ptr_calls.append(node)
elif node.op.name == "tirx.address_of":
address_calls.append(node)
tvm.tirx.stmt_functor.post_order_visit(lowered.body, collect)
assert not access_ptr_calls
assert len(address_calls) == 1
load = address_calls[0].args[0]
assert isinstance(load, tvm.tirx.BufferLoad)
assert int(tvm.arith.Analyzer().simplify(load.indices[0])) == 5
def get_ref_data():
"""Get reference data for every pairs"""
import itertools
x = range(-10, 10)
y = list(range(-10, 10))
y.remove(0)
return list(itertools.product(x, y))
@pytest.mark.skipif(not env.has_llvm(), reason="need llvm")
def test_lower_floordiv():
data = get_ref_data()
for dtype in ["int32", "int64", "int16"]:
x = tvm.tirx.Var("x", dtype)
y = tvm.tirx.Var("y", dtype)
zero = tvm.tirx.const(0, dtype)
# no constraints
res = lower_intrin([x, y], tvm.tirx.floordiv(x, y))
check_value(res, [x, y], data, lambda a, b: a // b)
# rhs >= 0
res = lower_intrin([x, y], tvm.tirx.Select(y >= 0, tvm.tirx.floordiv(x, y), zero))
check_value(res, [x, y], data, lambda a, b: a // b if b > 0 else 0)
# involves max
res = lower_intrin(
[x, y], tvm.tirx.Select(y >= 0, tvm.tirx.max(tvm.tirx.floordiv(x, y), zero), zero)
)
check_value(res, [x, y], data, lambda a, b: max(a // b, 0) if b > 0 else 0)
# lhs >= 0
res = lower_intrin(
[x, y], tvm.tirx.Select(tvm.tirx.all(y >= 0, x >= 0), tvm.tirx.floordiv(x, y), zero)
)
check_value(res, [x, y], data, lambda a, b: a // b if b > 0 and a >= 0 else 0)
# const power of two
res = lower_intrin([x, y], tvm.tirx.floordiv(x, tvm.tirx.const(8, dtype=dtype)))
check_value(res, [x, y], [(a, b) for a, b in data if b == 8], lambda a, b: a // b)
# floordiv(x + m, k), m and k are positive constant. 2 <= m <= k-1.
res = lower_intrin(
[x, y],
tvm.tirx.floordiv(x + tvm.tirx.const(4, dtype=dtype), tvm.tirx.const(5, dtype=dtype)),
)
check_value(res, [x, y], [(a, b) for a, b in data if b == 5], lambda a, b: (a + 4) // b)
@pytest.mark.skipif(not env.has_llvm(), reason="need llvm")
def test_lower_floormod():
data = get_ref_data()
for dtype in ["int32", "int64", "int16"]:
x = tvm.tirx.Var("x", dtype)
y = tvm.tirx.Var("y", dtype)
zero = tvm.tirx.const(0, dtype)
# no constraints
res = lower_intrin([x, y], tvm.tirx.floormod(x, y))
check_value(res, [x, y], data, lambda a, b: a % b)
# rhs >= 0
res = lower_intrin([x, y], tvm.tirx.Select(y >= 0, tvm.tirx.floormod(x, y), zero))
check_value(res, [x, y], data, lambda a, b: a % b if b > 0 else 0)
# lhs >= 0
res = lower_intrin(
[x, y], tvm.tirx.Select(tvm.tirx.all(y >= 0, x >= 0), tvm.tirx.floormod(x, y), zero)
)
check_value(res, [x, y], data, lambda a, b: a % b if b > 0 and a >= 0 else 0)
# const power of two
res = lower_intrin([x, y], tvm.tirx.floormod(x, tvm.tirx.const(8, dtype=dtype)))
check_value(res, [x, y], [(a, b) for a, b in data if b == 8], lambda a, b: a % b)
# floormod(x + m, k), m and k are positive constant. 2 <= m <= k-1.
res = lower_intrin(
[x, y],
tvm.tirx.floormod(x + tvm.tirx.const(4, dtype=dtype), tvm.tirx.const(5, dtype=dtype)),
)
check_value(res, [x, y], [(a, b) for a, b in data if b == 5], lambda a, b: (a + 4) % b)
@pytest.mark.skipif(not env.has_llvm(), reason="need llvm")
def test_lower_floordiv_overflow_checks():
"""
Regression tests for overflow checks in TryFindShiftCoefficientForPositiveRange.
Divisor is constant 3 (not 1 to avoid CSE, not power-of-two so we don't take the shift path).
Reuses lower_intrin and check_value; overflow tests use one var [x].
"""
# Check 3: (b-1) - a_min must not overflow (numerator and C++ int64).
# x (int64) full range -> min_value = -2^63. With b = 3: numerator = 2 - (-2^63) > LLONG_MAX.
x = tvm.tirx.Var("x", "int64")
res = lower_intrin([x], tvm.tirx.floordiv(x, tvm.tirx.const(3, "int64")))
data_check3 = [(-(2**63),), (0,), (100,)]
check_value(res, [x], data_check3, lambda a: a // 3)
# Check 4: c_value * b_value must not overflow dtype.
# x (int16) full range -> min_value = -32768, c = ceil(32770/3) = 10923; 10923*3 > 32767.
x = tvm.tirx.Var("x", "int16")
res = lower_intrin([x], tvm.tirx.floordiv(x, tvm.tirx.const(3, "int16")))
data_check4 = [(-32768,), (0,), (100,)]
check_value(res, [x], data_check4, lambda a: a // 3)
# Check 5: a_max + b*c must not overflow (offset numerator).
# tirx.min(tirx.max(x, -10), 32758) can give bounds [-10, 32758]; b=3, c=4; a_max + 12 > 32767.
# In practice this path may not be triggered. This test still validates correct lowering.
x = tvm.tirx.Var("x", "int16")
clamped = tvm.tirx.min(
tvm.tirx.max(x, tvm.tirx.const(-10, "int16")), tvm.tirx.const(32758, "int16")
)
res = lower_intrin([x], tvm.tirx.floordiv(clamped, tvm.tirx.const(3, "int16")))
data_check5 = [(-10,), (0,), (32758,), (32757,)]
check_value(res, [x], data_check5, lambda a: (min(max(a, -10), 32758)) // 3)
if __name__ == "__main__":
test_lower_floordiv()
test_lower_floormod()
test_lower_floordiv_overflow_checks()
@@ -0,0 +1,327 @@
# 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.
import numpy as np
import pytest
import tvm
import tvm.testing
from tvm.script import ir as I
from tvm.script import tirx as T
from tvm.testing import env
@tvm.register_global_func("tvm.test_matmul")
def my_matmul(a, b, c):
c.copyfrom(np.dot(a.numpy(), b.numpy()))
def test_lower_call_packed():
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(
A: T.Buffer((64, 64), "float32"),
B: T.Buffer((64, 64), "float32"),
C: T.Buffer((64, 64), "float32"),
):
T.func_attr({"target": tvm.target.Target("llvm")})
T.attr("", "device_id", T.int32(0))
T.call_packed("tvm.test_matmul", A, B, C)
@I.ir_module(check_well_formed=False)
class Expected:
@T.prim_func(s_tir=True)
def main(
A: T.Buffer((64, 64), "float32"),
B: T.Buffer((64, 64), "float32"),
C: T.Buffer((64, 64), "float32"),
):
T.func_attr({"target": tvm.target.Target("llvm")})
stack_ffi_any: T.let[T.handle] = T.tvm_stack_alloca("tvm_ffi_any", 4)
stack_array: T.let[T.handle] = T.tvm_stack_alloca("array", 3)
stack_shape: T.let[T.handle("int64")] = T.tvm_stack_alloca("shape", 6)
stack_shape_1 = T.decl_buffer((T.int64(6),), "int64", data=stack_shape)
stack_shape_1[0] = T.int64(64)
stack_shape_1[1] = T.int64(64)
T.tvm_struct_set(stack_array, 0, 1, A.data)
stack_shape_2 = T.Buffer((1,), "int64", data=stack_shape)
T.tvm_struct_set(stack_array, 0, 2, T.address_of(stack_shape_2[0]))
T.tvm_struct_set(stack_array, 0, 3, T.reinterpret("handle", T.uint64(0)))
T.tvm_struct_set(stack_array, 0, 4, 2)
T.tvm_struct_set(stack_array, 0, 5, T.uint8(2))
T.tvm_struct_set(stack_array, 0, 6, T.uint8(32))
T.tvm_struct_set(stack_array, 0, 7, T.uint16(1))
T.tvm_struct_set(stack_array, 0, 8, T.uint64(0))
T.tvm_struct_set(stack_array, 0, 9, 0)
T.tvm_struct_set(stack_array, 0, 10, 1)
stack_shape_1[2] = T.int64(64)
stack_shape_1[3] = T.int64(64)
T.tvm_struct_set(stack_array, 1, 1, B.data)
stack_shape_3 = T.Buffer((3,), "int64", data=stack_shape)
T.tvm_struct_set(stack_array, 1, 2, T.address_of(stack_shape_3[2]))
T.tvm_struct_set(stack_array, 1, 3, T.reinterpret("handle", T.uint64(0)))
T.tvm_struct_set(stack_array, 1, 4, 2)
T.tvm_struct_set(stack_array, 1, 5, T.uint8(2))
T.tvm_struct_set(stack_array, 1, 6, T.uint8(32))
T.tvm_struct_set(stack_array, 1, 7, T.uint16(1))
T.tvm_struct_set(stack_array, 1, 8, T.uint64(0))
T.tvm_struct_set(stack_array, 1, 9, 0)
T.tvm_struct_set(stack_array, 1, 10, 1)
stack_shape_1[4] = T.int64(64)
stack_shape_1[5] = T.int64(64)
T.tvm_struct_set(stack_array, 2, 1, C.data)
stack_shape_4 = T.Buffer((5,), "int64", data=stack_shape)
T.tvm_struct_set(stack_array, 2, 2, T.address_of(stack_shape_4[4]))
T.tvm_struct_set(stack_array, 2, 3, T.reinterpret("handle", T.uint64(0)))
T.tvm_struct_set(stack_array, 2, 4, 2)
T.tvm_struct_set(stack_array, 2, 5, T.uint8(2))
T.tvm_struct_set(stack_array, 2, 6, T.uint8(32))
T.tvm_struct_set(stack_array, 2, 7, T.uint16(1))
T.tvm_struct_set(stack_array, 2, 8, T.uint64(0))
T.tvm_struct_set(stack_array, 2, 9, 0)
T.tvm_struct_set(stack_array, 2, 10, 1)
T.tvm_struct_set(stack_ffi_any, 0, 13, 7)
T.tvm_struct_set(stack_ffi_any, 0, 14, 0)
T.tvm_struct_set(stack_ffi_any, 0, 15, T.tvm_struct_get(stack_array, 0, 0, "handle"))
T.tvm_struct_set(stack_ffi_any, 1, 13, 7)
T.tvm_struct_set(stack_ffi_any, 1, 14, 0)
T.tvm_struct_set(stack_ffi_any, 1, 15, T.tvm_struct_get(stack_array, 1, 0, "handle"))
T.tvm_struct_set(stack_ffi_any, 2, 13, 7)
T.tvm_struct_set(stack_ffi_any, 2, 14, 0)
T.tvm_struct_set(stack_ffi_any, 2, 15, T.tvm_struct_get(stack_array, 2, 0, "handle"))
T.tvm_struct_set(stack_ffi_any, 3, 13, 0)
T.tvm_struct_set(stack_ffi_any, 3, 14, 0)
T.tvm_struct_set(stack_ffi_any, 3, 15, T.int64(0))
T.call_packed_lowered("tvm.test_matmul", stack_ffi_any, 0, 3)
After = tvm.tirx.transform.LowerTVMBuiltin()(Before)
tvm.ir.assert_structural_equal(After, Expected)
@pytest.mark.skipif(not env.has_llvm(), reason="need llvm")
def test_lower_call_packed_raw_string():
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main():
T.func_attr({"target": tvm.target.Target("llvm")})
T.call_packed("testing.echo", "payload")
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main():
T.func_attr({"target": tvm.target.Target("llvm")})
stack_ffi_any: T.let[T.handle] = T.tvm_stack_alloca("tvm_ffi_any", 2)
T.tvm_struct_set(stack_ffi_any, 0, 13, 8)
T.tvm_struct_set(stack_ffi_any, 0, 14, 0)
T.tvm_struct_set(stack_ffi_any, 0, 15, T.reinterpret(T.handle().ty, "payload"))
T.tvm_struct_set(stack_ffi_any, 1, 13, 0)
T.tvm_struct_set(stack_ffi_any, 1, 14, 0)
T.tvm_struct_set(stack_ffi_any, 1, 15, T.int64(0))
T.call_packed_lowered("testing.echo", stack_ffi_any, 0, 1)
After = tvm.tirx.transform.LowerTVMBuiltin()(Before)
tvm.ir.assert_structural_equal(After, Expected)
# The typed pointer is required by the LLVM TVMFFIAny lowering.
tvm.compile(Before, target="llvm")
@pytest.mark.skipif(not env.has_llvm(), reason="need llvm")
def test_call_packed_return_non_i32():
# This call packed that return non i32 types
expected_value = np.array([1.2, 1.4], dtype="float32")
def packed_echo(value):
return tvm.tirx.call_intrin(
value.ty, tvm.ir.Op.get("tirx.tvm_call_packed"), "testing.echo", value
)
def build_tir():
Ab = tvm.tirx.decl_buffer((2,), "float32")
# Build statements using direct TIR construction (no ir_builder)
# 1. Store packed_echo(const) result into Ab[0]
store0 = tvm.tirx.BufferStore(
Ab, packed_echo(tvm.tirx.const(expected_value[0], "float32")), [0]
)
# 2. Let binding: Aptr_dup = packed_echo(Ab.data), then store const into Ab[1]
Aptr_dup = tvm.tirx.Var("Aptr_dup", Ab.data.ty)
store1 = tvm.tirx.BufferStore(Ab, tvm.tirx.const(expected_value[1], "float32"), [1])
bind_stmt = tvm.tirx.Bind(Aptr_dup, packed_echo(Ab.data))
# Combine into sequence
stmt = tvm.tirx.SeqStmt([store0, bind_stmt, store1])
return tvm.IRModule.from_expr(
tvm.tirx.PrimFunc([Ab], stmt).with_attr("global_symbol", "packed_test")
)
mod = build_tir()
f = tvm.compile(mod, None)
a = tvm.runtime.tensor(np.zeros(2, dtype="float32"))
f(a)
tvm.testing.assert_allclose(a.numpy(), expected_value)
def test_lower_overflow_int32():
@T.prim_func(check_well_formed=False, s_tir=True)
def variance4(rxplaceholder: T.Buffer((T.int64(1), T.int64(32), T.int64(25690112)), "float32")):
T.func_attr({"global_symbol": "variance4", "tirx.noalias": True})
rxplaceholder_red = T.alloc_buffer((32,), "float32")
T_subtract = T.alloc_buffer((822083584,), "float32")
rxplaceholder_red_1 = T.Buffer((T.int64(32),), data=rxplaceholder_red.data)
rxplaceholder_1 = T.Buffer((T.int64(822083584),), data=rxplaceholder.data)
T_subtract_1 = T.Buffer((T.int64(822083584),), data=T_subtract.data)
for ax1, ax2 in T.grid(32, 25690112):
cse_v1: T.let[T.int32] = ax1 * 25690112 + ax2
T_subtract_1[cse_v1] = rxplaceholder_1[cse_v1] - rxplaceholder_red_1[ax1]
func = variance4
tvm.compile(func, target="llvm") # should not crash
def test_lower_device_allocate():
"""Device allocations are lowered to TVMBackend* calls
This test validates the current behavior of LowerTVMBuiltin. This
unit test may be improved in the future by addressing:
- TVMScript always produces "handle" dtype for
`T.tvm_throw_last_error`, while LowerTVMBuiltin outputs "int32"
dtype.
"""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main():
T.func_attr({"target": T.target("llvm")})
T.attr("dummy", "device_type", 2) # kDLCuda
T.attr("dummy", "device_id", 0)
ptr = T.alloc_buffer((16,), "float32")
buf = T.decl_buffer(16, "float32", data=ptr.data)
buf[0] = 0.0
After = tvm.tirx.transform.LowerTVMBuiltin()(Before)
# Verify the lowered module can be printed (no crash)
script_output = After.script()
# Should contain TVMBackendAllocWorkspace and TVMBackendFreeWorkspace
assert "TVMBackendAllocWorkspace" in script_output
assert "TVMBackendFreeWorkspace" in script_output
# DeclBuffer should appear as a flat statement
assert "T.decl_buffer" in script_output
def test_lower_cpu_allocation():
"""CPU allocations can be handled at codegen time"""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main():
T.func_attr({"target": T.target("llvm")})
T.attr("dummy", "device_type", 1) # kDLCPU
T.attr("dummy", "device_id", 0)
ptr = T.alloc_buffer((16,), "float32")
buf = T.decl_buffer(16, "float32", data=ptr.data)
buf[0] = 0.0
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main():
T.func_attr({"target": T.target("llvm")})
ptr = T.alloc_buffer((16,), "float32")
buf = T.decl_buffer(16, "float32", data=ptr.data)
buf[0] = 0.0
After = tvm.tirx.transform.LowerTVMBuiltin()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_lower_allocate_requires_device_id():
"""If device id is missing, error."""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main():
T.func_attr({"target": T.target("llvm")})
T.attr("dummy", "device_type", 2) # kDLCuda
ptr = T.alloc_buffer((16,), "float32")
buf = T.decl_buffer(16, "float32", data=ptr.data)
buf[0] = 0.0
with pytest.raises(RuntimeError):
tvm.tirx.transform.LowerTVMBuiltin()(Before)
def test_lower_allocate_requires_device_type():
"""If device type is missing, error.
The device type can be inferred either from the `"device_type"`
statement attribute, or from the `"target"` function attribute.
Here, we provide neither. The `"tirx.is_host_func"` attribute is
provided as otherwise the function would be skipped altogether by
LowerTVMBuiltin.
"""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main():
T.func_attr({"tirx.is_host_func": True})
T.attr("dummy", "device_id", 0)
ptr = T.alloc_buffer((1024 * 1024,), "float32")
buf = T.decl_buffer(1024 * 1024, "float32", data=ptr.data)
buf[0] = 0.0
with pytest.raises(RuntimeError):
tvm.tirx.transform.LowerTVMBuiltin()(Before)
def test_lower_cpu_alloc_with_function_attr():
"""CPU allocations can be handled at codegen time
Like `test_lower_cpu_allocation`, but the device type is taken from
the function attribute. The `AttrStmt` can override the device
type for allocations within its scope, but it defaults to the
function's target.
"""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main():
T.func_attr({"target": T.target("llvm")})
ptr = T.alloc_buffer((16,), "float32")
buf = T.decl_buffer(16, "float32", data=ptr.data)
buf[0] = 0.0
# Expected is same as before for this transform
Expected = Before
After = tvm.tirx.transform.LowerTVMBuiltin()(Before)
tvm.ir.assert_structural_equal(After, Expected)
if __name__ == "__main__":
tvm.testing.main()
@@ -0,0 +1,453 @@
# 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.
"""Tests for tirx.transform.MakePackedAPI TIR transform.
Tests verify the transform output using TVMScript before/after patterns.
Runtime error tests are in tests/python/codegen/test_codegen_error_handling.py.
"""
import pytest
import tvm
import tvm.testing
from tvm import tirx
from tvm.script import ir as I
from tvm.script import tirx as T
def _find_compute_scope(func):
result = None
def _visitor(stmt):
if isinstance(stmt, tirx.AttrStmt) and stmt.attr_key == "compute_scope":
nonlocal result
result = stmt
tirx.stmt_functor.post_order_visit(func.body, _visitor)
return result
@pytest.mark.parametrize("use_global_symbol", [True, False])
def test_no_op_when_global_symbol_is_absent(use_global_symbol):
func_attr = {"target": tvm.target.Target("llvm", host="llvm")}
@T.prim_func(private=True, s_tir=True)
def before():
T.func_attr(func_attr)
T.evaluate(0)
if use_global_symbol:
before = before.with_attr("global_symbol", "main")
after = tvm.tirx.transform.MakePackedAPI()(tvm.IRModule.from_expr(before))["main"]
if use_global_symbol:
assert len(after.params) == 4
else:
tvm.ir.assert_structural_equal(before, after)
def test_target_host_removed():
"""After MakePackedAPI, host-side target should be the host
MakePackedAPI is the last transform that requires both the device
and the host. After MakePackedAPI, the target attribute should
only contain the host-side target.
"""
host = tvm.target.Target("llvm")
@I.ir_module
class before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(1, "float32")):
T.func_attr({"global_symbol": "main", "target": T.target("cuda", host=host)})
T.evaluate(0)
after = tvm.tirx.transform.MakePackedAPI()(before)
target_attr = after["main"].attrs["target"]
assert str(host) == str(target_attr)
def test_internal_subroutine_call():
"""Internal subroutines should not use the PackedFunc API
A subroutine without the "global_symbol" attribute is an internal
subroutine, and is not directly exposed to a user of the generated
`runtime.Module`. Therefore, it doesn't need to follow the
PackedFunc API.
"""
@I.ir_module
class before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(1, "float32")):
T.func_attr({"target": T.target("llvm", host="llvm")})
before.subroutine(A.data)
# this test fails if it's made public
@T.prim_func(private=True, s_tir=True)
def subroutine(A_data: T.handle("float32")):
T.func_attr({"target": T.target("llvm")})
T.evaluate(A_data)
after = tvm.tirx.transform.MakePackedAPI()(before)
tvm.ir.assert_structural_equal(before["subroutine"], after["subroutine"])
compute_scope = _find_compute_scope(after["main"])
subroutine_call_op = compute_scope.body.value.op
assert isinstance(subroutine_call_op, tvm.ir.GlobalVar), (
f"The main function's CallNode should use the subroutine's GlobalVar as the operation, "
f"but instead has an operation of type {subroutine_call_op}"
)
def test_subroutine_call_to_externally_visible_subroutine():
"""Externally-visible subroutines should use the PackedFunc API
Because the subroutine may be called directly by a user, it must
use the PackedFunc API. Its signature should be updated to the
PackedFunc signature, and call sites should be updated to use
`T.tvm_call_cpacked`.
"""
@I.ir_module
class before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(1, "float32")):
T.func_attr({"global_symbol": "main", "target": T.target("llvm", host="llvm")})
before.subroutine(A.data)
@T.prim_func(s_tir=True)
def subroutine(A_data: T.handle("float32")):
T.func_attr({"global_symbol": "subroutine", "target": T.target("llvm", host="llvm")})
T.evaluate(A_data)
after = tvm.tirx.transform.MakePackedAPI()(before)
main_compute_scope = _find_compute_scope(after["main"])
assert main_compute_scope is not None
subroutine_compute_scope = _find_compute_scope(after["subroutine"])
assert subroutine_compute_scope is not None
subroutine_call_op = main_compute_scope.body.value.op
assert (
isinstance(subroutine_call_op, tvm.ir.Op)
and subroutine_call_op.name == "tirx.tvm_call_cpacked"
), (
f"The main function's CallNode should be lowered to the builtin 'tirx.tvm_call_cpacked', "
f"but instead has an operation of type {subroutine_call_op}"
)
def test_zero_arg_function():
"""Zero-arg function emits num_args check but no null-pointer check."""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def func_without_arg() -> T.int64:
T.func_attr({"target": T.target("llvm", host="llvm")})
return T.int64(42)
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def func_without_arg(
self_handle: T.handle,
args: T.handle,
num_args: T.int32,
result: T.handle("void", "global"),
) -> T.int32:
T.func_attr(
{
"calling_conv": 1,
"global_symbol": "__tvm_ffi_func_without_arg",
"target": T.target("llvm"),
}
)
assert num_args == 0, (
"TypeError",
["Expected ", "0", " arguments", " when calling:\n `", "func_without_arg()", "`"],
)
with T.attr(0, "compute_scope", "func_without_arg_compute_"):
T.tvm_struct_set(result, 0, 13, 1)
T.tvm_struct_set(result, 0, 14, 0)
T.tvm_struct_set(result, 0, 15, T.Cast("int64", T.int64(42)))
return 0
return 0
After = tvm.tirx.transform.MakePackedAPI()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_pointer_return():
"""Pointer returns are stored as an opaque pointer in TVMFFIAny."""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(arg: T.handle) -> T.handle:
T.func_attr({"target": T.target("llvm", host="llvm")})
return arg
after = tvm.tirx.transform.MakePackedAPI()(Before)["main"]
return_type_indices = []
def collect(node):
if not isinstance(node, tvm.ir.Call) or node.op.name != "tirx.tvm_struct_set":
return
field = node.args[2]
if isinstance(field, tvm.tirx.IntImm) and int(field) == 13:
return_type_indices.append(int(node.args[3]))
tvm.tirx.stmt_functor.post_order_visit(after.body, collect)
assert 4 in return_type_indices # ffi::TypeIndex::kTVMFFIOpaquePtr
def test_int_parameter():
"""Int parameter emits type check accepting int or bool."""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(arg: T.int32) -> T.int32:
T.func_attr({"target": T.target("llvm", host="llvm")})
if arg > 0:
return 10
else:
return 20
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(
self_handle: T.handle,
args: T.handle,
num_args: T.int32,
result: T.handle("void", "global"),
) -> T.int32:
T.func_attr(
{
"calling_conv": 1,
"global_symbol": "__tvm_ffi_main",
"target": T.target("llvm"),
}
)
assert num_args == 1, (
"TypeError",
["Expected ", "1", " arguments", " when calling:\n `", "main(arg: int32)", "`"],
)
assert not T.isnullptr(args), (
"TypeError",
["args pointer is NULL", " when calling:\n `", "main(arg: int32)", "`"],
)
arg_type_index: T.let[T.int32] = T.tvm_struct_get(args, 0, 13, "int32")
assert arg_type_index == 1 or arg_type_index == 2, (
"TypeError",
[
"Mismatched type on argument #",
"0",
" when calling:\n `",
"main(arg: int32)",
"`,\n expected ",
"int",
],
)
arg: T.let[T.int32] = T.Cast("int32", T.tvm_struct_get(args, 0, 15, "int64"))
with T.attr(0, "compute_scope", "main_compute_"):
if arg > 0:
T.tvm_struct_set(result, 0, 13, 1)
T.tvm_struct_set(result, 0, 14, 0)
T.tvm_struct_set(result, 0, 15, T.Cast("int64", 10))
return 0
else:
T.tvm_struct_set(result, 0, 13, 1)
T.tvm_struct_set(result, 0, 14, 0)
T.tvm_struct_set(result, 0, 15, T.Cast("int64", 20))
return 0
return 0
After = tvm.tirx.transform.MakePackedAPI()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_bool_parameter():
"""Bool parameter emits type check accepting bool or int."""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(arg: T.bool) -> T.int32:
T.func_attr({"target": T.target("llvm", host="llvm")})
if arg:
return 10
else:
return 20
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(
self_handle: T.handle,
args: T.handle,
num_args: T.int32,
result: T.handle("void", "global"),
) -> T.int32:
T.func_attr(
{
"calling_conv": 1,
"global_symbol": "__tvm_ffi_main",
"target": T.target("llvm"),
}
)
assert num_args == 1, (
"TypeError",
["Expected ", "1", " arguments", " when calling:\n `", "main(arg: bool)", "`"],
)
assert not T.isnullptr(args), (
"TypeError",
["args pointer is NULL", " when calling:\n `", "main(arg: bool)", "`"],
)
arg_type_index: T.let[T.int32] = T.tvm_struct_get(args, 0, 13, "int32")
assert arg_type_index == 2 or arg_type_index == 1, (
"TypeError",
[
"Mismatched type on argument #",
"0",
" when calling:\n `",
"main(arg: bool)",
"`,\n expected ",
"boolean",
],
)
arg: T.let[T.bool] = T.Cast("bool", T.tvm_struct_get(args, 0, 15, "int64"))
with T.attr(0, "compute_scope", "main_compute_"):
if arg:
T.tvm_struct_set(result, 0, 13, 1)
T.tvm_struct_set(result, 0, 14, 0)
T.tvm_struct_set(result, 0, 15, T.Cast("int64", 10))
return 0
else:
T.tvm_struct_set(result, 0, 13, 1)
T.tvm_struct_set(result, 0, 14, 0)
T.tvm_struct_set(result, 0, 15, T.Cast("int64", 20))
return 0
return 0
After = tvm.tirx.transform.MakePackedAPI()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_float_parameter():
"""Float parameter emits type check accepting float, int, or bool."""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(arg: T.float32) -> T.int32:
T.func_attr({"target": T.target("llvm", host="llvm")})
if arg > T.float32(0):
return 10
else:
return 20
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(
self_handle: T.handle,
args: T.handle,
num_args: T.int32,
result: T.handle("void", "global"),
) -> T.int32:
T.func_attr(
{
"calling_conv": 1,
"global_symbol": "__tvm_ffi_main",
"target": T.target("llvm"),
}
)
assert num_args == 1, (
"TypeError",
["Expected ", "1", " arguments", " when calling:\n `", "main(arg: float32)", "`"],
)
assert not T.isnullptr(args), (
"TypeError",
["args pointer is NULL", " when calling:\n `", "main(arg: float32)", "`"],
)
arg_type_index: T.let[T.int32] = T.tvm_struct_get(args, 0, 13, "int32")
assert arg_type_index == 3 or arg_type_index == 1 or arg_type_index == 2, (
"TypeError",
[
"Mismatched type on argument #",
"0",
" when calling:\n `",
"main(arg: float32)",
"`,\n expected ",
"float",
],
)
arg: T.let[T.float32] = T.Select(
arg_type_index == 3,
T.Cast("float32", T.tvm_struct_get(args, 0, 15, "float64")),
T.Cast("float32", T.tvm_struct_get(args, 0, 15, "int64")),
)
with T.attr(0, "compute_scope", "main_compute_"):
if arg > T.float32(0.0):
T.tvm_struct_set(result, 0, 13, 1)
T.tvm_struct_set(result, 0, 14, 0)
T.tvm_struct_set(result, 0, 15, T.Cast("int64", 10))
return 0
else:
T.tvm_struct_set(result, 0, 13, 1)
T.tvm_struct_set(result, 0, 14, 0)
T.tvm_struct_set(result, 0, 15, T.Cast("int64", 20))
return 0
return 0
After = tvm.tirx.transform.MakePackedAPI()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_forward_reference_symbolic_variable():
"""MakePackedAPI succeeds when a symbolic variable is used before it is defined.
When buffer A has shape (batch_size+1,) and buffer B has shape (batch_size,),
batch_size is referenced (in A's shape check) before it is defined (from B's
shape). The three-sequence separation (init_nest, asserts, decl_buffers)
ensures all variable definitions precede all assertions.
"""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(a: T.handle, b: T.handle):
T.func_attr({"target": T.target("llvm", host="llvm")})
batch_size = T.int64()
A = T.match_buffer(a, (batch_size + 1,), "int32")
B = T.match_buffer(b, (batch_size,), "int32")
for i in range(batch_size):
B[i] = A[i] + A[i + 1]
# Should not raise "variable batch_size has been used before definition"
After = tvm.tirx.transform.MakePackedAPI()(Before)
assert len(After["main"].params) == 4
if __name__ == "__main__":
tvm.testing.main()
@@ -0,0 +1,322 @@
# 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.
import tvm
import tvm.testing
from tvm.script import tirx as T
from tvm.tirx import const
def lower_stmt(params, stmt, target_bits):
func = tvm.tirx.PrimFunc(params, stmt)
func = tvm.tirx.transform.NarrowDataType(target_bits)(tvm.IRModule.from_expr(func))["main"]
stmt = func.body
return stmt
def lower_func_body(func, target_bits):
"""Lower a TVMScript function and return the first For loop in the body."""
mod = tvm.IRModule.from_expr(func)
gvar = next(iter(mod.functions.keys()))
func = tvm.tirx.transform.NarrowDataType(target_bits)(mod)[gvar]
body = func.body
# With flat buffer semantics, navigate to the first For node
if isinstance(body, tvm.tirx.SeqStmt):
for stmt in body:
if isinstance(stmt, tvm.tirx.For):
return stmt
while hasattr(body, "body") and not isinstance(body, tvm.tirx.For):
body = body.body
return body
def test_basic():
def check_const(m, n, target_bits, target_dtype):
"""Check with constant values using TVMScript closure."""
@T.prim_func(s_tir=True)
def func(A: T.Buffer((m * n,), "float32"), B: T.Buffer((m * n,), "float32")):
for i in T.serial(m):
for j in T.serial(n):
B[i * n + j] = A[i * n + j] + T.float32(1)
stmt = lower_func_body(func, target_bits)
assert stmt.loop_var.ty.dtype == target_dtype
assert stmt.body.loop_var.ty.dtype == target_dtype
def check_symbolic(m_dtype, n_dtype, target_bits, target_dtype):
"""Check with symbolic shapes as function parameters."""
if m_dtype == "int32":
@T.prim_func(s_tir=True)
def func(A: T.handle("float32"), B: T.handle("float32"), m: T.int32, n: T.int32):
A_buf = T.decl_buffer((m * n,), "float32", data=A)
B_buf = T.decl_buffer((m * n,), "float32", data=B)
for i in T.serial(m):
for j in T.serial(n):
B_buf[i * n + j] = A_buf[i * n + j] + T.float32(1)
else:
@T.prim_func(s_tir=True)
def func(A: T.handle("float32"), B: T.handle("float32"), m: T.int64, n: T.int64):
A_buf = T.decl_buffer((m * n,), "float32", data=A)
B_buf = T.decl_buffer((m * n,), "float32", data=B)
for i in T.serial(m):
for j in T.serial(n):
B_buf[i * n + j] = A_buf[i * n + j] + T.float32(1)
stmt = lower_func_body(func, target_bits)
assert stmt.loop_var.ty.dtype == target_dtype
assert stmt.body.loop_var.ty.dtype == target_dtype
# const shape
# i32 -> i32
check_const(2, 2, 32, "int32")
# i64 -> i32
check_const(const(2, dtype="int64"), const(2, dtype="int64"), 32, "int32")
check_const(const(2**16, dtype="int64"), const(2**16, dtype="int64"), 32, "int64")
# i32 -> i16
check_const(2, 2, 16, "int16")
check_const(2**10, 2**10, 16, "int32")
# symbolic shape
check_symbolic("int32", "int32", 32, "int32")
check_symbolic("int64", "int64", 32, "int64")
def test_thread_axis():
# This test uses launch_thread to create AttrStmt nodes with "thread_extent"
# and checks the dtype of thread axis variables after narrowing.
def check_const(m, n, target_bits, target_dtype):
@T.prim_func(s_tir=True)
def func(A: T.Buffer((m * n,), "float32"), B: T.Buffer((m * n,), "float32")):
bx = T.launch_thread("blockIdx.x", m)
tx = T.launch_thread("threadIdx.x", n)
B[bx * n + tx] = A[bx * n + tx] + T.float32(1)
mod = tvm.IRModule.from_expr(func)
gvar = next(iter(mod.functions.keys()))
func_narrowed = tvm.tirx.transform.NarrowDataType(target_bits)(mod)[gvar]
stmt = func_narrowed.body
assert stmt.node.var.ty.dtype == target_dtype
assert stmt.body.node.var.ty.dtype == target_dtype
# i32 -> i32
check_const(2, 32, target_bits=32, target_dtype="int32")
# i64 -> i32
check_const(
const(2, dtype="int64"), const(32, dtype="int64"), target_bits=32, target_dtype="int32"
)
check_const(
const(2**30, dtype="int64"),
const(32, dtype="int64"),
target_bits=32,
target_dtype="int64",
)
# i32 -> i16
check_const(2, 32, target_bits=16, target_dtype="int16")
check_const(2**14, 32, target_bits=16, target_dtype="int32")
def test_multilanes():
# Test narrowing with vector types. Uses closure capture for m and lanes.
def check(m, lanes, target_bits, target_dtype):
vec_dtype = f"float32x{lanes}"
@T.prim_func(s_tir=True)
def func(
A: T.Buffer((m,), vec_dtype),
B: T.Buffer((m,), vec_dtype),
):
for i in T.serial(m):
B[i] = A[i] + T.Broadcast(T.float32(1), lanes)
A[0] = B[1]
mod = tvm.IRModule.from_expr(func)
gvar = next(iter(mod.functions.keys()))
func_narrowed = tvm.tirx.transform.NarrowDataType(target_bits)(mod)[gvar]
stmt = func_narrowed.body
assert stmt.seq[0].loop_var.ty.dtype == target_dtype
# i32 -> i32
check(const(2**10, dtype="int32"), 2, target_bits=32, target_dtype="int32")
# i64 -> i32
check(const(2**10, dtype="int64"), 2, target_bits=32, target_dtype="int32")
check(const(2**32, dtype="int64"), 2, target_bits=32, target_dtype="int64")
# i32 -> i16
check(const(2**10, dtype="int32"), 2, target_bits=16, target_dtype="int16")
check(const(2**16, dtype="int32"), 2, target_bits=16, target_dtype="int32")
def test_slice():
# Test narrowing with slice indexing where buffer B has different index ranges.
def check(m, n, target_bits, target_dtype):
# The index may overflow in B, while not in A
@T.prim_func(s_tir=True)
def func(
A: T.Buffer((m * n,), "float32"),
B: T.Buffer((m * n * 2,), "float32"),
):
for i in T.serial(m):
for j in T.serial(n):
A[i * n + j] = B[i * 2 * n + 2 * j] + T.float32(1)
stmt = lower_func_body(func, target_bits)
assert stmt.loop_var.ty.dtype == target_dtype
assert stmt.body.loop_var.ty.dtype == target_dtype
# The maximum index is (2**15 * 2**15 - 1) * 2 <= 2**31 - 1
check(const(2**15, "int64"), const(2**15, "int64"), target_bits=32, target_dtype="int32")
# The maximum index is (2**15 * 2**15 - 1 + 2**15) * 2 > 2**31 - 1
check(const(2**15, "int64"), const((2**15 + 1), "int64"), target_bits=32, target_dtype="int64")
def test_condition():
@T.prim_func(s_tir=True)
def before(A: T.Buffer((128,), "float32"), B: T.Buffer((130,), "float32")):
for i, j in T.grid(T.int64(2), T.int64(65)):
if i * T.int64(65) + j >= T.int64(0) and i * T.int64(65) + j < T.int64(128):
A[i * T.int64(65) + j] = 0.0
for i, j in T.grid(T.int64(2), T.int64(65)):
B[i * T.int64(65) + j] = T.if_then_else(
i * T.int64(65) + j >= T.int64(0) and i * T.int64(65) + j < T.int64(128),
A[i * T.int64(65) + j],
0.0,
dtype="float32",
)
@T.prim_func(s_tir=True)
def expected_after(A: T.Buffer(128, "float32"), B: T.Buffer(130, "float32")):
for i, j in T.grid(2, 65):
if i * 65 + j >= 0 and i * 65 + j < 128:
A[i * 65 + j] = T.float32(0)
for i, j in T.grid(2, 65):
B[i * 65 + j] = T.if_then_else(
i * 65 + j >= 0 and i * 65 + j < 128, A[i * 65 + j], T.float32(0), dtype="float32"
)
after = tvm.tirx.transform.NarrowDataType(32)(
tvm.IRModule.from_expr(before.with_attr("global_symbol", "main"))
)["main"]
tvm.ir.assert_structural_equal(after, expected_after.with_attr("global_symbol", "main"))
def test_block():
@T.prim_func(s_tir=True)
def before(A: T.Buffer((128,), "float32"), B: T.Buffer((128,), "float32")):
for i in T.serial(0, T.int64(16)):
for j in T.serial(0, T.int64(8)):
with T.sblock():
vi = T.axis.spatial(T.int64(128), i * T.int64(8) + j)
B[vi] = A[vi] + T.float32(1)
@T.prim_func(s_tir=True)
def expected_after(A: T.Buffer((128,), "float32"), B: T.Buffer((128,), "float32")):
for i in T.serial(0, T.int32(16)):
for j in T.serial(0, T.int32(8)):
with T.sblock():
vi = T.axis.spatial(T.int32(128), i * T.int32(8) + j)
B[vi] = A[vi] + T.float32(1)
after = tvm.tirx.transform.NarrowDataType(32)(
tvm.IRModule.from_expr(before.with_attr("global_symbol", "main"))
)["main"]
tvm.ir.assert_structural_equal(after, expected_after.with_attr("global_symbol", "main"))
def test_avg_pool2d():
@T.prim_func(s_tir=True)
def before(PSUM: T.Buffer((313600,), "int32"), PAVG: T.Buffer((313600,), "int32")):
for j in T.parallel(T.int64(0), T.int64(280)):
for i in T.serial(T.int64(0), T.int64(35)):
for vi in T.vectorized(T.int64(0), T.int64(32)):
PAVG[(((j * T.int64(1120)) + (i * T.int64(32))) + vi)] = T.cast(
T.Div(
T.cast(PSUM[(((j * T.int64(1120)) + (i * T.int64(32))) + vi)], "int64"),
T.max(
(
(
(
T.min(
T.int64(1),
(T.int64(34) - T.floormod(j, T.int64(35))),
)
+ T.int64(2)
)
- T.max(
(T.int64(1) - T.floormod(j, T.int64(35))), T.int64(0)
)
)
* (
(T.min(T.int64(1), (T.int64(34) - i)) + T.int64(2))
- T.max((T.int64(1) - i), T.int64(0))
)
),
T.int64(1),
),
),
"int32",
)
@T.prim_func(s_tir=True)
def expected_after(PSUM: T.Buffer((313600,), "int32"), PAVG: T.Buffer((313600,), "int32")):
for j in T.parallel(T.int32(0), T.int32(280)):
for i in T.serial(T.int32(0), T.int32(35)):
for vi in T.vectorized(T.int32(0), T.int32(32)):
PAVG[(((j * T.int32(1120)) + (i * T.int32(32))) + vi)] = T.Div(
PSUM[(((j * T.int32(1120)) + (i * T.int32(32))) + vi)],
(
(
(
T.min(T.int32(1), (T.int32(34) - T.floormod(j, T.int32(35))))
+ T.int32(2)
)
- T.max((T.int32(1) - T.floormod(j, T.int32(35))), T.int32(0))
)
* (
(T.min(T.int32(1), (T.int32(34) - i)) + T.int32(2))
- T.max((T.int32(1) - i), T.int32(0))
)
),
)
after = tvm.tirx.transform.NarrowDataType(32)(
tvm.IRModule.from_expr(before.with_attr("global_symbol", "main"))
)
after = tvm.tirx.transform.StmtSimplify()(after)
tvm.ir.assert_structural_equal(after["main"], expected_after.with_attr("global_symbol", "main"))
def test_narrow_i64_valued_bufferload_index_to_i32():
@T.prim_func(s_tir=True)
def before(A: T.Buffer((16,), "int64")):
for i in range(T.int64(15)):
A[i + T.int64(1)] = A[i] + T.int64(1)
@T.prim_func(s_tir=True)
def expect(A: T.Buffer((16,), "int64")):
for i in range(15):
A[i + 1] = A[i] + T.int64(1)
after = tvm.tirx.transform.NarrowDataType(32)(
tvm.IRModule.from_expr(before.with_attr("global_symbol", "main"))
)["main"]
tvm.ir.assert_structural_equal(after, expect.with_attr("global_symbol", "main"))
if __name__ == "__main__":
tvm.testing.main()
@@ -0,0 +1,146 @@
# 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, missing-docstring
import tvm
import tvm.testing
from tvm.script import ir as I
from tvm.script import tirx as T
def test_rewrite_to_shuffle_0():
transform = tvm.tirx.transform.PointerValueTypeRewrite()
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((16,), "float32"), B: T.Buffer((4,), "float32")):
A_local = T.alloc_buffer((16,), scope="local")
for i in range(4):
A_local[i * 4 : i * 4 + 4] = A[i * 4 : i * 4 + 4]
for i in range(4):
B[i] = A_local[i * 4] + A_local[i * 4 + 1] + A_local[i * 4 + 2] + A_local[i * 4 + 3]
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((4,), "float32x4"), B: T.Buffer((4,), "float32")):
A_local = T.alloc_buffer((4,), "float32x4", scope="local")
for i in range(4):
A_local[T.Div(i * 4, 4)] = A[T.Div(i * 4, 4)]
for i in range(4):
B[i] = (
T.Shuffle([A_local[T.Div(i * 4, 4)]], [0])
+ T.Shuffle([A_local[T.Div(i * 4 + 1, 4)]], [1])
+ T.Shuffle([A_local[T.Div(i * 4 + 2, 4)]], [2])
+ T.Shuffle([A_local[T.Div(i * 4 + 3, 4)]], [3])
)
After = transform(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_rewrite_to_shuffle_1():
transform = tvm.tirx.transform.PointerValueTypeRewrite()
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((8,), "float32"), B: T.Buffer((1,), "float32")):
A_local = T.alloc_buffer((8,), scope="local")
A_local[0:4] = A[0:4]
A_local[4:8] = A[4:8]
B[0] = (
A_local[0]
+ A_local[1]
+ A_local[2]
+ A_local[3]
+ A_local[4]
+ A_local[5]
+ A_local[6]
+ A_local[7]
)
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((2,), "float32x4"), B: T.Buffer((1,), "float32")):
A_local = T.alloc_buffer((2,), "float32x4", scope="local")
A_local[0] = A[0]
A_local[1] = A[1]
B[0] = (
T.Shuffle([A_local[0]], [0])
+ T.Shuffle([A_local[0]], [1])
+ T.Shuffle([A_local[0]], [2])
+ T.Shuffle([A_local[0]], [3])
+ T.Shuffle([A_local[1]], [0])
+ T.Shuffle([A_local[1]], [1])
+ T.Shuffle([A_local[1]], [2])
+ T.Shuffle([A_local[1]], [3])
)
After = transform(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_address_of():
transform = tvm.tirx.transform.PointerValueTypeRewrite()
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((16,), "float32"), B: T.Buffer((16,), "float32")):
for i in range(4):
T.evaluate(T.address_of(A[i * 4]))
B[i * 4 : i * 4 + 4] = A[i * 4 : i * 4 + 4]
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((16,), "float32"), B: T.Buffer((4,), "float32x4")):
for i in range(4):
T.evaluate(T.address_of(A[i * 4]))
B[T.Div(i * 4, 4)] = A[i * 4 : i * 4 + 4]
After = transform(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_scalar_read_without_write():
transform = tvm.tirx.transform.PointerValueTypeRewrite()
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((16,), "float32")):
for i in range(4):
T.evaluate(A[i * 4])
# Expected is the same as Before - no transformation
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((16,), "float32")):
for i in range(4):
T.evaluate(A[i * 4])
After = transform(Before)
tvm.ir.assert_structural_equal(After, Expected)
if __name__ == "__main__":
tvm.testing.main()
@@ -0,0 +1,67 @@
# 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.
import tvm
import tvm.testing
def test_prim_func_pass():
@tvm.tirx.transform.prim_func_pass(opt_level=1)
class TestReplaceFunc:
"""Simple test function to replace one argument to another."""
def __init__(self, new_func):
self.new_func = new_func
def transform_function(self, func, mod, ctx):
return self.new_func
x = tvm.tirx.Var("x", "int32")
y = tvm.tirx.Var("y", "int32")
b = tvm.tirx.decl_buffer((x,), "float32")
stmt = tvm.tirx.SeqStmt([tvm.tirx.Bind(x, 10), tvm.tirx.Evaluate(x + 1)])
func = tvm.tirx.PrimFunc([x, y, b], stmt)
new_func = tvm.tirx.PrimFunc([x, y, b], tvm.tirx.Evaluate(0))
mod = tvm.IRModule({"main": func})
mod = TestReplaceFunc(new_func)(mod)
tvm.ir.assert_structural_equal(mod["main"].body, new_func.body)
def test_cow_pass():
def fapply(f):
assert tvm.testing.object_use_count(f) == 1
return f
pidentity = tvm.tirx.transform.Apply(fapply)
x = tvm.tirx.Var("x", "int32")
func = tvm.tirx.PrimFunc([x], tvm.tirx.Evaluate(x)).with_attr("target_bits", 32)
func_hash = func.__hash__()
mod = tvm.IRModule({"main": func})
del func
# copy on write
mod_hash = mod.__hash__()
mod = tvm.transform.Sequential([pidentity, tvm.tirx.transform.NarrowDataType(32)])(mod._move())
assert mod_hash == mod.__hash__()
assert func_hash == mod["main"].__hash__()
if __name__ == "__main__":
test_cow_pass()
test_prim_func_pass()
@@ -0,0 +1,69 @@
# 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.
import tvm
import tvm.testing
from tvm.script import ir as I
from tvm.script import tirx as T
def test_remove_assume():
"""Remove any instance of T.assume"""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(1, "int32")):
T.evaluate(T.assume(A[0] == 5))
A[0] = 10
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(1, "int32")):
A[0] = 10
After = tvm.tirx.transform.RemoveAssume()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_remove_assume_loop():
"""Loops containing only T.assume should be removed"""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(16, "int32")):
for i in T.serial(16):
T.evaluate(T.assume(A[i] == 0))
for i in T.serial(16):
A[i] = 10
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(16, "int32")):
for i in T.serial(16):
A[i] = 10
After = tvm.tirx.transform.RemoveAssume()(Before)
tvm.ir.assert_structural_equal(After, Expected)
if __name__ == "__main__":
tvm.testing.main()
@@ -0,0 +1,528 @@
# 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: F841
import pytest
import tvm
import tvm.testing
from tvm.script import tirx as T
def nop():
return tvm.tirx.Evaluate(1)
def test_remove_no_op():
i = tvm.tirx.Var("i", "int32")
j = tvm.tirx.Var("j", "int32")
k = tvm.tirx.Var("k", "int32")
m = tvm.tirx.Var("m", "int32")
n = tvm.tirx.Var("n", "int32")
dtype = "int64"
Ab = tvm.tirx.decl_buffer((n,), dtype)
stmt = tvm.tirx.For(
i,
0,
4,
tvm.tirx.ForKind.SERIAL,
tvm.tirx.For(
j,
0,
n,
tvm.tirx.ForKind.SERIAL,
tvm.tirx.For(
k,
0,
m,
tvm.tirx.ForKind.SERIAL,
tvm.tirx.IfThenElse(
(i * m + j + k < n), tvm.tirx.Evaluate(m), tvm.tirx.Evaluate(n)
),
),
),
)
mod = tvm.IRModule.from_expr(tvm.tirx.PrimFunc([Ab], stmt))
ret = tvm.tirx.transform.RemoveNoOp()(mod)["main"].body
assert isinstance(ret, tvm.tirx.Evaluate)
store = tvm.tirx.BufferStore(Ab, tvm.tirx.BufferLoad(Ab, [i]) + 1, [i + 1])
stmt2 = tvm.tirx.SeqStmt([nop(), tvm.tirx.SeqStmt([store, nop()])])
mod = tvm.IRModule.from_expr(tvm.tirx.PrimFunc([Ab], stmt2))
ret = tvm.tirx.transform.RemoveNoOp()(mod)["main"].body
assert ret == store
# remove zero extent loop
stmt3 = tvm.tirx.For(i, 0, 0, tvm.tirx.ForKind.SERIAL, store)
mod = tvm.IRModule.from_expr(tvm.tirx.PrimFunc([Ab], stmt3))
ret = tvm.tirx.transform.RemoveNoOp()(mod)["main"].body
assert isinstance(ret, tvm.tirx.Evaluate)
def test_remove_no_op_with_invalid_extent():
@T.prim_func(s_tir=True)
def main(A: T.Buffer((16), "int32"), B: T.Buffer((16), "int32")) -> None:
for i in T.serial(16):
for j in T.serial(i - 20):
B[i] = A[i] + j
mod = tvm.ir.module.IRModule.from_expr(main)
ret = tvm.tirx.transform.RemoveNoOp()(mod)["main"].body
assert isinstance(ret, tvm.tirx.Evaluate)
def _apply_remove_no_op(mod, max_simplification_steps=0):
"""Helper function to apply RemoveNoOp transform with config."""
config = {
"tirx.RemoveNoOp": {
"max_simplification_steps": max_simplification_steps,
}
}
with tvm.transform.PassContext(config=config):
mod = tvm.tirx.transform.RemoveNoOp()(mod)
return mod
def test_remove_empty_for_loop():
"""A for-loop whose body is a no-op is itself a no-op."""
@T.prim_func(private=True, s_tir=True)
def before():
for i in T.serial(16):
T.evaluate(0)
@T.prim_func(private=True, s_tir=True)
def expected():
T.evaluate(0)
mod = tvm.IRModule.from_expr(before)
mod = _apply_remove_no_op(mod)
tvm.ir.assert_structural_equal(mod["main"], expected)
def test_remove_zero_extent_loop():
"""A for-loop with no extent is a no-op."""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(16, "int32")):
for i in T.serial(0):
A[i] = 42
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(16, "int32")):
T.evaluate(0)
mod = tvm.IRModule.from_expr(before)
mod = _apply_remove_no_op(mod)
tvm.ir.assert_structural_equal(mod["main"], expected)
def test_remove_unused_let():
"""With flat Bind, unused bindings are preserved by remove_no_op.
Unused Bind elimination requires a separate two-pass approach
and is not handled by the current remove_no_op pass.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(16, "int32")):
x = 5
for i in T.serial(16):
A[i] = 0
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(16, "int32")):
x = 5
for i in T.serial(16):
A[i] = 0
mod = tvm.IRModule.from_expr(before)
mod = _apply_remove_no_op(mod)
tvm.ir.assert_structural_equal(mod["main"], expected)
def test_remove_let_used_only_in_no_op():
"""With flat Bind, unused bindings are preserved by remove_no_op.
The zero-extent for loop is removed, but the Bind for x remains
since unused Bind elimination is not handled by remove_no_op.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(16, "int32")):
x = 5
for i in T.serial(0):
A[i] = x
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(16, "int32")):
x = 5
T.evaluate(0)
mod = tvm.IRModule.from_expr(before)
mod = _apply_remove_no_op(mod)
tvm.ir.assert_structural_equal(mod["main"], expected)
def test_keep_side_effects_of_let():
"""Side-effect Bind is preserved as-is by remove_no_op."""
@T.prim_func(private=True, s_tir=True)
def before():
x = T.call_extern("extern_func", dtype="int32")
T.evaluate(0)
@T.prim_func(private=True, s_tir=True)
def expected():
x = T.call_extern("extern_func", dtype="int32")
T.evaluate(0)
mod = tvm.IRModule.from_expr(before)
mod = _apply_remove_no_op(mod)
tvm.ir.assert_structural_equal(mod["main"], expected)
def test_remove_empty_then_case():
"""A no-op then_case can be removed."""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(16, "int32")):
for i in T.serial(16):
if i < 8:
T.evaluate(0)
else:
A[i] = 42
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(16, "int32")):
for i in T.serial(16):
if not (i < 8):
A[i] = 42
mod = tvm.IRModule.from_expr(before)
mod = _apply_remove_no_op(mod)
tvm.ir.assert_structural_equal(mod["main"], expected)
def test_remove_empty_else_case():
"""A no-op else_case can be removed."""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(16, "int32")):
for i in T.serial(16):
if i < 8:
A[i] = 42
else:
T.evaluate(0)
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(16, "int32")):
for i in T.serial(16):
if i < 8:
A[i] = 42
mod = tvm.IRModule.from_expr(before)
mod = _apply_remove_no_op(mod)
tvm.ir.assert_structural_equal(mod["main"], expected)
def test_suppress_removal_of_unused_write():
"""Sequential writes to the same location are not removed.
Dataflow analysis is no longer supported.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(16, "int32")):
for i in T.serial(16):
A[i] = 100
A[i] = 42
mod = tvm.IRModule.from_expr(before)
mod = _apply_remove_no_op(mod)
tvm.ir.assert_structural_equal(mod["main"], before)
def test_keep_first_write_when_used():
"""For two sequential writes, keep the first if it is used"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(16, "int32")):
for i in T.serial(16):
A[i] = 100
A[i] = A[i] + 1
mod = tvm.IRModule.from_expr(before)
mod = _apply_remove_no_op(mod)
tvm.ir.assert_structural_equal(mod["main"], before)
def test_keep_partially_overwritten_loop():
"""Keep partially overwritten regions
If the second loop doesn't entirely overwrite the first, the first
may not be removed be kept.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(16, "int32")):
for i in T.serial(16):
A[i] = 100
for i in T.serial(16):
if i < 12:
A[i] = 42
mod = tvm.IRModule.from_expr(before)
mod = _apply_remove_no_op(mod)
tvm.ir.assert_structural_equal(mod["main"], before)
def test_remove_read_write():
"""Writing a value to the same location as was just read is a no-op."""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(1, "int32")):
A[0] = A[0]
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(1, "int32")):
T.evaluate(0)
mod = tvm.IRModule.from_expr(before)
mod = _apply_remove_no_op(mod)
tvm.ir.assert_structural_equal(mod["main"], expected)
def test_keep_read_write_to_different_indices():
"""Writing a value to a different index should not be removed"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(16, "int32")):
for i in T.serial(15):
A[i] = A[i + 1]
mod = tvm.IRModule.from_expr(before)
mod = _apply_remove_no_op(mod)
tvm.ir.assert_structural_equal(mod["main"], before)
def test_remove_read_write_same_index_different_expression():
"""Writing a value to the same location as the read is a no-op.
If the value of the index can be proven to be the same, then the
store can be removed. With flat Bind, the Bind for i and the
enclosing loops remain since unused Bind elimination is not
handled by remove_no_op.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(16, "int32")):
for io, ii in T.grid(4, 4):
i: T.let[T.int32] = 4 * io + ii
A[4 * io + ii] = A[i]
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(16, "int32")):
for io in range(4):
for ii in range(4):
i: T.let[T.int32] = 4 * io + ii
mod = tvm.IRModule.from_expr(before)
mod = _apply_remove_no_op(mod)
tvm.ir.assert_structural_equal(mod["main"], expected)
def test_remove_read_write_same_index_using_constraint():
"""Writing a value to the same location as the read is a no-op.
If the value of the index can be proven to be the same, then the
no-op can be removed. This may require using the a constraint
that is known from a conditional containing the read/write.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(16, "int32")):
for i in T.serial(16):
if i != 0:
A[i] = A[i - 1]
else:
A[i] = A[0]
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(16, "int32")):
for i in T.serial(16):
if i != 0:
A[i] = A[i - 1]
mod = tvm.IRModule.from_expr(before)
mod = _apply_remove_no_op(mod)
tvm.ir.assert_structural_equal(mod["main"], expected)
@pytest.mark.xfail(reason="Dead alloc removal not yet implemented for flat AllocBuffer")
def test_remove_empty_temporary():
"""An allocation with a no-op body is a no-op."""
@T.prim_func(private=True, s_tir=True)
def before():
A = T.alloc_buffer((16,), "int32", scope="local")
T.evaluate(0)
@T.prim_func(private=True, s_tir=True)
def expected():
T.evaluate(0)
mod = tvm.IRModule.from_expr(before)
mod = _apply_remove_no_op(mod)
tvm.ir.assert_structural_equal(mod["main"], expected)
@pytest.mark.xfail(reason="Dead alloc removal not yet implemented for flat AllocBuffer")
def test_remove_empty_temporary_with_decl_buffer():
"""Remove DeclBuffer alongside Allocate
If an unused allocation is removed, any DeclBuffer instances that
refer to it should also be removed.
"""
@T.prim_func(private=True, s_tir=True)
def before():
A = T.decl_buffer([4, 4], "int32", scope="local")
A_flat = T.decl_buffer(16, "int32", scope="local", data=A.data)
T.evaluate(0)
@T.prim_func(private=True, s_tir=True)
def expected():
T.evaluate(0)
mod = tvm.IRModule.from_expr(before)
mod = _apply_remove_no_op(mod)
tvm.ir.assert_structural_equal(mod["main"], expected)
@pytest.mark.xfail(reason="Not implemented yet")
def test_remove_unused_temporary():
"""An unused allocation is a no-op."""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(16, "int32")):
B = T.alloc_buffer((16,), "int32", scope="local")
for i in T.serial(16):
A[i] = 1
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(16, "int32")):
for i in T.serial(16):
A[i] = 1
mod = tvm.IRModule.from_expr(before)
mod = _apply_remove_no_op(mod)
tvm.ir.assert_structural_equal(mod["main"], expected)
@pytest.mark.xfail(reason="Not implemented yet")
def test_remove_unused_write_into_temporary():
"""A write that only impacts a temporary allocation is a no-op."""
@T.prim_func(private=True, s_tir=True)
def before():
A = T.decl_buffer([16], "int32", scope="local")
for i in T.serial(16):
A[i] = 0
@T.prim_func(private=True, s_tir=True)
def expected():
T.evaluate(0)
mod = tvm.IRModule.from_expr(before)
mod = _apply_remove_no_op(mod)
tvm.ir.assert_structural_equal(mod["main"], expected)
def test_keep_used_write_into_temporary():
"""A write into a temporary that is used later must be kept."""
@T.prim_func(private=True, s_tir=True)
def before(B: T.Buffer(16, "int32")):
A = T.decl_buffer([16], "int32", scope="local")
for i in T.serial(16):
A[i] = 0
for i in T.serial(16):
B[i] = A[i]
mod = tvm.IRModule.from_expr(before)
mod = _apply_remove_no_op(mod)
tvm.ir.assert_structural_equal(mod["main"], before)
@pytest.mark.xfail(reason="Not implemented yet")
def test_remove_write_into_temporary():
"""A write that only impacts a temporary allocation is a no-op."""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(16, "int32"), C: T.Buffer(1, "int32")):
B = T.decl_buffer([16], "int32", scope="local")
for i in T.serial(16):
B[i] = A[i]
C[0] = 0
for i in T.serial(16):
C[0] = C[0] + B[i]
for i in T.serial(16):
B[i] = 0
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(16, "int32"), C: T.Buffer(1, "int32")):
B = T.decl_buffer([16], "int32", scope="local")
for i in T.serial(16):
B[i] = A[i]
C[0] = 0
for i in T.serial(16):
C[0] = C[0] + B[i]
mod = tvm.IRModule.from_expr(before)
mod = _apply_remove_no_op(mod)
tvm.ir.assert_structural_equal(mod["main"], expected)
def test_certain_condition():
"""The conditon of the If-Else node is certain.
This would cause `Segmentation fault` error before."""
@T.prim_func(private=True, s_tir=True)
def before():
if True:
T.evaluate(0)
else:
T.evaluate(0)
@T.prim_func(private=True, s_tir=True)
def expected():
T.evaluate(0)
mod = tvm.IRModule.from_expr(before)
mod = _apply_remove_no_op(mod)
tvm.ir.assert_structural_equal(mod["main"], expected)
if __name__ == "__main__":
tvm.testing.main()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,618 @@
# 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.
import tvm
import tvm.testing
from tvm.script import ir as I
from tvm.script import tirx as T
def test_public_api_surface():
assert hasattr(tvm.tirx.transform, "SplitHostDevice")
assert not hasattr(tvm.tirx.transform, "AnnotateDeviceRegions")
assert not hasattr(tvm.tirx.transform, "LowerDeviceKernelLaunch")
def test_ssa_across_entire_module():
"""The host and device functions should not share TIR vars
Any arguments that are passed from the host to the device should
be in terms of independent TIR variables.
"""
@I.ir_module
class before:
@T.prim_func(s_tir=True)
def main():
T.func_attr({"global_symbol": "main", "target": T.target("cuda", host="llvm")})
for i in range(16):
T.attr(0, "device_scope", 0)
for j in range(16):
T.evaluate(i)
after = tvm.tirx.transform.SplitHostDevice()(before)
loop_var = after["main"].body.loop_var
param_var = after["main_kernel"].params[0]
assert not loop_var.same_as(param_var)
def test_split_host_device():
"""SplitHostDevice divides a function at the "target" attribute"""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(n: T.int32):
T.func_attr({"target": T.target("cuda", host={"kind": "llvm", "opt-level": 0})})
T.attr(T.target("cuda"), "target", 0)
T.evaluate(n)
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(n: T.int32):
T.func_attr({"target": T.target("cuda", host={"kind": "llvm", "opt-level": 0})})
T.call_packed("main_kernel", n)
@T.prim_func(s_tir=True)
def main_kernel(n: T.int32):
T.func_attr(
{
"target": T.target("cuda"),
"calling_conv": 2,
"tirx.kernel_launch_params": [],
"global_symbol": "main_kernel",
"tirx.noalias": True,
"tirx.is_global_func": True,
}
)
T.evaluate(n)
After = tvm.tirx.transform.SplitHostDevice()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_split_host_device_on_cpu():
"""A kernel running on the CPU may return an error code"""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(n: T.int32):
T.func_attr({"target": T.target("cuda", host={"kind": "llvm", "opt-level": 0})})
T.attr(T.target("llvm"), "target", 0)
T.evaluate(n)
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(n: T.int32):
T.func_attr({"target": T.target("cuda", host={"kind": "llvm", "opt-level": 0})})
kernel_error_code: T.let[T.int32] = T.call_extern("int32", "main_kernel", n)
assert kernel_error_code == 0, "Error executing compute kernel"
@T.prim_func(s_tir=True)
def main_kernel(n: T.int32) -> T.int32:
T.func_attr(
{
"target": T.target("llvm"),
"tirx.noalias": True,
"tirx.is_global_func": True,
}
)
T.evaluate(n)
T.ret(0)
After = tvm.tirx.transform.SplitHostDevice()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_split_host_device_without_func_host_attribute():
"""Like test_split_host_device, but no host specified in the host's target
The `T.attr` specifying the device still requires splitting out
the kernel.
"""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(n: T.int32):
T.func_attr({"target": T.target("llvm")})
T.attr(T.target("cuda"), "target", 0)
T.evaluate(n)
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(n: T.int32):
T.func_attr({"target": T.target("llvm")})
T.call_packed("main_kernel", n)
@T.prim_func(s_tir=True)
def main_kernel(n: T.int32):
T.func_attr(
{
"target": T.target("cuda"),
"calling_conv": 2,
"tirx.kernel_launch_params": [],
"global_symbol": "main_kernel",
"tirx.noalias": True,
"tirx.is_global_func": True,
}
)
T.evaluate(n)
After = tvm.tirx.transform.SplitHostDevice()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_split_host_device_without_device_region():
"""Like test_split_host_device, but no device regions to extract
Because MakePackedAPI/MakeUnpackedAPI still require both the
device and host, SplitHostDevice does not modify the "target"
attribute.
"""
@T.prim_func(s_tir=True)
def Before():
T.func_attr({"target": T.target("ext_dev", host="llvm")})
T.evaluate(0)
Expected = Before
After = tvm.tirx.transform.SplitHostDevice()(tvm.IRModule.from_expr(Before))
tvm.ir.assert_structural_equal(After["Before"], Expected)
def test_split_host_device_name_collision():
"""Like test_split_host_device, but with the default name already taken
The default name is generated as `func.name + "_kernel"`. If this
name is already taken by another function in the IRModule, then
SplitHostDevice should select a different name.
"""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(n: T.int32):
T.func_attr({"target": T.target("cuda", host={"kind": "llvm", "opt-level": 0})})
T.attr(T.target("cuda"), "target", 0)
T.evaluate(n)
@T.prim_func(s_tir=True)
def main_kernel():
T.func_attr({"target": T.target("llvm")})
T.evaluate(0)
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(n: T.int32):
T.func_attr({"target": T.target("cuda", host={"kind": "llvm", "opt-level": 0})})
T.call_packed("main_kernel_1", n)
@T.prim_func(s_tir=True)
def main_kernel_1(n: T.int32):
T.func_attr(
{
"target": T.target("cuda"),
"calling_conv": 2,
"tirx.kernel_launch_params": [],
"global_symbol": "main_kernel_1",
"tirx.noalias": True,
"tirx.is_global_func": True,
}
)
T.evaluate(n)
@T.prim_func(s_tir=True)
def main_kernel():
T.func_attr({"target": T.target("llvm")})
T.evaluate(0)
After = tvm.tirx.transform.SplitHostDevice()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_dynamic_launch_thread():
"""Dynamic T.launch_thread may depend on host-side variable
A dynamic parameter for `T.launch_thread` may have an extent that
is computed using variables outside of the `T.target` section.
This is a regression test to catch a previous failure mode, in
which SplitHostDevice generated output with undefined variables,
if the only use of a variable occurred in the extent of a
`T.launch_thread` statement.
While the launch-lowering stage will hoist the
computation of the extent from the device kernel to the host
function, the IRModule must be well-defined at all stages of
lowering. Even if a variable is only used as part of a thread
extent, `SplitHostDevice` should treat it as a kernel parameter, to
provide a definition of the variable within the TIR device kernel.
"""
@I.ir_module
class before:
@T.prim_func(s_tir=True)
def default_function(var_A: T.handle, var_B: T.handle, seq_len: T.int32):
T.func_attr({"target": T.target("cuda")})
A = T.match_buffer(var_A, [seq_len], "int32")
B = T.match_buffer(var_B, [seq_len], "int32")
num_blocks: T.let[T.int32] = (seq_len + 127) // 128
with T.attr(T.target("cuda"), "target", 0):
blockIdx_x = T.launch_thread("blockIdx.x", num_blocks)
threadIdx_x = T.launch_thread("threadIdx.x", 128)
if blockIdx_x * 128 + threadIdx_x < seq_len:
B[blockIdx_x * 128 + threadIdx_x] = A[blockIdx_x * 128 + threadIdx_x]
@I.ir_module
class expected:
@T.prim_func(s_tir=True)
def default_function(var_A: T.handle, var_B: T.handle, seq_len: T.int32):
T.func_attr({"target": T.target("cuda")})
A = T.match_buffer(var_A, (seq_len,), "int32")
B = T.match_buffer(var_B, (seq_len,), "int32")
num_blocks: T.let[T.int32] = (seq_len + 127) // 128
expected.default_function_kernel(A.data, B.data, num_blocks, seq_len)
@T.prim_func(private=True, s_tir=True)
def default_function_kernel(
A_data: T.handle("int32"),
B_data: T.handle("int32"),
num_blocks: T.int32,
seq_len: T.int32,
):
T.func_attr(
{
"target": T.target("cuda"),
"tirx.is_global_func": True,
"tirx.noalias": True,
}
)
A = T.decl_buffer(seq_len, "int32", data=A_data)
B = T.decl_buffer(seq_len, "int32", data=B_data)
blockIdx_x = T.launch_thread("blockIdx.x", num_blocks)
threadIdx_x = T.launch_thread("threadIdx.x", 128)
if blockIdx_x * 128 + threadIdx_x < seq_len:
B[blockIdx_x * 128 + threadIdx_x] = A[blockIdx_x * 128 + threadIdx_x]
after = tvm.tirx.transform.SplitHostDevice()(before)
tvm.tirx.analysis.verify_well_formed(after)
tvm.ir.assert_structural_equal(expected, after)
def test_symbolic_var_parameter():
@I.ir_module
class Module:
@T.prim_func(s_tir=True)
def main(var_A: T.handle, var_B: T.handle):
T.func_attr({"target": T.target("cuda")})
m = T.int64()
A = T.match_buffer(var_A, (m,))
B = T.match_buffer(var_B, (m,))
T.attr(T.target("cuda"), "target", 0)
blockIdx_x = T.launch_thread("blockIdx.x", m)
B_1 = T.decl_buffer((m,), data=B.data)
A_1 = T.decl_buffer((m,), data=A.data)
B_1[blockIdx_x] = A_1[blockIdx_x]
after = tvm.tirx.transform.SplitHostDevice()(Module)
assert len(after["main_kernel"].params) == 3
assert isinstance(after["main_kernel"].params[2], tvm.tirx.Var)
def test_thread_extent_region_extracted_as_device_kernel():
"""A bare thread_extent is annotated and extracted as a device kernel."""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(16, "float32")):
T.func_attr({"target": T.target("cuda", host="llvm")})
i = T.launch_thread("threadIdx.x", 16)
A[i] = 0.0
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(16, "float32")):
T.func_attr({"target": T.target("cuda", host="llvm")})
T.call_packed("main_kernel", A.data, 16)
@T.prim_func(s_tir=True)
def main_kernel(A_data: T.handle("float32")):
T.func_attr(
{
"target": T.target("cuda"),
"calling_conv": 2,
"tirx.kernel_launch_params": ["threadIdx.x"],
"global_symbol": "main_kernel",
"tirx.noalias": True,
"tirx.is_global_func": True,
}
)
A = T.decl_buffer(16, dtype="float32", data=A_data)
i = T.launch_thread("threadIdx.x", 16)
A[i] = 0.0
After = tvm.tirx.transform.SplitHostDevice()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_device_scope_region_extracted_as_device_kernel():
"""A bare device_scope is annotated and extracted as a device kernel."""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(1, "float32")):
T.func_attr({"target": T.target("cuda", host="llvm")})
T.attr(0, "device_scope", 0)
A[0] = 0.0
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(1, "float32")):
T.func_attr({"target": T.target("cuda", host="llvm")})
T.call_packed("main_kernel", A.data)
@T.prim_func(s_tir=True)
def main_kernel(A_data: T.handle("float32")):
T.func_attr(
{
"target": T.target("cuda"),
"calling_conv": 2,
"tirx.kernel_launch_params": [],
"global_symbol": "main_kernel",
"tirx.noalias": True,
"tirx.is_global_func": True,
}
)
A = T.decl_buffer(1, dtype="float32", data=A_data)
T.attr(0, "device_scope", 0)
A[0] = 0.0
After = tvm.tirx.transform.SplitHostDevice()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_lower_device_kernel_launch():
"""Kernel calls are lowered using the public SplitHostDevice pass."""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(1, "float32")):
T.func_attr({"target": T.target("llvm")})
Before.kernel(A.data)
@T.prim_func(s_tir=True)
def kernel(A_data: T.handle("float32")):
T.func_attr({"target": T.target("cuda")})
A = T.decl_buffer(1, dtype="float32", data=A_data)
A[0] = 0.0
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(1, "float32")):
T.func_attr({"target": T.target("llvm")})
T.call_packed("kernel", A.data)
@T.prim_func(s_tir=True)
def kernel(A_data: T.handle("float32")):
T.func_attr(
{
"target": T.target("cuda"),
"calling_conv": 2,
"tirx.kernel_launch_params": [],
"global_symbol": "kernel",
"tirx.is_global_func": True,
}
)
A = T.decl_buffer(1, dtype="float32", data=A_data)
A[0] = 0.0
After = tvm.tirx.transform.SplitHostDevice()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_externally_visible_kernel_launch():
"""Kernel launch lowering preserves a pre-defined global_symbol."""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(1, "float32")):
T.func_attr({"target": T.target("llvm")})
Before.kernel(A.data)
@T.prim_func(s_tir=True)
def kernel(A_data: T.handle("float32")):
T.func_attr({"target": T.target("cuda"), "global_symbol": "kernel_by_another_name"})
A = T.decl_buffer(1, dtype="float32", data=A_data)
A[0] = 0.0
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(1, "float32")):
T.func_attr({"target": T.target("llvm")})
T.call_packed("kernel_by_another_name", A.data)
@T.prim_func(s_tir=True)
def kernel(A_data: T.handle("float32")):
T.func_attr(
{
"target": T.target("cuda"),
"calling_conv": 2,
"tirx.kernel_launch_params": [],
"global_symbol": "kernel_by_another_name",
"tirx.is_global_func": True,
}
)
A = T.decl_buffer(1, dtype="float32", data=A_data)
A[0] = 0.0
After = tvm.tirx.transform.SplitHostDevice()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_collect_launch_parameter():
"""Thread launch extents are appended to the host launch call."""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(16, "float32")):
T.func_attr({"target": T.target("llvm")})
Before.kernel(A.data)
@T.prim_func(s_tir=True)
def kernel(A_data: T.handle("float32")):
T.func_attr(
{
"target": T.target("cuda"),
"global_symbol": "kernel",
}
)
A = T.decl_buffer(16, dtype="float32", data=A_data)
i = T.launch_thread("threadIdx.x", 16)
A[i] = 0.0
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(16, "float32")):
T.func_attr({"target": T.target("llvm")})
T.call_packed("kernel", A.data, 16)
@T.prim_func(s_tir=True)
def kernel(A_data: T.handle("float32")):
T.func_attr(
{
"target": T.target("cuda"),
"calling_conv": 2,
"tirx.kernel_launch_params": ["threadIdx.x"],
"global_symbol": "kernel",
"tirx.is_global_func": True,
}
)
A = T.decl_buffer(16, dtype="float32", data=A_data)
i = T.launch_thread("threadIdx.x", 16)
A[i] = 0.0
After = tvm.tirx.transform.SplitHostDevice()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_same_device_different_target():
"""Same-device calls with different codegen are lowered to extern calls."""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(1, "float32")):
T.func_attr({"target": T.target("llvm")})
Before.kernel(A.data)
@T.prim_func(s_tir=True)
def kernel(A_data: T.handle("float32")):
T.func_attr({"target": T.target("c")})
A = T.decl_buffer(16, dtype="float32", data=A_data)
A[0] = 0.0
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(1, "float32")):
T.func_attr({"target": T.target("llvm")})
T.call_extern("kernel", A.data, dtype="void")
@T.prim_func(s_tir=True)
def kernel(A_data: T.handle("float32")):
T.func_attr(
{
"target": T.target("c"),
"global_symbol": "kernel",
"tirx.is_global_func": True,
}
)
A = T.decl_buffer(16, dtype="float32", data=A_data)
A[0] = 0.0
After = tvm.tirx.transform.SplitHostDevice()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_bind_before_thread_extent():
"""Bind-defined thread extents are inlined into launch arguments."""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(16, "float32"), n: T.int32):
T.func_attr({"target": T.target("llvm")})
Before.kernel(A.data, n)
@T.prim_func(s_tir=True)
def kernel(A_data: T.handle("float32"), n: T.int32):
T.func_attr({"target": T.target("cuda"), "global_symbol": "kernel"})
A = T.decl_buffer(16, dtype="float32", data=A_data)
v: T.let[T.int32] = n + 1
i = T.launch_thread("threadIdx.x", v)
A[i] = 0.0
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(16, "float32"), n: T.int32):
T.func_attr({"target": T.target("llvm")})
T.call_packed("kernel", A.data, n, n + 1)
@T.prim_func(s_tir=True)
def kernel(A_data: T.handle("float32"), n: T.int32):
T.func_attr(
{
"target": T.target("cuda"),
"calling_conv": 2,
"tirx.kernel_launch_params": ["threadIdx.x"],
"global_symbol": "kernel",
"tirx.is_global_func": True,
}
)
A = T.decl_buffer(16, dtype="float32", data=A_data)
v: T.let[T.int32] = n + 1
i = T.launch_thread("threadIdx.x", v)
A[i] = 0.0
After = tvm.tirx.transform.SplitHostDevice()(Before)
tvm.ir.assert_structural_equal(After, Expected)
if __name__ == "__main__":
tvm.testing.main()
@@ -0,0 +1,587 @@
# 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: F401, F841
import sys
import pytest
import tvm
import tvm.testing
from tvm.script import ir as I
from tvm.script import tirx as T
def test_alloc_seq():
scope_tb = "local.L0A"
@T.prim_func(s_tir=True)
def func(n: T.int32):
for i in T.serial(n):
for j in range(10):
A = T.alloc_buffer((200,), scope=scope_tb)
A[j] = T.float32(1.2)
for j in range(10):
B = T.alloc_buffer((200,), scope=scope_tb)
B[j] = T.float32(1.3)
mod = tvm.IRModule.from_expr(func)
body = tvm.tirx.transform.StorageRewrite()(mod)["func"].body
num_alloc = [0]
def verify(n):
if isinstance(n, tvm.tirx.AllocBuffer):
num_alloc[0] += 1
assert n.buffer.shape[0].value == 200
tvm.tirx.stmt_functor.post_order_visit(body, verify)
assert num_alloc[0] == 1
def test_alloc_different_dtypes():
# Test cross-loop buffer access with buffers allocated in parent scope
def make_mod(dtype_list, length):
assert len(dtype_list) == 4
@T.prim_func(s_tir=True)
def func():
# Allocate all buffers in parent scope (before any loops)
A = T.alloc_buffer((length,), dtype_list[0], scope="local.L0A")
B = T.alloc_buffer((length,), dtype_list[1], scope="local.L0A")
C = T.alloc_buffer((length,), dtype_list[2], scope="local.L0A")
D = T.alloc_buffer((length,), dtype_list[3], scope="local.L0A")
E = T.alloc_buffer((length,), "int8", scope="local.L0A")
for j in range(length):
A[j] = T.Cast(dtype_list[0], 1)
for j in range(length):
B[j] = T.Cast(dtype_list[1], 1)
for j in range(length):
C[j] = T.Cast(dtype_list[2], 1)
for j in range(length):
D[j] = T.Cast(dtype_list[3], 1)
for j in range(length):
E[j] = (
T.Cast("int8", A[j])
+ T.Cast("int8", B[j])
+ T.Cast("int8", C[j])
+ T.Cast("int8", D[j])
)
return tvm.IRModule.from_expr(func)
def dtype_bit_len(dtype):
index = 0
for i in dtype:
if i.isdigit():
break
index += 1
return int(dtype[index:])
def offset_generater(dtype_list, length):
dtype_len_list = [dtype_bit_len(i) for i in dtype_list]
base_len = dtype_len_list[0]
return sum([i * length / base_len for i in dtype_len_list])
def dtype_test(dtype_list, length):
def verify(n):
if isinstance(n, tvm.tirx.AllocBuffer):
assert n.buffer.shape[0].value == offset
mod = make_mod(dtype_list, length)
offset = offset_generater(dtype_list, length)
body = tvm.tirx.transform.StorageRewrite()(mod)["func"].body
tvm.tirx.stmt_functor.post_order_visit(body, verify)
length = 1024
dtype_list = ["float16", "int32", "uint16", "int8"]
dtype_test(dtype_list, length)
dtype_list = ["float32", "int32", "uint16", "int8"]
dtype_test(dtype_list, length)
dtype_list = ["float64", "int32", "uint16", "int8"]
dtype_test(dtype_list, length)
dtype_list = ["int8", "int32", "uint16", "uint8"]
dtype_test(dtype_list, length)
def test_address_of():
# In this test, the storage rewrite pass is allowed to
# combine buffers B and D, but not C
@T.prim_func(s_tir=True)
def before(A: T.Buffer(8, "float32"), E: T.Buffer(8, "float32")):
B = T.alloc_buffer((8,))
for i in range(8):
B[i] = (
T.call_extern("deref", T.address_of(A[i]), dtype="float32")
+ T.call_extern("deref", T.address_of(A[0]), dtype="float32")
+ T.float32(1)
)
C = T.alloc_buffer((8,))
for i in range(8):
C[i] = (
T.call_extern("deref", T.address_of(B[i]), dtype="float32")
+ T.call_extern("deref", T.address_of(B[0]), dtype="float32")
+ T.float32(2)
)
D = T.alloc_buffer((8,))
for i in range(8):
D[i] = (
T.call_extern("deref", T.address_of(C[i]), dtype="float32")
+ T.call_extern("deref", T.address_of(C[0]), dtype="float32")
+ T.float32(2)
)
for i in range(8):
E[i] = (
T.call_extern("deref", T.address_of(D[i]), dtype="float32")
+ T.call_extern("deref", T.address_of(D[0]), dtype="float32")
+ T.float32(3)
)
def verify(n):
if isinstance(n, tvm.tirx.AllocBuffer):
total_alloc[0] += n.buffer.shape[0].value
total_alloc = [0]
mod = tvm.IRModule.from_expr(before.with_attr("global_symbol", "main"))
tvm.tirx.stmt_functor.post_order_visit(mod["main"].body, verify)
assert total_alloc[0] == 24
total_alloc[0] = 0
mod = tvm.tirx.transform.StorageRewrite()(mod)
tvm.tirx.stmt_functor.post_order_visit(mod["main"].body, verify)
assert total_alloc[0] == 16
def test_parallel_alloc():
@T.prim_func(s_tir=True)
def func1(n: T.int32):
for i in T.parallel(n):
for j in range(10):
A = T.alloc_buffer((n,))
A[j] = A[j] + T.float32(2)
mod = tvm.IRModule.from_expr(func1)
body = tvm.tirx.transform.StorageRewrite()(mod)["func1"]
# With flat AllocBuffer, the for body is a SeqStmt; first element is AllocBuffer
assert isinstance(body.body.body[0], tvm.tirx.AllocBuffer)
@T.prim_func(s_tir=True)
def func2(n: T.int32):
for t in T.serial(n):
with T.attr(T.int32(1), "pragma_scope", "parallel_launch_point"):
for i in T.parallel(n):
for j in range(10):
A = T.alloc_buffer((n,))
A[j] = A[j] + T.float32(2)
mod = tvm.IRModule.from_expr(func2)
body = tvm.tirx.transform.StorageRewrite()(mod)["func2"]
assert isinstance(body.body.body.body.body[0], tvm.tirx.AllocBuffer)
def test_while_alloc():
@T.prim_func(s_tir=True)
def func_parallel(n: T.int32):
for i in T.parallel(n):
j = T.alloc_buffer((1,), "int32")
j[0] = 0
while j[0] < 10:
A = T.alloc_buffer((n,))
A[j[0]] = A[j[0]] + T.float32(2)
j[0] = j[0] + j[0] + 1
@T.prim_func(s_tir=True)
def func_serial(n: T.int32):
for i in T.serial(n):
j = T.alloc_buffer((1,), "int32")
j[0] = 0
while j[0] < 10:
A = T.alloc_buffer((n,))
A[j[0]] = A[j[0]] + T.float32(2)
j[0] = j[0] + j[0] + 1
mod = tvm.IRModule.from_expr(func_parallel)
# parallel (i, 0, n) {
# allocate j[int32 * 1]
# j[0] = 0
# while((j[0] < 10)){
# // attr [A] storage_scope = "global"
# allocate A[float32 * n]
# A[j[0]] = (A[j[0]] + 2f)
# j[0] = (j[0] + (j[0] + 1))
# }
# }
body = tvm.tirx.transform.StorageRewrite()(mod)["func_parallel"]
# Navigate to inside the for loop, then check that allocations exist
# The structure with DeclBuffer is:
# parallel (i, 0, n) { DeclBuffer(j, DeclBuffer(A, ...)) }
# or with Allocate+DeclBuffer pairs
inner = body.body.body # inside For
# Skip DeclBuffer nodes to find Allocate
num_alloc = [0]
def count_alloc(n):
if isinstance(n, tvm.tirx.AllocBuffer):
num_alloc[0] += 1
tvm.tirx.stmt_functor.post_order_visit(inner, count_alloc)
assert num_alloc[0] == 2 # j and A allocations
mod = tvm.IRModule.from_expr(func_serial)
body = tvm.tirx.transform.StorageRewrite()(mod)["func_serial"]
num_alloc[0] = 0
tvm.tirx.stmt_functor.post_order_visit(body.body, count_alloc)
assert num_alloc[0] == 2 # j and A allocations
def test_alloc_seq_type():
@T.prim_func(s_tir=True)
def func(n: T.int32):
for i in T.serial(n):
for j in range(10):
A = T.alloc_buffer((200,), scope="local.L0A")
A1 = T.alloc_buffer((200,), scope="local.L0A")
A[j] = T.float32(1.2)
A1[j] = T.float32(1.3)
B = T.alloc_buffer((200,), "int16", scope="local.L0A")
B[j] = T.int16(1)
C = T.alloc_buffer((200,), "int16", scope="local.L0A")
C[j] = T.int16(1)
D = T.alloc_buffer((200,), "int16", scope="local.L0A")
D[j] = B[j] + C[j]
A2 = T.alloc_buffer((200,), scope="local.L0A")
A2[j] = A[j]
mod = tvm.IRModule.from_expr(func)
body = tvm.tirx.transform.StorageRewrite()(mod)["func"].body
num_alloc = [0]
def verify(n):
if isinstance(n, tvm.tirx.AllocBuffer):
num_alloc[0] += 1
assert n.buffer.shape[0].value == 500
tvm.tirx.stmt_functor.post_order_visit(body, verify)
assert num_alloc[0] == 1
def test_alloc_seq_type2():
scope_tb = "local.L0A2"
@T.prim_func(s_tir=True)
def func(n: T.int32):
for i in T.serial(n):
for j in range(10):
A = T.alloc_buffer((200,), scope=scope_tb)
A[j] = T.float32(1.2)
for j in range(20):
B = T.alloc_buffer((400,), "int16", scope=scope_tb)
B[j] = T.int16(1)
for j in range(10):
C = T.alloc_buffer((200,), scope=scope_tb)
C[j] = T.float32(1.2)
mod = tvm.IRModule.from_expr(func)
body = tvm.tirx.transform.StorageRewrite()(mod)["func"].body
num_alloc = [0]
def verify(n):
if isinstance(n, tvm.tirx.AllocBuffer):
num_alloc[0] += 1
assert n.buffer.shape[0].value == 200
tvm.tirx.stmt_functor.post_order_visit(body, verify)
assert num_alloc[0] == 1
def test_reuse_small_buffer():
@T.prim_func(s_tir=True)
def func(n: T.int32):
for i in T.serial(n):
for j in range(10):
A = T.alloc_buffer((200,), "int16", scope="local.L0A")
A[j] = T.int16(1)
B = T.alloc_buffer((200,), "int16", scope="local.L0A")
B[j] = T.int16(1)
B1 = T.alloc_buffer((200,), "int16", scope="local.L0A")
B1[j] = A[j] + B[j]
C = T.alloc_buffer((400,), "int16", scope="local.L0A")
C[j] = T.int16(1)
D = T.alloc_buffer((400,), "int16", scope="local.L0A")
D[j] = T.int16(1)
E = T.alloc_buffer((400,), "int16", scope="local.L0A")
E[j] = C[j]
mod = tvm.IRModule.from_expr(func)
body = tvm.tirx.transform.StorageRewrite()(mod)["func"].body
num_alloc = [0]
def verify(n):
if isinstance(n, tvm.tirx.AllocBuffer):
num_alloc[0] += 1
assert n.buffer.shape[0].value == 800
tvm.tirx.stmt_functor.post_order_visit(body, verify)
assert num_alloc[0] == 1
def test_access_in_let_value():
@T.prim_func(s_tir=True)
def func(A: T.Buffer((8,), "float32")):
for i in range(8):
B = T.alloc_buffer((1,))
B[0] = 3.14
x: T.let[T.float32] = T.exp(B[0], dtype="float32")
A[i] = (x + 1.0) / (x - 1.0)
@T.prim_func(s_tir=True)
def func_rewritten(A: T.Buffer((8,), "float32")) -> None:
B = T.alloc_buffer((1,))
for i in range(8):
B[0] = 3.14
x: T.let[T.float32] = T.exp(B[0], dtype="float32")
A[i] = (x + 1.0) / (x - 1.0)
mod = tvm.tirx.transform.StorageRewrite()(
tvm.IRModule.from_expr(func.with_attr("global_symbol", "main"))
)
tvm.ir.assert_structural_equal(mod["main"], func_rewritten.with_attr("global_symbol", "main"))
def test_let_buffer_rewrite():
"""StorageRewrite replaces the bound var of backing allocations
If StorageRewrite replaces the backing variable of an array, such
as when vectorizing the storage type, the variable must be
replaced in the Bind that defines it. Currently, StmtMutator
only visits usage of variables, and does not visit definitions of
variables, so the definition in a Bind must be explicitly
handled.
"""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main() -> None:
A_data: T.let[T.handle("int32")] = T.call_extern(
"dummy_func", dtype=T.handle("int32").ty
)
A = T.decl_buffer([8], "int32", data=A_data)
A[0:8] = T.broadcast(42, 8)
@I.ir_module(check_well_formed=False)
class Expected:
@T.prim_func(s_tir=True)
def main() -> None:
A_data: T.let[T.handle("int32x8")] = T.call_extern(
"dummy_func", dtype=T.handle("int32x8").ty
)
A = T.decl_buffer([8], "int32", data=A_data)
A_1 = T.Buffer([1], "int32x8", data=A_data)
A_1[0] = T.broadcast(42, 8)
After = tvm.tirx.transform.StorageRewrite()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_rewrite_in_place_use_of_non_flat_buffer():
"""A non-flat buffer may be re-used for in-place operations"""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((16, 16), "float32"), D: T.Buffer((16, 16), "float32")):
B = T.decl_buffer(
[16, 16],
dtype="float32",
axis_separators=[1],
)
C = T.decl_buffer(
[16, 16],
dtype="float32",
axis_separators=[1],
)
for i, j in T.grid(16, 16):
B[i, j] = A[i, j]
for i, j in T.grid(16, 16):
C[i, j] = 2.0 * B[i, j]
for i, j in T.grid(16, 16):
D[i, j] = C[i, j]
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((16, 16), "float32"), D: T.Buffer((16, 16), "float32")):
B = T.decl_buffer([16, 16], dtype="float32", axis_separators=[1])
C = T.decl_buffer(
[16, 16],
dtype="float32",
axis_separators=[1],
data=B.data,
)
for i, j in T.grid(16, 16):
B[i, j] = A[i, j]
for i, j in T.grid(16, 16):
C[i, j] = 2.0 * B[i, j]
for i, j in T.grid(16, 16):
D[i, j] = C[i, j]
After = tvm.tirx.transform.StorageRewrite()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_no_rewrite_of_shared_non_flat_buffer():
"""In general, sharing of non-flat buffer isn't supported
The current packing algorithms in StorageRewrite assume a flat
memory space, and do not support packing of N-d buffers. For
buffers with axis separators, normal buffer sharing should be
disabled.
Like test_rewrite_in_place_use_of_non_flat_buffer, except that B and C do
not have matching shapes.
"""
@T.prim_func(s_tir=True)
def Before(A: T.Buffer((16, 16), "float32"), D: T.Buffer((16, 16), "float32")):
B = T.decl_buffer(
[16, 16],
dtype="float32",
axis_separators=[1],
)
C = T.decl_buffer(
[20, 20],
dtype="float32",
axis_separators=[1],
)
for i, j in T.grid(16, 16):
B[i, j] = A[i, j]
for i, j in T.grid(16, 16):
C[i, j] = 2.0 * B[i, j]
for i, j in T.grid(16, 16):
D[i, j] = C[i, j]
Expected = Before
After = tvm.tirx.transform.StorageRewrite()(tvm.IRModule.from_expr(Before))
tvm.ir.assert_structural_equal(After["Before"], Expected)
def test_rewrite_decl_buffer():
"""A DeclBuffer node may appear in StorageRewrite's input"""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(16, "float32"), D: T.Buffer(16, "float32")):
B = T.decl_buffer(16, dtype="float32")
C = T.decl_buffer(16, dtype="float32")
for i in range(16):
B[i] = A[i]
for i in range(16):
C[i] = 2.0 * B[i]
for i in range(16):
D[i] = C[i]
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(16, "float32"), D: T.Buffer(16, "float32")):
B = T.decl_buffer(16, dtype="float32")
C = T.decl_buffer(16, dtype="float32", data=B.data)
for i in range(16):
B[i] = A[i]
for i in range(16):
C[i] = 2.0 * B[i]
for i in range(16):
D[i] = C[i]
After = tvm.tirx.transform.StorageRewrite()(Before)
tvm.ir.assert_structural_equal(After, Expected)
def test_no_orphaned_decl_buffer():
"""A DeclBuffer of an unused Allocate should be removed
StorageRewrite removes any allocations that are unused. When it
does so, any DeclBuffer that refers to that allocation should also
be removed.
"""
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(16, "float32"), D: T.Buffer(16, "float32")):
B = T.decl_buffer(16, dtype="float32")
C = T.decl_buffer(16, dtype="float32")
Unused = T.decl_buffer(16, dtype="float32")
for i in range(16):
B[i] = A[i]
for i in range(16):
C[i] = 2.0 * B[i]
for i in range(16):
D[i] = C[i]
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(A: T.Buffer(16, "float32"), D: T.Buffer(16, "float32")):
B = T.decl_buffer(16, dtype="float32")
C = T.decl_buffer(16, dtype="float32", data=B.data)
for i in range(16):
B[i] = A[i]
for i in range(16):
C[i] = 2.0 * B[i]
for i in range(16):
D[i] = C[i]
After = tvm.tirx.transform.StorageRewrite()(Before)
tvm.ir.assert_structural_equal(After, Expected)
if __name__ == "__main__":
tvm.testing.main()
@@ -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.
import tvm
from tvm.script import ir as I
from tvm.script import tirx as T
def test_unroll_loop():
@I.ir_module
class Module:
@T.prim_func(s_tir=True)
def main(A: T.handle, n: T.int64):
Ab = T.match_buffer(A, (n,), "int64")
for i in T.serial(n, n + 2):
for j in T.unroll(8):
Ab[j + 1] = Ab[i] + T.int64(1)
mod = Module
stmt = mod["main"].body
assert isinstance(stmt, tvm.tirx.For)
with tvm.transform.PassContext(config={"tirx.UnrollLoop": {"auto_max_step": 16}}):
ret = tvm.tirx.transform.UnrollLoop()(mod)["main"].body
assert not isinstance(ret, tvm.tirx.For)
with tvm.transform.PassContext(config={"tirx.UnrollLoop": {"auto_max_step": 15}}):
ret = tvm.tirx.transform.UnrollLoop()(mod)["main"].body
assert isinstance(ret, tvm.tirx.For)
with tvm.transform.PassContext(
config={"tirx.UnrollLoop": {"auto_max_step": 16, "explicit_unroll": False}}
):
ret = tvm.tirx.transform.UnrollLoop()(mod)["main"].body
assert isinstance(ret, tvm.tirx.For)
assert ret.kind == tvm.tirx.ForKind.UNROLLED
@I.ir_module
class ModuleWithPragma:
@T.prim_func(s_tir=True)
def main(A: T.handle, n: T.int64):
Ab = T.match_buffer(A, (n,), "int64")
with T.attr(T.int32(0), "pragma_auto_unroll_max_step", 16):
for i in T.serial(n, n + 2):
for j in T.unroll(8):
Ab[j + 1] = Ab[i] + T.int64(1)
for i in T.serial(n, n + 2):
for j in T.unroll(8):
Ab[j + 1] = Ab[i] + T.int64(1)
with tvm.transform.PassContext(
config={"tirx.UnrollLoop": {"auto_max_depth": 8, "explicit_unroll": False}}
):
ret = tvm.tirx.transform.UnrollLoop()(ModuleWithPragma)["main"].body
assert isinstance(ret[0], tvm.tirx.For)
assert ret[0].kind == tvm.tirx.ForKind.UNROLLED
assert isinstance(ret[1], tvm.tirx.For)
assert ret[1].kind != tvm.tirx.ForKind.UNROLLED
def test_unroll_fake_loop():
@I.ir_module
class Module:
@T.prim_func(s_tir=True)
def main(A: T.handle, n: T.int64):
Ab = T.match_buffer(A, (n,), "int32")
for i in T.serial(1):
Ab[i * 2] = 3
for j in T.serial(10):
Ab[j + 1] = Ab[i] + 1
with tvm.transform.PassContext(
config={
"tirx.UnrollLoop": {"auto_max_depth": 8, "auto_max_extent": 1, "explicit_unroll": False}
}
):
ret = tvm.tirx.transform.UnrollLoop()(Module)["main"].body
assert isinstance(ret[0], tvm.tirx.BufferStore)
def test_unroll_allocations():
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main():
for i in T.unroll(2):
buf = T.alloc_buffer([16], "float32")
buf[0] = 0.0
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main():
buf1 = T.alloc_buffer([16], "float32")
buf1[0] = 0.0
buf2 = T.alloc_buffer([16], "float32")
buf2[0] = 0.0
after = tvm.tirx.transform.UnrollLoop()(Before)
tvm.ir.assert_structural_equal(after, Expected)
def test_unroll_local_access():
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(B: T.Buffer((64,), "float32")):
for bx in T.thread_binding(4, thread="blockIdx.x"):
for tx in T.thread_binding(4, thread="threadIdx.x"):
A_local = T.alloc_buffer((4,), scope="local")
for i in T.serial(4):
A_local[i] = T.float32(i)
@I.ir_module
class Expected:
@T.prim_func(s_tir=True)
def main(B: T.Buffer((64,), "float32")):
for bx in T.thread_binding(4, thread="blockIdx.x"):
for tx in T.thread_binding(4, thread="threadIdx.x"):
A_local = T.alloc_buffer((4,), scope="local")
A_local[0] = T.float32(0)
A_local[1] = T.float32(1)
A_local[2] = T.float32(2)
A_local[3] = T.float32(3)
with tvm.transform.PassContext(
config={
"tirx.UnrollLoop": {
"auto_max_depth": 0,
"auto_max_extent": 1,
"explicit_unroll": True,
"unroll_local_access": True,
}
}
):
after = tvm.tirx.transform.UnrollLoop()(Before)
after = tvm.tirx.transform.StmtSimplify()(after)
tvm.ir.assert_structural_equal(after, Expected)
if __name__ == "__main__":
test_unroll_local_access()
test_unroll_loop()
test_unroll_fake_loop()
test_unroll_allocations()
@@ -0,0 +1,820 @@
# 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: F841
import pytest
import tvm
import tvm.testing
from tvm.script import ir as I
from tvm.script import tirx as T
from tvm.target.codegen import llvm_version_major
simple_target = tvm.target.Target({"kind": "llvm", "mtriple": "x86_64-linux-gnu"})
sve_target = tvm.target.Target(
{
"kind": "llvm",
"device": "arm_cpu",
"mtriple": "aarch64-linux-gnu",
"mattr": ["+v8.2a", "+sve"],
}
)
@pytest.mark.parametrize("extent, target", [(4, simple_target), (T.vscale() * 4, sve_target)])
def test_vectorize_loop(extent, target):
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((16,), "float32")):
for j in T.vectorized(0, extent):
A[j] = 1
@I.ir_module
class After:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((16,), "float32")):
A[T.Ramp(0, 1, extent)] = T.Broadcast(1, extent)
with tvm.target.Target(target):
mod = tvm.tirx.transform.VectorizeLoop()(Before)
tvm.ir.assert_structural_equal(mod, After)
def test_vectorize_vector():
@I.ir_module
class Module:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((4,), "float32x4"), n: T.int32):
for i in range(n):
for j in T.vectorized(4):
A[j] = T.Broadcast(T.float32(1), 4)
mod = tvm.tirx.transform.VectorizeLoop()(Module)
stmt = mod["main"].body
assert isinstance(stmt, tvm.tirx.For)
assert not isinstance(stmt.body, tvm.tirx.For)
assert len(stmt.body.indices) == 1
assert isinstance(stmt.body.indices[0], tvm.tirx.Ramp)
assert isinstance(stmt.body.value, tvm.tirx.Broadcast)
def test_vectorize_vector_scalable_error():
@I.ir_module
class Module:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((25,), "float32")):
for j in T.vectorized(T.vscale() * 4):
A[j * 4 : j * 4 + 4] = T.Broadcast(T.float32(1), 4)
error_msg = "Creating scalable vectors from existing vectors is not supported."
with tvm.target.Target(sve_target):
with pytest.raises(tvm.error.InternalError, match=error_msg):
tvm.tirx.transform.VectorizeLoop()(Module)
def test_vectorize_vector_scalable_error2():
@I.ir_module
class Module:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((25,), "float32xvscalex4")):
for j in T.vectorized(4):
A[j] = T.Broadcast(T.float32(1), T.vscale() * 4)
error_msg = "Vectorizing over scalable buffer elements is not supported in vectorizer."
with pytest.raises(tvm.error.InternalError, match=error_msg):
tvm.tirx.transform.VectorizeLoop()(Module)
def test_vectorize_vector_scalable_error3():
@I.ir_module
class Module:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((25,), "float32")):
for j in T.vectorized(4):
A[j * T.vscale() * 4 : j * T.vscale() * 4 + T.vscale() * 4] = T.Broadcast(
T.float32(1), T.vscale() * 4
)
error_msg = "Vectorizing over existing scalable vectors is not supported."
with pytest.raises(tvm.error.InternalError, match=error_msg):
with tvm.target.Target(sve_target):
tvm.tirx.transform.VectorizeLoop()(Module)
def test_vectorize_vector_scalable_error4():
@I.ir_module
class Module:
@T.prim_func(private=True, s_tir=True)
def main(A: T.Buffer((25,), "float32")):
for j in T.vectorized(T.vscale() * 4):
A[j * T.vscale() * 4 : j * T.vscale() * 4 + T.vscale() * 4] = T.Broadcast(
T.float32(1), T.vscale() * 4
)
error_msg = "Creating scalable vectors from existing vectors is not supported."
with pytest.raises(tvm.error.InternalError, match=error_msg):
with tvm.target.Target(sve_target):
tvm.tirx.transform.VectorizeLoop()(Module)
def test_vectorize_with_if():
extent = 4
target = simple_target
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(a: T.handle, n: T.int32, x: T.int32):
A = T.match_buffer(a, (25,), "float32")
for i in T.vectorized(extent):
if x < n:
A[i] = A[i] + T.float32(1)
else:
if i < n:
A[i] = T.float32(2)
@I.ir_module
class After:
@T.prim_func(s_tir=True)
def main(a: T.handle, n: T.int32, x: T.int32):
A = T.match_buffer(a, (25,), "float32")
if x < n:
A[T.Ramp(0, 1, extent)] = A[T.Ramp(0, 1, extent)] + T.Broadcast(
T.float32(1), extent
)
else:
for i_s in range(extent):
if i_s < n:
A[i_s] = T.float32(2)
with tvm.target.Target(target):
mod = tvm.tirx.transform.VectorizeLoop()(Before)
tvm.ir.assert_structural_equal(mod, After)
def test_vectorize_if_scalable_extent():
extent = T.vscale() * 4
target = sve_target
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(a: T.handle, n: T.int32, x: T.int32):
A = T.match_buffer(a, (25,), "float32")
for i in T.vectorized(extent):
if x < n:
A[i] = A[i] + T.float32(1)
else:
if i < n:
A[i] = T.float32(2)
@I.ir_module
class After:
@T.prim_func(s_tir=True)
def main(a: T.handle, n: T.int32, x: T.int32):
A = T.match_buffer(a, (25,), "float32")
if x < n:
A[T.Ramp(0, 1, extent)] = A[T.Ramp(0, 1, extent)] + T.Broadcast(
T.float32(1), extent
)
else:
A.vstore(
[T.Ramp(0, 1, T.vscale() * 4)],
T.Broadcast(T.float32(2), T.vscale() * 4),
predicate=T.get_active_lane_mask("uint1xvscalex4", 0, n),
)
with tvm.target.Target(target):
mod = tvm.tirx.transform.VectorizeLoop()(Before)
tvm.ir.assert_structural_equal(mod, After)
@pytest.mark.parametrize("extent, target", [(4, simple_target), (T.vscale() * 4, sve_target)])
def test_vectorize_let(extent, target):
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((25,), "float32")):
for i in T.vectorized(extent):
v: T.let = A[i] + T.float32(1)
A[i] = v + T.float32(2)
@I.ir_module
class After:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((25,), "float32")):
v: T.let = A[T.Ramp(0, 1, extent)] + T.Broadcast(T.float32(1), extent)
A[T.Ramp(0, 1, extent)] = v + T.Broadcast(T.float32(2), extent)
with tvm.target.Target(target):
mod = tvm.tirx.transform.VectorizeLoop()(Before)
tvm.ir.assert_structural_equal(mod, After)
@pytest.mark.parametrize("extent, target", [(4, simple_target), (T.vscale() * 4, sve_target)])
def test_vectorize_with_le_cond(extent, target):
@I.ir_module
class Module:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((16,), "float32"), n: T.int32):
for i in T.vectorized(extent):
if i <= n:
A[i] = A[i] + T.float32(1)
with tvm.target.Target(target):
stmt = tvm.tirx.transform.VectorizeLoop()(Module)["main"].body
# Check that the loop wasn't vectorised
assert isinstance(stmt, tvm.tirx.For)
@pytest.mark.parametrize("extent, target", [(4, simple_target), (T.vscale() * 4, sve_target)])
def test_vectorize_with_ge_cond(extent, target):
@I.ir_module
class Module:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((16,), "float32"), n: T.int32):
for i in T.vectorized(extent):
if i >= n:
A[i] = A[i] + T.float32(1)
with tvm.target.Target(target):
stmt = tvm.tirx.transform.VectorizeLoop()(Module)["main"].body
# Check that the loop wasn't vectorised
assert isinstance(stmt, tvm.tirx.For)
@pytest.mark.parametrize("extent, target", [(4, simple_target), (T.vscale() * 4, sve_target)])
def test_vectorize_if_then_else_scalarize(extent, target):
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((25,), "float32")):
for i in T.vectorized(extent):
A[i] = T.if_then_else(i > 0, A[i] + T.float32(1), A[i])
@I.ir_module
class After:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((25,), "float32")):
for i_s in range(extent):
A[i_s] = T.if_then_else(i_s > 0, A[i_s] + T.float32(1), A[i_s])
with tvm.target.Target(target):
mod = tvm.tirx.transform.VectorizeLoop()(Before)
tvm.ir.assert_structural_equal(mod, After)
@pytest.mark.parametrize("extent, target", [(4, simple_target), (T.vscale() * 4, sve_target)])
def test_vectorize_if_then_else_vector(extent, target):
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((25,), "float32"), n: T.int32):
for i in range(n):
for j in T.vectorized(extent):
A[i * extent + j] = T.if_then_else(i > 0, A[i * extent + j], 0)
@I.ir_module
class After:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((25,), "float32"), n: T.int32):
for i in range(n):
A[T.Ramp(i * extent, 1, extent)] = T.if_then_else(
i > 0, A[T.Ramp(i * extent, 1, extent)], T.Broadcast(0, extent)
)
with tvm.target.Target(target):
mod = tvm.tirx.transform.VectorizeLoop()(Before)
tvm.ir.assert_structural_equal(mod, After)
def test_vectorize_let_if_then_else():
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main():
for i in T.vectorized(4):
if i < 2:
result: T.let[T.int32] = T.if_then_else(i < 1, 1, 2)
@I.ir_module
class After:
@T.prim_func(s_tir=True)
def main():
for i_s in range(4):
if i_s < 2:
result: T.let[T.int32] = T.if_then_else(i_s < 1, 1, 2)
T.evaluate(0)
with tvm.target.Target(simple_target):
mod = tvm.tirx.transform.VectorizeLoop()(Before)
tvm.ir.assert_structural_equal(mod, After)
def test_vectorize_while_fail():
"""A while loop inside a vectorized loop should fail."""
@I.ir_module
class Module:
@T.prim_func(s_tir=True)
def main(
A: T.Buffer((64,), "float32"),
B: T.Buffer((64,), "float32"),
C: T.Buffer((64,), "float32"),
):
# Initialize C to 0
for j in range(64):
C[j] = T.float32(0)
# While loop inside vectorized loop (should fail)
i = T.decl_buffer((1,), "int32", scope="local")
i[0] = 0
for j in T.vectorized(64):
while i[0] < 10:
C[j] = C[j] + A[j] + B[j]
i[0] = i[0] + 1
try:
tvm.compile(Module, target="llvm")
assert False
except RuntimeError as e:
error_msg = str(e).split("\n")[-1]
expected = "A while loop inside a vectorized loop not supported"
assert expected in error_msg
@pytest.mark.parametrize(
"extent, vec_str, target",
[(16, "float32x16", simple_target), (T.vscale() * 8, "float32xvscalex8", sve_target)],
)
def test_vectorize_with_reinterpret(extent, vec_str, target):
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((16,), "int32"), B: T.Buffer((16,), "float32")):
for i in T.vectorized(0, extent):
B[i] = T.reinterpret("float32", A[i])
@I.ir_module
class After:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((16,), "int32"), B: T.Buffer((16,), "float32")):
B[T.Ramp(0, 1, extent)] = T.reinterpret(vec_str, A[T.Ramp(0, 1, extent)])
with tvm.target.Target(target):
mod = tvm.tirx.transform.VectorizeLoop()(Before)
tvm.ir.assert_structural_equal(mod, After)
@pytest.mark.parametrize("extent, target", [(4, simple_target), (T.vscale() * 4, sve_target)])
@pytest.mark.parametrize(
"op",
(
T.Mul,
T.Add,
T.Sub,
T.Div,
T.Mod,
T.FloorDiv,
T.FloorMod,
T.Min,
T.Max,
T.EQ,
T.LT,
T.LE,
T.GE,
T.GT,
T.NE,
),
)
def test_vectorize_binary(op, extent, target):
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((25,), "float32"), B: T.Buffer((25,), "float32")):
for j in T.vectorized(extent):
A[j] = op(T.float32(3), B[j])
@I.ir_module
class After:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((25,), "float32"), B: T.Buffer((25,), "float32")):
A[T.Ramp(0, 1, extent)] = op(T.Broadcast(T.float32(3), extent), B[T.Ramp(0, 1, extent)])
with tvm.target.Target(target):
mod = tvm.tirx.transform.VectorizeLoop()(Before)
tvm.ir.assert_structural_equal(mod, After)
@pytest.mark.parametrize("extent, target", [(4, simple_target), (T.vscale() * 4, sve_target)])
@pytest.mark.parametrize("op", (T.And, T.Or))
def test_vectorize_logical(op, extent, target):
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((25,), "bool"), B: T.Buffer((25,), "bool")):
for j in T.vectorized(extent):
A[j] = op(T.bool(1), B[j])
@I.ir_module
class After:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((25,), "bool"), B: T.Buffer((25,), "bool")):
A[T.Ramp(0, 1, extent)] = op(T.Broadcast(T.bool(1), extent), B[T.Ramp(0, 1, extent)])
with tvm.target.Target(target):
mod = tvm.tirx.transform.VectorizeLoop()(Before)
tvm.ir.assert_structural_equal(mod, After)
@pytest.mark.parametrize("extent, target", [(4, simple_target), (T.vscale() * 4, sve_target)])
def test_vectorize_select(extent, target):
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((25,), "float32"), B: T.Buffer((25,), "float32")):
for j in T.vectorized(extent):
A[j] = T.Select(T.bool(True), A[j], B[j])
@I.ir_module
class After:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((25,), "float32"), B: T.Buffer((25,), "float32")):
A[T.Ramp(0, 1, extent)] = T.Select(
T.Broadcast(T.bool(True), extent),
A[T.Ramp(0, 1, extent)],
B[T.Ramp(0, 1, extent)],
)
with tvm.target.Target(target):
mod = tvm.tirx.transform.VectorizeLoop()(Before)
tvm.ir.assert_structural_equal(mod, After)
@pytest.mark.parametrize(
"extent, vec_str, target",
[(4, "int32x4", simple_target), (T.vscale() * 4, "int32xvscalex4", sve_target)],
)
def test_vectorize_cast(extent, vec_str, target):
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((25,), "int32"), B: T.Buffer((25,), "float32")):
for j in T.vectorized(extent):
A[j] = T.Cast("int32", B[j])
@I.ir_module
class After:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((25,), "int32"), B: T.Buffer((25,), "float32")):
A[T.Ramp(0, 1, extent)] = T.Cast(vec_str, B[T.Ramp(0, 1, extent)])
with tvm.target.Target(target):
mod = tvm.tirx.transform.VectorizeLoop()(Before)
tvm.ir.assert_structural_equal(mod, After)
def test_illegal_extent():
@I.ir_module(check_well_formed=False)
class Mod:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((25,), "int32")):
n = T.Var("n", dtype="int32")
for j in T.vectorized(n):
A[j] = 3
error_msg = "Failed to vectorize loop with extent n for target None"
with pytest.raises(tvm.error.InternalError, match=error_msg):
tvm.tirx.transform.VectorizeLoop()(Mod)
def test_illegal_vscale_in_non_sve_compilation():
@I.ir_module
class Mod:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((16,), "float32")):
for j in T.vectorized(0, 4 * T.vscale()):
A[j] = 13
msg = "Failed to vectorize loop with extent T.vscale\\(\\) \\* 4 for target"
with tvm.target.Target(simple_target):
with pytest.raises(tvm.error.InternalError, match=msg):
tvm.tirx.transform.VectorizeLoop()(Mod)
def test_vectorize_and_predicate_all_buffer_loads_stores():
@T.prim_func(s_tir=True)
def before(a: T.handle, b: T.handle):
A = T.match_buffer(a, (16,), "float32")
B = T.match_buffer(b, (16,), "float32")
T.func_attr({"global_symbol": "main", "tirx.noalias": True})
for i_0 in T.serial(T.ceildiv(14, 4)):
for i_1 in T.vectorized(4):
if i_0 * 4 + i_1 < 14:
B[i_0 * 4 + i_1] = A[i_0 * 4 + i_1] + 1.0
@T.prim_func(s_tir=True)
def expected(a: T.handle, b: T.handle):
A = T.match_buffer(a, (16,), "float32")
B = T.match_buffer(b, (16,), "float32")
T.func_attr({"global_symbol": "main", "tirx.noalias": True})
for i_0 in range(4):
load_a = T.meta_var(
A.vload(
[T.Ramp(i_0 * 4, 1, 4)],
predicate=T.get_active_lane_mask("uint1x4", i_0 * 4, 14),
)
)
add_1 = T.meta_var(load_a + T.Broadcast(T.float32(1), 4))
B.vstore(
[T.Ramp(i_0 * 4, 1, 4)],
add_1,
predicate=T.get_active_lane_mask("uint1x4", i_0 * 4, 14),
)
mod = tvm.IRModule.from_expr(before)
with tvm.transform.PassContext(config={"tirx.enable_buffer_level_predication": True}):
after = tvm.tirx.transform.VectorizeLoop()(mod)["main"]
tvm.ir.assert_structural_equal(after, expected)
def test_vectorize_and_predicate_some_buffer_loads_stores():
# Currently revert to scalarizing the block if not all accesses
# have been predicated, otherwise incorrect code is generated.
@T.prim_func(s_tir=True)
def before(a: T.handle, b: T.handle):
A = T.match_buffer(a, (16,), "float32")
B = T.match_buffer(b, (16,), "float32")
T.func_attr({"global_symbol": "main", "tirx.noalias": True})
for i_0 in T.serial(T.ceildiv(14, 4)):
for i_1 in T.vectorized(4):
if i_0 * 4 + i_1 < 14:
B[i_0 * 4 + i_1] = A[i_0] + 1.0
@T.prim_func(s_tir=True)
def expected(a: T.handle, b: T.handle):
A = T.match_buffer(a, (16,), "float32")
B = T.match_buffer(b, (16,), "float32")
T.func_attr({"global_symbol": "main", "tirx.noalias": True})
for i_0, i_1_s in T.grid(4, 4):
if i_0 * 4 + i_1_s < 14:
B[i_0 * 4 + i_1_s] = A[i_0] + T.float32(1)
mod = tvm.IRModule.from_expr(before)
with tvm.transform.PassContext(config={"tirx.enable_buffer_level_predication": True}):
after = tvm.tirx.transform.VectorizeLoop()(mod)["main"]
tvm.ir.assert_structural_equal(after, expected)
def test_vectorize_and_predicate_multiple_access_statements():
@T.prim_func(s_tir=True)
def before(a: T.handle, b: T.handle):
A = T.match_buffer(a, (16,), "float32")
B = T.match_buffer(b, (16,), "float32")
T.func_attr({"global_symbol": "main", "tirx.noalias": True})
for i_0 in T.serial(T.ceildiv(14, 4)):
for i_1 in T.vectorized(4):
if i_0 * 4 + i_1 < 14:
A[i_0 * 4 + i_1] = 2.0
B[i_0 * 4 + i_1] = 1.0
@T.prim_func(s_tir=True)
def expected(a: T.handle, b: T.handle):
A = T.match_buffer(a, (16,), "float32")
B = T.match_buffer(b, (16,), "float32")
T.func_attr({"global_symbol": "main", "tirx.noalias": True})
for i_0 in range(4):
A.vstore(
[T.Ramp(i_0 * 4, 1, 4)],
T.Broadcast(T.float32(2), 4),
predicate=T.get_active_lane_mask("uint1x4", i_0 * 4, 14),
)
B.vstore(
[T.Ramp(i_0 * 4, 1, 4)],
T.Broadcast(T.float32(1), 4),
predicate=T.get_active_lane_mask("uint1x4", i_0 * 4, 14),
)
before_mod = tvm.IRModule.from_expr(before)
with tvm.transform.PassContext(config={"tirx.enable_buffer_level_predication": True}):
after = tvm.tirx.transform.VectorizeLoop()(before_mod)["main"]
tvm.ir.assert_structural_equal(after, expected)
def test_vectorize_and_predicate_invalid_conditions():
@T.prim_func(s_tir=True)
def before(a: T.handle, b: T.handle):
A = T.match_buffer(a, (16,), "float32")
B = T.match_buffer(b, (16,), "float32")
T.func_attr({"global_symbol": "main", "tirx.noalias": True})
for i_0 in T.serial(T.ceildiv(14, 4)):
for i_1 in T.vectorized(4):
if i_0 * 4 + i_1 > 14:
A[i_0 * 4 + i_1] = 2.0
if 14 < i_0 * 4 + i_1:
A[i_0 * 4 + i_1] = 2.0
if i_0 * 4 + i_1 < i_0 * 4 + i_1:
A[i_0 * 4 + i_1] = 2.0
@T.prim_func(s_tir=True)
def expected(a: T.handle, b: T.handle):
A = T.match_buffer(a, (16,), "float32")
B = T.match_buffer(b, (16,), "float32")
T.func_attr({"global_symbol": "main", "tirx.noalias": True})
for i_0 in range(4):
for i_1_s in range(4):
if i_0 * 4 + i_1_s > 14:
A[i_0 * 4 + i_1_s] = T.float32(2)
for i_1_s in range(4):
if 14 < i_0 * 4 + i_1_s:
A[i_0 * 4 + i_1_s] = T.float32(2)
for i_1_s in range(4):
if i_0 * 4 + i_1_s < i_0 * 4 + i_1_s:
A[i_0 * 4 + i_1_s] = T.float32(2)
before_mod = tvm.IRModule.from_expr(before)
with tvm.transform.PassContext(config={"tirx.enable_buffer_level_predication": True}):
after = tvm.tirx.transform.VectorizeLoop()(before_mod)["main"]
tvm.ir.assert_structural_equal(after, expected)
def test_vectorize_with_explicitly_disabled_buffer_level_predication():
# Since the target has the VLA feature, buffer level predication is enabled
# by default. However, it has been explicitly disabled by the pass context
# option, so no buffer-level predicates should be added.
@T.prim_func(s_tir=True)
def before(a: T.handle, b: T.handle):
A = T.match_buffer(a, (16,), "float32")
B = T.match_buffer(b, (16,), "float32")
T.func_attr({"global_symbol": "main", "tirx.noalias": True})
for i_0 in T.serial(T.ceildiv(14, 4)):
for i_1 in T.vectorized(4):
if i_0 * 4 + i_1 < 14:
B[i_0 * 4 + i_1] = A[i_0 * 4 + i_1] + 1.0
@T.prim_func(s_tir=True)
def expected(a: T.handle, b: T.handle):
A = T.match_buffer(a, (16,), "float32")
B = T.match_buffer(b, (16,), "float32")
T.func_attr({"global_symbol": "main", "tirx.noalias": True})
for i_0, i_1_s in T.grid(4, 4):
if i_0 * 4 + i_1_s < 14:
B[i_0 * 4 + i_1_s] = A[i_0 * 4 + i_1_s] + T.float32(1)
mod = tvm.IRModule.from_expr(before)
with tvm.transform.PassContext(config={"tirx.enable_buffer_level_predication": False}):
with tvm.target.Target(sve_target):
after = tvm.tirx.transform.VectorizeLoop()(mod)["main"]
tvm.ir.assert_structural_equal(after, expected)
def test_vectorize_and_predicate_buffer_load_stores_with_sve_func_attr_target():
@T.prim_func(s_tir=True)
def before(a: T.handle, b: T.handle):
A = T.match_buffer(a, (16,), "float32")
B = T.match_buffer(b, (16,), "float32")
T.func_attr({"global_symbol": "main", "tirx.noalias": True, "target": sve_target})
for i_0 in T.serial(T.ceildiv(14, 4)):
for i_1 in T.vectorized(4):
if i_0 * 4 + i_1 < 14:
B[i_0 * 4 + i_1] = A[i_0 * 4 + i_1] + 1.0
@T.prim_func(s_tir=True)
def expected(a: T.handle, b: T.handle):
A = T.match_buffer(a, (16,), "float32")
B = T.match_buffer(b, (16,), "float32")
T.func_attr({"global_symbol": "main", "tirx.noalias": True, "target": sve_target})
for i_0 in range(4):
load_a = T.meta_var(
A.vload(
[T.Ramp(i_0 * 4, 1, 4)],
predicate=T.get_active_lane_mask("uint1x4", i_0 * 4, 14),
)
)
add_1 = T.meta_var(load_a + T.Broadcast(T.float32(1), 4))
B.vstore(
[T.Ramp(i_0 * 4, 1, 4)],
add_1,
predicate=T.get_active_lane_mask("uint1x4", i_0 * 4, 14),
)
mod = tvm.IRModule.from_expr(before)
after = tvm.tirx.transform.VectorizeLoop()(mod)["main"]
tvm.ir.assert_structural_equal(after, expected)
def test_vectorize_and_predicate_buffer_load_stores_with_sve_attr_scope_target():
@T.prim_func(s_tir=True)
def before(a: T.handle, b: T.handle):
A = T.match_buffer(a, (16,), "float32")
B = T.match_buffer(b, (16,), "float32")
T.func_attr({"global_symbol": "main", "tirx.noalias": True})
with T.attr(sve_target, "target", 0):
for i_0 in T.serial(T.ceildiv(14, 4)):
for i_1 in T.vectorized(4):
if i_0 * 4 + i_1 < 14:
B[i_0 * 4 + i_1] = A[i_0 * 4 + i_1] + 1.0
@T.prim_func(s_tir=True)
def expected(a: T.handle, b: T.handle):
A = T.match_buffer(a, (16,), "float32")
B = T.match_buffer(b, (16,), "float32")
T.func_attr({"global_symbol": "main", "tirx.noalias": True})
with T.attr(sve_target, "target", 0):
for i_0 in range(4):
load_a = T.meta_var(
A.vload(
[T.Ramp(i_0 * 4, 1, 4)],
predicate=T.get_active_lane_mask("uint1x4", i_0 * 4, 14),
)
)
add_1 = T.meta_var(load_a + T.Broadcast(T.float32(1), 4))
B.vstore(
[T.Ramp(i_0 * 4, 1, 4)],
add_1,
predicate=T.get_active_lane_mask("uint1x4", i_0 * 4, 14),
)
mod = tvm.IRModule.from_expr(before)
after = tvm.tirx.transform.VectorizeLoop()(mod)["main"]
tvm.ir.assert_structural_equal(after, expected)
@pytest.mark.parametrize(
"extent, vec_str, target",
[(4, "float32x4", simple_target)],
)
def test_vectorize_llvm_pure_intrin(extent, vec_str, target):
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((25,), "float32"), B: T.Buffer((25,), "float32")):
for j in T.vectorized(extent):
A[j] = T.call_llvm_pure_intrin("float32", "llvm.sqrt", B[j])
@I.ir_module
class After:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((25,), "float32"), B: T.Buffer((25,), "float32")):
A[T.Ramp(0, 1, extent)] = T.call_llvm_pure_intrin(
vec_str, "llvm.sqrt", B[T.Ramp(0, 1, extent)]
)
with tvm.target.Target(target):
mod = tvm.tirx.transform.VectorizeLoop()(Before)
tvm.ir.assert_structural_equal(mod, After)
mod = tvm.compile(mod, target=target)
@pytest.mark.parametrize(
"extent, vec_str, target",
[(4, "int32x4", simple_target)],
)
def test_vectorize_llvm_pure_intrin_fail(extent, vec_str, target):
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((25,), "int32"), B: T.Buffer((25,), "float32")):
for j in T.vectorized(extent):
A[j] = T.call_llvm_pure_intrin("int32", "llvm.lround", B[j])
@I.ir_module
class After:
@T.prim_func(s_tir=True)
def main(A: T.Buffer((25,), "int32"), B: T.Buffer((25,), "float32")):
A[T.Ramp(0, 1, extent)] = T.call_llvm_pure_intrin(
vec_str, "llvm.lround", B[T.Ramp(0, 1, extent)]
)
with tvm.target.Target(target):
mod = tvm.tirx.transform.VectorizeLoop()(Before)
tvm.ir.assert_structural_equal(mod, After)
# LLVM 20 added vector support for llvm.lround/llvm.llround. The IR Verifier's
# "Intrinsic does not support vectors" check was removed in release/20.x, so
# compilation only fails on LLVM <= 19.
if llvm_version_major() >= 20:
tvm.compile(mod, target=target)
else:
with pytest.raises(Exception, match="Intrinsic does not support vectors"):
tvm.compile(mod, target=target)
if __name__ == "__main__":
tvm.testing.main()