Source code for embedl_deploy._internal.lattice.modules.linear
# Copyright (C) 2026 Embedl AB
"""Lattice fused ``nn.Module`` replacement for Linear/GEMM patterns.
:class:`LatticeFusedLinear` wraps ``Linear -> [BatchNorm1d] -> [Activation]``
as a single fused block with an output quantizer. Both batch normalization and
the activation are optional — any prefix of the chain is accepted.
The ``output_quant_stub``
(:class:`~embedl_deploy._internal.lattice.modules.quant.LatticeQuant`) is
initially configured to INT8 (unfixed). The Lattice STATE_PREP patterns will
upgrade intermediate blocks to INT8 and the final block to INT16 before
calibration, choosing signedness from the activation (unsigned for ReLU, signed
otherwise).
Weight fake-quantization follows the same ONNX-export guard used by the
convolution modules: :func:`torch.onnx.is_in_onnx_export` is checked inside
:func:`_linear_weight_forward` to avoid emitting spurious
``QuantizeLinear/DequantizeLinear`` nodes for the weight path.
"""
from typing import cast
import torch
import torch.nn.functional as F
from torch import nn
from embedl_deploy._internal.core.modules import FusedModule
from embedl_deploy._internal.core.quantize.config import Precision
from embedl_deploy._internal.core.quantize.stubs import WeightFakeQuantize
from embedl_deploy._internal.lattice.modules.activation import (
LatticeActivationLike,
)
from embedl_deploy._internal.lattice.modules.quant import LatticeQuant
def _linear_weight_forward(
linear: nn.Linear,
weight_fake_quant: WeightFakeQuantize,
x: torch.Tensor,
) -> torch.Tensor:
"""Run a linear layer, fake-quantizing the weight when enabled."""
# Skip fake-quantize during ONNX export: torch.fake_quantize_* would be
# lowered to QuantizeLinear/DequantizeLinear, which are incorrect for the
# Lattice ONNX format. The toolchain quantizes weights independently.
weight = (
linear.weight
if torch.onnx.is_in_onnx_export()
else weight_fake_quant(linear.weight)
)
return F.linear(x, weight, linear.bias)
[docs]
class LatticeFusedLinear(FusedModule):
"""Fused ``Linear -> [BatchNorm1d] -> [Activation]`` for Lattice hardware.
The Lattice accelerator implements a linear (GEMM) layer with optional
batch normalization and optional activation as a single fused block. When
an activation is present it must be ``ReLU`` or ``LatticeLeakyReLU``.
:param linear:
The ``nn.Linear`` from the matched chain.
:param bn:
Optional batch normalization layer.
:param act:
Optional ``ReLU`` or ``LatticeLeakyReLU`` activation.
:param output_precision:
Initial precision for ``output_quant_stub``. ``STATE_PREP`` will
override this unless `output_fixed_calibration` is also supplied.
:param output_fixed_calibration:
If provided, ``(scale, zero_point)`` fixes the output quantizer so
that ``STATE_PREP`` and calibration leave it unchanged.
"""
inputs_to_quantize: set[int] = set()
def __init__(
self,
linear: nn.Linear,
bn: nn.BatchNorm1d | None = None,
act: LatticeActivationLike | None = None,
*,
output_precision: Precision = Precision.INT8,
output_fixed_calibration: tuple[float, int] | None = None,
) -> None:
super().__init__()
self.linear = linear
self.bn = bn
self.act = act
self.weight_fake_quant = WeightFakeQuantize({self})
self.output_quant_stub = LatticeQuant(
{self},
precision=output_precision,
fixed_calibration=output_fixed_calibration,
)
@property
def quantized_weight(self) -> torch.Tensor | None:
return self.linear.weight
[docs]
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Apply ``linear -> [bn] -> [act]``."""
x = _linear_weight_forward(self.linear, self.weight_fake_quant, x)
if self.bn is not None:
x = cast(torch.Tensor, self.bn(x))
if self.act is not None:
x = cast(torch.Tensor, self.act(x))
return x
def __repr__(self) -> str: # pragma: no cover
bn_info = ""
if self.bn is not None:
bn_info = f", bn={self.bn.num_features} (foldable)"
return (
f"LatticeFusedLinear("
f"{self.linear.in_features}→{self.linear.out_features}"
f"{bn_info})"
)