Skip to content

Commit 5af1d7b

Browse files
authored
Implement aten.grid_sampler_2d.default op (#19982)
Differential Revision: D106866109 Pull Request resolved: #19982
1 parent d9d3232 commit 5af1d7b

5 files changed

Lines changed: 391 additions & 0 deletions

File tree

backends/vulkan/op_registry.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1551,6 +1551,69 @@ def register_grid_priors():
15511551
)
15521552

15531553

1554+
# =============================================================================
1555+
# GridSampler2d.cpp
1556+
# =============================================================================
1557+
1558+
1559+
@update_features(exir_ops.edge.aten.grid_sampler_2d.default)
1560+
def register_grid_sampler_2d():
1561+
# The Vulkan implementation only supports the configuration used by RIFE's
1562+
# WarpModule: bilinear interpolation (0), border padding (1),
1563+
# align_corners=True. The C++ side has VK_CHECK_COND asserts for these,
1564+
# but those abort the whole inference at graph build — for any other model
1565+
# that contains a differently-configured grid_sampler_2d we want graceful
1566+
# CPU fallback, so we gate delegation here.
1567+
#
1568+
# Edge IR can hand us these scalar args as plain Python literals, SymInt /
1569+
# SymBool wrappers, or get_attr-style fx.Node references, so we unwrap
1570+
# each one defensively (mirrors the `isinstance(groups, int)` guard in
1571+
# check_conv_node / pick_conv_storage above). If we can't confidently pull
1572+
# a literal out of any arg, return False so the node stays on CPU instead
1573+
# of hitting a runtime VK_CHECK_COND.
1574+
def _unwrap_literal(arg: object) -> object:
1575+
# Plain Python literal (covers bool, since bool is a subclass of int).
1576+
if isinstance(arg, (bool, int, float)):
1577+
return arg
1578+
# get_attr / constant fx.Node — read the materialized value from meta.
1579+
if isinstance(arg, torch.fx.Node):
1580+
val = arg.meta.get("val", None)
1581+
if isinstance(val, (bool, int, float)):
1582+
return val
1583+
return None
1584+
# Symbolic int/bool (or anything else int-convertible) — try once.
1585+
try:
1586+
return int(arg) # pyre-ignore[6]
1587+
except (TypeError, ValueError):
1588+
return None
1589+
1590+
def check_grid_sampler_2d_node(node: torch.fx.Node) -> bool:
1591+
# Schema: aten::grid_sampler_2d(input, grid, interpolation_mode,
1592+
# padding_mode, align_corners)
1593+
if len(node.args) < 5:
1594+
return False
1595+
1596+
interp = _unwrap_literal(node.args[2])
1597+
padding = _unwrap_literal(node.args[3])
1598+
align_corners = _unwrap_literal(node.args[4])
1599+
1600+
if interp is None or padding is None or align_corners is None:
1601+
return False
1602+
1603+
# mode: 0 = bilinear; padding: 1 = border; align_corners must be True.
1604+
return interp == 0 and padding == 1 and bool(align_corners) is True
1605+
1606+
return OpFeatures(
1607+
inputs_storage=[
1608+
utils.CHANNELS_PACKED_TEXTURE, # input : [N, C, Hin, Win]
1609+
utils.CONTIGUOUS_BUFFER, # grid : [N, Hout, Wout, 2]
1610+
],
1611+
inputs_dtypes=utils.FP_T,
1612+
supports_resize=True,
1613+
are_node_inputs_supported_fn=check_grid_sampler_2d_node,
1614+
)
1615+
1616+
15541617
# =============================================================================
15551618
# Repeat.cpp
15561619
# =============================================================================
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#version 450 core
10+
11+
${define_required_extensions(STORAGE, DTYPE)}
12+
${define_required_extensions("buffer", DTYPE)}
13+
14+
#define PRECISION ${PRECISION}
15+
16+
#define VEC4_T ${texel_load_type(DTYPE, STORAGE)}
17+
#define T ${texel_load_component_type(DTYPE, "buffer")}
18+
19+
${define_active_storage_type(STORAGE)}
20+
21+
layout(std430) buffer;
22+
23+
#include "indexing.glslh"
24+
25+
${layout_declare_tensor(B, "w", "t_out", DTYPE, STORAGE)}
26+
${layout_declare_tensor(B, "r", "t_in", DTYPE, STORAGE)}
27+
// `t_grid` is always bound as a contiguous (width-packed) buffer of fp scalars
28+
// with logical shape [N, Hout, Wout, 2]. See add_grid_sampler_2d_node which
29+
// asserts this with `is_contiguous_buffer_tensor`.
30+
${layout_declare_tensor(B, "r", "t_grid", DTYPE, "buffer")}
31+
32+
${layout_declare_ubo(B, "TextureMetadata", "outp")}
33+
${layout_declare_ubo(B, "TextureMetadata", "inp")}
34+
35+
layout(local_size_x_id = 0, local_size_y_id = 1, local_size_z_id = 2) in;
36+
37+
// `out_layout` is passed for forward compatibility and is currently asserted
38+
// to be the standard channels-packed layout by `add_grid_sampler_2d_node`.
39+
// All texel math below assumes packed_dim = C (channels-packed), so the four
40+
// fp components of a texel share the same (N, Hout, Wout) and differ only in
41+
// channel. This lets one bilinear interpolation produce all 4 output channels.
42+
${layout_declare_spec_const(C, "int", "out_layout", "CONTIG_LAYOUT_INT")}
43+
44+
/*
45+
* Vulkan implementation of `aten.grid_sampler_2d.default` for the
46+
* specific configuration used by RIFE's `WarpModule`:
47+
* mode=bilinear, padding_mode=border, align_corners=true.
48+
*
49+
* Layout assumptions (validated in add_grid_sampler_2d_node):
50+
* - input : channels-packed texture3d, shape [N, C, Hin, Win]
51+
* - grid : contiguous (width-packed) buffer SSBO of fp scalars,
52+
* shape [N, Hout, Wout, 2] in normalized coords [-1, 1]
53+
* - output : channels-packed texture3d, shape [N, C, Hout, Wout]
54+
*
55+
* For channels-packed texture3d, the texel z extent is N * ceil(C/4),
56+
* laid out as z = n * num_z_per_n + c_slice. Both input and output share
57+
* the same N and C, so input z == output z.
58+
*
59+
* TextureMetadata layout (vtensor.md): sizes is WHCN order, so
60+
* outp.sizes.x = Wout, outp.sizes.y = Hout, outp.sizes.w = N.
61+
* outp.limits.z = N * ceil(C/4) (texel slices along z).
62+
*/
63+
void main() {
64+
const ivec3 pos = ivec3(gl_GlobalInvocationID);
65+
66+
if (out_of_bounds(pos, outp)) {
67+
return;
68+
}
69+
70+
// Derive batch index from texel z. Each batch occupies `num_z_per_n`
71+
// consecutive z-slices (one per 4-channel slice). Integer division by
72+
// num_z_per_n picks out the batch.
73+
const int N = outp.sizes.w;
74+
const int num_z_per_n = outp.limits.z / N;
75+
const int n = pos.z / num_z_per_n;
76+
77+
// Look up the (gx, gy) for this output pixel from the grid SSBO.
78+
// The grid is a contiguous buffer of [N, Hout, Wout, 2], so the linear
79+
// index for (n, h, w, comp) is ((n*Hout + h)*Wout + w)*2 + comp. This
80+
// relies on `inputs_storage` in op_registry.py pinning grid to
81+
// CONTIGUOUS_BUFFER and the C++ dispatcher re-checking with
82+
// `is_contiguous_buffer_tensor` — see GridSampler2d.cpp.
83+
const int Wout = outp.sizes.x;
84+
const int Hout = outp.sizes.y;
85+
const int grid_base = ((n * Hout + pos.y) * Wout + pos.x) * 2;
86+
const float gx_norm = float(t_grid[grid_base + 0]);
87+
const float gy_norm = float(t_grid[grid_base + 1]);
88+
89+
// Unnormalize for align_corners=true:
90+
// coord_pixel = (coord_norm + 1) * 0.5 * (size - 1)
91+
// Input W/H come from inp.sizes (WHCN), not inp.limits (texel space).
92+
const ivec2 max_in_xy = ivec2(inp.sizes.xy) - 1;
93+
const float gx_pixel = (gx_norm + 1.0) * 0.5 * float(max_in_xy.x);
94+
const float gy_pixel = (gy_norm + 1.0) * 0.5 * float(max_in_xy.y);
95+
96+
// padding_mode=border: clamp coordinates to [0, size-1].
97+
const float gx = clamp(gx_pixel, 0.0, float(max_in_xy.x));
98+
const float gy = clamp(gy_pixel, 0.0, float(max_in_xy.y));
99+
100+
const ivec2 lower = ivec2(floor(vec2(gx, gy)));
101+
// Clamp ceil to valid range for samples on the border.
102+
const ivec2 upper = clamp(lower + ivec2(1), ivec2(0), max_in_xy);
103+
const vec2 w = vec2(gx, gy) - vec2(lower);
104+
105+
// Fetch the four nearest texels (each carries 4 channels). Because input
106+
// is channels-packed, pos.z indexes the same channel slice in input as in
107+
// output, so we can reuse pos.z directly without remapping.
108+
VEC4_T s00 = texelFetch(t_in, ivec3(lower.x, lower.y, pos.z), 0);
109+
VEC4_T s10 = texelFetch(t_in, ivec3(upper.x, lower.y, pos.z), 0);
110+
VEC4_T s01 = texelFetch(t_in, ivec3(lower.x, upper.y, pos.z), 0);
111+
VEC4_T s11 = texelFetch(t_in, ivec3(upper.x, upper.y, pos.z), 0);
112+
113+
// Bilinear interpolation. Weights are scalars; mix() acts on all 4 channels.
114+
VEC4_T out_tex =
115+
mix(mix(s00, s10, w.x), mix(s01, s11, w.x), w.y);
116+
117+
imageStore(t_out, pos, out_tex);
118+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
grid_sampler_2d:
8+
parameter_names_with_default_values:
9+
DTYPE: float
10+
STORAGE: texture3d
11+
generate_variant_forall:
12+
DTYPE:
13+
- VALUE: half
14+
- VALUE: float
15+
shader_variants:
16+
- NAME: grid_sampler_2d
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#include <executorch/backends/vulkan/runtime/graph/ops/OperatorRegistry.h>
10+
11+
#include <executorch/backends/vulkan/runtime/graph/ops/impl/Common.h>
12+
13+
#include <executorch/backends/vulkan/runtime/graph/ops/impl/utils/TensorUtils.h>
14+
15+
#include <executorch/backends/vulkan/runtime/graph/ops/utils/ShaderNameUtils.h>
16+
17+
namespace vkcompute {
18+
19+
void resize_grid_sampler_2d_node(
20+
ComputeGraph* graph,
21+
const std::vector<ArgGroup>& args,
22+
const std::vector<ValueRef>& extra_args) {
23+
(void)extra_args;
24+
const ValueRef out = args.at(0).refs.at(0);
25+
const ValueRef in = args.at(1).refs.at(0);
26+
const ValueRef grid = args.at(1).refs.at(1);
27+
28+
const std::vector<int64_t> in_sizes = graph->sizes_of(in);
29+
const std::vector<int64_t> grid_sizes = graph->sizes_of(grid);
30+
31+
// input : [N, C, Hin, Win]
32+
// grid : [N, Hout, Wout, 2]
33+
// output : [N, C, Hout, Wout]
34+
std::vector<int64_t> out_sizes = {
35+
in_sizes.at(0), in_sizes.at(1), grid_sizes.at(1), grid_sizes.at(2)};
36+
37+
graph->virtual_resize(out, out_sizes);
38+
}
39+
40+
void add_grid_sampler_2d_node(
41+
ComputeGraph& graph,
42+
const ValueRef in,
43+
const ValueRef grid,
44+
const ValueRef interpolation_mode,
45+
const ValueRef padding_mode,
46+
const ValueRef align_corners,
47+
const ValueRef out) {
48+
// Runtime sanity checks. The Python partitioner is supposed to filter out
49+
// unsupported configurations, but guard against bypass paths here too.
50+
// mode: 0 = bilinear, 1 = nearest, 2 = bicubic
51+
VK_CHECK_COND(
52+
graph.extract_scalar<int64_t>(interpolation_mode) == 0,
53+
"Vulkan grid_sampler_2d only supports bilinear interpolation");
54+
// padding_mode: 0 = zeros, 1 = border, 2 = reflection
55+
VK_CHECK_COND(
56+
graph.extract_scalar<int64_t>(padding_mode) == 1,
57+
"Vulkan grid_sampler_2d only supports border padding");
58+
VK_CHECK_COND(
59+
graph.get_bool(align_corners),
60+
"Vulkan grid_sampler_2d requires align_corners=true");
61+
62+
// Defense-in-depth layout validation. The partitioner enforces these
63+
// layouts via `inputs_storage` in op_registry.py::register_grid_sampler_2d,
64+
// but the shader hard-codes channels-packed texture indexing for in/out and
65+
// contiguous buffer indexing for grid, so a layout mismatch here would be a
66+
// silent miscompute. Per the etvk-implement-operator skill ("Validate
67+
// tensor layout assumptions"), assert these explicitly.
68+
VK_CHECK_COND(
69+
graph.is_standard_channels_packed_texture_tensor(in),
70+
"Vulkan grid_sampler_2d requires input to be a channels-packed texture");
71+
VK_CHECK_COND(
72+
graph.is_standard_channels_packed_texture_tensor(out),
73+
"Vulkan grid_sampler_2d requires output to be a channels-packed texture");
74+
VK_CHECK_COND(
75+
graph.is_contiguous_buffer_tensor(grid),
76+
"Vulkan grid_sampler_2d requires grid to be a contiguous buffer");
77+
78+
// The shader binds t_in, t_out, and t_grid with a single DTYPE selected via
79+
// `dtype_of(out)` below. The op registry allows `grid` to be fp16 or fp32
80+
// independently of the input dtype, so without this guard a mixed-precision
81+
// model (e.g., fp32 flow grid + fp16 activations) would bind the fp32 grid
82+
// buffer as half and silently miscompute. Op tests use matching dtypes for
83+
// all args, so they would not catch this.
84+
VK_CHECK_COND(
85+
graph.dtype_of(grid) == graph.dtype_of(out),
86+
"Vulkan grid_sampler_2d requires grid and input to share dtype");
87+
88+
std::string kernel_name("grid_sampler_2d");
89+
kernel_name.reserve(kShaderNameReserve);
90+
add_dtype_suffix(kernel_name, graph.dtype_of(out));
91+
92+
graph.execute_nodes().emplace_back(new DynamicDispatchNode(
93+
graph,
94+
VK_KERNEL_FROM_STR(kernel_name),
95+
default_pick_global_wg_size,
96+
default_pick_local_wg_size,
97+
// Inputs and Outputs
98+
{{out, vkapi::kWrite}, {{in, grid}, vkapi::kRead}},
99+
// Shader params buffers. `meta_ubo` packs sizes, limits, axis_map, and
100+
// packed_dim into the canonical TextureMetadata struct (see vtensor.md);
101+
// the shader derives Wout/Hout/N/num_z_per_n from `outp.sizes` and
102+
// `outp.limits`, so no extra params buffer is needed.
103+
{graph.meta_ubo(out), graph.meta_ubo(in)},
104+
// Push Constants
105+
{},
106+
// Specialization Constants — pass the output tensor's hashed layout so
107+
// the shader can specialize on packed_dim at pipeline creation time.
108+
{graph.hashed_layout_of(out)},
109+
// Resize Args
110+
{},
111+
// Resizing Logic
112+
resize_grid_sampler_2d_node));
113+
}
114+
115+
void grid_sampler_2d(ComputeGraph& graph, const std::vector<ValueRef>& args) {
116+
// Argument order matches kernels/portable/cpu/op_grid_sampler_2d.cpp:
117+
// (input, grid, interpolation_mode, padding_mode, align_corners, out)
118+
return add_grid_sampler_2d_node(
119+
graph, args[0], args[1], args[2], args[3], args[4], args[5]);
120+
}
121+
122+
REGISTER_OPERATORS {
123+
VK_REGISTER_OP(aten.grid_sampler_2d.default, grid_sampler_2d);
124+
}
125+
126+
} // namespace vkcompute

0 commit comments

Comments
 (0)