2015 lines
72 KiB
JavaScript
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`);
|
|
};
|