Skip to content

perf(qwen3-14b decode): fold RoPE into the CANN FAI attention extern - #796

Merged
ChaoWao merged 4 commits into
hw-native-sys:mainfrom
ChaoWao:feat/decode-fwd-rope-fused-attention
Jul 20, 2026
Merged

perf(qwen3-14b decode): fold RoPE into the CANN FAI attention extern#796
ChaoWao merged 4 commits into
hw-native-sys:mainfrom
ChaoWao:feat/decode-fwd-rope-fused-attention

Conversation

@ChaoWao

@ChaoWao ChaoWao commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Fold QK-norm and RoPE into the CANN FAI attention extern, removing the
    standalone RoPE dispatch and its producer-to-consumer dependency edge.
  • Run the generated RoPE body on its original 32 logical AIV workers; the
    remaining AIV workers skip RoPE but still participate in the mixed-core
    barrier and attention phase.
  • Remove the two redundant dsb(DSB_DDR) calls around the RoPE-to-attention
    SyncAll<false> boundary while retaining the cache-backed metadata barrier.
  • Add a contract regression test that ties the C++ worker guard to the Python
    ROPE_CORES setting and generated RoPE stride.
  • Add a reusable guide for on-device InCore timestamp capture, exact phase
    partitioning, and reconciliation with L2 task timing.
  • Record the C220/CANN 9.0.0 no-DSB evidence, validation matrix, near-tie
    diagnosis, and re-evaluation boundary in a dated investigation note.
  • Validate the no-DSB candidate in ten synthetic 40-layer-plus-LM-head device
    runs: 9 clean, 1 reference near-tie, 0 hard failures, sampled token 160/160,
    all 24,330,240 logits within 5e-2, and no NaN or Inf. The synthetic harness
    repeats one generated layer and is not a real-checkpoint accuracy test.

Related Issues

None.

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Qwen3 decode attention now uses a fused CANN paged-attention entry that performs QK-norm and RoPE phase processing, publishes rotated Q/K/V data, and then executes attention with updated scheduling dependencies and inputs.

Changes

Qwen3 fused RoPE attention

Layer / File(s) Summary
Attention contract and decode wiring
models/qwen3/14b/paged_attention_cce.py, models/qwen3/14b/decode_fwd.py
Adds and exports paged_attention_rope_cce, then routes decode attention through it with raw projections, normalization weights, RoPE tables, inverse-RMS state, and paging inputs.
Kernel entry and attention orchestration
models/qwen3/14b/kernels/paged_attention_cce/attention_rope/entry.cpp, models/qwen3/14b/kernels/paged_attention_cce/kernel/fai_body.hpp
Adds the CANN entry point, metadata handling, optional RoPE execution, typed tensor access, and mixed all-core synchronization before attention.
Phase-0 normalization and RoPE execution
models/qwen3/14b/kernels/paged_attention_cce/kernel/rope_phase0.hpp
Implements inverse-RMS/QK normalization, RoPE rotation, Q output generation, and paged K/V cache writes for assigned KV-head and batch items.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DecodeLayer
  participant AttentionBridge
  participant KernelEntry
  participant RopePhase0
  participant PagedAttention
  DecodeLayer->>AttentionBridge: Submit projections and paging inputs
  AttentionBridge->>KernelEntry: Launch fused attention kernel
  KernelEntry->>RopePhase0: Normalize and rotate query and key
  RopePhase0->>KernelEntry: Return completed phase zero writes
  KernelEntry->>PagedAttention: Synchronize and execute attention
Loading

Possibly related PRs

Poem

