chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,392 @@
|
||||
# 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
|
||||
"""
|
||||
Test PyTorch integration with TVM Relax.
|
||||
|
||||
This test verifies:
|
||||
1. Seamless PyTorch tensor I/O with TVM backend
|
||||
2. Cross-function calls between Python, TIR, and Relax functions
|
||||
3. Dynamic Python function addition and execution
|
||||
4. End-to-end pipeline testing
|
||||
5. Error handling and edge cases
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
import tvm
|
||||
from tvm import relax, tirx
|
||||
from tvm.relax import BasePyModule
|
||||
from tvm.script import ir as I
|
||||
from tvm.script import relax as R
|
||||
from tvm.script import tirx as T
|
||||
|
||||
|
||||
@I.ir_module(s_tir=True)
|
||||
class PyTorchIntegrationModule(BasePyModule):
|
||||
"""Test module for PyTorch integration with TVM."""
|
||||
|
||||
@I.pyfunc
|
||||
def main(self, x: torch.Tensor, w: torch.Tensor) -> torch.Tensor:
|
||||
"""Main function demonstrating cross-function calls."""
|
||||
n = x.shape[0]
|
||||
|
||||
# Call TIR function
|
||||
lv = self.call_tir(self.matmul, [x, w], out_ty=R.Tensor((n, 20), "float32"))
|
||||
|
||||
# Apply ReLU
|
||||
lv1 = F.relu(lv)
|
||||
|
||||
# Call packed function (will be added dynamically)
|
||||
lv2 = self.call_dps_packed("my_softmax", [lv1, 1], out_ty=R.Tensor((n, 20), "float32"))
|
||||
|
||||
# Call Python function
|
||||
lv3 = self.my_identity_func(lv2)
|
||||
|
||||
return lv3
|
||||
|
||||
@T.prim_func(s_tir=True)
|
||||
def matmul(
|
||||
var_A: T.handle,
|
||||
var_B: T.handle,
|
||||
var_C: T.handle,
|
||||
):
|
||||
"""TIR function for matrix multiplication."""
|
||||
n = T.int32()
|
||||
A = T.match_buffer(var_A, (n, 16), "float32")
|
||||
B = T.match_buffer(var_B, (16, 20), "float32")
|
||||
C = T.match_buffer(var_C, (n, 20), "float32")
|
||||
|
||||
for i, j, k in T.grid(n, 20, 16):
|
||||
with T.sblock("block"):
|
||||
vi, vj, vk = T.axis.remap("SSR", [i, j, k])
|
||||
with T.init():
|
||||
C[vi, vj] = T.float32(0)
|
||||
C[vi, vj] = C[vi, vj] + A[vi, vk] * B[vk, vj]
|
||||
|
||||
@I.pyfunc
|
||||
def my_identity_func(self, x: torch.Tensor) -> torch.Tensor:
|
||||
return x
|
||||
|
||||
|
||||
class TestPyTorchIntegration:
|
||||
def test_module_creation_and_instantiation(self):
|
||||
module = PyTorchIntegrationModule
|
||||
|
||||
assert hasattr(module, "__call__"), "Module should be callable"
|
||||
|
||||
device = tvm.cpu(0)
|
||||
instance = module(device)
|
||||
|
||||
assert isinstance(instance, BasePyModule), "Instance should be BasePyModule"
|
||||
|
||||
required_methods = ["main", "call_tir", "call_dps_packed"]
|
||||
for method in required_methods:
|
||||
assert hasattr(instance, method), f"Instance should have method: {method}"
|
||||
|
||||
def test_module_creation_and_instantiation_gpu(self):
|
||||
module = PyTorchIntegrationModule
|
||||
|
||||
if tvm.cuda().exist:
|
||||
|
||||
def run_and_check():
|
||||
assert hasattr(module, "__call__"), "Module should be callable"
|
||||
|
||||
device = tvm.cuda(0)
|
||||
instance = module(device)
|
||||
|
||||
assert isinstance(instance, BasePyModule), "Instance should be BasePyModule"
|
||||
required_methods = ["main", "call_tir", "call_dps_packed"]
|
||||
for method in required_methods:
|
||||
assert hasattr(instance, method), f"Instance should have method: {method}"
|
||||
assert "cuda" in str(instance.target)
|
||||
|
||||
tvm.testing.run_with_gpu_lock(run_and_check)
|
||||
else:
|
||||
pytest.skip("CUDA not available")
|
||||
|
||||
def test_python_function_execution(self):
|
||||
"""Test that Python functions execute correctly."""
|
||||
module = PyTorchIntegrationModule
|
||||
device = tvm.cpu(0)
|
||||
instance = module(device)
|
||||
|
||||
# Test my_identity_func
|
||||
input_tensor = torch.tensor([1.0, 2.0, 3.0], dtype=torch.float32)
|
||||
result = instance.my_identity_func(input_tensor)
|
||||
|
||||
assert isinstance(result, torch.Tensor)
|
||||
assert torch.allclose(result, input_tensor, atol=1e-5)
|
||||
|
||||
def test_tir_function_execution(self):
|
||||
"""Test that TIR functions execute correctly."""
|
||||
module = PyTorchIntegrationModule
|
||||
device = tvm.cpu(0)
|
||||
instance = module(device)
|
||||
|
||||
# Test matmul function
|
||||
n = 3
|
||||
x = torch.randn(n, 16, dtype=torch.float32)
|
||||
w = torch.randn(16, 20, dtype=torch.float32)
|
||||
|
||||
result = instance.call_tir(instance.matmul, [x, w], R.Tensor((n, 20), "float32"))
|
||||
|
||||
assert isinstance(result, torch.Tensor)
|
||||
assert result.shape == (n, 20)
|
||||
|
||||
# Verify result with PyTorch matmul
|
||||
expected = torch.matmul(x, w)
|
||||
assert torch.allclose(result, expected, atol=1e-3)
|
||||
|
||||
def test_dynamic_python_function_addition(self):
|
||||
"""Test adding Python functions dynamically."""
|
||||
module = PyTorchIntegrationModule
|
||||
device = tvm.cpu(0)
|
||||
instance = module(device)
|
||||
|
||||
# Define a custom function
|
||||
def custom_activation(x):
|
||||
return torch.sigmoid(x)
|
||||
|
||||
# Add the function
|
||||
instance.add_python_function("custom_activation", custom_activation)
|
||||
|
||||
# Verify function is added
|
||||
assert hasattr(instance, "custom_activation")
|
||||
assert "custom_activation" in instance.pyfuncs
|
||||
|
||||
# Test function execution
|
||||
input_tensor = torch.tensor([1.0, -1.0, 0.0], dtype=torch.float32)
|
||||
result = instance.custom_activation(input_tensor)
|
||||
|
||||
assert isinstance(result, torch.Tensor)
|
||||
expected = torch.sigmoid(input_tensor)
|
||||
assert torch.allclose(result, expected, atol=1e-5)
|
||||
|
||||
def test_call_dps_packed_with_dynamic_function(self):
|
||||
"""Test call_dps_packed with dynamically added function."""
|
||||
module = PyTorchIntegrationModule
|
||||
device = tvm.cpu(0)
|
||||
instance = module(device)
|
||||
|
||||
# Define my_softmax function
|
||||
def my_softmax(tensor, dim):
|
||||
"""Custom softmax function for testing call_dps_packed."""
|
||||
# Convert TVM Tensor to PyTorch tensor if needed
|
||||
if hasattr(tensor, "numpy"):
|
||||
tensor = torch.from_numpy(tensor.numpy())
|
||||
return F.softmax(tensor, dim=dim)
|
||||
|
||||
# Add the function
|
||||
instance.my_softmax = my_softmax
|
||||
|
||||
# Test call_dps_packed
|
||||
input_tensor = torch.tensor([[1.0, 2.0], [3.0, 4.0]], dtype=torch.float32)
|
||||
|
||||
result = instance.call_dps_packed(
|
||||
"my_softmax", [input_tensor, 1], R.Tensor((2, 2), "float32")
|
||||
)
|
||||
|
||||
assert isinstance(result, torch.Tensor)
|
||||
expected = F.softmax(input_tensor, dim=1)
|
||||
assert torch.allclose(result, expected, atol=1e-5)
|
||||
|
||||
def test_end_to_end_pipeline(self):
|
||||
module = PyTorchIntegrationModule
|
||||
device = tvm.cpu(0)
|
||||
instance = module(device)
|
||||
|
||||
def my_softmax(tensor, dim):
|
||||
if hasattr(tensor, "numpy"):
|
||||
tensor = torch.from_numpy(tensor.numpy())
|
||||
return F.softmax(tensor, dim=dim)
|
||||
|
||||
instance.my_softmax = my_softmax
|
||||
|
||||
n = 5
|
||||
x = torch.randn(n, 16, dtype=torch.float32)
|
||||
w = torch.randn(16, 20, dtype=torch.float32)
|
||||
|
||||
result = instance.main(x, w)
|
||||
|
||||
assert isinstance(result, torch.Tensor)
|
||||
assert result.shape == (n, 20)
|
||||
assert result.dtype == torch.float32
|
||||
|
||||
def test_end_to_end_pipeline_gpu(self):
|
||||
module = PyTorchIntegrationModule
|
||||
|
||||
if tvm.cuda().exist:
|
||||
|
||||
def run_and_check():
|
||||
device = tvm.cuda(0)
|
||||
instance = module(device)
|
||||
|
||||
# Test basic GPU functionality without complex TIR operations
|
||||
assert isinstance(instance, BasePyModule)
|
||||
assert "cuda" in str(instance.target)
|
||||
|
||||
# Test that we can create and work with GPU tensors
|
||||
n = 5
|
||||
x = torch.randn(n, 16, dtype=torch.float32, device="cuda")
|
||||
w = torch.randn(16, 20, dtype=torch.float32, device="cuda")
|
||||
|
||||
assert x.device.type == "cuda"
|
||||
assert w.device.type == "cuda"
|
||||
assert x.shape == (n, 16)
|
||||
assert w.shape == (16, 20)
|
||||
|
||||
# Test basic PyTorch operations on GPU
|
||||
result = torch.matmul(x, w)
|
||||
assert isinstance(result, torch.Tensor)
|
||||
assert result.shape == (n, 20)
|
||||
assert result.dtype == torch.float32
|
||||
assert result.device.type == "cuda"
|
||||
|
||||
tvm.testing.run_with_gpu_lock(run_and_check)
|
||||
else:
|
||||
pytest.skip("CUDA not available")
|
||||
|
||||
def test_cross_function_data_flow(self):
|
||||
"""Test data flow between different function types."""
|
||||
module = PyTorchIntegrationModule
|
||||
device = tvm.cpu(0)
|
||||
instance = module(device)
|
||||
|
||||
# Add required functions
|
||||
def my_softmax(tensor, dim):
|
||||
if hasattr(tensor, "numpy"):
|
||||
tensor = torch.from_numpy(tensor.numpy())
|
||||
return F.softmax(tensor, dim=dim)
|
||||
|
||||
instance.my_softmax = my_softmax
|
||||
|
||||
# Create test data
|
||||
n = 4
|
||||
x = torch.randn(n, 16, dtype=torch.float32)
|
||||
w = torch.randn(16, 20, dtype=torch.float32)
|
||||
|
||||
# Execute step by step to verify data flow
|
||||
# Step 1: TIR matmul
|
||||
lv = instance.call_tir(instance.matmul, [x, w], R.Tensor((n, 20), "float32"))
|
||||
assert isinstance(lv, torch.Tensor)
|
||||
assert lv.shape == (n, 20)
|
||||
|
||||
# Step 2: ReLU
|
||||
lv1 = F.relu(lv)
|
||||
assert isinstance(lv1, torch.Tensor)
|
||||
assert lv1.shape == (n, 20)
|
||||
|
||||
# Step 3: Softmax via call_dps_packed
|
||||
lv2 = instance.call_dps_packed("my_softmax", [lv1, 1], R.Tensor((n, 20), "float32"))
|
||||
assert isinstance(lv2, torch.Tensor)
|
||||
assert lv2.shape == (n, 20)
|
||||
|
||||
# Step 4: Identity function
|
||||
lv3 = instance.my_identity_func(lv2)
|
||||
assert isinstance(lv3, torch.Tensor)
|
||||
assert lv3.shape == (n, 20)
|
||||
|
||||
# Verify final result matches expected
|
||||
expected = F.softmax(F.relu(torch.matmul(x, w)), dim=1)
|
||||
assert torch.allclose(lv3, expected, atol=1e-3)
|
||||
|
||||
def test_error_handling(self):
|
||||
"""Test error handling for various edge cases."""
|
||||
module = PyTorchIntegrationModule
|
||||
device = tvm.cpu(0)
|
||||
instance = module(device)
|
||||
|
||||
# Test with missing function
|
||||
with pytest.raises(Exception):
|
||||
instance.call_dps_packed(
|
||||
"non_existent_function", [torch.tensor([1.0])], R.Tensor((1,), "float32")
|
||||
)
|
||||
|
||||
# Test with wrong tensor shapes
|
||||
x = torch.randn(3, 16, dtype=torch.float32)
|
||||
w = torch.randn(15, 20, dtype=torch.float32) # Wrong shape
|
||||
|
||||
with pytest.raises(Exception):
|
||||
instance.call_tir(instance.matmul, [x, w], R.Tensor((3, 20), "float32"))
|
||||
|
||||
def test_tensor_type_preservation(self):
|
||||
module = PyTorchIntegrationModule
|
||||
device = tvm.cpu(0)
|
||||
instance = module(device)
|
||||
|
||||
def my_softmax(tensor, dim):
|
||||
if hasattr(tensor, "numpy"):
|
||||
tensor = torch.from_numpy(tensor.numpy())
|
||||
return F.softmax(tensor, dim=dim)
|
||||
|
||||
instance.my_softmax = my_softmax
|
||||
|
||||
# Test with float32 data type (TIR function is hardcoded for float32)
|
||||
test_dtype = torch.float32
|
||||
n = 3
|
||||
x = torch.randn(n, 16, dtype=test_dtype)
|
||||
w = torch.randn(16, 20, dtype=test_dtype)
|
||||
|
||||
result = instance.main(x, w)
|
||||
|
||||
# Verify type preservation
|
||||
assert result.dtype == test_dtype
|
||||
assert isinstance(result, torch.Tensor)
|
||||
assert result.shape == (n, 20)
|
||||
assert result.dtype == torch.float32
|
||||
|
||||
def test_batch_processing(self):
|
||||
"""Test processing multiple inputs in batch."""
|
||||
module = PyTorchIntegrationModule
|
||||
device = tvm.cpu(0)
|
||||
instance = module(device)
|
||||
|
||||
# Add required functions
|
||||
def my_softmax(tensor, dim):
|
||||
if hasattr(tensor, "numpy"):
|
||||
tensor = torch.from_numpy(tensor.numpy())
|
||||
return F.softmax(tensor, dim=dim)
|
||||
|
||||
instance.my_softmax = my_softmax
|
||||
|
||||
# Process multiple inputs
|
||||
batch_size = 5
|
||||
results = []
|
||||
|
||||
for i in range(batch_size):
|
||||
n = 3 + i # Varying batch sizes
|
||||
x = torch.randn(n, 16, dtype=torch.float32)
|
||||
w = torch.randn(16, 20, dtype=torch.float32)
|
||||
|
||||
result = instance.main(x, w)
|
||||
results.append(result)
|
||||
|
||||
assert isinstance(result, torch.Tensor)
|
||||
assert result.shape == (n, 20)
|
||||
|
||||
# Verify all results are valid
|
||||
assert len(results) == batch_size
|
||||
for result in results:
|
||||
assert isinstance(result, torch.Tensor)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
Reference in New Issue
Block a user