LLM 基础概念和核心问题整理

主要关注:

  • 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)

前缀

关于前缀

  1. 最佳实践:调整 Prompt 结构(把“死”的放前面)
    1. 失效做法: [用户闲聊] + [系统指令] + [长财报](用户一换,缓存全崩)。
    2. 优化做法: [长财报] + [系统指令] + [用户闲聊]。
  2. 基数树(Radix Tree)管理机制
    1. 如果你的输入是 A + B(A 是闲聊,B 是财报),它会先找 A 的缓存。
    2. 虽然它发现 A + B 不能直接复用 C + B 的缓存,但如果系统发现大量请求都有共同的 B,它会尝试在内部进行“多级缓存”匹配。
  3. 针对位置编码的魔改:相对位置编码(RoPE)与偏移
  4. DeepSeek 的特殊方案:跨序列的注意力(Multi-Head Latent Attention)

距离和位置编码

LLM 对“距离”的感知是弱结构化、统计性的,而不是显式离散结构化的:

  1. Transformer 本质上是“全连接注意力”

    1. 任意 token 都可以直接看到任意 token
    2. 不像 RNN 那样必须一步一步传递
    3. 所以模型天然没有“近的更近、远的更远”这种 inductive bias
    4. 如下所示,A attention 到 B 和 E 的距离是一样的
      1
      A B C D E
  2. Position Embedding 只是“补充位置信息”
    因为 attention 本身没有顺序概念,所以必须人为加 position encoding。诸如 token_embedding + position_embedding 或者 RoPE 等都是一种形式。

  3. 它“知道顺序”,但不是算法式知道

    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
2
3
Qi = xi * Wq
Ki = xi * Wk
Vi = xi * Wv

考虑下面的句子

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
2
3
X → Q = XWq
X → K = XWk
X → V = XWv

然后

1
Attention(Q, K, V)

为什么叫 Self?

  • 不是去外部文档查
  • 不是去另一段文本查
  • 而是在自己这段序列内部相互对齐

对应的是 Cross-Attention:

  • Q 来自 Decoder
  • K/V 来自 Encoder

容易发现,Cross-Attention 更适合翻译或者对齐另一段文本。而 Self-Attention 更适合理解一句话内部关系。

Multi-Head Attention

如果一个 token 同时想问多种不同类型的问题怎么办?引入多组问题呗。所以上面的三个矩阵会变成三组矩阵。

1
2
3
4
Wq¹ Wk¹ Wv¹
Wq² Wk² Wv²
Wq³ Wk³ Wv³
...

还是对上面的例子而言

对 it 这个 token:

  • Head1 问“我指代谁?”,会关注 animal(指代)
  • Head2 问“是否有因果关系?”,会关注 because(因果)
  • Head3 问“我与哪个动词相关?”,会关注 cross(动作)

KVCache

Why

Token 是模型处理文本的最小离散单位。所以 LLM 并不是直接处理文字,而是直接处理 token。Token 是通过分词器从文本切出来的子串单位。

1
"Hello world" → [15496, 2159]

但是

1
2
"refund" = 1 token
"refunding" = ["refund", "ing"]

不同的模型能够接受不同的上下文长度,因此,它们的 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
    13
    Input 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²)

Reference