【FHE 同态加密】我们如何实现同态加密推理(五):RNS-CKKS 的模数链管理——rescale、modswitch、modraise 到底谁是谁

简介: 本文详解RNS-CKKS模数链三大核心操作:`rescale`(切素数+归一缩放)、`modswitch`(仅降模)、`modraise`(Garner上提),厘清易混语义;阐明60-bit素数选择及`q_i ≡ 1 (mod 2n)`的数学硬约束,直指大模型密文推理的精度与可行性边界。(239字)

关键词:同态加密 | FHE | CKKS | RNS | 模数链 | rescale | modswitch | modraise | 60-bit 素数 | 大模型密文推理

导读:RNS-CKKS 是同态加密推理引擎的主干。它的模数链上有三个名字很像、作用完全不同的操作——rescale、modswitch、modraise,混起来会直接算错。本篇讲清三者的差别,以及两条硬约束:为什么素数选 60-bit,为什么每个素数必须满足 q_i ≡ 1 (mod 2n)。

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

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

0. 一句话结论

CKKS 的模数链上有三个操作,名字很像、作用完全不同,混起来会直接算错:

操作 一句话 什么时候用
rescale 切掉一个素数,并把 scale 归一 每次乘法之后(scale 从 2^60 翻到 2^120,必须拉回来)
modswitch 切掉一个素数,不归一 只想降级、不想改 scale 时
modraise 用 Garner 扩展提升回满链 自举的起点(本系列第 7 篇)

rescale = modswitch + 缩放归一。这个差别就是全部。


1. 为什么是 60-bit 素数,且必须 q_i ≡ 1 (mod 2n)

CKKS 那代用的是 RNS 表示:不把模数 Q 当成一个大整数,而是拆成一串小素数 q_0 … q_{k-1},所有运算在每个 q_i 上独立进行(RNS = 剩余类系统)。

两条硬约束:

① 为什么选 60-bit

不是 64-bit,也不是 32-bit。60-bit 是我们这个实现里"乘法安全"的上界——q_i 之间做乘法时,中间量必须能落进 64-bit 字里且留出余量。这也是为什么底层 NTT 对 60-bit 素数走 Montgomery、对 64-bit Goldilocks 只能走 __int128(见本系列第 3 篇)。

② 为什么必须 q_i ≡ 1 (mod 2n)

因为槽位打包需要 2n 次本原单位根(第 3 篇的那条约束一路传导到这里)。素数不满足这个同余式,那一个"槽"就根本不存在。

头文件里对 q 数组的注释就一句:模数链(递减)。注意是递减——链是按顺序消费的,rescale 每次砍掉最上面那个。


2. RNS 密文的内存布局

typedef struct {
   
    ckks_ctx_t ctx;      /* 密文自身模数链状态 */
    int comps;           /* 2 或 3 */
    uint64_t *c;         /* [comps*nprimes*n] 排列:c[(comp*nprimes+p)*n + i] */
} ckks_ct_t;

c 是一个平坦数组,索引公式是 (comp × nprimes + p) × n + i:分量 → 素数 → 系数。

这个布局选择有实际后果:同一分量的同一素数上的所有系数是连续的,所以一次 NTT 可以在这段内存上原地跑完,不用跳着访问。在所有可能的排列里,这是对缓存最友好的那一种。

还有一个细节值得注意(原文):

明文槽位 |z| < 0.5(2^60·z < 2^59 < 任一 q_i,保证中间层解密无需 CRT)

也就是说 scale 的取值与槽位取值范围是配套设计的:2^60 × 0.5 = 2^59,小于任何一个 60-bit 素数,于是中间层想解密看看时,不需要先做 CRT 重组。


3. scale 的收支:一进一出

CKKS 的实数编码是 X = round(x · scale),本项目 scale = 2^60(CKKS_SCALE_BITS 60)。

问题出在乘法上:

乘法前:  X_a = round(a · S)、X_b = round(b · S)
相乘后:  X_a · X_b ≈ a·b · S²      ← scale 变成了 S²

于是每次乘法都要做一次收支平衡:

乘法 → scale = 2^120 → rescale(切一个素数、除以该素数)→ scale 回到 2^60

代价是链长少 1。这就是"精度不是免费的"的具体样子:每一次乘法都在烧链长。

加密/解密这一进一出,也定义了两个边界操作的语义:

int ckks_encode(uint64_t *poly, const double *z, const ckks_ctx_t *ctx);
int ckks_decode(double *z, const uint64_t *poly, const ckks_ctx_t *ctx);
int ckks_encrypt(ckks_ct_t *ct, const ckks_sk_t *sk, const uint64_t *poly, const ckks_ctx_t *ctx);
int ckks_decrypt(uint64_t *poly, const ckks_sk_t *sk, const ckks_ct_t *ct);

4. 三个操作的辨析(本文核心)

int ckks_rescale(ckks_ct_t *ct, const ckks_ct_t *in);              /* 模数切换(切一个素数,scale 归一) */
int ckks_modswitch(ckks_ct_t *ct, const ckks_ct_t *in);            /* 降级:切素数不 rescale */
int ckks_modraise(ckks_ct_t *ct, const ckks_ct_t *in, int target); /* ModRaise:Garner 扩展提升回满链 */
rescale modswitch modraise
链长变化 −1 −1 提升到 target
scale 变化 除以被切掉的素数 不变 不变
加密语义 保持(近似误差重新定标) 引入额外近似误差 保持
方向 往下 往下 往上
典型用途 乘法之后 对齐两条链长不同的密文 自举第一步

最容易搞混的是 rescale 与 modswitch。 记法:rescale 里有 "scale",所以它动 scale;modswitch 只动 modulus。

modraise 是唯一一个"往上"的操作,也是自举能不能成立的关键:它把已经被消耗到很短的链,用 Garner 扩展重新抬回满链。注意这不是"免费恢复精度"——它不恢复已经损失的精度,只是把模数空间还给你,让你还有地方继续算。
这一点在本系列第 7 篇有一个反直觉的实测证据。


5. 链长就是油箱

把链长理解成"油箱表",整条流水线就清楚了:

阶段 链长(素数个数) 编译期链长
输入(上一跳产出) 112 —
lay 消费后 16 112(t23lay)
boot 刷新后 112 2100(t23boot)

lay 一路做乘法,把 112 个素数吃到只剩 16;boot 把油箱加回 112。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-1 怎么读:上半是链长收支,三根柱子的高度按素数个数成比例(所以中间那根只有 14% 高)——lay 一路乘法每次 rescale 切掉一个素数,一层吃掉 96 个;boot 的 modraise 用 Garner 扩展把模数空间还回来。下半是三个操作的辨析:rescale 与 modswitch 都切一个素数(顶端那块被划掉),差别只在动没动 scale;只有 modraise 是往上抬的。

矢量版 figs/fig05_modulus_chain.svg|Graphviz 源码 figs/fig05_modulus_chain.dot

而 CKKS_NPRIMES 是编译期量(该程序支持的最大链长),可以覆盖:

-DCKKS_NPRIMES=112     # lay
-DCKKS_NPRIMES=2100    # boot(自举内部折叠需要极深的链)

头注释里给了标定参考:32 个素数约合常规深度 10;而 sin 折叠函数约 31 深,所以折叠测试与全链自举要用 40 ~ 128 覆盖。

这就是"编译期链长"与"实际链长"的区别:t23boot 编到 2100,不代表跑到一半时链上有 2100 个素数——那只是它支持的上限,也是它需要的内存上界(所以 boot 比 lay 吃内存得多,也慢得多)。


6. 一处照实写的文档不一致

vllm_ckks.h 顶部「架构」段写的是:

模数链:4 个 60-bit 素数 q0..q3,总 240-bit,支持深 3。

