Skip to content

Part 3: 模型架构 ​

权重结构、量化、Attention、MoE

1. 公共 API 与权重结构 ​

284B 参数是什么、存在哪里、怎么找到它们?今天通过 ds4.h 公共 API 和 weights_bind 绑定过程,理解 Transformer 每层的完整权重组成——后续所有计算都是对这些权重的矩阵运算。

C 知识点 ​

1. opaque type(不透明类型) ​

ds4.h 只暴露类型名,不暴露内部定义:

c
// ds4.h(公共头文件)
typedef struct ds4_engine ds4_engine;   // 前向声明
typedef struct ds4_session ds4_session;
c
// ds4.c(内部定义)
struct ds4_engine {
    ds4_model model;
    ds4_model mtp_model;
    ds4_vocab vocab;
    ds4_weights weights;
    ds4_mtp_weights mtp_weights;
    ds4_backend backend;       // DS4_BACKEND_METAL / CUDA / CPU
    int mtp_draft_tokens;
    float mtp_margin;
    bool quality;
    bool metal_ready;          // GPU 后端已初始化
    bool mtp_ready;
};

调用者只能通过指针操作,不能访问字段。修改 ds4_engine 内部结构不需要重新编译 ds4_server.c。

2. 结构体嵌套设计 ​

ds4 的类型层次:

3. Session Rewrite API — 检查点恢复 ​

工具调用后,模型生成的 DSML 文本可能与"规范"提示不完全一致(格式等价但字节不同)。此时 session 的检查点需要"重写"来匹配下一轮的规范提示。

c
// 重写结果枚举
typedef enum {
    DS4_SESSION_REWRITE_ERROR = -1,         // 错误
    DS4_SESSION_REWRITE_OK = 0,             // 成功
    DS4_SESSION_REWRITE_REBUILD_NEEDED = 1, // 需要从头重建
} ds4_session_rewrite_result;

// 判断是否需要完整重建(Metal 图中有不可就地替换的 KV 行)
bool ds4_session_rewrite_requires_rebuild(int live_len, int canonical_len, int common);

// 从公共前缀开始重写 session
ds4_session_rewrite_result ds4_session_rewrite_from_common(
        ds4_session *s, const ds4_tokens *prompt, int common,
        char *err, size_t errlen);

关键洞察:session 检查点不仅是 token 向量,Metal 图中还有 raw SWA 行、压缩 KV 行、indexer 行等。替换 live tail 后面的内容需要恢复整个图前沿,因此不能简单就地修改。

渲染文本前缀匹配 ​

KV cache 查找从基于 token ID 的 SHA1 改为渲染文本字节的 SHA1:

  • 模型生成一个 token 的解码文本,客户端可能在下一轮把它作为两个 prompt token 发回(BPE 边界差异)
  • 渲染字节前缀匹配可以处理这种情况:命中后用 checkpoint 的精确 token 作为前缀,只对新增后缀重新分词
  • cache 文件名 <sha1>.kv 直接是渲染文本的 SHA1

对齐前沿检查点 ​

持续保存使用绝对对齐位置(如每 ~10k token),不相对于上一次保存点。这防止早期 cold checkpoint 偏移整个保存计划,确保长生成过程中有均匀分布的重启点。

Cold checkpoint 的裁剪锚定在 chat task boundary(最后一个 <User> token 之后、第一个 <Assistant> token 之前),保留稳定的用户角色脚手架,最大化跨会话复用。无 chat anchor 时回退到 trim + 对齐启发式。

4. Session 快照 API ​

增量基准测试(ds4-bench)需要在不同 context frontier 处保存和恢复 session 状态:

c
// 进度回调
typedef void (*ds4_session_progress_fn)(void *ud, int progress);
void ds4_session_set_progress(ds4_session *s, ds4_session_progress_fn fn, void *ud);

// 快照保存/加载/释放
void *ds4_session_save_snapshot(ds4_session *s);
int ds4_session_load_snapshot(ds4_session *s, void *snapshot);
void ds4_session_snapshot_free(void *snapshot);

快照保存 session 在某个 token 位置的完整状态(KV cache 行、logits 等),恢复后可以继续推理。ds4-bench 利用这个 API 在每个 frontier 处保存快照、测 decode 速度、再恢复快照继续 prefill,无需从头重新加载。

5. 独立分词调试 ​

c
// 不加载推理引擎,只打开模型文件的分词器部分
int ds4_dump_text_tokenization(const char *model_path, const char *text, FILE *fp);

这个函数调用 model_open(..., false, false)——prefetch_cpu=false 意味着不预取 CPU 映射,因为只需要分词器数据,不需要遍历 81GB 的张量数据。

6. 函数指针回调 ​

c
typedef void (*ds4_token_emit_fn)(void *ud, int token);
typedef void (*ds4_generation_done_fn)(void *ud);

生成循环中,每产生一个 token 就调用 emit,结束时调用 done。void *ud 是用户数据指针,用于在回调中传递上下文。这是 C 语言实现"观察者模式"的常见方式。

7. required_tensor — 字符串匹配绑定 ​

c
static void weights_bind(ds4_weights *w, const ds4_model *m) {
    w->token_embd = required_tensor(m, "token_embd.weight");
    w->output     = required_tensor(m, "output.weight");

    for (uint32_t il = 0; il < DS4_N_LAYER; il++) {
        ds4_layer_weights *l = &w->layer[il];
        l->attn_norm = required_tensorf(m, "blk.%u.attn_norm.weight", il);
        l->attn_q_a  = required_tensorf(m, "blk.%u.attn_q_a.weight", il);
        // ... 30+ 个权重张量
    }
}

required_tensor 在张量目录中按名称查找。如果找不到就报错退出——再次体现"早失败"。required_tensorf 是格式化版本,用 sprintf 构造名称如 "blk.0.attn_norm.weight"。

sscanf %n 锚点防止部分匹配 ​

deepseek4-quantize.c 中的 parse_expert_tensor 用 sscanf 匹配 MoE 专家张量名,但旧写法会误匹配:

c
// 旧:会匹配 blk.3.ffn_gate_exps.weight_bias 等不相关张量
sscanf(name, "blk.%d.ffn_%15[^_]_exps.weight", &layer, type)

// 新:用 %n 捕获解析位置,验证消耗了整个字符串
int rest = 0;
sscanf(name, "blk.%d.ffn_%15[^_]_exps.weight%n", &layer, type, &rest);
if (rest != strlen(name)) return false;  // 不是精确匹配

%n 是 sscanf 的特殊格式符,写入已消耗的字符数而不消耗输入。rest == strlen(name) 保证没有多余后缀。


LLM 知识点 ​

1. Transformer 权重组成 ​

一个 Transformer 层(ds4_layer_weights)的权重可以分组:

注意力子系统:

权重形状作用
attn_norm[4096]RMSNorm 缩放
attn_q_a[4096, 1024]Query LoRA 压缩
attn_q_a_norm[1024]压缩后归一化
attn_q_b[1024, 64×512]Query LoRA 解压
attn_kv[4096, 512]Key-Value 投影
attn_sinks[64]注意力汇聚点
attn_output_a/b[64×512, 1024], [1024, 4096]输出投影(LoRA)

MoE FFN 子系统:

权重形状作用
ffn_norm[4096]FFN RMSNorm
ffn_gate_inp[4096, 256]专家路由器
ffn_gate/up/down_exps[256, 2048, 4096]256 个路由专家
ffn_gate/up/down_shexp[2048, 4096]1 个共享专家

2. 超连接(Hyper-Connection) ​

ds4.c 的一个独特设计——用可学习的门控代替标准残差连接:

