Skip to content

Commit 81d0a3a

Browse files
authored
Merge pull request #97 from peterlau123/feat/add_triton_kernel_analysis
Feat/add triton kernel analysis
2 parents fb4f053 + 0b0bc3f commit 81d0a3a

1 file changed

Lines changed: 166 additions & 0 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
---
2+
title: Triton Fused Softmax Kernel
3+
subtitle: '学习与思考'
4+
layout: post
5+
author: peter_lau
6+
published: true
7+
categories:
8+
- AI
9+
tags:
10+
- AI
11+
- Engineering
12+
toc: true
13+
mermaid: true
14+
date: 2026-04-28 00:23:14 +0800
15+
---
16+
17+
本文代码来自[triton-02-fused-softmax](https://triton-lang.org/main/getting-started/tutorials/02-fused-softmax.html)
18+
19+
<mark style="background-color: yellow;">Triton的样本代码实现基于假设:**每一行数据可以完整的放入GPU的shared_memory中**</mark>
20+
21+
## kernel外围驱动代码
22+
23+
### 样本代码
24+
25+
```python
26+
properties = driver.active.utils.get_device_properties(DEVICE.index)
27+
NUM_SM = properties["multiprocessor_count"]
28+
NUM_REGS = properties["max_num_regs"]
29+
SIZE_SMEM = properties["max_shared_mem"]
30+
WARP_SIZE = properties["warpSize"]
31+
target = triton.runtime.driver.active.get_current_target()
32+
kernels = {}
33+
34+
def softmax(x):
35+
n_rows, n_cols = x.shape
36+
37+
# The block size of each loop iteration is the smallest power of two greater than the number of columns in `x`
38+
BLOCK_SIZE = triton.next_power_of_2(n_cols)
39+
40+
# Another trick we can use is to ask the compiler to use more threads per row by
41+
# increasing the number of warps (`num_warps`) over which each row is distributed.
42+
# You will see in the next tutorial how to auto-tune this value in a more natural
43+
# way so you don't have to come up with manual heuristics yourself.
44+
num_warps = 8
45+
46+
# Number of software pipelining stages.
47+
num_stages = 4 if SIZE_SMEM > 200000 else 2
48+
49+
# Allocate output
50+
y = torch.empty_like(x)
51+
52+
# pre-compile kernel to get register usage and compute thread occupancy.
53+
kernel = softmax_kernel.warmup(y, x, x.stride(0), y.stride(0), n_rows, n_cols, BLOCK_SIZE=BLOCK_SIZE,
54+
num_stages=num_stages, num_warps=num_warps, grid=(1, ))
55+
kernel._init_handles()
56+
n_regs = kernel.n_regs
57+
size_smem = kernel.metadata.shared
58+
if is_hip():
59+
# NUM_REGS represents the number of regular purpose registers. On CDNA architectures this is half of all registers available.
60+
# However, this is not always the case. In most cases all registers can be used as regular purpose registers.
61+
# ISA SECTION (3.6.4 for CDNA3)
62+
# VGPRs are allocated out of two pools: regular VGPRs and accumulation VGPRs. Accumulation VGPRs are used
63+
# with matrix VALU instructions, and can also be loaded directly from memory. A wave may have up to 512 total
64+
# VGPRs, 256 of each type. When a wave has fewer than 512 total VGPRs, the number of each type is flexible - it is
65+
# not required to be equal numbers of both types.
66+
NUM_GPRS = NUM_REGS
67+
if is_cdna():
68+
NUM_GPRS = NUM_REGS * 2
69+
70+
# MAX_NUM_THREADS represents maximum number of resident threads per multi-processor.
71+
# When we divide this number with WARP_SIZE we get maximum number of waves that can
72+
# execute on a CU (multi-processor) in parallel.
73+
MAX_NUM_THREADS = properties["max_threads_per_sm"]
74+
max_num_waves = MAX_NUM_THREADS // WARP_SIZE
75+
occupancy = min(NUM_GPRS // WARP_SIZE // n_regs, max_num_waves) // num_warps
76+
else:
77+
occupancy = NUM_REGS // (n_regs * WARP_SIZE * num_warps)
78+
occupancy = min(occupancy, SIZE_SMEM // size_smem)
79+
num_programs = NUM_SM * occupancy
80+
81+
num_programs = min(num_programs, n_rows)
82+
83+
# Create a number of persistent programs.
84+
kernel[(num_programs, 1, 1)](y, x, x.stride(0), y.stride(0), n_rows, n_cols, BLOCK_SIZE, num_stages)
85+
return y
86+
```
87+
88+
### 问题思考
89+
90+
1. num_stages作用是什么?一般怎么设置?
91+
92+
num_stages代表软件流水线的级数。它越大,流水线越长,意味着计算当前stage时,可以将其之前所有stage的数据与预加载至shared memory中。
93+
94+
对于Amper/Hopper架构的GPU,shared memory比较大,num_stages可以设置为4;对于更早的GPU,shared memory较小,num_stages需要设置较小。
95+
96+
2. Block数量计算方式
97+
98+
```python
99+
occupancy = NUM_REGS // (n_regs * WARP_SIZE * num_warps)
100+
```
101+
102+
这里的n_regs是一个线程占用的寄存器数量。NUM_REGS为SM的寄存器总数,WARP_SIZE*num_warps为一个block所占用的线程数,occupancy即为block数量。
103+
104+
请注意,block数量不仅受限于寄存器,还受限于shared memory。如果一个block使用的寄存器或者shared memory越大,那么block数量就会越少。
105+
106+
107+
## 计算kernel代码
108+
109+
### 样例代码
110+
111+
```python
112+
@triton.jit
113+
def softmax_kernel(output_ptr, input_ptr, input_row_stride, output_row_stride, n_rows, n_cols, BLOCK_SIZE: tl.constexpr,
114+
num_stages: tl.constexpr):
115+
# starting row of the program
116+
row_start = tl.program_id(0)
117+
row_step = tl.num_programs(0)
118+
for row_idx in tl.range(row_start, n_rows, row_step, num_stages=num_stages):
119+
# The stride represents how much we need to increase the pointer to advance 1 row
120+
row_start_ptr = input_ptr + row_idx * input_row_stride
121+
# The block size is the next power of two greater than n_cols, so we can fit each
122+
# row in a single block
123+
col_offsets = tl.arange(0, BLOCK_SIZE)
124+
input_ptrs = row_start_ptr + col_offsets
125+
# Load the row into SRAM, using a mask since BLOCK_SIZE may be > than n_cols
126+
mask = col_offsets < n_cols
127+
row = tl.load(input_ptrs, mask=mask, other=-float('inf'))
128+
# Subtract maximum for numerical stability
129+
row_minus_max = row - tl.max(row, axis=0)
130+
# Note that exponentiation in Triton is fast but approximate (i.e., think __expf in CUDA)
131+
numerator = tl.exp(row_minus_max)
132+
denominator = tl.sum(numerator, axis=0)
133+
softmax_output = numerator / denominator
134+
# Write back output to DRAM
135+
output_row_start_ptr = output_ptr + row_idx * output_row_stride
136+
output_ptrs = output_row_start_ptr + col_offsets
137+
tl.store(output_ptrs, softmax_output, mask=mask)
138+
```
139+
140+
### 几个问题
141+
142+
1. block内部的线程是如何分配的?没看到单个线程的索引
143+
144+
从CUDA的线程视角来看,这个确实不太明确。但是Triton的编程模型是SPMD,即single program multiple data。
145+
146+
在Triton你不用担心各个线程怎么索引,你只需将block作为一个整体来处理对应的块数据。
147+
148+
```python
149+
for row_idx in tl.range(row_start, n_rows, row_step, num_stages=num_stages)
150+
```
151+
上述的row_idx代表当前program(即当前block)需要处理的行索引
152+
153+
然后对于每一行,以block线程大小(即num_warps*WARP_SIZE)来处理整行数据,如下
154+
155+
```python
156+
col_offsets = tl.arange(0, BLOCK_SIZE)
157+
input_ptrs = row_start_ptr + col_offsets
158+
# Load the row into SRAM, using a mask since BLOCK_SIZE may be > than n_cols
159+
mask = col_offsets < n_cols
160+
row = tl.load(input_ptrs, mask=mask, other=-float('inf'))
161+
# Subtract maximum for numerical stability
162+
row_minus_max = row - tl.max(row, axis=0)
163+
```
164+
**tl.load****tl.max**等都是将数据按照张量形式来处理,在其内部Triton编译器会将其分配给block内的线程。
165+
166+

0 commit comments

Comments
 (0)