ai.onnx.DynamicQuantizeLinear / build /webgpu /dynamic-quantize-linear.wgsl.jinja
Xenova's picture
Xenova HF Staff
sync 2e7068faf55e
bbc3807 verified
Raw
History Blame
6.84 kB
{% if source.fromPartials %}
// Parallel range finalization folds the guarded min/max partials and computes
// the output scale and zero point. Each partial already includes zero in its range.
{% else %}
// The serial path computes the complete range, scale, zero point, and quantized
// output in one invocation.
{% endif %}
{{ env.wgsl.resourceDeclarations }}
// ONNX DynamicQuantizeLinear uses correctly rounded f32 division followed by
// round-half-to-even. Every execution path uses these helpers so their numerical
// behavior cannot drift apart.
// WGSL permits f32 division to differ from the correctly-rounded result by
// 2.5 ULP, and fma() inherits separate multiply/add accuracy rather than
// promising a fused residual. Reconstruct the correctly-rounded normal result
// with integer significand division when the quotient can affect an integer
// rounding boundary. This is backend-independent and uses only exact u32 ops.
fn dynamic_quantize_exact_div_normal(numerator: f32, denominator: f32) -> f32 {
if (numerator == 0.0) {
return numerator;
}
let numerator_bits = bitcast<u32>(numerator);
let denominator_bits = bitcast<u32>(denominator);
let sign_bits = (numerator_bits ^ denominator_bits) & 0x80000000u;
let numerator_abs = numerator_bits & 0x7fffffffu;
let denominator_abs = denominator_bits & 0x7fffffffu;
if (denominator_abs == 0u
|| (numerator_abs & 0x7f800000u) == 0x7f800000u
|| (denominator_abs & 0x7f800000u) == 0x7f800000u) {
return numerator / denominator;
}
var numerator_mantissa = numerator_abs & 0x007fffffu;
var denominator_mantissa = denominator_abs & 0x007fffffu;
let numerator_biased_exponent = (numerator_abs >> 23u) & 0xffu;
let denominator_biased_exponent = (denominator_abs >> 23u) & 0xffu;
var numerator_exponent: i32;
var denominator_exponent: i32;
if (numerator_biased_exponent == 0u) {
numerator_exponent = -126;
// Zero returned above. A non-zero subnormal reaches the implicit-bit
// position in at most 23 exact shifts.
while ((numerator_mantissa & 0x00800000u) == 0u) {
numerator_mantissa = numerator_mantissa << 1u;
numerator_exponent = numerator_exponent - 1;
}
} else {
numerator_mantissa = numerator_mantissa | 0x00800000u;
numerator_exponent = i32(numerator_biased_exponent) - 127;
}
if (denominator_biased_exponent == 0u) {
denominator_exponent = -126;
while ((denominator_mantissa & 0x00800000u) == 0u) {
denominator_mantissa = denominator_mantissa << 1u;
denominator_exponent = denominator_exponent - 1;
}
} else {
denominator_mantissa = denominator_mantissa | 0x00800000u;
denominator_exponent = i32(denominator_biased_exponent) - 127;
}
var quotient_exponent = numerator_exponent - denominator_exponent;
var remainder = numerator_mantissa;
if (remainder < denominator_mantissa) {
remainder = remainder << 1u;
quotient_exponent = quotient_exponent - 1;
}
// The normalized ratio is now in [1, 2). Emit its implicit bit followed by
// all 23 stored significand bits using exact binary long division.
var quotient_mantissa = 0x00800000u;
remainder = remainder - denominator_mantissa;
for (var digit = 0u; digit < 23u; digit = digit + 1u) {
remainder = remainder << 1u;
if (remainder >= denominator_mantissa) {
remainder = remainder - denominator_mantissa;
quotient_mantissa = quotient_mantissa | (1u << (22u - digit));
}
}
// Round the 24-bit significand to nearest, ties to even. remainder and its
// doubled value are below 2^25, so no u32 overflow is possible.
let twice_remainder = remainder << 1u;
if (twice_remainder > denominator_mantissa
|| (twice_remainder == denominator_mantissa && (quotient_mantissa & 1u) != 0u)) {
quotient_mantissa = quotient_mantissa + 1u;
}
if (quotient_mantissa == 0x01000000u) {
quotient_mantissa = quotient_mantissa >> 1u;
quotient_exponent = quotient_exponent + 1;
}
let biased_exponent = quotient_exponent + 127;
if (biased_exponent <= 0 || biased_exponent >= 255) {
// DynamicQuantizeLinear's GPU-supported ranges are normal and finite.
// Preserve the implementation-defined subnormal/overflow behavior outside
// that contract; the integer quantizer never takes this branch near a
// half-integer boundary.
return numerator / denominator;
}
let result_bits = sign_bits
| (u32(biased_exponent) << 23u)
| (quotient_mantissa & 0x007fffffu);
return bitcast<f32>(result_bits);
}
fn dynamic_quantize_division_may_cross_half(estimate: f32) -> bool {
let lower = floor(estimate);
let fraction = estimate - lower;
let magnitude = abs(estimate);
let magnitude_bits = bitcast<u32>(magnitude);
let adjacent = bitcast<f32>(magnitude_bits + 1u);
let ulp = adjacent - magnitude;
// Division is allowed 2.5 ULP error. Eight ULP also covers the factor-of-two
// ULP change when an estimate straddles the 0.5 exponent boundary.
return abs(fraction - 0.5) <= ulp * 8.0;
}
fn round_dynamic_half_to_even(value: f32, scale: f32) -> i32 {
let estimate = value / scale;
var scaled = estimate;
// `select` evaluates both value operands in WGSL; use control flow so the
// 23-bit software divide remains a rare boundary fallback, not O(23) work
// for every quantized element.
if (dynamic_quantize_division_may_cross_half(estimate)) {
scaled = dynamic_quantize_exact_div_normal(value, scale);
}
let lower = floor(scaled);
let fraction = scaled - lower;
if (fraction < 0.5) {
return i32(lower);
}
if (fraction > 0.5) {
return i32(lower + 1.0);
}
let upper = lower + 1.0;
let half_lower = floor(lower * 0.5);
let lower_is_even = (lower - half_lower * 2.0) == 0.0;
return i32(select(upper, lower, lower_is_even));
}
@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
if (gid.x != 0u) { return; }
{% if source.fromPartials %}
var min_value = partial_min[0];
var max_value = partial_max[0];
for (var i = 1u; i < params.numPartials; i = i + 1u) {
min_value = min(min_value, partial_min[i]);
max_value = max(max_value, partial_max[i]);
}
{% else %}
var min_value = 0.0;
var max_value = 0.0;
for (var i = 0u; i < params.count; i = i + 1u) {
min_value = min(min_value, x[i]);
max_value = max(max_value, x[i]);
}
{% endif %}
var scale = dynamic_quantize_exact_div_normal(max_value - min_value, 255.0);
if (scale == 0.0) {
scale = 1.0;
}
let zp_i32 = clamp(round_dynamic_half_to_even(-min_value, scale), 0, 255);
y_scale[0] = scale;
y_zero_point[0] = u32(zp_i32);
{% if not source.fromPartials %}
for (var i = 0u; i < params.count; i = i + 1u) {
let q = clamp(round_dynamic_half_to_even(x[i], scale) + zp_i32, 0, 255);
y[i] = u32(q);
}
{% endif %}
}