Skip to content

Commit 1cd434c

Browse files
authored
[KernelGen][Metax] Add special_gammainc operator for Metax backend (#98)
Co-authored-by: yzw1128 <yzw1128@users.noreply.github.com>
1 parent fb9d63d commit 1cd434c

4 files changed

Lines changed: 187 additions & 0 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
from .repeat_interleave import repeat_interleave_self_tensor
4545
from .resolve_conj import resolve_conj
4646
from .sigmoid import sigmoid
47+
from .special_gammainc import special_gammainc
4748
from .tanh import tanh
4849
from .unique import _unique2
4950
from .upsample_nearest2d import upsample_nearest2d
@@ -95,6 +96,7 @@
9596
"repeat_interleave_self_tensor",
9697
"resolve_conj",
9798
"sigmoid",
99+
"special_gammainc",
98100
"tanh",
99101
"upsample_nearest2d",
100102
"zeros",

src/flag_gems/runtime/backend/_metax/ops/lt_.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"""Metax-specific inplace wrapper for lt_ and lt_scalar_ with fused kernels
1717
that avoid uitofp i1->bf16 by using tl.where to produce 1.0/0.0 float
1818
values."""
19+
1920
import logging
2021

2122
import triton

src/flag_gems/runtime/backend/_metax/ops/masked_scatter.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
inter-block synchronization. num_warps is also made device-aware to
2626
respect Metax's 512-thread-per-block limit (warp_size=64 -> max 8 warps).
2727
"""
28+
2829
import logging
2930

3031
import torch
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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+
import logging
17+
18+
import torch
19+
import triton
20+
import triton.language as tl
21+
22+
import flag_gems
23+
from flag_gems.utils import tl_extra_shim
24+
25+
logger = logging.getLogger(__name__)
26+
27+
28+
@triton.jit
29+
def gammainc_kernel_metax(a_ptr, x_ptr, out_ptr, n_elements, BLOCK_SIZE: tl.constexpr):
30+
pid = tl.program_id(axis=0)
31+
block_start = pid * BLOCK_SIZE
32+
offsets = block_start + tl.arange(0, BLOCK_SIZE)
33+
mask = offsets < n_elements
34+
35+
a = tl.load(a_ptr + offsets, mask=mask, other=0.0)
36+
x = tl.load(x_ptr + offsets, mask=mask, other=0.0)
37+
38+
# Compute in float32 for better precision
39+
a_f32 = a.to(tl.float32)
40+
x_f32 = x.to(tl.float32)
41+
42+
# Handle edge cases
43+
# P(a, 0) = 0 for a > 0; NaN for a <= 0 or x < 0
44+
result = tl.where((a_f32 > 0.0) & (x_f32 >= 0.0), 0.0, float("nan"))
45+
46+
# Determine which method to use based on x and a
47+
# Series expansion is better when x < a + 1
48+
use_series = x_f32 < (a_f32 + 1.0)
49+
50+
# =========================================================================
51+
# Series expansion: P(a, x) = exp(-x) * x^a * sum_{n=0} x^n / Gamma(a+n+1)
52+
# Term recurrence: term_0 = 1/a, term_n = term_{n-1} * x / (a + n)
53+
# Use tl.where to zero out the term once converged, so subsequent
54+
# multiplications produce 0 and the sum is not affected.
55+
# 60 iterations is sufficient for float32 (machine eps ~1.2e-7).
56+
# =========================================================================
57+
series_sum = 0.0
58+
term = 1.0 / a_f32
59+
series_sum = term
60+
for i in range(1, 60):
61+
term = term * x_f32 / (a_f32 + tl.cast(i, tl.float32))
62+
# Zero out converged lanes to avoid unnecessary accumulation
63+
converged = tl.abs(term) < tl.abs(series_sum) * 1e-8
64+
term = tl.where(converged, 0.0, term)
65+
series_sum = series_sum + term
66+
67+
# Divide by Gamma(a) to get the regularized value P(a, x)
68+
log_gamma_a = tl_extra_shim.lgamma(a_f32)
69+
series_result = tl.exp(-x_f32 + a_f32 * tl.log(x_f32) - log_gamma_a) * series_sum
70+
71+
# =========================================================================
72+
# Lentz's continued fraction for Q(a,x) = Gamma(a,x)/Gamma(a)
73+
# for the large-x regime (x >= a + 1).
74+
#
75+
# CF = b_0 + a_1/(b_1 + a_2/(b_2 + ...))
76+
# b_0 = x + 1 - a
77+
# a_n = n(a - n), b_n = x + 2n + 1 - a (n >= 1)
78+
# Then Q = e^{-x} * x^a / (Gamma(a) * CF) and P = 1 - Q.
79+
#
80+
# 100 iterations is sufficient for float32 convergence.
81+
# Once |delta - 1| < eps, freeze delta at 1.0 so f_val stops changing.
82+
# =========================================================================
83+
tiny = 1e-30
84+
b0 = x_f32 + 1.0 - a_f32
85+
f_val = b0
86+
C_val = b0
87+
D_val = 0.0 * x_f32
88+
for i_val in range(1, 100):
89+
i_f = tl.cast(i_val, tl.float32)
90+
an = i_f * (a_f32 - i_f)
91+
bn = x_f32 + 2.0 * i_f + 1.0 - a_f32
92+
93+
D_val = bn + an * D_val
94+
D_val = tl.where(tl.abs(D_val) < tiny, tiny, D_val)
95+
96+
C_val = bn + an / C_val
97+
C_val = tl.where(tl.abs(C_val) < tiny, tiny, C_val)
98+
99+
D_val = 1.0 / D_val
100+
delta = C_val * D_val
101+
# Freeze converged lanes: once delta ~ 1, multiplying by 1 is a no-op
102+
cf_converged = tl.abs(delta - 1.0) < 1e-8
103+
delta = tl.where(cf_converged, 1.0, delta)
104+
f_val = f_val * delta
105+
106+
log_gamma_a2 = tl_extra_shim.lgamma(a_f32)
107+
log_q = a_f32 * tl.log(x_f32) - x_f32 - log_gamma_a2 - tl.log(f_val)
108+
q_val = tl.exp(log_q)
109+
q_val = tl.where(q_val > 1.0, 1.0, tl.where(q_val < 0.0, 0.0, q_val))
110+
frac_result = 1.0 - q_val
111+
112+
# Combine results
113+
result = tl.where(
114+
(a_f32 > 0.0) & (x_f32 > 0.0),
115+
tl.where(use_series, series_result, frac_result),
116+
result,
117+
)
118+
119+
# Store result
120+
tl.store(out_ptr + offsets, result, mask=mask)
121+
122+
123+
def _launch_gammainc_metax(out: torch.Tensor, a: torch.Tensor, x: torch.Tensor):
124+
assert (
125+
a.device.type == flag_gems.device
126+
and x.device.type == flag_gems.device
127+
and out.device.type == flag_gems.device
128+
), f"All tensors must be {flag_gems.device} tensors"
129+
assert (
130+
out.numel() == a.numel() == x.numel()
131+
), "All tensors must have the same number of elements"
132+
assert out.device == a.device == x.device, "All tensors must be on the same device"
133+
134+
# Ensure floating point compute
135+
a_in = a
136+
x_in = x
137+
out_in = out
138+
139+
if not a_in.is_floating_point():
140+
a_in = a_in.to(torch.get_default_dtype())
141+
if not x_in.is_floating_point():
142+
x_in = x_in.to(torch.get_default_dtype())
143+
144+
# Cast input to match the desired output dtype if needed
145+
if a_in.dtype != out_in.dtype:
146+
a_in = a_in.to(out_in.dtype)
147+
if x_in.dtype != out_in.dtype:
148+
x_in = x_in.to(out_in.dtype)
149+
150+
a_contig = a_in.contiguous()
151+
x_contig = x_in.contiguous()
152+
out_was_noncontig = not out_in.is_contiguous()
153+
out_contig = out_in.contiguous() if out_was_noncontig else out_in
154+
155+
n_elements = out_contig.numel()
156+
# 1024 provides good occupancy for element-wise gammainc kernel
157+
BLOCK_SIZE = 1024
158+
grid = lambda meta: (triton.cdiv(n_elements, meta["BLOCK_SIZE"]),)
159+
160+
gammainc_kernel_metax[grid](
161+
a_contig, x_contig, out_contig, n_elements, BLOCK_SIZE=BLOCK_SIZE
162+
)
163+
164+
if out_was_noncontig:
165+
out_in.copy_(out_contig)
166+
return out_in
167+
168+
169+
def special_gammainc(a: torch.Tensor, x: torch.Tensor, *, out: torch.Tensor = None):
170+
logger.debug("GEMS_METAX SPECIAL_GAMMAINC")
171+
if a.device.type != flag_gems.device:
172+
raise ValueError(
173+
f"gammainc: first input tensor must be on {flag_gems.device} device"
174+
)
175+
if x.device.type != flag_gems.device:
176+
raise ValueError(
177+
f"gammainc: second input tensor must be on {flag_gems.device} device"
178+
)
179+
180+
if out is None:
181+
out = torch.empty_like(a)
182+
_launch_gammainc_metax(out, a, x)
183+
return out

0 commit comments

Comments
 (0)