Skip to content

[PD] Validate decode state layout once at registration - #42051

Merged
ShangmingCai merged 8 commits into
sgl-project:mainfrom
ShangmingCai:pd-state-stride-registration
Oct 4, 2026
Merged

ShangmingCai merged 8 commits into
sgl-project:mainfrom
ShangmingCai:pd-state-stride-registration

Conversation

@ShangmingCai

@ShangmingCai ShangmingCai commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Why is this change needed?

Follow-up to #41607, which validates state entry strides inside
_send_kvcache_generic on every request that carries state.

Both inputs to that comparison are fixed when the peers register — the local
kv_args.state_item_lens and the peer's dst_state_item_lens off
KVArgsRegisterInfo — and neither side changes them for the life of the
session. The only per-request values in that path are the page indices, which
the check never touches, so the verdict is identical for every request on a
session. Two consequences:

  • A misconfigured decode is only noticed on the first request that happens to
    carry state indices (maybe_send_extra runs under
    if kv_chunk.state_indices and not skip_state), rather than when it registers.
  • Every later request re-derives the same verdict. On the positional path under
    PP with compressed MLA that re-runs _mla_slice_ptrs_for_pp, four
    O(num_layers) scans plus list builds, once per state component.

Modifications

  • Factor the comparison into _state_stride_mismatch, which returns the first
    offending entry or None. _send_kvcache_generic now calls it once instead
    of carrying two inline copies; the transfer-time behaviour is unchanged (log
    the cause, conclude_failure the affected room, return -1).
  • _validate_peer_state_layout walks every generic state component when a
    decode peer registers and returns (fully_checked, mismatch_reason).
  • A mismatch records the reason in state_layout_rejections, and the worker's
    early exit fails each request to that peer with that exact reason instead of
    the generic "Decode instance could be dead" text.
  • A clean verdict records the session in state_strides_validated, and
    _send_kvcache_generic skips the comparison for it.
  • Both verdicts are re-derived on every re-registration.

A layout rejection deliberately does not reuse failed_sessions. That set
is probe-recoverable — _run_one_probe_pass un-blacklists any session whose
transport probe succeeds — and a layout-incompatible decode is perfectly
reachable, so it would be un-blacklisted straight back into wrong-slot writes.
A mismatch is permanent for that peer until it re-registers with a matching
layout, which is also why a session blacklist is the right granularity here
while a transport fault still fails only the affected request.

_publish_peer_registration records the verdict and publishes
decode_kv_args_table last, under one lock, so a transfer can never read a new
layout while the previous registration's verdict still stands.

The layout branch that decides between the flat (one region per entry) and
the plain-MHA K/V-split path lives in one module-level predicate,
_uses_flat_entry_layout, which both _send_kvcache_generic and
_state_stride_mismatch call, so the transfer and the registration-time check
cannot drift apart. It is a free function rather than a method because the
test doubles in test_pp_hybrid_kv_transfer.py /
test_mooncake_transfer_batching.py borrow _send_kvcache_generic without
inheriting the class.

A decode that publishes fewer item lengths than entries used to raise
IndexError out of the stride check and take the transfer thread down. Both
pairing paths now return that as a rejection reason instead.

Registration does not adjudicate what it cannot see: where the peer published
no sizes for a component, or where entry pairing itself raises, it leaves the
per-transfer check armed so the request still fails with its usual context.

The decode peer is not told at registration time (the register message has no
reply path), so it learns per request, as it does for a transport blacklist.

