Skip to content

LLM y AsyncLLM: dos entradas, offline y de servicio

源码版本v0.25.1

Responsabilidades

LLM es la API Python síncrona que vLLM ofrece para escenarios de inferencia offline (llm.py:66-67),y AsyncLLM es el envoltorio asíncrono que usa el OpenAI API server en el lado de servicio (async_llm.py:70-71). Ambos heredan de la abstracción EngineClient, pero por debajo usan distintas implementaciones de EngineCoreClient: LLM va por defecto por SyncMPClient o InprocClient, mientras que AsyncLLM usa AsyncMPClient (o DPLBAsyncMPClient en el escenario DP). Ninguna de las dos clases ejecuta el forward directamente — son contenedores del trío InputProcessor, OutputProcessor y EngineCoreClient.

LLM.__init__ (llm.py:176-222) acepta casi todos los campos de EngineArgs como kwargs,y los vuelve a empaquetar en un EngineArgs(model=..., tensor_parallel_size=..., ...) (llm.py:305-345),que luego se entrega a LLMEngine.from_engine_args(engine_args, usage_context=UsageContext.LLM_CLASS) (llm.py:349-351). LLMEngine es en v1 una cáscara fina que preserva compatibilidad hacia atrás (llm_engine.py:48-49);internamente coloca como self.engine_core el cliente que devuelve EngineCoreClient.make_client(...) (llm_engine.py:105). Por eso, una llamada a LLM.generate() termina en un bucle de llm_engine.add_request + llm_engine.step.

AsyncLLM sigue otra ruta de ensamblaje. build_async_engine_client_from_engine_args (api_server.py:109-154) primero llama a engine_args.create_engine_config() para obtener VllmConfig,y luego AsyncLLM.from_vllm_config(vllm_config, ...);esta última invoca directamente EngineCoreClient.make_async_mp_client(...) (async_llm.py:146-153) para levantar un AsyncMPClient (o DPLBAsyncMPClient en el caso DP). AsyncLLM añade una corrutina residente output_handler (async_llm.py:637-660) que continuamente ejecuta get_output_async() para traer de vuelta al frontend la salida de EngineCore.

Motivación de diseño

  • Un mismo schema EngineArgs sirve a las dos rutas: LLM construye directamente EngineArgs(model=model, ...) (llm.py:305-345),mientras que AsyncLLM proviene de AsyncEngineArgs.from_cli_args(args) (api_server.py:95) — ambos reutilizan la misma definición de campos que se describe en /startup/engine-args,así que offline y servidor se comportan igual.
  • LLM es una cáscara síncrona: LLM.generate_run_completion_render_and_add_requests_add_request (offline_utils.py:552-571) → llm_engine.add_request, y luego _run_engine con while has_unfinished_requests(): step_outputs = self.llm_engine.step() (offline_utils.py:594-595) que impulsa el bucle. Así, en un script basta un for y el usuario no toca asyncio.
  • AsyncLLM esconde los bloqueos dentro de output_handler: add_request devuelve de inmediato un RequestOutputCollector (async_llm.py:280-297),y el flujo real de tokens se procesa en chunks dentro de output_handler en background mediante engine_core.get_output_async() (async_llm.py:656-676). VLLM_V1_OUTPUT_PROC_CHUNK_SIZE corta una salida enorme en porciones para evitar que el event loop quede bloqueado por un batch demasiado grande.
  • LLMEngine en v1 es solo una capa de compatibilidad: el docstring de class LLMEngine lo dice directamente: Legacy LLMEngine for backwards compatibility. (llm_engine.py:48-49).Su __init__ llama a EngineCoreClient.make_client(multiprocess_mode, asyncio_mode=False, ...),unificando el modo síncrono multiproceso y el modo en proceso. make_client (core_client.py:83-105) usa dos bools para escoger InprocClient / SyncMPClient / AsyncMPClient.
  • make_async_mp_client elige el cliente DP automáticamente: con data_parallel_size > 1 y data_parallel_external_lb se va por DPAsyncMPClient (un cliente por cada rank DP);si no,va por DPLBAsyncMPClient (con LB interno que reparte la petición entre todos los ranks DP) (core_client.py:116-132) — esta es la entrada de paralelismo de datos en v1.
  • LLM.__init__ normaliza la configuración: cuando compilation_config es un int se envuelve en CompilationConfig(mode=CompilationMode(int)) (llm.py:275-282);cuando kv_transfer_config es un dict se convierte a KVTransferConfig (llm.py:245-262);y cuando worker_cls es una clase se serializa con cloudpickle (llm.py:238-243) — estas conversiones ahorran boilerplate a quien invoca al SDK.
  • El classmethod from_engine_args unifica la entrada: LLM.from_engine_args (llm.py:387-389),AsyncLLM.from_engine_args (async_llm.py:232-254) y LLMEngine.from_engine_args (llm_engine.py:161-186) recorren los mismos pasos engine_args.create_engine_config()Executor.get_class(vllm_config),garantizando que la derivación de VllmConfig sea idéntica sin importar la entrada.

Archivos clave

Flujo de datos

LLM.generate(prompts) offline

Una vez generate (llm.py:422) valida runner_type,delega en OfflineInferenceMixin._run_completion,que llama a _render_and_add_requests (offline_utils.py:523);ahí cada prompt se procesa por el renderer hasta producir un EngineInput y luego _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,
    )

Nótese params.output_kind = RequestOutputKind.FINAL_ONLY — el escenario offline solo se preocupa por el resultado final y no por el flujo de tokens intermedio,lo que permite ahorrar en el lado de salida de EngineCore todo el coste de chunked-streaming. Tras _add_request, _run_engine impulsa EngineCore con un bucle síncrono:

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() internamente es un envoltorio fino sobre engine_core.get_output() + output_processor.process_outputs;cuando engine_core es InprocClient llama síncronamente a step_fn(),y cuando es SyncMPClient se bloquea recibiendo por ZMQ. En todo el flujo no hay asyncio, así que un script con for output in llm.generate(...) también funciona.

AsyncLLM.add_request en el lado servidor

El AsyncLLM del lado servidor sigue una ruta completamente distinta. build_async_engine_client_from_engine_args (api_server.py:109-154) convierte AsyncEngineArgs en VllmConfig,y luego llama a AsyncLLM.from_vllm_config (async_llm.py:203-229). Un paso clave del constructor es:

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) escoge AsyncMPClient o DPLBAsyncMPClient según la topología DP. A partir de ahí, la corrutina output_handler se ejecuta continuamente en background:

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

En el lado de la petición, una ruta OpenAI llama a async_llm.add_request(request_id, prompt, params, ...) (async_llm.py:280-297) y obtiene de inmediato un RequestOutputCollector,que luego solo necesita await para tener el stream. Mientras tanto,EngineCore ejecuta el forward,la salida vuelve por ZMQ al AsyncMPClient,y output_handler la extrae,la procesa en chunks y la empuja al collector — toda la cadena es totalmente asíncrona,por lo que el API server no se bloquea con una petición lenta.

Límites y fallos

  • LLM.generate no soporta un runner distinto de generate: cuando runner_type != "generate" lanza una excepción (llm.py:465-471) y sugiere usar --runner generate;LLM.chat y LLM.enqueue_chat hacen la misma comprobación (llm.py:684-689).
  • LLM(data_parallel_size>1) no permite un único proceso: con _dp_size > 1 y sin external_launcher ni TPU se lanza excepción (llm.py:295-303);se recomienda seguir el esquema multiproceso de examples/features/data_parallel/data_parallel_offline.py,que de otro modo se colgaría.
  • renderer_num_workers > 1 no hace nada en el LLM offline: LLM va por la ruta síncrona del renderer,y el pool de hilos multi-worker solo se consume en la ruta asíncrona de vllm serve / AsyncLLM;en el escenario offline se emite explícitamente warning_once (llm.py:370-379) para que el usuario no crea que está activando multihilo.
  • EngineDeadError: al inicio de AsyncLLM.add_request se comprueba self.errored (async_llm.py:300-301);si el proceso EngineCore ya ha muerto (en background se emitió ENGINE_CORE_DEAD),se lanza EngineDeadError (async_llm.py:1054-1058) sin aceptar nuevas peticiones.
  • async_llm.shutdown como red de seguridad: el bloque finally de build_async_engine_client_from_engine_args llama a async_llm.shutdown(timeout=vllm_config.shutdown_timeout) (api_server.py:152-154),de modo que incluso si el build lanza una excepción se limpian los procesos en background y los sockets ZMQ. AsyncLLM.__del__ (async_llm.py:256-257) también invoca shutdown como red adicional para evitar fugas durante el GC.
  • Validación de kv_transfer_config dict: en LLM.__init__,cuando la conversión de dict a KVTransferConfig falla,se hace logger.error y se lanza ValueError(f"Invalid 'kv_transfer_config' provided: {e}") (llm.py:253-262) envolviendo el ValidationError original en un error más amable.
  • swap_space está obsoleto: cuando swap_space aparece en kwargs se extrae con pop y se emite DeprecationWarning (llm.py:224-233);se eliminará en el futuro.
  • Degradación cuando output_handler no arranca: si en __init__ asyncio.get_running_loop() lanza RuntimeError (no hay event loop en ejecución),se omite output_handler (async_llm.py:170-176). En ese caso el usuario debe invocar explícitamente el equivalente de start_engine_loop=True o introducir AsyncLLM en un event loop antes de hacer await.
  • Selección estática del cliente DP: make_async_mp_client solo mira parallel_config.data_parallel_size y data_parallel_external_lb (core_client.py:126-132),sin soportar cambio de modo LB en runtime — la topología queda fijada al arrancar.

Resumen

LLM y AsyncLLM son las dos entradas principales que vLLM expone hacia arriba (scripts y OpenAI server);ambas son implementaciones de la abstracción EngineClient pero con distintos EngineCoreClient. LLM va por la ruta síncrona, internamente LLMEngine + SyncMPClient/InprocClient, y el usuario obtiene un iterador síncrono tipo for output in llm.generate(...); AsyncLLM va por la ruta asíncrona, internamente AsyncMPClient/DPLBAsyncMPClient + la corrutina output_handler en background, donde cada petición HTTP solo hace await sobre un RequestOutputCollector. Ambos obtienen VllmConfig a partir de EngineArgs.create_engine_config(),por lo que la configuración es totalmente consistente;la diferencia solo está en la selección del cliente y del modelo de event loop. Cómo se ensambla EngineArgs / VllmConfig se ve en /startup/engine-args;cómo la cadena del CLI llega a AsyncLLM.from_vllm_config se ve en /startup/cli;el proceso en background de EngineCore y su ZMQ interno se ve en /engine/engine-core,y los detalles de las tres implementaciones de EngineCoreClient en /client/inproc-mp.

Véase la documentación oficial: vLLM 文档 · README.