hugpy-engine 0.2.0a0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hugpy_engine/__init__.py +163 -0
- hugpy_engine/alloc_modes.py +1254 -0
- hugpy_engine/allocation.py +68 -0
- hugpy_engine/apis/__init__.py +0 -0
- hugpy_engine/apis/call_api.py +149 -0
- hugpy_engine/apis/get_module.py +606 -0
- hugpy_engine/apis/reclassify.py +123 -0
- hugpy_engine/apis/reconcile.py +628 -0
- hugpy_engine/apis/serve/__init__.py +22 -0
- hugpy_engine/apis/serve/serve.py +34 -0
- hugpy_engine/apis/serve/serve_cli.py +9 -0
- hugpy_engine/apis/systemd_units.py +9 -0
- hugpy_engine/backend.py +86 -0
- hugpy_engine/backends/__init__.py +5 -0
- hugpy_engine/backends/local.py +163 -0
- hugpy_engine/catalog.py +99 -0
- hugpy_engine/catalog_bridge.py +223 -0
- hugpy_engine/categories.py +117 -0
- hugpy_engine/chat_context/__init__.py +27 -0
- hugpy_engine/chat_context/chat_context.py +199 -0
- hugpy_engine/chat_context/context_budget.py +223 -0
- hugpy_engine/chat_context/unbounded.py +55 -0
- hugpy_engine/config/__init__.py +0 -0
- hugpy_engine/config/main.py +270 -0
- hugpy_engine/config/models/__init__.py +0 -0
- hugpy_engine/config/models/model_meta.py +169 -0
- hugpy_engine/config/models/models_config.py +1495 -0
- hugpy_engine/config/models/models_default.py +196 -0
- hugpy_engine/config/models/models_dict.py +73 -0
- hugpy_engine/contracts.py +44 -0
- hugpy_engine/dispatch/__init__.py +37 -0
- hugpy_engine/dispatch/acquire.py +59 -0
- hugpy_engine/dispatch/activity.py +92 -0
- hugpy_engine/dispatch/dispatch.py +1100 -0
- hugpy_engine/draft_models.py +72 -0
- hugpy_engine/engines.py +44 -0
- hugpy_engine/eviction.py +654 -0
- hugpy_engine/generate/__init__.py +16 -0
- hugpy_engine/generate/coder.py +667 -0
- hugpy_engine/generate/config.py +185 -0
- hugpy_engine/generate/generate_runner.py +273 -0
- hugpy_engine/gguf_election.py +259 -0
- hugpy_engine/llama/__init__.py +23 -0
- hugpy_engine/llama/runners/__init__.py +23 -0
- hugpy_engine/llama/runners/chat_runner.py +243 -0
- hugpy_engine/llama/runners/get.py +524 -0
- hugpy_engine/llama/runners/src/__init__.py +9 -0
- hugpy_engine/llama/runners/src/base_runner.py +529 -0
- hugpy_engine/llama/runners/src/ccp_runner.py +332 -0
- hugpy_engine/llama/runners/src/imports/__init__.py +25 -0
- hugpy_engine/llama/runners/src/imports/config.py +16 -0
- hugpy_engine/llama/runners/src/imports/constants.py +38 -0
- hugpy_engine/llama/runners/src/imports/init_imports.py +6 -0
- hugpy_engine/llama/runners/src/imports/utils.py +64 -0
- hugpy_engine/llama/runners/src/python_runner.py +432 -0
- hugpy_engine/llama/runners/src/shard_server.py +339 -0
- hugpy_engine/llama/runners/src/shard_server.py.bak-shardorder +329 -0
- hugpy_engine/llama/serve.py +9 -0
- hugpy_engine/manifest.py +53 -0
- hugpy_engine/model_classifier.py +374 -0
- hugpy_engine/model_index/__init__.py +74 -0
- hugpy_engine/model_index/client.py +94 -0
- hugpy_engine/model_index/query_registry.py +245 -0
- hugpy_engine/model_index/repositories.py +163 -0
- hugpy_engine/model_index/service.py +243 -0
- hugpy_engine/name_match.py +287 -0
- hugpy_engine/native/__init__.py +19 -0
- hugpy_engine/native/build.py +98 -0
- hugpy_engine/native/fetch.py +194 -0
- hugpy_engine/native/resolve.py +352 -0
- hugpy_engine/peft_adapters.py +293 -0
- hugpy_engine/placement.py +459 -0
- hugpy_engine/py.typed +0 -0
- hugpy_engine/query.py +173 -0
- hugpy_engine/resolvers/__init__.py +24 -0
- hugpy_engine/resolvers/allocator.py +126 -0
- hugpy_engine/resolvers/assure_model_key.py +331 -0
- hugpy_engine/resolvers/categories/__init__.py +22 -0
- hugpy_engine/resolvers/categories/builders.py +159 -0
- hugpy_engine/resolvers/categories/frameworks.py +67 -0
- hugpy_engine/resolvers/groups.py +651 -0
- hugpy_engine/resolvers/model_dict_resolver.py +122 -0
- hugpy_engine/resolvers/model_resolver.py +533 -0
- hugpy_engine/resolvers/remote.py +2992 -0
- hugpy_engine/runtime.py +68 -0
- hugpy_engine/schemas/__init__.py +0 -0
- hugpy_engine/schemas/chat_schemas.py +320 -0
- hugpy_engine/schemas/event_schemas.py +52 -0
- hugpy_engine/schemas/metadata_schemas.py +62 -0
- hugpy_engine/schemas/model_schemas.py +101 -0
- hugpy_engine/schemas/runner_schemas.py +33 -0
- hugpy_engine/schemas/task_schemas.py +45 -0
- hugpy_engine/serve/__init__.py +58 -0
- hugpy_engine/serve/chunksum_verify.py +213 -0
- hugpy_engine/serve/hot_cache.py +1014 -0
- hugpy_engine/serve/model_cache.py +218 -0
- hugpy_engine/serve/overrides.py +968 -0
- hugpy_engine/serve/policy.py +45 -0
- hugpy_engine/serve/profiles.py +362 -0
- hugpy_engine/serve/serve.py +889 -0
- hugpy_engine/serve/serve.py.bak-present-20260918-035851 +863 -0
- hugpy_engine/serve/serve.py.bak-servelog-20260918-035149 +863 -0
- hugpy_engine/serve/serve_cli.py +85 -0
- hugpy_engine/serve/slot_agent.py +2119 -0
- hugpy_engine/serve/slot_agent.py.bak-quietlog-20260918-023710 +2046 -0
- hugpy_engine/serve/slot_agent.py.bak-slotparallel-20260918-112917 +2064 -0
- hugpy_engine/serve/slot_agent.py.bak-wzfilter-20260918-041625 +2047 -0
- hugpy_engine/serve/slots.py +630 -0
- hugpy_engine/serve/supervisor.py +148 -0
- hugpy_engine/spill.py +1464 -0
- hugpy_engine/task_deps.py +67 -0
- hugpy_engine/tasks.py +349 -0
- hugpy_engine/utils/__init__.py +0 -0
- hugpy_engine/utils/json_scavenge.py +111 -0
- hugpy_engine/utils/no_think.py +370 -0
- hugpy_engine/wire/__init__.py +0 -0
- hugpy_engine/wire/chat_schemas.py +66 -0
- hugpy_engine/wire/config_schemas.py +31 -0
- hugpy_engine/wire/install_schemas.py +17 -0
- hugpy_engine/wire/model_schemas.py +43 -0
- hugpy_engine/wire/request_schemas.py +13 -0
- hugpy_engine/wire/specs_schemas.py +9 -0
- hugpy_engine-0.2.0a0.dist-info/METADATA +116 -0
- hugpy_engine-0.2.0a0.dist-info/RECORD +128 -0
- hugpy_engine-0.2.0a0.dist-info/WHEEL +5 -0
- hugpy_engine-0.2.0a0.dist-info/entry_points.txt +2 -0
- hugpy_engine-0.2.0a0.dist-info/licenses/LICENSE +41 -0
- hugpy_engine-0.2.0a0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1254 @@
|
|
|
1
|
+
"""The FIVE allocation modes — the operator-facing placement vocabulary (k37).
|
|
2
|
+
|
|
3
|
+
Operator spec 2026-07-23 (allocation-modes-spec): every model gets ONE of five
|
|
4
|
+
FLAT modes. Each mode = placement preference + bust condition:
|
|
5
|
+
|
|
6
|
+
gpu-only all layers on the GPU, no spill; won't fit GPU (after evict) -> bust
|
|
7
|
+
ram-only all in RAM, never the GPU (binds CPU even with a GPU present);
|
|
8
|
+
won't fit RAM -> bust
|
|
9
|
+
max-gpu as much GPU as available+needed, spill the rest to RAM;
|
|
10
|
+
can't satisfy -> bust (THE DEFAULT — a blank
|
|
11
|
+
model serves-and-spills, never OOMs: defaults-are-promises)
|
|
12
|
+
max-ram as much RAM as available+needed, spill the rest to GPU;
|
|
13
|
+
can't satisfy -> bust
|
|
14
|
+
explicit target VRAM/RAM budgets + a leniency%% + a device priority
|
|
15
|
+
(gpu default); can't fit even at the loosened floor -> bust
|
|
16
|
+
|
|
17
|
+
Internally max-gpu / max-ram are explicit(priority-device, ~100%% target,
|
|
18
|
+
generous leniency) — but they stay FIVE FLAT NAMES on every surface. The split
|
|
19
|
+
is a COGNITIVE-LOAD boundary, not an implementation one (operator ruling):
|
|
20
|
+
"use my GPU, spill the rest" must stay a zero-knob pick; the moment you tune
|
|
21
|
+
(%%/leniency/priority) you reach for explicit. Never collapse max-* into
|
|
22
|
+
explicit in any UI/API surface.
|
|
23
|
+
|
|
24
|
+
LENIENCY MATH (operator-confirmed): N%% leniency = up to N%% OF THE MODEL may
|
|
25
|
+
land off its ideal device before bust. Ideal 100%% GPU + 30%% leniency ->
|
|
26
|
+
degrade step-by-step down to the FLOOR 70%% GPU / 30%% RAM; only bust when even
|
|
27
|
+
the floor won't fit. The conversion onto the tolerance-band engine
|
|
28
|
+
(worker_agent/flex.py) is: whole = the MODEL's bytes, deviation = leniency%%,
|
|
29
|
+
so ``band_floor(target_bytes, leniency_pct, model_bytes)`` IS the floor.
|
|
30
|
+
|
|
31
|
+
THE HONEST RENAME (keeper owns nomenclature): today's console "Max GPU"
|
|
32
|
+
(n_gpu_layers=-1, all-or-OOM) is really **gpu-only**; today's "autofit"
|
|
33
|
+
(as-much-GPU-as-fits, spill rest) is really **max-gpu**. Legacy names are
|
|
34
|
+
accepted on INPUT (resolved + logged), never emitted back. NOTE the collision:
|
|
35
|
+
the STRING "max-gpu" exists in both vocabularies with different meanings —
|
|
36
|
+
resolution is canonical-first, so "max-gpu" always reads as the NEW max-gpu
|
|
37
|
+
(fit-and-spill); the historical -1 meaning is only reachable as "gpu-only"
|
|
38
|
+
(nothing persisted ever stored the old string — the old wire encoding was the
|
|
39
|
+
n_gpu_layers value itself, which read-time DERIVATION maps correctly).
|
|
40
|
+
|
|
41
|
+
WIRE ENCODING (unchanged for the three legacy-expressible modes — keeper
|
|
42
|
+
amendment 3: n_gpu_layers semantics NEVER change on the wire):
|
|
43
|
+
gpu-only -> {"n_gpu_layers": -1}
|
|
44
|
+
ram-only -> {"n_gpu_layers": "off"}
|
|
45
|
+
max-gpu -> {} (autofit, zero knobs)
|
|
46
|
+
max-ram -> {"alloc_mode": "max-ram"} (NEW keys)
|
|
47
|
+
|
|
48
|
+
⚠ max-gpu HAS TWO ENCODINGS, and the distinction is the whole point of the
|
|
49
|
+
2026-07-25 bugfix — they differ in PROVENANCE, not in behavior:
|
|
50
|
+
|
|
51
|
+
{} a DERIVED / blank max-gpu. Nothing is persisted;
|
|
52
|
+
the model re-derives against current conditions
|
|
53
|
+
on every read. Also THE CLEAR SIGNAL: assign_model
|
|
54
|
+
treats an empty spill as "remove this row".
|
|
55
|
+
{"alloc_mode": "max-gpu"} an EXPLICIT operator pick. Persisted, so it is
|
|
56
|
+
immune to the derivation changing under it.
|
|
57
|
+
|
|
58
|
+
Both mean max-gpu TO THE WORKER, and neither reaches it as a mode key: the
|
|
59
|
+
explicit form is stripped at the emission seam (WorkerStore.spill_for), so the
|
|
60
|
+
wire is byte-identical to today's {} in both cases. The stripping stays because
|
|
61
|
+
the wire contract is frozen, but it is no longer LOAD-BEARING for placement: a
|
|
62
|
+
mode key on the worker used to suppress the auto MoE split entirely (making an
|
|
63
|
+
explicit max-gpu WORSE than a blank one), and since the dense-backbone-first
|
|
64
|
+
ruling (2026-07-31) no mode can suppress it — every mode that grants a GPU
|
|
65
|
+
budget spends it on the dense backbone first (managers.serve.slot_agent.
|
|
66
|
+
_moe_gpu_budget + spill.moe_dense_first_plan). What a mode still decides is HOW
|
|
67
|
+
MUCH card the model may claim.
|
|
68
|
+
|
|
69
|
+
The old code collapsed the explicit form to {} here, which fed it straight into
|
|
70
|
+
the clear path: selecting max-gpu in the console DELETED the row instead of
|
|
71
|
+
writing it. See normalize_spill for the full incident note.
|
|
72
|
+
explicit -> {"alloc_mode": "explicit", gpu_mem_gib?, cpu_mem_gib?,
|
|
73
|
+
"leniency_pct"?, "priority_device"?, priority?} (NEW keys)
|
|
74
|
+
|
|
75
|
+
VERSION GATE (no dead knobs): the NEW spill keys are only emitted to workers
|
|
76
|
+
whose pkg_version honors them (>= MODE_MIN_PKG_VERSION). Released workers
|
|
77
|
+
IGNORE unknown spill keys (verified safe), but a selected mode must never be a
|
|
78
|
+
silent no-op — an old worker's request is downgraded to max-gpu ({} autofit)
|
|
79
|
+
and the downgrade is logged/surfaced (gate_spill_for_worker).
|
|
80
|
+
|
|
81
|
+
This module is PURE (stdlib only) so chaos, the routes, overrides, and the
|
|
82
|
+
worker can all share ONE vocabulary without import weight.
|
|
83
|
+
"""
|
|
84
|
+
from __future__ import annotations
|
|
85
|
+
|
|
86
|
+
import logging
|
|
87
|
+
from typing import Any, Optional
|
|
88
|
+
|
|
89
|
+
logger = logging.getLogger(__name__)
|
|
90
|
+
|
|
91
|
+
# The five flat operator-facing modes. Order matters only for display.
|
|
92
|
+
ALLOC_MODES = ("gpu-only", "ram-only", "max-gpu", "max-ram", "explicit")
|
|
93
|
+
|
|
94
|
+
# Legacy name -> canonical mode. Accepted on input, resolved + logged, never
|
|
95
|
+
# emitted back. "max-gpu" is listed for the HISTORICAL record (old "Max GPU"
|
|
96
|
+
# = -1 = new gpu-only) but is UNREACHABLE at runtime: resolution is
|
|
97
|
+
# canonical-first and "max-gpu" is a canonical name of the new vocabulary.
|
|
98
|
+
LEGACY_ALLOC_ALIASES = {
|
|
99
|
+
"autofit": "max-gpu", # old fit-and-spill default -> the new honest name
|
|
100
|
+
"cpu-only": "ram-only", # old CPU-only -> RAM is what it actually binds
|
|
101
|
+
"cpu_only": "ram-only",
|
|
102
|
+
"max-gpu": "gpu-only", # HISTORICAL ONLY (all-or-OOM); see module note
|
|
103
|
+
"max_gpu": "gpu-only", # (underscore form has no canonical collision)
|
|
104
|
+
"gpu_only": "gpu-only",
|
|
105
|
+
"ram_only": "ram-only",
|
|
106
|
+
"max_ram": "max-ram",
|
|
107
|
+
"budget": "explicit", # old chaos budget draw = an explicit gpu budget
|
|
108
|
+
"bands": "explicit", # old chaos band draw = explicit with a band
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Spill keys that only a mode-aware worker understands. Presence of ANY of
|
|
112
|
+
# these on a spill makes it version-gated (gate_spill_for_worker). n_cpu_moe
|
|
113
|
+
# (the MoE expert-split knob, 2026-07-24) ships in the SAME cut as the mode
|
|
114
|
+
# keys, so the one MODE_MIN_PKG_VERSION gate covers it — an older worker would
|
|
115
|
+
# silently drop the knob (unknown spill keys are ignored), and a selected knob
|
|
116
|
+
# must never be a silent no-op.
|
|
117
|
+
NEW_SPILL_KEYS = frozenset({"alloc_mode", "leniency_pct", "priority_device",
|
|
118
|
+
"n_cpu_moe"})
|
|
119
|
+
|
|
120
|
+
# First worker package version whose spill/env plumbing honors the new keys
|
|
121
|
+
# (Slice B2 ships in this cut). Anything older gets the max-gpu fallback.
|
|
122
|
+
MODE_MIN_PKG_VERSION = "0.1.203"
|
|
123
|
+
|
|
124
|
+
# ── k56: POLITE LOAD (``no_evict``) ─────────────────────────────────────────
|
|
125
|
+
# The per-model inverse of declare-need-then-evict (which stays the rule for
|
|
126
|
+
# unflagged loads): admission may spend only GENUINELY FREE headroom — the free
|
|
127
|
+
# VRAM left after the tolerance-band flex — and never triggers an eviction.
|
|
128
|
+
#
|
|
129
|
+
# It rides the SAME spill wire as the mode keys but carries its OWN version
|
|
130
|
+
# gate, because it ships a whole cut later (0.1.226) and the two must not be
|
|
131
|
+
# conflated: a 0.1.210 worker honors alloc_mode perfectly and would still evict
|
|
132
|
+
# a resident for a polite load. Deliberately NOT in NEW_SPILL_KEYS — that set
|
|
133
|
+
# means "downgrade the whole spill to max-gpu ({} autofit)", which for a polite
|
|
134
|
+
# load would silently DROP the politeness and keep the placement, i.e. exactly
|
|
135
|
+
# the silent-no-op failure the gate exists to prevent. The gate below strips the
|
|
136
|
+
# key and says so LOUDLY instead; central's resolution refuses to route a polite
|
|
137
|
+
# model to a worker that predates it (see workers._polite_admits).
|
|
138
|
+
NO_EVICT_SPILL_KEY = "no_evict"
|
|
139
|
+
NO_EVICT_MIN_PKG_VERSION = "0.1.226"
|
|
140
|
+
|
|
141
|
+
# ── t74: HARD NO-THINK — per-request engine chat keys ───────────────────────
|
|
142
|
+
# ``chat_template_kwargs`` (the Qwen3 ``enable_thinking:false`` idiom — the
|
|
143
|
+
# chat template pre-closes ``<think>\n\n</think>`` so generation starts with
|
|
144
|
+
# thinking already shut) and ``logit_bias`` (the <think>-token-ban fallback)
|
|
145
|
+
# now ride ChatRequest and are forwarded verbatim onto the slot child's
|
|
146
|
+
# llama-server /v1/chat/completions body. They exist because /no_think is a
|
|
147
|
+
# SOFT switch some models ignore outright (wazimondo~Qwen3.6-35B-A3B-
|
|
148
|
+
# Uncensored-Wasserstein-GGUF burns its whole budget thinking anyway); the
|
|
149
|
+
# template kwarg is enforced by the TEMPLATE, not by model obedience.
|
|
150
|
+
#
|
|
151
|
+
# These are NOT spill keys (they steer the request body, never placement), so
|
|
152
|
+
# they are gated separately from NEW_SPILL_KEYS — but by the same rule: a
|
|
153
|
+
# worker whose builder predates them silently DROPS the keys (its chat-request
|
|
154
|
+
# whitelist doesn't forward them), and a selected suppression must never be a
|
|
155
|
+
# silent no-op. Central strips them for an old worker and logs the downgrade;
|
|
156
|
+
# the soft directive + caller-side strip (utils/no_think.py) still ride the
|
|
157
|
+
# messages, so the request degrades to today's behavior, never breaks.
|
|
158
|
+
CHAT_EXTRAS_WIRE_KEYS = frozenset({"chat_template_kwargs", "logit_bias"})
|
|
159
|
+
CHAT_EXTRAS_MIN_PKG_VERSION = "0.1.229"
|
|
160
|
+
|
|
161
|
+
# Modes a non-GGUF (transformers/comfy) model may select. Slice C wired the gap
|
|
162
|
+
# loaders to the spill seam, and this slice (2026-07-24, operator-approved)
|
|
163
|
+
# OPENS the central gate for ``max-ram``: transformers honor it via
|
|
164
|
+
# transformers_max_memory's RAM-priority branch, diffusers via
|
|
165
|
+
# enable_model_cpu_offload — so it is a real, honest mode for non-GGUF now.
|
|
166
|
+
# ``explicit`` STAYS GGUF-only: its banded leniency floor is a llama.cpp concept
|
|
167
|
+
# with no transformers analogue (opening it would break the mode's promise). So
|
|
168
|
+
# the non-GGUF set is the four modes that have a working non-GGUF meaning; only
|
|
169
|
+
# explicit is dropped.
|
|
170
|
+
NONGGUF_ALLOWED_MODES = ("gpu-only", "ram-only", "max-gpu", "max-ram")
|
|
171
|
+
|
|
172
|
+
# GGUF family: the HF-canonical 'gguf' plus the llama_cpp synonym. A GGUF model
|
|
173
|
+
# is ALWAYS max-gpu by default (partial offload makes any size feasible — the
|
|
174
|
+
# runner spills whatever won't fit to RAM), so its feasible default never
|
|
175
|
+
# depends on the box's totals. Everything else (transformers / comfy) loads
|
|
176
|
+
# whole-tensor via accelerate, so an oversized model genuinely cannot land on
|
|
177
|
+
# the GPU and its feasible default is worker-dependent.
|
|
178
|
+
GGUF_ENGINES = frozenset({"gguf", "llama_cpp"})
|
|
179
|
+
|
|
180
|
+
# Headroom factor for the transformers GPU-fit test (feasible_default_mode).
|
|
181
|
+
# A transformers model whose effective footprint exceeds this fraction of the
|
|
182
|
+
# box's TOTAL GPU capacity "clearly cannot fit the GPU" and defaults to
|
|
183
|
+
# ram-only on that worker. 0.9 leaves ~10% for the CUDA context, activation
|
|
184
|
+
# working set, and KV/attention scratch that ride alongside the weights — a
|
|
185
|
+
# model at 0.95× total VRAM would OOM the moment it allocated its first
|
|
186
|
+
# forward-pass buffer, so 0.9 is the defensible "clearly cannot fit" line
|
|
187
|
+
# (not a fit PREDICTION — the real autofit/accelerate placement still runs; this
|
|
188
|
+
# only picks the read-time DEFAULT so a doomed max-gpu is never the blank
|
|
189
|
+
# promise). Above 0.9× GPU but within RAM -> ram-only; above RAM too -> leave
|
|
190
|
+
# max-gpu and let the worker refuse honestly (no invented fourth state).
|
|
191
|
+
#
|
|
192
|
+
# LIFTED 0.9 -> 0.98 (operator, 2026-07-25). Quant sizes are chosen against REAL
|
|
193
|
+
# card capacities, so a 23.5 GiB release IS the "fits a 24 GiB card" build. At
|
|
194
|
+
# 0.9 the cutoff was 21.6 GiB, so a 23.5 GiB model on a 24 GiB 3090 derived
|
|
195
|
+
# **ram-only** — never touching the card it was published for, at the ~7 tok/s
|
|
196
|
+
# floor the cliff sweep measured, when the alternative was ~135. Rejecting the
|
|
197
|
+
# top tier of every card in the fleet is a worse failure than an occasional
|
|
198
|
+
# honest load-time refusal.
|
|
199
|
+
#
|
|
200
|
+
# Why this is the safe one of the two margins lifted today: the derived mode is
|
|
201
|
+
# only a PREFERENCE. The real placement still runs autofit/accelerate, and the
|
|
202
|
+
# ~90% admission ceiling still gates the load — so a wrong guess here degrades
|
|
203
|
+
# to a clear refusal, never a crash. 0.98 rather than 1.0 keeps a token margin
|
|
204
|
+
# for the CUDA context itself.
|
|
205
|
+
_GPU_FIT_HEADROOM = 0.98
|
|
206
|
+
|
|
207
|
+
# Bytes per GiB, as a float, for the human-readable ``why`` lines and the
|
|
208
|
+
# gpu_mem_gib/cpu_mem_gib budget conversion in default_allocation.
|
|
209
|
+
_GIB_F = float(1 << 30)
|
|
210
|
+
|
|
211
|
+
# n_cpu_moe value meaning "ALL expert layers on CPU". MIRRORS
|
|
212
|
+
# ``managers.spill.MOE_ALL_LAYERS`` (999 — llama-server caps it to the model's
|
|
213
|
+
# layer count, so any large sentinel works). Duplicated as a literal ON PURPOSE:
|
|
214
|
+
# this module is PURE stdlib so chaos, the routes, overrides and the worker can
|
|
215
|
+
# share ONE vocabulary without import weight, and importing spill here would
|
|
216
|
+
# drag the GGUF reader (struct/glob/os.stat) into every one of those callers.
|
|
217
|
+
# The two are asserted equal in tests so the mirror can never drift silently.
|
|
218
|
+
MOE_ALL_LAYERS = 999
|
|
219
|
+
|
|
220
|
+
# ── bitsandbytes "specialization" (operator, 2026-07-26) ────────────────────
|
|
221
|
+
# A SEPARATE AXIS FROM THE MoE SPLIT, and the distinction matters:
|
|
222
|
+
# * the MoE split is PLACEMENT — the weights are untouched, we just decide
|
|
223
|
+
# which tensors sit on the card and which sit in RAM (coder-next is 45 GiB
|
|
224
|
+
# either way; only 1.49 GiB of it is non-expert);
|
|
225
|
+
# * bitsandbytes is COMPRESSION — it changes what the weights ARE, loading
|
|
226
|
+
# fp16 at nf4 so a 67 GiB model becomes ~17 GiB and genuinely fits a 24 GiB
|
|
227
|
+
# card. It needs no structural seam, so it applies to dense models too.
|
|
228
|
+
# They compose (a transformers MoE could be both) and they live at different
|
|
229
|
+
# layers: --n-cpu-moe is a llama.cpp flag, bitsandbytes is a transformers
|
|
230
|
+
# load-time quantization_config.
|
|
231
|
+
#
|
|
232
|
+
# 4-bit nf4 + double quant ~= 1/4 of fp16, but the RESIDUAL is not free: the
|
|
233
|
+
# quantization constants, the un-quantized layers (embeddings/norms/lm_head are
|
|
234
|
+
# commonly skipped) and the compute buffers all stay. 0.30 is the honest
|
|
235
|
+
# working figure rather than a flattering 0.25 — an OPTIMISTIC ratio here would
|
|
236
|
+
# derive "fits the GPU" for a model that then OOMs at load, which is exactly the
|
|
237
|
+
# failure defaults-are-promises forbids.
|
|
238
|
+
BNB_4BIT_SIZE_RATIO = 0.30
|
|
239
|
+
|
|
240
|
+
# Repos that are ALREADY quantized must never be offered bitsandbytes: loading a
|
|
241
|
+
# pre-quantized checkpoint under a second quantization config fails, and the
|
|
242
|
+
# size win is already banked. Matched on the key/hub id because that is what the
|
|
243
|
+
# registry reliably carries for a not-yet-downloaded model.
|
|
244
|
+
_PREQUANT_MARKERS = ("4bit", "8bit", "nvfp4", "fp4", "int4", "int8",
|
|
245
|
+
"gptq", "awq", "-nf4", "bnb")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def is_prequantized(model_key: Any) -> bool:
|
|
249
|
+
"""True if the model name declares an existing quantization."""
|
|
250
|
+
k = str(model_key or "").lower()
|
|
251
|
+
return any(m in k for m in _PREQUANT_MARKERS)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def bnb_eligible(engine: Any, model_key: Any, *, has_cuda: bool = True) -> bool:
|
|
255
|
+
"""Can this (model, worker) take a bitsandbytes specialization?
|
|
256
|
+
|
|
257
|
+
Three gates, all necessary:
|
|
258
|
+
* NOT a GGUF — llama.cpp carries its own quantization; bitsandbytes is a
|
|
259
|
+
transformers/diffusers loader concept and has no meaning here;
|
|
260
|
+
* the worker HAS CUDA — bitsandbytes' 4-bit kernels are CUDA-only, so
|
|
261
|
+
offering the lever on a CPU-only box would be a promise that fails at
|
|
262
|
+
load (op has no GPU);
|
|
263
|
+
* NOT already quantized — see is_prequantized.
|
|
264
|
+
"""
|
|
265
|
+
if is_gguf_engine(engine):
|
|
266
|
+
return False
|
|
267
|
+
if not has_cuda:
|
|
268
|
+
return False
|
|
269
|
+
return not is_prequantized(model_key)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def bnb_effective_bytes(model_bytes: Any) -> "Optional[int]":
|
|
273
|
+
"""The size the allocator should PRICE when bitsandbytes is enabled."""
|
|
274
|
+
n = _as_int(model_bytes)
|
|
275
|
+
return int(n * BNB_4BIT_SIZE_RATIO) if n else None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def is_gguf_engine(engine: Any) -> bool:
|
|
279
|
+
"""True for the GGUF family (gguf / llama_cpp), case-insensitive. Anything
|
|
280
|
+
else — including None/unknown — is treated as non-GGUF by the caller, but
|
|
281
|
+
the feasible-default logic degrades unknown engines to max-gpu regardless."""
|
|
282
|
+
return str(engine or "").strip().lower() in GGUF_ENGINES
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _as_int(v: Any) -> "Optional[int]":
|
|
286
|
+
try:
|
|
287
|
+
return int(v) if v else None
|
|
288
|
+
except (TypeError, ValueError):
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def feasible_modes(engine: Any,
|
|
293
|
+
model_bytes: "Optional[int]",
|
|
294
|
+
gpu_total_bytes: "Optional[int]",
|
|
295
|
+
ram_total_bytes: "Optional[int]",
|
|
296
|
+
moe_split_gpu_bytes: "Optional[int]" = None,
|
|
297
|
+
bnb: bool = False) -> tuple:
|
|
298
|
+
"""The allocation modes that are FEASIBLE for one (model x worker), in
|
|
299
|
+
ALLOC_MODES display order (operator ruling 2026-07-24 scope-extension: "the
|
|
300
|
+
user shouldn't be able to select an option that implies something it cannot
|
|
301
|
+
do"). The blank default is the best member of this set (feasible_default_mode
|
|
302
|
+
returns exactly that). PURE — the enforcement/surface glue lives at the
|
|
303
|
+
routes.
|
|
304
|
+
|
|
305
|
+
Feasibility matrix (each rule is a hard can-it-physically-land test, not a
|
|
306
|
+
fit prediction — the real placement still runs; this only bounds what may be
|
|
307
|
+
SELECTED so an impossible mode is never offered):
|
|
308
|
+
|
|
309
|
+
* ``gpu-only`` — the model fits the GPU total within the headroom factor
|
|
310
|
+
(``model <= _GPU_FIT_HEADROOM * gpu_total``). All-or-bust on the GPU, so
|
|
311
|
+
it must plausibly fit the GPU alone. STRICT for a MoE too (operator
|
|
312
|
+
ruling 2026-07-31): the FULL footprint is priced, experts included —
|
|
313
|
+
see ``moe_split_gpu_bytes`` below.
|
|
314
|
+
* ``ram-only`` — the model fits RAM total (``model <= ram_total``). Binds
|
|
315
|
+
the CPU; never touches the GPU.
|
|
316
|
+
* ``max-gpu`` — GGUF: ALWAYS (partial offload spills whatever won't fit to
|
|
317
|
+
RAM, so it is universally feasible). Transformers/comfy: the same
|
|
318
|
+
numbers rule as max-ram — fits GPU+RAM COMBINED (118ac64, operator
|
|
319
|
+
ruling 2026-07-29: max-gpu is a spill PREFERENCE; the non-GGUF loaders
|
|
320
|
+
honor the spill via max_memory / cpu_offload, so a fits-the-card-whole
|
|
321
|
+
gate here made spill-capable models unassignable).
|
|
322
|
+
* ``max-ram`` — a split exists: the model fits RAM+GPU COMBINED (its
|
|
323
|
+
overflow rides the GPU). Engine-agnostic as of 2026-07-24: GGUF and
|
|
324
|
+
non-GGUF both honor it (transformers RAM-priority max_memory, diffusers
|
|
325
|
+
enable_model_cpu_offload — Slice C wired the loaders, this slice opened
|
|
326
|
+
the gate). The numbers rule (fits GPU+RAM combined) applies to BOTH.
|
|
327
|
+
* ``explicit`` — some split exists (``model <= gpu_total + ram_total``);
|
|
328
|
+
ENGINE-GATED: non-GGUF stays dropped regardless of the numbers — its
|
|
329
|
+
banded leniency floor is a llama.cpp concept with no transformers
|
|
330
|
+
analogue, so opening it would break the mode's promise.
|
|
331
|
+
|
|
332
|
+
UNKNOWN size or unknown totals -> ALL modes feasible: never eliminate on
|
|
333
|
+
missing data (degrade to today's permissiveness). Specifically, if
|
|
334
|
+
``model_bytes`` is unknown, or the total a rule needs is unknown, that rule
|
|
335
|
+
does NOT eliminate its mode. The engine gate on ``explicit`` is the ONLY
|
|
336
|
+
elimination that fires without size/totals (it is a capability fact, not a
|
|
337
|
+
measurement); ``max-ram`` is now engine-agnostic and eliminated only by the
|
|
338
|
+
numbers.
|
|
339
|
+
|
|
340
|
+
``moe_split_gpu_bytes`` (MoE, 2026-07-24): the GPU-side need of a detected
|
|
341
|
+
MoE's expert split (non-expert bytes — surfaced by gguf_variants_detail's
|
|
342
|
+
``moe`` at enrichment); None for dense models. It USED to relax the gpu-only
|
|
343
|
+
gate: a 45 GiB MoE on a 24 GiB card was offered gpu-only because the split
|
|
344
|
+
would only put ~3 GiB on the card.
|
|
345
|
+
|
|
346
|
+
⚠ IT NO LONGER DOES (operator ruling 2026-07-31: "gpu-only must be honest to
|
|
347
|
+
its name"). ALL tensors on the card, experts included — a MoE whose full
|
|
348
|
+
footprint doesn't fit VRAM makes gpu-only INFEASIBLE, not "feasible with the
|
|
349
|
+
experts quietly in RAM", which silently redefined *only*. The
|
|
350
|
+
GPU-maximal-then-spill behavior the split expresses belongs to **max-gpu**,
|
|
351
|
+
whose contract already says spill and which is UNCONDITIONALLY feasible for
|
|
352
|
+
a GGUF for exactly that reason (the partial-offload/expert split is what
|
|
353
|
+
makes any size serveable) — so the split's feasibility home is that branch,
|
|
354
|
+
not this one, and nothing is lost by pricing gpu-only honestly.
|
|
355
|
+
|
|
356
|
+
The argument is still accepted: callers pass it, and central reports it in
|
|
357
|
+
``feasibility_context`` so a 409 can name the numbers the decision saw. It
|
|
358
|
+
is deliberately inert as a GATE — the split relaxes no mode's elimination
|
|
359
|
+
now that the mode it relaxed is the one that must not be relaxed."""
|
|
360
|
+
size = _as_int(model_bytes)
|
|
361
|
+
gpu_total = _as_int(gpu_total_bytes)
|
|
362
|
+
ram_total = _as_int(ram_total_bytes)
|
|
363
|
+
# t159: a priced expert share can only come from a GGUF header read, so an
|
|
364
|
+
# unlabelled engine (registry framework None) with one IS a GGUF — keep the
|
|
365
|
+
# selectable set in step with default_allocation's structure routing.
|
|
366
|
+
gguf = is_gguf_engine(engine) or _as_int(moe_split_gpu_bytes) is not None
|
|
367
|
+
# BITSANDBYTES RE-PRICING (operator ruling 2026-07-29: "its auto should
|
|
368
|
+
# change upon 4-bit delegation"). default_allocation has priced the 4-bit
|
|
369
|
+
# footprint since 2026-07-26 while THIS function — the set the console
|
|
370
|
+
# offers and /assign enforces — still priced the full fp16 size. The two
|
|
371
|
+
# disagreed: the allocator planned ~15 GiB for a 51.7 GiB model and would
|
|
372
|
+
# have placed it on the card, but the gate refused the mode outright. Same
|
|
373
|
+
# re-pricing, same place in the order (before any rule reads `size`), so
|
|
374
|
+
# 4-bit and MoE still compose.
|
|
375
|
+
#
|
|
376
|
+
# ⚠ ESTIMATE, NOT A MEASUREMENT: BNB_4BIT_SIZE_RATIO is a flat 0.30 of the
|
|
377
|
+
# whole directory, deliberately pessimistic vs the theoretical 0.25. It is
|
|
378
|
+
# NOT per-component, so for a diffusers PIPELINE (VAE/tokenizer/scheduler
|
|
379
|
+
# never quantize) it can still flatter. Feasibility is a gate, not a
|
|
380
|
+
# promise the load will fit — the worker's own admission preflight remains
|
|
381
|
+
# the measured authority.
|
|
382
|
+
if bool(bnb) and not gguf and size:
|
|
383
|
+
size = bnb_effective_bytes(size) or size
|
|
384
|
+
unknown_size = size is None
|
|
385
|
+
|
|
386
|
+
out = []
|
|
387
|
+
for mode in ALLOC_MODES:
|
|
388
|
+
if mode == "gpu-only":
|
|
389
|
+
# Fits GPU (headroom), priced on the FULL footprint — a MoE gets no
|
|
390
|
+
# discount here (see moe_split_gpu_bytes above: "only" means the
|
|
391
|
+
# experts are on the card too, and a footprint that needs RAM is
|
|
392
|
+
# max-gpu's job). Unknown size/gpu_total -> don't eliminate.
|
|
393
|
+
feasible = (unknown_size or gpu_total is None
|
|
394
|
+
or size <= _GPU_FIT_HEADROOM * gpu_total)
|
|
395
|
+
elif mode == "ram-only":
|
|
396
|
+
feasible = (unknown_size or ram_total is None
|
|
397
|
+
or size <= ram_total)
|
|
398
|
+
elif mode == "max-gpu":
|
|
399
|
+
if gguf:
|
|
400
|
+
# Partial offload: universal — AND the home of the MoE expert
|
|
401
|
+
# split since the 2026-07-31 gpu-only ruling. "As much GPU as
|
|
402
|
+
# fits, spill the rest" is precisely what a dense-first expert
|
|
403
|
+
# split does, so an oversized MoE is feasible HERE and only
|
|
404
|
+
# here.
|
|
405
|
+
feasible = True
|
|
406
|
+
else:
|
|
407
|
+
# SAME numbers rule as max-ram (operator ruling 2026-07-29:
|
|
408
|
+
# "feasible modes should be auto-derived from auto setting; if
|
|
409
|
+
# they derive non-feasible modes that's a blanket function
|
|
410
|
+
# problem"). max-gpu means "as much GPU as fits, SPILL THE REST
|
|
411
|
+
# TO RAM" — so the test is fits-GPU+RAM-combined, never
|
|
412
|
+
# fits-the-card-whole. That is the same spill the non-GGUF
|
|
413
|
+
# loaders already honor for max-ram (transformers RAM-priority
|
|
414
|
+
# max_memory, diffusers enable_model_cpu_offload — wired by
|
|
415
|
+
# Slice C), so gating the two differently was an inconsistency,
|
|
416
|
+
# not a capability fact.
|
|
417
|
+
#
|
|
418
|
+
# WHAT IT COST: every non-GGUF model larger than VRAM lost THE
|
|
419
|
+
# DEFAULT mode, so a stored/derived max-gpu became unassignable
|
|
420
|
+
# — /assign 409'd with "not feasible" on 8 of ae's designations
|
|
421
|
+
# after the 2026-07-29 in-VM migration (24 GiB card), making
|
|
422
|
+
# models that CAN spill simply uncallable. defaults-are-promises
|
|
423
|
+
# says the default must be a success path, not a refusal.
|
|
424
|
+
combined = None
|
|
425
|
+
if gpu_total is not None or ram_total is not None:
|
|
426
|
+
combined = (gpu_total or 0) + (ram_total or 0)
|
|
427
|
+
feasible = (unknown_size or combined is None
|
|
428
|
+
or size <= combined)
|
|
429
|
+
elif mode == "max-ram":
|
|
430
|
+
# Engine-agnostic (2026-07-24): both GGUF and non-GGUF honor max-ram
|
|
431
|
+
# now, so no engine gate — only the numbers rule (fits GPU+RAM
|
|
432
|
+
# combined). Unknown size/totals -> don't eliminate.
|
|
433
|
+
combined = None
|
|
434
|
+
if gpu_total is not None or ram_total is not None:
|
|
435
|
+
combined = (gpu_total or 0) + (ram_total or 0)
|
|
436
|
+
feasible = (unknown_size or combined is None
|
|
437
|
+
or size <= combined)
|
|
438
|
+
elif mode == "explicit":
|
|
439
|
+
# Engine gate (a capability fact, fires even on missing data): the
|
|
440
|
+
# banded leniency floor has no transformers analogue, so explicit
|
|
441
|
+
# stays GGUF-only. GGUF then also honors the numbers rule.
|
|
442
|
+
if not gguf:
|
|
443
|
+
feasible = False
|
|
444
|
+
else:
|
|
445
|
+
combined = None
|
|
446
|
+
if gpu_total is not None or ram_total is not None:
|
|
447
|
+
combined = (gpu_total or 0) + (ram_total or 0)
|
|
448
|
+
feasible = (unknown_size or combined is None
|
|
449
|
+
or size <= combined)
|
|
450
|
+
else: # pragma: no cover — ALLOC_MODES is closed
|
|
451
|
+
feasible = True
|
|
452
|
+
if feasible:
|
|
453
|
+
out.append(mode)
|
|
454
|
+
# Never return an empty set — a model must always have SOMETHING selectable
|
|
455
|
+
# (defaults-are-promises). If the numbers eliminated everything (e.g. a
|
|
456
|
+
# transformers model bigger than RAM and GPU), fall back to max-gpu so the
|
|
457
|
+
# worker can refuse HONESTLY downstream rather than the UI offering nothing.
|
|
458
|
+
return tuple(out) if out else ("max-gpu",)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def feasible_default_mode(engine: Any,
|
|
462
|
+
model_bytes: "Optional[int]",
|
|
463
|
+
gpu_total_bytes: "Optional[int]",
|
|
464
|
+
ram_total_bytes: "Optional[int]",
|
|
465
|
+
moe: "Optional[dict]" = None,
|
|
466
|
+
bnb: bool = False,
|
|
467
|
+
moe_force: "Optional[bool]" = None) -> str:
|
|
468
|
+
"""The BLANK default alloc mode derived by FEASIBILITY for one (model x
|
|
469
|
+
worker), engine-aware (operator ruling 2026-07-24). This ONLY supplies the
|
|
470
|
+
default when NOTHING is persisted — an explicit alloc_mode always wins
|
|
471
|
+
upstream; this is never consulted for a model that has one.
|
|
472
|
+
|
|
473
|
+
NAME-ONLY VIEW: this returns the mode NAME, for the surfaces that reason in
|
|
474
|
+
names (console dropdown, the /assign feasibility gate). The MoE leaf's
|
|
475
|
+
content is a SPLIT — a pair of numbers plus a knob — which a ``str`` cannot
|
|
476
|
+
carry, so callers that PERSIST an allocation must use
|
|
477
|
+
:func:`default_allocation` instead. The two agree by construction: the MoE
|
|
478
|
+
branch below delegates to it.
|
|
479
|
+
|
|
480
|
+
* GGUF DENSE -> ``gpu-only`` when it fits the card whole (operator
|
|
481
|
+
default order 2026-07-31: prefer full-card residency), else the
|
|
482
|
+
max-ram/ram-only tail; unknown size/totals degrade to ``max-gpu``
|
|
483
|
+
(partial offload keeps every size servable — never guess).
|
|
484
|
+
* GGUF MoE (``moe`` supplied and is_moe, 2026-07-25) -> the operator's MoE
|
|
485
|
+
branch via ``default_allocation``: ``explicit`` when the non-expert
|
|
486
|
+
share fits the GPU and the experts fit RAM, else that function's
|
|
487
|
+
fall-through. Absent a ``moe`` detail this is byte-identical to the
|
|
488
|
+
dense path — the caller simply didn't price the structure, and
|
|
489
|
+
degrade-not-guess says don't invent it.
|
|
490
|
+
* transformers/comfy:
|
|
491
|
+
- if the footprint CLEARLY cannot fit the GPU
|
|
492
|
+
(``model_bytes > _GPU_FIT_HEADROOM * gpu_total_bytes``) but DOES fit
|
|
493
|
+
RAM (``model_bytes <= ram_total_bytes``) -> ``ram-only`` (emits the
|
|
494
|
+
legacy ``{"n_gpu_layers": "off"}`` — works on ANY worker version, no
|
|
495
|
+
gate). This is the "68 GB model, 24 GB GPU, 124 GB RAM -> RAM-only"
|
|
496
|
+
case: the only feasible option, so it IS the default.
|
|
497
|
+
- if it plausibly fits the GPU -> ``max-gpu`` (today's default;
|
|
498
|
+
autofit/accelerate handles it).
|
|
499
|
+
- if it fits NEITHER (bigger than RAM too) -> ``max-gpu`` and let the
|
|
500
|
+
worker refuse honestly (no invented fourth state).
|
|
501
|
+
|
|
502
|
+
DEGRADE-NOT-GUESS: any missing input (unknown size, unknown GPU total, or —
|
|
503
|
+
for the ram-only decision — unknown RAM total) falls back to ``max-gpu``,
|
|
504
|
+
today's behavior. A default is never derived from a guessed number."""
|
|
505
|
+
# ── ONE TREE, TWO VIEWS (2026-07-25, eviction-flow slice) ────────────────
|
|
506
|
+
# This function is now a PURE PROJECTION of ``default_allocation``: same
|
|
507
|
+
# inputs, same tree, ``["mode"]`` taken off the end. It used to walk its own
|
|
508
|
+
# copy of the tree for the non-MoE cases, and the copies had DRIFTED — the
|
|
509
|
+
# spec's third open item ("feasible_default_mode and default_allocation
|
|
510
|
+
# still disagree for a large dense GGUF").
|
|
511
|
+
#
|
|
512
|
+
# THE DISAGREEMENT, precisely: for a DENSE GGUF this returned "max-gpu"
|
|
513
|
+
# unconditionally ("a GGUF is ALWAYS max-gpu by default — partial offload
|
|
514
|
+
# makes any size feasible"), while ``default_allocation``'s dense-GGUF leaf
|
|
515
|
+
# returns **max-ram** when the model exceeds the GPU's usable share but
|
|
516
|
+
# fits RAM (the PREFERENCE-vs-PROHIBITION ruling: prefer RAM, don't FORBID
|
|
517
|
+
# the card). So a 40 GiB dense GGUF on a 24 GiB card showed "max-gpu" in the
|
|
518
|
+
# console dropdown and persisted "max-ram" when chosen.
|
|
519
|
+
#
|
|
520
|
+
# WHY IT NOW MATTERS ENOUGH TO FIX. Under the eviction flow the mode is no
|
|
521
|
+
# longer just a placement label — it is the eviction PREFERENCE (key ①), so
|
|
522
|
+
# the label decides WHICH MODEL DIES. A dropdown reading "max-gpu" for a
|
|
523
|
+
# model the system treats as max-ram tells the operator its residents sort
|
|
524
|
+
# one way while they sort the other. That is not a cosmetic mismatch.
|
|
525
|
+
#
|
|
526
|
+
# WHICH SIDE WON, and why ``default_allocation``: it is the side that gets
|
|
527
|
+
# PERSISTED (it returns the wire encoding, so what the operator picks is
|
|
528
|
+
# what lands), it is the side with the measured rationale attached (the
|
|
529
|
+
# ~5x cliff), and it is the only one that can express the MoE leaf. The
|
|
530
|
+
# name-only view had no argument for its answer beyond "GGUFs spill" —
|
|
531
|
+
# true, and exactly why the other side chose max-ram rather than ram-only.
|
|
532
|
+
return default_allocation(engine, model_bytes, gpu_total_bytes,
|
|
533
|
+
ram_total_bytes, moe=moe, bnb=bnb,
|
|
534
|
+
moe_force=moe_force)["mode"]
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def default_allocation(engine: Any,
|
|
538
|
+
model_bytes: "Optional[int]",
|
|
539
|
+
gpu_total_bytes: "Optional[int]",
|
|
540
|
+
ram_total_bytes: "Optional[int]",
|
|
541
|
+
*,
|
|
542
|
+
moe: "Optional[dict]" = None,
|
|
543
|
+
bnb: bool = False,
|
|
544
|
+
moe_force: "Optional[bool]" = None) -> dict:
|
|
545
|
+
"""THE full operator decision tree (2026-07-25) for a model's INITIAL
|
|
546
|
+
DEFAULT allocation, DERIVED from its own structure instead of a blanket
|
|
547
|
+
stamp. Returns ``{"mode": <one of ALLOC_MODES>, "spill": {...}, "why": str}``
|
|
548
|
+
— the mode, the wire encoding to persist, and one honest line naming the
|
|
549
|
+
numbers that chose it.
|
|
550
|
+
|
|
551
|
+
This is a SIBLING of :func:`feasible_default_mode`, not a replacement. That
|
|
552
|
+
function answers "which of the five NAMES is the blank default" and is
|
|
553
|
+
consumed by surfaces (the console dropdown, the /assign gate) that reason in
|
|
554
|
+
mode names only. The MoE leaf of this tree cannot be expressed as a name:
|
|
555
|
+
its whole content is a SPLIT — a pair of numbers plus a knob — so a function
|
|
556
|
+
returning ``str`` structurally cannot carry it. Rather than widen the return
|
|
557
|
+
type of a function with several call sites that index it as a string, the
|
|
558
|
+
tree lives here and ``feasible_default_mode`` stays the name-only view
|
|
559
|
+
(it now delegates, so the two can never disagree).
|
|
560
|
+
|
|
561
|
+
THE TREE (operator, verbatim structure):
|
|
562
|
+
|
|
563
|
+
transformers ── gpu large enough? ─ yes ──────────────► max-gpu*
|
|
564
|
+
└ no ─ ram large? ─ yes ► ram-only
|
|
565
|
+
└ no ► break
|
|
566
|
+
gguf ─ is_moe? ─ yes ─ gpu large enough for the NON-EXPERT share?
|
|
567
|
+
│ ├ yes ─ ram large enough for the EXPERTS?
|
|
568
|
+
│ │ ├ yes ────────────► explicit (SPLIT)
|
|
569
|
+
│ │ └ no ─────────────► (fall through)
|
|
570
|
+
│ └ no ── ram large enough? ─ yes ► ram-only
|
|
571
|
+
│ └ no ► break
|
|
572
|
+
└ no (dense) ─ gpu large enough? ─ yes ─► max-gpu*
|
|
573
|
+
└ no ─ ram? ─► ram-only
|
|
574
|
+
└► break
|
|
575
|
+
|
|
576
|
+
(*) the tree's "-- gpu only" leaf is implemented LITERALLY as ``gpu-only``
|
|
577
|
+
since 2026-07-31 (operator default order; between 07-25 and 07-31 it read
|
|
578
|
+
max-gpu as a bust-avoidance departure — history and the reversal argument
|
|
579
|
+
are on ``_gpu_else_ram`` below). Only a fits-the-card-whole model reaches
|
|
580
|
+
it; every other leaf keeps a spill-capable or split spelling.
|
|
581
|
+
|
|
582
|
+
THE MoE LEAF IS THE POINT. coder-next Q4_K_M is 45 GiB of file but only
|
|
583
|
+
1.49 GiB of NON-EXPERT tensors; the 43.59 GiB of experts are meant for RAM.
|
|
584
|
+
Priced whole it "doesn't fit" a 24 GiB card and would be stamped max-gpu;
|
|
585
|
+
priced by STRUCTURE it fits with 22 GiB to spare, and the split is the
|
|
586
|
+
measured success path (+59%% tok/s at 5x less VRAM on ae). The default must
|
|
587
|
+
therefore be the split, DERIVED — never a blanket stamp.
|
|
588
|
+
|
|
589
|
+
WIRE ENCODING FOR THE MoE LEAF — ``{"alloc_mode": "explicit",
|
|
590
|
+
"n_cpu_moe": MOE_ALL_LAYERS, "n_gpu_layers": -1, "gpu_mem_gib":
|
|
591
|
+
<non-expert>, "cpu_mem_gib": <experts>}``. Both the knob AND the -1 are
|
|
592
|
+
load-bearing, and omitting either produces a plausible-looking dud:
|
|
593
|
+
|
|
594
|
+
* ``n_cpu_moe`` alone is NOT enough. Setting ``alloc_mode: "explicit"``
|
|
595
|
+
makes ``spill.alloc_mode_env()`` non-None on the worker, which is
|
|
596
|
+
exactly the gate that DISABLES ``slot_agent._build_cmd``'s auto MoE
|
|
597
|
+
policy ("a k37 alloc_mode -> no auto split; the operator is driving the
|
|
598
|
+
numbers themselves"). A derived default that silences the very policy
|
|
599
|
+
that would have produced the right answer is the incident in miniature.
|
|
600
|
+
Carrying the knob explicitly re-supplies what the gate suppressed —
|
|
601
|
+
``_build_cmd`` reads ``n_cpu_moe`` BEFORE the auto branch, so an
|
|
602
|
+
explicit value wins regardless of the mode.
|
|
603
|
+
* ``n_gpu_layers: -1`` is NOT redundant with it. On the SLOT path the
|
|
604
|
+
agent ships only budgets / n_cpu_moe / an explicit n_gpu_layers to the
|
|
605
|
+
child (``llama.runners.get``) — ``HUGPY_ALLOC_MODE`` is deliberately not
|
|
606
|
+
forwarded. So without the -1 the slot child autofits a LAYER split and
|
|
607
|
+
then adds an expert split on top: a hybrid, not the measured
|
|
608
|
+
configuration. With both, the child takes the ``moe_mode="explicit"``
|
|
609
|
+
branch at ngl=-1 — byte-identical argv to what the auto policy emits
|
|
610
|
+
(``--n-gpu-layers -1 --n-cpu-moe 999``), which is the point.
|
|
611
|
+
* the -1 is SAFE here precisely because the knob rides with it: the
|
|
612
|
+
inverse VRAM preflight that catches "-1 onto a card that can't hold it"
|
|
613
|
+
(the 5.5-hour ae stall) is explicitly skipped when an expert split is
|
|
614
|
+
configured, because total-bytes-vs-VRAM is the wrong comparison for a
|
|
615
|
+
split model. A bare -1 is the bug; -1 WITH the split is the fix.
|
|
616
|
+
* the budgets (``gpu_mem_gib`` / ``cpu_mem_gib``) are the honest
|
|
617
|
+
DECLARATION of the split's two sides — what the console shows and what
|
|
618
|
+
the worker's RAM-budget preflight prices the CPU share against. They
|
|
619
|
+
make the allocation self-describing rather than an opaque pair of flags.
|
|
620
|
+
|
|
621
|
+
VERSION GATE: ``alloc_mode`` and ``n_cpu_moe`` are both in NEW_SPILL_KEYS,
|
|
622
|
+
so this spill is gated by ``gate_spill_for_worker`` at the emission seam
|
|
623
|
+
like any other mode spill — a pre-0.1.203 worker gets {} (max-gpu autofit),
|
|
624
|
+
whose own auto MoE policy then does the right thing anyway. The fleet runs
|
|
625
|
+
0.1.208, so the gate passes today. NOTHING new needed on the wire: every key
|
|
626
|
+
used here is already in ``_SPILL_ENV`` on the released worker (verified:
|
|
627
|
+
``n_cpu_moe -> HUGPY_N_CPU_MOE``, cleared-when-absent so it cannot leak to
|
|
628
|
+
the next model).
|
|
629
|
+
|
|
630
|
+
DEGRADE-NOT-GUESS: every leaf requires MEASURED inputs. A missing size, a
|
|
631
|
+
missing GPU/RAM total, or an unreadable MoE detail falls back to today's
|
|
632
|
+
behavior (``max-gpu`` / ``{}``) — a default is NEVER derived from a guessed
|
|
633
|
+
number. Note the asymmetry that makes this safe: falling back COSTS nothing
|
|
634
|
+
(max-gpu on a GGUF still gets the worker's own auto split), whereas guessing
|
|
635
|
+
could persist a stamp that suppresses it.
|
|
636
|
+
|
|
637
|
+
THE "break" LEAVES (fits neither GPU nor RAM) return ``max-gpu`` / ``{}``
|
|
638
|
+
— deliberately NOT a fourth state. There is no "infeasible" allocation to
|
|
639
|
+
persist: the contract is that the worker refuses HONESTLY at load time with
|
|
640
|
+
a message naming the real numbers, and a blank max-gpu is what lets it get
|
|
641
|
+
that far. Inventing a refusal here would move the refusal away from the
|
|
642
|
+
place that can explain it.
|
|
643
|
+
"""
|
|
644
|
+
size = _as_int(model_bytes)
|
|
645
|
+
gpu_total = _as_int(gpu_total_bytes)
|
|
646
|
+
ram_total = _as_int(ram_total_bytes)
|
|
647
|
+
|
|
648
|
+
# BITSANDBYTES SPECIALIZATION (operator, 2026-07-26). The lever is a
|
|
649
|
+
# COMPRESSION choice, so it belongs here — at the PRICING step, before the
|
|
650
|
+
# placement tree walks. Re-pricing rather than adding a branch is what makes
|
|
651
|
+
# the two axes compose: a 4-bit model still goes through the SAME tree and
|
|
652
|
+
# can still take a MoE split if it has one. The visible effect is the point —
|
|
653
|
+
# a 67 GiB transformers model prices at ~20 GiB and flips ram-only ->
|
|
654
|
+
# max-gpu, i.e. it stops being banished to RAM for a size it no longer has.
|
|
655
|
+
# t159 (operator, 2026-09-14: "gpu-only when it fits VRAM, explicit for
|
|
656
|
+
# MoE"): ROUTE BY STRUCTURE, not by the registry label alone. A priced
|
|
657
|
+
# expert table only ever comes from a GGUF header read, so a model whose
|
|
658
|
+
# engine is unknown (framework None — every owner-qualified `Org~X-GGUF`
|
|
659
|
+
# key on ae) but carries ``is_moe`` IS a GGUF. Before this it fell into the
|
|
660
|
+
# transformers branch: bnb re-priced a 45 GiB coder-next to "13.5 GiB", the
|
|
661
|
+
# fits-test passed, and it derived gpu-only (all experts on the card) while
|
|
662
|
+
# its split was fully priceable — the blanket default the todo names.
|
|
663
|
+
detail = moe if isinstance(moe, dict) else None
|
|
664
|
+
gguf = is_gguf_engine(engine) or bool(detail and detail.get("is_moe"))
|
|
665
|
+
if bool(bnb) and not gguf and size:
|
|
666
|
+
size = bnb_effective_bytes(size) or size
|
|
667
|
+
|
|
668
|
+
def _plain(mode: str, why: str) -> dict:
|
|
669
|
+
return {"mode": mode, "spill": mode_to_spill(mode), "why": why}
|
|
670
|
+
|
|
671
|
+
# DEGRADE-NOT-GUESS gate, shared by every branch: without a measured size
|
|
672
|
+
# and a measured GPU total there is no tree to walk.
|
|
673
|
+
if size is None or gpu_total is None:
|
|
674
|
+
return _plain("max-gpu",
|
|
675
|
+
"size or GPU total unknown — kept the max-gpu default "
|
|
676
|
+
"(degrade-not-guess: a default is never derived from a "
|
|
677
|
+
"guessed number)")
|
|
678
|
+
|
|
679
|
+
gpu_fits = size <= _GPU_FIT_HEADROOM * gpu_total
|
|
680
|
+
ram_fits = ram_total is not None and size <= ram_total
|
|
681
|
+
|
|
682
|
+
def _gpu_else_ram(label: str) -> dict:
|
|
683
|
+
"""The shared 'gpu large enough? else ram large enough? else break'
|
|
684
|
+
tail — identical for transformers and for dense GGUF.
|
|
685
|
+
|
|
686
|
+
THE "gpu large enough -> gpu only" LEAF READS AS ``gpu-only``, taking
|
|
687
|
+
the operator's tree at its literal word (operator order 2026-07-31:
|
|
688
|
+
"change the default to gpu only rather than max-gpu"). HISTORY: from
|
|
689
|
+
2026-07-25 to 2026-07-31 this leaf deliberately read max-gpu — the
|
|
690
|
+
argument was that the fit test is a headroom heuristic against TOTAL
|
|
691
|
+
capacity, so a contended card could turn a derived gpu-only into a
|
|
692
|
+
hard OOM nobody chose. Two things changed under that argument:
|
|
693
|
+
|
|
694
|
+
* gpu-only is STRICT and HONEST now (k55): the fits-whole test here
|
|
695
|
+
is the same full-footprint pricing admission re-checks at load
|
|
696
|
+
time, and a miss is an explicit refusal naming max-gpu as the
|
|
697
|
+
remedy — not a llama.cpp OOM mid-load. The bust this leaf used to
|
|
698
|
+
fear became a diagnosable, recoverable verdict.
|
|
699
|
+
* defaults-are-promises still holds BECAUSE the leaf is guarded:
|
|
700
|
+
only a model that fits the card WHOLE derives gpu-only. Oversized
|
|
701
|
+
dense stays max-ram/ram-only below, MoE keeps its explicit-split
|
|
702
|
+
branch, and every degrade-not-guess path still lands on max-gpu.
|
|
703
|
+
The default remains a success path per (model x worker) — it just
|
|
704
|
+
prefers full-card residency wherever that is genuinely available.
|
|
705
|
+
"""
|
|
706
|
+
if gpu_fits:
|
|
707
|
+
return _plain("gpu-only",
|
|
708
|
+
f"{label}: {size / _GIB_F:.2f} GiB fits the "
|
|
709
|
+
f"{gpu_total / _GIB_F:.2f} GiB GPU "
|
|
710
|
+
f"(<= {_GPU_FIT_HEADROOM:.0%} headroom) -> gpu-only "
|
|
711
|
+
"(full-card residency; operator default order "
|
|
712
|
+
"2026-07-31 — oversized models still derive a "
|
|
713
|
+
"spill-capable mode below)")
|
|
714
|
+
if ram_total is None:
|
|
715
|
+
return _plain("max-gpu",
|
|
716
|
+
f"{label}: too big for the GPU but RAM total is "
|
|
717
|
+
"unknown — kept max-gpu (never derive ram-only from "
|
|
718
|
+
"a guess)")
|
|
719
|
+
if ram_fits:
|
|
720
|
+
# PREFERENCE vs PROHIBITION (operator, 2026-07-25): "the 'max'
|
|
721
|
+
# settings were intended to be indicative of a PREFERENCE for
|
|
722
|
+
# spill... the 'only' designations are the truly stringent ones."
|
|
723
|
+
#
|
|
724
|
+
# A large DENSE GGUF that overflows the card is the case where that
|
|
725
|
+
# distinction pays. It CAN use the GPU — it just cannot fit whole —
|
|
726
|
+
# so llama.cpp keeps the layers that fit and spills the rest.
|
|
727
|
+
# Deriving `ram-only` would FORBID the card entirely, and today's
|
|
728
|
+
# cliff sweep prices that mistake: a spilled dense model runs ~36
|
|
729
|
+
# tok/s with partial GPU vs ~7.5 tok/s at ngl=0. Defaulting to
|
|
730
|
+
# ram-only would be a ~5x self-inflicted loss, i.e. a default that
|
|
731
|
+
# promises dross.
|
|
732
|
+
#
|
|
733
|
+
# TRANSFORMERS ARE DIFFERENT and keep ram-only: the three
|
|
734
|
+
# all-or-fail loaders cannot partially offload, so "doesn't fit the
|
|
735
|
+
# GPU" really does mean RAM or nothing. Same leaf, two engines, two
|
|
736
|
+
# honest answers.
|
|
737
|
+
if gguf:
|
|
738
|
+
return _plain("max-ram",
|
|
739
|
+
f"{label}: {size / _GIB_F:.2f} GiB exceeds the "
|
|
740
|
+
f"GPU's usable "
|
|
741
|
+
f"{_GPU_FIT_HEADROOM * gpu_total / _GIB_F:.2f} "
|
|
742
|
+
f"GiB but fits {ram_total / _GIB_F:.2f} GiB RAM "
|
|
743
|
+
"-> max-ram (RAM-first PREFERENCE; a GGUF still "
|
|
744
|
+
"keeps the layers that fit — ram-only would "
|
|
745
|
+
"forbid the card and cost ~5x throughput)")
|
|
746
|
+
return _plain("ram-only",
|
|
747
|
+
f"{label}: {size / _GIB_F:.2f} GiB exceeds the GPU's "
|
|
748
|
+
f"usable {_GPU_FIT_HEADROOM * gpu_total / _GIB_F:.2f} "
|
|
749
|
+
f"GiB but fits {ram_total / _GIB_F:.2f} GiB RAM "
|
|
750
|
+
"-> ram-only (this loader is all-or-fail; it cannot "
|
|
751
|
+
"partially offload, so RAM is the only landing)")
|
|
752
|
+
return _plain("max-gpu",
|
|
753
|
+
f"{label}: {size / _GIB_F:.2f} GiB fits NEITHER the GPU "
|
|
754
|
+
f"({gpu_total / _GIB_F:.2f} GiB) nor RAM "
|
|
755
|
+
f"({ram_total / _GIB_F:.2f} GiB) — no feasible mode; kept "
|
|
756
|
+
"max-gpu so the worker refuses honestly at load with the "
|
|
757
|
+
"real numbers (no invented fourth state)")
|
|
758
|
+
|
|
759
|
+
if not gguf:
|
|
760
|
+
# ── transformers / comfy: whole-tensor placement, no split exists ────
|
|
761
|
+
return _gpu_else_ram("transformers")
|
|
762
|
+
|
|
763
|
+
# ── GGUF (by engine label, or by a priced expert structure — see above) ──
|
|
764
|
+
# OPERATOR OVERRIDE (2026-07-26). moe_force=False means "never split this
|
|
765
|
+
# one" — the escape hatch when a derived split misbehaves; it drops to the
|
|
766
|
+
# dense tail so the model still SERVES, just without the expert placement.
|
|
767
|
+
# moe_force=True cannot manufacture a split for a file with no expert
|
|
768
|
+
# structure (there would be nothing to place), so it never fabricates one —
|
|
769
|
+
# the priced branch below still has to agree the numbers work.
|
|
770
|
+
if moe_force is False:
|
|
771
|
+
return _gpu_else_ram("gguf (operator forced the MoE split OFF)")
|
|
772
|
+
if not (detail and detail.get("is_moe")):
|
|
773
|
+
return _gpu_else_ram("dense gguf")
|
|
774
|
+
|
|
775
|
+
non_expert = _as_int(detail.get("non_expert_bytes"))
|
|
776
|
+
experts = _as_int(detail.get("expert_bytes"))
|
|
777
|
+
if not non_expert or not experts:
|
|
778
|
+
# Detected MoE but the split is unpriceable — degrade to the dense tail
|
|
779
|
+
# rather than encode a split from numbers we don't have.
|
|
780
|
+
return _gpu_else_ram("gguf (MoE detected but split unpriceable)")
|
|
781
|
+
|
|
782
|
+
# "gpu large enough" for a MoE prices the NON-EXPERT share — the only part
|
|
783
|
+
# the card ever holds under the split. This is the whole derivation.
|
|
784
|
+
if non_expert <= _GPU_FIT_HEADROOM * gpu_total:
|
|
785
|
+
if ram_total is not None and experts <= ram_total:
|
|
786
|
+
spill = mode_to_spill(
|
|
787
|
+
"explicit",
|
|
788
|
+
gpu_mem_gib=round(non_expert / _GIB_F, 3),
|
|
789
|
+
cpu_mem_gib=round(experts / _GIB_F, 3))
|
|
790
|
+
# The two keys that make this a SPLIT rather than a label. See the
|
|
791
|
+
# docstring: n_cpu_moe re-supplies the auto policy that alloc_mode
|
|
792
|
+
# suppresses; the -1 is what the slot path actually forwards, and
|
|
793
|
+
# is safe only because the knob rides with it.
|
|
794
|
+
spill["n_cpu_moe"] = MOE_ALL_LAYERS
|
|
795
|
+
spill["n_gpu_layers"] = -1
|
|
796
|
+
return {
|
|
797
|
+
"mode": "explicit", "spill": spill,
|
|
798
|
+
"why": (f"MoE split: {non_expert / _GIB_F:.2f} GiB of "
|
|
799
|
+
f"non-expert tensors on the "
|
|
800
|
+
f"{gpu_total / _GIB_F:.2f} GiB GPU, "
|
|
801
|
+
f"{experts / _GIB_F:.2f} GiB of experts in "
|
|
802
|
+
f"{ram_total / _GIB_F:.2f} GiB RAM "
|
|
803
|
+
f"(--n-cpu-moe {MOE_ALL_LAYERS}, n_gpu_layers=-1) "
|
|
804
|
+
"-> explicit; the split IS the allocation"),
|
|
805
|
+
}
|
|
806
|
+
# Experts don't fit RAM (or RAM unknown) — the operator's tree falls
|
|
807
|
+
# THROUGH here rather than breaking, so the dense tail decides on the
|
|
808
|
+
# whole file. It will not choose gpu-only (the full model is far bigger
|
|
809
|
+
# than the non-expert share that just fit), so this lands on ram-only
|
|
810
|
+
# or the honest-refusal max-gpu.
|
|
811
|
+
return _gpu_else_ram(
|
|
812
|
+
"gguf MoE (non-expert fits GPU but experts exceed RAM)")
|
|
813
|
+
|
|
814
|
+
# Non-expert share alone won't fit the card: no split is worth encoding.
|
|
815
|
+
if ram_total is None:
|
|
816
|
+
return _plain("max-gpu",
|
|
817
|
+
"gguf MoE: non-expert share exceeds the GPU and RAM total "
|
|
818
|
+
"is unknown — kept max-gpu (degrade-not-guess)")
|
|
819
|
+
if ram_fits:
|
|
820
|
+
# max-ram, not ram-only: this is still a GGUF, so even when the
|
|
821
|
+
# non-expert share alone overflows the card the loader keeps whatever
|
|
822
|
+
# layers fit. Prefer RAM; do not FORBID the GPU. (See the dense leaf:
|
|
823
|
+
# ram-only on a spillable GGUF costs ~5x throughput.)
|
|
824
|
+
return _plain("max-ram",
|
|
825
|
+
f"gguf MoE: even the {non_expert / _GIB_F:.2f} GiB "
|
|
826
|
+
f"non-expert share exceeds the GPU's usable "
|
|
827
|
+
f"{_GPU_FIT_HEADROOM * gpu_total / _GIB_F:.2f} GiB, but "
|
|
828
|
+
f"the whole {size / _GIB_F:.2f} GiB fits "
|
|
829
|
+
f"{ram_total / _GIB_F:.2f} GiB RAM -> max-ram "
|
|
830
|
+
"(RAM-first preference; the loader still keeps the "
|
|
831
|
+
"layers that fit)")
|
|
832
|
+
return _plain("max-gpu",
|
|
833
|
+
f"gguf MoE: neither the non-expert share nor the whole "
|
|
834
|
+
f"{size / _GIB_F:.2f} GiB fits this box — no feasible mode; "
|
|
835
|
+
"kept max-gpu so the worker refuses honestly at load")
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
839
|
+
# CASE A — "will not fit ANY worker, in ANY mode" (operator ruling 2026-07-25)
|
|
840
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
841
|
+
# The operator: "models that … simply will not fit on a worker no matter what,
|
|
842
|
+
# if allocated, should be blocked. the user will be forced to acknowledge it and
|
|
843
|
+
# act or not". This is ARITHMETIC, not judgement, so it is automatic — with the
|
|
844
|
+
# existing /unblock as the escape hatch.
|
|
845
|
+
#
|
|
846
|
+
# WHY IT IS WORTH DOING AT ALL: today the "fits neither" leaf of
|
|
847
|
+
# ``default_allocation`` returns a soft max-gpu "so the worker refuses honestly
|
|
848
|
+
# at load". That refusal is real, but it arrives MINUTES LATER as a failed
|
|
849
|
+
# request rather than as visible state. Blocking makes the same fact loud, at
|
|
850
|
+
# the moment of allocation, with the numbers attached.
|
|
851
|
+
#
|
|
852
|
+
# THREE-VALUED BY CONSTRUCTION. ``worker_fit_verdict`` returns:
|
|
853
|
+
# True — a mode exists on this worker that can physically hold the model
|
|
854
|
+
# False — CONFIDENTLY cannot: measured size > measured GPU AND > measured RAM
|
|
855
|
+
# None — UNKNOWN: some input was missing, so this worker has NO OPINION
|
|
856
|
+
# DEGRADE-NOT-GUESS is the whole safety property here: only a False from at
|
|
857
|
+
# least one worker, and NO True from any worker, can block. A fleet of Nones
|
|
858
|
+
# blocks nothing — "defaults are promises", and a wrong auto-block takes a
|
|
859
|
+
# WORKING model out of the pool, which is strictly worse than a late refusal.
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def worker_fit_verdict(engine: Any,
|
|
863
|
+
model_bytes: "Optional[int]",
|
|
864
|
+
gpu_total_bytes: "Optional[int]",
|
|
865
|
+
ram_total_bytes: "Optional[int]",
|
|
866
|
+
moe_split_gpu_bytes: "Optional[int]" = None) -> "Optional[bool]":
|
|
867
|
+
"""Can ``model_bytes`` land on THIS one worker in SOME allocation mode?
|
|
868
|
+
|
|
869
|
+
``True`` = yes (some mode fits), ``False`` = confidently no, ``None`` =
|
|
870
|
+
unknown (missing data — this worker gets no vote).
|
|
871
|
+
|
|
872
|
+
THE TEST IS DELIBERATELY THE LOOSEST ONE THAT IS STILL TRUE. A model "fits
|
|
873
|
+
somewhere" if it fits the GPU **or** RAM **or** the two COMBINED — because
|
|
874
|
+
max-gpu/max-ram spill across both, so combined capacity is the real
|
|
875
|
+
physical ceiling, and refusing anything below it would be central inventing
|
|
876
|
+
a limit the loader does not have. No headroom factor is applied here (unlike
|
|
877
|
+
``_GPU_FIT_HEADROOM`` in the DEFAULT-picking path): a headroom fudge is the
|
|
878
|
+
right conservatism when choosing a default that must SUCCEED, and exactly
|
|
879
|
+
the wrong one when deciding to take a model out of the pool. Erring loose
|
|
880
|
+
means the worst case is a late honest load-time refusal — today's behavior —
|
|
881
|
+
instead of a working model being blocked.
|
|
882
|
+
|
|
883
|
+
NOTE the engine is accepted (and ignored) for signature parity with the rest
|
|
884
|
+
of this module: combined GPU+RAM is the ceiling for GGUF (partial offload)
|
|
885
|
+
and for transformers (accelerate's cpu/disk offload) alike, so no engine
|
|
886
|
+
distinction is defensible at THIS question's granularity.
|
|
887
|
+
|
|
888
|
+
``moe_split_gpu_bytes`` (mirroring ``feasible_modes``): a detected MoE's
|
|
889
|
+
GPU-resident share under the expert split (non-expert + mmproj). When
|
|
890
|
+
present it is the GPU-side figure tested — the expert bytes are file-backed
|
|
891
|
+
page cache (mmap-streamable), so charging the FULL file against the GPU
|
|
892
|
+
would refuse exactly the model the split makes serveable (a 45 GiB MoE with
|
|
893
|
+
a 1.5 GiB backbone on a 24 GiB box). The COMBINED GPU+RAM ceiling still
|
|
894
|
+
applies to the WHOLE file, though (coder-next/computron 2026-08-28): the
|
|
895
|
+
worker's own expert-RAM guard refuses a split whose expert tensors overflow
|
|
896
|
+
budgetable host RAM, so a box whose VRAM+RAM combined cannot hold the model
|
|
897
|
+
is a PERMANENT no — offering it per-request only harvests that refusal
|
|
898
|
+
(computron, 8 GiB card: passed the backbone-only test, refused every
|
|
899
|
+
dispatch). Loosening on the GPU side only — never past the box's physical
|
|
900
|
+
ceiling."""
|
|
901
|
+
size = _as_int(model_bytes)
|
|
902
|
+
moe_gpu = _as_int(moe_split_gpu_bytes)
|
|
903
|
+
gpu_total = _as_int(gpu_total_bytes)
|
|
904
|
+
ram_total = _as_int(ram_total_bytes)
|
|
905
|
+
if size is None and moe_gpu is None:
|
|
906
|
+
return None # unsizable model: nobody may vote
|
|
907
|
+
if gpu_total is None and ram_total is None:
|
|
908
|
+
return None # unmeasured box: it has no opinion
|
|
909
|
+
capacity = (gpu_total or 0) + (ram_total or 0)
|
|
910
|
+
if moe_gpu is not None:
|
|
911
|
+
if size is None:
|
|
912
|
+
return moe_gpu <= capacity # unsizable file: the split share votes
|
|
913
|
+
return moe_gpu <= capacity and size <= capacity
|
|
914
|
+
return size <= capacity
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
def fleet_fit_verdict(boxes: Any) -> dict:
|
|
918
|
+
"""Roll per-worker verdicts up into the FLEET answer.
|
|
919
|
+
|
|
920
|
+
``boxes`` is an iterable of dicts, each ``{"name", "engine", "model_bytes",
|
|
921
|
+
"gpu_total_bytes", "ram_total_bytes"}`` (the caller resolves them; this
|
|
922
|
+
stays pure). Returns::
|
|
923
|
+
|
|
924
|
+
{"fits_somewhere": bool|None, # True / False / None(=no confident data)
|
|
925
|
+
"blockable": bool, # True IFF it is safe to auto-block
|
|
926
|
+
"fits_on": [name, ...], # workers that CAN hold it
|
|
927
|
+
"refused_by": [name, ...], # workers that confidently CANNOT
|
|
928
|
+
"unknown": [name, ...], # workers with no opinion
|
|
929
|
+
"why": str} # the operator-facing reasoning line
|
|
930
|
+
|
|
931
|
+
``blockable`` is the ONLY field a caller should act on, and it is True only
|
|
932
|
+
when **at least one** worker returned a confident False and **no** worker
|
|
933
|
+
returned True. An empty fleet, an all-unknown fleet, or a single confident
|
|
934
|
+
"fits" anywhere all yield ``blockable=False``. That asymmetry is the
|
|
935
|
+
operator's "no matter what": a model that fits SOME worker must never be
|
|
936
|
+
blocked, and missing data is never evidence of anything."""
|
|
937
|
+
fits_on, refused_by, unknown = [], [], []
|
|
938
|
+
worst_gpu = worst_ram = 0
|
|
939
|
+
size_seen = None
|
|
940
|
+
for box in (boxes or []):
|
|
941
|
+
if not isinstance(box, dict):
|
|
942
|
+
continue
|
|
943
|
+
name = str(box.get("name") or box.get("id") or "?")
|
|
944
|
+
verdict = worker_fit_verdict(box.get("engine"), box.get("model_bytes"),
|
|
945
|
+
box.get("gpu_total_bytes"),
|
|
946
|
+
box.get("ram_total_bytes"))
|
|
947
|
+
if verdict is True:
|
|
948
|
+
fits_on.append(name)
|
|
949
|
+
elif verdict is False:
|
|
950
|
+
refused_by.append(name)
|
|
951
|
+
worst_gpu = max(worst_gpu, _as_int(box.get("gpu_total_bytes")) or 0)
|
|
952
|
+
worst_ram = max(worst_ram, _as_int(box.get("ram_total_bytes")) or 0)
|
|
953
|
+
size_seen = size_seen or _as_int(box.get("model_bytes"))
|
|
954
|
+
else:
|
|
955
|
+
unknown.append(name)
|
|
956
|
+
|
|
957
|
+
if fits_on:
|
|
958
|
+
return {"fits_somewhere": True, "blockable": False, "fits_on": fits_on,
|
|
959
|
+
"refused_by": refused_by, "unknown": unknown,
|
|
960
|
+
"why": (f"fits {fits_on[0]}" + (f" (+{len(fits_on) - 1} more)"
|
|
961
|
+
if len(fits_on) > 1 else "")
|
|
962
|
+
+ " — never blocked while ANY worker can hold it")}
|
|
963
|
+
if not refused_by:
|
|
964
|
+
return {"fits_somewhere": None, "blockable": False, "fits_on": [],
|
|
965
|
+
"refused_by": [], "unknown": unknown,
|
|
966
|
+
"why": ("no worker could return a confident verdict "
|
|
967
|
+
f"({len(unknown)} unknown) — degrade-not-guess: "
|
|
968
|
+
"never block on missing data")}
|
|
969
|
+
return {
|
|
970
|
+
"fits_somewhere": False, "blockable": True, "fits_on": [],
|
|
971
|
+
"refused_by": refused_by, "unknown": unknown,
|
|
972
|
+
"why": (f"auto: {(size_seen or 0) / _GIB_F:.1f} GiB exceeds every "
|
|
973
|
+
f"worker's GPU ({worst_gpu / _GIB_F:.1f}) and RAM "
|
|
974
|
+
f"({worst_ram / _GIB_F:.1f}) — refused by "
|
|
975
|
+
f"{', '.join(refused_by)}"
|
|
976
|
+
+ (f"; {len(unknown)} worker(s) had no data" if unknown else "")
|
|
977
|
+
+ " — unblock to override"),
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def resolve_alloc_mode(name: Any) -> "tuple[Optional[str], bool]":
|
|
982
|
+
"""``(canonical_mode, was_alias)`` for a mode name, canonical-first.
|
|
983
|
+
|
|
984
|
+
Canonical names pass through untouched. A legacy alias resolves to its
|
|
985
|
+
canonical mode with ``was_alias=True`` (callers log; never emit the alias
|
|
986
|
+
back). Unknown/empty -> ``(None, False)`` — the caller degrades (keeps its
|
|
987
|
+
default) rather than raising."""
|
|
988
|
+
if name is None:
|
|
989
|
+
return None, False
|
|
990
|
+
s = str(name).strip().lower()
|
|
991
|
+
if not s:
|
|
992
|
+
return None, False
|
|
993
|
+
if s in ALLOC_MODES:
|
|
994
|
+
return s, False
|
|
995
|
+
alias = LEGACY_ALLOC_ALIASES.get(s)
|
|
996
|
+
if alias:
|
|
997
|
+
return alias, True
|
|
998
|
+
return None, False
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def derive_alloc_mode(override: "Optional[dict]") -> str:
|
|
1002
|
+
"""The model's EFFECTIVE mode from a persisted override/spill dict —
|
|
1003
|
+
read-time derivation, the migration (no file rewrite ever needed).
|
|
1004
|
+
|
|
1005
|
+
* an explicit ``alloc_mode`` wins (aliases resolved);
|
|
1006
|
+
* else the legacy wire value: n_gpu_layers -1 -> gpu-only,
|
|
1007
|
+
0/"off"/"cpu"/"none" -> ram-only;
|
|
1008
|
+
* else an explicit-budget/band/leniency contract -> explicit
|
|
1009
|
+
(today's "budget"/"bands" spills ARE explicit allocations);
|
|
1010
|
+
* else -> max-gpu (THE DEFAULT: a blank model fits-and-spills, never
|
|
1011
|
+
OOMs — defaults-are-promises).
|
|
1012
|
+
"""
|
|
1013
|
+
ov = override or {}
|
|
1014
|
+
got, was_alias = resolve_alloc_mode(ov.get("alloc_mode"))
|
|
1015
|
+
if got:
|
|
1016
|
+
if was_alias:
|
|
1017
|
+
logger.info("alloc_mode legacy name %r resolved to %r",
|
|
1018
|
+
ov.get("alloc_mode"), got)
|
|
1019
|
+
return got
|
|
1020
|
+
ngl = ov.get("n_gpu_layers")
|
|
1021
|
+
if ngl is not None:
|
|
1022
|
+
s = str(ngl).strip().lower()
|
|
1023
|
+
if s == "-1":
|
|
1024
|
+
return "gpu-only"
|
|
1025
|
+
if s in ("0", "off", "cpu", "none"):
|
|
1026
|
+
return "ram-only"
|
|
1027
|
+
# positive layer count / "auto": a fit-and-spill flavor -> max-gpu
|
|
1028
|
+
# (the explicit layer count still rides the wire untouched).
|
|
1029
|
+
for k in ("leniency_pct", "gpu_mem_gib", "cpu_mem_gib",
|
|
1030
|
+
"gpu_mem_gib_deviation_pct", "cpu_mem_gib_deviation_pct"):
|
|
1031
|
+
if ov.get(k) is not None:
|
|
1032
|
+
return "explicit"
|
|
1033
|
+
return "max-gpu"
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
def mode_to_spill(mode: Any, *, ctx_pct: "Optional[int]" = None,
|
|
1037
|
+
gpu_mem_gib: "Optional[float]" = None,
|
|
1038
|
+
cpu_mem_gib: "Optional[float]" = None,
|
|
1039
|
+
leniency_pct: "Optional[float]" = None,
|
|
1040
|
+
priority: "Optional[int]" = None,
|
|
1041
|
+
priority_device: "Optional[str]" = None,
|
|
1042
|
+
explicit_pick: bool = False) -> dict:
|
|
1043
|
+
"""Materialize a mode (+ optional explicit knobs) into the /assign spill
|
|
1044
|
+
contract. Legacy aliases are resolved first. Unknown mode -> {} (max-gpu),
|
|
1045
|
+
logged — degrade, never raise.
|
|
1046
|
+
|
|
1047
|
+
``explicit_pick`` (2026-07-25) is THE DERIVED-vs-CHOSEN switch, and it only
|
|
1048
|
+
affects max-gpu — the one mode whose natural encoding ({}) collides with the
|
|
1049
|
+
"clear this override" signal:
|
|
1050
|
+
|
|
1051
|
+
* False (default) — max-gpu yields ``{}``. This is the DERIVED default's
|
|
1052
|
+
encoding: nothing is persisted, so the model keeps re-deriving against
|
|
1053
|
+
current conditions. ``default_allocation`` uses this path exclusively,
|
|
1054
|
+
which is why deriving max-gpu still writes nothing.
|
|
1055
|
+
* True — max-gpu yields ``{"alloc_mode": "max-gpu"}``: a real, persisted
|
|
1056
|
+
row recording that a human chose this. Non-empty, so assign_model
|
|
1057
|
+
WRITES it instead of reading it as a clear.
|
|
1058
|
+
|
|
1059
|
+
The other four modes already have non-empty encodings and ignore the flag.
|
|
1060
|
+
Defaulting to False keeps every existing caller byte-identical — only the
|
|
1061
|
+
operator-facing /assign path opts in."""
|
|
1062
|
+
canonical, was_alias = resolve_alloc_mode(mode)
|
|
1063
|
+
if canonical is None:
|
|
1064
|
+
if mode not in (None, ""):
|
|
1065
|
+
logger.warning("unknown alloc mode %r -> defaulting to max-gpu", mode)
|
|
1066
|
+
canonical = "max-gpu"
|
|
1067
|
+
elif was_alias:
|
|
1068
|
+
logger.info("alloc_mode legacy name %r resolved to %r", mode, canonical)
|
|
1069
|
+
spill: dict = {}
|
|
1070
|
+
if canonical == "gpu-only":
|
|
1071
|
+
spill = {"n_gpu_layers": -1}
|
|
1072
|
+
elif canonical == "ram-only":
|
|
1073
|
+
return {"n_gpu_layers": "off"} # ctx irrelevant off-GPU
|
|
1074
|
+
elif canonical == "max-gpu":
|
|
1075
|
+
# {} when DERIVED (stays unpersisted, re-derives); the explicit key when
|
|
1076
|
+
# a human picked it (persists, immune to the derivation moving).
|
|
1077
|
+
spill = {"alloc_mode": "max-gpu"} if explicit_pick else {}
|
|
1078
|
+
elif canonical == "max-ram":
|
|
1079
|
+
spill = {"alloc_mode": "max-ram"}
|
|
1080
|
+
elif canonical == "explicit":
|
|
1081
|
+
spill = {"alloc_mode": "explicit"}
|
|
1082
|
+
if gpu_mem_gib is not None:
|
|
1083
|
+
spill["gpu_mem_gib"] = float(gpu_mem_gib)
|
|
1084
|
+
if cpu_mem_gib is not None:
|
|
1085
|
+
spill["cpu_mem_gib"] = float(cpu_mem_gib)
|
|
1086
|
+
if leniency_pct is not None:
|
|
1087
|
+
spill["leniency_pct"] = float(leniency_pct)
|
|
1088
|
+
if priority is not None:
|
|
1089
|
+
spill["priority"] = int(priority)
|
|
1090
|
+
if priority_device is not None:
|
|
1091
|
+
spill["priority_device"] = str(priority_device)
|
|
1092
|
+
if ctx_pct is not None and canonical != "max-gpu":
|
|
1093
|
+
spill["ctx_pct"] = int(ctx_pct)
|
|
1094
|
+
return spill
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
def normalize_spill(spill: "Optional[dict]") -> "tuple[dict, Optional[str]]":
|
|
1098
|
+
"""Normalize a client-supplied spill's ``alloc_mode`` value IN PLACE of the
|
|
1099
|
+
wire encoding: legacy aliases resolve to canonical; the two legacy-
|
|
1100
|
+
expressible modes (gpu-only / ram-only) are REWRITTEN onto the unchanged
|
|
1101
|
+
legacy ``n_gpu_layers`` wire, so ``alloc_mode`` survives here only for
|
|
1102
|
+
max-ram / explicit (the version-gated pair) and max-gpu.
|
|
1103
|
+
|
|
1104
|
+
max-gpu KEEPS its key as of 2026-07-25 — it used to be rewritten to ``{}``,
|
|
1105
|
+
which is also the CLEAR signal, so an explicit max-gpu deleted its own row
|
|
1106
|
+
instead of persisting (see the branch comment below). The key is stripped
|
|
1107
|
+
again at the emission seam, so the WIRE is unchanged; this function's output
|
|
1108
|
+
is what gets PERSISTED, and persistence is exactly what max-gpu was missing.
|
|
1109
|
+
|
|
1110
|
+
Returns ``(normalized_spill, note)`` — note is a human line when something
|
|
1111
|
+
was resolved/rewritten (for logs / say-why), None when untouched. Unknown
|
|
1112
|
+
mode values are DROPPED with a note (degrade-not-500; the rest of the
|
|
1113
|
+
spill still applies)."""
|
|
1114
|
+
if not isinstance(spill, dict) or "alloc_mode" not in spill:
|
|
1115
|
+
return (dict(spill) if isinstance(spill, dict) else {}), None
|
|
1116
|
+
out = dict(spill)
|
|
1117
|
+
raw = out.pop("alloc_mode")
|
|
1118
|
+
canonical, was_alias = resolve_alloc_mode(raw)
|
|
1119
|
+
if canonical is None:
|
|
1120
|
+
note = (f"unknown alloc_mode {raw!r} dropped (recognized: "
|
|
1121
|
+
f"{', '.join(ALLOC_MODES)}); rest of the spill still applies")
|
|
1122
|
+
logger.warning("normalize_spill: %s", note)
|
|
1123
|
+
return out, note
|
|
1124
|
+
note = (f"alloc_mode {raw!r} resolved to {canonical!r}" if was_alias else None)
|
|
1125
|
+
if canonical == "gpu-only":
|
|
1126
|
+
out["n_gpu_layers"] = -1
|
|
1127
|
+
elif canonical == "ram-only":
|
|
1128
|
+
out["n_gpu_layers"] = "off"
|
|
1129
|
+
elif canonical == "max-gpu":
|
|
1130
|
+
# EXPLICIT max-gpu KEEPS ITS KEY (bugfix 2026-07-25). It used to be
|
|
1131
|
+
# rewritten to {} here — but {} is ALSO the "clear this override" signal
|
|
1132
|
+
# (assign_model), so an operator who deliberately picked max-gpu had
|
|
1133
|
+
# their row DELETED instead of written, and then silently inherited
|
|
1134
|
+
# whatever the read-time derivation produced. That was harmless only
|
|
1135
|
+
# while the derivation was max-gpu for everything; once derived defaults
|
|
1136
|
+
# landed, a 12.29 GiB GGUF on a 7.6 GiB card derived max-ram and the
|
|
1137
|
+
# operator's explicit max-gpu became max-ram (the live repro).
|
|
1138
|
+
#
|
|
1139
|
+
# So the PERSISTED encoding is {"alloc_mode": "max-gpu"} — non-empty,
|
|
1140
|
+
# therefore structurally distinguishable from a clear. The WIRE stays
|
|
1141
|
+
# byte-identical to before: WorkerStore.spill_for strips this key at the
|
|
1142
|
+
# emission seam (see _WIRE_INERT_MODES there), so no worker ever sees it
|
|
1143
|
+
# and no version gate is engaged. A DERIVED max-gpu still yields {} via
|
|
1144
|
+
# mode_to_spill — only an operator's explicit pick persists.
|
|
1145
|
+
out.pop("n_gpu_layers", None) # no layer knob: max-gpu IS autofit
|
|
1146
|
+
out["alloc_mode"] = "max-gpu"
|
|
1147
|
+
else: # max-ram / explicit keep the key
|
|
1148
|
+
out["alloc_mode"] = canonical
|
|
1149
|
+
if note:
|
|
1150
|
+
logger.info("normalize_spill: %s", note)
|
|
1151
|
+
return out, note
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
def _ver_tuple(v: Any) -> "Optional[tuple]":
|
|
1155
|
+
try:
|
|
1156
|
+
parts = str(v).strip().split(".")
|
|
1157
|
+
return tuple(int(p) for p in parts) if parts else None
|
|
1158
|
+
except (TypeError, ValueError):
|
|
1159
|
+
return None
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def worker_honors_mode_keys(pkg_version: Any) -> bool:
|
|
1163
|
+
"""True when a worker's reported package version understands the NEW spill
|
|
1164
|
+
keys (>= MODE_MIN_PKG_VERSION). Unknown/unparseable -> False (fail SAFE:
|
|
1165
|
+
never ship a knob we can't prove the worker reads)."""
|
|
1166
|
+
have = _ver_tuple(pkg_version)
|
|
1167
|
+
need = _ver_tuple(MODE_MIN_PKG_VERSION)
|
|
1168
|
+
return have is not None and need is not None and have >= need
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
def worker_honors_no_evict(pkg_version: Any) -> bool:
|
|
1172
|
+
"""True when a worker's reported package version honors the polite-load
|
|
1173
|
+
flag (>= NO_EVICT_MIN_PKG_VERSION). Unknown/unparseable -> False (fail
|
|
1174
|
+
SAFE, same rule as worker_honors_mode_keys: never promise politeness we
|
|
1175
|
+
can't prove the worker keeps — a dropped no_evict evicts residents)."""
|
|
1176
|
+
have = _ver_tuple(pkg_version)
|
|
1177
|
+
need = _ver_tuple(NO_EVICT_MIN_PKG_VERSION)
|
|
1178
|
+
return have is not None and need is not None and have >= need
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
def no_evict_downgrade_note(pkg_version: Any, worker_name: str = "") -> str:
|
|
1182
|
+
"""The LOUD note for a polite load aimed at a worker that predates it."""
|
|
1183
|
+
return (f"worker {worker_name or '?'} (pkg {pkg_version or 'unknown'}) "
|
|
1184
|
+
f"predates the polite-load flag (needs >= "
|
|
1185
|
+
f"{NO_EVICT_MIN_PKG_VERSION}); no_evict STRIPPED — this worker "
|
|
1186
|
+
f"would evict residents to make room, so central does not route a "
|
|
1187
|
+
f"polite model here (update the worker to honor it)")
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
def worker_honors_chat_extras(pkg_version: Any) -> bool:
|
|
1191
|
+
"""True when a worker's reported package version forwards the per-request
|
|
1192
|
+
engine chat keys (>= CHAT_EXTRAS_MIN_PKG_VERSION). Unknown/unparseable ->
|
|
1193
|
+
False (fail SAFE, same rule as worker_honors_mode_keys: never ship a knob
|
|
1194
|
+
we can't prove the worker reads)."""
|
|
1195
|
+
have = _ver_tuple(pkg_version)
|
|
1196
|
+
need = _ver_tuple(CHAT_EXTRAS_MIN_PKG_VERSION)
|
|
1197
|
+
return have is not None and need is not None and have >= need
|
|
1198
|
+
|
|
1199
|
+
|
|
1200
|
+
def gate_chat_extras_for_worker(payload: "Optional[dict]", pkg_version: Any,
|
|
1201
|
+
worker_name: str = "") -> "tuple[dict, Optional[str]]":
|
|
1202
|
+
"""t74 version gate at the relay: a chat payload carrying
|
|
1203
|
+
``chat_template_kwargs``/``logit_bias`` only ships them to a worker that
|
|
1204
|
+
honors them; an older worker gets the keys STRIPPED, with a note the caller
|
|
1205
|
+
logs/surfaces. The stripped request still carries the /no_think directive
|
|
1206
|
+
in its messages (utils/no_think.py appends it caller-side), so hard
|
|
1207
|
+
suppression degrades to the soft switch — never to a rejected request and
|
|
1208
|
+
never to a silent no-op.
|
|
1209
|
+
|
|
1210
|
+
Returns ``(payload_to_emit, downgrade_note)``. A payload with neither key
|
|
1211
|
+
passes through untouched (None note) regardless of version."""
|
|
1212
|
+
p = dict(payload or {})
|
|
1213
|
+
present = sorted(set(p) & CHAT_EXTRAS_WIRE_KEYS)
|
|
1214
|
+
if not present or worker_honors_chat_extras(pkg_version):
|
|
1215
|
+
return p, None
|
|
1216
|
+
for k in present:
|
|
1217
|
+
p.pop(k, None)
|
|
1218
|
+
note = (f"worker {worker_name or '?'} (pkg {pkg_version or 'unknown'}) "
|
|
1219
|
+
f"predates per-request engine chat keys (needs >= "
|
|
1220
|
+
f"{CHAT_EXTRAS_MIN_PKG_VERSION}); {', '.join(present)} STRIPPED — "
|
|
1221
|
+
f"hard no-think degrades to the /no_think directive for this "
|
|
1222
|
+
f"request (update the worker to honor it)")
|
|
1223
|
+
return p, note
|
|
1224
|
+
|
|
1225
|
+
|
|
1226
|
+
def gate_spill_for_worker(spill: "Optional[dict]", pkg_version: Any,
|
|
1227
|
+
worker_name: str = "") -> "tuple[dict, Optional[str]]":
|
|
1228
|
+
"""THE version gate at emission: a spill carrying NEW mode keys is only
|
|
1229
|
+
sent to a worker that honors them; an older worker gets max-gpu ({}
|
|
1230
|
+
autofit) for that request, with a note the caller logs/surfaces.
|
|
1231
|
+
|
|
1232
|
+
Returns ``(spill_to_emit, downgrade_note)``. A spill with no new keys
|
|
1233
|
+
passes through untouched (None note) regardless of version.
|
|
1234
|
+
|
|
1235
|
+
k56: ``no_evict`` is gated SEPARATELY (its own, later min version) and is
|
|
1236
|
+
stripped rather than collapsing the spill — see NO_EVICT_SPILL_KEY. Checked
|
|
1237
|
+
first so a polite spill carrying no mode keys at all is still gated."""
|
|
1238
|
+
s = dict(spill or {})
|
|
1239
|
+
polite_note = None
|
|
1240
|
+
if s.get(NO_EVICT_SPILL_KEY) and not worker_honors_no_evict(pkg_version):
|
|
1241
|
+
s.pop(NO_EVICT_SPILL_KEY, None)
|
|
1242
|
+
polite_note = no_evict_downgrade_note(pkg_version, worker_name)
|
|
1243
|
+
if not (set(s) & NEW_SPILL_KEYS):
|
|
1244
|
+
return s, polite_note
|
|
1245
|
+
if worker_honors_mode_keys(pkg_version):
|
|
1246
|
+
return s, polite_note
|
|
1247
|
+
mode = s.get("alloc_mode") or "explicit"
|
|
1248
|
+
note = (f"worker {worker_name or '?'} (pkg {pkg_version or 'unknown'}) "
|
|
1249
|
+
f"predates allocation-mode spill keys (needs >= "
|
|
1250
|
+
f"{MODE_MIN_PKG_VERSION}); '{mode}' downgraded to max-gpu "
|
|
1251
|
+
f"(autofit) for this request — update the worker to honor it")
|
|
1252
|
+
# A worker old enough to miss the mode keys misses politeness too; report
|
|
1253
|
+
# BOTH downgrades or the operator fixes one and re-hits the other.
|
|
1254
|
+
return {}, "; ".join(n for n in (polite_note, note) if n)
|