Skip to content

Commit 80c114f

Browse files
committed
feat: add bincount operator implementation, tests and benchmark
1 parent a267340 commit 80c114f

5 files changed

Lines changed: 335 additions & 0 deletions

File tree

benchmark/test_special_perf.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1535,3 +1535,81 @@ def unique_consecutive_input_fn(shape, dtype, device):
15351535
dtypes=INT_DTYPES,
15361536
)
15371537
bench.run()
1538+
1539+
1540+
class BincountBenchmark(Benchmark):
1541+
"""Benchmark for bincount operation."""
1542+
1543+
def __init__(self, op_name, torch_op, dtypes):
1544+
super().__init__(op_name=op_name, torch_op=torch_op, dtypes=dtypes)
1545+
1546+
def set_shapes(self, shape_file_path=None):
1547+
bincount_configs = [
1548+
(1000, 100),
1549+
(10000, 100),
1550+
(10000, 1000),
1551+
(100000, 100),
1552+
(100000, 1000),
1553+
(1000000, 100),
1554+
(1000000, 1000),
1555+
(1000000, 10000),
1556+
]
1557+
self.shapes = bincount_configs
1558+
1559+
def get_input_iter(self, cur_dtype):
1560+
for config in self.shapes:
1561+
yield from self.bincount_input_fn(config, cur_dtype, self.device)
1562+
1563+
def bincount_input_fn(self, config, dtype, device):
1564+
input_size, max_val = config
1565+
inp = torch.randint(0, max_val, (input_size,), dtype=torch.int64, device=device)
1566+
yield inp,
1567+
1568+
1569+
class BincountWeightsBenchmark(Benchmark):
1570+
"""Benchmark for bincount operation with weights."""
1571+
1572+
def __init__(self, op_name, torch_op, dtypes):
1573+
super().__init__(op_name=op_name, torch_op=torch_op, dtypes=dtypes)
1574+
1575+
def set_shapes(self, shape_file_path=None):
1576+
bincount_configs = [
1577+
(1000, 100),
1578+
(10000, 100),
1579+
(10000, 1000),
1580+
(100000, 100),
1581+
(100000, 1000),
1582+
(1000000, 100),
1583+
(1000000, 1000),
1584+
]
1585+
self.shapes = bincount_configs
1586+
1587+
def get_input_iter(self, cur_dtype):
1588+
for config in self.shapes:
1589+
yield from self.bincount_weights_input_fn(config, cur_dtype, self.device)
1590+
1591+
def bincount_weights_input_fn(self, config, dtype, device):
1592+
input_size, max_val = config
1593+
inp = torch.randint(0, max_val, (input_size,), dtype=torch.int64, device=device)
1594+
weights = torch.randn(input_size, dtype=dtype, device=device)
1595+
yield inp, {"weights": weights}
1596+
1597+
1598+
@pytest.mark.bincount
1599+
def test_perf_bincount():
1600+
bench = BincountBenchmark(
1601+
op_name="bincount",
1602+
torch_op=torch.bincount,
1603+
dtypes=[torch.int64],
1604+
)
1605+
bench.run()
1606+
1607+
1608+
@pytest.mark.bincount
1609+
def test_perf_bincount_with_weights():
1610+
bench = BincountWeightsBenchmark(
1611+
op_name="bincount_with_weights",
1612+
torch_op=torch.bincount,
1613+
dtypes=FLOAT_DTYPES,
1614+
)
1615+
bench.run()

src/flag_gems/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ def torch_ge(v):
115115
("bitwise_or_.Scalar", bitwise_or_scalar_),
116116
("bitwise_or_.Tensor", bitwise_or_tensor_),
117117
("bitwise_right_shift", bitwise_right_shift),
118+
("bincount", bincount),
118119
("bmm", bmm),
119120
("bmm.out", bmm_out),
120121
("cat", cat),

src/flag_gems/ops/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from flag_gems.ops.baddbmm import baddbmm
4343
from flag_gems.ops.batch_norm import batch_norm, batch_norm_backward
4444
from flag_gems.ops.bernoulli_ import bernoulli_
45+
from flag_gems.ops.bincount import bincount
4546
from flag_gems.ops.bitwise_and import (
4647
bitwise_and_scalar,
4748
bitwise_and_scalar_,
@@ -396,6 +397,7 @@
396397
"bitwise_or_tensor",
397398
"bitwise_or_tensor_",
398399
"bitwise_right_shift",
400+
"bincount",
399401
"bmm",
400402
"bmm_out",
401403
"cat",

src/flag_gems/ops/bincount.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import logging
2+
3+
import torch
4+
import triton
5+
import triton.language as tl
6+
7+
from ..utils import libentry
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
@libentry()
13+
@triton.jit
14+
def bincount_kernel(
15+
inp_ptr,
16+
out_ptr,
17+
N,
18+
BLOCK_SIZE: tl.constexpr,
19+
):
20+
"""Kernel for bincount without weights."""
21+
pid = tl.program_id(0)
22+
offsets = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
23+
mask = offsets < N
24+
25+
# Load input values (indices)
26+
indices = tl.load(inp_ptr + offsets, mask=mask, other=0)
27+
28+
# Atomic add 1 to the output at each index
29+
# Use int64 for the atomic add
30+
ones = tl.full((BLOCK_SIZE,), 1, dtype=tl.int64)
31+
tl.atomic_add(out_ptr + indices, ones, mask=mask, sem="relaxed")
32+
33+
34+
@libentry()
35+
@triton.jit
36+
def bincount_weights_kernel(
37+
inp_ptr,
38+
weights_ptr,
39+
out_ptr,
40+
N,
41+
BLOCK_SIZE: tl.constexpr,
42+
):
43+
"""Kernel for bincount with weights."""
44+
pid = tl.program_id(0)
45+
offsets = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
46+
mask = offsets < N
47+
48+
# Load input values (indices) and weights
49+
indices = tl.load(inp_ptr + offsets, mask=mask, other=0)
50+
weights = tl.load(weights_ptr + offsets, mask=mask, other=0.0)
51+
52+
# Atomic add weights to the output at each index
53+
tl.atomic_add(out_ptr + indices, weights, mask=mask, sem="relaxed")
54+
55+
56+
def bincount(inp, weights=None, minlength=0):
57+
"""
58+
Count the frequency of each value in an array of non-negative ints.
59+
60+
Args:
61+
inp: 1-d int tensor of non-negative integers
62+
weights: optional weights tensor of same size as inp
63+
minlength: optional minimum number of bins
64+
65+
Returns:
66+
Tensor of shape (max(inp) + 1,) or (minlength,) if minlength is larger
67+
"""
68+
logger.debug("GEMS BINCOUNT")
69+
70+
# Input validation
71+
assert inp.ndim == 1, "bincount only supports 1-d tensors"
72+
assert inp.dtype in (
73+
torch.int8,
74+
torch.int16,
75+
torch.int32,
76+
torch.int64,
77+
torch.uint8,
78+
), "bincount only supports integer tensors"
79+
80+
N = inp.numel()
81+
82+
# Handle empty input
83+
if N == 0:
84+
if weights is not None:
85+
return torch.zeros(minlength, dtype=weights.dtype, device=inp.device)
86+
return torch.zeros(minlength, dtype=torch.int64, device=inp.device)
87+
88+
# Compute output size
89+
max_val = int(inp.max().item())
90+
output_size = max(max_val + 1, minlength)
91+
92+
# Ensure input is contiguous
93+
inp = inp.contiguous()
94+
95+
if weights is not None:
96+
assert weights.shape == inp.shape, "weights must have same shape as input"
97+
weights = weights.contiguous()
98+
99+
# Output dtype matches weights dtype
100+
# For atomic_add compatibility, convert to float32 if float16/bfloat16
101+
weights_dtype = weights.dtype
102+
if weights_dtype in (torch.float16, torch.bfloat16):
103+
weights = weights.to(torch.float32)
104+
out = torch.zeros(output_size, dtype=torch.float32, device=inp.device)
105+
else:
106+
out = torch.zeros(output_size, dtype=weights.dtype, device=inp.device)
107+
108+
BLOCK_SIZE = 1024
109+
grid = (triton.cdiv(N, BLOCK_SIZE),)
110+
111+
bincount_weights_kernel[grid](
112+
inp,
113+
weights,
114+
out,
115+
N,
116+
BLOCK_SIZE=BLOCK_SIZE,
117+
)
118+
119+
# Convert back if needed
120+
if weights_dtype in (torch.float16, torch.bfloat16):
121+
out = out.to(weights_dtype)
122+
123+
return out
124+
else:
125+
# No weights: count occurrences
126+
out = torch.zeros(output_size, dtype=torch.int64, device=inp.device)
127+
128+
BLOCK_SIZE = 1024
129+
grid = (triton.cdiv(N, BLOCK_SIZE),)
130+
131+
bincount_kernel[grid](
132+
inp,
133+
out,
134+
N,
135+
BLOCK_SIZE=BLOCK_SIZE,
136+
)
137+
138+
return out

tests/test_bincount.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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+
BINCOUNT_SIZES = [16, 100, 1024, 10000] if not QUICK_MODE else [100, 1024]
10+
BINCOUNT_MAXVALS = [10, 100, 1000] if not QUICK_MODE else [100]
11+
12+
13+
@pytest.mark.bincount
14+
@pytest.mark.parametrize("size", BINCOUNT_SIZES)
15+
@pytest.mark.parametrize("max_val", BINCOUNT_MAXVALS)
16+
def test_accuracy_bincount(size, max_val):
17+
"""Test bincount without weights."""
18+
inp = torch.randint(0, max_val, (size,), dtype=torch.int64, device=flag_gems.device)
19+
ref_inp = utils.to_reference(inp)
20+
21+
ref_out = torch.bincount(ref_inp)
22+
with flag_gems.use_gems():
23+
res_out = torch.bincount(inp)
24+
25+
utils.gems_assert_equal(res_out, ref_out)
26+
27+
28+
@pytest.mark.bincount
29+
@pytest.mark.parametrize("size", BINCOUNT_SIZES)
30+
@pytest.mark.parametrize("max_val", BINCOUNT_MAXVALS)
31+
@pytest.mark.parametrize("dtype", utils.FLOAT_DTYPES)
32+
def test_accuracy_bincount_with_weights(size, max_val, dtype):
33+
"""Test bincount with weights."""
34+
inp = torch.randint(0, max_val, (size,), dtype=torch.int64, device=flag_gems.device)
35+
weights = torch.randn(size, dtype=dtype, device=flag_gems.device)
36+
ref_inp = utils.to_reference(inp)
37+
ref_weights = utils.to_reference(weights)
38+
39+
ref_out = torch.bincount(ref_inp, weights=ref_weights)
40+
with flag_gems.use_gems():
41+
res_out = torch.bincount(inp, weights=weights)
42+
43+
utils.gems_assert_close(res_out, ref_out, dtype)
44+
45+
46+
@pytest.mark.bincount
47+
@pytest.mark.parametrize("size", BINCOUNT_SIZES)
48+
@pytest.mark.parametrize("max_val", BINCOUNT_MAXVALS)
49+
@pytest.mark.parametrize("minlength", [0, 50, 2000])
50+
def test_accuracy_bincount_with_minlength(size, max_val, minlength):
51+
"""Test bincount with minlength parameter."""
52+
inp = torch.randint(0, max_val, (size,), dtype=torch.int64, device=flag_gems.device)
53+
ref_inp = utils.to_reference(inp)
54+
55+
ref_out = torch.bincount(ref_inp, minlength=minlength)
56+
with flag_gems.use_gems():
57+
res_out = torch.bincount(inp, minlength=minlength)
58+
59+
utils.gems_assert_equal(res_out, ref_out)
60+
61+
62+
@pytest.mark.bincount
63+
def test_accuracy_bincount_empty():
64+
"""Test bincount with empty input."""
65+
inp = torch.tensor([], dtype=torch.int64, device=flag_gems.device)
66+
ref_inp = utils.to_reference(inp)
67+
68+
ref_out = torch.bincount(ref_inp)
69+
with flag_gems.use_gems():
70+
res_out = torch.bincount(inp)
71+
72+
utils.gems_assert_equal(res_out, ref_out)
73+
74+
75+
@pytest.mark.bincount
76+
def test_accuracy_bincount_single():
77+
"""Test bincount with single element."""
78+
inp = torch.tensor([5], dtype=torch.int64, device=flag_gems.device)
79+
ref_inp = utils.to_reference(inp)
80+
81+
ref_out = torch.bincount(ref_inp)
82+
with flag_gems.use_gems():
83+
res_out = torch.bincount(inp)
84+
85+
utils.gems_assert_equal(res_out, ref_out)
86+
87+
88+
@pytest.mark.bincount
89+
def test_accuracy_bincount_all_zeros():
90+
"""Test bincount with all zeros."""
91+
inp = torch.zeros(100, dtype=torch.int64, device=flag_gems.device)
92+
ref_inp = utils.to_reference(inp)
93+
94+
ref_out = torch.bincount(ref_inp)
95+
with flag_gems.use_gems():
96+
res_out = torch.bincount(inp)
97+
98+
utils.gems_assert_equal(res_out, ref_out)
99+
100+
101+
@pytest.mark.bincount
102+
@pytest.mark.parametrize("dtype", utils.FLOAT_DTYPES)
103+
def test_accuracy_bincount_weights_edge_cases(dtype):
104+
"""Test bincount with edge case weights."""
105+
inp = torch.tensor([0, 1, 2, 1, 0], dtype=torch.int64, device=flag_gems.device)
106+
weights = torch.tensor(
107+
[1.0, 2.0, 3.0, 4.0, 5.0], dtype=dtype, device=flag_gems.device
108+
)
109+
ref_inp = utils.to_reference(inp)
110+
ref_weights = utils.to_reference(weights)
111+
112+
ref_out = torch.bincount(ref_inp, weights=ref_weights)
113+
with flag_gems.use_gems():
114+
res_out = torch.bincount(inp, weights=weights)
115+
116+
utils.gems_assert_close(res_out, ref_out, dtype)

0 commit comments

Comments
 (0)