Skip to content

LLM und AsyncLLM: Zwei Einstiegspfade für Offline und Serve

源码版本v0.25.1

Verantwortung

LLM ist die synchrone Python-API, die vLLM für Offline-Inferenz (offline inference) bereitstellt (llm.py:66-67); AsyncLLM ist die asynchrone Hülle, die der OpenAI-API-Server im Serve-Fall verwendet (async_llm.py:70-71). Beide erben von der EngineClient-Abstraktion, nutzen jedoch unterschiedliche EngineCoreClient-Implementierungen: LLM verwendet standardmäßig SyncMPClient oder InprocClient, AsyncLLM geht über AsyncMPClient (oder DPLBAsyncMPClient im DP-Fall). Beide Klassen selbst führen keinen Forward aus – sie sind Container für das Trio aus InputProcessor, OutputProcessor und EngineCoreClient.

LLM.__init__ (llm.py:176-222) akzeptiert fast alle EngineArgs-Felder als kwarg und verpackt sie in ein EngineArgs(model=..., tensor_parallel_size=..., ...) (llm.py:305-345), das an LLMEngine.from_engine_args(engine_args, usage_context=UsageContext.LLM_CLASS) (llm.py:349-351) übergeben wird. LLMEngine ist in v1 nur eine dünne, abwärtskompatible Hülle (llm_engine.py:48-49), die intern den von EngineCoreClient.make_client(...) (llm_engine.py:105) ausgewählten Client als self.engine_core hält. Ein Aufruf von LLM.generate() landet damit letztlich in einer Schleife aus llm_engine.add_request und llm_engine.step.

AsyncLLM geht einen anderen Montagepfad. build_async_engine_client_from_engine_args (api_server.py:109-154) ruft zuerst engine_args.create_engine_config() auf, um VllmConfig zu erhalten, dann AsyncLLM.from_vllm_config(vllm_config, ...), das intern direkt EngineCoreClient.make_async_mp_client(...) (async_llm.py:146-153) aufruft und AsyncMPClient (oder DPLBAsyncMPClient im DP-Fall) startet. AsyncLLM hält zusätzlich eine dauerhafte output_handler-Koroutine (async_llm.py:637-660), die laufend get_output_async() aufruft und die Ausgaben von EngineCore ins Frontend zieht.

Entwurfsmotivation

  • Ein EngineArgs-Schema bedient beide Pfade: LLM konstruiert direkt EngineArgs(model=model, ...) (llm.py:305-345), AsyncLLM entsteht aus AsyncEngineArgs.from_cli_args(args) (api_server.py:95) – beide verwenden die Felddefinition aus /startup/engine-args, sodass sich Offline- und Serve-Betrieb identisch verhalten.
  • LLM ist eine dünne synchrone Hülle: LLM.generate_run_completion_render_and_add_requests_add_request (offline_utils.py:552-571) → llm_engine.add_request, danach treibt _run_engine die Schleife while has_unfinished_requests(): step_outputs = self.llm_engine.step() (offline_utils.py:594-595) an. So lässt sich in einem Skript ein einzelner for-Ausdruck schreiben, ohne dass der Nutzer mit asyncio in Berührung kommt.
  • AsyncLLM verlagert Blockieren in den output_handler: add_request gibt sofort einen RequestOutputCollector (async_llm.py:280-297) zurück; der eigentliche Token-Strom wird im Hintergrund-output_handler über engine_core.get_output_async() in Chunks zurückgeholt und verarbeitet (async_llm.py:656-676). VLLM_V1_OUTPUT_PROC_CHUNK_SIZE zerteilt einen großen Ausgabeblock, damit der Event-Loop nicht von einem einzelnen, riesigen Batch blockiert wird.
  • LLMEngine ist in v1 nur Kompatibilitätsschicht: Der docstring von class LLMEngine lautet direkt Legacy LLMEngine for backwards compatibility. (llm_engine.py:48-49). Sein __init__ ruft EngineCoreClient.make_client(multiprocess_mode, asyncio_mode=False, ...) auf und vereintigt damit den synchronen Multiprozzess- und In-Process-Modus. make_client (core_client.py:83-105) wählt anhand zweier bools InprocClient / SyncMPClient / AsyncMPClient aus.
  • make_async_mp_client wählt automatisch den DP-Client: Bei data_parallel_size > 1 und data_parallel_external_lb wird DPAsyncMPClient verwendet (ein Client pro DP-Rank), sonst DPLBAsyncMPClient (interner LB verteilt Anfragen über alle DP-Ranks) (core_client.py:116-132). Das ist der Einstieg in die v1-Datenparallelität.
  • LLM.__init__ normalisiert die Konfiguration: Wenn compilation_config ein int ist, wird es automatisch in CompilationConfig(mode=CompilationMode(int)) gewickelt (llm.py:275-282); ist kv_transfer_config ein dict, wird es in KVTransferConfig konvertiert (llm.py:245-262); ist worker_cls eine Klasse, wird sie via cloudpickle serialisiert (llm.py:238-243). Diese Umwandlungen sparen SDK-Aufrufern Boilerplate-Code.
  • from_engine_args als einheitlicher Einstieg: LLM.from_engine_args (llm.py:387-389), AsyncLLM.from_engine_args (async_llm.py:232-254) und LLMEngine.from_engine_args (llm_engine.py:161-186) laufen alle über engine_args.create_engine_config()Executor.get_class(vllm_config) und garantieren, dass die Ableitung von VllmConfig über alle Einstiegspunkte identisch ist.

