vLLM Explained

The OpenAI-compatible server: launch modes, app, serving classes and streaming

IntermediatevLLM conceptsabout 17 min read + 7 min deep dives

Code version: vLLM @ 3e44c0d · 2026-10-22

In one sentence

vllm serve runs a FastAPI app on uvicorn; each route hands the request to a serving class, which renders it and calls AsyncLLM.

The idea#

The page The OpenAI-compatible API shows the API from the outside: routes, fields and the stream format. This page shows the inside of the API server. The API server is one process (or several) with an asyncio event loop. uvicorn reads HTTP, FastAPI matches the route, and a route function calls a serving class. The serving class checks the request, asks the renderer for token ids, makes the sampling parameters and calls AsyncLLM, the EngineClient (The engine client: AsyncLLM in the API server).

The server makes one object per serving class at start-up and keeps it in the app state. So a route function is short: it takes the serving object from app.state, calls one method, and turns the result into an HTTP response. The result is an error, a full JSON body, or an async generator of SSE strings.

Some work is the same for every route. Middleware checks CORS and the API key. Exception handlers turn Python exceptions into HTTP status codes. Decorators on the route functions watch for a client disconnect and count the load.

An everyday comparison. Think of the front desk of a large office. A guard at the door checks the visitor's badge (middleware). Signs send the visitor to the right counter (the router). The clerk at the counter checks the form and copies it onto the internal form (renderer and sampling parameters). Then the clerk puts it into the internal mail (AsyncLLM). The comparison stops being true for streams. The clerk promises the answer piece by piece before the mail has left the building. So a stream starts with HTTP 200 before the engine sees the request.

The picture#

Figure: the six layers that one chat request crosses in the API server (panel 1), and the three things a serving class can return (panel 2).
Open full size ↗

Read panel 1 along the arrows: the top row first, then the bottom row. Blue boxes are library code (uvicorn, Starlette, FastAPI); violet boxes are vLLM code; the amber box is the boundary to the engine. All six layers run in one process, on one event loop.

In the code#

Step 1: the launch modes of vllm serve#

ServeSubcommand.cmd picks one of five ways to start (vllm/entrypoints/cli/serve.py:54-156). The page entrypoints/cli/serve.py: the vllm serve subcommand and its launch modes reads it line by line; the short form is:

Figure: the checks of ServeSubcommand.cmd in order (panel 1) and the processes of three common modes (panel 2).
Open full size ↗

The Rust frontend (one paragraph). rust/ holds an experimental OpenAI-compatible frontend in Rust. It talks to the same engine core processes over the same ZMQ boundary (rust/README.md:1-5). With VLLM_USE_RUST_FRONTEND=1, vllm serve takes the multi-server path, forces the API server count to 1, and starts the Rust binary instead of Python API servers (vllm/entrypoints/cli/serve.py:131-136, vllm/entrypoints/cli/serve.py:346-365). The README calls it experimental and not feature-complete, and this site does not explain it further.

Step 2: build_app makes the FastAPI app#

After AsyncLLM is ready, build_and_serve asks the engine for the supported tasks, builds the app, fills the app state and starts uvicorn (vllm/entrypoints/launchers/api_server/entry.py:115-161):

    if args.disable_fastapi_docs:
        app = FastAPI(
            openapi_url=None, docs_url=None, redoc_url=None, lifespan=lifespan
        )
    elif args.enable_offline_docs:
        app = FastAPI(docs_url=None, redoc_url=None, lifespan=lifespan)
    else:
        app = FastAPI(lifespan=lifespan)
    app.state.args = args
    app.root_path = args.root_path

    register_api_routers(args, app, supported_tasks, model_config)

    # Endpoint plugin hooks run after core routers so plugins can inspect them.
    # This runs even for the CPU only render server. A plugin eligible for
    # the `render` task still gets its routes registered. It receives
    # `engine_client=None` at Phase B (see `_init_endpoint_plugins_state`).
    attach_endpoint_plugins(app, supported_tasks)

    init_exception_handler(app)
    init_entrypoints_middleware(args, app, supported_tasks)
    app = sagemaker_standards_bootstrap(app)
    return app
  1. Lines 34-41: --disable-fastapi-docs removes /docs, /redoc and the OpenAPI schema; --enable-offline-docs serves the bundled Swagger files.
  2. Line 45: register_api_routers adds the routers by task and flag (vllm/entrypoints/launchers/api_server/routers.py:12-90). The route table is on The OpenAI-compatible API.
  3. Line 51: endpoint plugins attach their routes after the core routes, so they can replace one (Plugins in the API server).
  4. Line 53: the exception handlers, in four layers (vllm/entrypoints/serve/exception_handling/register.py:26-42).
  5. Line 54: the middleware (vllm/entrypoints/serve/middleware/register.py:19-75):
  6. Line 55: the external package model_hosting_container_standards wraps the app for SageMaker hosting (vllm/entrypoints/serve/sagemaker/api_router.py:145-159). The SageMaker routes /ping and /invocations are already registered on every server (vllm/entrypoints/launchers/api_server/routers.py:40-44). /invocations picks a handler by the type of the body and runs the same serving objects as the /v1 routes (vllm/entrypoints/serve/sagemaker/api_router.py:98-132).

serve_http then prints every route and runs uvicorn with NoSignalServer, which leaves SIGINT and SIGTERM to vLLM's own shutdown handler (vllm/entrypoints/launchers/launcher.py:47-72, vllm/entrypoints/launchers/launcher.py:251-291). A watchdog task checks the engine every 5 seconds (vllm/entrypoints/launchers/launcher.py:314-337).

Step 3: init_app_state makes the serving objects#

    state.engine_client = engine_client
    state.log_stats = not args.disable_log_stats
    state.vllm_config = vllm_config
    state.args = args
    resolved_chat_template = load_chat_template(args.chat_template)
    default_chat_template_kwargs = resolve_default_chat_template_kwargs(args)
    state.tool_server = (
        await init_tool_server(args) if "generate" in supported_tasks else None
    )

    # Merge default_mm_loras into the static lora_modules
    default_mm_loras = (
        vllm_config.lora_config.default_mm_loras
        if vllm_config.lora_config is not None
        else {}
    )
    lora_modules = process_lora_modules(args.lora_modules, default_mm_loras)

    state.openai_serving_models = OpenAIServingModels(
        engine_client=engine_client,
        base_model_paths=base_model_paths,
        lora_modules=lora_modules,
    )
    await state.openai_serving_models.init_static_loras()

    state.online_renderer = OnlineRenderer(
        model_config=engine_client.model_config,
        renderer=engine_client.renderer,
        request_logger=request_logger,
        chat_template=resolved_chat_template,
        chat_template_content_format=args.chat_template_content_format,
        trust_request_chat_template=args.trust_request_chat_template,
        trust_request_mm_kwargs=args.trust_request_mm_kwargs,
        enable_auto_tools=args.enable_auto_tool_choice,
        exclude_tools_when_tool_choice_none=args.exclude_tools_when_tool_choice_none,
        tool_parser=args.tool_call_parser,
        tool_strict_level=args.tool_strict_level,
        reasoning_parser=args.structured_outputs_config.reasoning_parser,
        default_chat_template_kwargs=default_chat_template_kwargs,
        log_error_stack=args.log_error_stack,
    )
    state.online_renderer.warmup()

The state holds the engine client, the args, and the objects that every server needs. These are OpenAIServingModels (for /v1/models and LoRA names), the OnlineRenderer and the ServingTokenization object (vllm/entrypoints/launchers/api_server/app_state.py:122-130). For the generate task, init_generate_state adds one object per generate API (vllm/entrypoints/generate/api_router.py:104-172):

