feat(ep): add blockwise-FP8 combine to the AsyncLL (low-latency) path#465
Conversation
Blockwise-FP8 (E4M3, per-128-elem block max-scale) combine already exists
for the IntraNode/IntraNodeLL kernels but not for AsyncLL: the low-latency
combine only had the lossy direct-cast (`UseFp8DirectCast`, a plain bf16->e4m3
cast with a single fixed clamp and no per-block scale). Requesting
`Fp8BlockwiseQuant` on AsyncLL therefore raised
"only supports IntraNode/IntraNodeLL combine", forcing callers to either use
bf16 (full combine payload) or accept direct-cast's accuracy loss.
This ports the intranode blockwise path into the four AsyncLL combine kernels:
- SendCopy: quantize each token to [fp8 | f32 block-scales] via
WarpQuantizeToFp8Blockwise (bf16 T only).
- SendTransfer/RecvCopy: stride/slot accounts for the appended scale region.
- RecvCopy dequant-accumulate uses the fast vec8 path
(WarpAccumFp8Dequant{Full,Segment}BlockVec8Top8, block=128, AccumNum 8/9)
matching intranode, with a general-segment fallback for other shapes.
- Register `_bf16_fp8bwq` kernel variants (WRAP_BOOL2) and select them from
combine()/combine_recv(); relax the python guard to allow AsyncLL.
Same halved combine payload as direct-cast, at ~bf16-level accuracy.
Accuracy (DeepSeek-R1-MXFP4, gsm8k, AsyncLL combine, only combine dtype varies):
bf16 0.967 | blockwise-fp8 0.947 | direct-cast-fp8 0.873.
Round-trip parity covered by the extended AsyncLL dispatch/combine test
(quant_type="fp8_blockwise").
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Rita Brugarolas Brufau <rita.brugarolasbrufau@amd.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jhchouuu
left a comment
There was a problem hiding this comment.
LGTM overall, just left a couple of minor comments (dead template param on RecvTransfer + a top-9 test case).
Thanks for the nice work!
| /* ---------------------------------------------------------------------------------------------- */ | ||
|
|
||
| template <typename T, bool UseFp8DirectCast> | ||
| template <typename T, bool UseFp8DirectCast, bool UseFp8BlockwiseQuant = false> |
There was a problem hiding this comment.
seems UseFp8BlockwiseQuant is not used on EpCombineLowLatencyAsyncRecvTransfer_body...
There was a problem hiding this comment.
Good catch — removed. EpCombineLowLatencyAsyncRecvTransfer_body is the barrier and never reads the flag; only SendCopy/SendTransfer/RecvCopy have _fp8bwq variants. Dropped it back to template <typename T, bool UseFp8DirectCast>. No functional change
| @pytest.mark.parametrize("num_experts_per_rank", (32,)) | ||
| @pytest.mark.parametrize("num_experts_per_token", (8,)) | ||
| @pytest.mark.parametrize("quant_type", ("none", "fp8_direct_cast")) | ||
| @pytest.mark.parametrize("quant_type", ("none", "fp8_direct_cast", "fp8_blockwise")) |
There was a problem hiding this comment.
could you help add 9 to num_experts_per_token @pytest.mark.parametrize("num_experts_per_token", (8, 9)) for test, ty!
There was a problem hiding this comment.
@jhchouuu Added num_experts_per_token=(8, 9), and top-9 now actually runs. The test's dispatch-result positional check (check_dispatch_result) doesn't support non-multiple-of-8 top-k, so for those cases the test validates the dispatch via the combine round-trip instead (a small _AsyncLLCombineOnlyTestCase that skips only the dispatch-side positional check). The dispatch data is correct at top-9 — the combine round-trip reconstructs it and production runs top-9 fine (gsm8k 0.967/0.947) — so nothing is dropped functionally: top-9 exercises the full dispatch+combine including the AccumNum=9 path, and top-8 keeps the full check.
…mbine - low_latency_async.cpp: remove the unused UseFp8BlockwiseQuant template parameter from EpCombineLowLatencyAsyncRecvTransfer_body. That kernel is the barrier (quiet + crossDeviceBarrier) and never reads the flag; only SendCopy/SendTransfer/RecvCopy have _fp8bwq variants. No functional change. - test_dispatch_combine_async_ll.py: add num_experts_per_token=9 to test_dispatch_combine so the vec8 AccumNum=9 combine path is covered. The dispatch-result positional check (check_dispatch_result) does not support non-multiple-of-8 top-k, so those cases validate the dispatch via the combine round-trip only (a _AsyncLLCombineOnlyTestCase that skips the dispatch-side positional check). The dispatch data itself is correct at top-9 -- the combine round-trip reconstructs it and production runs top-9 fine. top-8 keeps the full dispatch+combine check. Signed-off-by: Rita Brugarolas Brufau <rita.brugarolasbrufau@amd.com>
# Conflicts: # python/mori/ops/dispatch_combine.py
Title
feat(ep): add blockwise-FP8 combine to the AsyncLL (low-latency) path
Motivation
Blockwise-FP8 (E4M3, per-128-elem block max-scale) combine already exists for the
IntraNode / IntraNodeLL kernels but not for AsyncLL. The low-latency combine only had the
lossy direct-cast fp8 (
UseFp8DirectCast— a plain bf16→e4m3 cast with a single fixed clamp andno per-block scale). So requesting
Fp8BlockwiseQuanton AsyncLL raisedFp8BlockwiseQuant currently only supports IntraNode/IntraNodeLL combine, forcing callers to eitheruse bf16 (full combine payload) or accept direct-cast's accuracy loss on the async path.
This extends the existing intranode blockwise path to the four AsyncLL combine kernels. It is an
extension of an existing MoRI feature to a new kernel path — not a new algorithm.
What changed
src/ops/dispatch_combine/low_latency_async.cpp— addbool UseFp8BlockwiseQuantto the 4 combinekernels:
[fp8 token | f32 block-scales](
WarpQuantizeToFp8Blockwise, bf16 T only).(
WarpAccumFp8Dequant{Full,Segment}BlockVec8Top8, block=128, AccumNum 8/9), matching intranode,with a general-segment fallback for other shapes.
src/ops/kernels/ep_async_ll.hip— register_bf16_fp8bwqvariants of SendCopy/SendTransfer/RecvCopy.src/ops/kernels/ep_common.hip— addWRAP_BOOL2macro (2 template bools).python/mori/ops/dispatch_combine.py— select the_fp8bwqkernels incombine()/combine_recv();relax the guard to allow AsyncLL. (Shared-mem already accounts for the recv-side scale-ptr array.)
tests/python/ops/test_dispatch_combine_async_ll.py— addfp8_blockwiseto the AsyncLLdispatch+combine round-trip parametrization (bf16-only skip guard).
Why the same payload as direct-cast but accurate
Both fp8 modes move ~½ the bf16 combine payload (14336 → 7392 B/token @ hidden 7168). Direct-cast
uses one fixed scale across the whole vector → it flushes small per-channel values to E4M3
subnormals/zero on real activations. Blockwise rescales each 128-elem block → preserves them.
Evidence
End-to-end gsm8k, DeepSeek-R1-MXFP4, AsyncLL combine, only the combine dtype varies:
→ blockwise ≈ bf16 accuracy at half payload; direct-cast costs ~7 pp for the same payload.
Correctness: extended AsyncLL round-trip test passes for
fp8_blockwise(dispatch+combinereconstruction, 8 ranks). The AsyncLL blockwise output is bit-faithful to the production intranode
blockwise combine on shared synthetic data.
How it was measured
Accuracy (gsm8k table under "Evidence")
Full SGLang server, DeepSeek-R1-MXFP4, one server per combine dtype (only
SGLANG_MORI_COMBINE_DTYPEchanges), split path (IntraNode dispatch + AsyncLL combine), no SDMA, cuda graph off:
Notes: top-9 effective (8 routed + 1 fused shared,
enforce-shared-experts-fusion) → exercises thevec8 AccumNum=9 path. bf16 was run at
--num-questions 150 --parallel 100(bf16 combine tail-hangs atparallel 300; accuracy is concurrency-independent). direct-cast & blockwise at 300/100.
Testing
SGLang — no change needed
SGLang already requests blockwise on the MoRI-EP path:
moriep.pymapsSGLANG_MORI_COMBINE_DTYPE=fp8 → combine_quant_type="fp8_blockwise"(upstream,AMD MoRI-EP integration, PR #29855). Pre-PR, AsyncLL rejected that request; this PR makes it honor
it. All logic (kernels + kernel selection + guard) is inside MoRI.