Softmax算子快速入门(SIMT)#
任务与目标#
本节介绍如何参考SIMD版的单Kernel流程,使用PyPTO Pro SIMT编程范式实现Softmax,并通过测试用例验证结果。您将了解如何将数据搬入片上Tile、在SIMT函数内完成归约与逐元素计算,以及将结果搬回全局内存。
本示例固定处理Shape为[1, 256]、数据类型为FP32的Tensor。一个Kernel启动256个SIMT线程,每个线程负责Tile中的一个元素;示例要求输入元素为有限数值。
算子设计规格#
表1 Softmax算子设计规格
name |
shape |
data type |
format |
|---|---|---|---|
inputs(输入) |
(1, 256) |
float32 |
ND |
outputs(输出) |
(1, 256) |
float32 |
ND |
数学表达式
\(M = \max(z),\quad \text{SoftMax}(z_i) = \frac{\exp(z_i-M)}{\sum_j \exp(z_j-M)}\)
使用的主要接口
Tile定义与数据搬运接口:pypto_pro.language.TileType、pypto_pro.language.make_tile_group、pypto_pro.language.load、pypto_pro.language.store
线程索引与块内同步接口:pypto_pro.language.simt.linear_thread_idx、pypto_pro.language.simt.syncthreads
指数与原子归约接口:pypto_pro.language.simt.exp、pypto_pro.language.simt.atomic_max、pypto_pro.language.simt.atomic_add
导入PyPTO Pro模块#
在开始实现Softmax算子之前,首先需要导入PyPTO Pro、PyTorch和torch_npu模块。PyPTO Pro模块用于定义SIMT函数和编译Kernel,PyTorch和torch_npu模块用于构造输入数据、执行NPU计算并进行结果验证。
import os
import pypto_pro.language as pl
import torch
import torch_npu
本示例输入最后一维的长度为256,因此设置SIMT线程数为256:
THREADS = 256
核心代码逻辑#
实现Tile上的SIMT计算函数。
与SIMD版先将数据搬入Tile再计算的流程一致,这里由一个SIMT函数完成最大值归约、指数求和及归一化。256个线程各处理一个元素,使用同一块内的stats_tile保存最大值与指数和。
@pl.vector_function(mode="simt", max_threads=THREADS) def softmax_tile(src_tile, out_tile, stats_tile): tid = pl.simt.linear_thread_idx() if tid == 0: stats_tile[0, 0] = src_tile[0, 0] stats_tile[0, 1] = 0.0 pl.simt.syncthreads() value = src_tile[0, tid] pl.simt.atomic_max(stats_tile[0, 0], value) pl.simt.syncthreads() exp_value = pl.simt.exp(value - stats_tile[0, 0]) pl.simt.atomic_add(stats_tile[0, 1], exp_value) pl.simt.syncthreads() out_tile[0, tid] = exp_value / stats_tile[0, 1]
第一个线程初始化共享归约状态;每次归约结束后,所有线程通过syncthreads()等待结果可见。三个屏障均由所有线程无条件到达。最大值从输入首元素初始化,避免额外的FP32最小值常量。
实现Softmax Kernel函数。
与SIMD版相同,Kernel先分配片上Tile Group,再在Vector段完成数据搬入、计算和数据搬出。输入与输出Tile各占1024字节;归约状态Tile占32字节,其中前两个FP32元素分别保存最大值与指数和。三个Tile Group使用互不重叠的片上地址和不同的Mutex ID。
@pl.jit(arch="3510", auto_mutex=True) def softmax_simt_kernel( src: pl.Tensor[[1, THREADS], pl.DT_FP32], dst: pl.Tensor[[1, THREADS], pl.DT_FP32], ): tile_type = pl.TileType( shape=[1, THREADS], dtype=pl.DT_FP32, target_memory=pl.MemorySpace.Vec, ) stats_type = pl.TileType( shape=[1, 8], dtype=pl.DT_FP32, target_memory=pl.MemorySpace.Vec, ) src_group = pl.make_tile_group(type=tile_type, addrs=0x0000, mutex_ids=[0]) out_group = pl.make_tile_group(type=tile_type, addrs=0x0400, mutex_ids=[1]) stats_group = pl.make_tile_group(type=stats_type, addrs=0x0800, mutex_ids=[2]) with pl.section_vector(): src_tile = src_group.current() out_tile = out_group.current() stats_tile = stats_group.current() pl.load(src_tile, src, [0, 0]) softmax_tile[THREADS](src_tile, out_tile, stats_tile) pl.store(dst, out_tile, [0, 0])
auto_mutex=True结合Tile Group的Mutex ID自动处理搬入、SIMT计算与搬出之间的流水依赖,不需要手动调用sync_src/sync_dst。SIMT函数内部的syncthreads()仍负责线程间的数据依赖,不能省略。
测试用例#
使用PyTorch Tensor构造普通输入和较大正负偏移输入,将单次Kernel的结果与PyTorch Softmax对比。由于本示例只处理一行,显式使用kernel[None, 1]启动一个核心;softmax_tile[THREADS]在单个aiv中启动256个SIMT线程。
device_id = int(os.environ.get("TILE_FWK_DEVICE_ID", 0))
device = f"npu:{device_id}"
torch.npu.set_device(device)
torch.manual_seed(0)
for offset in (0.0, 100.0, -100.0):
src = torch.randn(1, THREADS, dtype=torch.float32, device=device) + offset
dst = torch.empty_like(src)
softmax_simt_kernel[None, 1](src, dst)
torch.npu.synchronize()
golden = torch.softmax(src, dim=-1)
torch.testing.assert_close(dst, golden, rtol=1e-4, atol=1e-5)
print("SIMT Softmax kernel passed!")
归约状态在每次SIMT函数调用时由线程0初始化,不需要Host侧的中间Tensor。FP32原子加的累加顺序不固定,因此验证使用容差而非逐位比较。
编译与执行#
在已安装PyPTO Pro的环境中运行:
# 配置CANN环境变量
source /usr/local/Ascend/ascend-toolkit/set_env.sh
# 设置设备ID
export TILE_FWK_DEVICE_ID=0
# 执行脚本
python3 softmax_simt.py
程序执行成功后,显示以下信息:
SIMT Softmax kernel passed!