Operator Fusions#
Fusions combine sequences of operators into single fused modules that map
directly to hardware-accelerated kernels. After fusion, the model graph
contains modules like FusedConvBNAct instead of separate Conv2d,
BatchNorm2d, and ReLU layers.
Fused modules:
Are numerically equivalent to the original operator sequence (no weight folding at this stage — that’s handled by the hardware compiler).
Export cleanly to ONNX for downstream compilation.
Each FusedModule subclass declares inputs_to_quantize — the set of
positional input indices where the quantization pipeline places QDQ stubs.
See Quantization for the full table.
Convolution fusions#
ConvBNPattern#
Matches: Conv2d → [BatchNorm2d]
Produces: FusedConvBN
The most basic convolution fusion. The BatchNorm2d is optional — a bare
Conv2d is also matched (useful for ensuring QDQ stub placement even on
convolutions without batch normalization).
ConvBNActPattern#
Matches: Conv2d → [BatchNorm2d] → Activation
Produces: FusedConvBNAct
Fuses convolution, optional batch normalization, and an activation function.
The pattern accepts any activation in ActivationLike: ReLU, ReLU6,
LeakyReLU, ELU, GELU, SiLU, Mish, Hardswish, Hardsigmoid,
PReLU, Sigmoid, Tanh.
StemConvBNActMaxPoolPattern#
Matches: Conv2d(3in, 7×7) → [BatchNorm2d] → Activation → MaxPool2d
Produces: FusedConvBNActMaxPool
Captures the classification network stem found in ResNet and similar
architectures. The convolution is constrained to in_channels=3, kernel_size=(7,7) so only the actual stem is matched.
ConvBNAddActPattern#
Matches: Conv2d → BatchNorm2d → add(·, residual) → Activation
Produces: FusedConvBNAddAct
Captures the tail of ResNet-style bottleneck blocks where the convolution
path merges with a skip connection before the final activation. This is a
branching pattern — it matches a Fork topology with two inputs feeding
into an operator.add node.
Linear fusions#
LinearActPattern#
Matches: Linear → Activation
Produces: FusedLinearAct
LinearPattern#
Matches: standalone Linear
Produces: FusedLinear
LayerNormPattern#
Matches: LayerNorm
Produces: FusedLayerNorm
ActAddPattern#
Matches: Fork(Activation, residual) → operator.add
Produces: FusedActAdd
Captures the inverted residual pattern where an activation precedes the
skip-connection add (e.g. in transformer feed-forward blocks). Like
ConvBNAddActPattern, this is a branching pattern matching a Fork
topology.
MulAddPattern#
Matches: Fork(operator.mul, residual) → operator.add
Produces: FusedMulAdd
Captures the layer-scale residual tail used by ConvNeXt-style blocks: a runtime tensor scaled by a constant (a parameter or a scalar literal), then added to a skip connection. Fully dynamic multiplies (e.g. SE-style gating) and adds whose other operand is a weight are not matched. The fused module declares no quantized inputs, keeping the layer-scale residual in floating point — INT8 Q/DQ on this operation measurably hurts ConvNeXt accuracy while the latency cost of floating point is negligible.
Attention fusions#
These patterns match the sub-modules produced by the
DecomposeMultiheadAttentionPattern and DecomposeSwinAttentionPattern
conversions.
MHAInProjectionPattern#
Matches: MHAInProjection
Produces: FusedMHAInProjection
ScaledDotProductAttentionPattern#
Matches: ScaledDotProductAttention
Produces: FusedScaledDotProductAttention
SwinAttentionPattern#
Matches: SwinAttention
Produces: FusedSwinAttention
Wraps the Swin Transformer window-attention module produced by
DecomposeSwinAttentionPattern. The fused module carries shared spatial
state (SwinSpatialState) for window partition/reverse geometry.
Pooling fusions#
AdaptiveAvgPoolPattern#
Matches: AdaptiveAvgPool2d
Produces: FusedAdaptiveAvgPool2d
Tip
In mixed-precision workflows the AdaptiveAvgPoolPattern is often omitted
entirely from the pattern list so the pool is not fused and stays as a
plain AdaptiveAvgPool2d in the graph.
See Custom Patterns for details.
Fusion summary by architecture#
ResNet50#
Fused module |
Count |
Description |
|---|---|---|
|
1 |
Stem: Conv(7×7) + BN + ReLU + MaxPool |
|
16 |
Bottleneck residual blocks |
|
16 |
Main-path Conv + BN + ReLU |
|
17 |
Conv + BN without activation |
|
1 |
Global average pool |
Total |
51 |
Conversions applied: FlattenLinearToConv1x1Pattern converts the
Flatten → Linear classifier into Conv2d(1×1) → Flatten.
ConvNeXt (Tiny/Base/Large)#
Fused module |
Count |
Description |
|---|---|---|
|
23/41/41 |
Depthwise + downsampling convolutions |
|
23/41/41 |
LayerNorm layers |
|
18/36/36 |
Linear + GELU chains |
|
18/36/36 |
Standalone linear layers |
|
18/36/36 |
Layer-scale residual tails (kept in FP) |
|
1/1/1 |
Global average pool |
Counts increase with model depth (Base and Large share the block count and differ in channel width).
Conversions applied:
RemoveIdentityAdaptiveAvgPoolPatternremoves identity pooling ops.FlattenLinearToConv1x1Patternconverts the classifier head.
ConvNeXt uses depthwise separable convolutions extensively. The default pattern set quantizes all convolutions equally, but mixed-precision (see Custom Patterns) skips depthwise convolutions for better latency.
Vision Transformer (ViT-B/16)#
Fused module |
Count |
Description |
|---|---|---|
|
1 |
Patch embedding Conv2d |
|
12 |
Q/K/V projections |
|
12 |
Attention computation |
|
36+ |
Out-proj + MLP linear layers |
|
12 |
MLP hidden → GELU chains |
|
25 |
Pre/post-norm layers |
Conversions applied: DecomposeMultiheadAttentionPattern decomposes all 12
attention layers into explicit sub-modules.
Running fusions only#
from embedl_deploy import transform
from embedl_deploy.tensorrt import TENSORRT_FUSION_PATTERNS
result = transform(model, (example_input,), patterns=TENSORRT_FUSION_PATTERNS)
To inspect what was fused:
from collections import Counter
fused_counts = Counter(
type(m).__name__
for m in result.model.modules()
if type(m).__name__.startswith('Fused')
)
for name, count in sorted(fused_counts.items()):
print(f" {name}: {count}")