Skip to content

LLM 與 AsyncLLM:離線/服務兩條入口

源码版本v0.25.1

職責

LLM 是 vLLM 提供給離線推理 (offline inference) 場景的同步 Python API(llm.py:66-67),AsyncLLM 是服務場景下 OpenAI API server 用的非同步包裝(async_llm.py:70-71)。兩者都繼承自 EngineClient 抽象,但走的是不同的 EngineCoreClient 實現:LLM 預設走 SyncMPClientInprocClient,AsyncLLMAsyncMPClient(或 DP 場景下的 DPLBAsyncMPClient)。這兩個類本身都不直接跑前向——它們是 InputProcessorOutputProcessorEngineCoreClient 三件套的容器。

LLM.__init__(llm.py:176-222)接受幾乎所有 EngineArgs 欄位作為 kwarg,把它們重新打包成一個 EngineArgs(model=..., tensor_parallel_size=..., ...)(llm.py:305-345),再交給 LLMEngine.from_engine_args(engine_args, usage_context=UsageContext.LLM_CLASS)(llm.py:349-351)。LLMEngine 是 v1 裡一個保留向後兼容的薄殼(llm_engine.py:48-49),它內部把 EngineCoreClient.make_client(...)(llm_engine.py:105)選出來的客戶端當成 self.engine_core。所以 LLM.generate() 調下去最終就是 llm_engine.add_request + llm_engine.step 的循環。

AsyncLLM 走另外一條裝配路徑。build_async_engine_client_from_engine_args(api_server.py:109-154)先 engine_args.create_engine_config() 拿到 VllmConfig,再 AsyncLLM.from_vllm_config(vllm_config, ...),後者內部直接 EngineCoreClient.make_async_mp_client(...)(async_llm.py:146-153)起 AsyncMPClient(或者 DP 場景的 DPLBAsyncMPClient)。AsyncLLM 多一個常駐的 output_handler 協程(async_llm.py:637-660)不斷 get_output_async() 把 EngineCore 的輸出拉回前端。

設計動機

  • 同一份 EngineArgs schema 服務兩條路:LLM 構造時直接 EngineArgs(model=model, ...)(llm.py:305-345),AsyncLLM 來自 AsyncEngineArgs.from_cli_args(args)(api_server.py:95)——兩者都複用 /startup/engine-args 裡那一份欄位定義,離線和服務跑出來行為一致。
  • LLM 是同步薄殼:LLM.generate_run_completion_render_and_add_requests_add_request(offline_utils.py:552-571) → llm_engine.add_request,然後 _run_enginewhile has_unfinished_requests(): step_outputs = self.llm_engine.step()(offline_utils.py:594-595)循環驅動。這樣腳本場景一個 for 就能寫完,使用者不用碰 asyncio。
  • AsyncLLM 把阻塞藏進 output_handler:add_request 立刻返回一個 RequestOutputCollector(async_llm.py:280-297),真正的 token 流在後臺 output_handlerengine_core.get_output_async() 拉回來 chunk 處理(async_llm.py:656-676)。VLLM_V1_OUTPUT_PROC_CHUNK_SIZE 把一大坨輸出切片,防止事件循環被一個超大 batch 卡住。
  • LLMEngine 在 v1 只是兼容層:class LLMEngine 的 docstring 直接寫 Legacy LLMEngine for backwards compatibility.(llm_engine.py:48-49)。它的 __init__ 調 EngineCoreClient.make_client(multiprocess_mode, asyncio_mode=False, ...),把同步多行程 / 同行程兩種模式合一。make_client(core_client.py:83-105)用兩個 bool 選出 InprocClient / SyncMPClient / AsyncMPClient
  • make_async_mp_client 自動選 DP 客戶端:data_parallel_size > 1data_parallel_external_lbDPAsyncMPClient(每個 DP rank 一個 client);否則走 DPLBAsyncMPClient(內部 LB 把請求分發到所有 DP rank)(core_client.py:116-132)。這是 v1 資料並行入口。
  • LLM.__init__ 做配置歸一:compilation_config 是 int 時自動包成 CompilationConfig(mode=CompilationMode(int))(llm.py:275-282),kv_transfer_config 是 dict 時轉 KVTransferConfig(llm.py:245-262),worker_cls 是 class 時 cloudpickle 序列化(llm.py:238-243),這些轉換讓 SDK 呼叫方少寫樣板代碼。
  • from_engine_args 類方法統一入口:LLM.from_engine_args(llm.py:387-389)、AsyncLLM.from_engine_args(async_llm.py:232-254)、LLMEngine.from_engine_args(llm_engine.py:161-186)三個類方法都走 engine_args.create_engine_config()Executor.get_class(vllm_config),保證不同入口下 VllmConfig 的派生過程完全一樣。

