Skip to content

Add Triton kernels for vocab-parallel logprobs, entropy and KL - #8838

Merged
pengdurice merged 8 commits into
deepspeedai:masterfrom
jinyouzhi:vocab-parallel-ops-perf
Oct 21, 2026
Merged

pengdurice merged 8 commits into
deepspeedai:masterfrom
jinyouzhi:vocab-parallel-ops-perf

Conversation

@jinyouzhi

@jinyouzhi jinyouzhi commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #8791 (RFC #8790). The vocab-parallel logprobs / entropy / KL ops ran each row chunk as a dozen separate eager fp32 kernels. On the same local shard they were about 2–2.4× slower than a dense log_softmax, which offsets most of the memory benefit when they are used for OPSD / RL losses (e.g. deepspeedai/DeepSpeedExamples#1019).

Changes

  • New deepspeed/ops/triton_ops/vocab_parallel_softmax.py: Triton kernels with one program per row, looping over the local vocabulary in blocks. The module has no communication.
    • Local max and sum-exp are computed in a single read. They are then rescaled to the global max, so TP still needs only one MAX and one SUM all-reduce.
    • Fused forward and backward for entropy and KL, and a fused backward for logprobs (the top-k one-hot terms are added in fp32 before the single cast, as before).
  • deepspeed/sequence/vocab_parallel_ops.py dispatches to these kernels on CUDA/XPU when the inputs are contiguous and deepspeed.HAS_TRITON is true. Otherwise it runs the existing eager path, which is unchanged. Collectives and the autograd structure are the same.
  • Docs: a note in autotp-training.md.

Performance

Intel Arc Pro B70, bf16, 75968-column shard (Qwen vocab / TP2), forward + backward:

op N master this PR extra peak memory
reverse KL 8192 340.8 ms 15.5 ms 510 MiB → ~0
forward KL 8192 313.0 ms 15.5 ms 446 MiB → ~0
entropy 8192 205.5 ms 9.1 ms 335 MiB → ~0
logprobs (k=1) 8192 99.5 ms 7.2 ms 192 MiB → ~0
reverse KL 2048 85.1 ms 4.0 ms
  • KL is within ~10% of the 608 GB/s memory-bandwidth floor.
  • Logprobs with a large top-k is slower (k=256: 35 ms) but still about 3× faster than master.
  • For reference, torch.compile of the eager chunk functions reached 26.9 ms for reverse KL (N=8192), so I chose hand-written kernels.

Tests

  • New test_triton_kernels_match_eager_path (fp32 / bf16 × logprobs / entropy / forward KL / reverse KL). On an accelerator it compares the Triton outputs and gradients with the CPU eager path. Cases: multiple kernel blocks, partially masked vocabulary, all--inf rows, support mismatch with underflowing probability, large offsets, duplicate / ignored top-k ids, and a fully ignored row.
    • I checked that it fails when the kernel's support-mismatch inf is removed.
  • The TP=2 tests (uneven and empty shards) now run through the Triton path.

Executed on 2× Intel Arc Pro B70 (XPU), torch 2.14.0.dev+xpu, Triton 3.8.0:

  • tests/unit/v1/sequence_parallelism/test_vocab_parallel_ops.py: 41 passed.
  • The existing single-process semantic tests, with XPU as the default device (so they run through Triton): 30 passed.

Update: single-pass entropy/KL forward (by @0z5a)

The tables above were measured on the first commit. @0z5a's e8038bed3 reads each logit once in the entropy/KL forward, using online weighted statistics. Both commits were re-measured with the same script on 1× B70: bf16, a 75968-column shard, forward + backward, median of 20 runs.

op N 99f054192 head 97d4f66df
reverse KL 8192 18.7 ms 14.6 ms
forward KL 8192 18.7 ms 14.6 ms
entropy 8192 9.2 ms 7.1 ms
logprobs (k=1) 8192 7.4 ms 7.4 ms
reverse KL 2048 4.8 ms 3.9 ms

Extra peak memory, excluding inputs and gradients, is below 1 MiB on both commits.

