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-49class 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-111EngineCoreClient.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-334engine_core.get_output()output_processor.process_outputsabort_requestslogger_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