linear.py: ReplicatedLinear and the FP8 GEMM path
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.
| Files | deepinfer/model_executor/layers/linear.py (185 lines) and deepinfer/model_executor/models/v4_modules/linear.py (34 lines). |
|---|---|
| Main classes | Runtime: 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 in | The worker process of each rank, on the current device stream. Decode steps run inside CUDA graphs. |
| Called by | Runtime 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. |
| Calls | DeepGEMM: bf16_gemm_nt, fp8_fp4_gemm_nt, transform_sf_into_required_layout. tile_kernels.quant.per_token_cast. |
| Not in these files | Routed 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#
Who calls it#
- Attention makes runtime layers from merged container weights, for example
q_a_kv_swa_projandq_b_proj(deepinfer/model_executor/models/v4_modules/attn.py:284-304) andout_proj(deepinfer/model_executor/models/v4_modules/attn.py:152-156). It calls them in the forward pass (deepinfer/model_executor/models/v4_modules/attn.py:394,deepinfer/model_executor/models/v4_modules/attn.py:458,deepinfer/model_executor/models/v4_modules/attn.py:225). - Packed-KV projections use
use_fp32_outputwhere needed (deepinfer/model_executor/models/v4_modules/attn.py:337-343). The compressor wraps its key projection (deepinfer/model_executor/models/v4_modules/compress.py:42-46). - MoE. The gate is a BF16 runtime layer with FP32 output (
deepinfer/model_executor/models/v4_modules/mlp.py:222-227). The shared expert uses_replicated_linear(deepinfer/model_executor/models/v4_modules/mlp.py:144-145,deepinfer/model_executor/models/v4_modules/mlp.py:244-245) and adds its output into the MoE output without_acc(deepinfer/model_executor/models/v4_modules/mlp.py:561-562). - DFlash, the draft model, makes four runtime layers (
deepinfer/model_executor/models/mtp/dflash.py:57,deepinfer/model_executor/models/mtp/dflash.py:119,deepinfer/model_executor/models/mtp/dflash.py:148,deepinfer/model_executor/models/mtp/dflash.py:171). - The sampler computes the logits with
bf16_gemm_ntinto an FP32 tensor (deepinfer/model_executor/layers/sampler_utils.py:20-28). - FP8 producers share
get_fp8_cast_params: RMSNorm (deepinfer/model_executor/layers/layernorm.py:50), the attention output (deepinfer/model_executor/models/v4_modules/attn.py:200), and the SwiGLU of the experts (deepinfer/model_executor/models/v4_modules/mlp.py:140-141). mHC also calls it, but only in a branch for a group size other than 32, which does not run in this release (deepinfer/model_executor/models/v4_modules/hc.py:311-313). - Containers. The MoE container uses
ParallelRoutedLinearfor the routed experts and the containerReplicatedLinearfor the gate (deepinfer/model_executor/models/v4_modules/moe.py:67,deepinfer/model_executor/models/v4_modules/moe.py:97-108).prepare_meta_fp8readsuse_fp8_gemm(deepinfer/model_executor/models/v4_modules/containers.py:128).
What it calls#
deep_gemm.bf16_gemm_nt(a, b, d, c)(deepinfer/model_executor/layers/linear.py:33).tile_kernels.quant.per_token_cast(x, **get_fp8_cast_params())(deepinfer/model_executor/layers/linear.py:150).deep_gemm.fp8_fp4_gemm_nt(x, weight, out, out_acc, recipe=(1, 1, 32))(deepinfer/model_executor/layers/linear.py:165).deep_gemm.transform_sf_into_required_layout(sf, n, k, recipe=(1, 32, 32), is_sfa=False)at construction (deepinfer/model_executor/layers/linear.py:70-76).
This page describes only the arguments. The kernels are in DeepGEMM and tile_kernels, which this site does not explain.
Key classes and functions#
| Name | Line | What it does |
|---|---|---|
_OUTPUT_ALIGNMENT_BYTES | deepinfer/model_executor/layers/linear.py:10 | 32: every output row starts on a 32-byte boundary. |
get_fp8_casting_granularity | deepinfer/model_executor/layers/linear.py:13 | Returns 32, the FP8 group size of the whole model. |
get_fp8_cast_params | deepinfer/model_executor/layers/linear.py:17 | The cast settings: E4M3, 32 per scale, power-of-two scales, packed UE8M0, column-major scales. |
bf16_gemm_nt | deepinfer/model_executor/layers/linear.py:27 | Thin wrapper of the DeepGEMM BF16 GEMM. |
ReplicatedLinear.__init__ (runtime) | deepinfer/model_executor/layers/linear.py:47 | Takes a loaded weight; prepares FP8 scales. |
_allocate_output | deepinfer/model_executor/layers/linear.py:83 | Output with a padded row stride. |
_has_aligned_output_stride, _flatten_output, _align_output | deepinfer/model_executor/layers/linear.py:97-114 | Stride checks and a 2-D view of the output. |
apply_weight_bf16 | deepinfer/model_executor/layers/linear.py:116 | BF16 GEMM, optional bias, optional accumulator. |
apply_weight_fp8 | deepinfer/model_executor/layers/linear.py:140 | Cast the input to FP8 if needed; FP8 GEMM; BF16 output. |
apply_weight, forward | deepinfer/model_executor/layers/linear.py:168-185 | Select the path by the weight dtype. |
ReplicatedLinear (container) | deepinfer/model_executor/models/v4_modules/linear.py:8-20 | nn.Linear plus the flag use_fp8_gemm. |
ParallelRoutedLinear | deepinfer/model_executor/models/v4_modules/linear.py:23-34 | All 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- Lines 8-20: an ordinary
nn.Linearwith weight[out, in], plususe_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). - 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. - Line 34: the weight carries
n_experts. The loader needs it to allocate the packed FP4 layoutint8 [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- Lines 61-62:
use_fp32_outputis allowed only for a BF16 weight. - Line 65: the weight must be
[output_size, input_size]. - Line 66:
reconstruct_parammakes a new parameter on the same data and keepssf(deepinfer/model_executor/utils.py:22-26). - 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- Line 146: FP8 layers have no bias.
- 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 givesq_b_projthe FP8 output of its norm (deepinfer/model_executor/models/v4_modules/attn.py:458,deepinfer/model_executor/layers/layernorm.py:49-50). - Lines 154-162: the output is BF16. With
out_acc, the GEMM writes into that tensor. - Line 164: the scales of the input and of the weight must both have the platform dtype.
- Line 165:
out_accis also passed as the accumulator argumentc. 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- Line 127: the output dtype is FP32 with
use_fp32_output, else the input dtype. - Line 135: the GEMM sees a 2-D view
[m, k]of the input and a 2-D view of the output. - 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 == 0The 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 diveData flow: types and shapes
| Tensor | BF16 layer | FP8 layer |
|---|---|---|
| weight | bf16 [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 |
| input | bf16 [..., 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) |
| output | bf16 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 diveMerged 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_weightsLine 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 diveError paths
| Condition | Result | Source |
|---|---|---|
use_fp32_output with a non-BF16 weight | AssertionError | deepinfer/model_executor/layers/linear.py:61-62 |
Weight shape is not [output_size, input_size] | AssertionError | deepinfer/model_executor/layers/linear.py:65 |
| Bias on an FP8 layer | AssertionError | deepinfer/model_executor/layers/linear.py:146 |
| BF16 input of an FP8 layer is not 2-D | AssertionError | deepinfer/model_executor/layers/linear.py:149 |
out_acc with a wrong shape, dtype or device | AssertionError | deepinfer/model_executor/layers/linear.py:130-132, deepinfer/model_executor/layers/linear.py:159-161 |
| Output row stride not a multiple of 32 bytes | AssertionError("unaligned output ...") | deepinfer/model_executor/layers/linear.py:134, deepinfer/model_executor/layers/linear.py:163 |
| Scale dtype is not the platform dtype | AssertionError | deepinfer/model_executor/layers/linear.py:164 |
| Weight dtype is neither BF16 nor one byte | TypeError("unsupported weight dtype") | deepinfer/model_executor/layers/linear.py:178 |
Deep diveConfiguration
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.
Related concepts#
- Low-precision formats in DeepInfer: the formats, the scale rule and the packing.
- Number formats: BF16, FP8, FP4: E4M3, E2M1 and UE8M0 from the start.
- Weight loading: from a Hugging Face checkpoint to the device and hugging_face/weights.py and loading.py: the coverage plan and the three load channels: how the weights and scales arrive.
- Mega-MoE: the whole MoE layer in one call and v4_modules/mlp.py: the DeepseekMoE layer: the expert GEMMs, which do not use this class.
Check yourself#
- 1Which DeepGEMM function does the runtimeeasy
ReplicatedLinearcall for an FP8 weight, and with which recipe?Show answer
deep_gemm.fp8_fp4_gemm_nt(x, weight, out, out_acc, recipe=(1, 1, 32))(deepinfer/model_executor/layers/linear.py:165). - 2What is the weight shape ofeasy
ParallelRoutedLinear(h_in=7168, h_out=4096, n_local_experts=8)on meta?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). - 3When doesmedium
apply_weight_fp8skipper_token_cast, and why is that useful?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). - 4A BF16 layer hashard
output_size = 1000and returns BF16. What is the physical row length of its output, and what row stride does the returned view have?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.