Skip to content

MoE decode carries ~5-6.6 ms/step of fixed cost that dense models do not (Arc Pro B70 / BMG-G31) #509

Description

@doopeworld

On Intel Arc Pro B70 (BMG-G31, 608 GB/s), single-request decode of Qwen3.5-MoE hybrid models is limited by a fixed per-step cost of roughly 5-6.6 ms that is independent of how many bytes the model reads. A dense model of the same architecture family, on the same card and the same build, shows no such fixed cost and runs at the memory-bandwidth roofline.

Removing this overhead would take these models from ~71-89 tok/s to ~118-217 tok/s at batch 1.

Environment

  |   -- | -- GPU | Intel Arc Pro B70 32GB (BMG-G31), 608 GB/s CPU / RAM | Core Ultra 9 285H, 96 GB DDR5-5600 vLLM | 0.26.1.dev0+g568afb3a1 vllm-xpu-kernels | 0.1.12.1 torch | 2.13.0+xpu Env | VLLM_XPU_ENABLE_XPU_GRAPH=1, VLLM_USE_V2_MODEL_RUNNER=1, async scheduling on Models | Qwen3.5-MoE hybrid (40 layers: 30 GatedDeltaNet + 10 full attn, 256 experts, top-8 + 1 shared, moe_intermediate 512, hidden 2048)

8x the work for 1.21x the time. Consistent with a large fixed per-step cost that amortizes across the batch.

What was ruled out

Each of these was measured, not assumed.

Quantization format / coverage. MXFP4, GPTQ-Int4 and AutoRound-int4 all land in the 71-89 tok/s band despite an 85% spread in bytes read.

Level Zero runtime configuration. 14 configurations swept — UR_L0_USE_IMMEDIATE_COMMANDLISTS=0, UR_L0_BATCH_SIZE 16/64/128, UR_L0_REUSE_DISCARDED_EVENTS, UR_L0_DISABLE_EVENTS_CACHING,UR_L0_SERIALIZE, UR_L0_IN_ORDER_BARRIER_BY_SIGNAL, legacy SYCL_PI_* equivalents, and both Level Zero adapter versions. All results 12.56-12.67 ms except SYCL_UR_USE_LEVEL_ZERO_V2=0 at 13.33 ms. No knob moved it.

Kernel launch batching. XPU graphs work correctly. 200 sequential torch.relu calls on a 256-element tensor:

eager   0.862 ms   (4.3 us per launch)
replay  0.181 ms   (0.9 us per launch)   -> 4.75x

Graph capture also demonstrably matters end to end: enforce_eager=True gives 41.09 ms/step vs 12.6 ms with graphs.

vLLM Python-side work. In-process cProfile (VLLM_ENABLE_V1_MULTIPROCESSING=0), 64 decode steps, sorted by tottime:

63 calls  0.618 s   torch/xpu/graphs.py:105(replay)
64 calls  0.090 s   Event.synchronize
   ...
64 calls  0.002 s   gpu/model_runner.py:874(prepare_inputs)
64 calls  0.002 s   gpu/sample/sampler.py:70(__call__)
64 calls  0.002 s   gpu/attn_utils.py:566(build_attn_metadata)
65 calls  0.001 s   v1/core/sched/scheduler.py:425(schedule)

Scheduler, input preparation, attention metadata and sampling together account for roughly 0.15 ms/step. vLLM's Python layer is not the cost.

Empty expert group dispatch. XpuFusedMoe._apply_kernel calls cutlass_grouped_gemm_interface with num_experts=self.num_experts (256) on every token, though only 9 experts have rows. In MoEGEMM(grouped_gemm_xe2.hpp) every work-group serially scans all 256 entries of rows_per_expert to build a prefix sum before locating its tile.

An isolated microbenchmark suggested this was expensive (232 us for 256 groups vs 94 us for 8, identical work). It is not the cause. We patched cutlass_grouped_gemm_interface to accept an optional expert_ids tensor so group ireads ptr_B[expert_ids[i]], allowing the caller to pass only active experts with no weight copying, and verified the compaction engages (8 groups instead of 256). End-to-end gain at batch 1: zero (72.6 vs 74.5 tok/s, within noise).

Reporting it anyway since the 256-group dispatch is real and may matter at other shapes — happy to open a separate PR if useful.

Where the time appears

All remaining time is inside torch.xpu.XPUGraph.replay() — 9.8 ms/step by tottime. Since replay() blocks until completion, this is submit-plus-wait and cannot by itself distinguish genuine GPU execution from replay overhead.

Note that torch.profiler reports only 2.38 ms/step of XPU kernel time (152.4 ms over 64 steps), but it does not appear to capture kernels executed inside graph replays, so we do not rely on that figure.

The fixed-cost fit above is independent of any profiler and holds across four models and three quantization formats.

What would help

  1. Confirmation of whether ~5-6.6 ms/step of MoE-specific fixed cost is expected on this path, or a known gap.
  2. Guidance on measuring GPU time inside XPU graph replays — the profiler undercount makes attribution difficult for anyone hitting this.
  3. If the 256-group grouped-GEMM dispatch is considered worth fixing regardless, we have a working expert_idspatch against v0.1.12.1.

Reproduction

bash
# byte counts from safetensors headers + config
# batch sweep, prefill subtracted, median of 3
BATCHES=1,2,4,8 NGEN=64 python3 batchsweep.py /models/<model>

Scripts used for all measurements above can be attached on request.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions