跳到主要内容
TileLang 系统教程

tilelang-tutorial.huandu.me

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

/code/34-tilekernels-engram-mhc/

04-engram-hash-lookup/README.md3347 字节
# 示例 04:Engram 的 n-gram 哈希与 O(1) 查表

## 目的

Engram 论文说它的查表是 `O(1)`,而且因为寻址是**确定性的**,把上百亿参数的表
卸载到 host memory 也「开销可忽略」。这个脚本把这两句话落到可复算的代码上:

* 复现 `engram_hash_ref`(`../tilelang-corpus/14-deepseek-ascend-stack/raw/TileKernels/tile_kernels/torch/engram.py:118`)与内核的
  乘法-XOR 哈希(`../tilelang-corpus/14-deepseek-ascend-stack/raw/TileKernels/tile_kernels/engram/engram_hash_cuda.py:54-63`);
* 证明「同一串 token id → 同一组 index」(确定性);
* 说明「每个 token 取的行数是架构常量,与表大小无关」;
* 展示 image token 写 `-1` 这个「不查表」标记。

## 运行前提

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

## 运行命令

```bash
python3 engram_hash_lookup.py
```

## 期望输出

在写作机上的**实际输出**(index 数值是随机种子固定的,可以复现):

```text
== shapes ==
  ngram_token_ids     (6, 3) int32
  multipliers         (2, 3) int64
  vocab_sizes         (2, 2, 8) int32 (prime, per head)
  offsets             (2, 16) int32 (exclusive prefix sum)
  indices             (2, 6, 16) int32
    = num_layers x num_tokens x ((max_ngram-1) * heads) = 2 x 6 x 16
  index range: min=17329 max=2091973 (table spans 0 .. 4201439)

== deterministic addressing ==
  rerun on the same ids: identical = True
  layer 0, token 0 indices: [29861, 217345, 281072, 397629, ...]

== the number of gathered rows is set by the architecture ==
  table with   258,000 slots (~     3.9 MiB at 4 fp32 dims): rows gathered per token = 16
  table with 10,000,000 slots (~   152.6 MiB at 4 fp32 dims): rows gathered per token = 16
  => a 40x larger table costs the same number of lookups; ...

== image tokens: the kernel writes -1 ==
  mask                 [True, False, False, True, False, False]
  layer 0, token 0     [-1, -1, ...]  (all -1, no lookup)
  layer 0, token 1     [56815, 191622, 297290, ...]  (real indices)

== the actual lookup is a plain gather ==
  table (4201440, 4); gathered (16, 4) -> concat into the memory vector e_t
  e_t per token = 16 rows x 4 dims = 64 values
```

## 怎么读这个结果

* **确定性**:`indices` 只依赖 token id。真实实现里 `multipliers`、`vocab_sizes`
  (都是素数)与 `offsets` 是**权重之外**的固定超参,训练时不更新(见
  `../tilelang-corpus/14-deepseek-ascend-stack/raw/TileKernels/tile_kernels/engram/engram_hash_kernel.py:14-27` 的参数说明)。所以**前向还没开始**,
  系统就知道要读哪些行——这是「提前预取、把通信藏在上一层计算后面」的前提。
* **`O(1)` 取行数**:每个 token 每层取 `(max_ngram - 1) × heads` 行。脚本里是
  `2 × 8 = 16` 行。把表从 25.8 万槽放大到 1000 万槽(40 倍),取行数不变。
  论文的 `O(1) lookup` 说的就是这个(`../tilelang-corpus/14-deepseek-ascend-stack/raw/paper-deepseek-engram.md:27,71`)。
* **`-1` 标记**:图像 token 不做 Engram 查表,kernel 直接把整行输出写成 `-1`
  (`../tilelang-corpus/14-deepseek-ascend-stack/raw/TileKernels/tile_kernels/engram/engram_hash_cuda.py:45-47`),后续的 gather 与 gate 会跳过它。
  这正是正文 §3 里门控 kernel 需要 `image_token_mask` 的原因。
* **真正的查表**就是一次 gather:`table[indices]`。脚本给的表是 fp32 的玩具表;
  真实表的「大」体现在**容量**上(论文里到 130 亿参数),而不是每次访问的行数上。

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

**CPU 逻辑验证**:在 macOS (Apple Silicon)、Python 3.11、torch 2.1.1 上实际运行过。
哈希与 offsets 的实现与 corpus 的 `engram_hash_ref`、`make_offsets` 一致;
「确定性」「取行数与表大小无关」「`-1` 标记」三条结论都可以在这个脚本里直接看到。
**没有跑过 kernel,也没有跑过论文的模型。**

## 已知局限

* 论文里 `φ` 是「lightweight multiplicative-XOR hash」,本脚本按仓库的实现复现;
  论文的 appendix C 关于 tokenizer 压缩(词表缩小 23%)的细节没有在这里复现
  (那需要真实 tokenizer,见 `../tilelang-corpus/14-deepseek-ascend-stack/raw/Engram/engram_demo_v1.py` 的
  `CompressedTokenizer`)。
* 「把表放到 host memory、PCIe 预取的开销 < 3%」是**论文自报**的实验结论
  (`../tilelang-corpus/14-deepseek-ascend-stack/raw/paper-deepseek-engram.md:95-99`),本机无法复现,脚本也不涉及。

← 浏览本章全部代码