Skip to content

Part 1: 构建与加载

从 Makefile 到 GGUF 二进制格式,理解项目如何编译、模型如何加载到内存。

1. 项目全貌与编译系统

看不懂代码就先跑起来。今天从编译系统入手,理解项目怎么从 .c 变成可执行文件,再俯瞰整体架构——后续 14 天的所有代码走读都建立在这个全景之上。

C 知识点

1. Makefile 基础

Makefile 是一个构建脚本,告诉 make 工具如何把源文件编译成可执行文件。

核心语法:

makefile
target: prerequisites
	recipe          # 注意:必须用 Tab 缩进,不能用空格

ds4 的 Makefile 展示了几个重要概念:

变量赋值(?=:=

makefile
CC ?= cc                                        # ?= 表示如果未定义才赋值(用户可覆盖)
CFLAGS ?= -O3 -ffast-math -mcpu=native -Wall -Wextra -std=c99
UNAME_S := $(shell uname -s)                    # := 立即执行 shell 命令
  • ?=(条件赋值):只有变量未定义时才设置,用户可以在命令行覆盖:make CC=clang
  • :=(立即展开):定义时立即求值,不是使用时才求值
  • =(延迟展开):使用时才求值(本项目没用到)

条件编译(平台检测)

makefile
ifeq ($(UNAME_S),Darwin)
    # macOS:链接 Metal 框架
    METAL_LDLIBS := $(LDLIBS) -framework Foundation -framework Metal
    CORE_OBJS = ds4.o ds4_metal.o
else
    # Linux:禁用 Metal,纯 CPU
    CFLAGS += -DDS4_NO_METAL
    CORE_OBJS = ds4.o
endif

伪目标(.PHONY)

makefile
.PHONY: all clean test

声明这些目标不对应实际文件,即使存在同名文件也要执行。

自动变量

makefile
ds4.o: ds4.c ds4.h ds4_metal.h
	$(CC) $(CFLAGS) -c -o $@ ds4.c
  • $@ = 当前目标名(这里是 ds4.o
  • $^ = 所有依赖(这里是 ds4_cli.o linenoise.o ds4.o ds4_metal.o
  • $< = 第一个依赖

2. 编译流程

从 .c 文件到可执行文件分四步:

在 Makefile 中,这些步骤被拆成两条规则:

编译(.c → .o)

makefile
ds4.o: ds4.c ds4.h ds4_metal.h
	$(CC) $(CFLAGS) -c -o $@ ds4.c

-c 标志告诉编译器只编译不链接,生成目标文件(.o)。

链接(.o → 可执行文件)

makefile
ds4: ds4_cli.o linenoise.o $(CORE_OBJS)
	$(CC) $(CFLAGS) -o $@ ds4_cli.o linenoise.o $(CORE_OBJS) $(METAL_LDLIBS)

把多个 .o 文件和库合并成最终的可执行文件。

编译选项解读:

标志含义
-O3最高级别优化
-ffast-math放宽浮点精度要求以换取速度(不严格遵守 IEEE 754)
-mcpu=native针对当前 CPU 生成最优指令
-Wall -Wextra开启所有常见警告
-std=c99使用 C99 标准
-lm链接数学库(sin, cos, sqrt 等)
-pthread链接 POSIX 线程库
-framework Metal链接 Apple Metal GPU 框架(仅 macOS)

3. 条件编译(#ifdef)

在 C 代码中,用预处理指令控制哪些代码参与编译:

c
#ifndef DS4_NO_METAL
#include "ds4_metal.h"
#endif

#if defined(__ARM_NEON)
#include <arm_neon.h>
#endif
  • #ifndef = "if not defined":如果没有定义 DS4_NO_METAL 宏,就包含 Metal 头文件
  • Makefile 中通过 -DDS4_NO_METAL 定义这个宏(-D = define)
  • 这样同一份代码可以在有/无 GPU 的平台上编译

4. 头文件包含约定

c
#include <stdio.h>      // 尖括号:系统头文件,从系统路径搜索
#include "ds4.h"        // 引号:项目头文件,先从当前目录搜索

ds4.c 包含了 18 个系统头文件和 2 个项目头文件,覆盖了:

  • 文件操作:<fcntl.h>, <unistd.h>, <sys/stat.h>
  • 内存映射:<sys/mman.h>
  • 线程:<pthread.h>
  • 类型定义:<stdint.h>, <stdbool.h>, <stddef.h>

LLM 知识点

1. 推理引擎是什么

大语言模型(LLM)的训练和推理是两个完全不同的过程:

  • 训练:用海量数据更新模型参数,需要巨大的 GPU 集群,花费数周甚至数月
  • 推理:用训练好的模型生成文本,单台机器就能运行

推理引擎就是专门做推理的软件。它的工作是:

  1. 从磁盘加载模型文件(包含所有训练好的参数)
  2. 接收用户输入(文本),转换成数字序列(token IDs)
  3. 用矩阵运算计算"下一个最可能的词"
  4. 重复步骤 3,直到生成完整回答

2. ds4.c 的定位

ds4.c 是一个专用推理引擎,只为 DeepSeek V4 模型家族服务(Flash 和 PRO 两个变体)。

它的设计哲学是"垂直整合"(deliberately vertical):

  • 不依赖外部框架:没有 PyTorch、没有 TensorFlow,JSON 解析、HTTP 服务器、分词器全部手写
  • 固定模型架构:所有模型参数(层数、维度、专家数等)都硬编码为常量,不做通用 GGUF 加载器
  • Metal 优先:专门为 Apple Silicon GPU 优化,CPU 路径仅用于正确性验证
  • 模块化扩展:新增 ds4_agent.c(自主编码代理)、ds4_web.c(浏览器工具)、ds4_kvstore.c(模块化 KV 存储)三大模块

3. 模型参数概览

从 ds4.c 的常量定义中,可以看到 DeepSeek V4 Flash 的架构(详细模型信息参见 MODEL_CARD.md):

参数之间的数学关系:

  1. 注意力头的维度:64 头 × 512 维/头 = 32,768(多头拼接后的总维度)。嵌入维度 4,096 是输入/输出的隐藏维度,通过投影矩阵连接——QKV 投影将 4,096 映射到 32,768,输出投影再从 32,768 映射回 4,096。

  2. 嵌入层参数量:129,280(词表)× 4,096(嵌入维度)≈ 530M 参数。每个 token 对应一个 4,096 维的向量。

  3. MoE 激活比例:6 / 256 ≈ 2.3%。每次推理只激活 256 个路由专家中的 6 个(加 1 个共享专家 = 7 个),284B 总参数中实际参与计算的只有一小部分。

  4. 284B 总参数的大致构成:嵌入矩阵 ~530M + 43 层 ×(每层注意力参数 + MoE 专家参数)。MoE 专家是参数量的主要来源:256 个专家 × 每个专家的 FFN 权重。

  5. 上下文窗口与 KV 缓存:1,000,000 tokens × 层数 × 头维度,决定了推理时的显存需求。

关键洞察:虽然模型有 284B 参数,但每次推理只激活约 6/256 的 MoE 专家参数,所以实际计算量远小于同等规模的密集模型。

MODEL_CARD.md 包含从 DeepSeek 官方模型卡提取的关键信息,包括 DeepSeek-V4 家族两个模型的对比:

模型总参数激活参数上下文长度
DeepSeek-V4-Flash284B13B1M tokens
DeepSeek-V4-Pro1.6T49B1M tokens

架构上使用压缩稀疏注意力(CSA)+ 重度压缩注意力(HCA):前两层只用原始滑动窗口(128 token),之后偶数层按 ratio-4 压缩(带 indexer 选择最多 512 行),奇数层按 ratio-128 压缩。这是百万 token 上下文不需要全量 KV cache 的关键。

4. GGUF 格式简介

GGUF(GPT-Generated Unified Format)是 llama.cpp 生态使用的模型文件格式:

  • 二进制格式,紧凑高效
  • 包含模型参数(张量/tensor)和元数据(模型名称、架构参数等)
  • 支持多种量化级别(2-bit、4-bit 等),用更少的位存储参数以节省内存

ds4.c 支持:

  • Q2(2-bit 量化):128GB 内存可运行,模型文件约 81GB
  • Q4(4-bit 量化):需要 256GB+ 内存,精度更高

download_model.sh 现在支持 propro-imatrix 目标,可下载 DeepSeek V4 PRO(1.6T 总参数)的量化模型。PRO 模型需要 512GB 内存的 Mac Studio。

5. 项目的可执行文件

二进制用途对应源文件
./ds4交互式命令行聊天ds4_cli.c + linenoise.c
./ds4-serverHTTP API 服务器ds4_server.c
./ds4-bench吞吐量基准测试ds4_bench.c
./ds4-eval能力评估(93 道题回归测试)ds4_eval.c
./ds4-agent自主编码代理(9607 行)ds4_agent.c + ds4_web.c

make 默认编译 ds4ds4-serverds4-benchds4-evalds4-agent

./ds4-server 提供与 OpenAI/Anthropic 兼容的 API,可以被编程工具(如 Claude Code)直接调用。

6. 发布前 QA 检查清单(QA_BEFORE_RELEASES.md)

make test 只覆盖核心回归;发布前还有一份手动 QA 清单 QA_BEFORE_RELEASES.md(14 节),把每个子系统该验什么列成 checklist:

章节验证范围
1-2仓库与构建完整性、核心回归测试
3-4Metal Flash 路径、Metal PRO 路径
5SSD Streaming
6-7CUDA / DGX Spark、ROCm / Strix Halo
8-9分布式推理、磁盘 KV Cache
10-11Server APIs、ds4-agent
12-14下载脚本与模型文件、性能与功耗、发布签字

这份清单的存在本身就说明:推理引擎的"正确"是跨后端、跨模型变体的——同一份代码要在 Metal Flash、CUDA PRO、ROCm Strix Halo、分布式、SSD streaming 这些组合上都跑对,单元测试覆盖不到的组合靠发布前的人工 checklist 兜底。

2. 内存管理与 mmap

81GB 的模型文件怎么装进内存?答案是——根本不装。今天学习 mmap 零拷贝加载、内存分配的错误处理策略,以及为什么推理引擎必须在初始化阶段完成所有内存分配。

设计决策推导:81GB 模型的内存策略

问题 1:81GB 模型文件,直接 fread 读进内存?
  └─ 不行:需要 81GB 连续物理内存,启动慢(磁盘 I/O)
  └─ 方案:mmap — 让操作系统按需加载页面,启动瞬间完成(只建映射)
     首次访问某页时触发 page fault,OS 从磁盘读取该页

问题 2:mmap 的内存可以被 GPU 直接使用吗?
  └─ MAP_SHARED 映射的内存可以被 Metal 包装为"零拷贝 MTLBuffer"
     GPU 直接从文件映射读取权重,不需要额外的显存拷贝
     → 这就是为什么 Makefile 按平台分支编译

问题 3:推理过程中需要各种临时缓冲区(KV cache、注意力分数等),
        能不能在热循环中 malloc/free?
  └─ 绝对不行!malloc 是非确定性操作(可能触发系统调用、锁竞争)
  └─ 方案:预分配 scratch buffer — 启动时一次性分配所有工作内存,
     推理循环中只做指针操作,零 malloc
     → 这就是为什么没有 xfree

问题 4:calloc 天然初始化为零,为什么用 malloc + memset?
  └─ calloc 的"共享零页"优化让所有虚拟页指向同一个物理零页
     推理时逐 token 写入触发大量 page fault
     macOS 上大 mmap + 大量 page fault 可能导致 kernel panic
  └─ 方案:malloc + memset 在启动时集中触发所有 page fault
     把内存初始化的代价集中到可控的启动阶段

C 知识点

1. malloc/free 与错误处理

C 语言中动态内存分配的基本 API:

c
void *malloc(size_t size);      // 分配 size 字节,不初始化
void *calloc(size_t n, size_t size);  // 分配 n×size 字节,初始化为零
void *realloc(void *ptr, size_t size); // 调整已分配内存大小
void free(void *ptr);            // 释放内存

关键规则:分配可能失败(返回 NULL),必须检查。

ds4.c 用包装函数统一处理错误:

c
static void *xmalloc(size_t size) {
    void *p = malloc(size);
    if (!p) ds4_die("out of memory");  // 失败就直接退出
    return p;
}

这种模式叫 x-包装器(x-wrapper):x 前缀表示"失败时退出"。好处:

  • 每次调用不用重复写 NULL 检查
  • 错误处理策略统一
  • 代码更简洁

2. mmap — 零拷贝文件映射

mmap(memory map)把文件内容直接映射到进程的虚拟地址空间:

c
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);

工作原理

mmap 的优势:

  • 零拷贝:没有内核空间到用户空间的数据拷贝
  • 懒加载:只有实际访问某页时才从磁盘读取(page fault
  • 统一接口:文件内容当数组用,m->map[offset] 直接访问

ds4.c 中的 mmap 调用(model_open() 约 1174-1219 行):

c
static void model_open(ds4_model *m, const char *path, bool metal_mapping) {
    int fd = open(path, O_RDONLY);          // 打开文件
    struct stat st;
    fstat(fd, &st);                          // 获取文件大小

    // Metal 用 MAP_SHARED(Metal 可直接作为 GPU 缓冲区)
    // CPU 用 MAP_PRIVATE(避免 macOS 的 VM 内核 bug)
    const int mmap_flags = metal_mapping ? MAP_SHARED : MAP_PRIVATE;
    void *map = mmap(NULL, st.st_size, PROT_READ, mmap_flags, fd, 0);

    // 之后就可以用 map 指针直接访问文件内容
    m->map = map;
    m->size = st.st_size;
}

MAP_SHARED vs MAP_PRIVATE

MAP_SHARED

  • 多个进程共享同一映射
  • 修改对其他进程可见
  • Metal GPU 可以直接引用这块内存(零拷贝 GPU 缓冲区)

MAP_PRIVATE

  • 写时复制(copy-on-write)
  • 不影响原文件
  • ds4.c 在 CPU 路径使用它,是为了规避 macOS 内核的 VM 计数 bug

3. xmalloc_zeroed — 为什么不用 calloc?

ds4.c xmalloc_zeroed()(约 482-498 行)有一个特殊的函数:

c
static void *xmalloc_zeroed(size_t n, size_t size) {
    const size_t total = n * size;
    void *p = xmalloc(total ? total : 1);
    memset(p, 0, total);  // 手动清零,而不是用 calloc
    return p;
}

为什么不用 calloc?注释解释了原因:

大块未触碰的 calloc 内存可能被虚拟内存系统通过共享零页来管理。CPU 解码的 KV cache 每次增长一个 token,所以用 calloc 会把数千次首次触碰缺页移到生成阶段。在 Darwin 上我们观察到这会导致内核 cpt_mapcnt_inc 溢出 panic。

简单说:

  • calloc 返回的"零"内存可能共享一个物理零页(虚拟内存优化)
  • 第一次写入时触发 page fault,需要分配真实物理页
  • 对于巨大的 KV cache,大量同时 page fault 可能让 macOS 内核崩溃
  • malloc + memset 提前触发所有 page fault,把风险控制在初始化阶段

4. posix_madvise — 内存访问建议

c
int posix_madvise(void *addr, size_t len, int advice);

ds4.c 用 POSIX_MADV_WILLNEED 告诉内核"这块内存马上要用":

c
posix_madvise(m->map, m->size, POSIX_MADV_WILLNEED);

这不是强制操作,只是给内核一个提示,让内核提前把页面从磁盘读入缓存。

5. __thread — 线程局部存储

c
static __thread int g_parallel_depth;  // `g_parallel_depth` 定义处(约 613 行)

__thread 关键字使每个线程拥有变量的独立副本:

  • 线程 A 的 g_parallel_depth 和线程 B 的 g_parallel_depth 是不同的变量
  • 不需要 mutex 保护,因为每个线程只访问自己的副本
  • 线程创建时初始化为零

在 ds4.c 中,它跟踪当前线程在 CPU 并行工作池中的递归深度,防止递归爆炸。

6. 预分配缓冲区模式(Scratch Buffer)

ds4.c 不在热循环中分配内存。它在推理开始前一次性分配所有需要的缓冲区:

c
typedef struct {
    float *plain;      // 50+ 个预分配缓冲区
    float *cur;
    float *next;
    float *q;
    float *qr;
    // ... 每个对应推理中的一个计算步骤
} ds4_cpu_decode_scratch;

初始化时一次性分配:

c
static void cpu_decode_scratch_init(ds4_cpu_decode_scratch *s, uint32_t ctx_size) {
    s->plain = xmalloc(DS4_N_EMBD * sizeof(float));
    s->cur = xmalloc(hc_dim * sizeof(float));
    // ... 50 次 xmalloc ...
}

释放时一次性全部释放:

c
static void cpu_decode_scratch_free(ds4_cpu_decode_scratch *s) {
    free(s->output_norm);
    free(s->output_embd);
    // ... 50 次 free ...
}

还有一个分配守卫确保没有意外的热循环分配:

c
ds4_alloc_guard_begin("CPU token generation");
for (int i = 0; i < n_predict; i++) {
    // 如果这里有 malloc 调用,程序会立即退出并报错
}
ds4_alloc_guard_end();

7. 整数溢出保护

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

n * size 超过 SIZE_MAX(size_t 能表示的最大值)时,乘法会回绕,导致分配过小的缓冲区。这是经典的安全漏洞(CWE-190)。检查 n > SIZE_MAX / size 可以防止这种情况。


LLM 知识点

1. 模型文件加载策略

一个 DeepSeek V4 Flash 的 2-bit 量化模型约 81GB。如何高效加载它?

传统方式(不适合):

  • fread 把整个文件读入内存 → 81GB 拷贝,启动很慢
  • 只需要部分数据时也要全部读取

mmap 方式(ds4.c 的选择):

  • mmap 立即返回,没有数据拷贝
  • 实际访问某页时才触发 page fault,从磁盘读取
  • Metal GPU 可以零拷贝引用映射区域

懒加载的好处

  • 启动速度快:mmap 调用几乎是瞬间完成
  • 内存按需使用:只有推理用到的权重页面会被加载
  • 共享缓存:操作系统页缓存可以被多个进程共享

2. 权重预热(Weight Warmup)

可选的预热步骤,在推理前主动触碰所有权重页面:

c
static void model_warm_weights(const ds4_model *m) {
    const uint64_t page = sysconf(_SC_PAGESIZE);
    for (uint64_t off = start; off < end; off += page) {
        checksum += p[off];  // 每页读一个字节,强制 page fault
    }
}

为什么要预热?

  • 推理时首次访问某权重页面会触发 page fault → 暂停几十毫秒
  • MoE 模型中,不同 token 路由到不同专家,page fault 时机不可预测
  • 预热把所有 page fault 集中到启动阶段,推理时就不会卡顿

3. KV Cache 的内存布局

KV Cache 存储注意力机制的历史状态,是推理中最大的内存消耗之一。

每个 KV cache 行的大小 = DS4_N_HEAD_DIM × sizeof(float) = 512 × 4 = 2048 字节。

DeepSeek V4 Flash 的 KV cache 非常紧凑(这是选择它的重要原因之一),使得百万级上下文在本地成为可能。

4. 零拷贝张量访问

c
static const void *tensor_data(const ds4_model *m, const ds4_tensor *t) {
    return m->map + t->abs_offset;  // 直接返回 mmap 中的偏移
}

这行代码是整个内存策略的核心:张量数据永远不离开 mmap 区域。没有拷贝,没有额外的缓冲区。推理引擎的指针直接指向文件映射中的位置。


总结:ds4.c 的内存策略

核心理念:在热路径上零分配,把所有内存操作集中到初始化阶段

3. GGUF 二进制格式

模型以 GGUF 二进制文件分发。不理解文件格式,就不知道 81GB 里装了什么、怎么找到每一层权重。今天学习二进制解析、struct 对齐和"尽早失败"的验证策略。

C 知识点

1. struct 内存布局与对齐

C 编译器会在结构体成员之间插入"填充字节"(padding),确保每个成员正确对齐:

c
// 量化块结构体 `block_q2_K`(约 126-151 行)
typedef struct {
    uint8_t  scales[QK_K / 16];   // 16 字节,偏移 0
    uint8_t  qs[QK_K / 4];        // 64 字节,偏移 16
    uint16_t d;                    // 2 字节,偏移 80
    uint16_t dmin;                 // 2 字节,偏移 82
} block_q2_K;                      // 总计 84 字节

对齐规则:

  • uint8_t:1 字节对齐(任意地址)
  • uint16_t:2 字节对齐(偶数地址)
  • uint32_t:4 字节对齐(4 的倍数地址)
  • float:4 字节对齐
  • uint64_t:8 字节对齐

block_q2_K 中,字段顺序经过精心安排,没有浪费填充空间。84 字节恰好被 static_assert 验证:

c
DS4_STATIC_ASSERT(ds4_block_q2_k_size, sizeof(block_q2_K) == 84);

2. static_assert — 编译时断言

ds4.c 用一个巧妙的技巧实现编译时断言 DS4_STATIC_ASSERT(约 153 行):

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

原理:

  • 如果 cond 为真,声明 typedef char name[1] — 合法
  • 如果 cond 为假,声明 typedef char name[-1] — 非法,编译报错

这是 C99 中的 portable static assert。C11 引入了标准的 _Static_assert,但 ds4.c 用 -std=c99 编译。

3. 二进制解析 — Cursor 模式

ds4.c 用 cursor(游标)模式解析 GGUF 二进制文件:

c
typedef struct {
    const uint8_t *base;    // mmap 基地址
    uint64_t size;          // 总大小
    uint64_t pos;           // 当前位置
    char error[256];        // 错误信息
} ds4_cursor;

核心操作:

c
// 检查是否有 n 字节可读
static bool cursor_has(ds4_cursor *c, uint64_t n) {
    if (n > c->size || c->pos > c->size - n) {
        cursor_error(c, "truncated GGUF file");
        return false;
    }
    return true;
}

// 读 n 字节到 dst,推进位置
static bool cursor_read(ds4_cursor *c, void *dst, uint64_t n) {
    if (!cursor_has(c, n)) return false;
    memcpy(dst, c->base + c->pos, (size_t)n);
    c->pos += n;
    return true;
}

// 读一个 u32
static bool cursor_u32(ds4_cursor *c, uint32_t *v) {
    return cursor_read(c, v, sizeof(*v));
}

// 读一个字符串(先读长度,再读内容)
static bool cursor_string(ds4_cursor *c, ds4_str *s) {
    uint64_t len;
    if (!cursor_u64(c, &len)) return false;
    if (!cursor_has(c, len)) return false;
    s->ptr = (const char *)(c->base + c->pos);
    s->len = len;
    c->pos += len;
    return true;
}

注意 cursor_string 返回的 ds4_str 是一个非拥有引用——s->ptr 直接指向 mmap 内部,没有拷贝。

4. 字节序(Endianness)

ds4.c 不做字节序转换。所有 cursor_readmemcpy 直接拷贝到原生类型。

c
memcpy(dst, c->base + c->pos, (size_t)n);

这意味着 ds4.c 假设运行平台是小端序(little-endian)。GGUF 格式也定义为小端序。Apple Silicon(ARM)和 Intel x86 都是小端序,所以这个假设在目标平台上成立。

5. ds4_str — 轻量字符串视图

c
typedef struct {
    const char *ptr;   // 指向 mmap 中的字符串数据
    uint64_t len;      // 字符串长度
} ds4_str;

不同于 C 标准库的以 \0 结尾的字符串,ds4_str 用长度字段标记边界。好处:

  • 不需要拷贝/分配内存
  • 可以包含 \0 字符
  • O(1) 获取长度

LLM 知识点

1. GGUF 文件格式

GGUF 是一种二进制模型文件格式,结构如下:

关键设计:

  • 头部在前:先读头部,再根据头部信息定位数据
  • 元数据与数据分离:元数据(模型名称、参数等)在前面,张量数据在后面
  • 相对偏移:张量目录存储相对于 tensor_data_pos 的偏移,ds4.c 在解析时转为绝对偏移

2. 量化块格式

模型权重不需要 32 位浮点数精度。量化用更少的位存储权重,节省内存:

格式每权重位数块大小用途
F32321024 字节/256 权重未量化(参考)
Q8_K8292 字节/256 权重高精度量化
Q4_K4144 字节/256 权重中等量化
Q2_K~2.684 字节/256 权重激进量化
IQ2_XXS~2.0666 字节/256 权重超激进量化

每个量化块(block)包含 256 个权重值(QK_K = 256):

c
// Q2_K:每个 block 有 84 字节
typedef struct {
    uint8_t  scales[16];    // 缩放因子:16 组,每组 1 字节
    uint8_t  qs[64];        // 量化权重:256 个值,每个 2 bit(64 字节 = 256×2/8)
    uint16_t d;             // 全局缩放
    uint16_t dmin;          // 全局最小值偏移
} block_q2_K;

3. 模型验证 — "早失败"原则

ds4.c 读取模型元数据后,会严格验证每一个参数:

c
config_expect_u32("embedding_length", n_embd, DS4_N_EMBD);  // 必须是 4096
config_expect_u32("expert_count", n_expert, DS4_N_EXPERT);   // 必须是 256
config_expect_u32("attention.head_count", n_head, DS4_N_HEAD); // 必须是 64
// ... 30 多个参数全部检查 ...

如果任何一个参数不匹配,程序立即退出。这体现了"早失败"原则:

  • 比在推理时产生错误结果好得多
  • 比静默使用错误的模型参数好得多
  • 错误信息明确指出哪个参数不匹配

4. 张量(Tensor)概念

张量是多维数组的数学术语。在 LLM 中:

维度名称例子
0D标量学习率
1D向量偏置项 bias
2D矩阵权重矩阵 W
3D+张量MoE 专家权重 [256专家, 2048, 4096]

ds4.c 中的张量结构体支持最多 8 维:

c
typedef struct {
    ds4_str name;
    uint32_t ndim;           // 维度数
    uint64_t dim[DS4_MAX_DIMS]; // 每维大小
    uint32_t type;           // 数据类型(F32/Q2_K/Q4_K 等)
    uint64_t abs_offset;     // 在 mmap 中的绝对偏移
    uint64_t elements;       // 总元素数(dim[0]×dim[1]×...)
    uint64_t bytes;          // 在文件中占用的字节数
} ds4_tensor;