Skip to content

Commit

Permalink
wgsl: Add AF Division execution tests (#3074)
Browse files Browse the repository at this point in the history
Adds in forwarding of ULP and division interval calls to f32 for
abstract

Issue #1626
  • Loading branch information
zoddicus authored Oct 23, 2023
1 parent e5e7ee1 commit c2d5b05
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 28 deletions.
48 changes: 33 additions & 15 deletions src/unittests/floating_point.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down Expand Up @@ -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<ULPCase>(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
Expand Down Expand Up @@ -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<ScalarPairToIntervalCase>(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
Expand All @@ -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 },
Expand All @@ -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}`
Expand Down
4 changes: 4 additions & 0 deletions src/webgpu/listing_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
154 changes: 154 additions & 0 deletions src/webgpu/shader/execution/expression/binary/af_division.spec.ts
Original file line number Diff line number Diff line change
@@ -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
);
});
47 changes: 34 additions & 13 deletions src/webgpu/util/floating_point.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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());
}

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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<number | IntervalBounds | FPInterval> | FPVector[]): FPMatrix {
if (this.isMatrix(m)) {
if (
this.isMatrix(m) &&
every2DArray(m, (e: FPInterval) => {
return e.kind === this.kind;
})
) {
return m;
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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),
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -5364,7 +5385,7 @@ class F16Traits extends FPTraits {
}

export const FP = {
f32: new F32Traits(),
f32: kF32Traits,
f16: new F16Traits(),
abstract: new FPAbstractTraits(),
};
Expand Down
Loading

0 comments on commit c2d5b05

Please sign in to comment.