[unified-memory] Stop eviction when shared allocation capacity is sufficient (#33091)
Co-authored-by: seokwoosong <seokwoosong@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,246 @@
|
|||||||
|
"""Reproduce and measure peer-aware eviction in the unified memory pool.
|
||||||
|
|
||||||
|
The workload deliberately fills the shared FULL/Mamba pool with many small,
|
||||||
|
reusable prefixes. It then submits one long pressure request and probes every
|
||||||
|
prefix again. Keeping more probe prefixes cached demonstrates that allocator
|
||||||
|
capacity gained from a peer component stopped radix eviction early.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
python benchmark/unified_memory/bench_peer_aware_eviction.py \
|
||||||
|
--label proposed --output /tmp/proposed.json
|
||||||
|
|
||||||
|
Concurrent fan-out from a prefix retained only by the proposed path:
|
||||||
|
|
||||||
|
python benchmark/unified_memory/bench_peer_aware_eviction.py \
|
||||||
|
--model-path Qwen/Qwen3.5-4B \
|
||||||
|
--probe-output-len 32 --probe-concurrency 6 \
|
||||||
|
--burst-target-index 14 --burst-requests 30 \
|
||||||
|
--label proposed-concurrent --output /tmp/proposed-concurrent.json
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import statistics
|
||||||
|
import time
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from transformers import AutoTokenizer
|
||||||
|
|
||||||
|
|
||||||
|
def percentile(values: list[float], fraction: float) -> float:
|
||||||
|
values = sorted(values)
|
||||||
|
if not values:
|
||||||
|
return 0.0
|
||||||
|
return values[round((len(values) - 1) * fraction)]
|
||||||
|
|
||||||
|
|
||||||
|
def generate(base_url: str, text: str, max_new_tokens: int) -> dict:
|
||||||
|
start = time.perf_counter()
|
||||||
|
response = requests.post(
|
||||||
|
f"{base_url}/generate",
|
||||||
|
json={
|
||||||
|
"text": text,
|
||||||
|
"sampling_params": {
|
||||||
|
"max_new_tokens": max_new_tokens,
|
||||||
|
"temperature": 0,
|
||||||
|
"ignore_eos": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
timeout=600,
|
||||||
|
)
|
||||||
|
elapsed = time.perf_counter() - start
|
||||||
|
response.raise_for_status()
|
||||||
|
body = response.json()
|
||||||
|
meta = body["meta_info"]
|
||||||
|
prefill_finished_time = meta.get("prefill_finished_time")
|
||||||
|
forward_entry_time = meta.get("forward_entry_time")
|
||||||
|
return {
|
||||||
|
"prompt_tokens": meta["prompt_tokens"],
|
||||||
|
"cached_tokens": meta["cached_tokens"],
|
||||||
|
"e2e_latency_s": meta["e2e_latency"],
|
||||||
|
"client_latency_s": elapsed,
|
||||||
|
"prefill_latency_s": (
|
||||||
|
prefill_finished_time - forward_entry_time
|
||||||
|
if prefill_finished_time is not None and forward_entry_time is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"completion_tokens": meta["completion_tokens"],
|
||||||
|
"num_retractions": meta["num_retractions"],
|
||||||
|
"output_ids": body["output_ids"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def text_with_target_tokens(tokenizer, seed: str, target: int) -> str:
|
||||||
|
"""Create deterministic text whose tokenized length is close to ``target``."""
|
||||||
|
repeated = (seed + " ") * target
|
||||||
|
token_ids = tokenizer.encode(repeated, add_special_tokens=False)[:target]
|
||||||
|
return tokenizer.decode(token_ids, skip_special_tokens=True)
|
||||||
|
|
||||||
|
|
||||||
|
def summarize_probe(probes: list[dict], batch_wall_latency_s: float) -> dict[str, Any]:
|
||||||
|
cached = [item["cached_tokens"] for item in probes]
|
||||||
|
e2e = [item["e2e_latency_s"] for item in probes]
|
||||||
|
client = [item["client_latency_s"] for item in probes]
|
||||||
|
prefill = [
|
||||||
|
item["prefill_latency_s"]
|
||||||
|
for item in probes
|
||||||
|
if item["prefill_latency_s"] is not None
|
||||||
|
]
|
||||||
|
completion_tokens = sum(item["completion_tokens"] for item in probes)
|
||||||
|
return {
|
||||||
|
"cached_prefixes": sum(value > 0 for value in cached),
|
||||||
|
"cache_survival_rate": sum(value > 0 for value in cached) / len(cached),
|
||||||
|
"total_cached_tokens": sum(cached),
|
||||||
|
"mean_cached_tokens": statistics.mean(cached),
|
||||||
|
"cached_tokens": cached,
|
||||||
|
"mean_e2e_latency_s": statistics.mean(e2e),
|
||||||
|
"p95_e2e_latency_s": percentile(e2e, 0.95),
|
||||||
|
"mean_client_latency_s": statistics.mean(client),
|
||||||
|
"p95_client_latency_s": percentile(client, 0.95),
|
||||||
|
"mean_prefill_latency_s": statistics.mean(prefill) if prefill else None,
|
||||||
|
"p95_prefill_latency_s": percentile(prefill, 0.95) if prefill else None,
|
||||||
|
"batch_wall_latency_s": batch_wall_latency_s,
|
||||||
|
"request_throughput_rps": len(probes) / batch_wall_latency_s,
|
||||||
|
"output_throughput_tps": completion_tokens / batch_wall_latency_s,
|
||||||
|
"completion_tokens": completion_tokens,
|
||||||
|
"total_retractions": sum(item["num_retractions"] for item in probes),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--base-url", default="http://127.0.0.1:30000")
|
||||||
|
parser.add_argument("--model-path", default="Qwen/Qwen3.5-0.8B")
|
||||||
|
parser.add_argument("--label", required=True)
|
||||||
|
parser.add_argument("--output", type=Path, required=True)
|
||||||
|
parser.add_argument("--warm-prefixes", type=int, default=28)
|
||||||
|
parser.add_argument("--prefix-len", type=int, default=400)
|
||||||
|
parser.add_argument("--pressure-len", type=int, default=7000)
|
||||||
|
parser.add_argument("--output-len", type=int, default=1)
|
||||||
|
parser.add_argument("--probe-output-len", type=int)
|
||||||
|
parser.add_argument("--probe-concurrency", type=int, default=1)
|
||||||
|
parser.add_argument("--burst-target-index", type=int)
|
||||||
|
parser.add_argument("--burst-requests", type=int, default=30)
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.probe_concurrency < 1:
|
||||||
|
parser.error("--probe-concurrency must be at least 1")
|
||||||
|
if args.burst_requests < 1:
|
||||||
|
parser.error("--burst-requests must be at least 1")
|
||||||
|
if args.burst_target_index is not None and not (
|
||||||
|
0 <= args.burst_target_index < args.warm_prefixes
|
||||||
|
):
|
||||||
|
parser.error("--burst-target-index must select a warm prefix")
|
||||||
|
probe_output_len = args.probe_output_len or args.output_len
|
||||||
|
tokenizer = AutoTokenizer.from_pretrained(args.model_path)
|
||||||
|
|
||||||
|
prompt_pairs = []
|
||||||
|
prompt_bases = []
|
||||||
|
for index in range(args.warm_prefixes):
|
||||||
|
base = text_with_target_tokens(
|
||||||
|
tokenizer,
|
||||||
|
f"stable reusable unified cache prefix group {index}",
|
||||||
|
args.prefix_len - 16,
|
||||||
|
)
|
||||||
|
prompt_bases.append(base)
|
||||||
|
prompt_pairs.append(
|
||||||
|
(
|
||||||
|
base + f" warm suffix for group {index}",
|
||||||
|
base + f" replay suffix for group {index}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pressure_text = text_with_target_tokens(
|
||||||
|
tokenizer, "distinct long allocation pressure payload", args.pressure_len
|
||||||
|
)
|
||||||
|
|
||||||
|
requests.post(f"{args.base_url}/flush_cache", timeout=60).raise_for_status()
|
||||||
|
|
||||||
|
# Exclude server startup and first-request kernel initialization.
|
||||||
|
generate(args.base_url, "server kernel warmup " * 32, args.output_len)
|
||||||
|
requests.post(f"{args.base_url}/flush_cache", timeout=60).raise_for_status()
|
||||||
|
|
||||||
|
warm = []
|
||||||
|
for warm_text, _ in prompt_pairs:
|
||||||
|
warm.append(
|
||||||
|
generate(
|
||||||
|
args.base_url,
|
||||||
|
warm_text,
|
||||||
|
args.output_len,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# A distinct long request forces the FULL side toward the Mamba frontier.
|
||||||
|
pressure = generate(
|
||||||
|
args.base_url,
|
||||||
|
pressure_text,
|
||||||
|
args.output_len,
|
||||||
|
)
|
||||||
|
|
||||||
|
if args.burst_target_index is None:
|
||||||
|
probe_indices = list(reversed(range(len(prompt_pairs))))
|
||||||
|
probe_texts = [prompt_pairs[index][1] for index in probe_indices]
|
||||||
|
else:
|
||||||
|
probe_indices = [args.burst_target_index] * args.burst_requests
|
||||||
|
target_base = prompt_bases[args.burst_target_index]
|
||||||
|
probe_texts = [
|
||||||
|
target_base + f" concurrent burst replay suffix request {ordinal}"
|
||||||
|
for ordinal in range(args.burst_requests)
|
||||||
|
]
|
||||||
|
|
||||||
|
def run_probe(replay_text: str) -> dict:
|
||||||
|
return generate(
|
||||||
|
args.base_url,
|
||||||
|
replay_text,
|
||||||
|
probe_output_len,
|
||||||
|
)
|
||||||
|
|
||||||
|
probe_start = time.perf_counter()
|
||||||
|
if args.probe_concurrency == 1:
|
||||||
|
probes = [run_probe(replay_text) for replay_text in probe_texts]
|
||||||
|
else:
|
||||||
|
with ThreadPoolExecutor(max_workers=args.probe_concurrency) as executor:
|
||||||
|
probes = list(executor.map(run_probe, probe_texts))
|
||||||
|
probe_wall_latency_s = time.perf_counter() - probe_start
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"label": args.label,
|
||||||
|
"config": {
|
||||||
|
"warm_prefixes": args.warm_prefixes,
|
||||||
|
"prefix_len": args.prefix_len,
|
||||||
|
"pressure_len": args.pressure_len,
|
||||||
|
"output_len": args.output_len,
|
||||||
|
"probe_output_len": probe_output_len,
|
||||||
|
"probe_concurrency": args.probe_concurrency,
|
||||||
|
"burst_target_index": args.burst_target_index,
|
||||||
|
"burst_requests": (
|
||||||
|
args.burst_requests if args.burst_target_index is not None else None
|
||||||
|
),
|
||||||
|
"actual_warm_prompt_tokens": [item["prompt_tokens"] for item in warm],
|
||||||
|
},
|
||||||
|
"warm": {
|
||||||
|
"total_cached_tokens": sum(item["cached_tokens"] for item in warm),
|
||||||
|
"total_retractions": sum(item["num_retractions"] for item in warm),
|
||||||
|
},
|
||||||
|
"warm_requests": warm,
|
||||||
|
"pressure": pressure,
|
||||||
|
"probe": summarize_probe(probes, probe_wall_latency_s),
|
||||||
|
"probe_requests": probes,
|
||||||
|
"probe_indices": probe_indices,
|
||||||
|
"output_ids": {
|
||||||
|
"warm": [item["output_ids"] for item in warm],
|
||||||
|
"pressure": pressure["output_ids"],
|
||||||
|
"probe": [item["output_ids"] for item in probes],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
args.output.write_text(json.dumps(result, indent=2) + "\n")
|
||||||
|
print(json.dumps(result["probe"], indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -427,7 +427,9 @@ class DecodePreallocQueue(DecodeHiCachePreallocMixin):
|
|||||||
required = ceil_align(swa_tail_len, page_size)
|
required = ceil_align(swa_tail_len, page_size)
|
||||||
available = self.token_to_kv_pool_allocator.swa_available_size()
|
available = self.token_to_kv_pool_allocator.swa_available_size()
|
||||||
if available < required:
|
if available < required:
|
||||||
self.tree_cache.evict(EvictParams(swa_num_tokens=required - available))
|
self.tree_cache.evict_for_alloc(
|
||||||
|
EvictParams(swa_num_tokens=required - available)
|
||||||
|
)
|
||||||
available = self.token_to_kv_pool_allocator.swa_available_size()
|
available = self.token_to_kv_pool_allocator.swa_available_size()
|
||||||
|
|
||||||
if available < required:
|
if available < required:
|
||||||
@@ -1789,7 +1791,9 @@ class DecodePreallocQueue(DecodeHiCachePreallocMixin):
|
|||||||
and self._radix_full_available() < required_alloc_tokens
|
and self._radix_full_available() < required_alloc_tokens
|
||||||
):
|
):
|
||||||
num_to_evict = required_alloc_tokens - self._radix_full_available()
|
num_to_evict = required_alloc_tokens - self._radix_full_available()
|
||||||
result = self.tree_cache.evict(EvictParams(num_tokens=num_to_evict))
|
result = self.tree_cache.evict_for_alloc(
|
||||||
|
EvictParams(num_tokens=num_to_evict)
|
||||||
|
)
|
||||||
if self._radix_full_available() < required_alloc_tokens:
|
if self._radix_full_available() < required_alloc_tokens:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Eviction insufficient: needed {required_alloc_tokens} tokens, "
|
f"Eviction insufficient: needed {required_alloc_tokens} tokens, "
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ storing model-agnostic native cache snapshots.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Iterable, Optional
|
from typing import Any, Iterable, Iterator, Optional
|
||||||
|
|
||||||
import mlx.core as mx
|
import mlx.core as mx
|
||||||
import torch
|
import torch
|
||||||
@@ -95,6 +95,7 @@ class MlxAuxiliaryStatePool:
|
|||||||
self.mamba_cache = None
|
self.mamba_cache = None
|
||||||
self.mem_usage = 0
|
self.mem_usage = 0
|
||||||
self._snapshots: dict[int, dict[int, _CacheSnapshot]] = {}
|
self._snapshots: dict[int, dict[int, _CacheSnapshot]] = {}
|
||||||
|
self._alloc_iter: Optional[Iterator[torch.Tensor]] = None
|
||||||
self.clear()
|
self.clear()
|
||||||
|
|
||||||
def _tensor(self, indices: Any) -> torch.Tensor:
|
def _tensor(self, indices: Any) -> torch.Tensor:
|
||||||
@@ -108,7 +109,31 @@ class MlxAuxiliaryStatePool:
|
|||||||
def available_size(self) -> int:
|
def available_size(self) -> int:
|
||||||
return int(self.free_slots.numel())
|
return int(self.free_slots.numel())
|
||||||
|
|
||||||
|
def schedulable_available_size(self) -> int:
|
||||||
|
return self.available_size()
|
||||||
|
|
||||||
|
def alloc_group_begin(self, num_reqs: int) -> None:
|
||||||
|
self._alloc_iter = None
|
||||||
|
if num_reqs > 0:
|
||||||
|
slots = self._do_alloc(num_reqs)
|
||||||
|
if slots is not None:
|
||||||
|
self._alloc_iter = iter(slots.split(1))
|
||||||
|
|
||||||
|
def alloc_group_end(self) -> None:
|
||||||
|
if self._alloc_iter is not None:
|
||||||
|
remaining = list(self._alloc_iter)
|
||||||
|
if remaining:
|
||||||
|
self.free(torch.cat(remaining))
|
||||||
|
self._alloc_iter = None
|
||||||
|
|
||||||
def alloc(self, need_size: int) -> Optional[torch.Tensor]:
|
def alloc(self, need_size: int) -> Optional[torch.Tensor]:
|
||||||
|
if self._alloc_iter is not None and need_size == 1:
|
||||||
|
slot = next(self._alloc_iter, None)
|
||||||
|
if slot is not None:
|
||||||
|
return slot
|
||||||
|
return self._do_alloc(need_size)
|
||||||
|
|
||||||
|
def _do_alloc(self, need_size: int) -> Optional[torch.Tensor]:
|
||||||
if need_size > self.available_size():
|
if need_size > self.available_size():
|
||||||
return None
|
return None
|
||||||
slots = self.free_slots[:need_size].clone()
|
slots = self.free_slots[:need_size].clone()
|
||||||
@@ -128,6 +153,7 @@ class MlxAuxiliaryStatePool:
|
|||||||
self.free_slots = torch.cat([self.free_slots, indices])
|
self.free_slots = torch.cat([self.free_slots, indices])
|
||||||
|
|
||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
|
self._alloc_iter = None
|
||||||
self.free_slots = torch.arange(
|
self.free_slots = torch.arange(
|
||||||
1, self.size + 1, dtype=torch.int64, device=self.device
|
1, self.size + 1, dtype=torch.int64, device=self.device
|
||||||
)
|
)
|
||||||
@@ -227,6 +253,7 @@ class MlxAuxiliaryStateReqToTokenPool(ReqToTokenPool):
|
|||||||
size=auxiliary_state_size,
|
size=auxiliary_state_size,
|
||||||
device=device,
|
device=device,
|
||||||
)
|
)
|
||||||
|
self.mamba_allocator = self.mamba_pool
|
||||||
# The unified radix base MAMBA component still reads ``mamba_pool``.
|
# The unified radix base MAMBA component still reads ``mamba_pool``.
|
||||||
# Keep the MLX-owned name beside it so local code can avoid model-
|
# Keep the MLX-owned name beside it so local code can avoid model-
|
||||||
# specific terminology.
|
# specific terminology.
|
||||||
@@ -352,7 +379,7 @@ class MlxAuxiliaryStateComponent(MambaComponent):
|
|||||||
source_value
|
source_value
|
||||||
)
|
)
|
||||||
if forked_value is None:
|
if forked_value is None:
|
||||||
self.cache.evict(EvictParams(num_tokens=0, mamba_num=1))
|
self.cache.evict_for_alloc(EvictParams(num_tokens=0, mamba_num=1))
|
||||||
forked_value = (
|
forked_value = (
|
||||||
self.cache.req_to_token_pool.auxiliary_state_pool.fork_from(
|
self.cache.req_to_token_pool.auxiliary_state_pool.fork_from(
|
||||||
source_value
|
source_value
|
||||||
|
|||||||
@@ -257,7 +257,9 @@ def alloc_req_slots(
|
|||||||
if mamba_available_size < mamba_state_needed:
|
if mamba_available_size < mamba_state_needed:
|
||||||
if tree_cache is not None and tree_cache.supports_mamba():
|
if tree_cache is not None and tree_cache.supports_mamba():
|
||||||
mamba_num = max(0, mamba_state_needed - mamba_available_size)
|
mamba_num = max(0, mamba_state_needed - mamba_available_size)
|
||||||
tree_cache.evict(EvictParams(num_tokens=0, mamba_num=mamba_num))
|
tree_cache.evict_for_alloc(
|
||||||
|
EvictParams(num_tokens=0, mamba_num=mamba_num)
|
||||||
|
)
|
||||||
req_pool_indices = req_to_token_pool.alloc(reqs)
|
req_pool_indices = req_to_token_pool.alloc(reqs)
|
||||||
if req_pool_indices is None:
|
if req_pool_indices is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
|
|||||||
@@ -324,6 +324,16 @@ class BasePrefixCache(ABC, PrefixCacheTrait):
|
|||||||
def evict(self, params: EvictParams) -> EvictResult:
|
def evict(self, params: EvictParams) -> EvictResult:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def evict_for_alloc(self, params: EvictParams) -> EvictResult:
|
||||||
|
"""Evict cache entries to cover allocator shortfalls.
|
||||||
|
|
||||||
|
The default implementation preserves the component-count semantics of
|
||||||
|
:meth:`evict`. Multi-component caches backed by shared memory can
|
||||||
|
override this entry point to stop once collateral frees make the
|
||||||
|
requested allocation feasible.
|
||||||
|
"""
|
||||||
|
return self.evict(params)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def inc_lock_ref(self, node: Any) -> IncLockRefResult:
|
def inc_lock_ref(self, node: Any) -> IncLockRefResult:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -832,8 +832,12 @@ class BufferModePipeline:
|
|||||||
avail = cache.token_to_kv_pool_allocator.available_size()
|
avail = cache.token_to_kv_pool_allocator.available_size()
|
||||||
if avail < f.num_tokens:
|
if avail < f.num_tokens:
|
||||||
needed = f.num_tokens - avail
|
needed = f.num_tokens - avail
|
||||||
evicted = cache.evict(EvictParams(num_tokens=needed))
|
cache.evict_for_alloc(EvictParams(num_tokens=needed))
|
||||||
if evicted.num_tokens_evicted < needed:
|
if cache.supports_swa():
|
||||||
|
avail = cache.token_to_kv_pool_allocator.full_available_size()
|
||||||
|
else:
|
||||||
|
avail = cache.token_to_kv_pool_allocator.available_size()
|
||||||
|
if avail < f.num_tokens:
|
||||||
# Genuinely no room (locked pages): recompute.
|
# Genuinely no room (locked pages): recompute.
|
||||||
return _drop()
|
return _drop()
|
||||||
|
|
||||||
|
|||||||
@@ -130,14 +130,16 @@ def evict_from_tree_cache(tree_cache: BasePrefixCache | None, num_tokens: int):
|
|||||||
if full_available_size < num_tokens or swa_available_size < num_tokens:
|
if full_available_size < num_tokens or swa_available_size < num_tokens:
|
||||||
full_num_tokens = max(0, num_tokens - full_available_size)
|
full_num_tokens = max(0, num_tokens - full_available_size)
|
||||||
swa_num_tokens = max(0, num_tokens - swa_available_size)
|
swa_num_tokens = max(0, num_tokens - swa_available_size)
|
||||||
tree_cache.evict(
|
tree_cache.evict_for_alloc(
|
||||||
EvictParams(num_tokens=full_num_tokens, swa_num_tokens=swa_num_tokens)
|
EvictParams(num_tokens=full_num_tokens, swa_num_tokens=swa_num_tokens)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Standard allocator: evict only the shortfall (mirrors the SWA arm)
|
# Standard allocator: evict only the shortfall (mirrors the SWA arm)
|
||||||
available_size = allocator.available_size()
|
available_size = allocator.available_size()
|
||||||
if available_size < num_tokens:
|
if available_size < num_tokens:
|
||||||
tree_cache.evict(EvictParams(num_tokens=num_tokens - available_size))
|
tree_cache.evict_for_alloc(
|
||||||
|
EvictParams(num_tokens=num_tokens - available_size)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def retraction_backup(
|
def retraction_backup(
|
||||||
|
|||||||
@@ -48,6 +48,26 @@ def _get_allocator_type(server_args: ServerArgs) -> str:
|
|||||||
return get_allocator_type(server_args)
|
return get_allocator_type(server_args)
|
||||||
|
|
||||||
|
|
||||||
|
def _evict_swa_for_device_alloc(cache: UnifiedRadixCache, required_size: int) -> None:
|
||||||
|
from sglang.srt.mem_cache.base_prefix_cache import EvictParams
|
||||||
|
|
||||||
|
available_size = cache.token_to_kv_pool_allocator.swa_available_size()
|
||||||
|
shortfall = max(0, required_size - available_size)
|
||||||
|
if shortfall > 0:
|
||||||
|
cache.evict_for_alloc(EvictParams(swa_num_tokens=shortfall))
|
||||||
|
|
||||||
|
|
||||||
|
def _evict_mamba_for_device_alloc(cache: UnifiedRadixCache, required_size: int) -> None:
|
||||||
|
from sglang.srt.mem_cache.base_prefix_cache import EvictParams
|
||||||
|
|
||||||
|
available_size = (
|
||||||
|
cache.req_to_token_pool.mamba_allocator.schedulable_available_size()
|
||||||
|
)
|
||||||
|
shortfall = max(0, required_size - available_size)
|
||||||
|
if shortfall > 0:
|
||||||
|
cache.evict_for_alloc(EvictParams(mamba_num=shortfall))
|
||||||
|
|
||||||
|
|
||||||
def _make_layer_mapper(
|
def _make_layer_mapper(
|
||||||
layer_mapping: dict[int, int],
|
layer_mapping: dict[int, int],
|
||||||
transfer_layer_num: int,
|
transfer_layer_num: int,
|
||||||
@@ -1210,8 +1230,6 @@ class _DeepSeekV4Strategy(StackStrategy):
|
|||||||
model_name=None,
|
model_name=None,
|
||||||
enable_storage_metrics=False,
|
enable_storage_metrics=False,
|
||||||
):
|
):
|
||||||
from sglang.srt.mem_cache.base_prefix_cache import EvictParams
|
|
||||||
|
|
||||||
host_pool_group, cache_controller = build_deepseek_v4_hicache_stack(
|
host_pool_group, cache_controller = build_deepseek_v4_hicache_stack(
|
||||||
params=params,
|
params=params,
|
||||||
server_args=server_args,
|
server_args=server_args,
|
||||||
@@ -1219,7 +1237,7 @@ class _DeepSeekV4Strategy(StackStrategy):
|
|||||||
load_cache_event=load_cache_event,
|
load_cache_event=load_cache_event,
|
||||||
storage_backend=storage_backend,
|
storage_backend=storage_backend,
|
||||||
host_swa_evict_fn=lambda n: cache.evict_host(n, ComponentType.SWA),
|
host_swa_evict_fn=lambda n: cache.evict_host(n, ComponentType.SWA),
|
||||||
device_swa_evict_fn=lambda n: cache.evict(EvictParams(swa_num_tokens=n)),
|
device_swa_evict_fn=lambda n: _evict_swa_for_device_alloc(cache, n),
|
||||||
prefetch_threshold=prefetch_threshold,
|
prefetch_threshold=prefetch_threshold,
|
||||||
model_name=model_name,
|
model_name=model_name,
|
||||||
storage_backend_extra_config=storage_backend_extra_config,
|
storage_backend_extra_config=storage_backend_extra_config,
|
||||||
@@ -1286,8 +1304,6 @@ class _MambaStrategy(StackStrategy):
|
|||||||
model_name=None,
|
model_name=None,
|
||||||
enable_storage_metrics=False,
|
enable_storage_metrics=False,
|
||||||
):
|
):
|
||||||
from sglang.srt.mem_cache.base_prefix_cache import EvictParams
|
|
||||||
|
|
||||||
full_layer_mapping = dict(kvcache.full_attention_layer_id_mapping)
|
full_layer_mapping = dict(kvcache.full_attention_layer_id_mapping)
|
||||||
mamba_layer_mapping = dict(params.req_to_token_pool.mamba_map)
|
mamba_layer_mapping = dict(params.req_to_token_pool.mamba_map)
|
||||||
host_pool_group, cache_controller = build_hybrid_mamba_stack(
|
host_pool_group, cache_controller = build_hybrid_mamba_stack(
|
||||||
@@ -1301,7 +1317,7 @@ class _MambaStrategy(StackStrategy):
|
|||||||
storage_backend=storage_backend,
|
storage_backend=storage_backend,
|
||||||
use_mla=kvcache.use_mla,
|
use_mla=kvcache.use_mla,
|
||||||
host_mamba_evict_fn=lambda n: cache.evict_host(n, ComponentType.MAMBA),
|
host_mamba_evict_fn=lambda n: cache.evict_host(n, ComponentType.MAMBA),
|
||||||
device_mamba_evict_fn=lambda n: cache.evict(EvictParams(mamba_num=n)),
|
device_mamba_evict_fn=lambda n: _evict_mamba_for_device_alloc(cache, n),
|
||||||
prefetch_threshold=prefetch_threshold,
|
prefetch_threshold=prefetch_threshold,
|
||||||
model_name=model_name,
|
model_name=model_name,
|
||||||
storage_backend_extra_config=storage_backend_extra_config,
|
storage_backend_extra_config=storage_backend_extra_config,
|
||||||
@@ -1355,8 +1371,6 @@ class _SwaStrategy(StackStrategy):
|
|||||||
model_name=None,
|
model_name=None,
|
||||||
enable_storage_metrics=False,
|
enable_storage_metrics=False,
|
||||||
):
|
):
|
||||||
from sglang.srt.mem_cache.base_prefix_cache import EvictParams
|
|
||||||
|
|
||||||
full_layer_mapping, swa_layer_mapping = _swa_layer_mappings(kvcache)
|
full_layer_mapping, swa_layer_mapping = _swa_layer_mappings(kvcache)
|
||||||
host_pool_group, cache_controller = build_hybrid_swa_stack(
|
host_pool_group, cache_controller = build_hybrid_swa_stack(
|
||||||
params=params,
|
params=params,
|
||||||
@@ -1369,7 +1383,7 @@ class _SwaStrategy(StackStrategy):
|
|||||||
storage_backend=storage_backend,
|
storage_backend=storage_backend,
|
||||||
use_mla=False,
|
use_mla=False,
|
||||||
host_swa_evict_fn=lambda n: cache.evict_host(n, ComponentType.SWA),
|
host_swa_evict_fn=lambda n: cache.evict_host(n, ComponentType.SWA),
|
||||||
device_swa_evict_fn=lambda n: cache.evict(EvictParams(swa_num_tokens=n)),
|
device_swa_evict_fn=lambda n: _evict_swa_for_device_alloc(cache, n),
|
||||||
prefetch_threshold=prefetch_threshold,
|
prefetch_threshold=prefetch_threshold,
|
||||||
model_name=model_name,
|
model_name=model_name,
|
||||||
storage_backend_extra_config=storage_backend_extra_config,
|
storage_backend_extra_config=storage_backend_extra_config,
|
||||||
@@ -1417,8 +1431,6 @@ class _MambaSwaStrategy(StackStrategy):
|
|||||||
model_name=None,
|
model_name=None,
|
||||||
enable_storage_metrics=False,
|
enable_storage_metrics=False,
|
||||||
):
|
):
|
||||||
from sglang.srt.mem_cache.base_prefix_cache import EvictParams
|
|
||||||
|
|
||||||
full_layer_mapping, swa_layer_mapping = _swa_layer_mappings(kvcache)
|
full_layer_mapping, swa_layer_mapping = _swa_layer_mappings(kvcache)
|
||||||
mamba_layer_mapping = dict(params.req_to_token_pool.mamba_map)
|
mamba_layer_mapping = dict(params.req_to_token_pool.mamba_map)
|
||||||
host_pool_group, cache_controller = build_hybrid_mamba_swa_stack(
|
host_pool_group, cache_controller = build_hybrid_mamba_swa_stack(
|
||||||
@@ -1438,9 +1450,9 @@ class _MambaSwaStrategy(StackStrategy):
|
|||||||
pp_group=params.pp_cache_group,
|
pp_group=params.pp_cache_group,
|
||||||
storage_backend=storage_backend,
|
storage_backend=storage_backend,
|
||||||
host_swa_evict_fn=lambda n: cache.evict_host(n, ComponentType.SWA),
|
host_swa_evict_fn=lambda n: cache.evict_host(n, ComponentType.SWA),
|
||||||
device_swa_evict_fn=lambda n: cache.evict(EvictParams(swa_num_tokens=n)),
|
device_swa_evict_fn=lambda n: _evict_swa_for_device_alloc(cache, n),
|
||||||
host_mamba_evict_fn=lambda n: cache.evict_host(n, ComponentType.MAMBA),
|
host_mamba_evict_fn=lambda n: cache.evict_host(n, ComponentType.MAMBA),
|
||||||
device_mamba_evict_fn=lambda n: cache.evict(EvictParams(mamba_num=n)),
|
device_mamba_evict_fn=lambda n: _evict_mamba_for_device_alloc(cache, n),
|
||||||
prefetch_threshold=prefetch_threshold,
|
prefetch_threshold=prefetch_threshold,
|
||||||
model_name=model_name,
|
model_name=model_name,
|
||||||
storage_backend_extra_config=storage_backend_extra_config,
|
storage_backend_extra_config=storage_backend_extra_config,
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ class PoolEntry:
|
|||||||
device_pool: Any
|
device_pool: Any
|
||||||
layer_mapper: Callable[[int], int | None]
|
layer_mapper: Callable[[int], int | None]
|
||||||
is_primary_index_anchor: bool = False
|
is_primary_index_anchor: bool = False
|
||||||
|
# Reclaim callbacks receive the absolute allocation size n. The host
|
||||||
|
# callback evicts n slots; the device callback makes alloc(n) feasible.
|
||||||
host_evict_fn: Callable[[int], Any] | None = None
|
host_evict_fn: Callable[[int], Any] | None = None
|
||||||
device_evict_fn: Callable[[int], Any] | None = None
|
device_evict_fn: Callable[[int], Any] | None = None
|
||||||
device_alloc_fn: Callable[[int], Any] | None = None
|
device_alloc_fn: Callable[[int], Any] | None = None
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ class MambaComponent(TreeComponent):
|
|||||||
# stops at this request's window boundary instead of walking to
|
# stops at this request's window boundary instead of walking to
|
||||||
# root and over-decrementing locks held by other requests.
|
# root and over-decrementing locks held by other requests.
|
||||||
lock_result = self.cache.inc_lock_ref(result.best_match_node)
|
lock_result = self.cache.inc_lock_ref(result.best_match_node)
|
||||||
self.cache.evict(EvictParams(num_tokens=0, mamba_num=1))
|
self.cache.evict_for_alloc(EvictParams(num_tokens=0, mamba_num=1))
|
||||||
dst_index = self.cache.req_to_token_pool.mamba_allocator.alloc(1)
|
dst_index = self.cache.req_to_token_pool.mamba_allocator.alloc(1)
|
||||||
self.cache.dec_lock_ref(
|
self.cache.dec_lock_ref(
|
||||||
result.best_match_node, lock_result.to_dec_params()
|
result.best_match_node, lock_result.to_dec_params()
|
||||||
@@ -374,10 +374,14 @@ class MambaComponent(TreeComponent):
|
|||||||
device_frees: dict[ComponentType, list[torch.Tensor]],
|
device_frees: dict[ComponentType, list[torch.Tensor]],
|
||||||
host_frees: dict[ComponentType, list[torch.Tensor]],
|
host_frees: dict[ComponentType, list[torch.Tensor]],
|
||||||
) -> Optional[NodeId]:
|
) -> Optional[NodeId]:
|
||||||
"""Return the next device-leaf node for the driver to evict, or None.
|
"""Advance one device-eviction step and return a leaf, if selected.
|
||||||
Internal nodes are tombstoned inline (no IO). If the previous node's
|
|
||||||
eviction removed the cursor, the walk resumes from the partition
|
An internal tombstone is one complete step so the caller can apply its
|
||||||
sentinel with session refs on, else it restarts at the LRU tail."""
|
pending frees and recheck allocator capacity before the next mutation.
|
||||||
|
If the previous node's eviction removed the cursor, the walk resumes
|
||||||
|
from the partition sentinel with session refs on, else it restarts at
|
||||||
|
the LRU tail.
|
||||||
|
"""
|
||||||
ct = self.component_type
|
ct = self.component_type
|
||||||
lru = self.tree_core.lru_lists[ct]
|
lru = self.tree_core.lru_lists[ct]
|
||||||
enabled = self.tree_core.enable_session_radix_cache
|
enabled = self.tree_core.enable_session_radix_cache
|
||||||
@@ -387,34 +391,36 @@ class MambaComponent(TreeComponent):
|
|||||||
self._evict_device_cursor = (
|
self._evict_device_cursor = (
|
||||||
lru.cursor_next() if enabled else lru.get_lru_no_lock()
|
lru.cursor_next() if enabled else lru.get_lru_no_lock()
|
||||||
)
|
)
|
||||||
while (
|
if (
|
||||||
tracker[ct] < self._evict_device_request_cnt
|
tracker[ct] >= self._evict_device_request_cnt
|
||||||
and self._evict_device_cursor is not None
|
or self._evict_device_cursor is None
|
||||||
and lru.in_list(self._evict_device_cursor)
|
or not lru.in_list(self._evict_device_cursor)
|
||||||
):
|
):
|
||||||
x = self._evict_device_cursor
|
return None
|
||||||
assert x.component_data[ct].value is not None
|
|
||||||
if x in self.tree_core.evictable_device_leaves and (
|
x = self._evict_device_cursor
|
||||||
not enabled or self._can_evict_leaf_atomically(x)
|
assert x.component_data[ct].value is not None
|
||||||
):
|
if x in self.tree_core.evictable_device_leaves and (
|
||||||
self._evict_device_cursor = (
|
not enabled or self._can_evict_leaf_atomically(x)
|
||||||
lru.cursor_next() if enabled else lru.get_prev_no_lock(x)
|
):
|
||||||
)
|
self._evict_device_cursor = (
|
||||||
return x.id
|
lru.cursor_next() if enabled else lru.get_prev_no_lock(x)
|
||||||
if not enabled:
|
|
||||||
x_next = lru.get_prev_no_lock(x)
|
|
||||||
self.tree_core._evict_component_and_detach_lru(
|
|
||||||
x,
|
|
||||||
self,
|
|
||||||
target=EvictLayer.DEVICE,
|
|
||||||
tracker=tracker,
|
|
||||||
device_frees=device_frees,
|
|
||||||
host_frees=host_frees,
|
|
||||||
)
|
)
|
||||||
self.tree_core._cascade_evict(
|
return x.id
|
||||||
x, self, tracker, device_frees=device_frees, host_frees=host_frees
|
if not enabled:
|
||||||
)
|
x_next = lru.get_prev_no_lock(x)
|
||||||
self._evict_device_cursor = lru.cursor_next() if enabled else x_next
|
self.tree_core._evict_component_and_detach_lru(
|
||||||
|
x,
|
||||||
|
self,
|
||||||
|
target=EvictLayer.DEVICE,
|
||||||
|
tracker=tracker,
|
||||||
|
device_frees=device_frees,
|
||||||
|
host_frees=host_frees,
|
||||||
|
)
|
||||||
|
self.tree_core._cascade_evict(
|
||||||
|
x, self, tracker, device_frees=device_frees, host_frees=host_frees
|
||||||
|
)
|
||||||
|
self._evict_device_cursor = lru.cursor_next() if enabled else x_next
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _evict_device_end(self) -> None:
|
def _evict_device_end(self) -> None:
|
||||||
@@ -487,7 +493,7 @@ class MambaComponent(TreeComponent):
|
|||||||
"""Allocate one mamba pool slot, evicting if necessary."""
|
"""Allocate one mamba pool slot, evicting if necessary."""
|
||||||
slot = self.cache.req_to_token_pool.mamba_allocator.alloc(1)
|
slot = self.cache.req_to_token_pool.mamba_allocator.alloc(1)
|
||||||
if slot is None:
|
if slot is None:
|
||||||
self.cache.evict(EvictParams(num_tokens=0, mamba_num=1))
|
self.cache.evict_for_alloc(EvictParams(num_tokens=0, mamba_num=1))
|
||||||
slot = self.cache.req_to_token_pool.mamba_allocator.alloc(1)
|
slot = self.cache.req_to_token_pool.mamba_allocator.alloc(1)
|
||||||
assert slot is not None, "Can not alloc mamba cache"
|
assert slot is not None, "Can not alloc mamba cache"
|
||||||
return slot
|
return slot
|
||||||
@@ -660,7 +666,7 @@ class MambaComponent(TreeComponent):
|
|||||||
return PrepareLoadBackResult()
|
return PrepareLoadBackResult()
|
||||||
dst = self.cache.req_to_token_pool.mamba_allocator.alloc(1)
|
dst = self.cache.req_to_token_pool.mamba_allocator.alloc(1)
|
||||||
if dst is None:
|
if dst is None:
|
||||||
self.cache.evict(EvictParams(num_tokens=0, mamba_num=1))
|
self.cache.evict_for_alloc(EvictParams(num_tokens=0, mamba_num=1))
|
||||||
dst = self.cache.req_to_token_pool.mamba_allocator.alloc(1)
|
dst = self.cache.req_to_token_pool.mamba_allocator.alloc(1)
|
||||||
assert dst is not None, "Cannot alloc mamba for load_back"
|
assert dst is not None, "Cannot alloc mamba for load_back"
|
||||||
req.mamba_pool_idx = dst[0]
|
req.mamba_pool_idx = dst[0]
|
||||||
|
|||||||
@@ -535,10 +535,14 @@ class SWAComponent(TreeComponent):
|
|||||||
device_frees: dict[ComponentType, list[torch.Tensor]],
|
device_frees: dict[ComponentType, list[torch.Tensor]],
|
||||||
host_frees: dict[ComponentType, list[torch.Tensor]],
|
host_frees: dict[ComponentType, list[torch.Tensor]],
|
||||||
) -> Optional[NodeId]:
|
) -> Optional[NodeId]:
|
||||||
"""Return the next device-leaf node for the driver to evict, or None.
|
"""Advance one device-eviction step and return a leaf, if selected.
|
||||||
Internal nodes are tombstoned inline (no IO). If the previous node's
|
|
||||||
eviction removed the cursor, the walk resumes from the partition
|
An internal tombstone is one complete step so the caller can apply its
|
||||||
sentinel with session refs on, else it restarts at the LRU tail."""
|
pending frees and recheck allocator capacity before the next mutation.
|
||||||
|
If the previous node's eviction removed the cursor, the walk resumes
|
||||||
|
from the partition sentinel with session refs on, else it restarts at
|
||||||
|
the LRU tail.
|
||||||
|
"""
|
||||||
ct = self.component_type
|
ct = self.component_type
|
||||||
lru = self.tree_core.lru_lists[ct]
|
lru = self.tree_core.lru_lists[ct]
|
||||||
enabled = self.tree_core.enable_session_radix_cache
|
enabled = self.tree_core.enable_session_radix_cache
|
||||||
@@ -548,34 +552,36 @@ class SWAComponent(TreeComponent):
|
|||||||
self._evict_device_cursor = (
|
self._evict_device_cursor = (
|
||||||
lru.cursor_next() if enabled else lru.get_lru_no_lock()
|
lru.cursor_next() if enabled else lru.get_lru_no_lock()
|
||||||
)
|
)
|
||||||
while (
|
if (
|
||||||
tracker[ct] < self._evict_device_request_cnt
|
tracker[ct] >= self._evict_device_request_cnt
|
||||||
and self._evict_device_cursor is not None
|
or self._evict_device_cursor is None
|
||||||
and lru.in_list(self._evict_device_cursor)
|
or not lru.in_list(self._evict_device_cursor)
|
||||||
):
|
):
|
||||||
x = self._evict_device_cursor
|
return None
|
||||||
assert x.component_data[ct].value is not None
|
|
||||||
if x in self.tree_core.evictable_device_leaves and (
|
x = self._evict_device_cursor
|
||||||
not enabled or self._can_evict_leaf_atomically(x)
|
assert x.component_data[ct].value is not None
|
||||||
):
|
if x in self.tree_core.evictable_device_leaves and (
|
||||||
self._evict_device_cursor = (
|
not enabled or self._can_evict_leaf_atomically(x)
|
||||||
lru.cursor_next() if enabled else lru.get_prev_no_lock(x)
|
):
|
||||||
)
|
self._evict_device_cursor = (
|
||||||
return x.id
|
lru.cursor_next() if enabled else lru.get_prev_no_lock(x)
|
||||||
if not enabled:
|
|
||||||
x_next = lru.get_prev_no_lock(x)
|
|
||||||
self.tree_core._evict_component_and_detach_lru(
|
|
||||||
x,
|
|
||||||
self,
|
|
||||||
target=EvictLayer.DEVICE,
|
|
||||||
tracker=tracker,
|
|
||||||
device_frees=device_frees,
|
|
||||||
host_frees=host_frees,
|
|
||||||
)
|
)
|
||||||
self.tree_core._cascade_evict(
|
return x.id
|
||||||
x, self, tracker, device_frees=device_frees, host_frees=host_frees
|
if not enabled:
|
||||||
)
|
x_next = lru.get_prev_no_lock(x)
|
||||||
self._evict_device_cursor = lru.cursor_next() if enabled else x_next
|
self.tree_core._evict_component_and_detach_lru(
|
||||||
|
x,
|
||||||
|
self,
|
||||||
|
target=EvictLayer.DEVICE,
|
||||||
|
tracker=tracker,
|
||||||
|
device_frees=device_frees,
|
||||||
|
host_frees=host_frees,
|
||||||
|
)
|
||||||
|
self.tree_core._cascade_evict(
|
||||||
|
x, self, tracker, device_frees=device_frees, host_frees=host_frees
|
||||||
|
)
|
||||||
|
self._evict_device_cursor = lru.cursor_next() if enabled else x_next
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _evict_device_end(self) -> None:
|
def _evict_device_end(self) -> None:
|
||||||
|
|||||||
@@ -507,8 +507,11 @@ class TreeComponent(ABC):
|
|||||||
device_frees: dict[ComponentType, list[torch.Tensor]],
|
device_frees: dict[ComponentType, list[torch.Tensor]],
|
||||||
host_frees: dict[ComponentType, list[torch.Tensor]],
|
host_frees: dict[ComponentType, list[torch.Tensor]],
|
||||||
) -> Optional[NodeId]:
|
) -> Optional[NodeId]:
|
||||||
"""Return the next device-leaf node for the driver to evict, or None.
|
"""Advance one eviction step and return a device leaf, if selected.
|
||||||
Internal nodes are tombstoned inline (no IO)."""
|
|
||||||
|
Implementations must return after one allocator-relevant internal
|
||||||
|
mutation so the caller can drain pending frees before continuing.
|
||||||
|
"""
|
||||||
assert (
|
assert (
|
||||||
self.is_evict_device_ongoing
|
self.is_evict_device_ongoing
|
||||||
), f"{self.component_type} device eviction not started"
|
), f"{self.component_type} device eviction not started"
|
||||||
@@ -534,7 +537,7 @@ class TreeComponent(ABC):
|
|||||||
device_frees: dict[ComponentType, list[torch.Tensor]],
|
device_frees: dict[ComponentType, list[torch.Tensor]],
|
||||||
host_frees: dict[ComponentType, list[torch.Tensor]],
|
host_frees: dict[ComponentType, list[torch.Tensor]],
|
||||||
) -> Optional[NodeId]:
|
) -> Optional[NodeId]:
|
||||||
"""Advance the walk; return the next device leaf or None."""
|
"""Advance the walk by at most one allocator-relevant mutation."""
|
||||||
...
|
...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|||||||
@@ -1224,7 +1224,7 @@ class UnifiedTreeCore(UnifiedTreeCoreInterface):
|
|||||||
def evict_device_next_node(
|
def evict_device_next_node(
|
||||||
self, component_type: ComponentType, tracker: dict[ComponentType, int]
|
self, component_type: ComponentType, tracker: dict[ComponentType, int]
|
||||||
) -> EvictDeviceNextNodeResult:
|
) -> EvictDeviceNextNodeResult:
|
||||||
"""Return the next device leaf to evict for a component, or None when done."""
|
"""Advance one component eviction step and report whether it progressed."""
|
||||||
result = EvictDeviceNextNodeResult()
|
result = EvictDeviceNextNodeResult()
|
||||||
# The walk reads running totals for its doneness check; the result
|
# The walk reads running totals for its doneness check; the result
|
||||||
# carries only this step's delta.
|
# carries only this step's delta.
|
||||||
@@ -1236,6 +1236,7 @@ class UnifiedTreeCore(UnifiedTreeCoreInterface):
|
|||||||
delta = n - tracker.get(ct, 0)
|
delta = n - tracker.get(ct, 0)
|
||||||
if delta:
|
if delta:
|
||||||
result.tracker[ct] = delta
|
result.tracker[ct] = delta
|
||||||
|
result.made_progress = result.node_id is not None or bool(result.tracker)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def evict_device_end(self, component_type: ComponentType) -> None:
|
def evict_device_end(self, component_type: ComponentType) -> None:
|
||||||
|
|||||||
@@ -41,7 +41,15 @@ class BaseEvictionResult(msgspec.Struct):
|
|||||||
|
|
||||||
|
|
||||||
class EvictDeviceNextNodeResult(BaseEvictionResult):
|
class EvictDeviceNextNodeResult(BaseEvictionResult):
|
||||||
|
"""One device-walk step.
|
||||||
|
|
||||||
|
``node_id`` selects a leaf for the Controller to evict. ``made_progress``
|
||||||
|
also covers an internal tombstone that returned no leaf, distinguishing it
|
||||||
|
from true walk exhaustion.
|
||||||
|
"""
|
||||||
|
|
||||||
node_id: Optional[NodeId] = None
|
node_id: Optional[NodeId] = None
|
||||||
|
made_progress: bool = False
|
||||||
|
|
||||||
|
|
||||||
class EvictDeviceLeafResult(BaseEvictionResult):
|
class EvictDeviceLeafResult(BaseEvictionResult):
|
||||||
@@ -230,8 +238,11 @@ class UnifiedTreeCoreInterface(ABC):
|
|||||||
def evict_device_next_node(
|
def evict_device_next_node(
|
||||||
self, component_type: ComponentType, tracker: dict[ComponentType, int]
|
self, component_type: ComponentType, tracker: dict[ComponentType, int]
|
||||||
) -> EvictDeviceNextNodeResult:
|
) -> EvictDeviceNextNodeResult:
|
||||||
"""The next evictable node (None node_id when the walk is exhausted);
|
"""Advance one eviction step.
|
||||||
tracker is the caller's running totals, read for the doneness check."""
|
|
||||||
|
A missing ``node_id`` is exhausted only when ``made_progress`` is also
|
||||||
|
false. ``tracker`` is the caller's running totals, read for doneness.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|||||||
@@ -531,18 +531,68 @@ class UnifiedRadixCache(BasePrefixCache):
|
|||||||
self._apply_cache_actions(self.tree_core.end_insert())
|
self._apply_cache_actions(self.tree_core.end_insert())
|
||||||
|
|
||||||
def evict(self, params: EvictParams) -> EvictResult:
|
def evict(self, params: EvictParams) -> EvictResult:
|
||||||
|
return self._evict(params)
|
||||||
|
|
||||||
|
def evict_for_alloc(self, params: EvictParams) -> EvictResult:
|
||||||
|
"""Evict until the requested component allocations become feasible.
|
||||||
|
|
||||||
|
``params`` contains allocator shortfalls, not absolute eviction quotas.
|
||||||
|
A component eviction can cascade to its peers; with a shared memory pool,
|
||||||
|
those collateral frees can satisfy the original allocation before the
|
||||||
|
triggering component's requested count is reached.
|
||||||
|
"""
|
||||||
if self.disable:
|
if self.disable:
|
||||||
return EvictResult()
|
return EvictResult()
|
||||||
start_time = time.perf_counter()
|
|
||||||
tracker = {ct: 0 for ct in self.tree_components}
|
|
||||||
|
|
||||||
request_by_type = {
|
request_by_type = self._evict_request_by_type(params)
|
||||||
|
available_size_targets = {
|
||||||
|
ct: self._component_available_size(ct) + request_cnt
|
||||||
|
for ct, request_cnt in request_by_type.items()
|
||||||
|
if request_cnt > 0
|
||||||
|
}
|
||||||
|
return self._evict(params, available_size_targets)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _evict_request_by_type(params: EvictParams) -> dict[ComponentType, int]:
|
||||||
|
return {
|
||||||
ComponentType.FULL: params.num_tokens,
|
ComponentType.FULL: params.num_tokens,
|
||||||
ComponentType.SWA: params.swa_num_tokens,
|
ComponentType.SWA: params.swa_num_tokens,
|
||||||
ComponentType.MAMBA: params.mamba_num,
|
ComponentType.MAMBA: params.mamba_num,
|
||||||
ComponentType.C128: 0,
|
ComponentType.C128: 0,
|
||||||
}
|
}
|
||||||
self._evict_components(request_by_type, tracker)
|
|
||||||
|
def _component_available_size(self, component_type: ComponentType) -> int:
|
||||||
|
"""Return capacity usable by the component's next allocation.
|
||||||
|
|
||||||
|
Shared allocators expose schedulable capacity, which includes peer holes
|
||||||
|
that an urgent allocator flush can reclaim without further eviction.
|
||||||
|
"""
|
||||||
|
if component_type == ComponentType.FULL:
|
||||||
|
if self.supports_swa():
|
||||||
|
return self.token_to_kv_pool_allocator.full_available_size()
|
||||||
|
return self.token_to_kv_pool_allocator.available_size()
|
||||||
|
if component_type == ComponentType.SWA:
|
||||||
|
return self.token_to_kv_pool_allocator.swa_available_size()
|
||||||
|
if component_type == ComponentType.MAMBA:
|
||||||
|
return self.req_to_token_pool.mamba_allocator.schedulable_available_size()
|
||||||
|
raise ValueError(f"Unsupported cache component: {component_type}")
|
||||||
|
|
||||||
|
def _evict(
|
||||||
|
self,
|
||||||
|
params: EvictParams,
|
||||||
|
available_size_targets: Optional[dict[ComponentType, int]] = None,
|
||||||
|
) -> EvictResult:
|
||||||
|
if self.disable:
|
||||||
|
return EvictResult()
|
||||||
|
start_time = time.perf_counter()
|
||||||
|
tracker = {ct: 0 for ct in self.tree_components}
|
||||||
|
|
||||||
|
request_by_type = self._evict_request_by_type(params)
|
||||||
|
self._evict_components(
|
||||||
|
request_by_type,
|
||||||
|
tracker,
|
||||||
|
available_size_targets=available_size_targets,
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.cache_controller is not None
|
self.cache_controller is not None
|
||||||
@@ -581,12 +631,12 @@ class UnifiedRadixCache(BasePrefixCache):
|
|||||||
|
|
||||||
def _evict_device_next_node(
|
def _evict_device_next_node(
|
||||||
self, component_type: ComponentType, tracker: dict[ComponentType, int]
|
self, component_type: ComponentType, tracker: dict[ComponentType, int]
|
||||||
) -> Optional[NodeId]:
|
) -> tuple[Optional[NodeId], bool]:
|
||||||
"""Advance the eviction walk one node, consuming its step result."""
|
"""Advance the eviction walk one node, consuming its step result."""
|
||||||
result = self.tree_core.evict_device_next_node(component_type, tracker)
|
result = self.tree_core.evict_device_next_node(component_type, tracker)
|
||||||
self._free_values(result.device_frees, result.host_frees)
|
self._free_values(result.device_frees, result.host_frees)
|
||||||
self._accumulate_tracker(tracker, result.tracker)
|
self._accumulate_tracker(tracker, result.tracker)
|
||||||
return result.node_id
|
return result.node_id, result.made_progress
|
||||||
|
|
||||||
def _evict_device_leaf(
|
def _evict_device_leaf(
|
||||||
self, node_id: NodeId, tracker: dict[ComponentType, int]
|
self, node_id: NodeId, tracker: dict[ComponentType, int]
|
||||||
@@ -617,20 +667,39 @@ class UnifiedRadixCache(BasePrefixCache):
|
|||||||
self,
|
self,
|
||||||
request_by_type: dict[ComponentType, int],
|
request_by_type: dict[ComponentType, int],
|
||||||
tracker: dict[ComponentType, int],
|
tracker: dict[ComponentType, int],
|
||||||
|
available_size_targets: Optional[dict[ComponentType, int]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# Buffer mode: eviction always wins over queued backup intents — a
|
# Buffer mode: eviction always wins over queued backup intents — a
|
||||||
# destroyed victim's intent is stale-swept and the content rewrites
|
# destroyed victim's intent is stale-swept and the content rewrites
|
||||||
# after its recompute.
|
# after its recompute.
|
||||||
|
|
||||||
|
def target_reached(component_type: ComponentType) -> bool:
|
||||||
|
if available_size_targets is None:
|
||||||
|
return False
|
||||||
|
target = available_size_targets.get(component_type)
|
||||||
|
# Do not compact on every eviction step. Shared allocators include
|
||||||
|
# drainable peer holes here and flush the peer once in alloc().
|
||||||
|
return (
|
||||||
|
target is not None
|
||||||
|
and self._component_available_size(component_type) >= target
|
||||||
|
)
|
||||||
|
|
||||||
for ct in self.tree_components:
|
for ct in self.tree_components:
|
||||||
request_cnt = request_by_type[ct]
|
request_cnt = request_by_type[ct]
|
||||||
# Skip eviction walk if request is already met
|
# A preceding component may have cascade-evicted this component or,
|
||||||
if tracker[ct] >= request_cnt:
|
# on a shared pool, released enough bytes to satisfy its allocation.
|
||||||
|
if tracker[ct] >= request_cnt or target_reached(ct):
|
||||||
continue
|
continue
|
||||||
self.tree_core.evict_device_start(ct, request_cnt)
|
self.tree_core.evict_device_start(ct, request_cnt)
|
||||||
try:
|
try:
|
||||||
while (
|
while not target_reached(ct):
|
||||||
node_id := self._evict_device_next_node(ct, tracker)
|
node_id, made_progress = self._evict_device_next_node(ct, tracker)
|
||||||
) is not None:
|
if node_id is None:
|
||||||
|
if made_progress:
|
||||||
|
# Internal tombstone frees are now allocator-visible;
|
||||||
|
# recheck the allocation target before walking again.
|
||||||
|
continue
|
||||||
|
break
|
||||||
backup_kv = self._evict_device_leaf(node_id, tracker)
|
backup_kv = self._evict_device_leaf(node_id, tracker)
|
||||||
if backup_kv is not None:
|
if backup_kv is not None:
|
||||||
# Deferred demote: run the D->H backup, demote only on success.
|
# Deferred demote: run the D->H backup, demote only on success.
|
||||||
@@ -1395,14 +1464,11 @@ class UnifiedRadixCache(BasePrefixCache):
|
|||||||
self.dec_host_lock_ref(node_id, host_anchor_params)
|
self.dec_host_lock_ref(node_id, host_anchor_params)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.supports_swa():
|
avail = self._component_available_size(ComponentType.FULL)
|
||||||
avail = self.token_to_kv_pool_allocator.full_available_size()
|
|
||||||
else:
|
|
||||||
avail = self.token_to_kv_pool_allocator.available_size()
|
|
||||||
if avail < kv_tokens:
|
if avail < kv_tokens:
|
||||||
needed = kv_tokens - avail
|
needed = kv_tokens - avail
|
||||||
result = self.evict(EvictParams(num_tokens=needed))
|
self.evict_for_alloc(EvictParams(num_tokens=needed))
|
||||||
if result.num_tokens_evicted < needed:
|
if self._component_available_size(ComponentType.FULL) < kv_tokens:
|
||||||
self.dec_lock_ref(node_id, ancestor_lock_params)
|
self.dec_lock_ref(node_id, ancestor_lock_params)
|
||||||
self.dec_host_lock_ref(node_id, host_anchor_params)
|
self.dec_host_lock_ref(node_id, host_anchor_params)
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -417,6 +417,9 @@ class StreamingSession(BasePrefixCache):
|
|||||||
def evict(self, params: EvictParams) -> EvictResult:
|
def evict(self, params: EvictParams) -> EvictResult:
|
||||||
return self.inner.evict(params)
|
return self.inner.evict(params)
|
||||||
|
|
||||||
|
def evict_for_alloc(self, params: EvictParams) -> EvictResult:
|
||||||
|
return self.inner.evict_for_alloc(params)
|
||||||
|
|
||||||
def inc_lock_ref(self, node: Any) -> IncLockRefResult:
|
def inc_lock_ref(self, node: Any) -> IncLockRefResult:
|
||||||
result = self.try_inc_lock_ref(node)
|
result = self.try_inc_lock_ref(node)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
|
|||||||
@@ -877,6 +877,17 @@ class TestMlxAuxiliaryStateRunnerCache(unittest.TestCase):
|
|||||||
self.assertEqual(forked.tolist(), [3])
|
self.assertEqual(forked.tolist(), [3])
|
||||||
self.assertEqual(restored[0].state[0].tolist(), [1.0])
|
self.assertEqual(restored[0].state[0].tolist(), [1.0])
|
||||||
self.assertEqual(pool.available_size(), 3)
|
self.assertEqual(pool.available_size(), 3)
|
||||||
|
self.assertEqual(pool.schedulable_available_size(), 3)
|
||||||
|
|
||||||
|
def test_auxiliary_state_pool_returns_unused_group_slots(self):
|
||||||
|
pool = MlxAuxiliaryStatePool(size=4, device="cpu")
|
||||||
|
|
||||||
|
pool.alloc_group_begin(3)
|
||||||
|
allocated = pool.alloc(1)
|
||||||
|
pool.alloc_group_end()
|
||||||
|
|
||||||
|
self.assertEqual(allocated.tolist(), [1])
|
||||||
|
self.assertEqual(pool.available_size(), 3)
|
||||||
|
|
||||||
def test_auxiliary_state_pool_restores_instance_meta_state(self):
|
def test_auxiliary_state_pool_restores_instance_meta_state(self):
|
||||||
pool = MlxAuxiliaryStatePool(size=2, device="cpu")
|
pool = MlxAuxiliaryStatePool(size=2, device="cpu")
|
||||||
@@ -916,6 +927,7 @@ class TestMlxAuxiliaryStateRunnerCache(unittest.TestCase):
|
|||||||
self.assertIsNotNone(auxiliary_state_idx)
|
self.assertIsNotNone(auxiliary_state_idx)
|
||||||
self.assertIsNone(req.req_pool_idx)
|
self.assertIsNone(req.req_pool_idx)
|
||||||
self.assertIsNotNone(req.mamba_pool_idx)
|
self.assertIsNotNone(req.mamba_pool_idx)
|
||||||
|
self.assertIs(pool.mamba_allocator, pool.mamba_pool)
|
||||||
self.assertEqual(pool.auxiliary_state_pool.available_size(), 3)
|
self.assertEqual(pool.auxiliary_state_pool.available_size(), 3)
|
||||||
pool.free_auxiliary_state_cache(req)
|
pool.free_auxiliary_state_cache(req)
|
||||||
self.assertIsNone(req.mamba_pool_idx)
|
self.assertIsNone(req.mamba_pool_idx)
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ class TestDecodeLockRefScenarios(unittest.TestCase):
|
|||||||
error = queue._reclaim_swa_tail_capacity(129, "req-1")
|
error = queue._reclaim_swa_tail_capacity(129, "req-1")
|
||||||
|
|
||||||
self.assertIsNone(error)
|
self.assertIsNone(error)
|
||||||
params = queue.tree_cache.evict.call_args.args[0]
|
params = queue.tree_cache.evict_for_alloc.call_args.args[0]
|
||||||
self.assertEqual(params.num_tokens, 0)
|
self.assertEqual(params.num_tokens, 0)
|
||||||
self.assertEqual(params.swa_num_tokens, 128)
|
self.assertEqual(params.swa_num_tokens, 128)
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,12 @@
|
|||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from sglang.srt.mem_cache.base_prefix_cache import EvictParams
|
||||||
from sglang.srt.mem_cache.hybrid_cache.hybrid_pool_assembler import (
|
from sglang.srt.mem_cache.hybrid_cache.hybrid_pool_assembler import (
|
||||||
|
_evict_mamba_for_device_alloc,
|
||||||
|
_evict_swa_for_device_alloc,
|
||||||
_split_hicache_size,
|
_split_hicache_size,
|
||||||
build_full_draft_pools,
|
build_full_draft_pools,
|
||||||
)
|
)
|
||||||
@@ -23,6 +26,40 @@ class _Pool:
|
|||||||
return self._kv_bytes
|
return self._kv_bytes
|
||||||
|
|
||||||
|
|
||||||
|
class TestDeviceAllocEviction(CustomTestCase):
|
||||||
|
def test_swa_evicts_only_allocation_shortfall(self):
|
||||||
|
cache = MagicMock()
|
||||||
|
cache.token_to_kv_pool_allocator.swa_available_size.return_value = 8
|
||||||
|
|
||||||
|
_evict_swa_for_device_alloc(cache, required_size=10)
|
||||||
|
|
||||||
|
cache.evict_for_alloc.assert_called_once_with(EvictParams(swa_num_tokens=2))
|
||||||
|
cache.evict.assert_not_called()
|
||||||
|
|
||||||
|
def test_mamba_evicts_only_allocation_shortfall(self):
|
||||||
|
cache = MagicMock()
|
||||||
|
allocator = cache.req_to_token_pool.mamba_allocator
|
||||||
|
allocator.schedulable_available_size.return_value = 8
|
||||||
|
|
||||||
|
_evict_mamba_for_device_alloc(cache, required_size=10)
|
||||||
|
|
||||||
|
cache.evict_for_alloc.assert_called_once_with(EvictParams(mamba_num=2))
|
||||||
|
cache.evict.assert_not_called()
|
||||||
|
|
||||||
|
def test_sufficient_capacity_skips_eviction(self):
|
||||||
|
cache = MagicMock()
|
||||||
|
cache.token_to_kv_pool_allocator.swa_available_size.return_value = 10
|
||||||
|
cache.req_to_token_pool.mamba_allocator.schedulable_available_size.return_value = (
|
||||||
|
10
|
||||||
|
)
|
||||||
|
|
||||||
|
_evict_swa_for_device_alloc(cache, required_size=10)
|
||||||
|
_evict_mamba_for_device_alloc(cache, required_size=10)
|
||||||
|
|
||||||
|
cache.evict_for_alloc.assert_not_called()
|
||||||
|
cache.evict.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
class TestSplitHicacheSize(CustomTestCase):
|
class TestSplitHicacheSize(CustomTestCase):
|
||||||
def test_splits_total_budget_by_device_bytes(self):
|
def test_splits_total_budget_by_device_bytes(self):
|
||||||
# scalar and (k, v) tuple return shapes both supported
|
# scalar and (k, v) tuple return shapes both supported
|
||||||
|
|||||||
@@ -55,10 +55,10 @@ class _RatioCache:
|
|||||||
self.component_evictable_size_ = {ComponentType.MAMBA: 0}
|
self.component_evictable_size_ = {ComponentType.MAMBA: 0}
|
||||||
self.component_protected_size_ = {ComponentType.MAMBA: 0}
|
self.component_protected_size_ = {ComponentType.MAMBA: 0}
|
||||||
self.prefix_nodes = []
|
self.prefix_nodes = []
|
||||||
|
self.alloc_evict_params = []
|
||||||
|
|
||||||
def evict(self, params: EvictParams):
|
def evict_for_alloc(self, params: EvictParams):
|
||||||
# Reclaim up to mamba_num evictable (unlocked) prefix snapshots, mirroring
|
self.alloc_evict_params.append(params)
|
||||||
# what the real tree eviction can hand back under mamba pressure.
|
|
||||||
need = params.mamba_num
|
need = params.mamba_num
|
||||||
for node in list(self.prefix_nodes):
|
for node in list(self.prefix_nodes):
|
||||||
if need <= 0:
|
if need <= 0:
|
||||||
@@ -130,7 +130,9 @@ class TestMambaRatioEnvGate(unittest.TestCase):
|
|||||||
return KVCacheConfigurator._calculate_mamba_ratio(fake)
|
return KVCacheConfigurator._calculate_mamba_ratio(fake)
|
||||||
|
|
||||||
def test_flag_off_restores_original_ratios(self):
|
def test_flag_off_restores_original_ratios(self):
|
||||||
r = lambda **kw: self._ratio(skip=False, **kw)
|
def r(**kwargs):
|
||||||
|
return self._ratio(skip=False, **kwargs)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
r(extra_buffer=False, lazy=False, disable_overlap=True), 3
|
r(extra_buffer=False, lazy=False, disable_overlap=True), 3
|
||||||
) # no_buffer
|
) # no_buffer
|
||||||
@@ -142,7 +144,9 @@ class TestMambaRatioEnvGate(unittest.TestCase):
|
|||||||
) # overlap
|
) # overlap
|
||||||
|
|
||||||
def test_flag_on_drops_base_but_keeps_no_buffer(self):
|
def test_flag_on_drops_base_but_keeps_no_buffer(self):
|
||||||
r = lambda **kw: self._ratio(skip=True, **kw)
|
def r(**kwargs):
|
||||||
|
return self._ratio(skip=True, **kwargs)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
r(extra_buffer=False, lazy=False, disable_overlap=True), 3
|
r(extra_buffer=False, lazy=False, disable_overlap=True), 3
|
||||||
) # no_buffer
|
) # no_buffer
|
||||||
@@ -212,9 +216,12 @@ class TestDecSwaLockSkip(unittest.TestCase):
|
|||||||
class TestMambaDonatedAllocRatio(unittest.TestCase):
|
class TestMambaDonatedAllocRatio(unittest.TestCase):
|
||||||
def test_prefill_peak_ratio2_exhausts_pool(self):
|
def test_prefill_peak_ratio2_exhausts_pool(self):
|
||||||
# pool = 2N, all N prefixes admission-locked: no evictable victim.
|
# pool = 2N, all N prefixes admission-locked: no evictable victim.
|
||||||
component, _, _ = _build_peak(pool_size=2 * N, lock_prefixes=True)
|
component, cache, _ = _build_peak(pool_size=2 * N, lock_prefixes=True)
|
||||||
with self.assertRaisesRegex(AssertionError, "Can not alloc mamba cache"):
|
with self.assertRaisesRegex(AssertionError, "Can not alloc mamba cache"):
|
||||||
component._alloc_mamba_slot()
|
component._alloc_mamba_slot()
|
||||||
|
self.assertEqual(
|
||||||
|
cache.alloc_evict_params, [EvictParams(num_tokens=0, mamba_num=1)]
|
||||||
|
)
|
||||||
|
|
||||||
def test_prefill_peak_ratio3_has_headroom(self):
|
def test_prefill_peak_ratio3_has_headroom(self):
|
||||||
# pool = 3N: N free slots remain after own + locked prefix.
|
# pool = 3N: N free slots remain after own + locked prefix.
|
||||||
@@ -230,6 +237,9 @@ class TestMambaDonatedAllocRatio(unittest.TestCase):
|
|||||||
slot = component._alloc_mamba_slot()
|
slot = component._alloc_mamba_slot()
|
||||||
self.assertIsNotNone(slot)
|
self.assertIsNotNone(slot)
|
||||||
self.assertEqual(len(cache.prefix_nodes), N - 1)
|
self.assertEqual(len(cache.prefix_nodes), N - 1)
|
||||||
|
self.assertEqual(
|
||||||
|
cache.alloc_evict_params, [EvictParams(num_tokens=0, mamba_num=1)]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestPPMambaPoolSizing(unittest.TestCase):
|
class TestPPMambaPoolSizing(unittest.TestCase):
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
"""CPU-only tests for allocation-aware UnifiedRadixCache eviction."""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from sglang.srt.mem_cache.base_prefix_cache import EvictParams
|
||||||
|
from sglang.srt.mem_cache.common import evict_from_tree_cache
|
||||||
|
from sglang.srt.mem_cache.unified_cache.components import ComponentType
|
||||||
|
from sglang.srt.mem_cache.unified_radix_cache import UnifiedRadixCache
|
||||||
|
from sglang.test.ci.ci_register import register_cpu_ci
|
||||||
|
from sglang.test.test_utils import CustomTestCase
|
||||||
|
|
||||||
|
register_cpu_ci(est_time=2, suite="base-a-test-cpu")
|
||||||
|
|
||||||
|
|
||||||
|
class TestUnifiedRadixAllocationEviction(CustomTestCase):
|
||||||
|
@staticmethod
|
||||||
|
def _build_cache(*, collateral_capacity_gain: int):
|
||||||
|
cache = object.__new__(UnifiedRadixCache)
|
||||||
|
cache.disable = False
|
||||||
|
cache.tree_components = (ComponentType.FULL, ComponentType.MAMBA)
|
||||||
|
cache.is_swa_enabled = False
|
||||||
|
cache.cache_controller = None
|
||||||
|
cache.metrics_collector = None
|
||||||
|
cache.tree_core = MagicMock()
|
||||||
|
|
||||||
|
capacity = {"available": 30}
|
||||||
|
allocator = MagicMock()
|
||||||
|
allocator.available_size.side_effect = lambda: capacity["available"]
|
||||||
|
cache.token_to_kv_pool_allocator = allocator
|
||||||
|
cache.req_to_token_pool = MagicMock()
|
||||||
|
|
||||||
|
leaf_count = {"value": 0}
|
||||||
|
|
||||||
|
def next_node(component_type, tracker):
|
||||||
|
if tracker[component_type] >= 70:
|
||||||
|
return None, False
|
||||||
|
return leaf_count["value"] + 1, True
|
||||||
|
|
||||||
|
def evict_leaf(_node_id, tracker):
|
||||||
|
leaf_count["value"] += 1
|
||||||
|
tracker[ComponentType.FULL] += 20
|
||||||
|
tracker[ComponentType.MAMBA] += 1
|
||||||
|
capacity["available"] += (
|
||||||
|
collateral_capacity_gain if leaf_count["value"] == 1 else 20
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
cache._evict_device_next_node = MagicMock(side_effect=next_node)
|
||||||
|
cache._evict_device_leaf = MagicMock(side_effect=evict_leaf)
|
||||||
|
return cache, capacity, leaf_count
|
||||||
|
|
||||||
|
def test_allocation_eviction_stops_when_shared_capacity_is_sufficient(self):
|
||||||
|
cache, capacity, leaf_count = self._build_cache(collateral_capacity_gain=70)
|
||||||
|
|
||||||
|
result = cache.evict_for_alloc(EvictParams(num_tokens=70))
|
||||||
|
|
||||||
|
self.assertEqual(capacity["available"], 100)
|
||||||
|
self.assertEqual(leaf_count["value"], 1)
|
||||||
|
self.assertEqual(result.num_tokens_evicted, 20)
|
||||||
|
self.assertEqual(result.mamba_num_evicted, 1)
|
||||||
|
|
||||||
|
def test_explicit_evict_preserves_component_count_semantics(self):
|
||||||
|
cache, _, leaf_count = self._build_cache(collateral_capacity_gain=70)
|
||||||
|
|
||||||
|
result = cache.evict(EvictParams(num_tokens=70))
|
||||||
|
|
||||||
|
self.assertEqual(leaf_count["value"], 4)
|
||||||
|
self.assertEqual(result.num_tokens_evicted, 80)
|
||||||
|
self.assertEqual(result.mamba_num_evicted, 4)
|
||||||
|
|
||||||
|
def test_c128_component_keeps_zero_quota(self):
|
||||||
|
cache, _, _ = self._build_cache(collateral_capacity_gain=70)
|
||||||
|
cache.tree_components = (ComponentType.FULL, ComponentType.C128)
|
||||||
|
cache._evict_device_next_node.side_effect = None
|
||||||
|
cache._evict_device_next_node.return_value = (None, False)
|
||||||
|
|
||||||
|
result = cache.evict(EvictParams(num_tokens=1))
|
||||||
|
|
||||||
|
self.assertEqual(result.num_tokens_evicted, 0)
|
||||||
|
cache.tree_core.evict_device_start.assert_called_once_with(
|
||||||
|
ComponentType.FULL, 1
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_mamba_allocation_counts_collateral_full_capacity(self):
|
||||||
|
cache = object.__new__(UnifiedRadixCache)
|
||||||
|
cache.disable = False
|
||||||
|
cache.tree_components = (ComponentType.FULL, ComponentType.MAMBA)
|
||||||
|
cache.is_swa_enabled = False
|
||||||
|
cache.cache_controller = None
|
||||||
|
cache.metrics_collector = None
|
||||||
|
cache.tree_core = MagicMock()
|
||||||
|
cache.token_to_kv_pool_allocator = MagicMock()
|
||||||
|
|
||||||
|
capacity = {"available": 0}
|
||||||
|
mamba_allocator = MagicMock()
|
||||||
|
mamba_allocator.schedulable_available_size.side_effect = lambda: capacity[
|
||||||
|
"available"
|
||||||
|
]
|
||||||
|
cache.req_to_token_pool = MagicMock(mamba_allocator=mamba_allocator)
|
||||||
|
|
||||||
|
def next_node(component_type, tracker):
|
||||||
|
return (None, False) if tracker[component_type] >= 3 else (1, True)
|
||||||
|
|
||||||
|
def evict_leaf(_node_id, tracker):
|
||||||
|
tracker[ComponentType.FULL] += 20
|
||||||
|
tracker[ComponentType.MAMBA] += 1
|
||||||
|
capacity["available"] += 3
|
||||||
|
return None
|
||||||
|
|
||||||
|
cache._evict_device_next_node = MagicMock(side_effect=next_node)
|
||||||
|
cache._evict_device_leaf = MagicMock(side_effect=evict_leaf)
|
||||||
|
|
||||||
|
result = cache.evict_for_alloc(EvictParams(mamba_num=3))
|
||||||
|
|
||||||
|
self.assertEqual(capacity["available"], 3)
|
||||||
|
self.assertEqual(result.num_tokens_evicted, 20)
|
||||||
|
self.assertEqual(result.mamba_num_evicted, 1)
|
||||||
|
|
||||||
|
def test_common_helper_uses_allocation_aware_entry_point(self):
|
||||||
|
tree_cache = MagicMock()
|
||||||
|
tree_cache.is_chunk_cache.return_value = False
|
||||||
|
tree_cache.token_to_kv_pool_allocator.available_size.return_value = 30
|
||||||
|
|
||||||
|
evict_from_tree_cache(tree_cache, num_tokens=100)
|
||||||
|
|
||||||
|
tree_cache.evict_for_alloc.assert_called_once_with(EvictParams(num_tokens=70))
|
||||||
|
tree_cache.evict.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -343,6 +343,7 @@ def build_fixture(
|
|||||||
cfg: CacheConfig,
|
cfg: CacheConfig,
|
||||||
*,
|
*,
|
||||||
enable_kv_cache_events: bool = False,
|
enable_kv_cache_events: bool = False,
|
||||||
|
enable_session_radix_cache: bool = False,
|
||||||
tree_page_size: Optional[int] = None,
|
tree_page_size: Optional[int] = None,
|
||||||
mamba_cache_chunk_size: Optional[int] = None,
|
mamba_cache_chunk_size: Optional[int] = None,
|
||||||
):
|
):
|
||||||
@@ -472,6 +473,7 @@ def build_fixture(
|
|||||||
tree_components=cfg.components,
|
tree_components=cfg.components,
|
||||||
enable_mamba_extra_buffer=cfg.enable_mamba_extra_buffer,
|
enable_mamba_extra_buffer=cfg.enable_mamba_extra_buffer,
|
||||||
enable_kv_cache_events=enable_kv_cache_events,
|
enable_kv_cache_events=enable_kv_cache_events,
|
||||||
|
enable_session_radix_cache=enable_session_radix_cache,
|
||||||
eviction_policy=cfg.eviction_policy,
|
eviction_policy=cfg.eviction_policy,
|
||||||
is_eagle=cfg.is_eagle,
|
is_eagle=cfg.is_eagle,
|
||||||
)
|
)
|
||||||
@@ -481,6 +483,162 @@ def build_fixture(
|
|||||||
return cache, allocator, req_to_token_pool
|
return cache, allocator, req_to_token_pool
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(torch.cuda.is_available(), "cache fixtures need CUDA")
|
||||||
|
class TestUnifiedRadixAllocationEvictionRealComponents(CustomTestCase):
|
||||||
|
"""Allocation targets are observed between real auxiliary-tree steps."""
|
||||||
|
|
||||||
|
_SHORTFALL = 100
|
||||||
|
|
||||||
|
def _insert(self, cache, allocator, req_to_token_pool, tokens) -> None:
|
||||||
|
value = allocator.alloc(len(tokens))
|
||||||
|
self.assertIsNotNone(value)
|
||||||
|
params = InsertParams(
|
||||||
|
key=RadixKey(array("q", tokens)),
|
||||||
|
value=value[: len(tokens)],
|
||||||
|
)
|
||||||
|
if cache.supports_mamba():
|
||||||
|
req = Req(
|
||||||
|
rid=f"mamba-{len(tokens)}",
|
||||||
|
origin_input_text="",
|
||||||
|
origin_input_ids=array("q"),
|
||||||
|
sampling_params=SamplingParams(temperature=0, max_new_tokens=1),
|
||||||
|
)
|
||||||
|
req_to_token_pool.alloc([req])
|
||||||
|
params.mamba_value = req.mamba_pool_idx.unsqueeze(0)
|
||||||
|
cache.insert(params)
|
||||||
|
|
||||||
|
def _build_internal_chain(self, component_type, enable_session_radix_cache):
|
||||||
|
cfg = (
|
||||||
|
CacheConfig(
|
||||||
|
components=(ComponentType.FULL, ComponentType.SWA),
|
||||||
|
sliding_window_size=128,
|
||||||
|
)
|
||||||
|
if component_type is ComponentType.SWA
|
||||||
|
else CacheConfig(
|
||||||
|
components=(ComponentType.FULL, ComponentType.MAMBA),
|
||||||
|
mamba_cache_size=8,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cache, allocator, req_to_token_pool = build_fixture(
|
||||||
|
cfg, enable_session_radix_cache=enable_session_radix_cache
|
||||||
|
)
|
||||||
|
for length in (2, 4, 6):
|
||||||
|
self._insert(
|
||||||
|
cache,
|
||||||
|
allocator,
|
||||||
|
req_to_token_pool,
|
||||||
|
list(range(1, length + 1)),
|
||||||
|
)
|
||||||
|
|
||||||
|
lru = cache.tree_core.lru_lists[component_type]
|
||||||
|
first = lru.get_lru_no_lock()
|
||||||
|
second = lru.get_prev_no_lock(first)
|
||||||
|
leaf = lru.get_prev_no_lock(second)
|
||||||
|
self.assertNotIn(first, cache.tree_core.evictable_device_leaves)
|
||||||
|
self.assertNotIn(second, cache.tree_core.evictable_device_leaves)
|
||||||
|
self.assertIn(leaf, cache.tree_core.evictable_device_leaves)
|
||||||
|
for node in (first, second, leaf):
|
||||||
|
self.assertIsNotNone(node.component_data[component_type].value)
|
||||||
|
self.assertIsNotNone(node.component_data[ComponentType.FULL].value)
|
||||||
|
return cache, first, second, leaf
|
||||||
|
|
||||||
|
def _evict_for_alloc_after_first_drain(self, cache, component_type):
|
||||||
|
capacity = {"available": 0}
|
||||||
|
auxiliary_drains = {"count": 0}
|
||||||
|
real_available_size = cache._component_available_size
|
||||||
|
real_free_values = cache._free_values
|
||||||
|
|
||||||
|
def available_size(requested_type):
|
||||||
|
if requested_type is component_type:
|
||||||
|
return capacity["available"]
|
||||||
|
return real_available_size(requested_type)
|
||||||
|
|
||||||
|
def free_values(device_frees, host_frees):
|
||||||
|
freed_auxiliary = bool(device_frees.get(component_type))
|
||||||
|
real_free_values(device_frees, host_frees)
|
||||||
|
if freed_auxiliary:
|
||||||
|
auxiliary_drains["count"] += 1
|
||||||
|
capacity["available"] = self._SHORTFALL
|
||||||
|
|
||||||
|
params = (
|
||||||
|
EvictParams(swa_num_tokens=self._SHORTFALL)
|
||||||
|
if component_type is ComponentType.SWA
|
||||||
|
else EvictParams(mamba_num=self._SHORTFALL)
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
cache, "_component_available_size", side_effect=available_size
|
||||||
|
),
|
||||||
|
mock.patch.object(cache, "_free_values", side_effect=free_values),
|
||||||
|
):
|
||||||
|
result = cache.evict_for_alloc(params)
|
||||||
|
return result, auxiliary_drains["count"]
|
||||||
|
|
||||||
|
def test_allocation_target_stops_after_one_internal_tombstone(self):
|
||||||
|
for component_type in (ComponentType.SWA, ComponentType.MAMBA):
|
||||||
|
for enable_session_radix_cache in (False, True):
|
||||||
|
with self.subTest(
|
||||||
|
component_type=component_type,
|
||||||
|
enable_session_radix_cache=enable_session_radix_cache,
|
||||||
|
):
|
||||||
|
cache, first, second, leaf = self._build_internal_chain(
|
||||||
|
component_type, enable_session_radix_cache
|
||||||
|
)
|
||||||
|
first_size = len(first.component_data[component_type].value)
|
||||||
|
|
||||||
|
result, drain_count = self._evict_for_alloc_after_first_drain(
|
||||||
|
cache, component_type
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsNone(first.component_data[component_type].value)
|
||||||
|
self.assertIsNotNone(second.component_data[component_type].value)
|
||||||
|
self.assertIsNotNone(leaf.component_data[component_type].value)
|
||||||
|
self.assertIsNotNone(leaf.component_data[ComponentType.FULL].value)
|
||||||
|
self.assertEqual(result.num_tokens_evicted, 0)
|
||||||
|
self.assertEqual(drain_count, 1)
|
||||||
|
evicted = (
|
||||||
|
result.swa_num_tokens_evicted
|
||||||
|
if component_type is ComponentType.SWA
|
||||||
|
else result.mamba_num_evicted
|
||||||
|
)
|
||||||
|
self.assertEqual(evicted, first_size)
|
||||||
|
cache.sanity_check()
|
||||||
|
|
||||||
|
def test_explicit_evict_continues_across_internal_steps(self):
|
||||||
|
for component_type in (ComponentType.SWA, ComponentType.MAMBA):
|
||||||
|
for enable_session_radix_cache in (False, True):
|
||||||
|
with self.subTest(
|
||||||
|
component_type=component_type,
|
||||||
|
enable_session_radix_cache=enable_session_radix_cache,
|
||||||
|
):
|
||||||
|
cache, first, second, leaf = self._build_internal_chain(
|
||||||
|
component_type, enable_session_radix_cache
|
||||||
|
)
|
||||||
|
request_count = sum(
|
||||||
|
len(node.component_data[component_type].value)
|
||||||
|
for node in (first, second)
|
||||||
|
)
|
||||||
|
params = (
|
||||||
|
EvictParams(swa_num_tokens=request_count)
|
||||||
|
if component_type is ComponentType.SWA
|
||||||
|
else EvictParams(mamba_num=request_count)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = cache.evict(params)
|
||||||
|
|
||||||
|
self.assertIsNone(first.component_data[component_type].value)
|
||||||
|
self.assertIsNone(second.component_data[component_type].value)
|
||||||
|
self.assertIsNotNone(leaf.component_data[component_type].value)
|
||||||
|
self.assertIsNotNone(leaf.component_data[ComponentType.FULL].value)
|
||||||
|
evicted = (
|
||||||
|
result.swa_num_tokens_evicted
|
||||||
|
if component_type is ComponentType.SWA
|
||||||
|
else result.mamba_num_evicted
|
||||||
|
)
|
||||||
|
self.assertEqual(evicted, request_count)
|
||||||
|
cache.sanity_check()
|
||||||
|
|
||||||
|
|
||||||
class TestUnifiedRadixCacheEagleHiCacheStorageKey(CustomTestCase):
|
class TestUnifiedRadixCacheEagleHiCacheStorageKey(CustomTestCase):
|
||||||
cfg = CacheConfig(
|
cfg = CacheConfig(
|
||||||
page_size=4,
|
page_size=4,
|
||||||
@@ -5363,10 +5521,12 @@ class UnifiedRadixCacheSuite:
|
|||||||
"alloc",
|
"alloc",
|
||||||
side_effect=[None, retry_slot],
|
side_effect=[None, retry_slot],
|
||||||
),
|
),
|
||||||
mock.patch.object(cache, "evict", autospec=True) as evict,
|
mock.patch.object(
|
||||||
|
cache, "evict_for_alloc", autospec=True
|
||||||
|
) as evict_for_alloc,
|
||||||
):
|
):
|
||||||
prep = comp.prepare_load_back(leaf.id, req=req)
|
prep = comp.prepare_load_back(leaf.id, req=req)
|
||||||
evict.assert_called_once_with(EvictParams(num_tokens=0, mamba_num=1))
|
evict_for_alloc.assert_called_once_with(EvictParams(num_tokens=0, mamba_num=1))
|
||||||
self.assertIs(prep.allocated_mamba_slot, retry_slot)
|
self.assertIs(prep.allocated_mamba_slot, retry_slot)
|
||||||
self.assertEqual(int(req.mamba_pool_idx), int(retry_slot[0]))
|
self.assertEqual(int(req.mamba_pool_idx), int(retry_slot[0]))
|
||||||
|
|
||||||
@@ -5790,13 +5950,15 @@ class UnifiedRadixCacheSuite:
|
|||||||
int(swa_xfer.host_indices.numel()),
|
int(swa_xfer.host_indices.numel()),
|
||||||
)
|
)
|
||||||
|
|
||||||
with mock.patch.object(cache, "evict", wraps=cache.evict) as evict_mock:
|
with mock.patch.object(
|
||||||
|
cache, "evict_for_alloc", wraps=cache.evict_for_alloc
|
||||||
|
) as evict_for_alloc_mock:
|
||||||
self.assertTrue(cache.load_back(leaf.id))
|
self.assertTrue(cache.load_back(leaf.id))
|
||||||
|
|
||||||
# Full pre-eviction must not be triggered by SWA pool pressure.
|
# Full pre-eviction must not be triggered by SWA pool pressure.
|
||||||
full_pre_evict_calls = [
|
full_pre_evict_calls = [
|
||||||
call
|
call
|
||||||
for call in evict_mock.call_args_list
|
for call in evict_for_alloc_mock.call_args_list
|
||||||
if call.args and call.args[0].num_tokens > 0
|
if call.args and call.args[0].num_tokens > 0
|
||||||
]
|
]
|
||||||
self.assertEqual(full_pre_evict_calls, [])
|
self.assertEqual(full_pre_evict_calls, [])
|
||||||
@@ -5807,7 +5969,7 @@ class UnifiedRadixCacheSuite:
|
|||||||
call.args
|
call.args
|
||||||
and call.args[0].num_tokens == 0
|
and call.args[0].num_tokens == 0
|
||||||
and call.args[0].swa_num_tokens > 0
|
and call.args[0].swa_num_tokens > 0
|
||||||
for call in evict_mock.call_args_list
|
for call in evict_for_alloc_mock.call_args_list
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -7026,9 +7188,13 @@ class TestReturnedValuesDrain(_InsertWalkSuite):
|
|||||||
cases = [
|
cases = [
|
||||||
(
|
(
|
||||||
"evict_device_next_node",
|
"evict_device_next_node",
|
||||||
lambda: make(EvictDeviceNextNodeResult, node_id=node.id),
|
lambda: make(
|
||||||
|
EvictDeviceNextNodeResult,
|
||||||
|
node_id=node.id,
|
||||||
|
made_progress=True,
|
||||||
|
),
|
||||||
lambda: cache._evict_device_next_node(ComponentType.FULL, tracker),
|
lambda: cache._evict_device_next_node(ComponentType.FULL, tracker),
|
||||||
node.id,
|
(node.id, True),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"evict_device_leaf",
|
"evict_device_leaf",
|
||||||
|
|||||||
Reference in New Issue
Block a user