trisquel-icecat/icecat/testing/web-platform/tests/webnn/resources/utils.js

2015 lines
72 KiB
JavaScript

'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.length ?
sizes.reduce((accumulator, currentValue) => accumulator * currentValue) :
1;
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.<String, MLOperand>} namedOutputOperands
* @param {Object.<MLNamedArrayBufferViews>} 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<MLOperand> 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<unsigned long> beginningPadding, sequence<unsigned long> 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<unsigned long> newShape);
// MLOperand expand(MLOperand input, sequence<unsigned long> 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<unsigned long> starts, sequence<unsigned long> 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<MLOperand> split(MLOperand input,
// (unsigned long or sequence<unsigned long>) 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];
/**
* Checks if MLBuffer is implemented or not.
* @param {MLContext} ml_context - A ML context to test for MLBuffer support.
* @returns {Boolean} True if MLBuffer is supported; otherwise, False.
*/
const isMLBufferSupported =
(ml_context) => {
return (createBuffer(ml_context, 4) !== undefined);
}
/**
* 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`);
};
/**
* WebNN dispatch buffer operation test.
* @param {String} testName - The name of the test operation.
*/
const testDispatchWebNNBuffer = (testName) => {
let ml_context;
let ml_graph;
const shape = [3, 5];
let inputs = {};
let outputs = {};
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 + C, with two outputs.
const builder = new MLGraphBuilder(ml_context);
const operandType = {dataType: 'float32', dimensions: shape};
const lhs_operand = builder.input('lhs', operandType);
const rhs_operand = builder.input('rhs', operandType);
const output_1_operand = builder.add(lhs_operand, rhs_operand);
const output_2_operand = builder.add(lhs_operand, rhs_operand);
ml_graph = await builder.build(
{'output1': output_1_operand, 'output2': output_2_operand});
const ml_buffer_size =
TypedArrayDict['float32'].BYTES_PER_ELEMENT * sizeOfShape(shape);
// MLBuffer was unsupported for the deviceType.
if (!isMLBufferSupported(ml_context)) {
return;
}
inputs = {
'lhs': ml_context.createBuffer({size: ml_buffer_size}),
'rhs': ml_context.createBuffer({size: ml_buffer_size}),
};
outputs = {
'output1': ml_context.createBuffer({size: ml_buffer_size}),
'output2': ml_context.createBuffer({size: ml_buffer_size}),
};
});
promise_test(async () => {
// MLBuffer was unsupported for the deviceType.
if (!isMLBufferSupported(ml_context)) {
return;
}
let another_ml_context = await navigator.ml.createContext(contextOptions);
// Control case, same context.
ml_context.dispatch(ml_graph, inputs, outputs);
// Test the wrong context being used for inputs.
assert_throws_js(
TypeError,
() => ml_context.dispatch(
ml_graph, {
'lhs':
another_ml_context.createBuffer({size: inputs['lhs'].size()}),
'rhs': inputs['rhs'],
},
outputs));
// Test the wrong context being used for outputs.
assert_throws_js(TypeError, () => ml_context.dispatch(ml_graph, inputs, {
'output1':
another_ml_context.createBuffer({size: outputs['output1'].size()}),
'output2': outputs['output2'],
}));
}, `${testName} / context_mismatch`);
promise_test(async () => {
// MLBuffer was unsupported for the deviceType.
if (!isMLBufferSupported(ml_context)) {
return;
}
// Control case, valid size.
ml_context.dispatch(ml_graph, inputs, outputs);
// Input is too large.
assert_throws_js(
TypeError,
() => ml_context.dispatch(
ml_graph, {
'lhs': ml_context.createBuffer({size: inputs['lhs'].size() + 1}),
'rhs': inputs['rhs'],
},
outputs));
assert_throws_js(
TypeError,
() => ml_context.dispatch(
ml_graph, {
'lhs': inputs['lhs'],
'rhs': ml_context.createBuffer({size: inputs['rhs'].size() + 1}),
},
outputs));
// Output is too large.
assert_throws_js(TypeError, () => ml_context.dispatch(ml_graph, inputs, {
'output1': ml_context.createBuffer({size: outputs['output1'].size() + 1}),
'output2': outputs['output2'],
}));
assert_throws_js(TypeError, () => ml_context.dispatch(ml_graph, inputs, {
'output1': outputs['output1'],
'output2': ml_context.createBuffer({size: outputs['output2'].size() + 1}),
}));
}, `${testName} / invalid_size`);
promise_test(async () => {
// MLBuffer was unsupported for the deviceType.
if (!isMLBufferSupported(ml_context)) {
return;
}
// Control case, valid names.
ml_context.dispatch(ml_graph, inputs, outputs);
// No names is invalid.
assert_throws_js(TypeError, () => ml_context.dispatch(ml_graph, {}, {}));
// Input name is invalid.
assert_throws_js(
TypeError,
() => ml_context.dispatch(
ml_graph, {
'a_different_input_name': inputs['lhs'],
'rhs': inputs['rhs'],
},
outputs));
assert_throws_js(
TypeError,
() => ml_context.dispatch(
ml_graph, {
'lhs': inputs['lhs'],
'a_different_input_name': inputs['rhs'],
},
outputs));
// Output name is invalid.
assert_throws_js(TypeError, () => ml_context.dispatch(ml_graph, inputs, {
'a_different_output_name': outputs['output1'],
'output2': outputs['output2'],
}));
assert_throws_js(TypeError, () => ml_context.dispatch(ml_graph, inputs, {
'output1': outputs['output1'],
'a_different_output_name': outputs['output2'],
}));
// Too few named inputs is invalid.
assert_throws_js(
TypeError,
() => ml_context.dispatch(
ml_graph, {
'lhs': inputs['lhs'],
},
outputs));
// Too many named inputs is invalid.
assert_throws_js(
TypeError,
() => ml_context.dispatch(
ml_graph, {
'lhs': inputs['lhs'],
'rhs': inputs['rhs'],
'a_different_input_name':
ml_context.createBuffer({size: inputs['rhs'].size()}),
},
outputs));
// Too few named outputs is invalid.
assert_throws_js(TypeError, () => ml_context.dispatch(ml_graph, inputs, {
'output1': outputs['output1']
}));
// Too many named outputs is invalid.
assert_throws_js(TypeError, () => ml_context.dispatch(ml_graph, inputs, {
'output1': outputs['output1'],
'output2': outputs['output2'],
'a_different_output_name':
ml_context.createBuffer({size: outputs['output2'].size()}),
}));
}, `${testName} / invalid_name`);
promise_test(async () => {
// MLBuffer was unsupported for the deviceType.
if (!isMLBufferSupported(ml_context)) {
return;
}
// Control case, valid buffers.
ml_context.dispatch(ml_graph, inputs, outputs);
// Same buffer used as outputs more than once is invalid.
assert_throws_js(TypeError, () => ml_context.dispatch(ml_graph, inputs, {
'output1': outputs['output1'],
'output2': outputs['output1'],
}));
// Same buffer used as input and output is invalid.
assert_throws_js(TypeError, () => ml_context.dispatch(ml_graph, inputs, {
'output1': inputs['lhs'],
'output2': outputs['output2'],
}));
assert_throws_js(
TypeError,
() => ml_context.dispatch(
ml_graph, {
'lhs': outputs['output1'],
'rhs': inputs['rhs'],
},
outputs));
// Buffer that does not exist is invalid.
assert_throws_js(
TypeError,
() => ml_context.dispatch(
ml_graph, {
'lhs': undefined,
'rhs': inputs['rhs'],
},
outputs));
assert_throws_js(TypeError, () => ml_context.dispatch(ml_graph, inputs, {
'output1': undefined,
'output2': outputs['output2'],
}));
}, `${testName} / invalid_buffer`);
promise_test(async () => {
// MLBuffer was unsupported for the deviceType.
if (!isMLBufferSupported(ml_context)) {
return;
}
const dispatch_inputs = {
'lhs': ml_context.createBuffer({size: inputs['lhs'].size}),
'rhs': ml_context.createBuffer({size: inputs['rhs'].size}),
};
const dispatch_1_outputs = {
'output1': ml_context.createBuffer({size: outputs['output1'].size}),
'output2': ml_context.createBuffer({size: outputs['output2'].size}),
};
const dispatch_2_outputs = {
'output1': ml_context.createBuffer({size: outputs['output1'].size}),
'output2': ml_context.createBuffer({size: outputs['output2'].size}),
};
// Initialize inputs
const input_data =
new TypedArrayDict['float32'](sizeOfShape(shape)).fill(1.0);
ml_context.writeBuffer(dispatch_inputs['lhs'], input_data);
ml_context.writeBuffer(dispatch_inputs['rhs'], input_data);
// Output_1 = LHS + RHS = 1 + 1 = 2
ml_context.dispatch(ml_graph, dispatch_inputs, dispatch_1_outputs);
// Output_2 = LHS + RHS = 1 + 1 = 2
ml_context.dispatch(ml_graph, dispatch_inputs, dispatch_2_outputs);
await assert_buffer_data_equals(
ml_context, dispatch_1_outputs['output1'],
new Float32Array(sizeOfShape(shape)).fill(2.0));
await assert_buffer_data_equals(
ml_context, dispatch_1_outputs['output2'],
new Float32Array(sizeOfShape(shape)).fill(2.0));
await assert_buffer_data_equals(
ml_context, dispatch_2_outputs['output1'],
new Float32Array(sizeOfShape(shape)).fill(2.0));
await assert_buffer_data_equals(
ml_context, dispatch_2_outputs['output2'],
new Float32Array(sizeOfShape(shape)).fill(2.0));
}, `${testName} / same_inputs`);
promise_test(async () => {
// MLBuffer was unsupported for the deviceType.
if (!isMLBufferSupported(ml_context)) {
return;
}
const dispatch_1_inputs = {
'lhs': ml_context.createBuffer({size: inputs['lhs'].size}),
'rhs': ml_context.createBuffer({size: inputs['rhs'].size}),
};
const dispatch_2_inputs = {
'lhs': ml_context.createBuffer({size: inputs['lhs'].size}),
'rhs': ml_context.createBuffer({size: inputs['rhs'].size}),
};
const dispatch_outputs = {
'output1': ml_context.createBuffer({size: outputs['output1'].size}),
'output2': ml_context.createBuffer({size: outputs['output2'].size}),
};
// Initialize inputs
const input_1_data =
new TypedArrayDict['float32'](sizeOfShape(shape)).fill(1.0);
ml_context.writeBuffer(dispatch_1_inputs['lhs'], input_1_data);
ml_context.writeBuffer(dispatch_1_inputs['rhs'], input_1_data);
const input_2_data =
new TypedArrayDict['float32'](sizeOfShape(shape)).fill(2.0);
ml_context.writeBuffer(dispatch_2_inputs['lhs'], input_2_data);
ml_context.writeBuffer(dispatch_2_inputs['rhs'], input_2_data);
// Output = LHS_1 + RHS_1 = 1 + 1 = 2
ml_context.dispatch(ml_graph, dispatch_1_inputs, dispatch_outputs);
// Output = LHS_2 + RHS_2 = 2 + 2 = 4
ml_context.dispatch(ml_graph, dispatch_2_inputs, dispatch_outputs);
await assert_buffer_data_equals(
ml_context, dispatch_outputs['output1'],
new Float32Array(sizeOfShape(shape)).fill(4.0));
await assert_buffer_data_equals(
ml_context, dispatch_outputs['output2'],
new Float32Array(sizeOfShape(shape)).fill(4.0));
}, `${testName} / same_outputs`);
promise_test(async () => {
// MLBuffer was unsupported for the deviceType.
if (!isMLBufferSupported(ml_context)) {
return;
}
const dispatch_inputs = {
'lhs': ml_context.createBuffer({size: inputs['lhs'].size}),
'rhs': ml_context.createBuffer({size: inputs['rhs'].size}),
};
const dispatch_outputs = {
'output1': ml_context.createBuffer({size: outputs['output1'].size}),
'output2': ml_context.createBuffer({size: outputs['output2'].size}),
};
// Initialize inputs
const input_data =
new TypedArrayDict['float32'](sizeOfShape(shape)).fill(1.0);
ml_context.writeBuffer(dispatch_inputs['lhs'], input_data);
ml_context.writeBuffer(dispatch_inputs['rhs'], input_data);
// Output = LHS + RHS = 1 + 1 = 2
ml_context.dispatch(ml_graph, dispatch_inputs, dispatch_outputs);
ml_context.dispatch(ml_graph, dispatch_inputs, dispatch_outputs);
await assert_buffer_data_equals(
ml_context, dispatch_outputs['output1'],
new Float32Array(sizeOfShape(shape)).fill(2.0));
await assert_buffer_data_equals(
ml_context, dispatch_outputs['output2'],
new Float32Array(sizeOfShape(shape)).fill(2.0));
}, `${testName} / same_inputs_and_outputs`);
promise_test(async () => {
// MLBuffer was unsupported for the deviceType.
if (!isMLBufferSupported(ml_context)) {
return;
}
const dispatch_inputs = {
'lhs': ml_context.createBuffer({size: inputs['lhs'].size}),
'rhs': ml_context.createBuffer({size: inputs['rhs'].size}),
};
const dispatch_1_outputs = {
'output1': ml_context.createBuffer({size: outputs['output1'].size}),
'output2': ml_context.createBuffer({size: outputs['output2'].size}),
};
const dispatch_2_outputs = {
'output1': ml_context.createBuffer({size: outputs['output1'].size}),
'output2': ml_context.createBuffer({size: outputs['output2'].size}),
};
// Initialize inputs
const input_data =
new TypedArrayDict['float32'](sizeOfShape(shape)).fill(1.0);
ml_context.writeBuffer(dispatch_inputs['lhs'], input_data);
ml_context.writeBuffer(dispatch_inputs['rhs'], input_data);
// Output_1 = LHS + RHS = 1 + 1 = 2
ml_context.dispatch(ml_graph, dispatch_inputs, dispatch_1_outputs);
// Output_2 = Output_1_LHS + Output_1_RHS = 2 + 2 = 4
ml_context.dispatch(
ml_graph, {
'lhs': dispatch_1_outputs['output1'],
'rhs': dispatch_1_outputs['output2'],
},
dispatch_2_outputs);
// Output_1 = Output_2_LHS + Output_2_RHS = 4 + 4 = 8
ml_context.dispatch(
ml_graph, {
'lhs': dispatch_2_outputs['output1'],
'rhs': dispatch_2_outputs['output2'],
},
dispatch_1_outputs);
await assert_buffer_data_equals(
ml_context, dispatch_1_outputs['output1'],
new Float32Array(sizeOfShape(shape)).fill(8));
await assert_buffer_data_equals(
ml_context, dispatch_1_outputs['output2'],
new Float32Array(sizeOfShape(shape)).fill(8));
}, `${testName} / outputs_as_inputs`);
promise_test(async () => {
// MLBuffer was unsupported for the deviceType.
if (!isMLBufferSupported(ml_context)) {
return;
}
// Construct a simple graph: OUTPUT = LHS - RHS.
const builder = new MLGraphBuilder(ml_context);
const operandType = {dataType: 'float32', dimensions: shape};
const lhsOperand = builder.input('lhs', operandType);
const rhsOperand = builder.input('rhs', operandType);
const graph =
await builder.build({'output': builder.sub(lhsOperand, rhsOperand)});
const lhsBuffer = ml_context.createBuffer({size: inputs['lhs'].size});
const rhsBuffer = ml_context.createBuffer({size: inputs['rhs'].size});
const dispatchOutputs = {
'output': ml_context.createBuffer({size: outputs['output1'].size})
};
// Initialize inputs
ml_context.writeBuffer(
lhsBuffer, new TypedArrayDict['float32'](sizeOfShape(shape)).fill(5.0));
ml_context.writeBuffer(
rhsBuffer, new TypedArrayDict['float32'](sizeOfShape(shape)).fill(3.0));
// Output = LHS - RHS = 5 - 3 = 2
ml_context.dispatch(
graph, {
'lhs': lhsBuffer,
'rhs': rhsBuffer,
},
dispatchOutputs);
await assert_buffer_data_equals(
ml_context, dispatchOutputs['output'],
new Float32Array(sizeOfShape(shape)).fill(2));
// Output = RHS - LHS = 3 - 5 = -2
ml_context.dispatch(
graph, {
'lhs': rhsBuffer,
'rhs': lhsBuffer,
},
dispatchOutputs);
await assert_buffer_data_equals(
ml_context, dispatchOutputs['output'],
new Float32Array(sizeOfShape(shape)).fill(-2));
}, `${testName} / same name diff input buffers`);
promise_test(async () => {
// MLBuffer was unsupported for the deviceType.
if (!isMLBufferSupported(ml_context)) {
return;
}
const dispatchInputs = {
'lhs': ml_context.createBuffer({size: inputs['lhs'].size}),
'rhs': ml_context.createBuffer({size: inputs['rhs'].size}),
};
const outputBuffer1 =
ml_context.createBuffer({size: outputs['output1'].size});
const outputBuffer2 =
ml_context.createBuffer({size: outputs['output2'].size});
// Initialize inputs
const inputData1 =
new TypedArrayDict['float32'](sizeOfShape(shape)).fill(1.0);
ml_context.writeBuffer(dispatchInputs['lhs'], inputData1);
ml_context.writeBuffer(dispatchInputs['rhs'], inputData1);
// Output = LHS + RHS = 1 + 1 = 2
ml_context.dispatch(ml_graph, dispatchInputs, {
'output1': outputBuffer1,
'output2': outputBuffer2,
});
// Output = LHS + RHS = 2 + 2 = 4
const inputData2 =
new TypedArrayDict['float32'](sizeOfShape(shape)).fill(2.0);
ml_context.writeBuffer(dispatchInputs['lhs'], inputData2);
ml_context.writeBuffer(dispatchInputs['rhs'], inputData2);
ml_context.dispatch(ml_graph, dispatchInputs, {
'output1': outputBuffer1,
'output2': ml_context.createBuffer({size: outputs['output2'].size}),
});
// Ensure the last dispatch() did not modify the original second output
// buffer.
await assert_buffer_data_equals(
ml_context, outputBuffer2,
new Float32Array(sizeOfShape(shape)).fill(2));
}, `${testName} / same name diff outputs buffers`);
promise_test(async () => {
// MLBuffer was unsupported for the deviceType.
if (!isMLBufferSupported(ml_context)) {
return;
}
const dispatchInputs = {
'lhs': ml_context.createBuffer({size: inputs['lhs'].size}),
'rhs': ml_context.createBuffer({size: inputs['rhs'].size}),
};
const dispatchOutputs = {
'output1': ml_context.createBuffer({size: outputs['output1'].size}),
'output2': ml_context.createBuffer({size: outputs['output2'].size}),
};
// Initialize inputs
const inputData =
new TypedArrayDict['float32'](sizeOfShape(shape)).fill(1.0);
ml_context.writeBuffer(dispatchInputs['lhs'], inputData);
ml_context.writeBuffer(dispatchInputs['rhs'], inputData);
// Output = LHS + RHS = 1 + 1 = 2
ml_context.dispatch(ml_graph, dispatchInputs, dispatchOutputs);
// Check destroyed input buffers cannot be re-used in subsequent dispatches.
dispatchInputs['lhs'].destroy();
dispatchInputs['lhs'] = ml_context.createBuffer({size: inputs['lhs'].size});
const newInputData =
new TypedArrayDict['float32'](sizeOfShape(shape)).fill(2.0);
ml_context.writeBuffer(dispatchInputs['lhs'], newInputData);
// Output = LHS + RHS = 2 + 1 = 3
ml_context.dispatch(ml_graph, dispatchInputs, dispatchOutputs);
await assert_buffer_data_equals(
ml_context, dispatchOutputs['output1'],
new Float32Array(sizeOfShape(shape)).fill(3));
dispatchInputs['rhs'].destroy();
dispatchInputs['rhs'] = ml_context.createBuffer({size: inputs['rhs'].size});
ml_context.writeBuffer(dispatchInputs['rhs'], newInputData);
// Output = LHS + RHS = 2 + 2 = 4
ml_context.dispatch(ml_graph, dispatchInputs, dispatchOutputs);
await assert_buffer_data_equals(
ml_context, dispatchOutputs['output1'],
new Float32Array(sizeOfShape(shape)).fill(4));
}, `${testName} / same name diff inputs buffers destroy`);
promise_test(async () => {
// MLBuffer was unsupported for the deviceType.
if (!isMLBufferSupported(ml_context)) {
return;
}
const dispatchInputs = {
'lhs': ml_context.createBuffer({size: inputs['lhs'].size}),
'rhs': ml_context.createBuffer({size: inputs['rhs'].size}),
};
const dispatchOutputs = {
'output1': ml_context.createBuffer({size: outputs['output1'].size}),
'output2': ml_context.createBuffer({size: outputs['output2'].size}),
};
// Initialize inputs
const inputData =
new TypedArrayDict['float32'](sizeOfShape(shape)).fill(1.0);
ml_context.writeBuffer(dispatchInputs['lhs'], inputData);
ml_context.writeBuffer(dispatchInputs['rhs'], inputData);
// Output = LHS + RHS = 1 + 1 = 2
ml_context.dispatch(ml_graph, dispatchInputs, dispatchOutputs);
// Check destroyed output buffers cannot be re-used in subsequent
// dispatches.
dispatchOutputs['output1'].destroy();
dispatchOutputs['output1'] =
ml_context.createBuffer({size: outputs['output1'].size});
const newInputData =
new TypedArrayDict['float32'](sizeOfShape(shape)).fill(2.0);
ml_context.writeBuffer(dispatchInputs['lhs'], newInputData);
// Output = LHS + RHS = 2 + 1 = 3
ml_context.dispatch(ml_graph, dispatchInputs, dispatchOutputs);
await assert_buffer_data_equals(
ml_context, dispatchOutputs['output1'],
new Float32Array(sizeOfShape(shape)).fill(3));
}, `${testName} / same name diff outputs buffers destroy`);
};