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

253 lines
8.6 KiB
Python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# pylint: disable=missing-docstring
# ruff: noqa: F401
import subprocess
import tempfile
from pathlib import Path
import numpy as np
import tvm_ffi
import tvm
import tvm.libinfo
import tvm.testing
from tvm import relax
from tvm.relax.frontend import nn
from tvm.relax.frontend.nn import spec
from tvm.relax.transform import AttachExternModules
def _infer_scalar_add(x, y): # pylint: disable=invalid-name
assert isinstance(x, nn.Tensor)
assert isinstance(y, nn.Tensor)
assert x.ndim == 0 and x.dtype == "float32"
assert y.ndim == 0 and y.dtype == "float32"
return nn.Tensor.placeholder(shape=(), dtype="float32")
def _infer_test_sym(a, b): # pylint: disable=invalid-name
def _var_equal(a, b): # pylint: disable=invalid-name
return tvm_ffi.structural_equal(a, b, map_free_vars=True)
assert isinstance(a, nn.Tensor)
assert isinstance(b, nn.Tensor)
assert a.ndim == 3 and a.dtype == "float32" # [x, y, 1]
assert b.ndim == 3 and b.dtype == "float32" # [y, z, 5]
x, y, z = a.shape[0], b.shape[0], b.shape[1] # pylint: disable=invalid-name
assert _var_equal(a.shape[0], x)
assert _var_equal(a.shape[1], y)
assert a.shape[2] == 1
assert _var_equal(b.shape[0], y)
assert _var_equal(b.shape[1], z)
assert b.shape[2] == 5
return nn.Tensor.placeholder(shape=(x, y, z, 9), dtype="float32")
def _test_scalar_add(func):
# pylint: disable=invalid-name
x = tvm.runtime.tensor(np.array(1.0).astype("float32"))
y = tvm.runtime.tensor(np.array(3.0).astype("float32"))
z = func(x, y).numpy()
# pylint: enable=invalid-name
assert z.ndim == 0
assert z.dtype == "float32"
assert float(z) == 4.0
def _test_infer_sym(func, x, y, z): # pylint: disable=invalid-name
# pylint: disable=invalid-name
a = tvm.runtime.tensor(np.random.uniform(size=(x, y, 1)).astype("float32"))
b = tvm.runtime.tensor(np.random.uniform(size=(y, z, 5)).astype("float32"))
c = func(a, b).numpy()
# pylint: enable=invalid-name
assert c.shape == (x, y, z, 9)
def _check_ir_equality(mod):
# pylint: disable=import-outside-toplevel
from tvm.script import ir as I
from tvm.script import relax as R
from tvm.script import tirx as T
# pylint: enable=import-outside-toplevel
@I.ir_module
class ExpectedModule:
@R.function
def scalar_add(
a: R.Tensor((), dtype="float32"), b: R.Tensor((), dtype="float32")
) -> R.Tensor((), dtype="float32"):
R.func_attr({"num_input": 2})
with R.dataflow():
ext_scalar_add = R.call_dps_packed(
"ext_scalar_add", (a, b), out_ty=R.Tensor((), dtype="float32")
)
gv: R.Tensor((), dtype="float32") = ext_scalar_add
R.output(gv)
return gv
@R.function
def test_sym(
a: R.Tensor(("x", "y", 1), dtype="float32"), b: R.Tensor(("y", "z", 5), dtype="float32")
) -> R.Tensor(("x", "y", "z", 9), dtype="float32"):
x = T.int64()
y = T.int64()
z = T.int64()
R.func_attr({"num_input": 2})
with R.dataflow():
ext_test_sym = R.call_dps_packed(
"ext_test_sym", (a, b), out_ty=R.Tensor((x, y, z, 9), dtype="float32")
)
gv1: R.Tensor((x, y, z, 9), dtype="float32") = ext_test_sym
R.output(gv1)
return gv1
tvm.ir.assert_structural_equal(ExpectedModule, mod)
def _compile_cc(src: Path, dst: Path):
cmd = ["g++", str(src)]
default_include_paths = [
tvm.libinfo.find_include_path(),
tvm_ffi.libinfo.find_include_path(),
tvm_ffi.libinfo.find_dlpack_include_path(),
]
for include_path in default_include_paths:
cmd += ["-I", include_path]
cmd += [
"-c",
"-std=c++17",
"-fPIC",
"-o",
str(dst),
]
with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as proc:
(out, _) = proc.communicate()
if proc.returncode != 0:
msg = "Compilation error:\n"
msg += out.decode("utf-8", errors="replace")
msg += "\nCommand line: " + " ".join(cmd)
raise RuntimeError(msg)
def test_extern_object():
with tempfile.TemporaryDirectory() as temp_dir_str:
path = Path(temp_dir_str) / "main.o"
_compile_cc(
src=Path(__file__).parent / "frontend_nn_extern_module.cc",
dst=path,
)
class TestModule(nn.Module):
def __init__(self):
self.ext_mod = None
def _get_ext_mod(self):
if self.ext_mod is None:
self.ext_mod = nn.ObjectModule(
{
"ext_scalar_add": _infer_scalar_add,
"ext_test_sym": _infer_test_sym,
},
path,
)
nn.add_extern(self.ext_mod)
return self.ext_mod
def scalar_add(self, a: nn.Tensor, b: nn.Tensor): # pylint: disable=invalid-name
return self._get_ext_mod()["ext_scalar_add"](a, b)
def test_sym(self, a: nn.Tensor, b: nn.Tensor): # pylint: disable=invalid-name
return self._get_ext_mod()["ext_test_sym"](a, b)
mod, _, ext_mods = TestModule().export_tvm(
spec={
"scalar_add": {
"a": spec.Tensor((), "float32"),
"b": spec.Tensor((), "float32"),
},
"test_sym": {
"a": spec.Tensor(("x", "y", 1), "float32"),
"b": spec.Tensor(("y", "z", 5), "float32"),
},
},
allow_extern=True,
)
_check_ir_equality(mod)
mod = AttachExternModules(ext_mods)(mod) # pylint: disable=not-callable
compiled = tvm.runtime.vm.VirtualMachine(
tvm.compile(mod, target="llvm"),
device=tvm.cpu(),
)
_test_scalar_add(compiled["scalar_add"])
_test_infer_sym(compiled["test_sym"], x=3, y=4, z=2)
def test_extern_source():
source = Path(__file__).parent / "frontend_nn_extern_module.cc"
class TestModule(nn.Module):
def __init__(self):
self.ext_mod = None
def _get_ext_mod(self):
if self.ext_mod is None:
self.ext_mod = nn.SourceModule(
{
"ext_scalar_add": _infer_scalar_add,
"ext_test_sym": _infer_test_sym,
},
source_code=source,
source_format="cpp",
)
nn.add_extern(self.ext_mod)
return self.ext_mod
def scalar_add(self, a: nn.Tensor, b: nn.Tensor): # pylint: disable=invalid-name
return self._get_ext_mod()["ext_scalar_add"](a, b)
def test_sym(self, a: nn.Tensor, b: nn.Tensor): # pylint: disable=invalid-name
return self._get_ext_mod()["ext_test_sym"](a, b)
mod, _, ext_mods = TestModule().export_tvm(
spec={
"scalar_add": {
"a": spec.Tensor((), "float32"),
"b": spec.Tensor((), "float32"),
},
"test_sym": {
"a": spec.Tensor(("x", "y", 1), "float32"),
"b": spec.Tensor(("y", "z", 5), "float32"),
},
},
allow_extern=True,
)
_check_ir_equality(mod)
mod = AttachExternModules(ext_mods)(mod) # pylint: disable=not-callable
compiled = tvm.runtime.vm.VirtualMachine(
tvm.compile(mod, target="llvm"),
device=tvm.cpu(),
)
_test_scalar_add(compiled["scalar_add"])
_test_infer_sym(compiled["test_sym"], x=3, y=4, z=2)
if __name__ == "__main__":
test_extern_object()
test_extern_source()