Skip to content

Fix legacy normal kernels trapping when hidden exceeds the TMA staging buffer - #723

Open
yashkgp wants to merge 2 commits into
deepseek-ai:mainfrom
yashkgp:fix/legacy-normal-tma-hidden-limit
Open

Fix legacy normal kernels trapping when hidden exceeds the TMA staging buffer#723
yashkgp wants to merge 2 commits into
deepseek-ai:mainfrom
yashkgp:fix/legacy-normal-tma-hidden-limit

Conversation

@yashkgp

@yashkgp yashkgp commented Aug 12, 2026

Copy link
Copy Markdown

Fixes #722.

The legacy normal dispatch/combine kernels stage each token through a fixed-size per-warp TMA
buffer, but the staged payload grows with hidden. The only guard is EP_DEVICE_ASSERT, which
traps inside the kernel, so an oversized hidden surfaces as an asynchronous
CUDA error: an illegal instruction was encountered rather than a diagnosable failure. With BF16 and
num_topk=8 the effective caps were hidden <= 8184 (intranode dispatch), <= 8144 (internode
dispatch) and <= 8160 (internode combine sender), so hidden = 8192 could not run on the normal
path at all — while the low-latency kernels explicitly instantiate case 8192.

Changes

intranode.cu dispatch — lift the cap. Stage each token in as many TMA chunks as the buffer can
hold instead of hard-coding two halves:

num_tma_chunks = max(2, ceil_div(hidden_int4, (kNumTMABytesPerWarp - sizeof(uint64_t)) / sizeof(int4)))
tma_chunk_int4 = ceil_div(hidden_int4, num_tma_chunks)

Any hidden that fitted in two chunks before yields num_tma_chunks == 2 and
tma_chunk_int4 == hidden_int4 / 2, i.e. the identical split, so behaviour and shared memory usage
are unchanged for every configuration that works today. Larger hidden simply uses more chunks. This
also drops the hidden_int4 % 2 == 0 requirement, since ceil_div handles the remainder.

internode.cu combine — free headroom. The forwarder warps already set this kernel's dynamic
shared memory to 9248 * 24 = 221952 B, while the senders claimed only 16384 * 8 = 131072 B.
Deriving the sender budget from the forwarder one gives 27744 B per sender warp and raises the cap
from 8160 to 13840, with smem_size staying byte-for-byte identical at 221952 B
(max(27744 * 8, 9248 * 24) == max(16384 * 8, 9248 * 24)). An EP_STATIC_ASSERT pins the budget so
it can never shrink below the previous 16384.

internode.cu dispatch — host-side check only. This one stages a whole token (data +
SourceMeta + scales + top-k) in a single TMA, so lifting its cap needs chunking or a bigger budget;
16384 -> 24576 would grow dynamic shared memory from 128KB to 217KB and shrink L1, which wants a
benchmark I cannot run. Left as is, but the limit is now checked on the host so it raises an
immediate, actionable EP_HOST_ASSERT instead of trapping in the kernel. Same check added for the
combine sender.

The second commit is just bash format.sh (clang-format 15.0.7, as pinned in
requirements-lint.txt) normalizing the pre-existing lines of the two touched files, since
format.sh reformats whole files. It contains no functional change and touches none of the new code.

Verification — please read

I could not build or run this: no NVIDIA GPU available. What I did verify, by deriving both the host
and kernel formulas from the source and checking them exhaustively:

  • the new chunking fits kNumTMABytesPerWarp for every hidden_int4 in [1, 200000];
  • zero previously-working sizes change their chunk split, so the intranode path is
    bit-identical for hidden <= 8184;
  • the internode combine smem_size is unchanged at 221952 B.

Runtime check for a reviewer with hardware — this traps on main and should pass here:

python tests/legacy/test_intranode.py --hidden 8192
python tests/legacy/test_internode.py --hidden 8192   # still expected to raise EP_HOST_ASSERT (internode dispatch cap)

