【FHE 同态加密】我们如何实现同态加密推理(十四):为什么 `RESULT=PASS` 不是判据(纯 C11 · 零依赖)

【FHE 同态加密】我们如何实现同态加密推理(十四):为什么 RESULT=PASS 不是判据(纯 C11 · 零依赖)

关键词:同态加密 | FHE | 验证 | 可复现性 | RESULT=PASS | max|err| | SHA256 | 密文验收 | 大模型推理

导读:在同态加密推理这条链上,驱动打出的 RESULT=PASS 只表示"流程走完",不表示"数值是对的"。我们真的被它骗过一次:一个 8 字节文件缺失,引擎静默换了计算路径,照样打 PASS,而实际 max|err| 已超出验收容差。本篇讲正确的两层判据,以及如何做到"不信任打包者"。

项目仓库

Gitee 主仓:https://gitee.com/pei-xiaoguang/kestrel-llm

GitHub 镜像:https://github.com/m13253246268-ship-it/kestrel-llm

相关文档

术语与数据口径 |

性能与基准 |

构建与复现 |

快速上手 |

架构总览

0. 一句话结论

在我们这套密文链里,驱动打出的 RESULT=PASS 只表示"流程走完了",它不表示"数值是对的"。

这不是理论担忧。我们真的被它骗过一次:一个 8 字节的文件缺失 ,驱动静默换了计算路径,仍然打出 RESULT=PASS (0)------而实际的 max|err| 已经是 4.2e-2 ~ 8.5e-2,超出验收容差。

所以在这条链上,判据和数值是两把不同的尺子,必须分别量。


1. 事故复盘:8 个字节如何制造一次"假通过"

1.1 事件的经过

我们重跑层 0(lay0)做闭环验证。驱动正常退出、正常输出:

复制代码
RESULT=PASS (0)

按当时文档的口径,这就是"通过"。但同一批密文用独立复核工具一量:

复制代码
[U2 t3 h0] max|err|=4.460e-02  FAIL   [E2E:ref-deviate-ok]
verify_layer: FAIL 0/8

8 项全挂 ,而且产出的 u0 与归档版本逐字节不同。

1.2 根因

引擎用一个 8 字节的 f64 文件来决定非线性激活走哪条拟合路径:

文件内容 走的路径 适用场景
1.0 直接拟合(` gate
0.0 或文件缺失 ÷21 折叠(把动态范围压进逼近窗口) M3b 深层

而 tools/drivers/t23_m3p.c 里这两个分支直接读文件、不设"缺失即报错"。原文(含作者当时写的理由):

c 复制代码
/* silu 模式: silu{lay}.bin=1 → 直接拟合(|gate|<=8, 锚定0, M1c 路径; ÷21 拟合有 ~0.0127
 * DC 偏移 → 浅层小 gate 相对误差 12-35%); =0/缺省 → ÷21 折叠(M3b 深层) */
g_silu_direct = 0;
snprintf(sp, sizeof sp, ".tmp_tok/tail/silu%d.bin", lay);
fp = fopen(sp, "rb");
if (fp) {
    double sm = 0;
    if (fread(&sm, 8, 1, fp) == 1 && sm == 1.0) g_silu_direct = 1;
    fclose(fp);
}
if (g_silu_direct) printf("  [silu] lay%d direct\n", lay);

这段注释顺带解释了为什么必须有两条路径 :÷21 折叠的拟合带一个 ~0.0127 的直流偏移,在浅层小 gate 上会造成 12--35% 的相对误差。所以浅层必须走直接拟合。

于是"文件没生成"这件事,被静默解释成"请用折叠路径"------而折叠路径恰恰是浅层的错误选择。

1.3 为什么第二道防线也没拦住

严格说,驱动的输出里确实 有异常信号------段级误差项是 FAIL。但我们的判据设计里有两条规则,把这两个信号都过滤掉了:

  1. 段级 A/B/C/D 的 FAIL 不作为结论。这是有道理的:分段误差是"参考口径产物",中间层误差大不等于最终结果错。
  2. 端到端模式会降级偏差 :在 T23_E2EMODE 模式下,C/D/U2 这类项只报告误差,不计入 fails。

两条规则各自都合理,叠加起来就形成了一个盲区 :一个错误路径产出的、8 项全挂的结果,可以一路绿灯走到 RESULT=PASS。

修好路径(补上那个 8 字节文件)之后,同一层的日志头变成 [silu] lay0 direct,段级误差立刻回到正常量级:

复制代码
[A:xnorm t3] max|err|=5.642e-04  PASS
[B:q0    t3] max|err|=1.167e-03  PASS
[C:attn  t0] max|err|=2.858e-04  PASS

2. 为什么会这样:判据的设计意图与它的边界

把话说公道:RESULT=PASS 的原始设计意图是"流程验收",不是"数值验收"。 它回答的是"这一步跑完了吗、有没有崩、有没有缺文件(它知道的那几种)"。

在流水线调度里这很有用------接力者需要的是"我这跳交付了没有"。但对结论级的正确性,它必须让位给独立复核。

我们后来把这条写进了文档,并且明确要求:

判据日志只能用来排障,验收必须看 verify_layer 的 max|err|。


3. 正确的判据是两层

层 工具 它回答什么 通过标准
第一层:流程(跳内不崩) 驱动 t23lay / t23boot 这一跳跑完了吗? lay 看 RESULT=PASS;boot 看 BOOT=PASS 且 out np=2083 出现 8 次
第二层:数值(算对了吗) verify_layer(独立于驱动) 解密后对明文参考的误差是多少? `max

第二层的实现刻意"不信任任何人":

复制代码
读 sk.bin + 密文 → 解密 → 解码 → 与明文参考逐项比 → max|err| → PASS/FAIL

它不读驱动的判据 ,只读密文和私钥。默认容差 -Tol 3e-2,也可显式覆盖:

bash 复制代码
# 复核层 0 的产出
.tmp_tok/verify_layer.exe -L 0 -Ct u0

# 复核刷新后的密文(前缀 u0r112)
.tmp_tok/verify_layer.exe -L 0 -Ct u0r112

# 位级比对:两个密文是不是逐字节相同
.tmp_tok/verify_layer.exe -BitA u5 -BitB u5b -L 5

3.1 实测输出(本机复核,非引用)

| 对象 | 链长 | 结果 | max|err| |
|-----------------------|----------|-----------------------|--------------------------------------------------|
| u0 | np=16 | VERIFY_PASS (8/8) | 1.2712e-03 |
| u0r112(自举刷新后) | np=112 | VERIFY_PASS (8/8) | 1.2706e-03 |
| u26 / u26r112(链尾) | --- | PASS 8/8 | 4.4707e-03 ~ 2.2993e-02 |
| u27(链尾) | --- | ⚠️ 6/8 | t1h1=3.8216e-02、t3h1=7.9687e-02 超 3e-2 |

一个细节值得注意:自举刷新(boot)几乎不改变数值 ------u0 是 1.2712e-03,刷新成 u0r112 后是 1.2706e-03。刷新的作用是重置模数链,不是"提高精度"。


4. 让"第三方复核"真正成立的四块砖

如果复核逻辑还是打包者自己写的,那只是把信任问题往后挪了一层。所以交付物层做了四件事:

  1. 逐文件 SHA256 + 自校验 manifest

    归档实测:主包 104/104 、链尾 45/45 、工具包 6/6。

  2. 复核工具独立于驱动

    verify_layer.c 与 t23_m3p.c / t23_chain.c 是不同程序,只共享底层的 NTT/CKKS 与那把私钥。

  3. 参考值可自助生成

    复核要对的"明文参考"不是只有我们才有。约 7 GB 的数据包可以用 tools/preproc/ 从模型自助生成------我们核对过生成物与分发物逐字节相同 (权重 9/9、26 个逐层模式文件 26/26、embed4.bin、参考值、sk.bin 全部一致)。

  4. 产物位级可复现

    这是最强的一块,见下节。


5. 最强的一块砖:产物位级复现

复核只能证明"数值在容差内"。真正让"不信任"变得没必要的是------你能算出和我一模一样的东西。

我们用两份独立工作区(一份含既有 .tmp_tok,一份全新克隆)从自助生成的数据出发,重跑了 lay0 与 boot0:

比对对象 结果
u0_{t}_{h}.ct × 8 identical = 8,diff = 0
u0r112_{t}_{h}.ct × 8 identical = 8,diff = 0
lay0.out 日志 213 / 213 行,差异 4 处,全部是耗时行
lay0.err 日志 1 / 1 行,完全相同
boot0.out 日志 19 / 19 行,差异 1 处([boot profile] 总耗时)
boot0.err 日志 65 / 65 行,62 处差异全是 cts: u0=N done (X.Xs) 进度耗时

也就是说:所有数值、判据、分阶段链长逐行相同,唯一的差异是时间。

前提条件(必须一起说) :位级复现要求线程数一致 。T23_NT=4 与 T23_NT=8 的结果在位级上不同------根因是线程局部 RNG 用固定常量播种,见本系列第 13 篇。所以"位级一致"这句话,永远要带上线程数一起说。


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

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

特别说明:本文讨论的"可复现""可验证"都是正确性层面 的,与密码学安全无关。一个能被第三方完全复现的系统,和一个安全的系统,是两件事------在本项目里,前者是我们的目标,后者我们明确不主张。


7. 这一篇的未解问题

  1. u27 那两项超阈怎么定性 ------它们是"这一段确实算坏了",还是"容差 3e-2 定得不合理"?我们没有定论。目前的处理是照实记账并公开,而不是调容差把它变成通过。
  2. -Tol 3e-2 是一个选择,不是定理。它从哪来、对哪一层的哪一类值合适,我们只有经验值,没有推导。逐层容差应该是多少,是本项目最缺的一块理论。
  3. 验证还停留在"产出对不对",没管"输入有没有被换掉" 。如果上一棒交来的密文被替换成另一个合法密文,当前的复核发现不了。要堵这个口子,需要额外的机制(比如对输入的承诺),我们还没做。
  4. "缺失即默认"这个设计模式我们没清理干净 。这次是 silu{L}.bin,同类的"读不到就用默认值"的文件读取,值得全树扫一遍------沉默的默认值比崩溃更危险。

下一篇我们讲工程侧的另一条线:为什么我们把 OpenMP 换成了自研线程池------以及它和"位级可复现"这件事的隐秘关联。

相关推荐
pjj198541 小时前
NLP-情感分析项目(四):训练评估 + 主函数
人工智能·深度学习·机器学习
微三云生态系统架构师-彭丹1 小时前
微团AI红包风控与反作弊引擎:设备指纹与红包池熔断架构
人工智能·架构
FPGA信号处理1 小时前
【信号检测与估计】第四节课:非高斯噪声下的 BLUE、极大似然与 EM 算法
人工智能·算法·机器学习
IT大白鼠1 小时前
彭大帅的AI运维助手——自然语言管理 Linux 集群与网络设备——第 2 篇 · 安全守规矩的 AI:分级安全管控是灵魂
linux·运维·人工智能
闭包不眠1 小时前
端侧AI能省多少服务器钱:把账换成字节算
运维·服务器·图像处理·人工智能·计算机视觉
IT_陈寒1 小时前
Java的HashMap线程安全问题让我深夜掉光了头发
前端·人工智能·后端
CHENKONG_CK1 小时前
恶劣工况下 RFID 赋能喷涂产线自动化分拣与作业
运维·网络·人工智能·自动化·汽车·rfid·rfid
北京中科新远科技1 小时前
AI集群交换网络容量怎么算:端口、收敛比与Leaf-Spine验收
服务器·网络·人工智能