EngineCore: der eigentliche Kern der v1-Engine
Verantwortung
LLMEngine ist in der v1-Architektur nur eine dünne Schale: Sie nutzt InputProcessor, um den externen Prompt zu einem EngineCoreRequest zu verarbeiten, und OutputProcessor, um EngineCoreOutputs zurück in RequestOutput zu übersetzen. Die eigentliche Arbeit geht vollständig an EngineCore. Die Zeile self.engine_core = EngineCoreClient.make_client(...) in LLMEngine.__init__ (llm_engine.py:105-111) ist die Stelle, an der der Engine-Kern (EngineCore) wirklich eintritt.
Der Engine-Kern (EngineCore) fasst drei Dinge zusammen: model_executor (den Executor, also die executor_class-Instanz, die prozessübergreifend Forward ausführt), scheduler (den Scheduler, der die Warteschlangen waiting/running, Continuous Batching (continuous batching) und Verdrängung verwaltet) und KVCacheManager (den KV-Cache-Manager, der über _initialize_kv_caches nach dem Profiling bestimmt, wie viel GPU-Speicher pro Karte für den KV-Cache (KV cache) zur Verfügung steht). Im Konstruktor werden diese drei zunächst initialisiert (core.py:122-158), dann wird der Step-Funktionszeiger auf self.step oder self.step_with_batch_queue gesetzt (core.py:221-223); die anschließende Schleife ruft immer wieder diese Funktion auf.
step() macht eine Sache: schedulen, Forward ausführen, Outputs zurückholen (core.py:479-508). Konkret liefert scheduler.schedule() das SchedulerOutput, model_executor.execute_model(..., non_block=True) gibt sofort ein Future zurück, währenddessen berechnet der Scheduler die Grammar-Bitmask; sobald future.result() vorliegt, wird es an scheduler.update_from_output() übergeben, das die Tokens in die Anfrage zurückschreibt und EngineCoreOutputs produziert. Das ist eine v1-Iteration; alle oberen APIs (LLM.generate, AsyncLLMEngine.generate) landen schließlich in diesen wenigen Dutzend Zeilen.
Entwurfsmotivation
Warum LLMEngine / EngineCore / EngineCoreProc / EngineCoreClient in vier Schichten aufteilen?
- Prozessisolation:
EngineCoreProcist eine Unterklasse vonEngineCore(core.py:896-897). Es wird in einem separaten Hintergrundprozess ausgeführt; wenn das vordere Python abstürzt, reißt es nicht die Worker mit, die bereits Forward ausführen; ein Worker-OOM nimmt nicht auch noch den API-Prozess mit. - ZMQ-Entkopplung: Front- und Backend werden über einen ZMQ-Socket und zwei
queue.Queuegebridged (core.py:915-916). ZMQ gibt während des Socket-IOs den GIL frei und kann mit GPU-Forward tatsächlich überlappen. Eingang und Ausgang haben jeweils einen eigenen Thread (core.py:980-1001), sodass Serialisierung/Deserialisierung hinter dem Forward verschwinden. - In-Process-Fallback: Ohne Multiprocessing macht
InprocClientdirektself.engine_core = EngineCore(...)(core_client.py:286-287);get_output()ruft lokalstep_fn()auf und bewahrt die v0-SyntaxLLMEngine.add_request()/.step(). - Async-Einstieg:
AsyncMPClientkombiniert Multiprozess + asyncio; im Server-Szenario wickeltAsyncLLMdarüber EngineCore in einawait-bares Objekt, ohne das Frontend zu blockieren. - DP-Skalierung:
run_engine_corestartet bei mehreren DP-Ranks pro Rank einenEngineCoreProc(core.py:1192-1200). MoE verwendetDPEngineCoreProc, Nicht-MoE wird als unabhängiger DP=1 betrachtet; das Frontend macht darüberDPLBAsyncMPClientLoad-Balancing.
Schlüsseldateien
EngineCore class:96-97—class EngineCore:Definition; der Docstring lautetInner loop of vLLM's Engine.EngineCore.__init__:99-237— baut model_executor,_initialize_kv_caches, Scheduler, batch_queue, Prefix-Caching (prefix caching) Hasher._initialize_kv_caches:240-295— holtget_kv_cache_specs, profilt den Speicher, baut dieKVCacheConfig.EngineCore.step:479-508— die zentralen 30 Zeilen: Schedule + execute_model + update_from_output.step_with_batch_queue:519-535— verwendet im PP-Fall eine Batch-Queue, um mehrere Batches asynchron überlappen zu lassen und die Queue bevorzugt zu füllen.EngineCoreProc class:896-897—ZMQ-wrapper for running EngineCore in background process.run_engine_core:1154-1224— Prozesseinstieg; startet EngineCoreProc, registriert SIGTERM/SIGINT, ruftrun_busy_loop.run_busy_loop:1259-1267—while self._handle_shutdown(): _process_input_queue(); _process_engine_step()._process_engine_step:1300-1317— ruftstep_fn(), steckt Outputs in dieoutput_queue, ruftpost_step.LLMEngine.make_client:105-111— LLMEngine wählt überEngineCoreClient.make_clientInproc/MP/AsyncMP.EngineCoreClient.make_client:83-105— drei Optionen: Multiprozess-synchron / Multiprozess-async / In-Process.
Datenfluss
Wenn eine Anfrage vom Client kommt, wird sie in LLMEngine durch InputProcessor zu EngineCoreRequest verarbeitet und dann per engine_core.add_request(...) übergeben. Im Multiprozess-Modus serialisiert MPClient diesen Aufruf und sendet ihn über ZMQ an den Input-Socket des EngineCoreProc; der Thread process_input_sockets dekodiert die Daten und legt sie per put_nowait in die input_queue (core.py:915-916). Der Hauptschleife run_busy_loop wird beim Aufwachen aus der Queue geholt und an _handle_client_request dispatched (core.py:1372-1405); der Typ ADD landet schließlich bei self.scheduler.add_request(request) (core.py:372-403).
# 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) nimmt die von step_fn() gelieferten Outputs und führt für jeden output_queue.put_nowait(output) aus. Der Output-Thread sendet sie per ZMQ ans Frontend zurück. MPClient.get_output() deserialisiert und reicht sie an den engine_core.get_output()-Aufruf in LLMEngine.step() weiter (llm_engine.py:302-314); schließlich baut OutputProcessor.process_outputs die Token-Ströme zu RequestOutput zusammen.
Die Step-Funktion selbst besteht aus diesen drei Teilen:
# 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 sorgt dafür, dass execute_model sofort ein Future zurückgibt; der Haupt-Thread nutzt diese Lücke, um die Grammar-Bitmask zu berechnen — das ist die zentrale Aktion, mit der v1 Scheduler und Worker (worker) tatsächlich parallelisiert.
Grenzen und Fehler
- EngineCoreProc stürzt ab:
run_engine_coreruft imexcept Exception-Blockengine_core._send_engine_dead()auf (core.py:1229-1235), das die Byte-SequenzENGINE_CORE_DEADin dieoutput_queuesteckt (core.py:1470-1474); der vordereMPClientwirft beim Empfang einen Fehler und wartet nicht endlos. - Executor-Fehler-Callback: Beim Konstruieren wird
executor_fail_callbackbeimodel_executorregistriert (core.py:124-125); wenn im Worker ein Fehler auftritt, legt der CallbackEXECUTOR_FAILEDin dieinput_queue; der Haupt-Loop wirft beim Erhalt über_handle_client_requesteinraise RuntimeError("Executor failed.")(core.py:1400-1401). - Shutdown-Modi:
shutdown_timeout == 0ist Abort-Modus, der sofort alle in-flight-Anfragen alsFINISHED_ABORTEDmarkiert; ungleich 0 ist Drain-Modus, der darauf wartet, dass alle in-flight-Anfragen fertig sind (core.py:1329-1360). Neue ADD- und UTILITY-Aufrufe während des Shutdowns werden explizit abgelehnt (core.py:1407-1432). - Nicht-kausale Attention-Schichten: Einige Attention-Schichten (etwa Prefix LM) sind mit
non_causal=Truemarkiert; wenn_initialize_kv_cachesdas erkennt, werden Chunked Prefill und Prefix-Caching erzwungen ausgeschaltet (core.py:255-269), da der KV-Cache sonst überschrieben würde. - batch_queue gibt CPU frei: Wenn ein Step keinen Forward ausführt, der Scheduler aber noch offene Anfragen hat (z. B. auf Remote-KV-Transfer gewartet wird), gibt
time.sleep(0.001)aktiv den GIL frei (core.py:1311-1315), damit Hintergrund-Transfer-Threads nicht verhungern. - In-Process-Modus ohne Busy Loop:
InprocClient.get_outputruft direkt synchronstep_fnauf (core_client.py:289-292) und betreibt keinrun_busy_loop. Im In-Process-Modus gibt es also keine Input-/Output-Threads;add_requestgeht synchron in den Scheduler.
Zusammenfassung
Der Engine-Kern (EngineCore) ist der Motor von v1: Er baut Scheduler (scheduler), Executor (executor) und KV-Cache (KV cache) zusammen und wiederholt mit step() die Aktion Schedule + Forward + Einsammeln. Die Unterklasse EngineCoreProc verlegt ihn in einen eigenen Prozess und bridged Front- und Backend über ZMQ und zwei Queues, sodass GPU-Forward und Netzwerk-IO tatsächlich überlappen. Welcher Client vorne sitzt (InprocClient, MPClient, AsyncMPClient), bestimmt synchron oder asynchron; auf EngineCore-Ebene ist der Code für alle Frontends identisch. Weiter geht es mit /engine/llm-engine zur dünnen Schale der LLMEngine oder mit /engine/engine-core-proc tiefer in die ZMQ-Schicht; die drei Client-Implementierungen stehen in /client/inproc-mp; das Scheduler-Innere in /scheduler/scheduler.