feat(moe): support HIP and Triton backends for moe_permute, default to Triton - #455
feat(moe): support HIP and Triton backends for moe_permute, default to Triton#455zhenhuang12 wants to merge 8 commits into
Conversation
GroupedGemmFunc's backward took a dense gemm_impl path when len(group_lens) == 1. That reduces over all M rows of `a`, but under the sync-free MoE path `a` is worst-case allocated and its tail rows are never written -- the dense GEMM folds that uninitialized memory into grad_b. Route the single-group case through grouped_gemm_variable_k_impl like the multi-group branch already does, so the reduction stops at group_offs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
moe_permute/moe_unpermute gain a `backend` argument selecting between the HIP C++ extension and new Triton kernels. Both backends consume the same HIP-built row_id_map, so the Triton path is a drop-in swap of the scatter/ gather kernels only -- preprocessing is unchanged. `backend` defaults to "triton"; pass "hip" to fall back to the extension. DeepEPTokenDispatcher no longer needs to request the Triton path explicitly. Adds permute_with_hip_row_map / unpermute_with_hip_row_map to the Triton moe permutation module, a standalone benchmark, and tests that run both backends and assert the Triton results match HIP for topk probs, both expert_map dtypes, and padded and unpadded layouts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
moe_permute no longer computes and returns a dispatched-token count; the row bound is whatever DeepEP already knows, so the return tuple drops from 7 to 6 values and the strided row_id_map[:, -1] read is gone. - Route num_dispatched_tokens through the dispatcher instead of the kernel. - Cache the device-side row bound in DeepEPTokenDispatcher: DeepEP's recv height is stable, so the steady state stops allocating a torch.full per layer per step, and moe_unpermute's backward reuses the same tensor. - Drop the dead permute_with_hip_row_map / unpermute_with_hip_row_map paths and make backend auto-selection symmetric between permute and unpermute. - Move the triton permute and indices-converter impls under kernels/moe and fix the stale paths in LICENSE. Verified in-container on 8x MI355: 97 passed / 13 skipped for test_moe_permute.py + test_token_dispatcher.py, 36 passed for the router and mask tests, 3 passed for fused_mega_moe. A profiler sweep over a steady-state dispatch/combine/backward reports D2H=0 H2D=0 for TRITON pad=0 (host and cuda counts) and TURBO pad=128, with a positive control confirming the counter sees real copies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wrap the three moe_permute stages (preprocess / permute / unpermute) in torch.library.custom_op with explicit schema strings -- infer_schema cannot express the optional tensor returns -- plus register_fake meta impls so the whole roundtrip captures as a single graph. The preprocess op no longer passes ``probs`` straight through: a custom op may not return one of its own inputs. Backends now hand back ``multihot_probs``, non-None only when TRITON actually converted the top-k probs, and the python wrapper resolves it, keeping the public 8-tuple contract unchanged. Guard ``ctx.set_materialize_grads`` behind ``is_compiling()`` in both autograd Functions; dynamo cannot trace that ctx method and would break the graph. Eager and compiled forward/backward stay bit-identical. Tests: 84 passed / 13 skipped in tests/pytorch/ops/test_moe_permute.py (4 new torch.compile cases), 15 passed in test_token_dispatcher.py. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An idle high-priority pool stream still consumes one of GPU_MAX_HW_QUEUES. When force_current_stream is set, comm_stream is never used, so alias it to the caller's current stream instead of reserving a hardware queue for nothing. The env is read directly here because force_current_stream is declared after comm_stream and is not initialised yet at this point in the member init list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Triton-vs-HIP permute comparison served the backend bring-up and is not part of the shipped surface; nothing references it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…dding - treat num_permuted_tokens == 0 as a valid caller-provided bound - return 0 permuted rows in the meta kernel when there are no tokens - keep indices_position_map alive via save_for_backward - add the pad_multiple slack to the dispatcher permute bound - validate num_topk against topk_indices width and token_indices shape - reject deepep_num_worst_tokens without cuda tokens-per-expert - forward use_fp8 into the moe_unpermute default backend choice Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR refactors and extends the PyTorch MoE permute/unpermute path to support selecting between HIP (“TURBO”) and a new Triton backend, defaulting to Triton for the unpadded/non-FP8 case, and adds torch.compile traceability via torch.library custom ops.
Changes:
- Adds Triton-backed permute/unpermute implementation and backend selection logic (with TURBO retained for padding/FP8/capacity-related behavior).
- Updates DeepEP token dispatcher to reuse DeepEP-provided token counts and tightens input/shape validation.
- Updates and expands unit tests to cover both backends, padding/capacity edge cases, and torch.compile tracing behavior; removes/relocates older MoE permutation utilities under
pytorch/kernels/moe/.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/pytorch/ops/test_moe_permute.py | Expands coverage across TURBO/TRITON backends, adds compile and edge-case tests, updates API expectations. |
| tests/pytorch/modules/test_token_dispatcher.py | Improves multi-rank failure handling and strengthens dispatcher correctness checks (counts, padding alignment). |
| primus_turbo/triton/moe/permutation.py | Formats/adjusts Triton permutation kernels used by the TRITON backend. |
| primus_turbo/pytorch/ops/moe/permutation.py | Deletes legacy MoE permutation ops (moved/replaced by kernel backend layer). |
| primus_turbo/pytorch/ops/moe/moe_permute.py | Introduces backend selection + new API surface, routes to backend implementations, adds compile-friendly handling. |
| primus_turbo/pytorch/ops/moe/indices_converter.py | Deletes legacy indices converter (replaced by fused kernel impl). |
| primus_turbo/pytorch/ops/moe/init.py | Stops exporting removed legacy modules. |
| primus_turbo/pytorch/ops/grouped_gemm.py | Fixes single-group backward grad_b computation to avoid folding uninitialized tail rows. |
| primus_turbo/pytorch/modules/moe/token_dispatcher.py | Reuses DeepEP host counts, adjusts capacity sizing logic, tightens token_indices shape checks. |
| primus_turbo/pytorch/kernels/moe/moe_permute_impl.py | New backend abstraction + torch.library custom ops for preprocess/permute/unpermute (TURBO + TRITON). |
| primus_turbo/pytorch/kernels/moe/fused_moe_indices_converter_impl.py | New fused topk<->multihot converter kernel implementation for TRITON paths. |
| primus_turbo/pytorch/kernels/moe/init.py | Exposes new MoE kernel backend APIs. |
| LICENSE | Updates third-party attribution paths after file moves. |
| csrc/pytorch/deep_ep/deep_ep.cpp | Avoids reserving an idle HW queue by selecting current stream when force_current_stream is enabled. |
Suppressed comments (1)
primus_turbo/pytorch/ops/moe/moe_permute.py:247
_MoEUnpermute.forwardcallsctx.save_for_backward(row_id_map, num_dispatched_tokens_tensor), butnum_dispatched_tokens_tensoris optional and is typicallyNoneat call sites.save_for_backwardcannot acceptNone, somoe_unpermutewill raise at runtime.
Persist the optional bound as a ctx attribute and only save_for_backward the row_id_map tensor.
ctx.save_for_backward(row_id_map, num_dispatched_tokens_tensor)
return outputs
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| ctx.save_for_backward(row_id_map, num_dispatched_tokens, indices_position_map) | ||
| return ( |
Formatting only, no behavior change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
primus_turbo/pytorch/ops/moe/moe_permute.py:330
moe_permutecurrently allows bothrouting_mapandtopk_indicesto be passed. The backends resolve this differently (TURBO prefers topk_indices; TRITON prefers routing_map), which can change results depending on backend selection. Reject ambiguous calls by requiring exactly one of the two inputs.
if routing_map is None and topk_indices is None:
raise ValueError("moe_permute: one of routing_map / topk_indices must be provided")
| # Must be an upper bound: the kernel stores at dst row without clamping. | ||
| if num_permuted_tokens >= 0: | ||
| num_permuted = int(num_permuted_tokens) | ||
| else: | ||
| num_permuted = int(tokens_per_expert.sum().item()) |
Description
moe_permute/moe_unpermutepreviously only had the HIP extension path. This PR makes thebackend selectable between HIP (
BackendType.TURBO) and a new Triton one(
BackendType.TRITON), defaults to Triton for the plain (non-padded, non-FP8) case, and makesthe permute roundtrip traceable under
torch.compile.Fixes AIMA-219, AIMA-220
Type of change
Changes
Please list the changes introduced in this PR:
backendargument selecting between HIP andTriton. Padding and FP8 still go to HIP; everything else defaults to Triton. Permute and
unpermute must use the same backend.
torch.library.custom_opwithregister_fakemeta impls, so theroundtrip captures as a single graph.
moe_permutetakes keyword-onlyrouting_map/topk_indicesinstead ofexpert_map, and no longer returnsnum_dispatched_tokens(7 → 6 values). The row bound nowcomes from DeepEP through the dispatcher.
tokens_per_expertinstead of forcing a seconddevice sync, and tighten a few argument checks.
GroupedGemmFunc.backwardfolding uninitialized worst-case tail rows intograd_bfor thesingle-group case, plus several permute edge cases (zero tokens, padding,
num_topk).deep_ep.cppreserving an idle hardware queue whenforce_current_streamis set.pytorch/kernels/moe/and drop dead code.Checklist: