【FHE】我们如何实现同态加密推理(四):为什么我们最终放弃了 BFV——明文模上的除法如何逼死 GELU(纯 C11 · 零依赖)
项目仓库
Gitee 主仓:https://gitee.com/pei-xiaoguang/kestrel-llm
GitHub 镜像:https://github.com/m13253246268-ship-it/kestrel-llm
0. 一句话结论
我们引擎里同时留着两代同态加密实现:vllm_fhe.c(BFV 风格)和 vllm_ckks.c(CKKS 风格)。
换代不是因为 BFV "不好",而是因为一个非常具体的语义问题:在 BFV 的明文模环上做除法,得到的是"模数意义下的解",不是实数除法。而 Transformer 里那些非线性函数(GELU、SiLU)的逼近多项式天然带非整数系数——它们塞不进整数模环。
CKKS 用 scale 管理近似误差,才让这条路走得通。
1. BFV 那一代的真实设计
先把话说明白:第一代实现不是玩具,它有一份完整、自洽的设计。以下摘自 include/core/vllm_fhe.h 的头注释(原文):
| 项 | 值 | 出处 |
|---|---|---|
| 方案 | Ring-LWE / BGV 风格 leveled HE(Fan-Vercauteren BFV) | vllm_fhe.h |
| 环 | R_q = Z_q[x]/(x^n+1),n = 1024 |
VFHE_N_DEFAULT |
| 密文模 | q = 2^64 - 2^32 + 1(Goldilocks prime,64-bit 域) |
VFHE_Q_BITS 64 |
| 明文模 | t = 12289 |
VFHE_T_DEFAULT |
| 缩放 | Delta = floor(q/t),明文编码为 Delta * m(BFV scaling) |
头注释 |
| 乘法语义 | 3 分量 tensor + (t/q) rescale;解密用 (1, s, s^2) |
头注释 |
| 为什么选 t=12289 | t-1 = 12288 = 6 × 2048,支持 n=1024 的 SIMD 槽位打包 |
头注释 |
| 噪声 | 均匀小范围 [-B, B),B=8 |
VFHE_B_NOISE |
| 私钥 | 稀疏三元 {-1,0,1},非零个数 hw = 64 |
VFHE_KEY_HW |
1.1 一个刻意的取舍:不做重线性化
头注释里写得很直接(原文):
不做 relinearization:一次乘法分量数 =
deg(a)+deg(b),解密使用1, s, s^2, ...最高 5 分量(深度 2)。
代码里的体现是:
/* 密文分量数:2 (level1) / 3 (level2) / 5 (level3) ... */
#define VFHE_CT_MAX_COMP 5
这是个典型的原型期取舍:跳过 key-switch(重线性化)能少掉一整套密钥生成与数字分解逻辑,代价是密文尺寸随深度按分量数增长,而且深度被硬顶在 2。
1.2 它已经跑通了什么
同一份头文件里列出的自测项,说明这一代确实端到端验证过:
T1 定点编码往返精度
T2 对称/公钥 加密-解密往返
T3 同态加法(含负数中心化)
T4 同态乘法(深度 1)
T5 混合表达式 ((a*b)+c) 深度 2
T6 定点数同态加/乘
注意 T4 标注"深度 1"——这不是谦虚,是硬限制。
2. 出问题的地方:非整数系数无处安放
Transformer 的非线性激活不是多项式。工程上的做法是用多项式逼近它,例如 SiLU / GELU 的逼近式、LayerNorm 里的倒数、以及自举里的 sin 折叠。
这些逼近式的系数是实数,比如 0.5x + 0.7978...x³ - ...。
在 BFV 里,明文住在 Z_t(模 t 的环)里。你想表示 0.7978,只能把它"放进"模环——比如取 round(0.7978 × Delta)。问题在于:接下来所有的运算都在模 t 的意义下进行,除法给出的不是实数除法,而是模逆。
于是会出现两种后果:
-
精度语义错位:
t上的模运算结果,在解密后需要除以Delta才回到实数。当中间结果经过多次乘法与"除法",噪声不再只是"加性小扰动",而是与模数结构耦合——你很难再给出干净的误差界。 -
动态范围被
t卡死:t=12289只有约 13.6 bit。头注释里已经标注了这条硬限制(原文):深 2 需
t < ~5150,仅深 1也就是说,为了保住深度 2 的正确性,
t要压到 5150 以下;而我们默认的12289只够深度 1。这与 T4 标注的"深度 1"完全对应。
这就是死结:一个 28 层的模型,加上自举内部几十层深的折叠逼近,需要的乘法深度是几十,而 BFV 原型被卡在 1~2。
这里要特别说明:BFV 本身不是错的,也不是弱于 CKKS。BFV 适合精确整数运算的场景(例如需要精确的整数聚合、计票、集合运算)。我们的场景是"近似实数神经网络的推理",与 BFV 的精确整数语义天然不匹配。 头部注释里把这条判断写得很清楚,原文:
非整数系数多项式(GELU 等)在 BFV 明文模下不可行(模环除法给出模数解),CKKS 的 scale 管理 + 模数切换(rescale)天然支持——本模块解决该缺口。
3. CKKS 补上了什么
第二代 vllm_ckks.c 换了两件事:用 RNS 多素数链代替单一模数,用 scale 代替"精确整数除法"。
| 项 | BFV 那代 | CKKS 这代 | 出处 |
|---|---|---|---|
| 密文模 | 单个 64-bit Goldilocks q |
RNS 链:CKKS_NPRIMES 个 60-bit 素数,每个 q_i ≡ 1 mod 2n |
vllm_ckks.h |
| 实数编码 | 定点 round(x · 2^K) mod t |
scale = 2^60:X = round(x · S) |
CKKS_SCALE_BITS 60 |
| 乘法后处理 | (t/q) rescale(BFV scaling) |
rescale = 模数切换切掉一个素数,把 scale 从 2^120 拉回 2^60 |
ckks_rescale() |
| 深度瓶颈 | t 的大小(t<~5150 才够深 2) |
链长(NPRIMES),32 个素数 ≈ 常规深 10 |
CKKS_NPRIMES 注释 |
| 乘法分量 | 不做 relin,comps = 深度+2,上限 5 |
有 relin,3 → 2 分量 |
ckks_relin() |
| 槽位旋转 | 无 | Galois 自动态 + key-switch | ckks_rotate_k() |
关键的一句话:CKKS 把"精确"换成了"可控的近似"。 乘法后 scale 从 2^60 翻到 2^120,rescale 切掉一个素数把它拉回来——误差不再被模数结构耦合,而是变成可以被 scale 追踪的近似误差。这才是能量化误差、逐层定标(本系列第 9 篇)的前提。
还有一个副作用值得单说:CKKS 这代的私钥非零个数从 hw=64 降到了 hw=8。原因写在头注释里(原文):
降 hw 控制 bootstrapping 折叠混叠:混叠
I ~ hw/2 = 4,sin 逼近多项式 9 次;原型无安全可接受。
也就是说,为了自举的折叠逼近能收敛,我们主动把私钥稀疏度降下来——代价是安全性进一步退化(本项目本身就不主张安全强度,见 §5)。这类"为了数值可行性牺牲密码学参数"的取舍,在原型阶段是常态,但必须写清楚。
4. 两代并存:为什么不删掉旧的那代
vllm_fhe.c 至今留在源码树里。原因有三:
- 它是槽位打包与定点编码的参考实现。
vllm_ckks.c的头注释明确写着它复用了vllm_fhe的评估同态(R_t ≅ ∏ F_t)与槽位打包思想。 - 它是自测链条的基础。
vfhe_self_test()的 T1–T6 是"最简单的正确性验证",改动底层的 NTT 或模乘时,它是最快的一道防线。 - 它是一份"反面教材的正本"。把"为什么这条路走不通"的原样设计留在树里,比写一篇总结文档更有说服力——注释里的那条限制(
t<~5150,仅深 1)就是证据本身。
代价也很实在:两套密钥/密文结构、两套自测、两份维护成本,而且新人容易走错门。这个取舍我们目前是选择保留。
5. 安全边界(务请读完)
本文所述参数为机制验证级,远低于 HE 参数标准的 128-bit 水平,
不得用于保护真实数据。
本文主张的是:设计取舍的工程记录。
本文不主张:安全强度、性能优越性、与 BFV/CKKS 任一方案本身的优劣评判。
特别提醒:本文 §3 提到的 hw=8、非密码学 RNG、以及 n=2048,共同意味着这套实现不具备密码学安全强度。它验证的是"近似实数神经网络能不能在密文下算得动、算得对"。
6. 这一篇的未解问题
t的上界推导我们只做了实测:头注释里"深 2 需t < ~5150"是一个经验界,我们没有把它整理成一条可复用的噪声增长公式。如果谁有干净的推导,欢迎指教。- 换到 CKKS 之后的误差传播仍是逐层实测:
scale让误差"可追踪",但"可追踪"不等于"有闭式界"。链尾出现的超阈项(u27中t1h1=3.82e-2、t3h1=7.97e-2,超出verify_layer默认容差3e-2)就是这个问题的一次现形。 - 是否该给 BFV 那代一个明确的退役条件:目前是"保留但不用"。什么情况下可以删、什么时候必须修它,还没有判据。
下一篇我们回到密码学内核的最低层:手写 negacyclic NTT——为什么不用现成库,以及"怎么证明自己写对了"这件事在密文计算里意味着什么。
浙公网安备 33010602011771号