Skip to content

[Bugfix][NPU] Support piecewise block-causal attention for full_attn_spans - #9244

Merged
hsliuustc0106 merged 2 commits into
vllm-project:mainfrom
YawningPants:pr-8876-1022-mine
Oct 23, 2026
Merged

hsliuustc0106 merged 2 commits into
vllm-project:mainfrom
YawningPants:pr-8876-1022-mine

Conversation

@YawningPants

@YawningPants YawningPants commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

This PR supersedes #8876, preserving its original implementation and adding Ascend A5 validation results.

Purpose

FlashAttentionImpl.forward_fa_npu never read
AttentionMetadata.full_attn_spans. A model that emits block-causal span
metadata — e.g. Qwen-Image-2.1 prefill — therefore ran full bidirectional
attention
on Ascend instead of the block-causal pattern, silently producing
wrong results: condition images attended to the future target image and text
attended to image blocks. Because the prefill K/V is written into the
cross-step KV cache, the corruption propagated through the entire denoising
trajectory.

forward_cuda already dispatches on full_attn_spans; only the NPU path was
missing it.

Changes

Mirror forward_cuda: route spans through the shared piecewise_attn segment
plan, so segment construction is identical on CUDA and NPU and only the
per-segment kernel differs. Both segment kinds run on
torch_npu.npu_fusion_attention:

  • causal segments (text runs) reuse the existing shared compressed
    (2048, 2048) mask with sparse_mode=3 (rightDownCausal). piecewise_attn
    gives causal segments kv_end == q_end in global coordinates, so they always
    satisfy Sq <= Skv, and bottom-right alignment reproduces FlashAttention's
    attn(Q[:, s:e], K[:, :e], causal=True) contract. No new mask is created —
    this is the same _get_npu_compressed_causal_mask the native causal path uses.
  • full segments (condition/target image blocks) pass no mask at all —
    sparse_mode is ignored when atten_mask is None — so they are plain
    bidirectional attention over the truncated K/V prefix.

The operator call is factored into _npu_fusion_attention so the pre-existing
explicit-causal branch and the new piecewise path share one call site.

_resolve_npu_execution_path now reports npu_piecewise instead of falling
through to npu_unverified on span metadata. The FP8/MX quantized path already
rejects spans ("piecewise attention requires the float attention path"), so
no change was needed there.

Test Plan

CPU regression on the final revision (e19cc3d): 121 passed, 5 deselected. Tests use mocked NPU kernels and cover execution-path resolution, per-segment kernel/mask arguments, numerical agreement with a dense block-causal reference, both causal settings for piecewise attention, and the causal-without-spans routing boundary.

A5 experiments used the quay.io/ascend/vllm-omni:v0.31.0rc1-a5 base image.

A5 comparison variants

Variant Description Source used for the reported A5 measurements
Before Pre-fix baseline: the NPU path ignored full_attn_spans and performed full bidirectional attention instead of the intended block-causal pattern. 4a910f239079
Golden Correctness reference: the fixed implementation manually forced to the explicit dense block-causal mask path with attn_path="mask". 4be8385d4c8b + mask override
After Fixed implementation using the default NPU piecewise path, following the shared CUDA/NPU segment plan to preserve the intended block-causal pattern. 4be8385d4c8b

These revisions identify the sources used for the recorded A5 measurements. The final revision (e19cc3d) additionally fixes execution-path reporting and expands CPU coverage without changing attention kernel execution.

Shared experiment settings

Item Configuration
Hardware / precision / batch Ascend A5 / BF16 / batch size 1
Model Qwen-Image-2.1, local checkpoint
Denoising steps Kernel profiling: 2; image comparison: 50
CFG scale 1.0
T2I prompt "A ceramic teapot on a wooden table"
T2I seed 142 (example default)
I2I prompt "Let this mascot dance under the moon, surrounded by floating stars"
I2I seed 42
I2I reference input qwen_bear.png, RGBA; downloaded from the public reference image
Saved output resolution 1024 × 1024
Additional warmup runs 0
Measured profiling runs 1 per variant

Generation follows the Qwen-Image 2.1 offline examples, using the inputs and settings listed above. Profiling and image generation use the same generation settings except for the denoising step count.

I2I reference image (saved locally as qwen_bear.png):

I2I reference image

Test Result

Timings are accumulated device-kernel durations from a single two-step profiling run per variant, without additional warmup. They are bounded observations, not end-to-end generation latency.

Performance — I2I, accumulated kernel durations in ms

Metric Before Golden After
Prefill attention, 32 layers 110.30 126.88 88.78
Prefill attention including adjacent layout/mask conversions 124.51 144.64 95.66
Decode attention, 32 layers 53.75 53.67 54.29
All recorded kernels 1313.78 1333.96 1286.77

After reduces prefill attention time by 30.03% versus Golden, or 33.86% including adjacent conversions. Decode remains approximately unchanged. Total recorded kernel time decreases by 3.54%.

Image similarity — RGB pixel range 0–255, 50 denoising steps

Comparison Task PSNR (dB) MAE Max difference Cosine similarity
After vs Golden T2I 50.1904 0.376066 43 0.99997708
After vs Golden I2I 32.1254 0.931129 226 0.99836570
After vs Before T2I 22.4684 11.858681 197 0.98650763
After vs Before I2I 15.7083 17.934840 255 0.93501114

After is substantially closer to Golden than Before in both tasks. These are descriptive similarity measurements; no predefined image-quality acceptance threshold was used.

@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-22T08:32:09.351495Z
We couldn’t complete this review. Comment @codex review to try again.
4be8385 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.

@mergify

mergify Bot commented Oct 22, 2026

Copy link
Copy Markdown

⚠️ The sha of the head commit of this PR conflicts with #8876. Mergify cannot evaluate rules on this PR. Once #8876 is merged or closed, Mergify will resume processing this PR. ⚠️

@vllm-omni-review-bot

Copy link
Copy Markdown

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

Module owners: @Isotr0py @david6666666 @princepride

Routing: @Isotr0py via module of the changed files, CODEOWNERS; @david6666666 via module of the changed files; @princepride via module of the changed files

@YawningPants, 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.

@vllm-omni-review-bot

vllm-omni-review-bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Omni ReviewBot triage note

Automated triage of commit 32e9ea480c34 produced:

  • Priority: high. Prompt maintainer attention is suggested.

These are automated triage suggestions only — the final decision belongs to the maintainers.

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

@YawningPants, the source change follows the shared segment plan, and the new tests exercise the real NPU wrapper. The following validation evidence is still needed for 4be8385d4c8b020c2496af9607aa8bd8c58396a3:

  • Attach the command and output for the three test_npu_piecewise_* tests in test_flash_attn.py, using the same tests against the baseline implementation 717f8a3499fb7dc6b3128b58a8baeacded1a7bbb and this head. Acceptance: the original span-dropping behavior fails the relevant assertions, while the fixed implementation passes.
  • Make the reported A5 Before/Golden/After comparison reproducible: link the script/commands and tested source revisions, software/model versions, prompt/reference inputs and seed, warmup/measured counts, and timing scope. Keep the existing BF16/batch-1/two-step setup matched, and state the quality acceptance criterion for the residual I2I difference. Acceptance: the supplied record supports the stated kernel-time comparison and output-quality conclusion; a bounded smoke observation should be labeled as such.

Source inspection only on my side; I did not execute the CPU or A5 tests. These are evidence requests, not a demonstrated new code defect.

@Dong1017 Dong1017 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed 4be8385: no findings. The NPU path now mirrors forward_cuda's piecewise dispatch (same shared segment plan; spans take precedence over masks and packed metadata on both platforms), the quantized path already rejects spans, and resolve_execution_path is descriptive-only, so the new npu_piecewise string has no runtime consumers.

Independent verification of head 4be8385:

  • CPU contract suite on an H200 host (vllm 0.31.0 venv): 55 passed, 5 deselected. Running the same head tests against the pre-fix implementation (4a910f2) fails exactly the three new piecewise tests (52 passed), so they reproduce the silent full-attention defect instead of passing unconditionally.
  • Kernel semantics on Ascend 910B1 (torch_npu 2.10.0.post4, CANN 9.1.0), npu_fusion_attention vs a dense fp32 reference: sparse_mode=3 with the shared compressed mask matches bottom-right-aligned causal for Sq==Skv, Sq<Skv, and Skv=4100 beyond the 2048 mask width (max_abs ≤ 4.6e-3); sparse_mode=0 with atten_mask=None matches full attention (≤ 3.8e-3). Misalignment would show as O(1) error. Note this is 910B1, not the A5 the PR validates on. Scripts: /data/dxw/tmp/pr9244-verify/ (H200), /dataset/home/dxw/pr9244/npu_smoke.py (910B1).

Two gates still block merge:

  • Make the DCO check pass on the head commit (currently action_required).
  • Close superseded #8876 so Mergify can evaluate this PR.

The A5 before/golden/after tables remain author-provided; they are internally consistent, and the residual I2I After-vs-Golden difference is already disclosed in the description.

# ``forward_cuda``: the caller emitted ``full_attn_spans`` instead of a
# mask, so the generic mask handling below cannot express it.
if attn_metadata is not None and attn_metadata.full_attn_spans is not None:
return self._forward_piecewise_npu(query, key, value, attn_metadata)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mirrors forward_cuda (flash_attn.py:697-721): spans take precedence over attn_mask and packed metadata on both platforms, and query_ranges is forwarded the same way. Verified on the head tree: CPU contract suite 55 passed; the same head tests against the pre-fix impl (4a910f2) fail exactly the three new piecewise tests (52 passed), so the defect is reproduced on base.