app.state attributeClassRoutes
openai_serving_chatOpenAIServingChat/v1/chat/completions
openai_serving_chat_batchOpenAIServingChatBatch/v1/chat/completions/batch
openai_serving_completionOpenAIServingCompletion/v1/completions
openai_serving_responsesOpenAIServingResponses/v1/responses
anthropic_serving_messagesAnthropicServingMessages/v1/messages
serving_embedding, serving_pooling, ...pooling classes/v1/embeddings, /pooling, ... (only pooling models, vllm/entrypoints/pooling/factories.py:177-220)
serving_tokenizationServingTokenization/tokenize, /detokenize
openai_serving_modelsOpenAIServingModels/v1/models, LoRA routes

Each object reads its flags once, at construction. For example, OpenAIServingChat reads the model's default sampling parameters and builds its parser class in __init__ (vllm/entrypoints/openai/chat_completion/serving.py:167-187). So a change of the model's generation_config.json needs a restart.

Step 4: the serving class hierarchy#

Figure: the serving classes as a tree (panel 1) and the helpers that each layer adds (panel 2).
Open full size ↗

The three layers split the work:

  1. BaseServing knows the model names. _check_model accepts the served name, a loaded LoRA name, or (with VLLM_ALLOW_RUNTIME_LORA_UPDATING) a name that a LoRA resolver can load. Any other name gives HTTP 404 NotFoundError (vllm/entrypoints/serve/engine/serving.py:49-84). create_error_response maps an exception class to a status code (vllm/entrypoints/serve/exception_handling/error_response.py:14-96).
  2. GenerateBaseServing knows the engine. It takes the renderer and the input processor from the engine client (vllm/entrypoints/generate/base/serving.py:148-151). Its _preflight runs the engine checks that must come before a stream starts:
        def _preflight(self, n: int = 1) -> None:
            """Engine checks that must run before a response is started.
    
            This is required for the streaming case, where we return a success
            status before we actually start generating text :).
    
            Args:
                n: Number of sequences the request will occupy.
    
            """
            if self.engine_client.errored:
                raise self.engine_client.dead_error
            self.engine_client.check_admission(n)
  3. OpenAIServingChat (and its siblings) knows one API format: the fields, the chunks and the full response.

Step 5: the request flow inside one serving call#

For /v1/chat/completions, _create_chat_completion runs these steps in order (vllm/entrypoints/openai/chat_completion/serving.py:302-438). /v1/completions has the same shape without the chat template (vllm/entrypoints/openai/completion/serving.py:131-296).

  1. Validate. Pydantic and the model validators of the request class have already run in FastAPI. Errors there give HTTP 400 (The OpenAI-compatible API, errors deep dive).
  2. Check the model and the engine. _check_model, then _preflight(n) (vllm/entrypoints/openai/chat_completion/serving.py:278-283).
  3. Render. The online renderer applies the chat template and tokenizes; the prompt length check runs here (vllm/entrypoints/openai/chat_completion/serving.py:285). See Renderers and chat templates: from messages to token ids.
  4. Request id. "chatcmpl-" + the X-Request-Id header, else the body field request_id (a random UUID by default) (vllm/entrypoints/openai/chat_completion/serving.py:320-322, vllm/entrypoints/serve/engine/serving.py:132-142).
  5. Sampling parameters. get_max_tokens, then to_sampling_params with output_kind DELTA for a stream and FINAL_ONLY otherwise (vllm/entrypoints/openai/chat_completion/serving.py:349-369).
  6. Make the generator. engine_client.generate(...) with the priority, the DP rank header and the reasoning flags (vllm/entrypoints/openai/chat_completion/serving.py:396-408). This only makes an async generator; the engine sees nothing yet.
  7. Answer. For a stream, return chat_completion_stream_generator; else await chat_completion_full_generator (vllm/entrypoints/openai/chat_completion/serving.py:415-438).

Step 2 and step 6 meet: check_admission runs in _preflight and again inside AsyncLLM.add_request, when the generator starts (vllm/v1/engine/async_llm.py:551, vllm/v1/engine/async_llm.py:575-578). The first check gives a clean HTTP 503 before a stream starts; the second one sees the requests that other tasks added in between.