Schlüsseldateien

Datenfluss

Offline LLM.generate(prompts)

Nachdem generate (llm.py:422) den runner_type validiert hat, delegiert es an OfflineInferenceMixin._run_completion, das _render_and_add_requests (offline_utils.py:523) aufruft. Dort wird jeder Prompt über den Renderer zu einem EngineInput verarbeitet und _add_request aufgerufen:

python
# vllm/entrypoints/offline_utils.py L552-L571
def _add_request(
    self,
    prompt: EngineInput,
    params: SamplingParams | PoolingParams,
    lora_request: LoRARequest | None = None,
    priority: int = 0,
) -> str:
    if isinstance(params, SamplingParams):
        # We only care about the final output
        params.output_kind = RequestOutputKind.FINAL_ONLY

    request_id = str(next(self.request_counter))

    return self.llm_engine.add_request(
        request_id,
        prompt,
        params,
        lora_request=lora_request,
        priority=priority,
    )

Beachten Sie params.output_kind = RequestOutputKind.FINAL_ONLY – im Offline-Fall wird nur das Endergebnis betrachtet, kein Zwischen-Token-Strom, sodass EngineCore auf der Ausgabeseite eine ganze Reihe von Chunked-Streaming-Kosten einspart. Nach _add_request treibt _run_engine EngineCore über eine synchrone Schleife an:

python
# vllm/entrypoints/offline_utils.py L590-L599
# Run the engine.
outputs: list[_O] = []
total_in_toks = 0
total_out_toks = 0
while self.llm_engine.has_unfinished_requests():
    step_outputs = self.llm_engine.step()
    for output in step_outputs:
        assert isinstance(output, output_type)
        if output.finished:
            outputs.append(output)

LLMEngine.step() ist intern nur eine dünne Hülle aus engine_core.get_output() + output_processor.process_outputs; ist engine_core ein InprocClient, wird direkt synchron step_fn() aufgerufen, bei SyncMPClient wird blockierend ZMQ empfangen. Der gesamte Ablauf kommt ohne asyncio aus, sodass auch for output in llm.generate(...) in einem Skript funktioniert.

Serve AsyncLLM.add_request

Serverseitig geht AsyncLLM einen komplett anderen Weg. build_async_engine_client_from_engine_args (api_server.py:109-154) macht aus AsyncEngineArgs ein VllmConfig und ruft AsyncLLM.from_vllm_config (async_llm.py:203-229) auf. Im Konstruktor ist dieser Schritt zentral:

python
# vllm/v1/engine/async_llm.py L146-L153
# EngineCore (starts the engine in background process).
self.engine_core = EngineCoreClient.make_async_mp_client(
    vllm_config=vllm_config,
    executor_class=executor_class,
    log_stats=self.log_stats,
    client_addresses=client_addresses,
    client_count=client_count,
    client_index=client_index,
)

make_async_mp_client (core_client.py:109-132) wählt anhand der DP-Topologie AsyncMPClient oder DPLBAsyncMPClient aus. Danach läuft die output_handler-Koroutine dauerhaft im Hintergrund:

