量化契约与位级对齐
0. ⚠️ 适用范围:本 skill 的结论硬绑定到具体环境,必须先复测再采信
本文档所有公式、常量、数字与"最优参数",都来自一次特定环境的实测(下表各维度的具体取值
见会话产物归档 {run_results_dir}/archive/,正文只留取数方式):
| 维度 | 来源环境 |
|---|
| 芯片 | 目标设备 —— 现场查 core 数 / UB 容量(设备属性 / 算子 UT) |
| 软件栈 | 特定版本的 CANN / triton-ascend / torch_npu / MindIE-SD |
| 模型 | 本案例模型(hidden / heads / head_dim 等几何见归档),少步蒸馏 |
| 形状 | 每 rank (1, S, N/world, D) 符号形态(N = head 数,world = 并行度);S 的具体取值见归档 |
换任何一项,本文档的数字都不保证成立。 具体分四类:
- 硬规格(grid 上限、core 数、UB 容量)—— 可查文档确认,但要确认;
- 该版本的软件行为(
round_mode 取值限制、支持的后端布局、triton 对 cast 的优化、量化算子的 tiling 边界、退化块规则)—— 换版本必须重跑;
- 性能与收益数字(加速比、耗时、占比)—— 随拓扑、并发、机器负载变化,不可移植;
- 阈值与容差(字节一致率门限、"多少差异算正常")—— 是该模型/该任务类型的属性,不可跨任务类型迁移。
特别提醒:"某方案快 / 某方案被证伪"同样依赖环境。
本 skill §七 的已证伪清单在新环境值得各复测一次 —— 证伪结论可能只在原形状/原版本下成立
(例如 kernel 形态、tile 大小、是否压 UB 上限,都会随硬件改变结论)。
0.1 默认动作:枚举多个方案、逐一实测、择优使用
不要照抄本文档的参数值(BLOCK_S、tile 形状、DSL 选择、是否用库算子…)。默认流程:
- 列出候选(≥2):包括本文档推荐的、以及本文档证伪的;
- 统一口径测量:同一进程内交错 / ABBA,避免热漂移(细则见
../perf-gate/references/measurement-discipline.md §2);
- 先比正确性,再比性能:性能最快但数值错的配置必须淘汰 ——
来源环境真实出现过"看起来最快却数值错误(匹配率不足 100%)"的配置,
这是"很容易误采纳的改动";
- 记录两侧:落败方案的读数也要留档,才能说明"在本环境确实更差";
- 形成本环境自有基线,后续比较一律以此为参照;
- 只有在本环境复测过的结论才写进交付。
同理:本 skill 的契约本身也是"待验证的假设"而不是"标准答案"。
正确用法是照着 §五 的位级对拍 SOP 去验证,而不是假定成立 —— 这正是 §一/§二 标注证据等级的原因。
同理,契约与"为对齐它而生的技巧"也都是有寿命的:本 skill 记录的量化契约(scale 粒度、舍入模式、退化块规则)
与为匹配契约而必须做的动作(除数必须从内存载入、bf16 整数 RNE 技巧、大 tile + 64 行子块 amax、
补零对齐…),都是特定版本下逆向/试出来的 —— 下个版本可能把契约写进文档、暴露成原生 API,
或直接修掉逼出该技巧的工具链行为(例如常量除数的强度削减)。
因此套用本 skill 的任何绕行之前,先在本版本复测它是否仍然必需(一条探针 / 一个最小复现即可);
一旦不再需要就删除该条,不要留「以防万一」—— 否则会做无用绕行,甚至拒绝走已经可用的原生路径。
绕行的"存在方式"同样有纪律(单点在 ../README.md §7「禁止把一次性缺陷固化成结构」):
一次性缺陷的绕行一律走可一键关闭、可整体删除的开关,不长期分裂代码路径、不写兼容 shim,
以便确认修复后整块删掉——把绕行固化成结构,等于让"下个版本已修好"这条信息永远无法生效。
为什么需要单独一份契约
量化器是没有文档的二进制语义。它的三个自由度——scale 粒度、舍入模式、溢出行为——任何一个猜错,
结果都会"看起来差不多"(payload 99% 相符、PSNR 只掉一点),却在逐字节验收时全盘失败。
实践中猜错这三项的代价(示例):
| 猜错的东西 | 后果 |
|---|
| scale 粒度(每 tile 一个 vs 每 64-token 块一个) | 三种舍入模式一律 ~58% 字节不同 —— 让人误以为"舍入不是原因" |
| 舍入模式(截断 vs RNE) | 差近一半的字节,不是小误差 |
| 除数用法(kernel 内重算 vs 从内存载入) | 常除数被强度削减成倒数乘法(~2 ulp)→ 静默污染千分之一以下的 payload 字节 |
| 溢出行为(假定饱和 vs 实际回绕) | 去钳位后产生静默回绕垃圾,而不是饱和值 |
bf16 舍入来源(x.to(bf16).to(fp32)) | triton-ascend 上被静默消除(no-op)→ 损失个位数百分比的 payload |
一句话结论:契约必须从设备字节反推,不能猜、不能从"合理"推。
一、MXFP8(e8m0 scale)契约
证据等级:强。有独立逆向文档 + 独立推导 + 独立 numpy 参考实现逐字节完全相符(对拍规模见会话产物归档 {run_results_dir}/archive/)。
1.1 字节编码
byte = 127 + floor(log2(amax)) - 8 = 119 + floor(log2(amax)) clamped to [0, 254]
value(byte) = 2 ** (byte - 127)
语义:amax 被归一化进 [256, 512) —— 即选 scale 使最大元素至多需要 1.xxx * 2^8 表示。
因此一个值可以落在 (448, 512),必须饱和。
判别性测量(amax = 448 * 2**k 扫描):
| amax | 0.4375 | 3.5 | 224 | 448 | 896 | 7168 | 458752 |
|---|
| byte | 117 | 121 | 126 | 127 | 128 | 131 | 137 |
实现要点(承重):用 fp32 指数域取整,不要用 log2:
expf = (bits >> 23) & 0xFF
byte = clamp(expf - 8, 0, 254)
这是精确的(无 log2 舍入风险),且对 0 与 fp32 次正规数自然给出 byte 0(expf = 0 → -8 → clamp 0),与算子一致。
⚠️ 被证伪的公式:ceil(log2(amax/448)) 仅在约八成的组上相符,
在 amax = 450 / 1000 / 230 上失败。floor(log2(amax)) - 8 全部解释。
1.2 scale 布局
shape = [T, ceil(K/32/2), 2] # 连续行主序
swizzle = 恒等映射 # group g 就在扁平偏移 g
用标记法实测(给 group g 一个唯一可识别的 2 的幂 amax,再查该字节的扁平位置):
| group | 预期 byte | 实测扁平偏移 | (g//2, g%2) |
|---|
| 0 | 119 | 0 | (0,0) |
| 1 | 123 | 1 | (0,1) |
| 2 | 127 | 2 | (1,0) |
| 84 | 133 | 84 | (42,0) |
| 167 | 135 | 167 | (83,1) |
结论:[T, ceil(c/2), 2] 只是形状标注,不是置换。 这一发现"removes a whole class of would-be bugs" ——
不要假设它需要 deswizzle。
1.3 payload 舍入 = RNE,饱和于 ±448
单块 amax = 440(scale = 1.0),设备原始 payload 字节:
| value | 1.0625 | 1.1875 | 1.25 | 200 | 440 | 512 | 1000 | 1e30 | bf16 max | -440 |
|---|
| byte | 56 | 58 | 58 | 116 | 126 | 126 | 126 | 126 | 126 | 254 |
| decoded | 1.0 | 1.25 | 1.25 | 192 | 448 | 448 | 448 | 448 | 448 | -448 |
1.0625 是 1.0/1.125 的精确中点 → 1.0(偶数尾数)⇒ RNE,不是 round-half-away
1.1875 是 1.125/1.25 的中点 → 1.25(偶数尾数)⇒ RNE
- 有限溢出直接钳到 ±448(无 inf/NaN payload)
- 由完整 e4m3 幅值表 + 全部 165 个中点及其负值构成的独立参考,在两个指数域下完全相符(100%)
1.4 退化块规则(逐条实测,容易漏)
| 块内容 | scale byte | payload |
|---|
| 全零 | 0 | 全 0 |
amax < 2^-119(如 1e-38) | 0 | 全 0 |
amax = 2^-110(byte 9) | 9 | 正常 RNE |
任意位置 ±inf | 255 | 整块全 0 |
任意位置 nan | 255 | 整块全 0 |
byte 0 的真实语义是 flush-to-zero(byte 0 要求 amax < 2^-119 ⇒ 块内每个值都是 bf16 次正规数)。
融合内核应采用:组 byte 为 0 或 255 ⇒ 该组 payload 全 0;byte 1..254 走 RNE(bf16_value / 2**(byte-127)) + ±448 饱和。
1.5 被量化的对象是 bf16 结果,不是 fp32 中间值(承重)
生产链路是 norm(...) -> bf16 tensor -> quant ⇒ amax 与 payload 都基于 bf16 舍入后的值。
改用 fp32 中间值会"在块 amax 跨越 2 的幂时改变指数 —— 随机数据约每 100 块 1 块"。这是设计承重点。
二、int8 契约
⚠️ 证据等级:弱(必须如实标注)。int8 没有同强度的逆向文档;结论只有零散来源(实验脚本与单句记录),
且其中一部分仍处于「待完成的阶段」。引用时必须带上这个不确定性。
| 项 | 值 | 证据等级 |
|---|
| scale 粒度 | 每 (head, 64-token block) 一个(每 scale 归约 64×128 = 8192 元素) | 源码 + reshape 机制,较强 |
| 舍入 | RNE(magic-number 手法 12582912.0 = 1.5*2^23) | 较弱:核心数字(truncate 差近一半)仅有单一来源的一句话记录、未经独立复现,原始日志不在手、无交叉验证 |
| 饱和 | ±127 | 脚本实现,中等 |
| 除数 | 必须 LOADED(不得在内核内重算) | 强(有隔离实验,见 §3.1) |
| 尾块 | S % 64 != 0(序列长不被块尺寸整除)时必然存在尾块 ⇒ 必须精确复现 F.pad-then-slice(本案例的余数见归档) | 未完成(尚无已验证的复现实现) |
关键陷阱(值得单独记):粒度错误时三种舍入模式一律落到 ~58% 差异字节 ⇒
若你在粒度还错的情况下比较舍入模式,会得到"舍入不是原因"的错误结论。先对粒度,再比舍入。
RNE 的整数实现手法(可迁移):
magic = tl.full((1,), 12582912.0, tl.float32) # 1.5 * 2**23
q = tl.cast((v + magic) - magic, tl.int32)
q = tl.minimum(tl.maximum(q, -127), 127) # ±127 饱和
三、三条实现普适规则(比具体公式更重要)
3.1 除(divide),不要乘倒数;除数必须从内存载入
在内核内计算 amax / 448.0 会被 triton-ascend 强度削减为倒数乘法(约 2 ulp 偏差),
而 2 ulp 的 scale 误差会翻转千分之一以下的 e4m3 payload 字节 —— 静默、且看起来只差一点点。
隔离实验(分母 2 097 152):
| 写法 | 差异 fp8 字节 |
|---|
torch x / (amax/448) | 0 |
triton x / scale(scale 从内存加载) | 0 |
triton tl.fdiv(x, scale, ieee_rounding=True) | 0 |
triton x * (1/scale) | 零星(个位数) |
torch x * (448/amax) | 成百上千量级 |
triton/torch (x / amax) * 448 | 成百上千量级 |
(表中差异只保留量级;具体字节数与实验规模见会话产物归档 {run_results_dir}/archive/。)
结论:aclnn 的 payload 是 RNE(x / (amax/448)),使用正确舍入的 fp32 除法。
3.2 需要 bf16 舍入时,用整数 RNE 位技巧
b = modulated.to(tl.int32, bitcast=True)
m = ((b + 0x7FFF + ((b >> 16) & 1)) & -65536).to(tl.float32, bitcast=True) # == 硬件 bf16 RNE
# 不要用 modulated.to(tl.bfloat16).to(tl.float32) —— 在 triton-ascend 上是 no-op(静默消除)
证据:用 262 144 个精确 bf16 中点证明 x.to(bf16).to(fp32) 与 torch 的 bf16 cast 完全不相符(0%),且原样返回输入。使用原始 cast 让某 kernel 损失个位数百分比的 payload 字节
(≈ 8-bit 尾数输入在 e4m3 上的 1/32 中点率)。
3.3 8-bit 转换通常是回绕(wrap-around),不是饱和
不要从"未钳位结果的 min=-128 / max=127"推断会饱和 —— 任何 int8 张量都是这个范围,该推断无价值。
真证据:强制 inv = 1e5 得到 256 个大致均匀分布的不同输出值(频次峰值与均匀期望同量级)
—— 这是回绕签名。
实践后果:去掉钳位只因为归一化保证了范围(scale = amax/127 ⇒ |x/scale| ≤ 127 按构造成立)才安全,
不是因为 convert 会拦截溢出。必须把这个不变式写进注释 ——
任何破坏它的后续改动会产生静默回绕垃圾,而不是饱和值。
四、组尺度耦合:为什么融合量化器天生与字节精确为敌
一个组 scale 让 32(或 64)个元素共享命运:1 ULP 的激活差异若恰好命中该组最大值,
就会翻转整组的 e8m0 指数,从而改变该组全部 payload 字节。
实测(某融合量化器;模型几何、绝对计数与对拍规模见会话产物归档 {run_results_dir}/archive/,下列只保留量级):
groups with a DIFFERENT scale byte : 千分之几量级(全部 delta = -1)
rows with >= 1 changed group : 三成以上
bf16 bytes differing : 每元素 1 ULP(占全部字节的万分之几)
payload bytes differing : 个位数百分比
即:输入侧万分之几量级的差异 → payload 侧个位数百分比的差异(放大数百倍)。
这是任何重实现的固有属性,不是某个内核的缺陷(纯 PyTorch 参考也显示同样残差)。
诊断价值(高):错误 scale 字节全部恰好低一个指数({-1: 4374})是
"amax 取到了低一个 binade" 的签名 —— 即量化器看到的是 fp32(x) 而非 bf16(x)。
这类"全部同向、幅度恰为一个 binade"的残差比随机分布更有诊断力,优先看它的分布形状。
四·B、量化前移:把量化放到集合通信之前(组尺度对齐条件)
动机:量化后的载荷是 8-bit,而集合通信(a2a / all-gather)默认传 bf16 ⇒ 把量化搬到通信
之前,通信字节直接减半。这是"减少通信量",不是"掩盖通信",两者可叠加;本节的判据回答
「前移之后还能不能保持逐字节等价」。
关键在于每个 scale 的归约域落在哪一侧——按归约域分三类,逐类判定:
| scale 类型 | 归约域 | 前移是否字节精确 | 判定/代价 |
|---|
分片局部(如沿序列的 per-block scale,块尺寸 b) | 单个 rank 的分片内 | 可以,条件是块网格对齐 | S % (world × b) == 0:各 rank 等长且分片起点落在全局块网格上,块成员与前移前完全一致 |
| 跨分片全局(如 per-(head,channel) 的 amax,归约域 = 整条序列) | 全部分片 | 不可以,除非先补齐归约 | 量化前对 scale 张量做一次 all_reduce(MAX)(张量极小,但这是每层每块一次的新增集合通信,必须计入收益账) |
| 由全量数据派生的元数据(如稀疏 mask 由池化后的全序列 Q/K 生成) | 全部分片 | 不可以 | 要么把派生计算也搬到通信前(代价常吃掉收益),要么用后移数据反解(pool 块跨两个 scale 时取加权平均 (Σs₁·a₁ + Σs₂·a₂)/N),要么为它单独走小张量侧信道(pool 后仅 1/b 的字节)。⚠️ 更正:侧信道可以做到位级(不是"放弃字节精确")—— 条件是 定序归约 + 尾块感知除数(见 §四·B.1);条件不满足时才退化为 L2 数值门 |
不能精确时的处置(强制):报漂移清单——payload 差异计数、端到端 md5 / 相对误差、
差异归因(边界块改组 / 全局 amax / mask)。不得把「数值接近」写成「无损」;
反过来,md5 恒等也不是"没生效"(见 §五.5 的反向陷阱)。
收益账必须三头算(逐张量算,别用「减半」口头禅):
省下的字节 = Σ(被量化张量: 原字节 − 量化字节 − scale 字节) ← 未被量化的张量一分不省
新增成本 = 新增集合通信次数 × 单次固定开销(小张量集合是延迟主导,不是带宽主导)
+ 可能多出的一次全量/池化计算
有效时间 = t0 + 字节 ÷ B(字节) ← B 随消息变小而下降,必须实测,不得线性外推
- 只有被量化的张量减半:同一段通信里常夹着未量化的部分(注意力输出回传、残差/门控、
只量化了一侧的 KV…),它们保持原字节 ⇒ 期望降幅要按张量逐个算。本组合量级:一次 a2a
组含 4 张量(q/k/v 前向 + o 反向),只有 3 张可量化 ⇒ 期望降到
(3×0.5+1)/4 = 62.5%,
即减少约 3/8 而非 1/2。
- scale 自身也要传,但先算比例再决定是否计入:per-block scale 是「每 64 token × 整条
head_dim」一个 fp32 ⇒ 约占量化载荷的
4 ÷ (64 × head_dim)(head_dim=128 时约为万分之几);
per-channel scale(如 V 的 per-(head,channel) amax)更小。不要凭感觉给余量,
也不要因为它"要传"就否定整个方案——量级差三个数量级。
- 带宽不是常数(承重):
有效时间 ≈ 固定开销 t0 + 字节 ÷ B,且 B 随消息变小而下降
(大消息趋近峰值,小消息被启动/同步开销主导)。消息缩小后能否仍按原速率,必须用同
communicator、同拓扑的尺寸扫描实测(方法见
dit-parallel-opt/references/parallel-plan-attribution-method.md §6),不得线性外推;
最省事的同源证据 = 同一 A/B 里两臂的消息尺寸与耗时各自算一次隐含 B,直接读出「B 掉了多少」。
判定工作流:
- 列出被前移张量的每个 scale 及其归约域(哪一维、是否跨 rank);
- 对每个 scale 套上表判据,写出对齐式(
S % (world × b))并用实际 shape 验算,
不要用「应该没问题」代替;
- 若只需补一次小
all_reduce(MAX),先实测这次集合的固定开销(延迟主导),再决定是否前移;
- 若涉及 mask 等元数据,优先选侧信道传池化小张量(字节少一个数量级)而不是把派生计算前移;
该侧信道位级可达,条件是定序归约 + 尾块感知除数(§四·B.1),不满足时按 L2 数值门声明;
- 落地后按 §五 位级对拍:精确就报 0 字节差,不精确就报漂移与归因,二者不许混说。
与 §四 的关系:§四 说明「1 ULP 的输入差异会被组尺度放大」,本节是它在通信顺序上的推论——
前移改变了「谁参与同一个 scale 的归约」,因此放大的是改组后的整块 payload,
看起来"只差一点点输入"也会产生成片的字节差。
四·B.1 侧信道的位级可达性(旧判决"不可达"已作废:可达,但有两项条件)
⚠️ 更正(实测推翻,先读这条):本节曾写"部分和侧信道无法位级复现,因为归约结合顺序是
全局调用形状的函数"。该判决已作废 ✗ —— 它的依据(形状依赖表、"数十种候选结构无一匹配")
是在一个并不存在的"形状依赖结合顺序"里搜索出来的。位级一致是可达的。
推翻它的实测(单卡、无模型;口径 = 逐元素差异计数 / 总数):
| 测 | 内容 | 结果 |
|---|
| S | 固定一个池化块的同样数值,把调用内块数 n 从 1 变到生产形状(数百档) | 0 / 128 不同 ⇒ 该均值算子与 n 完全无关("形状依赖"不成立 ✗) |
| D1 | 逐 rank 部分和、统一步长 ÷ 块长 | 128 处不同、全部落在 tail cell,最大相对误差 1 − 32/128 = 0.75(该 cell 整体偏约 4 倍量级) |
| D2 | 逐 rank 部分和、尾块感知除数(tail 除以真实长度) | 0 差异 ⇒ 位级精确 ✓ |
| D3 | 部分和用 fp64 合成 | 0 差异 ✓ |
| D4 | 在拼齐序列上取精确 fp64 块均值 | 0 差异 ⇒ 库的结果就是正确舍入的块均值 ✓ |
⇒ 更正后的三条结论:
- 位级可达:分布式部分和分解 + 尾块感知除数即可逐位复现(D2/D3/D4 全 0);
- 真正的不一致来源是"我们自己的归约不确定",不是"库的结合顺序特殊" ——
同一输入重复调用同一归约就会得到成片的元素差异(规模见会话产物归档
{run_results_dir}/archive/)⇒ 换定序分段归约
(按 cell 排序后定序求和)是个工程任务,不是研究死局 ✓。
这也解释了"约 60 种候选结构一个都匹配不上":族搜错了 —— 正确姿势(先判形状相关性、再按
相邻配对 / 对折 / 跨步 / 分块逐族穷举)见 ../../operator-dev/references/triton-ascend-lowering-pitfalls.md §三。
另:放置类算子必须用「累加」语义 —— index_copy_ 对重复索引是覆盖(后者胜),
当两个来源合法地落到同一位置时会静默丢数据 ✗(本仓形态:同一 cell 内 k 与 k + L/2
会落到同一 (row, col));但累加类归约默认不确定,所以要用定序累加(同上),
不要用"重复调用结果不同"的算子做放置;
- 尾块是承重的:统一步长会把 tail cell 整体做偏(D1)⇒ 除数必须取 cell 的真实长度
(
counts[-1] = used_len % POOL 那种形态),不能用常量。
⇒ 判决表最后一行"放弃字节精确"相应改为:可达,条件是 ① 归约确定(定序,不得用
"重复调用结果不同"的归约)、② 除数尾块感知、③ 若还要求与全局归约一致,amax 这类
归约域的边界块要显式补一次小集合通信。任一条不满足 ⇒ 退化为 L2 数值门(层级见下)。
不要过度声称(另一半仍成立):位级可达说的是侧信道这条链路;库自身仍可能不精确 ——
实测里用精确 fp64 参照在多 seed 多族上比,仍有一族以上与库不一致,且阈值比较的平局
(probs >= threshold 形态)会把极小的数值差放大成离散的选择差异 ⇒ "可达"≠"到处都逐位":
每换形状 / seed / 阈值口径都要重新对拍(这也是 §五 SOP 要求逐形状的原因)。
三条可迁移判据(更正后):
- 先问"是不是我自己的归约不确定":出现"只有少数元素不同"的差异时,第一件事是同一输入
重复调用自己的归约看是否自洽(本仓实测不自洽,计数如上)。把责任先推给"库的顺序特殊",
会把一次工程修复拖成一轮研究 ✗;
- "与现役实现一致" ≠ "与参考实现一致":现役(eager)与融合实现偏离参考的量级相同、
且互有胜负(各自的近/远与相等样本都在同一量级;读数见会话产物归档
{run_results_dir}/archive/)⇒ 验收目标只能是
参考实现(例:拼齐序列上的 fp64 精确均值),"和现役逐位相同"会把错误一起固化 ✗;
- 顺序敏感性要用对抗分布才看得见:真实量级激活下 fp32 求和确实随顺序变
(三种顺序给出 23 / 11 / 26 组不同;动态范围拉到 2²³ 量级升到数千组),
但最终 bf16 结果与库 0 / 8192 全同 —— 是末级 bf16 舍入把这点关联误差吸收掉了 ✓。
更正:此前写的"8+7+7 位 < 24 位 ⇒ 求和本身精确"是错的 ✗(fp32 求和并非精确);
结论仍成立,但理由是末级舍入吸收。⇒ 判断"顺序会不会影响产物"必须在最终 dtype 上比。
层级不得混用:位级可达是有条件的 —— 条件不满足时该侧信道只是 L2 数值门
("数量级更小"≠"无损"),违反 ../accuracy-gate/SKILL.md 的铁律。
五、位级对拍 SOP
- 参考必须同进程、同数据运行生产链路 —— 跨进程/跨窗口比较会引入漂移。
- 分开计数:payload 用
view(torch.uint8)、scale 用 view(torch.int32),
报绝对计数(0 / 67 665 920)以及 max|Δscale|(期望 0.000e+00)。
- 独立参考实现(不复用被测代码路径),在完整张量上要求逐字节 100% 相符。
- 对抗性输入清单(缺一项就可能漏掉一整个 bug 类):
- randn / uniform[-1,1] / all-equal
- 全部 tie 中点(165 个 e4m3 中点 × 两个指数域)
- amax 恰为 2^k / 略高于 2^k
- 全零 / 全负 / big+small mix
- bf16 次正规 / 1e-38 / inf / nan(退化块)
- 饱和边界(>448 或 >127)
- 尾块(
S % 64 != 0)
- 端到端无损门:md5 恒等比任何 PSNR 阈值都强 —— 但前提是"管线本身逐位确定"已被单独验证。
⚠️ 反向陷阱:逐字节相同的 md5 不是"门控没生效",而正是字节精确优化所预测的结果。
实践中曾把它误读为失败信号。
- 诚实例外要写出来:唯一不精确的输入类别(如"整张 bf16 次正规",占比为千分之几量级、全是符号零;
读数见会话产物归档
{run_results_dir}/archive/)
应显式记录为"已刻画、不追",不要藏。
- 先证明仪器,再做对拍:参考实现自身也要过一遍「跨流依赖是否写全 / 顺序是否正确 / 快照是否与
被测动作同时刻」的检查(口径见
../perf-gate/references/measurement-discipline.md §8)。
本仓实测:参考侧漏了一处跨流依赖,把正确的载荷报成上千条"污染记录" ✗ ——
这类假阳性的症状与真实缺陷形态高度相似,靠看形态分不出来。
六、工具链陷阱
| 陷阱 | 现象 | 处置 |
|---|
Tensor.to(torch.float8_e4m3fn) | NPU 上抛异常(普通 dtype cast 到 fp8 未实现) | 用算子 API |
torch_npu.npu_dtype_cast 到 fp8 | 越界行为不同:449/512/1000 → byte 127 (NaN) | 它不等于量化器的饱和转换,不能当参考 |
torch.arange(dtype=torch.uint8, device='npu') | aclnnArange ... code 107001 | 用 torch.empty/zeros 分配 |
torch.frexp | NPU 上静默回落 CPU | 参考实现中禁用 |
.view(torch.uint8) 传给 triton 做 fp8 输出 | triton 看到 uint8 指针 → 截断而非量化(match 不足 1%) | 用原生 fp8 dtype |
w 与 w_scale 置换 | 只换 w 不换 w_scale → 结果静默塌到接近 0 | 必须同序置换 |
积分归约(amax/amin 两次读) | 两次读的基线耗时 | torch.linalg.vector_norm(v, inf, 2) 单次读快近一倍(绝对 µs 见归档),且 0/1792 精确 |
| 手写 Triton 融合归约 | 最优值也输给两次 torch 读 | "手写融合归约是错误答案;正确是一个本身只读一次的现成库算子" |
scale 公式实测:scale == amax/448.0 精确相等(max|scale - amax/448| = 0.0),
不是 2 的幂(max|scale - 2^ceil(log2(amax/448))| = 7.78e-3)。全零输入 → scale = 0.0。
七、已证伪清单(不要重复)
| 假设 | 证伪它的实测 |
|---|
MXFP8 指数字节 = ceil(log2(amax/448)) | 仅约八成相符;在 amax=450/1000/230 失败 |
npu_dtype_cast(x, fp8) 与量化器行为一致 | 449/512/1000 → NaN,不是饱和转换 |
| dtype cast 到 int8 会饱和 | 是回绕:inv=1e5 得 256 个均匀值 |
| 除数可以在内核内重算 | 常除数 → 倒数乘法(~2 ulp)→ 千分之一以下的 payload 差 |
用 x.to(bf16).to(fp32) 做 bf16 舍入 | 在 triton-ascend 上被静默消除,完全不相符(0%) |
| 用 split-tree 替代归约可绕过编译问题 | 约 4 倍更慢(绝对值见归档) |
| 量化器核心成本在 fp8 store / cast | A4(payload 存 bf16)与 A7(去 store)的读数都落在噪声内 ⇒ 免费;成本在归约及 scale-byte epilogue(占该核主要部分) |
| 非 I/O 成本随归约调用数/ tile 形状缩放 | 随"产生的 (row, group) 最大值个数"缩放(wide 对照耗时约为窄形态的一半) |
| 把转置 view 交给库算子会被"吸收" | 五个捷径全部字节精确但只省几十 µs 量级 ⇒ view 是被计价的(costed, not absorbed) |
| 手写融合归约比两次库读更快 | 手写融合归约慢于两次库读;对单次读的 vector_norm 更差 |
y * 0.0 可保留 flush 的符号 | 反方向错误:差异扩大若干数量级(读数见归档);字面 0.0 才正确 |
| int8 粒度猜错时可以从舍入模式看差异 | 粒度错时三种舍入一律 ~58%,无法区分 |
| "它编译了" / "scale 是对的" 可作为正确性证据 | double-tl.reshape 形式编译通过但 payload 接近但不等于 100% |
用逐 rank 部分和侧信道复现"组装后单张量归约"的位级结果 | 已作废 ✗✗:曾记"归约顺序是全局调用形状的函数 ⇒ 侧信道不可达",实测推翻 —— 该均值算子与调用内块数 n 无关(n=1…生产形状逐位相同),且尾块感知除数下部分和信道逐位精确(0 差异)。真正的差异来源是我们自己的归约不确定(重复调用同一归约会得到成片差异,规模见归档);旧结论作废见 §四·B.1 |
| 在 randn 量级输入上判"归约顺序是否影响位级结果" | 真实量级下 fp32 求和确实随顺序变,但最终 bf16 结果与库 0 / 8192 全同 ⇒ 末级舍入吸收了这点关联误差(更正:旧记载"8+7+7 位 < 24 位 ⇒ 求和本身精确"是错的 ✗)⇒ 判断顺序敏感性必须在最终 dtype 上比 |
| 用"与现役实现逐位相同"当侧信道验收目标 | 现役实现自身偏离参考(实测二者偏离量级相同)⇒ 该目标会把错误一起固化;验收目标只能是参考实现 |
| 把"数量级更小"的池化侧信道写成"无损" | 它是 L2 数值门,不是 L1 逐字节(层级不得混用,见 ../accuracy-gate/SKILL.md) |
八、参考文件
- 📐
references/contract-reverse-engineering.md —— 契约逆向六步法(判别性探针、标记法定布局、独立参考、退化覆盖、corrigendum)+ 完整探针数据与出处。要逆向一个新量化器时读它。
- 🔎
references/online-quant-contract.md —— 在线量化契约(算法分派、被量化的范围:哪些层/是否含 FA、图节点形态与布局互通)——自 dummy-run 下沉;判断"某框架的 online scheme 实际量化了什么、和参考实现对不上在哪"时读它。
- 🔗
references/lora-merge-and-precision.md —— 低秩适配器(LoRA)离线合并进权重的精度判据与形态选择(合并代数与"去冗余"定位、精度存活判据与三臂对拍、三形态选择决策、判据修正纪律、loader 与摆位契约)——何时读:任何"把运行时计算折进权重"的候选(适配器合并、把缩放/偏置折进相邻线性层、把量化残差折回权重)在定形态或下"合并后不等价 ⇒ 不可用"结论之前读它。
路由判据(何时读上面第三条):① 出现"把运行时的算子链换成权重里的一份预计算结果"的候选时——先按它的 §1 判"在 ckpt 里量化还是在 load 期量化",再进 §2/§3;
② 需要判"合并后增量还在不在"时——按它的 §2 三臂对拍取数,不要用自写模拟器;
③ 要因"数值上不等价"否掉一条合并路线时——按它的 §4:纯数值研究只能推翻等价性,可用性必须由服务端产物级 A/B 收口。
九、与既有 skill 的分工
operator-dev —— 算子开发/优化与 DSL 选型(CV→catlass / VV→triton);本 skill 是它的量化契约补充层。
- 其
fusion-scope-analyze/references/fusion-unit-method.md 与 fusion-benefit-method.md 讲融合范围与收益(含 §3.1 的"移除流量 vs 移除延迟"判据);本 skill 讲数值契约。
dit-perf-opt —— 回答**"该不该开量化、开哪一档"(特性选型与质量门禁);本 skill 回答"这个量化器到底怎么算的"**。
accuracy-gate/references/quality-gate.md 是有损档位的阈值门禁;本 skill 的位级对拍是无损证明,两者不可互相替代。
dit-parallel-opt —— 并行作用域失效(静默降级)的判定(DiT 侧)与并行陷阱清单;其
dit-parallel-opt/references/ascend-parallel-traps.md §8 只保留了量化契约的指针,契约本体在本 skill;
测量与上报口径见 perf-gate。
dit-parallel-opt —— 并行选型、通信掩盖与归因;本 skill §四·B 只回答「量化前移能否字节精确」的数值契约,形态选择与收益判定在那侧。
pattern-dev —— 量化融合 pattern 的图命中与收益核验;本 skill 提供"数值上到底对不对"的判据。
十、维护与更新
- §二 int8 一节的证据等级必须随新证据更新:目前"RNE / 误差差近一半"仅有单一来源的一句话记录、未经独立复现;
若能复现,改为强证据并补上分母与形状。
- 当 CANN 量化算子行为变化(派发 kernel 名、tiling 边界、退化块规则)时,重跑 §五 SOP 并更新 §一/§二。
- §七 已证伪清单应持续追加 —— 它比正面结论更省机时。
- §四·B.1 的"位级可达"结论必须随版本复测:可达性依赖 ① 该均值算子与调用形状无关
(换 CANN / 算子实现后可能变成形状相关 ⇒ 那时尾块感知除数不再够用),
② 该归约在库里的语义仍是"正确舍入的块均值"。
复核方法:构造一个固定池化块,分别在
n = 1 / 中间档 / 生产形状 三次调用里求均值,
比较该块取值是否仍与 n 无关(当前实测 0 / 128 不同);再用"统一步长 vs 尾块感知步长"
各合成一次部分和,看前者是否只在 tail cell 出错、后者是否 0 差异。
另:用大指数跨度分布跑一次顺序敏感性对拍(最终 dtype 上比),否则会得到"任何顺序都相等"的假阴性。
- "自己的归约是否确定"要作为常规前置检查:凡在侧信道/中间量里做归约,先重复调用两次比自身
(当前该归约不自洽,计数见 §四·B.1 结论 2);换成定序归约后本检查应为 0 差异。