Conversation
The plan broadcast stored only 4 * (3 * E * R // 4) int32 elements, so any world size whose broadcast length is not a multiple of 4 lost the trailing entries -- and api.py rejected exactly those shapes up front with an assert, which is why odd EP sizes, R = 1 included, could not construct a Buffer. Store the trailing 3 * E * R % 4 elements with a scalar multimem store and drop the assert. The tail is required, not cosmetic. Only rank 0 publishes, and only to peer chunks: every rank reads ALLOC | TPE | EOFF from its own chunk (plo = rank * ms + PLAN_OFF), while CU | ZFR | ETC | STATS are read straight from rank 0's chunk and are not replicated. So this loop is the only writer of a peer chunk's plan and nothing republishes an element the vector body leaves behind; the dropped entries are the last expert offsets, i.e. the top experts' destination offsets, in every peer chunk. At R = 1 there is no peer chunk and the plan already sits in rank 0's own chunk, so the publish is a pure self-copy and is skipped. Tests: the publish index model covers every element exactly once for nb in [1, 256) across several worker counts, keeps the tail length reachable at R = 1/3, and pins the pre-fix coverage as a negative control.
0z5a
marked this pull request as draft
September 19, 2026 16:01
Author
|
Draft: the fix is verified down to the compiled kernel, but the multimem tail has not been executed on hardware yet. Verified so far:
Not yet run: the planning/dispatch/combine tests at odd |
Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
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.
Incremental review: Files changed against
master.Refs #16.
What
PlanningKernelbroadcastsALLOC | TPE | EOFF(3 * E * Rint32) from rank 0 into every peer chunk with a vectorized multimem body, which stored only4 * (3 * E * R // 4)elements. A world size whose broadcast length is not a multiple of 4 therefore lost the trailing entries, andapi.pyrejected exactly those shapes withassert broadcast_elems % 4 == 0--R = 1included.This stores the trailing
3 * E * R % 4elements with a scalarmultimem.st.b32and drops the assert.Why the tail store is required (the open question in #16)
Only rank 0 publishes, and it publishes into peer chunks. The two sides of the plan disagree about where it is read from:
ALLOC | TPE | EOFF(first3 * E * Rint32)planning.py:649/721/815), then replicated by this publishplo = rank * ms + PLAN_OFF:tpe_cumsum_view,alloc_cumsum_view,expert_off_view)CU | ZFR | ETC | STATScu_src_begin = PB + CU_SUB + rank * pd_group_count + ...,remote_stats)No other path republishes the peer copies, so the elements this loop drops stay stale in every peer chunk -- and they are the last expert offsets, i.e. the top experts' destination offsets. The scalar tail is therefore required; relaxing the assert alone would be wrong for odd
R > 1.At
R = 1the multicast view maps rank 0's own chunk, which already holds the plan, so the publish is a pure self-copy and is now skipped undercutlass.const_expr(R > 1).Tests
tests/test_planning.pygains an index model of the publish:[0, nb)is written exactly once and the vector body never overlaps the scalar region, fornb in [1, 256)andworkers in {1, 8, 32, 128, 256, 512};nb % 4stays reachable atR = 1andR = 3for the existingPLANNING_CASES;nb % 4elements.The distributed planning/dispatch/combine/e2e tests that already exist for those shapes now construct a
Bufferinstead of aborting inapi.py.Verification performed
pytest tests/test_planning.py -q: 3 passed, 18 skipped;Bufferconstruction atR = 1for the shapes the old assert rejected (nb = 3, 9, 6);sm_90atnb = 3,36,54;nb % 4 == 0emits no scalar store,nb = 54emits one,R = 1emits none.Limitation: the machine used for this work has no SM90 device (the kernels refuse
sm_89at trace time) and no multicast support, so the multimem tail was compiled but not executed. The distributed tests above are what exercise it end to end on supported hardware.Speed comparison
The tail adds work only where
3 * E * R % 4 != 0; every other shape compiles to the pre-patch instruction stream (byte-identical SASS), so the table below is the whole cost of the fix.Method: 9 paired
APPAblocks per shape, 20000 launches per arm, grid 1 and 32 blocks x 512 threads, shapes fromR in {1,2,3,4,8}xepn in {1,2,3,4,5,7}. One launch is ~1.9 us, so the tail is below the harness resolution: under 1.7% and with no consistent sign. The tail-free group is the null control -- its true delta is exactly zero, and it shows the largest spread, so none of the residual is attributable to this patch.Two further points from the same run:
nbproduces an instruction-identical kernel (nbis acutlass.const_expr, so the guarded loop is compiled away); a tail-carryingnbadds exactly one load and one store, the rest being loop control.R = 1andR = 3,epn in {1,2,3}the old code raisedAssertionError: broadcast_elems (3/6/9/27/54/81) must be divisible by 4before any CUDA work; the patched tree reaches the allocator and stops only at the hardware capability check.Caveat: this is an instruction-shape proxy measured on an sm_89 host --
st.global.v4.s32/st.global.s32in place ofmultimem.st, which ptxas rejects belowsm_90. It measures instruction and store cost, not multicast fan-out semantics, and it is not a substitute for the end-to-end numbers the distributed lane will produce on supported hardware.Test and benchmark sources are retained locally; the validation results below refer to those local files.