I’m a bunny in the kernel, hopping Q and K,
Norm then spin the RoPE wreath before attention’s way.
Cache the heads and signal all,
Let fused computations call—
Clean decode paths now bloom today!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: folding RoPE into the CANN attention extern for Qwen3-14B decode.
Description check ✅ Passed The description is related to the changeset and accurately describes the RoPE/attention fusion at a high level.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request refactors the Qwen3-14B decode forward pass by folding the QK-norm and RoPE operations directly into the external paged attention kernel as an in-kernel phase 0, replacing the standalone rope_qkv block with paged_attention_rope_cce. Feedback on the implementation points out a critical issue in rope_phase0.hpp where AscendC::ReduceSum is called with overlapping srcLocal and workLocal buffers (sq), which violates Ascend C constraints and can cause undefined behavior. It is recommended to use the idle normed tensor as the scratch workspace instead.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread models/qwen3/14b/kernels/paged_attention_cce/kernel/rope_phase0.hpp Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@models/qwen3/14b/kernels/paged_attention_cce/kernel/rope_phase0.hpp`:
- Around line 23-28: Correct the phase-0 documentation to distinguish Q/K RoPE
rotation from V scaling: update
models/qwen3/14b/kernels/paged_attention_cce/kernel/rope_phase0.hpp lines 23-28
to describe rotated Q/K and inverse-RMS-scaled V, update
models/qwen3/14b/decode_fwd.py lines 471-475 to say “RoPE'd K and scaled V,” and
make the same distinction in
models/qwen3/14b/kernels/paged_attention_cce/kernel/fai_body.hpp lines 194-196
before the synchronization description.
- Around line 35-37: Update the documentation comment near the paged cache row
calculation to identify cache_row_base as args[17], matching the fused ABI and
the implementation’s scalar index. Keep the cache-row formula and surrounding
explanation unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f408691c-b8c0-4562-a20b-fdcd40f6b1b2

📥 Commits

Reviewing files that changed from the base of the PR and between f894b63 and 00cd688.

📒 Files selected for processing (5)
  • models/qwen3/14b/decode_fwd.py
  • models/qwen3/14b/kernels/paged_attention_cce/attention_rope/entry.cpp
  • models/qwen3/14b/kernels/paged_attention_cce/kernel/fai_body.hpp
  • models/qwen3/14b/kernels/paged_attention_cce/kernel/rope_phase0.hpp
  • models/qwen3/14b/paged_attention_cce.py

Comment thread models/qwen3/14b/kernels/paged_attention_cce/kernel/rope_phase0.hpp Outdated
Comment thread models/qwen3/14b/kernels/paged_attention_cce/kernel/rope_phase0.hpp Outdated
@ChaoWao
ChaoWao force-pushed the feat/decode-fwd-rope-fused-attention branch from 40c8a58 to d3852f1 Compare July 18, 2026 14:09
ChaoWao added a commit to ChaoWao/pypto-lib that referenced this pull request Jul 19, 2026
…n blocker

WIP snapshot of the hw-native-sys#796 RoPE→FAI fusion debugging.

- Replace the hand-written rope_phase0 with the verbatim pypto-generated
  rope_qkv (rope_qkv_generated.hpp); partial-dump proves the RoPE now writes
  bit-exact K/V to the paged cache (the rope source is NOT the blocker — both
  the hand-written and generated ropes fail the FA identically).
- Add docs/investigations/2026-07-qwen3-rope-fai-fusion-accuracy.md recording
  the decisive findings (partial dump: cache+metadata correct; rope no-op:
  FA perfect => the RoPE's *execution* corrupts the FA globally), the full
  ruled-out table (coherency / metadata / topology / UB-TPipe / mask / events /
  lanes / SyncAll / FD-combine), the masking traps, the partial-dump recipe,
  and next steps (in-rope on-device bisection).

Blocker remains: the fused attention output is garbage; not yet fixed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ChaoWao

ChaoWao commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator Author

Investigation snapshot — attention still garbage (BLOCKED)

Pushed docs/investigations/2026-07-qwen3-rope-fai-fusion-accuracy.md with the full localization. TL;DR:

Decisively proven (a2a3, partial dump via pl.dump_tag + enable_dump_args=1):

  • The RoPE writes bit-exact-correct K/V to the paged cache, and metadata.kv_lengths/cumulative_q are correct — yet the FA output is garbage. So it's not the rope, coherency, metadata, or write location.
  • RoPE no-op isolation (if (false) rope_qkv(...), keep barrier+FA): the FA becomes perfect (attn_out == seed V bit-exact). → the fusion structure is fine; the RoPE's execution corrupts the FA compute, globally.

Ruled out on device (no change): read-coherency (full dcci = no-op), FD-combine (force NonFd), UB/TPipe reset, vector-mask reset, noinline, restricting rope to lanes 0-31, and removing SyncAll<false>. The compiled FA path uses resource.ptoTopology (not the broken native GetSubBlockIdx).

Masking traps (why it looked fine): fixture wo is tiny (out≈residual) + LM-head argmax/logits are robust → single-layer golden false-passes even with attention broken (guide §6). The sharp signal is a strict ratio_allclose on attn_out (99.7% wrong).

Sub-result: hand-written rope replaced with the verbatim pypto-generated rope_qkv (rope now bit-exact); the FA blocker is independent of the rope source.

Next: in-rope_qkv return; bisection (guide §7.3) + partial-dump of attn_out to bracket the corrupting statement, then reset the shared pto↔AscendC hw/SPR/framework state it touches.

@ChaoWao

ChaoWao commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator Author

✅ Root-caused, fixed, and verified on-device (a2a3)

Confirming the fix in 8b123fe from an independent debugging pass (partial
dump_tag on the fused op output vs. what out_proj reads):

  • The FA output buffer was always correct; out_proj consumed a different
    buffer that was bit-exact RoPE'd Q (q_tnd). A pl.jit.extern returns its
    first mutable arg; folding the rope made query: pl.InOut lead the
    signature, so attn_out = paged_attention_rope_cce(...) bound to query, not
    out. Placing out first (+ WithRope-conditional ABI remap) fixes it.
  • Ruled out en route (each had no effect, so not the cause): a writer-side
    PipeBarrier<PIPE_ALL>+dsb drain, dropping allow_early_resolve, and out: pl.InOut — the wrong value was deterministic, i.e. a wrong-buffer bind, not a
    write-back race or early dispatch.

Strict per-element out golden (ratio_allclose atol=rtol=3e-3, ≤2% outliers, not the argmax/logits proxy):

case result
seq_len=1, seed 1234 PASS
seq_len=1, seed 7 PASS
varied multi-block KV (seq_lens up to 3756), seed 1234 PASS

test_fused_attention_declares_real_output_first passes.

⚠️ Unrelated pre-existing CI note

test_loaded_kernel_signatures_match_contract_arg_counts and
test_compile_arg_builders_follow_loaded_stage_specs fail on the prefill
stage (prefill_fwd has 25 params, its contract spec lists 24). Both test
functions pre-date this PR (they exist on main); this PR touches neither
prefill_fwd nor the contract registry, so the drift is pre-existing (last
prefill change on main is #799) and should be triaged separately — it does
not gate this fix.

@ChaoWao
ChaoWao force-pushed the feat/decode-fwd-rope-fused-attention branch 2 times, most recently from fa93983 to 21196a6 Compare July 19, 2026 08:20
ChaoWao and others added 3 commits July 19, 2026 23:56
…on extern

Fold the QK-norm + RoPE prologue into the paged-attention extern as an
AIV-only phase 0: its vector lanes rotate Q, RMS-norm and rotate K, and
publish paged K plus projected V, then a SyncAll<false> cube<->vector FFTS
barrier gates the vendored FusedInferAttention (FAI) phase. This single
`paged_attention_rope_cce` op subsumes the former separate rope_qkv scope and
its dependency edge. The rope body is the verbatim pypto-generated rope_qkv
(rope_qkv_generated.hpp).

The extern declares `out` as its first parameter on purpose: a single-result
pl.jit.extern binds its return to the first Out/InOut parameter. With the
folded rope making `query` (the RoPE'd-Q staging buffer) pl.InOut, listing it
first made `attn_out = paged_attention_rope_cce(...)` return the rotated query
alias instead of the FAI output, so out_proj computed q_tnd @ wo. Placing
`out` first fixes the return binding. run_qwen_fai serves both the fused and
attention-only ABIs, so its query/key/value/block_table/out indices are
constexpr-selected on the WithRope template parameter.

Verified on a2a3 with the strict per-element `out` golden (ratio_allclose
atol=rtol=3e-3, <=2% outliers): seq_len=1 (seeds 1234/7) and varied
multi-block KV (seq_lens up to 3756) all pass. test_qwen3_14b_contract adds a
regression asserting `out` is the first output-like parameter.

Docs: add docs/cce-extern-kernel-guide.md — the mixed-extern programming and
debugging playbook (arg-packing, the return-first-mutable-arg ABI rule, orch
inspection, on-device return-bisection, and the partial-dump technique that
localized the wrong-buffer read).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The generated RoPE body is specialized for a standalone 32-lane
dispatch, while the fused mixed kernel exposes 48 AIV lanes.

Run RoPE only on the original 32 logical workers while every AIV
continues through the mixed-core barrier and attention. Add a contract
check tying the C++ guard to the generated stride.

Preserve both existing DDR barriers around SyncAll.
Document a reusable on-device timestamp pattern for mixed-core extern
kernels.

Cover per-core capture, collective interpretation, exact phase
partitions, and reconciliation with L2 task timing.

Link the guide from the performance, extern-kernel, and project
documentation indexes.
@ChaoWao
ChaoWao force-pushed the feat/decode-fwd-rope-fused-attention branch from 21196a6 to f6478a9 Compare July 20, 2026 06:58
Remove the two DSB_DDR calls around the mixed-core SyncAll boundary
while retaining the metadata barrier.

Record the C220/CANN evidence, ten-case full-depth validation,
near-tie diagnosis, and decision boundary. Link the Qwen-specific
result from the extern-kernel synchronization guide.
@ChaoWao
ChaoWao merged commit 8ef71cc into hw-native-sys:main Jul 20, 2026
8 checks passed
@ChaoWao
ChaoWao deleted the feat/decode-fwd-rope-fused-attention branch July 20, 2026 09:21
zhangqi-chen added a commit to zhangqi-chen/pypto-lib that referenced this pull request Jul 23, 2026
The qwen3-14b decode kernel was renamed to decode_fwd.py in hw-native-sys#796, leaving
every doc example pointing at a file that no longer exists.

- Repoint the path in README.md, AGENTS.md, .claude/CLAUDE.md,
  performance-tuning.md, tools/README.md, and the two profiling helpers'
  usage docstrings
- `--enable-pmu` is not on that kernel: the PMU example now uses
  deepseek/v4-flash/decode_sparse_attn.py and notes the
  runtime_cfg={"enable_pmu": 2} route for kernels without the flag
- `--export-kernel-insight` no longer exists on any kernel: the insight
  examples now call tools/export_all_kernel_insight.py directly
- Swap the deprecated `--runtime-profiling` alias for
  `--enable-l2-swimlane` in both helper docstrings
zhangqi-chen added a commit that referenced this pull request Jul 23, 2026
## Summary

Benchmark knobs in `golden/runner.py`:

- `PYPTO_BENCH_RAW` prints every measured dispatch's Effective sample
per rank — for L2, L3, and the L3 flatten fallback where the per-round
summaries are least trustworthy; off by default
- `PYPTO_BENCH_ROUNDS` / `PYPTO_BENCH_WARMUP` override the hardcoded 100
/ 5 loop sizes, which do not fit a long prefill or a multi-card L3 run;
a malformed value warns and falls back instead of failing the run
- The resident L3 path clamps warmup to >= 1 via `_resident_loop_sizes`:
it spends warmup[0] on the validation dispatch, so warmup=0 would emit
rounds+1 dispatches and lose per-round segmentation
- Drop `device_wall` from every report (headline, per-rank, L3 detail);
Effective is the metric consumed downstream. Daily CI sets none of the
new vars, so its 100 / 5 baseline and the `effective_us .*mean=` number
it collects are unchanged
- Unit tests for the knobs plus a guard that exactly one report line
matches daily CI's collector; `tests/golden/conftest.py` clears the
`PYPTO_BENCH*` vars, which previously failed 22 unrelated tests when
exported

Benchmark documentation:

- `docs/performance-tuning.md` gains a "Measuring" section ahead of Part
1: enabling the timed loop, what Effective is, the `mean=` field daily
CI collects, the real-device / `SIMPLER_PROFILING` requirements, reading
the multi-card per-rank and `fallback_flattened` output, and the four
`PYPTO_BENCH*` knobs
- `docs/compile-runtime-workflow.md` gains phase 4b — the loop runs
after the correctness dispatch and before validation, and was
undocumented

Commit / PR conventions in `.claude/skills/`:

- `git-commit`, `github-pr` and `fix-pr` rewritten against one shared
convention: the seven commit types, a body derived only from the staged
diff or `BASE_REF..HEAD`, `## Summary` as the sole PR section, and no AI
co-author footers
- `git-commit` gains the type table with per-type usage, the rule that a
`Perf:` body states measured before -> after plus the configuration, and
an explicit no-bypass rule for pre-commit hooks
- `github-pr` replaces the prose walk-through with the state table
(effectively-on-main x uncommitted), the commit-type-to-branch-prefix
table, and `--force-with-lease` after a rebase
- `fix-pr` drops the ASCII flow diagram for numbered steps with a
5-iteration cap

