@amitiitbhu:新文章:vLLM 是如何工作的?请在此阅读:https://outcomeschool.com/blog/how-does-vllm-work…

X AI KOLs Timeline 工具

摘要

一篇详细的博客文章,解释了 vLLM 的工作原理,包括 PagedAttention、KV 缓存管理和连续批处理,以实现高效的 LLM 服务。

新文章:vLLM 是如何工作的?请在此阅读:https://outcomeschool.com/blog/how-does-vllm-work…
查看原文
查看缓存全文

缓存时间: 2026/06/17 16:02

新文章:vLLM 是如何工作的?阅读地址:https://outcomeschool.com/blog/how-does-vllm-work…


vLLM 是如何工作的?

来源:https://outcomeschool.com/blog/how-does-vllm-work vLLM 是如何工作的?

在这篇博客中,我们将了解 vLLM 是如何工作的。我们还将了解为什么需要它,它如何巧妙地管理内存,以及在现实世界中它在哪里被用来同时为许多用户提供服务大型语言模型。

我们将涵盖以下内容:

  • 什么是 LLM 服务
  • 快速回顾预填充(prefill)、解码(decode)和 KV 缓存(KV cache)
  • 问题:KV 缓存消耗 GPU 内存
  • 为什么朴素服务会浪费内存
  • 什么是 vLLM
  • 核心思想:PagedAttention
  • PagedAttention 如何共享内存
  • 连续批处理(Continuous batching)
  • 兼容 OpenAI 的 API 服务器
  • vLLM 的优势
  • vLLM 在现实世界中的应用

我是 Amit Shekhar,Outcome School (https://outcomeschool.com/) 的创始人。我教导和指导了许多开发者,他们的努力让他们获得了高薪技术工作,帮助了许多科技公司解决独特问题,并创建了许多被顶级公司使用的开源库。我热衷于通过开源、博客和视频分享知识。

我在 Outcome School 教授人工智能与机器学习 (https://outcomeschool.com/program/ai-and-machine-learning)。

让我们开始吧。

什么是 LLM 服务

在谈论 vLLM 之前,我们必须先理解服务一个 LLM 意味着什么。

一个大型语言模型,或者 LLM,是 ChatGPT 和 Claude 等工具背后的技术。我们给它一些文本,它返回给我们一些文本。

服务一个 LLM 意味着在计算机上运行该模型,以便许多用户可以同时向它发送问题并获得答案。

简单来说,服务是接收用户请求、通过模型运行请求、然后返回回复的部分。

假设我们构建了一个聊天助手。成千上万的人同时打开它。每个人输入一个问题。所有这些问题都到达我们的模型,每个用户都期望一个快速的答案。接受所有这些请求、为每个请求运行模型并返回每个回复的软件部分被称为服务引擎

我们可以把流程想象成下面这样:

`` SERVING AN LLM

User 1 –question–> +—————–+ +—————–+ User 2 –question–> | serving engine |—–>| model on GPU | User 3 –question–> | (takes requests| | (does the heavy | … | sends replies)|<—–| math, replies) | User N –question–> +—————–+ +—————–+ | +–reply–> back to each user ``

在这里,我们可以看到许多用户同时发送他们的请求。服务引擎位于中间。它收集所有请求,通过 GPU 上的模型运行它们,并将每个回复发送回正确的用户。

现在,这是重要部分。LLM 运行在一个称为 GPU 的特殊芯片上。GPU 是一种强大的处理器,非常擅长 LLM 所需的繁重数学计算。GPU 很贵,而且内存有限。所以,如果我们浪费 GPU 内存,我们服务的用户就会更少,成本就会上升。

所以,服务的整个游戏就是:我们想要在一个 GPU 上,尽可能快地为尽可能多的用户提供服务。记住这个目标,因为 vLLM 正是为了赢得这个游戏而构建的。

快速回顾预填充、解码和 KV 缓存

为了理解 vLLM,我们必须稍微了解一下 LLM 是如何产生答案的。别担心,我们会保持简单。

当我们发送一个提示时,模型不会将其作为完整的单词来阅读。它首先将文本分解成称为tokens的小片段。一个 token 是一小块文本,大致对应一个单词或单词的一部分。所以提示变成了一个 token 列表。

然后模型分两个阶段工作。

第一阶段是预填充。 这是模型读取我们整个提示并在写出回复的任何一个单词之前处理每个 token 的阶段。简单来说,预填充就是模型阅读和消化我们整个提示的阶段。

第二阶段是解码。 这是模型一次一个 token 写出回复的阶段。它写一个 token,然后查看目前为止的所有内容,然后写下一个 token,一直持续到答案完成。

现在,在两个阶段中,对于每个 token,模型都会计算一些内部值并存储它们。这些存储的值被保存在称为 KV 缓存 (https://outcomeschool.com/blog/kv-cache-in-llms) 的东西中。

让我们用简单的语言来理解 KV 缓存。当模型读取或写入每个 token 时,它会创建该 token 的一个小摘要,一种关于该 token 在它之前所有内容上下文中的含义的笔记。KV 缓存就是所有这些笔记的集合,每个 token 一组笔记。

这就是为什么 KV 缓存如此重要。为了在解码期间写出每个新 token,模型需要之前所有 token 的笔记。没有 KV 缓存,模型就必须为每个新单词重新计算所有那些笔记。那将会慢得可怕。所以,模型一次性存储笔记并重复使用它们。KV 缓存是使生成长答案变快的原因。

这是要记住的关键点:

KV 缓存随着答案的增长而增长。每个新 token 都会向 KV 缓存添加一组新的笔记,所有这些笔记都位于 GPU 内存中。

我们可以把两个阶段和不断增长的 KV 缓存想象成下面这样:

`` PREFILL then DECODE: the KV cache grows one set of notes per token

prompt tokens: [ Tell ][ me ][ a ][ joke ] | | | | PREFILL writes: [n] [n] [n] [n] (one note per prompt token)

so KV cache = [n][n][n][n] (4 notes after prefill)

DECODE step 1: writes “Why” KV cache: [n][n][n][n][n] DECODE step 2: writes “did” KV cache: [n][n][n][n][n][n] DECODE step 3: writes “the” KV cache: [n][n][n][n][n][n][n] … (grows by one each step) ``

在这里,我们可以注意到预填充一次性为每个提示 token 创建一组笔记。然后解码一次一个 token 写出回复,每个新 token 向 KV 缓存添加一组新的笔记。所以,答案越长,KV 缓存在 GPU 内存中变得越大。

这是我们需要的基石。现在我们准备看到真正的问题。

问题:KV 缓存消耗 GPU 内存

既然我们知道了什么是 KV 缓存,让我们看看它引起的麻烦。

模型本身占用了 GPU 内存的一大块。剩下的内存用于保存我们当前正在服务的所有请求的 KV 缓存。

所以,KV 缓存是决定我们一次可以服务多少个用户的东西。我们拥有的空闲 KV 缓存内存越多,我们可以一起运行的请求就越多。

简单来说。每个正在服务的请求都有自己的 KV 缓存,并且该 KV 缓存随着其答案的增长而不断增长。如果我们正在服务许多用户,他们所有的 KV 缓存都一起活在 GPU 内存中,争夺同样有限的空间。

所以,服务 LLM 的真正瓶颈不是数学计算速度。而是 KV 缓存内存。

这意味着服务 LLM 的整个挑战归结为管理内存。如果我们管理好 KV 缓存内存,我们就能服务更多用户。如果我们管理不好,就会浪费 GPU,服务更少的用户。

现在,下一个问题是,朴素方法如何管理这个内存,为什么它不好?让我们看看。

为什么朴素服务会浪费内存

让我们理解一个简单的朴素服务引擎如何处理 KV 缓存,以及它在哪里出错。

朴素方法做的事情感觉上是安全的,但实际上非常浪费。当一个请求来时,引擎不知道答案会有多长。所以,为了安全起见,它预留一个大的连续内存块,大到足以容纳可能的最长答案。

假设模型最多可以产生 2000 个 token。对于每一个请求,朴素引擎立即预留 2000 个 token 的 KV 缓存空间,甚至是在模型写出任何东西之前。

这就是问题所在。大多数答案都很短。如果一个用户的答案只有 50 个 token,那么其他 1950 个 token 的空间就闲置在那里,被预留但未使用,什么也不做。我们阻塞了大量的内存给一个从未需要它的答案。

这种浪费有两个名字,我们必须都理解。

第一个是过度预留。 这意味着我们预留的内存远多于请求实际使用的内存。预留但未使用的空间不能给其他人,所以是浪费的。

第二个是碎片化。 碎片化意味着空闲内存被分割成无法使用的小散碎片。让我们用一张图来理解。

我们可以把朴素方法想象成下面这样:

`` NAIVE SERVING: one big continuous block reserved per request

Request A: [#### used (50) …………… wasted, reserved for 2000 …………..] Request B: [###### used (120) ………… wasted, reserved for 2000 …………..] Request C: [## used (20) …………….. wasted, reserved for 2000 …………..]

free memory left: scattered tiny gaps -> cannot fit a new request ``

在这里,我们可以看到每个请求都抓取了一个巨大的连续块,但只使用了前面的一小部分。其余部分被浪费了。而块之间的小的空隙太小太分散,无法容纳一个新请求。所以,即使从技术上讲有很多空闲内存,我们也无法使用。这就是碎片化。

结果很糟糕。GPU 在纸面上有大量内存,但由于浪费和分散,我们一次只能服务少数用户。我们为强大的 GPU 付了钱,却只用了它的一小部分。

所以,vLLM 来救援了。

什么是 vLLM

既然我们理解了问题,让我们理解解决方案。

vLLM 是一个用于服务 LLM 的高吞吐量引擎。它通过非常高效地管理 KV 缓存内存,构建目的是在 GPU 上尽可能多地服务请求。

简单来说,vLLM 是一个聪明的服务引擎,它停止浪费 GPU 内存,因此它可以同时服务更多的用户。

让我们理解单词 throughput,因为它就在名字 “high-throughput” 中。吞吐量意味着我们在给定时间内完成了多少工作。高吞吐量意味着我们每秒服务大量的 token 和请求。这正是 vLLM 构建时想要最大化的。

vLLM 通过两个协同工作的主要思想解决了内存问题:

  • PagedAttention,它将 KV 缓存管理成小的固定大小的块,而不是一个巨大的块,这样就不会浪费内存。
  • 连续批处理,通过在每个步骤将完成的请求换出并将新请求换入,保持 GPU 忙碌。

别担心,我们将详细了解它们每一个。让我们从 PagedAttention 开始,因为它是 vLLM 的核心。

PagedAttention,核心思想

让我们一步步理解 vLLM 背后的核心思想。

PagedAttention 将 KV 缓存管理成小的固定大小的块,按需分配内存,而不是预先预留一个大的块。

简单来说,vLLM 不是为每个请求抓取一个巨大的内存块,而是以小而等大的块的形式分配内存,仅在请求实际需要更多时分配。

这个想法借鉴了操作系统管理内存的方式。操作系统使用称为 pages 的小型固定大小块来管理内存。当程序需要更多内存时,操作系统会再给它一个 page。这些 page 在内存中不必彼此相邻。操作系统维护一个小表,记录每个 page 在哪里。

vLLM 对 KV 缓存做了完全相同的事情。它将 KV 缓存内存分割成小的固定大小的 blocks,每个 block 保存固定数量 token 的笔记,例如 16 个 token。当一个请求需要存储更多 token 时,vLLM 再给它一个 block。这些 block 在内存中不必彼此相邻。vLLM 维护一个小表,称为 block table,记录哪些 blocks 属于哪个请求以及它们的顺序。

让我们通过一个例子来走一遍。

步骤 1: 一个请求来了,开始生成答案。vLLM 给它一个 block,足够容纳 16 个 token。请求开始填充那个 block。

步骤 2: 答案超过 16 个 token。第一个 block 满了。vLLM 简单地给请求再一个 block,无论内存中哪个 free block 可用。它不需要在第一个 block 旁边。

之后: 答案不断增长,vLLM 一次一个 block 地持续分配,仅按需分配。当答案完成时,vLLM 立即释放该请求的所有 blocks,这些 blocks 立即对其他请求可用。

我们可以用下面的简单图来表示:

`` PAGED ATTENTION: KV cache split into small fixed-size blocks

GPU memory: [B1][B2][B3][B4][B5][B6][B7][B8][B9] … (a pool of equal blocks)

Request A’s block table -> B1, B4, B7 (3 blocks, given as needed) Request B’s block table -> B2, B3 (2 blocks, given as needed) Request C’s block table -> B5 (1 block, just started)

free blocks ready to hand out: B6, B8, B9 ``

在这里,我们可以看到内存是一个共享的等大 blocks 池。每个请求只得到它实际需要的 blocks,它们可以分散在池中的任何地方。block table 是将请求与其 blocks 按正确顺序联系起来的小地图。当一个请求完成时,它的 blocks 直接返回到 free 池中,供下一个请求使用。

问题解决了。没有过度预留,因为我们只在真正需要时才分配一个 block。几乎没有碎片化,因为每个 block 大小相同,所以任何 free block 都能适合任何请求。浪费的内存降到了几乎为零。

要深入学习 PagedAttention、KV 缓存和 vLLM,请查看 Outcome School 的 AI 与机器学习课程 (https://outcomeschool.com/program/ai-and-machine-learning)。

PagedAttention 如何共享内存

PagedAttention 还为我们提供了一个美妙的东西,一旦我们有了 blocks,它就免费出现了。那就是共享

因为 KV 缓存现在由小 blocks 组成,两个不同的请求可以将它们的 block table 指向内存中的同一个 block,而不是每个都保留自己的副本。这意味着它们共享相同部分的笔记。

让我们通过两个实际的例子来理解共享如何大有帮助。

第一个例子是相同的前缀。 前缀是某事物的起始部分。假设许多用户发送的请求都以相同的长系统指令开头,例如 “You are a polite customer support agent for a car dealership.” 这个长的开头对每个人都是相同的。使用 blocks,vLLM 可以存储共享开头的 KV 缓存一次,然后让每个请求指向这些相同的 blocks。我们不多次存储相同的笔记。我们一次性存储并共享。我们有一篇详细的博客关于提示缓存,解释了如何为相同前缀重用 KV 缓存。

第二个例子是 beam search。 Beam search 是一种生成文本的方式,模型同时探索几个可能的答案,然后保留最好的一个。这几个答案,称为 beams,都共享相同的开头,只在后面不同。使用 blocks,所有 beams 可以共享共同开头的 blocks,只在它们实际分叉的地方使用单独的 blocks。

我们可以像下面这样想象共享:

`` SHARING WITH BLOCKS

shared beginning: [B1][B2] <- one copy in memory, used by all | +–––––––+–––––––+ | | | Request A Request B Beam C adds [B5] adds [B6]

相似文章

内存

Reddit r/artificial

解释了为什么由于KV缓存随上下文长度和并发用户数扩展,LLM推理越来越受内存带宽限制,以及像vLLM和PagedAttention这样的系统如何提高内存利用率。