// Proposal to add float16 TypedArrays to JavaScript. // URL: https://tc39.es/proposal-float16array/ // Use workaround Uint16 for Float16
float16: Uint16Array,
const findCompatibleType = (dataType, supportedTypes, castOpSupportLimits) => { if (!castOpSupportLimits.input.dataTypes.includes(dataType)) { // Cannot cast from `dataType` to any other type. returnnull;
}
for (let supportedType of supportedTypes) { if (kIntTypes.includes(dataType) &&
castOpSupportLimits.output.dataTypes.includes(dataType) &&
kIntTypes.indexOf(supportedType) > kIntTypes.indexOf(dataType)) { return supportedType;
}
if (kFloatTypes.includes(dataType)) { if (kFloatTypes.indexOf(supportedType) > kFloatTypes.indexOf(dataType)) { return supportedType;
}
}
} returnnull;
};
// The maximum index to validate for the output's expected value. const kMaximumIndexToValidate = 1000;
async function getContext() {
let context; try {
context = await navigator.ml.createContext(contextOptions);
} catch (e) { // A previous test case may kill the GPU process on which the WebNN service // runs. If you call `createContext` again immediately before the GPU // process restarts, it will fail again. So wait a moment and retry. if (e.message.includes('WebNN service connection error.')) {
await new Promise(resolve => setTimeout(resolve, 3000)); try {
context = await navigator.ml.createContext(contextOptions);
} catch (retryError) { thrownew AssertionError(
`Unable to create context for ${variant} variant on retry. ${retryError}`);
}
} else { thrownew AssertionError(
`Unable to create context for ${variant} variant. ${e}`);
}
} return context;
}
const tcNameArray = searchParams.getAll('tc');
function isTargetTest(test) { return tcNameArray.length === 0 || tcNameArray.includes(test.name);
}
const assertDescriptorsEquals = (outputOperand, expected) => { const dataType =
expected.castedType ? expected.castedType : expected.dataType;
assert_equals(
outputOperand.dataType, dataType, 'actual output dataType should be equal to expected output dataType');
assert_array_equals(
outputOperand.shape, expected.shape, 'actual output shape should be equal to expected output shape');
};
/* This method is faster than the OpenEXR implementation (very often *used,eg.inOgre),withtheadditionalbenefitofrounding,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. */ if (e == 255 && (x & 0x007fffff)) {
bits |= 1;
} 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;
};
const getTypedArrayData = (type, size, data) => {
let outData;
if (type === 'float16') { if (typeof (data) === 'number' && size > 1) { returnnew 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]);
}
} elseif (type === 'int64' || type === 'uint64') { if (typeof (data) === 'number' && size > 1) { returnnew 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]);
}
} elseif (type === 'uint4' || type === 'int4') { // The first nybble is stored in the first bits 0-3, and later bits 4-7 // store the later nybble. The data is packed, without any padding between // dimensions. For example: an array of uint4: // size = [2,5] // values = [1,2,3,4,5,6,7,8,9,10] // Would yield 5 hex bytes: // Uint8Array.of(0x21, 0x43, 0x65, 0x87, 0xA9); const array = new TypedArrayDict[type](Math.ceil(size / 2));
let i = 0; while (i < size - 1) { const packedByte = ((data[i + 1] & 0xF) << 4) | (data[i] & 0xF);
array[Math.floor(i / 2)] = packedByte;
i = i + 2;
} // Handle the odd size. if (i === size - 1) { const packedByte = data[i] & 0xF;
array[Math.floor(i / 2)] = packedByte;
} return array;
} else { if (typeof (data) === 'number' && size > 1) { returnnew TypedArrayDict[type](size).fill(data);
}
outData = new TypedArrayDict[type](data);
} return outData;
};
// TODO: See if callers can be updated to pass matching type.
nulp = typeof distance === 'bigint' ? BigInt(nulp) : Number(nulp);
assert_less_than_equal(distance, nulp,
`assert_array_approx_equals_ulp: ${description} actual ` +
`${
dataType === 'float16' ?
float16AsUint16ToNumber(actual[i]) :
actual[i]} should be close enough to expected ` +
`${expected[i]} by ULP distance:`);
}
}
};
/** *ComputetheULPdistancebetween``a``and``b``forthegiven``dataType``. * *@param{(Number|BigInt)}a-Firstvalue. *@param{(Number|BigInt)}b-Secondvalue. *@param{String}dataType-Adatatypestring,value:"float32", *moretypes,pleasesee: *https://www.w3.org/TR/webnn/#enumdef-mloperanddatatype
*/ const ulpDistance = (a, b, dataType) => {
let aBitwise, bBitwise; // measure the ULP distance if (dataType === 'float32') {
aBitwise = getBitwise(a, dataType);
bBitwise = getBitwise(b, dataType);
} elseif (dataType === 'float16') {
aBitwise = a; // convert b data of Float16 to Uint16
bBitwise = toHalf(b);
// Workaround to use mask to check returned special float16 value -0.0 which // is 32768 (1000 0000 0000 0000) of uint16 const signExclusionMask = 0x00007FFF; if ((aBitwise & signExclusionMask) === 0 &&
(bBitwise & signExclusionMask) === 0) { return0;
}
} elseif (dataType === 'int64' || dataType === 'uint64') {
aBitwise = BigInt(a);
bBitwise = BigInt(b);
} elseif (
dataType === 'int8' || dataType === 'uint8' || dataType === 'int32' ||
dataType === 'uint32' || dataType === 'int4' || dataType === 'uint4') {
aBitwise = a;
bBitwise = b;
} else { thrownew AssertionError(`Data type ${dataType} is not supported`);
} const distance = aBitwise - bBitwise; return distance >= 0 ? distance : -distance;
};
for (let operandName in actual) { const expectedSuboutput = expectedOutputs[operandName]; const expectedDescriptor = expectedSuboutput.descriptor;
let expectedData = expectedSuboutput.data;
outputData = actual[operandName]; // If data is scalar and shape is not, it means it's expecting to be // filled by the scalar value. Also limit the array size so it doesn't // timeout. if (typeof (expectedData) === 'number' && expectedDescriptor.shape &&
sizeOfShape(expectedDescriptor.shape) > 1) { const size = Math.min(
kMaximumIndexToValidate, sizeOfShape(expectedDescriptor.shape));
expectedData = new Array(size).fill(expectedData);
outputData = outputData.subarray(0, kMaximumIndexToValidate);
} elseif (
expectedDescriptor.dataType === 'uint4' ||
expectedDescriptor.dataType === 'int4') { // The int4/uint4 data were packed in Uint8Array. // The first nybble and later nybble of one int8/uint8 value store two // consecutive 4-bits values separately. After unpacking each 4-bits // value, the unpacked int4 value is stored in an element of // Int8Array, and the unpacked uint4 value is stored in an element of // Uint8Array. For example: an array of uint4: // size = [1, 5] // Uint8Array.of(0x21, 0x43, 0x65, 0x87, 0xA9) // Would yield 5 * 2 uint4 data: // Uint8Array.of(1,2,3,4,5,6,7,8,9,10); // Another example: an array of int4: // size = [1, 5] // Uint8Array.of(0xA9, 0xCB, 0xED, 0x0F, 0x21) // Would yield 5 * 2 int4 data: // Int8Array.of(-7, -6, -5, -4, -3, -2, -1, 0, 1, 2);
let newOutputData; if (expectedDescriptor.dataType === 'uint4') {
newOutputData = new Uint8Array(sizeOfShape(expectedDescriptor.shape));
} else {
newOutputData = new Int8Array(sizeOfShape(expectedDescriptor.shape));
} const signMask =
(expectedDescriptor.dataType === 'int4') ? 0x08 : 0x00; for (let i = 0; i < sizeOfShape(expectedDescriptor.shape); i++) { const byteIndex = Math.floor(i / 2);
let value = (outputData[byteIndex] >> ((i & 1) << 2)) & 0xF; // Handle the negative numbers. if (value & signMask) {
value |= 0xF0;
}
newOutputData[i] = value;
}
outputData = newOutputData;
}
doAssert(
operatorName, outputData, expectedData, metricType, toleranceValue,
expectedDescriptor.dataType);
}
};
// If input data type is not supported on current platform, attempt to use // a supported type to pass the data, then cast back to original type. if (!supportedDataTypes.includes(dataType)) { const compatibleType = findCompatibleType(
dataType, supportedDataTypes, context.opSupportLimits().cast); if (compatibleType) {
descriptor.castedType = compatibleType;
descriptor.dataType = compatibleType;
}
}
function getInputName(operatorArguments, operandName) { for (let argument of operatorArguments) { const name = Object.keys(argument)[0]; if (name === operandName) { return argument[operandName];
} elseif (name === 'options') { if (Object.keys(argument[name]).includes(operandName)) { return argument[name][operandName];
}
}
} returnnull;
}
// This assert() function is to check whether configurations of test case are // set correctly. functionassert(condition, message) { if (!condition) { thrownew Error(`Wrong test case, ${message}`);
}
}
function validateInputOrConstantDataTypeAndRank(
inputName, operatorSupportLimits, operand) { const inputDescriptor = graph.inputs[inputName].descriptor; const inputDataType = inputDescriptor.dataType; const inputRank = inputDescriptor.shape.length; if (inputDescriptor.constant) { // Check graph constant data type if (!constantDataTypes.includes(inputDataType) &&
!findCompatibleType(
inputDataType, constantDataTypes, castOpSupportLimits)) { thrownew TypeError(
`Unsupported data type, constant '${operand}' data type ${
inputDataType} must be one of [${constantDataTypes}].`);
}
// Check graph constant rank if (inputRank < constantRankRange.min) { thrownew TypeError(`Unsupported rank ${inputRank} for constant '${
operand}' (must be at least ${constantRankRange.min}).`);
} if (inputRank > constantRankRange.max) { thrownew TypeError(`Unsupported rank ${inputRank} for constant '${
operand}' (must be at most ${constantRankRange.max}).`);
}
} else { // Check graph input data type if (!inputDataTypes.includes(inputDataType) &&
!findCompatibleType(
inputDataType, inputDataTypes, castOpSupportLimits)) { thrownew TypeError(
`Unsupported data type, input '${operand}' data type ${
inputDataType} must be one of [${inputDataTypes}].`);
}
// Check graph input rank if (inputRank < inputRankRange.min) { thrownew TypeError(`Unsupported rank ${inputRank} for input '${
operand}' (must be at least ${inputRankRange.min}).`);
} if (inputRank > inputRankRange.max) { thrownew TypeError(`Unsupported rank ${inputRank} for input '${
operand}' (must be at most ${inputRankRange.max}).`);
}
}
const operandSupportLimits = operatorSupportLimits[operand]; // Check operand data type const inputOperandDataTypes = operandSupportLimits.dataTypes; if (!inputOperandDataTypes.includes(inputDataType) &&
!findCompatibleType(
inputDataType, inputDataTypes, castOpSupportLimits)) { thrownew TypeError(
`Unsupported data type, input '${operand}' data type ${
inputDataType} must be one of [${inputOperandDataTypes}].`);
}
// Check operand rank const limitsRankRange = operandSupportLimits.rankRange; if (inputRank < limitsRankRange.min) { thrownew TypeError(`Unsupported rank ${inputRank} for argument ${
operand} (must be at least ${limitsRankRange.min}).`);
}
if (inputRank > limitsRankRange.max) { thrownew TypeError(`Unsupported rank ${inputRank} for argument ${
operand} (must be at most ${limitsRankRange.max}).`);
}
}
function validateOutputDataTypeAndRank(
outputName, operatorSupportLimits, operand) { const outputDataType =
graph.expectedOutputs[outputName].descriptor.dataType; const outputRank =
graph.expectedOutputs[outputName].descriptor.shape.length; // Check graph output data type if (!outputDataTypes.includes(outputDataType) &&
!findCompatibleType(
outputDataType, outputDataTypes, castOpSupportLimits)) { thrownew TypeError(
`Unsupported data type, output '${operand}' data type ${
outputDataType} must be one of [${outputDataTypes}].`);
}
// Check graph output rank if (outputRank < outputRankRange.min) { thrownew TypeError(`Unsupported rank ${outputRank} for output '${
operand}' (must be at least ${outputRankRange.min}).`);
} if (outputRank > outputRankRange.max) { thrownew TypeError(`Unsupported rank ${outputRank} for output '${
operand}' (must be at most ${outputRankRange.max}).`);
}
// Check output operand data type const outputOperandDataTypes = operatorSupportLimits[operand].dataTypes; if (!outputOperandDataTypes.includes(outputDataType) &&
!findCompatibleType(
outputOperandDataTypes, outputDataTypes, castOpSupportLimits)) { thrownew TypeError(
`Unsupported data type, output '${operand}' data type ${
outputDataType} must be one of [${outputOperandDataTypes}].`);
}
// Check output operand rank const outputOperandRankRange = operatorSupportLimits[operand].rankRange; if (outputRank < outputOperandRankRange.min) { thrownew TypeError(`Unsupported rank ${outputRank} for output '${
operand}' (must be at least ${outputOperandRankRange.min}).`);
} if (outputRank > outputOperandRankRange.max) { thrownew TypeError(`Unsupported rank ${outputRank} for output '${
operand}' (must be at most ${outputOperandRankRange.max}).`);
}
}
try { for (let operator of graph.operators) { const operatorName = operator.name; const operatorSupportLimits = supportLimits[operatorName]; for (let operand of Object.keys(operatorSupportLimits)) { if (operand === 'output') { // single output operand assert( typeof operator.outputs === 'string',
`the outputs of ${operatorName} should be a string.`); if (!graph.expectedOutputs[operator.outputs]) { // intermediate output continue;
}
validateOutputDataTypeAndRank(
operator.outputs, operatorSupportLimits, 'output');
} elseif (operand === 'outputs') { // multiple output operands of split operator assert(
Array.isArray(operator.outputs),
`the outputs of ${operatorName} should be a string array.`); for (const outputName of operator.outputs) { assert( typeof outputName === 'string',
`the outputs' item of ${operatorName} should be a string.`); if (!graph.expectedOutputs[outputName]) { // intermediate output continue;
}
validateOutputDataTypeAndRank(
outputName, operatorSupportLimits, 'outputs');
}
} elseif (/output[0-2]/.test(operand)) { // multiple output operands of gru/lstm/lstmCell operators assert(
Array.isArray(operator.outputs),
`the outputs of ${operatorName} should be a string array.`); const index = parseInt(operand.match(/output([0-2])/)[1]); if (index < operator.outputs.length) {
validateOutputDataTypeAndRank(
operator.outputs[index], operatorSupportLimits, operand);
}
} else { // input operand(s) if (operatorName === 'concat') { const inputNameArray = operator.arguments[0][operand]; assert(
Array.isArray(inputNameArray),
`the inputs of ${operatorName} should be a string array.`); for (const inputName of inputNameArray) { assert( typeof inputName === 'string',
`the inputs' item of ${operatorName} should be a string.`); if (!graph.inputs[inputName]) { // intermediate input continue;
}
validateInputOrConstantDataTypeAndRank(
inputName, operatorSupportLimits, 'inputs');
}
} else { const inputName = getInputName(operator.arguments, operand); if (inputName === null || !graph.inputs[inputName]) { // default options argument or intermediate input continue;
}
validateInputOrConstantDataTypeAndRank(
inputName, operatorSupportLimits, operand);
}
}
}
} return/*supported*/ true;
} catch (error) { return/*not supported*/ false;
}
}
// Compile the constructed graph. const graph = await builder.build(namedOutputOperand);
// Execute the compiled graph. const result = await computeGraph(
context, graph, graphInputs, graphResources.expectedOutputs);
return {result, intermediateOperands};
};
const getGemmPrecisionTolerance =
(op, graphResources, intermediateOperands) => { // 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 {inputs} = graphResources; const args = op.arguments;
let ShapeA; const indexA = args[0][Object.keys(args[0])[0]]; if (inputs[indexA]) {
ShapeA = inputs[indexA].descriptor.shape;
} else {
ShapeA = intermediateOperands[indexA].shape;
} const options =
args.length === 3 ? {...args[2][Object.keys(args[2])[0]]} : {}; 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++;
}
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];
}
}
function isMinimumTest(test) {
let isMinimum = false; const graphResources = test.graph; const inputsResources = graphResources.inputs;
// check inputs for (let operator of graphResources.operators) { const minimumLimits = minimumDataTypeSet[operator.name]; for (let argument of operator.arguments) { for (let [operandName, value] of Object.entries(argument)) { if (operandName !== 'options') { if (typeof value === 'string' &&
inputsResources.hasOwnProperty(value)) {
isMinimum = checkMinimum(
inputsResources[value].descriptor, minimumLimits[operandName]); if (!isMinimum) { return isMinimum;
}
} elseif (Array.isArray(value)) { for (let subValue of value) { if (typeof subValue === 'string' &&
inputsResources.hasOwnProperty(subValue)) {
isMinimum = checkMinimum(
inputsResources[subValue].descriptor,
minimumLimits[operandName]); if (!isMinimum) { return isMinimum;
}
}
}
}
} else { for (let [optionOperandName, optionValue] of Object.entries(
argument['options'])) { if (typeof value === 'string' &&
inputsResources.hasOwnProperty(optionValue)) {
isMinimum = checkMinimum(
inputsResources[optionValue].descriptor,
minimumLimits[optionOperandName]); if (!isMinimum) { return isMinimum;
}
}
}
}
}
}
}
// check outputs const outputsResources = graphResources.expectedOutputs; for (let [outputOperandName, value] of Object.entries(outputsResources)) { const outputMinimumLimits =
getOutputMinimumLimits(graphResources.operators, outputOperandName)
isMinimum = checkMinimum(value.descriptor, outputMinimumLimits); if (!isMinimum) { return isMinimum;
}
}
return isMinimum;
}
// This array is to save skipped tests which are optional tests unsupported by // the context. It's helpful to debug to get detail skipped tests in browser // console by typing testsToSkip after running tests. const testsToSkip = [];
async function webnn_conformance_test(
tests, buildAndExecuteGraphFunc, toleranceFunc) { if (navigator.ml === undefined) {
test(() => assert_implements(navigator.ml, 'missing navigator.ml'));
} else { const testsToRun = [];
promise_setup(async () => { // Create a context for checking whether tests are supported. const context = await getContext();
minimumDataTypeSet = await getMinimumDataTypeSetJson();
tests.filter(isTargetTest).forEach((test) => { if (validateContextSupportsGraph(context, test.graph) ||
isMinimumTest(test)) {
testsToRun.push(test);
} else { // This test is optional so it can be skipped.
testsToSkip.push(test);
}
});
});
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.