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.
ConvBNActAddPattern#
Matches: Conv1d|2d → [BatchNorm1d|2d] → Activation → [Dropout] → add(·, residual)
Produces: FusedConvBNAct feeding FusedAdd
Captures the residual block whose activation precedes the skip add: a
detector bottleneck built from Conv → BN → SiLU blocks, or a temporal
convolution network’s residual tail. Like ConvBNAddActPattern, this is a
branching pattern matching a Fork topology.
The block would otherwise be claimed by ActAddPattern, which pairs the
activation with the add and so places a Q/DQ between the convolution and its
own activation. TensorRT cannot fold an activation into a convolution kernel
across that Q/DQ, so the activation and the add run as a separate
floating-point kernel behind a pair of reformats. Keeping the activation with
its convolution and quantizing both inputs of the add instead is the layout
TensorRT’s own implicit quantization picks.
Every absorbed link must have exactly one user. A convolution, batch norm,
activation or dropout output that another node still reads cannot move inside
a fused module without being recomputed there, so those blocks stay with
ConvBNActPattern and ActAddPattern.
ConvTransposeBNReLUPattern#
Matches: ConvTranspose1d|2d → [BatchNorm1d|2d] → [ReLU]
Produces: FusedConvTransposeBNReLU
Quantizes the activation input and transposed-convolution weight at either
spatial rank. Both trailing modules are optional. Per-channel weight
quantization uses axis 1 of the ONNX
[input channels, output channels / groups, *spatial] layout. The normal
upstream precision policy can still leave a matched chain in floating point.
This path supports INT8. FP8 and INT4 configurations fall back to floating
point, as do grouped INT8 convolutions when either per-group channel count is
not a multiple of four. Explicit output_size calls remain unfused; export
recomposition can specialize their output padding first. Other activation
functions remain outside the fused chain.
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. The activation must feed nothing but the add, or the dropout in
front of it: one that also feeds another layer, or both operands of the add,
is left unfused, since fusing it would only recompute it and quantize its
pre-activation input instead. An activation fed by a single-user
Conv → [BN] chain is claimed first by ConvBNActAddPattern, which keeps it
with its convolution.
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}")
Group normalization#
GroupNormPattern wraps nn.GroupNorm as FusedGroupNorm; exported
aten.group_norm.default calls are first recomposed by
AtenGroupNormPattern. GroupNorm keeps its input and affine parameters in
floating point, matching LayerNorm’s boundary policy. A quantizable consumer
can insert Q/DQ after normalization. The wrapper does not insert an output
quantizer by itself or claim that normalization executes in INT8.
The boundary also stops upstream precision deferral: floating arithmetic before normalization does not automatically demote a convolution after it. Input rounding can strongly perturb low-variance groups, so input INT8 is not enabled. TensorRT may fuse output quantization into surrounding kernels; latency improvements depend on the downstream consumer and tensor shape.
Precision through indexed operations#
Precision-state propagation follows feature values through gather, scatter and
grid sampling. Index and sampling-coordinate operands remain independent;
their floating arithmetic does not force feature consumers to remain floating.
Scatter retains the precision requirements of both its destination and source, including scalar value= inputs.
Unknown operations conservatively retain all input dependencies.
This does not change FusedMulAdd’s floating-point policy: a MulAdd whose output
contributes actual feature values can still demote downstream INT8 consumers.
The rule also does not move Q/DQ through a sampling operation or quantize its
coordinates.