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
378 lines
11 KiB
Python
378 lines
11 KiB
Python
# Copyright (c) 2026 LightSeek Foundation
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
import torch
|
|
from tokenspeed_kernel import (
|
|
quantize_fp8,
|
|
quantize_fp8_with_scale,
|
|
quantize_mxfp4,
|
|
quantize_mxfp8,
|
|
quantize_nvfp4,
|
|
)
|
|
from tokenspeed_kernel.ops.quantization.triton import fp8_quantize
|
|
from tokenspeed_kernel.platform import current_platform
|
|
|
|
FP8_E4M3_FNUZ_MAX = 240.0
|
|
|
|
|
|
def _bitwise_equal(a: torch.Tensor, b: torch.Tensor) -> bool:
|
|
return torch.equal(a.view(torch.uint8), b.view(torch.uint8))
|
|
|
|
|
|
def _e2m1_values(nibbles: torch.Tensor) -> torch.Tensor:
|
|
magnitude_bits = nibbles & 0x7
|
|
exponent = (magnitude_bits >> 1).to(torch.float32)
|
|
mantissa = (magnitude_bits & 0x1).to(torch.float32)
|
|
normal = (1.0 + 0.5 * mantissa) * torch.exp2(exponent - 1.0)
|
|
subnormal = 0.5 * mantissa
|
|
magnitude = torch.where(exponent == 0, subnormal, normal)
|
|
sign = 1.0 - 2.0 * ((nibbles >> 3) & 0x1).to(torch.float32)
|
|
return magnitude * sign
|
|
|
|
|
|
def _dequantize_mxfp4(packed: torch.Tensor, scale: torch.Tensor) -> torch.Tensor:
|
|
out = packed.new_empty(
|
|
(*packed.shape[:-1], packed.shape[-1] * 2),
|
|
dtype=torch.float32,
|
|
)
|
|
out[..., 0::2] = _e2m1_values(packed & 0xF)
|
|
out[..., 1::2] = _e2m1_values(packed >> 4)
|
|
scale_values = torch.pow(2.0, scale.to(torch.int32) - 127).to(torch.float32)
|
|
return out * scale_values.repeat_interleave(32, dim=-1)
|
|
|
|
|
|
@pytest.mark.parametrize("solution", ["triton"])
|
|
@pytest.mark.parametrize(
|
|
"shape",
|
|
[
|
|
(1, 2880),
|
|
(8, 2880),
|
|
(33, 2880),
|
|
(4, 4096),
|
|
(2, 1),
|
|
(3, 513),
|
|
],
|
|
)
|
|
def test_quantize_fp8_pure_cast_bf16(
|
|
device: str,
|
|
solution: str,
|
|
shape: tuple[int, ...],
|
|
require,
|
|
) -> None:
|
|
torch.manual_seed(0)
|
|
dtype = torch.bfloat16
|
|
require("quantization", "fp8", solution, dtype, "x")
|
|
|
|
x = torch.randn(shape, device=device, dtype=dtype) * 50
|
|
fp8 = current_platform().fp8e4m3fn
|
|
ref = x.to(fp8.dtype)
|
|
|
|
out = quantize_fp8(x, solution=solution)
|
|
torch.cuda.synchronize()
|
|
|
|
assert out.shape == ref.shape
|
|
assert out.dtype == ref.dtype
|
|
assert _bitwise_equal(out, ref)
|
|
|
|
|
|
@pytest.mark.parametrize("solution", ["triton"])
|
|
def test_quantize_mxfp4_dynamic_scales(
|
|
device: str,
|
|
solution: str,
|
|
require,
|
|
) -> None:
|
|
dtype = torch.bfloat16
|
|
require("quantization", "mxfp4", solution, dtype, "x")
|
|
|
|
base = torch.tensor(
|
|
[
|
|
0.0,
|
|
0.5,
|
|
-0.5,
|
|
1.0,
|
|
-1.0,
|
|
1.5,
|
|
-1.5,
|
|
2.0,
|
|
-2.0,
|
|
3.0,
|
|
-3.0,
|
|
4.0,
|
|
-4.0,
|
|
6.0,
|
|
-6.0,
|
|
0.0,
|
|
],
|
|
device=device,
|
|
dtype=dtype,
|
|
)
|
|
row = torch.cat([base, base, base * 0.25, base * 0.25], dim=0)
|
|
x = torch.stack([row, row], dim=0)
|
|
|
|
out, scale = quantize_mxfp4(x, scale_layout="linear", solution=solution)
|
|
torch.cuda.synchronize()
|
|
|
|
assert out.shape == (2, 32)
|
|
assert out.dtype == torch.uint8
|
|
assert scale.shape == (2, 2)
|
|
assert scale.dtype == torch.uint8
|
|
torch.testing.assert_close(
|
|
scale.cpu(),
|
|
torch.tensor([[127, 125], [127, 125]], dtype=torch.uint8),
|
|
)
|
|
|
|
dequant = _dequantize_mxfp4(out.cpu(), scale.cpu())
|
|
torch.testing.assert_close(dequant, x.cpu().to(torch.float32), rtol=0, atol=0)
|
|
|
|
|
|
@pytest.mark.parametrize("solution", ["triton"])
|
|
def test_quantize_fp8_strided_slice(
|
|
device: str,
|
|
solution: str,
|
|
require,
|
|
) -> None:
|
|
torch.manual_seed(1)
|
|
dtype = torch.bfloat16
|
|
require("quantization", "fp8", solution, dtype, "x")
|
|
|
|
s, h, qk_nope, v_head = 4096, 16, 128, 128
|
|
kv = torch.randn(s, h, qk_nope + v_head, device=device, dtype=dtype) * 50
|
|
v = kv[..., qk_nope:]
|
|
assert not v.is_contiguous()
|
|
|
|
fp8 = current_platform().fp8e4m3fn
|
|
ref = v.to(fp8.dtype)
|
|
|
|
out = quantize_fp8(v, solution=solution)
|
|
torch.cuda.synchronize()
|
|
|
|
assert _bitwise_equal(out, ref)
|
|
|
|
|
|
@pytest.mark.parametrize("solution", ["triton"])
|
|
@pytest.mark.parametrize("scale", [2.0, 0.5, 7.5])
|
|
def test_quantize_fp8_scale_float(
|
|
device: str,
|
|
solution: str,
|
|
scale: float,
|
|
require,
|
|
) -> None:
|
|
torch.manual_seed(2)
|
|
dtype = torch.bfloat16
|
|
require("quantization", "fp8", solution, dtype, "x")
|
|
|
|
x = torch.randn(2048, 512, device=device, dtype=dtype) * 100
|
|
fp8 = current_platform().fp8e4m3fn
|
|
inv_scale = 1.0 / scale
|
|
ref = (
|
|
(x.to(torch.float32) * inv_scale).clamp(min=fp8.min, max=fp8.max).to(fp8.dtype)
|
|
)
|
|
|
|
out = quantize_fp8(x, scale=scale, solution=solution)
|
|
torch.cuda.synchronize()
|
|
|
|
assert _bitwise_equal(out, ref)
|
|
|
|
|
|
@pytest.mark.parametrize("solution", ["triton"])
|
|
def test_quantize_fp8_scale_tensor(
|
|
device: str,
|
|
solution: str,
|
|
require,
|
|
) -> None:
|
|
torch.manual_seed(3)
|
|
dtype = torch.bfloat16
|
|
require("quantization", "fp8", solution, dtype, "x")
|
|
|
|
x = torch.randn(8, 2880, device=device, dtype=dtype) * 100
|
|
scale = torch.tensor([0.125], device=device, dtype=torch.float32)
|
|
fp8 = current_platform().fp8e4m3fn
|
|
inv_scale = (1.0 / scale.to(torch.float32)).reshape(())
|
|
ref = (
|
|
(x.to(torch.float32) * inv_scale).clamp(min=fp8.min, max=fp8.max).to(fp8.dtype)
|
|
)
|
|
|
|
out = quantize_fp8(x, scale=scale, solution=solution)
|
|
torch.cuda.synchronize()
|
|
|
|
assert _bitwise_equal(out, ref)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"n",
|
|
[
|
|
# gpt-oss-120b: H = 2880 (hidden), I/tp = 2880/2 = 1440 (per-rank
|
|
# ispp). Both are non-power-of-2, so the n-axis must be masked
|
|
# both on load and on store for the W4A8 MoE forward path.
|
|
2880,
|
|
1440,
|
|
# ``M`` not divisible by ``BLOCK_M`` exercises the m-axis tail mask
|
|
# while ``N`` is non-pow2, ruling out a simple "round both up" bug.
|
|
7,
|
|
333,
|
|
],
|
|
)
|
|
def test_pure_cast_non_pow2_n(device: str, n: int) -> None:
|
|
torch.manual_seed(0)
|
|
x = torch.randn(33, n, device=device, dtype=torch.bfloat16) * 50
|
|
ref = x.to(torch.float8_e4m3fn)
|
|
out = fp8_quantize(x)
|
|
torch.cuda.synchronize()
|
|
assert out.shape == ref.shape
|
|
assert _bitwise_equal(out, ref)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not current_platform().is_cdna3,
|
|
reason="float8_e4m3fnuz (tl.float8e4b8) is only supported on AMD CDNA3",
|
|
)
|
|
def test_pure_cast_e4m3fnuz(device: str) -> None:
|
|
"""CDNA3-specific fp8 dtype (bias=8). The Triton cast must saturate to
|
|
``±240`` to match ``x.to(torch.float8_e4m3fnuz)``."""
|
|
torch.manual_seed(0)
|
|
x = torch.randn(2048, 512, device=device, dtype=torch.bfloat16) * 50
|
|
ref = x.to(torch.float8_e4m3fnuz)
|
|
out = fp8_quantize(x, fp8_dtype=torch.float8_e4m3fnuz)
|
|
torch.cuda.synchronize()
|
|
assert out.dtype == torch.float8_e4m3fnuz
|
|
assert _bitwise_equal(out, ref)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not current_platform().is_cdna3,
|
|
reason="float8_e4m3fnuz (tl.float8e4b8) is only supported on AMD CDNA3",
|
|
)
|
|
@pytest.mark.parametrize("scale", [2.0, 0.5, 7.5])
|
|
def test_scaled_cast_e4m3fnuz_matches_reference(device: str, scale: float) -> None:
|
|
torch.manual_seed(0)
|
|
x = torch.randn(2048, 512, device=device, dtype=torch.bfloat16) * 100
|
|
inv_scale = 1.0 / scale
|
|
ref = (
|
|
(x.to(torch.float32) * inv_scale)
|
|
.clamp(-FP8_E4M3_FNUZ_MAX, FP8_E4M3_FNUZ_MAX)
|
|
.to(torch.float8_e4m3fnuz)
|
|
)
|
|
out = fp8_quantize(x, scale=scale, fp8_dtype=torch.float8_e4m3fnuz)
|
|
torch.cuda.synchronize()
|
|
assert _bitwise_equal(out, ref)
|
|
|
|
|
|
@pytest.mark.parametrize("solution", ["trtllm"])
|
|
@pytest.mark.parametrize("granularity", ["tensor", "token"])
|
|
def test_quantize_fp8_with_scale_tensor_and_token(
|
|
device: str,
|
|
solution: str,
|
|
granularity: str,
|
|
require,
|
|
) -> None:
|
|
torch.manual_seed(4)
|
|
dtype = torch.bfloat16
|
|
require("quantization", "fp8_with_scale", solution, dtype, "x")
|
|
|
|
x = torch.randn(16, 128, device=device, dtype=dtype) * 10
|
|
fp8 = current_platform().fp8e4m3fn
|
|
|
|
out, scale = quantize_fp8_with_scale(
|
|
x,
|
|
granularity=granularity,
|
|
solution=solution,
|
|
)
|
|
torch.cuda.synchronize()
|
|
|
|
assert out.shape == x.shape
|
|
assert out.dtype == fp8.dtype
|
|
assert scale.dtype == torch.float32
|
|
if granularity == "tensor":
|
|
assert scale.shape == (1,)
|
|
else:
|
|
assert scale.shape == (x.shape[0], 1)
|
|
|
|
|
|
@pytest.mark.parametrize("solution", ["trtllm", "triton"])
|
|
def test_quantize_fp8_with_scale_token_group(
|
|
device: str,
|
|
solution: str,
|
|
require,
|
|
) -> None:
|
|
torch.manual_seed(5)
|
|
dtype = torch.bfloat16
|
|
require("quantization", "fp8_with_scale", solution, dtype, "x")
|
|
|
|
x = torch.randn(16, 256, device=device, dtype=dtype) * 10
|
|
fp8 = current_platform().fp8e4m3fn
|
|
|
|
out, scale = quantize_fp8_with_scale(
|
|
x,
|
|
granularity="token_group",
|
|
group_size=128,
|
|
solution=solution,
|
|
)
|
|
torch.cuda.synchronize()
|
|
|
|
assert out.shape == x.shape
|
|
assert out.dtype == fp8.dtype
|
|
assert scale.dtype == torch.float32
|
|
assert scale.numel() > 0
|
|
|
|
|
|
@pytest.mark.parametrize("solution", ["flashinfer"])
|
|
def test_quantize_mxfp8_shape_and_scale(
|
|
device: str,
|
|
solution: str,
|
|
require,
|
|
) -> None:
|
|
torch.manual_seed(6)
|
|
dtype = torch.bfloat16
|
|
require("quantization", "mxfp8", solution, dtype, "x")
|
|
|
|
x = torch.randn(17, 2880, device=device, dtype=dtype)
|
|
out, scale = quantize_mxfp8(x, solution=solution)
|
|
torch.cuda.synchronize()
|
|
|
|
assert out.shape[:-1] == x.shape[:-1]
|
|
assert out.shape[-1] >= x.shape[-1]
|
|
assert scale.numel() > 0
|
|
|
|
|
|
@pytest.mark.parametrize("solution", ["flashinfer"])
|
|
def test_quantize_nvfp4_shape_and_scale(
|
|
device: str,
|
|
solution: str,
|
|
require,
|
|
) -> None:
|
|
torch.manual_seed(7)
|
|
dtype = torch.bfloat16
|
|
require("quantization", "nvfp4", solution, dtype, "x")
|
|
|
|
x = torch.randn(16, 256, device=device, dtype=dtype)
|
|
out, scale = quantize_nvfp4(
|
|
x,
|
|
scale=torch.tensor([0.125], device=device, dtype=torch.float32),
|
|
solution=solution,
|
|
)
|
|
torch.cuda.synchronize()
|
|
|
|
assert out.shape[:-1] == x.shape[:-1]
|
|
assert out.shape[-1] == x.shape[-1] // 2
|
|
assert scale.numel() > 0
|