| |
| """Correctness tests for transformer-layout-primitives.""" |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import importlib |
| import os |
| import sys |
| from pathlib import Path |
|
|
| import torch |
|
|
| ROOT = Path(__file__).resolve().parents[2] |
| PACKAGE = ROOT / "transformer-layout-primitives" |
| REGISTRATION_INCLUDE = ROOT.parent / "kernels" / "kernel-builder" / "src" / "pyproject" / "templates" / "torch" |
|
|
|
|
| class SourceOps: |
| def __init__(self, namespace: str) -> None: |
| self.ops = getattr(torch.ops, namespace) |
|
|
| def fill_neginf_bf16(self, dst): |
| self.ops.fill_neginf_bf16(dst) |
| return dst |
|
|
| def add_bias_bf16_(self, data, bias): |
| self.ops.add_bias_bf16_(data, bias) |
| return data |
|
|
| def repeat_interleave_heads_bf16(self, src, repeat, out=None): |
| if out is None: |
| out = torch.empty((src.shape[0], src.shape[1] * repeat, src.shape[2]), device=src.device, dtype=src.dtype) |
| self.ops.repeat_interleave_heads_bf16(src, int(repeat), out) |
| return out |
|
|
| def text_gather_bf16(self, src, batch, seq, out=None): |
| if out is None: |
| out = torch.empty((2 * batch, src.shape[1]), device=src.device, dtype=src.dtype) |
| self.ops.text_gather_bf16(src, int(batch), int(seq), out) |
| return out |
|
|
| def text_scatter_bf16(self, dst, src, batch, seq): |
| self.ops.text_scatter_bf16(dst, src, int(batch), int(seq)) |
| return dst |
|
|
| def rope_rotate_half_bf16_(self, x, cos, sin): |
| self.ops.rope_rotate_half_bf16_(x, cos, sin) |
| return x |
|
|
| def qk_rmsnorm_rope_bf16_(self, qk, weight, cos, sin, eps=1e-6): |
| self.ops.qk_rmsnorm_rope_bf16_(qk, weight, cos, sin, float(eps)) |
| return qk |
|
|
| def qk_pair_rmsnorm_rope_bf16( |
| self, q, k, q_weight, k_weight, cos, sin, eps=1e-6, q_out=None, k_out=None |
| ): |
| if q_out is None: |
| q_out = torch.empty_like(q) |
| if k_out is None: |
| k_out = torch.empty_like(k) |
| self.ops.qk_pair_rmsnorm_rope_bf16( |
| q, k, q_weight, k_weight, cos, sin, float(eps), q_out, k_out |
| ) |
| return q_out, k_out |
|
|
| def gather_rows_bf16(self, src, row_indices, out=None): |
| if out is None: |
| out = torch.empty( |
| (row_indices.numel(), src.shape[1]), device=src.device, dtype=src.dtype |
| ) |
| self.ops.gather_rows_bf16(src, row_indices, out) |
| return out |
|
|
| def scatter_rows_bf16(self, src, row_indices, destination_rows, out=None): |
| if out is None: |
| out = torch.zeros( |
| (destination_rows, src.shape[1]), device=src.device, dtype=src.dtype |
| ) |
| self.ops.scatter_rows_bf16(src, row_indices, out) |
| return out |
|
|
|
|
| def _arch_list() -> str: |
| major, minor = torch.cuda.get_device_capability(0) |
| return "12.0a" if major >= 12 else f"{major}.{minor}" |
|
|
|
|
| def load_source_ops() -> SourceOps: |
| from torch.utils.cpp_extension import load |
|
|
| os.environ.setdefault("TORCH_CUDA_ARCH_LIST", _arch_list()) |
| namespace = "transformer_layout_primitives_source_test" |
| load( |
| name=namespace, |
| sources=[ |
| str(PACKAGE / "torch-ext" / "torch_binding.cpp"), |
| str(PACKAGE / "csrc" / "transformer_layout_primitives.cu"), |
| ], |
| extra_include_paths=[str(PACKAGE / "csrc"), str(REGISTRATION_INCLUDE)], |
| extra_cflags=["-O3", "-DCUDA_KERNEL"], |
| extra_cuda_cflags=[ |
| "-O3", |
| "--expt-relaxed-constexpr", |
| "-DCUDA_KERNEL", |
| "-U__CUDA_NO_BFLOAT16_CONVERSIONS__", |
| "-U__CUDA_NO_BFLOAT16_OPERATORS__", |
| "-U__CUDA_NO_BFLOAT162_OPERATORS__", |
| ], |
| is_python_module=False, |
| verbose=False, |
| ) |
| return SourceOps(namespace) |
|
|
|
|
| def load_installed_ops(artifact: str | None): |
| if artifact: |
| sys.path.insert(0, artifact) |
| try: |
| return importlib.import_module("transformer_layout_primitives") |
| finally: |
| if artifact: |
| sys.path.remove(artifact) |
|
|
|
|
| def rotate_half_ref(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor: |
| half = x.shape[-1] // 2 |
| lo = x[..., :half].float() |
| hi = x[..., half:].float() |
| c = cos[:, None, :half].float() |
| s = sin[:, None, :half].float() |
| return torch.cat([lo * c - hi * s, hi * c + lo * s], dim=-1).to(torch.bfloat16) |
|
|
|
|
| def qk_rmsnorm_rope_ref( |
| qk: torch.Tensor, |
| weight: torch.Tensor, |
| cos: torch.Tensor, |
| sin: torch.Tensor, |
| eps: float = 1e-6, |
| ) -> torch.Tensor: |
| rms = torch.rsqrt((qk.float() * qk.float()).mean(dim=-1, keepdim=True) + eps) |
| normed = (qk.float() * rms * weight.float()).to(torch.bfloat16) |
| return rotate_half_ref(normed, cos, sin) |
|
|
|
|
| def qk_pair_rmsnorm_rope_ref( |
| q: torch.Tensor, |
| k: torch.Tensor, |
| q_weight: torch.Tensor, |
| k_weight: torch.Tensor, |
| cos: torch.Tensor, |
| sin: torch.Tensor, |
| eps: float = 1e-6, |
| ) -> tuple[torch.Tensor, torch.Tensor]: |
| return ( |
| qk_rmsnorm_rope_ref(q, q_weight, cos, sin, eps), |
| qk_rmsnorm_rope_ref(k, k_weight, cos, sin, eps), |
| ) |
|
|
|
|
| def metrics(got: torch.Tensor, ref: torch.Tensor) -> tuple[float, float, float]: |
| diff = (got.float() - ref.float()).abs() |
| cos = torch.nn.functional.cosine_similarity(got.float().flatten(), ref.float().flatten(), dim=0).item() |
| return float(diff.max().item()), float(diff.mean().item()), float(cos) |
|
|
|
|
| def bf16_max_ulp(got: torch.Tensor, ref: torch.Tensor) -> int: |
| def ordered_bits(value: torch.Tensor) -> torch.Tensor: |
| bits = value.contiguous().view(torch.int16).to(torch.int32) & 0xFFFF |
| magnitude = bits & 0x7FFF |
| return torch.where( |
| (bits & 0x8000) != 0, |
| 0x8000 - magnitude, |
| 0x8000 + magnitude, |
| ) |
|
|
| return int( |
| (ordered_bits(got) - ordered_bits(ref)).abs().max().item() |
| ) |
|
|
|
|
| def assert_close( |
| name: str, |
| got: torch.Tensor, |
| ref: torch.Tensor, |
| atol: float, |
| cos_min: float, |
| rtol: float = 0.0, |
| ) -> None: |
| max_abs, mean_abs, cos = metrics(got, ref) |
| diff = (got.float() - ref.float()).abs() |
| p99_abs = float(torch.quantile(diff, 0.99).item()) |
| tolerance = atol + rtol * ref.float().abs() |
| violations = int((diff > tolerance).sum().item()) |
| max_ulp = bf16_max_ulp(got, ref) |
| print( |
| f"{name}: max_abs={max_abs:.6f} p99_abs={p99_abs:.6f} " |
| f"mean_abs={mean_abs:.6e} max_ulp={max_ulp} cosine={cos:.8f} " |
| f"violations={violations} rtol={rtol} atol={atol}" |
| ) |
| if violations or cos < cos_min: |
| raise AssertionError( |
| f"{name} failed: max_abs={max_abs} p99_abs={p99_abs} " |
| f"max_ulp={max_ulp} cosine={cos} violations={violations}" |
| ) |
|
|
|
|
| def expect_runtime_error(name: str, fn) -> None: |
| try: |
| fn() |
| except RuntimeError: |
| print(f"{name}: rejected") |
| return |
| raise AssertionError(f"{name}: expected RuntimeError") |
|
|
|
|
| def run(ops, mode: str) -> int: |
| torch.manual_seed(31) |
| count = 0 |
| layout_shapes = [(2, 5, 128), (3, 49, 256)] if mode == "smoke" else [ |
| (1, 1, 128), |
| (2, 5, 128), |
| (3, 49, 256), |
| (4, 256, 1024), |
| (2, 2520, 2048), |
| ] |
| for batch, seq, dim in layout_shapes: |
| x = torch.randn((batch * seq, dim), device="cuda", dtype=torch.bfloat16) |
|
|
| dst = torch.empty_like(x) |
| ops.fill_neginf_bf16(dst) |
| ref_neginf = torch.full_like(x, -1e30) |
| torch.testing.assert_close(dst.float().cpu(), ref_neginf.float().cpu(), rtol=0, atol=0) |
| count += 1 |
|
|
| bias = torch.randn((dim,), device="cuda", dtype=torch.bfloat16) |
| got = x.clone() |
| ops.add_bias_bf16_(got, bias) |
| ref = (x.float() + bias.float()).to(torch.bfloat16) |
| torch.testing.assert_close(got.cpu(), ref.cpu(), rtol=0, atol=0) |
| count += 1 |
|
|
| gathered = ops.text_gather_bf16(x, batch, seq) |
| ref_gather = torch.stack([x[b * seq + offset] for b in range(batch) for offset in (0, seq - 1)], dim=0) |
| torch.testing.assert_close(gathered.cpu(), ref_gather.cpu(), rtol=0, atol=0) |
| count += 1 |
|
|
| scattered = torch.zeros_like(x) |
| ops.text_scatter_bf16(scattered, gathered, batch, seq) |
| ref_scatter = torch.zeros_like(x) |
| for b in range(batch): |
| ref_scatter[b * seq] = gathered[2 * b] |
| ref_scatter[b * seq + seq - 1] = gathered[2 * b + 1] |
| torch.testing.assert_close(scattered.cpu(), ref_scatter.cpu(), rtol=0, atol=0) |
| count += 1 |
|
|
| cpu_rng_state = torch.random.get_rng_state() |
| cuda_rng_state = torch.cuda.get_rng_state() |
| indexed_shapes = [(17, 5, 64), (277, 51, 128)] if mode == "smoke" else [ |
| (1, 1, 8), |
| (17, 5, 64), |
| (277, 51, 128), |
| (128, 60, 2048), |
| (2520, 105, 1152), |
| (5070, 257, 4096), |
| ] |
| for source_rows, selected_rows, hidden in indexed_shapes: |
| src = torch.randn( |
| (source_rows, hidden), device="cuda", dtype=torch.bfloat16 |
| ) |
| indices = torch.randperm(source_rows, device="cuda", dtype=torch.int64)[ |
| :selected_rows |
| ].contiguous() |
| got = ops.gather_rows_bf16(src, indices) |
| ref = src.index_select(0, indices) |
| torch.testing.assert_close(got, ref, rtol=0, atol=0) |
| count += 1 |
|
|
| destination_rows = source_rows + 3 |
| got = ops.scatter_rows_bf16(ref, indices, destination_rows) |
| ref_scatter = torch.zeros( |
| (destination_rows, hidden), device="cuda", dtype=torch.bfloat16 |
| ) |
| ref_scatter.index_copy_(0, indices, ref) |
| torch.testing.assert_close(got, ref_scatter, rtol=0, atol=0) |
| count += 1 |
| torch.random.set_rng_state(cpu_rng_state) |
| torch.cuda.set_rng_state(cuda_rng_state) |
|
|
| repeat_shapes = [(17, 4, 64, 2), (128, 8, 128, 4)] if mode == "smoke" else [ |
| (1, 1, 64, 8), |
| (17, 4, 64, 2), |
| (128, 8, 128, 4), |
| (2520, 8, 128, 4), |
| ] |
| for seq, heads, dim, repeat in repeat_shapes: |
| src = torch.randn((seq, heads, dim), device="cuda", dtype=torch.bfloat16) |
| got = ops.repeat_interleave_heads_bf16(src, repeat) |
| ref = src.repeat_interleave(repeat, dim=1) |
| torch.testing.assert_close(got.cpu(), ref.cpu(), rtol=0, atol=0) |
| count += 1 |
|
|
| rope_shapes = [(17, 4, 64), (128, 8, 128)] if mode == "smoke" else [ |
| (1, 1, 64), |
| (17, 4, 64), |
| (128, 8, 128), |
| (2520, 32, 128), |
| ] |
| for seq, heads, dim in rope_shapes: |
| x = torch.randn((seq, heads, dim), device="cuda", dtype=torch.bfloat16) |
| cos = torch.randn((seq, dim), device="cuda", dtype=torch.bfloat16) |
| sin = torch.randn((seq, dim), device="cuda", dtype=torch.bfloat16) |
| got = x.clone() |
| ops.rope_rotate_half_bf16_(got, cos, sin) |
| ref = rotate_half_ref(x, cos, sin) |
| assert_close(f"rope seq={seq} heads={heads} dim={dim}", got, ref, atol=0, cos_min=0.999999) |
| count += 1 |
|
|
| weight = torch.randn((dim,), device="cuda", dtype=torch.bfloat16) |
| got = x.clone() |
| ops.qk_rmsnorm_rope_bf16_(got, weight, cos, sin) |
| ref = qk_rmsnorm_rope_ref(x, weight, cos, sin) |
| |
| |
| |
| assert_close( |
| f"qk_rmsnorm_rope seq={seq} heads={heads} dim={dim}", |
| got, |
| ref, |
| atol=0.015625, |
| rtol=0.02, |
| cos_min=0.999999, |
| ) |
| count += 1 |
|
|
| pair_shapes = [(17, 16, 8, 128), (49, 16, 16, 72)] if mode == "smoke" else [ |
| (1, 16, 8, 128), |
| (17, 16, 8, 128), |
| (49, 16, 16, 72), |
| (51, 16, 16, 80), |
| (65, 32, 8, 128), |
| (277, 16, 8, 128), |
| (512, 24, 24, 128), |
| (2520, 24, 24, 128), |
| (5070, 24, 24, 128), |
| ] |
| for rows, q_heads, k_heads, dim in pair_shapes: |
| q = torch.randn((rows, q_heads, dim), device="cuda", dtype=torch.bfloat16) |
| k = torch.randn((rows, k_heads, dim), device="cuda", dtype=torch.bfloat16) |
| q_weight = torch.randn((dim,), device="cuda", dtype=torch.bfloat16) |
| k_weight = torch.randn((dim,), device="cuda", dtype=torch.bfloat16) |
| angles = torch.randn((rows, dim // 2), device="cuda", dtype=torch.float32) |
| cos_half = angles.cos().to(torch.bfloat16) |
| sin_half = angles.sin().to(torch.bfloat16) |
| cos = torch.cat((cos_half, cos_half), dim=-1) |
| sin = torch.cat((sin_half, sin_half), dim=-1) |
| got_q, got_k = ops.qk_pair_rmsnorm_rope_bf16( |
| q, k, q_weight, k_weight, cos, sin |
| ) |
| staged_q = q.clone() |
| staged_k = k.clone() |
| ops.qk_rmsnorm_rope_bf16_(staged_q, q_weight, cos, sin) |
| ops.qk_rmsnorm_rope_bf16_(staged_k, k_weight, cos, sin) |
| torch.testing.assert_close(got_q, staged_q, rtol=0, atol=0) |
| torch.testing.assert_close(got_k, staged_k, rtol=0, atol=0) |
| ref_q, ref_k = qk_pair_rmsnorm_rope_ref( |
| q, k, q_weight, k_weight, cos, sin |
| ) |
| label = f"qk_pair rows={rows} qh={q_heads} kh={k_heads} dim={dim}" |
| |
| |
| |
| assert_close( |
| f"{label}/q", got_q, ref_q, atol=0.015625, rtol=0.02, cos_min=0.999999 |
| ) |
| assert_close( |
| f"{label}/k", got_k, ref_k, atol=0.015625, rtol=0.02, cos_min=0.999999 |
| ) |
| count += 4 |
|
|
| q = torch.randn((17, 4, 64), device="cuda", dtype=torch.bfloat16) |
| k = torch.randn((17, 2, 64), device="cuda", dtype=torch.bfloat16) |
| weight = torch.ones((64,), device="cuda", dtype=torch.bfloat16) |
| cos = torch.ones((17, 64), device="cuda", dtype=torch.bfloat16) |
| sin = torch.zeros_like(cos) |
| expect_runtime_error( |
| "qk_pair mismatched rows", |
| lambda: ops.qk_pair_rmsnorm_rope_bf16( |
| q, k[:-1].contiguous(), weight, weight, cos, sin |
| ), |
| ) |
| expect_runtime_error( |
| "qk_pair invalid head_dim", |
| lambda: ops.qk_pair_rmsnorm_rope_bf16( |
| q[:, :, :-1].contiguous(), |
| k[:, :, :-1].contiguous(), |
| weight[:-1].contiguous(), |
| weight[:-1].contiguous(), |
| cos[:, :-1].contiguous(), |
| sin[:, :-1].contiguous(), |
| ), |
| ) |
| expect_runtime_error( |
| "qk_pair noncontiguous", |
| lambda: ops.qk_pair_rmsnorm_rope_bf16( |
| q.transpose(0, 1), k, weight, weight, cos, sin |
| ), |
| ) |
| count += 3 |
|
|
| return count |
|
|
|
|
| def run_compile_default_eps(ops) -> int: |
| seq, heads, dim = 17, 4, 64 |
| x = torch.randn((seq, heads, dim), device="cuda", dtype=torch.bfloat16) |
| weight = torch.randn((dim,), device="cuda", dtype=torch.bfloat16) |
| cos = torch.randn((seq, dim), device="cuda", dtype=torch.bfloat16) |
| sin = torch.randn((seq, dim), device="cuda", dtype=torch.bfloat16) |
| ref = qk_rmsnorm_rope_ref(x, weight, cos, sin) |
|
|
| def invoke(qk, rms_weight, rope_cos, rope_sin): |
| return ops.qk_rmsnorm_rope_bf16_(qk, rms_weight, rope_cos, rope_sin) |
|
|
| compiled = torch.compile(invoke, fullgraph=True) |
| got = x.clone() |
| compiled(got, weight, cos, sin) |
| assert_close("qk_rmsnorm_rope compile default eps", got, ref, atol=0.015625, cos_min=0.999999) |
|
|
| k = torch.randn((seq, 2, dim), device="cuda", dtype=torch.bfloat16) |
| k_weight = torch.randn((dim,), device="cuda", dtype=torch.bfloat16) |
| pair_ref = qk_pair_rmsnorm_rope_ref(x, k, weight, k_weight, cos, sin) |
|
|
| def invoke_pair(q, key, qw, kw, rope_cos, rope_sin): |
| return ops.qk_pair_rmsnorm_rope_bf16(q, key, qw, kw, rope_cos, rope_sin) |
|
|
| compiled_pair = torch.compile(invoke_pair, fullgraph=True) |
| got_q, got_k = compiled_pair(x, k, weight, k_weight, cos, sin) |
| assert_close("qk_pair compile default eps/q", got_q, pair_ref[0], atol=0.015625, cos_min=0.999999) |
| assert_close("qk_pair compile default eps/k", got_k, pair_ref[1], atol=0.015625, cos_min=0.999999) |
| return 3 |
|
|
|
|
| def run_indexed_compile(ops) -> int: |
| src = torch.randn((277, 128), device="cuda", dtype=torch.bfloat16) |
| indices = torch.randperm(277, device="cuda", dtype=torch.int64)[:51].contiguous() |
|
|
| def invoke(src, indices): |
| gathered = ops.gather_rows_bf16(src, indices) |
| return ops.scatter_rows_bf16(gathered, indices, src.shape[0]) |
|
|
| eager = invoke(src, indices) |
| compiled = torch.compile(invoke, fullgraph=True)(src, indices) |
| torch.testing.assert_close(compiled, eager, rtol=0, atol=0) |
| print("PASS gather/scatter torch.compile fullgraph") |
| return 1 |
|
|
|
|
| def run_cosmos_edge_indexed_graph(ops) -> int: |
| source_rows, selected_rows, hidden = 128, 60, 2048 |
| src = torch.randn((source_rows, hidden), device="cuda", dtype=torch.bfloat16) |
| indices = torch.randperm( |
| source_rows, device="cuda", dtype=torch.int64 |
| )[:selected_rows].contiguous() |
| gathered = torch.empty( |
| (selected_rows, hidden), device="cuda", dtype=torch.bfloat16 |
| ) |
| scattered = torch.zeros( |
| (source_rows, hidden), device="cuda", dtype=torch.bfloat16 |
| ) |
|
|
| graph = torch.cuda.CUDAGraph() |
| torch.cuda.synchronize() |
| with torch.cuda.graph(graph): |
| ops.gather_rows_bf16(src, indices, out=gathered) |
| ops.scatter_rows_bf16(gathered, indices, source_rows, out=scattered) |
| graph.replay() |
| torch.cuda.synchronize() |
| expected_gathered = src.index_select(0, indices) |
| expected_scattered = torch.zeros_like(scattered) |
| expected_scattered.index_copy_(0, indices, expected_gathered) |
| torch.testing.assert_close(gathered, expected_gathered, rtol=0.0, atol=0.0) |
| torch.testing.assert_close(scattered, expected_scattered, rtol=0.0, atol=0.0) |
| first = (gathered.clone(), scattered.clone()) |
| graph.replay() |
| torch.cuda.synchronize() |
| torch.testing.assert_close(gathered, first[0], rtol=0.0, atol=0.0) |
| torch.testing.assert_close(scattered, first[1], rtol=0.0, atol=0.0) |
| print("PASS Cosmos3-Edge gather/scatter CUDA Graph replay") |
| return 2 |
|
|
|
|
| def main() -> int: |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--backend", choices=["source", "installed"], default="source") |
| parser.add_argument("--artifact", default=None) |
| parser.add_argument("--mode", choices=["smoke", "full"], default="smoke") |
| args = parser.parse_args() |
| ops = load_source_ops() if args.backend == "source" else load_installed_ops(args.artifact) |
| count = run(ops, args.mode) |
| count += run_indexed_compile(ops) |
| if args.mode == "full": |
| count += run_cosmos_edge_indexed_graph(ops) |
| if args.backend == "installed": |
| count += run_compile_default_eps(ops) |
| print(f"transformer-layout-primitives {args.backend} {args.mode}: passed {count}/{count}") |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| raise SystemExit(main()) |
|
|