跳到主要内容
TileLang 系统教程

tilelang-tutorial.huandu.me

第 18 章 端到端 II:推理与部署

/code/18-e2e-inference-serving/

07-tilelang-moe-and-quant/moe_dispatch_contract.py7325 字节
#!/usr/bin/env python3
"""第 18 章 · MoE 推理的 dispatch/combine 契约(纯标准库,CPU 可跑)。

MoE 推理里最容易出错、也最容易被教程忽略的一段,是「路由结果」到
「grouped GEMM 的行区间」之间的映射。锁定版本的
`_vendor/tilelang-v0.1.15/examples/fusedmoe/example_fusedmoe_tilelang.py`
把它写成四个张量:

============================  ==========================================
`group_sizes`                 每个专家分到多少个 (token, expert) 对
`group_offsets`               每个专家的行区间在**紧凑**的 token 数组里的起点
`group_padded_offsets`        每个专家的行区间在**对齐后**的数组里的起点
`group_idx_for_bx`            第 bx 个 block 该去处理哪个专家
============================  ==========================================

本脚本用一个小规模例子把这份契约复算一遍,并断言三件事:

1. 每个 (token, expert) 对恰好出现一次(既不漏也不重);
2. 每个专家的「对齐后区间」完整覆盖它的「紧凑区间」;
3. 内核里那一行 `m_start = m_start_padded - group_padded_offsets[g]
   + group_offsets[g]` 能正确还原出紧凑数组里的行号。

**这不是在跑 kernel**,是在核对 host 侧那张表与 kernel 侧寻址公式是否自洽。

用法
----

    python3 moe_dispatch_contract.py
    python3 moe_dispatch_contract.py --tokens 17 --experts 5 --top-k 3
"""

from __future__ import annotations

import argparse
import math
import random


def ceil_div(a: int, b: int) -> int:
    return -(-a // b)


def build_contract(token_ids: list[int], expert_of: list[list[int]],
                   n_experts: int, padding_m: int, block_token: int) -> dict:
    """复刻 `example_fusedmoe_tilelang.py` 的 host 侧准备逻辑。

    对应源码:`example_fusedmoe_tilelang.py:344-382`
    (`idxs = flat_expert_indices.argsort()` 到 `group_idx_for_bx`)。
    """
    # 1) 每个 token 选 top_k 个专家,展平
    flat_expert: list[int] = []
    flat_token: list[int] = []
    for i, tok in enumerate(token_ids):
        for e in expert_of[i]:
            flat_expert.append(e)
            flat_token.append(tok)
    group_sum = len(flat_expert)

    # 2) 按专家排序:同一个专家的行在数组里连续
    order = sorted(range(group_sum), key=lambda k: flat_expert[k])
    counts = [0] * n_experts
    for e in flat_expert:
        counts[e] += 1

    group_offsets: list[int] = []
    acc = 0
    for c in counts:
        group_offsets.append(acc)
        acc += c

    # 3) 对齐后的偏移;源码里是 ceil((count + 1) / padding_M) * padding_M
    #    那个 +1 是给每个专家留一行余量(见 sources: :364-366)
    group_padded_offsets: list[int] = [0]
    for i in range(1, n_experts):
        prev = math.ceil((counts[i - 1] + 1) / padding_m) * padding_m
        group_padded_offsets.append(group_padded_offsets[i - 1] + prev)

    # 4) 第 bx 个 block 属于哪个专家
    m_blocks = ceil_div(group_sum, block_token) + n_experts
    group_idx_for_bx = []
    for bx in range(m_blocks):
        start = bx * block_token
        g = 0
        for i in range(n_experts):
            if start >= group_padded_offsets[i]:
                g = i
        group_idx_for_bx.append(g)

    return {
        "flat_expert": flat_expert,
        "flat_token": flat_token,
        "order": order,
        "counts": counts,
        "group_sizes": counts,
        "group_offsets": group_offsets,
        "group_padded_offsets": group_padded_offsets,
        "m_blocks": m_blocks,
        "group_idx_for_bx": group_idx_for_bx,
        "group_sum": group_sum,
    }


def check_coverage(contract: dict) -> None:
    """每个 (token, expert) 对恰好出现一次。"""
    seen = list(zip(contract["flat_token"], contract["flat_expert"]))
    assert len(seen) == len(set(seen)), "同一个 (token, expert) 对出现了两次"
    assert len(seen) == contract["group_sum"]
    total = sum(contract["counts"])
    assert total == contract["group_sum"], f"{total} != {contract['group_sum']}"
    print(f"  [ok] {contract['group_sum']} 个 (token, expert) 对恰好覆盖一次")


def check_padded_covers_compact(contract: dict, padding_m: int) -> None:
    """对齐后的区间必须完整覆盖紧凑区间。"""
    for i, (off, padded) in enumerate(zip(contract["group_offsets"],
                                          contract["group_padded_offsets"])):
        need = math.ceil((contract["counts"][i] + 1) / padding_m) * padding_m
        assert padded + need >= off + contract["counts"][i], (
            f"专家 {i}: 对齐后区间 [区{padded}, {padded + need}) "
            f"盖不住紧凑区间 [{off}, {off + contract['counts'][i]})")
    print(f"  [ok] 每个专家的对齐区间都盖住了它的紧凑区间(padding_M={padding_m})")


def check_kernel_addressing(contract: dict, block_token: int) -> None:
    """复算内核里的 `m_start` / `actual_rows` 公式。"""
    sizes = contract["group_sizes"]
    offsets = contract["group_offsets"]
    padded = contract["group_padded_offsets"]
    checked = 0
    for bx, g in enumerate(contract["group_idx_for_bx"]):
        m_start_padded = bx * block_token
        if m_start_padded < padded[g]:
            continue
        m_start = m_start_padded - padded[g] + offsets[g]
        actual_rows = max(0, min(block_token, sizes[g] - (m_start_padded - padded[g])))
        # 落在有效行里的位置必须能被内核寻址到
        if actual_rows > 0:
            assert m_start >= offsets[g]
            assert m_start + actual_rows <= offsets[g] + sizes[g]
        checked += 1
    print(f"  [ok] 复算了 {checked} 个 block 的 m_start / actual_rows 公式,"
          f"没有越界")


def print_contract(contract: dict, padding_m: int, tokens: int,
                   n_experts: int, top_k: int) -> None:
    print("=" * 78)
    print(f"例子:{tokens} 个 token × top_k={top_k} → {contract['group_sum']} 个"
          f"(token, expert) 对,{n_experts} 个专家")
    print("=" * 78)
    print(f"{'expert':>6} | {'group_size':>10} | {'group_offset':>12} | "
          f"{'padded_offset':>13} | {'padded 区间长度':>15}")
    for i in range(n_experts):
        need = math.ceil((contract['counts'][i] + 1) / padding_m) * padding_m
        print(f"{i:>6} | {contract['counts'][i]:>10} | "
              f"{contract['group_offsets'][i]:>12} | "
              f"{contract['group_padded_offsets'][i]:>13} | {need:>15}")
    print(f"\n  紧凑数组长度(group_sum)        : {contract['group_sum']}")
    print(f"  对齐后需要的 block 数(M)       : {contract['m_blocks']}"
          f" = ceil({contract['group_sum']}/{128}) + {n_experts}")
    print(f"  对齐浪费的行数                   : "
          f"{contract['m_blocks'] * 128 - contract['group_sum']}")
    print(f"\n  group_idx_for_bx(前 24 个): "
          f"{contract['group_idx_for_bx'][:24]}")


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--tokens", type=int, default=9)
    parser.add_argument("--experts", type=int, default=4)
    parser.add_argument("--top-k", type=int, default=2)
    parser.add_argument("--padding-m", type=int, default=128)
    parser.add_argument("--block-token", type=int, default=128)
    parser.add_argument("--seed", type=int, default=7)
    args = parser.parse_args()

    rng = random.Random(args.seed)
    token_ids = list(range(args.tokens))
    expert_of = [rng.sample(range(args.experts), args.top_k)
                 for _ in token_ids]

    contract = build_contract(token_ids, expert_of, args.experts,
                              args.padding_m, args.block_token)
    print_contract(contract, args.padding_m, args.tokens, args.experts,
                   args.top_k)
    print("\n三项自检:")
    check_coverage(contract)
    check_padded_covers_compact(contract, args.padding_m)
    check_kernel_addressing(contract, args.block_token)

    print("\n读法:`group_padded_offsets` 是「为了不让一个 block 跨两个专家」付的"
          "\n      对齐代价。token 数少、专家多的时候,这个代价可以远大于有效计算"
          "\n      ——这正是第 16 章说的「小 M 让专家 GEMM 退化成 memory-bound」。")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

← 浏览本章全部代码