@qvac/ocr-ggml
Advanced tools
| 'use strict' | ||
| const { OcrGgml } = require('../..') | ||
| const test = require('brittle') | ||
| const { platform, getImagePath, ensureModelPath, safeUnload } = require('./utils') | ||
| // QVAC-19798: Android OpenCL integration test for ocr-ggml. | ||
| // | ||
| // Android (specifically Qualcomm Adreno) is the primary OpenCL target. This | ||
| // test runs ONLY on Android — the desktop OpenCL opt-in path is covered by | ||
| // opencl-backend.test.js, and Apple has no OpenCL (deprecated; Metal instead). | ||
| // On every non-Android host this file is a clean skip. | ||
| // | ||
| // The test requests `backendDevice: 'opencl'` and asserts the contract: | ||
| // 1. The addon EITHER runs inference on an OpenCL device OR reports an | ||
| // explicit CPU fallback — never a silent wrong path. | ||
| // 2. Whichever backend is resolved, the OCR output must be CORRECT | ||
| // (accuracy gate), not merely "it executed". | ||
| // | ||
| // Unlike Vulkan, Adreno is NOT guarded off for OpenCL: OpenCL is Adreno's sound | ||
| // compute path (the inverse of the Vulkan Adreno guard). So on an Adreno device | ||
| // that ships libggml-opencl, this test exercises the real OpenCL GPU path; on a | ||
| // device without the OpenCL backend lib / driver it takes the explicit | ||
| // CPU-fallback branch. Either way the output must be correct — a numerically | ||
| // broken OpenCL path would fail the accuracy assertions, which is the signal we | ||
| // want. The resolved backend name/description is logged so Device Farm runs | ||
| // surface the actual GPU on the pool's devices. | ||
| const TEST_TIMEOUT = 300 * 1000 | ||
| const shouldSkip = platform !== 'android' | ||
| test('android opencl: selects OpenCL or reports CPU fallback, with correct OCR output', { timeout: TEST_TIMEOUT, skip: shouldSkip }, async function (t) { | ||
| const detectorPath = await ensureModelPath('detector_craft') | ||
| const recognizerPath = await ensureModelPath('recognizer_latin') | ||
| const imagePath = getImagePath('/test/images/basic_test.bmp') | ||
| const ocrGgml = new OcrGgml({ | ||
| params: { | ||
| pathDetector: detectorPath, | ||
| pathRecognizer: recognizerPath, | ||
| langList: ['en'], | ||
| backendDevice: 'opencl' | ||
| }, | ||
| opts: { stats: true } | ||
| }) | ||
| await ocrGgml.load() | ||
| t.pass('loaded with backendDevice: opencl') | ||
| const backendInfo = ocrGgml.getBackendInfo() | ||
| t.ok(backendInfo, 'getBackendInfo() returns backend info after load') | ||
| t.is(backendInfo.requested, 'opencl', 'requested device recorded as opencl') | ||
| // Probe: surface the actual device GPU (name + description) in Device Farm logs. | ||
| t.comment('Resolved backend info: ' + JSON.stringify(backendInfo)) | ||
| const openclSelected = | ||
| backendInfo.backendDevice === 'GPU' || backendInfo.backendDevice === 'IGPU' | ||
| if (openclSelected) { | ||
| t.is(backendInfo.fallbackReason, '', 'no fallback reason when OpenCL is selected') | ||
| t.ok(/opencl/i.test(backendInfo.backendName), 'selected backend name mentions OpenCL (' + backendInfo.backendName + ')') | ||
| } else { | ||
| // No usable OpenCL device (none present, or no backend lib shipped): the | ||
| // fallback to CPU MUST be reported explicitly. | ||
| t.is(backendInfo.backendDevice, 'CPU', 'fell back to the CPU device') | ||
| t.ok(backendInfo.fallbackReason.length > 0, 'explicit CPU fallback reason reported') | ||
| t.comment('CPU fallback reason: ' + backendInfo.fallbackReason) | ||
| } | ||
| try { | ||
| const response = await ocrGgml.run({ | ||
| path: imagePath, | ||
| options: { paragraph: false } | ||
| }) | ||
| let outputTexts = [] | ||
| await response | ||
| .onUpdate(output => { | ||
| t.ok(Array.isArray(output), 'output should be an array') | ||
| outputTexts = output.map(o => o[1]) | ||
| }) | ||
| .onError(error => { | ||
| t.fail('unexpected error: ' + JSON.stringify(error)) | ||
| }) | ||
| .await() | ||
| const stats = response.stats || {} | ||
| t.comment('Native addon stats: ' + JSON.stringify(stats)) | ||
| // The numeric `backendIsGpu` stat must agree with the resolved device. | ||
| t.is( | ||
| stats.backendIsGpu, | ||
| openclSelected ? 1 : 0, | ||
| 'backendIsGpu stat matches the resolved backend (' + backendInfo.backendDevice + ')' | ||
| ) | ||
| // Accuracy gate: whichever backend ran, the output must be correct. A | ||
| // numerically-broken OpenCL device would produce garbage here and fail. | ||
| t.ok(outputTexts.length > 0, 'inference produced text regions') | ||
| t.ok(outputTexts.includes('normal'), 'recognized expected text "normal" (backend: ' + backendInfo.backendDevice + ')') | ||
| t.pass('android opencl path exercised (' + backendInfo.backendDevice + ')') | ||
| } finally { | ||
| await safeUnload(ocrGgml) | ||
| await new Promise(resolve => setTimeout(resolve, 1000)) | ||
| } | ||
| }) |
| 'use strict' | ||
| const test = require('brittle') | ||
| const os = require('bare-os') | ||
| const process = require('bare-process') | ||
| const { isMobile, platform, getImagePath, ensureModelPath, runOcrComparison } = require('./utils') | ||
| const DESKTOP_TIMEOUT = 180 * 1000 // 3 minutes: runs the pipeline twice | ||
| // Guards the 1x1-conv -> ggml_mul_mat path added in QVAC-20532. The CRAFT | ||
| // detector's 1x1 convs can run either through ggml_conv_2d (im2col + GEMM) or a | ||
| // direct ggml_mul_mat; the addon picks per backend at load time (mul_mat on | ||
| // GPU, conv_2d on CPU). The rewrite must be numerically equivalent for | ||
| // 1x1/stride-1 convs, so this runs the SAME image on the same backend twice and | ||
| // forces each path explicitly — OCR_GGML_CONV1X1_CONV2D=1 (conv_2d) then | ||
| // OCR_GGML_CONV1X1_MULMAT=1 (mul_mat) — and asserts identical recognized | ||
| // output, plus that each pass is absolutely correct. The explicit force is | ||
| // required because the unset default is backend-dependent (it would otherwise | ||
| // run the same path twice on GPU vs CPU, making the comparison vacuous). | ||
| // | ||
| // The toggles are read by the native addon via getenv at model-load time, so we | ||
| // set them through bare-os (which maps to setenv) before constructing the addon | ||
| // and restore them afterwards. Desktop POSIX only: mobile device-farm runs | ||
| // don't propagate these process env vars, and on Windows uv_os_setenv | ||
| // (SetEnvironmentVariableW) does not update the CRT table that the addon's | ||
| // std::getenv reads — so the toggle wouldn't take effect. | ||
| const ENV_KEYS = ['OCR_GGML_CONV1X1_MULMAT', 'OCR_GGML_CONV1X1_CONV2D'] | ||
| test('EasyOCR 1x1-conv mul_mat matches conv_2d (CRAFT)', { timeout: DESKTOP_TIMEOUT }, async function (t) { | ||
| if (isMobile) { | ||
| t.pass('skipped on mobile (env toggle is a desktop-only A/B lever)') | ||
| return | ||
| } | ||
| if (platform === 'win32') { | ||
| t.pass('skipped on win32 (SetEnvironmentVariableW does not reach the addon CRT getenv)') | ||
| return | ||
| } | ||
| const hasGetEnv = typeof os.getEnv === 'function' | ||
| const hasSetEnv = typeof os.setEnv === 'function' | ||
| const prev = new Map() | ||
| for (const key of ENV_KEYS) { | ||
| prev.set(key, (hasGetEnv ? os.getEnv(key) : process.env[key]) || '') | ||
| } | ||
| function setEnv (key, val) { | ||
| if (hasSetEnv) os.setEnv(key, val) | ||
| process.env[key] = val | ||
| } | ||
| // Force exactly one path on (key=1) and clear every other toggle, so the | ||
| // backend-aware default never leaks into either pass. | ||
| function forceOnly (onKey) { | ||
| for (const key of ENV_KEYS) setEnv(key, key === onKey ? '1' : '') | ||
| } | ||
| function restoreEnv () { | ||
| for (const key of ENV_KEYS) { | ||
| const original = prev.get(key) | ||
| if (original) { | ||
| setEnv(key, original) | ||
| continue | ||
| } | ||
| if (typeof os.unsetEnv === 'function') os.unsetEnv(key) | ||
| else if (hasSetEnv) os.setEnv(key, '') | ||
| // bare-process's env proxy rejects `delete` (TypeError under strict mode); '' is sufficient since the addon reads via std::getenv. | ||
| process.env[key] = '' | ||
| } | ||
| } | ||
| function assertCorrect (output, tag) { | ||
| t.ok(Array.isArray(output), tag + ': output should be an array') | ||
| t.is(output.length, 3, tag + `: output length should be 3, got ${output.length}`) | ||
| const texts = output.map(o => o[1]) | ||
| t.ok(texts.includes('tilted'), tag + ': should contain "tilted"') | ||
| t.ok(texts.includes('normal'), tag + ': should contain "normal"') | ||
| t.ok(texts.includes('vertical'), tag + ': should contain "vertical"') | ||
| } | ||
| try { | ||
| const detectorPath = await ensureModelPath('detector_craft') | ||
| const recognizerPath = await ensureModelPath('recognizer_latin') | ||
| const imagePath = getImagePath('/test/images/basic_test.bmp') | ||
| const baseCfg = { | ||
| params: { pathDetector: detectorPath, pathRecognizer: recognizerPath, langList: ['en'] }, | ||
| imagePath, | ||
| runOptions: { paragraph: false }, | ||
| perfOpts: { skipReport: true } | ||
| } | ||
| t.comment('Pass A: forced conv_2d path; image: ' + imagePath + ', platform: ' + platform) | ||
| forceOnly('OCR_GGML_CONV1X1_CONV2D') | ||
| const resConv = await runOcrComparison(t, { | ||
| ...baseCfg, | ||
| perfLabel: '[EasyOCR basic_test conv2d]', | ||
| assertResult (output) { assertCorrect(output, 'conv_2d') } | ||
| }) | ||
| const outConv = resConv.output | ||
| t.comment('Pass B: forced 1x1 mul_mat path') | ||
| forceOnly('OCR_GGML_CONV1X1_MULMAT') | ||
| const resMul = await runOcrComparison(t, { | ||
| ...baseCfg, | ||
| perfLabel: '[EasyOCR basic_test 1x1-mulmat]', | ||
| assertResult (output) { assertCorrect(output, 'mul_mat') } | ||
| }) | ||
| const outMul = resMul.output | ||
| // Equivalence: the mul_mat rewrite must match conv_2d region-for-region. | ||
| t.is(outMul.length, outConv.length, 'mul_mat and conv_2d produce the same number of regions') | ||
| const n = Math.min(outMul.length, outConv.length) | ||
| for (let i = 0; i < n; i++) { | ||
| t.is(outMul[i][1], outConv[i][1], `region ${i}: mul_mat text matches conv_2d ("${outConv[i][1]}")`) | ||
| } | ||
| t.pass('1x1-conv mul_mat path matches conv_2d output') | ||
| } finally { | ||
| restoreEnv() | ||
| } | ||
| }) |
| 'use strict' | ||
| const test = require('brittle') | ||
| const os = require('bare-os') | ||
| const process = require('bare-process') | ||
| const { isMobile, platform, getImagePath, ensureModelPath, runOcrComparison } = require('./utils') | ||
| const DESKTOP_TIMEOUT = 180 * 1000 // 3 minutes: runs the pipeline twice | ||
| // Guards the CRNN recognizer bias broadcast path (QVAC-20906). The BiLSTM-linear | ||
| // and Prediction heads add their [F] bias via ggml_add's implicit broadcast over | ||
| // the (T, N) axes by default; OCR_GGML_CRNN_BIAS_REPEAT=1 forces the legacy | ||
| // ggml_repeat path. The two must be numerically equivalent, so this runs the | ||
| // SAME image on the same backend twice — repeat (forced) then broadcast | ||
| // (default) — and asserts identical recognized output, plus that each pass is | ||
| // absolutely correct. Mirrors the CRAFT conv-bias A/B (bias-broadcast.test.js). | ||
| // | ||
| // The toggle is read by the native addon via getenv at graph-build time, so we | ||
| // set it through bare-os (which maps to setenv) before constructing the addon | ||
| // and restore it afterwards. Desktop POSIX only: mobile device-farm runs don't | ||
| // propagate these process env vars, and on Windows uv_os_setenv | ||
| // (SetEnvironmentVariableW) does not update the CRT table that the addon's | ||
| // std::getenv reads — so the toggle wouldn't take effect and both passes would | ||
| // run the default broadcast path, making the comparison vacuous. | ||
| const ENV_KEY = 'OCR_GGML_CRNN_BIAS_REPEAT' | ||
| test('EasyOCR CRNN bias broadcast matches ggml_repeat', { timeout: DESKTOP_TIMEOUT }, async function (t) { | ||
| if (isMobile) { | ||
| t.pass('skipped on mobile (env toggle is a desktop-only A/B lever)') | ||
| return | ||
| } | ||
| if (platform === 'win32') { | ||
| t.pass('skipped on win32 (SetEnvironmentVariableW does not reach the addon CRT getenv)') | ||
| return | ||
| } | ||
| const hasGetEnv = typeof os.getEnv === 'function' | ||
| const hasSetEnv = typeof os.setEnv === 'function' | ||
| const prevValue = (hasGetEnv ? os.getEnv(ENV_KEY) : process.env[ENV_KEY]) || '' | ||
| function setEnv (val) { | ||
| if (hasSetEnv) os.setEnv(ENV_KEY, val) | ||
| process.env[ENV_KEY] = val | ||
| } | ||
| function restoreEnv () { | ||
| if (prevValue) { | ||
| setEnv(prevValue) | ||
| return | ||
| } | ||
| if (typeof os.unsetEnv === 'function') os.unsetEnv(ENV_KEY) | ||
| else if (hasSetEnv) os.setEnv(ENV_KEY, '') | ||
| // bare-process's env proxy rejects `delete` (TypeError under strict mode); '' is sufficient since the addon reads via std::getenv. | ||
| process.env[ENV_KEY] = '' | ||
| } | ||
| function assertCorrect (output, tag) { | ||
| t.ok(Array.isArray(output), tag + ': output should be an array') | ||
| t.is(output.length, 3, tag + `: output length should be 3, got ${output.length}`) | ||
| const texts = output.map(o => o[1]) | ||
| t.ok(texts.includes('tilted'), tag + ': should contain "tilted"') | ||
| t.ok(texts.includes('normal'), tag + ': should contain "normal"') | ||
| t.ok(texts.includes('vertical'), tag + ': should contain "vertical"') | ||
| } | ||
| try { | ||
| const detectorPath = await ensureModelPath('detector_craft') | ||
| const recognizerPath = await ensureModelPath('recognizer_latin') | ||
| const imagePath = getImagePath('/test/images/basic_test.bmp') | ||
| const baseCfg = { | ||
| params: { pathDetector: detectorPath, pathRecognizer: recognizerPath, langList: ['en'] }, | ||
| imagePath, | ||
| runOptions: { paragraph: false }, | ||
| perfOpts: { skipReport: true } | ||
| } | ||
| t.comment('Pass A: forced ggml_repeat CRNN bias; image: ' + imagePath + ', platform: ' + platform) | ||
| setEnv('1') | ||
| const resRepeat = await runOcrComparison(t, { | ||
| ...baseCfg, | ||
| perfLabel: '[EasyOCR basic_test crnn-bias-repeat]', | ||
| assertResult (output) { assertCorrect(output, 'repeat') } | ||
| }) | ||
| const outRepeat = resRepeat.output | ||
| t.comment('Pass B: default ggml_add broadcast CRNN bias') | ||
| setEnv('') | ||
| const resBroadcast = await runOcrComparison(t, { | ||
| ...baseCfg, | ||
| perfLabel: '[EasyOCR basic_test crnn-bias-broadcast]', | ||
| assertResult (output) { assertCorrect(output, 'broadcast') } | ||
| }) | ||
| const outBroadcast = resBroadcast.output | ||
| // Equivalence: broadcast must match the ggml_repeat path region-for-region. | ||
| t.is(outBroadcast.length, outRepeat.length, 'broadcast and repeat produce the same number of regions') | ||
| const n = Math.min(outBroadcast.length, outRepeat.length) | ||
| for (let i = 0; i < n; i++) { | ||
| t.is(outBroadcast[i][1], outRepeat[i][1], `region ${i}: broadcast text matches repeat ("${outRepeat[i][1]}")`) | ||
| } | ||
| t.pass('CRNN bias broadcast path matches ggml_repeat output') | ||
| } finally { | ||
| restoreEnv() | ||
| } | ||
| }) |
| 'use strict' | ||
| const { OcrGgml } = require('../..') | ||
| const test = require('brittle') | ||
| const os = require('bare-os') | ||
| const process = require('bare-process') | ||
| const { platform, getImagePath, ensureModelPath, safeUnload } = require('./utils') | ||
| const DESKTOP_TIMEOUT = 240 * 1000 // 4 minutes: loads + runs the pipeline twice | ||
| // Guards the backend-aware direct-conv path (QVAC-20909). EasyOCR's non-1x1 | ||
| // convs run through ggml_conv_2d (im2col) by default; OCR_GGML_DIRECT_CONV=1 | ||
| // forces the fused ggml_conv_2d_direct (GGML_OP_CONV_2D) path, and | ||
| // OCR_GGML_IM2COL_CONV=1 forces im2col. The two must be numerically equivalent, | ||
| // so this runs the SAME image on the SAME backend twice — im2col (forced) then | ||
| // direct (forced) — and asserts identical recognized output. | ||
| // | ||
| // Why Metal-only: ggml_conv_2d_direct's per-backend support matters here. ggml | ||
| // hard-aborts (GGML_ASSERT) when a graph contains an op the backend can't run, | ||
| // so we only force the direct path where GGML_OP_CONV_2D is known-supported. | ||
| // Apple Metal is confirmed (the DocTR doctrConv2d work, QVAC-19798, measured it | ||
| // on Metal); the real OpenCL/Adreno target is validated separately on the | ||
| // device farm by runAndroidOpenclTest. We deliberately do NOT exercise the | ||
| // linux CPU/Vulkan or Windows runners here (direct-conv support there is | ||
| // unverified) nor mobile (env toggles don't propagate to the device farm). | ||
| const ENV_KEYS = ['OCR_GGML_DIRECT_CONV', 'OCR_GGML_IM2COL_CONV'] | ||
| const shouldSkip = platform !== 'darwin' | ||
| test('EasyOCR direct-conv matches im2col (Metal)', { timeout: DESKTOP_TIMEOUT, skip: shouldSkip }, async function (t) { | ||
| const hasGetEnv = typeof os.getEnv === 'function' | ||
| const hasSetEnv = typeof os.setEnv === 'function' | ||
| const prev = new Map() | ||
| for (const key of ENV_KEYS) { | ||
| prev.set(key, (hasGetEnv ? os.getEnv(key) : process.env[key]) || '') | ||
| } | ||
| function setEnv (key, val) { | ||
| if (hasSetEnv) os.setEnv(key, val) | ||
| process.env[key] = val | ||
| } | ||
| // Force exactly one conv path on and clear the other so neither leaks. | ||
| function forceOnly (onKey) { | ||
| for (const key of ENV_KEYS) setEnv(key, key === onKey ? '1' : '') | ||
| } | ||
| function restoreEnv () { | ||
| for (const key of ENV_KEYS) { | ||
| const original = prev.get(key) | ||
| if (original) { | ||
| setEnv(key, original) | ||
| continue | ||
| } | ||
| if (typeof os.unsetEnv === 'function') os.unsetEnv(key) | ||
| else if (hasSetEnv) os.setEnv(key, '') | ||
| // bare-process's env proxy rejects `delete` (TypeError under strict mode); '' is sufficient since the addon reads via std::getenv. | ||
| process.env[key] = '' | ||
| } | ||
| } | ||
| // Run the EasyOCR pipeline once on Metal and return the recognized regions. | ||
| async function runMetalPass (tag) { | ||
| const detectorPath = await ensureModelPath('detector_craft') | ||
| const recognizerPath = await ensureModelPath('recognizer_latin') | ||
| const imagePath = getImagePath('/test/images/basic_test.bmp') | ||
| const ocrGgml = new OcrGgml({ | ||
| params: { pathDetector: detectorPath, pathRecognizer: recognizerPath, langList: ['en'], backendDevice: 'metal' }, | ||
| opts: { stats: true } | ||
| }) | ||
| await ocrGgml.load() | ||
| const info = typeof ocrGgml.getBackendInfo === 'function' ? ocrGgml.getBackendInfo() : null | ||
| const isGpu = !!info && (info.backendDevice === 'GPU' || info.backendDevice === 'IGPU') | ||
| if (!isGpu) { | ||
| // Metal didn't resolve to a GPU (e.g. no Metal device): bail rather than | ||
| // force the direct path onto a backend that may not implement it. | ||
| await safeUnload(ocrGgml) | ||
| return { skipped: true, output: [] } | ||
| } | ||
| let output = [] | ||
| try { | ||
| const response = await ocrGgml.run({ path: imagePath, options: { paragraph: false } }) | ||
| await response | ||
| .onUpdate(o => { output = o }) | ||
| .onError(error => { t.fail(tag + ': unexpected error: ' + JSON.stringify(error)) }) | ||
| .await() | ||
| return { skipped: false, output } | ||
| } finally { | ||
| await safeUnload(ocrGgml) | ||
| await new Promise(resolve => setTimeout(resolve, 1000)) | ||
| } | ||
| } | ||
| function assertCorrect (output, tag) { | ||
| t.ok(Array.isArray(output), tag + ': output should be an array') | ||
| t.is(output.length, 3, tag + `: output length should be 3, got ${output.length}`) | ||
| const texts = output.map(o => o[1]) | ||
| t.ok(texts.includes('tilted'), tag + ': should contain "tilted"') | ||
| t.ok(texts.includes('normal'), tag + ': should contain "normal"') | ||
| t.ok(texts.includes('vertical'), tag + ': should contain "vertical"') | ||
| } | ||
| try { | ||
| t.comment('Pass A: forced im2col conv (Metal); platform: ' + platform) | ||
| forceOnly('OCR_GGML_IM2COL_CONV') | ||
| const resImicol = await runMetalPass('im2col') | ||
| if (resImicol.skipped) { | ||
| t.pass('skipped: Metal GPU not available on this host') | ||
| return | ||
| } | ||
| assertCorrect(resImicol.output, 'im2col') | ||
| t.comment('Pass B: forced ggml_conv_2d_direct (Metal)') | ||
| forceOnly('OCR_GGML_DIRECT_CONV') | ||
| const resDirect = await runMetalPass('direct') | ||
| if (resDirect.skipped) { | ||
| t.pass('skipped: Metal GPU not available for the direct pass') | ||
| return | ||
| } | ||
| assertCorrect(resDirect.output, 'direct') | ||
| // Equivalence: direct must match the im2col path region-for-region. | ||
| t.is(resDirect.output.length, resImicol.output.length, 'direct and im2col produce the same number of regions') | ||
| const n = Math.min(resDirect.output.length, resImicol.output.length) | ||
| for (let i = 0; i < n; i++) { | ||
| t.is(resDirect.output[i][1], resImicol.output[i][1], `region ${i}: direct text matches im2col ("${resImicol.output[i][1]}")`) | ||
| } | ||
| t.pass('direct-conv path matches im2col output on Metal') | ||
| } finally { | ||
| restoreEnv() | ||
| } | ||
| }) |
| 'use strict' | ||
| const { OcrGgml } = require('../..') | ||
| const test = require('brittle') | ||
| const { isMobile, getImagePath, ensureModelPath, safeUnload, findOpenCLBackendLib, PREBUILDS_DIR } = require('./utils') | ||
| // QVAC-19798: opt-in OpenCL GGML backend. Requesting `backendDevice: 'opencl'` | ||
| // must EITHER run inference on an OpenCL device, OR report an explicit CPU | ||
| // fallback — never silently produce wrong behaviour. CPU stays the default | ||
| // (covered by the rest of the suite), so this test only exercises the OpenCL | ||
| // opt-in path. | ||
| // | ||
| // OpenCL is primarily an Android/Adreno path (the `opencl` vcpkg port is gated | ||
| // to Android in this and every sibling GGML package). Unlike Vulkan, Adreno is | ||
| // NOT guarded off for OpenCL — it is Adreno's sound compute path. The OpenCL | ||
| // execution path can only be validated where a `libggml-opencl` backend shared | ||
| // library was shipped into prebuilds/. We gate on that file so the test skips | ||
| // cleanly on hosts that never built the OpenCL backend (e.g. plain desktop CI) | ||
| // instead of failing. On a host that ships the lib but has no OpenCL-capable | ||
| // GPU/driver, the selection falls back to CPU and we assert the fallback is | ||
| // reported explicitly. | ||
| const TEST_TIMEOUT = 120 * 1000 | ||
| const openclBackendLib = findOpenCLBackendLib(PREBUILDS_DIR) | ||
| // Skip on mobile (prebuilds layout / device provisioning differ) and on any | ||
| // host that did not ship an OpenCL backend lib. | ||
| const shouldSkip = isMobile || !openclBackendLib | ||
| test('backendDevice opencl: selects OpenCL or reports an explicit CPU fallback', { timeout: TEST_TIMEOUT, skip: shouldSkip }, async function (t) { | ||
| const detectorPath = await ensureModelPath('detector_craft') | ||
| const recognizerPath = await ensureModelPath('recognizer_latin') | ||
| const imagePath = getImagePath('/test/images/basic_test.bmp') | ||
| t.comment('OpenCL backend lib: ' + openclBackendLib) | ||
| const ocrGgml = new OcrGgml({ | ||
| params: { | ||
| pathDetector: detectorPath, | ||
| pathRecognizer: recognizerPath, | ||
| langList: ['en'], | ||
| backendDevice: 'opencl' | ||
| }, | ||
| opts: { stats: true } | ||
| }) | ||
| await ocrGgml.load() | ||
| t.pass('loaded with backendDevice: opencl') | ||
| const backendInfo = ocrGgml.getBackendInfo() | ||
| t.ok(backendInfo, 'getBackendInfo() returns backend info after load') | ||
| t.is(backendInfo.requested, 'opencl', 'requested device recorded as opencl') | ||
| t.is(typeof backendInfo.deviceIndex, 'number', 'deviceIndex is a number') | ||
| t.is(typeof backendInfo.backendDescription, 'string', 'backendDescription is a string') | ||
| t.comment('Resolved backend info: ' + JSON.stringify(backendInfo)) | ||
| const openclSelected = | ||
| backendInfo.backendDevice === 'GPU' || backendInfo.backendDevice === 'IGPU' | ||
| if (openclSelected) { | ||
| t.is(backendInfo.fallbackReason, '', 'no fallback reason when OpenCL is selected') | ||
| t.ok(/opencl/i.test(backendInfo.backendName), 'selected backend name mentions OpenCL') | ||
| // A selected GPU device reports its ggml device index (>= 0). | ||
| t.ok(backendInfo.deviceIndex >= 0, 'selected GPU reports a non-negative ggml deviceIndex') | ||
| } else { | ||
| // No OpenCL device available: the fallback to CPU MUST be reported. | ||
| t.is(backendInfo.backendDevice, 'CPU', 'fell back to the CPU device') | ||
| t.ok(backendInfo.fallbackReason.length > 0, 'explicit CPU fallback reason reported') | ||
| t.is(backendInfo.deviceIndex, -1, 'CPU fallback reports deviceIndex -1') | ||
| t.comment('CPU fallback reason: ' + backendInfo.fallbackReason) | ||
| } | ||
| try { | ||
| const response = await ocrGgml.run({ | ||
| path: imagePath, | ||
| options: { paragraph: false } | ||
| }) | ||
| let outputTexts = [] | ||
| await response | ||
| .onUpdate(output => { | ||
| t.ok(Array.isArray(output), 'output should be an array') | ||
| outputTexts = output.map(o => o[1]) | ||
| }) | ||
| .onError(error => { | ||
| t.fail('unexpected error: ' + JSON.stringify(error)) | ||
| }) | ||
| .await() | ||
| const stats = response.stats || {} | ||
| t.comment('Native addon stats: ' + JSON.stringify(stats)) | ||
| // The numeric `backendIsGpu` stat must agree with the resolved device. | ||
| t.is( | ||
| stats.backendIsGpu, | ||
| openclSelected ? 1 : 0, | ||
| 'backendIsGpu stat matches the resolved backend (' + backendInfo.backendDevice + ')' | ||
| ) | ||
| // Inference must succeed regardless of which backend was used. | ||
| t.ok(outputTexts.length > 0, 'inference produced text regions') | ||
| t.ok(outputTexts.includes('normal'), 'recognized expected text "normal"') | ||
| t.pass('backendDevice opencl path exercised (' + backendInfo.backendDevice + ')') | ||
| } finally { | ||
| await safeUnload(ocrGgml) | ||
| await new Promise(resolve => setTimeout(resolve, 1000)) | ||
| } | ||
| }) |
+8
-3
@@ -67,2 +67,6 @@ import type { QvacResponse } from '@qvac/infer-base' | ||
| * library is required. | ||
| * - `'opencl'`: opt in to GPU inference on an OpenCL-capable device | ||
| * (primarily Android/Adreno, where OpenCL is the sound GPU path). | ||
| * Requires the `libggml-opencl` backend shared library to be present in | ||
| * `backendsDir`. | ||
| * When the requested GPU device is not present the pipeline transparently | ||
@@ -72,5 +76,6 @@ * falls back to CPU and records the reason (see | ||
| */ | ||
| backendDevice?: 'cpu' | 'vulkan' | 'metal' | ||
| backendDevice?: 'cpu' | 'vulkan' | 'metal' | 'opencl' | ||
| /** | ||
| * Explicit GPU device selection for `'vulkan'` / `'metal'`. 0-based index | ||
| * Explicit GPU device selection for `'vulkan'` / `'metal'` / `'opencl'`. | ||
| * 0-based index | ||
| * into the GPU/iGPU devices that match the requested backend, in ggml | ||
@@ -95,3 +100,3 @@ * enumeration order (the resolved index is reported as | ||
| export interface BackendInfo { | ||
| /** Requested device (`'cpu'` | `'vulkan'` | `'metal'`). */ | ||
| /** Requested device (`'cpu'` | `'vulkan'` | `'metal'` | `'opencl'`). */ | ||
| requested: string | ||
@@ -98,0 +103,0 @@ /** Resolved device type (`'CPU'` | `'GPU'` | `'IGPU'` | `'ACCEL'`). */ |
+2
-2
@@ -31,4 +31,4 @@ 'use strict' | ||
| * @param {string} [args.params.backendsDir] - override directory for ggml backend shared libs | ||
| * @param {'cpu'|'vulkan'|'metal'} [args.params.backendDevice='cpu'] - requested ggml backend device. `'vulkan'` (Linux/Windows/Android) and `'metal'` (Apple) opt in to GPU inference with transparent CPU fallback when no matching device is present. | ||
| * @param {number} [args.params.gpuDevice] - explicit 0-based index into the matching GPU/iGPU devices for `'vulkan'`/`'metal'`. Omit to prefer a discrete GPU (then integrated); out-of-range falls back to CPU. Ignored for `'cpu'`. | ||
| * @param {'cpu'|'vulkan'|'metal'|'opencl'} [args.params.backendDevice='cpu'] - requested ggml backend device. `'vulkan'` (Linux/Windows/Android), `'metal'` (Apple) and `'opencl'` (Android/Adreno) opt in to GPU inference with transparent CPU fallback when no matching device is present. | ||
| * @param {number} [args.params.gpuDevice] - explicit 0-based index into the matching GPU/iGPU devices for `'vulkan'`/`'metal'`/`'opencl'`. Omit to prefer a discrete GPU (then integrated); out-of-range falls back to CPU. Ignored for `'cpu'`. | ||
| * @param {Object} [args.opts] | ||
@@ -35,0 +35,0 @@ * @param {boolean} [args.opts.stats] - emit timing stats on finish |
+1
-1
| { | ||
| "name": "@qvac/ocr-ggml", | ||
| "version": "0.2.2", | ||
| "version": "0.3.0", | ||
| "description": "GGML-backed OCR addon for qvac (EasyOCR pipeline on GGUF weights)", | ||
@@ -5,0 +5,0 @@ "addon": true, |
+101
-16
@@ -110,4 +110,4 @@ # @qvac/ocr-ggml | ||
| | `params.backendsDir` | `string` | | `<package>/prebuilds` | directory holding `libggml-*.so` backend shared libs | | ||
| | `params.backendDevice` | `'cpu'` \| `'vulkan'` \| `'metal'` | | `'cpu'` | ggml backend device. `'vulkan'` (Linux/Windows/Android) and `'metal'` (Apple) opt in to GPU inference with transparent CPU fallback — see [Backend device](#backend-device-cpu--vulkan--metal) | | ||
| | `params.gpuDevice` | `number` | | _prefer discrete_ | 0-based index into the matching GPU/iGPU devices for `'vulkan'`/`'metal'`; out-of-range → CPU fallback — see [Selecting a specific GPU](#selecting-a-specific-gpu-gpudevice) | | ||
| | `params.backendDevice` | `'cpu'` \| `'vulkan'` \| `'metal'` \| `'opencl'` | | `'cpu'` | ggml backend device. `'vulkan'` (Linux/Windows/Android), `'metal'` (Apple) and `'opencl'` (Android/Adreno) opt in to GPU inference with transparent CPU fallback — see [Backend device](#backend-device-cpu--vulkan--metal--opencl) | | ||
| | `params.gpuDevice` | `number` | | _prefer discrete_ | 0-based index into the matching GPU/iGPU devices for `'vulkan'`/`'metal'`/`'opencl'`; out-of-range → CPU fallback — see [Selecting a specific GPU](#selecting-a-specific-gpu-gpudevice) | | ||
| | `opts.stats` | `boolean` | | `false` | emit timing stats on `finish` | | ||
@@ -126,7 +126,7 @@ | `logger` | `Object` | | `null` | optional `{ info, warn, error, debug }` — receives C++ log lines | | ||
| ### Backend device (CPU / Vulkan / Metal) | ||
| ### Backend device (CPU / Vulkan / Metal / OpenCL) | ||
| By default inference runs on the **CPU** ggml backend, which is always | ||
| available. Set `params.backendDevice` to `'vulkan'` (Linux/Windows/Android) or | ||
| `'metal'` (Apple) to opt in to GPU inference: | ||
| available. Set `params.backendDevice` to `'vulkan'` (Linux/Windows/Android), | ||
| `'metal'` (Apple) or `'opencl'` (Android/Adreno) to opt in to GPU inference: | ||
@@ -139,3 +139,3 @@ ```js | ||
| langList: ['en'], | ||
| backendDevice: 'metal' // 'cpu' (default) | 'vulkan' | 'metal' | ||
| backendDevice: 'metal' // 'cpu' (default) | 'vulkan' | 'metal' | 'opencl' | ||
| } | ||
@@ -149,2 +149,4 @@ }) | ||
| // no Metal device → { requested: 'metal', backendDevice: 'CPU', backendName: 'CPU', deviceIndex: -1, backendDescription: '…', fallbackReason: 'Metal backend requested but no Metal-capable GPU device was found; falling back to CPU' } | ||
| // OpenCL missing OCR ops → { requested: 'opencl', backendDevice: 'CPU', backendName: 'CPU', deviceIndex: -1, backendDescription: '…', fallbackReason: "OpenCL backend 'GPUOpenCL' (QUALCOMM Adreno(TM) 830) does not implement the OCR vision ops (e.g. POOL_2D); falling back to CPU" } | ||
| // no OpenCL device → { requested: 'opencl', backendDevice: 'CPU', backendName: 'CPU', deviceIndex: -1, backendDescription: '…', fallbackReason: 'OpenCL backend requested but no OpenCL-capable GPU device was found; falling back to CPU' } | ||
| ``` | ||
@@ -154,5 +156,5 @@ | ||
| - **Transparent CPU fallback.** When `'vulkan'` / `'metal'` is requested but no | ||
| matching GPU device is registered, the pipeline falls back to CPU and | ||
| records a non-empty `fallbackReason` (also reflected by the numeric | ||
| - **Transparent CPU fallback.** When `'vulkan'` / `'metal'` / `'opencl'` is | ||
| requested but no matching GPU device is registered, the pipeline falls back | ||
| to CPU and records a non-empty `fallbackReason` (also reflected by the numeric | ||
| `backendIsGpu` stat). It never silently does the wrong thing. | ||
@@ -162,8 +164,22 @@ - **Required backend libs.** Vulkan execution needs the `libggml-vulkan` | ||
| `backendsDir` (default `<package>/prebuilds/<target>/`), plus a working | ||
| Vulkan driver/ICD and a Vulkan-capable GPU on the host. **Metal** is compiled | ||
| into the addon (no extra shared library), and is available whenever ggml was | ||
| built with the qvac-fabric `gpu-backends` feature (the default on Apple). | ||
| These GPU backends are only produced on platforms/feature sets where the | ||
| upstream ggml port builds them; on other hosts the request quietly falls back | ||
| to CPU. | ||
| Vulkan driver/ICD and a Vulkan-capable GPU on the host. **OpenCL** likewise | ||
| needs the `libggml-opencl` backend shared library plus a working OpenCL | ||
| runtime (`libOpenCL.so`); it is built primarily for **Android** (the `opencl` | ||
| vcpkg dependency is Android-only). **Metal** is compiled into the addon (no | ||
| extra shared library), and is available whenever ggml was built with the | ||
| qvac-fabric `gpu-backends` feature (the default on Apple). These GPU backends | ||
| are only produced on platforms/feature sets where the upstream ggml port | ||
| builds them; on other hosts the request quietly falls back to CPU. | ||
| - **OpenCL is the Adreno path — but gated on op support.** Qualcomm **Adreno** | ||
| GPUs are *skipped* on the auto Vulkan path (their Vulkan compute is | ||
| numerically broken) and are not rejected for `'opencl'` (OpenCL is Adreno's | ||
| sound GPU family). However, the OCR graphs are CNNs built from `POOL_2D`, | ||
| conv, upscale and transpose-conv ops, and **ggml's current OpenCL backend does | ||
| not implement them** (it is matmul-focused, for LLM-style workloads). To avoid | ||
| a hard `GGML_ABORT` on an unsupported op, selection runs a `POOL_2D` | ||
| op-support probe on the chosen GPU device; a backend that cannot run it | ||
| (today: OpenCL) transparently falls back to CPU with a `fallbackReason`. So | ||
| requesting `'opencl'` is **safe (never crashes)** but currently resolves to | ||
| **CPU** for OCR until ggml's OpenCL backend gains the required vision ops. | ||
| Vulkan/Metal pass the probe and run on GPU as before. | ||
| - **DocTR recognizer.** Only the MobileNetV3 feature-extractor graph runs on | ||
@@ -174,3 +190,3 @@ the selected ggml device; the recognizer's downstream LSTM + linear | ||
| - **Threads.** `nThreads` only affects the CPU backend; it is ignored when a | ||
| Vulkan or Metal device is selected. | ||
| Vulkan, Metal or OpenCL device is selected. | ||
| - **Performance guidance (Metal).** The win depends on the detector. The | ||
@@ -272,2 +288,59 @@ EasyOCR pipeline's CRAFT detector is dense-convolution and benefits strongly | ||
| ### 1×1 conv path (backend-aware; `OCR_GGML_CONV1X1_MULMAT` / `OCR_GGML_CONV1X1_CONV2D`) | ||
| A 1×1 convolution is a per-pixel linear map over channels — i.e. a plain matrix | ||
| multiply. The EasyOCR pipeline can run a **1×1, stride-1, no-padding** conv | ||
| either through `ggml_conv_2d` (im2col → GEMM) or a direct `ggml_mul_mat` that | ||
| skips the im2col lowering and its materialised buffer. This mainly affects the | ||
| CRAFT detector's 1×1 convs (the `upconv*.conv.0` legs, `basenet.slice5.2`, and | ||
| `conv_cls.6/.8`). | ||
| Skipping im2col helps GPU GEMM backends but adds permute/cont overhead that does | ||
| not pay off on CPU, so the default is **backend-aware**, resolved once at | ||
| model-load time (mirrors the F16 kernel decision): | ||
| | Resolved backend | 1×1 conv default | | ||
| |---|---| | ||
| | GPU / accelerator (NVIDIA Vulkan, Apple Metal, Mali Vulkan) | **`mul_mat`** (~−19% total / −43% detection on NVIDIA, ~−10% on Metal, ~neutral on Mali — output verified identical) | | ||
| | **Adreno** on **Vulkan** | **`conv_2d`** — Adreno's Vulkan compute is numerically fragile (and is already auto-skipped to CPU). Keyed on the backend API, so a future Adreno-OpenCL backend is not affected. | | ||
| | Any CPU (x86, Apple-Silicon, non-Apple ARM) | **`conv_2d`** (`mul_mat` is neutral-to-slower there) | | ||
| Two env vars override the default (read once at model load; only the exact value | ||
| `1` applies; `CONV2D` wins if both are set): | ||
| | Env var | Effect | | ||
| |---|---| | ||
| | `OCR_GGML_CONV1X1_MULMAT=1` | force the `mul_mat` path on every backend | | ||
| | `OCR_GGML_CONV1X1_CONV2D=1` | force the `ggml_conv_2d` path on every backend | | ||
| These are useful for A/B-benchmarking the two paths or as an escape hatch if a | ||
| backend's `mul_mat` path ever misbehaves. They do not affect the DocTR pipeline. | ||
| ### Direct conv path (backend-aware; `OCR_GGML_DIRECT_CONV` / `OCR_GGML_IM2COL_CONV`) | ||
| The non-pointwise (e.g. 3×3) convs can run either through `ggml_conv_2d` | ||
| (im2col → GEMM) or the fused `ggml_conv_2d_direct` (`GGML_OP_CONV_2D`). On the | ||
| **OpenCL** backend (Adreno) the im2col path rides a slow f16×f16 GEMV, so the | ||
| direct kernel is much faster there (the EasyOCR counterpart of the DocTR | ||
| `doctrConv2d` work). On CPU/Vulkan/Metal the im2col path is kept (direct is | ||
| ~2× slower on Metal). The default is therefore **backend-aware**, resolved once | ||
| at model-load time: | ||
| | Resolved backend | non-1×1 conv default | | ||
| |---|---| | ||
| | OpenCL (Adreno) | **`ggml_conv_2d_direct`** | | ||
| | CPU / Vulkan / Metal | **`ggml_conv_2d`** (im2col) | | ||
| Two env vars override the default (read once at model load; `IM2COL` wins if both | ||
| are set): | ||
| | Env var | Effect | | ||
| |---|---| | ||
| | `OCR_GGML_DIRECT_CONV=1` | force `ggml_conv_2d_direct` on every backend | | ||
| | `OCR_GGML_IM2COL_CONV=1` | force the `ggml_conv_2d` (im2col) path on every backend | | ||
| > Note: `ggml_conv_2d_direct` is only implemented on some backends; forcing it | ||
| > on a backend without `GGML_OP_CONV_2D` will abort. It does not affect the | ||
| > DocTR pipeline. | ||
| ### Conv bias broadcast (`OCR_GGML_CRAFT_BIAS_REPEAT`) | ||
@@ -288,2 +361,14 @@ | ||
| ### CRNN recognizer bias broadcast (`OCR_GGML_CRNN_BIAS_REPEAT`) | ||
| The EasyOCR **recognizer** applies the same broadcast to its sequence biases: | ||
| the BiLSTM `Linear` and the final `Prediction` add their `[F]` bias via | ||
| `ggml_add`'s implicit broadcast over the `(T, N)` axes, instead of materialising | ||
| a full `[F, T, N]` `ggml_repeat` copy. Numerically identical to the legacy path. | ||
| Set `OCR_GGML_CRNN_BIAS_REPEAT=1` to fall back to the legacy `ggml_repeat` | ||
| broadcast — the recognizer-side counterpart of `OCR_GGML_CRAFT_BIAS_REPEAT` | ||
| (read once at graph-build time; only the exact value `1` enables it). It does | ||
| not affect the DocTR pipeline. | ||
| ### `run(input)` shape | ||
@@ -290,0 +375,0 @@ |
@@ -335,2 +335,35 @@ 'use strict' | ||
| /** | ||
| * Recursively search a directory for a ggml OpenCL backend shared library | ||
| * (`*ggml-opencl*.so/.dll/.dylib`). Returns the full path of the first match, | ||
| * or null when none is found (including when the directory does not exist). | ||
| * Mirrors {@link findVulkanBackendLib}; OpenCL primarily ships on Android. | ||
| * @param {string} dir - directory to search (typically {@link PREBUILDS_DIR}) | ||
| * @returns {string|null} | ||
| */ | ||
| function findOpenCLBackendLib (dir) { | ||
| let entries | ||
| try { | ||
| entries = fs.readdirSync(dir) | ||
| } catch (_) { | ||
| return null | ||
| } | ||
| for (const name of entries) { | ||
| const full = path.join(dir, name) | ||
| let st | ||
| try { | ||
| st = fs.statSync(full) | ||
| } catch (_) { | ||
| continue | ||
| } | ||
| if (st.isDirectory()) { | ||
| const nested = findOpenCLBackendLib(full) | ||
| if (nested) return nested | ||
| } else if (/ggml-opencl/i.test(name) && /\.(so|dll|dylib)$/i.test(name)) { | ||
| return full | ||
| } | ||
| } | ||
| return null | ||
| } | ||
| /** | ||
| * Resolves the ggml backend device for the integration suite. | ||
@@ -340,3 +373,3 @@ * | ||
| * 1. Explicit `OCR_GGML_BACKEND` (via `os.getEnv` then `process.env`) wins — | ||
| * accepts a known GPU backend ('vulkan' or 'metal', case-insensitive), | ||
| * accepts a known GPU backend ('vulkan', 'metal' or 'opencl', case-insensitive), | ||
| * else 'cpu'. This preserves a manual override (e.g. workflow_dispatch / | ||
@@ -363,3 +396,3 @@ * forcing CPU, or forcing a specific GPU backend). | ||
| * 5. Else 'cpu' (desktop without a GPU backend). | ||
| * @returns {'cpu'|'vulkan'|'metal'} | ||
| * @returns {'cpu'|'vulkan'|'metal'|'opencl'} | ||
| */ | ||
@@ -372,3 +405,3 @@ function getBackendDevice () { | ||
| if (override !== '') { | ||
| return (override === 'vulkan' || override === 'metal') ? override : 'cpu' | ||
| return (override === 'vulkan' || override === 'metal' || override === 'opencl') ? override : 'cpu' | ||
| } | ||
@@ -991,3 +1024,20 @@ // Apple platforms (desktop + iOS) ship the Metal backend; request it on both. | ||
| if (!isGpuPass1) return pass1 | ||
| if (!isGpuPass1) { | ||
| // The auto backend (Vulkan on Android) ran on CPU. On Adreno GPUs the addon | ||
| // rejects Vulkan (numerically broken for DocTR) and falls back to CPU, so no | ||
| // [GPU] row is recorded. Retry once with OpenCL — the sound Adreno GPU path — | ||
| // so Adreno devices (e.g. Galaxy S25/S26) still report a GPU number. | ||
| if (isMobile && platform === 'android') { | ||
| const passCl = await runDoctrOCR(t, { ...params, backendDevice: 'opencl' }, imagePath) | ||
| const isGpuCl = !!(passCl.stats && passCl.stats.backendIsGpu === 1) | ||
| if (perfLabel) { | ||
| t.comment(formatOCRPerformanceMetrics(perfLabel, passCl.stats, passCl.results.map(r => r.text), perfOpts)) | ||
| } | ||
| if (typeof assertResult === 'function') { | ||
| assertResult(passCl.results, { stats: passCl.stats, isGpuPass: isGpuCl }) | ||
| } | ||
| return isGpuCl ? passCl : pass1 | ||
| } | ||
| return pass1 | ||
| } | ||
@@ -1011,2 +1061,3 @@ const pass2 = await runDoctrOCR(t, { ...params, backendDevice: 'cpu' }, imagePath) | ||
| findVulkanBackendLib, | ||
| findOpenCLBackendLib, | ||
| getBackendDevice, | ||
@@ -1013,0 +1064,0 @@ createOcrGgml, |
@@ -14,2 +14,7 @@ 'use strict' | ||
| async function runAndroidOpenclTest (options = {}) { // eslint-disable-line no-unused-vars -- called dynamically by the mobile test runner via string lookup | ||
| if (typeof __shouldRunTest === 'function' && !__shouldRunTest('runAndroidOpenclTest')) return __FILTERED | ||
| return runIntegrationModule('../integration/android-opencl.test.js', options) | ||
| } | ||
| async function runAndroidVulkanTest (options = {}) { // eslint-disable-line no-unused-vars -- called dynamically by the mobile test runner via string lookup | ||
@@ -40,2 +45,17 @@ if (typeof __shouldRunTest === 'function' && !__shouldRunTest('runAndroidVulkanTest')) return __FILTERED | ||
| async function runConv1x1MulmatTest (options = {}) { // eslint-disable-line no-unused-vars -- called dynamically by the mobile test runner via string lookup | ||
| if (typeof __shouldRunTest === 'function' && !__shouldRunTest('runConv1x1MulmatTest')) return __FILTERED | ||
| return runIntegrationModule('../integration/conv1x1-mulmat.test.js', options) | ||
| } | ||
| async function runCrnnBiasBroadcastTest (options = {}) { // eslint-disable-line no-unused-vars -- called dynamically by the mobile test runner via string lookup | ||
| if (typeof __shouldRunTest === 'function' && !__shouldRunTest('runCrnnBiasBroadcastTest')) return __FILTERED | ||
| return runIntegrationModule('../integration/crnn-bias-broadcast.test.js', options) | ||
| } | ||
| async function runDirectConvTest (options = {}) { // eslint-disable-line no-unused-vars -- called dynamically by the mobile test runner via string lookup | ||
| if (typeof __shouldRunTest === 'function' && !__shouldRunTest('runDirectConvTest')) return __FILTERED | ||
| return runIntegrationModule('../integration/direct-conv.test.js', options) | ||
| } | ||
| async function runDoctrBasicTest (options = {}) { // eslint-disable-line no-unused-vars -- called dynamically by the mobile test runner via string lookup | ||
@@ -116,2 +136,7 @@ if (typeof __shouldRunTest === 'function' && !__shouldRunTest('runDoctrBasicTest')) return __FILTERED | ||
| async function runOpenclBackendTest (options = {}) { // eslint-disable-line no-unused-vars -- called dynamically by the mobile test runner via string lookup | ||
| if (typeof __shouldRunTest === 'function' && !__shouldRunTest('runOpenclBackendTest')) return __FILTERED | ||
| return runIntegrationModule('../integration/opencl-backend.test.js', options) | ||
| } | ||
| async function runParamValidationTest (options = {}) { // eslint-disable-line no-unused-vars -- called dynamically by the mobile test runner via string lookup | ||
@@ -118,0 +143,0 @@ if (typeof __shouldRunTest === 'function' && !__shouldRunTest('runParamValidationTest')) return __FILTERED |
@@ -18,2 +18,3 @@ { | ||
| "regularB": [ | ||
| "runAndroidOpenclTest", | ||
| "runAndroidVulkanTest", | ||
@@ -24,2 +25,5 @@ "runBackendDeviceTest", | ||
| "runCanvasSizeTest", | ||
| "runConv1x1MulmatTest", | ||
| "runCrnnBiasBroadcastTest", | ||
| "runDirectConvTest", | ||
| "runImageFormatsTest", | ||
@@ -30,2 +34,3 @@ "runKernelPrecisionTest", | ||
| "runOcrBasicTest", | ||
| "runOpenclBackendTest", | ||
| "runParamValidationTest", | ||
@@ -56,2 +61,5 @@ "runPipelineTest", | ||
| "runCanvasSizeTest", | ||
| "runConv1x1MulmatTest", | ||
| "runCrnnBiasBroadcastTest", | ||
| "runDirectConvTest", | ||
| "runImageFormatsTest", | ||
@@ -62,2 +70,3 @@ "runKernelPrecisionTest", | ||
| "runOcrBasicTest", | ||
| "runOpenclBackendTest", | ||
| "runParamValidationTest", | ||
@@ -64,0 +73,0 @@ "runPipelineTest", |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
545430126
0.05%69
7.81%4876
13.66%650
15.04%26
44.44%