Documentation cleanup:

- Remove
`docs/investigations/2026-07-qwen3-fused-attention-sync-barrier.md`;
`cce-extern-kernel-guide.md` keeps the conclusion (the C220
MTE3/TSTORE-to-MTE2 path was validated over 40 layers without an extra
DDR barrier) and cites #796 in place of the deleted file
lyfne123 added a commit to lyfne123/pypto-lib that referenced this pull request Jul 27, 2026
decode_fwd was static batch-16: the public batch was pinned to the
padded pipeline width, so one compiled program could serve only a full
batch. The batch axes are now pl.dynamic and the live batch is read from
the seq_lens descriptor, matching what prefill_fwd already did.

- Separate the two conflated concepts. The padded pipeline width (the M
  of every matmul) is BATCH_PAD; the public batch is BATCH_DYN /
  `batch`. It is a pad width, not an architectural ceiling, so it is not
  named MAX_BATCH. The contract now spells prefill's ("USER_BATCH",) and
  decode's ("B",) as one ("BATCH",). paged_attention_cce's BATCH also
  served two roles and is split into METADATA_BATCH_SLOTS (physical
  metadata layout, must stay 16) and BATCH_PAD (standalone test-kernel
  shapes).

- Regenerate rope_qkv_generated.hpp with a runtime batch. The embedded
  body had batch=16 baked in as C++ literals and cannot be hand-edited:
  ptoas CSEs constants by value, so 16 is both the batch divisor and the
  Q-RMSNorm tile row count, and 128 is both HEAD_DIM and the item bound.
  Add rope_qkv_regen.py as the regeneration source the header's
  instruction referred to -- the scope it was generated from was deleted
  in hw-native-sys#796, leaving that instruction unusable. Its --emit-header re-wraps
  the artifact and reproduces the committed body byte for byte.

- fai_body reads the batch from the seq_lens descriptor, as the tiler
  does, so the RoPE and attention phases cannot disagree about how many
  sequences are live. A static_assert on the function-pointer type
  guards the arity of the hand-written arg mapping.

- Relax the contract to 1 <= max_batch_size <= batch_pad and add
  -b/--batch to the decode harness.

- Add a sync-safety invariant test over the generated body. Both guards
  are unreachable at the padded batch (max item id 127 < 128), so the
  skip path only goes live once the batch shrinks. The test simulates
  every reachable path -- neither block, first only, both; a later item
  cannot pass a guard the earlier one failed -- and requires that no
  wait_flag runs without an outstanding set_flag and that the epilogue
  drains every credit.

Validated on a2a3: batch-16 output is bit-identical to the pre-change
baseline; batch 1/2/3/5/8/15/16 all pass --validate-fwd; 4-layer runs
pass at batch 1 and 16. Single-layer makespan 981.3 -> 969.2 us over the
same 488 aicore tasks.

greedy_sample_fwd stays static batch and is documented as not composable
with a dynamic-batch decode; decode samples inline and does not use it.
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