跳到主要内容
TileLang 系统教程

tilelang-tutorial.huandu.me

第 35 章 DeepGEMM-Ascend:把 GEMM 压到硬件极限

/code/35-deepgemm-ascend/

04-hc-prenorm-tilelang/hc_prenorm_sketch.py12231 字节
"""hc_prenorm 的 TileLang 骨架(提炼自 DeepGEMM-Ascend,仅静态核对)。

原文件:DeepGEMM-Ascend/deep_gemm/tilelang_ops/hc_prenorm.py
          (0.1.0 快照,commit 8491bbb4b8c0)

算法(原文件 :367-377):
    D       = float(A) @ B.T                 A:[M,K] bf16, B:[N,K] fp32, D:[M,N] fp32
    sqr_sum = sum(float(A) ** 2, dim=-1)     sqr_sum:[M] fp32

本文件把「结构」抽出来加注释,**不是可运行的替代品**;真机请调用
`deep_gemm.tf32_hc_prenorm_gemm`。验证级别:仅静态核对(见同目录 README)。
"""

import math

import torch
import tilelang
from tilelang.ascend import language as T
from tilelang.layout import make_ascend_compact_nz_layout

from deep_gemm import _C  # 只为拿 get_num_sms(),原文件 :26 用的也是这个接口


