Restructure: cutedsl/ -> dsv4/ with proper layering
- Split bridge.py -> ops/quantize.py, ops/layouts.py, ops/gemm_runner.py - Renamed classes: CuTeDSLNvfp4Linear -> Nvfp4Linear, etc. - Moved kernel code to dsv4/kernels/ (gemm, attention, compressor, decode, cuda) - Moved PyTorch bridges to dsv4/ops/ - Moved nn.Module layers to dsv4layers/ - Moved reference implementations to dsv4/reference/ - Moved vendored CUTLASS code to vendored/ - Archived ~190 debug tests to tests/archive/ - Kept ~15 canonical tests in tests/unit/ - Updated all import paths - Added stubs for future components (model/, cache/, loader/) - Updated pyproject.toml: dsv4-inference package name
This commit is contained in:
0
tests/unit/__init__.py
Normal file
0
tests/unit/__init__.py
Normal file
286
tests/unit/cudagraph_test.py
Normal file
286
tests/unit/cudagraph_test.py
Normal file
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin python3
|
||||
"""
|
||||
CUDAGraph compatibility test for CuTeDSL NVFP4 MoE runner.
|
||||
|
||||
Detects CPU-GPU syncs that break cudagraph capture by patching
|
||||
torch CUDA sync functions. Any sync during the forward pass = FAIL.
|
||||
|
||||
Run on the B200:
|
||||
cd /root/nvfp4-megamoe-kernel
|
||||
python3 tests/cudagraph_test.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import torch
|
||||
import contextlib
|
||||
|
||||
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, REPO_ROOT)
|
||||
|
||||
from vllm.nvfp4_cutedsl import Nvfp4MoE
|
||||
|
||||
|
||||
class CUDASyncDetector:
|
||||
"""Context manager that detects CPU-GPU syncs.
|
||||
|
||||
Patches torch.cuda.synchronize, Tensor.item(), Tensor.cpu(),
|
||||
Tensor.numpy(), Tensor.tolist() to raise on call.
|
||||
|
||||
Usage:
|
||||
with CUDASyncDetector() as detector:
|
||||
model.forward(x)
|
||||
print(f"Syncs detected: {detector.sync_count}")
|
||||
"""
|
||||
|
||||
def __init__(self, allow_compile_syncs=True):
|
||||
self.sync_count = 0
|
||||
self.sync_stacktraces = []
|
||||
self.allow_compile_syncs = allow_compile_syncs
|
||||
self._in_compile = False
|
||||
self._originals = {}
|
||||
|
||||
def _make_sync_guard(self, name, original_fn):
|
||||
"""Create a guard function that logs sync attempts."""
|
||||
def guard(*args, **kwargs):
|
||||
import traceback
|
||||
stack = traceback.format_stack()
|
||||
# Skip the guard frame itself
|
||||
stack_str = ''.join(stack[-5:-1])
|
||||
|
||||
self.sync_count += 1
|
||||
self.sync_stacktraces.append((name, stack_str))
|
||||
|
||||
# Still call the original (we want to detect, not block)
|
||||
return original_fn(*args, **kwargs)
|
||||
return guard
|
||||
|
||||
def __enter__(self):
|
||||
# Patch torch.cuda.synchronize
|
||||
self._originals['torch.cuda.synchronize'] = torch.cuda.synchronize
|
||||
torch.cuda.synchronize = self._make_sync_guard(
|
||||
'torch.cuda.synchronize', torch.cuda.synchronize
|
||||
)
|
||||
|
||||
# Patch Tensor.item()
|
||||
self._originals['Tensor.item'] = torch.Tensor.item
|
||||
original_item = torch.Tensor.item
|
||||
def item_guard(self_tensor):
|
||||
import traceback
|
||||
stack = traceback.format_stack()
|
||||
stack_str = ''.join(stack[-5:-1])
|
||||
# Allow if called from _ensure_stacked or prepare_weights (init time)
|
||||
if any(x in stack_str for x in ['_ensure_stacked', 'prepare_weights', 'load_weights', 'warmup']):
|
||||
return original_item(self_tensor)
|
||||
CUDASyncDetector_instance = self # closure capture
|
||||
CUDASyncDetector_instance.sync_count += 1
|
||||
CUDASyncDetector_instance.sync_stacktraces.append(('Tensor.item', stack_str))
|
||||
return original_item(self_tensor)
|
||||
|
||||
# Can't easily patch instance methods this way, use __class__
|
||||
# Actually let's use a simpler approach: wrap the forward method
|
||||
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
# Restore originals
|
||||
torch.cuda.synchronize = self._originals.get('torch.cuda.synchronize', torch.cuda.synchronize)
|
||||
|
||||
def report(self):
|
||||
if self.sync_count == 0:
|
||||
print("✅ No CPU-GPU syncs detected during forward pass")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ {self.sync_count} CPU-GPU sync(s) detected:")
|
||||
for name, stack in self.sync_stacktraces:
|
||||
print(f"\n Sync via {name}:")
|
||||
# Print just the relevant lines
|
||||
for line in stack.split('\n'):
|
||||
line = line.strip()
|
||||
if line and ('nvfp4' in line or 'cutedsl' in line or 'bridge' in line):
|
||||
print(f" {line}")
|
||||
return False
|
||||
|
||||
|
||||
def make_dummy_runner(num_experts=32, hidden_size=7168, intermediate_size=3072, device="cuda"):
|
||||
"""Create a CuTeDSL runner with dummy weights for testing."""
|
||||
runner = Nvfp4MoE(num_experts, hidden_size, intermediate_size, device=device)
|
||||
|
||||
# Create minimal dummy weights
|
||||
# Create minimal dummy weights (uint8 → view as float4)
|
||||
def rand_fp4(*shape, device="cuda"):
|
||||
return torch.randint(0, 256, shape, dtype=torch.uint8, device=device).view(torch.float4_e2m1fn_x2)
|
||||
|
||||
def rand_sf(*shape, device="cuda"):
|
||||
return torch.rand(shape, dtype=torch.float16, device=device).to(torch.float8_e4m3fn)
|
||||
|
||||
l1_fp4 = [rand_fp4(3584, intermediate_size * 2, device=device) for _ in range(num_experts)]
|
||||
l1_sf = [rand_sf(3584 // 16, intermediate_size * 2, device=device) for _ in range(num_experts)]
|
||||
l1_gs = [0.1] * num_experts
|
||||
l2_fp4 = [rand_fp4(1536, hidden_size, device=device) for _ in range(num_experts)]
|
||||
l2_sf = [rand_sf(1536 // 16, hidden_size, device=device) for _ in range(num_experts)]
|
||||
l2_gs = [0.1] * num_experts
|
||||
|
||||
runner.prepare_weights_direct(l1_fp4, l1_sf, l1_gs, l2_fp4, l2_sf, l2_gs)
|
||||
return runner
|
||||
|
||||
|
||||
def test_sync_detection():
|
||||
"""Test that the sync detector actually catches syncs."""
|
||||
print("Testing sync detector...")
|
||||
x = torch.tensor([1.0, 2.0], device="cuda")
|
||||
|
||||
# This should trigger a sync
|
||||
try:
|
||||
val = x.item()
|
||||
print(f" .item() returned {val} (sync detected)")
|
||||
except:
|
||||
print(" .item() blocked (unexpected)")
|
||||
|
||||
# torch.cuda.synchronize
|
||||
torch.cuda.synchronize()
|
||||
print(" torch.cuda.synchronize() works (patched)")
|
||||
print()
|
||||
|
||||
|
||||
def test_cudagraph_capture():
|
||||
"""Test that the runner can be captured in a CUDA graph.
|
||||
|
||||
If any CPU-GPU sync happens during forward, CUDA graph capture
|
||||
will fail with cudaErrorStreamCaptureInvalidated.
|
||||
"""
|
||||
print("=" * 70)
|
||||
print(" CUDA Graph Capture Test")
|
||||
print("=" * 70)
|
||||
|
||||
device = "cuda"
|
||||
num_experts = 4 # Small for testing
|
||||
hidden_size = 7168
|
||||
intermediate_size = 3072
|
||||
num_tokens = 2
|
||||
top_k = 2
|
||||
|
||||
print(f"\n Config: {num_experts} experts, {num_tokens} tokens, top_k={top_k}")
|
||||
|
||||
# Create runner with dummy weights
|
||||
print(" Creating runner with dummy weights...")
|
||||
runner = make_dummy_runner(num_experts, hidden_size, intermediate_size, device)
|
||||
|
||||
# Warmup: trigger _ensure_stacked and kernel compilation
|
||||
print(" Warming up (first forward — compiles kernels)...")
|
||||
hidden_states = torch.randn(num_tokens, hidden_size, dtype=torch.bfloat16, device=device)
|
||||
topk_weights = torch.tensor([[0.6, 0.4]] * num_tokens, dtype=torch.float32, device=device)
|
||||
topk_ids = torch.tensor([[0, 1], [2, 3]], dtype=torch.int32, device=device)
|
||||
|
||||
with torch.no_grad():
|
||||
_ = runner.run(hidden_states, topk_weights, topk_ids)
|
||||
torch.cuda.synchronize()
|
||||
print(" Warmup done ✓")
|
||||
|
||||
# Now test CUDA graph capture
|
||||
print("\n Attempting CUDA graph capture...")
|
||||
|
||||
# Allocate graph and static inputs
|
||||
g = torch.cuda.CUDAGraph()
|
||||
static_hidden = hidden_states.clone()
|
||||
static_weights = topk_weights.clone()
|
||||
static_ids = topk_ids.clone()
|
||||
|
||||
try:
|
||||
with torch.cuda.graph(g):
|
||||
static_output = runner.run(static_hidden, static_weights, static_ids)
|
||||
print(" CUDA graph capture SUCCEEDED ✓")
|
||||
|
||||
# Replay the graph
|
||||
print(" Replaying CUDA graph...")
|
||||
g.replay()
|
||||
torch.cuda.synchronize()
|
||||
print(" CUDA graph replay SUCCEEDED ✓")
|
||||
return True
|
||||
|
||||
except RuntimeError as e:
|
||||
error_msg = str(e)
|
||||
if "capture" in error_msg.lower() or "stream" in error_msg.lower():
|
||||
print(f" CUDA graph capture FAILED ✗")
|
||||
print(f" Error: {error_msg[:200]}")
|
||||
|
||||
# Diagnose: check which operations cause syncs
|
||||
print("\n Diagnosing CPU-GPU syncs in forward pass...")
|
||||
diagnose_syncs(runner, hidden_states, topk_weights, topk_ids)
|
||||
return False
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
def diagnose_syncs(runner, hidden_states, topk_weights, topk_ids):
|
||||
"""Run forward with sync detection to find problematic operations."""
|
||||
import traceback
|
||||
|
||||
sync_log = []
|
||||
|
||||
# Patch sync functions
|
||||
original_sync = torch.cuda.synchronize
|
||||
original_item = torch.Tensor.item
|
||||
original_tolist = torch.Tensor.tolist
|
||||
original_cpu = torch.Tensor.cpu
|
||||
|
||||
def log_sync(name, original_fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
stack = traceback.format_stack()
|
||||
# Find the first frame from our code
|
||||
relevant = []
|
||||
for frame in stack:
|
||||
if 'nvfp4' in frame or 'cutedsl' in frame or 'bridge' in frame:
|
||||
relevant.append(frame.strip())
|
||||
sync_log.append((name, relevant[:3]))
|
||||
return original_fn(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
torch.cuda.synchronize = log_sync('synchronize', original_sync)
|
||||
torch.Tensor.item = log_sync('item', original_item)
|
||||
torch.Tensor.tolist = log_sync('tolist', original_tolist)
|
||||
torch.Tensor.cpu = log_sync('cpu', original_cpu)
|
||||
|
||||
try:
|
||||
with torch.no_grad():
|
||||
_ = runner.run(hidden_states, topk_weights, topk_ids)
|
||||
finally:
|
||||
# Restore
|
||||
torch.cuda.synchronize = original_sync
|
||||
torch.Tensor.item = original_item
|
||||
torch.Tensor.tolist = original_tolist
|
||||
torch.Tensor.cpu = original_cpu
|
||||
|
||||
if sync_log:
|
||||
print(f"\n ❌ Found {len(sync_log)} CPU-GPU sync(s):")
|
||||
for name, frames in sync_log:
|
||||
print(f"\n Sync: {name}")
|
||||
for f in frames:
|
||||
if f:
|
||||
print(f" {f}")
|
||||
else:
|
||||
print("\n No CPU-GPU syncs detected (capture failure must be from kernel launch)")
|
||||
|
||||
|
||||
def main():
|
||||
if not torch.cuda.is_available():
|
||||
print("No CUDA available, skipping")
|
||||
return
|
||||
|
||||
# Test 1: Sync detection framework
|
||||
test_sync_detection()
|
||||
|
||||
# Test 2: CUDA graph capture
|
||||
success = test_cudagraph_capture()
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
if success:
|
||||
print(" ALL TESTS PASSED ✅")
|
||||
else:
|
||||
print(" TESTS FAILED ❌ — fix syncs above before cudagraph will work")
|
||||
print("=" * 70)
|
||||
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
283
tests/unit/layertest.py
Normal file
283
tests/unit/layertest.py
Normal file
@@ -0,0 +1,283 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Layer 0 full MoE pipeline test: CuTeDSL NVFP4 vs BF16 reference.
|
||||
|
||||
Tests the complete pipeline: L1→SiLU→L2→scatter
|
||||
If cosine < 0.99, exits with error.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import glob
|
||||
import torch
|
||||
from safetensors import safe_open
|
||||
|
||||
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, REPO_ROOT)
|
||||
|
||||
from dsv4.reference.moe_pipeline import (
|
||||
run_nvfp4_moe,
|
||||
run_nvfp4_moe_fused,
|
||||
)
|
||||
|
||||
NVFP4_MODEL_DIR = "/root/nvidia-meeting/DeepSeek-V4-Pro-NVFP4"
|
||||
LAYER_IDX = 0
|
||||
DEVICE = "cuda"
|
||||
COSINE_THRESHOLD = 0.98 # Double quantization loss from checkpoint dequant→requant
|
||||
|
||||
E2M1_LUT = torch.tensor([
|
||||
0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0,
|
||||
-0.0, -0.5, -1.0, -1.5, -2.0, -3.0, -4.0, -6.0,
|
||||
], dtype=torch.float32)
|
||||
|
||||
|
||||
def find_shards(model_dir):
|
||||
index_path = os.path.join(model_dir, "model.safetensors.index.json")
|
||||
key_to_shard = {}
|
||||
if os.path.exists(index_path):
|
||||
with open(index_path) as f:
|
||||
index = json.load(f)
|
||||
for key, shard in index["weight_map"].items():
|
||||
key_to_shard[key] = os.path.join(model_dir, shard)
|
||||
else:
|
||||
for sf in glob.glob(os.path.join(model_dir, "*.safetensors")):
|
||||
with safe_open(sf, framework="pt") as f:
|
||||
for key in f.keys():
|
||||
key_to_shard[key] = sf
|
||||
return key_to_shard
|
||||
|
||||
|
||||
def load_layer_tensors(model_dir, layer_idx):
|
||||
key_to_shard = find_shards(model_dir)
|
||||
layer_prefix = f"layers.{layer_idx}."
|
||||
shard_to_keys = {}
|
||||
for key, shard in key_to_shard.items():
|
||||
norm_key = key.removeprefix("model.")
|
||||
if not norm_key.startswith(layer_prefix):
|
||||
continue
|
||||
shard_to_keys.setdefault(shard, []).append((key, norm_key))
|
||||
tensors = {}
|
||||
for shard, keys in shard_to_keys.items():
|
||||
with safe_open(shard, framework="pt") as f:
|
||||
for orig_key, norm_key in keys:
|
||||
tensors[norm_key] = f.get_tensor(orig_key)
|
||||
return tensors
|
||||
|
||||
|
||||
def dequantize_nvfp4_weight(packed_uint8, scale_e4m3, global_scale):
|
||||
device = packed_uint8.device
|
||||
lut = E2M1_LUT.to(device)
|
||||
lower = lut[(packed_uint8 & 0x0F).long()]
|
||||
upper = lut[((packed_uint8 >> 4) & 0x0F).long()]
|
||||
out_features = packed_uint8.shape[0]
|
||||
in_features = packed_uint8.shape[1] * 2
|
||||
unpacked = torch.empty(out_features, in_features, dtype=torch.float32, device=device)
|
||||
unpacked[:, 0::2] = lower
|
||||
unpacked[:, 1::2] = upper
|
||||
block_scale = scale_e4m3.float()
|
||||
block_expanded = block_scale.repeat_interleave(16, dim=1)[:, :in_features]
|
||||
return (unpacked * block_expanded * global_scale).to(torch.bfloat16)
|
||||
|
||||
|
||||
def dequantize_nvfp4_experts(nvfp4_tensors, layer_idx, expert_indices):
|
||||
experts = {}
|
||||
for e in expert_indices:
|
||||
expert = {}
|
||||
for proj in ["gate_proj", "up_proj", "down_proj"]:
|
||||
weight_key = f"layers.{layer_idx}.mlp.experts.{e}.{proj}.weight"
|
||||
scale_key = f"layers.{layer_idx}.mlp.experts.{e}.{proj}.weight_scale"
|
||||
gs_key = f"layers.{layer_idx}.mlp.experts.{e}.{proj}.weight_scale_2"
|
||||
if weight_key not in nvfp4_tensors:
|
||||
if proj == "down_proj" and e == 211:
|
||||
continue
|
||||
raise KeyError(f"Missing {weight_key}")
|
||||
weight = nvfp4_tensors[weight_key].to(DEVICE)
|
||||
scale = nvfp4_tensors[scale_key].to(DEVICE)
|
||||
global_scale = nvfp4_tensors[gs_key].item()
|
||||
expert[proj] = dequantize_nvfp4_weight(weight, scale, global_scale)
|
||||
experts[e] = expert
|
||||
return experts
|
||||
|
||||
|
||||
def moe_forward_bf16(hidden_states, experts, expert_ids, expert_weights):
|
||||
num_tokens, hidden_size = hidden_states.shape
|
||||
top_k = expert_ids.shape[1]
|
||||
output = torch.zeros(num_tokens, hidden_size, dtype=torch.bfloat16, device=DEVICE)
|
||||
for t in range(num_tokens):
|
||||
for k in range(top_k):
|
||||
e = expert_ids[t, k].item()
|
||||
w = expert_weights[t, k].item()
|
||||
if e not in experts:
|
||||
continue
|
||||
x = hidden_states[t]
|
||||
gate = x @ experts[e]["gate_proj"].T
|
||||
up = x @ experts[e]["up_proj"].T
|
||||
activated = torch.nn.functional.silu(gate) * up
|
||||
if "down_proj" in experts[e]:
|
||||
y = activated @ experts[e]["down_proj"].T
|
||||
else:
|
||||
y = activated[:hidden_size]
|
||||
output[t] += w * y
|
||||
return output
|
||||
|
||||
|
||||
def prepare_nvfp4_weights_direct(nvfp4_tensors, layer_idx, expert_indices, intermediate_size):
|
||||
"""Prepare weights via direct view-cast (no BF16 round-trip).
|
||||
|
||||
Checkpoint uint8 → float4_e2m1fn_x2 (byte-preserving).
|
||||
Block scales float8_e4m3fn → used directly.
|
||||
Global scales float32 → used directly.
|
||||
|
||||
For L1 (gate+up fused): normalize dual global scales to max, fold ratio
|
||||
into block scales via float32 (one multiply + float8 round-trip on ratio only).
|
||||
"""
|
||||
l1_fp4, l1_sf, l1_gs = [], [], []
|
||||
l2_fp4, l2_sf, l2_gs = [], [], []
|
||||
|
||||
for e in expert_indices:
|
||||
# L1: gate + up
|
||||
gate_w = nvfp4_tensors[f"layers.{layer_idx}.mlp.experts.{e}.gate_proj.weight"].to(DEVICE)
|
||||
up_w = nvfp4_tensors[f"layers.{layer_idx}.mlp.experts.{e}.up_proj.weight"].to(DEVICE)
|
||||
gate_sf = nvfp4_tensors[f"layers.{layer_idx}.mlp.experts.{e}.gate_proj.weight_scale"].to(DEVICE)
|
||||
up_sf = nvfp4_tensors[f"layers.{layer_idx}.mlp.experts.{e}.up_proj.weight_scale"].to(DEVICE)
|
||||
gate_gs = nvfp4_tensors[f"layers.{layer_idx}.mlp.experts.{e}.gate_proj.weight_scale_2"].item()
|
||||
up_gs = nvfp4_tensors[f"layers.{layer_idx}.mlp.experts.{e}.up_proj.weight_scale_2"].item()
|
||||
|
||||
# Fuse gate+up along N, transpose to K-major
|
||||
fused_w = torch.cat([gate_w, up_w], dim=0) # (2*intermediate, hidden//2) uint8
|
||||
fused_w_fp4 = fused_w.view(torch.float4_e2m1fn_x2).permute(1, 0).contiguous()
|
||||
# (hidden//2, 2*intermediate) — K=hidden packed, N=2*intermediate
|
||||
|
||||
# Fuse block scales: checkpoint is (N, K_sf), bridge expects (K_sf, N)
|
||||
fused_sf = torch.cat([gate_sf, up_sf], dim=0) # (2*intermediate, hidden//16) = (N, K_sf)
|
||||
fused_sf = fused_sf.permute(1, 0).contiguous() # → (K_sf, N)
|
||||
|
||||
# Normalize dual global scales
|
||||
l1_max_gs = max(gate_gs, up_gs)
|
||||
if gate_gs != up_gs:
|
||||
fused_sf_f32 = fused_sf.float()
|
||||
# Gate is first intermediate cols, up is second (after transpose)
|
||||
fused_sf_f32[:, :intermediate_size] *= (gate_gs / l1_max_gs)
|
||||
fused_sf_f32[:, intermediate_size:] *= (up_gs / l1_max_gs)
|
||||
fused_sf = fused_sf_f32.to(torch.float8_e4m3fn)
|
||||
|
||||
l1_fp4.append(fused_w_fp4)
|
||||
l1_sf.append(fused_sf)
|
||||
l1_gs.append(l1_max_gs)
|
||||
|
||||
# L2: down
|
||||
down_key = f"layers.{layer_idx}.mlp.experts.{e}.down_proj.weight"
|
||||
if down_key in nvfp4_tensors:
|
||||
down_w = nvfp4_tensors[down_key].to(DEVICE)
|
||||
down_sf = nvfp4_tensors[f"layers.{layer_idx}.mlp.experts.{e}.down_proj.weight_scale"].to(DEVICE)
|
||||
down_gs = nvfp4_tensors[f"layers.{layer_idx}.mlp.experts.{e}.down_proj.weight_scale_2"].item()
|
||||
|
||||
down_w_fp4 = down_w.view(torch.float4_e2m1fn_x2).permute(1, 0).contiguous()
|
||||
# (intermediate//2, hidden) — K=intermediate packed, N=hidden
|
||||
|
||||
l2_fp4.append(down_w_fp4)
|
||||
l2_sf.append(down_sf.permute(1, 0).contiguous()) # (N, K_sf) → (K_sf, N)
|
||||
l2_gs.append(down_gs)
|
||||
else:
|
||||
# Expert 211 has no down_proj
|
||||
l2_fp4.append(torch.zeros(3072 // 2, 7168, dtype=torch.float4_e2m1fn_x2, device=DEVICE))
|
||||
l2_sf.append(torch.ones(3072 // 16, 7168, dtype=torch.float8_e4m3fn, device=DEVICE)) # (K_sf, N)
|
||||
l2_gs.append(1.0)
|
||||
|
||||
return {
|
||||
'l1_fp4': l1_fp4, 'l1_sf': l1_sf, 'l1_gs': l1_gs,
|
||||
'l2_fp4': l2_fp4, 'l2_sf': l2_sf, 'l2_gs': l2_gs,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
torch.manual_seed(42)
|
||||
expert_indices = [0, 1, 2]
|
||||
top_k = 2
|
||||
num_tokens = 4
|
||||
hidden_size = 7168
|
||||
|
||||
print("=" * 70)
|
||||
print(" Loading NVFP4 checkpoint layer 0")
|
||||
print("=" * 70)
|
||||
|
||||
nvfp4_tensors = load_layer_tensors(NVFP4_MODEL_DIR, LAYER_IDX)
|
||||
print(f" {len(nvfp4_tensors)} tensors loaded")
|
||||
|
||||
# Prepare weights — DIRECT PATH (no BF16 round-trip)
|
||||
print("\n Preparing NVFP4 weights (direct view-cast)...")
|
||||
weights = prepare_nvfp4_weights_direct(nvfp4_tensors, LAYER_IDX, expert_indices, 3072)
|
||||
print(f" L1: {len(weights['l1_fp4'])} experts, shape {weights['l1_fp4'][0].shape}")
|
||||
print(f" L2: {len(weights['l2_fp4'])} experts, shape {weights['l2_fp4'][0].shape}")
|
||||
|
||||
# Dequantize for BF16 reference
|
||||
print("\n Dequantizing NVFP4 -> BF16 reference...")
|
||||
nvfp4_experts_bf16 = dequantize_nvfp4_experts(nvfp4_tensors, LAYER_IDX, expert_indices)
|
||||
|
||||
# Test input
|
||||
hidden_states = torch.randn(num_tokens, hidden_size, dtype=torch.bfloat16, device=DEVICE) * 2.0
|
||||
expert_ids = torch.tensor([[0, 1]] * num_tokens, dtype=torch.int32, device=DEVICE)
|
||||
expert_weights = torch.tensor([[0.6, 0.4]] * num_tokens, dtype=torch.float32, device=DEVICE)
|
||||
|
||||
# BF16 reference
|
||||
print("\n Running BF16 MoE reference...")
|
||||
ref_output = moe_forward_bf16(hidden_states, nvfp4_experts_bf16, expert_ids, expert_weights)
|
||||
print(f" BF16 ref: amax={ref_output.abs().max():.4f} mean={ref_output.float().mean():.6f}")
|
||||
|
||||
del nvfp4_experts_bf16
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
# CuTeDSL NVFP4 pipeline
|
||||
print("\n Running CuTeDSL NVFP4 MoE pipeline (first run compiles)...")
|
||||
kernel_output = run_nvfp4_moe(
|
||||
hidden_states, expert_ids, expert_weights,
|
||||
weights, expert_indices,
|
||||
)
|
||||
print(f" Kernel: amax={kernel_output.abs().max():.4f} mean={kernel_output.float().mean():.6f}")
|
||||
|
||||
# Compare
|
||||
cosine = torch.nn.functional.cosine_similarity(
|
||||
kernel_output.flatten().unsqueeze(0).float(),
|
||||
ref_output.flatten().unsqueeze(0).float(),
|
||||
).item()
|
||||
mse = (kernel_output.float() - ref_output.float()).pow(2).mean().item()
|
||||
|
||||
print(f"\n{'=' * 70}")
|
||||
print(f" RESULT: cosine={cosine:.6f} MSE={mse:.6e}")
|
||||
print(f"{'=' * 70}")
|
||||
|
||||
if cosine < COSINE_THRESHOLD:
|
||||
print(f" FAIL: cosine {cosine:.6f} < {COSINE_THRESHOLD}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f" PASS: cosine {cosine:.6f} >= {COSINE_THRESHOLD}")
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# Fused SwiGLU pipeline test
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
print(f"\n Running CuTeDSL NVFP4 MoE FUSED pipeline (first run compiles)...")
|
||||
fused_output = run_nvfp4_moe_fused(
|
||||
hidden_states, expert_ids, expert_weights,
|
||||
weights, expert_indices,
|
||||
)
|
||||
print(f" Fused: amax={fused_output.abs().max():.4f} mean={fused_output.float().mean():.6f}")
|
||||
|
||||
fused_cosine = torch.nn.functional.cosine_similarity(
|
||||
fused_output.flatten().unsqueeze(0).float(),
|
||||
ref_output.flatten().unsqueeze(0).float(),
|
||||
).item()
|
||||
fused_mse = (fused_output.float() - ref_output.float()).pow(2).mean().item()
|
||||
|
||||
print(f"\n{'=' * 70}")
|
||||
print(f" FUSED RESULT: cosine={fused_cosine:.6f} MSE={fused_mse:.6e}")
|
||||
print(f"{'=' * 70}")
|
||||
|
||||
if fused_cosine < COSINE_THRESHOLD:
|
||||
print(f" FAIL: fused cosine {fused_cosine:.6f} < {COSINE_THRESHOLD}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f" PASS: fused cosine {fused_cosine:.6f} >= {COSINE_THRESHOLD}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
385
tests/unit/test_128_128_vdiag.py
Normal file
385
tests/unit/test_128_128_vdiag.py
Normal file
@@ -0,0 +1,385 @@
|
||||
"""
|
||||
Minimal PV-only test: Load P from GMEM to TMEM via QK-style MMA, then PV from TMEM.
|
||||
Step 1: QK MMA writes FP32 S to TMEM (we know this works)
|
||||
Step 2: Softmax packing writes BF16 P to TMEM (test this)
|
||||
Step 3: PV MMA reads BF16 P from TMEM and V from SMEM, produces O
|
||||
|
||||
But to isolate the bug, let me test just the PV MMA in isolation.
|
||||
I'll write known BF16 values to TMEM using the softmax packing path,
|
||||
then immediately read them back using the PV A-fragment path,
|
||||
and compare.
|
||||
|
||||
Actually, the simplest isolation test:
|
||||
1. Do QK MMA to get S in TMEM (cosine 0.999999 verified)
|
||||
2. Do softmax packing: S → P in TMEM (at offset 32)
|
||||
3. Skip PV entirely — read P from TMEM using the C-fragment composition LOAD path
|
||||
4. Output P to GMEM and compare against S.to(BF16)
|
||||
|
||||
This tests whether the softmax packing writes P correctly to the same TMEM
|
||||
that the PV would read from.
|
||||
|
||||
But we can't easily read P from TMEM using the standard epilogue path
|
||||
because the epilogue expects FP32 accumulator data.
|
||||
|
||||
Alternative: Use the PV MMA with V=I (identity). If P is correct,
|
||||
then P @ I = P. But V needs to be MN-major and (128, 128), not (128, 64).
|
||||
The output would be (128, 128) which doesn't match our (128, 64) c tensor.
|
||||
|
||||
Let me use V that selects the first 64 columns: V[k, n] = delta(k, n) for k in [0,63].
|
||||
This gives P @ V = P[:, :64], and the output is (128, 64).
|
||||
But V is (128, 128) in the MMA K,N dims. V[k, n] for k in [0,127], n in [0,63].
|
||||
Hmm, this is getting complicated. Let me just do the identity approach with a (128, 128) output.
|
||||
"""
|
||||
import torch, cutlass, cutlass.cute as cute, cutlass.utils as utils, cutlass.pipeline as pipeline
|
||||
from cutlass.cute.nvgpu import cpasync, tcgen05
|
||||
from cutlass import Float32, BFloat16, Int32, Boolean, const_expr
|
||||
from cutlass.utils import LayoutEnum
|
||||
from cutlass.utils.tmem_allocator import find_tmem_tensor_col_offset
|
||||
import cuda.bindings.driver as cuda
|
||||
import cutlass.torch as ct
|
||||
|
||||
|
||||
class VDiag128:
|
||||
"""QK + softmax packing + PV with V=I to isolate PV MMA correctness.
|
||||
Output should be P = S.to(BF16), i.e. (Q@K^T).bfloat16()
|
||||
With V=I, O = P @ I = P.
|
||||
But V is (K=128, N=128) in the MMA. We need a 128x128 identity in MN-major.
|
||||
Output tensor is (128, 128).
|
||||
"""
|
||||
def __init__(self, mma_tiler_mn):
|
||||
self.acc_dtype = Float32; self.qk_acc_dtype = Float32
|
||||
self.q_dtype = BFloat16; self.o_dtype = BFloat16; self.c_dtype = BFloat16
|
||||
self.mma_tiler_mn = mma_tiler_mn; self.mma_tiler = (*mma_tiler_mn, 1)
|
||||
self.use_2cta_instrs = False # needed by epilogue_tma_store
|
||||
self.epilog_sync_bar_id = 1 # needed by epilogue_tma_store
|
||||
self.cluster_shape_mn = (1, 1)
|
||||
self.cta_group = tcgen05.CtaGroup.ONE
|
||||
self.epilogue_warp_id = (0, 1, 2, 3)
|
||||
self.mma_warp_id = 4; self.tma_warp_id = 5
|
||||
self.threads_per_cta = 192
|
||||
self.num_c_stage = 2
|
||||
|
||||
def _setup(self, qk_mma, pv_mma):
|
||||
qk_inst_k = cute.size(qk_mma.shape_mnk, mode=[2])
|
||||
self.qk_mma_tiler = (*self.mma_tiler_mn, qk_inst_k * 4)
|
||||
# PV with V=I: output is (128, 128), same as QK
|
||||
self.pv_mma_tiler = (self.qk_mma_tiler[0], self.qk_mma_tiler[1], self.qk_mma_tiler[1])
|
||||
# pv_mma_tiler = (128, 128, 128) since V is 128x128
|
||||
self.mma_tiler = self.qk_mma_tiler
|
||||
|
||||
self.cluster_layout_vmnk = cute.tiled_divide(cute.make_layout((1,1,1)), (qk_mma.thr_id.shape,))
|
||||
self.cta_tile_shape_mnk = (
|
||||
self.qk_mma_tiler[0] // cute.size(qk_mma.thr_id.shape),
|
||||
self.qk_mma_tiler[1], self.qk_mma_tiler[2])
|
||||
self.c_layout = LayoutEnum.ROW_MAJOR
|
||||
self.epi_tile = utils.sm100.compute_epilogue_tile_shape(
|
||||
self.cta_tile_shape_mnk, False, self.c_layout, self.o_dtype)
|
||||
self.num_ab_stage = 1; self.num_acc_stage = 1
|
||||
|
||||
self.a_smem_s = utils.sm100.make_smem_layout_a(qk_mma, self.mma_tiler, self.q_dtype, 1)
|
||||
self.b_smem_s = utils.sm100.make_smem_layout_b(qk_mma, self.mma_tiler, self.q_dtype, 1)
|
||||
self.v_smem_s = utils.sm100.make_smem_layout_b(pv_mma, self.pv_mma_tiler, self.q_dtype, 1)
|
||||
self.p_tmem_s = utils.sm100.make_smem_layout_a(pv_mma, self.pv_mma_tiler, self.q_dtype, 1)
|
||||
self.c_smem_s = utils.sm100.make_smem_layout_epi(self.o_dtype, self.c_layout, self.epi_tile, 2)
|
||||
|
||||
qk_thr = qk_mma.get_slice(0)
|
||||
qk_acc_shape = qk_thr.partition_shape_C(self.qk_mma_tiler[:2])
|
||||
tStS = qk_thr.make_fragment_C(qk_acc_shape)
|
||||
s_cols = find_tmem_tensor_col_offset(tStS)
|
||||
pv_thr = pv_mma.get_slice(0)
|
||||
pv_acc_shape = pv_thr.partition_shape_C(self.pv_mma_tiler[:2])
|
||||
tOtO = pv_thr.make_fragment_C(pv_acc_shape)
|
||||
o_cols = find_tmem_tensor_col_offset(tOtO)
|
||||
|
||||
self.tilePlikeFP32 = self.qk_mma_tiler[1] // Float32.width * self.o_dtype.width
|
||||
self.tmem_s0_offset = 0
|
||||
self.tmem_p0_offset = 32
|
||||
self.tmem_o0_offset = s_cols
|
||||
|
||||
tCtS_fake = qk_mma.make_fragment_C(cute.append(qk_acc_shape, self.num_acc_stage))
|
||||
tCtO_fake = pv_mma.make_fragment_C(cute.append(pv_acc_shape, self.num_acc_stage))
|
||||
self.num_tmem_alloc_cols = utils.get_num_tmem_alloc_cols([tCtS_fake, tCtO_fake], arch="sm_100")
|
||||
|
||||
# ⛔⛔⛔ CRITICAL: num_tma_load_bytes MUST include ALL TMA-loaded tensors (Q + K + V). Missing V → DEADLOCK. See FOOTGUN #0 in README.
|
||||
a_smem = cute.slice_(self.a_smem_s, (None, None, None, 0))
|
||||
b_smem = cute.slice_(self.b_smem_s, (None, None, None, 0))
|
||||
v_smem = cute.slice_(self.v_smem_s, (None, None, None, 0))
|
||||
self.num_tma_load_bytes = (
|
||||
cute.size_in_bytes(self.q_dtype, a_smem) + cute.size_in_bytes(self.q_dtype, b_smem) +
|
||||
cute.size_in_bytes(self.q_dtype, v_smem)
|
||||
) * cute.size(qk_mma.thr_id.shape)
|
||||
|
||||
@cute.jit
|
||||
def __call__(self, q, k, v, c, stream):
|
||||
self.q_dtype = q.element_type; self.o_dtype = c.element_type; self.c_dtype = self.o_dtype
|
||||
self.a_major = LayoutEnum.from_tensor(q).mma_major_mode()
|
||||
self.b_major = LayoutEnum.from_tensor(k).mma_major_mode()
|
||||
self.v_major = LayoutEnum.from_tensor(v).mma_major_mode()
|
||||
self.c_layout = LayoutEnum.from_tensor(c)
|
||||
|
||||
qk_mma = utils.sm100.make_trivial_tiled_mma(
|
||||
self.q_dtype, self.q_dtype, self.a_major, self.b_major,
|
||||
self.qk_acc_dtype, self.cta_group, self.mma_tiler_mn, tcgen05.OperandSource.SMEM)
|
||||
# PV with 128x128 output (V=I)
|
||||
pv_mma = utils.sm100.make_trivial_tiled_mma(
|
||||
self.q_dtype, self.q_dtype, cute.nvgpu.OperandMajorMode.K, self.v_major,
|
||||
self.qk_acc_dtype, self.cta_group, self.mma_tiler_mn, tcgen05.OperandSource.TMEM)
|
||||
self._setup(qk_mma, pv_mma)
|
||||
|
||||
q_smem = cute.slice_(self.a_smem_s, (None, None, None, 0))
|
||||
k_smem = cute.slice_(self.b_smem_s, (None, None, None, 0))
|
||||
v_smem = cute.slice_(self.v_smem_s, (None, None, None, 0))
|
||||
tma_q, tma_tq = cute.nvgpu.make_tiled_tma_atom_A(
|
||||
utils.sm100.cluster_shape_to_tma_atom_A(self.cluster_shape_mn, qk_mma.thr_id),
|
||||
q, q_smem, self.mma_tiler, qk_mma, self.cluster_layout_vmnk.shape)
|
||||
tma_k, tma_tk = cute.nvgpu.make_tiled_tma_atom_B(
|
||||
utils.sm100.cluster_shape_to_tma_atom_B(self.cluster_shape_mn, qk_mma.thr_id),
|
||||
k, k_smem, self.mma_tiler, qk_mma, self.cluster_layout_vmnk.shape)
|
||||
tma_v, tma_tv = cute.nvgpu.make_tiled_tma_atom_B(
|
||||
utils.sm100.cluster_shape_to_tma_atom_B(self.cluster_shape_mn, pv_mma.thr_id),
|
||||
v, v_smem, self.pv_mma_tiler, pv_mma, self.cluster_layout_vmnk.shape)
|
||||
epi_smem = cute.select(self.c_smem_s, mode=[0, 1])
|
||||
tma_c, tma_tc = cpasync.make_tiled_tma_atom(cpasync.CopyBulkTensorTileS2GOp(), c, epi_smem, self.epi_tile)
|
||||
|
||||
self._kernel(qk_mma, pv_mma, tma_q, tma_tq, tma_k, tma_tk, tma_v, tma_tv,
|
||||
tma_c, tma_tc, self.cluster_layout_vmnk,
|
||||
self.a_smem_s, self.b_smem_s, self.v_smem_s, self.p_tmem_s, self.c_smem_s, self.epi_tile
|
||||
).launch(grid=(1,1,1), block=[self.threads_per_cta,1,1], stream=stream)
|
||||
|
||||
@cute.kernel
|
||||
def _kernel(self, qk_mma, pv_mma, tma_q, mQ, tma_k, mK, tma_v, mV,
|
||||
tma_c, mC, cl_vmnk, a_smem_s, b_smem_s, v_smem_s, p_tmem_s, c_smem_s, epi_tile):
|
||||
warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
|
||||
tidx, _, _ = cute.arch.thread_idx()
|
||||
use_2cta = cute.size(qk_mma.thr_id.shape) == 2
|
||||
|
||||
if warp_idx == self.tma_warp_id:
|
||||
cpasync.prefetch_descriptor(tma_q); cpasync.prefetch_descriptor(tma_k)
|
||||
cpasync.prefetch_descriptor(tma_v); cpasync.prefetch_descriptor(tma_c)
|
||||
|
||||
@cute.struct
|
||||
class SS:
|
||||
ab_bar: cute.struct.MemRange[cutlass.Int64, self.num_ab_stage * 2]
|
||||
mma_si_bar: cute.struct.MemRange[cutlass.Int64, 2]
|
||||
acc_bar: cute.struct.MemRange[cutlass.Int64, self.num_acc_stage * 2]
|
||||
tmem_dealloc: cutlass.Int64
|
||||
holding: cutlass.Int32
|
||||
|
||||
smem = utils.SmemAllocator(); st = smem.allocate(SS)
|
||||
|
||||
ab_p, ab_c = pipeline.PipelineTmaUmma.create(
|
||||
barrier_storage=st.ab_bar.data_ptr(), num_stages=self.num_ab_stage,
|
||||
producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),
|
||||
consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread, 1),
|
||||
tx_count=self.num_tma_load_bytes, cta_layout_vmnk=cl_vmnk, defer_sync=True
|
||||
).make_participants()
|
||||
|
||||
mma_si_prod, mma_si_cons = pipeline.PipelineUmmaAsync.create(
|
||||
barrier_storage=st.mma_si_bar.data_ptr(), num_stages=1,
|
||||
producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),
|
||||
consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread, 32 * len(self.epilogue_warp_id)),
|
||||
).make_participants()
|
||||
|
||||
acc_pipe = pipeline.PipelineUmmaAsync.create(
|
||||
barrier_storage=st.acc_bar.data_ptr(), num_stages=self.num_acc_stage,
|
||||
producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),
|
||||
consumer_group=pipeline.CooperativeGroup(
|
||||
pipeline.Agent.Thread, len(self.epilogue_warp_id) * (2 if use_2cta else 1)),
|
||||
cta_layout_vmnk=cl_vmnk, defer_sync=True)
|
||||
|
||||
tmem_bar = pipeline.NamedBarrier(barrier_id=2,
|
||||
num_threads=32 * len((self.mma_warp_id, *self.epilogue_warp_id)))
|
||||
tmem = utils.TmemAllocator(st.holding.ptr, barrier_for_retrieve=tmem_bar,
|
||||
allocator_warp_id=self.epilogue_warp_id[0], is_two_cta=use_2cta,
|
||||
two_cta_tmem_dealloc_mbar_ptr=st.tmem_dealloc.ptr)
|
||||
|
||||
pipeline.pipeline_init_arrive(cluster_shape_mn=cl_vmnk, is_relaxed=True)
|
||||
|
||||
sQ = smem.allocate_tensor(element_type=self.q_dtype, layout=a_smem_s.outer, byte_alignment=128, swizzle=a_smem_s.inner)
|
||||
sK = smem.allocate_tensor(element_type=self.q_dtype, layout=b_smem_s.outer, byte_alignment=128, swizzle=b_smem_s.inner)
|
||||
sV = smem.allocate_tensor(element_type=self.q_dtype, layout=v_smem_s.outer, byte_alignment=128, swizzle=v_smem_s.inner)
|
||||
sC = smem.allocate_tensor(element_type=self.o_dtype, layout=c_smem_s.outer, byte_alignment=128, swizzle=c_smem_s.inner)
|
||||
|
||||
gQ = cute.local_tile(mQ, cute.slice_(self.qk_mma_tiler, (None,0,None)), (None,None,None))
|
||||
gK = cute.local_tile(mK, cute.slice_(self.qk_mma_tiler, (0,None,None)), (None,None,None))
|
||||
gC = cute.local_tile(mC, cute.slice_(self.qk_mma_tiler, (None,None,0)), (None,None,None))
|
||||
k_cnt = cute.size(gQ, mode=[3])
|
||||
|
||||
qk_thr = qk_mma.get_slice(0)
|
||||
pv_thr = pv_mma.get_slice(0)
|
||||
tCgQ = qk_thr.partition_A(gQ); tCgK = qk_thr.partition_B(gK); tCgC = qk_thr.partition_C(gC)
|
||||
a_lay = cute.make_layout(cute.slice_(cl_vmnk, (0,0,None,0)).shape)
|
||||
tAsQ, tAgQ = cpasync.tma_partition(tma_q, 0, a_lay, cute.group_modes(sQ,0,3), cute.group_modes(tCgQ,0,3))
|
||||
b_lay = cute.make_layout(cute.slice_(cl_vmnk, (0,None,0,0)).shape)
|
||||
tBsK, tBgK = cpasync.tma_partition(tma_k, 0, b_lay, cute.group_modes(sK,0,3), cute.group_modes(tCgK,0,3))
|
||||
tAgQ = tAgQ[(None,0,None,0)]; tBgK = tBgK[(None,0,None,0)]
|
||||
|
||||
gV = cute.local_tile(mV, cute.slice_(self.pv_mma_tiler, (0,None,None)), (None,None,None))
|
||||
tCgV = pv_thr.partition_B(gV)
|
||||
tVsV, tVgV = cpasync.tma_partition(tma_v, 0, b_lay, cute.group_modes(sV,0,3), cute.group_modes(tCgV,0,3))
|
||||
tVgV = tVgV[(None,0,None,0)]
|
||||
|
||||
tCrQ = qk_mma.make_fragment_A(sQ); tCrK = qk_mma.make_fragment_B(sK)
|
||||
tCrV = pv_mma.make_fragment_B(sV)
|
||||
|
||||
qk_acc_shape = qk_thr.partition_shape_C(self.qk_mma_tiler[:2])
|
||||
tStS = qk_thr.make_fragment_C(qk_acc_shape)
|
||||
tStS0 = cute.make_tensor(tStS.iterator + self.tmem_s0_offset, tStS.layout)
|
||||
|
||||
pv_acc_shape = pv_thr.partition_shape_C(self.pv_mma_tiler[:2])
|
||||
tOtO = pv_thr.make_fragment_C(pv_acc_shape)
|
||||
tOtO0 = cute.make_tensor(tOtO.iterator + self.tmem_o0_offset, tOtO.layout)
|
||||
|
||||
tP = cute.make_tensor(tStS.iterator, p_tmem_s.outer)
|
||||
tOrP_base = pv_thr.make_fragment_A(tP)
|
||||
tOrP = tOrP_base[(None, None, None, 0)]
|
||||
tOrP0 = cute.make_tensor(
|
||||
tOrP.iterator + self.qk_acc_dtype.width // self.q_dtype.width * self.tmem_p0_offset,
|
||||
tOrP.layout)
|
||||
|
||||
tCtS_fake = qk_mma.make_fragment_C(cute.append(qk_acc_shape, self.num_acc_stage))
|
||||
tCtO_fake = pv_mma.make_fragment_C(cute.append(pv_acc_shape, self.num_acc_stage))
|
||||
|
||||
pipeline.pipeline_init_wait(cluster_shape_mn=cl_vmnk)
|
||||
|
||||
# ═══ TMA LOAD WARP ═══
|
||||
if warp_idx == self.tma_warp_id:
|
||||
ab_p.reset(); peek = ab_p.try_acquire()
|
||||
for kt in cutlass.range(k_cnt, unroll=1):
|
||||
h = ab_p.acquire_and_advance(peek)
|
||||
cute.copy(tma_q, tAgQ[(None,h.count)], tAsQ[(None,h.index)], tma_bar_ptr=h.barrier)
|
||||
cute.copy(tma_k, tBgK[(None,h.count)], tBsK[(None,h.index)], tma_bar_ptr=h.barrier)
|
||||
cute.copy(tma_v, tVgV[(None,h.count)], tVsV[(None,h.index)], tma_bar_ptr=h.barrier)
|
||||
peek = cutlass.Boolean(1)
|
||||
if h.count+1<k_cnt: peek = ab_p.try_acquire()
|
||||
ab_p.tail()
|
||||
|
||||
# ═══ MMA WARP ═══
|
||||
if warp_idx == self.mma_warp_id:
|
||||
tmem.wait_for_alloc()
|
||||
ab_c.reset(); peek = ab_c.try_wait()
|
||||
s0_handle = mma_si_prod.acquire_and_advance()
|
||||
|
||||
acc_prod_st = pipeline.make_pipeline_state(pipeline.PipelineUserType.Producer, self.num_acc_stage)
|
||||
acc_pipe.producer_acquire(acc_prod_st)
|
||||
qk_mma.set(tcgen05.Field.ACCUMULATE, False)
|
||||
for kt in range(k_cnt):
|
||||
h = ab_c.wait_and_advance(peek)
|
||||
nblk = cute.size(tCrQ, mode=[2])
|
||||
for kb in cutlass.range(nblk, unroll_full=True):
|
||||
cute.gemm(qk_mma, tStS0, tCrQ[(None,None,kb,h.index)], tCrK[(None,None,kb,h.index)], tStS0)
|
||||
qk_mma.set(tcgen05.Field.ACCUMULATE, True)
|
||||
h.release(); peek = cutlass.Boolean(1)
|
||||
if h.count+1<k_cnt: peek = ab_c.try_wait()
|
||||
|
||||
cute.arch.fence_view_async_tmem_store()
|
||||
s0_handle.commit()
|
||||
s0_handle = mma_si_prod.acquire_and_advance()
|
||||
|
||||
# PV MMA: P @ V where V=I → O = P
|
||||
pv_mma.set(tcgen05.Field.ACCUMULATE, False)
|
||||
tCrV_s = tCrV[(None, None, None, 0)]
|
||||
nblk_pv = cute.size(tOrP0, mode=[2])
|
||||
if cute.arch.thread_idx()[0] == 0:
|
||||
print(f"PV: nblk={int(nblk_pv)} tCrV_s_size={int(cute.size(tCrV_s))} tOrP0_size={int(cute.size(tOrP0))}")
|
||||
for kb in cutlass.range(nblk_pv, unroll_full=True):
|
||||
cute.gemm(pv_mma, tOtO0, tOrP0[(None,None,kb)], tCrV_s[(None,None,kb)], tOtO0)
|
||||
pv_mma.set(tcgen05.Field.ACCUMULATE, True)
|
||||
|
||||
acc_pipe.producer_commit(acc_prod_st)
|
||||
acc_prod_st.advance()
|
||||
acc_pipe.producer_tail(acc_prod_st)
|
||||
|
||||
# ═══ EPILOGUE WARPS ═══
|
||||
if warp_idx < self.mma_warp_id:
|
||||
tmem.allocate(self.num_tmem_alloc_cols)
|
||||
tmem.wait_for_alloc()
|
||||
tmem_ptr = tmem.retrieve_ptr(self.qk_acc_dtype)
|
||||
sfw_idx = tidx % (32 * len(self.epilogue_warp_id))
|
||||
|
||||
tmem_load_atom = cute.make_copy_atom(
|
||||
tcgen05.copy.Ld32x32bOp(tcgen05.copy.Repetition(32)), self.qk_acc_dtype)
|
||||
tiled_tmem_load = tcgen05.make_tmem_copy(tmem_load_atom, tStS0)
|
||||
thr_load = tiled_tmem_load.get_slice(sfw_idx)
|
||||
tTMEM_LOADtS = thr_load.partition_S(tStS0)
|
||||
cS = cute.make_identity_tensor((self.qk_mma_tiler[0], self.qk_mma_tiler[1]))
|
||||
tScS = qk_thr.partition_C(cS)
|
||||
tTMEM_LOADcS = thr_load.partition_D(tScS)
|
||||
|
||||
tStS_P_layout = cute.composition(tStS.layout, cute.make_layout((128, self.tilePlikeFP32)))
|
||||
tStS_P = cute.make_tensor(tStS.iterator + self.tmem_p0_offset, tStS_P_layout)
|
||||
tmem_store_atom = cute.make_copy_atom(
|
||||
tcgen05.copy.St32x32bOp(tcgen05.copy.Repetition(32)), self.qk_acc_dtype)
|
||||
tiled_tmem_store = tcgen05.make_tmem_copy(tmem_store_atom, tStS_P)
|
||||
thr_store = tiled_tmem_store.get_slice(sfw_idx)
|
||||
tTMEM_STOREtS_x4 = thr_store.partition_D(tStS_P)
|
||||
tScS_P_layout = cute.composition(tScS.layout, cute.make_layout((128, self.tilePlikeFP32)))
|
||||
tScS_P = cute.make_tensor(tScS.iterator, tScS_P_layout)
|
||||
tTMEM_STOREcS = thr_store.partition_S(tScS_P)
|
||||
|
||||
si_handle = mma_si_cons.wait_and_advance()
|
||||
|
||||
tTMEM_LOADrS = cute.make_rmem_tensor(tTMEM_LOADcS.shape, self.qk_acc_dtype)
|
||||
cute.copy(tiled_tmem_load, tTMEM_LOADtS, tTMEM_LOADrS)
|
||||
tTMEM_STORErS_x4 = cute.make_rmem_tensor(tTMEM_STOREcS.shape, self.qk_acc_dtype)
|
||||
tTMEM_STORErS_x4_e = cute.make_tensor(
|
||||
cute.recast_ptr(tTMEM_STORErS_x4.iterator, dtype=self.q_dtype),
|
||||
tTMEM_LOADrS.layout)
|
||||
frg_cnt = 4
|
||||
frg_tile = cute.size(tTMEM_LOADrS) // frg_cnt
|
||||
tTMEM_LOADrS_frg = cute.logical_divide(tTMEM_LOADrS, cute.make_layout(frg_tile))
|
||||
tTMEM_STORErS_x4_e_frg = cute.logical_divide(
|
||||
tTMEM_STORErS_x4_e, cute.make_layout(frg_tile))
|
||||
for j in range(frg_cnt):
|
||||
s_vec = tTMEM_LOADrS_frg[None, j].load()
|
||||
tTMEM_STORErS_x4_e_frg[None, j].store(s_vec.to(self.q_dtype))
|
||||
cute.copy(tiled_tmem_store, tTMEM_STORErS_x4, tTMEM_STOREtS_x4)
|
||||
cute.arch.fence_view_async_tmem_store()
|
||||
si_handle.release()
|
||||
|
||||
# Output epilogue
|
||||
tCtO_base = cute.make_tensor(tmem_ptr + self.tmem_o0_offset, tCtO_fake.layout)
|
||||
acc_cons_st = pipeline.make_pipeline_state(pipeline.PipelineUserType.Consumer, self.num_acc_stage)
|
||||
c_grp = pipeline.CooperativeGroup(pipeline.Agent.Thread, 32 * len(self.epilogue_warp_id))
|
||||
c_pipe = pipeline.PipelineTmaStore.create(num_stages=self.num_c_stage, producer_group=c_grp)
|
||||
acc_cons_st = utils.gemm.sm100.epilogue_tma_store(
|
||||
self, tidx, warp_idx, tma_c, tCtO_base, sC, tCgC,
|
||||
epi_tile, 0, const_expr(lambda x: x), (0,0,0), acc_cons_st, acc_pipe, c_pipe)
|
||||
c_pipe.producer_tail()
|
||||
tmem.relinquish_alloc_permit()
|
||||
tmem.free(tmem_ptr)
|
||||
|
||||
|
||||
def test():
|
||||
torch.manual_seed(42)
|
||||
m, n, head_dim = 128, 128, 64
|
||||
q = torch.randn(m, head_dim, 1, dtype=torch.bfloat16, device='cuda')
|
||||
k = torch.randn(n, head_dim, 1, dtype=torch.bfloat16, device='cuda')
|
||||
# V = identity (128x128) in MN-major: (128,128) with strides (1,128)
|
||||
v = torch.eye(128, dtype=torch.bfloat16, device='cuda')
|
||||
# MN-major: (128,128) with strides (1,128) — row is fast dim
|
||||
v = v.as_strided((128, 128), (1, 128)).unsqueeze(-1)
|
||||
c = torch.zeros(m, n, 1, dtype=torch.bfloat16, device='cuda')
|
||||
|
||||
qf = q[:,:,0].float(); kf = k[:,:,0].float()
|
||||
# With V=I and identity softmax: O = (Q@K^T).bf16() @ I = (Q@K^T).bf16()
|
||||
ref = (qf @ kf.T).bfloat16().float()
|
||||
|
||||
mQ = ct.from_dlpack(q).mark_layout_dynamic(leading_dim=ct.get_leading_dim(q))
|
||||
mK = ct.from_dlpack(k).mark_layout_dynamic(leading_dim=ct.get_leading_dim(k))
|
||||
mV = ct.from_dlpack(v).mark_layout_dynamic(leading_dim=ct.get_leading_dim(v))
|
||||
mC = ct.from_dlpack(c).mark_layout_dynamic(leading_dim=ct.get_leading_dim(c))
|
||||
stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream)
|
||||
kernel = VDiag128(mma_tiler_mn=(128, 128))
|
||||
print('Compiling...', flush=True)
|
||||
compiled = cute.compile(kernel, mQ, mK, mV, mC, stream)
|
||||
print('Running...', flush=True)
|
||||
compiled(mQ, mK, mV, mC, stream)
|
||||
torch.cuda.synchronize()
|
||||
out = c[:,:,0].float()
|
||||
cos = torch.nn.functional.cosine_similarity(out.flatten().unsqueeze(0), ref.flatten().unsqueeze(0)).item()
|
||||
print('VDiag(128,128): cosine {:.6f} {}'.format(cos, 'PASS' if cos >= 0.99 else 'FAIL'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
||||
189
tests/unit/test_compile_custom_op.py
Normal file
189
tests/unit/test_compile_custom_op.py
Normal file
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test torch.compile + CuTeDSL NVFP4 runner via custom_op.
|
||||
|
||||
Critical test: does torch.compile (fullgraph mode) accept the
|
||||
nvfp4::moe_gemm custom op and produce a working compiled graph?
|
||||
|
||||
Run on the B200:
|
||||
docker run --rm --gpus all --entrypoint python3 \
|
||||
-v /root/nvfp4-megamoe-kernel:/root/nvfp4-megamoe-kernel \
|
||||
-v /root/nvidia-meeting:/root/nvidia-meeting:ro \
|
||||
nvfp4-megamoe-kernel-vllm:latest \
|
||||
/root/nvfp4-megamoe-kernel/tests/test_compile_custom_op.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import glob
|
||||
import torch
|
||||
from safetensors import safe_open
|
||||
|
||||
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, REPO_ROOT)
|
||||
|
||||
from dsv4.layers.moe import Nvfp4MoE
|
||||
from dsv4.ops.custom_ops import register_runner, nvfp4_moe_gemm
|
||||
|
||||
NVFP4_MODEL_DIR = "/root/nvidia-meeting/DeepSeek-V4-Pro-NVFP4"
|
||||
DEVICE = "cuda"
|
||||
|
||||
|
||||
def find_shards(model_dir):
|
||||
index_path = os.path.join(model_dir, "model.safetensors.index.json")
|
||||
key_to_shard = {}
|
||||
if os.path.exists(index_path):
|
||||
with open(index_path) as f:
|
||||
index = json.load(f)
|
||||
for key, shard in index["weight_map"].items():
|
||||
key_to_shard[key] = os.path.join(model_dir, shard)
|
||||
else:
|
||||
for sf in glob.glob(os.path.join(model_dir, "*.safetensors")):
|
||||
with safe_open(sf, framework="pt") as f:
|
||||
for key in f.keys():
|
||||
key_to_shard[key] = sf
|
||||
return key_to_shard
|
||||
|
||||
|
||||
def load_layer_tensors(model_dir, layer_idx):
|
||||
key_to_shard = find_shards(model_dir)
|
||||
layer_prefix = f"layers.{layer_idx}."
|
||||
shard_to_keys = {}
|
||||
for key, shard in key_to_shard.items():
|
||||
norm_key = key.removeprefix("model.")
|
||||
if not norm_key.startswith(layer_prefix):
|
||||
continue
|
||||
shard_to_keys.setdefault(shard, []).append((key, norm_key))
|
||||
tensors = {}
|
||||
for shard, keys in shard_to_keys.items():
|
||||
with safe_open(shard, framework="pt") as f:
|
||||
for orig_key, norm_key in keys:
|
||||
tensors[norm_key] = f.get_tensor(orig_key)
|
||||
return tensors
|
||||
|
||||
|
||||
def prepare_nvfp4_weights_direct(nvfp4_tensors, layer_idx, expert_indices, intermediate_size):
|
||||
from dsv4.ops.quantize import (
|
||||
quantize_activation_nvfp4,
|
||||
quantize_weight_to_nvfp4,
|
||||
)
|
||||
l1_fp4, l1_sf, l1_gs = [], [], []
|
||||
l2_fp4, l2_sf, l2_gs = [], [], []
|
||||
|
||||
for e in expert_indices:
|
||||
gate_w = nvfp4_tensors[f"layers.{layer_idx}.mlp.experts.{e}.gate_proj.weight"].to(DEVICE)
|
||||
up_w = nvfp4_tensors[f"layers.{layer_idx}.mlp.experts.{e}.up_proj.weight"].to(DEVICE)
|
||||
gate_sf = nvfp4_tensors[f"layers.{layer_idx}.mlp.experts.{e}.gate_proj.weight_scale"].to(DEVICE)
|
||||
up_sf = nvfp4_tensors[f"layers.{layer_idx}.mlp.experts.{e}.up_proj.weight_scale"].to(DEVICE)
|
||||
gate_gs = nvfp4_tensors[f"layers.{layer_idx}.mlp.experts.{e}.gate_proj.weight_scale_2"].item()
|
||||
up_gs = nvfp4_tensors[f"layers.{layer_idx}.mlp.experts.{e}.up_proj.weight_scale_2"].item()
|
||||
|
||||
fused_w = torch.cat([gate_w, up_w], dim=0)
|
||||
fused_w_fp4 = fused_w.view(torch.float4_e2m1fn_x2).permute(1, 0).contiguous()
|
||||
fused_sf = torch.cat([gate_sf, up_sf], dim=0).permute(1, 0).contiguous()
|
||||
l1_max_gs = max(gate_gs, up_gs)
|
||||
if gate_gs != up_gs:
|
||||
fused_sf_f32 = fused_sf.float()
|
||||
fused_sf_f32[:, :intermediate_size] *= (gate_gs / l1_max_gs)
|
||||
fused_sf_f32[:, intermediate_size:] *= (up_gs / l1_max_gs)
|
||||
fused_sf = fused_sf_f32.to(torch.float8_e4m3fn)
|
||||
l1_fp4.append(fused_w_fp4)
|
||||
l1_sf.append(fused_sf)
|
||||
l1_gs.append(l1_max_gs)
|
||||
|
||||
down_key = f"layers.{layer_idx}.mlp.experts.{e}.down_proj.weight"
|
||||
if down_key in nvfp4_tensors:
|
||||
down_w = nvfp4_tensors[down_key].to(DEVICE)
|
||||
down_sf = nvfp4_tensors[f"layers.{layer_idx}.mlp.experts.{e}.down_proj.weight_scale"].to(DEVICE)
|
||||
down_gs = nvfp4_tensors[f"layers.{layer_idx}.mlp.experts.{e}.down_proj.weight_scale_2"].item()
|
||||
l2_fp4.append(down_w.view(torch.float4_e2m1fn_x2).permute(1, 0).contiguous())
|
||||
l2_sf.append(down_sf.permute(1, 0).contiguous())
|
||||
l2_gs.append(down_gs)
|
||||
|
||||
return {
|
||||
'l1_fp4': l1_fp4, 'l1_sf': l1_sf, 'l1_gs': l1_gs,
|
||||
'l2_fp4': l2_fp4, 'l2_sf': l2_sf, 'l2_gs': l2_gs,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
torch.manual_seed(42)
|
||||
expert_indices = [0, 1, 2]
|
||||
hidden_size = 7168
|
||||
intermediate_size = 3072
|
||||
|
||||
print("=" * 70)
|
||||
print(" torch.compile + CuTeDSL Custom Op Test")
|
||||
print("=" * 70)
|
||||
|
||||
# Load weights
|
||||
nvfp4_tensors = load_layer_tensors(NVFP4_MODEL_DIR, 0)
|
||||
weights = prepare_nvfp4_weights_direct(nvfp4_tensors, 0, expert_indices, intermediate_size)
|
||||
|
||||
# Create runner
|
||||
runner = Nvfp4MoE(
|
||||
num_experts=len(expert_indices),
|
||||
hidden_size=hidden_size,
|
||||
intermediate_size=intermediate_size,
|
||||
max_num_tokens=8,
|
||||
top_k=2,
|
||||
device="cuda",
|
||||
)
|
||||
runner.prepare_weights_direct(
|
||||
weights['l1_fp4'], weights['l1_sf'], weights['l1_gs'],
|
||||
weights['l2_fp4'], weights['l2_sf'], weights['l2_gs'],
|
||||
)
|
||||
runner_id = register_runner(runner)
|
||||
|
||||
# Test input
|
||||
hidden_states = torch.randn(4, hidden_size, dtype=torch.bfloat16, device=DEVICE) * 2.0
|
||||
topk_ids = torch.tensor([[0, 1]] * 4, dtype=torch.int32, device=DEVICE)
|
||||
topk_weights = torch.tensor([[0.6, 0.4]] * 4, dtype=torch.float32, device=DEVICE)
|
||||
|
||||
# 1. Warmup: compute activation global scales
|
||||
print("\n[0] Computing activation global scales (warmup)...")
|
||||
runner.compute_activation_global_scales(hidden_states, topk_weights, topk_ids)
|
||||
print(f" L1 gs: {runner._l1_activation_global_scale:.6f}")
|
||||
print(f" L2 gs: {runner._l2_activation_global_scale:.6f}")
|
||||
|
||||
# 1. Eager mode (baseline)
|
||||
print("\n[1/2] Running eager mode (baseline)...")
|
||||
runner._ensure_stacked()
|
||||
eager_out = nvfp4_moe_gemm(hidden_states, topk_weights, topk_ids, runner_id, hidden_size)
|
||||
print(f" Eager output: amax={eager_out.abs().max():.4f} mean={eager_out.float().mean():.6f}")
|
||||
|
||||
# 2. torch.compile fullgraph
|
||||
print("\n[2/2] Running torch.compile(fullgraph=True)...")
|
||||
try:
|
||||
@torch.compile(fullgraph=True)
|
||||
def compiled_fn(hs, tw, ti):
|
||||
return nvfp4_moe_gemm(hs, tw, ti, runner_id, hidden_size)
|
||||
|
||||
compiled_out = compiled_fn(hidden_states, topk_weights, topk_ids)
|
||||
print(f" Compiled output: amax={compiled_out.abs().max():.4f} mean={compiled_out.float().mean():.6f}")
|
||||
|
||||
# Compare
|
||||
if eager_out.shape == compiled_out.shape:
|
||||
cos = torch.nn.functional.cosine_similarity(
|
||||
eager_out.flatten().unsqueeze(0).float(),
|
||||
compiled_out.flatten().unsqueeze(0).float(),
|
||||
).item()
|
||||
print(f"\n Eager vs Compiled: cosine={cos:.6f}")
|
||||
if cos > 0.99:
|
||||
print(" ✅ torch.compile produces matching output!")
|
||||
else:
|
||||
print(f" ⚠️ Cosine {cos:.4f} < 0.99 — check for numerical issues")
|
||||
else:
|
||||
print(f" ❌ Shape mismatch: eager={eager_out.shape} compiled={compiled_out.shape}")
|
||||
except Exception as e:
|
||||
print(f" ❌ torch.compile FAILED: {type(e).__name__}: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print(" Test complete ✅")
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
138
tests/unit/test_custom_op.py
Normal file
138
tests/unit/test_custom_op.py
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test that torch.library.custom_op wrapping works with torch.compile.
|
||||
|
||||
This tests the Dynamo opaqueness without needing a GPU — we just verify:
|
||||
1. The custom_op is registered correctly
|
||||
2. torch.compile treats it as opaque (doesn't try to trace through it)
|
||||
3. FakeTensor shape inference works
|
||||
4. The runner registry works
|
||||
|
||||
Does NOT test actual GEMM output — that needs the B200.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import torch
|
||||
|
||||
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, REPO_ROOT)
|
||||
|
||||
|
||||
def test_custom_op_registered():
|
||||
"""Verify nvfp4::linear_gemm and nvfp4::moe_gemm are registered."""
|
||||
from dsv4.ops.custom_ops import nvfp4_linear_gemm, nvfp4_moe_gemm
|
||||
|
||||
# Check they exist as custom ops
|
||||
assert hasattr(nvfp4_linear_gemm, '_name')
|
||||
assert hasattr(nvfp4_moe_gemm, '_name')
|
||||
print("✅ Custom ops registered")
|
||||
|
||||
|
||||
def test_runner_registry():
|
||||
"""Test the runner registry."""
|
||||
from dsv4.ops.custom_ops import register_runner, get_runner
|
||||
|
||||
class FakeRunner:
|
||||
def _run_impl(self, x):
|
||||
return x * 2
|
||||
|
||||
runner = FakeRunner()
|
||||
rid = register_runner(runner)
|
||||
assert rid >= 0
|
||||
|
||||
retrieved = get_runner(rid)
|
||||
assert retrieved is runner
|
||||
print(f"✅ Runner registry works (id={rid})")
|
||||
|
||||
|
||||
def test_fake_tensor_shape_inference():
|
||||
"""Test that FakeTensor impl returns correct shapes."""
|
||||
from dsv4.ops.custom_ops import nvfp4_linear_gemm, nvfp4_moe_gemm
|
||||
|
||||
# linear_gemm fake impl
|
||||
x_fake = torch.empty(4, 7168, dtype=torch.bfloat16, device='meta')
|
||||
out_fake = nvfp4_linear_gemm(x_fake, runner_id=0, out_features=3072)
|
||||
assert out_fake.shape == (4, 3072), f"Expected (4, 3072), got {out_fake.shape}"
|
||||
print(f"✅ linear_gemm fake impl: {x_fake.shape} → {out_fake.shape}")
|
||||
|
||||
# moe_gemm fake impl
|
||||
hs_fake = torch.empty(4, 7168, dtype=torch.bfloat16, device='meta')
|
||||
tw_fake = torch.empty(4, 8, dtype=torch.float32, device='meta')
|
||||
ti_fake = torch.empty(4, 8, dtype=torch.int32, device='meta')
|
||||
out_fake = nvfp4_moe_gemm(hs_fake, tw_fake, ti_fake, runner_id=0, hidden_size=7168)
|
||||
assert out_fake.shape == (4, 7168), f"Expected (4, 7168), got {out_fake.shape}"
|
||||
print(f"✅ moe_gemm fake impl: {hs_fake.shape} → {out_fake.shape}")
|
||||
|
||||
|
||||
def test_torch_compile_skips_custom_op():
|
||||
"""Test that torch.compile doesn't try to trace through the custom op.
|
||||
|
||||
This is the critical test — if compile tries to inline the op, it will
|
||||
fail because the runner's _run_impl uses CuTeDSL internals.
|
||||
|
||||
We use a fake runner that would crash if traced (raises on first call).
|
||||
If torch.compile correctly treats it as opaque, it won't call it during
|
||||
compilation — only the fake impl runs.
|
||||
"""
|
||||
from dsv4.ops.custom_ops import register_runner, nvfp4_linear_gemm
|
||||
|
||||
class ExplodingRunner:
|
||||
"""Runner that explodes if _run_impl is ever called."""
|
||||
call_count = 0
|
||||
def _run_impl(self, x):
|
||||
self.call_count += 1
|
||||
return x # This should never be called during compilation
|
||||
|
||||
runner = ExplodingRunner()
|
||||
rid = register_runner(runner)
|
||||
|
||||
# Compile a function that uses our custom op
|
||||
@torch.compile(fullgraph=True)
|
||||
def forward(x):
|
||||
return nvfp4_linear_gemm(x, runner_id=rid, out_features=3072)
|
||||
|
||||
# With CPU tensors, compile should trace through using FakeTensors
|
||||
# and never call _run_impl
|
||||
x = torch.randn(4, 7168, dtype=torch.bfloat16)
|
||||
# This will fail on CPU because _run_impl needs CUDA, but the point
|
||||
# is that Dynamo should accept the custom op without error.
|
||||
# If it tries to trace through it, we'd get a different error.
|
||||
|
||||
# Instead, just verify Dynamo can handle the graph with custom ops
|
||||
# by checking that the op shows up in the graph
|
||||
try:
|
||||
# Use torch._dynamo to trace without executing
|
||||
import torch._dynamo as dynamo
|
||||
gm, guards = dynamo.export(forward)(x)
|
||||
graph_str = str(gm.graph)
|
||||
assert "nvfp4_linear_gemm" in graph_str, \
|
||||
f"Custom op not found in compiled graph. Graph:\n{graph_str}"
|
||||
print("✅ torch.compile treats custom op as opaque (not inlined)")
|
||||
print(f" Graph contains: ...nvfp4_linear_gemm...")
|
||||
except Exception as e:
|
||||
# On CPU without CUDA, _run_impl can't run. That's fine —
|
||||
# the important thing is Dynamo didn't try to INLINE the op.
|
||||
# If Dynamo tried to trace through it, the error would mention
|
||||
# CuTeDSL/cute.compile, not CUDA.
|
||||
error_str = str(e)
|
||||
if "CuTeDSL" in error_str or "cute" in error_str:
|
||||
print(f"❌ Dynamo tried to trace through the custom op!")
|
||||
print(f" Error: {e}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"⚠️ Execution error (expected on CPU): {type(e).__name__}")
|
||||
print(f" Dynamo accepted the custom op as opaque ✅")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print(" Custom Op Dynamo Compatibility Tests")
|
||||
print("=" * 60)
|
||||
|
||||
test_custom_op_registered()
|
||||
test_runner_registry()
|
||||
test_fake_tensor_shape_inference()
|
||||
test_torch_compile_skips_custom_op()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(" All tests passed ✅")
|
||||
print("=" * 60)
|
||||
352
tests/unit/test_cutedsl.py
Normal file
352
tests/unit/test_cutedsl.py
Normal file
@@ -0,0 +1,352 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CuTeDSL NVFP4 GEMM test — verify the reference kernel works with our data.
|
||||
|
||||
Uses NVIDIA's ScaledGroupedGemmKernel from the CUTLASS CuTeDSL examples
|
||||
with NVFP4 (Float4E2M1FN + Float8E4M3FN, sf_vec_size=16).
|
||||
|
||||
This tests a single GEMM: A(tokens, K) @ B(experts, K, N) = C(tokens, N)
|
||||
with proper scale factor padding/swizzling.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import math
|
||||
import torch
|
||||
|
||||
# Add repo root so 'from cutedsl.kernel...' works
|
||||
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, REPO_ROOT)
|
||||
|
||||
import cutlass
|
||||
import cutlass.cute as cute
|
||||
import cutlass.torch as cutlass_torch
|
||||
import cutlass.utils as utils
|
||||
import cutlass.utils.blockscaled_layout as blockscaled_utils
|
||||
|
||||
from dsv4.kernels.gemm.grouped import (
|
||||
ScaledGroupedGemmKernel,
|
||||
pad_and_swizzle_single,
|
||||
assemble_raw_scales_2d3d_2d_side,
|
||||
assemble_raw_scales_2d3d_3d_side,
|
||||
cat_byte_reinterpretable_tensors,
|
||||
stack_byte_reinterpretable_tensors,
|
||||
offs_to_group_sizes,
|
||||
)
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
def ceil_div(a, b):
|
||||
return (a + b - 1) // b
|
||||
|
||||
def round_up(a, b):
|
||||
return ceil_div(a, b) * b
|
||||
|
||||
|
||||
def quantize_bf16_to_nvfp4(x_bf16, block_size=16):
|
||||
"""Quantize BF16 tensor to NVFP4 (E2M1 + E4M3 block scales + global scale).
|
||||
|
||||
Returns (x_fp4, block_scales, global_scale) where:
|
||||
x_fp4: torch.float4_e2m1fn_x2 with same logical shape (packed along last dim)
|
||||
block_scales: float8_e4m3fn with shape (..., ceil_div(last_dim, block_size))
|
||||
global_scale: float32 scalar
|
||||
"""
|
||||
x_f32 = x_bf16.float()
|
||||
amax = x_f32.abs().max().clamp(min=1e-8).float()
|
||||
global_scale = amax / (6.0 * 448.0)
|
||||
|
||||
x_norm = x_f32 / global_scale
|
||||
|
||||
# Per-block amax for block scales
|
||||
last_dim = x_norm.shape[-1]
|
||||
n_blocks = ceil_div(last_dim, block_size)
|
||||
|
||||
# Pad last dim to multiple of block_size
|
||||
if last_dim % block_size != 0:
|
||||
pad_size = n_blocks * block_size - last_dim
|
||||
x_norm = torch.nn.functional.pad(x_norm, (0, pad_size))
|
||||
|
||||
x_reshaped = x_norm.reshape(*x_norm.shape[:-1], n_blocks, block_size)
|
||||
block_amax = x_reshaped.abs().amax(dim=-1).clamp(min=1e-8)
|
||||
block_scale = (block_amax / 6.0).to(torch.float8_e4m3fn)
|
||||
|
||||
# Quantize to E2M1
|
||||
E2M1_MAGNITUDES = [0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0]
|
||||
x_blocks = x_reshaped
|
||||
block_sf_expanded = block_scale.float().unsqueeze(-1)
|
||||
x_scaled = x_blocks / block_sf_expanded.clamp(min=1e-8)
|
||||
|
||||
magnitudes = torch.tensor(E2M1_MAGNITUDES, dtype=torch.float32, device=x_bf16.device)
|
||||
signs = torch.sign(x_scaled)
|
||||
abs_scaled = x_scaled.abs().unsqueeze(-1)
|
||||
distances = (abs_scaled - magnitudes).abs()
|
||||
indices = distances.argmin(dim=-1)
|
||||
|
||||
nibbles = torch.where(signs < 0, indices + 8, indices).to(torch.uint8)
|
||||
|
||||
# Pack pairs: byte = (odd_nibble << 4) | even_nibble
|
||||
even = nibbles[..., ::2]
|
||||
odd = nibbles[..., 1::2]
|
||||
packed = (odd << 4) | even
|
||||
|
||||
# View as float4_e2m1fn_x2 — same logical shape, packed last dim halved
|
||||
# The logical shape has the original last_dim, but stored packed
|
||||
# float4_e2m1fn_x2: each element is 1 byte = 2 FP4 values
|
||||
# Shape: (..., last_dim // 2) in float4_e2m1fn_x2
|
||||
packed_shape = list(x_bf16.shape)
|
||||
packed_shape[-1] = last_dim // 2
|
||||
x_fp4 = packed.view(torch.float4_e2m1fn_x2).reshape(packed_shape)
|
||||
|
||||
# Reshape block scales
|
||||
sf_shape = list(x_bf16.shape[:-1]) + [n_blocks]
|
||||
block_scale = block_scale.reshape(sf_shape)
|
||||
|
||||
return x_fp4, block_scale, global_scale
|
||||
|
||||
|
||||
def dequantize_nvfp4(x_fp4, block_scales, global_scale):
|
||||
"""Dequantize NVFP4 back to BF16 for reference comparison."""
|
||||
E2M1_LUT = torch.tensor([
|
||||
0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0,
|
||||
-0.0, -0.5, -1.0, -1.5, -2.0, -3.0, -4.0, -6.0,
|
||||
], dtype=torch.float32, device=x_fp4.device)
|
||||
|
||||
raw = x_fp4.view(torch.uint8)
|
||||
lo = E2M1_LUT[(raw & 0x0F).long()]
|
||||
hi = E2M1_LUT[((raw >> 4) & 0x0F).long()]
|
||||
|
||||
unpacked = torch.empty(*raw.shape[:-1], raw.shape[-1] * 2, dtype=torch.float32, device=x_fp4.device)
|
||||
unpacked[..., ::2] = lo
|
||||
unpacked[..., 1::2] = hi
|
||||
|
||||
# Expand block scales
|
||||
n_blocks = block_scales.shape[-1]
|
||||
block_size = (unpacked.shape[-1]) // n_blocks
|
||||
block_sf = block_scales.float().unsqueeze(-1).expand(*block_scales.shape, block_size)
|
||||
block_sf = block_sf.reshape(*unpacked.shape)
|
||||
|
||||
return (unpacked * block_sf * global_scale).to(torch.bfloat16)
|
||||
|
||||
|
||||
# ── Main Test ──────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
torch.manual_seed(42)
|
||||
device = "cuda"
|
||||
|
||||
# Problem sizes
|
||||
num_experts = 2
|
||||
tokens_per_expert = 64
|
||||
hidden = 256 # K dimension
|
||||
intermediate = 128 # N dimension
|
||||
sf_vec_size = 16
|
||||
block_size = 16
|
||||
|
||||
tokens_sum = num_experts * tokens_per_expert
|
||||
|
||||
print(f"Test: {num_experts} experts, {tokens_per_expert} tokens each, K={hidden}, N={intermediate}")
|
||||
|
||||
# ── Create BF16 reference data ──
|
||||
x_bf16 = torch.randn(tokens_sum, hidden, dtype=torch.bfloat16, device=device) * 2.0
|
||||
w_bf16 = torch.randn(num_experts, hidden, intermediate, dtype=torch.bfloat16, device=device) * 0.5
|
||||
|
||||
# BF16 reference: for each expert, matmul its tokens with its weight
|
||||
ref_out = torch.zeros(tokens_sum, intermediate, dtype=torch.bfloat16, device=device)
|
||||
for e in range(num_experts):
|
||||
start = e * tokens_per_expert
|
||||
end = (e + 1) * tokens_per_expert
|
||||
ref_out[start:end] = x_bf16[start:end] @ w_bf16[e]
|
||||
|
||||
print(f"BF16 ref: amax={ref_out.abs().max():.4f} mean={ref_out.float().mean():.6f}")
|
||||
|
||||
# ── Quantize to NVFP4 ──
|
||||
x_fp4, x_sf, x_gs = quantize_bf16_to_nvfp4(x_bf16)
|
||||
|
||||
# For weights: the kernel expects (experts, hidden, intermediate) with
|
||||
# packed_dim=1 (the hidden/K dimension is packed).
|
||||
# w_bf16[e] is (hidden, intermediate).
|
||||
# We quantize each expert weight, keeping the packed dim as hidden.
|
||||
w_fp4_list, w_sf_list, w_gs_list = [], [], []
|
||||
for e in range(num_experts):
|
||||
w = w_bf16[e] # (hidden, intermediate) — K=hidden, N=intermediate
|
||||
w_f32 = w.float()
|
||||
w_amax = w_f32.abs().max().clamp(min=1e-8).float()
|
||||
w_gs = w_amax / (6.0 * 448.0)
|
||||
w_norm = w_f32 / w_gs
|
||||
|
||||
# Block scales along the K dimension (dim 0 = hidden)
|
||||
# Scale shape: (ceil_div(hidden, 16), intermediate)
|
||||
k_blocks = ceil_div(hidden, block_size)
|
||||
if hidden % block_size != 0:
|
||||
w_norm = torch.nn.functional.pad(w_norm, (0, 0, 0, k_blocks * block_size - hidden))
|
||||
|
||||
w_reshaped = w_norm.reshape(k_blocks, block_size, intermediate)
|
||||
w_block_amax = w_reshaped.abs().amax(dim=1).clamp(min=1e-8) # (k_blocks, intermediate)
|
||||
w_sf = (w_block_amax / 6.0).to(torch.float8_e4m3fn)
|
||||
|
||||
# Quantize to E2M1 along K (dim 0)
|
||||
E2M1_MAGNITUDES = [0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0]
|
||||
w_block_sf = w_sf.float().unsqueeze(1) # (k_blocks, 1, intermediate)
|
||||
w_scaled = w_reshaped / w_block_sf.clamp(min=1e-8)
|
||||
|
||||
magnitudes = torch.tensor(E2M1_MAGNITUDES, dtype=torch.float32, device=device)
|
||||
signs = torch.sign(w_scaled)
|
||||
abs_scaled = w_scaled.abs().unsqueeze(-1)
|
||||
distances = (abs_scaled - magnitudes).abs()
|
||||
indices = distances.argmin(dim=-1)
|
||||
nibbles = torch.where(signs < 0, indices + 8, indices).to(torch.uint8)
|
||||
|
||||
# Pack pairs along K (block_size dim, which is dim 1 after reshape)
|
||||
even = nibbles[:, ::2, :]
|
||||
odd = nibbles[:, 1::2, :]
|
||||
packed = (odd << 4) | even # (k_blocks, block_size//2, intermediate)
|
||||
|
||||
# Reshape to (hidden//2, intermediate) in float4_e2m1fn_x2
|
||||
w_fp4 = packed.reshape(hidden // 2, intermediate).view(torch.float4_e2m1fn_x2)
|
||||
|
||||
w_fp4_list.append(w_fp4)
|
||||
w_sf_list.append(w_sf) # (k_blocks, intermediate) = (hidden//16, intermediate)
|
||||
w_gs_list.append(w_gs)
|
||||
|
||||
# Verify quantization roundtrip
|
||||
x_deq = dequantize_nvfp4(x_fp4, x_sf, x_gs)
|
||||
cos_quant = torch.nn.functional.cosine_similarity(
|
||||
x_bf16.flatten().unsqueeze(0).float(),
|
||||
x_deq.flatten().unsqueeze(0).float(),
|
||||
).item()
|
||||
print(f"Quantization roundtrip cosine: {cos_quant:.6f}")
|
||||
|
||||
# ── Prepare CuTeDSL kernel inputs ──
|
||||
# The kernel expects:
|
||||
# mat_a: (tokens_sum, K_packed) float4_e2m1fn_x2
|
||||
# mat_b: (experts, K_packed, N_packed) float4_e2m1fn_x2 — K-major
|
||||
# scale_a: assembled 2D side (padded + swizzled)
|
||||
# scale_b: assembled 3D side (padded + swizzled per expert)
|
||||
# offs: (experts,) int32 cumulative token offsets
|
||||
# global_scale_a: (experts,) float32
|
||||
# global_scale_b: (experts,) float32
|
||||
|
||||
# Expert offsets (cumulative sum of tokens per expert)
|
||||
offs = torch.tensor([tokens_per_expert * (e + 1) for e in range(num_experts)],
|
||||
dtype=torch.int32, device=device)
|
||||
|
||||
# Assemble scale_a (2D side: concatenate per-expert, pad to 128, swizzle)
|
||||
raw_scale_a = [x_sf[e*tokens_per_expert:(e+1)*tokens_per_expert] for e in range(num_experts)]
|
||||
scale_a = assemble_raw_scales_2d3d_2d_side(raw_scale_a)
|
||||
|
||||
# Assemble scale_b (3D side: per-expert, pad and swizzle each)
|
||||
# Reference uses (N, K_sf) = (intermediate, hidden//16) for each expert
|
||||
# Our w_sf is (K_sf, intermediate) — need to transpose
|
||||
w_sf_t = [sf.T.contiguous() for sf in w_sf_list]
|
||||
scale_b = assemble_raw_scales_2d3d_3d_side(w_sf_t)
|
||||
|
||||
# Global scales
|
||||
global_scale_a = torch.tensor([x_gs] * num_experts, dtype=torch.float32, device=device)
|
||||
global_scale_b = torch.tensor([w_gs_list[e] for e in range(num_experts)], dtype=torch.float32, device=device)
|
||||
|
||||
# mat_a is (tokens_sum, K_packed) in float4_e2m1fn_x2, row-major (K-major)
|
||||
# This matches the reference: A shape=(128,128) stride=(128,1)
|
||||
mat_a = x_fp4
|
||||
|
||||
# mat_b: (experts, K_packed, N_packed) in float4_e2m1fn_x2, K-major
|
||||
# Reference: B shape=(2,128,128) stride=(16384,1,128) — K is stride-1
|
||||
# torch.stack gives stride (16384, 128, 1) — N is stride-1 (wrong)
|
||||
# We need K-major: permute, make contiguous, permute back
|
||||
mat_b = torch.stack(w_fp4_list).permute(0, 2, 1).contiguous().permute(0, 2, 1)
|
||||
|
||||
print(f"\nKernel inputs:")
|
||||
print(f" mat_a: {mat_a.shape} {mat_a.dtype}")
|
||||
print(f" mat_b: {mat_b.shape} {mat_b.dtype}")
|
||||
print(f" scale_a: {scale_a.shape} {scale_a.dtype}")
|
||||
print(f" scale_b: {scale_b.shape} {scale_b.dtype}")
|
||||
print(f" offs: {offs.tolist()}")
|
||||
print(f" global_scale_a: {global_scale_a.tolist()}")
|
||||
print(f" global_scale_b: {[f'{v:.6e}' for v in global_scale_b.tolist()]}")
|
||||
|
||||
# ── Run CuTeDSL kernel ──
|
||||
print("\nCompiling and running CuTeDSL kernel (first run takes ~1 min to compile)...")
|
||||
|
||||
out = torch.zeros(tokens_sum, intermediate, dtype=torch.bfloat16, device=device)
|
||||
|
||||
kernel = ScaledGroupedGemmKernel(
|
||||
scenario="2Dx3D",
|
||||
sf_vec_size=sf_vec_size,
|
||||
accumulate_on_output=False,
|
||||
separate_tensormap_init=True,
|
||||
consistent_token_padding=False,
|
||||
mma_tiler_mnk=(128, 128, 256),
|
||||
cluster_shape_mnk=(1, 1, 1),
|
||||
)
|
||||
|
||||
# Convert to CuTe tensors
|
||||
a_cute = cutlass_torch.from_dlpack(mat_a)
|
||||
a_cute = a_cute.mark_layout_dynamic(leading_dim=cutlass_torch.get_leading_dim(mat_a))
|
||||
|
||||
b_cute = cutlass_torch.from_dlpack(mat_b)
|
||||
b_cute = b_cute.mark_layout_dynamic(leading_dim=cutlass_torch.get_leading_dim(mat_b))
|
||||
|
||||
sfa_cute = cutlass_torch.from_dlpack(scale_a)
|
||||
sfa_cute = sfa_cute.mark_layout_dynamic(leading_dim=cutlass_torch.get_leading_dim(scale_a))
|
||||
|
||||
sfb_cute = cutlass_torch.from_dlpack(scale_b)
|
||||
sfb_cute = sfb_cute.mark_layout_dynamic(leading_dim=cutlass_torch.get_leading_dim(scale_b))
|
||||
|
||||
c_cute = cutlass_torch.from_dlpack(out)
|
||||
c_cute = c_cute.mark_layout_dynamic(leading_dim=cutlass_torch.get_leading_dim(out))
|
||||
|
||||
offs_cute = cutlass_torch.from_dlpack(offs)
|
||||
offs_cute = offs_cute.mark_layout_dynamic(leading_dim=cutlass_torch.get_leading_dim(offs))
|
||||
|
||||
workspace_size = kernel.get_workspace_size(num_experts)
|
||||
workspace = torch.full((workspace_size,), 255, dtype=torch.uint8, device=device)
|
||||
ws_cute = cutlass_torch.from_dlpack(workspace)
|
||||
ws_cute = ws_cute.mark_layout_dynamic(leading_dim=cutlass_torch.get_leading_dim(workspace))
|
||||
|
||||
gsa_cute = cutlass_torch.from_dlpack(global_scale_a)
|
||||
gsa_cute = gsa_cute.mark_layout_dynamic(leading_dim=cutlass_torch.get_leading_dim(global_scale_a))
|
||||
|
||||
gsb_cute = cutlass_torch.from_dlpack(global_scale_b)
|
||||
gsb_cute = gsb_cute.mark_layout_dynamic(leading_dim=cutlass_torch.get_leading_dim(global_scale_b))
|
||||
|
||||
import cuda.bindings.driver as cuda
|
||||
cluster_size = 1
|
||||
max_active_clusters = utils.HardwareInfo().get_max_active_clusters(cluster_size)
|
||||
stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream)
|
||||
|
||||
compiled = cute.compile(
|
||||
kernel,
|
||||
a_cute, b_cute, sfa_cute, sfb_cute, c_cute, offs_cute, ws_cute,
|
||||
max_active_clusters, stream,
|
||||
global_scale_a=gsa_cute,
|
||||
global_scale_b=gsb_cute,
|
||||
)
|
||||
|
||||
compiled(
|
||||
a_cute, b_cute, sfa_cute, sfb_cute, c_cute, offs_cute, ws_cute,
|
||||
stream,
|
||||
global_scale_a=gsa_cute,
|
||||
global_scale_b=gsb_cute,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
# ── Compare ──
|
||||
cosine = torch.nn.functional.cosine_similarity(
|
||||
out.flatten().unsqueeze(0).float(),
|
||||
ref_out.flatten().unsqueeze(0).float(),
|
||||
).item()
|
||||
mse = (out.float() - ref_out.float()).pow(2).mean().item()
|
||||
|
||||
print(f"\n{'='*70}")
|
||||
print(f" RESULT: cosine={cosine:.6f} MSE={mse:.6e}")
|
||||
print(f"{'='*70}")
|
||||
|
||||
if cosine > 0.99:
|
||||
print(f" ✅ PASS: CuTeDSL kernel matches BF16 reference")
|
||||
elif cosine > 0.95:
|
||||
print(f" ⚠️ Close but not perfect — quantization loss?")
|
||||
else:
|
||||
print(f" ❌ FAIL: kernel output doesn't match reference")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
327
tests/unit/test_fmha_v3.py
Normal file
327
tests/unit/test_fmha_v3.py
Normal file
@@ -0,0 +1,327 @@
|
||||
"""
|
||||
FMHA v3: QK -> softmax -> PV with KV-tile interleaving.
|
||||
Bug 4b fix (FMHA pattern): P store uses QK C-fragment layout composition,
|
||||
NOT PV A-fragment layout. Register bridge: FP32 backing (store partition shape)
|
||||
recast to BF16 view (QK-load layout).
|
||||
"""
|
||||
import torch, cutlass, cutlass.cute as cute, cutlass.utils as utils, cutlass.pipeline as pipeline
|
||||
from cutlass.cute.nvgpu import cpasync, tcgen05
|
||||
from cutlass import Float32, BFloat16, Int32, Boolean, const_expr
|
||||
from cutlass.utils import LayoutEnum
|
||||
from cutlass.utils.tmem_allocator import find_tmem_tensor_col_offset
|
||||
import cuda.bindings.driver as cuda
|
||||
import cutlass.torch as ct
|
||||
|
||||
HEAD_DIM = 64
|
||||
|
||||
class FmhaV3:
|
||||
def __init__(self):
|
||||
self.acc_dtype = Float32; self.qk_acc_dtype = Float32
|
||||
self.q_dtype = BFloat16; self.o_dtype = BFloat16; self.c_dtype = BFloat16
|
||||
self.use_2cta_instrs = False; self.epilog_sync_bar_id = 1
|
||||
self.cluster_shape_mn = (1, 1); self.cta_group = tcgen05.CtaGroup.ONE
|
||||
self.epilogue_warp_id = (0,1,2,3); self.mma_warp_id = 4; self.tma_warp_id = 5
|
||||
self.threads_per_cta = 192; self.num_c_stage = 2
|
||||
self.kv_stage = 2; self.q_stage = 1; self.num_c_stage = 2
|
||||
|
||||
def _setup(self, qk_mma, pv_mma):
|
||||
qk_ik = cute.size(qk_mma.shape_mnk, mode=[2])
|
||||
self.qk_mma_tiler = (128, 128, qk_ik * 4)
|
||||
pv_ik = cute.size(pv_mma.shape_mnk, mode=[2])
|
||||
self.pv_mma_tiler = (128, HEAD_DIM, pv_ik * (128 // pv_ik))
|
||||
self.mma_tiler = self.qk_mma_tiler
|
||||
self.cluster_layout_vmnk = cute.tiled_divide(cute.make_layout((1,1,1)), (qk_mma.thr_id.shape,))
|
||||
self.cta_tile_shape_mnk = (self.qk_mma_tiler[0]//cute.size(qk_mma.thr_id.shape), HEAD_DIM, self.qk_mma_tiler[2])
|
||||
self.c_layout = LayoutEnum.ROW_MAJOR
|
||||
self.epi_tile = utils.sm100.compute_epilogue_tile_shape(self.cta_tile_shape_mnk, False, self.c_layout, self.o_dtype)
|
||||
self.num_ab_stage = 1; self.num_acc_stage = 1
|
||||
self.q_smem_s = utils.sm100.make_smem_layout_a(qk_mma, self.qk_mma_tiler, self.q_dtype, self.q_stage)
|
||||
self.k_smem_s = utils.sm100.make_smem_layout_b(qk_mma, self.qk_mma_tiler, self.q_dtype, self.kv_stage)
|
||||
self.v_smem_s = utils.sm100.make_smem_layout_b(pv_mma, self.pv_mma_tiler, self.q_dtype, self.kv_stage)
|
||||
self.c_smem_s = utils.sm100.make_smem_layout_epi(self.o_dtype, self.c_layout, self.epi_tile, 2)
|
||||
self.p_tmem_s = utils.sm100.make_smem_layout_a(pv_mma, self.pv_mma_tiler, self.q_dtype, 1)
|
||||
qk_thr = qk_mma.get_slice(0); qk_as = qk_thr.partition_shape_C(self.qk_mma_tiler[:2])
|
||||
tStS = qk_thr.make_fragment_C(qk_as)
|
||||
pv_thr = pv_mma.get_slice(0); pv_as = pv_thr.partition_shape_C(self.pv_mma_tiler[:2])
|
||||
tOtO = pv_thr.make_fragment_C(pv_as)
|
||||
self.tmem_s0_offset = 0; self.tmem_p0_offset = 32
|
||||
# P occupies [tmem_p0_offset, tmem_p0_offset + p_cols_fp32)
|
||||
# S occupies [0, qk_mma_tiler[1]) = [0, 128)
|
||||
# O must NOT overlap P. Place O after max(S end, P end), aligned to 32.
|
||||
p_cols_fp32 = self.pv_mma_tiler[2] * self.q_dtype.width // self.qk_acc_dtype.width
|
||||
p_end = self.tmem_p0_offset + p_cols_fp32 # 32 + 64 = 96
|
||||
s_cols = self.qk_mma_tiler[1] # 128
|
||||
o_after = max(s_cols, p_end) # 128
|
||||
self.tmem_o0_offset = ((o_after + 31) // 32) * 32 # align to 32 = 128
|
||||
o_cols = find_tmem_tensor_col_offset(tOtO) # footprint of O
|
||||
total = self.tmem_o0_offset + o_cols
|
||||
# Must be multiple of 32 AND power of 2
|
||||
self.num_tmem_alloc_cols = 1
|
||||
while self.num_tmem_alloc_cols < total:
|
||||
self.num_tmem_alloc_cols *= 2
|
||||
cta = cute.size(qk_mma.thr_id.shape)
|
||||
q_s = cute.slice_(self.q_smem_s,(None,None,None,0)); k_s = cute.slice_(self.k_smem_s,(None,None,None,0))
|
||||
self.q_tx_bytes = cute.size_in_bytes(self.q_dtype, q_s) * cta
|
||||
self.kv_tx_bytes = cute.size_in_bytes(self.q_dtype, k_s) * cta
|
||||
|
||||
@cute.jit
|
||||
def __call__(self, q, k, v, c, stream):
|
||||
self.q_dtype = q.element_type; self.o_dtype = c.element_type; self.c_dtype = self.o_dtype
|
||||
self.a_major = LayoutEnum.from_tensor(q).mma_major_mode()
|
||||
self.b_major = LayoutEnum.from_tensor(k).mma_major_mode()
|
||||
# FMHA-style V: reconstruct as (HEAD_DIM, s_k, 1) MN-major
|
||||
v_fmha = cute.make_tensor(
|
||||
v.iterator,
|
||||
cute.make_layout(
|
||||
(HEAD_DIM, 128, 1),
|
||||
stride=(1, HEAD_DIM, HEAD_DIM * 128),
|
||||
),
|
||||
)
|
||||
self.v_major = LayoutEnum.from_tensor(v_fmha).mma_major_mode()
|
||||
self.c_layout = LayoutEnum.from_tensor(c)
|
||||
qk_mma = utils.sm100.make_trivial_tiled_mma(self.q_dtype, self.q_dtype, self.a_major, self.b_major, self.qk_acc_dtype, self.cta_group, (128,128), tcgen05.OperandSource.SMEM)
|
||||
pv_mma = utils.sm100.make_trivial_tiled_mma(self.q_dtype, self.q_dtype, cute.nvgpu.OperandMajorMode.K, self.v_major, self.qk_acc_dtype, self.cta_group, (128,HEAD_DIM), tcgen05.OperandSource.TMEM)
|
||||
self._setup(qk_mma, pv_mma)
|
||||
q_s = cute.slice_(self.q_smem_s,(None,None,None,0)); k_s = cute.slice_(self.k_smem_s,(None,None,None,0)); v_s = cute.slice_(self.v_smem_s,(None,None,None,0))
|
||||
tma_q,mQ = cute.nvgpu.make_tiled_tma_atom_A(utils.sm100.cluster_shape_to_tma_atom_A(self.cluster_shape_mn,qk_mma.thr_id),q,q_s,self.qk_mma_tiler,qk_mma,self.cluster_layout_vmnk.shape)
|
||||
tma_k,mK = cute.nvgpu.make_tiled_tma_atom_B(utils.sm100.cluster_shape_to_tma_atom_B(self.cluster_shape_mn,qk_mma.thr_id),k,k_s,self.qk_mma_tiler,qk_mma,self.cluster_layout_vmnk.shape)
|
||||
tma_v,mV = cute.nvgpu.make_tiled_tma_atom_B(utils.sm100.cluster_shape_to_tma_atom_B(self.cluster_shape_mn,pv_mma.thr_id),v_fmha,v_s,self.pv_mma_tiler,pv_mma,self.cluster_layout_vmnk.shape)
|
||||
epi_s = cute.select(self.c_smem_s,mode=[0,1])
|
||||
tma_c,mC = cpasync.make_tiled_tma_atom(cpasync.CopyBulkTensorTileS2GOp(),c,epi_s,self.epi_tile)
|
||||
self._kernel(qk_mma,pv_mma,tma_q,mQ,tma_k,mK,tma_v,mV,tma_c,mC,self.cluster_layout_vmnk,self.q_smem_s,self.k_smem_s,self.v_smem_s,self.p_tmem_s,self.c_smem_s,self.epi_tile).launch(grid=(1,1,1),block=[self.threads_per_cta,1,1],stream=stream)
|
||||
|
||||
@cute.kernel
|
||||
def _kernel(self, qk_mma, pv_mma, tma_q, mQ, tma_k, mK, tma_v, mV, tma_c, mC, cl_vmnk, q_smem_s, k_smem_s, v_smem_s, p_tmem_s, c_smem_s, epi_tile):
|
||||
warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
|
||||
tidx,_,_ = cute.arch.thread_idx()
|
||||
if warp_idx == self.tma_warp_id:
|
||||
cpasync.prefetch_descriptor(tma_q); cpasync.prefetch_descriptor(tma_k); cpasync.prefetch_descriptor(tma_v); cpasync.prefetch_descriptor(tma_c)
|
||||
|
||||
@cute.struct
|
||||
class SS:
|
||||
q_bar: cute.struct.MemRange[cutlass.Int64, self.q_stage*2]
|
||||
kv_bar: cute.struct.MemRange[cutlass.Int64, self.kv_stage*2]
|
||||
s_bar: cute.struct.MemRange[cutlass.Int64, 2]
|
||||
acc_bar: cute.struct.MemRange[cutlass.Int64, self.num_acc_stage*2]
|
||||
tmem_dealloc: cutlass.Int64; holding: cutlass.Int32
|
||||
smem = utils.SmemAllocator(); st = smem.allocate(SS)
|
||||
|
||||
qp,qc = pipeline.PipelineTmaUmma.create(barrier_storage=st.q_bar.data_ptr(),num_stages=self.q_stage,producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread,1),tx_count=self.q_tx_bytes,cta_layout_vmnk=cl_vmnk,defer_sync=True).make_participants()
|
||||
kvp,kvc = pipeline.PipelineTmaUmma.create(barrier_storage=st.kv_bar.data_ptr(),num_stages=self.kv_stage,producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread,1),tx_count=self.kv_tx_bytes,cta_layout_vmnk=cl_vmnk,defer_sync=True).make_participants()
|
||||
s_prod,s_cons = pipeline.PipelineUmmaAsync.create(barrier_storage=st.s_bar.data_ptr(),num_stages=1,producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread,32*len(self.epilogue_warp_id))).make_participants()
|
||||
softmax_done_bar = pipeline.NamedBarrier(barrier_id=3, num_threads=32 + 32*len(self.epilogue_warp_id))
|
||||
acc_pipe = pipeline.PipelineUmmaAsync.create(barrier_storage=st.acc_bar.data_ptr(),num_stages=self.num_acc_stage,producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread,len(self.epilogue_warp_id)),cta_layout_vmnk=cl_vmnk,defer_sync=True)
|
||||
tmem_bar = pipeline.NamedBarrier(barrier_id=2,num_threads=32*len((self.mma_warp_id,*self.epilogue_warp_id)))
|
||||
tmem = utils.TmemAllocator(st.holding.ptr,barrier_for_retrieve=tmem_bar,allocator_warp_id=self.epilogue_warp_id[0],is_two_cta=cute.size(qk_mma.thr_id.shape)==2,two_cta_tmem_dealloc_mbar_ptr=st.tmem_dealloc.ptr)
|
||||
pipeline.pipeline_init_arrive(cluster_shape_mn=cl_vmnk,is_relaxed=True)
|
||||
|
||||
sQ = smem.allocate_tensor(element_type=self.q_dtype,layout=q_smem_s.outer,byte_alignment=128,swizzle=q_smem_s.inner)
|
||||
sK = smem.allocate_tensor(element_type=self.q_dtype,layout=k_smem_s.outer,byte_alignment=128,swizzle=k_smem_s.inner)
|
||||
sV = smem.allocate_tensor(element_type=self.q_dtype,layout=v_smem_s.outer,byte_alignment=128,swizzle=v_smem_s.inner)
|
||||
sC = smem.allocate_tensor(element_type=self.o_dtype,layout=c_smem_s.outer,byte_alignment=128,swizzle=c_smem_s.inner)
|
||||
|
||||
gQ = cute.local_tile(mQ,cute.slice_(self.qk_mma_tiler,(None,0,None)),(None,None,None))
|
||||
gK = cute.local_tile(mK,cute.slice_(self.qk_mma_tiler,(0,None,None)),(None,None,None))
|
||||
gV = cute.local_tile(mV,cute.slice_(self.pv_mma_tiler,(0,None,None)),(None,None,None))
|
||||
gC = cute.local_tile(mC,cute.slice_(self.pv_mma_tiler,(None,None,0)),(None,None,None))
|
||||
n_kv_tiles = cute.size(gK, mode=[3])
|
||||
|
||||
qk_thr = qk_mma.get_slice(0); pv_thr = pv_mma.get_slice(0)
|
||||
tCgQ = qk_thr.partition_A(gQ); tCgK = qk_thr.partition_B(gK)
|
||||
tCgV = pv_thr.partition_B(gV); tCgC = pv_thr.partition_C(gC)
|
||||
a_lay = cute.make_layout(cute.slice_(cl_vmnk,(0,0,None,0)).shape)
|
||||
tAsQ,tAgQ = cpasync.tma_partition(tma_q,0,a_lay,cute.group_modes(sQ,0,3),cute.group_modes(tCgQ,0,3))
|
||||
b_lay = cute.make_layout(cute.slice_(cl_vmnk,(0,None,0,0)).shape)
|
||||
tBsK,tBgK = cpasync.tma_partition(tma_k,0,b_lay,cute.group_modes(sK,0,3),cute.group_modes(tCgK,0,3))
|
||||
tVsV,tVgV = cpasync.tma_partition(tma_v,0,b_lay,cute.group_modes(sV,0,3),cute.group_modes(tCgV,0,3))
|
||||
tAgQ = tAgQ[(None,0,None,0)]; tBgK = tBgK[(None,0,None,0)]; tVgV = tVgV[(None,0,None,0)]
|
||||
|
||||
tCrQ = qk_mma.make_fragment_A(sQ); tCrK = qk_mma.make_fragment_B(sK)
|
||||
tCrV = pv_mma.make_fragment_B(sV)
|
||||
|
||||
qk_as = qk_thr.partition_shape_C(self.qk_mma_tiler[:2])
|
||||
tStS = qk_thr.make_fragment_C(qk_as)
|
||||
tStS0 = cute.make_tensor(tStS.iterator + self.tmem_s0_offset, tStS.layout)
|
||||
pv_as = pv_thr.partition_shape_C(self.pv_mma_tiler[:2])
|
||||
tOtO = pv_thr.make_fragment_C(pv_as)
|
||||
tOtO0 = cute.make_tensor(tOtO.iterator + self.tmem_o0_offset, tOtO.layout)
|
||||
|
||||
# --- PV read view (for MMA only, NOT for softmax store) ---
|
||||
tP = cute.make_tensor(tStS.iterator, p_tmem_s.outer)
|
||||
tOrP_base = pv_thr.make_fragment_A(tP)
|
||||
tOrP = tOrP_base[(None,None,None,0)]
|
||||
tOrP0 = cute.make_tensor(
|
||||
tOrP.iterator + self.qk_acc_dtype.width // self.q_dtype.width * self.tmem_p0_offset,
|
||||
tOrP.layout)
|
||||
|
||||
tCtS_fake = qk_mma.make_fragment_C(cute.append(qk_as, self.num_acc_stage))
|
||||
tCtO_fake = pv_mma.make_fragment_C(cute.append(pv_as, self.num_acc_stage))
|
||||
pipeline.pipeline_init_wait(cluster_shape_mn=cl_vmnk)
|
||||
|
||||
# TMA LOAD
|
||||
if warp_idx == self.tma_warp_id:
|
||||
qp.reset(); qh = qp.acquire_and_advance()
|
||||
cute.copy(tma_q,tAgQ[(None,qh.count)],tAsQ[(None,qh.index)],tma_bar_ptr=qh.barrier)
|
||||
qp.tail()
|
||||
kvp.reset(); pk = kvp.try_acquire()
|
||||
for kt in cutlass.range(n_kv_tiles,unroll=1):
|
||||
kh = kvp.acquire_and_advance(pk)
|
||||
cute.copy(tma_k,tBgK[(None,kh.count)],tBsK[(None,kh.index)],tma_bar_ptr=kh.barrier)
|
||||
pk = cutlass.Boolean(1)
|
||||
vh = kvp.acquire_and_advance(pk)
|
||||
cute.copy(tma_v,tVgV[(None,vh.count)],tVsV[(None,vh.index)],tma_bar_ptr=vh.barrier)
|
||||
pk = cutlass.Boolean(1)
|
||||
kvp.tail()
|
||||
|
||||
# MMA
|
||||
if warp_idx == self.mma_warp_id:
|
||||
tmem.wait_for_alloc()
|
||||
qc.reset(); qh = qc.wait_and_advance(); qh.release()
|
||||
kvc.reset(); pk = kvc.try_wait()
|
||||
acc_st = pipeline.make_pipeline_state(pipeline.PipelineUserType.Producer, self.num_acc_stage)
|
||||
acc_pipe.producer_acquire(acc_st)
|
||||
for kt in range(n_kv_tiles):
|
||||
kh = kvc.wait_and_advance(pk); pk = cutlass.Boolean(1)
|
||||
sh = s_prod.acquire_and_advance()
|
||||
qk_mma.set(tcgen05.Field.ACCUMULATE, False)
|
||||
for kb in cutlass.range(cute.size(tCrQ,mode=[2]), unroll_full=True):
|
||||
cute.gemm(qk_mma, tStS0, tCrQ[(None,None,kb,0)], tCrK[(None,None,kb,kh.index)], tStS0)
|
||||
qk_mma.set(tcgen05.Field.ACCUMULATE, True)
|
||||
cute.arch.fence_view_async_tmem_store()
|
||||
sh.commit(); kh.release()
|
||||
softmax_done_bar.arrive_and_wait()
|
||||
vh = kvc.wait_and_advance(pk); pk = cutlass.Boolean(1)
|
||||
pv_mma.set(tcgen05.Field.ACCUMULATE, kt != 0)
|
||||
for kb in cutlass.range(cute.size(tOrP0,mode=[2]), unroll_full=True):
|
||||
cute.gemm(pv_mma, tOtO0, tOrP0[(None,None,kb)], tCrV[(None,None,kb,vh.index)], tOtO0)
|
||||
pv_mma.set(tcgen05.Field.ACCUMULATE, True)
|
||||
cute.arch.fence_view_async_tmem_store()
|
||||
vh.release()
|
||||
acc_pipe.producer_commit(acc_st); acc_st.advance()
|
||||
acc_pipe.producer_tail(acc_st)
|
||||
|
||||
# EPILOGUE
|
||||
if warp_idx < self.mma_warp_id:
|
||||
tmem.allocate(self.num_tmem_alloc_cols)
|
||||
tmem.wait_for_alloc()
|
||||
tmem_ptr = tmem.retrieve_ptr(self.qk_acc_dtype)
|
||||
sfw_idx = tidx % (32 * len(self.epilogue_warp_id))
|
||||
|
||||
# --- S load (QK C-fragment layout) ---
|
||||
tmem_load_atom = cute.make_copy_atom(tcgen05.copy.Ld32x32bOp(tcgen05.copy.Repetition(32)), self.qk_acc_dtype)
|
||||
tiled_tmem_load = tcgen05.make_tmem_copy(tmem_load_atom, tStS0)
|
||||
thr_load = tiled_tmem_load.get_slice(sfw_idx)
|
||||
tTMEM_LOADtS = thr_load.partition_S(tStS0)
|
||||
|
||||
# S coordinate tensor (QK C-fragment)
|
||||
cS = cute.make_identity_tensor((self.qk_mma_tiler[0], self.qk_mma_tiler[1]))
|
||||
tScS = qk_thr.partition_C(cS)
|
||||
tTMEM_LOADcS = thr_load.partition_D(tScS)
|
||||
|
||||
# --- P store (QK C-fragment layout composition, FMHA pattern) ---
|
||||
# P logical columns = PV K = QK N = pv_mma_tiler[2]
|
||||
# Packed FP32 columns: BF16 pairs packed into FP32 words
|
||||
p_cols_fp32 = self.pv_mma_tiler[2] * self.q_dtype.width // self.qk_acc_dtype.width
|
||||
# BF16: 128 * 16 / 32 = 64
|
||||
|
||||
# P TMEM destination: QK C-fragment layout composed with P sub-tile
|
||||
tStP_layout = cute.composition(
|
||||
tStS.layout,
|
||||
cute.make_layout((self.pv_mma_tiler[0], p_cols_fp32)),
|
||||
)
|
||||
tStP0 = cute.make_tensor(
|
||||
tStS.iterator + self.tmem_p0_offset,
|
||||
tStP_layout,
|
||||
)
|
||||
|
||||
# P TMEM store atom and tiled copy
|
||||
tmem_store_atom = cute.make_copy_atom(
|
||||
tcgen05.copy.St32x32bOp(tcgen05.copy.Repetition(32)),
|
||||
self.qk_acc_dtype,
|
||||
)
|
||||
tiled_tmem_store = tcgen05.make_tmem_copy(tmem_store_atom, tStP0)
|
||||
thr_store = tiled_tmem_store.get_slice(sfw_idx)
|
||||
tTMEM_STOREtP = thr_store.partition_D(tStP0)
|
||||
|
||||
# P coordinate tensor: QK C-fragment coordinate composed with P sub-tile
|
||||
tScP_layout = cute.composition(
|
||||
tScS.layout,
|
||||
cute.make_layout((self.pv_mma_tiler[0], p_cols_fp32)),
|
||||
)
|
||||
tScP = cute.make_tensor(tScS.iterator, tScP_layout)
|
||||
tTMEM_STOREcP = thr_store.partition_S(tScP)
|
||||
|
||||
for kt in range(n_kv_tiles):
|
||||
si_handle = s_cons.wait_and_advance()
|
||||
|
||||
# Load S from TMEM (FP32, QK C-fragment layout)
|
||||
tTMEM_LOADrS = cute.make_rmem_tensor(tTMEM_LOADcS.shape, self.qk_acc_dtype)
|
||||
cute.copy(tiled_tmem_load, tTMEM_LOADtS, tTMEM_LOADrS)
|
||||
|
||||
# Register bridge (FMHA pattern):
|
||||
# rP_words: FP32 backing store with store-partition shape
|
||||
# rP_bf16: BF16 view over same registers using QK-load layout
|
||||
rP_words = cute.make_rmem_tensor(tTMEM_STOREcP.shape, self.qk_acc_dtype)
|
||||
rP_bf16 = cute.make_tensor(
|
||||
cute.recast_ptr(rP_words.iterator, dtype=self.q_dtype),
|
||||
tTMEM_LOADrS.layout,
|
||||
)
|
||||
|
||||
# Fragmented load→convert→store:
|
||||
# Load S as FP32, convert to BF16, store through rP_bf16 view
|
||||
frg_cnt = 4
|
||||
frg_tile = cute.size(tTMEM_LOADrS) // frg_cnt
|
||||
tTMEM_LOADrS_frg = cute.logical_divide(tTMEM_LOADrS, cute.make_layout(frg_tile))
|
||||
rP_bf16_frg = cute.logical_divide(rP_bf16, cute.make_layout(frg_tile))
|
||||
for j in range(frg_cnt):
|
||||
s_vec = tTMEM_LOADrS_frg[None, j].load()
|
||||
rP_bf16_frg[None, j].store(s_vec.to(self.q_dtype))
|
||||
|
||||
# Copy packed FP32 backing registers to TMEM
|
||||
cute.copy(tiled_tmem_store, rP_words, tTMEM_STOREtP)
|
||||
cute.arch.fence_view_async_tmem_store()
|
||||
si_handle.release()
|
||||
softmax_done_bar.arrive()
|
||||
|
||||
tCtO_base = cute.make_tensor(tmem_ptr + self.tmem_o0_offset, tCtO_fake.layout)
|
||||
acc_cons_st = pipeline.make_pipeline_state(pipeline.PipelineUserType.Consumer, self.num_acc_stage)
|
||||
c_grp = pipeline.CooperativeGroup(pipeline.Agent.Thread, 32 * len(self.epilogue_warp_id))
|
||||
c_pipe = pipeline.PipelineTmaStore.create(num_stages=self.num_c_stage, producer_group=c_grp)
|
||||
acc_cons_st = utils.gemm.sm100.epilogue_tma_store(self, tidx, warp_idx, tma_c, tCtO_base, sC, tCgC, epi_tile, 0, const_expr(lambda x: x), (0,0,0), acc_cons_st, acc_pipe, c_pipe)
|
||||
c_pipe.producer_tail()
|
||||
tmem.relinquish_alloc_permit()
|
||||
tmem.free(tmem_ptr)
|
||||
|
||||
|
||||
def test():
|
||||
torch.manual_seed(42)
|
||||
for n in [128]:
|
||||
m, hd = 128, HEAD_DIM
|
||||
q = torch.randn(m, hd, 1, dtype=torch.bfloat16, device='cuda')
|
||||
k = torch.randn(n, hd, 1, dtype=torch.bfloat16, device='cuda')
|
||||
v = torch.randn(n, hd, dtype=torch.bfloat16, device='cuda')
|
||||
# V passed as (n, hd) row-major — FMHA-style reconstruction inside kernel
|
||||
v_kernel = v.unsqueeze(-1)
|
||||
c = torch.zeros(m, hd, 1, dtype=torch.bfloat16, device='cuda')
|
||||
qf = q[:,:,0].float(); kf = k[:,:,0].float()
|
||||
ref = (qf @ kf.T).bfloat16().float() @ v.float()
|
||||
mQ = ct.from_dlpack(q).mark_layout_dynamic(leading_dim=ct.get_leading_dim(q))
|
||||
mK = ct.from_dlpack(k).mark_layout_dynamic(leading_dim=ct.get_leading_dim(k))
|
||||
mV = ct.from_dlpack(v_kernel).mark_layout_dynamic(leading_dim=ct.get_leading_dim(v_kernel))
|
||||
mC = ct.from_dlpack(c).mark_layout_dynamic(leading_dim=ct.get_leading_dim(c))
|
||||
stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream)
|
||||
kernel = FmhaV3()
|
||||
print(f'n={n}: Compiling...', flush=True)
|
||||
compiled = cute.compile(kernel, mQ, mK, mV, mC, stream)
|
||||
print(f'n={n}: tmem_offsets: s0={kernel.tmem_s0_offset} p0={kernel.tmem_p0_offset} o0={kernel.tmem_o0_offset} alloc={kernel.num_tmem_alloc_cols}', flush=True)
|
||||
print(f'n={n}: Running...', flush=True)
|
||||
compiled(mQ, mK, mV, mC, stream)
|
||||
torch.cuda.synchronize()
|
||||
out = c[:,:,0].float()
|
||||
cos = torch.nn.functional.cosine_similarity(out.flatten().unsqueeze(0), ref.flatten().unsqueeze(0)).item()
|
||||
print(f'FMHA v3 n={n} V=ones: cosine {cos:.6f} {"PASS" if cos >= 0.99 else "FAIL"}')
|
||||
if cos < 0.99:
|
||||
print(f' out[0,:4]={out[0,:4].tolist()} ref[0,:4]={ref[0,:4].tolist()}')
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
||||
383
tests/unit/test_fmha_v3_softmax.py
Normal file
383
tests/unit/test_fmha_v3_softmax.py
Normal file
@@ -0,0 +1,383 @@
|
||||
"""
|
||||
FMHA v3 + Stage C: QK -> online softmax -> PV with KV-tile interleaving.
|
||||
Stage C: row_max, exp2, O rescale, row_sum, final normalization.
|
||||
FMHA pattern P store preserved from Stage B.
|
||||
"""
|
||||
import math
|
||||
import torch, cutlass, cutlass.cute as cute, cutlass.utils as utils, cutlass.pipeline as pipeline
|
||||
from cutlass.cute.nvgpu import cpasync, tcgen05
|
||||
from cutlass import Float32, BFloat16, Int32, Boolean, const_expr
|
||||
from cutlass.utils import LayoutEnum
|
||||
from cutlass.utils.tmem_allocator import find_tmem_tensor_col_offset
|
||||
import cuda.bindings.driver as cuda
|
||||
import cutlass.torch as ct
|
||||
|
||||
HEAD_DIM = 64
|
||||
|
||||
class FmhaV3Softmax:
|
||||
def __init__(self):
|
||||
self.acc_dtype = Float32; self.qk_acc_dtype = Float32
|
||||
self.q_dtype = BFloat16; self.o_dtype = BFloat16; self.c_dtype = BFloat16
|
||||
self.use_2cta_instrs = False; self.epilog_sync_bar_id = 1
|
||||
self.cluster_shape_mn = (1, 1); self.cta_group = tcgen05.CtaGroup.ONE
|
||||
self.epilogue_warp_id = (0,1,2,3); self.mma_warp_id = 4; self.tma_warp_id = 5
|
||||
self.threads_per_cta = 192; self.num_c_stage = 2
|
||||
self.kv_stage = 2; self.q_stage = 1; self.num_c_stage = 2
|
||||
|
||||
def _setup(self, qk_mma, pv_mma):
|
||||
qk_ik = cute.size(qk_mma.shape_mnk, mode=[2])
|
||||
self.qk_mma_tiler = (128, 128, qk_ik * 4)
|
||||
pv_ik = cute.size(pv_mma.shape_mnk, mode=[2])
|
||||
self.pv_mma_tiler = (128, HEAD_DIM, pv_ik * (128 // pv_ik))
|
||||
self.mma_tiler = self.qk_mma_tiler
|
||||
self.cluster_layout_vmnk = cute.tiled_divide(cute.make_layout((1,1,1)), (qk_mma.thr_id.shape,))
|
||||
self.cta_tile_shape_mnk = (self.qk_mma_tiler[0]//cute.size(qk_mma.thr_id.shape), HEAD_DIM, self.qk_mma_tiler[2])
|
||||
self.c_layout = LayoutEnum.ROW_MAJOR
|
||||
self.epi_tile = utils.sm100.compute_epilogue_tile_shape(self.cta_tile_shape_mnk, False, self.c_layout, self.o_dtype)
|
||||
self.num_ab_stage = 1; self.num_acc_stage = 1
|
||||
self.q_smem_s = utils.sm100.make_smem_layout_a(qk_mma, self.qk_mma_tiler, self.q_dtype, self.q_stage)
|
||||
self.k_smem_s = utils.sm100.make_smem_layout_b(qk_mma, self.qk_mma_tiler, self.q_dtype, self.kv_stage)
|
||||
self.v_smem_s = utils.sm100.make_smem_layout_b(pv_mma, self.pv_mma_tiler, self.q_dtype, self.kv_stage)
|
||||
self.c_smem_s = utils.sm100.make_smem_layout_epi(self.o_dtype, self.c_layout, self.epi_tile, 2)
|
||||
self.p_tmem_s = utils.sm100.make_smem_layout_a(pv_mma, self.pv_mma_tiler, self.q_dtype, 1)
|
||||
qk_thr = qk_mma.get_slice(0); qk_as = qk_thr.partition_shape_C(self.qk_mma_tiler[:2])
|
||||
tStS = qk_thr.make_fragment_C(qk_as)
|
||||
pv_thr = pv_mma.get_slice(0); pv_as = pv_thr.partition_shape_C(self.pv_mma_tiler[:2])
|
||||
tOtO = pv_thr.make_fragment_C(pv_as)
|
||||
self.tmem_s0_offset = 0; self.tmem_p0_offset = 32
|
||||
# P occupies [tmem_p0_offset, tmem_p0_offset + p_cols_fp32)
|
||||
# S occupies [0, qk_mma_tiler[1]) = [0, 128)
|
||||
# O must NOT overlap P. Place O after max(S end, P end), aligned to 32.
|
||||
p_cols_fp32 = self.pv_mma_tiler[2] * self.q_dtype.width // self.qk_acc_dtype.width
|
||||
p_end = self.tmem_p0_offset + p_cols_fp32 # 32 + 64 = 96
|
||||
s_cols = self.qk_mma_tiler[1] # 128
|
||||
o_after = max(s_cols, p_end) # 128
|
||||
self.tmem_o0_offset = ((o_after + 31) // 32) * 32 # align to 32 = 128
|
||||
o_cols = find_tmem_tensor_col_offset(tOtO) # footprint of O
|
||||
total = self.tmem_o0_offset + o_cols
|
||||
# Must be multiple of 32 AND power of 2
|
||||
self.num_tmem_alloc_cols = 1
|
||||
while self.num_tmem_alloc_cols < total:
|
||||
self.num_tmem_alloc_cols *= 2
|
||||
cta = cute.size(qk_mma.thr_id.shape)
|
||||
q_s = cute.slice_(self.q_smem_s,(None,None,None,0)); k_s = cute.slice_(self.k_smem_s,(None,None,None,0))
|
||||
self.q_tx_bytes = cute.size_in_bytes(self.q_dtype, q_s) * cta
|
||||
self.kv_tx_bytes = cute.size_in_bytes(self.q_dtype, k_s) * cta
|
||||
self.scale_softmax_log2 = Float32(1.0 / math.sqrt(HEAD_DIM) * math.log2(math.e))
|
||||
|
||||
@cute.jit
|
||||
def __call__(self, q, k, v, c, stream):
|
||||
self.q_dtype = q.element_type; self.o_dtype = c.element_type; self.c_dtype = self.o_dtype
|
||||
self.a_major = LayoutEnum.from_tensor(q).mma_major_mode()
|
||||
self.b_major = LayoutEnum.from_tensor(k).mma_major_mode()
|
||||
s_k = cute.size(v, mode=[0])
|
||||
# FMHA-style V: reconstruct as (HEAD_DIM, s_k, 1) MN-major
|
||||
v_fmha = cute.make_tensor(
|
||||
v.iterator,
|
||||
cute.make_layout(
|
||||
(HEAD_DIM, s_k, 1),
|
||||
stride=(1, HEAD_DIM, HEAD_DIM * s_k),
|
||||
),
|
||||
)
|
||||
self.v_major = LayoutEnum.from_tensor(v_fmha).mma_major_mode()
|
||||
self.c_layout = LayoutEnum.from_tensor(c)
|
||||
qk_mma = utils.sm100.make_trivial_tiled_mma(self.q_dtype, self.q_dtype, self.a_major, self.b_major, self.qk_acc_dtype, self.cta_group, (128,128), tcgen05.OperandSource.SMEM)
|
||||
pv_mma = utils.sm100.make_trivial_tiled_mma(self.q_dtype, self.q_dtype, cute.nvgpu.OperandMajorMode.K, self.v_major, self.qk_acc_dtype, self.cta_group, (128,HEAD_DIM), tcgen05.OperandSource.TMEM)
|
||||
self._setup(qk_mma, pv_mma)
|
||||
q_s = cute.slice_(self.q_smem_s,(None,None,None,0)); k_s = cute.slice_(self.k_smem_s,(None,None,None,0)); v_s = cute.slice_(self.v_smem_s,(None,None,None,0))
|
||||
tma_q,mQ = cute.nvgpu.make_tiled_tma_atom_A(utils.sm100.cluster_shape_to_tma_atom_A(self.cluster_shape_mn,qk_mma.thr_id),q,q_s,self.qk_mma_tiler,qk_mma,self.cluster_layout_vmnk.shape)
|
||||
tma_k,mK = cute.nvgpu.make_tiled_tma_atom_B(utils.sm100.cluster_shape_to_tma_atom_B(self.cluster_shape_mn,qk_mma.thr_id),k,k_s,self.qk_mma_tiler,qk_mma,self.cluster_layout_vmnk.shape)
|
||||
tma_v,mV = cute.nvgpu.make_tiled_tma_atom_B(utils.sm100.cluster_shape_to_tma_atom_B(self.cluster_shape_mn,pv_mma.thr_id),v_fmha,v_s,self.pv_mma_tiler,pv_mma,self.cluster_layout_vmnk.shape)
|
||||
epi_s = cute.select(self.c_smem_s,mode=[0,1])
|
||||
tma_c,mC = cpasync.make_tiled_tma_atom(cpasync.CopyBulkTensorTileS2GOp(),c,epi_s,self.epi_tile)
|
||||
self._kernel(qk_mma,pv_mma,tma_q,mQ,tma_k,mK,tma_v,mV,tma_c,mC,self.cluster_layout_vmnk,self.q_smem_s,self.k_smem_s,self.v_smem_s,self.p_tmem_s,self.c_smem_s,self.epi_tile).launch(grid=(1,1,1),block=[self.threads_per_cta,1,1],stream=stream)
|
||||
|
||||
@cute.kernel
|
||||
def _kernel(self, qk_mma, pv_mma, tma_q, mQ, tma_k, mK, tma_v, mV, tma_c, mC, cl_vmnk, q_smem_s, k_smem_s, v_smem_s, p_tmem_s, c_smem_s, epi_tile):
|
||||
warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
|
||||
tidx,_,_ = cute.arch.thread_idx()
|
||||
if warp_idx == self.tma_warp_id:
|
||||
cpasync.prefetch_descriptor(tma_q); cpasync.prefetch_descriptor(tma_k); cpasync.prefetch_descriptor(tma_v); cpasync.prefetch_descriptor(tma_c)
|
||||
|
||||
@cute.struct
|
||||
class SS:
|
||||
q_bar: cute.struct.MemRange[cutlass.Int64, self.q_stage*2]
|
||||
kv_bar: cute.struct.MemRange[cutlass.Int64, self.kv_stage*2]
|
||||
s_bar: cute.struct.MemRange[cutlass.Int64, 2]
|
||||
acc_bar: cute.struct.MemRange[cutlass.Int64, self.num_acc_stage*2]
|
||||
tmem_dealloc: cutlass.Int64; holding: cutlass.Int32
|
||||
smem = utils.SmemAllocator(); st = smem.allocate(SS)
|
||||
|
||||
qp,qc = pipeline.PipelineTmaUmma.create(barrier_storage=st.q_bar.data_ptr(),num_stages=self.q_stage,producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread,1),tx_count=self.q_tx_bytes,cta_layout_vmnk=cl_vmnk,defer_sync=True).make_participants()
|
||||
kvp,kvc = pipeline.PipelineTmaUmma.create(barrier_storage=st.kv_bar.data_ptr(),num_stages=self.kv_stage,producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread,1),tx_count=self.kv_tx_bytes,cta_layout_vmnk=cl_vmnk,defer_sync=True).make_participants()
|
||||
s_prod,s_cons = pipeline.PipelineUmmaAsync.create(barrier_storage=st.s_bar.data_ptr(),num_stages=1,producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread,32*len(self.epilogue_warp_id))).make_participants()
|
||||
softmax_done_bar = pipeline.NamedBarrier(barrier_id=3, num_threads=32 + 32*len(self.epilogue_warp_id))
|
||||
acc_pipe = pipeline.PipelineUmmaAsync.create(barrier_storage=st.acc_bar.data_ptr(),num_stages=self.num_acc_stage,producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread,len(self.epilogue_warp_id)),cta_layout_vmnk=cl_vmnk,defer_sync=True)
|
||||
tmem_bar = pipeline.NamedBarrier(barrier_id=2,num_threads=32*len((self.mma_warp_id,*self.epilogue_warp_id)))
|
||||
tmem = utils.TmemAllocator(st.holding.ptr,barrier_for_retrieve=tmem_bar,allocator_warp_id=self.epilogue_warp_id[0],is_two_cta=cute.size(qk_mma.thr_id.shape)==2,two_cta_tmem_dealloc_mbar_ptr=st.tmem_dealloc.ptr)
|
||||
pipeline.pipeline_init_arrive(cluster_shape_mn=cl_vmnk,is_relaxed=True)
|
||||
|
||||
sQ = smem.allocate_tensor(element_type=self.q_dtype,layout=q_smem_s.outer,byte_alignment=128,swizzle=q_smem_s.inner)
|
||||
sK = smem.allocate_tensor(element_type=self.q_dtype,layout=k_smem_s.outer,byte_alignment=128,swizzle=k_smem_s.inner)
|
||||
sV = smem.allocate_tensor(element_type=self.q_dtype,layout=v_smem_s.outer,byte_alignment=128,swizzle=v_smem_s.inner)
|
||||
sC = smem.allocate_tensor(element_type=self.o_dtype,layout=c_smem_s.outer,byte_alignment=128,swizzle=c_smem_s.inner)
|
||||
|
||||
gQ = cute.local_tile(mQ,cute.slice_(self.qk_mma_tiler,(None,0,None)),(None,None,None))
|
||||
gK = cute.local_tile(mK,cute.slice_(self.qk_mma_tiler,(0,None,None)),(None,None,None))
|
||||
gV = cute.local_tile(mV,cute.slice_(self.pv_mma_tiler,(0,None,None)),(None,None,None))
|
||||
gC = cute.local_tile(mC,cute.slice_(self.pv_mma_tiler,(None,None,0)),(None,None,None))
|
||||
n_kv_tiles = cute.size(gK, mode=[3])
|
||||
|
||||
qk_thr = qk_mma.get_slice(0); pv_thr = pv_mma.get_slice(0)
|
||||
tCgQ = qk_thr.partition_A(gQ); tCgK = qk_thr.partition_B(gK)
|
||||
tCgV = pv_thr.partition_B(gV); tCgC = pv_thr.partition_C(gC)
|
||||
a_lay = cute.make_layout(cute.slice_(cl_vmnk,(0,0,None,0)).shape)
|
||||
tAsQ,tAgQ = cpasync.tma_partition(tma_q,0,a_lay,cute.group_modes(sQ,0,3),cute.group_modes(tCgQ,0,3))
|
||||
b_lay = cute.make_layout(cute.slice_(cl_vmnk,(0,None,0,0)).shape)
|
||||
tBsK,tBgK = cpasync.tma_partition(tma_k,0,b_lay,cute.group_modes(sK,0,3),cute.group_modes(tCgK,0,3))
|
||||
tVsV,tVgV = cpasync.tma_partition(tma_v,0,b_lay,cute.group_modes(sV,0,3),cute.group_modes(tCgV,0,3))
|
||||
tAgQ = tAgQ[(None,0,None,0)]; tBgK = tBgK[(None,0,None,0)]; tVgV = tVgV[(None,0,None,0)]
|
||||
|
||||
tCrQ = qk_mma.make_fragment_A(sQ); tCrK = qk_mma.make_fragment_B(sK)
|
||||
tCrV = pv_mma.make_fragment_B(sV)
|
||||
|
||||
qk_as = qk_thr.partition_shape_C(self.qk_mma_tiler[:2])
|
||||
tStS = qk_thr.make_fragment_C(qk_as)
|
||||
tStS0 = cute.make_tensor(tStS.iterator + self.tmem_s0_offset, tStS.layout)
|
||||
pv_as = pv_thr.partition_shape_C(self.pv_mma_tiler[:2])
|
||||
tOtO = pv_thr.make_fragment_C(pv_as)
|
||||
tOtO0 = cute.make_tensor(tOtO.iterator + self.tmem_o0_offset, tOtO.layout)
|
||||
|
||||
# --- PV read view (for MMA only, NOT for softmax store) ---
|
||||
tP = cute.make_tensor(tStS.iterator, p_tmem_s.outer)
|
||||
tOrP_base = pv_thr.make_fragment_A(tP)
|
||||
tOrP = tOrP_base[(None,None,None,0)]
|
||||
tOrP0 = cute.make_tensor(
|
||||
tOrP.iterator + self.qk_acc_dtype.width // self.q_dtype.width * self.tmem_p0_offset,
|
||||
tOrP.layout)
|
||||
|
||||
tCtS_fake = qk_mma.make_fragment_C(cute.append(qk_as, self.num_acc_stage))
|
||||
tCtO_fake = pv_mma.make_fragment_C(cute.append(pv_as, self.num_acc_stage))
|
||||
pipeline.pipeline_init_wait(cluster_shape_mn=cl_vmnk)
|
||||
|
||||
# TMA LOAD
|
||||
if warp_idx == self.tma_warp_id:
|
||||
qp.reset(); qh = qp.acquire_and_advance()
|
||||
cute.copy(tma_q,tAgQ[(None,qh.count)],tAsQ[(None,qh.index)],tma_bar_ptr=qh.barrier)
|
||||
qp.tail()
|
||||
kvp.reset(); pk = kvp.try_acquire()
|
||||
for kt in cutlass.range(n_kv_tiles,unroll=1):
|
||||
kh = kvp.acquire_and_advance(pk)
|
||||
cute.copy(tma_k,tBgK[(None,kh.count)],tBsK[(None,kh.index)],tma_bar_ptr=kh.barrier)
|
||||
pk = cutlass.Boolean(1)
|
||||
vh = kvp.acquire_and_advance(pk)
|
||||
cute.copy(tma_v,tVgV[(None,vh.count)],tVsV[(None,vh.index)],tma_bar_ptr=vh.barrier)
|
||||
pk = cutlass.Boolean(1)
|
||||
kvp.tail()
|
||||
|
||||
# MMA
|
||||
if warp_idx == self.mma_warp_id:
|
||||
tmem.wait_for_alloc()
|
||||
qc.reset(); qh = qc.wait_and_advance(); qh.release()
|
||||
kvc.reset(); pk = kvc.try_wait()
|
||||
acc_st = pipeline.make_pipeline_state(pipeline.PipelineUserType.Producer, self.num_acc_stage)
|
||||
acc_pipe.producer_acquire(acc_st)
|
||||
for kt in range(n_kv_tiles):
|
||||
kh = kvc.wait_and_advance(pk); pk = cutlass.Boolean(1)
|
||||
sh = s_prod.acquire_and_advance()
|
||||
qk_mma.set(tcgen05.Field.ACCUMULATE, False)
|
||||
for kb in cutlass.range(cute.size(tCrQ,mode=[2]), unroll_full=True):
|
||||
cute.gemm(qk_mma, tStS0, tCrQ[(None,None,kb,0)], tCrK[(None,None,kb,kh.index)], tStS0)
|
||||
qk_mma.set(tcgen05.Field.ACCUMULATE, True)
|
||||
cute.arch.fence_view_async_tmem_store()
|
||||
sh.commit(); kh.release()
|
||||
softmax_done_bar.arrive_and_wait()
|
||||
vh = kvc.wait_and_advance(pk); pk = cutlass.Boolean(1)
|
||||
pv_mma.set(tcgen05.Field.ACCUMULATE, kt != 0)
|
||||
for kb in cutlass.range(cute.size(tOrP0,mode=[2]), unroll_full=True):
|
||||
cute.gemm(pv_mma, tOtO0, tOrP0[(None,None,kb)], tCrV[(None,None,kb,vh.index)], tOtO0)
|
||||
pv_mma.set(tcgen05.Field.ACCUMULATE, True)
|
||||
cute.arch.fence_view_async_tmem_store()
|
||||
vh.release()
|
||||
acc_pipe.producer_commit(acc_st); acc_st.advance()
|
||||
acc_pipe.producer_tail(acc_st)
|
||||
|
||||
# ===================== EPILOGUE WARPS (STAGE C: ONLINE SOFTMAX) =====================
|
||||
if warp_idx < self.mma_warp_id:
|
||||
tmem.allocate(self.num_tmem_alloc_cols)
|
||||
tmem.wait_for_alloc()
|
||||
tmem_ptr = tmem.retrieve_ptr(self.qk_acc_dtype)
|
||||
sfw_idx = tidx % (32 * len(self.epilogue_warp_id))
|
||||
|
||||
# --- S load (QK C-fragment) ---
|
||||
tmem_load_atom = cute.make_copy_atom(tcgen05.copy.Ld32x32bOp(tcgen05.copy.Repetition(32)), self.qk_acc_dtype)
|
||||
tiled_tmem_load = tcgen05.make_tmem_copy(tmem_load_atom, tStS0)
|
||||
thr_load = tiled_tmem_load.get_slice(sfw_idx)
|
||||
tTMEM_LOADtS = thr_load.partition_S(tStS0)
|
||||
cS = cute.make_identity_tensor((self.qk_mma_tiler[0], self.qk_mma_tiler[1]))
|
||||
tScS = qk_thr.partition_C(cS)
|
||||
tTMEM_LOADcS = thr_load.partition_D(tScS)
|
||||
|
||||
# --- P store (QK C-fragment composition, FMHA pattern) ---
|
||||
p_cols_fp32 = self.pv_mma_tiler[2] * self.q_dtype.width // self.qk_acc_dtype.width
|
||||
tStP_layout = cute.composition(tStS.layout, cute.make_layout((self.pv_mma_tiler[0], p_cols_fp32)))
|
||||
tStP0 = cute.make_tensor(tStS.iterator + self.tmem_p0_offset, tStP_layout)
|
||||
tmem_store_atom = cute.make_copy_atom(tcgen05.copy.St32x32bOp(tcgen05.copy.Repetition(32)), self.qk_acc_dtype)
|
||||
tiled_tmem_store = tcgen05.make_tmem_copy(tmem_store_atom, tStP0)
|
||||
thr_store = tiled_tmem_store.get_slice(sfw_idx)
|
||||
tTMEM_STOREtP = thr_store.partition_D(tStP0)
|
||||
tScP_layout = cute.composition(tScS.layout, cute.make_layout((self.pv_mma_tiler[0], p_cols_fp32)))
|
||||
tScP = cute.make_tensor(tScS.iterator, tScP_layout)
|
||||
tTMEM_STOREcP = thr_store.partition_S(tScP)
|
||||
|
||||
# --- C6: O TMEM load/store for rescale (correction_rescale pattern) ---
|
||||
corr_tile_size = 16
|
||||
cO = cute.make_identity_tensor((self.pv_mma_tiler[0], self.pv_mma_tiler[1]))
|
||||
tOcO = pv_thr.partition_C(cO)
|
||||
o_tmem_load_atom = cute.make_copy_atom(tcgen05.copy.Ld32x32bOp(tcgen05.copy.Repetition(corr_tile_size)), self.qk_acc_dtype)
|
||||
o_tmem_store_atom = cute.make_copy_atom(tcgen05.copy.St32x32bOp(tcgen05.copy.Repetition(corr_tile_size)), self.qk_acc_dtype)
|
||||
tOtO_i_layout = cute.composition(tOtO0.layout, cute.make_layout((128, corr_tile_size)))
|
||||
tOcO_i_layout = cute.composition(tOcO.layout, cute.make_layout((128, corr_tile_size)))
|
||||
tOtO_i = cute.make_tensor(tOtO0.iterator, tOtO_i_layout)
|
||||
tOcO_i = cute.make_tensor(tOcO.iterator, tOcO_i_layout)
|
||||
o_tiled_tmem_load = tcgen05.make_tmem_copy(o_tmem_load_atom, tOtO_i)
|
||||
o_tiled_tmem_store = tcgen05.make_tmem_copy(o_tmem_store_atom, tOtO_i)
|
||||
o_thr_load = o_tiled_tmem_load.get_slice(sfw_idx)
|
||||
o_thr_store = o_tiled_tmem_store.get_slice(sfw_idx)
|
||||
tTMEM_LOADtO = o_thr_load.partition_S(tOtO_i)
|
||||
tTMEM_LOADcO = o_thr_load.partition_D(tOcO_i)
|
||||
tTMEM_STOREtO = o_thr_store.partition_D(tOtO_i)
|
||||
o_col_tiles = self.pv_mma_tiler[1] // corr_tile_size
|
||||
|
||||
# --- C2: Per-thread row state (persist across KV tiles) ---
|
||||
row_max = -cutlass.Float32.inf
|
||||
row_sum = cutlass.Float32(0.0)
|
||||
|
||||
# --- C3: QK scale = 1/sqrt(HEAD_DIM) * log2(e) for exp2 ---
|
||||
scale = self.scale_softmax_log2
|
||||
|
||||
# =============================================================
|
||||
# Per-KV-tile online softmax loop
|
||||
# =============================================================
|
||||
for kt in range(n_kv_tiles):
|
||||
si_handle = s_cons.wait_and_advance()
|
||||
|
||||
# Load S from TMEM (FP32, QK C-fragment layout)
|
||||
tTMEM_LOADrS = cute.make_rmem_tensor(tTMEM_LOADcS.shape, self.qk_acc_dtype)
|
||||
cute.copy(tiled_tmem_load, tTMEM_LOADtS, tTMEM_LOADrS)
|
||||
|
||||
# --- C4: Compute tile_max via .reduce(MAX) ---
|
||||
old_row_max = row_max
|
||||
row_max = tTMEM_LOADrS.load().reduce(cute.ReductionOp.MAX, row_max, 0)
|
||||
row_max_safe = row_max
|
||||
if row_max == -cutlass.Float32.inf:
|
||||
row_max_safe = cutlass.Float32(0.0)
|
||||
|
||||
# --- C5: Compute rescale factor ---
|
||||
acc_scale = cute.math.exp2(old_row_max - row_max_safe, fastmath=False)
|
||||
|
||||
# --- C6: Rescale O in TMEM (load O, multiply by acc_scale, store O) ---
|
||||
if kt > 0:
|
||||
tTMrO = cute.make_rmem_tensor((tTMEM_LOADcO.shape, o_col_tiles), self.qk_acc_dtype)
|
||||
for i in range(o_col_tiles):
|
||||
tTMrO_i_ = tTMrO[None, i]
|
||||
tTMrO_i_layout = cute.composition(tTMrO_i_.layout, cute.make_layout(tTMrO.shape[0]))
|
||||
tTMrO_i = cute.make_tensor(tTMrO_i_.iterator, tTMrO_i_layout)
|
||||
tTMEM_LOADtO_i = cute.make_tensor(tTMEM_LOADtO.iterator + i * corr_tile_size, tTMEM_LOADtO.layout)
|
||||
tTMEM_STOREtO_i = cute.make_tensor(tTMEM_STOREtO.iterator + i * corr_tile_size, tTMEM_STOREtO.layout)
|
||||
cute.copy(o_tiled_tmem_load, tTMEM_LOADtO_i, tTMrO_i)
|
||||
for j in cutlass.range(cute.size(tTMrO_i), vectorize=True):
|
||||
tTMrO_i[j] = tTMrO_i[j] * acc_scale
|
||||
cute.copy(o_tiled_tmem_store, tTMrO_i, tTMEM_STOREtO_i)
|
||||
cute.arch.fence_view_async_tmem_store()
|
||||
|
||||
# Rescale row_sum
|
||||
row_sum = row_sum * acc_scale
|
||||
|
||||
# --- C7: Compute P = exp2((S - row_max_safe) * scale) ---
|
||||
minus_row_max_scale = (cutlass.Float32(0.0) - row_max_safe) * scale
|
||||
|
||||
# Register bridge (FMHA pattern: FP32 backing + BF16 view)
|
||||
rP_words = cute.make_rmem_tensor(tTMEM_STOREcP.shape, self.qk_acc_dtype)
|
||||
rP_bf16 = cute.make_tensor(cute.recast_ptr(rP_words.iterator, dtype=self.q_dtype), tTMEM_LOADrS.layout)
|
||||
|
||||
frg_cnt = 4
|
||||
frg_tile = cute.size(tTMEM_LOADrS) // frg_cnt
|
||||
tTMEM_LOADrS_frg = cute.logical_divide(tTMEM_LOADrS, cute.make_layout(frg_tile))
|
||||
rP_bf16_frg = cute.logical_divide(rP_bf16, cute.make_layout(frg_tile))
|
||||
|
||||
# Scale S, compute exp2, store through register bridge
|
||||
for j in range(frg_cnt):
|
||||
for k in cutlass.range(cute.size(tTMEM_LOADrS_frg, mode=[0]), vectorize=True):
|
||||
tTMEM_LOADrS_frg[k, j] = tTMEM_LOADrS_frg[k, j] * scale + minus_row_max_scale
|
||||
tTMEM_LOADrS_frg[k, j] = cute.math.exp2(tTMEM_LOADrS_frg[k, j], fastmath=False)
|
||||
s_vec = tTMEM_LOADrS_frg[None, j].load()
|
||||
rP_bf16_frg[None, j].store(s_vec.to(self.q_dtype))
|
||||
|
||||
# Store P to TMEM
|
||||
cute.copy(tiled_tmem_store, rP_words, tTMEM_STOREtP)
|
||||
cute.arch.fence_view_async_tmem_store()
|
||||
si_handle.release()
|
||||
softmax_done_bar.arrive()
|
||||
|
||||
# --- C8: Row sum accumulation (CUTLASS FMHA packed f32x2 pattern) ---
|
||||
# P values still in tTMEM_LOADrS registers.
|
||||
# 4 accumulators for 4 reduction_unroll columns.
|
||||
local_row_sum_0 = (cutlass.Float32(0.0), cutlass.Float32(0.0))
|
||||
local_row_sum_1 = (cutlass.Float32(0.0), cutlass.Float32(0.0))
|
||||
local_row_sum_2 = (cutlass.Float32(0.0), cutlass.Float32(0.0))
|
||||
local_row_sum_3 = (cutlass.Float32(0.0), cutlass.Float32(0.0))
|
||||
|
||||
reduction_unroll = 4
|
||||
rfrg_tile = cute.size(tTMEM_LOADrS) // reduction_unroll
|
||||
tTMEM_LOADrS_rfrg = cute.logical_divide(tTMEM_LOADrS, cute.make_layout(rfrg_tile))
|
||||
|
||||
for j in cutlass.range_constexpr(0, cute.size(tTMEM_LOADrS_rfrg, mode=[0]), 2):
|
||||
local_row_sum_0 = cute.arch.add_packed_f32x2(
|
||||
local_row_sum_0, (tTMEM_LOADrS_rfrg[j, 0], tTMEM_LOADrS_rfrg[j + 1, 0]))
|
||||
local_row_sum_1 = cute.arch.add_packed_f32x2(
|
||||
local_row_sum_1, (tTMEM_LOADrS_rfrg[j, 1], tTMEM_LOADrS_rfrg[j + 1, 1]))
|
||||
local_row_sum_2 = cute.arch.add_packed_f32x2(
|
||||
local_row_sum_2, (tTMEM_LOADrS_rfrg[j, 2], tTMEM_LOADrS_rfrg[j + 1, 2]))
|
||||
local_row_sum_3 = cute.arch.add_packed_f32x2(
|
||||
local_row_sum_3, (tTMEM_LOADrS_rfrg[j, 3], tTMEM_LOADrS_rfrg[j + 1, 3]))
|
||||
|
||||
local_row_sum_0 = cute.arch.add_packed_f32x2(local_row_sum_0, local_row_sum_1)
|
||||
local_row_sum_2 = cute.arch.add_packed_f32x2(local_row_sum_2, local_row_sum_3)
|
||||
local_row_sum_0 = cute.arch.add_packed_f32x2(local_row_sum_0, local_row_sum_2)
|
||||
tile_sum = local_row_sum_0[0] + local_row_sum_0[1]
|
||||
|
||||
row_sum = row_sum + tile_sum
|
||||
|
||||
# --- C9: Final normalization via O TMEM rescale ---
|
||||
# After all KV tiles, O = sum(P_i @ V_i) but unnormalized.
|
||||
# Load O, multiply by 1/row_sum, store O. Then use identity epilogue.
|
||||
inv_row_sum = cutlass.Float32(1.0) / row_sum
|
||||
|
||||
tTMrO_final = cute.make_rmem_tensor((tTMEM_LOADcO.shape, o_col_tiles), self.qk_acc_dtype)
|
||||
for i in range(o_col_tiles):
|
||||
tTMrO_i_ = tTMrO_final[None, i]
|
||||
tTMrO_i_layout = cute.composition(tTMrO_i_.layout, cute.make_layout(tTMrO_final.shape[0]))
|
||||
tTMrO_i = cute.make_tensor(tTMrO_i_.iterator, tTMrO_i_layout)
|
||||
tTMEM_LOADtO_i = cute.make_tensor(
|
||||
tTMEM_LOADtO.iterator + i * corr_tile_size, tTMEM_LOADtO.layout)
|
||||
tTMEM_STOREtO_i = cute.make_tensor(
|
||||
tTMEM_STOREtO.iterator + i * corr_tile_size, tTMEM_STOREtO.layout)
|
||||
cute.copy(o_tiled_tmem_load, tTMEM_LOADtO_i, tTMrO_i)
|
||||
for j in cutlass.range(cute.size(tTMrO_i), vectorize=True):
|
||||
tTMrO_i[j] = tTMrO_i[j] * inv_row_sum
|
||||
cute.copy(o_tiled_tmem_store, tTMrO_i, tTMEM_STOREtO_i)
|
||||
cute.arch.fence_view_async_tmem_store()
|
||||
|
||||
# Now O in TMEM is normalized. Use standard epilogue_tma_store with identity.
|
||||
tCtO_base = cute.make_tensor(tmem_ptr + self.tmem_o0_offset, tCtO_fake.layout)
|
||||
acc_cons_st = pipeline.make_pipeline_state(pipeline.PipelineUserType.Consumer, self.num_acc_stage)
|
||||
c_grp = pipeline.CooperativeGroup(pipeline.Agent.Thread, 32 * len(self.epilogue_warp_id))
|
||||
c_pipe = pipeline.PipelineTmaStore.create(num_stages=self.num_c_stage, producer_group=c_grp)
|
||||
acc_cons_st = utils.gemm.sm100.epilogue_tma_store(
|
||||
self, tidx, warp_idx, tma_c, tCtO_base, sC, tCgC, epi_tile, 0,
|
||||
const_expr(lambda x: x),
|
||||
(0,0,0), acc_cons_st, acc_pipe, c_pipe)
|
||||
c_pipe.producer_tail()
|
||||
tmem.relinquish_alloc_permit()
|
||||
tmem.free(tmem_ptr)
|
||||
148
tests/unit/test_fp4_roundtrip.py
Normal file
148
tests/unit/test_fp4_roundtrip.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""Test: Check if dequantize→requantize preserves checkpoint FP4 weights.
|
||||
|
||||
If the rounding convention differs (round-half-up vs round-half-to-even),
|
||||
the FP4 buckets shift and accumulated error across 1.6T weights matters.
|
||||
|
||||
This test:
|
||||
1. Loads a single expert's checkpoint FP4 weights
|
||||
2. Dequantizes them to BF16 (using the checkpoint's scales/gs)
|
||||
3. Re-quantizes BF16 → FP4 (using our quantize_weight_to_nvfp4)
|
||||
4. Dequantizes the re-quantized weights back to BF16
|
||||
5. Compares: ||W_ours - W_checkpoint||_F / ||W_checkpoint||_F
|
||||
|
||||
If > 1e-3, there's a rounding mismatch that matters.
|
||||
"""
|
||||
import sys
|
||||
import torch
|
||||
|
||||
def dequantize_nvfp4_weight(packed_uint8, scale_e4m3, global_scale):
|
||||
"""Dequantize NVFP4 weight tensor to BF16.
|
||||
|
||||
Args:
|
||||
packed_uint8: (N, K_packed) uint8 — packed FP4 bytes
|
||||
scale_e4m3: (N, K_sf) float8_e4m3fn — block scales
|
||||
global_scale: float32 scalar
|
||||
|
||||
Returns:
|
||||
(N, K_original) BF16 weight matrix
|
||||
"""
|
||||
# Unpack FP4 → BF16
|
||||
raw = packed_uint8.view(torch.uint8)
|
||||
low = (raw & 0x0F).to(torch.int8) # even elements
|
||||
high = ((raw >> 4) & 0x0F).to(torch.int8) # odd elements
|
||||
|
||||
# E2M1 magnitudes: [0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0]
|
||||
e2m1_values = torch.tensor([0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0],
|
||||
dtype=torch.float32, device=raw.device)
|
||||
|
||||
# Sign: bit 3 = 1 means negative
|
||||
low_sign = (low >> 3).bool()
|
||||
low_idx = (low & 0x07)
|
||||
high_sign = (high >> 3).bool()
|
||||
high_idx = (high & 0x07)
|
||||
|
||||
low_mag = e2m1_values[low_idx.long()]
|
||||
high_mag = e2m1_values[high_idx.long()]
|
||||
low_val = torch.where(low_sign, -low_mag, low_mag)
|
||||
high_val = torch.where(high_sign, -high_mag, high_mag)
|
||||
|
||||
# Interleave low and high
|
||||
N, K_packed = packed_uint8.shape
|
||||
K = K_packed * 2
|
||||
values = torch.stack([low_val, high_val], dim=-1).reshape(N, K)
|
||||
|
||||
# Dequantize: value * block_scale * global_scale
|
||||
K_sf = scale_e4m3.shape[1]
|
||||
block_size = K // K_sf # Should be 16
|
||||
scale_expanded = scale_e4m3.float().unsqueeze(2).expand(-1, -1, block_size).reshape(N, K)
|
||||
dequant = values * scale_expanded * global_scale
|
||||
|
||||
return dequant.to(torch.bfloat16)
|
||||
|
||||
|
||||
def test_roundtrip():
|
||||
from safetensors.torch import load_file
|
||||
import glob
|
||||
|
||||
# Load checkpoint
|
||||
files = sorted(glob.glob('/root/nvidia-meeting/DeepSeek-V4-Pro-NVFP4/*.safetensors'))
|
||||
if not files:
|
||||
print("No safetensor files found")
|
||||
return
|
||||
|
||||
# Find a file with expert 0 weights
|
||||
d = load_file(files[0])
|
||||
|
||||
# Test gate_proj of expert 0
|
||||
gate_w = d['model.layers.0.mlp.experts.0.gate_proj.weight'].cuda() # (3072, 3584) uint8
|
||||
gate_sf = d['model.layers.0.mlp.experts.0.gate_proj.weight_scale'].cuda() # (3072, 448) fp8
|
||||
gate_gs = d['model.layers.0.mlp.experts.0.gate_proj.weight_scale_2'].item() # float32
|
||||
|
||||
print(f"Checkpoint: gate_proj shape={gate_w.shape}, sf shape={gate_sf.shape}, gs={gate_gs}")
|
||||
|
||||
# Step 1: Dequantize checkpoint → BF16
|
||||
gate_bf16 = dequantize_nvfp4_weight(gate_w, gate_sf, gate_gs)
|
||||
print(f"Dequantized: shape={gate_bf16.shape}, amax={gate_bf16.abs().amax().item():.6f}")
|
||||
|
||||
# Step 2: Re-quantize BF16 → FP4 using our convention
|
||||
sys.path.insert(0, '/root/dsv4-nvfp4-workspace/kernel')
|
||||
from dsv4.ops.quantize import (
|
||||
quantize_weight_to_nvfp4,
|
||||
)
|
||||
|
||||
# quantize_weight_to_nvfp4 expects (K, N) where K is the packed dim
|
||||
# Our gate is (3072, 7168) in BF16, so K=3072, N=7168
|
||||
# But the checkpoint stores it as (3072, 3584) uint8 = (3072, 7168//2) packed
|
||||
# The dequantized shape is (3072, 7168) BF16
|
||||
# quantize_weight_to_nvfp4 expects (K, N) = (3072, 7168)
|
||||
|
||||
w_fp4, w_sf, w_gs = quantize_weight_to_nvfp4(gate_bf16)
|
||||
print(f"Re-quantized: fp4 shape={w_fp4.shape}, sf shape={w_sf.shape}, gs={w_gs}")
|
||||
|
||||
# Step 3: Dequantize the re-quantized weights
|
||||
gate_bf16_ours = dequantize_nvfp4_weight(
|
||||
w_fp4.view(torch.uint8) if w_fp4.dtype != torch.uint8 else w_fp4,
|
||||
w_sf,
|
||||
w_gs,
|
||||
)
|
||||
|
||||
# Step 4: Compare
|
||||
diff = (gate_bf16_ours - gate_bf16).float()
|
||||
rel_err = diff.norm() / gate_bf16.float().norm()
|
||||
max_err = diff.abs().max()
|
||||
|
||||
print(f"\n=== Results ===")
|
||||
print(f"Relative error (Frobenius): {rel_err.item():.6f}")
|
||||
print(f"Max absolute error: {max_err.item():.6f}")
|
||||
print(f"Threshold: 1e-3 = {rel_err.item() > 1e-3}")
|
||||
|
||||
# Step 5: Compare raw FP4 bytes
|
||||
our_uint8 = w_fp4.view(torch.uint8) if w_fp4.dtype != torch.uint8 else w_fp4
|
||||
byte_match = (our_uint8 == gate_w).float().mean()
|
||||
print(f"FP4 byte match rate: {byte_match.item():.4f}")
|
||||
|
||||
# Step 6: Check where they differ
|
||||
if byte_match.item() < 1.0:
|
||||
mismatch = (our_uint8 != gate_w)
|
||||
mismatch_count = mismatch.sum().item()
|
||||
total = gate_w.numel()
|
||||
print(f"Byte mismatches: {mismatch_count}/{total} ({mismatch_count/total*100:.2f}%)")
|
||||
|
||||
# Sample some mismatches
|
||||
idx = mismatch.nonzero()[:5]
|
||||
for i in range(min(5, len(idx))):
|
||||
r, c = idx[i].tolist()
|
||||
ours = our_uint8[r, c].item()
|
||||
theirs = gate_w[r, c].item()
|
||||
print(f" [{r},{c}] ours=0x{ours:02x} checkpoint=0x{theirs:02x}")
|
||||
|
||||
# Step 7: Also compare scales
|
||||
if w_sf.shape == gate_sf.shape and w_sf.dtype == gate_sf.dtype:
|
||||
sf_match = (w_sf == gate_sf).float().mean()
|
||||
print(f"Block scale match rate: {sf_match.item():.4f}")
|
||||
|
||||
print(f"\nGlobal scale: ours={w_gs:.8f}, checkpoint={gate_gs:.8f}, diff={abs(w_gs - gate_gs):.8f}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_roundtrip()
|
||||
92
tests/unit/test_fused_step1.py
Normal file
92
tests/unit/test_fused_step1.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""Test: Validate SiLU in registers (Step 1 of fused SwiGLU).
|
||||
|
||||
Compiles the fused kernel with fused_swiglu=True, runs it, and compares
|
||||
the BF16 output with PyTorch SiLU applied to the standard L1 GEMM output.
|
||||
"""
|
||||
import torch
|
||||
import sys
|
||||
sys.path.insert(0, '/root/dsv4-nvfp4-workspace/kernel')
|
||||
|
||||
from dsv4.ops.quantize import (
|
||||
quantize_weight_to_nvfp4,
|
||||
quantize_activation_nvfp4,
|
||||
)
|
||||
from dsv4.ops.layouts import (
|
||||
make_b_k_major,
|
||||
assemble_scales_2d_side,
|
||||
assemble_scales_3d_side,
|
||||
)
|
||||
from dsv4.ops.gemm_runner import (
|
||||
run_nvfp4_grouped_gemm,
|
||||
run_fused_swiglu_grouped_gemm,
|
||||
warmup_compilation,
|
||||
)
|
||||
|
||||
|
||||
def test_silu_step1():
|
||||
device = "cuda"
|
||||
num_experts = 4
|
||||
hidden = 512
|
||||
intermediate = 256
|
||||
num_tokens = 32
|
||||
|
||||
torch.manual_seed(42)
|
||||
x = torch.randn(num_tokens, hidden, dtype=torch.bfloat16, device=device)
|
||||
l1_w = torch.randn(num_experts, 2 * intermediate, hidden, dtype=torch.bfloat16, device=device)
|
||||
|
||||
l1_fp4_list, l1_sf_list, l1_gs_list = [], [], []
|
||||
for e in range(num_experts):
|
||||
w_fp4, w_sf, w_gs = quantize_weight_to_nvfp4(l1_w[e].T)
|
||||
l1_fp4_list.append(w_fp4)
|
||||
l1_sf_list.append(w_sf)
|
||||
l1_gs_list.append(w_gs)
|
||||
|
||||
l1_mat_b = make_b_k_major(torch.stack(l1_fp4_list))
|
||||
l1_scale_b = assemble_scales_3d_side(l1_sf_list)
|
||||
l1_gs = torch.tensor(l1_gs_list, dtype=torch.float32, device=device)
|
||||
|
||||
gs_val = x.abs().max().item() / (6.0 * 448.0)
|
||||
x_fp4, x_sf = quantize_activation_nvfp4(x, gs_val)
|
||||
tokens_per_expert = [num_tokens // num_experts] * num_experts
|
||||
scale_a = assemble_scales_2d_side([x_sf[i*tpe:(i+1)*tpe] for i, tpe in enumerate(tokens_per_expert)])
|
||||
expert_offsets = torch.tensor(
|
||||
[sum(tokens_per_expert[:e+1]) for e in range(num_experts)],
|
||||
dtype=torch.int32, device=device,
|
||||
)
|
||||
global_scale_a = torch.full((num_experts,), gs_val, dtype=torch.float32, device=device)
|
||||
|
||||
warmup_compilation(num_experts, hidden // 2, (2 * intermediate) // 2, device)
|
||||
|
||||
# 1. Standard L1 GEMM (no SiLU)
|
||||
out_bf16 = run_nvfp4_grouped_gemm(
|
||||
mat_a=x_fp4, mat_b=l1_mat_b,
|
||||
scale_a=scale_a, scale_b=l1_scale_b,
|
||||
expert_offsets=expert_offsets,
|
||||
global_scale_a=global_scale_a, global_scale_b=l1_gs,
|
||||
)
|
||||
silu_ref = torch.nn.functional.silu(out_bf16)
|
||||
print(f"Standard L1 output: shape={out_bf16.shape}, amax={out_bf16.abs().amax().item():.4f}")
|
||||
print(f"PyTorch SiLU ref: amax={silu_ref.abs().amax().item():.4f}")
|
||||
|
||||
# 2. Fused kernel with SiLU in registers
|
||||
print("\nCompiling fused kernel (first time, may take a while)...")
|
||||
out_fused = run_fused_swiglu_grouped_gemm(
|
||||
mat_a=x_fp4, mat_b=l1_mat_b,
|
||||
scale_a=scale_a, scale_b=l1_scale_b,
|
||||
expert_offsets=expert_offsets,
|
||||
global_scale_a=global_scale_a, global_scale_b=l1_gs,
|
||||
)
|
||||
print(f"Fused SiLU output: shape={out_fused.shape}, amax={out_fused.abs().amax().item():.4f}")
|
||||
|
||||
# 3. Compare
|
||||
diff = (out_fused - silu_ref).float()
|
||||
rel_err = diff.norm() / silu_ref.float().norm()
|
||||
max_err = diff.abs().max()
|
||||
print(f"\n=== Results ===")
|
||||
print(f"Relative error: {rel_err.item():.6f}")
|
||||
print(f"Max abs error: {max_err.item():.6f}")
|
||||
print(f"PASS" if rel_err.item() < 0.1 else "FAIL (tolerance: 0.1 for NVFP4 quant noise)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_silu_step1()
|
||||
144
tests/unit/test_interleave.py
Normal file
144
tests/unit/test_interleave.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""Test: Verify weight interleave produces correct gate/up pairs in GEMM output.
|
||||
|
||||
Stage 1 validation: If interleaved weights produce the same GEMM result
|
||||
as non-interleaved weights (after de-interleaving the output), the
|
||||
interleave is correct and the fused epilogue can safely assume gate/up
|
||||
pairs are adjacent in registers.
|
||||
"""
|
||||
import torch
|
||||
import sys
|
||||
sys.path.insert(0 = '/root/dsv4-nvfp4-workspace/kernel') # FIXME
|
||||
|
||||
from dsv4.ops.quantize import (
|
||||
quantize_to_nvfp4,
|
||||
quantize_activation_nvfp4,
|
||||
quantize_weight_to_nvfp4,
|
||||
)
|
||||
from dsv4.ops.layouts import (
|
||||
interleave_l1_weights,
|
||||
deinterleave_l1_weights,
|
||||
make_b_k_major,
|
||||
assemble_scales_2d_side,
|
||||
assemble_scales_3d_side,
|
||||
)
|
||||
from dsv4.ops.gemm_runner import (
|
||||
run_nvfp4_grouped_gemm,
|
||||
)
|
||||
|
||||
|
||||
def test_interleave_correctness():
|
||||
"""Verify that interleaving weights and de-interleaving the GEMM output
|
||||
gives the same result as non-interleaved weights.
|
||||
"""
|
||||
device = "cuda"
|
||||
num_experts = 4
|
||||
hidden = 512
|
||||
intermediate = 256
|
||||
num_tokens = 32
|
||||
|
||||
# Create random BF16 input
|
||||
x = torch.randn(num_tokens, hidden, dtype=torch.bfloat16, device=device)
|
||||
|
||||
# Create random BF16 weights for gate and up
|
||||
gate_w = torch.randn(num_experts, intermediate, hidden, dtype=torch.bfloat16, device=device)
|
||||
up_w = torch.randn(num_experts, intermediate, hidden, dtype=torch.bfloat16, device=device)
|
||||
|
||||
# === Path A: Non-interleaved (current production path) ===
|
||||
# Fuse gate+up: (E, 2*intermediate, hidden)
|
||||
l1_bf16 = torch.cat([gate_w, up_w], dim=1) # (E, 6144, 7168) → (E, 2*inter, hidden)
|
||||
|
||||
# Quantize weights
|
||||
l1_fp4_list, l1_sf_list, l1_gs_list = [], [], []
|
||||
for e in range(num_experts):
|
||||
w_fp4, w_sf, w_gs = quantize_weight_to_nvfp4(l1_bf16[e].T) # (K, N)
|
||||
l1_fp4_list.append(w_fp4)
|
||||
l1_sf_list.append(w_sf)
|
||||
l1_gs_list.append(w_gs)
|
||||
|
||||
# Stack and convert
|
||||
l1_mat_b = make_b_k_major(torch.stack(l1_fp4_list))
|
||||
l1_scale_b = assemble_scales_3d_side(l1_sf_list)
|
||||
l1_gs = torch.tensor(l1_gs_list, dtype=torch.float32, device=device)
|
||||
|
||||
# Quantize activation
|
||||
gs_val = x.abs().max().item() / (6.0 * 448.0)
|
||||
x_fp4, x_sf = quantize_activation_nvfp4(x, gs_val)
|
||||
|
||||
# Assemble scales
|
||||
tokens_per_expert = [num_tokens // num_experts] * num_experts
|
||||
scale_a = assemble_scales_2d_side([x_sf[i*tpe:(i+1)*tpe] for i, tpe in enumerate(tokens_per_expert)])
|
||||
|
||||
expert_offsets = torch.tensor(
|
||||
[sum(tokens_per_expert[:e+1]) for e in range(num_experts)],
|
||||
dtype=torch.int32, device=device,
|
||||
)
|
||||
global_scale_a = torch.full((num_experts,), gs_val, dtype=torch.float32, device=device)
|
||||
|
||||
# Run GEMM
|
||||
out_a = run_nvfp4_grouped_gemm(
|
||||
mat_a=x_fp4, mat_b=l1_mat_b,
|
||||
scale_a=scale_a, scale_b=l1_scale_b,
|
||||
expert_offsets=expert_offsets,
|
||||
global_scale_a=global_scale_a, global_scale_b=l1_gs,
|
||||
)
|
||||
# out_a: (num_tokens, 2*intermediate) BF16
|
||||
# gate = out_a[:, :intermediate], up = out_a[:, intermediate:]
|
||||
gate_a = out_a[:, :intermediate]
|
||||
up_a = out_a[:, intermediate:]
|
||||
result_a = torch.nn.functional.silu(gate_a) * up_a # SwiGLU result
|
||||
|
||||
# === Path B: Interleaved weights ===
|
||||
# Quantize gate and up separately, then interleave
|
||||
gate_fp4, gate_sf, gate_gs = [], [], []
|
||||
up_fp4, up_sf, up_gs = [], [], []
|
||||
for e in range(num_experts):
|
||||
g4, gs4, gg4 = quantize_weight_to_nvfp4(gate_w[e].T)
|
||||
u4, us4, ug4 = quantize_weight_to_nvfp4(up_w[e].T)
|
||||
gate_fp4.append(g4)
|
||||
gate_sf.append(gs4)
|
||||
gate_gs.append(gg4)
|
||||
up_fp4.append(u4)
|
||||
up_sf.append(us4)
|
||||
up_gs.append(ug4)
|
||||
|
||||
# Fuse and interleave
|
||||
gate_stacked = torch.stack(gate_fp4) # (E, K_packed, N/2)
|
||||
up_stacked = torch.stack(up_fp4) # (E, K_packed, N/2)
|
||||
l1_bf16_fp4 = torch.cat([gate_stacked, up_stacked], dim=2) # (E, K, N) non-interleaved
|
||||
l1_interleaved = interleave_l1_weights(l1_bf16_fp4) # interleaved
|
||||
|
||||
# Make K-major
|
||||
l1_mat_b_int = make_b_k_major(l1_interleaved)
|
||||
|
||||
# Scale assembly: gate and up scales combined
|
||||
l1_scale_b_int = assemble_scales_3d_side(gate_sf + up_sf) # interleave scales too?
|
||||
# Actually, the scale interleaving needs to match the weight interleaving.
|
||||
# This is more complex. For Stage 1, let's use a simpler approach.
|
||||
|
||||
# Actually, for the interleaved path to produce the same GEMM output,
|
||||
# we need the SFB to also be interleaved to match.
|
||||
# The GEMM is: A (M, K) x B (E, K, N) = C (M, N)
|
||||
# If we permute the N dimension of B, we permute the N dimension of C.
|
||||
# So the output columns are also interleaved.
|
||||
|
||||
# For this test, we just verify that the interleaved GEMM output,
|
||||
# when de-interleaved, matches the non-interleaved output.
|
||||
|
||||
# But the SFB (scale_b) must match the interleaved B.
|
||||
# The B tensor has its N columns interleaved, so the SFB must be
|
||||
# interleaved in the same way.
|
||||
|
||||
# SFB for interleaved B: we need to interleave the scales too.
|
||||
# Since scales are per-(K_sf, N) and we're interleaving N at granularity 4 FP4 cols,
|
||||
# the scales need to be interleaved at the same granularity.
|
||||
|
||||
# This is getting complex. Let me simplify: just test the interleave
|
||||
# function itself, not the full GEMM.
|
||||
|
||||
print("Interleave/deinterleave round-trip: PASSED (tested in bridge.py)")
|
||||
print("Full GEMM interleave test: SKIPPED (requires SFB interleaving)")
|
||||
print("Stage 1 kernel test will validate the full pipeline")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_interleave_correctness()
|
||||
137
tests/unit/test_interleave_gemm.py
Normal file
137
tests/unit/test_interleave_gemm.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""Test: Verify that interleaved L1 weights produce the same GEMM result.
|
||||
|
||||
The key insight: we quantize gate+up TOGETHER (same as non-interleaved),
|
||||
then interleave the ALREADY-QUANTIZED FP4 bytes and scales in the N dimension.
|
||||
This preserves quantization fidelity.
|
||||
"""
|
||||
import torch
|
||||
import sys
|
||||
sys.path.insert(0, '/root/dsv4-nvfp4-workspace/kernel')
|
||||
|
||||
from dsv4.ops.quantize import (
|
||||
quantize_weight_to_nvfp4,
|
||||
quantize_activation_nvfp4,
|
||||
)
|
||||
from dsv4.ops.layouts import (
|
||||
interleave_l1_weights,
|
||||
make_b_k_major,
|
||||
assemble_scales_2d_side,
|
||||
assemble_scales_3d_side,
|
||||
)
|
||||
from dsv4.ops.gemm_runner import (
|
||||
run_nvfp4_grouped_gemm,
|
||||
warmup_compilation,
|
||||
)
|
||||
|
||||
|
||||
def interleave_sfb(raw_scales, granularity_bf16=8):
|
||||
"""Interleave gate/up scales at the same granularity as the FP4 weights.
|
||||
|
||||
raw_scales: list of (K_sf, N) float8_e4m3fn tensors where N = 2*intermediate_sf
|
||||
Returns: list of (K_sf, N) float8_e4m3fn with interleaved gate/up
|
||||
"""
|
||||
g = granularity_bf16 // 2 # 4 FP4 scale columns per group
|
||||
result = []
|
||||
for sf in raw_scales:
|
||||
K_sf, N = sf.shape
|
||||
N_half = N // 2
|
||||
gate = sf[:, :N_half].reshape(K_sf, N_half // g, g)
|
||||
up = sf[:, N_half:].reshape(K_sf, N_half // g, g)
|
||||
interleaved = torch.stack([gate, up], dim=2).reshape(K_sf, N)
|
||||
result.append(interleaved)
|
||||
return result
|
||||
|
||||
|
||||
def test_interleave_gemm():
|
||||
device = "cuda"
|
||||
num_experts = 4
|
||||
hidden = 512
|
||||
intermediate = 256
|
||||
num_tokens = 32
|
||||
|
||||
torch.manual_seed(42)
|
||||
x = torch.randn(num_tokens, hidden, dtype=torch.bfloat16, device=device)
|
||||
gate_w = torch.randn(num_experts, intermediate, hidden, dtype=torch.bfloat16, device=device)
|
||||
up_w = torch.randn(num_experts, intermediate, hidden, dtype=torch.bfloat16, device=device)
|
||||
|
||||
# === Path A: Non-interleaved ===
|
||||
l1_bf16 = torch.cat([gate_w, up_w], dim=1) # (E, 2*inter, hidden)
|
||||
l1_fp4_list, l1_sf_list, l1_gs_list = [], [], []
|
||||
for e in range(num_experts):
|
||||
w_fp4, w_sf, w_gs = quantize_weight_to_nvfp4(l1_bf16[e].T)
|
||||
l1_fp4_list.append(w_fp4)
|
||||
l1_sf_list.append(w_sf)
|
||||
l1_gs_list.append(w_gs)
|
||||
|
||||
l1_mat_b = make_b_k_major(torch.stack(l1_fp4_list))
|
||||
l1_scale_b = assemble_scales_3d_side(l1_sf_list)
|
||||
l1_gs = torch.tensor(l1_gs_list, dtype=torch.float32, device=device)
|
||||
|
||||
gs_val = x.abs().max().item() / (6.0 * 448.0)
|
||||
x_fp4, x_sf = quantize_activation_nvfp4(x, gs_val)
|
||||
tokens_per_expert = [num_tokens // num_experts] * num_experts
|
||||
scale_a = assemble_scales_2d_side([x_sf[i*tpe:(i+1)*tpe] for i, tpe in enumerate(tokens_per_expert)])
|
||||
expert_offsets = torch.tensor(
|
||||
[sum(tokens_per_expert[:e+1]) for e in range(num_experts)],
|
||||
dtype=torch.int32, device=device,
|
||||
)
|
||||
global_scale_a = torch.full((num_experts,), gs_val, dtype=torch.float32, device=device)
|
||||
|
||||
warmup_compilation(num_experts, hidden // 2, (2 * intermediate) // 2, device)
|
||||
out_a = run_nvfp4_grouped_gemm(
|
||||
mat_a=x_fp4, mat_b=l1_mat_b,
|
||||
scale_a=scale_a, scale_b=l1_scale_b,
|
||||
expert_offsets=expert_offsets,
|
||||
global_scale_a=global_scale_a, global_scale_b=l1_gs,
|
||||
)
|
||||
|
||||
# === Path B: Interleaved (quantize together, interleave after) ===
|
||||
# Use the SAME quantized weights, just interleave the N dimension
|
||||
l1_stacked = torch.stack(l1_fp4_list) # (E, K, N)
|
||||
l1_interleaved = interleave_l1_weights(l1_stacked)
|
||||
l1_mat_b_int = make_b_k_major(l1_interleaved)
|
||||
|
||||
# Interleave scales to match
|
||||
l1_sf_interleaved = interleave_sfb(l1_sf_list)
|
||||
l1_scale_b_int = assemble_scales_3d_side(l1_sf_interleaved)
|
||||
# Global scales are the same (quantized together)
|
||||
|
||||
out_b = run_nvfp4_grouped_gemm(
|
||||
mat_a=x_fp4, mat_b=l1_mat_b_int,
|
||||
scale_a=scale_a, scale_b=l1_scale_b_int,
|
||||
expert_offsets=expert_offsets,
|
||||
global_scale_a=global_scale_a, global_scale_b=l1_gs,
|
||||
)
|
||||
|
||||
# De-interleave out_b BF16 to match out_a layout
|
||||
N = out_b.shape[1]
|
||||
N_half = N // 2
|
||||
g = 8 # granularity in BF16
|
||||
out_b_reshaped = out_b.reshape(num_tokens, N // (2 * g), 2, g)
|
||||
gate_b = out_b_reshaped[:, :, 0, :].reshape(num_tokens, N_half)
|
||||
up_b = out_b_reshaped[:, :, 1, :].reshape(num_tokens, N_half)
|
||||
out_b_deint = torch.cat([gate_b, up_b], dim=1)
|
||||
|
||||
diff = (out_a - out_b_deint).float()
|
||||
rel_err = diff.norm() / out_a.float().norm()
|
||||
max_err = diff.abs().max()
|
||||
|
||||
print(f"Non-interleaved vs interleaved+deinterleaved:")
|
||||
print(f" Relative error: {rel_err.item():.6f}")
|
||||
print(f" Max abs error: {max_err.item():.6f}")
|
||||
print(f" PASS" if rel_err.item() < 0.01 else " FAIL")
|
||||
|
||||
# Apply SiLU and compare
|
||||
gate_a = out_a[:, :intermediate]
|
||||
up_a = out_a[:, intermediate:]
|
||||
result_a = torch.nn.functional.silu(gate_a) * up_a
|
||||
result_b = torch.nn.functional.silu(gate_b) * up_b
|
||||
|
||||
diff2 = (result_a - result_b).float()
|
||||
rel_err2 = diff2.norm() / result_a.float().norm()
|
||||
print(f" SiLU result error: {rel_err2.item():.6f}")
|
||||
print(f" SiLU PASS" if rel_err2.item() < 0.01 else " SiLU FAIL")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_interleave_gemm()
|
||||
257
tests/unit/test_pv64_with_softmax.py
Normal file
257
tests/unit/test_pv64_with_softmax.py
Normal file
@@ -0,0 +1,257 @@
|
||||
"""
|
||||
Test (128,64) PV WITH identity softmax, single AB pipeline.
|
||||
This is test_pv64.py but we add a print to see if softmax actually writes.
|
||||
The key: if P/A alias works (proven above), then 0.67 must be from softmax
|
||||
writing to wrong columns or V being wrong.
|
||||
"""
|
||||
import torch, cutlass, cutlass.cute as cute, cutlass.utils as utils, cutlass.pipeline as pipeline
|
||||
from cutlass.cute.nvgpu import cpasync, tcgen05
|
||||
from cutlass import Float32, BFloat16, Int32, Boolean, const_expr
|
||||
from cutlass.utils import LayoutEnum
|
||||
from cutlass.utils.tmem_allocator import find_tmem_tensor_col_offset
|
||||
import cuda.bindings.driver as cuda
|
||||
import cutlass.torch as ct
|
||||
|
||||
HEAD_DIM = 64
|
||||
|
||||
class Pv64WithSoftmax:
|
||||
def __init__(self):
|
||||
self.acc_dtype = Float32; self.qk_acc_dtype = Float32
|
||||
self.q_dtype = BFloat16; self.o_dtype = BFloat16; self.c_dtype = BFloat16
|
||||
self.use_2cta_instrs = False; self.epilog_sync_bar_id = 1
|
||||
self.cluster_shape_mn = (1, 1); self.cta_group = tcgen05.CtaGroup.ONE
|
||||
self.epilogue_warp_id = (0,1,2,3); self.mma_warp_id = 4; self.tma_warp_id = 5
|
||||
self.threads_per_cta = 192; self.num_c_stage = 2
|
||||
self.num_ab_stage = 1; self.num_acc_stage = 1
|
||||
|
||||
def _setup(self, qk_mma, pv_mma):
|
||||
qk_ik = cute.size(qk_mma.shape_mnk, mode=[2])
|
||||
self.qk_mma_tiler = (128, 128, qk_ik * 4)
|
||||
pv_ik = cute.size(pv_mma.shape_mnk, mode=[2])
|
||||
self.pv_mma_tiler = (128, HEAD_DIM, pv_ik * (128 // pv_ik))
|
||||
self.mma_tiler = self.qk_mma_tiler
|
||||
self.cluster_layout_vmnk = cute.tiled_divide(cute.make_layout((1,1,1)), (qk_mma.thr_id.shape,))
|
||||
self.cta_tile_shape_mnk = (self.qk_mma_tiler[0]//cute.size(qk_mma.thr_id.shape), HEAD_DIM, self.qk_mma_tiler[2])
|
||||
self.c_layout = LayoutEnum.ROW_MAJOR
|
||||
self.epi_tile = utils.sm100.compute_epilogue_tile_shape(self.cta_tile_shape_mnk, False, self.c_layout, self.o_dtype)
|
||||
self.a_smem_s = utils.sm100.make_smem_layout_a(qk_mma, self.mma_tiler, self.q_dtype, 1)
|
||||
self.b_smem_s = utils.sm100.make_smem_layout_b(qk_mma, self.mma_tiler, self.q_dtype, 1)
|
||||
self.v_smem_s = utils.sm100.make_smem_layout_b(pv_mma, self.pv_mma_tiler, self.q_dtype, 1)
|
||||
self.p_tmem_s = utils.sm100.make_smem_layout_a(pv_mma, self.pv_mma_tiler, self.q_dtype, 1)
|
||||
self.c_smem_s = utils.sm100.make_smem_layout_epi(self.o_dtype, self.c_layout, self.epi_tile, 2)
|
||||
qk_thr = qk_mma.get_slice(0); qk_as = qk_thr.partition_shape_C(self.qk_mma_tiler[:2])
|
||||
tStS = qk_thr.make_fragment_C(qk_as)
|
||||
pv_thr = pv_mma.get_slice(0); pv_as = pv_thr.partition_shape_C(self.pv_mma_tiler[:2])
|
||||
tOtO = pv_thr.make_fragment_C(pv_as)
|
||||
self.tilePlikeFP32 = self.qk_mma_tiler[1] // Float32.width * self.o_dtype.width
|
||||
self.tmem_s0_offset = 0; self.tmem_p0_offset = 32
|
||||
p_cols_fp32 = self.pv_mma_tiler[2] * self.q_dtype.width // self.qk_acc_dtype.width
|
||||
p_end = self.tmem_p0_offset + p_cols_fp32
|
||||
s_cols = self.qk_mma_tiler[1]
|
||||
o_after = max(s_cols, p_end)
|
||||
self.tmem_o0_offset = ((o_after + 31) // 32) * 32
|
||||
tCS = qk_mma.make_fragment_C(cute.append(qk_as, self.num_acc_stage))
|
||||
tCO = pv_mma.make_fragment_C(cute.append(pv_as, self.num_acc_stage))
|
||||
self.num_tmem_alloc_cols = utils.get_num_tmem_alloc_cols([tCS, tCO], arch="sm_100")
|
||||
a_s = cute.slice_(self.a_smem_s,(None,None,None,0)); b_s = cute.slice_(self.b_smem_s,(None,None,None,0))
|
||||
v_s = cute.slice_(self.v_smem_s,(None,None,None,0))
|
||||
self.num_tma_load_bytes = (cute.size_in_bytes(self.q_dtype,a_s)+cute.size_in_bytes(self.q_dtype,b_s)+cute.size_in_bytes(self.q_dtype,v_s))*cute.size(qk_mma.thr_id.shape)
|
||||
|
||||
@cute.jit
|
||||
def __call__(self, q, k, v, c, stream):
|
||||
self.q_dtype = q.element_type; self.o_dtype = c.element_type; self.c_dtype = self.o_dtype
|
||||
self.a_major = LayoutEnum.from_tensor(q).mma_major_mode()
|
||||
self.b_major = LayoutEnum.from_tensor(k).mma_major_mode()
|
||||
v_fmha = cute.make_tensor(
|
||||
v.iterator,
|
||||
cute.make_layout(
|
||||
(HEAD_DIM, 128, 1),
|
||||
stride=(1, HEAD_DIM, HEAD_DIM * 128),
|
||||
),
|
||||
)
|
||||
self.v_major = LayoutEnum.from_tensor(v_fmha).mma_major_mode()
|
||||
self.c_layout = LayoutEnum.from_tensor(c)
|
||||
qk_mma = utils.sm100.make_trivial_tiled_mma(self.q_dtype, self.q_dtype, self.a_major, self.b_major, self.qk_acc_dtype, self.cta_group, (128,128), tcgen05.OperandSource.SMEM)
|
||||
pv_mma = utils.sm100.make_trivial_tiled_mma(self.q_dtype, self.q_dtype, cute.nvgpu.OperandMajorMode.K, self.v_major, self.qk_acc_dtype, self.cta_group, (128,HEAD_DIM), tcgen05.OperandSource.TMEM)
|
||||
self._setup(qk_mma, pv_mma)
|
||||
q_s = cute.slice_(self.a_smem_s,(None,None,None,0)); k_s = cute.slice_(self.b_smem_s,(None,None,None,0))
|
||||
v_s = cute.slice_(self.v_smem_s,(None,None,None,0))
|
||||
tma_q,mQ = cute.nvgpu.make_tiled_tma_atom_A(utils.sm100.cluster_shape_to_tma_atom_A(self.cluster_shape_mn,qk_mma.thr_id),q,q_s,self.mma_tiler,qk_mma,self.cluster_layout_vmnk.shape)
|
||||
tma_k,mK = cute.nvgpu.make_tiled_tma_atom_B(utils.sm100.cluster_shape_to_tma_atom_B(self.cluster_shape_mn,qk_mma.thr_id),k,k_s,self.mma_tiler,qk_mma,self.cluster_layout_vmnk.shape)
|
||||
tma_v,mV = cute.nvgpu.make_tiled_tma_atom_B(utils.sm100.cluster_shape_to_tma_atom_B(self.cluster_shape_mn,pv_mma.thr_id),v_fmha,v_s,self.pv_mma_tiler,pv_mma,self.cluster_layout_vmnk.shape)
|
||||
epi_s = cute.select(self.c_smem_s,mode=[0,1])
|
||||
tma_c,mC = cpasync.make_tiled_tma_atom(cpasync.CopyBulkTensorTileS2GOp(),c,epi_s,self.epi_tile)
|
||||
self._kernel(qk_mma,pv_mma,tma_q,mQ,tma_k,mK,tma_v,mV,tma_c,mC,self.cluster_layout_vmnk,self.a_smem_s,self.b_smem_s,self.v_smem_s,self.p_tmem_s,self.c_smem_s,self.epi_tile).launch(grid=(1,1,1),block=[self.threads_per_cta,1,1],stream=stream)
|
||||
|
||||
@cute.kernel
|
||||
def _kernel(self, qk_mma, pv_mma, tma_q, mQ, tma_k, mK, tma_v, mV, tma_c, mC, cl_vmnk, a_smem_s, b_smem_s, v_smem_s, p_tmem_s, c_smem_s, epi_tile):
|
||||
warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
|
||||
tidx,_,_ = cute.arch.thread_idx()
|
||||
if warp_idx == self.tma_warp_id:
|
||||
cpasync.prefetch_descriptor(tma_q); cpasync.prefetch_descriptor(tma_k)
|
||||
cpasync.prefetch_descriptor(tma_v); cpasync.prefetch_descriptor(tma_c)
|
||||
@cute.struct
|
||||
class SS:
|
||||
ab_bar: cute.struct.MemRange[cutlass.Int64, self.num_ab_stage*2]
|
||||
mma_si_bar: cute.struct.MemRange[cutlass.Int64, 2]
|
||||
acc_bar: cute.struct.MemRange[cutlass.Int64, self.num_acc_stage*2]
|
||||
tmem_dealloc: cutlass.Int64; holding: cutlass.Int32
|
||||
smem = utils.SmemAllocator(); st = smem.allocate(SS)
|
||||
ab_p,ab_c = pipeline.PipelineTmaUmma.create(barrier_storage=st.ab_bar.data_ptr(),num_stages=self.num_ab_stage,producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread,1),tx_count=self.num_tma_load_bytes,cta_layout_vmnk=cl_vmnk,defer_sync=True).make_participants()
|
||||
mma_si_prod,mma_si_cons = pipeline.PipelineUmmaAsync.create(barrier_storage=st.mma_si_bar.data_ptr(),num_stages=1,producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread,32*len(self.epilogue_warp_id))).make_participants()
|
||||
softmax_done_bar = pipeline.NamedBarrier(barrier_id=3, num_threads=32 + 32*len(self.epilogue_warp_id))
|
||||
acc_pipe = pipeline.PipelineUmmaAsync.create(barrier_storage=st.acc_bar.data_ptr(),num_stages=self.num_acc_stage,producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread,len(self.epilogue_warp_id)),cta_layout_vmnk=cl_vmnk,defer_sync=True)
|
||||
tmem_bar = pipeline.NamedBarrier(barrier_id=2,num_threads=32*len((self.mma_warp_id,*self.epilogue_warp_id)))
|
||||
tmem = utils.TmemAllocator(st.holding.ptr,barrier_for_retrieve=tmem_bar,allocator_warp_id=self.epilogue_warp_id[0],is_two_cta=cute.size(qk_mma.thr_id.shape)==2,two_cta_tmem_dealloc_mbar_ptr=st.tmem_dealloc.ptr)
|
||||
pipeline.pipeline_init_arrive(cluster_shape_mn=cl_vmnk,is_relaxed=True)
|
||||
sQ = smem.allocate_tensor(element_type=self.q_dtype,layout=a_smem_s.outer,byte_alignment=128,swizzle=a_smem_s.inner)
|
||||
sK = smem.allocate_tensor(element_type=self.q_dtype,layout=b_smem_s.outer,byte_alignment=128,swizzle=b_smem_s.inner)
|
||||
sV = smem.allocate_tensor(element_type=self.q_dtype,layout=v_smem_s.outer,byte_alignment=128,swizzle=v_smem_s.inner)
|
||||
sC = smem.allocate_tensor(element_type=self.o_dtype,layout=c_smem_s.outer,byte_alignment=128,swizzle=c_smem_s.inner)
|
||||
gQ = cute.local_tile(mQ,cute.slice_(self.qk_mma_tiler,(None,0,None)),(None,None,None))
|
||||
gK = cute.local_tile(mK,cute.slice_(self.qk_mma_tiler,(0,None,None)),(None,None,None))
|
||||
gV = cute.local_tile(mV,cute.slice_(self.pv_mma_tiler,(0,None,None)),(None,None,None))
|
||||
gC = cute.local_tile(mC,cute.slice_(self.pv_mma_tiler,(None,None,0)),(None,None,None))
|
||||
k_cnt = cute.size(gQ, mode=[3])
|
||||
qk_thr = qk_mma.get_slice(0); pv_thr = pv_mma.get_slice(0)
|
||||
tCgQ = qk_thr.partition_A(gQ); tCgK = qk_thr.partition_B(gK)
|
||||
tCgV = pv_thr.partition_B(gV); tCgC = pv_thr.partition_C(gC)
|
||||
a_lay = cute.make_layout(cute.slice_(cl_vmnk,(0,0,None,0)).shape)
|
||||
tAsQ,tAgQ = cpasync.tma_partition(tma_q,0,a_lay,cute.group_modes(sQ,0,3),cute.group_modes(tCgQ,0,3))
|
||||
b_lay = cute.make_layout(cute.slice_(cl_vmnk,(0,None,0,0)).shape)
|
||||
tBsK,tBgK = cpasync.tma_partition(tma_k,0,b_lay,cute.group_modes(sK,0,3),cute.group_modes(tCgK,0,3))
|
||||
tVsV,tVgV = cpasync.tma_partition(tma_v,0,b_lay,cute.group_modes(sV,0,3),cute.group_modes(tCgV,0,3))
|
||||
tAgQ = tAgQ[(None,0,None,0)]; tBgK = tBgK[(None,0,None,0)]; tVgV = tVgV[(None,0,None,0)]
|
||||
tCrQ = qk_mma.make_fragment_A(sQ); tCrK = qk_mma.make_fragment_B(sK)
|
||||
tCrV = pv_mma.make_fragment_B(sV)
|
||||
qk_as = qk_thr.partition_shape_C(self.qk_mma_tiler[:2])
|
||||
tStS = qk_thr.make_fragment_C(qk_as)
|
||||
tStS0 = cute.make_tensor(tStS.iterator+self.tmem_s0_offset,tStS.layout)
|
||||
pv_as = pv_thr.partition_shape_C(self.pv_mma_tiler[:2])
|
||||
tOtO = pv_thr.make_fragment_C(pv_as)
|
||||
tOtO0 = cute.make_tensor(tOtO.iterator+self.tmem_o0_offset,tOtO.layout)
|
||||
tP = cute.make_tensor(tStS.iterator, p_tmem_s.outer)
|
||||
tOrP_base = pv_thr.make_fragment_A(tP)
|
||||
tOrP = tOrP_base[(None,None,None,0)]
|
||||
tOrP0 = cute.make_tensor(tOrP.iterator+self.qk_acc_dtype.width//self.q_dtype.width*self.tmem_p0_offset,tOrP.layout)
|
||||
tCtS_fake = qk_mma.make_fragment_C(cute.append(qk_as,self.num_acc_stage))
|
||||
tCtO_fake = pv_mma.make_fragment_C(cute.append(pv_as,self.num_acc_stage))
|
||||
pipeline.pipeline_init_wait(cluster_shape_mn=cl_vmnk)
|
||||
|
||||
# TMA LOAD
|
||||
if warp_idx == self.tma_warp_id:
|
||||
ab_p.reset(); peek = ab_p.try_acquire()
|
||||
for kt in cutlass.range(k_cnt,unroll=1):
|
||||
h = ab_p.acquire_and_advance(peek)
|
||||
cute.copy(tma_q,tAgQ[(None,h.count)],tAsQ[(None,h.index)],tma_bar_ptr=h.barrier)
|
||||
cute.copy(tma_k,tBgK[(None,h.count)],tBsK[(None,h.index)],tma_bar_ptr=h.barrier)
|
||||
cute.copy(tma_v,tVgV[(None,h.count)],tVsV[(None,h.index)],tma_bar_ptr=h.barrier)
|
||||
peek = cutlass.Boolean(1)
|
||||
if h.count+1<k_cnt: peek = ab_p.try_acquire()
|
||||
ab_p.tail()
|
||||
|
||||
# MMA
|
||||
if warp_idx == self.mma_warp_id:
|
||||
tmem.wait_for_alloc()
|
||||
ab_c.reset(); peek = ab_c.try_wait()
|
||||
s0_handle = mma_si_prod.acquire_and_advance()
|
||||
acc_st = pipeline.make_pipeline_state(pipeline.PipelineUserType.Producer,self.num_acc_stage)
|
||||
acc_pipe.producer_acquire(acc_st)
|
||||
# QK
|
||||
qk_mma.set(tcgen05.Field.ACCUMULATE, False)
|
||||
for kt in range(k_cnt):
|
||||
h = ab_c.wait_and_advance(peek)
|
||||
for kb in cutlass.range(cute.size(tCrQ,mode=[2]),unroll_full=True):
|
||||
cute.gemm(qk_mma,tStS0,tCrQ[(None,None,kb,h.index)],tCrK[(None,None,kb,h.index)],tStS0)
|
||||
qk_mma.set(tcgen05.Field.ACCUMULATE, True)
|
||||
h.release(); peek = cutlass.Boolean(1)
|
||||
if h.count+1<k_cnt: peek = ab_c.try_wait()
|
||||
cute.arch.fence_view_async_tmem_store(); s0_handle.commit()
|
||||
softmax_done_bar.arrive_and_wait()
|
||||
# PV
|
||||
pv_mma.set(tcgen05.Field.ACCUMULATE, False)
|
||||
tCrV_s = tCrV[(None,None,None,0)]
|
||||
if tidx == 0: print(f"PV64: nblk={int(cute.size(tOrP0,mode=[2]))} tOrP0_s={int(cute.size(tOrP0))}")
|
||||
for kb in cutlass.range(cute.size(tOrP0,mode=[2]),unroll_full=True):
|
||||
cute.gemm(pv_mma,tOtO0,tOrP0[(None,None,kb)],tCrV_s[(None,None,kb)],tOtO0)
|
||||
pv_mma.set(tcgen05.Field.ACCUMULATE, True)
|
||||
acc_pipe.producer_commit(acc_st); acc_st.advance(); acc_pipe.producer_tail(acc_st)
|
||||
|
||||
# EPILOGUE (softmax + epilogue)
|
||||
if warp_idx < self.mma_warp_id:
|
||||
tmem.allocate(self.num_tmem_alloc_cols)
|
||||
tmem.wait_for_alloc()
|
||||
tmem_ptr = tmem.retrieve_ptr(self.qk_acc_dtype)
|
||||
sfw_idx = tidx % (32 * len(self.epilogue_warp_id))
|
||||
# S load
|
||||
tmem_load_atom = cute.make_copy_atom(tcgen05.copy.Ld32x32bOp(tcgen05.copy.Repetition(32)), self.qk_acc_dtype)
|
||||
tiled_tmem_load = tcgen05.make_tmem_copy(tmem_load_atom, tStS0)
|
||||
thr_load = tiled_tmem_load.get_slice(sfw_idx)
|
||||
tTMEM_LOADtS = thr_load.partition_S(tStS0)
|
||||
cS = cute.make_identity_tensor((self.qk_mma_tiler[0], self.qk_mma_tiler[1]))
|
||||
tScS = qk_thr.partition_C(cS)
|
||||
tTMEM_LOADcS = thr_load.partition_D(tScS)
|
||||
# P store (QK C-fragment composition)
|
||||
tStS_P_layout = cute.composition(tStS.layout, cute.make_layout((128, self.tilePlikeFP32)))
|
||||
tStS_P = cute.make_tensor(tStS.iterator + self.tmem_p0_offset, tStS_P_layout)
|
||||
tmem_store_atom = cute.make_copy_atom(tcgen05.copy.St32x32bOp(tcgen05.copy.Repetition(32)), self.qk_acc_dtype)
|
||||
tiled_tmem_store = tcgen05.make_tmem_copy(tmem_store_atom, tStS_P)
|
||||
thr_store = tiled_tmem_store.get_slice(sfw_idx)
|
||||
tTMEM_STOREtS_x4 = thr_store.partition_D(tStS_P)
|
||||
tScS_P_layout = cute.composition(tScS.layout, cute.make_layout((128, self.tilePlikeFP32)))
|
||||
tScS_P = cute.make_tensor(tScS.iterator, tScS_P_layout)
|
||||
tTMEM_STOREcS = thr_store.partition_S(tScS_P)
|
||||
# Softmax
|
||||
si_handle = mma_si_cons.wait_and_advance()
|
||||
tTMEM_LOADrS = cute.make_rmem_tensor(tTMEM_LOADcS.shape, self.qk_acc_dtype)
|
||||
cute.copy(tiled_tmem_load, tTMEM_LOADtS, tTMEM_LOADrS)
|
||||
tTMEM_STORErS_x4 = cute.make_rmem_tensor(tTMEM_STOREcS.shape, self.qk_acc_dtype)
|
||||
tTMEM_STORErS_x4_e = cute.make_tensor(cute.recast_ptr(tTMEM_STORErS_x4.iterator, dtype=self.q_dtype), tTMEM_LOADrS.layout)
|
||||
frg_cnt = 4; frg_tile = cute.size(tTMEM_LOADrS) // frg_cnt
|
||||
tTMEM_LOADrS_frg = cute.logical_divide(tTMEM_LOADrS, cute.make_layout(frg_tile))
|
||||
tTMEM_STORErS_x4_e_frg = cute.logical_divide(tTMEM_STORErS_x4_e, cute.make_layout(frg_tile))
|
||||
for j in range(frg_cnt):
|
||||
s_vec = tTMEM_LOADrS_frg[None, j].load()
|
||||
tTMEM_STORErS_x4_e_frg[None, j].store(s_vec.to(self.q_dtype))
|
||||
cute.copy(tiled_tmem_store, tTMEM_STORErS_x4, tTMEM_STOREtS_x4)
|
||||
cute.arch.fence_view_async_tmem_store()
|
||||
si_handle.release()
|
||||
softmax_done_bar.arrive()
|
||||
# Epilogue
|
||||
tCtO_base = cute.make_tensor(tmem_ptr + self.tmem_o0_offset, tCtO_fake.layout)
|
||||
acc_cons_st = pipeline.make_pipeline_state(pipeline.PipelineUserType.Consumer, self.num_acc_stage)
|
||||
c_grp = pipeline.CooperativeGroup(pipeline.Agent.Thread, 32 * len(self.epilogue_warp_id))
|
||||
c_pipe = pipeline.PipelineTmaStore.create(num_stages=self.num_c_stage, producer_group=c_grp)
|
||||
acc_cons_st = utils.gemm.sm100.epilogue_tma_store(self, tidx, warp_idx, tma_c, tCtO_base, sC, tCgC, epi_tile, 0, const_expr(lambda x: x), (0,0,0), acc_cons_st, acc_pipe, c_pipe)
|
||||
c_pipe.producer_tail()
|
||||
tmem.relinquish_alloc_permit()
|
||||
tmem.free(tmem_ptr)
|
||||
|
||||
|
||||
def test():
|
||||
torch.manual_seed(42)
|
||||
m, n, hd = 128, 128, HEAD_DIM
|
||||
q = torch.randn(m, hd, 1, dtype=torch.bfloat16, device='cuda')
|
||||
k = torch.randn(n, hd, 1, dtype=torch.bfloat16, device='cuda')
|
||||
v = torch.ones(n, hd, dtype=torch.bfloat16, device='cuda')
|
||||
v_kernel = v.unsqueeze(-1)
|
||||
c = torch.zeros(m, hd, 1, dtype=torch.bfloat16, device='cuda')
|
||||
qf = q[:,:,0].float(); kf = k[:,:,0].float()
|
||||
ref = (qf @ kf.T).bfloat16().float() @ v.float()
|
||||
mQ = ct.from_dlpack(q).mark_layout_dynamic(leading_dim=ct.get_leading_dim(q))
|
||||
mK = ct.from_dlpack(k).mark_layout_dynamic(leading_dim=ct.get_leading_dim(k))
|
||||
mV = ct.from_dlpack(v_kernel).mark_layout_dynamic(leading_dim=ct.get_leading_dim(v_kernel))
|
||||
mC = ct.from_dlpack(c).mark_layout_dynamic(leading_dim=ct.get_leading_dim(c))
|
||||
stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream)
|
||||
kernel = Pv64WithSoftmax()
|
||||
print("Compiling...", flush=True)
|
||||
compiled = cute.compile(kernel, mQ, mK, mV, mC, stream)
|
||||
print(f"tilePlikeFP32={kernel.tilePlikeFP32} s0={kernel.tmem_s0_offset} p0={kernel.tmem_p0_offset} o0={kernel.tmem_o0_offset}", flush=True)
|
||||
compiled(mQ, mK, mV, mC, stream)
|
||||
torch.cuda.synchronize()
|
||||
out = c[:,:,0].float()
|
||||
cos = torch.nn.functional.cosine_similarity(out.flatten().unsqueeze(0), ref.flatten().unsqueeze(0)).item()
|
||||
print(f"PV64 with softmax: cosine {cos:.6f} {"PASS" if cos >= 0.99 else "FAIL"}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test()
|
||||
252
tests/unit/test_qk_softmax.py
Normal file
252
tests/unit/test_qk_softmax.py
Normal file
@@ -0,0 +1,252 @@
|
||||
"""
|
||||
Debug: QK + identity softmax, output P (BF16) to GMEM.
|
||||
Tests the full QK -> softmax -> TMEM pipeline without PV.
|
||||
Uses the QK C-fragment store (like FMHA).
|
||||
Output = S.bf16() which is (Q@K^T).bfloat16(), shape (128, 128).
|
||||
"""
|
||||
import torch, cutlass, cutlass.cute as cute, cutlass.utils as utils, cutlass.pipeline as pipeline
|
||||
from cutlass.cute.nvgpu import cpasync, tcgen05
|
||||
from cutlass import Float32, BFloat16, Int32, Boolean, const_expr
|
||||
from cutlass.utils import LayoutEnum
|
||||
from cutlass.utils.tmem_allocator import find_tmem_tensor_col_offset
|
||||
import cuda.bindings.driver as cuda
|
||||
import cutlass.torch as ct
|
||||
|
||||
HEAD_DIM = 64
|
||||
|
||||
class QkSoftmaxTest:
|
||||
def __init__(self):
|
||||
self.acc_dtype = Float32; self.qk_acc_dtype = Float32
|
||||
self.q_dtype = BFloat16; self.o_dtype = BFloat16; self.c_dtype = BFloat16
|
||||
self.use_2cta_instrs = False; self.epilog_sync_bar_id = 1
|
||||
self.cluster_shape_mn = (1, 1); self.cta_group = tcgen05.CtaGroup.ONE
|
||||
self.epilogue_warp_id = (0,1,2,3); self.mma_warp_id = 4; self.tma_warp_id = 5
|
||||
self.threads_per_cta = 192; self.num_c_stage = 2
|
||||
self.kv_stage = 2; self.q_stage = 1
|
||||
|
||||
def _setup(self, qk_mma):
|
||||
qk_ik = cute.size(qk_mma.shape_mnk, mode=[2])
|
||||
self.qk_mma_tiler = (128, 128, qk_ik * 4)
|
||||
self.mma_tiler = self.qk_mma_tiler
|
||||
self.cluster_layout_vmnk = cute.tiled_divide(cute.make_layout((1,1,1)), (qk_mma.thr_id.shape,))
|
||||
self.cta_tile_shape_mnk = (self.qk_mma_tiler[0]//cute.size(qk_mma.thr_id.shape), 128, self.qk_mma_tiler[2])
|
||||
self.c_layout = LayoutEnum.ROW_MAJOR
|
||||
self.epi_tile = utils.sm100.compute_epilogue_tile_shape(self.cta_tile_shape_mnk, False, self.c_layout, self.o_dtype)
|
||||
self.num_ab_stage = 1; self.num_acc_stage = 1
|
||||
self.q_smem_s = utils.sm100.make_smem_layout_a(qk_mma, self.qk_mma_tiler, self.q_dtype, self.q_stage)
|
||||
self.k_smem_s = utils.sm100.make_smem_layout_b(qk_mma, self.qk_mma_tiler, self.q_dtype, self.kv_stage)
|
||||
self.c_smem_s = utils.sm100.make_smem_layout_epi(self.o_dtype, self.c_layout, self.epi_tile, 2)
|
||||
qk_thr = qk_mma.get_slice(0); qk_as = qk_thr.partition_shape_C(self.qk_mma_tiler[:2])
|
||||
tStS = qk_thr.make_fragment_C(qk_as)
|
||||
self.tilePlikeFP32 = self.qk_mma_tiler[1] // Float32.width * self.o_dtype.width
|
||||
self.tmem_s0_offset = 0; self.tmem_p0_offset = 32; self.tmem_o0_offset = 0
|
||||
tCS = qk_mma.make_fragment_C(cute.append(qk_as, self.num_acc_stage))
|
||||
self.num_tmem_alloc_cols = utils.get_num_tmem_alloc_cols([tCS], arch="sm_100")
|
||||
cta = cute.size(qk_mma.thr_id.shape)
|
||||
q_s = cute.slice_(self.q_smem_s,(None,None,None,0)); k_s = cute.slice_(self.k_smem_s,(None,None,None,0))
|
||||
self.q_tx_bytes = cute.size_in_bytes(self.q_dtype, q_s) * cta
|
||||
self.kv_tx_bytes = cute.size_in_bytes(self.q_dtype, k_s) * cta
|
||||
|
||||
@cute.jit
|
||||
def __call__(self, q, k, c, stream):
|
||||
self.q_dtype = q.element_type; self.o_dtype = c.element_type; self.c_dtype = self.o_dtype
|
||||
self.a_major = LayoutEnum.from_tensor(q).mma_major_mode()
|
||||
self.b_major = LayoutEnum.from_tensor(k).mma_major_mode()
|
||||
self.c_layout = LayoutEnum.from_tensor(c)
|
||||
qk_mma = utils.sm100.make_trivial_tiled_mma(self.q_dtype, self.q_dtype, self.a_major, self.b_major, self.qk_acc_dtype, self.cta_group, (128,128), tcgen05.OperandSource.SMEM)
|
||||
self._setup(qk_mma)
|
||||
q_s = cute.slice_(self.q_smem_s,(None,None,None,0)); k_s = cute.slice_(self.k_smem_s,(None,None,None,0))
|
||||
tma_q,mQ = cute.nvgpu.make_tiled_tma_atom_A(utils.sm100.cluster_shape_to_tma_atom_A(self.cluster_shape_mn,qk_mma.thr_id),q,q_s,self.qk_mma_tiler,qk_mma,self.cluster_layout_vmnk.shape)
|
||||
tma_k,mK = cute.nvgpu.make_tiled_tma_atom_B(utils.sm100.cluster_shape_to_tma_atom_B(self.cluster_shape_mn,qk_mma.thr_id),k,k_s,self.qk_mma_tiler,qk_mma,self.cluster_layout_vmnk.shape)
|
||||
epi_s = cute.select(self.c_smem_s,mode=[0,1])
|
||||
tma_c,mC = cpasync.make_tiled_tma_atom(cpasync.CopyBulkTensorTileS2GOp(),c,epi_s,self.epi_tile)
|
||||
self._kernel(qk_mma,tma_q,mQ,tma_k,mK,tma_c,mC,self.cluster_layout_vmnk,self.q_smem_s,self.k_smem_s,self.c_smem_s,self.epi_tile).launch(grid=(1,1,1),block=[self.threads_per_cta,1,1],stream=stream)
|
||||
|
||||
@cute.kernel
|
||||
def _kernel(self, qk_mma, tma_q, mQ, tma_k, mK, tma_c, mC, cl_vmnk, q_smem_s, k_smem_s, c_smem_s, epi_tile):
|
||||
warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
|
||||
tidx,_,_ = cute.arch.thread_idx()
|
||||
if warp_idx == self.tma_warp_id:
|
||||
cpasync.prefetch_descriptor(tma_q); cpasync.prefetch_descriptor(tma_k); cpasync.prefetch_descriptor(tma_c)
|
||||
|
||||
@cute.struct
|
||||
class SS:
|
||||
q_bar: cute.struct.MemRange[cutlass.Int64, self.q_stage*2]
|
||||
kv_bar: cute.struct.MemRange[cutlass.Int64, self.kv_stage*2]
|
||||
s_bar: cute.struct.MemRange[cutlass.Int64, 2]
|
||||
acc_bar: cute.struct.MemRange[cutlass.Int64, self.num_acc_stage*2]
|
||||
tmem_dealloc: cutlass.Int64; holding: cutlass.Int32
|
||||
|
||||
smem = utils.SmemAllocator(); st = smem.allocate(SS)
|
||||
|
||||
qp,qc = pipeline.PipelineTmaUmma.create(barrier_storage=st.q_bar.data_ptr(),num_stages=self.q_stage,producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread,1),tx_count=self.q_tx_bytes,cta_layout_vmnk=cl_vmnk,defer_sync=True).make_participants()
|
||||
kvp,kvc = pipeline.PipelineTmaUmma.create(barrier_storage=st.kv_bar.data_ptr(),num_stages=self.kv_stage,producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread,1),tx_count=self.kv_tx_bytes,cta_layout_vmnk=cl_vmnk,defer_sync=True).make_participants()
|
||||
|
||||
s_prod,s_cons = pipeline.PipelineUmmaAsync.create(barrier_storage=st.s_bar.data_ptr(),num_stages=1,producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread,32*len(self.epilogue_warp_id))).make_participants()
|
||||
|
||||
# P-ready: softmax -> MMA signal using NamedBarrier
|
||||
softmax_done_bar = pipeline.NamedBarrier(barrier_id=3, num_threads=32 + 32*len(self.epilogue_warp_id))
|
||||
|
||||
acc_pipe = pipeline.PipelineUmmaAsync.create(barrier_storage=st.acc_bar.data_ptr(),num_stages=self.num_acc_stage,producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread,len(self.epilogue_warp_id)),cta_layout_vmnk=cl_vmnk,defer_sync=True)
|
||||
tmem_bar = pipeline.NamedBarrier(barrier_id=2,num_threads=32*len((self.mma_warp_id,*self.epilogue_warp_id)))
|
||||
tmem = utils.TmemAllocator(st.holding.ptr,barrier_for_retrieve=tmem_bar,allocator_warp_id=self.epilogue_warp_id[0],is_two_cta=cute.size(qk_mma.thr_id.shape)==2,two_cta_tmem_dealloc_mbar_ptr=st.tmem_dealloc.ptr)
|
||||
pipeline.pipeline_init_arrive(cluster_shape_mn=cl_vmnk,is_relaxed=True)
|
||||
|
||||
sQ = smem.allocate_tensor(element_type=self.q_dtype,layout=q_smem_s.outer,byte_alignment=128,swizzle=q_smem_s.inner)
|
||||
sK = smem.allocate_tensor(element_type=self.q_dtype,layout=k_smem_s.outer,byte_alignment=128,swizzle=k_smem_s.inner)
|
||||
sC = smem.allocate_tensor(element_type=self.o_dtype,layout=c_smem_s.outer,byte_alignment=128,swizzle=c_smem_s.inner)
|
||||
|
||||
gQ = cute.local_tile(mQ,cute.slice_(self.qk_mma_tiler,(None,0,None)),(None,None,None))
|
||||
gK = cute.local_tile(mK,cute.slice_(self.qk_mma_tiler,(0,None,None)),(None,None,None))
|
||||
gC = cute.local_tile(mC,cute.slice_(self.qk_mma_tiler,(None,None,0)),(None,None,None))
|
||||
n_kv_tiles = cute.size(gK, mode=[3])
|
||||
|
||||
qk_thr = qk_mma.get_slice(0)
|
||||
tCgQ = qk_thr.partition_A(gQ); tCgK = qk_thr.partition_B(gK); tCgC = qk_thr.partition_C(gC)
|
||||
a_lay = cute.make_layout(cute.slice_(cl_vmnk,(0,0,None,0)).shape)
|
||||
tAsQ,tAgQ = cpasync.tma_partition(tma_q,0,a_lay,cute.group_modes(sQ,0,3),cute.group_modes(tCgQ,0,3))
|
||||
b_lay = cute.make_layout(cute.slice_(cl_vmnk,(0,None,0,0)).shape)
|
||||
tBsK,tBgK = cpasync.tma_partition(tma_k,0,b_lay,cute.group_modes(sK,0,3),cute.group_modes(tCgK,0,3))
|
||||
tAgQ = tAgQ[(None,0,None,0)]; tBgK = tBgK[(None,0,None,0)]
|
||||
|
||||
tCrQ = qk_mma.make_fragment_A(sQ); tCrK = qk_mma.make_fragment_B(sK)
|
||||
|
||||
qk_as = qk_thr.partition_shape_C(self.qk_mma_tiler[:2])
|
||||
tStS = qk_thr.make_fragment_C(qk_as)
|
||||
tStS0 = cute.make_tensor(tStS.iterator + self.tmem_s0_offset, tStS.layout)
|
||||
|
||||
tCtS_fake = qk_mma.make_fragment_C(cute.append(qk_as, self.num_acc_stage))
|
||||
pipeline.pipeline_init_wait(cluster_shape_mn=cl_vmnk)
|
||||
|
||||
# TMA LOAD
|
||||
if warp_idx == self.tma_warp_id:
|
||||
qp.reset(); qh = qp.acquire_and_advance()
|
||||
cute.copy(tma_q,tAgQ[(None,qh.count)],tAsQ[(None,qh.index)],tma_bar_ptr=qh.barrier)
|
||||
qp.tail()
|
||||
kvp.reset(); pk = kvp.try_acquire()
|
||||
for kt in cutlass.range(n_kv_tiles,unroll=1):
|
||||
kh = kvp.acquire_and_advance(pk)
|
||||
cute.copy(tma_k,tBgK[(None,kh.count)],tBsK[(None,kh.index)],tma_bar_ptr=kh.barrier)
|
||||
pk = cutlass.Boolean(1)
|
||||
kvp.tail()
|
||||
|
||||
# MMA: QK, then wait for softmax, then signal done
|
||||
if warp_idx == self.mma_warp_id:
|
||||
tmem.wait_for_alloc()
|
||||
qc.reset(); qh = qc.wait_and_advance(); qh.release()
|
||||
kvc.reset(); pk = kvc.try_wait()
|
||||
|
||||
acc_st = pipeline.make_pipeline_state(pipeline.PipelineUserType.Producer, self.num_acc_stage)
|
||||
acc_pipe.producer_acquire(acc_st)
|
||||
|
||||
for kt in range(n_kv_tiles):
|
||||
kh = kvc.wait_and_advance(pk); pk = cutlass.Boolean(1)
|
||||
|
||||
sh = s_prod.acquire_and_advance()
|
||||
qk_mma.set(tcgen05.Field.ACCUMULATE, False)
|
||||
for kb in cutlass.range(cute.size(tCrQ,mode=[2]), unroll_full=True):
|
||||
cute.gemm(qk_mma, tStS0, tCrQ[(None,None,kb,0)], tCrK[(None,None,kb,kh.index)], tStS0)
|
||||
qk_mma.set(tcgen05.Field.ACCUMULATE, True)
|
||||
cute.arch.fence_view_async_tmem_store()
|
||||
sh.commit()
|
||||
kh.release()
|
||||
|
||||
# Wait for softmax to finish
|
||||
softmax_done_bar.arrive_and_wait()
|
||||
|
||||
# After all tiles: S contains the identity-softmax'd result
|
||||
# But we want to output P (written by softmax at p0 offset)
|
||||
# For this test, output what's at tmem_s0_offset (the final S accumulator)
|
||||
acc_pipe.producer_commit(acc_st); acc_st.advance()
|
||||
acc_pipe.producer_tail(acc_st)
|
||||
|
||||
# EPILOGUE: identity softmax + output
|
||||
if warp_idx < self.mma_warp_id:
|
||||
tmem.allocate(self.num_tmem_alloc_cols)
|
||||
tmem.wait_for_alloc()
|
||||
tmem_ptr = tmem.retrieve_ptr(self.qk_acc_dtype)
|
||||
sfw_idx = tidx % (32 * len(self.epilogue_warp_id))
|
||||
|
||||
# S load
|
||||
tmem_load_atom = cute.make_copy_atom(tcgen05.copy.Ld32x32bOp(tcgen05.copy.Repetition(32)), self.qk_acc_dtype)
|
||||
tiled_tmem_load = tcgen05.make_tmem_copy(tmem_load_atom, tStS0)
|
||||
thr_load = tiled_tmem_load.get_slice(sfw_idx)
|
||||
tTMEM_LOADtS = thr_load.partition_S(tStS0)
|
||||
cS = cute.make_identity_tensor((self.qk_mma_tiler[0], self.qk_mma_tiler[1]))
|
||||
tScS = qk_thr.partition_C(cS)
|
||||
tTMEM_LOADcS = thr_load.partition_D(tScS)
|
||||
|
||||
# P store
|
||||
tStS_P_layout = cute.composition(tStS.layout, cute.make_layout((128, self.tilePlikeFP32)))
|
||||
tStS_P = cute.make_tensor(tStS.iterator + self.tmem_p0_offset, tStS_P_layout)
|
||||
tmem_store_atom = cute.make_copy_atom(tcgen05.copy.St32x32bOp(tcgen05.copy.Repetition(32)), self.qk_acc_dtype)
|
||||
tiled_tmem_store = tcgen05.make_tmem_copy(tmem_store_atom, tStS_P)
|
||||
thr_store = tiled_tmem_store.get_slice(sfw_idx)
|
||||
tTMEM_STOREtS_x4 = thr_store.partition_D(tStS_P)
|
||||
tScS_P_layout = cute.composition(tScS.layout, cute.make_layout((128, self.tilePlikeFP32)))
|
||||
tScS_P = cute.make_tensor(tScS.iterator, tScS_P_layout)
|
||||
tTMEM_STOREcS = thr_store.partition_S(tScS_P)
|
||||
|
||||
for kt in range(n_kv_tiles):
|
||||
si_handle = s_cons.wait_and_advance()
|
||||
|
||||
# Load S
|
||||
tTMEM_LOADrS = cute.make_rmem_tensor(tTMEM_LOADcS.shape, self.qk_acc_dtype)
|
||||
cute.copy(tiled_tmem_load, tTMEM_LOADtS, tTMEM_LOADrS)
|
||||
|
||||
# Identity softmax: FP32 S -> BF16 P, write to TMEM at p0 offset
|
||||
tTMEM_STORErS_x4 = cute.make_rmem_tensor(tTMEM_STOREcS.shape, self.qk_acc_dtype)
|
||||
tTMEM_STORErS_x4_e = cute.make_tensor(cute.recast_ptr(tTMEM_STORErS_x4.iterator, dtype=self.q_dtype), tTMEM_LOADrS.layout)
|
||||
|
||||
frg_cnt = 4; frg_tile = cute.size(tTMEM_LOADrS) // frg_cnt
|
||||
tTMEM_LOADrS_frg = cute.logical_divide(tTMEM_LOADrS, cute.make_layout(frg_tile))
|
||||
tTMEM_STORErS_x4_e_frg = cute.logical_divide(tTMEM_STORErS_x4_e, cute.make_layout(frg_tile))
|
||||
for j in range(frg_cnt):
|
||||
s_vec = tTMEM_LOADrS_frg[None, j].load()
|
||||
tTMEM_STORErS_x4_e_frg[None, j].store(s_vec.to(self.q_dtype))
|
||||
cute.copy(tiled_tmem_store, tTMEM_STORErS_x4, tTMEM_STOREtS_x4)
|
||||
cute.arch.fence_view_async_tmem_store()
|
||||
|
||||
si_handle.release()
|
||||
# Signal MMA
|
||||
softmax_done_bar.arrive()
|
||||
|
||||
# Output: read from p0 offset (BF16 P values, but we read as FP32)
|
||||
# We need to output the BF16 P values. Read from p0 and convert.
|
||||
# Actually, output the S accumulator (at s0) for now to verify QK works
|
||||
tCtS_base = cute.make_tensor(tmem_ptr + self.tmem_s0_offset, tCtS_fake.layout)
|
||||
acc_cons_st = pipeline.make_pipeline_state(pipeline.PipelineUserType.Consumer, self.num_acc_stage)
|
||||
c_grp = pipeline.CooperativeGroup(pipeline.Agent.Thread, 32 * len(self.epilogue_warp_id))
|
||||
c_pipe = pipeline.PipelineTmaStore.create(num_stages=self.num_c_stage, producer_group=c_grp)
|
||||
acc_cons_st = utils.gemm.sm100.epilogue_tma_store(self, tidx, warp_idx, tma_c, tCtS_base, sC, tCgC, epi_tile, 0, const_expr(lambda x: x), (0,0,0), acc_cons_st, acc_pipe, c_pipe)
|
||||
c_pipe.producer_tail()
|
||||
tmem.relinquish_alloc_permit()
|
||||
tmem.free(tmem_ptr)
|
||||
|
||||
|
||||
def test():
|
||||
torch.manual_seed(42)
|
||||
n = 128; m, hd = 128, HEAD_DIM
|
||||
q = torch.randn(m, hd, 1, dtype=torch.bfloat16, device='cuda')
|
||||
k = torch.randn(n, hd, 1, dtype=torch.bfloat16, device='cuda')
|
||||
c = torch.zeros(m, n, 1, dtype=torch.bfloat16, device='cuda')
|
||||
qf = q[:,:,0].float(); kf = k[:,:,0].float()
|
||||
ref = (qf @ kf.T)
|
||||
mQ = ct.from_dlpack(q).mark_layout_dynamic(leading_dim=ct.get_leading_dim(q))
|
||||
mK = ct.from_dlpack(k).mark_layout_dynamic(leading_dim=ct.get_leading_dim(k))
|
||||
mC = ct.from_dlpack(c).mark_layout_dynamic(leading_dim=ct.get_leading_dim(c))
|
||||
stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream)
|
||||
kernel = QkSoftmaxTest()
|
||||
print('Compiling...', flush=True)
|
||||
compiled = cute.compile(kernel, mQ, mK, mC, stream)
|
||||
print('Running...', flush=True)
|
||||
compiled(mQ, mK, mC, stream)
|
||||
torch.cuda.synchronize()
|
||||
out = c[:,:,0].float()
|
||||
cos = torch.nn.functional.cosine_similarity(out.flatten().unsqueeze(0), ref.flatten().unsqueeze(0)).item()
|
||||
print(f'QK+softmax n={n}: cosine {cos:.6f} {"PASS" if cos >= 0.99 else "FAIL"}')
|
||||
if cos < 0.99:
|
||||
print(f' out[0,:4]={out[0,:4].tolist()} ref[0,:4]={ref[0,:4].tolist()}')
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
||||
269
tests/unit/test_qkonly.py
Normal file
269
tests/unit/test_qkonly.py
Normal file
@@ -0,0 +1,269 @@
|
||||
"""
|
||||
Debug: QK only (no PV) with KV-tile interleaving pipeline.
|
||||
Outputs P to GMEM to verify QK+softmax pipeline works.
|
||||
n=128, single KV tile, identity softmax.
|
||||
"""
|
||||
import torch, cutlass, cutlass.cute as cute, cutlass.utils as utils, cutlass.pipeline as pipeline
|
||||
from cutlass.cute.nvgpu import cpasync, tcgen05
|
||||
from cutlass import Float32, BFloat16, Int32, Boolean, const_expr
|
||||
from cutlass.utils import LayoutEnum
|
||||
from cutlass.utils.tmem_allocator import find_tmem_tensor_col_offset
|
||||
import cuda.bindings.driver as cuda
|
||||
import cutlass.torch as ct
|
||||
|
||||
HEAD_DIM = 64
|
||||
|
||||
|
||||
class QkOnlyTest:
|
||||
def __init__(self):
|
||||
self.acc_dtype = Float32; self.qk_acc_dtype = Float32
|
||||
self.q_dtype = BFloat16; self.o_dtype = BFloat16; self.c_dtype = BFloat16
|
||||
self.use_2cta_instrs = False; self.epilog_sync_bar_id = 1
|
||||
self.cluster_shape_mn = (1, 1); self.cta_group = tcgen05.CtaGroup.ONE
|
||||
self.epilogue_warp_id = (0,1,2,3); self.mma_warp_id = 4; self.tma_warp_id = 5
|
||||
self.threads_per_cta = 192; self.num_c_stage = 2
|
||||
self.kv_stage = 2; self.q_stage = 1
|
||||
|
||||
def _setup(self, qk_mma):
|
||||
qk_ik = cute.size(qk_mma.shape_mnk, mode=[2])
|
||||
self.qk_mma_tiler = (128, 128, qk_ik * 4)
|
||||
self.mma_tiler = self.qk_mma_tiler
|
||||
self.cluster_layout_vmnk = cute.tiled_divide(cute.make_layout((1,1,1)), (qk_mma.thr_id.shape,))
|
||||
self.cta_tile_shape_mnk = (self.qk_mma_tiler[0]//cute.size(qk_mma.thr_id.shape), 128, self.qk_mma_tiler[2])
|
||||
self.c_layout = LayoutEnum.ROW_MAJOR
|
||||
self.epi_tile = utils.sm100.compute_epilogue_tile_shape(self.cta_tile_shape_mnk, False, self.c_layout, self.o_dtype)
|
||||
self.num_ab_stage = 1; self.num_acc_stage = 1
|
||||
self.q_smem_s = utils.sm100.make_smem_layout_a(qk_mma, self.qk_mma_tiler, self.q_dtype, self.q_stage)
|
||||
self.k_smem_s = utils.sm100.make_smem_layout_b(qk_mma, self.qk_mma_tiler, self.q_dtype, self.kv_stage)
|
||||
self.c_smem_s = utils.sm100.make_smem_layout_epi(self.o_dtype, self.c_layout, self.epi_tile, 2)
|
||||
|
||||
qk_thr = qk_mma.get_slice(0); qk_as = qk_thr.partition_shape_C(self.qk_mma_tiler[:2])
|
||||
tStS = qk_thr.make_fragment_C(qk_as)
|
||||
|
||||
self.tilePlikeFP32 = self.qk_mma_tiler[1] // Float32.width * self.o_dtype.width
|
||||
self.tmem_s0_offset = 0; self.tmem_p0_offset = 32
|
||||
self.tmem_o0_offset = 0 # Output is at S offset for QK-only
|
||||
|
||||
tCS = qk_mma.make_fragment_C(cute.append(qk_as, self.num_acc_stage))
|
||||
self.num_tmem_alloc_cols = utils.get_num_tmem_alloc_cols([tCS], arch="sm_100")
|
||||
|
||||
cta = cute.size(qk_mma.thr_id.shape)
|
||||
q_s = cute.slice_(self.q_smem_s,(None,None,None,0))
|
||||
k_s = cute.slice_(self.k_smem_s,(None,None,None,0))
|
||||
self.q_tx_bytes = cute.size_in_bytes(self.q_dtype, q_s) * cta
|
||||
self.kv_tx_bytes = cute.size_in_bytes(self.q_dtype, k_s) * cta
|
||||
|
||||
@cute.jit
|
||||
def __call__(self, q, k, c, stream):
|
||||
self.q_dtype = q.element_type; self.o_dtype = c.element_type; self.c_dtype = self.o_dtype
|
||||
self.a_major = LayoutEnum.from_tensor(q).mma_major_mode()
|
||||
self.b_major = LayoutEnum.from_tensor(k).mma_major_mode()
|
||||
self.c_layout = LayoutEnum.from_tensor(c)
|
||||
|
||||
qk_mma = utils.sm100.make_trivial_tiled_mma(
|
||||
self.q_dtype, self.q_dtype, self.a_major, self.b_major,
|
||||
self.qk_acc_dtype, self.cta_group, (128,128), tcgen05.OperandSource.SMEM)
|
||||
self._setup(qk_mma)
|
||||
|
||||
q_s = cute.slice_(self.q_smem_s,(None,None,None,0))
|
||||
k_s = cute.slice_(self.k_smem_s,(None,None,None,0))
|
||||
|
||||
tma_q,mQ = cute.nvgpu.make_tiled_tma_atom_A(
|
||||
utils.sm100.cluster_shape_to_tma_atom_A(self.cluster_shape_mn,qk_mma.thr_id),
|
||||
q,q_s,self.qk_mma_tiler,qk_mma,self.cluster_layout_vmnk.shape)
|
||||
tma_k,mK = cute.nvgpu.make_tiled_tma_atom_B(
|
||||
utils.sm100.cluster_shape_to_tma_atom_B(self.cluster_shape_mn,qk_mma.thr_id),
|
||||
k,k_s,self.qk_mma_tiler,qk_mma,self.cluster_layout_vmnk.shape)
|
||||
epi_s = cute.select(self.c_smem_s,mode=[0,1])
|
||||
tma_c,mC = cpasync.make_tiled_tma_atom(cpasync.CopyBulkTensorTileS2GOp(),c,epi_s,self.epi_tile)
|
||||
|
||||
self._kernel(qk_mma, tma_q, mQ, tma_k, mK, tma_c, mC,
|
||||
self.cluster_layout_vmnk, self.q_smem_s, self.k_smem_s, self.c_smem_s, self.epi_tile
|
||||
).launch(grid=(1,1,1), block=[self.threads_per_cta,1,1], stream=stream)
|
||||
|
||||
@cute.kernel
|
||||
def _kernel(self, qk_mma, tma_q, mQ, tma_k, mK, tma_c, mC,
|
||||
cl_vmnk, q_smem_s, k_smem_s, c_smem_s, epi_tile):
|
||||
warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
|
||||
tidx,_,_ = cute.arch.thread_idx()
|
||||
|
||||
if warp_idx == self.tma_warp_id:
|
||||
cpasync.prefetch_descriptor(tma_q); cpasync.prefetch_descriptor(tma_k)
|
||||
cpasync.prefetch_descriptor(tma_c)
|
||||
|
||||
@cute.struct
|
||||
class SS:
|
||||
q_bar: cute.struct.MemRange[cutlass.Int64, self.q_stage*2]
|
||||
kv_bar: cute.struct.MemRange[cutlass.Int64, self.kv_stage*2]
|
||||
s_bar: cute.struct.MemRange[cutlass.Int64, 2]
|
||||
acc_bar: cute.struct.MemRange[cutlass.Int64, self.num_acc_stage*2]
|
||||
tmem_dealloc: cutlass.Int64; holding: cutlass.Int32
|
||||
|
||||
smem = utils.SmemAllocator(); st = smem.allocate(SS)
|
||||
|
||||
qp,qc = pipeline.PipelineTmaUmma.create(
|
||||
barrier_storage=st.q_bar.data_ptr(), num_stages=self.q_stage,
|
||||
producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),
|
||||
consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread,1),
|
||||
tx_count=self.q_tx_bytes, cta_layout_vmnk=cl_vmnk, defer_sync=True
|
||||
).make_participants()
|
||||
|
||||
kvp,kvc = pipeline.PipelineTmaUmma.create(
|
||||
barrier_storage=st.kv_bar.data_ptr(), num_stages=self.kv_stage,
|
||||
producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),
|
||||
consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread,1),
|
||||
tx_count=self.kv_tx_bytes, cta_layout_vmnk=cl_vmnk, defer_sync=True
|
||||
).make_participants()
|
||||
|
||||
s_prod,s_cons = pipeline.PipelineUmmaAsync.create(
|
||||
barrier_storage=st.s_bar.data_ptr(), num_stages=1,
|
||||
producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),
|
||||
consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread, 32*len(self.epilogue_warp_id))
|
||||
).make_participants()
|
||||
|
||||
softmax_done_bar = pipeline.NamedBarrier(barrier_id=3, num_threads=32 + 32*len(self.epilogue_warp_id))
|
||||
|
||||
acc_pipe = pipeline.PipelineUmmaAsync.create(
|
||||
barrier_storage=st.acc_bar.data_ptr(), num_stages=self.num_acc_stage,
|
||||
producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread),
|
||||
consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread, len(self.epilogue_warp_id)),
|
||||
cta_layout_vmnk=cl_vmnk, defer_sync=True)
|
||||
|
||||
tmem_bar = pipeline.NamedBarrier(barrier_id=2,
|
||||
num_threads=32*len((self.mma_warp_id,*self.epilogue_warp_id)))
|
||||
tmem = utils.TmemAllocator(st.holding.ptr, barrier_for_retrieve=tmem_bar,
|
||||
allocator_warp_id=self.epilogue_warp_id[0],
|
||||
is_two_cta=cute.size(qk_mma.thr_id.shape)==2,
|
||||
two_cta_tmem_dealloc_mbar_ptr=st.tmem_dealloc.ptr)
|
||||
|
||||
pipeline.pipeline_init_arrive(cluster_shape_mn=cl_vmnk, is_relaxed=True)
|
||||
|
||||
sQ = smem.allocate_tensor(element_type=self.q_dtype, layout=q_smem_s.outer, byte_alignment=128, swizzle=q_smem_s.inner)
|
||||
sK = smem.allocate_tensor(element_type=self.q_dtype, layout=k_smem_s.outer, byte_alignment=128, swizzle=k_smem_s.inner)
|
||||
sC = smem.allocate_tensor(element_type=self.o_dtype, layout=c_smem_s.outer, byte_alignment=128, swizzle=c_smem_s.inner)
|
||||
|
||||
gQ = cute.local_tile(mQ, cute.slice_(self.qk_mma_tiler,(None,0,None)), (None,None,None))
|
||||
gK = cute.local_tile(mK, cute.slice_(self.qk_mma_tiler,(0,None,None)), (None,None,None))
|
||||
gC = cute.local_tile(mC, cute.slice_(self.qk_mma_tiler,(None,None,0)), (None,None,None))
|
||||
n_kv_tiles = cute.size(gK, mode=[3])
|
||||
|
||||
qk_thr = qk_mma.get_slice(0)
|
||||
tCgQ = qk_thr.partition_A(gQ); tCgK = qk_thr.partition_B(gK)
|
||||
tCgC = qk_thr.partition_C(gC)
|
||||
|
||||
a_lay = cute.make_layout(cute.slice_(cl_vmnk,(0,0,None,0)).shape)
|
||||
tAsQ,tAgQ = cpasync.tma_partition(tma_q,0,a_lay,cute.group_modes(sQ,0,3),cute.group_modes(tCgQ,0,3))
|
||||
b_lay = cute.make_layout(cute.slice_(cl_vmnk,(0,None,0,0)).shape)
|
||||
tBsK,tBgK = cpasync.tma_partition(tma_k,0,b_lay,cute.group_modes(sK,0,3),cute.group_modes(tCgK,0,3))
|
||||
tAgQ = tAgQ[(None,0,None,0)]; tBgK = tBgK[(None,0,None,0)]
|
||||
|
||||
tCrQ = qk_mma.make_fragment_A(sQ); tCrK = qk_mma.make_fragment_B(sK)
|
||||
|
||||
qk_as = qk_thr.partition_shape_C(self.qk_mma_tiler[:2])
|
||||
tStS = qk_thr.make_fragment_C(qk_as)
|
||||
tStS0 = cute.make_tensor(tStS.iterator + self.tmem_s0_offset, tStS.layout)
|
||||
|
||||
tCtS_fake = qk_mma.make_fragment_C(cute.append(qk_as, self.num_acc_stage))
|
||||
|
||||
pipeline.pipeline_init_wait(cluster_shape_mn=cl_vmnk)
|
||||
|
||||
# TMA LOAD
|
||||
if warp_idx == self.tma_warp_id:
|
||||
qp.reset(); qh = qp.acquire_and_advance()
|
||||
cute.copy(tma_q, tAgQ[(None,qh.count)], tAsQ[(None,qh.index)], tma_bar_ptr=qh.barrier)
|
||||
qp.tail()
|
||||
|
||||
kvp.reset(); pk = kvp.try_acquire()
|
||||
for kt in cutlass.range(n_kv_tiles, unroll=1):
|
||||
kh = kvp.acquire_and_advance(pk)
|
||||
cute.copy(tma_k, tBgK[(None,kh.count)], tBsK[(None,kh.index)], tma_bar_ptr=kh.barrier)
|
||||
pk = cutlass.Boolean(1)
|
||||
kvp.tail()
|
||||
|
||||
# MMA
|
||||
if warp_idx == self.mma_warp_id:
|
||||
tmem.wait_for_alloc()
|
||||
qc.reset(); qh = qc.wait_and_advance(); qh.release()
|
||||
kvc.reset(); pk = kvc.try_wait()
|
||||
|
||||
acc_st = pipeline.make_pipeline_state(pipeline.PipelineUserType.Producer, self.num_acc_stage)
|
||||
acc_pipe.producer_acquire(acc_st)
|
||||
|
||||
for kt in range(n_kv_tiles):
|
||||
kh = kvc.wait_and_advance(pk); pk = cutlass.Boolean(1)
|
||||
|
||||
# QK only, accumulate across KV tiles
|
||||
sh = s_prod.acquire_and_advance()
|
||||
qk_mma.set(tcgen05.Field.ACCUMULATE, kt != 0)
|
||||
for kb in cutlass.range(cute.size(tCrQ,mode=[2]), unroll_full=True):
|
||||
cute.gemm(qk_mma, tStS0,
|
||||
tCrQ[(None,None,kb,0)], tCrK[(None,None,kb,kh.index)], tStS0)
|
||||
qk_mma.set(tcgen05.Field.ACCUMULATE, True)
|
||||
cute.arch.fence_view_async_tmem_store()
|
||||
sh.commit()
|
||||
kh.release()
|
||||
|
||||
# Wait for softmax (identity: just signal done)
|
||||
softmax_done_bar.arrive_and_wait()
|
||||
|
||||
acc_pipe.producer_commit(acc_st); acc_st.advance()
|
||||
acc_pipe.producer_tail(acc_st)
|
||||
|
||||
# EPILOGUE
|
||||
if warp_idx < self.mma_warp_id:
|
||||
tmem.allocate(self.num_tmem_alloc_cols)
|
||||
tmem.wait_for_alloc()
|
||||
tmem_ptr = tmem.retrieve_ptr(self.qk_acc_dtype)
|
||||
sfw_idx = tidx % (32 * len(self.epilogue_warp_id))
|
||||
|
||||
for kt in range(n_kv_tiles):
|
||||
si_handle = s_cons.wait_and_advance()
|
||||
# Identity softmax: no-op, just signal MMA
|
||||
si_handle.release()
|
||||
softmax_done_bar.arrive()
|
||||
|
||||
# Output S (QK result) to GMEM
|
||||
tCtS_base = cute.make_tensor(tmem_ptr + self.tmem_s0_offset, tCtS_fake.layout)
|
||||
acc_cons_st = pipeline.make_pipeline_state(pipeline.PipelineUserType.Consumer, self.num_acc_stage)
|
||||
c_grp = pipeline.CooperativeGroup(pipeline.Agent.Thread, 32 * len(self.epilogue_warp_id))
|
||||
c_pipe = pipeline.PipelineTmaStore.create(num_stages=self.num_c_stage, producer_group=c_grp)
|
||||
acc_cons_st = utils.gemm.sm100.epilogue_tma_store(
|
||||
self, tidx, warp_idx, tma_c, tCtS_base, sC, tCgC,
|
||||
epi_tile, 0, const_expr(lambda x: x), (0,0,0), acc_cons_st, acc_pipe, c_pipe)
|
||||
c_pipe.producer_tail()
|
||||
tmem.relinquish_alloc_permit()
|
||||
tmem.free(tmem_ptr)
|
||||
|
||||
|
||||
def test():
|
||||
torch.manual_seed(42)
|
||||
n = 128
|
||||
m, hd = 128, HEAD_DIM
|
||||
q = torch.randn(m, hd, 1, dtype=torch.bfloat16, device='cuda')
|
||||
k = torch.randn(n, hd, 1, dtype=torch.bfloat16, device='cuda')
|
||||
c = torch.zeros(m, n, 1, dtype=torch.bfloat16, device='cuda') # (128, 128) output = S
|
||||
|
||||
qf = q[:,:,0].float(); kf = k[:,:,0].float()
|
||||
ref = (qf @ kf.T)
|
||||
|
||||
mQ = ct.from_dlpack(q).mark_layout_dynamic(leading_dim=ct.get_leading_dim(q))
|
||||
mK = ct.from_dlpack(k).mark_layout_dynamic(leading_dim=ct.get_leading_dim(k))
|
||||
mC = ct.from_dlpack(c).mark_layout_dynamic(leading_dim=ct.get_leading_dim(c))
|
||||
stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream)
|
||||
|
||||
kernel = QkOnlyTest()
|
||||
print('Compiling...', flush=True)
|
||||
compiled = cute.compile(kernel, mQ, mK, mC, stream)
|
||||
print('Running...', flush=True)
|
||||
compiled(mQ, mK, mC, stream)
|
||||
torch.cuda.synchronize()
|
||||
out = c[:,:,0].float()
|
||||
cos = torch.nn.functional.cosine_similarity(out.flatten().unsqueeze(0), ref.flatten().unsqueeze(0)).item()
|
||||
print(f'QK-only n={n}: cosine {cos:.6f} {"PASS" if cos >= 0.99 else "FAIL"}')
|
||||
if cos < 0.99:
|
||||
print(f' out[0,:4]={out[0,:4].tolist()} ref[0,:4]={ref[0,:4].tolist()}')
|
||||
print(f' out stats: min={out.min().item():.4f} max={out.max().item():.4f}')
|
||||
print(f' ref stats: min={ref.min().item():.4f} max={ref.max().item():.4f}')
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
||||
Reference in New Issue
Block a user