Appearance
LLM 核心架构概念
基于 ds4.c(DeepSeek V4 Flash 推理引擎)代码学习整理。所有概念均可在源码中找到对应实现。
1. Embedding
问题:Token ID 是整数(如 token 42),整数之间无法做有意义的数学运算(99 - 42 = 57 没有语义)。需要将离散符号映射到连续向量空间。
方案:Embedding 查表。模型有一个 (129280, 4096) 的矩阵,每行对应一个 token 的 4096 维向量。
- 语义相近的 token 在向量空间中距离更近
- 向量运算能表达语义关系:
king - man + woman ≈ queen - 纯查表操作,无计算
c
// `embed_token_f16()` (ds4.c 约 2538 行)
const uint16_t *row = base + (uint64_t)token * stride; // 按 token ID 找行
for (uint64_t i = 0; i < stride; i++)
out[i] = f16_to_f32(row[i]); // 16位浮点转32位模型规模与 embedding 维度的典型对应:
| 模型规模 | 典型 N_EMBD |
|---|---|
| 小模型 (7B) | 4096 |
| 中模型 (70B) | 8192 |
| 大模型 (400B+) | 16384 |
2. 注意力机制(Attention)
Q / K / V
借用了数据库检索的概念:
- Query (Q):我在找什么
- Key (K):每个东西的标签
- Value (V):每个东西的内容
注意力分数 = Q 和 K 的点积,然后用 softmax 归一化,最后加权取出 V。
Multi-Query Attention (MQA)
DeepSeek 的配置:64 个 Q 头,1 组 K/V。
64 个 Q 头 = 64 种不同的"提问角度",捕获不同类型的关系
1 组 K/V = 共享的信息源Q 有 64 个头提供多样性,K/V 只有 1 组节省内存。对效果影响很小,但 kv-cache 内存缩小 64 倍。
c
// `layer_attention_rows_one()` (ds4.c 约 4751 行)(核心注意力计算)
for (uint32_t h = 0; h < DS4_N_HEAD; h++) { // 遍历64个头
// Q·K 点积算分数
score[r] = dot_f32(qh, kv, DS4_N_HEAD_DIM) * kq_scale;
// 内联 softmax:减最大值 → exp → 求和 → 归一化
// 加权求和 V
axpy_f32(oh, kv, weight, DS4_N_HEAD_DIM);
}3. RoPE 旋转位置编码
问题:注意力分数只看向量内容,不知道 token 顺序。"我爱猫"和"猫爱我"的 Q/K 值一样。
方案:按位置旋转 Q 和 K(不旋转 V,因为 V 不参与分数计算)。
将向量拆成若干对,每对做二维旋转,不同对用不同旋转频率:
高频分量(θ 大):精确区分相邻位置("放大镜")
低频分量(θ 小):感知远距离关系("广角镜")关键性质:点积只依赖相对距离。不管绝对位置在哪,距离相同的 token 对有相同的旋转差。
低频分量天然编码了绝对位置的大致信息("这个 token 大概在文本前 1/3"),高频分量编码精确的相对距离。
c
// RoPE 核心旋转循环(ds4.c 约 4574 行)
for (uint32_t i = 0; i < n_rot; i += 2) {
const float c = cosf(theta) * mscale;
const float s = sin_sign * sinf(theta) * mscale;
const float x0 = tail[i + 0];
const float x1 = tail[i + 1];
tail[i + 0] = x0 * c - x1 * s; // 二维旋转公式
tail[i + 1] = x0 * s + x1 * c;
theta_extrap *= theta_scale; // θ 递减,产生不同频率
}4. MoE 混合专家(Mixture of Experts)
核心思想
把一个大 FFN 拆成 256 个小专家网络,每次只激活 6 个 + 1 个共享专家。
路由过程
token 向量 → Router(线性层 + softmax)→ 256 个分数 → 取前 6 名- 不同 token 激活不同专家,所有 256 个专家都会被用到
- 共享专家(所有 token 必经)兜底通用知识
- 每个专家独立计算,输出按权重加权求和
为什么是 6 个
实验验证的效率甜点。从 1 到 4 效果提升明显,4 到 6 仍有明显收益,6 之后边际收益迅速下降。再增加专家数量效果提升极小但计算成本线性增加。
"6 个不够"的疑虑
- 每个 token 通常只需少量专家,信息需求集中在特定领域
- 43 层 × 6 个专家 = 258 次专家调用,通过残差连接逐步积累所需知识
- 某层没选到的专家,可能在其他层被选中
c
// MoE Router 打分(ds4.c 约 5023 行)
matvec_any(logits, model, layer->ffn_gate_inp, x); // x × W_router → 256个分数
for (int i = 0; i < DS4_N_EXPERT; i++)
probs[i] = sqrtf(softplus_stable(logits[i]));
// Top-K 选择(ds4.c 约 5065 行)
topk_desc(selection, DS4_N_EXPERT, DS4_N_EXPERT_USED, selected); // 256选65. LoRA 低秩适配
核心思想:不修改原始权重,在旁边加一个低秩的"补丁"。
output = X × W + X × A × B
↑ 冻结 ↑ 只训练这些
(4096×4096) (4096×1024) × (1024×4096)- B 初始化为零,训练从原始模型行为的安全起点逐步调整
- 训练完成后可合并进原始权重(
W_new = W + A×B),推理无额外开销 - 在 DeepSeek 中,LoRA 不是用于微调,而是模型架构本身的一部分,用于降低注意力投影的参数量
6. KV Cache
问题:逐 token 生成时,之前的 K/V 已算过但被丢弃,导致重复计算。
方案:缓存已算过的 K 和 V,每步只算新 token 的。
没有缓存:总计 1+2+3+...+n = n(n+1)/2 次 KV 计算
有缓存: 总计 n 次 KV 计算生成 1000 个 token,从约 50 万次降到 1000 次。
MQA(1 组 KV 头)让 KV Cache 内存缩小 64 倍,同样的显存能处理更长的上下文。
c
// KV cache push(ds4.c 约 7273 行)
kv_cache_push_raw(cache, scratch->kv); // 新 KV 追加到缓存7. 滑动窗口注意力(Sliding Window Attention)
问题:标准注意力 O(n²),长文本计算量和内存都爆炸。
方案:每个 token 只看最近 128 个 token。
单层感受野:128
43 层层层传递:128 × 43 = 5504远处信息通过残差连接在多层之间逐步传递,类似传话游戏——每个人只能和附近的人说话,但消息经过多次传递能传到很远。
8. 残差连接(Residual Connection)
核心:输出 = 输入 + 处理结果
每一层不是"重写"而是"批注"——在原始输入上追加新信息。
- 原始 embedding 信息经过 43 层后依然存在
- 梯度可以跳过所有层直接回流,解决深层网络的梯度消失问题
- 任何一层"什么都不做"都是安全的(output = input + 0)
c
// 残差保存(ds4.c 约 7242 行)
memcpy(scratch->attn_residual, inp_hc, ...); // 保存输入用于残差9. RMSNorm
问题:层数多了,数值不断放大最终溢出。
方案:每层把数值除以均方根,拉回合理范围。
c
// RMSNorm(ds4.c 约 2563 行)
ss = Σ x[i]² // 平方和
scale = 1 / sqrt(ss/n + eps) // 均方根的倒数
out[i] = x[i] * scale * weight[i] // 缩放 + 可学习权重比 BatchNorm 少了减均值这一步,更简单更快,效果相当。每层用两次:注意力前和 MoE 前。
10. SwiGLU 激活函数
MoE 专家内部的激活函数,替代经典的 ReLU。
c
// SwiGLU(ds4.c 约 4876 行)
output = silu(gate) × up // silu(x) = x / (1 + e^(-x))不像 ReLU 硬性截断,SwiGLU 是软性门控:gate 路决定"保留多少",up 路提供"要保留的内容",两者相乘得到过滤结果。
11. BPE 分词器(Byte Pair Encoding)
训练时统计相邻 token 对频率,不断合并最高频的对。推理时按学到的规则从长到短匹配切分。
- 高频组合合成一个 token(如"喜欢"),低频组合保持拆分
- 词表 129280 对中英文都有良好覆盖
- 更大的词表 → 中文编码效率高 → 同样的上下文长度装更多内容
12. 采样策略
模型输出 129280 个 logits,经过以下步骤选出最终 token:
Temperature
除以 temperature 缩放 logits 差距。低温(0.1)放大差距,输出确定;高温(2.0)缩小差距,输出随机。
Top-P(核采样)
按概率降序排列,累加直到总和达到 p,之后的 token 丢弃。自适应:模型确定时保留少量选项,不确定时保留更多。
Min-P
只保留概率 >= 最高概率 × min_p 的 token。砍掉和最优选差距太大的选项。
Argmax
直接选最高分,等价于 temperature=0,输出完全确定。
c
// 采样 `sample_full_vocab()`(ds4.c 约 14243 行)(轮盘赌采样)
float rnd = sample_rng_f32(rng) * sum;
for (int i = 0; i < DS4_N_VOCAB; i++) {
rnd -= probs[i];
if (rnd <= 0.0f) return i;
}13. 量化(Quantization)
将高精度浮点数压缩成低精度整数,让大模型能在消费级硬件上运行。
| 格式 | 每权重位数 | 相对 FP16 大小 | 用途 |
|---|---|---|---|
| FP16 | 16 bit | 1.0x | 重要权重 |
| Q8_K | 8 bit | 0.5x | 临时计算 |
| Q4_K | 4 bit | 0.25x | 主要权重 |
| Q2_K | 2 bit | 0.125x | 不太重要的权重 |
混合精度量化:重要权重(Embedding、Q/K/V 投影、Router)用高精度,不常激活的路由专家用极限压缩。
PRO 模型变体
DeepSeek V4 PRO 的参数与 Flash 有显著差异:
| 参数 | Flash | PRO |
|---|---|---|
| 总参数 | 284B | 1.6T |
| 激活参数 | 13B | 49B |
| 层数 | 43 | 相同架构,更大专家 |
| 专家数 | 256 | 更多路由专家 |
| 激活专家/token | 6 | 更多 |
PRO 模型的 Q4 量化需要 512GB 内存(如 Mac Studio),推理时 KV cache 和 MoE 计算开销都更大。
混合 GGUF 拼接
gguf-splice 工具支持字节级替换——将一个 GGUF 文件中的路由专家张量替换为另一个 GGUF 文件中的对应张量,无需反量化。这允许混合不同量化级别或不同训练检查点的专家,创建定制的模型变体。
概念到代码的映射
概念 关键函数 代码位置
────────────────────────────────────────────────────────────────────
Embedding 查表 `embed_token_f16()` ds4.c 约 2538 行
RoPE 旋转 `rope_tail_ext_inplace()` ds4.c 约 4548 行
注意力计算 `layer_attention_rows_one()` ds4.c 约 4751 行
MoE 路由 `layer_router_probs_one()` ds4.c 约 5023 行
Top-K 选择 `topk_desc()` ds4.c 约 5065 行
单层前向 `layer_forward_raw_swa_one()` ds4.c 约 7212 行
采样 `sample_full_vocab()` ds4.c 约 14243 行单层完整流程
输入向量(来自上一层 + 残差)
│
↓ RMSNorm(归一化,稳定数值)
↓ Q/K/V 投影(带 LoRA 低秩分解)
↓ RoPE 旋转(编码位置信息到 Q 和 K)
↓ KV Cache(缓存 K/V,避免重复计算)
↓ 注意力计算(64头 MQA,滑动窗口)
↓ 残差连接:input + attention_output
│
↓ RMSNorm(再次归一化)
↓ MoE(Router 选 6 专家 + 1 共享专家)
│ └── 每个专家内部:SwiGLU 门控激活
↓ 残差连接:input + attention + moe
│
↓ 传给下一层(共 43 层)模型参数速查
c
DS4_N_EMBD = 4096, // Embedding 维度
DS4_N_VOCAB = 129280, // 词表大小
DS4_N_LAYER = 43, // Transformer 层数
DS4_N_HEAD = 64, // Q 注意力头数
DS4_N_HEAD_KV = 1, // K/V 头数(Multi-Query Attention)
DS4_N_HEAD_DIM = 512, // 每个头的维度
DS4_N_ROT = 64, // RoPE 旋转频率数
DS4_N_LORA_Q = 1024, // Query LoRA 秩
DS4_N_LORA_O = 1024, // Output LoRA 秩
DS4_N_EXPERT = 256, // MoE 专家总数
DS4_N_EXPERT_USED = 6, // 每 token 激活专家数
DS4_N_EXPERT_SHARED = 1, // 共享专家数
DS4_N_FF_EXP = 2048, // 专家内部隐藏维度
DS4_N_SWA = 128, // 滑动窗口大小