Test Plan

  • Red → green against the merged 1a0250c5b6: 9 of the 11 new cases/subtests fail
    there and all pass with the change. The skip test is driven with entry sizes that would be rejected, so
    the transfer going through is proof the comparison was skipped rather than
    merely passing.
  • Full CPU wire-test module plus the four related PP and Mooncake batching
    modules and the two mem_cache modules that also borrow
    _send_kvcache_generic: 122 tests and 93 subtests passed. The whole
    test/registered/unit/disaggregation/ tree shows the same pass/fail counts as
    the merged base, plus the new cases.
    PYTHONPATH=python python -m pytest -q \
      test/registered/unit/disaggregation/test_disaggregation_wire.py \
      test/registered/unit/disaggregation/test_pp_hybrid_kv_transfer.py \
      test/registered/unit/disaggregation/test_pp_mla_kv_transfer.py \
      test/registered/unit/disaggregation/test_mooncake_transfer_batching.py \
      test/registered/unit/disaggregation/test_mooncake_custom_mem_pool_batch.py \
      test/registered/unit/mem_cache/test_hisparse_allocator.py \
      test/registered/unit/mem_cache/test_pd_envelope_transfer_layout.py
  • New coverage: the registration verdict for positional and layer-ID entries in
    both directions; that re-registering re-derives it and publishes the layout
    last; that a component the peer never published keeps the per-transfer check
    armed; that a validated session skips it; that a rejected peer fails every
    request with the recorded cause, writes nothing, and never joins the transport
    blacklist; that a successful probe clears a transport blacklist but leaves
    a layout rejection standing; and that a decode publishing fewer item lengths
    than entries yields a rejection reason on both pairing paths (IndexError on
    the base).
  • Upstream ruff format, ruff check and isort clean on both changed files.
  • Transport is mocked. No GPU inference or benchmark was run; the change
    inspects CPU metadata at handshake and removes work from the transfer path.

🤖 Generated with Claude Code


CI States

Latest PR Test (Base): ⏳ Run #37176704621
Latest PR Test (Extra): 🚫 Run #37184247524
Latest PR Test (AMD ROCm 10): ❌ Run #37176704622

@ShangmingCai ShangmingCai added the run-ci CI: run the baseline test suite on this PR label Oct 1, 2026
@ShangmingCai
ShangmingCai force-pushed the pd-state-stride-registration branch 4 times, most recently from 11885b6 to f0d24d1 Compare October 1, 2026 12:09
@ShangmingCai

Copy link
Copy Markdown
Collaborator Author

/tag-and-rerun-ci extra

@github-actions github-actions Bot added the run-ci-extra CI: also run the extra suite (requires run-ci) label Oct 1, 2026
@ShangmingCai

Copy link
Copy Markdown
Collaborator Author

/rerun-failed-ci

@ShangmingCai

Copy link
Copy Markdown
Collaborator Author

/rerun-group disaggregation

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Results for /rerun-group disaggregation:

🚀 4-gpu-gb300 (1 test): ❌ View workflow run

cd test/ && python3 registered/disaggregation/test_disaggregation_aarch64.py

🚀 2-gpu-h100 (6 tests): ✅ View workflow run

cd test/ && python3 registered/disaggregation/test_disaggregation_basic.py
cd test/ && python3 registered/disaggregation/test_disaggregation_chunked_prefill_abort.py
cd test/ && python3 registered/disaggregation/test_disaggregation_decode_offload.py
cd test/ && python3 registered/disaggregation/test_disaggregation_optimistic_prefill.py
cd test/ && python3 registered/disaggregation/test_disaggregation_rust_server.py
cd test/ && python3 registered/disaggregation/test_disaggregation_unified_memory.py

🚀 8-gpu-h20 (5 tests): ✅ View workflow run

cd test/ && python3 registered/disaggregation/test_disaggregation_decode_radix_cache.py
cd test/ && python3 registered/disaggregation/test_disaggregation_different_tp.py
cd test/ && python3 registered/disaggregation/test_disaggregation_dp_attention.py
cd test/ && python3 registered/disaggregation/test_disaggregation_nixl.py
cd test/ && python3 registered/disaggregation/test_disaggregation_pp.py

🚀 8-gpu-h200 (4 tests): ✅ View workflow run