關鍵檔案

資料流

離線 LLM.generate(prompts)

generate(llm.py:422)校驗完 runner_type 後委託給 OfflineInferenceMixin._run_completion,後者調 _render_and_add_requests(offline_utils.py:523),裡面把每條 prompt 經過 renderer 加工成 EngineInput_add_request:

python
# vllm/entrypoints/offline_utils.py L552-L571
def _add_request(
    self,
    prompt: EngineInput,
    params: SamplingParams | PoolingParams,
    lora_request: LoRARequest | None = None,
    priority: int = 0,
) -> str:
    if isinstance(params, SamplingParams):
        # We only care about the final output
        params.output_kind = RequestOutputKind.FINAL_ONLY

    request_id = str(next(self.request_counter))

    return self.llm_engine.add_request(
        request_id,
        prompt,
        params,
        lora_request=lora_request,
        priority=priority,
    )

注意 params.output_kind = RequestOutputKind.FINAL_ONLY——離線場景只關心最終結果,不要中間 token 流,這樣 EngineCore 輸出端可以省一堆 chunked-streaming 的開銷。_add_request 之後,_run_engine 用同步循環驅動 EngineCore:

python
# vllm/entrypoints/offline_utils.py L590-L599
# Run the engine.
outputs: list[_O] = []
total_in_toks = 0
total_out_toks = 0
while self.llm_engine.has_unfinished_requests():
    step_outputs = self.llm_engine.step()
    for output in step_outputs:
        assert isinstance(output, output_type)
        if output.finished:
            outputs.append(output)

LLMEngine.step() 內部就是 engine_core.get_output() + output_processor.process_outputs 的薄包裝;engine_coreInprocClient 時直接同步調 step_fn(),是 SyncMPClient 時就阻塞收 ZMQ。整個流程沒有 asyncio,所以腳本里寫 for output in llm.generate(...) 也能跑。

服務 AsyncLLM.add_request

服務端 AsyncLLM 走完全不同的路徑。build_async_engine_client_from_engine_args(api_server.py:109-154)把 AsyncEngineArgs 變成 VllmConfig,再 AsyncLLM.from_vllm_config(async_llm.py:203-229)。構造函數里關鍵的一步是:

python
# vllm/v1/engine/async_llm.py L146-L153
# EngineCore (starts the engine in background process).
self.engine_core = EngineCoreClient.make_async_mp_client(
    vllm_config=vllm_config,
    executor_class=executor_class,
    log_stats=self.log_stats,
    client_addresses=client_addresses,
    client_count=client_count,
    client_index=client_index,
)

make_async_mp_client(core_client.py:109-132)按 DP 拓撲選 AsyncMPClientDPLBAsyncMPClient。這之後 output_handler 協程在後臺持續運行:

