Skip to content

Commit e669aee

Browse files
authored
add one-sweep radix sort (#694)
* add one-sweep radix sort * implement lockless decoupled-lookback scan for sort * support inputs other and 2d tensors * merge several data-size-dependent grid dimension to avoid exceeding max grid size
1 parent be6acf3 commit e669aee

3 files changed

Lines changed: 334 additions & 51 deletions

File tree

benchmark/test_special_perf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def unique_input_fn(shape, dtype, device):
118118
def test_perf_sort():
119119
class SortBenchmark(GenericBenchmark2DOnly):
120120
def set_more_shapes(self):
121-
return [(1024, 1), (1024, 512)]
121+
return [(1024, 1), (1024, 512), (16, 128 * 1024), (8, 256 * 1024)]
122122

123123
def sort_input_fn(shape, dtype, device):
124124
inp = generate_tensor_input(shape, dtype, device)
@@ -128,7 +128,7 @@ def sort_input_fn(shape, dtype, device):
128128
input_fn=sort_input_fn,
129129
op_name="sort",
130130
torch_op=torch.sort,
131-
dtypes=INT_DTYPES + FLOAT_DTYPES,
131+
dtypes=BOOL_DTYPES + INT_DTYPES + FLOAT_DTYPES,
132132
)
133133
bench.run()
134134

src/flag_gems/ops/sort.py

Lines changed: 312 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import logging
2-
import math
32

43
import torch
54
import triton
65
import triton.language as tl
6+
from triton.language.core import _unwrap_if_constexpr
77

88
from ..runtime import torch_device_fn
99
from ..utils import libentry
@@ -12,6 +12,313 @@
1212
logger = logging.getLogger(__name__)
1313

1414

15+
@tl.constexpr
16+
def get_int_t(num_bits: tl.constexpr, signed: tl.constexpr) -> tl.dtype:
17+
num_bits = _unwrap_if_constexpr(num_bits)
18+
signed = _unwrap_if_constexpr(signed)
19+
return tl.core.get_int_dtype(num_bits, signed)
20+
21+
22+
@tl.constexpr
23+
def one_zeros(num_bits: tl.constexpr) -> int:
24+
num_bits = _unwrap_if_constexpr(num_bits)
25+
return 1 << (num_bits - 1)
26+
27+
28+
@tl.constexpr
29+
def zero_ones(num_bits: tl.constexpr) -> int:
30+
num_bits = _unwrap_if_constexpr(num_bits)
31+
return (1 << (num_bits - 1)) - 1
32+
33+
34+
@triton.jit
35+
def uint_to_uint(x, descending: tl.constexpr = False):
36+
out = ~x if descending else x
37+
return out
38+
39+
40+
@triton.jit
41+
def int_to_uint(x, descending: tl.constexpr = False):
42+
num_bits: tl.constexpr = x.dtype.primitive_bitwidth
43+
udtype = get_int_t(num_bits, False)
44+
ux = tl.cast(x, udtype, bitcast=True)
45+
if descending:
46+
# 0111111....1
47+
bit_mask: tl.constexpr = zero_ones(num_bits)
48+
out = ux ^ bit_mask
49+
else:
50+
# 1000000...0
51+
sign_bit_mask: tl.constexpr = one_zeros(num_bits)
52+
out = ux ^ sign_bit_mask
53+
return out
54+
55+
56+
@triton.jit
57+
def floating_to_uint(x, descending: tl.constexpr = False):
58+
num_bits: tl.constexpr = x.dtype.primitive_bitwidth
59+
sdtype = get_int_t(num_bits, True)
60+
udtype = get_int_t(num_bits, False)
61+
sx = x.to(sdtype, bitcast=True)
62+
ux = x.to(udtype, bitcast=True)
63+
64+
sign_bit_mask: tl.constexpr = one_zeros(num_bits)
65+
# mind the dtype, right_shift for signed is arithmetic right shift
66+
mask = sign_bit_mask | (sx >> (num_bits - 1)).to(udtype, bitcast=True)
67+
# 1000000000...0 for positive
68+
# 1111111111...1 for negative
69+
if descending:
70+
out = ux ^ (~mask)
71+
else:
72+
out = ux ^ mask
73+
return out.to(udtype, bitcast=True)
74+
75+
76+
@triton.jit
77+
def convert_to_uint_preverse_order(x: tl.tensor, descending: tl.constexpr = False):
78+
if x.dtype.is_floating():
79+
out = floating_to_uint(x, descending)
80+
elif x.dtype.is_int_signed():
81+
out = int_to_uint(x, descending)
82+
elif x.dtype.is_int_unsigned():
83+
out = uint_to_uint(x, descending)
84+
return out
85+
86+
87+
@triton.jit
88+
def compute_global_hist_kernel(
89+
arr_ptr,
90+
out_ptr,
91+
num_passes,
92+
m,
93+
n,
94+
tiles_n_per_cta,
95+
TILE_N: tl.constexpr,
96+
TILE_R: tl.constexpr,
97+
num_bits_per_pass: tl.constexpr,
98+
descending: tl.constexpr,
99+
):
100+
# arr_ptr: (m, n)
101+
# out_ptr: (m, n_passes, r), where r = 2 ** k_bits is the number of bins
102+
pid = tl.program_id(0)
103+
pid_n = pid // m
104+
pid_m = pid % m
105+
106+
r: tl.constexpr = 2**num_bits_per_pass
107+
bfe_mask: tl.constexpr = (1 << num_bits_per_pass) - 1 # a.k.a. 2 ** k_bits - 1
108+
CTA_TILE_N: tl.constexpr = TILE_N * tiles_n_per_cta
109+
cta_n_start = CTA_TILE_N * pid_n
110+
cta_n_end = tl.minimum(cta_n_start + CTA_TILE_N, n)
111+
112+
for p in range(0, num_passes): # parallel
113+
bit_offset = p * num_bits_per_pass
114+
for r_start in range(0, r, TILE_R): # parallel
115+
bin_indices = r_start + tl.arange(0, TILE_R)
116+
acc = tl.zeros((TILE_R, TILE_N), dtype=tl.int64)
117+
for n_start in range(cta_n_start, cta_n_end, TILE_N): # sequantial
118+
n_offsets = n_start + tl.arange(0, TILE_N) # (TILE_N, )
119+
mask = n_offsets < cta_n_end
120+
arr = tl.load(arr_ptr + pid_m * n + n_offsets, mask=mask)
121+
arr = convert_to_uint_preverse_order(arr, descending)
122+
key = (arr >> bit_offset) & bfe_mask # (TILE_N, )
123+
matches = tl.where(
124+
mask, (bin_indices[:, None] == key), False
125+
) # (TILE_R, TILE_N)
126+
acc += matches
127+
local_sum = tl.sum(acc, axis=1)
128+
tl.atomic_add(
129+
out_ptr + pid_m * num_passes * r + p * r + bin_indices,
130+
local_sum,
131+
sem="relaxed",
132+
)
133+
134+
135+
@triton.jit
136+
def sweep(
137+
arr_ptr,
138+
associate_arr_ptr, # inputs: (key & value)
139+
out_ptr,
140+
associate_out_ptr, # outputs: (key & value)
141+
excumsum_bins_ptr,
142+
status_ptr, # aux input and status
143+
n_passes,
144+
pass_id,
145+
bit_offset,
146+
m,
147+
N,
148+
OUT_N,
149+
TILE_N: tl.constexpr,
150+
TILE_R: tl.constexpr,
151+
k_bits: tl.constexpr,
152+
descending: tl.constexpr,
153+
):
154+
# r: num_bins = 2 ** k_bits
155+
# OUT_N: grid_n = cdiv(N, )
156+
157+
# arr_ptr: (m, N)
158+
# out_ptr: (m, N)
159+
# excumsum_bins_ptr: (m, n_passes, r)
160+
# flag_ptr: (m, r, OUT_N)
161+
162+
# grid: (m, grid_r, grid_n)
163+
164+
# load data
165+
pid = tl.program_id(0)
166+
pid_m = pid % m
167+
pid_n = pid // m
168+
pid_r = tl.program_id(1)
169+
170+
# bit masks
171+
aggregate_mask: tl.constexpr = 1 << 30
172+
inclusive_prefix_mask: tl.constexpr = 1 << 31
173+
v_mask: tl.constexpr = (1 << 30) - 1
174+
bfe_mask: tl.constexpr = (1 << k_bits) - 1 # a.k.a. 2 ** k_bits - 1
175+
176+
# initialize flag to zero-local sum is not ready
177+
r: tl.constexpr = 2**k_bits
178+
cta_r_start = pid_r * TILE_R
179+
cta_r_end = tl.minimum(cta_r_start + TILE_R, r)
180+
181+
# cumsum for a bin_index
182+
n_offsets = pid_n * TILE_N + tl.arange(0, TILE_N) # (TILE_N, )
183+
mask = n_offsets < N
184+
arr = tl.load(arr_ptr + pid_m * N + n_offsets, mask=mask)
185+
arr_u = convert_to_uint_preverse_order(arr, descending)
186+
key = (arr_u >> bit_offset) & bfe_mask # (TILE_N, )
187+
188+
# since triton can only use scalar as condition, loop by bin_index
189+
# status must be pre zero-initialized, or else we have to initialize it
190+
for bin_index in range(cta_r_start, cta_r_end):
191+
matches = tl.where(mask, key == bin_index, False) # (TILE_N, ) bool
192+
# cta level cumsum per bin
193+
# CAUTION: tl.sum in triton 3.2 does not promote type
194+
local_sum = tl.sum(matches.to(tl.uint32), axis=0)
195+
pack0 = aggregate_mask | local_sum
196+
status_offset = pid_m * (r * OUT_N) + bin_index * OUT_N + pid_n
197+
tl.store(status_ptr + status_offset, pack0, cache_modifier=".cg")
198+
199+
# decoupled lookback
200+
exclusive_prefix = tl.zeros((), dtype=tl.uint32)
201+
i_lookback = pid_n - 1
202+
while i_lookback >= 0:
203+
flag_offset_i = pid_m * (r * OUT_N) + bin_index * OUT_N + i_lookback
204+
pack1 = tl.load(status_ptr + flag_offset_i, volatile=True) # uin32
205+
while pack1 == 0:
206+
pack1 = tl.load(status_ptr + flag_offset_i, volatile=True)
207+
exclusive_prefix += pack1 & v_mask
208+
if (pack1 & aggregate_mask) == aggregate_mask:
209+
i_lookback -= 1
210+
else:
211+
i_lookback = -1
212+
pack2 = inclusive_prefix_mask | (exclusive_prefix + local_sum)
213+
tl.store(status_ptr + status_offset, pack2, cache_modifier=".cg")
214+
215+
local_ex_cumsum = (
216+
tl.cumsum(matches.to(tl.uint32), axis=0) - matches
217+
) # (TILE_N, )
218+
ex_cumsum_in_bin = (
219+
exclusive_prefix + local_ex_cumsum
220+
) # global ex_cumsum_in_bin (TILE_N, )
221+
222+
# ex_cumsum_bins (m, n_passes, r)
223+
ex_cumsum_bins = tl.load(
224+
excumsum_bins_ptr + pid_m * (n_passes * r) + pass_id * r + bin_index
225+
) # scalar
226+
pos = ex_cumsum_bins + ex_cumsum_in_bin # (TILE_N, )
227+
228+
# scatter
229+
tl.store(out_ptr + pid_m * N + pos, arr, mask=matches)
230+
if associate_arr_ptr is not None:
231+
associate_arr = tl.load(
232+
associate_arr_ptr + pid_m * N + n_offsets, mask=mask
233+
)
234+
tl.store(associate_out_ptr + pid_m * N + pos, associate_arr, mask=matches)
235+
236+
237+
def radix_sort(arr, k_bits=8, descending=False):
238+
n = arr.shape[-1]
239+
m = arr.numel() // n
240+
assert n < (1 << 30), "we have not implemented 2**30 per launch"
241+
dtype = arr.dtype
242+
num_bits = 1 if dtype == torch.bool else (arr.itemsize * 8)
243+
244+
TILE_N = 1024
245+
tiles_n_per_cta = 8
246+
CTA_TILE_N = tiles_n_per_cta * TILE_N
247+
248+
num_bins = 2**k_bits
249+
n_passes = triton.cdiv(num_bits, k_bits)
250+
TILE_R = 16
251+
252+
grid_n = triton.cdiv(n, CTA_TILE_N)
253+
grid_for_global_hist = (m * grid_n, 1, 1)
254+
255+
with torch_device_fn.device(arr.device):
256+
global_hist = torch.zeros(
257+
(m, n_passes, num_bins), device=arr.device, dtype=torch.int32
258+
)
259+
compute_global_hist_kernel[grid_for_global_hist](
260+
arr,
261+
global_hist,
262+
n_passes,
263+
m,
264+
n,
265+
tiles_n_per_cta,
266+
TILE_N,
267+
TILE_R,
268+
k_bits,
269+
descending,
270+
)
271+
ex_cumsum_bins = torch.cumsum(global_hist, -1) - global_hist
272+
ex_cumsum_bins = ex_cumsum_bins.to(torch.uint32)
273+
274+
# sort
275+
arr_in = torch.clone(arr)
276+
indices_in = (
277+
torch.arange(0, n, dtype=torch.int64, device=arr_in.device)
278+
.broadcast_to(arr.shape)
279+
.contiguous()
280+
)
281+
arr_out = torch.empty_like(arr)
282+
indices_out = torch.empty_like(indices_in)
283+
284+
TILE_R = 8
285+
grid_r = triton.cdiv(num_bins, TILE_R)
286+
TILE_N = 2048
287+
grid_n = triton.cdiv(n, TILE_N)
288+
grid_for_sweep = (m * grid_n, grid_r)
289+
290+
status = torch.empty(
291+
(m, num_bins, grid_n), device=arr.device, dtype=torch.uint32
292+
)
293+
294+
for i in range(0, n_passes):
295+
bit_offset = i * k_bits
296+
status.zero_()
297+
sweep[grid_for_sweep](
298+
arr_in,
299+
indices_in,
300+
arr_out,
301+
indices_out,
302+
ex_cumsum_bins,
303+
status,
304+
n_passes,
305+
i,
306+
bit_offset,
307+
m,
308+
n,
309+
grid_n,
310+
TILE_N,
311+
TILE_R,
312+
k_bits,
313+
descending,
314+
)
315+
# print(f"< sorted last {bit_offset + k_bits:>2d} bits: {arr_out}")
316+
arr_in, arr_out = arr_out, arr_in
317+
indices_in, indices_out = indices_out, indices_in
318+
319+
return arr_in, indices_in
320+
321+
15322
@libentry()
16323
@triton.jit()
17324
def sort_kernel(
@@ -51,32 +358,17 @@ def sort(inp, dim=-1, descending=False):
51358
sort_elem_cnt = inp.shape[dim]
52359
if sort_elem_cnt == 1:
53360
return inp, torch.zeros_like(inp, dtype=torch.int64)
54-
elif sort_elem_cnt > 512: # TODO: Optimize implementation for large cases.
55-
return torch.sort(inp, stable=False, dim=dim, descending=descending)
56-
block_size = triton.next_power_of_2(sort_elem_cnt)
57361

58362
if dim < 0:
59363
dim = dim + inp.ndim
60364
if dim != inp.ndim - 1:
61365
inp = torch.movedim(inp, dim, -1).contiguous()
62366
else:
63367
inp = inp.contiguous()
64-
batch_size = math.prod(inp.shape) // sort_elem_cnt
65-
66-
out = torch.empty_like(inp)
67-
out_index = torch.empty_like(inp, dtype=torch.int64)
68-
69-
with torch_device_fn.device(inp.device):
70-
sort_kernel[batch_size,](
71-
inp,
72-
out,
73-
out_index,
74-
N=sort_elem_cnt,
75-
BLOCK_SIZE=block_size,
76-
DESCENDING=descending,
77-
IS_FLOAT=inp.is_floating_point(),
78-
num_warps=4,
79-
)
368+
369+
dtype = inp.dtype
370+
num_bits_per_pass = 1 if dtype == torch.bool else 4
371+
out, out_index = radix_sort(inp, num_bits_per_pass, descending)
80372

81373
if dim != inp.ndim - 1:
82374
out = torch.movedim(out, -1, dim)

0 commit comments

Comments
 (0)