perf(qwen3-14b decode): fold RoPE into the CANN FAI attention extern - #796
Conversation
📝 WalkthroughWalkthroughQwen3 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. ChangesQwen3 fused RoPE attention
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
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
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. Comment |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
📒 Files selected for processing (5)
models/qwen3/14b/decode_fwd.pymodels/qwen3/14b/kernels/paged_attention_cce/attention_rope/entry.cppmodels/qwen3/14b/kernels/paged_attention_cce/kernel/fai_body.hppmodels/qwen3/14b/kernels/paged_attention_cce/kernel/rope_phase0.hppmodels/qwen3/14b/paged_attention_cce.py
40c8a58 to
d3852f1
Compare
…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>
Investigation snapshot — attention still garbage (BLOCKED)Pushed Decisively proven (a2a3, partial dump via
Ruled out on device (no change): read-coherency (full Masking traps (why it looked fine): fixture Sub-result: hand-written rope replaced with the verbatim pypto-generated Next: in- |
✅ Root-caused, fixed, and verified on-device (a2a3)Confirming the fix in
Strict per-element
|
fa93983 to
21196a6
Compare
…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.
21196a6 to
f6478a9
Compare
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.
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
## 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
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.
Summary
standalone RoPE dispatch and its producer-to-consumer dependency edge.
remaining AIV workers skip RoPE but still participate in the mixed-core
barrier and attention phase.
dsb(DSB_DDR)calls around the RoPE-to-attentionSyncAll<false>boundary while retaining the cache-backed metadata barrier.ROPE_CORESsetting and generated RoPE stride.partitioning, and reconciliation with L2 task timing.
diagnosis, and re-evaluation boundary in a dated investigation note.
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 harnessrepeats one generated layer and is not a real-checkpoint accuracy test.
Related Issues
None.