chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to SGLang project
|
||||
|
||||
"""File storage backend helpers for SGLang HiCache."""
|
||||
|
||||
from .lru_file_evictor import LRUFileEvictor
|
||||
|
||||
__all__ = [
|
||||
"LRUFileEvictor",
|
||||
]
|
||||
@@ -0,0 +1,393 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to SGLang project
|
||||
|
||||
"""LRU/size-based file eviction for the HiCache file storage backend.
|
||||
|
||||
``HiCacheFile`` is a thin raw-bytes store: it suffixes keys, reads/writes
|
||||
``.bin`` pages, and answers existence queries. Everything that bounds how much
|
||||
disk those pages consume -- the LRU recency index, per-file size accounting,
|
||||
free-space probing, scanning pre-existing files on startup, and unlinking
|
||||
victims -- lives here so the backend stays a plain key/value store.
|
||||
|
||||
A backend constructs one evictor and drives it through a small lifecycle::
|
||||
|
||||
touch(key, path) # read hit / already-on-disk: bump recency
|
||||
reserve(key, n_bytes) -> bool # admit a new write, evicting if needed
|
||||
commit(key) # write landed on disk
|
||||
abort(key) # write failed; release the reservation
|
||||
clear() # backend wiped all files
|
||||
|
||||
When eviction is not configured the evictor is inert: ``reserve`` always admits
|
||||
and the other calls are no-ops, so the backend behaves as unbounded storage.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from collections import OrderedDict
|
||||
from typing import Any, Callable, Optional, Set, Tuple
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.utils.common import human_readable_int
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _parse_size_to_bytes(value: Any) -> int:
|
||||
"""Parse a size to bytes via human_readable_int (e.g. '200G', '1Gi', '1048576').
|
||||
None / empty / '0' disables; an invalid value also disables (with a warning)."""
|
||||
if value is None:
|
||||
return 0
|
||||
if isinstance(value, (int, float)):
|
||||
return max(0, int(value))
|
||||
s = str(value).strip()
|
||||
if not s or s == "0":
|
||||
return 0
|
||||
try:
|
||||
return max(0, human_readable_int(s))
|
||||
except (argparse.ArgumentTypeError, ValueError):
|
||||
logger.warning(f"Invalid size {value!r} for HiCacheFile; disabling.")
|
||||
return 0
|
||||
|
||||
|
||||
class LRUFileEvictor:
|
||||
"""Bounds the on-disk size of a HiCacheFile directory via LRU eviction.
|
||||
|
||||
Tracks one ``.bin`` file per suffixed key (oldest at the front of the LRU),
|
||||
enforces an optional byte cap and an optional free-space watermark, and
|
||||
unlinks the least-recently-used files to stay within those bounds. Eviction
|
||||
config comes from ``extra_config`` (per-backend, takes precedence) falling
|
||||
back to the ``SGLANG_HICACHE_FILE_BACKEND_*`` env vars.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_path: str,
|
||||
config_suffix: str,
|
||||
*,
|
||||
tp_rank: int,
|
||||
is_mla_model: bool,
|
||||
extra_config: Optional[dict] = None,
|
||||
on_evict: Optional[Callable[[str], None]] = None,
|
||||
) -> None:
|
||||
self.file_path = file_path
|
||||
self.config_suffix = config_suffix
|
||||
self._tp_rank = tp_rank
|
||||
self._on_evict = on_evict
|
||||
|
||||
# MLA ranks share the same physical files, so centralize LRU bookkeeping
|
||||
# on rank 0; non-MLA ranks each own their own files via the suffix.
|
||||
self._is_storage_owner = (not is_mla_model) or (tp_rank == 0)
|
||||
|
||||
# suffixed_key -> file size in bytes; oldest at front.
|
||||
self._lru: OrderedDict[str, int] = OrderedDict()
|
||||
self._pending_writes: Set[str] = set()
|
||||
self._total_bytes: int = 0
|
||||
self._lock = threading.Lock()
|
||||
|
||||
self._load_config(extra_config or {})
|
||||
|
||||
self._eviction_configured = self.max_size_bytes > 0 or self.min_free_bytes > 0
|
||||
self._eviction_enabled = self._eviction_configured and self._is_storage_owner
|
||||
if self._eviction_configured and not self._is_storage_owner:
|
||||
logger.info(
|
||||
f"HiCacheFile rank {self._tp_rank} (MLA): eviction handled by rank 0; "
|
||||
f"this rank skips LRU bookkeeping and will not create new files."
|
||||
)
|
||||
|
||||
if not self._eviction_enabled:
|
||||
return
|
||||
|
||||
# Clamp max_size to the filesystem capacity so a too-large cap can't OOM tmpfs.
|
||||
fs = self._fs_stats()
|
||||
if fs is not None and self.max_size_bytes > 0:
|
||||
safe_max = max(0, fs[0] - self.min_free_bytes)
|
||||
if self.max_size_bytes > safe_max:
|
||||
logger.warning(
|
||||
f"HiCacheFile max_size exceeds filesystem capacity; "
|
||||
f"clamping to {safe_max} B."
|
||||
)
|
||||
self.max_size_bytes = safe_max
|
||||
|
||||
self._scan_existing_files()
|
||||
with self._lock:
|
||||
if self.max_size_bytes > 0 and self._total_bytes > self.max_size_bytes:
|
||||
self._evict_locked(0)
|
||||
if self.min_free_bytes > 0:
|
||||
self._enforce_free_space_locked(0)
|
||||
logger.info(
|
||||
f"HiCacheFile eviction enabled: cap={self.max_size_bytes} B, "
|
||||
f"watermark={self.eviction_ratio:.2f}, min_free={self.min_free_bytes} B, "
|
||||
f"existing={self._total_bytes} B ({len(self._lru)} entries)"
|
||||
)
|
||||
|
||||
def _load_config(self, extra: dict) -> None:
|
||||
# extra_config (per-backend) takes precedence over env vars.
|
||||
def _cfg(key, env):
|
||||
val = extra.get(key)
|
||||
return env.get() if val is None else val
|
||||
|
||||
self.max_size_bytes = _parse_size_to_bytes(
|
||||
_cfg("max_size", envs.SGLANG_HICACHE_FILE_BACKEND_MAX_SIZE)
|
||||
)
|
||||
self.min_free_bytes = _parse_size_to_bytes(
|
||||
_cfg("min_free_space", envs.SGLANG_HICACHE_FILE_BACKEND_MIN_FREE_SPACE)
|
||||
)
|
||||
|
||||
ratio_raw = _cfg(
|
||||
"eviction_ratio", envs.SGLANG_HICACHE_FILE_BACKEND_EVICTION_RATIO
|
||||
)
|
||||
try:
|
||||
self.eviction_ratio = float(ratio_raw)
|
||||
except (TypeError, ValueError):
|
||||
self.eviction_ratio = 0.9
|
||||
if not (0.0 < self.eviction_ratio <= 1.0):
|
||||
self.eviction_ratio = 0.9
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
"""True when this rank actively evicts (configured AND storage owner)."""
|
||||
return self._eviction_enabled
|
||||
|
||||
@property
|
||||
def configured(self) -> bool:
|
||||
"""True when a cap or free-space watermark is set (on any rank)."""
|
||||
return self._eviction_configured
|
||||
|
||||
@property
|
||||
def is_storage_owner(self) -> bool:
|
||||
"""True when this rank owns (and may create/evict) the on-disk files."""
|
||||
return self._is_storage_owner
|
||||
|
||||
def reserve(self, suffixed_key: str, value_bytes: int, key: str = "") -> bool:
|
||||
"""Admit a new write of ``value_bytes``, evicting LRU victims as needed.
|
||||
|
||||
On success the key is pre-reserved at MRU and flagged in-flight so a
|
||||
concurrent ``reserve`` won't evict it before the file is committed; the
|
||||
caller must then call ``commit`` (write landed) or ``abort`` (write
|
||||
failed). Returns ``False`` -- reserving nothing -- when the write is
|
||||
refused: this rank is not the storage owner, the value is larger than
|
||||
the cap, there is no evictable space, or the free-space watermark cannot
|
||||
be met. When eviction is not configured the write is always admitted.
|
||||
"""
|
||||
if not self._eviction_configured:
|
||||
return True # unbounded storage: nothing to enforce
|
||||
if not self._is_storage_owner:
|
||||
logger.warning(
|
||||
f"HiCacheFile rank {self._tp_rank} is not the MLA storage owner; "
|
||||
f"not caching new key {key} because file eviction is enabled."
|
||||
)
|
||||
return False
|
||||
if self.max_size_bytes > 0 and value_bytes > self.max_size_bytes:
|
||||
logger.warning(
|
||||
f"HiCacheFile: value {value_bytes} B exceeds cap "
|
||||
f"{self.max_size_bytes} B; not caching {key}"
|
||||
)
|
||||
return False
|
||||
|
||||
with self._lock:
|
||||
# Cap-based eviction: evict, then bail if still over cap.
|
||||
if (
|
||||
self.max_size_bytes > 0
|
||||
and (self._total_bytes + value_bytes) > self.max_size_bytes
|
||||
):
|
||||
self._evict_locked(value_bytes)
|
||||
if (self._total_bytes + value_bytes) > self.max_size_bytes:
|
||||
logger.warning(
|
||||
f"HiCacheFile: no evictable space for {value_bytes} B "
|
||||
f"under cap {self.max_size_bytes} B; not caching {key}"
|
||||
)
|
||||
return False
|
||||
# Free-space watermark.
|
||||
if self.min_free_bytes > 0 and not self._enforce_free_space_locked(
|
||||
value_bytes
|
||||
):
|
||||
logger.warning(
|
||||
f"HiCacheFile: filesystem hosting {self.file_path!r} "
|
||||
f"would fall below min_free={self.min_free_bytes} B "
|
||||
f"after writing {value_bytes} B; refusing {key} "
|
||||
f"to avoid OOM/ENOSPC."
|
||||
)
|
||||
return False
|
||||
# Pre-reserve at MRU so a concurrent evict won't grab this slot.
|
||||
prev = self._lru.pop(suffixed_key, None)
|
||||
if prev is not None:
|
||||
self._total_bytes -= prev
|
||||
self._lru[suffixed_key] = value_bytes
|
||||
self._pending_writes.add(suffixed_key)
|
||||
self._total_bytes += value_bytes
|
||||
return True
|
||||
|
||||
def commit(self, suffixed_key: str) -> None:
|
||||
"""Mark a reserved write as durably on disk (clears its in-flight flag)."""
|
||||
if not self._eviction_enabled:
|
||||
return
|
||||
with self._lock:
|
||||
self._pending_writes.discard(suffixed_key)
|
||||
|
||||
def abort(self, suffixed_key: str) -> None:
|
||||
"""Release a reservation whose write failed: drop it and refund the bytes."""
|
||||
if not self._eviction_enabled:
|
||||
return
|
||||
with self._lock:
|
||||
cur = self._lru.pop(suffixed_key, None)
|
||||
self._pending_writes.discard(suffixed_key)
|
||||
if cur is not None:
|
||||
self._total_bytes -= cur
|
||||
|
||||
def touch(self, suffixed_key: str, tensor_path: str) -> None:
|
||||
"""Mark key as MRU, adopting an untracked on-disk file if needed."""
|
||||
if not self._eviction_enabled:
|
||||
return
|
||||
with self._lock:
|
||||
if suffixed_key in self._lru:
|
||||
self._lru.move_to_end(suffixed_key, last=True)
|
||||
return
|
||||
# Untracked file: stat without holding the lock.
|
||||
try:
|
||||
size = os.path.getsize(tensor_path)
|
||||
except OSError:
|
||||
return
|
||||
with self._lock:
|
||||
if suffixed_key in self._lru:
|
||||
self._lru.move_to_end(suffixed_key, last=True)
|
||||
else:
|
||||
self._lru[suffixed_key] = size
|
||||
self._total_bytes += size
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Reset all bookkeeping after the backend has removed the files."""
|
||||
with self._lock:
|
||||
self._lru.clear()
|
||||
self._pending_writes.clear()
|
||||
self._total_bytes = 0
|
||||
|
||||
def _fs_stats(self) -> Optional[tuple]:
|
||||
"""(total, available) bytes for the filesystem; None if unavailable."""
|
||||
try:
|
||||
st = os.statvfs(self.file_path)
|
||||
except (OSError, AttributeError):
|
||||
return None
|
||||
total = st.f_blocks * st.f_frsize
|
||||
free = st.f_bavail * st.f_frsize
|
||||
return total, free
|
||||
|
||||
def _enforce_free_space_locked(self, value_bytes: int) -> bool:
|
||||
"""Evict until writing value_bytes still leaves min_free_bytes free.
|
||||
Caller holds _lock. Returns False if the write can't be satisfied."""
|
||||
if self.min_free_bytes <= 0:
|
||||
return True
|
||||
fs = self._fs_stats()
|
||||
if fs is None:
|
||||
return True # cannot probe -> permissive, fall back to OS errors
|
||||
# tmpfs frees space on unlink, so credit reclaimed bytes back to the
|
||||
# estimate rather than re-probing statvfs on every eviction.
|
||||
free = fs[1]
|
||||
self._evict_while(
|
||||
lambda reclaimed: (free + reclaimed) - value_bytes < self.min_free_bytes
|
||||
)
|
||||
# Re-probe: external writers may have changed free space meanwhile.
|
||||
fs = self._fs_stats()
|
||||
if fs is None:
|
||||
return True
|
||||
return fs[1] - value_bytes >= self.min_free_bytes
|
||||
|
||||
def _scan_existing_files(self) -> None:
|
||||
"""Seed LRU index from disk on startup (oldest mtime first)."""
|
||||
try:
|
||||
names = os.listdir(self.file_path)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
entries = []
|
||||
for fn in names:
|
||||
if not fn.endswith(".bin"):
|
||||
continue
|
||||
stem = fn[:-4]
|
||||
# Only files belonging to this rank/model.
|
||||
if not stem.endswith(self.config_suffix):
|
||||
continue
|
||||
fp = os.path.join(self.file_path, fn)
|
||||
try:
|
||||
st = os.stat(fp)
|
||||
except OSError:
|
||||
continue
|
||||
entries.append((st.st_mtime, stem, st.st_size))
|
||||
entries.sort(key=lambda e: e[0]) # oldest first
|
||||
for _, stem, size in entries:
|
||||
self._lru[stem] = size
|
||||
self._total_bytes += size
|
||||
|
||||
def _evict_one_lru_locked(self) -> Tuple[str, int]:
|
||||
"""Evict the single oldest evictable LRU entry. Caller holds _lock.
|
||||
|
||||
The shared pop / skip-pending / unlink / ``_total_bytes`` step driven by
|
||||
`_evict_while`. Returns ``(outcome, freed_bytes)``:
|
||||
|
||||
- ``("evicted", n)``: oldest entry dropped from the index; ``n`` disk
|
||||
bytes reclaimed (0 if the file was already gone).
|
||||
- ``("skipped", 0)``: oldest entry is an in-flight write; re-pinned at MRU
|
||||
so the writer is not evicted out from under itself.
|
||||
- ``("stop", 0)``: nothing evictable (empty index) or the unlink failed
|
||||
(entry re-pinned at LRU); the caller should stop its eviction loop.
|
||||
"""
|
||||
if not self._lru:
|
||||
return "stop", 0
|
||||
evict_stem, evict_size = self._lru.popitem(last=False) # oldest
|
||||
if evict_stem in self._pending_writes:
|
||||
# Keep in-flight reservations; their file isn't committed yet.
|
||||
self._lru[evict_stem] = evict_size
|
||||
return "skipped", 0
|
||||
tensor_path = os.path.join(self.file_path, f"{evict_stem}.bin")
|
||||
try:
|
||||
os.remove(tensor_path)
|
||||
freed = evict_size
|
||||
if self._on_evict is not None:
|
||||
self._on_evict(evict_stem)
|
||||
except FileNotFoundError:
|
||||
freed = 0 # file already gone; still drop the stale index entry
|
||||
if self._on_evict is not None:
|
||||
self._on_evict(evict_stem)
|
||||
except OSError as e:
|
||||
logger.warning(f"HiCacheFile eviction failed for {evict_stem}: {e}")
|
||||
self._lru[evict_stem] = evict_size
|
||||
self._lru.move_to_end(evict_stem, last=False)
|
||||
return "stop", 0
|
||||
self._total_bytes -= evict_size
|
||||
return "evicted", freed
|
||||
|
||||
def _evict_while(self, should_continue) -> int:
|
||||
"""Evict oldest non-pending entries while ``should_continue(reclaimed)``.
|
||||
|
||||
``should_continue`` is passed the disk bytes reclaimed so far and returns
|
||||
whether to keep evicting. In-flight writes are skipped; the loop is bounded
|
||||
so it can't spin once every remaining entry is pending. Caller holds _lock.
|
||||
Returns the total disk bytes reclaimed.
|
||||
"""
|
||||
reclaimed = 0
|
||||
attempts_left = len(self._lru)
|
||||
while self._lru and attempts_left > 0 and should_continue(reclaimed):
|
||||
outcome, freed = self._evict_one_lru_locked()
|
||||
if outcome == "stop":
|
||||
break
|
||||
if outcome == "skipped":
|
||||
attempts_left -= 1
|
||||
continue
|
||||
# An entry left the index; reset the skip budget and bank the bytes.
|
||||
reclaimed += freed
|
||||
attempts_left = len(self._lru)
|
||||
return reclaimed
|
||||
|
||||
def _evict_locked(self, needed_bytes: int) -> None:
|
||||
"""Evict LRU entries until total + needed <= cap*ratio. Caller holds _lock."""
|
||||
if self.max_size_bytes <= 0:
|
||||
return
|
||||
target = max(0, int(self.max_size_bytes * self.eviction_ratio) - needed_bytes)
|
||||
reclaimed = self._evict_while(lambda _: self._total_bytes > target)
|
||||
if reclaimed:
|
||||
logger.debug(
|
||||
f"HiCacheFile reclaimed {reclaimed} bytes; "
|
||||
f"now {self._total_bytes} bytes used"
|
||||
)
|
||||
Reference in New Issue
Block a user