Skip to main content

ds4 深度理解分析

理解验证状态

核心概念自我解释理解“为什么”应用迁移状态
专用 DS4 推理引擎已掌握
ds4_engine / ds4_session 边界已掌握
mmap-backed GGUF 加载已掌握
Metal whole-model graph已掌握
压缩 KV + 磁盘 KV cache已掌握
OpenAI/Anthropic API 适配已掌握
DSML tool call 精确回放已掌握

项目完整地图

完整目录树

ds4/
├── AGENT.md
├── README.md
├── Makefile
├── ds4.h
├── ds4.c
├── ds4_metal.h
├── ds4_metal.m
├── ds4_cli.c
├── ds4_server.c
├── linenoise.c / linenoise.h
├── rax.c / rax.h / rax_malloc.h
├── download_model.sh
├── metal/
│ ├── dense.metal
│ ├── flash_attn.metal
│ ├── moe.metal
│ ├── dsv4_kv.metal
│ ├── dsv4_rope.metal
│ ├── dsv4_misc.metal
│ ├── dsv4_hc.metal
│ ├── norm.metal
│ └── argsort/bin/concat/cpy/get_rows/glu/repeat/set_rows/softmax/sum_rows/unary.metal
└── tests/
├── ds4_test.c
├── long_context_security_prompt.txt
└── test-vectors/
├── README.md
├── manifest.json
├── official.vec
├── official/*.official.json
└── prompts/*.txt

文件清单(分类)

类别文件路径职责摘要
公共边界ds4.h对 CLI/server 暴露窄 API:engine、session、tokenize、chat、KV payload
核心引擎ds4.cGGUF mmap、tokenizer、权重绑定、CPU reference、Metal graph 调度、session/KV payload
Metal C APIds4_metal.h纯 C 可调用的 Metal tensor、command、kernel wrapper 接口
Metal runtimeds4_metal.mObjective-C Metal 设备、pipeline、mmap view、command buffer、tensor 生命周期
Metal kernelsmetal/*.metaldense、attention、MoE、RoPE、KV、norm、copy 等 GPU compute kernels
CLIds4_cli.cone-shot 生成、REPL、多轮 transcript、think mode、token printer
Serverds4_server.cOpenAI/Anthropic HTTP API、SSE、worker 队列、磁盘 KV cache、tool memory
测试tests/ds4_test.cMetal kernel、长上下文、官方向量、tool-call、server parser/cache 单测
构建MakefileDarwin 下链接 Foundation/Metal,非 Darwin 编译 DS4_NO_METAL CPU/reference 版本

入口文件 + 核心调用链

  • CLI 路径:main()parse_options()ds4_engine_open()run_generation()run_repl()ds4_session_sync() / ds4_session_eval()
  • Server 路径:client thread 解析 HTTP → enqueue()worker_main()generate_job()ds4_session_sync() / ds4_session_sample() / SSE 输出。
  • 引擎路径:ds4_engine_open()model_open() → metadata/tensor parse → weights_bind()weights_validate_layout() → Metal graph/session 创建。
  • Metal 路径:ds4.c 调用 ds4_metal_* wrapper → ds4_metal.m 选择 pipeline/command buffer → metal/*.metal kernel 执行。

1. 快速概览

这个项目是一个极窄、极专用的本地推理引擎:它只服务 DeepSeek V4 Flash,不试图成为通用 GGUF runner。README 在开头就把边界说得很清楚:它不是框架,不是包装另一个 runtime,而是 DS4-specific 的 Metal graph executor,配套 DS4-specific loading、prompt rendering、KV state 和 server API glue。

技术栈很朴素但目标很明确:核心是 C99,只有 Metal 必须使用 Objective-C 的地方才写 ds4_metal.m,GPU kernel 放在 metal/*.metal。这种选择的 WHY 很实际:C 让模型图调度、mmap、内存生命周期更直接;Objective-C 只承担 Metal runtime glue,避免把业务逻辑扩散到 ObjC;Metal kernel 独立存放,方便针对 Apple GPU 做性能优化。

项目最重要的设计关键词是“专用”。它不追求加载任意模型,而是把 DeepSeek V4 Flash 的固定结构写进代码:层数、embedding 维度、MoE 专家布局、压缩 KV 比例、indexer、MTP 权重、tokenizer 细节都被严格验证。这样做牺牲了通用性,但换来两个好处:第一,推理路径能写成 whole-model graph,不需要动态 tensor graph 框架;第二,发现模型文件不匹配时尽早失败,而不是跑到一半产生不可解释的 logits drift。

从系统形态看,它是三层架构:底层是 Metal runtime 和 kernels,中层是 ds4_engine / ds4_session 推理状态机,上层是 CLI 和 HTTP server。上层完全不碰 tensor 内部,只通过 ds4.h 操作“模型”和“会话”。这就是整个项目能保持可读性的关键:复杂的模型机制集中在引擎,API/CLI 只处理协议和交互。

2. 背景与动机(3 个 WHY)

问题本质

要解决的问题: 在 Mac / Mac Studio 这类高内存 Apple Silicon 机器上,让 DeepSeek V4 Flash 变成一个可长期本地使用的推理服务,而不只是“能跑一次 demo”。

WHY 需要解决: DeepSeek V4 Flash 的模型规模很大,但它有几个对本地推理非常有吸引力的特点:少量 active parameters、超长上下文、压缩 KV cache、2-bit 专家量化可用。如果只用通用 runner,很容易停留在“能加载、能生成”的层面,却难以把超长上下文、工具调用、agent API、磁盘 KV cache、官方 logits 验证做成一个稳定闭环。这个项目的动机就是把这些拼起来,让本地模型对 coding agent 真正可用。

方案选择

WHY 选择专用引擎: 专用引擎能把 DS4 的结构固化为常量和固定 kernel 调用链,不需要运行时构造通用计算图。这对 Metal 性能和可调试性都很重要,因为 Apple GPU 上大模型推理的瓶颈经常不是单个算子,而是 command 提交、buffer view、KV 布局、prefill/decode 切换这些系统工程细节。专用路径还让 layout validation 可以非常严格,模型文件稍有不匹配就停止,而不是产生“看起来能跑但质量漂移”的危险状态。

替代方案对比:

  • 通用 GGUF runner:优点是模型覆盖广,缺点是难以针对 DS4 的压缩 KV、indexer、MTP、tool replay 做极致定制。这个项目不选它,是因为目标不是跑很多模型,而是让一个模型在本地 agent 场景下跑得可信。
  • 直接 fork llama.cpp:优点是生态成熟,缺点是专用实验会被通用框架复杂度淹没。这个项目借鉴 GGML/GGUF/quant/kernel 经验,但保持独立窄边界,便于快速验证 DS4-specific 的长上下文和磁盘 KV 思路。
  • 纯 CPU 或纯高层语言实现:调试更简单,但对 284B 级模型不现实。CPU 路径在这里被明确定位为 reference/debug,生产路径是 Metal whole-model graph。

应用场景

适用场景: 高内存 Apple Silicon 机器上的本地 coding agent、长上下文问答、需要 OpenAI/Anthropic 兼容 API 的本地模型服务。WHY 适用,是因为 DS4 的压缩 KV 和 Apple 本地 SSD/统一内存组合能支撑长上下文复用,而 server 侧的磁盘 KV cache 又把“反复发送增长 transcript”的 agent 模式变得可承受。

不适用场景: 任意 GGUF 模型加载、多模型 batch serving、跨 GPU 后端、生产级多租户服务。WHY 不适用,是因为整个项目刻意选择单模型、单 worker、单 live session 的结构;这些限制不是疏漏,而是为了保证 KV 状态、Metal graph、工具调用 canonical replay 都可控。

3. 核心概念网络

概念 1:专用模型运行时

  • 是什么: 这个运行时只识别 DeepSeek V4 Flash 的 GGUF layout、metadata 和 tensor 命名。weights_validate_layout() 会逐层检查固定维度和 tensor type,而不是接受任意 shape。
  • WHY 需要: 大模型推理最怕“静默错”:权重 shape 能勉强读出来,但某个 layout 或 tokenizer 细节不同,最后 logits 偏一点,生成质量崩掉。专用运行时用严格校验把错误提前到启动阶段。
  • WHY 这样实现: 代码把 tensor name 绑定到 ds4_weights / ds4_layer_weights 字段,之后图调度直接访问语义字段。这比每次按字符串查找更快,也让后续代码读起来像模型结构,而不是一堆 map lookup。
  • WHY 不用其他方式: 通用 graph engine 能减少硬编码,但会引入动态 shape、动态 op dispatch 和更多抽象层。这里为了一个模型的性能和可解释性,硬编码是合理交换。

概念 2:ds4_engineds4_session

  • 是什么: ds4_engine 是“加载好的模型”,包含 GGUF map、词表、权重、backend、MTP 配置;ds4_session 是“一条推理时间线”,包含 Metal graph、checkpoint tokens、logits、KV 状态和进度回调。
  • WHY 需要: Server 和 REPL 都是多轮场景。模型加载应该只做一次,而每段对话的 KV/cache/logits 会随着 token 流推进。把二者拆开,才能让上层复用模型,同时清晰管理可变状态。
  • WHY 这样实现: ds4.h 明确要求 CLI/server 把 ds4_engine 当 loaded model,把 ds4_session 当 mutable inference timeline。调用方提供完整 token prefix,让 ds4_session_sync() 判断是扩展、重建还是复用。这正好匹配 stateless HTTP client 会反复发送完整 messages 的现实。
  • WHY 不用其他方式: 如果上层直接持有 KV tensor,会破坏边界,HTTP/CLI 需要理解 tensor layout。反过来如果 session 完全无状态,每一轮都从 token 0 prefill,长上下文 agent 几乎不可用。

概念 3:mmap-backed GGUF + no-copy Metal view

  • 是什么: 模型文件通过 mmap 映射,Metal 路径使用 shared mapping,并在 ds4_metal.m 中把 mmap range 包装成 no-copy MTLBuffer view。
  • WHY 需要: 2-bit / 4-bit 大模型文件动辄几十到上百 GB,启动时全量拷贝权重既慢又浪费内存。mmap 让 OS 按需分页,no-copy view 避免 CPU 到 GPU buffer 的重复搬运。
  • WHY 这样实现: Metal runtime 把模型 payload 映射成少量重叠的大 view,确保任意 tensor 能落在一个 view 内。这个设计像把一本巨书分成几张重叠的大书签,查任意章节都不用撕页重装。
  • WHY 不用其他方式: 每个 tensor 一个 MTLBuffer 会产生大量 VM/driver 对象;全量复制到 private GPU buffer 又需要巨大内存。少量重叠 no-copy view 是 Apple unified memory 下更合适的折中。

概念 4:Metal whole-model graph

  • 是什么: 推理不是把每个 token 的每个算子零散交给上层,而是在 ds4.c 里按 DS4 模型结构调度一整套 Metal kernels:prefill、attention、MoE、output head、decode。
  • WHY 需要: 长上下文 prefill 和 decode 的性能瓶颈不同。prefill 要批量吞 prompt,decode 要快速单 token 迭代;whole-model graph 可以在两个阶段采用不同策略。
  • WHY 这样实现: generate_metal_graph_raw_swa() 根据 prompt 长度选择 chunked/layer-major prefill 或 raw SWA prefill,然后循环 decode。session 版本的 ds4_session_sync() 还能在已有 checkpoint 上追加 suffix。
  • WHY 不用其他方式: 通用 eager execution 简单,但 command buffer、scratch、KV frontier 难以整体优化。这里通过专用 graph 把缓存和算子顺序控制在一处。

概念 5:压缩 KV 与磁盘 KV cache

  • 是什么: 模型内部有 raw SWA cache 和压缩 attention/indexer cache;server 还能把 session payload 序列化到磁盘,按 token prefix SHA1 查找并恢复。
  • WHY 需要: 1M token 上下文如果每次从零 prefill,体验不可接受。压缩 KV 降低内存占用,磁盘 KV 利用现代 SSD,把长会话 checkpoint 变成“可恢复资产”。
  • WHY 这样实现: 引擎负责 DS4-specific payload(raw ring、compressed rows、frontiers、logits、tokens),server 负责外层文件 header、evict policy、tool map。这个分工避免 server 理解 Metal tensor 细节。
  • WHY 不用其他方式: 只保存文本 transcript 不能省 prefill;只保存 raw KV 不够,因为压缩 attention 会选整个 prefix 的 compressed rows。payload 必须包含 raw window、compressed cache 和 compressor/indexer 状态。

概念 6:DSML tool call 精确回放

  • 是什么: Server 把 OpenAI/Anthropic tools 渲染成 DeepSeek DSML 格式,模型生成 DSML tool calls 后再映射回 API 原生 tool calls。同时记录模型实际生成的 DSML,用 tool id 在后续请求中精确回放。
  • WHY 需要: Agent 客户端常把上一轮 tool call 以规范 JSON 形式重新发送,但模型实际生成的字节序列可能和 canonical rendering 不完全一致。KV cache 是 token 级精确前缀复用,只要字节差一点,就会 cache miss。
  • WHY 这样实现: tool memory 记录 raw DSML,磁盘 KV cache 也保存 tool-id map。后续 messages 如果引用这些 tool ids,就能恢复模型当时的实际 token 序列,减少语义相同但 token 不同造成的重放失败。
  • WHY 不用其他方式: 单纯 canonicalize 所有 tool call 看似干净,但会要求重写 live graph 过去的后缀。当前代码明确指出:Metal graph 里不只是 token vector,还有 raw/compressed rows 和 frontiers,不能只改 checkpoint tokens。

概念关系矩阵

关系类型概念 A概念 BWHY 这样关联
拥有关系ds4_enginemmap GGUF / weights / vocabengine 是模型生命周期的容器,权重和词表不应该随会话复制
拥有关系ds4_sessionMetal graph / checkpoint / logitssession 是可变推理时间线,KV 和 logits 必须跟 token 位置同步
依赖关系Server/CLIds4.h上层只处理协议和交互,避免理解 tensor/layout
性能关系mmap viewMetal kernelskernels 直接读取 file-backed model weights,减少内存复制
正确性关系tool memoryKV cachetoken prefix 必须字节级一致,工具调用回放要服务 cache 命中
阶段关系prefilldecodeprefill 吞 prompt,decode 生成新 token;两者性能策略不同

4. 算法与理论

算法:Byte-level BPE + JoyAI pre-tokenization

  • 位置: ds4.c:13583ds4.c:13635ds4.c:13791
  • 时间复杂度: 单个 pre-tokenized piece 的 BPE merge 朴素实现接近 O(n²),整体还取决于 piece 数量。空间复杂度 O(n),保存当前 symbol 列表。
  • WHY 选择: 这里追求的是和 DeepSeek V4 Flash tokenizer 字节级一致,而不是 tokenizer 框架的通用性。官方 logits 对齐时,token 边界是第一道门;代码注释特别强调标点和 trailing newline 的 split shape 会影响代码 prompt 的 token stream。
  • WHY 复杂度可接受: 推理成本主要在模型 forward,tokenizer 的 O(n²) piece merge 通常不是瓶颈。更重要的是,长上下文场景里 tokenizer 只在请求进入时跑一次,而后续 KV 复用能抵消大量 prefill 成本。
  • 退化场景: 极长且没有自然 split 的单个 piece 会让 merge 变慢。JoyAI pre-tokenizer 通过数字、CJK、字母、标点、空白等规则拆分,降低这种风险。
  • 参考: GPT-2 byte-level BPE 思路可参考 OpenAI GPT-2 tokenizer;JoyAI 规则由模型 metadata tokenizer.ggml.pre = "joyai-llm" 驱动。

算法:Prefix-based session sync

  • 位置: ds4_session_sync()ds4.c:15885
  • 时间复杂度: prefix 判断 O(min(live_len, prompt_len));命中后只计算 suffix,suffix 短时 O(suffix × decode_cost),suffix 长时 O(suffix × prefill_cost_per_token);miss 时 O(prompt_len × prefill_cost_per_token)。空间复杂度主要由 session graph/KV 固定占用决定。
  • WHY 选择: HTTP/agent 客户端常每次发送完整 transcript,而不是只发增量。prefix sync 让 server 可以接受 stateless API 形态,同时在引擎内部保持 stateful KV。
  • WHY 复杂度可接受: prefix 比较只是整数数组比较,和一次模型 forward 相比很便宜。真正昂贵的是 suffix eval,所以代码还根据 metal_graph_resume_prefill_min_tokens() 在短 suffix decode 和长 suffix chunked prefill 间切换。
  • 退化场景: 如果 prompt canonicalization 导致中间 token 改写,live checkpoint 不能原地重写,只能恢复旧 checkpoint 或从头 prefill。代码通过 ds4_session_rewrite_requires_rebuild() 明确拒绝不安全重写。
  • 参考: 这是增量计算 / memoization 在 autoregressive KV cache 上的应用。

算法:Chunked layer-major prefill

  • 位置: generate_metal_graph_raw_swa()ds4.c:14531metal_graph_prefill_chunked()ds4.c:12829metal_graph_prefill_chunked_range()ds4.c:12672
  • 时间复杂度: 对 prompt token 数近似线性乘以每层 transformer 成本;attention 受 raw SWA 与压缩 KV/indexer 限制,不是朴素全量 O(n²) 存储。空间复杂度由 prefill_cap、raw window、compressed cache 决定。
  • WHY 选择: 长 prompt 无法一次性把所有中间激活都塞进固定 graph buffer。chunking 把 prefill 拆成可承受的窗口,layer-major 顺序让每层缓存按正确 frontier 完成。
  • WHY 复杂度可接受: DS4 的压缩 KV 让长上下文不必保留所有 raw K/V;chunked prefill 把峰值激活内存控制住。代价是实现复杂度变高,需要保证 compressor/indexer rows 的完成顺序和 cold prompt 一致。
  • 退化场景: 如果 chunk 边界处理和绝对位置不一致,会出现 logits drift。代码在 ds4_session_sync() 注释里专门强调 resumed prefill 要对齐 absolute chunk boundaries。
  • 参考: Transformer KV cache 和 chunked prefill 是长上下文推理常见策略,DS4 这里叠加了压缩 attention/indexer。

算法:Disk KV prefix lookup

  • 位置: kv_cache_find_prefix()ds4_server.c:5449kv_cache_try_load_tokens()ds4_server.c:5466
  • 时间复杂度: 查找遍历缓存 entry O(E),每个候选计算对应 prefix SHA1,整体 O(E × prefix_hash_cost)。空间复杂度由磁盘 cache 配额决定。
  • WHY 选择: 直接用 token prefix SHA1 做 key,比用文本更稳定,因为真正决定 KV 可复用的是 token 序列。server 还检查 quant bits 和 context size,避免把不兼容 payload 恢复进当前 runtime。
  • WHY 复杂度可接受: 缓存 entry 数通常由磁盘空间控制,不会无限增长。相比重新 prefill 几万到几十万 token,遍历少量 entry 和读一个 checkpoint 文件非常划算。
  • 退化场景: 如果 tool call canonicalization 造成 token mismatch,即使人看 transcript 一样也会 miss。项目用 tool memory 和 trace diagnostics 专门处理这种情况。
  • 参考: 内容寻址缓存(content-addressed cache)和 LRU/LFU eviction 的组合。

5. 设计模式

模式 1:Facade / Narrow Interface

应用位置: ds4.h:9 明确把 public engine boundary 限定为 ds4_engineds4_session。CLI/server 只 include ds4.h,不触碰 tensor internals。

WHY 使用: 这个项目内部非常复杂:GGUF、Metal、KV、MoE、tool replay 都混在一起。如果上层 API 直接暴露 tensor 或 graph,CLI/server 会快速变成“第二个引擎”。窄接口让上层只关心 prompt、sampling、streaming、HTTP 协议。

WHY 不用会怎样: HTTP server 会开始依赖具体 KV layout,未来改 raw ring 或压缩 cache 时要连协议层一起改。更危险的是,server 可能为了 cache 或 rewrite 直接改 token vector,破坏 Metal graph 内部 frontier。

潜在问题: Facade 太窄时,诊断能力可能不足。这个项目通过 log type、trace diagnostics、payload helpers、top logprobs 等 API 做了有限扩展,避免把内部结构直接暴露。

模式 2:Stateful Session / Unit of Work

应用位置: struct ds4_sessionds4.c:14781ds4_session_sync()ds4.c:15885

WHY 使用: 一次会话就是一条逐 token 推进的时间线,checkpoint、logits、KV、MTP draft 必须同步变化。把它们放进 session,可以保证每次 eval 后状态一致。

WHY 不用会怎样: 如果每个函数散落持有状态,prefill、decode、sample、payload save/load 之间很容易忘记 invalidate 某个字段,比如 MTP draft 仍指向旧 logits。当前代码在 sync/eval 过程中多处显式设置 mtp_draft_valid = false,体现了状态机思路。

潜在问题: 单 session 简化了正确性,但限制并发。Server 选择单 worker 串行推理,就是接受这个限制来保护 live graph。

模式 3:Adapter / Protocol Translation

应用位置: parse_chat_request()parse_anthropic_request()append_dsml_tool_calls_text()parse_generated_message()、SSE 输出函数。

WHY 使用: 模型理解的是 DS4/DSML prompt 形态,客户端期待 OpenAI 或 Anthropic API。Adapter 把协议差异吸收在 server 层,让引擎只接收 rendered prompt tokens。

WHY 不用会怎样: 如果引擎直接支持 OpenAI/Anthropic 结构,就会把 HTTP schema、tool schema、thinking blocks 混进推理核心。那会让 ds4.c 失去专注,也让 CLI/server 难以独立演进。

潜在问题: 协议翻译不是纯语义翻译,还影响 token cache。项目用 tool memory 记录 raw DSML,说明它已经意识到 adapter 的字节级副作用。

模式 4:Single Worker / Actor-like Serialization

应用位置: worker_main() 与 queue 在 ds4_server.c:6607 附近,generate_job()ds4_server.c:6095

WHY 使用: Metal graph 和 session KV 是一个 mutable object,不适合多个请求并发写。单 worker 像 actor 一样独占状态,client thread 只解析请求和等待结果。

WHY 不用会怎样: 多请求同时 eval 同一个 session 会破坏 checkpoint/logits/KV 的一致性。要支持真正并发,必须复制 session/graph 或做 batch scheduler,复杂度和内存占用都会大幅上升。

潜在问题: 独立请求不会 batch,吞吐受限。README 也明确说 concurrent requests wait their turn on the single live graph/session。

6. 关键代码深度解析

核心片段清单(6A)

编号片段名称所在文件:行号优先级识别理由
#1model_open()ds4.c:1176★★★模型 mmap 与 GGUF 解析入口,决定 no-copy 和 CPU/Metal 内存策略
#2weights_validate_layout()ds4.c:2139★★★专用引擎正确性核心,绑定 DS4 固定结构、MoE、压缩 KV、indexer
#3bpe_tokenize_text()ds4.c:13811★★☆tokenizer 字节级一致性影响官方 logits 与长代码 prompt
#4generate_metal_graph_raw_swa()ds4.c:14531★★★one-shot Metal whole graph 推理主流程
#5ds4_session_sync()ds4.c:15885★★★REPL/server KV 复用的核心状态机
#6ds4_session_save_payload()ds4.c:15138★★★磁盘 KV cache 的 DS4-specific payload 序列化核心
#7parse_generated_message()ds4_server.c:2732★★☆DSML tool call 到 API tool call 的关键桥接
#8kv_cache_try_load_tokens()ds4_server.c:5466★★☆磁盘 KV prefix cache 命中与安全恢复入口

跳过说明:

  • linenoise.c:第三方/辅助 REPL 输入,不影响推理设计原理。
  • rax.c:server tool memory / map 辅助结构,可作为依赖理解,不是 DS4 推理核心。
  • 多数 metal/*.metal 小工具 kernel:职责在依赖章节覆盖;逐行展开会稀释设计主线。

片段 #1:model_open()

位置:ds4.c:1176
优先级:★★★
一句话核心:这是模型进入系统的门卫,决定 GGUF 如何被映射、解析,并为后续 Metal no-copy 权重访问打地基。

1.1 代码整体作用

model_open() 做三件事:打开模型文件、把它 mmap 到进程地址空间、解析 GGUF header/metadata/tensor directory。它不是把权重复制到内存,而是保留 file-backed mapping,让后续 tensor payload 通过 offset 直接访问。这个设计对大模型至关重要,因为 DS4 q2/q4 文件非常大,启动时 eager copy 会造成巨额内存和时间成本。

它在系统层次里属于“模型加载器”。上游是 ds4_engine_open(),下游是 weights_bind()weights_validate_layout()、Metal runtime 的 model map view。没有它,后面所有 semantic tensor 字段都没有可靠来源;如果它的 mmap policy 错了,Metal no-copy 或 CPU debug 路径都可能出问题。

1.2 核心逻辑分析

执行流程:

输入 path/backend policy
→ open + fstat
→ 根据 metal_mapping 选择 MAP_SHARED/MAP_PRIVATE
→ mmap
→ 读取 GGUF magic/version/n_tensors/n_kv
→ parse_metadata
→ parse_tensors
→ 可选 CPU prefetch
→ 输出 ds4_model

关键算法/数据结构: ds4_cursor 是 mmap buffer 上的安全游标。WHY 用游标,是因为 GGUF 是二进制格式,任何越界读取都可能把错误延迟到很后面;游标把位置、长度、错误状态集中起来。另一个关键点是 mmap policy:Metal 使用 MAP_SHARED,CPU 使用 MAP_PRIVATE,这不是性能偏好,而是为了绕开 Darwin shared mmap 在巨大映射下的 VM 风险。

核心状态变量:

变量名初始值变化时机终态
m->fd-1open() 成功后模型文件 fd
m->mapNULLmmap() 成功后GGUF 文件映射基址
m->size0fstat()文件大小
m->version0读取 header 后GGUF version,必须为 3
m->n_tensors0读取 header 后tensor 目录项数量
m->n_kv0读取 header 后metadata 数量

多执行路径:

  • 路径 A(Metal 正常): metal_mapping=trueMAP_SHARED → mmap 后可被 ds4_metal_set_model_map_range() 包装成 no-copy buffer → kernel 直接读权重。
  • 路径 B(CPU/debug): metal_mapping=falseMAP_PRIVATE → 如果 prefetch_cpu=true 触摸页 → 避免 Metal shared mapping 的 Darwin VM 问题。

1.3 逐行代码解释

贯穿示例输入:path="ds4flash.gguf"metal_mapping=trueprefetch_cpu=false

/* Open and map the GGUF once.  Metal needs a shared mapping for no-copy
* MTLBuffers; CPU uses a private read-only mapping to avoid Darwin VM stress.
* Tokenizer-only callers pass prefetch_cpu=false so inspecting tokens never
* walks the huge tensor payload. */
static void model_open(ds4_model *m, const char *path, bool metal_mapping,
bool prefetch_cpu) {
memset(m, 0, sizeof(*m));
m->fd = -1;

int fd = open(path, O_RDONLY);
if (fd == -1) ds4_die_errno("cannot open model", path);

struct stat st;
if (fstat(fd, &st) == -1) ds4_die_errno("cannot stat model", path);
if (st.st_size < 32) ds4_die("model file is too small to be GGUF");

/*
* Metal wraps slices of this mapping as no-copy MTLBuffers, so the Metal
* path keeps the file-backed shared mapping. The CPU path only reads the
* weights through normal pointers and should not inherit Metal's VM policy:
* use a private read-only mapping there.
*
* This is deliberately defensive against an OS-level Darwin VM bug observed
* while the CPU backend streams the very large GGUF through a shared mmap:
* the kernel can panic in VM map-count accounting instead of returning a
* normal user-space failure. Keeping CPU inference off the shared mapping
* avoids that VM accounting path while preserving normal file-backed reads.
*/
const int mmap_flags = metal_mapping ? MAP_SHARED : MAP_PRIVATE;
void *map = mmap(NULL, (size_t)st.st_size, PROT_READ, mmap_flags, fd, 0);
if (map == MAP_FAILED) ds4_die_errno("cannot mmap model", path);

m->fd = fd;
m->map = map;
m->size = (uint64_t)st.st_size;

ds4_cursor c = cursor_at(m, 0);
uint32_t magic;
if (!cursor_u32(&c, &magic)) ds4_die(c.error);
if (magic != DS4_GGUF_MAGIC) ds4_die("model is not a GGUF file");
if (!cursor_u32(&c, &m->version)) ds4_die(c.error);
if (!cursor_u64(&c, &m->n_tensors)) ds4_die(c.error);
if (!cursor_u64(&c, &m->n_kv)) ds4_die(c.error);

if (m->version != 3) ds4_die("only GGUF v3 is supported");

parse_metadata(m, &c);
parse_tensors(m, &c);

if (!metal_mapping && prefetch_cpu) model_prefetch_cpu_mapping(m);
}

步骤 1:memsetm->fd=-1 先把输出对象放到可清理状态。WHY 这样做,是因为后续任何一步失败都可能走错误路径;明确 sentinel 值能让 close/unmap 逻辑知道哪些资源已经获得。此时 m 是空模型。

步骤 2:open / fstat 验证文件存在并拿到大小。WHY 先检查 st_size < 32,因为 GGUF header 至少要有 magic/version/counts;太小文件继续解析只会产生误导性错误。此时 fd 有效,st.st_size 是映射范围。

步骤 3:根据 metal_mapping 选择 mmap flags。WHY Metal 需要 MAP_SHARED,是因为 no-copy MTLBuffer 要包住 file-backed shared memory;CPU 不需要这个共享语义,反而可能触发 Darwin VM bug,所以用 MAP_PRIVATE。此时示例里 mmap_flags=MAP_SHARED

步骤 4:mmap 成功后写入 m->fd/map/size。WHY 不复制 tensor,是因为权重体量巨大,后续 tensor 只需要 m->map + abs_offset 就能定位。此时模型文件已经以只读地址空间形式进入进程。

步骤 5:用 cursor_at(m, 0) 从头读 GGUF magic/version/counts。WHY 用 cursor 而不是直接 cast struct,是为了避免端序/对齐/越界问题,并把错误记录在 c.error。此时 m->version/n_tensors/n_kv 已经可信。

步骤 6:要求 version == 3。WHY 不兼容其他版本,是因为 tensor directory 和 metadata 解释规则可能变;项目宁可拒绝启动,也不要把未知格式误读成模型权重。

步骤 7:parse_metadata()parse_tensors() 建立目录表。WHY 先解析目录再绑定权重,是为了把通用 GGUF 结构转换成后续 DS4 semantic binding 的输入。此时 ds4_model 已经有 metadata list、tensor list 和 tensor data offset。

1.4 关键设计点

设计维度分析内容
实现选择mmap 而不是 read/copy,是因为模型权重太大,按需分页更适合本地大模型。Metal/CPU 分别使用 shared/private mapping,是结合 Apple VM 行为做出的工程选择。
性能优化Metal no-copy view 的前提是 shared mmap。这样 kernel 读取权重时避免额外 staging buffer,启动时间和峰值内存都更低。
编译器相关不涉及编译器 IR;这是二进制格式加载和 OS VM 策略。
安全与健壮性检查文件大小、magic、version,并通过 cursor 防越界。失败时直接 ds4_die,符合“模型不匹配就不要继续”的原则。
可扩展性如果未来支持另一个模型,可以复用 GGUF parse 层,但需要新增独立 config/weights validation。当前函数本身是通用 GGUF v3 加载入口。
潜在问题错误处理是 fatal exit,不适合库式嵌入。对 CLI/server 可接受,因为模型加载失败本来就是启动失败。

1.5 完整示例(三组对比)

示例 1 — Metal 正常场景

  • 输入:model_open(&m, "ds4flash.gguf", true, false) → 使用 MAP_SHARED → metadata/tensors parsed → 后续 Metal view 可 no-copy 包装权重。

示例 2 — CPU reference 场景

  • 输入:model_open(&m, "ds4flash.gguf", false, true) → 使用 MAP_PRIVATE → 可选 prefetch → CPU 通过普通指针读取权重,避免 shared mapping 的 VM 风险。

示例 3 — 非 GGUF 文件

  • 输入:model_open(&m, "bad.bin", true, false) → magic 不等于 DS4_GGUF_MAGIC → 立即报错退出。这样不会让后续 tensor binding 在垃圾数据上继续运行。

1.6 使用注意与改进建议

  1. 调用者必须理解 metal_mapping 的语义,不要为了“统一”把 CPU 也改成 shared mapping。这里的分支背后是实际遇到过的 Darwin VM 风险,随意合并会把系统稳定性问题带回来。
  2. 不要在 tokenizer-only 路径启用 full prefetch。模型文件非常大,单纯检查 tokenization 不应该触摸所有 tensor 页,否则会让轻量命令变成巨型 I/O 操作。

可考虑的改进: 把 fatal error 改造成错误码返回会更适合库化。WHY 更好,是因为 server 可以把模型加载错误转换成结构化日志或 API 状态;但这会增加大量调用点错误处理复杂度,当前 CLI/server 程序式定位下 fatal exit 仍然合理。

片段 #2:weights_validate_layout()

位置:ds4.c:2139
优先级:★★★
一句话核心:这是“专用引擎”四个字落地的地方——它把 GGUF tensor 逐个证明为 DeepSeek V4 Flash 需要的形状。

2.1 代码整体作用

weights_validate_layout() 在权重绑定后运行,用固定常量检查 token embedding、output head、每层 attention、compressor、indexer、MoE routed experts、shared experts 等 tensor 的 type 和维度。它像进机场的安检:只有完全符合 DS4 预期的 GGUF 才能进入推理路径。

系统层次上,它位于模型加载和推理图调度之间。上游 weights_bind() 把 tensor name 绑定到语义字段,下游 Metal graph 假设这些字段的 layout 完全正确。没有这个函数,后续 kernel 可能用错误维度读取 mmap 权重,轻则 logits drift,重则 buffer 越界或 GPU validation error。

2.2 核心逻辑分析

执行流程:

输入 ds4_weights
→ 计算派生维度 hc_dim / hc_mix_dim / q_dim / out_low_dim
→ 校验全局 tensor
→ for each layer:
→ 校验 attention 基础权重
→ 按 compress ratio 校验 compressor/indexer
→ 校验 FFN/MoE/router/shared expert
→ 任一 mismatch 立即退出

关键算法/数据结构: 这里不是复杂算法,而是 schema validation。WHY schema validation 对模型推理重要,是因为大模型文件和代码之间没有类型系统;GGUF metadata/tensor directory 是运行时数据,必须用代码重建“类型检查”。tensor_expect_layout()tensor_expect_routed_expert() 就是在给 mmap 权重做类型检查。

核心状态变量:

变量名初始值变化时机终态
hc_dim由常量计算函数开始HC 展开维度
hc_mix_dim由常量计算函数开始HC mixer 输出维度
q_dim由 head/head_dim 计算函数开始Q projection 输出维度
ratioper-layer每层循环决定是否检查 compressor/indexer
l&w->layer[il]每层循环当前层权重集合

多执行路径:

  • 路径 A(普通层/无压缩): 校验基础 attention + FFN,不进入 compressor/indexer 条件分支。
  • 路径 B(ratio=4 层): 除基础 attention 外,还校验 attention compressor 和 indexer compressor,这是长上下文压缩可用的前提。

2.3 逐行代码解释

贯穿示例输入:w 来自 q2 DS4 GGUF,routed experts 为 IQ2_XXS / Q2_K,部分层 ratio=4

/* Verify every tensor type and dimension used by the specialized pipeline.
* After this succeeds, inference code can rely on fixed DS4 constants. */
static void weights_validate_layout(const ds4_weights *w) {
const uint64_t hc_dim = (uint64_t)DS4_N_EMBD * DS4_N_HC;
const uint64_t hc_mix_dim = 2u * DS4_N_HC + (uint64_t)DS4_N_HC * DS4_N_HC;
const uint64_t q_dim = (uint64_t)DS4_N_HEAD * DS4_N_HEAD_DIM;
const uint64_t out_low_dim = (uint64_t)DS4_N_OUT_GROUP * DS4_N_LORA_O;

tensor_expect_layout(w->token_embd, DS4_TENSOR_F16, 2, DS4_N_EMBD, DS4_N_VOCAB, 0);
tensor_expect_layout(w->output_hc_base, DS4_TENSOR_F32, 1, DS4_N_HC, 0, 0);
tensor_expect_layout(w->output_hc_fn, DS4_TENSOR_F16, 2, hc_dim, DS4_N_HC, 0);
tensor_expect_layout(w->output_hc_scale, DS4_TENSOR_F32, 1, 1, 0, 0);
tensor_expect_layout(w->output_norm, DS4_TENSOR_F32, 1, DS4_N_EMBD, 0, 0);
tensor_expect_layout(w->output, DS4_TENSOR_Q8_0, 2, DS4_N_EMBD, DS4_N_VOCAB, 0);

for (uint32_t il = 0; il < DS4_N_LAYER; il++) {
const ds4_layer_weights *l = &w->layer[il];
const uint32_t ratio = ds4_layer_compress_ratio(il);

tensor_expect_layout(l->hc_attn_fn, DS4_TENSOR_F16, 2, hc_dim, hc_mix_dim, 0);
tensor_expect_layout(l->hc_attn_scale, DS4_TENSOR_F32, 1, 3, 0, 0);
tensor_expect_layout(l->hc_attn_base, DS4_TENSOR_F32, 1, hc_mix_dim, 0, 0);
tensor_expect_layout(l->attn_norm, DS4_TENSOR_F32, 1, DS4_N_EMBD, 0, 0);
tensor_expect_layout(l->attn_q_a, DS4_TENSOR_Q8_0, 2, DS4_N_EMBD, DS4_N_LORA_Q, 0);
tensor_expect_layout(l->attn_q_a_norm, DS4_TENSOR_F32, 1, DS4_N_LORA_Q, 0, 0);
tensor_expect_layout(l->attn_q_b, DS4_TENSOR_Q8_0, 2, DS4_N_LORA_Q, q_dim, 0);
tensor_expect_layout(l->attn_kv, DS4_TENSOR_Q8_0, 2, DS4_N_EMBD, DS4_N_HEAD_DIM, 0);
tensor_expect_layout(l->attn_kv_a_norm, DS4_TENSOR_F32, 1, DS4_N_HEAD_DIM, 0, 0);
tensor_expect_layout(l->attn_sinks, DS4_TENSOR_F32, 1, DS4_N_HEAD, 0, 0);
tensor_expect_layout(l->attn_output_a, DS4_TENSOR_Q8_0, 2, DS4_N_HEAD_DIM * (DS4_N_HEAD / DS4_N_OUT_GROUP), out_low_dim, 0);
tensor_expect_layout(l->attn_output_b, DS4_TENSOR_Q8_0, 2, out_low_dim, DS4_N_EMBD, 0);

if (ratio != 0) {
const uint32_t coff = ratio == 4 ? 2u : 1u;
const uint64_t comp_width = (uint64_t)coff * DS4_N_HEAD_DIM;
tensor_expect_layout(l->attn_compressor_ape, DS4_TENSOR_F16, 2, comp_width, ratio, 0);
tensor_expect_layout(l->attn_compressor_kv, DS4_TENSOR_F16, 2, DS4_N_EMBD, comp_width, 0);
tensor_expect_layout(l->attn_compressor_gate, DS4_TENSOR_F16, 2, DS4_N_EMBD, comp_width, 0);
tensor_expect_layout(l->attn_compressor_norm, DS4_TENSOR_F32, 1, DS4_N_HEAD_DIM, 0, 0);
}
if (ratio == 4) {
const uint64_t index_q_dim = (uint64_t)DS4_N_INDEXER_HEAD * DS4_N_INDEXER_HEAD_DIM;
const uint64_t index_width = 2u * DS4_N_INDEXER_HEAD_DIM;
tensor_expect_layout(l->indexer_attn_q_b, DS4_TENSOR_F16, 2, DS4_N_LORA_Q, index_q_dim, 0);
tensor_expect_layout(l->indexer_proj, DS4_TENSOR_F16, 2, DS4_N_EMBD, DS4_N_INDEXER_HEAD, 0);
tensor_expect_layout(l->indexer_compressor_ape, DS4_TENSOR_F16, 2, index_width, ratio, 0);
tensor_expect_layout(l->indexer_compressor_kv, DS4_TENSOR_F16, 2, DS4_N_EMBD, index_width, 0);
tensor_expect_layout(l->indexer_compressor_gate, DS4_TENSOR_F16, 2, DS4_N_EMBD, index_width, 0);
tensor_expect_layout(l->indexer_compressor_norm, DS4_TENSOR_F32, 1, DS4_N_INDEXER_HEAD_DIM, 0, 0);
}

tensor_expect_layout(l->hc_ffn_fn, DS4_TENSOR_F16, 2, hc_dim, hc_mix_dim, 0);
tensor_expect_layout(l->hc_ffn_scale, DS4_TENSOR_F32, 1, 3, 0, 0);
tensor_expect_layout(l->hc_ffn_base, DS4_TENSOR_F32, 1, hc_mix_dim, 0, 0);
tensor_expect_layout(l->ffn_norm, DS4_TENSOR_F32, 1, DS4_N_EMBD, 0, 0);
tensor_expect_layout(l->ffn_gate_inp, DS4_TENSOR_F16, 2, DS4_N_EMBD, DS4_N_EXPERT, 0);
tensor_expect_optional(l->ffn_exp_probs_b, DS4_TENSOR_F32, 1, DS4_N_EXPERT, 0, 0);
tensor_expect_routed_expert(l->ffn_gate_exps, 3, DS4_N_EMBD, DS4_N_FF_EXP, DS4_N_EXPERT);
tensor_expect_routed_expert(l->ffn_up_exps, 3, DS4_N_EMBD, DS4_N_FF_EXP, DS4_N_EXPERT);
tensor_expect_routed_expert(l->ffn_down_exps, 3, DS4_N_FF_EXP, DS4_N_EMBD, DS4_N_EXPERT);
if (l->ffn_gate_exps->type != l->ffn_up_exps->type) {
fprintf(stderr, "ds4: routed gate/up experts use different quant types in layer %u\n", il);
exit(1);
}
tensor_expect_layout(l->ffn_gate_shexp, DS4_TENSOR_Q8_0, 2, DS4_N_EMBD, DS4_N_FF_EXP, 0);
tensor_expect_layout(l->ffn_up_shexp, DS4_TENSOR_Q8_0, 2, DS4_N_EMBD, DS4_N_FF_EXP, 0);
tensor_expect_layout(l->ffn_down_shexp, DS4_TENSOR_Q8_0, 2, DS4_N_FF_EXP, DS4_N_EMBD, 0);
if (il < DS4_N_HASH_LAYER) {
tensor_expect_layout(l->ffn_gate_tid2eid, DS4_TENSOR_I32, 2, DS4_N_EXPERT_USED, DS4_N_VOCAB, 0);
}
}
}

步骤 1:计算 hc_dim/hc_mix_dim/q_dim/out_low_dim。WHY 先计算派生维度,是因为模型结构里很多 tensor shape 不是单个常量,而是常量组合;把组合集中定义可以减少后面表达式错误。此时所有校验都建立在 DS4 固定超参数上。

步骤 2:校验全局 embedding/output。WHY output 是 Q8_0,而 embedding 是 F16,体现了量化策略不是一刀切;不同矩阵在质量/速度/内存之间有不同权衡。此时全局头部权重通过。

步骤 3:逐层校验 attention。WHY attention projection 使用 LoRA-like 分解字段(attn_q_a/q_battn_output_a/b),是模型结构本身决定的;kernel 会按这些 shape 调度,如果维度错,后续矩阵乘会读错 stride。此时当前层基础 attention 可用。

步骤 4:如果 ratio != 0,校验 attention compressor。WHY 压缩 KV 不是所有层都有,必须由 per-layer compression ratio 决定。coff = ratio == 4 ? 2 : 1 说明 ratio=4 层有更宽的压缩表示,需要额外处理。

步骤 5:如果 ratio == 4,校验 indexer compressor。WHY ratio=4 需要 indexer 来选择可见 compressed rows,否则长上下文 sparse attention 不知道看哪里。这个分支是 DS4 长上下文设计的核心痕迹。

步骤 6:校验 FFN/MoE。WHY routed expert 允许 IQ2_XXS/Q2_K/Q4_K 等量化类型,而 shared expert 固定 Q8_0,这正对应 README 里“只量化 routed MoE experts,其他关键部件保质量”的策略。此时 MoE 权重布局被证明和 kernel 期望一致。

2.4 关键设计点

设计维度分析内容
实现选择使用硬编码 shape 校验,而不是信任 metadata。WHY 是 DS4-specific kernel 没有动态 shape 保护,启动期必须做完整类型检查。
性能优化校验后推理路径无需重复检查 shape,可以直接使用常量调度 kernel。这样把安全成本放到启动阶段,热路径更干净。
编译器相关不涉及 IR,但很像运行时类型系统:给 mmap 权重补上 C 编译器无法检查的结构类型。
安全与健壮性mismatch 立即 exit(1),避免错误模型继续跑。对大模型推理,这是比“尽量兼容”更安全的策略。
可扩展性支持新 DS4 变体需要扩展常量和校验逻辑。它不适合模型泛化,但非常适合单模型优化。
潜在问题过度硬编码会让模型小版本更新成本变高。不过 README 也说明“exact model may change, constraint remains”,所以未来可能是一次明确迁移,而不是动态兼容。

2.5 完整示例(三组对比)

示例 1 — q2 正常模型

  • 输入:routed experts 为 IQ2_XXS gate/up、Q2_K down,其他 projection 为 Q8_0/F16/F32tensor_expect_routed_expert 接受 → 推理启动。

示例 2 — ratio=4 压缩层

  • 输入:某层 ds4_layer_compress_ratio(il)=4 → 同时校验 attn_compressor_*indexer_compressor_* → 后续 indexer sparse selection 有权重可用。

示例 3 — 错误 GGUF

  • 输入:ffn_gate_exps 维度为 [DS4_N_FF_EXP, DS4_N_EMBD, DS4_N_EXPERT](转置)→ tensor_expect_routed_expert 报 dim mismatch → 启动失败。这样比运行到 MoE kernel 才错安全得多。

2.6 使用注意与改进建议

  1. 不要为了兼容“差不多”的 GGUF 放宽这里的检查。这个函数保护的是后续所有 raw pointer / Metal buffer offset 访问,放宽会把清晰启动错误变成难查的 logits drift。
  2. 更新模型权重格式时,应该同步更新 binding、validation、kernel 三处。只改其中一处会造成语义字段和实际 tensor layout 不一致。

可考虑的改进: 生成一份启动时的 layout report,把实际 tensor type/shape 和期望一起输出到 debug 日志。WHY 更好,是因为排查模型转换问题时可以快速定位第一个 mismatch 之外的整体差异;代价是启动日志更长,应放在 debug flag 下。

片段 #3:ds4_session_sync()

位置:ds4.c:15885
优先级:★★★
一句话核心:它是把 stateless API 请求变成 stateful KV 复用的核心状态机。

3.1 代码整体作用

ds4_session_sync() 接收“完整 prompt token prefix”,把当前 live session 调整到这个 prefix。它先判断 live checkpoint 是否是新 prompt 的前缀;如果是,只计算新增 suffix;如果不是,就从头 prefill。这个函数让 server 和 REPL 可以简单地维护完整 transcript,而不用自己管理 KV cache 的复杂生命周期。

系统层次上,它位于 API/CLI 和 Metal graph 之间。上层只知道“这是当前完整 prompt”,不需要知道 raw SWA、compressed cache、indexer frontier;下层 Metal graph 根据 sync 的决策进行 chunked prefill 或 decode eval。没有它,server 每个请求都要从 token 0 开始,长上下文 agent 基本不可用。

3.2 核心逻辑分析

执行流程:

输入 session + full prompt
→ 校验 prompt 长度
→ 如果 live checkpoint 是 prompt 前缀:
→ suffix 长度为 0:直接成功
→ suffix 足够长:chunked range prefill
→ suffix 较短:逐 token decode eval
→ 否则:
→ 根据 prefill_cap 选择 chunked full prefill 或 raw prefill
→ 更新 checkpoint / invalidate MTP draft

关键算法/数据结构: prefix reuse。WHY 用 prefix 而不是 diff,是因为 autoregressive KV 只能安全复用从开头完全一致的前缀;中间任何 token 改动都会让后续所有 KV rows 失效。函数区分长 suffix 和短 suffix,也是工程上重要的性能选择:短增量用 decode 更快,长增量用 batched prefill 更合适。

核心状态变量:

变量名初始值变化时机终态
s->checkpointlive tokenssync 成功后等于 prompt
s->checkpoint_valid旧状态prefill/eval 成功或失败成功 true,失败 false
suffixprompt.len - checkpoint.lenprefix hit 时决定追加策略
resume_minenv/config 派生prefix hit 时长 suffix 阈值
s->logits上次 logitsprefill/eval 后prompt 最后一位后的 next-token logits
s->mtp_draft_valid旧状态sync/eval 时通常被置 false

多执行路径:

  • 路径 A(HTTP 增长 transcript): prompt 以 live checkpoint 开头,suffix 较长 → metal_graph_prefill_chunked_range() 只补新增段。
  • 路径 B(REPL 生成一个 token 后继续): suffix 很短 → 多次 metal_graph_eval_token_raw_swa(),避免 chunked prefill overhead。
  • 路径 C(prompt 改写或新会话): prefix 不匹配 → 全量 prefill,丢弃旧 checkpoint。

3.3 逐行代码解释

贯穿示例输入:live checkpoint 为 10000 tokens,新 prompt 为相同前缀加 2000 tokens,ctx_size=100000

/* Bring the Metal graph to exactly the supplied token prefix.
*
* ds4-server and the REPL are stateless at the text/API layer but stateful here:
* they resend or rebuild the full transcript, and this function decides whether
* the live checkpoint is a prefix. A matching prefix is extended in one of two
* ways:
*
* - long suffix: batched layer-major prefill, aligned to absolute chunk
* boundaries so compressor/indexer rows finalize in the same order as a
* cold prompt;
* - short suffix: ordinary one-token decode, which is faster below the
* measured crossover and preserves exact autoregressive semantics.
*
* A non-matching prompt discards the checkpoint and prefills from token zero.
*/
int ds4_session_sync(ds4_session *s, const ds4_tokens *prompt, char *err, size_t errlen) {
#ifdef DS4_NO_METAL
(void)s;
(void)prompt;
snprintf(err, errlen, "Metal support is not compiled in");
return 1;
#else
ds4_engine *e = s->engine;
if (prompt->len <= 0 || prompt->len >= s->ctx_size) {
snprintf(err, errlen, "prompt exceeds context");
return 1;
}

if (s->checkpoint_valid &&
prompt->len >= s->checkpoint.len &&
ds4_tokens_starts_with(prompt, &s->checkpoint))
{
s->mtp_draft_valid = false;
const int suffix = prompt->len - s->checkpoint.len;
const uint32_t resume_min = metal_graph_resume_prefill_min_tokens();
if (suffix > 0 && (uint32_t)suffix >= resume_min) {
ds4_sync_progress progress = {
.session = s,
.prompt = prompt,
.user = s->progress,
.user_ud = s->progress_ud,
};
ds4_session_progress_fn progress_fn =
s->progress ? ds4_session_note_prefill_progress : NULL;
bool ok = metal_graph_prefill_chunked_range(&s->graph,
&e->model,
&e->weights,
prompt,
(uint32_t)s->checkpoint.len,
(uint32_t)suffix,
s->logits,
false,
progress_fn,
progress_fn ? &progress : NULL);
if (!ok) {
snprintf(err, errlen, "Metal resumed prefill failed while extending checkpoint");
s->checkpoint_valid = false;
return 1;
}
ds4_tokens_copy(&s->checkpoint, prompt);
s->checkpoint_valid = true;
return 0;
}

for (int i = s->checkpoint.len; i < prompt->len; i++) {
if (!metal_graph_eval_token_raw_swa(&s->graph, &e->model, &e->weights,
(uint32_t)prompt->v[i],
(uint32_t)s->checkpoint.len,
s->logits))
{
snprintf(err, errlen, "Metal decode failed while extending checkpoint");
s->checkpoint_valid = false;
return 1;
}
token_vec_push(&s->checkpoint, prompt->v[i]);
}
return 0;
}

bool ok;
if (s->prefill_cap < (uint32_t)prompt->len) {
ds4_sync_progress progress = {
.session = s,
.prompt = prompt,
.user = s->progress,
.user_ud = s->progress_ud,
};
ds4_session_progress_fn progress_fn =
s->progress ? ds4_session_note_prefill_progress : NULL;
ok = metal_graph_prefill_chunked(&s->graph, &e->model, &e->weights,
prompt, prompt->len, s->logits, false,
progress_fn, progress_fn ? &progress : NULL);
} else {
ok = metal_graph_prefill_raw_swa(&s->graph, &e->model, &e->weights,
prompt, prompt->len, s->logits, false);
}
if (!ok) {
snprintf(err, errlen, "Metal prefill failed");
s->checkpoint_valid = false;
return 1;
}
ds4_tokens_copy(&s->checkpoint, prompt);
s->checkpoint_valid = true;
s->mtp_draft_valid = false;
s->graph.mtp_n_raw = 0;
return 0;
#endif
}

步骤 1:非 Metal 编译直接返回错误。WHY 这样做,是因为 session sync 依赖 Metal graph;CPU path 是 reference/debug,不提供完整 live graph session 能力。此时非 Metal server 不会假装支持 KV 复用。

步骤 2:检查 prompt 长度。WHY prompt->len >= ctx_size 要拒绝,是因为还需要空间生成下一 token;等于 context 上限时 next-token logits 语义已经不可继续。此时非法 prompt 会被上层转换为错误。

步骤 3:判断 live checkpoint 是否为 prompt 前缀。WHY 这是唯一安全复用条件,因为 KV cache 代表从 token 0 到当前位置的完整历史;只要中间一位不同,后面所有状态都失效。示例里前 10000 token 一致,因此进入 prefix hit。

步骤 4:prefix hit 后先 invalidate MTP draft。WHY MTP draft 是基于旧 logits/旧位置的预测;sync 到新 prompt 后 draft 不一定仍然有效。这个小状态如果忘记清,会让 speculative path 接受错误假设。

步骤 5:长 suffix 走 metal_graph_prefill_chunked_range()。WHY 不是逐 token decode 2000 次,是因为长 suffix batched prefill 更能利用 GPU 并行;注释还强调要按绝对 chunk boundary 对齐,保证 compressor/indexer rows 和 cold prefill 一致。此时成功后 checkpoint 被复制为完整 prompt。

步骤 6:短 suffix 逐 token eval。WHY 小增量时 batch setup 成本可能超过收益,decode path 更快,而且逐 token 自回归语义最直接。每 eval 一个 token 后立刻 push 到 checkpoint,保持 graph 和 token vector 同步。

步骤 7:prefix miss 走 full prefill。WHY 不能局部改写已有 graph,因为 graph 内部包含 raw ring、compressed rows 和 frontiers,不只是 token 数组。成功后 checkpoint 变成新 prompt,并清空 MTP raw 计数。

3.4 关键设计点

设计维度分析内容
实现选择full-prefix API + 内部 prefix reuse。WHY 是上层协议简单,下层仍可 stateful 优化。
性能优化长 suffix 用 chunked range prefill,短 suffix 用 decode eval。这个分叉体现了真实测量后的 crossover 思路。
编译器相关不涉及编译器,但涉及 GPU graph frontier 的一致性维护。
安全与健壮性任何 Metal prefill/eval 失败都会 invalidate checkpoint,避免上层继续使用半坏 session。
可扩展性未来如果支持 graph frontier snapshot,可以改进中间 rewrite;当前通过 rebuild-needed 明确拒绝危险路径。
潜在问题prefix miss 时成本很高。Server 通过磁盘 KV cache 和 tool memory 尽量减少 miss,但无法完全避免。

3.5 完整示例(三组对比)

示例 1 — 纯复用

  • 输入:live checkpoint 10000 tokens,prompt 也是同 10000 tokens → suffix=0 → 不执行 prefill/eval → logits 保持可用,快速返回。

示例 2 — 长 suffix

  • 输入:live 10000 tokens,prompt 12000 tokens,suffix=2000 且超过 resume_min → chunked range prefill token [10000,12000) → checkpoint 更新为 12000。

示例 3 — 中间改写

  • 输入:live 10000 tokens,prompt 在第 8000 token 不同 → prefix miss → full prefill prompt 全部 token。这样避免错误复用 8000 之后已经不可信的 KV rows。

3.6 使用注意与改进建议

  1. 上层应该尽量发送 canonical 且稳定的 prompt 字节序列。语义相同不代表 token 相同,尤其 tool call JSON 字段顺序、空白、DSML escaping 都会影响 prefix reuse。
  2. 不要直接改 s->checkpoint 试图“修复”prefix miss。checkpoint 只是 graph 状态的索引;真正状态还在 Metal tensors 里,单独改 token vector 会制造更隐蔽的不一致。

可考虑的改进: 增加可持久化的中间 frontier snapshot,用于安全处理 common prefix 后的 suffix rewrite。WHY 更好,是因为 tool canonicalization 或客户端修正历史消息时可以恢复到 common 点再 replay,而不是 full prefill;但实现难度很高,因为要保存每层 raw/compressed/indexer/frontier 的一致切片。

7. 测试用例分析

测试文件清单

测试文件/目录测试的模块测试用例数量
tests/ds4_test.cserver parser/cache、Metal kernels、长上下文、官方向量、tool calls5 个入口组
tests/test-vectors/official.vec官方 DeepSeek V4 Flash top-logprobs 对齐多 case fixture
tests/test-vectors/prompts/*.txt短推理、代码补全、长代码审计、长记忆档案等 prompt5 个 prompt
tests/test-vectors/official/*.json官方 API 原始响应5 个 JSON

功能覆盖矩阵

核心功能主代码位置测试覆盖覆盖率评估
Metal f16 matvec fast pathds4_metal.m / metal/dense.metal--metal-kernels覆盖数值回归
长上下文 continuationds4_session_sync() / graph prefill--long-context覆盖实际长 prompt 场景
官方 logits/top-logprobs 对齐tokenizer + forward--logprob-vectors对质量非常关键
DSML tool call 生成质量server + model generation--tool-call-quality依赖真实模型
Server parser/render/cache 单元ds4_server.c--server覆盖 API glue
CPU backendds4.c CPU reference⚠️主要作为 debug,不建议大跑
并发/多 workerds4_server.c设计上单 worker,不覆盖 batch serving

从测试中发现的边界条件

  1. test_metal_f16_matvec_fast_nr0_4() 明确是“long-context repetition failure”的短回归。这个测试说明 decode 期一 token F16 matvec 的 fast nr0=4 variant 曾经导致长上下文重复问题,所以性能 kernel 必须和 plain kernel 数值等价。
  2. test_long_security_continuation() 要求 prompt token 数超过 30000,并创建 100000 context session 后 sync。这不是普通单元测试,而是在验证长 prefill + continuation 的真实路径。
  3. 官方向量说明 hosted API 不暴露 full logits,只能存 top_logprobs=20。这意味着测试不是完整 logits 比较,但仍能抓 tokenizer、sampling top、prompt rendering 的大类错误。
  4. --server 单测通过 #include "../ds4_server.c" 直接访问 static 函数。这种写法不优雅但有效,说明 server 解析/cache 逻辑复杂到值得白盒测试。

测试质量评估

  • 正常流程:✅ 有 CLI/server/model generation 覆盖。
  • 边界输入:✅ 长上下文、tool calls、official vectors 都覆盖关键边界。
  • 异常输入:⚠️ parser 单测应有覆盖,但从入口看不出所有 JSON 错误路径是否充分。
  • 并发场景:⚠️ 单 worker 设计降低并发状态风险,但 client thread/queue 压测不明显。

测试质量建议

建议为 ds4_session_rewrite_requires_rebuild() 和 tool memory 精确回放增加更小的纯单元测试。WHY 是这些逻辑直接决定 KV cache hit/miss 和 graph 安全性,不应该只依赖集成场景才能发现问题。还可以增加 disk KV payload header mismatch 测试,例如不同 quant bits、不同 ctx、不同 raw window 的拒绝路径。

8. 应用迁移场景

场景 1:DS4 本地引擎 → 另一个专用大模型引擎

不变的原理:

  • 保持 engine / session 边界:模型资源和推理时间线分离。
  • 启动期严格校验权重 layout:不要在热路径猜 shape。
  • 上层协议只通过窄 API 调用推理:HTTP/CLI 不碰 tensor。

需要修改的部分:

  • 替换 DS4 常量、metadata keys、tensor binding、tokenizer pre-tokenization、Metal kernels。
  • 如果新模型没有压缩 KV,则 session payload 可以简化;如果新模型有不同 attention pattern,则 graph prefill/decode 调度要重写。

学到的通用模式: 专用模型 runtime 的价值在于把“模型结构知识”提前编译进代码。WHY 这样迁移,是因为本项目证明了窄通用层 + 深专用内核,比半通用半专用的模糊结构更容易保证 correctness。

场景 2:本地 LLM KV cache → 数据库查询计划缓存

不变的原理:

  • cache key 必须基于真实执行语义,而不是人类看起来相同的文本。
  • cache payload 必须包含恢复执行所需的完整内部状态,而不是只保存外层请求。
  • 遇到中间改写时,不安全局部重写应拒绝,宁可重建。

需要修改的部分:

  • token SHA1 换成 normalized query plan hash。
  • DS4 session payload 换成 query execution state / materialized intermediate results。
  • tool memory 的 raw DSML 回放概念可迁移为“保存优化器实际采用的 canonical plan”。

学到的通用模式: 高性能 cache 不是“保存输入输出”这么简单,而是保存可安全恢复的内部 frontier。WHY 这样迁移,是因为无论 KV cache 还是查询计划,中间状态都依赖完整历史;如果只看表面文本,很容易错误复用。

9. 依赖关系与使用示例

外部/系统依赖

Foundation + Metal frameworks

  • 用途: Darwin 下编译 ds4_metal.m,管理 MTLDeviceMTLBuffer、pipeline、command buffer。
  • WHY 选择: 目标机器是 Apple Silicon,Metal 是本地 GPU compute 的原生路径。使用系统 framework 避免额外 runtime 依赖,也能利用 unified memory/no-copy 特性。
  • WHY 不用 CUDA/Vulkan: CUDA 不适用于 Apple GPU;Vulkan/Metal abstraction 会降低对 mmap view、residency、command batching 的直接控制。

pthread / POSIX / mmap

  • 用途: server worker/client threads、CPU reference parallelism、file-backed model mapping、lock file。
  • WHY 选择: C 项目里这些接口足够稳定直接,避免引入大型 runtime。
  • WHY 不用高级并发框架: 项目并发模型很简单:server 多 client thread + 单 inference worker。高级框架只会增加复杂度。

linenoise

  • 用途: CLI REPL 输入和 history。
  • WHY 选择: 小型、嵌入式、符合项目“small native codebase”的目标。
  • WHY 不用 readline: readline 许可和平台集成成本更高,linenoise 足够。

rax

  • 用途: server tool memory / map-like 数据结构。
  • WHY 选择: 紧凑 C 实现,适合嵌入项目。
  • WHY 不用外部 hash map 库: 减少依赖,并保持 C 单仓库构建简单。

内部模块依赖

ds4_cli.c / ds4_server.cds4.h

  • 依赖原因: 上层只需要 engine/session/tokenize/chat/generation API。
  • WHY 这样设计: 保持协议层和模型内部解耦,避免 server 知道 tensor layout。

ds4.cds4_metal.h

  • 依赖原因: 引擎负责模型语义和 graph 调度,但 Metal 资源由 ObjC runtime 封装。
  • WHY 这样设计: C 端保持模型逻辑清晰,ObjC 端只处理 Metal 机制。

ds4_metal.mmetal/*.metal

  • 依赖原因: runtime 拼接/编译 kernel source 并提供 wrapper。
  • WHY 这样设计: kernel 代码按职责拆文件,runtime 统一编译和缓存 pipeline。

完整使用示例

make
./download_model.sh q2
./ds4 -p "Explain Redis streams in one paragraph."
./ds4-server --ctx 100000 --kv-disk-dir /tmp/ds4-kv --kv-disk-space-mb 8192

WHY 这个流程合理:先 build,确保 C/ObjC/Metal kernels 都能编译;再下载项目支持的 q2/q4 GGUF,而不是任意 GGUF;CLI 用于快速验证 tokenizer + generation;server 用于 agent API,并打开磁盘 KV cache 让长上下文多轮请求可复用。

10. 质量验证清单

理解深度

  • 每个核心概念都回答了 WHY:需要、实现方式、不用其他方式。
  • 能解释 ds4_engineds4_session 的边界。
  • 能解释为什么模型加载使用 mmap,Metal 使用 no-copy views。
  • 能解释为什么 KV cache 不能随便重写中间 token。

技术准确性

  • 算法部分覆盖 tokenizer、session sync、chunked prefill、disk KV prefix lookup。
  • 设计模式覆盖 Facade、Stateful Session、Adapter、Single Worker。
  • 关键代码解析引用真实代码和行号。
  • 测试章节覆盖已发现的测试入口和官方向量机制。

实用性

  • 给出两个迁移场景:专用模型 runtime、查询计划缓存。
  • 给出构建/运行示例。
  • 指出潜在改进:frontier snapshot、layout report、payload mismatch 单测。

最终“四能”测试

  1. ✅ 能理解代码的设计思路:专用模型、窄接口、stateful session、Metal graph、磁盘 KV。
  2. ✅ 能独立实现类似功能:知道模块边界和关键状态机。
  3. ✅ 能应用到不同场景:可迁移到其他专用 runtime 或内部状态 cache。
  4. ✅ 能向他人清晰解释:可以用“模型是 engine,会话是 timeline,KV 是时间线记忆”来讲清主线。

覆盖率摘要

文件覆盖情况

文件路径是否被分析分析章节备注
AGENT.md项目目标/约束明确专用、Metal、mmap、KV 目标
README.md背景与动机项目定位和使用方式
Makefile依赖关系构建目标和平台分支
ds4.h核心概念/设计模式公共 API 边界
ds4.c关键代码解析核心引擎、session、payload
ds4_metal.hMetal runtimeC API 封装
ds4_metal.mMetal runtimemmap view/pipeline/command 策略
metal/*.metal依赖关系/Metal kernel 职责按职责摘要覆盖
ds4_cli.cAPI/CLIREPL、think mode、token printer
ds4_server.cServer/API/cacheOpenAI/Anthropic、SSE、worker、KV cache
tests/ds4_test.c测试分析测试入口和覆盖矩阵
tests/test-vectors/*测试分析官方向量机制
linenoise.*⚠️依赖关系辅助 REPL,不深挖
rax.*⚠️依赖关系辅助 map,不深挖

模块覆盖率

  • 核心模块:100% 已覆盖。
  • 工具模块:约 70% 已覆盖,重点覆盖其用途而非实现细节。
  • 测试文件:主要测试入口 100% 覆盖,官方 fixture 机制已覆盖。

最重要的设计结论

这个项目的设计原理可以压缩成一句话:用非常窄的上层 API 包住一个高度专用的 DS4 Metal 推理状态机,把模型结构、KV 生命周期和工具调用字节一致性都控制在引擎/server 内部,从而让本地长上下文 agent 场景可用。