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/adminheader.py +59 -0
- frutebox/attest.py +43 -0
- frutebox/audiogen.py +364 -0
- frutebox/config.py +367 -0
- frutebox/deltasync.py +211 -0
- frutebox/economics.py +270 -0
- frutebox/engine_guard.py +718 -0
- frutebox/envelope.py +77 -0
- frutebox/envvars.py +167 -0
- frutebox/gpuprices.py +641 -0
- frutebox/gpuproviders.py +4489 -0
- frutebox/gui.py +270 -0
- frutebox/imagegen.py +152 -0
- frutebox/inference.py +624 -0
- frutebox/manifest.py +218 -0
- frutebox/mediaserving.py +603 -0
- frutebox/packaging.py +542 -0
- frutebox/pqctransport.py +660 -0
- frutebox/provider.py +1867 -0
- frutebox/providers.py +942 -0
- frutebox/registry/__init__.py +189 -0
- frutebox/registry/generate.py +311 -0
- frutebox/registry/generate_providers.py +120 -0
- frutebox/registry/models.yaml +1759 -0
- frutebox/registry/providers.yaml +518 -0
- frutebox/registry/registry.gen.json +2043 -0
- frutebox/registry_gen.py +3 -0
- frutebox/runpod_templates.py +143 -0
- frutebox/serving.py +691 -0
- frutebox/signedconfig.py +259 -0
- frutebox/signing.py +208 -0
- frutebox/thinkbudget.py +155 -0
- frutebox/videogen.py +118 -0
- frutebox_node-0.1.0.dist-info/METADATA +100 -0
- frutebox_node-0.1.0.dist-info/RECORD +36 -0
- frutebox_node-0.1.0.dist-info/WHEEL +4 -0
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.
|