LLMEngine: la shell sincrónica del motor v1
Responsabilidades
En la arquitectura v1, LLMEngine es la shell de entrada para las APIs sincrónicas (LLM.generate, LLM.chat). No corre forward, no programa, no asigna caché KV; solo pega tres cosas: con InputProcessor convierte el prompt entrante en EngineCoreRequest, con OutputProcessor traduce EngineCoreOutputs de vuelta a RequestOutput, y el trabajo real se lo deja a EngineCore. La línea al final de __init__, self.engine_core = EngineCoreClient.make_client(...) (llm_engine.py:105-111), es donde entra el núcleo del motor (EngineCore): make_client elige uno de tres clientes — InprocClient, SyncMPClient, AsyncMPClient — según si hay multiproceso y si hay asyncio.
El motivo de esta capa es preservar el uso sincrónico estilo v0 LLMEngine.add_request() / .step(): en modo en el mismo proceso, InprocClient tiene directamente la instancia de EngineCore (core_client.py:286-292); get_output() llama in situ a step_fn(), frontend y backend comparten memoria, no hay ZMQ ni busy loop; en modo multiproceso, SyncMPClient levanta un EngineCoreProc en segundo plano (core.py:896-897), LLMEngine sigue ofreciendo la misma interfaz sincrónica, solo que por debajo hay un puente ZMQ. Por eso LLMEngine tiene muy poco código (448 líneas); muchos métodos simplemente le pasan la llamada a engine_core (sleep/wake, profile, lora, reset_prefix_cache son todos traspasos de una línea).
Motivación de diseño
- Preservar la API sincrónica de v0: la clase
LLMexpone hacia afuera los métodos sincrónicosgenerate/chat, e internamente hace un loop deLLMEngine.add_request()+LLMEngine.step()(llm_engine.py:296-334); v1 funciona sin asyncio. Esta es la clave para que v1 reemplace a v0 sin romper los flujos de inferencia offline. - Tres backends, una sola interfaz: la lógica 3-en-1 de
make_client(core_client.py:83-105) hace que ningún método deLLMEnginenecesite preocuparse por si es en el mismo proceso, multiproceso sincrónico o multiproceso async; le basta con tener una instancia deEngineCoreClient, y el switch de backend se decide al construir. - Procesamiento de input/output en la shell:
InputProcessor(input_processor.py:36) trata lo multimodal (multimodal), tokenization, priority, trace_headers, LoRA;OutputProcessor(output_processor.py:417) se encarga de detokenize, chunking en streaming, matching de stop string, y convertirEngineCoreOutputsde vuelta aRequestOutput. Ambas tareas sucias y con datos sucios se quedan en la shell; EngineCore solo veEngineCoreRequest/EngineCoreOutputspuramente estructurados. - Fan-out multi-muestreo con
n>1: cuandoSamplingParams.nes mayor que 1,add_requestusaParentRequest(parallel_sampling.py:13) para partir una request padre en n requests hijas que se meten por separado en EngineCore y OutputProcessor (llm_engine.py:279-294); en la capa de output los streams de tokens de las hijas se agregan de vuelta al padre. - Limpieza al salir del proceso: cuando
multiprocess_mode=False, un finalizer con weakref al modelo driver (<SrcLink path="vllm/v1/engine/llm_engine.py" lines="129-133" label="llm_engine.py"/>) llama a_cleanup_instance_cachescuando el GC recolectaLLMEngine, para liberar la VRAM que el hook de byte code mantenía viva.
Archivos clave
LLMEngine class:48-49— definición declass LLMEngine:, el docstring se autodescribe comoLegacy LLMEngine for backwards compatibility..LLMEngine.__init__:51-141— ensambla renderer / InputProcessor / OutputProcessor / EngineCoreClient, y arranca StatLoggerManager y el cleanup finalizer.make_client 调用点:105-111—EngineCoreClient.make_client(multiprocess_mode=..., asyncio_mode=False, ...)elige el backend.from_vllm_config:143-158— construye a partir deVllmConfig;multiprocess_modelo decideenvs.VLLM_ENABLE_V1_MULTIPROCESSING.add_request:218-294— valida request_id,input_processor.process_inputs, fan-out porParentRequestsin>1, y al finalengine_core.add_request.step:296-334—engine_core.get_output()→output_processor.process_outputs→abort_requests→logger_manager.record.abort_request:212-216— primero marca en OutputProcessor, luego hace que EngineCore quite la request del scheduler.sleep / wake_up:361-376— sleep/wake que cruza renderer + engine_core; StatLoggerManager registra el estado de sleep.do_log_stats_with_interval:394-401— regula el logging conVLLM_LOG_STATS_INTERVAL, evitando flush en cada step._get_driver_model_for_cleanup:431-434— sigue la cadenamodel_executor.driver_worker.model_runner.modelpara obtener el modelo driver, que el finalizer libera de VRAM.
Flujo de datos
Una inferencia sincrónica es for req in requests: add_request(...) seguido de while has_unfinished_requests(): step(). La estructura de step() es directa: saca un lote de EngineCoreOutputs de engine_core, se lo pasa a OutputProcessor para traducir, devuelve a abort las requests que terminaron antes por stop string, y de paso registra estadísticas.
# vllm/v1/engine/llm_engine.py L296-L314
def step(self) -> list[RequestOutput | PoolingRequestOutput]:
if self.should_execute_dummy_batch:
self.should_execute_dummy_batch = False
self.engine_core.execute_dummy_batch()
return []
# 1) Get EngineCoreOutput from the EngineCore.
with record_function_or_nullcontext("llm_engine step: get_output"):
outputs = self.engine_core.get_output()
# 2) Process EngineCoreOutputs.
with record_function_or_nullcontext("llm_engine step: process_outputs"):
iteration_stats = IterationStats() if self.log_stats else None
processed_outputs = self.output_processor.process_outputs(
outputs.outputs,
engine_core_timestamp=outputs.timestamp,
iteration_stats=iteration_stats,
)
self.output_processor.update_scheduler_stats(outputs.scheduler_stats)Más adelante hay pasos 3 y 4 (llm_engine.py:317-332): el paso 3 entrega processed_outputs.reqs_to_abort a engine_core.abort_requests, porque el stop string solo se ve tras detokenize y EngineCore no lo sabe por sí mismo; el paso 4, si logger_manager no es None y hay scheduler_stats, llama record + do_log_stats_with_interval.
add_request con n>1 toma la rama de fan-out (llm_engine.py:279-294):
# vllm/v1/engine/llm_engine.py L279-L294
# Fan out child requests (for n>1).
parent_req = ParentRequest(request)
for idx in range(n):
request_id, child_params = parent_req.get_child_info(idx)
child_request = request if idx == n - 1 else copy(request)
child_request.request_id = request_id
child_request.sampling_params = child_params
# Make a new RequestState and queue.
self.output_processor.add_request(
child_request, prompt_text, parent_req, idx
)
# Add the request to EngineCore.
self.engine_core.add_request(child_request)
return req_idLa última hija reutiliza el objeto request original para ahorrar una copia; el ID de la request padre lo genera ParentRequest.get_child_info(idx); OutputProcessor, al recibir la referencia al padre y el idx, puede agregar de vuelta los outputs de las hijas.
Límites y fallos
EngineCoreRequestestá deprecado: siadd_requestrecibe unEngineCoreRequestpasado directo, lanza unwarning_oncede deprecación diciendo que desde v0.18 hay que usarRenderer.render_cmpl()/render_chat()(llm_engine.py:235-248); si elrequest_idpasado y elEngineCoreRequest.request_idno coinciden, gana el segundo y el primero se ignora.- Dummy batch en modo DP: en modo data parallel con lanzador externo, cuando un rank no tiene trabajo pero otros ranks siguen corriendo,
has_unfinished_requests_dpponeshould_execute_dummy_batchen True (llm_engine.py:197-203);step()arranca corriendo primero un dummy batch como placeholder (llm_engine.py:297-300) para que la comunicación collective no se cuelgue. model_executorsolo se obtiene en modo en el mismo proceso: solo en la ramaif not multiprocess_mode:se fijaself.model_executor = self.engine_core.engine_core.model_executor(llm_engine.py:123-125); en modo multiproceso ese objeto no se puede obtener entre procesos, así que el código que acceda amodel_executordebe asegurar antesmultiprocess_mode=False.- Regulación del logging de stats:
do_log_stats_with_intervalusaVLLM_LOG_STATS_INTERVALpara controlar el intervalo mínimo (llm_engine.py:394-401);_last_log_timees un atributo de instancia inicializado de forma perezosa en el primer acceso, evitando la molestia de fijarlo en__init__. - sleep coordina con el renderer:
sleep(level>=1)primero limpia la caché multimodal del renderer y luego llamaengine_core.sleep(llm_engine.py:361-367), porque tras wake_up los embeddings multimodales pueden haber quedado inválidos y el renderer debe recalcularlos. - El backend de EngineCore lo decide una variable de entorno: en
from_vllm_config,multiprocess_mode=envs.VLLM_ENABLE_V1_MULTIPROCESSING(llm_engine.py:157), yfrom_engine_argslo vuelve a aplicar (llm_engine.py:174-176); el switch de multiproceso queda fijado en la capa de configuración y no se puede cambiar en runtime.
Resumen
LLMEngine es la shell fina del camino sincrónico de v1: valida inputs, trata multimodal y salida en streaming con InputProcessor / OutputProcessor, elige backend con EngineCoreClient.make_client, y luego basta con un loop add_request + step. Delega todo el trabajo pesado a EngineCore (en el mismo proceso directamente, en multiproceso por ZMQ), y solo se ocupa de las cosas de la capa shell: compatibilidad con v0, fan-out con ParentRequest, regulación del logging, coordinación de sleep/wake. Para ver el ensamblaje del núcleo del motor continúa en /engine/engine-core, para el proceso en segundo plano con ZMQ en /engine/engine-core-proc, las tres implementaciones de cliente están en /client/inproc-mp, y el interior del scheduler en /scheduler/scheduler.