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.
Files changed (128) hide show
  1. hugpy_engine/__init__.py +163 -0
  2. hugpy_engine/alloc_modes.py +1254 -0
  3. hugpy_engine/allocation.py +68 -0
  4. hugpy_engine/apis/__init__.py +0 -0
  5. hugpy_engine/apis/call_api.py +149 -0
  6. hugpy_engine/apis/get_module.py +606 -0
  7. hugpy_engine/apis/reclassify.py +123 -0
  8. hugpy_engine/apis/reconcile.py +628 -0
  9. hugpy_engine/apis/serve/__init__.py +22 -0
  10. hugpy_engine/apis/serve/serve.py +34 -0
  11. hugpy_engine/apis/serve/serve_cli.py +9 -0
  12. hugpy_engine/apis/systemd_units.py +9 -0
  13. hugpy_engine/backend.py +86 -0
  14. hugpy_engine/backends/__init__.py +5 -0
  15. hugpy_engine/backends/local.py +163 -0
  16. hugpy_engine/catalog.py +99 -0
  17. hugpy_engine/catalog_bridge.py +223 -0
  18. hugpy_engine/categories.py +117 -0
  19. hugpy_engine/chat_context/__init__.py +27 -0
  20. hugpy_engine/chat_context/chat_context.py +199 -0
  21. hugpy_engine/chat_context/context_budget.py +223 -0
  22. hugpy_engine/chat_context/unbounded.py +55 -0
  23. hugpy_engine/config/__init__.py +0 -0
  24. hugpy_engine/config/main.py +270 -0
  25. hugpy_engine/config/models/__init__.py +0 -0
  26. hugpy_engine/config/models/model_meta.py +169 -0
  27. hugpy_engine/config/models/models_config.py +1495 -0
  28. hugpy_engine/config/models/models_default.py +196 -0
  29. hugpy_engine/config/models/models_dict.py +73 -0
  30. hugpy_engine/contracts.py +44 -0
  31. hugpy_engine/dispatch/__init__.py +37 -0
  32. hugpy_engine/dispatch/acquire.py +59 -0
  33. hugpy_engine/dispatch/activity.py +92 -0
  34. hugpy_engine/dispatch/dispatch.py +1100 -0
  35. hugpy_engine/draft_models.py +72 -0
  36. hugpy_engine/engines.py +44 -0
  37. hugpy_engine/eviction.py +654 -0
  38. hugpy_engine/generate/__init__.py +16 -0
  39. hugpy_engine/generate/coder.py +667 -0
  40. hugpy_engine/generate/config.py +185 -0
  41. hugpy_engine/generate/generate_runner.py +273 -0
  42. hugpy_engine/gguf_election.py +259 -0
  43. hugpy_engine/llama/__init__.py +23 -0
  44. hugpy_engine/llama/runners/__init__.py +23 -0
  45. hugpy_engine/llama/runners/chat_runner.py +243 -0
  46. hugpy_engine/llama/runners/get.py +524 -0
  47. hugpy_engine/llama/runners/src/__init__.py +9 -0
  48. hugpy_engine/llama/runners/src/base_runner.py +529 -0
  49. hugpy_engine/llama/runners/src/ccp_runner.py +332 -0
  50. hugpy_engine/llama/runners/src/imports/__init__.py +25 -0
  51. hugpy_engine/llama/runners/src/imports/config.py +16 -0
  52. hugpy_engine/llama/runners/src/imports/constants.py +38 -0
  53. hugpy_engine/llama/runners/src/imports/init_imports.py +6 -0
  54. hugpy_engine/llama/runners/src/imports/utils.py +64 -0
  55. hugpy_engine/llama/runners/src/python_runner.py +432 -0
  56. hugpy_engine/llama/runners/src/shard_server.py +339 -0
  57. hugpy_engine/llama/runners/src/shard_server.py.bak-shardorder +329 -0
  58. hugpy_engine/llama/serve.py +9 -0
  59. hugpy_engine/manifest.py +53 -0
  60. hugpy_engine/model_classifier.py +374 -0
  61. hugpy_engine/model_index/__init__.py +74 -0
  62. hugpy_engine/model_index/client.py +94 -0
  63. hugpy_engine/model_index/query_registry.py +245 -0
  64. hugpy_engine/model_index/repositories.py +163 -0
  65. hugpy_engine/model_index/service.py +243 -0
  66. hugpy_engine/name_match.py +287 -0
  67. hugpy_engine/native/__init__.py +19 -0
  68. hugpy_engine/native/build.py +98 -0
  69. hugpy_engine/native/fetch.py +194 -0
  70. hugpy_engine/native/resolve.py +352 -0
  71. hugpy_engine/peft_adapters.py +293 -0
  72. hugpy_engine/placement.py +459 -0
  73. hugpy_engine/py.typed +0 -0
  74. hugpy_engine/query.py +173 -0
  75. hugpy_engine/resolvers/__init__.py +24 -0
  76. hugpy_engine/resolvers/allocator.py +126 -0
  77. hugpy_engine/resolvers/assure_model_key.py +331 -0
  78. hugpy_engine/resolvers/categories/__init__.py +22 -0
  79. hugpy_engine/resolvers/categories/builders.py +159 -0
  80. hugpy_engine/resolvers/categories/frameworks.py +67 -0
  81. hugpy_engine/resolvers/groups.py +651 -0
  82. hugpy_engine/resolvers/model_dict_resolver.py +122 -0
  83. hugpy_engine/resolvers/model_resolver.py +533 -0
  84. hugpy_engine/resolvers/remote.py +2992 -0
  85. hugpy_engine/runtime.py +68 -0
  86. hugpy_engine/schemas/__init__.py +0 -0
  87. hugpy_engine/schemas/chat_schemas.py +320 -0
  88. hugpy_engine/schemas/event_schemas.py +52 -0
  89. hugpy_engine/schemas/metadata_schemas.py +62 -0
  90. hugpy_engine/schemas/model_schemas.py +101 -0
  91. hugpy_engine/schemas/runner_schemas.py +33 -0
  92. hugpy_engine/schemas/task_schemas.py +45 -0
  93. hugpy_engine/serve/__init__.py +58 -0
  94. hugpy_engine/serve/chunksum_verify.py +213 -0
  95. hugpy_engine/serve/hot_cache.py +1014 -0
  96. hugpy_engine/serve/model_cache.py +218 -0
  97. hugpy_engine/serve/overrides.py +968 -0
  98. hugpy_engine/serve/policy.py +45 -0
  99. hugpy_engine/serve/profiles.py +362 -0
  100. hugpy_engine/serve/serve.py +889 -0
  101. hugpy_engine/serve/serve.py.bak-present-20260918-035851 +863 -0
  102. hugpy_engine/serve/serve.py.bak-servelog-20260918-035149 +863 -0
  103. hugpy_engine/serve/serve_cli.py +85 -0
  104. hugpy_engine/serve/slot_agent.py +2119 -0
  105. hugpy_engine/serve/slot_agent.py.bak-quietlog-20260918-023710 +2046 -0
  106. hugpy_engine/serve/slot_agent.py.bak-slotparallel-20260918-112917 +2064 -0
  107. hugpy_engine/serve/slot_agent.py.bak-wzfilter-20260918-041625 +2047 -0
  108. hugpy_engine/serve/slots.py +630 -0
  109. hugpy_engine/serve/supervisor.py +148 -0
  110. hugpy_engine/spill.py +1464 -0
  111. hugpy_engine/task_deps.py +67 -0
  112. hugpy_engine/tasks.py +349 -0
  113. hugpy_engine/utils/__init__.py +0 -0
  114. hugpy_engine/utils/json_scavenge.py +111 -0
  115. hugpy_engine/utils/no_think.py +370 -0
  116. hugpy_engine/wire/__init__.py +0 -0
  117. hugpy_engine/wire/chat_schemas.py +66 -0
  118. hugpy_engine/wire/config_schemas.py +31 -0
  119. hugpy_engine/wire/install_schemas.py +17 -0
  120. hugpy_engine/wire/model_schemas.py +43 -0
  121. hugpy_engine/wire/request_schemas.py +13 -0
  122. hugpy_engine/wire/specs_schemas.py +9 -0
  123. hugpy_engine-0.2.0a0.dist-info/METADATA +116 -0
  124. hugpy_engine-0.2.0a0.dist-info/RECORD +128 -0
  125. hugpy_engine-0.2.0a0.dist-info/WHEEL +5 -0
  126. hugpy_engine-0.2.0a0.dist-info/entry_points.txt +2 -0
  127. hugpy_engine-0.2.0a0.dist-info/licenses/LICENSE +41 -0
  128. 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)