Skip to content

Commit abcd6ab

Browse files
chx7514Dongxu-H
andauthored
[KernelGen][thead] Add unsafe_masked_index_put_accumulate vendor specialization (#206)
Co-authored-by: Dongxu-H <dxhan@baai.ac.cn>
1 parent c06c77f commit abcd6ab

2 files changed

Lines changed: 260 additions & 0 deletions

File tree

src/flag_gems/runtime/backend/_thead/ops/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@
3030
from .special_shifted_chebyshev_polynomial_w import (
3131
special_shifted_chebyshev_polynomial_w,
3232
)
33+
from .unsafe_masked_index_put_accumulate import _unsafe_masked_index_put_accumulate
3334

3435
__all__ = [
3536
"_conv_depthwise2d",
3637
"_thnn_fused_lstm_cell_backward_impl",
38+
"_unsafe_masked_index_put_accumulate",
3739
"adaptive_max_pool3d_backward",
3840
"broadcast_tensors",
3941
"broadcast_to",
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
# Copyright 2026, The FlagOS Contributors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# Generated by KernelGen: https://github.com/flagos-ai/KernelGen
16+
17+
import logging
18+
19+
import triton
20+
import triton.language as tl
21+
22+
from flag_gems.utils import libentry
23+
24+
logger = logging.getLogger(__name__)
25+
26+
# Per-dimension index tensors are gathered into the kernel through a fixed set
27+
# of pointer arguments. Unused slots are padded and never read because RANK is
28+
# a compile-time constant that guards every access.
29+
_MAX_RANK = 8
30+
31+
32+
@libentry()
33+
@triton.jit
34+
def _masked_scatter_accumulate_flat_kernel(
35+
out_ptr,
36+
mask_ptr,
37+
indices_ptr,
38+
values_ptr,
39+
n_elements,
40+
out_numel,
41+
BLOCK_SIZE: tl.constexpr,
42+
):
43+
# Fast path: indices are already a single flat linear-index tensor, so the
44+
# kernel is a coalesced load + masked atomic-add with no index math.
45+
pid = tl.program_id(axis=0)
46+
offsets = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
47+
lane_mask = offsets < n_elements
48+
49+
mask_val = tl.load(mask_ptr + offsets, mask=lane_mask, other=0)
50+
idx = tl.load(indices_ptr + offsets, mask=lane_mask, other=0).to(tl.int64)
51+
vals = tl.load(values_ptr + offsets, mask=lane_mask, other=0)
52+
53+
index_valid = (idx >= 0) & (idx < out_numel)
54+
final_mask = lane_mask & (mask_val != 0) & index_valid
55+
56+
tl.atomic_add(out_ptr + idx, vals, mask=final_mask, sem="relaxed")
57+
58+
59+
@libentry()
60+
@triton.jit
61+
def _masked_scatter_accumulate_nd_kernel(
62+
out_ptr,
63+
mask_ptr,
64+
values_ptr,
65+
idx_ptr0,
66+
idx_ptr1,
67+
idx_ptr2,
68+
idx_ptr3,
69+
idx_ptr4,
70+
idx_ptr5,
71+
idx_ptr6,
72+
idx_ptr7,
73+
stride0,
74+
stride1,
75+
stride2,
76+
stride3,
77+
stride4,
78+
stride5,
79+
stride6,
80+
stride7,
81+
n_elements,
82+
out_numel,
83+
RANK: tl.constexpr,
84+
BLOCK_SIZE: tl.constexpr,
85+
):
86+
# Fused path: per-dimension index tensors are combined into a flat linear
87+
# index in-kernel, then accumulated. This removes the several separate
88+
# torch launches an equivalent host-side flatten would cost on the PPU.
89+
pid = tl.program_id(axis=0)
90+
offsets = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
91+
lane_mask = offsets < n_elements
92+
93+
idx = tl.load(idx_ptr0 + offsets, mask=lane_mask, other=0).to(tl.int64) * stride0
94+
if RANK > 1:
95+
idx += (
96+
tl.load(idx_ptr1 + offsets, mask=lane_mask, other=0).to(tl.int64) * stride1
97+
)
98+
if RANK > 2:
99+
idx += (
100+
tl.load(idx_ptr2 + offsets, mask=lane_mask, other=0).to(tl.int64) * stride2
101+
)
102+
if RANK > 3:
103+
idx += (
104+
tl.load(idx_ptr3 + offsets, mask=lane_mask, other=0).to(tl.int64) * stride3
105+
)
106+
if RANK > 4:
107+
idx += (
108+
tl.load(idx_ptr4 + offsets, mask=lane_mask, other=0).to(tl.int64) * stride4
109+
)
110+
if RANK > 5:
111+
idx += (
112+
tl.load(idx_ptr5 + offsets, mask=lane_mask, other=0).to(tl.int64) * stride5
113+
)
114+
if RANK > 6:
115+
idx += (
116+
tl.load(idx_ptr6 + offsets, mask=lane_mask, other=0).to(tl.int64) * stride6
117+
)
118+
if RANK > 7:
119+
idx += (
120+
tl.load(idx_ptr7 + offsets, mask=lane_mask, other=0).to(tl.int64) * stride7
121+
)
122+
123+
mask_val = tl.load(mask_ptr + offsets, mask=lane_mask, other=0)
124+
vals = tl.load(values_ptr + offsets, mask=lane_mask, other=0)
125+
126+
index_valid = (idx >= 0) & (idx < out_numel)
127+
final_mask = lane_mask & (mask_val != 0) & index_valid
128+
129+
tl.atomic_add(out_ptr + idx, vals, mask=final_mask, sem="relaxed")
130+
131+
132+
def _unsafe_masked_index_put_accumulate(inp, mask, indices, values):
133+
logger.debug("GEMS_HEAD UNSAFE_MASKED_INDEX_PUT_ACCUMULATE")
134+
135+
# Normalize the indices argument. torch passes Tensor?[] (a list/tuple of
136+
# per-dimension index tensors); the generic layer's internal calls pass a
137+
# single flat linear-index tensor.
138+
per_dim = None
139+
flat_indices = None
140+
if isinstance(indices, (list, tuple)):
141+
if len(indices) == 0 or indices[0] is None:
142+
raise ValueError("Empty indices list")
143+
if len(indices) == 1:
144+
flat_indices = indices[0]
145+
else:
146+
per_dim = list(indices)
147+
else:
148+
flat_indices = indices
149+
150+
ref_shape = mask.shape
151+
assert mask.shape == values.shape, (
152+
f"mask and values must have same shape, got {mask.shape} " f"and {values.shape}"
153+
)
154+
155+
# Co-locate everything on the value tensor's device.
156+
dev = values.device
157+
if inp.device != dev:
158+
inp = inp.to(dev)
159+
if mask.device != dev:
160+
mask = mask.to(dev)
161+
if values.device != dev:
162+
values = values.to(dev)
163+
164+
# Work on a contiguous copy so the flat linear index equals the memory
165+
# offset and the input is never mutated (out-of-place semantics).
166+
out = inp.contiguous()
167+
if out.data_ptr() == inp.data_ptr():
168+
out = out.clone()
169+
170+
n_elements = mask.numel()
171+
out_numel = out.numel()
172+
if n_elements == 0:
173+
return out
174+
175+
mask_flat = mask.contiguous().reshape(-1)
176+
values_flat = values.contiguous().reshape(-1)
177+
out_flat = out.reshape(-1)
178+
179+
# BLOCK_SIZE=256 / num_warps=1: tuned for the thead PPU scatter workload —
180+
# low launch overhead on the tiny shapes that dominate this op while giving
181+
# enough parallelism for the atomic accumulate on larger inputs.
182+
BLOCK_SIZE = 256
183+
grid = (triton.cdiv(n_elements, BLOCK_SIZE),)
184+
185+
if per_dim is not None:
186+
rank = len(per_dim)
187+
if rank > _MAX_RANK:
188+
raise ValueError(f"rank {rank} exceeds supported maximum {_MAX_RANK}")
189+
# Row-major logical strides of the input: stride[i] = prod(shape[i+1:]).
190+
logical_strides = [1] * rank
191+
acc = 1
192+
for i in range(rank - 1, -1, -1):
193+
logical_strides[i] = acc
194+
acc *= inp.shape[i]
195+
196+
idx_ptrs = []
197+
for t in per_dim:
198+
assert t.shape == ref_shape, (
199+
f"mask and indices must have same shape, got {ref_shape} "
200+
f"and {t.shape}"
201+
)
202+
it = t if t.device == dev else t.to(dev)
203+
idx_ptrs.append(it.contiguous().reshape(-1))
204+
# Pad pointer / stride slots up to _MAX_RANK; padded slots are guarded
205+
# out by the RANK constexpr and never dereferenced.
206+
pad_ptr = idx_ptrs[0]
207+
while len(idx_ptrs) < _MAX_RANK:
208+
idx_ptrs.append(pad_ptr)
209+
strides = logical_strides + [0] * (_MAX_RANK - rank)
210+
211+
_masked_scatter_accumulate_nd_kernel[grid](
212+
out_flat,
213+
mask_flat,
214+
values_flat,
215+
idx_ptrs[0],
216+
idx_ptrs[1],
217+
idx_ptrs[2],
218+
idx_ptrs[3],
219+
idx_ptrs[4],
220+
idx_ptrs[5],
221+
idx_ptrs[6],
222+
idx_ptrs[7],
223+
strides[0],
224+
strides[1],
225+
strides[2],
226+
strides[3],
227+
strides[4],
228+
strides[5],
229+
strides[6],
230+
strides[7],
231+
n_elements,
232+
out_numel,
233+
RANK=rank,
234+
BLOCK_SIZE=BLOCK_SIZE,
235+
num_warps=1,
236+
)
237+
return out
238+
239+
# Single flat-index path.
240+
assert flat_indices.shape == ref_shape, (
241+
f"mask and indices must have same shape, got {ref_shape} "
242+
f"and {flat_indices.shape}"
243+
)
244+
if flat_indices.device != dev:
245+
flat_indices = flat_indices.to(dev)
246+
indices_flat = flat_indices.contiguous().reshape(-1)
247+
248+
_masked_scatter_accumulate_flat_kernel[grid](
249+
out_flat,
250+
mask_flat,
251+
indices_flat,
252+
values_flat,
253+
n_elements,
254+
out_numel,
255+
BLOCK_SIZE=BLOCK_SIZE,
256+
num_warps=1,
257+
)
258+
return out

0 commit comments

Comments
 (0)