Skip to content

[Perf] disaggregation: Cache host-staged decode admission accounting across a transfer pass - #48037

Merged
cctry merged 1 commit into
mainfrom
oss-sync-621ba2d1a4
Nov 5, 2026
Merged

cctry merged 1 commit into
mainfrom
oss-sync-621ba2d1a4

Conversation

@metamergebot

@metamergebot metamergebot commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Motivation

DecodeTransferQueue.pop_transferred admits completed host-staged requests to device memory through DecodePreallocQueue.allocate_host_staged. #46898 made a pass stop device admission at the first refusal, because trying every later request recomputed the request-independent accounting (running-batch token sums, maximum per-request growth, transfer-queue device-destination count, retracted-token requirements) on every attempt: O(W × (R + Q + T)) per pass for W attempts, R running requests, Q transfer entries and T retracted requests.

Stopping at the first refusal trades utilization for that bound. A later, smaller request that would fit waits for the next pass even when device capacity is free, and near a full pool that is exactly when the capacity matters. This change lets every completed request in the pass try, and keeps the pass linear by computing the shared accounting once.

Modifications

  • _HostStagedAdmissionStats holds the pass-invariant accounting: retractable full/SWA token sums over the running batch, the maximum per-request growth (full and SWA), the number of transfer entries holding a device destination, and the retracted queue's full/SWA token requirements. Each field is filled lazily on first use and reused by later admission calls in the same pass.
  • DecodePreallocQueue._use_host_staged_admission_stats scopes the cache with a context manager around each allocate_host_staged call in pop_transferred. Outside that scope (every other caller, the next pass, and after an exception) _host_staged_admission_stats is None and all helpers read the live queues as before.
  • A successful admission increments the cached device-destination count so later checks in the same pass see it; the pass-local "first refusal" flag from [Perf] disaggregation: Stop device admission in a decode transfer pass at the first refusal #46898 is removed, so a refused request is skipped with continue and later ones still try, in queue order.
  • _need_space_for_single_req returns early for an empty running batch or a None retractable count, then computes the maximum per-request requirement once and subtracts the retractable tokens; the result is unchanged.
  • New _retracted_token_counts returns the retracted queue's (full, swa) requirements in one walk and replaces the two separate loops in the full and SWA allocatable-budget helpers.
  • New has_device_destinations property on DecodePreallocQueue; disagg_transfers_in_flight in disaggregation/utils.py reads it instead of scanning the transfer queue itself, so the unified-memory pin predicate also benefits from the cached count during admission.
  • test_unified_memory_move_gate.py: _FakePreallocQueue now subclasses DecodePreallocQueue and holds the fake transfer queue, so the move-gate test exercises the real has_device_destinations property.

Rebased onto main after #43908: the two new iterations over the decode transfer queue (has_device_destinations and _active_req_count) read self.transfer_queue.queue.items(), matching the request-list container that refactor introduced; the pop_transferred loop keeps its queue = list(self.queue.items()) form.

Allocator availability, eviction credit and SWA capacity are not cached; allocation, reclamation and failed-transfer cleanup inside a pass affect the next decision. The cache holds Python integers only; the admission path adds no device readback or GPU-to-CPU synchronization.

Testing

  • pre-commit run --files on the three changed files passes.
  • test/registered/unit/disaggregation/test_unified_memory_move_gate.py: 10 tests, 22 subtests pass on this branch.
  • test/registered/unit/mem_cache/test_decode_radix_lock_ref.py, test_decode_retraction_backup.py, test_hisparse_allocator.py and test_unified_dynamic_gate_capacity.py (the registered unit files that exercise the changed admission helpers): 35 tests, 23 subtests pass on this branch.

Co-authored-by: ZYHowell ZYHowell@users.noreply.github.com


CI States

Latest PR Test (Base): ✅ Run #41620158335
Latest PR Test (Extra): ❌ Run #41620158058
Latest PR Test (AMD ROCm 10): ❌ Run #41620158319

Tip

Base CI only covers the core cases, and its workload may not exercise your change. Use /rerun-test to run the tests your change touches, or copy this prompt for an agent:

Find the registered tests this PR affects and run them with /rerun-test, following https://github.com/sgl-project/sglang/blob/main/.agents/skills/rerun-test/SKILL.md

@claude claude Bot 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.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@metamergebot metamergebot added the run-ci CI: run the baseline test suite on this PR label Nov 3, 2026
…across a transfer pass

A decode transfer pass admits host-staged requests to device memory in
queue order. Since the pass stopped at the first refusal, a later, smaller
request that would fit had to wait for the next pass even when device
capacity was available. Letting every request in the pass try, with the
previous per-call accounting, would rescan the running batch, the transfer
queue and the retracted queue on each attempt: O(W * (R + Q + T)) per pass
for W attempts, R running requests, Q transfer entries and T retracted
requests.

Cache the parts of the admission accounting that do not change inside one
pass: the running-batch token sums, the maximum per-request growth, the
retracted-token requirements and the count of transfer entries holding a
device destination. The cache is scoped to the admission calls of a single
pass with a context manager, so every other caller, the next pass, and the
path after an exception read the live queues. A successful admission
increments the cached device-destination count; refusals reuse it, bringing
the pass to O(R + Q + T + W). Allocator availability, eviction credit and
SWA capacity stay live so allocation, reclamation and failed-transfer
cleanup affect the next decision. The unified-memory transfer pin predicate
reads the same device-destination count through a new
`has_device_destinations` property instead of scanning the transfer queue
on every call, and the move-gate test's fake prealloc queue now inherits
that property from `DecodePreallocQueue`.

The cache holds Python integers only; the admission path performs no
device readback or GPU-to-CPU synchronization.

Co-authored-by: ZYHowell <ZYHowell@users.noreply.github.com>
@metamergebot

Copy link
Copy Markdown
Collaborator Author

This port clears our bar on dcf1c92b3737541f1c16096a4d1d0fee67ba7f5e: every base-a and base-b job is green.
base-c and the accelerator suites are reported but not gated, because they run
on scarce hardware and upstream merges through them routinely.

gate✓ lint✓ build 3✓✗2✂1 A✓ B✓ C✓ other✓ · accel ✗amd

@charlotte12l @merrymercy @cctry @ZYHowell — could one of you take a look and merge it if you are happy? Nothing else is waiting on our side.

@cctry
cctry merged commit 3f1fcf9 into main Nov 5, 2026
155 of 165 checks passed
@cctry
cctry deleted the oss-sync-621ba2d1a4 branch November 5, 2026 23:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-ci CI: run the baseline test suite on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants