Skip to content

[#16767][fix] Fix DSpark rolling-window slot collision in disaggregated serving#16772

Open
longlee0622 wants to merge 2 commits into
NVIDIA:mainfrom
longlee0622:dev/dspark-disagg-slot-fix
Open

[#16767][fix] Fix DSpark rolling-window slot collision in disaggregated serving#16772
longlee0622 wants to merge 2 commits into
NVIDIA:mainfrom
longlee0622:dev/dspark-disagg-slot-fix

Conversation

@longlee0622

@longlee0622 longlee0622 commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Dev Engineer Review

  • Updated DSpark rolling-window request→slot mapping to prevent speculative-decoding accept-length collapse under disaggregated serving by routing padded/unknown/idle request IDs to a dedicated scratch row instead of slot 0.
  • Added DSparkWorker._scratch_slot and changed lazy allocation of rolling-window KV/context tensors to include one extra scratch row (max_batch + 1), with CUDA-graph dummy/padding IDs routed below a computed dummy-ID floor to the scratch row.
  • Updated DSparkSpecMetadata.prepare() host-side slot assignment:
    • _batch_to_slot no longer defaults missing/unknown request IDs to slot 0; it maps them to the scratch slot.
    • For real unseeded disaggregated generation request IDs, assigns persistent distinct rolling-window slots via worker._assign_slot(..., reset=False).
    • Excludes dummy/ADP-idle and CUDA-graph padding requests from consuming real slots (they stay on the scratch row).
  • Preserved aggregated-mode behavior.
  • End-to-end DEP8 validation remains pending.

QA Engineer Review

Unit test updates in tests/unittest/_torch/speculative/hw_agnostic/test_dspark_worker.py:

  • Added test_prepare_maps_unknown_request_to_scratch_row_not_slot_zero
  • Added test_prepare_assigns_slots_to_disagg_generation_requests
  • Added test_prepare_keeps_dummy_generation_requests_on_scratch_row

These validate scratch-row routing, distinct persistent slot assignment for unseeded disaggregated generation requests, dummy-request exclusion from real slot allocation, and updated buffer initialization expectations.

Verdict: sufficient for the DSpark slot-routing logic; needs follow-up for DEP8 end-to-end validation and any required CI/manual-QA test-list registration (not indicated).

Description

Fixes the DSpark speculative-decoding accept-length (AL) collapse under disaggregated serving reported in #16767.

Root cause. DSpark assigns each request a rolling-KV-window slot only inside _seed_context_windows, which runs only when num_contexts > 0. On the disaggregated generation server, transferred requests arrive as generation requests (prefill runs on the context server), so _seed_context_windows never runs there, DSparkWorker._req_to_slot stays empty, and every real generation request falls through to a single shared fallback rolling-window row. At generation batch size 1 that row is effectively private (AL correct), but at batch size > 1 all concurrent requests collide on it and corrupt each other's draft context, collapsing AL (e.g. ~1.4 vs ~4.5 at draft length 7 in a DEP8 repro). The request_id=0 noted in the issue is the incidental attention-DP idle-dummy id (ATTENTION_DP_DUMMY_REQUEST_ID), not the mechanism.

This PR has two commits:

  1. Scratch-row hardeningDSparkWorker allocates one extra scratch window row (index max_batch, never handed out through _free_slots); DSparkSpecMetadata.prepare() routes padded/unknown request IDs (CUDA-graph padding, ADP idle) to it instead of aliasing slot 0.
  2. Disagg slot assignmentprepare() now assigns each real generation-tail request its own persistent rolling-window slot when seeding never mapped it, skipping the ADP-idle (id 0) and CUDA-graph padding dummies (which stay on the scratch row). Context-prefix entries are still left to _seed_context_windows, so aggregated mode is unchanged.

The generation server never prompt-seeds the window in either mode (batch-1 disagg also runs unseeded and reaches AL ~4.5, building the window from generation back-fill), so unique per-request slots fully address the collapse — no KV/window transfer from the context server is required.

Test Coverage

Hardware-agnostic unit tests in tests/unittest/_torch/speculative/hw_agnostic/test_dspark_worker.py (all pass on B200):

  • test_prepare_maps_unknown_request_to_scratch_row_not_slot_zero — unknown/padding IDs route to the scratch row, live slot untouched.
  • test_prepare_assigns_slots_to_disagg_generation_requests — disagg gen requests (never seeded here) get distinct, stable, non-scratch slots.
  • test_prepare_keeps_dummy_generation_requests_on_scratch_row — ADP-idle (id 0) and CUDA-graph padding dummies never consume a real slot.
  • updated test_worker_lazy_init_window_buffers — scratch row + dummy-id floor.

Validation status

  • ✅ DSpark hardware-agnostic worker unit tests (B200).
  • ⏳ End-to-end DEP8 disagg re-run to confirm LBS16 accept length recovers toward the LBS1 curve — pending (reason this PR is opened as a draft).

PR Checklist

  • Please check this after reviewing the above items as appropriate for this PR.

GitHub Bot Help

To see a list of available CI bot commands, please comment /bot help.

@longlee0622
longlee0622 marked this pull request as ready for review July 23, 2026 05:46
@longlee0622
longlee0622 requested a review from a team as a code owner July 23, 2026 05:47
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: af48070f-316b-42b0-b846-f4e6e3193061

📥 Commits

Reviewing files that changed from the base of the PR and between d7c8c7f and ee5f4b5.

📒 Files selected for processing (2)
  • tensorrt_llm/_torch/speculative/dspark.py
  • tests/unittest/_torch/speculative/hw_agnostic/test_dspark_worker.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/unittest/_torch/speculative/hw_agnostic/test_dspark_worker.py
  • tensorrt_llm/_torch/speculative/dspark.py

Walkthrough

DSpark adds a dedicated scratch rolling-window row, expands lazy-init buffers, classifies dummy and unknown request IDs, and assigns persistent slots to disaggregated generation requests. Tests cover scratch routing, slot persistence, and dummy handling.

Changes

DSpark slot mapping

Layer / File(s) Summary
Scratch row initialization
tensorrt_llm/_torch/speculative/dspark.py, tests/unittest/_torch/speculative/hw_agnostic/test_dspark_worker.py
DSpark allocates an additional rolling-window row, records it as _scratch_slot, computes the CUDA-graph dummy threshold, and updates initialization expectations.
Request slot assignment
tensorrt_llm/_torch/speculative/dspark.py, tests/unittest/_torch/speculative/hw_agnostic/test_dspark_worker.py
prepare() routes unknown and dummy IDs to the scratch row, assigns persistent real slots to generation requests, and validates these mappings.

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

Possibly related issues

Suggested reviewers: cascade812

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main fix for DSpark rolling-window slot collisions in disaggregated serving.
Description check ✅ Passed The description includes the issue, root cause, solution, test coverage, validation status, and checklist items.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

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

🧹 Nitpick comments (2)
tensorrt_llm/_torch/speculative/dspark.py (1)

284-293: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Nit: comment says "runtime_draft_len" but code uses self.max_draft_len.

Harmless here since block_size == max_draft_len is enforced for DSpark, but worth aligning the wording to avoid confusion for future readers.

🤖 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 `@tensorrt_llm/_torch/speculative/dspark.py` around lines 284 - 293, Update the
comment above `_graph_dummy_id_floor` to describe the padding range using
`self.max_draft_len`/the configured maximum draft length instead of
“runtime_draft_len,” matching the calculation and preserving the existing
routing explanation.
tests/unittest/_torch/speculative/hw_agnostic/test_dspark_worker.py (1)

115-357: 🎯 Functional Correctness | 🔵 Trivial

QA test coverage summary.

Changed/added test functions (all in tests/unittest/_torch/speculative/hw_agnostic/test_dspark_worker.py, outside tests/integration/test_lists/):

  1. test_worker_lazy_init_window_buffers (modified) — asserts the new scratch-row buffer shapes, _scratch_slot, and _graph_dummy_id_floor.
  2. test_prepare_maps_unknown_request_to_scratch_row_not_slot_zero (new) — unknown/padded ids route to the scratch row, not slot 0.
  3. test_prepare_assigns_slots_to_disagg_generation_requests (new) — disagg gen requests get distinct, stable persistent slots.
  4. test_prepare_keeps_dummy_generation_requests_on_scratch_row (new) — ADP-idle and CUDA-graph-padding dummy ids never consume a real slot (boundary-tests _graph_dummy_id_floor exactly).

These are hardware-agnostic unit tests under tests/unittest/, not under tests/integration/test_lists/, so no test-db//qa/ list entries apply; they run via standard pytest CI collection.

Coverage verdict: sufficient for the four areas the PR objectives explicitly claim (scratch-row routing, disaggregated slot assignment, dummy-request handling, buffer initialization). Gaps not covered here (acceptable given stated scope): a mixed batch with simultaneous context and disagg-generation requests in one prepare() call, and the _assign_slot slot-exhaustion (RuntimeError) path. Per PR objectives, end-to-end DEP8 validation is explicitly still pending and should be tracked separately.

🤖 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 `@tests/unittest/_torch/speculative/hw_agnostic/test_dspark_worker.py` around
lines 115 - 357, The review identifies the added DSpark worker tests as
sufficient for scratch-row routing, disaggregated slot assignment, dummy
handling, and buffer initialization. Preserve these tests and their current
scope; no test-list entries are needed, and do not add coverage for the
explicitly accepted mixed-batch or slot-exhaustion gaps.

Source: Path instructions

🤖 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 `@tensorrt_llm/_torch/speculative/dspark.py`:
- Around line 284-293: Update the comment above `_graph_dummy_id_floor` to
describe the padding range using `self.max_draft_len`/the configured maximum
draft length instead of “runtime_draft_len,” matching the calculation and
preserving the existing routing explanation.

In `@tests/unittest/_torch/speculative/hw_agnostic/test_dspark_worker.py`:
- Around line 115-357: The review identifies the added DSpark worker tests as
sufficient for scratch-row routing, disaggregated slot assignment, dummy
handling, and buffer initialization. Preserve these tests and their current
scope; no test-list entries are needed, and do not add coverage for the
explicitly accepted mixed-batch or slot-exhaustion gaps.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 0b2c5d73-ec05-493f-8991-5936ef074520

📥 Commits

Reviewing files that changed from the base of the PR and between a19410e and d7c8c7f.

📒 Files selected for processing (2)
  • tensorrt_llm/_torch/speculative/dspark.py
  • tests/unittest/_torch/speculative/hw_agnostic/test_dspark_worker.py

…ow row

DSparkSpecMetadata.prepare() mapped unknown request IDs (CUDA-graph padding, ADP idle requests, or disagg seed forwards without a real id) to slot 0, letting them overwrite the rolling KV window of whichever real request owned slot 0.

Allocate one extra scratch window row at index max_batch (never handed out via _free_slots) and map unknown IDs to it. Adds a regression test asserting the live slot's window and ctx_len stay untouched (validated on B200).

Related to GitHub NVIDIA#16767; disagg real-request-id propagation into the DSpark seed forward remains open.

Signed-off-by: Jonas Li <6110159+longlee0622@users.noreply.github.com>
…equests

DSpark assigns per-request rolling-window slots only in _seed_context_windows, which runs only when num_contexts>0. On the disaggregated generation server the prompt is prefilled (and the window seeded) on the context server, so _seed_context_windows never runs there, _req_to_slot stays empty, and every real generation request falls through to the single shared scratch/slot-0 row. At batch 1 that row is effectively private (accept length fine), but at batch >1 all concurrent requests collide on it and corrupt each other's draft window, collapsing accept length (~1.4 vs ~4.5 in a DEP8 repro). This is GitHub NVIDIA#16767; the reporter's "request_id=0 at seed" was the incidental ADP-idle dummy id, not the mechanism.

Fix: DSparkSpecMetadata.prepare() now assigns each real generation-tail request its own persistent slot when seeding never mapped it, skipping the ADP-idle (id 0) and CUDA-graph padding dummies (which stay on the scratch row). Context-prefix entries are still left to _seed_context_windows, so aggregated mode is unchanged. Adds hw-agnostic regression tests (validated on B200).
Signed-off-by: Jonas Li <6110159+longlee0622@users.noreply.github.com>
@longlee0622
longlee0622 force-pushed the dev/dspark-disagg-slot-fix branch from d7c8c7f to ee5f4b5 Compare July 23, 2026 06:08
@longlee0622

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #61209 [ run ] triggered by Bot. Commit: ee5f4b5 Link to invocation

@zhaoyangwang-nvidia zhaoyangwang-nvidia 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.

The slot-map maintenance in prepare() is a near line-by-line duplicate of dflash.py:86-113. Consider extracting this shared logic into SpecWorkerBase as a follow up work.

longlee0622 added a commit to longlee0622/TensorRT-LLM that referenced this pull request Jul 23, 2026
Consolidated dev-branch notes for the DSpark implementation: current status, algorithm/implementation, validated behavior, and the review backlog. Records the rolling-window slot fixes (scratch-row aliasing + disagg gen-request slot assignment, GitHub NVIDIA#16767 / PR NVIDIA#16772), confirmed end-to-end at draft length 7 on disaggregated serving. Dev-branch only; not part of any PR.

Signed-off-by: Jonas Li <6110159+longlee0622@users.noreply.github.com>
@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #61209 [ run ] completed with state FAILURE. Commit: ee5f4b5
/LLM/main/L0_MergeRequest_PR pipeline #49452 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

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.

3 participants