Skip to content

Commit 10614f1

Browse files
committed
feat: add argsort operator implementation, tests and benchmark
1 parent a267340 commit 10614f1

6 files changed

Lines changed: 143 additions & 1 deletion

File tree

benchmark/test_special_perf.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,25 @@ def sort_input_fn(shape, dtype, device):
344344
bench.run()
345345

346346

347+
@pytest.mark.argsort
348+
def test_perf_argsort():
349+
class ArgsortBenchmark(GenericBenchmark2DOnly):
350+
def set_more_shapes(self):
351+
return [(1024, 1), (1024, 512), (16, 128 * 1024), (8, 256 * 1024)]
352+
353+
def argsort_input_fn(shape, dtype, device):
354+
inp = generate_tensor_input(shape, dtype, device)
355+
yield inp, {"dim": -1, "descending": False},
356+
357+
bench = ArgsortBenchmark(
358+
input_fn=argsort_input_fn,
359+
op_name="argsort",
360+
torch_op=torch.argsort,
361+
dtypes=INT_DTYPES + FLOAT_DTYPES,
362+
)
363+
bench.run()
364+
365+
347366
@pytest.mark.multinomial
348367
def test_multinomial_with_replacement():
349368
def multinomial_input_fn(shape, dtype, device):

conf/operators.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,16 @@ ops:
617617
- LinearAlg
618618
stages:
619619
- stable: '2.2'
620+
- name: argsort
621+
description: Returns the indices that sort a tensor along a given dimension in ascending order by value.
622+
for:
623+
- argsort
624+
labels:
625+
- aten
626+
kind:
627+
- Data
628+
stages:
629+
- stable: '3.0'
620630
- name: asinh_
621631
description: Computes the inverse hyperbolic sine for each element of a tensor in-place.
622632
for:

src/flag_gems/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ def torch_ge(v):
9090
("arcsinh_", arcsinh_),
9191
("argmax", argmax),
9292
("argmin", argmin),
93+
("argsort", argsort),
9394
("asinh_", asinh_),
9495
("atan", atan),
9596
("atan_", atan_),

src/flag_gems/ops/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
from flag_gems.ops.arctanh_ import arctanh_
2626
from flag_gems.ops.argmax import argmax
2727
from flag_gems.ops.argmin import argmin
28+
from flag_gems.ops.argsort import argsort
2829
from flag_gems.ops.asinh_ import asinh_
29-
from flag_gems.ops.assert_async import _assert_async
3030
from flag_gems.ops.atan import atan, atan_
3131
from flag_gems.ops.atan2 import atan2, atan2_out
3232
from flag_gems.ops.attention import (
@@ -371,6 +371,7 @@
371371
"arcsinh_",
372372
"argmax",
373373
"argmin",
374+
"argsort",
374375
"asinh_",
375376
"atan",
376377
"atan_",

src/flag_gems/ops/argsort.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import logging
2+
3+
import torch
4+
import triton
5+
6+
from flag_gems.ops.sort import sort_kernel, sort_stable
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
def argsort(inp, dim=-1, descending=False):
12+
"""Returns the indices that sort a tensor along a given dimension.
13+
14+
This is equivalent to calling torch.sort and returning only the indices.
15+
16+
Performance Notes:
17+
- For small N (≤4096): Uses bitonic sort (single kernel launch)
18+
Speedup: ~2.5-3x for N=64-256
19+
- For large N (>4096): Uses radix sort (multiple passes)
20+
Current performance: 0.1-0.3x slower than PyTorch for N≥1024
21+
TODO: Optimize radix sort for large N scenarios
22+
Benchmark results (N=1024-262144):
23+
- [1024, 1024]: 0.4-0.7x speedup
24+
- [4096, 4096]: 0.1-0.2x speedup
25+
- [1024, 65536]: 0.2-0.3x speedup
26+
"""
27+
logger.debug("GEMS ARGSORT")
28+
29+
# For small N, use bitonic sort (single kernel launch)
30+
# For large N, use radix sort (multiple passes)
31+
if dim < 0:
32+
dim = dim + inp.ndim
33+
34+
sort_elem_cnt = inp.shape[dim]
35+
36+
# Use bitonic sort for small sizes (faster single-kernel approach)
37+
if sort_elem_cnt <= 4096:
38+
if dim != inp.ndim - 1:
39+
inp = torch.movedim(inp, dim, -1).contiguous()
40+
else:
41+
inp = inp.contiguous()
42+
43+
N = inp.shape[-1]
44+
M = inp.numel() // N
45+
46+
out = torch.empty_like(inp)
47+
out_index = torch.empty(inp.shape, dtype=torch.int64, device=inp.device)
48+
49+
BLOCK_SIZE = triton.next_power_of_2(N)
50+
IS_FLOAT = inp.dtype.is_floating_point
51+
52+
grid = lambda meta: (M,)
53+
sort_kernel[grid](
54+
inp,
55+
out,
56+
out_index,
57+
N,
58+
BLOCK_SIZE=BLOCK_SIZE,
59+
DESCENDING=descending,
60+
IS_FLOAT=IS_FLOAT,
61+
)
62+
63+
if dim != inp.ndim - 1:
64+
out_index = torch.movedim(out_index, -1, dim)
65+
66+
return out_index
67+
else:
68+
# Use radix sort for large sizes
69+
_, indices = sort_stable(inp, stable=True, dim=dim, descending=descending)
70+
return indices

tests/test_argsort.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import pytest
2+
import torch
3+
4+
import flag_gems
5+
6+
from . import accuracy_utils as utils
7+
from .conftest import QUICK_MODE
8+
9+
ARGSORT_HIDDENSIZE = (
10+
[1, 256, 2048, 9333]
11+
if QUICK_MODE
12+
else [1, 256, 2048, 9333, 65536, 32768, 128 * 1024, 256 * 1024]
13+
)
14+
15+
16+
@pytest.mark.argsort
17+
@pytest.mark.parametrize("batch_size", [4, 8])
18+
@pytest.mark.parametrize("hiddensize", ARGSORT_HIDDENSIZE)
19+
@pytest.mark.parametrize("descending", [True, False])
20+
@pytest.mark.parametrize("dtype", utils.FLOAT_DTYPES + utils.INT_DTYPES)
21+
@pytest.mark.parametrize("dim", [0, -1])
22+
def test_accuracy_argsort(batch_size, hiddensize, descending, dtype, dim):
23+
if dtype in utils.BOOL_TYPES:
24+
y = torch.randint(
25+
0, 2, (batch_size, hiddensize), dtype=dtype, device=flag_gems.device
26+
)
27+
elif dtype in utils.ALL_INT_DTYPES:
28+
min_v, max_v = torch.iinfo(dtype).min, torch.iinfo(dtype).max
29+
y = torch.randint(
30+
min_v, max_v, (batch_size, hiddensize), dtype=dtype, device="cpu"
31+
).to(flag_gems.device)
32+
else:
33+
y = torch.randn((batch_size, hiddensize), dtype=dtype, device=flag_gems.device)
34+
35+
ref_y = utils.to_reference(y)
36+
ref_index = torch.argsort(ref_y, dim=dim, stable=True, descending=descending)
37+
38+
with flag_gems.use_gems():
39+
res_index = torch.argsort(y, dim=dim, stable=True, descending=descending)
40+
41+
utils.gems_assert_equal(res_index, ref_index)

0 commit comments

Comments
 (0)