<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: yuan lei</title>
    <description>The latest articles on DEV Community by yuan lei (@yuan_lei_e631e36865ed370b).</description>
    <link>https://dev.to/yuan_lei_e631e36865ed370b</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4113920%2F3c261565-e341-4f17-85ce-c00a473f11ab.png</url>
      <title>DEV Community: yuan lei</title>
      <link>https://dev.to/yuan_lei_e631e36865ed370b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yuan_lei_e631e36865ed370b"/>
    <language>en</language>
    <item>
      <title>Inside vLLM: Following One Request from the API to GPU Execution</title>
      <dc:creator>yuan lei</dc:creator>
      <pubDate>Mon, 07 Sep 2026 12:52:14 +0000</pubDate>
      <link>https://dev.to/yuan_lei_e631e36865ed370b/inside-vllm-following-one-request-from-the-api-to-gpu-execution-1ja6</link>
      <guid>https://dev.to/yuan_lei_e631e36865ed370b/inside-vllm-following-one-request-from-the-api-to-gpu-execution-1ja6</guid>
      <description>&lt;p&gt;Article 1 of 3 · vLLM Internals&lt;/p&gt;

&lt;p&gt;This English edition is adapted from the &lt;a href="https://zhuanlan.zhihu.com/p/2060033883849204419" rel="noopener noreferrer"&gt;published Chinese article on Zhihu&lt;/a&gt;. It preserves the source-code references, experimental boundaries, and reproducible artifacts while adapting the structure for an international engineering audience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Series:&lt;/strong&gt; Part 1 · Request lifecycle · &lt;a href="https://jacklei0901.github.io/articles/inside-vllm-cuda-kernels/" rel="noopener noreferrer"&gt;Part 2 · CUDA kernels and paged attention&lt;/a&gt; · &lt;a href="https://jacklei0901.github.io/articles/building-flashattention-pytorch-triton/" rel="noopener noreferrer"&gt;Part 3 · FlashAttention from PyTorch to Triton&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This article follows one offline inference request through vLLM V1: from&lt;br&gt;
&lt;code&gt;LLM.generate()&lt;/code&gt; and inter-process communication to scheduling, input&lt;br&gt;
flattening, GPU model execution, paged KV-cache access, sampling, and resource&lt;br&gt;
reclamation. The goal is to answer one concrete question: what happens behind&lt;br&gt;
the call to &lt;code&gt;llm.generate()&lt;/code&gt; before the completed result reaches the caller?&lt;/p&gt;

&lt;p&gt;The discussion assumes familiarity with Transformer inference, including&lt;br&gt;
prefill, decode, KV caching, and autoregressive generation. It focuses on how&lt;br&gt;
those concepts appear in vLLM source code rather than reteaching the model&lt;br&gt;
architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Version scope.&lt;/strong&gt; The source references were verified against vLLM 0.22.0;&lt;br&gt;
this edition was checked on September 3, 2026. vLLM evolves quickly, so some&lt;br&gt;
filenames and call boundaries will move. The&lt;br&gt;
long-lived ideas—continuous batching, token budgets, paged KV allocation, and&lt;br&gt;
the separation between scheduling and execution—are the real subject of the&lt;br&gt;
article.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Read the Source Instead of Another API Guide?
&lt;/h2&gt;

&lt;p&gt;Many introductions stop at the useful analogy that PagedAttention manages the&lt;br&gt;
KV cache much like virtual memory manages pages. The analogy does not tell us&lt;br&gt;
how a request is admitted, how variable-length requests become a flat token&lt;br&gt;
batch, or what the page table looks like at the kernel boundary.&lt;/p&gt;

&lt;p&gt;The answers are in the source. This article follows vLLM 0.22's V1 execution&lt;br&gt;
path from the public entry point to the CUDA boundary. It is a source-code&lt;br&gt;
walkthrough, not an API tutorial.&lt;/p&gt;
&lt;h2&gt;
  
  
  Start with a System Map
&lt;/h2&gt;

&lt;p&gt;Start with the process boundary and the engine loop. In vLLM V1, EngineCore&lt;br&gt;
runs continuously in a child process. Requests enter and leave the active set&lt;br&gt;
between steps; the caller does not drive GPU execution one token at a time.&lt;/p&gt;

&lt;p&gt;One detail shapes the entire path: in this configuration, vLLM 0.22 separates&lt;br&gt;
the caller-facing engine from EngineCore with a process boundary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REQUEST SUBMISSION · main process -&amp;gt; EngineCore child

Caller
  └─ LLM.generate(prompts)
       └─ LLMEngine.add_request()
            └─ EngineCoreClient
                 └─ IPC input queue ──────────────▶ scheduler.add_request()

CONTINUOUS ENGINE LOOP · EngineCore child

run_busy_loop()
  ├─ receive newly submitted requests
  └─ repeat EngineCore.step()
       1. scheduler.schedule()
          └─ spend token / KV-cache budgets -&amp;gt; SchedulerOutput
       2. model_executor.execute_model()
          └─ GPU forward pass and sampling -&amp;gt; sampled tokens
       3. scheduler.update_from_output()
          ├─ update request state and release finished KV blocks
          └─ EngineCoreOutputs ───────────────▶ IPC output queue

RESULT CONSUMPTION · EngineCore child -&amp;gt; main process

IPC output queue
  └─ LLMEngine.step() / get_output()
       └─ detokenize and assemble RequestOutput
            └─ return completed results to LLM.generate() and the caller
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This map establishes three boundaries used throughout the article.&lt;/p&gt;

&lt;p&gt;First, the main process consumes results; EngineCore produces them. Calling&lt;br&gt;
&lt;code&gt;LLMEngine.step()&lt;/code&gt; does not tell the GPU to perform one step. It retrieves&lt;br&gt;
already-produced output through &lt;code&gt;get_output()&lt;/code&gt;, then detokenizes and assembles&lt;br&gt;
the user-facing result. Scheduling and model execution advance independently&lt;br&gt;
inside EngineCore's busy loop.&lt;/p&gt;

&lt;p&gt;Second, &lt;code&gt;EngineCore.step()&lt;/code&gt; has three core stages: scheduling, execution, and&lt;br&gt;
state update. The rest of the article expands those stages one at a time.&lt;/p&gt;

&lt;p&gt;Third, this loop implements continuous batching. The request set changes from&lt;br&gt;
one step to the next: stage 3 removes completed requests and releases their&lt;br&gt;
resources, while the next call to &lt;code&gt;schedule()&lt;/code&gt; can admit waiting work. The GPU&lt;br&gt;
therefore does not have to wait for one fixed batch to finish before admitting&lt;br&gt;
another request.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Path We Will Follow
&lt;/h2&gt;

&lt;p&gt;The rest of the article expands that loop into six parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Entry:&lt;/strong&gt; how &lt;code&gt;LLM&lt;/code&gt; and &lt;code&gt;LLMEngine&lt;/code&gt; submit work to EngineCore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduling:&lt;/strong&gt; how &lt;code&gt;schedule()&lt;/code&gt; spends token and KV-cache budgets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input preparation:&lt;/strong&gt; how variable-length requests become flat tensors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model execution:&lt;/strong&gt; how metadata reaches attention inside the forward pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PagedAttention:&lt;/strong&gt; how the write and read paths address KV-cache blocks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output:&lt;/strong&gt; how logits become tokens and completed requests release resources.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each part keeps only the code needed to connect one boundary to the next.&lt;/p&gt;
&lt;h2&gt;
  
  
  Part 1: The Entry Point Behind &lt;code&gt;generate()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Start with the public interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;vllm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LLM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SamplingParams&lt;/span&gt;

&lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LLM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;facebook/opt-125m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Once upon a time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nc"&gt;SamplingParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those two lines initialize the entire engine stack. Start with the &lt;code&gt;LLM&lt;/code&gt; class.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;LLM&lt;/code&gt; Is a Thin Wrapper
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;entrypoints/llm.py&lt;/code&gt;, &lt;code&gt;LLM&lt;/code&gt; delegates engine construction to one factory&lt;br&gt;
call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# entrypoints/llm.py
&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm_engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LLMEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_engine_args&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;engine_args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;engine_args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;usage_context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;UsageContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LLM_CLASS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;LLM&lt;/code&gt; packages model, dtype, and other configuration into &lt;code&gt;EngineArgs&lt;/code&gt;, then&lt;br&gt;
uses the factory method to create &lt;code&gt;LLMEngine&lt;/code&gt;. It is the public facade;&lt;br&gt;
&lt;code&gt;LLMEngine&lt;/code&gt; owns the request-processing path followed below.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;generate()&lt;/code&gt; returns a &lt;code&gt;list[RequestOutput]&lt;/code&gt;, one complete result per request.&lt;br&gt;
The generated text is available as &lt;code&gt;output.outputs[0].text&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The distinction is useful throughout the article: &lt;code&gt;LLM&lt;/code&gt; is the facade, while&lt;br&gt;
&lt;code&gt;LLMEngine&lt;/code&gt; connects that facade to EngineCore.&lt;/p&gt;
&lt;h3&gt;
  
  
  At Its Core, &lt;code&gt;generate()&lt;/code&gt; Is a Loop
