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

1610 lines
66 KiB
Python

from __future__ import annotations
import logging
import math
import time
from array import array
from collections import defaultdict, deque
from dataclasses import dataclass
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
import numpy as np
import torch
import torch.distributed
from tqdm import tqdm
from sglang.srt.disaggregation.base.conn import KVPoll
from sglang.srt.disaggregation.utils import poll_and_all_reduce_attn_cp_tp_group
from sglang.srt.distributed.parallel_state import P2PWork
from sglang.srt.environ import envs
from sglang.srt.layers.dp_attention import (
get_attention_dp_rank,
get_attention_dp_size,
is_dp_attention_enabled,
set_is_extend_in_batch,
)
from sglang.srt.managers.overlap_utils import RelayPayload
from sglang.srt.managers.schedule_batch import Req, ScheduleBatch
from sglang.srt.managers.utils import (
GenerationBatchResult,
get_logprob_dict_from_result,
get_logprob_from_pp_outputs,
)
from sglang.srt.model_executor.forward_batch_info import (
ForwardBatch,
ForwardMode,
PPProxyTensors,
)
from sglang.srt.observability.req_time_stats import set_time_batch
from sglang.srt.sampling.sampling_params import SamplingParams
from sglang.srt.utils import DynamicGradMode, broadcast_pyobj, point_to_point_pyobj
from sglang.srt.utils.common import get_device_module, is_xpu
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from sglang.srt.managers.scheduler import Scheduler
def _pp_can_skip_output_comm(batch: ScheduleBatch) -> bool:
"""Check if output send/recv can be skipped for this batch."""
return (
envs.SGLANG_PP_SKIP_PURE_CHUNKED_OUTPUT_COMM.get()
and batch is not None
and batch.forward_mode == ForwardMode.EXTEND
and len(batch.reqs) == 1
and not batch.contains_last_prefill_chunk
and not batch.return_logprob
)
@dataclass
class PPBatchMetadata:
can_run_cuda_graph: bool
class SchedulerPPMixin:
@DynamicGradMode()
def event_loop_pp(self: Scheduler):
"""
A scheduler loop for pipeline parallelism.
Notes:
1. Each stage runs in the same order and is notified by the previous stage.
2. We use async send but sync recv to avoid desynchronization while minimizing the communication overhead.
3. We can use async batch depth to buffer the outputs in the last stage for to allow overlapping the GPU computation and CPU processing and avoid last PP rank staggler.
Unified Schedule:
====================================================================
Stage P
recv ith req from previous stage
recv ith proxy from previous stage
run ith batch
recv prev (i+1)% mb_size th outputs
process batch result of prev (i+1)% mb_size th batch (can be run in parallel with the curr batch GPU computation)
send ith req to next stage
send ith proxy to next stage
send current stage's outputs to next stage(can be stashed and delayed to send later)
the above order can be optimized and reordered to minimize communication-related CPU stall and overhead bubbles.
====================================================================
"""
self.init_pp_loop_state()
while True:
server_is_idle = True
for mb_id in range(self.pp_loop_size):
self.running_batch = self.running_mbs[mb_id]
self.last_batch = self.last_mbs[mb_id]
next_first_rank_mb_id = (mb_id + self.ps.pp_size) % self.pp_loop_size
next_mb_id = (mb_id + 1) % self.pp_loop_size
with torch.profiler.record_function("recv_requests"):
recv_reqs = self.request_receiver.recv_requests()
self.process_input_requests(recv_reqs)
if not self.pp_group.is_last_rank:
self._pp_commit_comm_work(self.send_req_work)
with torch.profiler.record_function("send_reqs_to_next_stage"):
self.send_req_work = self._pp_send_pyobj_to_next_stage(
recv_reqs,
async_send=True,
)
with torch.profiler.record_function("get_next_batch_to_run"):
plan = self.get_next_batch_to_run(
running_batch=self.running_batch, last_batch=self.last_batch
)
self.running_batch = plan.running_batch
self.mbs[mb_id] = plan.batch_to_run
self.running_mbs[mb_id] = self.running_batch
cur_batch: Optional[ScheduleBatch] = self.mbs[mb_id]
self.cur_batch_for_debug = cur_batch
if cur_batch:
server_is_idle = False
pp_proxy_tensors = self._pp_recv_proxy_tensors()
next_pp_outputs = None
next_batch_result = None
d2h_event = None
if self.server_args.pp_async_batch_depth > 0:
next_pp_outputs, next_batch_result, d2h_event = (
self._pp_commit_send_output_work_and_preprocess_output_tensors(
next_first_rank_mb_id,
next_mb_id,
)
)
self._pp_commit_comm_work(self.send_proxy_work)
if cur_batch:
result, self.launch_event = self._pp_launch_batch(
mb_id,
cur_batch,
pp_proxy_tensors,
self.mb_metadata,
self.last_rank_comm_queue,
)
if self.server_args.pp_async_batch_depth == 0:
next_pp_outputs, next_batch_result, d2h_event = (
self._pp_commit_send_output_work_and_preprocess_output_tensors(
next_first_rank_mb_id,
next_mb_id,
)
)
if self.mbs[next_mb_id] is not None:
d2h_event.synchronize()
with torch.profiler.record_function("process_batch_result"):
self._pp_process_batch_result(
self.mbs[next_mb_id],
next_batch_result,
)
self.last_mbs[next_mb_id] = self.mbs[next_mb_id]
if not self.pp_group.is_last_rank:
if cur_batch:
self.device_module.current_stream().wait_event(
self.launch_event
)
with torch.profiler.record_function(
"send_proxy_dict_to_next_stage"
):
self.send_proxy_work = self._pp_send_dict_to_next_stage(
result.pp_hidden_states_proxy_tensors.tensors,
async_send=True,
msg_type="proxy",
)
self.pp_outputs = next_pp_outputs
# When the server is idle, self-check and re-init some states
if server_is_idle:
self.on_idle()
@DynamicGradMode()
def event_loop_pp_disagg_prefill(self: Scheduler):
"""
This is the prefill server event loop for pipeline parallelism.
Notes:
1. Following the same rules as the event_loop_pp.
2. Adds extra steps for KV transfer process: bootstrap + release.
Prefill Server Schedule:
====================================================================
Stage P
recv ith req from previous stage
recv ith bootstrap req from previous stage
recv ith transferred req from previous stage
recv ith proxy from previous stage
run ith batch
recv prev (i+1) % mb_size th consensus bootstrapped req from previous stage
local consensus on bootstrapped req
recv prev (i+1) % mb_size th release req from previous stage
local consensus on release req
recv prev (i+1) % mb_size th outputs
process batch result of prev (i+1)% mb_size th batch (can be run in parallel with the curr batch GPU computation)
send ith req to next stage
send ith bootstrap req to next stage
send ith transferred req to next stage
send ith proxy to next stage
send current stage's outputs to next stage (can be stashed and delayed to send later)
the above order can be optimized and reordered to minimize communication-related CPU stall and overhead bubbles.
====================================================================
There are two additional elements compared to the regular schedule:
Bootstrap Requests + Release Requests:
- Both can have local failure and need to be consensus on. PP needs to guarantee eventual consistency of local failure and flush malfunc requests out as soft error.
"""
self.init_pp_loop_state()
# PD additional state initialization
bmbs = [None] * self.pp_loop_size
tmbs = [None] * self.pp_loop_size
consensus_bootstrapped_rids: Optional[List[str]] = None
transferred_rids: List[str] = []
release_rids: Optional[List[str]] = None
send_bootstrapped_work = []
send_transfer_work = []
send_consensus_bootstrapped_work = []
send_release_work = []
while True:
server_is_idle = True
for mb_id in range(self.pp_loop_size):
self.running_batch = self.running_mbs[mb_id]
self.last_batch = self.last_mbs[mb_id]
next_first_rank_mb_id = (mb_id + self.ps.pp_size) % self.pp_loop_size
next_mb_id = (mb_id + 1) % self.pp_loop_size
next_pp_outputs = None
next_release_rids = None
next_consensus_bootstrapped_rids = None
d2h_event = None
next_batch_result = None
recv_reqs = self.request_receiver.recv_requests()
self.process_input_requests(recv_reqs)
if not self.pp_group.is_last_rank:
self._pp_commit_comm_work(self.send_req_work)
bootstrapped_rids = self._pp_pd_get_bootstrapped_ids()
bmbs[mb_id] = bootstrapped_rids
self._pp_commit_comm_work(send_bootstrapped_work)
transferred_rids = self._pp_pd_get_prefill_transferred_ids()
self._pp_commit_comm_work(send_transfer_work)
tmbs[mb_id] = transferred_rids
self.process_prefill_chunk(
last_batch=self.last_batch, running_batch=self.running_batch
)
prefill_plan = self.get_new_batch_prefill(self.running_batch)
batch = prefill_plan.batch_to_run
self.running_batch = prefill_plan.running_batch
batch = self.dp_attn_adapter.maybe_prepare_mlp_sync_batch(batch)
self.mbs[mb_id] = batch
self.running_mbs[mb_id] = self.running_batch
cur_batch: Optional[ScheduleBatch] = self.mbs[mb_id]
self.cur_batch_for_debug = cur_batch
if cur_batch:
server_is_idle = False
pp_proxy_tensors = self._pp_recv_proxy_tensors()
if self.server_args.pp_async_batch_depth > 0:
next_pp_outputs, next_batch_result, d2h_event = (
self._pp_commit_send_output_work_and_preprocess_output_tensors(
next_first_rank_mb_id,
next_mb_id,
)
)
self._pp_commit_comm_work(self.send_proxy_work)
if cur_batch:
result, self.launch_event = self._pp_launch_batch(
mb_id,
cur_batch,
pp_proxy_tensors,
self.mb_metadata,
self.last_rank_comm_queue,
)
if self.server_args.pp_async_batch_depth == 0:
next_pp_outputs, next_batch_result, d2h_event = (
self._pp_commit_send_output_work_and_preprocess_output_tensors(
next_first_rank_mb_id,
next_mb_id,
)
)
send_consensus_bootstrapped_work, consensus_bootstrapped_rids = (
self._pp_pd_send_consensus_bootstrapped_ids(
bmbs,
next_first_rank_mb_id,
consensus_bootstrapped_rids,
bootstrapped_rids,
)
)
send_release_work, release_rids = (
self._pp_pd_send_consensus_release_ids(
tmbs, next_first_rank_mb_id, release_rids, transferred_rids
)
)
if bmbs[next_mb_id] is not None:
next_consensus_bootstrapped_rids = (
self._pp_recv_pyobj_from_prev_stage()
)
next_consensus_bootstrapped_rids = self.process_bootstrapped_queue(
next_consensus_bootstrapped_rids
)
self._pp_commit_comm_work(send_consensus_bootstrapped_work)
if tmbs[next_mb_id] is not None:
next_release_rids = self._pp_recv_pyobj_from_prev_stage()
self._pp_commit_comm_work(send_release_work)
# post-process the coming microbatch
if self.mbs[next_mb_id] is not None:
d2h_event.synchronize()
self._pp_process_batch_result(
self.mbs[next_mb_id],
next_batch_result,
)
self.last_mbs[next_mb_id] = self.mbs[next_mb_id]
if tmbs[next_mb_id] is not None:
self.process_disagg_prefill_inflight_queue(next_release_rids)
if not self.pp_group.is_last_rank:
self.send_req_work = self._pp_send_pyobj_to_next_stage(
recv_reqs, async_send=True
)
send_bootstrapped_work = self._pp_send_pyobj_to_next_stage(
bootstrapped_rids, async_send=True
)
send_transfer_work = self._pp_send_pyobj_to_next_stage(
transferred_rids, async_send=True
)
if cur_batch:
self.device_module.current_stream().wait_event(
self.launch_event
)
self.send_proxy_work = self._pp_send_dict_to_next_stage(
result.pp_hidden_states_proxy_tensors.tensors,
async_send=True,
msg_type="proxy",
)
self.pp_outputs = next_pp_outputs
release_rids = next_release_rids
consensus_bootstrapped_rids = next_consensus_bootstrapped_rids
self.running_batch.batch_is_full = False
# When the server is idle, self-check and re-init some states
if server_is_idle and len(self.disagg_prefill_inflight_queue) == 0:
self.on_idle()
@DynamicGradMode()
def event_loop_pp_disagg_decode(self: Scheduler):
self.init_pp_loop_state()
# PD additional state initialization
rmbs = [None] * self.pp_loop_size
pmbs = [None] * self.pp_loop_size
tmbs = [None] * self.pp_loop_size
consensus_retract_rids: Optional[List[str]] = None
consensus_prealloc_rids: Optional[List[str]] = None
release_rids: Optional[List[str]] = None # consensus transferred rids
send_retract_work = []
send_prealloc_work = []
send_transfer_work = []
send_consensus_retract_work = []
send_consensus_prealloc_work = []
send_release_work = []
while True:
server_is_idle = True
for mb_id in range(self.pp_loop_size):
self.running_batch = self.running_mbs[mb_id]
self.last_batch = self.last_mbs[mb_id]
next_first_rank_mb_id = (mb_id + self.ps.pp_size) % self.pp_loop_size
next_mb_id = (mb_id + 1) % self.pp_loop_size
next_pp_outputs = None
next_consensus_retract_rids = None
next_consensus_prealloc_rids = None
next_release_rids = None
d2h_event = None
next_batch_result = None
recv_reqs = self.request_receiver.recv_requests()
self.process_input_requests(recv_reqs)
if not self.pp_group.is_last_rank:
self._pp_commit_comm_work(self.send_req_work)
# reaching consensus through PP ranks
retract_rids = self._pp_pd_get_retract_ids(mb_id)
rmbs[mb_id] = retract_rids
self._pp_commit_comm_work(send_retract_work)
prealloc_rids = self._pp_pd_get_prealloc_ids()
pmbs[mb_id] = prealloc_rids
self._pp_commit_comm_work(send_prealloc_work)
transferred_rids = self._pp_pd_get_decode_transferred_ids()
tmbs[mb_id] = transferred_rids
self._pp_commit_comm_work(send_transfer_work)
# get batch to run and proxy tensors if needed
plan = self.get_next_disagg_decode_batch_to_run(
running_batch=self.running_batch
)
self.running_batch = plan.running_batch
batch = plan.batch_to_run
self.mbs[mb_id] = batch
self.running_mbs[mb_id] = self.running_batch
cur_batch: Optional[ScheduleBatch] = self.mbs[mb_id]
self.cur_batch_for_debug = cur_batch
if cur_batch:
server_is_idle = False
pp_proxy_tensors = None
if not cur_batch.forward_mode.is_prebuilt():
pp_proxy_tensors = self._pp_recv_proxy_tensors()
# early send output if possible
if self.server_args.pp_async_batch_depth > 0:
next_pp_outputs, next_batch_result, d2h_event = (
self._pp_commit_send_output_work_and_preprocess_output_tensors(
next_first_rank_mb_id,
next_mb_id,
)
)
self._pp_commit_comm_work(self.send_proxy_work)
if cur_batch:
result, self.launch_event = self._pp_launch_batch(
mb_id,
cur_batch,
pp_proxy_tensors,
self.mb_metadata,
self.last_rank_comm_queue,
)
if self.server_args.pp_async_batch_depth == 0:
next_pp_outputs, next_batch_result, d2h_event = (
self._pp_commit_send_output_work_and_preprocess_output_tensors(
next_first_rank_mb_id,
next_mb_id,
)
)
# reach consensus on last rank and send to PP=0
# otherwise, just pass along previous consensus
send_consensus_retract_work, consensus_retract_rids = (
self._pp_pd_send_consensus_bootstrapped_ids(
rmbs,
next_first_rank_mb_id,
consensus_retract_rids,
retract_rids,
)
)
send_consensus_prealloc_work, consensus_prealloc_rids = (
self._pp_pd_send_consensus_bootstrapped_ids(
pmbs,
next_first_rank_mb_id,
consensus_prealloc_rids,
prealloc_rids,
)
)
send_release_work, release_rids = (
self._pp_pd_send_consensus_release_ids(
tmbs, next_first_rank_mb_id, release_rids, transferred_rids
)
)
if self.server_args.disaggregation_decode_enable_offload_kvcache:
self.decode_offload_manager.check_offload_progress()
if rmbs[next_mb_id] is not None:
next_consensus_retract_rids = self._pp_recv_pyobj_from_prev_stage()
next_consensus_retract_rids = self.process_retract_queue(
next_consensus_retract_rids
)
self._pp_commit_comm_work(send_consensus_retract_work)
if pmbs[next_mb_id] is not None:
next_consensus_prealloc_rids = self._pp_recv_pyobj_from_prev_stage()
next_consensus_prealloc_rids = self.process_prealloc_queue(
next_consensus_prealloc_rids
)
self._pp_commit_comm_work(send_consensus_prealloc_work)
if tmbs[next_mb_id] is not None:
next_release_rids = self._pp_recv_pyobj_from_prev_stage()
next_release_rids = self.process_decode_transfer_queue(
next_release_rids
)
self._pp_commit_comm_work(send_release_work)
# post-process the coming microbatch
if self.mbs[next_mb_id] is not None:
if not self.mbs[next_mb_id].forward_mode.is_prebuilt():
d2h_event.synchronize()
self._pp_process_batch_result(
self.mbs[next_mb_id],
next_batch_result,
)
self.last_mbs[next_mb_id] = self.mbs[next_mb_id]
if not self.pp_group.is_last_rank:
self.send_req_work = self._pp_send_pyobj_to_next_stage(
recv_reqs, async_send=True
)
send_retract_work = self._pp_send_pyobj_to_next_stage(
retract_rids, async_send=True
)
send_prealloc_work = self._pp_send_pyobj_to_next_stage(
prealloc_rids, async_send=True
)
send_transfer_work = self._pp_send_pyobj_to_next_stage(
transferred_rids, async_send=True
)
if cur_batch and not cur_batch.forward_mode.is_prebuilt():
self.device_module.current_stream().wait_event(
self.launch_event
)
self.send_proxy_work = self._pp_send_dict_to_next_stage(
result.pp_hidden_states_proxy_tensors.tensors,
async_send=True,
msg_type="proxy",
)
self.pp_outputs = next_pp_outputs
release_rids = next_release_rids
consensus_retract_rids = next_consensus_retract_rids
consensus_prealloc_rids = next_consensus_prealloc_rids
self.running_batch.batch_is_full = False
# When the server is idle, self-check and re-init some states
queue_size = (
len(self.waiting_queue)
+ len(self.disagg_decode_transfer_queue.queue)
+ len(self.disagg_decode_prealloc_queue.queue)
)
if self.server_args.disaggregation_decode_enable_offload_kvcache:
queue_size += len(self.decode_offload_manager.ongoing_offload)
if server_is_idle and queue_size == 0:
self.on_idle()
def init_pp_loop_state(self: Scheduler):
self.pp_loop_size: int = self.ps.pp_size + self.server_args.pp_async_batch_depth
# In CP mode, attention weights are duplicated, eliminating the need for the attention TP all-gather operation.
self.require_attn_tp_allgather = (
not self.server_args.enable_dsa_prefill_context_parallel
)
self.mbs = [None] * self.pp_loop_size
self.last_mbs = [None] * self.pp_loop_size
self.running_mbs = [
ScheduleBatch(reqs=[], batch_is_full=False)
for _ in range(self.pp_loop_size)
]
self.mb_metadata: List[Optional[PPBatchMetadata]] = [None] * self.pp_loop_size
self.pp_outputs: Optional[PPProxyTensors] = None
self.last_rank_comm_queue: deque[Tuple[torch.Event, PPProxyTensors]] = deque()
self.send_req_work = []
self.send_proxy_work = []
self.send_output_work = []
self.launch_event = None
self._pp_tensor_dict_inbox: Dict[str, deque[Dict[str, torch.Tensor]]] = (
defaultdict(deque)
)
def profile_and_init_predictor(self: Scheduler):
"""
Profile prefill latency for dynamic chunk sizing.
Only runs on PP0 (first rank), then broadcasts data to all ranks.
All ranks fit coefficients using the same data.
"""
seq_lens: List[int] = []
latencies: List[float] = []
if self.pp_group.is_first_rank:
model_runner = self.tp_worker.model_runner
model_config = model_runner.model_config
input_ids_list: List[array[int]] = []
for i in range(128):
chunk_size = int(
self.chunked_prefill_size * 1.25
- i * (self.chunked_prefill_size * 1.25 // 128)
)
if chunk_size <= 0:
break
input_ids = array(
"q",
np.random.randint(
0, 10000, size=chunk_size, dtype=np.int64
).tobytes(),
)
input_ids_list.append(input_ids)
sampling_params = SamplingParams(
temperature=0,
max_new_tokens=1,
)
# Create and profile requests
for i, input_ids in enumerate(
tqdm(
input_ids_list,
desc="Profiling prefill latency for dynamic chunking",
)
):
req = Req(
rid=str(i),
origin_input_text="",
origin_input_ids=input_ids,
sampling_params=sampling_params,
)
req.full_untruncated_fill_ids = req.origin_input_ids
req.logprob_start_len = -1
req.set_extend_range(
len(req.prefix_indices), len(req.full_untruncated_fill_ids)
)
# Prepare batch
batch = ScheduleBatch.init_new(
[req],
self.req_to_token_pool,
self.token_to_kv_pool_allocator,
self.tree_cache,
self.model_config,
False,
self.spec_algorithm,
)
current_seq_len = req.extend_range.end
if is_dp_attention_enabled():
# For profiling, we only have one request on PP0
# Set global_num_tokens to indicate this rank has tokens, others have 0
dp_size = get_attention_dp_size()
global_num_tokens = [0] * dp_size
dp_rank = get_attention_dp_rank()
global_num_tokens[dp_rank] = current_seq_len
batch.global_num_tokens = global_num_tokens
batch.global_num_tokens_for_logprob = global_num_tokens
hs = (
getattr(model_config, "hc_hidden_size", None)
or model_config.hidden_size
)
proxy_tensors = {
"hidden_states": torch.zeros(
(current_seq_len, hs),
dtype=model_config.dtype,
device=self.device,
),
"residual": torch.zeros(
(current_seq_len, model_config.hidden_size),
dtype=model_config.dtype,
device=self.device,
),
}
pp_proxy_topk_size = model_runner.get_pp_proxy_topk_size()
if pp_proxy_topk_size is not None:
proxy_tensors["topk_indices"] = torch.zeros(
(current_seq_len, pp_proxy_topk_size),
dtype=torch.int32,
device=self.device,
)
pp_proxy = PPProxyTensors(proxy_tensors)
# Measure latency with device synchronization for accurate timing
device_module = get_device_module()
# Synchronize before starting timing to ensure clean measurement
device_module.synchronize()
start = time.perf_counter()
batch.prepare_for_extend()
# Resolve deferred H2D: prepare_for_extend now leaves input_ids=None
if batch.input_ids is None and batch.prefill_input_ids_cpu is not None:
batch.input_ids = batch.prefill_input_ids_cpu.to(
self.device, non_blocking=True
)
batch.prefill_input_ids_cpu = None
forward_batch = ForwardBatch.init_new(batch, model_runner)
set_is_extend_in_batch(batch.forward_mode.is_extend())
_ = model_runner.forward(
forward_batch=forward_batch, pp_proxy_tensors=pp_proxy
)
# Synchronize after forward to ensure GPU operations complete
device_module.synchronize()
latency_seconds = time.perf_counter() - start
latency_ms = latency_seconds * 1e3 # Convert to milliseconds
seq_lens.append(len(input_ids))
latencies.append(latency_ms)
# Release KV cache
if req.req_pool_idx is not None:
kv_indices = self.req_to_token_pool.req_to_token[
req.req_pool_idx, : req.extend_range.end
]
self.token_to_kv_pool_allocator.free(kv_indices)
self.req_to_token_pool.free(req)
logger.info(
f"[PP Dynamic Chunk] [PP0] Profiled {len(seq_lens)} samples: "
f"seq_lens={seq_lens}, latencies_ms={latencies}"
)
if self.ps.attn_tp_size > 1:
data_to_sync_tp = [seq_lens, latencies]
data_to_sync_tp = broadcast_pyobj(
data_to_sync_tp,
self.attn_tp_group.rank,
self.attn_tp_cpu_group,
src=self.attn_tp_group.ranks[0],
)
seq_lens, latencies = data_to_sync_tp
if self.ps.attn_cp_size > 1:
data_to_sync_tp = [seq_lens, latencies]
data_to_sync_tp = broadcast_pyobj(
data_to_sync_tp,
self.attn_cp_group.rank,
self.attn_cp_cpu_group,
src=self.attn_cp_group.ranks[0],
)
# Broadcast data to all ranks
if torch.distributed.is_available() and torch.distributed.is_initialized():
data_to_sync = [seq_lens, latencies]
self.pp_group.broadcast_object_list(data_to_sync, src=0)
seq_lens, latencies = data_to_sync
# Quadratic model: f(l) = al^2 + bl + c
self.length_predictor = ChunkSizePredictor()
self.length_predictor.fit(seq_lens, latencies)
self.length_predictor.set_target_latency(self.chunked_prefill_size)
self.length_predictor.is_ready = True
logger.info(
f"[PP Dynamic Chunk] [PP{self.ps.pp_rank}] Predictor ready (quadratic). "
f"Target latency: {self.length_predictor.target_latency:.2f}ms"
)
def predict_next_chunk_size(self: Scheduler, history_len: int) -> Optional[int]:
"""
Predict next chunk size dynamically based on current history length.
Args:
history_len: Current sequence length
Returns:
Predicted chunk size, or None to use default chunked_prefill_size
"""
if (
not self.enable_dynamic_chunking
or self.length_predictor is None
or not self.length_predictor.is_ready
):
return None
max_chunk_size = self.max_prefill_tokens
predicted_size = self.length_predictor.predict_next_chunk_size(
history_len=history_len,
base_chunk_size=self.chunked_prefill_size,
page_size=self.page_size,
context_len=self.model_config.context_len,
max_chunk_size=max_chunk_size,
)
if predicted_size is not None:
logger.debug(
f"[PP Dynamic Chunk] [PP{self.ps.pp_rank}] Predicted chunk size: "
f"{predicted_size} (history_len={history_len})"
)
return predicted_size
def process_bootstrapped_queue(
self: Scheduler, bootstrapped_rids: Optional[List[str]]
):
# finished consensus bootstrapped reqs and prepare the waiting queue
if bootstrapped_rids is not None:
(
good_consensus_bootstrapped_rids,
bad_consensus_bootstrapped_rids,
) = bootstrapped_rids
good_reqs, failed_reqs = (
self.disagg_prefill_bootstrap_queue.pop_bootstrapped(
return_failed_reqs=True,
rids_to_check=good_consensus_bootstrapped_rids
+ bad_consensus_bootstrapped_rids,
)
)
self.waiting_queue.extend(good_reqs)
return [[req.rid for req in good_reqs], [req.rid for req in failed_reqs]]
return None
def _pp_pd_get_bootstrapped_ids(self: Scheduler):
# communicate pre-consensus bootstrapp reqs
if self.pp_group.is_first_rank:
# First rank, pop the bootstrap reqs from the bootstrap queue
good_bootstrapped_rids, bad_bootstrapped_rids = self.get_rids(
self.disagg_prefill_bootstrap_queue.queue,
True,
[KVPoll.WaitingForInput],
[KVPoll.Failed],
)
else:
# Other ranks, receive the bootstrap reqs info from the previous rank and ensure the consensus
prev_bootstrapped_rids = self._pp_recv_pyobj_from_prev_stage()
prev_good_bootstrapped_rids, prev_bad_bootstrapped_rids = (
prev_bootstrapped_rids
)
curr_good_bootstrapped_rids, curr_bad_bootstrapped_rids = self.get_rids(
self.disagg_prefill_bootstrap_queue.queue,
True,
[KVPoll.WaitingForInput],
[KVPoll.Failed],
)
good_bootstrapped_rids = list(
set(prev_good_bootstrapped_rids) & set(curr_good_bootstrapped_rids)
)
bad_bootstrapped_rids = list(
set(prev_bad_bootstrapped_rids) | set(curr_bad_bootstrapped_rids)
)
return [good_bootstrapped_rids, bad_bootstrapped_rids]
def _pp_pd_get_prefill_transferred_ids(self: Scheduler):
# get the current stage transfer success
if self.pp_group.is_first_rank:
transferred_rids = self.get_rids(
self.disagg_prefill_inflight_queue,
True,
[KVPoll.Success, KVPoll.Failed],
)
# if other ranks, do intersection with the previous rank's transferred rids
else:
# 2 (Release): Receive the transferred rids from the previous rank
# 1. recv previous stage's transferred reqs info
prev_transferred_rids = self._pp_recv_pyobj_from_prev_stage()
# 2. get the current stage's transferred reqs info
curr_transferred_rids = self.get_rids(
self.disagg_prefill_inflight_queue,
True,
[KVPoll.Success, KVPoll.Failed],
)
# 3. new consensus rids = intersection(previous consensus rids, transfer finished rids)
transferred_rids = list(
set(prev_transferred_rids) & set(curr_transferred_rids)
)
return transferred_rids
def _pp_pd_send_consensus_bootstrapped_ids(
self: Scheduler,
bmbs: List[List[str]],
next_first_rank_mb_id: int,
consensus_bootstrapped_rids: List[str],
bootstrapped_rids: List[str],
):
# 3 (Release): send the release rids from last stage to the first stage
send_consensus_bootstrapped_work = []
if self.pp_group.is_last_rank:
if bmbs[next_first_rank_mb_id] is not None:
consensus_bootstrapped_rids = bootstrapped_rids
send_consensus_bootstrapped_work = self._pp_send_pyobj_to_next_stage(
consensus_bootstrapped_rids, async_send=True
)
# 4 (Release): send the release rids from non last rank to the next rank
else:
if consensus_bootstrapped_rids is not None:
send_consensus_bootstrapped_work = self._pp_send_pyobj_to_next_stage(
consensus_bootstrapped_rids, async_send=True
)
return send_consensus_bootstrapped_work, consensus_bootstrapped_rids
def _pp_pd_send_consensus_release_ids(
self: Scheduler,
tmbs: List[List[str]],
next_first_rank_mb_id: int,
release_rids: List[str],
transferred_rids: List[str],
):
send_release_work = []
if self.pp_group.is_last_rank:
if tmbs[next_first_rank_mb_id] is not None:
release_rids = transferred_rids
send_release_work = self._pp_send_pyobj_to_next_stage(
release_rids, async_send=True
)
# 4 (Release): send the release rids from non last rank to the next rank
else:
if release_rids is not None:
send_release_work = self._pp_send_pyobj_to_next_stage(
release_rids, async_send=True
)
return send_release_work, release_rids
def _pp_commit_comm_work(self: Scheduler, work: List[P2PWork]) -> None:
for p2p_work in work:
p2p_work.work.wait()
work.clear()
def _pp_commit_send_output_work_and_preprocess_output_tensors(
self: Scheduler,
next_first_rank_mb_id: int,
next_mb_id: int,
) -> Tuple[
Optional[PPProxyTensors],
Optional[GenerationBatchResult],
Optional[torch.Event],
]:
self._pp_commit_comm_work(work=self.send_output_work)
(
next_pp_outputs,
next_batch_result,
d2h_event,
self.send_output_work,
) = self._pp_send_recv_and_preprocess_output_tensors(
next_first_rank_mb_id,
next_mb_id,
self.mbs,
self.mb_metadata,
self.last_rank_comm_queue,
self.pp_outputs,
)
return next_pp_outputs, next_batch_result, d2h_event
def _pp_send_pyobj_to_next_stage(self: Scheduler, data, async_send: bool = False):
p2p_work = []
if self.ps.attn_tp_rank == 0 and self.ps.attn_cp_rank == 0:
dp_offset = (
self.ps.attn_dp_rank * self.ps.attn_cp_size * self.ps.attn_tp_size
)
p2p_work = point_to_point_pyobj(
data,
self.ps.pp_rank * self.ps.tp_size + dp_offset,
self.world_group.cpu_group,
self.ps.pp_rank * self.ps.tp_size + dp_offset,
((self.ps.pp_rank + 1) % self.ps.pp_size) * self.ps.tp_size + dp_offset,
async_send=async_send,
)
return p2p_work
def _pp_recv_pyobj_from_prev_stage(self: Scheduler):
if self.ps.attn_tp_rank == 0 and self.ps.attn_cp_rank == 0:
dp_offset = (
self.ps.attn_dp_rank * self.ps.attn_cp_size * self.ps.attn_tp_size
)
data = point_to_point_pyobj(
[],
self.ps.pp_rank * self.ps.tp_size + dp_offset,
self.world_group.cpu_group,
((self.ps.pp_rank - 1) % self.ps.pp_size) * self.ps.tp_size + dp_offset,
self.ps.pp_rank * self.ps.tp_size + dp_offset,
)
else:
data = None
if self.ps.attn_tp_size > 1:
data = broadcast_pyobj(
data,
self.attn_tp_group.rank,
self.attn_tp_cpu_group,
src=self.attn_tp_group.ranks[0],
)
if self.ps.attn_cp_size > 1:
data = broadcast_pyobj(
data,
self.attn_cp_group.rank,
self.attn_cp_cpu_group,
src=self.attn_cp_group.ranks[0],
)
return data
def _pp_prepare_tensor_dict(
self: Scheduler, result: GenerationBatchResult, batch: ScheduleBatch
) -> Dict[str, torch.Tensor]:
tensor_dict = {
"next_token_ids": result.next_token_ids,
}
if batch.return_logprob:
logprob_dict = get_logprob_dict_from_result(result)
tensor_dict = {
**tensor_dict,
**logprob_dict,
}
return tensor_dict
def _pp_send_dict_to_next_stage(
self: Scheduler,
tensor_dict: Dict[str, torch.Tensor],
async_send: bool = True,
msg_type: str = "default",
):
# Warn once if using default untyped messages
if msg_type == "default":
logger.warning_once(
"PP send: using default untyped message. "
"Consider adding msg_type='proxy' or 'output' to avoid recv conflicts."
)
tensor_dict["__msg_type__"] = msg_type
p2p_work = []
p2p_work.extend(
self.pp_group.send_tensor_dict(
tensor_dict=tensor_dict,
all_gather_group=(
self.attn_tp_group if self.require_attn_tp_allgather else None
),
async_send=async_send,
)
)
return p2p_work
def _pp_recv_typed_dict(
self: Scheduler,
expected_kind: str = "default",
all_gather_group: Optional = None,
) -> Dict[str, torch.Tensor]:
"""Receive a typed tensor dict, demultiplexing by msg_type.
If a message of the wrong kind is received, it's stashed in the queue
and we continue receiving until we get the expected kind.
"""
if expected_kind in self._pp_tensor_dict_inbox:
inbox_queue = self._pp_tensor_dict_inbox[expected_kind]
if inbox_queue:
return inbox_queue.popleft()
while True:
tensor_dict = self.pp_group.recv_tensor_dict(
all_gather_group=all_gather_group
)
received_kind = tensor_dict.get("__msg_type__", "default")
if received_kind == expected_kind:
if received_kind == "default":
logger.warning_once(
f"PP recv: got default untyped message. Content keys: {tensor_dict.keys()}"
"Consider adding msg_type='proxy' or 'output' to avoid recv conflicts."
)
return tensor_dict
else:
logger.debug(
f"PP recv: expected {expected_kind}, got {received_kind}, stashing"
)
self._pp_tensor_dict_inbox[received_kind].append(tensor_dict)
def _pp_recv_proxy_tensors(self: Scheduler) -> Optional[PPProxyTensors]:
pp_proxy_tensors = None
if not self.pp_group.is_first_rank:
pp_proxy_tensors = PPProxyTensors(
self._pp_recv_typed_dict(
expected_kind="proxy",
all_gather_group=(
self.attn_tp_group if self.require_attn_tp_allgather else None
),
)
)
return pp_proxy_tensors
def _pp_recv_dict_from_prev_stage(
self: Scheduler,
) -> Dict[str, torch.Tensor]:
return self._pp_recv_typed_dict(
expected_kind="output",
all_gather_group=(
self.attn_tp_group if self.require_attn_tp_allgather else None
),
)
def _pp_make_skip_output_result(
self: Scheduler,
batch: ScheduleBatch,
mb_metadata: Optional[PPBatchMetadata],
):
bs = len(batch.reqs)
placeholder = torch.zeros(bs, dtype=torch.int64, device=self.device)
# next_pp_outputs = None so non-last ranks skip forwarding
# (pp_outputs is None gate). Placeholder carried in
# batch_result.next_token_ids for process_batch_result_prefill.
batch.output_ids = placeholder
batch_result = GenerationBatchResult(
logits_output=None,
pp_hidden_states_proxy_tensors=None,
next_token_ids=placeholder,
can_run_cuda_graph=(
mb_metadata.can_run_cuda_graph if mb_metadata else False
),
skipped_output_comm=True,
)
d2h_event = self.device_module.Event()
d2h_event.record(self.device_module.current_stream())
return None, batch_result, d2h_event
def _pp_prep_batch_result(
self: Scheduler,
batch: ScheduleBatch,
mb_metadata: PPBatchMetadata,
pp_outputs: PPProxyTensors,
):
from sglang.srt.managers.scheduler import GenerationBatchResult
logits_output = None
extend_input_len_per_req = None
extend_logprob_start_len_per_req = None
if batch.return_logprob:
(
logits_output,
extend_input_len_per_req,
extend_logprob_start_len_per_req,
) = get_logprob_from_pp_outputs(pp_outputs)
batch.input_ids = pp_outputs["next_token_ids"].to(torch.int64)
# PP rank 0 also relays into output_tokens_buf so the next iter's
# resolve_forward_inputs finds these tokens for the decode portion
# of mixed-chunk batches (which gather via mix_running_indices).
self.future_map.stash(
batch.req_pool_indices, RelayPayload(bonus_tokens=batch.input_ids)
)
output_result = GenerationBatchResult(
logits_output=logits_output,
pp_hidden_states_proxy_tensors=None,
next_token_ids=pp_outputs["next_token_ids"],
extend_input_len_per_req=extend_input_len_per_req,
extend_logprob_start_len_per_req=extend_logprob_start_len_per_req,
can_run_cuda_graph=mb_metadata.can_run_cuda_graph,
)
return output_result
def _pp_process_batch_result(
self: Scheduler, batch: ScheduleBatch, output_result: GenerationBatchResult
):
self.process_batch_result(batch, output_result)
def _pp_send_output_to_next_stage(
self: Scheduler,
next_first_rank_mb_id: int,
mbs: List[ScheduleBatch],
last_rank_comm_queue: deque,
pp_outputs: PPProxyTensors | None,
) -> List[P2PWork]:
send_output_work = []
if self.pp_group.is_last_rank:
# send ready PP output to rank 0
target = mbs[next_first_rank_mb_id]
if target is not None:
q_event, pp_outputs_to_send = last_rank_comm_queue.popleft()
if (
not target.forward_mode.is_prebuilt()
and not _pp_can_skip_output_comm(target)
):
self.device_module.current_stream().wait_event(q_event)
with torch.profiler.record_function("send_res_dict_to_next_stage"):
send_output_work = self._pp_send_dict_to_next_stage(
pp_outputs_to_send.tensors,
async_send=True,
msg_type="output",
)
# send the outputs from the last round to let the next stage worker run post processing
if not self.pp_group.is_last_rank:
if pp_outputs:
with torch.profiler.record_function("send_res_dict_to_next_stage"):
send_output_work = self._pp_send_dict_to_next_stage(
pp_outputs.tensors,
async_send=True,
msg_type="output",
)
return send_output_work
def _pp_send_recv_and_preprocess_output_tensors(
self: Scheduler,
next_first_rank_mb_id: int,
next_mb_id: int,
mbs: List[ScheduleBatch],
mb_metadata: List[PPBatchMetadata],
last_rank_comm_queue: deque[Tuple[torch.Event, PPProxyTensors]],
pp_outputs: PPProxyTensors | None,
) -> Tuple[
Optional[PPProxyTensors],
Optional[GenerationBatchResult],
Optional[torch.Event],
List[P2PWork],
]:
next_pp_outputs = None
d2h_event = None
batch_result = None
send_output_work = []
# On CUDA, isend is async: it enqueues to the stream and returns,
# so every rank can send first safely. On some backends isend is
# effectively blocking and does not return until the peer posts a
# matching recv; if every PP rank sends first, all ranks block
# waiting for a receiver and the ring deadlocks. Order send/recv
# by pp_rank parity (even: send->recv, odd: recv->send) so each
# adjacent pair has one sender and one receiver posted at the
# same time.
# CUDA: send first
# XPU: even ranks send first, odd ranks recv first.
send_first = (not is_xpu()) or ((self.ps.pp_rank % 2) == 0)
def _do_send():
return self._pp_send_output_to_next_stage(
next_first_rank_mb_id,
mbs,
last_rank_comm_queue,
pp_outputs,
)
def _do_recv():
nonlocal next_pp_outputs, batch_result, d2h_event
target = mbs[next_mb_id]
if target is None or target.forward_mode.is_prebuilt():
return
if _pp_can_skip_output_comm(target):
next_pp_outputs, batch_result, d2h_event = (
self._pp_make_skip_output_result(target, mb_metadata[next_mb_id])
)
return
with torch.profiler.record_function("recv_res_dict_from_prev_stage"):
next_pp_outputs = PPProxyTensors(self._pp_recv_dict_from_prev_stage())
with self.copy_stream_ctx:
self.copy_stream.wait_stream(self.schedule_stream)
batch_result = self._pp_prep_batch_result(
target, mb_metadata[next_mb_id], next_pp_outputs
)
d2h_event = self.device_module.Event()
d2h_event.record(self.device_module.current_stream())
if send_first:
send_output_work = _do_send()
_do_recv()
else:
_do_recv()
send_output_work = _do_send()
return next_pp_outputs, batch_result, d2h_event, send_output_work
def _pp_launch_batch(
self: Scheduler,
mb_id: int,
cur_batch: ScheduleBatch,
pp_proxy_tensors: PPProxyTensors,
mb_metadata: List[Optional[PPBatchMetadata]],
last_rank_comm_queue: deque,
):
with torch.profiler.record_function("run_batch"):
with self.forward_stream_ctx:
self.forward_stream.wait_stream(self.schedule_stream)
set_time_batch(
cur_batch.reqs,
"set_run_batch_cpu_start_time",
trace_only=True,
)
result = self.run_batch(cur_batch, pp_proxy_tensors)
set_time_batch(
cur_batch.reqs,
"set_run_batch_cpu_end_time",
trace_only=True,
attrs={"pp_mb_id": mb_id},
)
mb_metadata[mb_id] = PPBatchMetadata(
can_run_cuda_graph=result.can_run_cuda_graph,
)
event = self.device_module.Event()
event.record(self.device_module.current_stream())
if self.pp_group.is_last_rank:
# (last rank) buffer the outputs for async batch depth
last_rank_comm_queue.append(
(
event,
PPProxyTensors(
self._pp_prepare_tensor_dict(result, cur_batch)
),
)
)
return result, event
def get_rids(
self: Scheduler, req_queue: List[Req], is_send: bool, *poll_statuses_group
):
"""
Used by PP, get the required rids with the given poll statuses.
"""
polls = poll_and_all_reduce_attn_cp_tp_group(
[req.disagg_kv_sender if is_send else req.kv_receiver for req in req_queue],
self.attn_cp_cpu_group,
self.attn_tp_cpu_group,
)
rids: List = []
for poll_statuses in poll_statuses_group:
rids.append(
[
req.rid if is_send else req.req.rid
for req, poll in zip(req_queue, polls)
if poll in poll_statuses
]
)
return tuple(rids) if len(rids) > 1 else rids[0]
def _pp_pd_get_retract_ids(self: Scheduler, mb_id: int):
# communicate pre-consensus retracted reqs
for req in self.disagg_decode_prealloc_queue.retracted_queue:
# assign retracted reqs to the current microbatch
if req.retraction_mb_id is None:
req.retraction_mb_id = mb_id
curr_retract_rids = [
req.rid
for req in self.disagg_decode_prealloc_queue.retracted_queue
if req.retraction_mb_id == mb_id
]
if self.pp_group.is_first_rank:
# First rank, get all retracted req ids for the microbatch
return curr_retract_rids
else:
# Other ranks, receive the retracted reqs info from the previous rank and ensure the consensus
prev_retract_rids = self._pp_recv_pyobj_from_prev_stage()
return list(set(prev_retract_rids) & set(curr_retract_rids))
def _pp_pd_get_prealloc_ids(self: Scheduler):
# communicate pre-consensus prealloc reqs
if self.pp_group.is_first_rank:
# First rank, pop the preallocated reqs from the prealloc queue
good_prealloc_rids, bad_prealloc_rids = self.get_rids(
self.disagg_decode_prealloc_queue.queue,
False,
[KVPoll.WaitingForInput],
[KVPoll.Failed],
)
else:
# Other ranks, receive the preallocated reqs info from the previous rank and ensure the consensus
prev_prealloc_rids = self._pp_recv_pyobj_from_prev_stage()
prev_good_prealloc_rids, prev_bad_prealloc_rids = prev_prealloc_rids
curr_good_prealloc_rids, curr_bad_prealloc_rids = self.get_rids(
self.disagg_decode_prealloc_queue.queue,
False,
[KVPoll.WaitingForInput],
[KVPoll.Failed],
)
good_prealloc_rids = list(
set(prev_good_prealloc_rids) & set(curr_good_prealloc_rids)
)
bad_prealloc_rids = list(
set(prev_bad_prealloc_rids) | set(curr_bad_prealloc_rids)
)
return [good_prealloc_rids, bad_prealloc_rids]
def _pp_pd_get_decode_transferred_ids(self: Scheduler):
# get the current stage transfer success
if self.pp_group.is_first_rank:
transferred_rids = self.get_rids(
self.disagg_decode_transfer_queue.queue,
False,
[KVPoll.Success, KVPoll.Failed],
)
# if other ranks, do intersection with the previous rank's transferred rids
else:
# 2 (Release): Receive the transferred rids from the previous rank
# 1. recv previous stage's transferred reqs info
prev_transferred_rids = self._pp_recv_pyobj_from_prev_stage()
# 2. get the current stage's transferred reqs info
curr_transferred_rids = self.get_rids(
self.disagg_decode_transfer_queue.queue,
False,
[KVPoll.Success, KVPoll.Failed],
)
# 3. new consensus rids = intersection(previous consensus rids, transfer finished rids)
transferred_rids = list(
set(prev_transferred_rids) & set(curr_transferred_rids)
)
return transferred_rids
def process_retract_queue(self: Scheduler, retract_rids: Optional[List[str]]):
if retract_rids is not None:
# try to resume retracted requests if there are enough space for another `num_reserved_decode_tokens` decode steps
resumed_reqs = self.disagg_decode_prealloc_queue.resume_retracted_reqs(
retract_rids
)
self.waiting_queue.extend(resumed_reqs)
return [req.rid for req in resumed_reqs]
return None
def process_prealloc_queue(self: Scheduler, prealloc_rids: Optional[List[str]]):
if len(self.disagg_decode_prealloc_queue.retracted_queue) > 0:
# if there are still retracted requests, we do not allocate new requests
return [[], []]
if prealloc_rids is not None:
(
good_consensus_prealloc_rids,
bad_consensus_prealloc_rids,
) = prealloc_rids
good_reqs, failed_reqs = self.disagg_decode_prealloc_queue.pop_preallocated(
rids_to_check=good_consensus_prealloc_rids
+ bad_consensus_prealloc_rids,
)
self.disagg_decode_transfer_queue.extend(good_reqs)
return [
[req.req.rid for req in good_reqs],
[req.req.rid for req in failed_reqs],
]
return None
def process_decode_transfer_queue(
self: Scheduler, release_rids: Optional[List[str]]
):
if release_rids is not None:
released_reqs = self.disagg_decode_transfer_queue.pop_transferred(
release_rids
)
if self.enable_hisparse:
for req in released_reqs:
self.hisparse_coordinator.admit_request_direct(req)
self.waiting_queue.extend(released_reqs)
return [req.rid for req in released_reqs]
return None
class ChunkSizePredictor:
"""
Predictor for dynamic chunk size based on quadratic latency model.
Models latency as: f(l) = a*l^2 + b*l + c
Predicts next chunk size x such that: f(L+x) - f(L) = target_latency
"""
def __init__(self):
self.quadratic_coeff_a = 0.0
self.linear_coeff_b = 0.0
self.constant_coeff_c = 0.0
self.target_latency: Optional[float] = None
self.is_ready = False
def fit(self, seq_lens: List[int], latencies: List[float]):
"""Fit quadratic coefficients f(l) = al^2 + bl + c from data points."""
# Skip the first data point to reduce fitting bias, as the first run is slower without warmup
L = np.array(seq_lens[1:], dtype=np.float64)
T = np.array(latencies[1:], dtype=np.float64)
if len(L) < 8:
raise ValueError(
f"Not enough data points for quadratic fitting ({len(L)} < 8). "
"Need at least 8 samples with different sequence lengths."
)
# Build design matrix for f(l) = al^2 + bl + c
X = np.column_stack([L * L, L, np.ones_like(L)]) # [l^2, l, 1]
try:
coeffs, residuals, rank, s = np.linalg.lstsq(X, T, rcond=None)
if len(coeffs) >= 3:
fitted_a = float(coeffs[0]) # quadratic coefficient
fitted_b = float(coeffs[1]) # linear coefficient
fitted_c = float(coeffs[2]) # constant coefficient
else:
raise ValueError("Failed to fit coefficients: insufficient rank")
except np.linalg.LinAlgError as e:
raise ValueError(f"Failed to fit f(l) = al^2 + bl + c: {e}")
# Validate coefficients
if fitted_a <= 0:
raise ValueError(
f"Fitted quadratic coefficient a={fitted_a:.2e} is not positive. "
"Attention has O(n^2) complexity, so a must be positive. "
"Check warmup data quality."
)
if fitted_b < 0:
logger.warning(
f"Fitted linear coefficient b={fitted_b:.2e} is negative. Setting b=0."
)
fitted_b = 0.0
self.quadratic_coeff_a = fitted_a
self.linear_coeff_b = fitted_b
self.constant_coeff_c = fitted_c
logger.info(
f"[ChunkSizePredictor] Fitted coefficients: a={fitted_a:.2e}, "
f"b={fitted_b:.2e}, c={fitted_c:.2e}"
)
def set_target_latency(self, base_chunk_size: int):
"""Set target latency based on base chunk size: target = f(base_chunk_size) - f(0)."""
def f(length: float) -> float:
"""Total latency function: f(length) = a*length^2 + b*length + c."""
return (
self.quadratic_coeff_a * length * length
+ self.linear_coeff_b * length
+ self.constant_coeff_c
)
self.target_latency = f(float(base_chunk_size)) - f(0.0)
if self.target_latency <= 0:
raise ValueError(
f"Calculated target_latency={self.target_latency:.2f}ms is not positive. "
"Check warmup data quality."
)
logger.info(
f"[ChunkSizePredictor] Target latency: {self.target_latency:.2f}ms "
f"(base_chunk_size={base_chunk_size})"
)
def predict_next_chunk_size(
self,
history_len: int,
base_chunk_size: int,
page_size: int,
context_len: int,
max_chunk_size: Optional[int] = None,
) -> Optional[int]:
"""
Predict next chunk size x such that f(history_len + x) - f(history_len) = target_latency.
Args:
history_len: Current sequence length (L)
base_chunk_size: Base chunk size
page_size: Page size for alignment
context_len: Maximum context length
max_chunk_size: Maximum allowed chunk size (optional)
Returns:
Predicted chunk size, or None if prediction fails
"""
if not self.is_ready or self.target_latency is None:
return None
# Handle quadratic model: f(l) = al^2 + bl + c
if self.quadratic_coeff_a <= 0:
return None
# Solve f(L+x) - f(L) = T
# where f(L) = a*L^2 + b*L + c
# This expands to: ax^2 + (2aL+b)x - T = 0
# A = a, B = 2aL + b, C = -T
A = self.quadratic_coeff_a
B = 2 * self.quadratic_coeff_a * history_len + self.linear_coeff_b
C = -self.target_latency
discriminant = B * B - 4 * A * C
if discriminant < 0:
logger.warning(
f"Discriminant is negative ({discriminant:.2e}). "
f"No real solution for chunk size. L={history_len}, T={self.target_latency:.2f}ms."
)
return None
sqrt_discriminant = math.sqrt(discriminant)
calculated_chunk_size_float = (-B + sqrt_discriminant) / (2 * A)
if calculated_chunk_size_float <= 0:
logger.warning(
f"Calculated chunk size is non-positive ({calculated_chunk_size_float:.2f}). "
f"L={history_len}, T={self.target_latency:.2f}ms."
)
return None
# Use a smooth coefficient to reduce the abrupt decrease in chunk size
smooth_coeff = envs.SGLANG_DYNAMIC_CHUNKING_SMOOTH_FACTOR.get()
smoothed_chunk_size = base_chunk_size + smooth_coeff * (
calculated_chunk_size_float - base_chunk_size
)
# Make sure the dynamic chunk size is at least 1/4 of the base chunk size
calculated_chunk_size = max(int(smoothed_chunk_size), base_chunk_size // 4)
# Align to page_size (minimum alignment size is 64)
alignment_size = max(page_size, 64)
dynamic_chunk_size = (calculated_chunk_size // alignment_size) * alignment_size
# Ensure aligned size is at least alignment_size
if dynamic_chunk_size < alignment_size:
dynamic_chunk_size = alignment_size
# Apply constraints
max_allowed = context_len - history_len - 100 # Leave 100 tokens margin
if max_chunk_size is not None:
max_allowed = min(max_allowed, max_chunk_size)
dynamic_chunk_size = min(dynamic_chunk_size, max_allowed)
# Align again after min operation
dynamic_chunk_size = (dynamic_chunk_size // alignment_size) * alignment_size
if dynamic_chunk_size < alignment_size:
return None
return dynamic_chunk_size