Skip to content

Commit e81c9b6

Browse files
chx7514Dongxu-H
andauthored
[KernelGen][thead] Add upsample_nearest_exact2d_backward vendor specialization (#207)
Co-authored-by: Dongxu-H <dxhan@baai.ac.cn>
1 parent abcd6ab commit e81c9b6

2 files changed

Lines changed: 187 additions & 0 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@
3131
special_shifted_chebyshev_polynomial_w,
3232
)
3333
from .unsafe_masked_index_put_accumulate import _unsafe_masked_index_put_accumulate
34+
from .upsample_nearest_exact2d_backward import _upsample_nearest_exact2d_backward
3435

3536
__all__ = [
3637
"_conv_depthwise2d",
3738
"_thnn_fused_lstm_cell_backward_impl",
3839
"_unsafe_masked_index_put_accumulate",
40+
"_upsample_nearest_exact2d_backward",
3941
"adaptive_max_pool3d_backward",
4042
"broadcast_tensors",
4143
"broadcast_to",
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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

Comments
 (0)