Files
wehub-resource-sync 94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

1177 lines
48 KiB
Python

"""
Life cycle of a request in the prefill server
1. Bootstrap Queue
a. Initialize a sender for each request
b. Use the queue to store requests whose bootstrap (handshake and preallocation) has not finished
c. Poll senders to check bootstrap state
d. Once bootstrap is complete, move request to Waiting Queue
2. Waiting Queue
a. Use PrefillAdder to pop requests
b. Run forward
c. Add the request to Inflight Queue
3. Inflight Queue
a. Poll (non-blocking) the sender of the request
b. Once the transfer has finished, return the request
"""
from __future__ import annotations
import hashlib
import logging
from array import array
from collections import deque
from http import HTTPStatus
from typing import TYPE_CHECKING, List, Optional
import numpy as np
import torch
from sglang.srt.disaggregation.base import KVPoll
from sglang.srt.disaggregation.base.conn import StateType
from sglang.srt.disaggregation.common.conn import CommonKVManager
from sglang.srt.disaggregation.utils import (
FAKE_BOOTSTRAP_HOST,
DisaggregationMode,
KVClassType,
MetadataBuffers,
ReqToMetadataIdxAllocator,
TransferBackend,
get_dsv4_c128_state_indices,
get_kv_class,
is_aborted,
is_dsv4_c128_online_enabled,
is_mla_backend,
poll_and_all_reduce_attn_cp_tp_group,
prepare_abort,
setup_state_kv_args,
)
from sglang.srt.environ import envs
from sglang.srt.managers.schedule_batch import (
FINISH_ABORT,
FINISH_LENGTH,
NextBatchPlan,
Req,
ScheduleBatch,
)
from sglang.srt.mem_cache.common import (
kv_to_page_indices,
kv_to_page_num,
maybe_cache_unfinished_req,
release_kv_cache,
)
from sglang.srt.mem_cache.deepseek_v4_memory_pool import DeepSeekV4TokenToKVPool
from sglang.srt.observability.req_time_stats import set_schedule_time_batch
from sglang.srt.utils.nvtx_utils import scheduler_nvtx_method
if TYPE_CHECKING:
from torch.distributed import ProcessGroup
from sglang.srt.managers.scheduler import GenerationBatchResult, Scheduler
from sglang.srt.mem_cache.memory_pool import KVCache
logger = logging.getLogger(__name__)
def should_force_retry(req: Req) -> bool:
"""Test hook to force a request into optimistic prefill retry."""
retry_prob = envs.SGLANG_TEST_FORCE_OPTIMISTIC_PREFILL_RETRY_PROB.get()
# Force only before/during the first attempt (count is 1 while it runs).
if retry_prob <= 0 or req.prefill_attempt_count > 1 or req.is_retracted:
return False
digest = hashlib.sha256(str(req.rid).encode()).digest()
return int.from_bytes(digest[:8], "big") < retry_prob * 2**64
def maybe_release_metadata_buffer(
req: Req, allocator: ReqToMetadataIdxAllocator
) -> None:
"""
Release the metadata buffer index allocated for a request in prefill disaggregation mode.
This function safely releases the metadata buffer index if it was allocated.
Args:
req: The request object that may have a metadata_buffer_index allocated
allocator: The ReqToMetadataIdxAllocator instance to free the index
"""
if req.metadata_buffer_index >= 0:
allocator.free(req.metadata_buffer_index)
req.metadata_buffer_index = -1
class PrefillBootstrapQueue:
"""
Store the requests in bootstrapping
"""
def __init__(
self,
token_to_kv_pool: KVCache,
draft_token_to_kv_pool: Optional[KVCache],
req_to_metadata_buffer_idx_allocator: ReqToMetadataIdxAllocator,
metadata_buffers: MetadataBuffers,
tp_rank: int,
tp_size: int,
gpu_id: int,
bootstrap_port: int,
gloo_group: ProcessGroup,
max_total_num_tokens: int,
scheduler: Scheduler,
pp_rank: int,
pp_size: int,
transfer_backend: TransferBackend,
):
self.token_to_kv_pool = token_to_kv_pool
self.draft_token_to_kv_pool = draft_token_to_kv_pool
self.is_mla_backend = is_mla_backend(token_to_kv_pool)
self.metadata_buffers = metadata_buffers
self.req_to_metadata_buffer_idx_allocator = req_to_metadata_buffer_idx_allocator
self.tp_rank = tp_rank
self.tp_size = tp_size
self.pp_rank = pp_rank
self.pp_size = pp_size
self.gpu_id = gpu_id
self.bootstrap_port = bootstrap_port
self.queue: List[Req] = []
self.gloo_group = gloo_group
self.scheduler = scheduler
self.max_total_num_tokens = (
self.scheduler.tp_worker.model_runner.max_token_pool_size
)
self.transfer_backend = transfer_backend
if envs.SGLANG_DISAGG_STAGING_BUFFER.get() and self.is_mla_backend:
raise RuntimeError(
"SGLANG_DISAGG_STAGING_BUFFER is designed for non-MLA models "
"(e.g. GQA, MHA). MLA models should not set this flag."
)
self.kv_manager = self._init_kv_manager()
def _init_kv_manager(self) -> CommonKVManager:
kv_args_class = get_kv_class(self.transfer_backend, KVClassType.KVARGS)
kv_args = kv_args_class()
kv_args.engine_rank = self.tp_rank
kv_args.pp_rank = self.pp_rank
kv_args.system_dp_rank = self.scheduler.ps.dp_rank
layer_shard_enabled = getattr(
self.token_to_kv_pool, "layer_shard_enabled", False
)
layer_shard_rank = getattr(self.token_to_kv_pool, "layer_shard_rank", None)
layer_shard_size = getattr(self.token_to_kv_pool, "layer_shard_size", 1)
transfer_draft_cache = (
not layer_shard_enabled or layer_shard_rank == layer_shard_size - 1
)
kv_args.prefill_start_layer = (
getattr(
self.token_to_kv_pool,
"layer_shard_start",
self.token_to_kv_pool.start_layer,
)
if layer_shard_enabled
else self.token_to_kv_pool.start_layer
)
kv_args.mla_compression_ratios = None
kv_data_ptrs, kv_data_lens, kv_item_lens = (
self.token_to_kv_pool.get_contiguous_buf_infos()
)
kv_args.prefill_end_layer = (
kv_args.prefill_start_layer + len(kv_data_ptrs)
if layer_shard_enabled
else getattr(self.token_to_kv_pool, "end_layer", None)
)
if self.draft_token_to_kv_pool is not None and transfer_draft_cache:
# We should also transfer draft model kv cache. The indices are
# always shared with a target model.
draft_kv_data_ptrs, draft_kv_data_lens, draft_kv_item_lens = (
self.draft_token_to_kv_pool.get_contiguous_buf_infos()
)
kv_data_ptrs += draft_kv_data_ptrs
kv_data_lens += draft_kv_data_lens
kv_item_lens += draft_kv_item_lens
kv_args.kv_data_ptrs = kv_data_ptrs
kv_args.kv_data_lens = kv_data_lens
kv_args.kv_item_lens = kv_item_lens
if not self.is_mla_backend:
kv_args.kv_head_num = self.token_to_kv_pool.head_num
kv_args.total_kv_head_num = (
self.scheduler.model_config.get_total_num_kv_heads()
)
kv_args.page_size = self.token_to_kv_pool.page_size
kv_args.aux_data_ptrs, kv_args.aux_data_lens, kv_args.aux_item_lens = (
self.metadata_buffers.get_buf_infos()
)
kv_args.ib_device = self.scheduler.server_args.disaggregation_ib_device
kv_args.gpu_id = self.scheduler.ps.gpu_id
req_to_token_pool = getattr(self.scheduler, "req_to_token_pool", None)
setup_state_kv_args(
kv_args,
self.token_to_kv_pool,
self.draft_token_to_kv_pool if transfer_draft_cache else None,
self.scheduler.model_config.num_hidden_layers,
req_to_token_pool=req_to_token_pool,
)
if isinstance(self.token_to_kv_pool, DeepSeekV4TokenToKVPool):
# V4's KVCache is organized by compression-ratio
# buckets rather than by layer.
kv_args.mla_compression_ratios = list(
self.token_to_kv_pool.compression_ratios
)
kv_manager_class = get_kv_class(self.transfer_backend, KVClassType.MANAGER)
kv_manager = kv_manager_class(
kv_args,
DisaggregationMode.PREFILL,
self.scheduler.server_args,
self.is_mla_backend,
)
# Pass KV pool tensor refs to the manager for GPU gather (staging mode)
if (
envs.SGLANG_DISAGG_STAGING_BUFFER.get()
and hasattr(kv_manager, "set_kv_buffer_tensors")
and not self.is_mla_backend
):
kv_pool = self.token_to_kv_pool
if hasattr(kv_pool, "full_kv_pool"):
kv_pool = kv_pool.full_kv_pool
if hasattr(kv_pool, "k_buffer") and hasattr(kv_pool, "v_buffer"):
kv_manager.set_kv_buffer_tensors(
kv_pool.k_buffer,
kv_pool.v_buffer,
kv_pool.page_size,
)
return kv_manager
def create_sender(self, req: Req, num_kv_heads: int) -> bool:
"""Create a KV sender for the request without enqueuing it.
Returns False if the request exceeds KV capacity."""
if self._check_if_req_exceed_kv_capacity(req):
return False
backend = (
TransferBackend.FAKE
if req.bootstrap_host == FAKE_BOOTSTRAP_HOST
else self.transfer_backend
)
kv_sender_class = get_kv_class(backend, KVClassType.SENDER)
dest_tp_ranks = [self.tp_rank]
req.disagg_kv_sender = kv_sender_class(
mgr=self.kv_manager,
bootstrap_addr=f"{req.bootstrap_host}:{self.bootstrap_port}",
bootstrap_room=req.bootstrap_room,
dest_tp_ranks=dest_tp_ranks,
pp_rank=self.pp_rank,
req_has_disagg_prefill_dp_rank=req.disagg_prefill_dp_rank is not None,
)
self._process_req(req)
req.pending_bootstrap = True
return True
def ensure_metadata_buffer(self, req: Req) -> bool:
if req.metadata_buffer_index >= 0:
return True
if self.req_to_metadata_buffer_idx_allocator.available_size() == 0:
return False
req.metadata_buffer_index = self.req_to_metadata_buffer_idx_allocator.alloc()
assert req.metadata_buffer_index is not None
return True
def finalize_bootstrap(self, req: Req) -> bool:
"""Initialize the sender after bootstrap completes.
Returns False if no metadata buffer is available (non-terminal)."""
assert req.pending_bootstrap, "finalize_bootstrap is not idempotent"
if not self.ensure_metadata_buffer(req):
return False
req.time_stats.set_bootstrap_done_time()
decode_prefix_len = req.disagg_kv_sender.pop_decode_prefix_len()
num_kv_indices = len(req.origin_input_ids)
req.start_send_idx = decode_prefix_len
num_kv_indices_to_send = num_kv_indices - decode_prefix_len
num_pages = kv_to_page_num(
num_kv_indices_to_send, self.token_to_kv_pool.page_size
)
req.disagg_kv_sender.init(num_pages, req.metadata_buffer_index)
req.pending_bootstrap = False
return True
def add(self, req: Req, num_kv_heads: int) -> None:
if not self.create_sender(req, num_kv_heads):
return
self.queue.append(req)
def extend(self, reqs: List[Req], num_kv_heads: int) -> None:
for req in reqs:
self.add(req, num_kv_heads)
def _check_if_req_exceed_kv_capacity(self, req: Req) -> bool:
if len(req.origin_input_ids) > self.max_total_num_tokens:
message = f"Request {req.rid} exceeds the maximum number of tokens: {len(req.origin_input_ids)} > {self.max_total_num_tokens}"
logger.error(message)
req.time_stats.trace_ctx.abort(abort_info={"reason": message})
prepare_abort(req, message, status_code=HTTPStatus.BAD_REQUEST)
self.scheduler.output_streamer.stream_output([req], req.return_logprob)
return True
return False
def _process_req(self, req: Req) -> None:
"""
Set max_new_tokens = 1, so PrefillAdder memory estimation is accurate
"""
req.sampling_params.max_new_tokens = 1
def pop_bootstrapped(
self,
return_failed_reqs: bool = False,
rids_to_check: Optional[List[str]] = None,
) -> List[Req]:
"""
pop the reqs which has finished bootstrapping
return_failed_reqs: For PP, on rank 0, also return the failed reqs to notify the next rank
rids_to_check: For PP, on rank > 0, check the rids from the previous rank has consensus with the current rank.
"""
bootstrapped_reqs = []
failed_reqs = []
indices_to_remove = set()
if len(self.queue) == 0:
if return_failed_reqs is False:
return []
else:
return [], []
polls = poll_and_all_reduce_attn_cp_tp_group(
[req.disagg_kv_sender for req in self.queue],
self.scheduler.attn_cp_cpu_group,
self.scheduler.attn_tp_cpu_group,
)
for i, (req, poll) in enumerate(zip(self.queue, polls)):
if (
rids_to_check is not None
and req.rid not in rids_to_check
and poll != KVPoll.Failed
):
# In PP mode, successful bootstrap still requires cross-rank
# consensus. Local failures are terminal and must be drained
# even if an earlier PP rank has already removed the request.
continue
if poll == KVPoll.Failed:
self.scheduler.handle_bootstrap_failure(req)
indices_to_remove.add(i)
failed_reqs.append(req)
elif poll == KVPoll.Bootstrapping:
if (
req.prefill_attempt_count
< self.scheduler.server_args.optimistic_prefill_attempts
and not req.is_retracted # engine paused
):
if not self.ensure_metadata_buffer(req):
continue # no more metadata buffer
req.prefill_attempt_count += 1
bootstrapped_reqs.append(req)
indices_to_remove.add(i)
req.time_stats.set_wait_queue_entry_time()
elif poll == KVPoll.WaitingForInput:
if should_force_retry(req): # skip checking for testing
if not self.ensure_metadata_buffer(req):
continue # no more metadata buffer
req.prefill_attempt_count += 1
elif not self.finalize_bootstrap(req):
continue
bootstrapped_reqs.append(req)
indices_to_remove.add(i)
req.time_stats.set_wait_queue_entry_time()
else:
raise RuntimeError(
f"Unexpected poll state {poll} for req {req.rid} in pop_bootstrapped"
)
self.queue = [
entry for i, entry in enumerate(self.queue) if i not in indices_to_remove
]
if return_failed_reqs is False:
return bootstrapped_reqs
else:
return bootstrapped_reqs, failed_reqs
def release_memory_occupation(self):
self.queue.clear()
if hasattr(self.kv_manager, "deregister_buffer_to_engine"):
self.kv_manager.deregister_buffer_to_engine()
def resume_memory_occupation(self):
if hasattr(self.kv_manager, "register_buffer_to_engine"):
self.kv_manager.register_buffer_to_engine()
class SchedulerDisaggregationPrefillMixin:
"""
Mixin for Scheduler to handle disaggregation prefill
"""
def maybe_prefetch_staging_for_batch(self: Scheduler, batch: ScheduleBatch) -> None:
"""Pre-send STAGING_REQ so decode allocates staging during GPU forward."""
kv_mgr = self.disagg_prefill_bootstrap_queue.kv_manager
prefetch = getattr(kv_mgr, "_prefetch_staging_reqs", None)
if prefetch is None:
return
for req in batch.reqs:
room = getattr(req, "bootstrap_room", None)
if room is not None and room in kv_mgr.transfer_infos:
prefetch(room)
def resolve_waiting_queue_bootstrap(self: Scheduler) -> None:
"""Resolve bootstrap status for waiting prefill requests before admission.
Covers the window between leaving the bootstrap queue and being admitted
into a running batch: aborts requests whose decode peer died, and
finalizes optimistic requests whose bootstrap completed so they skip
the post-forward bootstrap check.
"""
candidates = [req for req in self.waiting_queue if not is_aborted(req)]
if not candidates:
return
polls = poll_and_all_reduce_attn_cp_tp_group(
[req.disagg_kv_sender for req in candidates],
self.attn_cp_cpu_group,
self.attn_tp_cpu_group,
)
failed = set()
for req, poll in zip(candidates, polls):
if poll == KVPoll.Failed:
self.handle_bootstrap_failure(req)
failed.add(req)
elif (
poll == KVPoll.WaitingForInput
and req.pending_bootstrap
and not should_force_retry(req)
):
# Optimistic requests reserved a metadata buffer when popped, so
# finalize cannot fail here; if it ever does, the request stays
# pending and the post-forward check resolves it.
self.disagg_prefill_bootstrap_queue.finalize_bootstrap(req)
if failed:
self.waiting_queue = [
req for req in self.waiting_queue if req not in failed
]
def has_bootstrapped_waiting_req(self: Scheduler) -> bool:
return any(
not req.pending_bootstrap and not is_aborted(req)
for req in self.waiting_queue
)
@scheduler_nvtx_method("scheduler.get_next_batch_to_run")
def get_next_disagg_prefill_batch_to_run(
self: Scheduler,
running_batch: ScheduleBatch,
last_batch: Optional[ScheduleBatch],
) -> NextBatchPlan:
self.process_pending_chunked_abort()
# HACK (byronhsu): reset the batch_is_full flag because we never enter update_running_batch which resets it
# Otherwise, it hangs under high concurrency
running_batch.batch_is_full = False
self.resolve_waiting_queue_bootstrap()
self.process_prefill_chunk(last_batch=last_batch, running_batch=running_batch)
prefill_plan = self.get_new_batch_prefill(running_batch)
batch = prefill_plan.batch_to_run
running_batch = prefill_plan.running_batch
batch = self.dp_attn_adapter.maybe_prepare_mlp_sync_batch(batch)
if batch:
set_schedule_time_batch(batch)
return NextBatchPlan(batch_to_run=batch, running_batch=running_batch)
@torch.no_grad()
def event_loop_normal_disagg_prefill(self: Scheduler) -> None:
"""A normal scheduler loop for prefill worker in disaggregation mode."""
while True:
# Receive requests
recv_reqs = self.request_receiver.recv_requests()
self.process_input_requests(recv_reqs)
if self._engine_paused:
continue
self.waiting_queue.extend(
self.disagg_prefill_bootstrap_queue.pop_bootstrapped()
)
# Get the next batch to run
plan = self.get_next_disagg_prefill_batch_to_run(
running_batch=self.running_batch, last_batch=self.last_batch
)
self.running_batch = plan.running_batch
batch = plan.batch_to_run
self.cur_batch_for_debug = batch
# Launch the current batch
if batch:
if self.enable_staging:
self.maybe_prefetch_staging_for_batch(batch)
result = self.run_batch(batch)
self.process_batch_result(batch, result)
else:
self.on_idle()
self.process_disagg_prefill_inflight_queue()
# Update last_batch
self.last_batch = batch
@torch.no_grad()
def event_loop_overlap_disagg_prefill(self: Scheduler) -> None:
self.result_queue = deque()
while True:
# Receive requests
recv_reqs = self.request_receiver.recv_requests()
self.process_input_requests(recv_reqs)
if self._engine_paused:
continue
self.waiting_queue.extend(
self.disagg_prefill_bootstrap_queue.pop_bootstrapped()
)
self._apply_war_barrier()
# Get the next batch to run
plan = self.get_next_disagg_prefill_batch_to_run(
running_batch=self.running_batch, last_batch=self.last_batch
)
self.running_batch = plan.running_batch
batch = plan.batch_to_run
self.cur_batch_for_debug = batch
# Launch the current batch
if batch:
if self.enable_staging:
self.maybe_prefetch_staging_for_batch(batch)
batch_result = self.run_batch(batch)
self.result_queue.append((batch.copy(), batch_result))
else:
batch_result = None
# Process the last batch
if self.last_batch:
tmp_batch, tmp_result = self.result_queue.popleft()
self.process_batch_result(tmp_batch, tmp_result)
elif batch is None:
# When the server is idle, do self-check and re-init some states
self.on_idle()
self.process_disagg_prefill_inflight_queue()
# Run sample of the current batch
# It depends on the result of the last batch (e.g., grammar), so we run it after the last batch is processed.
self.launch_batch_sample_if_needed(batch_result, batch)
# Update last_batch
self.last_batch = batch
def process_batch_result_disagg_prefill(
self: Scheduler,
batch: ScheduleBatch,
result: GenerationBatchResult,
) -> None:
"""
Transfer kv for prefill completed requests and add it into disagg_prefill_inflight_queue
Adapted from process_batch_result_prefill
"""
(
logits_output,
next_token_ids,
extend_input_len_per_req,
extend_logprob_start_len_per_req,
copy_done,
) = (
result.logits_output,
result.next_token_ids,
result.extend_input_len_per_req,
result.extend_logprob_start_len_per_req,
result.copy_done,
)
if copy_done is not None:
copy_done.synchronize()
if result.routed_experts_output is not None:
result.routed_experts_output.finalize()
result.routed_experts_output = None
if result.indexer_topk_output is not None:
result.indexer_topk_output.finalize()
result.indexer_topk_output = None
logprob_pt = 0
# Transfer kv for prefill completed requests and add it into disagg_prefill_inflight_queue
next_token_ids = result.next_token_ids.tolist()
self.batch_result_processor.move_logprobs_to_cpu(
batch=batch,
logits_output=logits_output,
)
def advance_logprob_pt(i: int, req: Req) -> None:
nonlocal logprob_pt
if not req.return_logprob or extend_input_len_per_req is None:
return
extend_logprob_start_len = extend_logprob_start_len_per_req[i]
extend_input_len = extend_input_len_per_req[i]
if extend_logprob_start_len < extend_input_len:
logprob_pt += extend_input_len - extend_logprob_start_len
for i, (req, next_token_id) in enumerate(
zip(batch.reqs, next_token_ids, strict=True)
):
if req.inflight_middle_chunks <= 0:
req.time_stats.set_prefill_finished_time()
# Test hook: exercise the release/requeue retry path.
if req.pending_bootstrap and should_force_retry(req):
self.optimistic_release_and_requeue(req)
advance_logprob_pt(i, req)
continue
req.output_ids.append(next_token_id)
maybe_cache_unfinished_req(req, self.tree_cache)
self.disagg_prefill_inflight_queue.append(req)
if self.spec_algorithm.is_eagle() and batch.spec_info is not None:
req.output_topk_p = batch.spec_info.topk_p[i]
req.output_topk_index = batch.spec_info.topk_index[i]
req.hidden_states_tensor = (
batch.spec_info.hidden_states[i].cpu().clone()
)
else:
req.hidden_states_tensor = None
if req.return_logprob:
assert extend_logprob_start_len_per_req is not None
assert extend_input_len_per_req is not None
extend_logprob_start_len = extend_logprob_start_len_per_req[i]
extend_input_len = extend_input_len_per_req[i]
num_input_logprobs = extend_input_len - extend_logprob_start_len
self.batch_result_processor.logprob_result_processor.add_logprob_return_values(
i,
req,
logprob_pt,
next_token_ids,
num_input_logprobs,
logits_output,
)
logprob_pt += num_input_logprobs
if not req.pending_bootstrap:
self.send_kv_chunk(req, last_chunk=True)
req.time_stats.set_prefill_transfer_queue_entry_time()
if req.grammar is not None:
try:
req.grammar.accept_token(next_token_id)
except ValueError as e:
error_message = f"Grammar accept_token failed for req {req.rid} with token {next_token_id}: {e}"
release_kv_cache(req, self.tree_cache)
prepare_abort(
req,
error_message,
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
)
req.grammar.finished = req.finished()
else:
# being chunked reqs' prefill is not finished
req.inflight_middle_chunks -= 1
# Still chunking iff its next chunk was launched: either it is
# still self.chunked_req, or its final chunk (extend_range
# reaching the end of the input) is in flight. A yielded req
# is neither, so do its deferred release here.
still_chunking = self.chunked_req is req or (
req.extend_range is not None
and req.extend_range.end >= len(req.origin_input_ids)
)
if req.pending_bootstrap and not still_chunking:
self.optimistic_release_and_requeue(req)
advance_logprob_pt(i, req)
req.time_stats.set_last_chunked_prefill_finish_time()
continue
# Optimistic bootstrap can fail while this overlapped chunk is
# already running. Drop aborted chunks instead of sending KV.
if is_aborted(req):
advance_logprob_pt(i, req)
req.time_stats.set_last_chunked_prefill_finish_time()
continue
if req.return_logprob:
extend_logprob_start_len = extend_logprob_start_len_per_req[i]
extend_input_len = extend_input_len_per_req[i]
if extend_logprob_start_len < extend_input_len:
num_input_logprobs = extend_input_len - extend_logprob_start_len
self.batch_result_processor.logprob_result_processor.add_input_logprob_return_values(
i,
req,
logits_output,
logprob_pt,
num_input_logprobs,
last_prefill_chunk=False,
)
logprob_pt += num_input_logprobs
# In non-overlap-mode, KV is sent in process_prefill_chunk
# Only send when req's sender is initialized
if self.enable_overlap and not req.pending_bootstrap:
assert (
req.metadata_buffer_index >= 0
), f"Req {req.rid} does not have metadata buffer allocated"
self.send_kv_chunk(req, last_chunk=False, end_idx=req.tmp_end_idx)
req.time_stats.set_last_chunked_prefill_finish_time()
can_run_cuda_graph = result.can_run_cuda_graph
self.metrics_reporter.report_prefill_stats(
batch=batch,
prefill_stats=batch.prefill_stats,
can_run_cuda_graph=can_run_cuda_graph,
dp_cooperation_info=batch.dp_cooperation_info,
)
def process_disagg_prefill_inflight_queue(
self: Scheduler, rids_to_check: Optional[List[str]] = None
) -> List[Req]:
"""
Poll the requests in the middle of transfer. If done, return the request.
rids_to_check: For PP, on rank > 0, check the rids from the previous rank has consensus with the current rank.
"""
if len(self.disagg_prefill_inflight_queue) == 0:
return []
done_reqs = []
polls = poll_and_all_reduce_attn_cp_tp_group(
[req.disagg_kv_sender for req in self.disagg_prefill_inflight_queue],
self.attn_cp_cpu_group,
self.attn_tp_cpu_group,
)
undone_reqs: List[Req] = []
# Check .poll() for the reqs in disagg_prefill_inflight_queue. If Success, respond to the client and remove it from the queue
for req, poll in zip(self.disagg_prefill_inflight_queue, polls):
if rids_to_check is not None:
if req.rid not in rids_to_check:
undone_reqs.append(req)
continue
# In PP mode, the previous rank may have reached a terminal
# state (Success/Failed) while this rank's local poll is still
# in a transient state due to clock skew or propagation delay.
# Treat non-terminal states as undone instead of crashing.
if poll not in (
KVPoll.Success,
KVPoll.Failed,
):
logger.warning_once(
f"PP rank {self.ps.pp_rank}: unexpected poll state {poll} for rid {req.rid} "
f"from consensus; treating as undone",
)
undone_reqs.append(req)
continue
if req.pending_bootstrap and poll != KVPoll.Failed:
# prefill finished before bootstrap
if poll == KVPoll.WaitingForInput:
assert self.disagg_prefill_bootstrap_queue.finalize_bootstrap(req)
self.send_kv_chunk(req, last_chunk=True)
undone_reqs.append(req)
elif poll in [KVPoll.WaitingForInput, KVPoll.Transferring]:
undone_reqs.append(req)
elif poll == KVPoll.Success: # transfer done
release_kv_cache(req, self.tree_cache) # unlock the tree
req.finished_reason = FINISH_LENGTH(length=0)
# FIXME: clean up req's data in transfer engine
if hasattr(req.disagg_kv_sender, "clear"):
req.disagg_kv_sender.clear()
done_reqs.append(req)
req.time_stats.set_prefill_kv_transfer_finish_time()
elif poll == KVPoll.Failed:
error_message = f"Prefill transfer failed for request rank={self.ps.tp_rank} {req.rid=} {req.bootstrap_room=}"
is_propagated = False
try:
req.disagg_kv_sender.failure_exception()
except Exception as e:
error_message += f" with exception {e}"
is_propagated = getattr(e, "is_from_another_rank", False)
# Mute error message for propagated exceptions to avoid duplicate logging
if is_propagated:
logger.debug(error_message)
else:
logger.warning(error_message)
req.time_stats.trace_ctx.abort(abort_info={"reason": error_message})
release_kv_cache(req, self.tree_cache) # unlock the tree
prepare_abort(
req, error_message, status_code=HTTPStatus.INTERNAL_SERVER_ERROR
)
done_reqs.append(req)
if self.metrics_reporter.enable_metrics:
if req.pending_bootstrap:
self.metrics_collector.increment_bootstrap_failed_reqs()
else:
self.metrics_collector.increment_transfer_failed_reqs()
else:
logger.warning_once(
f"Unexpected polling state {poll} for rid {req.rid} in inflight queue; "
f"treating as undone",
)
undone_reqs.append(req)
for req in done_reqs:
req.time_stats.set_completion_time()
for req in done_reqs:
if isinstance(req.finished_reason, FINISH_ABORT):
continue
if req.bootstrap_host == FAKE_BOOTSTRAP_HOST:
continue
kv_mgr = getattr(req.disagg_kv_sender, "kv_mgr", None)
if kv_mgr and getattr(kv_mgr, "is_dummy_cp_rank", False):
continue
metrics = req.time_stats.compute_and_observe_kv_transfer_metrics(
req.disagg_kv_sender.get_transfer_metric()
)
if metrics:
# Update last-value for REST API
if "latency_ms" in metrics:
self.metrics_reporter.kv_transfer_latency_ms = metrics["latency_ms"]
if "speed_gb_s" in metrics:
self.metrics_reporter.kv_transfer_speed_gb_s = metrics["speed_gb_s"]
# Stream requests which have finished transfer
self.output_streamer.stream_output(
done_reqs,
any(req.return_logprob for req in done_reqs),
None,
)
for req in done_reqs:
req: Req
maybe_release_metadata_buffer(
req, self.req_to_metadata_buffer_idx_allocator
)
self.disagg_prefill_inflight_queue = undone_reqs
return done_reqs
def get_transferred_rids(self: Scheduler) -> List[str]:
"""
Used by PP, get the transferred rids but **do not pop**
"""
polls = poll_and_all_reduce_attn_cp_tp_group(
[req.disagg_kv_sender for req in self.disagg_prefill_inflight_queue],
self.attn_cp_cpu_group,
self.attn_tp_cpu_group,
)
transferred_rids: List[str] = []
for req, poll in zip(self.disagg_prefill_inflight_queue, polls):
if poll == KVPoll.Success or poll == KVPoll.Failed:
transferred_rids.append(req.rid)
return transferred_rids
def handle_bootstrap_failure(self: Scheduler, req: Req) -> None:
error_message = (
f"Prefill bootstrap failed for request rank={self.ps.tp_rank} "
f"{req.rid=} {req.bootstrap_room=}"
)
is_propagated = False
try:
req.disagg_kv_sender.failure_exception()
except Exception as e:
error_message += f" with exception {e}"
is_propagated = getattr(e, "is_from_another_rank", False)
# Mute error message for propagated exceptions to avoid duplicate logging
if is_propagated:
logger.debug(error_message)
else:
logger.warning(error_message)
req.time_stats.trace_ctx.abort(abort_info={"reason": error_message})
if req.req_pool_idx is not None or self.tree_cache.supports_mamba():
release_kv_cache(req, self.tree_cache)
maybe_release_metadata_buffer(req, self.req_to_metadata_buffer_idx_allocator)
req.pending_bootstrap = False
prepare_abort(req, error_message, status_code=HTTPStatus.INTERNAL_SERVER_ERROR)
self.output_streamer.stream_output([req], req.return_logprob)
if self.metrics_reporter.enable_metrics:
self.metrics_collector.increment_bootstrap_failed_reqs()
if self.enable_hicache_storage:
self.tree_cache.release_aborted_request(req.rid)
def handle_pending_bootstrap(self: Scheduler, req: Req, poll: KVPoll) -> bool:
"""Return True when bootstrap is finalized and KV transfer can proceed."""
if poll == KVPoll.Failed:
self.handle_bootstrap_failure(req)
return False
elif poll == KVPoll.Bootstrapping:
return False
elif poll == KVPoll.WaitingForInput:
if should_force_retry(req): # test hook
return False
# Metadata buffer was allocated in pop_bootstrapped before
# the request entered the waiting queue, so finalize should not fail.
assert self.disagg_prefill_bootstrap_queue.finalize_bootstrap(req)
return True
else:
raise RuntimeError(
f"Unexpected poll state {poll} for req {req.rid} in handle_pending_bootstrap"
)
def check_bootstrap(self: Scheduler, req: Req) -> bool:
"""Check bootstrap status for an optimistic prefilled request.
Returns True if bootstrap is finished."""
if not req.pending_bootstrap:
return True
polls = poll_and_all_reduce_attn_cp_tp_group(
[req.disagg_kv_sender],
self.attn_cp_cpu_group,
self.attn_tp_cpu_group,
)
return self.handle_pending_bootstrap(req, polls[0])
def process_prefill_chunk(
self: Scheduler,
last_batch: Optional[ScheduleBatch],
running_batch: ScheduleBatch,
) -> None:
chunked_req_to_exclude = set()
if (req := self.chunked_req) is not None:
chunked_req_to_exclude.add(req)
maybe_cache_unfinished_req(req, self.tree_cache, chunked=True)
if not self.check_bootstrap(req):
if is_aborted(req):
# bootstrap failed
self.chunked_req = None
elif self.has_bootstrapped_waiting_req():
# optimistic request yields to waiting requests
self.chunked_req = None
if not self.enable_overlap:
self.optimistic_release_and_requeue(req)
# else: still bootstrapping, keep computing without sending
elif self.enable_overlap:
# Delay KV transfer to process_batch_result_disagg_prefill when overlap is enabled to ensure results are resolved
req.tmp_end_idx = min(
req.extend_range.end,
len(req.origin_input_ids),
)
else:
self.send_kv_chunk(req)
if self.chunked_req is not None:
running_batch.batch_is_full = False
if last_batch and last_batch.forward_mode.is_extend():
if last_batch.chunked_req:
# In the context pipeline parallelism, after the last chunk, the current microbatch still track outdated chunked_req.
# We need to discard it.
chunked_req_to_exclude.add(last_batch.chunked_req)
last_bs = last_batch.batch_size()
last_batch.filter_batch(chunked_req_to_exclude=list(chunked_req_to_exclude))
if last_batch.batch_size() < last_bs:
running_batch.batch_is_full = False
def maybe_send_cached_prefix_chunk(self: Scheduler, req: Req) -> None:
# Only bootstrap-finalized requests; staging excluded.
if (
not envs.SGLANG_DISAGG_PREFILL_EARLY_SEND_CACHED_PREFIX.get()
or self.enable_staging
or req.pending_bootstrap
):
return
# Device-resident prefix only; page-aligned so start_send_idx stays exact.
cached_end = len(req.prefix_indices) - req.host_hit_length
if cached_end <= req.start_send_idx:
return
assert cached_end % self.token_to_kv_pool_allocator.page_size == 0
self.send_kv_chunk(req, last_chunk=False, end_idx=cached_end)
def send_kv_chunk(
self: Scheduler,
req: Req,
last_chunk: bool = False,
end_idx: Optional[int] = None,
) -> None:
"""
Send a prefilled chunk to the decode server
"""
page_size = self.token_to_kv_pool_allocator.page_size
start_idx = req.start_send_idx
transfer_input_len = len(req.origin_input_ids)
end_idx = (
end_idx
if end_idx is not None
else min(req.extend_range.end, transfer_input_len)
)
if not last_chunk:
# if not the last chunk and the last page is partial, delay the last partial page to the next send
end_idx = end_idx - end_idx % page_size
if end_idx < start_idx:
logger.debug(
"send_kv_chunk skip: rid=%s start_send_idx=%s end_idx=%s",
req.rid,
start_idx,
end_idx,
)
return
kv_indices = (
self.req_to_token_pool.req_to_token[req.req_pool_idx, start_idx:end_idx]
.cpu()
.numpy()
)
state_indices: Optional[List] = None
if last_chunk:
self.disagg_metadata_buffers.set_buf(req)
# Most state payloads read token-pool rows and should match the KV
# range actually materialized on prefill. C128 state is request
# scoped, so its transfer index must use the logical input length
# that decode used to register the destination row.
seq_len = min(req.extend_range.end, transfer_input_len)
c128_seq_len = transfer_input_len
def _mamba_payload():
return [
self.req_to_token_pool.req_index_to_mamba_index_mapping[
req.req_pool_idx
]
.cpu()
.numpy()
]
def _swa_payload():
window_size = self.sliding_window_size
window_start = max(0, seq_len - window_size)
window_start = (window_start // page_size) * page_size
window_kv_indices_full = self.req_to_token_pool.req_to_token[
req.req_pool_idx, window_start:seq_len
]
window_kv_indices_swa = (
self.token_to_kv_pool_allocator.translate_loc_from_full_to_swa(
window_kv_indices_full
)
)
return kv_to_page_indices(
window_kv_indices_swa.cpu().numpy(), page_size
)
def _dsa_payload():
kv_indices_full = self.req_to_token_pool.req_to_token[
req.req_pool_idx, :seq_len
]
return kv_to_page_indices(kv_indices_full.cpu().numpy(), page_size)
def _swa_ring_payload():
# Unified_kv SWA ring rows (req_pool_idx*ring_stride + pos%ring_stride)
# for the last `window` positions, in ascending position order so
# decode (its own req_pool_idx) matches positionally.
_pool = self.token_to_kv_pool_allocator.get_kvcache()
ring_stride = _pool.unified_swa_ring_size
window_size = _pool.unified_swa_window
window_start = max(0, seq_len - window_size)
positions = np.arange(window_start, seq_len, dtype=np.int64)
state_slot = int(req.req_pool_idx)
ring_rows = state_slot * ring_stride + (positions % ring_stride)
return ring_rows.astype(np.int32)
def _c128_state_payload():
online = is_dsv4_c128_online_enabled()
ring_size = (
1
if online
else self.token_to_kv_pool_allocator.get_kvcache().get_ring_size(
128
)
)
return get_dsv4_c128_state_indices(
int(req.req_pool_idx),
c128_seq_len,
online=online,
ring_size=ring_size,
)
state_types = (
self.disagg_prefill_bootstrap_queue.kv_manager.kv_args.state_types
)
state_indices = []
for st in state_types:
if st == StateType.MAMBA:
state_indices.append(_mamba_payload())
elif st == StateType.SWA:
state_indices.append(_swa_payload())
elif st == StateType.DSA:
state_indices.append(_dsa_payload())
elif st == StateType.MINIMAX_INDEX_K:
# Index rows live at the same loc as main KV on the same
# page_size, so reuse the full-seq page-ids.
state_indices.append(_dsa_payload())
elif st == StateType.SWA_RING:
state_indices.append(_swa_ring_payload())
elif st == StateType.C128_STATE:
state_indices.append(_c128_state_payload())
else:
state_indices.append(None)
page_indices = kv_to_page_indices(kv_indices, page_size)
if not req.disagg_kv_sender.should_send_kv_chunk(len(page_indices), last_chunk):
return
req.disagg_kv_sender.send(page_indices, state_indices)
req.start_send_idx = end_idx
def optimistic_release_and_requeue(self: Scheduler, req: Req) -> None:
"""Release KV cache and requeue an optimistic prefill request."""
max_attempts = self.server_args.optimistic_prefill_attempts
maybe_cache_unfinished_req(req, self.tree_cache)
release_kv_cache(req, self.tree_cache)
req.reset_for_retract()
req.output_ids = array("q")
req.start_send_idx = 0
req.tmp_end_idx = -1
req.hidden_states_tensor = None
req.pending_bootstrap = True
req.time_stats.reset_prefill_retry_time()
if req.prefill_attempt_count >= max_attempts:
logger.info(
f"Req {req.rid} exhausted optimistic prefill attempts "
"falling back to bootstrap queue"
)
# Reset it so the next real bootstrap done can be recorded.
req.time_stats.bootstrap_done_time = 0.0
self.disagg_prefill_bootstrap_queue.queue.append(req)
else:
req.prefill_attempt_count += 1
logger.info(
f"Req {req.rid} optimistic prefill yielded "
f"({req.prefill_attempt_count}/{max_attempts} attempts used)"
)
if self.metrics_reporter.enable_metrics:
self.metrics_collector.increment_prefill_retries(1)
req.time_stats.set_wait_queue_entry_time()
self.waiting_queue.insert(0, req)