Skip to content

perf(dsv4): qproj wq_b transpose + serving contract; sparse_attn/indexer tuning#676

Open
Hzfengsy wants to merge 4 commits into
hw-native-sys:mainfrom
Hzfengsy:dsv4-qproj-indexer-perf
Open

perf(dsv4): qproj wq_b transpose + serving contract; sparse_attn/indexer tuning#676
Hzfengsy wants to merge 4 commits into
hw-native-sys:mainfrom
Hzfengsy:dsv4-qproj-indexer-perf

Conversation

@Hzfengsy

@Hzfengsy Hzfengsy commented Jul 2, 2026

Copy link
Copy Markdown
Member

Summary

DeepSeek-V4 decode perf work plus the cube-tile-tuning tooling behind it. Four independent commits.

1. docs(cube-tile-tuning) — rules + b_trans tooling

  • .claude/rules/optimization-rules.md: Rule 1 (single-pipeline if-first cube K-reduction) + Rule 2 (PH001 L2 innermost-dim granularity, 512 B a2a3 / 128 B a5).
  • Skill: layout-aware --b-trans (transposed [N,K] K-contiguous weight) in tile_budget.py/hint_l1_tile.py; transpose-lever section in SKILL.md.

2. perf(dsv4 sparse_attn) — if-first pipeline for proj_a/proj_b

Fold each cube K-reduction into one stage=2 pipeline (k==0 → matmul, else matmul_acc); iter 0 stays inside the pipeline so its prologue prefetches iter 1. −15.7 % proj_b_mm (722 → 609 µs aggregate). Reference impl for Rule 1.

3. perf(dsv4 qproj) — wq_b transpose + serving contract + save-cores ✅ main win

  • Store wq_b transposed [H*HEAD_DIM, Q_LORA], consume via b_trans=True → K-contiguous MTE2 (DN2ZN load). qproj_matmul 55.8 → 31.9 µs; decode span ~−15 µs. Per-output-channel scale is transpose-invariant. Propagated through decode/prefill attention + layer + fwd.
  • ⚠️ Serving-framework change required: emit wq_b as [N,K] (scale unchanged). Contract consolidated under a grep-able [WEIGHT-PERMUTE] block in decode_layer.py / prefill_layer.py.
  • qr_proj_matmul QR_N_TILE 128 → 256 (8 tasks) to free ~8 cube cores — a deliberate +5.6 µs qkv-leaf tradeoff for core headroom (seeded a2a3, 161.5 → 167.1 µs median; recorded inline).

4. perf(dsv4 indexer) — fuse qr_hadamard_quant into score ⚠️ WIP / regression

Hoist the per-token query hadamard-transform + INT8 quant into the score task head (sole consumer; IDX_N_HEADS == QH_QUANT_TILE → 1:1 per-token map). Golden PASS (score + topk_idxs), but a measured net regression — ~128 µs median (110–134, unstable) vs ~114 µs (112–123). A cube B-operand must route through GM, so the fused intra-task write→read serializes worse than the 2-scope pipeline. Kept per request; WIP pending the on-chip fix (query as matmul A-operand + transposed score epilogue). Baseline preserved locally at decode_indexer.py.bak (== HEAD~1).

Why draft

Commit 4 is an intentional WIP regression. Commit 3's wq_b transpose is the ready-to-merge win but shares this branch; can split on request.

Validation

  • qkv_proj_rope decode/prefill golden PASS; sparse_attn if-first −15.7 % measured.
  • indexer fused: standalone golden PASS; perf regression documented above.
  • decode_fwd full-path compile: **PASS** (exit 0).

Hzfengsy added 4 commits July 2, 2026 18:41
…b_trans support

Add .claude/rules/optimization-rules.md:
- Rule 1: express a cube K-reduction as ONE stage=2 pipeline with an if-first
  branch (k0==0 -> matmul, else matmul_acc). Folding chunk 0 into the pipeline lets
  its prologue prefetch chunk 1, so an MTE2-bound cube stays busy from iter 0.
- Rule 2: keep L2-traversing tile.load/store innermost dim >= the backend
  granularity (PH001 TileInnermostDimGranularity; 512B on a2a3, 128B on a5).

