EUP Payne-Hanek 范围归约
本页所有地址都适用于
libtpu-0.0.40-cp314wheel 中的libtpu.so(构建libtpu_lts_20260413_b_RC00,BuildID md589edbbe81c5b328a958fe628a9f2207d)。该二进制未去符号化,下文每个符号都是反混淆后的 C++ 名称。段映射:.text/.rodataVMA == 文件偏移;.data.rel.roVMA − 0x200000 == 文件偏移。其他 libtpu 构建会有所不同。
摘要
EUP sinq/cosq 超越函数不能直接推入原始参数。一次 sin/cos push 期望参数已经折叠到单个象限中,大致为 [−π/4, +π/4] 加上一个 2-bit 象限索引,因为 push 所求值的硅上多项式只在这个很窄的窗口内准确。对于 |x| ≫ 1,朴素折叠 x − round(x/(π/2))·(π/2) 是灾难性的:π/2 的 float 表示只有约 24 bit 精度,所以很大的 x 在相减后没有任何幸存的小数 bit,归约后的参数只是噪声。修复方法是经典的 Payne-Hanek 归约:不去减一个低精度的 π/2 倍数,而是将 x 乘以 1/(2π) 的极高精度定点展开,只保留由该参数二进制指数决定为相关的乘积小数窗口,并从这个窗口化的小数重建归约后的参数。这是与 trig 路径上的 EUP sinq/cosq push 配对的整数预处理;push 本身、它的 slot-3 编码以及延迟 pop 是 slot-eup-transcendental 的主题。
该归约位于 PayneHanekRangeReduction(@0x1d5819c0,在 xla::jellyfish 匿名命名空间中)。它不是一个 .rodata 常量池表,1/(2π) 展开被具现化为六个 mov esi, imm32 字面量,直接嵌在发射器的 .text 中,每个都送入 LloModule::VectorU32Constant(@0x1d506400),后者在生成的 kernel 中把它们降为 LLO VectorU32Constant op。因此该归约是编译进 trig lowering 的,而不是从数据段加载。该函数发射整数窗口化乘法,把整数圈数转换为 float(象限索引 k),并用 float-π/2 有效数和 −30 二进制点指数的一次 VcomposeF32 重建小数余量 r。
对于重新实现,其契约是:
1/(2π)表。 六个连续 32-bit word(MSB-first),在.text中逐字节精确,可重建为0.15915494309189535 = 1/(2π)到 fp64。- 窗口化乘法序列。 指数 → 窗口索引(对
0x20做SubS32)、逐 word 的Shrl/Shll/Or窗口组装,以及VshllU64High(@0x1d583ac0)+VmulU6464-bit 高位乘积提取,只保留相关小数 bit。 - 象限索引
k。 整数圈数的VcvtF32ToS32/SimplifyConvertS32ToF32(LLO unop0x108),以及选择逐指数窗口的 16 条SimplifySelect分支;k mod 4选择符号/交换。 - 重建。
VcomposeF32(π/2 significand0x00c90fdb, exponent = −30 − window)和最终形成r = frac · (π/2)的VmulF32。
| 函数 | xla::jellyfish::(anon)::PayneHanekRangeReduction @0x1d5819c0 |
| 常量加载器 | LloModule::VectorU32Constant @0x1d506400(每个 imm32 一个 LLO op) |
| 表形式 | 6 × u32,MSB-first,作为 .text 立即数,不是 .rodata 池 |
| 重建为 | Σ wᵢ·2^(−32(i+1)) = 0.15915494309189535 = 1/(2π)(精确到 fp64) |
| 高位乘积辅助函数 | VshllU64High @0x1d583ac0;宽乘路径上的 VmulU64/VmulU32(@lines 855–858) |
| 二进制点偏移 | 0xffffffe2(−30)→ SimplifySubS32 → CreateVectorBinop(0x121) |
| π/2 有效数 | 0x00c90fdb(float π/2 = 0x3fc90fdb 的 24-bit 有效数) |
| 重建 | VcomposeF32 @0x1d555860;最终 VmulF32 r = k_frac·(π/2) |
| 象限选择 | 16 × SimplifySelect(窗口/象限修正);k mod 4 符号/交换为推断 |
为什么是 1/(2π),而不是 2/π
目的
传统名称是 “2/π reduction”,因为目标是 r = x mod (π/2) 和 k = floor(x/(π/2)),即用 π/2 为单位度量参数。该二进制实际使用的表是 1/(2π),即用整圈度量参数,而象限索引从小数圈数的高位恢复。重新实现者如果加载 2/π 表,会得到错误常量和 4× 的缩放错误。
证据
首 word 是 0x28be60db = 683565275。把六个 word 视为基数 2³² 下的小数 0.w₀w₁w₂w₃w₄w₅:
0x28be60db / 2³² = 683565275 / 4294967296 = 0.15915494…
floor(2³² / (2π)) = floor(4294967296 / 6.283185…) = 683565275 = 0x28be60db ✓
```text
完整的六 word 重建 `Σᵢ wᵢ·2^(−32(i+1))` 计算为 `0.15915494309189535`,也就是到最后一个 fp64 bit 的 `1/(2π)`。它**不是** `2/π = 0.6366197…`(其首 word 会是 `0xa2f9836e`)。`π/2` 乘法在*末尾*由 float-`π/2` 有效数的 `VcomposeF32` 恢复:`x·(1/2π)` 给出圈数,其小数部分 `f ∈ [0,1)` 随后通过等价于 `2π·(π/2)` 的重建缩放回来,使得在象限折叠后 `r = f·(π/2)`。净映射仍是标准的 `r = x − k·(π/2)`,只是经由圈数表示到达。
> **注意 —** 同一个 `683565275 = 0x28be60db` 首 word 在著名的 fdlibm / glibc Payne-Hanek `two_over_pi[]` 表中作为*第二*个 word 出现(fdlibm 表把 `2/π` 展开提前一位开始)。libtpu 的展开是 `1/(2π)`,并且六个 word 自包含,足够覆盖 fp32 参数范围;它没有复用更长的 fdlibm `2/π` 数组。
---
## `1/(2π)` 常量表
### 布局
六个 32-bit word,按 MSB-first 加载(word 0 是最高有效的小数 limb)。每个都是一次不同 `VectorU32Constant` 调用的 `imm32` 操作数;`.text` 偏移是具现化它的 `mov esi, imm32` 地址(VMA == 文件偏移)。`PayneHanekRangeReduction` 第 522–527 行的反编译顺序与反汇编逐字节一致。
| word | `u32` | `.text` off (imm) | `1/(2π)` 的 bit 范围 |
|---|---|---|---|
| w0 | `0x28be60db` | `0x1d581b8a` | bits[191..160](MSB limb)= `floor(2³²/(2π))` = 683565275 |
| w1 | `0x9391054a` | `0x1d581b9e` | bits[159..128] |
| w2 | `0x7f09d5f4` | `0x1d581bb3` | bits[127..96] |
| w3 | `0x7d4d3770` | `0x1d581bc8` | bits[95..64] |
| w4 | `0x36d8a566` | `0x1d581bdd` | bits[63..32] |
| w5 | `0x4f10e410` | `0x1d581bf2` | bits[31..0](LSB limb) |
```text
1/(2π) ≈ 0. 28be60db 9391054a 7f09d5f4 7d4d3770 36d8a566 4f10e410 (base 2³², MSB→LSB)
= Σ wᵢ · 2^(−32·(i+1))
= 0.15915494309189535 (== 1/(2π), fp64-exact)区别 — 这六个 word 不是
.rodata表。六个 word 的连续 little-endian blob 不会出现在.rodata的任何位置;搜索结果为空。它们只作为PayneHanekRangeReduction的.text中的指令立即数存在。每次VectorU32Constant(immᵢ)调用都会向正在编译的 kernel 发射一个新的 LLOVectorU32Constantop,所以该表会复制进每个 trig kernel,而不是从共享常量池引用。重新实现者必须把这六个立即数硬编码到 trig-lowering 发射器中,而不是放进数据段。
重建常量
同一个 VectorU32Constant 通道承载窗口化乘法和最终 compose 的标量控制常量。它们都是同一函数内的 .text 立即数;偏移是具现化它的 mov。
u32 | 值 | 作用 | .text off |
|---|---|---|---|
0x00000020 | 32 | 每 limb 移位宽度;窗口索引 = 0x20 − exp_window(@line 528) | 0x1d581c0a |
0x0000001f | 31 | 低 5 bit 指数掩码(SimplifyAndU32,@line 509) | — |
0x00000005 | 5 | >>5,把指数拆成(limb-index, intra-limb shift)(@line 495) | — |
0x00000fff | 4095 | 12-bit 小数窗口 / 舍入掩码(@line 911) | — |
0x0000001e | 30 | 小数乘积提取的窗口 bit 数(@lines 1495,1508) | — |
0xffffffe2 | −30 | 二进制点指数偏移 → SubS32 → CreateVectorBinop(0x121)(@line 1593) | — |
0x20000000 | 2²⁹ | 小数的舍入 / half-ULP 偏置(@line 1482) | — |
0x00c90fdb | — | float π/2 的 24-bit 有效数(π/2 = 0x3fc90fdb)→ VcomposeF32(@line 1606) | — |
指数窗口拆分是这个技巧的核心:有偏指数的低 5 bit 成为 limb 内移位,exp >> 5 选择乘积窗口中跨过二进制点的 32-bit limb。0x20 − shift 是 Shll/Shrl/Or limb 拼接的互补移位,因此无论 |x| 多大,都会组装出 x · 1/(2π) 乘积中被选中的 64-bit 窗口,小数 bit 不会因抵消而丢失。
算法
窗口化乘法
乘积 x · (1/2π) 是定点整数乘小数。该归约从不形成完整的 192-bit 乘积;它只形成包围二进制点的 64-bit 窗口,该窗口由 x 的指数选择。@0x1d5819c0 处的反编译控制流是:
// PayneHanekRangeReduction(builder, x, ...) @0x1d5819c0
exp = SimplifyAddS32(x_exponent, 1) // VectorU32Constant(1) seed
exp = SimplifyMaxS32(exp, 0) // clamp negative exponents to 0
limb_idx = SimplifyShrlU32(exp, 5) // exp >> 5 → which u32 limb
shift = SimplifyAndU32(exp, 0x1f) // exp & 31 → intra-limb shift
cshift = SimplifySubS32(0x20, shift) // 32 - shift (CreateVectorBinop 0x121)
// per-limb window assembly: for each of the six 1/(2π) words wᵢ
// hi = SimplifyShrlU32(wᵢ, cshift) (CreateVectorBinop 0x19a = SHRL)
// lo = SimplifyShllU32(wᵢ, shift) (CreateVectorBinop 0x19c = SHLL)
// window_limb = SimplifyOrU32(hi, lo) (CreateVectorBinop 0x15e = OR)
// → six assembled limbs (w0..w5) realigned to the binary point
// wide-multiply path (target supports VmulU64, @line 838 capability query):
prod_lo = VmulU64(builder, x_mantissa, window_lo) // @line 855 : low product
prod_hi = VmulU64(builder, x_mantissa, window_hi) // @line 856
prod_x = VmulU32(builder, x_mantissa, window_mid) // @line 858, + AddCarryU32 @line 872
// limb-by-limb path otherwise: VshllU64High (@0x1d583ac0, lines 982/1052/1592)
// feeds the 8-iteration SimplifyMulF32/VcvtF32ToS32 accumulation loop (@lines 1194-1337)
```text
`VshllU64High`(`@0x1d583ac0`)是无符号 64×64→high-64 乘法,保留高位乘积 bit;圈数的整数部分是象限载体,小数部分成为归约后的参数。`Shrl`/`Shll`/`Or` 链(LLO binop `0x19a`/`0x19c`/`0x15e`)对每个 limb 出现一次,在乘法前把每个 `1/(2π)` limb 按指数选择的二进制点重新对齐。
> **注意 —** 该函数带有两条乘积路径,由目标能力查询门控(`@line 838`,dispatch `(*vtbl+1568)(...,8)`)。当目标支持宽乘时,窗口化乘积由两次 `VmulU64` 加一次 `VmulU32`(`@lines 855–858`)形成,并通过显式 `AddCarryU32` 传播进位(`@line 872`)。否则,小数通过 `VshllU64High`(`@lines 982, 1052, 1592`)逐 limb 组装,并经过八次迭代的 `SimplifyMulF32`/`VcvtF32ToS32` 累加循环(`@lines 1194–1337`),该循环把 float 转换后的 mantissa nibble 乘以窗口 limb。两条路径产生同一个窗口化 `x · (1/2π)` 乘积;该乘积的高/低整数与小数拆分是从周围重建过程**推断**出来的,而不是一个 opcode 标志。
### 象限索引和重建
窗口化乘法之后,整数圈数被转换为 float(EUP push 将用来选择象限的值),而小数余量用 `π/2` 有效数重新组装为 float:
```c
// integer turn-count → float (the quadrant carrier)
k_int = VcvtF32ToS32(...) // @line 1213 (round mode 0xffffffff)
k_float = SimplifyConvertS32ToF32(turns) // CreateVectorUnop 0x108
// reduced fraction → float with π/2 significand and a −30 binary point
bp_exp = SimplifySubS32(0xffffffe2, window) // -30 - window (CreateVectorBinop 0x121)
pio2 = VectorU32Constant(0x00c90fdb) // 24-bit significand of π/2
r_compose = VcomposeF32(builder, bp_exp, pio2, 0,0,0) // @0x1d555860 : assemble float r·(π/2)
frac_f = SimplifyConvertS32ToF32(frac_int) // CreateVectorUnop 0x108
r = SimplifyMulF32(frac_f, r_compose) // r = frac · (π/2) ← reduced argumentVcomposeF32(@0x1d555860)从整数指数(−30 − window,计入 30-bit 小数窗口和 limb 位置)以及固定的 π/2 有效数 0x00c90fdb 构造一个 float。最终的 VmulF32(frac, π/2-composed) 产生归约后的参数 r ∈ [−π/4, +π/4](象限折叠之后),供 EUP sinq/cosq push 消费。该函数的返回值(@line 1615-1621)就是这个 r;象限索引 k 通过 16 分支 select 网络并行传递。
象限选择网络
十六次 SimplifySelect 调用实现逐指数窗口/象限修正。函数早期(第 742–749 行)用 VcmpHelper(comparison kind 4, predicate 5 = "<=")把窗口索引与 1、2、3、4 比较,产生四个掩码;SimplifySelect 分支(第 751、759、766、773 行等)把这些掩码串起来,选择哪些组装后的 limb 送入高/低乘积,即为当前指数桶选择 1/(2π) 展开中跨过二进制点的 32-bit 窗口。所得整数圈数的低两 bit 是象限索引 k;k mod 4 选择三角恒等式:
k mod 4 sin(x) = cos(x) =
0 +sin(r) +cos(r)
1 +cos(r) −sin(r)
2 −sin(r) −cos(r)
3 −cos(r) +sin(r)
```text
> 上面的 `k mod 4 → {sign, sin↔cos swap}` 真值表是标准象限映射,也是两个低位圈数 bit 的*预期*含义,但它**不是**从 16 个 `SimplifySelect` 分支逐字节转录得到的。select 网络是按结构解码的(按指数桶选择窗口),整数到象限的绑定与 EUP `sinq`/`cosq` push 配对,而不是完全存在于这个函数内部。请把该真值表视为文档化约定,而不是二进制确认事实;符号/交换分支最可能应用在包装 EUP push 的 `Vsinq`/`Vcosq` builder 处(参见 [`slot-eup-transcendental`](../isa/slot-eup-transcendental.md))。对精确分支到象限分配的置信度**低**。
### 示例 — 为什么窗口对大 `|x|` 仍能保留
取 `x = 1e20`(二进制指数约 66)。朴素折叠会在 fp32 中计算 `x − round(x · 2/π)·(π/2)`;但 `x` 在指数 66 处没有小数 bit,而 `π/2` 只有 24 bit 精度,所以相减是 `(garbage) − (garbage)`,结果没有正确 bit。Payne-Hanek 改为以 192-bit 定点精度计算乘积 `x · (1/2π)`,并保留二进制点附近的窗口:
```text
exponent of x ≈ 66 → limb_idx = 66 >> 5 = 2, shift = 66 & 31 = 2
→ the binary point of x·(1/2π) lands inside limb w2 (bits 127..96)
window = realign(w1, w2, w3) by (shift=2, cshift=30) via Shll/Shrl/Or
→ a 64-bit slice of the 1/(2π) expansion straddling the point
prod_hi = VshllU64High(mantissa(x), window) → integer-turn bits → k (quadrant)
prod_lo = VmulU64(mantissa(x), window) → fractional-turn bits → reduced fraction f
r = VcomposeF32(exp = −30 − window, sig = 0x00c90fdb) · convert(f)小数 f ∈ [0,1) 无论 |x| 多大都携带完整的 24 个幸存有效数 bit,因为相关的 1/(2π) limb 是由 x 的指数选择,而不是被该指数截断。这正是该表宽达 192 bit 的全部原因:它必须在二进制点下方提供足够的小数 bit,使得最大 fp32 指数把二进制点一路滑到 limb w5 时,窗口化乘积仍有 24 个好 bit。
数据结构和 LLO Opcode
该归约完全由通用 LloRegionBuilder simplifier 以及 CreateVectorBinop/CreateVectorUnop op 构建,没有专用的 “PayneHanek” LLO opcode。涉及的 opcode 全部在反编译中确认:
| LLO op | helper | 含义 |
|---|---|---|
0x108 | CreateVectorUnop / SimplifyConvertS32ToF32 | s32 → f32(圈数和小数转 float) |
0x11b | CreateVectorBinop / SimplifyAddS32 | +(指数 seed,@line 473) |
0x121 | CreateVectorBinop / SimplifySubS32 | −(窗口索引和二进制点偏移) |
0x15c | CreateVectorBinop / SimplifyAndU32 | bitwise AND(指数掩码 0x1f) |
0x15e | CreateVectorBinop / SimplifyOrU32 | bitwise OR(limb 窗口拼接) |
0x19a | CreateVectorBinop / SimplifyShrlU32 | 逻辑右移(limb 重新对齐) |
0x19c | CreateVectorBinop / SimplifyShllU32 | 逻辑左移(limb 重新对齐) |
参数作为 LloValue*(float x)进入;它的 mantissa 和 exponent 由 CastTo(0x12, …)(@line 470)以及上面的指数算术分解。调用之间不保留状态,该函数是一个纯 SSA 扩展器,向当前 LloRegion 发射这些 op 的固定 DAG。
重新实现说明:立即数流 vs 常量池
对重新实现者来说,最重要的结构事实是表位于哪里。从头实现的 LLVM 型 trig lowering 很自然会把 192-bit 常量放进 .rodata 数组并用 GlobalAddress 引用。libtpu 没有这样做:六个 word 的每一个都是被 VectorU32Constant 消费的 mov esi, imm32 立即数,后者构造一个新的逐 kernel LLO 常量 op。后果很具体:
- 没有 relocation,没有常量池项。 扫描
.rodata找六 word blob 会返回空,word 只在发射器的.text中。搜索捕获 kernel 的数据段来找1/(2π)也会失败;这些常量是 LLO op,在指数静态已知时由 simplifier 折叠(SimplifyShrlU32等会在发射时常量折叠)。 - 每个 trig kernel 都复制。 由于常量是被发射而非引用,每个命中大参数路径的
sin/coslowering 都携带自己的六 word 副本。不存在可去重的共享two_over_pi符号。 - 控制常量同样内联。
0x20、0x1f、0x05、0xfff、0x1e、0xffffffe2、0x20000000、0x00c90fdb全都是同一.text窗口中的mov立即数操作数,不从内存加载。
忠实的重新实现因此必须把这八个常量硬编码进 trig-lowering pass 本身,并把它们作为 IR 常量发射,以匹配 libtpu 的“编译进来,而非加载”的模型。
函数映射
| 函数 | 地址 | 作用 |
|---|---|---|
PayneHanekRangeReduction | 0x1d5819c0 | 完整的 trig 参数归约(发射窗口化乘法 + 重建) |
LloModule::VectorU32Constant | 0x1d506400 | 将每个 imm32(六个 1/(2π) word + 控制常量)具现化为 LLO op |
VshllU64High | 0x1d583ac0 | 无符号 64×64 → high-64 乘积(逐 limb 窗口化乘法) |
LloRegionBuilder::VcomposeF32 | 0x1d555860 | 从(指数, π/2 有效数)组装归约后的参数 float |
SimplifyConvertS32ToF32 | (inline) | s32 → f32,用于圈数 k 和小数 |
LloRegionBuilder::VcmpHelper | (inline) | 向 select 供给的四个窗口比较(<= 1,2,3,4) |
VsinReduced / Vsinq / VcosqDecomposed | (trig builders) | 围绕 r 包装 EUP sinq/cosq push;应用 k 象限符号/交换 |
注意事项
该归约由 sin/cos trig lowering(Vtrigfun / TrigFlavor builder)调用,后者决定参数幅值是否值得走完整 Payne-Hanek 路径,还是走更便宜的范围内折叠;这里没有钉住该门控阈值。六 word 1/(2π) 展开足够覆盖完整 fp32 指数范围(|x| < 2¹²⁸):192-bit 小数覆盖了 24-bit fp32 有效数加 8-bit 指数滑动还更多。对于 fp64 trig 路径,该表需要加宽,但 libtpu 的 EUP trig 是 fp32/bf16,所以六个 word 是生产宽度。成本模型上的后果记录在 eup-latency-overview:归约的 ~50 个 LLO op 在 sinq/cosq push 之前在 VALU 上执行,因此 Payne-Hanek DAG 是隐藏 EUP push→pop 延迟的 VALU-correction 窗口的一部分,而不是单独的流水线阶段。
交叉引用
- EUP / 超越函数 Slot —
sinq/cosqpush 编码(VALU slot 3,function selector0x17/0x18F32,0x1e/0x1fBF16)、延迟 pop,以及象限符号/交换应用的位置 - EUP 延迟概览 — Payne-Hanek VALU DAG 如何填充 push→pop 软件流水窗口
- EUP 修正系数 — 兄弟修正多项式目录(tanh rational、Newton refinements、
*NoEupF32fallbacks) - EUP 分世代延迟整数 — trig 路径必须隐藏的分世代 push→pop 延迟边(PF 7 / VF 6 / GL 13·F32 / 14·BF16)
- 返回索引 — Part VII — Cost & Latency Model / EUP / transcendental latency