qwen3_5_moe: run lm_head on sampled rows only (fixes 32 GB first-prefill OOM) - #342
Open
chrisqianz wants to merge 1 commit into
Open
qwen3_5_moe: run lm_head on sampled rows only (fixes 32 GB first-prefill OOM)#342chrisqianz wants to merge 1 commit into
chrisqianz wants to merge 1 commit into
Conversation
…ill OOM The engine samples one row per request (batch_logits = logits[:batch.size]); the remaining rows of the forward window are overlap context. Projecting the whole window through the vocab GEMM allocated M x vocab bf16 -- a default 8192-token chunk at Qwen3.8's 248k vocab is 3.79 GiB, a guaranteed OOM on 32 GB cards at the first prefill of every session -- and spent FLOPs on rows nobody reads. Slice to batch.size rows before the lm_head GEMM. All three call sites already consume exactly this contract: the eager path slices logits[:batch.size], graph capture assigns into buffer.logits[:bs], and the prefill warmup discards the output. Long prefills additionally get a much cheaper lm_head pass. Report: unsloth/Qwen3.8-27B-NVFP4 on RTX 5090D. (cherry picked from commit 1d28547)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
Serving any qwen3_5_moe dense model (vocab 248,320 — e.g. Qwen3.6-27B-NVFP4, unsloth/Qwen3.8-27B-NVFP4) on a 32 GB card dies on the first request of every session:
The worker exits and the supervisor stops the API (
Backend worker is gone and cannot be restarted— the #20 symptom family). A shorter first prompt "works": the fatal allocation scales with the prefill window, so it fires exactly when the first request's chunk fills.Root cause
The eager model forward runs the vocab GEMM over the entire forward window, but the engine only ever consumes one row per request:
engine.py:933—batch_logits = logits[: batch.size](the rest is overlap context, discarded);graph.py:175— CUDA-graph capture already assignsself.buffer.logits[:bs] = model.forward(), i.e. the graph path is built around abs-row output;engine.py:985) discards the output entirely.So for a default 8192-token chunk the eager path transiently allocates 8194 × 248,320 × bf16 = 3.79 GiB — exactly the number in the traceback (8194 =
max_extend_tokens8192 + sampled rows) — and spends ~M × vocab × hiddenFLOPs on rows nobody reads. On a 32 GB card holding ~21 GB of weights + the KV pool, the first prefill of every session deterministically OOMs.Fix
Slice to the sampled rows before the lm_head GEMM:
qwen3_5_moe's forward window already carries the sampled rows first (that ordering is exactly what
logits[:batch.size]assumes), so no gather is needed. This is the same reduction deepseek_v4 already ships —F.linear(h[0, last_indices], self.head) # [B, vocab]— just expressed as a slice for this family's layout.Transient buffer: 3.79 GiB →
batch.size × vocab × 2B(~0.5 MB at concurrency 1); long-prefill lm_head cost collapses from a full-window GEMM to a single-row GEMM.Scope / prior art
Present since the initial release (
3af9d90); surfaced while servingunsloth/Qwen3.8-27B-NVFP4on an RTX 5090D (32 GB) in the #208 discussion, but #208 never touchedforward()— this is a pre-existing engine bug that also threatens Qwen3.6-27B-NVFP4 on the same hardware. Distinct from the other M-proportional prefill OOM reports: #171 (dsv4 pool-derived chunk budget), #172 (dsv4 sliding-window re-prefill), #110 (MoE expert workspace). Several sibling families (llama,qwen3,glm4_moe, … ) still returnself.lm_head.forward(output)over the full window and could adopt the same one-liner; this PR keeps to the family verified end-to-end.Verification
prompt_tokens=9500), server stays healthy; chat conversations run normally.batch.sizerows (slice is a no-op), CUDA graphs capture with the same shapes as before.tests/models/test_qwen3_5_moe_config.py+test_qwen3_5_moe_weight.py: 29 passed.