'use strict'; // https://webmachinelearning.github.io/webnn/#enumdef-mloperanddatatype const TypedArrayDict = { // workaround use Uint16 for Float16 float16: Uint16Array, float32: Float32Array, int32: Int32Array, uint32: Uint32Array, int8: Int8Array, uint8: Uint8Array, int64: BigInt64Array, }; const kContextOptionsForVariant = { cpu: { deviceType: 'cpu', }, gpu: { deviceType: 'gpu', } }; // The maximum index to validate for the output's expected value. const kMaximumIndexToValidate = 1000; const getTypedArrayData = (type, size, data) => { let outData; if (type === 'float16') { if (typeof (data) === 'number' && size > 1) { return new TypedArrayDict[type](size).fill(toHalf(data)); } // workaround to convert Float16 to Uint16 outData = new TypedArrayDict[type](data.length); for (let i = 0; i < data.length; i++) { outData[i] = toHalf(data[i]); } } else if (type === 'int64') { if (typeof (data) === 'number' && size > 1) { return new TypedArrayDict[type](size).fill(BigInt(data)); } outData = new TypedArrayDict[type](data.length); for (let i = 0; i < data.length; i++) { outData[i] = BigInt(data[i]); } } else { if (typeof (data) === 'number' && size > 1) { return new TypedArrayDict[type](size).fill(data); } outData = new TypedArrayDict[type](data); } return outData; }; const sizeOfShape = (array) => { return array.reduce((accumulator, currentValue) => accumulator * currentValue, 1); }; /** * Get tests resources from test data JSON file of specified operation name. * @param {String} operationName - An operation name * @returns {Object} Tests resources */ const loadTests = (operationName) => { const loadJSON = (file) => { let xmlhttp = new XMLHttpRequest(); xmlhttp.open("GET", file, false); xmlhttp.overrideMimeType("application/json"); xmlhttp.send(); if (xmlhttp.status == 200 && xmlhttp.readyState == 4) { return xmlhttp.responseText; } else { throw new Error(`Failed to load ${file}`); } }; const capitalLetterMatches = operationName.match(/[A-Z]/g); if (capitalLetterMatches !== null) { // for example: the test data JSON file for leakyRelu is leaky_relu.json and for reduceLogSum is reduce_log_sum.json capitalLetterMatches.forEach( capitalLetter => operationName = operationName.replace(capitalLetter, `_${capitalLetter.toLowerCase()}`) ) } const json = loadJSON(`/webnn/resources/test_data/${operationName}.json`); const resources = JSON.parse(json.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => g ? "" : m)); return resources.tests; }; /** * Get expected resource from given resources with output name. * @param {Array} resources - An array of given resources * @param {String} outputName - An output name * @returns {Object} An object of expected resource */ const getNamedResource = (resources, outputName) => { let ret; for (let resource of resources) { if (resource.name === outputName) { ret = resource; break; } } if (ret === undefined) { throw new Error(`Failed to get expected resource by ${outputName}`); } return ret; }; /** * Get ULP tolerance of conv2d/convTranspose2d operation. * @param {Object} resources - Resources used for building a graph * @param {String} operationName - An operation name * @returns {Number} A tolerance number */ const getConv2dPrecisionTolerance = (resources, operationName) => { // number of reduced input elements multiplied by filter and summed (a sliding dot product like pooling) const inputNameArray = Object.keys(resources.inputs); const inputShape = resources.inputs[inputNameArray[0]].shape; const filterShape = resources.inputs[inputNameArray[1]].shape; const options = resources.options; let groups = 1; let inputChannels = inputShape[1]; // default nchw inputLayout // default oihw filterLayout for conv2d or default iohw filterLayout for convTranspose2d let filterWidth = filterShape[3]; let filterHeight = filterShape[2]; if (options) { if (options.groups) { groups = options.groups; } if (options.inputLayout) { if (!['nchw', 'nhwc'].includes(options.inputLayout)) { throw new Error(`Unsupported inputLayout ${options.inputLayout}`); } inputChannels = options.inputLayout === 'nchw' ? inputChannels : inputShape[3]; } if (options.filterLayout) { let filterLayouts = ['oihw', 'hwio', 'ohwi', 'ihwo']; // default for conv2d if (operationName === 'convTranspose2d') { filterLayouts = ['iohw', 'hwoi', 'ohwi']; } if (!filterLayouts.includes(options.filterLayout)) { throw new Error(`Unsupported filterLayout ${options.filterLayout}`); } switch (options.filterLayout) { case 'oihw': case 'iohw': // Just use the existing filterWidth and filterHeight above. break; case 'hwio': case 'hwoi': filterWidth = filterShape[1]; filterHeight = filterShape[0]; break; case 'ohwi': case 'ihwo': filterWidth = filterShape[2]; filterHeight = filterShape[1]; break; default: break; } } } const tolerance = filterWidth * filterHeight * (inputChannels / groups) * 2; return tolerance; }; /** * Get ULP tolerance of gemm operation. * @param {Object} resources - Resources used for building a graph * @param {String} operationName - An operation name * @returns {Number} A tolerance number */ const getGemmPrecisionTolerance = (resources, operationName) => { // GEMM : alpha * (A x B) + beta * C // An upper bound for the worst serial ordering is bounded by // the number of lossy operations, where matrix multiplication // is a dot product (mul and add times the number of elements) // plus bias operations. const shapeA = resources.inputs[Object.keys(resources.inputs)[0]].shape; const options = {...resources.options}; const width = options.aTranspose ? shapeA[0] : shapeA[1]; let tolerance = width * 2; // default options.alpha is 1.0 if (options.alpha !== undefined && options.alpha !== 1.0) { tolerance++; } if (options.c && options.beta !== 0.0) { // default options.beta is 1.0 if (options.beta !== undefined && options.beta !== 1.0) { tolerance++; } tolerance++; } return tolerance; }; /** * Get ULP tolerance of matmul operation. * @param {Object} resources - Resources used for building a graph * @param {String} operationName - An operation name * @returns {Number} A tolerance number */ const getMatmulPrecisionTolerance = (resources, operationName) => { // Matmul : Compute the matrix product of two input tensors. // If a is 1-D, WebNN converts it to a 2-D tensor by prepending a 1 to its dimensions, [n] -> [1, n]. // So we can just always check the last dimension here. const shapeA = resources.inputs[Object.keys(resources.inputs)[0]].shape; const tolerance = shapeA[shapeA.length - 1] * 2; return tolerance; }; /** * Get ULP tolerance of averagePool2d or l2Pool2d operation. * @param {Object} resources - Resources used for building a graph * @param {String} operationName - An operation name * @returns {Number} A tolerance number */ const getAveragePool2dPrecisionTolerance = (resources, operationName) => { const inputShape = resources.inputs[Object.keys(resources.inputs)[0]].shape; let height; let width; const options = {...resources.options}; if (options.windowDimensions) { height = options.windowDimensions[0]; width = options.windowDimensions[1]; } else { // If not present, the window dimensions are assumed to be the height and width dimensions of the input shape if (options.layout && options.layout === 'nhwc') { height = inputShape[1]; width = inputShape[2]; } else { // nhwc layout of input height = inputShape[2]; width = inputShape[3]; } } const tolerance = height * width + 2; return tolerance; }; /** * Get ULP tolerance of softmax operation. * @param {Object} resources - Resources used for building a graph * @param {String} operationName - An operation name * @returns {Number} A tolerance number */ const getSoftmaxPrecisionTolerance = (resources, operationName) => { // Compute the softmax values of the 2-D input tensor along axis 1. const inputShape = resources.inputs[Object.keys(resources.inputs)[0]].shape; const tolerance = inputShape[1] * 3 + 3; return tolerance; }; /** * Get ULP tolerance of reduction operations. * @param {Object} resources - Resources used for building a graph * @param {String} operationName - An operation name * @returns {Number} A tolerance number */ const getReductionPrecisionTolerance = (resources, operationName) => { const inputShape = resources.inputs[Object.keys(resources.inputs)[0]].shape; const rank = inputShape.length; const options = {...resources.options}; let sizes; if (options && options.axes) { sizes = options.axes.map( (axis) => axis < 0 ? inputShape[axis + rank] : inputShape[axis] ); } else { sizes = inputShape; } const reducedElementCount = sizes.reduce( (accumulator, currentValue) => accumulator * currentValue ); let tolerance; switch (operationName) { case 'reduceL1': case 'reduceProduct': case 'reduceSum': tolerance = reducedElementCount; break; case 'reduceL2': tolerance = reducedElementCount * 2 + 1; break; case 'reduceMean': tolerance = reducedElementCount + 2; break; case 'reduceLogSum': tolerance = reducedElementCount + 18; break; case 'reduceLogSumExp': tolerance = reducedElementCount * 2 + 18; break; case 'reduceSumSquare': tolerance = reducedElementCount * 2; break; default: break; } return tolerance; }; /** * Get ULP tolerance of resample2d operations. * @param {Object} resources - Resources used for building a graph * @param {String} operationName - An operation name * @returns {Number} A tolerance number */ const getResample2dPrecisionTolerance = (resources, operationName) => { const options = {...resources.options}; let tolerance; if (options.mode && options.mode === 'linear') { // interpolation mode is linear const precisionType = resources.expected.type; if (precisionType === 'float32') { tolerance = 84; } else if (precisionType === 'float16') { tolerance = 10; } else { tolerance = 1; } } else { // interpolation mode is nearest-neighbor tolerance = 0; } return tolerance; }; // Refer to precision metrics on https://github.com/webmachinelearning/webnn/issues/265#issuecomment-1256242643 const PrecisionMetrics = { argMax: {ULP: {int64: 0}}, argMin: {ULP: {int64: 0}}, batchNormalization: {ULP: {float32: 6, float16: 6}}, cast: {ULP: {float32: 1, float16: 1, int32: 0, uint32: 0, int64: 0, int8: 0, uint8: 0}}, clamp: {ULP: {float32: 0, float16: 0}}, concat: {ULP: {float32: 0, float16: 0}}, constant: {ULP: {float32: 2, float16: 2, int32: 0, uint32: 0, int64: 0, int8: 0, uint8: 0}}, conv2d: {ULP: {float32: getConv2dPrecisionTolerance, float16: getConv2dPrecisionTolerance}}, convTranspose2d: {ULP: {float32: getConv2dPrecisionTolerance, float16: getConv2dPrecisionTolerance}}, // Begin Element-wise binary operations add: {ULP: {float32: 1, float16: 1}}, sub: {ULP: {float32: 1, float16: 1}}, mul: {ULP: {float32: 1, float16: 1}}, div: {ULP: {float32: 2, float16: 2}}, max: {ULP: {float32: 0, float16: 0}}, min: {ULP: {float32: 0, float16: 0}}, pow: {ULP: {float32: 32, float16: 2}}, // End Element-wise binary operations // Begin Element-wise logical operations equal: {ULP: {uint8: 0}}, greater: {ULP: {uint8: 0}}, greaterOrEqual: {ULP: {uint8: 0}}, lesser: {ULP: {uint8: 0}}, lesserOrEqual: {ULP: {uint8: 0}}, logicalNot: {ULP: {uint8: 0}}, // End Element-wise logical operations // Begin Element-wise unary operations abs: {ULP: {float32: 0, float16: 0}}, ceil: {ULP: {float32: 0, float16: 0}}, cos: {ATOL: {float32: 1/1024, float16: 1/512}}, erf: {ATOL: {float32: 1/1024, float16: 1/512}}, exp: {ULP: {float32: 32, float16: 1}}, floor: {ULP: {float32: 0, float16: 0}}, identity: {ULP: {float32: 0, float16: 0}}, log: {ATOL: {float32: 1/1024, float16: 1/1024}}, neg: {ULP: {float32: 0, float16: 0}}, reciprocal: {ULP: {float32: 2, float16: 2}}, sin: {ATOL: {float32: 1/1024, float16: 1/512}}, sqrt: {ULP: {float32: 1, float16: 1}}, tan: {ATOL: {float32: 1/1024, float16: 1/512}}, // End Element-wise unary operations elu: {ULP: {float32: 18, float16: 18}}, expand: {ULP: {float32: 0, float16: 0}}, gather: {ULP: {float32: 0, float16: 0}}, gemm: {ULP: {float32: getGemmPrecisionTolerance, float16: getGemmPrecisionTolerance}}, instanceNormalization: {ULP: {float32: 840, float16: 8400}}, hardSigmoid: {ULP: {float32: 2, float16: 2}}, hardSwish: {ULP: {float32: 4, float16: 4}}, layerNormalization: {ATOL: {float32: 1/1024, float16: 1/512}}, leakyRelu: {ULP: {float32: 1, float16: 1}}, linear: {ULP: {float32: 2, float16: 2}}, matmul: {ULP: {float32: getMatmulPrecisionTolerance, float16: getMatmulPrecisionTolerance}}, pad: {ULP: {float32: 0, float16: 0}}, // Begin Pooling operations averagePool2d: {ULP: {float32: getAveragePool2dPrecisionTolerance, float16: getAveragePool2dPrecisionTolerance}}, l2Pool2d: {ULP: {float32: getAveragePool2dPrecisionTolerance, float16: getAveragePool2dPrecisionTolerance}}, maxPool2d: {ULP: {float32: 0, float16: 0}}, // End Pooling operations prelu: {ULP: {float32: 1, float16: 1}}, // Begin Reduction operations reduceL1: {ULP: {float32: getReductionPrecisionTolerance, float16: getReductionPrecisionTolerance}}, reduceL2: {ULP: {float32: getReductionPrecisionTolerance, float16: getReductionPrecisionTolerance}}, reduceLogSum: {ULP: {float32: getReductionPrecisionTolerance, float16: getReductionPrecisionTolerance}}, reduceLogSumExp: {ULP: {float32: getReductionPrecisionTolerance, float16: getReductionPrecisionTolerance}}, reduceMax: {ULP: {float32: 0, float16: 0}}, reduceMean: {ULP: {float32: getReductionPrecisionTolerance, float16: getReductionPrecisionTolerance}}, reduceMin: {ULP: {float32: 0, float16: 0}}, reduceProduct: {ULP: {float32: getReductionPrecisionTolerance, float16: getReductionPrecisionTolerance}}, reduceSum: {ULP: {float32: getReductionPrecisionTolerance, float16: getReductionPrecisionTolerance}}, reduceSumSquare: {ULP: {float32: getReductionPrecisionTolerance, float16: getReductionPrecisionTolerance}}, // End Reduction operations relu: {ULP: {float32: 0, float16: 0}}, resample2d: {ULP: {float32: getResample2dPrecisionTolerance, float16: getResample2dPrecisionTolerance}}, reshape: {ULP: {float32: 0, float16: 0}}, sigmoid: {ULP: {float32: 32+2, float16: 3}}, // float32 (leaving a few ULP for roundoff) slice: {ULP: {float32: 0, float16: 0}}, softmax: {ULP: {float32: getSoftmaxPrecisionTolerance, float16: getSoftmaxPrecisionTolerance}}, softplus: {ULP: {float32: 18, float16: 18}}, softsign: {ULP: {float32: 3, float16: 3}}, split: {ULP: {float32: 0, float16: 0}}, tanh: {ATOL: {float32: 1/1024, float16: 1/512}}, transpose: {ULP: {float32: 0, float16: 0}}, triangular: {ULP: {float32: 0, float16: 0}}, where: {ULP: {float32: 0, float16: 0}}, }; /** * Get precison tolerance value. * @param {String} operationName - An operation name * @param {String} metricType - Value: 'ULP', 'ATOL' * @param {Object} resources - Resources used for building a graph * @returns {Number} A tolerance number */ const getPrecisonTolerance = (operationName, metricType, resources) => { // the outputs by split or gru is a sequence const precisionType = Array.isArray(resources.expected) ? resources.expected[0].type : resources.expected.type; let tolerance = PrecisionMetrics[operationName][metricType][precisionType]; // If the tolerance is dynamic, then evaluate the function to get the value. if (tolerance instanceof Function) { tolerance = tolerance(resources, operationName); } return tolerance; }; /** * Get bitwise of the given value. * @param {Number} value * @param {String} dataType - A data type string, like "float32", "float16", * more types, please see: * https://webmachinelearning.github.io/webnn/#enumdef-mloperanddatatype * @return {Number} A 64-bit signed integer. */ const getBitwise = (value, dataType) => { const buffer = new ArrayBuffer(8); const int64Array = new BigInt64Array(buffer); int64Array[0] = value < 0 ? ~BigInt(0) : BigInt(0); let typedArray; if (dataType === "float32") { typedArray = new Float32Array(buffer); } else { throw new AssertionError(`Data type ${dataType} is not supported`); } typedArray[0] = value; return int64Array[0]; }; /** * Assert that each array property in ``actual`` is a number being close enough to the corresponding * property in ``expected`` by the acceptable ULP distance ``nulp`` with given ``dataType`` data type. * * @param {Array} actual - Array of test values. * @param {Array} expected - Array of values expected to be close to the values in ``actual``. * @param {Number} nulp - A BigInt value indicates acceptable ULP distance. * @param {String} dataType - A data type string, value: "float32", * more types, please see: * https://webmachinelearning.github.io/webnn/#enumdef-mloperanddatatype * @param {String} description - Description of the condition being tested. */ const assert_array_approx_equals_ulp = (actual, expected, nulp, dataType, description) => { /* * Test if two primitive arrays are equal within acceptable ULP distance */ assert_true(actual.length === expected.length, `assert_array_approx_equals_ulp: ${description} lengths differ, expected ${expected.length} but got ${actual.length}`); let actualBitwise, expectedBitwise, distance; for (let i = 0; i < actual.length; i++) { if (actual[i] === expected[i]) { continue; } else { // measure the ULP distance if (dataType === 'float32') { actualBitwise = getBitwise(actual[i], dataType); expectedBitwise = getBitwise(expected[i], dataType); } else if (dataType === 'float16') { actualBitwise = actual[i]; // convert expected data of Float16 to Uint16 expectedBitwise = toHalf(expected[i]); } else if (dataType === 'int64') { actualBitwise = actual[i]; expectedBitwise = BigInt(expected[i]); } distance = actualBitwise - expectedBitwise; distance = distance >= 0 ? distance : -distance; assert_true(distance <= nulp, `assert_array_approx_equals_ulp: ${description} actual ${actual[i]} should be close enough to expected ${expected[i]} by the acceptable ${nulp} ULP distance, but they have ${distance} ULP distance`); } } }; /** * Assert actual results with expected results. * @param {String} operationName - An operation name * @param {(Number[]|Number)} actual * @param {(Number[]|Number)} expected * @param {Number} tolerance * @param {String} operandType - An operand type string, value: "float32", * more types, please see: * https://webmachinelearning.github.io/webnn/#enumdef-mloperanddatatype * @param {String} metricType - Value: 'ULP', 'ATOL' */ const doAssert = (operationName, actual, expected, tolerance, operandType, metricType) => { const description = `test ${operationName} ${operandType}`; if (typeof expected === 'number') { // for checking a scalar output by matmul 1D x 1D expected = [expected]; actual = [actual]; } if (metricType === 'ULP') { assert_array_approx_equals_ulp(actual, expected, tolerance, operandType, description); } else if (metricType === 'ATOL') { assert_array_approx_equals(actual, expected, tolerance, description); } }; /** * Check computed results with expected data. * @param {String} operationName - An operation name * @param {Object.} namedOutputOperands * @param {Object.} outputs - The resources of required outputs * @param {Object} resources - Resources used for building a graph */ const checkResults = (operationName, namedOutputOperands, outputs, resources) => { const metricType = Object.keys(PrecisionMetrics[operationName])[0]; const expected = resources.expected; let tolerance; let operandType; let outputData; let expectedData; if (Array.isArray(expected)) { // the outputs of split() or gru() is a sequence for (let operandName in namedOutputOperands) { const suboutputResource = getNamedResource(expected, operandName); assert_array_equals(namedOutputOperands[operandName].shape(), suboutputResource.shape ?? []); outputData = outputs[operandName]; tolerance = getPrecisonTolerance(operationName, metricType, resources); doAssert(operationName, outputData, suboutputResource.data, tolerance, suboutputResource.type, metricType) } } else { assert_array_equals(namedOutputOperands[expected.name].shape(), expected.shape ?? []); outputData = outputs[expected.name]; expectedData = expected.data; operandType = expected.type; tolerance = getPrecisonTolerance(operationName, metricType, resources); doAssert(operationName, outputData, expectedData, tolerance, operandType, metricType) } }; /** * Create a constant operand * @param {MLGraphBuilder} builder - A ML graph builder * @param {Object} resources - Resources used for constant operand * @returns {MLOperand} A constant operand */ const createConstantOperand = (builder, resources) => { const bufferView = (typeof (resources.data) === 'number' && sizeOfShape(resources.shape) > 1) ? new TypedArrayDict[resources.type](sizeOfShape(resources.shape)) .fill(resources.data) : new TypedArrayDict[resources.type](resources.data); return builder.constant({dataType: resources.type, type: resources.type, dimensions: resources.shape}, bufferView); }; /** * Create single input operands for a graph. * @param {MLGraphBuilder} builder - A ML graph builder * @param {Object} resources - Resources used for building a graph * @param {String} [inputOperandName] - An inputOperand name * @returns {MLOperand} An input operand */ const createSingleInputOperand = (builder, resources, inputOperandName) => { inputOperandName = inputOperandName ? inputOperandName : Object.keys(resources.inputs)[0]; const inputResources = resources.inputs[inputOperandName]; let operand; if (resources.inputs[inputOperandName].hasOwnProperty('constant') && resources.inputs[inputOperandName]['constant']) { operand = createConstantOperand(builder, resources.inputs[inputOperandName]); } else { operand = builder.input(inputOperandName, {dataType: inputResources.type, type: inputResources.type, dimensions: inputResources.shape}); } return operand; }; /** * Create multi input operands for a graph. * @param {MLGraphBuilder} builder - A ML graph builder * @param {Object} resources - Resources used for building a graph * @returns {MLOperand[]} Input operands array */ const createMultiInputOperands = (builder, resources) => { let inputOperands = []; const inputOperandNameArray = Object.keys(resources.inputs); inputOperandNameArray.forEach(inputOperandName => { const operand = createSingleInputOperand(builder, resources, inputOperandName); inputOperands.push(operand); }); return inputOperands; }; /** * Build an operation which has a single input. * @param {String} operationName - An operation name * @param {MLGraphBuilder} builder - A ML graph builder * @param {Object} resources - Resources used for building a graph * @returns {MLNamedOperands} */ const buildOperationWithSingleInput = (operationName, builder, resources) => { const namedOutputOperand = {}; const inputOperand = createSingleInputOperand(builder, resources); const outputOperand = resources.options ? builder[operationName](inputOperand, resources.options) : builder[operationName](inputOperand); namedOutputOperand[resources.expected.name] = outputOperand; return namedOutputOperand; }; /** * Build an operation which has two inputs. * @param {String} operationName - An operation name * @param {MLGraphBuilder} builder - A ML graph builder * @param {Object} resources - Resources used for building a graph * @returns {MLNamedOperands} */ const buildOperationWithTwoInputs = (operationName, builder, resources) => { // For example: MLOperand matmul(MLOperand a, MLOperand b); const namedOutputOperand = {}; const [inputOperandA, inputOperandB] = createMultiInputOperands(builder, resources); const outputOperand = resources.options ? builder[operationName](inputOperandA, inputOperandB, resources.options) : builder[operationName](inputOperandA, inputOperandB); namedOutputOperand[resources.expected.name] = outputOperand; return namedOutputOperand; }; const buildBatchNorm = (operationName, builder, resources) => { // MLOperand batchNormalization(MLOperand input, MLOperand mean, MLOperand variance, // optional MLBatchNormalizationOptions options = {}); const namedOutputOperand = {}; const [inputOperand, meanOperand, varianceOperand] = createMultiInputOperands(builder, resources); const batchNormOptions = {...resources.options}; if (batchNormOptions.scale) { batchNormOptions.scale = createConstantOperand(builder, batchNormOptions.scale); } if (batchNormOptions.bias) { batchNormOptions.bias = createConstantOperand(builder, batchNormOptions.bias); } if (batchNormOptions.activation) { batchNormOptions.activation = builder[batchNormOptions.activation](); } // invoke builder.batchNormalization() namedOutputOperand[resources.expected.name] = builder[operationName](inputOperand, meanOperand, varianceOperand, batchNormOptions); return namedOutputOperand; }; const buildCast = (operationName, builder, resources) => { // MLOperand cast(MLOperand input, MLOperandDataType type); const namedOutputOperand = {}; const inputOperand = createSingleInputOperand(builder, resources); // invoke builder.cast() namedOutputOperand[resources.expected.name] = builder[operationName](inputOperand, resources.type); return namedOutputOperand; }; const buildConcat = (operationName, builder, resources) => { // MLOperand concat(sequence inputs, unsigned long axis); const namedOutputOperand = {}; const inputOperands = []; let operand; for (let input of resources.inputs) { if (input.hasOwnProperty('constant') && input['constant']) { operand = createConstantOperand(builder, input); } else { operand = builder.input(input.name, {dataType: input.type, type: input.type, dimensions: input.shape}); } inputOperands.push(operand); } // invoke builder.concat() namedOutputOperand[resources.expected.name] = builder[operationName](inputOperands, resources.axis); return namedOutputOperand; }; const buildConstantRange = (operationName, builder, resources) => { const namedOutputOperand = {}; // invoke builder.constant(start, step, outputShape, type) namedOutputOperand[resources.expected.name] = builder[operationName](resources.inputs.start, resources.inputs.step, resources.outputShape, resources.type); return namedOutputOperand; }; const buildConvTranspose2d = (operationName, builder, resources) => { // MLOperand convTranspose2d(MLOperand input, MLOperand filter, optional MLConvTranspose2dOptions options = {}); const namedOutputOperand = {}; const [inputOperand, filterOperand] = createMultiInputOperands(builder, resources); let convTranspose2dOptions = {...resources.options}; if (convTranspose2dOptions.bias) { convTranspose2dOptions.bias = createConstantOperand(builder, convTranspose2dOptions.bias); } if (convTranspose2dOptions.activation) { convTranspose2dOptions.activation = builder[convTranspose2dOptions.activation](); } namedOutputOperand[resources.expected.name] = builder[operationName](inputOperand, filterOperand, convTranspose2dOptions); return namedOutputOperand; }; const buildConv2d = (operationName, builder, resources) => { // MLOperand conv2d(MLOperand input, MLOperand filter, optional MLConv2dOptions options = {}); const namedOutputOperand = {}; const [inputOperand, filterOperand] = createMultiInputOperands(builder, resources); let conv2dOptions = {...resources.options}; if (conv2dOptions.bias) { conv2dOptions.bias = createConstantOperand(builder, conv2dOptions.bias); } if (conv2dOptions.activation) { conv2dOptions.activation = builder[conv2dOptions.activation](); } namedOutputOperand[resources.expected.name] = builder[operationName](inputOperand, filterOperand, conv2dOptions); return namedOutputOperand; }; const buildGemm = (operationName, builder, resources) => { // MLOperand gemm(MLOperand a, MLOperand b, optional MLGemmOptions options = {}); const namedOutputOperand = {}; const [inputOperandA, inputOperandB] = createMultiInputOperands(builder, resources); let gemmOptions = {...resources.options}; if (gemmOptions.c) { if (gemmOptions.c.shape) { gemmOptions.c = createConstantOperand(builder, gemmOptions.c); } else { // MLOperand c; // Create a single-value operand when c is a scalar gemmOptions.c = builder.constant({dataType: 'float32', type: 'float32', dimensions: [1]}, new Float32Array([gemmOptions.c])); } } namedOutputOperand[resources.expected.name] = builder[operationName](inputOperandA, inputOperandB, gemmOptions); return namedOutputOperand; }; const buildLayerNorm = (operationName, builder, resources) => { // MLOperand layerNormalization(MLOperand input, optional MLLayerNormalizationOptions options = {}); // MLOperand instanceNormalization(MLOperand input, optional MLInstanceNormalizationOptions options = {}); const namedOutputOperand = {}; const inputOperand = createSingleInputOperand(builder, resources); const layerNormOptions = {...resources.options}; if (layerNormOptions.scale) { layerNormOptions.scale = createConstantOperand(builder, layerNormOptions.scale); } if (layerNormOptions.bias) { layerNormOptions.bias = createConstantOperand(builder, layerNormOptions.bias); } // invoke builder.layerNormalization() or builder.instanceNormalization() namedOutputOperand[resources.expected.name] = builder[operationName](inputOperand, layerNormOptions); return namedOutputOperand; }; const buildPad = (operationName, builder, resources) => { // MLOperand pad(MLOperand input, sequence beginningPadding, sequence endingPadding, optional MLPadOptions options = {}); const namedOutputOperand = {}; const inputOperand = createSingleInputOperand(builder, resources); // invoke builder.pad() namedOutputOperand[resources.expected.name] = builder[operationName](inputOperand, resources.beginningPadding, resources.endingPadding, resources.options); return namedOutputOperand; }; const buildReshape = (operationName, builder, resources) => { // MLOperand reshape(MLOperand input, sequence newShape); // MLOperand expand(MLOperand input, sequence newShape); const namedOutputOperand = {}; const inputOperand = createSingleInputOperand(builder, resources); // invoke builder.reshape() or builder.expand() namedOutputOperand[resources.expected.name] = builder[operationName](inputOperand, resources.newShape); return namedOutputOperand; }; const buildSlice = (operationName, builder, resources) => { // MLOperand slice(MLOperand input, sequence starts, sequence sizes); const namedOutputOperand = {}; const inputOperand = createSingleInputOperand(builder, resources); // invoke builder.slice() namedOutputOperand[resources.expected.name] = builder[operationName](inputOperand, resources.starts, resources.sizes); return namedOutputOperand; }; const buildSplit = (operationName, builder, resources) => { // sequence split(MLOperand input, // (unsigned long or sequence) splits, // optional MLSplitOptions options = {}); const namedOutputOperand = {}; const inputOperand = createSingleInputOperand(builder, resources); // invoke builder.split() const outputOperands = builder[operationName](inputOperand, resources.splits, resources.options); resources.expected.forEach((resourceDict, index) => { namedOutputOperand[resourceDict.name] = outputOperands[index]; }); return namedOutputOperand; }; const buildWhere = (operationName, builder, resources) => { // MLOperand where(MLOperand condition, MLOperand trueValues, MLOperand falseValues); const namedOutputOperand = {}; const [conditionOperand, trueValuesOperand, falseValuesOperand] = createMultiInputOperands(builder, resources); // invoke builder.where() namedOutputOperand[resources.expected.name] = builder[operationName](conditionOperand, trueValuesOperand, falseValuesOperand); return namedOutputOperand; }; /** * Build a graph. * @param {String} operationName - An operation name * @param {MLGraphBuilder} builder - A ML graph builder * @param {Object} resources - Resources used for building a graph * @param {Function} buildFunc - A build function for an operation * @returns [namedOperands, inputs, outputs] */ const buildGraph = (operationName, builder, resources, buildFunc) => { const namedOperands = buildFunc(operationName, builder, resources); let inputs = {}; if (Array.isArray(resources.inputs)) { // the inputs of concat() is a sequence for (let subInput of resources.inputs) { if (!subInput.hasOwnProperty('constant') || !subInput.constant) { inputs[subInput.name] = getTypedArrayData( subInput.type, sizeOfShape(subInput.shape), subInput.data); } } } else { for (let inputName in resources.inputs) { const subTestByName = resources.inputs[inputName]; if (!subTestByName.hasOwnProperty('constant') || !subTestByName.constant) { inputs[inputName] = getTypedArrayData( subTestByName.type, sizeOfShape(subTestByName.shape), subTestByName.data); } } } let outputs = {}; if (Array.isArray(resources.expected)) { // the outputs of split() or gru() is a sequence for (let i = 0; i < resources.expected.length; i++) { const subExpected = resources.expected[i]; outputs[subExpected.name] = new TypedArrayDict[subExpected.type](sizeOfShape(subExpected.shape)); } } else { // matmul 1D with 1D produces a scalar which doesn't have its shape const shape = resources.expected.shape ? resources.expected.shape : [1]; outputs[resources.expected.name] = new TypedArrayDict[resources.expected.type](sizeOfShape(shape)); } return [namedOperands, inputs, outputs]; }; /** * Build a graph, compile graph and execute, then check computed results. * @param {String} operationName - An operation name * @param {MLContext} context - A ML context * @param {MLGraphBuilder} builder - A ML graph builder * @param {Object} resources - Resources used for building a graph * @param {Function} buildFunc - A build function for an operation */ const run = async (operationName, context, builder, resources, buildFunc) => { // build a graph const [namedOutputOperands, inputs, outputs] = buildGraph(operationName, builder, resources, buildFunc); // compile the graph up to the output operand const graph = await builder.build(namedOutputOperands); // execute the compiled graph const result = await context.compute(graph, inputs, outputs); checkResults(operationName, namedOutputOperands, result.outputs, resources); }; const variant = location.search.substring(1); const contextOptions = kContextOptionsForVariant[variant]; /** * Run WebNN operation tests. * @param {(String[]|String)} operationName - An operation name array or an operation name * @param {Function} buildFunc - A build function for an operation */ const testWebNNOperation = (operationName, buildFunc) => { let operationNameArray; if (typeof operationName === 'string') { operationNameArray = [operationName]; } else if (Array.isArray(operationName)) { operationNameArray = operationName; } let context; let builder; operationNameArray.forEach((subOperationName) => { const tests = loadTests(subOperationName); promise_setup(async () => { let supported = false; try { context = await navigator.ml.createContext(contextOptions); supported = true; } catch (e) { } assert_implements( supported, `Unable to create context for ${variant} variant`); builder = new MLGraphBuilder(context); }); for (const subTest of tests) { promise_test(async () => { await run(subOperationName, context, builder, subTest, buildFunc); }, `${subTest.name}`); } }); }; /** * WebNN parallel compute operation test. */ const testParallelCompute = () => { let ml_context; let ml_graph; promise_setup(async () => { let supported = false; try { ml_context = await navigator.ml.createContext(contextOptions); supported = true; } catch (e) { } assert_implements( supported, `Unable to create context for ${variant} variant`); // Construct a simple graph: A = B * 2. const builder = new MLGraphBuilder(ml_context); const operandType = {dataType: 'float32', dimensions: [1]}; const input_operand = builder.input('input', operandType); const const_operand = builder.constant(operandType, Float32Array.from([2])); const output_operand = builder.mul(input_operand, const_operand); ml_graph = await builder.build({'output': output_operand}); }); promise_test(async () => { const test_inputs = [1, 2, 3, 4]; const actual_outputs = await Promise.all(test_inputs.map(async (input) => { let inputs = {'input': Float32Array.from([input])}; let outputs = {'output': new Float32Array(1)}; ({inputs, outputs} = await ml_context.compute(ml_graph, inputs, outputs)); return outputs.output[0]; })); const expected_outputs = [2, 4, 6, 8]; assert_array_equals(actual_outputs, expected_outputs); }); }; /** * Run WebNN conformance tests by specified operation. * @param {(String[]|String)} operationName - An operation name array or an * operation name * @param {Function} buildFunc - A build function for an operation */ const runWebNNConformanceTests = (operationName, buildFunc) => { // Link to https://github.com/web-platform-tests/wpt/pull/44883 // Check navigator.ml is defined before trying to run WebNN tests if (navigator.ml) { testWebNNOperation(operationName, buildFunc); } else { // Show indication to users why the test failed test( () => assert_not_equals( navigator.ml, undefined, 'ml property is defined on navigator')); } }; // ref: http://stackoverflow.com/questions/32633585/how-do-you-convert-to-half-floats-in-javascript const toHalf = (value) => { let floatView = new Float32Array(1); let int32View = new Int32Array(floatView.buffer); /* This method is faster than the OpenEXR implementation (very often * used, eg. in Ogre), with the additional benefit of rounding, inspired * by James Tursa's half-precision code. */ floatView[0] = value; let x = int32View[0]; let bits = (x >> 16) & 0x8000; /* Get the sign */ let m = (x >> 12) & 0x07ff; /* Keep one extra bit for rounding */ let e = (x >> 23) & 0xff; /* Using int is faster here */ /* If zero, or denormal, or exponent underflows too much for a denormal * half, return signed zero. */ if (e < 103) { return bits; } /* If NaN, return NaN. If Inf or exponent overflow, return Inf. */ if (e > 142) { bits |= 0x7c00; /* If exponent was 0xff and one mantissa bit was set, it means NaN, * not Inf, so make sure we set one mantissa bit too. */ bits |= ((e == 255) ? 0 : 1) && (x & 0x007fffff); return bits; } /* If exponent underflows but not too much, return a denormal */ if (e < 113) { m |= 0x0800; /* Extra rounding may overflow and set mantissa to 0 and exponent * to 1, which is OK. */ bits |= (m >> (114 - e)) + ((m >> (113 - e)) & 1); return bits; } bits |= ((e - 112) << 10) | (m >> 1); /* Extra rounding. An overflow will set mantissa to 0 and increment * the exponent, which is OK. */ bits += m & 1; return bits; }; /** * WebNN buffer creation. * @param {MLContext} context - the context used to create the buffer. * @param {Number} bufferSize - Size of the buffer to create, in bytes. * @returns {MLBuffer} the created buffer. */ const createBuffer = (context, bufferSize) => { let buffer; try { buffer = context.createBuffer({size: bufferSize}); assert_equals(buffer.size, bufferSize); } catch (e) { assert_true(e instanceof DOMException); assert_equals(e.name, "NotSupportedError"); } return buffer; }; /** * WebNN destroy buffer twice test. * @param {String} testName - The name of the test operation. */ const testDestroyWebNNBuffer = (testName) => { let context; let buffer; promise_setup(async () => { let supported = false; try { context = await navigator.ml.createContext(contextOptions); supported = true; } catch (e) { } assert_implements( supported, `Unable to create context for ${variant} variant`); buffer = createBuffer(context, 4); }); promise_test(async () => { // MLBuffer is not supported for this deviceType. if (buffer === undefined) { return; } buffer.destroy(); buffer.destroy(); }, `${testName}`); }; /** * WebNN create buffer test. * @param {String} testName - The name of the test operation. * @param {Number} bufferSize - Size of the buffer to create, in bytes. */ const testCreateWebNNBuffer = (testName, bufferSize) => { let context; promise_setup(async () => { let supported = false; try { context = await navigator.ml.createContext(contextOptions); supported = true; } catch (e) { } assert_implements( supported, `Unable to create context for ${variant} variant`); }); promise_test(async () => { createBuffer(context, bufferSize); }, `${testName} / ${bufferSize}`); }; /** * Asserts the buffer data in MLBuffer matches expected. * @param {MLContext} ml_context - The context used to create the buffer. * @param {MLBuffer} ml_buffer - The buffer to read and compare data. * @param {Array} expected - Array of the expected data in the buffer. */ const assert_buffer_data_equals = async (ml_context, ml_buffer, expected) => { const actual = await ml_context.readBuffer(ml_buffer); assert_array_equals( new expected.constructor(actual), expected, 'Read buffer data equals expected data.'); }; /** * WebNN write buffer operation test. * @param {String} testName - The name of the test operation. */ const testWriteWebNNBuffer = (testName) => { let ml_context; promise_setup(async () => { let supported = false; try { ml_context = await navigator.ml.createContext(contextOptions); supported = true; } catch (e) { } assert_implements( supported, `Unable to create context for ${variant} variant`); }); promise_test(async () => { let ml_buffer = createBuffer(ml_context, 4); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { return; } let array_buffer = new ArrayBuffer(ml_buffer.size); // Writing with a size that goes past that source buffer length. assert_throws_js( TypeError, () => ml_context.writeBuffer( ml_buffer, new Uint8Array(array_buffer), /*srcOffset=*/ 0, /*srcSize=*/ ml_buffer.size + 1)); assert_throws_js( TypeError, () => ml_context.writeBuffer( ml_buffer, new Uint8Array(array_buffer), /*srcOffset=*/ 3, /*srcSize=*/ 4)); // Writing with a source offset that is out of range of the source size. assert_throws_js( TypeError, () => ml_context.writeBuffer( ml_buffer, new Uint8Array(array_buffer), /*srcOffset=*/ ml_buffer.size + 1)); // Writing with a source offset that is out of range of implicit copy size. assert_throws_js( TypeError, () => ml_context.writeBuffer( ml_buffer, new Uint8Array(array_buffer), /*srcOffset=*/ ml_buffer.size + 1, /*srcSize=*/ undefined)); assert_throws_js( TypeError, () => ml_context.writeBuffer( ml_buffer, new Uint8Array(array_buffer), /*srcOffset=*/ undefined, /*srcSize=*/ ml_buffer.size + 1)); assert_throws_js( TypeError, () => ml_context.writeBuffer( ml_buffer, Uint8Array.from([0xEE, 0xEE, 0xEE, 0xEE, 0xEE]))); }, `${testName} / error`); promise_test(async () => { let ml_buffer = createBuffer(ml_context, 4); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { return; } // Writing data to a destroyed MLBuffer should throw. ml_buffer.destroy(); assert_throws_dom( 'InvalidStateError', () => ml_context.writeBuffer(ml_buffer, new Uint8Array(ml_buffer.size))); }, `${testName} / destroy`); promise_test(async () => { let ml_buffer = createBuffer(ml_context, 4); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { return; } const array_buffer = new ArrayBuffer(ml_buffer.size); const detached_buffer = array_buffer.transfer(); assert_true(array_buffer.detached, 'array buffer should be detached.'); ml_context.writeBuffer(ml_buffer, array_buffer); }, `${testName} / detached`); promise_test(async () => { let ml_buffer = createBuffer(ml_context, 4); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { return; } let another_ml_context = await navigator.ml.createContext(contextOptions); let another_ml_buffer = createBuffer(another_ml_context, ml_buffer.size); let input_data = new Uint8Array(ml_buffer.size).fill(0xAA); assert_throws_js( TypeError, () => ml_context.writeBuffer(another_ml_buffer, input_data)); assert_throws_js( TypeError, () => another_ml_context.writeBuffer(ml_buffer, input_data)); }, `${testName} / context_mismatch`); }; /** * WebNN read buffer operation test. * @param {String} testName - The name of the test operation. */ const testReadWebNNBuffer = (testName) => { let ml_context; promise_setup(async () => { let supported = false; try { ml_context = await navigator.ml.createContext(contextOptions); supported = true; } catch (e) { } assert_implements( supported, `Unable to create context for ${variant} variant`); }); promise_test(async t => { let ml_buffer = createBuffer(ml_context, 4); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { return; } // Reading a destroyed MLBuffer should reject. ml_buffer.destroy(); await promise_rejects_dom( t, 'InvalidStateError', ml_context.readBuffer(ml_buffer)); }, `${testName} / destroy`); promise_test(async () => { let ml_buffer = createBuffer(ml_context, 4); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { return; } // Initialize the buffer. ml_context.writeBuffer( ml_buffer, Uint8Array.from([0xAA, 0xAA, 0xAA, 0xAA])); ml_context.writeBuffer(ml_buffer, Uint32Array.from([0xBBBBBBBB])); await assert_buffer_data_equals( ml_context, ml_buffer, Uint32Array.from([0xBBBBBBBB])); ; }, `${testName} / full_size`); promise_test(async () => { let ml_buffer = createBuffer(ml_context, 4); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { return; } // Initialize the buffer. ml_context.writeBuffer( ml_buffer, Uint8Array.from([0xAA, 0xAA, 0xAA, 0xAA])); // Writing to the remainder of the buffer from source offset. ml_context.writeBuffer( ml_buffer, Uint8Array.from([0xCC, 0xCC, 0xBB, 0xBB]), /*srcOffset=*/ 2); await assert_buffer_data_equals( ml_context, ml_buffer, Uint8Array.from([0xBB, 0xBB, 0xAA, 0xAA])); }, `${testName} / src_offset_only`); promise_test(async () => { let ml_buffer = createBuffer(ml_context, 4); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { return; } // Initialize the buffer. const input_data = [0xAA, 0xAA, 0xAA, 0xAA]; ml_context.writeBuffer(ml_buffer, Uint8Array.from(input_data)); // Writing zero bytes at the end of the buffer. ml_context.writeBuffer( ml_buffer, Uint32Array.from([0xBBBBBBBB]), /*srcOffset=*/ 1); await assert_buffer_data_equals( ml_context, ml_buffer, Uint8Array.from(input_data)); }, `${testName} / zero_write`); promise_test(async () => { let ml_buffer = createBuffer(ml_context, 4); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { return; } // Initialize the buffer. ml_context.writeBuffer( ml_buffer, Uint8Array.from([0xAA, 0xAA, 0xAA, 0xAA])); // Writing with both a source offset and size. ml_context.writeBuffer( ml_buffer, Uint8Array.from([0xDD, 0xDD, 0xCC, 0xDD]), /*srcOffset=*/ 2, /*srcSize=*/ 1); await assert_buffer_data_equals( ml_context, ml_buffer, Uint8Array.from([0xCC, 0xAA, 0xAA, 0xAA])); }, `${testName} / src_offset_and_size`); promise_test(async () => { let ml_buffer = createBuffer(ml_context, 4); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { return; } // Initialize the buffer. ml_context.writeBuffer( ml_buffer, Uint8Array.from([0xAA, 0xAA, 0xAA, 0xAA])); // Using an offset allows a larger source buffer to fit. ml_context.writeBuffer( ml_buffer, Uint8Array.from([0xEE, 0xEE, 0xEE, 0xEE, 0xEE]), /*srcOffset=*/ 1); await assert_buffer_data_equals( ml_context, ml_buffer, Uint8Array.from([0xEE, 0xEE, 0xEE, 0xEE])); }, `${testName} / larger_src_data`); promise_test(async () => { let ml_buffer = createBuffer(ml_context, 4); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { return; } const input_data = [0xAA, 0xAA, 0xAA, 0xAA]; // Writing with a source offset of undefined should be treated as 0. ml_context.writeBuffer( ml_buffer, Uint8Array.from(input_data), /*srcOffset=*/ undefined, /*srcSize=*/ input_data.length); await assert_buffer_data_equals( ml_context, ml_buffer, Uint8Array.from(input_data)); }, `${testName} / no_src_offset`); promise_test(async t => { let ml_buffer = createBuffer(ml_context, 4); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { return; } let another_ml_context = await navigator.ml.createContext(contextOptions); let another_ml_buffer = createBuffer(another_ml_context, ml_buffer.size); await promise_rejects_js( t, TypeError, ml_context.readBuffer(another_ml_buffer)); await promise_rejects_js( t, TypeError, another_ml_context.readBuffer(ml_buffer)); }, `${testName} / context_mismatch`); };