Skip to content

Commit e0015e1

Browse files
authored
Flow warp ops: et_vk.warp, et_vk.warp_blend (#24692)
Summary: Custom ops for optical-flow frame interpolation networks: - `et_vk.warp(input, flow)` samples `input` bilinearly at `out_px + flow`, with border padding. A flow warp written in PyTorch builds a normalized grid and calls `grid_sampler_2d`; this op skips the grid, which removes its dispatches and is more accurate in fp16, because the grid's normalized coordinates lose precision at large resolutions. - `et_vk.warp_blend(img0, img1, flow, mask)` fuses the final step of such a network into one shader: a bilinear upsample of `flow` and `mask` to the image size (when they are smaller), two warps (flow channels 0-1 and 2-3), a sigmoid on the mask, the blend and the clamp. Both have a reference implementation and a `Meta` kernel, so H and W can be dynamic at export. The partitioner only delegates shapes the shaders handle: flow `[N, 2, H, W]` for `warp`, flow `[N, 4, h, w]` and mask `[N, 1, h, w]` for `warp_blend`, with no broadcasting. The ops have no CPU kernel, so a rejected node fails at load. The two shaders share a bilinear border-sampling helper. Graphs that do not emit these ops are unchanged. `etvk_ops_bench` gets CPU references for both ops and `operators/warp/cases/correctness.json`: fp32 and fp16, N=2, C in {3, 4, 5, 8}, a 1-pixel-wide input, flows up to +-100 px, and same-size, 2x, non-integer and downsampled flow for `warp_blend`. The `warp` cases are added to the operator lists of `test-vulkan-operators-linux` in `pull.yml` and of the GPU job in `vulkan.yml`. Reviewed By: SS-JIA Differential Revision: D129150747 Pull Request resolved: #24692
1 parent a84676e commit e0015e1

15 files changed

Lines changed: 991 additions & 2 deletions

File tree

‎.github/workflows/pull.yml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2084,7 +2084,8 @@ jobs:
20842084
# Custom operator tests
20852085
PYTHON_EXECUTABLE=python bash backends/vulkan/test/ops/benchmark_v2/build.sh
20862086
for op in binary/add gemm/linear/qw/q8csw convolution/qw/q8csw gemm/linear/qw/q4gsw \
2087-
quantization/choose_qparams_per_row quantize/q8ta clone/q8ta binary/q8ta; do
2087+
quantization/choose_qparams_per_row quantize/q8ta clone/q8ta binary/q8ta \
2088+
warp; do
20882089
./cmake-out/backends/vulkan/test/ops/benchmark_v2/etvk_ops_bench --validation_only \
20892090
--config=backends/vulkan/test/ops/benchmark_v2/operators/${op}/cases/correctness.json
20902091
done

‎.github/workflows/vulkan.yml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ jobs:
123123
# selected above.
124124
PYTHON_EXECUTABLE=python bash backends/vulkan/test/ops/benchmark_v2/build.sh
125125
for op in binary/add gemm/linear/qw/q8csw convolution/qw/q8csw gemm/linear/qw/q4gsw \
126-
quantization/choose_qparams_per_row quantize/q8ta clone/q8ta binary/q8ta; do
126+
quantization/choose_qparams_per_row quantize/q8ta clone/q8ta binary/q8ta \
127+
warp; do
127128
./cmake-out/backends/vulkan/test/ops/benchmark_v2/etvk_ops_bench --validation_only \
128129
--config=backends/vulkan/test/ops/benchmark_v2/operators/${op}/cases/correctness.json
129130
done

‎backends/vulkan/custom_ops_lib.py‎

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1349,3 +1349,133 @@ def q4gsw_requant_meta(
13491349
lib.impl(name, q4gsw_requant_impl, "CompositeExplicitAutograd")
13501350
lib.impl(name, q4gsw_requant_meta, "Meta")
13511351
q4gsw_requant_op = getattr(getattr(torch.ops, namespace), name)
1352+
1353+
1354+
################
1355+
## warp_blend ##
1356+
################
1357+
1358+
1359+
def _warp_blend_warp(img: torch.Tensor, flow: torch.Tensor) -> torch.Tensor:
1360+
# Bilinear warp by a pixel-unit flow field with border padding, written as
1361+
# a normalized grid + grid_sample (align_corners=True). The Vulkan shader
1362+
# samples at out_px + flow directly, which is the same coordinate.
1363+
_, _, height, width = img.shape
1364+
xs = torch.linspace(-1.0, 1.0, width, dtype=img.dtype, device=img.device)
1365+
ys = torch.linspace(-1.0, 1.0, height, dtype=img.dtype, device=img.device)
1366+
grid_y, grid_x = torch.meshgrid(ys, xs, indexing="ij")
1367+
base_grid = torch.stack((grid_x, grid_y), dim=-1).unsqueeze(0)
1368+
# A 1-pixel axis has no extent to scale by; border padding clamps every
1369+
# sample to it, so any scale gives the same result.
1370+
norm_scale = torch.tensor(
1371+
[2.0 / max(width - 1.0, 1.0), 2.0 / max(height - 1.0, 1.0)],
1372+
dtype=img.dtype,
1373+
device=img.device,
1374+
)
1375+
grid = base_grid + flow.permute(0, 2, 3, 1) * norm_scale
1376+
return torch.nn.functional.grid_sample(
1377+
img, grid, mode="bilinear", padding_mode="border", align_corners=True
1378+
)
1379+
1380+
1381+
def warp_blend_impl(
1382+
img0: torch.Tensor,
1383+
img1: torch.Tensor,
1384+
flow: torch.Tensor,
1385+
mask: torch.Tensor,
1386+
) -> torch.Tensor:
1387+
# img0/img1: [N, C, Hout, Wout]. flow: [N, 4, Hin, Win] displacements in
1388+
# flow pixels (ch 0,1 warp img0; ch 2,3 warp img1). mask: [N, 1, Hin, Win]
1389+
# logits. A smaller flow/mask is bilinearly upsampled to the output size and
1390+
# the displacement rescaled per axis; the shader does the same in place.
1391+
_, _, out_h, out_w = img0.shape
1392+
_, _, in_h, in_w = flow.shape
1393+
if in_h != out_h or in_w != out_w:
1394+
flow = torch.nn.functional.interpolate(
1395+
flow, size=(out_h, out_w), mode="bilinear", align_corners=False
1396+
)
1397+
sx = out_w / in_w
1398+
sy = out_h / in_h
1399+
flow = flow * torch.tensor(
1400+
[sx, sy, sx, sy], dtype=flow.dtype, device=flow.device
1401+
).view(1, 4, 1, 1)
1402+
mask = torch.nn.functional.interpolate(
1403+
mask, size=(out_h, out_w), mode="bilinear", align_corners=False
1404+
)
1405+
warped0 = _warp_blend_warp(img0, flow[:, :2])
1406+
warped1 = _warp_blend_warp(img1, flow[:, 2:4])
1407+
m = torch.sigmoid(mask)
1408+
merged = warped0 * m + warped1 * (1.0 - m)
1409+
return torch.clamp(merged, 0.0, 1.0)
1410+
1411+
1412+
def warp_blend_meta(
1413+
img0: torch.Tensor,
1414+
img1: torch.Tensor,
1415+
flow: torch.Tensor,
1416+
mask: torch.Tensor,
1417+
) -> torch.Tensor:
1418+
return torch.empty_like(img0)
1419+
1420+
1421+
name = "warp_blend"
1422+
lib.define(f"{name}(Tensor img0, Tensor img1, Tensor flow, Tensor mask) -> Tensor")
1423+
lib.impl(name, warp_blend_impl, "CompositeExplicitAutograd")
1424+
# Export only needs the output shape; tracing the reference would specialize
1425+
# H and W through linspace and the grid scale.
1426+
lib.impl(name, warp_blend_meta, "Meta")
1427+
warp_blend_op = getattr(getattr(torch.ops, namespace), name)
1428+
1429+
1430+
# When lowering to executorch, ops are converted from default to out variant.
1431+
def warp_blend_out_impl(
1432+
img0: torch.Tensor,
1433+
img1: torch.Tensor,
1434+
flow: torch.Tensor,
1435+
mask: torch.Tensor,
1436+
out: torch.Tensor,
1437+
) -> torch.Tensor:
1438+
out = warp_blend_impl(img0, img1, flow, mask)
1439+
return out
1440+
1441+
1442+
name = "warp_blend.out"
1443+
lib.define(
1444+
f"{name}(Tensor img0, Tensor img1, Tensor flow, Tensor mask, *, Tensor(a!) out) -> Tensor(a!)"
1445+
)
1446+
lib.impl(name, warp_blend_out_impl, "CompositeExplicitAutograd")
1447+
1448+
##########
1449+
## warp ##
1450+
##########
1451+
1452+
1453+
def warp_impl(input: torch.Tensor, flow: torch.Tensor) -> torch.Tensor:
1454+
# input: [N, C, H, W]; flow: [N, 2, H, W] displacement in pixels.
1455+
return _warp_blend_warp(input, flow)
1456+
1457+
1458+
def warp_meta(input: torch.Tensor, flow: torch.Tensor) -> torch.Tensor:
1459+
return torch.empty_like(input)
1460+
1461+
1462+
name = "warp"
1463+
lib.define(f"{name}(Tensor input, Tensor flow) -> Tensor")
1464+
lib.impl(name, warp_impl, "CompositeExplicitAutograd")
1465+
lib.impl(name, warp_meta, "Meta")
1466+
warp_op = getattr(getattr(torch.ops, namespace), name)
1467+
1468+
1469+
# When lowering to executorch, ops are converted from default to out variant.
1470+
def warp_out_impl(
1471+
input: torch.Tensor,
1472+
flow: torch.Tensor,
1473+
out: torch.Tensor,
1474+
) -> torch.Tensor:
1475+
out = warp_impl(input, flow)
1476+
return out
1477+
1478+
1479+
name = "warp.out"
1480+
lib.define(f"{name}(Tensor input, Tensor flow, *, Tensor(a!) out) -> Tensor(a!)")
1481+
lib.impl(name, warp_out_impl, "CompositeExplicitAutograd")

‎backends/vulkan/op_registry.py‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1860,6 +1860,84 @@ def check_grid_sampler_2d_node(node: torch.fx.Node) -> bool:
18601860
)
18611861

18621862

1863+
# =============================================================================
1864+
# Warp.cpp
1865+
# =============================================================================
1866+
1867+
1868+
def _node_vals(node: torch.fx.Node, n_args: int) -> Optional[List[torch.Tensor]]:
1869+
if len(node.args) < n_args:
1870+
return None
1871+
vals = []
1872+
for arg in list(node.args[:n_args]) + [node]:
1873+
if not isinstance(arg, torch.fx.Node) or arg.meta.get("val") is None:
1874+
return None
1875+
vals.append(arg.meta["val"])
1876+
return vals
1877+
1878+
1879+
def _check_warp_shapes(node: torch.fx.Node) -> bool:
1880+
"""Reject shapes the Vulkan warp shader can't handle, such as a flow that
1881+
broadcasts over batch or space. These ops have no CPU kernel, so a rejected
1882+
node fails at load time."""
1883+
vals = _node_vals(node, 2)
1884+
if vals is None:
1885+
return False
1886+
inp, flow, out = vals
1887+
return (
1888+
inp.dtype == flow.dtype == out.dtype
1889+
and inp.shape == out.shape
1890+
and flow.dim() == 4
1891+
and flow.shape[:2] == (out.shape[0], 2)
1892+
and flow.shape[-2:] == out.shape[-2:]
1893+
)
1894+
1895+
1896+
def _check_warp_blend_shapes(node: torch.fx.Node) -> bool:
1897+
"""Reject shapes the Vulkan warp_blend shader can't handle, such as a mask
1898+
with more than one channel or a flow or mask that broadcasts over batch.
1899+
These ops have no CPU kernel, so a rejected node fails at load time."""
1900+
vals = _node_vals(node, 4)
1901+
if vals is None:
1902+
return False
1903+
img0, img1, flow, mask, out = vals
1904+
return (
1905+
img0.dtype == img1.dtype == flow.dtype == mask.dtype == out.dtype
1906+
and img0.shape == out.shape
1907+
and img1.shape == out.shape
1908+
and flow.dim() == 4
1909+
and mask.dim() == 4
1910+
and flow.shape[:2] == (out.shape[0], 4)
1911+
and mask.shape[:2] == (out.shape[0], 1)
1912+
and flow.shape[-2:] == mask.shape[-2:]
1913+
)
1914+
1915+
1916+
@update_features(exir_ops.edge.et_vk.warp.default)
1917+
def register_warp():
1918+
return OpFeatures(
1919+
inputs_storage=utils.CHANNELS_PACKED_TEXTURE,
1920+
inputs_dtypes=utils.FP_T,
1921+
supports_resize=True,
1922+
are_node_inputs_supported_fn=_check_warp_shapes,
1923+
)
1924+
1925+
1926+
# =============================================================================
1927+
# WarpBlend.cpp
1928+
# =============================================================================
1929+
1930+
1931+
@update_features(exir_ops.edge.et_vk.warp_blend.default)
1932+
def register_warp_blend():
1933+
return OpFeatures(
1934+
inputs_storage=utils.CHANNELS_PACKED_TEXTURE,
1935+
inputs_dtypes=utils.FP_T,
1936+
supports_resize=True,
1937+
are_node_inputs_supported_fn=_check_warp_blend_shapes,
1938+
)
1939+
1940+
18631941
# =============================================================================
18641942
# Repeat.cpp
18651943
# =============================================================================
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
#ifndef BILINEAR_BORDER_SAMPLE_GLSLH
10+
#define BILINEAR_BORDER_SAMPLE_GLSLH
11+
12+
/*
13+
* Bilinear sample of a channels-packed texture at pixel coordinate c on texel
14+
* slice z, with border padding: c is clamped to [0, max_xy].
15+
*/
16+
VEC4_T bilinear_border_sample(
17+
sampler3D tex,
18+
vec2 c,
19+
const int z,
20+
const ivec2 max_xy) {
21+
c = clamp(c, vec2(0.0), vec2(max_xy));
22+
const ivec2 lower = ivec2(floor(c));
23+
const ivec2 upper = min(lower + ivec2(1), max_xy);
24+
const vec2 w = c - vec2(lower);
25+
26+
const VEC4_T s00 = VEC4_T(texelFetch(tex, ivec3(lower.x, lower.y, z), 0));
27+
const VEC4_T s10 = VEC4_T(texelFetch(tex, ivec3(upper.x, lower.y, z), 0));
28+
const VEC4_T s01 = VEC4_T(texelFetch(tex, ivec3(lower.x, upper.y, z), 0));
29+
const VEC4_T s11 = VEC4_T(texelFetch(tex, ivec3(upper.x, upper.y, z), 0));
30+
return mix(mix(s00, s10, w.x), mix(s01, s11, w.x), w.y);
31+
}
32+
33+
#endif // BILINEAR_BORDER_SAMPLE_GLSLH
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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+
13+
#define PRECISION ${PRECISION}
14+
15+
#define VEC4_T ${texel_load_type(DTYPE, STORAGE)}
16+
17+
${define_active_storage_type(STORAGE)}
18+
19+
layout(std430) buffer;
20+
21+
#include "indexing.glslh"
22+
#include "bilinear_border_sample.glslh"
23+
24+
${layout_declare_tensor(B, "w", "t_out", DTYPE, STORAGE)}
25+
${layout_declare_tensor(B, "r", "t_in", DTYPE, STORAGE)}
26+
${layout_declare_tensor(B, "r", "t_flow", DTYPE, STORAGE)}
27+
28+
${layout_declare_ubo(B, "TextureMetadata", "outp")}
29+
${layout_declare_ubo(B, "TextureMetadata", "inp")}
30+
31+
layout(local_size_x_id = 0, local_size_y_id = 1, local_size_z_id = 2) in;
32+
33+
/*
34+
* Bilinear warp by a pixel-unit flow field, with border padding:
35+
* out[n, c, y, x] = in[n, c, y + flow[n, 1, y, x], x + flow[n, 0, y, x]]
36+
*
37+
* in, out: channels-packed texture3d, [N, C, H, W]
38+
* flow : channels-packed texture3d, [N, 2, H, W] (one z-slice per batch)
39+
*/
40+
void main() {
41+
const ivec3 pos = ivec3(gl_GlobalInvocationID);
42+
43+
if (out_of_bounds(pos, outp)) {
44+
return;
45+
}
46+
47+
const int num_z_per_n = outp.limits.z / outp.sizes.w;
48+
const int n = pos.z / num_z_per_n;
49+
50+
const VEC4_T flow_texel = texelFetch(t_flow, ivec3(pos.x, pos.y, n), 0);
51+
const vec2 c = vec2(pos.xy) + vec2(flow_texel.xy);
52+
53+
imageStore(
54+
t_out,
55+
pos,
56+
bilinear_border_sample(t_in, c, pos.z, ivec2(inp.sizes.xy) - 1));
57+
}
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+
warp:
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: warp

0 commit comments

Comments
 (0)