Skip to content

Commit 935d2f4

Browse files
Schopenhauer-loves-Hegelfactnnclaude
authored
【KernelGen】Add histc operator (flagos-ai#1742)
* feat: add histc operator implementation, tests and benchmark Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: rename benchmark file, update to new format --------- Co-authored-by: factnn <1050552884@qq.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b2a132d commit 935d2f4

5 files changed

Lines changed: 234 additions & 0 deletions

File tree

benchmark/test_histc.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import pytest
2+
import torch
3+
4+
from . import base
5+
6+
7+
def _input_fn(shape, dtype, device):
8+
inp = torch.rand(shape, dtype=dtype, device=device) * 10
9+
yield inp, {"bins": 100, "min": 0, "max": 10}
10+
11+
12+
@pytest.mark.histc
13+
def test_histc():
14+
bench = base.GenericBenchmark2DOnly(
15+
input_fn=_input_fn,
16+
op_name="histc",
17+
torch_op=torch.histc,
18+
dtypes=[torch.float32],
19+
)
20+
bench.run()

src/flag_gems/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ def torch_ge(v):
256256
("hardsigmoid", hardsigmoid),
257257
("hardsigmoid.out", hardsigmoid_out),
258258
("hardswish_", hardswish_),
259+
("histc", histc),
259260
("hstack", hstack),
260261
("hypot", hypot),
261262
("i0", i0),

src/flag_gems/ops/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@
155155
from flag_gems.ops.hadamard_transform import hadamard_transform
156156
from flag_gems.ops.hardsigmoid import hardsigmoid, hardsigmoid_out
157157
from flag_gems.ops.hardswish_ import hardswish_
158+
from flag_gems.ops.histc import histc
158159
from flag_gems.ops.hstack import hstack
159160
from flag_gems.ops.hypot import hypot, hypot_out
160161
from flag_gems.ops.i0 import i0, i0_out
@@ -555,6 +556,7 @@
555556
"hardsigmoid",
556557
"hardsigmoid_out",
557558
"hardswish_",
559+
"histc",
558560
"hstack",
559561
"hypot",
560562
"hypot_out",

src/flag_gems/ops/histc.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import logging
2+
3+
import torch
4+
import triton
5+
import triton.language as tl
6+
7+
from flag_gems.runtime import torch_device_fn
8+
from flag_gems.utils import libentry
9+
from flag_gems.utils import triton_lang_extension as tle
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
@libentry()
15+
@triton.jit
16+
def histc_kernel(
17+
inp_ptr,
18+
out_ptr,
19+
n_elements,
20+
bins: tl.constexpr,
21+
min_val,
22+
max_val,
23+
BLOCK_SIZE: tl.constexpr,
24+
):
25+
"""
26+
Compute histogram of input tensor.
27+
Each thread processes BLOCK_SIZE elements, computing which bin they belong to
28+
and atomically incrementing the corresponding bin counter.
29+
"""
30+
pid = tle.program_id(0)
31+
offset = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
32+
mask = offset < n_elements
33+
34+
# Load input values
35+
inp_val = tl.load(inp_ptr + offset, mask=mask, other=0.0)
36+
37+
# Convert to float32 for computation
38+
inp_val = inp_val.to(tl.float32)
39+
40+
# Compute bin range
41+
bin_width = (max_val - min_val) / bins
42+
43+
# Compute bin indices
44+
# Elements equal to max_val go to the last bin (bins - 1)
45+
# Elements outside [min_val, max_val] or NaN are ignored
46+
bin_idx = ((inp_val - min_val) / bin_width).to(tl.int32)
47+
48+
# Clamp to valid range [0, bins-1] for elements in range
49+
# Elements outside range or NaN should be excluded
50+
in_range = (inp_val >= min_val) & (inp_val <= max_val)
51+
52+
# Handle edge case: elements exactly equal to max go to last bin
53+
bin_idx = tl.where(inp_val == max_val, bins - 1, bin_idx)
54+
bin_idx = tl.where(bin_idx < 0, 0, bin_idx)
55+
bin_idx = tl.where(bin_idx >= bins, bins - 1, bin_idx)
56+
57+
# Only count elements in range (excludes NaN via the comparison)
58+
valid_mask = mask & in_range
59+
60+
# Atomic add to histogram bins
61+
# We need to iterate through each element and add to the appropriate bin
62+
for i in range(BLOCK_SIZE):
63+
if tl.load(valid_mask.to(tl.int8).reshape(BLOCK_SIZE) + i) != 0:
64+
idx = tl.load(bin_idx.reshape(BLOCK_SIZE) + i)
65+
tl.atomic_add(out_ptr + idx, 1.0, sem="relaxed")
66+
67+
68+
@libentry()
69+
@triton.jit
70+
def histc_kernel_simple(
71+
inp_ptr,
72+
out_ptr,
73+
n_elements,
74+
bins,
75+
min_val,
76+
max_val,
77+
BLOCK_SIZE: tl.constexpr,
78+
):
79+
"""
80+
Simple histogram kernel - each program handles one element at a time.
81+
"""
82+
pid = tle.program_id(0)
83+
offset = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
84+
mask = offset < n_elements
85+
86+
# Load input values
87+
inp_val = tl.load(inp_ptr + offset, mask=mask, other=float("nan"))
88+
89+
# Convert to float32 for computation
90+
inp_val = inp_val.to(tl.float32)
91+
92+
# Compute bin width
93+
bin_width = (max_val - min_val) / bins
94+
95+
# Compute bin indices
96+
bin_idx = ((inp_val - min_val) / bin_width).to(tl.int64)
97+
98+
# Handle edge case: elements exactly equal to max go to last bin
99+
bin_idx = tl.where(inp_val == max_val, bins - 1, bin_idx)
100+
101+
# Check if elements are in valid range (excludes NaN)
102+
in_range = (inp_val >= min_val) & (inp_val <= max_val)
103+
104+
# Clamp bin indices to valid range
105+
bin_idx = tl.where(bin_idx < 0, 0, bin_idx)
106+
bin_idx = tl.where(bin_idx >= bins, bins - 1, bin_idx)
107+
108+
valid_mask = mask & in_range
109+
110+
# Atomically add to histogram
111+
tl.atomic_add(out_ptr + bin_idx, 1.0, mask=valid_mask, sem="relaxed")
112+
113+
114+
def histc(inp, bins=100, min=0, max=0):
115+
"""
116+
Compute the histogram of a tensor.
117+
118+
Args:
119+
inp: Input tensor
120+
bins: Number of histogram bins (default: 100)
121+
min: Lower end of the range (inclusive). If min == max == 0, uses data min.
122+
max: Upper end of the range (inclusive). If min == max == 0, uses data max.
123+
124+
Returns:
125+
Tensor: Histogram represented as a tensor of shape (bins,)
126+
"""
127+
logger.debug("GEMS HISTC")
128+
129+
# Ensure input is contiguous
130+
inp = inp.contiguous()
131+
132+
# Get min and max values
133+
min_val = float(min)
134+
max_val = float(max)
135+
136+
if min_val == 0 and max_val == 0:
137+
# Use actual min/max of the data
138+
min_val = float(inp.min().item())
139+
max_val = float(inp.max().item())
140+
141+
# Handle edge case where min == max
142+
if min_val == max_val:
143+
# All elements go to the first bin if they equal min_val
144+
out = torch.zeros(bins, dtype=inp.dtype, device=inp.device)
145+
# Count how many elements equal min_val (excluding NaN)
146+
count = ((inp == min_val) & ~torch.isnan(inp)).sum().item()
147+
out[0] = count
148+
return out
149+
150+
# Create output histogram tensor
151+
out = torch.zeros(bins, dtype=inp.dtype, device=inp.device)
152+
153+
n_elements = inp.numel()
154+
155+
if n_elements == 0:
156+
return out
157+
158+
# Choose block size
159+
BLOCK_SIZE = 1024
160+
161+
# Calculate grid size
162+
grid = (triton.cdiv(n_elements, BLOCK_SIZE),)
163+
164+
with torch_device_fn.device(inp.device):
165+
histc_kernel_simple[grid](
166+
inp,
167+
out,
168+
n_elements,
169+
bins,
170+
min_val,
171+
max_val,
172+
BLOCK_SIZE=BLOCK_SIZE,
173+
)
174+
175+
return out

tests/test_histc.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import pytest
2+
import torch
3+
4+
import flag_gems
5+
6+
from .accuracy_utils import gems_assert_close, to_reference
7+
8+
HISTC_SHAPES = [(64,), (1024,), (4096,), (100, 100), (32, 64, 16)]
9+
HISTC_BINS = [10, 50, 100]
10+
HISTC_DTYPES = [torch.float32]
11+
12+
13+
@pytest.mark.histc
14+
@pytest.mark.parametrize("shape", HISTC_SHAPES)
15+
@pytest.mark.parametrize("bins", HISTC_BINS)
16+
@pytest.mark.parametrize("dtype", HISTC_DTYPES)
17+
def test_accuracy_histc(shape, bins, dtype):
18+
inp = torch.rand(shape, dtype=dtype, device=flag_gems.device) * 10
19+
ref_inp = to_reference(inp)
20+
ref_out = torch.histc(ref_inp, bins=bins, min=0, max=0)
21+
with flag_gems.use_gems():
22+
res_out = torch.histc(inp, bins=bins, min=0, max=0)
23+
gems_assert_close(res_out, ref_out, dtype)
24+
25+
26+
@pytest.mark.histc
27+
@pytest.mark.parametrize("shape", HISTC_SHAPES)
28+
@pytest.mark.parametrize("bins", HISTC_BINS)
29+
@pytest.mark.parametrize("dtype", HISTC_DTYPES)
30+
def test_accuracy_histc_with_range(shape, bins, dtype):
31+
inp = torch.rand(shape, dtype=dtype, device=flag_gems.device) * 20 - 5
32+
ref_inp = to_reference(inp)
33+
ref_out = torch.histc(ref_inp, bins=bins, min=0, max=10)
34+
with flag_gems.use_gems():
35+
res_out = torch.histc(inp, bins=bins, min=0, max=10)
36+
gems_assert_close(res_out, ref_out, dtype)

0 commit comments

Comments
 (0)