Skip to content

[Refactor] Build seven more decoders from stage boundaries - #41779

Merged
ch-wan merged 8 commits into
mainfrom
cheng/refactor/plain-stack-boundaries
Sep 30, 2026
Merged

ch-wan merged 8 commits into
mainfrom
cheng/refactor/plain-stack-boundaries

Conversation

@ch-wan

@ch-wan ch-wan commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

This PR is part of a stack (oldest at bottom):

Motivation

Apertus, Spark 2.5, Mixtral, Arcee, Kimi-Linear, EXAONE 4.0 and Nanbeige still thread their residual tensor through each decoder layer by hand, and send both tensors to the next pipeline rank themselves. Building them from stage boundaries gives them the same attention and FFN boundaries as the other decoders: the pipeline handoff, the attention-DP rows and the aux hidden-state captures.

Modifications

  • finish_complete_output() moves an FFN output that compute has already reduced. On two paths the move bound for the exit also completes the sum, with a reduce-scatter onto the attention-TP slices of the residual or a CP reduce-scatter, so such an output was summed twice. The non-summing counterpart of those moves is now bound at construction and used there.
  • residual/post_norm.py: a plain read and a post-norm add (residual + norm(output)), for decoders that read the residual without a norm and normalize each sublayer's output before adding it. The plain read rejects a quantized input format and a post-residual addition, with or without a residual to update.
  • One commit per model:
    • The attention output projection leaves its TP sum to the FFN input (reduce_results=False), which runs the same all-reduce and fused add + norm as before.
    • The MLP or MoE still completes its own sum and finishes through finish_complete_output(). No sum is left to the next layer.
    • The model enters and leaves the stack through residual_batch (start, from_pp, to_pp, final_norm). EAGLE3 and DFlash captures come from the boundary; Kimi-Linear's DSpark captures read the stream through residual_batch.snapshot() and still travel to the next pipeline rank.
    • Kimi-Linear declares per layer whether its FFN is sparse, and Mixtral declares every FFN sparse.
    • EXAONE 4.0 is post-LN and uses the post-norm operations. The layer writes the MLP's post-norm add itself, so the stream it hands on is already written.
    • Nanbeige folds the residual back in between loops, applies the loop norm when the config asks for it, and starts a new stream for the next pass over the layers.
  • Behavior changes:
    • EXAONE 4.0 loops over its own layers only. It used to call every layer, including the placeholders of other pipeline ranks, which made --pp-size 2 fail at launch. Its split-prefill path now ends with the same final norm as forward() instead of norming the output added to itself.
    • Spark 2.5 already builds its attention on the attention-TP group. Under attention DP the boundaries now also bring its MLP the gathered rows.
    • With an explicit --enable-flashinfer-allreduce-fusion or --enable-quant-communications, the attention all-reduce of these models now takes the fused or quantized path. None of these architectures enables the fusion automatically, so the default configuration runs the same kernels as before.

Accuracy Tests

Each model on a real checkpoint, the tree before this PR against this PR. TP2 is python -m sglang.benchmark.one_batch --correctness-test (prefill logits and three generations), run twice on the parent. The server runs greedy-decode four prompts with input and output logprobs and the top-5 logprobs.

Model GPU TP2 one_batch Server
swiss-ai/Apertus-8B-2509 H200 identical PP2 identical
XHToken/Spark-X2.5-4B H200 identical PP2 identical
mistralai/Mixtral-8x7B-Instruct-v0.1 H200 identical PP2 identical
arcee-ai/AFM-4.5B-Base H200 identical PP2 identical
moonshotai/Kimi-Linear-48B-A3B-Instruct B200 identical PP2 identical (see below)
LGAI-EXAONE/EXAONE-4.0-1.2B H200 identical TP1 identical
LGAI-EXAONE/EXAONE-4.0-32B H200 TP1 identical; PP2 of this PR identical to its TP1 (the parent fails to launch with PP2)
Nanbeige/Nanbeige4.2-3B (two loops) H200 identical TP1 identical
  • Kimi-Linear PP2: the parent's first launch differs from its own second launch by up to 0.177 in logprob, from first-launch kernel tuning. The parent's second run and both runs of this PR are bitwise identical.
  • 109 affected unit test files (2,697 tests) pass at the top of this stack, the same as on main.

Not addressed here: EXAONE 4.0 checkpoints with tied embeddings (the 1.2B) still cannot run with PP, because the last pipeline rank has no lm_head.

Not tested: Nanbeige with PP. As before this PR, each pipeline rank runs every loop over its own layers only, so looped checkpoints (num_loops > 1) are not claimed to work with PP.

Speed Tests and Profiling

Not applicable: the same kernels and collectives run in the default configuration.

Checklist

Review and Merge Process

  1. Ping Merge Oncalls to start the process. See the PR Merge Process.
  2. Get approvals from CODEOWNERS and other reviewers.
  3. Trigger CI tests with comments or contact authorized users to do so.
    • Common commands include /tag-and-rerun-ci, /tag-run-ci-label, /rerun-failed-ci
  4. After green CI and required approvals, ask Merge Oncalls or people with Write permission to merge the PR.

CI States

Latest PR Test (Base): 🚫 Run #36668115383
Latest PR Test (Extra): 🚫 Run #36668115221
Latest PR Test (AMD ROCm 10): ⏳ Run #36668115633

