【FHE】 我们如何实现同态加密推理(二):引擎全景——一次密态推理的完整数据流

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

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

0. 一句话结论

整套系统被一条线劈成两个世界

  • 明文侧(Python):把模型权重、非线性函数的逼近系数、以及"正确答案"(明文参考)算出来,落成 .bin
  • 密文侧(纯 C):只读这些 .bin,在密文上把前向跑完,产出密文。

分界线是两样东西:拟合系数与明文参考。 密文侧从不重新拟合任何东西,明文侧从不碰密文——这个切法让两侧可以独立验证。


1. 两个世界的产物对照

明文侧(tools/preproc/,Python) 密文侧(src/core/ + 驱动,C)
输入 model.safetensors(约 4.26 GB) 明文侧落下的 .bin + 密文
产出 权重、拟合系数、明文参考值、尺度表 密文.ct)+ 日志
依赖 numpysafetensors libc + libm
是否可重跑 可,且产出逐字节可复现 可,且产出位级可复现(需固定线程数)
怎么验 与分发物比对哈希 verify_layer 独立解密复核

2. 明文侧:四个阶段,24 个脚本

tools/preproc/ 下是 24 个 Python 脚本,按职责分四组(完整执行顺序以 tools/preproc/README.md 的「执行顺序」一节为准):

① 导出

  • _export_weights.py:权重导出(tail/w0..w27/,每层 9 个文件)
  • _embed4.pylay0 的明文输入——按 token id 从 embedding 表取 4 行(l0/embed4.bin,4×2048 float32,32768 B)

② 定路径

  • _silu_mode.py:逐层写出 8 字节的 tail/silu{L}.bin,决定该层的 SiLU 走"直接拟合"还是"÷21 折叠"(本系列第 8 篇专门讲这个开关,它也是最危险的一个文件)

③ 生成参考值("正确答案")

  • _full_layers.py:L0..L25 的明文因果前向参考
  • _tail_ref.py:链尾(L26..L27)参考
  • 以及 _ln_fit*.py / _m2c* / _refit_lnq.py 等:LayerNorm 与倒数之类的分支参考

④ 拟合与定标

  • _sin_fit*.py / _cos_fit.py / _recip_fit.py / _attn_fit.py:把非线性函数(sin、cos、倒数、exp)拟合成多项式,直接生成 C 数组(例如 EXP_Q[7]RECIP_Q[9]
  • _fold_fit.py / _fold_sim.py / _cfold_plan.py:折叠方案的设计与仿真
  • _adap_scale.py逐层尺度自适应(本系列第 9 篇)
  • _silu_err.py / _logits_tol.py:误差与容差评估工具

一个反直觉的事实:这些脚本产出的数据包约 7 GB,但它不随仓库分发。任何人都能用同一个模型把整包重新生成出来——我们核对过生成物与分发物逐字节相同


3. 密文侧:四层职责

┌──────────────────────────────────────────────┐
│ 驱动 t23_m3p.c (lay) / t23_chain.c (boot)     │  ← 一个"层"的业务逻辑
├──────────────────────────────────────────────┤
│ vllm_ckks.c    RNS-CKKS:编码/加密/加乘/       │  ← 密文语义
│                rescale/relin/rotate/modraise  │
├──────────────────────────────────────────────┤
│ vllm_ntt.c     负循环 NTT(多项式乘法)        │  ← 数学原语
│ vllm_tp.c      自研线程池(替换 OpenMP)       │  ← 并行原语
└──────────────────────────────────────────────┘

关键点:下层不知道上层在算什么。NTT 不知道自己在做 attention,CKKS 不知道自己在算 Transformer。这条分层让"位级可验证"成为可能——你可以单独证明 NTT 是对的(与教科书 O(n²) 位级对照),而不用先信任整个模型。


4. 一跳内部发生了什么

4.1 lay(层内前向)

layL: u(L-1)r112 → uL,一串同态算子的组合:QKV 投影、attention、FFN、以及需要密文化的非线性(SiLU、倒数、exp 等,全部用预先拟合好的多项式代替)。

4.2 boot(自举刷新)

bootL: uL → u(L)r112。驱动 t23_chain.c[boot profile] 打印把整条流水线摊开了,实测的七段是

modraise → coeff_to_slot → rotate_k → conj_extract
        → sin_fold_re / sin_fold_im → restore_merge → slot_to_coeff
作用
modraise 用 Garner 扩展把模数链抬回满链(自举的起点)
coeff_to_slot 系数域 → 槽位域(Galois 旋转 + key-switch)
rotate_k 旋转
conj_extract 共轭提取(分出实部/虚部)
sin_fold_re/im 对实部、虚部分别做 sin 折叠(EvalMod 核心)
restore_merge 还原并合并
slot_to_coeff 槽位域 → 系数域

这七段的时间分布极不均匀——coeff_to_slotslot_to_coeff 两段就占了约 80%。本系列第 15 篇给完整账本。


5. 交接:一跳的产物

一跳结束,落盘的东西很少:

产物 数量 / 规格
密文 8 枚(4 个 token 位置 × 2 个分量),u{L}r112_*.ct,单文件 ≈3.5 MB
日志 <stage>.out / <stage>.err
元数据 + 哈希 manifest.sha256(逐文件 SHA256,可自校验)

这个包就是"接力棒"。 下一棒只需要三样:这个包、sk.bin(同一把私钥)、以及编译好的驱动。


6. 一张总图

![图 2-1 一次密态推理的完整数据流:两个世界]
fig02_dataflow

图 2-1 怎么读:左边是明文侧(Python),只产出拟合系数明文参考;右边是密文侧(纯 C),只读这两样东西,从不重新拟合任何函数。中间那条红色虚线就是分界线——它同时是两边的接口,也是两边可以各自独立验证的原因。

矢量版 figs/fig02_dataflow.svg|Graphviz 源码 figs/fig02_dataflow.dot(本机未装 Graphviz 时,图片由 figs/make_figs.py 生成)

下面是同一张图的纯文本版,便于在终端或纯文本环境里阅读:

model.safetensors ──┐
                    ├─[preproc]─→ .tmp_tok/tail/{w*,l*_ref,silu*.bin,scale*.bin}
                    │                     │
                    │                     ▼
                    │            [t23lay (np=112)]
   明文输入 embed4 ──→ 密文 u0 ──────────┘
                              │
                              ▼
                       [t23boot (np=2100)]  → u0r112
                              │
                              ▼
                  8 × u0r112_*.ct + manifest.sha256
                              │
                              ├─→ 交给下一棒(lay1)
                              └─→ [verify_layer] 独立复核 max|err|

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

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

8. 这一篇的未解问题

  1. 明文侧与密文侧的耦合点还是太多。拟合系数(如 EXP_Q)是"密文侧的常量",但它的取值范围来自明文侧的统计——一旦换模型,这些常量全部要重算。我们希望把这条依赖显式化成一份"契约文件",目前还散在脚本里。
  2. _silu_mode.py 的逐层选择是固化调参结果,不是从模型推导出来的规则。它目前是一个硬编码的层号集合,这一点让"换模型自动适配"做不到。
  3. 数据流图还缺一张真正的位级对照实验:同一条链在 x86-64 与 aarch64 上逐系数一致(我们已在层 0–4 验证),但"哪些步骤位级一致、哪些只是数值等价",我们只做了整链对照,没有分段归因。

下一篇我们下到最底层:手写 negacyclic NTT——为什么不用现成库,以及"怎么证明自己写对了"在密文计算里意味着什么。

posted @ 2026-09-18 16:34  haliu  阅读(3)  评论(0)    收藏  举报