blob: 385a81045517632760597daf85db4351932f8794 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ruff: noqa: E741
import numpy as np
import pytest
import tvm
from tvm import relax, tirx
from tvm.ir import IRModule
from tvm.relax.base_py_module import BasePyModule
from tvm.script import ir as I
from tvm.script import relax as R
from tvm.script import tirx as T
def _make_module():
return IRModule({})
def test_infer_concrete_shape_from_numpy_input():
mod = _make_module()
bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")
n = tirx.Var("n", "int64")
m = tirx.Var("m", "int64")
sym_shape = [n, m]
x = np.zeros((3, 4), dtype="float32")
inferred = bpm._infer_concrete_shape_from_args(sym_shape, [x])
assert inferred == [3, 4]
def test_infer_concrete_shape_all_concrete_dims():
mod = _make_module()
bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")
shape = [tirx.IntImm("int32", 5), 6]
inferred = bpm._infer_concrete_shape_from_args(shape, in_args=[])
assert inferred == [5, 6]
def test_infer_concrete_shape_error_when_uninferrable():
mod = _make_module()
bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")
k = tirx.Var("k", "int64")
with pytest.raises(ValueError):
bpm._infer_concrete_shape_from_args([k, 8], in_args=[])
@I.ir_module
class AddModuleSymbolic(BasePyModule):
@T.prim_func
def add_tir(var_x: T.handle, var_y: T.handle, var_out: T.handle):
T.func_attr({"global_symbol": "add_tir"})
n = T.int64()
x = T.match_buffer(var_x, (n,), dtype="float32")
y = T.match_buffer(var_y, (n,), dtype="float32")
out = T.match_buffer(var_out, (n,), dtype="float32")
for i in T.serial(n):
out[i] = x[i] + y[i]
@R.function
def main_relax(x: R.Tensor(("n",), "float32"), y: R.Tensor(("n",), "float32")) -> R.Tensor(
("n",), "float32"
):
return R.add(x, y)
def test_base_py_module_relax_symbolic_end_to_end():
bpm = AddModuleSymbolic(device=tvm.cpu(0), target="llvm")
a = np.random.randn(5).astype("float32")
b = np.random.randn(5).astype("float32")
out = bpm.main_relax(a, b)
assert isinstance(out, np.ndarray) or hasattr(out, "numpy")
out_np = out if isinstance(out, np.ndarray) else out.numpy()
tvm.testing.assert_allclose(out_np, a + b, rtol=1e-6, atol=1e-6)
a7 = np.random.randn(7).astype("float32")
b7 = np.random.randn(7).astype("float32")
out2 = bpm.main_relax(a7, b7)
out2_np = out2 if isinstance(out2, np.ndarray) else out2.numpy()
tvm.testing.assert_allclose(out2_np, a7 + b7, rtol=1e-6, atol=1e-6)
def test_base_py_module_tir_symbolic_end_to_end():
bpm = AddModuleSymbolic(device=tvm.cpu(0), target="llvm")
a = np.random.randn(5).astype("float32")
b = np.random.randn(5).astype("float32")
n = tirx.Var("n", "int64")
out_sinfo = relax.TensorStructInfo((n,), "float32")
out = bpm.call_tir("add_tir", [a, b], out_sinfo)
out_np = out if isinstance(out, np.ndarray) else out.numpy()
tvm.testing.assert_allclose(out_np, a + b, rtol=1e-6, atol=1e-6)
def test_infer_concrete_shape_multiple_symbolic_dims():
"""Test shape inference with multiple symbolic dimensions."""
mod = _make_module()
bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")
n = tirx.Var("n", "int64")
m = tirx.Var("m", "int64")
k = tirx.Var("k", "int64")
sym_shape = [n, m, k]
x = np.zeros((2, 3, 4), dtype="float32")
inferred = bpm._infer_concrete_shape_from_args(sym_shape, [x])
assert inferred == [2, 3, 4]
def test_infer_concrete_shape_mixed_concrete_symbolic():
"""Test shape inference with mixed concrete and symbolic dimensions."""
mod = _make_module()
bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")
n = tirx.Var("n", "int64")
sym_shape = [n, 5, 10] # First dim is symbolic, others are concrete
x = np.zeros((3, 5, 10), dtype="float32")
inferred = bpm._infer_concrete_shape_from_args(sym_shape, [x])
assert inferred == [3, 5, 10]
def test_infer_concrete_shape_from_tvm_tensors():
"""Test shape inference from TVM tensors."""
try:
# Try to create TVM tensor using new API
x_np = np.zeros((3, 4), dtype="float32")
x_tvm = tvm.runtime.tensor(x_np)
mod = _make_module()
bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")
n = tirx.Var("n", "int64")
m = tirx.Var("m", "int64")
sym_shape = [n, m]
inferred = bpm._infer_concrete_shape_from_args(sym_shape, [x_tvm])
assert inferred == [3, 4]
except AttributeError:
# Skip if tvm.runtime.tensor is not available
pytest.skip("tvm.runtime.tensor not available")
def test_infer_concrete_shape_multiple_inputs():
"""Test shape inference when multiple inputs are available."""
mod = _make_module()
bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")
n = tirx.Var("n", "int64")
m = tirx.Var("m", "int64")
sym_shape = [n, m]
# Multiple inputs with different shapes - should use first matching one
x1 = np.zeros((2, 3), dtype="float32")
x2 = np.zeros((4, 5), dtype="float32")
inferred = bpm._infer_concrete_shape_from_args(sym_shape, [x1, x2])
assert inferred == [2, 3] # Should use first input
def test_infer_concrete_shape_wrong_ndim():
"""Test shape inference when input has wrong number of dimensions."""
mod = _make_module()
bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")
n = tirx.Var("n", "int64")
m = tirx.Var("m", "int64")
sym_shape = [n, m] # 2D
x = np.zeros((3,), dtype="float32") # 1D - wrong ndim
with pytest.raises(ValueError, match="Cannot infer concrete output shape"):
bpm._infer_concrete_shape_from_args(sym_shape, [x])
@I.ir_module
class MatrixModuleSymbolic(BasePyModule):
@T.prim_func
def matmul_tir(var_a: T.handle, var_b: T.handle, var_c: T.handle):
T.func_attr({"global_symbol": "matmul_tir"})
m = T.int64()
n = T.int64()
k = T.int64()
a = T.match_buffer(var_a, (m, k), dtype="float32")
b = T.match_buffer(var_b, (k, n), dtype="float32")
c = T.match_buffer(var_c, (m, n), dtype="float32")
for i in T.serial(m):
for j in T.serial(n):
c[i, j] = 0.0
for l in T.serial(k):
c[i, j] = c[i, j] + a[i, l] * b[l, j]
@R.function
def matmul_relax(
a: R.Tensor(("m", "k"), "float32"), b: R.Tensor(("k", "n"), "float32")
) -> R.Tensor(("m", "n"), "float32"):
return R.matmul(a, b)
def test_base_py_module_multiple_symbolic_dims():
"""Test BasePyModule with multiple symbolic dimensions."""
bpm = MatrixModuleSymbolic(device=tvm.cpu(0), target="llvm")
# Test Relax function with multiple symbolic dims
a = np.random.randn(2, 3).astype("float32")
b = np.random.randn(3, 4).astype("float32")
out = bpm.matmul_relax(a, b)
out_np = out if isinstance(out, np.ndarray) else out.numpy()
expected = np.matmul(a, b)
tvm.testing.assert_allclose(out_np, expected, rtol=1e-6, atol=1e-6)
# Test TIR function with multiple symbolic dims
# Use concrete shapes for TIR function to avoid constraint issues
out_sinfo = relax.TensorStructInfo((2, 4), "float32")
out_tir = bpm.call_tir("matmul_tir", [a, b], out_sinfo)
out_tir_np = out_tir if isinstance(out_tir, np.ndarray) else out_tir.numpy()
tvm.testing.assert_allclose(out_tir_np, expected, rtol=1e-6, atol=1e-6)
def test_base_py_module_call_dps_packed_symbolic():
"""Test call_dps_packed with symbolic shapes."""
try:
# Register a simple test function
@tvm.register_global_func("test_add_packed")
def test_add_packed(a, b, out):
"""Add two tensors element-wise."""
a_np = a.numpy()
b_np = b.numpy()
result = a_np + b_np
out[:] = result
mod = _make_module()
bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")
a = np.random.randn(5).astype("float32")
b = np.random.randn(5).astype("float32")
n = tirx.Var("n", "int64")
out_sinfo = relax.TensorStructInfo((n,), "float32")
out = bpm.call_dps_packed("test_add_packed", [a, b], out_sinfo)
out_np = out if isinstance(out, np.ndarray) else out.numpy()
tvm.testing.assert_allclose(out_np, a + b, rtol=1e-6, atol=1e-6)
except AttributeError as e:
pytest.skip(f"call_dps_packed test requires register_global_func: {e}")
def test_base_py_module_call_dps_packed_multiple_args():
"""Test call_dps_packed with multiple arguments and symbolic shapes."""
try:
# Register a function that takes multiple arguments
@tvm.register_global_func("test_matmul_packed")
def test_matmul_packed(a, b, out):
"""Matrix multiplication."""
a_np = a.numpy()
b_np = b.numpy()
result = np.matmul(a_np, b_np)
out[:] = result
mod = _make_module()
bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")
a = np.random.randn(2, 3).astype("float32")
b = np.random.randn(3, 4).astype("float32")
out_sinfo = relax.TensorStructInfo((2, 4), "float32")
out = bpm.call_dps_packed("test_matmul_packed", [a, b], out_sinfo)
out_np = out if isinstance(out, np.ndarray) else out.numpy()
expected = np.matmul(a, b)
tvm.testing.assert_allclose(out_np, expected, rtol=1e-6, atol=1e-6)
except AttributeError as e:
pytest.skip(f"call_dps_packed test requires register_global_func: {e}")
def test_base_py_module_call_dps_packed_scalar_args():
"""Test call_dps_packed with scalar arguments and symbolic shapes."""
try:
# Register a function that takes scalar arguments
@tvm.register_global_func("test_add_scalar_packed")
def test_add_scalar_packed(x, scalar, out):
"""Add scalar to tensor."""
x_np = x.numpy()
if hasattr(scalar, "numpy"):
scalar_val = scalar.numpy()
else:
scalar_val = scalar
result = x_np + scalar_val
out[:] = result
mod = _make_module()
bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")
x = np.random.randn(4).astype("float32")
scalar = 2.5
n = tirx.Var("n", "int64")
out_sinfo = relax.TensorStructInfo((n,), "float32")
out = bpm.call_dps_packed("test_add_scalar_packed", [x, scalar], out_sinfo)
out_np = out if isinstance(out, np.ndarray) else out.numpy()
expected = x + scalar
tvm.testing.assert_allclose(out_np, expected, rtol=1e-6, atol=1e-6)
except AttributeError as e:
pytest.skip(f"call_dps_packed test requires register_global_func: {e}")
def test_infer_concrete_shape_from_pytorch_tensors():
"""Test shape inference from PyTorch tensors (if available)."""
try:
import torch
except ImportError:
pytest.skip("PyTorch not available")
mod = _make_module()
bpm = BasePyModule(mod, device=tvm.cpu(0), target="llvm")
n = tirx.Var("n", "int64")
m = tirx.Var("m", "int64")
sym_shape = [n, m]
x_torch = torch.zeros((3, 4), dtype=torch.float32)
inferred = bpm._infer_concrete_shape_from_args(sym_shape, [x_torch])
assert inferred == [3, 4]
def test_base_py_module_relax_with_pytorch_tensors():
"""Test Relax functions with PyTorch tensors and symbolic shapes."""
try:
import torch
except ImportError:
pytest.skip("PyTorch not available")
bpm = AddModuleSymbolic(device=tvm.cpu(0), target="llvm")
a_torch = torch.randn(5, dtype=torch.float32)
b_torch = torch.randn(5, dtype=torch.float32)
out = bpm.main_relax(a_torch, b_torch)
out_np = out if isinstance(out, np.ndarray) else out.numpy()
expected = a_torch.numpy() + b_torch.numpy()
tvm.testing.assert_allclose(out_np, expected, rtol=1e-6, atol=1e-6)
if __name__ == "__main__":
tvm.testing.main()