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。