DeepInfer Explained

Number formats: BF16, FP8, FP4

BeginnerLearn the basicsabout 8 min read + 6 min deep dives

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

In one sentence

A number format decides how many bits each value uses; fewer bits save memory and time, and a shared scale keeps small formats accurate.

The idea#

Why fewer bits?#

Every weight, activation and KV cache entry is a number in device memory. The dtype of a tensor sets how many bits each number uses. Fewer bits help in three ways:

  • Memory. A model in 8-bit numbers needs half the memory of the same model in 16-bit numbers.
  • Bandwidth. Decode is memory-bound (see GPUs, kernels, and speed). Half the bytes means about half the time.
  • Math. Tensor Cores do more FP8 or FP4 operations per second than BF16 operations.

The cost is accuracy. A small format has fewer possible values, so most numbers must be rounded. Engineers make this rounding small with quantization methods, mainly with scales.

Sign, exponent and mantissa#

A floating-point number has three bit fields:

value = (−1)^sign × 1.mantissa × 2^(exponent − bias)

sign      1 bit: + or −
exponent  e bits: the power of two (sets the range)
mantissa  m bits: the digits after the binary point (sets the precision)

More exponent bits give a larger range: very large and very small numbers. More mantissa bits give finer steps between neighbour values. When the exponent field is 0, the number is subnormal: 0.mantissa × 2^(1 − bias). Subnormals fill the gap between 0 and the smallest normal number.

The formats on this page#

FormatBits (sign / exponent / mantissa)BiasLargest valueStep near 1.0
FP321 / 8 / 231273.4 × 10382−23
BF161 / 8 / 71273.4 × 10382−7 = 0.0078
FP161 / 5 / 101565,5042−10 ≈ 0.001
FP8 E4M31 / 4 / 374480.125
FP8 E5M21 / 5 / 21557,3440.25
FP4 E2M11 / 2 / 1160.5
UE8M0 (scales only)0 / 8 / 01272127only powers of two

BF16 has the same exponent as FP32, so it has the same range with less precision. This is why models train and run in BF16 without overflow. FP16 is more precise, but its range ends at 65,504.

FP8 has two common layouts. E4M3 has more precision; E5M2 has more range. Inference engines usually use E4M3 for weights and activations. FP4 E2M1 has only 16 codes. Its positive values are 0, 0.5, 1, 1.5, 2, 3, 4 and 6.

A worked conversion: 0.3#

0.3 = 1.2 × 2−2. So the exponent is −2 and the mantissa is 0.2. Each format rounds 0.2 to its mantissa bits:

FormatMantissaBitsStored valueError
BF160.2 × 27 = 25.6 → 260 01111101 0011010 = 0x3E9A(1 + 26/128) × 0.25 = 0.300781250.26 %
FP160.2 × 210 = 204.8 → 2050 01101 0011001101 = 0x34CD0.300048830.016 %
E4M30.2 × 23 = 1.6 → 20 0101 010 = 0x2A(1 + 2/8) × 0.25 = 0.31254.2 %
E2M1(0.3 lies between the codes 0 and 0.5)0 00 10.567 %

The exponent field of E4M3 is −2 + 7 = 5 = 0101. The exponent field of BF16 is −2 + 127 = 125 = 01111101.

Scales: keep small formats accurate#

E2M1 cannot store 0.3 well on its own. But if we first divide by a scale factor, the value can land near a code. Divide 0.3 by 0.0625: the result is 4.8, and the nearest code is 4. Then 4 × 0.0625 = 0.25, an error of 17 % instead of 67 %.

The scale must fit the numbers that share it. With one scale for a whole tensor, one large value forces a large scale on all other values. So modern formats use block scaling: a small block of values (for example 16 or 32) shares one scale.

  • MXFP4: 32 E2M1 values + 1 UE8M0 scale = 17 bytes, 4.25 bits per value.
  • MXFP8: 32 FP8 values + 1 UE8M0 scale = 33 bytes, 8.25 bits per value.
  • FP4 with an E4M3 scale per 16 values: 9 bytes per 16 values, 4.5 bits per value.

A UE8M0 scale holds only a power of two: 2^(code − 127). Multiplying by a power of two changes only the exponent, so it adds no rounding error of its own. An E4M3 scale can be any FP8 value, so it fits the block better, but it costs a few more bits per value.

Try it. The interactive number-format converter shows one number in BF16, FP8 E4M3 and FP4 E2M1. It also encodes a block of up to 32 values with one shared UE8M0 scale. Add an outlier and see how many small values become 0.

An everyday comparison#

Think of a price list with the header "all prices in thousands of euros". Each row needs only a short number, such as 4 or 1.5, and the header gives the size. A block scale is that header. A list with very different prices (5 euros and 5 million euros) needs several headers, one per small group of rows.

The comparison stops here: a price list can write any short number, but an FP4 row can only write one of 16 codes. And a UE8M0 header can only be a power of two, such as "in units of 4" or "in units of 8".

The picture#

Figure: panel 1 shows how each format splits its bits. Panel 2 shows that fewer mantissa bits give a larger rounding error for the same value. Click the diagram to zoom.
Open full size ↗

Read panel 1 from top to bottom: the strips get shorter, and the mantissa (amber) shrinks first. Panel 2 shows the price: the error grows as the mantissa shrinks.

Figure: panel 1 is a worked MXFP4 block. Panel 2 lists the formats that DeepInfer uses. Click the diagram to zoom.
Open full size ↗

In DeepInfer#

This is a basics page, so this part only gives pointers. The page Low-precision formats in DeepInfer explains the full code.

  1. The checkpoint must be FP8 with FP4 experts. The config loader accepts only one quantization setup, and turns it into the flags fp8_attn, fp8_mlp, mxfp8 and mxfp4:
    quant = hf["quantization_config"]
    for key, expected in {
        "quant_method": "fp8",
        "expert_dtype": "fp4",
        "activation_scheme": "dynamic",
        "weight_block_size": [32, 32],
        "scale_fmt": "ue8m0",
    }.items():
        if quant[key] != expected:
            raise ValueError(f"Unsupported HF {key}: {quant[key]!r}; expected {expected!r}")
    fp8 = quant["quant_method"] == "fp8"
    mxfp4 = quant["expert_dtype"] == "fp4"
FP8 weights with 32 × 32 blocks, FP4 experts, dynamic activation scales and UE8M0 scales. The flags are set on lines 301-306 (deepinfer/hugging_face/config.py:301-306).
  1. The KV cache has its own formats. The main cache uses FP4 with an E4M3 scale per 16 values (288 bytes per entry) by default, or MXFP8 (528 bytes) (deepinfer/model_executor/layers/tilelang_ops/cache_common.py:109-128, deepinfer/hugging_face/config.py:303). See The KV cache.
  2. Scales are packed per platform. FP8 GEMMs take UE8M0 scales packed into torch.int32 on NVIDIA and torch.int16 on Ascend (deepinfer/platform.py:63, deepinfer/platform.py:138). See Hardware platforms: NVIDIA and Ascend.

Deep dive#

The sections below give the full detail. Open them one at a time, or use Open all deep dives at the top of the page.

Deep dive

Decoding bit patterns, step by step

E4M3 (bias 7). Split the byte into s eeee mmm.

ByteFieldsValue
0x2A0 · 0101 · 010(1 + 2/8) × 25 − 7 = 1.25 × 0.25 = 0.3125
0x7E0 · 1111 · 110(1 + 6/8) × 215 − 7 = 1.75 × 256 = 448 (the maximum)
0x7F0 · 1111 · 111NaN (E4M3 "fn" has no infinity)
0x080 · 0001 · 0001 × 21 − 7 = 2−6 (smallest normal)
0x010 · 0000 · 001subnormal: (1/8) × 21 − 7 = 2−9 (smallest positive)
0xC41 · 1000 · 100−(1 + 4/8) × 28 − 7 = −3

DeepInfer's kernels use the same limits: FLOAT8E4M3_MAX = 448.0 and FLOAT8E4M3_MIN_POSITIVE = 2**-9 (deepinfer/model_executor/layers/tilelang_ops/common.py:26-27).

E2M1 (bias 1). Split 4 bits into s ee m. All 8 positive codes:

Code00000001001000110100010101100111
Value00.5 (subnormal)11.52346

Example: 0101 is exponent field 2 and mantissa 1: (1 + 1/2) × 22 − 1 = 3. The maximum is 6 (deepinfer/model_executor/layers/tilelang_ops/common.py:24). Two FP4 codes share one byte. DeepInfer stores packed expert weights as int8 with shape (n_local, h_out, h_in // 2) (deepinfer/hugging_face/weights.py:283).

UE8M0 (bias 127). The whole byte is the exponent. DeepInfer decodes it exactly so:

def _ue8m0_to_fp32(scale: torch.Tensor) -> torch.Tensor:
    """F8_E8M0 (exponent only) → fp32: value = 2 ** (bits - 127), lossless restore."""
    assert scale.dtype == torch.float8_e8m0fnu, f"expected F8_E8M0, got {scale.dtype}"
    exponents = scale.view(torch.uint8).to(torch.int32) - 127
    return torch.ldexp(torch.ones_like(exponents, dtype=torch.float32), exponents)
0x7E = 126 gives 2−1 = 0.5. 0x7F = 127 gives 1.0. 0x85 = 133 gives 26 = 64.
Deep dive

Block scaling: the math and two worked blocks

Granularity. A scale can cover a whole tensor, one row (one token or one output channel), or a small block. Smaller groups follow the data better, but they need more scales. The cost in bits per value is value_bits + scale_bits / group_size.

FormatGroupScaleBits per value
BF16nonenone16
MXFP8 (E4M3)32UE8M08 + 8/32 = 8.25
MXFP4 (E2M1)32UE8M04 + 8/32 = 4.25
FP4, E4M3 scale16E4M34 + 8/16 = 4.5

A power-of-two scale (MXFP4). A common rule: scale = 2^ceil(log2(amax / 6)), so that the largest value fits into the E2M1 maximum. Block (8 of 32 values shown): 0.12, −0.53, 1.70, 0.04, −2.90, 0.88, 0.31, −0.07.

  1. amax = 2.90. 2.90 / 6 = 0.483. log2(0.483) = −1.05, ceil = −1. Scale = 2−1 = 0.5, UE8M0 code 126.
  2. Divide: 0.24, −1.06, 3.4, 0.08, −5.8, 1.76, 0.62, −0.14.
  3. Round to E2M1: 0, −1, 3, 0, −6, 2, 0.5, −0.
  4. Multiply back: 0, −0.5, 1.5, 0, −3.0, 1.0, 0.25, 0. The largest error is 0.20 (for 1.70).

An E4M3 scale per 16 values. DeepInfer's reference code for the FP4 KV cache takes amax / 6 and stores it as E4M3 (deepinfer/model_executor/layers/tilelang_ops/cache_common.py:339-345). For the same 8 values: 2.90 / 6 = 0.483, and the nearest E4M3 value is 0.46875.

  1. Divide: 0.26, −1.13, 3.63, 0.09, −6.19, 1.88, 0.66, −0.15. The value −6.19 is outside the range and becomes −6.
  2. Round to E2M1: 0.5, −1, 4, 0, −6, 2, 0.5, −0.
  3. Multiply back: 0.23, −0.47, 1.88, 0, −2.81, 0.94, 0.23, 0.

The sum of the 8 errors is 0.68, against 0.74 for the power-of-two scale. The finer scale helps a little, and smaller blocks help more.

Deep dive

Weights in DeepInfer: shapes, scales and bytes

Linear layers (FP8). Before loading, DeepInfer turns each FP8 linear weight into float8_e4m3fn and declares a scale tensor with one value per 32 × 32 block. The token embedding, the head and the FP4 experts are skipped (deepinfer/model_executor/models/v4_modules/containers.py:113-135):

weight  W  : float8_e4m3fn  [N, K]
scale   sf : float32        [ceil(N/32), ceil(K/32)]   (one scale per 32 × 32 block)

The loader reads the UE8M0 scale bytes, turns them into FP32 without loss, and then packs them for the GEMM library (deepinfer/hugging_face/weights.py:418-427, deepinfer/hugging_face/weights.py:13-16).

Worked example. W = [4,096, 4,096]. Data: 16 MiB in FP8 (32 MiB in BF16). Scales: 128 × 128 = 16,384 blocks, 16 KiB as UE8M0 bytes.

Routed experts (MXFP4). One parameter holds the experts of one rank as packed FP4 in int8, plus a packed scale tensor of dtype int16 or int32 (deepinfer/hugging_face/weights.py:304-305). The expert GEMM reads one scale per 32 weights (deepinfer/model_executor/models/v4_modules/mlp.py:118-128). Example: one expert linear1 with h_out = 4,096 and h_in = 4,096. Data: 4,096 × 2,048 bytes = 8 MiB. Scales: 4,096 × 128 = 512 KiB. Total 8.5 MiB, which is 4.25 bits per weight.

BF16 parts. The checkpoint must be in BF16 apart from the quantized weights (deepinfer/hugging_face/config.py:58-59). The gate projection stays in BF16 (deepinfer/model_executor/models/v4_modules/moe.py:66-67). The logits come from a BF16 GEMM with an FP32 output (deepinfer/model_executor/layers/sampler_utils.py:20-28).

Deep dive

Activations and GEMMs in DeepInfer

A ReplicatedLinear with an FP8 weight casts its BF16 input to FP8 on the fly. Each token row gets one scale per 32 values:

    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 150: per_token_cast with the parameters of lines 17-24: format e4m3, 32 values per scale, power-of-two scales (round_sf), packed UE8M0.
  2. Line 156: the output is BF16.
  3. Line 164: the activation scales and the weight scales must both have the platform scale dtype.
  4. Line 165: deep_gemm.fp8_fp4_gemm_nt multiplies, with the recipe (1, 1, 32).

Shapes for one GEMM input, with M = 1,536 tokens and K = 4,096:

x       BF16    [1536, 4096]      12 MiB
x_fp8   E4M3    [1536, 4096]       6 MiB
scales  UE8M0   [1536, 4096/32] = [1536, 128] codes
        packed: [1536, 32] int32 (NVIDIA, 4 codes each) or [1536, 64] int16 (Ascend, 2 codes each)

Counting FLOPs. DeepInfer counts an FP8 × FP4 expert GEMM at the FP8 rate, because FP4 storage alone does not mean FP4 × FP4 math (csrc/engine/flops_estimator.hpp:84-86).

Deep dive

KV cache formats, field by field

KVCacheFormat describes one token entry (deepinfer/model_executor/layers/tilelang_ops/cache_common.py:24-42). Its size is quantized_bytes + sf_bytes, where FP4 counts 4 bits and FP8 counts 8 bits per value (deepinfer/model_executor/layers/tilelang_ops/cache_common.py:60-101).

FormatValuesPer scaleScaleBytesUse
V41_FORMAT512 × E4M332UE8M0512 + 16 = 528main (MXFP8) and SWA
V41_FP4_FORMAT512 × E2M116E4M3256 + 32 = 288main, default
FP4_INDEX_KV_FORMAT128 × E2M132UE8M064 + 4 = 68indexer keys
MXFP8_INDEX_KV_FORMAT128 × E4M332UE8M0128 + 4 = 132indexer keys (other option)
FP8_FLOAT32_SF_INDEX_KV_FORMAT128 × E4M3128FP32128 + 4 = 132NVIDIA only

Sources: deepinfer/model_executor/layers/tilelang_ops/cache_common.py:109-162. The last format is not in the supported list on NPU (line 160). The model logs its choice at load time, for example "Main KV: FP4; Indexer QK: FP4" (deepinfer/model_executor/models/deepseek.py:459-460).

Worked example. A C1 layer stores one entry per token. With FP4: 128 tokens × 288 B = 36,864 B per block and layer. With MXFP8: 128 × 528 = 67,584 B. FP4 saves 45 % of the main cache. In BF16, the same 512 values would need 1,024 B per token.

What can go wrong#

  • A checkpoint in another format. The config loader raises ValueError("Unsupported HF ...") if the quantization method, expert dtype, activation scheme, block size or scale format differ (deepinfer/hugging_face/config.py:254-262). A model dtype other than BF16 also raises (deepinfer/hugging_face/config.py:58-59).
  • FP4 experts from a plain FP8 checkpoint. The dummy FP4 path asserts that the FP8 bytes are FP4-compatible: "Only support FP4-QAT checkpoint" (deepinfer/model_executor/models/v4_modules/mlp.py:60). QAT means that training already used FP4.
  • Wrong scale dtype. An FP8 GEMM asserts that both scale tensors have the platform type, int32 or int16 (deepinfer/model_executor/layers/linear.py:164).
  • Overflow and outliers. E4M3 ends at 448 and E2M1 at 6. One very large value in a block makes the scale large, and the small values of the same block round to 0. Block scaling limits this damage to one block.

Why it matters. The format of a tensor sets its memory, its bandwidth and the speed of the math on it. In DeepInfer, FP4 experts need about a quarter of the BF16 memory. FP8 activations halve the GEMM input traffic. The FP4 KV cache lets more sequences run at the same time.

Check yourself#

  1. 1
    Why can a model run in BF16 without overflow, when FP16 sometimes overflows?
    easy
    Show answer

    BF16 has 8 exponent bits, like FP32, so its largest value is about 3.4 × 1038. FP16 has only 5 exponent bits and ends at 65,504. BF16 gives up mantissa bits (7 instead of 10) to keep the range.

  2. 2
    Decode the E4M3 byte 0x44.
    easy
    Show answer

    0x44 = 0 1000 100. Exponent field 8, so 28 − 7 = 2. Mantissa 4/8 = 0.5. Value = 1.5 × 2 = 3.

  3. 3
    An MXFP4 block has amax = 10. Which power-of-two scale does the rule 2^ceil(log2(amax / 6)) give, and what is its UE8M0 code?
    medium
    Show answer

    10 / 6 = 1.67; log2(1.67) = 0.74; ceil = 1. Scale = 21 = 2, so the largest value becomes 5, a tie between the codes 4 and 6. UE8M0 code = 1 + 127 = 128 = 0x80 (decoded by deepinfer/hugging_face/weights.py:163-167).

  4. 4
    How many bytes does one main KV cache entry take in DeepInfer with the default format, and how is that number made?
    medium
    Show answer

    288 bytes: 512 FP4 values (256 bytes) plus one E4M3 scale per 16 values (32 bytes) (deepinfer/model_executor/layers/tilelang_ops/cache_common.py:119-128). The default comes from use_fp4_sparse_kv, which is True unless the checkpoint config says otherwise (deepinfer/hugging_face/config.py:303).

  5. 5
    An FP8 GEMM input has M = 512 tokens and K = 7,168. How many UE8M0 codes are there, and how many int32 values hold them on NVIDIA?
    hard
    Show answer

    One code per 32 values of a row: 7,168 / 32 = 224 codes per token, 512 × 224 = 114,688 codes (deepinfer/model_executor/layers/linear.py:17-24). On NVIDIA, four codes share one int32 (deepinfer/platform.py:63): 114,688 / 4 = 28,672 int32 values, or 56 per token.