Skip to content

Part 1: 代码走读

1. 项目全貌与编译系统

追踪 1:从 make 到可执行文件的完整路径

依赖关系图

make (默认目标: all)
├── ds4                    (CLI 交互式聊天)
│   ├── ds4_cli.o          ← ds4_cli.c + ds4.h + linenoise.h
│   ├── linenoise.o        ← linenoise.c + linenoise.h
│   ├── ds4.o              ← ds4.c + ds4.h + ds4_metal.h
│   └── ds4_metal.o        ← ds4_metal.m + ds4_metal.h + metal/*.metal

├── ds4-server             (HTTP API 服务器)
│   ├── ds4_server.o       ← ds4_server.c + ds4.h
│   ├── ds4.o              ← (同上)
│   └── ds4_metal.o        ← (同上)

├── ds4-bench              (吞吐量基准测试)
│   ├── ds4_bench.o        ← ds4_bench.c + ds4.h
│   └── ds4.o + ds4_metal.o

└── ds4-eval               (能力评估,93 道题回归测试)
    ├── ds4_eval.o         ← ds4_eval.c + ds4.h
    └── ds4.o + ds4_metal.o

核心库 ds4.o + ds4_metal.o 被所有可执行文件共享。

追踪编译命令

bash
# 步骤 1:编译推理引擎核心
cc -O3 -ffast-math -mcpu=native -Wall -Wextra -std=c99 -c -o ds4.o ds4.c

# 步骤 2:编译 Metal GPU 桥接层
cc -O3 -ffast-math -mcpu=native -Wall -Wextra -fobjc-arc -c -o ds4_metal.o ds4_metal.m

# 步骤 3:编译 CLI 前端
cc -O3 -ffast-math -mcpu=native -Wall -Wextra -std=c99 -c -o ds4_cli.o ds4_cli.c

# 步骤 4:编译行编辑库
cc -O3 -ffast-math -mcpu=native -Wall -Wextra -std=c99 -c -o linenoise.o linenoise.c

# 步骤 5:链接 CLI
cc -O3 -ffast-math ... -o ds4 ds4_cli.o linenoise.o ds4.o ds4_metal.o -lm -pthread -framework Foundation -framework Metal

注意第 5 步把所有 .o 文件和库合并成最终可执行文件。


追踪 2:ds4.c 文件头部(行 1-120)

文件头注释(行 1-15)

ds4.c:735KB, ~20,000+ 行

开头的注释说明了这个文件的设计哲学:

"This file is deliberately vertical" 这个文件是刻意垂直整合的——它拥有 GGUF 加载、固定张量布局、CPU 参考内核、Metal 图驱动器和分词器连接。

关键词:mmap based(基于内存映射加载),张量数据留在内核页缓存中,直到推理需要它。

标准库包含(行 17-35)

ds4.c 的 18 个 #include 可以按功能分组:

功能头文件
文件 I/O<fcntl.h>, <unistd.h>, <sys/stat.h>, <sys/file.h>
内存管理<sys/mman.h>, <stdlib.h>
字符串/内存<string.h>, <ctype.h>
数学运算<math.h>, <float.h>
类型定义<stdint.h>, <stdbool.h>, <stddef.h>, <inttypes.h>
线程<pthread.h>
输入输出<stdio.h>
可变参数<stdarg.h>
时间<time.h>
错误<errno.h>

条件包含(行 37-44)

c
#include "ds4.h"                    // 公共 API —— 始终包含

#ifndef DS4_NO_METAL
#include "ds4_metal.h"              // Metal GPU 支持 —— 仅 macOS
#endif

#if defined(__ARM_NEON)
#include <arm_neon.h>               // ARM SIMD 指令 —— 仅 ARM 芯片
#endif

这展示了条件编译的两种用法:

  • #ifndef DS4_NO_METAL:在 Makefile 中通过 -DDS4_NO_METAL 禁用 GPU
  • #if defined(__ARM_NEON):编译器自动根据 CPU 架构定义的宏

数学常量与模型超参(行 46-71)

c
#define DS4_NEG_INF (-1.0e30f)      // 不是真正的 -∞,是足够大的负数
#define DS4_POS_INF ( 1.0e30f)      // 同理,用于 softmax 中的掩码
#define DS4_RMS_EPS  ( 1.0e-6f)     // RMSNorm 的 epsilon,防止除零

为什么用 1.0e30f 而不是 INFINITY

  • float 的范围约 ±3.4e38,1e30 足够大
  • 避免浮点异常和 NaN 传播
  • 在 softmax 等运算中,任何正常值减去 1e30 都会变成 0(exp(-大数) ≈ 0)

DeepSeek V4 Flash 架构常量(行 82-105)

c
enum {
    DS4_N_LAYER            = 43,     // Transformer 层数
    DS4_N_EMBD             = 4096,   // 嵌入维度(每个 token 的向量长度)
    DS4_N_VOCAB            = 129280, // 词表大小
    DS4_N_HEAD             = 64,     // 注意力头数
    DS4_N_HEAD_KV          = 1,      // KV 头数(GQA:64:1 分组)
    DS4_N_HEAD_DIM         = 512,    // 每个头的维度
    DS4_N_EXPERT           = 256,    // MoE 专家总数
    DS4_N_EXPERT_USED      = 6,      // 每个 token 激活的专家数
    ...
};

为什么用 enum 而不是 #define

  • 编译器可以进行类型检查
  • 调试器中可以看到枚举名(DS4_N_LAYER43 有意义得多)
  • 所有常量在一个 enum 块中,便于阅读和维护

追踪 3:ds4.h 公共 API(行 1-161)

设计模式:opaque type(不透明类型)

c
typedef struct ds4_engine ds4_engine;     // 前向声明,不暴露内部
typedef struct ds4_session ds4_session;   // 同上

这是 C 语言实现封装的经典手法:

  • 头文件只声明类型名,不定义结构体字段
  • 结构体的完整定义藏在 ds4.c 中
  • 外部代码(CLI、server)只能通过指针操作,不能直接访问字段

好处:

  1. 内部实现可以随意修改,不影响调用者
  2. 强制使用者通过 API 函数操作,不会误用内部状态
  3. 编译时间更快(修改 ds4.c 不需要重新编译 ds4_server.c)

API 层次

ds4_engine           模型(加载一次,只读)
  ├── open/close     生命周期管理
  ├── generate       简单推理(一次性)
  └── tokenize       分词

ds4_session          会话(每次对话一个,可变)
  ├── create/free    生命周期管理
  ├── sync           同步到指定 prompt 前缀(复用 KV cache)
  ├── argmax/sample  选择下一个 token
  └── eval           执行一步推理

ds4_tokens           动态数组
  └── push/free/copy 管理操作

关键概念:ds4_session_sync() 是高性能的关键——如果新 prompt 和已有 cache 有共同前缀,只需计算新增部分,不需要从头开始。


追踪 4:项目文件大小对比

ds4.c          735 KB   推理引擎核心(最大)
ds4_metal.m    639 KB   Metal GPU 桥接
ds4_server.c   269 KB   HTTP 服务器
linenoise.c     83 KB   行编辑库
ds4_cli.c       48 KB   CLI 前端
ds4_metal.h     31 KB   Metal 头文件
ds4.h            6 KB   公共 API
linenoise.h      5 KB   行编辑头文件

ds4.c 占了项目代码的大部分。它是一个典型的"单文件巨石"(monolithic)设计,所有推理逻辑集中在一个文件中。

2. 内存管理与 mmap

追踪 1:模型加载完整调用链

从用户运行 ./ds4 -p "Hello" 到模型加载完成的调用链:

main()                          [ds4_cli.c]
  └── ds4_engine_open()         [ds4.h 约 81 行, ds4.c 约 15716 行]
        ├── xcalloc(1, sizeof(*e))           分配引擎结构体
        ├── model_open(&e->model, path, metal, true)  加载主模型
        │     ├── open(path, O_RDONLY)            打开文件
        │     ├── fstat(fd, &st)                  获取文件大小
        │     ├── mmap(NULL, size, PROT_READ,     映射到内存
        │     │       MAP_SHARED/MAP_PRIVATE, fd, 0)
        │     ├── cursor_u32() × 4                 解析 GGUF 头部
        │     ├── parse_metadata(m, &c)            解析元数据表
        │     │     └── calloc(n_kv, sizeof(kv))     分配 KV 数组
        │     ├── parse_tensors(m, &c)             解析张量目录
        │     │     └── calloc(n_tensors, ...)       分配张量数组
        │     └── model_prefetch_cpu_mapping(m)    若 CPU 后端且 prefetch_cpu=true
        ├── model_warm_weights()                可选预热
        │     └── posix_madvise(WILLNEED)           提示内核预读
        │     └── for each page: p[off]             触发 page fault
        ├── vocab_load()                        加载分词器
        ├── config_validate_model()             验证模型参数
        └── weights_bind()                      绑定权重指针
              └── tensor_data(m, t)                 返回 mmap 偏移

model_openprefetch_cpu 参数:第四个参数控制是否触发 CPU 映射预取。正常推理引擎传 true,但 ds4_dump_text_tokenization() 只需要分词器,不需要遍历巨大的张量数据,因此传 false 以节省时间。

关键数据流

磁盘文件 (81GB GGUF)

    ▼ open() + mmap()

虚拟地址空间 (m->map 指针)

    ├── parse_metadata() → m->kv[]       元数据(模型名称、参数等)
    ├── parse_tensors()  → m->tensors[]  张量目录(名称、偏移、大小)

    ▼ weights_bind()

weights 结构体(指针数组,全部指向 mmap 内部)
    ├── tok_embd     → m->map + offset_0   词嵌入矩阵
    ├── layer[0].wq  → m->map + offset_1   第 0 层 query 权重
    ├── layer[0].wk  → m->map + offset_2   第 0 层 key 权重
    ├── ...
    └── output       → m->map + offset_N   输出投影

追踪 2:ds4_model 结构体(行 895-908)

c
typedef struct {
    int fd;                    // 文件描述符(mmap 需要)
    const uint8_t *map;        // mmap 返回的基地址
    uint64_t size;             // 文件/映射总大小

    uint32_t version;          // GGUF 版本号(必须是 3)
    uint64_t n_kv;             // 元数据 KV 对数量
    uint64_t n_tensors;        // 张量数量
    uint64_t alignment;        // 数据对齐要求
    uint64_t tensor_data_pos;  // 张量数据在文件中的起始位置

    ds4_kv *kv;                // 元数据数组(calloc 分配)
    ds4_tensor *tensors;       // 张量目录数组(calloc 分配)
} ds4_model;

内存所有权:

  • map:mmap 拥有,munmap 释放
  • kvcalloc 分配,free 释放
  • tensorscalloc 分配,free 释放
  • fdopen 返回,close 关闭

model_close(行 1057-1065)— 释放顺序

c
static void model_close(ds4_model *m) {
    if (!m) return;
    free(m->kv);                                        // 先释放小对象
    free(m->tensors);
    if (m->map) munmap((void *)m->map, (size_t)m->size); // 再释放大映射
    if (m->fd >= 0) close(m->fd);                        // 最后关文件
    memset(m, 0, sizeof(*m));                             // 清零结构体
    m->fd = -1;                                          // 重置为无效值
}

注意 memset(m, 0, sizeof(*m))m->fd = -1:确保关闭后结构体处于安全的"空"状态。


追踪 3:错误处理层次

ds4.c 有三个层次的错误处理:

致命错误(不可恢复)

c
// 行 400-403
static void ds4_die(const char *msg) {
    fprintf(stderr, "ds4: %s\n", msg);
    exit(1);
}

// 行 413-416
static void ds4_die_errno(const char *what, const char *path) {
    fprintf(stderr, "ds4: %s '%s': %s\n", what, path, strerror(errno));
    exit(1);
}

用于:内存不足、文件不存在、格式错误等。这类错误发生后程序无法继续运行。

警告(可恢复)

c
// posix_madvise 失败时只打印警告
if (rc != 0) {
    ds4_log(stderr, DS4_LOG_WARNING,
            "ds4: warning: POSIX_MADV_WILLNEED failed: %s\n", strerror(rc));
}

posix_madvise 只是建议,失败了不影响正确性,只是性能可能受影响。

分配守卫(调试用)

c
// 行 437-459: 分配守卫基础设施
static bool g_alloc_guard_enabled;
static const char *g_alloc_guard_phase;

static void ds4_alloc_guard_check(const char *op, size_t size) {
    if (!g_alloc_guard_enabled) return;    // 守卫未启用,跳过
    fprintf(stderr, "ds4: internal allocation during %s: %s(%zu)\n",
            g_alloc_guard_phase, op, size);
    exit(1);                               // 热循环中有分配就报错
}

// 行 14470: 在生成循环中启用守卫
ds4_alloc_guard_begin("CPU token generation");
for (int i = 0; i < n_predict && pos < ctx_size; i++) {
    // 这里任何 malloc/calloc/realloc 都会触发 ds4_die
}
ds4_alloc_guard_end();

追踪 4:xmalloc_zeroed vs calloc(行 482-498)

c
static void *xmalloc_zeroed(size_t n, size_t size) {
    // 溢出检查
    if (size != 0 && n > SIZE_MAX / size) ds4_die("allocation size overflow");

    const size_t total = n * size;
    void *p = xmalloc(total ? total : 1);

    /* 注释解释了为什么不用 calloc:
     * 大块未触碰的 calloc 内存可能通过共享零页管理。
     * CPU 解码 KV cache 每次增长一个 token,用 calloc 会把
     * 首次缺页移到生成阶段。Darwin 上观察到内核 panic。 */
    memset(p, 0, total);

    return p;
}

调用链:

kv_cache_init()                     [行 6027]
  └── for each layer:
        └── xmalloc_zeroed(raw_cap * DS4_N_HEAD_DIM, sizeof(float))   [行 482]
              ├── 溢出检查
              ├── xmalloc(total)     malloc + NULL 检查
              └── memset(p, 0, total) 手动清零

追踪 5:线程局部存储(行 605-614)

c
// 行 605-610: 线程池结构体
typedef struct {
    // ... pthread 同步原语 ...
} ds4_thread_pool;

// 行 612-614: 全局变量
static ds4_thread_pool g_pool;              // 全局共享的线程池
static __thread int g_parallel_depth;       // 每线程独立的递归深度
static uint32_t g_requested_threads;        // 请求的线程数

__thread 的内存模型:

线程 1 (主线程)                   线程 2 (工作线程)
┌─────────────────┐              ┌─────────────────┐
│ g_parallel_depth │              │ g_parallel_depth │
│     = 0          │              │     = 1          │
└─────────────────┘              └─────────────────┘
      独立副本                         独立副本

g_pool ←─── 两个线程共享同一个线程池 ───→ g_pool

追踪 6:模型预热的页面扫描(行 1328-1353)

c
static void model_warm_weights(const ds4_model *m) {
    const uint64_t start = m->tensor_data_pos;   // 张量数据起始位置
    const uint64_t end = m->size;                 // 文件末尾
    const uint64_t page = sysconf(_SC_PAGESIZE);  // 系统页大小(通常 16KB)

    volatile uint64_t checksum = 0;               // volatile 防止优化器删除

    // 先用 posix_madvise 提示内核
    posix_madvise(p + start, end - start, POSIX_MADV_WILLNEED);

    // 每页读一个字节,强制 page fault
    for (uint64_t off = start; off < end; off += page) {
        checksum += p[off];
    }
    checksum += p[end - 1];                       // 最后一个字节
}

为什么用 volatile?编译器看到 checksum 计算后没有被使用,可能会优化掉整个循环。volatile 告诉编译器"这个变量可能被外部修改,不要优化掉对它的读写"。

实际上这里的目的不是真的要 checksum,而是强制触发每一页的 page fault

3. GGUF 二进制格式

追踪 1:GGUF 文件头部解析

调用链

model_open()                            [行 1174]
  ├── open(path, O_RDONLY)                 打开文件
  ├── fstat(fd, &st)                       获取大小
  ├── mmap(NULL, size, PROT_READ, ...)     内存映射

  ├── cursor_at(m, 0)                      创建游标,从偏移 0 开始
  │     base = m->map
  │     size = m->size
  │     pos = 0

  ├── cursor_u32(&c, &magic)               读 4 字节 → 0x46554747 ("GGUF")
  │     └── cursor_read(&c, &magic, 4)
  │           └── memcpy(&magic, base+0, 4)  → pos=4

  ├── cursor_u32(&c, &version)             读 4 字节 → 3
  │     └── memcpy(&version, base+4, 4)    → pos=8

  ├── cursor_u64(&c, &n_tensors)           读 8 字节 → 张量数量
  │     └── memcpy(&n_tensors, base+8, 8)  → pos=16

  └── cursor_u64(&c, &n_kv)               读 8 字节 → 元数据 KV 数量
        └── memcpy(&n_kv, base+16, 8)      → pos=24

