809 lines
27 KiB
Python
809 lines
27 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, 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()
|