// 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, gl.RGBA, 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); }); } });