Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rounding operators #817

Open
fdwr opened this issue Feb 14, 2025 · 0 comments
Open

Rounding operators #817

fdwr opened this issue Feb 14, 2025 · 0 comments

Comments

@fdwr
Copy link
Collaborator

fdwr commented Feb 14, 2025

Summary

While writing out the emulation decomposition for quantizeLinear, I lacked rounding functions in WebNN, particularly the default IEEE standard round-tied-halves-to-nearest-even. There's not a nice way to simulate this missing function, and it's a basic primitive that belongs in a library anyway. So I propose filling in some gaps ⭐:

  • Add roundEven() which is the IEEE standard's default, and relevant ML libraries support it (see support below).
  • Document trunc via emulation at least. I originally thought it worth adding, but I haven't actually seen it in models, and evidently only DML ROUND and PyTorch torch.trunc have it, not TFLite/CoreML/ONNX. There are emulations below.

Modes

Rounding mode WebNN IEEE C++ equivalent JS equivalent
RHE nearest int, halves to even ⭐ (banker's rounding) convertToIntegerTiesToEven(x)
The recommended IEEE float default
C23 roundeven
std::nearbyint FE_TONEAREST
❌😲??
RHAZ nearest int, halves away from zero floor(abs(x) + 0.5) * sign(x) convertToIntegerTiesToAway(x) std::round
RHTZ nearest int, halves toward zero ceil(abs(x) - 0.5) * sign(x)
RHU nearest int, halves up floor(x + 0.5) Math.round
RHD nearest int, halves down ceil(x - 0.5)
RAZ away from zero ceil(abs(x) * sign(x))
RTZ toward zero (trunc) ⭐ floor(abs(x) * sign(x)) convertToIntegerTowardZero(x) std::trunc
std::nearbyint FE_TOWARDZERO
Math.trunc
RU up to positive infinity (ceil) ceil(x) convertToIntegerTowardPositive(x) std::ceil
std::nearbyint FE_UPWARD
Math.ceil
RD down to negative infinity (floor) floor(x) convertToIntegerTowardNegative(x) std::floor
std::nearbyint FE_DOWNWARD
Math.floor

Note

  • Some of these modes are not generally useful, but they are very useful within their domain. For example, RHD is used often in graphics when rasterizing triangles or resampling images rather than RHE or other modes to avoid staggered/asymmetric pattern artifacts (WebNN's resample uses RHD internally). I'm not proposing those though.
  • Javascript's Math.round is oddly aberrant, being neither RHE or RHAZ (the two most common IEEE nearest modes). So beware when using it as a baseline reference. Also I'm using "up" (higher value toward positive infinity) and "down" (lower value toward negative infinity) like C's usage, not Java's bidirectional RoundingMode where up and down mean away from zero and toward zero.

Library support

Examples

Original value -2.5 -1.75 -1.5 -1.25 0 1.25 1.5 1.75 2.5
RHE nearest int, halves to even -2 -2 -2 -1 0 1 2 2 2
RHAZ nearest int, halves away from zero -3 -2 -2 -1 0 1 2 2 3
RHTZ nearest int, halves toward zero -2 -2 -1 -1 0 1 1 2 2
RHU nearest int, halves up -2 -2 -1 -1 0 1 2 2 3
RHD nearest int, halves down -3 -2 -2 -1 0 1 1 1 2
RAZ away from zero -3 -2 -2 -2 x 2 2 2 3
RTZ toward zero (trunc) -2 -1 -1 -1 0 1 1 1 2
RU up to positive infinity (ceil) -2 -1 -1 -1 0 2 2 2 3
RD down to negative infinity (floor) -3 -2 -2 -2 0 1 1 1 2

Emulation

RHE (roundEven)

A backend can emulate RHE if it has modulus/remainder and trunc, as x - std::remainder(x, static_cast<T>(1)) (source) (even though WebNN itself lacks both those operators).

Another odd approach that evidently works for float32 (but not as-is for float16/float64) uses a few condition checks with addition and subtraction, but that ends up being a clumsy construction in WebNN.

float roundHalvesToNearestEven(float f)
{
    if (!(f > -8388608.0f && f < 8388608.0f)) // Return true for NaN
        return f;
    else if (f > 0)
        return float(f + 8388608.0f) - 8388608.0f;
    else
        return float(f - 8388608.0f) + 8388608.0f;
}
// output = abs(input) < magicValue ? (abs(input) + magicValue - magicValue) * sign(input) : input
function roundEven(builder, input)
{
    const magicValue = builder.const("float32", 8388608.0f);
    const absInput = builder.abs(input);
    builder.where(
        builder.less(absInput, magicValue),
        builder.mul(
            build.sub(builder.add(absInput, magicValue), magicValue),
            builder.sign(input)
        ),
        input
    );
}

RTZ (trunc)

// output = floor(abs(input)) * sign(input)
function trunc(builder, input)
{
    return builder.mul(
        builder.floor(builder.abs(input)),
        builder.sign(input)
    );
}

There's also an approximation using a round-trip cast (float -> int -> float), but that's lossy with large numbers.

Models

  • RHE:
    • tiny-yolov3-11\yolov3-tiny.onnx
    • QWNet
  • RTZ
    • ? (none personally known)

API

partial interface MLGraphBuilder {
  MLOperand roundEven(MLOperand input, optional MLOperatorOptions options = {});
};

partial dictionary MLOpSupportLimits {
  MLSingleInputSupportLimits roundEven;
};
operand allowed data types allowed ranks
input "float32", "float16" N
output same as input same as input

Naming: Why not just "round"? round is woefully ambiguous as evidenced from the differences between C++ std::round (RHAZ) vs Javascript/Java Math.round (RHU) vs round in others (RHE) 😕. roundEven (like C23 roundeven) is directly clear in the name.

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

No branches or pull requests

2 participants