Skip to content

LLMEngine : la coquille synchrone du moteur v1 ​

源码版本v0.25.1

Responsabilités ​

Dans l'architecture v1, LLMEngine est la fine coquille d'entrée pour les API synchrones (LLM.generate, LLM.chat). Elle ne lance pas de forward, n'ordonnance pas, n'alloue pas de KV cache ; elle se contente de coller les trois ensemble : InputProcessor transforme le prompt entrant en EngineCoreRequest, OutputProcessor traduit les EngineCoreOutputs en RequestOutput, et le vrai travail est confié à EngineCore. La dernière ligne de __init__, self.engine_core = EngineCoreClient.make_client(...) (llm_engine.py:105-111), est le vrai point d'entrée du cœur du moteur (EngineCore) — make_client choisit parmi InprocClient, SyncMPClient et AsyncMPClient selon qu'on est multi-processus ou asyncio.

L'intérêt de cette couche est de préserver l'usage synchrone LLMEngine.add_request() / .step() de l'ère v0 : en mode in-process, InprocClient détient directement l'instance d'EngineCore (core_client.py:286-292), get_output() appelle step_fn() en place, front et back partagent la mémoire, pas de ZMQ, pas de busy loop ; en mode multi-processus, SyncMPClient lance un EngineCoreProc en arrière-plan (core.py:896-897), LLMEngine expose la même API synchrone, simplement pontée par ZMQ en dessous. C'est pourquoi LLMEngine est très peu de code (448 lignes) : beaucoup de méthodes ne font que transmettre l'appel à engine_core (sleep/wake, profile, lora, reset_prefix_cache sont toutes des transmissions d'une ligne).

Motivation de conception ​

  • Préserver l'API synchrone v0 : la classe LLM expose les méthodes synchrones generate/chat, et en interne LLMEngine.add_request() + LLMEngine.step() en boucle (llm_engine.py:296-334) ; pas besoin d'asyncio pour faire tourner v1. C'est la clé pour que v1 remplace v0 sans casser les workflows d'inférence offline.
  • Trois backends, une seule interface : la sélection 3-en-1 de make_client (core_client.py:83-105) fait qu'aucune méthode de LLMEngine n'a à se soucier de savoir si on est in-process, multi-processus synchrone ou multi-processus asynchrone ; il suffit d'avoir une instance d'EngineCoreClient, et le backend est choisi à la construction.
  • Transformation entrée/sortie laissée à la couche shell : InputProcessor (input_processor.py:36) gère le multimodal (multimodal), la tokenization, la priority, les trace_headers, LoRA ; OutputProcessor (output_processor.py:417) gère la détokenization, le découpage en streaming, le matching de stop string, et la conversion des EngineCoreOutputs en RequestOutput. Ces deux salles de données sales restent dans la couche shell ; EngineCore ne voit que les structures pures EngineCoreRequest / EngineCoreOutputs.
  • Échantillonnage multiple n>1 : quand SamplingParams.n est supérieur à 1, add_request utilise ParentRequest (parallel_sampling.py:13) pour dérouler une requête parent en n sous-requêtes poussées dans EngineCore et OutputProcessor (llm_engine.py:279-294) ; la couche output réagrège ensuite les flux de tokens des sous-requêtes en une requête parent.
  • Filet de sécurité à la sortie du processus : quand multiprocess_mode=False, un finalizer à weak ref sur le modèle driver est posé (llm_engine.py:129-133) ; à la GC de LLMEngine, il appelle _cleanup_instance_caches pour libérer la mémoire GPU retenue par les byte code hooks.

Fichiers clés ​

  • LLMEngine class:48-49 — class LLMEngine:, docstring auto-proclamée Legacy LLMEngine for backwards compatibility..
  • LLMEngine.__init__:51-141 — assemble renderer / InputProcessor / OutputProcessor / EngineCoreClient, puis lance StatLoggerManager et le cleanup finalizer.
  • make_client appel:105-111 — EngineCoreClient.make_client(multiprocess_mode=..., asyncio_mode=False, ...) choisit le backend.
  • from_vllm_config:143-158 — construit depuis VllmConfig ; multiprocess_mode est décidé par envs.VLLM_ENABLE_V1_MULTIPROCESSING.
  • add_request:218-294 — valide request_id, input_processor.process_inputs, branchement ParentRequest si n>1, puis engine_core.add_request.
  • step:296-334 — engine_core.get_output() → output_processor.process_outputs → abort_requests → logger_manager.record.
  • abort_request:212-216 — marque d'abord dans OutputProcessor, puis laisse EngineCore retirer la requête du scheduler.
  • sleep / wake_up:361-376 — sleep/wake traversant renderer + engine_core, StatLoggerManager enregistre l'état sleep.
  • do_log_stats_with_interval:394-401 — utilise VLLM_LOG_STATS_INTERVAL pour throttler les logs et éviter un flush à chaque step.
  • _get_driver_model_for_cleanup:431-434 — suit la chaîne model_executor.driver_worker.model_runner.model pour récupérer le modèle driver, à des fins de libération mémoire par le finalizer.

Flux de données ​

Une inférence synchrone, c'est for req in requests: add_request(...) puis while has_unfinished_requests(): step(). La structure de step() est directe : tirer un batch d'EngineCoreOutputs depuis engine_core, le passer à OutputProcessor pour le traduire, renvoyer au abort les requêtes terminées en avance par stop string, et au passage enregistrer des statistiques.

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)

Il y a ensuite des étapes 3 et 4 (llm_engine.py:317-332) : l'étape 3 passe processed_outputs.reqs_to_abort à engine_core.abort_requests, parce que les stop strings ne sont visibles qu'après détokenization, EngineCore lui-même n'en sait rien ; l'étape 4, si logger_manager est non vide et qu'il y a des scheduler_stats, appelle record + do_log_stats_with_interval.

add_request prend la branche fan-out quand n>1 (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

Le dernier child réutilise l'objet request pour économiser une copie ; l'ID de la requête parent est produit par ParentRequest.get_child_info(idx), et l'OutputProcessor, qui récupère la référence parent et l'idx, peut ensuite réagréger les sorties des children.

Limites et échecs ​

  • EngineCoreRequest est déprécié : si add_request reçoit directement un EngineCoreRequest, on déclenche un warning_once de dépréciation indiquant que depuis v0.18 il faut utiliser Renderer.render_cmpl() / render_chat() (llm_engine.py:235-248) ; si le request_id passé ne correspond pas à EngineCoreRequest.request_id, c'est ce dernier qui l'emporte, le premier est ignoré.
  • Dummy batch en mode DP : en mode data parallel external launcher, si un rank n'a rien à faire mais que d'autres tournent encore, has_unfinished_requests_dp met should_execute_dummy_batch à True (llm_engine.py:197-203) ; step() commence alors par lancer un dummy batch pour occuper la place (llm_engine.py:297-300) afin de ne pas bloquer la communication collective.
  • model_executor n'est accessible qu'en mode in-process : uniquement dans la branche if not multiprocess_mode: on a self.model_executor = self.engine_core.engine_core.model_executor (llm_engine.py:123-125) ; en mode multi-processus cet objet n'est pas accessible depuis le frontal, donc tout code qui touche à model_executor doit d'abord s'assurer que multiprocess_mode=False.
  • Throttling des logs de stats : do_log_stats_with_interval contrôle l'intervalle minimal via VLLM_LOG_STATS_INTERVAL (llm_engine.py:394-401) ; _last_log_time est une instance à initialisation paresseuse au premier accès, ce qui contourne la nécessité de l'initialiser dans __init__.
  • sleep couplé au renderer : sleep(level>=1) nettoie d'abord le cache multimodal du renderer avant d'appeler engine_core.sleep (llm_engine.py:361-367) ; après un wake_up, les embeddings multimodaux peuvent être invalides et doivent être recalculés par le renderer.
  • Le backend d'EngineCore est fixé par une variable d'env : dans from_vllm_config, multiprocess_mode=envs.VLLM_ENABLE_V1_MULTIPROCESSING (llm_engine.py:157) ; from_engine_args le re-définit une fois (llm_engine.py:174-176), donc l'option multi-processus est figée à la configuration, pas de bascule à runtime.

Résumé ​

LLMEngine est la coquille fine du chemin synchrone v1 : valider les entrées, gérer le multimodal et le streaming via InputProcessor / OutputProcessor, choisir le backend via EngineCoreClient.make_client, puis boucler sur add_request + step. Elle délègue tout le gros travail à EngineCore (en in-process directement, sinon via ZMQ), et ne conserve que les affaires de la couche shell : compat v0, fan-out ParentRequest, throttling des stats, couplage sleep/wake. Pour descendre : /engine/engine-core pour l'assemblage du cœur, /engine/engine-core-proc pour l'implémentation du processus arrière ZMQ, les trois implémentations client à /client/inproc-mp, et l'intérieur du scheduler à /scheduler/scheduler.

Voir la documentation officielle : Documentation vLLM · README