Inputs follow the model-side ``(B, S, H, D)`` convention == BSND.
"""
if causal:
atten_mask = _get_npu_compressed_causal_mask(query.device)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Device-checked this contract on Ascend 910B1 (torch_npu 2.10.0.post4): sparse_mode=3 with the shared (2048, 2048) strict-upper mask reproduces bottom-right-aligned causal against a dense reference for Sq==Skv (64/64), Sq<Skv (64/2048), and Skv=4100 beyond the mask width, max_abs ≤ 4.6e-3 (bf16 scale; misalignment would be O(1)). inner_precise=0 is right here since piecewise causal segments always have Sq ≤ Skv. Note 910B1, not the A5 this PR validates on.

assert full_call.args[0].shape == (1, 3, 2, 4)
assert full_call.args[1].shape == (1, 5, 2, 4)
assert full_call.kwargs["sparse_mode"] == 0
assert full_call.kwargs["atten_mask"] is None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same contract device-checked on 910B1: sparse_mode=0 with atten_mask=None matches plain full attention (128×512 and 4096×4096, max_abs ≤ 3.8e-3 vs an fp32 reference), so asserting the absent mask pins the right producer contract.

if attn_metadata is not None and attn_metadata.full_attn_spans is not None:
return replace(
result,
path="npu_piecewise",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking question: forward_fa_npu now takes the piecewise branch before its self.causal check, but this resolver still returns npu_unverified for self.causal (the early-return above fires first) — so for causal ∧ spans, routing reports unverified while execution runs piecewise. No current producer combines them (diffusion DiT impls are non-causal). Should the spans check move above the causal early-return, or is leaving that combination unverified intentional?

@YawningPants YawningPants Oct 22, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for catching this. Fixed in e19cc3d: the resolver now checks full_attn_spans before the self.causal early return, matching forward_fa_npu.

The three piecewise tests now cover both causal settings, with an additional causal-without-spans regression test. CPU regression: 121 passed, 5 deselected, using mocked NPU kernels. The new causal+spans routing test fails before the fix and passes afterward. Kernel execution is unchanged by this follow-up. DCO now passes.

``forward_fa_npu`` never read ``AttentionMetadata.full_attn_spans``, so a
model emitting block-causal span metadata (e.g. Qwen-Image-2.1 prefill)
silently ran full bidirectional attention on Ascend: condition images
attended to the future target image and text attended to image blocks.
Because the prefill K/V is written into the cross-step KV cache, the
wrong results propagated through the whole denoising trajectory.

Mirror ``forward_cuda`` and route spans through the shared ``piecewise_attn``
segment plan, so segment construction is identical on CUDA and NPU and only
the per-segment kernel differs. Both segment kinds run on
``torch_npu.npu_fusion_attention``:

- causal segments (text runs) reuse the shared compressed (2048, 2048) mask
  with ``sparse_mode=3`` (rightDownCausal). ``piecewise_attn`` gives causal
  segments ``kv_end == q_end``, so they always satisfy ``Sq <= Skv``, and
  bottom-right alignment reproduces FlashAttention's
  ``attn(Q[:, s:e], K[:, :e], causal=True)`` contract.
- full segments (condition/target image blocks) pass no mask at all --
  ``sparse_mode`` is ignored when ``atten_mask`` is ``None`` -- so they are
  plain bidirectional attention over the truncated K/V prefix.

The op call moves into ``_npu_fusion_attention`` so the pre-existing explicit
causal path and the new piecewise path share one call site.
``_resolve_npu_execution_path`` now reports ``npu_piecewise`` instead of
bailing out on span metadata, and the FP8/MX quantized path already rejects
spans ("piecewise attention requires the float attention path").

Tests (CPU, fake torch_npu, matching the file's existing NPU contract
section): the execution path resolves to ``npu_piecewise``; a two-segment
input issues one kernel call per segment with the expected mask/mode; and a
four-segment input matches a dense ``(q >= kv) or same_image_block``
reference.

Keep execution-path reporting aligned with forward_fa_npu when both
causal=True and full_attn_spans are present. Extend the CPU contracts to
both causal settings and retain the causal-without-spans boundary.

Resubmitted by YawningPants with A5 validation and CPU regression testing.
Original implementation: vllm-project#8876.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: YawningPants <54137998+YawningPants@users.noreply.github.com>
Signed-off-by: YawningPants <54137998+YawningPants@users.noreply.github.com>
@YawningPants

Copy link
Copy Markdown
Contributor Author

@hsliuustc0106 Thanks. The A5 configurations, reference input, comparison variants, run counts and timing scope are now documented in the PR description.

For the requested CPU comparison, the exact same test file from 4be8385 was used against baseline 717f8a3 and the reviewed fix 4be8385.

Equivalent pytest selection:

python -m pytest tests/diffusion/attention/test_flash_attn.py \
  -k 'test_npu_piecewise_' \
  -m 'core_model and cpu' \
  -vv --tb=short -p no:cacheprovider

Both runs were executed through a shared CPU harness. It supplied CPU kernel stand-ins, including the baseline MindIE dense fallback, and isolated an unrelated optional Mooncake shutdown issue. The command above shows the pytest selection, not the complete harness setup. Production routing and the test assertions were unchanged.

Observed output summary:

Baseline 717f8a3:
  test_npu_piecewise_reports_its_own_execution_path        FAILED
  test_npu_piecewise_runs_one_fusion_attention_per_segment FAILED
  test_npu_piecewise_matches_block_causal_reference       FAILED
  3 failed, 57 deselected; exit code 1

Reviewed fix 4be8385:
  All three tests PASSED
  3 passed, 57 deselected; exit code 0

The baseline failures were:

  • Execution path: npu_unverified instead of npu_piecewise.
  • Per-segment fusion calls: 0 instead of 2.
  • Dense-reference comparison: 48/72 mismatched elements, maximum absolute difference 1.9339167.

Separately, CPU regression on the final revision e19cc3d completed with 121 passed, 5 deselected, using mocked NPU kernels.

@Dong1017

Copy link
Copy Markdown
Contributor

@amy-why-3459 PTAL

@Bounty-hunter Bounty-hunter 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.

LGTM

@Bounty-hunter Bounty-hunter added diffusion codes related to diffusion models CI/CD codes related to changes to CI/CD labels Oct 23, 2026

@amy-why-3459 amy-why-3459 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.

Reviewed tip 32e9ea48 (feature commit e19cc3dc; tip itself is merge-from-main only). Covered forward_fa_npu / _forward_piecewise_npu / _npu_piecewise_attn_func / _npu_fusion_attention, _resolve_npu_execution_path, the quantized-path span rejection, and the new CPU contracts in test_flash_attn.py.

Dispatch

  • Spans take precedence in forward_fa_npu before the causal / mask branches, matching forward_cuda (flash_attn.py ~735–760): same shared piecewise_attn segment plan and query_ranges forwarding; only the per-segment kernel differs.
  • _resolve_npu_execution_path now checks full_attn_spans before the self.causal early-return, so causal ∧ spans reports npu_piecewise instead of npu_unverified. That closes the mismatch Dong flagged on 4be8385; test_npu_piecewise_reports_its_own_execution_path[True/False] and test_npu_causal_without_spans_stays_unverified pin both sides.
  • FP8/MX quantized NPU path still rejects spans ("piecewise attention requires the float attention path"); no change needed there.

Kernel contract

  • Causal segments: shared compressed (2048, 2048) mask + sparse_mode=3. piecewise_attn sets kv_end == q_end for those segments, so Sq <= Skv always holds and default inner_precise=0 is consistent with the explicit-causal branch (which only bumps inner_precise when Sq > Skv).
  • Full segments: atten_mask=None, sparse_mode=0 — Ascend ignores sparse mode when the mask is absent, so this is plain bidirectional attention over the truncated K/V prefix.
  • _npu_fusion_attention correctly shares one call site between the pre-existing explicit-causal path and the new piecewise path.

Tests

  • Per-segment call shapes/modes (test_npu_piecewise_runs_one_fusion_attention_per_segment) and dense block-causal numerical agreement (test_npu_piecewise_matches_block_causal_reference) look right for the intended Qwen-Image-2.1 prefill pattern. I did not re-run the suite or the A5 measurements here; Dong’s 910B1 kernel smokes and the author’s A5 Before/Golden/After tables are consistent with the claimed contract.

Process (not code)

  • Mergify still cannot evaluate while superseded #8876 shares the head SHA — that needs #8876 closed/merged before this PR can proceed through Mergify.

No new code defect found in this pass.

@Bounty-hunter Bounty-hunter added the ready label to trigger buildkite CI label Oct 23, 2026
@Dong1017

Dong1017 commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

@amy-why-3459 @Bounty-hunter the ci shows failure, is this an obstacle?

@hsliuustc0106
hsliuustc0106 merged commit 0be1493 into vllm-project:main Oct 23, 2026
10 of 11 checks passed
tzhouam pushed a commit to tzhouam/vllm-omni that referenced this pull request Oct 24, 2026
…spans (vllm-project#9244)

Signed-off-by: YawningPants <54137998+YawningPants@users.noreply.github.com>
Co-authored-by: changetheway <guotaoyuan1@h-partners.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: dengyunyang <584797741@qq.com>
(cherry picked from commit 0be1493)
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI/CD codes related to changes to CI/CD diffusion codes related to diffusion models ready label to trigger buildkite CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants