mmh3testing / lora /cache.py
observantdistressed's picture
refactor: split app.py into focused modules (#5)
64db20c
Raw History Blame Contribute Delete
3.16 kB
"""LoRA cache quota management.
Two helpers:
- `_lora_cache_usage_bytes()` — best-effort total size of files in the cache dir.
- `_evict_lora_cache_for_quota(needed)` — LRU-evict cache files (skipping any
referenced by an in-memory LoRA store) until `needed` more bytes fit, or
raise `gr.Error` if the quota still cannot accommodate the request.
"""
from __future__ import annotations
import os
import re
import gradio as gr
from state import (
_CUSTOM_LoRA,
_H3_MAX_LORA_CACHE_BYTES,
_LORA_CACHE_DIR,
_log,
)
def _lora_cache_usage_bytes() -> int:
"""Sum sizes of files under the LoRA cache dir (best-effort)."""
total = 0
try:
if not os.path.isdir(_LORA_CACHE_DIR):
return 0
for name in os.listdir(_LORA_CACHE_DIR):
p = os.path.join(_LORA_CACHE_DIR, name)
if os.path.isfile(p) and not name.endswith(".tmp"):
try:
total += os.path.getsize(p)
except OSError:
pass
except OSError:
pass
return total
def _evict_lora_cache_for_quota(needed: int) -> None:
"""Evict oldest unreferenced cache files until *needed* bytes fit, or raise.
Only deletes files that are not currently referenced by in-memory LoRA stores.
Never crashes the process solely because the cache is full — rejects the new
artifact instead.
"""
if needed <= 0:
return
usage = _lora_cache_usage_bytes()
if usage + needed <= _H3_MAX_LORA_CACHE_BYTES:
return
# Build set of keys currently held in RAM (do not evict those).
live = set()
for k in list(_CUSTOM_LoRA.keys()):
if isinstance(k, tuple) and len(k) >= 2:
live.add(re.sub(r"[^A-Za-z0-9._-]", "_", f"{k[0].replace('/', '__')}__{k[1]}"))
candidates = []
try:
for name in os.listdir(_LORA_CACHE_DIR):
if name.endswith(".tmp") or name.startswith("."):
continue
p = os.path.join(_LORA_CACHE_DIR, name)
if not os.path.isfile(p):
continue
# Skip if name is a prefix match of a live key
if any(name.startswith(lk) or lk.startswith(name.split("__rev_")[0]) for lk in live):
continue
try:
candidates.append((os.path.getmtime(p), os.path.getsize(p), p, name))
except OSError:
pass
except OSError:
pass
candidates.sort() # oldest first
freed = 0
for _mtime, size, p, name in candidates:
if usage + needed - freed <= _H3_MAX_LORA_CACHE_BYTES:
break
try:
os.remove(p)
freed += size
_log(f"LoRA cache evict (quota): {name} ({size / 1e6:.1f} MB)")
except OSError as _e:
_log(f"LoRA cache evict failed for {name}: {_e}", "WARN")
if usage + needed - freed > _H3_MAX_LORA_CACHE_BYTES:
raise gr.Error(
f"LoRA cache quota exceeded (need {needed / 1e6:.0f} MB, "
f"limit {_H3_MAX_LORA_CACHE_BYTES / 1e9:.1f} GB). "
"Try again later or use a smaller LoRA."
)