想象你经营着一家专做“多道菜宴席”的餐厅:客人一坐下,后厨就要一道接一道地上菜,而且每一道新菜都必须“回味”之前所有菜的味道,才能决定这道菜怎么做。大模型生成文字就是这个过程——它一次只能吐一个词(token),但每吐一个新词,都要“回头”参考之前的所有词。如果每上一道菜都把前面的菜全部重做一遍,厨师再能干也撑不住。聪明的主厨会为每道菜留下一张“配方卡”——记录这道菜的关键用料和成品模样,下次上菜只需瞄一眼卡片。在 AI 世界里,这些配方卡就叫 KV Cache(键值缓存)。 可是配方卡越攒越多,厨房的储物柜(显存)很快就满了。传统做法是每个客人一进门就预留一整排柜子,结果许多柜子空着,新客人又挤不进来——这就是“碎片化浪费”。而新一代的 PagedAttention(分页注意力) 像一位训练有素的仓库管理员:把所有配方卡装进统一规格的收纳盒,见缝插针散放在各个空位,再用一张索引卡记住每个盒子在哪里。于是同一间厨房能服务更多客人、接更长的菜单。本文将带你走进这座厨房,看懂显存背后的收纳智慧。

岗位关联

掌握 KV Cache 与 PagedAttention,你就能精准估算推理显存、消除碎片浪费,把一张 GPU 的吞吐压榨到极致——这是大模型部署工程师必须拿下的核心硬技能。

目录
  1. 可视化演示
  2. 自回归推理
  3. KV Cache 原理
  4. 内存占用模型
  5. 碎片化问题
  6. 分页机制
  7. vLLM 配置
  8. 部署效果
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

本节配有交互动效(进入视口后自动加载,约 3KB)。

自回归推理中KV Cache的增长过程 GPU显存 (24GB) 每个token生成后追加K/V向量到缓存 KV Cache计算公式 Size = 2 x L x S x H x dtype L=层数,S=序列长度,H=头维度 KV Cache与PagedA...

自回归推理

宴席开始上菜了。这家餐厅有个铁规矩:菜必须一道一道上,绝不允许一次性把整桌菜端齐。更麻烦的是,主厨每做一道新菜,都要把前面每一道菜的味道放进嘴里“回味”一遍——酱油放多少、辣度几何、客人哪道菜动过筷子,全部要参考。大模型的“自回归推理”正是如此:模型一次只输出一个 token,而每输出一个新的 token,都要和之前所有 token 做一次“注意力计算”,就像新菜要重新翻阅全部旧菜。

这种“翻阅”在数学上叫 Self-Attention(自注意力)。假设现在已经生成了 \(T\) 个词,那么新词的计算需要同时看到这 \(T\) 个词各自的三张“卡片”:Q(查询)、K(键)、V(值)。Q 是新词自己的“问题清单”,K 是每个旧词的“标签”,V 是每个旧词的“内容档案”。新词拿自己的 Q 去和所有旧词的 K 匹配,匹配度越高,就越该参考该词的 V。

Attention(Q,K,V)=softmax(QK⊤dk)V\displaystyle \text{Attention}(Q,K,V)=\text{softmax}\left(\frac{QK^{\top}}{\sqrt{d_k}}\right)V

1. 三个输入的角色:Q 来自当前正在生成的新词,K 和 V 来自包括新词在内的全部 \(T\) 个位置;\(Q,K,V\in\mathbb{R}^{T\times d_k}\)。

Q∈RT×dk,K,V∈RT×dkQ \in \mathbb{R}^{T\times d_k},\quad K,V \in \mathbb{R}^{T\times d_k}

2. 点积打分:用新词的 Q 与每个旧词的 K 做点积,得到“这道旧菜值不值得细看”的相似度分数。

QK⊤QK^{\top}

3. 缩放防溢出:除以 \(\sqrt{d_k}\),防止向量维度大时分数爆炸,像厨师把火候调小避免炒糊。

QK⊤dk\frac{QK^{\top}}{\sqrt{d_k}}

4. 归一化成权重:softmax 把所有分数压成总和为 1 的权重,表示每道旧菜该被参考多少。

softmax(⋅)\text{softmax}(\cdot)

5. 加权汇总:按权重把各旧位置的 V 加权求和,得到融合了全部历史信息的新词表示。

×V\times V

问题来了:每上一道新菜就“回味”一遍所有旧菜,如果没有留下任何记录,就得把旧菜全部重做一次才能回味——第 \(T\) 道菜要重做 \(T-1\) 道旧菜,整桌菜下来工作量是平方级爆炸。宴席越到后面,后厨就越绝望。聪明的厨师当然不会这么干,下一节我们看看他留下的“配方卡”。

KV Cache 原理

主厨想出了一个办法:每做完一道菜,就立刻把这道菜的“配方卡”留下——卡片正面写 K(风味标签:咸甜、香料、火候关键词),背面贴 V(成品照片:这道菜最终的样子)。有了配方卡,之后做任何新菜想“回味”旧菜,只需要扫一眼卡片,而不必把旧菜重新端回厨房重做。在 Transformer 里,这个“边做边留卡”的动作,就是 KV Cache(键值缓存):把已经计算出的每个 token 的 K 向量和 V 向量暂存在显存里。

具体流程分两个阶段。预填充阶段(Prefill):用户输入一整段提示词,模型并行算出每个 token 的 K/V,并全部写入缓存;解码阶段(Decode):每生成一个新 token,只计算它自己的 Q,用它和缓存中所有旧 token 的 K 做注意力,最后把这个新 token 的 K/V 追加进缓存。这样一来,计算量从“每一步重算全部历史”降为“每一步只算一个新位置”——就像 10 位客人每人点 20 道菜,主厨不再疯狂重做,只是不停地往配方卡柜子里添新卡。

KV Cachet={(k1,v1),(k2,v2),…,(kt,vt)},ki,vi∈Rdmodel\displaystyle \text{KV Cache}_t = \left\{(k_1,v_1),(k_2,v_2),\dots,(k_t,v_t)\right\},\quad k_i,v_i\in\mathbb{R}^{d_{\text{model}}}

1. 下标 \(i\) 遍历历史:\(i\) 从 1 数到当前已生成的 token 数 \(t\),每生成一个 token 就多一张卡。

i=1,2,…,ti=1,2,\dots,t

2. 每张卡的内容:第 \(i\) 个 token 的 K 向量和 V 向量,维度都是模型的隐藏维度 \(d_{\text{model}}\)。

ki∈Rdmodel,vi∈Rdmodelk_i\in\mathbb{R}^{d_{\text{model}}},\quad v_i\in\mathbb{R}^{d_{\text{model}}}

3. 缓存的整体形态:这是一个不断变长的集合,每步生成后追加一条,容量随序列长度线性增长。

KV Cachet=KV Cachet−1∪{(kt,vt)}\text{KV Cache}_t = \text{KV Cache}_{t-1}\cup\{(k_t,v_t)\}

不过天下没有免费的午餐。配方卡柜子也是要占地方的:每个 token 的 K/V 卡在每一层都要存一份,层数越多、词越长、同时就餐的客人(批大小)越多,柜子就膨胀得越快。这给我们提出了一个必须精确回答的问题:显存到底会被吃掉多少?

内存占用模型

现在我们来当一次“厨房面积规划师”:要给配方卡柜子预留多大的空间?把各因素摆上桌:模型有 \(L\) 层(每层都要存一套 K/V 卡)、每张卡是 \(d_{\text{model}}\) 维的向量、当前序列长 \(S\)、同时服务的请求数(批大小)为 \(B\)。K 一份、V 一份,所以先乘 2;现代显卡惯用 FP16 半精度存储,每个数值占 2 字节,再乘一个 2。于是:

M=2×L×dmodel×S×B×2 字节=4×L×dmodel×S×B 字节\displaystyle M = 2 \times L \times d_{\text{model}} \times S \times B \times 2\ \text{字节} = 4 \times L \times d_{\text{model}} \times S \times B\ \text{字节}

1. 两份向量:每个 token 每层都要缓存 K 和 V 两份向量,所以先乘 2。

×2\times 2

2. 层数 \(L\) 与维度 \(d_{\text{model}}\):模型越深、每层向量越宽,配方卡就越多,这是模型结构决定的常数。

×L×dmodel\times L \times d_{\text{model}}

3. 序列长度 \(S\) 与批大小 \(B\):每个请求各自的词都在累积,同时处理的请求越多,总卡数成倍增长。

×S×B\times S \times B

4. FP16 每元素 2 字节:半精度存储的每个数值占 2 字节,最终单位是字节。

×2 字节\times 2\ \text{字节}

把这个公式套到现实中会非常震撼。以 LLaMA-7B 为例:\(L=32\) 层、维度 \(d=4096\),当序列长度 \(S=2048\)、批大小 \(B=16\) 时,KV Cache 占用 \(2\times32\times4096\times2048\times16\times2\approx17.2\) GB——而模型权重本身只有约 14 GB!也就是说,长序列 + 大批量时,配方卡柜子比菜谱本(权重)还要占地方。部署工程师必须养成“先算 4LHSB,再定方案”的职业习惯。

碎片化问题

如果柜子规划得不好,再大的厨房也会“看起来满满当当,实际没装多少”。传统的推理服务采用 预留式分配(pre-allocation):每个请求一进来,就按系统允许的最大序列长度给它划一整排柜子。比如系统上限是 2048 个 token,那就给每个请求预留 2048 格。可实际情况是,大部分请求聊几句就结束了,可能只用掉 300 格。预留 2048 格、实际用 300 格,其余 1700 格全在吃灰——这就是 内部碎片(internal fragmentation)。

更糟的是 外部碎片(external fragmentation):请求结束后柜子被回收,但各个请求长度不同,回收出来的空隙长短不一、东一块西一块。新请求想要一段连续的大空间,却怎么也拼不齐;而总空闲量明明够用。就像几个客人拼桌,剩下的座位全是零散单座,坐不下一个 6 人家庭。这种低效可以用利用率来衡量:

η=∑i=1NsiN×smax⁡×100%\displaystyle \eta=\frac{\sum_{i=1}^{N} s_i}{N \times s_{\max}}\times 100\%

1. 分子:实际使用的总量:\(s_i\) 是第 \(i\) 个请求实际用掉的缓存格数,把所有 \(N\) 个请求加总。

∑i=1Nsi\sum_{i=1}^{N} s_i

2. 分母:预留出来的总量:每个请求都按最大长度 \(s_{\max}\) 预留,\(N\) 个请求共预留 \(N\times s_{\max}\) 格。

N×smax⁡N \times s_{\max}

3. 比值 \(\eta\):实际用量占预留用量的百分比,预留越狠、实际越短,\(\eta\) 越低。

η=实际预留\eta = \frac{\text{实际}}{\text{预留}}

不少线上服务实测发现,\(\eta\) 常常只有 40%~60%——意味着近一半显存在白白浪费。碎片像厨房里被遗忘的死角,谁都不想要,但传统分配方式注定会产生。要根治,就不能再“一进门就预留一整排”,而要把收纳方式彻底换掉。

分页机制

新来的仓库管理员推翻了“每人一整排柜子”的旧规矩,改为“统一收纳盒 + 散放”:所有配方卡(K/V)都装进规格完全相同的收纳盒,一个盒子固定装 16 个 token 的 K/V。盒子可以放在任何空位上——柜子 A 的第三格、柜子 B 的第一格,完全不用连续。管理员手里有一张“索引卡”(block table),记录着“逻辑盒号 → 物理位置”的对应关系。这就是 PagedAttention(分页注意力机制),它把操作系统虚拟内存的分页思想搬进了显存管理。

解码时,当前 token 的 Q 只需按照索引卡,去读取本请求已占用的那几张物理盒即可;生成完继续装盒,盒子满了就再申请一个空闲盒,填到哪算哪。因为盒子规格统一、随用随取,房间里再也不会出现“凑不齐的整排座位”——这正是虚拟内存页表的核心逻辑:逻辑上连续,物理上散落,由映射表兜底。

ℓi↦pi,0≤i<⌈SBsize⌉\displaystyle \ell_i \mapsto p_i,\qquad 0 \le i < \left\lceil\frac{S}{B_{\text{size}}}\right\rceil

1. 逻辑块号 \(\ell_i\):第 \(i\) 个逻辑块,按序列顺序编号,从 0 到总块数减 1。

ℓi,i=0,1,2,…\ell_i,\quad i=0,1,2,\dots

2. 总块数:序列长度 \(S\) 除以每块容量 \(B_{\text{size}}\)(如 16),并向上取整——多一个 token 也要多占一整盒。

⌈SBsize⌉\left\lceil\frac{S}{B_{\text{size}}}\right\rceil

3. 映射 \( \mapsto \) 到物理块号 \(p_i\):块表把每个逻辑块指向物理内存里的任意空闲物理块,物理块不必相邻。

ℓi↦pi\ell_i \mapsto p_i

分页还有个隐藏彩蛋:前缀共享。如果多个请求(如多人问同一份文档、beam search 的多个分支)拥有相同的前缀,它们可以共享同一个物理块,只需在块表里给该块加引用计数——相当于多位客人共享同一批“前菜配方卡”,省下的显存相当可观。管理员不必再担心碎片,要担心的只剩一件事:厨房总面积到底划多少给收纳盒。

vLLM 配置

这位聪明的仓库管理员,在开源世界里有个响亮的名字——vLLM。它把上面这套分页思想做成了生产级系统。部署工程师要做的,就是给管理员定好几条“厨房规矩”。第一条规矩:收纳盒容量(block_size),vLLM 默认每个盒装 16 个 token 的 K/V。盒子越小越省空间但索引开销大,盒子越大访存越高效但碎得更细,默认 16 是经过权衡的甜点值。第二条规矩:显存利用率(gpu_memory_utilization),默认 0.9,意思是把 90% 的显存交给 vLLM 统一支配(包括权重、激活和 KV 块池),留 10% 作为缓冲。

第三条规矩藏在调度里:连续批处理(continuous batching)。传统“动态批处理”是一桌客人全部吃完才让下一批入座,只要有一个人慢,整桌人陪着等;连续批处理则是谁吃完谁走,空位立刻让新客人坐下——翻台率大幅提升。合理配置这些参数前,先要算清有多少显存能留给 KV 盒:

MKV=MGPU×u−Mweights−Mactivations−Mreserve\displaystyle M_{KV} = M_{GPU} \times u - M_{\text{weights}} - M_{\text{activations}} - M_{\text{reserve}}

1. 工作区总预算:显卡总显存 \(M_{GPU}\) 乘以利用率 \(u\)(如 0.9),这是管理员可支配的总面积。

MGPU×uM_{GPU} \times u

2. 减去权重和激活:模型权重 \(M_{\text{weights}}\) 和前向计算的中间激活 \(M_{\text{activations}}\) 必须优先保证。

−Mweights−Mactivations- M_{\text{weights}} - M_{\text{activations}}

3. 保留安全垫:减去 \(M_{\text{reserve}}\) 的预留缓冲,防止瞬时峰值导致 OOM,剩余的就是 KV 块池。

−Mreserve- M_{\text{reserve}}

配置时还要盯住监控指标:num_kv_blocks(空闲块数)、gpu_cache_usage_perc(KV 池占用率)等,让管理员始终有“余盒可用”。此外 enable_prefix_caching 能自动复用重复前缀,量化权重则能压缩权重占用从而给 KV 腾出更多空间——一切都围绕“用最少的浪费服务最多的请求”。

部署效果

搬进新厨房后,效果立竿见影。先看吞吐:想象原来一晚上只能接待 8 桌客人,现在同样的灶台、同样的储物面积,却能接待 20 桌——因为不再有人占着整排柜子却只吃两三道菜。PagedAttention 把显存利用率从约 50% 拉高到 90% 以上,配合连续批处理,线上实测吞吐往往能提升 2~10 倍。再看长序列:以前 2048 token 就到头了,因为预留 8192 的柜子太奢侈;现在盒子按需增长,轻松支持 32K、128K 的长文档问答。

Throughput=∑j=1RNjTtotal\displaystyle \text{Throughput}=\frac{\sum_{j=1}^{R} N_j}{T_{\text{total}}}

1. 分子:生成的总 token 数:\(N_j\) 是第 \(j\) 个请求最终生成的 token 数,跨 \(R\) 个请求求和。

∑j=1RNj\sum_{j=1}^{R} N_j

2. 分母:总耗时:\(T_{\text{total}}\) 包含排队、预填充和解码的端到端时间。

TtotalT_{\text{total}}

3. 吞吐量:单位时间产出的 token 数,是衡量推理服务性价比的核心指标,分页管理直接提升这个数。

Throughput=总 tokens总时间\text{Throughput} = \frac{\text{总 tokens}}{\text{总时间}}

回望整个故事:从“每道菜重做”到“配方卡”,从“整排预留”到“统一收纳盒”,大模型推理的显存管理完成了一次厨房革命。今天,vLLM、SGLang 等主流推理框架都已内置分页缓存;理解 4LHSB 的计算、碎片化的成因和块表的映射,你就掌握了这座现代化厨房的核心图纸——无论面对多长的序列、多大的并发,都能从容规划、精准扩容。

AI 应用场景

高并发在线对话机器人的吞吐瓶颈

阶段:部署推理(Serving)

解决的问题:传统推理服务在几十路并发下频繁 OOM——每个会话都按最大长度预留 KV 显存,空闲时浪费、高峰时崩溃。使用 vLLM 的 PagedAttention 后,KV 按需分配成 16 token 的小块,碎片几乎归零,单卡能稳定承载数百路会话。

具体案例:某客服公司用 Qwen2-7B 部署了 200 路并发对话,原方案在 64 路时便显存溢出;接入 vLLM 并设置 gpu_memory_utilization=0.9 后,吞吐提升约 4 倍,P95 首字延迟保持在 500ms 以内。

长文档智能问答的超长上下文支持

阶段:长序列推理(Long-context Inference)

解决的问题:阅读几百页合同或论文时,序列长度动辄上万 token。传统预留式缓存会让显存瞬间爆掉;PagedAttention 按需增长 + 前缀共享,让同一文档的前缀块被多个提问者复用。

具体案例:法务团队用 32K 上下文的模型审阅尽调报告,部署时开启 enable_prefix_caching,多轮追问共享文档前缀块,显存占用比逐个独立推理降低约 60%,单张 A100 即可支撑 5 名律师并行提问。

小显存显卡上的低成本微服务

阶段:资源规划与容量评估

解决的问题:创业团队只有一张 24GB 的 3090,既要跑 7B 模型权重又要服务多路用户。先用公式 \(4LHSB\) 估算 KV 需求,再用连续批处理让请求“吃完就走”,把单卡的接待能力用到极致。

具体案例:某 AI 写作插件团队将 LLaMA-7B 部署在单张 3090 上,通过 vLLM 分页缓存把并发路数从 12 路提升到 48 路,月成本节约 70%,用户平均等待时间反而缩短了三成。

Python 演示

python68 行
# -*- coding: utf-8 -*-"""分页KV缓存与朴素预留在显存利用率上的对比模拟"""import random  class BlockAllocator:    """把显存看作固定大小的物理块池, 按需分配与回收"""     def __init__(self, total_blocks):        self.total_blocks = total_blocks        self.free_blocks = set(range(total_blocks))     def allocate(self, num_blocks):        """申请 num_blocks 个物理块, 可以来自任意散落位置"""        blocks = []        for _ in range(num_blocks):            if not self.free_blocks:                raise MemoryError("显存不足")            blocks.append(self.free_blocks.pop())        return blocks     def free(self, blocks):        for b in blocks:            self.free_blocks.add(b)     def usage(self):        return self.total_blocks - len(self.free_blocks)  def naive_reserve(total_cells, requests, max_len):    """朴素方案: 每个请求一进来就预留 max_len 个缓存单元"""    served = used = reserved = 0    for length in requests:        if reserved + max_len > total_cells:            break        reserved += max_len        used += length        served += 1    return served, used, reserved  def paged_serve(total_cells, requests, block_size):    """分页方案: 按实际长度向上取整申请块, 块可散落任意物理位置"""    allocator = BlockAllocator(total_cells // block_size)    served = used_blocks = 0    for length in requests:        need = (length + block_size - 1) // block_size        try:            allocator.allocate(need)            used_blocks += need            served += 1        except MemoryError:            break    return served, used_blocks * block_size, total_cells  def main():    random.seed(42)    requests = [random.randint(8, 28) for _ in range(40)]    total_cells = 192    s1, u1, r1 = naive_reserve(total_cells, requests, 32)    s2, u2, t2 = paged_serve(total_cells, requests, 8)    print("朴素预留: 服务 %d 个请求, 利用率 %.1f%%" % (s1, u1 * 100.0 / r1))    print("分页管理: 服务 %d 个请求, 利用率 %.1f%%" % (s2, u2 * 100.0 / t2))  if __name__ == "__main__":    main()
目的:用 192 个缓存单元模拟 40 个随机长度请求,对比“每人预留 32 格”与“8 格一盒分页散放”两种策略能服务的请求数和显存利用率。
  1. self.free_blocks = set(range(total_blocks))初始化空闲物理块集合,模拟一整片可散落分配的显存。
  2. blocks.append(self.free_blocks.pop())每次从集合中随机取一个空闲块,体现“物理块可以不相邻”。
  3. if reserved + max_len > total_cells: break朴素方案累积预留空间,空间不够就停止接待,造成大量浪费。
  4. need = (length + block_size - 1) // block_size按实际长度向上取整算出需要的块数,多一个 token 也要多占一盒。
  5. allocator.allocate(need)分页方案只按需申请,申请失败才停止,因此能接待更多请求。
  6. used_blocks * block_size分页方案的实际占用是“块数 × 块容量”,块内若有空余属于块级粒度损耗,但远小于预留损耗。

常见误区

误区既然 KV Cache 只是保存历史 token 的向量,放哪里不都一样吗?放 CPU 内存还能省显存,反正顺序生成,慢慢搬呗。我见过有人把 KV Cache 挂到 CPU 上,觉得“显存不够就借内存”。

正解解码阶段每一步都要用全部历史 K/V 做注意力计算,属于高频率、高带宽访问。如果在 CPU 内存,每一步都要先 PCIe 搬回 GPU,传输时延会让吞吐崩塌。这就像主厨做菜要“回味”配方卡,结果卡片放在仓库、每做一道菜都要跑去仓库翻一遍——灶台全在空等。

为何常见:看到“缓存”二字就联想到磁盘缓冲、浏览器缓存,忽略了 Transformer 推理中 KV 是每步 decode 的热路径数据。

误区很多人以为 KV Cache 只是“一个序列”的缓存,算显存时只考虑序列长度,把批大小完全忘掉,导致线上并发一高立刻 OOM“猝死”。

正解批大小 \(B\) 与序列长度 \(S\) 是乘法关系:同时服务的每个请求都有自己的 KV Cache,10 个请求就会放大 10 倍。公式 \(4LHSB\) 中 \(S\) 和 \(B\) 是平等相乘的,正如 10 位客人每人都有自己的配方卡柜,而不是共用一张。

为何常见:学习阶段通常用单条样本演示,脑子里“了一条序列”的图景根深蒂固,忽略了真实服务的并发维度。

误区有人觉得分页管理就是把显存物理切成固定大小的小格,跟“给柜子加隔板”没本质区别,换谁来做都一样。

正解分页的精髓是“逻辑连续、物理散落”的块表映射,加上按需分配、引用计数和前缀共享。切片只是手段,块表才是灵魂——它让一个请求的缓存可以散落在任意物理位置,并让多请求共享相同前缀块。没有映射关系,单纯切块无法解决外部碎片。

为何常见:把“分页”直觉理解为“分块”,忽略了虚拟内存页表的映射机制这一核心思想。

误区反正是自家显存,利用率设成 0.99 甚至 1.0,KV 池不就最大了吗?设置越大越“榨干”显卡,不是吗?

正解还要给模型权重、激活、CUDA context 和碎片留空间。利用率设过高,KV 池预分配时就会把显存吃满,一旦权重加载或激活峰值到来直接 OOM。默认 0.9 正是“留好安全垫”的经验值,而不是贪婪的上限。

为何常见:只盯着“KV 池越大越好”的局部目标,忽视了整个显存生态里还有权重与激活这些“固定房租”。

误区一听 7B 模型就以为 14GB 权重是显存大头,推理时只算权重的账,对 KV Cache 的膨胀毫无防备。

正解权重是静态的“菜谱本”,放一次就完事;KV Cache 是动态增长的“配方卡”,随每次对话持续累积。序列 2048、批量 16 时 7B 模型的 KV 能达到约 17GB,超过权重本身。长序列 + 大批量下,KV 才是真正的显存吞金兽。

为何常见:训练时代的“显存 ≈ 模型大小”经验深入人心,忽略了推理阶段动态缓存的线性增长特性。

面试题

Q1. KV Cache 缓存的内容是什么?

单选题 · easy

  • A.每层所有历史位置的 K/V 向量← 正确答案
  • B.每层所有历史位置的 Q 向量
  • C.每层所有模型参数的权重
  • D.每层所有注意力得分矩阵

答案:A。缓存的是已生成历史 token 在各层的 K 和 V 向量,供后续 token 做注意力时反复使用。Q 只需当前 token 现算;权重是静态参数无需“缓存”;注意力矩阵只是中间结果。好比配方卡只留“风味标签”和“成品照片”,不必留“点菜问题”。

Q2. 若不使用 KV Cache,每生成一个新 token 会怎样?

单选题 · mid

  • A.需重新计算全部历史词 KV← 正确答案
  • B.需重新加载全部模型权重
  • C.需重新训练全部模型参数
  • D.需重新采样全部候选词

答案:A。没有缓存,解码第 \(T\) 步就要把前 \(T-1\) 个 token 的注意力全部重算一遍,总开销呈平方级爆炸。权重本来就在显存里,无需“重新加载”;训练和采样都不涉及。这正是配方卡存在的意义——避免每道新菜都重做旧菜。

Q3. 公式 \(M = 4 \times L \times H \times S \times B\) 字节中的系数 4 如何而来?

单选题 · mid

  • A.2 份 K/V 乘 2 字节 FP16← 正确答案
  • B.4 个注意力头的并行度
  • C.每词 4 字节的嵌入总数
  • D.权重占 4 倍显存开销

答案:A。每个 token 每层要缓存 K、V 两份向量(乘 2),FP16 半精度每元素占 2 字节(再乘 2),合起来是 4。注意力头数、词嵌入字节数都与系数来源无关。

Q4. 下列哪些属于显存碎片化问题?

多选题 · mid

  • A.预留缓存远大于实际使用量← 正确答案
  • B.缓存回收后留下零散空隙← 正确答案
  • C.模型权重与激活间的间隙
  • D.缓存块被多个请求瓜分

答案:AB。A 是内部碎片——每请求按最大长度预留,用不满就浪费;B 是外部碎片——回收后空隙长短不一,新请求凑不齐连续空间。权重与激活的间隙属于正常布局;多请求共享块恰是分页想达成的目标,不是问题。

Q5. PagedAttention 中 block table 的作用是什么?

单选题 · hard

  • A.记录逻辑块对应的物理块位置← 正确答案
  • B.存储每个块的平均注意力权重
  • C.压缩每个块的 K/V 向量维度
  • D.保存每个块的哈希签名数值

答案:A。块表是分页的灵魂,把逻辑上连续的块号映射到物理上散落的空闲块,实现“逻辑连续、物理任意”。它不存注意力权重、不压缩向量、也不做主存哈希,而是像仓库的索引卡一样回答“第 i 盒放在哪”。

Q6. vLLM 中默认的 block_size 是多少?

单选题 · easy

  • A.每个块容纳 16 个 token← 正确答案
  • B.每个块容纳 8 个 token
  • C.每个块容纳 32 个 token
  • D.每个块容纳 64 个 token

答案:A。vLLM 默认每个 KV 块装 16 个 token 的 K/V。块太小则块表索引开销大,块太大则块内碎片增多,16 是工程权衡后的默认值。记住“16 个 token 一盒”就记住了默认配置。

Q7. 连续批处理(continuous batching)的核心改进是?

单选题 · mid

  • A.请求完成就腾位并接入新请求← 正确答案
  • B.固定批大小以稳定输出速度
  • C.每步全部请求同时结束
  • D.所有请求共享同一前缀

答案:A。传统批处理像“整桌吃完才翻台”,慢请求拖累全批;连续批处理让完成的请求立刻退出、新请求马上进入,GPU 永不空等。它不要求批大小固定,更不要求请求同步结束,共享前缀是分页的另一项能力。

Q8. PagedAttention 引入的收益有哪些?

多选题 · hard

  • A.消除预留导致的显存碎片← 正确答案
  • B.支持多序列共享前缀块← 正确答案
  • C.减少模型参数的冗余度
  • D.支持超长序列按需分配← 正确答案

答案:ABD。分页按需分配小块,消除了大块预留造成的碎片;引用计数让相同前缀的请求共享物理块;序列多长就申请多少块,不再按上限预留。模型参数量完全没变,C 是干扰项。

Q9. KV Cache 显存大小与哪些因素成正比?

多选题 · mid

  • A.模型层数与隐藏维度← 正确答案
  • B.序列长度与批大小← 正确答案
  • C.学习率与训练轮数
  • D.采样温度与解码方式

答案:AB。公式 \(4LHSB\) 表明 KV 显存只由层数 \(L\)、隐藏维度 \(H\)、序列长度 \(S\)、批大小 \(B\) 决定。训练超参和采样参数只在推理之外起作用,完全不影响缓存占用。

Q10. 缓解 KV 显存压力的正确做法是?

单选题 · hard

  • A.提高 gpu 显存利用率上限
  • B.降低批大小或使用分页缓存← 正确答案
  • C.增加模型层与注意力头数
  • D.提高采样温度放大随机性

答案:B。降低批大小直接按乘法关系缩小 \(B\),分页缓存则消除碎片并支持前缀共享。把利用率调到接近 1.0 反而可能让权重和激活无地容身而 OOM;加深模型只会让 KV 更大;采样温度与显存完全无关。