# 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__])