frutebox-node 0.1.0__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.
frutebox/serving.py ADDED
@@ -0,0 +1,691 @@
1
+ """Serving-config RESOLVER (POST_TEST_REWORK #2 / "any model serves when armed").
2
+
3
+ The benchmark session proved that *how* to serve a model is model-specific and easy to get wrong:
4
+ Ollama cannot serve sharded GGUF (every Kimi quant is sharded) so those need llama.cpp's server;
5
+ big MoE models need multi-GPU placement and the right quant; reasoning models need a large output
6
+ budget. The owner must not babysit any of this — arming a toggle must Just Work for ANY catalog
7
+ model. This module is the single place that answers, for a given model id:
8
+
9
+ * which ENGINE serves it (llama.cpp for sharded GGUF, Ollama for simple single-file models),
10
+ * the container IMAGE + how the model is referenced (HF GGUF repo:quant, or an Ollama tag),
11
+ * how much VRAM the chosen quant needs, and
12
+ * a COST-RANKED, cheapest-first list of GPU configs that actually FIT it — which both selects the
13
+ cheapest placement and fixes the "no capacity / won't place" failures (try the next config).
14
+
15
+ Pure + deterministic (no network); RunPod stock filtering happens at deploy time by walking the
16
+ returned prioritized list. Unit-tested.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from dataclasses import dataclass, field
21
+
22
+ import logging
23
+
24
+ from .envvars import env as envvar
25
+ from .manifest import _BY_ID, classify_capability
26
+
27
+ log = logging.getLogger(__name__)
28
+
29
+ # --- GPU inventory: type -> (usable VRAM GB, on-demand $/hr). Prices are grounded in what we
30
+ # observed on RunPod this session (A100 PCIe x8 = $9.52/hr -> ~$1.19/gpu; H200 x5 = $22.95 ->
31
+ # ~$4.59/gpu; RTX PRO 6000 x8 = $16.72 -> ~$2.09/gpu) plus standard small-GPU rates. Cheapest-first
32
+ # ordering here drives "cheapest GPU that fits". Operator can override via config later.
33
+ # `fp8` = the card has NATIVE FP8 tensor cores (Hopper / Ada / Blackwell). Ampere (A40, A100,
34
+ # A6000, Ax000) does NOT: an FP8 checkpoint placed there either refuses to load or falls back to
35
+ # emulation that is slower than the FP16 it was supposed to beat. Cost-ranking alone would pick
36
+ # 6xA40 at $2.40/hr over 2xH200 at $9.18/hr and produce a pod that cannot serve the model — the
37
+ # cheapest config that CANNOT RUN THE JOB is not the cheapest config.
38
+ GPU_INVENTORY: list[dict] = [
39
+ {"type": "RTX A4000", "vram_gb": 16, "usd_per_hr": 0.17, "fp8": False},
40
+ {"type": "RTX A5000", "vram_gb": 24, "usd_per_hr": 0.26, "fp8": False},
41
+ {"type": "A40", "vram_gb": 48, "usd_per_hr": 0.40, "fp8": False},
42
+ {"type": "RTX A6000", "vram_gb": 48, "usd_per_hr": 0.49, "fp8": False},
43
+ {"type": "A100 PCIe", "vram_gb": 80, "usd_per_hr": 1.19, "fp8": False},
44
+ {"type": "A100 SXM", "vram_gb": 80, "usd_per_hr": 1.29, "fp8": False},
45
+ {"type": "RTX PRO 6000", "vram_gb": 96, "usd_per_hr": 2.09, "fp8": True},
46
+ {"type": "H100 SXM", "vram_gb": 80, "usd_per_hr": 2.69, "fp8": True},
47
+ {"type": "H200 SXM", "vram_gb": 141, "usd_per_hr": 4.59, "fp8": True},
48
+ ]
49
+ # Max GPUs we'll pool into a single node (RunPod caps multi-GPU pods).
50
+ _MAX_GPUS = 8
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class GpuConfig:
55
+ gpu_type: str
56
+ count: int
57
+ total_vram_gb: int
58
+ usd_per_hr: float
59
+
60
+ def label(self) -> str:
61
+ return f"{self.gpu_type} x{self.count} ({self.total_vram_gb}GB, ${self.usd_per_hr:.2f}/hr)"
62
+
63
+
64
+ def gpu_plan(vram_needed_gb: float, *, headroom: float = 1.15, max_gpus: int = _MAX_GPUS,
65
+ inventory=GPU_INVENTORY, require_fp8: bool = False) -> list[GpuConfig]:
66
+ """Cost-ranked (cheapest-first) list of GPU configs whose total VRAM covers vram_needed*headroom.
67
+ A node arms by walking this list and taking the first that RunPod has in stock — so it always
68
+ lands on the CHEAPEST placement available, and never fails just because the ideal config is out
69
+ of stock."""
70
+ need = vram_needed_gb * headroom
71
+ out: list[GpuConfig] = []
72
+ for g in inventory:
73
+ if require_fp8 and not g.get("fp8"):
74
+ continue
75
+ # smallest count of this type that covers need (cap at max_gpus)
76
+ for n in range(1, max_gpus + 1):
77
+ if g["vram_gb"] * n >= need:
78
+ out.append(GpuConfig(g["type"], n, g["vram_gb"] * n, round(g["usd_per_hr"] * n, 2)))
79
+ break
80
+ out.sort(key=lambda c: (c.usd_per_hr, c.total_vram_gb))
81
+ return out
82
+
83
+
84
+ # --- Per-model serving recipes.
85
+ #
86
+ # FILE 01-B — WHY THIS CHANGED. The frontier pod served Kimi through llama.cpp with
87
+ # `--parallel 1`: one request at a time on a $17.95-22.95/hr node, which works out at roughly
88
+ # $165-210 per 1M output tokens. The GPU is idle for most of every request and the cost is
89
+ # carried by a single user. Batching is the whole fix — the same pod serving 8-16 concurrent
90
+ # streams divides that fixed hourly cost across all of them.
91
+ #
92
+ # Engine choice, in order of preference:
93
+ # * SGLang — PRIMARY. RadixAttention reuses the KV prefix across turns automatically, which
94
+ # is exactly our workload (multi-turn chat over a stable system prompt + brain
95
+ # context). Serves safetensors/FP8, not GGUF.
96
+ # * vLLM — FALLBACK for models SGLang cannot serve. Broad coverage, PagedAttention.
97
+ # * llama.cpp — the ONLY engine for GGUF-only quants (every Kimi quant is sharded GGUF), and
98
+ # the right one for the small always-warm C/B node. Now batched, not `--parallel 1`.
99
+ # * Ollama — simple single-file models on the small node.
100
+ LLAMACPP_IMAGE = "ghcr.io/ggml-org/llama.cpp:server-cuda"
101
+ OLLAMA_IMAGE = "ollama/ollama:latest"
102
+ SGLANG_IMAGE = "lmsysorg/sglang:latest"
103
+ #: ComfyUI, for the diffusion models (image/video). VERIFIED TO EXIST before being written here:
104
+ #: `runpod/worker-comfyui` and `yanwk/comfyui-boot` are both 404 on Docker Hub; this one answers 200.
105
+ #: An image reference nobody checked is the "RUNNING with no runtime" failure waiting to happen.
106
+ COMFYUI_IMAGE = "ghcr.io/ai-dock/comfyui:latest"
107
+ #: ComfyUI's HTTP port, and the path that proves the SERVER is up rather than the container.
108
+ #: `/` serves the web UI; `/system_stats` is the API's own health answer — the same distinction
109
+ #: `PodSpec.health_path` exists for (SGLang has no `/`).
110
+ COMFYUI_PORT = 8188
111
+ COMFYUI_HEALTH_PATH = "/system_stats"
112
+
113
+ #: Concurrent sequences a tier-S pod serves. 16 is the file-11-B5 starting point: high enough
114
+ #: to divide the hourly cost properly, low enough that per-stream latency stays acceptable.
115
+ TIER_S_MAX_SEQS = 16
116
+ #: llama.cpp's equivalent, for the GGUF models SGLang cannot serve.
117
+ LLAMACPP_PARALLEL = 8
118
+
119
+ # HF GGUF repo + a default quant (repo:quant) + that quant's approx VRAM, for the models that must
120
+ # run on llama.cpp. Quant chosen as the quality/size balance that fits a reasonable node.
121
+ #: Models served by SGLang from FP8 safetensors. FP8 weights + an FP8 KV cache roughly halve
122
+ #: memory versus FP16 with minimal quality loss on Hopper, which is what lets the 235B MoE fit
123
+ #: on 2xH200 (~$7.18/hr Community) instead of the 5xH200 pod at $17.95-22.95/hr.
124
+ _FP8_MODELS: dict[str, dict] = {
125
+ # 235B params at FP8 ~= 235 GB of weights; 2xH200 = 282 GB leaves headroom for the KV
126
+ # cache, which is what makes the $7.18/hr Community placement possible at all.
127
+ #
128
+ # `reasoning: True` and the parser are not a guess — MEASURED on a live pod 2026-08-28: this
129
+ # model emits <think> blocks. Without `--reasoning-parser` SGLang returns that reasoning
130
+ # inside `content`, so the user is shown the model thinking out loud instead of its answer,
131
+ # and short max_tokens truncates mid-thought with no answer at all (observed: 120 tokens of
132
+ # "Okay, so I need to find 17.3% of 4821. Hmm..." and nothing else).
133
+ "qwen3-235b-a22b": {"repo": "Qwen/Qwen3-235B-A22B-FP8", "vram_gb": 235,
134
+ "reasoning": True, "ctx": 32768, "min_gpus": 2,
135
+ "reasoning_parser": "qwen3"},
136
+ # The tier-A default, and one of the two models file 02-C2 keeps on a seeded backup volume
137
+ # ("the tier-S default + tier-A default"). It was in the catalog with no way to serve it, so
138
+ # the backup lane could seed it and then not bring it up.
139
+ #
140
+ # SGLang, not llama.cpp: the engine order above makes SGLang primary and reserves llama.cpp
141
+ # for GGUF-ONLY quants. Qwen publishes official FP8 safetensors, so this is not GGUF-only.
142
+ #
143
+ # vram_gb is MEASURED from the repo (30.9 GB of FP8 weights), not taken from the catalog's
144
+ # 22.0 — that figure appears to describe a smaller quant, and a serving spec that under-states
145
+ # its own weights picks a GPU the model does not fit on. Rounded up, leaving KV-cache room on
146
+ # a 48 GB card.
147
+ #
148
+ # FP8 needs Hopper/Ada/Blackwell; Ampere (A40/A6000/A5000) cannot run it. `gpu_plan(...,
149
+ # require_fp8=True)` already enforces that, so an Ampere-only region will simply find no
150
+ # placement rather than starting and failing.
151
+ #
152
+ # reasoning=True with the qwen3 parser: the 235B in this same family was MEASURED emitting
153
+ # <think> blocks on live hardware, and the asymmetry favours declaring it — a parser on a
154
+ # model that does not think is harmless, while no parser on one that does puts the reasoning
155
+ # in `content` and truncates the answer. To be confirmed on this model's first live run.
156
+ "qwen-3.6-27b": {"repo": "Qwen/Qwen3.6-27B-FP8", "vram_gb": 31,
157
+ "reasoning": True, "ctx": 32768, "min_gpus": 1,
158
+ "reasoning_parser": "qwen3"},
159
+ }
160
+
161
+ _LLAMACPP_MODELS: dict[str, dict] = {
162
+ "kimi-k2.7-code": {"repo": "unsloth/Kimi-K2.7-Code-GGUF", "quant": "UD-Q8_K_XL",
163
+ "vram_gb": 594, "reasoning": True, "ctx": 32768},
164
+ "kimi-k2.6": {"repo": "unsloth/Kimi-K2.6-GGUF", "quant": "UD-Q4_K_XL",
165
+ "vram_gb": 590, "reasoning": False, "ctx": 16384},
166
+ "qwen3-235b-a22b": {"repo": "unsloth/Qwen3-235B-A22B-GGUF", "quant": "UD-Q4_K_XL",
167
+ "vram_gb": 140, "reasoning": False, "ctx": 16384},
168
+ }
169
+
170
+
171
+ # ---- FILE 11-B2: speculative decoding ---------------------------------------------------------
172
+ #
173
+ # WHY IT IS WORTH THE COMPLICATION. Speculative decoding is quality-LOSSLESS — the target model
174
+ # verifies every drafted token, so the output distribution is unchanged — and it buys roughly 2-3x on
175
+ # decode, which is the half of latency the user watches. 11-A's stream-rate budgets (>=30 tok/s local,
176
+ # >=20 tok/s tier-S) are decode budgets.
177
+ #
178
+ # WHICH METHOD IS A FACT ABOUT THE MODEL, so it lives in the registry (`speed.spec_decode`, added to
179
+ # `frutebox/registry/models.yaml`), not in a table here. A serving module with its own opinion about
180
+ # a model's drafting method is a second source of truth, and the registry already owns the others.
181
+ #
182
+ # P-EAGLE IS NOT AVAILABLE ON OUR PRIMARY ENGINE, and the plan says to use it. `implementation_plan/
183
+ # libraries.md` line 16 (restated at 151) records that P-EAGLE is vLLM v0.16+ only and not merged in
184
+ # SGLang; SGLang is primary. So `p-eagle` in the registry renders as EAGLE3 on SGLang and gets its
185
+ # parallel-drafting flag only on vLLM.
186
+ #
187
+ # AND THE HEAD PATH IS NOT GUESSED. SGLang's EAGLE3 needs `--speculative-draft-model-path <head>`,
188
+ # published per target model as a separate artefact. A guessed repo name produces a pod that fails to
189
+ # start — or one that starts with the wrong head, which is worse because it looks like it worked. So
190
+ # the path comes from configuration, and when it is missing speculative decoding is OFF and SAYS SO.
191
+ # The alternative — shipping a plausible-looking path — is how an unverifiable claim reaches a
192
+ # runbook.
193
+ _SPEC_ENV_PREFIX = "EAGLE3_HEAD_"
194
+
195
+
196
+ def eagle3_head(model_id: str) -> str:
197
+ """The configured EAGLE-3 head for a model, or "" when nobody has named one.
198
+
199
+ Env name is the model id upper-cased with `-`/`.` as `_`, e.g.
200
+ `FRUTEBOX_EAGLE3_HEAD_QWEN3_CODER`.
201
+ """
202
+ key = _SPEC_ENV_PREFIX + model_id.upper().replace("-", "_").replace(".", "_")
203
+ return (envvar(key, "") or "").strip()
204
+
205
+
206
+ #: ONE VARIABLE TURNS IT OFF. These flags have not yet been accepted by a live SGLang process on this
207
+ #: account — 11's own done-criterion is a MEASURED >=1.6x, which needs an armed pod — and an engine
208
+ #: that rejects an unknown argument does not start. So the switch exists to make recovery a config
209
+ #: change rather than a code change, and the A/B the criterion asks for is one flag each way.
210
+ SPEC_DECODE_ENABLED = envvar("SPEC_DECODE", "true").strip().lower() == "true"
211
+
212
+
213
+ def spec_decode_args(model_id: str, engine: str, *, speed: "dict | None" = None,
214
+ enabled: "bool | None" = None) -> str:
215
+ """The engine flags for this model's speculative decoding, or "" when it has none.
216
+
217
+ Returns flags only for a method the ENGINE can actually run with the pieces we HAVE. Every other
218
+ case returns nothing and logs the reason at resolve time rather than at boot: a pod is the most
219
+ expensive place to discover a missing head.
220
+ """
221
+ if not (SPEC_DECODE_ENABLED if enabled is None else enabled):
222
+ return ""
223
+ if speed is None:
224
+ try:
225
+ from .registry_gen import BY_ID
226
+ speed = (BY_ID.get(model_id) or {}).get("speed") or {}
227
+ except Exception: # pragma: no cover - registry import guard
228
+ speed = {}
229
+ method = str((speed or {}).get("spec_decode") or "none")
230
+ k = int((speed or {}).get("K") or 7)
231
+ if method == "none":
232
+ return ""
233
+ if engine == "llamacpp":
234
+ # MTP comes from the target's own heads; llama.cpp enables it by serving a model that has
235
+ # them, with no flag of its own. Nothing to add, and nothing to claim.
236
+ return ""
237
+ if method == "mtp":
238
+ return "" # an MTP target on a non-llama.cpp engine: no flag
239
+ if method == "draft":
240
+ draft = str((speed or {}).get("draft_model") or "").strip()
241
+ if not draft:
242
+ return ""
243
+ if engine == "sglang":
244
+ return (f"--speculative-algorithm EAGLE3 --speculative-draft-model-path {draft} "
245
+ f"--speculative-num-steps {k}")
246
+ if engine == "vllm":
247
+ return f"--speculative-config {{\"model\":\"{draft}\",\"num_speculative_tokens\":{k}}}"
248
+ return ""
249
+ # eagle3 / p-eagle: both need a HEAD, and only vLLM can do the parallel-drafting variant.
250
+ head = str((speed or {}).get("draft_model") or "").strip() or eagle3_head(model_id)
251
+ if not head:
252
+ log.warning("speculative decoding OFF for %s: method %s needs an EAGLE-3 head and none is "
253
+ "configured (set FRUTEBOX_%s%s). Serving correctly, just not faster.",
254
+ model_id, method, _SPEC_ENV_PREFIX,
255
+ model_id.upper().replace("-", "_").replace(".", "_"))
256
+ return ""
257
+ if engine == "sglang":
258
+ return (f"--speculative-algorithm EAGLE3 --speculative-draft-model-path {head} "
259
+ f"--speculative-num-steps {k}")
260
+ if engine == "vllm":
261
+ cfg = (f'{{"method":"eagle3","model":"{head}","num_speculative_tokens":{k}'
262
+ + (',"parallel_drafting":true' if method == "p-eagle" else "") + "}")
263
+ return f"--speculative-config {cfg}"
264
+ return ""
265
+
266
+
267
+ @dataclass(frozen=True)
268
+ class ServingSpec:
269
+ model: str
270
+ engine: str # "llamacpp" | "ollama"
271
+ image: str
272
+ port: int
273
+ model_ref: str # "repo:quant" (llama.cpp -hf) or an ollama pull tag
274
+ vram_gb: float # VRAM the chosen quant needs
275
+ reasoning: bool
276
+ ctx: int
277
+ args: str # container args (llama.cpp) — empty for ollama (uses env/pull)
278
+ #: THE FOREGROUND COMMAND a pod must run, `args` included. A recipe that stops at flags is not
279
+ #: deployable: `lmsysorg/sglang` has no server CMD of its own, so a pod created with only the
280
+ #: flags starts, exits, and shows up as the "RUNNING with no runtime" failure the RunPod KB
281
+ #: opens with. llama.cpp's image DOES have one (`/app/llama-server`), so its flags are the
282
+ #: whole command. Ollama's comes from the node image's entrypoint and stays empty.
283
+ #:
284
+ #: This field exists because `ServingSpec.image`/`args` had no launcher: until 01-B4's bench
285
+ #: runner, `serving_spec` was called only by `model_fit_error`, which reads `vram_gb` and
286
+ #: `engine` and never starts anything. The tier-S recipe was therefore never executed.
287
+ command: str = ""
288
+ gpu_configs: list = field(default_factory=list) # cost-ranked placements that fit
289
+
290
+ def cheapest(self):
291
+ return self.gpu_configs[0] if self.gpu_configs else None
292
+
293
+
294
+ #: The runtimes `serving_spec` can actually produce a deployable command for.
295
+ #:
296
+ #: ComfyUI IS listed, and only because `_COMFYUI_MODELS` now builds a real fetch-and-launch command
297
+ #: for the four models in it (image, music, video). It is still NOT a blanket claim about the runtime:
298
+ #: the remaining comfyui rows have no entry there and `source_problem` reports them as missing a
299
+ #: recipe. What this line must never go back to is naming a runtime whose branch does not exist —
300
+ #: see the vllm note below for what that cost.
301
+ #:
302
+ #: **`vllm` WAS LISTED HERE AND SHOULD NOT HAVE BEEN** (caught 2026-10-01, by checking). There is no
303
+ #: vLLM branch in `serving_spec` — the engines it builds for are SGLang, llama.cpp and Ollama — so
304
+ #: naming vllm here made `source_problem()` report nothing for 34 models that resolve to an empty
305
+ #: command, three of them EU models whose sources were pinned the same day (`mistral-large-3`,
306
+ #: `pixtral`, `apertus-70b`). A list of what we can serve, kept by hand beside the code that serves,
307
+ #: is the same drift this whole section is about; `test_servable_runtimes_are_runtimes_we_can_really
308
+ #: _serve` now derives the check from the code instead of trusting this line.
309
+ SERVABLE_RUNTIMES = frozenset({"sglang", "llamacpp", "ollama", "comfyui"})
310
+
311
+
312
+ def _shquote(s: str) -> str:
313
+ """Quote one shell word. `json.dumps` would escape non-ASCII into \\uXXXX inside a shell string."""
314
+ return "'" + s.replace("'", "'\\''") + "'"
315
+
316
+
317
+ def registry_source(model_id: str) -> dict:
318
+ """The PINNED source the registry records for this model, or `{}`.
319
+
320
+ 28 §B. The weights' location is a fact about the MODEL, so it lives in `registry/models.yaml`
321
+ beside its licence and its bloc, and codegen refuses an enabled model that has neither a source
322
+ nor a stated reason for having none. It used to live in two hand-maintained dicts in this file,
323
+ which covered 4 of 29 enabled models — the other 25 fell through to the Ollama branch below and
324
+ got an EMPTY command, while this function's docstring claimed "nothing is unservable".
325
+ """
326
+ # `registry_gen.BY_ID`, NOT `manifest._BY_ID`. They are different dictionaries: the manifest holds
327
+ # its own hand-written `MODEL_CATALOG` with a reduced field set and no `source` at all, so reading
328
+ # it here returned `{}` for every model and the pins would have been dead data.
329
+ from .registry_gen import BY_ID as _REG
330
+ return dict((_REG.get(model_id) or {}).get("source") or {})
331
+
332
+
333
+ def hf_token_configured() -> bool:
334
+ """Is a Hugging Face credential resolvable? PRESENCE ONLY — the value is never read here or logged.
335
+
336
+ `registry/providers.yaml` declares `HF_TOKEN` on the `huggingface` row and **nothing in the codebase
337
+ read it**, which matters now that five pinned sources are GATED (both meta-llama Llamas,
338
+ FLUX.1-schnell, SD-3.5-large, stable-audio-open). A gated repo pulled without a token answers 401, so
339
+ the pod reaches RUNNING, fails to start a server, is reaped by the readiness guard, and the whole
340
+ thing is diagnosed as a capacity shortage — the "fails after the money is spent" shape this plan
341
+ keeps finding. Asking BEFORE the launch turns that into a refusal that costs nothing.
342
+
343
+ Goes through `gpuproviders.provider_credential`, the one door: file 28 §O made the database the
344
+ source of truth, so an `os.environ` read here would answer "absent" for a deployment whose token
345
+ lives in the DB.
346
+ """
347
+ try:
348
+ from .gpuproviders import provider_credential
349
+ return bool(provider_credential("huggingface", "HF_TOKEN"))
350
+ except Exception: # pragma: no cover - registry unreachable offline
351
+ return False
352
+
353
+
354
+ def source_problem(model_id: str) -> str:
355
+ """Why this model cannot be served from a pinned source, or "" when it can.
356
+
357
+ Said out loud rather than returned as an empty command. An unservable model used to be
358
+ indistinguishable from a servable one until a pod had been paid for and reaped.
359
+ """
360
+ from .registry_gen import BY_ID as _REG
361
+ cat = _REG.get(model_id)
362
+ if cat is None:
363
+ return f"{model_id} is not in the model registry"
364
+ if cat.get("source_unavailable"):
365
+ return f"{model_id} has no open weights: {cat['source_unavailable']}"
366
+ src = registry_source(model_id)
367
+ if not src:
368
+ return (f"{model_id} has no pinned source in registry/models.yaml, so there is nowhere to "
369
+ f"fetch its weights from")
370
+ runtimes = list(cat.get("runtime") or [])
371
+ if "ollama" in runtimes and not src.get("ollama_tag"):
372
+ return (f"{model_id} declares runtime 'ollama' and the registry has no ollama_tag for it "
373
+ f"(the catalog id is not a pullable tag)")
374
+ if not runtimes:
375
+ return f"{model_id} declares no runtime"
376
+ # AND A RUNTIME THIS MODULE CANNOT BUILD A COMMAND FOR IS A REASON, NOT A SILENCE. The first
377
+ # version of this function returned "" for the 13 `comfyui` models, which have real pinned
378
+ # sources and no serving recipe anywhere in the codebase — so it reported them as fine while
379
+ # `serving_spec` handed back an empty command. A pinned source is necessary and not sufficient.
380
+ if not any(r in SERVABLE_RUNTIMES for r in runtimes):
381
+ return (f"{model_id} declares runtime(s) {runtimes} and this module builds commands only for "
382
+ f"{sorted(SERVABLE_RUNTIMES)}. Its source is pinned; what is missing is a serving "
383
+ f"recipe for that runtime.")
384
+ if "comfyui" in runtimes and model_id not in _COMFYUI_MODELS:
385
+ return (f"{model_id} declares runtime 'comfyui' and has no entry in the ComfyUI serving "
386
+ f"table; this module builds commands only for the ComfyUI models listed there. "
387
+ f"Its source is pinned; what is missing is a serving recipe for that model")
388
+ if "llamacpp" in runtimes and "ollama" not in runtimes and model_id not in _LLAMACPP_MODELS:
389
+ return (f"{model_id} declares runtime 'llamacpp' and has no entry in the llama.cpp serving "
390
+ f"table, so there is no quant or context length to serve it with")
391
+ # THE SAME GAP, FOR SGLANG, and it was silent until the 2026-10-10 scan walked into it.
392
+ # `SERVABLE_RUNTIMES` includes sglang, but the recipe comes from `_FP8_MODELS`, which has two
393
+ # entries. So a model declaring sglang with a PINNED source passed every check above and
394
+ # returned "" — reported as fine while `serving_spec` produced an empty command. The llamacpp
395
+ # branch directly above exists for exactly this shape; this is its missing sibling, found by
396
+ # `test_28_pinned_model_sources.py::test_no_model_anywhere_is_silently_unservable` the moment
397
+ # two rows with real pinned sources (deepseek-v4.1-flash, glm-5.2) declared sglang.
398
+ if ("sglang" in runtimes and "ollama" not in runtimes and "llamacpp" not in runtimes
399
+ and model_id not in _FP8_MODELS):
400
+ return (f"{model_id} declares runtime 'sglang' and has no entry in the FP8/SGLang serving "
401
+ f"table, so there is no tensor-parallel degree or context length to serve it with")
402
+ # GATED AND NO TOKEN: pinned, real, and not fetchable. REPORTED LAST, after the runtime questions,
403
+ # because a missing recipe is the more fundamental blocker — telling someone to accept a licence and
404
+ # store a token for a model that nothing in this codebase can stand up sends them to do useless work.
405
+ # All three gated models that reach here are ComfyUI, and that is what they are told.
406
+ #
407
+ # Only where the weights would actually come from the gated repo: `gated` is a fact about the HUGGING
408
+ # FACE repo, and Ollama serves several of these with no HF token at all — `meta-llama/Llama-3.2-1B` is
409
+ # gated and `llama3.2:1b` is not. Without that narrowing this reported both Llamas as unservable,
410
+ # which would have been a false refusal of two models that work today.
411
+ if src.get("gated") and not src.get("ollama_tag") and not hf_token_configured():
412
+ return (f"{model_id} is pinned to the GATED repo {src.get('hf_repo')!r} and no Hugging Face "
413
+ f"credential resolves, so the pull would answer 401 after the GPU is already billing. "
414
+ f"Accept the model's licence on Hugging Face and store an HF_TOKEN credential.")
415
+ return ""
416
+
417
+
418
+
419
+ #: ComfyUI models we have a REAL recipe for. Two shapes:
420
+ #: `file` — one checkpoint that exists inside the REGISTRY-PINNED repo (SDXL publishes one).
421
+ #: `repack` — the pinned repo is not ComfyUI-loadable, so the weights come from a repackaged
422
+ #: republication, named explicitly with its own revision. See the rows for why.
423
+ #: Keyed per model, NOT per runtime, and that distinction is load-bearing: an earlier version of
424
+ #: this keyed the branch on `runtime == comfyui` and so claimed a recipe for all 13 comfyui models
425
+ #: while hardcoding SDXL`s filename. The remaining rows have pinned sources but no checked file
426
+ #: layout, and a generic fetch would download nothing and serve nothing.
427
+ #:
428
+ #: ADD A ROW ONLY WITH EVERY PATH CHECKED AGAINST THE HF FILE TREE AT THAT EXACT REVISION — not
429
+ #: from memory of what these repos usually contain. The nine paths here were verified 200, with a
430
+ #: deliberately bogus path checked for 404 first to prove the check could fail at all.
431
+ _COMFYUI_MODELS = {
432
+ "sdxl-1.0": {"file": "sd_xl_base_1.0.safetensors", "vram_gb": 10.0},
433
+ # AUDIO GENERATION (sound/SFX, distinct from ACE-Step`s music). The pinned repo publishes a
434
+ # top-level single-file checkpoint that ComfyUI core loads, so this is the `file` shape and
435
+ # needs no repack — checked in the HF tree at the pinned revision.
436
+ #
437
+ # `as` EXISTS BECAUSE OF THIS ROW. The published name is literally `model.safetensors`, and
438
+ # the network volume is SHARED by every model in this table: dropped in as-is it would sit at
439
+ # models/checkpoints/model.safetensors, where the next model that publishes a generically
440
+ # named checkpoint silently overwrites it — and the `[ -s ]` guard would then treat the WRONG
441
+ # 4.9 GB file as already-downloaded and skip the fetch forever. A distinct basename is what
442
+ # makes the idempotence guard mean what it says.
443
+ #
444
+ # STILL GATED: the pull answers 401 without an accepted licence and an HF_TOKEN credential.
445
+ # That is deliberately NOT worked around here; `source_problem` reports it, and it reports it
446
+ # only now that a recipe exists, because a licence is useless for a model nothing can start.
447
+ "stable-audio-open-1.5": {"file": "model.safetensors", "vram_gb": 6.0,
448
+ "as": "stable_audio_open_1.0.safetensors"},
449
+ # THE OTHER THREE CANNOT USE `file`, AND THE REASON IS THE WHOLE POINT OF `repack`.
450
+ # Their registry pins are the ORIGINAL publications, and none of them is a single file
451
+ # ComfyUI can load (checked against the HF file tree at the pinned revision, not recalled):
452
+ # Qwen/Qwen-Image -> diffusers layout: model_index.json + transformer/ in
453
+ # 9 shards + text_encoder/ in 4. No single checkpoint.
454
+ # Wan-AI/Wan2.2-T2V-A14B -> original Wan layout: high_noise_model/ and
455
+ # low_noise_model/ in 6 shards each + an 11.4 GB .pth
456
+ # text encoder. ~115 GB, and not a ComfyUI checkpoint.
457
+ # ACE-Step/ACE-Step-v1-3.5B -> ace_step_transformer/ + umt5-base/, diffusers-style.
458
+ # So the REGISTRY PIN REMAINS THE PROVENANCE RECORD and `repack` names the ComfyUI-loadable
459
+ # re-publication of those same weights that the pod actually downloads. The served bytes are
460
+ # therefore NOT the pinned bytes, which is a real difference and is why it is a named field
461
+ # with its own revision rather than a quiet substitution inside the fetch string.
462
+ "qwen-image-20b": {"vram_gb": 24.0, "repack": {
463
+ "repo": "Comfy-Org/Qwen-Image_ComfyUI",
464
+ "revision": "1f12b17be14c89b026c51a91d67c32f84bb047bc",
465
+ # fp8 over bf16: 20.4 GB vs 40.9 GB, and the registry sizes this model at 24 GB VRAM —
466
+ # the bf16 weights alone would not fit the placement the catalog promises.
467
+ "files": [("split_files/diffusion_models/qwen_image_fp8_e4m3fn.safetensors",
468
+ "diffusion_models"),
469
+ ("split_files/text_encoders/qwen_2.5_vl_7b_fp8_scaled.safetensors",
470
+ "text_encoders"),
471
+ ("split_files/vae/qwen_image_vae.safetensors", "vae")]}},
472
+ "ace-step-1.5": {"vram_gb": 8.0, "repack": {
473
+ "repo": "Comfy-Org/ACE-Step_ComfyUI_repackaged",
474
+ "revision": "e39503e8265a02363b8c6d3fed3732944f4fc67f",
475
+ # The only all-in-one in this set: transformer + text encoder + VAE in one checkpoint,
476
+ # so it loads from models/checkpoints like SDXL does.
477
+ "files": [("all_in_one/ace_step_v1_3.5b.safetensors", "checkpoints")]}},
478
+ "wan-2.2": {"vram_gb": 24.0, "repack": {
479
+ "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
480
+ "revision": "ee6f4a40737a995bf5818954cfce6d59443b0f04",
481
+ # Wan 2.2 A14B is a TWO-EXPERT model: a high-noise expert for the early steps and a
482
+ # low-noise one for the late steps. BOTH are required — a graph with one of them is not
483
+ # this model — which is why two diffusion_models rows is the minimum, not a choice.
484
+ # fp8_scaled (14.3 GB each) over fp16 (28.6 GB each) for the same 24 GB reason as above;
485
+ # the two experts load in sequence, so peak VRAM is one expert, not both.
486
+ "files": [("split_files/diffusion_models/wan2.2_t2v_high_noise_14B_fp8_scaled.safetensors",
487
+ "diffusion_models"),
488
+ ("split_files/diffusion_models/wan2.2_t2v_low_noise_14B_fp8_scaled.safetensors",
489
+ "diffusion_models"),
490
+ ("split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors",
491
+ "text_encoders"),
492
+ ("split_files/vae/wan2.2_vae.safetensors", "vae")]}},
493
+ }
494
+
495
+ #: Disk the weights occupy on the network volume, per model, from the sizes in the HF file tree at
496
+ #: the revisions above. Recorded because the volume is a FIXED, DATACENTER-LOCKED size: the recipes
497
+ #: below total ~81 GB, so a 60 GB volume cannot hold all four and the bring-up fails LATE — after
498
+ #: the GPU is billing — with a disk-full error rather than a refusal. Callers that place these
499
+ #: models are expected to check this against the volume before renting anything.
500
+ COMFYUI_WEIGHT_GB = {
501
+ "sdxl-1.0": 6.94,
502
+ "stable-audio-open-1.5": 4.85,
503
+ "qwen-image-20b": 30.06, # 20.43 + 9.38 + 0.25
504
+ "ace-step-1.5": 7.70,
505
+ "wan-2.2": 36.73, # 14.29 + 14.29 + 6.74 + 1.41
506
+ }
507
+
508
+ def serving_spec(model_id: str) -> ServingSpec:
509
+ """Resolve the correct, deployable serving recipe for ANY catalog model. Falls back to an Ollama
510
+ recipe sized from the manifest VRAM for models without an explicit entry, so nothing is
511
+ unservable — the resolver always returns a working plan."""
512
+ if model_id in _FP8_MODELS:
513
+ m = _FP8_MODELS[model_id]
514
+ # Tensor parallelism splits attention heads across GPUs, so the degree has to divide the
515
+ # head count — in practice a power of two. A 3-GPU placement is cheaper on paper and
516
+ # then refuses to start, so it is not a placement at all.
517
+ plan = [c for c in gpu_plan(m["vram_gb"], require_fp8=True)
518
+ if c.count & (c.count - 1) == 0]
519
+ # Tensor-parallel degree must equal the GPU count of the placement we actually take,
520
+ # so it is resolved per config at deploy time; the cheapest fitting config sets it.
521
+ tp = max(int(m.get("min_gpus", 1)), plan[0].count if plan else 1)
522
+ parser = f" --reasoning-parser {m['reasoning_parser']}" if m.get("reasoning_parser") else ""
523
+ # FILE 11-B2. Appended rather than interleaved so the flags are readable in a log line, and
524
+ # EMPTY when this model has no method or no head — never a half-configured one.
525
+ spec = spec_decode_args(model_id, "sglang")
526
+ # REPO AND REVISION FROM THE REGISTRY (28 §B). `m['repo']` stays as the fallback for a model
527
+ # this dict knows and the registry does not, but the registry is the source of truth and
528
+ # carries the PIN — without `--revision` this pod served whatever `main` was that morning.
529
+ src = registry_source(model_id)
530
+ repo = src.get("hf_repo") or m["repo"]
531
+ rev = f" --revision {src['revision']}" if src.get("revision") else ""
532
+ args = (f"--model-path {repo}{rev} --host 0.0.0.0 --port 8080 "
533
+ f"--tp {tp} --context-length {m['ctx']}{parser} "
534
+ # SGLang spells the concurrency cap --max-running-requests; vLLM calls the same
535
+ # thing --max-num-seqs. Using the wrong name is silently ignored, which would
536
+ # leave the pod unbatched and put us straight back at $165-210/1M tokens.
537
+ f"--max-running-requests {TIER_S_MAX_SEQS} "
538
+ f"--kv-cache-dtype fp8_e5m2 --enable-hierarchical-cache"
539
+ + (f" {spec}" if spec else ""))
540
+ return ServingSpec(model_id, "sglang", SGLANG_IMAGE, 8080, repo, m["vram_gb"],
541
+ m["reasoning"], m["ctx"], args,
542
+ command=f"python3 -m sglang.launch_server {args}", gpu_configs=plan)
543
+ if model_id in _LLAMACPP_MODELS:
544
+ m = _LLAMACPP_MODELS[model_id]
545
+ # The GGUF repo and quant come from the registry where it has them. The revision is RECORDED
546
+ # there and deliberately NOT passed here: llama.cpp's `-hf` takes `repo:quant` and has no
547
+ # revision argument, so the pin documents what was tested rather than constraining the pull.
548
+ # Claiming otherwise with a flag the binary ignores would be worse than saying it plainly.
549
+ src = registry_source(model_id)
550
+ repo = src.get("gguf_repo") or m["repo"]
551
+ quant = src.get("gguf_quant") or m["quant"]
552
+ ref = f"{repo}:{quant}"
553
+ # --parallel 1 served ONE request at a time on the most expensive node we run. With
554
+ # continuous batching the same pod overlaps 8 streams, dividing the hourly cost.
555
+ args = (f"-hf {ref} --host 0.0.0.0 --port 8080 -ngl 999 -c {m['ctx']} --jinja "
556
+ f"--parallel {LLAMACPP_PARALLEL} --cont-batching")
557
+ return ServingSpec(model_id, "llamacpp", LLAMACPP_IMAGE, 8080, ref, m["vram_gb"],
558
+ m["reasoning"], m["ctx"], args,
559
+ # The image's ENTRYPOINT is llama-server itself, so the flags ARE the
560
+ # command — passing it again would run llama-server inside llama-server.
561
+ command=args, gpu_configs=gpu_plan(m["vram_gb"]))
562
+ # ComfyUI path: the diffusion models (image/video). 28 §B pins their weights; what was missing
563
+ # until now was a RECIPE, so `serving_spec` returned a spec with an EMPTY command and
564
+ # `SERVABLE_RUNTIMES` honestly excluded `comfyui`.
565
+ #
566
+ # WHY THE WEIGHTS GO TO THE NETWORK VOLUME AND NOT THE CONTAINER DISK. SDXL is ~7 GB; on the
567
+ # container disk it is re-downloaded on every boot and the pod bills the whole time it pulls.
568
+ # RunPod mounts a network volume at /workspace, which is also where this image keeps ComfyUI,
569
+ # so pointing the checkpoint dir there makes the download happen ONCE — the same trade
570
+ # `_volume_attachment` documents for the frontier model.
571
+ if model_id in _COMFYUI_MODELS:
572
+ m = _COMFYUI_MODELS[model_id]
573
+ src = registry_source(model_id)
574
+ repo, rev = src.get("hf_repo") or "", src.get("revision") or ""
575
+ if not repo:
576
+ log.warning("serving_spec(%s) has no runnable command: %s", model_id,
577
+ source_problem(model_id) or "no pinned source for comfyui")
578
+ return ServingSpec(model_id, "comfyui", COMFYUI_IMAGE, COMFYUI_PORT, "",
579
+ float(m["vram_gb"]), False, 0, "", command="",
580
+ gpu_configs=gpu_plan(float(m["vram_gb"])))
581
+ from .registry_gen import BY_ID as _REG_V
582
+ vram = float((_REG_V.get(model_id) or {}).get("vram_gb")
583
+ or (_BY_ID.get(model_id) or {}).get("vram_gb") or m["vram_gb"])
584
+ models_root = "/workspace/ComfyUI/models"
585
+ ckpt = f"{models_root}/checkpoints"
586
+ # WEIGHTS ON THE NETWORK VOLUME, not the container disk. RunPod mounts the volume at
587
+ # /workspace, so the weights are downloaded ONCE and every later boot is fast —
588
+ # the same trade `_volume_attachment` documents for the frontier model. The `[ -s ... ]`
589
+ # guard is what makes the second boot skip the download instead of repeating it.
590
+ pack = m.get("repack")
591
+ if pack:
592
+ # A REPACKAGED SOURCE, so `repo`/`rev` from the registry are NOT what we download —
593
+ # see the comment on `_COMFYUI_MODELS`. Each file is fetched into its own ComfyUI
594
+ # subfolder and renamed to its bare basename, because ComfyUI's loader nodes list
595
+ # `models/<kind>/` and would not see a file still nested under `split_files/`.
596
+ steps = []
597
+ for path, sub in pack["files"]:
598
+ base = path.rsplit("/", 1)[-1]
599
+ top = path.split("/", 1)[0]
600
+ d = f"{models_root}/{sub}"
601
+ # Download, lift the file out of the repo-relative path, drop the empty tree.
602
+ # `[ -s ... ]` first so a restart costs nothing.
603
+ steps.append(
604
+ f"mkdir -p {d} && ( [ -s {d}/{base} ] || ( "
605
+ f"huggingface-cli download {pack['repo']} {path}"
606
+ + (f" --revision {pack['revision']}" if pack.get("revision") else "")
607
+ + f" --local-dir {d} && mv -f {d}/{path} {d}/{base} && rm -rf {d}/{top} ) )")
608
+ fetch = " && ".join(steps)
609
+ else:
610
+ # `as` renames the checkpoint to a distinct basename; see the stable-audio row for
611
+ # why a generic published name is a correctness problem on a shared volume, not a
612
+ # cosmetic one. Without `as` the published name is kept (SDXL`s already distinct).
613
+ dest = m.get("as") or m["file"]
614
+ ren = f" && mv -f {ckpt}/{m['file']} {ckpt}/{dest}" if dest != m["file"] else ""
615
+ fetch = (f"mkdir -p {ckpt} && ( [ -s {ckpt}/{dest} ] || ( "
616
+ f"huggingface-cli download {repo} {m['file']}"
617
+ + (f" --revision {rev}" if rev else "")
618
+ + f" --local-dir {ckpt}{ren} ) )")
619
+ args = f"--listen 0.0.0.0 --port {COMFYUI_PORT}"
620
+ return ServingSpec(model_id, "comfyui", COMFYUI_IMAGE, COMFYUI_PORT, repo, vram,
621
+ False, 0, args,
622
+ command=f"bash -lc '{fetch}; exec python3 /opt/ComfyUI/main.py {args}'",
623
+ gpu_configs=gpu_plan(vram))
624
+ # Ollama path: single-file models, pulled by their PINNED TAG.
625
+ #
626
+ # THIS BRANCH USED TO RETURN AN UNSERVABLE SPEC AND SAY NOTHING. `model_ref` was `model_id` — the
627
+ # catalog id, e.g. `qwen3-8b`, which Ollama has never heard of; the real tag is `qwen3:8b` — and
628
+ # `command` was the empty string, so a pod built from it starts `ollama/ollama` with no server
629
+ # and no model: RUNNING with no runtime, billing until reaped. 25 of 29 enabled models reached
630
+ # this branch on 2026-10-01, which is the whole of 28 §B.
631
+ # VRAM FROM THE REGISTRY FIRST. `manifest._BY_ID` is the reduced hand-written catalog, so any model
632
+ # absent from it fell through to a FABRICATED 8.0 — and unfinished.md L13 names that as a money risk
633
+ # in as many words: "`serving_spec()` FABRICATES an 8 GB ollama spec for any unknown id ... so a
634
+ # provisioning script that trusts it rents an 8 GB pod for a 400 GB model". `mistral-large-3`
635
+ # (registry: 400 GB) reported 8.0. The empty command already stops a pod being created from it, but
636
+ # `vram_gb` and `gpu_configs` are read by other callers — `model_fit_error` among them — and an
637
+ # under-stated weight is the "bills and cannot start" shape.
638
+ from .registry_gen import BY_ID as _REG
639
+ reg = _REG.get(model_id) or {}
640
+ cat = _BY_ID.get(model_id)
641
+ vram = float(reg.get("vram_gb") or (cat or {}).get("vram_gb") or 8.0)
642
+ ctx = 16384
643
+ src = registry_source(model_id)
644
+ tag = str(src.get("ollama_tag") or "")
645
+ if not tag:
646
+ # NO GUESSING. `model_id` is not a tag, and a wrong tag is a pod that pulls nothing or pulls
647
+ # the wrong model. An empty command is returned ONLY here, where there is genuinely nothing
648
+ # to run — and the reason is LOGGED rather than left for the caller to discover by deploying.
649
+ #
650
+ # This is also `source_problem`'s production caller. Written without one it was a function the
651
+ # tests proved and nothing used (rule 12, caught by `test_no_orphan_implementations`), which is
652
+ # the same shape as the 25 unservable models: a truth recorded nowhere anybody reads.
653
+ log.warning("serving_spec(%s) has no runnable command: %s", model_id,
654
+ source_problem(model_id) or "no pinned source for its runtime")
655
+ return ServingSpec(model_id, "ollama", OLLAMA_IMAGE, 11434, "", max(vram, 1.0),
656
+ False, ctx, "", command="",
657
+ gpu_configs=gpu_plan(vram if vram >= 8 else 8))
658
+ # `ollama serve` is the foreground process and `ollama pull` needs it already listening, so the
659
+ # pull happens in a subshell that waits for the port rather than sleeping a guessed number of
660
+ # seconds. If the pull fails the container EXITS instead of serving an endpoint with no model
661
+ # behind it — the same choice the 11-B2 two-server pod makes, and for the same reason.
662
+ args = (f'(until ollama list >/dev/null 2>&1; do sleep 1; done; '
663
+ f'ollama pull {tag} || kill 1) & exec ollama serve')
664
+ return ServingSpec(model_id, "ollama", OLLAMA_IMAGE, 11434, tag, max(vram, 1.0),
665
+ False, ctx, args, command=f"bash -lc {_shquote(args)}",
666
+ gpu_configs=gpu_plan(vram if vram >= 8 else 8))
667
+
668
+
669
+ # `engine_for()` AND `vllm_args()` WERE DELETED HERE, 2026-09-23 (rule 12).
670
+ #
671
+ # Both were second spellings of a decision `serving_spec()` already owns, and this module's own
672
+ # docstring says it is "the single place that answers, for a given model id: which ENGINE serves
673
+ # it ... [and] the container IMAGE". Neither had a caller.
674
+ #
675
+ # engine_for(m) == serving_spec(m).engine, off the same two tables, in the same order. The
676
+ # one production consumer of this module — `gpuproviders.model_fit_error` — already holds a
677
+ # `ServingSpec` when it needs the engine, so a caller for `engine_for` could only ever have
678
+ # been a redundant second lookup that drifts the day the resolver grows a branch.
679
+ #
680
+ # vllm_args(m) was the 01-B1 fallback engine, and it DISAGREED with the primary path in
681
+ # three ways that each produce a pod that bills and cannot serve:
682
+ # * it emitted no `--reasoning-parser`, the flag `serving_spec` carries because a live pod
683
+ # on 2026-08-28 was MEASURED returning its <think> block inside `content` without it;
684
+ # * `tp` defaulted to 1, where `serving_spec` derives the tensor-parallel degree from the
685
+ # placement it actually takes (a 235 GB model on 2xH200 at --tp 1 does not load);
686
+ # * for a GGUF-only model it cheerfully produced vLLM args for an engine that cannot read
687
+ # GGUF at all.
688
+ # Nothing in the deployment serves vLLM: no entrypoint, no spec, and `VLLM_IMAGE` — which
689
+ # went with them — was referenced by nothing at all, not even by `vllm_args`. The fallback
690
+ # engine of 01-B1 is unbuilt, and a weaker copy of the args is not a head start on building
691
+ # it: the reasoning-parser flag above is the record of what shipping the weaker copy costs.