【FHE 同态加密】我们如何实现同态加密推理(三):手写 negacyclic NTT——同态加密的多项式乘法,以及怎么证明自己写对了(CKKS 内核)

简介: 本项目在纯C11中手写负循环NTT(ψⁿ=−1),实现CKKS方案底层多项式乘法,追求位级可控与可验证性:所有结果严格对标教科书O(n²)朴素卷积,逐系数零误差。专注FHE密文大模型推理基础构建。(239字)

关键词:同态加密 | 全同态加密(FHE)| NTT | 数论变换 | negacyclic NTT | 多项式乘法 | CKKS | 大模型密文推理 | 纯 C11

导读:同态加密(FHE)最底层的原语是环上多项式乘法,它决定了整套同态加密推理引擎的速度与正确性。我们没用一个现成 FHE 库,而是在纯 C11 里手写了负循环 NTT(数论变换),理由是位级可控。本篇讲清它的数学(ψ^n = −1、双模乘路径),以及我们如何用"与教科书 O(n²) 朴素卷积逐系数位级对照"证明它写对了。

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

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

0. 一句话结论

全同态加密里最底层的原语是环上多项式乘法。我们没用一个现成 FHE 库,而是自己写了负循环 NTT(vllm_ntt.c),理由是位级可控。

而"自己写对了"这件事,在我们的语境里有可操作的定义:不是"数值上接近",而是与教科书 O(n²) 朴素卷积逐系数位级相同。这条判据比任何单元测试断言都硬。


1. 为什么是"负循环" NTT

我们工作的环不是 Z_q[x]/(x^n - 1),而是

R_q = Z_q[x] / (x^n + 1)

多出来的那个减号变成加号,就是"负循环"的由来。其数学后果是:普通的 n 次单位根不管用了,你需要一个 2n 次本原单位根 ψ,满足 ψ^n = −1。

于是:

  • ψ:2n 次本原单位根,ψ^n = −1
  • w = ψ²:n 次单位根(用于常规蝶形)

头注释把变换式子写得很明确(原文):

fwd:  A[i] = Σ_j a[j]·ψ^j·w^(ij)     (先乘 ψ^j,再 DIT radix-2 蝶形)
inv:  a[j] = n^{-1}·ψ^{-j}·Σ_i A[i]·w^(-ij)   (蝶形用 w^{-1},最后归一)

"先乘 ψ^j"这一步是负循环 NTT 的核心:它把 x^n+1 上的卷积"扭"成普通循环卷积,之后就能用标准 radix-2 蝶形。

1.1 前提条件:模数必须"配合"

ψ 存在是有条件的:模数 q 必须是素数,且 2n | q − 1。

这条约束会一路传导到上层的参数选择——本系列第 5 篇会看到,CKKS 那代的每个 60-bit 素数都必须满足 q_i ≡ 1 mod 2n,否则 2n 次本原单位根根本不存在,整条链无从谈起。


2. 两种模乘路径(一个必须踩的坑)

NTT 的每个蝶形都含模乘。模乘怎么写,取决于 q 有多大:

情形 路径 原因
q < 2^63(我们的 60-bit 素数) Montgomery 乘法(R = 2^64) q 足够小,Montgomery 归约安全且快
q = 2^64 − 2^32 + 1(Goldilocks,64-bit) __int128 直接取模 Montgomery 的中间量 t + m·q 会溢出 2^128,不能走

头注释把这条决策写在了结构体里(原文):

int use_mont;   /* q < 2^63 时启用 Montgomery(60-bit 素数安全;
                   64-bit Goldilocks 的 t+m·q 可溢 2^128,走 __int128) */

这是一个典型的"两代实现共用一层底层"的代价:BFV 那代用 Goldilocks(64-bit),CKKS 这代用 60-bit 素数——同一个 NTT 模块必须同时支持两种模乘路径。代价是多一个分支,收益是两代实现能共享同一份(且已被位级验证过的)卷积内核。


3. 预计算表:把能提前算的都提前算

ntt_ctx_t 里全是预计算表,这是 NTT 性能的关键:

字段 内容
q, n, psi, n_inv 模数、次数、2n 次本原根、n^{-1}
qinv, r2 Montgomery 常数:−q^{-1} mod 2^64、R² mod q
wlen_fwd / wlen_inv [log2(n)+1] 个每级的 w^(n/len) / w^(-n/len)
psi_pow / inv_psi_pow [n] 个 ψ^j / ψ^{-j}(负循环的"扭转"因子)
rev [n] 位反转表
wpow_fwd / wpow_inv [(lg+1)·n/2] 每级预计算的 w^(j)
bufA / bufB [n] 工作区(避免每次分配)

注意所有表都是"Montgomery 域内"的。这意味着变换内的模乘全部是同域运算,不需要反复进出 Montgomery 域——少一层转换,少一次出错的机会。


4. 怎么证明它是对的:与教科书版位级对照

这是本文最想讲的部分。

浮点世界里,"写对了"通常意味着"误差在容差内"。但在我们这里不行——整数模运算没有"接近"这回事:a·b mod q 要么完全正确,要么完全错误,没有中间状态。

所以自测 ntt_self_test()(代号 T13)做三件事:

  1. 往返位级:iNTT(NTT(x)) == x,逐系数比较;
  2. 与教科书 O(n²) 朴素 negacyclic 卷积逐系数对照:ntt_negacyclic_mul() 的结果必须与"两层循环硬算"的结果完全一致;
  3. 性能计时:确认加速确实存在。

第 2 条是关键。教科书实现(O(n²))慢但显然正确,NTT(O(n log n))快但不显然正确。用前者锚定后者,就得到了一条不依赖任何外部参考实现的可信链。

这套思路在本项目里是通用的:用慢而显然正确的东西,去锚定快而不显然正确的东西。 后面你会看到同一招出现在别的层——verify_layer 用"解密后对明文参考"去锚定整条密文链,本质上是一回事。


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

本文所述参数为机制验证级(n=2048、112 素数内层链、2100 素数自举链),
远低于 HE 参数标准的 128-bit 水平,不得用于保护真实数据。
本文主张的是:实现选择与验证方法。
本文不主张:安全强度、性能优越性(本文不含任何跨实现性能对比)。

另外说明:n 仅支持 2 的幂(本项目用 1024 与 2048)。这是 radix-2 蝶形的直接结果,不是疏漏。


6. 这一篇的未解问题

  1. radix-2 而已,没有做 radix-4 或 SIMD 化。头注释里"教科书 O(n²) 优先保证正确性"的阶段已经过去,但 NTT 本身仍有明显的常数级优化空间——这是本项目里少数几个"题做完了但没做完"的地方。
  2. use_mont 这条双路径缺一个交叉验证:我们验证了"两条路径各自与教科书一致",但没有验证"同一个多项式在两条路径下结果一致"(理论上不需要,因为模数不同;但作为实现健壮性检查,它有价值)。
  3. 没有形式化验证。目前的可信性来自"逐系数位级对照",不是数学证明。对于原型目标是够的,但对一个要被第三方长期依赖的内核,这不够。

下一篇我们往上一层:RNS-CKKS 的模数链管理——rescale、modswitch、modraise 这三个最容易混淆的操作到底各干了什么。

相关文章
|
并行计算 编译器 调度
0.8MB 跑通 Qwen|第 1-2 篇:推理引擎的 ARM 工具链第一课——原生编译 vs 交叉编译,以及一个真实的 -march 翻车现场
本文深入解析ARM交叉开发三大核心:原生vs交叉编译的本质区别、`-march`/`-mcpu`的语义差异,以及`dotprod`扩展为何不可或缺;并通过真实编译报错复现,揭示编译器如何在编译期主动拦截潜在运行时崩溃——不是刁难,而是保护。
 0.8MB 跑通 Qwen|第 1-2 篇:推理引擎的 ARM 工具链第一课——原生编译 vs 交叉编译,以及一个真实的 -march 翻车现场
