Skip to content

Commit 4e3f235

Browse files
authored
Support mode 'bilinear' for torch.nn.interpolate (#2241)
1 parent 3c7ad98 commit 4e3f235

3 files changed

Lines changed: 136 additions & 64 deletions

File tree

‎thunder/tests/opinfos.py‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9805,6 +9805,15 @@ def interpolate_sample_generator(op, device, dtype, requires_grad, **kwargs):
98059805
yield SampleInput(make(a_shape), size=size)
98069806
yield SampleInput(make(a_shape), size=size, mode="nearest-exact")
98079807

9808+
# mode = "bilinear" supports only 4D inputs in PyTorch, so 2 spatial dimensions
9809+
n_spatial_dims_bilinear = (2,)
9810+
for b, c, l, dim in itertools.product(batch, channels, dim_options, n_spatial_dims_bilinear):
9811+
for size in itertools.product(dim_options[l], repeat=dim):
9812+
spatial_dims = (l,) * dim
9813+
a_shape = b + c + spatial_dims
9814+
9815+
yield SampleInput(make(a_shape), size=size, mode="bilinear")
9816+
98089817
# Test scale/scale_factor passed as a scalar
98099818
yield SampleInput(make(1, 1, 5, 5), scale_factor=0.5)
98109819
yield SampleInput(make(1, 1, 5, 5), size=10)
@@ -9865,9 +9874,19 @@ def interpolate_error_generator(op, device, dtype=torch.float32, **kwargs):
98659874
"scale_factor(.*?) is expected to be (.*?) a sequence of strictly positive floating point numbers",
98669875
)
98679876
yield (
9868-
SampleInput(make(1, 1, 1, 1), mode="bilinear"),
9877+
SampleInput(make(1, 1, 1, 1), mode="trilinear"),
9878+
RuntimeError,
9879+
"only modes 'nearest', 'nearest-exact' and 'bilinear' are supported at the moment, but got mode=(.*?)",
9880+
)
9881+
yield (
9882+
SampleInput(make(1, 1, 5), scale_factor=2.0, mode="bilinear"),
9883+
RuntimeError,
9884+
"bilinear interpolation supports exactly two spatial dims, got 1",
9885+
)
9886+
yield (
9887+
SampleInput(make(1, 1, 5, 5, 5), scale_factor=2.0, mode="bilinear"),
98699888
RuntimeError,
9870-
"only modes 'nearest' and 'nearest-exact' are supported at the moment, but got mode=(.*?)",
9889+
"bilinear interpolation supports exactly two spatial dims, got 3",
98719890
)
98729891

98739892

‎thunder/tests/test_ops.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,8 +357,10 @@ def foo(mode):
357357
return t1
358358

359359
tfoo = thunder.jit(foo)
360-
for mode in ["linear", "bilinear", "bicubic", "trilinear", "area"]:
361-
match = f"only modes 'nearest' and 'nearest-exact' are supported at the moment, but got mode='{mode}'"
360+
for mode in ["linear", "bicubic", "trilinear", "area"]:
361+
match = (
362+
f"only modes 'nearest', 'nearest-exact' and 'bilinear' are supported at the moment, but got mode='{mode}'"
363+
)
362364
with pytest.raises(NotImplementedError, match=match):
363365
tfoo(mode)
364366

‎thunder/torch/__init__.py‎

Lines changed: 111 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -5470,9 +5470,9 @@ def _interpolate_scale_factor_helper(
54705470
scale_factor: Sequence[float] | float,
54715471
mode: str = "nearest",
54725472
) -> TensorLike:
5473-
if mode not in ("nearest", "nearest-exact"):
5473+
if mode not in ("nearest", "nearest-exact", "bilinear"):
54745474
raise ValueError(
5475-
f"_interpolate_scale_factor_helper expected mode to be 'nearest' or 'nearest-exact', but got {mode}"
5475+
f"_interpolate_scale_factor_helper expected mode to be 'nearest', 'nearest-exact' or 'bilinear', but got {mode}"
54765476
)
54775477

54785478
# a is assumed to be at least 3D.
@@ -5493,66 +5493,117 @@ def _interpolate_scale_factor_helper(
54935493
f"a sequence of strictly positive floating point numbers of length {dim}",
54945494
)
54955495

