LLM et AsyncLLM : les deux entrées, hors ligne et serveur
Responsabilités
LLM est l'API Python synchrone que vLLM expose pour les scénarios d'inférence hors ligne (offline inference)(llm.py:66-67) ; AsyncLLM est le wrapper asynchrone utilisé par l'OpenAI API server dans le scénario service(async_llm.py:70-71). Tous deux héritent de l'abstraction EngineClient, mais empruntent des implémentations EngineCoreClient différentes : LLM passe par défaut par SyncMPClient ou InprocClient, AsyncLLM par AsyncMPClient (ou DPLBAsyncMPClient en scénario DP). Ni l'un ni l'autre n'exécute directement la passe avant — ce sont des conteneurs pour le triptyque InputProcessor, OutputProcessor, EngineCoreClient.
LLM.__init__(llm.py:176-222) accepte presque tous les champs d'EngineArgs comme kwargs, les ré-empacte dans EngineArgs(model=..., tensor_parallel_size=..., ...)(llm.py:305-345) et confie le tout à LLMEngine.from_engine_args(engine_args, usage_context=UsageContext.LLM_CLASS)(llm.py:349-351). Dans v1, LLMEngine est une coquille fine de rétro-compatibilité(llm_engine.py:48-49), qui wrappe en interne le client choisi par EngineCoreClient.make_client(...)(llm_engine.py:105) sous self.engine_core. Ainsi, un appel LLM.generate() se traduit in fine par llm_engine.add_request + une boucle de llm_engine.step.
AsyncLLM suit un autre chemin d'assemblage. build_async_engine_client_from_engine_args(api_server.py:109-154) commence par engine_args.create_engine_config() pour obtenir VllmConfig, puis appelle AsyncLLM.from_vllm_config(vllm_config, ...), qui à l'intérieur lance directement EngineCoreClient.make_async_mp_client(...)(async_llm.py:146-153) pour instancier AsyncMPClient (ou DPLBAsyncMPClient en scénario DP). AsyncLLM ajoute une coroutine résidente output_handler(async_llm.py:637-660) qui rappelle sans cesse get_output_async() pour remonter les sorties d'EngineCore vers le frontend.
Motivation de conception
- Un même schéma EngineArgs pour les deux chemins :
LLMconstruit directementEngineArgs(model=model, ...)(llm.py:305-345) ;AsyncLLMprovient deAsyncEngineArgs.from_cli_args(args)(api_server.py:95) — les deux réutilisent la même définition de champs décrite dans /startup/engine-args, hors ligne et service ont un comportement cohérent. LLMest une coquille synchrone fine :LLM.generate→_run_completion→_render_and_add_requests→_add_request(offline_utils.py:552-571) →llm_engine.add_request, puis_run_engineutilisewhile has_unfinished_requests(): step_outputs = self.llm_engine.step()(offline_utils.py:594-595) pour piloter la boucle. En scripting, un simpleforsuffit, pas d'asyncio à gérer pour l'utilisateur.AsyncLLMcache le blocage dans output_handler :add_requestrenvoie immédiatement unRequestOutputCollector(async_llm.py:280-297) ; le flux réel de tokens est récupéré en arrière-plan parengine_core.get_output_async()dansoutput_handler, qui traite les outputs par chunks(async_llm.py:656-676).VLLM_V1_OUTPUT_PROC_CHUNK_SIZEdécoupe une grosse salve d'outputs pour éviter qu'un batch énorme ne bloque la boucle d'événements.LLMEnginen'est qu'une couche de compatibilité en v1 : la docstring declass LLMEnginele dit explicitement :Legacy LLMEngine for backwards compatibility.(llm_engine.py:48-49). Son__init__appelleEngineCoreClient.make_client(multiprocess_mode, asyncio_mode=False, ...), unifiant les modes multi-processus synchrone et intra-processus.make_client(core_client.py:83-105) sélectionneInprocClient/SyncMPClient/AsyncMPClientselon deux booléens.make_async_mp_clientchoisit automatiquement le client DP :data_parallel_size > 1etdata_parallel_external_lb→DPAsyncMPClient(un client par DP rank) ; sinonDPLBAsyncMPClient(LB interne qui distribue les requêtes à tous les DP ranks)(core_client.py:116-132). C'est l'entrée du parallélisme de données en v1.LLM.__init__normalise la config : sicompilation_configest un int, on l'enveloppe enCompilationConfig(mode=CompilationMode(int))(llm.py:275-282) ; sikv_transfer_configest un dict, conversion enKVTransferConfig(llm.py:245-262) ; siworker_clsest une classe, on sérialise via cloudpickle(llm.py:238-243). Ces conversions évitent au SDK côté appelant du code boilerplate.from_engine_argsunifie l'entrée :LLM.from_engine_args(llm.py:387-389)、AsyncLLM.from_engine_args(async_llm.py:232-254)、LLMEngine.from_engine_args(llm_engine.py:161-186) — ces trois méthodes de classe passent toutes parengine_args.create_engine_config()→Executor.get_class(vllm_config), garantissant que la dérivation deVllmConfigest identique quel que soit l'entrée.
Fichiers clés
LLM 类:66-67—class LLM(BeamSearchOfflineMixin, PoolingOfflineMixin, OfflineInferenceMixin).LLM.__init__:176-222— accepte près de 40 kwargs +**kwargs; convertit en interne dict/int en instances de config correspondantes.EngineArgs 装配:305-345— emballe tous les kwargs de LLM dansEngineArgs(model=..., ...)puislog_non_default_args.LLMEngine.from_engine_args:349-353—usage_context=UsageContext.LLM_CLASS, récupèrellm_engine.model_configetsupported_tasks.LLM.from_engine_args:387-389—cls(**vars(engine_args)): une ligne qui réutilise le constructeur.LLM.generate:422-485— vérifierunner_type == "generate", sampling params par défaut, délègue à_run_completion._add_request:552-571—params.output_kind = RequestOutputKind.FINAL_ONLY, puisllm_engine.add_request._run_engine:573-595—while self.llm_engine.has_unfinished_requests(): step_outputs = self.llm_engine.step(), boucle synchrone.LLMEngine 类:48-49— la coquille fine v1 :Legacy LLMEngine for backwards compatibility..make_client 调用:105-111—self.engine_core = EngineCoreClient.make_client(multiprocess_mode, asyncio_mode=False, ...).LLMEngine.from_engine_args:161-186— exécuteengine_args.create_engine_config, active ou non le multi-processus selonVLLM_ENABLE_V1_MULTIPROCESSING.make_client:83-105— deux booléens pour choisirInprocClient/SyncMPClient/AsyncMPClient.make_async_mp_client:109-132— en scénario DP, choisit automatiquementDPAsyncMPClientouDPLBAsyncMPClient.AsyncLLM 类:70-71—class AsyncLLM(EngineClient):.AsyncLLM.__init__:73-176— assemble le triptyqueInputProcessor,OutputProcessor,EngineCoreClient.make_async_mp_client.AsyncLLM.from_vllm_config:203-229— construit directement à partir deVllmConfig+Executor.get_class, entrée côté serveur.AsyncLLM.from_engine_args:232-254—engine_args.create_engine_config→Executor.get_class→cls(...)._run_output_handler:637-676— coroutine en arrière-plan :get_output_async+output_processor.process_outputs; chunk size contrôlé parVLLM_V1_OUTPUT_PROC_CHUNK_SIZE.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, et dans lefinally,async_llm.shutdown(timeout=vllm_config.shutdown_timeout).AsyncLLM.shutdown:259-271—shutdown_prometheus,renderer.shutdown,engine_core.shutdown, canceloutput_handler.
Flux de données
Hors ligne LLM.generate(prompts)
generate(llm.py:422) valide runner_type puis délègue à OfflineInferenceMixin._run_completion, qui appelle _render_and_add_requests(offline_utils.py:523) ; chaque prompt y est transformé par le renderer en EngineInput puis passé à _add_request :
# 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,
)À noter params.output_kind = RequestOutputKind.FINAL_ONLY — en hors ligne, on ne s'intéresse qu'au résultat final, pas au flux intermédiaire de tokens, ce qui permet à la sortie d'EngineCore d'économiser toute la machinerie de streaming chunked. Après _add_request, _run_engine pilote EngineCore avec une boucle synchrone :
# 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() est une fine enveloppe autour de engine_core.get_output() + output_processor.process_outputs ; si engine_core est InprocClient, il appelle directement step_fn() de façon synchrone, si c'est SyncMPClient, il bloque sur la réception ZMQ. Aucun asyncio dans tout le flux, donc for output in llm.generate(...) fonctionne en script.
Service AsyncLLM.add_request
Côté serveur, AsyncLLM suit un chemin totalement différent. build_async_engine_client_from_engine_args(api_server.py:109-154) transforme AsyncEngineArgs en VllmConfig, puis appelle AsyncLLM.from_vllm_config(async_llm.py:203-229). L'étape clé dans le constructeur est :
# 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) choisit AsyncMPClient ou DPLBAsyncMPClient selon la topologie DP. Ensuite, la coroutine output_handler tourne en arrière-plan :
# 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_statsCôté requête, les routes OpenAI appellent async_llm.add_request(request_id, prompt, params, ...)(async_llm.py:280-297) qui renvoie immédiatement un RequestOutputCollector qu'il suffit ensuite d'await comme un flux. De son côté, EngineCore exécute la passe avant ; les outputs reviennent par ZMQ à AsyncMPClient, puis sont tirés par output_handler, traités par chunks et pushés au collector. Toute la chaîne est asynchrone, l'API server ne se bloque pas sur une requête lente.
Limites et échecs
LLM.generaterefuse les runner non generate : sirunner_type != "generate", lève directement(llm.py:465-471) en suggérant--runner generate;LLM.chatetLLM.enqueue_chatfont le même contrôle(llm.py:684-689).LLM(data_parallel_size>1)n'accepte pas le mono-processus : si_dp_size > 1et que l'on n'est pas enexternal_launcherni sur TPU, lève directement(llm.py:295-303) ; il faut sinon passer par le schéma multi-processusexamples/features/data_parallel/data_parallel_offline.py, sinon ça hang.renderer_num_workers > 1est inopérant surLLMhors ligne :LLMsuit un chemin renderer synchrone ; le pool de threads multi-worker n'est consommé que sur les chemins asynchronesvllm serve/AsyncLLM. En hors ligne, unwarning_onceest émis(llm.py:370-379) pour prévenir l'utilisateur qui pensait activer le multi-thread.EngineDeadError: au début deAsyncLLM.add_request, on vérifieself.errored(async_llm.py:300-301) ; si le processus EngineCore est mort (unENGINE_CORE_DEADa été émis en arrière-plan), on lèveEngineDeadError(async_llm.py:1054-1058) au lieu d'accepter de nouvelles requêtes.async_llm.shutdownen filet de sécurité : dans le blocfinallydebuild_async_engine_client_from_engine_args, on appelleasync_llm.shutdown(timeout=vllm_config.shutdown_timeout)(api_server.py:152-154), qui nettoie les processus en arrière-plan et les sockets ZMQ même en cas d'erreur pendant le build.AsyncLLM.__del__(async_llm.py:256-257) appelle aussi shutdown en filet de sécurité, pour éviter une fuite lors du GC.- Validation du dict
kv_transfer_config: dansLLM.__init__, si la conversion du dict enKVTransferConfigéchoue, on log en error puis on lèveValueError(f"Invalid 'kv_transfer_config' provided: {e}")(llm.py:253-262) pour emballer le ValidationError d'origine dans un message plus friendly. swap_spaceest déprécié : si présent danskwargs, on le pop et on émet unDeprecationWarning(llm.py:224-233), à supprimer à l'avenir.- Dégradation si output_handler n'a pas démarré : dans
__init__, siasyncio.get_running_loop()lèveRuntimeError(pas de boucle d'événements en cours), on saute output_handler(async_llm.py:170-176). L'utilisateur doit alors appeler explicitement l'équivalent destart_engine_loop=True, ou intégrer AsyncLLM dans une boucle d'événements avant de l'await. - Sélection du client DP statique :
make_async_mp_clientne regarde queparallel_config.data_parallel_sizeetdata_parallel_external_lb(core_client.py:126-132) — pas de bascule de mode LB à l'exécution, la topologie est figée au démarrage.
Résumé
LLM et AsyncLLM sont les deux entrées principales que vLLM expose à la couche supérieure (scripts et OpenAI server) ; tous deux implémentent l'abstraction EngineClient mais empruntent des EngineCoreClient différents. LLM suit un chemin synchrone, porté par LLMEngine + SyncMPClient/InprocClient, et expose un itérateur synchrone de type for output in llm.generate(...). AsyncLLM suit un chemin asynchrone, porté par AsyncMPClient/DPLBAsyncMPClient + la coroutine output_handler en arrière-plan ; chaque requête HTTP de l'API server ne fait qu'await un RequestOutputCollector. Tous deux obtiennent VllmConfig via EngineArgs.create_engine_config(), donc la configuration est strictement identique ; seuls diffèrent le choix du client et le modèle de boucle d'événements. Pour la façon dont EngineArgs / VllmConfig sont assemblés, voir /startup/engine-args ; pour la chaîne CLI jusqu'à AsyncLLM.from_vllm_config, voir /startup/cli ; pour le processus en arrière-plan EngineCore et le ZMQ interne, voir /engine/engine-core ; pour les détails des trois implémentations EngineCoreClient, voir /client/inproc-mp.
Voir la documentation officielle : Documentation vLLM · README