Skip to content

学习日志


Day 1 — 项目全貌与编译系统

日期:2025-05-09 状态:已完成

今日摘要

  • 理解了 Makefile 的完整构建链:make → 编译 .o → 链接可执行文件
  • 掌握了条件编译:-DDS4_NO_METAL 控制平台分支,__ARM_NEON 由编译器预定义
  • 读了 ds4.c 文件头(行 1-120):18 个系统头文件、模型架构常量 enum
  • 理解了 opaque type 封装:ds4_engine / ds4_session 前向声明
  • 学习了 DeepSeek V4 Flash 架构:43 层、284B 参数、256 专家 MoE(仅激活 6 个)
  • 关键洞察:专用引擎 > 通用框架——固定架构换来简化和优化

困惑与突破

  • 困惑:为什么 makeds4_metal.o-fobjc-arc 而不是 -std=c99?一直以为 C 和 Objective-C 只是语法糖的区别。
  • 突破.m 文件是 Objective-C,ARC 是 ObjC 的自动内存管理机制。C 和 ObjC 是两种不同的语言,编译器标志完全不同。ds4.c 用纯 C 是为了让引擎核心可移植,Metal 桥接单独用 ObjC 处理。

Day 2 — 内存管理与 mmap

日期:2025-05-09 状态:已完成

今日摘要

  • 掌握了 xmalloc/xcalloc/xrealloc 包装器模式和为什么没有 xfree
  • 深入理解 mmap:MAP_SHARED(Metal 零拷贝)vs MAP_PRIVATE(CPU 避内核 bug)
  • 理解 xmalloc_zeroed 为什么不用 calloc(Darwin 零页 panic 风险)
  • 学习 posix_madvise 预取和 volatile 防优化技巧
  • 理解 __thread 线程局部存储和预分配 scratch buffer 模式
  • 追踪了完整加载链:open → fstat → mmap → cursor 解析 → weights_bind
  • 关键洞察:热路径零分配,所有内存操作集中到初始化阶段
  • 上游更新model_open 新增 prefetch_cpu 参数,ds4_dump_text_tokenization() 传 false 避免遍历张量数据

困惑与突破

  • 困惑:为什么 malloc + memsetcalloc 更安全?calloc 不是天然初始化为零吗?
  • 突破:calloc 的"零页优化"让所有虚拟页指向同一个物理零页。推理时逐 token 写入 KV cache 会触发大量 page fault。在 macOS 上,大 mmap + 大量 page fault 叠加可能导致内核计数器溢出(kernel panic)。malloc + memset 在启动时集中触发所有 page fault,把风险控制在初始化阶段。

Day 3 — GGUF 二进制格式

日期:2025-05-09 状态:已完成

今日摘要

  • 学习了 GGUF 二进制文件格式:头部(24B) → 元数据表 → 张量目录 → 张量数据
  • 掌握了 cursor 游标模式解析二进制:has/read/skip 操作
  • 理解了 struct 对齐和 static_assert 编译时检查技巧
  • 学习了量化块格式:Q2_K(84B)、Q4_K(144B)、Q8_K(292B)、IQ2_XXS(66B)
  • 理解了 parse_metadata 的懒加载和 parse_tensors 的两步解析
  • 学习了 config_validate_model 的"早失败"验证
  • 关键洞察:懒加载 + 零拷贝字符串引用,避免不必要的内存分配

困惑与突破

  • 困惑cursor_string 返回的 ds4_str 没有拷贝数据,那 mmap 被释放后不就野指针了?
  • 突破:ds4.c 的设计是"加载后永不释放"——模型的生命周期覆盖整个进程。ds4_model 持有 mmap 映射直到进程退出,所以 ds4_str 指针始终有效。这是推理引擎常见的设计权衡:用内存常驻换取零拷贝。

Day 4 — BPE 分词器

日期:2025-05-09 状态:已完成

今日摘要

  • 学习了开放寻址哈希表:线性探测、2 的幂容量、& mask 代替取模
  • 掌握了 FNV-1a 哈希函数和动态数组翻倍增长模式
  • 深入理解 BPE 算法:字节编码→预分词→合并循环→查表得 token ID
  • 理解 GPT-2 字节级编码:不可打印字节映射到 256+ 码点
  • 学习了 Chat 模板组装:BOS + system + User + prompt + Assistant + 💭
  • 关键洞察:分词器是 LLM 的"眼睛"——文本必须先变成数字,模型才能处理

困惑与突破

  • 困惑:BPE 合并顺序为什么是从最低 rank 开始而不是从最高?直觉上应该先合并最频繁的。
  • 突破:词表中的 rank 越小表示优先级越高。BPE 每轮扫描所有相邻对,找到 rank 最小(优先级最高)的对先合并。"最低 rank = 最高优先级"——这是训练时按频率排序编码的结果,rank 1 就是训练中出现最频繁的合并。

Day 5 — 采样策略

日期:2025-05-09 状态:已完成

今日摘要

  • 学习了数值稳定的 softmax(减最大值防溢出)
  • 理解 Temperature/Top-K/Top-P/Min-P 四种采样策略及组合
  • 掌握了插入排序维护 top-k 数组(O(n×k) 优于 qsort 的 O(n log n))
  • 学习了 SplitMix64 PRNG 和均匀浮点转换
  • 追踪了完整采样链:session_sample → top_p_min_p → argmax/full_vocab
  • 关键洞察:采样策略在确定性和多样性之间找平衡,温度是核心控制旋钮

困惑与突破

  • 困惑:为什么 softmax 要先减最大值再 exp?直接算 exp 再归一化不行吗?
  • 突破:浮点数溢出!比如 exp(1000) 是 infinity,整个计算就废了。减去最大值后,最大值变成 exp(0)=1,其他都是 exp(负数) < 1,不会溢出。这个"数值稳定性"技巧从 Day 5 一路用到 Day 8 的注意力计算。

Day 6 — 公共 API 与权重结构

日期:2025-05-09 状态:已完成

今日摘要

  • 深入理解 opaque type 封装:ds4_engine/ds4_session 前向声明
  • 掌握了结构体嵌套设计:engine → model + vocab + weights[43 layers]
  • 理解 weights_bind 按名称查找张量的绑定过程
  • 学习了 Transformer 每层的完整权重组成(注意力/FFN/HC)
  • 理解了超连接(Hyper-Connection)替代标准残差连接
  • 关键洞察:LoRA 低秩分解把 Query 参数减少 72%
  • 上游更新:ds4.h 新增 session rewrite API(ds4_session_rewrite_from_common),用于工具调用后检查点恢复;新增 ds4_dump_text_tokenization 独立分词调试
  • 上游更新(2025-05-15):CUDA 新增 managed KV cache 支持——百万 token 级上下文时,KV cache 张量改用 cudaMallocManaged(统一内存按需分页),避免 DGX Spark 统一内存系统中 GPU 显存分配饿死 CPU 侧。普通上下文仍用 cudaMalloc 设备分配保持性能。通过 ds4_gpu_should_use_managed_kv_cache() 自动判断是否启用。

困惑与突破

  • 困惑:LoRA 不是用于微调的吗?为什么 ds4.c 的 Q 投影用了 LoRA 结构?
  • 突破:LoRA 的低秩分解不仅用于微调——DeepSeek V4 把它作为模型架构的一部分。Q 投影从 4096 直出 32768 维太重了,拆成 4096→1024→32768 两步,参数减少 72%。这不是"微调补丁"而是"架构瘦身"。

Day 7 — 量化与矩阵运算

日期:2025-05-09 状态:已完成

今日摘要

  • 掌握了位操作提取 2-bit/4-bit 值的技巧
  • 理解了量化 block 布局:Q2_K(84B/256权)、Q4_K(144B)、IQ2_XXS(66B)
  • 学习了非对称量化策略:路由专家 2-bit、其他组件 4-8 bit
  • 理解了查表法在 IQ2_XXS 解码中的应用
  • 学习了 matvec 矩阵-向量乘法在推理中的核心地位
  • 关键洞察:284B 模型压缩到 81GB,在 128GB MacBook 上可运行

困惑与突破

  • 困惑:2-bit 量化意味着每个权重只有 4 个可能值(00, 01, 10, 11),这怎么可能不严重损失精度?
  • 突破:关键在于"非对称量化"——不是所有权重都用 2-bit。路由专家用 IQ2_XXS(~2 bit)因为每个 token 只激活 6/256 个专家,量化误差被加权平均稀释。但 attention 投影和共享专家用 Q4_K 甚至 Q8_K 保持精度。这是"好钢用在刀刃上"的工程取舍。

Day 8 — 注意力机制

日期:2025-05-09 状态:已完成

今日摘要

  • 深入理解 Self-Attention:分数计算 → softmax → 加权求和
  • 掌握了 KV Cache 的必要性、滑动窗口和 memmove 实现
  • 学习了 Attention Sink:可学习 logit 参与 softmax 但不贡献 value
  • 理解了 MLA 压缩:KV cache 大小减少 64 倍
  • 掌握了混合注意力:raw (最近128) + compressed (历史) 并行
  • 关键洞察:压缩 KV cache 让百万 token 上下文在本地成为可能

困惑与突破

  • 困惑:滑动窗口 SWA=128 意味着每个 token 只看最近 128 个 token,那 128 之前的信息不就全丢了?
  • 突破:两层机制保住信息:(1) 压缩 KV cache(MLA)把历史 token 压缩后保留,注意力时同时看 raw(最近 128)和 comp(历史压缩行);(2) 43 层堆叠——每层看 128 个 token 的信息经过残差连接传到下一层,128×43 = 5504 的"感受野"。不是"传话游戏会传错",而是"每层都在批注原始信息"。

Day 9 — MoE 混合专家

日期:2025-05-09 状态:已完成

今日摘要

  • 理解了 MoE 路由:哈希路由(前3层)和 top-k 路由(后续层)
  • 掌握了 √(softplus(x)) 路由激活和偏置选择/无偏权重的分离
  • 学习了 SwiGLU 激活:silu(gate) × up,以及 2-bit 量化时的 clamp
  • 理解了路由专家(2-bit)+ 共享专家(8-bit)的非对称精度设计
  • 计算了 MoE 效率:284B 参数中每次只激活约 0.1%
  • 关键洞察:MoE 用"宽度换深度"——参数多但计算少

困惑与突破

  • 困惑:256 个专家只有 6 个被激活,剩下 250 个不是浪费吗?为什么不直接做 6 个大专家?
  • 突破:不同 token 激活不同专家——所有 256 个都会被用到,只是不是同时。43 层 × 6 专家 = 258 次专家调用,通过残差连接逐步积累。这好比一个医院有 256 个专科医生,每个病人只需要看 6 个,但不同病人看不同的 6 个。如果只有 6 个全科医生,每个人都要处理所有问题,反而更差。

Day 10 — RoPE 与推理循环

日期:2025-05-09 状态:已完成

今日摘要

  • 理解了 RoPE 旋转位置编码:通过 2D 旋转矩阵编码相对位置
  • 学习了 YaRN 扩展:混合外推和内插,支持 16× 上下文扩展
  • 掌握了 prefill vs decode:批量处理 vs 逐 token 串行
  • 理解了自回归生成的本质:每步输出成为下步输入,无法并行
  • 学习了 MTP 推测解码的原理和实现
  • 关键洞察:decode 的瓶颈是内存带宽,不是计算——每个 token 要读 81GB 权重

困惑与突破

  • 困惑:RoPE 把向量做 2D 旋转编码位置,但为什么旋转后点积只依赖相对距离?这个性质是巧合还是设计?
  • 突破:这是 2D 旋转矩阵的数学性质:R(a)^T · R(b) = R(b-a)。两个旋转后的向量做点积,结果只包含角度差(相对位置),不包含绝对角度。RoPE 利用了这个性质,所以它不是"巧合"而是精心选择的编码方式。

Day 11 — 线程池

日期:2025-05-09 状态:已完成

今日摘要

  • 学习了 pthread API:mutex、condvar、create/join
  • 理解了线程池设计:worker 循环、generation 计数器、行范围分配
  • 掌握了 cond_wait 必须在 while 循环中的原因(虚假唤醒)
  • 理解了 __thread TLS 防止嵌套并行的机制
  • 关键洞察:主线程也参与计算,不浪费一个核

困惑与突破

  • 困惑:为什么 pthread_cond_wait 必须放在 while 循环里?用 if 不就行了吗——signal 了就说明条件满足了?
  • 突破:POSIX 规范明确允许"虚假唤醒"——没有 signal 的情况下 cond_wait 也可能返回。而且多个 worker 被 broadcast 唤醒时,只有一个能拿到任务,其他必须继续等。while 循环保证每次醒来都重新检查条件,不信任任何唤醒。

Day 12 — HTTP 服务器

日期:2025-05-09 状态:已完成

今日摘要

  • 学习了 POSIX socket:socket/bind/listen/accept/recv/send
  • 理解了每连接一线程模型和单推理 worker 的架构
  • 学习了 poll() 处理慢客户端的 EAGAIN 场景
  • 掌握了 SSE 流式响应格式(data: + 双换行)
  • 关键洞察:推理串行但 HTTP 并发,用队列解耦
  • 上游更新(2025-05-15):新增 /v1/responses 端点(OpenAI Responses API),支持 Codex CLI 等 Agent 工具。Server 日志新增 Responses API 标记和进度日志改进,方便调试多轮工具调用。

困惑与突破

  • 困惑:为什么每个连接一个线程而不是用 epoll/kqueue 做事件驱动?C10K 问题怎么办?
  • 突破:ds4-server 的瓶颈是推理,不是连接。推理同时只能跑一个请求(GPU 独占),所以并发连接数在正常使用中不会超过几十个。每连接一线程简单可靠,是正确的工程选择。如果未来支持 batching,可能需要更复杂的事件模型。

Day 13 — KV Cache 持久化

日期:2025-05-09 状态:已完成

今日摘要

  • 学习了原子写入模式(tmp + rename 防崩溃)
  • 理解了 SHA1 缓存 key:基于 token IDs 而非文本
  • 掌握了磁盘 KV cache 文件格式(KVC 头 + 文本 + session payload)
  • 理解了四种保存触发和前缀匹配查找
  • 关键洞察:磁盘 KV cache 让长 prompt 的重复 prefill 从 10 秒降到 1 秒

困惑与突破

  • 困惑:SHA1 基于 token IDs 而不是文本做缓存 key——为什么?文本不是更直观吗?
  • 突破:同一个文本可能被不同版本的分词器切成不同的 token。比如"你好"可能是一个 token 也可能是两个。用 token IDs 做缓存的 key 保证一致性——同样的 token 序列一定产生同样的 KV cache,不受文本编码变化影响。

Day 14 — API 兼容层

日期:2025-05-09 状态:已完成

今日摘要

  • 学习了手写递归下降 JSON 解析器:json_string/number/skip_value
  • 理解了 OpenAI vs Anthropic API 的格式差异
  • 掌握了 Tool Calling 流程:DSML ↔ OpenAI/Anthropic 双向转换
  • 理解了 Thinking 模式的三种级别和流式输出
  • 关键洞察:零依赖设计——JSON、HTTP、SHA1 全部手写
  • 上游更新:引入 rax 基数树实现工具调用文本记忆(精确回放模型采样的 DSML);随机 tool call ID(/dev/urandom);DSML 参数文本不再转义 <> &,只转义关闭标签;session rewrite 检查点恢复机制;JSON 解析器新增 JSON_MAX_NESTING 256 嵌套深度限制,防止深层嵌套 JSON 栈溢出 DoS
  • 上游更新(2025-05-15):Anthropic live tool continuation——工具调用后保留 live KV 状态不变,下一请求通过 tool_use.id / tool_result.tool_use_id 匹配后只追加后缀 token,跳过完整 prefill。Responses API 同理:call_id 绑定 live KV 前沿,避免"工具调用后重建整个上下文"的停顿。核心思想:采样的 KV 状态是最高保真度的状态,客户端可见的协议对象只应"选择"这个状态,而不是强迫服务器重建它。详见 misc/RESPONSE_API.mdmisc/ANTHROPIC_LIVE_CONTINUATION.md

困惑与突破

  • 困惑:手写 JSON 解析器为什么不用 state machine 而用递归下降?递归不是有栈溢出风险吗?
  • 突破:确实有风险——这就是为什么上游加了 JSON_MAX_NESTING 256 的深度限制。递归下降的好处是代码清晰、容易跳过不关心的字段(json_skip_value)。对于一个只需解析固定几个字段的推理服务器,递归下降的简单性远比完整 JSON 库更实用。

Day 15 — Metal GPU + 总结

日期:2025-05-09 状态:已完成

今日摘要

  • 学习了 ObjC 互操作:C 头文件隐藏 ObjC 类型
  • 理解了 Metal 计算管线:Device → Pipeline → CommandBuffer → dispatch
  • 学习了 Flash Attention 分块计算的思想
  • 理解了零拷贝 MTLBuffer:GPU 直接引用 mmap 内存
  • 用完整请求生命周期串联了 15 天的知识
  • 关键洞察:ds4.c 的哲学——垂直整合、专用化、零拷贝、热路径零分配
  • 上游更新:Metal shader 参数改为 char* 表达只读语义;encoder 可选 buffer 绑定零值占位满足 debug layer 校验
  • 上游更新(2025-05-15):回退了 Metal Q4 expert tensor model views 的扩展(DS4_METAL_MODEL_MAX_TENSOR_BYTES 从 2 GiB 回到 ~672 MiB)。原因:该修复是由 AI 生成的且未经用户确认,上游坚持不接受未经人工验证的修复。

困惑与突破

  • 困惑:Metal 可以零拷贝直接引用 mmap 内存,那 81GB 模型岂不是不需要任何显存?
  • 突破:Apple Silicon 的"统一内存"架构下确实如此——GPU 和 CPU 共享同一块物理内存。Metal 的 newBufferWithBytesNoCopy 只是创建一个指向 mmap 区域的 GPU 缓冲区描述符,不拷贝数据。但 GPU 执行时仍然需要把这些数据读入缓存层次,所以带宽是瓶颈。零拷贝省的是"额外的拷贝",不是"不读数据"。

上游同步 — 2026-06-16

子模块vendor/ds4c463029 前进到 e34a808(main 分支,43 文件、~27K 行新增)。本轮主线是 SSD streaming 的成熟化 + ROCm 后端并入 main。已把内容融入各章节:

SSD streaming(融入 Part 6术语表

  • 运行期路由热度淘汰取代纯 LRU:route_hotness[layer][expert] 表每 16 个 decode token 整表右移衰减一位,淘汰最低热度者。表是提示局部的(新 prompt 清零),但常驻缓存跨会话保温——区分"易变启发式"与"昂贵物化缓存"。
  • 三路 prefill 调度:极短走逐 token decode 式、中等走 selected-expert 批量、长 prompt 走全层 layer-major,按量化调阈值。
  • 缓存上限与 mlock 余量:显式 NGB 推理前封顶保持可锁定;mlock 失败时释放锁定余量、用实测可锁大小继续,而非硬塞可换页条目。
  • 混合精度路由专家(per-layer boosted quants):检测提升层纳入 decode-span 兜底;slab 尺寸类启动时预置、对超大尺寸 freeze+reject 而非 last-writer-wins,防 slab 毒化。
  • streaming API 重构为传一张 ds4_gpu_stream_expert_table 表;CUDA/ROCm 不再是 stub,已实现 expert cache。
  • 新增 --ssd-streaming-preload-experts N(默认预热封顶 4096)。

ROCm / Strix Halo(融入 Part 6 新增 §4c)

  • ROCm 后端并入 main(此前只在独立 rocm 分支)。make strix-halo(别名 make rocm)用 hipccds4_rocm.o + rocm/*.cuh(~18K 行),基于 rocWMMA。
  • 难点:补全 rocWMMA 内部头、扩大 GTT aperture(内核参数让 128GB 系统暴露 ~124GB GPU 可见内存)、避免 mixed IQ2/IQ4 GGUF 触发系统 OOM。详见上游 STRIXHALO.md
  • 融合算子重构为可选后端钩子ds4.c 不再写死 Metal/CUDA 分支,改为 ds4_gpu.h 上的可选钩子,新后端按需实现。

推测解码 / MTP(融入 Part 4

  • 修复 --mtp-draft > 2 时验证器调错 topk_tensor 参数(单行取 top-N vs 每行取 top-1),导致接受了模型从未生成的 token。bug 只在深度 >2 暴露(深度 2 走专用 argmax 分支)。回归测试 --mtp-verify-depth 断言"每个提交 token 是其位置近似 argmax"这条不变量。

Agent / Serving(融入 Part 5

  • 未关闭 <think> 内的工具调用恢复:检测到完整 stanza 开头时强制喂 </think> 前向恢复,而非重写已采样上下文。
  • edit 工具锚定 [upto] 匹配修复(old tail anchor 在 old head 之后找不到)+ 新增 ds4-agent edit 回归测试纳入 make test
  • 终端 raw 模式状态在 quit/help/Ctrl+C 路径上正确恢复。

工程治理(融入 Part 1

  • 新增 QA_BEFORE_RELEASES.md 发布前 14 节 checklist(覆盖 Metal/CUDA/ROCm/分布式/SSD streaming/KV cache/agent 等组合)。

困惑与突破

  • 困惑:为什么运行期淘汰用"每 16 token 整表右移衰减"而不是逐 token 衰减或 LRU 时间戳?
  • 突破:逐 token 衰减每步要扫整张 [layer][expert] 表(43×256=11K 项),太贵;纯 LRU 又不适应工作负载切换。整表一次性 >>= 1 把衰减摊到 16 token 一个周期,开销恒定且小;新热点会因连续命中迅速盖过衰减变热,旧热点 16 token 后就降温——刚好匹配 decode 的局部性节奏。这是"精度换成本"的经典工程取舍:淘汰启发式不需要逐 token 精确,只需要相对热度排序大致正确。