Skip to content

EUP Payne-Hanek 范围归约

本页所有地址都适用于 libtpu-0.0.40-cp314 wheel 中的 libtpu.so(构建 libtpu_lts_20260413_b_RC00,BuildID md5 89edbbe81c5b328a958fe628a9f2207d)。该二进制去符号化,下文每个符号都是反混淆后的 C++ 名称。段映射:.text/.rodata VMA == 文件偏移;.data.rel.ro VMA − 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。
  • 窗口化乘法序列。 指数 → 窗口索引(对 0x20SubS32)、逐 word 的 Shrl/Shll/Or 窗口组装,以及 VshllU64High@0x1d583ac0)+ VmulU64 64-bit 高位乘积提取,只保留相关小数 bit。
  • 象限索引 k 整数圈数的 VcvtF32ToS32/SimplifyConvertS32ToF32(LLO unop 0x108),以及选择逐指数窗口的 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)→ SimplifySubS32CreateVectorBinop(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₅

text
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 发射一个新的 LLO VectorU32Constant op,所以该表会复制进每个 trig kernel,而不是从共享常量池引用。重新实现者必须把这六个立即数硬编码到 trig-lowering 发射器中,而不是放进数据段。

重建常量

同一个 VectorU32Constant 通道承载窗口化乘法和最终 compose 的标量控制常量。它们都是同一函数内的 .text 立即数;偏移是具现化它的 mov

u32作用.text off
0x0000002032每 limb 移位宽度;窗口索引 = 0x20 − exp_window@line 5280x1d581c0a
0x0000001f31低 5 bit 指数掩码(SimplifyAndU32@line 509
0x000000055>>5,把指数拆成(limb-index, intra-limb shift)(@line 495
0x00000fff409512-bit 小数窗口 / 舍入掩码(@line 911
0x0000001e30小数乘积提取的窗口 bit 数(@lines 1495,1508
0xffffffe2−30二进制点指数偏移 → SubS32CreateVectorBinop(0x121)@line 1593
0x200000002²⁹小数的舍入 / half-ULP 偏置(@line 1482
0x00c90fdbfloat π/2 的 24-bit 有效数(π/2 = 0x3fc90fdb)→ VcomposeF32@line 1606

指数窗口拆分是这个技巧的核心:有偏指数的低 5 bit 成为 limb 内移位,exp >> 5 选择乘积窗口中跨过二进制点的 32-bit limb。0x20 − shiftShll/Shrl/Or limb 拼接的互补移位,因此无论 |x| 多大,都会组装出 x · 1/(2π) 乘积中被选中的 64-bit 窗口,小数 bit 不会因抵消而丢失。


算法

窗口化乘法

乘积 x · (1/2π) 是定点整数乘小数。该归约从不形成完整的 192-bit 乘积;它只形成包围二进制点的 64-bit 窗口,该窗口由 x 的指数选择。@0x1d5819c0 处的反编译控制流是:

c
// 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 855858`)形成,并通过显式 `AddCarryU32` 传播进位(`@line 872`)。否则,小数通过 `VshllU64High`(`@lines 982, 1052, 1592`)逐 limb 组装,并经过八次迭代的 `SimplifyMulF32`/`VcvtF32ToS32` 累加循环(`@lines 11941337`),该循环把 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 argument

VcomposeF32@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 = "<=")把窗口索引与 1234 比较,产生四个掩码;SimplifySelect 分支(第 751、759、766、773 行等)把这些掩码串起来,选择哪些组装后的 limb 送入高/低乘积,即为当前指数桶选择 1/(2π) 展开中跨过二进制点的 32-bit 窗口。所得整数圈数的低两 bit 是象限索引 kk mod 4 选择三角恒等式:

text
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 ophelper含义
0x108CreateVectorUnop / SimplifyConvertS32ToF32s32 → f32(圈数和小数转 float)
0x11bCreateVectorBinop / SimplifyAddS32+(指数 seed,@line 473
0x121CreateVectorBinop / SimplifySubS32(窗口索引和二进制点偏移)
0x15cCreateVectorBinop / SimplifyAndU32bitwise AND(指数掩码 0x1f
0x15eCreateVectorBinop / SimplifyOrU32bitwise OR(limb 窗口拼接)
0x19aCreateVectorBinop / SimplifyShrlU32逻辑右移(limb 重新对齐)
0x19cCreateVectorBinop / 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/cos lowering 都携带自己的六 word 副本。不存在可去重的共享 two_over_pi 符号。
  • 控制常量同样内联。 0x200x1f0x050xfff0x1e0xffffffe20x200000000x00c90fdb 全都是同一 .text 窗口中的 mov 立即数操作数,不从内存加载。

忠实的重新实现因此必须把这八个常量硬编码进 trig-lowering pass 本身,并把它们作为 IR 常量发射,以匹配 libtpu 的“编译进来,而非加载”的模型。


函数映射

函数地址作用
PayneHanekRangeReduction0x1d5819c0完整的 trig 参数归约(发射窗口化乘法 + 重建)
LloModule::VectorU32Constant0x1d506400将每个 imm32(六个 1/(2π) word + 控制常量)具现化为 LLO op
VshllU64High0x1d583ac0无符号 64×64 → high-64 乘积(逐 limb 窗口化乘法)
LloRegionBuilder::VcomposeF320x1d555860从(指数, π/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 / 超越函数 Slotsinq/cosq push 编码(VALU slot 3,function selector 0x17/0x18 F32,0x1e/0x1f BF16)、延迟 pop,以及象限符号/交换应用的位置
  • EUP 延迟概览 — Payne-Hanek VALU DAG 如何填充 push→pop 软件流水窗口
  • EUP 修正系数 — 兄弟修正多项式目录(tanh rational、Newton refinements、*NoEupF32 fallbacks)
  • EUP 分世代延迟整数 — trig 路径必须隐藏的分世代 push→pop 延迟边(PF 7 / VF 6 / GL 13·F32 / 14·BF16)
  • 返回索引 — Part VII — Cost & Latency Model / EUP / transcendental latency