From 5a412bf110ba7bb6da2ef7fa25cd8c75949e70e6 Mon Sep 17 00:00:00 2001 From: Yulong Wang Date: Tue, 14 May 2019 17:35:13 -0700 Subject: [PATCH] webgl: add full texture cache --- lib/api/onnx.ts | 4 + lib/backends/backend-webgl.ts | 4 + lib/backends/webgl/inference-handler.ts | 25 ++--- lib/backends/webgl/ops/conv.ts | 40 ++++---- lib/backends/webgl/ops/softmax.ts | 4 +- lib/backends/webgl/ops/split.ts | 4 +- lib/backends/webgl/ops/uint8-encode.ts | 4 +- lib/backends/webgl/program-manager.ts | 8 -- lib/backends/webgl/session-handler.ts | 12 ++- .../{texture-helper.ts => texture-manager.ts} | 99 +++++++++++++++---- lib/backends/webgl/types.ts | 2 - lib/backends/webgl/webgl-context.ts | 4 +- tools/test-runner-cli-args.ts | 9 +- tools/test-runner-cli.ts | 3 + 14 files changed, 140 insertions(+), 82 deletions(-) rename lib/backends/webgl/{texture-helper.ts => texture-manager.ts} (55%) diff --git a/lib/api/onnx.ts b/lib/api/onnx.ts index 307af923..1e30dc6c 100644 --- a/lib/api/onnx.ts +++ b/lib/api/onnx.ts @@ -30,6 +30,10 @@ export declare namespace Backend { * set or get the maximum batch size for matmul. 0 means to disable batching. */ matmulMaxBatchSize?: number; + /** + * set or get the texture cache mode + */ + textureCacheMode?: 'initializerOnly'|'full'; } /** * set options for the WebAssembly backend diff --git a/lib/backends/backend-webgl.ts b/lib/backends/backend-webgl.ts index 5612c2bb..0ce0b64e 100644 --- a/lib/backends/backend-webgl.ts +++ b/lib/backends/backend-webgl.ts @@ -22,6 +22,7 @@ export class WebGLBackend implements Backend, WebGLOptions { glContext: WebGLContext; contextId?: 'webgl'|'webgl2'; matmulMaxBatchSize?: number; + textureCacheMode?: 'initializerOnly'|'full'; initialize(): boolean { try { @@ -29,6 +30,9 @@ export class WebGLBackend implements Backend, WebGLOptions { if (typeof this.matmulMaxBatchSize !== 'number') { this.matmulMaxBatchSize = 16; } + if (typeof this.textureCacheMode !== 'string') { + this.textureCacheMode = 'full'; + } Logger.verbose('WebGLBackend', `Created WebGLContext: ${typeof this.glContext}`); return true; } catch (e) { diff --git a/lib/backends/webgl/inference-handler.ts b/lib/backends/webgl/inference-handler.ts index 966ab71c..170f95fe 100644 --- a/lib/backends/webgl/inference-handler.ts +++ b/lib/backends/webgl/inference-handler.ts @@ -7,33 +7,27 @@ import {Tensor} from '../../tensor'; import {ShapeUtil} from '../../util'; import {WebGLUint8Encode} from './ops/uint8-encode'; -import {ProgramManager} from './program-manager'; import {WebGLSessionHandler} from './session-handler'; import {Encoder} from './texture-data-encoder'; -import {TextureHelper} from './texture-helper'; import {WidthHeightPrefs} from './texture-layout-strategy'; import {TextureData, TextureLayout, WebGLOperator} from './types'; import {getPackedShape} from './utils'; export class WebGLInferenceHandler implements InferenceHandler { - textureHelper: TextureHelper; - programManager: ProgramManager; private textureDataCache: Map; constructor(public session: WebGLSessionHandler) { - this.textureHelper = session.textureHelper; - this.programManager = session.programManager; this.textureDataCache = new Map(); } run(op: WebGLOperator, inputs: Tensor[]): Tensor[] { - let artifact = this.programManager.getArtifact(op); + let artifact = this.session.programManager.getArtifact(op); if (!artifact) { const programInfo = op.createProgramInfo(this, inputs); - artifact = this.programManager.build(programInfo); - this.programManager.setArtifact(op, artifact); + artifact = this.session.programManager.build(programInfo); + this.session.programManager.setArtifact(op, artifact); } const runData = op.createRunData(this, artifact.programInfo, inputs); - this.programManager.run(artifact, runData); + this.session.programManager.run(artifact, runData); return [runData.outputTextureData.tensor]; } @@ -90,7 +84,7 @@ export class WebGLInferenceHandler implements InferenceHandler { layout: TextureLayout, dataType: Tensor.DataType, data?: Tensor.NumberType, tensor?: Tensor, usage?: Encoder.Usage): TextureData { Logger.verbose('InferenceHandler', `Creating TextureData: layout:[${JSON.stringify(layout)}]`); - const texture = this.textureHelper.createTextureFromLayout(dataType, layout, data, usage); + const texture = this.session.textureManager.createTextureFromLayout(dataType, layout, data, usage); return this.createTextureDataFromTexture(layout, dataType, texture, tensor); } @@ -175,8 +169,8 @@ export class WebGLInferenceHandler implements InferenceHandler { } dispose(): void { - this.textureHelper.clearActiveTextures(); - this.textureDataCache.forEach(td => this.textureHelper.releaseTexture(td)); + this.session.textureManager.clearActiveTextures(); + this.textureDataCache.forEach(td => this.session.textureManager.releaseTexture(td)); this.textureDataCache = new Map(); } @@ -184,9 +178,8 @@ export class WebGLInferenceHandler implements InferenceHandler { if (!this.session.backend.glContext.isFloat32DownloadSupported) { const op = new WebGLUint8Encode(); const uint8TD = op.runInternal(this, textureData); - return this.textureHelper.readUint8TextureAsFloat(uint8TD); + return this.session.textureManager.readUint8TextureAsFloat(uint8TD); } - const values = this.textureHelper.readTexture(textureData, textureData.tensor.type, textureData.channels); - return values; + return this.session.textureManager.readTexture(textureData, textureData.tensor.type, textureData.channels); } } diff --git a/lib/backends/webgl/ops/conv.ts b/lib/backends/webgl/ops/conv.ts index c607913b..ea633d1e 100644 --- a/lib/backends/webgl/ops/conv.ts +++ b/lib/backends/webgl/ops/conv.ts @@ -12,12 +12,12 @@ import {WebGLContext} from '../webgl-context'; export class WebGLConv extends Conv { run(inferenceHandler: WebGLInferenceHandler, inputs: Tensor[]): Tensor[] { - const programManager = inferenceHandler.programManager; + const programManager = inferenceHandler.session.programManager; if (!this.artifacts) { this.artifacts = []; const programInfos = this.createProgramInfos(inferenceHandler, inputs); for (let i = 0; i < programInfos.length; ++i) { - const artifact = inferenceHandler.programManager.build(programInfos[i]); + const artifact = inferenceHandler.session.programManager.build(programInfos[i]); this.artifacts.push(artifact); } } @@ -70,40 +70,38 @@ export class WebGLConv extends Conv { inputTDs.push(inferenceHandler.getOrCreateTextureData(b)); } const outputTD = inferenceHandler.createTextureDataFromLayout(programInfos[1].outputLayout, inputs[0].type); - const blendEnabled = inferenceHandler.session.backend.glContext.isBlendSupported; const runDataDotProduct = { inputTextureDatas: inputTDs, outputTextureData: outputTD, uniformData: {}, - preRun: blendEnabled ? - (glContext: WebGLContext, artifact: Artifact) => { - const gl = glContext.gl; - gl.enable(gl.BLEND); - glContext.checkError(); - gl.blendEquation(gl.FUNC_ADD); - glContext.checkError(); - gl.blendFunc(gl.ONE, gl.ONE); - glContext.checkError(); - } : - undefined, - postRun: blendEnabled ? - (glContext: WebGLContext, artifact: Artifact) => { - const gl = glContext.gl; - gl.disable(gl.BLEND); - glContext.checkError(); - } : - undefined, draw: (glContext: WebGLContext, artifact: Artifact) => { const gl = glContext.gl; const sharedDim = artifact.programInfo.params!.sharedDim as number; const sharedDimReadSize = artifact.programInfo.params!.sharedDimReadSize as number; const sharedDimOffsetLocation = artifact.uniformLocations.find(l => l.name === 'sharedDimOffset')!.location; + let blend = false; for (let k = 0; k < sharedDim; k += sharedDimReadSize) { Logger.verbose('MatMul2D', `k = ${k}, sharedDim: ${sharedDim}, readSize = ${sharedDimReadSize}`); + + if (k === sharedDimReadSize) { + blend = true; + gl.enable(gl.BLEND); + glContext.checkError(); + gl.blendEquation(gl.FUNC_ADD); + glContext.checkError(); + gl.blendFunc(gl.ONE, gl.ONE); + glContext.checkError(); + } + gl.uniform1i(sharedDimOffsetLocation, k); glContext.checkError(); glContext.draw(); } + + if (blend) { + gl.disable(gl.BLEND); + glContext.checkError(); + } } }; return [runtDataIm2Col, runDataDotProduct]; diff --git a/lib/backends/webgl/ops/softmax.ts b/lib/backends/webgl/ops/softmax.ts index 32d84083..ba7bfa5c 100644 --- a/lib/backends/webgl/ops/softmax.ts +++ b/lib/backends/webgl/ops/softmax.ts @@ -17,13 +17,13 @@ export class WebGLSoftmax extends Softmax { this.artifacts = []; const programInfos = this.createProgramInfos(inferenceHandler, inputs); programInfos.forEach((pi, i) => { - const artifact = inferenceHandler.programManager.build(pi); + const artifact = inferenceHandler.session.programManager.build(pi); this.artifacts.push(artifact); }); } const runDatas = this.createRunDatas(inferenceHandler, this.artifacts.map(a => a.programInfo), inputs); - runDatas.forEach((v, i) => inferenceHandler.programManager.run(this.artifacts[i], v)); + runDatas.forEach((v, i) => inferenceHandler.session.programManager.run(this.artifacts[i], v)); // return only the last output return [runDatas[runDatas.length - 1].outputTextureData.tensor]; } diff --git a/lib/backends/webgl/ops/split.ts b/lib/backends/webgl/ops/split.ts index b2618fe0..3ae4cee1 100644 --- a/lib/backends/webgl/ops/split.ts +++ b/lib/backends/webgl/ops/split.ts @@ -14,7 +14,7 @@ export class WebGLSplit extends Split { this.artifacts = []; for (let i = 0; i < count; ++i) { const programInfo = this.createProgramInfo(inferenceHandler, inputs[0], i); - const artifact = inferenceHandler.programManager.build(programInfo); + const artifact = inferenceHandler.session.programManager.build(programInfo); this.artifacts.push(artifact); } } @@ -22,7 +22,7 @@ export class WebGLSplit extends Split { this.artifacts.forEach(artifact => { const rundata = this.createRunData(inferenceHandler, artifact.programInfo, inputs); - inferenceHandler.programManager.run(artifact, rundata); + inferenceHandler.session.programManager.run(artifact, rundata); results.push(rundata.outputTextureData.tensor); }); return results; diff --git a/lib/backends/webgl/ops/uint8-encode.ts b/lib/backends/webgl/ops/uint8-encode.ts index 3d65cba8..69529821 100644 --- a/lib/backends/webgl/ops/uint8-encode.ts +++ b/lib/backends/webgl/ops/uint8-encode.ts @@ -72,7 +72,7 @@ export class WebGLUint8Encode { ${glsl.output} = encodeAsUint8(value); }`; const programInfo = {inputLayouts: [input], outputLayout, samplers: ['X'], shaderSource, hasMain: true}; - const artifact = inferenceHandler.programManager.build(programInfo); + const artifact = inferenceHandler.session.programManager.build(programInfo); const encoder = inferenceHandler.session.backend.glContext.getEncoder('byte', 4); const texture = @@ -80,7 +80,7 @@ export class WebGLUint8Encode { const outputTextureData = inferenceHandler.createSharedTextureData(outputLayout, 'uint8', texture, {}); const runData = {inputTextureDatas: [input], outputTextureData, uniformData: {}}; - inferenceHandler.programManager.run(artifact, runData); + inferenceHandler.session.programManager.run(artifact, runData); return runData.outputTextureData; } } diff --git a/lib/backends/webgl/program-manager.ts b/lib/backends/webgl/program-manager.ts index 37ae3ab6..1fcad84c 100644 --- a/lib/backends/webgl/program-manager.ts +++ b/lib/backends/webgl/program-manager.ts @@ -35,10 +35,6 @@ export class ProgramManager { } run(buildArtifact: Artifact, runData: RunData): void { this.profiler.event('backend', 'ProgramManager.run', () => { - if (runData.preRun) { - Logger.verbose('ProgramManager', 'PreRun'); - runData.preRun(this.glContext, buildArtifact); - } const gl = this.glContext.gl; const program = buildArtifact.program; gl.useProgram(program); @@ -56,10 +52,6 @@ export class ProgramManager { this.doDraw(buildArtifact, runData); gl.flush(); }); - if (runData.postRun) { - Logger.verbose('ProgramManager', 'PostRun'); - runData.postRun(this.glContext, buildArtifact); - } }); } dispose(): void { diff --git a/lib/backends/webgl/session-handler.ts b/lib/backends/webgl/session-handler.ts index ea2dda2a..9d236a6f 100644 --- a/lib/backends/webgl/session-handler.ts +++ b/lib/backends/webgl/session-handler.ts @@ -13,13 +13,13 @@ import {WebGLBackend} from '../backend-webgl'; import {WebGLInferenceHandler} from './inference-handler'; import {WEBGL_OP_RESOLVE_RULES} from './op-resolve-rules'; import {ProgramManager} from './program-manager'; -import {TextureHelper} from './texture-helper'; import {AlwaysKeepOriginalSizeStrategy, TextureLayoutStrategy} from './texture-layout-strategy'; +import {TextureManager} from './texture-manager'; import {TextureData} from './types'; export class WebGLSessionHandler implements SessionHandler { programManager: ProgramManager; - textureHelper: TextureHelper; + textureManager: TextureManager; layoutStrategy: TextureLayoutStrategy; textureDataCache: Map; initializers: Set; @@ -27,7 +27,9 @@ export class WebGLSessionHandler implements SessionHandler { constructor(public readonly backend: WebGLBackend, public readonly context: Session.Context) { this.programManager = new ProgramManager(this.context.profiler, backend.glContext); this.layoutStrategy = new AlwaysKeepOriginalSizeStrategy(backend.glContext.maxTextureSize); - this.textureHelper = new TextureHelper(backend.glContext, this.layoutStrategy, this.context.profiler); + this.textureManager = new TextureManager( + backend.glContext, this.layoutStrategy, this.context.profiler, + {reuseTextures: backend.textureCacheMode === 'full'}); this.textureDataCache = new Map(); } @@ -50,8 +52,8 @@ export class WebGLSessionHandler implements SessionHandler { } dispose(): void { this.programManager.dispose(); - this.textureHelper.clearActiveTextures(); - this.textureDataCache.forEach(td => this.textureHelper.releaseTexture(td)); + this.textureManager.clearActiveTextures(); + this.textureDataCache.forEach(td => this.textureManager.releaseTexture(td, true)); this.textureDataCache = new Map(); } resolve(node: Graph.Node, opsets: ReadonlyArray): Operator { diff --git a/lib/backends/webgl/texture-helper.ts b/lib/backends/webgl/texture-manager.ts similarity index 55% rename from lib/backends/webgl/texture-helper.ts rename to lib/backends/webgl/texture-manager.ts index 3fd09402..0fdc8fe0 100644 --- a/lib/backends/webgl/texture-helper.ts +++ b/lib/backends/webgl/texture-manager.ts @@ -9,41 +9,77 @@ import {TextureLayoutStrategy} from './texture-layout-strategy'; import {TextureData, TextureLayout} from './types'; import {WebGLContext} from './webgl-context'; +export interface TextureManagerConfig { + reuseTextures?: boolean; +} + /** - * Texture Manager is the mainly responsible for caching Textures + * TextureManager is the mainly responsible for caching Textures * Textures are cached in 2 levels: * 1. the texures which are associated with a dataId (from Tensor) * Caching these is crucial to performance. These are In-use Textures * 2. textures which are not in use by any current ProgramInfo/Tensor * These are called Free Textures - * TextureHelper is also used to help creating textures. For this it + * TextureManager is also used to help creating textures. For this it * uses WebGLContext and TextureLayoutStrategy */ -export class TextureHelper { - glContext: WebGLContext; - gl: WebGLRenderingContext; - layoutStrategy: TextureLayoutStrategy; - profiler: Readonly; +export class TextureManager { + private readonly inUseTextures: Map; + private readonly idleTextures: Map; + private readonly textureLookup: Map; - constructor(context: WebGLContext, layoutStrategy: TextureLayoutStrategy, profiler: Readonly) { - this.glContext = context; - this.gl = context.gl; - this.layoutStrategy = layoutStrategy; - this.profiler = profiler; + constructor( + public glContext: WebGLContext, public layoutStrategy: TextureLayoutStrategy, public profiler: Readonly, + private config: TextureManagerConfig) { + if (config.reuseTextures) { + this.inUseTextures = new Map(); + this.idleTextures = new Map(); + this.textureLookup = new Map(); + } } createTextureFromLayout( dataType: Tensor.DataType, layout: TextureLayout, data?: Tensor.NumberType, usage?: Encoder.Usage) { const textureDataType = this.toEncoderType(dataType); - Logger.verbose('TextureHelper', `Creating new texture of size ${layout.width}x${layout.height}`); const encoder = this.glContext.getEncoder(textureDataType, layout.channels || 1, usage); - return this.glContext.allocateTexture(layout.width, layout.height, encoder, this.toTextureData(dataType, data)); + + let key: string|undefined; + let inUseTextures: WebGLTexture[]|undefined; + if (this.config.reuseTextures) { + key = `${layout.width}x${layout.height}_${encoder.format}_${encoder.internalFormat}_${encoder.textureType}`; + inUseTextures = this.inUseTextures.get(key); + if (!inUseTextures) { + inUseTextures = []; + this.inUseTextures.set(key, inUseTextures); + } + + const idleTextures = this.idleTextures.get(key); + if (idleTextures && idleTextures.length > 0) { + const texture = idleTextures.pop()!; + inUseTextures.push(texture); + if (usage === Encoder.Usage.UploadOnly) { + this.glContext.updateTexture( + texture, layout.width, layout.height, encoder, this.toTextureData(dataType, data)!); + } + return texture; + } + } + + Logger.verbose('TextureManager', `Creating new texture of size ${layout.width}x${layout.height}`); + const texture = + this.glContext.allocateTexture(layout.width, layout.height, encoder, this.toTextureData(dataType, data)); + + if (this.config.reuseTextures) { + inUseTextures!.push(texture); + this.textureLookup.set(texture, key!); + } + return texture; } readTexture(td: TextureData, dataType: Tensor.DataType, channels?: number): Tensor.NumberType { if (!channels) { channels = 1; } - return this.profiler.event('backend', 'TextureHelper.readTexture', () => { + return this.profiler.event('backend', 'TextureManager.readTexture', () => { const dataSize = td.shape.reduce((a, b) => a * b) * channels!; const data = this.glContext.readTexture( td.texture, td.width, td.height, dataSize, this.toEncoderType(dataType), channels!); @@ -51,15 +87,40 @@ export class TextureHelper { }); } readUint8TextureAsFloat(td: TextureData): Float32Array { - return this.profiler.event('backend', 'TextureHelper.readUint8TextureAsFloat', () => { + return this.profiler.event('backend', 'TextureManager.readUint8TextureAsFloat', () => { const dataSize = td.shape.reduce((a, b) => a * b); const data = this.glContext.readTexture(td.texture, td.width, td.height, dataSize * 4, 'byte', 4); return new Float32Array(data.buffer, data.byteOffset, dataSize); }); } - releaseTexture(texture: TextureData): void { - Logger.verbose('TextureHelper', `Deleting texture of size ${texture.width}x${texture.height}`); - this.glContext.deleteTexture(texture.texture); + releaseTexture(textureData: TextureData, deleteTexture?: boolean): void { + let key: string|undefined; + if (this.config.reuseTextures) { + key = this.textureLookup.get(textureData.texture); + if (key) { + if (deleteTexture) { + this.textureLookup.delete(key); + } + const inUseTextures = this.inUseTextures.get(key); + if (inUseTextures) { + const index = inUseTextures.indexOf(textureData.texture); + if (index !== -1) { + inUseTextures.splice(index, 1); + let idleTextures = this.idleTextures.get(key); + if (!idleTextures) { + idleTextures = []; + this.idleTextures.set(key, idleTextures); + } + idleTextures.push(textureData.texture); + } + } + } + } + + if (!key || deleteTexture) { + Logger.verbose('TextureManager', `Deleting texture of size ${textureData.width}x${textureData.height}`); + this.glContext.deleteTexture(textureData.texture); + } } toTensorData(dataType: Tensor.DataType, data: Encoder.DataArrayType): Tensor.NumberType { return (data.constructor === Float32Array) ? data as Float32Array : new Float32Array(data); diff --git a/lib/backends/webgl/types.ts b/lib/backends/webgl/types.ts index faa13ba9..5b93f0a3 100644 --- a/lib/backends/webgl/types.ts +++ b/lib/backends/webgl/types.ts @@ -121,7 +121,5 @@ export interface RunData { inputTextureDatas: TextureData[]; outputTextureData: TextureData; uniformData: UniformData; - preRun?: (glContext: WebGLContext, artifact: Artifact) => void; - postRun?: (glContext: WebGLContext, artifact: Artifact) => void; draw?: (glContext: WebGLContext, artifact: Artifact) => void; } diff --git a/lib/backends/webgl/webgl-context.ts b/lib/backends/webgl/webgl-context.ts index f31e4578..e1f9c78c 100644 --- a/lib/backends/webgl/webgl-context.ts +++ b/lib/backends/webgl/webgl-context.ts @@ -76,11 +76,9 @@ export class WebGLContext { return texture as WebGLTexture; } updateTexture( - texture: WebGLTexture, width: number, height: number, dataType: Encoder.DataType, channels: number, - data: Encoder.DataArrayType): void { + texture: WebGLTexture, width: number, height: number, encoder: DataEncoder, data: Encoder.DataArrayType): void { const gl = this.gl; gl.bindTexture(gl.TEXTURE_2D, texture); - const encoder = this.getEncoder(dataType, channels); const buffer = encoder.encode(data, width * height); gl.texSubImage2D( gl.TEXTURE_2D, diff --git a/tools/test-runner-cli-args.ts b/tools/test-runner-cli-args.ts index 066c8ea7..7a56013e 100644 --- a/tools/test-runner-cli-args.ts +++ b/tools/test-runner-cli-args.ts @@ -64,8 +64,9 @@ Options: --wasm-worker Set the WebAssembly worker number --wasm-cpu-fallback Set whether to allow WebAssembly backend to fallback to CPU --wasm-init-timeout Set the timeout for WebAssembly backend initialization, in milliseconds - --webgl-context-id Set the WebGL context ID + --webgl-context-id Set the WebGL context ID (webgl/webgl2) --webgl-matmul-max-batch-size Set the WebGL matmulMaxBatchSize + --webgl-texture-cache-mode Set the WebGL texture cache mode (initializerOnly/full) *** Browser Options *** @@ -335,7 +336,11 @@ function parseWebglOptions(args: minimist.ParsedArgs): Backend.WebGLOptions { if (matmulMaxBatchSize !== undefined && typeof matmulMaxBatchSize !== 'number') { throw new Error('Flag "webgl-matmul-max-batch-size" must be a number value'); } - return {contextId, matmulMaxBatchSize}; + const textureCacheMode = args['webgl-texture-cache-mode']; + if (textureCacheMode !== undefined && textureCacheMode !== 'initializerOnly' && textureCacheMode !== 'full') { + throw new Error('Flag "webgl-texture-cache-mode" is invalid'); + } + return {contextId, matmulMaxBatchSize, textureCacheMode}; } function parseBooleanArg(arg: unknown, defaultValue: boolean): boolean; diff --git a/tools/test-runner-cli.ts b/tools/test-runner-cli.ts index 6da31358..341c2128 100644 --- a/tools/test-runner-cli.ts +++ b/tools/test-runner-cli.ts @@ -540,6 +540,9 @@ function saveConfig(config: Test.Config) { if (config.options.webgl && config.options.webgl.matmulMaxBatchSize !== undefined) { setOptions += `onnx.backend.webgl.matmulMaxBatchSize = ${config.options.webgl.matmulMaxBatchSize};`; } + if (config.options.webgl && config.options.webgl.textureCacheMode !== undefined) { + setOptions += `onnx.backend.webgl.textureCacheMode = ${JSON.stringify(config.options.webgl.textureCacheMode)};`; + } if (config.options.wasm && config.options.wasm.worker !== undefined) { setOptions += `onnx.backend.wasm.worker = ${JSON.stringify(config.options.wasm.worker)};`; }