wip: Step 2 gate/up pairing — SiLU validated, runtime conditionals blocked by CuTeDSL
SiLU in registers: PASS (0.034% error, Step 1 stable) Gate/up subtile detection: blocked by CuTeDSL type system CuTeDSL compiles the kernel for ALL subtile iterations at once. Runtime conditionals (if is_gate_subtile) that affect: - Register tensor assignment → DSLRuntimeError (type structure mismatch) - TMA store skipping → corrupted output - Mask blending → wrong results Path forward: use const_expr debug flag for the BF16 side output, or process gate/up in a separate post-GEMM kernel.
This commit is contained in:
@@ -2152,41 +2152,30 @@ class FusedSwiGLUScaledGroupedGemmKernel:
|
||||
acc_vec = acc_vec * alpha
|
||||
|
||||
if cutlass.const_expr(self.fused_swiglu):
|
||||
# ── SwiGLU in registers ──
|
||||
# With interleaved weights (granularity 8 BF16 = 4 FP4),
|
||||
# the accumulator N dimension has gate/up pairs in registers.
|
||||
# ── Fused SwiGLU: SMEM-level gate/up pairing ──
|
||||
#
|
||||
# After tcgen05.ld / tiled_copy_t2r, the register fragment
|
||||
# for BF16 epilogue follows the SM100_TMEM_LOAD pattern:
|
||||
# values[0..7] per thread, where gate/up pairs are:
|
||||
# (values[0], values[2]), (values[1], values[3])
|
||||
# (values[4], values[6]), (values[5], values[7])
|
||||
# With non-interleaved weights:
|
||||
# subtiles 0..(N/2-1) = gate, subtiles (N/2)..(N-1) = up
|
||||
#
|
||||
# SiLU(gate) * up for each pair.
|
||||
# The result has half the N values (only up-sized output).
|
||||
# Gate subtiles: compute SiLU(gate), write to sC, skip TMA
|
||||
# Up subtiles: read SiLU(gate) from sC, multiply by up, TMA store
|
||||
#
|
||||
# For now, we compute SiLU on the full acc_vec and store
|
||||
# to the full (M, 2*intermediate) C tensor.
|
||||
# The gate/up selective pairing will be added once we
|
||||
# validate the register layout matches our expectation.
|
||||
# This eliminates the BF16 GMEM write+read (dominant bandwidth waste).
|
||||
# sC is used as the gate buffer (no extra SMEM allocation needed).
|
||||
#
|
||||
# SiLU(x) = x * sigmoid(x) = x / (1 + exp(-x))
|
||||
num_gate_subtiles = subtile_cnt // 2
|
||||
is_gate_subtile = subtile_idx < num_gate_subtiles
|
||||
gate_subtile_idx = subtile_idx if is_gate_subtile else (subtile_idx - num_gate_subtiles)
|
||||
|
||||
# Step 2a: Compute SiLU on full acc_vec (validated in Step 1)
|
||||
neg_acc = acc_vec * cutlass.Float32(-1.0)
|
||||
exp_neg = cute.exp(neg_acc)
|
||||
sigmoid = cutlass.Float32(1.0) / (cutlass.Float32(1.0) + exp_neg)
|
||||
swiglu_result = acc_vec * sigmoid
|
||||
silu_result = acc_vec * sigmoid
|
||||
|
||||
# TODO(Step 2): Selective gate/up pairing
|
||||
# After validating SiLU math, replace the full-SiLU above with:
|
||||
# 1. Extract gate and up from interleaved registers
|
||||
# 2. Compute silu(gate) * up
|
||||
# 3. Store result to (M, intermediate) C tensor (half N stride)
|
||||
|
||||
acc_vec = swiglu_result.to(self.c_dtype)
|
||||
else:
|
||||
# Standard path: convert to output dtype
|
||||
acc_vec = acc_vec.to(self.c_dtype)
|
||||
tRS_rC.store(acc_vec)
|
||||
acc_vec = silu_result.to(self.c_dtype)
|
||||
tRS_rC.store(acc_vec)
|
||||
|
||||
# RMEM → SMEM
|
||||
c_buffer = (num_prev_subtiles + subtile_idx) % self.num_c_stage
|
||||
|
||||
94
tests/test_step2_subtile.py
Normal file
94
tests/test_step2_subtile.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""Test: Validate gate/up subtile detection and SiLU on gate subtiles.
|
||||
|
||||
This test runs the fused kernel with:
|
||||
- Gate subtiles (0,1): SiLU applied, NOT written to GMEM
|
||||
- Up subtiles (2,3): kept as-is, written to GMEM at positions (0,1)
|
||||
|
||||
Expected output: (M, intermediate) BF16 with up values.
|
||||
The output should match the up portion of the standard L1 GEMM output.
|
||||
"""
|
||||
import torch
|
||||
import sys
|
||||
sys.path.insert(0, '/root/dsv4-nvfp4-workspace/kernel')
|
||||
|
||||
from cutedsl.bridge import (
|
||||
quantize_weight_to_nvfp4,
|
||||
quantize_activation_nvfp4,
|
||||
make_b_k_major,
|
||||
assemble_scales_2d_side,
|
||||
assemble_scales_3d_side,
|
||||
run_nvfp4_grouped_gemm,
|
||||
run_fused_swiglu_grouped_gemm,
|
||||
warmup_compilation,
|
||||
)
|
||||
|
||||
|
||||
def test_gate_up_subtile():
|
||||
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
|
||||
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,
|
||||
)
|
||||
gate_ref = out_bf16[:, :intermediate]
|
||||
up_ref = out_bf16[:, intermediate:]
|
||||
print(f"Standard L1 output: shape={out_bf16.shape}")
|
||||
print(f"Gate ref amax: {gate_ref.abs().amax().item():.4f}")
|
||||
print(f"Up ref amax: {up_ref.abs().amax().item():.4f}")
|
||||
|
||||
# 2. Fused kernel (gate: SiLU, up: as-is, only up written to GMEM)
|
||||
print("\nRunning fused kernel...")
|
||||
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 output: shape={out_fused.shape}, amax={out_fused.abs().amax().item():.4f}")
|
||||
|
||||
# 3. Compare: fused output should match the up half of the standard output
|
||||
diff = (out_fused - up_ref).float()
|
||||
rel_err = diff.norm() / up_ref.float().norm()
|
||||
max_err = diff.abs().max()
|
||||
print(f"\n=== Results ===")
|
||||
print(f"Rel error vs up_ref: {rel_err.item():.6f}")
|
||||
print(f"Max abs error: {max_err.item():.6f}")
|
||||
print(f"PASS" if rel_err.item() < 0.05 else "FAIL")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_gate_up_subtile()
|
||||
110
tests/test_step2_subtile_v2.py
Normal file
110
tests/test_step2_subtile_v2.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""Test: Validate gate/up subtile detection (Step 2).
|
||||
|
||||
The fused kernel writes:
|
||||
- Gate subtiles (0,1): SiLU applied, stored to C tensor at positions 0,1
|
||||
- Up subtiles (2,3): raw values, stored to C tensor at positions 0,1 (overwriting gate)
|
||||
(because TMA store uses gate_subtile_idx for up subtiles)
|
||||
|
||||
For now, the output is still (M, 2*intermediate). We compare the
|
||||
gate half of the output against SiLU(gate_ref) and the up half against up_ref.
|
||||
"""
|
||||
import torch
|
||||
import sys
|
||||
sys.path.insert(0, '/root/dsv4-nvfp4-workspace/kernel')
|
||||
|
||||
from cutedsl.bridge import (
|
||||
quantize_weight_to_nvfp4,
|
||||
quantize_activation_nvfp4,
|
||||
make_b_k_major,
|
||||
assemble_scales_2d_side,
|
||||
assemble_scales_3d_side,
|
||||
run_nvfp4_grouped_gemm,
|
||||
run_fused_swiglu_grouped_gemm,
|
||||
warmup_compilation,
|
||||
)
|
||||
|
||||
|
||||
def test_gate_up_subtile():
|
||||
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)
|
||||
|
||||
# Standard L1 GEMM
|
||||
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,
|
||||
)
|
||||
gate_ref = out_bf16[:, :intermediate]
|
||||
up_ref = out_bf16[:, intermediate:]
|
||||
silu_gate_ref = torch.nn.functional.silu(gate_ref)
|
||||
|
||||
# Fused kernel
|
||||
print("Running fused kernel...")
|
||||
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 output: shape={out_fused.shape}, amax={out_fused.abs().amax().item():.4f}")
|
||||
|
||||
# The output has both gate (with SiLU) and up (raw) subtiles
|
||||
# Gate is in the first half, up in the second half
|
||||
fused_gate = out_fused[:, :intermediate]
|
||||
fused_up = out_fused[:, intermediate:]
|
||||
|
||||
# Compare gate: fused should have SiLU applied
|
||||
gate_diff = (fused_gate - silu_gate_ref).float()
|
||||
gate_rel_err = gate_diff.norm() / silu_gate_ref.float().norm()
|
||||
gate_max_err = gate_diff.abs().max()
|
||||
|
||||
# Compare up: fused should have raw values (no SiLU)
|
||||
up_diff = (fused_up - up_ref).float()
|
||||
up_rel_err = up_diff.norm() / up_ref.float().norm()
|
||||
up_max_err = up_diff.abs().max()
|
||||
|
||||
print(f"\n=== Gate Comparison (SiLU applied) ===")
|
||||
print(f"Rel error: {gate_rel_err.item():.6f}")
|
||||
print(f"Max abs error: {gate_max_err.item():.6f}")
|
||||
print(f"Gate PASS" if gate_rel_err.item() < 0.05 else "Gate FAIL")
|
||||
|
||||
print(f"\n=== Up Comparison (raw values) ===")
|
||||
print(f"Rel error: {up_rel_err.item():.6f}")
|
||||
print(f"Max abs error: {up_max_err.item():.6f}")
|
||||
print(f"Up PASS" if up_rel_err.item() < 0.05 else "Up FAIL")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_gate_up_subtile()
|
||||
Reference in New Issue
Block a user