LLM und AsyncLLM: Zwei Einstiegspfade für Offline und Serve
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:
LLMkonstruiert direktEngineArgs(model=model, ...)(llm.py:305-345),AsyncLLMentsteht ausAsyncEngineArgs.from_cli_args(args)(api_server.py:95) – beide verwenden die Felddefinition aus /startup/engine-args, sodass sich Offline- und Serve-Betrieb identisch verhalten. LLMist 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_enginedie Schleifewhile has_unfinished_requests(): step_outputs = self.llm_engine.step()(offline_utils.py:594-595) an. So lässt sich in einem Skript ein einzelnerfor-Ausdruck schreiben, ohne dass der Nutzer mit asyncio in Berührung kommt.AsyncLLMverlagert Blockieren in den output_handler:add_requestgibt sofort einenRequestOutputCollector(async_llm.py:280-297) zurück; der eigentliche Token-Strom wird im Hintergrund-output_handlerüberengine_core.get_output_async()in Chunks zurückgeholt und verarbeitet (async_llm.py:656-676).VLLM_V1_OUTPUT_PROC_CHUNK_SIZEzerteilt einen großen Ausgabeblock, damit der Event-Loop nicht von einem einzelnen, riesigen Batch blockiert wird.LLMEngineist in v1 nur Kompatibilitätsschicht: Der docstring vonclass LLMEnginelautet direktLegacy LLMEngine for backwards compatibility.(llm_engine.py:48-49). Sein__init__ruftEngineCoreClient.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 boolsInprocClient/SyncMPClient/AsyncMPClientaus.make_async_mp_clientwählt automatisch den DP-Client: Beidata_parallel_size > 1unddata_parallel_external_lbwirdDPAsyncMPClientverwendet (ein Client pro DP-Rank), sonstDPLBAsyncMPClient(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: Wenncompilation_configein int ist, wird es automatisch inCompilationConfig(mode=CompilationMode(int))gewickelt (llm.py:275-282); istkv_transfer_configein dict, wird es inKVTransferConfigkonvertiert (llm.py:245-262); istworker_clseine Klasse, wird sie via cloudpickle serialisiert (llm.py:238-243). Diese Umwandlungen sparen SDK-Aufrufern Boilerplate-Code.from_engine_argsals einheitlicher Einstieg:LLM.from_engine_args(llm.py:387-389),AsyncLLM.from_engine_args(async_llm.py:232-254) undLLMEngine.from_engine_args(llm_engine.py:161-186) laufen alle überengine_args.create_engine_config()→Executor.get_class(vllm_config)und garantieren, dass die Ableitung vonVllmConfigüber alle Einstiegspunkte identisch ist.
Schlüsseldateien
LLM-Klasse:66-67— Definition vonclass LLM(BeamSearchOfflineMixin, PoolingOfflineMixin, OfflineInferenceMixin).LLM.__init__:176-222— akzeptiert knapp 40 kwargs +**kwargs, wandelt dict/int intern in die entsprechenden Config-Instanzen um.EngineArgs-Zusammenbau:305-345— LLM-kwargs werden inEngineArgs(model=..., ...)gepackt, gefolgt vonlog_non_default_args.LLMEngine.from_engine_args:349-353—usage_context=UsageContext.LLM_CLASS, liefertllm_engine.model_configundsupported_tasks.LLM.from_engine_args:387-389—cls(**vars(engine_args))in einer Zeile, direkte Wiederverwendung des Konstruktors.LLM.generate:422-485— prüftrunner_type == "generate", wendet Default-Sampling-Params an, delegiert an_run_completion._add_request:552-571—params.output_kind = RequestOutputKind.FINAL_ONLY, ruftllm_engine.add_requestauf._run_engine:573-595—while self.llm_engine.has_unfinished_requests(): step_outputs = self.llm_engine.step()als synchrone Schleife.LLMEngine-Klasse:48-49— Die v1-Hülle mitLegacy LLMEngine for backwards compatibility..make_client-Aufruf:105-111—self.engine_core = EngineCoreClient.make_client(multiprocess_mode, asyncio_mode=False, ...).LLMEngine.from_engine_args:161-186— führtengine_args.create_engine_configaus und entscheidet anhand vonVLLM_ENABLE_V1_MULTIPROCESSING, ob Multiprozzess aktiviert wird.make_client:83-105— wählt anhand zweier boolsInprocClient/SyncMPClient/AsyncMPClient.make_async_mp_client:109-132— wählt im DP-Fall automatischDPAsyncMPClientoderDPLBAsyncMPClient.AsyncLLM-Klasse:70-71— Definition vonclass AsyncLLM(EngineClient):.AsyncLLM.__init__:73-176— Montage des Trios ausInputProcessor,OutputProcessor,EngineCoreClient.make_async_mp_client.AsyncLLM.from_vllm_config:203-229— konstruiert direkt ausVllmConfig+Executor.get_class, serverseitiger Einstieg.AsyncLLM.from_engine_args:232-254—engine_args.create_engine_config→Executor.get_class→cls(...)._run_output_handler:637-676— Hintergrund-Koroutine,get_output_async+output_processor.process_outputs, Chunk-Größe überVLLM_V1_OUTPUT_PROC_CHUNK_SIZEgesteuert.build_async_engine_client:78-105—AsyncEngineArgs.from_cli_args→build_async_engine_client_from_engine_args.build_async_engine_client_from_engine_args:109-154—engine_args.create_engine_config→AsyncLLM.from_vllm_config, im finally-Blockasync_llm.shutdown(timeout=vllm_config.shutdown_timeout).AsyncLLM.shutdown:259-271—shutdown_prometheus,renderer.shutdown,engine_core.shutdownund Abbruch desoutput_handler.
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:
# 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:
# 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:
# 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:
# 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_statsAuf 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.generateunterstützt keinen Nicht-generate-Runner: Beirunner_type != "generate"wird direkt ein raise geworfen (llm.py:465-471), mit dem Hinweis,--runner generatezu verwenden;LLM.chatundLLM.enqueue_chatprüfen ebenfalls (llm.py:684-689).LLM(data_parallel_size>1)im Single-Prozess nicht erlaubt: Bei_dp_size > 1und fehlendemexternal_launcherohne TPU wird direkt ein raise geworfen (llm.py:295-303); empfohlen wird der Multiprozess-Ansatz ausexamples/features/data_parallel/data_parallel_offline.py, da es sonst zu einem Hang kommt.renderer_num_workers > 1hat beim Offline-LLMkeine Wirkung:LLMgeht über den synchronen Renderer-Pfad; der Multi-Worker-Threadpool wird nur invllm serve/AsyncLLMim asynchronen Pfad konsumiert. Im Offline-Fall wird explizitwarning_onceausgegeben (llm.py:370-379), damit Nutzer nicht annehmen, Multi-Threading sei aktiv.EngineDeadError: Am Beginn vonAsyncLLM.add_requestwirdself.errored(async_llm.py:300-301) geprüft; ist der EngineCore-Prozess bereits tot (das Backend hat zuvorENGINE_CORE_DEADgesendet), wird direktEngineDeadError(async_llm.py:1054-1058) geworfen und keine neuen Anfragen mehr angenommen.async_llm.shutdownals Sicherheitsnetz: Imfinally-Block vonbuild_async_engine_client_from_engine_argswirdasync_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. AuchAsyncLLM.__del__(async_llm.py:256-257) ruft shutdown auf, um Lecks bei der GC zu vermeiden.- Validierung von
kv_transfer_configals dict: WennLLM.__init__das dict inKVTransferConfigumwandelt und dies fehlschlägt, erfolgtlogger.errorund anschließendraise ValueError(f"Invalid 'kv_transfer_config' provided: {e}")(llm.py:253-262), wodurch die ursprüngliche ValidationError in eine freundlichere Fehlermeldung eingebettet wird. swap_spaceist veraltet: Trittswap_spaceinkwargsauf, wird es entfernt und eineDeprecationWarningausgegeben (llm.py:224-233); künftige Entfernung ist geplant.- Degradation, wenn output_handler nicht startet: Löst
asyncio.get_running_loop()in__init__eineRuntimeErroraus (kein laufender Event-Loop), wird der output_handler übersprungen (async_llm.py:170-176). In diesem Fall muss der Nutzer selbst das Äquivalent vonstart_engine_loop=Truesetzen oder AsyncLLM in einen Event-Loop bringen undawaitverwenden. - Statische DP-Client-Auswahl:
make_async_mp_clientbetrachtet ausschließlichparallel_config.data_parallel_sizeunddata_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.