diff --git a/src/unittests/floating_point.spec.ts b/src/unittests/floating_point.spec.ts index 62f77ecbb07e..23d298106a04 100644 --- a/src/unittests/floating_point.spec.ts +++ b/src/unittests/floating_point.spec.ts @@ -2079,15 +2079,17 @@ const kULPErrorValue = { g.test('ulpInterval') .params(u => u - .combine('trait', ['f32', 'f16'] as const) + .combine('trait', ['abstract', 'f32', 'f16'] as const) .beginSubcases() .expandWithParams(p => { - const constants = FP[p.trait].constants(); - const ULPValue = kULPErrorValue[p.trait]; - const plusOneULP = kPlusOneULPFunctions[p.trait]; - const plusNULP = kPlusNULPFunctions[p.trait]; - const minusOneULP = kMinusOneULPFunctions[p.trait]; - const minusNULP = kMinusNULPFunctions[p.trait]; + // For ULP purposes, abstract float behaves like f32, so swizzling it in. + const trait = p.trait === 'abstract' ? 'f32' : p.trait; + const constants = FP[trait].constants(); + const ULPValue = kULPErrorValue[trait]; + const plusOneULP = kPlusOneULPFunctions[trait]; + const plusNULP = kPlusNULPFunctions[trait]; + const minusOneULP = kMinusOneULPFunctions[trait]; + const minusNULP = kMinusNULPFunctions[trait]; // prettier-ignore return [ // Edge Cases @@ -4364,11 +4366,14 @@ const kDivisionInterval64BitsNormalCases = { g.test('divisionInterval') .params(u => u - .combine('trait', ['f32', 'f16'] as const) + .combine('trait', ['abstract', 'f32', 'f16'] as const) .beginSubcases() .expandWithParams(p => { - const trait = FP[p.trait]; - const constants = trait.constants(); + // This is a ULP based interval, so abstract should behave like f32, so + // swizzling the trait as needed. + const trait = p.trait === 'abstract' ? 'f32' : p.trait; + const fp = FP[trait]; + const constants = fp.constants(); // prettier-ignore return [ // Representable normals @@ -4384,7 +4389,7 @@ g.test('divisionInterval') { input: [-4, -2], expected: 2 }, // 64-bit normals that can not be exactly represented - ...kDivisionInterval64BitsNormalCases[p.trait], + ...kDivisionInterval64BitsNormalCases[trait], // Denominator out of range { input: [1, constants.positive.infinity], expected: kUnboundedBounds }, @@ -4400,17 +4405,21 @@ g.test('divisionInterval') }) ) .fn(t => { - const trait = FP[t.params.trait]; + // This is a ULP based interval, so abstract should behave like f32, so + // swizzling the trait as needed for calculating the expected result. + const trait = t.params.trait === 'abstract' ? 'f32' : t.params.trait; + const fp = FP[trait]; const error = (n: number): number => { - return 2.5 * trait.oneULP(n); + return 2.5 * fp.oneULP(n); }; const [x, y] = t.params.input; t.params.expected = applyError(t.params.expected, error); - const expected = trait.toInterval(t.params.expected); - const got = trait.divisionInterval(x, y); + // Do not swizzle here, so the correct implementation under test is called. + const expected = FP[t.params.trait].toInterval(t.params.expected); + const got = FP[t.params.trait].divisionInterval(x, y); t.expect( objectEquals(expected, got), `${t.params.trait}.divisionInterval(${x}, ${y}) returned ${got}. Expected ${expected}` diff --git a/src/webgpu/listing_meta.json b/src/webgpu/listing_meta.json index 3e6856ef9693..486f22cf9b47 100644 --- a/src/webgpu/listing_meta.json +++ b/src/webgpu/listing_meta.json @@ -863,6 +863,10 @@ "webgpu:shader,execution,expression,binary,af_comparison:less_equals:*": { "subcaseMS": 19.651 }, "webgpu:shader,execution,expression,binary,af_comparison:less_than:*": { "subcaseMS": 19.975 }, "webgpu:shader,execution,expression,binary,af_comparison:not_equals:*": { "subcaseMS": 19.651 }, + "webgpu:shader,execution,expression,binary,af_division:scalar:*": { "subcaseMS": 563.200 }, + "webgpu:shader,execution,expression,binary,af_division:scalar_vector:*": { "subcaseMS": 567.101 }, + "webgpu:shader,execution,expression,binary,af_division:vector:*": { "subcaseMS": 237.134 }, + "webgpu:shader,execution,expression,binary,af_division:vector_scalar:*": { "subcaseMS": 580.000 }, "webgpu:shader,execution,expression,binary,af_matrix_addition:matrix:*": { "subcaseMS": 11169.534 }, "webgpu:shader,execution,expression,binary,af_matrix_subtraction:matrix:*": { "subcaseMS": 14060.956 }, "webgpu:shader,execution,expression,binary,af_multiplication:scalar:*": { "subcaseMS": 777.901 }, diff --git a/src/webgpu/shader/execution/expression/binary/af_division.spec.ts b/src/webgpu/shader/execution/expression/binary/af_division.spec.ts new file mode 100644 index 000000000000..e473acb20d8f --- /dev/null +++ b/src/webgpu/shader/execution/expression/binary/af_division.spec.ts @@ -0,0 +1,154 @@ +export const description = ` +Execution Tests for non-matrix AbstractFloat division expression +`; + +import { makeTestGroup } from '../../../../../common/framework/test_group.js'; +import { GPUTest } from '../../../../gpu_test.js'; +import { TypeAbstractFloat, TypeVec } from '../../../../util/conversion.js'; +import { FP, FPVector } from '../../../../util/floating_point.js'; +import { sparseF64Range, sparseVectorF64Range } from '../../../../util/math.js'; +import { makeCaseCache } from '../case_cache.js'; +import { onlyConstInputSource, run } from '../expression.js'; + +import { abstractBinary } from './binary.js'; + +const divisionVectorScalarInterval = (v: number[], s: number): FPVector => { + return FP.abstract.toVector(v.map(e => FP.abstract.divisionInterval(e, s))); +}; + +const divisionScalarVectorInterval = (s: number, v: number[]): FPVector => { + return FP.abstract.toVector(v.map(e => FP.abstract.divisionInterval(s, e))); +}; + +export const g = makeTestGroup(GPUTest); + +const scalar_cases = { + ['scalar']: () => { + return FP.abstract.generateScalarPairToIntervalCases( + sparseF64Range(), + sparseF64Range(), + 'finite', + FP.abstract.divisionInterval + ); + }, +}; + +const vector_scalar_cases = ([2, 3, 4] as const) + .map(dim => ({ + [`vec${dim}_scalar`]: () => { + return FP.abstract.generateVectorScalarToVectorCases( + sparseVectorF64Range(dim), + sparseF64Range(), + 'finite', + divisionVectorScalarInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +const scalar_vector_cases = ([2, 3, 4] as const) + .map(dim => ({ + [`scalar_vec${dim}`]: () => { + return FP.abstract.generateScalarVectorToVectorCases( + sparseF64Range(), + sparseVectorF64Range(dim), + 'finite', + divisionScalarVectorInterval + ); + }, + })) + .reduce((a, b) => ({ ...a, ...b }), {}); + +export const d = makeCaseCache('binary/af_division', { + ...scalar_cases, + ...vector_scalar_cases, + ...scalar_vector_cases, +}); + +g.test('scalar') + .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') + .desc( + ` +Expression: x / y, where x and y are scalars +Accuracy: 2.5 ULP for |y| in the range [2^-126, 2^126] +` + ) + .params(u => u.combine('inputSource', onlyConstInputSource)) + .fn(async t => { + const cases = await d.get('scalar'); + await run( + t, + abstractBinary('/'), + [TypeAbstractFloat, TypeAbstractFloat], + TypeAbstractFloat, + t.params, + cases + ); + }); + +g.test('vector') + .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') + .desc( + ` +Expression: x / y, where x and y are vectors +Accuracy: 2.5 ULP for |y| in the range [2^-126, 2^126] +` + ) + .params(u => + u.combine('inputSource', onlyConstInputSource).combine('vectorize', [2, 3, 4] as const) + ) + .fn(async t => { + const cases = await d.get('scalar'); // Using vectorize to generate vector cases based on scalar cases + await run( + t, + abstractBinary('/'), + [TypeAbstractFloat, TypeAbstractFloat], + TypeAbstractFloat, + t.params, + cases + ); + }); + +g.test('vector_scalar') + .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') + .desc( + ` +Expression: x / y, where x is a vector and y is a scalar +Accuracy: Correctly rounded +` + ) + .params(u => u.combine('inputSource', onlyConstInputSource).combine('dim', [2, 3, 4] as const)) + .fn(async t => { + const dim = t.params.dim; + const cases = await d.get(`vec${dim}_scalar`); + await run( + t, + abstractBinary('/'), + [TypeVec(dim, TypeAbstractFloat), TypeAbstractFloat], + TypeVec(dim, TypeAbstractFloat), + t.params, + cases + ); + }); + +g.test('scalar_vector') + .specURL('https://www.w3.org/TR/WGSL/#floating-point-evaluation') + .desc( + ` +Expression: x / y, where x is a scalar and y is a vector +Accuracy: Correctly rounded +` + ) + .params(u => u.combine('inputSource', onlyConstInputSource).combine('dim', [2, 3, 4] as const)) + .fn(async t => { + const dim = t.params.dim; + const cases = await d.get(`scalar_vec${dim}`); + await run( + t, + abstractBinary('/'), + [TypeAbstractFloat, TypeVec(dim, TypeAbstractFloat)], + TypeVec(dim, TypeAbstractFloat), + t.params, + cases + ); + }); diff --git a/src/webgpu/util/floating_point.ts b/src/webgpu/util/floating_point.ts index ec3808589056..06f496c26457 100644 --- a/src/webgpu/util/floating_point.ts +++ b/src/webgpu/util/floating_point.ts @@ -40,7 +40,6 @@ import { map2DArray, oneULPF16, oneULPF32, - oneULPF64, quantizeToF32, quantizeToF16, unflatten2DArray, @@ -3228,11 +3227,10 @@ export abstract class FPTraits { // This op is implemented differently for f32 and f16. private DivisionIntervalOpBuilder(): ScalarPairToIntervalOp { - assert(this.kind === 'f32' || this.kind === 'f16'); const constants = this.constants(); const domain_x = [this.toInterval([constants.negative.min, constants.positive.max])]; const domain_y = - this.kind === 'f32' + this.kind === 'f32' || this.kind === 'abstract' ? [this.toInterval([-(2 ** 126), -(2 ** -126)]), this.toInterval([2 ** -126, 2 ** 126])] : [this.toInterval([-(2 ** 14), -(2 ** -14)]), this.toInterval([2 ** -14, 2 ** 14])]; return { @@ -3259,7 +3257,6 @@ export abstract class FPTraits { } protected divisionIntervalImpl(x: number | FPInterval, y: number | FPInterval): FPInterval { - assert(this.kind === 'f32' || this.kind === 'f16'); return this.runScalarPairToIntervalOp( this.toInterval(x), this.toInterval(y), @@ -4727,6 +4724,10 @@ class F32Traits extends FPTraits { public readonly quantizeToF16Interval = this.quantizeToF16IntervalImpl.bind(this); } +// Need to separately allocate f32 traits, so they can be referenced by +// FPAbstractTraits for forwarding. +const kF32Traits = new F32Traits(); + // Pre-defined values that get used multiple times in _constants' initializers. Cannot use FPTraits members, since this // executes before they are defined. const kAbstractUnboundedInterval = new FPInterval( @@ -4920,6 +4921,36 @@ class FPAbstractTraits extends FPTraits { return FPAbstractTraits._constants; } + // Utilities - Proxies + // Wrappers for forwarding ULP and absolute error interval calls to f32. + // AbstractFloat accuracies are technically unbounded for ULP and absolute + // error interval, but testing that implementations are at least as good as + // f32. + + /** Forwarder for ULPInterval */ + protected forwardUlpInterval(n: number, numULP: number): FPInterval { + const result = FP['f32'].ulpInterval(n, numULP); + if (!result.isFinite()) { + return this.constants().unboundedInterval; + } + + return this.toInterval(result.bounds()); + } + + /** Forwarder for scalar pair to interval generator */ + protected forwardScalarPairToInterval( + func: (x: number | FPInterval, y: number | FPInterval) => FPInterval, + x: number | FPInterval, + y: number | FPInterval + ): FPInterval { + const result = func(x, y); + if (!result.isFinite()) { + return this.constants().unboundedInterval; + } + + return this.toInterval(result.bounds()); + } + // Utilities - Overrides // number is represented as a f64 internally, so all number values are already // quantized to f64 @@ -4930,14 +4961,16 @@ class FPAbstractTraits extends FPTraits { public readonly isFinite = Number.isFinite; public readonly isSubnormal = isSubnormalNumberF64; public readonly flushSubnormal = flushSubnormalNumberF64; - public readonly oneULP = oneULPF64; + public readonly oneULP = (_target: number, _mode: FlushMode = 'flush'): number => { + unreachable(`'FPAbstractTraits.oneULP should never be called`); + }; public readonly scalarBuilder = abstractFloat; // Framework - Fundamental Error Intervals - Overrides public readonly absoluteErrorInterval = this.unboundedAbsoluteErrorInterval.bind(this); public readonly correctlyRoundedInterval = this.correctlyRoundedIntervalImpl.bind(this); public readonly correctlyRoundedMatrix = this.correctlyRoundedMatrixImpl.bind(this); - public readonly ulpInterval = this.unboundedUlpInterval.bind(this); + public readonly ulpInterval = this.forwardUlpInterval.bind(this); // Framework - API - Overrides public readonly absInterval = this.absIntervalImpl.bind(this); @@ -4974,9 +5007,9 @@ class FPAbstractTraits extends FPTraits { 'determinantInterval' ); public readonly distanceInterval = this.unimplementedDistance.bind(this); - public readonly divisionInterval = this.unimplementedScalarPairToInterval.bind( + public readonly divisionInterval = this.forwardScalarPairToInterval.bind( this, - 'divisionInterval' + kF32Traits.divisionInterval ); public readonly dotInterval = this.unimplementedVectorPairToInterval.bind(this, 'dotInterval'); public readonly expInterval = this.unimplementedScalarToInterval.bind(this, 'expInterval'); @@ -5364,7 +5397,7 @@ class F16Traits extends FPTraits { } export const FP = { - f32: new F32Traits(), + f32: kF32Traits, f16: new F16Traits(), abstract: new FPAbstractTraits(), };