已学完
💡 一句话解释: 不用缓存时,生成 1000 个字总共要算 50 万次矩阵;有了缓存,只要算 1000 次!
# 没有缓存:O(N^2) 耗时爆炸 | 启用缓存:O(N) 线性平稳
outputs = model(**inputs, past_key_values=past_key_values, use_cache=True)
烦恼:吐第100个字要重读前99个
每写一个字,从头背一遍全书
办法:老词做成书签,只算最新一个
记在小账本,只写新一笔
解决了什么:彻底消灭了生成过程中对历史 Token 的全量重复投影运算,推理速度飙升几十倍。
代价是什么:显存开销随生成长度线性膨胀,长文本生成极容易遭遇 GPU 显存爆炸(OOM)。
已学完
💡 一句话解释: 为什么老词的 K 和 V 能够原封不动复用?因为时间不能倒流,后说的话绝不能影响前面的字!
$$ ext{Attention}(Q, K, V) = ext{softmax}\left( rac{Q K^ op}{\sqrt{d_{ ext{head}}}} imes ext{mask}
ight) V$$
困惑:以后产生的新词会不会改写历史?
若是未来改过去,缓存全报废
办法:下三角单向因果掩码遮蔽未来
单向时间流,历史永不变
解决了什么:因果掩码阻断了未来信息对过去的篡改,奠定了自回归生成可以安全复用历史向量的数学基石。
代价是什么:仅适用于自回归单向语言模型(如 GPT、Llama),掩码语言模型(如 BERT)无法使用此缓存机制。
已学完
💡 一句话解释: 模型有几十个 Transformer 层,不能到处乱塞,官方抽象出标准 `Cache` 基类统一对账!
new_K, new_V = cache.update(k_t, v_t, layer_idx)
# 自动按层(layer_idx)管理 [batch, num_heads, seq_len, head_dim]
烦恼:以前几十层元组到处传参
元组满天飞,代码极难维护
办法:分层抽屉柜,统一 update 存取
标准抽屉柜,按层归档
解决了什么:将以往脆弱杂乱的 `tuple(tuple(tensor))` 重构成可扩展的 `Cache` 对象,解耦底层存储。
代价是什么:打破了早期的元组签名习惯,自定义解码循环时需要主动维护注意力掩码形状。
已学完
💡 一句话解释: 每蹦出一个字,就把新的 K 和 V 往张量尾巴上一贴,像不断拉长的小火车。
from transformers import DynamicCache
past_key_values = DynamicCache() # 随着 token 增加在 seq_len 维度动态 torch.cat
烦恼:每次加车厢都要重搭轨道
反复重新找地盘,效率有损
办法:最灵活开箱即用,自动拼接
自动贴尾巴,最省心简单
解决了什么:无需预先指定生成长度,根据实际生成字数动态弹性分配,内存浪费最小。
代价是什么:张量形状不断变化(Dynamic Shape),完全无法使用 CUDA Graph 和 torch.compile 加速。
已学完
💡 一句话解释: 一开局就预先包下一整排 1024 个固定座位,新字直接按号入座,形状再也不变!
from transformers import StaticCache
# 预先固定最大长度,完美解锁 torch.compile 与 CUDA Graph
cache = StaticCache(config=model.config, max_batch_size=1, max_cache_len=1024, dtype=torch.bfloat16)
烦恼:形状一变,编译器当场崩溃
忽长忽短,硬件图无法固化
办法:开局圈好固定包厢,指针就位
固定包厢,流水线全速狂飙
解决了什么:张量尺寸在生命周期内恒定,完美支持 `torch.compile` 和 CUDA 图录制,吞吐翻倍。
代价是什么:无论实际说几个字,显存必须按 `max_cache_len` 顶格预先占满,短文本会浪费显存。
已学完
💡 一句话解释: 只保留最近 4096 个字的清晰记忆,来了新词就把最古老的老词踢出窗口(金鱼的记忆法)!
# 针对 Mistral / Gemma 等支持局部注意力模型,窗口满了自动左移剔除
cache = StaticSlidingWindowCache(config, max_cache_len=4096, sliding_window=4096)
烦恼:聊了上万句,显存彻底爆仓
什么都不忘,迟早要撑死
办法:固定窗框移动,新来旧退
窗口滑动,显存永远恒定
解决了什么:显存开销固定为 $O(W)$(常数级),再长上下文生成也不会显存溢出。
代价是什么:模型彻底遗忘窗口之外的前文细节,需搭配注意力汇(Attention Sink)防止文本崩溃。
已学完
💡 一句话解释: 原来存一个数要用 16 位的胖箱子(FP16/BF16),压缩成 8 位甚至 4 位的小便签存(INT8/INT4)!
from transformers import QuantoQuantizedCache # 或 HqqQuantizedCache
# 内存直降 50% ~ 75%,原本只能跑 8K 的卡能跑 32K!
烦恼:高精度浮点数塞满显卡
箱子太大,卡车很快装满
办法:无损压缩为 INT8 / INT4 压缩包
真空打包,容量轻松翻几倍
解决了什么:在显存极其紧张的端侧或单卡上,以几乎可忽略的精度损失将 KV Cache 显存砍半甚至变为 1/4。
代价是什么:每次读取与写入时伴随轻微的反量化/量化运算开销。
已学完
💡 一句话解释: 以前 32 个侦探(Q 头)每人都要配一套完整的卷宗(KV),现在每 4 个侦探共享 1 份卷宗!
# Llama-3 / Qwen 等现代模型默认架构:num_key_value_heads = 8, num_attention_heads = 32 (GQA 4:1)
烦恼:32 个头各自存一份纯属浪费
人手一份,严重冗余
办法:一组人共享一份核心档案
合资拼单,效果完全不减
解决了什么:从模型骨架层面将每层需缓存的 KV 头数由 32/64 缩减为 8/4,大幅减轻显存带宽瓶颈。
代价是什么:模型需在预训练阶段按 GQA 结构训练,无法对旧版 MHA 模型无缝直接启用。
已学完
💡 一句话解释: 当你多次对同一个 PDF 提问,或者共用长达数千字的 System Prompt 时,这段前缀只算一次并锁进库里!
# 提示词完全相同的前缀块生成哈希,下次请求直接从缓存提取,首字等待时间(TTFT)暴降 80%
烦恼:换个问题,整篇论文又重读一次
反复重读同一本书,极慢极贵
办法:前缀打上哈希戳,直接秒读快照
熟读成诵,换个问题秒接
解决了什么:跨多轮对话或同文档并发请求时,跳过庞大 System Prompt 的 Prefill 算力消耗。
代价是什么:需要缓存调度器维护前缀树(Radix Tree)并在请求稍有改动时及时置换。
下一站
💡 一句话解释: 当几百个人同时在线聊天时,像操作系统的虚拟内存一样,把显存切成小分页(Page),零碎片极致利用!
# vLLM / SGLang 等现代化推理服务器的底层支柱:彻底告别连续显存预分配
Logical KV Block ➔ Page Table ➔ Physical GPU Pages (显存利用率达 96%+)
烦恼:连续预留显存,到处是空闲碎片
虚占茅坑不拉屎,显存极度浪费
办法:按需分配 16-Token 物理小切片
小块拼图,并发翻三倍
解决了什么:将非连续显存物理块通过页表映射串联,彻底消灭显存碎片,支持超大规模高并发服务。
代价是什么:需要重写底层 GPU 算子(非标准 PyTorch 连续张量原生算子),工程实现极其复杂。