modeld: no-spill placement (refuse, don’t degrade)
Status: draft blueprint, 2026-07-16. Grounded in a screen of the current capacity/placement code. Slice 1 scoped; follow-ups listed.
Ethos this enforces
modeld serves a right-sized model with good latency and is safe to run 24/7 on a machine you also game/work on. When a model doesn’t fit the device budget, modeld should refuse (or shrink context to fit) and say why — not silently degrade to hybrid CPU inference. Low latency + host safety beat “it technically loaded.”
The bug this fixes (observed 2026-07-16)
Asking a 6 GB GPU to serve a ~4.8 GB model made modeld shed GPU layers to the CPU to reach a usable-context floor — running weights and their KV in host RAM, pegging ~430% CPU and driving the box toward swap, to answer “hi”. That is the ethos violated: it heroically loaded a model that doesn’t fit instead of refusing and pointing at a right-sized one.
Root cause (screened)
modeld/capacity/capacity.goResolveis fine — it already shrinks the context window to the memory budget and flagsClamped/Reason.modeld/llama/service.goresolveGPULayersForBudgetis the culprit: in AUTO mode (no explicitnum_ctx) it walks GPU layers down from full offload, trading offloaded layers for a bigger KV window to reachMinHotContextTokens— i.e. it puts weights on the CPU to buy context.resolveSessionalready refuses when even maximal shedding can’t reach the floor (theHotContextTokens < floorcheck) — so the machinery to refuse exists; it just tries to spill first.
Slice 1 — no-spill auto placement (careful, focused)
The single behavioral change, plus honest messaging. Do NOT touch the working inference path beyond this.
- Auto mode never sheds layers. In
resolveGPULayersForBudget, when there is no explicit GPU-layer cap and no explicitnum_ctx, evaluate placement at full offload only and return the full (all-layers-that-fit-the-model) count. Do not walk down to buy context. The resulting context is whatever fits at full offload (capacity.Resolvealready computes it). - Refuse, don’t partial-offload, when full offload doesn’t fit. If weights
- minimum-KV + overhead exceed the device budget at full offload (context resolves to ~0), REFUSE — do not shed to a partial-CPU placement.
- Honest refusal. Improve the
resolveSessionrefusal (and theEffectiveContext<=0path) to state the budget arithmetic and the action: e.g. “model X needs ~A GiB (weights B + min-KV C + reserve D) but the device has ~E GiB free — use a smaller model or quant, or free VRAM.” Actionable, not a bare error code. - Shedding stays an explicit operator override only.
CONTENOX_LLAMA_GPU_LAYERS=n(and a model-profile cap) still work exactly as before — the walk-down logic remains reachable only when an explicit cap is set. Auto is no-spill; forcing hybrid is a deliberate operator choice. - Tests. Unit-cover: a model that fits full-offload with room → full
layers, sensible context; a model too big for full offload → refuse with the
arithmetic in the message (NOT a partial-layer placement); an explicit
CONTENOX_LLAMA_GPU_LAYERScap → still honored (walk-down still reachable); CPU-only host (no accelerator) → unchanged (0 GPU layers by design, runs on CPU). Keep the existing capacity/service tests green.
Net effect: qwen3-8b on a 6 GB card now refuses with a clear message (correct); qwen3-4b loads fully on-device with a real context (correct); nobody’s gaming rig melts answering “hi”.
Follow-ups (later slices, NOT slice 1)
- Placement plan + doctor authority. A read-only, header-only pre-load
report (weights/KV/overhead footprint, achievable context per device, a
fit/won’t-fit verdict) as versioned JSON shared by CLI/API/UI — the basis for
the UI “fits on your GPU?” indicator and an honest
contenox doctorplacement check that runs before loading. (Extends the existingDescribe/ModelInfo budget breakdown, which already logs all the pieces.) - Reserve-stack rationalization. The current budget stacks min-free +
DefaultMaxResidentFrac(80%) +DefaultHeadroomFrac(10%) + a batch-scaled compute reserve — together withholding ~45% of a small card. Decide: keep min-free + compute reserve (the real costs), drop/floor the headroom fraction and the 80% resident cap (redundant with min-free). Needs its own careful pass + the maintainer’s call on the reserve sizes. - Warm-KV / session-resume latency for agentic multi-turn (compare against the existing prefix-stable/coldstore/snapshot path).