diff options
Diffstat (limited to 'testing/web-platform/tests/webnn/resources/utils_validation.js')
-rw-r--r-- | testing/web-platform/tests/webnn/resources/utils_validation.js | 359 |
1 files changed, 359 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webnn/resources/utils_validation.js b/testing/web-platform/tests/webnn/resources/utils_validation.js new file mode 100644 index 0000000000..7f1d4a4a94 --- /dev/null +++ b/testing/web-platform/tests/webnn/resources/utils_validation.js @@ -0,0 +1,359 @@ +'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`); + } +} |