Studio: tighten CPU-only detection and resident-footprint accounting

Follow-up review on the CPU-only / NUMA hardening:

- Treat a user-forced zero offload (-ngl 0 / --n-gpu-layers 0 / --gpu-layers 0)
  as CPU-only even when a GPU is visible, so the safe defaults (--fit off,
  --flash-attn off, NUMA interleave) and the RAM-aware context fit apply instead
  of a GPU launch that then runs on CPU and hits the graph-reserve abort.
- Size the CPU context fit against the fitted footprint (weights + compute
  buffer), not raw weights, so a context that only fits when the buffer is
  ignored can no longer slip through and get OS-killed at startup.
- Size the NUMA interleave decision against the full resident footprint
  (fitted weights + KV at the launched parallel slots + MTP reserve).

model_size_fit and the MTP reserve are bound before the fit try so the
GPU-selection except path cannot leave them unbound. New and updated unit
tests plus the offline simulation cover the zero-offload path and the math.
This commit is contained in:
Daniel Han 2026-06-29 06:47:51 +00:00
commit 32c9de2201
2 changed files with 90 additions and 17 deletions

View file

@ -1114,6 +1114,24 @@ def _extra_args_n_ubatch(
return None
def _extra_args_forces_cpu_offload(extra_args: Optional[Iterable[str]]) -> bool:
"""True when extras pin GPU offload to zero (-ngl 0 / --n-gpu-layers 0 /
--gpu-layers 0): the launch runs fully on CPU despite a visible GPU, so the
CPU-only safe defaults apply. Last occurrence wins, as llama-server parses it."""
args = [str(a) for a in extra_args] if extra_args else []
forced = False
for i, raw in enumerate(args):
flag, eq, inline = raw.partition("=")
if flag not in ("-ngl", "--n-gpu-layers", "--gpu-layers"):
continue
value = inline if eq else (args[i + 1] if i + 1 < len(args) else "")
try:
forced = int(value) == 0
except (TypeError, ValueError):
continue
return forced
def _build_ngram_mod_flags(
caps: Optional[dict],
n_match: int = 24,
@ -4955,6 +4973,8 @@ class LlamaCppBackend:
"image input will be disabled for this session"
)
model_size = None # set in the fit try; used by the APU RAM guard
model_size_fit = None # weights + compute buffer; set in the fit try
_mtp_reserve_bytes = 0 # MTP draft reserve at effective_ctx; set in the fit try
# Layer-fallback min GPUs; raised below on a tensor downgrade. Bound
# before the try so the --fit-on except path still has it (no UnboundLocal).
_layer_min_gpus = 1
@ -4976,6 +4996,12 @@ class LlamaCppBackend:
_gpu_mem = self._get_gpu_memory()
gpus = [(idx, free) for idx, free, _t in _gpu_mem]
total_by_idx = {idx: total for idx, _f, total in _gpu_mem}
# A user -ngl 0 / --n-gpu-layers 0 pins every layer on CPU even with a
# visible GPU; drop the GPU list so the CPU-only safe defaults and the
# RAM-aware fit apply, not a GPU launch that then runs on CPU anyway.
if gpus and _extra_args_forces_cpu_offload(extra_args):
logger.info("User set zero GPU offload (-ngl 0): treating as CPU-only.")
gpus, total_by_idx = [], {}
_cpu_only = (not gpus) and not _is_apple_silicon()
def _gpu_usable(g, frac = _CTX_FIT_VRAM_FRACTION):
@ -5704,8 +5730,12 @@ class LlamaCppBackend:
try:
if _avail_mib and model_size and self._can_estimate_kv():
_budget_b = _avail_mib * _CPU_RAM_BUDGET_FRAC * 1024 * 1024
if model_size >= _budget_b:
# Weights alone over budget: _fit_context_to_vram returns
# Fixed footprint = weights + compute buffer (the same lump
# the load allocates), so a context that only fits ignoring
# the buffer can't slip through and get OS-killed at startup.
_fixed = model_size_fit or model_size
if _fixed >= _budget_b:
# Footprint alone over budget: _fit_context_to_vram returns
# the ceiling unchanged, but KV at 32k could OOM. Floor to
# the minimum so the tightest fit gets the smallest context.
_cpu_cap = 4096
@ -5713,7 +5743,7 @@ class LlamaCppBackend:
_fit = self._fit_context_to_vram(
requested_ctx = _CPU_CTX_AUTO_CEILING,
available_mib = _avail_mib,
model_size_bytes = model_size,
model_size_bytes = _fixed,
cache_type_kv = cache_type_kv,
min_ctx = 4096,
n_parallel = n_parallel,
@ -5992,17 +6022,23 @@ class LlamaCppBackend:
try:
from core.inference.numa import decide_interleave
# Decide on the resident footprint (weights + KV at the capped
# context), not weights alone, so a model whose weights fit one node
# but whose footprint does not still interleaves.
_numa_footprint = model_size
if model_size and effective_ctx > 0 and self._can_estimate_kv():
# Decide on the full resident footprint (weights + compute buffer + KV
# at the capped context + MTP reserve), not weights alone, so a model
# whose weights fit one node but whose footprint does not still
# interleaves instead of first-touch thrashing on one node.
_resident = model_size_fit or model_size
_numa_footprint = _resident
if _resident and effective_ctx > 0 and self._can_estimate_kv():
try:
_numa_footprint = model_size + self._estimate_kv_cache_bytes(
effective_ctx, cache_type_kv, n_parallel = n_parallel
_numa_footprint = (
_resident
+ self._estimate_kv_cache_bytes(
effective_ctx, cache_type_kv, n_parallel = n_parallel
)
+ _mtp_reserve_bytes
)
except Exception:
_numa_footprint = model_size
_numa_footprint = _resident
_numa = decide_interleave(_numa_footprint, cpu_only = _cpu_only)
if _numa.interleave:
self._numa_prefix = list(_numa.prefix)

View file

@ -126,19 +126,56 @@ def test_cpu_ram_preflight_warns_when_weights_exceed_ram():
def test_cpu_context_floors_to_min_when_weights_exceed_budget():
"""When weights alone exceed the RAM budget, _fit_context_to_vram returns the
ceiling unchanged; the cap must floor to the minimum instead (PR review fix)."""
"""When the fixed footprint exceeds the RAM budget, _fit_context_to_vram returns
the ceiling unchanged; the cap must floor to the minimum instead (PR review fix)."""
src = _load_model_src()
assert "model_size >= _budget_b" in src
# The check uses the fitted footprint (weights + compute buffer), not raw weights.
assert "_fixed = model_size_fit or model_size" in src
assert "_fixed >= _budget_b" in src
assert "_cpu_cap = 4096" in src
def test_cpu_context_fit_uses_fitted_footprint():
"""The CPU RAM fit must pass the fitted footprint (weights + compute buffer), so a
context that only fits when the buffer is ignored can't slip through (PR review fix)."""
src = _load_model_src()
assert "model_size_bytes = _fixed" in src
def test_numa_decision_uses_footprint_not_just_weights():
"""The NUMA interleave decision must use weights + KV (at the launched parallel
slots), so a model whose weights fit one node but whose footprint does not still
interleaves (PR review fixes)."""
"""The NUMA interleave decision must use the full resident footprint (weights +
compute buffer + KV at the launched parallel slots + MTP reserve), so a model whose
weights fit one node but whose footprint does not still interleaves (PR review fixes)."""
src = _load_model_src()
assert "_numa_footprint" in src
assert "decide_interleave(_numa_footprint" in src
# Footprint = fitted weights (incl. compute buffer) + KV + MTP reserve.
assert "_resident = model_size_fit or model_size" in src
assert "_mtp_reserve_bytes" in src
# KV must be sized for the launched --parallel slots, not the n_parallel=1 default.
assert "effective_ctx, cache_type_kv, n_parallel = n_parallel" in src
def test_extra_args_forces_cpu_offload_helper():
"""The zero-offload detector: -ngl 0 / --n-gpu-layers 0 / --gpu-layers 0 (last wins)."""
from core.inference.llama_cpp import _extra_args_forces_cpu_offload as f
assert f(["-ngl", "0"])
assert f(["--n-gpu-layers", "0"])
assert f(["--gpu-layers", "0"])
assert f(["-ngl=0"])
assert not f(["-ngl", "99"])
assert not f([])
assert not f(None)
assert not f(["--flash-attn", "on"])
# Last occurrence wins, matching llama-server's own parsing.
assert f(["-ngl", "99", "-ngl", "0"])
assert not f(["-ngl", "0", "-ngl", "99"])
def test_zero_offload_folds_into_cpu_only():
"""A visible GPU plus a user -ngl 0 must be treated as CPU-only: the GPU list is
dropped before _cpu_only is computed so the CPU safe defaults apply (PR review fix)."""
src = _load_model_src()
assert "_extra_args_forces_cpu_offload(extra_args)" in src
assert "gpus, total_by_idx = [], {}" in src