LLM y AsyncLLM: dos entradas, offline y de servicio
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:
LLMconstruye directamenteEngineArgs(model=model, ...)(llm.py:305-345),mientras queAsyncLLMproviene deAsyncEngineArgs.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. LLMes 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_engineconwhile has_unfinished_requests(): step_outputs = self.llm_engine.step()(offline_utils.py:594-595) que impulsa el bucle. Así, en un script basta unfory el usuario no toca asyncio.AsyncLLMesconde los bloqueos dentro de output_handler:add_requestdevuelve de inmediato unRequestOutputCollector(async_llm.py:280-297),y el flujo real de tokens se procesa en chunks dentro deoutput_handleren background medianteengine_core.get_output_async()(async_llm.py:656-676).VLLM_V1_OUTPUT_PROC_CHUNK_SIZEcorta una salida enorme en porciones para evitar que el event loop quede bloqueado por un batch demasiado grande.LLMEngineen v1 es solo una capa de compatibilidad: el docstring declass LLMEnginelo dice directamente:Legacy LLMEngine for backwards compatibility.(llm_engine.py:48-49).Su__init__llama aEngineCoreClient.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 escogerInprocClient/SyncMPClient/AsyncMPClient.make_async_mp_clientelige el cliente DP automáticamente: condata_parallel_size > 1ydata_parallel_external_lbse va porDPAsyncMPClient(un cliente por cada rank DP);si no,va porDPLBAsyncMPClient(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: cuandocompilation_configes un int se envuelve enCompilationConfig(mode=CompilationMode(int))(llm.py:275-282);cuandokv_transfer_configes un dict se convierte aKVTransferConfig(llm.py:245-262);y cuandoworker_clses una clase se serializa con cloudpickle (llm.py:238-243) — estas conversiones ahorran boilerplate a quien invoca al SDK.- El classmethod
from_engine_argsunifica la entrada:LLM.from_engine_args(llm.py:387-389),AsyncLLM.from_engine_args(async_llm.py:232-254) yLLMEngine.from_engine_args(llm_engine.py:161-186) recorren los mismos pasosengine_args.create_engine_config()→Executor.get_class(vllm_config),garantizando que la derivación deVllmConfigsea idéntica sin importar la entrada.
Archivos clave
LLM class:66-67— definiciónclass LLM(BeamSearchOfflineMixin, PoolingOfflineMixin, OfflineInferenceMixin).LLM.__init__:176-222— acepta casi 40 kwargs +**kwargs,convierte dict/int en instancias de config correspondientes.EngineArgs assembly:305-345— vuelca todos los kwargs de LLM enEngineArgs(model=..., ...)y luegolog_non_default_args.LLMEngine.from_engine_args:349-353—usage_context=UsageContext.LLM_CLASS,obtienellm_engine.model_configysupported_tasks.LLM.from_engine_args:387-389—cls(**vars(engine_args))reutiliza directamente el constructor en una línea.LLM.generate:422-485— validarunner_type == "generate",aplica sampling params por defecto y delega en_run_completion._add_request:552-571—params.output_kind = RequestOutputKind.FINAL_ONLY,llama allm_engine.add_request._run_engine:573-595—while self.llm_engine.has_unfinished_requests(): step_outputs = self.llm_engine.step()bucle síncrono.LLMEngine class:48-49— cáscara v1 con docstringLegacy LLMEngine for backwards compatibility..make_client call:105-111—self.engine_core = EngineCoreClient.make_client(multiprocess_mode, asyncio_mode=False, ...).LLMEngine.from_engine_args:161-186— ejecutaengine_args.create_engine_config,decide si abrir multiproceso segúnVLLM_ENABLE_V1_MULTIPROCESSING.make_client:83-105— dos bools eligenInprocClient/SyncMPClient/AsyncMPClient.make_async_mp_client:109-132— en el escenario DP elige automáticamenteDPAsyncMPClientoDPLBAsyncMPClient.AsyncLLM class:70-71— definiciónclass AsyncLLM(EngineClient):.AsyncLLM.__init__:73-176— ensamblaje del tríoInputProcessor,OutputProcessor,EngineCoreClient.make_async_mp_client.AsyncLLM.from_vllm_config:203-229— construye directamente desdeVllmConfig+Executor.get_class,entrada del lado servidor.AsyncLLM.from_engine_args:232-254—engine_args.create_engine_config→Executor.get_class→cls(...)._run_output_handler:637-676— corrutina de background,get_output_async+output_processor.process_outputs,chunk size controlado porVLLM_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,y en elfinallyasync_llm.shutdown(timeout=vllm_config.shutdown_timeout).AsyncLLM.shutdown:259-271—shutdown_prometheus,renderer.shutdown,engine_core.shutdown,cancelaoutput_handler.
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:
# 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:
# 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:
# 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:
# 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_statsEn 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.generateno soporta un runner distinto de generate: cuandorunner_type != "generate"lanza una excepción (llm.py:465-471) y sugiere usar--runner generate;LLM.chatyLLM.enqueue_chathacen la misma comprobación (llm.py:684-689).LLM(data_parallel_size>1)no permite un único proceso: con_dp_size > 1y sinexternal_launcherni TPU se lanza excepción (llm.py:295-303);se recomienda seguir el esquema multiproceso deexamples/features/data_parallel/data_parallel_offline.py,que de otro modo se colgaría.renderer_num_workers > 1no hace nada en elLLMoffline:LLMva por la ruta síncrona del renderer,y el pool de hilos multi-worker solo se consume en la ruta asíncrona devllm serve/AsyncLLM;en el escenario offline se emite explícitamentewarning_once(llm.py:370-379) para que el usuario no crea que está activando multihilo.EngineDeadError: al inicio deAsyncLLM.add_requestse compruebaself.errored(async_llm.py:300-301);si el proceso EngineCore ya ha muerto (en background se emitióENGINE_CORE_DEAD),se lanzaEngineDeadError(async_llm.py:1054-1058) sin aceptar nuevas peticiones.async_llm.shutdowncomo red de seguridad: el bloquefinallydebuild_async_engine_client_from_engine_argsllama aasync_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_configdict: enLLM.__init__,cuando la conversión de dict aKVTransferConfigfalla,se hacelogger.errory se lanzaValueError(f"Invalid 'kv_transfer_config' provided: {e}")(llm.py:253-262) envolviendo el ValidationError original en un error más amable. swap_spaceestá obsoleto: cuandoswap_spaceaparece enkwargsse extrae conpopy se emiteDeprecationWarning(llm.py:224-233);se eliminará en el futuro.- Degradación cuando output_handler no arranca: si en
__init__asyncio.get_running_loop()lanzaRuntimeError(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 destart_engine_loop=Trueo introducir AsyncLLM en un event loop antes de hacerawait. - Selección estática del cliente DP:
make_async_mp_clientsolo miraparallel_config.data_parallel_sizeydata_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.