# 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, invalid-name, unused-argument # ruff: noqa: F401, F841 import pytest import tvm 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 @I.ir_module class SimplePyFuncModule(BasePyModule): """Test simple Python functions with basic operations.""" @I.pyfunc def add(self, x, y): """Simple addition function.""" x_tvm = self._convert_pytorch_to_tvm(x) y_tvm = self._convert_pytorch_to_tvm(y) result = self.call_tir(self.add_tir, [x_tvm, y_tvm], out_ty=R.Tensor((5,), "float32")) return self._convert_tvm_to_pytorch(result) @I.pyfunc def multiply(self, x, y): """Simple multiplication function.""" x_tvm = self._convert_pytorch_to_tvm(x) y_tvm = self._convert_pytorch_to_tvm(y) result = self.call_tir(self.multiply_tir, [x_tvm, y_tvm], out_ty=R.Tensor((5,), "float32")) return self._convert_tvm_to_pytorch(result) @T.prim_func(s_tir=True) def add_tir(var_x: T.handle, var_y: T.handle, var_out: T.handle): x = T.match_buffer(var_x, (5,), "float32") y = T.match_buffer(var_y, (5,), "float32") out = T.match_buffer(var_out, (5,), "float32") for i in range(5): out[i] = x[i] + y[i] @T.prim_func(s_tir=True) def multiply_tir(var_x: T.handle, var_y: T.handle, var_out: T.handle): x = T.match_buffer(var_x, (5,), "float32") y = T.match_buffer(var_y, (5,), "float32") out = T.match_buffer(var_out, (5,), "float32") for i in range(5): out[i] = x[i] * y[i] @R.function def main_relax(x: R.Tensor((5,), "float32"), y: R.Tensor((5,), "float32")) -> R.Tensor( (5,), "float32" ): return R.add(x, y) @I.ir_module class ComplexPyFuncModule(BasePyModule): """Test complex Python logic with ML pipeline and error handling.""" @I.pyfunc def ml_pipeline(self, input_data, model_params): """Complex ML pipeline with data validation and error handling.""" # Data validation if input_data is None or model_params is None: raise ValueError("Inputs cannot be None") try: # Convert to TVM format tvm_data = self._convert_pytorch_to_tvm(input_data) tvm_params = self._convert_pytorch_to_tvm(model_params) # Run ML inference features = self.call_tir( self.extract_features, [tvm_data], out_ty=R.Tensor((10,), "float32") ) predictions = self.call_tir( self.ml_inference, [features, tvm_params], out_ty=R.Tensor((5,), "float32") ) # Post-process results final_result = self.call_tir( self.post_process, [predictions], out_ty=R.Tensor((5,), "float32") ) return self._convert_tvm_to_pytorch(final_result) except Exception as e: self._log_error(f"ML pipeline failed: {e}") return self._get_default_value() @I.pyfunc def data_preprocessing(self, raw_data): """Data preprocessing with conditional logic.""" if hasattr(raw_data, "numpy"): # Vectorized path for numpy-compatible data data_np = raw_data.numpy() processed = self._vectorized_preprocess(data_np) else: # Fallback path for other data types processed = self._elementwise_preprocess(raw_data) # Convert and return tvm_processed = self._convert_pytorch_to_tvm(processed) result = self.call_tir( self.normalize_data, [tvm_processed], out_ty=R.Tensor((10,), "float32") ) return self._convert_tvm_to_pytorch(result) @T.prim_func(s_tir=True) def extract_features(data: T.handle, features: T.handle): T.func_attr({"tirx.noalias": True}) Data = T.match_buffer(data, (10,), "float32") Features = T.match_buffer(features, (10,), "float32") for i in range(10): Features[i] = T.sqrt(Data[i]) @T.prim_func(s_tir=True) def ml_inference(features: T.handle, params: T.handle, output: T.handle): T.func_attr({"tirx.noalias": True}) Features = T.match_buffer(features, (10,), "float32") Params = T.match_buffer(params, (10,), "float32") Output = T.match_buffer(output, (5,), "float32") for i in range(5): Output[i] = Features[i] * Params[i] + Features[i + 5] * Params[i + 5] @T.prim_func(s_tir=True) def post_process(predictions: T.handle, final: T.handle): T.func_attr({"tirx.noalias": True}) Predictions = T.match_buffer(predictions, (5,), "float32") Final = T.match_buffer(final, (5,), "float32") for i in range(5): Final[i] = T.max(Predictions[i], 0.0) @T.prim_func(s_tir=True) def normalize_data(data: T.handle, normalized: T.handle): T.func_attr({"tirx.noalias": True}) Data = T.match_buffer(data, (10,), "float32") Normalized = T.match_buffer(normalized, (10,), "float32") for i in range(10): Normalized[i] = Data[i] / 255.0 @I.ir_module class EdgeCasePyFuncModule(BasePyModule): """Test edge cases and boundary conditions.""" @I.pyfunc def empty_func(self): """Empty function with no operations.""" pass @I.pyfunc def single_return(self, x): """Function with immediate return.""" return x @I.pyfunc def nested_conditionals(self, data, threshold): """Function with complex nested conditional logic.""" if data is None: return None if hasattr(data, "shape"): if len(data.shape) == 1: if data.shape[0] > threshold: return self._process_large_data(data) else: return self._process_small_data(data) elif len(data.shape) == 2: return self._process_2d_data(data) else: return self._process_nd_data(data) else: return self._process_scalar_data(data) @I.pyfunc def loop_with_break(self, data, max_iter): """Function with loop and break statement.""" result = [] for i, item in enumerate(data): if i >= max_iter: break if item > 0: result.append(item * 2) else: result.append(0) return result @T.prim_func(s_tir=True) def dummy_tir(data: T.handle, output: T.handle): T.func_attr({"tirx.noalias": True}) Data = T.match_buffer(data, (1,), "float32") Output = T.match_buffer(output, (1,), "float32") Output[0] = Data[0] @I.ir_module class PerformancePyFuncModule(BasePyModule): """Test performance optimization patterns.""" @I.pyfunc def vectorized_operation(self, x, y): """Vectorized operation with numpy fallback.""" try: # Try vectorized operation first if hasattr(x, "numpy") and hasattr(y, "numpy"): x_np = x.numpy() y_np = y.numpy() result_np = x_np + y_np return self._convert_numpy_to_pytorch(result_np) except Exception: pass # Fallback to TVM processing x_tvm = self._convert_pytorch_to_tvm(x) y_tvm = self._convert_pytorch_to_tvm(y) result = self.call_tir( self.vectorized_add, [x_tvm, y_tvm], out_ty=R.Tensor((10,), "float32") ) return self._convert_tvm_to_pytorch(result) @I.pyfunc def batch_processing(self, batch_data): """Batch processing with memory optimization.""" batch_size = len(batch_data) results = [] # Process in chunks to optimize memory usage chunk_size = min(batch_size, 100) for i in range(0, batch_size, chunk_size): chunk = batch_data[i : i + chunk_size] chunk_result = self._process_chunk(chunk) results.extend(chunk_result) return results @I.pyfunc def memory_efficient_transform(self, large_tensor): """Memory-efficient tensor transformation.""" # Use in-place operations when possible if hasattr(large_tensor, "requires_grad") and not large_tensor.requires_grad: # In-place operation for efficiency large_tensor.add_(1.0) return large_tensor else: # Create new tensor if gradients are needed return large_tensor + 1.0 @T.prim_func(s_tir=True) def vectorized_add(a: T.handle, b: T.handle, c: T.handle): T.func_attr({"tirx.noalias": True}) A = T.match_buffer(a, (10,), "float32") B = T.match_buffer(b, (10,), "float32") C = T.match_buffer(c, (10,), "float32") for i in range(10): C[i] = A[i] + B[i] @I.ir_module class IntegrationPyFuncModule(BasePyModule): """Test integration with external libraries and complex workflows.""" @I.pyfunc def sklearn_integration(self, input_data, scaler_params): """Integration with scikit-learn preprocessing.""" try: # Import sklearn components from sklearn.decomposition import PCA from sklearn.preprocessing import StandardScaler # Create and fit scaler scaler = StandardScaler() if scaler_params is not None: scaler.mean_ = scaler_params["mean"] scaler.scale_ = scaler_params["scale"] else: scaler.fit(input_data) # Transform data scaled_data = scaler.transform(input_data) # Apply PCA if needed if input_data.shape[1] > 10: pca = PCA(n_components=10) reduced_data = pca.fit_transform(scaled_data) else: reduced_data = scaled_data # Convert to TVM and process tvm_data = self._convert_pytorch_to_tvm(reduced_data) result = self.call_tir( self.final_transform, [tvm_data], out_ty=R.Tensor((reduced_data.shape[0], 10), "float32"), ) return self._convert_tvm_to_pytorch(result) except ImportError: # Fallback if sklearn is not available return self._fallback_preprocessing(input_data) @I.pyfunc def multi_stage_pipeline(self, raw_input): """Multi-stage processing pipeline.""" # Stage 1: Data cleaning cleaned = self._clean_data(raw_input) # Stage 2: Feature extraction features = self._extract_features(cleaned) # Stage 3: Model inference predictions = self._run_inference(features) # Stage 4: Post-processing final_result = self._post_process_output(predictions) return final_result @T.prim_func(s_tir=True) def final_transform(data: T.handle, output: T.handle): T.func_attr({"tirx.noalias": True}) Data = T.match_buffer(data, (10, 10), "float32") Output = T.match_buffer(output, (10, 10), "float32") for i in range(10): for j in range(10): Output[i, j] = T.tanh(Data[i, j]) @I.ir_module class ErrorHandlingPyFuncModule(BasePyModule): """Test comprehensive error handling and validation.""" @I.pyfunc def robust_data_processing(self, input_data, config): """Robust data processing with comprehensive error handling.""" try: # Validate inputs if not self._validate_inputs(input_data, config): raise ValueError("Invalid input data or configuration") # Check data types if not self._check_data_types(input_data): raise TypeError("Unsupported data types") # Process data with retry logic max_retries = config.get("max_retries", 3) for attempt in range(max_retries): try: result = self._process_with_validation(input_data, config) if self._validate_output(result): return result else: raise RuntimeError("Output validation failed") except Exception as e: if attempt == max_retries - 1: raise self._log_warning(f"Attempt {attempt + 1} failed: {e}") continue except Exception as e: self._log_error(f"Data processing failed: {e}") return self._get_safe_fallback(input_data, config) @I.pyfunc def graceful_degradation(self, primary_input, fallback_input): """Function that gracefully degrades when primary path fails.""" try: # Try primary processing path result = self._primary_processing(primary_input) return result except Exception as e: self._log_warning(f"Primary processing failed: {e}") try: # Try fallback path result = self._fallback_processing(fallback_input) return result except Exception as e2: self._log_error(f"Fallback processing also failed: {e2}") # Return safe default return self._get_safe_default() @T.prim_func(s_tir=True) def safe_transform(data: T.handle, output: T.handle): T.func_attr({"tirx.noalias": True}) Data = T.match_buffer(data, (5,), "float32") Output = T.match_buffer(output, (5,), "float32") for i in range(5): # Safe operation that handles edge cases if Data[i] > 0: Output[i] = T.sqrt(Data[i]) else: Output[i] = 0.0 # Pytest test functions to verify the classes work correctly def test_simple_pyfunc_module_creation(): """Test that SimplePyFuncModule can be created.""" # Get the IRModule instance from the TVMScript decorated class ir_mod = SimplePyFuncModule device = tvm.cpu() # Create BasePyModule instance module = BasePyModule(ir_mod, device) assert isinstance(module, BasePyModule) # Note: Python functions are stored in pyfuncs, not as direct attributes # We need to check if they exist in the IRModule's pyfuncs if hasattr(ir_mod, "pyfuncs"): assert "add" in ir_mod.pyfuncs assert "multiply" in ir_mod.pyfuncs # Check that TIR functions exist assert hasattr(module, "add_tir") assert hasattr(module, "multiply_tir") # Note: This particular TVMScript is for testing purpose only, and cannot compile # Relax functions may not be available due to TVMScript compilation issues print("Note: This TVMScript is for testing purpose only, and cannot compile") def test_complex_pyfunc_module_creation(): """Test that ComplexPyFuncModule can be created.""" ir_mod = ComplexPyFuncModule device = tvm.cpu() module = BasePyModule(ir_mod, device) assert isinstance(module, BasePyModule) # Check Python functions in pyfuncs if hasattr(ir_mod, "pyfuncs"): assert "ml_pipeline" in ir_mod.pyfuncs assert "data_preprocessing" in ir_mod.pyfuncs # Check TIR functions assert hasattr(module, "extract_features") assert hasattr(module, "ml_inference") assert hasattr(module, "post_process") assert hasattr(module, "normalize_data") def test_edge_case_pyfunc_module_creation(): """Test that EdgeCasePyFuncModule can be created.""" ir_mod = EdgeCasePyFuncModule device = tvm.cpu() module = BasePyModule(ir_mod, device) assert isinstance(module, BasePyModule) # Check Python functions in pyfuncs if hasattr(ir_mod, "pyfuncs"): assert "empty_func" in ir_mod.pyfuncs assert "single_return" in ir_mod.pyfuncs assert "nested_conditionals" in ir_mod.pyfuncs assert "loop_with_break" in ir_mod.pyfuncs # Check TIR function assert hasattr(module, "dummy_tir") def test_performance_pyfunc_module_creation(): """Test that PerformancePyFuncModule can be created.""" ir_mod = PerformancePyFuncModule device = tvm.cpu() module = BasePyModule(ir_mod, device) assert isinstance(module, BasePyModule) # Check Python functions in pyfuncs if hasattr(ir_mod, "pyfuncs"): assert "vectorized_operation" in ir_mod.pyfuncs assert "batch_processing" in ir_mod.pyfuncs assert "memory_efficient_transform" in ir_mod.pyfuncs # Check TIR function assert hasattr(module, "vectorized_add") def test_integration_pyfunc_module_creation(): """Test that IntegrationPyFuncModule can be created.""" ir_mod = IntegrationPyFuncModule device = tvm.cpu() module = BasePyModule(ir_mod, device) assert isinstance(module, BasePyModule) # Check Python functions in pyfuncs if hasattr(ir_mod, "pyfuncs"): assert "sklearn_integration" in ir_mod.pyfuncs assert "multi_stage_pipeline" in ir_mod.pyfuncs # Check TIR function assert hasattr(module, "final_transform") def test_error_handling_pyfunc_module_creation(): """Test that ErrorHandlingPyFuncModule can be created.""" ir_mod = ErrorHandlingPyFuncModule device = tvm.cpu() module = BasePyModule(ir_mod, device) assert isinstance(module, BasePyModule) # Check Python functions in pyfuncs if hasattr(ir_mod, "pyfuncs"): assert "robust_data_processing" in ir_mod.pyfuncs assert "graceful_degradation" in ir_mod.pyfuncs # Check TIR function assert hasattr(module, "safe_transform") def test_all_modules_inherit_from_base(): """Test that all modules properly inherit from BasePyModule.""" modules = [ SimplePyFuncModule, ComplexPyFuncModule, EdgeCasePyFuncModule, PerformancePyFuncModule, IntegrationPyFuncModule, ErrorHandlingPyFuncModule, ] device = tvm.cpu() for ir_mod in modules: module = BasePyModule(ir_mod, device) assert isinstance(module, BasePyModule) assert hasattr(module, "script") assert hasattr(module, "show") def test_pyfunc_decorators(): """Test that all @I.pyfunc decorated functions are present.""" ir_mod = SimplePyFuncModule device = tvm.cpu() module = BasePyModule(ir_mod, device) # Check that the functions exist in pyfuncs if hasattr(ir_mod, "pyfuncs"): assert "add" in ir_mod.pyfuncs assert "multiply" in ir_mod.pyfuncs # Get the actual function objects add_func = ir_mod.pyfuncs["add"] multiply_func = ir_mod.pyfuncs["multiply"] # Check that they are callable assert callable(add_func) assert callable(multiply_func) # Check function signatures import inspect add_sig = inspect.signature(add_func) assert len(add_sig.parameters) == 3 # self, x, y multiply_sig = inspect.signature(multiply_func) assert len(multiply_sig.parameters) == 3 # self, x, y def test_tir_functions(): """Test that TIR functions are properly defined.""" ir_mod = SimplePyFuncModule device = tvm.cpu() module = BasePyModule(ir_mod, device) # Check TIR function attributes assert hasattr(module, "add_tir") assert hasattr(module, "multiply_tir") # These should be callable (though they're TIR functions) assert callable(module.add_tir) assert callable(module.multiply_tir) def test_relax_functions(): """Test that Relax functions are properly defined.""" ir_mod = SimplePyFuncModule device = tvm.cpu() module = BasePyModule(ir_mod, device) # Note: This particular TVMScript is for testing purpose only, and cannot compile # Relax functions may not be available due to TVMScript compilation issues print("Note: This TVMScript is for testing purpose only, and cannot compile") # We can still check that the module was created successfully assert isinstance(module, BasePyModule) assert hasattr(module, "script") assert hasattr(module, "show") def test_module_docstrings(): """Test that all modules have proper docstrings.""" modules = [ SimplePyFuncModule, ComplexPyFuncModule, EdgeCasePyFuncModule, PerformancePyFuncModule, IntegrationPyFuncModule, ErrorHandlingPyFuncModule, ] for module_class in modules: # TVMScript decorator changes the class, so we check that it's callable # and can create instances instead of checking docstrings assert callable(module_class) # We can't directly instantiate TVMScript decorated classes # but we can create BasePyModule instances with them device = tvm.cpu() instance = BasePyModule(module_class, device) assert isinstance(instance, BasePyModule) def test_python_function_complexity(): """Test that complex Python functions have the expected structure.""" ir_mod = ComplexPyFuncModule device = tvm.cpu() module = BasePyModule(ir_mod, device) # Check that complex functions exist in pyfuncs if hasattr(ir_mod, "pyfuncs"): assert "ml_pipeline" in ir_mod.pyfuncs assert "data_preprocessing" in ir_mod.pyfuncs # Get the actual function objects ml_func = ir_mod.pyfuncs["ml_pipeline"] preprocess_func = ir_mod.pyfuncs["data_preprocessing"] # These should be callable assert callable(ml_func) assert callable(preprocess_func) # Check function signatures import inspect ml_sig = inspect.signature(ml_func) assert len(ml_sig.parameters) == 3 # self, input_data, model_params preprocess_sig = inspect.signature(preprocess_func) assert len(preprocess_sig.parameters) == 2 # self, raw_data def test_script_and_show_methods(): """Test that script() and show() methods work correctly.""" ir_mod = SimplePyFuncModule device = tvm.cpu() module = BasePyModule(ir_mod, device) # Test script() method script_output = module.script() assert isinstance(script_output, str) assert len(script_output) > 0 # Test show() method try: module.show() # If we get here, show() worked assert True except Exception as e: # If show() fails, the feature is not working properly pytest.fail(f"show() method failed: {e}") def test_python_functions_in_irmodule(): """Test that Python functions are properly stored in IRModule pyfuncs.""" ir_mod = SimplePyFuncModule device = tvm.cpu() module = BasePyModule(ir_mod, device) # Check that pyfuncs attribute exists and contains our functions if hasattr(ir_mod, "pyfuncs"): pyfuncs = ir_mod.pyfuncs assert isinstance(pyfuncs, dict) assert "add" in pyfuncs assert "multiply" in pyfuncs # Check that the functions are callable assert callable(pyfuncs["add"]) assert callable(pyfuncs["multiply"]) # Check function names assert pyfuncs["add"].__name__ == "add" assert pyfuncs["multiply"].__name__ == "multiply" else: pytest.fail("pyfuncs attribute not found in IRModule") def test_call_py_func_with_base_py_module(): """Test R.call_py_func with BasePyModule.""" import numpy as np import torch from tvm.relax import TensorType, Var from tvm.relax.expr import StringImm from tvm.relax.op import call_py_func # Test 1: Operator creation and basic properties x = Var("x", TensorType((5,), "float32")) y = Var("y", TensorType((5,), "float32")) call_expr = call_py_func(StringImm("test_func"), (x, y), out_ty=R.Tensor((5,), "float32")) assert call_expr.op.name == "relax.call_py_func" assert call_expr.args[0].value == "test_func" assert len(call_expr.args) == 2 # Test 2: Compilation validation try: call_py_func( "invalid", (Var("x", TensorType((5,), "float32")),), out_ty=R.Tensor((5,), "float32"), ) assert False, "Should raise type error" except Exception as e: assert "Mismatched type" in str(e) or "Expected" in str(e) # Test 3: Validation and error handling @I.ir_module class ValidationTestModule(BasePyModule): @R.function def test_invalid_call(x: R.Tensor((5,), "float32")) -> R.Tensor((5,), "float32"): result = R.call_py_func("non_existent_func", (x,), out_ty=R.Tensor((5,), "float32")) return result device = tvm.cpu() module = ValidationTestModule(device) x = torch.randn(5, dtype=torch.float32) with pytest.raises(ValueError, match="Python function 'non_existent_func' not found"): module.call_py_func("non_existent_func", [x]) # Test 4: Using call_py_func within Relax functions @I.ir_module class RelaxCallPyFuncModule(BasePyModule): @I.pyfunc def torch_relu(self, x): """PyTorch ReLU implementation.""" return torch.relu(x) @I.pyfunc def torch_softmax(self, x, dim=0): """PyTorch softmax implementation.""" return torch.softmax(x, dim=dim) @R.function def mixed_computation(x: R.Tensor((10,), "float32")) -> R.Tensor((10,), "float32"): relu_result = R.call_py_func("torch_relu", (x,), out_ty=R.Tensor((10,), "float32")) final_result = R.call_py_func( "torch_softmax", (relu_result,), out_ty=R.Tensor((10,), "float32") ) return final_result device = tvm.cpu() module = RelaxCallPyFuncModule(device) x = torch.randn(10, dtype=torch.float32) expected = torch.softmax(torch.relu(x), dim=0) relu_result = module.call_py_func("torch_relu", [x]) final_result = module.call_py_func("torch_softmax", [relu_result]) # Convert to numpy for comparison if isinstance(final_result, tvm.runtime.Tensor): final_result_np = final_result.numpy() else: final_result_np = final_result if isinstance(expected, torch.Tensor): expected_np = expected.numpy() else: expected_np = expected # Use numpy for comparison since we have numpy arrays tvm.testing.assert_allclose(final_result_np, expected_np, rtol=1e-5, atol=1e-5) if __name__ == "__main__": tvm.testing.main()