Skip to content

fix: size MegaMoE full-pool MXFP8FP4 pull buffer - #82

Open
shyeh25 wants to merge 1 commit into
sgl-project:devfrom
shyeh25:fix/issue-37559-mxfp8fp4-full-pool-buffer
Open

fix: size MegaMoE full-pool MXFP8FP4 pull buffer#82
shyeh25 wants to merge 1 commit into
sgl-project:devfrom
shyeh25:fix/issue-37559-mxfp8fp4-full-pool-buffer

Conversation

@shyeh25

@shyeh25 shyeh25 commented Sep 4, 2026

Copy link
Copy Markdown

Summary

  • Keep a complete-token pull scratch slice when the SM100 MegaMoE MXFP8FP4
    kernel selects its one-shot full-pool path.
  • Preserve the existing 4 KiB chunking heuristic for reusable-ring and other
    non-full-pool paths.
  • Mirror the device full-pool predicate in the host configuration and add a
    compile-time assertion at the transfer site to prevent future host/device
    drift.

Related issue: sgl-project/sglang#37559

Root cause

The v0.1.7 host heuristic repeatedly halves num_bytes_per_pull until it is at
most 4 KiB. For DeepSeek-V4's 7,168-byte FP8 activation token, this produces a
3,584-byte per-warp scratch slice.

The retained MXFP8FP4 full-pool fast path does not consume that token in chunks:
it issues a one-shot TMA transfer for all 7,168 bytes. The transfer therefore
writes beyond the 3,584-byte slice and corrupts adjacent shared memory,
eventually surfacing as a CUDA illegal-memory-access failure under sustained
high-concurrency serving.

Validation

All GPU testing used Blackwell B300/GB300 systems.

Correctness and memory safety

  • Reproduced the native v0.1.7 MXFP8FP4/W4A8 illegal memory access on eight
    B300 GPUs.
  • The original W4A8 issue workload passed at concurrency 2,048, and an
    additional concurrency-4,096 stress run also passed.
  • The applicable installed-wheel sgl_deep_gemm/run_tests.sh attention and
    MegaMoE tests passed; documented architecture/upstream exclusions remained
    skips.
  • W4A8 full-pool and reusable-ring GSM8K-200 controls both scored 0.98 exact
    match.
  • Native and fixed W4A4 GSM8K-200 controls both scored 0.98, with identical
    normalized per-sample results.

No functional or accuracy regression was detected in the validation above.

Keep a complete-token pull buffer only when the SM100 MXFP8FP4 kernel selects the one-shot full-pool path. Preserve chunked pulls for all other paths and assert the full-token invariant at compile time.
const int num_max_pool_tokens = layout::get_num_max_pool_tokens(
num_ranks, num_max_tokens_per_rank, num_topk, num_experts_per_rank);
const bool use_full_pool_fp8_fp4_path =
mma_kind == MmaKind::MXFP8FP4 and

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Why doesn't other kinds of mma_kind have the same issue?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BF16 does not have this issue because it uses a separate kernel whose dispatch pull always transfers the token in kNumBytesPerPull chunks.

MXFP4/NVFP4 share this kernel, but their activations are packed FP4. For the affected hidden=7168 shape, a complete token is 3,584 bytes, already below the 4 KiB threshold, so the host heuristic does not split it.

MXFP8FP4 uses FP8 activations, making the complete token 7,168 bytes for this shape. The host reduced the scratch slice to 3,584 bytes while the full-pool branch still issued a one-shot 7,168-byte TMA transfer, causing the overflow.

The added static assertion also guards against a future configuration producing the same host/device size mismatch.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up question: where does the hidden=7168 assumption come from? These kernels support many models with many kinds of hidden size.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. The hidden=7168 value comes from the DeepSeek-V4 workload that reproduced the issue, but the safety of the other MMA kinds does not actually depend on that hidden size.

For MXFP4 and NVFP4, get_element_bits(mma_kind) is 4. The heuristic initially selects a block_k of either 128 or 256, then scales it with:

block_k = block_k * 8 / get_element_bits(mma_kind);
Therefore, MXFP4/NVFP4 always use a final block_k of 256 or 512, while block_n is fixed at 128. They can never satisfy the existing block_k == block_n condition in kUseFullPoolFP8FP4Path, so they always use the chunked pull path regardless of hidden size.

BF16 uses a separate kernel whose dispatch pull is also always chunked. MXFP8FP4 has 8-bit activations, so its final block_k can remain 128 and satisfy block_k == block_n, allowing it to enter the one-shot full-pool path.

My previous answer focused too much on the reproduced hidden=7168 shape; the block-shape condition is the general reason the other MMA kinds are unaffected.

const bool use_full_pool_fp8_fp4_path =
mma_kind == MmaKind::MXFP8FP4 and
num_ring_tokens >= num_max_pool_tokens and
num_shared_experts == 0 and

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Why num_shared_experts > 0 won't have this issue?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When num_shared_experts > 0, kHasShared is true, so the existing device-side predicate disables kUseFullPoolFP8FP4Path.

The kernel therefore uses the general chunked pull path, where every TMA transfer is limited to kNumBytesPerPull, so a reduced scratch slice is safe. Shared-expert phases also use separate buffers and synchronization counters; the current full-pool arrival protocol is only enabled for the routed-only case.

This host-side check simply mirrors that existing device-side eligibility condition.

mma_kind == MmaKind::MXFP8FP4 and
num_ring_tokens >= num_max_pool_tokens and
num_shared_experts == 0 and
block_k == block_n and

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: what is this checking? what if block_k != block_n?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The full-pool protocol uses an arrival bitmask to connect L1 output tiles, produced along N in BLOCK_N units, with L2 input tiles, consumed along K in BLOCK_K units. Its indexing assumes a one-to-one mapping between those tiles, which requires BLOCK_K == BLOCK_N.

If BLOCK_K != BLOCK_N, the existing device predicate makes kUseFullPoolFP8FP4Path false, and the kernel falls back to the general counter-based/chunked path. Therefore, the complete-token scratch buffer is not required in that case.

This condition also mirrors the static assertion already present in the device full-pool branch.

@nvpohanh

nvpohanh commented Sep 9, 2026

Copy link
Copy Markdown

@Fridge003 could you review this? Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants