unsloth/studio/backend/tests/test_cpu_only_defaults.py
Daniel Han 1813035d50 Studio: reserve fallback MTP RAM, bound NUMA on smallest node, capture abort early
Follow-up review on the CPU-only / NUMA hardening:

- Reserve a flat MTP fraction in the CPU context fit when MTP engages but the draft
  KV cannot be byte-sized (mtp_overhead_fn is None): budget_frac otherwise skips the
  flat reserve, so the fit could pick a context that OOMs once the draft allocates.
- Bound the NUMA local-placement decision on the smallest node's free RAM, not the
  largest. The loader is not pinned, so first-touch may land on any node; interleave
  a footprint that fits only the larger node instead of gambling on placement.
- Capture the first launch's scheduler abort before the no-spec MTP fallback resets
  the stdout tail, then fold it into the terminal memo decision, so a fallback that
  fails for another reason does not drop the memo and let the UI replay the load.

Unit tests and the offline simulations updated and passing.
2026-06-29 08:32:54 +00:00

294 lines
12 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Guards for CPU-only safe llama-server defaults in load_model.
On a CPU-only host (no discrete GPU, not Apple Metal) none of the GPU/Apple fit
branches run, so the launch used to keep GPU defaults: `--flash-attn on`, `--fit on`,
and the model's full native context. For large MLA/sparse-attention/MTP GGUFs (e.g.
GLM-5.2) `--fit`'s graph-reserve estimator aborts (llama.cpp #21932) and CPU flash
attention is unsafe. These tests pin that the launch now derives `_cpu_only` and uses
it to disable --fit and default flash-attn off, while leaving GPU/Apple behaviour and
user overrides intact. Source/AST level: load_model is too entangled to drive E2E.
"""
from __future__ import annotations
import ast
import inspect
import sys
import textwrap
import types as _types
from pathlib import Path
_BACKEND_DIR = str(Path(__file__).resolve().parent.parent)
if _BACKEND_DIR not in sys.path:
sys.path.insert(0, _BACKEND_DIR)
try:
import structlog # noqa: F401
except ImportError:
_s = _types.ModuleType("structlog")
_s.get_logger = lambda *a, **k: __import__("logging").getLogger("stub")
_s.BoundLogger = type("BoundLogger", (), {})
sys.modules["structlog"] = _s
# Importing core.inference.* runs core/inference/__init__.py (orchestrator + loggers +
# httpx); stub those when absent so a dependency-light run can still collect this file.
try:
import loggers # noqa: F401
except ImportError:
_loggers_stub = _types.ModuleType("loggers")
_loggers_stub.get_logger = lambda name: __import__("logging").getLogger(name)
sys.modules["loggers"] = _loggers_stub
try:
import httpx # noqa: F401
except ImportError:
_httpx_stub = _types.ModuleType("httpx")
for _exc in (
"ConnectError",
"TimeoutException",
"ReadTimeout",
"ReadError",
"RemoteProtocolError",
"CloseError",
"HTTPError",
"RequestError",
):
setattr(_httpx_stub, _exc, type(_exc, (Exception,), {}))
_httpx_stub.Timeout = type("T", (), {"__init__": lambda s, *a, **k: None})
_httpx_stub.Response = type("Response", (), {})
_httpx_stub.Client = type(
"C",
(),
{
"__init__": lambda s, **kw: None,
"__enter__": lambda s: s,
"__exit__": lambda s, *a: None,
},
)
sys.modules["httpx"] = _httpx_stub
from core.inference.llama_cpp import LlamaCppBackend # noqa: E402
def _load_model_src() -> str:
return textwrap.dedent(inspect.getsource(LlamaCppBackend.load_model))
def test_cpu_only_flag_derived_from_no_gpu_and_not_apple():
src = _load_model_src()
assert "_cpu_only = (not gpus) and not _is_apple_silicon()" in src
def test_flash_attn_default_is_off_on_cpu_only():
"""The base cmd must use a computed flash default, off for CPU-only."""
src = _load_model_src()
assert '_flash_default = "off" if _cpu_only else "on"' in src
# And the base cmd must NOT hardcode flash-attn on anymore.
assert '"on", # Force flash attention for speed' not in src
# The --flash-attn argument in the base cmd list is the computed default.
assert "_flash_default, # CPU-only" in src
def test_fit_disabled_on_cpu_only():
src = _load_model_src()
assert "if _cpu_only and use_fit:" in src
fn = ast.parse(src).body[0]
# There is an `if _cpu_only and use_fit:` whose body sets use_fit = False.
found = False
for node in ast.walk(fn):
if (
isinstance(node, ast.If)
and isinstance(node.test, ast.BoolOp)
and any(isinstance(v, ast.Name) and v.id == "_cpu_only" for v in ast.walk(node.test))
):
for n in node.body:
if (
isinstance(n, ast.Assign)
and any(isinstance(t, ast.Name) and t.id == "use_fit" for t in n.targets)
and isinstance(n.value, ast.Constant)
and n.value.value is False
):
found = True
assert found, "expected `if _cpu_only and use_fit: ... use_fit = False`"
def test_gpu_path_still_emits_fit_on():
"""Backwards compat: the GPU branch still emits --fit on (only CPU changes)."""
src = _load_model_src()
assert 'cmd.extend(["--fit", "on"])' in src
def test_cpu_only_emits_explicit_fit_off():
"""--fit defaults to on in llama.cpp, so CPU-only must pass --fit off explicitly,
not just skip --fit on (PR review fix)."""
src = _load_model_src()
assert "elif _cpu_only:" in src
assert 'cmd.extend(["--fit", "off"])' in src
# ---- Phase 3: CPU context cap + RAM preflight ------------------------------
def test_cpu_context_ceiling_constant_is_sane():
from core.inference import llama_cpp as m
assert hasattr(m, "_CPU_CTX_AUTO_CEILING")
# A safe chat ceiling, far below a model's million-token native context.
assert 4096 <= m._CPU_CTX_AUTO_CEILING <= 131072
assert 0.5 < m._CPU_RAM_BUDGET_FRAC <= 1.0
def test_cpu_only_caps_auto_context_but_honors_explicit():
src = _load_model_src()
# The fit runs for any auto context (requested_ctx <= 0), against a ceiling of
# min(native, 32k); an explicit -c (requested_ctx > 0) is honored untouched.
assert "requested_ctx <= 0 and effective_ctx > 0" in src
assert "_ctx_ceiling = min(effective_ctx, _CPU_CTX_AUTO_CEILING)" in src
assert "effective_ctx = _cpu_cap" in src
def test_cpu_context_fit_runs_below_ceiling_too():
"""A large GGUF with a native context already <= 32k must still be RAM-fit, not
skipped, so it can be reduced toward 4096 instead of OS-killed (PR review fix)."""
src = _load_model_src()
# The gate is `> 0`, not `> _CPU_CTX_AUTO_CEILING`, and the fit ceiling is clamped.
assert "effective_ctx > _CPU_CTX_AUTO_CEILING" not in src
assert "requested_ctx = _ctx_ceiling" in src
def _nows(s: str) -> str:
"""Whitespace-stripped source, so assertions survive the formatter wrapping a line."""
return "".join(s.split())
def test_cpu_context_fit_accounts_for_mtp():
"""mtp_engaged alone is a no-op once budget_frac is set, so the CPU fit must pass the
byte-accurate MTP overhead fn to actually reserve MTP KV (PR review fix)."""
src = _nows(_load_model_src())
assert _nows("mtp_overhead_fn = (_mtp_bytes if _mtp_will_engage_cpu else None)") in src
def test_cpu_context_cap_reuses_fit_helper_against_ram():
"""The cap reuses _fit_context_to_vram with the system-RAM budget, not VRAM."""
src = _load_model_src()
assert "_available_system_memory_mib()" in src
assert "self._fit_context_to_vram(" in src
assert "_cpu_budget = _CPU_RAM_BUDGET_FRAC" in src
assert "budget_frac = _cpu_budget" in src
def test_cpu_ram_preflight_warns_when_weights_exceed_ram():
src = _load_model_src()
assert "CPU-only memory preflight" in src
def test_cpu_context_floors_to_min_when_weights_exceed_budget():
"""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()
# 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 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
# MTP is recomputed at the post-cap context, not the stale pre-cap reserve.
assert _nows("_numa_mtp = _mtp_bytes(effective_ctx) if _mtp_will_engage_cpu else 0") in _nows(
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_numa_surfaces_total_ram_failure():
"""When the footprint exceeds total RAM across all nodes, decide_interleave returns
an actionable 'interleave cannot help' reason; the caller must surface it, not only
the missing-numactl case (PR review fix)."""
src = _load_model_src()
assert '"interleave cannot help" in _numa.reason' in src
def test_explicit_user_numa_skips_auto_interleave_prefix():
"""An explicit user --numa must skip the numactl argv prefix (which user extra args
can't override), not just the --numa distribute flag (PR review fix)."""
src = _load_model_src()
assert 'if _numa.interleave and _extra_args_set_any_flag(extra_args, {"--numa"}):' in src
assert "leaving auto-interleave off" in src
def test_extra_args_forces_cpu_offload_helper():
"""The CPU-force detector: zero GPU layers or --device none (PR review fixes)."""
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"])
# Each flag's last occurrence wins, matching llama-server's own parsing.
assert f(["-ngl", "99", "-ngl", "0"])
assert not f(["-ngl", "0", "-ngl", "99"])
# --device/-dev none also forces CPU, independently of -ngl.
assert f(["--device", "none"])
assert f(["-dev", "none"])
assert f(["--device=none"])
assert not f(["--device", "CUDA0"])
# The two controls are independent: -ngl 0 stays CPU even with a device named.
assert f(["-ngl", "0", "--device", "CUDA0"])
assert f(["--device", "none", "-ngl", "99"])
def test_cpu_cap_lowers_advertised_ceiling():
"""When the CPU cap reduces the launched context, max_available_ctx must drop too,
so /status and the UI safe-zone reflect the real window, not native (PR review fix)."""
src = _load_model_src()
assert "max_available_ctx = min(max_available_ctx, _cpu_cap)" in src
def test_cpu_fit_skips_mtp_reserve_when_mla_auto_drops():
"""Auto drops embedded MTP for MLA models, so the CPU cap / NUMA footprint must not
reserve a target-KV copy for a drafter that won't launch (PR review fix)."""
src = _nows(_load_model_src())
assert _nows("_mtp_will_engage_cpu = _mtp_will_engage and not (") in src
assert _nows("not _mla_mtp_auto_enabled()") in src
# The CPU cap and NUMA recompute use the gated flag, not the raw _mtp_will_engage.
assert _nows("mtp_overhead_fn = (_mtp_bytes if _mtp_will_engage_cpu else None)") in src
assert _nows("_numa_mtp = _mtp_bytes(effective_ctx) if _mtp_will_engage_cpu else 0") in src
def test_cpu_fit_reserves_flat_mtp_when_draft_unsized():
"""When MTP engages but the draft KV can't be byte-sized (mtp_overhead_fn is None),
budget_frac skips the flat reserve, so the CPU budget is trimmed to still hold MTP
RAM back instead of fitting a context that OOMs once the draft allocates (PR review)."""
src = _load_model_src()
assert "if _mtp_will_engage_cpu and mtp_overhead_fn is None:" in src
assert "_cpu_budget -= _MTP_VRAM_RESERVE_FRAC" in src
assert "budget_frac = _cpu_budget" in src
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