【FHE】(十三):位级可复现性——4 线程与 8 线程为什么算出不同的结果

项目仓库

Gitee 主仓:https://gitee.com/pei-xiaoguang/kestrel-llm

GitHub 镜像:https://github.com/m13253246268-ship-it/kestrel-llm

相关文档

术语与数据口径 |

性能与基准 |

构建与复现 |

快速上手 |

架构总览

0. 一句话结论

同一份源码、同一份输入,T23_NT=4 与 T23_NT=8 跑出来的密文在位级上不同。

根因不是浮点误差,而是随机性的来源不对 :随机数状态是线程局部的、且用固定常量播种------于是"哪个线程抽到哪几个随机数"取决于调度器,而不是取决于数学。

修复方案早就定了(按自同构指数 k 播种)。但按本项目"不漂亮的结论照实写"的纪律,必须说清楚:

截至本文写作,这个修复还没有落地。

源码里 ckks_rng_state 仍然由固定常量初始化。因此"不同线程数结果位级不同"这条限制当前仍然成立。


1. 现象:它是被"比字节"发现的,不是被"看误差"发现的

这个问题的发现方式本身就是重点。

如果我们只看 max|err|,一切正常------4 线程和 8 线程的结果都在容差内 ,verify_layer 都会打 PASS。它们只是"不完全相同",而 max|err| 从来不是用来衡量"相同"的。

发现它靠的是逐字节比对:

复制代码
u0_{t}_{h}.ct  × 8   →  BIT_IDENTICAL: 8 / 8    ← 同一线程数、同一输入
                       换成另一个线程数 → 逐字节不同

(顺带说明:我们的工具链里本来就有这个能力------verify_layer -BitA u5 -BitB u5b -L 5 就是专门做位级比对的。)

教训:如果你从不比字节,你就永远不会知道自己的"可复现"只到"数值接近"这一层。


2. 根因:三行代码

全部问题就在这几行(vllm_ckks.c):

c 复制代码
/* RNG(xorshift64*,原型级) */
static _Thread_local uint64_t ckks_rng_state = 0x243F6A8885A308D3ull;   /* ← 固定常量 */

static uint64_t ckks_rng_u64(void) {
    ...
    uint64_t x = ckks_rng_state;
    x ^= x >> 12; x ^= x << 25; x ^= x >> 27;
    ckks_rng_state = x;                       /* ← 只有线程自己推进 */
    return x * 0x2545F4914F6CDD1Dull;
}

两个要素叠加成灾:

要素 后果
static _Thread_local 每个线程有自己独立的随机数序列,互不干扰
初值是固定常量 所有线程从同一个状态起步

于是:每个线程都会从同一个种子开始,生成"同样的"随机数流;但谁生成了几个、用在哪个对象上,完全由调度决定。

举个具体样子:假设某次加密需要 100 个随机系数,4 个线程平分------线程 0 拿前 25 个、线程 1 拿 26--50 个......8 个线程平分则变成每线程 12~13 个。同一个逻辑对象拿到的随机数,在两种线程数下是不同的。 噪声因此不同,密文自然不同。

而"噪声不同"在同态加密里是完全合法 的------噪声本来就是随机的,只要在界内。这就是为什么没有报错。


3. 为什么 max|err| 永远看不见它

max|err| 衡量的是"解密后与明文参考差多少"。两种线程数的结果都 是合法的密文,都 在噪声界内,所以误差都在容差里。

这引出一个更普遍的判断:

"数值接近"与"结果相同"是两个不同的性质,而前者无法推出后者。

在浮点计算里我们习惯了"误差是连续的";但在整数模运算 + 噪声的世界里,差异可以是非连续的------两个结果可能一模一样,也可能差得毫无规律,而它们的"误差"看起来都在正常范围。


4. 为什么修复方案是"按 k 播种"

修复思路很直接:既然随机性不可消除,那就让它变成输入的函数。

具体做法是按自同构指数 k 来播种 ------而这条路之所以顺,是因为代码里本来就有按 k 组织的入口(本系列第 6 篇):

c 复制代码
int ckks_gk_gen_k(ckks_gk_t *gk, const ckks_sk_t *sk, const ckks_ctx_t *ctx, uint64_t k);
int ckks_rotate_k(ckks_ct_t *ct, ..., uint64_t k);   /* 一般 Galois σ_k(k 任意奇数) */

也就是说:结构上早就是"按 k 索引"的,只是随机数播种没跟上。 修复的难度不在改架构,而在改播种点------并且要确保每一个使用随机性的地方都覆盖到(密钥生成、加密、以及一切需要采噪声的操作)。

为什么这样能修好 :一旦播种只依赖 k(以及其他确定性输入),那么"线程 5 抽到哪几个随机数"就不再由调度决定------每个逻辑对象自己有一个确定的随机数流,与谁在执行、有几个执行者无关。


5. 当前状态与我们的诚实口径

源码现状(本文写作时):

c 复制代码
static _Thread_local uint64_t ckks_rng_state = 0x243F6A8885A308D3ull;   /* 固定常量,推理路径 */

文件里唯一一处"时间相关"的播种在自测函数里:

c 复制代码
int ckks_self_test(void) {
    ...
    ckks_rng_state = (uint64_t)time(NULL) ^ 0xDEADBEEFCAFEBABEull;

------它不在推理路径上。 所以对 lay / boot 的实际计算没有任何影响(这一点我们核对过,避免把"自测里的随机"误当成"推理里的随机")。

因此我们对外一律这么讲:

说法 是否成立
"同一源码、同一线程数,在 x86-64 与 aarch64 上逐系数位级一致" ✅ 成立(层 0--4 已验证)
"同一源码,不同线程数,结果位级一致" ❌ 不成立
"结果在容差内可复现" ✅ 成立(与线程数无关)

所有可复现性声明都必须绑定 T23_NT。 这句话我们写进了交付文档,也写在了本系列第 1 篇与第 14 篇里。


6. 同一类的第二个案例:懒初始化的并发首次触发

在自举驱动里有一条修复记录:

c 复制代码
tab_prepare(n);   /* M1c 修复:coeff_to_slot/slot_to_coeff 的 omp 区并发首次触发 ... */

原来某些预计算表是在并行区里首次访问时懒初始化 的。多个线程同时"第一次"进入 → 并发触发初始化 → 结果取决于哪个线程先到。

这和 RNG 是同一类问题:

RNG 播种 懒初始化
不确定性来源 线程局部状态 + 固定初值 初始化时机与线程到达顺序
表现 位级不同 数据竞争,偶发错误
会不会报错 ❌ 不会 ❌ 通常不会
修法 让随机性成为输入的函数 在并行区之前完成初始化

共同点:都是"执行顺序被当成了隐式输入"。


7. 一份可复用的排查清单

如果你在做任何"并行 + 随机化 + 结果比对"的流水线,下面这些模式值得逐一排查:

  1. 线程局部随机状态 (_Thread_local / thread_local / TLS RNG),尤其是初值为常量的;
  2. 懒初始化:在并行区内首次访问触发的初始化;
  3. 浮点归约顺序:并行求和的分组方式不同 → 结果位级不同(对我们是整数模运算,但同样的陷阱在别处存在);
  4. 非确定的分块策略 :动态调度(schedule(dynamic))、工作窃取;
  5. 哈希/字典遍历顺序:某些语言的字典迭代顺序与插入/内存布局相关;
  6. 时间或地址参与运算 :把 time()、指针值当作种子或输入。

判断标准很简单:问一句"这个结果只依赖输入吗?" 如果答案里出现了"取决于调度/时序/内存布局",那它就不是确定的。


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

复制代码
本文所述参数为机制验证级,远低于 HE 参数标准的 128-bit 水平,不得用于保护真实数据。
本文主张的是:一个真实的可复现性缺陷的定位与结论。
本文不主张:安全强度、性能优越性(4 线程与 8 线程的性能差异不在本文讨论范围)。

另一处必须说明的 :本文引用的 RNG 是原型级的 xorshift (源码注释即写"原型级")。它不是为了密码学安全设计的,不应被当作安全随机源使用。这与"本项目不主张安全强度"的边界一致。


9. 这一篇的未解问题

  1. 补丁还没落地 。方案清楚、入口现成,但做 和说是两件事。在它落地之前,我们的任何"位级复现"结论都带着一个星号。
  2. 修复的覆盖面需要审计 。不只是"播种点",还要确认所有消耗随机性的路径都被覆盖------漏掉一处,问题就以更低概率复现(更难查)。
  3. 没有一条自动检查来防回归。理想情况下,CI 里应该有一个"同输入、不同线程数,比对字节"的测试,一旦不一致就报警。我们目前靠人工比对。
  4. T23_NT 之外还有哪些隐式输入,没有系统梳理 。比如 g_fold / g_chk 这些阶段参数、预计算表的构建顺序,都值得过一遍 §7 的清单。

下一篇给出账本:一跳 1.8 小时,钱到底花在哪------以及为什么"再优化乘法"基本是白费力气。

相关推荐
2601_962381132 小时前
做城市历史视频时,AI 自动生成变迁动效的实现路径拆解
人工智能·音视频
知几蜗牛2 小时前
从Holo4理解GUI Agent的坐标离散化、反映射与误差
人工智能
Token掘金室2 小时前
MCP 怎么接大模型?Model Context Protocol 接入教程
人工智能
谢亮_vipxieliang2 小时前
容器日志收集与管理:从 stdout 规范到 ELK/Loki 落地
运维·网络·人工智能·elk·docker·容器
YOLO数据集集合2 小时前
风力发电机检测数据集 | 风机检测 电缆塔识别 风电运维 无人机巡检 9122期
运维·人工智能·目标检测·目标跟踪·无人机·风力发电·电力巡检
像风一样自由20202 小时前
42.VueReactNextjs如何为AI应用设计前端交互
前端·人工智能·大模型·交互·rag·智能体
知几蜗牛2 小时前
Claude Sonnet 5.5迁移:用离线评测和灰度指标防止静默回归
人工智能
“AI国潮设计-小江”2 小时前
【AIGC实战】Python+SDXL打造潮汕国潮IP:英歌舞麻将的自动化生成与商用思路
开发语言·人工智能·python·prompt·aigc