Skip to content

Commit

Permalink
wgsl: Add AF Division execution tests
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 committed Oct 19, 2023
1 parent 6696b0e commit f58eab5
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 24 deletions.
39 changes: 24 additions & 15 deletions src/unittests/floating_point.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<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];
// 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
Expand Down Expand Up @@ -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<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 +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 },
Expand All @@ -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}`
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
);
});
51 changes: 42 additions & 9 deletions src/webgpu/util/floating_point.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import {
map2DArray,
oneULPF16,
oneULPF32,
oneULPF64,
quantizeToF32,
quantizeToF16,
unflatten2DArray,
Expand Down Expand Up @@ -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 {
Expand All @@ -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),
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -5364,7 +5397,7 @@ class F16Traits extends FPTraits {
}

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

0 comments on commit f58eab5

Please sign in to comment.