但同一份文件里 CKKS_NPRIMES 的默认值已经是 32,注释也写明"60-bit×32=1920-bit,常规深 10"。

架构描述停留在最初 4 素数的原型阶段,没有跟着默认值更新。 这不是 bug(代码走的是 CKKS_NPRIMES),但它会误导刚读代码的人——包括我们自己。按本项目"不漂亮的结论照实写"的纪律,记在这里。


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

本文所述参数为机制验证级,远低于 HE 参数标准的 128-bit 水平,不得用于保护真实数据。
本文主张的是:模数链管理的实现事实。
本文不主张:安全强度、性能优越性。

8. 这一篇的未解问题

  1. 没有自动的链长规划器。哪些操作消耗几个素数、整层总共消耗多少,目前靠"编译时给足 + 跑挂了下调"来定。112 与 16 这两个数字更像是调出来的,不是算出来的。
  2. modswitch 在我们这条链上几乎没用。它目前的实际价值是"对齐两条链长不同的密文",而这个场景在我们的流水线里很少出现。它更像是一个为了语义完整而留的接口。
  3. 素数链的选取没有做数值优化。q_i 的具体数值对噪声增长有影响,我们目前是按约束(素数、60-bit、≡1 mod 2n、递减)生成的,没有做"选哪一组更好"的搜索。

下一篇讲一个把"数量级"讲清楚的例子:Key-Switch——为什么不做数字分解,噪声会到 2^73,而 2^73 会直接爆掉 60-bit 素数。

相关文章
|
15天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
8155 15
|
14天前
|
人工智能 并行计算 PyTorch
秋叶 ComfyUI 2026 整合包 v3.2 完整部署教程:Python 3.13 + Torch 2.13 全栈升级
秋叶aaaki ComfyUI 2026年8月整合包v3.2正式发布!全面升级Python 3.13.11、PyTorch 2.13.0+cu130及ComfyUI v0.30.2,原生支持MiniMax H3、Wan 2.2、Qwen-Image-2.1等2026主流音视频/图像模型,解压即用,无需环境配置。
2331 13
|
13天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
1843 4
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
12天前
|
人工智能 编解码 并行计算
MiniMax-H3 一键整合包技术文档:8G 显存运行 AI 漫剧制作 —— 角色替换 / 动作迁移 / 文图生视频部署与调参指南
MiniMax H3 是 MiniMax 开源的全模态视频生成模型,支持文/图/音/视多条件输入,输出最高2K、15秒带双声道音频视频。本文档详述其Int8量化版在8GB显存下的本地一键部署、三段式工作流(EDIT/REPLACE/CONTINUE)、参数调优及常见问题排查。(239字)
|
8天前
|
人工智能 Linux 开发者
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
Codex是OpenAI推出的AI编程智能体,可读取本地项目、理解需求并自动修改代码。支持桌面GUI、命令行(CLI)及VS Code/Cursor插件三种形态,覆盖可视化操作、终端高效开发与编辑器无缝集成场景,助开发者用自然语言驱动编码全流程。(239字)
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
|
8天前
|
人工智能 JSON 编解码
【2026最新版】ComfyUI本地部署教程,新手也能看懂!
ComfyUI是本地运行的AI绘画工具,采用节点式工作流设计:通过拖拽连接“加载模型”“提示词编码”“采样”“解码”等模块,实现高度可控的文生图。新手推荐使用秋叶整合包,一键启动、内置模型管理与插件安装器,轻松上手。(239字)
|
22天前
|
缓存 IDE Java
【保姆级】Android Studio下载、安装和汉化教程(2026最新)
Android Studio 是 Google 官方推出的免费 Android 应用开发集成环境,基于 IntelliJ IDEA,内置模拟器、调试器、性能分析及 Compose 界面工具,功能全面,文档丰富,是安卓开发首选工具。(239字)
2329 1

热门文章

最新文章