Files
apache--tvm/tests/python/tirx-transform/test_tir_transform_simplify.py
T
wehub-resource-sync 26446540fa
Lint / lint (push) Waiting to run
CI / MacOS (push) Waiting to run
CI / Windows (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:36:25 +08:00

1318 lines
46 KiB
Python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import tvm
import tvm.testing
from tvm.script import ir as I
from tvm.script import tirx as T
def test_stmt_simplify():
@T.prim_func(private=True, s_tir=True)
def func(A: T.handle("float32"), C: T.handle("float32"), n: T.int32):
A_ptr = T.decl_buffer((10,), "float32", data=A)
C_ptr = T.decl_buffer((10,), "float32", data=C)
n_val: T.let[T.int32] = 10
for i in T.serial(n_val):
if i < 12:
A_ptr[i] = C_ptr[i]
mod = tvm.IRModule.from_expr(func)
body = tvm.tirx.transform.StmtSimplify()(mod)["main"].body
# Navigate through DeclBuffer nodes to reach the inner body
while isinstance(body, tvm.tirx.DeclBuffer):
body = body.body
# After simplification, Bind is kept (not inlined) but the if is eliminated
# since i < 12 is always true for i in 0..10.
# Body is SeqStmt(Bind(n_val, 10), For(i, ...))
stmts = body if isinstance(body, tvm.tirx.SeqStmt) else [body]
# Find the For loop in the sequence
for_stmt = [s for s in stmts if isinstance(s, tvm.tirx.For)]
assert len(for_stmt) == 1, f"Expected one For loop, got {len(for_stmt)}"
assert isinstance(for_stmt[0].body, tvm.tirx.BufferStore)
def test_thread_extent_simplify():
@T.prim_func(private=True, s_tir=True)
def func(A: T.handle("float32"), C: T.handle("float32"), n: T.int32):
A_ptr = T.decl_buffer((10,), "float32", data=A)
C_ptr = T.decl_buffer((10,), "float32", data=C)
n_val: T.let[T.int32] = 10
for tx in T.thread_binding(n_val, thread="threadIdx.x"):
for ty in T.thread_binding(1, thread="threadIdx.y"):
if tx + ty < 12:
A_ptr[tx] = C_ptr[tx + ty]
mod = tvm.IRModule.from_expr(func)
body = tvm.tirx.transform.StmtSimplify()(mod)["main"].body
# Navigate through DeclBuffer nodes to reach the inner body
while isinstance(body, tvm.tirx.DeclBuffer):
body = body.body
# After simplification: Bind is kept but the if is eliminated
# since tx + ty < 12 is always true for tx in 0..10 and ty = 0.
stmts = list(body) if isinstance(body, tvm.tirx.SeqStmt) else [body]
for_stmts = [s for s in stmts if isinstance(s, tvm.tirx.For)]
assert len(for_stmts) >= 1, f"Expected For loop, got stmts: {[type(s).__name__ for s in stmts]}"
# The outermost For is the tx loop
tx_loop = for_stmts[0]
assert isinstance(tx_loop, tvm.tirx.For) # tx loop
assert isinstance(tx_loop.body, tvm.tirx.For) # ty loop
assert isinstance(tx_loop.body.body, tvm.tirx.BufferStore) # The if was eliminated
def test_if_likely():
@T.prim_func(private=True, s_tir=True)
def func(A: T.handle("float32"), C: T.handle("float32"), n: T.int32):
A_ptr = T.decl_buffer((32,), "float32", data=A)
C_ptr = T.decl_buffer((1024,), "float32", data=C)
for tx in T.thread_binding(32, thread="threadIdx.x"):
for ty in T.thread_binding(32, thread="threadIdx.y"):
if T.likely(tx * 32 + ty < n):
if T.likely(tx * 32 + ty < n):
A_ptr[tx] = C_ptr[tx * 32 + ty]
mod = tvm.IRModule.from_expr(func)
body = tvm.tirx.transform.StmtSimplify()(mod)["main"].body
# With flat semantics, skip DeclBuffer/AllocBuffer siblings to find the For
if isinstance(body, tvm.tirx.SeqStmt):
for_stmts = [s for s in body.seq if isinstance(s, tvm.tirx.For)]
body = for_stmts[0] if for_stmts else body
# Structure: For(tx) -> For(ty) -> IfThenElse
assert isinstance(body.body.body, tvm.tirx.IfThenElse)
assert not isinstance(body.body.body.then_case, tvm.tirx.IfThenElse)
def test_loop_body_knows_dynamic_extent_is_positive():
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer((1,), "float32"), m: T.int32, n: T.int32):
for i in T.serial(m, n // 4):
if n // 4 - m > 0:
A[0] = 1.0
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer((1,), "float32"), m: T.int32, n: T.int32):
for i in T.serial(m, n // 4):
A[0] = 1.0
after = tvm.tirx.transform.StmtSimplify()(tvm.IRModule.from_expr(before))["main"]
tvm.ir.assert_structural_equal(after, expected)
def _apply_simplify(
func,
transitively_prove_inequalities=False,
convert_boolean_to_and_of_ors=False,
apply_constraints_to_boolean_branches=False,
):
"""Helper to apply simplify transform with config options."""
config = {
"tirx.StmtSimplify": {
"transitively_prove_inequalities": transitively_prove_inequalities,
"convert_boolean_to_and_of_ors": convert_boolean_to_and_of_ors,
"apply_constraints_to_boolean_branches": apply_constraints_to_boolean_branches,
}
}
mod = tvm.IRModule.from_expr(func)
with tvm.transform.PassContext(config=config):
mod = tvm.tirx.transform.StmtSimplify()(mod)
return mod["main"]
def test_load_store_noop():
"""Store of a value that was just read from the same location is a no-op."""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer((1,), "float32")):
A[0] = A[0]
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer((1,), "float32")):
T.evaluate(0)
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_load_store_noop_after_simplify():
"""As test_load_store_noop, but requiring simplification to identify.
Previously, a bug caused the self-assignment of a buffer to
checked based on the pre-simplification assignment, not the
post-simplification. This test is to identify any similar
regression.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer((1,), "float32")):
A[0] = A[0] + (5.0 - 5.0)
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer((1,), "float32")):
T.evaluate(0)
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_nested_condition():
"""Nested IfThenElse with the same condition can be simplified.
Requires const_int_bound to narrow scope of i within the
conditional, or for rewrite_simplify to recognize the literal
constraint.
"""
@T.prim_func(private=True, s_tir=True)
def before(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 expected(A: T.Buffer((16,), "float32")):
for i in T.serial(16):
if i == 5:
A[i] = 0.0
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_nested_provable_condition():
"""Simplify inner conditional using constraint from outer.
Requires const_int_bound to narrow scope of i within the
conditional.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer((16,), "float32")):
for i in T.serial(16):
if i == 5:
if i < 7:
A[i] = 0.0
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer((16,), "float32")):
for i in T.serial(16):
if i == 5:
A[i] = 0.0
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_nested_var_condition():
"""Simplify inner conditional using constraint from outer.
Requires for rewrite_simplify to recognize the repeated
constraint.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer((16,), "float32"), n: T.int32):
for i in T.serial(16):
if i == n:
if i == n:
A[i] = 0.0
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer((16,), "float32"), n: T.int32):
for i in T.serial(16):
if i == n:
A[i] = 0.0
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_altered_buffer_contents():
"""No simplification of data-dependent conditionals.
A literal constraint must not be propagated if the values
referenced may change. TIR requires single assignment of
variables, so Var objects may be assumed constant, but BufferLoad
may not.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer((1,), "int32"), n: T.int32):
if A[0] == n:
A[0] = A[0] + 1
if A[0] == n:
A[0] = 0
expected = before
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_negation_of_condition():
"""Use negation of outer condition to simplify innner.
Within the body of an if statement, the negation of the
condition is known to be false.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer((16,), "int32")):
for i in T.serial(16):
if i == 5:
if i != 5:
A[i] = 0
else:
A[i] = 1
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer((16,), "int32")):
for i in T.serial(16):
if i == 5:
A[i] = 1
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_negation_of_not_equal():
"""As test_negation_of_var_condition, but with a != outer condition.
Because ConstIntBoundAnalyzer only tracks the min and max allowed
values, the outer i!=5 condition does provide a constraint on the
bounds. This test relies on RewriteSimplifier to recognize
``i==5`` as the negation of a literal constraint.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer((16,), "int32")):
for i in T.serial(16):
if i != 5:
if i == 5:
A[i] = 0
else:
A[i] = 1
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer((16,), "int32")):
for i in T.serial(16):
if i != 5:
A[i] = 1
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_negation_of_var_condition():
"""As test_negation_of_var_condition, but with a dynamic condition.
This simplification cannot be done with ConstIntBoundAnalyzer, and
must rely on RewriteSimplifier recognizing the repeated literal.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer((16,), "int32"), n: T.int32):
for i in T.serial(16):
if i == n:
if i != n:
A[i] = 0
else:
A[i] = 1
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer((16,), "int32"), n: T.int32):
for i in T.serial(16):
if i == n:
A[i] = 1
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_literal_constraint_split_boolean_and():
"""Split a boolean AND into independent constraints
A single if condition may impose multiple literal constraints.
Each constraint that is ANDed together to form the condition
should be treated as an independent constraint. The use of n in
the condition is to ensure we exercise RewriteSimplifier.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer((16, 16), "int32"), n: T.int32):
for i, j in T.grid(16, 16):
if i == n and j == n:
if i == n:
A[i, j] = 0
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer((16, 16), "int32"), n: T.int32):
for i, j in T.grid(16, 16):
if i == n and j == n:
A[i, j] = 0
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_literal_constraint_split_boolean_or():
"""Split a boolean OR into independent constraints
Similar to test_literal_constraint_split_boolean_and, but splitting a
boolean OR into independent conditions. This uses the
simplification that ``!(x || y) == !x && !y``.
The use of ``n`` in the condition is to ensure we exercise
RewriteSimplifier.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer((16, 16), "int32"), n: T.int32):
for i, j in T.grid(16, 16):
if i == n or j == n:
A[i, j] = 0
else:
if i == n:
A[i, j] = 1
else:
A[i, j] = 2
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer((16, 16), "int32"), n: T.int32):
for i, j in T.grid(16, 16):
if i == n or j == n:
A[i, j] = 0
else:
A[i, j] = 2
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_prove_condition_using_let():
"""Simplify conditions using non-inlined let bindings
Not all let bindings are inlined when they occur in later
expressions. However, even if they are not inlined, they may be
used to prove the value of a condition.
With flat Bind, the analyzer binds the variable to its value for
constraint proving, which also substitutes the variable in later
expressions.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(4, "bool")):
for i in T.serial(4):
condition: T.let[T.bool] = i < 3
if condition or i >= 3:
A[i] = condition
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(4, "bool")):
for i in T.serial(4):
condition: T.let[T.bool] = i < 3 # noqa: F841
A[i] = i < 3
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_prove_let_condition():
"""Simplify conditions using non-inlined let bindings
With flat Bind, analyzer binds variable to value, which also
substitutes the variable in later expressions.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(4, "bool")):
for i in T.serial(4):
condition: T.let[T.bool] = i < 3
if i < 3:
if condition:
A[i] = condition
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(4, "bool")):
for i in T.serial(4):
condition: T.let[T.bool] = i < 3 # noqa: F841
if i < 3:
A[i] = T.bool(True)
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_prove_repeated_let_condition():
"""Simplify conditions using non-inlined let bindings
With analyzer Bind, the variable is substituted with its value,
so `if condition` becomes `if i < 3`, and within that context
the inner `if condition` simplifies to True and is eliminated.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(4, "bool")):
for i in T.serial(4):
condition: T.let[T.bool] = i < 3
if condition:
if condition:
A[i] = condition
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(4, "bool")):
for i in T.serial(4):
condition: T.let[T.bool] = i < 3 # noqa: F841
if i < 3:
A[i] = T.bool(True)
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_if_then_else_expr():
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(16, "float32")):
for i in T.serial(16):
if i < 12:
A[i] = T.if_then_else(i < 12, 1.0, 2.0, dtype="float32")
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(16, "float32")):
for i in T.serial(16):
if i < 12:
A[i] = 1.0
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_ceil_log2_int():
"""Simplify expressions resulting from topi.math.ceil_log2"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(1, "int32")):
A[0] = T.cast(
T.ceil(T.log2(T.cast(14, "float64"), dtype="float64"), dtype="float64"), dtype="int32"
)
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(1, "int32")):
A[0] = 4
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_left_ceil_log2_lower_bound():
"""Integer bounds are propagated through topi.math.ceil_log2
With flat Bind, the Bind is kept even when the variable is unused
after simplification. The if condition is still eliminated.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(16, "float32")):
for i in T.serial(16):
x: T.let[T.int32] = T.cast(
T.ceil(T.log2(T.cast(i + 1024 + 1, "float64"), dtype="float64"), dtype="float64"),
dtype="int32",
)
if x == 11:
A[i] = 0.0
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(16, "float32")):
for i in T.serial(16):
x: T.let[T.int32] = T.Cast( # noqa: F841
"int32",
T.ceil(T.log2(T.Cast("float64", i + 1025))),
)
A[i] = T.float32(0)
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_left_shift_lower_bound():
"""Integer bounds are propagated through left shift
min(1 << i) = 1 << min(i)
= 1 << 0
= 1
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(16, "float32")):
for i in T.serial(16):
if T.shift_left(1, i, dtype="int32") >= 1:
A[i] = 0.0
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(16, "float32")):
for i in T.serial(16):
A[i] = 0.0
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_left_shift_upper_bound():
"""Integer bounds are propagated through left shift
max(31 << i) = 31 << max(i)
= 31 << 15
= 1015808
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(16, "float32")):
for i in T.serial(16):
if T.shift_left(31, i, dtype="int32") <= 1015808:
A[i] = 0.0
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(16, "float32")):
for i in T.serial(16):
A[i] = 0.0
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_left_shift_of_negative_value():
"""No const int bounds of left shift of negative value.
This is target dependent, and does not currently have a specified
behavior in TIR. For example, in CodeGenC, this generates C code
with undefined behavior.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(16, "float32")):
for i in T.serial(16):
if -64 <= T.shift_left(-i, 4, dtype="int32"):
A[i] = 0.0
expected = before
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_left_shift_by_negative_value():
"""No const int bounds of left shift by negative bit count.
This is target dependent, and does not currently have a specified
behavior in TIR. For example, in CodeGenC, this generates C code
with undefined behavior.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(16, "float32")):
for i in T.serial(16):
if T.shift_left(16, -i, dtype="int32") <= 16:
A[i] = 0.0
expected = before
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_remove_transitively_provable_condition():
"""Remove comparisons that may be proven using multiple others
For example, the `0 < i` and `i <= j` conditions can be used to prove
that `0 < j`.
"""
i, j, k = [tvm.tirx.Var(name, "int32") for name in "ijk"]
zero = tvm.tirx.IntImm("int32", 0)
test_cases = [
(tvm.tirx.all(zero < i, i <= j), zero < j, True),
# Transitive comparisons from LT
(tvm.tirx.all(i < j, j < k), i < k, True),
(tvm.tirx.all(i < j, j == k), i < k, True),
(tvm.tirx.all(i < j, j <= k), i < k, True),
(tvm.tirx.all(i < j, j > k), i < k, False),
(tvm.tirx.all(i < j, j >= k), i < k, False),
(tvm.tirx.all(i < j, j != k), i < k, False),
# Transitive comparisons from LE
(tvm.tirx.all(i <= j, j < k), i < k, True),
(tvm.tirx.all(i <= j, j == k), i == k, False),
(tvm.tirx.all(i <= j, j == k), i <= k, True),
(tvm.tirx.all(i <= j, j <= k), i <= k, True),
(tvm.tirx.all(i <= j, j <= k), i < k, False),
(tvm.tirx.all(i <= j, j > k), i < k, False),
(tvm.tirx.all(i <= j, j >= k), i < k, False),
(tvm.tirx.all(i <= j, j != k), i < k, False),
# Transitive comparisons from GT
(tvm.tirx.all(i > j, j > k), i > k, True),
(tvm.tirx.all(i > j, j == k), i > k, True),
(tvm.tirx.all(i > j, j >= k), i > k, True),
(tvm.tirx.all(i > j, j < k), i > k, False),
(tvm.tirx.all(i > j, j <= k), i > k, False),
(tvm.tirx.all(i > j, j != k), i > k, False),
# Transitive comparisons from GE
(tvm.tirx.all(i >= j, j > k), i > k, True),
(tvm.tirx.all(i >= j, j == k), i == k, False),
(tvm.tirx.all(i >= j, j == k), i >= k, True),
(tvm.tirx.all(i >= j, j >= k), i >= k, True),
(tvm.tirx.all(i >= j, j >= k), i > k, False),
(tvm.tirx.all(i >= j, j < k), i > k, False),
(tvm.tirx.all(i >= j, j <= k), i > k, False),
(tvm.tirx.all(i >= j, j != k), i > k, False),
# GT or LT may be used to prove NE
(tvm.tirx.all(i == j, j != k), i != k, True),
(tvm.tirx.all(i == j, j < k), i != k, True),
(tvm.tirx.all(i == j, j > k), i != k, True),
(tvm.tirx.all(i == j, j != k), i < k, False),
(tvm.tirx.all(i == j, j != k), i > k, False),
# Because these are integers, x<y is equivalent to x <= y-1,
# and may be used in equivalent simplifications.
(tvm.tirx.all(i <= j - 1, j < k), i < k, True),
(tvm.tirx.all(i <= j - 1, j == k), i < k, True),
(tvm.tirx.all(i <= j - 1, j <= k), i < k, True),
(tvm.tirx.all(i <= j - 1, j > k), i < k, False),
(tvm.tirx.all(i <= j - 1, j >= k), i < k, False),
(tvm.tirx.all(i <= j - 1, j != k), i < k, False),
# Either or both inequalities may have an additive offset.
(tvm.tirx.all(i <= j + 5, j <= k + 7), i <= k + 12, True),
(tvm.tirx.all(i <= j + 5, j <= k + 7), i <= k + 11, False),
# For floats, x < y + c1 and y < z + c2 implies that x < z + (c1 + c2).
# Because this simplification applies to integers, transitive
# application of LT or GT can give a tighter constraint.
#
# i < j + c1, j < k + c2
# i <= j + c1 - 1, j <= k + c2 - 1
# i + 1 - c1 <= j, j <= k + c2 - 1
# i + 1 - c1 <= k + c2 - 1
# i <= k + c1 + c2 - 2
# i < k + (c1 + c2 - 1)
#
(tvm.tirx.all(i < j + 5, j < k + 7), i < k + 11, True),
(tvm.tirx.all(i < j + 5, j < k + 7), i < k + 10, False),
]
analyzer = tvm.arith.Analyzer()
for priors, postulate, provable in test_cases:
# well formed checker complains of undefined variables in condition
@T.prim_func(private=True, check_well_formed=False, s_tir=True)
def before_func(A: T.Buffer(1, "bool")):
if priors:
A[0] = postulate
priors_simplified = analyzer.canonical_simplify(priors)
if provable:
# well formed checker complains of undefined variables in condition
@T.prim_func(private=True, check_well_formed=False, s_tir=True)
def expected_func(A: T.Buffer(1, "bool")):
if priors_simplified:
A[0] = True
else:
postulate_simplified = analyzer.canonical_simplify(postulate)
# well formed checker complains of undefined variables in condition
@T.prim_func(private=True, check_well_formed=False, s_tir=True)
def expected_func(A: T.Buffer(1, "bool")):
if priors_simplified:
A[0] = postulate_simplified
after = _apply_simplify(before_func, transitively_prove_inequalities=True)
tvm.ir.assert_structural_equal(after, expected_func)
def test_suppress_transitively_provable_condition():
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(1, "bool"), i: T.int32, j: T.int32, k: T.int32):
if i < j and j < k:
A[0] = i < k
expected = before
after = _apply_simplify(before, transitively_prove_inequalities=False)
tvm.ir.assert_structural_equal(after, expected)
def test_rewrite_as_and_of_ors():
"""If enabled, rewrite boolean expressions into AND of OR"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(3, "bool")):
T.evaluate(A[0] or (A[1] and A[2]))
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(3, "bool")):
T.evaluate((A[0] or A[1]) and (A[0] or A[2]))
after = _apply_simplify(before, convert_boolean_to_and_of_ors=True)
tvm.ir.assert_structural_equal(after, expected)
def test_suppress_rewrite_as_and_of_ors():
"""Only rewrite into AND of OR when allowed"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(3, "bool")):
T.evaluate(A[0] or (A[1] and A[2]))
expected = before
after = _apply_simplify(before, convert_boolean_to_and_of_ors=False)
tvm.ir.assert_structural_equal(after, expected)
def test_rewrite_as_and_of_ors_with_top_level_and():
"""The expression being rewritten may start with an AND
Like test_rewrite_as_and_of_ors, but with an AndNode as the outermost
booelan operator. Even though it is primarily OR nodes that are
being rewritten, the call to SimplifyAsAndOfOrs should apply to
the outermost AndNode or OrNode in order to enable better
simplification.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(4, "bool")):
T.evaluate((A[0] or A[1]) and (A[1] or (A[0] and A[2] and A[3])))
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(4, "bool")):
# If the simplification is applied to the OrNode, then a
# redundant `(A[1] or A[0])` would't be canceled out. When
# applying SimplifyAsAndOfOrs to the top-level AndNode, the
# internal representation is `[[0,1], [1,0], [1,2], [1,3]]`, and
# the redundant `[1,0]` can be removed.
#
# If the simplification were only applied when encountering an
# OrNode, the internal representation would be `[[0,1]]` during
# the first call and `[[1,0], [1,2], [1,3]]` during the second
# call. As a result, the `[0,1]` and `[1,0]` representations
# wouldn't occur within the same call, and the redundant `[1,0]`
# wouldn't be removed.
T.evaluate((A[0] or A[1]) and (A[1] or A[2]) and (A[1] or A[3]))
after = _apply_simplify(before, convert_boolean_to_and_of_ors=True)
tvm.ir.assert_structural_equal(after, expected)
def test_rewrite_as_and_of_ors_with_simplification_between_groups():
"""Apply rewrite rules between OR groups that differ by a single element
The expression `(k==20 and k!=30)` could be rewritten into `(k==20)`.
However, by default these two terms must appear as part of an explicit part
of the simplified expression. The AndOfOr simplification checks for
rewrite patterns of the form `(A or B) and (A or C)`, where `(B and C)` can
simplify to a single expression `D`. These can be rewritten to `(A or D)`.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(1, "bool"), i: T.int32, j: T.int32, k: T.int32):
A[0] = (i == 0 or j == 10 or k == 20) and (i == 0 or j == 10 or k != 30)
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(1, "bool"), i: T.int32, j: T.int32, k: T.int32):
A[0] = i == 0 or j == 10 or k == 20
after = _apply_simplify(before, convert_boolean_to_and_of_ors=True)
tvm.ir.assert_structural_equal(after, expected)
def test_rewrite_as_and_of_ors_with_simplification_between_reordered_groups():
"""Rewrite rules between OR groups do not depend on order
Like test_rewrite_as_and_of_ors_with_simplification_between_groups, but the groups
are ordered differently. If this removes a group entirely, the result is
ordered according to the first group in the expression.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(1, "bool"), i: T.int32, j: T.int32, k: T.int32):
A[0] = (i == 0 or j == 10 or k == 20) and (j == 10 or k != 30 or i == 0)
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(1, "bool"), i: T.int32, j: T.int32, k: T.int32):
A[0] = j == 10 or k == 20 or i == 0
after = _apply_simplify(before, convert_boolean_to_and_of_ors=True)
tvm.ir.assert_structural_equal(after, expected)
def test_rewrite_as_and_of_or_using_simplification_across_and():
"""Apply AndNode rewrites to non-adjacent expressions
The RewriteSimplifier rules only check for simplifications between
left/right branches of an And/Or node. Simplifications that would require
rearranging components in a chain of And/Or nodes are not performed.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(1, "bool"), i: T.int32, j: T.int32, k: T.int32):
A[0] = (k == 20) and ((i == 0 or j == 10) and (k != 30))
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(1, "bool"), i: T.int32, j: T.int32, k: T.int32):
A[0] = (i == 0 or j == 10) and (k == 20)
after = _apply_simplify(before, convert_boolean_to_and_of_ors=True)
tvm.ir.assert_structural_equal(after, expected)
def test_rewrite_as_and_of_or_using_simplification_within_or():
"""Rewrite rules between OR groups do not depend on order
The RewriteSimplifier rules only check for simplifications between
left/right branches of an And/Or node. Simplifications that would require
rearranging components in a chain of And/Or nodes are not performed.
This test validates that `(i == 20) or (i != 30)` can be rewritten to
`(i != 30)`, even when there's an intervening clause between the
clauses being simplified.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(1, "bool"), i: T.int32, j: T.int32, k: T.int32):
A[0] = (i == 20) or (j == 0) or (i != 30)
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(1, "bool"), i: T.int32, j: T.int32, k: T.int32):
A[0] = (j == 0) or (i != 30)
after = _apply_simplify(before, convert_boolean_to_and_of_ors=True)
tvm.ir.assert_structural_equal(after, expected)
def test_conditional_floor_mod():
"""A regression test for negative floormod denominator
Previously, simplifying this function could throw an error. First, the
`canonical_simplify` would rewrite `floormod(0-i,2)` to the equivalent
`floormod(i,-2)`. Then, the rewrite_simplifier would enter a
constrained context in which `floormod(i,-2)==1`. Passing this
expression to `ModularSet::EnterConstraint`, which previously did not
support a negative value for the second argument, threw an error.
The analogous failure mode never occurred for `truncmod`, because
`truncmod(0-i,2)` would be canonicalized to `truncmod(i, -2) * -1`, and
the pattern matching in `ModularSet` didn't recognize the constant
factor.
This failure mode was resolved by supporting negative arguments in
`ModularSet`, using the same sign convention as is used by
`canonical_simplify`.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(1, "bool"), i: T.int32):
if T.floormod(0 - i, 2) == 0:
A[0] = T.floormod(i, 2) == 0
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(1, "bool"), i: T.int32):
if T.floormod(i, -2) == 0:
A[0] = True
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_simplify_rhs_of_boolean_and_using_lhs():
"""Boolean expressions can introduce contexts.
In `A and B`, the result of `B` only matters when `A` is
true, and can be simplified under that context. This test
simplifies `n < 10` under the assumption that `n < 5`.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(1, "bool"), n: T.int32):
A[0] = n < 5 and n < 10
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(1, "bool"), n: T.int32):
A[0] = n < 5
after = _apply_simplify(before, apply_constraints_to_boolean_branches=True)
tvm.ir.assert_structural_equal(after, expected)
def test_simplify_lhs_of_boolean_and_using_rhs():
"""Boolean expressions can introduce contexts for their arguments.
Like test_simplify_rhs_of_boolean_and_using_lhs, but using the RHS to
simplify the LHS.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(1, "bool"), n: T.int32):
A[0] = n < 10 and n < 5
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(1, "bool"), n: T.int32):
A[0] = n < 5
after = _apply_simplify(before, apply_constraints_to_boolean_branches=True)
tvm.ir.assert_structural_equal(after, expected)
def test_simplify_rhs_of_boolean_or_using_lhs():
"""Boolean expressions can introduce contexts.
In `A or B`, the result of `B` only matters when `A` is false, so
`B` can be simplified under the assumption that `A` is false.
This test simplifies `n < 5` under the assumption that `!(n < 10)`
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(1, "bool"), n: T.int32):
A[0] = n < 10 or n < 5
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(1, "bool"), n: T.int32):
A[0] = n < 10
after = _apply_simplify(before, apply_constraints_to_boolean_branches=True)
tvm.ir.assert_structural_equal(after, expected)
def test_simplify_lhs_of_boolean_or_using_rhs():
"""Boolean expressions can introduce contexts for their arguments.
Like test_simplify_rhs_of_boolean_or_using_lhs, but using the RHS to
simplify the LHS.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(1, "bool"), n: T.int32):
A[0] = n < 5 or n < 10
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(1, "bool"), n: T.int32):
A[0] = n < 10
after = _apply_simplify(before, apply_constraints_to_boolean_branches=True)
tvm.ir.assert_structural_equal(after, expected)
def test_simplify_rhs_of_boolean_and_using_lhs_without_const():
"""Boolean expressions can introduce contexts.
Like test_simplify_rhs_of_boolean_and_using_lhs, but with variables in
the conditions, preventing ConstIntBoundAnalyzer from handling it.
This proof requires the extension to transitively prove
inequalities.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(1, "bool"), n: T.int32, m: T.int32):
A[0] = n < m + 5 and n < m + 10
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(1, "bool"), n: T.int32, m: T.int32):
A[0] = n < m + 5
after = _apply_simplify(
before, apply_constraints_to_boolean_branches=True, transitively_prove_inequalities=True
)
tvm.ir.assert_structural_equal(after, expected)
def test_simplify_lhs_of_boolean_and_using_rhs_without_const():
"""Boolean expressions can introduce contexts for their arguments.
Like test_simplify_lhs_of_boolean_and_using_rhs, but with variables in
the conditions, preventing ConstIntBoundAnalyzer from handling it.
This proof requires the extension to transitively prove
inequalities.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(1, "bool"), n: T.int32, m: T.int32):
A[0] = n < m + 10 and n < m + 5
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(1, "bool"), n: T.int32, m: T.int32):
A[0] = n < m + 5
after = _apply_simplify(
before, apply_constraints_to_boolean_branches=True, transitively_prove_inequalities=True
)
tvm.ir.assert_structural_equal(after, expected)
def test_simplify_rhs_of_boolean_or_using_lhs_without_const():
"""Boolean expressions can introduce contexts.
Like test_simplify_rhs_of_boolean_or_using_lhs, but with variables in the
conditions, preventing ConstIntBoundAnalyzer from handling it.
This proof requires the extension to transitively prove
inequalities.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(1, "bool"), n: T.int32, m: T.int32):
A[0] = n < m + 10 or n < m + 5
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(1, "bool"), n: T.int32, m: T.int32):
A[0] = n < m + 10
after = _apply_simplify(
before, apply_constraints_to_boolean_branches=True, transitively_prove_inequalities=True
)
tvm.ir.assert_structural_equal(after, expected)
def test_simplify_lhs_of_boolean_or_using_rhs_without_const():
"""Boolean expressions can introduce contexts for their arguments.
Like test_simplify_lhs_of_boolean_or_using_rhs, but with variables in the
conditions, preventing ConstIntBoundAnalyzer from handling it.
This proof requires the extension to transitively prove
inequalities.
"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(1, "bool"), n: T.int32, m: T.int32):
A[0] = n < m + 5 or n < m + 10
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(1, "bool"), n: T.int32, m: T.int32):
A[0] = n < m + 10
after = _apply_simplify(
before, apply_constraints_to_boolean_branches=True, transitively_prove_inequalities=True
)
tvm.ir.assert_structural_equal(after, expected)
def test_provable_condition_with_offset():
"""Use scoped-constraint to prove inequalities"""
@T.prim_func(private=True, s_tir=True)
def before(A: T.Buffer(1, "bool"), i: T.int32, j: T.int32):
if i < j:
A[0] = i < j + 1
@T.prim_func(private=True, s_tir=True)
def expected(A: T.Buffer(1, "bool"), i: T.int32, j: T.int32):
if i < j:
A[0] = True
after = _apply_simplify(before, transitively_prove_inequalities=False)
tvm.ir.assert_structural_equal(after, expected)
def test_most_restrictive_conditional():
"""Preferentially prove part of a compound conditional.
Even if we cannot prove a conditional as true or false on its own,
proving that a conditional must satisfy a stronger condition may
allow for later rewrites. For example, if it is known that `a <= b`,
then `a >= b` cannot be proven, but can be reduced to `a == b`.
"""
i, j, k = [tvm.tirx.Var(name, "int32") for name in "ijk"]
tir_int = tvm.tirx.IntImm("int32", 0)
test_cases = [
(i <= tir_int, tir_int <= i, i == tir_int),
(i <= tir_int, i != tir_int, i < tir_int),
(i != tir_int, i <= tir_int, i < tir_int),
(i != tir_int, tir_int <= i, tir_int < i),
(i <= j, j <= i, j == i),
(i <= j, i != j, i < j),
(i != j, i <= j, i < j),
(i != j, j <= i, j < i),
]
for priors, expr_before, expr_after in test_cases:
# well formed checker complains of undefined variables in condition
@T.prim_func(private=True, check_well_formed=False, s_tir=True)
def before_func(A: T.Buffer(1, "bool")):
if priors:
A[0] = expr_before
# well formed checker complains of undefined variables in condition
@T.prim_func(private=True, check_well_formed=False, s_tir=True)
def expected_func(A: T.Buffer(1, "bool")):
if priors:
A[0] = expr_after
after = _apply_simplify(before_func)
tvm.ir.assert_structural_equal(after, expected_func)
def test_simplify_trivial_let_buffer_var():
"""A Bind used in a buffer definition should be retained"""
@T.prim_func(private=True, s_tir=True)
def before(A_ptr: T.handle("float32")):
A_ptr_redef: T.let[T.handle("float32")] = A_ptr
A = T.decl_buffer(1, "float32", data=A_ptr_redef)
A[0] = 42.0
expected = before
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_simplify_trivial_let_elem_offset():
"""A Bind used in a buffer definition should be retained"""
@T.prim_func(private=True, s_tir=True)
def before(A_ptr: T.handle("float32"), A_offset: T.int32):
A_offset_redef = A_offset
A = T.decl_buffer(1, "float32", elem_offset=A_offset_redef, data=A_ptr)
A[0] = 42.0
@T.prim_func(private=True, s_tir=True)
def expected(A_ptr: T.handle("float32"), A_offset: T.int32):
A_offset_redef = A_offset
A = T.decl_buffer(1, "float32", elem_offset=A_offset_redef, data=A_ptr)
A[0] = 42.0
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_simplify_trivial_let_shape():
"""A Bind used in a buffer definition should be retained"""
@T.prim_func(private=True, s_tir=True)
def before(A_ptr: T.handle("float32"), A_size: T.int32):
A_size_redef = A_size
A = T.decl_buffer([A_size_redef], "float32", data=A_ptr)
A[0] = 42.0
@T.prim_func(private=True, s_tir=True)
def expected(A_ptr: T.handle("float32"), A_size: T.int32):
A_size_redef = A_size
A = T.decl_buffer([A_size_redef], "float32", data=A_ptr)
A[0] = 42.0
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_simplify_trivial_let_stride():
"""A Bind used in a buffer definition should be retained"""
@T.prim_func(private=True, s_tir=True)
def before(A_ptr: T.handle("float32"), A_stride: T.int32):
A_stride_redef = A_stride
A = T.decl_buffer(1, "float32", strides=[A_stride_redef], data=A_ptr)
A[0] = 42.0
@T.prim_func(private=True, s_tir=True)
def expected(A_ptr: T.handle("float32"), A_stride: T.int32):
A_stride_redef = A_stride
A = T.decl_buffer(1, "float32", strides=[A_stride_redef], data=A_ptr)
A[0] = 42.0
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
def test_simplify_buffer_identity_well_formed():
"""Regression: Simplify must not diverge buffer identity between DeclBuffer and BufferLoad.
The simplifier's VisitExpr calls analyzer_->Simplify() directly, bypassing
normal ExprMutator dispatch. If VisitBufferDef remaps a buffer at a DeclBuffer
site (e.g. inlining n_val -> n in the shape), BufferLoad inside a BufferStore
value would NOT pick up the remap because VisitBufferUse is never called.
This causes DeclBuffer/BufferLoad buffer identity divergence.
"""
@T.prim_func(private=True, s_tir=True)
def before(A_ptr: T.handle("float32"), B_ptr: T.handle("float32"), n: T.int32):
n_val = n
A = T.decl_buffer([n_val], "float32", data=A_ptr)
B = T.decl_buffer([n_val], "float32", data=B_ptr)
B[0] = A[0]
after = _apply_simplify(before)
tvm.tirx.analysis.verify_well_formed(after)
def test_buffer_shape_constraint():
@I.ir_module(check_well_formed=False)
class Before:
@T.prim_func(s_tir=True)
def main(a: T.handle):
n = T.int64()
A = T.match_buffer(a, (n * 32,), "float32")
A[T.min(T.int64(0), n)] = T.float32(0)
@I.ir_module(check_well_formed=False)
class Expected:
@T.prim_func(s_tir=True)
def main(a: T.handle):
n = T.int64()
A = T.match_buffer(a, (n * 32,), "float32")
A[T.int64(0)] = T.float32(0)
after = tvm.tirx.transform.StmtSimplify()(Before)
tvm.ir.assert_structural_equal(after["main"], Expected["main"])
def test_buffer_shape_constraint_with_offset():
@I.ir_module(check_well_formed=False)
class Before:
@T.prim_func(s_tir=True)
def main(a: T.handle):
n = T.int64()
A = T.match_buffer(a, (n * 32 + 1 - 2,), "float32")
A[T.min(T.int64(1), n)] = T.float32(0)
@I.ir_module(check_well_formed=False)
class Expected:
@T.prim_func(s_tir=True)
def main(a: T.handle):
n = T.int64()
A = T.match_buffer(a, (n * 32 + 1 - 2,), "float32")
A[T.int64(1)] = T.float32(0)
after = tvm.tirx.transform.StmtSimplify()(Before)
tvm.ir.assert_structural_equal(after["main"], Expected["main"])
def test_nested_if_elimination():
@T.prim_func(private=True, s_tir=True)
def before(a: T.Buffer((2, 8), "int32"), b: T.Buffer((2, 8), "int32")):
for i0, j0 in T.grid(2, 8):
b[i0, j0] = T.if_then_else(
i0 == 1 and 6 <= j0, 0, T.max(0, T.if_then_else(i0 == 1 and 6 <= j0, 0, a[i0, j0]))
)
@T.prim_func(private=True, s_tir=True)
def expected(a: T.Buffer((2, 8), "int32"), b: T.Buffer((2, 8), "int32")):
for i0, j0 in T.grid(2, 8):
b[i0, j0] = T.if_then_else(i0 == 1 and 6 <= j0, 0, T.max(0, a[i0, j0]))
after = _apply_simplify(before)
tvm.ir.assert_structural_equal(after, expected)
if __name__ == "__main__":
tvm.testing.main()