Skip to content

[Performance] Balance the latent-MQA indexer's CP rows with a dual-chunk swap - #1762

Merged
xxyux merged 1 commit into
PaddlePaddle:developfrom
xxyux:feat/mqa-indexer-dualchunk-cp-develop
Aug 19, 2026
Merged

[Performance] Balance the latent-MQA indexer's CP rows with a dual-chunk swap#1762
xxyux merged 1 commit into
PaddlePaddle:developfrom
xxyux:feat/mqa-indexer-dualchunk-cp-develop

Conversation

@xxyux

@xxyux xxyux commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

PR Category

Performance Optimization

PR Types

Improvements

Description

Under contiguous CP the latent-MQA indexer scores each rank's own row slice, so with a causal mask its cost grows with the rank index: measured at 256k/cp16, 2.2ms on cp0 against 66.8ms on cp15 per layer per pass (a 30x spread), which makes the slowest rank do 1.94x the average while every other rank waits for it at the next collective. Only the indexer has this shape -- attention already runs a fixed index_topk + window columns per row, and the indexer backward a fixed index_topk, both measured flat across ranks.

mqa_indexer_cp_mode="dualchunk_p2p" splits the global sequence into 2 * cp_size chunks and has rank r score chunks (2r, 2*cp_size-1-2r) instead of its own (2r, 2r+1). The ids sum to 2*cp_size-1 everywhere, and a causal row's candidate count grows linearly with its global position, so the work is equal on every rank.

Three properties keep the change contained:

  • Only the indexer's per-row inputs move. query and the layer output stay put, which at s_local=16384/h=64 is 2.3GB of traffic not incurred.
  • Rank r keeps the chunk contiguous CP already gave it and swaps the other with rank cp_size-1-r, so the exchange is one point-to-point sendrecv per tensor rather than an all-to-all, and it is an involution -- the same call undoes it.
  • The permutation lives entirely inside paddle.no_grad() on detached inputs. The indexer gradient reaches the weights through the loss scaler applied to the unpermuted tensors, so nothing here has to be differentiable and the backward is byte-identical between the two modes.

The cuDNN indexer call is split in two because the kernel learns where its rows sit globally from q_causal_offsets, one scalar per batch, so a single affine map cannot describe two disjoint segments. Each chunk is internally contiguous and carries its own seq_offset -- the same shape as the query tiling the dense path already does. The results are concatenated back before the loss scaler sees them: applying it per chunk would halve target.shape[1], where its backward reads the row-count denominator, and double the gradient.

The layer output is bit-identical either way, which is what the new CP test asserts. That equality is only possible because the per-(row, column) score is itself bit-identical: a CTA covers q_stage * q_tokens_per_tile rows and both offsets are multiples of that, so a given global row lands in the same aligned tile and the kernel walks the same key blocks in the same order. Gradients are held to the rel ~2e-3 the shared cuDNN DSA backward already carries for dkv (atomics), with the off-vs-off pair checked against the same bound so the tolerance is visibly the kernel's and not this switch's.

是否引起精度变化

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

序号 位置 优先级 状态
1 indexer loss 梯度回归 P1
2 CP 模式校验契约 P2
Powered by Nyanpasu with gpt-5.5 xhigh, please check the suggestions carefully.

Comment thread tests/multi_card_tests/transformer/test_mqa_indexer_dualchunk_cp.py Outdated
Comment thread src/paddlefleet/transformer/transformer_config.py Outdated
@xxyux
xxyux force-pushed the feat/mqa-indexer-dualchunk-cp-develop branch from f1e8133 to 68a5450 Compare August 17, 2026 09:10
@xxyux
xxyux force-pushed the feat/mqa-indexer-dualchunk-cp-develop branch from 68a5450 to 2904008 Compare August 17, 2026 10:54
@xxyux

xxyux commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

/re-run all-failed

1 similar comment
@xxyux

xxyux commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

/re-run all-failed

@xxyux
xxyux force-pushed the feat/mqa-indexer-dualchunk-cp-develop branch from 2904008 to d43affc Compare August 18, 2026 03:42
…unk swap

