第 7 章 · 01 kvpersist.h KV 持久化与 MLA 压缩 本节摘要:本节深潜 ——Colibrì 把每轮对话累积的注意力状态写回磁盘( 文件)的机制。对话跨引擎重启可暖启动:压缩 MLA KV-cache 在每一 turn 后增量追加;写策略 crash-safe(记录数 永远最后写)。本节串起来读头格式、记录字节布局、四组磁盘操作,并澄清 MLA KV 状态 57× 压缩是"内存/延迟/正确性"属性而非吞吐声明。 内容来源:原项目源码 (全文 119 行,本节贴关键行) ⚠️ 注意: 依赖 、 、 、 、 已在外层定义,必须在它们之后 。 是全局开关,默认 (开),置 即彻底关闭持久化(纯内存引擎)。 学习目标 说清 文件头的 8 字节魔数与 8 个 字段含义。
本节摘要:本节深潜
c/kv_persist.h——Colibrì 把每轮对话累积的注意力状态写回磁盘(.coli_kv文件)的机制。对话跨引擎重启可暖启动:压缩 MLA KV-cache 在每一 turn 后增量追加;写策略 crash-safe(记录数nrec永远最后写)。本节串起来读头格式、记录字节布局、四组磁盘操作,并澄清 MLA KV 状态 57× 压缩是"内存/延迟/正确性"属性而非吞吐声明。
内容来源:原项目源码
c/kv_persist.h(全文 119 行,本节贴关键行)
⚠️ 注意:
kv_persist.h依赖Model、KVState、Cfg、now_s()、g_draft已在外层定义,必须在它们之后#include。g_kvsave是全局开关,默认1(开),置0即彻底关闭持久化(纯内存引擎)。
.coli_kv 文件头的 8 字节魔数与 8 个 int32_t 字段含义。kv_rec_bytes() 如何按 MLA 压缩记录字节布局算出每条记录大小。kv_disk_open / kv_disk_truncate / kv_disk_append / kv_disk_load。流式 MoE 引擎的一个现实代价是冷启动贵:第一句话要 prefill 系统提示 + 历史,所有路由专家要从磁盘按需加载一遍。如果用户关掉引擎再开,理想情况下不应该把刚才已经算过的状态再算一次。kv_persist.h 解决的就是这个问题——把每轮 turn 累积下来的压缩 MLA KV 状态增量落盘,下次启动时直接 mmap 回内存,跳过重新 prefill。
这里有一个 Colibrì 风格的诚实前提必须摆明:KV 持久化省的是算 + 磁盘 IO 的成本,换来的属性是内存占用低(因为 MLA 已经压到 1/57)、首句延迟低(暖启动)、语义不变(写回的是原始位置的状态张量,解码出来逐 token 等价)。这些是 memory/latency/correctness 属性,不是 blanket throughput claim。
💡 深潜要点:
.coli_kv不是"加速序列生成"的优化,而是"对话记忆跨进程"的工程能力。它和kv_prefix.h(下一节)是配合关系:前者解决"重启后还在",后者解决"同一进程内下一轮别重算"。
KV_MAGIC / kv_hdr / g_kvsave源码最顶上定义全局开关和魔数:
1 /* kv_persist.h — .coli_kv on-disk KV cache persistence. 2 * Conversations reopen warm across engine restarts: the compressed MLA KV-cache 3 * is appended incrementally after every turn, crash-safe (nrec written last). 4 * Include after Model/KVState/Cfg are defined; requires now_s() and g_draft. */ 8 static int g_kvsave=1; 9 #define KV_MAGIC "COLIKV1\0"
KV_MAGIC 是 8 字节串 "COLIKV1\0"(含结尾 \0,正好 8 字节)。它是文件身份的硬凭证——kv_disk_load() 一上来就 memcmp(mg,KV_MAGIC,8),不匹配直接拒载。第 8 行的 g_kvsave 是全局开关,任何路径都先 if(!g_kvsave) return; 短路。
kv_hdr() 把当前模型的关键几何信息编码成 8 个 int32_t:
11 static void kv_hdr(Model *m, int32_t *h, int nrec){ 12 Cfg *c=&m->c; int nic=0; 13 for(int i=0;i<c->n_layers;i++) if(m->Ic && m->Ic[i]) nic++; 14 h[0]=c->n_layers; h[1]=c->kv_lora; h[2]=c->qk_rope; 15 h[3]=m->has_dsa?c->index_hd:0; h[4]=nic; h[5]=c->vocab; h[6]=nrec; h[7]=0; 16 }
七个字段含义:h[0]=n_layers(层数)、h[1]=kv_lora(MLA 压缩的 KV 低秩维度)、h[2]=qk_rope(解耦 RoPE 的维度)、h[3]=index_hd(若有 DSA 则为索引宽度,否则 0)、h[4]=nic(拥有 DSA 索引的层数)、h[5]=vocab(词表大小,仅作指纹)、h[6]=nrec(当前已落盘的记录数)、h[7]=0(保留位)。
⚠️ 注意:这 7 个字段是模型几何指纹。
kv_disk_load()会拿当前模型的kv_hdr()结果和文件头逐字段比对(h[0]..h[5]),任何一个不匹配就打印ignoring .coli_kv from a different model or version然后return 0。这保证绝不会把别的模型的 KV 状态错误地"暖"进当前模型——一个静默错误远比崩溃可怕。
kv_rec_bytes() 与 MLA 压缩18 static int64_t kv_rec_bytes(Model *m){ 19 Cfg *c=&m->c; 20 int64_t rec = 4 + (int64_t)c->n_layers*(c->kv_lora+c->qk_rope)*4; 21 if(m->has_dsa) for(int i=0;i<c->n_layers;i++) if(m->Ic[i]) rec+=(int64_t)c->index_hd*4; 22 return rec; 23 }
每条记录由三段构成,都是 float(4 字节):
token id(int32_t),用于和 kv_prefix.h 的 fed 数组对账;n_layers × (kv_lora + qk_rope) × 4 字节——这就是 MLA 的压缩 KV:不是每个 head 都存 K/V,只存低秩 kv_lora 维潜向量 + qk_rope 维解耦 RoPE 向量;index_hd × 4 字节,即 faithful DSA 的稀疏索引。把 kv_lora + qk_rope(典型只有几百维)与未压缩的 head_dim × n_kv_heads × 2(K 和 V) 相比,就得到 57× 这个压缩比——这是 MLA KV 状态体量的算术事实,不是引擎吞吐。
💡 深潜要点:为什么是 57× 而不是更高或更低?这是 DeepSeek MLA 结构的设计常数:潜空间维度
kv_lora远小于n_heads × head_dim,信息瓶颈压住了 KV 张量但保留了注意力重建能力。Colibrì 只是忠实地把这个压缩表示原样落盘,不做二次量化。
四个函数里 kv_disk_truncate() 和 kv_disk_append() 共同实现了 crash-safe。先看截断:
42 static void kv_disk_truncate(Model *m, int nrec){ 43 if(!g_kvsave) return; 44 KVState *k=m->kv; 45 if(k->disk_fp){ fclose(k->disk_fp); k->disk_fp=NULL; } 46 FILE *f=fopen(k->disk_path,"r+b"); 47 if(!f){ k->disk_nrec=0; return; } 48 k->disk_nrec=nrec; 49 int32_t nr=nrec; fseek(f,8+6*4,SEEK_SET); fwrite(&nr,4,1,f); 50 fflush(f); fclose(f); 51 }
注意第 49 行偏移 8+6*4:跳过 8 字节魔数 + 6 个 int32(h[0]..h[5]),正好落在 h[6]=nrec。只重写 nrec 一个 int32,数据区不动。
再看追加:
55 static void kv_disk_append(Model *m, const int *hist, int len){ 56 KVState *k=m->kv; 57 if(!g_kvsave || len<=k->disk_nrec) return; ... 67 fseek(f, 8+8*4 + (int64_t)k->disk_nrec*rec, SEEK_SET); 68 for(int p=k->disk_nrec;p<len;p++){ ... 78 fwrite(k->disk_buf, 1, (size_t)rec, f); 79 } 80 fflush(f); 81 int32_t nr=len; fseek(f,8+6*4,SEEK_SET); fwrite(&nr,4,1,f); 82 fflush(f); 83 k->disk_nrec=len; 84 }
策略是数据先写、长度后写:
disk_nrec..len 这些位置的 token + KV 数据顺序写到数据区尾部;fflush 把数据刷到磁盘;nrec 写到头里,再 fflush。后果:如果在写数据时崩溃,nrec 还停在旧值,启动时 kv_disk_load() 最多读到 nrec 条记录,数据区可能多出"半截"尾部,但不会被当作有效记录读入。这就是 crash-safe——引擎绝不会被一个写了一半的 KV 状态污染语义。
kv_disk_load() 的逐 token 校验86 static int kv_disk_load(Model *m, int *hist, int maxctx){ 87 if(!g_kvsave) return 0; ... 92 if(fread(mg,1,8,f)!=8 || memcmp(mg,KV_MAGIC,8) || fread(h,4,8,f)!=8 || 93 h[0]!=w[0]||h[1]!=w[1]||h[2]!=w[2]||h[3]!=w[3]||h[4]!=w[4]||h[5]!=w[5]){ 94 fprintf(stderr,"[KV] ignoring .coli_kv from a different model or version\n"); ...} 95 int nrec=h[6]; 97 if(nrec>=maxctx-8-g_draft){ 98 fprintf(stderr,"[KV] saved conversation (%d tokens) exceeds the context: starting over\n",...); ... 101 for(int p=0;p<nrec;p++){ 102 int32_t tk; if(fread(&tk,4,1,f)!=1){ nrec=p; break; } hist[p]=tk; 103 for(int i=0;i<c->n_layers;i++){ 104 if(fread(m->Lc[i]+(int64_t)p*c->kv_lora, 4, c->kv_lora, f)!=(size_t)c->kv_lora || 105 fread(m->Rc[i]+(int64_t)p*c->qk_rope, 4, c->qk_rope, f)!=(size_t)c->qk_rope){ nrec=p; goto out; } 106 } 107 if(m->has_dsa) for(int i=0;i<c->n_layers;i++) if(m->Ic[i]) 108 if(fread(m->Ic[i]+(int64_t)p*c->index_hd, 4, c->index_hd, f)!=...){ nrec=p; goto out; } 109 } ... 114 fprintf(stderr,"[KV] resumed conversation from disk: %d tokens in %.1fs (no re-prefill)\n",...);
第 92-94 行做魔数 + 几何指纹的双重校验;第 97 行检查 nrec 不超过上下文长度,留 8 个 token 余量给 g_draft(推测解码草稿槽)。第 101-109 行的循环读任何一个 fread 失败都把 nrec=p 收缩,跳到 out 关文件——读到的数据都是完整的、可对账的 token + KV。第 114 行的日志 no re-prefill 正是这套机制给用户的承诺:重启 = 把状态装回来,不是重新算一遍。
.coli_kv 文件 = 8 字节魔数 + 8 个 int32 头 + nrec 条 MLA 压缩记录;魔数和 6 个几何字段是"防止暖错模型"的硬凭证。kv_rec_bytes() = 4 + n_layers×(kv_lora+qk_rope)×4 + (可选)nic×index_hd×4,这是 MLA 压缩 KV 的字面字节布局。fflush 强制落盘;崩溃后 nrec 永远指向完整结尾。kv_disk_load() 在魔数 + 几何指纹 + 上下文余量三重校验后才装回状态,任一失败优雅地"从头开始"。下一节我们将深潜
kv_prefix.h——同一进程内,如何让第 N+1 轮对话复用第 N 轮已经算好的注意力状态,跳过重置只 prefill 新增尾部。