Skip to content

EngineCore: el núcleo real del motor en v1

源码版本v0.25.1

Responsabilidades

LLMEngine en la arquitectura v1 es solo una shell fina: toma un InputProcessor que convierte el prompt entrante en EngineCoreRequest, y un OutputProcessor que traduce EngineCoreOutputs de vuelta a RequestOutput; el trabajo real se lo deja a EngineCore. Esa línea self.engine_core = EngineCoreClient.make_client(...) en LLMEngine.__init__ (llm_engine.py:105-111) es donde entra al tablero el núcleo del motor (EngineCore).

EngineCore arma tres piezas: model_executor (el executor, es decir, la instancia de executor_class, encargada de correr el forward entre workers), scheduler (el planificador, encargado de las colas waiting/running, del batching continuo (continuous batching) y del preempt), y KVCacheManager (el gestor de caché KV, que a través de _initialize_kv_caches determina cuánta VRAM puede destinar cada GPU a la caché KV después del profiling). El constructor inicializa esas tres piezas (core.py:122-158), y luego fija el puntero de la función step a self.step o self.step_with_batch_queue (core.py:221-223); el loop después simplemente la llama una y otra vez.

step() hace una sola cosa: scheduler, forward, recuperar outputs (core.py:479-508). En concreto, scheduler.schedule() devuelve un SchedulerOutput, model_executor.execute_model(..., non_block=True) devuelve de inmediato un future, mientras tanto el scheduler calcula de paso el grammar bitmask; cuando el future hace result(), se lo pasa a scheduler.update_from_output() para escribir el token de vuelta en la request y producir los EngineCoreOutputs. Esa es una iteración de v1: todas las APIs de arriba (LLM.generate, AsyncLLMEngine.generate) terminan cayendo en esas decenas de líneas.

Motivación de diseño

¿Por qué separar LLMEngine / EngineCore / EngineCoreProc / EngineCoreClient en cuatro capas?

  • Aislamiento de procesos: EngineCoreProc es una subclase de EngineCore (core.py:896-897); al meterlo en un proceso en segundo plano independiente, si el Python del frontend se cae no arrastra a los workers que ya están corriendo forward, y un OOM de un worker no se lleva por delante al proceso de API.
  • Desacople con ZMQ: frontend y backend se puentean con sockets ZMQ + dos queue.Queue (core.py:915-916); ZMQ libera el GIL al hacer IO del socket, así que puede solaparse de verdad con el forward en GPU; input y output tienen hilos independientes (core.py:980-1001), y la serialización/deserialización también queda escondida detrás del forward.
  • Fallback en el mismo proceso: cuando no se usa multiproceso, InprocClient hace directamente self.engine_core = EngineCore(...) (core_client.py:286-287); get_output() llama in situ a step_fn(), preservando el uso sincrónico estilo v0 LLMEngine.add_request() / .step().
  • Entrada async: AsyncMPClient combina multiproceso + asyncio; en escenarios de servidor, AsyncLLM lo usa para envolver EngineCore en un objeto awaitable, sin bloquear el frontend.
  • Extensión DP: cuando hay varios DP rank, run_engine_core lanza un EngineCoreProc por rank (core.py:1192-1200); MoE va por DPEngineCoreProc, y lo que no es MoE se trata como DP=1 independiente; el frontend entonces hace balance de carga con DPLBAsyncMPClient.

Archivos clave

Flujo de datos

Una request entra desde el cliente, primero es procesada por InputProcessor en LLMEngine hasta convertirse en EngineCoreRequest, y luego se envía con engine_core.add_request(...). En modo multiproceso, esta llamada termina serializada por MPClient y enviada por ZMQ al socket de input de EngineCoreProc; el hilo process_input_sockets la decodifica y hace put_nowait en input_queue (core.py:915-916). El loop principal run_busy_loop al despertar la saca de la cola y la pasa a _handle_client_request para despacharla (core.py:1372-1405); el tipo ADD finalmente cae en self.scheduler.add_request(request) (core.py:372-403).

python
# vllm/v1/engine/core.py L1259-L1267
def run_busy_loop(self):
    """Core busy loop of the EngineCore."""
    while self._handle_shutdown():
        # 1) Poll the input queue until there is work to do.
        self._process_input_queue()
        # 2) Step the engine core and return the outputs.
        self._process_engine_step()

    raise SystemExit

_process_engine_step (core.py:1300-1317) toma los outputs que devuelve step_fn() y hace output_queue.put_nowait(output) uno por uno. El hilo de output los envía de vuelta por ZMQ al frontend, MPClient.get_output() los deserializa y se los entrega a la llamada engine_core.get_output() que hizo LLMEngine.step() (llm_engine.py:302-314); finalmente OutputProcessor.process_outputs recompone el stream de tokens en un RequestOutput.

El cuerpo de la función step es tres bloques:

python
# vllm/v1/engine/core.py L488-L508
if not self.scheduler.has_requests():
    return {}, False
scheduler_output = self.scheduler.schedule(self._should_throttle_prefills())
future = self.model_executor.execute_model(scheduler_output, non_block=True)
grammar_output = self.scheduler.get_grammar_bitmask(scheduler_output)
with (
    self.log_error_detail(scheduler_output),
    self.log_iteration_details(scheduler_output),
):
    model_output = future.result()
    if model_output is None:
        model_output = self.model_executor.sample_tokens(grammar_output)

# Before processing the model output, process any aborts that happened
# during the model execution.
self._process_aborts_queue()
engine_core_outputs = self.scheduler.update_from_output(
    scheduler_output, model_output
)

non_block=True hace que execute_model devuelva de inmediato un future; el hilo principal aprovecha ese hueco para calcular el grammar bitmask — esta es la acción clave que en v1 vuelve realmente concurrentes al scheduler y al worker.

Límites y fallos

  • Caída del proceso EngineCoreProc: run_engine_core en except Exception llama engine_core._send_engine_dead() (core.py:1229-1235), metiendo en output_queue la cadena de bytes ENGINE_CORE_DEAD (core.py:1470-1474); el MPClient del frontend al recibirla lanza un error, no se queda esperando para siempre.
  • Callback de fallo del executor: al construir se registra executor_fail_callback en model_executor (core.py:124-125); cuando un worker falla internamente, el callback mete EXECUTOR_FAILED en input_queue, y el loop principal al recibirlo en _handle_client_request lanza raise RuntimeError("Executor failed.") (core.py:1400-1401).
  • Modo shutdown: shutdown_timeout == 0 es modo abort, marca todas las requests en vuelo como FINISHED_ABORTED de inmediato; si no es 0 es modo drain, espera a que terminen todas (core.py:1329-1360). Durante el shutdown, las ADD y UTILITY nuevas se rechazan explícitamente (core.py:1407-1432).
  • Capas de atención no causales: algunas capas de attention (p. ej. Prefix LM) marcan non_causal=True; al detectarlas en _initialize_kv_caches se desactivan forzosamente chunked prefill y prefix caching (core.py:255-269), porque si no la caché KV se ensucia.
  • batch_queue cede la CPU: cuando algún step no ejecuta forward pero el scheduler aún tiene requests sin terminar (por ejemplo, esperando transferencia remota de KV), un time.sleep(0.001) cede el GIL de forma proactiva (core.py:1311-1315), para no dejar morir de hambre a los hilos de transferencia en segundo plano.
  • El modo en el mismo proceso no tiene busy loop: InprocClient.get_output llama directamente y de forma sincrónica a step_fn (core_client.py:289-292), sin correr run_busy_loop; eso significa que en modo en el mismo proceso no hay hilos de input/output, y add_request entra al scheduler de forma sincrónica.

Resumen

El núcleo del motor (EngineCore) es el motor de v1: ensambla las tres piezas —planificador (scheduler), executor, caché KV (KV cache)— y con step() repite una y otra vez schedule + forward + recuperación. La subclase EngineCoreProc lo mete en un proceso independiente y puentea frontend y backend con ZMQ + dos queues, para que el forward en GPU y el IO de red se solapen de verdad. Qué cliente se use en el frontend (InprocClient, MPClient, AsyncMPClient) decide si corre sincrónico o asincrónico, pero la capa del núcleo del motor es la misma para todos los frontends. Para ver qué envuelve exactamente la shell de LLMEngine continúa en /engine/llm-engine, o en /engine/engine-core-proc para entrar en la capa ZMQ; 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.