Skip to content

[PyTorch FE] Restore dynamic batch in window-reverse view shapes#36555

Closed
evkotov wants to merge 5 commits into
openvinotoolkit:masterfrom
evkotov:CVS-172206
Closed

[PyTorch FE] Restore dynamic batch in window-reverse view shapes#36555
evkotov wants to merge 5 commits into
openvinotoolkit:masterfrom
evkotov:CVS-172206

Conversation

@evkotov

@evkotov evkotov commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Problem

SwinIR-style window_reverse computes the batch as plain Python arithmetic:

B = int(windows.shape[0] / (H * W / ws / ws))
x = windows.view(B, H // ws, W // ws, ws, ws, -1)

Under torch.jit.trace the int(...) collapses to a literal. In the IR the
view shape becomes a Concat where only the spatial dims stay live (aten::size),
the leading batch is a baked Constant, and the channel is the trailing -1:

view(windows, [ B=Const(1), H//ws, W//ws, 8, 8, Const(-1) ])
                baked        live   live              infer

At the traced batch=1 this is correct:

windows = (361, 8, 8, 180),  total = 4_158_720
H//ws = W//ws = 19  (live)
known product = 1 * 19 * 19 * 8 * 8 = 23_104
-1 = 4_158_720 / 23_104 = 180   -> channel 180, correct

After model.reshape(batch=2) the input becomes (2, 3, 152, 152). The spatial
dims do not depend on batch, so the live H//ws, W//ws stay 19, 19, and the
leading Const(1) stays 1 (it is a literal with no input for reshape to rewrite):

windows = (722, 8, 8, 180),  total = 8_317_440   (twice the elements)
shape   = (1, 19, 19, 8, 8, -1)                  (leading still 1)
known product = 1 * 19 * 19 * 8 * 8 = 23_104     (unchanged)
-1 = 8_317_440 / 23_104 = 360                     (was 180)

The doubled element count divides by the same known product, so the only free
axis (-1) must absorb the extra factor of 2: the batch leaks into the channel,
180 -> 360, and the window grid is scrambled.

The corruption is silent because the final view rebuilds a correct shape -- its
batch is live and its channel is the literal embed_dim:

Reshape_8: [1, 19, 19, 8, 8, 360]
permute  : [1, 19, 8, 19, 8, 360]
Reshape_9 = view(1, H, W, -1) -> [1, 152, 152, 360]
final view(B, H*W, C) -> [2, 23104, 180]   shape correct, data wrong

Compilation, inference, and output shapes all pass; only the values are wrong
(max_abs_diff ~3.47 on the SwinIR_Classical models at batch=2).

Fix

Add ReshapeBatchDimResolver, a PyTorch-FE ModelPass run in normalize().
For a reshape carrying the window-reverse signature (leading positive-int
constant, exactly one trailing -1, at least one dynamic interior dim) it frees
the leading dim to -1 and pins the channel to its batch-independent value as a
Constant, so the batch is inferred from the real element count and no longer
leaks into the channel.

The channel is recovered by a deterministic walk-back rather than OV shape
inference: window_reverse uses two chained views and the second takes its data
through a permute whose output stays dynamic. The pass walks back from the data
to a static last dim, through a last-axis-preserving Transpose, or to a view
already selected for rewrite.

Two guards keep it from touching ordinary reshapes that share the structure:

  • if the output last dim is statically known, it must equal the recovered channel;
  • on the direct path the rewrite must only re-partition the leading dim and keep
    the data's entire trailing block, which rejects merge/split reshapes such as
    view(1, T//2, -1) whose -1 spans more than the data's last dim.

Tickets:

  • 172206

evkotov added 5 commits June 22, 2026 14:39
A model traced with a fixed batch bakes the batch into a view shape such as
`x.view(B, H, W, -1)` as a literal leading constant, while the channel becomes
the single `-1` infer slot. After the converted model is reshaped to a different
batch, the frozen leading constant cannot propagate the new batch and the `-1`
channel absorbs it instead, scrambling the data (seen as a large accuracy drop
on SwinIR-style models at batch > 1).

Add ReshapeBatchDimResolver, a normalize() MatcherPass that detects this shape
Concat (positive-int leading constant, a single trailing `-1`, at least one
dynamic interior dim, dynamic data batch) and rebuilds it per reshape so the
leading dim becomes `-1` (inferred from the real element count) and the channel
is pinned to the data tensor's last dimension via Gather(ShapeOf(data), -1).
The rewrite is a no-op at the traced batch and tracks the real batch afterwards.

Add a layer test covering rebatch accuracy, the structural rewrite, and a
negative case proving the pass does not fire on ordinary reshapes.
…k-back

Tracing a window-reverse style model at a fixed batch bakes the batch as a
literal Constant in the leading slot of a view-shape Concat, with the channel
as the trailing -1. After model.reshape to a new batch the frozen leading
constant cannot carry it, so the -1 channel absorbs the extra batch and the
windows are mis-partitioned, producing wrong values.

ReshapeBatchDimResolver previously pinned the restored channel to a runtime
Gather of the data's last dimension, which is only correct for window-reverse
and corrupted ordinary reshapes whose -1 is a product of several dimensions
(spatial flatten, attention head-merge). Gating on a static data last dim
fixed those false positives but left the second of the two chained
window-reverse views unrewritten, because OpenVINO does not propagate the
first view's static channel through the intervening permute.

Recover the channel structurally instead of relying on shape propagation: walk
back from the reshape's data, taking a statically known last dimension
directly, recursing through a last-axis-preserving Transpose, or reusing the
channel of a reshape already selected for rewrite. Collect matches in
topological order, guard each against the statically inferred output channel,
then rewrite. The channel is pinned to a static Constant (batch-independent)
and the leading dimension becomes -1.

Add a window-reverse layer test that projects to a fixed channel width (as the
real model does) so the static-channel path is exercised, and keep the
negative tests for the ordinary-reshape idioms.
…hapes

ReshapeBatchDimResolver could fire on an ordinary reshape that shares the
window-reverse signature -- a leading positive-int constant, a dynamic interior
dimension and a single trailing -1 -- and corrupt the result even at the traced
batch. A view such as Linear(D, D) followed by reshape(1, T//2, -1) has a static
data last dimension (so the channel walk-back resolves it) and a dynamic output
last dimension (so the existing static-output-channel guard is vacuous), yet its
-1 spans more than data's last dimension, so pinning the channel to that last dim
and freeing the batch produces the wrong shape.

Add a trailing-block value-preservation guard on the direct resolution path
(channel recovered from the data's own static last dimension): the rewrite is
applied only when it merely re-partitions data's leading dimension and keeps
data's entire trailing block, i.e. the new shape's last rank_data-1 elements
statically equal data's trailing dimensions. This is exactly the window-reverse
semantics and rejects merge/split reshapes. The walk-back path (the second view,
whose data last dimension is dynamic) is unaffected.

Also skip the full-model re-inference when no reshape carries the structural
signature: a cheap structural pre-scan (which needs no shape inference) gates the
validation, so the common case of models without this pattern pays nothing.

Add regression tests covering the traced-batch false positive, idempotency, the
dynamic-channel scope boundary, and walk-back fragility.
…ts tests

Reuse shared constant helpers (ov::op::util::has_constant_value /
get_constant_value) in ReshapeBatchDimResolver instead of hand-rolled
constant inspection, and add a static_last_dim() helper to replace three
copy-pasted partial-shape last-dimension extractions. Behavior is unchanged.

In the layer test, extract the duplicated scalar-constant walk and the
compile/compare-to-torch boilerplate into module-level helpers, and merge the
three structurally identical must-not-fire cases into one parametrized class.

Note: merging those test classes renames some pytest node IDs (now under
TestResolverDoesNotFire::test_not_fired_and_b1_ok).
… graphs

Rewrite the ReshapeBatchDimResolver header doc to match the convention used by
common transformations (before/after Unicode box-drawn graphs). Add a graph of
the shape-Concat rewrite (leading Constant(B) -> Constant(-1), trailing
Constant(-1) -> Constant(channel), interior dims kept) and a channel-recovery
walk-back graph for the two chained window-reverse views. Documentation only.
@evkotov evkotov self-assigned this Jun 24, 2026
@evkotov evkotov requested a review from a team as a code owner June 24, 2026 15:51
@evkotov evkotov requested a review from PiotrKrzem June 24, 2026 15:51
@github-actions github-actions Bot added the category: PyTorch FE OpenVINO PyTorch Frontend label Jun 24, 2026

@mvafin mvafin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a good fix. The problem is in model, the model is not-reshapable. @evkotov please check the actual pytorch model code, does it really supports any other batch then 1? We also have SmartReshape transformations, maybe it can be covered there?

@evkotov

evkotov commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

created #36574 with alternative approach with fix in SmartReshape

@evkotov evkotov closed this Jun 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

category: PyTorch FE OpenVINO PyTorch Frontend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants