#!/usr/bin/env python3 """Test fused RMSNorm + NVFP4 quantize kernel. Validates the fused kernel against the reference (unfused) path: Reference: rmsnorm(x, weight) → quantize_nvfp4_gpu_fused(rmsnormed) Fused: rmsnorm_quantize_nvfp4(x, weight, eps, divisor) → (fp4, sf, gsa, inv_rms) Tests at production shapes: (1, 7168) for decode, (8, 7168) for prefill. """ import torch import math def rmsnorm_ref(x, weight, eps=1e-6): """PyTorch reference RMSNorm.""" xf = x.float() rms = xf.pow(2).mean(dim=-1, keepdim=True).add(eps).rsqrt() return xf * rms * weight.float() def main(): from dsv4.ops.quantize import quantize_nvfp4_gpu_fused, rmsnorm_quantize_nvfp4, dequantize_nvfp4 device = "cuda" torch.manual_seed(42) DIVISOR = 6.0 * 448.0 # same as production EPS = 1e-6 HIDDEN = 7168 # production hidden_size test_configs = [ (1, HIDDEN, "decode T=1"), (8, HIDDEN, "prefill T=8"), (128, HIDDEN, "prefill T=128"), ] all_pass = True for M, N, label in test_configs: print(f"\n=== Test: {label} (M={M}, N={N}) ===") x = torch.randn(M, N, dtype=torch.bfloat16, device=device) weight = torch.randn(N, dtype=torch.float32, device=device) # Ensure weight is positive-ish (norm weights are near 1.0 in practice) weight = weight * 0.1 + 1.0 # Reference path: unfused x_normed = rmsnorm_ref(x, weight, EPS) # (M, N) FP32 x_normed_bf16 = x_normed.bfloat16() # Unfused quantize x_fp4_ref, x_sf_ref, gsa_ref = quantize_nvfp4_gpu_fused(x_normed_bf16, DIVISOR) # Fused path x_fp4_fused, x_sf_fused, gsa_fused, inv_rms_fused = rmsnorm_quantize_nvfp4( x, weight, EPS, DIVISOR ) # Validate shapes assert x_fp4_fused.shape == (M, N // 2), f"fp4 shape: {x_fp4_fused.shape} != {(M, N//2)}" assert x_sf_fused.shape == (M, N // 16), f"sf shape: {x_sf_fused.shape} != {(M, N//16)}" assert gsa_fused.shape == (M,), f"gsa shape: {gsa_fused.shape} != {(M,)}" assert inv_rms_fused.shape == (M,), f"inv_rms shape: {inv_rms_fused.shape} != {(M,)}" # Validate gsa matches # gsa is per-row; unfused computes a scalar from the whole tensor # For T=1 they should match exactly. For T>1, fused is per-row, unfused is scalar. if M == 1: gsa_diff = (gsa_fused[0] - gsa_ref[0]).abs().item() print(f" gsa: fused={gsa_fused[0].item():.6f} ref={gsa_ref[0].item():.6f} diff={gsa_diff:.2e}") if gsa_diff > 1e-3: print(f" FAIL: gsa mismatch") all_pass = False # Dequantize and compare end-to-end dq_fused = dequantize_nvfp4(x_fp4_fused, x_sf_fused, gsa_fused) dq_ref = dequantize_nvfp4(x_fp4_ref, x_sf_ref, gsa_ref) # Compute cosine similarity of dequantized outputs cos = torch.nn.functional.cosine_similarity( dq_fused.float().flatten().unsqueeze(0), dq_ref.float().flatten().unsqueeze(0) ).item() print(f" Dequant cosine (fused vs unfused): {cos:.6f}") # Also compare against the true RMSNorm output cos_vs_ref = torch.nn.functional.cosine_similarity( dq_fused.float().flatten().unsqueeze(0), x_normed.flatten().unsqueeze(0) ).item() print(f" vs true RMSNorm: {cos_vs_ref:.6f}") if cos < 0.995: print(f" FAIL: dequant cosine too low ({cos:.6f})") all_pass = False if cos_vs_ref < 0.990: print(f" FAIL: vs true RMSNorm cosine too low ({cos_vs_ref:.6f})") all_pass = False # Note: fused uses per-row gsa, unfused uses scalar gsa. # Per-row gsa is MORE correct. Small cosine diff (0.996-0.999) is expected # because the quantization scaling differs slightly between the two paths. # The key metric is cos_vs_ref (vs true RMSNorm) which is ~0.994 for both. # Test: unweighted RMSNorm (for q_b norm and kv norm) print("\n=== Test: unweighted RMSNorm (weight=1.0) ===") for M, N, label in [(1, HIDDEN, "decode"), (8, HIDDEN, "prefill")]: x = torch.randn(M, N, dtype=torch.bfloat16, device=device) weight_ones = torch.ones(N, dtype=torch.float32, device=device) x_normed = rmsnorm_ref(x, weight_ones, EPS).bfloat16() x_fp4_ref, x_sf_ref, gsa_ref = quantize_nvfp4_gpu_fused(x_normed, DIVISOR) x_fp4_fused, x_sf_fused, gsa_fused, inv_rms_fused = rmsnorm_quantize_nvfp4( x, weight_ones, EPS, DIVISOR ) dq_fused = dequantize_nvfp4(x_fp4_fused, x_sf_fused, gsa_fused) dq_ref = dequantize_nvfp4(x_fp4_ref, x_sf_ref, gsa_ref) cos = torch.nn.functional.cosine_similarity( dq_fused.float().flatten().unsqueeze(0), dq_ref.float().flatten().unsqueeze(0) ).item() print(f" {label}: cos={cos:.6f}") if cos < 0.995: print(f" FAIL") all_pass = False # Test: validate gsa against per-row reference print("\n=== Test: gsa per-row validation ===") for M, N, label in [(1, HIDDEN, "decode"), (8, HIDDEN, "prefill")]: x = torch.randn(M, N, dtype=torch.bfloat16, device=device) weight = torch.randn(N, dtype=torch.float32, device=device) * 0.1 + 1.0 x_fp4_fused, x_sf_fused, gsa_fused, inv_rms_fused = rmsnorm_quantize_nvfp4( x, weight, EPS, DIVISOR ) max_diff = 0.0 for row in range(M): x_normed_row = rmsnorm_ref(x[row:row+1], weight, EPS) ref_amax = x_normed_row.abs().max().item() ref_gsa = max(ref_amax, 1e-8) / DIVISOR diff = abs(gsa_fused[row].item() - ref_gsa) max_diff = max(max_diff, diff) print(f" {label}: max gsa diff vs reference = {max_diff:.2e}") if max_diff > 0.1: # FP4 quantization + BF16 round-trip introduces some error print(f" FAIL: gsa diff too large") all_pass = False print(f"\n{'='*60}") print(f"{'ALL TESTS PASSED' if all_pass else 'SOME TESTS FAILED'}") return 0 if all_pass else 1 if __name__ == "__main__": exit(main())