A throughput sanity check at hidden = 7168 would also be worth doing, to confirm the intranode
dispatch loop becoming dynamically bounded costs nothing measurable.

🤖 Generated with Claude Code

yashkgp and others added 2 commits August 12, 2026 16:18
…g buffer

The legacy high-throughput dispatch/combine kernels stage every token through a
fixed-size per-warp TMA buffer, but the staged payload grows with `hidden`. The
only guard is an `EP_DEVICE_ASSERT`, which `trap`s inside the kernel, so an
oversized hidden shows up as an unrelated asynchronous CUDA error instead of a
diagnosable failure. With BF16 and `num_topk=8` the effective caps were:

  - intranode dispatch        : hidden <= 8184
  - internode dispatch fwd/recv: hidden <= 8144
  - internode combine sender  : hidden <= 8160  (also hit by FP8 dispatch,
                                                 since combine is always BF16)

So `hidden=8192` could not run on the normal path at all, even though the
low-latency kernels explicitly instantiate it in `SWITCH_HIDDEN`.

  - intranode dispatch: stage each token in as many TMA chunks as the buffer can
    hold, instead of hard-coding two halves. Every hidden that fitted in two
    chunks before keeps the exact same split, so behaviour and shared memory
    usage are unchanged there; larger hidden sizes now simply use more chunks.

  - internode combine: the forwarder warps already size this kernel's dynamic
    shared memory, so the senders were leaving ~11KB per warp unused. Derive the
    sender budget from the forwarder one, which raises its cap to 13840 while
    keeping the total dynamic shared memory byte-for-byte identical (221952 B).

  - internode dispatch: keep the budget as is (raising it would grow dynamic
    shared memory from 128KB to 217KB and shrink L1), but check the limit on the
    host so it fails immediately with an actionable message.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`format.sh` reformats every file a change touches, so this normalizes the
pre-existing lines of the two files above with the pinned clang-format 15.0.7.
No functional change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment on lines +1267 to +1268
EP_HOST_ASSERT(get_num_bytes_per_token(hidden_int4, num_scales, num_topk, num_topk) + sizeof(uint64_t) <= kNumTMABytesPerWarp and
"Hidden size is too large for the internode dispatch TMA buffer");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 warning: 将容量检查移到 CUDA 流切换之前: 当 allocate_on_comm_stream=True 且 hidden 超限时,调用方已在 csrc/legacy/buffer.hpp:992 将当前流切换为通信流;这里抛出的异常会跳过 buffer.hpp:1228 的恢复逻辑,导致调用者捕获异常后,后续计算仍被静默提交到通信流。combine 新增的检查也有相同问题;应在切换流之前校验,或使用 RAII 保证异常时恢复。

🤖 v6