cube-tile-tuning skill: add --b-trans (transposed [N,K], K-contiguous weight)
support to tile_budget.py and hint_l1_tile.py with a layout-aware cache-line floor
(N-contig for [K,N], K-contig for [N,K]); document the transpose lever in SKILL.md.
Fold each cube K-reduction into ONE stage=2 pipeline that starts at chunk 0 with an
if-first branch (k==0 -> matmul, else matmul_acc), instead of peeling the first
matmul outside the loop. Iteration 0 stays a real matmul (matmul_acc from a zero
carry trips the TLOAD DN->NZ assertion, pypto#1540) but now sits inside the pipeline
so its stage=2 prologue prefetches iter 1 -- the INT8 cube stays busy from the first
chunk. Measured -15.7% proj_b_mm (722 -> 609us aggregate exec). Reference impl for
optimization-rules Rule 1.
…t; qr_proj save-cores tile

Store wq_b transposed [H*HEAD_DIM, Q_LORA] and consume it via b_trans=True so the
qproj MTE2 weight stream is K-contiguous (DN2ZN load): fewer/longer bursts, measurably
more MTE2-efficient. qproj_matmul 55.8 -> 31.9us, decode span ~-15us. Per-output-channel
scale is transpose-invariant. Propagated through decode/prefill attention, layer, and
fwd; the serving-framework weight-layout contract is consolidated under a grep-able
[WEIGHT-PERMUTE] block in decode_layer.py and prefill_layer.py (serving must emit wq_b
as [N,K], scale unchanged).

Also qr_proj_matmul: QR_N_TILE 128 -> 256 (ON=4, OK=2 -> 8 spmd tasks) to free ~8 of
the 24 cube cores for other work. This is a DELIBERATE core-headroom tradeoff: measured
+5.6us on the isolated qkv leaf (seeded decode a2a3, 161.5 -> 167.1us median); the
2-scope core-budget sweep confirmed it, and the measurement is recorded inline.
Hoist the per-token query hadamard-transform + INT8 quant into the head of each score
task (score is its sole consumer and IDX_N_HEADS == QH_QUANT_TILE gives a 1:1 per-token
map), removing the standalone qr_hadamard_quant scope. Golden PASS (score + topk_idxs).

MEASURED: this is a net regression vs the 2-scope baseline -- ~128us median (110-134,
unstable) vs ~114us (112-123) on a2a3 decode. A cube B-operand must route through GM,
so the fused intra-task query write->read serializes worse than the 2-scope form, which
pipelines that vector->cube handoff across tasks. Kept as the default path per request;
WIP pending the on-chip fix (feed the query as the matmul A-operand + transposed score
epilogue). The 2-scope baseline is preserved locally at decode_indexer.py.bak.
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds performance-rule documentation and extends cube-tile-tuning scripts with a --b-trans flag for transposed-weight tile analysis. It changes the wq_b tensor layout from [Q_LORA, H*HEAD_DIM] to [H*HEAD_DIM, Q_LORA] across DeepSeek-V4 decode/prefill kernels, refactors matmul K-reduction loops to an if-first pipeline pattern in decode_sparse_attn.py, and fuses hadamard/INT8 query quantization into the score task in decode_indexer.py.

Changes

Documentation and Tuning Tools

Layer / File(s) Summary
Optimization rules doc
.claude/rules/optimization-rules.md
New doc defines the if-first pipeline pattern for cube matmul K-reductions and the L2 tile-transfer contiguity rule with PH001 diagnostics.
b_trans tuning tool support
.claude/skills/cube-tile-tuning/SKILL.md, .../hint_l1_tile.py, .../tile_budget.py
Adds --b-trans flag and layout-dependent cache-line/DMA accounting for transposed weight layouts.

Estimated code review effort: 2 (Simple) | ~12 minutes

wq_b Transposed Layout Migration

Layer / File(s) Summary
qkv_proj_rope kernel
models/deepseek/v4/qkv_proj_rope.py
wq_b becomes [H*HEAD_DIM, Q_LORA]; matmul uses b_trans=True with if-first pipelined reduction; tiling constants retuned; golden/test paths transpose accordingly.
Decode attention kernels
decode_attention_csa.py, decode_attention_hca.py, decode_attention_swa.py
wq_b signatures and test fixtures updated to the transposed layout.
Decode forward/layer drivers
decode_fwd.py, decode_layer.py
Packed wq_b slicing and layout contracts updated to match transposed shape.
Prefill attention kernels
prefill_attention_csa.py, prefill_attention_hca.py, prefill_attention_swa.py
Same wq_b layout update applied to prefill attention entrypoints and fixtures.
Prefill forward/layer drivers
prefill_fwd.py, prefill_layer.py
Packed wq_b indexing/slices and a new serving weight-layout contract block updated.

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

Pipeline Refactor and Indexer Fusion

Layer / File(s) Summary
If-first pipeline for output projection
decode_sparse_attn.py
proj_a/proj_b_mm reductions replace peeled-first-matmul with a single if-first pipelined loop.
Fused hadamard/INT8 query quantization
decode_indexer.py
Standalone quantization scope removed; fused per-token quantization moved inside score task, changing the matmul query slice source.

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

Sequence Diagram(s)

sequenceDiagram
  participant ScoreTask as score task
  participant Hadamard as Hadamard transform
  participant Quant as INT8 quantizer
  participant QBuffer as qr_hadamard_i8
  participant Matmul as score matmul

  ScoreTask->>Hadamard: compute qh_nope/qh_rope from qr_proj_flat
  Hadamard->>Quant: per-head amax reduction
  Quant->>Quant: compute dequant scale
  Quant->>QBuffer: write INT8 rows at qh_row0
  ScoreTask->>Matmul: slice QBuffer[qh_row0] vs cached KV tile
  Matmul-->>ScoreTask: accumulated score
Loading

Possibly related PRs

Suggested labels: enhancement

Poem

A weight once lay as [K, N],
Now flipped and transposed again,
With if-first hops through k0's gate,
No more peeling matmuls, straight!
This bunny thumps in tuned delight —
Cache lines snug, and tiles just right. 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main changes: qproj wq_b transpose/serving contract plus sparse_attn and indexer tuning.
Description check ✅ Passed The description clearly covers the cube-tile-tuning docs, sparse_attn pipeline change, qproj transpose, and indexer fusion.
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.

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 introduces performance optimizations and structural updates for PyPTO-Lib kernels, specifically targeting DeepSeek-V4. It establishes a new weight-layout contract where the Q-LoRA up-projection weight wq_b is stored transposed as [H * HEAD_DIM, Q_LORA] and consumed via b_trans=True to ensure K-contiguous DMA transfers, significantly improving MTE2 efficiency. Correspondingly, optimization rules, tuning scripts (hint_l1_tile.py, tile_budget.py), and attention layer signatures across decode and prefill paths have been updated to support this transposed layout. Additionally, the matmul K-reductions in qkv_proj_rope.py and decode_sparse_attn.py were refactored into a single-pipeline if k==0 form to maximize load/compute overlap, and the query-side hadamard-quantization scope in decode_indexer.py was fused into the score task to reduce dispatch overhead. There are no review comments to assess, so I have no feedback to provide.

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.

@Hzfengsy Hzfengsy marked this pull request as ready for review July 2, 2026 10:56

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9b887ccbd8

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# task: the INT8 query lands in the qr_hadamard_i8 GM buffer (a cube B-operand
# must come via GM) and its per-head dequant scale qh_scale_s [1, IDX_N_HEADS]
# stays in UB for reuse. Merging the scope drops the ~7us dispatch gap into score.
qh_row0 = qb + s * IDX_N_HEADS

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep regressed indexer fusion off the default path

For CSA decode layers, this newly inlined hadamard/quant path now runs unconditionally inside every score task, but the measurements added just above report the fused form as a net regression (~128us median vs ~114us for the previous two-scope path) because the same-task GM read-after-write serializes the query handoff. That means production decode takes the slower path by default; keep the two-scope implementation as the default or gate this WIP until the on-chip query rework is available.

Useful? React with 👍 / 👎.

@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: 1

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

155-171: 🚀 Performance & Scalability | 🔵 Trivial

Fused path is a documented net regression shipping as the default decode path.

The block clearly documents that this fused form regresses (~128us vs ~114us baseline) and is intentional/WIP per request, with the 2-scope baseline preserved in decode_indexer.py.bak. No correctness concern (golden passes), but shipping a known decode-latency regression as the default is worth tracking so the follow-up rework (keep the query on-chip as the matmul A-operand + transposed score epilogue) isn't lost.

Want me to open a tracking issue capturing the on-chip A-operand rework and the .bak baseline reference?

🤖 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_indexer.py` around lines 155 - 171, The default
decode path in score is a documented latency regression, so make the fused
qr_hadamard_quant-in-score flow non-default or revert the default to the
preserved two-scope baseline. Update the score path selection in
decode_indexer.py so the known-regressing fused implementation is behind a
flag/WIP guard, and keep the baseline path as the default until the on-chip
A-operand rework and transposed epilogue are implemented. Preserve the existing
qr_hadamard_quant reference and the .bak baseline as the fallback/tracking
anchor.
🤖 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 @.claude/skills/cube-tile-tuning/tile_budget.py:
- Around line 144-158: The `k_fit` computation in `tile_budget.py` uses
`a.cache_line` as if it were an element count in the `a.b_trans` branch, but
`k_max` is in elements; update the rounding to use the already computed
`cl_elems` (elements per cache line) in the `if a.b_trans` path of the
tile-budget logic. Keep the existing `n_fit` behavior, and make sure the
follow-up log message in the `print(...)` block still reports the corrected
`k_fit` value for `--b-trans` mode.

---

Nitpick comments:
In `@models/deepseek/v4/decode_indexer.py`:
- Around line 155-171: The default decode path in score is a documented latency
regression, so make the fused qr_hadamard_quant-in-score flow non-default or
revert the default to the preserved two-scope baseline. Update the score path
selection in decode_indexer.py so the known-regressing fused implementation is
behind a flag/WIP guard, and keep the baseline path as the default until the
on-chip A-operand rework and transposed epilogue are implemented. Preserve the
existing qr_hadamard_quant reference and the .bak baseline as the
fallback/tracking anchor.
🪄 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: 26104726-cae2-4024-a397-03d28e3d23dc

📥 Commits

Reviewing files that changed from the base of the PR and between 98271e1 and 9b887cc.

📒 Files selected for processing (17)
  • .claude/rules/optimization-rules.md
  • .claude/skills/cube-tile-tuning/SKILL.md
  • .claude/skills/cube-tile-tuning/hint_l1_tile.py
  • .claude/skills/cube-tile-tuning/tile_budget.py
  • models/deepseek/v4/decode_attention_csa.py
  • models/deepseek/v4/decode_attention_hca.py
  • models/deepseek/v4/decode_attention_swa.py
  • models/deepseek/v4/decode_fwd.py
  • models/deepseek/v4/decode_indexer.py
  • models/deepseek/v4/decode_layer.py
  • models/deepseek/v4/decode_sparse_attn.py
  • models/deepseek/v4/prefill_attention_csa.py
  • models/deepseek/v4/prefill_attention_hca.py
  • models/deepseek/v4/prefill_attention_swa.py
  • models/deepseek/v4/prefill_fwd.py
  • models/deepseek/v4/prefill_layer.py
  • models/deepseek/v4/qkv_proj_rope.py

Comment on lines +144 to +158
if a.b_trans:
# K is contiguous -> keep K on a cache-line multiple; N is free (16-multiple).
n_fit = max(0, int(n_max // 16 * 16))
k_fit = max(0, int(k_max // a.cache_line * a.cache_line))
else:
# N is contiguous -> keep N >= cache-line floor; K is free (16-multiple).
n_fit = max(0, int(n_max // 16 * 16))
k_fit = max(0, int(k_max // 16 * 16))
print(f" Mat is the wall: at K={a.K}, N<={n_fit} fits; at N={a.N}, K<={k_fit} fits.")
print(f" -> trade K down to {k_fit} (>= cache-line {a.cache_line}) to keep N={a.N}, "
f"or shrink N to {n_fit}.")
if a.b_trans:
print(f" -> weight is K-contiguous: keep K>={a.cache_line}B (K>={cl_elems} elems), "
f"trade N down to {n_fit} to keep K={a.K}, or shrink K to {k_fit}.")
else:
print(f" -> weight is N-contiguous: keep N>={a.cache_line}B (N>={cl_elems} elems), "
f"trade K down to {k_fit} to keep N={a.N}, or shrink N to {n_fit} (>= {cl_elems}).")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

k_fit rounds by byte count instead of element count in the --b-trans branch.

k_max is an element count, but Line 147 rounds it to a multiple of a.cache_line (bytes), not cl_elems (elements-per-cache-line, already computed at Line 133). This only happens to be correct when bytes_in == 1 (INT8, the CLI default); for BF16 (bytes_in=2) or FP32 weights, cl_elems is 256/128 while the code rounds to 512, silently producing wrong (often 0) tile-shrink suggestions in --b-trans mode — exactly the mode this PR adds support for.

🐛 Proposed fix
             if a.b_trans:
                 # K is contiguous -> keep K on a cache-line multiple; N is free (16-multiple).
                 n_fit = max(0, int(n_max // 16 * 16))
-                k_fit = max(0, int(k_max // a.cache_line * a.cache_line))
+                k_fit = max(0, int(k_max // cl_elems * cl_elems))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if a.b_trans:
# K is contiguous -> keep K on a cache-line multiple; N is free (16-multiple).
n_fit = max(0, int(n_max // 16 * 16))
k_fit = max(0, int(k_max // a.cache_line * a.cache_line))
else:
# N is contiguous -> keep N >= cache-line floor; K is free (16-multiple).
n_fit = max(0, int(n_max // 16 * 16))
k_fit = max(0, int(k_max // 16 * 16))
print(f" Mat is the wall: at K={a.K}, N<={n_fit} fits; at N={a.N}, K<={k_fit} fits.")
print(f" -> trade K down to {k_fit} (>= cache-line {a.cache_line}) to keep N={a.N}, "
f"or shrink N to {n_fit}.")
if a.b_trans:
print(f" -> weight is K-contiguous: keep K>={a.cache_line}B (K>={cl_elems} elems), "
f"trade N down to {n_fit} to keep K={a.K}, or shrink K to {k_fit}.")
else:
print(f" -> weight is N-contiguous: keep N>={a.cache_line}B (N>={cl_elems} elems), "
f"trade K down to {k_fit} to keep N={a.N}, or shrink N to {n_fit} (>= {cl_elems}).")
if a.b_trans:
# K is contiguous -> keep K on a cache-line multiple; N is free (16-multiple).
n_fit = max(0, int(n_max // 16 * 16))
k_fit = max(0, int(k_max // cl_elems * cl_elems))
else:
# N is contiguous -> keep N >= cache-line floor; K is free (16-multiple).
n_fit = max(0, int(n_max // 16 * 16))
k_fit = max(0, int(k_max // 16 * 16))
print(f" Mat is the wall: at K={a.K}, N<={n_fit} fits; at N={a.N}, K<={k_fit} fits.")
if a.b_trans:
print(f" -> weight is K-contiguous: keep K>={a.cache_line}B (K>={cl_elems} elems), "
f"trade N down to {n_fit} to keep K={a.K}, or shrink K to {k_fit}.")
else:
print(f" -> weight is N-contiguous: keep N>={a.cache_line}B (N>={cl_elems} elems), "
f"trade K down to {k_fit} to keep N={a.N}, or shrink N to {n_fit} (>= {cl_elems}).")
🤖 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 @.claude/skills/cube-tile-tuning/tile_budget.py around lines 144 - 158, The
`k_fit` computation in `tile_budget.py` uses `a.cache_line` as if it were an
element count in the `a.b_trans` branch, but `k_max` is in elements; update the
rounding to use the already computed `cl_elems` (elements per cache line) in the
`if a.b_trans` path of the tile-budget logic. Keep the existing `n_fit`
behavior, and make sure the follow-up log message in the `print(...)` block
still reports the corrected `k_fit` value for `--b-trans` mode.

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