cd test/ && python3 registered/disaggregation/test_disaggregation_decode_radix_cache_swa.py
cd test/ && python3 registered/disaggregation/test_disaggregation_dsv4.py
cd test/ && python3 registered/disaggregation/test_disaggregation_hisparse.py
cd test/ && python3 registered/disaggregation/test_disaggregation_hybrid_attention.py

🚀 4-gpu-b200 (2 tests): ✅ View workflow run

cd test/ && python3 registered/disaggregation/test_disaggregation_dwdp_gpt_oss.py
cd test/ && python3 registered/disaggregation/test_disaggregation_inkling_mxfp8.py

🚀 4-gpu-h100 (2 tests): ✅ View workflow run

cd test/ && python3 registered/disaggregation/test_disaggregation_kimi_linear.py
cd test/ && python3 registered/disaggregation/test_epd_disaggregation.py

⛔ registered/disaggregation/test_disaggregation_xpu.py: test/registered/disaggregation/test_disaggregation_xpu.py is registered for XPU (suite stage-b-test-1-gpu-xpu), not for CUDA or CPU; rerun-test.yml has no XPU job. Rerun it with /rerun-failed-ci, or dispatch the XPU workflow manually.

Both inputs to the state stride check are fixed when the peers register, so
the verdict is identical for every request on a session. Reach it when the
decode peer registers instead of on the first request that happens to carry
state indices, and skip the per-transfer comparison for a session already
cleared there.

A layout rejection gets its own gate rather than joining failed_sessions:
that set is probe-recoverable, and an incompatible decode is perfectly
reachable, so it would be un-blacklisted straight back into wrong-slot
writes. Only a re-registration with a matching layout clears it.

_publish_peer_registration records the verdict and publishes
decode_kv_args_table last, under one lock, so a transfer can never read a new
layout while the previous registration's verdict still stands.

_state_stride_mismatch has to repeat _send_kvcache_generic's layout branch so
registration can reach it; a test compares the two ASTs so they cannot drift.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ShangmingCai
ShangmingCai force-pushed the pd-state-stride-registration branch from f0d24d1 to d7eb604 Compare October 2, 2026 06:00
@ShangmingCai

Copy link
Copy Markdown
Collaborator Author

/rerun-failed-ci

ShangmingCai and others added 2 commits October 2, 2026 18:02
Pull the layout branch that _send_kvcache_generic and
_state_stride_mismatch both evaluated into one module-level predicate, so
the transfer and the registration-time check cannot drift apart. The AST
comparison test that pinned the two copies together goes with it.

A decode that publishes fewer item lengths than entries used to raise
IndexError out of the stride check and take the transfer thread down.
Both pairing paths now return it as a rejection reason instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ShangmingCai

Copy link
Copy Markdown
Collaborator Author

/rerun-group disaggregation

@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Results for /rerun-group disaggregation:

🚀 4-gpu-gb300 (1 test): ✅ View workflow run

cd test/ && python3 registered/disaggregation/test_disaggregation_aarch64.py

🚀 2-gpu-h100 (6 tests): ✅ View workflow run

cd test/ && python3 registered/disaggregation/test_disaggregation_basic.py
cd test/ && python3 registered/disaggregation/test_disaggregation_chunked_prefill_abort.py
cd test/ && python3 registered/disaggregation/test_disaggregation_decode_offload.py
cd test/ && python3 registered/disaggregation/test_disaggregation_optimistic_prefill.py
cd test/ && python3 registered/disaggregation/test_disaggregation_rust_server.py
cd test/ && python3 registered/disaggregation/test_disaggregation_unified_memory.py

🚀 8-gpu-h20 (5 tests): ✅ View workflow run

cd test/ && python3 registered/disaggregation/test_disaggregation_decode_radix_cache.py
cd test/ && python3 registered/disaggregation/test_disaggregation_different_tp.py
cd test/ && python3 registered/disaggregation/test_disaggregation_dp_attention.py
cd test/ && python3 registered/disaggregation/test_disaggregation_nixl.py
cd test/ && python3 registered/disaggregation/test_disaggregation_pp.py