# ---------------------------------------------------------------------------
# 1) get_config:三个「2 的幂」约束取最小,决定 K 方向切几份
#    原文件 hc_prenorm.py:13-57
# ---------------------------------------------------------------------------
def get_config(mhc_hidden_size: int, mhc_mult3: int, num_tokens: int) -> dict:
    block_num_tokens = 128
    block_hidden = 256
    l0b_size_bytes = 64 * 1024
    l0c_size_bytes = 256 * 1024            # Ascend 950 口径(common.hpp:57)

    assert 0 < mhc_mult3 <= 64 and mhc_mult3 % 8 == 0
    assert mhc_hidden_size > 0 and mhc_hidden_size % block_hidden == 0

    num_aic_cores = _C.get_num_sms()
    block_mhc_mult3 = (mhc_mult3 + 15) // 16 * 16   # 物理对齐到 16、逻辑仍是 mhc_mult3
    num_hidden_blocks = mhc_hidden_size // block_hidden
    num_token_blocks = math.ceil(num_tokens / block_num_tokens)
    num_l0b_stages = l0b_size_bytes // (block_mhc_mult3 * block_hidden * 4)
    num_l0c_stages = l0c_size_bytes // (block_num_tokens * block_mhc_mult3 * 4)

    # 「x & -x」取出一个数里最大的 2 的幂因子,保证切分不产生尾块。
    split_limit = min(
        max(1, num_aic_cores * num_l0c_stages // num_token_blocks),  # token 并行度够
        num_hidden_blocks & -num_hidden_blocks,                      # 整除 K 方向块数
        num_aic_cores & -num_aic_cores,                              # 整除核数
    )
    num_k_splits = 1 << (split_limit.bit_length() - 1)
    active_aic_cores = num_k_splits * min(num_aic_cores // num_k_splits, num_token_blocks)
    cores_per_split = active_aic_cores // num_k_splits
    return {
        'block_mhc_mult3': block_mhc_mult3,
        'num_aic_cores': active_aic_cores,
        'num_k_splits': num_k_splits,
        'cores_per_split': cores_per_split,
        'num_hidden_blocks_per_split': num_hidden_blocks // num_k_splits,
        'num_l0b_stages': num_l0b_stages,
        'num_l0c_stages': num_l0c_stages,
        'num_token_blocks_per_chunk': cores_per_split * num_l0c_stages,
    }


# ---------------------------------------------------------------------------
# 2) kernel 骨架
#    原文件 hc_prenorm.py:59-359
# ---------------------------------------------------------------------------
@tilelang.jit
def hc_prenorm_sketch_kernel(
    x, fn, out_mul, sqrsum,
    block_mhc_mult3: int, num_aic_cores: int, num_k_splits: int,
    cores_per_split: int, num_hidden_blocks_per_split: int,
    num_l0b_stages: int, num_l0c_stages: int, num_token_blocks_per_chunk: int,
    deterministic: bool = False,
):
    block_num_tokens, block_hidden, mad_hidden = 128, 256, 64
    num_aivs = 2
    half_block_num_tokens = block_num_tokens // num_aivs
    num_mads_per_block = block_hidden // mad_hidden
    num_load_stages, num_cast_stages, num_l1_x_stages, num_mad_stages = 3, 2, 3, 2

    num_tokens = T.dynamic('num_tokens')
    mhc_mult3 = T.const('mhc_mult3')
    mhc_hidden_size = T.const('mhc_hidden_size')

    x: T.Tensor[(num_tokens, mhc_hidden_size), T.bfloat16]
    fn: T.Tensor[(mhc_mult3, mhc_hidden_size), T.float32]
    out_mul: T.Tensor[(num_tokens, mhc_mult3), T.float32]
    sqrsum: T.Tensor[(num_tokens,), T.float32]

    # 非确定性模式下:相位 0 让 split 0 普通写,其余 split 再原子加。
    num_store_phases = num_k_splits if deterministic else (2 if num_k_splits > 1 else 1)
    nz_stage_rows = half_block_num_tokens + 1

    with T.MixedKernel(num_aic_cores) as (core_id, aiv_id):
        split_id = core_id // cores_per_split
        core_in_split = core_id % cores_per_split
        num_token_blocks = T.ceildiv(num_tokens, block_num_tokens)
        num_token_chunks = T.ceildiv(num_token_blocks, num_token_blocks_per_chunk)

        # --- 片上缓冲:UB / L1 / L0A / L0B / L0C --------------------------------
        x_ub = T.alloc_shared((half_block_num_tokens, block_hidden), T.bfloat16)
        x_nz_ub = T.alloc_shared((nz_stage_rows, block_hidden), T.float32)
        T.annotate_layout({x_nz_ub: make_ascend_compact_nz_layout(x_nz_ub)})
        sqrsum_ub = T.alloc_shared((num_l0c_stages, half_block_num_tokens), T.float32)
        x_l1 = T.alloc_l1((block_num_tokens, block_hidden), T.float32)
        fn_l1 = T.alloc_l1((block_mhc_mult3, block_hidden), T.float32)
        x_l0 = T.alloc_l0a((block_num_tokens, mad_hidden), T.float32)
        fn_l0 = T.alloc_l0b((num_mads_per_block, block_mhc_mult3, mad_hidden), T.float32)
        out_mul_l0 = T.alloc_l0c((num_l0c_stages, block_num_tokens, block_mhc_mult3),
                                 T.float32)

        T.annotate_buffer_versions({
            x_ub: num_load_stages, x_nz_ub: num_cast_stages, x_l1: num_l1_x_stages,
            fn_l1: num_l0b_stages, x_l0: num_mad_stages, fn_l0: num_l0b_stages,
        })

        # K-split 合并:只有 split 0 不做原子加。
        if split_id != 0:
            T.set_atomic('add', 'float32')
        T.set_hf32_mode('nearest_even')

        for chunk_idx in T.Serial(num_token_chunks):
            token_block_base = chunk_idx * num_token_blocks_per_chunk + core_in_split
            num_valid_token_stages = T.min(
                num_l0c_stages,
                T.max(T.ceildiv(num_token_blocks - token_block_base, cores_per_split), 0),
            )

            # 隐藏维流水:每块 256 个 K 元素;权重沿 GM→L1→L0B 走一次。
            for local_hidden_block in T.Pipelined(num_hidden_blocks_per_split, num_stages=2):
                global_hidden_block = split_id * num_hidden_blocks_per_split + local_hidden_block
                hidden_begin = global_hidden_block * block_hidden

                # 物理 buffer 按 block_mhc_mult3 对齐,逻辑只搬 mhc_mult3 行。
                T.copy(fn[0:mhc_mult3, hidden_begin:hidden_begin + block_hidden],
                       fn_l1[0:mhc_mult3, :])
                for hidden_mad in T.Serial(num_mads_per_block):
                    T.copy(fn_l1[0:mhc_mult3,
                                 hidden_mad * mad_hidden:(hidden_mad + 1) * mad_hidden],
                           fn_l0[hidden_mad, 0:mhc_mult3, :])

                for token_stage in T.Serial(
                    num_valid_token_stages,
                    annotations={'multi_buffer_eligible': [x_l1]},
                ):
                    token_block = token_block_base + token_stage * cores_per_split
                    token_begin = token_block * block_num_tokens
                    aiv_token_begin = token_begin + aiv_id * half_block_num_tokens
                    actual_aiv = T.min(num_tokens - aiv_token_begin, half_block_num_tokens)

                    if aiv_token_begin < num_tokens:
                        T.copy(x[aiv_token_begin:aiv_token_begin + actual_aiv,
                                 hidden_begin:hidden_begin + block_hidden],
                               x_ub[0:actual_aiv, :],
                               l2_cache_ctrl='notalloc_keep', pad_value=0)

                        # 一趟遍历做完三件事:bf16→fp32 展宽、NZ 打包、行平方和。
                        with T.SimdVF(latency=640):
                            f32_mask = T.simd.pset(32)
                            nz_stride = T.int32((nz_stage_rows << 16) | (8 * nz_stage_rows))
                            zero_bf16 = T.simd.vdup(T.bfloat16(0), 'bfloat16', T.simd.pset(16))
                            for row in T.Serial(half_block_num_tokens):
                                row_acc = T.simd.alloc_var('float32')
                                row_acc = T.simd.vdup(T.float32(0), 'float32', f32_mask)
                                nz_ptr = T.simd.make_ubuf_ptr(
                                    T.access_ptr(x_nz_ub[row, 0], 'w', 1, block_hidden),
                                    'float32')
                                for pass_id in T.Serial(block_hidden // 128):
                                    packed = T.simd.vld(x_ub[row, pass_id * 128])
                                    lo_bits, hi_bits = T.simd.vintlv(zero_bf16, packed)
                                    lo = T.reinterpret(lo_bits, 'float32x64')
                                    hi = T.reinterpret(hi_bits, 'float32x64')
                                    row_acc = T.simd.vadd(row_acc,
                                                          T.simd.vmul(lo, lo, f32_mask),
                                                          f32_mask)
                                    row_acc = T.simd.vadd(row_acc,
                                                          T.simd.vmul(hi, hi, f32_mask),
                                                          f32_mask)
                                    nz_ptr = T.simd.vsstb(lo, nz_ptr, nz_stride, f32_mask,
                                                          update=True)
                                    nz_ptr = T.simd.vsstb(hi, nz_ptr, nz_stride, f32_mask,
                                                          update=True)

                        # UB → L1:劈成两个 AIV 的半块(dual_copy 的语义见第 25 章)。
                        T.dual_copy(x_nz_ub[0:half_block_num_tokens, 0:block_hidden],
                                    x_l1[0:block_num_tokens, 0:block_hidden])

                    # L1 → L0A,然后一条 MAD 出 L0C。
                    for hidden_mad in T.Serial(num_mads_per_block):
                        T.copy(x_l1[0:block_num_tokens,
                                    hidden_mad * mad_hidden:(hidden_mad + 1) * mad_hidden],
                               x_l0[0:block_num_tokens, :])
                        T.gemm(x_l0[0:block_num_tokens, :],
                               fn_l0[hidden_mad, 0:mhc_mult3, :],
                               out_mul_l0[token_stage, 0:block_num_tokens, 0:mhc_mult3],
                               transpose_B=True,
                               clear_accum=(local_hidden_block == 0 and hidden_mad == 0),
                               unit_flag_ctrl=T.Select(
                                   local_hidden_block == num_hidden_blocks_per_split - 1
                                   and hidden_mad == num_mads_per_block - 1, 3, 2))

            # store 阶段放在 T.PerCoreTask 里,靠核间同步串起来(原文件 :294-356)。
            with T.PerCoreTask():
                for store_phase in T.Serial(num_store_phases):
                    store_this_phase = T.if_then_else(
                        deterministic, split_id == store_phase,
                        T.if_then_else(store_phase == 0, split_id == 0, split_id != 0))
                    out_mul_flag = (chunk_idx * num_store_phases + store_phase) % 4
                    if store_this_phase:
                        with T.Task():
                            for token_stage in T.Serial(num_valid_token_stages):
                                token_begin = (token_block_base
                                               + token_stage * cores_per_split) * block_num_tokens
                                actual = T.min(num_tokens - token_begin, block_num_tokens)
                                T.copy(out_mul_l0[token_stage, 0:actual, 0:mhc_mult3],
                                       out_mul[token_begin:token_begin + actual, 0:mhc_mult3],
                                       unit_flag_ctrl=3)
                    num_store_syncs = T.if_then_else(
                        store_phase < (num_store_phases if deterministic else 1), 1,
                        T.min(num_token_chunks - chunk_idx - 1,
                              0 if deterministic or num_k_splits == 1 else 1))
                    for _ in T.Serial(num_store_syncs):
                        T.ascend_sync_inter_arrive('PIPE_FIX', out_mul_flag)
                        T.ascend_sync_inter_wait('PIPE_FIX', out_mul_flag)

        if split_id != 0:
            T.set_atomic_none()


if __name__ == '__main__':
    # 本骨架只用于阅读;这里仅演示配置怎么算出来(原文件 :392-397 的调用路径)。
    print('提示:真机上请调用 deep_gemm.tf32_hc_prenorm_gemm(a, b, d, sqr_sum)。')
    print('本机没有 NPU,未编译、未运行。')

← 浏览本章全部代码