Skip to content

Commit 4f024d6

Browse files
【KernelGen】Add roll operator (#1759)
* Add roll operator implementation, tests and benchmark * fix: codestyle * Rewrite roll with Triton kernel --------- Co-authored-by: factnn <1050552884@qq.com>
1 parent b4eb25a commit 4f024d6

5 files changed

Lines changed: 224 additions & 0 deletions

File tree

benchmark/test_generic_pointwise_perf.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ def flip_input_fn(shape, cur_dtype, device):
2121
yield inp, {"dims": (0,)}
2222

2323

24+
def roll_input_fn(shape, cur_dtype, device):
25+
inp = generate_tensor_input(shape, cur_dtype, device)
26+
if len(shape) > 1:
27+
yield inp, {"shifts": (1, 2), "dims": (0, 1)}
28+
else:
29+
yield inp, {"shifts": 1, "dims": 0}
30+
31+
2432
def where_input_fn(shape, cur_dtype, device):
2533
inp1 = generate_tensor_input(shape, cur_dtype, device)
2634
inp2 = generate_tensor_input(shape, cur_dtype, device)
@@ -108,6 +116,13 @@ def addcdiv_input_fn(shape, cur_dtype, device):
108116
FLOAT_DTYPES + INT_DTYPES,
109117
marks=pytest.mark.flip,
110118
),
119+
pytest.param(
120+
"roll",
121+
torch.roll,
122+
roll_input_fn,
123+
FLOAT_DTYPES + INT_DTYPES,
124+
marks=pytest.mark.roll,
125+
),
111126
pytest.param(
112127
"where", torch.where, where_input_fn, FLOAT_DTYPES, marks=pytest.mark.where
113128
),

src/flag_gems/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ def torch_ge(v):
376376
("resolve_conj", resolve_conj),
377377
("resolve_neg", resolve_neg),
378378
("rms_norm", rms_norm),
379+
("roll", roll),
379380
("round", round),
380381
("round.out", round_out),
381382
("round_", round_),

src/flag_gems/ops/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@
254254
from flag_gems.ops.resolve_conj import resolve_conj
255255
from flag_gems.ops.resolve_neg import resolve_neg
256256
from flag_gems.ops.rms_norm import rms_norm, rms_norm_backward, rms_norm_forward
257+
from flag_gems.ops.roll import roll
257258
from flag_gems.ops.round import round, round_, round_out
258259
from flag_gems.ops.rrelu_with_noise_backward import rrelu_with_noise_backward
259260
from flag_gems.ops.rsqrt import rsqrt, rsqrt_
@@ -638,6 +639,7 @@
638639
"rms_norm",
639640
"rms_norm_backward",
640641
"rms_norm_forward",
642+
"roll",
641643
"round",
642644
"round_",
643645
"round_out",

src/flag_gems/ops/roll.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import logging
2+
3+
import torch
4+
import triton
5+
import triton.language as tl
6+
7+
from flag_gems.utils import libentry
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
@libentry()
13+
@triton.jit
14+
def roll_kernel(
15+
inp_ptr,
16+
out_ptr,
17+
N,
18+
dim_size,
19+
shift,
20+
inner_size,
21+
BLOCK_SIZE: tl.constexpr,
22+
):
23+
pid = tl.program_id(0)
24+
offset = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE).to(tl.int64)
25+
mask = offset < N
26+
27+
# Decompose flat index into (outer, dim_idx, inner)
28+
outer_stride = dim_size * inner_size
29+
outer_idx = offset // outer_stride
30+
remainder = offset % outer_stride
31+
dim_idx = remainder // inner_size
32+
inner_idx = remainder % inner_size
33+
34+
# Apply roll: source_dim_idx = (dim_idx - shift) % dim_size
35+
source_dim_idx = (dim_idx - shift + dim_size) % dim_size
36+
37+
# Reconstruct source flat index
38+
source_offset = outer_idx * outer_stride + source_dim_idx * inner_size + inner_idx
39+
40+
val = tl.load(inp_ptr + source_offset, mask=mask, other=0.0)
41+
tl.store(out_ptr + offset, val, mask=mask)
42+
43+
44+
def _roll_single_dim(inp: torch.Tensor, shift: int, dim: int) -> torch.Tensor:
45+
size = inp.size(dim)
46+
if size == 0:
47+
return inp.clone()
48+
49+
shift = shift % size
50+
if shift == 0:
51+
return inp.clone()
52+
53+
inp_contig = inp.contiguous()
54+
out = torch.empty_like(inp_contig)
55+
56+
inner_size = 1
57+
for i in range(dim + 1, inp.ndim):
58+
inner_size *= inp.size(i)
59+
60+
N = inp.numel()
61+
BLOCK_SIZE = 512
62+
grid = lambda meta: (triton.cdiv(N, BLOCK_SIZE),)
63+
64+
roll_kernel[grid](
65+
inp_contig,
66+
out,
67+
N,
68+
size,
69+
shift,
70+
inner_size,
71+
BLOCK_SIZE=BLOCK_SIZE,
72+
)
73+
return out
74+
75+
76+
def roll(inp: torch.Tensor, shifts, dims=None) -> torch.Tensor:
77+
logger.debug("GEMS ROLL")
78+
79+
if inp.numel() == 0:
80+
return inp.clone()
81+
82+
if dims is None:
83+
if isinstance(shifts, (list, tuple)):
84+
shift = shifts[0] if len(shifts) == 1 else sum(shifts)
85+
else:
86+
shift = shifts
87+
original_shape = inp.shape
88+
flat = inp.contiguous().reshape(-1)
89+
out_flat = _roll_single_dim(flat, shift, 0)
90+
return out_flat.reshape(original_shape)
91+
92+
if isinstance(dims, int):
93+
dims = [dims]
94+
if isinstance(shifts, int):
95+
shifts = [shifts]
96+
97+
assert len(shifts) == len(dims), "shifts and dims must have the same length"
98+
99+
ndim = inp.ndim
100+
dims = [d % ndim for d in dims]
101+
102+
result = inp
103+
for shift, dim in zip(shifts, dims):
104+
result = _roll_single_dim(result, shift, dim)
105+
return result

tests/test_unary_pointwise_ops.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2656,6 +2656,38 @@ def test_accuracy_floor_(shape, dtype):
26562656
gems_assert_equal(res_out, ref_out)
26572657

26582658

2659+
ROLL_SHIFTS_DIMS = [
2660+
(1, 0),
2661+
(-1, 0),
2662+
(2, -1),
2663+
(3, 1),
2664+
]
2665+
2666+
2667+
@pytest.mark.roll
2668+
@pytest.mark.parametrize("shape", POINTWISE_SHAPES)
2669+
@pytest.mark.parametrize("dtype", FLOAT_DTYPES + ALL_INT_DTYPES)
2670+
@pytest.mark.parametrize("shifts_dims", ROLL_SHIFTS_DIMS)
2671+
def test_accuracy_roll_single_dim(shape, dtype, shifts_dims):
2672+
shifts, dims = shifts_dims
2673+
ndim = len(shape)
2674+
# Adjust dims if it's out of range for this shape
2675+
if dims >= ndim or dims < -ndim:
2676+
pytest.skip(f"dims {dims} out of range for shape {shape}")
2677+
2678+
if dtype in ALL_FLOAT_DTYPES:
2679+
inp = torch.randn(shape, dtype=dtype, device=flag_gems.device)
2680+
else:
2681+
inp = torch.randint(-1000, 1000, shape, device=flag_gems.device).to(dtype)
2682+
ref_inp = to_reference(inp, False)
2683+
2684+
ref_out = torch.roll(ref_inp, shifts, dims)
2685+
with flag_gems.use_gems():
2686+
res_out = torch.roll(inp, shifts, dims)
2687+
2688+
gems_assert_equal(res_out, ref_out)
2689+
2690+
26592691
@pytest.mark.special_i0e
26602692
@pytest.mark.parametrize("shape", [(2, 3), (128, 256), (512, 512)])
26612693
@pytest.mark.parametrize("dtype", FLOAT_DTYPES)
@@ -2690,3 +2722,72 @@ def test_accuracy_special_i0e_out(shape, dtype):
26902722
act_out = torch.ops.aten.special_i0e.out(x, out=out_act)
26912723
gems_assert_close(act_out, ref_out, dtype)
26922724
gems_assert_close(out_act, out_ref, dtype)
2725+
2726+
2727+
ROLL_MULTI_DIMS = [
2728+
((1, 2), (0, 1)),
2729+
((-1, 1), (0, -1)),
2730+
((2, -2), (-2, -1)),
2731+
]
2732+
2733+
2734+
@pytest.mark.roll
2735+
@pytest.mark.parametrize("shape", POINTWISE_SHAPES)
2736+
@pytest.mark.parametrize("dtype", FLOAT_DTYPES)
2737+
@pytest.mark.parametrize("shifts_dims", ROLL_MULTI_DIMS)
2738+
def test_accuracy_roll_multi_dims(shape, dtype, shifts_dims):
2739+
shifts, dims = shifts_dims
2740+
ndim = len(shape)
2741+
# Check all dims are valid for this shape
2742+
for d in dims:
2743+
if d >= ndim or d < -ndim:
2744+
pytest.skip(f"dims {d} out of range for shape {shape}")
2745+
2746+
inp = torch.randn(shape, dtype=dtype, device=flag_gems.device)
2747+
ref_inp = to_reference(inp, False)
2748+
2749+
ref_out = torch.roll(ref_inp, shifts, dims)
2750+
with flag_gems.use_gems():
2751+
res_out = torch.roll(inp, shifts, dims)
2752+
2753+
gems_assert_equal(res_out, ref_out)
2754+
2755+
2756+
ROLL_FLATTEN_SHIFTS = [1, -1, 5, -3]
2757+
2758+
2759+
@pytest.mark.roll
2760+
@pytest.mark.parametrize("shape", POINTWISE_SHAPES)
2761+
@pytest.mark.parametrize("dtype", FLOAT_DTYPES)
2762+
@pytest.mark.parametrize("shifts", ROLL_FLATTEN_SHIFTS)
2763+
def test_accuracy_roll_flatten(shape, dtype, shifts):
2764+
inp = torch.randn(shape, dtype=dtype, device=flag_gems.device)
2765+
ref_inp = to_reference(inp, False)
2766+
2767+
# Roll without specifying dims (flatten case)
2768+
ref_out = torch.roll(ref_inp, shifts)
2769+
with flag_gems.use_gems():
2770+
res_out = torch.roll(inp, shifts)
2771+
2772+
gems_assert_equal(res_out, ref_out)
2773+
2774+
2775+
@pytest.mark.roll
2776+
@pytest.mark.parametrize("shape", POINTWISE_SHAPES)
2777+
@pytest.mark.parametrize("dtype", FLOAT_DTYPES)
2778+
def test_accuracy_roll_with_non_dense_input(shape, dtype):
2779+
if len(shape) < 2:
2780+
pytest.skip("Need at least 2D for non-dense test")
2781+
2782+
shape_dilated = tuple(item * 2 for item in shape)
2783+
inp = torch.randn(shape_dilated, dtype=dtype, device=flag_gems.device)[::2, ::2]
2784+
ref_inp = to_reference(inp, False)
2785+
2786+
shifts = 2
2787+
dims = 0
2788+
2789+
ref_out = torch.roll(ref_inp, shifts, dims)
2790+
with flag_gems.use_gems():
2791+
res_out = torch.roll(inp, shifts, dims)
2792+
2793+
gems_assert_equal(res_out, ref_out)

0 commit comments

Comments
 (0)