Files
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

205 lines
7.5 KiB
Python

from __future__ import annotations
import pytest
import torch
from tokenspeed_kernel.ops.activation.triton import (
fused_gate_sigmoid_mul_add,
sigmoid_mul,
silu_and_mul,
)
from tokenspeed_kernel.platform import current_platform
platform = current_platform()
torch.manual_seed(42)
pytestmark = pytest.mark.skipif(
not (platform.is_nvidia or platform.is_amd),
reason="Triton activation tests require an NVIDIA or AMD GPU.",
)
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16, torch.float32])
@pytest.mark.parametrize(
"shape",
# Qwen3.5 attn_output_gate decode shapes (num_tokens, num_heads * head_dim).
[(1, 4096), (17, 6144), (128, 4096), (256, 8192)],
)
def test_sigmoid_mul_matches_eager(
dtype: torch.dtype, shape: tuple[int, int], device: str
) -> None:
x = torch.randn(shape, device=device, dtype=dtype)
gate = torch.randn(shape, device=device, dtype=dtype)
ref = x.to(torch.float32) * gate.to(torch.float32).sigmoid()
ref = ref.to(dtype)
out = sigmoid_mul(x.clone(), gate)
tol = 1e-2 if dtype == torch.bfloat16 else 5e-3
torch.testing.assert_close(out, ref, atol=tol, rtol=tol)
def test_sigmoid_mul_is_inplace(device: str) -> None:
x = torch.randn(8, 256, device=device, dtype=torch.bfloat16)
gate = torch.randn_like(x)
same = sigmoid_mul(x, gate)
assert same.data_ptr() == x.data_ptr()
def test_sigmoid_mul_empty(device: str) -> None:
x = torch.empty(0, 256, device=device, dtype=torch.bfloat16)
gate = torch.empty_like(x)
out = sigmoid_mul(x, gate)
assert out.shape == x.shape
def test_sigmoid_mul_rejects_shape_mismatch(device: str) -> None:
x = torch.randn(4, 32, device=device, dtype=torch.bfloat16)
gate = torch.randn(4, 16, device=device, dtype=torch.bfloat16)
with pytest.raises(ValueError, match="shape mismatch"):
sigmoid_mul(x, gate)
def test_sigmoid_mul_rejects_dtype_mismatch(device: str) -> None:
x = torch.randn(4, 32, device=device, dtype=torch.bfloat16)
gate = torch.randn(4, 32, device=device, dtype=torch.float16)
with pytest.raises(ValueError, match="dtype mismatch"):
sigmoid_mul(x, gate)
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
@pytest.mark.parametrize(
"num_heads,num_kv_heads,head_dim",
# qwen3.5 attn_output_gate variants: q=16/kv=2/d=256 (base default) plus
# head_dim=128 fall-backs.
[(16, 2, 256), (32, 8, 128), (40, 8, 128), (48, 8, 128)],
)
def test_sigmoid_mul_strided_gate_from_qkv_split(
dtype: torch.dtype,
num_heads: int,
num_kv_heads: int,
head_dim: int,
device: str,
) -> None:
"""Runtime path: gate is the [T, H, D] strided view obtained via
``qkv.split`` → ``.view(T, H, 2*D)`` → ``torch.chunk(q_gate, 2, dim=-1)``.
``gate.stride(0)`` is the full qkv row width (q_size*2 + 2*kv_size),
not just H*2*D. The kernel must read this strided view directly without
a contiguous copy."""
num_tokens = 19
q_size = num_heads * head_dim
kv_size = num_kv_heads * head_dim
qkv = torch.randn(num_tokens, 2 * q_size + 2 * kv_size, device=device, dtype=dtype)
q_gate, _k, _v = qkv.split([2 * q_size, kv_size, kv_size], dim=-1)
q_gate = q_gate.view(num_tokens, num_heads, 2 * head_dim)
_q, gate = torch.chunk(q_gate, 2, dim=-1)
# Lock in the production-shape stride: row stride is the full qkv width.
assert not gate.is_contiguous()
assert gate.stride(0) == 2 * q_size + 2 * kv_size
assert gate.stride(-1) == 1
x = torch.randn(num_tokens, q_size, device=device, dtype=dtype)
ref = x.to(torch.float32) * gate.reshape(num_tokens, -1).to(torch.float32).sigmoid()
ref = ref.to(dtype)
out = sigmoid_mul(x.clone(), gate)
tol = 1e-2 if dtype == torch.bfloat16 else 5e-3
torch.testing.assert_close(out, ref, atol=tol, rtol=tol)
def test_sigmoid_mul_rejects_4d_gate(device: str) -> None:
x = torch.randn(4, 32, device=device, dtype=torch.bfloat16)
gate = torch.randn(4, 2, 4, 4, device=device, dtype=torch.bfloat16)
with pytest.raises(ValueError, match="gate must be 2D or 3D"):
sigmoid_mul(x, gate)
# --- silu_and_mul tests ---
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16, torch.float32])
@pytest.mark.parametrize("shape", [(1, 7168 * 2), (17, 1024), (128, 9216 * 2)])
def test_silu_and_mul_matches_eager(
dtype: torch.dtype, shape: tuple[int, int], device: str
) -> None:
x = torch.randn(shape, device=device, dtype=dtype)
d = shape[-1] // 2
ref = torch.nn.functional.silu(x[..., :d].float()) * x[..., d:].float()
ref = ref.to(dtype)
out = silu_and_mul(x)
tol = 1e-2 if dtype == torch.bfloat16 else 5e-3
torch.testing.assert_close(out, ref, atol=tol, rtol=tol)
def test_silu_and_mul_writes_provided_output(device: str) -> None:
x = torch.randn(8, 512, device=device, dtype=torch.bfloat16)
out = torch.empty(8, 256, device=device, dtype=torch.bfloat16)
same = silu_and_mul(x, out)
assert same.data_ptr() == out.data_ptr()
def test_silu_and_mul_empty(device: str) -> None:
x = torch.empty(0, 512, device=device, dtype=torch.bfloat16)
out = silu_and_mul(x)
assert out.shape == (0, 256)
def test_silu_and_mul_rejects_bad_output_shape(device: str) -> None:
x = torch.randn(4, 512, device=device, dtype=torch.bfloat16)
out = torch.empty(4, 128, device=device, dtype=torch.bfloat16)
with pytest.raises(ValueError, match="out shape"):
silu_and_mul(x, out)
# --- fused_gate_sigmoid_mul_add tests ---
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
@pytest.mark.parametrize(
"num_tokens,hidden_dim",
[(1, 3584), (1, 5120), (17, 3584), (128, 5120), (256, 3584)],
)
def test_fused_gate_sigmoid_mul_add_matches_eager(
dtype: torch.dtype, num_tokens: int, hidden_dim: int, device: str
) -> None:
hidden_states = torch.randn(num_tokens, hidden_dim, device=device, dtype=dtype)
gate_weight = torch.randn(hidden_dim, device=device, dtype=dtype)
shared_output = torch.randn(num_tokens, hidden_dim, device=device, dtype=dtype)
final = torch.randn(num_tokens, hidden_dim, device=device, dtype=dtype)
# Eager reference
gate_val = (hidden_states.float() @ gate_weight.float().unsqueeze(1)).sigmoid()
ref = final.float() + gate_val * shared_output.float()
ref = ref.to(dtype)
out = fused_gate_sigmoid_mul_add(
hidden_states, gate_weight, shared_output.clone(), final.clone()
)
tol = 1e-2 if dtype == torch.bfloat16 else 5e-3
torch.testing.assert_close(out, ref, atol=tol, rtol=tol)
def test_fused_gate_sigmoid_mul_add_is_inplace(device: str) -> None:
hidden_states = torch.randn(8, 256, device=device, dtype=torch.bfloat16)
gate_weight = torch.randn(256, device=device, dtype=torch.bfloat16)
shared_output = torch.randn(8, 256, device=device, dtype=torch.bfloat16)
final = torch.randn(8, 256, device=device, dtype=torch.bfloat16)
result = fused_gate_sigmoid_mul_add(
hidden_states, gate_weight, shared_output, final
)
assert result.data_ptr() == final.data_ptr()
def test_fused_gate_sigmoid_mul_add_empty(device: str) -> None:
hidden_states = torch.empty(0, 256, device=device, dtype=torch.bfloat16)
gate_weight = torch.randn(256, device=device, dtype=torch.bfloat16)
shared_output = torch.empty(0, 256, device=device, dtype=torch.bfloat16)
final = torch.empty(0, 256, device=device, dtype=torch.bfloat16)
out = fused_gate_sigmoid_mul_add(hidden_states, gate_weight, shared_output, final)
assert out.shape == (0, 256)