DeepInfer Explained

linear.py: ReplicatedLinear and the FP8 GEMM path

IntermediateCode mapabout 8 min read + 3 min deep dives

Code version: DeepInfer @ 9bd9cf2 · 2026-10-21

Purpose#

layers/linear.py runs the dense, non-expert linear layers of the model and the logits GEMM on DeepGEMM, in BF16 or FP8; v4_modules/linear.py only describes the shapes of the checkpoint weights.

Filesdeepinfer/model_executor/layers/linear.py (185 lines) and deepinfer/model_executor/models/v4_modules/linear.py (34 lines).
Main classesRuntime: ReplicatedLinear(torch.nn.Module) (deepinfer/model_executor/layers/linear.py:36). Containers: ReplicatedLinear(nn.Linear) (deepinfer/model_executor/models/v4_modules/linear.py:8) and ParallelRoutedLinear(nn.Linear) (deepinfer/model_executor/models/v4_modules/linear.py:23). Two classes have the same name; this page says "runtime" or "container" each time.
Runs inThe worker process of each rank, on the current device stream. Decode steps run inside CUDA graphs.
Called byRuntime class: attention projections, the compressor, the MoE gate and shared expert, DFlash. bf16_gemm_nt: the logits. get_fp8_cast_params: norms, attention output, expert SwiGLU. Containers: the HF model builders and the loader.
CallsDeepGEMM: bf16_gemm_nt, fp8_fp4_gemm_nt, transform_sf_into_required_layout. tile_kernels.quant.per_token_cast.
Not in these filesRouted experts (grouped GEMM and mega-MoE in v4_modules/mlp.py), the attention einsum (attn.py), and weight loading (hugging_face/). There is no column- or row-parallel linear: no tensor parallelism exists.

Where it sits#

Figure: the path of a weight from the container to the runtime layer (panel 1), and the two GEMM paths of the runtime class (panel 2). Click to zoom.
Open full size ↗

Who calls it#

What it calls#

This page describes only the arguments. The kernels are in DeepGEMM and tile_kernels, which this site does not explain.

Key classes and functions#

NameLineWhat it does
_OUTPUT_ALIGNMENT_BYTESdeepinfer/model_executor/layers/linear.py:1032: every output row starts on a 32-byte boundary.
get_fp8_casting_granularitydeepinfer/model_executor/layers/linear.py:13Returns 32, the FP8 group size of the whole model.
get_fp8_cast_paramsdeepinfer/model_executor/layers/linear.py:17The cast settings: E4M3, 32 per scale, power-of-two scales, packed UE8M0, column-major scales.
bf16_gemm_ntdeepinfer/model_executor/layers/linear.py:27Thin wrapper of the DeepGEMM BF16 GEMM.
ReplicatedLinear.__init__ (runtime)deepinfer/model_executor/layers/linear.py:47Takes a loaded weight; prepares FP8 scales.
_allocate_outputdeepinfer/model_executor/layers/linear.py:83Output with a padded row stride.
_has_aligned_output_stride, _flatten_output, _align_outputdeepinfer/model_executor/layers/linear.py:97-114Stride checks and a 2-D view of the output.
apply_weight_bf16deepinfer/model_executor/layers/linear.py:116BF16 GEMM, optional bias, optional accumulator.
apply_weight_fp8deepinfer/model_executor/layers/linear.py:140Cast the input to FP8 if needed; FP8 GEMM; BF16 output.
apply_weight, forwarddeepinfer/model_executor/layers/linear.py:168-185Select the path by the weight dtype.
ReplicatedLinear (container)deepinfer/model_executor/models/v4_modules/linear.py:8-20nn.Linear plus the flag use_fp8_gemm.
ParallelRoutedLineardeepinfer/model_executor/models/v4_modules/linear.py:23-34All local experts of one projection in one 2-D weight.

Walk through the main methods#

The two containers#

class ReplicatedLinear(nn.Linear):
    """Complete linear weights, optionally prepared as FP8 on meta."""

    def __init__(
        self,
        in_features: int,
        out_features: int,
        *,
        bias: bool = False,
        use_fp8_gemm: bool = False,
    ) -> None:
        super().__init__(in_features, out_features, bias=bias)
        self.use_fp8_gemm = use_fp8_gemm


class ParallelRoutedLinear(nn.Linear):
    """Expert-major grouped GEMM metadata: (n_local_experts * h_in, h_out).

    HF materialization allocates packed FP4 directly. Dummy initialization uses
    this 2D representation before converting to the runtime layout.
    """

    def __init__(self, h_in: int, h_out: int, n_local_experts: int, *, use_fp8_gemm: bool = False) -> None:
        super().__init__(h_out, h_in * n_local_experts, bias=False)
        self.use_fp8_gemm = use_fp8_gemm
        self.n_local_experts = n_local_experts
        self.weight.n_experts = n_local_experts
  1. Lines 8-20: an ordinary nn.Linear with weight [out, in], plus use_fp8_gemm. The model is built on the meta device, so no memory is used yet (deepinfer/model_executor/models/v4_modules/containers.py:1-5).
  2. Line 31: nn.Linear(h_out, h_in * n_local_experts) has the weight shape [h_in · n_local, h_out]: the experts are stacked along the first axis. The docstring calls this the "expert-major" 2-D metadata.
  3. Line 34: the weight carries n_experts. The loader needs it to allocate the packed FP4 layout int8 [n_local, h_out, h_in / 2] (deepinfer/model_executor/models/v4_modules/containers.py:72-87).

Each rank holds only its own experts, so the split is by expert, not inside one matrix. n_local = n_physical / ep_size (deepinfer/model_executor/models/v4_modules/moe.py:94-95).

Runtime __init__: prepare the scales once#

    def __init__(
        self,
        input_size: int,
        output_size: int,
        use_fp32_output: bool = False,
        *,
        weight: torch.nn.Parameter,
        bias: torch.nn.Parameter | None = None,
    ) -> None:
        super().__init__()

        # Keep input parameters
        self.input_size = input_size
        self.output_size = output_size
        if weight.dtype != torch.bfloat16:
            assert not use_fp32_output
        self.use_fp32_output = use_fp32_output

        assert weight.shape == (output_size, input_size)
        self.weight = reconstruct_param(weight)
        if self.weight.element_size() == 1:
            self.weight.sf = weight.sf
            if self.weight.sf.dtype != PLATFORM.fp8_sf_type:
                self.weight.sf = deep_gemm.transform_sf_into_required_layout(
                    self.weight.sf,
                    self.weight.size(0),
                    self.weight.size(1),
                    recipe=(1, get_fp8_casting_granularity(), get_fp8_casting_granularity()),
                    is_sfa=False,
                )

        if bias is None:
            self.register_parameter("bias", None)
        else:
            self.bias = bias
  1. Lines 61-62: use_fp32_output is allowed only for a BF16 weight.
  2. Line 65: the weight must be [output_size, input_size].
  3. Line 66: reconstruct_param makes a new parameter on the same data and keeps sf (deepinfer/model_executor/utils.py:22-26).
  4. Lines 67-76: a one-byte weight is FP8. If its scale is not yet in the platform dtype (int32 on NVIDIA, int16 on Ascend), DeepGEMM transforms it once, with the recipe (1, 32, 32). A loaded weight arrives with FP32 scales, one per 32 × 32 block (deepinfer/hugging_face/weights.py:418-420), so this transform runs at model build.

apply_weight_fp8: cast, check, multiply#

    def apply_weight_fp8(
        self,
        x: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
        bias: torch.Tensor | None = None,
        out_acc: torch.Tensor | None = None,
    ) -> torch.Tensor:
        assert bias is None

        if isinstance(x, torch.Tensor):
            assert x.dim() == 2, f"{x.shape=}, {x.stride()=}"
            x = tile_kernels.quant.per_token_cast(x, **get_fp8_cast_params())

        # FP8 GEMM.
        weight = (self.weight, self.weight.sf)
        out_shape = x[0].shape[:-1] + (self.weight.shape[0],)
        if out_acc is None:
            out = self._allocate_output(out_shape, torch.bfloat16, x[0].device)
        else:
            out = out_acc
            assert out.shape == out_shape
            assert out.dtype == torch.bfloat16
            assert out.device == x[0].device

        assert self._has_aligned_output_stride(out), f"unaligned output: {out.shape=}, {out.stride()=}, {out.dtype=}"
        assert x[1].dtype == weight[1].dtype == PLATFORM.fp8_sf_type, f"{x[1].dtype=}, {weight[1].dtype=}"
        deep_gemm.fp8_fp4_gemm_nt(x, weight, out, out_acc, recipe=(1, 1, get_fp8_casting_granularity()))
        return out
  1. Line 146: FP8 layers have no bias.
  2. Lines 148-150: a BF16 input must be 2-D and is cast here: one E4M3 row per token and one UE8M0 scale per 32 values. An input that is already a tuple (fp8, sf) skips the cast. For example, the attention gives q_b_proj the FP8 output of its norm (deepinfer/model_executor/models/v4_modules/attn.py:458, deepinfer/model_executor/layers/layernorm.py:49-50).
  3. Lines 154-162: the output is BF16. With out_acc, the GEMM writes into that tensor.
  4. Line 164: the scales of the input and of the weight must both have the platform dtype.
  5. Line 165: out_acc is also passed as the accumulator argument c. The MoE uses this to add the shared expert to the routed output in one GEMM (deepinfer/model_executor/models/v4_modules/mlp.py:526).