#ifndef DISABLE_SM90_FEATURES
#pragma unroll
for (int i = 0; i < 2; ++i) {
for (int offset_int4 = 0; offset_int4 < hidden_int4; offset_int4 += tma_chunk_int4) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔵 suggestion: 接收端拷贝循环从 '#pragma unroll for (i < 2)' 常量边界改为按 tma_chunk_int4 动态步进后,编译器不再能完全展开该循环,这是本 PR 唯一的潜在性能疑点。合入前建议由有硬件的 reviewer 在典型配置(如 hidden=7168、BF16)下跑一次 intranode dispatch 吞吐对比,确认无可测量回退;同时执行 'python tests/legacy/test_intranode.py --hidden 8192' 验证原本 trap 的用例现在通过。

🤖 v5

auto half_hidden_bytes = half_hidden_int4 * static_cast<int>(sizeof(int4));
// Stage each token through the per-warp TMA buffer, using as few chunks as the buffer can hold.
// NOTES: the m-barrier lives right after the staged chunk, so a chunk may only use
// `kNumTMABytesPerWarp - sizeof(uint64_t)` bytes. We always use at least 2 chunks, so that the

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔵 suggestion: PR 描述与提交信息中的旧上限数字有一处出入,建议修正以免误导后续读者:描述称旧 intranode dispatch 上限为 hidden ≤ 8184,但旧代码同时断言 hidden_int4 % 2 == 0,故实际旧上限是 8176(hidden_int4=1022);8184(hidden_int4=1023,奇数)在旧代码下会触发断言,是本改动移除偶数约束后才首次可用。这不影响代码正确性,只需更新文字表述。

🤖 v5

@ds-review-bot

Copy link
Copy Markdown
Collaborator

🤖 ds-review-bot Code Review

v6

核心 TMA 分块和共享内存预算调整看起来合理,但新增的可诊断异常在公开的通信流分配模式下会泄漏当前 CUDA 流状态,影响异常恢复后的后续计算。

v5

改动与描述一致,审查通过(LGTM,附带两条 suggestion)。提交 4e38668 修复了 legacy normal dispatch/combine 内核在 hidden 超出每 warp TMA 暂存缓冲时以 EP_DEVICE_ASSERT trap(表现为异步 'illegal instruction' CUDA 错误)的问题。逐项核验结论:(1) intranode.cu dispatch 的动态分块公式已解析证明对所有 hidden_int4 ≥ 1 都满足 kNumTMABytesPerWarp=8192 约束(kNumMaxTMAChunkInt4=511;h≤1022 时 chunk=⌈h/2⌉≤511,h>1022 时 n=⌈h/511⌉ ⇒ ⌈h/n⌉≤511),比描述中穷举 [1,200000] 更强;原先所有可运行的配置(偶数 hidden_int4≤1022)得到 num_tma_chunks==2、chunk==hidden_int4/2 的完全相同切分,mbarrier 偏移与旧布局一致,行为与共享内存用量位相同;尾块 min(...) 处理及 hidden_int4=1 边界均安全;grep 确认 half_hidden 无残留引用。(2) internode.cu combine 的 sender 预算 align_down(924824/8,16)=27744B(本身 16 对齐),smem_size=max(277448, 924824)=221952B 与原值逐字节相同;EP_STATIC_ASSERT(>=16384) 有效防止预算回退;按 num_bytes_per_token=align_up(hidden_bytes+8(SourceMeta)+num_topk4,16) 核算,BF16+num_topk=8 下新上限恰为 13840,与描述一致;host 检查显式以 nv_bfloat16 换算正确(combine 始终以 nv_bfloat16 实例化),与内核 sender 侧 EP_DEVICE_ASSERT 逐项对应。(3) internode.cu dispatch 的 host 侧 EP_HOST_ASSERT 公式与内核第 527/581 行完全一致,能在 launch 前以可诊断异常替代 trap;保持 16384 预算(避免动态共享内存 128KB→217KB 挤压 L1)是合理的保守选择;'cond and "msg"' 写法与仓库既有用法(internode_ll.cu:510、nccl.cu:104)一致。(4) 提交 ba6a33b(Run format.sh)逐 hunk 检查确认为纯换行/缩进重排(internode.cu 长表达式折行、intranode.cu BARRIER_LAUNCH_CASE 缩进),无语义变化且未触碰新增代码。验证限制:本环境无 GPU 且 shell 受限,无法编译、无法运行 tests/legacy/test_intranode.py --hidden 8192 / test_internode.py,也无法用 clang-format 15.0.7 复核格式;需有硬件的 reviewer 按描述执行运行时检查。

v4p

该 MR 修复 legacy normal 路径下 hidden 尺寸超过 TMA staging buffer 时内核 trap 的问题:intranode dispatch 改为按 TMA buffer 容量动态分块,internode combine 提高 sender warp 的 TMA 预算,并对 internode dispatch/combine 增加主机侧 EP_HOST_ASSERT。整体实现与既有模式一致,未发现功能性问题。

Files reviewed: 2
Issues found: 🟡 1 warning | 🔵 2 suggestion
Inline comments posted: 3

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.

Legacy normal kernels trap for hidden >= 8192: TMA staging buffer caps hidden with no host-side check

2 participants