LLMEngine: die synchrone dünne Schale der v1-Engine
Verantwortung
In der v1-Architektur ist LLMEngine die Eingangsschale für die synchronen APIs (LLM.generate, LLM.chat). Sie führt selbst keinen Forward aus, scheduliert nicht und alloziert keinen KV-Cache (KV cache), sondern klebt diese drei Dinge nur zusammen: Mit InputProcessor wird der von außen kommende Prompt zu einem EngineCoreRequest verarbeitet, mit OutputProcessor werden EngineCoreOutputs zurück in RequestOutput übersetzt, die eigentliche Arbeit geht vollständig an EngineCore. Die Zeile self.engine_core = EngineCoreClient.make_client(...) am Ende von __init__ (llm_engine.py:105-111) ist die Stelle, an der der Engine-Kern (EngineCore) wirklich eintritt — make_client wählt abhängig von Multiprocessing und asyncio einen von drei Clients aus: InprocClient, SyncMPClient oder AsyncMPClient.
Diese Schicht existiert, um die synchrone v0-Nutzung über LLMEngine.add_request() / .step() zu erhalten: Im In-Process-Modus hält InprocClient direkt eine EngineCore-Instanz (core_client.py:286-292), get_output() ruft lokal step_fn() auf, Front- und Backend teilen sich den Speicher, ohne ZMQ, ohne Busy Loop. Im Multiprozess-Modus startet SyncMPClient einen Hintergrund-EngineCoreProc (core.py:896-897), LLMEngine bleibt dieselbe synchrone Schnittstelle, nur dass darunter eine ZMQ-Brücke liegt. Deshalb ist LLMEngine mit 448 Zeilen klein; viele Methoden reichen Aufrufe nur an engine_core weiter (sleep/wake, profile, lora, reset_prefix_cache sind alles Einzeiler).
Entwurfsmotivation
- v0-sync-API bewahren: Die
LLM-Klasse exponiertgenerate/chatals synchrone Methoden, intern betreibtLLMEngine.add_request()+LLMEngine.step()die Schleife (llm_engine.py:296-334), sodass v1 auch ohne asyncio läuft. Das ist der Schlüssel, v1 einzusetzen, ohne Offline-Inferenz-Workflows zu brechen. - Drei Backends, eine Schnittstelle: Die 3-aus-1-Logik von
make_client(core_client.py:83-105) macht alle Methoden vonLLMEngineunabhängig davon, ob In-Process, Multiprozess-synchron oder Multiprozess-async. Es reicht, eineEngineCoreClient-Instanz zu erhalten; das Backend wird zur Konstruktionszeit festgelegt. - Ein-/Ausgabeverarbeitung in der Shell-Schicht:
InputProcessor(input_processor.py:36) behandelt Multimodal (multimodal), Tokenisierung, Priorität, trace_headers, LoRA;OutputProcessor(output_processor.py:417) übernimmt Detokenisierung, Streaming-Chunking, Stop-String-Matching und wandeltEngineCoreOutputsinRequestOutputum. Diese beiden schmutzigen Arbeiten verbleiben in der Shell-Schicht; EngineCore sieht nur strukturierteEngineCoreRequest/EngineCoreOutputs. n>1-Multi-Sampling-Fächerung: WennSamplingParams.ngrößer als 1 ist, fächertadd_requestmitParentRequest(parallel_sampling.py:13) eine Eltern-Anfrage in n Kind-Anfragen auf, die separat in EngineCore und OutputProcessor gesteckt werden (llm_engine.py:279-294); die Ausgabeschicht aggregiert die Token-Ströme der Kinder zurück in die Eltern-Anfrage.- Fallback bei Prozessende: Für
multiprocess_mode=Falseholt der Finalizer einen Weakref auf das Driver-Modell (<SrcLink path="vllm/v1/engine/llm_engine.py" lines="129-133" label="llm_engine.py"/>); wennLLMEngineper GC eingesammelt wird, ruft er_cleanup_instance_cachesauf, um den durch Byte-Code-Hooks festgehaltenen GPU-Speicher freizugeben.
Schlüsseldateien
LLMEngine class:48-49—class LLMEngine:Definition, der Docstring bezeichnet sich alsLegacy LLMEngine for backwards compatibility..LLMEngine.__init__:51-141— baut renderer / InputProcessor / OutputProcessor / EngineCoreClient zusammen und startet StatLoggerManager und Cleanup-Finalizer.make_client Aufrufstelle:105-111—EngineCoreClient.make_client(multiprocess_mode=..., asyncio_mode=False, ...)wählt das Backend.from_vllm_config:143-158— Konstruktion ausVllmConfig;multiprocess_modewird durchenvs.VLLM_ENABLE_V1_MULTIPROCESSINGbestimmt.add_request:218-294— validiert request_id,input_processor.process_inputs,n>1geht durchParentRequest-Fächerung, abschließendengine_core.add_request.step:296-334—engine_core.get_output()→output_processor.process_outputs→abort_requests→logger_manager.record.abort_request:212-216— markiert zuerst im OutputProcessor, dann lässt EngineCore die Anfrage aus dem Scheduler entfernen.sleep / wake_up:361-376— Sleep/Wake über renderer + engine_core hinweg; StatLoggerManager protokolliert den Sleep-Zustand.do_log_stats_with_interval:394-401— drosselt Logging mitVLLM_LOG_STATS_INTERVAL, damit nicht jeder Step flushed._get_driver_model_for_cleanup:431-434— geht entlang der Kettemodel_executor.driver_worker.model_runner.model, um das Driver-Modell für den Finalizer zu holen und GPU-Speicher freizugeben.
Datenfluss
Ein synchroner Inferenzdurchlauf ist einfach for req in requests: add_request(...) und danach while has_unfinished_requests(): step(). step() ist direkt aufgebaut: eine Charge EngineCoreOutputs von engine_core ziehen, an OutputProcessor zum Übersetzen übergeben, die wegen Stop-String vorzeitig beendeten Anfragen zurück zum Abbrechen geben und nebenbei Statistiken schreiben.
# 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)Danach folgen Schritt 3 und 4 (llm_engine.py:317-332): Schritt 3 übergibt processed_outputs.reqs_to_abort an engine_core.abort_requests, da Stop-String erst nach Detokenisierung sichtbar wird und EngineCore das selbst nicht weiß; Schritt 4 ruft bei nicht-leerem logger_manager und vorhandenen scheduler_stats record + do_log_stats_with_interval auf.
add_request nimmt bei n>1 den Fächerungspfad (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_idDas letzte Kind verwendet das ursprüngliche Request-Objekt und spart eine Kopie. Die Eltern-ID wird von ParentRequest.get_child_info(idx) erzeugt; mit der Eltern-Referenz und idx kann der OutputProcessor die Kind-Ausgaben wieder zur Eltern-Anfrage zusammenführen.
Grenzen und Fehler
- EngineCoreRequest veraltet: Wenn
add_requestdirekt einEngineCoreRequestübergeben bekommt, erfolgtwarning_onceals deprecated; es wird darauf hingewiesen, ab v0.18Renderer.render_cmpl()/render_chat()zu verwenden (llm_engine.py:235-248). Weicht die übergebenerequest_idvonEngineCoreRequest.request_idab, gewinnt letztere; erstere wird ignoriert. - Dummy Batch im DP-Modus: Im Datenparallel- (data parallel) Modus mit externem Starter wird
should_execute_dummy_batchauf True gesetzt, wenn ein Rank nichts zu tun hat, andere aber noch laufen (has_unfinished_requests_dp,llm_engine.py:197-203);step()führt zu Beginn einen Dummy Batch aus (llm_engine.py:297-300), damit die kollektive Kommunikation nicht hängt. model_executornur im In-Process-Modus: Nur im Zweigif not multiprocess_mode:wirdself.model_executor = self.engine_core.engine_core.model_executorgesetzt (llm_engine.py:123-125); im Multiprozess-Modus ist dieses Objekt prozessübergreifend nicht erreichbar. Downstream-Code muss vor dem Zugriff aufmodel_executoralsomultiprocess_mode=Falsesicherstellen.- Statistik-Log-Drosselung:
do_log_stats_with_intervalverwendetVLLM_LOG_STATS_INTERVAL, um das Minimum-Intervall zu steuern (llm_engine.py:394-401)._last_log_timeist eine bei Erstzugriff lazy initialisierte Instanz-Attribut, was das Setzen eines Anfangswerts in__init__umgeht. - Sleep koppelt an renderer:
sleep(level>=1)leert zuerst den Multimodal-Cache des Renderers und ruft dannengine_core.sleepauf (llm_engine.py:361-367), da nach dem Aufwecken (wake_up) Multimodal-Embeddings invalide werden können und der Renderer sie neu berechnen muss. - EngineCore-Backend wird per Env-Variablen festgelegt: In
from_vllm_configgiltmultiprocess_mode=envs.VLLM_ENABLE_V1_MULTIPROCESSING(llm_engine.py:157),from_engine_argsschichtet noch einmal darüber (llm_engine.py:174-176). Der Multiprozess-Schalter ist also bereits auf Konfigurationsebene fixiert und kann zur Laufzeit nicht umgeschaltet werden.
Zusammenfassung
LLMEngine ist die dünne Schale auf dem v1-Synchron-Pfad: Eingaben validieren, mit InputProcessor / OutputProcessor Multimodal und Streaming-Ausgaben behandeln, mit EngineCoreClient.make_client das Backend wählen und dann add_request + step in einer Schleife durchlaufen. Sie delegiert alle schweren Arbeiten an EngineCore (In-Process direkt gehalten, Multiprozess über ZMQ) und kümmert sich selbst nur um v0-Kompatibilität, ParentRequest-Fächerung, Statistik-Drosselung und Sleep/Wake-Kopplung. Weiter geht es mit /engine/engine-core für den Aufbau des Engine-Kerns, /engine/engine-core-proc für den ZMQ-Hintergrundprozess; die drei Client-Implementierungen stehen in /client/inproc-mp; das Scheduler-Innere in /scheduler/scheduler.