GGUF 头部字节布局

偏移  大小  字段          值
0     4     magic         "GGUF" = 0x47 0x55 0x47 0x46
4     4     version       3
8     8     n_tensors     ~500+(取决于模型)
16    8     n_kv          ~100+(模型元数据对数)
24    ...   metadata[0]   开始

追踪 2:parse_metadata(行 1093-1119)

解析流程

parse_metadata(m, &c)                     [行 1093]
  │ pos = 24(头部之后)

  ├── calloc(n_kv, sizeof(ds4_kv))        分配 KV 数组

  └── for i = 0..n_kv-1:                  遍历每个 KV 对

        ├── cursor_string(&c, &kv->key)      读 key
        │     ├── cursor_u64(&c, &len)         读字符串长度
        │     └── s->ptr = base + pos; pos += len  指向 mmap 内部

        ├── cursor_u32(&c, &kv->type)        读类型(0=bool, 1=u32, ...)

        ├── kv->value_pos = c->pos           记录 value 起始位置(不解析!)

        └── skip_value(&c, kv->type, 0)      跳过 value 内容

懒加载策略

ds4_kv 只存储 value_pos(value 在文件中的偏移),不立即解析:

c
typedef struct {
    ds4_str key;          // key 字符串(指向 mmap)
    uint32_t type;        // value 类型
    uint64_t value_pos;   // value 在 mmap 中的偏移
} ds4_kv;

