编程 ds4 深度解析:当 Redis 之父用 C 语言手写 AI 推理引擎——从「窄而深」哲学到把 284B 模型塞进一台 MacBook 的技术全拆解

2026-06-12 18:19:08 +0800 CST views 13

ds4 深度解析:当 Redis 之父用 C 语言手写 AI 推理引擎——从「窄而深」哲学到把 284B 模型塞进一台 MacBook 的技术全拆解

一、背景:一个写 Redis 的人为什么开始写 AI 推理引擎

2026 年 5 月 7 日深夜,Salvatore Sanfilippo(antirez)在 X 上发了一条推文,甩出一个 GitHub 仓库:github.com/antirez/ds4——一个为 DeepSeek V4 Flash 量身打造的本地推理引擎,纯 C 语言实现,Metal 专属,目标平台是高端 Mac。

对于后端工程师来说,antirez 这个名字不需要介绍。2009 年他用 C 写出第一版 Redis,亲自带了 11 年,把一个缓存服务做成了今天全球几乎所有后端系统都绕不过去的基础设施。2020 年他从 Redis 一线退下来去写小说、玩业余项目,2024 年底又以 "Redis evangelist" 的身份回归社区。而这次回归之后他挑的新方向,竟然是 AI 推理引擎。

一条推文,22 小时 6.2 万浏览,评论区聚集了 GGML 作者 Georgi Gerganov、Node.js 核心贡献者 Matteo Collina 等一票熟面孔。Collina 评论说:"如果你用的是 128GB 的 Mac,本地 AI 时代已经接近尾声。"

这项目到底做了什么,让一群开源大佬如此兴奋?

二、「窄而深」:一个反常识的工程决策

2.1 它不是什么

ds4 不是又一个 llama.cpp。

在 README 里,antirez 把项目的定位说得非常明确:它不是通用的 GGUF 运行器,不是 llama.cpp 的 wrapper,也不是任何现有推理运行时的前端。它是一个完全自包含(self-contained)的、只服务一个模型族的专用推理引擎

整个代码主路径只干一件事:跑 DeepSeek V4 Flash 的 Metal 计算图。专门处理它的加载逻辑、prompt 模板、KV 状态和 server API。不多不少。

2.2 「窄而深」的哲学

antirez 在 README 里写了一段话,我觉得可以作为这个项目的核心哲学:

"这项目故意押一个很窄的赌注:一次只做一个模型,以官方 logits 向量做正确性回归,做长上下文测试,做足够的 Agent 集成来确认它真的能工作。模型可能随着时间换,但约束不变——在高端个人机(96/128GB 起)上做可信的本地推理。"

这段话透露了几个关键信息:

  1. 一次只做一个模型:和 llama.cpp 走「支持所有模型」的广度路线相反
  2. 官方 logits 回归:用 DeepSeek 官方推理实现的输出做逐 token 对比验证
  3. 做到能用为止:不只是能生成文字,而是能在编码 Agent 场景下实际干活

2.3 为什么是 DeepSeek V4 Flash

antirez 选这个模型不是随机的。他在 README 里列了七八条理由:

特性为什么重要
MoE 架构,激活参数远小于总参数推理快,284B 总量但实际激活的参数少
思考链长度自适应不开 max thinking 时,思考长度与问题复杂度正比,简单题不浪费 token
100 万 token 上下文窗口可以塞进整个代码库,塞进多个长文档
284B 参数的「知识广度」问边缘话题明显比小尺寸密集模型存货多
KV cache 被压得极小长上下文能本地跑,而且可以存到磁盘上
2-bit 量化下表现不崩用特殊的非对称量化方式处理后,128GB MacBook 就能跑

三、技术架构:三层分离,零依赖

3.1 整体架构

ds4 的架构非常清晰,可以分为三层:

┌─────────────────────────────────────────────────────────┐
│                    应用层                                │
│   ds4_cli.c (REPL)  ds4_web.c (WebUI)  ds4_server.c     │
│                                          + ds4_agent.c   │
├─────────────────────────────────────────────────────────┤
│                    公开 API 层 (ds4.h)                   │
│   ds4_engine (已加载模型,只读,线程安全)                 │
│   ds4_session (推理会话,可变,每个用户一个)             │
│   disk KV: ds4_kvstore.h                                │
├─────────────────────────────────────────────────────────┤
│                    核心引擎层                            │
│   ds4.c (GGUF 解析 · Tokenizer · CPU 参考 · 图调度)     │
│   ds4_distributed.c (多机分布式推理)                     │
├─────────────────────────────────────────────────────────┤
│                    GPU 后端层                            │
│   ds4_metal.m + metal/*.metal    ds4_cuda.cu             │
└─────────────────────────────────────────────────────────┘

关键设计决策:公开 API 层不包含任何张量内部知识。这意味着:

  • ds4_cli.c、ds4_server.c、ds4_agent.c 这三个上层应用不包含任何模型特定代码
  • 模型从 Flash 升级到 Flash-2 或 PRO-2,只需要改 ds4.c + GPU 后端,上层零改动
  • 公开 API 可以独立版本化(通过 model_id 检查 KV 兼容性)

3.2 engine 与 session 的职责划分

antirez 在 ds4.h 开头写了一句核心注释:

"The CLI and server should treat ds4_engine as the loaded model and ds4_session as one mutable inference timeline. Keep this header narrow so HTTP/CLI code does not depend on tensor internals."

这个设计把「模型」和「会话」分离开来:

ds4_engine(只读,可多 session 共享)     ds4_session(可变,每个用户一个)
────────────────────────────────────────────────────────────────────
GGUF 文件 mmap + 元数据解析               活跃 KV 缓存(RAM 中)
Tokenizer 权重                             当前 logits 向量
模型形状信息(layer 数、d_head 等)       当前位置 pos
Metal/CUDA 图模板(编译一次复用)          采样 RNG 状态
量化反量化内核                            与磁盘 KV 交互的句柄
MTP draft 模型(如果启用)                会话级配置(温度、top-p 等)
// ds4.h 的核心数据结构

typedef struct ds4_engine ds4_engine;   // 加载后的模型,不可变,线程安全
typedef struct ds4_session ds4_session;  // 一个推理时间线

// ===== engine 生命周期 =====
ds4_engine *ds4_engine_open(const char *model_path, 
                             ds4_engine_params *params);
void ds4_engine_close(ds4_engine *e);

// ===== session 生命周期 =====
ds4_session *ds4_session_create(ds4_engine *e);
void ds4_session_destroy(ds4_session *s);

// ===== 核心推理循环 =====
int ds4_session_sync(ds4_session *s, const ds4_tokens *prompt);
int ds4_session_sample(ds4_session *s);
int ds4_session_eval(ds4_session *s, ds4_token token);

// ===== 序列化 =====
int ds4_session_save_payload(ds4_session *s, FILE *fp);
int ds4_session_load_payload(ds4_session *s, FILE *fp,
                              ds4_engine *e, ds4_session_params *params);

3.3 一次对话的完整路径

用户输入 prompt(文本)
        │
        ▼ ds4_tokenize_text()
token 序列
        │
        ▼ ds4_session_sync() ← 自动找最长公共前缀 + 磁盘 KV 恢复
推理状态对齐到 prompt 末尾
        │
        ├─ 循环:
        │   ds4_session_sample() ← 基于当前 logits 采样下一个 token
        │   ds4_session_eval(token) ← 前向传播
        │   输出 token 给用户
        │   检查 EOS / max_tokens
        │
        ▼ ds4_kvstore_store_live_prefix() ← 保存 KV 检查点到磁盘
结束

ds4_session_sync() 是一个设计得非常漂亮的关键函数——它接收一个完整 prompt 的 token 序列,然后自动做 diff:

  1. 如果会话的当前位置已经是 prompt 的前缀,只跑后缀部分
  2. 如果磁盘 KV 缓存中有这个 prompt 前缀的 checkpoint,从磁盘恢复后只跑后缀
  3. 如果都没有,从零重建

这个设计让调用方(server/agent)完全不需要关心「上下文增量更新」——上层只要传入完整 prompt,引擎自己会求出需要重跑的部分。

四、四大核心创新

4.1 磁盘 KV 缓存:把 KV Cache 当一等公民

这是整个项目最具颠覆性的设计决策。

传统观念里有一条铁律:KV 缓存必须在显存或内存里,磁盘太慢了。Transformer 推理的每步生成都要查 KV 缓存,磁盘 I/O 延迟是毫秒级的,而 GPU 计算是微秒级的,差距三个数量级。

但 antirez 的思路是反过来的:DeepSeek V4 Flash 的 KV cache 被压缩得极小,加上现代 MacBook 的 SSD 读速是 7-10 GB/s(Apple Silicon 的统一内存架构让 NVMe 延迟也远低于传统 x86)。磁盘应该被视为一等公民。

在 AGENT.md 中,项目明确了这一条设计目标:

"Make long local agent sessions practical through live KV reuse and disk KV checkpoints."

(通过活 KV 复用和磁盘 KV 检查点,让长时间的本地 Agent 会话变得实际可行。)

磁盘 KV 存储的完整结构

// 每个 checkpoint 文件的固定头(48 字节)
Magic "DSV4"            // 魔数
Version (uint8)         // 格式版本
Model ID (uint8)        // Flash=0, PRO=1
Quant bits (uint8)      // 量化位数
Reason (uint8)          // 保存原因:cold / continued / evict / shutdown
Token count (uint32)    // checkpoint 包含的 token 数
Hit count (uint32)      // 命中次数(LRU 加权用)
Context size (uint32)   // 创建时的 ctx 大小
Created at (uint64)     // 创建时间,Unix 毫秒
Last used (uint64)      // 最后访问时间
Payload bytes (uint64)  // KV 负载大小
Text bytes (uint64)     // 文本 prompt 大小
File size (uint64)      // 文件总大小
Extended flags (uint8)  // thinking 可见性、tool map、session title 等
→ 随后是每个 layer 的压缩 KV payload
→ 可选 trailer:对话文本、thinking 状态、tool map

核心算法:prompt 前缀匹配

ds4_kvstore 的关键能力是 ds4_kvstore_find_text_prefix()——给定一段 prompt 的 token 序列,在所有已有的 checkpoint 中找到最长的可匹配前缀。

// 简化版核心逻辑
int ds4_kvstore_find_text_prefix(
    ds4_kvstore *store,
    const ds4_tokens *prompt,
    ds4_kvstore_entry **best_match
) {
    // 1. 计算 prompt 的 SHA1
    // 2. 查找是否有同名 checkpoint 文件
    // 3. 如果有,对比确认 token 序列匹配
    // 4. 返回最长匹配的 entry
    
    // 如果没有完全匹配,可以用前缀搜索:
    // - 从最长的可能前缀开始,逐步缩短
    // - 找到第一个有 checkpoint 的匹配长度
    // - 这是「最优」恢复点
}

四个保存时机

Server 在以下四个时机自动写入 checkpoint:

时机触发条件用途
冷启动长 prompt 处理到稳定前缀后,生成开始前保证下次同样 prompt 直接恢复
持续保存prefill 或生成推进超过 N 个 token长时间对话不丢失进度
驱逐前内存中的活跃 KV cache 要被新会话顶替LRU 回收,保留可恢复状态
干净退出服务正常关闭所有会话状态完整保留

LRU 淘汰策略

// ds4_kvstore 的淘汰逻辑(简化)
void ds4_kvstore_maybe_evict(ds4_kvstore *store, uint64_t budget_bytes) {
    uint64_t total_size = 0;
    // 遍历所有条目,计算总大小
    for (entry in store->entries) {
        total_size += entry->file_size;
    }
    
    while (total_size > budget_bytes) {
        // 按「频率衰减分数」排序:半衰期 6 小时
        // score = hits * exp(-lambda * age)
        // age = (now - last_used)
        // lambda = ln(2) / 21600  (6 小时 = 21600 秒)
        
        ds4_kvstore_entry *victim = find_lowest_score(store);
        unlink(victim->path);  // 删除文件
        total_size -= victim->file_size;
        remove_from_index(victim);
    }
}

这不是简单的纯 LRU,而是频率加权 LRU——被命中次数多的 checkpoint 获得更高的生存分数,即使它可能「最近一次使用时间」比别的 checkpoint 更久。

为什么用 SHA1 而不是文本做 key

antirez 特意在文档里说明了这个设计选择:key 是 token ID 序列的 SHA1 哈希,而不是原始文本字符串。

原因很实际:如果用户把「请帮我重构 auth_service.py」改一个字变成「请帮我重构 auth_service.py 吧」,文本变了但 token 序列的前缀可能完全一致。用 token 序列做 key 可以最大化命中率。

为什么用普通 read/write 而不是 mmap

另一个有意思的选择:checkpoint 文件用普通的 read/write I/O,而不是 mmap。

原因是:模型 GGUF 文件已经通过 mmap 占用了大量虚拟地址空间,如果 checkpoint 也用 mmap,虚拟地址空间碎片化会加剧。而且 checkpoint 文件通常很小(一个 32K token 的 checkpoint 只有几十 MB),普通 read/write 的额外开销可以忽略。

4.2 非对称 2-bit 量化:只量化「可以被牺牲」的部分

DeepSeek V4 Flash 有 284B 参数,全精度超过 500GB。想让它在 128GB 的消费级设备上跑,量化是唯一出路。但通用 2-bit 量化对质量损伤严重——这是整个本地推理社区都知道的难题。

antirez 的解法不是发明新的量化算法,而是用了一个非常聪明的非对称量化策略

模型组件                         量化方式                  理由
─────────────────────────────────────────────────────────────────
Routed MoE Experts               IQ2_XXS / Q2_K (2-bit)   数量最多,每 token 只激活少数
  ├─ Up / Gate 投影              IQ2_XXS
  └─ Down 投影                   Q2_K

Shared Experts                   保留高精度 (4-bit+)      对所有 token 都执行,质量敏感
Attention 权重                   不量化                   骨架,不能省
Projections                      不量化
Routing 模块                     不量化

配合 imatrix(重要性矩阵)——一个在模型校准集上测量每一层权重对输出 logits 敏感度的矩阵——ds4 对「更重要」的权重分配更多量化比特,对「不重要」的权重分配更少。

这听起来简单,但实际操作需要:

  1. 在官方校准数据集上跑完整的 forward,记录每个权重的梯度敏感度
  2. 用敏感度矩阵指导量化器的 bit 分配决策
  3. 和官方 logits 向量做逐 token 对比,验证量化后模型的行为没有漂移
# imatrix 构建流程(简化,来自项目中的 build_ds4_imatrix_dataset.py)
def build_imatrix(model, calibration_prompts):
    """收集每个权重的输出敏感度矩阵"""
    activations = {}  # layer_name -> [activation_batch]
    
    for prompt in calibration_prompts:
        tokens = tokenize(prompt)
        # 在每层的输出位置挂钩子
        with hook_outputs(model, layers=ALL_LAYERS) as hooks:
            output = model.forward(tokens)
            for name, activation in hooks.items():
                if name not in activations:
                    activations[name] = []
                activations[name].append(activation)
    
    # 对每个 tensor,计算其对 logits 的 Fisher 信息矩阵
    imatrix = {}
    for name, acts in activations.items():
        # Fisher = E[∇_θ log p(y|x) · ∇_θ log p(y|x)^T]
        # 用对角近似(每参数独立)
        grads = compute_gradient_wrt_weights(name, acts)
        imatrix[name] = (grads ** 2).mean(axis=0)  # 对角近似
    
    return imatrix

实际效果:Flash q2-imatrix 版本(约 90GB)可以在 96-128 GB 机器上跑,在 coding agent 场景下质量几乎不退化。antirez 原话:"这套 2-bit 量化不是开玩笑,能稳定跑 Coding Agent、可靠地调用工具。"

4.3 纯 mmap 模型加载:不解压不拷贝

ds4 的内存管理策略直接用的 OS 能力:

// ds4.c 中的模型加载逻辑(简化)
ds4_engine *ds4_engine_open(const char *path, ds4_engine_params *params) {
    int fd = open(path, O_RDONLY);
    struct stat st;
    fstat(fd, &st);
    
    // 直接把整个 GGUF 文件映射进进程虚拟地址空间
    // 不做任何拷贝,不做任何预解压
    void *mapped = mmap(NULL, st.st_size, PROT_READ, 
                        MAP_PRIVATE | MAP_NORESERVE, fd, 0);
    
    // 解析 GGUF 头部元数据(只读,不碰 tensor 数据)
    gguf_header *header = parse_gguf_header(mapped);
    
    // 为每个 tensor 记录它在 mmap 区域中的偏移和大小
    // GPU 内核直接从 mmap 区域读取权重
    for (tensor in header->tensors) {
        register_tensor_offset(engine, tensor->name, 
                               mapped + tensor->offset, tensor->size);
    }
    
    // Metal 图预编译(只做一次)
    engine->metal_graph = compile_metal_graph(engine);
    
    return engine;
}

几个关键点:

  • 不 eager load:GPU 后端只在实际执行计算时,才从 mmap 区域中读取需要的 tensor
  • OS page cache 管理:常用的 layer 自然留在物理内存中,不常用的被 OS 自动换出到 disk cache
  • 多进程共享:多个 worker 可以用 MAP_SHARED 映射同一份 GGUF 文件,不重复占用物理内存

实际效果:

  • 冷启动速度极快——一个 ~90GB 的模型,几秒钟内就绪
  • 实际 RSS(物理内存占用)可以比模型大小小很多——「工作集」随上下文增长逐渐增加
  • 不像量化引擎那样需要 30 秒到 2 分钟的模型加载时间

4.4 Session 抽象:把推理变成可回放的时间线

ds4 的 session 抽象设计得非常优雅。核心思想是:把一次推理会话当作一个可回放、可恢复、可对比的时间线

最关键的 API 是 ds4_session_sync()

// 概念上的行为(实际实现更复杂)
int ds4_session_sync(ds4_session *s, const ds4_tokens *full_prompt) {
    ds4_tokens *current_prefix = ds4_session_current_tokens(s);
    
    // 1. 找最长公共前缀
    int common_len = longest_common_prefix(current_prefix, full_prompt);
    
    if (common_len == full_prompt->len) {
        // 完全匹配,什么都不用做
        return DS4_OK;
    }
    
    if (common_len > 0 && common_len == current_prefix->len) {
        // prompt 变长了(append),只跑新增的 suffix
        ds4_tokens *suffix = token_slice(full_prompt, common_len, -1);
        int ret = ds4_session_eval_tokens(s, suffix);
        return ret;
    }
    
    // 2. 尝试从磁盘 KV 恢复
    ds4_kvstore_entry *match;
    if (ds4_kvstore_find_text_prefix(s->kvstore, full_prompt, &match)) {
        ds4_session_load_payload(s, fopen(match->path, "r"), ...);
        match->hits++;  // 增加命中计数
        
        // 只跑 checkpoint 之后的部分
        ds4_tokens *suffix = token_slice(full_prompt, match->tokens, -1);
        return ds4_session_eval_tokens(s, suffix);
    }
    
    // 3. 用户修改了 prompt 中间的某条消息
    // 回退到 common_len,重新跑后面部分
    ds4_session_rewind_to(s, common_len);
    ds4_tokens *suffix = token_slice(full_prompt, common_len, -1);
    return ds4_session_eval_tokens(s, suffix);
}

这个设计对 Agent 场景的价值非常具体。假设你正在用 Claude Code 接 ds4 做本地编码:

  1. Claude Code 每次请求都带着上一轮的所有上下文(初始 prompt 动辄 25000 token)
  2. 第一次跑必然慢——prefill 需要处理全部 25000 token
  3. 但第二次请求时,前缀 24980 token 和上次完全一样
  4. ds4_session_sync() 发现前缀没变,直接跳过 prefill,只跑新增的 20 个 token
  5. 第三次、第四次……同理

如果没有这个设计,每次 Claude Code 调用 ds4 都要从零跑 25000+ token 的 prefill——这对本地引擎来说是不可接受的。

4.5 方向引导(Directional Steering):给模型加方向盘

这是一个实验性功能,位于 dir-steering/ 目录下。核心思想:不是通过 prompt engineering(「请简洁回答」)来引导模型,而是直接在激活层面操作。

工作流程:
1. 收集两组 prompt:
   - 正样本:「请简洁回答:1+1=?」「用一句话总结 C 语言的优点」...
   - 负样本:「请详细说明 1+1 为什么等于 2」「用 500 字解释 C 语言的优点」...
2. 在相同的中间层收集两组的激活向量
3. 计算差异向量(方向向量),存为 .f32 文件(约 700KB / 176K float32)
4. 推理时,在指定层的输出上加上 steering_attn × direction_vector
# 使用示例:让模型回答更简洁
./ds4 --directional-steering-file dir-steering/out/verbosity.f32 \
      --directional-steering-attn 0.5 \
      --directional-steering-ffn 0.2

这个功能的实用性在于:有些模型「对齐过度」,表现为过度礼貌、过度谨慎、过度啰嗦。通过方向引导,可以在不改变 prompt 的情况下反转某些行为倾向。这是一种模型级别的行为调控,比 prompt engineering 更底层、更可控。

五、代码架构:一个文件一个职责

5.1 文件结构总览

ds4/
├── ds4.c                    # 🔴 核心推理引擎 (~25K 行 C)
├── ds4.h                    # 🔴 公开 API 边界 (~400 行)
├── ds4_cli.c                # 🟢 命令行 REPL (~63KB)
├── ds4_server.c             # 🟢 HTTP 服务器 (~598KB)
├── ds4_agent.c              # 🟢 内置 Coding Agent (~387KB)
├── ds4_web.c                # 🟡 浏览器端聊天界面 (~49KB)
├── ds4_metal.m              # 🔵 Metal 后端 (~1.1MB Obj-C)
├── metal/                   # 🔵 Metal compute kernels
│   ├── attention.metal
│   ├── moe_routing.metal
│   ├── rms_norm.metal
│   └── quant_dequant.metal
├── ds4_cuda.cu              # 🔵 CUDA 后端 (~512KB)
├── ds4_kvstore.c/.h         # 🟡 磁盘 KV 缓存层
├── ds4_distributed.c/.h     # 🟡 分布式推理
├── ds4_eval.c               # 🧪 评估与回归测试 (~194KB)
├── ds4_bench.c              # 📊 基准测试
├── gguf-tools/              # ⚙️ 离线模型工具链
│   ├── deepseek4-quantize.c # 专用量化工具
│   └── imatrix/             # 重要性矩阵数据
├── dir-steering/            # 🧭 方向引导实验
└── Makefile                 # 🔧 构建 (~24KB)

每个文件是单文件的——antirez 坚持了他一贯的风格(Redis 早期就是单文件 C 程序)。没有复杂的分包、没有模块系统、没有依赖管理。

5.2 ds4_server.c:一个文件实现完整的 LLM API Server

这个 598KB 的单文件是 ds4 项目中除了引擎核心外最大的文件,它包含了:

  • 自制 HTTP/1.1 解析器:不依赖 libcurl、libmicrohttpd、nghttp2
  • 自制 JSON 解析与生成:标准 C99,无第三方 JSON 库
  • 自制工作队列:线程池 + 任务队列,每个请求独立 session
  • SSE 流式输出:Server-Sent Events,支持 stream_options
  • 双协议兼容:OpenAI 和 Anthropic 协议都可以接

支持的端点:

GET  /v1/models              — 列出可用模型
POST /v1/chat/completions     — OpenAI 兼容的 chat completion
POST /v1/completions          — OpenAI 兼容的 completion(legacy)
POST /v1/messages             — Anthropic 兼容的消息接口

API 适配的核心是工具调用格式转换

// ds4_server.c 中的工具调用翻译逻辑(概念层)
// OpenAI → DSML(喂给模型)
json_to_dsml_tools(openai_tools) {
    // OpenAI tools: [{"type":"function","function":{"name":"read_file",...}}]
    // 转换为 DSML 格式在 system prompt 中注入
    // <|tool_schema|>{"name":"read_file","description":"...","parameters":{}}<|/tool_schema|>
}

// DSML → OpenAI(模型输出后翻译回)
dsml_to_openai_tool_calls(dsml_output) {
    // 模型生成: <|tool_calls|>[{"name":"read_file","arguments":{...}}]<|/tool_calls|>
    // 翻译回 OpenAI 格式:
    // {
    //   "role": "assistant",
    //   "tool_calls": [{
    //     "id": "call_xxx",
    //     "type": "function",
    //     "function": {"name": "read_file", "arguments": "{...}"}
    //   }]
    // }
}

antirez 在文档里坦白了一个限制:当前 server 不做并发请求的 batch,所有推理串行通过一个 Metal worker。多个并发请求只能排队。一个人用没问题,多人共享要慎重。但考虑到这是零依赖 C 语言实现,这个 trade-off 是合理的。

5.3 ds4_agent.c:内置的离线编码助手

ds4_agent.c(387KB)实现了一个可以在本地完全离线运行的 coding agent:

// 支持的 Agent 操作(简化版)
typedef struct {
    void (*view_files)(const char *pattern);      // 读取文件 + glob 支持
    void (*edit_file)(const char *path, ...);      // 插入/编辑/重写
    void (*run_command)(const char *cmd,            // 执行 shell 命令
                        int timeout_sec,
                        int max_output_bytes);
    void (*list_directory)(const char *path);       // 列出目录
    void (*grep_code)(const char *pattern,          // 代码搜索
                      const char *path);
} ds4_agent_tools;

// Agent 状态机
typedef enum {
    DS4_AGENT_READY,        // 等待用户输入
    DS4_AGENT_THINKING,     // 模型正在思考/推理
    DS4_AGENT_TOOL_CALL,    // 正在执行工具调用
    DS4_AGENT_WAITING_USER, // 等待用户确认(如删除操作)
} ds4_agent_state;

它的定位是「简单够用」,不是「全面超越 Cursor/Windsurf」——目标是让你在完全离线的情况下也能做 AI 编码。Agent 可以读文件、编辑代码、运行 shell 命令、理解项目结构,所有上下文通过 KV 缓存累积,不丢失历史。

六、性能数据与实际部署

6.1 官方基准测试

以下数据来自 antirez 自己的测试(--ctx 32768、--nothink、greedy 采样、-n 256):

机器量化Prompt 长度Prefill 速度生成速度
M3 Max 128GBq2short58.52 t/s26.68 t/s
M3 Max 128GBq211,709250.11 t/s21.47 t/s
M5 Max 128GBq2short87.25 t/s34.27 t/s
M5 Max 128GBq211,707463.44 t/s25.90 t/s
M3 Ultra 512GBq212,018468.03 t/s27.39 t/s
M3 Ultra 512GBq412,018448.82 t/s26.62 t/s
M3 Ultra 512GBPRO q232,768138.82 t/s9.56 t/s
DGX Spark GB10q27,047343.81 t/s13.75 t/s

几个反直觉的观察:

1. 长 prompt 的 prefill 比短 prompt 更快。
短 prompt(几个 token)prefill 速度 58-87 t/s,长 prompt(11000+ token)反而飙升到 250-468 t/s。原因是图执行引擎在大批量输入时的 GPU 利用率更高——同一张 Metal 图,输入 buffer 越大,GPU 核心利用率越充分。

2. q4 生成速度和 q2 几乎一样。
M3 Ultra 上,q2 生成 27.39 t/s,q4 生成 26.62 t/s,差异不到 3%。原因是在生成阶段,瓶颈主要是激活参数的路由和计算,不是权重读取。量化更多节省的是内存占用,而非生成速度。

3. NVIDIA DGX Spark 反而比 Apple Silicon 慢。
DGX Spark GB10 在 q2 下生成速度只有 13.75 t/s,而同样配置的 Mac 上能到 26-34 t/s。说明 Metal 路径对这个特定模型/量化方案的优化远强于 CUDA 路径——这也印证了 antirez「主战场在 Mac」的定位。

6.2 分布式推理

ds4 支持多机分布式推理。以两台 M5 Max 128GB 通过 Thunderbolt 5 连接为例:

Prompt 长度单机参考双机分布式加速比
9,421 tokens421.70 t/s582.22 t/s1.38×
28,684 tokens405.30 t/s674.16 t/s1.66×
63,819 tokens353.62 t/s654.79 t/s1.85×

加速比随 prompt 长度增长而提升——因为更长的 prefill 意味着更多的流水线级可以利用。

分布式的工作原理是:

// 每台机器用 --layers N:M 只加载自己负责的 layer 子集
// 激活张量通过 TCP 在 worker 之间流动
// Worker 0 → Worker 1 → Worker 2 → ... → Coordinator
// Coordinator 拿到最终 logits,采样下一个 token

// 流水线化 prefill:
// Worker N 处理 chunk K 的同时,Worker N+1 处理 chunk K-1
// 吞吐量可以超过单机

但需要注意的是,生成阶段(自回归解码)是串行的,分布式会引入跨机器延迟。所以生成阶段反而比单机慢约 20%。分布式主要适用于:(a) 跑单机放不下的大模型;(b) 加速长 prefill。

6.3 Claude Code 接入实战

这是 ds4 最实用的场景之一。README 给了完整的配置示例:

#!/bin/bash
# claude-code-ds4.sh — 让 Claude Code 跑在本地 ds4 上

export ANTHROPIC_BASE_URL="http://localhost:8000"
export ANTHROPIC_API_KEY="not-needed"

# 启动 ds4 server
./ds4-server \
  -m ds4flash-q2-imatrix.gguf \
  --ctx 100000 \
  --kv-cache-dir ~/.ds4-kv \
  --kv-cache-space-mb 8192 \
  --host 127.0.0.1 \
  --port 8000 &

# Claude Code 对话
claude "帮我重构 src/auth 模块,支持 JWT + OAuth2"

配置要点:

  1. ANTHROPIC_BASE_URL 指向本地的 ds4 server(Anthropic 兼容 /v1/messages)
  2. API key 随便填(server 不校验)
  3. Claude Code 会通过 /v1/messages 接口和 ds4 交互
  4. ds4 把 Anthropic 格式转换为 DeepSeek 原生格式执行推理
  5. 每次请求自动复用磁盘 KV 缓存中的前缀

6.4 快速上手指南

# Step 1:下载模型(约 90GB,需要能访问 HuggingFace)
./download_model.sh q2-imatrix        # 日常使用推荐
# ./download_model.sh q2-q4-imatrix   # 更高质量
# ./download_model.sh q4-imatrix      # 需要 256GB+ 内存
# ./download_model.sh pro-q2-imatrix  # 需要 512GB 机器

# Step 2:编译(根据平台选择)
make                   # macOS Metal(推荐)
make cuda-spark        # Linux + DGX Spark / GB10
make cuda-generic      # Linux + 普通 CUDA GPU
make cpu               # Linux CPU-only(仅调试,macOS 勿用!)

# Step 3:启动 CLI
./ds4 -m ds4flash.gguf --ctx 32768

# Step 4:或启动 HTTP Server
./ds4-server -m ds4flash.gguf --host 127.0.0.1 --port 8080

# Step 5:用 curl 测试
curl http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"ds4","messages":[{"role":"user","content":"用 Python 写一个冒泡排序"}]}'

七、设计哲学:antirez 的工程风格

7.1 零依赖

整个 ds4 项目没有使用任何第三方 C 库。HTTP 解析自己写,JSON 解析自己写,工作队列自己写,SSE 自己写。这在 2026 年的开源生态中几乎是一种「行为艺术」——大多数人会选择 libcurl + json-c + pthreads,但 antirez 选择了全部从零实现。

这不是技术洁癖,而是一种工程判断:「零依赖」带来的收益在这个场景下大于成本:

  • 启动时间:不需要动态链接十几个 .so/.dylib
  • 部署复杂度:一个二进制文件,scp 到服务器就能跑
  • 调试可控性:所有代码在自己的掌控范围内,不依赖上游版本兼容性
  • 安全面:攻击面 = 自己的代码 + 系统调用,没有供应链风险

7.2 「承认 AI 写的代码」

ds4 README 里有一句话值得单独摘出来:

"这个软件在 GPT 5.5 的强力辅助下开发,由人类主导想法、测试和调试。我们公开说明这一点,因为它塑造了项目的构建方式。如果你不喜欢 AI 写的代码,那这个软件不适合你。"

对 antirez 这种以亲手写代码闻名的工程师来说,公开承认大量代码出自 AI 协作,分量比一般项目要重得多。这不仅是一种透明态度,更是一种立场表态:AI 工具本质上是放大你能力的杠杆,不是替代你的替代品

7.3 对前置工作的尊重

ds4 的 LICENSE 文件保留了 GGML 作者的版权声明,README 里专门开了一节 "Acknowledgements to llama.cpp and GGML"。

一位叫 lifcc 的网友在 antirez 的推文下评论道:

"GGML 的影响力非常惊人,今年每一个新出来的推理引擎,本质上都继承了一个免费的 C++ 优化栈,「影子贡献者」名单远远超过实际贡献者。"

antirez 回复:

"我同意 GGML 的影响。但关于影子贡献者名单这件事,我没办法在 DS4 的 README 里把每个名字都列出来。我意识到这是个问题,Redis 也有同样的情况,但具体要怎么解决,并不明显。"

这段对话戳到了开源生态的一个普遍困境——「站在巨人肩膀上」的项目,怎么把巨人的名字写全?一个新推理引擎可以不依赖 GGML 库本身,但仍然大量参考了 GGML 的实现思路——量化方案、kernel 设计、GGUF 格式规范。这种潜在的知识继承很难在传统的 LICENSE 文件里完全体现。

antirez 至少保持了端正要写致谢,态度比「砍掉 BSD 头注释悄悄重写一遍」要体面得多。

7.4 「窄而深」vs「广而浅」

我把 ds4 和 llama.cpp 做一组对比:

                    llama.cpp                    ds4
──────────────────────────────────────────────────────────
目标                支持所有模型                   只支持一个模型族
量化方案            通用量化(Q2-Q8)              模型专用的非对称量化
正确性验证          community report             官方 logits 逐 token 回归
GPU 优化            通用 kernel                  每算子针对模型张量形态调优
Agent 集成          通过第三方                    内置 coding agent + 磁盘 KV
依赖                GGML 库                      零依赖(GGML 知识继承)
代码风格            C++(现代 C++ 特性)           纯 C11 + Obj-C
主要平台            全平台                        macOS(主)+ Linux(CUDA)

这两条路线各有各的价值。llama.cpp 的广度路线让每个人都能在本地跑任意开源模型,这是不可替代的生态价值。但 ds4 的深度路线也证明了:当你不需要考虑通用性时,很多工程设计可以变得非常简洁和高效

八、适用场景与局限

8.1 适合 ds4 的用户

用户画像为什么适合推荐配置
MacBook Pro 128GB 拥有者目前唯一在消费级笔记本上「原生」跑 284B MoE 的引擎Flash q2-imatrix
注重隐私的开发者全离线,代码/文档/内部信息不离开机器Flash q2-imatrix + coding agent
DeepSeek 深度用户把 DeepSeek API 的能力移植到本地离线场景Flash q4-imatrix
研究人员直接看 Metal/CUDA 源码 + 官方 logits 回归Flash 任意 + --trace
Mac Studio 512GB 用户跑 PRO q2-imatrix,本地接近 frontier 级PRO q2-imatrix
多机协同工作场景多台 MacBook 同网络下跑更大模型Flash q4 分布式 / PRO q4 分布式

8.2 不适合的用户

用户画像为什么不适合替代方案
没有 96GB+ 内存的用户96GB 是最低实用门槛ollama / llama.cpp + 小模型
需要多模型切换ds4 只跑 DeepSeek V4 系列ollama / llama.cpp / vLLM
需要 GUI IDE 集成没有原生 VSCode/Cursor 插件通过 API 桥接,体验有折扣
Windows 用户Metal 后端不可用,CUDA 可用但非主目标等 Windows CUDA 成熟度提升
追求最快推理速度通用引擎对 FlashAttention 等积累更多vLLM / TensorRT-LLM + H100

8.3 典型工作流

开发者日常编码助手

# 第一天
$ ./ds4-server -m ds4flash.gguf --kv-cache-dir ~/.ds4-kv
# → Server listening on 127.0.0.1:8080

# IDE 配置为 http://localhost:8080
# "给我重构 auth_service.py,让它支持 OAuth2"
# → 模型本地推理,Agent 读文件 → 修改 → shell 运行测试
# → KV 检查点自动保存

# 第二天
$ ./ds4-server -m ds4flash.gguf --kv-cache-dir ~/.ds4-kv
# → 发现昨天的 prompt 前缀匹配,跳过已处理的 25000 token
# "昨天的问题,我想加一个 rate limiter"
# → 只跑新增的 15 个 token,秒出结果

九、总结与展望

9.1 三条启示

第一,「窄而深」是一种被低估的工程策略。 在一个「做得更多」成为默认选择的世界里,antirez 选择了「做得更少但更好」。ds4 的成功不是因为技术上有任何「黑科技」(量化、Metal 内核、KV 缓存全都是已知技术),而是因为它敢于只为一个模型写引擎,把所有注意力都集中在一条窄路上。

第二,磁盘 KV 缓存是本地推理的 Game Changer。 把 KV cache 从「内存贵宾」降级为「磁盘一等公民」,这个决定让「长时间 Agent 会话」从「能用」变成了「好用」。对编码助手类场景来说,这意味着每次对话补 25000+ token 的上下文不是一次性开销,而是一次跑完永久复用。

第三,单人项目 + AI 协作的组合拳正在改写「什么级别的项目需要多大团队」的计算公式。 antirez 用 AI 辅助写出了一个能在 production 级设备上跑 284B 模型的推理引擎——包含 Metal 内核、HTTP server、编码 Agent、分布式推理——代码量几十万行。这在 2024 年之前是不可想象的。

9.2 值得关注的方向

antirez 在 README 末尾留了一句话:"模型会随生态演进而变。"

这意味着 ds4 的「窄」也可能变成「动态的窄」——不是永远绑定 DeepSeek V4 Flash,而是在每个时间点选一个最有价值的模型做深度优化。下次换模型可能是 V4 Flash-2、PRO-2、甚至完全另一个模型族。

从更广的视角看,ds4 代表的一种趋势:专业化推理引擎。不是去和 vLLM / llama.cpp 比通用性,而是在特定模型 + 特定硬件的组合上做到极致。这种思路可能会催生更多类似项目:

  • 「这个引擎专为 Qwen 3 优化,在 Apple M6 上跑」
  • 「那个引擎专为 Gemma 4 优化,在 NVIDIA Jetson 上跑」

通用框架和专用引擎不是替代关系,而是互补关系。当通用框架解决了「有没有」的问题后,专用引擎来解决「好不好」的问题。

9.3 ds4 真正的意义

ds4 不是要取代 llama.cpp,也不是要在跑分上压谁一头。它更像一个范式实验:当一个世界级的系统软件工程师(Redis 之父)决定不做通用框架,专心把一个具体模型在一类具体硬件上做到极致,最后会得到什么。

答案大致是这样:

  • 一个 README 里写得清清楚楚自己边界在哪的项目
  • 一个把磁盘当 KV cache 一等公民的设计
  • 一个同时兼容 OpenAI 和 Anthropic 协议的零依赖 HTTP server
  • 一份对前置项目(GGML、llama.cpp)态度恭敬的致谢清单
  • 对自己 AI 协作开发方式的公开承认

如果你有一台 128GB 的 MacBook Pro 或 Mac Studio,ds4 是你在本地跑 284B MoE 模型的最优解之一。如果不是——这个项目的源代码和设计文档本身就是最好的系统编程教材。去看看 antirez 怎么用 C 语言组织一个单文件 25K 行的推理引擎,怎么设计 API 边界,怎么处理 KV 缓存序列化——这些工程决策本身,比任何技术文章都更有价值。


项目地址: https://github.com/antirez/ds4

技术栈: C (核心引擎) + Objective-C (Metal) + CUDA C++ (NVIDIA GPU)

许可证: MIT License

最低硬件: Apple Silicon Mac (M3 Max+) 96GB+ 内存

推荐文章

维护网站维护费一年多少钱?
2024-11-19 08:05:52 +0800 CST
js迭代器
2024-11-19 07:49:47 +0800 CST
使用 Nginx 获取客户端真实 IP
2024-11-18 14:51:58 +0800 CST
10个极其有用的前端库
2024-11-19 09:41:20 +0800 CST
imap_open绕过exec禁用的脚本
2024-11-17 05:01:58 +0800 CST
CSS实现亚克力和磨砂玻璃效果
2024-11-18 01:21:20 +0800 CST
windows下mysql使用source导入数据
2024-11-17 05:03:50 +0800 CST
程序员茄子在线接单