Step 6: streaming#

The route wraps the generator of SSE strings in a StreamingResponse (vllm/entrypoints/openai/chat_completion/api_router.py:80-85). Three details matter in production:

Step 7: non-streaming answers, n > 1, logprobs and usage#

Without a stream, the output kind is FINAL_ONLY. The output processor makes no RequestOutput until the request is finished (vllm/v1/engine/output_processor.py:364-369). With n > 1, AsyncLLM sends n child requests, and the parent collects their final outputs. It returns one RequestOutput with all n outputs when the last child finishes (vllm/v1/engine/parallel_sampling.py:117-128). The full generator then reads one result:

        created_time = int(time.time())
        final_res: RequestOutput | None = None

        try:
            async for res in result_generator:
                final_res = res
        except asyncio.CancelledError:
            return self.create_error_response("Client disconnected")

        if final_res is None:
            return self.create_error_response(
                "No output received from the engine.",
                err_type="InternalServerError",
                status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
            )

It builds one choice per output, with a new parser per choice because parsers keep state (vllm/entrypoints/openai/chat_completion/serving.py:980-990). Logprobs are built only when logprobs is true (vllm/entrypoints/openai/chat_completion/serving.py:994-1005). The usage counts the prompt once and the tokens of all choices (vllm/entrypoints/openai/chat_completion/serving.py:1147-1163). With --enable-prompt-tokens-details, it adds the cached prompt tokens (vllm/entrypoints/openai/chat_completion/serving.py:1164-1169).

Step 8: request ids, priority and admission limits#

Step 9: the other routes#

The full route table is on The OpenAI-compatible API. Some routes have rules that the table does not show:

Deep dive#

The sections below give the full detail. Open them one at a time, or use Open all deep dives at the top of the page.

Deep dive

Worked example: one non-streaming completion with n = 2

Start vllm serve Qwen/Qwen2.5-0.5B (one GPU, default flags) and send this request (example, not in the repo):