c
// 标准残差:out = x + sublayer(x)
// 超连接:out = HC_post(x, sublayer(HC_pre(x)))

// HC_pre: 计算 HC 维度 = DS4_N_EMBD × DS4_N_HC (4096 × 4 = 16384)
hc_from_plain_embedding(cur, plain, DS4_N_EMBD, DS4_N_HC);

// HC_post: 学习每个流的权重
// fn × scale × sigmoid(x) + base → 门控权重

DS4_N_HC = 4 意味着每个位置有 4 个并行流(streams),通过可学习门控混合。DeepSeek V4/V4.1 与 Qwen3.8(§9,n_hc = 4、低秩 n_hc_lowrank = 320)都用 4 流超连接——V4.1 的 Engram 记忆还按流注入(engram)。

3. LoRA 低秩分解 ​

Query 和 Output 投影使用 LoRA(Low-Rank Adaptation)分解:

秩 r = 1024(DS4_N_LORA_Q),大幅减少参数量。虽然 LoRA 通常用于微调,DeepSeek V4 把它作为架构的一部分来压缩 KV cache。

4. 重要性矩阵(imatrix)校准 ​

量化时不同权重对模型输出影响不同。imatrix(importance matrix)通过在校准数据上运行推理,记录每个权重的贡献度,指导量化器为重要权重保留更高精度:

校准数据(prompts)→ 前向推理 → 记录每层激活 → 统计重要性 → 量化时参考

ds4 的校准数据集位于 gguf-tools/imatrix/dataset/,已扩展覆盖多语言编程、工具调用、算法回忆、Bash 脚本、多语言翻译、语言/散文任务、代码合成、agent transcript 回放、trace 诊断、长上下文检索与比较等场景,并包含 ds4-eval 评测题(不含答案键)作为校准输入,确保量化后的模型在各种任务上保持质量。

5. GLM 5.2 —— 第二种支持的模型架构 ​

DwarfStar 不再是 DeepSeek 专用:现在也支持 GLM 5.2。Flash 仍是首要教学模型(本主题讲的 43 层 / 284B / 256 专家 / MLA 压缩都是 Flash),但引擎已能按模型族分发。GLM 同样是 dense + 路由 MoE 架构,但维度、专家布局、注意力结构与 Flash 不同,靠加载时检测分发:

c
// ds4.h — 模型族检测与 GLM 专用 API
bool ds4_engine_is_glm_dsa(ds4_engine *e);
bool ds4_engine_glm_layer_payload_bytes(ds4_engine *e, uint32_t layer, ...);
const char *ds4_glm_reasoning_effort_text(ds4_think_mode mode);

// model_id: 缓存兼容性稳定 id。0 = 原始 Flash 形状
// (旧 KV 文件保留字节为 0 仍与 Flash 兼容);PRO、GLM 等用非零 id
int ds4_engine_model_id(ds4_engine *e);

量化布局差异(这是 GLM 和 Flash 在权重结构上最实际的差别):GLM 的 dense / 模型控制张量走现有 Q8/F32 路径;路由专家的 gate/up 支持 Q2_K/Q4_K/Q5_K,down 支持 Q2_K/Q4_K/Q5_K/Q6_K。和 Flash 一样采取非对称量化——路由专家(占模型大部分空间)用更激进的低位,dense/共享/注意力保持高精度。

与 Flash 的能力差异:GLM 不支持方向性引导、--power < 100、显式 --prefill-chunk、外部 --mtp 文件;它的 MTP 块在主 GGUF 内部,用 --glm-mtp 启用实验性贪心推测(Flash 的DSpark走独立支持文件)。两机张量并行要求 ownership-aware 的 IQ2_XXS/Q2_K 路由布局,路由 Q4 的 GLM 必须在评估前拒绝。GLM 在三后端的批处理/放置差异见 Part 6 §4l。

GLM 的 GGUF 由 download_model.sh 的 glm-* 目标下载(如 glm-antirez-iq2xxs、glm-antirez-q2、glm-antirez-q4、glm-unsloth-q4)。

6. GLM 5.3 —— 混合递归/稀疏架构(KDA + DSA) ​

2026-09 起 GLM 5.3 Flash 成为第三种一等公民模型(ec7642c)。它与 GLM 5.2 同属 DS4_MODEL_FAMILY_GLM_DSA 族、同是 dense + 路由 MoE,但架构上跨出了一大步:混合递归/稀疏模型——每 4 层里 3 层是 KDA 递归线性注意力(没有 KV cache),第 4 层才是完整 DSA 稀疏注意力(带 pool-4 索引器)。分发谓词一行就能看出这个节奏:

c
// ds4.c — 一层(MTP 块以下)是 KDA 当且仅当 il % 4 != 3
bool ds4_glm53_layer_is_kda(uint32_t il);
// 对偶:GLM 5.3 的该层是否用完整索引器(即 !is_kda)
bool glm_graph_layer_uses_full_indexer(uint32_t il);

模型形状(DS4_SHAPE_GLM53):46 层、4096 维、154880 词表、64 头 / 1 KV 头、DSA cache 行 = 512 latent + 零 RoPE 尾巴(n_rot = 0)、288 专家取 8 + 1 共享、4 路超连接、MTP 块内建(n_nextn_predict = 1)。校验器 config_validate_glm53_layer_types / config_validate_glm53_model 用层类型字段(0 = KDA,1 = DSA)钉死布局。变体枚举扩为 DS4_VARIANT_FLASH / PRO / GLM52 / GLM53,公共查询 ds4_engine_is_glm53()。

KDA —— 没有 KV cache 的注意力层 ​

KDA = Kimi Delta Attention(内核从 kimi-k3 分支改编):逐头门控 delta 规则递归线性注意力。它不存任何 KV cache——历史被压缩进一个固定大小的递归状态矩阵 H(每头一个 head_dim × head_dim 状态),内存与上下文长度无关。decode 数学(kernel_glm53_kda_decode,metal/glm53_kda.metal)五步:

  1. 深度 4 短卷积:q/k/v 各过一个 3 步滚动的 conv_state(n_kda_conv = 4),再 silu
  2. 逐通道衰减:d = exp(lower_bound · sigmoid(exp(a_log) · (raw_gate + dt_bias)))——门控 delta 规则的"遗忘门"
  3. 归一化:q、k 做 L2 归一化(q 额外乘 1/√2 量级缩放)
  4. Delta 规则状态更新(核心):H = H·d; delta_v = (v − H·k) · sigmoid(β); H += k ⊗ delta_v; o = H·q——先用当前 k 探测状态里"已经记住的 v"、只把残差写回去,这就是"delta"的含义
  5. 输出门:RMSNorm(kda_o_norm)后乘 sigmoid(输出门)(g_a/g_b 低秩投影)

prefill 走三内核流水:kernel_glm53_kda_prefill_prepare → _recurrence → _output;GPU 入口 ds4_gpu_glm53_kda_decode / ds4_gpu_glm53_kda_prefill(ds4_gpu.h)。会话状态按层预算:session_glm_kda_conv_state_bytes(卷积状态)+ session_glm_kda_recurrent_state_bytes(8192×128 f32 的递归状态)。

与 Flash 的 MLA 对照最能看清取舍:MLA 把历史压进 512 维 latent 但仍按 token 存行(cache 随上下文线性增长);KDA 直接把历史卷进固定状态(常数内存),代价是丢弃精确检索能力——所以每第 4 层保留完整 DSA 注意力 + pool-4 索引器(DS4_GLM53_INDEX_POOL_SIZE = 4,索引器每 4 个 token 池化一次,配新的 indexer_compressor_ape/gate 张量)负责精确回忆。GLM 5.3 的超连接也从全局标量变成逐层张量。