Numerics vs the first commit. Compared on 1× and 2× B70 (TP=1, TP=2), fp32 and bf16. Cases: 512×32000, 256×151936, and 64×32000 with a +1000 offset and -inf masks. Each output and gradient was also compared with an fp64 CPU reference.

  • logprobs: outputs and gradients are bitwise identical.
  • entropy / KL: not bitwise identical, because the summation order changed. Forward outputs differ by at most 35 ULP. The forward is closer to fp64 on almost every case: e.g. fp32 entropy relative error drops from 3e-7 to 1.5e-7, and KL from about 6–9e-7 to 3–8e-7. Gradient errors stay at the same level, and bf16 gradient errors are unchanged (about 2e-3).
  • There are no inf/NaN placement differences.

Contribution checklist

Related work. This is a performance follow-up to #8791 (RFC #8790). A search for vocab-parallel Triton kernels and entropy/KL found no overlapping PR.

Commands and results. Hardware and software: 2× Intel Arc Pro B70 (XPU, XCCL), Level Zero driver 1.15.39122 (compute runtime 26.27.39122.11), torch 2.14.0.dev20260804+xpu, Triton 3.8.0.

pytest -p no:randomly tests/unit/v1/sequence_parallelism/test_vocab_parallel_ops.py

At head 8f463213f: 49 passed on 2× B70; with DS_ACCELERATOR=cpu, 33 passed and the 16 Triton tests are skipped.

Compatibility matrix.

Validated Not validated
Precision fp32, bf16 fp16
Parallelism TP=1, TP=2 (uneven and empty shards) TP>2 on device
Layout Contiguous rows, plus row_layout strides from e8038bed3
Accelerator XPU (Triton); CUDA (Triton, 2× H100, validated by @0z5a); CPU (eager fallback, Triton tests skipped)

ZeRO and offload do not interact with these ops.

AI assistance. This PR was developed with AI assistance (GitHub Copilot CLI). I reviewed every changed line and ran the tests above.

The vocab-parallel ops ran each row chunk as a dozen separate eager fp32
kernels, making them 2-2.4x slower than a dense log_softmax on the same shard
and keeping several fp32 copies of each chunk alive.

On CUDA/XPU with Triton available, each pass now runs in one kernel per row
that loops over the local vocabulary: local max and sum-exp in a single read
(rescaled to the global max, still one MAX and one SUM all-reduce), then the
fused forward and backward passes. Other devices, non-contiguous inputs, and
deepspeed.HAS_TRITON = False keep the existing eager path unchanged.

Intel Arc Pro B70, bf16, 75968-column shard, N=8192, fwd+bwd:
  reverse KL 340.8 -> 15.5 ms, forward KL 313.0 -> 15.5 ms,
  entropy 205.5 -> 9.1 ms, logprobs 99.5 -> 7.2 ms;
  extra peak memory 192-510 MiB -> ~0. KL is within ~10% of the
  608 GB/s bandwidth floor.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
The Triton forward for entropy and KL ran two passes per input: a stats pass for the
local max and sum-exp, then a second pass for the entropy or KL terms. On H100 the
kernels are at the memory-bandwidth floor of that pass count, so the remaining cost
is the extra read.

Each shard now accumulates, in the same pass as its max and sum-exp, the
probability-weighted sum of logits centred on that max (for KL, the centred log-ratio
weighted by p_A, plus a flag for support that B masks out). After the MAX all-reduce
the sums are re-centred on the global max, so H = log S - W / S and
KL = W / S_A - log S_A + log S_B. Centring on the max keeps a large common offset
from cancelling in fp32. With TP the forward needs one MAX and one SUM all-reduce
instead of one MAX and two SUMs.

Rows are now addressed with up to two strides, so a slice such as logits[:, :-1] is
read in place instead of being copied by reshape in both forward and backward. Other
layouts are copied first, as before. Gradients are returned contiguous.

H100, bf16, 8192 x 75968 shard, fwd+bwd vs the two-pass kernels:
  reverse KL 5.83 -> 4.48 ms (fp32 input 11.5 -> 8.39 ms), entropy 3.46 -> 2.69 ms;
  logits[:, :-1] input: reverse KL 13.6 -> 4.5 ms and 2.3 GiB less peak memory.
logprobs is unchanged (within 1% at 8192 rows, about 25 us slower at 1024 rows).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
@0z5a

0z5a commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

Thanks for the Triton follow-up. I validated it on H100 and tried a few kernel-level changes on top of it; numbers and a patch are below.

