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
487 lines
19 KiB
Python
487 lines
19 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from collections import deque
|
|
from dataclasses import dataclass, field
|
|
from typing import (
|
|
TYPE_CHECKING,
|
|
Callable,
|
|
Deque,
|
|
List,
|
|
Optional,
|
|
Tuple,
|
|
)
|
|
|
|
import torch
|
|
|
|
from sglang.srt.disaggregation.utils import DisaggregationMode
|
|
from sglang.srt.environ import envs
|
|
from sglang.srt.managers.scheduler_components.pool_stats_observer import (
|
|
PoolStats,
|
|
SchedulerPoolStatsObserver,
|
|
)
|
|
from sglang.srt.mem_cache.allocator import BaseTokenToKVPoolAllocator
|
|
from sglang.srt.mem_cache.base_prefix_cache import BasePrefixCache
|
|
from sglang.srt.mem_cache.memory_pool import ReqToTokenPool
|
|
from sglang.srt.server_args import ServerArgs
|
|
from sglang.srt.utils.common import (
|
|
ceil_align,
|
|
raise_error_or_warn,
|
|
)
|
|
from sglang.srt.utils.watchdog import WatchdogRaw
|
|
|
|
if TYPE_CHECKING:
|
|
from sglang.srt.managers.scheduler import Scheduler
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Number of recent busy-check messages buffered for the level-1 dump-on-leak path.
|
|
BUSY_MEM_CHECK_LOG_RING_SIZE = 1000
|
|
|
|
|
|
@dataclass(kw_only=True, slots=True)
|
|
class SchedulerInvariantChecker:
|
|
is_hybrid_swa: bool
|
|
is_hybrid_ssm: bool
|
|
disaggregation_mode: DisaggregationMode
|
|
page_size: int
|
|
full_tokens_per_layer: Optional[int]
|
|
swa_tokens_per_layer: Optional[int]
|
|
max_total_num_tokens: int
|
|
server_args: ServerArgs
|
|
tree_cache: BasePrefixCache
|
|
token_to_kv_pool_allocator: BaseTokenToKVPoolAllocator
|
|
req_to_token_pool: ReqToTokenPool
|
|
pool_stats_observer: SchedulerPoolStatsObserver
|
|
get_last_batch: Callable
|
|
get_running_batch: Callable
|
|
count_req_pool_leak_warnings: int = 0
|
|
count_memory_leak_warnings: int = 0
|
|
recent_busy_msgs: Deque[str] = field(
|
|
default_factory=lambda: deque(maxlen=BUSY_MEM_CHECK_LOG_RING_SIZE)
|
|
)
|
|
|
|
@staticmethod
|
|
def _check_pool_invariant(
|
|
pool_name: str,
|
|
available: int,
|
|
evictable: int,
|
|
protected: int,
|
|
session_held: int,
|
|
total: int,
|
|
uncached: int = 0,
|
|
) -> Tuple[bool, str]:
|
|
"""Check: available + evictable + protected + session_held + uncached == total."""
|
|
total_accounted = available + evictable + protected + session_held + uncached
|
|
leak = total_accounted != total
|
|
msg = (
|
|
f"[{pool_name}] {total=}, {available=}, {evictable=}, "
|
|
f"{protected=}, {session_held=}, {uncached=}"
|
|
)
|
|
return leak, msg
|
|
|
|
def _check_full_pool(self, ps: PoolStats, uncached: int = 0) -> Tuple[bool, str]:
|
|
if self.is_hybrid_swa and not self.full_tokens_per_layer:
|
|
return False, ""
|
|
if self.is_hybrid_swa:
|
|
protected = self.tree_cache.full_protected_size()
|
|
session_held = self.pool_stats_observer.session_held_full_tokens()
|
|
total = self.full_tokens_per_layer
|
|
elif self.is_hybrid_ssm:
|
|
# Branch on cache type for the protected accessor (MambaRadixCache
|
|
# splits full/mamba; ChunkCache only has the single protected_size).
|
|
# Use the allocator's `.size` for `total`: static max_total_num_tokens for
|
|
# non-unified pools, the dynamic byte-coordinated cap (matching
|
|
# `available_size`) for the unified pool.
|
|
if self.tree_cache.supports_mamba():
|
|
protected = self.tree_cache.full_protected_size()
|
|
else:
|
|
protected = self.tree_cache.protected_size()
|
|
session_held = self.pool_stats_observer.session_held_tokens()
|
|
total = self.token_to_kv_pool_allocator.size
|
|
else:
|
|
protected = self.tree_cache.protected_size()
|
|
session_held = self.pool_stats_observer.session_held_tokens()
|
|
total = self.max_total_num_tokens
|
|
full_evictable_size = ps.full_evictable_size
|
|
allocator = self.token_to_kv_pool_allocator
|
|
if getattr(self.server_args, "dcp_size", 1) > 1 and allocator.page_size > 1:
|
|
# DCP stores logical tokens in widened physical pages. Prefix cache
|
|
# counters are logical-token based, while the allocator frees whole
|
|
# physical pages, so round cached tokens up to physical page units.
|
|
full_evictable_size = (
|
|
(full_evictable_size + allocator.page_size - 1)
|
|
// allocator.page_size
|
|
* allocator.page_size
|
|
)
|
|
leak, msg = self._check_pool_invariant(
|
|
"full",
|
|
ps.full_available_size,
|
|
full_evictable_size,
|
|
protected,
|
|
session_held,
|
|
total,
|
|
uncached,
|
|
)
|
|
if (
|
|
leak
|
|
and getattr(self.server_args, "dcp_size", 1) > 1
|
|
and allocator.page_size > 1
|
|
):
|
|
# Radix/Mamba cache accounting is logical-token based while DCP full
|
|
# KV allocation is physical-page based. Partial physical pages can
|
|
# leave a small page-level slack even when all pages are owned by
|
|
# either the allocator or the prefix cache.
|
|
return False, f"{msg}, dcp_physical_page_slack_allowed=True"
|
|
return leak, msg
|
|
|
|
def _check_swa_pool(self, ps: PoolStats, uncached: int = 0) -> Tuple[bool, str]:
|
|
return self._check_pool_invariant(
|
|
"swa",
|
|
ps.swa_available_size,
|
|
ps.swa_evictable_size,
|
|
self.tree_cache.swa_protected_size(),
|
|
self.pool_stats_observer.session_held_swa_tokens(),
|
|
self.swa_tokens_per_layer,
|
|
uncached,
|
|
)
|
|
|
|
def _check_mamba_pool(self, ps: PoolStats) -> Tuple[bool, str]:
|
|
ckpt_pool = getattr(self.req_to_token_pool, "mamba_ckpt_pool", None)
|
|
if ckpt_pool is not None:
|
|
return self._check_mamba_pool_with_int8(ps, ckpt_pool)
|
|
leak, msg = self._check_pool_invariant(
|
|
"mamba",
|
|
ps.mamba_available_size,
|
|
ps.mamba_evictable_size,
|
|
self.tree_cache.mamba_protected_size(),
|
|
self.pool_stats_observer.session_held_mamba_slots(),
|
|
self.req_to_token_pool.mamba_pool.size,
|
|
)
|
|
if leak:
|
|
# Page-level leak diagnosis for mamba
|
|
free_full_pages = set(
|
|
self.token_to_kv_pool_allocator.free_pages.tolist()
|
|
+ self.token_to_kv_pool_allocator.release_pages.tolist()
|
|
)
|
|
cached_full_pages = set(self.tree_cache.all_values_flatten().tolist())
|
|
expected_full_pages = set(
|
|
range(1, self.token_to_kv_pool_allocator.size + 1)
|
|
)
|
|
leaked_full_pages = (
|
|
expected_full_pages - free_full_pages - cached_full_pages
|
|
)
|
|
mamba_allocator = self.req_to_token_pool.mamba_allocator
|
|
free_mamba_pages = set(mamba_allocator.free_slots.tolist())
|
|
cached_mamba_pages = set(
|
|
self.tree_cache.all_mamba_values_flatten().tolist()
|
|
)
|
|
expected_mamba_pages = set(range(1, mamba_allocator.size + 1))
|
|
leaked_mamba_pages = (
|
|
expected_mamba_pages - free_mamba_pages - cached_mamba_pages
|
|
)
|
|
msg += (
|
|
f", leaked_full_pages={leaked_full_pages or None}"
|
|
f", leaked_mamba_pages={leaked_mamba_pages or None}"
|
|
)
|
|
return leak, msg
|
|
|
|
def _check_mamba_pool_with_int8(self, ps: PoolStats, ckpt_pool) -> Tuple[bool, str]:
|
|
"""Two-pool invariant for int8 mamba checkpoints.
|
|
|
|
The radix-cached states live in the int8 checkpoint pool, NOT the active
|
|
bf16 pool. So the single-pool equation (active.available + radix_cached ==
|
|
active.size) is wrong -- it double-counts the radix states against a pool
|
|
that does not hold them. Instead check the two pools independently:
|
|
|
|
* active bf16 pool: backs running requests only; the radix owns ZERO
|
|
active slots. Checked at idle (in-flight == 0) -> available == total.
|
|
* int8 checkpoint pool: backs the radix-cached states; its occupancy is
|
|
exactly the radix evictable + protected counts.
|
|
"""
|
|
active_leak, active_msg = self._check_pool_invariant(
|
|
"mamba-active",
|
|
ps.mamba_available_size,
|
|
ps.mamba_evictable_size, # 0 in int8 mode (radix owns no active slots)
|
|
0,
|
|
self.pool_stats_observer.session_held_mamba_slots(),
|
|
self.req_to_token_pool.mamba_pool.size,
|
|
)
|
|
int8_leak, int8_msg = self._check_pool_invariant(
|
|
"mamba-int8",
|
|
ckpt_pool.available_size(),
|
|
self.tree_cache.mamba_evictable_size(),
|
|
self.tree_cache.mamba_protected_size(),
|
|
0,
|
|
ckpt_pool.num_slots,
|
|
)
|
|
return active_leak or int8_leak, active_msg + "\n" + int8_msg
|
|
|
|
def _get_total_uncached_sizes(
|
|
self,
|
|
) -> Tuple[int, int]:
|
|
"""Sum uncached tokens for full and SWA pools across all active batches.
|
|
|
|
Returns (full_uncached, swa_uncached). For non-SWA models, swa_uncached is 0.
|
|
|
|
For full pool: uncached = allocated - cache_protected_len
|
|
For SWA pool: uncached = allocated - max(cache_protected_len, swa_evicted_seqlen)
|
|
"""
|
|
# After decode: running_batch IS last_batch (same object), count once.
|
|
# After prefill: they differ, both hold uncached tokens.
|
|
# Use identity (is / is not), not membership or ==: ScheduleBatch's
|
|
# dataclass __eq__ compares tensor fields and raises on ambiguous bools.
|
|
last_batch = self.get_last_batch()
|
|
running_batch = self.get_running_batch()
|
|
batches = [last_batch]
|
|
if (
|
|
running_batch is not None
|
|
and running_batch is not last_batch
|
|
and not running_batch.is_empty()
|
|
):
|
|
batches.append(running_batch)
|
|
|
|
full_uncached = 0
|
|
swa_uncached = 0
|
|
for batch in batches:
|
|
for req in batch.reqs:
|
|
assert req.kv_committed_freed == req.kv_overallocated_freed
|
|
if req.kv_committed_freed or req.req_pool_idx is None:
|
|
continue
|
|
|
|
allocated_len = req.kv_allocated_len
|
|
if self.page_size > 1:
|
|
allocated_len = ceil_align(allocated_len, self.page_size)
|
|
assert req.cache_protected_len % self.page_size == 0
|
|
|
|
full_uncached += allocated_len - req.cache_protected_len
|
|
if self.is_hybrid_swa:
|
|
swa_uncached += allocated_len - max(
|
|
req.cache_protected_len, req.swa_evicted_seqlen
|
|
)
|
|
|
|
return full_uncached, swa_uncached
|
|
|
|
def self_check_during_busy(self):
|
|
if self.get_last_batch() is None:
|
|
return
|
|
|
|
ps = self.pool_stats_observer.get_pool_stats()
|
|
full_uncached, swa_uncached = self._get_total_uncached_sizes()
|
|
|
|
full_leak, full_msg = self._check_full_pool(ps, uncached=full_uncached)
|
|
|
|
swa_leak, swa_msg = False, ""
|
|
if self.is_hybrid_swa:
|
|
swa_leak, swa_msg = self._check_swa_pool(ps, uncached=swa_uncached)
|
|
|
|
level = envs.SGLANG_ENABLE_STRICT_MEM_CHECK_DURING_BUSY.get()
|
|
full_line = f"[Mem Check (BUSY)] {full_msg}"
|
|
swa_line = f"[Mem Check (BUSY)] {swa_msg}" if swa_msg else None
|
|
|
|
if level > 1:
|
|
# Verbose: log every iteration.
|
|
logger.info(full_line)
|
|
if swa_line:
|
|
logger.info(swa_line)
|
|
elif level == 1:
|
|
# Quiet: buffer and stay silent; flush the recent ones only on a leak.
|
|
self.recent_busy_msgs.append(full_line)
|
|
if swa_line:
|
|
self.recent_busy_msgs.append(swa_line)
|
|
if full_leak or swa_leak:
|
|
for msg in self.recent_busy_msgs:
|
|
logger.info(msg)
|
|
|
|
assert not full_leak, f"Full Pool Mem Leak Detected! {full_msg}"
|
|
assert not swa_leak, f"SWA Pool Mem Leak Detected! {swa_msg}"
|
|
|
|
if envs.SGLANG_CHECK_KV_PAGE_INVARIANTS.get():
|
|
self._check_kv_page_invariants()
|
|
|
|
def _check_kv_page_invariants(self):
|
|
"""committed<=allocated for every req/slot, and no double free:
|
|
A. no owner references a page that is in the free pool (use-after-free).
|
|
B. the free pool has no duplicate pages (two owners freed the same page).
|
|
All heavy work runs on GPU to avoid per-token device->host sync."""
|
|
rtt = self.req_to_token_pool.req_to_token
|
|
row_width = rtt.shape[1]
|
|
|
|
def _add_owner(req_or_slot, label, rpi, committed, allocated):
|
|
assert 0 <= committed <= allocated <= row_width
|
|
owners.append((label, rpi, allocated))
|
|
|
|
owners: list[tuple[str, Optional[int], int]] = []
|
|
batch = self.get_last_batch()
|
|
if batch is not None:
|
|
for req in batch.reqs:
|
|
_add_owner(
|
|
req,
|
|
f"req {req.rid}",
|
|
req.req_pool_idx,
|
|
req.kv_committed_len,
|
|
req.kv_allocated_len,
|
|
)
|
|
sess = getattr(self.tree_cache, "slots", None)
|
|
if sess:
|
|
for sid, slot in sess.items():
|
|
if getattr(slot, "is_holding_kv", False):
|
|
_add_owner(
|
|
slot,
|
|
f"slot {sid[:8]}",
|
|
slot.req_pool_idx,
|
|
slot.kv_committed_len,
|
|
slot.kv_allocated_len,
|
|
)
|
|
|
|
active = [
|
|
(label, rpi, al) for label, rpi, al in owners if rpi is not None and al > 0
|
|
]
|
|
if not active:
|
|
return
|
|
|
|
idx = torch.as_tensor([rpi for _, rpi, _ in active], device=rtt.device)
|
|
allocs = torch.as_tensor([al for _, _, al in active], device=rtt.device)
|
|
mask = torch.arange(row_width, device=rtt.device)[None, :] < allocs[:, None]
|
|
owner_pages = rtt[idx][mask] // self.page_size
|
|
|
|
# Sub-allocators to check: a flat allocator is its own single sub; a
|
|
# hybrid-SWA wrapper exposes full_attn_allocator + swa_attn_allocator.
|
|
alloc = self.token_to_kv_pool_allocator
|
|
sub_allocs = (
|
|
[alloc]
|
|
if getattr(alloc, "free_pages", None) is not None
|
|
else [
|
|
sub
|
|
for n in ("full_attn_allocator", "swa_attn_allocator")
|
|
if (sub := getattr(alloc, n, None)) is not None
|
|
and getattr(sub, "free_pages", None) is not None
|
|
]
|
|
)
|
|
if not sub_allocs:
|
|
return
|
|
|
|
def _free_pages(a):
|
|
free = a.free_pages
|
|
release = getattr(a, "release_pages", None)
|
|
return (
|
|
torch.cat((free, release))
|
|
if release is not None and len(release) > 0
|
|
else free
|
|
)
|
|
|
|
# Check B: every sub-pool's free set has no duplicate pages.
|
|
for i, sub in enumerate(sub_allocs):
|
|
free = _free_pages(sub)
|
|
uniq = torch.unique(free)
|
|
if uniq.numel() != free.numel():
|
|
raise_error_or_warn(
|
|
self,
|
|
envs.SGLANG_ENABLE_STRICT_MEM_CHECK_DURING_IDLE.get(),
|
|
"count_memory_leak_warnings",
|
|
f"KV double free: sub-pool {i} has {free.numel() - uniq.numel()} duplicate pages.",
|
|
)
|
|
|
|
# Check A: owner pages (full-pool indices) must not be in the full free
|
|
# set (sub_allocs[0] is the full pool, even on hybrid-SWA).
|
|
full_unique = torch.unique(_free_pages(sub_allocs[0]))
|
|
stale = owner_pages[torch.isin(owner_pages, full_unique)]
|
|
if stale.numel() > 0:
|
|
raise_error_or_warn(
|
|
self,
|
|
envs.SGLANG_ENABLE_STRICT_MEM_CHECK_DURING_IDLE.get(),
|
|
"count_memory_leak_warnings",
|
|
f"KV page use-after-free: {stale.numel()} owner page refs are in "
|
|
f"the free pool, sample pages={torch.unique(stale)[:8].tolist()}.",
|
|
)
|
|
|
|
def _check_req_pool(self):
|
|
if self.disaggregation_mode == DisaggregationMode.DECODE:
|
|
req_total_size = (
|
|
self.req_to_token_pool.size + self.req_to_token_pool.pre_alloc_size
|
|
)
|
|
else:
|
|
req_total_size = self.req_to_token_pool.size
|
|
|
|
session_req_count = self.pool_stats_observer.session_held_req_count()
|
|
if len(self.req_to_token_pool.free_slots) + session_req_count != req_total_size:
|
|
msg = (
|
|
"req_to_token_pool memory leak detected!"
|
|
f"available_size={len(self.req_to_token_pool.free_slots)}, "
|
|
f"session_held={session_req_count}, "
|
|
f"total_size={self.req_to_token_pool.size}\n"
|
|
)
|
|
raise_error_or_warn(
|
|
self,
|
|
envs.SGLANG_ENABLE_STRICT_MEM_CHECK_DURING_IDLE.get(),
|
|
"count_req_pool_leak_warnings",
|
|
msg,
|
|
)
|
|
|
|
def _report_leak(self, pool_name: str, token_msg: str):
|
|
msg = f"{pool_name} memory leak detected! {token_msg}"
|
|
raise_error_or_warn(
|
|
self,
|
|
envs.SGLANG_ENABLE_STRICT_MEM_CHECK_DURING_IDLE.get(),
|
|
"count_memory_leak_warnings",
|
|
msg,
|
|
)
|
|
|
|
def _check_all_pools(
|
|
self, ps: PoolStats, uncached: int = 0
|
|
) -> Tuple[bool, List[str]]:
|
|
"""Check memory invariant across all pools. Returns (has_leak, messages)."""
|
|
has_leak = False
|
|
messages = []
|
|
|
|
full_leak, full_msg = self._check_full_pool(ps, uncached=uncached)
|
|
has_leak |= full_leak
|
|
messages.append(full_msg)
|
|
|
|
if self.is_hybrid_swa:
|
|
swa_leak, swa_msg = self._check_swa_pool(ps)
|
|
has_leak |= swa_leak
|
|
messages.append(swa_msg)
|
|
|
|
if self.is_hybrid_ssm and self.tree_cache.supports_mamba():
|
|
mamba_leak, mamba_msg = self._check_mamba_pool(ps)
|
|
has_leak |= mamba_leak
|
|
messages.append(mamba_msg)
|
|
|
|
return has_leak, messages
|
|
|
|
def _check_tree_cache(self):
|
|
if (
|
|
self.tree_cache.is_tree_cache()
|
|
and (self.is_hybrid_swa and self.tree_cache.supports_swa())
|
|
or (self.is_hybrid_ssm and self.tree_cache.supports_mamba())
|
|
):
|
|
self.tree_cache.sanity_check()
|
|
|
|
|
|
def create_scheduler_watchdog(
|
|
scheduler: Scheduler, watchdog_timeout: float, soft: bool = False
|
|
) -> WatchdogRaw:
|
|
def dump_info() -> str:
|
|
if scheduler.is_initializing:
|
|
return ""
|
|
_, messages = scheduler.invariant_checker._check_all_pools(
|
|
scheduler.pool_stats_observer.get_pool_stats(),
|
|
)
|
|
return (
|
|
f"{scheduler.cur_batch_for_debug.batch_size()=}\n"
|
|
f"{scheduler.cur_batch_for_debug.reqs=}\n" + "\n".join(messages)
|
|
)
|
|
|
|
return WatchdogRaw(
|
|
debug_name="Scheduler",
|
|
get_counter=lambda: scheduler.forward_ct,
|
|
is_active=lambda: (
|
|
scheduler.is_initializing or scheduler.cur_batch_for_debug is not None
|
|
),
|
|
watchdog_timeout=watchdog_timeout,
|
|
soft=soft,
|
|
dump_info=dump_info,
|
|
)
|