Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wgsl: Add AF Division execution tests #3074

Merged
merged 4 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about introducing some kind of FP mapping dictionary for ULP propose, instead of using ?: in multiple places?

Something in the file scope like

// For ULP purposes, abstract float behaves like f32, so swizzling it in.
const kFPTraitForULP = {
  abstract: 'f32',
  f32: 'f32',
  f16: 'f16',
} as const;

and then use e.g.

        const constants = FP[kFPTraitForULP[p.trait]].constants();
        const ULPValue = kULPErrorValue[kFPTraitForULP[p.trait]];
        const plusOneULP = kPlusOneULPFunctions[kFPTraitForULP[p.trait]];
        const plusNULP = kPlusNULPFunctions[kFPTraitForULP[p.trait]];
        const minusOneULP = kMinusOneULPFunctions[kFPTraitForULP[p.trait]];
        const minusNULP = kMinusNULPFunctions[kFPTraitForULP[p.trait]];

or

        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];

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

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(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some data point in this range may be out of domain_y of division interval operation, and result in unbounded expectation. Good thing is that this won't cause false error.

'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'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By using the same domain_y for af and f32, the division interval op will return unbounded interval for af in the wide range of normal af number that out of f32 range.

But since the spec didn't mention what to expect in that range for af, this would be OK.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Thank you for reading this very closely.

The thinking is that we don't want WGSL to specify anything more strict than ECMAScript, and ECMAScript does not specify much in terms of ULP. On the other hand, we also don't want AbstractFloat to be worse than f32.

? [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
Loading