對象:想在既有的 vLLM / SGLang / llama.cpp 之上,加入第 N 個推理引擎(例:TensorRT-LLM、 MLC、Ollama…)的人。本文照抄 SGLang、llama.cpp 兩次實際加入的路徑,列出每一個要動的檔案、 新引擎必須滿足的隱含契約,以及最容易踩的坑。
背景設計見 multi-backend-engine-design_zh-CN.md; 可擴展性評估與 P1 收斂見 multi-engine-extensibility-review_zh-TW.md。
現有的 Launcher / LaunchSpec 模型隱含假設每個引擎是這樣的(vLLM/SGLang/llama.cpp 剛好都符合):
- 單行程、用 CLI 旗標啟動 —
command: list[str]由spawn_process拉起,停止用 process-group kill。 - HTTP
/health200 = ready — reconciler 用通用 probe(llama.cpp 是「載入後才綁 port」→ 啟動期 connection-refused,也被當 not-ready,OK)。 - OpenAI 相容
/v1,且/v1/models廣告 served name(router 轉發、boot adopt 身分驗證都靠它)。 - Prometheus text
/metrics,指標名有獨特前綴(vllm:/sglang:/llamacpp:)。 - 啟動進度反映在 log 檔成長 — progress-aware timeout 靠
os.path.getsize(log)。 - 綁 port 即可服務(不需離線編譯 / 預熱階段)。
若你的引擎踩線(例:TensorRT-LLM 需離線 build engine、Triton 是多行程;Ollama 是一個 daemon 管多模型;
disaggregated prefill/decode 一個「instance」= 多行程),先別硬塞——那需要在 Launcher protocol 上補
prepare() / readiness_probe() hook(見 §7 roadmap),否則會被迫在 manager/reconciler 裡塞 engine 特判,
那是架構開始腐爛的起點。典型引擎(單行程、OpenAI 相容、有 /metrics)才是本文的 happy path。
✅ = 設計良好、純增量;
| # | 檔案 | 要做什麼 | 級別 |
|---|---|---|---|
| Backend(主要工作量) | |||
| 1 | apps/backend/app/llmops/launchers.py |
新 Launcher 類別 + 純函式 CLI builder |
✅ |
| 2 | apps/backend/app/main.py(launchers 清單,約 :129) |
註冊新 launcher(1 行) | ✅ 🔴 漏了 = 引擎不存在 |
| 3 | packages/config-schema/schema.py(engine: Literal[...],約 :62) |
加上新引擎名 | |
| 4 | apps/backend/app/services/vllm_command.py |
(選配)paste-command 解析器 + sniff 規則 | |
| 5 | apps/backend/app/perf/manager.py、app/services/lora_convert.py |
只有引擎有特殊 tokenizer / 轉檔格式時才需要 | ✅ |
| Router(單檔集中) | |||
| 6 | apps/router-server/src/llm_router/vllm_metrics_client.py METRIC_NAMES_BY_ENGINE |
加一條(5 個指標名) | ✅ 單一宣告點 |
| 7 | 同檔 ENGINE_SLEEP_CAPABLE |
引擎若支援 sleep 才加 | |
| Frontend(P1 之後基本免動) | |||
| 8 | — | AddModelDialog 已改吃 GET /api/engines,capability/選單/遮蔽鍵自動帶出 |
✅ 引擎特有 UI 文案才需補 |
| 9 | apps/frontend_llmops/src/views/ModelsView.vue、ModelGroupCard.vue 顏色表 |
(選配)給引擎一個 badge 顏色 | |
| 10 | i18n/locales/*.ts |
(選配)引擎特有文案 | |
| Deploy / 監控 | |||
| 11 | deploy/engine-<X>.Dockerfile |
新 base image + backend 程式碼(照三個範本抄) | ✅ |
| 12 | deploy/docker-compose.mixed.yaml |
新 backend service(profile、LLMOPS_NODE_ENGINES=<X>、SD path) |
✅ |
| 13 | Makefile(_ALL_ENGINES) |
引擎清單加值 | |
| 14 | deploy/grafana/dashboards/<X>/ |
(選配)該引擎指標前綴的 dashboard |
核心流程完全不用動:manager 的 sleep/LoRA gating 走 capability、
_post_lora走lora_endpoint_prefix、reconciler/probe 是通用/health、schedulernode_supports()對引擎名是 不透明字串、_defer_to_owner引擎無關、Prometheus file_sd 用 glob 自動發現targets-*.json。這些是 「架構對了」的部分,不要去碰。
在 launchers.py 加一個類別。以 LlamacppLauncher 為最貼近的範本。必須提供:
class MyEngineLauncher:
kind = ModelKind.LLM
engine = "myengine" # (kind, engine) 是 dispatch key,必須全域唯一
# --- 能力宣告:callers 一律 gate 在這些 capability 上,不看引擎名 ---
capabilities = frozenset({
CAP_LORA_MODULES, # 啟動時 --lora-modules 靜態掛載
CAP_METRICS_MYENGINE, # 有 Prometheus /metrics(需在 §6 定義新 CAP 常數)
# CAP_SLEEP # 只有真的有 /sleep+/wake_up 才加(見 §5 坑)
# CAP_RUNTIME_LORA # 只有真的能 runtime 熱掛 LoRA 才加
# CAP_KV_TRANSFER # 只有真的能跨實例共享 KV 才加
})
# --- LoRA runtime 端點前綴(vLLM 是 "/v1",SGLang 是 "") ---
lora_endpoint_prefix = ""
# --- 目錄 metadata(GET /api/engines;前端靠它自動長出 UI,不要漏)---
metric_prefix = "myengine" # Prometheus 指標名前綴(myengine:*)
inapplicable_keys = frozenset({ # 這個引擎沒有、CLI builder 會 drop 的 model_config 鍵
"gpu_memory_utilization", ... # 前端會把這些欄位 grey out
})
paste_example = "CUDA_VISIBLE_DEVICES=0 my-engine-server --model ... --port 8099"
def keys(self, config) -> list[str]:
# 回傳「engine 屬於自己」的所有 <group>::<instance_id>
out = []
for tag, eng in config.LLM_engines.items():
if getattr(eng.settings, "engine", "vllm") != self.engine:
continue
out.extend(f"{tag}::{i.id}" for i in eng.instances)
return out
def build_spec(self, config, config_path, key) -> LaunchSpec:
# 解析 merged model_config -> LaunchSpec(見下)
...照抄現有 launcher 的 merge 樣板(merged = engine.settings.model_dump(); merged.update(inst.model_dump()),
單卡 cuda_device→CUDA_VISIBLE_DEVICES,merged.pop("id")),然後:
| 欄位 | 說明 / 坑 |
|---|---|
command |
["my-engine-server", *build_myengine_cli_args(cli_cfg)] |
host / port |
用 inst.host / inst.port;bind host 走 LLMOPS_VLLM_BIND_HOST(共用,0.0.0.0 供跨容器路由) |
probe_url |
http://{host}:{port}/health |
engine / capabilities |
self.engine / self.capabilities |
model_tag |
engine.settings.model_tag |
served_name |
merged.get("served_model_name") or engine.settings.model_tag — 🔴 漏了會害 boot adopt 認不得自己、在同 port 起第二份行程 → crash loop(見 §5) |
sleep_enabled |
只有支援 sleep 的引擎才 True,且要在 env 開對應 dev flag |
比照 build_llamacpp_cli_args:把 engine-neutral 的 model_config 鍵翻譯成該引擎的旗標。三個要點:
- skip/drop 清單:
_ROUTER_ONLY_KEYS(routing_strategy/kind/engine——永遠不進 CLI)必扣掉; 加上你引擎的_MYENGINE_SKIP_CLI_KEYS。🔴 跨引擎污染是真的:vLLM 的 LoRA 旗標會讓 llama-server 直接 exit——你的引擎也要把不認得的鍵 drop 掉,別原封不動透傳。 - 參數改名:像 llama.cpp 的
_LLAMACPP_PARAM_MAP = {"max_model_len": "ctx-size"}。 - bool 語意:注意 store_true 旗標有沒有
--no-xxx反向式(llama.cpp 沒有,別亂合成)。
inapplicable_keys(#1 的 metadata)通常 = 你的 _MYENGINE_DROP_KEYS,兩者對齊,前端才會把該遮的欄位遮掉。
# apps/backend/app/main.py (約 :129)
launchers = [VllmLauncher(), SglangLauncher(), LlamacppLauncher(), MyEngineLauncher(), EmbeddingLauncher()]所有 engine image 都註冊全部 launcher(build_spec 只是組指令、不需要該引擎裝在本機);哪個 node 真的跑得動,
由 LLMOPS_NODE_ENGINES gate。所以這行漏了 = 該引擎在每個 backend 都不存在。
# packages/config-schema/schema.py (約 :62)
engine: Literal["vllm", "sglang", "llamacpp", "myengine"] = "vllm"🔴 這個 Literal 必須 == 已註冊的 launcher 引擎名集合。 只加 Literal、忘了 launcher(或反過來)=
phantom engine:schema 驗過、但沒人認領 → 群組在 backend 靜默消失、router 照樣列在 /v1/models 並黑洞路由。
(trtllm 曾經就是這種鬼——P1 已清掉。)
保險:backend 啟動時 unregistered_engine_groups() 會對「有 engine、無 launcher」的群組記 loud error;
router 啟動時 unknown_metric_engines() 會對「有 engine、無指標表」warning。加引擎時看一下這兩條 log確認沒漏。
served_name沒填 → boot 重啟後,設了served_model_name的模型 adopt 失敗、同 port 起第二份 → bind 失敗 → FAILED → auto-restart 迴圈。build_spec一定要填served_name。- capability 亂宣告:
CAP_SLEEP/CAP_RUNTIME_LORA/CAP_KV_TRANSFER只有引擎真的支援才加。 router 的ENGINE_SLEEP_CAPABLE(#7)也要對齊——backend 說會 sleep、router 不去 probe/is_sleeping是典型的半套 drift。 - 指標名沒有獨特前綴 / router 表沒加:
METRIC_NAMES_BY_ENGINE缺你的引擎 → router 用 vLLM 名表解析 → 全 miss → (經 H#2)回 unreachable + warning。routing 會避開它、autoscaler 拿不到訊號。#6 一定要補。
# apps/router-server/src/llm_router/vllm_metrics_client.py
METRIC_NAMES_BY_ENGINE = {
...,
"myengine": {
"running": "myengine:requests_running",
"waiting": "myengine:requests_waiting",
"kv_cache_usage_perc": "myengine:kv_usage", # 沒有就指一個不存在的名(→ 0.0),別亂填
"prompt_tokens": "myengine:prompt_tokens_total",
"generation_tokens": "myengine:generation_tokens_total",
},
}
ENGINE_SLEEP_CAPABLE = frozenset({"vllm", ...}) # 只有真的有 sleep/wake 才加 myenginemetrics_poller 會自動用 model_config.engine 選對的指標表,無需再改。backend 若有定義新 CAP_METRICS_MYENGINE
常數,記得在 launchers.py 頂部宣告。
- Dockerfile:
deploy/engine-myengine.Dockerfile,FROM <該引擎官方 base image>,把 backend/router 程式碼 COPY 進去(照engine-llamacpp.Dockerfile抄——注意 base 可能自帶錯的 HEALTHCHECK/ENTRYPOINT 要清、LD_LIBRARY_PATH之類的坑)。 - compose(
docker-compose.mixed.yaml):加一個myengine-backendservice:myengine-backend: profiles: ["myengine"] build: { context: .., dockerfile: deploy/engine-myengine.Dockerfile } environment: - LLMOPS_NODE_ENGINES=myengine - LLMOPS_PROMETHEUS_SD_PATH=/sd/targets-myengine.json # Prometheus glob 自動抓,不用改 scrape config - LLMOPS_NODE_HOST=mixed-myengine-backend - ...(照其他 backend 抄)
- Makefile:
_ALL_ENGINES := vllm sglang llamacpp myengine(讓make up-mixed ENGINES=...認得它、 切換時能正確停掉)。
Prometheus / file_sd / compose profile / ENGINES= 都已參數化,以上就是全部。
tests/unit/test_launchers.py:新 launcher 的keys()/build_spec()(含served_name)、CLI builder 的參數翻譯 + skip 清單。tests/unit/test_vllm_command.py:若有寫 paste 解析器。tests/api/test_engines_routes.py:GET /api/engines會自動帶出新引擎(capability/metadata 對不對)。- router
tests/unit/test_vllm_metrics_client.py:新指標表能正規化成VLLMInstanceMetrics形狀。 - 加完跑
make test(backend + router + schema)。
-
MyEngineLauncher:engine、capabilities、lora_endpoint_prefix、metric_prefix、inapplicable_keys、paste_example、keys()、build_spec()(含served_name)。 - CLI builder 純函式 + skip/drop 清單(扣掉
_ROUTER_ONLY_KEYS+ 自己不認得的鍵)。 -
main.py註冊 launcher。 - schema
Literal加引擎名(== launcher 集合)。 - router
METRIC_NAMES_BY_ENGINE+(如支援)ENGINE_SLEEP_CAPABLE。 -
engine-myengine.Dockerfile+ compose service(profile / NODE_ENGINES / SD path)+ Makefile 清單。 - 測試齊全,
make test綠。 - 啟動看 backend
unregistered_engine_groups/ routerunknown_metric_engines兩條 log 沒有警告。 - 前端 Add Model 選單自動出現新引擎、capability 欄位正確(不用改前端;若沒出現→
/api/engines沒帶到,回頭查 metadata)。
若第 N 個引擎不符合 §0 契約,先做這些 protocol 擴充(現有引擎不實作、行為零變):
Launcher.available():shutil.which(...)/ import probe 檢查 binary 在不在本 image,collapsed 模式 create/start 時直接明確拒絕(取代「crash 三次才知道 image 沒裝」)。async Launcher.prepare(spec):啟動前置(TRT engine build / artifact 下載),manager 在 spawn 前呼叫, 期間 state 停 STARTING。Launcher.readiness_probe(spec):覆寫預設/health(自訂路徑/判準)。model_config.engine_args巢狀命名空間:原生旗標放這裡,engine-neutral 鍵留頂層,skip/drop 清單不再隨 引擎數線性成長。