Skip to content

Commit 8b4940b

Browse files
committed
[KernelGen][MThreads] Add bucketize Moore Threads specialized operator
1 parent ef794b3 commit 8b4940b

4 files changed

Lines changed: 159 additions & 2 deletions

File tree

benchmark/test_bucketize.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import pytest
1616
import torch
1717

18+
import flag_gems
19+
1820
from . import base, consts
1921

2022

@@ -26,10 +28,15 @@ def _input_fn(shape, cur_dtype, device):
2628

2729
@pytest.mark.bucketize
2830
def test_bucketize_perf():
31+
if flag_gems.vendor_name == "mthreads":
32+
dtypes = [torch.float32]
33+
else:
34+
dtypes = consts.FLOAT_DTYPES
35+
2936
bench = base.GenericBenchmark(
3037
op_name="bucketize",
3138
input_fn=_input_fn,
3239
torch_op=torch.bucketize,
33-
dtypes=consts.FLOAT_DTYPES,
40+
dtypes=dtypes,
3441
)
3542
bench.run()

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from .arange import arange, arange_start
2121
from .argmin import argmin
2222
from .batch_norm import batch_norm, batch_norm_backward
23+
from .bucketize import bucketize
2324
from .celu import celu
2425
from .conv2d import conv2d
2526
from .dropout import dropout, dropout_backward
@@ -76,6 +77,7 @@
7677
"argmin",
7778
"batch_norm",
7879
"batch_norm_backward",
80+
"bucketize",
7981
"celu",
8082
# "celu_",
8183
"conv2d",
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Copyright 2026 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+
import logging
16+
import math
17+
18+
import torch
19+
import triton
20+
import triton.language as tl
21+
22+
from flag_gems.ops.bucketize import bucketize as default_bucketize
23+
from flag_gems.runtime import torch_device_fn
24+
from flag_gems.utils import libentry
25+
from flag_gems.utils import triton_lang_extension as tle
26+
27+
logger = logging.getLogger(
28+
f'flag_gems.runtime.backend._mthreads.ops.{__name__.split(".")[-1]}'
29+
)
30+
31+
32+
@libentry()
33+
@triton.autotune(
34+
configs=[
35+
triton.Config({"BLOCK_SIZE": 512}, num_warps=4, num_stages=1),
36+
triton.Config({"BLOCK_SIZE": 1024}, num_warps=4, num_stages=1),
37+
triton.Config({"BLOCK_SIZE": 1024}, num_warps=8, num_stages=2),
38+
triton.Config({"BLOCK_SIZE": 2048}, num_warps=8, num_stages=1),
39+
triton.Config({"BLOCK_SIZE": 2048}, num_warps=8, num_stages=2),
40+
triton.Config({"BLOCK_SIZE": 4096}, num_warps=16, num_stages=1),
41+
],
42+
key=["n_elements", "n_boundaries"],
43+
)
44+
@triton.jit
45+
def bucketize_kernel(
46+
inp_ptr,
47+
boundaries_ptr,
48+
out_ptr,
49+
n_elements,
50+
n_boundaries,
51+
right: tl.constexpr,
52+
N_BOUNDARY_ITERS: tl.constexpr,
53+
BLOCK_SIZE: tl.constexpr,
54+
):
55+
pid = tle.program_id(0)
56+
block_start = pid * BLOCK_SIZE
57+
offsets = block_start + tl.arange(0, BLOCK_SIZE)
58+
mask = offsets < n_elements
59+
60+
inp_val = tl.load(inp_ptr + offsets, mask=mask, other=0)
61+
62+
# Vectorized binary search: each lane keeps its own [lo, hi) window and
63+
# narrows it over a fixed iteration count (ceil(log2(n_boundaries + 1))).
64+
lo = tl.zeros([BLOCK_SIZE], dtype=tl.int32)
65+
hi = tl.full([BLOCK_SIZE], n_boundaries, dtype=tl.int32)
66+
67+
for _ in range(N_BOUNDARY_ITERS):
68+
mid = tl.minimum((lo + hi) // 2, n_boundaries - 1)
69+
mid_val = tl.load(boundaries_ptr + mid)
70+
# right=True -> upper_bound (first boundary strictly greater than val)
71+
# right=False -> lower_bound (first boundary >= val)
72+
if right:
73+
cond = mid_val <= inp_val
74+
else:
75+
cond = mid_val < inp_val
76+
lo = tl.where(cond, mid + 1, lo)
77+
hi = tl.where(cond, hi, mid)
78+
79+
tl.store(out_ptr + offsets, lo, mask=mask)
80+
81+
82+
# Moore Threads hardware does not support fp64 compute. The specialized kernel
83+
# targets the real floating types; other dtypes / empty boundaries / dtype
84+
# mismatches defer to the generic implementation for correctness.
85+
_SUPPORTED_DTYPES = {torch.float16, torch.bfloat16, torch.float32}
86+
87+
88+
def _use_triton_kernel(inp, boundaries):
89+
if inp.device.type != "musa":
90+
return False
91+
if inp.dtype not in _SUPPORTED_DTYPES:
92+
return False
93+
if boundaries.numel() == 0:
94+
return False
95+
# Binary search compares input against boundaries; keep them the same
96+
# element type so the search is exact and no implicit promotion is needed.
97+
if boundaries.dtype != inp.dtype:
98+
return False
99+
return True
100+
101+
102+
def bucketize(input, boundaries, *, out_int32=False, right=False):
103+
logger.debug("GEMS_MTHREADS BUCKETIZE")
104+
105+
if not _use_triton_kernel(input, boundaries):
106+
return default_bucketize(input, boundaries, out_int32=out_int32, right=right)
107+
108+
output_dtype = torch.int32 if out_int32 else torch.int64
109+
110+
n_elements = input.numel()
111+
n_boundaries = boundaries.numel()
112+
search_iterations = math.ceil(math.log2(n_boundaries + 1))
113+
114+
# Allocate a contiguous flat output so kernel stores land in the returned
115+
# tensor regardless of the input's memory layout (empty_like would inherit a
116+
# non-contiguous layout and flatten() could then copy).
117+
input_flat = input.contiguous().flatten()
118+
output_flat = torch.empty(n_elements, dtype=output_dtype, device=input.device)
119+
boundaries = boundaries.contiguous()
120+
121+
grid = lambda meta: (triton.cdiv(n_elements, meta["BLOCK_SIZE"]),) # noqa: E731
122+
123+
with torch_device_fn.device(input.device):
124+
bucketize_kernel[grid](
125+
input_flat,
126+
boundaries,
127+
output_flat,
128+
n_elements,
129+
n_boundaries,
130+
right,
131+
search_iterations,
132+
)
133+
134+
return output_flat.reshape(input.shape)

tests/test_bucketize.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@
1919

2020
from . import accuracy_utils as utils
2121

22+
# Moore Threads (MUSA) has no native torch.bucketize with integer boundaries;
23+
# the reference call itself raises "Bucketize func unsupported!". Gate that one
24+
# parametrization for mthreads only (hardware limitation, not a kernel bug) so
25+
# other backends still exercise the int64-boundary path.
26+
_MTHREADS = flag_gems.vendor_name == "mthreads"
27+
2228

2329
def _reference_bucketize(inp, boundaries, **kwargs):
2430
ref_inp = utils.to_reference(inp, True)
@@ -76,7 +82,15 @@ def test_bucketize_int32(shape, dtype):
7682
@pytest.mark.parametrize(
7783
("boundary_values", "boundary_dtype"),
7884
[
79-
pytest.param([1, 3, 5, 7, 9], torch.int64, id="integer"),
85+
pytest.param(
86+
[1, 3, 5, 7, 9],
87+
torch.int64,
88+
id="integer",
89+
marks=pytest.mark.skipif(
90+
_MTHREADS,
91+
reason="MUSA native torch.bucketize does not support integer boundaries",
92+
),
93+
),
8094
pytest.param([], torch.float32, id="empty"),
8195
pytest.param([5.0], torch.float32, id="single"),
8296
pytest.param([1.0, 3.0], torch.float32, id="two"),

0 commit comments

Comments
 (0)