跳到主要内容
TileLang 系统教程

tilelang-tutorial.huandu.me

第 34 章 TileKernels(下):Engram 与流形超连接

/code/34-tilekernels-engram-mhc/

02-sinkhorn-iterations/README.md3372 字节
# 示例 02:两处 Sinkhorn——同一个词,两件不同的事

## 目的

TileKernels 里有**两个**叫 Sinkhorn 的算子族,很容易混为一谈:

* `mhc/sinkhorn_*`:对每个 token 的 `4 × 4` mix 矩阵先做 **row softmax**,
  再交替做**行和 / 列和归一化**,把它推向**双随机矩阵**(doubly stochastic)。
* `engram/sinkhorn_*`:对一张 `(num_rows, head_dim)` 的大矩阵交替做 **L2 行缩放 /
  列缩放**,并被拆成 `momentum → step → step_reduce → finalize` 四个 kernel。

脚本把两个过程都在 CPU 上跑一遍,好让读者看出「迭代结构」「归一化用什么范数」
「有没有 softmax」「状态大小」这四点上的差别。

## 运行前提

* Python 3.9+、PyTorch(CPU 即可)。

## 运行命令

```bash
python3 sinkhorn_iterations.py
```

## 期望输出

在写作机上的**实际输出**(节选;完整输出见运行结果):

```text
== mHC Sinkhorn: 4 x 4, softmax then row/col normalisation ==
  repeat   max|row_sum-1|  max|col_sum-1|  max|X_k - X_{k-1}|
       1        4.563e-01       1.729e-06                 nan
       3        1.539e-01       1.192e-06           1.518e-01
      10        5.696e-03       1.073e-06           6.599e-02
      30        1.431e-06       1.073e-06           2.646e-03

  the row softmax is what keeps large logits finite:
  offset   +100: finite=True  max|row-1|=1.214e-04
  offset   -100: finite=True  max|row-1|=1.214e-04

== Engram Sinkhorn: alternating L2 row / column scaling ==
  matrix (3, 4) fp32; row_scale (3,); col_scale (4,)
  iterate = row_scale[:, None] * matrix * col_scale
  step   max|row L2 - 1| after row step        col L2 before col step
     0                      5.960e-08   [1.02401, 0.48524, 1.10937, 0.6966]
     2                      1.192e-07   [0.85121, 0.85212, 0.91841, 0.84014]
     7                      1.192e-07   [0.86589, 0.86586, 0.86646, 0.8659]
  after a few rounds the row / column scale profiles stop changing:
    row L2 before finalize: [1.15488, 1.15442, 1.1548]
    row L2 after  finalize (alpha = sqrt(head_dim) = 2.0): [2.0, 2.0, 2.0]
```

## 怎么读这些数字

* **mHC 版**:`row softmax` 那一步让行和恒为 1(列和还有偏差),随后每多迭代一轮,
  行列和就同时更接近 1。`repeat = 10` 时最大偏差约 `5.7e-3`;`repeat = 30` 时到
  `1.4e-6`。上表里的 `max|col_sum-1| ≈ 1.1e-6` 并不是误差没收敛,而是 `eps = 1e-6`
  本身留下的固定偏移——kernel 里每一步都是除以 `(sum + eps)`(
  `../tilelang-corpus/14-deepseek-ascend-stack/raw/TileKernels/tile_kernels/mhc/sinkhorn_cuda.py:39,43,48,52`)。**这就是为什么 kernel 里的 `repeat`
  是个可调参数**:它决定「快」与「准」的取舍。
  另外要注意口径:**论文用的迭代次数是 `t_max = 20`**
  (`../tilelang-corpus/14-deepseek-ascend-stack/raw/paper-deepseek-mhc.md:405-406`),
  而本脚本与仓库测试用的是 `repeat = 10`;两者的「一次迭代」定义并不相同
  (论文一次 = 行 + 列,代码从 softmax 后的列归一化开始计数)。
  要做论文口径的对照,先读正文 §5.2 那张「论文 vs 源码」表。
* **offset ±100 两行**:即使把整个 4×4 矩阵平移到 ±100,结果仍然有限。原因是
  **先做行 softmax**(内部减掉行最大值),这与你熟悉的 online softmax 稳定性
  是同一个道理。
* **Engram 版**:`iterate = row_scale ⊗ matrix ⊗ col_scale` 是「矩阵外积缩放」,
  归一化用的是 **L2 范数**而不是和。每做一次行步,行 L2 变成 1;再做一次列步,
  列 L2 变成 1,行 L2 又偏离 1。交替几轮后行/列的缩放分布稳定下来(互为倒数),
  最后由 `finalize` 用一个 `alpha` 把全局因子定死——所以你会看到「finalize 之后
  每一行的 L2 范数正好等于 `alpha = sqrt(head_dim)`」。

## 作者实际做到的验证级别

**CPU 逻辑验证**:在 macOS (Apple Silicon)、Python 3.11、torch 2.1.1 上实际运行过。
脚本里的两个 `*_ref` 函数是从 corpus 的
`tile_kernels/torch/mhc.py:8` 与 `tile_kernels/torch/engram.py:39,66` **原样抄来**
的,所以这里的数字就是仓库自带 CPU 基线的行为。**没有跑过 kernel。**

## 已知局限

* 小矩阵(`3 × 4`、`2 × 4 × 4`)不代表真实规模。mHC 的真实矩阵是每个 token 一个
  `4 × 4`(`hc = 4`);Engram 的矩阵在测试里是 `10⁵ × 5120` 量级
  (`tests/engram/test_engram_sinkhorn.py:98`)。
* L2 交替缩放的收敛速度与矩阵本身有关;脚本只展示「它会收敛」和「`finalize` 做什么」,
  不给收敛率。
* Engram 版在真实训练里挂在哪个参数上(哪一步用哪个 kernel 组合),TileKernels
  仓库里没有调用方,本章只能给出**源码 → 行为推断**,详见正文 §5.3 与
  `../references.md` 的「待补资料」。

← 浏览本章全部代码