blob: db77419665f83e217ef32167b4fb909a31e45357 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ruff: noqa: F401
import tvm
import tvm.testing
from tvm import ir, tir
from tvm.script import ir as I
from tvm.script import tir as T
def test_reuse_in_sequential_let_stmt():
"""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 = tir.Var("var", "int32")
sequential_bindings = tir.SeqStmt(
[
tir.LetStmt(var, 16, tir.Evaluate(var)),
tir.LetStmt(var, 32, tir.Evaluate(var)),
]
)
before = tir.PrimFunc([], sequential_bindings)
@T.prim_func(private=True)
def expected():
with T.LetStmt(T.int32(16)) as var1:
T.evaluate(var1)
with T.LetStmt(T.int32(32)) as var2:
T.evaluate(var2)
mod = tvm.IRModule.from_expr(before)
mod = tvm.tir.transform.ConvertSSA()(mod)
tvm.ir.assert_structural_equal(mod["main"], expected)
def test_reuse_in_nested_let_stmt():
"""De-dup nested bindings
Use of a variable with nested bindings is de-duplicated to refer
to the inner-most binding that contains the use site.
"""
# Manually construct the PrimFunc body, as SSA violations are
# not valid TIR, and may not be expressible in future versions
# of TVMSCript.
var = tir.Var("var", "int32")
inner_let = tir.LetStmt(var, 16, tir.Evaluate(var))
outer_let = tir.LetStmt(
var,
32,
tir.SeqStmt(
[
tir.Evaluate(var),
inner_let,
tir.Evaluate(var),
]
),
)
before = tir.PrimFunc([], outer_let)
@T.prim_func(private=True)
def expected():
with T.LetStmt(T.int32(32)) as outer:
T.evaluate(outer)
with T.LetStmt(T.int32(16)) as inner:
T.evaluate(inner)
T.evaluate(outer)
mod = tvm.IRModule.from_expr(before)
mod = tvm.tir.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)
def func():
with T.LetStmt(10) as var:
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
def func_a():
var = T.int32(10)
T.evaluate(var)
@T.prim_func
def func_b():
var = T.int32(10)
T.evaluate(var)
after = tvm.tir.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 `tir.Var` instance is used for the
parameter `n` in both functions.
"""
@T.prim_func(private=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
def func_a(n: T.int32):
T.evaluate(n)
@T.prim_func
def func_b(n: T.int32):
T.evaluate(n)
after = tvm.tir.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)
def func(a: T.handle("float32")):
A = T.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
def func_a(a: T.handle("float32")):
A = T.Buffer(shape=1, dtype="float32", data=a)
T.evaluate(A[0])
@T.prim_func
def func_b(a: T.handle("float32")):
A = T.Buffer(shape=1, dtype="float32", data=a)
T.evaluate(A[0])
after = tvm.tir.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)
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
def func_a(A: T.Buffer(1, "float32")):
T.evaluate(A[0])
@T.prim_func
def func_b(A: T.Buffer(1, "float32")):
T.evaluate(A[0])
after = tvm.tir.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
def func(A: T.Buffer(1, "float32")):
T.evaluate(A[0])
after = tvm.tir.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 `tir::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
`tir::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
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.tir.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.tir.Var("threadIdx_x", "int32")
# threadIdx_x is defined outside
@I.ir_module(check_well_formed=False)
class before:
@T.prim_func
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
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
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
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.tir.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 `tir.Var` inside the `IterVar`.
"""
threadIdx_x = tvm.tir.Var("threadIdx_x", "int32")
iter_var = tvm.tir.IterVar(
tvm.ir.Range(0, 256), threadIdx_x, tvm.tir.IterVar.ThreadIndex, "threadIdx.x"
)
# complaints of multiple definitions for threadIdx_x
@I.ir_module(check_well_formed=False)
class before:
@T.prim_func
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
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
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
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.tir.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.tir.Var("threadIdx_x", "int32")
iter_var = tvm.tir.IterVar(
tvm.ir.Range(0, 256), threadIdx_x, tvm.tir.IterVar.ThreadIndex, "threadIdx.x"
)
# complaints of multiple definitions of threadIdx_x
@I.ir_module(check_well_formed=False)
class before:
@T.prim_func
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
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
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
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.tir.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 tir.Var"""
# Generate the PrimFunc, which is already SSA
#
# This is constructed directly, rather than using TVMScript.
# This test case requires a `tir.AttrStmt` that references a
# variable, followed by the `tir.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 = tir.Var("i0_outer_outer", "int32")
i0_outer_inner = tir.Var("i0_outer_inner", "int32")
i0_inner = tir.Var("i0_inner", "int32")
A = tir.decl_buffer(1024, "float32", "A")
B = tir.decl_buffer(1024, "float32", "B")
index = i0_outer_outer * 52 + i0_outer_inner * 4 + i0_inner
stmt = tir.BufferStore(B, tir.BufferLoad(A, [index]), [index])
stmt = tir.IfThenElse(i0_outer_outer * 13 + i0_outer_inner < 256, stmt, None)
stmt = tir.For(i0_inner, 0, 4, tir.ForKind.VECTORIZED, stmt)
stmt = tir.For(i0_outer_inner, 0, 13, tir.ForKind.PARALLEL, stmt)
stmt = tir.AttrStmt(
T.iter_var(i0_outer_inner, None, "DataPar", ""),
"pragma_parallal_barrier_when_finish",
1,
stmt,
)
stmt = tir.AttrStmt(
T.iter_var(i0_outer_inner, None, "DataPar", ""),
"pragma_parallal_stride_pattern",
1,
stmt,
)
stmt = tir.For(i0_outer_outer, 0, 20, tir.ForKind.SERIAL, stmt)
stmt = tir.AttrStmt(
T.iter_var(i0_outer_outer, None, "DataPar", ""),
"pragma_parallal_launch_point",
1,
stmt,
)
A_handle = tir.Var("A_handle", "handle")
B_handle = tir.Var("B_handle", "handle")
before = tir.PrimFunc(
[A_handle, B_handle],
stmt,
buffer_map={A_handle: A, B_handle: B},
)
mod = tvm.IRModule.from_expr(before)
after = tvm.tir.transform.ConvertSSA()(mod)
tvm.ir.assert_structural_equal(after["main"], before)
if __name__ == "__main__":
tvm.testing.main()