量化与 SSD streaming ​

  • 量化策略(gguf-tools/glm53_quantize.py 的 regular_qtype()):embedding/output 和所有 KDA dense 张量在 Q2/Q4 工件里都升到 Q8_0(Q2 工件单独保留 kda_q/kda_k 在 Q4_K——精度关键);Q4 工件路由专家 gate/up/down 走 Q4_K,Q2 用 IQ2_XXS gate/up + Q2_K down;共享专家与前 3 层 dense FFN 保持 Q8_0,索引器保持 BF16。配套 glm53_manifest.py / glm53_full_quantize.py(305 GB FP8 "packaged weights" 路径)/ glm53_validate_gguf.py。c8b6e4f 把 imatrix 覆盖扩到 GLM 路由专家
  • SSD streaming 支持全量检查点:Q2 工件把路由专家留在 SSD、常驻层区间整层映射(glm_stream_resident_decode_layer_supported/enabled,接受 gate/up/down 一致为 Q2_K / Q4_K / IQ2_XXS+Q2_K 的层);常驻层把路由张量纳入运行期映射图(weights_model_map_decode_runtime_spans);ds4_streaming_hotlist_glm52.inc 提供专家预热表
  • 自适应内存预算(f5ee85b):显式 cache 尺寸从硬数字降级为"目标值",经 glm_graph_memory_guard_budget 按 模型/图/上下文/平台 预留之后收缩,Metal 与 ROCm 共用同一套守卫数学
  • FP32 紧凑 KV cache(81c8c39):GLM 紧凑 cache 不再写死 F16,DS4_GPU_GLM_COMPACT_CACHE_F16 编译期开关让同一套内核按 cache_f16 选元素宽度——零 RoPE 尾巴(n_rot=0)让 FP32 存储在这里没有旋转混合的复杂性
  • M3 Ultra BF16 decode(6cf658a):Q4 工件的 KDA kda_q/k/v 保持 BF16,Apple Silicon 上走融合 ds4_gpu_glm53_matmul_bf16_qkv——一个内核同时产出 q/k/v(kernel_glm53_mul_mv_bf16_f32_qkv,metal/glm53_bf16.metal),砍掉每 KDA 层三次独立 dispatch;M3 Ultra 512GB 上 Q4 工件 decode 24.74 t/s
  • 续 prefill 修复(639d4eb):GLM 5.3 常驻图会在 prefill 中途切换注意力切片,短续写后缀(检查点 + 后缀低于最小值)强制 chunk = 1 并跳过索引/batch prefill 路径(glm53_graph_resume_prefill_min_tokens)

GLM 5.3 的 GGUF:download_model.sh 的 glm53-q2(~90 GiB)/ glm53-q4(~178 GiB)/ glm53-fp8(~305 GiB,仅打包权重、引擎暂不能执行)/ glm53-full-q2(SSD streaming 用全量检查点变体)。

7. 视觉编码器与多模态输入(Vision) ​

同一轮同步给两个模型族都加上了视觉输入:DeepSeek V4 Flash 的 "Vision-Exp" 检查点(fc8bf3c)和 GLM 5.3(4771329)。视觉是 GPU-only 路径——ds4.c 里没有 CPU 视觉编码器;编码器权重是独立的侧车 GGUF,用 --vision 传入(ds4_engine_vision_encode_image 先把映射注册进 GPU:Apple 走 ds4_gpu_set_model_map_range,CUDA/ROCm 走 ds4_gpu_set_aux_model_map_range——DGX Spark 这类统一内存系统上直接映射零拷贝,767e517)。

图像解码(ds4_image.c + third_party/iris):PNG/JPEG 魔数嗅探后用 vendored 的头文件-only 解码器(png.h / jpeg.h,零 libpng/libjpeg 依赖);EXIF 方向解析(ds4_jpeg_orientation);上限 64 MiB 编码字节 / 16384 边长 / 64M 像素。每张图算一个 SHA-256 指纹——它是多模态 KV cache 的身份标识(见 视觉术语表)。

两族预处理与编码器对比:

GLM 5.3DeepSeek V4 Flash Vision-Exp
缩放Qwen-VL 系 smart resize:对齐因子 28、每 token 2·28·28 像素、二分搜索塞进 16..8000 token 预算(ds4_glm53_smart_resize),双三次插值 + CLIP 均值方差归一化最小 147456 像素、8:1 长宽比钳制、对齐 14;硬上限 384 token(压缩网格 (H/14+2)/3),灰 127 画布居中填充、v/127.5−1 归一化
切块14×14 patch、时间维 2(Qwen2.5-VL 布局)、2×2 空间合并 → token 数 = patch 数 / 4朴素 3×14×14 行主序 patch,合并重排在布局阶段做(行对交织 + 对齐 PAD,ds4_deepseek4_image_layout_build)
ViT24 层 / hidden 1024 / FFN 4096 SwiGLU(带偏置)/ 16 头,QK-RMSNorm,融合偏置 QKV32 层 / hidden 1024 / FFN 2816 SwiGLU(无偏置)/ 16 头,无 QK norm;每个中间结果舍入 BF16 对齐参考数值
位置编码base-10000 RoPE,16 频率 y/x 分半,作用在合并后网格同样的 y/x 分半 RoPE,作用在原始 patch网格
投影2×2 downsample(4·1024→4096)+ merger SwiGLU + post normaligner_w1/w2 → 4096;哨兵嵌入(start/pad/newline/end)装在视觉模型里

嵌进 LLM 上下文的方式两族完全不同——这是最值得读的部分:

  • GLM:往 prompt 里压 vision_start + N × vision_image 占位词表 token + vision_end(ID 从视觉 GGUF 元数据绑定);prefill 时 glm_vision_overlay_prepare 把与当前 chunk 相交的行收集进 GPU overlay 张量,用编码器输出的浮点嵌入覆盖这些占位行
  • DeepSeek:发合成哨兵 token(DS4_N_VOCAB + type,type ∈ START/PAD/IMAGE/NEWLINE/END),行对交织、块对齐到 pos%4==0、整块 ≤ 384 token,IMAGE 行按 layout.perm 置换填充。更妙的是语言模型侧带 visual_router_bias[43]——MoE 路由器对图像 token 施加一组专门的偏置(ds4_gpu_router_select_batch_visual_tensor):视觉 token 走不同的专家分布,这是把多模态做进路由而不只是嵌入拼接

多模态会话状态与 KV 复用的安全设计(98b9135 / b0982a1)、服务器图像 API 契约,见 Part 5 §7 与 视觉术语表。

8. DeepSeek V4.1 Flash —— Engram 外部记忆架构(2026-09) ​

2026-09-12 的 bd66c40 给 ds4 加进第四个模型族:DeepSeek V4.1 Flash(DS4_MODEL_FAMILY_DEEPSEEK41 = 2,DS4_VARIANT_FLASH41 = 4,GGUF general.architecture = "deepseek41")。发布时只用 Metal;2026-09 中旬的 a04f46f 把文本推理带上了 CUDA(单台 DGX Spark Q2 SSD streaming、双 Spark RDMA TP——见 Part 6 §4r;视觉仍 Metal 专用)。它有自己的分词器与推理图(全新路径 ds41_gpu_graph,decode ds41_graph_step、层主序 prefill ds41_graph_prefill_sweep——不再复用 ds4_gpu_graph),与 V4 Flash 的权重、DSpark 支持文件、视觉编码器互不通用。与 Flash 0731 的形状对比:

V4 FlashV4.1 Flash
层数 / 维度43 层 / 409640 层 / 5120
词表~129k(含合成哨兵)129280(<|System|>、<tool_result> 成为真正特殊 token)
注意力MLAMLA,64 头 / 1 KV 头 / head_dim 512 / q_lora 1280 / o_lora 1024 / o_groups 8,SWA 128
索引器DSA 稀疏候选块索引器:32×128、top-512、8 个源层(index_source_layer_ids)
MoE256 专家取 6 + 1 共享384 专家取 6 + 1 共享,n_ff_exp 2304,路由 sqrtsoftplus + noaux_tc
逐层压缩—compress_ratios:<2 层 dense、<20 层 2×、其余 1×(KV 随层位压缩)
上下文64K(YaRN 扩展)原生 65536、max_position_embeddings 1048576,compress-rope 160000
Engram无第 1、14 层挂盘上 n-gram 记忆表(189 GiB)

Engram:挂在残差流上的盘上记忆 ​

V4.1 最大的架构新意是 Engram——把一部分知识外置成一张哈希探测的 n-gram 查找表(机制全解见 Engram 术语表):最近 4 个 token 经词表压缩(129280→99092)和乘法哈希,在每层表里探 24 行(3 个历史位置 × 8 头,各对一个素数桶数取模),读回 24×256 个 E4M3+E8M0 标量,再由该层 F16 权重 engram_kv([6144, 5·5120])投影成 4 条超连接流各自的 key + 1 个共享 value;kernel_dsv41_engram_add(metal/dsv41.metal)用缩放余弦做门控:

gate = sigmoid(copysign(√|cos(h_s, k_s)|, cos(h_s, k_s)))
h_s  ← bf16(h_s + gate · bf16(v))          // 每条 HC 流独立门控,共享 value

它在 HC 混合与 attn_norm 之前注入(ds41_graph_before_attention / 批处理孪生 ds41_before_attention_batch),图像 token 位置整段抑制。表永远不进内存:GGUF 尾部 189 GiB 加载时就被 model_unmap_engram munmap,之后走 F_NOCACHE + F_RDAHEAD=0 的裸 pread——读过的页不许缓存,这 189 GiB 在任何运行模式(常驻 / SSD streaming / TP)下都住在磁盘上,所以模型文件必须放快速本地 SSD。

think-level:数值化的推理努力 ​

V4.1 把 thinking 从三档开关变成连续旋钮:--think-level 25 / /think 25,1–100 设置推理努力、0 关闭(--think = 75,--think-max = 100;仅 V4.1,其他引擎直接拒绝)。实现极简:ds4_think_mode 用 DS4_THINK_LEVEL_BASE + level(基值 1000)把 101 档编码进现有枚举,ds4_deepseek41_reasoning_effort_text() 渲染成紧跟 BOS 的一段系统文本 "Reasoning Effort: N (range 1-100, ...)"。改档位会改写最靠前的 token,前缀匹配(ds4_tokens_starts_with)随之失败、KV 缓存整段重建——这是"努力档位活在 prompt 里"的直接代价。恢复会话时 agent 会用 101 档逐一渲染候选前缀、按最长匹配还原当时的档位。

工件与转换 ​

目标文件主权重说明
ds41f-q2341 GiB152 GiB路由 gate/up IQ2_XXS、down Q2_K,注意力/共享/头 Q8_0;单台 128 GB Mac SSD streaming,或双 128 GB Mac TP(每 rank ~81 GiB)
ds41f-q4483 GiB294 GiB路由专家 Q4_K + imatrix 校准;两段式下载自动拼接校验;需 SSD streaming 或 512 GB Mac 常驻
ds41f-vision0.9 GiB—266 个 BF16 张量原样拷贝,投影 5120 维、图像 token 上限 1024

两者都内嵌 189 GiB Engram 表(无损直拷,不参与量化)。gguf-tools/deepseek41_*.py 四件套负责转换:_metadata.py 烘焙 Engram 布局常量(素数表用 sympy nextprime 从 engram_vocab_size−1 起、乘子用固定种子 10007·layer 的 RNG——逐位复现 DeepSeek 官方 inference/engram.py,C 侧只消费数组);_quantize.py 处理原生 FP8 32×32 块源格式(E8M0 缩放 LUT、路由专家 2-per-byte nibble 解包)并支持从原始 safetensors 重新生成 imatrix(校准记录在 deepseek41.calibration,从不二次量化 GGUF);_validate_gguf.py 重导计划逐张量校验形状/类型/偏移,--payload 还会逐字节比对抽样专家与 Engram 行。源 FP32→F16 溢出、NaN 码(E4M3 的 0x7F / 缩放 255)都会硬拒绝。

能力边界(引擎打开时的硬校验) ​

V4.1 发布时拒绝一切非 Metal 后端;CUDA 落地后边界变为:文本在 Metal 与 CUDA(单 Spark Q2 SSD streaming、双 Spark RDMA TP、Q2 SSD 模式最多 8 行 decode 批处理)上可用,视觉/DSpark/ROCm/流水线仍不支持,CUDA 网络 TP 按会话有序服务(5 个以上就绪 session 才跨双 Spark 批处理)。方向性引导、--power < 100、ctx > 1M 依旧拒绝。视觉走自己的编码器(共享 V4 Flash 的 patch 几何与 ViT 骨架,但 token 预算 1024、行主序布局、无 8:1 长宽比钳制——见 Part 5 §7)。Metal 执行细节与性能见 Part 6 §4q,CUDA 与双 Spark TP 见 Part 6 §4r。

9. Qwen3.8 Flash Next —— GDN 混合架构与盘上 n-gram PLE(2026-09) ​

ccea768 + f06d3eb(2026-09 中旬合入 main)给 ds4 加进第五个模型族:Qwen3.8 Flash Next(DS4_MODEL_FAMILY_QWEN4_EXP,GGUF general.architecture = "qwen4exp"),Metal 与单 GPU CUDA(含 DGX Spark)双后端。它的架构是三条已知路线的"全家桶"——GLM 5.3 式的递归/稀疏混合层(KDA 的近亲 GDN)、V4.1 式的盘上哈希 n-gram 记忆(Engram 的近亲 PLE)、再加上 DeepSeek 式的 MoE 与超连接。形状(DS4_SHAPE_QWEN4_EXP):

DeepSeek V4.1GLM 5.3Qwen3.8 Flash Next
层数 / 维度40 层 / 5120—48 trunk 层 + 1 MTP 块 / 2560
层混合全注意力(MLA + SWA)3/4 KDA + 1/4 DSA3 GDN + 1 门控 GQA(n_full_attn_interval = 4)
线性层状态—64 头 × 128×12848 v 头 × 128×128(16 k 头共享)
注意力MLA,1 KV 头DSA + pool-4 索引器GQA 24Q:2KV(12:1)+ QSA 索引器(4 头 × 128、top-2048、4-token 块)
门控注意力无无有:Q 投影出 2D,一半 query 一半 sigmoid 门
MoE384 取 6 + 1 共享288 取 8 + 1 共享512 取 10 + 1 共享(共享专家带学习到的 sigmoid 缩放 ffn_gate_inp_shexp),n_ff_exp 640
超连接4 流—4 流(低秩 320,见 §2)
外部记忆Engram 4-gram(189 GiB,2 层)—PLE 3-gram(95.37 GiB,第 1 层)
MTP不支持1 nextn 块1 nextn 块(--mtp)
上下文原生 64K—原生 262144,DS4_QWEN4_YARN_FACTOR=2/4 静态 YaRN

GDN:delta 规则递归层(qwen4_ref_linear) ​

