Skip to content

Commit 0b5f06e

Browse files
committed
feat: add index_copy_ operator with tests and benchmark
1 parent 6c4621a commit 0b5f06e

5 files changed

Lines changed: 396 additions & 0 deletions

File tree

benchmark/test_index_copy_perf.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import pytest
2+
import torch
3+
4+
from benchmark.performance_utils import GenericBenchmark2DOnly
5+
from flag_gems.utils import shape_utils
6+
7+
8+
def index_copy_gbps(bench_fn_args, latency):
9+
index = bench_fn_args[2]
10+
src = bench_fn_args[3]
11+
io_amount = sum([shape_utils.size_in_bytes(item) for item in [index, src, src]])
12+
return io_amount * 1e-9 / (latency * 1e-3)
13+
14+
15+
def index_copy_input_fn(shape, dtype, device):
16+
inp = torch.randn(shape, dtype=dtype, device=device)
17+
dim = 0 if len(shape) == 1 else 1
18+
src_shape = list(inp.shape)
19+
index_max = src_shape[dim]
20+
index_len = index_max // 2 if index_max >= 2 else 1
21+
index = torch.randperm(index_len, device=device)
22+
src_shape[dim] = index_len
23+
src = torch.randn(src_shape, dtype=dtype, device=device)
24+
yield inp, dim, index, src
25+
26+
27+
@pytest.mark.index_copy
28+
def test_index_copy():
29+
bench = GenericBenchmark2DOnly(
30+
op_name="index_copy",
31+
torch_op=torch.index_copy,
32+
input_fn=index_copy_input_fn,
33+
dtypes=[torch.float16, torch.float32, torch.bfloat16],
34+
get_gbps=index_copy_gbps,
35+
)
36+
bench.run()
37+
38+
39+
@pytest.mark.index_copy_
40+
def test_index_copy_():
41+
bench = GenericBenchmark2DOnly(
42+
op_name="index_copy_",
43+
torch_op=torch.Tensor.index_copy_,
44+
input_fn=index_copy_input_fn,
45+
dtypes=[torch.float16, torch.float32, torch.bfloat16],
46+
get_gbps=index_copy_gbps,
47+
inplace=True,
48+
)
49+
bench.run()

src/flag_gems/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,8 @@ def torch_ge(v):
259259
("index.Tensor", index),
260260
("index_add", index_add),
261261
("index_add_", index_add_),
262+
("index_copy", index_copy),
263+
("index_copy_", index_copy_),
262264
("index_put", index_put),
263265
("index_put_", index_put_),
264266
("index_select", index_select),

src/flag_gems/ops/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@
157157
from flag_gems.ops.i0_ import i0_
158158
from flag_gems.ops.index import index
159159
from flag_gems.ops.index_add import index_add, index_add_
160+
from flag_gems.ops.index_copy_ import index_copy, index_copy_
160161
from flag_gems.ops.index_put import _index_put_impl_, index_put, index_put_
161162
from flag_gems.ops.index_select import index_select
162163
from flag_gems.ops.isclose import allclose, isclose
@@ -552,6 +553,8 @@
552553
"index",
553554
"index_add",
554555
"index_add_",
556+
"index_copy",
557+
"index_copy_",
555558
"index_put",
556559
"index_put_",
557560
"index_select",

src/flag_gems/ops/index_copy_.py

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
import importlib
2+
import logging
3+
import os
4+
from typing import Any, Callable, List, Mapping, Tuple
5+
6+
import torch
7+
8+
from flag_gems.utils.code_cache import code_cache_dir
9+
from flag_gems.utils.code_utils import IndentedBuffer
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
def generate_imports(code: IndentedBuffer) -> IndentedBuffer:
15+
code.writeline("import triton")
16+
code.writeline("import triton.language as tl")
17+
code.writeline("from flag_gems.utils import libentry")
18+
19+
code.newline()
20+
code.newline()
21+
22+
return code
23+
24+
25+
def generate_index_copy_kernel(
26+
rank: int,
27+
kernel_name: str,
28+
code: IndentedBuffer,
29+
) -> IndentedBuffer:
30+
# the decorators
31+
code.writeline("@libentry()")
32+
code.writeline("@triton.jit")
33+
34+
# signature
35+
code.writeline(f"def {kernel_name}(")
36+
with code.indent():
37+
if rank > 0:
38+
code.writeline("index,")
39+
code.writeline("src,")
40+
code.writeline("out,")
41+
code.writeline("N,")
42+
code.writeline("inp_numel,")
43+
code.writeline("inp_stride_dim,")
44+
code.writeline("inp_shape_dim,")
45+
code.writeline("src_shape_dim,")
46+
code.writeline("delta,")
47+
48+
stride_args = ", ".join(f"src_stride_{i}: int" for i in range(rank))
49+
code.writeline(f"{stride_args}, # stride for src")
50+
51+
shape_args = ", ".join(f"src_shape_{i}: int" for i in range(rank))
52+
code.writeline(f"{shape_args}, # shape for src")
53+
54+
code.writeline("BLOCK_SIZE: tl.constexpr,")
55+
56+
code.writeline("):")
57+
58+
# Kernel Code
59+
with code.indent():
60+
code.writeline("pid = tl.program_id(axis=0)")
61+
code.writeline("offsets = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)")
62+
code.writeline("mask = offsets < N")
63+
64+
for i in range(rank - 1, -1, -1):
65+
code.writeline(f"src_offset{i} = offsets % src_shape_{i}")
66+
code.writeline(f"offsets = offsets // src_shape_{i}")
67+
code.newline()
68+
comp = [f"src_offset{i} * src_stride_{i}" for i in range(rank)]
69+
code.writeline(f"src_offset = {' + '.join(comp)}")
70+
71+
code.writeline("pre_cal = (inp_stride_dim * src_shape_dim)")
72+
73+
# index copy
74+
code.writeline("pre_idx = (src_offset // pre_cal).to(tl.int64)")
75+
code.writeline(
76+
"dim_idx = (src_offset % pre_cal // inp_stride_dim).to(tl.int64)"
77+
)
78+
code.writeline(
79+
"src_dim_idx = (tl.load(index + dim_idx, mask=mask, other=0)).to(tl.int64)"
80+
)
81+
code.writeline(
82+
'assert src_dim_idx >= 0 and src_dim_idx < inp_shape_dim, "0 <= index < self.size(dim)"'
83+
)
84+
code.writeline(
85+
"input_idx = (src_offset + (delta * pre_idx + src_dim_idx - dim_idx) * inp_stride_dim).to(tl.int64)"
86+
)
87+
88+
code.writeline("input_mask = (input_idx >= 0) & (input_idx < inp_numel)")
89+
code.writeline("store_mask = mask & input_mask")
90+
code.writeline("src_val = tl.load(src + src_offset, mask=mask, other=0)")
91+
code.writeline("tl.store(out + input_idx, src_val, mask=store_mask)")
92+
93+
code.newline()
94+
code.newline()
95+
return code
96+
97+
98+
def parameter_for_wrapper() -> str:
99+
# out, index, src, dim, inp_stride_dim, src_shape_dim, delta, N, inp.numel()
100+
parameters: List[str] = []
101+
parameters.append("out")
102+
parameters.append("index")
103+
parameters.append("src")
104+
parameters.append("dim")
105+
parameters.append("inp_stride_dim")
106+
parameters.append("inp_shape_dim")
107+
parameters.append("src_shape_dim")
108+
parameters.append("delta")
109+
parameters.append("N")
110+
parameters.append("inp_numel")
111+
112+
return ", ".join(parameters)
113+
114+
115+
def generate_destination_passing_wrapper(
116+
rank: int,
117+
wrapper_name: str,
118+
kernel_name: str,
119+
code: IndentedBuffer,
120+
) -> IndentedBuffer:
121+
parameters: str = parameter_for_wrapper()
122+
wrapper_signature: str = f"def {wrapper_name} ({parameters}):"
123+
code.writeline(wrapper_signature)
124+
125+
with code.indent():
126+
code.writeline("src_strides = list(src.stride())")
127+
code.writeline("src_shapes = list(src.shape)")
128+
129+
# kernel launch
130+
code.writeline("BLOCK_SIZE = 128") # BLOCK_SIZE setting
131+
code.writeline("grid = (triton.cdiv(N, BLOCK_SIZE),)")
132+
kernel_launch: str = f"{kernel_name}[grid]("
133+
code.writeline(kernel_launch)
134+
with code.indent():
135+
code.writeline(
136+
"index, src, out, N, inp_numel, inp_stride_dim, inp_shape_dim, src_shape_dim, delta, "
137+
)
138+
if rank > 0:
139+
s = ", ".join(f"src_strides[{i}]" for i in range(rank))
140+
code.writeline(f"{s},")
141+
142+
s = ", ".join(f"src_shapes[{i}]" for i in range(rank))
143+
code.writeline(f"{s},")
144+
code.writeline("BLOCK_SIZE=BLOCK_SIZE")
145+
code.writeline(")")
146+
code.writeline("return out")
147+
148+
return code
149+
150+
151+
def generate_code(
152+
inputs: Tuple[Any],
153+
wrapper_name: str,
154+
kernel_name: str,
155+
code: IndentedBuffer,
156+
) -> IndentedBuffer:
157+
# inputs: [out, index, src, dim, inp_stride_dim, inp_shape_dim, src_shape_dim, delta, N, inp.numel()]
158+
shape = inputs[2].shape
159+
rank = len(shape)
160+
161+
code = generate_imports(code)
162+
code = generate_index_copy_kernel(rank, kernel_name, code)
163+
code = generate_destination_passing_wrapper(rank, wrapper_name, kernel_name, code)
164+
return code
165+
166+
167+
class IndexCopyFunction:
168+
def __init__(self):
169+
self.pid = os.getpid()
170+
self.overloads: Mapping[str, Callable] = {}
171+
172+
def __call__(self, *args, **kwargs):
173+
key = f"{self.arg_key(*args)}"
174+
if key in self.overloads:
175+
overload = self.overloads[key]
176+
else:
177+
code = IndentedBuffer()
178+
code = generate_code(
179+
args,
180+
"_index_copy_wrapper",
181+
"_index_copy_jit_function",
182+
code,
183+
)
184+
185+
file_name = f"index_copy_rank_{key}_pid_{self.pid}.py"
186+
187+
with open(code_cache_dir() / file_name, "wt", encoding="utf-8") as f:
188+
f.write(code.getvalue())
189+
190+
# load
191+
spec = importlib.util.spec_from_file_location(
192+
f"_gen_module_rank_{key}_pid_{self.pid}",
193+
f.name,
194+
)
195+
196+
m = importlib.util.module_from_spec(spec)
197+
spec.loader.exec_module(m)
198+
overload = getattr(m, "_index_copy_wrapper")
199+
self.overloads[key] = overload
200+
201+
return overload(*args, **kwargs)
202+
203+
def arg_key(self, *args):
204+
tensors = [item for item in args if torch.is_tensor(item)]
205+
max_rank = max(item.ndim for item in tensors)
206+
return max_rank
207+
208+
209+
_index_copy_func = IndexCopyFunction()
210+
211+
212+
_FALLBACK_KEYSET = torch._C.DispatchKeySet(
213+
torch._C.DispatchKey.CompositeExplicitAutograd
214+
)
215+
216+
217+
def index_copy(inp, dim, index, src):
218+
logger.debug("GEMS INDEX_COPY")
219+
assert ((0 <= index) * (index < inp.size(dim))).equal(
220+
torch.ones(tuple(index.shape), dtype=torch.bool, device=inp.device)
221+
), "0 <= index < self.size(dim)"
222+
assert dim >= -inp.ndim and dim < inp.ndim, "Invalid dim"
223+
assert index.numel() == src.size(
224+
dim
225+
), "The dimth dimension of source must have the same size as the length of index"
226+
assert (
227+
inp.ndim == src.ndim
228+
), "Self and source should have the same number of dimensions"
229+
assert all(
230+
(inp.size(i) == src.size(i)) or i == dim for i in range(0, inp.ndim)
231+
), "src.size(d) == self.size(d) for all dimensions d != dim"
232+
233+
# Use native clone to avoid potential issues with FlagGems copy_ dispatch
234+
out = torch.ops.aten.clone.default.redispatch(_FALLBACK_KEYSET, inp)
235+
236+
dim %= inp.ndim
237+
inp_stride_dim = inp.stride(dim)
238+
src_shape_dim = src.size(dim)
239+
inp_shape_dim = inp.size(dim)
240+
delta = inp.size(dim) - src_shape_dim
241+
N = src.numel()
242+
243+
_index_copy_func(
244+
out,
245+
index,
246+
src,
247+
dim,
248+
inp_stride_dim,
249+
inp_shape_dim,
250+
src_shape_dim,
251+
delta,
252+
N,
253+
inp.numel(),
254+
)
255+
return out
256+
257+
258+
def index_copy_(inp, dim, index, src):
259+
logger.debug("GEMS INDEX_COPY_")
260+
assert ((0 <= index) * (index < inp.size(dim))).equal(
261+
torch.ones(tuple(index.shape), dtype=torch.bool, device=inp.device)
262+
), "0 <= index < self.size(dim)"
263+
assert dim >= -inp.ndim and dim < inp.ndim, "Invalid dim"
264+
assert index.numel() == src.size(
265+
dim
266+
), "The dimth dimension of source must have the same size as the length of index"
267+
assert (
268+
inp.ndim == src.ndim
269+
), "Self and source should have the same number of dimensions"
270+
assert all(
271+
(inp.size(i) == src.size(i)) or i == dim for i in range(0, inp.ndim)
272+
), "src.size(d) == self.size(d) for all dimensions d != dim"
273+
274+
dim %= inp.ndim
275+
inp_stride_dim = inp.stride(dim)
276+
src_shape_dim = src.size(dim)
277+
inp_shape_dim = inp.size(dim)
278+
delta = inp.size(dim) - src_shape_dim
279+
N = src.numel()
280+
281+
_index_copy_func(
282+
inp,
283+
index,
284+
src,
285+
dim,
286+
inp_stride_dim,
287+
inp_shape_dim,
288+
src_shape_dim,
289+
delta,
290+
N,
291+
inp.numel(),
292+
)
293+
return inp

0 commit comments

Comments
 (0)