Under contiguous CP the latent-MQA indexer scores each rank's own row slice, so
with a causal mask its cost grows with the rank index: measured at 256k/cp16,
2.2ms on cp0 against 66.8ms on cp15 per layer per pass (a 30x spread), which
makes the slowest rank do 1.94x the average while every other rank waits for it
at the next collective. Only the indexer has this shape -- attention already
runs a fixed ``index_topk + window`` columns per row, and the indexer backward a
fixed ``index_topk``, both measured flat across ranks.

``mqa_indexer_cp_mode="dualchunk_p2p"`` splits the global sequence into
``2 * cp_size`` chunks and has rank ``r`` score chunks ``(2r, 2*cp_size-1-2r)``
instead of its own ``(2r, 2r+1)``. The ids sum to ``2*cp_size-1`` everywhere, and
a causal row's candidate count grows linearly with its global position, so the
work is equal on every rank.

Three properties keep the change contained:

* Only the indexer's per-row inputs move. ``query`` and the layer output stay
  put, which at s_local=16384/h=64 is 2.3GB of traffic not incurred.
* Rank ``r`` keeps the chunk contiguous CP already gave it and swaps the other
  with rank ``cp_size-1-r``, so the exchange is one point-to-point sendrecv per
  tensor rather than an all-to-all, and it is an involution -- the same call
  undoes it.
* The permutation lives entirely inside ``paddle.no_grad()`` on detached inputs.
  The indexer gradient reaches the weights through the loss scaler applied to the
  *unpermuted* tensors, so nothing here has to be differentiable and the backward
  is byte-identical between the two modes.

The cuDNN indexer call is split in two because the kernel learns where its rows
sit globally from ``q_causal_offsets``, one scalar per batch, so a single affine
map cannot describe two disjoint segments. Each chunk is internally contiguous
and carries its own ``seq_offset`` -- the same shape as the query tiling the
dense path already does. The results are concatenated back before the loss
scaler sees them: applying it per chunk would halve ``target.shape[1]``, where
its backward reads the row-count denominator, and double the gradient.

The layer output is bit-identical either way, which is what the new CP test
asserts. That equality is only possible because the per-(row, column) score is
itself bit-identical: a CTA covers ``q_stage * q_tokens_per_tile`` rows and both
offsets are multiples of that, so a given global row lands in the same aligned
tile and the kernel walks the same key blocks in the same order. Gradients are
held to the rel ~2e-3 the shared cuDNN DSA backward already carries for ``dkv``
(atomics), with the off-vs-off pair checked against the same bound so the
tolerance is visibly the kernel's and not this switch's.
@xxyux
xxyux force-pushed the feat/mqa-indexer-dualchunk-cp-develop branch from d43affc to 8df527d Compare August 18, 2026 12:32
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.05882% with 2 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (develop@219e684). Learn more about missing BASE report.

Files with missing lines Patch % Lines
...rc/paddlefleet/transformer/mqa_latent_attention.py 93.10% 1 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             develop    #1762   +/-   ##
==========================================
  Coverage           ?   97.05%           
==========================================
  Files              ?        3           
  Lines              ?       68           
  Branches           ?       13           
==========================================
  Hits               ?       66           
  Misses             ?        1           
  Partials           ?        1           
Flag Coverage Δ
coverage_combine 97.05% <97.05%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/paddlefleet/transformer/cp_utils.py 100.00% <100.00%> (ø)
src/paddlefleet/transformer/transformer_config.py 100.00% <100.00%> (ø)
...rc/paddlefleet/transformer/mqa_latent_attention.py 93.10% <93.10%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ForFishes ForFishes left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@liuhao2638

Copy link
Copy Markdown
Contributor

@risemeup1111 再次review

@sneaxiy sneaxiy 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 for changes of transformer_config.py

@xxyux
xxyux merged commit b163b07 into PaddlePaddle:develop Aug 19, 2026
35 of 42 checks passed
DanielSun11 added a commit to DanielSun11/PaddleFleet that referenced this pull request Aug 19, 2026
develop's PaddlePaddle#1762 (dual-chunk indexer CP rebalance) appended both a new
``indexer_dualchunk`` attribute and three helper methods at the exact
end of ``MQALatentAttention.__init__`` where this branch had added
``global_kv_idx_remap_fusion``. Both additions are independent, so both
are kept.
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.

7 participants