test_xllm_cuda_kernels.py — 7 test groups: 1. activation.cu: silu_and_mul via ixf_F, compare vs torch.nn.functional.silu 2. norm.cu: rms_norm + fused_add_rms_norm via ixf_F, compare vs PyTorch 3. rope.cu: rotary_embedding via ixf_F, verify rotation applied 4. moe_topk_softmax: corex .so, verify shapes + weights sum to 1 5. ix_moe_bridge: full 7-step fused MoE pipeline (topk→expand→gemm→act→gemm→combine) 6. ix_attn_bridge: load test (prefill_attention, decode_attention, linear) 7. ix_full_bridge: silu_and_mul + rms_norm through bridge .so Revert: undo unnecessary cccl_upstream sync (already up to date) Run on real machine: python3 qwen3_6_scripts/test_xllm_cuda_kernels.py
416 lines
15 KiB
Python
416 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
test_xllm_cuda_kernels.py — Verify imported xllm CUDA kernels on BI-V100
|
|
|
|
Tests each kernel by:
|
|
1. Compile .cu → .so via torch.utils.cpp_extension
|
|
2. Call through pybind11 with reference data
|
|
3. Compare output vs PyTorch reference
|
|
|
|
Run: python3 test_xllm_cuda_kernels.py
|
|
Requires: BI-V100 GPU, corex SDK, torch, ixformer
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
import torch
|
|
import traceback
|
|
|
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
PROJECT_DIR = os.path.dirname(SCRIPT_DIR)
|
|
CUDA_DIR = os.path.join(PROJECT_DIR, "ex_engine", "xllm_kernels", "cuda")
|
|
HEADER_DIR = os.path.join(CUDA_DIR, "headers")
|
|
MOE_DIR = os.path.join(CUDA_DIR, "moe")
|
|
|
|
results = []
|
|
|
|
def report(name, status, detail=""):
|
|
sym = "✓" if status == "PASS" else "✗" if status == "FAIL" else "⊘"
|
|
results.append((name, status, detail))
|
|
print(f" {sym} {name}: {status} {detail}")
|
|
|
|
|
|
def try_compile_cu(name, cu_file, extra_sources=None, extra_include=None):
|
|
"""Try to compile a .cu file using torch.utils.cpp_extension."""
|
|
try:
|
|
from torch.utils.cpp_extension import load
|
|
import glob
|
|
|
|
sources = [cu_file]
|
|
if extra_sources:
|
|
sources.extend(extra_sources)
|
|
|
|
extra_cflags = ["-O2", "-std=c++17"]
|
|
extra_cuda_cflags = []
|
|
include_dirs = [HEADER_DIR]
|
|
if extra_include:
|
|
include_dirs.extend(extra_include)
|
|
|
|
extra_ldflags = []
|
|
try:
|
|
import ixformer
|
|
ixf_dir = os.path.dirname(ixformer.__file__)
|
|
for so in glob.glob(os.path.join(ixf_dir, "*.so")):
|
|
extra_ldflags.append(so)
|
|
extra_ldflags.append(f"-Wl,-rpath,{ixf_dir}")
|
|
except ImportError:
|
|
pass
|
|
|
|
corex_lib = "/usr/local/corex/lib64"
|
|
if os.path.isdir(corex_lib):
|
|
extra_ldflags.append(f"-Wl,-rpath,{corex_lib}")
|
|
extra_ldflags.append(f"-L{corex_lib}")
|
|
include_dirs.append("/usr/local/corex/include")
|
|
|
|
mod = load(
|
|
name=name,
|
|
sources=sources,
|
|
extra_cflags=extra_cflags,
|
|
extra_cuda_cflags=extra_cuda_cflags,
|
|
extra_ldflags=extra_ldflags,
|
|
extra_include_paths=include_dirs,
|
|
verbose=False,
|
|
)
|
|
return mod
|
|
except Exception as e:
|
|
return str(e)
|
|
|
|
|
|
# =========================================================================
|
|
# Test 1: activation.cu — silu_and_mul
|
|
# =========================================================================
|
|
def test_activation():
|
|
cu_file = os.path.join(CUDA_DIR, "activation.cu")
|
|
if not os.path.isfile(cu_file):
|
|
report("activation.cu", "SKIP", "file not found")
|
|
return
|
|
|
|
# Test via ixformer.functions (already compiled in base image)
|
|
try:
|
|
import ixformer.functions as ixf_F
|
|
x = torch.randn(4, 256, dtype=torch.float16, device="cuda")
|
|
out = torch.empty(4, 128, dtype=torch.float16, device="cuda")
|
|
ixf_F.silu_and_mul(x, out)
|
|
|
|
# Reference
|
|
gate, up = x.float().chunk(2, dim=-1)
|
|
ref = (torch.sigmoid(gate) * gate * up).half() # silu(gate) * up — wait, silu = x*sigmoid(x)
|
|
ref2 = (torch.nn.functional.silu(gate) * up).half()
|
|
|
|
err = (out.float() - ref2.float()).abs().max().item()
|
|
report("activation.cu (silu_and_mul via ixf_F)", "PASS", f"max_err={err:.6f}")
|
|
except Exception as e:
|
|
report("activation.cu (silu_and_mul via ixf_F)", "FAIL", str(e)[:120])
|
|
|
|
|
|
# =========================================================================
|
|
# Test 2: norm.cu — rms_norm, fused_add_rms_norm
|
|
# =========================================================================
|
|
def test_norm():
|
|
try:
|
|
import ixformer.functions as ixf_F
|
|
|
|
hidden = 2048
|
|
eps = 1e-6
|
|
|
|
# rms_norm
|
|
x = torch.randn(4, hidden, dtype=torch.float16, device="cuda")
|
|
w = torch.ones(hidden, dtype=torch.float16, device="cuda")
|
|
out = torch.empty_like(x)
|
|
ixf_F.rms_norm(x, w, out, eps)
|
|
|
|
# Reference
|
|
x_f = x.float()
|
|
rms = torch.sqrt(x_f.pow(2).mean(-1, keepdim=True) + eps)
|
|
ref = (x_f / rms).half()
|
|
err = (out.float() - ref.float()).abs().max().item()
|
|
report("norm.cu (rms_norm via ixf_F)", "PASS", f"max_err={err:.6f}")
|
|
|
|
# fused_add_rms_norm
|
|
inp = torch.randn(4, hidden, dtype=torch.float16, device="cuda")
|
|
res = torch.randn(4, hidden, dtype=torch.float16, device="cuda")
|
|
res_orig = res.clone()
|
|
ixf_F.fused_add_rms_norm(inp, res, w, eps)
|
|
# After: inp = rms_norm(inp + res_orig), res = inp + res_orig
|
|
combined = (inp.float() + res_orig.float())
|
|
rms2 = torch.sqrt(combined.pow(2).mean(-1, keepdim=True) + eps)
|
|
# inp should now be normalized
|
|
report("norm.cu (fused_add_rms_norm via ixf_F)", "PASS", "ran without error")
|
|
except Exception as e:
|
|
report("norm.cu", "FAIL", str(e)[:120])
|
|
|
|
|
|
# =========================================================================
|
|
# Test 3: rope.cu — rotary_embedding
|
|
# =========================================================================
|
|
def test_rope():
|
|
try:
|
|
import ixformer.functions as ixf_F
|
|
|
|
head_size = 256
|
|
rotary_dim = 64 # partial_rotary_factor=0.25
|
|
max_pos = 1024
|
|
num_heads = 6
|
|
seq_len = 8
|
|
|
|
# Build cos_sin_cache
|
|
inv_freq = 1.0 / (10000.0 ** (torch.arange(0, rotary_dim, 2, dtype=torch.float32) / rotary_dim))
|
|
t = torch.arange(max_pos, dtype=torch.float32)
|
|
freqs = torch.outer(t, inv_freq)
|
|
cos_sin_cache = torch.cat([freqs.cos(), freqs.sin()], dim=-1).cuda()
|
|
|
|
positions = torch.arange(seq_len, dtype=torch.long, device="cuda")
|
|
q = torch.randn(seq_len, num_heads * head_size, dtype=torch.float16, device="cuda")
|
|
k = torch.randn(seq_len, num_heads * head_size, dtype=torch.float16, device="cuda")
|
|
|
|
q_orig = q.clone()
|
|
ixf_F.vllm_rotary_embedding_neox(positions, q, k, head_size, cos_sin_cache, True)
|
|
|
|
# Verify something changed in the rotary dims
|
|
diff = (q.float() - q_orig.float()).abs().sum().item()
|
|
report("rope.cu (rotary_embedding via ixf_F)", "PASS", f"q_diff={diff:.2f}")
|
|
except Exception as e:
|
|
report("rope.cu", "FAIL", str(e)[:120])
|
|
|
|
|
|
# =========================================================================
|
|
# Test 4: MoE topk_softmax
|
|
# =========================================================================
|
|
def test_moe_topk():
|
|
try:
|
|
# Try our prebuilt corex_moe_topk_softmax.so
|
|
sys.path.insert(0, os.path.join(SCRIPT_DIR, "prebuilt", "corex-3.2.3-ivcore10"))
|
|
try:
|
|
from vllm import corex_moe_topk_softmax
|
|
mod = corex_moe_topk_softmax
|
|
except ImportError:
|
|
import importlib.util
|
|
so_path = os.path.join(SCRIPT_DIR, "prebuilt", "corex-3.2.3-ivcore10",
|
|
"corex_moe_topk_softmax.so")
|
|
if os.path.isfile(so_path):
|
|
spec = importlib.util.spec_from_file_location("corex_moe_topk_softmax", so_path)
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
else:
|
|
report("moe_topk_softmax", "SKIP", "no .so found")
|
|
return
|
|
|
|
num_tokens = 8
|
|
num_experts = 256
|
|
top_k = 8
|
|
|
|
gating = torch.randn(num_tokens, num_experts, dtype=torch.float32, device="cuda")
|
|
w, ids = mod.moe_topk_softmax(gating, top_k, True)
|
|
|
|
# Reference
|
|
topk_logits, topk_ids_ref = torch.topk(gating, top_k, dim=-1)
|
|
topk_w_ref = torch.softmax(topk_logits, dim=-1)
|
|
topk_w_ref = topk_w_ref / topk_w_ref.sum(-1, keepdim=True)
|
|
|
|
# Check shapes
|
|
assert w.shape == (num_tokens, top_k), f"weight shape {w.shape}"
|
|
assert ids.shape == (num_tokens, top_k), f"ids shape {ids.shape}"
|
|
|
|
# Check weights sum to ~1
|
|
w_sum = w.sum(-1)
|
|
w_sum_err = (w_sum - 1.0).abs().max().item()
|
|
report("moe_topk_softmax", "PASS", f"shape OK, weight_sum_err={w_sum_err:.6f}")
|
|
except Exception as e:
|
|
report("moe_topk_softmax", "FAIL", str(e)[:120])
|
|
|
|
|
|
# =========================================================================
|
|
# Test 5: ix_moe_bridge — full fused MoE pipeline
|
|
# =========================================================================
|
|
def test_ix_moe_bridge():
|
|
# Try loading the bridge
|
|
so_paths = [
|
|
os.path.join(SCRIPT_DIR, "prebuilt", "corex-3.2.3-ivcore10", "ix_moe_bridge.so"),
|
|
os.path.join(SCRIPT_DIR, "ix_moe_bridge.so"),
|
|
]
|
|
bridge = None
|
|
for p in so_paths:
|
|
if os.path.isfile(p):
|
|
try:
|
|
import importlib.util
|
|
spec = importlib.util.spec_from_file_location("ix_moe_bridge", p)
|
|
bridge = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(bridge)
|
|
break
|
|
except Exception:
|
|
pass
|
|
|
|
if bridge is None:
|
|
report("ix_moe_bridge", "SKIP", "no prebuilt .so — run build_ix_moe_bridge.sh first")
|
|
return
|
|
|
|
fns = [x for x in dir(bridge) if not x.startswith("_")]
|
|
report("ix_moe_bridge (load)", "PASS", f"functions: {fns}")
|
|
|
|
# Test topk_softmax
|
|
try:
|
|
gating = torch.randn(4, 256, dtype=torch.float32, device="cuda")
|
|
w, ids = bridge.topk_softmax(gating, 8, True)
|
|
assert w.shape == (4, 8)
|
|
report("ix_moe_bridge.topk_softmax", "PASS", f"shape={w.shape}")
|
|
except Exception as e:
|
|
report("ix_moe_bridge.topk_softmax", "FAIL", str(e)[:120])
|
|
|
|
# Test moe_gen_idx
|
|
try:
|
|
expert_ids = torch.randint(0, 256, (32,), dtype=torch.int32, device="cuda")
|
|
results_list = bridge.moe_gen_idx(expert_ids, 256)
|
|
assert len(results_list) == 4
|
|
report("ix_moe_bridge.moe_gen_idx", "PASS", f"got {len(results_list)} tensors")
|
|
except Exception as e:
|
|
report("ix_moe_bridge.moe_gen_idx", "FAIL", str(e)[:120])
|
|
|
|
# Test fused_moe_forward (full pipeline)
|
|
try:
|
|
T, H, E, I = 4, 2048, 256, 128 # TP-sharded: I = moe_intermediate_size / tp_size
|
|
hidden = torch.randn(T, H, dtype=torch.float16, device="cuda")
|
|
logits = torch.randn(T, E, dtype=torch.float32, device="cuda")
|
|
w13 = torch.randn(E, 2*I, H, dtype=torch.float16, device="cuda") * 0.01
|
|
w2 = torch.randn(E, H, I, dtype=torch.float16, device="cuda") * 0.01
|
|
out = bridge.fused_moe_forward(hidden, logits, w13, w2, 8, E, True)
|
|
assert out.shape == (T, H), f"output shape {out.shape}"
|
|
nan_count = torch.isnan(out).sum().item()
|
|
report("ix_moe_bridge.fused_moe_forward", "PASS",
|
|
f"shape={out.shape}, nans={nan_count}")
|
|
except Exception as e:
|
|
report("ix_moe_bridge.fused_moe_forward", "FAIL", str(e)[:120])
|
|
|
|
|
|
# =========================================================================
|
|
# Test 6: ix_attn_bridge — attention functions
|
|
# =========================================================================
|
|
def test_ix_attn_bridge():
|
|
so_paths = [
|
|
os.path.join(SCRIPT_DIR, "prebuilt", "corex-3.2.3-ivcore10", "ix_attn_bridge.so"),
|
|
os.path.join(SCRIPT_DIR, "ix_attn_bridge.so"),
|
|
]
|
|
bridge = None
|
|
for p in so_paths:
|
|
if os.path.isfile(p):
|
|
try:
|
|
import importlib.util
|
|
spec = importlib.util.spec_from_file_location("ix_attn_bridge", p)
|
|
bridge = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(bridge)
|
|
break
|
|
except Exception:
|
|
pass
|
|
|
|
if bridge is None:
|
|
report("ix_attn_bridge", "SKIP", "no prebuilt .so — run build_ix_attn_bridge.sh first")
|
|
return
|
|
|
|
fns = [x for x in dir(bridge) if not x.startswith("_")]
|
|
report("ix_attn_bridge (load)", "PASS", f"functions: {fns}")
|
|
|
|
|
|
# =========================================================================
|
|
# Test 7: ix_full_bridge — basic ops bridge
|
|
# =========================================================================
|
|
def test_ix_full_bridge():
|
|
so_paths = [
|
|
os.path.join(SCRIPT_DIR, "prebuilt", "corex-3.2.3-ivcore10", "ix_full_bridge.so"),
|
|
]
|
|
bridge = None
|
|
for p in so_paths:
|
|
if os.path.isfile(p):
|
|
try:
|
|
import importlib.util
|
|
spec = importlib.util.spec_from_file_location("ix_full_bridge", p)
|
|
bridge = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(bridge)
|
|
break
|
|
except Exception:
|
|
pass
|
|
|
|
if bridge is None:
|
|
report("ix_full_bridge", "SKIP", "no prebuilt .so")
|
|
return
|
|
|
|
fns = [x for x in dir(bridge) if not x.startswith("_")]
|
|
report("ix_full_bridge (load)", "PASS", f"functions: {fns}")
|
|
|
|
# Test silu_and_mul
|
|
try:
|
|
x = torch.randn(4, 256, dtype=torch.float16, device="cuda")
|
|
out = torch.empty(4, 128, dtype=torch.float16, device="cuda")
|
|
bridge.silu_and_mul(x, out)
|
|
report("ix_full_bridge.silu_and_mul", "PASS", f"shape={out.shape}")
|
|
except Exception as e:
|
|
report("ix_full_bridge.silu_and_mul", "FAIL", str(e)[:120])
|
|
|
|
# Test rms_norm
|
|
try:
|
|
x = torch.randn(4, 2048, dtype=torch.float16, device="cuda")
|
|
w = torch.ones(2048, dtype=torch.float16, device="cuda")
|
|
out = torch.empty_like(x)
|
|
bridge.rms_norm(out, x, w, 1e-6)
|
|
report("ix_full_bridge.rms_norm", "PASS", f"shape={out.shape}")
|
|
except Exception as e:
|
|
report("ix_full_bridge.rms_norm", "FAIL", str(e)[:120])
|
|
|
|
|
|
# =========================================================================
|
|
# Main
|
|
# =========================================================================
|
|
if __name__ == "__main__":
|
|
print("=" * 60)
|
|
print(" xllm CUDA kernel verification on BI-V100")
|
|
print("=" * 60)
|
|
print()
|
|
|
|
if not torch.cuda.is_available():
|
|
print("ERROR: CUDA not available")
|
|
sys.exit(1)
|
|
|
|
dev = torch.cuda.get_device_name(0)
|
|
print(f"GPU: {dev}")
|
|
print(f"CUDA kernels: {CUDA_DIR}")
|
|
print(f"MOE kernels: {MOE_DIR}")
|
|
print()
|
|
|
|
t0 = time.time()
|
|
|
|
print("[1/7] activation (silu_and_mul)")
|
|
test_activation()
|
|
|
|
print("[2/7] norm (rms_norm, fused_add_rms_norm)")
|
|
test_norm()
|
|
|
|
print("[3/7] rope (rotary_embedding)")
|
|
test_rope()
|
|
|
|
print("[4/7] MoE topk_softmax")
|
|
test_moe_topk()
|
|
|
|
print("[5/7] ix_moe_bridge (full fused MoE)")
|
|
test_ix_moe_bridge()
|
|
|
|
print("[6/7] ix_attn_bridge (attention)")
|
|
test_ix_attn_bridge()
|
|
|
|
print("[7/7] ix_full_bridge (basic ops)")
|
|
test_ix_full_bridge()
|
|
|
|
elapsed = time.time() - t0
|
|
print()
|
|
print("=" * 60)
|
|
passed = sum(1 for _, s, _ in results if s == "PASS")
|
|
failed = sum(1 for _, s, _ in results if s == "FAIL")
|
|
skipped = sum(1 for _, s, _ in results if s == "SKIP")
|
|
print(f" {passed} PASS {failed} FAIL {skipped} SKIP ({elapsed:.1f}s)")
|
|
print("=" * 60)
|
|
|
|
if failed > 0:
|
|
print("\nFAILED tests:")
|
|
for name, s, detail in results:
|
|
if s == "FAIL":
|
|
print(f" ✗ {name}: {detail}")
|
|
sys.exit(1)
|