[MoE] Allow reusable workspaces in XPU fused MoE#392
Conversation
Signed-off-by: Eric Hartford <eric@quixi.ai>
297e6af to
f26ed94
Compare
There was a problem hiding this comment.
Pull request overview
This PR adds optional caller-provided scratch workspaces to the XPU fused MoE kernel path (XpuFusedMoe.apply() / _apply_kernel()) to avoid repeated large temporary allocations during decode-heavy inference, while preserving the existing allocation behavior when workspaces are omitted.
Changes:
- Extends
XpuFusedMoe.apply()/_apply_kernel()with optionalworkspace13andworkspace2and uses views into them for major scratch tensors when provided. - Adds a kernel-path guard for invalid
num_experts <= 0. - Expands fused MoE tests (including EP mini-scope entries) and adds a regression test comparing workspace reuse vs the allocation path.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
vllm_xpu_kernels/fused_moe_interface.py |
Adds optional workspace parameters and workspace-backed views for scratch tensors in the kernel MoE path. |
tests/fused_moe/test_fused_moe.py |
Adds a regression test for workspace reuse equivalence and extends mini-scope parameters for EP variants. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def _workspace_view(workspace, shape, name): | ||
| required = math.prod(shape) | ||
| if workspace.numel() < required: | ||
| raise ValueError( | ||
| f"{name} has {workspace.numel()} elements, but {required} " | ||
| f"are required for shape {shape}") | ||
| return workspace.view(-1)[:required].view(*shape) | ||
|
|
| w13.data = w13.transpose(-1, -2).contiguous() | ||
| w2.data = w2.transpose(-1, -2).contiguous() |
| num_moe_inputs = input_len * topk | ||
| workspace13 = torch.empty((num_moe_inputs, | ||
| max(hidden_size, intermediate_size)), | ||
| device=DEVICE, dtype=dtype) | ||
| workspace2 = torch.empty((num_moe_inputs, | ||
| max(hidden_size, 2 * intermediate_size)), | ||
| device=DEVICE, dtype=dtype) |
|
Num_moe_inputs is a dynamic number. |
|
@mayuyuace can you investigate whether the memory can be re-used across layers in the single iteration? |
|
@xinyu-intel |
is it possible to allocate/release workspace memory once per iteration instead of per layer? |
|
@xinyu-intel |
I think no. |
you're right, we do see aten::fill host/device call in the trace of each moe layer. Though the device time is very limited, the host call and device bubble cause the decode overhead. |
I mean per iteration, a.k.a (num_tokens, hidden_size), it should be known at the beginning of the iteration. |
I see what you mean: you want to allocate a block of memory before each step starts and have every layer reuse it. I think this is feasible, but the change would need to be implemented within vLLM itself; it cannot be achieved solely at the kernels level. |
exactly. to check if vLLM modular MOE design can support this. |
|
just try running qwen3.6-35b with and without this patch. shrug not sure what the debate is about. |
Summary
Fixes #390.
XpuFusedMoe.apply()currently allocates several large scratch tensors on every kernel-path call:In decode-heavy MoE inference this creates avoidable allocator churn, since the required workspace shapes are deterministic from
hidden_states.shape,n_experts_per_token,hidden_size, andinter_size.This PR adds optional caller-provided workspaces to the XPU fused MoE kernel path. Existing callers keep the current allocation behavior when workspaces are omitted, while optimized callers can preallocate scratch buffers and reuse them across repeated decode steps.
Changes
workspace13andworkspace2parameters toXpuFusedMoe.apply()/_apply_kernel().num_experts <= 0in the kernel path.Compatibility
The API change is additive. Existing callers do not need to pass any new arguments and should see the same behavior as before.
The workspace buffers are scratch only. The implementation reuses each workspace only after the previous view backed by that workspace has been consumed:
workspace13: remapped hidden states, then activation outputworkspace2: GEMM1 output, then GEMM2 outputTest Plan
Built this branch independently from
origin/mainin a clean worktree with:Import smoke:
Focused fused MoE tests:
Test Result
Related Work
There is nearby MoE refactor work in #376. This PR is intentionally narrow: it does not refactor the MoE class hierarchy or grouped GEMM signatures; it only adds optional workspace reuse while preserving the default allocation path.