onnxruntime/js/web/test/unittests/backends/webgl/test-pack-unpack.ts
Yulong Wang abdc31de40
[js] change default formatter for JavaScript/TypeScript from clang-format to Prettier (#21728)
### Description

See
454996d496
for manual changes (excluded auto-generated formatting changes)

### Why

Because the toolsets for old clang-format is out-of-date. This reduces
the development efficiency.

- The NPM package `clang-format` is already in maintenance mode. not
updated since 2 years ago.
- The VSCode extension for clang-format is not maintained for a while,
and a recent Node.js security update made it not working at all in
Windows.

No one in community seems interested in fixing those.

Choose Prettier as it is the most popular TS/JS formatter.

### How to merge

It's easy to break the build:
- Be careful of any new commits on main not included in this PR.
- Be careful that after this PR is merged, other PRs that already passed
CI can merge.

So, make sure there is no new commits before merging this one, and
invalidate js PRs that already passed CI, force them to merge to latest.
2024-08-14 16:51:22 -07:00

431 lines
17 KiB
TypeScript

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import { expect } from 'chai';
import { Backend, InferenceHandler, resolveBackend, SessionHandler } from '../../../../lib/onnxjs/backend';
import { WebGLInferenceHandler } from '../../../../lib/onnxjs/backends/webgl/inference-handler';
import { createPackProgramInfoLoader } from '../../../../lib/onnxjs/backends/webgl/ops/pack';
import { createUnpackProgramInfoLoader } from '../../../../lib/onnxjs/backends/webgl/ops/unpack';
import { createTextureLayoutFromShape } from '../../../../lib/onnxjs/backends/webgl/texture-layout';
import { Profiler } from '../../../../lib/onnxjs/instrument';
import { Tensor } from '../../../../lib/onnxjs/tensor';
import { ShapeUtil } from '../../../../lib/onnxjs/util';
import {
createArrayFromTexture,
createAscendingArray,
createTextureFromArray,
generateExpected,
getExpectedElementCount,
} from './test-utils';
interface TestData {
elementCount: number;
inputShape: number[];
outputShape: number[];
inputTextureShape: number[];
outputTextureShape: number[];
rawData?: Float32Array;
useGeneratedOutput?: boolean;
}
function getTestData(isPacked = true): TestData[] {
if (isPacked) {
return [
// test scalar
{ elementCount: 1, inputShape: [], outputShape: [], inputTextureShape: [], outputTextureShape: [1, 1] },
// test 1D tensor
{ elementCount: 1, inputShape: [1], outputShape: [], inputTextureShape: [], outputTextureShape: [1, 1] },
{ elementCount: 16, inputShape: [16], outputShape: [], inputTextureShape: [], outputTextureShape: [1, 8] },
{ elementCount: 9, inputShape: [9], outputShape: [], inputTextureShape: [], outputTextureShape: [1, 5] },
// test 2D tensor
{ elementCount: 1, inputShape: [1, 1], outputShape: [], inputTextureShape: [], outputTextureShape: [1, 1] },
{ elementCount: 16, inputShape: [4, 4], outputShape: [], inputTextureShape: [], outputTextureShape: [2, 2] },
{ elementCount: 16, inputShape: [2, 8], outputShape: [], inputTextureShape: [], outputTextureShape: [1, 4] },
{ elementCount: 16, inputShape: [8, 2], outputShape: [], inputTextureShape: [], outputTextureShape: [4, 1] },
{ elementCount: 15, inputShape: [3, 5], outputShape: [], inputTextureShape: [], outputTextureShape: [2, 3] },
{ elementCount: 18, inputShape: [3, 6], outputShape: [], inputTextureShape: [], outputTextureShape: [2, 3] },
{ elementCount: 10, inputShape: [2, 5], outputShape: [], inputTextureShape: [], outputTextureShape: [1, 3] },
{ elementCount: 6, inputShape: [1, 6], outputShape: [], inputTextureShape: [], outputTextureShape: [1, 3] },
{ elementCount: 6, inputShape: [6, 1], outputShape: [], inputTextureShape: [], outputTextureShape: [3, 1] },
{ elementCount: 5, inputShape: [5, 1], outputShape: [], inputTextureShape: [], outputTextureShape: [3, 1] },
{ elementCount: 5, inputShape: [1, 5], outputShape: [], inputTextureShape: [], outputTextureShape: [1, 3] },
// test 3D tensor
{ elementCount: 1, inputShape: [1, 1, 1], outputShape: [], inputTextureShape: [], outputTextureShape: [1, 1] },
{ elementCount: 16, inputShape: [2, 2, 4], outputShape: [], inputTextureShape: [], outputTextureShape: [2, 2] },
{ elementCount: 24, inputShape: [2, 3, 4], outputShape: [], inputTextureShape: [], outputTextureShape: [4, 2] },
{ elementCount: 30, inputShape: [5, 3, 2], outputShape: [], inputTextureShape: [], outputTextureShape: [10, 1] },
{ elementCount: 9, inputShape: [1, 3, 3], outputShape: [], inputTextureShape: [], outputTextureShape: [2, 2] },
{ elementCount: 8, inputShape: [1, 4, 2], outputShape: [], inputTextureShape: [], outputTextureShape: [2, 1] },
{ elementCount: 8, inputShape: [4, 2, 1], outputShape: [], inputTextureShape: [], outputTextureShape: [4, 1] },
{ elementCount: 8, inputShape: [4, 1, 2], outputShape: [], inputTextureShape: [], outputTextureShape: [4, 1] },
// test 4D tensor
{ elementCount: 1, inputShape: [1, 1, 1, 1], outputShape: [], inputTextureShape: [], outputTextureShape: [1, 1] },
{
elementCount: 15,
inputShape: [1, 1, 3, 5],
outputShape: [],
inputTextureShape: [],
outputTextureShape: [2, 3],
},
{
elementCount: 16,
inputShape: [1, 2, 2, 4],
outputShape: [],
inputTextureShape: [],
outputTextureShape: [2, 2],
},
{
elementCount: 32,
inputShape: [2, 2, 2, 4],
outputShape: [],
inputTextureShape: [],
outputTextureShape: [4, 2],
},
{
elementCount: 36,
inputShape: [2, 2, 3, 3],
outputShape: [],
inputTextureShape: [],
outputTextureShape: [8, 2],
},
{
elementCount: 80,
inputShape: [2, 5, 2, 4],
outputShape: [],
inputTextureShape: [],
outputTextureShape: [10, 2],
},
{
elementCount: 12,
inputShape: [2, 1, 3, 2],
outputShape: [],
inputTextureShape: [],
outputTextureShape: [4, 1],
},
{ elementCount: 8, inputShape: [4, 1, 1, 2], outputShape: [], inputTextureShape: [], outputTextureShape: [4, 1] },
{
elementCount: 3840,
inputShape: [1, 1, 48, 80],
outputShape: [],
inputTextureShape: [],
outputTextureShape: [24, 40],
},
// test 6D tensor
{
elementCount: 32,
inputShape: [1, 1, 2, 2, 2, 4],
outputShape: [],
inputTextureShape: [],
outputTextureShape: [4, 2],
},
{
elementCount: 3840,
inputShape: [1, 1, 2, 24, 2, 40],
outputShape: [],
inputTextureShape: [],
outputTextureShape: [48, 20],
},
];
} else {
return [
// test 1D tensor
{
elementCount: 8,
inputShape: [8],
outputShape: [8],
inputTextureShape: [4, 1],
outputTextureShape: [1, 8],
rawData: new Float32Array([1, 2, 0, 0, 3, 4, 0, 0, 5, 6, 0, 0, 7, 8, 0, 0]),
},
{
elementCount: 18,
inputShape: [3, 6],
outputShape: [3, 6],
inputTextureShape: [3, 2],
outputTextureShape: [3, 6],
rawData: new Float32Array([1, 2, 7, 8, 3, 4, 9, 10, 5, 6, 11, 12, 13, 14, 0, 0, 15, 16, 0, 0, 17, 18, 0, 0]),
},
// // test 2D tensor
{
elementCount: 16,
inputShape: [4, 4],
outputShape: [4, 4],
inputTextureShape: [2, 2],
outputTextureShape: [4, 4],
useGeneratedOutput: true,
},
{
elementCount: 8,
inputShape: [2, 4],
outputShape: [2, 4],
inputTextureShape: [2, 1],
outputTextureShape: [2, 4],
useGeneratedOutput: true,
},
{
elementCount: 6,
inputShape: [2, 3],
outputShape: [2, 3],
inputTextureShape: [2, 1],
outputTextureShape: [2, 3],
rawData: new Float32Array([1, 2, 4, 5, 3, 0, 6, 0]),
},
// // test 3d tensor
{
elementCount: 16,
inputShape: [2, 2, 4],
outputShape: [2, 2, 4],
inputTextureShape: [2, 2],
outputTextureShape: [4, 4],
useGeneratedOutput: true,
},
{
elementCount: 24,
inputShape: [2, 3, 4],
outputShape: [2, 3, 4],
inputTextureShape: [2, 4],
outputTextureShape: [6, 4],
rawData: new Float32Array([
1, 2, 5, 6, 3, 4, 7, 8, 9, 10, 0, 0, 11, 12, 0, 0, 13, 14, 17, 18, 15, 16, 19, 20, 21, 22, 0, 0, 23, 24, 0, 0,
]),
},
// test 4d tensor
{
elementCount: 32,
inputShape: [2, 2, 2, 4],
outputShape: [2, 2, 2, 4],
inputTextureShape: [2, 4],
outputTextureShape: [8, 4],
useGeneratedOutput: true,
},
{
elementCount: 64,
inputShape: [2, 2, 4, 4],
outputShape: [2, 2, 4, 4],
inputTextureShape: [2, 8],
outputTextureShape: [16, 4],
useGeneratedOutput: true,
},
// test 6d tensor
{
elementCount: 32,
inputShape: [1, 1, 2, 2, 2, 4],
outputShape: [1, 1, 2, 2, 2, 4],
inputTextureShape: [2, 4],
outputTextureShape: [8, 4],
useGeneratedOutput: true,
},
{
elementCount: 64,
inputShape: [1, 2, 1, 2, 4, 4],
outputShape: [1, 2, 1, 2, 4, 4],
inputTextureShape: [2, 8],
outputTextureShape: [16, 4],
useGeneratedOutput: true,
},
];
}
}
let backend: Backend | undefined;
let sessionhandler: SessionHandler | undefined;
let inferenceHandler: InferenceHandler | undefined;
describe('#UnitTest# - pack - Tensor pack', () => {
before('Initialize Context', async () => {
const profiler = Profiler.create();
backend = await resolveBackend('webgl');
sessionhandler = backend!.createSessionHandler({ profiler });
inferenceHandler = sessionhandler.createInferenceHandler();
});
const testDataSet = getTestData();
// iterate through different input texture layout.
// 'hw-reverted' is the new texture layout all packed kernels use
// 'hw-unreverted' is the old texture layout existing unpacked kernels use
// before we unify those two texture layout, pack kernel should be able to handle
// both texture layout correctly
const textureLayout = ['hw-reverted', 'hw-unreverted'];
for (let w = 0; w < textureLayout.length; ++w) {
for (let k = 0; k < testDataSet.length; ++k) {
const testData = testDataSet[k];
describe('Test pack', () => {});
it(`Test pack kernal ${textureLayout[w]} ${JSON.stringify(testData)}`, () => {
const webglInferenceHandler = inferenceHandler as WebGLInferenceHandler;
const elementCount = testData.elementCount;
const inputData = createAscendingArray(elementCount);
const inputTensorShape = testData.inputShape;
const outputTextureShape = testData.outputTextureShape;
const inputTensor = new Tensor(inputTensorShape, 'float32', undefined, undefined, inputData);
// test old texture layout with width and height not inverted
if (w === 1) {
console.log('Testing unreverted HW input texture');
// use inputTensorShape to create a texture layout that is unpacked(channel === 1)&& hw unreverted.
const inputUnpackedLayout = createTextureLayoutFromShape(
webglInferenceHandler.session.layoutStrategy,
inputTensorShape,
);
// create texture data from the layout. The texture data is cached inside inference handler such that
// when pack kernel is invoked, it will read this texture data from cache instead of creating it from
// scratch
webglInferenceHandler.createTextureDataFromLayoutBindTensor(
inputUnpackedLayout,
inputTensor.type,
inputTensor.numberData,
inputTensor,
);
}
// compile shader code
const programInfo = createPackProgramInfoLoader(inferenceHandler! as WebGLInferenceHandler, inputTensor);
// run kernal and get output
const resultTextureData = webglInferenceHandler.executeProgram(programInfo, [inputTensor]);
const gl = webglInferenceHandler.session.textureManager.glContext.gl;
const resultDataBuffer = createArrayFromTexture(
gl,
resultTextureData.texture,
outputTextureShape[1],
outputTextureShape[0],
);
expect(resultDataBuffer).to.not.equal(null);
const outputElementCount = getExpectedElementCount(testData.inputShape);
expect(resultDataBuffer).to.have.lengthOf(outputElementCount);
const expectedOutput = generateExpected(inputData, testData.inputShape);
expect(resultDataBuffer).to.deep.equal(expectedOutput);
});
}
}
});
describe('#UnitTest# - unpack - Tensor unpack', () => {
before('Initialize Context', async () => {
const profiler = Profiler.create();
backend = await resolveBackend('webgl');
sessionhandler = backend!.createSessionHandler({ profiler });
inferenceHandler = sessionhandler.createInferenceHandler();
});
const testDataSet = getTestData(false);
for (let k = 0; k < testDataSet.length; ++k) {
const testData = testDataSet[k];
describe(`Test unpack ${JSON.stringify(testData)}`, () => {});
it(`Test unpack kernal ${testData.inputShape}`, () => {
const webglInferenceHandler = inferenceHandler as WebGLInferenceHandler;
const elementCount = testData.elementCount;
const inputTensorShape = testData.inputShape;
const inputTextureShape = testData.inputTextureShape;
const outputTensorShape = testData.outputShape;
// create input data and tensor. The input data will be used to verify if the output tensor contains the
// same value but possibly different order depending on our packing algorithm.
const inputData = createAscendingArray(elementCount);
const inputTensor = new Tensor(inputTensorShape, 'float32', undefined, undefined, inputData);
// manually creat packed texture from inputTensor, and insert in cache
const gl = webglInferenceHandler.session.textureManager.glContext.gl;
webglInferenceHandler.session.textureManager.glContext.checkError();
const webglTexture = createTextureFromArray(
webglInferenceHandler.session.textureManager.glContext,
testData.rawData ? testData.rawData : inputData,
inputTextureShape[0],
inputTextureShape[1],
);
webglInferenceHandler.session.textureManager.glContext.checkError();
const packedShape = inputTextureShape;
const textureData = {
width: inputTextureShape[0],
height: inputTextureShape[1],
channels: 4 as const,
isPacked: true,
shape: packedShape,
strides: ShapeUtil.computeStrides(packedShape),
unpackedShape: outputTensorShape,
tensor: inputTensor,
texture: webglTexture!,
};
webglInferenceHandler.setTextureData(inputTensor.dataId, textureData, true);
// compile shader code
const programInfo = createUnpackProgramInfoLoader(inferenceHandler! as WebGLInferenceHandler, inputTensor);
// run kernal and get output
const resultTextureData = webglInferenceHandler.executeProgram(programInfo, [inputTensor]);
const result = resultTextureData.tensor.data;
const resultDataBuffer = createArrayFromTexture(gl, webglTexture!, inputTextureShape[0], inputTextureShape[1]);
webglInferenceHandler.session.textureManager.glContext.checkError();
// verify result.
const expectedOutput = testData.useGeneratedOutput ? generateExpected(inputData, testData.inputShape) : inputData;
expect(result).to.not.equal(null);
expect(result).to.have.lengthOf(elementCount);
expect(resultDataBuffer).to.deep.equal(testData.rawData ? testData.rawData : inputData);
const outputElementCount = getExpectedElementCount(testData.inputShape);
expect(resultDataBuffer).to.have.lengthOf(outputElementCount);
expect(result).to.deep.equal(expectedOutput);
});
}
});
describe('#UnitTest# - pack-unpack round trip', () => {
before('Initialize Context', async () => {
const profiler = Profiler.create();
backend = await resolveBackend('webgl');
sessionhandler = backend!.createSessionHandler({ profiler });
inferenceHandler = sessionhandler.createInferenceHandler();
});
const testDataSet = getTestData();
for (let k = 0; k < testDataSet.length; ++k) {
const testData = testDataSet[k];
describe(`Test pack-unpack ${JSON.stringify(testData)}`, () => {});
it(`Test pack-unpack round trip ${JSON.stringify(testData)}`, () => {
const webglInferenceHandler = inferenceHandler as WebGLInferenceHandler;
const elementCount = testData.elementCount;
const inputData = createAscendingArray(elementCount);
const inputTensorShape = testData.inputShape;
const inputTensor = new Tensor(inputTensorShape, 'float32', undefined, undefined, inputData);
// compile pack shader code
const packProgramInfo = createPackProgramInfoLoader(inferenceHandler! as WebGLInferenceHandler, inputTensor);
const packResultData = webglInferenceHandler.executeProgram(packProgramInfo, [inputTensor]);
// create unpack kernel
// compile unpack shader code
const unpackProgramInfo = createPackProgramInfoLoader(
inferenceHandler! as WebGLInferenceHandler,
packResultData.tensor,
);
// run unpack kernal and get output
const unpackResultData = webglInferenceHandler.executeProgram(unpackProgramInfo, [inputTensor]);
const resultData = unpackResultData.tensor.data;
expect(resultData).to.not.equal(null);
expect(resultData).to.have.lengthOf(testData.elementCount);
expect(unpackResultData.tensor.data).to.deep.equal(inputTensor.data);
});
}
});