Add BMM_Theory: bmm_theory/timing.py

This commit is contained in:
2026-09-03 08:09:28 +00:00
parent d08242489d
commit b2d1dd62d6

View File

@@ -0,0 +1,132 @@
"""时延评估引擎: 按 NPU 硬件流水模型计算各级时延.
模型依据 (v0.98 §3.1 + 用户补充说明):
T_total = max(T_MMAD, T_MTE2, T_MTE1, T_Fixpipe [, T_Reduce]) + T_drain
搬入 (MTE2) 的两条路径:
1. GM->L1 直读: 数据量按 GM 带宽计时.
算子软件可配置是否随路驻留 L2 (Cache Hint allocate);
无论是否驻留, 该次搬入时延都按 GM 带宽计算, 不累加后续 L2->L1.
驻留 L2 的意义只在于: 后续重复读可以走路径 2.
2. L2->L1: 数据已驻留 L2 后的再次/重复读取, 按 L2 带宽 (5.2TB/s) 计时.
Cube (MMAD): 按单核算力 Q16 x 使用核数计时. MergeBatch 的冗余计算计入 cube_flops.
Fixpipe 搬出: 写出 dtype = C 矩阵 dtype (fp16/fp8 时随路转换, 数据量按 C dtype 计);
StreamK 的中间部分和为防精度丢失按 L0C dtype (4B) 计, 且需再次读入 AIV 归约.
流水掩盖: 双缓冲下稳态时延取各级最大; T_drain 为末级排空暴露, 由各分支模型给出.
"""
from __future__ import annotations
from dataclasses import dataclass
from .hardware import NpuSpec, ASCEND950PR
from .models import HardwareTiming
@dataclass
class MoveInPlan:
"""一次稳态搬入的描述 (单核视角, 单位 Byte)."""
gm_bytes: float = 0.0 # GM->L1 直读量
l2_bytes: float = 0.0 # L2->L1 命中量
dma_cmds: float = 0.0 # GM->L1 DMA 命令次数
def eval_mte2(move: MoveInPlan, spec: NpuSpec = ASCEND950PR,
active_cores: int | None = None) -> tuple[float, float, float, float]:
"""MTE2 搬入时延.
返回 (t_mte2_gm, t_mte2_l2, t_mte2_total, t_dma_cmd).
带宽模型: 每核独立 DMA 引擎, 单核带宽份额 = 聚合带宽/C (尾轮文档 §2.4).
active_cores < C 时聚合带宽 = active_cores x 单核份额 (尾轮不加速).
"""
cores = active_cores or spec.aic_num
bw_gm_eff = spec.bw_pc * cores if cores < spec.aic_num else spec.bw_gm
bw_l2_eff = spec.bw_l2_pc * cores if cores < spec.aic_num else spec.bw_l2
# move 为单核数据量; 聚合时延 = 单核量 / 单核带宽份额 (等价 总量/聚合带宽)
t_gm = move.gm_bytes / spec.bw_pc if move.gm_bytes > 0 else 0.0
t_l2 = move.l2_bytes / spec.bw_l2_pc if move.l2_bytes > 0 else 0.0
t_cmd = move.dma_cmds * spec.t_cmd
# GM 直读与 L2 命中是不同数据的两段搬入, 时延相加; DMA 开销叠加在 GM 路径上
return t_gm, t_l2, t_gm + t_l2 + t_cmd, t_cmd
def eval_mmad(flops_per_core: float, spec: NpuSpec = ASCEND950PR) -> float:
"""Cube 计算时延: 单核计算量 / 单核算力."""
return flops_per_core / spec.q16 if flops_per_core > 0 else 0.0
def eval_fixpipe(bytes_per_core: float, to_l2: bool,
spec: NpuSpec = ASCEND950PR) -> float:
"""Fixpipe 搬出时延.
to_l2=True: 写出驻留 L2 (5.2TB/s 写口, 异步回写 GM 不占算子内时延);
to_l2=False: 直写 GM, 与读共享 1.6TB/s 总线.
"""
if bytes_per_core <= 0:
return 0.0
bw = spec.bw_l2_pc if to_l2 else spec.bw_pc
return bytes_per_core / bw
def eval_streamk_reduce(tile_elems: float, grid_k: int, out_dtype_bytes: int,
spec: NpuSpec = ASCEND950PR) -> float:
"""StreamK 单 tile 归约时延 (v0.98 §七).
部分和 dtype = L0C dtype (4B, 防精度丢失), 驻留 L2, AIV 归约:
AIC 写部分和 grid_k x tile x 4B / W_L2
AIV 读回 grid_k x tile x 4B / W_L2
AIV 求和 grid_k x tile / Q_AIV
写回 tile x outB / W_L2
"""
b4 = 4
w_l2 = spec.bw_l2
t_write_partial = grid_k * tile_elems * b4 / w_l2
t_read_back = grid_k * tile_elems * b4 / w_l2
t_sum = grid_k * tile_elems / spec.q_aiv
t_write_out = tile_elems * out_dtype_bytes / w_l2
return t_write_partial + t_read_back + t_sum + t_write_out
def assemble_timing(t_mte2_gm: float, t_mte2_l2: float, t_dma_cmd: float,
t_mmad: float, t_fixpipe: float, t_reduce: float,
t_drain: float,
gm_read_bytes: float, l2_read_bytes: float,
dma_cmd_count: float, cube_flops: float,
fixpipe_bytes: float) -> HardwareTiming:
"""汇总各级时延, 判定瓶颈."""
t_mte2 = t_mte2_gm + t_mte2_l2 + t_dma_cmd
stages = {
"MTE2_GM": t_mte2_gm + t_dma_cmd,
"MTE2_L2": t_mte2_l2,
"MMAD": t_mmad,
"FIXPIPE": t_fixpipe,
"REDUCE": t_reduce,
}
bottleneck = max(stages, key=stages.get)
t_steady = max(stages.values())
return HardwareTiming(
gm_read_bytes=gm_read_bytes, l2_read_bytes=l2_read_bytes,
t_mte2_gm=t_mte2_gm, t_mte2_l2=t_mte2_l2, t_mte2=t_mte2,
dma_cmd_count=dma_cmd_count, t_dma_cmd=t_dma_cmd,
cube_flops=cube_flops, t_mmad=t_mmad,
fixpipe_bytes=fixpipe_bytes, t_fixpipe=t_fixpipe,
t_reduce=t_reduce,
t_steady=t_steady, t_drain=t_drain, t_total=t_steady + t_drain,
bottleneck=bottleneck,
)
def bound_type_of(bottleneck: str) -> str:
return {
"MMAD": "计算Bound",
"MTE2_GM": "访存Bound(GM)",
"MTE2_L2": "访存Bound(L2)",
"FIXPIPE": "写出Bound",
"REDUCE": "归约Bound",
}.get(bottleneck, "")