runpod: per-model boot/load timeouts (big models need longer)

The 300s boot cap was a hard-coded constant; large models need much more time for
the image pull + weight download. Both budgets are now per-model config:

- boot_timeout_s (default 300) — until the pod exposes its port; a stuck machine
  is still dropped fast and the next candidate tried.
- load_timeout_s (default 600) — until vLLM answers /v1/models; raise for big models.

Parsed in RunpodModelConfig (min 30s), used by _provision_one in place of the class
constants (which remain the fallback), whitelisted in api_model_configure, and
exposed on the model page with hints.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R6iugoNeEgt9PkajKrFyq4
parent d9e49200
...@@ -2789,7 +2789,7 @@ async def api_model_configure(request: Request, username: str = Depends(require_ ...@@ -2789,7 +2789,7 @@ async def api_model_configure(request: Request, username: str = Depends(require_
# ints # ints
for k in ("container_disk_gb", "volume_gb", "port", "ctx", "min_pods", for k in ("container_disk_gb", "volume_gb", "port", "ctx", "min_pods",
"max_pods", "scale_up_inflight_per_pod", "idle_timeout_s", "max_pods", "scale_up_inflight_per_pod", "idle_timeout_s",
"min_workers", "max_workers"): "boot_timeout_s", "load_timeout_s", "min_workers", "max_workers"):
v = src.get(k) v = src.get(k)
if v not in (None, ""): if v not in (None, ""):
try: try:
......
...@@ -743,6 +743,18 @@ window.__DEFAULT_WHISPER_SERVER_PATH__ = {{ default_whisper_server_path|tojson } ...@@ -743,6 +743,18 @@ window.__DEFAULT_WHISPER_SERVER_PATH__ = {{ default_whisper_server_path|tojson }
<div class="form-row" style="margin:0"><label class="form-label">Max pods</label><input type="number" id="cfg-rp-max-pods" class="form-input" min="1" placeholder="1"></div> <div class="form-row" style="margin:0"><label class="form-label">Max pods</label><input type="number" id="cfg-rp-max-pods" class="form-input" min="1" placeholder="1"></div>
<div class="form-row" style="margin:0"><label class="form-label">Idle timeout (s)</label><input type="number" id="cfg-rp-idle" class="form-input" min="0" placeholder="300"></div> <div class="form-row" style="margin:0"><label class="form-label">Idle timeout (s)</label><input type="number" id="cfg-rp-idle" class="form-input" min="0" placeholder="300"></div>
</div> </div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.5rem;margin-top:.5rem">
<div class="form-row" style="margin:0">
<label class="form-label">Boot timeout (s)</label>
<input type="number" id="cfg-rp-boot" class="form-input" min="30" placeholder="300">
<span class="form-hint" style="font-size:11px">Wait for the pod to expose its port; a stuck machine is dropped and the next one tried.</span>
</div>
<div class="form-row" style="margin:0">
<label class="form-label">Model load timeout (s)</label>
<input type="number" id="cfg-rp-load" class="form-input" min="30" placeholder="600">
<span class="form-hint" style="font-size:11px">Wait for vLLM to serve the model — raise this for big models (long image pull + weight download).</span>
</div>
</div>
<div style="display:grid;grid-template-columns:2fr 1fr 1fr;gap:.5rem;margin-top:.5rem"> <div style="display:grid;grid-template-columns:2fr 1fr 1fr;gap:.5rem;margin-top:.5rem">
<div class="form-row" style="margin:0"><label class="form-label">Pod image <span class="muted">(blank = default vLLM)</span></label><input type="text" id="cfg-rp-image" class="form-input" placeholder="vllm/vllm-openai:latest"></div> <div class="form-row" style="margin:0"><label class="form-label">Pod image <span class="muted">(blank = default vLLM)</span></label><input type="text" id="cfg-rp-image" class="form-input" placeholder="vllm/vllm-openai:latest"></div>
<div class="form-row" style="margin:0"><label class="form-label">Disk (GB)</label><input type="number" id="cfg-rp-disk" class="form-input" min="1" placeholder="40"></div> <div class="form-row" style="margin:0"><label class="form-label">Disk (GB)</label><input type="number" id="cfg-rp-disk" class="form-input" min="1" placeholder="40"></div>
...@@ -3991,6 +4003,8 @@ function _populateRunpod(s){ ...@@ -3991,6 +4003,8 @@ function _populateRunpod(s){
set('cfg-rp-min-pods', rp.min_pods != null ? rp.min_pods : ''); set('cfg-rp-min-pods', rp.min_pods != null ? rp.min_pods : '');
set('cfg-rp-max-pods', rp.max_pods != null ? rp.max_pods : ''); set('cfg-rp-max-pods', rp.max_pods != null ? rp.max_pods : '');
set('cfg-rp-idle', rp.idle_timeout_s != null ? rp.idle_timeout_s : ''); set('cfg-rp-idle', rp.idle_timeout_s != null ? rp.idle_timeout_s : '');
set('cfg-rp-boot', rp.boot_timeout_s != null ? rp.boot_timeout_s : '');
set('cfg-rp-load', rp.load_timeout_s != null ? rp.load_timeout_s : '');
set('cfg-rp-image', rp.image || ''); set('cfg-rp-image', rp.image || '');
set('cfg-rp-disk', rp.container_disk_gb != null ? rp.container_disk_gb : ''); set('cfg-rp-disk', rp.container_disk_gb != null ? rp.container_disk_gb : '');
set('cfg-rp-port', rp.port != null ? rp.port : ''); set('cfg-rp-port', rp.port != null ? rp.port : '');
...@@ -4030,6 +4044,8 @@ function _collectRunpod(){ ...@@ -4030,6 +4044,8 @@ function _collectRunpod(){
min_pods: intOrU('cfg-rp-min-pods'), min_pods: intOrU('cfg-rp-min-pods'),
max_pods: intOrU('cfg-rp-max-pods'), max_pods: intOrU('cfg-rp-max-pods'),
idle_timeout_s: intOrU('cfg-rp-idle'), idle_timeout_s: intOrU('cfg-rp-idle'),
boot_timeout_s: intOrU('cfg-rp-boot'),
load_timeout_s: intOrU('cfg-rp-load'),
image: (val('cfg-rp-image') || '').trim(), image: (val('cfg-rp-image') || '').trim(),
container_disk_gb: intOrU('cfg-rp-disk'), container_disk_gb: intOrU('cfg-rp-disk'),
port: intOrU('cfg-rp-port'), port: intOrU('cfg-rp-port'),
......
...@@ -56,6 +56,9 @@ class RunpodModelConfig: ...@@ -56,6 +56,9 @@ class RunpodModelConfig:
max_pods: int = 1 max_pods: int = 1
scale_up_inflight_per_pod: int = 4 scale_up_inflight_per_pod: int = 4
idle_timeout_s: int = 300 # destroy a pod this long after its last request idle_timeout_s: int = 300 # destroy a pod this long after its last request
# Boot budgets — bigger models need longer (image pull + weight download).
boot_timeout_s: int = 300 # until the pod exposes its port
load_timeout_s: int = 600 # until vLLM answers /v1/models
# --- serverless --- # --- serverless ---
endpoint_id: str = "" # reference an existing serverless endpoint endpoint_id: str = "" # reference an existing serverless endpoint
min_workers: int = 0 min_workers: int = 0
...@@ -130,6 +133,8 @@ def parse_model_runpod(block: Optional[dict]) -> RunpodModelConfig: ...@@ -130,6 +133,8 @@ def parse_model_runpod(block: Optional[dict]) -> RunpodModelConfig:
cfg.scale_up_inflight_per_pod = max(1, _as_int(b.get("scale_up_inflight_per_pod"), cfg.scale_up_inflight_per_pod = max(1, _as_int(b.get("scale_up_inflight_per_pod"),
cfg.scale_up_inflight_per_pod)) cfg.scale_up_inflight_per_pod))
cfg.idle_timeout_s = max(0, _as_int(b.get("idle_timeout_s"), cfg.idle_timeout_s)) cfg.idle_timeout_s = max(0, _as_int(b.get("idle_timeout_s"), cfg.idle_timeout_s))
cfg.boot_timeout_s = max(30, _as_int(b.get("boot_timeout_s"), cfg.boot_timeout_s))
cfg.load_timeout_s = max(30, _as_int(b.get("load_timeout_s"), cfg.load_timeout_s))
cfg.endpoint_id = (b.get("endpoint_id") or "").strip() cfg.endpoint_id = (b.get("endpoint_id") or "").strip()
cfg.min_workers = max(0, _as_int(b.get("min_workers"), cfg.min_workers)) cfg.min_workers = max(0, _as_int(b.get("min_workers"), cfg.min_workers))
cfg.max_workers = max(1, _as_int(b.get("max_workers"), cfg.max_workers)) cfg.max_workers = max(1, _as_int(b.get("max_workers"), cfg.max_workers))
...@@ -429,17 +434,19 @@ class RunpodPodPool: ...@@ -429,17 +434,19 @@ class RunpodPodPool:
print(f"[runpod] pod {pod_id} created for {self.model_key!r} " print(f"[runpod] pod {pod_id} created for {self.model_key!r} "
f"({sel['display_name']} {sel['cloud_type']}) — booting; logs: {console}", f"({sel['display_name']} {sel['cloud_type']}) — booting; logs: {console}",
flush=True) flush=True)
boot_to = float(self.mcfg.boot_timeout_s or self.PORT_TIMEOUT_S)
load_to = float(self.mcfg.load_timeout_s or self.HEALTH_TIMEOUT_S)
try: try:
url = client.wait_ready(pod_id, port, ready_timeout=self.PORT_TIMEOUT_S) url = client.wait_ready(pod_id, port, ready_timeout=boot_to)
# Wait for the OpenAI server inside the pod (image pull + model load). # Wait for the OpenAI server inside the pod (image pull + model load).
deadline = time.time() + self.HEALTH_TIMEOUT_S deadline = time.time() + load_to
while time.time() < deadline: while time.time() < deadline:
if _pod_health_ok(url): if _pod_health_ok(url):
break break
time.sleep(5) time.sleep(5)
else: else:
raise RunpodError(f"pod {pod_id} OpenAI server (vLLM) did not answer " raise RunpodError(f"pod {pod_id} OpenAI server (vLLM) did not answer "
f"/v1/models within {int(self.HEALTH_TIMEOUT_S)}s") f"/v1/models within {int(load_to)}s")
except Exception as exc: except Exception as exc:
# Self-diagnose: pull the vLLM/container log before tearing down, so # Self-diagnose: pull the vLLM/container log before tearing down, so
# the cause (OOM / bad args / model gate / stuck machine) is in the log. # the cause (OOM / bad args / model gate / stuck machine) is in the log.
......
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