#8838 on H100

  • tests/unit/v1/sequence_parallelism/test_vocab_parallel_ops.py on 2× H100 (CUDA path, including the TP=2 uneven and empty-shard tests): 41 passed. The single-process semantic tests with CUDA as the default device (so they run through Triton): 39 passed.
  • torch 2.12.1+cu130, Triton 3.7.1, driver 595.91.07.
  • The kernels are already at the memory-bandwidth floor for their pass count. For reverse KL with a detached teacher, fwd+bwd reads the student and teacher shards three times and writes one gradient (7 full passes). At the measured copy bandwidth that is about 5.9 ms for a bf16 8192 × 75968 shard, and the kernels take 5.83 ms.
  • The default launch configuration is already the best on H100. In a sweep of BLOCK 1024–8192 × 4–32 warps, nothing was more than 1% faster than 4096 / 16; some settings were up to 9% slower.

Suggested change: read each logit once in the entropy / KL forward

Since the kernels are bandwidth bound, the remaining lever is the number of passes. The forward currently reads each input twice: once for the local max and sum-exp, then again for the entropy or KL terms. The first pass can also accumulate the probability-weighted sum of logits centred on the running max: Σ e^(z−m)(z−m) for entropy, and Σ e^(a−m_a)((a−m_a)−(b−m_b)) for KL(A‖B). When the max grows, these sums are rescaled the same way as the sum-exp.

After the MAX all-reduce, each shard's sums are re-centred on the global max:

  • H = log S − W / S
  • KL = W / S_A − log S_A + log S_B

Centring on the max keeps a large common offset from cancelling in fp32; your large-offset test still passes. Support mismatch, meaning B masks a token that A supports, is a flag decided on the logits; it shares the MAX all-reduce. With TP the forward then needs one MAX and one SUM all-reduce instead of one MAX and two SUMs. The backward kernels are unchanged.

Separately, rows are addressed with up to two strides, so a slice such as logits[:, :-1] is read in place. Today _flatten copies such a slice with reshape in both forward and backward. Layouts the kernels cannot address, such as a strided vocabulary dimension, are still copied first, as before. Gradients are returned contiguous. I added one sentence about this to the tutorial note.

H100, fwd+bwd, 8192 × 75968 shard. The #8838 and patch columns are medians of 3 interleaved runs with nothing else on the node; the eager column comes from a single run.

