278 lines
8.9 KiB
Python
278 lines
8.9 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.
|
|
"""
|
|
Tests for Example NPU Backend
|
|
|
|
This test file demonstrates how to test a custom NPU backend
|
|
implementation using TVM's testing infrastructure.
|
|
"""
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
import tvm
|
|
import tvm.testing
|
|
from tvm import relax
|
|
from tvm.relax.backend.pattern_registry import get_patterns_with_prefix
|
|
from tvm.relax.transform import FuseOpsByPattern, MergeCompositeFunctions, RunCodegen
|
|
from tvm.script import relax as R
|
|
|
|
|
|
@tvm.script.ir_module
|
|
class MatmulReLU:
|
|
"""Example module with matrix multiplication and ReLU"""
|
|
|
|
@R.function
|
|
def main(
|
|
x: R.Tensor((2, 4), "float32"),
|
|
w: R.Tensor((4, 8), "float32"),
|
|
) -> R.Tensor((2, 8), "float32"):
|
|
with R.dataflow():
|
|
y = relax.op.matmul(x, w)
|
|
z = relax.op.nn.relu(y)
|
|
R.output(z)
|
|
return z
|
|
|
|
|
|
@tvm.script.ir_module
|
|
class Conv2dReLU:
|
|
"""Example module with 2D convolution and ReLU"""
|
|
|
|
@R.function
|
|
def main(
|
|
x: R.Tensor((1, 3, 32, 32), "float32"),
|
|
w: R.Tensor((16, 3, 3, 3), "float32"),
|
|
) -> R.Tensor((1, 16, 30, 30), "float32"):
|
|
with R.dataflow():
|
|
y = relax.op.nn.conv2d(x, w)
|
|
z = relax.op.nn.relu(y)
|
|
R.output(z)
|
|
return z
|
|
|
|
|
|
@tvm.script.ir_module
|
|
class MultipleOps:
|
|
"""Example module with multiple operations that can be fused"""
|
|
|
|
@R.function
|
|
def main(
|
|
x: R.Tensor((1, 16, 32, 32), "float32"),
|
|
) -> R.Tensor((1, 16, 16, 16), "float32"):
|
|
with R.dataflow():
|
|
# First ReLU
|
|
y = relax.op.nn.relu(x)
|
|
# Max pooling
|
|
z = relax.op.nn.max_pool2d(y, pool_size=(2, 2), strides=(2, 2))
|
|
# Second ReLU
|
|
out = relax.op.nn.relu(z)
|
|
R.output(out)
|
|
return out
|
|
|
|
|
|
@tvm.script.ir_module
|
|
class Softmax:
|
|
"""Example module with softmax"""
|
|
|
|
@R.function
|
|
def main(x: R.Tensor((2, 8), "float32")) -> R.Tensor((2, 8), "float32"):
|
|
with R.dataflow():
|
|
z = relax.op.nn.softmax(x)
|
|
R.output(z)
|
|
return z
|
|
|
|
|
|
# Check if the example NPU runtime is available
|
|
has_example_npu_codegen = tvm.get_global_func("relax.ext.example_npu", True)
|
|
has_example_npu_runtime = tvm.get_global_func("runtime.ExampleNPUJSONRuntimeCreate", True)
|
|
has_example_npu = has_example_npu_codegen and has_example_npu_runtime
|
|
|
|
example_npu_enabled = pytest.mark.skipif(
|
|
not has_example_npu,
|
|
reason="Example NPU backend not enabled. Compile with the example NPU runtime.",
|
|
)
|
|
|
|
|
|
def test_example_npu_patterns_registered():
|
|
"""Test that all expected patterns are registered"""
|
|
import tvm.relax.backend.contrib.example_npu # noqa: F401
|
|
|
|
patterns = get_patterns_with_prefix("example_npu")
|
|
pattern_names = {p.name for p in patterns}
|
|
|
|
# Core patterns that should always be available
|
|
core_patterns = {
|
|
"example_npu.dense",
|
|
"example_npu.matmul",
|
|
"example_npu.conv1d",
|
|
"example_npu.conv2d",
|
|
"example_npu.max_pool2d",
|
|
}
|
|
|
|
assert core_patterns.issubset(pattern_names), (
|
|
f"Missing core patterns: {core_patterns - pattern_names}"
|
|
)
|
|
|
|
# Check that at least some activation patterns are available
|
|
activation_patterns = {name for name in pattern_names if "relu" in name or "sigmoid" in name}
|
|
assert len(activation_patterns) > 0, "No activation patterns found"
|
|
|
|
|
|
@example_npu_enabled
|
|
def test_example_npu_matmul_relu_partitioning():
|
|
"""Test graph partitioning for MatMul + ReLU pattern"""
|
|
import tvm.relax.backend.contrib.example_npu # noqa: F401
|
|
|
|
mod = MatmulReLU
|
|
patterns = get_patterns_with_prefix("example_npu")
|
|
|
|
# Partition the graph
|
|
partitioned_mod = FuseOpsByPattern(patterns, bind_constants=False, annotate_codegen=False)(mod)
|
|
partitioned_mod = MergeCompositeFunctions()(partitioned_mod)
|
|
|
|
# Verify partitioning happened
|
|
assert partitioned_mod is not None
|
|
|
|
# Check that composite functions were created
|
|
for gvar, func in partitioned_mod.functions.items():
|
|
if gvar.name_hint != "main":
|
|
# This should be a composite function
|
|
assert "Composite" in str(func)
|
|
|
|
|
|
@example_npu_enabled
|
|
def test_example_npu_conv2d_relu_partitioning():
|
|
"""Test graph partitioning for Conv2D + ReLU pattern"""
|
|
import tvm.relax.backend.contrib.example_npu # noqa: F401
|
|
|
|
mod = Conv2dReLU
|
|
patterns = get_patterns_with_prefix("example_npu")
|
|
|
|
# Partition the graph
|
|
partitioned_mod = FuseOpsByPattern(patterns, bind_constants=False, annotate_codegen=False)(mod)
|
|
partitioned_mod = MergeCompositeFunctions()(partitioned_mod)
|
|
|
|
assert partitioned_mod is not None
|
|
|
|
|
|
@example_npu_enabled
|
|
def test_example_npu_multiple_ops():
|
|
"""Test partitioning with multiple fusable operations"""
|
|
import tvm.relax.backend.contrib.example_npu # noqa: F401
|
|
|
|
mod = MultipleOps
|
|
patterns = get_patterns_with_prefix("example_npu")
|
|
|
|
# Partition the graph
|
|
partitioned_mod = FuseOpsByPattern(patterns, bind_constants=False, annotate_codegen=False)(mod)
|
|
partitioned_mod = MergeCompositeFunctions()(partitioned_mod)
|
|
|
|
assert partitioned_mod is not None
|
|
|
|
|
|
@example_npu_enabled
|
|
def test_example_npu_softmax_partitioning():
|
|
"""Test graph partitioning for softmax pattern"""
|
|
import tvm.relax.backend.contrib.example_npu # noqa: F401
|
|
|
|
mod = Softmax
|
|
patterns = get_patterns_with_prefix("example_npu")
|
|
|
|
partitioned_mod = FuseOpsByPattern(patterns, bind_constants=False, annotate_codegen=False)(mod)
|
|
partitioned_mod = MergeCompositeFunctions()(partitioned_mod)
|
|
|
|
assert partitioned_mod is not None
|
|
|
|
for gvar, func in partitioned_mod.functions.items():
|
|
if gvar.name_hint != "main":
|
|
assert "Composite" in str(func)
|
|
|
|
|
|
@example_npu_enabled
|
|
def test_example_npu_codegen():
|
|
"""Test code generation for the example NPU backend"""
|
|
import tvm.relax.backend.contrib.example_npu # noqa: F401
|
|
|
|
mod = MatmulReLU
|
|
patterns = get_patterns_with_prefix("example_npu")
|
|
|
|
# Partition and generate code
|
|
partitioned_mod = FuseOpsByPattern(patterns, bind_constants=False, annotate_codegen=True)(mod)
|
|
partitioned_mod = MergeCompositeFunctions()(partitioned_mod)
|
|
partitioned_mod = RunCodegen()(partitioned_mod)
|
|
|
|
assert partitioned_mod is not None
|
|
|
|
# The module should now contain external function calls
|
|
main_func = partitioned_mod["main"]
|
|
assert main_func is not None
|
|
|
|
|
|
@example_npu_enabled
|
|
def test_example_npu_runtime_execution():
|
|
"""Test end-to-end execution with the example NPU runtime"""
|
|
import tvm.relax.backend.contrib.example_npu
|
|
|
|
# Create simple test inputs
|
|
np.random.seed(42)
|
|
x_np = np.random.randn(2, 4).astype("float32")
|
|
w_np = np.random.randn(4, 8).astype("float32")
|
|
|
|
# Expected output (computed with NumPy)
|
|
expected = np.maximum(0, np.matmul(x_np, w_np))
|
|
|
|
# Build and run with example NPU backend
|
|
mod = MatmulReLU
|
|
patterns = get_patterns_with_prefix("example_npu")
|
|
|
|
# Apply transformations
|
|
mod = FuseOpsByPattern(patterns, bind_constants=False, annotate_codegen=True)(mod)
|
|
mod = MergeCompositeFunctions()(mod)
|
|
mod = RunCodegen()(mod)
|
|
|
|
# Build the module
|
|
target = tvm.target.Target("llvm")
|
|
with tvm.transform.PassContext(opt_level=3):
|
|
built = relax.build(mod, target)
|
|
|
|
# Create VM and run
|
|
vm = relax.VirtualMachine(built, tvm.cpu())
|
|
|
|
x_tvm = tvm.runtime.tensor(x_np, tvm.cpu())
|
|
w_tvm = tvm.runtime.tensor(w_np, tvm.cpu())
|
|
|
|
result = vm["main"](x_tvm, w_tvm)
|
|
|
|
# Verify the result shape is correct (the runtime is a stub and does not compute numerically)
|
|
assert result.numpy().shape == expected.shape
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Run tests locally for debugging
|
|
test_example_npu_patterns_registered()
|
|
|
|
if has_example_npu:
|
|
print("Example NPU backend is available, running tests...")
|
|
test_example_npu_matmul_relu_partitioning()
|
|
test_example_npu_conv2d_relu_partitioning()
|
|
test_example_npu_softmax_partitioning()
|
|
test_example_npu_multiple_ops()
|
|
test_example_npu_codegen()
|
|
test_example_npu_runtime_execution()
|
|
print("All tests passed!")
|
|
else:
|
|
print("Example NPU backend not available. Compile with example NPU runtime to run tests.")
|