主要关注:
- LLM 的基础原理
- Transformer 的架构
- KVCache
基本来自于我的问题,以及我对通过 chat 学习到的东西的总结,由 AI 整理。完整版我放在腾讯文档中,这里选取一些方便摘录的写成博客。
LLM 的基础理论(AI Infra)
大模型的计算复杂度
Prefill 和 Decode
N 为上下文长度:
- 如果没有 KVCache,O(N^2)
- 如果有 KVCache,O(N)
O(N) 的原因:
- Prefill 阶段,需要拿一个 $N \times d$ 的 Query 矩阵,去乘以一个 $d \times N$ 的 Key 矩阵($d$ 是维度)。这会生成一个 $N \times N$ 的注意力分数矩阵(Attention Map)。这个对应了“为什么当你扔进去一本 10 万字的小说,模型在吐出第一个字之前,会卡顿很久(首字延迟 TTFT 很高)。”
- 当然,这里和 N_head 也有关系。每个 head 是单独算的。
- Decode 阶段,这里是 KVCache 的优化了。
- 没有 KV Cache 的傻瓜做法: 假设模型要吐出第 $N+1$ 个词。如果没缓存,它必须把前面的 $N$ 个词加上新词,重新过一遍 $O((N+1)^2)$ 的计算。这显然极其愚蠢且缓慢。
- 有了 KV Cache 的聪明做法: 因为前 $N$ 个词的 Key 和 Value 已经存在显存里了。对于第 $N+1$ 个词,模型只需要单独为这 1 个新词计算它的 Query(大小为 $1 \times d$)。
- 数学本质: 此时的注意力计算,变成了拿这个 $1 \times d$ 的 Query 向量,去乘以缓存中 $d \times N$ 的 Key 矩阵。
- 复杂度: 向量乘矩阵,计算复杂度瞬间从 $O(N^2)$ 暴降到了 $O(N)$!
两个耗时:
TTFT (Time To First Token - 首字时间): 衡量 Prefill(预填充) 阶段的耗时。即你按下回车,到屏幕上蹦出第一个字等了多久。
- 优势: 它是高度并行的。现代 GPU 非常擅长干这种巨大的矩阵乘法,虽然计算量大,但 GPU 的几万个核心都在同时工作,效率极高。这被称为 Compute-bound(算力瓶颈)。
TPOT (Time Per Output Token - 每输出词元时间): 衡量 Decode(解码) 阶段的速度。即后续的字像吐牙膏一样,每个字蹦出来平均要多久。
- 为什么耗时: 它必须是串行(Sequential)的!因为模型必须等第 1 个字出来,才能去算第 2 个字。在这个阶段,计算量虽然降到了 $O(N)$,但就像我们之前说的,为了算这 1 个新词,GPU 必须把显存里庞大的 KV Cache 重新搬运一遍到计算单元。
- 劣势: 此时 GPU 的算力核心其实大部分时间在“闲置摸鱼”,它在等显存(HBM)把数据一点点搬过来。这被称为 Memory-bound(内存带宽瓶颈)。
场景 A:长输入,短输出(Prefill 耗时长)
场景 B:短输入,长输出(Decode 耗时长)
尽管如此,O(N) 依然是恐怖的:
当 $N$ 达到 1,000,000 时,即便只是单步 $O(N)$ 的计算,对于内存带宽的压力也是极其巨大的。因为你要为了这一个新词,把显存里 100 万个历史 Key 和 Value 全都搬运到计算单元里比对一次。这种现象叫做 Memory Bound(内存带宽瓶颈),此时 GPU 算力可能只发挥了 5%,剩下的时间都在等显存传输数据。
关于注意力头
在阅读大模型论文或源码时,经常会看到两个容易混用的“维度”概念:
- 模型的总维度 ($d_{\text{model}}$): 这是模型在每一层之间传递数据的整体“宽度”。比如一个典型的 13B 模型,它的 $d_{\text{model}}$ 可能是 5120。
- 注意力头的维度 ($d_{\text{head}}$): 因为大模型采用的是“多头机制(Multi-Head)”,它不会拿 5120 这么庞大的向量直接去算,而是把它“切分”给多个不同的“头”去并行计算,让每个头关注不同的特征。
它们的严格数学关系是:
$$
d_{\text{model}} = N_{\text{heads}} \times d_{\text{head}}
$$
例如,把 5120 的总维度分给 40 个头($N_{\text{heads}} = 40$),那么每个头的维度 $d_{\text{head}}$ 就是 128。
PagedAttention
vLLM 是一个用于大语言模型(LLM)的开源高性能推理和服务引擎。vLLM 最具革命性的特性是 PagedAttention 技术:传统方法分配 KV Cache 像住宾馆,必须预留连续的、足够大的房间(连续显存),导致大量显存碎片被浪费。PagedAttention 借鉴了操作系统的虚拟内存机制,将 KV Cache 拆分成一个个极小的“内存页”(Block),按需分配,非连续存放。这几乎消灭了显存碎片,让系统能同时服务的用户数翻了好几倍。
架构层面:
- vLLM 就像是生产线上的“超级组装机”,负责飞速地计算和吐出 Token
- 3FS 则是紧挨着生产线的“超高速立体仓库”,专门负责存放那些流水线上放不下的半成品(KV Cache)。
vLLM 本身自带了前缀缓存(Prefix Caching)功能,但这通常局限在单台服务器内部。3FS 的目的是全局视角的缓存共享。
PagedAttention 是如何实现的,如下其实还是比较传统的:
- 它将每个用户请求的 KV Cache 划分成固定大小的“块”(Block)。每个块不再是以字节(Byte)为单位,而是以 Token 数量为单位(默认通常是 16 个或 32 个 Token)。
- 逻辑与物理分离 + 维护块表(Block Table)
不同点:
- OS 是以 Bytes 为基础。PA 以 Token 为基础。一个 Block 占用多少显存,取决于模型的层数、注意力头数和数据精度。例如,在 Llama-2 13B 模型中,一个 16-Token 的 Block 可能占用几兆字节(MB)的显存。
- 访问模式不同:OS 是随机访问多。PA 是可预测的,大模型的文本生成(自回归)是严格按顺序追加(Append-only)的。模型永远是一个词一个词往后吐,KV Cache 只会顺着时间轴往后写。
- 触发 offloading 的时机不同:OS 就是缺页中断。vLLM 是在应用层(框架层)预测显存够不够。如果不够了,vLLM 会主动把一些 Block 踢到系统内存(Offload),这被称为抢占(Preemption)或重计算(Recomputation),完全由软件逻辑控制。
那么如何利用这个特性?
- 减少碎片
- 消灭内部碎片: 一个物理块一旦分配,模型就会按顺序把它填满。除了当前正在生成的最后那个未填满的 Block,所有已分配的 Block 的空间利用率都是 100%。
- 消灭外部碎片: 因为物理块在显存中是不连续的,只要显存里还有任何一个空闲的 Block,就可以立刻分配给任何请求。这让显存的整体利用率从传统方案的 20%-40% 瞬间飙升到了 90% 以上。
- 由于 PagedAttention 是纯软件层面的控制,它拥有“全局视野”。所以它可以选择重新计算,还是 offload 到 3FS 中。
Hybrid Sparse Attention
DeepSeek(特别是在其最新的 V4 架构中)采用的 Hybrid Sparse Attention,是从算法数学层面下刀,它混合了多种注意力策略来“偷懒”却不掉精度:
- 滑动窗口注意力 (Sliding Window Attention, SWA): 对最近的一小段上下文(比如最近 128 个 Token),采用全量密集注意力,保证局部的绝对精确。
- 压缩稀疏注意力 (Compressed Sparse Attention, CSA): 对远古的历史上下文,它不再让当前 Token 和所有历史 Token 一一比对,而是利用轻量级的“索引器(Indexer)”挑选出最相关的 Top-K 个 Token,只计算这部分极其重要的记忆,其余的直接忽略(稀疏化)。
- 重度压缩注意力 (Heavily Compressed Attention, HCA): 把极其冗长的历史记录,像打“压缩包”一样,将多个 Token 融合压缩成少量的特征向量,提供一个全局视角的模糊记忆。
vLLM 和训推存储底座
vLLM 并没有完全撒手不管“存”。它管理最宝贵、最快的 GPU 显存(HBM)。一旦发现显存快满了,就立刻把暂时不用的 KV Cache 通过超高速网络(如 RDMA)卸载给 3FS。
RDMA(Remote Direct Memory Access,远程直接内存访问)是一种无需计算机操作系统内核接入、无需CPU参与,即可直接在两台机器内存之间读写数据的网络技术。它通过内核旁路(Kernel Bypass)和零拷贝技术,实现了高吞吐量、低延迟和低CPU占用的网络传输,适用于高性能计算和大数据中心。
RDMA 编程通常基于 Verbs API (如 libibverbs) 进行,核心流程是建立连接并交换内存地址信息,随后进行直接读写。
- 内存注册 (Memory Registration, MR): RDMA 必须先将应用内存区域(Buffer)注册到网卡,锁定物理地址,并获取远程访问的键(R_Key)。
- 队列对 (Queue Pair, QP): 每个RDMA应用通过QP通信,包括发送队列(SQ)和接收队列(RQ)。
- 工作请求 (Work Request, WR): 应用通过WR提交发送/接收指令给网卡。
- 完成队列 (Completion Queue, CQ): 网卡完成传输后,将完成结果放入 CQ,应用通过轮询 CQ 获取结果。
这套机制的作用:
- 独立扩容:vLLM 加显卡,3FS 加 SSD
- 无状态的 vLLM
- 多级缓存:VRAM(显存) -> DRAM(内存) -> 3FS(高速网络 SSD)
前缀
关于前缀
- 最佳实践:调整 Prompt 结构(把“死”的放前面)
- 失效做法: [用户闲聊] + [系统指令] + [长财报](用户一换,缓存全崩)。
- 优化做法: [长财报] + [系统指令] + [用户闲聊]。
- 基数树(Radix Tree)管理机制
- 如果你的输入是 A + B(A 是闲聊,B 是财报),它会先找 A 的缓存。
- 虽然它发现 A + B 不能直接复用 C + B 的缓存,但如果系统发现大量请求都有共同的 B,它会尝试在内部进行“多级缓存”匹配。
- 针对位置编码的魔改:相对位置编码(RoPE)与偏移
- DeepSeek 的特殊方案:跨序列的注意力(Multi-Head Latent Attention)
距离和位置编码
LLM 对“距离”的感知是弱结构化、统计性的,而不是显式离散结构化的:
Transformer 本质上是“全连接注意力”
- 任意 token 都可以直接看到任意 token
- 不像 RNN 那样必须一步一步传递
- 所以模型天然没有“近的更近、远的更远”这种 inductive bias
- 如下所示,A attention 到 B 和 E 的距离是一样的
1
A B C D E
Position Embedding 只是“补充位置信息”
因为 attention 本身没有顺序概念,所以必须人为加 position encoding。诸如token_embedding + position_embedding或者 RoPE 等都是一种形式。它“知道顺序”,但不是算法式知道
1
Tom gave Jerry a book because he trusted him.
大模型能够语义地知道 Tom 这里是 he。但是它不是通过 Tom – he,Jerry – him 这样的位置对应关系知道的。
随着上下文变长:
- nesting depth 增大
- distribution shift 出现
- 模型就容易崩
这也是下面这些比较难做的原因:
- long-context reasoning
- counting
- exact matching
Lost in the Middle 问题
指的是 Transformer 对“长距离结构”和“中间位置”缺乏稳定位置感的一种表现。
Transformer
Attention 机制
NLP 对 token 序列 X 的三种编码方式:
RNN
RNN 是递归的结构,所以只能串行计算。1
y_t = f(y_{t-1}, x_t)
CNN
CNN 能够并行计算,但是因为引入了窗口,所以只能看到局部信息。1
y_t = f(x_{t-1}, x_t, x_{t+1})
Attention
Q、K、V
从定义上看,对于 token 流
1 | x1, x2, x3, ... xn |
每个 xi 通过三组线性变换生成:
1 | Qi = xi * Wq |
考虑下面的句子
1 | The animal didn’t cross the street because it was too tired. |
句子中的每一个 token,都有一个自己的 Q。用 Wq 可以提取出这个 Q,如下:
- animal → 它在问:后面有没有补充信息?
- because → 它在问:因果关系是什么?
- it → 它在问:我指的是谁?
- tired → 它在问:谁在 tired?
对于 K,则告诉了这个 token 可以回答什么样的 Q:
- animal → 我是一个名词、可能是指代目标
- street → 我是一个地点名词
- because → 我是因果连接词
- tired → 我是状态形容词
- cross → 我是动作
对于 V,承载了语义信息的本体:
- animal → 动物这个实体的语义
- street → 街道的概念
- tired → 疲劳的状态语义
- cross → 穿越动作
Self-Attention
Self-Attention 指的是 Q、K、V 都来自同一组 token 的 attention。即
1 | X → Q = XWq |
然后
1 | Attention(Q, K, V) |
为什么叫 Self?
- 不是去外部文档查
- 不是去另一段文本查
- 而是在自己这段序列内部相互对齐
对应的是 Cross-Attention:
- Q 来自 Decoder
- K/V 来自 Encoder
容易发现,Cross-Attention 更适合翻译或者对齐另一段文本。而 Self-Attention 更适合理解一句话内部关系。
Multi-Head Attention
如果一个 token 同时想问多种不同类型的问题怎么办?引入多组问题呗。所以上面的三个矩阵会变成三组矩阵。
1 | Wq¹ Wk¹ Wv¹ |
还是对上面的例子而言
对 it 这个 token:
- Head1 问“我指代谁?”,会关注 animal(指代)
- Head2 问“是否有因果关系?”,会关注 because(因果)
- Head3 问“我与哪个动词相关?”,会关注 cross(动作)
KVCache
Why
Token 是模型处理文本的最小离散单位。所以 LLM 并不是直接处理文字,而是直接处理 token。Token 是通过分词器从文本切出来的子串单位。
1 | "Hello world" → [15496, 2159] |
但是
1 | "refund" = 1 token |
不同的模型能够接受不同的上下文长度,因此,它们的 KVCache 也要更大:
- GPT-3.5 4k tokens
- GPT-4 8k / 32k
- Claude 100k
那么 token 是不是特指用户 prompt输入的 token 或者模型输出的 token 呢?其实根据下面的自回归生成,这两个是一个东西。
1 | prompt1 → prompt2 → prompt3 → output1 → output2 ... |
自回归生成:模型按顺序生成 token,每个 token 都只依赖之前已经生成的 token。
例如,下面的 token 序列中,x1 到 x3 是用户的 prompt 输入
1 | x1 → x2 → x3 → x4 → ... → xT |
则 xT 生成的方式是
1 | P(x_t | x_1, x_2, ..., x_{T-1}) |
由此可见,自回归生成导致推理是串行的。为什么要自回归生成呢?原因比较深入,可以理解为:
- 语言本质是序列,唯一通用可行的分解方式就是 chain rule
- 自回归训练极其稳定
输入是前缀,目标是预测下一个 token,loss 是交叉熵。不需要强化学习。 - 从历史演化角度来看,n-gram、RNN、LSTM 等都是自回归的
因为自回归生成,所以导致了 KVCache 的出现。KVCache 把 O(n²) 变成 O(n)。
KVCache 具有如下的形式:
1 | KVCache[layer][token_index] = (K, V) |
layer 是 Transformer 架构的层
现在的 LLM 大都是 Decoder only 的架构,所以这里的层如下所示。1
2
3
4
5
6
7
8
9
10
11
12
13Input tokens
↓
Embedding
↓
Decoder Block 1
↓
Decoder Block 2
↓
...
↓
Decoder Block N
↓
LM Head不同层的 schema 都不同:
- 第一层:按字形索引
- 中间层:按语法索引
- 高层:按语义索引
每一层内部包含:
- Masked Self-Attention
让每个 token 从历史 token 中选择性地读取信息。
当前 token 只能看到自己和之前的 token,不能看到未来的 token。 - FFN
这里就是前馈神经网络,目的是在单个 token 维度上做语义升维和重映射。可以看成是在理解输入的 token。 - Residual / Norm
token_index 表示这是第几个 token
因为 attention 在第 t 步需要:当前 token 的 Q、对比所有历史 token 的 K、加权读取所有历史 token 的 V。所以这些 K 和 V 需要按照 token_index 来存储。K 和 V
K 是我提供什么信息给别人关注。
V 是别人关注我时能读到什么内容。为什么不需要缓存 Q?
因为 Q 只在当前步骤被使用。
在第 t 步,会用Q_t去访问K_{0..t-1},V_{0..t-1}。但是在未来,不会再去访问Q_t了。
如果没有 KVCache,每生成一个新 token 需要重新计算所有历史 token 的 K/V 复杂度是 O(n²)