From c2d5b057361cecba6bed8df79d819e56dc17dab3 Mon Sep 17 00:00:00 2001 From: Ryan Harrison Date: Mon, 23 Oct 2023 12:45:56 -0400 Subject: [PATCH] wgsl: Add AF Division execution tests (#3074) Adds in forwarding of ULP and division interval calls to f32 for abstract Issue #1626 --- src/unittests/floating_point.spec.ts | 48 ++++-- src/webgpu/listing_meta.json | 4 + .../expression/binary/af_division.spec.ts | 154 ++++++++++++++++++ src/webgpu/util/floating_point.ts | 47 ++++-- src/webgpu/util/math.ts | 16 ++ 5 files changed, 241 insertions(+), 28 deletions(-) create mode 100644 src/webgpu/shader/execution/expression/binary/af_division.spec.ts diff --git a/src/unittests/floating_point.spec.ts b/src/unittests/floating_point.spec.ts index 62f77ecbb07e..2bb30dbab7ee 100644 --- a/src/unittests/floating_point.spec.ts +++ b/src/unittests/floating_point.spec.ts @@ -20,6 +20,16 @@ import { UnitTest } from './unit_test.js'; export const g = makeTestGroup(UnitTest); +/** + * For ULP purposes, abstract float behaves like f32, so need to swizzle it in + * for expectations. + */ +const kFPTraitForULP = { + abstract: 'f32', + f32: 'f32', + f16: 'f16', +} as const; + /** Bounds indicating an expectation of unbounded error */ const kUnboundedBounds: IntervalBounds = [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY]; @@ -2079,15 +2089,16 @@ 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]; + const trait = kFPTraitForULP[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 +4375,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 +4398,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 +4414,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..efe4119fc4ba 100644 --- a/src/webgpu/util/floating_point.ts +++ b/src/webgpu/util/floating_point.ts @@ -40,10 +40,10 @@ import { map2DArray, oneULPF16, oneULPF32, - oneULPF64, quantizeToF32, quantizeToF16, unflatten2DArray, + every2DArray, } from './math.js'; /** Indicate the kind of WGSL floating point numbers being operated on */ @@ -631,12 +631,19 @@ export abstract class FPTraits { public abstract constants(): FPConstants; // Utilities - Implemented + /** @returns an interval containing the point or the original interval */ public toInterval(n: number | IntervalBounds | FPInterval): FPInterval { if (n instanceof FPInterval) { if (n.kind === this.kind) { return n; } + + // Preserve if the original interval was unbounded or bounded + if (!n.isFinite()) { + return this.constants().unboundedInterval; + } + return new FPInterval(this.kind, ...n.bounds()); } @@ -700,7 +707,7 @@ export abstract class FPTraits { /** @returns an FPVector representation of an array of values if possible */ public toVector(v: (number | IntervalBounds | FPInterval)[]): FPVector { - if (this.isVector(v)) { + if (this.isVector(v) && v.every(e => e.kind === this.kind)) { return v; } @@ -764,7 +771,12 @@ export abstract class FPTraits { /** @returns an FPMatrix representation of an array of an array of values if possible */ public toMatrix(m: Array2D | FPVector[]): FPMatrix { - if (this.isMatrix(m)) { + if ( + this.isMatrix(m) && + every2DArray(m, (e: FPInterval) => { + return e.kind === this.kind; + }) + ) { return m; } @@ -3228,11 +3240,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 +3270,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 +4737,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( @@ -4930,14 +4944,18 @@ 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 = (n: number, numULP: number): FPInterval => { + return this.toInterval(kF32Traits.ulpInterval(n, numULP)); + }; // Framework - API - Overrides public readonly absInterval = this.absIntervalImpl.bind(this); @@ -4974,10 +4992,13 @@ class FPAbstractTraits extends FPTraits { 'determinantInterval' ); public readonly distanceInterval = this.unimplementedDistance.bind(this); - public readonly divisionInterval = this.unimplementedScalarPairToInterval.bind( - this, - 'divisionInterval' - ); + public readonly divisionInterval = ( + x: number | FPInterval, + y: number | FPInterval + ): FPInterval => { + return this.toInterval(kF32Traits.divisionInterval(x, y)); + }; + public readonly dotInterval = this.unimplementedVectorPairToInterval.bind(this, 'dotInterval'); public readonly expInterval = this.unimplementedScalarToInterval.bind(this, 'expInterval'); public readonly exp2Interval = this.unimplementedScalarToInterval.bind(this, 'exp2Interval'); @@ -5364,7 +5385,7 @@ class F16Traits extends FPTraits { } export const FP = { - f32: new F32Traits(), + f32: kF32Traits, f16: new F16Traits(), abstract: new FPAbstractTraits(), }; diff --git a/src/webgpu/util/math.ts b/src/webgpu/util/math.ts index cc7b5e44a99f..1fb1d1a7ff69 100644 --- a/src/webgpu/util/math.ts +++ b/src/webgpu/util/math.ts @@ -2241,3 +2241,19 @@ export function map2DArray(m: T[][], op: (input: T) => S): S[][] { } return result; } + +/** + * Performs a .every over a matrix and return the result + * + * @param m input matrix of type T + * @param op operation that performs a test on an element + * @returns a boolean indicating if the test passed for every element + */ +export function every2DArray(m: T[][], op: (input: T) => boolean): boolean { + const r = m[0].length; + assert( + m.every(c => c.length === r), + `Unexpectedly received jagged array to map` + ); + return m.every(col => col.every(el => op(el))); +}