&lt;/h3&gt;

&lt;p&gt;The internal logic of &lt;code&gt;generate&lt;/code&gt;, after stripping away the details, is as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Conceptual skeleton
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;prompts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;   &lt;span class="c1"&gt;# 1. Register the request with the engine
&lt;/span&gt;
&lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has_unfinished_requests&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;  &lt;span class="c1"&gt;# 2. While any request is unfinished
&lt;/span&gt;    &lt;span class="n"&gt;step_outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;         &lt;span class="c1"&gt;#    advance the engine by one step
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;step_outputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;finished&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;outputs&lt;/span&gt;                                     &lt;span class="c1"&gt;# 3. Return after collecting every result
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;code&gt;while ... step()&lt;/code&gt; loop continues until every submitted request is&lt;br&gt;
finished. Unlike a hand-written &lt;code&gt;for _ in range(max_tokens)&lt;/code&gt; loop over one&lt;br&gt;
sequence, it tracks a set of requests whose lengths and completion times differ.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;step()&lt;/code&gt; Does Not Submit the Request
&lt;/h3&gt;

&lt;p&gt;The name &lt;code&gt;step()&lt;/code&gt; can suggest a synchronous "submit, execute, return" call.&lt;br&gt;
The implementation does something different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# v1/engine/llm_engine.py (simplified)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# 1) Fetch outputs already produced by EngineCore
&lt;/span&gt;    &lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;engine_core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_output&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# 2) Decode tokens and evaluate stopping conditions
&lt;/span&gt;    &lt;span class="n"&gt;processed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process_outputs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;

    &lt;span class="c1"&gt;# 3) Handle requests terminated by a stop string
&lt;/span&gt;    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;engine_core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort_requests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reqs_to_abort&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 4) Record statistics
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;processed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_outputs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;step()&lt;/code&gt; never submits a request or directly drives model execution. It calls&lt;br&gt;
&lt;code&gt;get_output()&lt;/code&gt; to consume results. Submission already happened in&lt;br&gt;
&lt;code&gt;add_request()&lt;/code&gt;; the actual computation runs independently elsewhere.&lt;/p&gt;

&lt;p&gt;This description assumes the default multiprocess V1 configuration. If&lt;br&gt;
&lt;code&gt;VLLM_ENABLE_V1_MULTIPROCESSING=0&lt;/code&gt; is set for debugging, the in-process client&lt;br&gt;
drives EngineCore from &lt;code&gt;get_output()&lt;/code&gt; and the child-process producer/consumer&lt;br&gt;
boundary collapses. The scheduling, execution, and update stages discussed&lt;br&gt;
below still apply, but they no longer run in a separate EngineCore process.&lt;/p&gt;

&lt;p&gt;This producer-consumer split is asynchronous across the process boundary. In&lt;br&gt;
configurations that enable asynchronous scheduling, the startup log also states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO ... Asynchronous scheduling is enabled.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That "elsewhere" is a separate child process. A running server prints a prefix&lt;br&gt;
such as &lt;code&gt;(EngineCore pid=8884)&lt;/code&gt; in its logs: this is the &lt;code&gt;EngineCore&lt;/code&gt; process&lt;br&gt;
that performs scheduling and model execution.&lt;/p&gt;

&lt;p&gt;The main process (&lt;code&gt;LLMEngine&lt;/code&gt;) is the consumer: &lt;code&gt;get_output()&lt;/code&gt; receives results,&lt;br&gt;
decodes them, and returns them to the caller. The child process (&lt;code&gt;EngineCore&lt;/code&gt;)&lt;br&gt;
is the producer: its busy loop schedules requests, runs the model, produces&lt;br&gt;
tokens, and writes results to an IPC queue.&lt;/p&gt;

&lt;p&gt;This lets host-side output processing overlap EngineCore execution. In an online&lt;br&gt;
server, request tokenization and admission can also proceed independently of an&lt;br&gt;
already-running GPU step. The IPC queues separate those execution timelines.&lt;/p&gt;
&lt;h3&gt;
  
  
  What This Part Established
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;LLM&lt;/code&gt; is the facade; &lt;code&gt;LLMEngine&lt;/code&gt; is the engine entry point.&lt;/li&gt;
&lt;li&gt;At the heart of &lt;code&gt;generate&lt;/code&gt; is the &lt;code&gt;while has_unfinished_requests(): step()&lt;/code&gt; loop.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LLMEngine.step()&lt;/code&gt; consumes and decodes output; the separate &lt;code&gt;EngineCore&lt;/code&gt;
process performs scheduling and execution asynchronously.&lt;/li&gt;
&lt;li&gt;IPC decouples production in the child process from consumption in the main
process, allowing CPU work to overlap GPU execution.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next we cross the IPC boundary and enter &lt;code&gt;EngineCore&lt;/code&gt;, whose busy loop has three&lt;br&gt;
core stages: schedule, execute, and update.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb47r5ozypez00uwno2lh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb47r5ozypez00uwno2lh.png" alt="fig2-class-structure" width="800" height="833"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Figure: ownership among vLLM's core classes. &lt;code&gt;LLM&lt;/code&gt; owns &lt;code&gt;LLMEngine&lt;/code&gt;, which&lt;br&gt;
communicates with &lt;code&gt;EngineCore&lt;/code&gt;; &lt;code&gt;EngineCore&lt;/code&gt; in turn owns the scheduler,&lt;br&gt;
model executor, and KV-cache manager. The horizontal line is the process&lt;br&gt;
boundary: main process/consumer above, child process/producer below.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Part 2: Scheduling and Continuous Batching
&lt;/h2&gt;

&lt;p&gt;The previous section stopped at the child-process boundary. Each iteration of&lt;br&gt;
the &lt;code&gt;EngineCore&lt;/code&gt; busy loop runs the three stages of &lt;code&gt;step()&lt;/code&gt;; this section&lt;br&gt;
starts with &lt;code&gt;scheduler.schedule()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Continuous batching is not a standalone module. It emerges from the decisions&lt;br&gt;
made on every call to &lt;code&gt;schedule()&lt;/code&gt;. One useful mental model is an operating-system&lt;br&gt;
scheduler whose scarce resources are token-compute budget and GPU KV-cache&lt;br&gt;
pages.&lt;/p&gt;

&lt;p&gt;The scheduler lives in &lt;code&gt;v1/core/sched/scheduler.py&lt;/code&gt;; its central method is&lt;br&gt;
&lt;code&gt;schedule()&lt;/code&gt; (&lt;code&gt;scheduler.py:329&lt;/code&gt;). The file is large, but this path only needs&lt;br&gt;
three pieces: the main loop, preemption, and the final packaged output.&lt;/p&gt;
&lt;h3&gt;
  
  
  A Mental Model: Two Budgets and Two Queues
&lt;/h3&gt;

&lt;p&gt;The V1 scheduler can be summarized in one sentence: during each engine step,&lt;br&gt;
advance as many requests as possible without exceeding the token budget or the&lt;br&gt;
available KV blocks.&lt;/p&gt;

&lt;p&gt;These concepts map naturally to familiar operating-system terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;token_budget&lt;/code&gt;: the maximum number of tokens processed in one step
(&lt;code&gt;max_num_batched_tokens=8192&lt;/code&gt; in the example startup log), analogous to a
bounded compute time slice.&lt;/li&gt;
&lt;li&gt;KV-cache blocks: the free cache capacity in GPU memory, analogous to physical
page frames.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;waiting&lt;/code&gt;: requests waiting to begin or continue prefill, analogous to a ready
queue.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;running&lt;/code&gt;: admitted requests eligible to advance in the current step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The comment at the start of &lt;code&gt;schedule()&lt;/code&gt; is the best guide to its model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# v1/core/sched/scheduler.py:330
# There's no "decoding phase" nor "prefill phase" in the scheduler.
# Each request just has the num_computed_tokens and num_tokens_with_spec.
# At each step, the scheduler tries to assign tokens to the requests
# so that each request's num_computed_tokens can catch up its
# num_tokens_with_spec.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The V1 scheduler does not maintain separate prefill and decode phases. Instead,&lt;br&gt;
it compares each request's &lt;code&gt;num_computed_tokens&lt;/code&gt; with&lt;br&gt;
&lt;code&gt;num_tokens_with_spec&lt;/code&gt; and decides how many tokens to advance in the current&lt;br&gt;
step. A decode request commonly advances by one token; a prefill request may&lt;br&gt;
advance by a larger chunk. The same progress model also accommodates chunked&lt;br&gt;
prefill and prefix-cache hits.&lt;/p&gt;

&lt;p&gt;The following sketch is a useful map of &lt;code&gt;schedule()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;One schedule() step:
  pass 1  RUNNING first (decode) ──── no free block → preempt from the tail
  pass 2  fill from WAITING (prefill) ── over budget → clamp or defer
  pack    SchedulerOutput (the execution plan)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmy9jfuu0wjh21sj7jg62.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmy9jfuu0wjh21sj7jg62.png" alt="fig3-schedule-flow" width="800" height="740"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Figure: one &lt;code&gt;schedule()&lt;/code&gt; step. The scheduler considers running requests,&lt;br&gt;
admits waiting requests with any remaining budget, handles allocation failure&lt;br&gt;
through preemption, and packages the result as &lt;code&gt;SchedulerOutput&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Pass 1: Why Running Requests Come First
&lt;/h3&gt;

&lt;p&gt;The first substantive loop in &lt;code&gt;schedule()&lt;/code&gt; traverses &lt;code&gt;running&lt;/code&gt;&lt;br&gt;
(&lt;code&gt;scheduler.py:364&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# v1/core/sched/scheduler.py:364
# First, schedule the RUNNING requests.
&lt;/span&gt;&lt;span class="n"&gt;req_index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;req_index&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;running&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;token_budget&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;running&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;req_index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;num_new_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;num_tokens_with_spec&lt;/span&gt;
                      &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;num_output_placeholders&lt;/span&gt;
                      &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;num_computed_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;num_new_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_new_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token_budget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source comments make the order explicit: running requests are considered&lt;br&gt;
before waiting requests. This ordering tends to protect inter-token latency for&lt;br&gt;
requests that are already generating, although the observed latency trade-off&lt;br&gt;
still depends on the configured scheduling policy and workload. It also lets&lt;br&gt;
requests that already hold KV blocks continue making progress toward completion&lt;br&gt;
and eventual reclamation. These are useful consequences of the order, not a&lt;br&gt;
claim that every running request always outranks every waiting request under all&lt;br&gt;
policies.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;waiting&lt;/code&gt; queue is an admission queue, not a tokenization cache. Requests in&lt;br&gt;
it are waiting for scheduler resources; prompt preprocessing has already&lt;br&gt;
happened before this point.&lt;/p&gt;
&lt;h3&gt;
  
  
  Pass 2: Chunked Prefill Is a Budget Clamp
&lt;/h3&gt;

&lt;p&gt;After considering running requests, the scheduler uses any remaining budget to&lt;br&gt;
admit work from &lt;code&gt;waiting&lt;/code&gt; (&lt;code&gt;scheduler.py:544&lt;/code&gt;). This is where the chunked-prefill&lt;br&gt;
budget clamp appears.&lt;/p&gt;

&lt;p&gt;A waiting request may have 5,000 prompt tokens while only 2,000 tokens remain&lt;br&gt;
in the current step's budget. The relevant control flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Budget clamp inside the waiting loop
&lt;/span&gt;&lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheduler_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable_chunked_prefill&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;num_new_tokens&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;token_budget&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# If chunked_prefill is disabled, we can stop the scheduling here.
&lt;/span&gt;    &lt;span class="k"&gt;break&lt;/span&gt;

&lt;span class="n"&gt;num_new_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_new_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token_budget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;num_new_tokens&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter here.&lt;/p&gt;

&lt;p&gt;First, &lt;code&gt;break&lt;/code&gt; does not drop the request. It exits the waiting-loop because the&lt;br&gt;
remaining budget cannot admit another request in this step. The request stays&lt;br&gt;
in &lt;code&gt;waiting&lt;/code&gt; and is reconsidered the next time &lt;code&gt;schedule()&lt;/code&gt; runs.&lt;/p&gt;

&lt;p&gt;With chunked prefill enabled, &lt;code&gt;min()&lt;/code&gt; admits the portion that fits and leaves&lt;br&gt;
the rest for a later step. With it disabled, an over-budget request stays in&lt;br&gt;
&lt;code&gt;waiting&lt;/code&gt; and is retried as a whole. Neither path loses the request; removal&lt;br&gt;
happens only after normal completion or an explicit abort.&lt;/p&gt;

&lt;p&gt;A useful rule when reading schedulers is to ask where an item goes after a&lt;br&gt;
&lt;code&gt;break&lt;/code&gt; or &lt;code&gt;continue&lt;/code&gt;. Here it remains in the same queue for a later step, just&lt;br&gt;
as an OS process that misses one time slice remains runnable.&lt;/p&gt;

&lt;p&gt;Second, chunked prefill reduces to a budget clamp:&lt;br&gt;
&lt;code&gt;num_new_tokens = min(num_new_tokens, token_budget)&lt;/code&gt;. A long prompt contributes&lt;br&gt;
only the tokens that fit in the current step; the remainder is scheduled later.&lt;br&gt;
The full policy considers more state, but this clamp is what divides a long&lt;br&gt;
prefill across scheduler steps.&lt;/p&gt;

&lt;p&gt;(The running-request loop has the same clamp at &lt;code&gt;scheduler.py:392&lt;/code&gt;. Because V1&lt;br&gt;
does not maintain separate prefill and decode phases, the budget logic applies&lt;br&gt;
uniformly to both.)&lt;/p&gt;
&lt;h3&gt;
  
  
  Preemption: Discard the KV State and Recompute
&lt;/h3&gt;

&lt;p&gt;Another branch appears when a running request needs more KV blocks and&lt;br&gt;
&lt;code&gt;allocate_slots&lt;/code&gt; cannot satisfy the request. The allocation-retry loop beginning&lt;br&gt;
at &lt;code&gt;scheduler.py:443&lt;/code&gt; can then preempt a victim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# v1/core/sched/scheduler.py:443 (simplified)
&lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;new_blocks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kv_cache_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allocate_slots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_new_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;new_blocks&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;                              &lt;span class="c1"&gt;# Allocation succeeded; schedule normally
&lt;/span&gt;    &lt;span class="c1"&gt;# Allocation failed; choose a victim to preempt
&lt;/span&gt;    &lt;span class="n"&gt;preempted_req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;running&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;      &lt;span class="c1"&gt;# Default policy: pop from the tail (LIFO)
&lt;/span&gt;    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_preempt_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;preempted_req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scheduled_timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;preempted_req&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;                              &lt;span class="c1"&gt;# No victim remains; defer this request
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the default policy, &lt;code&gt;self.running.pop()&lt;/code&gt; selects a victim from the tail&lt;br&gt;
(LIFO). This usually victimizes a more recently admitted request with less&lt;br&gt;
accumulated progress, reducing the work discarded, although queue position is&lt;br&gt;
not a universal measure of recomputation cost. With &lt;code&gt;PRIORITY&lt;/code&gt;&lt;br&gt;
scheduling, victim selection also considers priority (&lt;code&gt;scheduler.py:456&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;_preempt_request&lt;/code&gt; (&lt;code&gt;scheduler.py:929&lt;/code&gt;) defines what happens to the preempted&lt;br&gt;
request's KV-cache state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# v1/core/sched/scheduler.py:929
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_preempt_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kv_cache_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# ← release its KV blocks immediately
&lt;/span&gt;    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encoder_cache_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RequestStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PREEMPTED&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;num_computed_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;              &lt;span class="c1"&gt;# ← reset computed progress
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;waiting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepend_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;# ← prepend it to the waiting queue
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter here. First, &lt;code&gt;num_computed_tokens = 0&lt;/code&gt; records recomputation&lt;br&gt;
semantics: the scheduler no longer treats the request's previous progress as&lt;br&gt;
resident KV state. When rescheduled, it must rebuild the required state, subject&lt;br&gt;
to any cache reuse the current configuration can legitimately recover. This is&lt;br&gt;
different from swapping the victim's KV tensors to host memory and restoring&lt;br&gt;
them later.&lt;/p&gt;

&lt;p&gt;The trade-off is workload-dependent. Swapping consumes host-device bandwidth&lt;br&gt;
and host memory; recomputation consumes additional GPU work. vLLM V1 chooses&lt;br&gt;
the latter for this path, avoiding a KV transfer over PCIe at the cost of&lt;br&gt;
re-running model computation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi0i90htrtk6cg6m24973.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi0i90htrtk6cg6m24973.png" alt="fig4-preemption" width="800" height="716"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Figure: two preemption strategies. Swapping preserves KV state through a&lt;br&gt;
host-device transfer; recomputation releases the blocks and later rebuilds&lt;br&gt;
the required state.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second detail is &lt;code&gt;prepend_request&lt;/code&gt;, an anti-starvation measure. A preempted&lt;br&gt;
request returns to the head of &lt;code&gt;waiting&lt;/code&gt; rather than the tail. Appending it&lt;br&gt;
behind every newly arrived request could cause repeated preemption and&lt;br&gt;
indefinite delay; prepending gives the sacrificed request priority on the next&lt;br&gt;
admission pass.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Scheduler Produces &lt;code&gt;SchedulerOutput&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;schedule()&lt;/code&gt; packages its decisions into &lt;code&gt;SchedulerOutput&lt;/code&gt; (defined in&lt;br&gt;
&lt;code&gt;v1/core/sched/output.py:181&lt;/code&gt;) and hands that execution plan to the model&lt;br&gt;
runner. The execution layer consumes the decision; it does not reschedule the&lt;br&gt;
requests.&lt;/p&gt;

&lt;p&gt;Many fields support optional features such as speculative decoding,&lt;br&gt;
multimodality, structured output, and connectors. Four core fields are enough&lt;br&gt;
to enter the execution layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# v1/core/sched/output.py:181 (selected core fields)
&lt;/span&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SchedulerOutput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;scheduled_new_reqs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;NewRequestData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="c1"&gt;# First scheduling: send complete request data
&lt;/span&gt;    &lt;span class="n"&gt;scheduled_cached_reqs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CachedRequestData&lt;/span&gt;      &lt;span class="c1"&gt;# Previously scheduled: send only the delta
&lt;/span&gt;    &lt;span class="n"&gt;num_scheduled_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;          &lt;span class="c1"&gt;# req_id -&amp;gt; tokens processed in this step
&lt;/span&gt;    &lt;span class="n"&gt;total_num_scheduled_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;finished_req_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                     &lt;span class="c1"&gt;# Completed requests; tell the worker to reclaim state
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;new_block_ids_to_zero&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="c1"&gt;# Newly allocated blocks that the worker must zero
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Their roles become clearer at the execution boundary.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;scheduled_new_reqs&lt;/code&gt; carries the state needed when a request is first scheduled;&lt;br&gt;
&lt;code&gt;scheduled_cached_reqs&lt;/code&gt; carries updates for requests already known to the model&lt;br&gt;
runner. This avoids rebuilding and transmitting the complete request state on&lt;br&gt;
every step—a state-cache plus incremental-update pattern.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;num_scheduled_tokens&lt;/code&gt; implements the unified progress model: it records how&lt;br&gt;
many tokens each request will process in this step. The execution layer can use&lt;br&gt;
those counts to pack prefill and decode work into one flat token batch.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;finished_req_ids&lt;/code&gt; tells the worker which completed requests can release their&lt;br&gt;
cached state, analogous to reclaiming OS resources after a process exits.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;new_block_ids_to_zero&lt;/code&gt; identifies freshly allocated cache blocks that must be&lt;br&gt;
cleared before the forward pass when KV-cache zeroing is enabled. Zeroing keeps&lt;br&gt;
stale data or NaNs from an earlier owner from corrupting attention or&lt;br&gt;
state-space-model computation. The field is conditional; not every&lt;br&gt;
configuration needs this step.&lt;/p&gt;
&lt;h3&gt;
  
  
  Mapping the Scheduler to OS Concepts
&lt;/h3&gt;

&lt;p&gt;Several scheduler concepts have useful operating-system analogies:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operating system concepts&lt;/th&gt;
&lt;th&gt;vLLM Scheduler correspondence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ready Queue&lt;/td&gt;
&lt;td&gt;&lt;code&gt;waiting&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run Queue&lt;/td&gt;
&lt;td&gt;&lt;code&gt;running&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Physical page frame allocator&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kv_cache_manager&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Page fault/out of memory&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;allocate_slots&lt;/code&gt; returns None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Process swap&lt;/td&gt;
&lt;td&gt;Preemption by releasing KV state and recomputing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;time slice&lt;/td&gt;
&lt;td&gt;token budget&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Priority compensation after preemption&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;prepend_request&lt;/code&gt; returns the victim to the head of &lt;code&gt;waiting&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output of scheduling decision&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SchedulerOutput&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Continuous batching follows from rebuilding the execution batch at every&lt;br&gt;
&lt;code&gt;schedule()&lt;/code&gt; call. Completed requests leave, waiting requests enter, long&lt;br&gt;
prompts may be split into chunks, and allocation pressure may trigger&lt;br&gt;
preemption. The engine can therefore reuse newly available capacity without&lt;br&gt;
waiting for an earlier fixed batch to finish in full.&lt;/p&gt;

&lt;p&gt;Next, follow this &lt;code&gt;SchedulerOutput&lt;/code&gt; plan into the execution layer and see how&lt;br&gt;
variable-length requests become flat tensors for the model.&lt;/p&gt;
&lt;h2&gt;
  
  
  Part 3: Flattening Variable-Length Requests
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;SchedulerOutput&lt;/code&gt; from the previous part reaches &lt;code&gt;GPUModelRunner&lt;/code&gt; in the&lt;br&gt;
worker. Suppose it assigns four tokens to request A and one token each to&lt;br&gt;
requests B and C. How does the runner turn those unequal slices into tensors for&lt;br&gt;
the GPU?&lt;/p&gt;

&lt;p&gt;A conventional implementation might pad every request to the same length and&lt;br&gt;
stack a &lt;code&gt;[batch, seq_len]&lt;/code&gt; tensor. This path instead packs the scheduled tokens.&lt;/p&gt;

&lt;p&gt;The path enters &lt;code&gt;GPUModelRunner.execute_model&lt;/code&gt;&lt;br&gt;
(&lt;code&gt;v1/worker/gpu_model_runner.py:3913&lt;/code&gt;) and calls &lt;code&gt;_prepare_inputs&lt;/code&gt;&lt;br&gt;
(&lt;code&gt;gpu_model_runner.py:1839&lt;/code&gt;). &lt;code&gt;InputBatch&lt;/code&gt;&lt;br&gt;
(&lt;code&gt;v1/worker/gpu/input_batch.py:36&lt;/code&gt;) holds the prepared state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# v1/worker/gpu/input_batch.py:36 (excerpt)
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InputBatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;req_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;num_reqs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;

    &lt;span class="c1"&gt;# [num_reqs] tokens processed for each request in this step
&lt;/span&gt;    &lt;span class="n"&gt;num_scheduled_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;
    &lt;span class="c1"&gt;# sum(num_scheduled_tokens): total tokens in this step
&lt;/span&gt;    &lt;span class="n"&gt;num_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;

    &lt;span class="c1"&gt;# [num_reqs + 1] request boundaries in the flattened sequence (prefix sum)
&lt;/span&gt;    &lt;span class="n"&gt;query_start_loc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tensor&lt;/span&gt;
    &lt;span class="c1"&gt;# [num_reqs] current total length of each request, including history
&lt;/span&gt;    &lt;span class="n"&gt;seq_lens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tensor&lt;/span&gt;

    &lt;span class="c1"&gt;# [num_tokens] ← one-dimensional, not [batch, seq_len]
&lt;/span&gt;    &lt;span class="n"&gt;input_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tensor&lt;/span&gt;
    &lt;span class="c1"&gt;# [num_tokens]
&lt;/span&gt;    &lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tensor&lt;/span&gt;

    &lt;span class="c1"&gt;# [total_num_logits] positions that actually require logits
&lt;/span&gt;    &lt;span class="n"&gt;logits_indices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tensor&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key shape is &lt;code&gt;input_ids: [num_tokens]&lt;/code&gt;: the scheduled tokens form one&lt;br&gt;
packed dimension rather than a padded batch matrix.&lt;/p&gt;
&lt;h3&gt;
  
  
  Flattening Requests into One Token Stream
&lt;/h3&gt;

&lt;p&gt;V1 concatenates the tokens selected for the current step into one packed&lt;br&gt;
sequence, regardless of which request they belong to or whether they represent&lt;br&gt;
prefill or decode work.&lt;/p&gt;

&lt;p&gt;In the example, A contributes a four-token prefill chunk, while B and C each&lt;br&gt;
contribute one decode token. If their current positions are 50 and 30, the&lt;br&gt;
packed tensors look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;input_ids&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;A0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;A1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;A2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;A3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;B0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;C0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# six values, one-dimensional
&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# each token's position within its own request
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A starts at position 0; B and C retain positions from their own sequences rather&lt;br&gt;
than being renumbered within the packed batch. Here&lt;br&gt;
&lt;code&gt;num_scheduled_tokens = [4, 1, 1]&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;query_start_loc = [0, 4, 5, 6]&lt;/code&gt;. The latter is a prefix sum that marks each&lt;br&gt;
request's range. Attention combines these boundaries with &lt;code&gt;seq_lens&lt;/code&gt; to keep&lt;br&gt;
requests separate.&lt;/p&gt;

&lt;p&gt;This design has two advantages.&lt;/p&gt;

&lt;p&gt;First, packing avoids computation on padding positions. Every scheduled position&lt;br&gt;
corresponds to actual request work, although kernels may still pad the packed&lt;br&gt;
token count internally for CUDA graphs or alignment.&lt;/p&gt;

&lt;p&gt;Second, packing carries the scheduler's unified token-count abstraction into&lt;br&gt;
execution. The runner uses &lt;code&gt;num_scheduled_tokens&lt;/code&gt; to place each request's slice&lt;br&gt;
in the flat sequence. Prefill and decode work can therefore coexist in one&lt;br&gt;
step, while metadata preserves their separate boundaries.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsh50uq64weuvttlmbwkg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsh50uq64weuvttlmbwkg.png" alt="fig5-flatten-input" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Figure: three variable-length requests—a prefill chunk and two decode&lt;br&gt;
requests—packed into one-dimensional &lt;code&gt;input_ids&lt;/code&gt;, &lt;code&gt;positions&lt;/code&gt;, and&lt;br&gt;
&lt;code&gt;slot_mapping&lt;/code&gt; arrays.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Two Values to Remember: &lt;code&gt;slot_mapping&lt;/code&gt; and &lt;code&gt;logits_indices&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Two additional fields connect this packed representation to later stages.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;slot_mapping&lt;/code&gt; is one-dimensional with shape &lt;code&gt;[num_tokens]&lt;/code&gt;, just like&lt;br&gt;
&lt;code&gt;input_ids&lt;/code&gt;. For every token in the current step, it records the destination&lt;br&gt;
slot for the newly computed K and V tensors. Part 5 follows this value into the&lt;br&gt;
KV-cache write kernel.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;logits_indices&lt;/code&gt; has shape &lt;code&gt;[total_num_logits]&lt;/code&gt;, which can be much smaller than&lt;br&gt;
&lt;code&gt;num_tokens&lt;/code&gt;. To generate the next token, vLLM usually needs only the final&lt;br&gt;
hidden state selected for each request. If a prefill chunk processes four&lt;br&gt;
tokens, the first three positions do not need logits. &lt;code&gt;logits_indices&lt;/code&gt; selects&lt;br&gt;
only the positions that feed the sampler; Part 6 follows that path.&lt;/p&gt;

&lt;p&gt;The model runner therefore produces a set of aligned flat views: &lt;code&gt;input_ids&lt;/code&gt;&lt;br&gt;
and &lt;code&gt;positions&lt;/code&gt; feed the forward pass; &lt;code&gt;query_start_loc&lt;/code&gt; and &lt;code&gt;seq_lens&lt;/code&gt; delimit&lt;br&gt;
requests for attention; &lt;code&gt;slot_mapping&lt;/code&gt; selects KV-cache destinations; and&lt;br&gt;
&lt;code&gt;logits_indices&lt;/code&gt; selects the positions that require logits.&lt;/p&gt;
&lt;h3&gt;
  
  
  What This Part Established
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The execution layer packs scheduled request slices into one-dimensional
&lt;code&gt;input_ids: [num_tokens]&lt;/code&gt; rather than a padded &lt;code&gt;[batch, seq]&lt;/code&gt; matrix.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;query_start_loc&lt;/code&gt; marks request boundaries in the packed query, while
&lt;code&gt;seq_lens&lt;/code&gt; supplies each request's visible KV length.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;num_scheduled_tokens&lt;/code&gt; connects scheduler decisions to the packed execution
batch, allowing prefill and decode work to coexist in one step.&lt;/li&gt;
&lt;li&gt;Two values connect later stages: &lt;code&gt;slot_mapping&lt;/code&gt; selects KV destinations in
Part 5, while &lt;code&gt;logits_indices&lt;/code&gt; selects the positions sampled in Part 6.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next, the flat token sequence enters the model forward pass. The remaining&lt;br&gt;
question is how request boundaries and KV-block mappings reach attention&lt;br&gt;
operators buried inside many model layers.&lt;/p&gt;
&lt;h2&gt;
  
  
  Part 4: Passing Attention Metadata into Model Execution
&lt;/h2&gt;

&lt;p&gt;The packed sequence now enters the model forward pass. Attention still needs&lt;br&gt;
step-specific metadata that does not appear in an ordinary model signature.&lt;/p&gt;

&lt;p&gt;That metadata includes &lt;code&gt;block_table&lt;/code&gt;, &lt;code&gt;seq_lens&lt;/code&gt;, and &lt;code&gt;query_start_loc&lt;/code&gt;. The&lt;br&gt;
request set and lengths change each step, yet every attention layer needs access&lt;br&gt;
to the corresponding state. How does it reach operators buried inside the&lt;br&gt;
Transformer stack?&lt;/p&gt;

&lt;p&gt;A direct design would pass the metadata through every layer:&lt;br&gt;
&lt;code&gt;forward(input_ids, attn_metadata)&lt;/code&gt;, then &lt;code&gt;layer(x, attn_metadata)&lt;/code&gt;, then&lt;br&gt;
&lt;code&gt;attention(x, attn_metadata)&lt;/code&gt;. In the OPT model definition, however, the&lt;br&gt;
forward signature contains &lt;code&gt;input_ids&lt;/code&gt; and &lt;code&gt;positions&lt;/code&gt; but no&lt;br&gt;
&lt;code&gt;attn_metadata&lt;/code&gt;. The metadata enters through a different route.&lt;/p&gt;
&lt;h3&gt;
  
  
  Attention Metadata Lives in &lt;code&gt;forward_context&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The answer is in &lt;code&gt;execute_model&lt;/code&gt;. The model's forward is wrapped in a context manager and runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Conceptual skeleton corresponding to execute_model in v1/worker
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;set_forward_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attn_metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vllm_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...):&lt;/span&gt;
    &lt;span class="n"&gt;hidden_states&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;input_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;set_forward_context&lt;/code&gt; (&lt;code&gt;vllm/forward_context.py:250&lt;/code&gt;) installs&lt;br&gt;
&lt;code&gt;attn_metadata&lt;/code&gt;, &lt;code&gt;slot_mapping&lt;/code&gt;, and related state in a scoped&lt;br&gt;
&lt;code&gt;forward_context&lt;/code&gt; while the model executes, then restores the previous context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# vllm/forward_context.py:250 (structural excerpt)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_forward_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attn_metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vllm_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...,&lt;/span&gt; &lt;span class="n"&gt;slot_mapping&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...):&lt;/span&gt;
    &lt;span class="n"&gt;forward_context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_forward_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;attn_metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vllm_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...,&lt;/span&gt; &lt;span class="n"&gt;slot_mapping&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;override_forward_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forward_context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;   &lt;span class="c1"&gt;# ← install the context globally
&lt;/span&gt;            &lt;span class="k"&gt;yield&lt;/span&gt;                                          &lt;span class="c1"&gt;# ← model forward executes inside this scope
&lt;/span&gt;    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;                                                &lt;span class="c1"&gt;# restore the previous context afterwards
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The metadata therefore travels through scoped context rather than through every&lt;br&gt;
model-layer signature. The ordinary forward path can keep tensor arguments such&lt;br&gt;
as &lt;code&gt;input_ids&lt;/code&gt; and &lt;code&gt;positions&lt;/code&gt; while attention retrieves its runtime metadata&lt;br&gt;
separately.&lt;/p&gt;

&lt;p&gt;Deep inside the model, the attention layer retrieves that context. Consider&lt;br&gt;
&lt;code&gt;unified_attention_with_output&lt;/code&gt;&lt;br&gt;
(&lt;code&gt;model_executor/layers/attention/attention.py:734&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# model_executor/layers/attention/attention.py:734 (excerpt)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;unified_attention_with_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;layer_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...):&lt;/span&gt;
    &lt;span class="n"&gt;layer_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_resolve_layer_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;layer_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ↓ attn_metadata is retrieved rather than passed as an argument
&lt;/span&gt;    &lt;span class="n"&gt;attn_metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kv_cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_attention_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;layer_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;impl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kv_cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attn_metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function receives &lt;code&gt;layer_name&lt;/code&gt;, not &lt;code&gt;attn_metadata&lt;/code&gt; directly.&lt;br&gt;
&lt;code&gt;get_attention_context(layer_name)&lt;/code&gt; resolves the layer and retrieves the&lt;br&gt;
runtime state from &lt;code&gt;forward_context&lt;/code&gt; (&lt;code&gt;attention.py:648&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# model_executor/layers/attention/attention.py:648 (excerpt)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_attention_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;layer_name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;forward_context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_forward_context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="c1"&gt;# ← retrieve the global context
&lt;/span&gt;    &lt;span class="n"&gt;attn_metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;forward_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attn_metadata&lt;/span&gt;    &lt;span class="c1"&gt;# retrieve this layer's metadata by layer_name
&lt;/span&gt;    &lt;span class="n"&gt;attn_layer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;forward_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;no_compile_layers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;layer_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;kv_cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attn_layer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kv_cache&lt;/span&gt;
    &lt;span class="n"&gt;slot_mapping&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;forward_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slot_mapping&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;layer_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;attn_metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attn_layer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kv_cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slot_mapping&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting path is: &lt;code&gt;execute_model&lt;/code&gt; installs the context, the model runs with&lt;br&gt;
its normal tensor inputs, and each attention operator uses &lt;code&gt;layer_name&lt;/code&gt; to&lt;br&gt;
retrieve &lt;code&gt;attn_metadata&lt;/code&gt;, &lt;code&gt;kv_cache&lt;/code&gt;, and &lt;code&gt;slot_mapping&lt;/code&gt; before dispatching to&lt;br&gt;
&lt;code&gt;self.impl.forward&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftetcybeucxl4jvpcseo7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftetcybeucxl4jvpcseo7.png" alt="fig6-forward-context" width="800" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Figure: &lt;code&gt;execute_model&lt;/code&gt; installs &lt;code&gt;attn_metadata&lt;/code&gt; in &lt;code&gt;forward_context&lt;/code&gt;; each&lt;br&gt;
attention layer retrieves its state by &lt;code&gt;layer_name&lt;/code&gt; before calling the backend.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Why This Indirection Helps &lt;code&gt;torch.compile&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Passing data through context is less explicit than ordinary arguments, but it&lt;br&gt;
serves the compiled execution path. &lt;code&gt;torch.compile&lt;/code&gt; benefits from stable graph&lt;br&gt;
inputs and shapes; threading a changing Python metadata object through every&lt;br&gt;
layer can introduce graph breaks, guards, or recompilation. Keeping that object&lt;br&gt;
outside the model signature lets the compiled graph consume the tensor state it&lt;br&gt;
needs through a controlled side channel. The exact compilation behavior still&lt;br&gt;
depends on the selected backend and configuration, so this should be read as&lt;br&gt;
the design goal rather than a guarantee that recompilation can never occur.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Dummy Dependency That Preserves Ordering
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;unified_attention_with_output&lt;/code&gt; also accepts an apparently unused parameter,&lt;br&gt;
&lt;code&gt;kv_cache_dummy_dep&lt;/code&gt;. It is intentionally present and is not discarded in the&lt;br&gt;
function body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# attention.py:744
# kv_cache_dummy_dep is not used but accepting it creates a data dependency
# that ensures torch.compile preserves ordering between KV cache update and
# attention forward.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;KV-cache update must precede the attention read, but mutation alone may not&lt;br&gt;
expose that ordering to the compiler. The dummy tensor makes the dependency&lt;br&gt;
explicit in the graph: the update produces a value consumed by attention, which&lt;br&gt;
prevents the two operations from being reordered across that edge.&lt;/p&gt;
&lt;h3&gt;
  
  
  What This Part Established
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Step-specific &lt;code&gt;attn_metadata&lt;/code&gt; stays out of the ordinary model-forward
signature and is installed in a scoped &lt;code&gt;forward_context&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Each attention layer retrieves its metadata, KV cache, and &lt;code&gt;slot_mapping&lt;/code&gt;
through &lt;code&gt;layer_name&lt;/code&gt; before entering the backend.&lt;/li&gt;
&lt;li&gt;This boundary is designed to make the model graph friendlier to
&lt;code&gt;torch.compile&lt;/code&gt; while still exposing dynamic runtime state.&lt;/li&gt;
&lt;li&gt;A dummy tensor dependency preserves the required update-before-read ordering
for the KV cache.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next part follows &lt;code&gt;slot_mapping&lt;/code&gt;, &lt;code&gt;block_table&lt;/code&gt;, and the KV cache into the&lt;br&gt;
write and read paths.&lt;/p&gt;
&lt;h2&gt;
  
  
  Part 5: Writing and Reading the Paged KV Cache
&lt;/h2&gt;

&lt;p&gt;PagedAttention is closely associated with vLLM, but the virtual-memory analogy&lt;br&gt;
alone does not identify the actual address structures. This part follows the&lt;br&gt;
write table and the read table in code.&lt;/p&gt;

&lt;p&gt;The KV cache is a pool of fixed-size blocks rather than one contiguous region&lt;br&gt;
per request. Each block stores K and V for &lt;code&gt;block_size&lt;/code&gt; tokens (use&lt;br&gt;
&lt;code&gt;block_size=16&lt;/code&gt; as a concrete example). A request's logical token sequence may&lt;br&gt;
span physically scattered blocks. This paging model removes the need to reserve&lt;br&gt;
one large contiguous allocation for every request and substantially reduces&lt;br&gt;
fragmentation from variable request lengths.&lt;/p&gt;

&lt;p&gt;Here, &lt;strong&gt;block&lt;/strong&gt; means a KV-cache allocation unit. Part 2 will also use CUDA&lt;br&gt;
thread blocks and matrix tiles; those are separate execution and computation&lt;br&gt;
concepts.&lt;/p&gt;

&lt;p&gt;The two paths use different address structures: newly computed K/V is written&lt;br&gt;
by token, while attention reads a request's history by logical cache block.&lt;/p&gt;
&lt;h3&gt;
  
  
  Write Path: One Token, One &lt;code&gt;slot_mapping&lt;/code&gt; Entry
&lt;/h3&gt;

&lt;p&gt;The previous part showed the attention layer retrieving &lt;code&gt;slot_mapping&lt;/code&gt; from&lt;br&gt;
&lt;code&gt;forward_context&lt;/code&gt;. We can now follow it into the write kernel.&lt;/p&gt;

&lt;p&gt;Each cache block contains &lt;code&gt;block_size&lt;/code&gt; token slots. Flattening physical block&lt;br&gt;
and in-block coordinates gives a cache-wide slot index:&lt;br&gt;
&lt;code&gt;physical_block_id × block_size + in_block_offset&lt;/code&gt;. &lt;code&gt;slot_mapping&lt;/code&gt;, with shape&lt;br&gt;
&lt;code&gt;[num_tokens]&lt;/code&gt;, gives that destination for every token processed in the step.&lt;/p&gt;

&lt;p&gt;Under the FlashAttention and FlashInfer paths, KV writing finally falls to &lt;code&gt;reshape_and_cache_flash&lt;/code&gt; (bound as &lt;code&gt;ops.reshape_and_cache_flash&lt;/code&gt; in &lt;code&gt;vllm/v1/attention/backends/fa_utils.py&lt;/code&gt;, defined in &lt;code&gt;vllm/_custom_ops.py:2744&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# vllm/_custom_ops.py:2744
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reshape_and_cache_flash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tensor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# K produced in this step
&lt;/span&gt;    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tensor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# V produced in this step
&lt;/span&gt;    &lt;span class="n"&gt;key_cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tensor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# K block pool
&lt;/span&gt;    &lt;span class="n"&gt;value_cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tensor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# V block pool
&lt;/span&gt;    &lt;span class="n"&gt;slot_mapping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tensor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# ← destination slot for each token
&lt;/span&gt;    &lt;span class="n"&gt;kv_cache_dtype&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;k_scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tensor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;v_scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tensor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: vLLM provides both &lt;code&gt;reshape_and_cache&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;reshape_and_cache_flash&lt;/code&gt;, paired with different KV-cache layouts and read&lt;br&gt;
kernels. In V1, KV update is represented separately from the attention read;&lt;br&gt;
the dependency described in Part 4 preserves their required order under&lt;br&gt;
compilation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For token &lt;code&gt;i&lt;/code&gt;, the kernel reads the corresponding K and V vectors and writes&lt;br&gt;
them to &lt;code&gt;slot_mapping[i]&lt;/code&gt;. Decode commonly contributes one new position per&lt;br&gt;
request; prefill contributes a chunk. The mapping is a per-token destination&lt;br&gt;
table, not the data itself.&lt;/p&gt;
&lt;h3&gt;
  
  
  Read Path: One &lt;code&gt;block_table&lt;/code&gt; per Request
&lt;/h3&gt;

&lt;p&gt;Reading introduces logical-to-physical indirection. A query attends to visible&lt;br&gt;
historical K/V positions that may be scattered across physical cache blocks;&lt;br&gt;
&lt;code&gt;block_table&lt;/code&gt; locates those blocks.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;block_table&lt;/code&gt; is part of &lt;code&gt;attn_metadata&lt;/code&gt;. For each request, entry &lt;code&gt;j&lt;/code&gt; stores the&lt;br&gt;
physical cache block backing logical block &lt;code&gt;j&lt;/code&gt;. With &lt;code&gt;block_size=16&lt;/code&gt;, logical&lt;br&gt;
tokens 0–15 use the physical block named by &lt;code&gt;block_table[0]&lt;/code&gt;, tokens 16–31 use&lt;br&gt;
&lt;code&gt;block_table[1]&lt;/code&gt;, and so on. This logical-to-physical array is the concrete&lt;br&gt;
structure behind the page-table analogy.&lt;/p&gt;

&lt;p&gt;On the GPU, the actual reading occurs in &lt;code&gt;forward&lt;/code&gt; in the FlashAttention backend, which hands &lt;code&gt;block_table&lt;/code&gt; directly to the flash kernel (&lt;code&gt;vllm/v1/attention/backends/flash_attn.py:796&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# vllm/v1/attention/backends/flash_attn.py:758 (excerpt)
&lt;/span&gt;&lt;span class="n"&gt;cu_seqlens_q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attn_metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_start_loc&lt;/span&gt;
&lt;span class="n"&gt;seqused_k&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attn_metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seq_lens&lt;/span&gt;
&lt;span class="n"&gt;block_table&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attn_metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;block_table&lt;/span&gt;
&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="nf"&gt;flash_attn_varlen_func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;num_actual_tokens&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key_cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;value_cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;num_actual_tokens&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;cu_seqlens_q&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cu_seqlens_q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# request boundaries in the flattened sequence (query_start_loc)
&lt;/span&gt;    &lt;span class="n"&gt;seqused_k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;seqused_k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;# history length of each request (seq_lens)
&lt;/span&gt;    &lt;span class="n"&gt;block_table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;block_table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# ← page table: logical block → physical block
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The GPU kernel follows &lt;code&gt;block_table&lt;/code&gt; while loading the K/V history used by the&lt;br&gt;
current query. The equivalent address translation is easier to see in the CPU&lt;br&gt;
backend. The following excerpt is simplified from&lt;br&gt;
&lt;code&gt;csrc/cpu/cpu_attn_impl.hpp&lt;/code&gt;; the FlashAttention backend performs the same kind&lt;br&gt;
of logical-to-physical lookup within its own tiled implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified from the attention read loop in csrc/cpu/cpu_attn_impl.hpp&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block_idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;start_block_idx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;block_idx&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;end_block_idx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="n"&gt;block_idx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;physical_block_idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;block_table&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;block_idx&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;        &lt;span class="c1"&gt;// ← logical block number → physical block number&lt;/span&gt;
    &lt;span class="n"&gt;kv_cache_t&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;k_cache_block_ptr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;k_head_cache_ptr&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;physical_block_idx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;kv_cache_num_blocks_stride&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// Compute Q@K using K from this block ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;physical_block_idx = block_table[block_idx]&lt;/code&gt; is the essential page-table&lt;br&gt;
lookup: traverse logical blocks in sequence, resolve each physical block, then&lt;br&gt;
load K and V from that location.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why the Write and Read Paths Use Different Addressing
&lt;/h3&gt;

&lt;p&gt;In PagedAttention, two sets of addressing with different granularity are used for writing and reading.&lt;/p&gt;

&lt;p&gt;Writes use &lt;code&gt;slot_mapping&lt;/code&gt; at token granularity: each newly computed token has&lt;br&gt;
one destination slot in the cache.&lt;/p&gt;

&lt;p&gt;Reads use &lt;code&gt;block_table&lt;/code&gt; at block granularity. Each request owns a sequence of&lt;br&gt;
logical blocks whose entries identify the physical blocks containing its KV&lt;br&gt;
history.&lt;/p&gt;

&lt;p&gt;Keeping the two addressing schemes separate is what makes paging practical.&lt;br&gt;
In the OS analogy, &lt;code&gt;block_table&lt;/code&gt; is the page table, the block pool is physical&lt;br&gt;
memory, and &lt;code&gt;slot_mapping&lt;/code&gt; gives the physical destination for a write. It is&lt;br&gt;
not a literal implementation of virtual memory, but the indirection is closely&lt;br&gt;
related.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fep8es8jeqwk8a121ndle.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fep8es8jeqwk8a121ndle.png" alt="fig7-paged-attention" width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Figure: &lt;code&gt;slot_mapping&lt;/code&gt; sends each newly computed token to one physical cache&lt;br&gt;
slot, while &lt;code&gt;block_table&lt;/code&gt; maps a request's logical history to a sequence of&lt;br&gt;
physical blocks for reading.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  What This Part Established
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The KV cache is a pool of fixed-size blocks. A request may occupy physically
non-contiguous blocks. Paging substantially reduces external fragmentation,
although the final partially filled block can still waste capacity.&lt;/li&gt;
&lt;li&gt;On write, &lt;code&gt;reshape_and_cache_flash&lt;/code&gt; uses &lt;code&gt;slot_mapping: [num_tokens]&lt;/code&gt; to place
each newly computed K/V pair in its destination slot.&lt;/li&gt;
&lt;li&gt;Read: &lt;code&gt;block_table&lt;/code&gt; stores the physical block numbers for each request. The
GPU attention path uses it to resolve &lt;code&gt;physical = block_table[logical]&lt;/code&gt; while
gathering the request's KV history.&lt;/li&gt;
&lt;li&gt;Writes use token-slot addresses; reads use logical-to-physical block mapping.
The mechanism resembles virtual-memory paging, but it is a specialized KV
allocator rather than a full virtual-memory implementation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After attention consumes the updated cache, the model produces hidden states.&lt;br&gt;
The final part follows selected hidden states through logits and sampling, then&lt;br&gt;
shows how completed requests release their blocks.&lt;/p&gt;
&lt;h2&gt;
  
  
  Part 6: From Logits to Tokens and Block Reclamation
&lt;/h2&gt;

&lt;p&gt;The model forward returns a packed set of hidden states. Only selected positions&lt;br&gt;
need logits for next-token sampling.&lt;/p&gt;
&lt;h3&gt;
  
  
  Compute Logits Only for the Required Positions
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;logits_indices&lt;/code&gt; prepared in Part 3 now determines which hidden states are&lt;br&gt;
projected into logits.&lt;/p&gt;

&lt;p&gt;The model computes a hidden state for every scheduled token, but ordinary&lt;br&gt;
next-token generation needs logits only at selected positions—typically the&lt;br&gt;
last scheduled position for each request. For a four-token prefill chunk, the&lt;br&gt;
first three hidden states therefore need not pass through &lt;code&gt;lm_head&lt;/code&gt; for that&lt;br&gt;
sampling step.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;logits_indices&lt;/code&gt; selects those hidden states before the vocabulary projection.&lt;br&gt;
Because &lt;code&gt;lm_head&lt;/code&gt; projects into a vocabulary with tens or hundreds of thousands&lt;br&gt;
of entries, avoiding unused positions saves substantial matrix-multiplication&lt;br&gt;
work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hidden_states[num_tokens]  --index_select(logits_indices)--&amp;gt;  keep only the required final position for each request
                           --lm_head--&amp;gt;  logits[num_reqs, vocab_size]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Sampling: From Logits to Token IDs
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Sampler&lt;/code&gt; converts each &lt;code&gt;vocab_size&lt;/code&gt;-dimensional logits vector into a token ID&lt;br&gt;
according to temperature, top-p, top-k, or greedy selection.&lt;/p&gt;

&lt;p&gt;The sampling results are packed into &lt;code&gt;ModelRunnerOutput&lt;/code&gt;—whose central field is&lt;br&gt;
&lt;code&gt;sampled_token_ids&lt;/code&gt;—and sent back to &lt;code&gt;EngineCore&lt;/code&gt; across the IPC boundary. This&lt;br&gt;
completes the worker-side work for the scheduler step.&lt;/p&gt;
&lt;h3&gt;
  
  
  Return to the Scheduler: Append, Finish, and Recycle
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;sampled_token_ids&lt;/code&gt; returns to EngineCore,&lt;br&gt;
&lt;code&gt;scheduler.update_from_output&lt;/code&gt; (&lt;code&gt;v1/core/sched/scheduler.py:1283&lt;/code&gt;) performs the&lt;br&gt;
third stage of &lt;code&gt;EngineCore.step()&lt;/code&gt;: applying model output to request state.&lt;/p&gt;

&lt;p&gt;First, take out the new token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# scheduler.py:1363 (excerpt)
&lt;/span&gt;&lt;span class="n"&gt;req_index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model_runner_output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;req_id_to_index&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;req_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;generated_token_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sampled_token_ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;req_index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sampled_token_ids&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, &lt;code&gt;_update_request_with_output&lt;/code&gt; appends the new token and evaluates stop&lt;br&gt;
conditions such as EOS, &lt;code&gt;max_tokens&lt;/code&gt;, and configured stop strings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# scheduler.py:1407 (excerpt)
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;new_token_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;new_token_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stopped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_update_request_with_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_token_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Third, when a request stops, its KV blocks are returned to the pool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# scheduler.py:1474 (excerpt)
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;stopped&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;finish_reason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_finished_reason&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;finished&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_handle_stopped_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;finished&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;kv_transfer_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_free_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# ← release its KV blocks
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;stopped_running_reqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the cycle ends, remove these stopped requests from the &lt;code&gt;running&lt;/code&gt; queue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# scheduler.py:1528 (excerpt)
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;stopped_running_reqs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;running&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;remove_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;running&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stopped_running_reqs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This closes the continuous-batching loop. When a request finishes,&lt;br&gt;
&lt;code&gt;_free_request&lt;/code&gt; returns its blocks to the pool and removes one entry from&lt;br&gt;
&lt;code&gt;running&lt;/code&gt;. On the next step, &lt;code&gt;schedule()&lt;/code&gt; can spend those blocks on requests in&lt;br&gt;
&lt;code&gt;waiting&lt;/code&gt;. Resource reclamation is therefore part of admission control, not an&lt;br&gt;
unrelated cleanup detail.&lt;/p&gt;

&lt;p&gt;An unfinished request remains eligible in &lt;code&gt;running&lt;/code&gt;; a later scheduling step can&lt;br&gt;
advance it again.&lt;/p&gt;

&lt;p&gt;Finally, EngineCore packages the results as &lt;code&gt;EngineCoreOutputs&lt;/code&gt; and sends them&lt;br&gt;
to the main process over IPC. &lt;code&gt;LLMEngine.step()&lt;/code&gt; consumes them through&lt;br&gt;
&lt;code&gt;get_output()&lt;/code&gt;, detokenizes token IDs, assembles &lt;code&gt;RequestOutput&lt;/code&gt;, and returns it&lt;br&gt;
to the caller. That completes the path from an API request to generated text.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbaf6s8hrjwedhxxnczkc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbaf6s8hrjwedhxxnczkc.png" alt="fig8-output-sampling" width="799" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Figure: &lt;code&gt;logits_indices&lt;/code&gt; selects the required hidden states, &lt;code&gt;lm_head&lt;/code&gt;&lt;br&gt;
produces logits, and sampling yields new token IDs. Finished requests release&lt;br&gt;
their KV blocks; unfinished requests remain active.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What This Part Established
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;logits_indices&lt;/code&gt; selects the positions that require vocabulary projection,
avoiding &lt;code&gt;lm_head&lt;/code&gt; work for unused intermediate positions.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Sampler&lt;/code&gt; produces token IDs according to each request's sampling parameters
and returns them in &lt;code&gt;ModelRunnerOutput&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;update_from_output&lt;/code&gt; appends new tokens, evaluates stopping conditions, and
calls &lt;code&gt;_free_request&lt;/code&gt; for completed requests.&lt;/li&gt;
&lt;li&gt;Reclaimed KV capacity becomes available to later scheduler steps.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What to Carry Forward
&lt;/h2&gt;

&lt;p&gt;The six parts reduce vLLM's request path to a scheduler, a paged memory manager,&lt;br&gt;
and a GPU execution pipeline. Four design choices are especially reusable:&lt;/p&gt;

&lt;p&gt;First, the scheduler expresses both prefill and decode as per-request token&lt;br&gt;
progress. The execution layer then packs those scheduled tokens into one&lt;br&gt;
dimension. Chunked prefill and continuous batching build on the same contract&lt;br&gt;
instead of requiring separate batch formats.&lt;/p&gt;

&lt;p&gt;Second, V1 preempts by recomputing rather than swapping. Under KV-cache&lt;br&gt;
pressure, it frees the victim's blocks, resets &lt;code&gt;num_computed_tokens&lt;/code&gt;, and&lt;br&gt;
requeues the request for prefill instead of moving KV state to CPU memory and&lt;br&gt;
back over PCIe. This trades additional computation for less host-device data&lt;br&gt;
movement.&lt;/p&gt;

&lt;p&gt;Third, &lt;code&gt;forward_context&lt;/code&gt; keeps step-specific &lt;code&gt;attn_metadata&lt;/code&gt; out of the forward&lt;br&gt;
signature so &lt;code&gt;torch.compile&lt;/code&gt; can reuse a stable graph. A dummy tensor dependency&lt;br&gt;
then constrains operator ordering. The indirection is less obvious to read, but&lt;br&gt;
it serves compilation and execution correctness.&lt;/p&gt;

&lt;p&gt;Fourth, writes are addressed per token while reads are resolved per block.&lt;br&gt;
&lt;code&gt;slot_mapping&lt;/code&gt; selects each token's write slot; &lt;code&gt;block_table&lt;/code&gt; maps a request's&lt;br&gt;
logical KV blocks to physical blocks. At its core, the page-table lookup is&lt;br&gt;
&lt;code&gt;physical = block_table[logical]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The same discipline appears throughout the path: identify whether the scarce&lt;br&gt;
resource is compute, bandwidth, KV capacity, TTFT, or inter-token latency, then&lt;br&gt;
make the trade-off explicit. vLLM's throughput does not come from one isolated&lt;br&gt;
trick. It comes from small mechanisms that compose: a budget clamp expressed by&lt;br&gt;
&lt;code&gt;min&lt;/code&gt;, one level of &lt;code&gt;block_table&lt;/code&gt; indirection, and a deliberate&lt;br&gt;
&lt;code&gt;num_computed_tokens = 0&lt;/code&gt; on preemption.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this article doesn’t cover
&lt;/h2&gt;

&lt;p&gt;To keep the path traceable, this article stops at the Python/CUDA boundary. It&lt;br&gt;
does not cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CUDA and Triton kernel internals. We stop at the attention-backend call rather
than tracing tiling and online softmax.&lt;/li&gt;
&lt;li&gt;Block-allocation details inside &lt;code&gt;kv_cache_manager&lt;/code&gt;, including reference
counting and prefix-cache hash reuse. The article treats it only as a block
allocator.&lt;/li&gt;
&lt;li&gt;Multi-GPU parallelism: communication and partitioning under tensor (TP),
pipeline (PP), and data parallelism (DP).&lt;/li&gt;
&lt;li&gt;Advanced features such as speculative decoding, multimodality, and structured
output. Their state appears in &lt;code&gt;SchedulerOutput&lt;/code&gt;, but they are outside this
request path.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This walkthrough targets the vLLM 0.22.0 V1 architecture. Source locations are&lt;br&gt;
included so readers can compare the excerpts with their installed version. The&lt;br&gt;
editable Excalidraw sources for Figures 2–8 are published with this site.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vllm</category>
      <category>llm</category>
      <category>inference</category>
      <category>python</category>
    </item>
  </channel>
</rss>
