【实测】服务器看不懂你问了什么,却算出了答案——同态加密推理是怎么实现的(RNS-CKKS 全流程拆解)
发布时间:2026/9/16 5:04:53 锦皓数字建站
`)
一句话结论我把一个 2B 大模型的推理过程整个搬进了密文里——从第一层到最后一层中间的每一层中间结果都是密文服务器全程看不到任何一个明文数字但它确实把答案算出来了。本文讲清三件事用的什么技术、怎么实现的、一次完整的推理流程长什么样附可复制的编译与运行命令、实测数据以及 6 个会让人得出错误结论的坑。所有数字来自本机实测AMD 9800X3D8C/16T驱动源码与日志均可核验。目录一、先说人话同态加密推理到底在干什么二、为什么这事比加密传输难得多三、用到的技术栈全景四、怎么实现的参数、代码结构、编译命令五、一次完整的推理流程逐跳拆解 真实日志六、实测数据七、6 个坑其中 3 个会让你得出错误结论八、诚实边界必读九、核验入口一、先说人话同态加密推理到底在干什么普通 AI 的流程你问我这份体检报告有问题吗这句话原文明文发到服务器服务器原文明文算完返回答案。同态加密FHE的流程你的问题 --加密-- 密文 -- 服务器在密文上算 -- 密文答案 -- 你解密 -- 答案 ↑ 服务器全程不知道你问了什么关键在于在密文上算这四个字。服务器拿到的是一堆看起来完全随机的大整数它对这些数字做加法和乘法算出来的结果解密之后正好等于明文计算的结果。这就是同态。放到大模型上难度直接翻 1000 倍——因为一次推理不是一次乘法而是28 层 ×注意力 MLP 两个 RMSNorm中间所有张量Q/K/V、注意力分数、激活值、隐状态全部必须是密文。这就是本文要做的事让一整条 Transformer 推理链在密文上跑起来。二、为什么这事比加密传输难得多很多人第一反应是我 HTTPS 不就行了。不是一回事。HTTPS 保护的是传输过程——数据到了服务器服务器要解密才能算。而 FHE 保护的是计算过程——服务器从来没有解密过。代价是三堵墙墙 1密文上只天然支持加法和乘法Transformer 里到处都是非线性Softmax、SiLU/GeLU、RMSNorm 里的1/sqrt(...)、最后的 argmax。这些在密文上没有原生算子。只能这么绕明文里的一行密文里怎么算1/sqrt(mean(x²)eps)多项式拟合 牛顿迭代要迭代 3~4 次SiLU(x) x·sigmoid(x)多项式分段拟合softmax先算 exp多项式拟合再算倒数牛顿迭代再乘argmax不能算——只能把整个 logits 解密出来在外面比注意最后一行argmax 必须解密。所以 FHE 推理是密文推理 末端解密不是全流程黑箱。这是这类方案的一个本质边界别被宣传语骗了。墙 2噪声会累积乘一次涨一次CKKS 是近似同态加密每个密文里带噪声。每做一次乘法噪声就涨一截噪声涨到超过模数就解不出来了。控制手段是模数链Modulus Chain给密文准备一串素数每消耗一个rescale就把噪声压回去一格。本项目用的是112 个 60-bit 素数的层链。但链是有限的——28 层不可能一路用到底。所以必须定期刷新。墙 3刷新自举本身就是最贵的操作自举Bootstrapping把一个快没噪声预算的密文刷回干净状态。原理是把解密电路本身用加密的方式算一遍。代价有多高实测一次boot约 74 分钟是层内前向lay约 33 分钟的 2.2 倍而且开销几乎全在两处boot内部分段单次实测 4393.5 s8 个密文耗时占比coeff_to_slotslot_to_coeff域转换1737.20 s 1736.19 s79.1%sin_fold(re)sin_fold(im)正弦查表折叠441.84 s 445.70 s20.2%其余 5 段提升模数、旋转、共轭提取、恢复合并合计0.7%这个 profile 直接定位了瓶颈自举的开销几乎全在slot ↔ coeff 域转换上不在旋转、不在共轭。没有这一行任何优化自举的讨论都是猜。三、用到的技术栈全景技术干什么用本项目实现RNS-CKKS密文方案本体支持近似实数运算适合神经网络自研src/core/vllm_ckks.cNTT / INTT多项式乘法加速把 O(n²) 降到 O(n log n)自研src/core/vllm_ntt.cRNS 分解大整数拆成多个小素数避开大数运算模数链112 个 60-bit 素数模数链 rescale控制噪声增长每层消耗链长自举 Bootstrapping噪声刷新让链能继续用T23_PHASEbootRNS 基底转换coeff↔slot自举的核心步骤占自举 79.2% 开销Galois 旋转 / 共轭槽置换——矩阵乘的转置/移位Galois keysckks_gk_genbaby-step giant-step矩阵乘的旋转次数优化驱动里的matmul2多项式拟合 查表实现非线性函数RMSNorm 倒数、SiLU、exp自研线程池多核并行不依赖 OpenMP 的部分src/core/vllm_tp.c层链驱动把算子串成层和链tools/drivers/语言和依赖纯C11只依赖libc libm零第三方运行时。并行用 OpenMP 自研线程池。四、怎么实现的参数、代码结构、编译命令4.1 关键参数先记住这五个n2048// 多项式次数slots1024// 一个密文能装 1024 个实数 n/2scale2^60// 定点精度LCHAIN112// 层链的素数个数lay 用 112NPC2100// 自举用的素数个数boot 用 2100输出 2083一个密文 素数个数 × n × 8 字节。所以lay输出的密文链长剩 3 时约622 KBboot输出的密文2083 素数约3.6 MB4.2 代码结构引擎 驱动两层src/core/vllm_ntt.c # NTT / INTT多项式乘法 src/core/vllm_ckks.c # RNS-CKKS加解密、乘法、rescale、自举、Galois keys src/core/vllm_tp.c # 自研线程池 tools/drivers/t23_m3p.c # 层链驱动lay / l1 / t26 / t27 / fin tools/drivers/t23_chain.c # 层链驱动boot自举刷新 tools/relay/verify_layer.c # 独立验证器解密→解码→对明文参考算 max|err|为什么要有独立的验证器因为输出是密文没有肉眼可判断的对错。验证器必须不复用被测代码的 PASS 逻辑自己解密、自己解码、自己算误差——否则等于自证。4.3 里程碑怎么切的驱动里的T23_PHASE环境变量就是里程碑的化石层里程碑覆盖T23_PHASE为什么单独切M2RMSNorml1除法是密文上的硬骨头M3层内前向lay/boot自举第一次真正用上M3b末层 输出头t26/t27/finfinal RMSNorm lm_head分块 matmul → logitsM4自举工程化boot从能刷到稳定刷、可验收T23_LAY的合法范围是[0, 27]t23_m3p.c:1635的守卫。4.4 编译命令可复制# lay 驱动层内前向112 素数gcc-O2-fopenmp-Wno-implicit-function-declaration\-Iinclude-Iinclude/core-Iinclude/common\-DCKKS_N2048-DCKKS_NPRIMES112-DBB32-DGG32\src/core/vllm_ntt.c src/core/vllm_ckks.c src/core/vllm_tp.c\tools/drivers/t23_m3p.c-o.tmp_tok/t23lay-lm# boot 驱动自举刷新2100 素数大栈必需gcc-O2-fopenmp-Wno-implicit-function-declaration-Wl,--stack,33554432\-Iinclude-Iinclude/core-Iinclude/common\-DCKKS_N2048-DCKKS_NPRIMES2100-DBB32-DGG32\src/core/vllm_ntt.c src/core/vllm_ckks.c src/core/vllm_tp.c\tools/drivers/t23_chain.c-o.tmp_tok/t23boot-lm# 独立验证器gcc-O2-fopenmp-Wno-implicit-function-declaration\-Iinclude-Iinclude/core-Iinclude/common\-DCKKS_N2048-DCKKS_NPRIMES112-DBB32-DGG32\src/core/vllm_ntt.c src/core/vllm_ckks.c src/core/vllm_tp.c\tools/relay/verify_layer.c-o.tmp_tok/verify_layer-lm三个编译期的坑-I必须同时给include、include/core、include/common线程池依赖common/vllm_platform.hPowerShell 下-Wl,--stack,33554432的逗号会被当参数分隔符→ 整段加引号boot用 2100 素数MinGW 默认栈会溢出0xC00000FD→ 必须加大栈五、一次完整的推理流程逐跳拆解 真实日志5.1 链式协议为什么是两跳一层layL : u{L-1}r112 ──► uL 层内前向注意链长在递减 bootL: uL ──► u{L}r112 自举刷新把链长拉回 112为什么必须成对lay会消耗模数链噪声涨、链变短链短到一定程度就算不动下一层了。所以每层算完必须boot把链拉回来。每一跳的产物8 个密文文件t0..3四个 token ×h0..1两个分量u{L-1}r112_0_0.ct u{L-1}r112_0_1.ct u{L-1}r112_1_0.ct u{L-1}r112_1_1.ct u{L-1}r112_2_0.ct u{L-1}r112_2_1.ct u{L-1}r112_3_0.ct u{L-1}r112_3_1.ct整条链 28 层 × 2 跳 56 跳每跳 8 个密文。5.2 跑一跳层内前向的真实日志下面这段是第 26 层末层之一的真实输出命令可原样复现T23_PHASEt26T23_NT4T23_E2EMODE1.tmp_tok/t23lay[A:xnorm t0] max|err|1.622e-02 PASS ← RMSNormA 段 [B:q0 t0] max|err|1.853e-02 PASS ← Q 投影B 段 [B:k t0] max|err|1.127e-02 PASS [B:v t0] max|err|1.425e-02 PASS [C:attn t0] max|err|2.892e-03 PASS ← 注意力打分C 段 [D:o t0] max|err|6.638e-03 PASS ← O 投影D 段 [rms E] in xa np61 xb np61 ← MLP 前的第二个 RMSNormE 段 [E t0] gate0 np40 up0 np40 xn2freed ← SiLU(gate)·up [U2 t0 h0] max|err|5.315e-03 PASS ← 本层最终输出 CHAIN: dumped u26_[t]_[h].ct (y/fold) ← 落盘 RESULTPASS (0)几个值得注意的点np是模数链剩余素数个数一路从112掉到40——这就是链在缩短的可视化A/B/C/D/E 是分段自检每段都有自己的明文参考来自 PyTorch 参考实现max|err|是解密后 vs 明文参考的最大绝对误差容差3e-25.3 跑一跳boot的真实日志T23_PHASEbootT23_BIu26T23_BOu26r .tmp_tok/t23boot[boot t0 h0] in np19 refresh... ← 输入只剩 19 个素数快用完了 [boot t0 h0] out np2083 ← 刷新后 2083 个满血 [boot t1 h0] in np19 refresh... [boot t1 h0] out np2083 ...共 8 次 [boot profile] modraise0.49s coeff_to_slot1737.20s rotate_k12.25s conj_extract8.16s sin_fold_re441.84s sin_fold_im445.70s restore_merge11.68s slot_to_coeff1736.19s total4393.50s (n8) BOOTPASS (0)in np19 → out np2083这一行就是自举的意义把快耗尽的密文救回来。5.4 全流程怎么串起来# 一层一层往前推每层 lay bootT23_PHASElayT23_LAY0...# u0 - u0T23_PHASEbootT23_BIu0T23_BOu0r...T23_PHASElayT23_LAY1...# u0r112 - u1T23_PHASEbootT23_BIu1T23_BOu1r...# ... 一直到第 27 层T23_PHASEt27...# 末层T23_PHASEbootT23_BIu27T23_BOu27r...T23_PHASEfin...# final RMSNorm lm_head - logits5.5 独立验收不看驱动的 PASS自己解密比对.tmp_tok/verify_layer.exe-L4-Ctu4r112-Dir.tmp_tok/chain-Tol3e-2[verify] layer 4 ctu4r112 modefull 8/8 [t0 h0] max_err1.3823e-02 PASS ... VERIFY_PASS (8/8)这条路径完全不经过驱动的 PASS 逻辑自己解密 → 自己解码 → 自己对明文参考算误差。六、实测数据6.1 单跳耗时本机 AMD 9800X3D8C/16T4 线程类型5 跳实测墙钟说明lay层内前向1942 / 1949 / 1962 / 1972 / 2008 s≈33 min / 跳boot自举刷新4339 / 4350 / 4377 / 4392 / 4891 s≈74 min / 跳10 跳合计32182 s ≈ 8.94 hlay 9833 s boot 22349 s所以一整层layboot≈ 1.8 小时28 层全链 ≈ 50 小时。6.2 精度项实测备注verify_layer -Mode lay5/5 ALL_LAYERS_VERIFY_PASS0–4 层verify_layer -Mode boot5/5 ALL_LAYERS_VERIFY_PASS0–4 层层 4 maxerr层 2 maxerr层 2 那条要特别注意误差是随深度累积的层 2 就已经吃掉 94.6% 的预算。6.3 尾部第 26/27 层 输出头的实测步墙钟结果t262079 sU28/8 PASS4.471e-03 ~ 2.299e-02boot264469 sBOOTPASS8×out np2083t272068 sU22/8 超阈3.822e-02 / 7.969e-02⚠️t27这个2/8 超阈但判据仍显示 PASS是本项目最值得讲的坑见 §7.3。七、6 个坑其中 3 个会让你得出错误结论坑 1线程数是结果变量不是性能参数 ⭐现象同一份输入改一下线程数输出密文位级不一致。原因vllm_ckks.c里的随机数生成器是_Thread_local且种子固定而 Galois key 生成ckks_gk_gen()在 parallel-for 里按 item 调用。于是output f(input, 线程数)后果用几个线程不是性能选择是口径选择。一旦定了整条链必须统一——否则接力交接的密文对不上。这也解释了一个常见困惑为什么加线程能不能提速的答案会分裂因为有两个口径都对口径结论层内[AB]段OpenMP 主导4 线程最快225 / 255 / 384 / 263 s整跳boot自研线程池驱动的域转换8 线程快约 20%4507 s → 3600 s混着一说就是错的。本归档统一固定T23_NT4。坑 2位级一致 ≠ 数值等价现象两份密文用 SHA256 一比——8/8 全不同。看着像算错了。真相用同一把密钥解密后逐项比对max|err|完全相同1.2706e-038/8 PASS下游消费这份密文同样RESULTPASS。结论位级不一致但数值等价。密文层面对不上不代表算错了——要看解密后的数值。这两件事必须分开验收。坑 3RESULTPASS (0)是弱判据⭐现象末层t27打印出完美的结果16:02:52 t27 PASS wall2068s RESULTPASS (0)但真实数字是这样的[U2 t1 h1] max|err|3.822e-02 FAIL [E2E:ref-deviate-ok] ← 超容差 1.27 倍 [U2 t3 h1] max|err|7.969e-02 FAIL [E2E:ref-deviate-ok] ← 超容差 2.66 倍8 个分量里有 2 个超阈判据照样报 PASS。原因开了T23_E2EMODE1后段级偏差被降级// E2E 模式段级偏差计入 rf不计入 failscheck_ct_scale(...,e2e?rf:fails,...);printf(RESULT%s (%d; ref-deviate %d),fails?FAILED:PASS,fails,rf);fails0只代表没有硬错误。教训拿到任何 PASS去 grep 判定它的那行代码看fails到底怎么累加的。坑 4跨代参考不可比差一天就白跑现象用独立验证器复核早期实验的末层密文结果很难看u26 VERIFY_FAIL (2/8) max_err6.30e-02 u27 VERIFY_FAIL (2/8) max_err1.92e-01看着像末层算错了。先别下结论去对时间线文件时间实验密文u26*/u27*2026-09-03 12:45 → 17:06参考l26_u2_ref.bin/l27_u2_ref.bin/logits.bin2026-09-04 08:05整批重生成密文比参考早一整天——那批参考是修完一个 bug 后重新生成的。拿新参考比旧密文得出的 FAIL 毫无意义。可复用的三连问① 两边是同一代产物吗② 容差和缩放口径一致吗③ 是算错还是不可比坑 5双重 BOM 让验收脚本假绿现象自动验收脚本看起来跑过了——日志文件里有内容。但内容其实是 PowerShell 的解析报错。原因脚本文件开头是EF BB BF EF BB BF——两个 BOM。PowerShell 5.1 只吃第一个第二个变成正文的 UFEFF把param(...)块解析带偏脚本连语法都没过。教训日志有内容 ≠ 脚本跑成功。含中文的.ps1必须存为 UTF-8 with BOM且只有一个 BOM纯 ASCII 脚本就别加 BOM。坑 6改对了参数但代码根本不看它空转实验现象做了一个换输入密文的等价性实验拿到结论数值等价。真相整个实验是空转的。读源码才发现第 0 层走的是明文种子加密分支根本不打开任何链上密文——注入的密文从头到尾没被读过。教训实验前先确认被注入的东西真的会被读到。加一行in np...之类的自证日志比事后推理可靠得多。八、诚实边界必读这些必须写清楚否则上面全是营销。28 层全链从未端到端跑完。完整验证过的只有0–4 层10 跳、8.94 h。5–25 层在当前链参数下尚未打通。末层的结论还没定。t27的最终输出 8 个分量里有2 个超容差最大 7.969e-02而fin输出 logits top1 验收在我写这篇时还在跑。本文只报告已确证的中间数据。精度余量很小且随深度恶化。层 2 距阈值只剩5.4%t27的 7.969e-02 就是这个趋势的延续。argmax必须解密后算。FHE 推理不是全流程黑箱——logits 要解出来在客户端比。别被全程密文的说法误导。性能数字是本机单点实测AMD 9800X3D8C/16T4 线程不可外推。换机器必须重测且性能分析必须基于同轮交错 A/B不能跨会话比绝对值。早期实验出过 top1 基线但不作数。那是 96 素数链 修复前参考系跑的项目自己的论文稿里就标着需真模型重测。九、核验入口内容位置引擎核心src/core/vllm_ntt.c、vllm_ckks.c、vllm_tp.c层链驱动tools/drivers/t23_m3p.clay/fin、t23_chain.cboot独立验证器tools/relay/verify_layer.c实测数据附录文章/同态加密推理接力讨论/03_实测数据附录.md归档产物含manifest.sha256文章/同态加密推理接力讨论/归档结果/L0-4/接力协调仓https://gitee.com/pei-xiaoguang/kestrel-fhe-relay仓库https://gitee.com/pei-xiaoguang/kestrel-llm如果你在自己的机器上跑出不一样的数据欢迎来 Issues 砸场子——我宁愿被纠正也不愿被误引。附一SEO 关键词同态加密 大模型推理 | FHE LLM inference | RNS-CKKS 实现 | 全同态加密 神经网络 密文推理 流程 | CKKS 自举 Bootstrapping | 模数链 rescale | NTT 多项式乘法 隐私计算 AI 推理 | 联邦学习 vs 同态加密 | 密态推理 精度 | max|err| 验收 纯 C 同态加密 | 零依赖 FHE | Galois 旋转 BSGS | 多项式拟合 非线性算子附二配图清单与提示词位置图意生成提示词文生图封面明文问题进、密文乱码、答案出来left a clean readable text bubble, middle a chaotic block of random hex digits, right a checkmark answer card, arrows between them, dark background, cinematic, 16:9§一FHE 流程四步图加密→密文计算→返回→解密minimal four-step flow diagram: encrypt, compute on ciphertext, return, decrypt, blue/orange palette, white background, 16:9§二 墙 2噪声随时间上涨、rescale 压回去的锯齿曲线sawtooth line chart: noise rising then reset, labeled rescale, minimal flat style, red/green, 16:9§二 墙 3自举分段占比饼图79.1% 域转换 / 20.2% sin_fold / 0.7% 其余建议直接绘制环形图两段高亮标注 79.1% / 20.2% / 0.7%§五 5.1lay→boot 两跳循环图链长 112→短→112circular diagram: chain of 112 segments shrinking across a layer, then a refresh restoring it, isometric, blue/green, 16:9§六单跳耗时对比lay 33min vs boot 74min建议脚本绘制两条横条 8.94h 合计标注终端输出截图[U2 t0 h0] max|err|...、[boot t0 h0] in np19 ... out np2083、[boot profile] coeff_to_slot...建议直接截屏——比任何生成的图都更有说服力而且这些日志本身就是本文最硬的证据。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。