python
# vllm/v1/engine/async_llm.py L656-L676
async def output_handler():
    try:
        while True:
            # 1) Pull EngineCoreOutputs from the EngineCore.
            outputs = await engine_core.get_output_async()
            num_outputs = len(outputs.outputs)

            iteration_stats = (
                IterationStats() if (log_stats and num_outputs) else None
            )

            # Split outputs into chunks of at most
            # VLLM_V1_OUTPUT_PROC_CHUNK_SIZE, so that we don't block the
            # event loop for too long.
            engine_core_outputs = outputs.outputs
            for start in range(0, num_outputs, chunk_size):
                end = start + chunk_size
                outputs_slice = engine_core_outputs[start:end]
                # 2) Process EngineCoreOutputs.
                processed_outputs = output_processor.process_outputs(
                    outputs_slice, outputs.timestamp, iteration_stats

請求側 OpenAI 路由調 async_llm.add_request(request_id, prompt, params, ...)(async_llm.py:280-297)立刻拿到 RequestOutputCollector,後續 await 它的流就行。EngineCore 那邊跑前向、輸出經 ZMQ 迴流到 AsyncMPClient,再被 output_handler 拉出來 chunk 處理後 push 到 collector——整個鏈路是全非同步的,API server 不會被一個慢請求卡住。

邊界與失敗

  • LLM.generate 不支持非 generate runner:runner_type != "generate" 時直接 raise(llm.py:465-471),提示改用 --runner generate;LLM.chatLLM.enqueue_chat 同樣檢查(llm.py:684-689)。
  • LLM(data_parallel_size>1) 不允許單行程:_dp_size > 1 且不是 external_launcher、不是 TPU 時直接 raise(llm.py:295-303),建議走 examples/features/data_parallel/data_parallel_offline.py 的多行程方案,否則會 hang。
  • renderer_num_workers > 1 在離線 LLM 上無效:LLM 走同步 renderer 路徑,多 worker 執行緒池只在 vllm serve / AsyncLLM 的非同步路徑裡被消費,離線場景顯式 warning_once(llm.py:370-379),免得使用者以為開了多執行緒。
  • EngineDeadError:AsyncLLM.add_request 開頭檢測 self.errored(async_llm.py:300-301),如果 EngineCore 行程已經死了(後臺發過 ENGINE_CORE_DEAD),直接拋 EngineDeadError(async_llm.py:1054-1058),不收新請求。
  • async_llm.shutdown 兜底:build_async_engine_client_from_engine_argsfinally 塊裡調 async_llm.shutdown(timeout=vllm_config.shutdown_timeout)(api_server.py:152-154),即使 build 過程拋錯也會清理後臺行程和 ZMQ socket。AsyncLLM.__del__(async_llm.py:256-257)也兜底調 shutdown,防 GC 時洩漏。
  • kv_transfer_config dict 校驗:LLM.__init__ 把 dict 轉成 KVTransferConfig 時失敗會 logger.error 後 raise ValueError(f"Invalid 'kv_transfer_config' provided: {e}")(llm.py:253-262),把原始 ValidationError 包成更友好的錯誤。
  • swap_space 已廢棄:kwargs 裡出現 swap_space 時 pop 掉並 DeprecationWarning(llm.py:224-233),未來會刪除。
  • output_handler 沒啟動的退化:在 __init__asyncio.get_running_loop()RuntimeError(沒有運行中的事件循環)就跳過 output_handler(async_llm.py:170-176)。這種情況下使用者得自己顯式調 start_engine_loop=True 等價物,或者把 AsyncLLM 拿進 event loop 裡再 await
  • DP 客戶端選擇靜態:make_async_mp_client 僅看 parallel_config.data_parallel_sizedata_parallel_external_lb(core_client.py:126-132),不支持運行時切換 LB 模式——拓撲在啟動時就定死。

小結

LLMAsyncLLM 是 vLLM 暴露給上層(腳本和 OpenAI server)的兩條主入口,都是 EngineClient 抽象的實現但走不同的 EngineCoreClientLLM 走同步路徑,內部是 LLMEngine + SyncMPClient/InprocClient,使用者拿到的就是一個 for output in llm.generate(...) 風格的同步迭代器;AsyncLLM 走非同步路徑,內部是 AsyncMPClient/DPLBAsyncMPClient + 後臺 output_handler 協程,API server 每個 HTTP 請求只 await 一個 RequestOutputCollector。兩者都從 EngineArgs.create_engine_config() 拿到 VllmConfig,所以配置側完全一致;差異只在客戶端選擇和事件循環模型。EngineArgs / VllmConfig 怎麼被組裝見 /startup/engine-args,CLI 那條鏈怎麼落到 AsyncLLM.from_vllm_config/startup/cli,EngineCore 後臺行程和 ZMQ 內部見 /engine/engine-core,三種 EngineCoreClient 實現的細節見 /client/inproc-mp

對照官方資料:vLLM 文件 · README