【FHE】(九):逐层尺度自适应——为什么每层都要单独定标

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

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

0. 一句话结论

密文槽位的可表示范围是固定的(scale = 2^60,且要求 |z| < 0.5)。但 28 层的中间值分布千差万别——有的层输出在 1 附近,有的层能到 55。

所以每层需要一个自己的尺度因子 F{L},把该层的值压进可表示范围。而这个定标不是随便取的,它被两个硬约束夹住:

约束 值 来源
上限:压完后的 max ≤ ~0.35(目标 TGT_MAX = 0.40) decode / boot 的可表示界
下限:压完后的 rms ≥ ~0.02 信噪比 ≥ 20,对抗约 1e-3 的噪声地板

上限防溢出,下限防"压太小被噪声吃掉"。 两层之间的空间不宽,这就是这项工作的核心难点。


1. 为什么"全链一个尺度"行不通

如果所有层共用一个 F:

  • 按最大的那层选 F → 其他层被压得过小 → rms 掉到噪声地板以下 → 精度被噪声吞掉;
  • 按最小的那层选 F → 大值层直接溢出 → 整个槽位废掉。

这不是理论担忧。实际链上确实存在"爆炸层"——脚本注释里点名列了一个(真链 y2 附近能到 ~55)。一层爆炸,全链定标策略就得改。


2. 链状态的定义

脚本 tools/preproc/_adap_scale.py 的核心变量是"链状态"(原文):

s{L} = y{L} / F{L}
  • y{L}:第 L 层的真实输出(明文侧算出来的)
  • F{L}:该层的尺度因子,取 2 的幂
  • s{L}:真正被送进密文链的量

用 2 的幂有两个好处:与 scale = 2^60 的二进制定标天然对齐(乘以 2 的幂只改指数);而且 F 的选择可以离散化成"取到最近的 2 的幂",避免浮点抖动导致不同机器选出不同的 F。


3. 两个硬约束的来源

3.1 上限 s max ≤ ~0.35(TGT_MAX = 0.40)

理由写在脚本注释里:decode 与 boot 的兼容界。

回顾第 5 篇:scale = 2^60、槽位要求 |z| < 0.5,这样 2^60 · z < 2^59 小于任一 60-bit 素数,中间层解密无需 CRT。所以密文槽位能装的值上限就是 0.5——而 0.40 是留了余量的工程目标值。

又因为 F 取 2 的幂,向上取整后实际的 max 落在 (TGT_MAX/2, TGT_MAX] 区间内,所以目标实质上被实现成 s max ≤ ~0.35。

3.2 下限 s rms ≥ ~0.02(SNR_MIN = 0.02)

这一条是很多人会漏掉的。 把值压小虽然能防溢出,但密文里始终存在噪声——本项目的噪声地板约 1e-3。如果 s 的均方根被压到和噪声同量级,信号就被噪声淹没。

脚本给的判据是 s rms ≥ 0.02,对应信噪比 ≥ 20。

所以定标是一个双侧夹逼问题,不是"压得越小越安全"。

3.3 还有一条硬界:至少压到 ≤ 0.5

除了 0.40 的目标界,脚本里还单独算了一个 need50:至少把 max 压到 0.5 以下(否则 decode/boot 根本不能用)。当目标界与硬界冲突时,硬界优先。


4. F 的调度规则

脚本里的规则可以完整复述出来(这也是本文最实用的部分):

规则 内容
取幂 F{L} = 2^ceil(log2(ymax / TGT_MAX)),即按该层 max 取到 2 的幂
单调不减 只允许"下缩",不允许回缩(F 单调不减)
相邻比 ≤ 4 相邻两层的 F 比值上限 RATIO = 4,避免尺度突变引入额外误差
L0 例外 lay0 的输入是明文(没有被前层稀释过),所以只按 max 定 F,不做单调 clamp
爆炸层例外 如果某层真的爆了(如 y2 ~ 55),允许跳升到 need50,此时可以超过 4× 比值

最后一条是"规则为现实让路"的典型:4× 比值是一条经验约束,不是不变式。当某层确实越界时,宁可破例,也不让整层溢出。


5. 一个有价值的工程性质:幂等重建

定标脚本会覆盖一部分已有文件(残差路径的参考值 l{L}_u1 / l{L}_u2_ref / l{L}_mid16,以及 ln / m2c 的拟合系数),所以"可重复运行"是一个必须保证的性质。

脚本用的办法很朴素但很有效:

  1. 先把原始文件备份到 _g1024_bak/;
  2. 每次运行时,先把备份恢复回去;
  3. 然后从 embed4 + o + mlp 重新前向积分出真值 y{L}。

为什么这能保证幂等?因为重建用的 o(attention 输出)与 mlp_out 永远不会被这个脚本覆盖。所以"真值的来源"不受上一次运行影响——输入不变,输出必然不变。

而"以为自己在重建、其实用的上次结果"是这类脚本最常见的隐性 bug。这条设计是刻意的。

顺带一个已被验证的前提(脚本注释原文):明文恒等式

y{L} = y{L-1} + o{L} + mlp_out{L}        已验 1.5e-5

也就是说"残差链路的加法关系"在明文侧是对得上的(误差 1.5e-5)——先证明明文恒等式成立,再去调整密文定标,顺序不能反。


6. 一个不愿加 eps 的细节

ln 分支(求 1/√u)的拟合里有一行注释值得单独抄出来(原文):

y = 1.0 / np.sqrt(u) / 128.0   # 无 eps: m2c~1e-6 时 +1e-6 会污染 ~20% (u>=0.5*m2c 永不触 0)

含义:通常写数值代码时,1/sqrt(u) 会加一个 eps 防止 u = 0 除零。但这里故意不加,因为:

  • 拟合区间已经限定为 u ∈ [0.5·m2c, 1.5·m2c]——从不触及 0;
  • 而 m2c 的量级约 1e-6,此时加 1e-6 的 eps 相当于把分母改变了一倍以上,会污染约 20% 的取值。

这是一个"防御性编程反而造成系统性偏差"的实例。 加了 eps 更"安全",但更错。拟合区间 z = (u − c0)/(8·s0) 的标准化、以及 deg-8 多项式,都是为了在已知的确定区间上把误差压住,而不是靠 eps 兜底。


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

本文所述参数为机制验证级,远低于 HE 参数标准的 128-bit 水平,不得用于保护真实数据。
本文主张的是:定标约束与调度规则。
本文不主张:安全强度、性能优越性。

8. 这一篇的未解问题

  1. TGT_MAX = 0.40 / SNR_MIN = 0.02 是经验值。它们分别来自"可表示界留余量"与"信噪比 ≥20",但留多少余量、信噪比为什么是 20 而不是 15 或 30,没有推导。这两个数是整条链精度的总闸门,却是拍的。
  2. RATIO = 4 同样是拍的。它约束的是"相邻层尺度不要突变",但突变到多少会真的伤精度,没有测。
  3. 定标是逐层独立做的,没有全局优化。严格说,每层可用误差预算应该由"下游还剩多少层"反推,我们目前是逐层局部决策 + 事后整链验收。
  4. 换模型不能自动适配。这套规则依赖对明文参考的统计,统计要重跑,中间还有人工判断(比如爆炸层要不要破例)。

下一篇我们看 attention:密文里没有 max,那 softmax 的数值稳定技巧怎么办?答案是"用一个巨大的定标常数把它换掉"。

posted @ 2026-09-25 09:50  haliu  阅读(8)  评论(0)    收藏  举报