☰
【FHE 同态加密】我们如何实现同态加密推理(十一):密文链协议——两条不变式,和 112 与 2100 的素数分工
2026/10/12 18:25:35 网站建设 项目流程

【FHE 同态加密】我们如何实现同态加密推理(十一):密文链协议——两条不变式,和 112 与 2100 的素数分工

关键词:同态加密 | FHE | 密文链协议 | lay | boot | 接力 | 素数分工 | 112 | 2100 | 大模型密文推理

导读:整条同态加密推理链的契约窄到可以写在信封背面:每一跳是一个幂等的纯函数,下一棒不需要知道上一棒是谁,也不需要信任他。本篇讲清两条不变式、112 与 2100 个素数的分工,以及这套"密文接力"协议为什么能让可复现、可验证成立。

项目仓库
Gitee 主仓:https://gitee.com/pei-xiaoguang/kestrel-llm
GitHub 镜像:https://github.com/m13253246268-ship-it/kestrel-llm

相关文档
术语与数据口径 |
性能与基准 |
构建与复现 |
快速上手 |
架构总览

0. 一句话结论

整条链的契约窄到可以写在信封背面:

layL : u(L-1)r112 ──→ uL bootL: uL ──→ u(L)r112

每一跳都是一个幂等的纯函数:给定输入密文 + 编译好的驱动 + 同一把私钥,输出唯一。这就是"接力"在工程上能成立的全部理由——下一棒不需要知道上一棒是谁,也不需要信任他。


1. 两条不变式

不变式内容为什么重要
输入形态固定每一跳的输入永远是"已刷新、链长 112"的密文任一棒拿到的都是同一种东西,不用为上游差异写分支
输出形态固定每一跳产出 8 枚密文(4 个 token 位置 × 2 个分量)交接包结构统一,验证脚本可以通用

第二条里的"8"值得解释:这一层同时处理4 个 token 位置,而 CKKS 密文有2 个分量(comps = 2,重线性化之后)。所以4 × 2 = 8。


2. 阶段表:T23_PHASE

驱动用环境变量T23_PHASE选择"这一跳是哪一种",源码里的定义(原文):

intphase=0;/* 0=l0 1=boot 2=l1(M2 layer1) 3=t26(layer26) 4=t27(layer27) 5=fin 6=lay */intlay=-1;/* phase==6: T23_LAY 指定中间层 0..25 */
T23_PHASE含义参考前缀权重目录
0l0(第一层,生成私钥)l0_tail/w0
1boot(自举刷新)——
2l1(M2 阶段的第一层)—.tmp_tok/l1w
3t26(第 26 层)l26_tail/w26
4t27(第 27 层)l27_tail/w27
5fin(收尾:norm / logits)——
6lay(中间层,用T23_LAY指定)l{L}_tail/w{L}

中间层的层号由T23_LAY指定,合法范围[0, 27](代码里有显式检查,越界直接退出)。阶段3/4/6走同一套参数:

g_fold=1024.0;g_chk=4096.0;g_refpre=pref;g_causal=1;

注意g_causal = 1:中间层与尾层都是因果注意力(这符合自回归语言模型的事实),而g_fold/g_chk是这一个阶段的折叠与校验尺度。


3. 为什么lay只要 112,而boot要 2100

这是最初最让人困惑的一点:同一层数据,为什么两个程序编的链长差近 20 倍?

程序编译期链长实际看到的链长原因
t23lay112输入 112 → 输出16层内前向只需要"够用"的链
t23boot2100输入 16 → 输出112自举内部的sin折叠约需 31 层深度(本系列第 7 篇)

关键区分:

  • 编译期链长 = 该程序"支持的最大链长",也就是它的内存上界;
  • 实际链长 = 这一刻密文上真正剩多少素数。

所以t23boot编到 2100,不代表它跑的时候链上真有 2100 个素数——它只是必须准备那么多空间,因为modraise那一步(第 7 篇)会先把链抬满,再做几十层深的折叠。

这也直接解释了为什么boot比lay慢得多、吃内存多得多:

lay0 : 1920 s,工作集 ≈1.0 GB boot0 : 4285 s,链长 np=2100(建议内存 ≥8 GB)

4. 私钥从哪来:一把钥匙贯穿全链

这是个容易被忽略但很关键的设计点:

if(phase==0)ckks_sk_gen(&sk,(uint32_t)n);elseif(t23_sk_load(&sk,".tmp_tok/chain/sk.bin")!=0){printf("sk load fail\n");return1;}

只有phase=0(第一层)生成私钥,其余所有跳都从.tmp_tok/chain/sk.bin读同一把。

含义有两面:

  • 好的一面:接力者之间不需要传密钥分发协议——sk.bin就在数据包里(也因此,这个包不能公开分发,它只适合熟人/内部接力);
  • 必须诚实的一面:sk.bin出现在接力包里,意味着任何拿到包的人都能解密整条链。这不是设计缺陷,而是"机制验证"这一阶段的必然——我们还没有做密钥托管或分段解密。

另外,只想生成密钥时可以走T23_KEYONLY=1:驱动会打印KEYONLY: sk.bin saved后退出。这对搭建测试环境很有用(注意:生成目录必须先建好,驱动用裸fopen落盘、不会自建目录)。


5. 交接包与断点续跑

一跳的产物只有三样:

chain/u{L}r112_{t}_{h}.ct # 8 枚密文,单文件 ≈3.5 MB relay_rerun/{lay,boot}{L}.out # 日志 manifest.sha256 # 逐文件哈希,可自校验

而"跑层"脚本(rerun5_lay_boot.ps1)有一个很实用的性质:已有产物自动跳过。也就是说:

  • 跑到一半中断,重跑不会从头再来;
  • 层与层之间天然可并行(不同的人跑不同的层,互不阻塞)。

这是"横向切链"这个设计的直接红利——它不只是为了省内存,也是为了让人可以各跑一段。


6. 一处照实标注的未核实项

boot的日志里会打出:

out np=2083

按文档口径,这一行出现8 次(每层 4 token × 2 分量)视为"刷回满链"的判据。

但**2083与编译期2100、落盘112之间的精确对应关系,本文不做断言**——它涉及modraise抬升到多少、以及折叠过程中又消耗了多少个素数。这个数字需要以源码为准逐行确认,我不在这里凭推测解释它。

这条按本项目纪律标注:未核实的东西不写结论,宁可留一个洞,不给一个猜测。


7. 安全边界(务请读完)

本文所述参数为机制验证级(n=2048、112 素数内层链、2100 素数自举链), 远低于 HE 参数标准的 128-bit 水平,不得用于保护真实数据。 本文主张的是:链协议的不变式与工具链行为。 本文不主张:安全强度、性能优越性。

特别提醒 §4:sk.bin与密文一起在接力包里流转——这套机制目前不具备"服务提供方看不到数据"的性质,请勿据此宣传任何隐私保证。


8. 这一篇的未解问题

  1. 协议没有版本号。如果引擎参数变了(比如链长从 112 改成 128),旧包和新包长得一样,但互不兼容。目前靠"文档约定 + 论文式对齐"来避免混用,没有机制上的防护。
  2. 交接包不带"输入指纹"。下一棒无法验证"我拿到的u{L-1}r112就是上一棒声明的那个"——只能验哈希(如果上一棒把哈希公开的话)。这在本系列第 14 篇里作为验证学的第 4 条未解问题出现过。
  3. phase 2 (l1)与phase 6 (lay)的语义有重叠。它们都做"中间层前向",区别来自开发阶段(M2 阶段只有一层)。对新人来说这是历史包袱。
  4. fin阶段的门槛与前面几层不同(它的输出是对齐 logits 而不是密文链的下一跳),这一点在协议文档里描述得不够显眼——我们自己在复现时就撞过。

下一篇回到工程侧:为什么我们把 OpenMP 换成了自研线程池。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询