curl http://localhost:8000/v1/completions -H "Content-Type: application/json" \
  -d '{"model": "Qwen/Qwen2.5-0.5B", "prompt": "The capital of France is",
       "n": 2, "max_tokens": 8, "temperature": 0.8}'
  1. HTTP layers. uvicorn parses the request; CORS passes (no Origin header); there is no API key. FastAPI builds a CompletionRequest and calls create_completion under with_cancellation (vllm/entrypoints/openai/completion/api_router.py:37-60).
  2. Model and engine checks. The name matches the served name. _preflight(2) checks that the engine is alive and calls check_admission(2), which does nothing because no queue limit is set (vllm/entrypoints/openai/completion/serving.py:107-113, vllm/v1/engine/async_llm.py:352-383).
  3. Render. render_cmpl_async sets arrival_time and tokenizes in the thread pool (vllm/renderers/base.py:1304-1329). The prompt has 5 tokens, [785, 6722, 315, 9625, 374], with the Qwen2.5 tokenizer (measured outside the repo; Qwen2.5 adds no BOS token).
  4. Ids and parameters. The request id is cmpl-<uuid>, and the one prompt gets the item id cmpl-<uuid>-0 (vllm/entrypoints/openai/completion/serving.py:147, vllm/entrypoints/openai/completion/serving.py:183). get_max_tokens gives min(max_model_len − 5, 8) = 8 (vllm/entrypoints/serve/utils/api_utils.py:253-270). The output kind is FINAL_ONLY (vllm/entrypoints/openai/completion/protocol.py:423-425).
  5. Generator. generate() makes a generator, and merge_async_iterators wraps it; with one prompt it takes the fast path (vllm/entrypoints/openai/completion/serving.py:210-223, vllm/utils/async_utils.py:91-103). The handler then iterates it at once, because there is no stream (vllm/entrypoints/openai/completion/serving.py:244-248).
  6. Fan-out in AsyncLLM. The internal id becomes cmpl-<uuid>-0-k3m9q2zv (8 random characters). Because n = 2, a ParentRequest makes the children 0_cmpl-…-k3m9q2zv and 1_cmpl-…-k3m9q2zv. With no seed, both share one copy of the parameters with n = 1. check_admission(2) runs again, and both children go to the engine core (vllm/v1/engine/async_llm.py:538-565, vllm/v1/engine/parallel_sampling.py:69-96).
  7. Engine core. The scheduler sees two independent requests with the same 5-token prompt. The prompt is shorter than one block of 16 tokens, and only full blocks can hit the prefix cache, so both compute the prefill (vllm/v1/core/kv_cache_manager.py:264-266). Each request runs 1 prefill step and then decode steps, until 8 tokens or an end token.
  8. Collect. Each child output is held back until it is finished (vllm/v1/engine/output_processor.py:364-369). When child 0 finishes first, the parent stores it and returns nothing. When child 1 finishes, the parent returns both outputs, and the output processor makes one RequestOutput with the external id cmpl-<uuid>-0 (vllm/v1/engine/parallel_sampling.py:117-128, vllm/v1/engine/output_processor.py:406-420).
  9. Build the body. request_output_to_completion_response makes choice 0 and choice 1 in output order. If both stop at max_tokens, the usage is prompt 5 (counted once per prompt), completion 8 + 8 = 16, total 21 (vllm/entrypoints/openai/completion/serving.py:581-661). The response id is cmpl-<uuid>, without -0 (vllm/entrypoints/openai/completion/serving.py:697-707).
  10. Answer. The route returns a JSONResponse with status 200 (vllm/entrypoints/openai/completion/api_router.py:66-70).
{"id": "cmpl-3f9c...", "object": "text_completion", "model": "Qwen/Qwen2.5-0.5B",
 "choices": [{"index": 0, "text": " Paris. It is the largest city", "finish_reason": "length", ...},
             {"index": 1, "text": " Paris, which is known for its", "finish_reason": "length", ...}],
 "usage": {"prompt_tokens": 5, "completion_tokens": 16, "total_tokens": 21, ...},
 "system_fingerprint": "vllm-<version>-<hash8>"}

The client waits for the slower child: with FINAL_ONLY, no byte goes out before both children finish. If the client disconnects while it waits, with_cancellation cancels the handler task. generate() then aborts the parent and both children, and the route returns an empty response (vllm/entrypoints/serve/utils/api_utils.py:106-109, vllm/v1/engine/output_processor.py:599-661).

Deep dive

Exceptions and HTTP status codes in the server

The error body and the four handler layers are on The OpenAI-compatible API. This table adds where each status comes from in the serving code:

StatusCauseWhere
400invalid fields, prompt too long, chat template error, bad tool choice, invalid structured outputvllm/entrypoints/serve/exception_handling/error_response.py:43-46, vllm/renderers/params.py:518-538
401wrong or missing Authorization: Bearer on a guarded pathvllm/entrypoints/serve/middleware/authenticate.py:59-61
403scale-out route without --enable-scale-outvllm/entrypoints/scale_out/factories.py:100-127
404unknown model namevllm/entrypoints/serve/engine/serving.py:75-84
500engine dead, or a request finished with reason errorvllm/entrypoints/serve/exception_handling/error_response.py:60-77
501the model does not support the route (handler is None)vllm/entrypoints/openai/chat_completion/api_router.py:63-65
503queue limits (GracefulHTTPError), retryable request error, /health after engine deathvllm/entrypoints/serve/exception_handling/error_response.py:39-42, vllm/entrypoints/serve/exception_handling/error_response.py:60-66, vllm/entrypoints/serve/instrumentator/health.py:29-33

In a stream, all of these after the first byte become one data: chunk with the same JSON body and the status code inside it (vllm/entrypoints/openai/chat_completion/serving.py:937-943).

Deep dive