|
3天前
|
编译器 C语言
0.8MB 跑通 Qwen|第 2-1 篇:推理引擎的 C11 `_Static_assert`——让编译器守卫你的内存布局
本文详解C11 `_Static_assert` 在内存布局守卫中的关键作用:针对mmap直挂场景,通过编译期断言钉死VQF格式三结构体(200/432/64字节),杜绝因对齐差异导致的静默错位。真机RK3588实测验证,实现错误前移。
0.8MB 跑通 Qwen|第 2-1 篇:推理引擎的 C11 `_Static_assert`——让编译器守卫你的内存布局
|
3天前
|
芯片 AI芯片
0.8MB 跑通 Qwen|第 2-3 篇:推理引擎的设备画像——识别 RK3588,而不是"猜"(vllm_device.c)
本文实测RK3588设备画像机制:通过启发式匹配`/proc/device-tree/model`识别板型,自动生成含NPU支持、量化推荐等配置的`VDevProfile`;支持`--device`人工覆盖,验证画像如何精准驱动引擎决策。(239字)
 0.8MB 跑通 Qwen|第 2-3 篇:推理引擎的设备画像——识别 RK3588,而不是"猜"(vllm_device.c)
|
1天前
|
编译器 C语言
0.8MB 跑通 Qwen|第 6-1 篇:推理引擎的 ARMv8.2 dotprod——`vdotq_s32` 一条指令做 4 个点积
本系列《0.8MB跑通Qwen》聚焦ARM零依赖纯C推理引擎,实测RK3588平台,适配Qwen3-VL多模型。本文详解ARMv8.2 dotprod指令(vdotq_s32)如何在真实大内核中释放性能,破除微基准误导,揭示寄存器压力下加速本质。(239字)
|
3天前
|
缓存 测试技术 C语言
0.8MB 跑通 Qwen|第 1-3 篇:推理引擎的自检门——没有模型的确定性测试,第一次读懂 PASS/FAIL
本文介绍RK3588平台LLM推理引擎的“自检门”机制:无需模型文件,通过标量参考对拍、固定种子随机与确定性PASS/FAIL测试,快速验证编译后代码正确性;实测覆盖L3缓存、量化点积等核心逻辑,体现工业级可信验证思想。(239字)
 0.8MB 跑通 Qwen|第 1-3 篇:推理引擎的自检门——没有模型的确定性测试,第一次读懂 PASS/FAIL
|
10月前
|
监控 安全 Unix
iOS 崩溃排查不再靠猜!这份分层捕获指南请收好
从 Mach 内核异常到 NSException,从堆栈遍历到僵尸对象检测,阿里云 RUM iOS SDK 基于 KSCrash 构建了一套完整、异步安全、生产可用的崩溃捕获体系,让每一个线上崩溃都能被精准定位。
3088 167
|
23小时前
|
安全 调度 数据安全/隐私保护
【FHE 同态加密】我们如何实现同态加密推理(一):把 2B 大模型拆成 28 段密文链条来算(纯 C11 · 零依赖)
本系列记录纯C11、零依赖、CPU-only实现2B大模型(Qwen3-VL-2B)全密文推理的全过程:将模型拆为28段可验证密文链,每跳约1.8小时,聚焦可复现性与机制验证,非安全级部署。(239字)
|
1天前
|
编解码 应用服务中间件 API
# 0.8MB 跑通 Qwen|第 19-3 篇:视频帧与媒体模块——推理引擎默认不启用的 H.264 独立模块
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,适配Qwen3-VL多模态模型(2B/8B/30B),在RK3588上实测。视频支持分两层:帧序列路径已落地;MP4/H.264解码为独立未完成模块,API清晰、默认不构建、不演示未验证功能,体现严谨工程边界。(239字)
|
1天前
|
JSON 网络协议 前端开发
0.8MB 跑通 Qwen|第 20-3 篇:SSE 流式——推理引擎响应怎么写一半就发给客户端
本系列《0.8MB跑通Qwen》用纯C手搓零依赖推理引擎,适配Qwen3-VL多尺寸模型,在RK3588(aarch64)实测SSE流式响应:首token延迟130ms,后续约50ms/token,支持标准+自定义事件,真正实现端侧“打字机”体验。(239字)
|
1天前
|
JSON 数据格式 Python
0.8MB 跑通 Qwen|第 18-2 篇:tokenizer.json → vocab.bin——推理引擎的 build_vocab_bin.py 在干嘛
本系列《0.8MB跑通Qwen》聚焦ARM端零依赖纯C推理引擎,适配Qwen3-VL多模态模型(2B/8B/30B),在RK3588平台实测通过。本文详解词表编译:将7MB tokenizer.json编译为1.95MB可审计vocab.bin,精准处理byte-level解码(如0xAD/0x7F等历史坑),实现板端逐位一致、冷启动仅约2秒。(239字)