forked from ccf-ai-infra/TileOPs-Metax
249 lines
8.2 KiB
Python
249 lines
8.2 KiB
Python
"""Tests for elementwise kernel caching and autotune_configs.
|
|
|
|
Validates:
|
|
- UnaryKernel, FusedGatedKernel, and custom kernels cache compiled functions
|
|
after init_config (no per-forward recompilation).
|
|
- autotune_configs is defined for UnaryKernel and FusedGatedKernel with >= 3 configs.
|
|
- Custom kernels (LeakyRelu, Elu, etc.) also cache compiled functions.
|
|
- Serialization-fallback autotune works for UnaryKernel and FusedGatedKernel.
|
|
"""
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
from tileops.kernels.elementwise import (
|
|
AbsFwdKernel,
|
|
# Concrete binary
|
|
AddFwdKernel,
|
|
AlibiFwdKernel,
|
|
ClampFwdKernel,
|
|
EluFwdKernel,
|
|
GeluAndMulFwdKernel,
|
|
GeluTanhAndMulFwdKernel,
|
|
HardtanhFwdKernel,
|
|
# Custom kernels
|
|
LeakyReluFwdKernel,
|
|
MaskedFillFwdKernel,
|
|
NanToNumFwdKernel,
|
|
PreluFwdKernel,
|
|
# Concrete unary
|
|
ReluFwdKernel,
|
|
SigmoidFwdKernel,
|
|
# Concrete fused gated
|
|
SiluAndMulFwdKernel,
|
|
SinusoidalFwdKernel,
|
|
SoftplusFwdKernel,
|
|
# Base classes
|
|
WhereFwdKernel,
|
|
)
|
|
|
|
N = 2048 # small enough for fast tests
|
|
|
|
|
|
# 1. UnaryKernel caching: _compiled_fn exists after init
|
|
|
|
|
|
class TestUnaryCaching:
|
|
"""UnaryKernel subclasses should have _compiled_fn after __init__."""
|
|
|
|
@pytest.mark.full
|
|
@pytest.mark.parametrize("kernel_cls", [ReluFwdKernel, SigmoidFwdKernel, AbsFwdKernel])
|
|
def test_unary_has_compiled_fn(self, kernel_cls):
|
|
k = kernel_cls(N, torch.float16)
|
|
assert hasattr(k, "_compiled_fn"), (
|
|
f"{kernel_cls.__name__} missing _compiled_fn after init"
|
|
)
|
|
assert k._compiled_fn is not None
|
|
|
|
@pytest.mark.full
|
|
def test_unary_forward_uses_cached_fn(self):
|
|
"""forward() should use _compiled_fn, not re-lookup the kernel."""
|
|
k = ReluFwdKernel(N, torch.float16)
|
|
fn1 = k._compiled_fn
|
|
x = torch.randn(N, dtype=torch.float16, device="cuda")
|
|
_ = k(x)
|
|
# _compiled_fn should not change after forward
|
|
assert k._compiled_fn is fn1
|
|
|
|
@pytest.mark.full
|
|
def test_unary_direct_strategy_caching(self):
|
|
"""Direct strategy kernels should also cache _compiled_fn."""
|
|
k = ReluFwdKernel(N, torch.float16, config={"strategy": "direct"})
|
|
assert hasattr(k, "_compiled_fn")
|
|
assert k._compiled_fn is not None
|
|
|
|
|
|
# 2. FusedGatedKernel caching: _compiled_fn exists after init
|
|
|
|
|
|
class TestFusedGatedCaching:
|
|
"""FusedGatedKernel subclasses should have _compiled_fn after __init__."""
|
|
|
|
@pytest.mark.full
|
|
@pytest.mark.parametrize("kernel_cls", [
|
|
SiluAndMulFwdKernel, GeluAndMulFwdKernel, GeluTanhAndMulFwdKernel,
|
|
])
|
|
def test_fused_gated_has_compiled_fn(self, kernel_cls):
|
|
k = kernel_cls(32, 64, torch.float16)
|
|
assert hasattr(k, "_compiled_fn"), (
|
|
f"{kernel_cls.__name__} missing _compiled_fn after init"
|
|
)
|
|
assert k._compiled_fn is not None
|
|
|
|
@pytest.mark.full
|
|
def test_fused_gated_forward_uses_cached_fn(self):
|
|
k = SiluAndMulFwdKernel(32, 64, torch.float16)
|
|
fn1 = k._compiled_fn
|
|
x = torch.randn(32, 128, dtype=torch.float16, device="cuda")
|
|
_ = k(x)
|
|
assert k._compiled_fn is fn1
|
|
|
|
|
|
# 3. Custom kernel caching: _compiled_fn exists after init
|
|
|
|
|
|
class TestCustomKernelCaching:
|
|
"""Custom (non-template) kernels should also cache _compiled_fn."""
|
|
|
|
@pytest.mark.full
|
|
def test_leaky_relu_caching(self):
|
|
k = LeakyReluFwdKernel(N, torch.float16)
|
|
assert hasattr(k, "_compiled_fn")
|
|
assert k._compiled_fn is not None
|
|
|
|
@pytest.mark.full
|
|
def test_elu_caching(self):
|
|
k = EluFwdKernel(N, torch.float16)
|
|
assert hasattr(k, "_compiled_fn")
|
|
assert k._compiled_fn is not None
|
|
|
|
@pytest.mark.full
|
|
def test_hardtanh_caching(self):
|
|
k = HardtanhFwdKernel(N, torch.float16)
|
|
assert hasattr(k, "_compiled_fn")
|
|
assert k._compiled_fn is not None
|
|
|
|
@pytest.mark.full
|
|
def test_softplus_caching(self):
|
|
k = SoftplusFwdKernel(N, torch.float16)
|
|
assert hasattr(k, "_compiled_fn")
|
|
assert k._compiled_fn is not None
|
|
|
|
@pytest.mark.full
|
|
def test_prelu_caching(self):
|
|
k = PreluFwdKernel(N, 4, 512, torch.float16)
|
|
assert hasattr(k, "_compiled_fn")
|
|
assert k._compiled_fn is not None
|
|
|
|
@pytest.mark.full
|
|
def test_where_caching(self):
|
|
k = WhereFwdKernel(N, torch.float16)
|
|
assert hasattr(k, "_compiled_fn")
|
|
assert k._compiled_fn is not None
|
|
|
|
@pytest.mark.full
|
|
def test_clamp_caching(self):
|
|
k = ClampFwdKernel(N, torch.float16, min_val=-1.0, max_val=1.0)
|
|
assert hasattr(k, "_compiled_fn")
|
|
assert k._compiled_fn is not None
|
|
|
|
@pytest.mark.full
|
|
def test_masked_fill_caching(self):
|
|
k = MaskedFillFwdKernel(N, torch.float16, fill_value=0.0)
|
|
assert hasattr(k, "_compiled_fn")
|
|
assert k._compiled_fn is not None
|
|
|
|
@pytest.mark.full
|
|
def test_nan_to_num_caching(self):
|
|
k = NanToNumFwdKernel(N, torch.float16)
|
|
assert hasattr(k, "_compiled_fn")
|
|
assert k._compiled_fn is not None
|
|
|
|
@pytest.mark.full
|
|
def test_alibi_caching(self):
|
|
k = AlibiFwdKernel(32, 4, torch.float16)
|
|
assert hasattr(k, "_compiled_fn")
|
|
assert k._compiled_fn is not None
|
|
|
|
@pytest.mark.full
|
|
def test_sinusoidal_caching(self):
|
|
k = SinusoidalFwdKernel(32, 64, torch.float16)
|
|
assert hasattr(k, "_compiled_fn")
|
|
assert k._compiled_fn is not None
|
|
|
|
|
|
# 4. autotune_configs defined with >= 3 configs
|
|
|
|
|
|
class TestAutotuneConfigs:
|
|
"""UnaryKernel and FusedGatedKernel must define autotune_configs."""
|
|
|
|
@pytest.mark.full
|
|
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32])
|
|
def test_unary_autotune_configs_count(self, dtype):
|
|
k = ReluFwdKernel(N, dtype)
|
|
configs = k.autotune_configs
|
|
assert configs is not None, "UnaryKernel.autotune_configs should not be None"
|
|
assert len(configs) >= 3, f"Expected >= 3 configs, got {len(configs)}"
|
|
# Each config must have threads and num_per_thread keys
|
|
for c in configs:
|
|
assert "threads" in c
|
|
assert "num_per_thread" in c
|
|
|
|
@pytest.mark.full
|
|
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32])
|
|
def test_fused_gated_autotune_configs_count(self, dtype):
|
|
k = SiluAndMulFwdKernel(32, 64, dtype)
|
|
configs = k.autotune_configs
|
|
assert configs is not None, "FusedGatedKernel.autotune_configs should not be None"
|
|
assert len(configs) >= 3, f"Expected >= 3 configs, got {len(configs)}"
|
|
for c in configs:
|
|
assert "threads" in c
|
|
assert "num_per_thread" in c
|
|
|
|
@pytest.mark.full
|
|
def test_binary_autotune_configs_still_works(self):
|
|
"""BinaryKernel autotune_configs must still work (no regression)."""
|
|
k = AddFwdKernel(N, torch.float16, (N,), (1,), (1,), N, N)
|
|
configs = k.autotune_configs
|
|
assert configs is not None
|
|
assert len(configs) >= 3
|
|
|
|
|
|
# 5. Correctness: caching does not change results
|
|
|
|
|
|
class TestCachingCorrectness:
|
|
"""Verify that caching produces the same results as before."""
|
|
|
|
@pytest.mark.full
|
|
def test_unary_relu_correctness(self):
|
|
k = ReluFwdKernel(N, torch.float16)
|
|
x = torch.randn(N, dtype=torch.float16, device="cuda")
|
|
out = k(x)
|
|
ref = torch.relu(x.float()).to(torch.float16)
|
|
torch.testing.assert_close(out, ref, atol=1e-3, rtol=1e-3)
|
|
|
|
@pytest.mark.full
|
|
def test_fused_gated_silu_correctness(self):
|
|
M, Nhalf = 32, 64
|
|
k = SiluAndMulFwdKernel(M, Nhalf, torch.float16)
|
|
x = torch.randn(M, 2 * Nhalf, dtype=torch.float16, device="cuda")
|
|
out = k(x)
|
|
gate = x[:, :Nhalf].float()
|
|
value = x[:, Nhalf:].float()
|
|
ref = (torch.nn.functional.silu(gate) * value).to(torch.float16)
|
|
torch.testing.assert_close(out, ref, atol=1e-2, rtol=1e-2)
|
|
|
|
@pytest.mark.full
|
|
def test_custom_leaky_relu_correctness(self):
|
|
k = LeakyReluFwdKernel(N, torch.float16, negative_slope=0.01)
|
|
x = torch.randn(N, dtype=torch.float16, device="cuda")
|
|
out = k(x)
|
|
ref = torch.nn.functional.leaky_relu(x.float(), 0.01).to(torch.float16)
|
|
torch.testing.assert_close(out, ref, atol=1e-3, rtol=1e-3)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-vvs"])
|