blob: 7f1d4a4a9457e6a782063a17c10ea601f765adee [file] [log] [blame]
'use strict';
// https://webmachinelearning.github.io/webnn/#enumdef-mloperanddatatype
const allWebNNOperandDataTypes = [
'float32',
'float16',
'int32',
'uint32',
'int64',
'uint64',
'int8',
'uint8'
];
const unsignedLongType = 'unsigned long';
const dimensions0D = [];
const dimensions1D = [2];
const dimensions2D = [2, 3];
const dimensions3D = [2, 3, 4];
const dimensions4D = [2, 3, 4, 5];
const dimensions5D = [2, 3, 4, 5, 6];
const adjustOffsetsArray = [
// Decrease 1
-1,
// Increase 1
1
];
// TODO
// Add more 5+ dimensions
const allWebNNDimensionsArray = [
dimensions0D,
dimensions1D,
dimensions2D,
dimensions3D,
dimensions4D,
dimensions5D
];
const notUnsignedLongAxisArray = [
// String
'abc',
// BigInt
BigInt(100),
// Object
{
value: 1
},
// Array Object
[0, 1],
// Date Object
new Date("2024-01-01"),
];
function getRank(inputDimensions) {
return inputDimensions.length;
}
function getAxisArray(inputDimensions) {
return Array.from({length: inputDimensions.length}, (_, i) => i);
}
function getAxesArrayContainSameValues(inputDimensions) {
// TODO
// Currently this function returns an array containing each element which all have the same value.
// For example axes: [0, 1, 2] for 3D input tensor
// this function returns
// [
// // two values are same
// [0, 0],
// [1, 1],
// [2, 2],
// // three values are same
// [0, 0, 0],
// [1, 1, 1]
// [2, 2, 2]
// ]
// while it should return
// [
// // two values are same
// [0, 0],
// [1, 1],
// [2, 2],
// [0, 0, 1],
// [0, 0, 2],
// [0, 1, 0],
// [0, 2, 0],
// [1, 0, 0],
// [2, 0, 0],
// [1, 1, 0],
// [1, 1, 2],
// [1, 0, 1],
// [1, 2, 1],
// [0, 1, 1],
// [2, 1, 1],
// [2, 2, 0],
// [2, 2, 1],
// [2, 0, 2],
// [2, 1, 2],
// [0, 2, 2],
// [1, 2, 2],
// // three (all) values are same
// [0, 0, 0],
// [1, 1, 1]
// [2, 2, 2]
// ]
const axesArrayContainSameValues = [];
const length = inputDimensions.length;
if (length >= 2) {
const validAxesArrayFull = getAxisArray(inputDimensions);
for (let index = 0; index < length; index++) {
axesArrayContainSameValues.push(new Array(2).fill(validAxesArrayFull[index]));
if (length > 2) {
axesArrayContainSameValues.push(new Array(3).fill(validAxesArrayFull[index]));
}
}
}
return axesArrayContainSameValues;
}
function generateUnbroadcastableDimensionsArray(dimensions) {
// Currently this function returns an array of some unbroadcastable dimensions.
// for example given dimensions [2, 3, 4]
// this function returns
// [
// [3, 3, 4],
// [2, 2, 4],
// [2, 4, 4],
// [2, 3, 3],
// [2, 3, 5],
// [3],
// [5],
// [1, 3],
// [1, 5],
// [1, 1, 3],
// [1, 1, 5],
// [1, 1, 1, 3],
// [1, 1, 1, 5],
// ]
if (dimensions.every(v => v === 1)) {
throw new Error(`[${dimensions}] always can be broadcasted`);
}
const resultDimensions = [];
const length = dimensions.length;
if (!dimensions.slice(0, length - 1).every(v => v === 1)) {
for (let i = 0; i < length; i++) {
if (dimensions[i] !== 1) {
for (let offset of [-1, 1]) {
const dimensionsB = dimensions.slice();
dimensionsB[i] += offset;
if (dimensionsB[i] !== 1) {
resultDimensions.push(dimensionsB);
}
}
}
}
}
const lastDimensionSize = dimensions[length - 1];
if (lastDimensionSize !== 1) {
for (let j = 0; j <= length; j++) {
if (lastDimensionSize > 2) {
resultDimensions.push(Array(j).fill(1).concat([lastDimensionSize - 1]));
}
resultDimensions.push(Array(j).fill(1).concat([lastDimensionSize + 1]));
}
}
return resultDimensions;
}
function generateOutOfRangeValuesArray(type) {
let range, outsideValueArray;
switch (type) {
case 'unsigned long':
// https://webidl.spec.whatwg.org/#idl-unsigned-long
// The unsigned long type is an unsigned integer type that has values in the range [0, 4294967295].
range = [0, 4294967295];
break;
default:
throw new Error(`Unsupport ${type}`);
}
outsideValueArray = [range[0] - 1, range[1] + 1];
return outsideValueArray;
}
let inputIndex = 0;
let inputAIndex = 0;
let inputBIndex = 0;
let context, builder;
test(() => assert_not_equals(navigator.ml, undefined, "ml property is defined on navigator"));
promise_setup(async () => {
if (navigator.ml === undefined) {
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 => {
for (let dataType of allWebNNOperandDataTypes) {
for (let dimensions of allWebNNDimensionsArray) {
if (dimensions.length > 0) {
const inputA = builder.input(`inputA${++inputAIndex}`, {dataType, dimensions});
const unbroadcastableDimensionsArray = generateUnbroadcastableDimensionsArray(dimensions);
for (let unbroadcastableDimensions of unbroadcastableDimensionsArray) {
const inputB = builder.input(`inputB${++inputBIndex}`, {dataType, dimensions: unbroadcastableDimensions});
assert_throws_dom('DataError', () => builder[operationName](inputA, inputB));
assert_throws_dom('DataError', () => builder[operationName](inputB, inputA));
}
}
}
}
}, `[${operationName}] DataError is expected if two inputs aren't broadcastable`);
}
function validateTwoInputsOfSameDataType(operationName) {
if (navigator.ml === undefined) {
return;
}
let operationNameArray;
if (typeof operationName === 'string') {
operationNameArray = [operationName];
} else if (Array.isArray(operationName)) {
operationNameArray = operationName;
} else {
throw new Error(`${operationName} should be an operation name string or an operation name string array`);
}
for (let subOperationName of operationNameArray) {
promise_test(async t => {
for (let dataType of allWebNNOperandDataTypes) {
for (let dimensions of allWebNNDimensionsArray) {
const inputA = builder.input(`inputA${++inputAIndex}`, {dataType, dimensions});
for (let dataTypeB of allWebNNOperandDataTypes) {
if (dataType !== dataTypeB) {
const inputB = builder.input(`inputB${++inputBIndex}`, {dataType: dataTypeB, dimensions});
assert_throws_dom('DataError', () => builder[subOperationName](inputA, inputB));
}
}
}
}
}, `[${subOperationName}] DataError is expected if two inputs aren't of same data type`);
}
}
/**
* Validate options.axes by given operation and input rank for
* argMin/Max / layerNormalization / Reduction operations / resample2d operations
* @param {(String[]|String)} operationName - An operation name array or an operation name
* @param {Number} [inputRank]
*/
function validateOptionsAxes(operationName, inputRank) {
if (navigator.ml === undefined) {
return;
}
let operationNameArray;
if (typeof operationName === 'string') {
operationNameArray = [operationName];
} else if (Array.isArray(operationName)) {
operationNameArray = operationName;
} else {
throw new Error(`${operationName} should be an operation name string or an operation name string array`);
}
const invalidAxisArray = generateOutOfRangeValuesArray(unsignedLongType);
for (let subOperationName of operationNameArray) {
// TypeError is expected if any of options.axes elements is not an unsigned long interger
promise_test(async t => {
if (inputRank === undefined) {
// argMin/Max / layerNormalization / Reduction operations
for (let dataType of allWebNNOperandDataTypes) {
for (let dimensions of allWebNNDimensionsArray) {
const rank = getRank(dimensions);
if (rank >= 1) {
const input = builder.input(`input${++inputIndex}`, {dataType, dimensions});
for (let invalidAxis of invalidAxisArray) {
assert_throws_js(TypeError, () => builder[subOperationName](input, {axes: invalidAxis}));
}
for (let axis of notUnsignedLongAxisArray) {
assert_false(typeof axis === 'number' && Number.isInteger(axis), `[${subOperationName}] any of options.axes elements should be of 'unsigned long'`);
assert_throws_js(TypeError, () => builder[subOperationName](input, {axes: [axis]}));
}
}
}
}
} else {
// resample2d
for (let dataType of allWebNNOperandDataTypes) {
const input = builder.input(`input${++inputIndex}`, {dataType, dimensions: allWebNNDimensionsArray[inputRank]});
for (let invalidAxis of invalidAxisArray) {
assert_throws_js(TypeError, () => builder[subOperationName](input, {axes: invalidAxis}));
}
for (let axis of notUnsignedLongAxisArray) {
assert_false(typeof axis === 'number' && Number.isInteger(axis), `[${subOperationName}] any of options.axes elements should be of 'unsigned long'`);
assert_throws_js(TypeError, () => builder[subOperationName](input, {axes: [axis]}));
}
}
}
}, `[${subOperationName}] TypeError is expected if any of options.axes elements is not an unsigned long interger`);
// DataError is expected if any of options.axes elements is greater or equal to the size of input
promise_test(async t => {
if (inputRank === undefined) {
// argMin/Max / layerNormalization / Reduction operations
for (let dataType of allWebNNOperandDataTypes) {
for (let dimensions of allWebNNDimensionsArray) {
const rank = getRank(dimensions);
if (rank >= 1) {
const input = builder.input(`input${++inputIndex}`, {dataType, dimensions});
assert_throws_dom('DataError', () => builder[subOperationName](input, {axes: [rank]}));
assert_throws_dom('DataError', () => builder[subOperationName](input, {axes: [rank + 1]}));
}
}
}
} else {
// resample2d
for (let dataType of allWebNNOperandDataTypes) {
const input = builder.input(`input${++inputIndex}`, {dataType, dimensions: allWebNNDimensionsArray[inputRank]});
assert_throws_dom('DataError', () => builder[subOperationName](input, {axes: [inputRank]}));
assert_throws_dom('DataError', () => builder[subOperationName](input, {axes: [inputRank + 1]}));
}
}
}, `[${subOperationName}] DataError is expected if any of options.axes elements is greater or equal to the size of input`);
// DataError is expected if two or more values are same in the axes sequence
promise_test(async t => {
if (inputRank === undefined) {
// argMin/Max / layerNormalization / Reduction operations
for (let dataType of allWebNNOperandDataTypes) {
for (let dimensions of allWebNNDimensionsArray) {
const rank = getRank(dimensions);
if (rank >= 2) {
const input = builder.input(`input${++inputIndex}`, {dataType, dimensions});
const axesArrayContainSameValues = getAxesArrayContainSameValues(dimensions);
for (let axes of axesArrayContainSameValues) {
assert_throws_dom('DataError', () => builder[subOperationName](input, {axes}));
}
}
}
}
} else {
// resample2d
for (let dataType of allWebNNOperandDataTypes) {
const dimensions = allWebNNDimensionsArray[inputRank];
const input = builder.input(`input${++inputIndex}`, {dataType, dimensions});
const axesArrayContainSameValues = getAxesArrayContainSameValues(dimensions);
for (let axes of axesArrayContainSameValues) {
assert_throws_dom('DataError', () => builder[subOperationName](input, {axes}));
}
}
}
}, `[${subOperationName}] DataError is expected if two or more values are same in the axes sequence`);
}
}