Skip to content

LLM et AsyncLLM : les deux entrées, hors ligne et serveur

源码版本v0.25.1

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 : LLM construit directement EngineArgs(model=model, ...)(llm.py:305-345) ; AsyncLLM provient de AsyncEngineArgs.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.
  • LLM est 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_engine utilise while has_unfinished_requests(): step_outputs = self.llm_engine.step()(offline_utils.py:594-595) pour piloter la boucle. En scripting, un simple for suffit, pas d'asyncio à gérer pour l'utilisateur.
  • AsyncLLM cache le blocage dans output_handler : add_request renvoie immédiatement un RequestOutputCollector(async_llm.py:280-297) ; le flux réel de tokens est récupéré en arrière-plan par engine_core.get_output_async() dans output_handler, qui traite les outputs par chunks(async_llm.py:656-676). VLLM_V1_OUTPUT_PROC_CHUNK_SIZE découpe une grosse salve d'outputs pour éviter qu'un batch énorme ne bloque la boucle d'événements.
  • LLMEngine n'est qu'une couche de compatibilité en v1 : la docstring de class LLMEngine le dit explicitement : Legacy LLMEngine for backwards compatibility.(llm_engine.py:48-49). Son __init__ appelle EngineCoreClient.make_client(multiprocess_mode, asyncio_mode=False, ...), unifiant les modes multi-processus synchrone et intra-processus. make_client(core_client.py:83-105) sélectionne InprocClient / SyncMPClient / AsyncMPClient selon deux booléens.
  • make_async_mp_client choisit automatiquement le client DP : data_parallel_size > 1 et data_parallel_external_lbDPAsyncMPClient (un client par DP rank) ; sinon DPLBAsyncMPClient (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 : si compilation_config est un int, on l'enveloppe en CompilationConfig(mode=CompilationMode(int))(llm.py:275-282) ; si kv_transfer_config est un dict, conversion en KVTransferConfig(llm.py:245-262) ; si worker_cls est une classe, on sérialise via cloudpickle(llm.py:238-243). Ces conversions évitent au SDK côté appelant du code boilerplate.
  • from_engine_args unifie 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 par engine_args.create_engine_config()Executor.get_class(vllm_config), garantissant que la dérivation de VllmConfig est identique quel que soit l'entrée.

Fichiers clés

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 :

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

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

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

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) choisit AsyncMPClient ou DPLBAsyncMPClient selon la topologie DP. Ensuite, la coroutine output_handler tourne en arrière-plan :

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

Cô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.generate refuse les runner non generate : si runner_type != "generate", lève directement(llm.py:465-471) en suggérant --runner generate ; LLM.chat et LLM.enqueue_chat font le même contrôle(llm.py:684-689).
  • LLM(data_parallel_size>1) n'accepte pas le mono-processus : si _dp_size > 1 et que l'on n'est pas en external_launcher ni sur TPU, lève directement(llm.py:295-303) ; il faut sinon passer par le schéma multi-processus examples/features/data_parallel/data_parallel_offline.py, sinon ça hang.
  • renderer_num_workers > 1 est inopérant sur LLM hors ligne : LLM suit un chemin renderer synchrone ; le pool de threads multi-worker n'est consommé que sur les chemins asynchrones vllm serve / AsyncLLM. En hors ligne, un warning_once est émis(llm.py:370-379) pour prévenir l'utilisateur qui pensait activer le multi-thread.
  • EngineDeadError : au début de AsyncLLM.add_request, on vérifie self.errored(async_llm.py:300-301) ; si le processus EngineCore est mort (un ENGINE_CORE_DEAD a été émis en arrière-plan), on lève EngineDeadError(async_llm.py:1054-1058) au lieu d'accepter de nouvelles requêtes.
  • async_llm.shutdown en filet de sécurité : dans le bloc finally de build_async_engine_client_from_engine_args, on appelle async_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 : dans LLM.__init__, si la conversion du dict en KVTransferConfig échoue, on log en error puis on lève ValueError(f"Invalid 'kv_transfer_config' provided: {e}")(llm.py:253-262) pour emballer le ValidationError d'origine dans un message plus friendly.
  • swap_space est déprécié : si présent dans kwargs, on le pop et on émet un DeprecationWarning(llm.py:224-233), à supprimer à l'avenir.
  • Dégradation si output_handler n'a pas démarré : dans __init__, si asyncio.get_running_loop() lève RuntimeError (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 de start_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_client ne regarde que parallel_config.data_parallel_size et data_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