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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:36:25 +08:00
commit 26446540fa
3151 changed files with 974126 additions and 0 deletions
@@ -0,0 +1,287 @@
# 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 pytest
import tvm
import tvm.testing
from tvm import tirx
from tvm.arith.analyzer import CompareResult, Extension
from tvm.runtime import Object
def test_analyzer_is_ffi_object_with_persistent_state():
analyzer = tvm.arith.Analyzer()
x = tirx.Var("x", "int64")
assert isinstance(analyzer, Object)
analyzer.bind(x, tvm.ir.Range(0, 8))
assert analyzer.const_int_bound_is_bound(x)
assert analyzer.can_prove(x < 8)
assert not analyzer.can_prove(x < 4)
bound = analyzer.const_int_bound(x + 1)
assert bound.min_value == 1
assert bound.max_value == 8
def test_analyzer_object_constraint_scope_and_override_bind():
analyzer = tvm.arith.Analyzer()
x = tirx.Var("x", "int64")
with analyzer.constraint_scope(x % 3 == 0):
assert analyzer.modular_set(x).coeff == 3
assert analyzer.modular_set(x).coeff != 3
analyzer = tvm.arith.Analyzer()
y = tirx.Var("y", "int64")
analyzer.bind(y, tirx.const(4, "int64"))
tvm.ir.assert_structural_equal(analyzer.simplify(y + 1), tirx.const(5, "int64"))
analyzer.bind(y, tirx.const(8, "int64"), allow_override=True)
tvm.ir.assert_structural_equal(analyzer.simplify(y + 1), tirx.const(9, "int64"))
def test_analyzer_object_update_const_int_bound():
analyzer = tvm.arith.Analyzer()
x = tirx.Var("x", "int64")
analyzer.update(x, tvm.arith.ConstIntBound(2, 5))
bound = analyzer.const_int_bound(x + 1)
assert bound.min_value == 3
assert bound.max_value == 6
def test_analyzer_object_update_modular_set():
analyzer = tvm.arith.Analyzer()
x = tirx.Var("x", "int32")
assert analyzer.modular_set(x).coeff == 1
analyzer.update(x, tvm.arith.ModularSet(4, 0))
result = analyzer.modular_set(x)
assert result.coeff == 4
assert result.base == 0
def test_analyzer_object_update_int_set():
analyzer = tvm.arith.Analyzer()
y = tirx.Var("y", "int32")
analyzer.update(y, tvm.arith.IntervalSet(0, 8))
int_set = analyzer.int_set(y)
assert int_set.min_value.value == 0
assert int_set.max_value.value == 8
def test_analyzer_object_update_rejects_unknown_info():
analyzer = tvm.arith.Analyzer()
y = tirx.Var("y", "int32")
with pytest.raises(TypeError):
analyzer.update(y, "not-an-info-object")
def test_analyzer_object_can_prove_comparison_predicates():
analyzer = tvm.arith.Analyzer()
x = tirx.Var("x", "int32")
analyzer.bind(x, tvm.ir.Range(0, 8))
assert analyzer.can_prove(x >= 0)
assert not analyzer.can_prove(x >= 1)
assert analyzer.can_prove(x < 8)
assert not analyzer.can_prove(x < 7)
def test_analyzer_object_update_const_int_bound_half_space():
analyzer = tvm.arith.Analyzer()
n = tirx.Var("n", "int32")
assert not analyzer.can_prove(n >= 0)
analyzer.update(n, tvm.arith.ConstIntBound(0, tvm.arith.ConstIntBound.POS_INF))
assert analyzer.can_prove(n >= 0)
def test_analyzer_object_int_set_from_bound_vars():
analyzer = tvm.arith.Analyzer()
x = tirx.Var("x", "int32")
analyzer.bind(x, tvm.ir.Range(0, 8))
int_set = analyzer.int_set(x + 1)
assert int_set.min_value.value == 1
assert int_set.max_value.value == 8
def test_analyzer_object_set_maximum_rewrite_steps():
x = tirx.Var("x", "int32")
y = tirx.Var("y", "int32")
expr = (x + y) * 2 - x * 2 - y * 2 + tirx.max(x, y) - tirx.min(x, y)
capped = tvm.arith.Analyzer()
capped.set_maximum_rewrite_steps(1)
with pytest.raises(RuntimeError):
capped.rewrite_simplify(expr)
# A generous limit must not interfere with normal simplification.
relaxed = tvm.arith.Analyzer()
relaxed.set_maximum_rewrite_steps(1000)
relaxed.rewrite_simplify(expr)
def test_analyzer_object_try_compare_transitive():
analyzer = tvm.arith.Analyzer()
x = tirx.Var("x", "int32")
y = tirx.Var("y", "int32")
z = tirx.Var("z", "int32")
assert analyzer.try_compare(x, y) == CompareResult.UNKNOWN
with analyzer.constraint_scope(x < y):
with analyzer.constraint_scope(y < z):
# Direct known comparison.
assert analyzer.try_compare(x, y) == CompareResult.LT
# Transitive chain x < y < z is found only when propagation is enabled.
assert analyzer.try_compare(x, z) == CompareResult.LT
assert analyzer.try_compare(x, z, propagate_inequalities=False) == CompareResult.UNKNOWN
def test_analyzer_object_enabled_extensions_round_trip():
analyzer = tvm.arith.Analyzer()
assert analyzer.enabled_extensions == Extension.NoExtensions
analyzer.enabled_extensions = Extension.ComparisonOfProductAndSum
assert analyzer.enabled_extensions == Extension.ComparisonOfProductAndSum
analyzer.enabled_extensions = Extension.NoExtensions
assert analyzer.enabled_extensions == Extension.NoExtensions
def test_analyzer_object_rewrite_simplify_stats():
analyzer = tvm.arith.Analyzer()
x = tirx.Var("x", "int32")
analyzer.reset_rewrite_simplify_stats()
assert analyzer.rewrite_simplify_stats.nodes_visited == 0
analyzer.rewrite_simplify(x + 0)
assert analyzer.rewrite_simplify_stats.nodes_visited > 0
analyzer.reset_rewrite_simplify_stats()
assert analyzer.rewrite_simplify_stats.nodes_visited == 0
def test_analyzer_object_state_persists_across_ffi_calls():
analyzer = tvm.arith.Analyzer()
tile = tirx.Var("tile", "int32")
i = tirx.Var("i", "int32")
analyzer.bind(tile, tvm.tirx.const(8, "int32"))
# The same analyzer object is borrowed by the C++ DetectIterMap entry point;
# its binding makes the otherwise-undetectable floormod recognizable.
result = tvm.arith.detect_iter_map([i % tile], {i: tvm.ir.Range(0, 32)}, analyzer=analyzer)
assert len(result.indices) == 1
# The binding still lives in the same stateful object after the FFI call.
tvm.ir.assert_structural_equal(analyzer.simplify(tile), tvm.tirx.const(8, "int32"))
def test_analyzer_object_clone_is_independent():
analyzer = tvm.arith.Analyzer()
x = tirx.Var("x", "int64")
y = tirx.Var("y", "int64")
z = tirx.Var("z", "int64")
analyzer.bind(x, tvm.ir.Range(0, 8))
clone = analyzer.clone()
assert clone is not analyzer
assert clone.can_prove(x < 8)
clone.bind(y, tvm.ir.Range(0, 4))
assert clone.can_prove(y < 4)
assert not analyzer.can_prove(y < 4)
analyzer.bind(z, tvm.ir.Range(0, 4))
assert analyzer.can_prove(z < 4)
assert not clone.can_prove(z < 4)
assert analyzer.can_prove(x < 8)
assert clone.can_prove(x < 8)
def test_analyzer_object_clone_copies_every_sub_analyzer():
analyzer = tvm.arith.Analyzer()
x = tirx.Var("x", "int64")
w = tirx.Var("w", "int64")
v = tirx.Var("v", "int64")
analyzer.bind(x, tvm.ir.Range(0, 8))
analyzer.update(x, tvm.arith.ModularSet(4, 0))
analyzer.bind(w, tirx.const(4, "int64"))
analyzer.update(v, tvm.arith.IntervalSet(2, 9))
analyzer.enabled_extensions = Extension.ComparisonOfProductAndSum
clone = analyzer.clone()
assert clone.can_prove(x < 8)
assert clone.modular_set(x).coeff == 4
tvm.ir.assert_structural_equal(clone.simplify(w + 1), tirx.const(5, "int64"))
assert clone.int_set(v).max_value.value == 9
assert clone.enabled_extensions == Extension.ComparisonOfProductAndSum
assert clone.try_compare(x, tirx.const(0, "int64")) == CompareResult.GE
t = tirx.Var("t", "int64")
clone.update(x, tvm.arith.ModularSet(8, 0), override=True)
clone.update(v, tvm.arith.IntervalSet(0, 3), override=True)
clone.bind(w, tirx.const(8, "int64"), allow_override=True)
clone.bind(t, tvm.ir.Range(0, 4))
clone.enabled_extensions = Extension.NoExtensions
assert analyzer.modular_set(x).coeff == 4
assert clone.modular_set(x).coeff == 8
assert analyzer.int_set(v).max_value.value == 9
assert clone.int_set(v).max_value.value == 3
tvm.ir.assert_structural_equal(analyzer.simplify(w + 1), tirx.const(5, "int64"))
tvm.ir.assert_structural_equal(clone.simplify(w + 1), tirx.const(9, "int64"))
assert analyzer.enabled_extensions == Extension.ComparisonOfProductAndSum
assert clone.enabled_extensions == Extension.NoExtensions
assert clone.try_compare(t, tirx.const(0, "int64")) == CompareResult.GE
assert analyzer.try_compare(t, tirx.const(0, "int64")) == CompareResult.UNKNOWN
def test_analyzer_object_clone_resets_rewrite_stats():
analyzer = tvm.arith.Analyzer()
x = tirx.Var("x", "int64")
y = tirx.Var("y", "int64")
analyzer.bind(x, tvm.ir.Range(0, 8))
analyzer.bind(y, tvm.ir.Range(0, 8))
analyzer.simplify((x + y) * 2 - x - y)
source_attempts = analyzer.rewrite_simplify_stats.rewrites_attempted
assert source_attempts > 0
clone = analyzer.clone()
assert clone.rewrite_simplify_stats.rewrites_attempted == 0
assert analyzer.rewrite_simplify_stats.rewrites_attempted == source_attempts
if __name__ == "__main__":
tvm.testing.main()
@@ -0,0 +1,538 @@
# 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: E731, F841
import tvm_ffi
import tvm
import tvm.testing
from tvm import te, tirx
from tvm.script import tirx as T
class CanonicalChecker:
def __init__(self):
self.analyzer = tvm.arith.Analyzer()
def _convert(self, expr):
# TODO(Lunderberg): Make utility functions `tirx.convert` and
# `relax.convert` that convert to their respective IR types.
# Implementation should be in C++, and should only consist of
# conversions that are applied automatically through FFI.
if isinstance(expr, int):
return T.int32(expr)
else:
return expr
def verify(self, data, expected):
res = self.analyzer.canonical_simplify(data)
expected = self._convert(expected)
assert tvm_ffi.structural_equal(res, expected), (
f"\ndata={data}\nres={res}\nexpected={expected}"
)
def test_mul_sum_simplify():
ck = CanonicalChecker()
x, y, z = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32"), tvm.tirx.Var("z", "int32")
ck.verify(2 + (3 * x + z + y + 1) * 4 + x, x * 13 + z * 4 + y * 4 + 6)
ck.verify(x * 3 - 4 * x + 1, 1 - x)
ck.verify(y + x * 3 - 5 * x + 1 + y, y * 2 + 1 - x * 2)
tdiv = tvm.tirx.truncdiv
tmod = tvm.tirx.truncmod
# trucdiv
ck.verify(tdiv(x + y + x + y * 3, 2), y * 2 + x)
ck.verify(tmod(x + y + x + y * 3, 2), 0)
# floordiv
fld = tvm.tirx.floordiv
flm = tvm.tirx.floormod
ck.verify(flm(x + x + y * 3, 2), flm(y * 3, 2))
ck.verify(fld(x + y + x + y * 3, 2), y * 2 + x)
ck.verify(flm(x + y + x + y * 3, 2), 0)
ck.verify(fld(x + x + y * 3, 2), fld(y * 3, 2) + x)
def test_split_index_simplify():
ck = CanonicalChecker()
x, y, z = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32"), tvm.tirx.Var("z", "int32")
# trucdiv
tdiv = tvm.tirx.truncdiv
tmod = tvm.tirx.truncmod
# split div const
ck.verify(tdiv(x, 3) * 3 + tmod(x, 3), x)
ck.verify(tdiv(x, 6) * 6 + tmod(tdiv(x, 3), 2) * 3 + tmod(x, 3), x)
ck.verify(tdiv(tdiv(tmod(x, 16), 2) * 2, 4), tdiv(tmod(x, 16), 4))
ck.verify(tdiv(tmod(x, 2), 8), 0)
ck.verify(tdiv(tmod(x, 2), 7), 0)
ck.verify(tdiv(tdiv(tmod(x, 16), 2) * 2, 6), tdiv(tmod(x, 16), 6))
# split mod const
ck.verify(tmod((x * 8), 16), tmod(x, 2) * 8)
ck.verify(tmod(x * 8, 2), 0)
# simplify then fold
ck.analyzer.update(x, tvm.arith.ConstIntBound(0, 1000))
ck.analyzer.update(y, tvm.arith.ConstIntBound(0, 1000))
ck.verify(tdiv(x * 4 + y, 2) * 2 + tmod(x * 4 + y, 2), x * 4 + y)
# complex fold
ck.verify(tdiv(z * 9 + y, 2) * 2 + tmod(z * 9 + y, 2), z * 9 + y)
ck.analyzer.update(x, tvm.arith.ConstIntBound(-100, 1000), True)
ck.analyzer.update(y, tvm.arith.ConstIntBound(-100, 1000), True)
ck.verify(tdiv(x * 4 + y, 2) * 2 + tmod(x * 4 + y, 2), x * 4 + y)
# floordiv
fld = tvm.tirx.floordiv
flm = tvm.tirx.floormod
ck.verify(fld(x * 5, 2), fld(x * 5, 2))
ck.verify(fld(x, 3) * 3 + flm(x, 3), x)
ck.verify(fld(x, 6) * 6 + flm(fld(x, 3), 2) * 3 + flm(x, 3), x)
ck.verify(fld(fld(flm(x, 16), 2) * 2, 4), fld(flm(x, 16), 4))
ck.verify(fld(flm(x, 2), 8), 0)
ck.verify(fld(flm(x, 2), 7), 0)
ck.verify(fld(fld(flm(x, 16), 2) * 2, 6), fld(flm(x, 16), 6))
# floordiv(floormod(sum, m*n), n) => floormod(floordiv(sum, n), m)
# when sum has parts divisible by n
d_tile = te.var("d_tile")
i = te.var("i")
v = te.var("v")
ck.analyzer.update(d_tile, tvm.arith.ConstIntBound(0, 7), True)
ck.analyzer.update(i, tvm.arith.ConstIntBound(0, 1), True)
ck.analyzer.update(v, tvm.arith.ConstIntBound(0, 7), True)
ck.verify(fld(flm(d_tile * 16 + i * 8 + v, 64), 8), flm(d_tile * 2 + i, 8))
# cannot simplify mixed case, unless we canonicalize into one mode.
ck.verify(tdiv(x, 6) * 2 + tmod(fld(x, 3), 2), tdiv(x, 6) * 2 + tmod(fld(x, 3), 2))
ck.verify(tmod(-x, 2), tmod(x, -2) * -1)
def test_div_simplify():
ck = CanonicalChecker()
x = tvm.tirx.Var("x", "int32")
tdiv = tvm.tirx.truncdiv
# truc div
ck.verify(tdiv(16 + 48 * x, 16), x * 3 + 1)
# (17+48*x)/16 is not simplifiable for arbitrary x because when 17+48*x<0
# (17+48*x)/16 != 1+3*x
ck.verify(tdiv(17 + 48 * x, 16), tdiv(x * 48 + 17, 16))
# However, when x >= 0, then 17+48*x >= 0 and (17+48*x)/16 can be simplified
ck.analyzer.update(x, tvm.arith.ConstIntBound(0, 10))
ck.verify(tdiv(17 + 48 * x, 16), x * 3 + 1)
# Trying expressions that are not simplifiable for any values of the variables
ck.verify(tdiv(17 + 47 * x, 16), tdiv(x * 47 + 17, 16))
# floordiv
fld = tvm.tirx.floordiv
ck.analyzer.update(x, tvm.arith.ConstIntBound(-1000, 10000), True)
ck.verify(fld(16 + 48 * x, 16), x * 3 + 1)
ck.verify(fld(17 + 48 * x, 16), x * 3 + 1)
ck.verify(fld(17 + 47 * x, 16), fld(x * 47 + 17, 16))
def test_fp16_const_fold():
ck = CanonicalChecker()
zero = tvm.tirx.const(0, "float16")
one = tvm.tirx.const(1, "float16")
half = tvm.tirx.const(0.5, "float16")
ck.verify(zero + half, half)
ck.verify(half - zero, half)
ck.verify(zero * half, zero)
ck.verify(half * one, half)
ck.verify(half / one, half)
ck.verify(zero / half, zero)
def test_floormod_simplify():
ck = CanonicalChecker()
flm = tvm.tirx.floormod
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
ck.verify(flm(flm((x * 4) + y - 466036, 24528) - 24512, 16), flm((x * 4) + y + 12, 16))
ck.verify(flm(flm((x * 4), 16), 8), flm(x, 2) * 4)
ck.verify(flm(-x, 2), flm(x, -2) * -1)
def test_canonical_mixed():
ck = CanonicalChecker()
x = tvm.tirx.Var("x", "int32")
z = tvm.tirx.const(3, "int32")
tdiv = tvm.tirx.truncdiv
tmod = tvm.tirx.truncmod
ck.verify(tdiv(x, (z * z)) - tdiv(x, (z * z)), 0)
ck.verify(tdiv(x, (z + z)) - tdiv(x, (z + z)), 0)
ck.verify(x - 2 < 3, x < 5)
ck.verify(tvm.tirx.max(x, 1) - tvm.tirx.max(x, 1), 0)
ck.verify(tvm.tirx.min(x, 1) - tvm.tirx.min(x, 1), 0)
ck.verify(x * x - x * x, 0)
ck.verify(tmod(tdiv(tmod(x, 20), 2) * 2, 4), tdiv(tmod(x, 4), 2) * 2)
fld = tvm.tirx.floordiv
ck.verify(fld(x, (z * z)) - fld(x, (z * z)), 0)
ck.verify(fld(x, (z + z)) - fld(x, (z + z)), 0)
def test_reduce_combiner_simplify():
ck = CanonicalChecker()
dummy = tvm.tirx.Var("dummy", "int32")
comm_reducer = te.comm_reducer
prod = comm_reducer(lambda x, y: x * y, lambda t0: tvm.tirx.const(1, t0))
sum_or_prod = comm_reducer(
lambda x, y: tvm.tirx.Select(dummy < 0, x + y, x * y),
lambda t0: tvm.tirx.Select(dummy < 0, tvm.tirx.const(0, t0), tvm.tirx.const(1, t0)),
)
sum_and_prod = comm_reducer(
lambda x, y: (x[0] + y[0], x[1] * y[1]),
lambda t0, t1: (tvm.tirx.const(0, t0), tvm.tirx.const(5, t1) - tvm.tirx.const(4, t1)),
)
some_reducer1 = comm_reducer(
lambda x, y: (
x[0] + y[0],
x[0] + y[0] + x[1] + y[1],
x[0] * y[2] + y[0] * x[2],
x[1] + y[2],
4.0,
),
lambda t0, t1, t2, t3, t4: (
tvm.tirx.const(0, t0),
tvm.tirx.const(1, t1),
tvm.tirx.const(2, t2),
tvm.tirx.const(3, t3),
tvm.tirx.const(4, t4),
),
)
k = te.reduce_axis((0, 10), name="k")
A = te.placeholder((10,), name="A")
# Test that SimplifyCombiner makes use of vranges
ck.analyzer.update(dummy, tvm.arith.ConstIntBound(-10, -4))
ck.verify(sum_or_prod(A[k], k), te.sum(A[k], k))
ck.verify(sum_or_prod(A[k], k, init=1), te.sum(A[k], k, init=1))
ck.analyzer.update(dummy, tvm.arith.ConstIntBound(5, 9), True)
ck.verify(sum_or_prod(A[k], k), prod(A[k], k))
ck.verify(sum_or_prod(A[k], k, init=1), prod(A[k], k, init=1))
ck.analyzer.update(dummy, tvm.arith.ConstIntBound(-10, 100), True)
ck.verify(sum_and_prod((A[k], A[10 - k]), k)[0], te.sum(A[k], k))
ck.verify(sum_and_prod((A[k], A[10 - k]), k)[1], prod(A[10 - k], k))
reference_simplified_sources = [
[A[0]],
[A[0], A[1]],
[A[0], A[2]],
[A[0], A[1], A[2], A[3]],
[A[4]],
]
for j in range(5):
# Here we use the j-th component of the result, so only it and the components it
# depends on are left.
simplified = ck.analyzer.canonical_simplify(
some_reducer1((A[0], A[1], A[2], A[3], A[4]), k)[j]
)
# Check that the remaining components are the expected ones.
for lhs, rhs in zip(simplified.source, reference_simplified_sources[j]):
tvm.ir.assert_structural_equal(lhs, rhs)
# Test that components with side effects are not removed
dummy = tvm.ir.GlobalVar("dummy")
side_effect = lambda *xs: tvm.ir.Call(dummy, xs, ret_ty="int32")
ck.verify(
sum_and_prod((A[k], side_effect(A[10 - k])), k)[0],
sum_and_prod((A[k], side_effect(A[10 - k])), k)[0],
)
ck.verify(sum_and_prod((side_effect(A[k]), A[10 - k]), k)[0], te.sum(side_effect(A[k]), k))
def test_reduce_simplify():
ck = CanonicalChecker()
k = te.reduce_axis((0, 10), name="k")
j = te.reduce_axis((-5, 3), name="j")
A = te.placeholder((10,), name="A")
ck.verify(te.sum(tvm.tirx.Select(k + j < 12, k + j, 0), [k, j]), te.sum(k + j, [k, j]))
ck.verify(te.sum(A[3], []), A[3])
ck.verify(te.sum(A[3], [], where=k > 12, init=1.0), tvm.tirx.const(1.0, dtype="float32"))
# The rule below is not typical, removed for now
ck.verify(te.sum(tvm.tirx.div(k, 10), k), te.sum(tvm.tirx.const(0, "int32"), k))
def test_simplify_if_then_else():
ck = CanonicalChecker()
x = tvm.tirx.Var("x", "int32")
y = tvm.tirx.Var("y", "int32")
tdiv = tvm.tirx.truncdiv
tmod = tvm.tirx.truncmod
# simplification that takes condition into account.
res = tvm.tirx.if_then_else(
(x * 4 + y) >= 466036,
tvm.tirx.if_then_else(
24512 <= tmod(((x * 4) + y) - 466036, 24528),
tmod(tmod(((x * 4) + y) - 466036, 24528) - 24512, 16),
x,
),
y,
)
res2 = tvm.tirx.if_then_else(
(x * 4) >= 466036 - y,
tvm.tirx.if_then_else(
24512 <= tmod(((x * 4) + y) - 466036, 24528),
tmod(tmod(((x * 4) + y) - 466036, 24528) - 24512, 16),
x,
),
y,
)
expected = tvm.tirx.if_then_else(
tvm.tirx.LE(466036, (x * 4 + y)),
tvm.tirx.if_then_else(
tvm.tirx.LE(24512, tmod(((x * 4) + y) - 4, 24528)), tmod(((x * 4) + y) - 4, 16), x
),
y,
)
ck.verify(res, expected)
ck.verify(res2, expected)
# can only simplify if condition
res = tvm.tirx.Select(tvm.tirx.all(x >= -1, y >= 0), tmod(x + y + 100, 3), tmod(x + 100, 3))
expected = tvm.tirx.Select(tvm.tirx.all(x >= -1, y >= 0), tmod(x + y + 1, 3), tmod(x + 100, 3))
ck.verify(res, ck.analyzer.canonical_simplify(expected))
res = tvm.tirx.Select(x >= 10, tvm.tirx.if_then_else(tdiv(x, 3) > 2, x, 0), 0)
expected = tvm.tirx.Select(x >= 10, x, 0)
ck.verify(res, ck.analyzer.canonical_simplify(expected))
res = tvm.tirx.Select(x >= 10, tvm.tirx.if_then_else(tdiv(x, 3) < 2, x, 0), 0)
ck.verify(res, 0)
def test_complex_cases():
ck = CanonicalChecker()
x = tvm.tirx.Var("x", "int32")
y = tvm.tirx.Var("y", "int32")
tdiv = tvm.tirx.truncdiv
tmod = tvm.tirx.truncmod
res2 = (
tdiv(tdiv(tmod(x * 128 + y, 1296), 36) * 2 + 1, 2) * 36
+ tdiv(tmod((x * 128) + y, 36) * 2 + 1, 2)
- tmod((x * 128) + y, 1296)
+ 1
)
ck.analyzer.update(x, tvm.arith.ConstIntBound(0, 5))
ck.analyzer.update(y, tvm.arith.ConstIntBound(0, 127))
ck.verify(res2, 1)
ck.analyzer.update(y, tvm.arith.ConstIntBound(0, 1024), True)
res3 = (
tdiv(x * 1024 + y, 65536)
+ tdiv(tmod(x * 1024 + y, 65536), 256)
+ tdiv(tmod(x * 1024 + y, 256), 16)
+ tmod(x * 1024 + y, 16)
- tdiv(y, 256)
- tdiv(tmod(y, 256), 16)
- tmod(y, 16)
- (x * 4)
)
ck.verify(res3, tdiv((x * 1024) + y, 256) - tdiv(y, 256) - (x * 4))
def test_simplify_cast():
ck = CanonicalChecker()
tcast = tvm.tirx.Cast
fld = tvm.tirx.floordiv
flm = tvm.tirx.floormod
# cast(i64, i + j + 1) - cast(i64, i)
i = tvm.tirx.Var("i", "int32")
j = tvm.tirx.Var("j", "int32")
res = tcast("int64", i + j + 1) - tcast("int64", i)
ck.verify(res, tcast("int64", j) + tvm.tirx.const(1, "int64"))
# cast(i32, i + j + 1) - cast(i32, i)
i = tvm.tirx.Var("i", "int64")
j = tvm.tirx.Var("j", "int64")
ck.analyzer.update(i, tvm.arith.ConstIntBound(0, 10))
ck.analyzer.update(j, tvm.arith.ConstIntBound(0, 10))
res = tcast("int32", i + j + 1) - tcast("int32", i)
ck.verify(res, tcast("int32", j) + 1)
# cast(i32, i + j - 100)
i = tvm.tirx.Var("i", "int64")
j = tvm.tirx.Var("j", "int64")
ck.analyzer.update(i, tvm.arith.ConstIntBound(0, 2**31 - 1))
ck.analyzer.update(j, tvm.arith.ConstIntBound(0, 10))
res = tcast("int32", i + j - 100)
ck.verify(res, res)
# cast(i32, flm(axis, 7i64) * 2i64 + 1i64) + 1i32
# - cast(i32, flm(axis, 7i64) * 2i64)
axis = tvm.tirx.Var("axis", "int64")
ck.analyzer.update(axis, tvm.arith.ConstIntBound(0, 42))
res = (
tcast(
"int32",
flm(axis, tvm.tirx.const(7, "int64")) * tvm.tirx.const(2, "int64")
+ tvm.tirx.const(1, "int64"),
)
+ tvm.tirx.const(1, "int32")
- tcast("int32", flm(axis, tvm.tirx.const(7, "int64")) * tvm.tirx.const(2, "int64"))
)
ck.verify(res, 2)
def test_simplify_normalize_min_value_expr():
ck = CanonicalChecker()
x = tvm.tirx.Var("x", "int32")
ck.verify(tvm.tirx.min_value("int32") - x == 0, x == tvm.tirx.min_value("int32"))
ck.verify(tvm.tirx.min_value("int32") + x == 0, tirx.const(False))
ck.verify(0 == tvm.tirx.min_value("int32") - x, x == tvm.tirx.min_value("int32"))
ck.verify(0 == tvm.tirx.min_value("int32") + x, tirx.const(False))
ck.verify(-x + tvm.tirx.min_value("int32") == 0, x == tvm.tirx.min_value("int32"))
ck.verify(x + tvm.tirx.min_value("int32") == 0, tirx.const(False))
ck.verify(0 == -x + tvm.tirx.min_value("int32"), x == tvm.tirx.min_value("int32"))
ck.verify(0 == x + tvm.tirx.min_value("int32"), tirx.const(False))
def test_proddiv_simplify():
ck = CanonicalChecker()
flm = tvm.tirx.floormod
fld = tvm.tirx.floordiv
tdiv = tvm.tirx.truncdiv
tmod = tvm.tirx.truncmod
x, y, z = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32"), tvm.tirx.Var("y", "int32")
ck.verify(flm(x * 32 * x, x), 0)
ck.verify(flm(z * x * 32 * x * y, x * z), 0)
ck.verify(flm(z * x * 32 * x * y, x * z * y * 8 * x), 0)
ck.verify(flm(z * x * 32 * (x * y), 6 * x * z), flm(x * y * 16, 3) * (x * z * 2))
ck.verify(flm(x * 32 * x, x * z), flm(x * 32, z) * x)
ck.verify(tmod(x * 32 * x, x), 0)
ck.verify(tmod(z * x * 32 * x * y, x * z), 0)
ck.verify(tmod(z * x * 32 * (x * y), 6 * x * z), tmod(x * y * 16, 3) * (x * z * 2))
ck.verify(tmod(x * 32 * x, x * z), tmod(x * 32, z) * x)
ck.verify(fld(x * 2 * x * z, 4 * x * x * x), fld(z, x * 2))
ck.verify(fld(x * (2 * y) * 3, 3 * y), x * 2)
ck.verify(fld(x * (2 * y) * 3, 3 * y * z), fld(x * 2, z))
ck.verify(tdiv(x * 2 * x * z, 4 * x * x * x), tdiv(z, x * 2))
ck.verify(tdiv(x * (2 * y) * 3, 3 * y), x * 2)
ck.verify(tdiv(x * (2 * y) * 3, 3 * y * z), tdiv(x * 2, z))
def test_floormod_two():
ck = CanonicalChecker()
flm = tvm.tirx.floormod
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
ck.verify(flm(x * 10 + 1 + y * 2 + 2, 2), 1)
def test_simplify_le():
ck = CanonicalChecker()
# Case 1. Ignore the extra expr if it's small than the division number
x, y, z = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32"), tvm.tirx.Var("z", "int32")
ck.analyzer.bind(y, tvm.ir.Range(0, 8))
ck.analyzer.bind(z, tvm.ir.Range(0, 2))
ck.verify(x * 8 + y < 16, x < 2)
ck.verify(x * 8 + z * 4 < 16, x < 2)
ck.verify(x * 8 + z * 4 < 16, x < 2)
# TODO: Not sure why `-2 < x` will be convert to `x > -2`, use a explicit simplify here.
ck.verify(x * -8 + y < 16, ck.analyzer.rewrite_simplify(-2 < x))
ck.verify(x * -8 + z * 4 < 16, ck.analyzer.rewrite_simplify(-2 < x))
ck.verify(x * 8 + y + z < 16, x * 8 + y + z < 16)
n = tvm.tirx.Var("n", "int32")
ck.verify(x * 8 + y < n, x * 8 + y < n)
# Case 2. Simplify the extra expr
x1, x2, ty, tx, vec = (
tvm.tirx.Var("x1", "int32"),
tvm.tirx.Var("x2", "int32"),
tvm.tirx.Var("ty", "int32"),
tvm.tirx.Var("tx", "int32"),
tvm.tirx.Var("vec", "int32"),
)
ck.analyzer.bind(x1, tvm.ir.Range(0, 2))
ck.analyzer.bind(x2, tvm.ir.Range(0, 3))
ck.analyzer.bind(ty, tvm.ir.Range(0, 8))
ck.analyzer.bind(tx, tvm.ir.Range(0, 32))
ck.analyzer.bind(vec, tvm.ir.Range(0, 8))
ck.verify(
x1 * 5632 + (((x2 * 8 + ty) * 32 + tx) * 8 + vec) % 5632 < 11008,
x1 * 22 + (x2 * 8 + ty) % 22 < 43,
)
ck.verify(tx // 2 % 8 + vec < 8, tx % 16 // 2 + vec < 8)
# Case 3. No failure
x, y, z = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32"), tvm.tirx.Var("z", "int32")
ck.analyzer.bind(y, tvm.ir.Range(0, 1024))
ck.verify(x * 1024 + y < z * 7168, x - z * 7 < 0)
def test_simplify_le_negative_scale_extra():
"""Regression: Case 2 of the LT-with-divisible-coeffs rewrite must not
fire when the leftover split term has a negative scale.
The rewrite ``S + xn < 0 ⇔ S/d + xn // d < 0`` is only sound when
the leftover ``xn`` has scale ``+1``. With scale ``-1`` the equivalence
becomes ``≤`` rather than ``<`` and the rewrite silently strengthens
the predicate. The original bug surfaced as ``row > col`` masks of
``.16x*b`` tcgen05 readbacks collapsing to plain ``warp_id > k``
comparisons (lower-triangle writes were silently dropped on the
boundary warp).
"""
ck = CanonicalChecker()
tx = tvm.tirx.Var("tx", "int32")
warp = tvm.tirx.Var("warp", "int32")
ck.analyzer.bind(tx, tvm.ir.Range(0, 128))
ck.analyzer.bind(warp, tvm.ir.Range(0, 4))
# Same-source joint projection: the comparison genuinely depends on tx
# at warp == 0 (e.g. tx == 4 ⇒ 0 < 1 = True; tx == 1 ⇒ 2 < 0 = False),
# so the simplifier must keep both sides. Pre-fix this folded to
# ``0 < warp`` and dropped every True case in warp 0.
expr = (tx % 4) * 2 < warp * 16 + (tx % 32) // 4
ck.verify(expr, expr)
# The simpler ``scale = -1`` with ``lower_factor = 1`` shape. Pre-fix
# this folded to ``False`` (drops all warp >= 1 cases where the rhs
# actually exceeds 8*warp).
expr = warp * 8 < (tx % 32)
ck.verify(expr, expr)
# The corresponding ``scale = +1`` Case 2 path (the rewrite this guards)
# must still optimize — verifies we did not over-restrict.
x1 = tvm.tirx.Var("x1", "int32")
y1 = tvm.tirx.Var("y1", "int32")
ck.verify(x1 * 64 + (y1 % 64) < 120, x1 * 8 + (y1 % 64) // 8 < 15)
# The truly-always-true comparison that arises from the same kernel
# (``r = 2 / va = 1`` in the tcgen05.ld.16x256b readback) must still
# fold to True so the masked store can be elided.
expr_true = (tx % 4) * 2 < warp * 16 + (tx % 32) // 4 + 8
ck.verify(expr_true, tvm.tirx.const(True, "bool"))
if __name__ == "__main__":
tvm.testing.main()
@@ -0,0 +1,323 @@
# 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: RUF012
import contextlib
import pytest
import tvm
import tvm.testing
from tvm.arith import ConstIntBound
NEG_INF = ConstIntBound.NEG_INF
POS_INF = ConstIntBound.POS_INF
class TestCase:
def __init__(self, expr, expected_bounds, known_bounds=None, constraint=None):
self.expr = expr
self.expected_bounds = expected_bounds
if known_bounds is None:
self.known_bounds = {}
else:
self.known_bounds = known_bounds
self.constraint = constraint
@property
def __name__(self):
return str(self.expr)
class BaseCompare:
def test_const_bounds(self, test_case):
analyzer = tvm.arith.Analyzer()
for var, bounds in test_case.known_bounds.items():
analyzer.update(var, ConstIntBound(*bounds))
assert analyzer.const_int_bound_is_bound(var)
with contextlib.ExitStack() as stack:
if test_case.constraint is not None:
stack.enter_context(analyzer.constraint_scope(test_case.constraint))
bounds = analyzer.const_int_bound(test_case.expr)
if test_case.expected_bounds[0] is None:
assert bounds.max_value == test_case.expected_bounds[1]
elif test_case.expected_bounds[1] is None:
assert bounds.min_value == test_case.expected_bounds[0]
else:
assert (bounds.min_value, bounds.max_value) == test_case.expected_bounds
class TestDataType(BaseCompare):
test_case = tvm.testing.parameter(
TestCase(tvm.tirx.Var("x", "int64"), (NEG_INF, POS_INF)),
TestCase(tvm.tirx.Var("x", "int8"), (-128, 127)),
TestCase(tvm.tirx.Var("x", "uint8"), (0, 255)),
TestCase(tvm.tirx.Var("x", "int32"), (-(2**31), 2**31 - 1)),
)
def test_plain_var_non_negative_bound_requires_context():
var = tvm.tirx.Var("x", "int64")
analyzer = tvm.arith.Analyzer()
assert analyzer.const_int_bound(var).min_value == NEG_INF
with analyzer.constraint_scope(var >= 0):
assert analyzer.const_int_bound(var).min_value == 0
assert analyzer.const_int_bound(var).min_value == NEG_INF
class TestCastBound(BaseCompare):
x = tvm.tirx.Var("x", "int8")
tmod = tvm.tirx.truncmod
test_case = tvm.testing.parameter(
TestCase(tmod(x, 3).astype("uint32"), (0, 2)),
TestCase(tmod(x, 3).astype("float32").astype("int32"), (-2, 2)),
)
class TestAddSubBound(BaseCompare):
x = tvm.tirx.Var("x", "int64")
y = tvm.tirx.Var("y", "int64")
test_case = tvm.testing.parameter(
TestCase(x + y, (NEG_INF, POS_INF)),
TestCase(x + y, (1, 14), known_bounds={x: (0, 4), y: (1, 10)}),
TestCase(x - y, (-10, 3), known_bounds={x: (0, 4), y: (1, 10)}),
TestCase(x - y, (-10, POS_INF), known_bounds={x: (0, POS_INF), y: (1, 10)}),
TestCase(1 - x, (NEG_INF, 1), known_bounds={x: (0, POS_INF), y: (1, 10)}),
)
@pytest.mark.xfail(reason="Not currently supported")
class TestBoundsUsingReciprocals(BaseCompare):
"""Special handling for differences of reciprocals
These terms can appear when comparing the number of operations for
different orderings of matrix multiplications, with A, B, and C
known to be positive values.
In these cases, comparing `(A+B)*C < A*B` is equivalent to
`1/A + 1/B < 1/C`. Working in terms of the reciprocals
allows the ConstIntBound analyzer to provide a tighter
bound for these differences than would otherwise be
available.
For `(A+B)*C - A*B`, the normal bottom-up integer bounds are unable to
provide the bounds required to provide these inequalities, because they
treat the terms as uncorrelated. That is, they assume that `(A+B)*C` may
achieve its minimum while `A*B` simultaneously achieves its maximum.
"""
A, B, C = [tvm.tirx.Var(letter, "int64") for letter in "ABC"]
symmetric_bounds = {A: (1, 4095), B: (1, 4095), C: (2048, 2048)}
asymmetric_bounds = {A: (1, 1024), B: (1, POS_INF), C: (2048, 2048)}
test_case = tvm.testing.parameter(
TestCase((A + B) * C - A * B, (2048, None), known_bounds=symmetric_bounds),
TestCase((A + B) * C - B * A, (2048, None), known_bounds=symmetric_bounds),
TestCase(A * B - (A + B) * C, (None, -2048), known_bounds=symmetric_bounds),
TestCase(B * A - (A + B) * C, (None, -2048), known_bounds=symmetric_bounds),
TestCase((A + B) * C - A * B, (2048, None), known_bounds=asymmetric_bounds),
TestCase((A + B) * C - B * A, (2048, None), known_bounds=asymmetric_bounds),
TestCase(A * B - (A + B) * C, (None, -2048), known_bounds=asymmetric_bounds),
TestCase(B * A - (A + B) * C, (None, -2048), known_bounds=asymmetric_bounds),
)
class TestMulBound(BaseCompare):
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
test_case = tvm.testing.parameter(
TestCase(x * y + 20, (0, 60), {x: (-2, 4), y: (4, 10)}),
TestCase(x * y, (-32, 24), {x: (-3, 4), y: (-8, 2)}),
TestCase(x * y, (NEG_INF, POS_INF), {x: (NEG_INF, 4), y: (-8, 2)}),
)
class TestTruncDivBound(BaseCompare):
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
expr = tvm.tirx.truncdiv(x, y)
test_case = tvm.testing.parameter(
TestCase(expr, (-2, None), {x: (-9, 4), y: (4, 10)}),
TestCase(expr, (-4, 9), {x: (-9, 4), y: (-2, 0)}),
TestCase(expr, (NEG_INF, POS_INF), {x: (NEG_INF, 4), y: (-2, 1)}),
TestCase(expr, (-9, 9), {x: (-9, 4), y: (-4, 12)}),
)
class TestTruncModBound(BaseCompare):
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
expr = tvm.tirx.truncmod(x, y)
test_case = tvm.testing.parameter(
TestCase(expr, (-9, 4), {x: (-9, 4), y: (4, 10)}),
TestCase(expr, (-9, 9), {x: (NEG_INF, POS_INF), y: (4, 10)}),
TestCase(expr, (0, 9), {x: (1, POS_INF), y: (4, 10)}),
)
class TestFloorDivBound(BaseCompare):
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
ux = tvm.tirx.Var("x", "uint32")
uy = tvm.tirx.Var("y", "uint32")
test_case = tvm.testing.parameter(
TestCase(x // y, (-9 // 4, None), {x: (-9, 4), y: (4, 10)}),
TestCase(x // y, (-4, 9), {x: (-9, 4), y: (-2, 0)}),
TestCase(x // y, (NEG_INF, POS_INF), {x: (NEG_INF, 4), y: (-2, 1)}),
TestCase(x // y, (-9, 9), {x: (-9, 4), y: (-4, 12)}),
TestCase(ux // uy, (0, 4), {ux: (1, 4), uy: (0, 12)}),
)
class TestFloorModBound(BaseCompare):
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
test_case = tvm.testing.parameter(
TestCase(x % y, (0, 9), {x: (-9, 4), y: (4, 10)}),
TestCase(x % y, (0, 9), {x: (NEG_INF, POS_INF), y: (4, 10)}),
TestCase(x % y, (0, 9), {x: (1, POS_INF), y: (4, 10)}),
)
class TestMinMaxBound(BaseCompare):
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
test_case = tvm.testing.parameter(
TestCase(tvm.tirx.min(x, y), (-9, 10), {x: (-9, 11), y: (4, 10)}),
TestCase(tvm.tirx.min(x, y), (NEG_INF, 10), {x: (NEG_INF, POS_INF), y: (4, 10)}),
TestCase(tvm.tirx.max(x, y), (4, POS_INF), {x: (NEG_INF, POS_INF), y: (4, 10)}),
TestCase(tvm.tirx.max(x, y), (4, POS_INF), {x: (1, POS_INF), y: (4, 10)}),
)
class TestSelectBound(BaseCompare):
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
test_case = tvm.testing.parameter(
TestCase(
tvm.tirx.Select(x > 1, (y < 0).astype("int32"), y + 1),
(0, 11),
{x: (-9, 11), y: (4, 10)},
),
)
class TestShiftAndBound(BaseCompare):
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
test_case = tvm.testing.parameter(
TestCase(x >> y, (-3, 2), {x: (-9, 11), y: (2, 10)}),
TestCase(x & y, (0, 10), {x: (-9, 11), y: (2, 10)}),
TestCase(x & y, (0, 10), {x: (10, 11), y: (2, 10)}),
)
class TestMixIndexBound(BaseCompare):
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
tdiv = tvm.tirx.truncdiv
tmod = tvm.tirx.truncmod
test_case = tvm.testing.parameter(
TestCase(tmod(x, 8) + tdiv(x, 8) * 8, (0, 24 - 1), {x: (0, 24 - 1), y: (0, 3 - 1)}),
TestCase(y + x * 3, (0, 24 * 3 - 1), {x: (0, 24 - 1), y: (0, 3 - 1)}),
TestCase(
tmod(x, 7) + tdiv(x, 7) * 7, (0, (23 // 7) * 7 + 6), {x: (0, 24 - 1), y: (0, 3 - 1)}
),
)
class TestLetBound(BaseCompare):
x = tvm.tirx.Var("x", "int32")
test_case = tvm.testing.parameter(
TestCase(tvm.tirx.Let(x, 1, x + 1), (2, 2)),
)
class TestFloorModNegativeDivisor(BaseCompare):
flm, fld = tvm.tirx.floormod, tvm.tirx.floordiv
a, b = tvm.tirx.Var("a", "int32"), tvm.tirx.Var("b", "int32")
test_case = tvm.testing.parameter(
TestCase(a % b, (-4, 6), {a: (0, 6), b: (-5, 7)}),
)
class TestDivModAssumeNoZeroDivisor(BaseCompare):
"""Divmod non negative expression makes assumption that divide by
zero won't occur this assumption is important to get best result
from symbolic shape programs
"""
a, b = tvm.tirx.Var("a", "int32"), tvm.tirx.Var("b", "int32")
test_case = tvm.testing.parameter(
TestCase(a // b, (0, 6), {a: (0, 6), b: (0, POS_INF)}),
TestCase(a % b, (0, 6), {a: (0, 6), b: (0, POS_INF)}),
)
class TestMultipleCondition(BaseCompare):
a = tvm.tirx.Var("a", "int32")
test_case = tvm.testing.parameter(
TestCase(
a % 58 - 1,
(0, None),
known_bounds={a: (0, 128)},
constraint=tvm.tirx.all(1 <= a % 58, a % 58 < 57),
),
)
class TestBroadcastBound(BaseCompare):
a = tvm.tirx.Var("a", "int32")
test_case = tvm.testing.parameter(
TestCase(tvm.tirx.Broadcast(a, 4), (0, 128), {a: (0, 128)}),
)
class TestRampBound(BaseCompare):
a = tvm.tirx.Var("a", "int32")
test_case = tvm.testing.parameter(
TestCase(tvm.tirx.Ramp(a, 2, 4) + 2, (2, 128 + 2 * 3 + 2), {a: (0, 128)}),
)
class TestModularSetBound(BaseCompare):
analyzer = tvm.arith.Analyzer()
tx = tvm.tirx.Var("tx", "int32")
bx = tvm.tirx.Var("bx", "int32")
expr = (bx * 2048 + tx * 16) % 7168
test_case = tvm.testing.parameter(
TestCase(expr, (0, 7152), {bx: (0, 3584), tx: (0, 128)}),
)
if __name__ == "__main__":
tvm.testing.main()
@@ -0,0 +1,272 @@
# 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 pytest
import tvm
import tvm.testing
from tvm.tirx.buffer import decl_buffer
def test_deduce():
a = tvm.tirx.Var("a", "int32")
b = tvm.tirx.Var("b", "int32")
c = tvm.tirx.Var("c", "int32")
d = tvm.tirx.Var("d", "int32")
b_s = tvm.arith.IntervalSet(2, 3)
c_s = tvm.arith.IntervalSet(10, 15)
d_s = tvm.arith.IntervalSet(-3, -1)
zero = tvm.tirx.const(0, "int32")
fdiv = tvm.tirx.floordiv
e0 = (-b) * a + c - d
res0 = tvm.arith.deduce_bound(a, e0 >= 0, {b: b_s, c: c_s, d: d_s}, {})
ans0 = fdiv(d - c, b * -1)
tvm.testing.assert_prim_expr_equal(res0.max_value, ans0)
# expression containing variable a is on rhs
res0 = tvm.arith.deduce_bound(a, zero <= e0, {b: b_s, c: c_s, d: d_s}, {})
tvm.testing.assert_prim_expr_equal(res0.max_value, ans0)
e0 = d * a + c - d
res0 = tvm.arith.deduce_bound(a, e0 >= 0, {b: b_s, c: c_s, d: d_s}, {})
ans0 = fdiv(d - c, d)
tvm.testing.assert_prim_expr_equal(res0.max_value, ans0)
# expression containing variable a is on rhs
res0 = tvm.arith.deduce_bound(a, zero <= e0, {b: b_s, c: c_s, d: d_s}, {})
tvm.testing.assert_prim_expr_equal(res0.max_value, ans0)
e1 = a * 4 + b < c
res1 = tvm.arith.deduce_bound(a, e1, {b: b_s, c: c_s, d: d_s}, {})
ans1 = fdiv(c - 1 - b, 4)
tvm.testing.assert_prim_expr_equal(res1.max_value, ans1)
# expression containing variable a is on rhs
e1 = c > a * 4 + b
res1 = tvm.arith.deduce_bound(a, e1, {b: b_s, c: c_s, d: d_s}, {})
tvm.testing.assert_prim_expr_equal(res1.max_value, ans1)
e2 = tvm.tirx.max(5, a * 4) < 0
res2 = tvm.arith.deduce_bound(a, e2, {b: b_s, c: c_s, d: d_s}, {})
assert str(res2.max_value) == "neg_inf"
assert str(res2.min_value) == "pos_inf"
# expression containing variable a is on rhs
e2 = zero < tvm.tirx.max(5, a * 4)
res2 = tvm.arith.deduce_bound(a, e2, {b: b_s, c: c_s, d: d_s}, {})
assert str(res2.max_value) == "neg_inf"
assert str(res2.min_value) == "pos_inf"
e3 = (-b) + a * c - d
res3 = tvm.arith.deduce_bound(a, e3 >= 0, {b: b_s, c: c_s, d: d_s}, {b: b_s, d: d_s})
ans3 = fdiv(2, c) + 1
tvm.testing.assert_prim_expr_equal(res3.min_value, ans3)
res3 = tvm.arith.deduce_bound(a, zero <= e3, {b: b_s, c: c_s, d: d_s}, {b: b_s, d: d_s})
tvm.testing.assert_prim_expr_equal(res3.min_value, ans3)
# tests for `EQ` op
res4 = tvm.arith.deduce_bound(a, a == b, {}, {})
tvm.testing.assert_prim_expr_equal(res4.max_value, b)
tvm.testing.assert_prim_expr_equal(res4.min_value, b)
# Unsatisfiable `EQ`, variable as one of the Operand
res5 = tvm.arith.deduce_bound(a, (a == b), {b: b_s}, {b: b_s})
assert str(res5.max_value) == "neg_inf"
assert str(res5.min_value) == "pos_inf"
# variable `a` on the RHS side
res6 = tvm.arith.deduce_bound(a, 10 == a, {}, {})
tvm.testing.assert_prim_expr_equal(res6.max_value, 10)
tvm.testing.assert_prim_expr_equal(res6.min_value, 10)
# Add, Sub in `EQ`
e4 = (a - c) == (b + d)
ans4 = b + d + c
res7 = tvm.arith.deduce_bound(a, e4, {b: b_s, c: c_s, d: d_s}, {})
tvm.testing.assert_prim_expr_equal(res7.max_value, ans4)
tvm.testing.assert_prim_expr_equal(res7.min_value, ans4)
# Satisfiable Mul in `EQ` with negative sign
res8 = tvm.arith.deduce_bound(a, (5 * a == -10), {}, {})
tvm.testing.assert_prim_expr_equal(res8.max_value, -2)
tvm.testing.assert_prim_expr_equal(res8.min_value, -2)
# Unsatisfiable Mul in `EQ`
e5 = 4 * a == b
res9 = tvm.arith.deduce_bound(a, e5, {b: b_s}, {})
assert str(res9.max_value) == "neg_inf"
assert str(res9.min_value) == "pos_inf"
res10 = tvm.arith.deduce_bound(a, (b * a == b), {b: b_s}, {})
# simplifier is now able to prove symbolic relation (b * a % b == 0)
tvm.testing.assert_prim_expr_equal(res10.max_value, 1)
tvm.testing.assert_prim_expr_equal(res10.min_value, 1)
def test_check():
a = tvm.tirx.Var("a", "int32")
b = tvm.tirx.Var("b", "int32")
c = tvm.tirx.Var("c", "int32")
d = tvm.tirx.Var("d", "int32")
b_s = tvm.arith.IntervalSet(2, 3)
c_s = tvm.arith.IntervalSet(5, 7)
d_s = tvm.arith.IntervalSet(-3, -1)
# no compare operator
res1 = tvm.arith.deduce_bound(a, a + b, {b: b_s}, {})
assert res1.is_nothing()
# multiple compare operators
res2 = tvm.arith.deduce_bound(a, (a + b > 3).astype(c.ty) > c, {b: b_s, c: c_s}, {})
assert res2.is_nothing()
# multiple target variable
res2 = tvm.arith.deduce_bound(a, a * 2 - a > b, {b: b_s}, {})
assert res2.is_nothing()
def test_deduce_basic():
def test_basic(a1, a2, coff):
a = tvm.tirx.Var("a", "int32")
b = tvm.tirx.Var("b", "int32")
b_s = tvm.arith.IntervalSet(a1, a2)
e0 = b + a * coff + 3
res1 = tvm.arith.deduce_bound(a, e0 < 17, {b: b_s}, {b: b_s})
[x, y] = [res1.max_value, b_s.max_value] if coff > 0 else [res1.min_value, b_s.min_value]
tvm.testing.assert_prim_expr_equal((x * coff + 3 + y) < 17, True)
# expression containing variable a is on rhs
res1 = tvm.arith.deduce_bound(a, tvm.tirx.const(17, "int32") < e0, {b: b_s}, {b: b_s})
[x, y] = [res1.max_value, b_s.max_value] if coff < 0 else [res1.min_value, b_s.min_value]
tvm.testing.assert_prim_expr_equal((x * coff + 3 + y) > 17, True)
# expression containing variable a is on rhs
res1 = tvm.arith.deduce_bound(a, tvm.tirx.const(17, "int32") >= e0, {b: b_s}, {b: b_s})
[x, y] = [res1.max_value, b_s.max_value] if coff > 0 else [res1.min_value, b_s.min_value]
tvm.testing.assert_prim_expr_equal((x * coff + 3 + y) <= 17, True)
res1 = tvm.arith.deduce_bound(a, e0 >= 17, {b: b_s}, {b: b_s})
[x, y] = [res1.max_value, b_s.max_value] if coff < 0 else [res1.min_value, b_s.min_value]
tvm.testing.assert_prim_expr_equal((x * coff + 3 + y) >= 17, True)
test_basic(0, 4, 4)
test_basic(1, 5, 4)
test_basic(2, 6, 4)
test_basic(0, 4, -4)
test_basic(1, 5, -4)
test_basic(2, 6, -4)
def test_deduce_complex():
def test_complex(a1, a2, coff):
a = tvm.tirx.Var("a", "int32")
b = tvm.tirx.Var("b", "int32")
b_s = tvm.arith.IntervalSet(a1, a2)
e0 = (b * 3 + a * coff) * 4
res1 = tvm.arith.deduce_bound(a, e0 < 63, {b: b_s}, {b: b_s})
[t, x] = [res1.max_value, b_s.max_value] if coff > 0 else [res1.min_value, b_s.min_value]
tvm.testing.assert_prim_expr_equal(((x * 3 + t * coff) * 4) < 63, True)
# expression containing variable a is on rhs
res1 = tvm.arith.deduce_bound(a, tvm.tirx.const(63, "int32") >= e0, {b: b_s}, {b: b_s})
[t, x] = [res1.max_value, b_s.max_value] if coff > 0 else [res1.min_value, b_s.min_value]
tvm.testing.assert_prim_expr_equal(((x * 3 + t * coff) * 4) <= 63, True)
res1 = tvm.arith.deduce_bound(a, e0 > 63, {b: b_s}, {b: b_s})
[t, x] = [res1.max_value, b_s.max_value] if coff < 0 else [res1.min_value, b_s.min_value]
tvm.testing.assert_prim_expr_equal(((x * 3 + t * coff) * 4) > 63, True)
# expression containing variable a is on rhs
res1 = tvm.arith.deduce_bound(a, tvm.tirx.const(63, "int32") <= e0, {b: b_s}, {b: b_s})
[t, x] = [res1.max_value, b_s.max_value] if coff < 0 else [res1.min_value, b_s.min_value]
tvm.testing.assert_prim_expr_equal(((x * 3 + t * coff) * 4) >= 63, True)
test_complex(0, 4, 4)
test_complex(0, 4, -4)
test_complex(2, 6, 4)
test_complex(0, 4, -4)
test_complex(1, 5, -4)
test_complex(2, 6, -4)
def test_deduce_non_support():
a = tvm.tirx.Var("a", "int32")
def test_non_support(lhs):
res = tvm.arith.deduce_bound(a, lhs < 10, {}, {})
assert res.is_nothing()
test_non_support(tvm.tirx.floormod(a, 16))
test_non_support(tvm.tirx.Min(a, 16))
test_non_support(tvm.tirx.Max(a, 16))
test_non_support(tvm.tirx.LE(a, 16))
test_non_support(tvm.tirx.LT(a, 16))
test_non_support(tvm.tirx.GE(a, 16))
test_non_support(tvm.tirx.GT(a, 16))
test_non_support(tvm.tirx.EQ(a, 16))
test_non_support(tvm.tirx.NE(a, 16))
test_non_support(tvm.tirx.log(a))
test_non_support(tvm.tirx.BufferLoad(decl_buffer([16], "int32"), [a]))
def test_deduce_floordiv():
def do_test(gen_expr, dom_map, expect_min, expect_max):
a = tvm.tirx.Var("a", "int32")
expr = gen_expr(a)
res = tvm.arith.deduce_bound(a, expr, dom_map, dom_map)
if isinstance(expect_min, str):
assert str(res.min_value) == expect_min
else:
tvm.testing.assert_prim_expr_equal(res.min_value, expect_min)
if isinstance(expect_max, str):
assert str(res.max_value) == expect_max
else:
tvm.testing.assert_prim_expr_equal(res.max_value, expect_max)
# test basic cases
do_test(lambda a: a // 8 > 3, {}, 32, "pos_inf")
do_test(lambda a: a // 8 >= 3, {}, 24, "pos_inf")
do_test(lambda a: a // 8 < 3, {}, "neg_inf", 23)
do_test(lambda a: a // 8 <= 3, {}, "neg_inf", 31)
do_test(lambda a: a // 8 == 3, {}, "pos_inf", "neg_inf")
do_test(lambda a: a // 8 > -3, {}, -16, "pos_inf")
do_test(lambda a: a // 8 >= -3, {}, -24, "pos_inf")
do_test(lambda a: a // -8 > 3, {}, "neg_inf", -32)
do_test(lambda a: a // -8 >= 3, {}, "neg_inf", -24)
do_test(lambda a: a // -8 < 3, {}, -23, "pos_inf")
do_test(lambda a: a // -8 <= 3, {}, -31, "pos_inf")
do_test(lambda a: 8 // a >= 2, {}, "pos_inf", "neg_inf")
# test nested cases
b = tvm.tirx.Var("b", "int32")
bs = {b: tvm.arith.IntervalSet(2, 6)}
do_test(lambda a: b * 3 + a // 8 < 63, bs, "neg_inf", 359)
do_test(lambda a: b * 3 + a // 8 <= 63, bs, "neg_inf", 367)
do_test(lambda a: b * 3 + a // 8 > 63, bs, 464, "pos_inf")
do_test(lambda a: b * 3 + a // 8 >= 63, bs, 456, "pos_inf")
if __name__ == "__main__":
tvm.testing.main()
@@ -0,0 +1,55 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import tvm
import tvm.testing
def test_basic():
a = tvm.tirx.Var("a", "int32")
b = tvm.tirx.Var("b", "int32")
c = tvm.tirx.Var("c", "int32")
m = tvm.arith.detect_clip_bound(tvm.tirx.all(a * 1 < b * 6, a - 1 > 0), [a])
tvm.testing.assert_prim_expr_equal(m[1], b * 6 - 1)
assert m[0].value == 2
m = tvm.arith.detect_clip_bound(tvm.tirx.all(a * 1 < b * 6, a - 1 > 0), [a, b])
assert len(m) == 0
m = tvm.arith.detect_clip_bound(tvm.tirx.all(a + 10 * c <= 20, b - 1 > 0), [a, b])
tvm.testing.assert_prim_expr_equal(m[1], 20 - 10 * c)
tvm.testing.assert_prim_expr_equal(m[2], 2)
m = tvm.arith.detect_clip_bound(tvm.tirx.all(tvm.tirx.Not(a * 1 > b * 6), a - 1 > 0), [a])
tvm.testing.assert_prim_expr_equal(m[1], b * 6)
m = tvm.arith.detect_clip_bound(tvm.tirx.all(tvm.tirx.Min(a, b) > 3, a - 10 < 0), [a, b])
tvm.testing.assert_prim_expr_equal(m[0], 4)
tvm.testing.assert_prim_expr_equal(m[1], 9)
tvm.testing.assert_prim_expr_equal(m[2], 4)
def test_trivial_eq():
a = tvm.tirx.Var("a", "int32")
b = tvm.tirx.Var("b", "int32")
m = tvm.arith.detect_clip_bound(b == 3, [a, b])
tvm.testing.assert_prim_expr_equal(m[2], 3)
tvm.testing.assert_prim_expr_equal(m[3], 3)
m = tvm.arith.detect_clip_bound(tvm.tirx.all(a == 4, b == 3), [a, b])
tvm.testing.assert_prim_expr_equal(m[0], 4)
tvm.testing.assert_prim_expr_equal(m[1], 4)
tvm.testing.assert_prim_expr_equal(m[2], 3)
tvm.testing.assert_prim_expr_equal(m[3], 3)
if __name__ == "__main__":
test_basic()
@@ -0,0 +1,82 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import tvm
import tvm.testing
def test_basic():
a = tvm.tirx.Var("a", "int32")
b = tvm.tirx.Var("b", "int32")
m = tvm.arith.detect_linear_equation(a * 4 + b * 6 + 7, [a])
assert m[0].value == 4
tvm.testing.assert_prim_expr_equal(m[1], b * 6 + 7)
m = tvm.arith.detect_linear_equation(a * 4 * (a + 1) + b * 6 + 7, [a])
assert len(m) == 0
m = tvm.arith.detect_linear_equation(a * 4 + (a + 1) + b * 6 + 7, [a])
assert m[0].value == 5
tvm.testing.assert_prim_expr_equal(m[1], b * 6 + 7 + 1)
m = tvm.arith.detect_linear_equation(a * b + 7, [a])
assert m[0] == b
m = tvm.arith.detect_linear_equation(b * 7, [a])
assert m[0].value == 0
m = tvm.arith.detect_linear_equation(b * 7, [])
assert len(m) == 1
tvm.testing.assert_prim_expr_equal(m[0], b * 7)
c = tvm.tirx.Var("c", "uint32")
m = tvm.arith.detect_linear_equation(128 - c, [c])
assert m[0].value == -1
def test_multivariate():
v = [tvm.tirx.Var(f"v{i}", "int32") for i in range(4)]
b = tvm.tirx.Var("b", "int32")
m = tvm.arith.detect_linear_equation(v[0] * (b + 4) + v[0] + v[1] * 8, v)
tvm.testing.assert_prim_expr_equal(m[0], b + 5)
assert m[1].value == 8
m = tvm.arith.detect_linear_equation(v[0] * (b + 4) + v[0] + v[1] * 8 * v[2], v)
assert len(m) == 0
m = tvm.arith.detect_linear_equation(v[0] * (b + 4) + v[0] + v[1] * 8 * v[1] + v[3], v)
assert len(m) == 0
m = tvm.arith.detect_linear_equation(((v[0] * b + v[1]) * 8 + v[2] + 1) * 2, v)
assert m[1].value == 16
assert m[2].value == 2
assert m[len(m) - 1].value == 2
m = tvm.arith.detect_linear_equation((v[0] - v[1]), [v[2]])
assert m[0].value == 0
tvm.testing.assert_prim_expr_equal(m[1], v[0] - v[1])
m = tvm.arith.detect_linear_equation((v[0] - v[1]), [])
assert len(m) == 1
tvm.testing.assert_prim_expr_equal(m[0], v[0] - v[1])
if __name__ == "__main__":
test_basic()
test_multivariate()
@@ -0,0 +1,95 @@
# 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 pytest
import tvm_ffi
import tvm
from tvm.script import tirx as T
@T.prim_func(s_tir=True)
def scalar_func(a: T.handle, b: T.handle):
m = T.int32()
n = T.meta_var(100)
A = T.match_buffer(a, (n, m))
B = T.match_buffer(b, (n, m))
for i, j in T.grid(n, m):
A[i, j] = B[i - 1, j + 1] + A[i - 1, j - 1]
def test_domain_touched():
func = scalar_func
a, b = [func.buffer_map[var] for var in func.params]
ir = func.body
a_domain_r = tvm.arith._ffi_api.DomainTouched(ir, a, True, False)
assert a_domain_r[0].min.value == -1
assert a_domain_r[0].extent.value == 100
assert a_domain_r[1].min.value == -1
assert a_domain_r[1].extent.name == "m"
a_domain_w = tvm.arith._ffi_api.DomainTouched(ir, a, False, True)
assert a_domain_w[0].min.value == 0
assert a_domain_w[0].extent.value == 100
assert a_domain_w[1].min.value == 0
assert a_domain_w[1].extent.name == "m"
a_domain_rw = tvm.arith._ffi_api.DomainTouched(ir, a, True, True)
assert a_domain_rw[0].min.value == -1
assert a_domain_rw[0].extent.value == 101
assert a_domain_rw[1].min.value == -1
assert isinstance(a_domain_rw[1].extent, tvm.tirx.Add)
assert a_domain_rw[1].extent.a.name == "m"
assert a_domain_rw[1].extent.b.value == 1
b_domain_r = tvm.arith._ffi_api.DomainTouched(ir, b, True, False)
assert b_domain_r
assert b_domain_r[0].min.value == -1
assert b_domain_r[0].extent.value == 100
assert b_domain_r[1].min.value == 1
assert b_domain_r[1].extent.name == "m"
b_domain_w = tvm.arith._ffi_api.DomainTouched(ir, b, False, True)
assert isinstance(b_domain_w, tvm_ffi.Array)
assert len(b_domain_w) == 0
def test_domain_touched_vector():
pytest.skip("BufferRegion arithmetic in expressions not supported")
m = tvm.runtime.convert(128)
@T.prim_func(s_tir=True)
def func(a: T.handle, b: T.handle, n: T.int32):
A = T.match_buffer(a, (n * m,))
B = T.match_buffer(b, (n * m,))
for i in T.serial(n):
A[i * m : (i + 1) * m : 1] = A[i * m : (i + 1) * m : 1] + B[i * m : (i + 1) * m : 1]
a, b = [func.buffer_map[var] for var in func.params[:2]]
assert tvm.arith._ffi_api.DomainTouched(func.body, a, True, False)[0].extent.value == 128
assert tvm.arith._ffi_api.DomainTouched(func.body, a, True, False)[0].extent.value == 128
assert tvm.arith._ffi_api.DomainTouched(func.body, a, True, True)[0].extent.value == 128
assert tvm.arith._ffi_api.DomainTouched(func.body, b, True, False)[0].extent.value == 128
assert tvm.arith._ffi_api.DomainTouched(func.body, b, True, False)[0].extent.value == 128
if __name__ == "__main__":
test_domain_touched()
+456
View File
@@ -0,0 +1,456 @@
# 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.arith.analyzer import Analyzer
class IntSetChecker:
def __init__(self):
self.analyzer = tvm.arith.Analyzer()
def verify(self, data, dmap, expected):
res = self.analyzer.int_set(data, dmap)
def err_msg():
return f"\ndata={data}\ndmap={dmap}\nres={res}\nexpected={expected}"
assert self.analyzer.can_prove_equal(res.min_value, expected[0]), err_msg()
assert self.analyzer.can_prove_equal(res.max_value, expected[1]), err_msg()
def test_basic():
s = tvm.arith.IntervalSet(2, 3)
assert s.min_value.value == 2
assert s.max_value.value == 3
s = tvm.arith.IntSet.single_point(2)
assert s.min_value.value == 2
assert s.max_value.value == 2
def test_vector():
base = 10
stride = 3
lanes = 2
s = tvm.arith.IntSet.vector(tvm.tirx.Ramp(base, stride, lanes))
assert s.min_value.value == base
assert s.max_value.value == base + stride * (lanes - 1)
def test_scalable_vector():
base = 5
s = tvm.arith.IntSet.vector(tvm.tirx.Ramp(base, 2, tvm.tirx.vscale() * 4))
assert s.min_value.value == base
assert s.max_value.same_as(tvm.arith.int_set.pos_inf())
def test_add_sub():
ck = IntSetChecker()
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
ck.verify(x + y, {x: tvm.arith.IntervalSet(0, 10)}, (y, 10 + y))
ck.verify(x + y, {x: tvm.arith.IntervalSet(0, 10), y: tvm.arith.IntervalSet(1, 11)}, (1, 21))
ck.verify(x - y, {x: tvm.arith.IntervalSet(0, 10), y: tvm.arith.IntervalSet(1, 11)}, (-11, 9))
def test_mul_div():
ck = IntSetChecker()
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
tdiv = tvm.tirx.truncdiv
ck.analyzer.update(y, tvm.arith.ConstIntBound(1, 100), override=True)
ck.verify(x * y, {x: tvm.arith.IntervalSet(0, 10)}, (0, 10 * y))
ck.verify(x * 2, {x: tvm.arith.IntervalSet(1, 10)}, (2, 20))
ck.verify(x * -2, {x: tvm.arith.IntervalSet(1, 10)}, (-20, -2))
ck.verify(tdiv(x, y), {x: tvm.arith.IntervalSet(0, 10)}, (0, tdiv(10, y)))
ck.verify(tdiv(x, 2), {x: tvm.arith.IntervalSet(1, 10)}, (0, 5))
fld = tvm.tirx.floordiv
ck.verify(fld(x, y), {x: tvm.arith.IntervalSet(0, 10)}, (0, fld(10, y)))
ck.verify(fld(x, 2), {x: tvm.arith.IntervalSet(-1, 10)}, (-1, 5))
def test_mod():
ck = IntSetChecker()
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
tmod = tvm.tirx.truncmod
ck.analyzer.update(y, tvm.arith.ConstIntBound(1, 100), override=True)
ck.verify(tmod(x, y), {x: tvm.arith.IntervalSet(0, 10)}, (0, y - 1))
ck.verify(tmod(x, 10), {x: tvm.arith.IntervalSet(1, 10)}, (0, 9))
flm = tvm.tirx.floormod
ck.verify(flm(x, 10), {x: tvm.arith.IntervalSet(-10, 10)}, (0, 9))
ck.verify(flm(x, 10), {x: tvm.arith.IntervalSet(3, 5)}, (3, 5))
ck.verify(flm(x, 10), {x: tvm.arith.IntervalSet(13, 15)}, (3, 5))
ck.verify(flm(x, 10), {x: tvm.arith.IntervalSet(3, 15)}, (0, 9))
ck.verify(flm(x, 10), {x: tvm.arith.IntervalSet(3, 11)}, (0, 9))
ck.verify(flm(x, 10), {x: tvm.arith.IntervalSet(1, 21)}, (0, 9))
fld = tvm.tirx.floordiv
z = tvm.tirx.Var("z", "int32")
ck.analyzer.bind(x, tvm.ir.Range.from_min_extent(0, 3))
ck.verify(
flm(y, 8),
{y: tvm.arith.IntervalSet(z * 8 + x * 4, z * 8 + x * 4 + 3)},
(
z * 8 + x * 4 - 8 * fld(z * 8 + x * 4, 8),
z * 8 + x * 4 + 3 - 8 * fld(z * 8 + x * 4, 8),
),
)
ck1 = IntSetChecker()
ck1.analyzer.bind(x, tvm.ir.Range.from_min_extent(0, 2))
ck1.verify(
flm(y, 8), {y: tvm.arith.IntervalSet(z * 8 + x * 4, z * 8 + x * 4 + 3)}, (x * 4, x * 4 + 3)
)
def test_max_min():
ck = IntSetChecker()
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
ck.verify(tvm.tirx.max(x, x + 1), {x: tvm.arith.IntervalSet(0, 10)}, (1, 11))
ck.verify(tvm.tirx.min(x - 1, x + 1), {x: tvm.arith.IntervalSet(0, 10)}, (-1, 9))
ck.verify(tvm.tirx.min(x, y), {}, (tvm.tirx.min(x, y), tvm.tirx.min(x, y)))
ck.verify(tvm.tirx.max(x, y), {}, (tvm.tirx.max(x, y), tvm.tirx.max(x, y)))
def test_select():
ck = IntSetChecker()
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
ck.verify(tvm.tirx.Select(x > 0, x - 1, x + 1), {x: tvm.arith.IntervalSet(0, 10)}, (-1, 11))
def check_region_bound(expect_region, var_dom, mode, predicate=None):
"""Helper to check region bound estimation.
Parameters
----------
expect_region: dict
The keys are of form (begin, end) or Expr as a single point. The values are
expected estimated region or region dict on different bindings.
var_dom: dict
Map var to iteration domain range.
mode: str
Specify "lowerbound", "upperbound" or else use strict bound estimation.
predicate: Expr
Extra predicate, defaults to True.
"""
if predicate is None:
predicate = tvm.tirx.IntImm("bool", 1)
region = []
expect = []
for k, v in expect_region.items():
if not isinstance(k, tuple | list):
k = (k, k + 1)
region.append(tvm.ir.Range.from_min_extent(k[0], Analyzer().simplify(k[1] - k[0])))
expect.append(v)
if mode == "lowerbound":
result = tvm.arith.estimate_region_lower_bound(
region=region, var_dom=var_dom, predicate=predicate
)
elif mode == "upperbound":
result = tvm.arith.estimate_region_upper_bound(
region=region, var_dom=var_dom, predicate=predicate
)
else:
result = tvm.arith.estimate_region_strict_bound(
region=region, var_dom=var_dom, predicate=predicate
)
if result is None:
assert all([_ is None for _ in expect])
return
assert len(result) == len(expect)
for intset, expect_desc in zip(result, expect):
if isinstance(expect_desc, dict):
# check range on different free var bindings
for binding in expect_desc:
analyzer = Analyzer()
for k, v in binding:
analyzer.bind(k, v)
expect_begin, expect_end = expect_desc[binding]
result_begin = analyzer.simplify(intset.min_value, 3)
result_end = analyzer.simplify(intset.max_value + 1, 3)
assert analyzer.can_prove_equal(result_begin - expect_begin, 0), (
f"{result_begin} vs {expect_begin}"
)
assert analyzer.can_prove_equal(result_end - expect_end, 0), (
f"{result_end} vs {expect_end}"
)
else:
# check range
expect_begin, expect_end = expect_desc
analyzer = Analyzer()
assert analyzer.can_prove_equal(intset.min_value - expect_begin, 0), (
f"{intset.min_value} vs {expect_begin}"
)
assert analyzer.can_prove_equal(intset.max_value - expect_end + 1, 0), (
f"{intset.max_value} vs {expect_end - 1}"
)
def test_region_bound_not_independent():
# (i, i+2) and (i+2, i+4) are dependent, this the lowerbound is not available
i = tvm.tirx.Var("i", "int32")
var_dom = {
i: tvm.ir.Range(begin=0, end=64),
}
check_region_bound({(i, i + 2): None, (i + 2, i + 4): None}, var_dom, mode="lowerbound")
check_region_bound({(i, i + 2): (0, 65), (i + 2, i + 4): (2, 67)}, var_dom, mode="upperbound")
# when only a subset of access indices are affine
i, j, k = tvm.tirx.Var("i", "int32"), tvm.tirx.Var("j", "int32"), tvm.tirx.Var("k", "int32")
var_dom = {
i: tvm.ir.Range(begin=0, end=16),
j: tvm.ir.Range(begin=0, end=16),
k: tvm.ir.Range(begin=0, end=16),
}
check_region_bound(
{i // 4: None, j * 4 + i % 4: None, tirx.truncdiv(k, 2): None},
var_dom,
predicate=j * 4 + i % 4 > 3,
mode="lowerbound",
)
check_region_bound(
{i // 4: (0, 4), j * 4 + i % 4: (4, 64), tirx.truncdiv(k, 2): (0, 8)},
var_dom,
predicate=j * 4 + i % 4 > 3,
mode="upperbound",
)
def test_region_bound_stride_too_wide():
i = tvm.tirx.Var("i", "int32")
var_dom = {i: tvm.ir.Range(begin=0, end=64)}
check_region_bound({(i * 4, i * 4 + 2): None}, var_dom, mode="lowerbound")
check_region_bound({(i * 4, i * 4 + 2): (0, 254)}, var_dom, mode="upperbound")
def test_region_bound_small_stride():
i = tvm.tirx.Var("i", "int32")
var_dom = {
i: tvm.ir.Range(begin=0, end=64),
}
check_region_bound({(i * 4, i * 4 + 8): (0, 260)}, var_dom, mode="lowerbound")
def test_region_lower_bound_split_predicate():
x_o = tvm.tirx.Var("xo", "int32")
x_i = tvm.tirx.Var("xi", "int32")
x = x_o * 4 + x_i
var_dom = {
x_o: tvm.ir.Range(begin=0, end=16),
x_i: tvm.ir.Range(begin=0, end=4),
}
check_region_bound({(x * 4, x * 4 + 8): (0, 256)}, var_dom, predicate=x < 63, mode="lowerbound")
check_region_bound(
{(x * 4, x * 4 + 8): (0, 256), (x * 3, x * 3 + 5): (0, 191)},
var_dom,
predicate=x < 63,
mode="upperbound",
)
def test_region_lower_bound_multiple_variables():
div = tvm.tirx.floordiv
mod = tvm.tirx.floormod
x = tvm.tirx.Var("x", "int32")
wid = tvm.tirx.Var("wid", "int32")
i = div(x, 16)
j = div(mod(x, 16), 4) * 8 + mod(x, 4) + div(wid, 32) * 4
k = wid % 32
var_dom = {
x: tvm.ir.Range(begin=0, end=32),
wid: tvm.ir.Range(begin=0, end=64),
}
check_region_bound({i: (0, 2), j: (0, 32), k: (0, 32)}, var_dom, mode="lowerbound")
def test_region_lower_bound_negative_scale():
i = tvm.tirx.Var("i", "int32")
j = tvm.tirx.Var("j", "int32")
var_dom = {
i: tvm.ir.Range(begin=0, end=4),
j: tvm.ir.Range(begin=0, end=4),
}
check_region_bound(
{(1 - i, 5 - i): (-2, 5), (20 - j * 4, 36 - j * 4): (8, 36)}, var_dom, mode="lowerbound"
)
def test_region_lower_bound_for_non_perfect_tile():
h1 = tvm.tirx.Var("h1", "int32")
h2 = tvm.tirx.Var("h2", "int32")
h3 = tvm.tirx.Var("h3", "int32")
# non-uniform tiling, single inner variable
var_dom = {
h2: tvm.ir.Range(begin=0, end=10),
}
check_region_bound(
{
h3 * 8 + h2: {
(): (
tvm.tirx.max(h3 * 8, 1),
tvm.tirx.min(0, h3 * 8 - 214) + 224,
),
((h3, 0),): (1, 10), # h3 == 0: region is [1, 10)
((h3, 10),): (h3 * 8, h3 * 8 + 10), # 0 < h3 <= 26: region is [h3 * 8, h3 * 8 + 10)
((h3, 27),): (h3 * 8, 224), # h3 > 26: region is [h3 * 8, 224)
}
},
var_dom,
predicate=tvm.tirx.all(1 <= h3 * 8 + h2, h3 * 8 + h2 < 224),
mode="lowerbound",
)
# non-uniform tiling, two inner variables
var_dom = {
h1: tvm.ir.Range(begin=0, end=5),
h2: tvm.ir.Range(begin=0, end=2),
}
check_region_bound(
{
h3 * 8 + h2 * 5 + h1: {
(): (
tvm.tirx.max(h3 * 8, 1),
tvm.tirx.min(0, h3 * 8 - 214) + 224,
),
((h3, 0),): (1, 10),
((h3, 10),): (h3 * 8, h3 * 8 + 10),
((h3, 27),): (h3 * 8, 224),
}
},
var_dom,
predicate=tvm.tirx.all(1 <= h3 * 8 + h2 * 5 + h1, h3 * 8 + h2 * 5 + h1 < 224),
mode="lowerbound",
)
# lowerbound should fail on incompatible predicates
check_region_bound(
{h3 * 8 + h2 * 5 + h1: None},
var_dom,
predicate=tvm.tirx.all(1 <= h3 * 8 + h2 * 5 + h1, h3 * 8 + h1 * 2 + h2 < 224),
mode="lowerbound",
)
check_region_bound(
{h3 * 8 + h2 * 5 + h1: (h3 * 8, h3 * 8 + 10)},
var_dom,
predicate=tvm.tirx.all(1 <= h3 * 8 + h2 * 5 + h1, h3 * 8 + h1 * 2 + h2 < 224),
mode="upperbound",
)
def test_region_lower_bound_unfusable():
var_dom = {
tvm.tirx.Var("i", "int32"): tvm.ir.Range(8),
tvm.tirx.Var("j", "int32"): tvm.ir.Range(4),
}
i, j = var_dom
check_region_bound({(i + j) // 2: (0, 6)}, var_dom, mode="lowerbound")
def test_union_lower_bound():
neg_inf = tvm.arith.int_set.neg_inf()
pos_inf = tvm.arith.int_set.pos_inf()
set_0 = tvm.arith.IntervalSet(min_value=neg_inf, max_value=0)
set_1 = tvm.arith.IntervalSet(min_value=1, max_value=pos_inf)
result = tvm.arith.int_set.union_lower_bound([set_0, set_1])
assert result.min_value.same_as(neg_inf)
assert result.max_value.same_as(pos_inf)
set_2 = tvm.arith.IntervalSet(min_value=pos_inf, max_value=neg_inf)
result = tvm.arith.int_set.union_lower_bound([set_0, set_1, set_2])
assert result.min_value.same_as(neg_inf)
assert result.max_value.same_as(pos_inf)
def test_modular_set():
ck = IntSetChecker()
x = tvm.tirx.Var("x", "int32")
y = tvm.tirx.Var("y", "int32")
expr = (x * 2048 + y * 16) % 7168
ck.verify(
expr, {x: tvm.arith.IntervalSet(0, 128), y: tvm.arith.IntervalSet(0, 3584)}, (0, 7152)
)
def test_relax_deep_variable_dependency_chain():
"""Regression test for exponential variable-relaxation blowup.
When a variable's interval bound references another variable that is also in
the domain map, the evaluator relaxes it transitively. A diamond-shaped
chain -- where each variable's bound references the next one in *both* its
min and its max -- used to be re-expanded along every path, costing
O(2^depth) and hanging indefinitely. The relaxation is now memoized per
variable, so this completes in linear time.
"""
ck = IntSetChecker()
n = 64 # 2^64 expansions without memoization; trivially fast with it.
xs = [tvm.tirx.Var(f"x{i}", "int32") for i in range(n + 1)]
dmap = {xs[i]: tvm.arith.IntervalSet(xs[i + 1] - 1, xs[i + 1] + 1) for i in range(n)}
dmap[xs[n]] = tvm.arith.IntervalSet(0, 100)
# x0 relaxes through the whole chain: [0 - n, 100 + n].
ck.verify(xs[0], dmap, (-n, 100 + n))
def test_relax_cyclic_variable_dependency():
"""A cyclic variable dependency must terminate (and stay symbolic)."""
ana = tvm.arith.Analyzer()
x = tvm.tirx.Var("x", "int32")
y = tvm.tirx.Var("y", "int32")
# x depends on y and y depends on x: relaxation must not loop forever.
dmap = {x: tvm.arith.IntervalSet(y, y), y: tvm.arith.IntervalSet(x, x)}
res = ana.int_set(x, dmap)
assert res is not None
def test_estimate_region_accepts_external_analyzer():
i = tvm.tirx.Var("i", "int32")
tile = tvm.tirx.Var("tile", "int32")
region = [tvm.ir.Range.from_min_extent(i % tile, 1)]
dom = {i: tvm.ir.Range(0, 16)}
# Without knowing `tile`, the affine detection fails for exact bounds.
assert tvm.arith.estimate_region_lower_bound(region, dom, True) is None
assert tvm.arith.estimate_region_strict_bound(region, dom, True) is None
upper_without_analyzer = tvm.arith.estimate_region_upper_bound(region, dom, True)
analyzer = tvm.arith.Analyzer()
analyzer.bind(tile, tvm.tirx.const(4, "int32"))
# The external binding lets the affine detection succeed.
for estimate_region in [
tvm.arith.estimate_region_lower_bound,
tvm.arith.estimate_region_strict_bound,
tvm.arith.estimate_region_upper_bound,
]:
result = estimate_region(region, dom, True, analyzer=analyzer)
assert result is not None
assert analyzer.can_prove_equal(result[0].min_value, 0)
assert analyzer.can_prove_equal(result[0].max_value, 3)
# The upper-bound fallback without analyzer is safe but much wider.
assert not analyzer.can_prove_equal(upper_without_analyzer[0].min_value, 0)
if __name__ == "__main__":
tvm.testing.main()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,234 @@
# 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 te
def test_cast():
analyzer = tvm.arith.Analyzer()
x = tvm.tirx.Var("x", "int8")
m = analyzer.modular_set((x * 3).astype("uint32"))
assert m.coeff == 3
assert m.base == 0
m = analyzer.modular_set((x * 3 + 1).astype("float32").astype("int32"))
assert m.coeff == 3
assert m.base == 1
def test_add_sub():
analyzer = tvm.arith.Analyzer()
x, y = tvm.tirx.Var("x", "int64"), tvm.tirx.Var("y", "int64")
m = analyzer.modular_set(x * 6 + y * 4)
assert m.coeff == 2
assert m.base == 0
analyzer.bind(y, x * 4 + 1)
m = analyzer.modular_set(1 - y)
assert m.coeff == 4
assert m.base == 0
def test_mul():
analyzer = tvm.arith.Analyzer()
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
m = analyzer.modular_set((x * 4 + 2) * (y * 6 + 1))
assert m.coeff == 4
assert m.base == 2
def test_shift_left():
analyzer = tvm.arith.Analyzer()
x, y = te.var("x"), te.var("y")
m = analyzer.modular_set((x * 4 + 2) << 2)
assert m.coeff == 16
assert m.base == 8
def test_floormod():
analyzer = tvm.arith.Analyzer()
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
m = analyzer.modular_set(tvm.tirx.floormod(x * 128 + y * 4, 256))
assert m.coeff == 4
assert m.base == 0
def test_div_shift():
analyzer = tvm.arith.Analyzer()
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
# not sure if x is non-negative
tdiv = tvm.tirx.truncdiv
m = analyzer.modular_set(tdiv(x * 4 + 2, 2))
assert m.coeff == 1
assert m.base == 0
# right shift always round down so it is fine
m = analyzer.modular_set((x * 4 + 2) >> 1)
assert m.coeff == 2
assert m.base == 1
fld = tvm.tirx.floordiv
m = analyzer.modular_set(fld(x * 4 + 2, 2))
assert m.coeff == 2
assert m.base == 1
# x is non-negative
analyzer.update(x, tvm.arith.ConstIntBound(0, 100))
m = analyzer.modular_set(tdiv(x * 4 + 2, 2))
assert m.coeff == 2
assert m.base == 1
def test_mod():
analyzer = tvm.arith.Analyzer()
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
tmod = tvm.tirx.truncmod
fmod = tvm.tirx.floormod
# not sure if x is non-negative
m = analyzer.modular_set(tmod(x * 4 + 1, 4))
assert m.coeff == 1
assert m.base == 0
# no need to be positive if base == 0
m = analyzer.modular_set(tmod(x * 4, 4))
assert m.coeff == 4
assert m.base == 0
# floor mod tests
m = analyzer.modular_set(fmod(x * 4 + 3, 2))
assert m.coeff == 2
assert m.base == 1
m = analyzer.modular_set(fmod(x * 4 + 3, 8))
assert m.coeff == 4
assert m.base == 3
# x is non-negative
analyzer.update(x, tvm.arith.ConstIntBound(0, 100))
m = analyzer.modular_set(tmod(x * 4 + 3, 2))
assert m.coeff == 2
assert m.base == 1
def test_min_max_select():
analyzer = tvm.arith.Analyzer()
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
m = analyzer.modular_set(tvm.tirx.min(x * 3, y * 9))
assert m.coeff == 3
assert m.base == 0
m = analyzer.modular_set(tvm.tirx.max(x * 3 + 1, y * 9 + 4))
assert m.coeff == 3
assert m.base == 1
m = analyzer.modular_set(tvm.tirx.Select(x > 0, x * 3 + 1, y * 9 + 2))
assert m.coeff == 1
assert m.base == 0
def test_mix_index():
a = tvm.tirx.Var("a", "int32")
b = tvm.tirx.Var("b", "int32")
analyzer = tvm.arith.Analyzer()
tdiv = tvm.tirx.truncdiv
m = analyzer.modular_set(a * 4 + b * 6 + 7)
assert m.coeff == 2
assert m.base == 1
m = analyzer.modular_set((a * 4 + 1) * (b * 8 + 3))
assert m.coeff == 4
assert m.base == 3
m = analyzer.modular_set(tdiv(a * 4 + 1, b * 8 + 3))
assert m.coeff == 1
assert m.base == 0
m = analyzer.modular_set((a * 4 + 1) * tdiv(b * 8, 4))
assert m.coeff == 2
assert m.base == 0
m = analyzer.modular_set((a * 12 + 1) - (b * 3 * 7 + 2))
assert m.coeff == 3
assert m.base == 2
m = analyzer.modular_set(a * 12 + tvm.tirx.min(b * 3 * 7, 2))
assert m.coeff == 1
assert m.base == 0
def test_constraint_scope():
a = tvm.tirx.Var("a", "int32")
b = tvm.tirx.Var("b", "int32")
analyzer = tvm.arith.Analyzer()
tmod = tvm.tirx.truncmod
with analyzer.constraint_scope(tmod(b, 4) == 2):
m = analyzer.modular_set(b + 1)
assert m.coeff == 4
assert m.base == 3
with analyzer.constraint_scope(tmod(a, 2) == 1):
m = analyzer.modular_set(b + a * 2)
assert m.coeff == 4
assert m.base == 0
m = analyzer.modular_set(b + a * 2)
assert m.coeff == 2
assert m.base == 0
m = analyzer.modular_set(b + 1)
assert m.coeff == 1
assert m.base == 0
def test_intersect():
a = tvm.tirx.Var("a", "int32")
analyzer = tvm.arith.Analyzer()
tmod = tvm.tirx.truncmod
with analyzer.constraint_scope(tmod(a, 4) == 1):
with analyzer.constraint_scope(tmod(a, 3) == 1):
m = analyzer.modular_set(a)
assert m.coeff == 12
assert m.base == 1
with analyzer.constraint_scope(tmod(a, 3) == 2):
with analyzer.constraint_scope(tmod(a, 5) == 3):
with analyzer.constraint_scope(tmod(a, 7) == 2):
m = analyzer.modular_set(a)
assert m.coeff == 105
assert m.base == 23
def test_let():
analyzer = tvm.arith.Analyzer()
x = tvm.tirx.Var("x", "int32")
y = tvm.tirx.Var("y", "int32")
m = analyzer.modular_set(tvm.tirx.Let(x, y * 10, x + 1))
assert m.coeff == 10
assert m.base == 1
def test_bitwise_and():
analyzer = tvm.arith.Analyzer()
x = tvm.tirx.Var("x", "int32")
y = tvm.tirx.Var("y", "int32")
# RHS of bitwise_and is 2^p - 1
m = analyzer.modular_set((x * 16 + y * 4) & 31)
assert m.coeff == 4
assert m.base == 0
# arbitrary RHS
m = analyzer.modular_set((x * 16 + y * 4) & 17)
assert m.coeff == 1
assert m.base == 0
if __name__ == "__main__":
tvm.testing.main()
File diff suppressed because it is too large Load Diff
+195
View File
@@ -0,0 +1,195 @@
# 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 pytest
import tvm
import tvm.ir
import tvm.testing
from tvm import tirx
from tvm.script import tirx as T
def test_simplify_reshape_flattened_index():
ana = tvm.arith.Analyzer()
i0 = tirx.Var("i0", "int64")
i1 = tirx.Var("i1", "int64")
ana.bind(i0, tvm.ir.Range(0, 8))
ana.bind(i1, tvm.ir.Range(0, 3))
i_flattened = i0 * 3 + i1
tvm.ir.assert_structural_equal(
ana.simplify((i_flattened) // 12 * 12 + (i_flattened) % 12 // 4 * 4 + (i_flattened) % 4),
i_flattened,
)
dtype = tvm.testing.parameter(
"uint8",
"uint16",
"uint32",
"uint64",
"int8",
"int16",
"int32",
"int64",
"float16",
"float32",
"float64",
)
def test_can_prove_self_identity(dtype):
ana = tvm.arith.Analyzer()
n = tirx.Var("n", dtype)
assert ana.can_prove(n == n)
def test_can_prove_self_equal_to_self(dtype):
ana = tvm.arith.Analyzer()
n = tirx.Var("n", dtype)
assert ana.can_prove_equal(n, n)
def test_simplify_symbolic_comparison():
ana = tvm.arith.Analyzer()
i0 = tirx.Var("i0", "int64")
i1 = tirx.Var("i1", "int64")
n, m = tvm.tirx.Var("n", "int64"), tvm.tirx.Var("m", "int64")
outer = (n + 31) // 32
PS = tvm.arith.ProofStrength
non_negative = tvm.arith.ConstIntBound(0, tvm.arith.ConstIntBound.POS_INF)
ana.update(n, non_negative)
ana.update(m, non_negative)
ana.bind(i0, tvm.ir.Range(0, outer))
ana.bind(i1, tvm.ir.Range(0, 32))
assert not ana.can_prove(i0 * 32 + i1 < (n + 31) // 32 * 32, PS.DEFAULT)
assert ana.can_prove(i0 * 32 + i1 < (n + 31) // 32 * 32, PS.SYMBOLIC_BOUND)
assert ana.can_prove(i0 * 32 + i1 < (n + 31) // 32 * 32 + m, PS.SYMBOLIC_BOUND)
assert ana.can_prove(i0 * 32 + i1 + 1 <= (n + 31) // 32 * 32, PS.SYMBOLIC_BOUND)
assert ana.can_prove((n + 31) // 32 * 32 >= i0 * 32 + i1 + 1, PS.SYMBOLIC_BOUND)
assert ana.can_prove((n + 31) // 32 * 32 >= i0 * 32 + i1, PS.SYMBOLIC_BOUND)
# These tests exercised arith::CanProve's substitution-based proof loop for
# vscale-bearing expressions (iterating over known vscale values for a VLA target).
# That loop has been removed -- arith no longer attempts target-dependent proofs
# about scalable-vector lengths. The LOG(WARNING) for non-VLA targets is also gone.
@pytest.mark.xfail(reason="arith no longer proves vscale-bearing inequalities via substitution")
@pytest.mark.parametrize(
"expression",
[
T.vscale() * 32 < T.vscale() * 64,
T.vscale() * 2 * (T.vscale() * 2) >= T.vscale() * 4,
(T.vscale() * 4 + 114) // (T.vscale() * 4) * (T.vscale() * 4) >= 115,
64 % T.vscale() <= T.vscale(),
],
)
def test_simplify_vscale_comparison_with_sve_target(expression):
ana = tvm.arith.Analyzer()
with tvm.target.Target({"kind": "llvm", "mtriple": "aarch64-linux-gnu", "mattr": ["+sve"]}):
assert ana.can_prove(expression)
@pytest.mark.xfail(
reason="arith no longer emits a LOG(WARNING) for vscale proofs on non-VLA targets"
)
def test_simplify_vscale_comparison_without_sve_target(capfd):
ana = tvm.arith.Analyzer()
vs = tvm.tirx.vscale()
with pytest.raises(AssertionError):
with tvm.target.Target({"kind": "llvm", "mtriple": "aarch64-linux-gnu"}):
assert ana.can_prove(vs * 32 < vs * 64)
warning_prefix = (
"Warning: The expression contains scalable values. An attempt to prove by substituting "
"with known values of vscale was not performed. This proof currently only supports "
"VLA targets, but the target was "
)
capture = capfd.readouterr().err
assert warning_prefix in capture
assert '"kind":"llvm"' in capture
assert '"mtriple":"aarch64-linux-gnu"' in capture
def test_regression_simplify_inf_recursion():
ana = tvm.arith.Analyzer()
cond = tirx.Var("cond", "int32")
res = (tvm.tirx.NE(cond, 0).astype("int8") - tvm.tirx.NE(cond, 0).astype("int8")).astype(
"int32"
) == 0
# regression in a previous case
# try compare and int set recursive call can cause infinite loop
ana.rewrite_simplify(res)
def test_bind_allow_override():
ana = tvm.arith.Analyzer()
x = tirx.Var("x", "int64")
ana.bind(x, tvm.ir.Range(0, 10))
ana.bind(x, tvm.ir.Range(0, 5), allow_override=True)
assert ana.can_prove(x < 5)
with pytest.raises(RuntimeError, match="Trying to update var 'x' with a different const bound"):
ana.bind(x, tvm.ir.Range(0, 3))
def test_simplify_floor_mod_with_linear_offset():
"""
Test that the floor_mod is simplified correctly when the offset is linear.
"""
ana = tvm.arith.Analyzer()
past_decoder_sequence_length = tirx.Var("past_decoder_sequence_length", "int64")
expr1 = (past_decoder_sequence_length + 1) * 64
divisor1 = (past_decoder_sequence_length + 1) * 32
assert ana.can_prove_equal(tvm.tirx.floormod(expr1, divisor1), 0)
divisor2 = 32 * (past_decoder_sequence_length + 1)
assert ana.can_prove_equal(tvm.tirx.floormod(expr1, divisor2), 0)
def test_simplify_uint_floormod_const_scale_divisible():
"""uint32 floormod(x * c1, c2) -> 0 when c1 % c2 == 0 (overflow-free)."""
ana = tvm.arith.Analyzer()
q = tirx.Var("q_stage_idx", "uint32")
expr = q * tirx.Cast("uint32", 128)
mod = expr % tirx.const(4, "uint32")
assert ana.can_prove_equal(mod, tirx.const(0, "uint32"))
tvm.ir.assert_structural_equal(ana.rewrite_simplify(mod), tirx.const(0, "uint32"))
def test_simplify_float_division():
# Test for the discussion:
# https://discuss.tvm.apache.org/t/discuss-is-constant-division-to-multiplication-rewrite-in-tvm-necessary/18615
ana = tvm.arith.Analyzer()
x = tirx.Var("x", "float32")
ry = x / 27
# in old version, the division will be rewritten into x * T.float32(1 / 27)
sy = ana.rewrite_simplify(ry)
tvm.ir.assert_structural_equal(ry, sy)
if __name__ == "__main__":
tvm.testing.main()
@@ -0,0 +1,186 @@
# 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 random
import sys
import pytest
import tvm_ffi
import tvm
from tvm import arith, ir, testing, tirx
from tvm.script import tirx as T
def test_solution_consistency():
seed = random.randrange(sys.maxsize)
print(
"\nThis test is intentionally non-deterministic, "
f"if it fails please report it in GitHub issue together with this seed {seed}\n"
)
random.seed(seed)
def _check(num_vars, num_formulas, coef=(-5, 5), bounds=(-20, 20)):
variables = [tvm.tirx.Var("x" + str(i), "int32") for i in range(num_vars)]
relations = []
for i in range(num_formulas):
s1 = sum([v * random.randint(coef[0], coef[1]) for v in variables])
s1 += random.randint(coef[0], coef[1])
s2 = sum([v * random.randint(coef[0], coef[1]) for v in variables])
s2 += random.randint(coef[0], coef[1])
if random.random() < 0.7:
op = tvm.tirx.EQ
else:
# we also make sure it can correctly handle inequalities
op = random.choice([tvm.tirx.LE, tvm.tirx.LT, tvm.tirx.GE, tvm.tirx.GT])
relations.append(op(s1, s2))
vranges = {v: tvm.ir.expr.Range(bounds[0], bounds[1] + 1) for v in variables}
solution = arith.solve_linear_equations(relations, variables, vranges)
testing.check_int_constraints_trans_consistency(solution)
# leaving some variables as parameters should also be ok
for k in [1, 2]:
if len(variables) > k:
solution = arith.solve_linear_equations(relations, variables[:-k], vranges)
param_ranges = {v: vranges[v] for v in variables[-k:]}
testing.check_int_constraints_trans_consistency(solution, param_ranges)
for i in range(2):
_check(num_vars=1, num_formulas=1)
for i in range(2):
_check(num_vars=1, num_formulas=2)
for i in range(2):
_check(num_vars=2, num_formulas=1)
for i in range(2):
_check(num_vars=2, num_formulas=2)
for i in range(2):
_check(num_vars=2, num_formulas=3)
for i in range(3):
_check(num_vars=3, num_formulas=3, coef=(-2, 2))
for i in range(3):
_check(num_vars=3, num_formulas=4, coef=(-2, 2))
for i in range(3):
_check(num_vars=4, num_formulas=3, coef=(-1, 1))
for i in range(3):
_check(num_vars=10, num_formulas=2, coef=(-1, 1), bounds=(0, 4))
for i in range(3):
_check(num_vars=10, num_formulas=3, coef=(0, 1), bounds=(0, 4))
def test_empty_var_to_solve():
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
equations = [
tvm.tirx.EQ(x + y, 20),
tvm.tirx.EQ(x - y, 10),
]
solution = arith.solve_linear_equations(equations)
assert len(solution.src_to_dst) == 0
assert len(solution.dst_to_src) == 0
assert len(solution.src.variables) == 0
assert len(solution.src.ranges) == 0
assert tvm_ffi.structural_equal(solution.src.relations, equations)
assert tvm_ffi.structural_equal(solution.src, solution.dst)
def test_unique_solution():
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
solution = arith.solve_linear_equations(
[
tvm.tirx.EQ(x + y, 20),
tvm.tirx.EQ(x - y, 10),
],
[x, y],
)
assert list(solution.dst.variables) == []
assert tvm_ffi.structural_equal(solution.src_to_dst[x], T.int32(15))
assert tvm_ffi.structural_equal(solution.src_to_dst[y], T.int32(5))
def test_low_rank():
x, y, z = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32"), tvm.tirx.Var("z", "int32")
ranges = {}
solution = arith.solve_linear_equations(
[
tvm.tirx.EQ(x + y + z, 15),
tvm.tirx.EQ(x + y, 10),
],
[x, y, z],
ranges,
)
[n0] = solution.dst.variables
assert tvm_ffi.structural_equal(solution.src_to_dst[x], n0 + 10)
assert tvm_ffi.structural_equal(solution.src_to_dst[y], -n0)
assert tvm_ffi.structural_equal(solution.src_to_dst[z], T.int32(5))
def test_infer_range():
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
ranges = {
x: tvm.ir.Range.from_min_extent(-5, 10),
y: tvm.ir.Range.from_min_extent(0, 10),
}
solution = arith.solve_linear_equations(
[
tvm.tirx.EQ(x + y, 0),
],
[x, y],
ranges,
)
[n0] = solution.dst.variables
assert tvm_ffi.structural_equal(solution.src_to_dst[x], n0)
assert tvm_ffi.structural_equal(solution.src_to_dst[y], -n0)
# inferred from y's range
assert tvm_ffi.structural_equal(solution.dst.ranges[n0].min, T.int32(-9))
assert tvm_ffi.structural_equal(solution.dst.ranges[n0].extent, T.int32(10))
# additional inequality is added into the system for x
[ineq] = solution.dst.relations
assert isinstance(ineq, tvm.tirx.LE)
assert tvm_ffi.structural_equal(ineq.a, T.int32(-5))
assert tvm_ffi.structural_equal(ineq.b, n0)
def test_ill_formed():
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
solution = arith.solve_linear_equations(
[
tvm.tirx.EQ(x + y, 0),
tvm.tirx.EQ(x - y, 0),
tvm.tirx.EQ(x, 5),
],
[x, y],
{},
)
assert list(solution.dst.variables) == []
[rel] = solution.dst.relations
ir.assert_structural_equal(rel, tirx.const(False))
assert len(solution.src_to_dst) == 0
assert len(solution.dst_to_src) == 0
if __name__ == "__main__":
tvm.testing.main()
@@ -0,0 +1,223 @@
# 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 random
import sys
import pytest
import tvm_ffi
import tvm
from tvm import arith, ir, testing, tirx
from tvm.script import tirx as T
@pytest.mark.skip(reason="See https://github.com/apache/tvm/issues/11458")
def test_solution_consistency():
seed = random.randrange(sys.maxsize)
print(
"\nThis test is intentionally non-deterministic, "
f"if it fails please report it in GitHub issue together with this seed {seed}\n"
)
random.seed(seed)
def _check(variables, formulas, coef=(-5, 5), bounds=(-20, 20)):
vs = [tvm.tirx.Var("x" + str(i), "int32") for i in range(variables)]
fs = []
for i in range(formulas):
s1 = sum([v * random.randint(coef[0], coef[1]) for v in vs])
s1 += random.randint(coef[0], coef[1])
s2 = sum([v * random.randint(coef[0], coef[1]) for v in vs])
s2 += random.randint(coef[0], coef[1])
op = random.choice(
[tirx.expr.EQ, tirx.expr.LE, tirx.expr.LT, tirx.expr.GE, tirx.expr.GT]
)
fs.append(op(s1, s2))
vranges = {v: tvm.ir.expr.Range(bounds[0], bounds[1] + 1) for v in vs}
before = tvm.tirx.all(tirx.const(1, "bool"), *fs)
after = arith._ffi_api.SolveInequalitiesAsCondition(vs, vranges, fs)
after = tvm.tirx.all(tirx.const(1, "bool"), *after)
testing.check_bool_expr_is_true(before == after, vranges)
solution = arith.solve_linear_inequalities(fs, vs, vranges, deskew_range=True)
testing.check_int_constraints_trans_consistency(solution)
for i in range(3):
_check(1, 1)
for i in range(3):
_check(1, 2)
for i in range(3):
_check(2, 1)
for i in range(3):
_check(2, 2)
for i in range(3):
_check(2, 3)
# Somewhere here coefficients in the results become too large, leading to overflow,
# so we use smaller initial coefficients
for i in range(5):
_check(3, 3, coef=(-2, 2))
for i in range(5):
_check(3, 4, coef=(-2, 2))
for i in range(5):
_check(4, 3, coef=(-1, 1))
for i in range(5):
_check(10, 2, coef=(-1, 1), bounds=(0, 4))
for i in range(5):
_check(10, 3, coef=(0, 1), bounds=(0, 4))
def test_dual_variable():
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
variables = [x, y]
ranges = {
x: tvm.ir.Range(-100, 100),
y: tvm.ir.Range(0, 10),
}
problem = [
tvm.tirx.LE(x + y, 20),
tvm.tirx.GE(x - y, 10),
]
# solution as conditions
solution = arith._ffi_api.SolveInequalitiesAsCondition(variables, ranges, problem)
assert tvm_ffi.structural_equal(solution[0], x >= (y + 10))
assert tvm_ffi.structural_equal(solution[1], x <= (20 - y))
assert tvm_ffi.structural_equal(solution[2], y >= 0)
assert tvm_ffi.structural_equal(solution[3], y <= 5)
# solve and get the ranges
solution = arith.solve_linear_inequalities(problem, variables, ranges)
# 0 <= y <=5
assert solution.ranges[y].min == 0
assert solution.ranges[y].extent == 6
# y + 10 <= x <= 20 - y
assert tvm_ffi.structural_equal(solution.ranges[x].min, y + 10)
assert solution.ranges[x].extent == 11 # max(10 - 2y)
# deskew the solved ranges to be starting from zero
solution = arith.solve_linear_inequalities(problem, variables, ranges, deskew_range=True)
[x_new, y_new] = solution.dst.variables
[rel] = solution.dst.relations
assert tvm_ffi.structural_equal(rel, (y_new * 2) + x_new <= 10)
assert tvm_ffi.structural_equal(solution.dst.ranges[x_new].min, T.int32(0))
assert tvm_ffi.structural_equal(solution.dst.ranges[x_new].extent, T.int32(11))
assert tvm_ffi.structural_equal(solution.dst.ranges[y_new].min, T.int32(0))
assert tvm_ffi.structural_equal(solution.dst.ranges[y_new].extent, T.int32(6))
assert tvm_ffi.structural_equal(solution.src_to_dst[x], x_new + (y_new + 10))
assert tvm_ffi.structural_equal(solution.src_to_dst[y], y_new)
assert tvm_ffi.structural_equal(solution.dst_to_src[x_new], x - y - 10)
assert tvm_ffi.structural_equal(solution.dst_to_src[y_new], y)
def test_equal():
x, y = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32")
problem = [
tvm.tirx.GE(x + y, 10),
tvm.tirx.GE(x - y, 2),
tvm.tirx.LE(x, 6),
]
solution = arith.solve_linear_inequalities(problem, [x, y])
assert solution.ranges[x].min == 6
assert solution.ranges[x].extent == 1
assert solution.ranges[y].min == 4
assert solution.ranges[y].extent == 1
solution = arith.solve_linear_inequalities(problem, [x, y], deskew_range=True)
assert len(solution.dst.variables) == 0
assert len(solution.dst.ranges) == 0
assert len(solution.dst.relations) == 0
assert solution.src_to_dst[x] == 6
assert solution.src_to_dst[y] == 4
def test_multi_equal():
x, y, z = tvm.tirx.Var("x", "int32"), tvm.tirx.Var("y", "int32"), tvm.tirx.Var("z", "int32")
problem = [
tvm.tirx.LE(x, 6),
tvm.tirx.GE(x, 6),
tvm.tirx.GE(x - z * y, 0),
tvm.tirx.LE(x - z * y, 0),
]
solution = arith.solve_linear_inequalities(problem, [x, y, z])
assert solution.ranges[x].min == 6
assert solution.ranges[x].extent == 1
assert len(solution.relations) == 3
assert tvm_ffi.structural_equal(solution.relations[0], x == z * y)
assert isinstance(solution.relations[1], tvm.tirx.LE)
assert solution.relations[1].b == 0
assert isinstance(solution.relations[2], tvm.tirx.LE)
assert solution.relations[2].b == 0
# (z*y - 6) <= 0 && (6 - z*y) <= 0
ana = tvm.arith.Analyzer()
assert ana.simplify(solution.relations[1].a + solution.relations[2].a) == 0
assert tvm_ffi.structural_equal(
solution.relations[1].a, (z * y - 6)
) or tvm_ffi.structural_equal(solution.relations[2].a, (z * y - 6))
solution = arith.solve_linear_inequalities(problem, [x, y, z], deskew_range=True)
assert solution.src_to_dst[y] == y
assert solution.src_to_dst[z] == z
assert solution.src_to_dst[x] == 6
def test_no_solution():
x = tvm.tirx.Var("x0", "int32")
vranges = {x: tvm.ir.Range.from_min_extent(-20, 41)}
problem = [-x - 4 <= -5 * x + 2, x * 4 + 5 <= x * 5]
solution = arith.solve_linear_inequalities(problem, [x], vranges, deskew_range=True)
assert list(solution.dst.variables) == []
[rel] = solution.dst.relations
ir.assert_structural_equal(rel, tirx.const(False))
assert len(solution.src_to_dst) == 0
assert len(solution.dst_to_src) == 0
solution = arith.solve_linear_inequalities(problem, [x], vranges)
assert len(solution.variables) == 0
assert len(solution.ranges) == 0
[rel] = solution.relations
assert not rel
def test_unbound_var_range():
x = tvm.tirx.Var("x0", "int32")
free_var = tvm.tirx.Var("fv", "int32")
vranges = {
x: tvm.ir.Range.from_min_extent(0, tvm.tirx.Cast("int32", 1 + tvm.tirx.log(free_var)))
}
problem = [x > 3]
solution = arith.solve_linear_inequalities(
problem,
[x],
vranges,
)
assert len(solution.variables) == 1
assert len(solution.ranges) == 0
assert len(solution.relations) == 3
if __name__ == "__main__":
tvm.testing.main()
+756
View File
@@ -0,0 +1,756 @@
# 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 gc
import queue
import threading
import pytest
import tvm
import tvm.testing
from tvm import tirx
from tvm.arith import Analyzer, ProofStrength
# The Z3 prover is only consulted at the kSymbolicBound strength so the common
# default path never pays the prover cost.
SB = ProofStrength.SYMBOLIC_BOUND
def _require_z3(analyzer):
if not analyzer.is_z3_enabled:
pytest.skip("Z3 prover is disabled in this build")
def implies(x, y):
return tirx.Or(tirx.Not(x), y)
# ---------------------------------------------------------------------------
# API availability (works regardless of whether Z3 is built)
# ---------------------------------------------------------------------------
def test_z3_capability_query():
# `is_z3_enabled` is the supported way to detect the build configuration.
# The Z3-specific debug/config methods work only when it is True, and raise
# a clear error otherwise.
analyzer = Analyzer()
assert isinstance(analyzer.is_z3_enabled, bool)
if analyzer.is_z3_enabled:
assert isinstance(analyzer.get_smtlib2(), str)
assert isinstance(analyzer.get_z3_stats(), str)
else:
with pytest.raises(RuntimeError):
analyzer.get_smtlib2()
with pytest.raises(RuntimeError):
analyzer.get_z3_stats()
with pytest.raises(RuntimeError):
analyzer.set_z3_timeout_ms(1000)
with pytest.raises(RuntimeError):
analyzer.set_z3_rlimit(0)
def test_z3_context_lifetime_outlives_worker_thread():
_require_z3(Analyzer())
result_queue = queue.Queue()
def worker():
try:
analyzer = Analyzer()
x = tirx.Var("x", "int32")
analyzer.bind(x, tvm.ir.Range(0, 16))
assert analyzer.can_prove(x >= 0, SB)
result_queue.put(("analyzer", analyzer))
except BaseException as err: # pylint: disable=broad-exception-caught
result_queue.put(("error", err))
thread = threading.Thread(target=worker)
thread.start()
thread.join()
kind, payload = result_queue.get_nowait()
if kind == "error":
raise payload
del payload
gc.collect()
# ---------------------------------------------------------------------------
# Examples the native analyzer cannot prove but Z3 can.
#
# Each case asserts both that the native analyzers (kDefault, Z3 gated off)
# fail and that Z3 (kSymbolicBound) succeeds. This demonstrates the added value
# of the Z3 backend and that it is correctly gated behind kSymbolicBound.
# ---------------------------------------------------------------------------
def test_z3_floor_division_identity_constraint():
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
b = tirx.Var("b", "int32")
c = tirx.Var("c", "int32")
expr = ((b - a) // c) * c + a <= b
with analyzer.constraint_scope(tirx.all(a > 0, b > 0, c > 0)):
assert not analyzer.can_prove(expr)
assert analyzer.can_prove(expr, SB)
def test_z3_floor_division_identity_via_bind_range():
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
b = tirx.Var("b", "int32")
c = tirx.Var("c", "int32")
analyzer.bind(a, tvm.ir.Range(1, 100000))
analyzer.bind(b, tvm.ir.Range(1, 100000))
analyzer.bind(c, tvm.ir.Range(1, 100000))
expr = ((b - a) // c) * c + a <= b
assert analyzer.can_prove(expr, SB)
def test_z3_multiplication_monotonicity():
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
b = tirx.Var("b", "int32")
c = tirx.Var("c", "int32")
d = tirx.Var("d", "int32")
expr = implies(tirx.all(a < b, b < c, a * d < b * d), b * d < c * d)
assert not analyzer.can_prove(expr)
assert analyzer.can_prove(expr, SB)
def test_z3_nested_floor_division_collapse():
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
expr = implies(
tirx.all(a >= 0, a < 128),
a // 128 == (a // 64 * 32 + a % 32 // 16 * 8) // 64,
)
assert not analyzer.can_prove(expr)
assert analyzer.can_prove(expr, SB)
def test_z3_deeply_nested_floor_division_identity():
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
expr = implies(
tirx.all(a >= 0, a < 128),
(
a % 16 * 64
+ a // 64 * 32
+ a % 8 // 4 * 32
+ (a % 32 // 16 + a % 2) % 2 * 8
+ 16
- (a // 64 + a % 8 // 4) // 2 * 64
)
// 512
== (
a % 16 * 64
+ a // 64 * 32
+ a % 8 // 4 * 32
+ (a % 32 // 16 + a % 2) % 2 * 8
- (a // 64 + a % 8 // 4) // 2 * 64
)
// 512,
)
assert analyzer.can_prove(expr, SB)
def test_z3_min_max_sum_identity():
analyzer = Analyzer()
_require_z3(analyzer)
x = tirx.Var("x", "int32")
y = tirx.Var("y", "int32")
expr = tirx.max(x, y) + tirx.min(x, y) == x + y
assert analyzer.can_prove(expr, SB)
def test_z3_select_absolute_value_nonneg():
analyzer = Analyzer()
_require_z3(analyzer)
x = tirx.Var("x", "int32")
expr = tirx.Select(x >= 0, x, -x) >= 0
assert not analyzer.can_prove(expr)
assert analyzer.can_prove(expr, SB)
def test_z3_transitive_inequality():
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
b = tirx.Var("b", "int32")
c = tirx.Var("c", "int32")
expr = implies(tirx.all(a <= b, b <= c), a <= c)
assert analyzer.can_prove(expr, SB)
def test_z3_square_expansion_nonneg():
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
b = tirx.Var("b", "int32")
expr = (a + b) * (a + b) >= a * a + b * b
with analyzer.constraint_scope(tirx.all(a >= 0, b >= 0)):
assert not analyzer.can_prove(expr)
assert analyzer.can_prove(expr, SB)
def test_z3_square_monotonicity():
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
b = tirx.Var("b", "int32")
expr = implies(tirx.all(0 <= a, a <= b), a * a <= b * b)
assert not analyzer.can_prove(expr)
assert analyzer.can_prove(expr, SB)
def test_z3_strict_multiplication():
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
b = tirx.Var("b", "int32")
d = tirx.Var("d", "int32")
expr = implies(tirx.all(a < b, d > 0), a * d < b * d)
assert not analyzer.can_prove(expr)
assert analyzer.can_prove(expr, SB)
def test_z3_floor_division_monotonicity():
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
b = tirx.Var("b", "int32")
c = tirx.Var("c", "int32")
expr = implies(tirx.all(a <= b, c > 0), tirx.floordiv(a, c) <= tirx.floordiv(b, c))
assert not analyzer.can_prove(expr)
analyzer.set_z3_rlimit(0)
assert analyzer.can_prove(expr, SB)
def test_z3_floor_division_lower_bound():
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
b = tirx.Var("b", "int32")
expr = implies(b > 0, tirx.floordiv(a, b) * b <= a)
assert not analyzer.can_prove(expr)
assert analyzer.can_prove(expr, SB)
def test_z3_floor_modulo_range():
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
b = tirx.Var("b", "int32")
expr = implies(b > 0, tirx.all(0 <= tirx.floormod(a, b), tirx.floormod(a, b) < b))
assert not analyzer.can_prove(expr)
assert analyzer.can_prove(expr, SB)
def test_z3_flattened_index_bound():
# Classic index-flattening bound used throughout TVM: for a row index i in
# [0, m) and a column index j in [0, n), the flattened index i * n + j stays
# within [0, m * n).
analyzer = Analyzer()
_require_z3(analyzer)
i = tirx.Var("i", "int32")
j = tirx.Var("j", "int32")
m = tirx.Var("m", "int32")
n = tirx.Var("n", "int32")
expr = tirx.all(0 <= i * n + j, i * n + j < m * n)
with analyzer.constraint_scope(tirx.all(0 <= i, i < m, 0 <= j, j < n, m > 0, n > 0)):
assert not analyzer.can_prove(expr)
analyzer.set_z3_rlimit(0)
assert analyzer.can_prove(expr, SB)
def test_z3_modular_combination():
# Native modular_set tracks single-variable moduli, but combining two
# independent modular facts to reason about their sum is left to Z3.
analyzer = Analyzer()
_require_z3(analyzer)
x = tirx.Var("x", "int32")
y = tirx.Var("y", "int32")
expr = tirx.floormod(x + y, 2) == 0
with analyzer.constraint_scope(tirx.all(tirx.floormod(x, 6) == 0, tirx.floormod(y, 6) == 0)):
assert not analyzer.can_prove(expr)
assert analyzer.can_prove(expr, SB)
def test_z3_square_non_negative():
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
assert not analyzer.can_prove(a * a >= 0)
assert analyzer.can_prove(a * a >= 0, SB)
def test_z3_min_max_average_bounds():
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
b = tirx.Var("b", "int32")
assert not analyzer.can_prove(tirx.max(a, b) * 2 >= a + b)
assert analyzer.can_prove(tirx.max(a, b) * 2 >= a + b, SB)
assert analyzer.can_prove(tirx.min(a, b) * 2 <= a + b, SB)
def test_z3_symbolic_bind_range_with_constraint():
# Combine a symbolic range binding (x in [0, n)) with a constraint on the
# extent to derive a concrete bound on x.
analyzer = Analyzer()
_require_z3(analyzer)
x = tirx.Var("x", "int32")
n = tirx.Var("n", "int32")
analyzer.bind(x, tvm.ir.Range(0, n))
with analyzer.constraint_scope(n <= 8):
assert not analyzer.can_prove(x < 8)
assert analyzer.can_prove(x < 8, SB)
def test_z3_equality_congruence():
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
b = tirx.Var("b", "int32")
expr = implies(a == b, a * a == b * b)
assert not analyzer.can_prove(expr)
assert analyzer.can_prove(expr, SB)
def test_z3_integer_strict_transitivity():
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
b = tirx.Var("b", "int32")
c = tirx.Var("c", "int32")
# Over the integers, a < b and b < c implies a + 1 < c.
expr = implies(tirx.all(a < b, b < c), a + 1 < c)
assert not analyzer.can_prove(expr)
assert analyzer.can_prove(expr, SB)
def test_z3_if_then_else_absolute_value():
analyzer = Analyzer()
_require_z3(analyzer)
x = tirx.Var("x", "int32")
expr = tirx.if_then_else(x >= 0, x, -x) >= 0
assert not analyzer.can_prove(expr)
assert analyzer.can_prove(expr, SB)
def test_z3_unsigned_non_negative():
analyzer = Analyzer()
_require_z3(analyzer)
u = tirx.Var("u", "uint32")
assert not analyzer.can_prove(u >= 0)
assert analyzer.can_prove(u >= 0, SB)
def test_z3_unsigned64_non_negative():
# Exercises the special-cased uint64 range handling (UINT64_MAX bound).
analyzer = Analyzer()
_require_z3(analyzer)
u = tirx.Var("u", "uint64")
assert not analyzer.can_prove(u >= 0)
assert analyzer.can_prove(u >= 0, SB)
def test_z3_int64_square_expansion():
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int64")
b = tirx.Var("b", "int64")
expr = (a + b) * (a + b) >= a * a + b * b
with analyzer.constraint_scope(tirx.all(a >= 0, b >= 0)):
assert not analyzer.can_prove(expr)
assert analyzer.can_prove(expr, SB)
def test_z3_boolean_variable_reasoning():
analyzer = Analyzer()
_require_z3(analyzer)
p = tirx.Var("p", "bool")
q = tirx.Var("q", "bool")
expr = implies(tirx.And(p, q), tirx.Or(p, q))
assert not analyzer.can_prove(expr)
assert analyzer.can_prove(expr, SB)
def test_z3_not_equal_from_strict_less():
analyzer = Analyzer()
_require_z3(analyzer)
x = tirx.Var("x", "int32")
y = tirx.Var("y", "int32")
expr = implies(x < y, tirx.NE(x, y))
assert not analyzer.can_prove(expr)
assert analyzer.can_prove(expr, SB)
def test_z3_let_expression():
analyzer = Analyzer()
_require_z3(analyzer)
y = tirx.Var("y", "int32")
t = tirx.Var("t", "int32")
let = tirx.Let(t, y * 2, t)
assert not analyzer.can_prove(let == y * 2)
assert analyzer.can_prove(let == y * 2, SB)
def test_z3_cast_preserves_bounds():
analyzer = Analyzer()
_require_z3(analyzer)
s = tirx.Var("s", "int16")
widened = tirx.Cast("int32", s)
assert analyzer.can_prove(widened <= 32767, SB)
assert analyzer.can_prove(widened >= -32768, SB)
def test_z3_bitwise_and_mask_bound():
analyzer = Analyzer()
_require_z3(analyzer)
x = tirx.Var("x", "int32")
analyzer.bind(x, tvm.ir.Range(0, 256))
assert analyzer.can_prove(tirx.bitwise_and(x, tirx.IntImm("int32", 7)) < 8, SB)
def test_z3_bitwise_and_le_operand():
analyzer = Analyzer()
_require_z3(analyzer)
x = tirx.Var("x", "int32")
y = tirx.Var("y", "int32")
analyzer.bind(x, tvm.ir.Range(0, 256))
analyzer.bind(y, tvm.ir.Range(0, 256))
# Bit-vector reasoning over two variables exceeds the default deterministic
# rlimit; lift it (0 == unlimited, still deterministic) for this proof.
analyzer.set_z3_rlimit(0)
assert analyzer.can_prove(tirx.bitwise_and(x, y) <= x, SB)
def test_z3_bitwise_or_ge_operand():
analyzer = Analyzer()
_require_z3(analyzer)
x = tirx.Var("x", "int32")
y = tirx.Var("y", "int32")
analyzer.bind(x, tvm.ir.Range(0, 256))
analyzer.bind(y, tvm.ir.Range(0, 256))
analyzer.set_z3_rlimit(0)
assert analyzer.can_prove(tirx.bitwise_or(x, y) >= x, SB)
def test_z3_bitwise_xor_bound():
analyzer = Analyzer()
_require_z3(analyzer)
x = tirx.Var("x", "int32")
y = tirx.Var("y", "int32")
analyzer.bind(x, tvm.ir.Range(0, 256))
analyzer.bind(y, tvm.ir.Range(0, 256))
analyzer.set_z3_rlimit(0)
assert analyzer.can_prove(tirx.bitwise_xor(x, y) < 256, SB)
def test_z3_bitwise_not_identity():
analyzer = Analyzer()
_require_z3(analyzer)
x = tirx.Var("x", "int32")
analyzer.bind(x, tvm.ir.Range(0, 256))
analyzer.set_z3_rlimit(0)
# Two's complement: ~x == -x - 1.
assert analyzer.can_prove(tirx.bitwise_not(x) == -x - 1, SB)
def test_z3_shift_right_halves():
analyzer = Analyzer()
_require_z3(analyzer)
x = tirx.Var("x", "int32")
analyzer.bind(x, tvm.ir.Range(0, 256))
analyzer.set_z3_rlimit(0)
# For non-negative x, (x >> 1) * 2 <= x.
assert analyzer.can_prove(tirx.shift_right(x, tirx.IntImm("int32", 1)) * 2 <= x, SB)
def test_z3_shift_left_lower_bound():
analyzer = Analyzer()
_require_z3(analyzer)
x = tirx.Var("x", "int32")
n = tirx.Var("n", "int32")
# Keep operands small so the 32-bit left shift cannot overflow; then
# x << n == x * 2 ** n >= x for x >= 1.
analyzer.bind(x, tvm.ir.Range(1, 16))
analyzer.bind(n, tvm.ir.Range(0, 4))
# Bit-vector shift reasoning exceeds the default deterministic rlimit.
analyzer.set_z3_rlimit(0)
assert analyzer.can_prove(tirx.shift_left(x, n) >= x, SB)
# ---------------------------------------------------------------------------
# Soundness / negative tests (Z3 must NOT prove false predicates)
# ---------------------------------------------------------------------------
def test_z3_negative_unprovable_inequality():
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
b = tirx.Var("b", "int32")
# a < b does not hold for arbitrary a, b.
assert not analyzer.can_prove(a < b, SB)
# a * a > a is false (e.g. a == 0).
assert not analyzer.can_prove(a * a > a, SB)
def test_z3_truncmod_can_be_negative():
# Regression test for truncated div/mod semantics: TVM Div/Mod round toward
# zero, so truncmod(a, 4) can be negative. A solver that modeled them as
# Euclidean would unsoundly "prove" truncmod(a, 4) >= 0.
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
assert not analyzer.can_prove(tirx.truncmod(a, 4) >= 0, SB)
def test_z3_truncdiv_truncmod_identity():
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
b = tirx.Var("b", "int32")
expr = tirx.truncdiv(a, b) * b + tirx.truncmod(a, b) == a
with analyzer.constraint_scope(b != 0):
assert analyzer.can_prove(expr, SB)
def test_z3_floormod_nested_identities():
# Ported from TileLang's test_divmod. Here `%` is floormod: nested floormod
# by opposite-sign divisors collapses to the single-divisor result, while
# the mixed case does not.
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
assert not analyzer.can_prove(a % 2 % -2 - a % 2 == 0, SB)
assert analyzer.can_prove(a % -2 % 2 - a % 2 == 0, SB)
def test_z3_floormod_nonnegative():
# In contrast to truncmod, floormod with a positive divisor is always in
# [0, divisor), which Z3 should be able to prove.
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
assert analyzer.can_prove(tirx.floormod(a, 4) >= 0, SB)
assert analyzer.can_prove(tirx.floormod(a, 4) < 4, SB)
def test_z3_shift_does_not_poison_solver():
# Regression test: evaluating a shift expression must not add permanent
# assertions (such as `b >= 0` / `b < 64`) to the shared solver. Otherwise
# an unrelated, unbounded `b` would be wrongly provable to be < 100.
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
b = tirx.Var("b", "int32")
# Touch a shift expression so the prover visits the shift amount `b`.
analyzer.can_prove(tirx.shift_left(a, b) >= 0, SB)
# `b` is otherwise unconstrained, so this must remain unprovable.
assert not analyzer.can_prove(b < 100, SB)
assert not analyzer.can_prove(b >= 0, SB)
def test_z3_constraint_scope_is_popped():
# Constraints entered through a scope must be removed once the scope exits,
# i.e. EnterConstraint's solver.push()/pop() must be balanced.
analyzer = Analyzer()
_require_z3(analyzer)
x = tirx.Var("x", "int32")
with analyzer.constraint_scope(x > 5):
assert analyzer.can_prove(x > 0, SB)
# The constraint is gone; x is unconstrained again.
assert not analyzer.can_prove(x > 0, SB)
def test_z3_opaque_call_is_safe():
# An opaque/unsupported sub-expression is modeled as a fresh free variable.
# It must neither crash nor be provable on its own, yet still be usable as a
# constraint.
analyzer = Analyzer()
_require_z3(analyzer)
x = tirx.Var("x", "int32")
call = tirx.call_extern("int32", "foo", x)
assert not analyzer.can_prove(call > 0, SB)
with analyzer.constraint_scope(call > 0):
assert analyzer.can_prove(call > 0, SB)
assert not analyzer.can_prove(call > 0, SB)
def test_z3_shift_overflow_is_not_proven():
# Z3 models fixed-width shifts via bit-vectors, so it correctly refuses to
# prove `x << n >= x` for an unbounded `x` (a large `x` overflows int32 and
# wraps to a negative value). This guards against unsound shift modeling.
analyzer = Analyzer()
_require_z3(analyzer)
x = tirx.Var("x", "int32")
n = tirx.Var("n", "int32")
analyzer.set_z3_rlimit(0)
expr = implies(tirx.all(x >= 1, n >= 0, n < 8), tirx.shift_left(x, n) >= x)
assert not analyzer.can_prove(expr, SB)
def test_z3_analyzers_are_isolated():
# Analyzers share a thread-local Z3 context but own separate solvers, so
# constraints and bindings in one must never leak into another.
analyzer_a = Analyzer()
analyzer_b = Analyzer()
_require_z3(analyzer_a)
x = tirx.Var("x", "int32")
with analyzer_a.constraint_scope(x > 100):
assert analyzer_a.can_prove(x > 50, SB)
assert not analyzer_b.can_prove(x > 50, SB)
analyzer_c = Analyzer()
analyzer_d = Analyzer()
analyzer_c.bind(x, tvm.ir.Range(0, 10))
assert analyzer_c.can_prove(x < 10, SB)
assert not analyzer_d.can_prove(x < 10, SB)
def test_z3_repeated_can_prove_is_consistent():
# Repeated queries must be stateless: a CanProve call must not pollute the
# solver and change the result of a subsequent call.
analyzer = Analyzer()
_require_z3(analyzer)
x = tirx.Var("x", "int32")
assert analyzer.can_prove(x > 0, SB) == analyzer.can_prove(x > 0, SB)
analyzer.bind(x, tvm.ir.Range(5, 10))
assert analyzer.can_prove(x >= 5, SB)
assert analyzer.can_prove(x >= 5, SB)
def test_z3_is_gated_behind_symbolic_bound():
# The Z3 fallback must not run at the default strength.
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
b = tirx.Var("b", "int32")
c = tirx.Var("c", "int32")
expr = ((b - a) // c) * c + a <= b
with analyzer.constraint_scope(tirx.all(a > 0, b > 0, c > 0)):
assert not analyzer.can_prove(expr, ProofStrength.DEFAULT)
assert analyzer.can_prove(expr, SB)
# ---------------------------------------------------------------------------
# SMT-LIB2 export
# ---------------------------------------------------------------------------
def test_z3_smtlib2_roundtrip():
z3 = pytest.importorskip("z3")
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
b = tirx.Var("b", "int32")
c = tirx.Var("c", "int32")
expr = ((b - a) // c) * c + a <= b
solver = z3.Solver()
with analyzer.constraint_scope(tirx.all(a > 0, b > 0, c > 0)):
solver.from_string(analyzer.get_smtlib2(expr))
assert solver.check() == z3.unsat
def test_z3_smtlib2_roundtrip_with_timeout():
z3 = pytest.importorskip("z3")
analyzer = Analyzer()
_require_z3(analyzer)
a = tirx.Var("a", "int32")
b = tirx.Var("b", "int32")
c = tirx.Var("c", "int32")
analyzer.set_z3_timeout_ms(1000)
expr = implies(tirx.all(a > 0, b > 0, c > 0), ((b - a) // c) * c + a <= b)
solver = z3.Solver()
solver.from_string(analyzer.get_smtlib2(expr))
assert solver.check() == z3.unsat
if __name__ == "__main__":
tvm.testing.main()