Repository navigation
[Bugfix][DeepSeek-V4] Keep mHC finite under float16 for attention-sink rows - #658
Merged
Merged
Conversation
…k rows DeepSeek V4 is trained in bfloat16. Its attention-sink row (the BOS token) carries one residual channel far above the rest: about 2k at layer 27, 20k at layer 33, 35k at layer 35, and past 65504 from layer 41 on. Under float16 three places in the mHC kernels lose that row: - hc_prenorm_gemm_tilelang and its block_m variant square float16 values in float16. Beyond |x| = 255.9 the square sum is inf, the norm factor 0, and the row's mixes fall back to hc_base alone. Only the large-batch path (more than 16 tokens, non-SM70) does this; the small-batch kernel and the SM70 Triton staging square in float32, so a 13-token and a 26-token prompt disagree on the same BOS row. - A residual beyond 65504 is stored as inf, and the next attention turns it into NaN for every token that still has the row in its window. Residual stores now saturate at the largest finite float16 (tilelang post and fused kernels, the SM70 fp32-stage kernel, the SM70 Triton post kernel, mhc_post_torch); the value a kernel reuses is the saturated one. - The unnormalized pre-mix sum of the four streams is staged in float16 before the fused RMSNorm scales it down to about 64. float16 runs stage it in float32, and the SM70 Triton pre-norm kernel drops the float16 round trip it copied from that staging. The saturation and the float32 staging are gated on use_fp16. bfloat16 runs only see the two GEMM kernels square in float32 instead of bfloat16, same range, more precise. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Peuqui <peuqui@github.com>
Peuqui
pushed a commit
to Peuqui/1Cat-vLLM
that referenced
this pull request
Sep 28, 2026
…ckport vllm#44744) Backport of vllm-project/vllm#44744 (GHSA-8wr5-jm2h-8r4f): sample_recovered_tokens_kernel masks the lanes past the vocabulary to -inf before the tiled argmax and clamps recovered_id to vocab_size - 1, together with its regression test test_sample_recovered_tokens_vocab_boundary. On 1Cat main the kernel returns vocab_size for an all-NaN target row on the path without draft probabilities (DSpark, MTP, EAGLE): tl.max skips NaN and picks a padded lane of the last tile. The next step feeds that id to the embedding lookup, a device-side assert (the chain 1CatAI#658 describes). The added test covers that case with vocab sizes 1000, 129280 and 151936. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Peuqui <peuqui@github.com>
This was referenced Sep 28, 2026
yangzhuxinyzx
pushed a commit
to Peuqui/1Cat-vLLM
that referenced
this pull request
Oct 2, 2026
…ckport vllm#44744) Backport of vllm-project/vllm#44744 (GHSA-8wr5-jm2h-8r4f): sample_recovered_tokens_kernel masks the lanes past the vocabulary to -inf before the tiled argmax and clamps recovered_id to vocab_size - 1, together with its regression test test_sample_recovered_tokens_vocab_boundary. On 1Cat main the kernel returns vocab_size for an all-NaN target row on the path without draft probabilities (DSpark, MTP, EAGLE): tl.max skips NaN and picks a padded lane of the last tile. The next step feeds that id to the embedding lookup, a device-side assert (the chain 1CatAI#658 describes). The added test covers that case with vocab sizes 1000, 129280 and 151936. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Peuqui <peuqui@github.com>
Peuqui
pushed a commit
to Peuqui/1Cat-vLLM
that referenced
this pull request
Oct 5, 2026
…-sink rows 1CatAI#658 saturates the float16 stores of the mHC pre/post kernels; the fused hc_head kernel still wrote its float32 weighted sum straight into the float16 output. For an attention-sink row, whose four streams add up past 65504, the head input became inf. Clamp the store to the float16 range under use_fp16, the same as the other mHC stores. The new test builds a sink row whose exact hc_head output exceeds float16 and checks that the kernel stays finite, saturates that row and matches a float32 reference elsewhere; on main it fails with inf in the sink row. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Peuqui <peuqui@github.com>
Peuqui
pushed a commit
to Peuqui/1Cat-vLLM
that referenced
this pull request
Oct 11, 2026
…-sink rows 1CatAI#658 saturates the float16 stores of the mHC pre/post kernels; the fused hc_head kernel still wrote its float32 weighted sum straight into the float16 output. For an attention-sink row, whose four streams add up past 65504, the head input became inf. Clamp the store to the float16 range under use_fp16, the same as the other mHC stores. The new test builds a sink row whose exact hc_head output exceeds float16 and checks that the kernel stays finite, saturates that row and matches a float32 reference elsewhere; on main it fails with inf in the sink row. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Peuqui <peuqui@github.com> (cherry picked from commit 721ba1f)
yangzhuxinyzx
pushed a commit
that referenced
this pull request
Oct 28, 2026
…-sink rows (#715) #658 saturates the float16 stores of the mHC pre/post kernels; the fused hc_head kernel still wrote its float32 weighted sum straight into the float16 output. For an attention-sink row, whose four streams add up past 65504, the head input became inf. Clamp the store to the float16 range under use_fp16, the same as the other mHC stores. The new test builds a sink row whose exact hc_head output exceeds float16 and checks that the kernel stays finite, saturates that row and matches a float32 reference elsewhere; on main it fails with inf in the sink row. Signed-off-by: Peuqui <peuqui@github.com> Co-authored-by: Peuqui <peuqui@github.com> Co-authored-by: Claude <noreply@anthropic.com>
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.
Purpose
DeepSeek V4 is trained in bfloat16. Its attention-sink row (the BOS token)
carries one residual channel far above the rest. Measured on
DeepSeek-V4-Flash, per-row maximum of the residual stream: about 2k at
layer 27, 20k at layer 33, 35k at layer 35, 63.5k at layer 40, and past
65504 from layer 41 on. Every other token stays below about 200. Under
float16 three places in the mHC kernels lose that row.
hc_prenorm_gemm_tilelangand itsblock_mvariant readx_valasfloat16 and accumulate
sqr[0] += x_val * x_val. The product of twofloat16 values is float16, so beyond |x| = 255.9 it is inf before it
reaches the float32 accumulator. The square sum is inf, the norm factor 0,
and the row's mixes fall back to
hc_basealone. Only the large-batch path(more than 16 tokens, not SM70) does this;
mhc_fused_tilelangand theSM70 Triton staging square in float32. A 13-token and a 26-token prompt
therefore disagree on the very same BOS row, which is causal and cannot
depend on the prompt. We saw it as different expert routing for token 0:
one dominant expert at weight 1.45 on the small path, six smeared weights
on the large one.
A residual beyond 65504 is stored as inf. The next attention turns that
into NaN for every token that still has the row in its sliding window,
which is every token of a prompt shorter than the window. Residual stores
now saturate at the largest finite float16:
mhc_fused_tilelang,mhc_post_tilelang,sm70_mhc_post_fp32_stage_tilelang, the SM70 Tritonpost kernel and
mhc_post_torch. A kernel that reuses the value it storesreuses the saturated one, so what it computes on matches what is stored.
The unnormalized pre-mix sum of the four streams is staged in float16
before the fused RMSNorm scales it down to about 64. Four streams near
65504 times a pre weight of about 0.6 is about 157k. float16 runs now
stage it in float32, which is exact instead of clipped. The SM70 Triton
pre-norm kernel carried a deliberate
.to(tl.float16).to(tl.float32)round trip that copied this staging; it is removed.
For bfloat16 runs the saturation and the float32 staging are gated on
use_fp16and do not apply. The one thing that changes there too is that thetwo GEMM kernels now square in float32 instead of bfloat16, which has the
same range and is more precise. I have no bfloat16-capable card to measure
that on.
How it surfaced for us: with async scheduling a few steps run past the end
of a request with token id 0 in the first row, which is BOS for this
tokenizer. That BOS in the middle of a context overflowed, its logits row
became NaN,
sample_recovered_tokensreturnsvocab_sizefor an all-NaNrow, and the next step looked that id up in the embedding table: "CUDA
error: device-side assert triggered", engine dead right after a request
ended. Greedy requests never showed it, argmax over NaN is a valid id.
Not covered here:
hc_head_fuse_tilelangwrites its output in theactivation dtype before the final norm and sums four streams as well. We
could not test it (its TileLang codegen does not build on our pre-Ampere
cards), so it is left alone rather than changed blind. #643 (hidden state
growing without bound, NaN spreading causally from an all-zero-id dummy)
reads like the same family on another model; we have not checked it.
Test Plan
The new test drives
mhc_fused_post_pre_tilelangon both sides of the16-token switch (6, 13, 16, 17, 26 tokens) with a sink channel of 200, 3000
and 35000, and with a residual that has to exceed float16 (exact value
82800). It checks the stored residual, the mixes and the layer input of the
sink row against
mhc_post_torchandmhc_pre_torchin float32.Test Result
Tesla V100-PCIE-32GB (sm70) and Quadro RTX 8000 (sm75), one card each,
CUDA_DEVICE_ORDER=PCI_BUS_ID.With this change: V100
88 passed, 9 skipped, RTX 800074 passed, 23 skippedfor the three files together.The new test alone against unmodified
main(b711d53):11 failed, 10 passed. All 35000 cases and all overflow cases failwith
Greatest absolute difference: inf at index (3077,)in the layerinput, plus the torch store.
13 failed, 8 passed. The same, plus the 3000 cases at 17 and26 tokens, which is defect 1: the mixes come back as
[1.005, 0.98, 1.045, 1.04]against a reference of[0.718, 1.301, 0.752, 0.812].pre-commit: all hooks passed; mypy-3.10 manual stage passed.
On our rig (DeepSeek-V4-Flash NVFP4 + DSpark, pipeline parallel over two RTX
8000 and three V100, float16) we run a fork of this repository. There the
same kernel changes, together with a saturating cast at the end of the model
that only exists in the fork's pre-Ampere torch paths, were measured: a
stress test of 120 short requests, three in flight, greedy and temperature
1.0 mixed, killed the engine within three requests before and runs clean now,
three times. Prompts of 13 and 14 tokens that came back empty answer
correctly. A 30k-token needle test finds 4 of 4, decode speed and draft
acceptance are unchanged. I have not run this exact branch as a server.
Not a duplicate
No open PR touches
vllm/model_executor/kernels/mhc/. The onlymhchit isthe dependabot PR #590. Open issue #620 is about the TileLang version, #643
is mentioned above.
AI assistance
AI assistance (Claude) was used to trace the NaN to these kernels, to write
the change and the test, and to draft this text. I have read every changed
line and ran the tests above on my own hardware.
Essential Elements of an Effective PR Description Checklist
supported_models.mdandexamplesfor a new model.