feat(vllm): op-injection plugin + decode MoE experts kernel (1.28x at small M) - #43
feat(vllm): op-injection plugin + decode MoE experts kernel (1.28x at small M)#43jiejingzhangamd wants to merge 8 commits into
Conversation
…entry_points (#40) Out-of-tree vLLM plugin (no fork) that will inject Infera/HyperLoom kernels: - entry_points: vllm.platform_plugins (register_platform) + vllm.general_plugins (register_ops); no-op unless ROCm is present and INFERA_VLLM_OPS_DISABLE != 1. - Attention seam: InferaPlatform(RocmPlatform).get_attn_backend_cls (pass-through). - MoE seam: install_moe_ops() — FusedMoEModularKernel experts, pass-through stub. Validated on the vLLM ROCm image: vLLM discovers `infera` in both plugin groups and `import vllm` is clean with the default install. Platform activation is opt-in (INFERA_VLLM_OPS_PLATFORM=1) pending an import-safety fix: activating the RocmPlatform subclass here re-enters vllm.platforms during its eager current_platform resolution and circular-imports. Planned fix (#40): move the attention seam to a register_ops monkey-patch of get_attn_backend_cls (ATOM's MLA approach), keeping the platform subclass only if made import-safe. Signed-off-by: Zhang, Jiejing <jiejing.zhang@amd.com>
…gin patch (#40) Replace the platform-subclass activation (which circular-imported during vLLM's eager current_platform resolution) with a single vllm.general_plugins hook that patches the resolved platform after vllm.platforms is initialized: - register_ops installs both seams; drop the vllm.platform_plugins entry point and InferaPlatform subclass. - attention.py: install_attention_ops() monkey-patches get_attn_backend_cls on the current platform — pass-through by default, returns INFERA_ATTN_BACKEND ("module:Backend") when set. ATOM's MLA-patch style, but import-safe. - moe.py: MoE experts seam unchanged (pass-through stub). Validated on the vLLM 0.23 ROCm image: `import vllm` is clean; the general plugin is discovered and run; the attention seam patches RocmPlatform and the override path returns the injected backend. No circular import, no fork. Signed-off-by: Zhang, Jiejing <jiejing.zhang@amd.com>
#40) install_moe_ops() now patches the MoE modular-kernel compute methods (FusedMoEKernel.apply / apply_monolithic in vLLM 0.23) with a delegating pass-through — the single point where an Infera/HyperLoom experts kernel replaces the default while keeping vLLM's routing / quant / EP-DP dispatch. INFERA_MOE_EXPERTS is reserved for selecting a custom impl. Defensive against MoE-API churn: no-ops with a warning if the kernel class/methods aren't found. Validated on the vLLM 0.23 ROCm image: load_general_plugins() installs both seams; FusedMoEKernel.apply and apply_monolithic are wrapped; import stays clean. Signed-off-by: Zhang, Jiejing <jiejing.zhang@amd.com>
#40) Turn the MoE seam into a measurable optimize loop: - infera_fused_experts(): the plugin's swappable experts op, same signature as vLLM fused_experts. A variant registry (@register_experts_variant) lets a custom/HyperLoom kernel be selected via INFERA_MOE_EXPERTS; empty by default, so the op is a bitwise pass-through to the built-in (which on ROCm already dispatches to aiter). Falls back to built-in on any failure. - bench/op_loop/moe_experts_loop.py: measures built-in (baseline), plugin op, and a pure-torch reference (correctness oracle) on identical inputs at Kimi-2.6 MoE dims (E=384, H=7168, I=2048, top_k=8) — median latency + max abs/rel error. Measured on MI355X (vLLM 0.23 ROCm): built-in experts = 5.81 ms/call at Kimi-2.6 dims; plugin op is a verified pass-through (0 diff); the torch reference matches the built-in within bf16 tolerance (rel 5e-3) and the harness detects a 10x latency gap vs the reference. The loop is ready: a real kernel is one @register_experts_variant + re-run. Signed-off-by: Zhang, Jiejing <jiejing.zhang@amd.com>
…at small M (#40) infera_decode: a small-M SwiGLU experts variant (@register_experts_variant, INFERA_MOE_EXPERTS=infera_decode) — two Triton GEMV kernels (fp32 accumulate, no tl.dot): - gate/up + SiLU fused, expert-id-indexed weight reads (no gather/scatter), - down-projection fused with the router-weighted top-k combine (no atomics). Reads each selected expert's weights once per (token, slot) pair — the traffic floor for decode — hitting ~5.2 TB/s effective HBM after tuning (block sizes env-tunable via INFERA_MOE_*). A dispatch guard delegates to the builtin where the kernel would lose or be unsafe: T > 16 or 2*T*topk > E (collision-heavy / large batch), non-bf16/fp16 weights (so Kimi MXFP4 fp4-packed weights delegate), or expert_map / non-SiLU / apply_router_weight_on_input. So it never runs where it loses. Measured (MI355X, vLLM 0.23 ROCm, Kimi-2.6 dims E=384 H=7168 I=2048 top_k=8, bf16): T=1 1.28x (0.173->0.135 ms) | T=8 1.09x | T=16 1.07x | T>16 delegates (1.00x, 0 diff) rel err vs builtin <= 5e-3 (below the fp32 torch oracle's own error vs builtin). Independently re-verified via bench/op_loop/moe_experts_loop.py. Signed-off-by: Zhang, Jiejing <jiejing.zhang@amd.com>
#40) Add the two missing rings so it's a real profile → tune → inject → re-profile loop: - bench/op_loop/profile_op.py: roofline profile of an experts impl — achieved HBM BW vs peak → bandwidth-bound (near optimal) vs launch/occupancy-bound (headroom); --kernels adds the per-kernel device-time split (torch profiler). - bench/op_loop/tune_op.py: coordinate-descent autotune of the infera_decode block/warp config (keeps only correct configs, rel < 1e-2); --inject rewrites _TUNE_DEFAULTS in the plugin (the inject step) so re-profile picks it up. - moe.py: _decode_tunables() precedence per-key env > INFERA_MOE_TUNE_FILE (JSON) > baked _TUNE_DEFAULTS, so the loop drives config without code edits. - bench/op_loop/README.md: the loop, with commands. Demonstrated one full cycle on MI355X (Kimi-2.6 dims, decode T=1): profile → bandwidth-bound at 5.1 TB/s (64% of HBM peak), gate/up kernel dominates (64%); tune --inject found DN_WARPS 4->8 and baked it in (1.28x -> 1.31x); re-profile confirmed 5.23 TB/s. _TUNE_DEFAULTS updated to the tuned (32,512,8,8,512,8). Signed-off-by: Zhang, Jiejing <jiejing.zhang@amd.com>
) Make the MoE seam actually run the selected variant in a real serve (it was a pass-through), and add a weight-layout gate so it's safe regardless of the aiter flag: - _make_seam intercepts the modular FusedMoEKernel.apply: when a variant is selected and the request is one it supports, run it and return the combined [T,H] output; otherwise delegate to the untouched original. - Self-gating (never a regression, never garbage): delegates unless plain (non-aiter-shuffled) bf16/fp16 weights + small M + unquantized + no expert_map/EP + no shared-expert overlap + modular apply. The key gate is _weights_are_aiter_shuffled() — aiter pre-shuffles MoE weights in-place with no per-tensor marker, so we read the aiter master flag (INFERA_MOE_ASSUME_PLAIN=1 overrides). This is what the earlier "garbage with aiter on" was; now it cleanly delegates. - A fire counter (INFERA_MOE_DECODE_DEBUG) proves the kernel actually runs in a serve, not silently delegates. - bench/_moe_decode_e2e_{serve.sh,client.py}: single-stream batch-1 decode ITL. E2E (Qwen3.5-35B-A3B bf16 MoE, MI355X, TP=1, batch-1 decode), independently reproduced (5 reps, no overlap): aiter-off + infera_decode = 5.26 ms/tok vs the aiter-on production default 5.75 ms/tok — the fastest config, +8.5% ITL / +9% tok/s (aiter gives no batch-1 decode benefit here, so disabling it is free and the MoE kernel then wins). Confined to bf16 / batch<=16 / plain weights, all self-delegating. Details in bench/op_loop/README.md. Signed-off-by: Zhang, Jiejing <jiejing.zhang@amd.com>
Wired into the serve path + end-to-end decode win (09c4369)The MoE seam now actually runs Weight-layout state is now on the interface ( E2E — Qwen3.5-35B-A3B (bf16 MoE), MI355X, TP=1, batch-1 decode (independently reproduced, 5 reps, no overlap; a fire counter confirms the kernel ran):
Limitations (all self-delegating — never a regression): bf16/fp16 only (MXFP4/Kimi delegates), batch ≤ 16, plain weight layout (aiter-shuffled delegates). Mixing aiter-prefill + this decode kernel needs weight un-shuffling — future work. |
Generalize the loop from three MoE-specific scripts into a reusable scaffold —
the point is the framework, not any one kernel:
- framework.py: OpSpec + registry + generic measure/profile/tune. An op declares
make_inputs / baseline / candidate / reference / traffic_bytes / tune_*; the
loop drives all three rings from that, skipping whatever an op doesn't provide.
- loop.py: one CLI over any registered op — `loop.py {measure,profile,tune,list}
--op <name> [-d k=v] [--kernels] [--inject]`.
- ops/moe_experts.py: the MoE spec (the worked example / template). Adding an op
is now writing one OpSpec file + register_op — no new script.
- Remove moe_experts_loop.py / profile_op.py / tune_op.py (folded in).
- README rewritten around the scaffold + an "adding an op" table.
Re-validated via the new CLI on MI355X (Kimi-2.6 dims, T=1): measure = 1.29x
candidate vs builtin (oracle rel 3.9e-3), profile = bandwidth-bound at 5.1 TB/s
(64% peak), tune = 1.33x. infera_decode is unchanged — just the candidate the
moe_experts spec points at.
Signed-off-by: Zhang, Jiejing <jiejing.zhang@amd.com>
Closes the first loop of #40: inject custom Attention/MoE kernels into stock vLLM out-of-tree (no fork), plus a first real optimized kernel with a measured win.
Mechanism (no fork)
A single
vllm.general_pluginsentry point (register_ops) installs both seams aftervllm.platformsinitializes, so it's import-safe (aplatform_pluginssubclass circular-imports during vLLM's eagercurrent_platformresolution). No-op unless ROCm +INFERA_VLLM_OPS_DISABLE != 1.get_attn_backend_clson the resolved platform (pass-through;INFERA_ATTN_BACKEND="mod:Backend"injects a custom backend). ATOM's MLA-patch style, import-safe.infera_fused_experts(same signature as vLLMfused_experts) with a variant registry (@register_experts_variant,INFERA_MOE_EXPERTS=name); default is a bitwise pass-through to the builtin.Optimize loop
bench/op_loop/moe_experts_loop.pymeasures builtin (baseline) vs plugin op vs a pure-torch reference (correctness oracle) at a model's real MoE dims. A new kernel is one@register_experts_variant+ re-run.First real kernel:
infera_decodeA small-M SwiGLU experts kernel — two Triton GEMV kernels (fp32 accumulate, expert-id-indexed weight reads, fused gate/up/SiLU and down+router-combine, ~5.2 TB/s effective HBM). A dispatch guard delegates to the builtin where it'd lose or be unsafe (
T>16or2·T·topk>E; non-bf16/fp16 weights ⇒ Kimi MXFP4 safely delegates;expert_map/non-SiLU/router-on-input).Measured — MI355X, vLLM 0.23 ROCm, Kimi-2.6 dims (E=384, H=7168, I=2048, top_k=8, bf16):
A decode-step specialist (the latency-critical, token-by-token regime), not an aiter replacement — safe by construction everywhere else. rel err is below the fp32 torch oracle's own error vs the builtin.
Validation
import vllmclean; both seams install viaload_general_plugins; results independently re-verified.ruffclean. Full progression + negative-result notes in #40.