GDN(Gated DeltaNet)与 KDA 同族——不存 KV cache,历史压缩进每层固定大小的递归状态——数学骨架也一致(参考实现 ds4.c:67353):

qkv → 深度 4 短卷积 + silu → q/k L2 归一化(q × 1/√D)
g    = exp(A · softplus(a + dt_bias))          // 逐 v 头衰减
β    = sigmoid(b)                              // 写入门
H   *= g                                       // 衰减旧状态
δ    = (v − H·k) · β                           // 探测残差:只写"还没记住的"
H   += k ⊗ δ ;  o = H·q
o    → RMSNorm → × sigmoid(z)                  // 输出门

差异在头组织:48 个 v 头配 16 个 k 头(v 头 j 配 k 头 j % 16,权重按此平铺),状态 [Hv][128][128] f32。prefill 是逐 token 扫描(scan),decode 是 O(1) 状态刷新——所以 Qwen 的"KV cache"只有 1/4 的层(12 个注意力层)存在,递归层零 KV,长上下文内存压力远小于纯注意力模型。

门控 GQA + QSA 块稀疏索引器(qwen4_ref_attention / qwen4_ref_select) ​

每第 4 层是全注意力层,但三处非常规:GQA 极端化(24 个 query 头共享 2 个 KV 头);Q 投影宽度 2D——每头前一半是 query、后一半过 sigmoid 直接乘在注意力输出上(gated attention,输出门控而非路由);QSA 索引器做块稀疏:位置按 4-token 分块,轻量索引器(4 头 × 128 维,独立 q/k 投影 + relu 求和)给每块打分,取 top-512 块(×4 = top 2048 token)+ 未满尾块,softmax 只在选中 token 上算——与 GLM 5.3 DSA 的 pool-4 索引器同构,但选择粒度是"块"而非"token"。RoPE 只转每头前 64 维(n_rot = 64 of 256),位置来自 3D rope 表(st->pos3,多模态的 t/h/w 三轴)。

PLE:挂在第 1 层的盘上 3-gram 记忆(qwen4_ref_ple) ​

Qwen 版的"Engram"叫 PLE(n-gram embedding 层),机制与 V4.1 Engram 惊人地同构(对照 Engram 术语表):

  • 哈希:qwen4_ple_step 对 3-gram 做 multiply-xor 哈希(乘子与每头素数桶数都从 GGUF 元数据逐字读入,qwen4exp.ple.layer_multipliers 等),2-gram 与 3-gram 各 8 个头共 16 行/token;跨句用专用 EOS id(248044)切断
  • I/O:95.37 GiB 原生 BF16 表钉在 GGUF 尾部,加载时 model_unmap_qwen_ngrams munmap,运行时每行 320 字节 pread 直读(qwen4_ngram_read,≥2 行的请求在 Apple 上走 GCD 并发 reader——Part 6 §4s);拒绝任何量化过的表("repack with qwen4_native_ngrams.py")
  • 注入:行 → key/value 投影(4 条 HC 流各一组 key、共享 value)→ 缩放余弦门控 sigmoid(copysign(√|g|, g))(与 Engram 的门同形状)→ 按 n-gram 大小膨胀的深度卷积 → 像一层 mixer 一样混进残差流

量化工件与转换 ​

目标文件常驻权重配方
qwen38-q2137.10 GiB41.73 GiBgate/up 专家 imatrix 校准 IQ2_XXS;down 专家 Q2_K,640 逻辑输入列填充到 768(对齐 K 块);64 GB Mac 起步
qwen38-q4k165.11 GiB69.74 GiBgate/up Q4_K 校准、down MXFP4;需更大机器
qwen38-vision——llama.cpp 的 Q8_0 mmproj(Qwen3-VL 塔)

两者都内嵌 95.37 GiB 原生 BF16 n-gram 表(磁盘直读,不参与量化、不映射不预载)——保真优先:量化过的表无法还原精度,运行时也直接拒收。转换链 gguf-tools/qwen4_pack*.py / qwen4_native_ngrams.py / qwen4_iq2.py / qwen4_exp_convert.py 负责 HF → qwen4exp、老 fast-pack 迁移与 n-gram 回填。

视觉、分词与其他 ​

视觉走 llama.cpp 生态的 Qwen3-VL mmproj(clip + qwen3vl_merger)——ds4 第一次直接消费 llama.cpp 的标准转换格式而非自制片(qwen4_vision_weights_bind,1152 维 / 16 头 / patch 16 / 2×2 合并);图像缩放到 32 的倍数、预算 64..1024 token(DS4_QWEN4_IMAGE_MAX_TOKENS 可调),编码后取模型的 3D rope 位置。分词器是 qwen35 预分词(英语缩写 + Unicode 类交替正则,Unicode 类由 Python regex 模块表生成 ds4_qwen4_unicode.inc)。thinking 支持 low/medium 档(DS4_THINK_LOW/MEDIUM 枚举,其他模型渲染成 HIGH)+ 服务器 reasoning_effort 数值字符串(Part 5 §3)。工具调用用原生 <tool_call><function=...><parameter=...> 格式。不支持:TP、流水线、SSD expert streaming、ROCm;CPU 路径只是逐 token 的 f32 对齐参考实现(qwen4_ref_forward_token),不是推理后端。

2. 量化与矩阵运算 ​

284B 参数的 FP16 模型需要 568GB 内存,消费级硬件根本装不下。量化把每个参数从 16 bit 压缩到 2-4 bit,让 81GB 的模型在 128GB MacBook 上可运行。今天学习位操作提取压缩值、block 布局和矩阵-向量乘法。

C 知识点 ​

1. 位操作 ​

量化核心是把 32 位浮点数压缩为 2-8 位整数。这需要大量位操作:

提取 2-bit 值:

c
// 从 1 字节中提取 4 个 2-bit 值
uint8_t byte = qs[i];             // 8 位 = 4 个 2-bit 值
uint8_t v0 = byte & 0x03;         // 最低 2 位
uint8_t v1 = (byte >> 2) & 0x03;  // 次低 2 位
uint8_t v2 = (byte >> 4) & 0x03;  // 次高 2 位
uint8_t v3 = (byte >> 6) & 0x03;  // 最高 2 位

Q8_K 量化(8-bit):

c
// 把 float 数组量化为 int8
static void ds4_quantize_row_q8_K(const float *x, block_q8_K *y, int64_t n) {
    for (int j = 0; j < n; j += QK_K) {
        float amax = 0.0f;
        for (int i = 0; i < QK_K; i++) {
            float ax = fabsf(x[j + i]);
            if (ax > amax) amax = ax;
        }
        float scale = amax / 127.0f;
        y->d = scale;
        for (int i = 0; i < QK_K; i++)
            y->qs[i] = (int8_t)roundf(x[j + i] / scale);
    }
}

2. 查表法(Lookup Table) ​

IQ2_XXS 量化使用预计算表来解码 2-bit 编码:

c
// 预计算的 IQ2_XXS 解码表(来自 llama.cpp/GGML)
static const uint64_t iq2xxs_grid[256] = { ... };

// 从 2-bit 索引查表得到 8 个 int8 值
const uint64_t grid = iq2xxs_grid[qs_byte];

查表法是经典的性能优化:用内存(表)换取计算(乘法/移位),特别适合量化解码这种简单但高频的操作。

3. Block 布局 ​

量化数据以 block 为单位存储,每个 block 包含 256 个权重值:

Q2_K block (84 字节):
┌─────────────────────────────────────┐
│ scales[16]    │ 16 组缩放因子 (1B)  │  16 字节
│ qs[64]        │ 256 个 2-bit 值    │  64 字节
│ d (uint16)    │ 全局缩放           │   2 字节
│ dmin (uint16) │ 全局最小值偏移      │   2 字节
└─────────────────────────────────────┘
  每权重: 84 × 8 / 256 = 2.625 bit
Q4_K block (144 字节):
┌─────────────────────────────────────┐
│ d (uint16)    │ 全局缩放           │   2 字节
│ dmin (uint16) │ 全局最小值偏移      │   2 字节
│ scales[12]    │ 12 组缩放+偏移     │  12 字节
│ qs[128]       │ 256 个 4-bit 值    │ 128 字节
└─────────────────────────────────────┘
  每权重: 144 × 8 / 256 = 4.5 bit

4. 定点点积 ​

量化推理的核心操作是"量化向量的点积":

c
// Q8_K × Q2_K 点积(简化版)
float dot_q2_k_q8_k(const block_q2_K *a, const block_q8_K *b) {
    float sum = 0.0f;
    // 解量化 a 的值: value = d * (scale_i * qs_value + dmin * offset_i)
    // 乘以 b 的值: value_b = b->d * b->qs[i]
    // 累加
    return sum;
}

实际实现考虑了分组缩放、偏移、以及 SIMD 优化(ARM NEON)。


LLM 知识点 ​

1. 模型量化原理 ​

训练时使用 FP32(32-bit 浮点),推理时可以用更少的位数:

FP32 → 每个权重 32 bit → 284B 模型 ≈ 1.1 TB
Q8   → 每个权重  8 bit → 284B 模型 ≈ 284 GB
Q4   → 每个权重  4 bit → 284B 模型 ≈ 142 GB
Q2   → 每个权重 ~2 bit → 284B 模型 ≈  81 GB

2. 非对称量化 ​

DeepSeek V4 Flash 使用"非对称量化"——不同组件用不同精度:

为什么可以这样?路由专家占模型空间的绝大部分,但每个 token 只使用 6/256。即使单个专家的精度较低,6 个专家的混合效果仍然很好。

3. 量化对推理的影响 ​

量化的好处:

  • 内存减少:81GB(Q2)vs 284GB(FP32)
  • 带宽减少:读写更快,推理速度提升
  • 可运行:128GB MacBook 可以运行 Q2 模型

量化的代价:

  • 精度损失:模型输出质量略有下降
  • 计算开销:需要量化解码(查表、缩放)
  • 但实际影响很小:2-bit 量化的 DeepSeek V4 Flash 仍能通过编码代理测试

4. 矩阵-向量乘法(matvec) ​

推理中最频繁的操作:一个向量乘以一个权重矩阵。

y = x @ W

输入向量 x: [4096]     (1D)
权重矩阵 W: [4096, 2048] (2D,可能被量化)
输出向量 y: [2048]     (1D)

在 ds4.c 中有多个变体:

  • matvec_f16: FP16 权重
  • matvec_q8_0: Q8_0 权重
  • matvec_q2_k: Q2_K 权重
  • matvec_iq2_xxs: IQ2_XXS 权重(用于路由专家)
  • matvec_q8_0_pair: 同时计算 gate 和 up(共享输入)

5. GGUF 量化工具链与 imatrix ​

gguf-tools/ 目录提供独立的 HF-to-GGUF 量化器 deepseek4-quantize.c(1885 行),不依赖 GGML/llama.cpp:

  • 支持 imatrix 感知的路由专家量化,内置 Q8_0/Q4_K/Q2_K/IQ2_XXS 量化器
  • imatrix(重要性矩阵):通过校准数据集收集各张量的 token 频率,量化时对高频张量分配更高精度
  • 新增 quality-testing/ 质量测试框架,含评分脚本(score_official)和校准 prompt 语料库
  • q2-imatrix 版本:用 imatrix 调优的 q2 权重,logits 误差接近 q4——以极低 bit 宽获得接近高精度量化的质量

总结:量化与精度的权衡 ​

ds4.c 的选择: 路由专家用 IQ2_XXS/Q2_K,其他用 Q4/Q8 → 在 128GB 内存上运行 284B 参数模型。

3. 注意力机制 ​

注意力是 Transformer 的核心——它让模型决定"看哪里"。今天理解 Self-Attention 的分数计算、KV Cache 避免重复计算、MLA 压缩让百万 token 上下文成为可能。这是整个学习中最关键的一天。

设计决策推导:从问题到方案 ​

今天的每个知识点都不是孤立的——它们是一条解决问题的链条:

问题 1:如何让模型"关注"不同位置的信息?
  └─ 方案:Self-Attention(Q·K^T 点积算分数,softmax 加权)

问题 2:逐 token 生成时,之前的 K/V 已经算过了,为什么要重复算?
  └─ 方案:KV Cache(缓存历史的 K/V,每步只算新 token 的)

问题 3:KV Cache 随序列线性增长,长上下文时内存爆了怎么办?
  └─ 方案 1:滑动窗口(只保留最近 128 个 token 的精确 KV)
  └─ 方案 2:MLA 压缩(历史 token 用低维压缩表示,64 倍压缩)

问题 4:滑动窗口丢弃了旧 token,模型怎么"记得"很久以前的内容?
  └─ 方案:混合注意力(raw 最近 128 + comp 压缩历史,统一 softmax)

问题 5:标准注意力是 O(n²),压缩后行数仍然很多怎么办?
  └─ 方案:Indexer(模型学习哪些压缩行值得看,跳过无关行)

问题 6:64 个注意力头的 KV Cache 是 1 个头的 64 倍,太大了?
  └─ 方案:MQA(64 个 Q 头共享 1 组 K/V,KV 体积减少 64 倍)

每个方案解决了一个具体问题,同时又引入了新问题,驱动下一个设计决策。理解这条链条比记住每个概念的定义更重要。

C 知识点 ​

1. 多维数组索引 ​

注意力涉及多维数据:[heads, seq_len, head_dim]。在 C 中用一维数组 + 手动索引:

c
// 第 h 个注意力头,第 r 个 KV 行,第 d 个维度
float val = kv_rows[(uint64_t)r * DS4_N_HEAD_DIM + d];

// 第 h 个头的 query 向量
const float *qh = q + (uint64_t)h * DS4_N_HEAD_DIM;

// 第 h 个头的输出
float *oh = out_heads + (uint64_t)h * DS4_N_HEAD_DIM;

公式:array[i][j][k] = base[i * J * K + j * K + k]

2. 指针算术 ​

c
const float *kv = raw_kv + (uint64_t)r * DS4_N_HEAD_DIM;  // 指向第 r 行
float *head_out = out + (uint64_t)h * DS4_N_HEAD_DIM;       // 指向第 h 个头的输出

raw_kv 是 float *,+ n 实际偏移 n * sizeof(float) 字节。这是 C 指针算术的核心。

3. 向量操作(BLAS 风格) ​

ds4.c 实现了基本的 BLAS-1 操作:

c
// 点积: sum(a[i] * b[i])
static float dot_f32(const float *a, const float *b, uint64_t n);

// 缩放: a[i] *= s
static void scale_f32(float *a, float s, uint64_t n);

// AXPY: y[i] += alpha * x[i]
static void axpy_f32(float *y, const float *x, float alpha, uint64_t n);

这些是线性代数的基本操作(DOT, SCAL, AXPY 来自 BLAS 标准)。


LLM 知识点 ​

1. Self-Attention 机制 ​

注意力是 Transformer 的核心:让每个位置"关注"所有相关位置。采样阶段使用 softmax(参见 采样 softmax),KV Cache 通过 mmap & KV Cache 的内存映射加载。

ds4.c 实现 layer_attention_rows_one():

