vLLM工作原理解析

amitiitbhu 发布于 2026-06-24 阅读 102

vLLM是一个高吞吐率的LLM服务引擎,通过PagedAttention技术将KV缓存分成固定大小的块,按需分配内存,避免过度预留和碎片化,并支持请求间共享相同前缀的缓存。同时采用连续批处理,在每一步动态替换已完成请求,保持GPU始终忙碌。vLLM提供与OpenAI兼容的API,易于集成,能显著提升GPU利用率和吞吐量,降低服务成本。文章详细解释了LLM服务的挑战、KV缓存问题、传统方法的浪费,以及vLLM的核心机制和实际应用场景。

图像

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

我们将涵盖以下内容:

  • 什么是为大语言模型提供服务
  • 快速回顾 prefill、decode 和 KV cache
  • 问题:KV cache 消耗 GPU 内存
  • 为什么朴素的服务方式浪费内存
  • 什么是 vLLM
  • 核心思想:PagedAttention
  • PagedAttention 如何共享内存
  • Continuous batching
  • 兼容 OpenAI 的 API 服务器
  • vLLM 的优势
  • vLLM 在现实世界中的应用

什么是为大语言模型提供服务

在谈论 vLLM 之前,我们必须先理解何为为大语言模型提供服务。

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

为大语言模型提供服务意味着在计算机上运行该模型,以便许多用户可以同时向它发送问题并得到答案。

简单来说,服务部分负责接收用户请求、通过模型运行它们并返回回复。

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

我们可以将流程描绘如下:

为大语言模型提供服务

用户 1  --问题-->  +-----------------+      +-----------------+
用户 2  --问题-->  |  服务引擎        |----->|  模型在 GPU 上   |
用户 3  --问题-->  |  (接收请求       |      | (进行繁重计算,   |
   ...             |   发送回复)      |<-----|  返回回复)       |
用户 N  --问题-->  +-----------------+      +-----------------+
                              |
                              +--回复--> 返回给每个用户

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

现在,这是重要部分。LLM 运行在一种称为 GPU 的特殊芯片上。GPU 是一种强大的处理器,非常擅长执行 LLM 所需的繁重数学计算。GPU 价格昂贵,且内存容量有限。因此,如果我们浪费 GPU 内存,能够服务的用户数量就会减少,成本也会上升。

因此,服务的核心就是:我们希望在单个 GPU 上尽可能多地服务用户,并且尽可能快。牢记这个目标,因为 vLLM 正是为达成这个目标而构建的。

快速回顾 prefill、decode 和 KV cache

为了理解 vLLM,我们必须了解一些关于 LLM 如何生成答案的知识。别担心,我们会保持简单。

当我们发送一个 prompt 时,模型不会将其作为完整单词读取。它首先将文本分解成称为 token 的小块。一个 token 是一小块文本,大致相当于一个单词或单词的一部分。因此,prompt 变成了一个 token 列表。

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

第一个阶段是 prefill。这是模型读取我们整个 prompt 并处理每一个 token 的阶段,甚至在写出回复的第一个单词之前。简单来说,prefill 是模型读取并消化我们整个 prompt 的过程。

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

现在,在这两个阶段中,对于每个 token,模型都会计算一些内部值并将它们存储起来。这些存储的值保存在称为 KV cache 的东西中。

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

这就是 KV cache 如此重要的原因。在 decode 期间写入每个新 token 时,模型需要之前每个 token 的笔记。如果没有 KV cache,模型将不得不为每个新词重新计算所有那些笔记,那将会非常慢。因此,模型一次性存储这些笔记并重复使用它们。KV cache 正是使生成长答案变得快速的原因。

以下是需要记住的关键点:

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

我们可以将两个阶段和不断增长的 KV cache 描绘如下:

PREFILL 然后 DECODE:每个 token 添加一组笔记,KV cache 增长

prompt tokens:  [ Tell ][ me ][ a ][ joke ]
                   |      |     |     |
PREFILL 写入:   [n]    [n]   [n]   [n]      (每个 prompt token 一组笔记)

所以 KV cache =   [n][n][n][n]                  (prefill 后有 4 组笔记)

DECODE 步骤 1:  写入 "Why"      KV cache: [n][n][n][n][n]
DECODE 步骤 2:  写入 "did"      KV cache: [n][n][n][n][n][n]
DECODE 步骤 3:  写入 "the"      KV cache: [n][n][n][n][n][n][n]
                   ...                          (每一步增加一组)

在这里,我们可以注意到 prefill 一次性为每个 prompt token 创建一组笔记。然后 decode 逐 token 写出回复,每个新 token 都会向 KV cache 添加一组笔记。因此,答案越长,GPU 内存中的 KV cache 就越大。

这就是我们所需的基础。现在我们已经准备好看到真正的问题了。

问题:KV cache 消耗 GPU 内存

既然我们已经了解了什么是 KV cache,让我们看看它带来的麻烦。

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

因此,KV cache 是决定我们一次能服务多少用户的因素。我们拥有的空闲 KV cache 内存越多,就越能并行处理更多请求。

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

因此,服务 LLM 的真正瓶颈不在于数学计算速度,而在于 KV cache 内存。

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

那么,下一个问题是,朴素的方法是如何管理这些内存的,以及它为什么不好?让我们来看看。

为什么朴素的服务方式浪费内存

让我们了解一个简单、朴素的服务引擎如何处理 KV cache,以及它在哪方面出了错。

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

假设模型最多可以生成 2000 个 token。对于每一个请求,朴素引擎立即为 2000 个 token 的 KV cache 预留空间,甚至在模型写出任何内容之前。

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

这种浪费有两种表现形式,我们必须理解两者。

第一种是过度预留(over-reservation)。这意味着我们预留的内存远远超过请求实际使用的量。预留但未使用的空间不能分配给其他任何人,因此被浪费。

第二种是碎片化(fragmentation)。碎片化意味着空闲内存被分成许多分散的小块,我们无法使用。让我们用一张图来理解。

我们可以将朴素的方法描绘如下:

朴素服务:每个请求预留一个大的连续块

请求 A: [#### used (50) ............... 浪费,预留了 2000 ..............]
请求 B: [###### used (120) ............ 浪费,预留了 2000 ..............]
请求 C: [## used (20) ................. 浪费,预留了 2000 ..............]

剩余空闲内存:分散的小间隙  ->  无法容纳新请求

在这里,我们可以看到每个请求都抓住了一个巨大的连续块,但只使用了前端的很小一部分。其余部分被浪费。块之间留下的小间隙太小且太分散,无法容纳新请求。因此,尽管技术上有很多空闲内存,但我们无法使用它。这就是碎片化。

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

于是,vLLM 出现了。

什么是 vLLM

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

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

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

让我们理解一下“吞吐量”(throughput)这个词,因为它就包含在“高吞吐量”这个名称中。吞吐量意味着我们在给定时间内完成多少工作。高吞吐量意味着我们每秒服务大量 token 和请求。这正是 vLLM 旨在最大化的目标。

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

  • PagedAttention,它将 KV cache 管理成小的固定大小的块,而不是一个巨大的块,因此没有内存被浪费。
  • Continuous batching,它通过在每个步骤中将完成的请求换出并将新请求换入,使 GPU 保持忙碌。

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

PagedAttention,核心思想

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

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

简单来说,vLLM 不为每个请求抓取一个巨大的内存块,而是仅在真正需要时才以相等大小的块提供内存。

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

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

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

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

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

之后:答案不断增长,vLLM 根据需要一次只分发一个块。当答案完成时,vLLM 立即释放该请求的所有块,这些块立即可供其他请求使用。

我们可以用简单的图来描绘:

PAGED ATTENTION:KV cache 被分割成小的固定大小的块

GPU 内存:  [B1][B2][B3][B4][B5][B6][B7][B8][B9] ... (一组大小相等的块)

请求 A 的块表  ->  B1, B4, B7      (3 个块,按需分配)
请求 B 的块表  ->  B2, B3          (2 个块,按需分配)
请求 C 的块表  ->  B5              (1 个块,刚启动)

可分配的空闲块:B6, B8, B9

在这里,我们可以看到内存是一个共享的块池,所有块大小相等。每个请求只获得它实际需要的块,并且这些块可以分散在池中的任何位置。块表是一个小地图,将请求与其块按正确顺序关联起来。当请求完成时,它的块立即回到空闲池中,供下一个请求使用。

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

PagedAttention 如何共享内存

PagedAttention 还给我们带来了另一个美妙之处,而且一旦我们有块,它就自然而然地出现了。这就是共享。

由于 KV cache 现在由小块组成,两个不同的请求可以让它们的块表指向内存中完全相同的块,而不是每个都保留自己的副本。这意味着它们共享相同部分的副本。

让我们通过两个实际案例来理解共享会带来很大帮助。

第一个案例是相同前缀(identical prefixes)。前缀是某事物的起始部分。假设许多用户发送的请求都以长的系统指令开头,例如“你是一名汽车经销商的礼貌客户支持代理”。这个长的开头对每个人都是相同的。使用块,vLLM 可以一次性存储该共享开头的 KV cache,并让每个请求指向那些相同的块。我们不会多次存储相同的笔记。我们只存储一次并共享。我们有一篇关于 prompt caching 的详细博客,解释了如何为相同前缀重用 KV cache。

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

我们可以将共享描绘如下:

使用块进行共享

共享开头:   [B1][B2]   <- 内存中只有一个副本,所有使用
                       |
        +--------------+--------------+
        |              |              |
   请求 A          请求 B          束 C
   添加 [B5]       添加 [B6]       添加 [B7]

在这里,我们可以看到块 B1 和 B2 保存了相同的开头,并且只存储一次。三个不同的路径都指向共享部分的这两个相同块,并且每个路径只为其独特的部分添加自己的单独块。我们节省了将该开头存储三次所需的内存。

这就是 PagedAttention 不仅阻止浪费,而且让请求共享内存的方式,从而在同一个 GPU 上装入更多用户。

Continuous batching

现在,让我们学习 vLLM 中的第二个重要概念,它与 PagedAttention 协同工作。

为了理解它,我们首先需要理解批处理(batching)。批处理意味着一次性运行许多请求,而不是一个一个地运行。GPU 在处理多个请求时效率更高,因此批处理是我们保持 GPU 忙碌并获得高吞吐量的方法。

但是朴素的批处理方式有一个问题。让我们来看看。

朴素的方式称为静态批处理(static batching)。在静态批处理中,我们收集一批请求,一起运行它们,并且必须等待批次中的每个请求都完成,才能开始下一个批次。

问题是:不同请求产生的答案长度差异很大。一个用户的答案有 20 个 token,而另一个有 800 个 token。在静态批处理中,短请求早早完成,然后就闲置在那里等待长请求完成,因为整个批次是一起移动的。在等待期间,已完成请求的 GPU 插槽什么也不做。这是 GPU 时间的浪费。

于是,continuous batching 出现了。

Continuous batching 在每个步骤将完成的请求换出,并将新的等待请求换入,而不是等待整个批次完成。

简单来说,一旦一个请求完成,vLLM 就移除它,并立即从等待队列中拉入一个新请求来占据它的位置。GPU 永远不会闲置等待。

请记住,decode 是逐 token 生成答案的,因此有许多小步骤。在每个步骤,vLLM 检查:是否有请求刚刚完成?如果有,就丢弃该请求并添加一个新请求。批次始终保持活跃工作满载。

让我们通过下面两个方式的对比图来比较:

静态批处理(朴素):整个批次等待最慢的一个

步骤:   1    2    3    4    5    6    7    8
请求 A:  X    X    X    done -    -    -    -     <- 闲置,浪费插槽
请求 B:  X    X    X    X    X    X    X    done

CONTINUOUS BATCHING (vLLM): 完成的插槽立即被重新填充

步骤:   1    2    3    4    5    6    7    8
插槽1:  A    A    A    C    C    C    D    D     <- A 完成,C 跳入,然后 D
插槽2:  B    B    B    B    B    B    B    done

在这里,我们可以看到在静态批处理中,请求 A 早早完成,但它的插槽一直闲置直到慢请求 B 完成。而在 continuous batching 中,A 一完成,请求 C 就跳入该插槽,当 C 完成时,请求 D 跳入。GPU 始终保持忙碌。没有插槽被浪费。

Continuous batching 和 PagedAttention 完美契合。PagedAttention 立即释放已完成请求的块,而 continuous batching 立即使用释放的内存和插槽来服务新的等待请求。它们共同使 GPU 内存和 GPU 计算都得到充分利用。

这就是 vLLM 如何使 GPU 全速工作。

兼容 OpenAI 的 API 服务器

现在,让我们看看如何实际使用 vLLM。

vLLM 暴露了一个兼容 OpenAI 的 API 服务器。

vLLM 可以作为一个服务器运行,监听聊天请求并返回模型的回复。

这一点非常重要,因为大量的工具和应用程序已经编写成与 OpenAI 的 API 通信。如果 vLLM 使用相同的语言,那么我们只需更改地址就可以将那些现有工具指向我们自己的 vLLM 服务器,而无需重写代码。我们可以在自己的 GPU 上运行自己的模型,应用程序以与和 OpenAI 通信相同的方式与它通信。

因此,vLLM 在内部给我们提供了一个高吞吐量的引擎,在外部提供了一个熟悉且易于使用的 API。这是我们必须欣赏的部分,因为它使得 vLLM 在实际项目中非常容易采用。

vLLM 的优势

让我们快速总结一下优势,因为这是我们使用 vLLM 的原因。

  • 更高的吞吐量。由于 PagedAttention 避免了内存浪费,continuous batching 避免了 GPU 时间浪费,vLLM 每秒可以服务的 token 和用户数量远多于朴素引擎。
  • 更好的 GPU 利用率。利用率指我们实际使用 GPU 的程度。vLLM 使 GPU 内存和 GPU 计算都接近完全使用,因此我们从昂贵的硬件中获得更多价值。
  • 每个请求的成本更低。因为一个 GPU 现在服务更多用户,分摊到每个用户的成本大大降低。
  • 易于采用。兼容 OpenAI 的 API 意味着我们只需很少的更改就可以将 vLLM 插入现有的应用程序。

简单来说,vLLM 让我们在同样的 GPU 上服务更多用户,更快、更便宜,而且不改变答案质量。模型仍然产生相同的回复。vLLM 只是停止了内存和时间的浪费。

这就是 vLLM 的美妙之处。

vLLM 在现实世界中的应用

现在,让我们看看 vLLM 在真实系统中的使用情况。

vLLM 是最流行的开源服务引擎之一,被希望在自有 GPU 上运行开源大语言模型的公司广泛使用。在任何需要同时为许多用户提供服务模型的地方,vLLM 都是一个强有力的选择。

它在两种系统中尤其强大。

第一种是高流量聊天应用。当许多用户同时聊天时,continuous batching 使每个 GPU 插槽保持满载,PagedAttention 将许多用户的 KV cache 装入同一内存。因此,我们用更少的 GPU 服务大量人群。

第二种是 agent 系统。Agent 是一种逐步执行任务的 AI 程序,通常调用工具并经过多轮交互来完成工作。Agent 在每一步都发送相同的一大块指令,因此 PagedAttention 中相同前缀的共享可以节省大量内存,而 continuous batching 使许多短步骤流畅运行,没有空闲时间。

因此,在任何需要高效且廉价地为许多用户服务 LLM 的地方,vLLM 都能极大地帮助我们。

这就是 vLLM 的工作原理。它将 KV cache 视为操作系统管理内存的方式,按需分配小的固定大小的块,并通过 PagedAttention 在请求之间共享它们;它通过 continuous batching 在每个步骤将请求换入换出,使 GPU 始终保持忙碌;并且它将这一切包装在熟悉的、兼容 OpenAI 的 API 之后,使得我们在相同的硬件上获得更高的吞吐量和更好的 GPU 利用率。

  • 原文链接: x.com/amitiitbhu/status/...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论