Fix OOB in scale assembly: size padded_x_sf for max tokens, fix top_k/max_num_tokens passing, support variable-size expert blocks

Bug 9: padded_x_sf was sized for num_experts*128 rows, but with 8192 tokens
and top_k=6, the actual padded row count can exceed 6144. Also:
- Pass top_k and max_num_tokens from deepseek_v4.py (was defaulting to 8/8192)
- Phase 2 of scale assembly now handles experts with >128 tokens (multiple 128-row chunks)
- Remove debug prints
This commit is contained in:
2026-05-17 09:56:28 +00:00
parent 55ac60eb91
commit d9bae6d770
2 changed files with 29 additions and 40 deletions

View File

@@ -90,9 +90,6 @@ class CuTeDSLMoERunner:
def _allocate_buffers(self):
"""Pre-allocate scale buffers at max size for cudagraph compatibility."""
K_sf = cutedsl_ceil_div(self.hidden_size, 16)
padded_cols = cutedsl_ceil_div(K_sf, 4) * 4
# Per-expert scale buffers: separate L1/L2 since K_sf differs
K_sf_l1 = cutedsl_ceil_div(self.hidden_size, 16)
padded_cols_l1 = cutedsl_ceil_div(K_sf_l1, 4) * 4
@@ -108,12 +105,15 @@ class CuTeDSLMoERunner:
for _ in range(self.num_experts)
]
# Padded x_sf buffers: num_experts * 128 rows, separate L1/L2
# Padded x_sf buffers for Phase 1 scatter.
# Sized for max_num_tokens * top_k rows (worst case: all tokens in one expert,
# padded to 128). This is larger than num_experts * 128 when tokens >> experts.
max_rows = cutedsl_ceil_div(self.max_num_tokens * self.top_k, 128) * 128
self._padded_x_sf_buf_l1 = torch.zeros(
self.num_experts * 128, padded_cols_l1, dtype=torch.float16, device=self.device
max_rows, padded_cols_l1, dtype=torch.float16, device=self.device
).to(torch.float8_e4m3fn)
self._padded_x_sf_buf_l2 = torch.zeros(
self.num_experts * 128, padded_cols_l2, dtype=torch.float16, device=self.device
max_rows, padded_cols_l2, dtype=torch.float16, device=self.device
).to(torch.float8_e4m3fn)
self._buffers_allocated = True
@@ -149,10 +149,6 @@ class CuTeDSLMoERunner:
self.max_num_tokens * self.top_k, dtype=torch.int32, device=self.device
)
self._fill_token_indices()
# Verify the fill worked (CuTeDSL JIT may have corrupted GPU memory)
torch.cuda.synchronize()
assert self._token_indices.max().item() == self.max_num_tokens - 1, \
f"token_indices corrupted after fill! max={self._token_indices.max().item()}"
self._needs_token_refill = True # GEMM JIT may corrupt; refill after first run
self._expert_id_range = torch.arange(
@@ -192,18 +188,14 @@ class CuTeDSLMoERunner:
padded_x_sf_buf, per_expert_bufs):
"""Assemble 2D-side activation scales (cudagraph-safe, no CPU sync).
Two-phase approach:
1. Scatter x_sf rows into 128-aligned positions in padded_x_sf (GPU-only)
2. Per-expert: copy 128 rows from padded_x_sf, swizzle, accumulate
Phase 1: Scatter x_sf rows into 128-aligned positions in padded_x_sf.
Phase 2: Per-expert, copy from padded_x_sf at the right offset,
pad_and_swizzle, then concatenate.
All operations are fixed-shape. No .item(), no .tolist(), no variable
slicing with GPU scalars.
The output has sum(padded_rows_per_expert) rows (variable per expert).
"""
num_experts = self.num_experts
K_sf = x_sf.shape[1]
# ── Phase 1: Scatter x_sf into 128-aligned padded buffer ──
# Use GPU-computed indices (same as original _assemble_scales_cudagraph_safe)
padded_x_sf = padded_x_sf_buf
padded_x_sf.zero_()
@@ -221,21 +213,29 @@ class CuTeDSLMoERunner:
dst_rows = padded_expert_offsets[expert_assign] + local_row
padded_x_sf[dst_rows, :K_sf] = x_sf
# ── Phase 2: Per-expert swizzle ──
# Each expert gets its own 128-row buf, copied from padded_x_sf at
# the 128-aligned offset, then swizzled independently
# Phase 2: Per-expert swizzle
swizzled_parts = []
for e in range(num_experts):
n_padded = padded_rows_per_expert[e].item()
if n_padded == 0:
continue
start = padded_expert_offsets[e].item()
buf = per_expert_bufs[e]
buf.zero_()
buf[:, :K_sf] = padded_x_sf[e * 128:e * 128 + 128]
swizzled = pad_and_swizzle_single(buf)
swizzled_parts.append(swizzled)
# buf is only 128 rows; process in 128-row chunks
offset = start
remaining = n_padded
while remaining > 0:
chunk = min(remaining, 128)
buf.zero_()
buf[:chunk, :K_sf] = padded_x_sf[offset:offset + chunk]
swizzled = pad_and_swizzle_single(buf)
swizzled_parts.append(swizzled)
offset += chunk
remaining -= chunk
# Concatenate all expert blocks
all_flat = torch.cat([p.view(torch.uint8) for p in swizzled_parts], dim=0)
all_flat = all_flat.view(torch.float8_e4m3fn)
return all_flat.reshape(num_experts * 128, -1)
return all_flat
def compute_activation_global_scales(self, hidden_states_sample, topk_weights, topk_ids):
"""Compute activation global scales from a warmup forward pass.
@@ -347,19 +347,6 @@ class CuTeDSLMoERunner:
expert_offsets[1:self.num_experts + 1] = tokens_per_expert.cumsum(0)
# -- Gather hidden states into slot order --
# DEBUG: Check for OOB indices before indexing
ti_max = self._token_indices[:num_slots].max().item()
ti_min = self._token_indices[:num_slots].min().item()
si_max = sorted_token_ids.max().item()
si_min = sorted_token_ids.min().item()
print(f"[CLAWMINE] num_tokens={num_tokens} top_k={top_k} num_slots={num_slots} "
f"token_indices max={ti_max} min={ti_min} "
f"sorted_token_ids max={si_max} min={si_min} "
f"experts_start_idx={self.experts_start_idx} num_experts={self.num_experts} "
f"topk_ids max={topk_ids.max().item()} min={topk_ids.min().item()}", flush=True)
if si_max >= num_tokens or si_min < 0:
print(f"[CLAWMINE BUG] sorted_token_ids OOB! "
f"max={si_max} min={si_min} num_tokens={num_tokens}", flush=True)
slot_hidden = hidden_states[sorted_token_ids]
# === L1: gate + up ===

View File

@@ -503,6 +503,8 @@ class DeepseekV4MegaMoEExperts(nn.Module):
num_experts=self.num_local_experts,
hidden_size=self.hidden_size,
intermediate_size=self.intermediate_size,
max_num_tokens=self.max_num_tokens,
top_k=self.top_k,
device=l1_fp4[0].device,
experts_start_idx=self.experts_start_idx,
)