Repository navigation
[PD] Charge each state component its own item length in the KV transfer metric - #33806
Open
AMD-yanfeiwang wants to merge 1 commit into
Open
AMD-yanfeiwang wants to merge 1 commit into
AMD-yanfeiwang wants to merge 1 commit into
Conversation
AMD-yanfeiwang
requested review from
ByronHsu,
HaiShaw,
ShangmingCai,
hnyls2002 and
sogalin
as code owners
August 6, 2026 03:49
`get_transfer_metric()` computed the state contribution as a product of two
independently flattened sums:
self._transfer_num_state_indices * self.kv_mgr.state_item_lens_sum
`state_item_lens` is `List[List[int]]` -- per component, then per layer -- and
`_record_transfer_indices` collapsed the index side the same way, adding
`len(component_indices)` into one counter without keeping the component. The
product is therefore a cross product: every index of every component is charged
the item length of every OTHER component as well.
With N components of index counts n_i and item-length sums L_i, the reported
figure is (Σ n_i)(Σ L_i) instead of Σ n_i·L_i -- inflated by exactly the cross
terms. It is right only when a single component is in play, which is why the
existing test (one component) could not see it.
The overcharge is worst when component sizes are lopsided, which is the normal
case: a sliding-window ring ships a short index list against a much longer
compressed-state one, so each pays the other's price. It also makes the metric
partly insensitive to transfer length, because the cross terms scale with the
other component's size rather than with what this component actually sent.
Accumulate the bytes where the component is still known, rather than trying to
reconstruct them afterwards from two sums that have each lost the association.
The KV side keeps its scalar product: all layers ship the SAME page indices, so
one index really does cost the sum over layers.
Applies to every PD backend -- mooncake, nixl and mori all inherit
`CommonKVSender._record_transfer_indices` and none override it.
AMD-yanfeiwang
force-pushed
the
fix-pd-state-transfer-bytes
branch
from
August 6, 2026 03:58
fe70a22 to
d986b4f
Compare
5 tasks done
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
CommonKVSender.get_transfer_metric()computes the state contribution as a product of two independently flattened sums:KVArgs.state_item_lensisList[List[int]]— per component, then per layer — and it is flattened at construction:_record_transfer_indicesflattens the index side the same way, addinglen(component_indices)into a single counter without keeping track of which component it came from.The product of those two is a cross product. With
Ncomponents of index countsn_iand item-length sumsL_i, the metric reports— inflated by exactly the cross terms
Σ_{i≠j} n_i · L_j. Every index of every component is charged the item length of every other component as well.It is correct only when a single component is in play, which is why
test_kv_transfer_replica_metric.py(one component) never caught it.Why it matters
The overcharge is worst when component sizes are lopsided, which is the normal case. A sliding-window ring ships a short index list; a compressed-state component ships a much longer one. Each ends up paying the other's price, and the two errors do not cancel.
It also makes the metric partly insensitive to transfer length: the cross terms scale with the other component's size rather than with what this component actually sent, so a large share of the reported bytes does not move when the real transfer does. That defeats the metric's main use — telling whether a change reduced KV traffic.
Concretely, with 2 components — 3 indices at 10 B and 1 index at 1000 B:
3·10 + 1·1000)(3+1)·(10+1000))Change
Accumulate state bytes where the component association still exists, instead of trying to reconstruct them afterwards from two sums that have each lost it:
The KV side keeps its scalar product — all layers ship the same page indices, so one KV index genuinely costs the sum over layers. Only the state side needed the per-component treatment.
state_item_lens_sumbecomesstate_item_lens_per_component, and the sender's_transfer_num_state_indicescounter becomes_transfer_state_bytes. Both were private to this path; no other caller in the tree reads either.Scope
Applies to every PD backend: mooncake, nixl and mori all call the inherited
CommonKVSender._record_transfer_indicesand none overrideget_transfer_metric. Notably the backends' data paths already indexstate_item_lensper component when computing offsets — only the accounting flattened it, so the transfers themselves were always correct.Tests
test/registered/unit/disaggregation/test_kv_transfer_replica_metric.py:test_each_state_component_is_charged_its_own_item_length— two components with deliberately lopsided item lengths; asserts the exact byte count and, separately, that it is not the flattened value.test_a_component_with_no_indices_costs_nothing— aNoneslot must not inflate its neighbours' per-index price.The existing replication cases are updated to the new field names and still pass unchanged in meaning.
Mutation-checked: restoring the flattened form fails exactly those two new cases and nothing else. Full
test/registered/unit/disaggregation/suite: 130 passed.Interaction with #31993
#31993 (open) reworks this same function to fix a different problem — bytes being paired with a latency window that covers only the last chunk. It preserves the byte formula as-is, carrying the cross product into its new
_transfer_bytes()helper:The two changes are complementary — that one fixes when bytes are measured, this one fixes how many — but they touch adjacent lines and will conflict textually. Happy to rebase onto whichever lands first.
CI States
Latest PR Test (Base): ❌ Run #31069926741
Latest PR Test (Extra): ❌ Run #31069926654