Skip to content

Add low-precision floating point data types explainer - #938

Open
mklimenko-nv wants to merge 1 commit into
webmachinelearning:mainfrom
mklimenko-nv:low-precision-fp
Open

Add low-precision floating point data types explainer#938
mklimenko-nv wants to merge 1 commit into
webmachinelearning:mainfrom
mklimenko-nv:low-precision-fp

Conversation

@mklimenko-nv

Copy link
Copy Markdown

This PR is a first step to add low-precision floating point data types to the WebNN specification. It's a follow-up to #930 with a conservative addition of bfloat16 and float8 types only, with a potential follow-up for microscaled data types.

@fdwr fdwr left a comment

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.

Tis a nice document, thanks for preparing, and I support adding low precision data types (I may just have "are we sure xyz" questions 🙂).



## 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!

- [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.

enum MLQuantizationScheme {
"affine", // scale * (x – zp) // existing
"symmetric-float", // x * scale, zp = 0
"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 had a Phi-3 WebNN demo (now Phi 4) running that used block scales.

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


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

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 becaming 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 shapes of dequantization 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.

```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.

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants