Skip to content

LLMEngine: die synchrone dünne Schale der v1-Engine

源码版本v0.25.1

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 exponiert generate/chat als synchrone Methoden, intern betreibt LLMEngine.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 von LLMEngine unabhängig davon, ob In-Process, Multiprozess-synchron oder Multiprozess-async. Es reicht, eine EngineCoreClient-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 wandelt EngineCoreOutputs in RequestOutput um. Diese beiden schmutzigen Arbeiten verbleiben in der Shell-Schicht; EngineCore sieht nur strukturierte EngineCoreRequest / EngineCoreOutputs.
  • n>1-Multi-Sampling-Fächerung: Wenn SamplingParams.n größer als 1 ist, fächert add_request mit ParentRequest (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=False holt der Finalizer einen Weakref auf das Driver-Modell (<SrcLink path="vllm/v1/engine/llm_engine.py" lines="129-133" label="llm_engine.py"/>); wenn LLMEngine per GC eingesammelt wird, ruft er _cleanup_instance_caches auf, um den durch Byte-Code-Hooks festgehaltenen GPU-Speicher freizugeben.

Schlüsseldateien

  • LLMEngine class:48-49class LLMEngine: Definition, der Docstring bezeichnet sich als Legacy LLMEngine for backwards compatibility..
  • LLMEngine.__init__:51-141 — baut renderer / InputProcessor / OutputProcessor / EngineCoreClient zusammen und startet StatLoggerManager und Cleanup-Finalizer.
  • make_client Aufrufstelle:105-111EngineCoreClient.make_client(multiprocess_mode=..., asyncio_mode=False, ...) wählt das Backend.
  • from_vllm_config:143-158 — Konstruktion aus VllmConfig; multiprocess_mode wird durch envs.VLLM_ENABLE_V1_MULTIPROCESSING bestimmt.
  • add_request:218-294 — validiert request_id, input_processor.process_inputs, n>1 geht durch ParentRequest-Fächerung, abschließend engine_core.add_request.
  • step:296-334engine_core.get_output()output_processor.process_outputsabort_requestslogger_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 mit VLLM_LOG_STATS_INTERVAL, damit nicht jeder Step flushed.
  • _get_driver_model_for_cleanup:431-434 — geht entlang der Kette model_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.

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)

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

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

Das 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_request direkt ein EngineCoreRequest übergeben bekommt, erfolgt warning_once als deprecated; es wird darauf hingewiesen, ab v0.18 Renderer.render_cmpl() / render_chat() zu verwenden (llm_engine.py:235-248). Weicht die übergebene request_id von EngineCoreRequest.request_id ab, gewinnt letztere; erstere wird ignoriert.
  • Dummy Batch im DP-Modus: Im Datenparallel- (data parallel) Modus mit externem Starter wird should_execute_dummy_batch auf 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_executor nur im In-Process-Modus: Nur im Zweig if not multiprocess_mode: wird self.model_executor = self.engine_core.engine_core.model_executor gesetzt (llm_engine.py:123-125); im Multiprozess-Modus ist dieses Objekt prozessübergreifend nicht erreichbar. Downstream-Code muss vor dem Zugriff auf model_executor also multiprocess_mode=False sicherstellen.
  • Statistik-Log-Drosselung: do_log_stats_with_interval verwendet VLLM_LOG_STATS_INTERVAL, um das Minimum-Intervall zu steuern (llm_engine.py:394-401). _last_log_time ist 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 dann engine_core.sleep auf (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_config gilt multiprocess_mode=envs.VLLM_ENABLE_V1_MULTIPROCESSING (llm_engine.py:157), from_engine_args schichtet 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.

Siehe offizielle Dokumentation: vLLM 文档 · README.