Skip to content

[GG] fix/perf: isolate B12X graph channels and capture DSpark context KV - #251

Merged
lukealonso merged 7 commits into
local-inference-lab:dev/gilded-gnosisfrom
voipmonitor:perf/gg-dspark-context-cudagraph-20260807
Aug 11, 2026
Merged

lukealonso merged 7 commits into
local-inference-lab:dev/gilded-gnosisfrom
voipmonitor:perf/gg-dspark-context-cudagraph-20260807

Conversation

@voipmonitor

@voipmonitor voipmonitor commented Aug 7, 2026

Copy link
Copy Markdown

Purpose

Make B12X CUDA-graph transport ownership deterministic and move the remaining DSpark context-KV projection/write stage into a dedicated FULL CUDA graph family.

These changes are intentionally reviewed together. The context-KV graph creates an additional distributed graph owner, so it requires the semantic channel lifecycle in the same source state. Keeping the two changes in independent PRs against dev/gilded-gnosis produced an order-dependent conflict and allowed the context graph to compile only through hidden stacked ancestry.

This PR supersedes #247 and the previous stacked form of #251. Its history now starts at the current dev/gilded-gnosis head; there is no private integration commit.

Distributed graph lifecycle

  • Assigns stable, rank-identical channel IDs to target, draft, encoder, eager all-reduce, and DCP owners.
  • Separates profile and production captures so disposable profiling channels cannot retain production buffers or streams.
  • Rolls back channels created during memory profiling and preserves pre-existing owners.
  • Makes finalizers non-collective and performs coordinated B12X/custom-all-reduce teardown while process groups are still valid.
  • Fails closed when a distributed capture has no semantic channel identity or an eager owner is rebound to another stream.

DSpark context FULL graphs

  • Adds a DSpark-only context-KV graph manager.
  • Maps each verifier-row count to the smallest configured capture bucket.
  • Makes padding inert with position 0 and PAD_SLOT_ID.
  • Keeps eager fallback for profiling, dummy execution, unsupported shapes, and non-DSpark DFlash models.
  • Gives profile and production context graphs distinct B12X channel identities.
  • Does not alter logits, verification, sampling, or model arithmetic.

Measured result

deepseek-ai/DeepSeek-V4-Flash-0731, TP2/DCP1, B12X W4A8, FP8 DS-MLA KV, InstantTensor, fixed probabilistic K5, same physical GPUs 4-5:

60 s steady-state test Baseline Context FULL graph Delta
CC1 server decode 182.82 tok/s 190.68 tok/s +4.3%
CC32 server throughput 1,256.93 tok/s 1,250.39 tok/s -0.5%
CC32 aggregate throughput 1,257.73 tok/s 1,253.69 tok/s -0.3%

The rank-0 four-step trace reduced DSpark propose mean from 2.408 ms to 1.510 ms, eager CUDA launches from 296 to 188, and traced GPU span from 68.44 ms to 63.74 ms. CC32 remained within run variance.

At MNS64 / graph-row envelope 384, the graph pool grew by about 0.01 GiB, estimated graph reservation by about 0.03 GiB, and engine KV capacity decreased by 74 tokens.

Validation

  • Rebased cleanly on current GG (e2666d9a65).
  • Focused lifecycle/context/spec tests: 19 passed, 1 CUDA-only skipped in the local CPU gate.
  • Previous CUDA qualification: fixed K5 and K7 captured all FULL graph families; C1/CC32 output remained coherent.
  • Ruff, Python byte-compilation, git diff --check, and clean release composition: pass.

Prefill remains PIECEWISE. Host metadata preparation, rejection sampling, and output bookkeeping remain outside CUDA graphs; this PR removes the remaining eager DSpark model-compute stage rather than claiming a literally zero-eager API step.

Summary by CodeRabbit

  • New Features

    • Added distinct CUDA graph capture channels for target, draft, encoder, profiling, and production workflows.
    • Added graph-backed DFlash context processing with bucket selection, replay, and eager fallback.
    • Added context input padding for reliable CUDA graph execution.
  • Bug Fixes

    • Improved distributed communication cleanup and failure handling.
    • Added validation for missing capture channel identifiers and conflicting stream bindings.
    • Improved graph state restoration when capture or cleanup fails.

@coderabbitai

ghost commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9dbed713-040f-4914-b06f-c0bbecb8e6ff

📥 Commits

Reviewing files that changed from the base of the PR and between 45849684bea6266acec672a968db7e33b79daf0c and c66ce73.

📒 Files selected for processing (9)
  • tests/distributed/test_b12x_fused_all_reduce.py
  • tests/distributed/test_dcp_a2a.py
  • tests/v1/spec_decode/test_dflash_cudagraph_lifetime.py
  • tests/v1/worker/test_gpu_autoregressive_speculator.py
  • vllm/distributed/device_communicators/cuda_communicator.py
  • vllm/distributed/device_communicators/custom_all_reduce.py
  • vllm/distributed/parallel_state.py
  • vllm/v1/attention/ops/dcp_alltoall.py
  • vllm/v1/worker/gpu/model_runner.py
🚧 Files skipped from review as they are similar to previous changes (6)
  • vllm/distributed/device_communicators/cuda_communicator.py
  • vllm/v1/worker/gpu/model_runner.py
  • vllm/v1/attention/ops/dcp_alltoall.py
  • tests/distributed/test_dcp_a2a.py
  • vllm/distributed/device_communicators/custom_all_reduce.py
  • vllm/distributed/parallel_state.py

📝 Walkthrough

Walkthrough

The PR propagates stable semantic channel IDs through B12X communication, distributed graph capture, DCP all-to-all, DFlash context graphs, and model-runner CUDA graph capture. It also changes communicator cleanup ordering and adds lifecycle, validation, rollback, and padding tests.

Changes

B12X communication and lifecycle

Layer / File(s) Summary
B12X runtime channels and cleanup
vllm/distributed/device_communicators/custom_all_reduce.py, vllm/distributed/device_communicators/cuda_communicator.py, tests/distributed/test_b12x_fused_all_reduce.py, tests/distributed/test_custom_allreduce_lifecycle.py
B12X PCIe operations use stable eager channels and explicit capture IDs. Cleanup closes runtimes in order and avoids implicit finalization while resources remain active. Tests cover forwarding, stream ownership, initialization, and failure paths.
Distributed capture and DCP channels
vllm/distributed/parallel_state.py, vllm/v1/attention/ops/dcp_alltoall.py, tests/distributed/test_dcp_a2a.py
Graph capture contexts carry channel IDs through group coordination, DCP capture, B12X pool capture, reductions, and gathers. Missing IDs are rejected when registered pools require them.

CUDA graph capture

Layer / File(s) Summary
Speculator graph identity
vllm/v1/worker/gpu/cudagraph_utils.py, vllm/v1/worker/gpu/model_runner.py, vllm/v1/worker/gpu/spec_decode/*, tests/v1/cudagraph/*, tests/v1/worker/test_gpu_autoregressive_speculator.py
Graph managers require channel IDs. Speculator capture requires a profile or production phase and generates phase-specific prefill and decode channels.
DFlash context graph dispatch
vllm/v1/worker/gpu/spec_decode/dflash/*, tests/v1/spec_decode/*
DFlash adds context graph managers, capture-bucket selection, graph replay, eager fallback, and padded context input preparation using inert positions and PAD_SLOT_ID values.
Model-runner capture orchestration
vllm/v1/worker/gpu_model_runner.py, tests/v1/worker/test_gpu_model_runner.py
Target, draft, and encoder graph captures use separate channels and pools. Profiling and cleanup restore graph state and B12X channel state after failures.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

  • local-inference-lab/rtx6kpro#34 — Shares PCIe DMA ownership and CUDA graph-pool lifecycle changes.
  • local-inference-lab/sparkinfer#95 — Directly relates to semantic channel propagation and stale-stream handling in DCP A2A capture paths.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the two main changes: isolating B12X graph channels and capturing DSpark context KV.
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

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.

ghost 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.

🧹 Nitpick comments (6)
tests/distributed/test_dcp_a2a.py (1)

749-814: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the new channel_id validation branches.

This test passes a context that already carries a channel ID, so it exercises only the pass-through branch of graph_capture. Two new branches in vllm/distributed/parallel_state.py stay untested: the ValueError raised when channel_id conflicts with context.channel_id, and the clone performed when the context has no ID. Add two short cases for them.

🤖 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/distributed/test_dcp_a2a.py` around lines 749 - 814, Extend
test_global_graph_capture_enters_b12x_dcp_pool with two focused cases for
graph_capture: assert it raises ValueError when an explicit channel_id conflicts
with context.channel_id, and verify it clones a context lacking an ID with the
requested channel_id while preserving the original context unchanged. Reuse the
existing fake groups/context setup and validate the resulting context’s
channel_id.
vllm/distributed/parallel_state.py (1)

1682-1690: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Preserve the original GraphCaptureContext identity when injecting channel_id.

context = GraphCaptureContext(context.stream, channel_id=channel_id) at line 1690 returns a different object for the caller if context.channel_id is None. A caller that reads channel_id from its own reference after the block will still see None, even though the yielded context carries the ID. If GraphCaptureContext is mutable, update the existing instance instead of cloning it. If it is frozen, document the object substitution in the docstring.

🤖 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 `@vllm/distributed/parallel_state.py` around lines 1682 - 1690, The channel_id
injection in the graph capture context path must preserve the caller’s original
GraphCaptureContext identity. Update the existing context’s channel_id when it
is None, while retaining the conflict validation for mismatched IDs; only use
object replacement if GraphCaptureContext is immutable, and document that
substitution in the surrounding API docstring.
vllm/v1/worker/gpu_model_runner.py (2)

6835-6854: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Cleanup after a clear_all_graphs failure is skipped.

The inner try guarantees pool restore, and the outer try guarantees channel rollback. It does not guarantee the remaining cleanup. If CUDAGraphWrapper.clear_all_graphs() or encoder_cudagraph_manager.clear() raises, the code skips dispatcher key reset, LoRA removal, _cleanup_profiling_kv_cache(), and the capture-counter restore. The profiling KV cache then stays allocated. The parameterized test at tests/v1/worker/test_gpu_model_runner.py lines 2143-2146 encodes this behavior, so the gap is intentional today. Consider moving _cleanup_profiling_kv_cache() into its own finally so GPU memory is released even when graph teardown fails.

🤖 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 `@vllm/v1/worker/gpu_model_runner.py` around lines 6835 - 6854, Ensure
_cleanup_profiling_kv_cache() runs even when
CUDAGraphWrapper.clear_all_graphs(),
BreakableCUDAGraphWrapper.clear_all_graphs(), or
encoder_cudagraph_manager.clear() raises. Move the profiling KV-cache cleanup
into an appropriate finally block while preserving graph-pool restoration,
dispatcher reset, LoRA removal, counter restoration, and channel rollback
behavior.

6725-6739: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated component filter.

profile_cudagraph_memory and capture_model build component_descs with identical logic. The two copies must stay in sync, because a change to the drafter-separation rule in one place silently diverges from the other.

♻️ Proposed helper
def _component_capture_descs(
    self,
    component: str,
    capture_descs: list[tuple[CUDAGraphMode, list[BatchDescriptor]]],
) -> list[tuple[CUDAGraphMode, list[BatchDescriptor]]]:
    """Select the capture descriptors owned by one graph component.

    Args:
        component: Either ``"target"`` or ``"draft"``.
        capture_descs: All capture descriptors grouped by CUDA graph mode.

    Returns:
        The descriptor groups this component must capture.
    """
    return [
        (mode, descs)
        for mode, descs in capture_descs
        if descs
        and (
            component == "target"
            or self._captures_independent_drafter_graphs(mode)
        )
    ]

Also applies to: 6899-6913

🤖 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 `@vllm/v1/worker/gpu_model_runner.py` around lines 6725 - 6739, Extract the
duplicated component-desc filtering into a shared helper, such as
_component_capture_descs, near the existing capture logic. Update both
profile_cudagraph_memory and capture_model to call it for their target and draft
components, preserving the current drafter-separation condition and empty-result
handling.
tests/v1/worker/test_gpu_model_runner.py (1)

1786-1863: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for capture-mode reset when capture_model fails.

capture_model now disables capture mode in a finally block (vllm/v1/worker/gpu_model_runner.py lines 6940-6942). No test in this file exercises that path. set_cudagraph_capturing_enabled is global state, so a regression leaves capture mode enabled for the rest of the process. The existing fixture already patches set_cudagraph_capturing_enabled; recording its argument and raising from _capture_cudagraphs covers the case.

Line 1801 also assigns runner.encoder_cudagraph_manager = None and line 1825 overwrites it. Remove the first assignment.

🤖 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/v1/worker/test_gpu_model_runner.py` around lines 1786 - 1863, The test
test_v1_capture_separates_target_and_draft_semantic_channels should also cover
capture_model failure and verify set_cudagraph_capturing_enabled is called with
False in the finally path; record the patched setter’s arguments and make
_capture_cudagraphs raise, while preserving the existing setup as appropriate.
Remove the redundant initial runner.encoder_cudagraph_manager = None assignment
since it is overwritten later.
vllm/v1/worker/gpu/spec_decode/dflash/speculator.py (1)

197-237: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reset context_cudagraph_manager on the ineligible branch.

init_cudagraph_manager assigns query_cudagraph_manager unconditionally, but it only assigns context_cudagraph_manager inside the wants_full and supports_full and self._speculator_name == "DSpark" branch. If init_cudagraph_manager runs a second time with a mode that no longer qualifies, the stale context manager survives and propose keeps dispatching context graphs that were captured for the previous configuration.

♻️ Proposed change
         if wants_full and supports_full and self._speculator_name == "DSpark":
             self.context_cudagraph_manager = DFlashContextCudaGraphManager(
                 self.vllm_config,
                 self.device,
                 max_num_context_tokens=self.max_num_tokens,
             )
+        else:
+            self.context_cudagraph_manager = None
🤖 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 `@vllm/v1/worker/gpu/spec_decode/dflash/speculator.py` around lines 197 - 237,
Update init_cudagraph_manager to explicitly set context_cudagraph_manager to
None before the wants_full/supports_full/DSpark eligibility branch, so repeated
initialization cannot retain a manager from a previous configuration; leave
creation through DFlashContextCudaGraphManager unchanged for eligible DSpark
modes.
🤖 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 `@tests/distributed/test_dcp_a2a.py`:
- Around line 749-814: Extend test_global_graph_capture_enters_b12x_dcp_pool
with two focused cases for graph_capture: assert it raises ValueError when an
explicit channel_id conflicts with context.channel_id, and verify it clones a
context lacking an ID with the requested channel_id while preserving the
original context unchanged. Reuse the existing fake groups/context setup and
validate the resulting context’s channel_id.

In `@tests/v1/worker/test_gpu_model_runner.py`:
- Around line 1786-1863: The test
test_v1_capture_separates_target_and_draft_semantic_channels should also cover
capture_model failure and verify set_cudagraph_capturing_enabled is called with
False in the finally path; record the patched setter’s arguments and make
_capture_cudagraphs raise, while preserving the existing setup as appropriate.
Remove the redundant initial runner.encoder_cudagraph_manager = None assignment
since it is overwritten later.

In `@vllm/distributed/parallel_state.py`:
- Around line 1682-1690: The channel_id injection in the graph capture context
path must preserve the caller’s original GraphCaptureContext identity. Update
the existing context’s channel_id when it is None, while retaining the conflict
validation for mismatched IDs; only use object replacement if
GraphCaptureContext is immutable, and document that substitution in the
surrounding API docstring.

In `@vllm/v1/worker/gpu_model_runner.py`:
- Around line 6835-6854: Ensure _cleanup_profiling_kv_cache() runs even when
CUDAGraphWrapper.clear_all_graphs(),
BreakableCUDAGraphWrapper.clear_all_graphs(), or
encoder_cudagraph_manager.clear() raises. Move the profiling KV-cache cleanup
into an appropriate finally block while preserving graph-pool restoration,
dispatcher reset, LoRA removal, counter restoration, and channel rollback
behavior.
- Around line 6725-6739: Extract the duplicated component-desc filtering into a
shared helper, such as _component_capture_descs, near the existing capture
logic. Update both profile_cudagraph_memory and capture_model to call it for
their target and draft components, preserving the current drafter-separation
condition and empty-result handling.

In `@vllm/v1/worker/gpu/spec_decode/dflash/speculator.py`:
- Around line 197-237: Update init_cudagraph_manager to explicitly set
context_cudagraph_manager to None before the wants_full/supports_full/DSpark
eligibility branch, so repeated initialization cannot retain a manager from a
previous configuration; leave creation through DFlashContextCudaGraphManager
unchanged for eligible DSpark modes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8ae7897d-143d-4646-85fc-5d922ecc1977

📥 Commits

Reviewing files that changed from the base of the PR and between 3003860 and 45849684bea6266acec672a968db7e33b79daf0c.

📒 Files selected for processing (20)
  • tests/distributed/test_b12x_fused_all_reduce.py
  • tests/distributed/test_custom_allreduce_lifecycle.py
  • tests/distributed/test_dcp_a2a.py
  • tests/v1/cudagraph/test_breakable_cudagraph.py
  • tests/v1/spec_decode/test_dflash_context_cudagraph.py
  • tests/v1/spec_decode/test_dflash_cudagraph_lifetime.py
  • tests/v1/worker/test_gpu_autoregressive_speculator.py
  • tests/v1/worker/test_gpu_model_runner.py
  • vllm/distributed/device_communicators/cuda_communicator.py
  • vllm/distributed/device_communicators/custom_all_reduce.py
  • vllm/distributed/parallel_state.py
  • vllm/v1/attention/ops/dcp_alltoall.py
  • vllm/v1/worker/gpu/cudagraph_utils.py
  • vllm/v1/worker/gpu/model_runner.py
  • vllm/v1/worker/gpu/spec_decode/autoregressive/cudagraph_utils.py
  • vllm/v1/worker/gpu/spec_decode/autoregressive/speculator.py
  • vllm/v1/worker/gpu/spec_decode/dflash/cudagraph.py
  • vllm/v1/worker/gpu/spec_decode/dflash/speculator.py
  • vllm/v1/worker/gpu/spec_decode/speculator.py
  • vllm/v1/worker/gpu_model_runner.py

Michel Belleau and others added 6 commits August 7, 2026 16:07
Assisted-by: OpenAI Codex

Signed-off-by: Michel Belleau <michel.belleau@malaiwah.com>
Signed-off-by: Michel Belleau <michel.belleau@malaiwah.com>
Signed-off-by: Michel Belleau <michel.belleau@malaiwah.com>
Signed-off-by: Michel Belleau <michel.belleau@malaiwah.com>
Address semantic-channel review feedback by closing custom all-reduce before its process groups, keeping finalizers non-collective, and hardening CUDA graph profiling cleanup.

Assisted-by: OpenAI Codex

Signed-off-by: Michel Belleau <michel.belleau@malaiwah.com>
@voipmonitor
voipmonitor force-pushed the perf/gg-dspark-context-cudagraph-20260807 branch from 4584968 to f8fb9ad Compare August 7, 2026 16:27
@voipmonitor voipmonitor changed the title [GG] perf(spec-decode): capture DSpark context KV in FULL graphs [GG] fix/perf: isolate B12X graph channels and capture DSpark context KV Aug 7, 2026
@voipmonitor

ghost commented Aug 7, 2026

Copy link
Copy Markdown
Author

Final r30 composition exposed and fixed one stale call-site after the current GG graph-manager API change: DFlash and autoregressive speculators still passed channel_id to CudaGraphManager.__init__(), while the lifecycle contract now supplies it only to capture().

Commit c66ce7327ee586e5b600b9d7e12db30e93e8ec96 removes those constructor arguments and adds exact-signature contract tests for both DSpark and autoregressive/MTP managers. The focused Docker gate passed 12/12 tests; Ruff and git diff --check also passed.

The clean r30 image then completed live TP2 qualification on physical GPUs 4-5:

  • target FULL graph captured;
  • DSpark proposal FULL graph captured;
  • DFlash context-KV FULL graph captured (19 buckets);
  • 1,792 concurrent stress requests completed without an HTTP or EngineCore error;
  • post-stress output remained correct.

This confirms the semantic capture-channel integration against the current GG API rather than only against the earlier stacked source state.

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