runpod: an engines pod image — ds4, colibri, k3 compiled for every pod GPU

The local ds4-server is an sm_86 build against CUDA 13 and the local
colibri has no GPU backend: neither could be copied into a pod. The new
engines image compiles both, plus kimi-k3-in-c, in a CUDA 12.8 stage for
sm_80/86/89/90/120a (ds4's Blackwell kernels need the arch-specific
target; plain sm_120 fails in ptxas) on a portable x86-64-v3 baseline,
and fails the build if a binary cannot find its libraries.

A RunPod-only model names the engine on its runpod block (backend:
runpod takes the pin slot); a local model pinned to the engine and
bursting carries the pin. Either way the pod arrives with the engine
enabled and this install's settings forwarded (CODERAI_<ENG>_CONFIG),
and volume_path points it at weights already on the network volume —
the only sane way to run 150 GB+ models. The model page warns loudly
the moment one of these engines is picked. ktransformers is refused
with a reason: it has no pod image.

Also: the batch build/publish scripts no longer list core-light and
venv profiles as images.
Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0182DhR1eFNPQyGmDrpHbedY
parent 362ff475
...@@ -55,3 +55,6 @@ tools/coderai_media/ ...@@ -55,3 +55,6 @@ tools/coderai_media/
.oci-*.log .oci-*.log
venv_build.log venv_build.log
.build.pid .build.pid
# staged engine sources for the engines pod image (build_capability_image.sh)
packaging/runpod/engines-src/
...@@ -97,9 +97,19 @@ Plus: ...@@ -97,9 +97,19 @@ Plus:
each carrying its own torch on a torch-free core so it stays under RunPod's image size each carrying its own torch on a torch-free core so it stays under RunPod's image size
limit. Fourteen have completed a real inference on a rented GPU; `faceswap` is the one limit. Fourteen have completed a real inference on a rented GPU; `faceswap` is the one
that needs real faces to test that needs real faces to test
- **The native MoE engines on rented GPUs**: an `engines` image compiles ds4, colibri and
kimi-k3-in-c for every pod GPU generation (Ampere → Blackwell) rather than copying this
machine's builds; a model names its engine on its runpod block and the pod arrives with
it enabled and configured. ⚠ Their weights are 100 GB–1.5 TB: put them on a network
volume (`volume_path`) or every cold pod downloads them — the UI warns, loudly
- **Multi-GPU pods**: `gpu_count` rents one machine with N cards, priced and VRAM-checked as
a whole; vLLM and llama.cpp are told to shard across them
- **Or a machine you already have**: `"backend": "host"` points a model at a capability - **Or a machine you already have**: `"backend": "host"` points a model at a capability
image running anywhere — always-on, or started by a command and stopped after idle — image running anywhere — always-on, or started by a command and stopped after idle —
with none of the renting machinery with none of the renting machinery. Both are one choice on the model page: *Runs on →
this machine / RunPod only / a machine of yours*
- **Visible while it runs**: every request sent to a pod or host is a row on the Tasks page
naming the machine it landed on, and `/admin/runpod` lists the live pods
Full guide: [`docs/runpod.md`](docs/runpod.md) · Full guide: [`docs/runpod.md`](docs/runpod.md) ·
remote placement of every model type, the capability images and the `host` backend: remote placement of every model type, the capability images and the `host` backend:
......
...@@ -2795,7 +2795,7 @@ async def api_model_configure(request: Request, username: str = Depends(require_ ...@@ -2795,7 +2795,7 @@ async def api_model_configure(request: Request, username: str = Depends(require_
"registry_auth_id", "api_key", "pool", "registry_auth_id", "api_key", "pool",
# where the POD gets the weights # where the POD gets the weights
"source", "hf_repo", "model_url", "quantization", "source", "hf_repo", "model_url", "quantization",
"network_volume_id", "volume_mount_path", "network_volume_id", "volume_mount_path", "volume_path",
"venv_name", "slim_image"): "venv_name", "slim_image"):
v = src.get(k) v = src.get(k)
if isinstance(v, str) and v.strip(): if isinstance(v, str) and v.strip():
......
...@@ -786,9 +786,15 @@ window.__DEFAULT_WHISPER_SERVER_PATH__ = {{ default_whisper_server_path|tojson } ...@@ -786,9 +786,15 @@ window.__DEFAULT_WHISPER_SERVER_PATH__ = {{ default_whisper_server_path|tojson }
<option value="vllm">vLLM (HF repo)</option> <option value="vllm">vLLM (HF repo)</option>
<option value="llamacpp">llama.cpp (GGUF)</option> <option value="llamacpp">llama.cpp (GGUF)</option>
<option value="coderai">coderai (a capability image)</option> <option value="coderai">coderai (a capability image)</option>
<option value="ds4">ds4 — DeepSeek-V4 (engines image)</option>
<option value="colibri">colibri — GLM-5.2 / DeepSeek-V4 / Kimi-K3 (engines image)</option>
<option value="k3">k3 — Kimi-K3 (engines image)</option>
<option value="custom">Custom image</option> <option value="custom">Custom image</option>
</select> </select>
<span class="form-hint" id="cfg-rp-engine-hint" style="font-size:11px"></span> <span class="form-hint" id="cfg-rp-engine-hint" style="font-size:11px"></span>
<div id="cfg-rp-engine-warn" style="display:none;margin-top:.4rem;padding:.5rem .6rem;border:1px solid #c9a227;border-radius:6px;background:rgba(201,162,39,.12);font-size:12px;line-height:1.4">
<b>⚠ Frontier-size weights.</b> Models on this engine are 100 GB and up — DeepSeek-V4 alone is ~154 GB, Kimi-K3 is measured in terabytes. Without a network volume <b>every cold pod downloads all of it</b> before answering, typically one to two hours of rental per boot. Set a network volume and, better, the "weights already on the volume" path so nothing is downloaded at all. The pod's disk is sized from <i>Weights (GB)</i> when no volume is set.
</div>
</div> </div>
<div class="form-row" style="margin:0"> <div class="form-row" style="margin:0">
<label class="form-label">GGUF to serve <span class="muted">(llama.cpp pods)</span></label> <label class="form-label">GGUF to serve <span class="muted">(llama.cpp pods)</span></label>
...@@ -822,6 +828,11 @@ window.__DEFAULT_WHISPER_SERVER_PATH__ = {{ default_whisper_server_path|tojson } ...@@ -822,6 +828,11 @@ window.__DEFAULT_WHISPER_SERVER_PATH__ = {{ default_whisper_server_path|tojson }
<input type="text" id="cfg-rp-volume" class="form-input" placeholder="blank = the account default, or none"> <input type="text" id="cfg-rp-volume" class="form-input" placeholder="blank = the account default, or none">
<span class="form-hint" style="font-size:11px">Weights, uploads and adapters land here and outlive the pod. Secure Cloud only; pods are pinned to the volume's region.</span> <span class="form-hint" style="font-size:11px">Weights, uploads and adapters land here and outlive the pod. Secure Cloud only; pods are pinned to the volume's region.</span>
</div> </div>
<div class="form-row" style="margin:0">
<label class="form-label">Weights already on the volume <span class="muted">(path)</span></label>
<input type="text" id="cfg-rp-volume-path" class="form-input" placeholder="models/DeepSeek-V4-Q4.gguf, or a container dir">
<span class="form-hint" style="font-size:11px">Relative to the volume mount. The pod loads this instead of downloading — the only sane way to run a 150 GB+ engine model (ds4, colibri, k3).</span>
</div>
<div class="form-row" style="margin:0;align-self:start"> <div class="form-row" style="margin:0;align-self:start">
<label style="display:flex;align-items:center;gap:.3rem;font-size:12px;margin-top:1.4rem"><input type="checkbox" id="cfg-rp-venv-volume" style="width:auto"> Keep dependencies on the volume</label> <label style="display:flex;align-items:center;gap:.3rem;font-size:12px;margin-top:1.4rem"><input type="checkbox" id="cfg-rp-venv-volume" style="width:auto"> Keep dependencies on the volume</label>
<span class="form-hint" style="font-size:11px">Boots a small image and runs from a venv on the volume — the first pod builds it, later pods skip the 7 GB pull. Needs a volume; imports are slower off network storage.</span> <span class="form-hint" style="font-size:11px">Boots a small image and runs from a venv on the volume — the first pod builds it, later pods skip the 7 GB pull. Needs a volume; imports are slower off network storage.</span>
...@@ -4290,6 +4301,7 @@ function _populateRunpod(s){ ...@@ -4290,6 +4301,7 @@ function _populateRunpod(s){
set('cfg-rp-hf-gguf', rp.hf_gguf || ''); set('cfg-rp-hf-gguf', rp.hf_gguf || '');
set('cfg-rp-pool', rp.pool || ''); set('cfg-rp-pool', rp.pool || '');
set('cfg-rp-volume', rp.network_volume_id || ''); set('cfg-rp-volume', rp.network_volume_id || '');
set('cfg-rp-volume-path', rp.volume_path || '');
chk('cfg-rp-venv-volume', rp.venv_on_volume); chk('cfg-rp-venv-volume', rp.venv_on_volume);
set('cfg-rp-source', rp.source || 'auto'); set('cfg-rp-source', rp.source || 'auto');
set('cfg-rp-hf-repo', rp.hf_repo || ''); set('cfg-rp-hf-repo', rp.hf_repo || '');
...@@ -4383,9 +4395,14 @@ function _updateRunpodEngineHint(){ ...@@ -4383,9 +4395,14 @@ function _updateRunpodEngineHint(){
vllm: 'Serves an HF repo. vLLM\'s GGUF support is experimental — use llama.cpp for those.', vllm: 'Serves an HF repo. vLLM\'s GGUF support is experimental — use llama.cpp for those.',
llamacpp: 'Serves the GGUF named on the right; the pod downloads it itself.', llamacpp: 'Serves the GGUF named on the right; the pod downloads it itself.',
coderai: 'A whole coderai serving one capability. Needs a pod image you have pushed.', coderai: 'A whole coderai serving one capability. Needs a pod image you have pushed.',
ds4: 'ds4-server on the engines image, built for every pod GPU. Downloads the configured variant unless the weights are on the volume.',
colibri: 'colibri on the engines image (GLM-5.2 on CUDA; DeepSeek-V4 and Kimi-K3 on CPU). Never downloads: the container dir must be on the volume.',
k3: 'kimi-k3-in-c on the engines image (CPU). Never downloads: the checkpoint must be on the volume.',
custom: 'Your image, your docker args, your health path.', custom: 'Your image, your docker args, your health path.',
}; };
hint.textContent = HINTS[el.value] || ''; hint.textContent = HINTS[el.value] || '';
const warn = document.getElementById('cfg-rp-engine-warn');
if (warn) warn.style.display = (el.value === 'ds4' || el.value === 'colibri' || el.value === 'k3') ? '' : 'none';
} }
// Build the per-model spillover block. Undefined when disabled, so the entry // Build the per-model spillover block. Undefined when disabled, so the entry
...@@ -4461,6 +4478,7 @@ function _collectRunpod(){ ...@@ -4461,6 +4478,7 @@ function _collectRunpod(){
hf_gguf: (val('cfg-rp-hf-gguf') || '').trim(), hf_gguf: (val('cfg-rp-hf-gguf') || '').trim(),
pool: (val('cfg-rp-pool') || '').trim(), pool: (val('cfg-rp-pool') || '').trim(),
network_volume_id: (val('cfg-rp-volume') || '').trim(), network_volume_id: (val('cfg-rp-volume') || '').trim(),
volume_path: (val('cfg-rp-volume-path') || '').trim(),
venv_on_volume: document.getElementById('cfg-rp-venv-volume').checked, venv_on_volume: document.getElementById('cfg-rp-venv-volume').checked,
source: val('cfg-rp-source') || 'auto', source: val('cfg-rp-source') || 'auto',
hf_repo: (val('cfg-rp-hf-repo') || '').trim(), hf_repo: (val('cfg-rp-hf-repo') || '').trim(),
...@@ -4821,7 +4839,8 @@ async function testRunModel(){ ...@@ -4821,7 +4839,8 @@ async function testRunModel(){
if (d.ran === 'reachability') { if (d.ran === 'reachability') {
show(`${d.model}\nwhere: ${d.where} (${d.target})\n${d.note}\n${d.detail || ''}`, 'busy'); show(`${d.model}\nwhere: ${d.where} (${d.target})\n${d.note}\n${d.detail || ''}`, 'busy');
} else if (d.ok) { } else if (d.ok) {
show(`OK — ${d.ran} in ${d.seconds}s\nwhere: ${d.where} (${d.target})\n` show((d.warning ? `⚠ ${d.warning}\n\n` : '')
+ `OK — ${d.ran} in ${d.seconds}s\nwhere: ${d.where} (${d.target})\n`
+ `reply: ${d.sample || '(no sample)'}`, 'ok'); + `reply: ${d.sample || '(no sample)'}`, 'ok');
} else { } else {
show(`FAILED — ${d.ran || ''} ${d.status || ''} after ${d.seconds || '?'}s\n` show(`FAILED — ${d.ran || ''} ${d.status || ''} after ${d.seconds || '?'}s\n`
......
...@@ -70,6 +70,7 @@ class RunpodModelConfig: ...@@ -70,6 +70,7 @@ class RunpodModelConfig:
# account-wide one. Weights, uploads and adapters on it outlive the pod. # account-wide one. Weights, uploads and adapters on it outlive the pod.
network_volume_id: str = "" network_volume_id: str = ""
volume_mount_path: str = "" # blank = the account default volume_mount_path: str = "" # blank = the account default
volume_path: str = "" # weights already on the volume (file or dir)
# Keep this pod's Python dependencies on the VOLUME instead of in the image, # Keep this pod's Python dependencies on the VOLUME instead of in the image,
# and boot a small image that uses them. The first pod builds the venv (a few # and boot a small image that uses them. The first pod builds the venv (a few
# minutes); every pod after skips both the 7 GB image pull and the install. # minutes); every pod after skips both the 7 GB image pull and the install.
...@@ -203,8 +204,19 @@ PUBLISHED_CAPABILITY_IMAGES = ( ...@@ -203,8 +204,19 @@ PUBLISHED_CAPABILITY_IMAGES = (
# SENT a local LoRA adapter, which is the only way a local text adapter runs # SENT a local LoRA adapter, which is the only way a local text adapter runs
# on RunPod. Build and push it with packaging/runpod before using it. # on RunPod. Build and push it with packaging/runpod before using it.
"text", "text",
# The native MoE engines — ds4, colibri, kimi-k3-in-c — compiled for every
# pod GPU generation rather than copied from this machine (whose ds4 is an
# sm_86 build against CUDA 13, and whose colibri has no GPU backend). Picked
# by a model's `backend`, not its section: it is still a text model.
"engines",
) )
#: Model backends served by the engines image. `kt` (ktransformers) is NOT here:
#: it is a Python stack (kt-kernel + SGLang) built for AMX CPUs, several GB in
#: its own venv, and has never been built for a pod — a model pinned to it is
#: refused with that reason rather than sent to an image that cannot run it.
_ENGINE_POD_BACKENDS = ("ds4", "colibri", "k3")
#: Capabilities served by a published image other than their own name. #: Capabilities served by a published image other than their own name.
_CAPABILITY_IMAGE_ALIASES = { _CAPABILITY_IMAGE_ALIASES = {
"rerank": "embeddings", # same sentence-transformers stack "rerank": "embeddings", # same sentence-transformers stack
...@@ -355,6 +367,13 @@ def resolve_pod_engine(mcfg: "RunpodModelConfig", model_key: str = "", ...@@ -355,6 +367,13 @@ def resolve_pod_engine(mcfg: "RunpodModelConfig", model_key: str = "",
vLLM. vLLM.
""" """
want = (mcfg.engine or "auto").strip().lower() want = (mcfg.engine or "auto").strip().lower()
if "kt" in (want, str((entry or {}).get("backend") or "").strip().lower()):
raise RuntimeError(
"RunPod: this model is pinned to ktransformers, which has no pod "
"image — it is a CPU-AMX Python stack (kt-kernel + SGLang) that "
"has never been built for a rented machine. Serve it from a host "
"you own (backend: host), or pin the model to ds4/colibri/k3, "
"which the engines image carries.")
if want in ("vllm", "llamacpp", "coderai", "custom"): if want in ("vllm", "llamacpp", "coderai", "custom"):
return want return want
# A non-text model is served by a whole coderai carrying that capability. # A non-text model is served by a whole coderai carrying that capability.
...@@ -362,6 +381,11 @@ def resolve_pod_engine(mcfg: "RunpodModelConfig", model_key: str = "", ...@@ -362,6 +381,11 @@ def resolve_pod_engine(mcfg: "RunpodModelConfig", model_key: str = "",
entry = _model_entry(model_key) entry = _model_entry(model_key)
if model_capability(entry or {}): if model_capability(entry or {}):
return "coderai" return "coderai"
# A model pinned to a native engine is served by a coderai carrying that
# engine: vLLM cannot load a DeepSeek-V4 GGUF laid out for ds4, and there
# is no colibri outside of coderai.
if engine_pod_backend(entry or {}, mcfg):
return "coderai"
# A text model whose LoRA lives only on this disk has one option: a coderai # A text model whose LoRA lives only on this disk has one option: a coderai
# pod, which can be sent the adapter. vLLM and llama.cpp resolve adapters # pod, which can be sent the adapter. vLLM and llama.cpp resolve adapters
# themselves at launch and have no endpoint to receive one. # themselves at launch and have no endpoint to receive one.
...@@ -383,6 +407,22 @@ def resolve_pod_engine(mcfg: "RunpodModelConfig", model_key: str = "", ...@@ -383,6 +407,22 @@ def resolve_pod_engine(mcfg: "RunpodModelConfig", model_key: str = "",
return "vllm" return "vllm"
def engine_pod_backend(entry: dict, mcfg: "RunpodModelConfig" = None) -> str:
"""The native engine a pod must run for this model, or ''.
Two ways to say it. A RunPod-only model has `backend: runpod` — that slot
is taken — so it names the engine on its runpod block (`engine: ds4`),
beside vllm and llamacpp. A LOCAL model pinned to `backend: ds4` that
bursts to RunPod carries the pin itself. Either way the answer is the
same image and the same seed.
"""
want = str(getattr(mcfg, "engine", "") or "").strip().lower() if mcfg else ""
if want in _ENGINE_POD_BACKENDS:
return want
b = str((entry or {}).get("backend") or "").strip().lower()
return b if b in _ENGINE_POD_BACKENDS else ""
#: Readiness probe per engine. A coderai pod answers /healthz long before any #: Readiness probe per engine. A coderai pod answers /healthz long before any
#: model is loaded, which is exactly what we want: it is ready to be asked. #: model is loaded, which is exactly what we want: it is ready to be asked.
_HEALTH_PATHS = {"vllm": "/v1/models", "llamacpp": "/v1/models", _HEALTH_PATHS = {"vllm": "/v1/models", "llamacpp": "/v1/models",
...@@ -411,6 +451,8 @@ def pod_plan(mcfg: "RunpodModelConfig", served: str, model_key: str = "", ...@@ -411,6 +451,8 @@ def pod_plan(mcfg: "RunpodModelConfig", served: str, model_key: str = "",
# everything with "not available". # everything with "not available".
image = mcfg.image image = mcfg.image
capability_hint = model_capability(entry or {}, include_text=True) capability_hint = model_capability(entry or {}, include_text=True)
if not image and engine_pod_backend(entry or {}, mcfg):
image = default_capability_image("engines")
if not image: if not image:
image = default_capability_image(capability_hint) if capability_hint else "" image = default_capability_image(capability_hint) if capability_hint else ""
if not image: if not image:
...@@ -687,6 +729,61 @@ def venv_bootstrap_script(mount: str, profile: str, venv_name: str = "", ...@@ -687,6 +729,61 @@ def venv_bootstrap_script(mount: str, profile: str, venv_name: str = "",
]) ])
#: Engine settings that describe the MODEL and how to run it, and so travel to
#: a pod. Not in the list, deliberately: enabled (the pod gets its own switch),
#: install_dir / repo_url / auto_build (the image has the binary), host / port /
#: service_url (this machine's wiring), model_path (a local file).
_ENGINE_FORWARDED_FIELDS = {
"ds4": ("ctx", "ssd_streaming", "extra_args", "extra_env", "model_variant",
"auto_download", "expert_cache_reserve_gb", "model_id"),
"colibri": ("ctx", "kv_slots", "cap", "cuda_expert_gb", "extra_args",
"extra_env", "model_id"),
"k3": ("ctx", "preset", "trunk_gb", "cache_gb", "extra_args", "extra_env",
"model_id"),
}
def engine_config_for_pod(engine: str) -> dict:
"""This install's settings for a native engine, minus what is local to it."""
try:
from codai.admin.routes import config_manager
cfg = getattr(getattr(config_manager, "config", None), engine, None)
except Exception:
cfg = None
if cfg is None:
return {}
out = {}
for k in _ENGINE_FORWARDED_FIELDS.get(engine, ()):
if not hasattr(cfg, k):
continue
v = getattr(cfg, k)
if v in (None, "", 0, False):
continue
out[k] = v
if engine == "ds4" and "auto_download" not in out:
# A pod that cannot fetch its own weights is a pod that answers
# nothing. Locally this is opt-in because the download is 154 GB onto
# someone's disk; on a pod the disk was rented for exactly that.
out["auto_download"] = True
return out
def pod_volume_path(mcfg: "RunpodModelConfig", account=None) -> str:
"""Where the weights already are ON THE POD, or ''.
`volume_path` on the runpod block names a file or directory on the network
volume — relative to its mount, or absolute. It is how a 150 GB engine
model is used without any pod ever downloading it: put it there once.
"""
rel = str(getattr(mcfg, "volume_path", "") or "").strip()
if not rel:
return ""
if rel.startswith("/"):
return rel
_vol, mount = volume_for(mcfg, account)
return f"{mount.rstrip('/')}/{rel}"
def volume_env(mount: str) -> dict: def volume_env(mount: str) -> dict:
"""Point everything that writes big files at the volume. """Point everything that writes big files at the volume.
...@@ -706,6 +803,10 @@ def volume_env(mount: str) -> dict: ...@@ -706,6 +803,10 @@ def volume_env(mount: str) -> dict:
"DIFFUSERS_CACHE": f"{mount}/diffusers", "DIFFUSERS_CACHE": f"{mount}/diffusers",
"CODERAI_MODELS_DIR": f"{mount}/models", "CODERAI_MODELS_DIR": f"{mount}/models",
"CODERAI_CACHE_DIR": f"{mount}/cache", "CODERAI_CACHE_DIR": f"{mount}/cache",
# ds4's download_model.sh writes here; on the volume it survives the
# pod, and the move into the GGUF cache beside it is a rename rather
# than a 154 GB copy.
"DS4_GGUF_DIR": f"{mount}/cache/ds4",
} }
...@@ -743,7 +844,9 @@ def _plan(engine, image, args, mcfg, api_key, entry, served, ...@@ -743,7 +844,9 @@ def _plan(engine, image, args, mcfg, api_key, entry, served,
seeds = [] seeds = []
if entry: if entry:
kind, value = resolve_model_source(entry, mcfg) kind, value = resolve_model_source(entry, mcfg)
one = seed_model_env(entry, served, value if kind in ("hf", "url") else "") one = seed_model_env(entry, served, value if kind in ("hf", "url") else "",
volume_path=pod_volume_path(mcfg, _account_hint()),
engine=engine_pod_backend(entry, mcfg))
if one: if one:
seeds.extend(_json.loads(one)) seeds.extend(_json.loads(one))
for extra in (seed_entries or []): for extra in (seed_entries or []):
...@@ -836,6 +939,20 @@ def _plan(engine, image, args, mcfg, api_key, entry, served, ...@@ -836,6 +939,20 @@ def _plan(engine, image, args, mcfg, api_key, entry, served,
# for surya by name is that decision. # for surya by name is that decision.
env["CODERAI_OCR_SURYA_ACCEPT_LICENSE"] = "1" env["CODERAI_OCR_SURYA_ACCEPT_LICENSE"] = "1"
eng = engine_pod_backend(entry or {}, mcfg)
if eng:
# The engine ships disabled, like OCR does, and its settings live in
# THIS install's config.json (ctx, expert cache, extra args, download
# variant). The pod gets the switch and the settings as environment —
# never its install_dir or ports, which describe this machine.
env[f"CODERAI_{eng.upper()}_ENABLED"] = "1"
forwarded = engine_config_for_pod(eng)
if forwarded:
env[f"CODERAI_{eng.upper()}_CONFIG"] = _json.dumps(forwarded)
warn = weight_transfer_warning(entry, mcfg, _account_hint())
if warn:
print(f"[runpod] WARNING: {warn}", flush=True)
plan = {"engine": engine, "image": image, "args": args, "env": env, plan = {"engine": engine, "image": image, "args": args, "env": env,
"health_path": mcfg.health_path or _HEALTH_PATHS.get(engine, "/healthz")} "health_path": mcfg.health_path or _HEALTH_PATHS.get(engine, "/healthz")}
...@@ -869,7 +986,8 @@ def _account_hint(): ...@@ -869,7 +986,8 @@ def _account_hint():
return None return None
def seed_model_env(entry: dict, served: str = "", source: str = "") -> str: def seed_model_env(entry: dict, served: str = "", source: str = "",
volume_path: str = "", engine: str = "") -> str:
"""The model entry to register on a fresh pod, as JSON for CODERAI_SEED_MODELS. """The model entry to register on a fresh pod, as JSON for CODERAI_SEED_MODELS.
A pod starts with an EMPTY catalogue: it would refuse a request for a model A pod starts with an EMPTY catalogue: it would refuse a request for a model
...@@ -887,12 +1005,25 @@ def seed_model_env(entry: dict, served: str = "", source: str = "") -> str: ...@@ -887,12 +1005,25 @@ def seed_model_env(entry: dict, served: str = "", source: str = "") -> str:
# Give the pod something it can actually fetch: a repo id it pulls from # Give the pod something it can actually fetch: a repo id it pulls from
# HuggingFace, or a URL it downloads (coderai's loader takes both). # HuggingFace, or a URL it downloads (coderai's loader takes both).
path = source path = source
elif volume_path:
path = volume_path
elif (engine or engine_pod_backend(entry)) and path:
# An engine model named by a local file: the basename travels. ds4
# resolves it against the pod's GGUF cache — where its own download
# lands, or where a volume already holds it — and colibri/k3 say
# plainly that they never download when nothing is there.
path = path.rsplit("/", 1)[-1]
elif not path or path.startswith("/"): elif not path or path.startswith("/"):
# A local path cannot be resolved on a rented machine, and nothing said # A local path cannot be resolved on a rented machine, and nothing said
# where else to get it. # where else to get it.
return "" return ""
keep = ("path", "model_type", "model_types", "video_subtypes", "capabilities", keep = ("path", "model_type", "model_types", "video_subtypes", "capabilities",
"alias", "config_name", "load_in_4bit", "load_in_8bit", "n_ctx", "alias", "config_name", "load_in_4bit", "load_in_8bit", "n_ctx",
# The native engine a model is pinned to, and its per-model tuning
# block — what makes the pod run ds4-server for it rather than try
# to load a 154 GB GGUF with transformers. Other backend pins are
# stripped below: vulkan/opencl say something about THIS machine.
"backend", "ds4", "colibri", "k3",
"flash_attention", "model_template", "acceleration", "component_quantization", "flash_attention", "model_template", "acceleration", "component_quantization",
"languages", "supports_translation", "parser", "max_instances", "languages", "supports_translation", "parser", "max_instances",
# LoRA settings travel too: a coderai pod applies them itself, and # LoRA settings travel too: a coderai pod applies them itself, and
...@@ -908,6 +1039,20 @@ def seed_model_env(entry: dict, served: str = "", source: str = "") -> str: ...@@ -908,6 +1039,20 @@ def seed_model_env(entry: dict, served: str = "", source: str = "") -> str:
) )
out = {k: entry[k] for k in keep if k in entry and entry[k] is not None} out = {k: entry[k] for k in keep if k in entry and entry[k] is not None}
out["path"] = path out["path"] = path
if engine:
# The pod runs this native engine for the model, whichever way it was
# said here (the runpod block's engine, or the model's own pin).
out["backend"] = engine
elif "backend" in out and not engine_pod_backend(out):
# runpod, host, vulkan, nvidia…: placement and hardware of THIS install.
# On the pod they would mean "rent a pod" or "use a card it lacks".
out.pop("backend", None)
if volume_path:
# The weights already sit on the network volume, put there once by
# hand or by an earlier pod. The pod's engine resolves this path the
# way it resolves a local one here: a directory for colibri/k3, a
# .gguf for ds4. No download, no size check, no surprise.
out["path"] = volume_path
# Adapters the pod cannot resolve by name are named by CONTENT id instead. # Adapters the pod cannot resolve by name are named by CONTENT id instead.
# The hash is computed here, so the pod is told the id when it is created and # The hash is computed here, so the pod is told the id when it is created and
# sent the bytes before the first request — the two agree without the pod # sent the bytes before the first request — the two agree without the pod
...@@ -1038,6 +1183,7 @@ def parse_model_runpod(block: Optional[dict]) -> RunpodModelConfig: ...@@ -1038,6 +1183,7 @@ def parse_model_runpod(block: Optional[dict]) -> RunpodModelConfig:
# in the list just produces candidates the attach would reject. # in the list just produces candidates the attach would reject.
cfg.cloud_types = ["SECURE"] cfg.cloud_types = ["SECURE"]
cfg.volume_mount_path = (b.get("volume_mount_path") or "").strip() cfg.volume_mount_path = (b.get("volume_mount_path") or "").strip()
cfg.volume_path = (b.get("volume_path") or "").strip()
cfg.venv_on_volume = _as_bool(b.get("venv_on_volume"), cfg.venv_on_volume) cfg.venv_on_volume = _as_bool(b.get("venv_on_volume"), cfg.venv_on_volume)
cfg.venv_name = (b.get("venv_name") or "").strip() cfg.venv_name = (b.get("venv_name") or "").strip()
cfg.slim_image = (b.get("slim_image") or "").strip() cfg.slim_image = (b.get("slim_image") or "").strip()
......
...@@ -1358,6 +1358,40 @@ class ConfigManager: ...@@ -1358,6 +1358,40 @@ class ConfigManager:
# inherits the decision made here; it cannot make it itself. # inherits the decision made here; it cannot make it itself.
self.config.ocr.surya_accept_license = True self.config.ocr.surya_accept_license = True
# The native engines — ds4, colibri, k3 — ship disabled for the same
# reason OCR does, and an engines pod is rented to run exactly one of
# them. CODERAI_<ENGINE>_ENABLED switches it on; CODERAI_<ENGINE>_CONFIG
# carries the renting install's settings for it (context, expert
# cache, download variant…) as JSON, applied field by field onto the
# dataclass — only fields it already has, so a stray key cannot plant
# anything. Where the binary lives is the image's business
# (CODERAI_<ENGINE>_DIR, read by the worker itself), not this block's.
for name in ("ds4", "colibri", "k3"):
if not _flag(f"CODERAI_{name.upper()}_ENABLED"):
continue
eng = getattr(self.config, name, None)
if eng is None:
continue
eng.enabled = True
raw = (_os.environ.get(f"CODERAI_{name.upper()}_CONFIG") or "").strip()
if not raw:
continue
try:
forwarded = json.loads(raw)
except Exception as exc:
print(f"[seed] CODERAI_{name.upper()}_CONFIG is not valid JSON: {exc}",
flush=True)
continue
if not isinstance(forwarded, dict):
continue
for k, v in forwarded.items():
if k in ("enabled", "install_dir", "repo_url", "service_url",
"host", "port", "model_path", "auto_build"):
continue # local to whichever machine set them
if hasattr(eng, k):
setattr(eng, k, v)
print(f"[seed] {name} enabled with {sorted(forwarded)}", flush=True)
def _seed_models_from_env(self) -> None: def _seed_models_from_env(self) -> None:
"""Register models named by CODERAI_SEED_MODELS (JSON list of entries). """Register models named by CODERAI_SEED_MODELS (JSON list of entries).
......
...@@ -654,6 +654,10 @@ volume at once can corrupt it — coderai's writes are write-once (content-addre ...@@ -654,6 +654,10 @@ volume at once can corrupt it — coderai's writes are write-once (content-addre
adapter blobs, named model files), but keep that in mind before pointing many adapter blobs, named model files), but keep that in mind before pointing many
pods at one volume for anything else. pods at one volume for anything else.
**Engine models live here or nowhere.** DeepSeek-V4 on ds4 is ~154 GB, Kimi-K3
is terabytes; the engines pod image loads them from `volume_path` on the volume
rather than downloading. See "Engine pods" in `docs/runpod.md` — and its warning.
#### Dependencies on the volume, small image in the registry #### Dependencies on the volume, small image in the registry
A capability image is ~7 GB and nearly all of it is torch. With a volume those A capability image is ~7 GB and nearly all of it is torch. With a volume those
......
...@@ -173,6 +173,73 @@ entirely — CoderAI logs `Model 'X' is RunPod-served — skipping local downloa ...@@ -173,6 +173,73 @@ entirely — CoderAI logs `Model 'X' is RunPod-served — skipping local downloa
and never fetches weights. So a RunPod model can be configured, enabled and served and never fetches weights. So a RunPod model can be configured, enabled and served
on a machine that has neither the disk space nor the GPU for it. on a machine that has neither the disk space nor the GPU for it.
### Engine pods — ds4, colibri, k3 on a rented GPU
> ⚠ **The weights are the cost, not the GPU.** Models on these engines are
> 100 GB and up — DeepSeek-V4 is ~154 GB, Kimi-K3 is measured in terabytes.
> A pod with **no network volume downloads all of it on every cold start**,
> which is typically one to two hours of rental per boot, before it answers
> anything. The model page shows this warning the moment you pick one of these
> engines; the provisioning log repeats it; the test run reports it. Attach a
> network volume, and put the weights on it once.
The native MoE engines run on RunPod through the **engines image**
(`ghcr.io/nextime/coderai-engines`), a coderai pod that carries `ds4-server`,
colibri's `colibri` / `deepseek_v4` / `kimi_k3` and kimi-k3-in-c's `k3`. They
are **compiled in the image**, not copied from your machine: the local
`ds4-server` is built for the one card here (sm_86) against the host's CUDA 13,
and the local colibri has no GPU backend at all — neither would run on an A100.
The image builds them in a CUDA 12.8 toolchain stage for every generation RunPod
rents (sm_80 A100, sm_86, sm_89 L40S/4090, sm_90 H100, sm_120a RTX 50 / RTX PRO
Blackwell) and a portable x86-64-v3 CPU baseline, then keeps only the binaries.
```bash
./packaging/runpod/build_capability_image.sh engines ghcr.io/nextime/coderai-engines:0.2.18
```
It stages the engine sources from `~/.coderai/{ds4,colibri,kimi-k3-in-c}` (or
`CODERAI_DS4_DIR` etc.), so the pod runs the same engine code the local install
does.
**Naming the engine.** A RunPod-only model has `backend: runpod` — that slot is
taken — so the engine goes on the runpod block, beside `vllm` and `llamacpp`:
```json
{ "path": "DeepSeek-V4-Pro-Q4K.gguf", "backend": "runpod",
"runpod": { "engine": "ds4", "network_volume_id": "abc123",
"volume_path": "models/DeepSeek-V4-Pro-Q4K.gguf",
"min_vram_gb": 80, "boot_timeout_s": 900, "load_timeout_s": 3600 } }
```
A *local* model pinned to `backend: ds4` / `colibri` / `k3` that bursts to
RunPod carries the pin itself; either way it lands on the engines image with
that engine switched on (`CODERAI_DS4_ENABLED=1`) and **this install's settings
for it** forwarded as `CODERAI_DS4_CONFIG` — context, expert cache, extra args,
the ds4 download variant — minus anything local (install dir, ports, paths).
`ds4.auto_download` is forced on for the pod: a pod that cannot fetch its own
weights answers nothing.
**Where the weights come from, per engine:**
| Engine | On the pod | Without `volume_path` |
|---|---|---|
| `ds4` | loads `volume_path` if set | runs `download_model.sh <model_variant>` into `<volume>/cache/ds4` (or the container disk — see the warning) |
| `colibri` | loads the container directory at `volume_path` | **never downloads**: fails with "no model container resolved" |
| `k3` | loads the checkpoint directory at `volume_path` | **never downloads**: the checkpoint is ~1.56 TB |
`volume_path` is relative to the volume mount (`/workspace` by default) or
absolute. Put the weights there once — from a pod, with `scp` through the RunPod
console, or by letting one ds4 pod download and every later one attach.
**ktransformers is not in the image.** It is a Python stack (kt-kernel +
SGLang) for AMX CPUs, several GB in a venv of its own, and has never been built
for a pod. A model pinned to `kt` is refused with that reason at provision time;
serve it from a machine you own with the `host` backend instead.
**The engines are CPU-hungry too.** colibri's DeepSeek-V4 and Kimi-K3 engines
and `k3` stream experts through RAM by design (no CUDA path); pick a pod with
the RAM they need, not just the VRAM — `min_vram_gb` says nothing about RAM.
--- ---
## 3. Request flow ## 3. Request flow
......
# CoderAI engines pod — ds4, colibri and kimi-k3-in-c, compiled for any pod GPU.
#
# The local install carries these engines as binaries built on THIS machine:
# ds4-server for the one card here (sm_86) against the host's CUDA 13, colibri
# without a GPU backend at all. Copying them into a pod image would ship an
# engine that fails at first kernel launch on an A100 and cannot find
# libcudart.so.13 anywhere. So this image builds them itself, in a CUDA 12.8
# toolchain stage, for every GPU generation RunPod rents (Ampere through
# Blackwell) and for a portable x86-64-v3 CPU baseline — then keeps only the
# binaries and the scripts beside them. The toolchain never reaches the image.
#
# The runtime side is the ordinary full core: torch's cu128 wheels carry the
# libcudart.so.12 / libcublas.so.12 the engines link, so nothing else is added.
#
# Built by build_capability_image.sh engines, which first stages the engine
# source trees into packaging/runpod/engines-src/ (they live outside the repo).
ARG CORE_BASE=coderai-capability-base:latest
ARG CUDA_DEVEL=nvidia/cuda:12.8.1-devel-ubuntu24.04
# ---------------------------------------------------------------- build stage
FROM ${CUDA_DEVEL} AS engines
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential git python3 ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# SASS for every card RunPod rents. colibri's Makefile spells its own list as
# CUDA_ARCH=portable (plus a PTX fallback); ds4 takes the raw flags, and its
# Blackwell kernels use block-scaled MXF4 MMA, which exists only on the
# arch-specific sm_120a target — plain sm_120 fails in ptxas, and so would a
# PTX fallback, so there is none: a card newer than this list gets no ds4.
ENV GENCODE="-gencode arch=compute_80,code=sm_80 -gencode arch=compute_86,code=sm_86 -gencode arch=compute_89,code=sm_89 -gencode arch=compute_90,code=sm_90 -gencode arch=compute_120a,code=sm_120a"
WORKDIR /src
COPY packaging/runpod/engines-src/ /src/
COPY packaging/patch-k3.py /src/patch-k3.py
# ds4: CUDA, all archs, portable CPU flags. `-B` because the staged tree may
# carry the host's .o files, which would "already be built" for the wrong card.
RUN set -eux; cd /src/ds4; \
make -B ds4-server CUDA_ARCH=sm_80 NVCC_ARCH_FLAGS="${GENCODE}" \
NATIVE_CPU_FLAG=-march=x86-64-v3 -j"$(nproc)"; \
./ds4-server --help >/dev/null 2>&1 || true; \
ls -la ds4-server
# colibri: the GLM engine with the CUDA backend; DeepSeek-V4 and Kimi-K3 are
# CPU engines by design (their Makefiles have no CUDA path) and only need the
# portable ARCH so they run on whatever CPU the pod has.
RUN set -eux; cd /src/colibri/c; \
rm -f *.o .build-config; \
make colibri CUDA=1 CUDA_ARCH=portable ARCH=x86-64-v3 -j"$(nproc)"; \
make deepseek-v4 ARCH=x86-64-v3 -j"$(nproc)"; \
make kimi_k3 ARCH=x86-64-v3 -j"$(nproc)"; \
ls -la colibri deepseek_v4 kimi_k3
# kimi-k3-in-c: coderai's resident serve-loop patch, then the portable build.
RUN set -eux; cd /src/kimi-k3-in-c; \
rm -rf build; \
python3 /src/patch-k3.py src/cli/k3_run.c; \
make portable -j"$(nproc)"; \
ls -la bin/k3
# What ships: binaries, scripts, docs — no objects, no toolchain, no git.
RUN set -eux; mkdir -p /out/ds4 /out/colibri /out/kimi-k3-in-c; \
cd /src/ds4 && cp -a ds4-server download_model.sh README.md LICENSE /out/ds4/ 2>/dev/null || true; \
cd /src/colibri && cp -a . /out/colibri/ && find /out/colibri -name '*.o' -delete && rm -rf /out/colibri/.git; \
cd /src/kimi-k3-in-c && mkdir -p /out/kimi-k3-in-c/bin && cp -a bin/k3 /out/kimi-k3-in-c/bin/ \
&& cp -a README.md LICENSE Makefile /out/kimi-k3-in-c/ 2>/dev/null || true; \
du -sh /out/*
# -------------------------------------------------------------- runtime stage
FROM ${CORE_BASE}
ARG PROFILE=engines
ARG TORCH_INDEX=https://download.pytorch.org/whl/cu128
ENV PROFILE=${PROFILE}
ARG IMAGE_TAG=unknown
ENV CODERAI_IMAGE_TAG=${IMAGE_TAG}
WORKDIR /opt/coderai/app
COPY packaging/runpod/profiles/ /tmp/profiles/
RUN --mount=type=cache,target=/root/.cache/pip set -eux; \
python -m pip install --extra-index-url ${TORCH_INDEX} -r /tmp/profiles/${PROFILE}.txt; \
python -c "import torch; print('torch', torch.__version__, 'cuda', torch.version.cuda)"; \
rm -rf /tmp/profiles
COPY --from=engines /out/ds4/ /opt/coderai/ds4/
COPY --from=engines /out/colibri/ /opt/coderai/colibri/
COPY --from=engines /out/kimi-k3-in-c/ /opt/coderai/kimi-k3-in-c/
# Where the workers look (codai/api/{ds4,colibri,k3}_worker.default_install_dir),
# and the CUDA runtime the engines link, from torch's own wheels.
ENV CODERAI_DS4_DIR=/opt/coderai/ds4 \
CODERAI_COLIBRI_DIR=/opt/coderai/colibri \
CODERAI_K3_DIR=/opt/coderai/kimi-k3-in-c \
LD_LIBRARY_PATH=/usr/local/lib/python3.12/site-packages/nvidia/cuda_runtime/lib:/usr/local/lib/python3.12/site-packages/nvidia/cublas/lib:${LD_LIBRARY_PATH}
# Fail the BUILD if a binary is missing a library: the alternative is a pod that
# boots, takes the request, and dies at the engine's first exec.
RUN set -eux; for b in /opt/coderai/ds4/ds4-server /opt/coderai/colibri/c/colibri \
/opt/coderai/colibri/c/deepseek_v4 /opt/coderai/colibri/c/kimi_k3 \
/opt/coderai/kimi-k3-in-c/bin/k3; do \
test -x "$b"; \
if ldd "$b" | grep -q 'not found'; then echo "ERROR: $b:" >&2; ldd "$b" | grep 'not found' >&2; exit 1; fi; \
echo "ok: $b"; \
done; \
n=$(grep -a -o '\.target sm_[0-9]*a\?' /opt/coderai/ds4/ds4-server | sort -u | wc -l); \
echo "ds4-server SASS targets: $n"; test "$n" -ge 5
COPY . /opt/coderai/app
RUN python -c "import codai.api.app; print('coderai imports OK for profile ${PROFILE}')"
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --start-period=120s \
CMD curl -fsS http://127.0.0.1:8000/healthz || exit 1
COPY packaging/runpod/boot.sh /opt/coderai/boot.sh
RUN chmod +x /opt/coderai/boot.sh
CMD ["/opt/coderai/boot.sh"]
...@@ -15,7 +15,7 @@ VERSION="${VERSION:-$(python3 -c 'import re,pathlib; print(re.search(r"__version ...@@ -15,7 +15,7 @@ VERSION="${VERSION:-$(python3 -c 'import re,pathlib; print(re.search(r"__version
# A profile's extra-venv specs live beside it as <profile>.venv-<name>.txt, so # A profile's extra-venv specs live beside it as <profile>.venv-<name>.txt, so
# a bare *.txt glob would invent a profile called "audio.venv-audiocraft" and # a bare *.txt glob would invent a profile called "audio.venv-audiocraft" and
# try to build an image for it. # try to build an image for it.
PROFILES="${PROFILES:-$(ls packaging/runpod/profiles/*.txt | xargs -n1 basename | sed 's/\.txt$//' | grep -v '^core$' | grep -v '\.venv-' | tr '\n' ' ')}" PROFILES="${PROFILES:-$(ls packaging/runpod/profiles/*.txt | xargs -n1 basename | sed 's/\.txt$//' | grep -v '^core' | grep -v '\.venv-' | tr '\n' ' ')}"
if [[ -z "$NS" ]]; then if [[ -z "$NS" ]]; then
echo "usage: $0 <registry/namespace> e.g. ghcr.io/nextime" >&2 echo "usage: $0 <registry/namespace> e.g. ghcr.io/nextime" >&2
......
...@@ -64,9 +64,41 @@ else ...@@ -64,9 +64,41 @@ else
echo "==> reusing the shared core $CORE_BASE" echo "==> reusing the shared core $CORE_BASE"
fi fi
echo "==> building $TAG from profile '$PROFILE'" # A profile may name its own Dockerfile (`<profile>.dockerfile`) when the generic
# one cannot express it — the engines image compiles C/CUDA in a toolchain stage.
DOCKERFILE=packaging/runpod/Dockerfile.capability
if [[ -f "$PROFILES_DIR/$PROFILE.dockerfile" ]]; then
DOCKERFILE="packaging/runpod/$(head -1 "$PROFILES_DIR/$PROFILE.dockerfile")"
fi
# The engine sources live outside the repo (the same trees the local install
# builds from: ~/.coderai/ds4, colibri, kimi-k3-in-c). Stage them into the build
# context without weights, objects or git history, so the image builds the SAME
# code the local engines run, for every pod GPU instead of this one.
if [[ "$PROFILE" == "engines" ]]; then
SRC=packaging/runpod/engines-src
rm -rf "$SRC"; mkdir -p "$SRC"
for pair in "ds4:${CODERAI_DS4_DIR:-$HOME/.coderai/ds4}" \
"colibri:${CODERAI_COLIBRI_DIR:-$HOME/.coderai/colibri}" \
"kimi-k3-in-c:${CODERAI_K3_DIR:-$HOME/.coderai/kimi-k3-in-c}"; do
name="${pair%%:*}"; dir="${pair#*:}"
if [[ ! -d "$dir" ]]; then
echo "engine source tree missing: $dir (set CODERAI_$(echo "$name" | tr a-z- A-Z_ | sed 's/KIMI_K3_IN_C/K3/')_DIR)" >&2
exit 2
fi
rsync -a --exclude '.git/' --exclude 'build/' --exclude '*.o' --exclude '*.gguf' \
--exclude '*.gguf.*' --exclude 'gguf/' --exclude '*.safetensors' --exclude '*.bin' \
--exclude 'glm52_i4/' --exclude 'web/node_modules/' --exclude 'containers/' \
--exclude '/ds4flash.gguf' --exclude '/ds4-server' --exclude '/ds4' --exclude '/ds4-agent' \
--exclude '/ds4-bench' --exclude '/ds4-eval' --exclude '/speed-bench' \
"$dir/" "$SRC/$name/"
echo " staged $name from $dir ($(du -sh "$SRC/$name" | cut -f1))"
done
fi
echo "==> building $TAG from profile '$PROFILE' ($DOCKERFILE)"
# BuildKit for the pip cache mount: nine profiles, one torch download. # BuildKit for the pip cache mount: nine profiles, one torch download.
DOCKER_BUILDKIT=1 docker build -f packaging/runpod/Dockerfile.capability \ DOCKER_BUILDKIT=1 docker build -f "$DOCKERFILE" \
--build-arg "PROFILE=$PROFILE" \ --build-arg "PROFILE=$PROFILE" \
--build-arg "IMAGE_TAG=$TAG" \ --build-arg "IMAGE_TAG=$TAG" \
--build-arg "CORE_BASE=$CORE_BASE" \ --build-arg "CORE_BASE=$CORE_BASE" \
......
# Native MoE engines — ds4 (DeepSeek-V4, CUDA), colibri (GLM-5.2 CUDA;
# DeepSeek-V4 and Kimi-K3 CPU) and kimi-k3-in-c — a coderai pod that serves
# frontier-size models through the same C engines the local install uses.
#
# The engines are not pip packages: they are compiled in a CUDA 12.8 toolchain
# stage of Dockerfile.capability-engines (see the `engines.dockerfile` marker)
# for EVERY pod GPU generation (sm_80/86/89/90/120), not the one card this
# machine has, and copied in beside their download scripts. The Python side
# needs nothing beyond the core: the engines bring their own tokenizers.
#
# THE WEIGHTS ARE THE COST. DeepSeek-V4 is ~154 GB, Kimi-K3 is measured in
# terabytes. Put them on a network volume (docs/runpod.md, "Engine pods") —
# a pod without one downloads all of it on every cold start.
sentencepiece>=0.1.99
protobuf>=3.20.0
...@@ -22,7 +22,7 @@ HERE="packaging/runpod" ...@@ -22,7 +22,7 @@ HERE="packaging/runpod"
NS="${NS:-ghcr.io/nextime}" NS="${NS:-ghcr.io/nextime}"
REGISTRY="${NS%%/*}" REGISTRY="${NS%%/*}"
VERSION="${VERSION:-$(python3 -c 'import re,pathlib; print(re.search(r"__version__ = \"([^\"]+)\"", pathlib.Path("codai/__init__.py").read_text()).group(1))')}" VERSION="${VERSION:-$(python3 -c 'import re,pathlib; print(re.search(r"__version__ = \"([^\"]+)\"", pathlib.Path("codai/__init__.py").read_text()).group(1))')}"
PROFILES="${PROFILES:-$(ls $HERE/profiles/*.txt | xargs -n1 basename | sed 's/\.txt$//' | grep -v '^core$' | tr '\n' ' ')}" PROFILES="${PROFILES:-$(ls $HERE/profiles/*.txt | xargs -n1 basename | sed 's/\.txt$//' | grep -v '^core' | grep -v '\.venv-' | tr '\n' ' ')}"
REBUILD="${REBUILD:-0}" REBUILD="${REBUILD:-0}"
NO_PUSH="${NO_PUSH:-0}" NO_PUSH="${NO_PUSH:-0}"
YES="${YES:-0}" YES="${YES:-0}"
......
...@@ -872,3 +872,116 @@ def test_a_multi_gpu_pod_is_priced_and_sharded_as_a_whole(monkeypatch): ...@@ -872,3 +872,116 @@ def test_a_multi_gpu_pod_is_priced_and_sharded_as_a_whole(monkeypatch):
assert "--split-mode layer" in rw._llamacpp_docker_args(ml, "m", {}) assert "--split-mode layer" in rw._llamacpp_docker_args(ml, "m", {})
# A single card asks for neither: no needless flags on the common case. # A single card asks for neither: no needless flags on the common case.
assert "tensor-parallel" not in rw._vllm_docker_args(m1, "org/m", {"path": "org/m"}) assert "tensor-parallel" not in rw._vllm_docker_args(m1, "org/m", {"path": "org/m"})
def test_an_engine_model_gets_the_engines_image_and_its_engine_switched_on(monkeypatch):
"""The local ds4-server is an sm_86 build against CUDA 13 and the local
colibri has no GPU backend: neither can be copied into a pod. The engines
image builds them for every pod GPU, and a model that names an engine —
on its runpod block, since `backend: runpod` takes the pin slot — lands on
that image with the engine enabled and this install's settings for it."""
import json
import codai.api.runpod_worker as rw
from codai.api.runpod_worker import pod_plan, parse_model_runpod
class _Cfg:
ctx = 65536; ssd_streaming = True; extra_args = "--foo"; extra_env = ""
model_variant = "q4-imatrix"; auto_download = False
expert_cache_reserve_gb = 0; model_id = "deepseek-v4"
install_dir = "/home/me/.coderai/ds4"; port = 1234
class _Root:
ds4 = _Cfg()
class _CM:
config = _Root()
monkeypatch.setattr("codai.admin.routes.config_manager", _CM(), raising=False)
entry = {"path": "/AI/gguf/DeepSeek-V4-Q4.gguf", "model_type": "text_models",
"backend": "runpod", "weights_gb": 154,
"ds4": {"ssd_streaming": True}}
mcfg = parse_model_runpod({"engine": "ds4"})
plan = pod_plan(mcfg, "deepseek", entry=entry)
assert plan["engine"] == "coderai"
assert plan["image"].endswith("coderai-engines:latest")
env = plan["env"]
assert env["CODERAI_DS4_ENABLED"] == "1"
fwd = json.loads(env["CODERAI_DS4_CONFIG"])
assert fwd["ctx"] == 65536 and fwd["ssd_streaming"] is True
assert fwd["auto_download"] is True, "a pod that cannot fetch answers nothing"
assert "install_dir" not in fwd and "port" not in fwd
seed = json.loads(env["CODERAI_SEED_MODELS"])[0]
assert seed["backend"] == "ds4", "the pod must run ds4 for it, not transformers"
assert seed["ds4"] == {"ssd_streaming": True}
assert "weights_gb" not in seed # sizes the disk here, not the model there
assert seed["path"] == "DeepSeek-V4-Q4.gguf", "a local path means nothing there"
# Weights already on the volume: the seed points straight at them.
mcfg = parse_model_runpod({"engine": "ds4", "network_volume_id": "vol1",
"volume_path": "models/DeepSeek-V4-Q4.gguf"})
seed = json.loads(pod_plan(mcfg, "deepseek", entry=entry)["env"]["CODERAI_SEED_MODELS"])[0]
assert seed["path"] == "/workspace/models/DeepSeek-V4-Q4.gguf"
# A local model pinned to the engine itself, bursting: same answer.
local = {"path": "/AI/glm52", "model_type": "text_models", "backend": "colibri"}
plan = pod_plan(parse_model_runpod({"network_volume_id": "v", "volume_path": "glm52"}),
"glm", entry=local)
assert plan["image"].endswith("coderai-engines:latest")
assert plan["env"]["CODERAI_COLIBRI_ENABLED"] == "1"
assert json.loads(plan["env"]["CODERAI_SEED_MODELS"])[0]["backend"] == "colibri"
# The other pins describe THIS machine and are stripped from the seed.
vk = {"path": "org/m", "model_type": "text_models", "backend": "vulkan"}
seed = json.loads(pod_plan(parse_model_runpod({"engine": "coderai", "image": "x"}),
"m", entry=vk)["env"]["CODERAI_SEED_MODELS"])[0]
assert "backend" not in seed
# ktransformers has no pod image, and says so instead of shipping a pod
# that cannot run it.
import pytest
with pytest.raises(RuntimeError, match="ktransformers"):
pod_plan(parse_model_runpod({"engine": "kt"}), "m",
entry={"path": "org/m", "model_type": "text_models"})
# The volume env points ds4's downloader at the volume too.
assert rw.volume_env("/workspace")["DS4_GGUF_DIR"] == "/workspace/cache/ds4"
def test_a_pod_switches_its_engine_on_from_env_and_takes_the_settings(tmp_path, monkeypatch):
import json
from codai.config import ConfigManager
monkeypatch.setenv("CODERAI_DS4_ENABLED", "1")
monkeypatch.setenv("CODERAI_DS4_CONFIG", json.dumps(
{"ctx": 65536, "auto_download": True, "install_dir": "/nope",
"port": 9, "not_a_field": 1}))
cm = ConfigManager(str(tmp_path))
cm.load()
assert cm.config.ds4.enabled is True
assert cm.config.ds4.ctx == 65536 and cm.config.ds4.auto_download is True
assert cm.config.ds4.install_dir != "/nope" and cm.config.ds4.port != 9
assert not hasattr(cm.config.ds4, "not_a_field")
assert cm.config.colibri.enabled is False # only what was asked for
on_disk = json.loads((tmp_path / "config.json").read_text())
assert on_disk.get("ds4", {}).get("enabled") is not True
def test_the_engines_image_is_built_from_source_for_every_pod_gpu():
"""Shipping this machine's binaries would ship an sm_86-only ds4 linked
to CUDA 13. The image compiles in a CUDA 12.8 stage for Ampere through
Blackwell, and the build fails if a binary cannot find its libraries."""
from pathlib import Path
from codai.api.runpod_worker import PUBLISHED_CAPABILITY_IMAGES, default_capability_image
assert "engines" in PUBLISHED_CAPABILITY_IMAGES
assert default_capability_image("engines").endswith("coderai-engines:latest")
prof = Path("packaging/runpod/profiles")
assert (prof / "engines.txt").exists()
assert (prof / "engines.dockerfile").read_text().strip() == "Dockerfile.capability-engines"
df = Path("packaging/runpod/Dockerfile.capability-engines").read_text()
for arch in ("sm_80", "sm_86", "sm_89", "sm_90", "sm_120"):
assert arch in df
assert "CUDA_ARCH=portable" in df and "x86-64-v3" in df
assert "patch-k3.py" in df and "not found" in df
for var in ("CODERAI_DS4_DIR", "CODERAI_COLIBRI_DIR", "CODERAI_K3_DIR"):
assert var in df
sh = Path("packaging/runpod/build_capability_image.sh").read_text()
assert ".dockerfile" in sh and "engines-src" in sh
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment