Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions low-precision-data-types-explainer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Low Precision Data Types Explainer

## Introduction
This explainer summarizes the importance of low-precision floating point data types for web-based inference and proposes the expansion of MLOperandDataType to support emerging AI data type formats.

- [Low Precision Data Types Explainer](#low-precision-data-types-explainer)
- [Introduction](#introduction)
- [Motivation](#motivation)
- [Proposed data types overview and use cases](#proposed-data-types-overview-and-use-cases)
- [1. `bfloat16`](#1bfloat16)
- [2. `float8`](#2float8)

@fdwr fdwr Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How certain are we that this specific float8 is the float8 bit allocation that we want to codify into the API without any clarifying suffix? Will we need another float8 in the future, one that would need a distinguishing suffix (making it odd to have float8 and float8suffixed)? After all, we already determined that IEEE float16 (float16m10e5s1) was insufficient for all models, warranting float16m7e8s1.

Just to double check, the flavor of float8 proposed is this one ⭐, being IEEE-compliant, with full infinity, NaN, and distinct positive/negative zero?

float8m3e4s1_t ⭐     { uint8_t mantissa: 3; uint8_t exponent: 4; uint8_t sign: 1;} // infinities, NaNs
float8m2e5s1_t        { uint8_t mantissa: 2; uint8_t exponent: 5; uint8_t sign: 1;} // infinities, NaNs
float8m3e4s1fnuz_t    { uint8_t mantissa: 3; uint8_t exponent: 4; uint8_t sign: 1;} // no infinities, NaN as -0
float8m2e5s1fnuz_t    { uint8_t mantissa: 2; uint8_t exponent: 5; uint8_t sign: 1;} // no infinities, NaN as -0
float8m4e3s1_t        { uint8_t mantissa: 4; uint8_t exponent: 3; uint8_t sign: 1;} // infinities, NaN's
float8m0e8s0fn_t      { uint8_t mantissa: 8; uint8_t exponent: 0; uint8_t sign: 0;} // no infinities, NaN via all ones (purely range scaling)
float8m3e4b11s1fnuz_t { uint8_t mantissa: 3; uint8_t exponent: 4; uint8_t sign: 1;} // exp bias 11, no infinities, NaN as -0
float8m3e4s1fn_t      { uint8_t mantissa: 3; uint8_t exponent: 4; uint8_t sign: 1;} // infinities, NaNs

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the float16 vs bfloat16 use cases, but with float8 I find it important to maintain a balance between being explicit and user-friendly. We see that the e4m3 variant is used primarily in inference with e5m2 being more popular in training. For the sake of completeness, it is possible to specify the suffix upfront, I'll update the explainer.

On the _fnuz subvariants and other flavors my personal stance is that they're vendor-specific and would overcomplicate the specification.

@fdwr fdwr Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with float8 I find it important to maintain a balance between being explicit and user-friendly

Yeah, I'm okay with no suffix (I don't want web devs to have to type out cryptic suffixes either :b), but I'm just double checking the confidence percentage that this float8 is the float8, and if the answer is "yes, because this is what most current/upcoming hardware prefers, and this is what the models need for inference", then so be it. All the ML libraries I surveyed showed support for that type (along with others).

On the _fnuz subvariants and other flavors my personal stance...

I too would prefer the more IEEE-like float8, consistent with WebNN's existing float16 and float32 (which have infinity, NaN, and dual-sign zero), and though there's no official IEEE float8, m3e4s1 is one of the first examples shown here.

- [Proposed changes](#proposed-changes)
- [1. Extend MLOperandDataType](#1extend-mloperanddatatype)
- [2. New scale descriptor for quantization](#2new-scale-descriptor-for-quantization)
- [3. Extend per-operator type tables](#3extend-per-operator-type-tables)
- [4. Extend buffer validation mechanism](#4extend-buffer-validation-mechanism)
- [Data types and Q/DQ nodes handling](#data-types-and-qdq-nodes-handling)
- [Accuracy implications](#accuracy-implications)
- [Pros and cons](#pros-and-cons)
- [Pros](#pros)
- [Cons](#cons)
- [Security and privacy considerations](#security-and-privacy-considerations)


## Motivation
The latest developments in both specialized hardware, as well as AI and large language models (LLMs) have radically shifted the bottlenecks of machine learning. Specifically in inference, such models are rarely compute-bound nowadays, they're limited by memory bandwidth and capacity. Loading and storing billions of parameters in 32-bit (`float8`) or even 16-bit (`float16`) floating-point formats results in a massive memory footprint and overwhelms the memory bandwidth, causing high latency and power consumption.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parameters in 32-bit (float8)

Did you mean 32-bit (float32) here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's a typo, thanks for noticing!


Hardware vendors in cooperation with software researchers proved, that in most cases model weights and activations can leverage lower-precision data types with negligible accuracy impact. This led to development of native silicon support in GPUs and NPUs for such data types, allowing to both save memory footprint by compressing the weights and activations, as well as increase the compute throughput.

Currently, WebNN supports `float32` and `float16`. To bring the desktop-class AI performance to the browser, WebNN must support the data types that modern hardware is designed to accelerate. This would enable developers to deploy massive models directly in the browser with native performance, dramatically reduced download times and accelerated inference speeds.

## Proposed data types overview and use cases
### 1. `bfloat16`
Bfloat16 is a 16-bit floating-point format designed by Google Brain for training purposes. The main purpose of this format was to provide a full `float32` range at the expense of the precision. This was mainly used for the backpropagation passes of the training process to avoid an “exploding gradients” issue during the first several iterations. Combined with `float32` master weights this allowed to reduce the memory traffic and later leverage the hardware accelerated blocks with no influence on the quality of the trained model.

However, this format has been getting more traction lately in multiple application fields in inference as well, thanks to the high range of values. To name a few examples:
1. StyleGAN-like models use modulated convolutions with renormalized weights at runtime. This weights normalization produces large intermediate values that can result in `float16` overflow, forcing the usage of higher-precision data types.
2. In case of LLMs running with long contexts, KV-cache tensors and attention intermediates can span a wide dynamic range across layers and sequence length. `float16` range increases the risk of clipping and accumulated drift over long prefills and generation, degrading the output quality and causing inconsistencies.
3. When using Mixture-of-Experts (MoE) models, different experts have sparse and uneven activation scales, sometimes varying by orders of magnitude. Extended range of `bfloat16` allows for more robust expert routing.

### 2. `float8`
Float8 is a family of 8-bit floating point formats standardized under the OFP8 and Microscaling Formats (MX) Specification, extending the idea of using lower-precision data types and designed to roughly double the compute throughput relative to 16-bit floating point data types. Unlike 16-bit floating point data types, `float8` have a limited range and rely on scaling to stay numerically stable. Depending on the model, this scaling can be performed on a per-tensor, per-channel or per-block level.

OCP specification lists two main formats of `float8`, rearranging the bit allocation between the exponent and mantissa, named E5M2 (typical for gradients in training) and E4M3 (typical for weights and activations in inference), with one bit reserved for the sign. Those data types are primarily used in LLMs to speed up matrix multiplications and compress KV-cache but are also quite useful for diffusion and classical computer vision models, providing a hardware-friendly alternative to integer quantization, with competitive accuracy when properly scaled.

Despite easier upcast to `float16`, E5M2 subformat is primarily used in training and provides limited benefits for inference. In this explainer we'll focus on E4M3 format only.

## Proposed changes
We propose extending the MLOperandDataType enum and define, how these types interact with typed arrays, since JavaScript lacks native `bfloat16` and `float8` arrays.

### 1. Extend MLOperandDataType
```javascript
enum MLOperandDataType {
"float32",
"float16",
"int32",
"uint32",
"int64",
"uint64",
"int8",
"uint8",
// Proposed additions:
"bfloat16",
"float8" // Synonymous to float8e4m3 in this scenario
};
```
To address the lack of those data types in JavaScript, developers will pass those weights as `Uint16Array` for `bfloat16`, and `Uint8Array` for `float8`.

### 2. New scale descriptor for quantization
Current Q/DQ approach is designed with affine integer semantics in mind. The following changes extend it to address block-wise and microscaling formats.

```javascript
enum MLQuantizationScheme {
"affine", // scale * (x – zp) // existing
"symmetric-float", // x * scale, zp = 0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a pending issue to make zeroPoint optional, which would be equivalent to symmetric, right?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's correct. I'd like to add to that that the 3% performance improvement is a conservative estimation based on my previous experiments.

"blockwise-float" // per-block scaling tensor

@fdwr fdwr Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dequantizeLinear already supports blockwise scales? See emulated decomposition:

function dequantizeLinear(builder, input, scale, zeroPoint, options)
{
    // output = (input - zeroPoint) * scale
    const floatInput = builder.cast(input, scale.dataType);
    const floatZeroPoint = builder.cast(zeroPoint, scale.dataType);
    const upsampledScale = blockwiseExpand(builder, scale, input.shape);
    const upsampledZeroPoint = blockwiseExpand(builder, floatZeroPoint, input.shape);
    return builder.mul(builder.sub(floatInput, upsampledZeroPoint), upsampledScale);
}

I think we had a Phi-3 WebNN demo running some time ago that used block scales (though, it's now Phi 4, and I'm not sure if it's been kept up to date with the API) .

If this was unclear from the documentation, we should clarify it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed it, thanks for pointing out

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

};

dictionary MLQuantizationOptions : MLOperatorOptions {
MLQuantizationScheme scheme = "affine";
unsigned long blockSize;

@fdwr fdwr Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We didn't add blockSize after becoming apparent that it was redundant from the tensor shapes, and given the ratio of the input tensor's shape and zero-point shape would have needed to be validated anyway, it's cleaner to just remove a potential failure point. Plus it's more flexible than a single block size since multiple flavors of dequantization shapes exist (along rows, columns, square blocks, asymmetric...). Of course, performance is best when callers use shapes that align to whatever the underlying backend's have been optimized for (and it's actually worse performance if it's incompatible and you have to emulate things), but it's hard to know what backend you'll run on, and the current design doesn't lock us into so rigid a box when it changes later.

};
```

### 3. Extend per-operator type tables
Every operator specification provides a tensor limits table with allowed data types for different operands. With an introduction of additional low-precision floating-point data types those tables need to be reviewed and extended.

### 4. Extend buffer validation mechanism
Currently, buffer validation is only done for strongly typed buffers. In case of low-precision floating-point data types, this check is significantly simplified and would always return True for `ArrayBufferView` due to usage of `Uint8Array`. The algorithm should consider that the low-precision data types are represented using non-native JavaScript data types, but the dimensions and the element count still needs to be validated.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the low-precision data types are represented using non-native JavaScript data types

Note there's precedent for using a different underlying buffer type than the tensor's data type, as we had to use Uint16Array for float16 tensors, before Float16Array existed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll rephrase it for the next iteration of the explainer, thanks!


## Data types and Q/DQ nodes handling
Due to a significant variety of hardware available to users, spanning across multiple generations, it is expected that some data types could be unsupported. WebNN delegates the graph execution to the underlying framework (like DirectML, CoreML, Windows ML).

If the data type is supported on the target hardware, the underlying inference framework will fuse the Q/DQ nodes into the operation, emitting a quantized operation. This is the best-case scenario with low memory footprint and good performance.

Figure 1 shows the explicit graph with Q/DQ nodes, that is obtained either by quantization-aware training (QAT) or by post-training quantization (PTQ) processes. The model quantization process treats those nodes as "fake" quantization, coarsening up the precision.

```mermaid
flowchart TD
W([Weights])
W -->|"Weights (fp32)"| QL1[QuantizeLinear]
QL1 -->|"Weights (quantized)"| DQL1[DequantizeLinear]
DQL1 -->|"Weights (fp32)"| Op[Op]

Input[ ] -->|"Input (fp32)"| QL2[QuantizeLinear]
QL2 -->|"Input (quantized)"| DQL2[DequantizeLinear]
DQL2 -->|"Input (fp32)"| Op

Op -->|"Output (fp32)"| QL3[QuantizeLinear]
QL3 -->|"Output (quantized)"| Output[ ]

subgraph sub1 [" "]
W
QL1
end

subgraph sub2 [" "]
DQL1
DQL2
Op
QL3
end

classDef invisible fill:none,stroke:none,color:none
class Input,Output invisible
```
Figure 1. Subgraph with explicit Q/DQ nodes


There are two ways to execute such graph, depending on the hardware and software capabilities: native fused execution or emulation. In case the software stack supports fused execution, the graph in figure 1 would be transformed into the graph, depicted in figure 2.

```mermaid
flowchart TD
W([Weights])
W -->|"Weights (quantized)"| QOp[QuantizedOp]

Input[ ] -->|"Input (fp32)"| QL[QuantizeLinear]
QL -->|"Input (quantized)"| QOp

QOp -->|"Output (quantized)"| Output[ ]

classDef invisible fill:none,stroke:none,color:none
class Input,Output invisible
```
Figure 2. Subgraph with fused Q/DQ nodes

It is important to mention, that the fused quantized operation doesn't necessarily mean optimized hardware support for the specific data type in matrix multiplication acceleration units. It can also represent the ability to provide an operation, that dequantizes the data on the registers without the need to submit a separate operation or kernel. The prime example of this is LLM inference with 4-bit weights and `float16` activations, where the weights are dequantized in the kernel prologue.

In case quantization fusion isn't supported for a specific operation-data type combination, the frameworks are expected to gracefully fall back to an explicit execution of quantize/dequantize operations. Those nodes can't be omitted for two reasons: quantization is a non-linear process, and there is a possibility, that during the QAT the weights were adjusted to meet the accuracy targets, which won't be reached without quantization.

The end goal is to execute the provided model when the hardware natively supports those data types and when it doesn't, a warning could be emitted to let the user know that this model is suboptimal on this machine due to a specific reason, to give the possibility of locating another model or requantizing it. WebNN backends can expose the list of supported data types as a first-line checks to provide a high-level feedback before passing the model for execution.

## Accuracy implications
It is evident, that reducing precision from 32-bit to 8-bit or even 4-bit introduces quantization noise. Thankfully, floating-point data types degrade a lot more gracefully than their integer counterparts (`int8` and `int4`).
1. `bfloat16` — often near-lossless accuracy loss for most models, in most cases can be used as a stand-in replacement for `float16`.
2. `float8` — in most cases requires a calibration dataset for post-training quantization. In most cases, outliers are handled better than in `int8` due to 4-bit exponent in E4M3 flavor.

## Pros and cons
### Pros
1. Significant memory footprint and bandwidth savings compared to `float32`. Not all models are conformant in `float16`, currently forcing users to revert to `float32` instead of possible `bfloat16` variant.
2. Unlocks the full hardware TOPS capabilities, which are often advertised in 8-bit or 4-bit data types.
3. Low-precision floating point data types handle activation outliers better than a regular int8 symmetric quantization.
4. Storing the weights in low-precision data types results in smaller model size, resulting in reducing the download time and memory footprint.

### Cons
1. Introduces additional API complexity into `quantizeLinear` and `dequantizeLinear` ops to take block sizes and scales into account.
2. Packing and unpacking low-precision floating point types using placeholder data storage containers like `Uint8Array` places an ergonomic burden on the web developer and reduces the amount of available type checks.
3. Additional pressure is put on the WebNN backends to implement robust software fallbacks when hardware support is absent.

## Security and privacy considerations
The introduction of low-precision types doesn't introduce additional security threats beyond those already discussed in the main WebNN specification, like out-of-bounds memory access or timing attacks.

Different hardware architectures may leverage different data types for accumulation and implement different rounding approaches, because of that, model outputs may lack bit-wise determinism across devices. This fingerprinting vector is shared by all floating-point hardware acceleration units and should be mitigated by the same privacy mechanisms standard to WebNN.