CUDA graph: Implement eager-break-at-attention decoder with sub-graph A/B split
Architecture: - Sub-graph A (per layer): mHC pre + fused rmsnorm/quantize + Q/KV projections + RoPE - Eager section: KV append + Compressor + Indexer + KV gather + FMHA + Inverse RoPE - Sub-graph B (per layer): o_proj + mHC post(attn) + mHC pre(FFN) + fused rmsnorm/quantize + Router + MoE + SE + mHC post(FFN) - lm_head graph on cuda:0 Key features: - Per-GPU token/position buffers (avoids cross-device .to() inside graphs) - Pre-allocated I/O buffers with fixed addresses for graph capture - Uses fused P5 rmsnorm+quantize path inside graphs (production path) - Captures after step 0 warmup (after CuTeDSL compile + gsa fix) - Eager path unchanged for warmup and --no-cuda-graph runs - eager_attention() extracted from forward_attention() for graph replay path Wires --cuda-graph flag into main() decode loop.
This commit is contained in:
@@ -134,53 +134,96 @@ def unweighted_rmsnorm(x, eps=1e-6):
|
||||
class CUDAGraphDecoder:
|
||||
"""Captures and replays CUDA graphs for the decode loop.
|
||||
|
||||
After one warmup step, each layer's compute is captured as a CUDA graph.
|
||||
Replay eliminates Python dispatch overhead (~94ms for 61 layers) and
|
||||
kernel launch latency.
|
||||
Architecture: Eager-break-at-attention (Phase 1)
|
||||
|
||||
Each layer's forward is split into two captured sub-graphs with an eager
|
||||
section between them:
|
||||
|
||||
Sub-graph A (pre-attention compute):
|
||||
mHC pre_block → RMSNorm + quantize → Q/KV projections → RoPE
|
||||
Outputs: q_heads (roped), kv_roped, x_normed (for compressor), q_a (for indexer)
|
||||
|
||||
EAGER section (not captured — dynamic shapes from compressor/FMHA):
|
||||
KV append → Compressor → Indexer → KV gather → FMHA → Inverse RoPE → o_proj
|
||||
Outputs: F_attn (for mHC post_block), attn_out (intermediate, for o_proj)
|
||||
|
||||
Sub-graph B (post-attention compute + FFN):
|
||||
mHC post_block (attn) → mHC pre_block (FFN) → RMSNorm → Router → MoE → SE → mHC post_block (FFN)
|
||||
|
||||
This eliminates ~80% of Python dispatch overhead while keeping the dynamic-shape
|
||||
attention path eager. Phase 2 will add paged KV for full capture.
|
||||
|
||||
Cross-GPU transfers (X.to(cuda:N)) happen OUTSIDE graphs between layers.
|
||||
The hc_head + norm + lm_head are captured as a separate graph on cuda:0.
|
||||
|
||||
Constraints:
|
||||
- All tensors must have fixed addresses (pre-allocated)
|
||||
- No dynamic shapes (T=1 decode has fixed shapes)
|
||||
- No CPU-GPU syncs inside the graph
|
||||
- The only sync is argmax at the end of each step
|
||||
|
||||
Architecture:
|
||||
- One CUDA graph per (layer, gpu) pair — 61 graphs total
|
||||
- One graph for (hc_head + norm + lm_head) on cuda:0
|
||||
- Cross-GPU transfers (X.to(cuda:N)) happen outside graphs
|
||||
- The warmup step also computes and fixes gsa values
|
||||
- The only per-step sync is argmax for sampling (outside graph)
|
||||
"""
|
||||
|
||||
def __init__(self, n_layers, num_gpus, devices):
|
||||
def __init__(self, n_layers, num_gpus, hidden_size, devices):
|
||||
self.n_layers = n_layers
|
||||
self.num_gpus = num_gpus
|
||||
self.hidden_size = hidden_size
|
||||
self.devices = devices
|
||||
self.graphs = {} # (li) -> torch.cuda.CUDAGraph
|
||||
self.lm_graph = None # single graph for hc_head + norm + lm_head
|
||||
self.captured = False
|
||||
|
||||
# Sub-graphs: 2 per layer (A = pre-attention, B = post-attention + FFN)
|
||||
self.graphs_a = {} # li -> torch.cuda.CUDAGraph (pre-attention compute)
|
||||
self.graphs_b = {} # li -> torch.cuda.CUDAGraph (post-attention + FFN compute)
|
||||
self.lm_graph = None # single graph for hc_head + norm + lm_head on cuda:0
|
||||
|
||||
# Pre-allocated I/O buffers — fixed addresses for graph capture
|
||||
# Each layer reads X_in and writes X_out
|
||||
self.x_in_bufs = {} # li -> tensor on device of layer li
|
||||
self.x_out_bufs = {} # li -> tensor on device of layer li
|
||||
self.logits_buf = None # (1, 129280) on cuda:0
|
||||
# Sub-graph A I/O (on layer's device)
|
||||
self.x_in_a = {} # li -> (1, 4, H) BF16 — input X_l to both sub-graphs
|
||||
self.q_heads_buf = {} # li -> (1, n_h, hd) BF16 — Q heads after RoPE
|
||||
self.kv_roped_buf = {} # li -> (1, hd) BF16 — KV after RoPE (for KV append)
|
||||
self.q_a_buf = {} # li -> (1, 1536) BF16 — q_a output (for indexer)
|
||||
self.x_normed_buf = {} # li -> (1, H) BF16 — normalized input (for compressor)
|
||||
|
||||
# Eager → sub-graph B I/O (on layer's device)
|
||||
self.attn_out_buf = {} # li -> (1, n_h, hd) BF16 — FMHA output after inverse RoPE
|
||||
|
||||
# Sub-graph B output (on layer's device)
|
||||
self.x_next_buf = {} # li -> (1, 4, H) BF16 — output X_next
|
||||
|
||||
# lm_head graph buffers (on cuda:0)
|
||||
self.x_lm_in = None # (1, 4, H) BF16 on cuda:0
|
||||
self.logits_buf = None # (1, vocab_size) BF16 on cuda:0
|
||||
|
||||
def pre_allocate(self, cfg, attn_mhcs, ffn_mhcs, attn_norms, ffn_norms,
|
||||
kv_caches, compressors, indexers, moe_runners, se_runners,
|
||||
routers, prod_lins, layer_w, rope_caches, hc_head,
|
||||
final_norm_w, lm_head_lin, comp_rope_caches=None):
|
||||
def pre_allocate(self, cfg):
|
||||
"""Pre-allocate all I/O buffers with fixed addresses."""
|
||||
H = self.hidden_size
|
||||
n_h = cfg["num_attention_heads"]
|
||||
hd = cfg["head_dim"]
|
||||
V = cfg.get("vocab_size", 129280)
|
||||
q_a_dim = 1536 # q_a_proj output dimension
|
||||
|
||||
for li in range(self.n_layers):
|
||||
dev = self.devices[li % self.num_gpus]
|
||||
# X is (1, 4, 7168) BF16
|
||||
self.x_in_bufs[li] = torch.zeros(1, 4, cfg["hidden_size"], dtype=torch.bfloat16, device=dev)
|
||||
self.x_out_bufs[li] = torch.zeros(1, 4, cfg["hidden_size"], dtype=torch.bfloat16, device=dev)
|
||||
self.logits_buf = torch.zeros(1, cfg.get("vocab_size", 129280), dtype=torch.bfloat16, device='cuda:0')
|
||||
|
||||
# Sub-graph A I/O
|
||||
self.x_in_a[li] = torch.zeros(1, 4, H, dtype=torch.bfloat16, device=dev)
|
||||
self.q_heads_buf[li] = torch.zeros(1, n_h, hd, dtype=torch.bfloat16, device=dev)
|
||||
self.kv_roped_buf[li] = torch.zeros(1, hd, dtype=torch.bfloat16, device=dev)
|
||||
self.q_a_buf[li] = torch.zeros(1, q_a_dim, dtype=torch.bfloat16, device=dev)
|
||||
self.x_normed_buf[li] = torch.zeros(1, H, dtype=torch.bfloat16, device=dev)
|
||||
|
||||
# Eager → sub-graph B
|
||||
self.attn_out_buf[li] = torch.zeros(1, n_h, hd, dtype=torch.bfloat16, device=dev)
|
||||
|
||||
# Sub-graph B output
|
||||
self.x_next_buf[li] = torch.zeros(1, 4, H, dtype=torch.bfloat16, device=dev)
|
||||
|
||||
# lm_head graph I/O (cuda:0 only)
|
||||
self.x_lm_in = torch.zeros(1, 4, H, dtype=torch.bfloat16, device='cuda:0')
|
||||
self.logits_buf = torch.zeros(1, V, dtype=torch.bfloat16, device='cuda:0')
|
||||
|
||||
def capture(self, cfg, attn_mhcs, ffn_mhcs, attn_norms, ffn_norms,
|
||||
kv_caches, compressors, indexers, moe_runners, se_runners,
|
||||
routers, prod_lins, layer_w, rope_caches, hc_head,
|
||||
final_norm_w, lm_head_lin, positions, token_id, comp_rope_caches=None):
|
||||
final_norm_w, lm_w, dec_pos_per_gpu, dec_tid32_per_gpu, comp_rope_caches=None):
|
||||
"""Capture CUDA graphs for all layers + lm_head.
|
||||
|
||||
Must be called after one warmup step so that:
|
||||
@@ -188,53 +231,134 @@ class CUDAGraphDecoder:
|
||||
2. gsa values are fixed (from warmup_gsa)
|
||||
3. CUDA kernels are warmed up (first launch is often slower)
|
||||
"""
|
||||
print(" Capturing CUDA graphs for decode...", flush=True)
|
||||
from dsv4.ops.quantize import (
|
||||
rmsnorm_quantize_nvfp4, mhc_rmsnorm_quantize_nvfp4,
|
||||
dequantize_nvfp4,
|
||||
)
|
||||
from dsv4.layers.mhc import mHCContext
|
||||
|
||||
# Capture each layer as a separate graph
|
||||
H = self.hidden_size
|
||||
n_h = cfg["num_attention_heads"]
|
||||
hd = cfg["head_dim"]
|
||||
rd = cfg.get("qk_rope_head_dim", 64)
|
||||
|
||||
print(" Capturing CUDA graphs for decode (eager-break-at-attention)...", flush=True)
|
||||
|
||||
# Capture each layer's two sub-graphs
|
||||
for li in range(self.n_layers):
|
||||
gpu = li % self.num_gpus
|
||||
dev = self.devices[gpu]
|
||||
torch.cuda.set_device(gpu)
|
||||
|
||||
# Copy current X into the fixed input buffer
|
||||
# (In practice, the warmup step's X is already on the right device)
|
||||
attn_mhc = attn_mhcs.get(li)
|
||||
ffn_mhc = ffn_mhcs.get(li)
|
||||
attn_norm_w = attn_norms.get(li)
|
||||
ffn_norm_w = ffn_norms.get(li)
|
||||
pl = prod_lins.get(li)
|
||||
w = layer_w[li]
|
||||
pfx = f"model.layers.{li}.self_attn"
|
||||
rope_cos, rope_sin = rope_caches[gpu]
|
||||
router = routers.get(li)
|
||||
moe = moe_runners.get(li)
|
||||
se = se_runners.get(li)
|
||||
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph):
|
||||
X_out = forward_layer(
|
||||
self.x_in_bufs[li], layer_w[li], li, cfg, *rope_caches[gpu],
|
||||
attn_mhcs.get(li), ffn_mhcs.get(li),
|
||||
attn_norms.get(li), ffn_norms.get(li),
|
||||
kv_caches[li], positions, token_id,
|
||||
compressors.get(li), indexers.get(li),
|
||||
moe_runners.get(li), se_runners.get(li), routers.get(li),
|
||||
prod_lin=prod_lins.get(li),
|
||||
_use_fused_rmsnorm_quantize=True,
|
||||
comp_rope_cos=comp_rope_caches[gpu][0] if comp_rope_caches else None,
|
||||
comp_rope_sin=comp_rope_caches[gpu][1] if comp_rope_caches else None,
|
||||
)
|
||||
# Copy output to fixed buffer
|
||||
self.x_out_bufs[li].copy_(X_out)
|
||||
# ---- Sub-graph A: mHC pre + rmsnorm + Q/KV projections ----
|
||||
graph_a = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph_a):
|
||||
X_l = self.x_in_a[li]
|
||||
|
||||
# mHC pre_block + rmsnorm + NVFP4 quantize (fused P5 path)
|
||||
A_l_a, B_l_a, C_l_a = attn_mhc._dynamic_params(X_l)
|
||||
x_quant_attn = mhc_rmsnorm_quantize_nvfp4(
|
||||
X_l, A_l_a, attn_norm_w.to(dev, torch.float32))
|
||||
x_normed = dequantize_nvfp4(
|
||||
x_quant_attn.x_fp4, x_quant_attn.x_sf, x_quant_attn.gsa)
|
||||
|
||||
# Q projections: q_a → q_a_norm → q_b → q_b_norm
|
||||
q_a = pl['q_a'].run_from_quantized(x_quant_attn)
|
||||
q_norm_w = w.get(f"{pfx}.q_a_norm.weight")
|
||||
if q_norm_w is not None:
|
||||
q_a_quant = rmsnorm_quantize_nvfp4(q_a, q_norm_w.to(dev, torch.float32))
|
||||
q_a_deq = dequantize_nvfp4(q_a_quant.x_fp4, q_a_quant.x_sf, q_a_quant.gsa)
|
||||
q = pl['q_b'].run_from_quantized(q_a_quant)
|
||||
else:
|
||||
q_a_deq = q_a
|
||||
q = pl['q_b'](q_a)
|
||||
q = unweighted_rmsnorm(q).bfloat16()
|
||||
q_heads = q.reshape(1, n_h, hd)
|
||||
q_heads = _apply_rope(q_heads, dec_pos_per_gpu[gpu], rope_cos, rope_sin, rd)
|
||||
|
||||
# KV projection
|
||||
kv = pl['kv'].run_from_quantized(x_quant_attn)
|
||||
kv_norm_w = w.get(f"{pfx}.kv_norm.weight")
|
||||
if kv_norm_w is not None:
|
||||
kv = rmsnorm(kv, kv_norm_w.to(dev, torch.float32))
|
||||
kv_3d = kv.reshape(1, 1, hd)
|
||||
kv_3d = _apply_rope(kv_3d, dec_pos_per_gpu[gpu], rope_cos, rope_sin, rd)
|
||||
kv_roped = kv_3d.reshape(1, hd)
|
||||
|
||||
# Write to output buffers (fixed-address copies for graph output)
|
||||
self.q_heads_buf[li].copy_(q_heads)
|
||||
self.kv_roped_buf[li].copy_(kv_roped)
|
||||
self.q_a_buf[li].copy_(q_a_deq if q_norm_w is not None else q_a)
|
||||
self.x_normed_buf[li].copy_(x_normed)
|
||||
|
||||
self.graphs_a[li] = graph_a
|
||||
|
||||
# ---- Sub-graph B: o_proj → mHC post(attn) → FFN → mHC post(FFN) ----
|
||||
graph_b = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph_b):
|
||||
X_l = self.x_in_a[li] # same X_l (both sub-graphs share this buffer)
|
||||
attn_out = self.attn_out_buf[li] # set by eager section (FMHA + inv RoPE)
|
||||
|
||||
# Output projection: attn_out → o_a (grouped) → o_b
|
||||
wo_a_lin = pl.get('o_a')
|
||||
o_groups = cfg.get('o_groups', 16)
|
||||
o_rank = cfg.get('o_lora_rank', 1024)
|
||||
if wo_a_lin is not None:
|
||||
g_3d = wo_a_lin.run(attn_out) # (1, n_groups, o_rank)
|
||||
g_flat = g_3d.reshape(1, -1) # (1, n_groups * o_rank)
|
||||
F_attn = pl['o_b'](g_flat)
|
||||
else:
|
||||
F_attn = torch.zeros(1, H, dtype=torch.bfloat16, device=dev)
|
||||
|
||||
# mHC post_block (attention)
|
||||
A_l_a2, B_l_a2, C_l_a2 = attn_mhc._dynamic_params(X_l)
|
||||
ctx_a = mHCContext(B_l=B_l_a2, C_l=C_l_a2)
|
||||
X_mid = attn_mhc.post_block(X_l, F_attn, ctx_a)
|
||||
|
||||
# mHC pre_block (FFN) + rmsnorm + NVFP4 quantize
|
||||
A_l_f, B_l_f, C_l_f = ffn_mhc._dynamic_params(X_mid)
|
||||
x_quant_ffn = mhc_rmsnorm_quantize_nvfp4(
|
||||
X_mid, A_l_f, ffn_norm_w.to(dev, torch.float32))
|
||||
x_ffn = dequantize_nvfp4(
|
||||
x_quant_ffn.x_fp4, x_quant_ffn.x_sf, x_quant_ffn.gsa)
|
||||
|
||||
# Router + MoE + SE
|
||||
F_ffn = moe_forward(x_ffn, li, moe, se, router, dec_tid32_per_gpu[gpu])
|
||||
|
||||
# mHC post_block (FFN)
|
||||
ctx_f = mHCContext(B_l=B_l_f, C_l=C_l_f)
|
||||
X_next = ffn_mhc.post_block(X_mid, F_ffn, ctx_f)
|
||||
self.x_next_buf[li].copy_(X_next)
|
||||
|
||||
self.graphs_b[li] = graph_b
|
||||
|
||||
self.graphs[li] = graph
|
||||
if (li + 1) % 10 == 0:
|
||||
print(f" Captured {li+1}/{self.n_layers} layer graphs", flush=True)
|
||||
print(f" Captured {li+1}/{self.n_layers} layer graphs (A+B)", flush=True)
|
||||
|
||||
# Capture hc_head + norm + lm_head on cuda:0
|
||||
# ---- Capture hc_head + norm + lm_head on cuda:0 ----
|
||||
torch.cuda.set_device(0)
|
||||
self.lm_graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(self.lm_graph):
|
||||
# Note: x_in_bufs for the last layer is on the last layer's device.
|
||||
# For the lm_head graph, we need the X on cuda:0.
|
||||
# We'll handle the cross-GPU transfer outside the graph.
|
||||
x_out = self.x_out_bufs[self.n_layers - 1] # may be on different GPU
|
||||
x_cuda0 = x_out.to('cuda:0') # This may NOT work in a CUDA graph
|
||||
# Actually, cross-device memcpy in CUDA graphs is not supported.
|
||||
# We need to do the transfer outside and use a cuda:0 buffer.
|
||||
pass # Will handle this differently
|
||||
x_out = hc_head.forward(self.x_lm_in) if hc_head is not None else self.x_lm_in[:, 0, :]
|
||||
if final_norm_w is not None:
|
||||
x_out = rmsnorm(x_out, final_norm_w)
|
||||
logits = torch.nn.functional.linear(x_out, lm_w)
|
||||
self.logits_buf.copy_(logits)
|
||||
|
||||
self.captured = True
|
||||
print(f" Captured {len(self.graphs)} layer graphs", flush=True)
|
||||
print(f" Captured {len(self.graphs_a)} sub-graph A + {len(self.graphs_b)} sub-graph B + lm_head", flush=True)
|
||||
# =====================================================================
|
||||
def dequant_nvfp4(weight, weight_scale, weight_scale_2=None, input_scale=None):
|
||||
O, I2 = weight.shape; I = I2 * 2
|
||||
@@ -797,6 +921,81 @@ def _run_production_fmha_mixed(q_heads, kv_nope_fp8, kv_nope_scale, kv_rope_bf16
|
||||
# =====================================================================
|
||||
# Attention — ALL production kernels
|
||||
# =====================================================================
|
||||
def eager_attention(q_heads, kv_roped, x_normed, q_a, w, li, cfg,
|
||||
rope_cos, rope_sin, kv_cache, positions,
|
||||
compressor, indexer, comp_rope_cos=None, comp_rope_sin=None):
|
||||
"""Eager attention section — runs OUTSIDE CUDA graph capture.
|
||||
|
||||
This function handles the dynamic-shape parts of attention:
|
||||
KV append → Compressor → Indexer → KV gather → FMHA → Inverse RoPE
|
||||
|
||||
Returns: attn_out (1, n_h, hd) — output of FMHA after inverse RoPE.
|
||||
The caller (sub-graph B) will apply o_proj and mHC post_block.
|
||||
"""
|
||||
dev = x_normed.device; T = q_heads.shape[0]
|
||||
n_h = cfg["num_attention_heads"]; hd = cfg["head_dim"]; rd = cfg.get("qk_rope_head_dim", 64)
|
||||
ratio = compressor.ratio if compressor is not None else 0
|
||||
scale = 1.0 / math.sqrt(hd); pfx = f"model.layers.{li}.self_attn"
|
||||
nope_dim = hd - rd
|
||||
if positions.device != rope_cos.device: positions = positions.to(rope_cos.device)
|
||||
|
||||
# KV append (already roped from sub-graph A)
|
||||
kv_cache.append_swa(kv_roped, positions)
|
||||
|
||||
# Compressor → compressed KV (mixed storage: FP8 + BF16 RoPE)
|
||||
comp_pos, block_bias = None, None; comp_idx_kv = None
|
||||
if compressor is not None and compressor.ratio > 0:
|
||||
comp_kv_fp32, comp_pos, block_bias = compressor.forward(x_normed, positions)
|
||||
if comp_kv_fp32 is not None:
|
||||
from dsv4.kernels.cuda.loader import get_cuda_module
|
||||
kv_mod = get_cuda_module("kv_quantize", ["kv_quantize.cu"])
|
||||
nope_fp32 = comp_kv_fp32[:, :nope_dim].contiguous()
|
||||
rope_bf16 = comp_kv_fp32[:, nope_dim:].bfloat16().contiguous()
|
||||
rope_3d = rope_bf16.unsqueeze(1)
|
||||
crc = comp_rope_cos if comp_rope_cos is not None else rope_cos
|
||||
crs = comp_rope_sin if comp_rope_sin is not None else rope_sin
|
||||
rope_3d = _apply_rope(rope_3d, comp_pos, crc, crs, rd)
|
||||
rope_bf16 = rope_3d.squeeze(1)
|
||||
nope_fp8, nope_scale = kv_mod.quantize_fp8_e4m3_from_fp32(nope_fp32)
|
||||
kv_cache.set_compressed_mixed(nope_fp8, nope_scale, rope_bf16, comp_pos)
|
||||
if compressor.is_csa and indexer is not None and indexer.compressor is not None:
|
||||
comp_idx_kv, _, _ = indexer.compressor.forward(x_normed, positions)
|
||||
kv_cache.set_indexer_keys_fp8(comp_idx_kv)
|
||||
|
||||
# Indexer top-k (CSA)
|
||||
topk_idx = None
|
||||
if indexer is not None and ratio == 4:
|
||||
topk_idx = indexer.forward(q_a, x_normed, kv_cache, positions, layer_idx=li)
|
||||
|
||||
# Gather KV — B1 storage-native mixed path
|
||||
swa_kv, _swa_pos = kv_cache.get_swa()
|
||||
swa_len = swa_kv.shape[0]
|
||||
if kv_cache.n_comp > 0:
|
||||
if ratio == 4:
|
||||
assert topk_idx is not None, f"CSA layer {li}: indexer returned no top-k"
|
||||
tk = topk_idx[0].clamp(0, kv_cache.n_comp - 1).int()
|
||||
kv_nope_fp8, kv_nope_scale, kv_rope_bf16 = kv_cache.gather_mixed_selective(tk)
|
||||
elif ratio > 4:
|
||||
kv_nope_fp8, kv_nope_scale, kv_rope_bf16 = kv_cache.gather_mixed_all()
|
||||
else:
|
||||
kv_nope_fp8, kv_nope_scale, kv_rope_bf16 = kv_cache.gather_mixed_swa_only()
|
||||
else:
|
||||
kv_nope_fp8, kv_nope_scale, kv_rope_bf16 = kv_cache.gather_mixed_swa_only()
|
||||
seq_len = kv_nope_scale.shape[0]
|
||||
if seq_len == 0:
|
||||
return torch.zeros(T, n_h, hd, dtype=torch.bfloat16, device=dev)
|
||||
|
||||
# Production FMHA — B1 mixed FP8/BF16 decode path
|
||||
attn_out = _run_production_fmha_mixed(
|
||||
q_heads, kv_nope_fp8, kv_nope_scale, kv_rope_bf16,
|
||||
n_h, hd, T, seq_len, scale, dev, li, w, pfx, rd)
|
||||
|
||||
# Inverse RoPE
|
||||
attn_out = _apply_rope(attn_out, positions, rope_cos, rope_sin, rd, inverse=True)
|
||||
|
||||
return attn_out
|
||||
|
||||
|
||||
def forward_attention(x_normed, w, li, cfg, rope_cos, rope_sin,
|
||||
kv_cache, positions, compressor, indexer, prod_lin,
|
||||
x_quant=None,
|
||||
@@ -1549,6 +1748,10 @@ def main():
|
||||
dec_tid_buf = torch.zeros(1, dtype=torch.long, device='cuda:0')
|
||||
dec_pos_buf = torch.zeros(1, dtype=torch.long, device='cuda:0')
|
||||
dec_tid32_buf = torch.zeros(1, dtype=torch.int32, device='cuda:0')
|
||||
# Per-GPU token ID buffers — each GPU needs its own copy for graph capture
|
||||
# (cross-device .to() inside a CUDA graph is not reliable)
|
||||
dec_tid32_per_gpu = {g: torch.zeros(1, dtype=torch.int32, device=f'cuda:{g}') for g in range(NUM_GPUS)}
|
||||
dec_pos_per_gpu = {g: torch.zeros(1, dtype=torch.long, device=f'cuda:{g}') for g in range(NUM_GPUS)}
|
||||
|
||||
# Decode
|
||||
print(f"\nDecoding (max {MAX_NEW_TOKENS} tokens)...")
|
||||
@@ -1579,47 +1782,96 @@ def main():
|
||||
# Pre-allocate decode X buffer — zero per-step allocation
|
||||
# init_state writes to this buffer in-place (no .clone() allocation)
|
||||
dec_X_buf = torch.zeros(1, 4, H, dtype=torch.bfloat16, device='cuda:0')
|
||||
# Pre-allocate embedding output buffer — embed() returns a new tensor each call.
|
||||
# For graph capture, we'd copy into this buffer. For now, used as reference.
|
||||
dec_embed_buf = torch.zeros(1, H, dtype=torch.bfloat16, device='cuda:0')
|
||||
# Pre-allocate pinned CPU buffer for token ID transfer (graph-capturable)
|
||||
# Writing a Python int to a GPU tensor causes CPU→GPU sync. Instead:
|
||||
# 1. Write to pinned CPU buffer (no sync)
|
||||
# 2. copy_ to GPU buffer (async, graph-capturable)
|
||||
dec_tid_pinned = torch.zeros(1, dtype=torch.long, device='cpu').pin_memory()
|
||||
dec_tid32_pinned = torch.zeros(1, dtype=torch.int32, device='cpu').pin_memory()
|
||||
dec_pos_pinned = torch.zeros(1, dtype=torch.long, device='cpu').pin_memory()
|
||||
|
||||
# ---- CUDA Graph Setup ----
|
||||
graph_decoder = None
|
||||
if _args.cuda_graph:
|
||||
print(" CUDA graph capture requested — will capture after warmup step")
|
||||
graph_decoder = CUDAGraphDecoder(n_layers, NUM_GPUS, H, [f'cuda:{g}' for g in range(NUM_GPUS)])
|
||||
graph_decoder.pre_allocate(cfg)
|
||||
|
||||
for step in range(MAX_NEW_TOKENS):
|
||||
t1 = time.time()
|
||||
# Write token/position to pinned CPU buffers, then async copy to GPU
|
||||
# This avoids the CPU→GPU sync from dec_tid_buf[0] = python_int
|
||||
dec_tid_pinned[0] = all_tokens[-1]
|
||||
dec_tid_buf.copy_(dec_tid_pinned)
|
||||
dec_tid32_pinned[0] = all_tokens[-1]
|
||||
dec_tid32_buf.copy_(dec_tid32_pinned)
|
||||
dec_pos_pinned[0] = len(all_tokens) - 1
|
||||
dec_pos_buf.copy_(dec_pos_pinned)
|
||||
# Copy token/position to per-GPU buffers for graph capture
|
||||
for g in range(NUM_GPUS):
|
||||
dec_tid32_per_gpu[g].copy_(dec_tid32_pinned)
|
||||
dec_pos_per_gpu[g].copy_(dec_pos_pinned)
|
||||
|
||||
t_e = time.perf_counter()
|
||||
X = mHCLayer.init_state(embed(dec_tid_buf), out_buf=dec_X_buf)
|
||||
for li in range(n_layers):
|
||||
gpu = li % NUM_GPUS
|
||||
if X.device != torch.device(f"cuda:{gpu}"): X = X.to(f"cuda:{gpu}")
|
||||
torch.cuda.set_device(gpu)
|
||||
X = forward_layer(X, layer_w[li], li, cfg, *rope_caches[gpu],
|
||||
attn_mhcs.get(li), ffn_mhcs.get(li),
|
||||
attn_norms.get(li), ffn_norms.get(li),
|
||||
kv_caches[li], dec_pos_buf, dec_tid32_buf,
|
||||
compressors.get(li), indexers.get(li),
|
||||
moe_runners.get(li), se_runners.get(li), routers.get(li),
|
||||
prod_lin=prod_lins.get(li),
|
||||
_profile_detail=(profile and step == 1),
|
||||
_profile_times=cuda_layer_events if (profile and step == 1) else None,
|
||||
_use_fused_rmsnorm_quantize=not _args.no_fused_rmsnorm,
|
||||
comp_rope_cos=comp_rope_caches[gpu][0], comp_rope_sin=comp_rope_caches[gpu][1],
|
||||
)
|
||||
X = X.to('cuda:0'); torch.cuda.set_device(0)
|
||||
|
||||
# ---- Forward: graph replay or eager ----
|
||||
if graph_decoder is not None and graph_decoder.captured:
|
||||
# CUDA graph replay path
|
||||
for li in range(n_layers):
|
||||
gpu = li % NUM_GPUS
|
||||
torch.cuda.set_device(gpu)
|
||||
|
||||
# Copy X into graph input buffer (copy_ handles cross-GPU transfer)
|
||||
graph_decoder.x_in_a[li].copy_(X)
|
||||
|
||||
# Replay sub-graph A (mHC pre + rmsnorm + Q/KV projections)
|
||||
graph_decoder.graphs_a[li].replay()
|
||||
|
||||
# Eager attention section (dynamic shapes)
|
||||
attn_out = eager_attention(
|
||||
graph_decoder.q_heads_buf[li],
|
||||
graph_decoder.kv_roped_buf[li],
|
||||
graph_decoder.x_normed_buf[li],
|
||||
graph_decoder.q_a_buf[li],
|
||||
layer_w[li], li, cfg, *rope_caches[gpu],
|
||||
kv_caches[li], dec_pos_per_gpu[gpu],
|
||||
compressors.get(li), indexers.get(li),
|
||||
comp_rope_cos=comp_rope_caches[gpu][0] if comp_rope_caches else None,
|
||||
comp_rope_sin=comp_rope_caches[gpu][1] if comp_rope_caches else None,
|
||||
)
|
||||
# Copy attn_out into graph buffer for sub-graph B
|
||||
graph_decoder.attn_out_buf[li].copy_(attn_out)
|
||||
|
||||
# Replay sub-graph B (o_proj + mHC post + FFN)
|
||||
graph_decoder.graphs_b[li].replay()
|
||||
|
||||
# Read output from sub-graph B (no clone — buffer is stable until next step)
|
||||
X = graph_decoder.x_next_buf[li]
|
||||
|
||||
# Transfer last layer output to cuda:0 for lm_head graph
|
||||
graph_decoder.x_lm_in.copy_(X) # cross-GPU copy if needed
|
||||
|
||||
# lm_head graph replay
|
||||
graph_decoder.lm_graph.replay()
|
||||
logits = graph_decoder.logits_buf
|
||||
|
||||
else:
|
||||
# Eager forward path (warmup or no --cuda-graph)
|
||||
for li in range(n_layers):
|
||||
gpu = li % NUM_GPUS
|
||||
if X.device != torch.device(f"cuda:{gpu}"): X = X.to(f"cuda:{gpu}")
|
||||
torch.cuda.set_device(gpu)
|
||||
X = forward_layer(X, layer_w[li], li, cfg, *rope_caches[gpu],
|
||||
attn_mhcs.get(li), ffn_mhcs.get(li),
|
||||
attn_norms.get(li), ffn_norms.get(li),
|
||||
kv_caches[li], dec_pos_buf, dec_tid32_buf,
|
||||
compressors.get(li), indexers.get(li),
|
||||
moe_runners.get(li), se_runners.get(li), routers.get(li),
|
||||
prod_lin=prod_lins.get(li),
|
||||
_profile_detail=(profile and step == 1),
|
||||
_profile_times=cuda_layer_events if (profile and step == 1) else None,
|
||||
_use_fused_rmsnorm_quantize=not _args.no_fused_rmsnorm,
|
||||
comp_rope_cos=comp_rope_caches[gpu][0], comp_rope_sin=comp_rope_caches[gpu][1],
|
||||
)
|
||||
X = X.to('cuda:0'); torch.cuda.set_device(0)
|
||||
t_layers = time.perf_counter()
|
||||
|
||||
# After first decode step: fix gsa values from runtime amax
|
||||
@@ -1654,9 +1906,28 @@ def main():
|
||||
lm_head_lin._use_runtime_gsa = False
|
||||
n_fixed += 1
|
||||
print(f" Warmup gsa: fixed {n_fixed} projection gsa values from step 0 (MoE/SE keep runtime gsa)", flush=True)
|
||||
x_out = hc_head.forward(X) if hc_head is not None else X[:, 0, :]
|
||||
if final_norm_w is not None: x_out = rmsnorm(x_out, final_norm_w)
|
||||
logits = torch.nn.functional.linear(x_out, lm_w) if lm_head_lin is None else lm_head_lin(x_out)
|
||||
|
||||
# ---- lm_head: graph replay or eager ----
|
||||
if graph_decoder is not None and graph_decoder.captured:
|
||||
# logits already computed by lm_head graph replay above
|
||||
pass
|
||||
else:
|
||||
x_out = hc_head.forward(X) if hc_head is not None else X[:, 0, :]
|
||||
if final_norm_w is not None: x_out = rmsnorm(x_out, final_norm_w)
|
||||
logits = torch.nn.functional.linear(x_out, lm_w) if lm_head_lin is None else lm_head_lin(x_out)
|
||||
|
||||
# ---- CUDA graph capture after warmup ----
|
||||
if graph_decoder is not None and not graph_decoder.captured and step == 0:
|
||||
print(" Step 0 warmup done. Capturing CUDA graphs...", flush=True)
|
||||
torch.cuda.synchronize()
|
||||
graph_decoder.capture(
|
||||
cfg, attn_mhcs, ffn_mhcs, attn_norms, ffn_norms,
|
||||
kv_caches, compressors, indexers, moe_runners, se_runners,
|
||||
routers, prod_lins, layer_w, rope_caches, hc_head,
|
||||
final_norm_w, lm_w, dec_pos_per_gpu, dec_tid32_per_gpu,
|
||||
comp_rope_caches=comp_rope_caches,
|
||||
)
|
||||
print(f" CUDA graphs captured. Graph replay starts on step 1.", flush=True)
|
||||
if profile: torch.cuda.synchronize()
|
||||
t_lm = time.perf_counter()
|
||||
# Check thinking start token logit on first step
|
||||
|
||||
Reference in New Issue
Block a user