Skip to content

[Fix] scheduler: keep chunked prefill within allocatable KV under memory pressure - #46643

Open
aryatsc wants to merge 8 commits into
sgl-project:mainfrom
modal-projects:devin/pr-sched
Open

aryatsc wants to merge 8 commits into
sgl-project:mainfrom
modal-projects:devin/pr-sched

Conversation

@aryatsc

@aryatsc aryatsc commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Under overload on Kimi-K3 (MI355X, TP8, chunked prefill + hybrid KDA/MLA), the scheduler could stall or fail allocations:

  • A continuing chunk was sized only by the token budget, so it could ask alloc_extend for more KV than is free.
  • A second unfinished chunked request could be admitted under policies other than shortest-prefill-first, even though only one can be tracked (assert crash).
  • A latched batch_is_full was cleared only when the running batch shrank, so the waiting queue stalled.
  • On the aiter backend, an over-capacity extend inside graph context could leave the chunked-KV path.

Modifications

  • available_chunk_tokens caps a continuing chunk by free + evictable KV, less the page alloc_extend over-asks. When that leaves nothing, the chunk proceeds with the free KV or is deferred.
  • The one-chunked-request guard applies under every schedule policy.
  • batch_is_full is re-evaluated every scheduling step; PrefillAdder still enforces the real limits.
  • Over-capacity aiter extends stay on the chunked-KV path.

Accuracy Tests

New unit tests (test_prefill_memory_budget.py, test_prefill_adder.py, test_scheduler_batch_is_full_latch.py, test_aiter_attention_backend_handler.py): all 5 new cases fail on main and pass with this PR (76 passed).
End to end, the Kimi-K3 MI355X production build includes these fixes. On a 40-min overload soak (48 concurrent requests against 12 slots), 901/901 completed requests returned 200, the queue peaked at 37, and there were 0 OOM, HSA or assert errors.

Speed Tests and Profiling

Scheduling only; no kernel change.

Checklist


CI States

Latest PR Test (Base): ❌ Run #40956379040
Latest PR Test (Extra): ❌ Run #40956378917
Latest PR Test (AMD ROCm 10): ❌ Run #40956378897

aryatsc and others added 4 commits October 26, 2026 17:46
available_chunk_tokens only looked at the token budget, so under memory
pressure a continuing chunk could ask alloc_extend for more KV than is free
and fail. Cap it by the free + evictable KV (minus the page alloc_extend
over-asks); when the decode-headroom estimate leaves nothing, progress with
the free KV or defer the chunk.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…licy

Only one unfinished chunked request can be tracked, but the guard that
stops a second one was limited to shortest-prefill-first.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
batch_is_full was otherwise only cleared when the running batch shrank, so a
latched flag stalled the waiting queue until an unrelated request finished.
PrefillAdder already enforces the real limits.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

