Skip to content

fix(dsv4/sparse_attn): per-row inverse-RoPE to avoid a5 gather lowering bug#801

Closed
lwDavid wants to merge 3 commits into
hw-native-sys:mainfrom
lwDavid:fix/dsv4-sparse-attn-a5-inv-rope-gather
Closed

fix(dsv4/sparse_attn): per-row inverse-RoPE to avoid a5 gather lowering bug#801
lwDavid wants to merge 3 commits into
hw-native-sys:mainfrom
lwDavid:fix/dsv4-sparse-attn-a5-inv-rope-gather

Conversation

@lwDavid

@lwDavid lwDavid commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

What

Fixes an a5 (Ascend 950) correctness bug in the inverse-RoPE of DeepSeek-V4 sparse_attn (and the _hca / _swa variants).

Root cause

On a5 the tensor.gather flat-index lowering (PR #2044 area) miscompiles when the source tile spans more than one FP32 vector box (>8 rows). The inverse-RoPE in merge_norm did:

m_rope = n_full[0 : H_TILE, NOPE_DIM : HEAD_DIM]   # strided slice of [H_TILE=16, HEAD_DIM]
m_swapped = pl.gather(m_rope, dim=-1, index=m_swap_idx)   # [16, ROPE_DIM] gather

H_TILE=16 spans two vector boxes, so only tile rows 0 and 8 (the first head of each 8-head O_GROUP) received correct data — the ROPE columns of all other heads were corrupted. qkv_proj_rope is unaffected because its rotation tile is exactly [8, ROPE_DIM] (one box).

Evidence (measured on a5)

Bypassing proj and emitting pre-proj o_packed directly:

segment rel-L2 before rel-L2 after fix
NOPE 0.154 0.156
ROPE 0.417 0.153

Per-head ROPE rel-L2 before: ~0.15 for n_hh==0 heads vs ~0.44 for n_hh==1..7 (the smoking gun). After fix: uniform ~0.15 across all heads. decode_sparse_attn final fail rate 59.5% → 47.7%.

Fix

Unroll the inverse-RoPE per head-row using 1-row [1, ROPE_DIM] tiles (mr_swap_idx / mr_cos_il / mr_sin_signed at [1, ROPE_DIM], loop n_hi over H_TILE, pl.mul on matched-shape 1-row tiles). Every gather now fits within a single FP32 vector box regardless of H_TILE. The rotation math is identical to the original (computed per row), so a2a3 is unaffected.

Applied to all three sparse_attn variants: decode_sparse_attn.py, decode_sparse_attn_hca.py, decode_sparse_attn_swa.py.

Scope note

A separate, uniform ~15% base-attention precision divergence on a5 remains in the qk_pv / merge_norm path (individual ops pass but the composed attention is ~15% rel-L2 off vs <0.5% on a2a3). That is a different issue — likely a5 cube/vector precision at the compiler/simpler level, not a kernel lowering bug — and is tracked separately. This PR addresses only the inverse-RoPE gather lowering bug.

…ng bug

On a5 (Ascend 950) the tensor.gather flat-index lowering miscompiles when
the source tile spans more than one FP32 vector box (>8 rows). The inverse-
RoPE in merge_norm gathered a [H_TILE=16, ROPE_DIM] tile of a strided slice
of n_full, spanning two vector boxes, which corrupted the ROPE columns of
every head except the first of each 8-head O_GROUP (only tile rows 0 and 8
received correct data). qkv_proj_rope avoids this because its rotation tile
is exactly [8, ROPE_DIM] (one vector box).

Fix: unroll the inverse-RoPE per head-row using 1-row [1, ROPE_DIM] tiles
(mr_swap_idx / mr_cos_il / mr_sin_signed at [1, ROPE_DIM], loop n_hi over
H_TILE, pl.mul on matched-shape 1-row tiles). Every gather now fits within a
single FP32 vector box regardless of H_TILE. The rotation math is identical
to the original (computed per row), so a2a3 is unaffected.

Applied to all three sparse_attn variants (decode_sparse_attn, _hca, _swa).
Verified on a5: pre-proj o_packed ROPE rel-L2 drops 0.417 -> 0.153 (matching
NOPE) and the per-O_GROUP-head error pattern is eliminated.

A separate, uniform ~15% base-attention precision divergence on a5 remains
(qk_pv/merge_norm path) and is tracked independently; this commit addresses
only the inverse-RoPE gather lowering bug.
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 464aac4f-90fc-452b-a1c0-a499b0ff2d33

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Inverse RoPE rotation and packing were changed from bulk H_TILE processing to per-head-row processing in three DeepSeek v4 sparse attention implementations. Each row is gathered, rotated, converted to BF16, and written to the packed output.

Changes

Inverse RoPE packing

Layer / File(s) Summary
Per-head-row inverse RoPE rotation and packing
models/deepseek/v4/decode_sparse_attn.py, models/deepseek/v4/decode_sparse_attn_hca.py, models/deepseek/v4/decode_sparse_attn_swa.py
The three implementations replace bulk head-tile gathers and rotations with 1-row operations, converting each rotated row to BF16 and updating the packed output or intermediate tensor per head row.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested labels: bug

Poem

I’m a bunny with rows to rotate,
One head at a time through the gate.
Cosines hop, sines align,
BF16 packs up just fine.
Three paths now dance in a straighter line!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: per-row inverse-RoPE to avoid the a5 gather lowering bug.
Description check ✅ Passed The description matches the changeset and explains the bug, fix, scope, and measured impact.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

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.

@lwDavid lwDavid self-assigned this Jul 20, 2026
@lwDavid lwDavid added the bug Something isn't working label Jul 20, 2026
@lwDavid lwDavid moved this to Done in pto project Jul 20, 2026

@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 inverse RoPE computation across several decode sparse attention files to process one head-row at a time (1-row tiles) instead of using a fused multi-row gather, working around an A5 compiler bug where large gathers miscompile. The feedback suggests optimizing decode_sparse_attn_swa.py by reusing the precomputed rope_swap_idx rather than recomputing it from scratch.

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/deepseek/v4/decode_sparse_attn_swa.py 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.

🧹 Nitpick comments (1)
models/deepseek/v4/decode_sparse_attn_swa.py (1)

321-338: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Remove now-dead rope_swap_idx precompute.

Introducing the per-row mr_swap_idx here makes the earlier rope_swap_idx tensor (allocated and filled at lines 261-274, in the same rope_cs block) dead — it's no longer read anywhere in the function. That block still computes swap_ones/swap_col/... and writes into rope_swap_idx for nothing, wasting vector-engine work in the rope_cs pl.at scope on every call.

♻️ Proposed cleanup (lines 261-274)
-    rope_swap_idx = pl.create_tensor([H_TILE, ROPE_DIM], dtype=pl.INT32)
     with pl.at(level=pl.Level.CORE_GROUP, name_hint="rope_cs") as rope_tid:
-        swap_ones = pl.full([H_TILE, ROPE_DIM], dtype=pl.FP32, value=1.0)
-        swap_range_i32 = pl.arange(0, [1, ROPE_DIM], dtype=pl.INT32)
-        swap_range = pl.cast(swap_range_i32, target_type=pl.FP32)
-        swap_col = pl.col_expand_mul(swap_ones, swap_range)
-        swap_half = pl.mul(swap_col, 0.5)
-        swap_dup_i32 = pl.cast(swap_half, target_type=pl.INT32, mode="trunc")
-        swap_dup_f = pl.cast(swap_dup_i32, target_type=pl.FP32)
-        swap_lane = pl.sub(swap_col, pl.mul(swap_dup_f, 2.0))
-        swap_next = pl.add(swap_col, 1.0)
-        swap_stride = pl.mul(swap_lane, 2.0)
-        swap_idx_f = pl.sub(swap_next, swap_stride)
-        rope_swap_idx[:, :] = pl.cast(swap_idx_f, target_type=pl.INT32)
-
         cs_ones = pl.full([T, ROPE_INTERLEAVE_TILE], dtype=pl.FP32, value=1.0)
🤖 Prompt for 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.

In `@models/deepseek/v4/decode_sparse_attn_swa.py` around lines 321 - 338, Remove
the obsolete rope_swap_idx precomputation in the rope_cs block, including its
swap_ones/swap_col and related intermediate calculations and the writes through
pl.at. Keep the per-row mr_swap_idx logic used by the n_rope_bf16 loop
unchanged.
🤖 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.

Nitpick comments:
In `@models/deepseek/v4/decode_sparse_attn_swa.py`:
- Around line 321-338: Remove the obsolete rope_swap_idx precomputation in the
rope_cs block, including its swap_ones/swap_col and related intermediate
calculations and the writes through pl.at. Keep the per-row mr_swap_idx logic
used by the n_rope_bf16 loop unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 966ba3a4-dce1-4166-87b6-cc6a84a5d214

📥 Commits

Reviewing files that changed from the base of the PR and between 9931cac and a16fe03.

📒 Files selected for processing (3)
  • models/deepseek/v4/decode_sparse_attn.py
  • models/deepseek/v4/decode_sparse_attn_hca.py
  • models/deepseek/v4/decode_sparse_attn_swa.py

…erge_norm swap idx

Slice the head-invariant rope_swap_idx[0:1, :] instead of recomputing the
[1, ROPE_DIM] swap-index pattern from scratch inside merge_norm. Every row
of rope_swap_idx (filled in the rope_cs task) is identical, so the first
row is mathematically equal to the rebuilt pattern. Slicing drops the
redundant vector ops while keeping the per-row gather tile 1-row, so the
a5 gather-lowering fix is preserved.

A5-validated (decode_sparse_attn_swa, 4 runs): baseline 68.47/68.90% vs
change 69.09/68.84% fail ratio -- within ~0.4% run-to-run noise; max abs
diff unchanged (0.0011-0.0013). No correctness regression.

Addresses gemini-code-assist review on PR hw-native-sys#801.
…ASS)

PV-cube fusion workaround (decode_sparse_attn{,_hca,_swa}.py, prefill_sparse_attn.py):
on a5 the composed qk_pv task mis-fuses the exp->cast(bf16)->PV-cube chain, losing
~15% precision (cube bf16xbf16->fp32 precise standalone at K=128/256/512 b_trans+
no-b_trans; softmax row_max+exp precise; KV gather correct; mi/li correct; only the
fused PV output drifts). Materializing qk_exp_bf16 to GM before the PV cube breaks
the fusion -> precision restored. +13 PASS (decode/prefill sparse_attn x4,
decode/prefill_attention_{csa,hca,swa} x6, decode_layer/decode_mtp/prefill_mtp x3).
Survey: 21 -> 34 PASS.

hc_pre: lift the a5 guard for the core-count-agnostic _hc_pre_separate impl (the
syncall impl still needs 910B). +1 PASS (decode+prefill).

Real fix pending in pypto/simpler: find the fusion that loses precision on a5; then
remove the 4-file GM workaround (perf cost: one GM round-trip per PV cube).
@lwDavid
lwDavid force-pushed the fix/dsv4-sparse-attn-a5-inv-rope-gather branch from c8e391a to 00799b0 Compare July 21, 2026 06:20
@lwDavid lwDavid closed this Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant