Skip to content

Latest commit

 

History

History
257 lines (199 loc) · 13.6 KB

File metadata and controls

257 lines (199 loc) · 13.6 KB

新增一個推理引擎要注意什麼(實作指南)

對象:想在既有的 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


0. 先確認:你的引擎符合「隱含契約」嗎?

現有的 Launcher / LaunchSpec 模型隱含假設每個引擎是這樣的(vLLM/SGLang/llama.cpp 剛好都符合):

  1. 單行程、用 CLI 旗標啟動command: list[str]spawn_process 拉起,停止用 process-group kill。
  2. HTTP /health 200 = ready — reconciler 用通用 probe(llama.cpp 是「載入後才綁 port」→ 啟動期 connection-refused,也被當 not-ready,OK)。
  3. OpenAI 相容 /v1,且 /v1/models 廣告 served name(router 轉發、boot adopt 身分驗證都靠它)。
  4. Prometheus text /metrics,指標名有獨特前綴(vllm: / sglang: / llamacpp:)。
  5. 啟動進度反映在 log 檔成長 — progress-aware timeout 靠 os.path.getsize(log)
  6. 綁 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。


1. 觸點總表(照這張清單逐項做)

✅ = 設計良好、純增量;⚠️ = 需複製一份表;🔴 = 做錯會靜默出事。

# 檔案 要做什麼 級別
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
4 apps/backend/app/services/vllm_command.py (選配)paste-command 解析器 + sniff 規則 ⚠️
5 apps/backend/app/perf/manager.pyapp/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.vueModelGroupCard.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_loralora_endpoint_prefix、reconciler/probe 是通用 /health、scheduler node_supports() 對引擎名是 不透明字串、_defer_to_owner 引擎無關、Prometheus file_sd 用 glob 自動發現 targets-*.json這些是 「架構對了」的部分,不要去碰。


2. 寫 Launcher 類別(#1,主要工作)

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(見下)
        ...

build_spec 必須填對的 LaunchSpec 欄位

照抄現有 launcher 的 merge 樣板(merged = engine.settings.model_dump(); merged.update(inst.model_dump()), 單卡 cuda_deviceCUDA_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

CLI builder(純函式,好單測)

比照 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,兩者對齊,前端才會把該遮的欄位遮掉。


3. 註冊(#2)—— 漏了就「引擎不存在」

# apps/backend/app/main.py  (約 :129)
launchers = [VllmLauncher(), SglangLauncher(), LlamacppLauncher(), MyEngineLauncher(), EmbeddingLauncher()]

所有 engine image 都註冊全部 launcher(build_spec 只是組指令、不需要該引擎裝在本機);哪個 node 真的跑得動, 由 LLMOPS_NODE_ENGINES gate。所以這行漏了 = 該引擎在每個 backend 都不存在。


4. schema Literal(#3)—— 別造出 phantom engine

# 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確認沒漏。


5. 三個最容易踩的坑

  1. served_name 沒填 → boot 重啟後,設了 served_model_name 的模型 adopt 失敗、同 port 起第二份 → bind 失敗 → FAILED → auto-restart 迴圈。build_spec 一定要填 served_name
  2. capability 亂宣告:CAP_SLEEP / CAP_RUNTIME_LORA / CAP_KV_TRANSFER 只有引擎真的支援才加。 router 的 ENGINE_SLEEP_CAPABLE(#7)也要對齊——backend 說會 sleep、router 不去 probe /is_sleeping 是典型的半套 drift。
  3. 指標名沒有獨特前綴 / router 表沒加:METRIC_NAMES_BY_ENGINE 缺你的引擎 → router 用 vLLM 名表解析 → 全 miss → (經 H#2)回 unreachable + warning。routing 會避開它、autoscaler 拿不到訊號。#6 一定要補。

6. Router(#6、#7)

# 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 才加 myengine

metrics_poller 會自動用 model_config.engine 選對的指標表,無需再改。backend 若有定義新 CAP_METRICS_MYENGINE 常數,記得在 launchers.py 頂部宣告。


7. Deploy(#11、#12、#13)

  • 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-backend service:
    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= 都已參數化,以上就是全部。


8. 測試(照現有樣板)

  • 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)。

9. 驗收清單(TL;DR)

  • MyEngineLauncher:enginecapabilitieslora_endpoint_prefixmetric_prefixinapplicable_keyspaste_examplekeys()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 / router unknown_metric_engines 兩條 log 沒有警告。
  • 前端 Add Model 選單自動出現新引擎、capability 欄位正確(不用改前端;若沒出現→ /api/engines 沒帶到,回頭查 metadata)。

附:非典型引擎的 roadmap(§0 踩線時)

若第 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 清單不再隨 引擎數線性成長。

詳見 multi-engine-extensibility-review_zh-TW.md §5 P2。