c
for (uint32_t h = 0; h < DS4_N_HEAD; h++) {         // 64 个头
    const float *qh = q + h * DS4_N_HEAD_DIM;         // 当前头的 query

    float max_score = sinks[h];                        // sink 作为初始最大值
    for (uint32_t r = 0; r < n_kv; r++) {              // 遍历所有 KV 行
        score[r] = dot_f32(qh, kv + r * HEAD_DIM, HEAD_DIM) * kq_scale;
        if (score[r] > max_score) max_score = score[r];
    }

    float denom = expf(sinks[h] - max_score);          // softmax 分母
    for (uint32_t r = 0; r < n_kv; r++) {
        float weight = expf(score[r] - max_score);     // softmax 权重
        axpy_f32(oh, kv + r * HEAD_DIM, weight, HEAD_DIM); // 加权累加
        denom += weight;
    }
    scale_f32(oh, 1.0f / denom, HEAD_DIM);             // 归一化
}

2. KV Cache ​

KV Cache 存储历史 token 的 Key 和 Value,避免重复计算。

为什么需要 KV Cache?

text
没有 KV Cache:
  生成 token 100 时,重新计算 token 0-99 的 K 和 V
  → O(n²) 计算
text
有 KV Cache:
  生成 token 100 时,只计算 token 100 的 K 和 V
  → O(n) 计算
  用缓存的 K[0-99] 和 V[0-99] 做注意力

ds4.c 的 KV Cache 结构:

c
typedef struct {
    float *raw_kv;        // 原始滑动窗口 KV 行
    uint32_t n_raw;       // 当前行数
    uint32_t cap_raw;     // 容量(滑动窗口大小)

    float *attn_comp_kv;  // 压缩后的 KV 行
    uint32_t n_comp;      // 压缩行数

    float *attn_state_kv;   // 压缩器状态
    float *attn_state_score; // 压缩器注意力分数
} ds4_layer_cache;

滑动窗口(Sliding Window Attention):

KV Cache 不保留所有历史,而是只保留最近 N 个 token(SWA = 128):

c
static void kv_cache_push_raw(ds4_layer_cache *cache, const float *kv) {
    if (cache->n_raw < cache->cap_raw) {
        // 还有空间,直接追加
        memcpy(dst, kv, ...);
        cache->n_raw++;
    } else {
        // 满了,滑动窗口:移除最旧的,添加最新的
        memmove(raw_kv, raw_kv + HEAD_DIM, (cap-1) * HEAD_DIM);
        memcpy(raw_kv + (cap-1) * HEAD_DIM, kv, ...);
    }
}

3. 注意力汇聚(Attention Sinks) ​

ds4.c 的独特设计:每个头有一个可学习的"sink"值:

c
const float *sinks = tensor_data(model, layer->attn_sinks);
float max_score = sinks[h];  // sink 作为 softmax 的基准分数
float denom = expf(sinks[h] - max_score);  // sink 参与 softmax 分母

Sink 不贡献 value 向量,但参与 softmax 归一化。它允许模型"丢弃"不重要的位置,而不是被迫分配注意力。

4. MLA(Multi-head Latent Attention) ​

DeepSeek V4 Flash 使用 MLA 压缩 KV cache:

text
标准注意力:  KV cache 大小 = n_layer × seq_len × n_head × head_dim
           = 43 × seq_len × 64 × 512 = 1,413,376 × seq_len

MLA 压缩后:  KV cache 大小 = n_layer × seq_len × 1 × head_dim
           = 43 × seq_len × 1 × 512 = 22,016 × seq_len

GQA 头分组比:64×(64 个 Q 头共享 1 个 KV 头)。 MLA 实际内存压缩比:128×(原始 KV 总维度 65536 ÷ 潜在表示 512)。详见 mla。

压缩器工作流程:

text
每 ratio 个 token:
1. 输入 token → KV 投影 → 压缩器状态更新
2. 压缩器状态 → 注意力加权池化 → 压缩 KV 行
3. 压缩 KV 行存入 attn_comp_kv

注意力时:
→ 同时关注 raw_kv(最近 128 个)和 attn_comp_kv(历史压缩行)
→ 由 indexer 选择哪些压缩行可以关注

5. 混合注意力(Mixed Attention) ​

ds4.c 的 layer_attention_mixed_one 同时使用原始和压缩 KV:

c
// 原始行:最近 128 个 token
for (r = 0; r < n_raw; r++)
    score[idx] = dot(q, raw_kv[r]);

// 压缩行:历史 token 的压缩表示
for (r = 0; r < n_comp; r++) {
    if (!comp_allowed[r]) continue;   // indexer 决定是否允许
    score[idx] = dot(q, comp_kv[r]);
}

// 统一 softmax + 加权求和

这就是为什么 DeepSeek V4 Flash 可以支持百万 token 上下文:大部分历史被压缩,但关键信息通过 indexer 选择性地保留。

5b. 逐层压缩缓存容量 ​

Metal GPU graph 使用 comp_cap 控制每层压缩 KV 行的最大数量。之前的实现用全局 comp_cap(基于最小压缩比计算),所有层共用同一个悲观容量。对于 ratio-128 的层(128 个 token 才产生一行压缩行),实际只需要 ratio-4 层 1/32 的容量:

c
// 旧:全局容量,所有层用 ratio-4 的悲观值
g->comp_cap = ctx_size / min_ratio + 2;  // min_ratio 通常为 4

// 新:逐层容量,按每层自己的压缩比计算
for (uint32_t il = 0; il < DS4_N_LAYER; il++) {
    const uint32_t ratio = ds4_layer_compress_ratio(il);
    g->layer_comp_cap[il] = ctx_size / ratio + 2;
}

效果:ratio-128 层的持久缓存(layer_attn_comp_cache、layer_index_comp_cache)分配量大幅减少,节省 GPU 显存。工作缓冲区仍使用全局 comp_cap,因为 ratio-4 的 indexer 层可能达到该上限。

4. MoE 混合专家 ​

284B 参数全部计算太慢了。MoE 的思路是"参数可以多,但每次只用一小部分"——256 个专家里只激活 6 个,实际计算量不到总参数的 0.1%。今天学习路由策略、SwiGLU 激活和非对称精度设计。

MoE 在注意力机制(同一主题前一章节)的基础上,用稀疏路由替换全连接 FFN。

设计决策推导:为什么用 MoE? ​

问题 1:284B 参数的模型,每次推理要读取所有权重——decode 瓶颈是内存带宽
  └─ 方案:把大 FFN 拆成 256 个小专家,每次只用 6 个
     → 读取量从 256× 降到 6×,但参数总量不变

问题 2:如何决定每个 token 用哪些专家?
  └─ 方案 A(前 3 层):Hash 路由——确定性分配,不同 token 分散到不同专家
  └─ 方案 B(后续层):Top-K 路由——学习到的 Router 打分,选分数最高的 6 个

问题 3:为什么是 6 个而不是 3 个或 12 个?
  └─ 实验验证的甜点:1→4 效果提升明显,4→6 仍有收益,6 之后边际递减
     → 更多专家 = 更多内存读取 = 更慢,收益不足以抵消成本

问题 4:6 个专家够吗?知识覆盖会不会不够?
  └─ 方案:加 1 个共享专家(所有 token 必经)兜底通用知识
     → 43 层 × 6 专家 = 258 次选择,通过残差连接逐步积累

问题 5:256 个路由专家的参数量太大(~6.4B),怎么压缩模型文件?
  └─ 方案:非对称量化——路由专家用 2-bit(不常被激活,误差被稀释),
     共享专家和注意力投影用 4-8 bit(每次都用,需要精度)
     → 284B 参数压缩到 81GB,在 128GB MacBook 上可运行

