Commit 559e5fe2 authored by wooway777's avatar wooway777 Committed by MaYuhang
Browse files

issue/573 - supporting multiple outputs in test framework

parent 10cfd2b0
...@@ -181,7 +181,10 @@ pip install . -e ...@@ -181,7 +181,10 @@ pip install . -e
#### 运行 InfiniCore Python算子接口测试 #### 运行 InfiniCore Python算子接口测试
```bash ```bash
python test/infinicore/run.py --verbose --bench [--cpu | --nvidia | --cambricon | --ascend | --iluvatar | --metax | --moore | --kunlun] # 测试单算子
python test/infinicore/ops/[operator].py [--cpu | --nvidia | --cambricon | --ascend | --iluvatar | --metax | --moore | --kunlun | --Hygon]
# 测试全部算子
python test/infinicore/run.py [--cpu | --nvidia | --cambricon | --ascend | --iluvatar | --metax | --moore | --kunlun]
``` ```
使用 -h 查看更多参数。 使用 -h 查看更多参数。
......
...@@ -9,14 +9,10 @@ from .utils import ( ...@@ -9,14 +9,10 @@ from .utils import (
profile_operation, profile_operation,
rearrange_tensor, rearrange_tensor,
convert_infinicore_to_torch, convert_infinicore_to_torch,
get_operator_help_info,
print_operator_testing_tips,
) )
from .config import ( from .config import (
get_args, get_args,
get_hardware_args_group, get_hardware_args_group,
get_hardware_help_text,
get_supported_hardware_platforms,
get_test_devices, get_test_devices,
) )
from .devices import InfiniDeviceEnum, InfiniDeviceNames, torch_device_map from .devices import InfiniDeviceEnum, InfiniDeviceNames, torch_device_map
...@@ -41,13 +37,9 @@ __all__ = [ ...@@ -41,13 +37,9 @@ __all__ = [
"debug", "debug",
"get_args", "get_args",
"get_hardware_args_group", "get_hardware_args_group",
"get_hardware_help_text",
"get_operator_help_info",
"get_supported_hardware_platforms",
"get_test_devices", "get_test_devices",
"get_tolerance", "get_tolerance",
"infinicore_tensor_from_torch", "infinicore_tensor_from_torch",
"print_operator_testing_tips",
"profile_operation", "profile_operation",
"rearrange_tensor", "rearrange_tensor",
# Utility functions # Utility functions
......
...@@ -27,6 +27,8 @@ class TestCase: ...@@ -27,6 +27,8 @@ class TestCase:
comparison_target=None, comparison_target=None,
description="", description="",
tolerance=None, tolerance=None,
output_count=1,
output_specs=None,
): ):
""" """
Initialize a test case with complete configuration Initialize a test case with complete configuration
...@@ -34,10 +36,12 @@ class TestCase: ...@@ -34,10 +36,12 @@ class TestCase:
Args: Args:
inputs: List of TensorSpec objects or scalars inputs: List of TensorSpec objects or scalars
kwargs: Additional keyword arguments for the operator kwargs: Additional keyword arguments for the operator
output_spec: TensorSpec for output tensor (for in-place operations) output_spec: TensorSpec for output tensor (for single output operations)
output_specs: List of TensorSpec for multiple output tensors
comparison_target: Target for comparison ('out', index, or None for return value) comparison_target: Target for comparison ('out', index, or None for return value)
description: Test case description description: Test case description
tolerance: Tolerance settings for this test case {'atol': float, 'rtol': float} tolerance: Tolerance settings for this test case {'atol': float, 'rtol': float}
output_count: Number of outputs (default: 1)
""" """
self.inputs = [] self.inputs = []
...@@ -52,9 +56,26 @@ class TestCase: ...@@ -52,9 +56,26 @@ class TestCase:
self.kwargs = kwargs or {} self.kwargs = kwargs or {}
self.output_spec = output_spec self.output_spec = output_spec
self.output_specs = output_specs
self.comparison_target = comparison_target self.comparison_target = comparison_target
self.description = description self.description = description
self.tolerance = tolerance or {"atol": 1e-5, "rtol": 1e-3} self.tolerance = tolerance or {"atol": 1e-5, "rtol": 1e-3}
self.output_count = output_count
# Validate output configuration
if self.output_count == 1:
if self.output_specs is not None:
raise ValueError("output_specs cannot be used when output_count=1")
else:
if self.output_spec is not None:
raise ValueError("output_spec cannot be used when output_count>1")
if (
self.output_specs is not None
and len(self.output_specs) != self.output_count
):
raise ValueError(
f"output_specs count ({len(self.output_specs)}) must match output_count ({self.output_count})"
)
def get_tensor_input_count(self): def get_tensor_input_count(self):
"""Count the number of tensor inputs (excluding scalars)""" """Count the number of tensor inputs (excluding scalars)"""
...@@ -92,34 +113,56 @@ class TestCase: ...@@ -92,34 +113,56 @@ class TestCase:
base_str += f"{self.description}" base_str += f"{self.description}"
base_str += f" - inputs=[{', '.join(input_strs)}]" base_str += f" - inputs=[{', '.join(input_strs)}]"
if self.kwargs or self.output_spec: if self.kwargs or self.output_spec or self.output_specs:
kwargs_strs = [] kwargs_strs = []
for key, value in self.kwargs.items(): for key, value in self.kwargs.items():
if key == "out" and isinstance(value, int): if key == "out" and isinstance(value, int):
kwargs_strs.append(f"{key}={value}") kwargs_strs.append(f"{key}={value}")
else: else:
kwargs_strs.append(f"{key}={value}") kwargs_strs.append(f"{key}={value}")
output_spec = self.output_spec
if output_spec and isinstance(output_spec, TensorSpec): # Handle output specifications
dtype_str = f", {output_spec.dtype}" if output_spec.dtype else "" if self.output_count == 1 and self.output_spec:
dtype_str = (
f", {self.output_spec.dtype}" if self.output_spec.dtype else ""
)
init_str = ( init_str = (
f", init={output_spec.init_mode}" f", init={self.output_spec.init_mode}"
if output_spec.init_mode != TensorInitializer.RANDOM if self.output_spec.init_mode != TensorInitializer.RANDOM
else "" else ""
) )
if hasattr(output_spec, "strides") and output_spec.strides: if hasattr(self.output_spec, "strides") and self.output_spec.strides:
strides_str = f", strides={output_spec.strides}" strides_str = f", strides={self.output_spec.strides}"
kwargs_strs.append( kwargs_strs.append(
f"out=tensor{output_spec.shape}{strides_str}{dtype_str}{init_str}" f"out=tensor{self.output_spec.shape}{strides_str}{dtype_str}{init_str}"
) )
else: else:
kwargs_strs.append( kwargs_strs.append(
f"out=tensor{output_spec.shape}{dtype_str}{init_str}" f"out=tensor{self.output_spec.shape}{dtype_str}{init_str}"
) )
elif self.output_count > 1 and self.output_specs:
output_strs = []
for i, spec in enumerate(self.output_specs):
dtype_str = f", {spec.dtype}" if spec.dtype else ""
init_str = (
f", init={spec.init_mode}"
if spec.init_mode != TensorInitializer.RANDOM
else ""
)
if hasattr(spec, "strides") and spec.strides:
strides_str = f", strides={spec.strides}"
output_strs.append(
f"out_{i}=tensor{spec.shape}{strides_str}{dtype_str}{init_str}"
)
else:
output_strs.append(
f"out_{i}=tensor{spec.shape}{dtype_str}{init_str}"
)
kwargs_strs.extend(output_strs)
base_str += f", kwargs={{{', '.join(kwargs_strs)}}}" base_str += f", kwargs={{{', '.join(kwargs_strs)}}}"
base_str += ")" base_str += f", outputs={self.output_count})"
return base_str return base_str
...@@ -209,10 +252,20 @@ class BaseOperatorTest(ABC): ...@@ -209,10 +252,20 @@ class BaseOperatorTest(ABC):
else: else:
inputs.append(input_spec) inputs.append(input_spec)
# Prepare output tensor if specified in output_spec # Prepare output tensors based on output_count
if test_case.output_count == 1:
# Single output case
if test_case.output_spec is not None: if test_case.output_spec is not None:
output_tensor = test_case.output_spec.create_torch_tensor(device) output_tensor = test_case.output_spec.create_torch_tensor(device)
kwargs["out"] = output_tensor kwargs["out"] = output_tensor
else:
# Multiple outputs case
if test_case.output_specs is not None:
# Create output tuple for in-place multiple outputs
output_tensors = tuple(
spec.create_torch_tensor(device) for spec in test_case.output_specs
)
kwargs["out"] = output_tensors
# Handle integer indices for in-place operations # Handle integer indices for in-place operations
if "out" in kwargs and isinstance(kwargs["out"], int): if "out" in kwargs and isinstance(kwargs["out"], int):
...@@ -264,13 +317,24 @@ class BaseOperatorTest(ABC): ...@@ -264,13 +317,24 @@ class BaseOperatorTest(ABC):
# Handle infinicore output # Handle infinicore output
infini_kwargs = kwargs.copy() infini_kwargs = kwargs.copy()
if "out" in infini_kwargs and isinstance(infini_kwargs["out"], torch.Tensor): if "out" in infini_kwargs:
out_value = infini_kwargs["out"]
if isinstance(out_value, torch.Tensor):
# Single tensor output
if isinstance(comparison_target, int): if isinstance(comparison_target, int):
infini_kwargs["out"] = infini_inputs[comparison_target] infini_kwargs["out"] = infini_inputs[comparison_target]
else: else:
cloned_out = infini_kwargs["out"].clone().detach() cloned_out = out_value.clone().detach()
torch_input_clones.append(cloned_out) torch_input_clones.append(cloned_out)
infini_kwargs["out"] = infinicore_tensor_from_torch(cloned_out) infini_kwargs["out"] = infinicore_tensor_from_torch(cloned_out)
elif isinstance(out_value, (tuple, list)):
# Multiple tensor outputs
infini_outputs = []
for tensor in out_value:
cloned_tensor = tensor.clone().detach()
torch_input_clones.append(cloned_tensor)
infini_outputs.append(infinicore_tensor_from_torch(cloned_tensor))
infini_kwargs["out"] = tuple(infini_outputs)
# Check operator implementations # Check operator implementations
torch_implemented = True torch_implemented = True
...@@ -307,32 +371,85 @@ class BaseOperatorTest(ABC): ...@@ -307,32 +371,85 @@ class BaseOperatorTest(ABC):
) )
if config.bench: if config.bench:
if torch_implemented: self._run_benchmarking(
config,
device_str,
torch_implemented,
infini_implemented,
inputs,
kwargs,
infini_inputs,
infini_kwargs,
test_case.output_count,
comparison_target,
)
return
def torch_op(): # ==========================================================================
return self.torch_operator(*inputs, **kwargs) # MULTIPLE OUTPUTS COMPARISON LOGIC
# ==========================================================================
if test_case.output_count > 1:
# Handle multiple outputs comparison
profile_operation( # Determine what to compare based on comparison_target
"PyTorch ", if comparison_target is None:
torch_op, # Compare return values (out-of-place multiple outputs)
device_str, torch_comparison = torch_result
config.num_prerun, infini_comparison = infini_result
config.num_iterations, elif comparison_target == "out":
# Compare output tuple from kwargs (explicit multiple outputs)
torch_comparison = kwargs.get("out")
infini_comparison = infini_kwargs.get("out")
else:
raise ValueError(
f"Invalid comparison target for multiple outputs: {comparison_target}"
) )
if infini_implemented:
def infini_op(): # Validate that we have multiple outputs to compare
return self.infinicore_operator(*infini_inputs, **infini_kwargs) if not isinstance(torch_comparison, (tuple, list)) or not isinstance(
infini_comparison, (tuple, list)
):
raise ValueError(
f"Multiple outputs expected but got single result: "
f"torch={type(torch_comparison)}, infinicore={type(infini_comparison)}"
)
profile_operation( if len(torch_comparison) != len(infini_comparison):
"InfiniCore", raise ValueError(
infini_op, f"Output count mismatch: torch={len(torch_comparison)}, infinicore={len(infini_comparison)}"
device_str, )
config.num_prerun,
config.num_iterations, if len(torch_comparison) != test_case.output_count:
raise ValueError(
f"Output count mismatch: expected {test_case.output_count}, got {len(torch_comparison)}"
) )
return
# Compare each output pair individually
all_valid = True
for i, (torch_out, infini_out) in enumerate(
zip(torch_comparison, infini_comparison)
):
atol = test_case.tolerance.get("atol", 1e-5)
rtol = test_case.tolerance.get("rtol", 1e-3)
compare_fn = create_test_comparator(
config, atol, rtol, f"{test_case.description} - output_{i}"
)
is_valid = compare_fn(infini_out, torch_out)
if not is_valid:
print(f"❌ Output {i} comparison failed")
all_valid = False
else:
print(f"✅ Output {i} comparison passed")
assert all_valid, f"Multiple outputs comparison failed for {test_case}"
# ==========================================================================
# SINGLE OUTPUT COMPARISON LOGIC
# ==========================================================================
else:
# Determine comparison targets for single output
if comparison_target is None: if comparison_target is None:
# Compare return values (out-of-place) # Compare return values (out-of-place)
torch_comparison = torch_result torch_comparison = torch_result
...@@ -343,7 +460,6 @@ class BaseOperatorTest(ABC): ...@@ -343,7 +460,6 @@ class BaseOperatorTest(ABC):
infini_comparison = infini_kwargs.get("out") infini_comparison = infini_kwargs.get("out")
elif isinstance(comparison_target, int): elif isinstance(comparison_target, int):
# Compare specific input tensor (in-place operation on input) # Compare specific input tensor (in-place operation on input)
# For in-place operations, we compare the modified input tensor
if 0 <= comparison_target < len(inputs): if 0 <= comparison_target < len(inputs):
torch_comparison = inputs[comparison_target] torch_comparison = inputs[comparison_target]
infini_comparison = infini_inputs[comparison_target] infini_comparison = infini_inputs[comparison_target]
...@@ -362,21 +478,58 @@ class BaseOperatorTest(ABC): ...@@ -362,21 +478,58 @@ class BaseOperatorTest(ABC):
atol = test_case.tolerance.get("atol", 1e-5) atol = test_case.tolerance.get("atol", 1e-5)
rtol = test_case.tolerance.get("rtol", 1e-3) rtol = test_case.tolerance.get("rtol", 1e-3)
compare_fn = create_test_comparator(config, atol, rtol, test_case.description) compare_fn = create_test_comparator(
config, atol, rtol, test_case.description
)
is_valid = compare_fn(infini_comparison, torch_comparison) is_valid = compare_fn(infini_comparison, torch_comparison)
assert is_valid, f"Result comparison failed for {test_case}" assert is_valid, f"Result comparison failed for {test_case}"
# Benchmarking # ==========================================================================
# UNIFIED BENCHMARKING LOGIC
# ==========================================================================
if config.bench: if config.bench:
self._run_benchmarking(
config,
device_str,
True,
True,
inputs,
kwargs,
infini_inputs,
infini_kwargs,
test_case.output_count,
comparison_target,
)
def _run_benchmarking(
self,
config,
device_str,
torch_implemented,
infini_implemented,
inputs,
kwargs,
infini_inputs,
infini_kwargs,
output_count,
comparison_target,
):
"""
Unified benchmarking logic
"""
if torch_implemented:
if output_count > 1:
# For multiple outputs, just call the operator
def torch_op():
return self.torch_operator(*inputs, **kwargs)
else:
if comparison_target is None: if comparison_target is None:
# Out-of-place benchmarking # Out-of-place benchmarking
def torch_op(): def torch_op():
return self.torch_operator(*inputs, **kwargs) return self.torch_operator(*inputs, **kwargs)
def infini_op():
return self.infinicore_operator(*infini_inputs, **infini_kwargs)
else: else:
# In-place benchmarking # In-place benchmarking
def torch_op(): def torch_op():
...@@ -387,6 +540,22 @@ class BaseOperatorTest(ABC): ...@@ -387,6 +540,22 @@ class BaseOperatorTest(ABC):
else inputs[comparison_target] else inputs[comparison_target]
) )
profile_operation(
"PyTorch ",
torch_op,
device_str,
config.num_prerun,
config.num_iterations,
)
if infini_implemented:
if comparison_target is None:
# Out-of-place benchmarking
def infini_op():
return self.infinicore_operator(*infini_inputs, **infini_kwargs)
else:
# In-place benchmarking
def infini_op(): def infini_op():
self.infinicore_operator(*infini_inputs, **infini_kwargs) self.infinicore_operator(*infini_inputs, **infini_kwargs)
return ( return (
...@@ -395,13 +564,6 @@ class BaseOperatorTest(ABC): ...@@ -395,13 +564,6 @@ class BaseOperatorTest(ABC):
else infini_inputs[comparison_target] else infini_inputs[comparison_target]
) )
profile_operation(
"PyTorch ",
torch_op,
device_str,
config.num_prerun,
config.num_iterations,
)
profile_operation( profile_operation(
"InfiniCore", "InfiniCore",
infini_op, infini_op,
......
...@@ -27,24 +27,6 @@ def get_supported_hardware_platforms(): ...@@ -27,24 +27,6 @@ def get_supported_hardware_platforms():
] ]
def get_hardware_help_text():
"""
Get formatted help text for hardware platforms.
Returns:
str: Formatted help text for argument parsers
"""
platforms = get_supported_hardware_platforms()
help_lines = ["Supported Hardware Platforms:"]
for flag, description in platforms:
# Remove leading dashes for cleaner display
name = flag.lstrip("-")
help_lines.append(f" - {name.upper():<10} {description}")
return "\n".join(help_lines)
def get_hardware_args_group(parser): def get_hardware_args_group(parser):
""" """
Add hardware platform arguments to an argument parser. Add hardware platform arguments to an argument parser.
...@@ -82,7 +64,6 @@ Examples: ...@@ -82,7 +64,6 @@ Examples:
# Run performance profiling with custom iterations # Run performance profiling with custom iterations
python test_operator.py --nvidia --bench --num_prerun 50 --num_iterations 5000 python test_operator.py --nvidia --bench --num_prerun 50 --num_iterations 5000
{get_hardware_help_text()}
""", """,
) )
......
import torch import torch
import time import time
import infinicore import infinicore
import numpy as np
from .datatypes import to_infinicore_dtype, to_torch_dtype from .datatypes import to_infinicore_dtype, to_torch_dtype
def get_operator_help_info():
"""
Get help information for operator testing framework
Returns:
str: Comprehensive help information about the testing framework
"""
return """
InfiniCore Operator Testing Framework
This framework provides comprehensive testing for InfiniCore operators across
multiple hardware platforms with the following features:
Key Features:
-------------
1. Multi-platform Support: CPU, NVIDIA, Cambricon, Ascend, Iluvatar, Metax,
Moore, Kunlun, and Hygon devices
2. Flexible Testing: Out-of-place and in-place operations
3. Performance Benchmarking: Accurate timing with warm-up runs
4. Debug Capabilities: Detailed tensor comparison and discrepancy analysis
5. Tolerance Control: Configurable absolute and relative tolerances per data type
Usage Patterns:
--------------
Basic testing:
python test_operator.py --cpu --nvidia
With benchmarking:
python test_operator.py --nvidia --bench --num_iterations 1000
Debug mode:
python test_operator.py --cpu --debug
Multiple devices:
python test_operator.py --cpu --nvidia
Data Type Support:
-----------------
- Floating point: float16, bfloat16, float32
- Integer: int8, int16, int32, int64, uint8
- Boolean: bool
Tensor Initialization Modes:
---------------------------
- RANDOM: Random values using torch.rand
- ZEROS: All zeros using torch.zeros
- ONES: All ones using torch.ones
- RANDINT: Random integers using torch.randint
- MANUAL: Use pre-existing tensor with shape/strides validation
- BINARY: Use pre-existing tensor with shape validation only
- FROM_FILE: Load tensor data from file
For detailed examples and advanced usage, refer to the individual operator
test files and the framework documentation.
"""
def print_operator_testing_tips():
"""Print useful tips for operator testing"""
tips = """
Operator Testing Tips:
---------------------
1. Start with CPU tests for basic functionality validation
2. Use --debug flag to identify precision issues in early development
3. Benchmark with sufficient iterations (--num_iterations) for stable results
4. Set appropriate tolerances for different data types (float16 needs higher tolerance)
5. Test both contiguous and non-contiguous tensor layouts
6. Validate in-place operations separately from out-of-place operations
7. Check edge cases: empty tensors, broadcasting, different tensor shapes
Common Tolerance Settings:
-------------------------
- float32: atol=1e-5, rtol=1e-3
- float16: atol=1e-3, rtol=1e-2
- bfloat16: atol=1e-2, rtol=1e-1
- Integer types: exact equality (atol=0, rtol=0)
"""
print(tips)
def synchronize_device(torch_device): def synchronize_device(torch_device):
"""Device synchronization""" """Device synchronization"""
if torch_device == "cuda": if torch_device == "cuda":
...@@ -235,13 +156,12 @@ def infinicore_tensor_from_torch(torch_tensor): ...@@ -235,13 +156,12 @@ def infinicore_tensor_from_torch(torch_tensor):
) )
def convert_infinicore_to_torch(infini_result, torch_reference): def convert_infinicore_to_torch(infini_result):
""" """
Convert infinicore tensor to PyTorch tensor for comparison Convert infinicore tensor to PyTorch tensor for comparison
Args: Args:
infini_result: infinicore tensor result infini_result: infinicore tensor result
torch_reference: PyTorch tensor reference (for shape and device)
dtype: infinicore data type dtype: infinicore data type
device_str: torch device string device_str: torch device string
...@@ -249,7 +169,7 @@ def convert_infinicore_to_torch(infini_result, torch_reference): ...@@ -249,7 +169,7 @@ def convert_infinicore_to_torch(infini_result, torch_reference):
torch.Tensor: PyTorch tensor with infinicore data torch.Tensor: PyTorch tensor with infinicore data
""" """
torch_result_from_infini = torch.zeros( torch_result_from_infini = torch.zeros(
torch_reference.shape, infini_result.shape,
dtype=to_torch_dtype(infini_result.dtype), dtype=to_torch_dtype(infini_result.dtype),
device=infini_result.device.type, device=infini_result.device.type,
) )
...@@ -263,38 +183,68 @@ def compare_results( ...@@ -263,38 +183,68 @@ def compare_results(
): ):
""" """
Generic function to compare infinicore result with PyTorch reference result Generic function to compare infinicore result with PyTorch reference result
Supports both floating-point (with tolerance) and integer (exact) comparison Supports both single and multiple outputs
Args: Args:
infini_result: infinicore tensor result infini_result: infinicore tensor result (single or tuple)
torch_result: PyTorch tensor reference result torch_result: PyTorch tensor reference result (single or tuple)
atol: absolute tolerance (for floating-point only) atol: absolute tolerance (for floating-point only)
rtol: relative tolerance (for floating-point only) rtol: relative tolerance (for floating-point only)
debug_mode: whether to enable debug output debug_mode: whether to enable debug output
Returns: Returns:
bool: True if results match within tolerance (FP) or exactly (integer) bool: True if all results match within tolerance
""" """
# Convert infinicore result to PyTorch tensor for comparison # Handle multiple outputs
torch_result_from_infini = convert_infinicore_to_torch(infini_result, torch_result) if isinstance(infini_result, (tuple, list)) and isinstance(
torch_result, (tuple, list)
# Handle scalar integer comparison
if isinstance(torch_result_from_infini, (int, float)) and isinstance(
torch_result, (int, float)
): ):
if isinstance(torch_result_from_infini, int) and isinstance(torch_result, int): if len(infini_result) != len(torch_result):
return False
all_match = True
for i, (infini_out, torch_out) in enumerate(zip(infini_result, torch_result)):
match = compare_results(infini_out, torch_out, atol, rtol, debug_mode)
all_match = all_match and match
return all_match
# Handle scalar and bool comparisons
if not isinstance(torch_result, torch.Tensor):
is_infini_int = isinstance(infini_result, (int, np.integer))
is_torch_int = isinstance(torch_result, (int, np.integer))
if isinstance(infini_result, bool) and isinstance(torch_result, bool):
# Bool comparison
result_equal = infini_result == torch_result
if debug_mode:
status = "match" if result_equal else "mismatch"
print(
f"Boolean values {status}: {infini_result} {'==' if result_equal else '!='} {torch_result}"
)
return result_equal
elif is_infini_int and is_torch_int:
# Exact integer scalar comparison # Exact integer scalar comparison
result_equal = torch_result_from_infini == torch_result result_equal = infini_result == torch_result
if debug_mode and not result_equal: if debug_mode:
status = "match" if result_equal else "mismatch"
print( print(
f"Integer scalar mismatch: {torch_result_from_infini} != {torch_result}" f"Integer scalar {status}: {infini_result} {'==' if result_equal else '!='} {torch_result}"
) )
return result_equal return result_equal
else: else:
# Floating-point scalar comparison with tolerance # Floating-point scalar comparison with tolerance
return abs(torch_result_from_infini - torch_result) <= atol + rtol * abs( result_equal = abs(infini_result - torch_result) <= atol + rtol * abs(
torch_result torch_result
) )
if debug_mode:
status = "match" if result_equal else "mismatch"
print(
f"Floating-point scalar {status}: {infini_result} {'~=' if result_equal else '!~='} {torch_result} (tolerance: {atol + rtol * abs(torch_result)})"
)
return result_equal
# Convert infinicore result to PyTorch tensor for comparison
torch_result_from_infini = convert_infinicore_to_torch(infini_result)
# Debug mode: detailed comparison # Debug mode: detailed comparison
if debug_mode: if debug_mode:
......
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
import torch
import infinicore
from framework.base import BaseOperatorTest, TensorSpec, TestCase
from framework.runner import GenericTestRunner
from framework.utils import is_broadcast
# ==============================================================================
# Operator-specific configuration for aminmax
# ==============================================================================
# Test cases format: (shape, dim, keepdim, input_strides, min_strides, max_strides)
_TEST_CASES_DATA = [
# Basic cases - out-of-place
((13, 4), None, False, None, None, None),
((13, 4), 0, False, None, None, None),
((13, 4), 1, False, None, None, None),
((13, 4), -1, False, None, None, None),
# With keepdim - out-of-place
((13, 4), None, True, None, None, None),
((13, 4), 0, True, None, None, None),
((13, 4), 1, True, None, None, None),
# 3D cases - out-of-place
((4, 5, 6), None, False, None, None, None),
((4, 5, 6), 1, False, None, None, None),
((4, 5, 6), 1, True, None, None, None),
((4, 5, 6), -1, True, None, None, None),
# Edge cases - out-of-place
((10,), None, False, None, None, None),
((10,), 0, False, None, None, None),
((1, 5), None, False, None, None, None),
# In-place cases with strided tensors
(
(13, 4),
None,
False,
(10, 1),
None,
None,
), # Global min/max - no strides for scalar outputs
((13, 4), 0, False, None, (3,), (3,)),
((13, 4), 1, False, (20, 1), (10,), (10,)),
# 3D in-place cases
((4, 5, 6), 1, True, None, (4, 1, 6), (4, 1, 6)),
((4, 5, 6), -1, False, (30, 6, 1), (4, 5), (4, 5)),
]
# Tolerance configuration
_TOLERANCE_MAP = {
infinicore.float16: {"atol": 1e-3, "rtol": 1e-2},
infinicore.float32: {"atol": 1e-5, "rtol": 1e-4},
infinicore.bfloat16: {"atol": 1e-2, "rtol": 5e-2},
}
# Data types to test
_TENSOR_DTYPES = [infinicore.float16, infinicore.bfloat16, infinicore.float32]
def calculate_output_shape(input_shape, dim, keepdim):
"""
Calculate the output shape for aminmax operation based on input shape, dim, and keepdim
"""
if dim is None:
# Global min/max - output should be scalar tensors
if keepdim:
# When keepdim=True with dim=None, output has same rank but all dimensions are 1
return tuple(1 for _ in input_shape)
else:
# Scalar tensors
return ()
else:
# Reduction along specific dimension
output_shape = list(input_shape)
if keepdim:
output_shape[dim] = 1
else:
output_shape.pop(dim)
return tuple(output_shape)
def parse_test_cases():
"""
Parse aminmax test cases including both out-of-place and in-place variants
aminmax supports: torch.aminmax(input, *, dim=None, keepdim=False, out=(min_tensor, max_tensor))
"""
test_cases = []
for data in _TEST_CASES_DATA:
shape = data[0]
dim = data[1] if len(data) > 1 else None
keepdim = data[2] if len(data) > 2 else False
input_strides = data[3] if len(data) > 3 else None
min_strides = data[4] if len(data) > 4 else None
max_strides = data[5] if len(data) > 5 else None
# Generate test cases for all data types
for dtype in _TENSOR_DTYPES:
tolerance = _TOLERANCE_MAP.get(dtype, {"atol": 1e-5, "rtol": 1e-4})
# Create input tensor spec
input_spec = TensorSpec.from_tensor(shape, input_strides, dtype)
# Build description
description_parts = ["aminmax"]
if dim is not None:
description_parts.append(f"dim={dim}")
if keepdim:
description_parts.append("keepdim=True")
if input_strides is not None:
description_parts.append(f"input_strides={input_strides}")
base_description = " - ".join(description_parts)
# Prepare common kwargs
kwargs = {}
if dim is not None:
kwargs["dim"] = dim
kwargs["keepdim"] = keepdim
# ==================================================================
# Test Case 1: Out-of-place (return values)
# ==================================================================
test_cases.append(
TestCase(
inputs=[input_spec],
kwargs=kwargs,
output_spec=None, # No output spec for return value comparison
comparison_target=None, # Compare return values
tolerance=tolerance,
description=f"{base_description} - OUT_OF_PLACE",
output_count=2, # aminmax returns 2 tensors: (min, max)
)
)
# ==================================================================
# Test Case 2: In-place with explicit output tensors
# ==================================================================
# Only create in-place test cases if we have valid output configurations
# For global min/max (dim=None), we need special handling
if dim is None:
# Global min/max - output shapes are either () or (1,1,...) depending on keepdim
output_shape = calculate_output_shape(shape, dim, keepdim)
# For scalar outputs, we don't use strides (they would be empty tuples)
if output_shape == ():
# Scalar tensors - create without strides
min_spec = TensorSpec.from_tensor(output_shape, None, dtype)
max_spec = TensorSpec.from_tensor(output_shape, None, dtype)
else:
# keepdim=True case - use provided strides or None
min_spec = TensorSpec.from_tensor(output_shape, min_strides, dtype)
max_spec = TensorSpec.from_tensor(output_shape, max_strides, dtype)
# Check if output tensors support in-place operations
min_supports_inplace = not is_broadcast(
getattr(min_spec, "strides", None)
)
max_supports_inplace = not is_broadcast(
getattr(max_spec, "strides", None)
)
if min_supports_inplace and max_supports_inplace:
inplace_kwargs = kwargs.copy()
test_cases.append(
TestCase(
inputs=[input_spec],
kwargs=inplace_kwargs,
output_specs=[
min_spec,
max_spec,
], # Multiple output specs for in-place
comparison_target="out", # Compare the output tuple from kwargs
tolerance=tolerance,
description=f"{base_description} - INPLACE(out)",
output_count=2, # Specify 2 outputs
)
)
else:
# Reduction along specific dimension
if min_strides is not None and max_strides is not None:
output_shape = calculate_output_shape(shape, dim, keepdim)
# Create output tensor specs
min_spec = TensorSpec.from_tensor(output_shape, min_strides, dtype)
max_spec = TensorSpec.from_tensor(output_shape, max_strides, dtype)
# Check if output tensors support in-place operations
min_supports_inplace = not is_broadcast(min_strides)
max_supports_inplace = not is_broadcast(max_strides)
if min_supports_inplace and max_supports_inplace:
inplace_kwargs = kwargs.copy()
test_cases.append(
TestCase(
inputs=[input_spec],
kwargs=inplace_kwargs,
output_specs=[
min_spec,
max_spec,
], # Multiple output specs for in-place
comparison_target="out", # Compare the output tuple from kwargs
tolerance=tolerance,
description=f"{base_description} - INPLACE(out)",
output_count=2, # Specify 2 outputs
)
)
return test_cases
class OpTest(BaseOperatorTest):
"""aminmax operator test with multiple outputs support"""
def __init__(self):
super().__init__("aminmax")
def get_test_cases(self):
return parse_test_cases()
def torch_operator(self, x, dim=None, keepdim=False, out=None, **kwargs):
return torch.aminmax(x, dim=dim, keepdim=keepdim, out=out)
def main():
"""Main entry point"""
runner = GenericTestRunner(OpTest)
runner.run_and_exit()
if __name__ == "__main__":
main()
...@@ -142,21 +142,19 @@ def run_all_op_tests(ops_dir=None, specific_ops=None, extra_args=None): ...@@ -142,21 +142,19 @@ def run_all_op_tests(ops_dir=None, specific_ops=None, extra_args=None):
if extra_args: if extra_args:
cmd.extend(extra_args) cmd.extend(extra_args)
# Run with captured output
result = subprocess.run( result = subprocess.run(
cmd, cmd,
cwd=ops_dir, cwd=ops_dir,
capture_output=True, stdout=None,
text=True, stderr=None,
timeout=300, # 5 minute timeout per test
) )
success = result.returncode == 0 success = result.returncode == 0
results[test_name] = ( results[test_name] = (
success, success,
result.returncode, result.returncode,
result.stdout, "",
result.stderr, "",
) )
# Print the output from the test script # Print the output from the test script
...@@ -173,13 +171,9 @@ def run_all_op_tests(ops_dir=None, specific_ops=None, extra_args=None): ...@@ -173,13 +171,9 @@ def run_all_op_tests(ops_dir=None, specific_ops=None, extra_args=None):
status_icon = "✅" if success else "❌" status_icon = "✅" if success else "❌"
print( print(
f"\n{status_icon} {test_name}: {'PASSED' if success else 'FAILED'} (return code: {result.returncode})" f"{status_icon} {test_name}: {'PASSED' if success else 'FAILED'} (return code: {result.returncode})"
) )
except subprocess.TimeoutExpired:
print(f"⏰ {test_name}: TIMEOUT (exceeded 5 minutes)")
results[test_name] = (False, -2, "", "Test execution timed out")
except Exception as e: except Exception as e:
print(f"💥 {test_name}: ERROR - {str(e)}") print(f"💥 {test_name}: ERROR - {str(e)}")
results[test_name] = (False, -1, "", str(e)) results[test_name] = (False, -1, "", str(e))
...@@ -279,8 +273,8 @@ def generate_help_epilog(ops_dir): ...@@ -279,8 +273,8 @@ def generate_help_epilog(ops_dir):
epilog_parts.append(" # Run all operator tests on CPU") epilog_parts.append(" # Run all operator tests on CPU")
epilog_parts.append(" python run.py --cpu") epilog_parts.append(" python run.py --cpu")
epilog_parts.append("") epilog_parts.append("")
epilog_parts.append(" # Run specific operators with benchmarking") epilog_parts.append(" # Run specific operators")
epilog_parts.append(" python run.py --ops add matmul --nvidia --bench") epilog_parts.append(" python run.py --ops add matmul --nvidia")
epilog_parts.append("") epilog_parts.append("")
epilog_parts.append(" # Run with debug mode on multiple devices") epilog_parts.append(" # Run with debug mode on multiple devices")
epilog_parts.append(" python run.py --cpu --nvidia --debug") epilog_parts.append(" python run.py --cpu --nvidia --debug")
...@@ -288,11 +282,6 @@ def generate_help_epilog(ops_dir): ...@@ -288,11 +282,6 @@ def generate_help_epilog(ops_dir):
epilog_parts.append(" # List available tests without running") epilog_parts.append(" # List available tests without running")
epilog_parts.append(" python run.py --list") epilog_parts.append(" python run.py --list")
epilog_parts.append("") epilog_parts.append("")
epilog_parts.append(" # Run with custom performance settings")
epilog_parts.append(
" python run.py --nvidia --bench --num_prerun 50 --num_iterations 5000"
)
epilog_parts.append("")
# Available operators section # Available operators section
if operators: if operators:
...@@ -315,6 +304,9 @@ def generate_help_epilog(ops_dir): ...@@ -315,6 +304,9 @@ def generate_help_epilog(ops_dir):
epilog_parts.append( epilog_parts.append(
" - Operators are automatically discovered from the ops directory" " - Operators are automatically discovered from the ops directory"
) )
epilog_parts.append(
" - --bench option is disabled in batch mode (run individual tests for benchmarking)"
)
return "\n".join(epilog_parts) return "\n".join(epilog_parts)
...@@ -356,6 +348,15 @@ def main(): ...@@ -356,6 +348,15 @@ def main():
list_available_tests(args.ops_dir) list_available_tests(args.ops_dir)
return return
# Check for --bench option in extra arguments
for arg in unknown_args:
if arg in ["--bench"]:
print("❌ ERROR: --bench option is not allowed in batch testing mode.")
print("")
print("Solution: Run individual test scripts for benchmarking:")
print(" python path/to/individual_test.py --bench --<platform>")
sys.exit(1)
# Auto-detect ops directory if not provided # Auto-detect ops directory if not provided
if args.ops_dir is None: if args.ops_dir is None:
ops_dir = find_ops_directory() ops_dir = find_ops_directory()
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment