Skip to content

Commit

Permalink
webnn: Allow an MLGraphBuilder to build at most one MLGraph
Browse files Browse the repository at this point in the history
See webmachinelearning/webnn#717

Bug: 354724062
Change-Id: I8ac2bf94b1f5a0db93a042babdc2556eab35034a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5684454
Reviewed-by: Reilly Grant <reillyg@chromium.org>
Commit-Queue: Austin Sullivan <asully@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1332702}
  • Loading branch information
a-sully authored and chromium-wpt-export-bot committed Jul 25, 2024
1 parent 2f1700a commit b7777fa
Show file tree
Hide file tree
Showing 42 changed files with 269 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ if (navigator.ml) {
const contextOptions = kContextOptionsForVariant[variant];

let context;
let builder;

promise_setup(async () => {
let supported = false;
Expand All @@ -26,10 +25,10 @@ if (navigator.ml) {
}
assert_implements(
supported, `Unable to create context for ${variant} variant`);
builder = new MLGraphBuilder(context);
});

promise_test(async t => {
const builder = new MLGraphBuilder(context);
const a = builder.input('a', {dataType: 'float32', dimensions: [2]});
const b = builder.relu(a);
const graph = await builder.build({b});
Expand All @@ -45,6 +44,7 @@ if (navigator.ml) {
}, 'Test compute() working for input ArrayBufferView created from bigger ArrayBuffer');

promise_test(async t => {
const builder = new MLGraphBuilder(context);
const a = builder.input('a', {dataType: 'float32', dimensions: [2]});
const b = builder.relu(a);
const graph = await builder.build({b});
Expand Down
3 changes: 1 addition & 2 deletions webnn/resources/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -1063,7 +1063,6 @@ const testWebNNOperation = (operationName, buildFunc) => {
}

let context;
let builder;
operationNameArray.forEach((subOperationName) => {
const tests = loadTests(subOperationName);
promise_setup(async () => {
Expand All @@ -1075,10 +1074,10 @@ const testWebNNOperation = (operationName, buildFunc) => {
}
assert_implements(
supported, `Unable to create context for ${variant} variant`);
builder = new MLGraphBuilder(context);
});
for (const subTest of tests) {
promise_test(async () => {
const builder = new MLGraphBuilder(context);
await run(subOperationName, context, builder, subTest, buildFunc);
}, `${subTest.name}`);
}
Expand Down
14 changes: 12 additions & 2 deletions webnn/resources/utils_validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ function generateOutOfRangeValuesArray(type) {
let inputIndex = 0;
let inputAIndex = 0;
let inputBIndex = 0;
let context, builder;
let context;

test(() => assert_not_equals(navigator.ml, undefined, "ml property is defined on navigator"));

Expand All @@ -204,14 +204,14 @@ promise_setup(async () => {
return;
}
context = await navigator.ml.createContext();
builder = new MLGraphBuilder(context);
}, {explicit_timeout: true});

function validateTwoInputsBroadcastable(operationName) {
if (navigator.ml === undefined) {
return;
}
promise_test(async t => {
const builder = new MLGraphBuilder(context);
for (let dataType of allWebNNOperandDataTypes) {
if (!context.opSupportLimits().input.dataTypes.includes(dataType)) {
assert_throws_js(
Expand Down Expand Up @@ -251,6 +251,7 @@ function validateTwoInputsOfSameDataType(operationName) {
}
for (let subOperationName of operationNameArray) {
promise_test(async t => {
const builder = new MLGraphBuilder(context);
for (let dataType of allWebNNOperandDataTypes) {
if (!context.opSupportLimits().input.dataTypes.includes(dataType)) {
assert_throws_js(
Expand Down Expand Up @@ -304,6 +305,7 @@ function validateOptionsAxes(operationName) {
for (let subOperationName of operationNameArray) {
// TypeError is expected if any of options.axes elements is not an unsigned long interger
promise_test(async t => {
const builder = new MLGraphBuilder(context);
for (let dataType of allWebNNOperandDataTypes) {
if (!context.opSupportLimits().input.dataTypes.includes(dataType)) {
assert_throws_js(
Expand Down Expand Up @@ -338,6 +340,7 @@ function validateOptionsAxes(operationName) {
// TypeError is expected if any of options.axes elements is greater or equal
// to the size of input
promise_test(async t => {
const builder = new MLGraphBuilder(context);
for (let dataType of allWebNNOperandDataTypes) {
if (!context.opSupportLimits().input.dataTypes.includes(dataType)) {
assert_throws_js(
Expand All @@ -364,6 +367,7 @@ function validateOptionsAxes(operationName) {

// TypeError is expected if two or more values are same in the axes sequence
promise_test(async t => {
const builder = new MLGraphBuilder(context);
for (let dataType of allWebNNOperandDataTypes) {
if (!context.opSupportLimits().input.dataTypes.includes(dataType)) {
assert_throws_js(
Expand Down Expand Up @@ -403,6 +407,7 @@ function validateOptionsAxes(operationName) {
function validateUnaryOperation(
operationName, supportedDataTypes, alsoBuildActivation = false) {
promise_test(async t => {
const builder = new MLGraphBuilder(context);
for (let dataType of supportedDataTypes) {
if (!context.opSupportLimits().input.dataTypes.includes(dataType)) {
assert_throws_js(
Expand All @@ -423,6 +428,7 @@ function validateUnaryOperation(
const unsupportedDataTypes =
new Set(allWebNNOperandDataTypes).difference(new Set(supportedDataTypes));
promise_test(async t => {
const builder = new MLGraphBuilder(context);
for (let dataType of unsupportedDataTypes) {
if (!context.opSupportLimits().input.dataTypes.includes(dataType)) {
assert_throws_js(
Expand All @@ -440,6 +446,7 @@ function validateUnaryOperation(

if (alsoBuildActivation) {
promise_test(async t => {
const builder = new MLGraphBuilder(context);
builder[operationName]();
}, `[${operationName}] Test building an activation`);
}
Expand All @@ -454,6 +461,7 @@ function validateUnaryOperation(
function validateSingleInputOperation(
operationName, alsoBuildActivation = false) {
promise_test(async t => {
const builder = new MLGraphBuilder(context);
const supportedDataTypes =
context.opSupportLimits()[operationName].input.dataTypes;
for (let dataType of supportedDataTypes) {
Expand All @@ -467,6 +475,7 @@ function validateSingleInputOperation(
}, `[${operationName}] Test building the operator with supported data type.`);

promise_test(async t => {
const builder = new MLGraphBuilder(context);
const unsupportedDataTypes =
new Set(allWebNNOperandDataTypes)
.difference(new Set(
Expand All @@ -488,6 +497,7 @@ function validateSingleInputOperation(

if (alsoBuildActivation) {
promise_test(async t => {
const builder = new MLGraphBuilder(context);
builder[operationName]();
}, `[${operationName}] Test building an activation.`);
}
Expand Down
1 change: 1 addition & 0 deletions webnn/validation_tests/argMinMax.https.any.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const tests = [
function runTests(operatorName, tests) {
tests.forEach(test => {
promise_test(async t => {
const builder = new MLGraphBuilder(context);
const input = builder.input(
'input',
{ dataType: test.input.dataType, dimensions: test.input.dimensions });
Expand Down
1 change: 1 addition & 0 deletions webnn/validation_tests/batchNormalization.https.any.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ const tests = [

tests.forEach(
test => promise_test(async t => {
const builder = new MLGraphBuilder(context);
const input = builder.input(
'input',
{dataType: test.input.dataType, dimensions: test.input.dimensions});
Expand Down
85 changes: 85 additions & 0 deletions webnn/validation_tests/build-more-than-once.https.any.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// META: title=ensure MLMLGraphBuilder may build at most one MLGraph
// META: global=window,dedicatedworker
// META: script=../resources/utils_validation.js

const kExampleInputDescriptor = {
dataType: 'float32',
dimensions: [2]
};

promise_test(async t => {
const builder = new MLGraphBuilder(context);
const a = builder.input('a', kExampleInputDescriptor);
const b = builder.input('b', kExampleInputDescriptor);
const c = builder.add(a, b);
const graph = await builder.build({c});

await promise_rejects_dom(t, 'InvalidStateError', builder.build({c}));
}, 'Throw if attempting to build a second graph with an MLGraphBuilder');

promise_test(async t => {
const builder = new MLGraphBuilder(context);
const a = builder.input('a', kExampleInputDescriptor);
const b = builder.input('b', kExampleInputDescriptor);
const c = builder.add(a, b);
const graph = await builder.build({c});

assert_throws_dom('InvalidStateError', () => builder.sub(a, b));
}, 'Throw if an operand-yielding method is called on a built MLGraphBuilder');

promise_test(async t => {
const builder = new MLGraphBuilder(context);
const a = builder.input('a', kExampleInputDescriptor);
const b = builder.input('b', kExampleInputDescriptor);
const c = builder.add(a, b);
const graph = await builder.build({c});

assert_throws_dom(
'InvalidStateError', () => builder.input('d', kExampleInputDescriptor));
}, 'Throw if adding an input operand to a built MLGraphBuilder');

promise_test(async t => {
const builder = new MLGraphBuilder(context);
const a = builder.input('a', kExampleInputDescriptor);
const b = builder.input('b', kExampleInputDescriptor);
const c = builder.add(a, b);
const graph = await builder.build({c});

const buffer = new ArrayBuffer(8);
const bufferView = new Float32Array(buffer);

assert_throws_dom(
'InvalidStateError',
() => builder.constant(kExampleInputDescriptor, bufferView));
}, 'Throw if adding a constant operand to a built MLGraphBuilder');

promise_test(async t => {
const builder = new MLGraphBuilder(context);
const a = builder.input('a', kExampleInputDescriptor);
const b = builder.input('b', kExampleInputDescriptor);
const c = builder.add(a, b);

// Call build() with invalid parameters.
await promise_rejects_js(t, TypeError, builder.build({a}));

// Passing valid parameters successfully creates the graph...
const graph = await builder.build({c});

// ...exactly once!
await promise_rejects_dom(t, 'InvalidStateError', builder.build({c}));
}, 'An MLGraphBuilder remains unbuilt if build() is called with invalid paramaters');

promise_test(async t => {
const builder1 = new MLGraphBuilder(context);
const builder2 = new MLGraphBuilder(context);

const a1 = builder1.input('a', kExampleInputDescriptor);
const b1 = builder1.input('b', kExampleInputDescriptor);
const c1 = builder1.add(a1, b1);
const graph1 = await builder1.build({c1});

const a2 = builder2.input('a', kExampleInputDescriptor);
const b2 = builder2.input('b', kExampleInputDescriptor);
const c2 = builder2.add(a2, b2);
const graph2 = await builder2.build({c2});
}, 'Build two graphs with separate MLGraphBuilders');
4 changes: 4 additions & 0 deletions webnn/validation_tests/clamp.https.any.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ validateInputFromAnotherBuilder('clamp');
validateUnaryOperation('clamp', allWebNNOperandDataTypes);

promise_test(async t => {
const builder = new MLGraphBuilder(context);
const options = {minValue: 1.0, maxValue: 3.0};
if (!context.opSupportLimits().input.dataTypes.includes('uint32')) {
assert_throws_js(
Expand All @@ -25,6 +26,7 @@ promise_test(async t => {
}, '[clamp] Test building an operator with options');

promise_test(async t => {
const builder = new MLGraphBuilder(context);
const options = {minValue: 0, maxValue: 0};
if (!context.opSupportLimits().input.dataTypes.includes('int32')) {
assert_throws_js(
Expand All @@ -41,6 +43,7 @@ promise_test(async t => {
}, '[clamp] Test building an operator with options.minValue == options.maxValue');

promise_test(async t => {
const builder = new MLGraphBuilder(context);
const options = {minValue: 3.0, maxValue: 1.0};
if (!context.opSupportLimits().input.dataTypes.includes('uint8')) {
assert_throws_js(
Expand All @@ -57,6 +60,7 @@ promise_test(async t => {
// To be removed once infinite `minValue` is allowed. Tracked in
// https://github.com/webmachinelearning/webnn/pull/647.
promise_test(async t => {
const builder = new MLGraphBuilder(context);
const options = {minValue: -Infinity};
const input = builder.input('input', {dataType: 'float16', dimensions: []});
assert_throws_js(TypeError, () => builder.clamp(input, options));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,52 @@
// These tests are used to reproduce the Chromium issue:
// https://issues.chromium.org/issues/332002364
promise_test(async t => {
const a = builder.input('a', {dataType: 'float32', dimensions: [2]});
const b = builder.input('b', {dataType: 'float32', dimensions: [2]});
const c = builder.add(a, b);
const graph = await builder.build({c});
const arraybuffer = new ArrayBuffer(100);
const aBuffer = new Float32Array(arraybuffer, 0, 2);
const bBuffer = new Float32Array(arraybuffer, 8, 2);
const cBuffer = new Float32Array(2);
const promise = context.compute(graph, {'a': aBuffer, 'b': bBuffer}, {'c': cBuffer});
promise_rejects_js(t, TypeError, promise);
}, 'Throw if two input ArrayBufferViews sharing the same ArrayBuffer');
const builder = new MLGraphBuilder(context);
const a = builder.input('a', {dataType: 'float32', dimensions: [2]});
const b = builder.input('b', {dataType: 'float32', dimensions: [2]});
const c = builder.add(a, b);
const graph = await builder.build({c});
const arraybuffer = new ArrayBuffer(100);
const aBuffer = new Float32Array(arraybuffer, 0, 2);
const bBuffer = new Float32Array(arraybuffer, 8, 2);
const cBuffer = new Float32Array(2);
const promise =
context.compute(graph, {'a': aBuffer, 'b': bBuffer}, {'c': cBuffer});
promise_rejects_js(t, TypeError, promise);
}, 'Throw if two input ArrayBufferViews sharing the same ArrayBuffer');

promise_test(async t => {
const a = builder.input('a', {dataType: 'float32', dimensions: [2]});
const [b, c] = builder.split(a, 2);
const graph = await builder.build({b, c});
const aBuffer = new Float32Array(2);
const arraybuffer = new ArrayBuffer(100);
const bBuffer = new Float32Array(arraybuffer, 0, 1);
const cBuffer = new Float32Array(arraybuffer, 4, 1);
const promise = context.compute(graph, {'a': aBuffer}, {'b': bBuffer, 'c': cBuffer});
promise_rejects_js(t, TypeError, promise);
const builder = new MLGraphBuilder(context);
const a = builder.input('a', {dataType: 'float32', dimensions: [2]});
const [b, c] = builder.split(a, 2);
const graph = await builder.build({b, c});
const aBuffer = new Float32Array(2);
const arraybuffer = new ArrayBuffer(100);
const bBuffer = new Float32Array(arraybuffer, 0, 1);
const cBuffer = new Float32Array(arraybuffer, 4, 1);
const promise =
context.compute(graph, {'a': aBuffer}, {'b': bBuffer, 'c': cBuffer});
promise_rejects_js(t, TypeError, promise);
}, 'Throw if two output ArrayBufferViews sharing the same ArrayBuffer');

promise_test(async t => {
const a = builder.input('a', {dataType: 'float32', dimensions: [2]});
const b = builder.relu(a);
const graph = await builder.build({b});
const arraybuffer = new ArrayBuffer(100);
const aBuffer = new Float32Array(arraybuffer, 0, 2);
const bBuffer = new Float32Array(arraybuffer, 8, 2);
const promise = context.compute(graph, {'a': aBuffer}, {'b': bBuffer});
promise_rejects_js(t, TypeError, promise);
const builder = new MLGraphBuilder(context);
const a = builder.input('a', {dataType: 'float32', dimensions: [2]});
const b = builder.relu(a);
const graph = await builder.build({b});
const arraybuffer = new ArrayBuffer(100);
const aBuffer = new Float32Array(arraybuffer, 0, 2);
const bBuffer = new Float32Array(arraybuffer, 8, 2);
const promise = context.compute(graph, {'a': aBuffer}, {'b': bBuffer});
promise_rejects_js(t, TypeError, promise);
}, 'Throw if input and output ArrayBufferViews sharing the same ArrayBuffer');

promise_test(async t => {
const a = builder.input('a', {dataType: 'float32', dimensions: [2]});
const b = builder.relu(a);
const graph = await builder.build({b});
const buffer = new Float32Array(2);
const promise = context.compute(graph, {'a': buffer}, {'b': buffer});
promise_rejects_js(t, TypeError, promise);
const builder = new MLGraphBuilder(context);
const a = builder.input('a', {dataType: 'float32', dimensions: [2]});
const b = builder.relu(a);
const graph = await builder.build({b});
const buffer = new Float32Array(2);
const promise = context.compute(graph, {'a': buffer}, {'b': buffer});
promise_rejects_js(t, TypeError, promise);
}, 'Throw if input and output are the same ArrayBufferView');
8 changes: 4 additions & 4 deletions webnn/validation_tests/concat.https.any.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ const tests = [

];

tests.forEach(test =>
promise_test(async t => {
tests.forEach(
test => promise_test(async t => {
const builder = new MLGraphBuilder(context);
let inputs = [];
if (test.inputs) {
for (let i = 0; i < test.inputs.length; ++i) {
Expand All @@ -88,8 +89,7 @@ tests.forEach(test =>
} else {
assert_throws_js(TypeError, () => builder.concat(inputs, test.axis));
}
}, test.name)
);
}, test.name));

multi_builder_test(async (t, builder, otherBuilder) => {
const operandDescriptor = {dataType: 'float32', dimensions: [2, 2]};
Expand Down
Loading

0 comments on commit b7777fa

Please sign in to comment.