Skip to content

LLMEngine: la shell sincrónica del motor v1 ​

源码版本v0.25.1

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 LLM expone hacia afuera los métodos sincrónicos generate/chat, e internamente hace un loop de LLMEngine.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 de LLMEngine necesite preocuparse por si es en el mismo proceso, multiproceso sincrónico o multiproceso async; le basta con tener una instancia de EngineCoreClient, 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 convertir EngineCoreOutputs de vuelta a RequestOutput. Ambas tareas sucias y con datos sucios se quedan en la shell; EngineCore solo ve EngineCoreRequest / EngineCoreOutputs puramente estructurados.
  • Fan-out multi-muestreo con n>1: cuando SamplingParams.n es mayor que 1, add_request usa ParentRequest (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_caches cuando el GC recolecta LLMEngine, para liberar la VRAM que el hook de byte code mantenía viva.

Archivos clave ​

  • LLMEngine class:48-49 — definición de class LLMEngine:, el docstring se autodescribe como Legacy 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 de VllmConfig; multiprocess_mode lo decide envs.VLLM_ENABLE_V1_MULTIPROCESSING.
  • add_request:218-294 — valida request_id, input_processor.process_inputs, fan-out por ParentRequest si n>1, y al final engine_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 con VLLM_LOG_STATS_INTERVAL, evitando flush en cada step.
  • _get_driver_model_for_cleanup:431-434 — sigue la cadena model_executor.driver_worker.model_runner.model para 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.

python
# 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):

python
# 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_id

La ú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 ​

  • EngineCoreRequest está deprecado: si add_request recibe un EngineCoreRequest pasado directo, lanza un warning_once de deprecación diciendo que desde v0.18 hay que usar Renderer.render_cmpl() / render_chat() (llm_engine.py:235-248); si el request_id pasado y el EngineCoreRequest.request_id no 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_dp pone should_execute_dummy_batch en 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_executor solo se obtiene en modo en el mismo proceso: solo en la rama if not multiprocess_mode: se fija self.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 a model_executor debe asegurar antes multiprocess_mode=False.
  • Regulación del logging de stats: do_log_stats_with_interval usa VLLM_LOG_STATS_INTERVAL para controlar el intervalo mínimo (llm_engine.py:394-401); _last_log_time es 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 llama engine_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), y from_engine_args lo 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.

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