目录

ds4.c:DeepSeek V4 Flash 本地推理实现

ds4.c 是 antirez 为 DeepSeek V4 Flash 编写的专用本地推理引擎。它不追求通用 GGUF 兼容,而是把模型权重、量化方案、Metal 执行路径、KV 状态管理和 HTTP 接口放在同一组工程假设里实现。整套实现都围绕单模型收紧边界。

为什么 ds4.c 不做通用框架

README 开头先把边界写清楚:它不是通用 GGUF 运行器,不是别的运行时外面的壳,也不追求框架化。项目核心是一条 DeepSeek V4 Flash 专用的 Metal 图执行路径,外加 prompt 渲染、KV 状态管理、HTTP API 兼容层,以及围绕这个模型写定的一组工程前提。

README 给出的理由包括:

  • 模型激活参数更少,推理速度更适合高端个人设备。
  • 常规 thinking 模式下,思考段通常比很多其他模型短得多,而且长度更接近问题复杂度。
  • 上下文窗口达到 100 万 token。
  • KV 缓存压缩能力很强,足以让磁盘持久化成为现实策略,而不只是实验性功能。

ds4.c 并不是在否认 llama.cpp 的价值。README 的原意恰好相反:项目不直接链接 GGML,但明确承认自己建立在 llama.cpp 打开的路径上,包括 GGUF 生态、量化格式、部分实现经验和验证方法。它不是 llama.cpp 的二次封装,而是沿着这套经验为单一模型重写了一条专用路径。

先把三个对象分清楚:engine、session 和 server

把 ds4.c 笼统称作“推理引擎”,并不能说明它的内部结构。按头文件和 server 注释,可以分成下面四层:

核心对象负责什么
模型层ds4_engine持有已加载模型、图执行配置、可选的 MTP 支持与后端能力
会话层ds4_session表示一条可变推理时间线,保存 live KV、token 序列和下一 token 的 logits
服务层ds4-server解析请求、维护任务队列、决定何时写入或恢复磁盘 KV cache
适配层CLI 与 HTTP 请求处理渲染 prompt、做 tokenization、映射 thinking 选项和工具调用协议

README 和 ds4_server.c 都写到,server 只维护一个 live 的图状态和 KV checkpoint。HTTP 层可以并发接收请求,但真正的推理会被串行送进单个 Metal worker。当前 ds4-server 的并发模型更接近“多客户端排队,共用一条会话时间线”,而不是“多请求并行推理”。

执行链路:

request -> prompt rendering/tokenization -> ds4_session_sync
        -> Metal prefill/decode -> sampling 或工具调用映射
        -> SSE / JSON response

在这套分层里,磁盘 KV cache 不是附加功能,而是单 live session 架构的延伸。

磁盘 KV cache 的目标:复用稳定前缀

把磁盘 KV cache 理解成“长上下文附带能力”并不准确。它首先服务于 agent 客户端的无状态请求模式。

Claude Code、opencode、Pi 这一类客户端,通常会在每次请求时把整段对话重新发给服务端。对服务端来说,这意味着即使只改了最后几行,也可能要从 token 0 开始重新 prefill。ds4-server 的处理方式不是保存抽象的“聊天历史”,而是保存某个稳定前缀对应的完整会话状态。下次只要 token 前缀一致,就可以从 checkpoint 恢复,跳过重复 prefill。

README 对内存缓存和磁盘缓存的分工是:

  • 内存里的 live checkpoint 负责当前活跃会话。
  • 磁盘 KV cache 负责不同会话切换时的恢复,以及服务重启后的续跑。

这也是为什么缓存键不是原始文本,而是 token ID 序列的 SHA1。对模型来说,决定能否复用的是 token 前缀是否一致,而不是字符串是否相似。KVC 文件里会保留可读的 prompt 文本,但它只用于排障和人工检查,不参与匹配。

KVC 文件里实际存了什么

磁盘缓存文件的外层结构并不复杂,但信息量很高:

  • 固定 48 字节头部:包含 magic、版本号、量化 bit 数、保存原因、token 数、命中数、上下文大小、时间戳和 payload 字节数。
  • 渲染后的文本片段:仅供观察,不是 key。
  • DS4 session payload:包括 checkpoint 的 token IDs、下一 token 的 float32 logits、每层压缩行数,以及 raw/compressed/indexer 三类 KV 相关张量状态。

把 logits 一起保存后,恢复过程可以直接从同一份下一 token 分布继续,而不用额外做一次 decode。与之对应,MTP 的 draft state 并不会持久化,恢复后需要重新建立。

另一个容易被忽略的细节是,缓存恢复使用普通 read/write I/O,而不是 mmap。原因是进程本来就已经映射了一个很大的 GGUF,再额外依赖 mmap 恢复 cache,只会增加虚拟内存映射负担。

为什么还要 trim 和对齐

源码注释里对这个问题解释得很到位。Tokenizer 可能会在 prompt 边界附近发生合并,如果把“刚好到末尾”的 token 前缀直接存盘,后面追加一点文本时,重新 tokenize 的结果未必还能保持原前缀不变。于是 ds4-server 在 cold save 时会做两件事:

  • 故意裁掉一小段尾部 token,默认是 32 个。
  • 向下对齐到 prefill chunk 边界,默认是 2048 token。

前者是为了降低 BPE 边界重分词导致的复用失败,后者则让磁盘 checkpoint 和实际 chunked prefill 的完成点保持一致。这里优先保证复用成功率,而不是把缓存长度尽量做满。

四种写盘时机

ds4-server 会在四个时刻写入缓存:

时机含义
cold长首 prompt 到达稳定前缀后,在生成前先存一份可复用 checkpoint
continued活跃会话继续增长到配置间隔后,增量保存最新进度
evict新请求要替换当前 live session 时,先把旧会话写走
shutdown服务器正常退出时保存最终快照

默认情况下,2-bit 和 4-bit routed expert 量化版本之间可以共享同一前缀的缓存,只要 token 前缀一致。如果你更看重严格隔离,可以加 --kv-cache-reject-different-quant 禁用这种跨量化复用。

推理路径:围绕状态复用组织

从流程上看,ds4.c 仍然是 prefill 和 decode 两段。不同之处在于,这两个阶段都围绕状态复用来组织:哪些结果可以复用,checkpoint 在哪里切分,哪些状态必须和前缀保持一致,源码里都写得比较明确。

Prefill 采用 chunked 方式推进,这让长 prompt 的工作集更稳定,也让磁盘 checkpoint 更容易落在固定边界上。Decode 阶段则围绕 live session 的 raw KV、compressed KV 与 indexer 状态继续前进,Metal 内核按当前 token 位置扫描这些状态完成注意力计算。

如果只看外部接口,ds4.c 并不复杂;它和通用框架的差别主要体现在这些约束上:session 可以回放,checkpoint 可以落盘,prompt 前缀可以比较,服务端只维护一条活跃时间线。

MTP 目前还是实验性加速

ds4.c 支持可选的 MTP GGUF,用于 speculative decoding。README 对它的表述是:这是一个 correctness-gated、目前最多只带来轻微收益的实验性路径。

具体到实现,可以注意三点:

  • correctness-gated:draft token 必须通过验证才会被接受。
  • slight speedup:当前收益有限。
  • 主要针对贪婪解码:非贪婪采样下,收益有限。

因此,MTP 可以作为实现细节介绍,但不宜写成当前版本的核心亮点。

Thinking、工具调用与接口兼容

ds4-server 同时暴露 OpenAI 风格和 Anthropic 风格接口。关键不在字段兼容本身,而在于它把工具调用和 thinking 模式都做了针对 DeepSeek 的协议映射。

服务端支持的主要端点包括:

  • GET /v1/models
  • GET /v1/models/deepseek-v4-flash
  • POST /v1/chat/completions
  • POST /v1/completions
  • POST /v1/messages

工具调用走的是一条双向转换链:OpenAI tool schema 或 Anthropic tool blocks 会被转换成 DeepSeek 的 DSML 表达形式,模型输出的 DSML tool call 再被映射回客户端使用的结构。对 agent 来说,兼容通常断在协议细节,而不是 HTTP 路径本身。

Thinking 模式还有几条边界:

  • 服务端默认是常规 thinking 模式。
  • reasoning_effort=max 只有在 --ctx 至少达到 393216 时才会进入 Think Max。
  • 上下文不够大时,请求会自动回落到普通 thinking,而不是硬上 Think Max。
  • thinking: {type: "disabled"}think: false,或者模型别名 deepseek-chat 都会切到 non-thinking。

还有一个实现细节:thinking 模式下,客户端传来的采样旋钮会按官方 API 的行为被忽略;工具调用场景还会额外强制更稳定的采样设置。ds4-server 在这里追求的是与官方服务行为对齐,而不是暴露尽可能多的自由度。

Agent 客户端接入

README 给出了三类客户端的接入方式:

客户端走哪条接口文章里真正值得关心的点
opencodeOpenAI-compatible适合验证 OpenAI 生态下的本地 provider 替换
PiOpenAI-compatible额外需要处理 DeepSeek 风格的 thinking 兼容字段
Claude CodeAnthropic-compatible初始系统提示很长,更能放大磁盘 KV cache 的收益

这组配置说明,agent 兼容性在 ds4.c 里不是附带项。README 甚至直接提供了 Claude Code wrapper 的环境变量示例,关注点也不是 curl 是否能通,而是长 prompt、工具调用和多轮续跑是否稳定。

量化不是附属优化,而是和模型包绑定的设计前提

在 ds4.c 里,量化和模型包是一起设计的。README 讲得很清楚,这不是任意 GGUF 的通用加载器;张量布局、量化混合方式、元数据和可选 MTP 状态,都是按这一套引擎预期制作的。

当前官方脚本给出的三个下载目标分别是:

  • q2:约 81 GB,面向 128 GB 内存机器。
  • q4:约 153 GB,面向 256 GB 及以上机器。
  • mtp:约 3.5 GB,作为可选 speculative decoding 组件。

q2 使用的是非对称量化:只有 routed MoE experts 被激进压缩,上行和门控使用 IQ2_XXS,下行使用 Q2_K,其余共享 experts、投影和路由相关组件保持不变。README 对这一路线的评价也比较收敛,不是说 2-bit 一定最好,而是说这种量化在 coding agent 场景里“works well”,工具调用依然可靠。

因此,ds4.c 的量化更像是和目标工作流一起设计出来的工程折中,而不是单纯追求更小的模型文件。

官方向量回归如何约束实现

ds4.c 没把“模型能出字”当成完成条件。仓库里的 tests/test-vectors 保存了来自官方 DeepSeek V4 Flash API 的短上下文和长上下文 continuation 向量,本地则可以用 ./ds4 --dump-logprobs 生成对应结果,再按 token bytes 与 top-logprobs 做比对。

它约束的不是模糊的体感,而是 tokenizer、prompt template、attention 路径这些最容易在本地推理里漂移的环节。很多项目在首个 prompt 跑通后就算阶段性完成,ds4.c 则把回归检测前移到更靠近官方行为的层面。对一个只支持单模型的专用引擎来说,这种验证方式比继续增加参数开关更有用。

性能数据

README 给出了单次 Metal CLI 测试数据,测试条件是 --ctx 32768 --nothink --greedy -n 256。这组数据有参考价值,但它说明的是指定硬件和指定参数下的表现,不是普适 benchmark。

机器量化PromptPrefillGeneration
MacBook Pro M3 Max 128 GBq2short58.52 t/s26.68 t/s
MacBook Pro M3 Max 128 GBq211709 tokens250.11 t/s21.47 t/s
Mac Studio M3 Ultra 512 GBq2short84.43 t/s36.86 t/s
Mac Studio M3 Ultra 512 GBq211709 tokens468.03 t/s27.39 t/s
Mac Studio M3 Ultra 512 GBq4short78.95 t/s35.50 t/s
Mac Studio M3 Ultra 512 GBq412018 tokens448.82 t/s26.62 t/s

这组数字至少给出两个结论:

  • 128 GB 的 Apple Silicon 机器已经能承载 q2 这条主路径。
  • q4 依旧是更大内存机器的选项,不是“顺手就能升级”的通用档位。

当前限制

ds4.c 当前的限制并不隐晦,README 和源码都把这些限制写了出来:

  • 它是 Metal-only 的 server;--cpu 和通用 --backend 在 server 模式下会被直接拒绝。
  • CPU 路径主要用于 correctness check,而且 README 明确警告当前 macOS 的虚拟内存问题可能导致 kernel panic。
  • 服务器只保留一个 live session,不做独立请求批处理;并发请求会排队。
  • MTP 仍是实验性路径,不应被当成成熟加速方案。
  • 整个项目仍处于 alpha 质量阶段,离通用产品还有距离。

这些限制和前面的设计方向一致:优先收紧核心路径,而不是先扩展支持面。

结语

ds4.c 还远不是成熟的通用方案,但它提供了一条明确的路线:把模型包、执行路径、状态复用、接口兼容和回归验证放在一起设计。

这里能看到三点取向:

  • 推理引擎要和模型包一起设计。
  • 会话状态要能复用、能持久化、能在 agent 场景里回本。
  • 评估标准不能停在跑通首个 prompt,而要包括官方向量验证、长上下文行为和工具调用兼容性。

很多项目的目标是把模型跑起来;ds4.c 的目标更具体:让同一个模型在固定硬件、固定权重包和固定工作流里形成一条稳定路径。因此,它更像一条窄而深的实现路线,而不是一个尽量扩展支持面的框架。

参考资料