From 290825d28c1308f38ea1b0667d2ebf698f9ea3f8 Mon Sep 17 00:00:00 2001 From: Du Li Date: Mon, 26 Apr 2021 21:27:55 -0700 Subject: [PATCH 1/3] bug fixing (#289) --- lib/backends/webgl/ops/conv-pack.ts | 13 ++++++------- lib/backends/webgl/ops/conv.ts | 17 +++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/backends/webgl/ops/conv-pack.ts b/lib/backends/webgl/ops/conv-pack.ts index b08104c7..414bf64d 100644 --- a/lib/backends/webgl/ops/conv-pack.ts +++ b/lib/backends/webgl/ops/conv-pack.ts @@ -38,8 +38,8 @@ export class WebGLConvPacked extends Conv { const matmul = new WebGLMatMulPacked(); const reshape = new WebGLReshapePacked(); // shape for kernel reshape - const shape = new Tensor( - [2], 'int32', undefined, undefined, new Float32Array([kshape[0], kshape[1] * kshape[2] * kshape[3]])); + const shape = + new Tensor([2], 'int32', undefined, undefined, new Int32Array([kshape[0], kshape[1] * kshape[2] * kshape[3]])); if (!this.artifacts) { this.artifacts = []; this.programInfo = []; @@ -78,12 +78,11 @@ export class WebGLConvPacked extends Conv { const matmulOutput = runDataMatmul.outputTextureData.tensor; // reshape output - const outputShapeTensor = new Tensor([outputShape.length], 'int32'); + const outputShapeTensor = new Tensor( + [outputShape.length], 'int32', undefined, undefined, + new Int32Array([outputShape[0], outputShape[1], outputShape[2], outputShape[3]])); - for (let i = 0; i < outputShape.length; i++) { - outputShapeTensor.data[i] = outputShape[i]; - } - assert(this.artifacts.length > 1, () => 'expect at least 3 artifacts created'); + assert(this.artifacts.length > 2, () => 'expect at least 3 artifacts created'); if (this.artifacts.length === 3) { this.programInfo[3] = reshape.createProgramInfo(inferenceHandler, [matmulOutput, outputShapeTensor]); this.artifacts[3] = programManager.build(this.programInfo[3]); diff --git a/lib/backends/webgl/ops/conv.ts b/lib/backends/webgl/ops/conv.ts index 62ec3509..0a99a62d 100644 --- a/lib/backends/webgl/ops/conv.ts +++ b/lib/backends/webgl/ops/conv.ts @@ -146,19 +146,19 @@ export class WebGLUnpackedConv extends Conv { const programManager = inferenceHandler.session.programManager; if (!this.artifacts) { this.artifacts = []; - const programInfos = this.createProgramInfo(inferenceHandler, inputs); + const programInfos = this.createProgramInfoArray(inferenceHandler, inputs); for (let i = 0; i < programInfos.length; ++i) { const artifact = inferenceHandler.session.programManager.build(programInfos[i]); this.artifacts.push(artifact); } } - const runDatas = this.createRunData(inferenceHandler, this.artifacts.map(a => a.programInfo), inputs); - inferenceHandler.checkAndUpdateTextureForm(this.artifacts[0], runDatas[0]); - programManager.run(this.artifacts[0], runDatas[0]); - programManager.run(this.artifacts[1], runDatas[1]); - return [runDatas[1].outputTextureData.tensor]; + const runDataArray = this.createRunDataArray(inferenceHandler, this.artifacts.map(a => a.programInfo), inputs); + inferenceHandler.checkAndUpdateTextureForm(this.artifacts[0], runDataArray[0]); + programManager.run(this.artifacts[0], runDataArray[0]); + programManager.run(this.artifacts[1], runDataArray[1]); + return [runDataArray[1].outputTextureData.tensor]; } - createProgramInfo(inferenceHandler: WebGLInferenceHandler, inputs: Tensor[]): ProgramInfo[] { + createProgramInfoArray(inferenceHandler: WebGLInferenceHandler, inputs: Tensor[]): ProgramInfo[] { const xshape = inputs[0].dims.slice(); const kshape = inputs[1].dims.slice(); // if kernelShape is not specified in the attributes of this op, infer it from the weight tensor dims @@ -180,7 +180,8 @@ export class WebGLUnpackedConv extends Conv { this.createDotProductProgramInfo(inferenceHandler, im2colProgramInfo.outputLayout, inputs, outputShape); return [im2colProgramInfo, dotProductProgramInfo]; } - createRunData(inferenceHandler: WebGLInferenceHandler, programInfos: ProgramInfo[], inputs: Tensor[]): RunData[] { + createRunDataArray(inferenceHandler: WebGLInferenceHandler, programInfos: ProgramInfo[], inputs: Tensor[]): + RunData[] { const k = inputs[1]; const b = inputs.length >= 3 ? inputs[2] : undefined; let kTD = inferenceHandler.getTextureData(k.dataId); From 0c998cc45e7d53109dcf20a1abd893a47650819c Mon Sep 17 00:00:00 2001 From: Xueyun Zhu <40807589+xzhu1900@users.noreply.github.com> Date: Tue, 27 Apr 2021 18:57:29 -0700 Subject: [PATCH 2/3] add texture cache support for pack/unpack mode (#290) * squash change * uncomment test verification * clean up * clean up and remove unused files * adding comment * remove unused code * fix reshape merge * add guid as tensor id * add file * fix test failure --- lib/backends/webgl/glsl-coordinate-lib.ts | 25 +++- lib/backends/webgl/inference-handler.ts | 114 ++++++++++-------- lib/backends/webgl/ops/binary-op.ts | 10 +- lib/backends/webgl/ops/reshape-packed.ts | 9 +- lib/backends/webgl/ops/reshape.ts | 2 +- lib/backends/webgl/ops/uint8-encode.ts | 2 +- lib/backends/webgl/ops/unpack.ts | 7 +- lib/backends/webgl/program-manager.ts | 7 +- lib/backends/webgl/session-handler.ts | 35 ++++-- lib/backends/webgl/texture-layout-strategy.ts | 1 - lib/backends/webgl/types.ts | 2 + lib/tensor.ts | 16 +-- package-lock.json | 36 +++++- package.json | 1 + .../backends/webgl/test_pack_unpack.ts | 2 +- 15 files changed, 192 insertions(+), 77 deletions(-) diff --git a/lib/backends/webgl/glsl-coordinate-lib.ts b/lib/backends/webgl/glsl-coordinate-lib.ts index c82edfcd..b0adbe61 100644 --- a/lib/backends/webgl/glsl-coordinate-lib.ts +++ b/lib/backends/webgl/glsl-coordinate-lib.ts @@ -690,7 +690,7 @@ export class CoordsGlslLib extends GlslLib { return ${texFuncSnippet}(${unpackedCoordsSnippet}); } `; - return new GlslLibRoutine(source); + return new GlslLibRoutine(source, ['coordinates.getOutputCoords']); } /** @@ -1216,4 +1216,27 @@ export class CoordsGlslLib extends GlslLib { } `; } + + /** + * Produces a packed value getter function for the name and rank given + * If a transpose is set proper offsetToCoords mapping will be used + * @param name name of the function + * @param rank rank of the input + * @param transpose whether or not should generate a transpose variation + */ + protected getPackedValueFrom(varName: string, rank: number, width: number, height: number, transpose: boolean): + string { + let name = `_${varName}_Pack`; + if (transpose) { + name = name + '_T'; + } + const glsl = getGlsl(this.context.glContext.version); + return ` + vec4 ${name}(int m[${rank}]) { + int offset = indicesToOffset_${varName}(m); + vec2 coords = offsetToCoords(offset, ${width}, ${height}); + return ${glsl.texture2D}(${varName}, coords); + } + `; + } } diff --git a/lib/backends/webgl/inference-handler.ts b/lib/backends/webgl/inference-handler.ts index 163eeb46..b7b1bbe5 100644 --- a/lib/backends/webgl/inference-handler.ts +++ b/lib/backends/webgl/inference-handler.ts @@ -16,9 +16,16 @@ import {Artifact, RunData, TextureData, TextureLayout, WebGLOperator} from './ty import {getPackedShape} from './utils'; export class WebGLInferenceHandler implements InferenceHandler { - private textureDataCache: Map; + private packedTextureDataCache: Map; + private unpackedTextureDataCache: Map; + private pack2unpackMap: Map; + private unpack2packMap: Map; constructor(public session: WebGLSessionHandler) { - this.textureDataCache = new Map(); + this.packedTextureDataCache = new Map(); + this.unpackedTextureDataCache = new Map(); + + this.pack2unpackMap = new Map(); + this.unpack2packMap = new Map(); } run(op: WebGLOperator, inputs: Tensor[]): Tensor[] { @@ -33,36 +40,18 @@ export class WebGLInferenceHandler implements InferenceHandler { return [runData.outputTextureData.tensor]; } - /** - * Check the runData's input texture mode with the program's artifact. - * If the artifact expects a packed input, while the RunData's input - * is unpacked, perform a pack operation on this input to align the - * texture mode with artifact. Similar on unpacked input. - */ checkAndUpdateTextureForm(artifact: Artifact, runData: RunData) { // pack/unpack inputs - runData.inputTextureDatas.forEach(input => { + for (let i = 0; i < runData.inputTextureDatas.length; ++i) { + const input = runData.inputTextureDatas[i]; if (input.isPacked && !artifact.programInfo.expectPackedInputs) { - // unpack this input - const unpacked = this.unpack(input); - input.height = unpacked.height; - input.isPacked = unpacked.isPacked; - input.texture = unpacked.texture; - input.width = unpacked.width; - + runData.inputTextureDatas[i] = this.unpack(input); } else if (!input.isPacked && artifact.programInfo.expectPackedInputs) { - // pack this input - const packed = this.pack(input); - input.height = packed.height; - input.isPacked = packed.isPacked; - input.texture = packed.texture; - input.width = packed.width; + runData.inputTextureDatas[i] = this.pack(input); } - }); + } } runProgram(artifact: Artifact, runData: RunData) { - // if the runData has different expected texture pack/unpack mode, process pack/unpack - // operation on the texture before executing the kernel. this.checkAndUpdateTextureForm(artifact, runData); // output should match @@ -84,15 +73,28 @@ export class WebGLInferenceHandler implements InferenceHandler { * Creates a texture data object associated with the given tensor. * @param tensor the tensor with data to upload */ - getOrCreateTextureData(tensor: Tensor, layout?: TextureLayout) { - let td = this.getTextureData(tensor.dataId); + getOrCreateTextureData(tensor: Tensor, layout?: TextureLayout, isPacked = false) { + let td = this.getTextureData(tensor.dataId, isPacked); if (!td) { Logger.verbose('InferenceHandler', `Creating new TextureData for dims: [${tensor.dims}]`); if (!layout) { layout = this.createTextureLayoutFromShape(tensor.dims.slice()); } - // graph inputs or initializers - td = this.createTextureData(layout, tensor.type, tensor.numberData, tensor, Encoder.Usage.UploadOnly); + // if we don't find the texture data with specific pack mode in the cache, try with the different + // pack mode to see if the tensor is cached using that pack mode. If succeed, we can return this + // tensor data and later apply a pack/unpack op on this texture, no need to create a new one here. + td = this.getTextureData(tensor.dataId, !isPacked); + if (!td) { + if (isPacked) { + const unpackedTextureLayout = this.getOrCreateTextureLayout(tensor, 1, false, [], true); + const unpackedTextureData = this.createTextureData( + unpackedTextureLayout, tensor.type, tensor.numberData, tensor, Encoder.Usage.UploadOnly); + td = this.pack(unpackedTextureData); + } else { + td = this.createTextureData( + layout, tensor.type, tensor.numberData, tensor, Encoder.Usage.UploadOnly, isPacked); + } + } } else { Logger.verbose('InferenceHandler', `Retrieving TextureData from cache: [${tensor.dims}]`); } @@ -104,7 +106,7 @@ export class WebGLInferenceHandler implements InferenceHandler { * Usage = Encoder.Usage.Default. * @param dataType the tensor data type */ - createTextureDataFromLayout(layout: TextureLayout, dataType: Tensor.DataType): TextureData { + createTextureDataFromLayout(layout: TextureLayout, dataType: Tensor.DataType, isPacked = false): TextureData { return this.createTextureData(layout, dataType); } @@ -118,13 +120,14 @@ export class WebGLInferenceHandler implements InferenceHandler { * @param tensor the tensor to bind. tensor's data is ignored. */ createTextureDataFromLayoutBindTensor( - layout: TextureLayout, dataType: Tensor.DataType, data: Tensor.NumberType, tensor: Tensor): TextureData { - return this.createTextureData(layout, dataType, data, tensor, Encoder.Usage.UploadOnly); + layout: TextureLayout, dataType: Tensor.DataType, data: Tensor.NumberType, tensor: Tensor, + isPacked = false): TextureData { + return this.createTextureData(layout, dataType, data, tensor, Encoder.Usage.UploadOnly, isPacked); } private createTextureData( layout: TextureLayout, dataType: Tensor.DataType, data?: Tensor.NumberType, tensor?: Tensor, - usage?: Encoder.Usage): TextureData { + usage?: Encoder.Usage, isPacked = false): TextureData { Logger.verbose('InferenceHandler', `Creating TextureData: layout:[${JSON.stringify(layout)}]`); const texture = this.session.textureManager.createTextureFromLayout(dataType, layout, data, usage); return this.createTextureDataFromTexture(layout, dataType, texture, tensor); @@ -137,8 +140,9 @@ export class WebGLInferenceHandler implements InferenceHandler { * @param texture the WebGLTexture object to share * @param tensorId the tensor ID of the shared tensor data */ - createSharedTextureData(layout: TextureLayout, dataType: Tensor.DataType, texture: WebGLTexture, tensorId: Tensor.Id): - TextureData { + createSharedTextureData( + layout: TextureLayout, dataType: Tensor.DataType, texture: WebGLTexture, tensorId?: Tensor.Id, + isPacked = false): TextureData { return this.createTextureDataFromTexture(layout, dataType, texture, undefined, tensorId); } @@ -155,29 +159,32 @@ export class WebGLInferenceHandler implements InferenceHandler { undefined, undefined, tensorId), texture }; - this.setTextureData(textureData.tensor.dataId, textureData); + this.setTextureData(textureData.tensor.dataId, textureData, layout.isPacked); return textureData; } - getTextureData(tensorId: Tensor.Id): TextureData|undefined { - return this.session.isInitializer(tensorId) ? this.session.getTextureData(tensorId) : - this.textureDataCache.get(tensorId); + getTextureData(tensorId: Tensor.Id, isPacked = false): TextureData|undefined { + return this.session.isInitializer(tensorId) ? + this.session.getTextureData(tensorId, isPacked) : + isPacked ? this.packedTextureDataCache.get(tensorId) : this.unpackedTextureDataCache.get(tensorId); } - setTextureData(tensorId: Tensor.Id, td: TextureData): void { + setTextureData(tensorId: Tensor.Id, td: TextureData, isPacked = false): void { if (this.session.isInitializer(tensorId)) { - this.session.setTextureData(tensorId, td); + this.session.setTextureData(tensorId, td, isPacked); } else { - this.textureDataCache.set(tensorId, td); + isPacked ? this.packedTextureDataCache.set(tensorId, td) : this.unpackedTextureDataCache.set(tensorId, td); } } - + isTextureLayoutCached(tensor: Tensor, isPacked = false): boolean { + return !!this.getTextureData(tensor.dataId, isPacked); + } /** * Create a TextureLayout object from a tensor. If a related texture data is found, returns the cached texture layout. */ getOrCreateTextureLayout( tensor: Tensor, channels: 1|4 = 1, isPacked = false, unpackedShape?: ReadonlyArray, reverseWH = false): TextureLayout { - const td = this.getTextureData(tensor.dataId); + const td = this.getTextureData(tensor.dataId, isPacked); if (td) { return td; } @@ -229,14 +236,17 @@ export class WebGLInferenceHandler implements InferenceHandler { isPacked, shape: inferredDims, strides: ShapeUtil.computeStrides(inferredDims), - unpackedShape + unpackedShape, + reversedWH: (prefs && prefs.reverseWH) }; } dispose(): void { this.session.textureManager.clearActiveTextures(); - this.textureDataCache.forEach(td => this.session.textureManager.releaseTexture(td)); - this.textureDataCache = new Map(); + this.packedTextureDataCache.forEach(td => this.session.textureManager.releaseTexture(td)); + this.packedTextureDataCache = new Map(); + this.unpackedTextureDataCache.forEach(td => this.session.textureManager.releaseTexture(td)); + this.unpackedTextureDataCache = new Map(); } readTexture(textureData: TextureData): Tensor.NumberType { @@ -252,6 +262,10 @@ export class WebGLInferenceHandler implements InferenceHandler { } pack(input: TextureData): TextureData { + const cachedId = this.unpack2packMap.get(input.tensor.dataId); + if (cachedId) { + return this.packedTextureDataCache.get(cachedId)!; + } const key = `${input.shape}`; let op = this.session.packOpCache.get(key); if (!op) { @@ -266,10 +280,15 @@ export class WebGLInferenceHandler implements InferenceHandler { } const runData = op.createRunData(this, artifact.programInfo, [input.tensor]); this.runProgram(artifact, runData); + this.unpack2packMap.set(input.tensor.dataId, runData.outputTextureData.tensor.dataId); return runData.outputTextureData; } unpack(input: TextureData): TextureData { + const cachedId = this.pack2unpackMap.get(input.tensor.dataId); + if (cachedId) { + return this.unpackedTextureDataCache.get(cachedId)!; + } // For unpacked kernel, cache it by using input's unpackedShape as cache key. // Note that we need to use input.unpackedShape instead of input.shape here, // as the shape infers the packed texture shape. Different unpackedShape can have the @@ -290,6 +309,7 @@ export class WebGLInferenceHandler implements InferenceHandler { } const runData = op.createRunData(this, artifact.programInfo, [input.tensor]); this.runProgram(artifact, runData); + this.pack2unpackMap.set(input.tensor.dataId, runData.outputTextureData.tensor.dataId); return runData.outputTextureData; } } diff --git a/lib/backends/webgl/ops/binary-op.ts b/lib/backends/webgl/ops/binary-op.ts index 8c010ed5..100afa8b 100644 --- a/lib/backends/webgl/ops/binary-op.ts +++ b/lib/backends/webgl/ops/binary-op.ts @@ -22,6 +22,10 @@ export class WebGLBinaryOp extends BinaryOp implements WebGLOperator { const inputLayouts = handler.session.pack ? inputs.map(t => handler.getOrCreateTextureLayout(t, 4, true, t.dims, true)) : inputs.map(t => handler.getOrCreateTextureLayout(t)); + const ouputLayout = handler.session.pack ? + handler.createTextureLayoutFromShape(inputs[0].dims, 4, inputs[0].dims, {isPacked: true, reverseWH: true}) : + handler.createTextureLayoutFromShape(inputs[0].dims); + const isBroadcast = !ShapeUtil.areEqual(inputs[0].dims, inputs[1].dims); if (isBroadcast) { const outputShape = BroadcastUtil.calcShape(inputs[0].dims, inputs[1].dims, false); @@ -33,6 +37,8 @@ export class WebGLBinaryOp extends BinaryOp implements WebGLOperator { const bRank = inputs[1].dims.length !== 0 ? inputs[1].dims.length : 1; const aBcast = inputs[0].dims.length !== 0 ? `bcastIndices_A(indices, aindices);` : `aindices[0] = 0;`; const bBcast = inputs[1].dims.length !== 0 ? `bcastIndices_B(indices, bindices);` : `bindices[0] = 0;`; + + // TODO: for packed tensors, we need to implement logic to caculate textCoords for broadcast tensor const shaderSource = ` ${this.glslFunc.body} float process(int indices[${outputRank}]) { @@ -51,6 +57,8 @@ export class WebGLBinaryOp extends BinaryOp implements WebGLOperator { outputLayout, samplers: ['A', 'B'], shaderSource, + expectPackedInputs: handler.session.pack, + expectPackedOutputs: handler.session.pack }; } const glsl = getGlsl(handler.session.backend.glContext.version); @@ -67,7 +75,7 @@ export class WebGLBinaryOp extends BinaryOp implements WebGLOperator { return { hasMain: true, inputLayouts, - outputLayout: handler.createTextureLayoutFromShape(inputs[0].dims), + outputLayout: ouputLayout, samplers: ['A', 'B'], shaderSource, expectPackedInputs: true, diff --git a/lib/backends/webgl/ops/reshape-packed.ts b/lib/backends/webgl/ops/reshape-packed.ts index 80fc1126..9cdc6346 100644 --- a/lib/backends/webgl/ops/reshape-packed.ts +++ b/lib/backends/webgl/ops/reshape-packed.ts @@ -117,10 +117,17 @@ export class WebGLReshapePacked extends Reshape implements WebGLOperator { createRunData(handler: WebGLInferenceHandler, programInfo: ProgramInfo, inputs: Tensor[]): RunData { const inputTDs = [handler.getOrCreateTextureData(inputs[0], handler.getOrCreateTextureLayout(inputs[0], 1, false, [], false))]; + let outputLayout = this.originalOutputLayout; + if (outputLayout === undefined) { + const originInputShape = inputs[0].dims; + const outputShape = ShapeUtil.calculateReshapedDims(originInputShape, inputs[1].integerData); + outputLayout = + handler.createTextureLayoutFromShape(outputShape, 4, outputShape, {isPacked: true, reverseWH: true}); + } // return run data for reshape. Here, we use the original calculate outputLayout to create the real output layout. return { inputTextureDatas: inputTDs, - outputTextureData: handler.createTextureDataFromLayout(this.originalOutputLayout, inputTDs[0].tensor.type), + outputTextureData: handler.createTextureDataFromLayout(outputLayout, inputTDs[0].tensor.type), uniformData: {} }; } diff --git a/lib/backends/webgl/ops/reshape.ts b/lib/backends/webgl/ops/reshape.ts index 102970fa..be56a18f 100644 --- a/lib/backends/webgl/ops/reshape.ts +++ b/lib/backends/webgl/ops/reshape.ts @@ -43,6 +43,6 @@ export function reshape( unpackedShape: reshapedDims, }; - const newTextureData = inferenceHandler.createSharedTextureData(newTextureLayout, input.type, inputTD.texture, {}); + const newTextureData = inferenceHandler.createSharedTextureData(newTextureLayout, input.type, inputTD.texture); return newTextureData.tensor; } diff --git a/lib/backends/webgl/ops/uint8-encode.ts b/lib/backends/webgl/ops/uint8-encode.ts index 1e329dbc..104110b0 100644 --- a/lib/backends/webgl/ops/uint8-encode.ts +++ b/lib/backends/webgl/ops/uint8-encode.ts @@ -77,7 +77,7 @@ export class WebGLUint8Encode { const encoder = inferenceHandler.session.backend.glContext.getEncoder('byte', 4); const texture = inferenceHandler.session.backend.glContext.allocateTexture(outputLayout.width, outputLayout.height, encoder); - const outputTextureData = inferenceHandler.createSharedTextureData(outputLayout, 'uint8', texture, {}); + const outputTextureData = inferenceHandler.createSharedTextureData(outputLayout, 'uint8', texture); const runData = {inputTextureDatas: [input], outputTextureData, uniformData: {}}; inferenceHandler.session.programManager.run(artifact, runData); diff --git a/lib/backends/webgl/ops/unpack.ts b/lib/backends/webgl/ops/unpack.ts index 5df72668..6b73482e 100644 --- a/lib/backends/webgl/ops/unpack.ts +++ b/lib/backends/webgl/ops/unpack.ts @@ -6,7 +6,6 @@ import {getGlsl} from '../glsl-source'; import {WebGLInferenceHandler} from '../inference-handler'; import {ProgramInfo, RunData, WebGLOperator} from '../types'; import {getCoordsDataType} from '../utils'; - import {getChannels, unpackFromChannel} from './packing_utils'; export class WebGLUnpack implements WebGLOperator { @@ -18,7 +17,7 @@ export class WebGLUnpack implements WebGLOperator { throw new Error(`Pack kernel should have input tensor count to 1.`); } - const inputTexture = handler.getTextureData(inputs[0].dataId); + const inputTexture = handler.getTextureData(inputs[0].dataId, true); if (!inputTexture) { throw new Error(`packed input texture must exist`); } @@ -49,7 +48,7 @@ export class WebGLUnpack implements WebGLOperator { `; return { - inputLayouts: [handler.getOrCreateTextureLayout(inputs[0])], + inputLayouts: [handler.getOrCreateTextureLayout(inputs[0], 4, true, inputs[0].dims, true)], outputLayout, samplers: ['A'], shaderSource, @@ -59,7 +58,7 @@ export class WebGLUnpack implements WebGLOperator { }; } createRunData(handler: WebGLInferenceHandler, programInfo: ProgramInfo, inputs: Tensor[]): RunData { - const inputTDs = [handler.getOrCreateTextureData(inputs[0], programInfo.inputLayouts[0])]; + const inputTDs = [handler.getOrCreateTextureData(inputs[0], programInfo.inputLayouts[0], true)]; return { inputTextureDatas: inputTDs, outputTextureData: handler.createTextureDataFromLayout(programInfo.outputLayout, inputTDs[0].tensor.type), diff --git a/lib/backends/webgl/program-manager.ts b/lib/backends/webgl/program-manager.ts index 6f4ad68f..36cedbf8 100644 --- a/lib/backends/webgl/program-manager.ts +++ b/lib/backends/webgl/program-manager.ts @@ -105,11 +105,12 @@ ${fragShaderScript} return program; } bindOutput(td: TextureData): void { + const width = td.width; + const height = td.height; Logger.verbose( 'ProrgramManager', - `Binding output texture to Framebuffer: w/h=${td.width}/${td.height}, shape=${td.shape}, type=${ - td.tensor.type}`); - this.glContext.attachFramebuffer(td.texture, td.width, td.height); + `Binding output texture to Framebuffer: w/h=${width}/${height}, shape=${td.shape}, type=${td.tensor.type}`); + this.glContext.attachFramebuffer(td.texture, width, height); } bindAttributes(attribLocations: Artifact.AttribLocations): void { const positionHandle = attribLocations.position; diff --git a/lib/backends/webgl/session-handler.ts b/lib/backends/webgl/session-handler.ts index f73612e8..75e3d692 100644 --- a/lib/backends/webgl/session-handler.ts +++ b/lib/backends/webgl/session-handler.ts @@ -21,7 +21,10 @@ export class WebGLSessionHandler implements SessionHandler { programManager: ProgramManager; textureManager: TextureManager; layoutStrategy: TextureLayoutStrategy; - textureDataCache: Map; + packedTextureDataCache: Map; + unpackedTextureDataCache: Map; + pack2unpackMap: Map; + unpack2packMap: Map; initializers: Set; packOpCache: Map; unpackOpCache: Map; @@ -33,10 +36,13 @@ export class WebGLSessionHandler implements SessionHandler { this.textureManager = new TextureManager( backend.glContext, this.layoutStrategy, this.context.profiler, {reuseTextures: backend.textureCacheMode === 'full'}); - this.textureDataCache = new Map(); + this.packedTextureDataCache = new Map(); + this.unpackedTextureDataCache = new Map(); this.packOpCache = new Map(); this.unpackOpCache = new Map(); this.pack = backend.pack; + this.pack2unpackMap = new Map(); + this.unpack2packMap = new Map(); } createInferenceHandler() { @@ -49,18 +55,31 @@ export class WebGLSessionHandler implements SessionHandler { isInitializer(tensorId: Tensor.Id): boolean { return this.initializers ? this.initializers.has(tensorId) : false; } - getTextureData(tensorId: Tensor.Id): TextureData|undefined { - return this.textureDataCache.get(tensorId); + addInitializer(tensorId: Tensor.Id): void { + this.initializers.add(tensorId); } - setTextureData(tensorId: Tensor.Id, textureData: TextureData): void { + getTextureData(tensorId: Tensor.Id, isPacked: boolean): TextureData|undefined { + if (isPacked) { + return this.packedTextureDataCache.get(tensorId); + } else { + return this.unpackedTextureDataCache.get(tensorId); + } + } + setTextureData(tensorId: Tensor.Id, textureData: TextureData, isPacked = false): void { Logger.verbose('WebGLSessionHandler', 'Storing Texture data in cache'); - this.textureDataCache.set(tensorId, textureData); + if (isPacked) { + this.packedTextureDataCache.set(tensorId, textureData); + } else { + this.unpackedTextureDataCache.set(tensorId, textureData); + } } dispose(): void { this.programManager.dispose(); this.textureManager.clearActiveTextures(); - this.textureDataCache.forEach(td => this.textureManager.releaseTexture(td, true)); - this.textureDataCache = new Map(); + this.packedTextureDataCache.forEach(td => this.textureManager.releaseTexture(td, true)); + this.packedTextureDataCache = new Map(); + this.unpackedTextureDataCache.forEach(td => this.textureManager.releaseTexture(td, true)); + this.unpackedTextureDataCache = new Map(); } resolve(node: Graph.Node, opsets: ReadonlyArray, graph: Graph): Operator { const op = resolveOperator(node, opsets, WEBGL_OP_RESOLVE_RULES); diff --git a/lib/backends/webgl/texture-layout-strategy.ts b/lib/backends/webgl/texture-layout-strategy.ts index 267fe162..c6e4096c 100644 --- a/lib/backends/webgl/texture-layout-strategy.ts +++ b/lib/backends/webgl/texture-layout-strategy.ts @@ -196,7 +196,6 @@ export function parseAxisParam(axis: number|number[], shape: number[]): number[] // Handle negative axis. return axis.map(a => a < 0 ? rank + a : a); } - export function isInt(a: number): boolean { return a % 1 === 0; } diff --git a/lib/backends/webgl/types.ts b/lib/backends/webgl/types.ts index 4e3a2847..4e26e94d 100644 --- a/lib/backends/webgl/types.ts +++ b/lib/backends/webgl/types.ts @@ -42,6 +42,8 @@ export interface TextureLayout { * the original shape(dims) of the corresponding tensor */ unpackedShape: ReadonlyArray; + + reversedWH?: boolean; } export interface TextureData extends TextureLayout { tensor: Tensor; diff --git a/lib/tensor.ts b/lib/tensor.ts index 3dbf5e9c..aadb201b 100644 --- a/lib/tensor.ts +++ b/lib/tensor.ts @@ -1,13 +1,18 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +import {Guid} from 'guid-typescript'; import Long from 'long'; import {onnx} from 'onnx-proto'; + import {onnxruntime} from './ortSchema/ort_generated'; + import ortFbs = onnxruntime.experimental.fbs; import {ProtoUtil, ShapeUtil} from './util'; +export let globalId = 0; + export declare namespace Tensor { export interface DataTypeMap { bool: Uint8Array; @@ -31,16 +36,13 @@ export declare namespace Tensor { export type FloatType = Tensor.DataTypeMap['float32']|Tensor.DataTypeMap['float64']; export type NumberType = BooleanType|IntegerType|FloatType; - export interface Id { - // this field helps typescript to perform type check, comparing to use `Id` as an alias of object. - _tensorDataId_unused?: never; - } + export type Id = Guid; } type TensorData = Tensor.DataTypeMap[Tensor.DataType]; -type DataProvider = (id: Tensor.Id) => TensorData; -type AsyncDataProvider = (id: Tensor.Id) => Promise; +type DataProvider = (id: Guid) => TensorData; +type AsyncDataProvider = (id: Guid) => Promise; export class Tensor { /** @@ -169,7 +171,7 @@ export class Tensor { /** * get the data ID that used to map to a tensor data */ - public readonly dataId: Tensor.Id = {}) { + public readonly dataId: Guid = Guid.create()) { this.size = ShapeUtil.validateDimsAndCalcSize(dims); const size = this.size; const empty = (dataProvider === undefined && asyncDataProvider === undefined && cache === undefined); diff --git a/package-lock.json b/package-lock.json index dd397db2..9cfccf2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -788,6 +788,16 @@ "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "bl": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", @@ -2469,6 +2479,13 @@ "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", "dev": true }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "filename-reserved-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", @@ -3076,6 +3093,12 @@ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, + "guid-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", + "dev": true + }, "hard-source-webpack-plugin": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/hard-source-webpack-plugin/-/hard-source-webpack-plugin-0.13.1.tgz", @@ -4766,6 +4789,13 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "dev": true, + "optional": true + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -7265,7 +7295,11 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", diff --git a/package.json b/package.json index 41f0d785..bedaaf51 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "electron": "^9.2.0", "fs-extra": "^9.0.1", "globby": "^11.0.1", + "guid-typescript": "^1.0.9", "hard-source-webpack-plugin": "^0.13.1", "husky": "^4.2.5", "karma": "^5.1.1", diff --git a/test/unittests/backends/webgl/test_pack_unpack.ts b/test/unittests/backends/webgl/test_pack_unpack.ts index b6baa423..cee318ed 100644 --- a/test/unittests/backends/webgl/test_pack_unpack.ts +++ b/test/unittests/backends/webgl/test_pack_unpack.ts @@ -146,7 +146,7 @@ describe('#UnitTest# - unpack - Tensor unpack', () => { texture: webglTexture! }; - webglInferenceHandler.setTextureData(inputTensor.dataId, textureData); + webglInferenceHandler.setTextureData(inputTensor.dataId, textureData, true); // compile shader code const programInfo = op.createProgramInfo(inferenceHandler! as WebGLInferenceHandler, [inputTensor]); From 94d384344beb158dd3928a040672d9b58442e1f3 Mon Sep 17 00:00:00 2001 From: Tixxx Date: Mon, 3 May 2021 11:47:01 -0700 Subject: [PATCH 3/3] swap last 2 dims in packed concat for reversed WH (#293) --- lib/backends/webgl/glsl-coordinate-lib.ts | 13 ++--- lib/backends/webgl/ops/concat_packed.ts | 14 ++++-- .../backends/webgl/test_concat_packed.ts | 49 ++++++------------- 3 files changed, 33 insertions(+), 43 deletions(-) diff --git a/lib/backends/webgl/glsl-coordinate-lib.ts b/lib/backends/webgl/glsl-coordinate-lib.ts index b0adbe61..f971e0bf 100644 --- a/lib/backends/webgl/glsl-coordinate-lib.ts +++ b/lib/backends/webgl/glsl-coordinate-lib.ts @@ -89,7 +89,7 @@ export class CoordsGlslLib extends GlslLib { * Generates code for packed output sampler. */ protected getPackedOutputSamplingSnippet(outputLayout: TextureLayout): {[name: string]: GlslLibRoutine;} { - const outShape = outputLayout.shape; + const outShape = outputLayout.unpackedShape; const outTexShape = [outputLayout.width, outputLayout.height]; const result: {[name: string]: GlslLibRoutine} = {}; const funcName = 'getOutputCoords'; @@ -231,7 +231,7 @@ export class CoordsGlslLib extends GlslLib { const packedTexShape = texShape; // texels needed to accommodate a logical row - const texelsInLogicalRow = shape[1]; + const texelsInLogicalRow = Math.ceil(shape[1] / 2); /** * getOutputCoords @@ -264,8 +264,9 @@ export class CoordsGlslLib extends GlslLib { */ protected getOutputPacked3DCoords(shape: [number, number, number], texShape: [number, number]): GlslLibRoutine { const packedTexShape = [texShape[0], texShape[1]]; - const texelsInLogicalRow = shape[2]; - const texelsInBatch = texelsInLogicalRow * shape[1]; + const texelsInLogicalRow = Math.ceil(shape[2] / 2); + + const texelsInBatch = texelsInLogicalRow * Math.ceil(shape[1] / 2); const source = ` ivec3 getOutputCoords() { ivec2 resTexRC = ivec2(TexCoords.xy * @@ -291,8 +292,8 @@ export class CoordsGlslLib extends GlslLib { protected getOutputPackedNDCoords(shape: ReadonlyArray, texShape: [number, number]): GlslLibRoutine { const packedTexShape = [texShape[0], texShape[1]]; - const texelsInLogicalRow = shape[shape.length - 1]; - const texelsInBatch = texelsInLogicalRow * shape[shape.length - 2]; + const texelsInLogicalRow = Math.ceil(shape[shape.length - 1] / 2); + const texelsInBatch = texelsInLogicalRow * Math.ceil(shape[shape.length - 2] / 2); let texelsInBatchN = texelsInBatch; let batches = ``; let coords = 'b, r, c'; diff --git a/lib/backends/webgl/ops/concat_packed.ts b/lib/backends/webgl/ops/concat_packed.ts index e3453879..16c5e65e 100644 --- a/lib/backends/webgl/ops/concat_packed.ts +++ b/lib/backends/webgl/ops/concat_packed.ts @@ -45,7 +45,8 @@ export class WebGLPackedConcat extends Concat implements WebGLOperator { const unpackChannel = unpackFromChannel(); const shapes = inputs.map(i => i.dims); - const channels = ['x', 'y', 'z', 'w', 'u', 'v'].slice(0, rank); + const allGlChannels = ['x', 'y', 'z', 'w', 'u', 'v']; + const channels = allGlChannels.slice(0, rank); const offsets: number[] = new Array(shapes.length - 1); const samplers = inputs.map((v, i) => `X${i}`); @@ -88,6 +89,10 @@ export class WebGLPackedConcat extends Concat implements WebGLOperator { void main() { ${dtype} coords = getOutputCoords(); + int lastDim = coords.${allGlChannels[rank - 1]}; + coords.${allGlChannels[rank - 1]} = coords.${allGlChannels[rank - 2]}; + coords.${allGlChannels[rank - 2]} = lastDim; + vec4 result = vec4(getValue(${coords}), 0., 0., 0.); ${coords[rank - 1]} = ${coords[rank - 1]} + 1; @@ -110,8 +115,9 @@ export class WebGLPackedConcat extends Concat implements WebGLOperator { `; return { - inputLayouts: inputs.map(t => handler.getOrCreateTextureLayout(t)), - outputLayout: handler.createTextureLayoutFromShape(outputShape), + inputLayouts: inputs.map(t => handler.getOrCreateTextureLayout(t, 4, true, t.dims, true)), + outputLayout: + handler.createTextureLayoutFromShape(outputShape, 4, outputShape, {isPacked: true, reverseWH: true}), samplers, shaderSource, hasMain: true, @@ -120,7 +126,7 @@ export class WebGLPackedConcat extends Concat implements WebGLOperator { }; } createRunData(handler: WebGLInferenceHandler, programInfo: ProgramInfo, inputs: Tensor[]): RunData { - const inputTDs = inputs.map((t, i) => handler.getOrCreateTextureData(t, programInfo.inputLayouts[i])); + const inputTDs = inputs.map((t, i) => handler.getOrCreateTextureData(t, programInfo.inputLayouts[i], true)); return { inputTextureDatas: inputTDs, outputTextureData: handler.createTextureDataFromLayout(programInfo.outputLayout, inputTDs[0].tensor.type), diff --git a/test/unittests/backends/webgl/test_concat_packed.ts b/test/unittests/backends/webgl/test_concat_packed.ts index f545710a..a2776f6d 100644 --- a/test/unittests/backends/webgl/test_concat_packed.ts +++ b/test/unittests/backends/webgl/test_concat_packed.ts @@ -39,6 +39,7 @@ describe('#UnitTest# - packed concat - Tensor concat', () => { describe(`Test concat ${JSON.stringify(testData)}`, () => {}); it(`Test packed concat kernel `, () => { const webglInferenceHandler = inferenceHandler as WebGLInferenceHandler; + // webglInferenceHandler.session.pack = false; // TODO support WebGl 1.0 if (webglInferenceHandler.session.textureManager.glContext.version === 1) { @@ -55,7 +56,6 @@ describe('#UnitTest# - packed concat - Tensor concat', () => { 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. @@ -65,6 +65,7 @@ describe('#UnitTest# - packed concat - Tensor concat', () => { // manually creat packed texture from inputTensor, and insert in cache const gl = webglInferenceHandler.session.textureManager.glContext.gl; + webglInferenceHandler.session.textureManager.glContext.checkError(); const webglTextureA = createTextureFromArray( webglInferenceHandler.session.textureManager.glContext, testData.rawInput ? testData.rawInput : inputData, @@ -82,7 +83,7 @@ describe('#UnitTest# - packed concat - Tensor concat', () => { isPacked: true, shape: packedShape, strides: ShapeUtil.computeStrides(packedShape), - unpackedShape: outputTensorShape, + unpackedShape: inputTensorShape, tensor: inputTensorA, texture: webglTextureA! }; @@ -93,13 +94,13 @@ describe('#UnitTest# - packed concat - Tensor concat', () => { isPacked: true, shape: packedShape, strides: ShapeUtil.computeStrides(packedShape), - unpackedShape: outputTensorShape, + unpackedShape: inputTensorShape, tensor: inputTensorB, texture: webglTextureB! }; - webglInferenceHandler.setTextureData(inputTensorA.dataId, textureDataA); - webglInferenceHandler.setTextureData(inputTensorB.dataId, textureDataB); + webglInferenceHandler.setTextureData(inputTensorA.dataId, textureDataA, true); + webglInferenceHandler.setTextureData(inputTensorB.dataId, textureDataB, true); // compile shader code const programInfo = @@ -117,7 +118,6 @@ describe('#UnitTest# - packed concat - Tensor concat', () => { // verify result. const expectedOutput = testData.expectedOutput; expect(result).to.not.equal(null); - expect(result).to.have.lengthOf(elementCount * 2); expect(result).to.deep.equal(expectedOutput); @@ -167,24 +167,7 @@ function getTestData(): TestData[] { outputShape: [4, 4], inputTextureShape: [2, 1], outputTextureShape: [2, 2], - expectedOutput: new Float32Array([ - 1, - 2, - 5, - 6, - 3, - 4, - 7, - 8, - 1, - 2, - 5, - 6, - 3, - 4, - 7, - 8, - ]), + expectedOutput: new Float32Array([1, 2, 5, 6, 3, 4, 7, 8, 1, 2, 5, 6, 3, 4, 7, 8]), }, { elementCount: 8, @@ -192,7 +175,7 @@ function getTestData(): TestData[] { inputShape: [2, 4], outputShape: [2, 8], inputTextureShape: [2, 1], - outputTextureShape: [2, 4], + outputTextureShape: [4, 2], expectedOutput: new Float32Array([ 1, 2, @@ -291,8 +274,8 @@ function getTestData(): TestData[] { outputTextureShape: [8, 4], expectedOutput: new Float32Array([ 1, 2, 5, 6, 3, 4, 7, 8, 9, 10, 13, 14, 11, 12, 15, 16, 1, 2, 5, 6, 3, 4, - 7, 8, 9, 10, 13, 14, 11, 12, 15, 16, 25, 26, 29, 30, 27, 28, 31, 32, 25, 26, 29, 30, - 27, 28, 31, 32, 25, 26, 29, 30, 27, 28, 31, 32, 25, 26, 29, 30, 27, 28, 31, 32 + 7, 8, 9, 10, 13, 14, 11, 12, 15, 16, 17, 18, 21, 22, 19, 20, 23, 24, 25, 26, 29, 30, + 27, 28, 31, 32, 17, 18, 21, 22, 19, 20, 23, 24, 25, 26, 29, 30, 27, 28, 31, 32 ]) }, @@ -304,9 +287,9 @@ function getTestData(): TestData[] { inputTextureShape: [2, 4], outputTextureShape: [8, 4], expectedOutput: new Float32Array([ - 1, 2, 5, 6, 3, 4, 7, 8, 1, 2, 5, 6, 3, 4, 7, 8, 17, 18, 21, 22, 19, 20, - 23, 24, 17, 18, 21, 22, 19, 20, 23, 24, 25, 26, 29, 30, 27, 28, 31, 32, 25, 26, 29, 30, - 27, 28, 31, 32, 25, 26, 29, 30, 27, 28, 31, 32, 25, 26, 29, 30, 27, 28, 31, 32 + 1, 2, 5, 6, 3, 4, 7, 8, 1, 2, 5, 6, 3, 4, 7, 8, 9, 10, 13, 14, 11, 12, + 15, 16, 9, 10, 13, 14, 11, 12, 15, 16, 17, 18, 21, 22, 19, 20, 23, 24, 17, 18, 21, 22, + 19, 20, 23, 24, 25, 26, 29, 30, 27, 28, 31, 32, 25, 26, 29, 30, 27, 28, 31, 32 ]) }, { @@ -317,9 +300,9 @@ function getTestData(): TestData[] { inputTextureShape: [2, 4], outputTextureShape: [8, 4], expectedOutput: new Float32Array([ - 1, 2, 5, 6, 1, 2, 5, 6, 3, 4, 7, 8, 3, 4, 7, 8, 17, 18, 21, 22, 17, 18, - 21, 22, 19, 20, 23, 24, 19, 20, 23, 24, 25, 26, 29, 30, 25, 26, 29, 30, 27, 28, 31, 32, - 27, 28, 31, 32, 25, 26, 29, 30, 25, 26, 29, 30, 27, 28, 31, 32, 27, 28, 31, 32 + 1, 2, 5, 6, 1, 2, 5, 6, 3, 4, 7, 8, 3, 4, 7, 8, 9, 10, 13, 14, 9, 10, + 13, 14, 11, 12, 15, 16, 11, 12, 15, 16, 17, 18, 21, 22, 17, 18, 21, 22, 19, 20, 23, 24, + 19, 20, 23, 24, 25, 26, 29, 30, 25, 26, 29, 30, 27, 28, 31, 32, 27, 28, 31, 32 ]) }, ];