LLM 與 AsyncLLM:離線/服務兩條入口
職責
LLM 是 vLLM 提供給離線推理 (offline inference) 場景的同步 Python API(llm.py:66-67),AsyncLLM 是服務場景下 OpenAI API server 用的非同步包裝(async_llm.py:70-71)。兩者都繼承自 EngineClient 抽象,但走的是不同的 EngineCoreClient 實現:LLM 預設走 SyncMPClient 或 InprocClient,AsyncLLM 走 AsyncMPClient(或 DP 場景下的 DPLBAsyncMPClient)。這兩個類本身都不直接跑前向——它們是 InputProcessor、OutputProcessor、EngineCoreClient 三件套的容器。
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_engine用while 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_handler裡engine_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 > 1且data_parallel_external_lb走DPAsyncMPClient(每個 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 类:66-67—class LLM(BeamSearchOfflineMixin, PoolingOfflineMixin, OfflineInferenceMixin)定義。LLM.__init__:176-222— 接受近 40 個 kwarg +**kwargs,內部把 dict/int 轉成對應 config 實例。EngineArgs 装配:305-345— 把 LLM kwarg 全部塞進EngineArgs(model=..., ...),然後log_non_default_args。LLMEngine.from_engine_args:349-353—usage_context=UsageContext.LLM_CLASS,拿llm_engine.model_config和supported_tasks。LLM.from_engine_args:387-389—cls(**vars(engine_args))一行直接複用構造函數。LLM.generate:422-485— 校驗runner_type == "generate",預設 sampling params,委託給_run_completion。_add_request:552-571—params.output_kind = RequestOutputKind.FINAL_ONLY,調llm_engine.add_request。_run_engine:573-595—while self.llm_engine.has_unfinished_requests(): step_outputs = self.llm_engine.step()同步循環。LLMEngine 类:48-49—Legacy LLMEngine for backwards compatibility.的 v1 薄殼。make_client 调用:105-111—self.engine_core = EngineCoreClient.make_client(multiprocess_mode, asyncio_mode=False, ...)。LLMEngine.from_engine_args:161-186— 跑engine_args.create_engine_config,按VLLM_ENABLE_V1_MULTIPROCESSING決定是否開多行程。make_client:83-105— 兩個 bool 選InprocClient/SyncMPClient/AsyncMPClient。make_async_mp_client:109-132— DP 場景自動選DPAsyncMPClient或DPLBAsyncMPClient。AsyncLLM 类:70-71—class AsyncLLM(EngineClient):定義。AsyncLLM.__init__:73-176—InputProcessor、OutputProcessor、EngineCoreClient.make_async_mp_client三件套裝配。AsyncLLM.from_vllm_config:203-229— 從VllmConfig+Executor.get_class直接構造,服務端入口。AsyncLLM.from_engine_args:232-254—engine_args.create_engine_config→Executor.get_class→cls(...)。_run_output_handler:637-676— 後臺協程,get_output_async+output_processor.process_outputs,chunk size 由VLLM_V1_OUTPUT_PROC_CHUNK_SIZE控制。build_async_engine_client:78-105—AsyncEngineArgs.from_cli_args→build_async_engine_client_from_engine_args。build_async_engine_client_from_engine_args:109-154—engine_args.create_engine_config→AsyncLLM.from_vllm_config,finally 裡async_llm.shutdown(timeout=vllm_config.shutdown_timeout)。AsyncLLM.shutdown:259-271—shutdown_prometheus、renderer.shutdown、engine_core.shutdown、canceloutput_handler。
資料流
離線 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:
# 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:
# 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_core 是 InprocClient 時直接同步調 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)。構造函數里關鍵的一步是:
# 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 拓撲選 AsyncMPClient 或 DPLBAsyncMPClient。這之後 output_handler 協程在後臺持續運行:
# 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.chat和LLM.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_args的finally塊裡調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_configdict 校驗:LLM.__init__把 dict 轉成KVTransferConfig時失敗會logger.error後 raiseValueError(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_size和data_parallel_external_lb(core_client.py:126-132),不支持運行時切換 LB 模式——拓撲在啟動時就定死。
小結
LLM 和 AsyncLLM 是 vLLM 暴露給上層(腳本和 OpenAI server)的兩條主入口,都是 EngineClient 抽象的實現但走不同的 EngineCoreClient。LLM 走同步路徑,內部是 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。