Introduce global alloc_len_per_decode & clean check decode memory (#15115)

This commit is contained in:
Liangsheng Yin
2026-01-26 10:26:20 -08:00
committed by GitHub
parent 8643fb2f52
commit 85d077f44d
10 changed files with 76 additions and 101 deletions
+1 -9
View File
@@ -616,15 +616,7 @@ class DecodePreallocQueue:
and len(self.scheduler.running_batch.reqs) > 0 and len(self.scheduler.running_batch.reqs) > 0
else 0 else 0
) )
available_size = self.token_to_kv_pool_allocator.available_size()
if self.scheduler.model_config.is_hybrid_swa:
available_size = min(
self.token_to_kv_pool_allocator.full_available_size(),
self.token_to_kv_pool_allocator.swa_available_size(),
)
else:
available_size = self.token_to_kv_pool_allocator.available_size()
allocatable_tokens = available_size - max( allocatable_tokens = available_size - max(
# preserve some space for future decode # preserve some space for future decode
self.num_reserved_decode_tokens self.num_reserved_decode_tokens
+30 -45
View File
@@ -4,6 +4,7 @@ import enum
from sglang.srt.dllm.config import DllmConfig from sglang.srt.dllm.config import DllmConfig
from sglang.srt.model_executor.forward_batch_info import ForwardBatch from sglang.srt.model_executor.forward_batch_info import ForwardBatch
from sglang.srt.utils.common import ceil_align
# Copyright 2023-2024 SGLang Team # Copyright 2023-2024 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1794,46 +1795,43 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin):
self.extend_logprob_start_lens.extend([0] * running_bs) self.extend_logprob_start_lens.extend([0] * running_bs)
self.is_prefill_only = False self.is_prefill_only = False
def new_page_count_next_decode(self, selected_indices: Optional[List[int]] = None): def new_tokens_required_next_decode(
self, selected_indices: Optional[List[int]] = None
):
page_size = self.token_to_kv_pool_allocator.page_size page_size = self.token_to_kv_pool_allocator.page_size
requests = ( requests = (
self.reqs self.reqs
if selected_indices is None if selected_indices is None
else [self.reqs[i] for i in selected_indices] else [self.reqs[i] for i in selected_indices]
) )
if page_size == 1:
return len(requests)
if not self.spec_algorithm.is_none(): if self.spec_algorithm.is_none():
# A loose bound that err towards safety new_pages = sum(1 for r in requests if r.kv_committed_len % page_size == 0)
server_args = get_global_server_args() return new_pages * page_size
thresh = server_args.speculative_num_draft_tokens + (
(server_args.speculative_eagle_topk or 1)
* (server_args.speculative_num_steps or 1)
)
return sum(
1 for req in requests if ((req.seqlen + thresh) % page_size) <= thresh
)
# In the decoding phase, the length of a request's KV cache should be server_args = get_global_server_args()
# the total length of the request minus 1 len_per_topk = server_args.speculative_num_steps or 1
return ( spec_topk = server_args.speculative_eagle_topk or 1
sum(1 for req in requests if req.seqlen % page_size == 0) spec_tokens = server_args.speculative_num_draft_tokens
if self.enable_overlap
else sum(1 for req in requests if (req.seqlen - 1) % page_size == 0)
)
def check_decode_mem( if page_size > 1 and spec_topk > 1:
self, buf_multiplier=1, selected_indices: Optional[List[int]] = None # last partial page and ceil alignment
): len_per_topk = ceil_align(len_per_topk + page_size, page_size)
num_tokens = ( spec_tokens = ceil_align(spec_tokens, page_size)
self.new_page_count_next_decode(selected_indices) elif page_size > 1:
* buf_multiplier # only page alignment
* self.token_to_kv_pool_allocator.page_size len_per_topk = ceil_align(len_per_topk, page_size)
) spec_tokens = ceil_align(spec_tokens, page_size)
num_tokens = max(len_per_topk * spec_topk, spec_tokens) * len(requests)
# v2 eagle has over-allocation
return num_tokens * (1 + self.is_spec_v2)
def check_decode_mem(self, selected_indices: Optional[List[int]] = None):
num_tokens = self.new_tokens_required_next_decode(selected_indices)
evict_from_tree_cache(self.tree_cache, num_tokens) evict_from_tree_cache(self.tree_cache, num_tokens)
return self._is_available_size_sufficient(num_tokens) return self.token_to_kv_pool_allocator.available_size() >= num_tokens
def retract_all(self, server_args: ServerArgs): def retract_all(self, server_args: ServerArgs):
retracted_reqs = self.reqs retracted_reqs = self.reqs
@@ -1844,9 +1842,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin):
return retracted_reqs return retracted_reqs
def retract_decode( def retract_decode(
self, self, server_args: ServerArgs
server_args: ServerArgs,
buf_multiplier: int = 1,
) -> Tuple[List[Req], float, List[Req]]: ) -> Tuple[List[Req], float, List[Req]]:
"""Retract the decoding requests when there is not enough memory.""" """Retract the decoding requests when there is not enough memory."""
sorted_indices = list(range(len(self.reqs))) sorted_indices = list(range(len(self.reqs)))
@@ -1868,9 +1864,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin):
retracted_reqs = [] retracted_reqs = []
first_iter = True first_iter = True
while first_iter or ( while first_iter or (
not self.check_decode_mem( not self.check_decode_mem(selected_indices=sorted_indices)
selected_indices=sorted_indices, buf_multiplier=buf_multiplier
)
): ):
if len(sorted_indices) == 1: if len(sorted_indices) == 1:
# Always keep at least one request # Always keep at least one request
@@ -1884,7 +1878,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin):
self.release_req(idx, len(sorted_indices), server_args) self.release_req(idx, len(sorted_indices), server_args)
if len(sorted_indices) <= 1 and not self.check_decode_mem( if len(sorted_indices) <= 1 and not self.check_decode_mem(
selected_indices=sorted_indices, buf_multiplier=buf_multiplier selected_indices=sorted_indices
): ):
# Retracting loops ends and still not enough memory # Retracting loops ends and still not enough memory
raise ValueError( raise ValueError(
@@ -2320,15 +2314,6 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin):
self.token_to_kv_pool_allocator.free_swa(free_slots) self.token_to_kv_pool_allocator.free_swa(free_slots)
req.swa_evicted_seqlen = new_swa_evicted_seqlen req.swa_evicted_seqlen = new_swa_evicted_seqlen
def _is_available_size_sufficient(self, num_tokens: int) -> bool:
if self.is_hybrid_swa:
return (
self.token_to_kv_pool_allocator.full_available_size() >= num_tokens
and self.token_to_kv_pool_allocator.swa_available_size() >= num_tokens
)
else:
return self.token_to_kv_pool_allocator.available_size() >= num_tokens
def __str__(self): def __str__(self):
return ( return (
f"ScheduleBatch(forward_mode={self.forward_mode.name if self.forward_mode else 'None'}, " f"ScheduleBatch(forward_mode={self.forward_mode.name if self.forward_mode else 'None'}, "
+6 -32
View File
@@ -704,18 +704,6 @@ class Scheduler(
else: else:
self.decode_offload_manager = None self.decode_offload_manager = None
self.decode_mem_cache_buf_multiplier = (
1
if self.spec_algorithm.is_none()
else (
server_args.speculative_num_draft_tokens
+ (
(server_args.speculative_eagle_topk or 1)
* (server_args.speculative_num_steps or 1)
)
)
)
embedding_cache_size = envs.SGLANG_VLM_CACHE_SIZE_MB.get() embedding_cache_size = envs.SGLANG_VLM_CACHE_SIZE_MB.get()
init_mm_embedding_cache(embedding_cache_size * 1024 * 1024) init_mm_embedding_cache(embedding_cache_size * 1024 * 1024)
@@ -2162,29 +2150,15 @@ class Scheduler(
return batch return batch
# Check if decode out of memory # Check if decode out of memory
if ( if (kv_full_retract_flag := not batch.check_decode_mem()) or (
kv_full_retract_flag := not batch.check_decode_mem( TEST_RETRACT and self.forward_ct % TEST_RETRACT_INTERVAL == 0
self.decode_mem_cache_buf_multiplier ):
) old_available_tokens = self.token_to_kv_pool_allocator.available_size()
) or (TEST_RETRACT and self.forward_ct % TEST_RETRACT_INTERVAL == 0):
if self.is_hybrid_swa:
old_available_tokens = min(
self.token_to_kv_pool_allocator.full_available_size(),
self.token_to_kv_pool_allocator.swa_available_size(),
)
else:
old_available_tokens = self.token_to_kv_pool_allocator.available_size()
old_ratio = self.new_token_ratio old_ratio = self.new_token_ratio
retracted_reqs, new_token_ratio, reqs_to_abort = batch.retract_decode( retracted_reqs, new_token_ratio, reqs_to_abort = batch.retract_decode(
self.server_args, self.decode_mem_cache_buf_multiplier self.server_args
) )
if self.is_hybrid_swa: new_available_tokens = self.token_to_kv_pool_allocator.available_size()
new_available_tokens = min(
self.token_to_kv_pool_allocator.full_available_size(),
self.token_to_kv_pool_allocator.swa_available_size(),
)
else:
new_available_tokens = self.token_to_kv_pool_allocator.available_size()
new_token_gained = new_available_tokens - old_available_tokens new_token_gained = new_available_tokens - old_available_tokens
self.num_retracted_reqs = len(retracted_reqs) self.num_retracted_reqs = len(retracted_reqs)
+28
View File
@@ -11,6 +11,7 @@ from sglang.srt.layers.logits_processor import LogitsProcessorOutput
from sglang.srt.managers.overlap_utils import FutureIndices from sglang.srt.managers.overlap_utils import FutureIndices
from sglang.srt.managers.schedule_batch import Req from sglang.srt.managers.schedule_batch import Req
from sglang.srt.model_executor.forward_batch_info import PPProxyTensors from sglang.srt.model_executor.forward_batch_info import PPProxyTensors
from sglang.srt.server_args import ServerArgs
if TYPE_CHECKING: if TYPE_CHECKING:
from sglang.srt.managers.scheduler import GenerationBatchResult from sglang.srt.managers.scheduler import GenerationBatchResult
@@ -176,3 +177,30 @@ def get_logprob_from_pp_outputs(
] ]
return logits_output, extend_input_len_per_req, extend_logprob_start_len_per_req return logits_output, extend_input_len_per_req, extend_logprob_start_len_per_req
def get_alloc_len_per_decode(server_args: Optional[ServerArgs] = None) -> int:
if server_args is None:
from sglang.srt.server_args import get_global_server_args
server_args = get_global_server_args()
if server_args.speculative_algorithm is None:
return 1
# Spec v1:
# 1) alloc topk * num_steps when draft decoding and then restore the allocation
# 2) alloc num_draft_tokens when verifying the drafts
# Sepc v2: allocate max(topk * num_steps, num_draft_tokens)
spec_steps = server_args.speculative_num_steps or 1
spec_topk = server_args.speculative_eagle_topk or 1
spec_tokens = server_args.speculative_num_draft_tokens
page_size = server_args.page_size
if page_size == 1 or spec_topk == 1:
return max(spec_steps * spec_topk, spec_tokens)
else:
raise NotImplementedError(
"get_alloc_len_per_decode not implemented for page_size > 1 and spec_topk > 1"
)
+1 -2
View File
@@ -235,8 +235,7 @@ def evict_from_tree_cache(tree_cache: BasePrefixCache | None, num_tokens: int):
allocator = tree_cache.token_to_kv_pool_allocator allocator = tree_cache.token_to_kv_pool_allocator
# Check if this is a hybrid allocator if isinstance(allocator, SWATokenToKVPoolAllocator):
if hasattr(allocator, "full_available_size"):
# Hybrid allocator # Hybrid allocator
full_available_size = allocator.full_available_size() full_available_size = allocator.full_available_size()
swa_available_size = allocator.swa_available_size() swa_available_size = allocator.swa_available_size()
@@ -297,8 +297,10 @@ class SWATokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
self._kvcache.register_mapping(weakref.proxy(self.full_to_swa_index_mapping)) self._kvcache.register_mapping(weakref.proxy(self.full_to_swa_index_mapping))
def available_size(self): def available_size(self):
# Note: use full_available_size() and swa_available_size() instead. return min(
raise NotImplementedError() self.full_attn_allocator.available_size(),
self.swa_attn_allocator.available_size(),
)
def full_available_size(self): def full_available_size(self):
return self.full_attn_allocator.available_size() return self.full_attn_allocator.available_size()
+1 -4
View File
@@ -1,7 +1,7 @@
import logging import logging
from copy import copy from copy import copy
from dataclasses import dataclass from dataclasses import dataclass
from typing import ClassVar, List, Optional, Tuple from typing import List, Optional, Tuple
import torch import torch
import torch.nn.functional as F import torch.nn.functional as F
@@ -614,9 +614,6 @@ class EagleVerifyInput(SpecInput, EagleVerifyInputV2Mixin):
@dataclass @dataclass
class EagleDraftInput(SpecInput, EagleDraftInputV2Mixin): class EagleDraftInput(SpecInput, EagleDraftInputV2Mixin):
# Constant: alloc length per decode step
ALLOC_LEN_PER_DECODE: ClassVar[int] = None
# The inputs for decode # The inputs for decode
# shape: (b, topk) # shape: (b, topk)
topk_p: torch.Tensor = None topk_p: torch.Tensor = None
@@ -10,6 +10,7 @@ import triton.language as tl
from sglang.srt.layers.logits_processor import LogitsProcessorOutput from sglang.srt.layers.logits_processor import LogitsProcessorOutput
from sglang.srt.managers.schedule_batch import ModelWorkerBatch, ScheduleBatch from sglang.srt.managers.schedule_batch import ModelWorkerBatch, ScheduleBatch
from sglang.srt.managers.utils import get_alloc_len_per_decode
from sglang.srt.mem_cache.common import ( from sglang.srt.mem_cache.common import (
alloc_paged_token_slots_extend, alloc_paged_token_slots_extend,
alloc_token_slots, alloc_token_slots,
@@ -92,9 +93,10 @@ class EagleDraftInputV2Mixin:
cur_kv_lens_cpu = [] cur_kv_lens_cpu = []
nxt_kv_lens_cpu = [] nxt_kv_lens_cpu = []
num_needed_tokens = 0 num_needed_tokens = 0
alloc_len_per_decode = get_alloc_len_per_decode()
for r in batch.reqs: for r in batch.reqs:
# Over-allocation happens here # Over-allocation happens here
x = r.kv_committed_len + 2 * self.ALLOC_LEN_PER_DECODE - r.kv_allocated_len x = r.kv_committed_len + 2 * alloc_len_per_decode - r.kv_allocated_len
cur_kv_lens_cpu.append(r.kv_allocated_len) cur_kv_lens_cpu.append(r.kv_allocated_len)
nxt_kv_lens_cpu.append(r.kv_allocated_len + x) nxt_kv_lens_cpu.append(r.kv_allocated_len + x)
num_needed_tokens += x num_needed_tokens += x
@@ -394,10 +394,11 @@ class EAGLEWorker(TpModelWorker):
# [ topk 0 ] [ topk 1 ] # [ topk 0 ] [ topk 1 ]
# [iter=0, iter=1, iter=2] [iter=0, iter=1, iter=2] # [iter=0, iter=1, iter=2] [iter=0, iter=1, iter=2]
if self.page_size == 1: if self.page_size == 1:
alloc_len_per_decode = self.speculative_num_steps * self.topk
# TODO: We only need self.speculative_num_steps - 1 * topk cache loc # TODO: We only need self.speculative_num_steps - 1 * topk cache loc
out_cache_loc, token_to_kv_pool_state_backup = alloc_token_slots( out_cache_loc, token_to_kv_pool_state_backup = alloc_token_slots(
batch.tree_cache, batch.tree_cache,
num_seqs * self.speculative_num_steps * self.topk, num_seqs * alloc_len_per_decode,
backup_state=True, backup_state=True,
) )
else: else:
@@ -107,11 +107,6 @@ class EagleDraftWorker(BaseDraftWorker):
server_args.speculative_algorithm server_args.speculative_algorithm
) )
# Set constant
EagleDraftInput.ALLOC_LEN_PER_DECODE = max(
self.speculative_num_steps * self.topk, self.speculative_num_draft_tokens
)
# Do not capture cuda graph in `TpModelWorker` init, # Do not capture cuda graph in `TpModelWorker` init,
# will capture later with init_cuda_graphs() # will capture later with init_cuda_graphs()
backup_disable_cuda_graph = server_args.disable_cuda_graph backup_disable_cuda_graph = server_args.disable_cuda_graph