Skip to content

[Hardware][Intel] Wan VAE: run wan_vae_fastpath on XPU - #8968

Merged
hsliuustc0106 merged 7 commits into
vllm-project:mainfrom
xuechendi:xpu/wan-vae-perf
Oct 23, 2026
Merged

hsliuustc0106 merged 7 commits into
vllm-project:mainfrom
xuechendi:xpu/wan-vae-perf

Conversation

@xuechendi

@xuechendi xuechendi commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

1. What outcome do you want to achieve?

apply the same wan_vae_fastpath that CUDA already uses, on XPU.
XPU users should get the lossless decode/encode speedup by default, and XPU should share one maintained code path with CUDA.

2. What is the highest-leverage variable for that outcome?

Whether wan_vae_fastpath runs on XPU at all. Previously, the registry install and every Triton kernel are gated on CUDA, so XPU always runs the upstream diffusers VAE. That VAE spends much of its time on copies and elementwise passes around the convolutions, which is exactly the work the fast path removes.

3. How will this work move that variable?

  • Opens the gates: the fast path's device gates now accept cuda and xpu (KERNEL_DEVICE_TYPES), so it installs and runs on XPU by default. The Triton kernels are device-agnostic, and the conv-padding rewrite already checks bit-exactness per shape.
  • Fixes the one XPU difference: oneDNN adds a conv's bias inside its fp32 accumulator, so deferring the bias changes the output by an ulp. The lossless level keeps the bias in the conv on XPU, so the output stays bit-exact.
  • Lets spatial_shard_* decode run without forcing --vae-use-tiling.
  • Results on Intel Arc Pro B70, Wan2.2-T2V-A14B VAE, 720p/81f:
    • Decode on one card: 1.72x un-tiled (17.97 → 10.44 s) and 1.64x tiled (29.31 → 17.90 s), rel_l2 0.
    • End to end (20 steps, CFG2 × TP2, tiled VAE): vae.decode 30.54 → 21.61 s, and the output video is byte-identical.

4. Whose customer or user needs does this address, and what are those needs?

