|
| 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