Inside vLLM: Anatomy of a High-Throughput LLM Inference System (2025)
https://www.aleksagordic.com/blog/vllm📌 【深度解析】拆解 vLLM 架構:高吞吐量 LLM 推論系統的核心設計
TL;DR:深入剖析 vLLM 如何透過 PagedAttention 與 Continuous Batching 實現高效能推論。
在 LLM 推論領域,vLLM 已成為實現高吞吐量(High-throughput)的關鍵技術。然而,這個系統內部究竟是如何運作,從 Request 的進入到最終 Output 的產出,中間經歷了哪些複雜的調度與記憶體管理?
🎣 從離線執行到線上服務的演進
雖然我們常使用 vLLM 來提供 Web 服務,但其最基礎的構建塊是「LLM Engine」。在最簡單的離線(Offline)設定下,它能處理一組固定的 Prompts,並在單一 GPU 上以同步方式執行。然而,現代推論系統需要的是「非同步(Async)」且「多 GPU」的線上服務能力,這正是 vLLM 複雜架構發揮作用的地方。
🧩 LLM Engine 的核心組成元件
一個完整的 vLLM Engine 由以下幾個關鍵部分組成:
- vLLM Config:包含模型設定、快取(Cache)與並行(Parallelism)等所有參數的控制開關。
- Processor:將原始輸入(如 Text)轉換為 EngineCoreRequest,包含驗證與 Tokenization(斷詞)流程。
- Engine Core Client:負責與核心引擎溝通,從單機的 InprocClient 演進到可擴展的 DPLBAsyncMPClient。
- Output Processor:將引擎輸出的原始資料轉換為使用者可讀的 RequestOutput。
🤔 Engine Core 的內部運作機制
引擎核心(Engine Core)是系統的大腦,其內部包含三個關鍵子系統:
- Model Executor:驅動模型的 Forward Pass(前向傳播)。
- Structured Output Manager:負責 Guided Decoding(引導式解碼)。
- Scheduler(調度器):決定哪些請求進入下一個執行步驟。
- 策略設定:支援 FCFS(先來先服務)或 Priority(優先級)策略。
- 隊列管理:包含 Waiting Queue(等待隊列)與 Running Queue(執行隊列)。
- KV Cache Manager:這是 PagedAttention 的核心,維護著一個龐大的
free_block_queue(可用快取塊池),透過索引結構將 Token 映射到計算好的 KV Cache 區塊。
📊 KV Cache 的計算與記憶體配置
對於標準的 Transformer 層,一個 Block 的大小計算公式如下:
2 (key/value) * block_size (預設 16) * num_kv_heads * head_size * dtype_num_bytes
在初始化 Worker 時,系統會執行以下關鍵步驟:
- 分配 VRAM:根據
gpu_memory_utilization(例如 0.8,即 80%)檢查可用顯存。 - 初始化 Model Runner:持有 Sampler、KV Cache 以及 InputBatch(包含 CPU 端的 Forward-pass 緩衝區與 Block Tables)。
- CUDA Graphs 捕捉:若未指定
--enforce-eager,系統會對 Warmup Batch 進行 Dummy Run 並捕捉 CUDA Graphs,透過重放(Replay)預先編譯好的圖形來減少 Kernel Launch 的開銷,提升延遲(Latency)表現。
💡 從 Request 到生成的生命週期
當一個 Prompt 進入系統後,流程如下:
- 封裝:建立唯一 Request ID,進行 Tokenization 並包裝成 EngineCoreRequest。
- 排隊:請求被標記為
WAITING並加入 Scheduler 的等待隊列。 - 執行:引擎不斷呼叫
step()函式。 - 連續批處理 (Continuous Batching):在非同步引擎中,系統在每一步都會同時考慮新舊請求,利用自定義 Kernel 高效處理扁平化後的 Batch。
⚠️ 技術限制與版本說明
- 本分析基於 2025 年 8 月的 Commit (42172ad) 進行。
- 隨著 V0 引擎被棄用(Deprecated),具體的類別名稱(Class names)與細節可能會隨版本更迭而改變,但核心設計理念(如 PagedAttention)仍具參考價值。
🎯 實務啟示
對於需要優化推論效能的工程師來說,理解 vLLM 的架構有助於掌握兩大關鍵:記憶體管理(KV Cache)與調度策略(Scheduling)。如果你追求極致的延遲,關注 CUDA Graphs 的應用;如果你追求吞吐量,則需深入研究 PagedAttention 如何減少記憶體碎片化。
🔗 來源
- 標題:Inside vLLM: Anatomy of a High-Throughput LLM Inference System (2025)
- 連結:https://www.aleksagordic.com/blog/vllm
#vLLM #LLM #Inference #MachineLearning #DeepLearning #PagedAttention #ContinuousBatching #GPU #MachineLearningEngineering #AIInfrastructure
原始資料 Hacker News · 收集於 2026-08-07
摘要原文
In this post, I'll gradually introduce all of the core system components and advanced features that make up a modern high-throughput LLM inference system. In particular I'll be doing a breakdown of how vLLM [1] works. This post is the first in a series. It starts broad and then layers in detail (following an inverse-pyramid approach) so you can form an accurate high-level mental model of the complete system without drowning in minutiae. Later posts will dive into specific subsystems. This post is structured into five parts: LLM engine & engine core : fundamentals of vLLM (scheduling, paged attention, continuous batching, etc.) Advanced features : chunked prefill, prefix caching, guided & speculative decoding, disaggregated P/D Scaling up : from single-GPU to multi-GPU execution Serving layer : distributed / concurrent web scaffolding Benchmarks and auto-tuning : measuring latency and throughput 📝 Notes Analysis is based on commit 42172ad (August 9th, 2025). Target audience: anyone curious about how state-of-the-art LLM engines work, as well as those interested in contributing to vLLM, SGLang, etc. I'll focus on the V1 engine . I also explored V0 ( now deprecated ), which was valuable for understanding how the project evolved, and many concepts still carry over. The first section on LLM Engine / Engine Core might be a bit overwhelming/dry - but the rest of the blog has plenty examples and visuals. :) LLM Engine & Engine Core The LLM engine is the fundamental building block of vLLM. On its own, it already enables high-throughput inference - but only in an offline setting. You can't serve it to customers over the web yet. We'll use the following offline inference snippet as our running example (adapted from basic.py ). from vllm import LLM , SamplingParams prompts = [ "Hello, my name is" , "The president of the United States is" , ] sampling_params = SamplingParams ( temperature = 0.8 , top_p = 0.95 ) def main ( ) : llm = LLM ( model = "TinyLlama/TinyLlama-1.1B-Chat-v1.0" ) outputs = llm . generate ( prompts , sampling_params ) if __name__ == "__main__" : main ( ) 📝 Environment vars: VLLM_USE_V1="1" # we're using engine V1 VLLM_ENABLE_V1_MULTIPROCESSING="0" # we're running in a single process This configuration is: offline (no web/distributed system scaffolding) synchronous (all execution happens in a single blocking process) single-GPU (no data/model/pipeline/expert parallelism; DP/TP/PP/EP = 1) using standard transformer [2] (supporting hybrid models like Jamba requires a more complex hybrid KV-cache memory allocator) From here, we'll gradually build up to an online, async, multi-GPU, multi-node inference system - but still serving a standard transformer. In this example we do two things, we: Instantiate an engine Call generate on it to sample from the given prompts Let's start analyzing the constructor. LLM Engine constructor The main components of the engine are: vLLM config (contains all of the knobs for configuring model, cache, parallelism, etc.) processor (turns raw inputs → EngineCoreRequests via validation, tokenization, and processing) engine core client (in our running example we're using InprocClient which is basically == EngineCore ; we'll gradually build up to DPLBAsyncMPClient which allows serving at scale) output processor (converts raw EngineCoreOutputs → RequestOutput that the user sees) 📝 Note: With the V0 engine being deprecated, class names and details may shift. I'll emphasize the core ideas rather than exact signatures. I'll abstract away some but not all of those details. Engine core itself is made up of several sub components: Model Executor (drives forward passes on the model, we're currently dealing with UniProcExecutor which has a single Worker process on a single GPU). We'll gradually build up to MultiProcExecutor which supports multiple GPUs Structured Output Manager (used for guided decoding - we'll cover this later) Scheduler (decides which requests go into the next engine step) - it further contains: policy setting - it can be either FCFS (first come first served) or priority (higher priority requests are served first) waiting and running queues KV cache manager - the heart of paged attention [3] The KV-cache manager maintains a free_block_queue - a pool of available KV-cache blocks (often on the order of hundreds of thousands, depending on VRAM size and block size). During paged attention, the blocks serve as the indexing structure that map tokens to their computed KV cache blocks. Core components described in this section and their relationships Block size for a standard transformer layer (non-MLA [4] ) is computed as follows: 2 (key/value) * block_size (default=16) * num_kv_heads * head_size * dtype_num_bytes (e.g. 2 for bf16) During model executor construction, a Worker object is created, and three key procedures are executed. (Later, with MultiProcExecutor , these same procedures run independently on each worker process across different GPUs.) Init device: Assign a CUDA device (e.g. "cuda:0") to the worker and check that the model dtype is supported (e.g. bf16) Verify enough VRAM is available, given the requested gpu_memory_utilization (e.g. 0.8 → 80% of total VRAM) Set up distributed settings (DP / TP / PP / EP, etc.) Instantiate a model_runner (holds the sampler, KV cache, and forward-pass buffers such as input_ids , positions , etc.) Instantiate an InputBatch object (holds CPU-side forward-pass buffers, block tables for KV-cache indexing, sampling metadata, etc.) Load model: Instantiate the model architecture Load the model weights Call model.eval() (PyTorch's inference mode) Optional: call torch.compile() on the model Initialize KV cache Get per-layer KV-cache spec. Historically this was always FullAttentionSpec (homogeneous transformer), but with hybrid models (sliding window, Transformer/SSM like Jamba) it became more complex (see Jenga [5] ) Run a dummy/profiling forward pass and take a GPU memory snapshot to compute how many KV cache blocks fit in available VRAM Allocate, reshape and bind KV cache tensors to attention layers Prepare attention metadata (e.g. set the backend to FlashAttention) later consumed by kernels during the fwd pass Unless --enforce-eager is provided, for each of warmup batch sizes do a dummy run and capture CUDA graphs. CUDA graphs record the whole sequence of GPU work into a DAG. Later during fwd pass we launch/replay pre-baked graphs and cut on kernel launch overhead and thus improve latency. I've abstracted away many low-level details here — but these are the core pieces I'll introduce now, since I'll reference them repeatedly in the following sections. Now that we have the engine initialized let's proceed to the generate function. Generate function The first step is to validate and feed requests into the engine. For each prompt we: Create a unique request ID and capture its arrival time Call an input preprocessor that tokenizes the prompt and returns a dictionary containing prompt , prompt_token_ids , and a type (text, tokens, embeds, etc.) Pack this info into an EngineCoreRequest , adding priority, sampling params, and other metadata Pass the request into the engine core, which wraps it in a Request object and sets its status to WAITING . This request is then added to the scheduler's waiting queue (append if FCFS, or heap-push if priority) At this point the engine has been fed and execution can begin. In the synchronous engine example, these initial prompts are the only ones we'll process — there's no mechanism to inject new requests mid-run. In contrast, the asynchronous engine supports this (aka continuous batching [6] ): after each step, both new and old requests are considered. Because the forward pass flattens the batch into a single sequence and custom kernels handle it efficiently, continuous batching is fundamentally supported even in the synchronous engine. Next, as long as there are requests to process, the engine repeatedly calls its step() function. (90 points, 5 comments on Hacker News)
由 tencent/hy3:free 自動生成