|
| 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 | +import math |
| 18 | +from typing import Optional, Tuple |
| 19 | + |
| 20 | +import torch |
| 21 | +import triton |
| 22 | +import triton.language as tl |
| 23 | + |
| 24 | +from flag_gems.runtime import device, torch_device_fn |
| 25 | + |
| 26 | +logger = logging.getLogger(__name__) |
| 27 | + |
| 28 | + |
| 29 | +# Gather-style backward kernel. |
| 30 | +# |
| 31 | +# The generic implementation scatters every output position into the input |
| 32 | +# gradient with ``tl.atomic_add`` accumulated in a float32 staging buffer, |
| 33 | +# then casts back to the input dtype in a separate kernel. On the PPU that |
| 34 | +# means three launches (zeros + scatter + cast) plus atomic contention, which |
| 35 | +# dominates latency on the small feature maps this op sees. |
| 36 | +# |
| 37 | +# Here we invert the mapping: each program owns one *input* gradient pixel and |
| 38 | +# sums the contiguous block of output positions that map to it. For the |
| 39 | +# nearest(-exact) forward index ``ih = floor(oh * reciprocal_scale)`` the set |
| 40 | +# of ``oh`` mapping to a given ``ih`` is the half-open range |
| 41 | +# ``[ceil(ih / reciprocal_scale), ceil((ih + 1) / reciprocal_scale))``. These |
| 42 | +# ranges tile ``[0, OH)`` exactly, so the gathered sum is over the identical |
| 43 | +# set of gradient values as the scatter — numerics are unchanged. Because no |
| 44 | +# lane writes the same address twice, atomics are unnecessary and the sum is |
| 45 | +# accumulated in fp32 registers and cast once on store, removing the float32 |
| 46 | +# buffer and the trailing cast kernel entirely. |
| 47 | +@triton.jit |
| 48 | +def _upsample_nearest_exact2d_backward_gather_kernel( |
| 49 | + grad_output_ptr, |
| 50 | + grad_input_ptr, |
| 51 | + M, # total input gradient elements = N * C * IH * IW |
| 52 | + OH, |
| 53 | + OW, |
| 54 | + IH, |
| 55 | + IW, |
| 56 | + scale_h, # scales_h (outputs per input row) when USE_SCALES else unused |
| 57 | + scale_w, # scales_w when USE_SCALES else unused |
| 58 | + USE_SCALES: tl.constexpr, |
| 59 | + MAX_H: tl.constexpr, # upper bound on output rows mapping to one input row |
| 60 | + MAX_W: tl.constexpr, |
| 61 | + BLOCK_SIZE: tl.constexpr, |
| 62 | +): |
| 63 | + pid = tl.program_id(0) |
| 64 | + offs = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) |
| 65 | + mask = offs < M |
| 66 | + |
| 67 | + # Decompose the flat contiguous input offset into (nc, ih, iw). |
| 68 | + iw = offs % IW |
| 69 | + t = offs // IW |
| 70 | + ih = t % IH |
| 71 | + nc = t // IH # flattened (n, c) plane index |
| 72 | + |
| 73 | + base_out = nc * OH * OW |
| 74 | + |
| 75 | + # Half-open output ranges [oh0, oh1) x [ow0, ow1) that map onto this input |
| 76 | + # pixel, i.e. the inverse of the forward index floor(oh / scale). |
| 77 | + if USE_SCALES: |
| 78 | + oh0 = tl.ceil(ih.to(tl.float32) * scale_h).to(tl.int32) |
| 79 | + oh1 = tl.ceil((ih + 1).to(tl.float32) * scale_h).to(tl.int32) |
| 80 | + ow0 = tl.ceil(iw.to(tl.float32) * scale_w).to(tl.int32) |
| 81 | + ow1 = tl.ceil((iw + 1).to(tl.float32) * scale_w).to(tl.int32) |
| 82 | + else: |
| 83 | + # Exact integer ceil(ih * OH / IH) = (ih * OH + IH - 1) // IH. |
| 84 | + oh0 = (ih * OH + IH - 1) // IH |
| 85 | + oh1 = ((ih + 1) * OH + IH - 1) // IH |
| 86 | + ow0 = (iw * OW + IW - 1) // IW |
| 87 | + ow1 = ((iw + 1) * OW + IW - 1) // IW |
| 88 | + |
| 89 | + # The forward index is clamped to IH-1 / IW-1, so the last input row/col |
| 90 | + # absorbs every trailing output position. Extend its range to the edge to |
| 91 | + # keep the ranges a perfect partition of [0, OH) x [0, OW). |
| 92 | + oh1 = tl.where(ih == IH - 1, OH, oh1) |
| 93 | + ow1 = tl.where(iw == IW - 1, OW, ow1) |
| 94 | + |
| 95 | + acc = tl.zeros((BLOCK_SIZE,), dtype=tl.float32) |
| 96 | + for dh in tl.static_range(MAX_H): |
| 97 | + oh = oh0 + dh |
| 98 | + valid_h = mask & (oh < oh1) |
| 99 | + row_off = base_out + oh * OW |
| 100 | + for dw in tl.static_range(MAX_W): |
| 101 | + ow = ow0 + dw |
| 102 | + valid = valid_h & (ow < ow1) |
| 103 | + g = tl.load(grad_output_ptr + row_off + ow, mask=valid, other=0.0) |
| 104 | + acc += g.to(tl.float32) |
| 105 | + |
| 106 | + tl.store(grad_input_ptr + offs, acc, mask=mask) |
| 107 | + |
| 108 | + |
| 109 | +def _upsample_nearest_exact2d_backward( |
| 110 | + grad_output: torch.Tensor, |
| 111 | + output_size: Tuple[int, int], |
| 112 | + input_size: Tuple[int, int, int, int], |
| 113 | + scales_h: Optional[float] = None, |
| 114 | + scales_w: Optional[float] = None, |
| 115 | +) -> torch.Tensor: |
| 116 | + logger.debug("GEMS_THEAD _UPSAMPLE_NEAREST_EXACT2D_BACKWARD") |
| 117 | + |
| 118 | + assert grad_output.device.type == device.name |
| 119 | + assert grad_output.ndim == 4, "The ndim of grad_output must be 4" |
| 120 | + assert len(output_size) == 2, "The len of output_size must be 2" |
| 121 | + assert len(input_size) == 4, "The len of input_size must be 4" |
| 122 | + |
| 123 | + OH, OW = output_size |
| 124 | + N, C, IH, IW = input_size |
| 125 | + |
| 126 | + assert grad_output.shape == (N, C, OH, OW), ( |
| 127 | + f"grad_output shape {grad_output.shape} does not match " |
| 128 | + f"expected shape (N={N}, C={C}, OH={OH}, OW={OW})" |
| 129 | + ) |
| 130 | + |
| 131 | + grad_input = torch.empty( |
| 132 | + (N, C, IH, IW), device=grad_output.device, dtype=grad_output.dtype |
| 133 | + ) |
| 134 | + |
| 135 | + if grad_input.numel() == 0: |
| 136 | + return grad_input |
| 137 | + |
| 138 | + if not grad_output.is_contiguous(): |
| 139 | + grad_output = grad_output.contiguous() |
| 140 | + |
| 141 | + # Forward source index uses reciprocal_scale = IH/OH (or 1/scales_h). When |
| 142 | + # scales are given the gather range uses that scale directly; otherwise we |
| 143 | + # use exact integer arithmetic in-kernel from OH/IH. |
| 144 | + use_scales = scales_h is not None or scales_w is not None |
| 145 | + scale_h = float(scales_h) if scales_h is not None else OH / IH |
| 146 | + scale_w = float(scales_w) if scales_w is not None else OW / IW |
| 147 | + |
| 148 | + # Tight upper bound on how many output rows/cols map to a single input |
| 149 | + # pixel. Non-final ranges have width <= ceil(OH / IH); the clamped final |
| 150 | + # pixel spans to OH so account for it explicitly. |
| 151 | + def _max_span(o, i): |
| 152 | + if i <= 0 or o <= 0: |
| 153 | + return 1 |
| 154 | + base = (o + i - 1) // i |
| 155 | + last0 = math.ceil((i - 1) * o / i) |
| 156 | + return max(base, o - last0, 1) |
| 157 | + |
| 158 | + MAX_H = _max_span(OH, IH) |
| 159 | + MAX_W = _max_span(OW, IW) |
| 160 | + |
| 161 | + M = N * C * IH * IW |
| 162 | + # BLOCK=256: tuned for thead PPU — balances parallelism (grid = ceil(M/256)) |
| 163 | + # against per-block register pressure for the backward gather loop. |
| 164 | + BLOCK = 256 |
| 165 | + grid = (triton.cdiv(M, BLOCK),) |
| 166 | + |
| 167 | + with torch_device_fn.device(grad_output.device): |
| 168 | + _upsample_nearest_exact2d_backward_gather_kernel[grid]( |
| 169 | + grad_output, |
| 170 | + grad_input, |
| 171 | + M, |
| 172 | + OH, |
| 173 | + OW, |
| 174 | + IH, |
| 175 | + IW, |
| 176 | + scale_h, |
| 177 | + scale_w, |
| 178 | + use_scales, |
| 179 | + MAX_H, |
| 180 | + MAX_W, |
| 181 | + BLOCK, |
| 182 | + num_warps=4, |
| 183 | + ) |
| 184 | + |
| 185 | + return grad_input |
0 commit comments