5496-
# perform nearest up/down-sampling
5497-
def nearest_sampler(
5498-
t: TensorLike,
5499-
input_dim: int,
5500-
output_dim: int,
5501-
*,
5502-
scale: float,
5503-
dim: int,
5504-
exact: bool,
5505-
) -> TensorLike:
5506-
# It is expected that output_dim = int(input_dim * scale).
5507-
# Indices [0, ..., output_dim - 1] are mapped to [0, ..., input_dim - 1]
5508-
# with the rule i -> int(i * scale) or i -> round((i + 0.5) * scale - 0.5),
5509-
# corresponding to modes 'nearest' or 'nearest-exact', respectively.
5510-
# Values at these indices is the result.
5511-
# References https://github.com/pytorch/pytorch/blob/main/aten/src/ATen/native/UpSample.h
5512-
selected_idx = arange(0, output_dim, device=a.device)
5513-
selected_idx = to((selected_idx + exact * 0.5) * scale, selected_idx.dtype)
5514-
return clang.take(t, selected_idx, dim=dim)
5515-
5516-
def dim_expander(t, dim, n_repeats):
5517-
t = unsqueeze(t, dim + 1)
5518-
t = expand(t, t.shape[: dim + 1] + (n_repeats,) + t.shape[dim + 2 :])
5519-
return t
5520-
5521-
res_output_spatial_dims = []
5522-
5523-
for k, (scale, input_dim) in enumerate(zip(reversed(scale_factor), reversed(spatial_dims))):
5524-
output_dim = int(scale * input_dim)
5496+
if mode == "bilinear":
5497+
5498+
def _bilinear_sampler_2d(
5499+
t: TensorLike,
5500+
in_h: int,
5501+
in_w: int,
5502+
out_h: int,
5503+
out_w: int,
5504+
*,
5505+
dim_h: int = 2,
5506+
dim_w: int = 3,
5507+
) -> TensorLike:
5508+
# The X pass
5509+
x_dst = arange(out_w, device=t.device)
5510+
scale_w = to(in_w / out_w, a.dtype)
5511+
# The 0.5s come from the fact that we treat each element as a pixel,
5512+
# that has its centerpoint at x0 + 0.5. With the formula below we
5513+
# make sure that the centerpoints of these "images" align, because align_corners
5514+
# is not yet implemented
5515+
x_src_f = to((x_dst + 0.5) * scale_w - 0.5, a.dtype)
5516+
x0 = clamp(clang.floor(x_src_f), 0, in_w - 1).to(to_dtype(torch.int64))
5517+
x1 = clamp((x0 + 1), max=in_w - 1)
5518+
wx = unsqueeze(unsqueeze((x_src_f - x0.to(x_src_f.dtype)), 0), 0)
5519+
5520+
v0 = clang.take(t, x0, dim=dim_w)
5521+
v1 = clang.take(t, x1, dim=dim_w)
5522+
# Linear interpolation in the width-direction
5523+
t_x = v0 * (1 - wx) + v1 * wx
5524+
5525+
# The Y pass
5526+
y_dst = arange(out_h, device=t.device)
5527+
scale_h = to(in_h / out_h, a.dtype)
5528+
y_src_f = to((y_dst + 0.5) * scale_h - 0.5, a.dtype)
5529+
y0 = clamp(clang.floor(y_src_f), 0, in_h - 1).to(to_dtype(torch.int64))
5530+
y1 = clamp((y0 + 1), max=in_h - 1)
5531+
wy = unsqueeze(unsqueeze(unsqueeze((y_src_f - y0.to(y_src_f.dtype)), 0), 0), -1)
5532+
5533+
v0 = clang.take(t_x, y0, dim=dim_h)
5534+
v1 = clang.take(t_x, y1, dim=dim_h)
5535+
return v0 * (1 - wy) + v1 * wy
5536+
55255537
utils.check(
5526-
output_dim > 0,
5527-
lambda: f"provided scale_factor value {scale} results in a zero length output at dimension {k + 2}",
5538+
len(spatial_dims) == 2,
5539+
lambda: f"bilinear interpolation supports exactly two spatial dims, got {len(spatial_dims)}",
55285540
)
5529-
res_output_spatial_dims.append(output_dim)
5530-
5531-
# k iterates from the end, and we skip the first 2
5532-
# dimenions corresponding to batches and channels.
5533-
curr_dim = 2 + (len(spatial_dims) - k - 1)
5534-
5535-
exact: bool = mode == "nearest-exact"
5536-
if output_dim <= input_dim:
5537-
if output_dim <= input_dim // 2:
5538-
# scale_factor <= 1 (i.e. output_dim <= input_dim) implies simple slice
5539-
# when output_dim <= input_dim // 2.
5540-
stride = input_dim // output_dim
5541-
end = input_dim - (input_dim % output_dim)
5542-
a = clang.slice_in_dim(a, 0, end, stride=stride, dim=curr_dim)
5543-
else:
5544-
# In this case slice will not do and explicit downsample is needed.
5545-
a = nearest_sampler(a, input_dim, output_dim, scale=1.0 / scale, dim=curr_dim, exact=exact)
5546-
else:
5547-
if output_dim % input_dim == 0:
5548-
# In this case we can just expand dim.
5549-
n_repeats = output_dim // input_dim
5550-
a = dim_expander(a, curr_dim, n_repeats)
5541+
in_h, in_w = spatial_dims
5542+
out_h = int(in_h * scale_factor[0])
5543+
out_w = int(in_w * scale_factor[1])
5544+
utils.check(out_h > 0 and out_w > 0, lambda: f"scale_factor leads to zero-size output ({out_h}x{out_w})")
5545+
return _bilinear_sampler_2d(a, in_h, in_w, out_h, out_w)
5546+
elif mode == "nearest" or mode == "nearest-exact":
5547+
# perform nearest up/down-sampling
5548+
def nearest_sampler(
5549+
t: TensorLike,
5550+
input_dim: int,
5551+
output_dim: int,
5552+
*,
5553+
scale: float,
5554+
dim: int,
5555+
exact: bool,
5556+
) -> TensorLike:
5557+
# It is expected that output_dim = int(input_dim * scale).
5558+
# Indices [0, ..., output_dim - 1] are mapped to [0, ..., input_dim - 1]
5559+
# with the rule i -> int(i * scale) or i -> round((i + 0.5) * scale - 0.5),
5560+
# corresponding to modes 'nearest' or 'nearest-exact', respectively.
5561+
# Values at these indices is the result.
5562+
# References https://github.com/pytorch/pytorch/blob/main/aten/src/ATen/native/UpSample.h
5563+
selected_idx = arange(0, output_dim, device=a.device)
5564+
selected_idx = to((selected_idx + exact * 0.5) * scale, selected_idx.dtype)
5565+
return clang.take(t, selected_idx, dim=dim)
5566+
5567+
def dim_expander(t, dim, n_repeats):
5568+
t = unsqueeze(t, dim + 1)
5569+
t = expand(t, t.shape[: dim + 1] + (n_repeats,) + t.shape[dim + 2 :])
5570+
return t
5571+
5572+
res_output_spatial_dims = []
5573+
5574+
for k, (scale, input_dim) in enumerate(zip(reversed(scale_factor), reversed(spatial_dims))):
5575+
output_dim = int(scale * input_dim)
5576+
utils.check(
5577+
output_dim > 0,
5578+
lambda: f"provided scale_factor value {scale} results in a zero length output at dimension {k + 2}",
5579+
)
5580+
res_output_spatial_dims.append(output_dim)
5581+
5582+
# k iterates from the end, and we skip the first 2
5583+
# dimenions corresponding to batches and channels.
5584+
curr_dim = 2 + (len(spatial_dims) - k - 1)
5585+
5586+
exact: bool = mode == "nearest-exact"
5587+
if output_dim <= input_dim:
5588+
if output_dim <= input_dim // 2:
5589+
# scale_factor <= 1 (i.e. output_dim <= input_dim) implies simple slice
5590+
# when output_dim <= input_dim // 2.
5591+
stride = input_dim // output_dim
5592+
end = input_dim - (input_dim % output_dim)
5593+
a = clang.slice_in_dim(a, 0, end, stride=stride, dim=curr_dim)
5594+
else:
5595+
# In this case slice will not do and explicit downsample is needed.
5596+
a = nearest_sampler(a, input_dim, output_dim, scale=1.0 / scale, dim=curr_dim, exact=exact)
55515597
else:
5552-
# In this case expand will not cut it and explicit upsampling is needed.
5553-
a = nearest_sampler(a, input_dim, output_dim, scale=1.0 / scale, dim=curr_dim, exact=exact)
5598+
if output_dim % input_dim == 0:
5599+
# In this case we can just expand dim.
5600+
n_repeats = output_dim // input_dim
5601+
a = dim_expander(a, curr_dim, n_repeats)
5602+
else:
5603+
# In this case expand will not cut it and explicit upsampling is needed.
5604+
a = nearest_sampler(a, input_dim, output_dim, scale=1.0 / scale, dim=curr_dim, exact=exact)
55545605

5555-
output_shape = [batch, channels] + res_output_spatial_dims[::-1]
5606+
output_shape = [batch, channels] + res_output_spatial_dims[::-1]
55565607
return reshape(a, output_shape)
55575608

55585609

@@ -5596,8 +5647,8 @@ def interpolate(
55965647
antialias: bool = False,
55975648
) -> TensorLike:
55985649
utils.check(
5599-
(mode == "nearest" or mode == "nearest-exact"),
5600-
lambda: f"only modes 'nearest' and 'nearest-exact' are supported at the moment, but got {mode=}",
5650+
(mode == "nearest" or mode == "nearest-exact" or mode == "bilinear"),
5651+
lambda: f"only modes 'nearest', 'nearest-exact' and 'bilinear' are supported at the moment, but got {mode=}",
56015652
exception_type=NotImplementedError,
56025653
)
56035654

0 commit comments

Comments
 (0)