python
# vllm/v1/engine/async_llm.py L656-L676
async def output_handler():
    try:
        while True:
            # 1) Pull EngineCoreOutputs from the EngineCore.
            outputs = await engine_core.get_output_async()
            num_outputs = len(outputs.outputs)

            iteration_stats = (
                IterationStats() if (log_stats and num_outputs) else None
            )

            # Split outputs into chunks of at most
            # VLLM_V1_OUTPUT_PROC_CHUNK_SIZE, so that we don't block the
            # event loop for too long.
            engine_core_outputs = outputs.outputs
            for start in range(0, num_outputs, chunk_size):
                end = start + chunk_size
                outputs_slice = engine_core_outputs[start:end]
                # 2) Process EngineCoreOutputs.
                processed_outputs = output_processor.process_outputs(
                    outputs_slice, outputs.timestamp, iteration_stats

Auf der Anfrageseite ruft die OpenAI-Route async_llm.add_request(request_id, prompt, params, ...) (async_llm.py:280-297) auf und erhält sofort einen RequestOutputCollector, dessen Stream sie anschließend awaited. EngineCore führt unterdessen den Forward aus, die Ausgaben fließen über ZMQ zurück zum AsyncMPClient, werden von output_handler in Chunks abgeholt und verarbeitet und schließlich in den Collector gepusht – die gesamte Kette ist voll asynchron, ein einzelner langsamer Request blockiert den API-Server nicht.

Grenzen und Fehler

  • LLM.generate unterstützt keinen Nicht-generate-Runner: Bei runner_type != "generate" wird direkt ein raise geworfen (llm.py:465-471), mit dem Hinweis, --runner generate zu verwenden; LLM.chat und LLM.enqueue_chat prüfen ebenfalls (llm.py:684-689).
  • LLM(data_parallel_size>1) im Single-Prozess nicht erlaubt: Bei _dp_size > 1 und fehlendem external_launcher ohne TPU wird direkt ein raise geworfen (llm.py:295-303); empfohlen wird der Multiprozess-Ansatz aus examples/features/data_parallel/data_parallel_offline.py, da es sonst zu einem Hang kommt.
  • renderer_num_workers > 1 hat beim Offline-LLM keine Wirkung: LLM geht über den synchronen Renderer-Pfad; der Multi-Worker-Threadpool wird nur in vllm serve / AsyncLLM im asynchronen Pfad konsumiert. Im Offline-Fall wird explizit warning_once ausgegeben (llm.py:370-379), damit Nutzer nicht annehmen, Multi-Threading sei aktiv.
  • EngineDeadError: Am Beginn von AsyncLLM.add_request wird self.errored (async_llm.py:300-301) geprüft; ist der EngineCore-Prozess bereits tot (das Backend hat zuvor ENGINE_CORE_DEAD gesendet), wird direkt EngineDeadError (async_llm.py:1054-1058) geworfen und keine neuen Anfragen mehr angenommen.
  • async_llm.shutdown als Sicherheitsnetz: Im finally-Block von build_async_engine_client_from_engine_args wird async_llm.shutdown(timeout=vllm_config.shutdown_timeout) (api_server.py:152-154) aufgerufen; auch wenn während des Build eine Exception auftritt, werden Hintergrund-Prozess und ZMQ-Socket aufgeräumt. Auch AsyncLLM.__del__ (async_llm.py:256-257) ruft shutdown auf, um Lecks bei der GC zu vermeiden.
  • Validierung von kv_transfer_config als dict: Wenn LLM.__init__ das dict in KVTransferConfig umwandelt und dies fehlschlägt, erfolgt logger.error und anschließend raise ValueError(f"Invalid 'kv_transfer_config' provided: {e}") (llm.py:253-262), wodurch die ursprüngliche ValidationError in eine freundlichere Fehlermeldung eingebettet wird.
  • swap_space ist veraltet: Tritt swap_space in kwargs auf, wird es entfernt und eine DeprecationWarning ausgegeben (llm.py:224-233); künftige Entfernung ist geplant.
  • Degradation, wenn output_handler nicht startet: Löst asyncio.get_running_loop() in __init__ eine RuntimeError aus (kein laufender Event-Loop), wird der output_handler übersprungen (async_llm.py:170-176). In diesem Fall muss der Nutzer selbst das Äquivalent von start_engine_loop=True setzen oder AsyncLLM in einen Event-Loop bringen und await verwenden.
  • Statische DP-Client-Auswahl: make_async_mp_client betrachtet ausschließlich parallel_config.data_parallel_size und data_parallel_external_lb (core_client.py:126-132); ein Umschalten des LB-Modus zur Laufzeit wird nicht unterstützt – die Topologie ist beim Start festgelegt.

Zusammenfassung

LLM und AsyncLLM sind die beiden Haupteinstiege, die vLLM nach oben (Skripte und OpenAI-Server) freigibt; beide implementieren die EngineClient-Abstraktion, nutzen jedoch unterschiedliche EngineCoreClient. LLM geht den synchronen Pfad, intern bestehend aus LLMEngine + SyncMPClient/InprocClient; der Nutzer erhält einen synchronen Iterator im Stil for output in llm.generate(...). AsyncLLM geht den asynchronen Pfad, intern bestehend aus AsyncMPClient/DPLBAsyncMPClient + Hintergrund-output_handler-Koroutine; jede HTTP-Anfrage des API-Servers awaitet lediglich einen RequestOutputCollector. Beide erhalten ihr VllmConfig aus EngineArgs.create_engine_config(), sodass die Konfigurationsseite vollständig konsistent ist; die Unterschiede liegen nur in der Client-Auswahl und im Event-Loop-Modell. Wie EngineArgs / VllmConfig zusammengebaut werden, steht in /startup/engine-args; wie die CLI-Kette bei AsyncLLM.from_vllm_config ankommt, in /startup/cli; zu EngineCore-Hintergrundprozess und ZMQ-Interna siehe /engine/engine-core, die Details der drei EngineCoreClient-Implementierungen in /client/inproc-mp.

Siehe offizielle Dokumentation: vLLM 文档 · README.