跳到主要内容
TileLang 系统教程

tilelang-tutorial.huandu.me

第 16 章 算子实战 IV:MoE 与 MLA

/code/16-operators-moe-mla/

06-ascend-map/ascend_map.py6492 字节
#!/usr/bin/env python3
"""第 16 章 · 昇腾侧的 MoE / 稀疏注意力对照(纯标准库,CPU 可跑)。

两件事:

A. **代码对照**:把昇腾侧的四个 example(fusedmoe / moe_token_permute /
   dispatch_combine / deepseek_v4)与本章的概念对齐,并核对每条引用。
B. **专家并行的通信账**:dispatch/combine 是 all-to-all。这里给出每个 token、
   每个 batch 要搬多少字节,并把「通信字节」与「激活字节」放在一起比,
   说明为什么 EP(expert parallel)场景下瓶颈会从矩阵乘挪到通信。

通信带宽**不写死**:脚本只算字节量,时间 = 字节 / 带宽。任何一个具体带宽都是
占位示例,不是任何设备的实测规格。

用法
----

    python3 ascend_map.py
    python3 ascend_map.py --tokens 8192 --top-k 8 --d-model 7168
"""

from __future__ import annotations

import argparse
from pathlib import Path

# (文件, 行号, 该行必须包含的 token, 说明)
ASCEND: list[tuple[str, int, str, str]] = [
    ("examples/fusedmoe/example_fusedmoe.py", 30, "TL_ASCEND_AUTO_CV_COMBINE",
     "combineCV + auto sync:把 Cube/Vector 合流并自动插同步"),
    ("examples/fusedmoe/example_fusedmoe.py", 89, "def dual_gemm_kernel",
     "gate/up 融成一个 kernel,input 只读一次"),
    ("examples/fusedmoe/example_fusedmoe.py", 148, "block_metadata",
     "grouped GEMM 用 [expert, m_start, valid_m] 路由"),
    ("examples/fusedmoe/example_fusedmoe.py", 230, "L0C = 2*(64*256*4) = 128KB",
     "官方注释给出片上容量账(L0C 打满)"),
    ("examples/moe_token_permute/moe_token_permute.py", 79, "T.alloc_ub",
     "token permute 在 UB 上做直方图/前缀和"),
    ("examples/moe_token_permute/moe_token_permute.py", 38, "workspace_idx",
     "permute 需要一块 workspace 做跨核前缀和"),
    ("examples/dispatch_combine/dispatch_combine_shmem.py", 148,
     "T.shmem_ub_put_nbi", "dispatch:跨 rank 的 non-blocking put"),
    ("examples/dispatch_combine/dispatch_combine_shmem.py", 68,
     "ep_receive_count", "每个专家收到多少 token 的前缀和"),
    ("examples/dispatch_combine/dispatch_combine_shmem.py", 254,
     "expert_token_nums_out", "用前缀和差分得到每个专家最终的 token 数"),
    ("examples/deepseek_v4/lightning_indexer.py", 575, "T.alloc_L1",
     "V4 的 indexer 在昇腾上用 L1 缓冲"),
    ("examples/deepseek_v4/sparse_flash_mla/sparse_flash_mla_kernel.py", 1199,
     "cmp_indices", "V4 sparse flash MLA:压缩 KV 由 top-k 索引选中"),
    ("examples/deepseek_v4/sparse_flash_mla/bench_mark.md", 1, "Sparse flash MLA",
     "V4 sparse flash MLA 的官方 benchmark 说明(含 swa/hca/csa 三种访问模式)"),
]


def check(book: Path) -> tuple[int, int]:
    base = book / "_vendor" / "tilelang-ascend-v0.1.2.000"
    if not base.is_dir():
        print("[skip] _vendor/tilelang-ascend-v0.1.2.000 不存在")
        return 0, 0
    ok = bad = 0
    for rel, line, token, why in ASCEND:
        path = base / rel
        if not path.is_file():
            print(f"  MISSING FILE {rel}")
            bad += 1
            continue
        lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()
        if 1 <= line <= len(lines) and token in lines[line - 1]:
            print(f"  ok    {rel}:{line}   {why}")
            ok += 1
        else:
            got = lines[line - 1].strip() if 1 <= line <= len(lines) else "<越界>"
            print(f"  FAIL  {rel}:{line}  期望含 {token!r},实际 {got!r}")
            bad += 1
    return ok, bad


def comm_model(tokens: int, top_k: int, d_model: int, b: int,
               ranks: int, local_experts: int) -> None:
    print("\n" + "=" * 78)
    print("B. 专家并行(EP)的通信账")
    print("=" * 78)
    print(f"\ntokens={tokens} top_k={top_k} d_model={d_model} "
          f"每元素 {b} 字节  EP ranks={ranks} local_experts={local_experts}")
    per_token_per_way = top_k * d_model * b
    dispatch = tokens * per_token_per_way
    combine = dispatch
    total = dispatch + combine
    print(f"\n  每个 token 每个方向: k*d*b = {per_token_per_way} 字节 "
          f"= {per_token_per_way / 1024:.1f} KB")
    print(f"  dispatch 总量 : {dispatch / 1e6:.1f} MB")
    print(f"  combine  总量 : {combine / 1e6:.1f} MB")
    print(f"  合计          : {total / 1e6:.1f} MB(一层,单方向全量)")

    # 与「激活」比:一层里 token 激活本身只有 tokens*d 个元素。
    activation = tokens * d_model * b
    print(f"\n  对照:这一层的激活(tokens*d*b)只有 {activation / 1e6:.1f} MB;")
    print(f"  也就是说,EP 的 dispatch+combine 要搬 {total / activation:.1f} x 的激活量。")

    # 与专家的权重流量比:本地专家权重 = local_experts * 3*d*de*b。
    # 这里 d_expert 未知,用比值的形式给出,避免臆造参数。
    print("\n  再把互联带宽 BW 代进去:通信时间 = 字节 / BW。")
    print("  (下面带宽只是**占位示例**,不是任何设备的实测规格。)")
    print(f"\n  {'BW (GB/s)':>12} | {'dispatch 时间':>16} | {'combine 时间':>16}")
    for bw_gbps in (50, 100, 200, 400):
        bw = bw_gbps * 1e9
        print(f"  {bw_gbps:>12} | {dispatch / bw * 1e6:>14.1f} us | "
              f"{combine / bw * 1e6:>14.1f} us")
    print("\n  读法:把你们集群的实际互联带宽代进去,就能判断一层 MoE 里")
    print("  「通信」和「专家 GEMM」谁更长。跨机 EP 时前者常常更大。")
    print("  注意:这里的字节量是**单方向全量**的下界,未计协议开销、")
    print("  状态同步、以及 dispatch 里那份 3 元组 (rank, token, topk_id)。")


def pipeline_map() -> None:
    print("\n" + "=" * 78)
    print("A. 昇腾侧 MoE 一层的数据流(对照官方 example)")
    print("=" * 78)
    steps = [
        ("1 gating", "host 或小 kernel", "softmax + topk + 计数(host_preprocess)"),
        ("2 dispatch", "all-to-all", "把 token 发到专家所在 rank(shmem put)"),
        ("3 permute", "UB 直方图/前缀和", "把收到的 token 按专家紧凑排列"),
        ("4 gate/up GEMM", "grouped dual GEMM", "input 读一次,出 gate 与 up 两个结果"),
        ("5 SiLU + mul", "PyTorch 或融合", "up * silu(gate)"),
        ("6 down GEMM", "grouped GEMM", "de -> d_model"),
        ("7 写回 + 加权", "elementwise", "乘 gating 权重,写回 token 布局"),
        ("8 combine", "all-to-all", "把结果送回原 rank 并按 token 求和"),
    ]
    for name, where, what in steps:
        print(f"  {name:<16} | {where:<22} | {what}")
    print("\n  昇腾侧的三个关键点(都能在源码里指到行):")
    print("   * 累加器在 L0C、输入/权重在 L1,容量要手算(官方注释直接给出 L0C=128KB);")
    print("   * combineCV + auto sync 把 Cube/Vector 合流并自动插同步,省掉手写 barrier;")
    print("   * 跨 rank 用 shmem 的 put/get,dispatch/combine 在源码里是一对 kernel。")


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--tokens", type=int, default=8192)
    parser.add_argument("--top-k", type=int, default=8)
    parser.add_argument("--d-model", type=int, default=7168)
    parser.add_argument("--bytes", type=int, default=2)
    parser.add_argument("--ranks", type=int, default=16)
    parser.add_argument("--local-experts", type=int, default=3)
    args = parser.parse_args()

    book = Path(__file__).resolve().parents[4]
    print(f"教材根目录: {book}")
    print("\n引用核对(tilelang-ascend v0.1.2.000)")
    ok, bad = check(book)
    print(f"  命中 {ok} 处,未命中 {bad} 处。")
    pipeline_map()
    comm_model(args.tokens, args.top_k, args.d_model, args.bytes,
               args.ranks, args.local_experts)
    return 1 if bad else 0


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

← 浏览本章全部代码