Control plane for one GPU workload at a time, many logical model names, and a stable client URL.
Portal: http://sparky/ → Inference. Gateway: http://sparky:9000/v1.
| Layer | Multi-model? | How |
|---|---|---|
| Gateway / API | Yes | Many ids (sparky, qwen3.6-27b-dflash, …) |
| Physical GB10 | One heavy slot | One recipe loaded; switch = evict + start |
| Agents | Yes, with swap cost | Request any registered name; cold start returns 503 |
Agents are not locked to one model. They wait out switch time when the requested profile is not active. Prefer model sparky so the client always hits whatever is already up.
flowchart TB
clients["OpenCode, Grok, Hermes, Open WebUI, curl"]
gw["Gateway :9000/v1"]
api["spark inference API :8767"]
subgraph slot["One GPU slot"]
direction LR
eugr["eugr container<br/>vllm_node :8000"]
llama["llama.cpp host<br/>:8081"]
ds4["ds4 host<br/>:8000"]
si["SparkInfer container<br/>:8000"]
sg["SGLang container<br/>qwen38-sglang :8000"]
fn["Flash-Next container<br/>vllm-fn-tp1 :8000"]
tf["TensorFold container<br/>qwen38-flash-next-tf :8000"]
end
clients --> gw --> api --> slot
eugr has always been a container (vendor/spark-vllm-docker, usually vllm_node). llama.cpp and ds4 are host processes. SparkInfer, SGLang, Flash-Next, and TensorFold are also containers. Everything except llama.cpp binds :8000. Never run two GPU engines.
spark inference up stops every engine before the next start. spark engine llama up stops Flash-Next and TensorFold the same way. Do not start a second engine by hand while one of those containers is up.
| Engine | Process | Container | Port |
|---|---|---|---|
| eugr | vLLM in spark-vllm-docker |
vllm_node |
8000 |
| llama.cpp | host llama-server |
none | 8081 |
| ds4 | host ds4-server |
none | 8000 |
| sparkinfer | patched vLLM | deepseek-v4-flash-spark |
8000 |
| sglang | lmsysorg/sglang pin |
qwen38-sglang |
8000 |
| flashnext | vllm/vllm-openai:qwen38-flash-next |
vllm-fn-tp1 |
8000 |
| tensorfold | tensorfold-native serve in a local Zig image |
qwen38-flash-next-tf |
8000 |
A recipe may set tensorfold.container or flashnext.container. The control plane treats the active recipe's name, and the default name above, as "that engine is up."
One YAML per profile under recipes/ (drafts in recipes/drafts/). Enabled ids live in host-local data/inference-profiles.yaml.
id: opencode-qwen36-250k
name: Qwen3.6-35B-A3B 250k NVFP4
catalog_id: nvidia/qwen3.6-35b-a3b-nvfp4
inventory_path: nvidia/qwen3.6-35b-a3b
engine: eugr # eugr | llamacpp | ds4 | sparkinfer | sglang | flashnext | tensorfold
tier: heavy
eugr_recipe: /opt/spark/services/eugr-qwen36-local.yaml
served_name: qwen3.6-35b-a3b-nvfp4
port: 8000
lifecycle: works
context:
default: 256000
kv_default: fp8Public display names drop client branding. Profile ids keep the opencode- prefix so existing up commands stay stable.
Scaffold: spark recipe scaffold <lab/slug> <engine>. Extend the router in spark-inference.py plus catalog engine / capabilities. Hand-write YAML only for MoE, multimodal, DFlash, ds4, MTP, SGLang, Flash-Next, or TensorFold.
Recipes that leave draft (testing, works, production, or promote) must set roi_profile to an id in git-tracked data/roi-profiles.yaml. That file is the cloud-SKU identity (provider, model name, family) — not dollar prices. Per-token prices stay in host-local run/inference-pricing.json, keyed by ROI profile id. Token counters stay keyed by recipe id.
roi_profile: qwen3.6-35b-a3bTag an existing profile: spark recipe roi <recipe-id> <roi-profile-id>. Create one and tag: spark recipe roi-new <recipe-id> id= name= family= version= params= provider= model= in= out=. Recipe files win over the host-local tag map run/roi-recipe-tags.json (backfill: /opt/spark/venv/bin/python3 scripts/spark-roi.py backfill). The portal v2 usage card rolls tagged recipes up under each ROI profile for ledger vs cloud comps.
Native DeepSeek V4 Flash. Pin: data/ds4-dwarfstar.yaml. Production profile: antirez-deepseek-v4-flash-ds4.
engine: ds4
served_name: deepseek-v4-flash
port: 8000
ds4_args:
- -c
- "32768"
- --dspark
- /models/antirez/deepseek-v4-flash/gguf/DSpark-drafter-Q2K-Q8-0731.ggufOOM guard: scripts/ds4-oom-guard.sh. Casual chat: :8002/v1 (thinking off) or model id deepseek-chat on :8000.
Mia/0xSero patched vLLM for DeepSeek V4 Flash 0731 EXL3. Pin: data/sparkinfer-mia.yaml. Production profile: 0xsero-deepseek-v4-flash-0731-sparkinfer.
engine: sparkinfer
served_name: deepseek-v4-flash-0731
port: 8000Launch needs ≥114 GiB free. Default 384k / 1 seq / DSpark K5. Gateway ids: sparky (think off), sparky-think, plus deepseek-v4-flash-0731 / -think. Smoke: docs/runbooks/smoke-sparkinfer.md.
Qwen3.8-27B DFlash2. Container qwen38-sglang, image pin on the recipe (sglang.image). Port 8000. Profiles: radixark-qwen3-8-27b-dflash2-sglang and the text-only twin.
Mia vLLM pack for Qwen3.8-Flash-Next NVFP4. Container vllm-fn-tp1. The kit under the model tree runs docker run; Spark does not shell out to vLLM on the host. Profile: mia-ailab-qwen3.8-flash-next-vllm.
Mia Zig engine (tensorfold-native), not vLLM. Both TensorFold recipes share container qwen38-flash-next-tf, so the FP4 card and the INT4-AR card cannot run together. Spark sets PORT=8000. The kit's own default is 8888.
sequenceDiagram
participant CLI as spark inference up
participant TF as scripts/spark-tensorfold
participant Kit as host-local start.sh
participant Docker as qwen38-flash-next-tf
CLI->>TF: stop every other engine, then up
TF->>Kit: PORT=8000 and the recipe env
Kit->>Docker: docker run --gpus all --network host
Note over Docker: tensorfold-native serve<br/>weights mounted read-only
Docker-->>CLI: /health, then one smoke completion
The kit is not in git. spark engine tensorfold up fails with "kit not installed" if start.sh is missing. It does not clone. Set one of:
| Recipe | Kit |
|---|---|
azampatti-qwen3.8-flash-next-int4-autoround-tensorfold |
tensorfold.launcher → /models/azampatti/qwen3.8-flash-next-int4-autoround/kit |
nvidia-qwen3.8-flash-next-fp4-local-tensorfold |
SPARK_TENSORFOLD_LAUNCHER in gitignored host.env |
prepare: "0" on the FP4 recipe skips the kit's image build and weight download. Weights are the hardlink tree under tensorfold.hf_home, mounted read-only at /root/.cache/huggingface. The image tag is local (tensorfold-qwen38:zig-…), not a registry pull.
spark inference status
spark inference list
spark inference up opencode-qwen36-250k
spark inference down
spark inference logs
spark inference benchAgents: spark inference help or GET /api/inference/status. Full table: spark-cli.md.
scripts/spark-inference-api.py (:8767) reloads spark-inference.py each request. Routine logic changes need no restart. Restart the API process only when spark-inference-api.py itself changes: sudo bash install/spark-install restart inference-api.
Stable URL: http://sparky:9000/v1 (or 127.0.0.1:9000 on the box).
- Thin proxy:
scripts/spark-inference-gateway.py - Forwards
/v1/*with SSE streaming sparky/sparky-think/sparky-fastalways use the active served model (no auto-switch)- Named aliases can trigger
start_switch_job()and return 503 + Retry-After until ready - Headers:
X-Spark-Active-Profile,X-Spark-Upstream-Port,X-Spark-Served-Model - Systemd:
spark-inference-gateway.service - Activity: each chat/completions POST appends
run/inference-activity.jsonl(auth stripped; 50 MB / 7d).spark-client-activity.py(:8769,/api/activity) rolls 1h/24h for the portal and folds token rows into host-localrun/inference-usage.json
curl http://sparky:9000/v1/models
curl -X POST http://sparky:9000/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"sparky","messages":[{"role":"user","content":"hi"}],"max_tokens":32}'
spark gateway --list-aliasesHardcoded aliases live in spark-inference-gateway.py (hermes-local, qwen-local, …). Extend there.
| Profile ID | Served name | Context | Role |
|---|---|---|---|
opencode-qwen36-250k |
qwen3.6-35b-a3b-nvfp4 |
256k, fp8 KV | MoE coding / general agents |
opencode-qwen27-dflash-262k |
qwen3.6-27b-dflash |
262k, DFlash | Dense 27B, design / architecture |
qwen36-35b-a3b-mtp-eugr |
NVIDIA NVFP4 + MTP | 256k | Golden companion for nvidia/qwen3.6-35b-a3b |
spark inference up opencode-qwen36-250kGateway alias qwen3.6-27b-dflash maps to opencode-qwen27-dflash-262k. Reload at a new ctx: spark inference down && spark inference up <id>.
qwen36-nvfp4 is deprecated. Do not document it as the default.
MTP, DFlash, DSpark, Eagle live on the recipe (speculative: / mtp:), not in portal settings. eugr flags go in the service YAML command: block. llama.cpp flags go in llamacpp_args.
Inference tab: active profile, switch, logs, engine filter (vLLM / llama.cpp / ds4 / SparkInfer / SGLang / Flash-Next / TensorFold), eugr upgrade banner. System tab: GPU + client activity. Benchmaster tab: overnight queue.
Open WebUI (:3000) can point at the gateway or at an engine directly. Gateway is the one URL that survives a profile switch.