ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

kimi-k3-in-c如何从磁盘流式读取109GB Trunk?Pinned前缀+环形槽位设计,以及LRU命中率为何恰好是0%

2026/9/2 12:52:41 拓冰建站 浏览量
kimi-k3-in-c如何从磁盘流式读取109GB Trunk?Pinned前缀+环形槽位设计,以及LRU命中率为何恰好是0% kimi-k3-in-c如何从磁盘流式读取109GB TrunkPinned前缀环形槽位设计以及LRU命中率为何恰好是0%【免费下载链接】kimi-k3-in-cA 2.78-trillion-parameter Kimi K3 running inference on a single CPU in 8.24 GB of RAM. Portable C99: no BLAS, no framework, no GPU.项目地址: https://gitcode.com/gh_mirrors/ki/kimi-k3-in-ckimi-k3-in-c是一个用纯 C99 写的推理引擎能让 2.78 万亿参数的 Kimi K3 在单 CPU、8.24 GB 内存零 GPU下跑通推理。它不依赖 BLAS、不依赖任何框架整个引擎只有 176 KB。而它最精妙的设计之一就是如何从磁盘流式读取 109 GB 的 Trunk密集层主干把 93 层权重打包进一个trunk.bin用Pinned 前缀钉住层 环形槽位ring slot管理内存配合预取线程把磁盘读取藏进计算里。更反直觉的是——如果这里用最常见的 LRU 淘汰策略命中率会恰好是 0%。这篇文章带你完整拆解这套磁盘流式推理的机制。一、为什么必须流式读而不是量化压缩先给个背景账目你就能理解 109 GB 这个数字从哪来组成大小策略路由专家896×92 层MXFP4 4-bit 打包1.447 TB占 checkpoint 93%永不驻留按需从磁盘读密集主干 Trunk93 层bf16108.81 GB每个 token 都要走一遍但可流式Embedding lm_head4.70 GB驻留内存关键矛盾在于Trunk 的 93 层每个 token 都要完整走一遍没有任何稀疏性可跳过。所以问题不是如何少读而是读进来的字节放在哪。而量化压缩 Trunk是看起来最顺手的方案却被实测否定了Kimi K3 官方技术报告明确说明只有专家用了 MXFP4 量化感知训练非专家部分注意力投影、共享专家、路由器等保持高精度。在真实注意力张量上实测事后 int4 量化的权重重构误差是 17.4%int8 只有 0.96%——约 18 倍差距。流式读取则零误差读进来的就是 checkpoint 自己的原始字节。于是设计目标变成——让内存需求变成一个旋钮dial而不是一道地板floor。二、一次性打包93 层 → 一个 109 GB 的 trunk.bin原始 checkpoint 是 96 个 safetensors 分片共 1.56 TB某一层的几十个张量散落在一个 17 GB 的文件里读一层意味着在分片里翻找一堆不连续的区间。所以运行前有一个只做一次的打包步骤scripts/pack-trunk.sh约 4 分钟./scripts/pack-trunk.sh ~/k3model ~/k3trunk它便宜的原因是一个关键事实每一层的 Trunk 张量在分片内本来就是一段连续区间。所以打包只是 93 次区间复制而不是散读拼装。打包器会先验证连续性——如果区间里有空洞整段复制会把专家字节也拖进来把 Trunk 撑到 400 GB打包器会直接拒绝。打包产物就是~/k3trunk/下的trunk.bintrunk.json每层的偏移、大小、张量清单层 L 落在一个已知偏移一次pread读完——这是后面一切流式设计的地基每段区间头部按4096 字节对齐这样才能用O_DIRECT直接 I/O绕过页缓存最大的一层第 0 层唯一带 33792 宽密集 MLP 的层是2.341 GB这个数字决定了后面环形槽位的最小尺寸。为什么坚持O_DIRECT实测数据在 32 GB 内存上限下走页缓存的读取会退化到 1878 MB/s而O_DIRECT保持 6553 MB/s。原因是每个流式层每 token 只读一次、淘汰前绝不会再用页缓存存它只是白白复制一遍、还挤掉别的东西。三、Pinned 前缀 环形槽位流式机制本体核心设计一句话预算里能钉住多少层就钉住多少层Pinned 前缀剩下的层在少数几个环形槽位里轮转进出。为什么必须是**前缀prefix**而不是缓存这正是全文最关键的一处放在后面第五章细讲。先看工程细节代码在 src/io/k3_trunk.c 与 src/io/k3_trunk.h3.1 钉住层用精确尺寸分配不统一尺寸所有槽位统一按最大层2.34 GB分配的话等于把预算浪费近一半——因为前缀钉住永远先钉第 0 层它恰恰是最大的。所以钉住层各自按精确大小分配环形槽位才用统一尺寸。3.2 钉住数与槽位大小互相依赖迭代到不动点钉住更多层 → 流式层变少 → 槽位可以变小 → 省下的预算又能钉更多层。两者互相咬合代码用一个有界的迭代循环收敛到不动点for (int pass 0; pass 4; pass) { size_t avail budget slot * (size_t)nring ? budget - slot * (size_t)nring : 0; int n 0; size_t used 0; while (n tr-n_layers used tr-lay[n].nbytes avail) { used tr-lay[n].nbytes; n; } size_t need 0; /* 还没被钉住的最大一层 */ for (int L n; L tr-n_layers; L) if (tr-lay[L].nbytes need) need tr-lay[L].nbytes; if (need 0) need 4096; if (n npin need slot) break; /* 收敛 */ npin n; slot k3_align_up(need, K3_TRUNK_ALIGN); }3.3 用 2 MB 大页分配缓冲区不是化妆品级优化O_DIRECT读取期间内核必须把目标页钉住pin。一个 2.37 GB 的槽位若用 4 KB 页是约 57.8 万页每 token 流式层被读 93 次合计每 token 近 5400 万次页面 pin 操作光内核记账就烧掉数秒——而这段开销不会出现在引擎自己的 I/O 计时器里。改用 2 MB 大页后操作数直接除以 512。3.4 预算不足时的降级与自报家门第二个环形槽位用于异步预取要付出整整一个槽位的内存在低配预算下可能花不起。代码的策略是申请而非保证装得下就给 2 个槽位装不下就退回 1 个槽位同步读取并在启动日志里明确告知读没有和计算重叠请加--trunk-gb。正确性不依赖走哪条路输出 token 完全一致。各预设档位下钉住多少层预设Trunk 预算钉住层数稳态命中率laptop3 GB少数几层≈ 5%desktop16 GB约 10 层10.8%workstation60 GB48 层51.6%server110 GB90 / 93 层96.8%四、预取线程算第 L 层时把第 L1 层读进来Trunk 的访问顺序是固定的——每个 token 都是层 0、1、…、92下一个要读谁永远提前知道。这让预取变得安全且简单主线程在层 L 上计算时一个独立的 reader 线程把层 L1 读进备用槽位。主线程: [绑定 L][计算 L][绑定 L1][计算 L1]... reader: \____ 读 L1 ____/ \____ 读 L2 ____/实测在 laptop 预设上这一条优化把 71.75 s/token 降到42.27 s/token1.70×甚至超过了同模型但给 4 倍内存、不做重叠的效果。这里还有个正确性细节值得注意1 个槽位 预取线程是危险的——worker 会把 L1 的数据直接覆盖在主线程还在计算中的层 L 的字节上而且读取成功、无报错、输出流畅但全错。所以代码规定只有 ≥2 个槽位时才启动 reader 线程。真实测过的事故数据同一段 prompt正确输出是17374 20829 10 427 ...被覆盖后变成32609 2329 146429 2539 ...没有任何诊断信息。五、核心揭秘为什么 LRU 命中率恰好是 0%现在回答标题里最反直觉的问题。LRU最近最少使用是缓存的默认选择但在 Trunk 这里是灾难性的错误——不是次优是错到最坏方向。原因一句话就能说清引擎每个 token 都按 0→92 的固定顺序循环扫描93 层。而 LRU 的淘汰规则是淘汰最久没用的。当扫描回到层 0 时层 0 恰好就是全缓存里最久没用的那个——它刚在扫描转一圈的过程中被淘汰了。换句话说循环顺序扫描就是 LRU 的经典病理场景用 LRU 开 90 个槽位去缓存 93 层命中率 恰好 0%加内存也救不了因为每次回来都精确踩中刚被驱逐的层用钉住前缀钉住前 N 层命中率 N/93确定性可预测钉 90 层就是 96.8%钉 10 层就是 10.8%。启动日志会直接把这个确定性命中率打出来而不是留给你猜trunk stream: 108.81 GB packed, 10/93 layers PINNED (13.16 GB), ring 1 x 2.37 GB reads use O_DIRECT (page cache bypassed) deterministic hit rate 10.8% (a cyclic scan defeats LRU, so a pinned prefix is used instead)对比一下两种策略的每加 1 GB 内存换到什么策略8 GB 预算90 GB 预算行为LRU 环形缓存0.0%0.0%永远 0内存全白给Pinned 前缀≈5%96.8%每 GB 都换到应得的份额这就是为什么 src/io/k3_trunk.c 的头部注释直言LRU 在这里是最坏的淘汰策略并解释专家缓存继续用 LRU是因为专家复用是数据相关的取决于路由而 Trunk 复用是结构确定的固定循环扫描。同一套代码库里两种缓存用了两种策略正是因为访问模式本质不同。六、延伸阅读用一条 trace 回答缓存该开多大同样的思想也用在了 1.45 TB 专家池的 LRU 缓存调参上因为路由选择不依赖缓存跑一次就能记录全部(层, 专家)请求序列再用 tools/sim_cache.py 在任意容量下回放对比 LRU 与理论最优Beladypython3 tools/sim_cache.py tests/fixtures/expert_trace.bin结论之一同样反直觉8→64 GB 之间 LRU 命中率完全平躺36.24% 纹丝不动而 Belady 从 39% 爬到 62%——平的锅在策略不在负载。完整数据在 docs/data/expert-cache-capacity.txt。七、想看源码三处入口流式 Trunk 全部逻辑src/io/k3_trunk.c打包布局注释在 src/io/k3_trunk.h 头部含为什么 LRU 是最坏策略的完整推演一次性打包脚本scripts/pack-trunk.sh 与 tools/pack_trunk.py专家 LRU 缓存与三阶段流水线src/cache/k3_cache.c运行报告里的命中率/IO 占比解读docs/PERFORMANCE.md一句话总结kimi-k3-in-c 流式读取 109 GB Trunk 的配方是——一次打包成对齐的单文件 O_DIRECT 大页直读 Pinned 前缀确定性 N/93 命中 环形槽位 固定顺序预取重叠计算。而LRU 命中率恰好 0%这个设计反面教材提醒我们缓存策略必须匹配访问模式——顺序循环扫描面前最久未用就是马上要用。【免费下载链接】kimi-k3-in-cA 2.78-trillion-parameter Kimi K3 running inference on a single CPU in 8.24 GB of RAM. Portable C99: no BLAS, no framework, no GPU.项目地址: https://gitcode.com/gh_mirrors/ki/kimi-k3-in-c创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考