// Proposal to add float16 TypedArrays to JavaScript. // URL: https://tc39.es/proposal-float16array/ // Use workaround Uint16 for Float16
float16: Uint16Array,
/* 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;
};
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') { 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;
};
/** * Get bitwise of the given value. * @param {Number} value * @param {String} dataType - A data type string, like "float32", "float16", * more types, please see: * https://www.w3.org/TR/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 { thrownew 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://www.w3.org/TR/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);
} elseif (dataType === 'float16') {
actualBitwise = actual[i]; // convert expected data of Float16 to Uint16
expectedBitwise = toHalf(expected[i]);
} elseif (dataType === 'int64') {
actualBitwise = actual[i];
expectedBitwise = BigInt(expected[i]);
} elseif (dataType === 'uint64') {
actualBitwise = actual[i];
expectedBitwise = BigUint64Array(expected[i]);
} elseif (
dataType === 'int8' || dataType === 'uint8' || dataType === 'int32' ||
dataType === 'uint32' || dataType === 'int4' ||
dataType === 'uint4') {
actualBitwise = actual[i];
expectedBitwise = expected[i];
}
distance = actualBitwise - expectedBitwise;
distance = distance >= 0 ? distance : -distance;
// if true, invoke assert_true() in failure case // if false, it's expected, not invoke assert_true() in success case to // prevent spammy output if (distance > nulp) {
assert_true( false,
`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`);
}
}
}
};
/** * This function converts a Float16 stored as the bits of a Uint16 into a * JavaScript Number. * @param {Number} uint16 - a Float16 stored as the bits of a Uint16 * @returns An emulated Float16 number.
*/ function float16AsUint16ToNumber(uint16) { const sign = (uint16 >> 15) & 0x1; const exponent = (uint16 >> 10) & 0x1F; const mantissa = uint16 & 0x3FF;
let float16;
if (exponent === 0) { // Subnormal number
float16 = (mantissa / 1024) * Math.pow(2, -14);
} elseif (exponent === 0x1F) { // NaN or Infinity
float16 = mantissa ? NaN : Infinity;
} else { // Normalized number
float16 = (1 + mantissa / 1024) * Math.pow(2, exponent - 15);
}
/** * Assert actual results with expected results. * @param {String} operatorName * @param {(Number[]|Number)} actual * @param {(Number[]|Number)} expected * @param {String} metricType - Value: 'ULP', 'ATOL' * @param {Number} toleranceValue * @param {String} dataType - An operand type string, value: "float32", * more types, please see: * https://www.w3.org/TR/webnn/#enumdef-mloperanddatatype
*/ const doAssert =
(operatorName, actual, expected, metricType, toleranceValue, dataType) => { const description = `test ${operatorName} ${dataType}`; if (typeof expected === 'number') {
expected = [expected];
actual = [actual];
} if (metricType === 'ULP') {
assert_array_approx_equals_ulp(
actual, expected, toleranceValue, dataType, description);
} elseif (metricType === 'ATOL') {
let actualData; if (dataType === 'float16') { // workaround for float16
actualData = new Array(actual.length);
actual.forEach(
(x, index) => actualData[index] = float16AsUint16ToNumber(x));
} else {
actualData = actual;
}
assert_array_approx_equals(
actualData, expected, toleranceValue, description);
} else { thrownew AssertionError(
`Tolerance Metric type '${metricType}' is not supported`);
}
};
/** * Assert computed results be equal to expected data. * @param {Object} toleranceFunc * @param {Map<String, ArrayBufferView> | * Array[Map<String, ArrayBufferView>]} actual * @param {Object} graphResources - Resources used for building a graph
*/ const assertResultsEquals =
(toleranceFunc, actual, graphResources, intermediateOperands) => { const operatorName =
graphResources.operators.map(operator => operator.name).join(' '); const expectedOutputs = graphResources.expectedOutputs; const toleranceInfo = toleranceFunc(graphResources, intermediateOperands); const metricType = toleranceInfo.metricType; const toleranceValue = toleranceInfo.value;
let outputData;
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); 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 validateInputOrConstantDataType(
inputName, operatorSupportLimits, operand) { const inputDataType = graph.inputs[inputName].descriptor.dataType; if (graph.inputs[inputName].constant) { if (!constantDataTypes.includes(inputDataType)) { thrownew TypeError(
`Unsupported data type, constant '${operand}' data type ${
inputDataType} must be one of [${constantDataTypes}].`);
}
} else { if (!inputDataTypes.includes(inputDataType)) { thrownew TypeError(
`Unsupported data type, input '${operand}' data type ${
inputDataType} must be one of [${inputDataTypes}].`);
}
}
if (!operatorSupportLimits[operand].dataTypes.includes(inputDataType)) { thrownew TypeError(`Unsupported data type, input '${
operand}' data type ${inputDataType} must be one of [${
operatorSupportLimits[operand].dataTypes}].`);
}
}
function validateOutputDataType(outputName, operatorSupportLimits, operand) { const outputDataType =
graph.expectedOutputs[outputName].descriptor.dataType; if (!outputDataTypes.includes(outputDataType)) { thrownew TypeError(
`Unsupported data type, output '${operand}' data type ${
outputDataType} must be one of [${outputDataTypes}].`);
}
if (!operatorSupportLimits[operand].dataTypes.includes(outputDataType)) { thrownew TypeError(`Unsupported data type, output '${
operand}' data type ${outputDataType} must be one of [${
operatorSupportLimits[operand].dataTypes}].`);
}
}
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;
}
validateOutputDataType(
operator.outputs, operatorSupportLimits, 'output');
} elseif (operand === 'outputs') { // multiples output operands 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;
}
validateOutputDataType(outputName, operatorSupportLimits, 'outputs');
}
} 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.`);
validateInputOrConstantDataType(
inputName, operatorSupportLimits, 'inputs');
}
} else { const inputName = getInputName(operator.arguments, operand); if (inputName === null || !graph.inputs[inputName]) { // default options argument or intermediate input continue;
}
validateInputOrConstantDataType(
inputName, operatorSupportLimits, operand);
}
}
}
}
}
/** * This function is to execute the compiled graph. * @param {MLContext} context * @param {MLGraph} graph * @param {Map<String, { * data: Array.<Number>|Number, * descriptor: MLOperandDescriptor, * constant?: Boolean * }>} graphInputs * @param {Map<String, { * data: Array.<Number>|Number, * descriptor: MLOperandDescriptor, * }>} expectedOutputs * @returns A result object.
*/
async function computeGraph(context, graph, graphInputs, expectedOutputs) { const inputs = await prepareInputsForGraph(context, graphInputs); const outputs = await prepareOutputsForGraph(context, expectedOutputs);
// Execute the compiled graph.
context.dispatch(graph, inputs, outputs);
// 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++;
}
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.