@ch-wan
ch-wan force-pushed the cheng/refactor/layer-boundary-cleanup branch from 13d2309 to d82c3cf Compare September 30, 2026 00:17
@ch-wan
ch-wan force-pushed the cheng/refactor/plain-stack-boundaries branch from 7517a33 to a62289b Compare September 30, 2026 00:17
@ch-wan ch-wan added run-ci-extra CI: also run the extra suite (requires run-ci) bypass-fail-fast CI: a failing job no longer aborts its siblings (lint still gates) parallel-stages CI: stages dispatch together instead of waiting on each other run-ci CI: run the baseline test suite on this PR labels Sep 30, 2026
@ch-wan
ch-wan force-pushed the cheng/refactor/layer-boundary-cleanup branch from d82c3cf to f0f45a0 Compare September 30, 2026 00:33
@ch-wan
ch-wan force-pushed the cheng/refactor/plain-stack-boundaries branch from a62289b to aae865a Compare September 30, 2026 00:33
@ch-wan
ch-wan force-pushed the cheng/refactor/layer-boundary-cleanup branch from f0f45a0 to b83b468 Compare September 30, 2026 01:01
@ch-wan
ch-wan force-pushed the cheng/refactor/plain-stack-boundaries branch 2 times, most recently from d62d9dc to 9a59370 Compare September 30, 2026 01:51
@ch-wan
ch-wan force-pushed the cheng/refactor/layer-boundary-cleanup branch from b83b468 to 8a90925 Compare September 30, 2026 04:12
Base automatically changed from cheng/refactor/layer-boundary-cleanup to main September 30, 2026 04:16
@mintlify

mintlify Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
sglang-doc 🟢 Ready View Preview Sep 30, 2026, 4:18 AM

💡 Tip: Enable Automations to automatically generate PRs for you.

finish_complete_output() moves an FFN output that compute has already
reduced, but it ran the move bound for the exit, which on two paths also
completes the sum with a reduce-scatter: the residual on attention-TP slices
and a CP take-back that sums. The output was then summed twice. Bind the
non-summing counterpart of those moves at construction (the slice of the
complete output, and the CP take-back) and use it there.
The decoder layer threaded its residual tensor by hand and the model sent
both tensors to the next pipeline rank. Declare the attention and MLP stages
instead: the attention projection leaves its TP sum to the MLP input, which
runs the same all-reduce and fused add + norm as before, and the MLP still
completes its own sum and finishes through finish_complete_output(). The
model enters and leaves the stack through residual_batch, and EAGLE3 captures
come from the boundary.
As for Apertus: the attention projection leaves its sum to the MLP input,
which runs the same all-reduce and fused add + norm, the MLP completes its own
sum and finishes through finish_complete_output(), and the model enters and
leaves the stack through residual_batch. The attention was already built on
the attention-TP group; the boundaries now also bring the MLP its rows under
attention DP.
The attention projection leaves its TP sum to the MoE input, which runs the
same all-reduce and fused add + norm; the MoE block still all-reduces its own
output and finishes through finish_complete_output(). Every layer declares a
sparse FFN. The model enters and leaves the stack through residual_batch.
As for Apertus: the attention projection leaves its sum to the MLP input,
the MLP completes its own sum and finishes through finish_complete_output(),
the model enters and leaves the stack through residual_batch, and EAGLE3
captures come from the boundary.
Both attention kinds (KDA and MLA) leave their TP sum to the FFN input,
which runs the same all-reduce and fused add + norm; the dense MLP and the
MoE block still complete their own sums and finish through
finish_complete_output(). Each layer declares whether its FFN is sparse.
DSpark captures read the residual stream through residual_batch.snapshot(),
the same output plus residual, and still travel to the next pipeline rank
next to the stream.
EXAONE 4.0 is post-LN: each sublayer reads the residual as it is and its
output is normalized before it is added. Add the matching residual
operations, a plain read and a post-norm add, and declare the stages with
them. The attention leaves its TP sum to the MLP input, which completes it
and then runs the post-norm add; the layer writes the MLP's post-norm add
itself, so the stream it hands on is already written.

The model now loops over its own layers only. It used to call every layer,
including the placeholders of other pipeline ranks, which do not return a
pair. The split-prefill path now ends with the same final norm as forward()
instead of norming the output added to itself.
As for Apertus: the attention projection leaves its sum to the MLP input,
the MLP completes its own sum and finishes through finish_complete_output(),
and EAGLE3 / DFlash captures come from the boundary. Between loops the model
folds the residual back in, applies the loop norm when the config asks for
it, and starts a new stream for the next pass over the layers.
@ch-wan
ch-wan force-pushed the cheng/refactor/plain-stack-boundaries branch from 9a59370 to 491df15 Compare September 30, 2026 04:16
@ch-wan
ch-wan merged commit d41904f into main Sep 30, 2026
6 of 12 checks passed
@ch-wan
ch-wan deleted the cheng/refactor/plain-stack-boundaries branch September 30, 2026 04:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bypass-fail-fast CI: a failing job no longer aborts its siblings (lint still gates) parallel-stages CI: stages dispatch together instead of waiting on each other run-ci CI: run the baseline test suite on this PR run-ci-extra CI: also run the extra suite (requires run-ci)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant