【FHE 同态加密】我们如何实现同态加密推理(十五):实测账本——一跳 1.8 小时,钱花在哪(同态加密推理性能剖析)
关键词:同态加密 | FHE | 性能剖析 | 实测账本 | 自举 | 域变换 | CPU 推理 | 大模型密文推理 | 耗时分析
导读:一次同态加密推理的一跳(lay + boot)在 8 核 CPU 上约 1.8 小时,而这个时间绝大部分不在多项式乘法上——自举的两段域变换单独就占了约 80%。本篇是实测账本,讲清钱花在哪,以及为什么"继续优化卷积基本是白费力气"。所有数字都绑定线程数、且只在同轮交错 A/B 下可比。
项目仓库
Gitee 主仓:https://gitee.com/pei-xiaoguang/kestrel-llm
GitHub 镜像:https://github.com/m13253246268-ship-it/kestrel-llm
0. 一句话结论
一跳(lay + boot)约 1.8 小时(8 核 CPU,纯 CPU、无 GPU/NPU)。而这个时间绝大部分不在多项式乘法上:
自举的两段"域变换"——coeff_to_slot 与 slot_to_coeff——单独就占了自举耗时的约 80%。
结论直接改变优化方向:继续优化卷积基本是白费力气。
1. 观测口径(先交代前提,再上数字)
按本项目《术语与数据口径》§2.5 的纪律:
| 项 | 值 / 说明 |
|---|---|
| 平台 | AMD Ryzen 7 9800X3D(8 核 16 线程) |
| 线程数 | T23_NT(所有数字都绑定线程数,见本系列第 13 篇) |
| 比对方式 | 同轮交错 A/B;不使用跨轮次的绝对耗时 |
| 编译 | gcc -O2 -fopenmp,-DCKKS_N=2048 |
| 链长 | lay 编译期 112;boot 编译期 2100 |
为什么强调"同轮交错":跨会话/跨轮次的机器状态(频率、缓存、后台负载)不可控,绝对值不可比。这是本文所有数字成立的前提。
2. 一跳的账单
| 阶段 | 耗时 | 判据 | 内存 |
|---|---|---|---|
lay0 |
1920 s | RESULT=PASS |
工作集 ≈1.0 GB |
boot0 |
4285 s | BOOT=PASS,8× out np=2083 |
链长 np=2100,建议 ≥8 GB |
| 合计 | ≈ 6205 s ≈ 1.72 h | — | — |
链尾同数量级(供参照):boot26 4469 s、boot27 4489 s。
一眼可见的结构:自举比层内前向贵一倍以上。 一跳 1.8 小时里,约 1.2 小时花在 boot 上。
3. 自举的钱花在哪(本文核心)
boot0 的 [boot profile] 分段(字段名对应本系列第 7 篇的七段流水线):
| 段 | 耗时 | 占自举比例 |
|---|---|---|
coeff_to_slot |
1688 s | ~39% |
slot_to_coeff |
1716 s | ~40% |
sin_fold(实部 + 虚部) |
~850 s | ~20% |
modraise / rotate_k / conj_extract / restore_merge |
余下 | 剩余零头 |
total |
4285.03 s | 100% |
两段域变换合计约 3400 s,占 80%。
3.1 这个结论推翻了什么
我们最初的优化直觉是"优化最热的内核"——也就是 NTT / 多项式乘法,因为它是每次乘法都要跑的东西,profiling 里出现次数最多。
但出现次数最多 ≠ 占用时间最多。在这条链上,真正的成本是:
coeff_to_slot / slot_to_coeff
= 大量 Galois 旋转
+ 每次旋转都要做 key-switch
+ key-switch 要消耗那对 3×2 的密钥(每个都是 nprimes × n)
也就是说:瓶颈在"搬运"(域变换 + 置换 + 密钥切换),不在"计算"(卷积)。
而 key-switch 的密钥体积是 nprimes × n × 64 bit 量级——链长 2100 时,这些密钥本身就大到缓存装不下。所以这里的成本很可能不是算力,而是内存带宽。
⚠️ 这一条是推断,不是结论。我们只测了"这段花了多少时间",没有测它的微架构行为(缓存命中、带宽、指令数)。要证实"瓶颈是带宽",需要额外一轮 profiling。按本项目纪律,这里标注为未验证的假设。
4. 精度账本(一并公开)
性能之外,本项目同样需要公开的是精度,而且这一份更不能挑数字。
| 对象 | 结果 | max|err| |
|---|---|---|
u0(lay0,np=16) |
PASS 8/8 | 1.2712e-03 |
u0r112(自举刷新后,np=112) |
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) |
三个值得注意的点:
- 自举几乎不改变数值:
1.2712e-03→1.2706e-03。自举是"加油",不是"纠错"(本系列第 7 篇)。 - 误差随深度增长:从层 0 的
1.27e-03到链尾的2.3e-02,涨了约一个数量级。 - 链尾有超阈项:
u27八项中两项越过3e-2。我们不调容差把它"变成通过",而是照实记账。
5. 另外三份账
5.1 位级复现账
| 比对 | 结果 |
|---|---|
u0 × 8 vs 归档 |
identical 8 / diff 0 |
u0r112 × 8 vs 归档 |
identical 8 / diff 0 |
lay0.out 日志 |
213 / 213 行,差异 4 处全是耗时行 |
lay0.err 日志 |
1 / 1 行,完全相同 |
boot0.out 日志 |
19 / 19 行,差异 1 处(profile 总耗时) |
boot0.err 日志 |
65 / 65 行,62 处差异全是进度耗时 |
(前提:线程数固定。)
5.2 数据账
| 项 | 结果 |
|---|---|
| 自助生成权重 vs 分发物 | 各 9/9 逐字节相同 |
silu0..25.bin(26 个) |
26/26 逐字节相同 |
embed4.bin / 参考值 / sk.bin |
相同 |
| 归档 manifest 自校验 | 主包 104/104、链尾 45/45、工具包 6/6 |
这条最重要:约 7 GB 的数据包不随仓库分发,但任何人可以用同一个模型把它重新生成出来,且逐字节相同。
5.3 进度账
| 部分 | 状态 |
|---|---|
| 层 0–4(链头) | ✅ 可复现、可独立验证 |
| 层 26–27 + 收尾 | ✅ 已跑通并归档 |
| 层 5–25(21 跳) | ⏳ 待认领 |
6. 安全边界(务请读完)
本文所述参数为机制验证级(n=2048、112 素数内层链、2100 素数自举链),
远低于 HE 参数标准的 128-bit 水平,不得用于保护真实数据。
本文主张的是:本实现在我们这台机器上的工程量账本。
本文不主张:安全强度、性能优越性、与任何其他方案的对比。
特别声明:本文所有耗时都是我们这份实现、我们这台机器的数字,不能用来推断任何其他系统(包括同态加密推理的商用实现)的性能。按项目纪律,跨轮/跨平台绝对值不可比。
7. 这一篇的未解问题
- 域变换为什么贵,没有微架构归因(§3.1 的标注)。这是本项目最该做、收益最大的一块分析。
- 没有"最小可用链长"的扫描。
boot编到 2100、实际用多少、能不能压到 1500——直接决定内存与耗时,却是拍的。 - 精度预算没有闭环。逐层实测 + 逐层定标(第 9 篇),但没有"从 28 层反推每层可用误差"的推导。链尾超阈项是这个问题的一次现形。
- 单跳耗时里"内存带宽"的占比未测。如果瓶颈真是带宽,优化方向应该是减少取数(降链长、减密钥体积),而不是加速计算——但这一点还没被证实。
下一篇是最后一篇:未完成清单与已知边界——把这一路所有"我们知道但没解决"的事一次列清。
浙公网安备 33010602011771号