Skip to content

feat(optim): fold Conv Add BatchNormalization pattern#1171

Open
xieofxie wants to merge 3 commits into
mainfrom
hualxie/fold_batchnorm
Open

feat(optim): fold Conv Add BatchNormalization pattern#1171
xieofxie wants to merge 3 commits into
mainfrom
hualxie/fold_batchnorm

Conversation

@xieofxie

Copy link
Copy Markdown
Contributor

Summary

  • add source patterns for both operand orders of Add(Conv(...), static) followed by inference BatchNormalization
  • fold BatchNormalization parameters into copied Conv weights/bias and the static Add tensor
  • support Conv with or without an existing bias while preserving Conv attributes
  • reject training mode, dynamic parameters, invalid variance, and non-removable/shared intermediates
  • register the opt-in conv-add-batch-normalization-folding rewrite capability

Why this uses the pattern framework

Unlike the static Split-to-Slice rewrite and channel-affine folding, this transformation has fixed source and target topology. It always replaces Conv -> Add -> BatchNormalization with Conv -> Add, has one output, and removes the complete matched subgraph without topology-dependent node generation or graph-wide route aggregation. The pattern framework can therefore express removability, support both commutative Add input orders as source variants, and share one target implementation. Match-specific constants and optional Conv bias are validated and captured by the source pattern before the target generates the folded parameters.

@xieofxie
xieofxie requested a review from a team as a code owner July 23, 2026 00:43
Comment thread src/winml/modelkit/pattern/conv_batchnorm_patterns.py Fixed
Comment thread tests/unit/pattern/test_conv_batchnorm_patterns.py Fixed
@xieofxie

Copy link
Copy Markdown
Contributor Author

Simple example

Assume:

Conv produces:       10
Static tensor adds:   2
BatchNormalization:  halve the value, then subtract 1

The original graph calculates:

(10 + 2) * 0.5 - 1 = 5

ModelKit moves the fixed BatchNormalization work into the earlier operations:

Modified Conv result: 10 * 0.5 - 1 = 4
Modified static value: 2 * 0.5     = 1
Final Add:             4 + 1       = 5

Both paths produce 5.

scaled_add = _scale_broadcast_tensor(add_tensor, output_shape, gamma)
if scaled_add is None:
return None
scaled_weight = weight * gamma.reshape((channels,) + (1,) * (weight.ndim - 1))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Only gamma is checked for finiteness, but the dtype-preserving fold can still overflow. With float16 W=40000, X=0.25, scale=2, var=1, and epsilon=0, the original graph produces finite 20000, while this multiplication creates an inf weight and the rewritten graph returns inf. Please reject the match when any generated weight, bias, or scaled Add constant is non-finite.

if input_names is None or output_names is None:
raise PatternMismatchedError("Input and output names are required")

weight_name = f"{prefix}weight"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

prefix is unique only among rewrites, not against tensor names already in the model. If the graph already has Rewrite_FoldedConvAddPattern_0_weight, PatternRewriter skips appending this new initializer and the Conv silently consumes the existing value; a checker-valid probe produced different outputs. Please allocate generated tensor names against all names in the parent graph, or remap them before insertion.

except ValueError:
return None
factors = gamma.reshape((1, len(gamma)) + (1,) * (len(output_shape) - 2))
return np.asarray(broadcast_values * factors, dtype=values.dtype)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

broadcast_to is a view, but this multiplication materializes the full expanded array. For output [1,512,1024,1024] and static Add input [1,1,1024,1024], matching allocates a 2-GiB array and rewriting embeds it as an initializer; this pattern is enabled for default analysis. Please reject or cap expansions that would create excessively large constants.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants