Triton Ascend Autotune 原理:Event 计时与 NPU Profiler 机制深度解析

本文基于 Triton Ascend 当前源码实现,系统分析 Autotune 的配置搜索流程,以及默认 NPU Event 计时和 TRITON_BENCH_METHOD=npu 两种性能测量方式的原理、边界与适用场景。

关键词: Triton、Ascend、Autotune、NPU Event、NPU Profiler、L2 Cache、性能调优

一、为什么需要 Autotune

同一个 Triton Kernel 可以使用多组编译和调度参数。例如:

triton.Config(
    {"BLOCK_M": 64, "BLOCK_N": 64, "BLOCK_K": 32},
    num_warps=4,
    num_stages=2,
)

不同配置虽然计算结果相同,但会改变:

  • Tile 和工作集大小;
  • 计算单元并行度;
  • 全局内存访问模式;
  • L2 Cache 命中和替换行为;
  • 片上存储及 UB 占用;
  • 指令流水和多缓冲效率;
  • Kernel 调度与尾块利用率。

最优配置通常与输入 Shape、数据类型、Kernel 结构和硬件资源有关,很难仅依靠静态规则确定。Autotune 因此采用实测搜索:为同一个 Kernel 构造多个候选版本,在真实 NPU 上测量,再选择最快的配置。

其目标可以表示为:

c ∗ ( k ) = arg ⁡ min ⁡ c ∈ C ( k )    T ( c , k ) c^*(k)=\underset{c\in C(k)}{\arg\min}\;T(c,k) c(k)=cC(k)argminT(c,k)

其中:

  • k k k 表示当前输入对应的 tuning key;
  • C ( k ) C(k) C(k) 表示该输入下的有效候选配置集合;
  • T ( c , k ) T(c,k) T(c,k) 表示配置 c c c 的实测执行时间;
  • c ∗ ( k ) c^*(k) c(k) 表示最终选出的最优配置。

Autotune 的本质不是修改算法,而是在多个合法实现中寻找实际硬件执行时间最短的版本。

二、Autotune 的完整工作流程

整体流程如下:

输入 Kernel 和运行参数
        ↓
生成 tuning key
        ↓
查询内存或磁盘缓存
        ↓ cache miss
生成候选配置
        ↓
规则与性能模型裁剪
        ↓
编译候选 Kernel
        ↓
剔除编译失败或资源超限配置
        ↓
测量每个有效配置
        ↓
选择耗时最小的配置
        ↓
缓存 best config 并正式执行

2.1 生成 tuning key

Ascend Autotuner 根据以下信息构造 tuning key:

  • @triton.autotune 指定的 key 参数;
  • Tensor 数据类型;
  • compile_mode
  • 与配置生成相关的运行参数。

相关实现位于:

third_party/ascend/backend/runtime/autotuner.py:2017

如果 key 已经存在于缓存中,Autotuner 会直接复用对应的最优配置,不再重新测量。因此,运行过程中修改 TRITON_BENCH_METHOD 后,如果当前 key 已命中内存或磁盘缓存,不一定会触发重新 tune。

2.2 生成和裁剪候选配置

候选配置既可以来自用户定义的 triton.Config,也可以由 Ascend 后端结合轴信息和配置提示自动生成。随后可能经过两类裁剪:

  1. early_config_prune:排除资源超限、参数冲突或明显不合法的配置;
  2. perf_model + top_k:通过性能模型估算耗时,只保留预测较好的配置。

其目的是降低首次调优成本。候选配置越多,找到全局最优解的概率通常越高,但每个配置都需要编译和 benchmark,首次运行的成本也会随之增加。

2.3 编译候选 Kernel

每个候选配置会被封装为独立的 kernel_call,其中合并:

  • Kernel 原始参数;
  • 当前候选配置;
  • UB tuner 配置;
  • pre-hook 和 post-hook;
  • Ascend 默认编译选项。

候选 Kernel 可以串行或并行编译。编译期断言失败、MLIR 编译失败或硬件资源不足的配置会被排除。

编译成功后,Autotuner 还会从 metadata 中获取 kernel_name。NPU Profiler 模式会用这个名字从 profiling 数据中识别目标 Triton Kernel。

相关实现位于:

third_party/ascend/backend/runtime/autotuner.py:2194

2.4 选择并缓存最优配置

完成测量后会得到如下映射:

timings = {
    config_0: time_0,
    config_1: time_1,
    config_2: time_2,
}

最终通过:

best_config = min(timings, key=timings.get)

选择耗时最小的配置,并将其缓存。后续相同 tuning key 可以直接执行该配置,不再支付完整的调优开销。

三、默认模式:基于 NPU Event 的计时

未设置 TRITON_BENCH_METHOD=npu 时,Autotuner 调用通用 do_bench(),通过 NPU Event 测量候选配置。

实现位于:

python/triton/testing.py:127

3.1 Event 对象来自哪里

代码首先获取设备接口:

di = runtime.driver.active.get_device_interface()

torch_npu 后端中,该接口返回 torch.npu,因此:

start_event = di.Event(enable_timing=True)

等价于:

start_event = torch.npu.Event(enable_timing=True)

3.2 start_event.record() 的底层机制

start_event.record() 不是读取 CPU 墙钟时间,而是在当前 NPU Stream 中插入一个 Event 标记。调用链可以概括为:

torch.npu.Event.record()
        ↓
PyTorch 通用 Event 接口
        ↓
torch_npu NPUGuardImpl::record()
        ↓
LaunchRecordEventTask()
        ↓
aclrtRecordEvent()

Event 在 NPU Stream 真正执行到该位置时记录设备时间戳。enable_timing=True 会让底层 Event 具备 timeline 能力,当前官方 torch_npu 实现将其映射为 ACL_EVENT_TIME_LINE,或者在支持的新接口中映射为:

ACL_EVENT_TIME_LINE | ACL_EVENT_SYNC

record() 通常是异步操作。它表示“向当前 Stream 提交一个时间标记”,而不是“立即等待 NPU 并读取时间”。

3.3 正式测量的 Stream 顺序

默认模式每轮执行:

runtime.driver.active.clear_cache(cache)
start_event.record()
fn()
end_event.record()

在所有操作使用同一个当前 NPU Stream 的前提下,设备执行顺序为:

192 MiB cache.zero_()
          ↓
     START Event
          ↓
        fn()
          ↓
      END Event

同一 Stream 内任务严格保序,因此 START Event 会在缓存清理完成后生效,END Event 会在 fn() 提交的设备任务之后生效。

最后调用:

start_event.elapsed_time(end_event)

底层使用 aclrtEventElapsedTime() 计算两个设备 Event 的时间戳差值。因此默认模式不需要启动 NPU Profiler,也不会生成 kernel_details.csv

3.4 耗时估算与自适应采样

正式 benchmark 前,默认模式先运行一次 fn() 并同步,用于完成初始编译、运行时初始化和基础 warmup。

随后通过 5 次运行估算平均耗时:

start_event.record()
for _ in range(5):
    clear_cache(cache)
    fn()
end_event.record()

因此,估算值实际由“缓存清理时间”和“fn() 执行时间”共同组成。这是一个容易忽略的实现细节:估算阶段的 Event 位于循环外,所以缓存清理耗时会进入 estimate_ms

随后,代码用 25 ms 除以估算耗时得到 warmup 次数,用 100 ms 除以估算耗时得到 repeat 次数,并保证两者至少为 1。也就是说,默认模式不是固定运行若干次,而是根据估算耗时控制总体测试预算。

这种机制能够控制总体 benchmark 时间,但对于非常短的 Kernel,192 MiB 缓存清理可能占据估算值的大部分,从而减少最终重复次数。

3.5 统计方式:以 P50 作为主要选优指标

Autotuner 请求:

quantiles=(0.5, 0.2, 0.8)

每个配置返回:

[P50, P20, P80]

配置比较时,Python 会首先比较列表的第一个元素,因此实际主要按照 P50,也就是中位数选优。中位数对偶发慢样本更鲁棒,能够降低系统负载、调度抖动和少量异常数据对配置选择的影响。

3.6 默认模式真正测量的对象

默认 Event 模式测量的是:

START Event 与 END Event 之间,当前 NPU Stream 上的全部设备工作。

如果 fn() 中包含目标 Triton Kernel、辅助 Kernel、pre-hook 或 post-hook 产生的设备任务,只要它们位于同一 Stream 和 Event 区间内,就可能全部计入结果。

因此,默认模式更接近“当前 Stream 上一次调用的设备总时间”,不一定等于单个 Triton Kernel 的纯执行时间。

四、NPU 模式:基于 Profiler Kernel 记录的计时

设置:

$env:TRITON_BENCH_METHOD = "npu"

后,Autotuner 会尝试调用 do_bench_npu()

do_bench_npu(
    funcs,
    warmup=5,
    active=30,
    clear_l2_cache=True,
    target_kernel_name=target_kernel_name,
)

相关实现位于:

third_party/ascend/backend/runtime/autotuner.py:2259
third_party/ascend/backend/testing.py:42

4.1 NPU 模式的生效条件

设置环境变量后并不意味着所有 tune 都会启动 Profiler。还需要满足:

  1. 编译后至少存在两个有效配置;
  2. 用户没有传入自定义 do_bench
  3. 当前 tuning key 没有命中已有最优配置缓存。

如果最终只剩一个有效配置,代码会绕过昂贵的 Profiler,直接使用默认 benchmark。如果用户提供了自定义 do_bench,Autotuner 也会尊重用户实现。

4.2 Profiler 执行流程

NPU 模式启动:

torch_npu.profiler.profile(
    activities=[torch_npu.profiler.ProfilerActivity.NPU],
    ...
)

然后在同一个 profiler session 中依次运行所有候选配置。普通模式下,每个配置执行 5 次 warmup 和 30 次 active 测量。

每一轮执行顺序为:

buffer.sum()
torch.npu.synchronize()
fn()
torch.npu.synchronize()

即:

读取 192 MiB Buffer
        ↓
等待缓存扰动完成
        ↓
执行目标 Kernel
        ↓
等待 Kernel 完成

与默认模式的批量异步提交不同,NPU 模式每次缓存扰动和 Kernel 执行后都会同步,因此轮次之间不会发生异步重叠,但调优时间通常更长。

4.3 从 kernel_details.csv 提取性能

Profiler 完成后,代码读取 kernel_details.csv,主要使用:

  • Name
  • Type
  • Duration(us)

前 5 条 warmup 数据被丢弃,后 30 条 active 数据的 Duration(us) 相加后除以 30,得到该配置的平均执行时间。因此,NPU 模式按照平均值选择配置,而不是默认模式使用的 P50 中位数。

平均值能反映总体时间水平,但对异常慢样本更加敏感。如果 29 次执行为 10 μs,1 次执行为 100 μs,那么中位数仍接近 10 μs,而平均值约为 13 μs。

4.4 按 Kernel 名称进行精确归因

编译候选配置时,Autotuner 会尝试获取目标 Kernel 名称:

packed_metadata["kernel_name"]

数据收集阶段再过滤:

filter_df = filter_df[
    filter_df["Name"] == target_kernel_name
]

这使 NPU 模式可以排除:

  • pre-hook 或 post-hook 产生的设备操作;
  • 框架辅助 Kernel;
  • 缓存扰动 Kernel;
  • 同一调用中的其他无关任务。

这也是 NPU Profiler 模式最重要的价值:它能够提供目标 Triton Kernel 级别的性能归因,而不是测量整个调用区间。

4.5 数据一致性校验与回退

按目标名称过滤后,每个候选配置都应该对应 5 条 warmup 记录和 30 条 active 记录,也就是 35 条目标 Kernel 记录。预期总行数等于有效配置数量乘以 35。

如果实际行数与预期不一致,可能说明:

  • Kernel name 不一致;
  • 某些轮次没有被正确记录;
  • 一次调用产生了多个同名 Kernel;
  • Profiler 数据不完整。

此时 Autotuner 会发出警告并回退到默认 Event 模式,避免使用行号错位的 profiler 数据。

如果没有解析到 target_kernel_name,则无法执行严格的目标名称校验。此时结果依赖过滤后 Kernel 记录的顺序,一次 fn() 包含多个辅助 Kernel 时可靠性会降低。

五、两种模式如何处理 L2 Cache

两种模式都会在正式测量前扰动 L2 Cache,因此都倾向于测量冷缓存性能。

5.1 默认模式:写入驱逐

Ascend Driver 创建一个固定 192 MiB 的缓冲区:

cache_size = 192 * 1024 * 1024

每轮执行:

cache.zero_()

通过写入整个缓冲区,将之前驻留在 L2 中的数据逐步驱逐出去。

实现位于:

third_party/ascend/backend/driver.py:311

5.2 NPU 模式:读取驱逐

NPU 模式使用:

buffer.sum()

通过读取整个 192 MiB 缓冲区产生缓存竞争。该操作在 Profiler 中表现为 ReduceSum,数据收集阶段会将它过滤掉,因此其耗时不会计入目标 Kernel。

需要注意,两种方式都不是调用硬件级 L2 invalidate 指令,而是通过访问远大于 L2 容量的数据进行缓存驱逐。因此这里的“清理 L2”是一种工程近似,不能理解为严格保证每条 Cache Line 都已失效。

此外,Profiler 配置中的:

l2_cache=False

表示关闭 Profiler 的 L2 指标采集,与 clear_l2_cache=True 是否执行缓存扰动没有关系。

六、两种计时机制的核心区别

对比维度 默认 Event 模式 NPU Profiler 模式
计时来源 NPU Event 时间戳差 Profiler 的 Duration(us)
是否启动 Profiler
测量边界 当前 Stream 的 Event 区间 指定名称的目标 Kernel
统计指标 P50 中位数 30 次算术平均值
采样次数 按时间预算自适应 默认固定 30 个有效样本
多配置执行 每个配置分别 benchmark 所有配置处于一个 profiler session
同步方式 多轮提交后统一同步 每次清缓存和 Kernel 后同步
L2 扰动 192 MiB zero_() 192 MiB sum()
辅助 Kernel 可能被计入 可按 Kernel 名称排除
异常值敏感度 较低 较高
Tune 开销 相对较低 相对较高
数据异常处理 直接使用 Event 结果 行数异常时回退 Event 模式

两者最本质的差异是测量边界:Event 模式统计 START 与 END 之间当前 Stream 上的全部设备工作;Profiler 模式则尝试只提取目标 Triton Kernel 的执行时间。

七、哪一种方式更准确

“更准确”必须结合测量目标判断,而不能脱离场景给出绝对结论。

7.1 面向单个 Triton Kernel 的配置搜索

如果调优目标是单个 Triton Kernel 的 tile、warp、stage 或流水配置,NPU Profiler 模式通常具有更准确的性能归因。

例如,一次 fn() 包含:

pre-hook Kernel       5 μs
目标 Triton Kernel   20 μs
post-hook Kernel      3 μs

默认 Event 模式可能测得约 28 μs,因为三个设备任务都处于 Event 区间内;NPU Profiler 按名称过滤后只保留目标 Kernel,测得约 20 μs。

如果 Autotune 只负责优化目标 Triton Kernel,那么 20 μs 是更纯净的比较指标。

7.2 面向整个调用链的性能优化

如果业务关心的是一次函数调用在设备侧的总体代价,默认 Event 模式可能更符合目标,因为配置变化可能同时影响:

  • 辅助 Kernel;
  • 数据恢复操作;
  • pre-hook 和 post-hook;
  • Kernel 间依赖;
  • 当前 Stream 上的调度间隔。

此时只测目标 Kernel 可能低估整个调用链的真实成本。

但需要注意,NPU Event 测量的仍然是设备 Stream 时间,并非严格意义上的 CPU 到 CPU 端到端延迟。

7.3 微秒级短 Kernel

对于非常短的 Kernel,两种模式各有优势:

  • Profiler 可以直接归因目标 Kernel,测量边界更纯;
  • Event 模式使用中位数,抗异常值能力更强;
  • Event 模式可以根据估算值调整重复次数;
  • NPU 模式固定 30 次有效采样,样本数量更可预测;
  • Event 的结果依赖 START、Kernel 和 END 位于同一 Stream;
  • 如果提交不及时,Event 区间也可能包含设备空闲间隔。

因此,短 Kernel 的可靠性不能只通过单次测量判断,应观察多次 tune 是否稳定选择同一个配置。

八、两种模式都不等于真实 warm-cache 性能

当前实现中,两条测量路径都会主动扰动 L2,因此主要描述冷缓存或近似冷缓存场景。如果实际业务会连续执行同一个 Kernel,并复用 L2 中的数据,那么生产环境看到的 warm-cache 性能可能不同。

不同缓存状态可能改变最优配置。例如:

  • 大 Tile 在冷缓存下可能更受全局内存带宽限制;
  • 小 Tile 的单次工作集更小,对缓存容量更友好;
  • 热缓存下,具有更强数据复用能力的配置可能获得额外收益;
  • 不同 Tile 对 L2 替换和工作集容量的敏感度不同。

因此,Autotune 结果不能替代真实业务验证。对于缓存复用明显的场景,应补充 warm-cache benchmark 或自定义 do_bench

九、推荐的工程实践

建议采用两阶段调优策略。

第一阶段:Kernel 级搜索

设置:

$env:TRITON_BENCH_METHOD = "npu"

使用 NPU Profiler 完成目标 Triton Kernel 的配置搜索。该模式更适合比较:

  • Tile 大小;
  • num_warps
  • num_stages
  • 流水与多缓冲配置;
  • 其他直接影响目标 Kernel 的编译参数。

第二阶段:真实业务验证

将排名靠前的配置放入真实调用链,重点观察:

  • 整体吞吐;
  • P50、P90 和 P99 延迟;
  • 连续运行时的 warm-cache 性能;
  • 不同 Shape 和 dtype 下的稳定性;
  • 频率、温度和系统负载造成的性能波动;
  • 多 Stream 或异步执行场景。

如果两种模式选出了不同配置,通常不意味着其中一种必然错误,更可能是以下原因:

  1. Event 模式测量了整个 Stream 区间;
  2. Profiler 模式只测量目标 Kernel;
  3. Event 模式使用中位数,Profiler 模式使用平均值;
  4. 两种模式的采样次数和同步策略不同;
  5. 辅助 Kernel 或 hook 被不同方式计入;
  6. 多个候选配置之间的差异已经接近测量噪声。

当候选配置差异不足 1%~2% 时,一次 tune 通常不足以证明某个配置绝对更优。应重复测量,检查配置选择的一致性,并结合真实业务指标做最终决策。

十、结论

Triton Ascend Autotune 是一个面向真实硬件的配置搜索系统。它通过 tuning key 区分输入场景,生成并裁剪候选配置,编译多个 Kernel 版本,测量实际执行时间,最后选择并缓存最优配置。

默认 Event 模式使用 NPU Stream 中两个 Event 的时间戳差进行测量,具有以下特点:

  • 不需要启动 Profiler;
  • 调优开销相对较低;
  • 采样次数按时间预算自适应;
  • 使用 P50 中位数,抗异常值能力较强;
  • 测量范围可能包含目标 Kernel 之外的设备任务。

TRITON_BENCH_METHOD=npu 模式使用 NPU Profiler 的 Kernel 记录,具有以下特点:

  • 能按 Kernel 名称精确归因;
  • 更接近目标 Triton Kernel 的纯执行时间;
  • 固定使用 warmup 和 active 样本;
  • 以算术平均值作为选优指标;
  • Profiler 和逐轮同步带来更高的调优成本;
  • 数据不一致时可以回退到默认 Event 模式。

因此,两种模式不能简单理解为“准确”和“不准确”,而应理解为测量目标不同:

默认 Event 模式更关注当前 Stream 调用区间,NPU Profiler 模式更关注目标 Triton Kernel 本体。

对于单个 Triton Kernel 的编译参数搜索,可以优先使用 NPU Profiler 模式;对于最终性能结论,则应在真实输入、真实缓存状态和真实业务调用链中再次验证。

参考源码

third_party/ascend/backend/runtime/autotuner.py
third_party/ascend/backend/testing.py
third_party/ascend/backend/driver.py
third_party/ascend/backend/backend_register.py
python/triton/runtime/autotuner.py
python/triton/testing.py

torch_npu Event 官方实现:

https://github.com/Ascend/pytorch/blob/master/torch_npu/csrc/core/npu/impl/NPUGuardImpl.cpp
Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