apply_weight_bf16 and apply_weight#

    def apply_weight_bf16(
        self,
        x: torch.Tensor,
        bias: torch.Tensor | None = None,
        out_acc: torch.Tensor | None = None,
    ) -> torch.Tensor:
        weight = self.weight
        m_shape = x.shape[:-1]
        m = m_shape.numel()
        n, k = weight.shape
        if out_acc is None:
            out = self._allocate_output((*m_shape, n), torch.float if self.use_fp32_output else x.dtype, x.device)
        else:
            out = out_acc
            assert out.shape == (*m_shape, n)
            assert out.dtype == (torch.float if self.use_fp32_output else x.dtype)
            assert out.device == x.device
            out_acc = self._flatten_output(out_acc)
        assert self._has_aligned_output_stride(out), f"unaligned output: {out.shape=}, {out.stride()=}, {out.dtype=}"
        bf16_gemm_nt(x.view(m, k), weight, self._flatten_output(out), out_acc)
        if bias is not None:
            out += bias
        return out
  1. Line 127: the output dtype is FP32 with use_fp32_output, else the input dtype.
  2. Line 135: the GEMM sees a 2-D view [m, k] of the input and a 2-D view of the output.
  3. Lines 136-137: the bias, if any, is added after the GEMM with a normal tensor add.

apply_weight selects the path: BF16 weight → line 175; one-byte weight → line 177; anything else → TypeError (deepinfer/model_executor/layers/linear.py:168-178).

Output allocation and the 32-byte row stride#

    def _allocate_output(
        self,
        shape: tuple[int, ...],
        dtype: torch.dtype,
        device: torch.device,
    ) -> torch.Tensor:
        assert len(shape) >= 2 and shape[-1] == self.output_size, f"{shape=}, {self.output_size=}"
        itemsize = dtype.itemsize
        assert _OUTPUT_ALIGNMENT_BYTES % itemsize == 0, f"{dtype=}"
        outputs_per_alignment = _OUTPUT_ALIGNMENT_BYTES // itemsize
        physical_output_size = (self.output_size + outputs_per_alignment - 1) // outputs_per_alignment * outputs_per_alignment
        storage = torch.empty((*shape[:-1], physical_output_size), dtype=dtype, device=device)
        return storage[..., : self.output_size]

    def _has_aligned_output_stride(self, output: torch.Tensor) -> bool:
        assert output.ndim >= 2 and output.shape[-1] == self.output_size and output.stride(-1) == 1
        return output.stride(-2) * output.element_size() % _OUTPUT_ALIGNMENT_BYTES == 0

The output row stride must be a multiple of 32 bytes. _allocate_output rounds the physical row length up and returns a narrower view. The GEMM paths assert the stride before they run (deepinfer/model_executor/layers/linear.py:134, deepinfer/model_executor/layers/linear.py:163).

Deep dive#

Deep dive

Data flow: types and shapes

TensorBF16 layerFP8 layer
weightbf16 [N, K]float8_e4m3fn [N, K]
weight.sf (at load)—float32 [ceil(N/32), ceil(K/32)] (deepinfer/model_executor/models/v4_modules/containers.py:128-135)
weight.sf (after __init__)—platform dtype, layout from DeepGEMM
inputbf16 [..., K]bf16 [M, K], or (e4m3 [M, K], packed sf)
packed input sf—K / 32 scales per token, 4 per int32 (NVIDIA) or 2 per int16 (Ascend)
outputbf16 or float32 [..., N]bf16 [M, N]

Worked example (example sizes, not from the repository). An FP8 layer with N = 1,000, K = 7,168, and M = 256 tokens:

  • Weight: 7,168,000 bytes. Load-time scales: ceil(1,000/32) × 224 = 32 × 224 = 7,168 FP32 values.
  • Input cast: 256 × 7,168 bytes of E4M3, and 256 × 224 scale bytes = 256 × 56 int32 on NVIDIA.
  • Output: BF16, 16 values per 32 bytes, so the physical row is ceil(1,000 / 16) × 16 = 1,008 values. The tensor is [256, 1008], and the layer returns the view [256, 1000] with a row stride of 2,016 bytes.
  • The same layer with use_fp32_output (BF16 weight only): 8 values per 32 bytes; 1,000 is a multiple of 8, so there is no padding.
Deep dive

Merged weights

Attention merges several container weights into one runtime layer, for example Wq.A and Wkv.A into q_a_kv_swa_proj (deepinfer/model_executor/models/v4_modules/attn.py:281-288). merge_linear_weights concatenates the weights and their scales along the output axis, and frees the parts:

def merge_linear_weights(weights: list[nn.Parameter], dim: int = 0) -> nn.Parameter:
    gathered_weights = nn.Parameter(torch.cat(weights, dim=dim))
    if gathered_weights.element_size() == 1:
        for weight in weights[:-1]:  # NOTE The shape of the last weight does not have to be a multiple of 128.
            assert weight.shape[dim] % 128 == 0, f"{weight.shape=}"
        gathered_weights.sf = torch.cat([weight.sf for weight in weights], dim=dim)
    for w in weights:
        if w.element_size() == 1:
            w.sf.untyped_storage().resize_(0)
        w.untyped_storage().resize_(0)
    return gathered_weights

Line 55: every part except the last must have a multiple of 128 rows. Because 128 is a multiple of 32, each part then starts on a new 32-row scale block, so the concatenated scales (line 56) still belong to the right rows. The comment on line 54 says only that the last weight need not be a multiple of 128; the code does not say why the limit is 128 and not 32.

Deep dive

Error paths

ConditionResultSource
use_fp32_output with a non-BF16 weightAssertionErrordeepinfer/model_executor/layers/linear.py:61-62
Weight shape is not [output_size, input_size]AssertionErrordeepinfer/model_executor/layers/linear.py:65
Bias on an FP8 layerAssertionErrordeepinfer/model_executor/layers/linear.py:146
BF16 input of an FP8 layer is not 2-DAssertionErrordeepinfer/model_executor/layers/linear.py:149
out_acc with a wrong shape, dtype or deviceAssertionErrordeepinfer/model_executor/layers/linear.py:130-132, deepinfer/model_executor/layers/linear.py:159-161
Output row stride not a multiple of 32 bytesAssertionError("unaligned output ...")deepinfer/model_executor/layers/linear.py:134, deepinfer/model_executor/layers/linear.py:163
Scale dtype is not the platform dtypeAssertionErrordeepinfer/model_executor/layers/linear.py:164
Weight dtype is neither BF16 nor one byteTypeError("unsupported weight dtype")deepinfer/model_executor/layers/linear.py:178
Deep dive

Configuration

These files read no environment variable. The FP8 group size is a constant: get_fp8_casting_granularity() returns 32 (deepinfer/model_executor/layers/linear.py:13-14). Other code asserts this value, for example the packed FP4 expert layout (deepinfer/model_executor/models/v4_modules/containers.py:82). The platform scale dtype comes from PLATFORM.fp8_sf_type (deepinfer/platform.py:63, deepinfer/platform.py:138). Whether a layer is FP8 comes from the checkpoint, through use_fp8_gemm on the container.

Check yourself#

  1. 1
    Which DeepGEMM function does the runtime ReplicatedLinear call for an FP8 weight, and with which recipe?
    easy
    Show answer

    deep_gemm.fp8_fp4_gemm_nt(x, weight, out, out_acc, recipe=(1, 1, 32)) (deepinfer/model_executor/layers/linear.py:165).

  2. 2
    What is the weight shape of ParallelRoutedLinear(h_in=7168, h_out=4096, n_local_experts=8) on meta?
    easy
    Show answer

    nn.Linear(4096, 7168 × 8) gives the weight [57344, 4096]: the 8 experts are stacked along the first axis (deepinfer/model_executor/models/v4_modules/linear.py:31).

  3. 3
    When does apply_weight_fp8 skip per_token_cast, and why is that useful?
    medium
    Show answer

    When the input is already a tuple (fp8, sf) (deepinfer/model_executor/layers/linear.py:148-150). A kernel that runs just before the GEMM, such as an FP8 RMSNorm, can write FP8 output directly, so the layer does not read the BF16 data again (deepinfer/model_executor/layers/layernorm.py:49-50).

  4. 4
    A BF16 layer has output_size = 1000 and returns BF16. What is the physical row length of its output, and what row stride does the returned view have?
    hard
    Show answer

    32 bytes hold 16 BF16 values, so the row is rounded up to 1,008 values (deepinfer/model_executor/layers/linear.py:90-95). The view has 1,000 columns and a row stride of 1,008 elements = 2,016 bytes, which is a multiple of 32.