# Conflicts:
#	test/registered/unit/mem_cache/test_prefill_memory_budget.py
detain added a commit to detain/sglang that referenced this pull request Oct 29, 2026
[Fix] scheduler: keep chunked prefill within allocatable KV under memory
pressure. Three admission bugs: (1) a continuation chunk could ask
alloc_extend for more KV than is free -- PrefillBudget.
available_chunk_tokens now caps by real free KV minus the one-page
over-ask and defers (None) instead of forcing a full chunk; (2) the
single-unfinished-chunk guard now applies to ALL policies, not just
shortest-prefill-first (a second unfinished chunk was untrackable);
(3) batch_is_full is cleared unconditionally each round -- a latched
flag was otherwise only cleared when the running batch shrinks,
stalling the waiting queue until an unrelated finish (absorbs closed
sgl-project#35609).

UNION / DEVIATIONS:
- scheduler.py hunk hand-merged: our region carried the entry-198
  hybrid-SSM reset arm + is_empty arm; both are strict subsets of the
  unconditional clear and were removed. Entry-198's COW NO_TOKEN-retry
  semantics are unaffected (admission re-runs every round anyway).
- Consequently the two entry-198 gate tests were re-pinned to the new
  unconditional behavior (non-hybrid now retries every round too);
  their mamba-arm and COW-recovery cases pass unchanged.
- PR's new test_scheduler_batch_is_full_latch.py NOT imported: the
  file already exists in-tree (entry-222 wave) with fork stubs; kept
  ours, refreshed the stale sgl-project#37612-arm comment.
- AITER backend hunk + test_aiter_attention_backend_handler.py SKIPPED
  per instruction (ROCm aiter path irrelevant to this stack).
- test_prefill_memory_budget import hunk conflicted (our tree lacks the
  upstream ChainAllocator test rework): resolved as
  PrefillBudget + SWAPrefillBudget union; new TestContinuingChunkCap
  verbatim.
- Chunk-guard hunk applied verbatim: our _select_prefill_admission
  matched the PR base exactly; no mixed-chunk adaptation fights it
  (mixed-chunk tracks its rows via the scheduler, not new_chunked_req).

CPU: targeted (prefill_adder + memory_budget + 3 latch/retry +
held_row) 83P/67sub; full managers dir 993P/2F/3skip/904sub == the
2F are pre-existing order-dependent test_mm_process_config failures
(verified identical on the stashed baseline, 992P/2F).

(cherry picked from commit 0954308)
(cherry picked from commit d2bf2d29c4b5382291e672a3a1f0ac844cba196b)
detain added a commit to detain/sglang that referenced this pull request Oct 29, 2026
[Fix] scheduler: preserve Mamba fork+tail states across chunked
prefills via opt-in SGLANG_SPLIT_PREFILL_AT_MAMBA_FORK (EnvBool, default
False = byte-identical). One extend tracks one Mamba state, so a chunk
crossing the cached fork loses it; with the flag on (requires
extra-buffer radix strategy + hybrid SSM cache, non-dllm) eligible
prefills end at the aligned fork so the fork state and the tail state
both reach the cache — through initial admission, continuation, and a
host load-back that moved the prefix.

UNION / DEVIATIONS:
- Single-unfinished-chunk guard reconciled per instruction in favor of
  the landed sgl-project#46643 logic: the unconditional 'if has_chunked_req' guard
  is kept (it subsumes this PR's split-conditioned reopening); the PR's
  'or split_prefill_at_mamba_fork' term is a no-op against it.
- Consequently the PR's test
  test_second_unfinished_chunk_guard_follows_policy_unless_fork_split
  is re-pinned to test_second_unfinished_chunk_guard_is_unconditional:
  every policy + both flag states now refuse the second unfinished
  chunk. All other 8 fork tests verbatim.
- add_chunked_req: the fork cut is applied after the sgl-project#45607 hybrid
  page-align (both land on page multiples; order-safe) and before the
  request-slot reserve, matching the PR's position relative to that
  line. truncation_align_size now threads from the scheduler (PR's
  scheduler.py hunk) but is consumed only by _mamba_fork_split_len —
  our entry-177 _align_prefill_chunk call keeps its existing (default)
  argument so no QSA-continuation behavior drifts.
- Test import hunk: the PR base's PrefillBootstrapQueue import does not
  exist in our file; resolved to os + environ.envs + policy-enum
  additions only.

CPU: test_prefill_adder + held_row_admission 69P/85sub (8 fork-matrix
cases pass incl. re-pinned guard); full managers dir +
prefill_memory_budget + mamba donated/capacity +
unified_radix_cache_unittest = 1091P/2888skip/969sub; the only 2F are
the pre-existing order-dependent test_mm_process_config failures
(verified identical on the ported-branch baseline at sgl-project#46643).

(cherry picked from commit 0e20970)
(cherry picked from commit 8832eae148114995e251aed28c16a20985dfd8c5)
detain added a commit to detain/sglang that referenced this pull request Oct 29, 2026
…gl-project#45607/sgl-project#45789 (entries 267-270)

Second admission docs commit. Records: sgl-project#46496 DRAFT-upstream status;
sgl-project#46643's BEHAVIOR CHANGE (admission re-runs every round for ALL models --
TTFT/starvation watch at the owed restart, entry-198 precedent; the two
entry-198 arms removed as strict subsets, COW-retry semantics untouched);
sgl-project#45607 page-align composition with entry-177's lcm aligner (both shrink-
only; exact_chunk_fill arm untouched upstream too); sgl-project#45789 default-OFF
fork-split with the guard reconciliation vs sgl-project#46643 (policy-reopening term
proven no-op, test re-pinned), fork cut after sgl-project#45607 align, and the
deliberate no-thread of truncation_align_size into entry-177's aligner.

