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-111EngineCoreClient.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-334engine_core.get_output()output_processor.process_outputsabort_requestslogger_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.