op eager (#8791) #8838 #8838 + patch
reverse KL, bf16 133 ms 5.83 ms 4.48 ms
reverse KL, fp32 input 117 ms 11.5 ms 8.39 ms
entropy, bf16 79.6 ms 3.46 ms 2.69 ms
logprobs k=1, bf16 37.7 ms 2.80 ms 2.82 ms
reverse KL, bf16 logits[:, :-1] 141 ms 13.6 ms (+2.3 GiB peak) 4.47 ms (no extra peak)
  • The forward alone is 1.7× (bf16) to 1.9× (fp32) faster. fp32 reverse KL is at the 5-pass bandwidth floor.
  • The single-pass statistics keep up to six running values per lane, so they use 2048-wide blocks. With 4096 they ran about 1.2× slower on H100.
  • logprobs is unchanged: within 1% at 8192 rows, and about 25 µs slower at 1024 rows.
  • At an OPSD chunk of 1024 rows, bf16 reverse KL is about the same (0.82 → 0.81 ms), and fp32 input is 1.27× faster.

All outputs and gradients match the eager path within your test tolerances, with NaN/inf in the same places. Validation:

  • The full test file passes on 2× H100: 49 passed, including the new test. The new test covers a sequence slice and a strided vocabulary; positions outside the view hold NaN, so reading them would fail.
  • The single-process semantic tests pass on CUDA with fp32 default tolerances: 47 passed.
  • A CPU run under TRITON_INTERPRET=1, including gloo TP=2 with uneven and empty shards, matches the eager path.

One side effect I noticed, on a 1024 × 75968 KL in both directions: with the patch, bf16 and fp32 inputs give bitwise identical outputs, and the bf16 gradient equals the round-to-nearest-even of the fp32 one. With the current kernels they differ by up to 4e-6. My guess is that the per-thread layout, and so the reduction order, depends on the dtype; I have not verified that. Either way it is harmless, just a note.

I have not run anything on XPU, so the 2048-wide statistics blocks are only tuned on H100. I opened the patch as a single-commit PR against your branch: jinyouzhi#5. Feel free to take all or part of it.

AI assistance was used for the implementation and analysis; I reviewed the changes and the measurements.

Read each logit once in the vocab-parallel entropy/KL forward
@jinyouzhi

Copy link
Copy Markdown
Contributor Author

@0z5a Thank you for your optimization patch. I merged it.

@jinyouzhi

Copy link
Copy Markdown
Contributor Author

One side effect I noticed, on a 1024 × 75968 KL in both directions: with the patch, bf16 and fp32 inputs give bitwise identical outputs, and the bf16 gradient equals the round-to-nearest-even of the fp32 one. With the current kernels they differ by up to 4e-6. My guess is that the per-thread layout, and so the reduction order, depends on the dtype; I have not verified that. Either way it is harmless, just a note.

It’s worth checking batch invariance when we move on to RL rollouts.

jinyouzhi and others added 2 commits October 17, 2026 17:09
The docs and the test skip now follow the dispatch predicate instead of a
hard-coded CUDA/XPU list.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
@jinyouzhi
jinyouzhi marked this pull request as ready for review October 21, 2026 07:39
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T07:48:41.894653Z 987bd4a Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 987bd4aad5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +42 to +44
if logits[0].device.type not in ("cuda", "xpu"):
return False
return deepspeed.HAS_TRITON

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Gate Triton dispatch on accelerator capability

On CUDA devices that DeepSpeed marks as Triton-unsupported (CudaAccelerator.is_triton_supported() returns false below compute capability 8, including V100/T4), HAS_TRITON only establishes that a Triton backend driver is active, so this predicate still selects these kernels. Any of these formerly eager vocab-parallel ops then reaches an unsupported Triton JIT instead of falling back; check the accelerator capability here, and represent the validated XPU support through that capability rather than hard-coding device types.

AGENTS.md reference: AGENTS.md:L40-L40

Useful? React with 👍 / 👎.

@jinyouzhi jinyouzhi Oct 21, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed the device specific dispatch and follow upper capacibility status.

    accelerator = get_accelerator()
    return deepspeed.HAS_TRITON and accelerator.is_triton_supported() and accelerator.on_accelerator(logits[0])

Comment on lines +91 to +92
scale = tl.where(seen, tl.exp(run_max - row_max), 0.0)
run_weighted = tl.where(seen, scale * (run_weighted - (row_max - run_max) * run_sum), 0.0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Guard zero rescaling before applying infinite shifts

When a valid fp32/bfloat16 row spans the finite extremes (for example [-3e38, 3e38]), row_max - run_max overflows to inf while scale underflows to zero, so this evaluates 0 * (0 - inf) and writes NaN into the weighted sum. The eager implementation returns entropy 0 for this one-hot distribution; the analogous updates in _kl_stats_kernel and the TP re-centering in _triton_entropy/_triton_kl_div have the same 0×inf or inf−inf failure, even yielding NaN for KL of two identical rows. Mask the weighted contribution when the rescale is zero, including in the distributed re-centering, before multiplying by the shift.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reproduced (entropy and KL were NaN on XPU for a row spanning ±3e38) and fixed in 6a53407: zero-weight terms are skipped in both kernels (in-loop rescale, per-element term, lane re-centring) and in the TP re-centring. Added single-process (fp32/bf16, peak in first and last block) and TP=2 tests; on a one-hot row entropy and KL(p || p) are exactly 0 with zero gradient. The new tests fail without the fix.

jinyouzhi and others added 3 commits October 21, 2026 08:02
CUDA devices below compute capability 8 now take the eager path: a Triton
driver may load there although Triton does not support them. The XPU
accelerator still reports no Triton support, to keep DeepSpeed's other
Triton ops off, so XPU is enabled here directly.

When a row spans finite extremes such as -3e38 and 3e38, the difference of
two maxima overflows: the rescale underflows to 0 while the shift is
infinite, and entropy and KL became NaN. Terms with zero weight are now
skipped in the kernels and in the tensor-parallel re-centring, which leaves
every finite result unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
Use the same check as the vocab-parallel cross entropy: Triton installed,
the accelerator reports Triton support, and the logits live on it. XPU
falls back to the eager path until its accelerator enables Triton.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
@pengdurice
pengdurice enabled auto-merge October 21, 2026 14:55
@pengdurice
pengdurice added this pull request to the merge queue Oct 21, 2026
Merged via the queue into deepspeedai:master with commit 0b0d408 Oct 21, 2026
15 checks passed
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.

3 participants