Battery: every worker number REPRODUCED EXACT AT THEIR OWN TREES (method
identity), then re-measured on the combined tree with full attribution of
every drift; managers dir RE-PINNED 1013P/2F/3S/933sub (base 988). The
worker's stashed-baseline '992P' decomposes only as fresh-base 988 +
their own picks (+5 at sgl-project#46643 EXACT) -- flagged as measurement noise.
detain added a commit to detain/sglang that referenced this pull request Oct 29, 2026
…20 commits since 12dc623 @ merge-sgl-project#4)

Conflicts: 167 files (149 content / 14 add/add / 4 modify/delete) at the settled
309 tip d28e7cd -- dossier predicted 146 +/-25; the +21 shift is entries
275-309 landing qsa-ring/hicache-ctrl/gdn/moe/llguard deltas into the conflict
zone. By class: A 13 (auto-takes-upstream, dedupe-verified), B 80, C 56, D 18.
102 TODO(merge5) re-plant/fold markers planted: B1 x15, B2 x29, B3 x12, B4 x23,
B5 x23. Re-plant stock: 117 files under /tmp/opencode/merge5-dossier/replant/
(76 fork-hunks patches, 19 upstream-side patches, 14 AA fork copies, 8
preremove snapshots) -- sessions 2/3 re-plant from real code, not memory.

D-list outcomes (must-not-auto-resolve, handled personally):
- sgl-project#45289-vs-sgl-project#44858 (entry 301): VERIFIED, not re-resolved -- the gate/up-order
  final form is in origin/main (5b1dbc2 in range); scales test taken as
  upstream oracle, fork 191-line version saved to replant/ for B5 diffing.
  sgl-project#44858's call-site shape CANNOT survive (our base never had it).
- entry-230 dispatcher vs sgl-project#44993/sgl-project#44994: took upstream structure in
  token_dispatcher/standard.py; published-value CONTRACT lives upstream via
  get_moe_runner_backend() in the LoRA-EP guard; fork's warn-once mismatch
  diagnostic re-plants (B5 marker). UNION GATE: standard_dispatcher 16P (was
  12P, suite grew), model_overrides 121P/1S/210sub (was 112P/1S/181sub),
  fold probe 6P/2F (upstream added a runner_backend arg to the predicate --
  B5 re-pins the probe).
- entry-219 gauge names vs sgl-project#43727 return + sgl-project#44968: PER-POOL NAMES KEPT as the
  serving schema -- hicache_host_pool_{used,total}_tokens{pool} dataclass
  fields, Gauge registrations and labels(pool=) emission re-restored over the
  #4697x structure; hicache_host_pool_evicted_tokens_total already survived
  auto-merge; producer dict restored by union in metrics_reporter. ANTI-NEEDLE
  SATISFIED: old mamba-only gauges are NOT the serving schema.
- entry-221 eager-direct add/add: DEDUPED to upstream (bodies identical except
  est_time 15->9); MEASUREMENT DEBT RECORDED: prefill t/s must be re-measured
  post-B4 before trusting the est_time.
- entry-168 D2H fence vs sgl-project#44800: the fence is ALREADY UPSTREAM (both
  load_fence_stream + write_fence_stream with the load-thread-corruption
  comment in origin/main's cache_controller.py) -- nothing re-planted; the two
  real regions resolved as import-union + comment wording (merge sgl-project#4 stale-tail
  pattern avoided). Fork hunks still saved to replant/ (belt).
- entry-209 clamp + 216/218 ordering + 288-295 hicache-ctrl-era vs #46970x KV
  rewrite: ADOPTED-UPSTREAM across unified_radix_cache/hicache_storage/
  hybrid_cache_controller/memory_pool/kv_cache_builder; EVERY fork hunk
  extracted to replant/*.fork-hunks.patch; B3 re-plant markers planted.
  sgl-project#45703's short-full-match seam arrives with upstream.
- sgl-project#45090 SGLANG_ENABLE_UNIFIED_RADIX_TREE: REMOVAL TAKEN (fork had already
  moved it to _DeprecatedEnv; now gone). grep: ZERO .py readers repo-wide;
  2 upstream-owned docs jsx snippets still export it (deepseek-v4, glm-5.2 --
  upstream's own debt). OPERATOR-OWED CHECK: confirm no /root harness exports
  it (unreadable from this seat).
- sgl-project#39727 unified L3 key: VERIFIED OPT-IN -- merged hicache_storage.py keeps
  the DEFAULT legacy suffix composition _<model>_<tp_rank>_<tp_size>
  (content-anchor: HiCacheFile._init_config_suffix, raise-guarded when
  _unified_chunk_spans is set). ANTI-NEEDLE: no silent key-scheme switch;
  warm /root/hicache_store contents stay addressable.

Modify/delete (4, all victims of upstream sgl-project#45672 test prune + URC split):
- test_unified_radix_cache_unittest.py: REMOVED (adopted split) -- its bare
  unified_tree_core_inspector imports are unreachable post-sgl-project#45236 re-home;
  fork source + hunks saved; B2 marker planted in the new core test.
- test_fused_moe_triton_config.py: REMOVED -- imports common_utils, absent
  from both sides post-sgl-project#45672; the 2 live entry-3/235 tests re-plant in B2.
- test_scheduler_batch_is_full_latch.py: REMOVED -- fork delta was 3 stub-attr
  lines documenting sgl-project#46643's absorbed arms; upstream deletion correct.
- test_qsa_kv_pool.py: KEPT -- fork's 147 added lines are entry-187/200
  sharding + 285-287 QSA-ring tests with NO upstream replacement;
  qsa_kv_pool.py sides converged byte-identical; collects clean. B2 marker.

Cascading silent-auto-merge repairs (classes not in the conflict set but
broken by auto-merge + union splices -- all found by gates, now fixed):
kernels/ops/activation/__init__.py (dead _CUDA registration dup), kernels/ops/
diffusion/__init__.py (5-tuple _SPECS entry), kernels ops embeddings/moe/
attention/mamba __init__ (KernelSpec fields removed upstream: description/
format_signature stripped, CapabilityRequirement->CUDA_ONLY), base_runner/
eager_runner cp_extend + shared_read_event imports retargeted per upstream's
"no imports in runner_utils/__init__" contract, and 4 fork helpers grafted
back onto their take-upstream homes because merged code/tests still import
them (memory_pool.spec_intermediate_bytes_per_req, kv_cache_builder.
_adjust_hicache_size_for_draft + _device_pool_size_per_token,
hybrid_cache_controller.local_restorable_mask, validation_hook.
validate_replayssm_spec_algorithm, arg_groups/moe_hook b12x validator).
flashinfer_cutlass.py flipped to FORK-OWNED (b12x route is fork-only, 760 vs
95 lines; upstream sgl-project#44993 hunks = B5 fold patch saved).

Silent-semantics notes: sgl-project#46127/sgl-project#44912 riders arrived via per-PR picks (union);
sgl-project#45520 (entry 302) is STILL OPEN upstream -- entry-230 fail-safe layer stays
as entry 303 records; if a future sync brings it, re-apply the fail-safe reads.
qwen4_exp holder-contract fused (ctor takes optional batch).

Gates at commit: import sglang OK (CVD=9, prod venv); check_registered_tests/
check_no_bare_pytest_main/check_no_registered_tests_in_package rc=0; ruff F821
+E9+format clean on the resolution set; collect-only over unit/{mem_cache,
model_executor,distributed,layers/moe} = 4337 nodes, 3 ERRORS: 2x cargo/torch-
sys libtorch probe (REPRODUCED IDENTICALLY AT THE PRE-MERGE TIP -- env issue,
not merge damage) + test_hicache_async_ready_sync.py (entry-253-era attribute
gone with the URC rewrite -- EXPECTED pre-B3).

sessions 2/3 owed: B2 test re-home, B3 mem_cache+rust re-plant (+hicache-ctrl
re-ports), B4 scheduler/admission, B5 moe/quant/dispatcher, B6 environ+C0a+
CAVEATS re-pin. PROD BOOT FORBIDDEN until B3 completes (209 clamp + 218
ordering re-plant).
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

# Conflicts:
#	python/sglang/srt/mem_cache/prefill_budget.py
detain added a commit to detain/sglang that referenced this pull request Oct 30, 2026
…entries 122,198,200,211,215,220,267,268,305

Re-plants the scheduler.py fork-hunks (replant/...managers__scheduler.py.fork-hunks.patch)
onto the post-sgl-project#46977 admission structure. tp_worker marker consumed (15/23 B4 markers
still on the tree; per-file counts, damage-state from entry 310's map).

PER-HUNK DECISIONS (all VERIFIED against the merged tree, not taken on faith):
- entry 122 (sgl-project#40458): grammar-sync overlap disable RE-PLANTED for mixed mode
  (is_decode() OR is_mixed()) + the fork's trimmed comments; upstream's
  host-draft-only arm was the pre-sgl-project#40458 form (origin/main verified: absent).
- entry 220 (sgl-project#39765, OPEN port): _has_pending_mamba_cache_update + the early
  pop_and_process/short-circuited disable-overlap RE-PLANTED into
  event_loop_overlap; the require_mlp_sync/disable/extra-buffer gate verbatim.
- entry 198 (sgl-project#37612): THE CATCHER — B3-5 landed the raiser; the
  try/except MambaCowAllocError around init_next_round_input (res=NO_TOKEN,
  request stays queued; the revert block + unconditional alloc_group_end were
  already in the merged tree = the no-leak halves VERIFIED PRESENT) RE-PLANTED
  with the import. COW component test re-anchored to the #46970x kv_storage
  seam the B3-5 replant moved _state() behind (stub was the pre-split
  mamba_allocator shape).
- entry 268 (sgl-project#46643, OPEN port -- VERIFIED NEVER MERGED: git log
  12dc623..2a6c204 --grep 46643 = ZERO; only sgl-project#45491/sgl-project#45435/sgl-project#45472/sgl-project#45670
  of the briefed merged-list arrived in-range): the UNCONDITIONAL batch_is_full
  clear RE-PLANTED on the scheduler side, subsuming the entry-198 hybrid-SSM
  arm and the priority-preemption/is_empty arms (policy side lands in B4-3).
- entry 267 (sgl-project#46496): retract_decode new_token_ratio FLOOR clamp RE-PLANTED.
- entry 211: triton_load_watch.prewarm_device_loads(self.model_worker) line
  RE-PLANTED between install() and mark_serving_started (the utils-side
  function survived the merge; only the call site was dropped).
- entry 200 (sgl-project#43527) CAPACITY PAIR RE-PLANTED: model_runner
  .logical_max_total_num_tokens passes allocator= again (the configurator's
  PageInterleavePoolAllocator -> allocator.size branch SURVIVED the merge
  byte-identical, so the merged tree had the fork predicate with upstream's
  consumer-side multiplications = DOUBLE-COUNT); the three scheduler
  * kv_shard_widening multipliers back to the fork form (pool-stats shape,
  load_inquirer, and the init_req one-page-per-shard subtraction);
  tp_worker drops page_interleave_shard_size x + import. Upstream's
  invariant-checker hunk: OBSOLETE (the constructor no longer takes
  max_total_num_tokens -- absorbed by refactor).
- entry 215 (sgl-project#44351/sgl-project#44401/sgl-project#44420): VERIFIED ALREADY PRESENT at the merge
  (filter_batch-every-step + reset ride, num_held_rows, budget_state FIRST
  statement, PD-gate deletion all byte-compare to the fork form) -- DEDUPED,
  no edit. Entry 107's local early-revision of sgl-project#40940 stays superseded
  (entry 215 rule), and its re-plant here would double-count.
- entry 220's/305's pause-window busy accounting + write_fence_stream +
  is_empty arm: VERIFIED PRESENT (entry 305 landed patch-identical via
  sgl-project#46127; the fence assignment is the B3-2 controller half) -- DEDUPED.

TEST FIXTURE RE-ANCHORS (not B2's: no markers, pure new-upstream-contract
drift, per the entry-198 fixture-adaptation precedent): the two hybrid-ssm
admission stubs gain disagg_prefill_bootstrap_queue=None (upstream's
routing-key bootstrap_queue arg in calc_priority arrived in-range).

PRUNE vs BREAKAGE (managers −523sub audit): the three managers test files
vanished in-range (test_schedule_policy_dfs_weight, test_scheduler_batch_is
_full_latch, test_uno_token_accounting) were ALL deleted by upstream's OWN
sgl-project#45672 prune (3438ed6). Two are byte-identical upstream copies = zero
fork delta. The third carried a 3-line fork comment/stub delta whose
SEMANTICS the re-planted unconditional clear pins: its case was the
is_empty-arm mirror of test_scheduler_hybrid_ssm_admission_latch (present,
green, hybrid_swa/non-hybrid arms included) -- NOT re-homed, recorded.

ACTUALS (damage -> after, CVD=9, prod venv, PYTHONPATH=python, per-file):
  test_scheduler_hybrid_ssm_admission_retry   4F -> ** 4P **
  test_scheduler_hybrid_ssm_admission_latch   3F -> ** 4P **
  test_mamba_prefill_cache_boundary  2F/12subF -> ** 5P/12sub EXACT (220 pin) **
  test_scheduler_retract_ratio                1F -> ** 2P EXACT (267 pin) **
  test_scheduler_init_req_max_new_tokens      1F -> ** 10P/6sub **
  test_scheduler_grammar_mixed_tail_sync      1F -> ** 4P **
  adjacent scheduler-stub files (held_row, running_batch_finished,
  pause_generation, disagg_idle, dp_converted, hicache_events) 56P/64sub
  UNMOVED -- no regression from the clear/catcher/capacity edits.
Lint: ruff format + F821/E9 clean on all five files; imports OK (no GPU
execution, no push).

Co-Authored-By: OpenCode <noreply@opencode.ai>
detain added a commit to detain/sglang that referenced this pull request Oct 30, 2026
…46977 pricing reconciliation (entries 177,266,268,269,270 + 217)

2 markers consumed (schedule_policy.py, scheduler.py). B4 tree count now 15.
prefill_budget.py needed NO edit: sgl-project#46643's fix(1) (available_chunk_tokens
capped by real free KV minus the one-page over-ask, defer-not-force) SURVIVED
the merge byte-identical in the base class -- deduped with evidence (diff
309-tip vs HEAD shows only upstream's additive state_slot plumbing).

DECISION TABLE (per-hunk, sgl-project#46977 new admission pricing vs entries 265..270):
- entry 177 prefill_chunk_alignment: RE-PLANTED as-is -- kwarg + attribute +
  _align_prefill_chunk (LCM w/ kv_shard_granule) verbatim; the new tree's
  add_chunked_req grew a kv_shard_granule floor (upstream) that idempotently
  composes with the LCM (both floor-to-multiple); scheduler-side get_kvcache
  read RE-PLANTED too (dropped at the merge = the held_row/adder TypeErrors).
- entry 266 sgl-project#43484 classic-pool slot ledger: RE-ADAPTED. The unified half is
  SUPERSEDED-BY-EQUIVALENT: sgl-project#46977 moved the exact fork ledger onto the budget
  (ChainPrefillBudget.rem_state_slots = mamba_allocator.schedulable +
  mamba_evictable; debit rem -= state_slot_tokens//cost in reserve(); gate via
  budget_state's state_slots_exhausted()) -- factor 1, charge == reserve //
  cost, byte-for-byte identical semantics; the adder-side twin was DROPPED to
  avoid a double ledger. The CLASSIC half SURVIVES as fork delta (the
  base-class budget never sees state): isinstance(HybridReqToTokenPool) gate
  (NOT hasattr -- MagicMock discipline, 266(c) preserved), per-req charge
  1 + mamba_initial_tracking_slots (PR-bug math substitution kept, 266(a)),
  budget_state gate on the FULL charge (< per_req), add_one_req required-slot
  check incl. the defer_staged mamba branch, and slot-unit DEBIT threading at
  all five commit sites + _commit_prefill_admission.
- entry 267: (B4-1) done. entry 268 fix(2): RE-PLANTED -- the
  second-unfinished-chunk guard now applies to ALL policies (the merged
  shortest-prefill-first-only restriction was upstream's own pre-sgl-project#46643 form;
  git log: sgl-project#46643 never merged). sgl-project#45789's policy-reopening term stays the
  recorded PROVEN NO-OP (270(i)); test re-pins ..._is_unconditional.
- entry 269 page-align: RE-PLANTED verbatim at its 3 sites, composed with
  sgl-project#46977's floor_chunked_tokens (DCP grid) as page-align-then-grid-then-fork.
- entry 270 fork-split: RE-PLANTED (gate on get_exec().mamba.enable_mamba_
  extra_buffer + SGLANG_SPLIT_PREFILL_AT_MAMBA_FORK on our env -- the env
  union landed complete at the merge, environ.py untouched), incl. the
  continuation/commit/new-chunk/ignore-eos sites, the larger-host-hit
  re-clamp (270's fork cut lands AFTER page-align, order-safe), the
  truncation_align_size threading from scheduler (270(iii): _align_prefill_chunk
  call KEEPS its default arg deliberately), and the 'no other prefill may
  take its slot' return.
- entry 217 (PrefillAdder closes admission on retraction): the
  finish_storage_prefetch_admission sites VERIFIED PRESENT (retraction/discard
  x2) -- DEDUPED, no edit.
- entry 265: the brp required-slot block's defer/NO_TOKEN shape above; id-set
  half was B4-2.

BEHAVIOR (DEFAULT-ON) reaffirmed for the owed restart-watch: sgl-project#45472's
sync-free planning (B4-2 verified in-tree) + sgl-project#46643's UNCONDITIONAL per-round
admission re-runs (re-planting this cluster) are the two entry-198-class
behavior changes owed TTFT/starvation A/B at the prod restart (still banned;
zero GPU execution this session).

TEST RE-ANCHORS (upstream rewrites of these files silently dropped the
fork's fixture halves -- restored from the 309-tip versions, each pinned by
entry 215/266's own text): held_row's _Adder.add_chunked_req truncation_
align_size kwarg + get_kvcache allocator stub (entry-177 pair, upstream
sgl-project#46980 rewrite dropped them); dcp_logical's pools NS gains state=None
(#46970x _InitializedPools field) and test_scheduler_consumers_receives
_logical_capacity re-adapted to the new seams (pool-stats via the storage
shape, invariant checker's capacity kwarg REMOVED upstream, load_inquirer
direct) -- same anti-double-widening intent, kv_shard_widening=4 fixture
obsolete by design after the B4-1 re-plant.

ACTUALS (damage -> after, CVD=9, prod venv, per-file):
  test_prefill_adder   7F/32subF (39F storm) -> ** 71P/0F/94sub **
  test_dcp_logical_capacity 1F/6subF -> ** 5P/6sub **
  test_scheduler_held_row_admission (regressed by my scheduler wiring, then
  repaired) -> ** 1P/2sub EXACT (215 pin) **
  hybrid_ssm latch+retry family ** 9P ** (entry-198's '8P' + B4-1's case)
  DIR: unit/managers 1048P/48F/9S/439sub -> ** 1055P/3F/9S/474sub **
  (3F = the two order-dependent test_mm_process_config ENV F's [reproduce
  at base] + test_io_struct 1F [diagnosed B4-5, fixture half]).
Lint: ruff format + F821/E9 clean. No GPU; no push.

Co-Authored-By: OpenCode <noreply@opencode.ai>
detain added a commit to detain/sglang that referenced this pull request Oct 30, 2026
…re-plant) SESSION 2B COMPLETE

Entry 312 records: the 5 cluster commits (9bd9007..5cde7a5) with per-
cluster batteries; THE sgl-project#46977 RECONCILIATION DECISION TABLE (per-hunk
survive-as-is / re-adapt / supersede, incl. the git-log proof that the
spec-mamba four arrived in-range while the admission six NEVER merged --
correcting the briefing's dedupe list, and sgl-project#46643 fix(1)'s reconcile as
already-merged base-PrefillBudget form = the reason prefill_budget.py got
ZERO B4 edits); the entry-200 CAPACITY-PAIR restoration (our configurator's
allocator.size branch survived -> upstream's consumer-side multiplications
double-counted -> model_runner allocator= kwarg + tp_worker/scheduler
subtractive forms re-planted; invariant-checker hunk OBSOLETE by upstream
refactor); the io_struct NEEDS-ATTENTION VERDICT (upstream's own
pointer_head guard vs stale fork stub -- test-side one-field re-anchor,
matching sibling stubs); the -523sub PRUNE-vs-BREAKAGE audit (all three
vanished managers files died to upstream's OWN sgl-project#45672 prune, two byte-
identical to origin/main, the third's fork delta subsumed by sgl-project#46643 --
recorded, not re-homed); the B3 cross-note closures (MambaCowAllocError
CATCHER landed; 216/217 admission interplay VERIFIED absorbed); the 2err
population correction (replayssm_ring_fold pruned by sgl-project#44690, gdn cutedsl
probe lives in the kernels zone, the spec dir's real err was merge damage,
now 0err); the runner-zone finds the damage map missed (_init_model_hooks
BOOT-INTEGRITY loss -- real decode-runner builds AttributeError, and
component_manager's dropped residency half that parallel_executor already
CALLS: both re-planted, 7F+crash-class closed); the true marker census (23
tree-carried, NOT the briefing's 20 -- B3 lesson applied; ALL consumed,
B4=0, B1/B2/B5 diff-audited intact).

GATES: trailing_endpoint ** 17P = 17P ** re-verified post-B4 (DoD-d, B3's
gate unmoved). DoD(e) diff audit: ZERO mem_cache/ files changed, ZERO in
qsa/moe-quant zones (moe_hook diff is marker-removal-only).

NEW PINS: unit/managers 1009P/81F/9S/412sub/2err -> 1056P/2F/9S/474sub (2F
= the two documented order-dependent mm_process_config ENV F's; the dir's
2err never reproduced -- population is the spec row's); unit/spec
363P/20F/21S/238sub/2err -> 388P/0F/21S/238sub/0err EXACT over 7
consecutive full-dir runs. OWED RESTART-WATCH REAFFIRMED: sgl-project#45472's
sync-free planning (verified present, upstream form) + sgl-project#46643's
unconditional per-round admission re-runs (re-planted) ride the entry-201
restart; entry-198's TTFT/starvation measure STILL OWED.

Header: STACK -> qwennext-stack-312+merge5-b4-2026-10-29; +merge5-b4 wave
tag; tip -> 5cde7a5; entries 1-312; INTEGRATION OWED annotated B4
LANDED (boot ban STAYS). wanted_stack: UNTOUCHED (no S-item changed by B4
findings -- recorded here). ENV debts recorded NOT-fixed: pcie_vocab_gather
GPU suite (27F identical at the merge commit), openai 2.6.1<2.8.0
serving_responses import (upstream's own beam.py), imageio (whole diffusion
import chain). NOTHING PUSHED; tree clean; zero GPU execution; zero prod
boots all session.

Co-Authored-By: OpenCode <noreply@opencode.ai>

@rkarhila-amd rkarhila-amd 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.

This PR has three small changes on scheduling:
(1) bysassing "shortest-prefill-first" check in chunked prefill policy means that all policies lead to same branch
(2) always resetting "batch_is_full" in _get_new_batch_prefill_raw function in the scheduler
(3) changing available_chunk_count calculation in prefill budget, so that available tokens calculation take into account evictable KV tokens among other factors. Also if 0 tokens are available, None is returned, as opposed to old behaviour where full chunk size was returned.

The changes are small but impact all models, and for this reason, I kindly ask for reproduction commands to show the problems that are fixed, and to verify with several other models that the performance with these scheduling changes is not adversely affected.

@aryatsc

aryatsc commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

@rkarhila-amd thanks. Reproductions and the multi-model A/B are below.

Reproduction commands

Each fix has a unit test that fails on the merge base (fbc90a860e) and passes on this branch (ae64753ca5). To reproduce, run the new tests against the old scheduler code:

git worktree add /tmp/sched-base fbc90a860e
cd /tmp/sched-base
git checkout ae64753ca5 -- \
  test/registered/unit/managers/test_prefill_adder.py \
  test/registered/unit/managers/test_scheduler_batch_is_full_latch.py \
  test/registered/unit/mem_cache/test_prefill_memory_budget.py \
  test/registered/unit/models/test_aiter_attention_backend_handler.py
PYTHONPATH=python python -m pytest -q <the four files above>
Fix Commit Test (fails on base, passes on PR) What it reproduces
1. One unfinished chunked request under every policy ba516e7 test_prefill_adder.py::TestPrefillAdder::test_default_policy_rejects_second_unfinished_chunk Under the default (non-SPF) policy, PrefillAdder starts a second chunk while one is already in flight. The scheduler only tracks one chunked_req, so the first one is lost.
2. Re-evaluate batch_is_full each step 274ec4c test_scheduler_batch_is_full_latch.py::TestEmptyBatchFullLatch::{test_latched_empty_batch_still_admits, test_latched_running_batch_still_admits} Once batch_is_full is set, _get_new_batch_prefill_raw returns None until an unrelated request finishes, even if the KV has freed up. Waiting requests stall.
3. Cap a continuing chunk by allocatable KV (incl. evictable), else defer af2910e test_prefill_memory_budget.py::TestContinuingChunkCap::{test_chunk_is_deferred_without_free_kv, test_chunk_leaves_the_page_alloc_extend_over_asks} With no free or evictable KV, the old code returned chunk_limit anyway, so alloc_extend over-asks and fails. Now it returns a size that fits, or None to defer.
(ROCm) keep over-capacity aiter extends chunked 514ee13 test_aiter_attention_backend_handler.py::TestAiterOverCapacityExtend::test_over_capacity_extend_stays_chunked_in_graph_context

Result: 6 failed, 71 passed on base with the PR tests, and 77 passed on the PR branch. CPU only.

Multi-model performance (MI355X, TP1)

Same image for both arms; only python/ differs: base is fbc90a860e, PR is ae64753ca5. Server flags are --tp 1 --chunked-prefill-size 8192. kv-pressure adds --max-total-tokens 40000, which forces chunk continuation, batch_is_full, and eviction on every step. Each arm was run twice in alternating order (base, pr, base, pr) on a fresh server:

python -m sglang.bench_serving --backend sglang --dataset-name random \
  --random-input-len 3000 --random-output-len 300 --random-range-ratio 0.5 \
  --num-prompts 320 --max-concurrency 64 --seed 1
Model Config Arm Completed Output tok/s Mean TTFT (ms) P99 TTFT (ms) Mean TPOT (ms)
Qwen3-8B default base 320 / 320 3386 256 1733 17.1
Qwen3-8B default PR 320 / 320 3397 255 1723 17.1
Qwen3-8B kv-pressure base 320 / 320 2039 4928 6121 7.2
Qwen3-8B kv-pressure PR 320 / 320 2037 4926 6110 7.2
Qwen3-30B-A3B default base 320 / 320 2659 259 1607 22.0
Qwen3-30B-A3B default PR 320 / 320 2612 262 1617 22.4
Qwen3-30B-A3B kv-pressure base 320 / 320 1441 6958 8705 10.2
Qwen3-30B-A3B kv-pressure PR 320 / 320 1435 6978 8692 10.3
DeepSeek-V2-Lite default base 320 / 320 3134 263 1464 18.4
DeepSeek-V2-Lite default PR 320 / 320 3111 264 1449 18.6
DeepSeek-V2-Lite kv-pressure base 320 / 320 1526 6605 8228 9.6
DeepSeek-V2-Lite kv-pressure PR 320 / 320 1522 6619 8246 9.6

Values are means of the two runs. One exception: the first base run of each model in the default config was the container's first server start (cold JIT, for example 980 tok/s on Qwen3-8B), so the base default rows use only the second base run. Raw values for every run:

All runs (output tok/s, mean TTFT ms)
Model Config base r1 PR r1 base r2 PR r2
Qwen3-8B default 980 / 443 (cold) 3402 / 255 3386 / 256 3391 / 255
Qwen3-8B kv-pressure 2037 / 4930 2041 / 4916 2040 / 4926 2033 / 4936
Qwen3-30B-A3B default 2441 / 284 (cold) 2619 / 258 2659 / 259 2605 / 266
Qwen3-30B-A3B kv-pressure 1442 / 6950 1442 / 6945 1439 / 6965 1428 / 7011
DeepSeek-V2-Lite default 2993 / 430 (cold) 3106 / 261 3134 / 263 3116 / 266
DeepSeek-V2-Lite kv-pressure 1522 / 6624 1523 / 6616 1530 / 6586 1521 / 6622

Every request completed in all 24 runs. Differences are within ±1% on every row except Qwen3-30B-A3B default, which is −1.8% against a single warm base run; that is inside the base r1-to-r2 spread on the same row. DeepSeek-V2-Lite server logs had 2–4 lines matching out of memory|Traceback|assert in both arms, while the Qwen logs had none. No run crashed, failed a request, or emitted an OOM.

This branch has not been deployed

No deployments
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.

2 participants