Flags and environment variables of the API server

Flag or variableDefaultEffect
--host, --port, --udsall addresses, 8000, none (vllm/entrypoints/common/cli_args.py:284-305)Listen address; a Unix socket replaces host and port.
--api-server-countDP size, 1, local DP size or 0 by mode (vllm/entrypoints/cli/serve.py:113-136)Number of API server processes.
--api-key / VLLM_API_KEYnoneBearer token check on /v1, /v2, /inference, /cohere; the flag wins (vllm/entrypoints/serve/middleware/register.py:32-36).
--allowed-origins, --allowed-methods, --allowed-headers, --allow-credentials[], ["*"], ["*"], false (vllm/entrypoints/common/cli_args.py:318-325)CORS rules.
--enable-request-id-headersfalse (vllm/entrypoints/common/cli_args.py:358-359)Echo X-Request-Id in the response.
--enable-server-load-trackingfalse (vllm/entrypoints/common/cli_args.py:152-153)Count in-flight requests for /load.
--sse-keep-alive-interval0 (vllm/entrypoints/common/cli_args.py:156)Seconds between keep-alive comments; negative raises at start-up (vllm/entrypoints/common/cli_args.py:529-534).
--middlewarenone (vllm/entrypoints/common/cli_args.py:352-357)Import path of an ASGI class or an HTTP middleware function; anything else raises ValueError (vllm/entrypoints/serve/middleware/register.py:65-75).
--h11-max-incomplete-event-size, --h11-max-header-count4,194,304 bytes, 256 (vllm/entrypoints/launchers/utils/constants.py:6-7)Header abuse limits of the h11 parser.
--max-num-queued-reqs, --max-num-queued-tokensnone (vllm/config/scheduler.py:92, vllm/config/scheduler.py:105)Admission limits; HTTP 503 when full.
--enable-force-include-usagefalse (vllm/entrypoints/common/cli_args.py:154)Usage in every stream, also per chunk (vllm/entrypoints/serve/utils/api_utils.py:276-277).
VLLM_HTTP_TIMEOUT_KEEP_ALIVE5 s (vllm/envs.py:1134-1136)uvicorn's idle time for a kept-alive connection (vllm/entrypoints/launchers/api_server/entry.py:152).
VLLM_SERVER_DEV_MODE0 (vllm/envs.py:1466)Adds the development routes.
VLLM_SKIP_MODEL_NAME_VALIDATIONfalse (vllm/envs.py:2200-2203)Accept any model value (vllm/entrypoints/serve/engine/serving.py:86-91).
VLLM_KEEP_ALIVE_ON_ENGINE_DEATH0 (vllm/envs.py:1107-1109)Keep uvicorn up after the engine dies (vllm/entrypoints/launchers/launcher.py:325-337).

The uvicorn settings that build_and_serve passes contain one time limit only: timeout_keep_alive (vllm/entrypoints/launchers/api_server/entry.py:141-161). It closes an idle connection between two requests; it does not limit a running request. A long request is limited by max_tokens and by the client or proxy timeout.

Deep dive

Several API servers: what changes for one request

With N API servers, all of the above runs in each ApiServer_i process. Each process has its own AsyncLLM, renderer and serving objects, and its own client index (vllm/v1/utils.py:230-252). Four points change:

  1. A connection lands on the process with the fewest open connections. A worker that is not the least loaded waits up to 5 × 1 ms before it accepts (vllm/entrypoints/launchers/launcher.py:39-44, vllm/entrypoints/launchers/launcher.py:103-125).
  2. Prometheus uses a multiprocess directory, so /metrics of any process shows all of them (vllm/entrypoints/cli/serve.py:282-283).
  3. The admission counters are shared through a RawArray, but only when --max-num-queued-reqs is set (vllm/v1/utils.py:207-214).
  4. Runtime LoRA loading is refused at start-up, because each process would load its own copy (vllm/entrypoints/cli/serve.py:315-318).

The engine core sends each output only to the API server that sent the request (Data parallel serving: several engine cores behind one endpoint).

What can go wrong#

  • 404 "The model ... does not exist". The model field must be a served name (--served-model-name, else the model path) or a LoRA name (vllm/entrypoints/serve/engine/serving.py:75-84).
  • 401 on /v1 only. This is the expected behaviour of --api-key; the paths outside the guarded prefixes are open (warning above). OPTIONS requests are never checked (vllm/entrypoints/serve/middleware/authenticate.py:48-54).
  • A browser client fails with a CORS error. No origin is allowed by default; set --allowed-origins (vllm/entrypoints/common/cli_args.py:320-321).
  • A proxy closes slow streams. Nothing is sent during a long queue or prefill. Set --sse-keep-alive-interval.
  • 503 under load with queue limits set. This is the intended behaviour of --max-num-queued-reqs; the token limit counts whole prompts and can reject early (vllm/v1/engine/async_llm.py:337-341).
  • The event loop is the bottleneck. Rendering, detokenization and JSON for all requests share one event loop per API server. More API servers split this work (Step 1).
  • /load always shows 0. Turn on --enable-server-load-tracking.

Why it matters. Every request of vllm serve crosses these layers twice. Most serving problems (wrong status codes, early 503s, lost streams behind proxies, an overloaded front end) are visible here before they reach the engine.

Check yourself#

  1. 1
    Which object does the route function of /v1/chat/completions call, and where does it come from?
    easy
    Show answer

    app.state.openai_serving_chat, an OpenAIServingChat object (vllm/entrypoints/openai/chat_completion/api_router.py:35-36). init_generate_state makes it at start-up, only when the model supports the generate task (vllm/entrypoints/generate/api_router.py:149-151).

  2. 2
    A client sends "model": "gpt-4" to a server that serves Qwen. Which status does it get, and from which class?
    easy
    Show answer

    HTTP 404 with NotFoundError, from BaseServing._check_model (vllm/entrypoints/serve/engine/serving.py:75-84), unless VLLM_SKIP_MODEL_NAME_VALIDATION is set or a LoRA resolver loads an adapter with that name.

  3. 3
    A client sends the header X-Request-Id: abc to a server without --enable-request-id-headers. Which id does the chat response carry, and does the response have an X-Request-Id header?
    medium
    Show answer

    The response id is chatcmpl-abc, because _base_request_id reads the header in every case (vllm/entrypoints/serve/engine/serving.py:132-142, vllm/entrypoints/openai/chat_completion/serving.py:320-322). The response has no X-Request-Id header, because only XRequestIdMiddleware adds it, and only with the flag (vllm/entrypoints/serve/middleware/register.py:38-41).

  4. 4
    A non-streaming completion asks for n = 2. The first child finishes after 3 tokens with EOS; the second after 8 tokens. When does the client get bytes, and what is completion_tokens?
    hard
    Show answer

    Only after the second child finishes. With FINAL_ONLY each child gives no output before it finishes, and the parent returns nothing until all children are done (vllm/v1/engine/output_processor.py:364-369, vllm/v1/engine/parallel_sampling.py:117-128). completion_tokens is the sum of the token ids of both outputs: 3 + 8 = 11 (the EOS token counts as an output token) (vllm/entrypoints/openai/completion/serving.py:653).

  5. 5
    You start vllm serve M -dp 2 on one node. How many API server processes run, where does vllm serve itself go, and how does a new connection pick a process?
    hard
    Show answer

    Two API servers, because the count defaults to the DP size with internal load balancing (vllm/entrypoints/cli/serve.py:124-130). The CLI process runs run_multi_api_server and only supervises (vllm/entrypoints/cli/serve.py:151-152, vllm/entrypoints/cli/serve.py:384-390). Both processes share one socket, and each publishes its number of open connections in a shared array. A process accepts when its count is the smallest, or after 5 checks of 1 ms (vllm/entrypoints/launchers/launcher.py:103-125).