C 知识点 ​

1. 指针数组 ​

MoE 中每个专家有自己的权重矩阵,通过指针数组索引:

c
// 256 个专家,每个有独立的 gate/up/down 权重
ds4_tensor *ffn_gate_exps;   // 指向 256 个专家的 gate 权重
ds4_tensor *ffn_up_exps;     // 指向 256 个专家的 up 权重
ds4_tensor *ffn_down_exps;   // 指向 256 个专家的 down 权重

selected[6] 数组存储被选中的 6 个专家索引,计算时只加载这 6 个专家的权重。

2. 稀疏索引 ​

top-k 选择只关心前 6 个最大的值,不关心其余 250 个:

c
static void topk_desc(const float *score, int n, int k, int *idx) {
    for (int i = 0; i < k; i++) idx[i] = -1;        // 初始化为无效
    for (int i = 0; i < n; i++) {                     // 扫描所有 n=256 个
        for (int j = 0; j < k; j++) {                  // 与当前 top-k 比较
            if (idx[j] < 0 || score[i] > score[idx[j]]) {
                for (int m = k - 1; m > j; m--)         // 后移
                    idx[m] = idx[m - 1];
                idx[j] = i;                              // 插入
                break;
            }
        }
    }
}

这是"部分排序"——只需要前 k 个,不需要完全排序所有 256 个。

3. 数学辅助函数 ​

c
// SiLU 激活: x × sigmoid(x)
static float silu(float x) {
    return x * sigmoid_stable(x);
}

// 数值稳定的 softplus: log(1 + exp(x))
static float softplus_stable(float x) {
    if (x > 20.0f) return x;           // 避免 exp 溢出
    if (x < -20.0f) return expf(x);    // log(1+小量) ≈ 小量
    return log1pf(expf(x));            // log1p 比 log(1+x) 更精确
}

LLM 知识点 ​

1. MoE(Mixture of Experts) ​

传统 Transformer 每个 token 经过同一个 FFN。MoE 把 FFN 替换为多个"专家":

标准 Transformer

MoE Transformer

DeepSeek V4 Flash 的 MoE 配置:

  • 256 个路由专家(DS4_N_EXPERT = 256)
  • 每个 token 只激活 6 个(DS4_N_EXPERT_USED = 6)
  • 1 个共享专家(DS4_N_EXPERT_SHARED = 1)

DeepSeek V4 PRO 是更大的模型变体(DS4_VARIANT_PRO = 1),MoE 规模远大于 Flash:

  • 384 个路由专家(DS4_SHAPE_PRO),每个 token 激活 6 个
  • 61 层(Flash 为 43 层),7168 嵌入维度
  • 路由专家全部使用 Q4_K 量化(gate、up、down 张量均为 Q4_K)
  • 总参数 ~1.6T,模型文件 ~430GB(IQ2_XXS)或 ~838GB(Q4 分片)
  • 单机推理需要 512GB 内存的 Mac Studio,或通过分布式推理跨两台机器运行

2. 路由(Gating) ​

ds4.c 有两种路由策略:

哈希路由(前 3 层,DS4_N_HASH_LAYER = 3):

c
// 直接通过 token ID 查表得到专家
const int32_t *row = table + (uint64_t)token * DS4_N_EXPERT_USED;
for (int i = 0; i < DS4_N_EXPERT_USED; i++)
    selected[i] = row[i];

固定映射:同一个 token ID 永远路由到同一组专家。简单、无计算开销。

Top-K 路由(后续层):

c
// 1. 计算路由分数
float logits[256];
matvec_any(logits, model, layer->ffn_gate_inp, x);  // 输入 → 256 维

// 2. 特殊激活: √(softplus(logit))
for (int i = 0; i < 256; i++)
    probs[i] = sqrtf(softplus_stable(logits[i]));

// 3. 选择 top-6
topk_desc(probs, 256, 6, selected);

// 4. 归一化权重(加偏置选,用无偏倚的权重)
// 选择时可以加 bias(ffn_exp_probs_b)
// 但权重从无偏倚的 probs 计算
float sum = Σ probs[selected[i]];
for (int i = 0; i < 6; i++)
    expert_weight[i] = probs[selected[i]] / sum * DS4_EXPERT_WEIGHT_SCALE;

为什么用 √(softplus(x)) 而不是标准 softmax?

  • softplus 保证非负
  • 平方根压缩分布,避免单一专家占主导
  • 实验验证这个变体效果更好

3. SwiGLU 激活 ​

每个专家的 FFN 使用 SwiGLU 激活函数:

标准 FFN:  output = W_down × ReLU(W_gate × x)
SwiGLU:    output = W_down × (SiLU(W_gate × x) ⊙ (W_up × x))
c
static void swiglu(float *out, const float *gate, const float *up, uint64_t n) {
    for (uint64_t i = 0; i < n; i++) {
        out[i] = silu(gate[i]) * up[i];    // ⊙ 是逐元素乘法
    }
}

SwiGLU 比 ReLU 效果更好:

  • 门控机制(gate)让网络学会"什么时候激活"
  • SiLU 的平滑性避免了 ReLU 的"死神经元"问题

在 2-bit 量化中还有额外的 clamp:

c
const float limit = DS4_SWIGLU_CLAMP_EXP;   // 10.0f
if (gate[j] > limit) gate[j] = limit;
if (up[j] > limit) up[j] = limit;
if (up[j] < -limit) up[j] = -limit;

4. 完整的 MoE 前向传播 ​

关键洞察:路由专家用 2-bit 量化(省空间),共享专家用 8-bit(保质量)。路由专家的权重占模型大部分空间,所以压缩它们收益最大。

PRO 模型的路由专家使用 Q4_K 量化(~4.5 bit),比 Flash 的 IQ2_XXS 精度更高但体积也更大。ds4 支持三种后端的 PRO Q4_K 推理路径:

  • CPU:matvec_q4_k_experts_mid_prequant() + ds4_vec_dot_q4_K_q8_K(),ARM NEON dot-product 加速
  • Metal:metal_graph_decode_pro_q4_expert_table_expected() 优化的专家查找表
  • CUDA:专用 Q4_K rowspan kernel(moe_gate_up_mid_q4K_expert_tile8_rowspan_kernel 等)

5. MoE 的效率优势 ​

284B 参数模型(Flash),每个 token 只激活:
- 6/256 路由专家 × 3 矩阵 × 2048×4096 = 151M 参数
- 1 共享专家 × 3 矩阵 × 2048×4096 = 25M 参数
- 注意力参数: ~100M
- 总计激活: ~276M 参数 (不到总参数的 0.1%)

这就是为什么 DeepSeek V4 Flash 虽然有 284B 参数,但推理速度接近 27B 的密集模型。

PRO 模型更大但激活参数仅 49B(总参数 1.6T),同样得益于 MoE 的稀疏激活。详见 分布式推理。

6. MoE 专家缓存与 SSD Streaming ​

MoE 的稀疏激活(每 token 只用 6/256 专家)天然适合缓存策略——只把"热"专家放 RAM,"冷"专家留磁盘按需加载:

这就是 SSD Streaming 的基础——把"模型能否运行"从"RAM 必须 ≥ 模型大小"的硬截止,变成"RAM 越多越快"的连续速度谱。启动时根据 RAM 预算计算 ds4_ssd_cache_plan,按流行度排名(ds4_streaming_hotlist.inc)预加载热门专家。Metal 后端(ds4_metal.m)实现了完整的异步加载和 LRU 淘汰。

详见 SSD Streaming 术语表和 Part 6 — GPU 加速。