From a20c0a3a504188a42a235e5c1a9f7874230ec5cd Mon Sep 17 00:00:00 2001 From: Ryan Harrison Date: Thu, 8 Sep 2022 15:13:59 -0400 Subject: [PATCH] Implement `quantizeToF16` f32 tests (#1828) Adds tests for quantizeToF16 and related infrastructure Implements f16 support in the framework as needed for quantizeToF16 Adds petamoriken/float16 v3.6.6 Upstream https://github.com/petamoriken/float16 This adds native-like float16 support for implementing quantizeToF16 tests, and future f16 work. Fixes #1812 Fixes #1200 --- .eslintignore | 1 + .eslintrc.json | 2 +- Gruntfile.js | 4 +- src/common/tools/setup-ts-in-node.js | 42 +- src/common/util/util.ts | 2 + src/external/README.md | 31 + src/external/petamoriken/float16/LICENSE.txt | 21 + src/external/petamoriken/float16/float16.d.ts | 471 +++++++ src/external/petamoriken/float16/float16.js | 1228 +++++++++++++++++ src/unittests/f32_interval.spec.ts | 36 + src/unittests/maths.spec.ts | 44 +- .../expression/binary/f32_logical.spec.ts | 6 +- .../call/builtin/quantizeToF16.spec.ts | 28 +- src/webgpu/util/constants.ts | 82 +- src/webgpu/util/conversion.ts | 36 +- src/webgpu/util/f32_interval.ts | 45 +- src/webgpu/util/math.ts | 232 +++- tsconfig.json | 7 +- 18 files changed, 2246 insertions(+), 72 deletions(-) create mode 100644 .eslintignore create mode 100644 src/external/README.md create mode 100644 src/external/petamoriken/float16/LICENSE.txt create mode 100644 src/external/petamoriken/float16/float16.d.ts create mode 100644 src/external/petamoriken/float16/float16.js diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000000..a4a42b126672 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +/src/external/* diff --git a/.eslintrc.json b/.eslintrc.json index aa6803ff41b1..f651148bee5f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -112,7 +112,7 @@ { "target": "./src/common", "from": "./src", - "except": ["./common"], + "except": ["./common", "./external"], "message": "Non common/ code imported from common/" } ] diff --git a/Gruntfile.js b/Gruntfile.js index fe32339aa14d..e73c974c9470 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -32,7 +32,7 @@ module.exports = function (grunt) { cmd: 'node', args: [ 'node_modules/@babel/cli/bin/babel', - '--extensions=.ts', + '--extensions=.ts,.js', '--source-maps=true', '--out-dir=out/', 'src/', @@ -42,7 +42,7 @@ module.exports = function (grunt) { cmd: 'node', args: [ 'node_modules/@babel/cli/bin/babel', - '--extensions=.ts', + '--extensions=.ts,.js', '--source-maps=false', '--delete-dir-on-start', '--out-dir=out-wpt/', diff --git a/src/common/tools/setup-ts-in-node.js b/src/common/tools/setup-ts-in-node.js index 9a10d23f3186..89e91e8c9df3 100644 --- a/src/common/tools/setup-ts-in-node.js +++ b/src/common/tools/setup-ts-in-node.js @@ -14,25 +14,35 @@ const Module = require('module'); // Redirect imports of .js files to .ts files const resolveFilename = Module._resolveFilename; Module._resolveFilename = (request, parentModule, isMain) => { - if (request.startsWith('.') && parentModule.filename.endsWith('.ts')) { - // Required for browser (because it needs the actual correct file path and - // can't do any kind of file resolution). - if (request.endsWith('/index.js')) { - throw new Error( - "Avoid the name `index.js`; we don't have Node-style path resolution: " + request - ); - } + do { + if (request.startsWith('.') && parentModule.filename.endsWith('.ts')) { + // Required for browser (because it needs the actual correct file path and + // can't do any kind of file resolution). + if (request.endsWith('/index.js')) { + throw new Error( + "Avoid the name `index.js`; we don't have Node-style path resolution: " + request + ); + } - // Import of Node addon modules are valid and should pass through. - if (request.endsWith('.node')) { - return resolveFilename.call(this, request, parentModule, isMain); - } + // Import of Node addon modules are valid and should pass through. + if (request.endsWith('.node')) { + break; + } - if (!request.endsWith('.js')) { - throw new Error('All relative imports must end in .js: ' + request); + if (!request.endsWith('.js')) { + throw new Error('All relative imports must end in .js: ' + request); + } + + try { + const tsRequest = request.substring(0, request.length - '.js'.length) + '.ts'; + return resolveFilename.call(this, tsRequest, parentModule, isMain); + } catch (ex) { + // If the .ts file doesn't exist, try .js instead. + break; + } } - request = request.substring(0, request.length - '.js'.length) + '.ts'; - } + } while (0); + return resolveFilename.call(this, request, parentModule, isMain); }; diff --git a/src/common/util/util.ts b/src/common/util/util.ts index f61b7013fcd4..f7a9ec7c5ea7 100644 --- a/src/common/util/util.ts +++ b/src/common/util/util.ts @@ -1,3 +1,4 @@ +import { Float16Array } from '../../external/petamoriken/float16/float16.js'; import { Logger } from '../internal/logging/logger.js'; import { keysOf } from './data_tables.js'; @@ -195,6 +196,7 @@ const TypedArrayBufferViewInstances = [ new Int8Array(), new Int16Array(), new Int32Array(), + new Float16Array(), new Float32Array(), new Float64Array(), ] as const; diff --git a/src/external/README.md b/src/external/README.md new file mode 100644 index 000000000000..84fbf9c732c9 --- /dev/null +++ b/src/external/README.md @@ -0,0 +1,31 @@ +# External Modules + +This directory contains external modules that are used by the WebGPU +CTS. These are included in the repo, as opposed to being fetched via a +package manager or CDN, so that there is a single canonical source of +truth for the CTS tests and the CTS tests can be run as a standalone +suite without needing to pull from a CDN or similar process. + +## Adding modules + +Each module that is added should be done consciously with a clear +reasoning on what the module is providing, since the bar for adding +new modules should be relatively high. + +The module will need to be licensed via a compatible license to the +BSD-3 clause & W3C CTS licenses that the CTS currently is covered by. + +It is preferred to use a single source build of the module if possible. + +In addition to the source for the module a LICENSE file should be +included in the directory clearly identifying the owner of the module +and the license it is covered by. + +Details of the specific module, including version, origin and purpose +should be listed below. + +## Current Modules + +| **Name** | **Origin** | **License** | **Version** | **Purpose** | +|----------------------|--------------------------------------------------|-------------|-------------|------------------------------------------------| +| petamoriken/float16 | [github](https://github.com/petamoriken/float16) | MIT | 3.6.6 | Fluent support for f16 numbers via TypedArrays | diff --git a/src/external/petamoriken/float16/LICENSE.txt b/src/external/petamoriken/float16/LICENSE.txt new file mode 100644 index 000000000000..e8eacf4e7f13 --- /dev/null +++ b/src/external/petamoriken/float16/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017-2021 Kenta Moriuchi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/external/petamoriken/float16/float16.d.ts b/src/external/petamoriken/float16/float16.d.ts new file mode 100644 index 000000000000..c9d66ab7cab8 --- /dev/null +++ b/src/external/petamoriken/float16/float16.d.ts @@ -0,0 +1,471 @@ +/** + * A typed array of 16-bit float values. The contents are initialized to 0. If the requested number + * of bytes could not be allocated an exception is raised. + */ +export interface Float16Array { + /** + * The size in bytes of each element in the array. + */ + readonly BYTES_PER_ELEMENT: number; + + /** + * The ArrayBuffer instance referenced by the array. + */ + readonly buffer: ArrayBufferLike; + + /** + * The length in bytes of the array. + */ + readonly byteLength: number; + + /** + * The offset in bytes of the array. + */ + readonly byteOffset: number; + + [Symbol.iterator](): IterableIterator; + + /** + * Returns an array of key, value pairs for every entry in the array + */ + entries(): IterableIterator<[number, number]>; + + /** + * Returns an list of keys in the array + */ + keys(): IterableIterator; + + /** + * Returns an list of values in the array + */ + values(): IterableIterator; + + /** + * Returns the item located at the specified index. + * @param index The zero-based index of the desired code unit. A negative index will count back from the last item. + */ + at(index: number): number | undefined; + + /** + * Returns the this object after copying a section of the array identified by start and end + * to the same array starting at position target + * @param target If target is negative, it is treated as length+target where length is the + * length of the array. + * @param start If start is negative, it is treated as length+start. If end is negative, it + * is treated as length+end. + * @param end If not specified, length of the this object is used as its default value. + */ + copyWithin(target: number, start: number, end?: number): this; + + /** + * Determines whether all the members of an array satisfy the specified test. + * @param callbackfn A function that accepts up to three arguments. The every method calls + * the callbackfn function for each element in the array until the callbackfn returns a value + * which is coercible to the Boolean value false, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + every( + callbackfn: (value: number, index: number, array: Float16Array) => unknown, + thisArg?: any, + ): boolean; + + /** + * Returns the this object after filling the section identified by start and end with value + * @param value value to fill array section with + * @param start index to start filling the array at. If start is negative, it is treated as + * length+start where length is the length of the array. + * @param end index to stop filling the array at. If end is negative, it is treated as + * length+end. + */ + fill(value: number, start?: number, end?: number): this; + + /** + * Returns the elements of an array that meet the condition specified in a callback function. + * @param predicate A function that accepts up to three arguments. The filter method calls + * the predicate function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the predicate function. + * If thisArg is omitted, undefined is used as the this value. + */ + filter( + predicate: (value: number, index: number, array: Float16Array) => any, + thisArg?: any, + ): Float16Array; + + /** + * Returns the value of the first element in the array where predicate is true, and undefined + * otherwise. + * @param predicate find calls predicate once for each element of the array, in ascending + * order, until it finds one where predicate returns true. If such an element is found, find + * immediately returns that element value. Otherwise, find returns undefined. + * @param thisArg If provided, it will be used as the this value for each invocation of + * predicate. If it is not provided, undefined is used instead. + */ + find( + predicate: (value: number, index: number, obj: Float16Array) => boolean, + thisArg?: any, + ): number | undefined; + + /** + * Returns the index of the first element in the array where predicate is true, and -1 + * otherwise. + * @param predicate find calls predicate once for each element of the array, in ascending + * order, until it finds one where predicate returns true. If such an element is found, + * findIndex immediately returns that element index. Otherwise, findIndex returns -1. + * @param thisArg If provided, it will be used as the this value for each invocation of + * predicate. If it is not provided, undefined is used instead. + */ + findIndex( + predicate: (value: number, index: number, obj: Float16Array) => boolean, + thisArg?: any, + ): number; + + /** + * Returns the value of the last element in the array where predicate is true, and undefined + * otherwise. + * @param predicate find calls predicate once for each element of the array, in descending + * order, until it finds one where predicate returns true. If such an element is found, findLast + * immediately returns that element value. Otherwise, findLast returns undefined. + * @param thisArg If provided, it will be used as the this value for each invocation of + * predicate. If it is not provided, undefined is used instead. + */ + findLast( + predicate: (value: number, index: number, obj: Float16Array) => boolean, + thisArg?: any, + ): number | undefined; + + /** + * Returns the index of the last element in the array where predicate is true, and -1 + * otherwise. + * @param predicate find calls predicate once for each element of the array, in descending + * order, until it finds one where predicate returns true. If such an element is found, + * findLastIndex immediately returns that element index. Otherwise, findLastIndex returns -1. + * @param thisArg If provided, it will be used as the this value for each invocation of + * predicate. If it is not provided, undefined is used instead. + */ + findLastIndex( + predicate: (value: number, index: number, obj: Float16Array) => boolean, + thisArg?: any, + ): number; + + /** + * Performs the specified action for each element in an array. + * @param callbackfn A function that accepts up to three arguments. forEach calls the + * callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + forEach( + callbackfn: (value: number, index: number, array: Float16Array) => void, + thisArg?: any, + ): void; + + /** + * Determines whether an array includes a certain element, returning true or false as appropriate. + * @param searchElement The element to search for. + * @param fromIndex The position in this array at which to begin searching for searchElement. + */ + includes(searchElement: number, fromIndex?: number): boolean; + + /** + * Returns the index of the first occurrence of a value in an array. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the + * search starts at index 0. + */ + indexOf(searchElement: number, fromIndex?: number): number; + + /** + * Adds all the elements of an array separated by the specified separator string. + * @param separator A string used to separate one element of an array from the next in the + * resulting String. If omitted, the array elements are separated with a comma. + */ + join(separator?: string): string; + + /** + * Returns the index of the last occurrence of a value in an array. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the + * search starts at index 0. + */ + lastIndexOf(searchElement: number, fromIndex?: number): number; + + /** + * The length of the array. + */ + readonly length: number; + + /** + * Calls a defined callback function on each element of an array, and returns an array that + * contains the results. + * @param callbackfn A function that accepts up to three arguments. The map method calls the + * callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + map( + callbackfn: (value: number, index: number, array: Float16Array) => number, + thisArg?: any, + ): Float16Array; + + /** + * Calls the specified callback function for all the elements in an array. The return value of + * the callback function is the accumulated result, and is provided as an argument in the next + * call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduce method calls the + * callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an argument + * instead of an array value. + */ + reduce( + callbackfn: ( + previousValue: number, + currentValue: number, + currentIndex: number, + array: Float16Array, + ) => number, + ): number; + reduce( + callbackfn: ( + previousValue: number, + currentValue: number, + currentIndex: number, + array: Float16Array, + ) => number, + initialValue: number, + ): number; + reduce( + callbackfn: ( + previousValue: U, + currentValue: number, + currentIndex: number, + array: Float16Array, + ) => U, + initialValue: U, + ): U; + + /** + * Calls the specified callback function for all the elements in an array, in descending order. + * The return value of the callback function is the accumulated result, and is provided as an + * argument in the next call to the callback function. + * @param callbackfn A function that accepts up to four arguments. The reduceRight method calls + * the callbackfn function one time for each element in the array. + * @param initialValue If initialValue is specified, it is used as the initial value to start + * the accumulation. The first call to the callbackfn function provides this value as an + * argument instead of an array value. + */ + reduceRight( + callbackfn: ( + previousValue: number, + currentValue: number, + currentIndex: number, + array: Float16Array, + ) => number, + ): number; + reduceRight( + callbackfn: ( + previousValue: number, + currentValue: number, + currentIndex: number, + array: Float16Array, + ) => number, + initialValue: number, + ): number; + reduceRight( + callbackfn: ( + previousValue: U, + currentValue: number, + currentIndex: number, + array: Float16Array, + ) => U, + initialValue: U, + ): U; + + /** + * Reverses the elements in an Array. + */ + reverse(): this; + + /** + * Sets a value or an array of values. + * @param array A typed or untyped array of values to set. + * @param offset The index in the current array at which the values are to be written. + */ + set(array: ArrayLike, offset?: number): void; + + /** + * Returns a section of an array. + * @param start The beginning of the specified portion of the array. + * @param end The end of the specified portion of the array. This is exclusive of the element at the index 'end'. + */ + slice(start?: number, end?: number): Float16Array; + + /** + * Determines whether the specified callback function returns true for any element of an array. + * @param callbackfn A function that accepts up to three arguments. The some method calls + * the callbackfn function for each element in the array until the callbackfn returns a value + * which is coercible to the Boolean value true, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + some( + callbackfn: (value: number, index: number, array: Float16Array) => unknown, + thisArg?: any, + ): boolean; + + /** + * Sorts an array. + * @param compareFn Function used to determine the order of the elements. It is expected to return + * a negative value if first argument is less than second argument, zero if they're equal and a positive + * value otherwise. If omitted, the elements are sorted in ascending. + */ + sort(compareFn?: (a: number, b: number) => number): this; + + /** + * Gets a new Float16Array view of the ArrayBuffer store for this array, referencing the elements + * at begin, inclusive, up to end, exclusive. + * @param begin The index of the beginning of the array. + * @param end The index of the end of the array. + */ + subarray(begin?: number, end?: number): Float16Array; + + /** + * Converts a number to a string by using the current locale. + */ + toLocaleString(): string; + + /** + * Returns a string representation of an array. + */ + toString(): string; + + /** + * Returns the primitive value of the specified object. + */ + valueOf(): Float16Array; + + readonly [Symbol.toStringTag]: "Float16Array"; + + [index: number]: number; +} + +export interface Float16ArrayConstructor { + readonly prototype: Float16Array; + new (): Float16Array; + new (length: number): Float16Array; + new (elements: Iterable): Float16Array; + new (array: ArrayLike | ArrayBufferLike): Float16Array; + new ( + buffer: ArrayBufferLike, + byteOffset: number, + length?: number, + ): Float16Array; + + /** + * The size in bytes of each element in the array. + */ + readonly BYTES_PER_ELEMENT: number; + + /** + * Returns a new array from a set of elements. + * @param items A set of elements to include in the new array object. + */ + of(...items: number[]): Float16Array; + + /** + * Creates an array from an array-like or iterable object. + * @param elements An iterable object to convert to an array. + */ + from(elements: Iterable): Float16Array; + + /** + * Creates an array from an array-like or iterable object. + * @param elements An iterable object to convert to an array. + * @param mapfn A mapping function to call on every element of the array. + * @param thisArg Value of 'this' used to invoke the mapfn. + */ + from( + elements: Iterable, + mapfn: (v: T, k: number) => number, + thisArg?: any, + ): Float16Array; + + /** + * Creates an array from an array-like or iterable object. + * @param arrayLike An array-like object to convert to an array. + */ + from(arrayLike: ArrayLike): Float16Array; + + /** + * Creates an array from an array-like or iterable object. + * @param arrayLike An array-like object to convert to an array. + * @param mapfn A mapping function to call on every element of the array. + * @param thisArg Value of 'this' used to invoke the mapfn. + */ + from( + arrayLike: ArrayLike, + mapfn: (v: T, k: number) => number, + thisArg?: any, + ): Float16Array; +} +export declare const Float16Array: Float16ArrayConstructor; + +/** + * Returns `true` if the value is a Float16Array instance. + * @since v3.4.0 + */ +export declare function isFloat16Array(value: unknown): value is Float16Array; + +/** + * Returns `true` if the value is a type of TypedArray instance that contains Float16Array. + * @since v3.6.0 + */ +export declare function isTypedArray( + value: unknown, +): value is + | Uint8Array + | Uint8ClampedArray + | Uint16Array + | Uint32Array + | Int8Array + | Int16Array + | Int32Array + | Float16Array + | Float32Array + | Float64Array + | BigUint64Array + | BigInt64Array; + +/** + * Gets the Float16 value at the specified byte offset from the start of the view. There is + * no alignment constraint; multi-byte values may be fetched from any offset. + * @param byteOffset The place in the buffer at which the value should be retrieved. + * @param littleEndian If false or undefined, a big-endian value should be read, + * otherwise a little-endian value should be read. + */ +export declare function getFloat16( + dataView: DataView, + byteOffset: number, + littleEndian?: boolean, +): number; + +/** + * Stores an Float16 value at the specified byte offset from the start of the view. + * @param byteOffset The place in the buffer at which the value should be set. + * @param value The value to set. + * @param littleEndian If false or undefined, a big-endian value should be written, + * otherwise a little-endian value should be written. + */ +export declare function setFloat16( + dataView: DataView, + byteOffset: number, + value: number, + littleEndian?: boolean, +): void; + +/** + * Returns the nearest half-precision float representation of a number. + * @param x A numeric expression. + */ +export declare function hfround(x: number): number; diff --git a/src/external/petamoriken/float16/float16.js b/src/external/petamoriken/float16/float16.js new file mode 100644 index 000000000000..54843a4842b7 --- /dev/null +++ b/src/external/petamoriken/float16/float16.js @@ -0,0 +1,1228 @@ +/*! @petamoriken/float16 v3.6.6 | MIT License - https://github.com/petamoriken/float16 */ + +const THIS_IS_NOT_AN_OBJECT = "This is not an object"; +const THIS_IS_NOT_A_FLOAT16ARRAY_OBJECT = "This is not a Float16Array object"; +const THIS_CONSTRUCTOR_IS_NOT_A_SUBCLASS_OF_FLOAT16ARRAY = + "This constructor is not a subclass of Float16Array"; +const THE_CONSTRUCTOR_PROPERTY_VALUE_IS_NOT_AN_OBJECT = + "The constructor property value is not an object"; +const SPECIES_CONSTRUCTOR_DIDNT_RETURN_TYPEDARRAY_OBJECT = + "Species constructor didn't return TypedArray object"; +const DERIVED_CONSTRUCTOR_CREATED_TYPEDARRAY_OBJECT_WHICH_WAS_TOO_SMALL_LENGTH = + "Derived constructor created TypedArray object which was too small length"; +const ATTEMPTING_TO_ACCESS_DETACHED_ARRAYBUFFER = + "Attempting to access detached ArrayBuffer"; +const CANNOT_CONVERT_UNDEFINED_OR_NULL_TO_OBJECT = + "Cannot convert undefined or null to object"; +const CANNOT_MIX_BIGINT_AND_OTHER_TYPES = + "Cannot mix BigInt and other types, use explicit conversions"; +const ITERATOR_PROPERTY_IS_NOT_CALLABLE = "@@iterator property is not callable"; +const REDUCE_OF_EMPTY_ARRAY_WITH_NO_INITIAL_VALUE = + "Reduce of empty array with no initial value"; +const OFFSET_IS_OUT_OF_BOUNDS = "Offset is out of bounds"; + +function uncurryThis(target) { + return (thisArg, ...args) => { + return ReflectApply(target, thisArg, args); + }; +} +function uncurryThisGetter(target, key) { + return uncurryThis( + ReflectGetOwnPropertyDescriptor( + target, + key + ).get + ); +} +const { + apply: ReflectApply, + construct: ReflectConstruct, + defineProperty: ReflectDefineProperty, + get: ReflectGet, + getOwnPropertyDescriptor: ReflectGetOwnPropertyDescriptor, + getPrototypeOf: ReflectGetPrototypeOf, + has: ReflectHas, + ownKeys: ReflectOwnKeys, + set: ReflectSet, + setPrototypeOf: ReflectSetPrototypeOf, +} = Reflect; +const NativeProxy = Proxy; +const { + MAX_SAFE_INTEGER: MAX_SAFE_INTEGER, + isFinite: NumberIsFinite, + isNaN: NumberIsNaN, +} = Number; +const { + iterator: SymbolIterator, + species: SymbolSpecies, + toStringTag: SymbolToStringTag, + for: SymbolFor, +} = Symbol; +const NativeObject = Object; +const { + create: ObjectCreate, + defineProperty: ObjectDefineProperty, + freeze: ObjectFreeze, + is: ObjectIs, +} = NativeObject; +const ObjectPrototype = NativeObject.prototype; +const ObjectPrototype__lookupGetter__ = (ObjectPrototype).__lookupGetter__ + ? uncurryThis( (ObjectPrototype).__lookupGetter__) + : (object, key) => { + if (object == null) { + throw NativeTypeError( + CANNOT_CONVERT_UNDEFINED_OR_NULL_TO_OBJECT + ); + } + let target = NativeObject(object); + do { + const descriptor = ReflectGetOwnPropertyDescriptor(target, key); + if (descriptor !== undefined) { + if (ObjectHasOwn(descriptor, "get")) { + return descriptor.get; + } + return; + } + } while ((target = ReflectGetPrototypeOf(target)) !== null); + }; +const ObjectHasOwn = (NativeObject).hasOwn || + uncurryThis(ObjectPrototype.hasOwnProperty); +const NativeArray = Array; +const ArrayIsArray = NativeArray.isArray; +const ArrayPrototype = NativeArray.prototype; +const ArrayPrototypeJoin = uncurryThis(ArrayPrototype.join); +const ArrayPrototypePush = uncurryThis(ArrayPrototype.push); +const ArrayPrototypeToLocaleString = uncurryThis( + ArrayPrototype.toLocaleString +); +const NativeArrayPrototypeSymbolIterator = ArrayPrototype[SymbolIterator]; +const ArrayPrototypeSymbolIterator = uncurryThis(NativeArrayPrototypeSymbolIterator); +const MathTrunc = Math.trunc; +const NativeArrayBuffer = ArrayBuffer; +const ArrayBufferIsView = NativeArrayBuffer.isView; +const ArrayBufferPrototype = NativeArrayBuffer.prototype; +const ArrayBufferPrototypeSlice = uncurryThis(ArrayBufferPrototype.slice); +const ArrayBufferPrototypeGetByteLength = uncurryThisGetter(ArrayBufferPrototype, "byteLength"); +const NativeSharedArrayBuffer = typeof SharedArrayBuffer !== "undefined" ? SharedArrayBuffer : null; +const SharedArrayBufferPrototypeGetByteLength = NativeSharedArrayBuffer + && uncurryThisGetter(NativeSharedArrayBuffer.prototype, "byteLength"); +const TypedArray = ReflectGetPrototypeOf(Uint8Array); +const TypedArrayFrom = TypedArray.from; +const TypedArrayPrototype = TypedArray.prototype; +const NativeTypedArrayPrototypeSymbolIterator = TypedArrayPrototype[SymbolIterator]; +const TypedArrayPrototypeKeys = uncurryThis(TypedArrayPrototype.keys); +const TypedArrayPrototypeValues = uncurryThis( + TypedArrayPrototype.values +); +const TypedArrayPrototypeEntries = uncurryThis( + TypedArrayPrototype.entries +); +const TypedArrayPrototypeSet = uncurryThis(TypedArrayPrototype.set); +const TypedArrayPrototypeReverse = uncurryThis( + TypedArrayPrototype.reverse +); +const TypedArrayPrototypeFill = uncurryThis(TypedArrayPrototype.fill); +const TypedArrayPrototypeCopyWithin = uncurryThis( + TypedArrayPrototype.copyWithin +); +const TypedArrayPrototypeSort = uncurryThis(TypedArrayPrototype.sort); +const TypedArrayPrototypeSlice = uncurryThis(TypedArrayPrototype.slice); +const TypedArrayPrototypeSubarray = uncurryThis( + TypedArrayPrototype.subarray +); +const TypedArrayPrototypeGetBuffer = uncurryThisGetter( + TypedArrayPrototype, + "buffer" +); +const TypedArrayPrototypeGetByteOffset = uncurryThisGetter( + TypedArrayPrototype, + "byteOffset" +); +const TypedArrayPrototypeGetLength = uncurryThisGetter( + TypedArrayPrototype, + "length" +); +const TypedArrayPrototypeGetSymbolToStringTag = uncurryThisGetter( + TypedArrayPrototype, + SymbolToStringTag +); +const NativeUint16Array = Uint16Array; +const Uint16ArrayFrom = (...args) => { + return ReflectApply(TypedArrayFrom, NativeUint16Array, args); +}; +const NativeUint32Array = Uint32Array; +const NativeFloat32Array = Float32Array; +const ArrayIteratorPrototype = ReflectGetPrototypeOf([][SymbolIterator]()); +const ArrayIteratorPrototypeNext = uncurryThis(ArrayIteratorPrototype.next); +const GeneratorPrototypeNext = uncurryThis((function* () {})().next); +const IteratorPrototype = ReflectGetPrototypeOf(ArrayIteratorPrototype); +const DataViewPrototype = DataView.prototype; +const DataViewPrototypeGetUint16 = uncurryThis( + DataViewPrototype.getUint16 +); +const DataViewPrototypeSetUint16 = uncurryThis( + DataViewPrototype.setUint16 +); +const NativeTypeError = TypeError; +const NativeRangeError = RangeError; +const NativeWeakSet = WeakSet; +const WeakSetPrototype = NativeWeakSet.prototype; +const WeakSetPrototypeAdd = uncurryThis(WeakSetPrototype.add); +const WeakSetPrototypeHas = uncurryThis(WeakSetPrototype.has); +const NativeWeakMap = WeakMap; +const WeakMapPrototype = NativeWeakMap.prototype; +const WeakMapPrototypeGet = uncurryThis(WeakMapPrototype.get); +const WeakMapPrototypeHas = uncurryThis(WeakMapPrototype.has); +const WeakMapPrototypeSet = uncurryThis(WeakMapPrototype.set); + +const arrayIterators = new NativeWeakMap(); +const SafeIteratorPrototype = ObjectCreate(null, { + next: { + value: function next() { + const arrayIterator = WeakMapPrototypeGet(arrayIterators, this); + return ArrayIteratorPrototypeNext(arrayIterator); + }, + }, + [SymbolIterator]: { + value: function values() { + return this; + }, + }, +}); +function safeIfNeeded(array) { + if (array[SymbolIterator] === NativeArrayPrototypeSymbolIterator) { + return array; + } + const safe = ObjectCreate(SafeIteratorPrototype); + WeakMapPrototypeSet(arrayIterators, safe, ArrayPrototypeSymbolIterator(array)); + return safe; +} +const generators = new NativeWeakMap(); +const DummyArrayIteratorPrototype = ObjectCreate(IteratorPrototype, { + next: { + value: function next() { + const generator = WeakMapPrototypeGet(generators, this); + return GeneratorPrototypeNext(generator); + }, + writable: true, + configurable: true, + }, +}); +for (const key of ReflectOwnKeys(ArrayIteratorPrototype)) { + if (key === "next") { + continue; + } + ObjectDefineProperty(DummyArrayIteratorPrototype, key, ReflectGetOwnPropertyDescriptor(ArrayIteratorPrototype, key)); +} +function wrap(generator) { + const dummy = ObjectCreate(DummyArrayIteratorPrototype); + WeakMapPrototypeSet(generators, dummy, generator); + return dummy; +} + +function isObject(value) { + return (value !== null && typeof value === "object") || + typeof value === "function"; +} +function isObjectLike(value) { + return value !== null && typeof value === "object"; +} +function isNativeTypedArray(value) { + return TypedArrayPrototypeGetSymbolToStringTag(value) !== undefined; +} +function isNativeBigIntTypedArray(value) { + const typedArrayName = TypedArrayPrototypeGetSymbolToStringTag(value); + return typedArrayName === "BigInt64Array" || + typedArrayName === "BigUint64Array"; +} +function isArrayBuffer(value) { + try { + ArrayBufferPrototypeGetByteLength( (value)); + return true; + } catch (e) { + return false; + } +} +function isSharedArrayBuffer(value) { + if (NativeSharedArrayBuffer === null) { + return false; + } + try { + SharedArrayBufferPrototypeGetByteLength( (value)); + return true; + } catch (e) { + return false; + } +} +function isOrdinaryArray(value) { + if (!ArrayIsArray(value)) { + return false; + } + if (value[SymbolIterator] === NativeArrayPrototypeSymbolIterator) { + return true; + } + const iterator = value[SymbolIterator](); + return iterator[SymbolToStringTag] === "Array Iterator"; +} +function isOrdinaryNativeTypedArray(value) { + if (!isNativeTypedArray(value)) { + return false; + } + if (value[SymbolIterator] === NativeTypedArrayPrototypeSymbolIterator) { + return true; + } + const iterator = value[SymbolIterator](); + return iterator[SymbolToStringTag] === "Array Iterator"; +} +function isCanonicalIntegerIndexString(value) { + if (typeof value !== "string") { + return false; + } + const number = +value; + if (value !== number + "") { + return false; + } + if (!NumberIsFinite(number)) { + return false; + } + return number === MathTrunc(number); +} + +const brand = SymbolFor("__Float16Array__"); +function hasFloat16ArrayBrand(target) { + if (!isObjectLike(target)) { + return false; + } + const prototype = ReflectGetPrototypeOf(target); + if (!isObjectLike(prototype)) { + return false; + } + const constructor = prototype.constructor; + if (constructor === undefined) { + return false; + } + if (!isObject(constructor)) { + throw NativeTypeError(THE_CONSTRUCTOR_PROPERTY_VALUE_IS_NOT_AN_OBJECT); + } + return ReflectHas(constructor, brand); +} + +const buffer = new NativeArrayBuffer(4); +const floatView = new NativeFloat32Array(buffer); +const uint32View = new NativeUint32Array(buffer); +const baseTable = new NativeUint32Array(512); +const shiftTable = new NativeUint32Array(512); +for (let i = 0; i < 256; ++i) { + const e = i - 127; + if (e < -27) { + baseTable[i] = 0x0000; + baseTable[i | 0x100] = 0x8000; + shiftTable[i] = 24; + shiftTable[i | 0x100] = 24; + } else if (e < -14) { + baseTable[i] = 0x0400 >> (-e - 14); + baseTable[i | 0x100] = (0x0400 >> (-e - 14)) | 0x8000; + shiftTable[i] = -e - 1; + shiftTable[i | 0x100] = -e - 1; + } else if (e <= 15) { + baseTable[i] = (e + 15) << 10; + baseTable[i | 0x100] = ((e + 15) << 10) | 0x8000; + shiftTable[i] = 13; + shiftTable[i | 0x100] = 13; + } else if (e < 128) { + baseTable[i] = 0x7c00; + baseTable[i | 0x100] = 0xfc00; + shiftTable[i] = 24; + shiftTable[i | 0x100] = 24; + } else { + baseTable[i] = 0x7c00; + baseTable[i | 0x100] = 0xfc00; + shiftTable[i] = 13; + shiftTable[i | 0x100] = 13; + } +} +function roundToFloat16Bits(num) { + floatView[0] = (num); + const f = uint32View[0]; + const e = (f >> 23) & 0x1ff; + return baseTable[e] + ((f & 0x007fffff) >> shiftTable[e]); +} +const mantissaTable = new NativeUint32Array(2048); +const exponentTable = new NativeUint32Array(64); +const offsetTable = new NativeUint32Array(64); +for (let i = 1; i < 1024; ++i) { + let m = i << 13; + let e = 0; + while((m & 0x00800000) === 0) { + m <<= 1; + e -= 0x00800000; + } + m &= ~0x00800000; + e += 0x38800000; + mantissaTable[i] = m | e; +} +for (let i = 1024; i < 2048; ++i) { + mantissaTable[i] = 0x38000000 + ((i - 1024) << 13); +} +for (let i = 1; i < 31; ++i) { + exponentTable[i] = i << 23; +} +exponentTable[31] = 0x47800000; +exponentTable[32] = 0x80000000; +for (let i = 33; i < 63; ++i) { + exponentTable[i] = 0x80000000 + ((i - 32) << 23); +} +exponentTable[63] = 0xc7800000; +for (let i = 1; i < 64; ++i) { + if (i !== 32) { + offsetTable[i] = 1024; + } +} +function convertToNumber(float16bits) { + const m = float16bits >> 10; + uint32View[0] = mantissaTable[offsetTable[m] + (float16bits & 0x3ff)] + exponentTable[m]; + return floatView[0]; +} + +function ToIntegerOrInfinity(target) { + const number = +target; + if (NumberIsNaN(number) || number === 0) { + return 0; + } + return MathTrunc(number); +} +function ToLength(target) { + const length = ToIntegerOrInfinity(target); + if (length < 0) { + return 0; + } + return length < MAX_SAFE_INTEGER + ? length + : MAX_SAFE_INTEGER; +} +function SpeciesConstructor(target, defaultConstructor) { + if (!isObject(target)) { + throw NativeTypeError(THIS_IS_NOT_AN_OBJECT); + } + const constructor = target.constructor; + if (constructor === undefined) { + return defaultConstructor; + } + if (!isObject(constructor)) { + throw NativeTypeError(THE_CONSTRUCTOR_PROPERTY_VALUE_IS_NOT_AN_OBJECT); + } + const species = constructor[SymbolSpecies]; + if (species == null) { + return defaultConstructor; + } + return species; +} +function IsDetachedBuffer(buffer) { + if (isSharedArrayBuffer(buffer)) { + return false; + } + try { + ArrayBufferPrototypeSlice(buffer, 0, 0); + return false; + } catch (e) {} + return true; +} +function defaultCompare(x, y) { + const isXNaN = NumberIsNaN(x); + const isYNaN = NumberIsNaN(y); + if (isXNaN && isYNaN) { + return 0; + } + if (isXNaN) { + return 1; + } + if (isYNaN) { + return -1; + } + if (x < y) { + return -1; + } + if (x > y) { + return 1; + } + if (x === 0 && y === 0) { + const isXPlusZero = ObjectIs(x, 0); + const isYPlusZero = ObjectIs(y, 0); + if (!isXPlusZero && isYPlusZero) { + return -1; + } + if (isXPlusZero && !isYPlusZero) { + return 1; + } + } + return 0; +} + +const BYTES_PER_ELEMENT = 2; +const float16bitsArrays = new NativeWeakMap(); +function isFloat16Array(target) { + return WeakMapPrototypeHas(float16bitsArrays, target) || + (!ArrayBufferIsView(target) && hasFloat16ArrayBrand(target)); +} +function assertFloat16Array(target) { + if (!isFloat16Array(target)) { + throw NativeTypeError(THIS_IS_NOT_A_FLOAT16ARRAY_OBJECT); + } +} +function assertSpeciesTypedArray(target, count) { + const isTargetFloat16Array = isFloat16Array(target); + const isTargetTypedArray = isNativeTypedArray(target); + if (!isTargetFloat16Array && !isTargetTypedArray) { + throw NativeTypeError(SPECIES_CONSTRUCTOR_DIDNT_RETURN_TYPEDARRAY_OBJECT); + } + if (typeof count === "number") { + let length; + if (isTargetFloat16Array) { + const float16bitsArray = getFloat16BitsArray(target); + length = TypedArrayPrototypeGetLength(float16bitsArray); + } else { + length = TypedArrayPrototypeGetLength(target); + } + if (length < count) { + throw NativeTypeError( + DERIVED_CONSTRUCTOR_CREATED_TYPEDARRAY_OBJECT_WHICH_WAS_TOO_SMALL_LENGTH + ); + } + } + if (isNativeBigIntTypedArray(target)) { + throw NativeTypeError(CANNOT_MIX_BIGINT_AND_OTHER_TYPES); + } +} +function getFloat16BitsArray(float16) { + const float16bitsArray = WeakMapPrototypeGet(float16bitsArrays, float16); + if (float16bitsArray !== undefined) { + const buffer = TypedArrayPrototypeGetBuffer(float16bitsArray); + if (IsDetachedBuffer(buffer)) { + throw NativeTypeError(ATTEMPTING_TO_ACCESS_DETACHED_ARRAYBUFFER); + } + return float16bitsArray; + } + const buffer = (float16).buffer; + if (IsDetachedBuffer(buffer)) { + throw NativeTypeError(ATTEMPTING_TO_ACCESS_DETACHED_ARRAYBUFFER); + } + const cloned = ReflectConstruct(Float16Array, [ + buffer, + (float16).byteOffset, + (float16).length, + ], float16.constructor); + return WeakMapPrototypeGet(float16bitsArrays, cloned); +} +function copyToArray(float16bitsArray) { + const length = TypedArrayPrototypeGetLength(float16bitsArray); + const array = []; + for (let i = 0; i < length; ++i) { + array[i] = convertToNumber(float16bitsArray[i]); + } + return array; +} +const TypedArrayPrototypeGetters = new NativeWeakSet(); +for (const key of ReflectOwnKeys(TypedArrayPrototype)) { + if (key === SymbolToStringTag) { + continue; + } + const descriptor = ReflectGetOwnPropertyDescriptor(TypedArrayPrototype, key); + if (ObjectHasOwn(descriptor, "get") && typeof descriptor.get === "function") { + WeakSetPrototypeAdd(TypedArrayPrototypeGetters, descriptor.get); + } +} +const handler = ObjectFreeze( ({ + get(target, key, receiver) { + if (isCanonicalIntegerIndexString(key) && ObjectHasOwn(target, key)) { + return convertToNumber(ReflectGet(target, key)); + } + if (WeakSetPrototypeHas(TypedArrayPrototypeGetters, ObjectPrototype__lookupGetter__(target, key))) { + return ReflectGet(target, key); + } + return ReflectGet(target, key, receiver); + }, + set(target, key, value, receiver) { + if (isCanonicalIntegerIndexString(key) && ObjectHasOwn(target, key)) { + return ReflectSet(target, key, roundToFloat16Bits(value)); + } + return ReflectSet(target, key, value, receiver); + }, + getOwnPropertyDescriptor(target, key) { + if (isCanonicalIntegerIndexString(key) && ObjectHasOwn(target, key)) { + const descriptor = ReflectGetOwnPropertyDescriptor(target, key); + descriptor.value = convertToNumber(descriptor.value); + return descriptor; + } + return ReflectGetOwnPropertyDescriptor(target, key); + }, + defineProperty(target, key, descriptor) { + if ( + isCanonicalIntegerIndexString(key) && + ObjectHasOwn(target, key) && + ObjectHasOwn(descriptor, "value") + ) { + descriptor.value = roundToFloat16Bits(descriptor.value); + return ReflectDefineProperty(target, key, descriptor); + } + return ReflectDefineProperty(target, key, descriptor); + }, +})); +class Float16Array { + constructor(input, _byteOffset, _length) { + let float16bitsArray; + if (isFloat16Array(input)) { + float16bitsArray = ReflectConstruct(NativeUint16Array, [getFloat16BitsArray(input)], new.target); + } else if (isObject(input) && !isArrayBuffer(input)) { + let list; + let length; + if (isNativeTypedArray(input)) { + list = input; + length = TypedArrayPrototypeGetLength(input); + const buffer = TypedArrayPrototypeGetBuffer(input); + const BufferConstructor = !isSharedArrayBuffer(buffer) + ? (SpeciesConstructor( + buffer, + NativeArrayBuffer + )) + : NativeArrayBuffer; + if (IsDetachedBuffer(buffer)) { + throw NativeTypeError(ATTEMPTING_TO_ACCESS_DETACHED_ARRAYBUFFER); + } + if (isNativeBigIntTypedArray(input)) { + throw NativeTypeError(CANNOT_MIX_BIGINT_AND_OTHER_TYPES); + } + const data = new BufferConstructor( + length * BYTES_PER_ELEMENT + ); + float16bitsArray = ReflectConstruct(NativeUint16Array, [data], new.target); + } else { + const iterator = input[SymbolIterator]; + if (iterator != null && typeof iterator !== "function") { + throw NativeTypeError(ITERATOR_PROPERTY_IS_NOT_CALLABLE); + } + if (iterator != null) { + if (isOrdinaryArray(input)) { + list = input; + length = input.length; + } else { + list = [... (input)]; + length = list.length; + } + } else { + list = (input); + length = ToLength(list.length); + } + float16bitsArray = ReflectConstruct(NativeUint16Array, [length], new.target); + } + for (let i = 0; i < length; ++i) { + float16bitsArray[i] = roundToFloat16Bits(list[i]); + } + } else { + float16bitsArray = ReflectConstruct(NativeUint16Array, arguments, new.target); + } + const proxy = (new NativeProxy(float16bitsArray, handler)); + WeakMapPrototypeSet(float16bitsArrays, proxy, float16bitsArray); + return proxy; + } + static from(src, ...opts) { + const Constructor = this; + if (!ReflectHas(Constructor, brand)) { + throw NativeTypeError( + THIS_CONSTRUCTOR_IS_NOT_A_SUBCLASS_OF_FLOAT16ARRAY + ); + } + if (Constructor === Float16Array) { + if (isFloat16Array(src) && opts.length === 0) { + const float16bitsArray = getFloat16BitsArray(src); + const uint16 = new NativeUint16Array( + TypedArrayPrototypeGetBuffer(float16bitsArray), + TypedArrayPrototypeGetByteOffset(float16bitsArray), + TypedArrayPrototypeGetLength(float16bitsArray) + ); + return new Float16Array( + TypedArrayPrototypeGetBuffer(TypedArrayPrototypeSlice(uint16)) + ); + } + if (opts.length === 0) { + return new Float16Array( + TypedArrayPrototypeGetBuffer( + Uint16ArrayFrom(src, roundToFloat16Bits) + ) + ); + } + const mapFunc = opts[0]; + const thisArg = opts[1]; + return new Float16Array( + TypedArrayPrototypeGetBuffer( + Uint16ArrayFrom(src, function (val, ...args) { + return roundToFloat16Bits( + ReflectApply(mapFunc, this, [val, ...safeIfNeeded(args)]) + ); + }, thisArg) + ) + ); + } + let list; + let length; + const iterator = src[SymbolIterator]; + if (iterator != null && typeof iterator !== "function") { + throw NativeTypeError(ITERATOR_PROPERTY_IS_NOT_CALLABLE); + } + if (iterator != null) { + if (isOrdinaryArray(src)) { + list = src; + length = src.length; + } else if (isOrdinaryNativeTypedArray(src)) { + list = src; + length = TypedArrayPrototypeGetLength(src); + } else { + list = [...src]; + length = list.length; + } + } else { + if (src == null) { + throw NativeTypeError( + CANNOT_CONVERT_UNDEFINED_OR_NULL_TO_OBJECT + ); + } + list = NativeObject(src); + length = ToLength(list.length); + } + const array = new Constructor(length); + if (opts.length === 0) { + for (let i = 0; i < length; ++i) { + array[i] = (list[i]); + } + } else { + const mapFunc = opts[0]; + const thisArg = opts[1]; + for (let i = 0; i < length; ++i) { + array[i] = ReflectApply(mapFunc, thisArg, [list[i], i]); + } + } + return array; + } + static of(...items) { + const Constructor = this; + if (!ReflectHas(Constructor, brand)) { + throw NativeTypeError( + THIS_CONSTRUCTOR_IS_NOT_A_SUBCLASS_OF_FLOAT16ARRAY + ); + } + const length = items.length; + if (Constructor === Float16Array) { + const proxy = new Float16Array(length); + const float16bitsArray = getFloat16BitsArray(proxy); + for (let i = 0; i < length; ++i) { + float16bitsArray[i] = roundToFloat16Bits(items[i]); + } + return proxy; + } + const array = new Constructor(length); + for (let i = 0; i < length; ++i) { + array[i] = items[i]; + } + return array; + } + keys() { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + return TypedArrayPrototypeKeys(float16bitsArray); + } + values() { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + return wrap((function* () { + for (const val of TypedArrayPrototypeValues(float16bitsArray)) { + yield convertToNumber(val); + } + })()); + } + entries() { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + return wrap((function* () { + for (const [i, val] of TypedArrayPrototypeEntries(float16bitsArray)) { + yield ([i, convertToNumber(val)]); + } + })()); + } + at(index) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + const length = TypedArrayPrototypeGetLength(float16bitsArray); + const relativeIndex = ToIntegerOrInfinity(index); + const k = relativeIndex >= 0 ? relativeIndex : length + relativeIndex; + if (k < 0 || k >= length) { + return; + } + return convertToNumber(float16bitsArray[k]); + } + map(callback, ...opts) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + const length = TypedArrayPrototypeGetLength(float16bitsArray); + const thisArg = opts[0]; + const Constructor = SpeciesConstructor(float16bitsArray, Float16Array); + if (Constructor === Float16Array) { + const proxy = new Float16Array(length); + const array = getFloat16BitsArray(proxy); + for (let i = 0; i < length; ++i) { + const val = convertToNumber(float16bitsArray[i]); + array[i] = roundToFloat16Bits( + ReflectApply(callback, thisArg, [val, i, this]) + ); + } + return proxy; + } + const array = new Constructor(length); + assertSpeciesTypedArray(array, length); + for (let i = 0; i < length; ++i) { + const val = convertToNumber(float16bitsArray[i]); + array[i] = ReflectApply(callback, thisArg, [val, i, this]); + } + return (array); + } + filter(callback, ...opts) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + const length = TypedArrayPrototypeGetLength(float16bitsArray); + const thisArg = opts[0]; + const kept = []; + for (let i = 0; i < length; ++i) { + const val = convertToNumber(float16bitsArray[i]); + if (ReflectApply(callback, thisArg, [val, i, this])) { + ArrayPrototypePush(kept, val); + } + } + const Constructor = SpeciesConstructor(float16bitsArray, Float16Array); + const array = new Constructor(kept); + assertSpeciesTypedArray(array); + return (array); + } + reduce(callback, ...opts) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + const length = TypedArrayPrototypeGetLength(float16bitsArray); + if (length === 0 && opts.length === 0) { + throw NativeTypeError(REDUCE_OF_EMPTY_ARRAY_WITH_NO_INITIAL_VALUE); + } + let accumulator, start; + if (opts.length === 0) { + accumulator = convertToNumber(float16bitsArray[0]); + start = 1; + } else { + accumulator = opts[0]; + start = 0; + } + for (let i = start; i < length; ++i) { + accumulator = callback( + accumulator, + convertToNumber(float16bitsArray[i]), + i, + this + ); + } + return accumulator; + } + reduceRight(callback, ...opts) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + const length = TypedArrayPrototypeGetLength(float16bitsArray); + if (length === 0 && opts.length === 0) { + throw NativeTypeError(REDUCE_OF_EMPTY_ARRAY_WITH_NO_INITIAL_VALUE); + } + let accumulator, start; + if (opts.length === 0) { + accumulator = convertToNumber(float16bitsArray[length - 1]); + start = length - 2; + } else { + accumulator = opts[0]; + start = length - 1; + } + for (let i = start; i >= 0; --i) { + accumulator = callback( + accumulator, + convertToNumber(float16bitsArray[i]), + i, + this + ); + } + return accumulator; + } + forEach(callback, ...opts) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + const length = TypedArrayPrototypeGetLength(float16bitsArray); + const thisArg = opts[0]; + for (let i = 0; i < length; ++i) { + ReflectApply(callback, thisArg, [ + convertToNumber(float16bitsArray[i]), + i, + this, + ]); + } + } + find(callback, ...opts) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + const length = TypedArrayPrototypeGetLength(float16bitsArray); + const thisArg = opts[0]; + for (let i = 0; i < length; ++i) { + const value = convertToNumber(float16bitsArray[i]); + if (ReflectApply(callback, thisArg, [value, i, this])) { + return value; + } + } + } + findIndex(callback, ...opts) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + const length = TypedArrayPrototypeGetLength(float16bitsArray); + const thisArg = opts[0]; + for (let i = 0; i < length; ++i) { + const value = convertToNumber(float16bitsArray[i]); + if (ReflectApply(callback, thisArg, [value, i, this])) { + return i; + } + } + return -1; + } + findLast(callback, ...opts) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + const length = TypedArrayPrototypeGetLength(float16bitsArray); + const thisArg = opts[0]; + for (let i = length - 1; i >= 0; --i) { + const value = convertToNumber(float16bitsArray[i]); + if (ReflectApply(callback, thisArg, [value, i, this])) { + return value; + } + } + } + findLastIndex(callback, ...opts) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + const length = TypedArrayPrototypeGetLength(float16bitsArray); + const thisArg = opts[0]; + for (let i = length - 1; i >= 0; --i) { + const value = convertToNumber(float16bitsArray[i]); + if (ReflectApply(callback, thisArg, [value, i, this])) { + return i; + } + } + return -1; + } + every(callback, ...opts) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + const length = TypedArrayPrototypeGetLength(float16bitsArray); + const thisArg = opts[0]; + for (let i = 0; i < length; ++i) { + if ( + !ReflectApply(callback, thisArg, [ + convertToNumber(float16bitsArray[i]), + i, + this, + ]) + ) { + return false; + } + } + return true; + } + some(callback, ...opts) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + const length = TypedArrayPrototypeGetLength(float16bitsArray); + const thisArg = opts[0]; + for (let i = 0; i < length; ++i) { + if ( + ReflectApply(callback, thisArg, [ + convertToNumber(float16bitsArray[i]), + i, + this, + ]) + ) { + return true; + } + } + return false; + } + set(input, ...opts) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + const targetOffset = ToIntegerOrInfinity(opts[0]); + if (targetOffset < 0) { + throw NativeRangeError(OFFSET_IS_OUT_OF_BOUNDS); + } + if (input == null) { + throw NativeTypeError( + CANNOT_CONVERT_UNDEFINED_OR_NULL_TO_OBJECT + ); + } + if (isNativeBigIntTypedArray(input)) { + throw NativeTypeError( + CANNOT_MIX_BIGINT_AND_OTHER_TYPES + ); + } + if (isFloat16Array(input)) { + return TypedArrayPrototypeSet( + getFloat16BitsArray(this), + getFloat16BitsArray(input), + targetOffset + ); + } + if (isNativeTypedArray(input)) { + const buffer = TypedArrayPrototypeGetBuffer(input); + if (IsDetachedBuffer(buffer)) { + throw NativeTypeError(ATTEMPTING_TO_ACCESS_DETACHED_ARRAYBUFFER); + } + } + const targetLength = TypedArrayPrototypeGetLength(float16bitsArray); + const src = NativeObject(input); + const srcLength = ToLength(src.length); + if (targetOffset === Infinity || srcLength + targetOffset > targetLength) { + throw NativeRangeError(OFFSET_IS_OUT_OF_BOUNDS); + } + for (let i = 0; i < srcLength; ++i) { + float16bitsArray[i + targetOffset] = roundToFloat16Bits(src[i]); + } + } + reverse() { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + TypedArrayPrototypeReverse(float16bitsArray); + return this; + } + fill(value, ...opts) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + TypedArrayPrototypeFill( + float16bitsArray, + roundToFloat16Bits(value), + ...safeIfNeeded(opts) + ); + return this; + } + copyWithin(target, start, ...opts) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + TypedArrayPrototypeCopyWithin(float16bitsArray, target, start, ...safeIfNeeded(opts)); + return this; + } + sort(compareFn) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + const sortCompare = compareFn !== undefined ? compareFn : defaultCompare; + TypedArrayPrototypeSort(float16bitsArray, (x, y) => { + return sortCompare(convertToNumber(x), convertToNumber(y)); + }); + return this; + } + slice(start, end) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + const Constructor = SpeciesConstructor(float16bitsArray, Float16Array); + if (Constructor === Float16Array) { + const uint16 = new NativeUint16Array( + TypedArrayPrototypeGetBuffer(float16bitsArray), + TypedArrayPrototypeGetByteOffset(float16bitsArray), + TypedArrayPrototypeGetLength(float16bitsArray) + ); + return new Float16Array( + TypedArrayPrototypeGetBuffer( + TypedArrayPrototypeSlice(uint16, start, end) + ) + ); + } + const length = TypedArrayPrototypeGetLength(float16bitsArray); + const relativeStart = ToIntegerOrInfinity(start); + const relativeEnd = end === undefined ? length : ToIntegerOrInfinity(end); + let k; + if (relativeStart === -Infinity) { + k = 0; + } else if (relativeStart < 0) { + k = length + relativeStart > 0 ? length + relativeStart : 0; + } else { + k = length < relativeStart ? length : relativeStart; + } + let final; + if (relativeEnd === -Infinity) { + final = 0; + } else if (relativeEnd < 0) { + final = length + relativeEnd > 0 ? length + relativeEnd : 0; + } else { + final = length < relativeEnd ? length : relativeEnd; + } + const count = final - k > 0 ? final - k : 0; + const array = new Constructor(count); + assertSpeciesTypedArray(array, count); + if (count === 0) { + return array; + } + const buffer = TypedArrayPrototypeGetBuffer(float16bitsArray); + if (IsDetachedBuffer(buffer)) { + throw NativeTypeError(ATTEMPTING_TO_ACCESS_DETACHED_ARRAYBUFFER); + } + let n = 0; + while (k < final) { + array[n] = convertToNumber(float16bitsArray[k]); + ++k; + ++n; + } + return (array); + } + subarray(begin, end) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + const Constructor = SpeciesConstructor(float16bitsArray, Float16Array); + const uint16 = new NativeUint16Array( + TypedArrayPrototypeGetBuffer(float16bitsArray), + TypedArrayPrototypeGetByteOffset(float16bitsArray), + TypedArrayPrototypeGetLength(float16bitsArray) + ); + const uint16Subarray = TypedArrayPrototypeSubarray(uint16, begin, end); + const array = new Constructor( + TypedArrayPrototypeGetBuffer(uint16Subarray), + TypedArrayPrototypeGetByteOffset(uint16Subarray), + TypedArrayPrototypeGetLength(uint16Subarray) + ); + assertSpeciesTypedArray(array); + return (array); + } + indexOf(element, ...opts) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + const length = TypedArrayPrototypeGetLength(float16bitsArray); + let from = ToIntegerOrInfinity(opts[0]); + if (from === Infinity) { + return -1; + } + if (from < 0) { + from += length; + if (from < 0) { + from = 0; + } + } + for (let i = from; i < length; ++i) { + if ( + ObjectHasOwn(float16bitsArray, i) && + convertToNumber(float16bitsArray[i]) === element + ) { + return i; + } + } + return -1; + } + lastIndexOf(element, ...opts) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + const length = TypedArrayPrototypeGetLength(float16bitsArray); + let from = opts.length >= 1 ? ToIntegerOrInfinity(opts[0]) : length - 1; + if (from === -Infinity) { + return -1; + } + if (from >= 0) { + from = from < length - 1 ? from : length - 1; + } else { + from += length; + } + for (let i = from; i >= 0; --i) { + if ( + ObjectHasOwn(float16bitsArray, i) && + convertToNumber(float16bitsArray[i]) === element + ) { + return i; + } + } + return -1; + } + includes(element, ...opts) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + const length = TypedArrayPrototypeGetLength(float16bitsArray); + let from = ToIntegerOrInfinity(opts[0]); + if (from === Infinity) { + return false; + } + if (from < 0) { + from += length; + if (from < 0) { + from = 0; + } + } + const isNaN = NumberIsNaN(element); + for (let i = from; i < length; ++i) { + const value = convertToNumber(float16bitsArray[i]); + if (isNaN && NumberIsNaN(value)) { + return true; + } + if (value === element) { + return true; + } + } + return false; + } + join(separator) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + const array = copyToArray(float16bitsArray); + return ArrayPrototypeJoin(array, separator); + } + toLocaleString(...opts) { + assertFloat16Array(this); + const float16bitsArray = getFloat16BitsArray(this); + const array = copyToArray(float16bitsArray); + return ArrayPrototypeToLocaleString(array, ...safeIfNeeded(opts)); + } + get [SymbolToStringTag]() { + if (isFloat16Array(this)) { + return ("Float16Array"); + } + } +} +ObjectDefineProperty(Float16Array, "BYTES_PER_ELEMENT", { + value: BYTES_PER_ELEMENT, +}); +ObjectDefineProperty(Float16Array, brand, {}); +ReflectSetPrototypeOf(Float16Array, TypedArray); +const Float16ArrayPrototype = Float16Array.prototype; +ObjectDefineProperty(Float16ArrayPrototype, "BYTES_PER_ELEMENT", { + value: BYTES_PER_ELEMENT, +}); +ObjectDefineProperty(Float16ArrayPrototype, SymbolIterator, { + value: Float16ArrayPrototype.values, + writable: true, + configurable: true, +}); +ReflectSetPrototypeOf(Float16ArrayPrototype, TypedArrayPrototype); + +function isTypedArray(target) { + return isNativeTypedArray(target) || isFloat16Array(target); +} + +function getFloat16(dataView, byteOffset, ...opts) { + return convertToNumber( + DataViewPrototypeGetUint16(dataView, byteOffset, ...safeIfNeeded(opts)) + ); +} +function setFloat16(dataView, byteOffset, value, ...opts) { + return DataViewPrototypeSetUint16( + dataView, + byteOffset, + roundToFloat16Bits(value), + ...safeIfNeeded(opts) + ); +} + +function hfround(x) { + const number = +x; + if (!NumberIsFinite(number) || number === 0) { + return number; + } + const x16 = roundToFloat16Bits(number); + return convertToNumber(x16); +} + +export { Float16Array, getFloat16, hfround, isFloat16Array, isTypedArray, setFloat16 }; diff --git a/src/unittests/f32_interval.spec.ts b/src/unittests/f32_interval.spec.ts index 483fba0592e8..79646d094bca 100644 --- a/src/unittests/f32_interval.spec.ts +++ b/src/unittests/f32_interval.spec.ts @@ -41,6 +41,7 @@ import { multiplicationInterval, negationInterval, powInterval, + quantizeToF16Interval, radiansInterval, remainderInterval, roundInterval, @@ -1171,6 +1172,41 @@ g.test('negationInterval') ); }); +g.test('quantizeToF16Interval') + .paramsSubcasesOnly( + // prettier-ignore + [ + { input: kValue.f32.infinity.negative, expected: kAny }, + { input: kValue.f32.negative.min, expected: kAny }, + { input: kValue.f16.negative.min, expected: [kValue.f16.negative.min] }, + { input: -1, expected: [-1] }, + { input: -0.1, expected: [hexToF32(0xbdcce000), hexToF32(0xbdccc000)] }, // ~-0.1 + { input: kValue.f16.negative.max, expected: [kValue.f16.negative.max] }, + { input: kValue.f16.subnormal.negative.min, expected: [kValue.f16.subnormal.negative.min] }, + { input: kValue.f16.subnormal.negative.max, expected: [kValue.f16.subnormal.negative.max] }, + { input: kValue.f32.subnormal.negative.max, expected: [kValue.f16.subnormal.negative.max, 0] }, + { input: 0, expected: [0] }, + { input: kValue.f32.subnormal.positive.min, expected: [0, kValue.f16.subnormal.positive.min] }, + { input: kValue.f16.subnormal.positive.min, expected: [kValue.f16.subnormal.positive.min] }, + { input: kValue.f16.subnormal.positive.max, expected: [kValue.f16.subnormal.positive.max] }, + { input: kValue.f16.positive.min, expected: [kValue.f16.positive.min] }, + { input: 0.1, expected: [hexToF32(0x3dccc000), hexToF32(0x3dcce000)] }, // ~0.1 + { input: 1, expected: [1] }, + { input: kValue.f16.positive.max, expected: [kValue.f16.positive.max] }, + { input: kValue.f32.positive.max, expected: kAny }, + { input: kValue.f32.infinity.positive, expected: kAny }, + ] + ) + .fn(t => { + const expected = new F32Interval(...t.params.expected); + + const got = quantizeToF16Interval(t.params.input); + t.expect( + objectEquals(expected, got), + `quantizeToF16Interval(${t.params.input}) returned ${got}. Expected ${expected}` + ); + }); + g.test('radiansInterval') .paramsSubcasesOnly( // prettier-ignore diff --git a/src/unittests/maths.spec.ts b/src/unittests/maths.spec.ts index c120223da599..6cd02a124414 100644 --- a/src/unittests/maths.spec.ts +++ b/src/unittests/maths.spec.ts @@ -5,7 +5,15 @@ Util math unit tests. import { makeTestGroup } from '../common/framework/test_group.js'; import { objectEquals } from '../common/util/util.js'; import { kBit, kValue } from '../webgpu/util/constants.js'; -import { f32, f32Bits, float32ToUint32, Scalar } from '../webgpu/util/conversion.js'; +import { + f32, + f32Bits, + float16ToUint16, + float32ToUint32, + Scalar, + uint16ToFloat16, + uint32ToFloat32, +} from '../webgpu/util/conversion.js'; import { biasedRange, cartesianProduct, @@ -16,7 +24,7 @@ import { hexToF64, lerp, linearRange, - nextAfter, + nextAfterF32, oneULP, withinULP, } from '../webgpu/util/math.js'; @@ -104,7 +112,7 @@ g.test('nextAfterFlushToZero') const dir = t.params.dir; const expect = t.params.result; const expect_type = typeof expect; - const got = nextAfter(val, dir, true); + const got = nextAfterF32(val, dir, true); const got_type = typeof got; t.expect( got.value === expect.value || (Number.isNaN(got.value) && Number.isNaN(expect.value)), @@ -170,7 +178,7 @@ g.test('nextAfterNoFlush') const dir = t.params.dir; const expect = t.params.result; const expect_type = typeof expect; - const got = nextAfter(val, dir, false); + const got = nextAfterF32(val, dir, false); const got_type = typeof got; t.expect( got.value === expect.value || (Number.isNaN(got.value) && Number.isNaN(expect.value)), @@ -830,7 +838,33 @@ g.test('f32LimitsEquivalency') const value = test.params.value; const val_to_bits = bits === float32ToUint32(value); - const bits_to_val = value === (f32Bits(bits).value as number); + const bits_to_val = value === uint32ToFloat32(bits); + test.expect( + val_to_bits && bits_to_val, + `bits = ${bits}, value = ${value}, returned val_to_bits as ${val_to_bits}, and bits_to_val as ${bits_to_val}, they are expected to be equivalent` + ); + }); + +// Test to confirm kBit and kValue constants are equivalent for f16 +g.test('f16LimitsEquivalency') + .paramsSimple([ + { bits: kBit.f16.positive.max, value: kValue.f16.positive.max }, + { bits: kBit.f16.positive.min, value: kValue.f16.positive.min }, + { bits: kBit.f16.negative.max, value: kValue.f16.negative.max }, + { bits: kBit.f16.negative.min, value: kValue.f16.negative.min }, + { bits: kBit.f16.subnormal.positive.max, value: kValue.f16.subnormal.positive.max }, + { bits: kBit.f16.subnormal.positive.min, value: kValue.f16.subnormal.positive.min }, + { bits: kBit.f16.subnormal.negative.max, value: kValue.f16.subnormal.negative.max }, + { bits: kBit.f16.subnormal.negative.min, value: kValue.f16.subnormal.negative.min }, + { bits: kBit.f16.infinity.positive, value: kValue.f16.infinity.positive }, + { bits: kBit.f16.infinity.negative, value: kValue.f16.infinity.negative }, + ]) + .fn(test => { + const bits = test.params.bits; + const value = test.params.value; + + const val_to_bits = bits === float16ToUint16(value); + const bits_to_val = value === uint16ToFloat16(bits); test.expect( val_to_bits && bits_to_val, `bits = ${bits}, value = ${value}, returned val_to_bits as ${val_to_bits}, and bits_to_val as ${bits_to_val}, they are expected to be equivalent` diff --git a/src/webgpu/shader/execution/expression/binary/f32_logical.spec.ts b/src/webgpu/shader/execution/expression/binary/f32_logical.spec.ts index 33615e96e3de..1c926b917d81 100644 --- a/src/webgpu/shader/execution/expression/binary/f32_logical.spec.ts +++ b/src/webgpu/shader/execution/expression/binary/f32_logical.spec.ts @@ -6,7 +6,7 @@ import { makeTestGroup } from '../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../gpu_test.js'; import { anyOf } from '../../../../util/compare.js'; import { bool, f32, Scalar, TypeBool, TypeF32 } from '../../../../util/conversion.js'; -import { flushSubnormalScalar, fullF32Range } from '../../../../util/math.js'; +import { flushSubnormalScalarF32, fullF32Range } from '../../../../util/math.js'; import { allInputSources, Case, run } from '../expression.js'; import { binary } from './binary.js'; @@ -24,8 +24,8 @@ function makeCase( ): Case { const f32_lhs = f32(lhs); const f32_rhs = f32(rhs); - const lhs_options = new Set([f32_lhs, flushSubnormalScalar(f32_lhs)]); - const rhs_options = new Set([f32_rhs, flushSubnormalScalar(f32_rhs)]); + const lhs_options = new Set([f32_lhs, flushSubnormalScalarF32(f32_lhs)]); + const rhs_options = new Set([f32_rhs, flushSubnormalScalarF32(f32_rhs)]); const expected: Array = []; lhs_options.forEach(l => { rhs_options.forEach(r => { diff --git a/src/webgpu/shader/execution/expression/call/builtin/quantizeToF16.spec.ts b/src/webgpu/shader/execution/expression/call/builtin/quantizeToF16.spec.ts index 1d11c868b1f0..8b5ed2b41b21 100644 --- a/src/webgpu/shader/execution/expression/call/builtin/quantizeToF16.spec.ts +++ b/src/webgpu/shader/execution/expression/call/builtin/quantizeToF16.spec.ts @@ -10,7 +10,13 @@ Component-wise when T is a vector. import { makeTestGroup } from '../../../../../../common/framework/test_group.js'; import { GPUTest } from '../../../../../gpu_test.js'; -import { allInputSources } from '../../expression.js'; +import { kValue } from '../../../../../util/constants.js'; +import { TypeF32 } from '../../../../../util/conversion.js'; +import { quantizeToF16Interval } from '../../../../../util/f32_interval.js'; +import { fullF32Range } from '../../../../../util/math.js'; +import { allInputSources, Case, makeUnaryToF32IntervalCase, run } from '../../expression.js'; + +import { builtin } from './builtin.js'; export const g = makeTestGroup(GPUTest); @@ -20,4 +26,22 @@ g.test('f32') .params(u => u.combine('inputSource', allInputSources).combine('vectorize', [undefined, 2, 3, 4] as const) ) - .unimplemented(); + .fn(async t => { + const makeCase = (x: number): Case => { + return makeUnaryToF32IntervalCase(x, quantizeToF16Interval); + }; + + const cases = [ + kValue.f16.negative.min, + kValue.f16.negative.max, + kValue.f16.subnormal.negative.min, + kValue.f16.subnormal.negative.max, + kValue.f16.subnormal.positive.min, + kValue.f16.subnormal.positive.max, + kValue.f16.positive.min, + kValue.f16.positive.max, + ...fullF32Range(), + ].map(makeCase); + + await run(t, builtin('quantizeToF16'), [TypeF32], TypeF32, t.params, cases); + }); diff --git a/src/webgpu/util/constants.ts b/src/webgpu/util/constants.ts index 72e2c1b106a8..c357d3259f47 100644 --- a/src/webgpu/util/constants.ts +++ b/src/webgpu/util/constants.ts @@ -1,3 +1,5 @@ +import { Float16Array } from '../../external/petamoriken/float16/float16.js'; + // MAINTENANCE_TODO(sarahM0): Perhaps instead of kBit and kValue tables we could have one table // where every value is a Scalar instead of either bits or value? // Then tests wouldn't need most of the Scalar.fromX calls, @@ -80,6 +82,44 @@ export const kBit = { }, }, + // Limits of f16 + f16: { + positive: { + min: 0x0400, + max: 0x7bff, + zero: 0x0000, + }, + negative: { + max: 0x8400, + min: 0xfbff, + zero: 0x8000, + }, + subnormal: { + positive: { + min: 0x0001, + max: 0x03ff, + }, + negative: { + max: 0x8001, + min: 0x83ff, + }, + }, + nan: { + negative: { + s: 0xfc01, + q: 0xfe01, + }, + positive: { + s: 0x7c01, + q: 0x7e01, + }, + }, + infinity: { + positive: 0x7c00, + negative: 0xfc00, + }, + }, + // 32-bit representation of power(2, n) n = {-31, ..., 31} // A uint32 representation as a JS `number` // {toMinus31, ..., to31} ie. {-31, ..., 31} @@ -224,13 +264,23 @@ export const kBit = { /** * Converts a 32-bit hex value to a 32-bit float value * - * Using a locally defined function here, instead of uint32ToFloat32 or f32Bits - * functions, to avoid compile time dependency issues. + * Using a locally defined function here to avoid compile time dependency + * issues. * */ function hexToF32(hex: number): number { return new Float32Array(new Uint32Array([hex]).buffer)[0]; } +/** + * Converts a 16-bit hex value to a 16-bit float value + * + * Using a locally defined function here to avoid compile time dependency + * issues. + * */ +function hexToF16(hex: number): number { + return new Float16Array(new Uint16Array([hex]).buffer)[0]; +} + export const kValue = { // Limits of i32 i32: { @@ -296,6 +346,34 @@ export const kValue = { }, }, + // Limits of f16 + f16: { + positive: { + min: hexToF16(kBit.f16.positive.min), + max: hexToF16(kBit.f16.positive.max), + zero: hexToF16(kBit.f16.positive.zero), + }, + negative: { + max: hexToF16(kBit.f16.negative.max), + min: hexToF16(kBit.f16.negative.min), + zero: hexToF16(kBit.f16.negative.zero), + }, + subnormal: { + positive: { + min: hexToF16(kBit.f16.subnormal.positive.min), + max: hexToF16(kBit.f16.subnormal.positive.max), + }, + negative: { + max: hexToF16(kBit.f16.subnormal.negative.max), + min: hexToF16(kBit.f16.subnormal.negative.min), + }, + }, + infinity: { + positive: hexToF16(kBit.f16.infinity.positive), + negative: hexToF16(kBit.f16.infinity.negative), + }, + }, + powTwo: { to0: Math.pow(2, 0), to1: Math.pow(2, 1), diff --git a/src/webgpu/util/conversion.ts b/src/webgpu/util/conversion.ts index 162748792c93..db96e491a9b8 100644 --- a/src/webgpu/util/conversion.ts +++ b/src/webgpu/util/conversion.ts @@ -1,7 +1,8 @@ import { Colors } from '../../common/util/colors.js'; import { assert, TypedArrayBufferView } from '../../common/util/util.js'; +import { Float16Array } from '../../external/petamoriken/float16/float16.js'; -import { clamp, isSubnormalNumber } from './math.js'; +import { clamp, isSubnormalNumberF32 } from './math.js'; /** * Encodes a JS `number` into a "normalized" (unorm/snorm) integer representation with `bits` bits. @@ -313,6 +314,30 @@ export function uint32ToInt32(u32: number): number { return i32Arr[0]; } +/** Converts a 16-bit float value to a 16-bit unsigned integer value */ +export function float16ToUint16(f16: number): number { + const f16Arr = new Float16Array(1); + f16Arr[0] = f16; + const u16Arr = new Uint16Array(f16Arr.buffer); + return u16Arr[0]; +} + +/** Converts a 16-bit unsigned integer value to a 16-bit float value */ +export function uint16ToFloat16(u16: number): number { + const u16Arr = new Uint16Array(1); + u16Arr[0] = u16; + const f16Arr = new Float16Array(u16Arr.buffer); + return f16Arr[0]; +} + +/** Converts a 16-bit float value to a 16-bit signed integer value */ +export function float16ToInt16(f16: number): number { + const f16Arr = new Float16Array(1); + f16Arr[0] = f16; + const i16Arr = new Int16Array(f16Arr.buffer); + return i16Arr[0]; +} + /** A type of number representable by Scalar. */ export type ScalarKind = | 'f64' @@ -552,7 +577,7 @@ export class Scalar { if (n !== null && isFloatValue(this)) { let str = this.value.toString(); str = str.indexOf('.') > 0 || str.indexOf('e') > 0 ? str : `${str}.0`; - return isSubnormalNumber(n.valueOf()) + return isSubnormalNumberF32(n.valueOf()) ? `${Colors.bold(str)} (0x${hex} subnormal)` : `${Colors.bold(str)} (0x${hex})`; } @@ -572,6 +597,11 @@ export function f32(value: number): Scalar { const arr = new Float32Array([value]); return new Scalar(TypeF32, arr[0], arr); } +/** Create an f16 from a numeric value, a JS `number`. */ +export function f16(value: number): Scalar { + const arr = new Float16Array([value]); + return new Scalar(TypeF16, arr[0], arr); +} /** Create an f32 from a bit representation, a uint32 represented as a JS `number`. */ export function f32Bits(bits: number): Scalar { const arr = new Uint32Array([bits]); @@ -580,7 +610,7 @@ export function f32Bits(bits: number): Scalar { /** Create an f16 from a bit representation, a uint16 represented as a JS `number`. */ export function f16Bits(bits: number): Scalar { const arr = new Uint16Array([bits]); - return new Scalar(TypeF16, float16BitsToFloat32(bits), arr); + return new Scalar(TypeF16, new Float16Array(arr.buffer)[0], arr); } /** Create an i32 from a numeric value, a JS `number`. */ diff --git a/src/webgpu/util/f32_interval.ts b/src/webgpu/util/f32_interval.ts index 840a5af0c67c..1929221d3340 100644 --- a/src/webgpu/util/f32_interval.ts +++ b/src/webgpu/util/f32_interval.ts @@ -3,10 +3,11 @@ import { assert, unreachable } from '../../common/util/util.js'; import { kValue } from './constants.js'; import { cartesianProduct, + correctlyRoundedF16, correctlyRoundedF32, - flushSubnormalNumber, - isF32Finite, - isSubnormalNumber, + flushSubnormalNumberF32, + isFiniteF32, + isSubnormalNumberF32, oneULP, } from './math.js'; @@ -63,7 +64,7 @@ export class F32Interval { /** @returns if this interval only contains f32 finite values */ public isFinite(): boolean { - return isF32Finite(this.begin) && isF32Finite(this.end); + return isFiniteF32(this.begin) && isFiniteF32(this.end); } /** @returns an interval with the tightest bounds that includes all provided intervals */ @@ -147,7 +148,7 @@ function toF32Vector(v: number[] | F32Vector): F32Vector { * returns the input */ function addFlushedIfNeeded(values: number[]): number[] { - return values.some(isSubnormalNumber) ? values.concat(0) : values; + return values.some(isSubnormalNumberF32) ? values.concat(0) : values; } /** @@ -560,7 +561,7 @@ function AbsoluteErrorIntervalOp(error_range: number): PointToIntervalOp { }, }; - if (isF32Finite(error_range)) { + if (isFiniteF32(error_range)) { op.impl = (n: number) => { assert(!Number.isNaN(n), `absolute error not defined for NaN`); return new F32Interval(n - error_range, n + error_range); @@ -584,7 +585,7 @@ function ULPIntervalOp(numULP: number): PointToIntervalOp { }, }; - if (isF32Finite(numULP)) { + if (isFiniteF32(numULP)) { op.impl = (n: number) => { assert(!Number.isNaN(n), `ULP error not defined for NaN`); @@ -593,8 +594,8 @@ function ULPIntervalOp(numULP: number): PointToIntervalOp { const end = n + numULP * ulp; return new F32Interval( - Math.min(begin, flushSubnormalNumber(begin)), - Math.max(end, flushSubnormalNumber(end)) + Math.min(begin, flushSubnormalNumberF32(begin)), + Math.max(end, flushSubnormalNumberF32(end)) ); }; } @@ -1104,6 +1105,32 @@ export function powInterval(x: number | F32Interval, y: number | F32Interval): F return runBinaryOp(toF32Interval(x), toF32Interval(y), PowIntervalOp); } +// Once a full implementation of F16Interval exists, the correctlyRounded for that can potentially be used instead of +// having a bespoke operation implementation. +const QuantizeToF16IntervalOp: PointToIntervalOp = { + impl: (n: number): F32Interval => { + // This will perform FTZ for f16, this might need to change depending on the outcome of + // https://github.com/gpuweb/gpuweb/issues/3421 + const rounded = correctlyRoundedF16(n); + // All f16 values are representable as normal f32 values, so there is no need to handle flushing on the output of + // correctlyRoundedF16 + if (rounded.length === 2) { + return new F32Interval(rounded[0], rounded[1]); + } + if (rounded.length === 1) { + return new F32Interval(rounded[0]); + } + unreachable( + `Result of correctlyRoundedF16(${n}) = [${rounded}] is expected to have 1 or 2 elements` + ); + }, +}; + +/** Calculate an acceptance interval of quanitizeToF16(x) */ +export function quantizeToF16Interval(n: number): F32Interval { + return runPointOp(toF32Interval(n), QuantizeToF16IntervalOp); +} + const RadiansIntervalOp: PointToIntervalOp = { impl: (n: number): F32Interval => { return multiplicationInterval(n, 0.017453292519943295474); diff --git a/src/webgpu/util/math.ts b/src/webgpu/util/math.ts index d9aa579848d5..1afefab88196 100644 --- a/src/webgpu/util/math.ts +++ b/src/webgpu/util/math.ts @@ -1,7 +1,18 @@ import { assert } from '../../common/util/util.js'; +import { Float16Array } from '../../external/petamoriken/float16/float16.js'; import { kBit, kValue } from './constants.js'; -import { f32, f32Bits, i32, Scalar } from './conversion.js'; +import { + f16, + f16Bits, + f32, + f32Bits, + floatBitsToNumber, + i32, + kFloat16Format, + kFloat32Format, + Scalar, +} from './conversion.js'; /** * A multiple of 8 guaranteed to be way too large to allocate (just under 8 pebibytes). @@ -34,20 +45,20 @@ export function clamp(n: number, { min, max }: { min: number; max: number }): nu } /** @returns 0 if |val| is a subnormal f32 number, otherwise returns |val| */ -export function flushSubnormalNumber(val: number): number { - return isSubnormalNumber(val) ? 0 : val; +export function flushSubnormalNumberF32(val: number): number { + return isSubnormalNumberF32(val) ? 0 : val; } /** @returns 0 if |val| is a subnormal f32 number, otherwise returns |val| */ -export function flushSubnormalScalar(val: Scalar): Scalar { - return isSubnormalScalar(val) ? f32(0) : val; +export function flushSubnormalScalarF32(val: Scalar): Scalar { + return isSubnormalScalarF32(val) ? f32(0) : val; } /** * @returns true if |val| is a subnormal f32 number, otherwise returns false * 0 is considered a non-subnormal number by this function. */ -export function isSubnormalScalar(val: Scalar): boolean { +export function isSubnormalScalarF32(val: Scalar): boolean { if (val.type.kind !== 'f32') { return false; } @@ -60,27 +71,64 @@ export function isSubnormalScalar(val: Scalar): boolean { return (u32_val & 0x7f800000) === 0; } -/** Utility to pass TS numbers into |isSubnormalNumber| */ -export function isSubnormalNumber(val: number): boolean { - return val > kValue.f32.negative.max && val < kValue.f32.positive.min; +/** U/** @returns if number is within subnormal range of f32 */ +export function isSubnormalNumberF32(n: number): boolean { + return n > kValue.f32.negative.max && n < kValue.f32.positive.min; } /** @returns if number is in the finite range of f32 */ -export function isF32Finite(n: number) { +export function isFiniteF32(n: number) { return n >= kValue.f32.negative.min && n <= kValue.f32.positive.max; } +/** @returns 0 if |val| is a subnormal f16 number, otherwise returns |val| */ +export function flushSubnormalNumberF16(val: number): number { + return isSubnormalNumberF16(val) ? 0 : val; +} + +/** @returns 0 if |val| is a subnormal f16 number, otherwise returns |val| */ +export function flushSubnormalScalarF16(val: Scalar): Scalar { + return isSubnormalScalarF16(val) ? f16(0) : val; +} + /** - * @returns the next single precision floating point value after |val|, + * @returns true if |val| is a subnormal f16 number, otherwise returns false + * 0 is considered a non-subnormal number by this function. + */ +export function isSubnormalScalarF16(val: Scalar): boolean { + if (val.type.kind !== 'f16') { + return false; + } + + if (val === f16(0)) { + return false; + } + + const u16_val = new Uint16Array(new Float16Array([val.value.valueOf() as number]).buffer)[0]; + return (u16_val & 0x7f800000) === 0; +} + +/** @returns if number is within subnormal range of f16 */ +export function isSubnormalNumberF16(n: number): boolean { + return n > kValue.f16.negative.max && n < kValue.f16.positive.min; +} + +/** @returns if number is in the finite range of f16 */ +export function isFiniteF16(n: number) { + return n >= kValue.f16.negative.min && n <= kValue.f16.positive.max; +} + +/** + * @returns the next f32 value after |val|, * towards +inf if |dir| is true, otherwise towards -inf. * If |flush| is true, all subnormal values will be flushed to 0, * before processing. * If |flush| is false, the next subnormal will be calculated when appropriate, - * and for -/+0 the nextAfter will be the closest subnormal in the correct + * and for -/+0 the nextAfterF32 will be the closest subnormal in the correct * direction. * val needs to be in [min f32, max f32] */ -export function nextAfter(val: number, dir: boolean = true, flush: boolean): Scalar { +export function nextAfterF32(val: number, dir: boolean = true, flush: boolean): Scalar { if (Number.isNaN(val)) { return f32Bits(kBit.f32.nan.positive.s); } @@ -98,7 +146,7 @@ export function nextAfter(val: number, dir: boolean = true, flush: boolean): Sca `${val} is not in the range of float32` ); - val = flush ? flushSubnormalNumber(val) : val; + val = flush ? flushSubnormalNumberF32(val) : val; // -/+0 === 0 returns true if (val === 0) { @@ -126,9 +174,9 @@ export function nextAfter(val: number, dir: boolean = true, flush: boolean): Sca // Rounding was in the direction requested u32_result = new Uint32Array(new Float32Array([converted]).buffer)[0]; } else { - // Round was opposite of the direction requested, so need nextAfter in the requested direction. + // Round was opposite of the direction requested, so need nextAfterF32 in the requested direction. // This will not recurse since converted is guaranteed to be a float32 due to the conversion above. - const next = nextAfter(converted, dir, flush).value.valueOf() as number; + const next = nextAfterF32(converted, dir, flush).value.valueOf() as number; u32_result = new Uint32Array(new Float32Array([next]).buffer)[0]; } } @@ -143,7 +191,83 @@ export function nextAfter(val: number, dir: boolean = true, flush: boolean): Sca } const f32_result = f32Bits(u32_result); - return flush ? flushSubnormalScalar(f32_result) : f32_result; + return flush ? flushSubnormalScalarF32(f32_result) : f32_result; +} + +/** + * @returns the next f16 value after |val|, + * towards +inf if |dir| is true, otherwise towards -inf. + * If |flush| is true, all subnormal values will be flushed to 0, + * before processing. + * If |flush| is false, the next subnormal will be calculated when appropriate, + * and for -/+0 the nextAfterF16 will be the closest subnormal in the correct + * direction. + * val needs to be in [min f16, max f16] + */ +export function nextAfterF16(val: number, dir: boolean = true, flush: boolean): Scalar { + if (Number.isNaN(val)) { + return f16Bits(kBit.f16.nan.positive.s); + } + + if (val === Number.POSITIVE_INFINITY) { + return f16Bits(kBit.f16.infinity.positive); + } + + if (val === Number.NEGATIVE_INFINITY) { + return f16Bits(kBit.f16.infinity.negative); + } + + assert( + val <= kValue.f16.positive.max && val >= kValue.f16.negative.min, + `${val} is not in the range of float16` + ); + + val = flush ? flushSubnormalNumberF16(val) : val; + + // -/+0 === 0 returns true + if (val === 0) { + if (dir) { + return flush ? f16Bits(kBit.f16.positive.min) : f16Bits(kBit.f16.subnormal.positive.min); + } else { + return flush ? f16Bits(kBit.f16.negative.max) : f16Bits(kBit.f16.subnormal.negative.max); + } + } + + const converted: number = new Float16Array([val])[0]; + let u16_result: number; + if (val === converted) { + // val is expressible precisely as a float16 + u16_result = new Uint16Array(new Float16Array([val]).buffer)[0]; + const is_positive = (u16_result & 0x8000) === 0; + if (dir === is_positive) { + u16_result += 1; + } else { + u16_result -= 1; + } + } else { + // val had to be rounded to be expressed as a float16 + if (dir === converted > val) { + // Rounding was in the direction requested + u16_result = new Uint16Array(new Float16Array([converted]).buffer)[0]; + } else { + // Round was opposite of the direction requested, so need nextAfterF16 in the requested direction. + // This will not recurse since converted is guaranteed to be a float16 due to the conversion above. + const next = nextAfterF16(converted, dir, flush).value.valueOf() as number; + u16_result = new Uint16Array(new Float16Array([next]).buffer)[0]; + } + } + + // Checking for overflow + if ((u16_result & 0x7f800000) === 0x7f800000) { + if (dir) { + return f16Bits(kBit.f16.infinity.positive); + } else { + return f16Bits(kBit.f16.infinity.negative); + } + } + + const f16_result = f16Bits(u16_result); + return flush ? flushSubnormalScalarF16(f16_result) : f16_result; } /** @@ -161,7 +285,7 @@ function oneULPImpl(target: number, flush: boolean): number { return Number.NaN; } - target = flush ? flushSubnormalNumber(target) : target; + target = flush ? flushSubnormalNumberF32(target) : target; // For values at the edge of the range or beyond ulp(x) is defined as the distance between the two nearest // f32 representable numbers to the appropriate edge. @@ -175,8 +299,8 @@ function oneULPImpl(target: number, flush: boolean): number { // before <= x <= after // before =/= after // before and after are f32 representable - const before = nextAfter(target, false, flush).value.valueOf() as number; - const after = nextAfter(target, true, flush).value.valueOf() as number; + const before = nextAfterF32(target, false, flush).value.valueOf() as number; + const after = nextAfterF32(target, true, flush).value.valueOf() as number; const converted: number = new Float32Array([target])[0]; if (converted === target) { // |target| is f32 representable, so either before or after will be x @@ -237,7 +361,7 @@ export function withinULP(val: number, target: number, n: number = 1) { * * TS/JS's number type is internally a f64, so quantization needs to occur when * converting to f32 for WGSL. WGSL does not specify a specific rounding mode, - * so if there if a number is not precisely representable in 32-bits, but in the + * so if a number is not precisely representable in 32-bits, but in the * range, there are two possible valid quantizations. If it is precisely * representable, there is only one valid quantization. This function calculates * the valid roundings and returns them in an array. @@ -271,12 +395,61 @@ export function correctlyRoundedF32(n: number): number[] { } if (converted > n) { - // x_32 rounded towards +inf, so is after x - const other = nextAfter(n_32, false, false).value as number; + // n_32 rounded towards +inf, so is after n + const other = nextAfterF32(n_32, false, false).value as number; + return [other, converted]; + } else { + // n_32 rounded towards -inf, so is before n + const other = nextAfterF32(n_32, true, false).value as number; + return [converted, other]; + } +} + +/** + * Calculate the valid roundings when quantizing to 16-bit floats + * + * TS/JS's number type is internally a f64, so quantization needs to occur when + * converting to f16 for WGSL. WGSL does not specify a specific rounding mode, + * so if a number is not precisely representable in 16-bits, but in the + * range, there are two possible valid quantizations. If it is precisely + * representable, there is only one valid quantization. This function calculates + * the valid roundings and returns them in an array. + * + * This function does not consider flushing mode, so subnormals are maintained. + * The caller is responsible to flushing before and after as appropriate. + * + * Out of range values return the appropriate infinity and edge value. + * + * @param n number to be quantized + * @returns all of the acceptable roundings for quantizing to 16-bits in + * ascending order. + */ +export function correctlyRoundedF16(n: number): number[] { + assert(!Number.isNaN(n), `correctlyRoundedF16 not defined for NaN`); + // Above f16 range + if (n === Number.POSITIVE_INFINITY || n > kValue.f16.positive.max) { + return [kValue.f16.positive.max, Number.POSITIVE_INFINITY]; + } + + // Below f16 range + if (n === Number.NEGATIVE_INFINITY || n < kValue.f16.negative.min) { + return [Number.NEGATIVE_INFINITY, kValue.f16.negative.min]; + } + + const n_16 = new Float16Array([n])[0]; + const converted: number = n_16; + if (n === converted) { + // n is precisely expressible as a f16, so should not be rounded + return [n]; + } + + if (converted > n) { + // n_16 rounded towards +inf, so is after n + const other = nextAfterF16(n_16, false, false).value as number; return [other, converted]; } else { - // x_32 rounded towards -inf, so is before x - const other = nextAfter(n_32, true, false).value as number; + // n_16 rounded towards -inf, so is before n + const other = nextAfterF16(n_16, true, false).value as number; return [converted, other]; } } @@ -520,9 +693,14 @@ export function lcm(a: number, b: number): number { return (a * b) / gcd(a, b); } -/** Converts a 32-bit hex values to a 32-bit float value */ +/** Converts a 32-bit hex value to a 32-bit float value */ export function hexToF32(hex: number): number { - return new Float32Array(new Uint32Array([hex]).buffer)[0]; + return floatBitsToNumber(hex, kFloat32Format); +} + +/** Converts a 16-bit hex value to a 16-bit float value */ +export function hexToF16(hex: number): number { + return floatBitsToNumber(hex, kFloat16Format); } /** Converts two 32-bit hex values to a 64-bit float value */ diff --git a/tsconfig.json b/tsconfig.json index 04bd2b3b176d..2b6090059869 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ /* Output options */ "noEmit": true, /* Strict type-checking options */ - "allowJs": false, + "allowJs": true, "strict": true, /* tsc lint options */ "noImplicitReturns": true, @@ -19,7 +19,10 @@ "esModuleInterop": false, "skipLibCheck": true, }, - "include": ["src/**/*.ts"], + "include": [ + "src/**/*.ts", + "src/external/**/*.js", + ], "typedocOptions": { "entryPointStrategy": "expand", "entryPoints": [