Users serving Wan2.2 T2V/I2V video generation on Intel XPU [e.g. the team, customer or deployment, if public]. They need:

  • lower per-request latency: the VAE decode is a fixed cost on every video;
  • identical output quality: the fast path is bit-exact;
  • one shared fast path with CUDA, so XPU picks up future fast-path work, such as spatial-parallel decode ([Kernel] Enable Wan VAE fast paths for spatial-parallel decode #8893), without a separate implementation to maintain.

Purpose

Wan VAE performance on Intel XPU. This PR builds on the existing, CUDA-only
wan_vae_fastpath (#7056 decode, #8067 encode) instead of adding a parallel XPU
implementation.

Area Before After
wan_vae_fastpath on XPU Never installed: registry._apply_wan_vae_fastpath_if_enabled returned on non-CUDA, and every Triton kernel gated on x.is_cuda. Installed on CUDA and XPU (KERNEL_DEVICE_TYPES = {"cuda", "xpu"} in diffusion/layers/vae_norm_epilogue.py). On XPU, lossless keeps the conv bias inside the convolution (see below), so it stays bit-exact.
spatial_shard_* decode Needed --vae-use-tiling, and registry.initialize_model forced it on, although the sharded decode is full-frame and ignores tiles. is_distributed_enabled() requires use_tiling and the registry auto-enables it only for tile mode. DistributedAutoencoderKLWan._decode routes spatial_shard_* before the tiling check; a VAE without distributed state keeps the parent decode. In spatial-shard modes the encode is distributed only with explicit --vae-use-tiling (documented in the VAE parallelism guide).

Spatial-shard decode still runs without the fast path.

  • install_wan_vae_fastpath refuses spatial_shard_* modes: wan_spatial_shard swaps
    every causal conv for a halo-exchanging one and rebinds the same block forwards.
  • This PR only lets those modes run without --vae-use-tiling.

** --vae-fast-path has three levels. **

  • off
  • lossless (the default, bit-exact)
  • channels_last, which is opt-in and tolerance-based:
    • the decoder's conv weights are converted to channels-last, so the activations run NDHWC end to end;
    • norm + SiLU and the conv bias are folded into channels-last kernels;
    • SiLU is fused even where it isn't proven bit-exact.

Test Plan

vLLM Version: v0.31.0 (vllm/vllm-openai-xpu:v0.31.0 base of docker/Dockerfile.xpu)

vLLM-Omni Commit: 0a14ba414 on origin/main 0d4d09cf7

Hardware: Intel Arc Pro B70 (31.9 GiB), torch 2.14.0+xpu, triton-xpu 3.7.2,
diffusers 0.40.0. Image built from this branch with docker/Dockerfile.xpu.

  1. Unit and kernel tests on XPU:
    pytest tests/diffusion/distributed/test_autoencoder_kl_wan.py \
           tests/diffusion/distributed/test_wan_vae_fastpath_install.py \
           tests/diffusion/distributed/test_wan_vae_encoder_fastpath.py \
           tests/diffusion/distributed/test_wan_vae_fastpath_verdicts.py \
           tests/diffusion/distributed/test_wan_vae_fastpath_kernels.py \
           tests/diffusion/distributed/test_wan_vae_encoder_fastpath_kernels.py
    Also run: all 53 test files that import the Wan VAE, wan_vae_fastpath, the
    spatial-shard code, the distributed VAE executor or the diffusion registry. This
    includes tests/diffusion/models/wan2_2/ and test_wan_spatial_shard.py; its 2-rank
    tests need 2 cards.
  2. VAE decode, one card, Wan2.2-T2V-A14B VAE, 720p/81f bf16, un-tiled and tiled:
    • diffusers AutoencoderKLWan vs OmniAutoencoderKLWan +
      install_wan_vae_fastpath(level=...), same weights and latent;
    • best of 2 after a warmup.
  3. End to end: Wan2.2-T2V-A14B, 720p/81f, 20 steps, 4 cards, one request per fresh
    server:
    export VLLM_OMNI_VIDEO_SYNC_TIMEOUT=3600    # see "Known limitations"
    export VLLM_OMNI_DLO_DP_WAVE_TIMEOUT=3600
    vllm-omni serve Wan-AI/Wan2.2-T2V-A14B-Diffusers --omni \
        --cfg-parallel-size 2 --tensor-parallel-size 2 \
        --boundary-ratio 0.875 --flow-shift 5.0 \
        --vae-fast-path lossless --vae-use-slicing --vae-use-tiling \
        --diffusion-offload-config '{"mode":"module","components":["dit","text_encoder"]}' \
        --enable-diffusion-pipeline-profiler
    curl -sS -D headers.txt -X POST http://127.0.0.1:8000/v1/videos/sync \
        -F "prompt=Two anthropomorphic cats in comfy boxing gear and bright gloves fight intensely on a spotlighted stage." \
        -F height=720 -F width=1280 -F num_frames=81 -F num_inference_steps=20 \
        -F guidance_scale=4.0 -F guidance_scale_2=3.0 -F seed=1234 \
        -F boundary_ratio=0.875 -F flow_shift=5.0 -F fps=16 -o out.mp4
    Stage times come from the X-Stage-Durations response header. The arms are
    --vae-fast-path lossless vs off.

Test Result

1. Tests (XPU, commit 0a14ba414): the 53 affected test files: 3034 passed, 24 skipped, 0 failed.
This includes the fast-path suite above, test_pruned_real_tiny_distributed_wan_vae_retains_numerics,
and the new test_registry_auto_enables_vae_tiling_only_for_tile_mode.

  • 17 of the skips are deliberate XPU skips with a reason:
    • the lossless bias-deferral mechanism tests (_LOSSLESS_DEFERS_CONV_BIAS); the new
      test_xpu_lossless_keeps_conv_bias_in_the_conv covers the XPU behaviour instead;
    • the fp32 fused-SiLU kernel case, which lossless never takes.
  • The other 2 skips are pre-existing.

2. VAE decode, one B70, Wan2.2-T2V-A14B VAE, 720p/81f bf16:

diffusers fast path lossless speedup rel_l2 / max_abs vs diffusers
un-tiled 17.97 s 10.44 s 1.72x 0 / 0
tiled 29.31 s 17.90 s 1.64x 0 / 0
--level off 17.94 s 17.96 s 1.00x 0 / 0

2b. All three --vae-fast-path levels, one B70, Wan2.2-T2V-A14B VAE, 720p/81f bf16
(commit 103ddff8c, best of 2 after a warmup):

off (upstream) lossless channels_last
un-tiled 17.89 s 10.37 s (1.72x), rel_l2 0 9.21 s (1.94x), rel_l2 6.5e-3, max_abs 5.0e-2
tiled 29.31 s 18.09 s (1.62x), rel_l2 0 15.97 s (1.83x), rel_l2 6.2e-3, max_abs 4.8e-2
peak memory, un-tiled / tiled 10.1 / 2.6 GiB 11.3 / 2.6 GiB 10.5 / 2.8 GiB

3. End to end, Wan2.2-T2V-A14B, 720p/81f, 20 steps, CFG 2 × TP 2 on 4 B70s, tiled VAE:

stage --vae-fast-path off --vae-fast-path lossless change
Wan22Pipeline.vae.decode 30.54 s 21.61 s −8.93 s (1.41x)
Wan22Pipeline.diffuse 660.91 s 660.52 s —
Wan22Pipeline.text_encoder.forward 0.31 s 0.31 s —
request (X-Inference-Time-S) 708.38 s 702.06 s −6.3 s
peak memory (X-Peak-Memory-MB) 25038 MB 25038 MB —
output video md5 d85ab93d2ba09c9c7570ef6921b3a6b8 d85ab93d2ba09c9c7570ef6921b3a6b8 identical

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-15T05:49:03.047169Z 103ddff PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@vllm-omni-review-bot

Copy link
Copy Markdown

This PR appears to belong to: docs/design/module/diffusion/index.md.

Module owners: @david6666666 @princepride @Isotr0py

Routing: @david6666666 via module of the changed files, module named in the PR description, CODEOWNERS; @princepride via module of the changed files, module named in the PR description, CODEOWNERS; @Isotr0py via module of the changed files, module named in the PR description

@xuechendi, please review your own changes and leave a short self-review comment describing what you checked. PRs without author self-review may not be assigned a reviewer.

Please take a look when you have a chance. If you would like an automated review, mention @vllm-omni-review-bot in a comment.

@xuechendi

xuechendi commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

Author self-review

What I checked on 103ddff:

  • Scope: XPU enablement of the existing wan_vae_fastpath, plus spatial_shard_* decode no longer requiring --vae-use-tiling. No new fast-path implementation, and no new flags: --vae-fast-path levels are unchanged from [optimization] Added Wan 2.2 VAE decode optimizations #7056.
  • Correctness on XPU: the fast path's lossless bias deferral isn't exact on oneDNN, which adds the bias inside its accumulator; deferring changed ~27% of bf16/fp16 outputs by an ulp. _may_defer_conv_bias keeps the bias in the conv on XPU at the lossless level. With this, the decode is bit-exact against diffusers (rel_l2 = 0, 720p/81f, un-tiled and tiled), and the e2e output video is byte-identical with the fast path off and on.
  • CUDA unchanged by design: the gates now accept cuda and xpu, and _may_defer_conv_bias is True on CUDA. CUDA hardware was not available to me, so the CUDA fast-path kernel tests still need to run in CI.
  • Tests: on Intel Arc Pro B70, 1215 passed, 19 skipped, 0 failed. The 17 XPU-specific skips are deliberate and each carries a reason. test_xpu_lossless_keeps_conv_bias_in_the_conv covers the XPU bias rule.
  • Pre-commit: all hooks pass locally, including mypy and SPDX. Three pre-existing mypy errors in a touched test file are fixed.
  • Performance evidence: measured on B70, with the configs in the description.
    • Decode: 1.72x un-tiled and 1.64x tiled.
    • E2e Wan2.2-T2V-A14B 720p/81f, 20 steps, CFG2 × TP2: vae.decode 30.54 → 21.61 s.
    • channels_last on XPU: 1.94x / 1.83x, not bit-exact.
  • Known gaps, listed in the description:

@hsliuustc0106

Copy link
Copy Markdown
Collaborator

Reviewed head 103ddff8c9c2343dc572c65d184f5f4f8292e437.

  • [P1] Preserve decode without distributed initialization — autoencoder_kl_wan.py:489. Every decode now calls _spatial_shard_decode_enabled(), which unconditionally reads self.distributed_executor. Directly constructed VAEs lack that attribute, so previously working untiled decode raises AttributeError. The existing test_pruned_real_tiny_distributed_wan_vae_retains_numerics[False] exercises this case. Guard absent distributed state and retain the parent decode fallback.

  • [P2] Remove the remaining automatic tiling requirement — distributed_vae_executor.py:226. The new spatial-shard exemption is incomplete: registry.py::load_model still automatically enables tiling whenever VAE parallelism exceeds one and the mode is not batch. Serving therefore still forces tiling for both spatial-shard modes, also affecting encoding. Restrict that automatic enablement to tile mode.

Reported CI passes; diff hygiene passes. Review was static: I did not execute fork code locally or independently verify CUDA/XPU correctness and performance.

@hsliuustc0106 hsliuustc0106 added the high priority high priority issue, needs to be done asap label Oct 15, 2026 — with ChatGPT Codex Connector
@hsliuustc0106 hsliuustc0106 added the xpu PR related to XPU label Oct 16, 2026
Wan VAE performance on Intel XPU, built on the existing CUDA-only
wan_vae_fastpath instead of a parallel XPU implementation.

wan_vae_fastpath on XPU
- Gate the fused Triton kernels and the registry install on
  KERNEL_DEVICE_TYPES ({"cuda", "xpu"}, in diffusion/layers/vae_norm_epilogue)
  instead of `is_cuda`. The kernels are plain Triton, and the conv
  spatial-padding rewrite is already guarded by a per-shape bit-exactness
  probe, so it adapts to oneDNN as it does to cuDNN.
- Keep the conv bias inside the convolution on XPU at the lossless level
  (`_may_defer_conv_bias`). Deferring it to the following norm/residual kernel
  is exact on CUDA, where ATen adds the bias after cuDNN rounds, but oneDNN
  adds it inside the fp32 accumulator; deferring changed ~27% of bf16/fp16
  outputs by an ulp. channels_last (tolerance-based) still defers everywhere.

Spatial-shard decode without tiling
- `is_distributed_enabled()` requires use_tiling only for "tile" mode,
  `registry.initialize_model` auto-enables tiling only for "tile" mode, and
  DistributedAutoencoderKLWan._decode routes spatial_shard_* before the tiling
  check: the sharded decode is full-frame. A VAE without distributed state
  (constructed directly) keeps the parent decode. In spatial-shard modes the
  encode is distributed only with explicit --vae-use-tiling (documented).

Tests: the fast-path kernel tests are device-agnostic and run on XPU (B60
marks); XPU skips the bias-deferral mechanism tests and the fp32 fused-SiLU
kernel case (never taken at the lossless level) with a reason and adds
test_xpu_lossless_keeps_conv_bias_in_the_conv.

Measured on Intel Arc Pro B70 (lossless = bit-exact vs diffusers):
- Wan2.2-T2V-A14B VAE decode, 720p/81f bf16, 1 card: 17.97 -> 10.44 s
  un-tiled (1.72x), 29.31 -> 17.90 s tiled (1.64x), rel_l2 0.
- Wan2.2-T2V-A14B e2e 720p/81f/20 steps, CFG2 x TP2, tiled VAE, module
  offload: vae.decode 30.54 -> 21.61 s, request 708.4 -> 702.1 s, identical
  output video.

Signed-off-by: Chendi Xue <chendi.xue@intel.com>
@xuechendi xuechendi changed the title [Hardware][Intel] Wan VAE: run wan_vae_fastpath on XPU, compile the fused norms [Hardware][Intel] Wan VAE: run wan_vae_fastpath on XPU Oct 16, 2026
@xuechendi

Copy link
Copy Markdown
Collaborator Author

@hsliuustc0106 thanks, both addressed in 0a14ba4.

[P1] Decode without distributed initialization.

  • DistributedAutoencoderKLWan._decode now routes to the sharded decode only when the VAE has distributed state (distributed_executor). A directly constructed VAE falls through to the parent decode, as on main.
  • test_pruned_real_tiny_distributed_wan_vae_retains_numerics[False] passes.
  • My earlier run only covered the fast-path test files, so this time I ran all 53 test files that import the Wan VAE, wan_vae_fastpath, the spatial-shard code, the distributed VAE executor or the registry. That's 3034 passed, 24 skipped, 0 failed on XPU (B70).

[P2] Automatic tiling.

  • registry.initialize_model now auto-enables vae_use_tiling only for tile mode. Spatial-shard (and batch) modes keep the requested setting.
  • New regression test: test_registry_auto_enables_vae_tiling_only_for_tile_mode (tile → on; spatial_shard_height/_width → unchanged).
  • One consequence, now documented in the VAE parallelism guide's "Automatic VAE Tiling" note: in spatial-shard modes the encode is distributed only through the tiled path, so a multi-rank encode needs an explicit --vae-use-tiling. The decode stays spatially sharded either way, because _decode routes spatial_shard_* before the tiling check.

I also updated the PR title and description (the norm compile was dropped from this PR earlier).

Copy link
Copy Markdown
Collaborator

Reviewed head 0a14ba41415784a35e274ae8edde4e2952d9807d. The earlier direct-construction and automatic-tiling findings are addressed. Two decode routes still need to be updated for the new no-tiling default:

  • [P2] Route chunked decode through spatial sharding without requiring tiling — registry.py:513. On a fresh instance with --tensor-parallel-size 2 --vae-patch-parallel-size 2 --vae-parallel-mode spatial_shard_height, no --vae-use-tiling, and extra_args={"preencode_mp4": true}, the pipeline calls decode_to_mp4 → decode_with_chunks → _decode_with_chunks, bypassing the new _decode override. _decode_with_chunks still enters the sharded route only under use_tiling. Consequently the first request decodes the full frame independently on every rank; only the callback is suppressed on non-owner ranks. This loses the configured sharding's compute and activation-memory savings. Dispatch eligible spatial-shard chunked decode before the tiling check, and extend test_distributed_chunked_routing to cover use_tiling=False on a fresh decoder.

  • [P2] Preserve tile-parallel fallback for a partial VAE group — autoencoder_kl_wan.py:501. With --tensor-parallel-size 4 --vae-patch-parallel-size 2 --vae-parallel-mode spatial_shard_height and no explicit tiling, _spatial_shard_decode_enabled rejects the size mismatch and logs “falling back to tiled decode”. However, the parent _decode now sees use_tiling=False and calls _decode_temporal: full local decode on all four workers, rather than the documented two-rank tile-parallel fallback. Before this change, registry auto-tiling made that fallback reachable. Preserve the fallback dispatch independently of the user's native-tiling setting and test a large latent through _decode, rather than checking only the spatial-shard gate's boolean result. The same issue applies to width sharding.

Validation: source/call-chain review and static parsing at the pinned head; no CUDA/XPU model execution or numerical/performance reproduction.

@xuechendi

Copy link
Copy Markdown
Collaborator Author

@hsliuustc0106 thanks, both addressed in 6b002e3.

[P2] Chunked decode without tiling.

  • DistributedAutoencoderKLWan._decode_with_chunks now routes spatial_shard_* to spatial_shard_decode(..., on_chunk=...) before the parent's use_tiling check. A fresh instance without --vae-use-tiling therefore streams the sharded decode, with the callback only on the output-owning rank. tile mode and VAEs without distributed state keep the parent route.
  • test_distributed_chunked_routing now runs with use_tiling True and False for all three modes. With False: spatial_shard_* → spatial_shard_decode, tile → the temporal decode.

[P2] Partial VAE group fallback.

  • In spatial_shard_* mode, when the shard gate rejects the group size, _decode now sends a latent larger than one tile to the tile-parallel executor (_tile_parallel_decode, split out of tiled_decode). It no longer depends on native tiling. A latent within one tile still uses the local decode, which matches what auto-tiling did before.
  • New test_partial_vae_group_decode_falls_back_to_tile_parallel covers height and width, with large and small latents. It goes through _decode: vae pp 2 in a 4-rank group, use_tiling=False.
  • The chunked route has no tile-parallel streaming. With a partial group and no tiling, it decodes on each rank and calls back only on rank 0. With explicit tiling it still raises the existing "require spatial-shard decode" error.

The VAE parallelism guide's spatial-shard section now says when the fallback applies and what the chunked route does.

Tests on XPU (Arc Pro B70), commit 6b002e3: the 51 test files that import the Wan VAE, the spatial-shard code, the distributed VAE executor, the registry or chunked_video. 2873 passed, 21 skipped, 83 failed. The same 83 tests fail on origin/main in this environment: offline HF lookups and missing local checkpoints (mammoth_moda2, anima, sana_video, one fast-path config test), and lingbot multi-card tests that need more visible cards. The new routing and fallback cases fail at 0a14ba4 (4 of 10) and pass at 6b002e3 (10 of 10).

…k without tiling

spatial_shard_* no longer forces vae_use_tiling, so two decode routes still
depended on it:
- _decode_with_chunks entered the sharded decode only under use_tiling; a
  fresh instance decoded the full frame on every rank. Route spatial_shard_*
  chunked decode before the tiling check.
- With a partial VAE group (vae pp size != DiT group size) the shard gate
  falls back, but the parent _decode then took the local decode instead of
  the tile-parallel executor. Keep that fallback for latents larger than a
  tile, independent of native tiling.

Extend test_distributed_chunked_routing to use_tiling=False and add
test_partial_vae_group_decode_falls_back_to_tile_parallel.

Signed-off-by: Chendi Xue <chendi.xue@intel.com>
xuechendi added a commit to xuechendi/vllm-omni that referenced this pull request Oct 17, 2026
…tial_shard_*

Spatial-shard modes may not auto-enable VAE tiling (see vllm-project#8968). Without
tiling the encode runs locally and VLLM_OMNI_WAN_VAE_ENCODE_SPLIT has no
effect, so the serving example and the requirements list now pass
--vae-use-tiling explicitly.

Signed-off-by: Chendi Xue <chendi.xue@intel.com>

@hsliuustc0106 hsliuustc0106 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The four earlier routing/initialization findings are addressed in the source at 1dbbee11c. The supplied B70 parity and stage timings are useful; I have not independently executed them.

  • Please complete the repeatability evidence for the claimed XPU decode speedup. Keep the documented B70, Wan2.2-T2V-A14B checkpoint, software, 720p/81f bf16 input and topology fixed, and provide runnable component benchmark commands plus paired off/lossless raw timings after stated warm-up, with repeat count and spread rather than only best-of-two. For the end-to-end claim, repeat the paired 20-step CFG2 × TP2 comparison and report decode-stage and request timings separately, keeping fresh-server/cold and warmed samples separate. Bind results to this head or identify the measured SHA and unchanged relevant source. Retain the identical-output check for lossless; if retaining channels_last claims, report that opt-in arm separately with its explicit numerical tolerance. Acceptance is a reproducible paired comparison showing the gain and its variability without changing workload or quality criteria. This is a performance-evidence gap, not a newly demonstrated code defect.

@hsliuustc0106 hsliuustc0106 added the ready label to trigger buildkite CI label Oct 21, 2026
xuechendi added a commit to xuechendi/vllm-omni that referenced this pull request Oct 21, 2026
…tial_shard_*

Spatial-shard modes may not auto-enable VAE tiling (see vllm-project#8968). Without
tiling the encode runs locally and VLLM_OMNI_WAN_VAE_ENCODE_SPLIT has no
effect, so the serving example and the requirements list now pass
--vae-use-tiling explicitly.

Signed-off-by: Chendi Xue <chendi.xue@intel.com>
xuechendi added a commit to xuechendi/vllm-omni that referenced this pull request Oct 22, 2026
…tial_shard_*

Spatial-shard modes may not auto-enable VAE tiling (see vllm-project#8968). Without
tiling the encode runs locally and VLLM_OMNI_WAN_VAE_ENCODE_SPLIT has no
effect, so the serving example and the requirements list now pass
--vae-use-tiling explicitly.

Signed-off-by: Chendi Xue <chendi.xue@intel.com>
…pu 3.8

triton-xpu 3.8 (the vLLM v0.32.0 XPU image, vllm-project#9131) lowers tl.math.div_rn
to a plain fdiv, which IGC compiles to its approximate divide: ~29% of
fp32 quotients differ from ATen's x / d by an ulp, so the fp32 bit-exact
RMSNorm/encoder/decoder kernel tests fail in the Intel CI. triton-xpu
3.7 lowered it to OpenCL's correctly rounded divide_cr.

Add vae_norm_epilogue.div_rn: on XPU it calls divide_cr directly (exact
and as fast as the approximate divide on 3.7.2 and 3.8.0), elsewhere it
is tl.math.div_rn, so CUDA is unchanged. libdevice div_rn was not used:
it is a software routine on XPU that made the RMSNorm epilogue ~30x
slower and the lossless decode slower than off.

Signed-off-by: Chendi Xue <chendi.xue@intel.com>
- Select the device with torch.accelerator, so the benchmark runs on
  XPU as well as CUDA (--profile stays CUDA-only).
- --tiling decodes with the VAE's spatial tiling on one GPU.
- Report the median of --iters decodes with min, max, standard deviation
  and every raw sample; the speedup compares medians.

Signed-off-by: Chendi Xue <chendi.xue@intel.com>
Signed-off-by: Chendi Xue <chendi.xue@intel.com>
@xuechendi

Copy link
Copy Markdown
Collaborator Author

@hsliuustc0106 thanks. Below: a CI fix found while re-measuring, and the component decode evidence at the new head. I haven't re-run the end-to-end comparison yet; it's covered at the end.

Intel CI fix (a7cf00a)
The red Intel CI at 1dbbee1 came from the main merge, which moved the XPU image to vLLM v0.32.0 / triton-xpu 3.8.0 (#9131). There, tl.math.div_rn lowers to a plain fdiv, which IGC compiles as an approximate divide; triton-xpu 3.7.2 emitted OpenCL's correctly rounded divide_cr. About 29% of fp32 quotients were off by an ulp, so the 45 fp32 bit-exact kernel tests failed.

  • New vae_norm_epilogue.div_rn: calls divide_cr on XPU and stays tl.math.div_rn elsewhere, so CUDA is unchanged.
  • Measured exact against ATen, including subnormal, inf and zero inputs, and as fast as the approximate divide on both triton versions.
  • libdevice div_rn is not used: on XPU it is a software routine that made the lossless decode slower than off (0.62x).
  • test_div_rn_matches_aten_fp32_division covers it. It fails with plain tl.math.div_rn on triton-xpu 3.8.

The AMD failures (Qwen-Image / MammothModa2 fused QK-norm-RoPE) are in tests that import nothing this PR changes. #9229 fixed the Qwen-Image ones on main, and a1757fe merges main.

Component decode benchmark, head a1757fe
The benchmark is unchanged from the description, apart from the software stack. Setup:

  • Workload: Intel Arc Pro B70 (1 card), Wan2.2-T2V-A14B-Diffusers VAE (snapshot 5be7df96), 1280x720 / 81 frames, bf16, seeded latent.
  • Software: vLLM v0.32.0 XPU image (current docker/Dockerfile.xpu), torch 2.14.0+xpu, triton-xpu 3.8.0.
  • Sampling: 1 warmup, then 5 timed decodes per level. Each command was run 3 times as fresh processes.
  • bench_wan_vae_decode.py now runs on XPU and prints every sample (273f07b).
python benchmarks/diffusion/bench_wan_vae_decode.py --model Wan-AI/Wan2.2-T2V-A14B-Diffusers \
    --size 1280x720 --frames 81 --dtype bf16 --fast-path off,lossless,channels_last --warmup 1 --iters 5 [--tiling]

Median decode per run, in seconds. Within-run stdev is 2-36 ms everywhere (≤0.2%).

off lossless speedup channels_last speedup
un-tiled, run 1/2/3 17.942 / 17.952 / 17.961 10.273 / 10.273 / 10.276 1.75x 9.345 / 9.345 / 9.352 1.92x
tiled, run 1/2/3 29.145 / 29.156 / 29.163 17.856 / 17.852 / 17.833 1.63x (1.63–1.64) 15.979 / 16.015 / 16.005 1.82x
  • lossless is bit-identical to off in every run (torch.equal and bitwise, max abs diff 0).
  • channels_last is opt-in and not bit-exact. Against off it gives PSNR 59.43 dB (un-tiled) and 59.68 dB (tiled), max abs diff 2.5e-2 and 2.9e-2. It isn't part of the lossless claim.
  • Peak memory, un-tiled: 10.08 GiB (off), 10.53 GiB (lossless), 9.69 GiB (channels_last).
Raw samples (ms), un-tiled
run 1 off           [17903.4, 17912.4, 17942.4, 17953.5, 17952.3]
run 1 lossless      [10283.3, 10271.5, 10271.4, 10273.1, 10273.1]
run 1 channels_last [9340.5, 9344.3, 9344.7, 9345.1, 9345.3]
run 2 off           [17933.3, 17928.6, 17952.2, 17958.3, 17954.2]
run 2 lossless      [10282.6, 10272.6, 10272.6, 10272.9, 10273.4]
run 2 channels_last [9339.9, 9344.9, 9345.7, 9345.9, 9343.6]
run 3 off           [17950.5, 17938.0, 17961.1, 17968.2, 17962.9]
run 3 lossless      [10290.9, 10276.1, 10275.6, 10277.7, 10274.7]
run 3 channels_last [9345.1, 9352.2, 9353.2, 9353.5, 9351.3]
Raw samples (ms), tiled
run 1 off           [29187.0, 29160.6, 29145.3, 29134.6, 29139.1]
run 1 lossless      [17898.0, 17856.9, 17855.5, 17824.7, 17838.7]
run 1 channels_last [15963.9, 15980.4, 15978.7, 15975.2, 15984.8]
run 2 off           [29172.8, 29156.1, 29150.0, 29158.0, 29146.5]
run 2 lossless      [17879.1, 17831.7, 17845.6, 17852.1, 17856.0]
run 2 channels_last [16002.1, 16014.2, 16014.7, 16015.4, 16019.5]
run 3 off           [29180.3, 29162.9, 29158.3, 29158.8, 29162.7]
run 3 lossless      [17920.8, 17827.2, 17835.3, 17832.8, 17831.1]
run 3 channels_last [15996.6, 16006.0, 16004.8, 16000.6, 16005.5]

The un-tiled off median and the channels_last speedups match the description's v0.31.0 / triton-xpu 3.7.2 numbers (17.97 s; 1.94x / 1.83x). lossless is 1.75x / 1.63x here, against 1.72x / 1.64x there.

Tests at a1757fe (B70, same image)

  • 1583 passed, 19 skipped.
  • 4 failed + 2 errors, all offline Hugging Face lookups that fail identically on origin/main in this environment: test_omni_diffusion_config_validates_vae_fast_path, 3 spatial-shard tests needing the Wan2.1-1.3B config, and 2 test_diffusers_loader tests.
  • The six fast-path test files were also run on triton-xpu 3.7.2 (vLLM v0.31.0 image). The only failure was the same offline test_omni_diffusion_config_validates_vae_fast_path.

End to end
The paired 20-step CFG2 × TP2 runs (decode stage and request time, with cold and warm samples kept separate) have not been re-run at this head. I'll post them as a follow-up. One caveat: on this node, a second request on the same server has hung after the first module-offload expert swap. So the e2e samples may be fresh-server (cold) only, and I'll report that explicitly.

@xuechendi xuechendi added ready label to trigger buildkite CI and removed ready label to trigger buildkite CI labels Oct 22, 2026
The two fast-path kernel test files were scheduled on XPU in full: about
8 minutes on one B70 (1030 cases). Intel CI only needs to show that the
fast path is bit-exact on XPU, so the file-level marks go back to CUDA
only and an _XPU_CI mark selects 88 cases: the div_rn and RMSNorm
bit-exactness checks, the fused SiLU verdict, the XPU conv-bias rule,
the lossless decoder and encoder end to end, the Omni VAE decode and a
channels_last smoke test. 53 s wall on a B70 with a cold Triton cache.
Every test still runs on XPU when invoked directly.

Signed-off-by: Chendi Xue <chendi.xue@intel.com>
@xuechendi xuechendi added ready label to trigger buildkite CI and removed ready label to trigger buildkite CI labels Oct 22, 2026

Copy link
Copy Markdown
Collaborator

Holding approve for two items:

  1. Perf evidence still missing (from prior review): on a fixed B70 config, paired raw timings for off vs lossless, with repeats + range; e2e should separate decode-phase vs full request, and cold vs warmed. Head as of this comment still doesn’t include that package.
  2. CUDA behavior change: registry.py no longer forces tiling under spatial_shard_*. Without an explicit --vae-use-tiling, encode may stop using the distributed path after upgrade — docs mention it, but this needs a clear changelog / release-note callout for CUDA users.

Also please check why Intel CI is red on this XPU-focused PR. Happy to re-review once (1)+(2) are addressed.

@hsliuustc0106
hsliuustc0106 merged commit 5825844 into vllm-project:main Oct 23, 2026
11 checks passed
xuechendi added a commit to xuechendi/vllm-omni that referenced this pull request Oct 23, 2026
…-gather halos on XPU

Spatial-shard encode
- Under vae_parallel_mode=spatial_shard_{height,width}, DistributedAutoencoderKLWan._encode
  shards the encoder along the split dimension with the existing halo modules, before the
  native-tiling check (as _decode does for decode since vllm-project#8968). It mirrors diffusers' untiled
  _encode loop and all-gathers the moments to every rank. It runs on a parameter-sharing copy
  of vae.encoder kept outside the module tree, so state_dict is unchanged and the original
  encoder still serves the fallback.
- Only taken when the split extent divides into stride-aligned shards
  (world_size * 2**downsamples); otherwise, with the encoder fast path installed, or with a
  partial VAE group, encode takes the regular path (tile/rank-split parallel when use_tiling).
- Fix: _patch_decoder_modules never replaced nn.ZeroPad2d, so the encoder's stride-2
  downsample padded every rank's edge. The decoder has no ZeroPad2d.

All-gather halo exchange on XPU
- A batched isend/irecv halo costs ~1.3 ms on XCCL against ~0.1-0.2 ms for an all-gather of
  the same rows; XPU now exchanges halos with one all_gather_into_tensor. Other platforms keep
  point-to-point.

Measured on Intel Arc Pro B70, Wan2.2-A14B VAE bf16, 720x1280, 81 frames, I2V condition,
median of 3 (rel_l2 vs a 1-card untiled encode/decode):
- encode 4 ranks: tile 4.49 s (9.5e-3), rank split 3.17 s (3.0e-3), spatial_shard_width 3.06 s (1.2e-3)
- encode 8 ranks: tile 2.32 s (9.5e-3), rank split 1.63 s (3.8e-3), spatial_shard_width 1.83 s (1.2e-3)
- decode 4 ranks: tile 8.03 s -> spatial_shard_width 5.42 s (P2P halos 7.13 s)
- decode 8 ranks: tile 4.20 s -> spatial_shard_width 3.19 s (P2P halos 4.47 s)

Tests: CPU gloo parity (fp64, Wan2.1 and residual Wan2.2 VAEs, P2P and all-gather halos,
max diff < 1e-10), gate/dispatch unit tests, and a real-weights multi-card encode parity test.
xuechendi added a commit to xuechendi/vllm-omni that referenced this pull request Oct 23, 2026
…-gather halos on XPU

Spatial-shard encode
- Under vae_parallel_mode=spatial_shard_{height,width}, DistributedAutoencoderKLWan._encode
  shards the encoder along the split dimension with the existing halo modules, before the
  native-tiling check (as _decode does for decode since vllm-project#8968). It mirrors diffusers' untiled
  _encode loop and all-gathers the moments to every rank. It runs on a parameter-sharing copy
  of vae.encoder kept outside the module tree, so state_dict is unchanged and the original
  encoder still serves the fallback.
- Only taken when the split extent divides into stride-aligned shards
  (world_size * 2**downsamples); otherwise, with the encoder fast path installed, or with a
  partial VAE group, encode takes the regular path (tile/rank-split parallel when use_tiling).
- Fix: _patch_decoder_modules never replaced nn.ZeroPad2d, so the encoder's stride-2
  downsample padded every rank's edge. The decoder has no ZeroPad2d.

All-gather halo exchange on XPU
- A batched isend/irecv halo costs ~1.3 ms on XCCL against ~0.1-0.2 ms for an all-gather of
  the same rows; XPU now exchanges halos with one all_gather_into_tensor. Other platforms keep
  point-to-point.

Measured on Intel Arc Pro B70, Wan2.2-A14B VAE bf16, 720x1280, 81 frames, I2V condition,
median of 3 (rel_l2 vs a 1-card untiled encode/decode):
- encode 4 ranks: tile 4.49 s (9.5e-3), rank split 3.17 s (3.0e-3), spatial_shard_width 3.06 s (1.2e-3)
- encode 8 ranks: tile 2.32 s (9.5e-3), rank split 1.63 s (3.8e-3), spatial_shard_width 1.83 s (1.2e-3)
- decode 4 ranks: tile 8.03 s -> spatial_shard_width 5.42 s (P2P halos 7.13 s)
- decode 8 ranks: tile 4.20 s -> spatial_shard_width 3.19 s (P2P halos 4.47 s)

Tests: CPU gloo parity (fp64, Wan2.1 and residual Wan2.2 VAEs, P2P and all-gather halos,
max diff < 1e-10), gate/dispatch unit tests, and a real-weights multi-card encode parity test.

Signed-off-by: Chendi Xue <chendi.xue@intel.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

high priority high priority issue, needs to be done asap ready label to trigger buildkite CI xpu PR related to XPU

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants