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

主要关注:

  • LLM 的基础原理
  • KVCache

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

相关场景

  • 训练 infra
    分布式训练、参数同步、checkpoint、通信优化
    DeepSpeed, Megatron-LM, FSDP, NCCL, ZeRO
  • 推理 infra
    模型加载、KV Cache 管理、动态批处理、并发调度
    vLLM, TensorRT-LLM, TGI, Ray Serve
  • 模型存储与加载
    权重分片、lazy loading、权重格式
    Safetensors, GGUF, Tensor Parallel
  • 向量检索
    向量数据库、索引结构、量化 FAISS, Milvus, ScaNN, HNSW, IVF
  • 资源编排与调度
    相比传统 k8s 多了 GPU 调度、混部、弹性伸缩。
    相关技术:K8S, Ray, RunPod, vGPU
  • 数据管线与特征存储
    数据清洗、分片、版本控制
    Petastorm, Delta Lake, Feature Store

Reference