🚀 8-gpu-h200 (4 tests): ✅ View workflow run

cd test/ && python3 registered/disaggregation/test_disaggregation_decode_radix_cache_swa.py
cd test/ && python3 registered/disaggregation/test_disaggregation_dsv4.py
cd test/ && python3 registered/disaggregation/test_disaggregation_hisparse.py
cd test/ && python3 registered/disaggregation/test_disaggregation_hybrid_attention.py

🚀 4-gpu-b200 (2 tests): ✅ View workflow run

cd test/ && python3 registered/disaggregation/test_disaggregation_dwdp_gpt_oss.py
cd test/ && python3 registered/disaggregation/test_disaggregation_inkling_mxfp8.py

🚀 4-gpu-h100 (2 tests): ✅ View workflow run

cd test/ && python3 registered/disaggregation/test_disaggregation_kimi_linear.py
cd test/ && python3 registered/disaggregation/test_epd_disaggregation.py

⛔ registered/disaggregation/test_disaggregation_xpu.py: test/registered/disaggregation/test_disaggregation_xpu.py is registered for XPU (suite stage-b-test-1-gpu-xpu), not for CUDA or CPU; rerun-test.yml has no XPU job. Rerun it with /rerun-failed-ci, or dispatch the XPU workflow manually.

ShangmingCai and others added 5 commits October 3, 2026 17:24
The transfer worker now reads state_layout_rejections on every request, so
the MooncakeKVManager built via __new__ in
test_worker_cleanup_finishes_before_sender_can_report_success needs it
alongside failed_sessions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ender

The test still passed dest_tp_ranks/pp_rank, which CommonKVSender.__init__
no longer takes, and its MagicMock manager exposed a truthy
deferred_bootstrap, so __init__ took the duplicate-room early exit before
reaching the DP-rank branch. Drop the removed args and set
deferred_bootstrap=None. Test-only; this fails on main too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ShangmingCai ShangmingCai added bypass-maintenance bypass-fail-fast CI: a failing job no longer aborts its siblings (lint still gates) labels Oct 4, 2026
@ShangmingCai

Copy link
Copy Markdown
Collaborator Author

@aurickq Could you take a quick look of this PR?

@ShangmingCai
ShangmingCai merged commit 148da8e into sgl-project:main Oct 4, 2026
191 of 238 checks passed
@ShangmingCai
ShangmingCai deleted the pd-state-stride-registration branch October 4, 2026 10:32
ShangmingCai added a commit to salexspb/sglang that referenced this pull request Oct 5, 2026
TestMooncakeEarlySend drives the unbound MooncakeKVManager.transfer_worker
against a hand-rolled SimpleNamespace. That fake was complete when written,
but two changes have since landed on main that made transfer_worker read
attributes it does not carry:

  - sgl-project#42051 added self.state_layout_rejections, read under session_lock for
    every non-dummy req.
  - sgl-project#37077 added self.poison_deferred_ack_room(), called from the worker's
    except path.

Merging main into the branch picked up the new conn.py without touching the
fake, and the merge was clean because the two sides touch different files.
test_worker_waits_before_reading_kv and test_worker_preserves_no_event_path
then died with AttributeError on state_layout_rejections, which the except
handler turned into a second AttributeError on poison_deferred_ack_room.

Only the fixture was stale: the real manager initializes
state_layout_rejections on the prefill path in __init__ and inherits
poison_deferred_ack_room from CommonKVManager, so no shipping path was
affected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bypass-fail-fast CI: a failing job no longer aborts its siblings (lint still gates) bypass-maintenance run-ci CI: run the baseline test suite on this PR run-ci-extra CI: also run the extra suite (requires run-ci)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant