Skip to content

Commit 0287ce1

Browse files
Arm backend: Fuse RIFE heads without PixelShuffle (#24790)
Accept 13-channel block0 heads with 2x, 4x or 8x bilinear upsampling in the packed-warp export pass. Read the head directly in the shader and preserve the integer resize rounding and quantization boundaries. Reuse the packed flow for the final residual addition so the existing final-blend fusion can match. This removes three state resize ops and restores two neural and two compute dispatches for the scale-4 model. Retain the existing PixelShuffle path and reject unsupported layouts. Extend C16 concat padding to quarter-size RGB branches. Reuse existing padding only when its value matches, preserving shared image resources and the numerical behavior of the unpadded graph. cc @digantdesai @freddan80 @per @zingo @mansnils @Sebastian-Larsson @robell @rascani Signed-off-by: Oscar Andersson <oscar.andersson@arm.com>
1 parent 4eb094b commit 0287ce1

6 files changed

Lines changed: 371 additions & 201 deletions

File tree

‎examples/arm/QAT_example/rife_vgf/passes/pack_rife_warps_and_concat.py‎

Lines changed: 93 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,29 @@ def _match_pixel_shuffle(slices, height, width):
262262
return phase, nodes
263263

264264

265+
def _match_previous_flow(concat, customs, nodes):
266+
flow_transpose = customs[0].args[0][1]
267+
residuals = [n for n in flow_transpose.users if n not in customs]
268+
if len(residuals) != 1 or not _is_op(residuals[0], TOSA.RESCALE.default, 5):
269+
raise ValueError("Expected one previous-flow rescale.")
270+
residual = residuals[0]
271+
if (
272+
residual.args[1] != torch.int32
273+
or len(residual.args[2]) != 1
274+
or residual.args[4] != 0
275+
or any(residual.kwargs.values())
276+
):
277+
raise ValueError("Expected a signed per-tensor INT32 previous-flow rescale.")
278+
nodes.add(flow_transpose)
279+
allowed = nodes | set(customs) | {concat, residual}
280+
if any(user not in allowed for node in nodes for user in node.users):
281+
raise ValueError("The block0 tail has additional consumers.")
282+
positions = {node: i for i, node in enumerate(concat.graph.nodes)}
283+
if any(positions[user] <= positions[concat] for user in residual.users):
284+
raise ValueError("Previous flow is needed before the packed shader.")
285+
return residual
286+
287+
265288
def _match_block0_tail(concat, customs):
266289
_, height, width, _ = _tensor_shape(concat)
267290
if height % 4 or width % 4:
@@ -291,26 +314,62 @@ def _match_block0_tail(concat, customs):
291314
slices.append(source)
292315
phase, shuffle_nodes = _match_pixel_shuffle(slices, height, width)
293316
nodes.update(shuffle_nodes)
294-
flow_transpose = customs[0].args[0][1]
295-
residuals = [n for n in flow_transpose.users if n not in customs]
296-
if len(residuals) != 1 or not _is_op(residuals[0], TOSA.RESCALE.default, 5):
297-
raise ValueError("Expected one previous-flow rescale.")
298-
residual = residuals[0]
299-
if (
300-
residual.args[1] != torch.int32
301-
or len(residual.args[2]) != 1
302-
or residual.args[4] != 0
303-
or any(residual.kwargs.values())
304-
):
305-
raise ValueError("Expected a signed per-tensor INT32 previous-flow rescale.")
306-
nodes.add(flow_transpose)
307-
allowed = nodes | set(customs) | {concat, residual}
308-
if any(user not in allowed for node in nodes for user in node.users):
309-
raise ValueError("The block0 tail has additional consumers.")
310-
positions = {node: i for i, node in enumerate(concat.graph.nodes)}
311-
if any(positions[user] <= positions[concat] for user in residual.users):
312-
raise ValueError("Previous flow is needed before the packed shader.")
313-
return phase, residual
317+
return phase, _match_previous_flow(concat, customs, nodes)
318+
319+
320+
def _match_direct_head_tail(concat, customs):
321+
_, height, width, _ = _tensor_shape(concat)
322+
nodes = set()
323+
head = None
324+
factor = None
325+
for rescale, offset, channels in zip(concat.args[0][5:], (4, 5, 0), (1, 8, 4)):
326+
if not _is_op(rescale, TOSA.RESCALE.default, 5):
327+
raise ValueError("Expected a resized head rescale.")
328+
resize = rescale.args[0]
329+
if not _is_op(resize, TOSA.RESIZE.default, 4):
330+
raise ValueError("Expected a resized head slice.")
331+
source = resize.args[0]
332+
if not _is_op(source, TOSA.SLICE.default, 3):
333+
raise ValueError("Expected an NHWC head slice.")
334+
current_head = source.args[0]
335+
batch, h, w, c = _tensor_shape(current_head)
336+
if batch != 1 or c != 13 or h <= 0 or w <= 0 or height % h or width % w:
337+
raise ValueError("Expected a 13-channel integer-scale head.")
338+
current_factor = height // h
339+
if current_factor not in (2, 4, 8) or width // w != current_factor:
340+
raise ValueError("Expected a uniform 2x, 4x or 8x head resize.")
341+
if head is not None and (head is not current_head or factor != current_factor):
342+
raise ValueError("Expected one shared head for all state slices.")
343+
head, factor = current_head, current_factor
344+
denominator = 2 * factor
345+
if (
346+
current_head.meta["val"].dtype != torch.int8
347+
or _shape_values(source.args[1]) != [0, 0, 0, offset]
348+
or _shape_values(source.args[2]) != [1, h, w, channels]
349+
or resize.meta["val"].dtype != torch.int32
350+
or resize.kwargs != {"resize_mode": "bilinear"}
351+
or [_shape_values(v) for v in resize.args[1:]]
352+
!= [
353+
[denominator, 2, denominator, 2],
354+
[1 - factor, 1 - factor],
355+
[factor - 1, factor - 1],
356+
]
357+
or rescale.args[1:] != (torch.int8, [1.0 / denominator**2], 0, 0)
358+
or any(rescale.kwargs.values())
359+
):
360+
raise ValueError("Expected the original signed integer head resize.")
361+
nodes.update((source, resize, rescale))
362+
return (head, _match_previous_flow(concat, customs, nodes)), factor
363+
364+
365+
def _match_head_tail(program, concat, customs):
366+
try:
367+
tail = _match_block0_tail(concat, customs)
368+
parameters = _phase_major_parameters(program, tail[0])
369+
return tail, parameters, True, 2
370+
except ValueError:
371+
tail, factor = _match_direct_head_tail(concat, customs)
372+
return tail, None, False, factor
314373

315374

316375
def _phase_major_parameters(program, phase):
@@ -348,6 +407,8 @@ def _phase_major_parameters(program, phase):
348407

349408

350409
def _reorder_pixel_shuffle_output(program, phase, parameters):
410+
if parameters is None:
411+
return
351412
producer, weight, bias, scales = parameters
352413
graph = phase.graph
353414
constants = []
@@ -368,7 +429,7 @@ def _reorder_pixel_shuffle_output(program, phase, parameters):
368429
class PackRifeWarpsAndConcatPass(ArmPass):
369430
"""Pack four private RIFE warps and their C28 concat into one C32 shader.
370431
371-
Fuse a compatible block0 PixelShuffle and state preparation into the shader.
432+
Fuse compatible block0 head resizing and state preparation into the shader.
372433
Reorder block0 output channels during export so each PixelShuffle read
373434
accesses 13 contiguous channels. Keep sampling quantization boundaries.
374435
Four zero weight columns make the following convolution ignore padding.
@@ -489,18 +550,25 @@ def call(self, graph_module: GraphModule) -> PassResult:
489550
return PassResult(graph_module, False)
490551

491552
phase_parameters = None
553+
pixel_shuffle = True
554+
resize_scale = 2
492555
try:
493-
tail = _match_block0_tail(concat, customs)
494-
phase_parameters = _phase_major_parameters(exported_program, tail[0])
556+
tail, phase_parameters, pixel_shuffle, resize_scale = _match_head_tail(
557+
exported_program, concat, customs
558+
)
495559
except ValueError as error:
496-
logger.debug("Keeping the original PixelShuffle layout: %s", error)
560+
logger.debug("Keeping the original head layout: %s", error)
497561
tail = None
498562
if tail is None:
499563
payload = build_packed_warp_payload(payloads, timestep)
500564
shader_inputs = [*images, *concat.args[0][5:]]
501565
else:
502566
payload = build_packed_warp_payload(
503-
payloads, timestep, fuse_block0_tail=True
567+
payloads,
568+
timestep,
569+
fuse_block0_tail=True,
570+
pixel_shuffle=pixel_shuffle,
571+
resize_scale=resize_scale,
504572
)
505573
shader_inputs = [*images, tail[0]]
506574
_reorder_pixel_shuffle_output(exported_program, tail[0], phase_parameters)

‎examples/arm/QAT_example/rife_vgf/passes/pad_rife_block0_concat.py‎

Lines changed: 28 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -4,89 +4,52 @@
44
# LICENSE file in the root directory of this source tree.
55

66
import logging
7-
import operator
87

98
import torch
109
from executorch.backends.arm._passes import ArmPass
1110
from executorch.backends.arm._passes.tosa_lowering_utils import create_const
1211
from executorch.backends.arm.tosa.mapping import TosaSpecialDtype
13-
from executorch.backends.arm.vgf.shaders.grid_sampler import (
14-
CUSTOM_SHADER_DOMAIN_NAME,
15-
flow_offset_grid_sampler_operator_name,
16-
)
1712
from executorch.backends.transforms.utils import create_constant_placeholder
1813
from executorch.exir.pass_base import ExportPass, PassResult
1914
from torch.export import ExportedProgram
2015
from torch.export.graph_signature import InputKind
2116
from torch.fx import GraphModule
2217
from torch.fx.passes.shape_prop import _extract_tensor_metadata
2318

24-
from ..shaders.final_warps import BLEND_OPERATOR_NAME
2519
from .pack_rife_warps_and_concat import (
2620
_constant_tensor,
2721
_is_op,
2822
_shape_values,
2923
_symmetric_weight,
3024
_tensor_shape,
31-
OPERATOR_NAME,
3225
TOSA,
3326
)
3427

3528
logger = logging.getLogger(__name__)
3629

3730

38-
def _ignores_alpha(custom, image):
39-
if (
40-
not _is_op(custom, TOSA.CUSTOM.default, 1)
41-
or custom.kwargs.get("domain_name") != CUSTOM_SHADER_DOMAIN_NAME
42-
):
43-
return False
44-
name = custom.kwargs.get("operator_name")
45-
if name in (OPERATOR_NAME, BLEND_OPERATOR_NAME):
46-
return image in custom.args[0][:2] and image not in custom.args[0][2:]
47-
if (
48-
name != flow_offset_grid_sampler_operator_name()
49-
or custom.args[0][0] is not image
50-
):
51-
return False
52-
for output in custom.users:
53-
if not _is_op(output, operator.getitem, 2) or output.args[1] != 0:
54-
return False
55-
for user in output.users:
56-
channel_axis = 3
57-
crops = [user]
58-
if _is_op(user, TOSA.TRANSPOSE.default, 2):
59-
if _shape_values(user.args[1]) != [0, 3, 1, 2]:
60-
return False
61-
channel_axis = 1
62-
crops = list(user.users)
63-
for crop in crops:
64-
if (
65-
not _is_op(crop, TOSA.SLICE.default, 3)
66-
or _shape_values(crop.args[1])[channel_axis] != 0
67-
or _shape_values(crop.args[2])[channel_axis] != 3
68-
):
69-
return False
70-
return True
71-
72-
73-
def _reusable_padding(image):
31+
def _reusable_padding(image, code):
7432
for candidate in image.users:
7533
if (
7634
_is_op(candidate, TOSA.PAD.default, 3)
7735
and _shape_values(candidate.args[1]) == [0, 0, 0, 0, 0, 0, 0, 1]
78-
and all(_ignores_alpha(user, candidate) for user in candidate.users)
36+
and _is_op(candidate.args[2], TOSA.CONST.default, 3)
37+
and candidate.args[2].args == ([code], [1], torch.int8)
7938
):
8039
return candidate
8140
return None
8241

8342

84-
def _move_padding_before_resize(image, resize, positions):
43+
def _move_padding_before_resize(image, resize):
44+
positions = {node: i for i, node in enumerate(image.graph.nodes)}
8545
if positions[image] < positions[resize]:
8646
return
87-
padding = image.args[1]
88-
if isinstance(padding, torch.fx.Node) and positions[padding] > positions[resize]:
89-
resize.prepend(padding)
47+
for operand in image.args[1:]:
48+
if (
49+
isinstance(operand, torch.fx.Node)
50+
and positions[operand] > positions[resize]
51+
):
52+
resize.prepend(operand)
9053
resize.prepend(image)
9154

9255

@@ -142,18 +105,24 @@ def _match(self, concat):
142105
):
143106
raise ValueError("Expected a private signed 1/4 image rescale.")
144107
resize = rescale.args[0]
108+
if not _is_op(resize, TOSA.RESIZE.default, 4):
109+
raise ValueError("Expected a bilinear RGB resize.")
110+
parameters = [_shape_values(arg) for arg in resize.args[1:]]
111+
if parameters not in (
112+
[[2, 4, 2, 4], [1, 1], [-1, -1]],
113+
[[2, 8, 2, 8], [3, 3], [-3, -3]],
114+
):
115+
raise ValueError("Expected a half-size or quarter-size RGB resize.")
116+
factor = parameters[0][1] // 2
145117
if (
146-
not _is_op(resize, TOSA.RESIZE.default, 4)
147-
or [_shape_values(arg) for arg in resize.args[1:]]
148-
!= [[2, 4, 2, 4], [1, 1], [-1, -1]]
149-
or resize.kwargs != {"resize_mode": "bilinear"}
118+
resize.kwargs != {"resize_mode": "bilinear"}
150119
or resize.meta["val"].dtype != torch.int32
151120
or set(resize.users) != {rescale}
152121
or _tensor_shape(resize.args[0])
153-
!= (shape[0], shape[1] * 2, shape[2] * 2, 3)
122+
!= (shape[0], shape[1] * factor, shape[2] * factor, 3)
154123
or resize.args[0].meta["val"].dtype != torch.int8
155124
):
156-
raise ValueError("Expected the original half-size RGB resize.")
125+
raise ValueError("Expected a private signed INT8 RGB resize.")
157126
return conv, weight, int(timestep.flatten()[0])
158127

159128
def call(self, graph_module: GraphModule) -> PassResult:
@@ -185,17 +154,12 @@ def call(self, graph_module: GraphModule) -> PassResult:
185154
)
186155
for rescale, code in zip(concat.args[0][:2], (timestep, 0)):
187156
resize = rescale.args[0]
188-
image = _reusable_padding(resize.args[0])
189-
positions = {node: i for i, node in enumerate(graph.nodes)}
190-
anchor = (
191-
image
192-
if image is not None and positions[image] < positions[resize]
193-
else resize
194-
)
195-
with graph.inserting_before(anchor):
196-
pad_value = create_const(graph, code, torch.int8, resize.args[0])
157+
image = _reusable_padding(resize.args[0], code)
197158
if image is None:
198159
with graph.inserting_before(resize):
160+
pad_value = create_const(
161+
graph, code, torch.int8, resize.args[0]
162+
)
199163
padding = graph.call_function(
200164
TOSA.CONST_SHAPE.default, ([0, 0, 0, 0, 0, 0, 0, 1],)
201165
)
@@ -208,9 +172,7 @@ def call(self, graph_module: GraphModule) -> PassResult:
208172
)
209173
image.meta = dict(resize.args[0].meta)
210174
else:
211-
# The recognized shaders only use RGB; their alpha padding is unused.
212-
image.args = (*image.args[:2], pad_value)
213-
_move_padding_before_resize(image, resize, positions)
175+
_move_padding_before_resize(image, resize)
214176
resize.args = (image, *resize.args[1:])
215177
# Constant bilinear input becomes 4 * code, then RESCALE divides by 4.
216178
for node in (image, resize, rescale):

0 commit comments

Comments
 (0)