Files
2026-07-09 19:53:29 +10:00

297 lines
12 KiB
Python

"""Parity + integration tests for the OrbitQuant Triton kernels.
Strategy: compare the Triton kernels against the pure-PyTorch reference in
_orbitquant.py (which is the numerical ground truth and the converter-matched
implementation). All tests require CUDA + Triton; skipped otherwise.
"""
from __future__ import annotations
import pytest
import torch
from _intcrush import load
_oq = load("_orbitquant")
_qu = load("_quant_utils")
_tr = load("_triton_runtime")
CUDA = torch.cuda.is_available() and _tr.TRITON_AVAILABLE
TRITON_RPBH = CUDA and _tr.TRITON_RPBH_ROTATE
TRITON_CB = CUDA and _tr.TRITON_CODEBOOK_DEQUANT
# Realistic (d, h) pairs from the paper / FLUX / Wan / CogVideoX.
_RPBH_SHAPES = [(3072, 1024), (12288, 4096), (256, 256), (1920, 64), (4096, 4096)]
# A (d, h) where d_padded > d exercises the zero-padding path.
_PAD_SHAPES = [(3000, 1024)] # d_padded = ceil(3000/1024)*1024 = 3072
def _make_codebook(bits: int) -> torch.Tensor:
"""Uniform codebook (tests only -- production uses Lloyd-Max on f_d)."""
n = 1 << bits
return torch.linspace(-1.0, 1.0, n, dtype=torch.float32)
# ── RPBH ────────────────────────────────────────────────────────────────────
@pytest.mark.skipif(not TRITON_RPBH, reason="CUDA/Triton RPBH unavailable")
@pytest.mark.parametrize("d,h", _RPBH_SHAPES + _PAD_SHAPES)
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32])
def test_rpbh_parity_vs_pytorch(d, h, dtype):
perm, signs, _ = _oq.make_rpbh_params(d, seed=42)
d_padded = perm.shape[0]
perm = perm.cuda()
signs = signs.cuda()
M = 64
x = torch.randn(M, d, dtype=dtype, device="cuda")
ref = _oq.rpbh_rotate(x, perm, signs, h)
got = _tr.rpbh_rotate_triton(x, perm, signs, h, d_padded)
assert got.shape == (M, d_padded), f"shape {got.shape} != ({M},{d_padded})"
assert got.dtype == x.dtype
# fp32 internal compute; tolerate fp16/bf16 storage rounding.
atol = 1e-3 if dtype != torch.float32 else 1e-4
rtol = 1e-3
torch.testing.assert_close(got.float(), ref.float(), atol=atol, rtol=rtol)
@pytest.mark.skipif(not TRITON_RPBH, reason="CUDA/Triton RPBH unavailable")
@pytest.mark.parametrize("d,h", _RPBH_SHAPES)
def test_rpbh_norm_preservation(d, h):
perm, signs, _ = _oq.make_rpbh_params(d, seed=7)
perm = perm.cuda()
signs = signs.cuda()
x = torch.randn(32, d, device="cuda")
y = _tr.rpbh_rotate_triton(x, perm, signs, h)
# RPBH is orthogonal: ||y|| == ||x|| (up to padding zeros, which x lacks).
rel = (x.norm() - y.norm()).abs().item() / x.norm().item()
assert rel < 1e-3, f"norm not preserved: rel={rel}"
@pytest.mark.skipif(not TRITON_RPBH, reason="CUDA/Triton RPBH unavailable")
@pytest.mark.parametrize("d,h", _RPBH_SHAPES)
def test_rpbh_cancellation(d, h):
"""(W @ Pi^T) @ (x @ Pi^T)^T == W @ x^T (the sacred contract)."""
perm, signs, _ = _oq.make_rpbh_params(d, seed=42)
perm = perm.cuda()
signs = signs.cuda()
W = torch.randn(16, d, device="cuda", dtype=torch.float32)
x = torch.randn(8, d, device="cuda", dtype=torch.float32)
W_rot = _tr.rpbh_rotate_triton(W, perm, signs, h)
x_rot = _tr.rpbh_rotate_triton(x, perm, signs, h)
y_rot = W_rot @ x_rot.t()
y_ref = W @ x.t()
rel = (y_rot - y_ref).norm().item() / y_ref.norm().item()
assert rel < 1e-3, f"cancellation broken: rel={rel}"
@pytest.mark.skipif(not TRITON_RPBH, reason="CUDA/Triton RPBH unavailable")
def test_rpbh_dtype_preservation():
d, h = 1024, 1024
perm, signs, _ = _oq.make_rpbh_params(d, seed=1)
perm = perm.cuda()
signs = signs.cuda()
for dt in (torch.float16, torch.bfloat16, torch.float32):
x = torch.randn(4, d, dtype=dt, device="cuda")
y = _tr.rpbh_rotate_triton(x, perm, signs, h)
assert y.dtype == dt
@pytest.mark.skipif(not TRITON_RPBH, reason="CUDA/Triton RPBH unavailable")
def test_rpbh_zero_padding():
"""d < d_padded: padded positions contribute 0, matching rpbh_rotate."""
d, h = 3000, 1024 # d_padded = 3072
perm, signs, _ = _oq.make_rpbh_params(d, seed=42)
d_padded = perm.shape[0]
assert d_padded > d
perm = perm.cuda()
signs = signs.cuda()
x = torch.randn(16, d, device="cuda", dtype=torch.float32)
ref = _oq.rpbh_rotate(x, perm, signs, h)
got = _tr.rpbh_rotate_triton(x, perm, signs, h, d_padded)
torch.testing.assert_close(got, ref, atol=1e-4, rtol=1e-4)
# ── Codebook dequant ──────────────────────────────────────────────────────────
@pytest.mark.skipif(not TRITON_CB, reason="CUDA/Triton codebook dequant unavailable")
@pytest.mark.parametrize("bits", [4, 8])
def test_codebook_dequant_parity(bits):
d = 256
N = 32
codebook = _make_codebook(bits).cuda()
W = torch.randn(N, d, device="cuda") * 0.1
norms = W.norm(dim=1, keepdim=True).clamp(min=1e-10)
W_dir = W / norms
# Nearest-centroid indices.
boundaries = (codebook[:-1] + codebook[1:]) / 2.0
indices = torch.searchsorted(boundaries, W_dir.reshape(-1)).reshape(N, d)
indices = indices.clamp(0, (1 << bits) - 1)
offset = 1 << (bits - 1)
indices_i8 = (indices - offset).to(torch.int8)
if bits == 4:
packed = _qu.pack_int4(indices_i8.cpu()).to(torch.uint8).cuda()
else:
packed = indices_i8.cuda()
norms_bf = norms.to(torch.bfloat16).cuda()
# Reference returns float32.
if bits == 4:
ref = _oq.codebook_dequant_int4(packed, norms_bf, codebook, d, d)
else:
ref = _oq.codebook_dequant_int8(packed, norms_bf, codebook, d, d)
# Triton default -> fp16; compare in fp32 with fp16 tolerance.
got = _tr.codebook_dequant_triton(packed, norms_bf, codebook, d, bits)
assert got.shape == (N, d)
torch.testing.assert_close(got.float(), ref.float(), atol=2e-3, rtol=1e-2)
# fp32 output path (used by OrbitQuantLayout.dequantize for LoRA parity).
out32 = torch.empty(N, d, dtype=torch.float32, device="cuda")
got32 = _tr.codebook_dequant_triton(packed, norms_bf, codebook, d, bits, out=out32)
torch.testing.assert_close(got32, ref, atol=1e-5, rtol=1e-5)
@pytest.mark.skipif(not TRITON_CB, reason="CUDA/Triton codebook dequant unavailable")
def test_codebook_dequant_no_trim():
"""Output last dim is d_padded, never trimmed to d (inference-basis match)."""
bits = 4
d_padded = 256
N = 8
codebook = _make_codebook(bits).cuda()
packed = torch.zeros(N, (d_padded + 1) // 2, dtype=torch.uint8, device="cuda")
norms = torch.ones(N, dtype=torch.bfloat16, device="cuda")
out = _tr.codebook_dequant_triton(packed, norms, codebook, d_padded, bits)
assert out.shape == (N, d_padded), f"shape {out.shape} != ({N},{d_padded})"
assert out.dtype == torch.float16
# ── Dispatch / fallback ─────────────────────────────────────────────────────
@pytest.mark.skipif(not CUDA, reason="CUDA unavailable")
def test_rpbh_fallback_when_triton_disabled(monkeypatch):
"""With TRITON_RPBH_ROTATE=False, the fast dispatcher uses PyTorch."""
if not _tr.TRITON_RPBH_ROTATE:
pytest.skip("needs Triton RPBH loaded to test the flag-off path")
d, h = 256, 256
perm, signs, _ = _oq.make_rpbh_params(d, seed=42)
perm = perm.cuda()
signs = signs.cuda()
x = torch.randn(8, d, device="cuda", dtype=torch.float32)
monkeypatch.setattr(_tr, "TRITON_RPBH_ROTATE", False)
y = _oq.rpbh_rotate_fast(x, perm, signs, h)
ref = _oq.rpbh_rotate(x, perm, signs, h)
torch.testing.assert_close(y, ref, atol=1e-4, rtol=1e-4)
@pytest.mark.skipif(not CUDA, reason="CUDA unavailable")
def test_codebook_dequant_fallback_when_triton_disabled(monkeypatch):
"""With TRITON_CODEBOOK_DEQUANT=False, the fast dispatcher uses PyTorch."""
if not _tr.TRITON_CODEBOOK_DEQUANT:
pytest.skip("needs Triton codebook dequant loaded to test the flag-off path")
bits = 4
d = 256
codebook = _make_codebook(bits).cuda()
indices_i8 = torch.zeros(16, d, dtype=torch.int8, device="cuda")
packed = _qu.pack_int4(indices_i8.cpu()).to(torch.uint8).cuda()
norms = torch.ones(16, dtype=torch.bfloat16, device="cuda")
monkeypatch.setattr(_tr, "TRITON_CODEBOOK_DEQUANT", False)
y = _oq.codebook_dequant_fast(packed, norms, codebook, d, d, bits)
ref = _oq.codebook_dequant(packed, norms, codebook, d, d, bits)
torch.testing.assert_close(y.float(), ref.float(), atol=1e-5, rtol=1e-5)
@pytest.mark.skipif(not CUDA, reason="CUDA unavailable")
def test_rpbh_cpu_uses_pytorch_reference():
"""CPU inputs always go through the PyTorch reference (no Triton on CPU)."""
d, h = 256, 256
perm, signs, _ = _oq.make_rpbh_params(d, seed=42)
x = torch.randn(4, d, dtype=torch.float32) # CPU
y = _oq.rpbh_rotate_fast(x, perm, signs, h)
ref = _oq.rpbh_rotate(x, perm, signs, h)
torch.testing.assert_close(y, ref, atol=1e-5, rtol=1e-5)
# ── Integration: forward-path contracts ───────────────────────────────────────
@pytest.mark.skipif(not TRITON_RPBH, reason="CUDA/Triton RPBH unavailable")
def test_adaln_never_rotated():
"""AdaLN-RTN activations must NOT be RPBH-rotated (paper Sec B.2).
An adaln_rtn layer has _intcrush_oq_perm = None (loader sets RPBH only
for codebook layers; adaln gets a {"perm": None, ...} fallback).
_rpbh_rotate would raise on None perm, which is exactly the guard --
the forward path calls adaln_rtn_dequant + F.linear directly, bypassing
_rpbh_rotate. This test documents that contract.
"""
d, h = 256, 1024
perm, signs, _ = _oq.make_rpbh_params(d, seed=42)
perm = perm.cuda()
signs = signs.cuda()
x = torch.randn(8, d, device="cuda", dtype=torch.float32)
# A codebook-layer activation, rotated, is well-defined:
y = _tr.rpbh_rotate_triton(x, perm, signs, h)
assert y.shape == (8, perm.shape[0])
# An adaln-layer activation is used RAW -- this is the reference value:
y_adaln = x # literally x_2d unchanged
assert y_adaln.shape == (8, d)
@pytest.mark.skipif(not (TRITON_RPBH and TRITON_CB), reason="needs both kernels")
def test_forward_orbitquant_matches_reference():
"""End-to-end: rpbh(x) @ dequant(W) == reference (cancellation preserves Wx)."""
d, h = 1024, 1024
N_out = 64
M = 16
bits = 4
codebook = _make_codebook(bits).cuda()
# Build a random weight, rotate it offline (converter-side), quantize to codebook.
W = torch.randn(N_out, d, device="cuda", dtype=torch.float32)
perm, signs, _ = _oq.make_rpbh_params(d, seed=42)
perm = perm.cuda()
signs = signs.cuda()
d_padded = perm.shape[0]
# Rotate weight into the shared basis (offline step the converter does).
W_rot = _oq.rpbh_rotate(W, perm, signs, h)
# Quantize the rotated directions to the codebook (simulates converter).
norms = W_rot.norm(dim=1, keepdim=True).clamp(min=1e-10)
Wrot_dir = W_rot / norms
boundaries = (codebook[:-1] + codebook[1:]) / 2.0
indices = torch.searchsorted(boundaries, Wrot_dir.reshape(-1)).reshape(N_out, d_padded)
indices = indices.clamp(0, (1 << bits) - 1)
offset = 1 << (bits - 1)
indices_i8 = (indices - offset).to(torch.int8)
packed = _qu.pack_int4(indices_i8.cpu()).to(torch.uint8).cuda()
norms_bf = norms.to(torch.bfloat16).cuda()
# Reference forward (pure PyTorch):
x = torch.randn(M, d, device="cuda", dtype=torch.float32)
x_rot_ref = _oq.rpbh_rotate(x, perm, signs, h)
W_f_ref = _oq.codebook_dequant_int4(packed, norms_bf, codebook, d, d_padded)
y_ref = x_rot_ref @ W_f_ref.to(torch.float32).t()
# Triton forward:
x_rot = _tr.rpbh_rotate_triton(x, perm, signs, h, d_padded)
W_f = _tr.codebook_dequant_triton(
packed, norms_bf, codebook, d_padded, bits,
out=torch.empty(N_out, d_padded, dtype=torch.float32, device="cuda"),
)
y = x_rot.float() @ W_f.t()
rel = (y - y_ref).norm().item() / max(y_ref.norm().item(), 1e-12)
assert rel < 2e-3, f"end-to-end mismatch: rel={rel}"