只有在需要时(如 config_validate_model),才用 cursor_at(m, kv->value_pos) 读取具体值。这避免了为每个元数据值分配内存。

skip_value — 递归跳过(行 932-970)

c
static bool skip_value(ds4_cursor *c, uint32_t type, int depth) {
    uint64_t scalar = scalar_value_size(type);
    if (scalar != 0) return cursor_skip(c, scalar);     // 标量类型:直接跳过

    if (type == GGUF_VALUE_STRING) {                    // 字符串:读长度+跳过
        ds4_str ignored;
        return cursor_string(c, &ignored);
    }

    if (type == GGUF_VALUE_ARRAY) {                     // 数组:递归跳过每个元素
        uint32_t item_type;
        uint64_t len;
        cursor_u32(c, &item_type);
        cursor_u64(c, &len);
        for (uint64_t i = 0; i < len; i++)
            skip_value(c, item_type, depth + 1);
        return true;
    }
}

追踪 3:parse_tensors(行 1123-1170)

解析流程

parse_tensors(m, &c)                      [行 1123]

  ├── calloc(n_tensors, sizeof(ds4_tensor))   分配张量数组

  └── for i = 0..n_tensors-1:                遍历每个张量

        ├── cursor_string(&c, &t->name)        名称
        ├── cursor_u32(&c, &t->ndim)           维度数
        ├── for d = 0..ndim-1:
        │     └── cursor_u64(&c, &t->dim[d])   每维大小
        │     └── t->elements *= t->dim[d]     计算总元素数
        ├── cursor_u32(&c, &t->type)           数据类型
        └── cursor_u64(&c, &t->rel_offset)     相对偏移


  ├── m->tensor_data_pos = align_up(c->pos, m->alignment)
  │     计算张量数据的起始位置(对齐到 alignment 字节边界)

  └── for i = 0..n_tensors-1:                  转换为绝对偏移
        t->abs_offset = tensor_data_pos + t->rel_offset
        验证: abs_offset + bytes <= m->size    不超出文件范围

为什么分两步解析?

  1. 先读元信息:名称、维度、类型、相对偏移
  2. 再计算绝对偏移:需要知道 tensor_data_pos,它是在所有张量元信息读完之后才确定的

追踪 4:量化块结构体大小验证

c
// 行 153
#define DS4_STATIC_ASSERT(name, cond) typedef char name[(cond) ? 1 : -1]

// 行 154-157
DS4_STATIC_ASSERT(ds4_block_q2_k_size,    sizeof(block_q2_K)    == 84);   // ✓ 16+64+2+2 = 84
DS4_STATIC_ASSERT(ds4_block_q4_k_size,    sizeof(block_q4_K)    == 144);  // ✓ 2+2+12+128 = 144
DS4_STATIC_ASSERT(ds4_block_q8_k_size,    sizeof(block_q8_K)    == 292);  // ✓ 4+256+32 = 292
DS4_STATIC_ASSERT(ds4_block_iq2_xxs_size, sizeof(block_iq2_xxs) == 66);   // ✓ 2+64 = 66

block_q4_K 内存布局验证

c
typedef struct {
    uint16_t d;            // 2 字节,偏移 0
    uint16_t dmin;         // 2 字节,偏移 2
    uint8_t  scales[12];   // 12 字节,偏移 4
    uint8_t  qs[QK_K/2];   // 128 字节,偏移 16 (QK_K=256, 256/2=128)
} block_q4_K;              // 总计 144 字节 ✓

128 个 uint8_t 存储 256 个 4-bit 权重:每个字节存 2 个权重(高 4 bit + 低 4 bit)。


追踪 5:config_validate_model(行 2343-2425)

config_validate_model(&e->model)         [行 2343]

  ├── 从元数据读取模型参数
  │     required_u32(m, "deepseek4.block_count")          → 43
  │     required_u32(m, "deepseek4.embedding_length")     → 4096
  │     required_u32(m, "deepseek4.vocab_size")           → 129280
  │     required_u32(m, "deepseek4.attention.head_count") → 64
  │     ...
  │     (30 多个参数)

  ├── 逐个与硬编码常量比较
  │     config_expect_u32("block_count", 43, DS4_N_LAYER)      ✓
  │     config_expect_u32("vocab_size", 129280, DS4_N_VOCAB)   ✓
  │     ...

  └── 任何一个不匹配 → fprintf(stderr, ...) + exit(1)

这个函数确保"文件里的模型"和"代码期望的模型"完全一致。是"专用引擎"设计的关键体现。