Skip to content

feat(vllm): op-injection plugin + decode MoE experts kernel (1.28x at small M) - #43

Draft
jiejingzhangamd wants to merge 8 commits into
mainfrom
feat/vllm-op-injection
Draft

feat(vllm): op-injection plugin + decode MoE experts kernel (1.28x at small M)#43
jiejingzhangamd wants to merge 8 commits into
mainfrom
feat/vllm-op-injection

Conversation

@jiejingzhangamd

Copy link
Copy Markdown
Collaborator

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_plugins entry point (register_ops) installs both seams after vllm.platforms initializes, so it's import-safe (a platform_plugins subclass circular-imports during vLLM's eager current_platform resolution). No-op unless ROCm + INFERA_VLLM_OPS_DISABLE != 1.

  • Attention — patches get_attn_backend_cls on the resolved platform (pass-through; INFERA_ATTN_BACKEND="mod:Backend" injects a custom backend). ATOM's MLA-patch style, import-safe.
  • MoEinfera_fused_experts (same signature as vLLM fused_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.py measures 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_decode

A 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>16 or 2·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):

tokens baseline ms infera_decode ms speedup rel err
1 0.173 0.135 1.28× 2.4e-3
8 0.959 0.878 1.09× 4.9e-3
16 1.767 1.657 1.07× 4.9e-3
64 4.351 4.349 1.00× (delegated) 0
512 5.815 5.810 1.00× (delegated) 0

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 vllm clean; both seams install via load_general_plugins; results independently re-verified. ruff clean. Full progression + negative-result notes in #40.

…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>
@jiejingzhangamd

Copy link
Copy Markdown
Collaborator Author

Wired into the serve path + end-to-end decode win (09c4369)

The MoE seam now actually runs infera_decode in a real serve (it was a pass-through), self-gating so it's never a regression.

Weight-layout state is now on the interface (_weights_are_aiter_shuffled): aiter pre-shuffles MoE weights in-place with no per-tensor marker, so the seam reads the aiter master flag and delegates when weights are shuffled — this is what the earlier "garbage with aiter on" was; it now cleanly delegates to the builtin instead. INFERA_MOE_ASSUME_PLAIN=1 overrides. Verified: aiter-on → delegate (correct), aiter-off → kernel runs.

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):

config ITL (ms/tok) decode tok/s
aiter on — production default 5.754 173.3
aiter off — builtin (triton) 5.650 176.7
aiter off — infera_decode 5.264 188.8

infera_decode is the fastest config — +8.5% ITL / +9% tok/s over the aiter-on default. At batch-1 aiter gives no decode benefit here (slightly negative), so turning it off is free and the MoE kernel then wins. The op-level ~1.3× dilutes to single-digit e2e because routed experts are ~⅓ of the bandwidth-bound batch-1 decode step.

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>
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.

1 participant