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