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
1177 lines
48 KiB
Python
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)
|