diff --git a/README.md b/README.md index 5636ca0..65d24dc 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,8 @@ ImageSharp 默认分配器会把像素拆成 4MB 块,大图上拿不到一整 | CI 架构 | Windows x64 / x86 / ARM64,Linux x64 / ARM64,macOS x64 / ARM64 | | SIMD | .NET 10 运行时探测 AVX → AVX2 → AVX-512 / VNNI;无对应指令集或 ARM 时用 Vector/标量 | | 输入 | 交错像素内存(默认 BGR24,也可 RGB24 / BGRA32 / RGBA32);无图片路径、文件或图片库 API | -| 设备 | CPU only,无 GPU | +| 设备 | 默认 CPU;`net10.0` 另有可选 Vulkan 后端(`OcrBackend`),直连系统加载器:Windows `vulkan-1.dll`、Linux `libvulkan.so.1`、Android `libvulkan.so`;`netstandard2.0` 只有 CPU | +| 安卓 | 目前通过开发用宿主 `test/Sdcb.SimdPaddleOCR.AndroidBench`(`net10.0-android`,引用 `net10.0` 库,adb 驱动)在骁龙 8 Gen 3 上跑 CPU 与 Vulkan,见 [`docs/vulkan-8gen3.md`](docs/vulkan-8gen3.md);桌面的 Vulkan 路由和 shader 没有变 | | NativeAOT | 裁剪发布时请保留核心程序集和所用模型程序集 | ## 许可证与第三方组件 diff --git a/README_EN.md b/README_EN.md index 3afd1c1..358692b 100644 --- a/README_EN.md +++ b/README_EN.md @@ -202,7 +202,8 @@ ImageSharp's default allocator splits pixels into 4MB chunks, so large images do | CI architectures | Windows x64 / x86 / ARM64, Linux x64 / ARM64, macOS x64 / ARM64 | | SIMD | .NET 10 probes AVX → AVX2 → AVX-512 / VNNI at runtime; Vector/scalar when those ISAs are missing or on ARM | | Input | Interleaved pixels (BGR24 by default; RGB24 / BGRA32 / RGBA32 also accepted); no image path, file, or image-library API | -| Device | CPU only, no GPU | +| Device | CPU by default; `net10.0` also has an optional Vulkan backend (`OcrBackend`) loading the system loader directly: `vulkan-1.dll` on Windows, `libvulkan.so.1` on Linux, `libvulkan.so` on Android; `netstandard2.0` is CPU only | +| Android | Currently run through the dev host `test/Sdcb.SimdPaddleOCR.AndroidBench` (`net10.0-android`, references the `net10.0` library, driven over adb) on a Snapdragon 8 Gen 3, CPU and Vulkan; see [`docs/vulkan-8gen3.md`](docs/vulkan-8gen3.md). Desktop Vulkan routing and shaders are unchanged | | NativeAOT | Keep the core assembly and the model assemblies you use when publishing trimmed | ## License and third-party components diff --git a/docs/perf.md b/docs/perf.md index 6881d20..ed14ac0 100644 --- a/docs/perf.md +++ b/docs/perf.md @@ -162,7 +162,7 @@ x64 / ARM64 本库 peak 大约少 **300 MB**。c 仍然略省,但更慢。Open ### Vulkan GPU(RTX 3080 Ti,`8d62a35`) -同机同轮:HOME-MAIN 5800X + RTX 3080 Ti(驱动 581.80),`.NET 10.0.11`,`--engine sharp|vulkan --workers 4 --benchmark-kind simd --warmup 1`,同一 `dataset/` 100 张变尺寸图(对 GPU 最不利的逐图新 shape)。墙钟 n=99。**CPU 列是同轮重测**,不要拿上表 1.4.2 的 `97c1448` mean 硬接(那次 tiny 63.1 / small 200 / medium 585)。B580 另一台机、另一份尺子,见 [vulkan-b580.md](vulkan-b580.md);Radeon 880M 核显见 [vulkan-880m.md](vulkan-880m.md)。Intel UHD 770 没有协作矩阵,`Auto` 仍走 CPU,显式 `Vulkan` 走较慢的无矩阵档,见 [vulkan-uhd770.md](vulkan-uhd770.md)。 +同机同轮:HOME-MAIN 5800X + RTX 3080 Ti(驱动 581.80),`.NET 10.0.11`,`--engine sharp|vulkan --workers 4 --benchmark-kind simd --warmup 1`,同一 `dataset/` 100 张变尺寸图(对 GPU 最不利的逐图新 shape)。墙钟 n=99。**CPU 列是同轮重测**,不要拿上表 1.4.2 的 `97c1448` mean 硬接(那次 tiny 63.1 / small 200 / medium 585)。B580 另一台机、另一份尺子,见 [vulkan-b580.md](vulkan-b580.md);Radeon 880M 核显见 [vulkan-880m.md](vulkan-880m.md)。Intel UHD 770 没有协作矩阵,`Auto` 仍走 CPU,显式 `Vulkan` 走较慢的无矩阵档,见 [vulkan-uhd770.md](vulkan-uhd770.md)。骁龙 8 Gen 3(Adreno 750,安卓)同样没有协作矩阵,但无矩阵档三档都快于本机 CPU,`Auto` 走 GPU,见 [vulkan-8gen3.md](vulkan-8gen3.md)。 端到端(median ms/图,越低越好;加速 = CPU median / Vulkan median): @@ -227,6 +227,7 @@ JSON:`bench-out/local-5800x-c-{tiny,small,medium}-4w.json`。 | 本机 5800X | — | `97c1448` / `68a009a` | 墙钟/内存 `97c1448`;CER 左 1:4 后同机复测(1.4.2) | | 本机 Vulkan | — | `8d62a35` | 5800X + 3080 Ti 同轮 sharp/vulkan 4w;tiny/small/medium;A/B 基线 `d6c260c` | | 880M Vulkan | — | `2520c77` | 锐龙 AI 9 H365 + Radeon 880M 同轮 sharp/vulkan 4w;A/B 基线 `713fc03` / `d6c260c` | +| 8 Gen 3 Vulkan | — | `vulkan-android-8gen3` 分支 | 真我 GT5 Pro,`test/Sdcb.SimdPaddleOCR.AndroidBench`,本机 CPU / Vulkan 交替 3 轮 4w;见 [vulkan-8gen3.md](vulkan-8gen3.md) | 推送或手动触发 [`.github/workflows/test.yml`](../.github/workflows/test.yml),下载 `perf-report` artifact。本地同一套数据: diff --git a/docs/vulkan-8gen3.md b/docs/vulkan-8gen3.md new file mode 100644 index 0000000..e4621b4 --- /dev/null +++ b/docs/vulkan-8gen3.md @@ -0,0 +1,145 @@ +# SimdPaddleOCR — 骁龙 8 Gen 3(Adreno 750)Vulkan + +实测机:真我 GT5 Pro(RMX3888),骁龙 8 Gen 3(SM8650),Adreno 750,16 GB LPDDR5X 与 GPU 共享,Android 16(API 36)。驱动报 Vulkan 1.3.128,`driverVersion`=0x802fa029(GLES 同版本串 V@0762.41)。进程是安卓应用 `test/Sdcb.SimdPaddleOCR.AndroidBench`(`net10.0-android`,CoreCLR,.NET 10.0.11),引用库的 `net10.0` 构建:CPU 走 AdvSimd 内核,Vulkan 直连系统 `libvulkan.so`。数据集用仓库 `dataset/` 100 张,`--workers 4 --warmup 1`,选项与桌面 `Tests` 相同,n=99。 + +**结论:Adreno 750 没有协作矩阵,compute subgroup 只能是 64 或 128,走无矩阵档(5.3 第 3 条)。原样上机时三档都算错(DET 概率图几乎全零,会话却报告 GPU 正常),修掉三处只在这颗 GPU 上暴露的问题后三档精度与本机 CPU 一致;再按 Adreno 调 GEMM 和两处路由,纯 GPU 快 14–33%。三档端到端都快于本机 CPU(tiny 约 1.3×,small 约 3.3×,medium 约 2.2×),所以 `Auto` 在“无协作矩阵且 subgroup 最小 64”的设备上改走 GPU。桌面 sg16 / sg32 / sg32l 的路由和 spv 没有改。** + +## 设备能力(`--caps` 探针) + +| 项 | 值 | +|---|---| +| 设备 | `Adreno (TM) 750`,vendor 0x5143,deviceId 0x43051401,`INTEGRATED_GPU`;加载的库是 `libvulkan.so` | +| subgroup | 默认 64;`VK_EXT_subgroup_size_control`,`sgRange` **64–128**,`requiredSubgroupSizeStages` 含 compute | +| 实际 lane 数 | 同一 `VkDevice.NewPipeline` 路径的探针 shader:请求 0 → `gl_SubgroupSize`=64,请求 64 → 64,请求 128 → 128;16 / 32 在范围外,按规范不能请求,没有请求 | +| cooperative matrix | 没有 `VK_KHR_cooperative_matrix`(`vkGetPhysicalDeviceCooperativeMatrixPropertiesKHR` 不导出),协作矩阵列表为空 | +| fp16 | `shaderFloat16`、16-bit storage 都有 | +| 共享内存 | `maxComputeSharedMemorySize` = **32 KB**;`maxComputeWorkGroupInvocations` 1024 | +| 绑定限制 | `maxStorageBufferRange` = **128 MB**;`minStorageBufferOffsetAlignment` = 64 B | +| 内存 | heap0 15203 MB、heap1 4095 MB,都 `DEVICE_LOCAL`;有 DL\|HV\|HC 和 DL\|HV\|HC\|CACHED 类型(输入缓冲落前者,回读落后者) | +| 队列 | family 0 `0x19f`×3(图形+计算),family 1 `0x2`×1(仅计算,被选中),family 2 `0x8`;三族 `timestampValidBits`=48,周期 52.08 ns,时间戳可用(探针 0.26 ms 一次 dispatch 读回正常);`globalPriority` 未授予(0) | +| push descriptor | 有 | +| 峰值 | `--peak`:纯寄存器 fp32 FMA **2.09 TFLOPS**;f16vec4 FMA 只有 0.91 TFLOPS(没有翻倍)。GEMM 必须 fp32 累加,按 2.09 算 | + +`CoopGemm` 为假、`SubgroupMin` > 16,所以 `GpuGraphModel` 进 `_nocm`,不创建任何 `conv1x1_cm*` / `convk_cm*` 管线。锁不住 8 lane,所以是新加的 `_ncWide` 子档。 + +## 走了哪一档 + +5.3 第 3 条:无协作矩阵。大 GEMM(1×1 里没被 dot 接住的、MatMul)走 `gemm_nc` 的 `-DDIRECT` 构建(`gemm_nc_d` / `gemm_nc_ds`),不请求 subgroup 大小(驱动给 64),内核不用 subgroup 内置量;kxk 稠密卷积一律走直接卷积 `convk_dot`;`conv1x1_dot` / `mmdot_sk` / `conv_dw*` / 其余内核照旧。能锁 8 lane 的 UHD 770 仍走原来的 `gemm_nc` / `gemm_nc_s`(`requiredSubgroupSize=8`),spv 逐字节不变。 + +## 上机后修掉的正确性问题 + +逐层对比用 `--layers`(把 GPU 图截断到节点 k,读回该节点输出的统计量),同一份代码链接进桌面 GpuBench,在 3080 Ti 上取参照;再用 `--gemm` 把单个 GEMM 和 fp64 参考比。 + +1. **`gemm_nc` 的寄存器预取在 Adreno 编译器下算错。** 第一处分叉就是第一个走 `gemm_nc_s` 的卷积;单测 6400×32×64 有 188871/204800 个输出错。fma 换乘加、if 换 select 都没用;去掉“下一块 A/B 先读进 `f16vec4` 数组”的预取、或把块缩到 32×32 / 4×4,就完全正确。改成 `-DDIRECT`:每块直接从全局读进共享内存。 +2. **标量常量上传只有 2 字节,Adreno 按绑定范围做越界检查。** `elem4` 按 `uint` 读 `b[0]`,桌面驱动不检查,Adreno 越界读回 0:CLS 头里乘常量那一步整个变 0,tiny CLS 只对 306/1022,转错方向的行又把 REC 拖到 CER 57%。`ConstF16` / `VecF16` 补齐到 16 字节的整数倍(只追加 0)。 +3. **im2col 矩阵超过 `maxStorageBufferRange`。** medium DET 960×960 的 7×7 卷积(M 57600,K 1568)im2col 有 180 MB,128 MB 之后全读成 0,medium 在大图上丢框(只检出 818 行,CER 13.2%)。放不进一个绑定时改走 `convk_dot`;仍会超范围的计划在运行前抛异常回退 CPU,不再返回半张零图。 + +试过但确认不需要的:把 arena slab 起点按 `minStorageBufferOffsetAlignment`(64 B)对齐、SE 计数器按 64 B 步进、把描述符 range 截到 128 MB。在这个驱动上都不改变任何输出(逐图、逐像素相同),所以没有留下;按规范它们仍属于未对齐 / 超范围的用法,换驱动时值得再看。 + +## 端到端(4 workers,median ms/图) + +同一时段 CPU / Vulkan 交替 3 轮(`ab.ps1`,轮间降温 60 s;tiny、small 第 2、1 轮的 CPU 因启动脚本竞态丢了,修好脚本后补测一轮,记在最后)。热状态(`PowerManager.CurrentThermalStatus`)tiny/small 为 2–3,medium 全程 3,机身明显发烫。 + +| 模型 | 本机 CPU | Vulkan | 对 CPU | +|---|---|---|---:| +| tiny | 218.8 / 202.6 / 276.4 | 156.1 / 170.4 / 154.9 / 151.7 | 约 1.3× | +| small | 867.1 / 785.0 / 811.7 | 251.3 / 247.3 / 246.2 / 255.1 | 约 3.3× | +| medium | 3073.8 / 3294.1 / 3368.9 | 1466.4 / 1458.1 / 1467.0 | 约 2.2× | + +- 发热是最大的误差来源:同一版本 medium Vulkan 在热状态 2 时 median 833 ms(det_graph 213、rec_graph 558),热状态 3 时 1460 ms(385 / 984)。纯 GPU 的最小值基本不受影响,受影响的是持续负载下的频率。表里 CPU / Vulkan 都是同一热状态下交替测的。 +- 工作集峰值:medium CPU 约 1.60 GB,Vulkan 约 1.09 GB;small 约 1.01 / 0.99 GB;tiny 约 0.96 / 0.95 GB。 + +分阶段 mean ms/图(第 3 轮): + +| 模型 | 阶段 | CPU | Vulkan | +|---|---|---:|---:| +| tiny | det_graph | 118.9 | 20.1 | +| tiny | rec_graph(CPU 为各线程累加) | 270.0 | 64.0 | +| tiny | lines_wall | 83.2 | 84.5 | +| small | det_graph | 384.4 | 40.2 | +| small | lines_wall | 382.5 | 166.2 | +| medium | det_graph | 1365.1 | 384.8 | +| medium | lines_wall | 2036.3 | 1010.8 | + +tiny 的收益全在检测上,识别墙钟与 CPU 4 线程持平。Vulkan 运行时 `crop` 阶段(CPU 做透视裁剪)从 9–12 ms 涨到 16–27 ms,推测是 GPU 忙时 CPU 频率被压低,没有再查。 + +### 精度 + +| 模型 | 本机 CPU exact_lines / CER | Vulkan exact_lines / CER | 逐图 hash/detected/texts 一致 | 框数一致 | +|---|---|---|---:|---:| +| tiny | 742 / 2.37% | 743 / 2.40% | 78/100 | 99/100 | +| small | 950 / 0.41% | 949 / 0.42% | 90/100 | 100/100 | +| medium | 1004 / 0.14% | 1003 / 0.15% | 97/100 | 100/100 | + +本机 CPU 三档与 Windows 基线(742 / 950 / 1004,CER 2.37% / 0.41% / 0.14%)完全一致;CLS 三档都是满分。DET 对拍(同一输入进 CPU / Vulkan 会话):tiny 10/10 图框数相同、概率图最大偏差 0.05–0.15(3080 Ti 对它自己的 CPU 是 0.03–0.18);medium 6/6 图框数相同、0.03–0.07。 + +并发(`--conc`,24 张 × 3):tiny 4 / 8 线程、small 4 线程、medium 4 线程、`Auto` tiny 4 线程,`mismatches=0`,日志里没有 fallback;medium 4 线程工作集峰值 980 MB,没有被系统杀掉。 + +## 纯 GPU + +`gprof.ps1`(逐 dispatch 时间戳,次数取最小,ms)。“调优前”是三处正确性修复之后、调优之前的版本;两列之间的每一步都做过同一时段交替 A/B,见各提交说明。 + +| 用例 | 调优前 | 调优后 | +|---|---:|---:| +| medium DET 960×960 | 380.5 | 298.6 | +| medium DET 640×960 | 270.1 | 205.2 | +| medium REC 8×480 | 358.9 | 242.3 | +| medium REC 1×320 | 40.0 | 34.4 | +| small DET 960×960 | 64.5 | 52.7 | +| small REC 8×480 | 72.6 | 54.5 | +| tiny DET 960×960 | 27.9 | 23.7 | +| tiny REC 8×480 | 15.9 | 12.3 | + +调优后热点:medium DET 是 `gemm_nc_ds` 92.6、`convk_dot` 88.9、`gemm_nc_d` 53.3、`conv1x1_dot` 19.4、`conv_dw4` 15.8、`conv_dw4t` 11.1;medium REC 是 `gemm_nc_ds` 110.9 + `gemm_nc_d` 95.7(合计 85%),其余是 `conv_dw4t` 20.6、`convk_dot` 6.3。 + +相对峰值:热点 GEMM 形状合计 0.64 TFLOPS,是 fp32 FMA 峰值 2.09 的 31%;`convk_dot` 在 57600×2304×64 上约 0.56 TFLOPS。 + +## 调优(每项都在手机上有收益才保留) + +1. **GEMM:fp16 共享内存 + BK 8。** `--gemmsweep` 在 11 个最热形状上交替比较(总 ms):原 64×64 / BK16 / fp32 暂存 109.5;BK 8 83.1;128×128、每线程 8×4 84.1;BK 8 + fp16 暂存 **76.2**(保留);同样配置换 128×64 / 64×128 块 74.6 / 75.0(噪声内,64×128 在 N=64 上浪费半块)。暂存的值本来就来自 fp16,逐位不变,e2e 与上一版 100/100。 +2. **两阶段空间均值在任意尺寸都用。** `reduce_hw` 一个工作组只算一个通道,在 Adreno 上按 C×2 字节跨步读;medium REC 8×480 269.7 → 243.3,medium DET 324.4 → 317.0。 +3. **kxk 稠密卷积一律直接卷积。** `CONVD_KMAX` 256 / 512 / 1024 / 2304:medium DET 315.2 / 312.3 / 304.0 / 299.3,small 64.6 / 60.7 / 52.8 / 52.8,tiny 32.8 / 29.9 / 23.7 / 23.7;640×960 216.9 → 206.2。直接卷积也不需要 im2col 暂存区。 + +重扫但保持原值的: + +- K≤128 的 dot → GEMM 门槛(M×cout > 2¹² / 2¹⁴ / 2¹⁶ / 2¹⁸ / 2²⁰):2¹²–2¹⁶ 相差 0.3 ms 以内,2¹⁸ 起 tiny DET 23.7 → 26.5,保持 2¹⁶。 +- `StreamCuts`(small 端到端,两轮):现有切分 median 234 / 235;不切分 254 / 290;0.3/0.6/0.85 236 / 258;0.7 252 / 262。CPU ArgMax 和后面的 GPU 波重叠在手机上仍然赚,保持原样。 + +## 试过但没留下的 + +| 尝试 | 结果 | +|---|---| +| 9×9 depthwise 走共享内存分块的 `conv_dw4t`(sg32 档允许到 9) | medium DET 303.7 → 317.0,small 52.7 → 55.3,更慢 | +| GEMM 不用共享内存,每线程直接按 f16vec4 读自己的 A/B 行 | 慢 2–4 倍;全展开的 8×8 版本又被编译器算错 | +| GEMM BK 4 / BK 32 / 32×32 4×4 块 | 83.2 / 200.6 / 148.1(对 76.2) | +| 共享内存按 f16vec4 向量读 | 76.8,与标量读相同 | +| fp16 乘法 fp32 累加 | 没做:f16vec4 FMA 峰值只有 fp32 的 0.43 倍,且会改变数值 | +| slab / 计数器按 64 B 对齐、描述符 range 截到 128 MB | 输出不变,没有收益 | + +## 留下的优化 + +- GEMM 只到峰值的 31%。不用共享内存、全靠缓存的版本慢 2–4 倍,推测 SSBO 读在这里命中不了足够近的缓存;下一步可以试通过纹理路径读(需要给管线加 texel buffer 绑定),没有验证。 +- medium DET 的 `convk_dot` 已经第二(88.9 ms),都是 7×7 / 大 K 卷积,M 方向 4 行一组,可以试更大的寄存器块。 +- REC 的 `conv_dw4t` 20 ms、DET 的 `conv_dw4` 16 ms。 +- 发热:持续跑 medium 时热状态到 3,端到端从 833 ms 涨到 1460 ms。更少的 dispatch / 更短的峰值比单纯缩短纯 GPU 时间更值。 +- 规范层面:未对齐的绑定偏移(64 B)和超出 128 MB 的描述符 range 在这个驱动上无害,但属于无效用法。 + +## `Auto` + +三档在所有交替轮次里 Vulkan 都快于本机 CPU,所以 `GpuBackend.UsesGpu` 对 `Auto` 在“没有协作矩阵、`SubgroupMin` ≥ 64”的设备上返回真(按能力判定,没有写死骁龙 / Adreno)。实测 `--backend auto`:usesGpu=True,与 `--backend vulkan` 逐图 100/100;`Auto` + `SIMD_OCR_BACKEND=cpu`:与 CPU 100/100。能锁 8 lane 的 UHD 770 和其它无矩阵设备(最小 subgroup < 64,例如不带协作矩阵驱动的 NVIDIA、Mali)没有在这条规则里,`Auto` 仍走 CPU,显式 `Vulkan` 才走 GPU。 + +## 桌面路径 + +sg16 / sg32 / sg32l 的 shader、spv 和选择条件都没动;全量重编后 `gemm_nc.spv`、`gemm_nc_s.spv` 和其余 spv 逐字节不变(`se_fused_dbg.spv` 删掉,不提交)。共享的 `GpuGraph` 改了两处会在桌面执行的地方:fp16 常量补齐到 16 字节、按 `maxStorageBufferRange` 的门槛和保护(桌面范围是 GB 级,不触发)。3080 Ti(驱动 581.80)同一时段对 `feature/2.0` 交替 2 轮,`--engine vulkan` median ms:tiny 18.0 / 19.9 → 18.5 / 20.7,small 26.7 / 30.3 → 29.2 / 28.5,medium 36.5 / 37.6 → 37.3 / 37.3(噪声内);精度 741 / 950 / 1006 不变;逐图 100/100(三档),`--engine sharp` tiny / small 100/100。B580、880M、UHD 770 没法测:它们都不满足 `SubgroupMin` ≥ 64,走的路由没变,只有常量缓冲补齐这一处共享改动。 + +## 复现 + +```text +cd test/Sdcb.SimdPaddleOCR.AndroidBench +.\run.ps1 -Build -Install -Push -Bench "--caps" # 推 bench-out/models 和 dataset/,打印能力 +.\run.ps1 -Bench "--e2e --model medium --backend vulkan" # JSON 与 logcat 拉回 bench-out/android +.\ab.ps1 -tag x -models tiny,small,medium -rounds 3 # CPU / Vulkan 交替 +.\gprof.ps1 -tag x # 纯 GPU +``` + +需要 .NET SDK ≥ 10.0.302 和 `android` workload、Android SDK platform 36、JDK 17/21。模型从手机存储读(medium 的 DET+REC 约 130 MB,不打进 APK)。 diff --git a/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/GpuBackend.cs b/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/GpuBackend.cs index cfe9d45..ab1b7c1 100644 --- a/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/GpuBackend.cs +++ b/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/GpuBackend.cs @@ -50,12 +50,14 @@ private static bool IsVulkanExplicit(OcrBackend backend) => /// Whether a session for this option runs on the GPU. Without an /// fp16 cooperative matrix of the cm shaders' shape () - /// only an explicit Vulkan choice gets the no-coopmat GEMM tier: it trails - /// the CPU on every model measured (docs/vulkan-uhd770.md), so Auto stays - /// on the CPU there. + /// Auto takes the no-coopmat GEMM tier only where compute subgroups cannot + /// go below 64 lanes: there (Adreno 750) it beats the CPU on every model + /// measured (docs/vulkan-8gen3.md), while on UHD 770, which pins 8 lanes, + /// it trails the CPU (docs/vulkan-uhd770.md). Other no-coopmat devices + /// only get the GPU for an explicit Vulkan choice. internal static bool UsesGpu(OcrBackend backend) => IsVulkanSelected(backend) && TryGetDevice() is { } dev - && (dev.CoopGemm || IsVulkanExplicit(backend)); + && (dev.CoopGemm || dev.SubgroupMin >= 64 || IsVulkanExplicit(backend)); /// Creates a session on the resolved backend; CPU on any GPU failure. internal static IOcrSession CreateSession(CompiledModel compiled, OcrBackend backend) diff --git a/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/GpuGraph.cs b/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/GpuGraph.cs index 7e475e1..bb3f296 100644 --- a/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/GpuGraph.cs +++ b/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/GpuGraph.cs @@ -81,11 +81,14 @@ internal void Release() private readonly bool _lite; // No coopmat of the selected cm set's shape (VkDevice.CoopGemm): // subgroup-free gemm_nc instead of every cm pipe - // (Intel's compiler aborts the process on those shaders). Sessions only - // reach this for an explicit OcrBackend.Vulkan — GpuBackend.UsesGpu. + // (Intel's compiler aborts the process on those shaders). Auto sessions + // reach this only on wave64-minimum devices — GpuBackend.UsesGpu. private readonly bool _nocm; // gemm_nc built with the unrolled no-act / relu / hardswish, single-output tail private readonly VkPipeline? _pGemmNcS; + // no-coopmat tier on a device that cannot pin a narrow subgroup (Adreno + // wave64): direct-load GEMM plus the routing measured on Adreno 750 + private readonly bool _ncWide; // fp16 copies of constant tensors (weights, biases, scalars), lazy private readonly Dictionary<(int, int, int, int), VkBuffer> _constF16 = new(); @@ -197,10 +200,13 @@ private GpuGraphModel(VkDevice dev, CompiledModel compiled) { // One tile; n64/n32 stay aliases so CmTile's sg16 cout split is // not reused here. 8 lanes keep its 64 fp32 accumulators in - // registers (UHD 770: SIMD16 spills, 4x slower). + // registers (UHD 770: SIMD16 spills, 4x slower). Without an + // 8-lane pin (wave64 Adreno) the register-prefetch form is + // miscompiled there; the direct-load build is exact and faster. bool sg8 = dev.ComputeSubgroupSize && dev.SubgroupMin <= 8 && dev.SubgroupMax >= 8; - _pConv1x1 = Pipe("gemm_nc", 6, 16, sg8 ? 8u : 0u); - _pGemmNcS = Pipe("gemm_nc_s", 6, 16, sg8 ? 8u : 0u); + _ncWide = !sg8; + _pConv1x1 = sg8 ? Pipe("gemm_nc", 6, 16, 8u) : Pipe("gemm_nc_d", 6, 16); + _pGemmNcS = sg8 ? Pipe("gemm_nc_s", 6, 16, 8u) : Pipe("gemm_nc_ds", 6, 16); _pConv1x1N64 = _pConv1x1; _pConv1x1N32 = _pConv1x1; } @@ -314,7 +320,9 @@ private VkBuffer ConstF16(int tensorIndex, int padRows = 0, int rowElems = 0, } else { - alloc = Math.Max(n, padRows * rowElems); + // whole 16 B: kernels read scalars as a packed half pair / f16vec4, + // and Adreno bounds-checks every load against the binding range + alloc = (Math.Max(n, padRows * rowElems) + 7) / 8 * 8; h = new Half[alloc]; for (int i = 0; i < n; i++) h[i] = (Half)f32[i]; } @@ -347,7 +355,7 @@ private ReadOnlySpan CstF32(int tensorIndex) => private VkBuffer VecF16((int, int) key, float[] v) { if (_vecF16.TryGetValue(key, out VkBuffer? cached)) return cached; - Half[] h = new Half[v.Length]; + Half[] h = new Half[(v.Length + 7) / 8 * 8]; for (int i = 0; i < v.Length; i++) h[i] = (Half)v[i]; VkBuffer buf = _dev.NewStorageBuffer((ulong)h.Length * 2, hostVisible: false); unsafe { fixed (Half* p = h) _dev.Upload(buf, p, (ulong)h.Length * 2); } @@ -665,6 +673,9 @@ bool Perm(int ni, params int[] perm) if (i == inIdx && inCinPad != 0) sz = numel[i] / shapes[i][1] * inCinPad; slabElems[i] = (sz + 7) / 8 * 8 + 128 * 64; + if (slabElems[i] * 2 > (long)_dev.MaxStorageRange) + throw new NotSupportedException( + $"tensor {i} ({sz * 2} B) exceeds maxStorageBufferRange {_dev.MaxStorageRange}"); cursor += slabElems[i]; } // the im2col scratch sits past every slab; its size (and the arena @@ -1407,10 +1418,15 @@ bool Absorbable(int cn, int srcPhys) // sg32: implicit-GEMM coopmat conv instead (same tap // addressing, tensor cores; any K, batched or not) bool convkCm = _sg32 && cinIn % 8 == 0 && scalarBias == 0; + // the im2col matrix is one binding: past + // maxStorageBufferRange (Adreno: 128 MB) go direct + bool im2colFits = (long)M * Kp * 2 <= (long)_dev.MaxStorageRange; + // wide no-coopmat parts: direct at every K measured + // (Kp <= 2304 in these models), im2col+GEMM never won if (convkCm || (cout % 4 == 0 && scalarBias == 0 && cinIn % 4 == 0 - && (nb > 1 || Kp <= (Environment.GetEnvironmentVariable( + && (nb > 1 || !im2colFits || Kp <= (Environment.GetEnvironmentVariable( "SIMD_OCR_CONVD_KMAX") is string km - ? int.Parse(km) : 1024)) + ? int.Parse(km) : _ncWide ? int.MaxValue : 1024)) && Environment.GetEnvironmentVariable("SIMD_OCR_NODCONV") == null)) { // stem conv on the fp32 NCHW input: skip nchw2nhwc @@ -1494,6 +1510,9 @@ bool Absorbable(int cn, int srcPhys) if (nb > 1) throw new NotSupportedException( $"im2col conv path lacks batch support (n{ni})"); + if (!im2colFits) + throw new NotSupportedException( + $"im2col matrix {M}x{Kp} exceeds maxStorageBufferRange (n{ni})"); maxIm2col = Math.Max(maxIm2col, (long)M * Kp); Emit(_pIm2col, $"im2col n{ni} M{M} K{K}", [(arena, SlotOf(node.Inputs[0]), 2), @@ -1717,9 +1736,9 @@ int ConstIn(NodeRecord nd, int other) seCtr += nb; // one ticket counter per batch break; } - // lite parts take it at any size: reduce_hw's one-channel - // workgroups read 2 bytes at a C*2 stride - if (c % 4 == 0 && 256 % cv4 == 0 && (hw >= 4096 || _lite)) + // lite and wide no-coopmat parts take it at any size: + // reduce_hw's one-channel workgroups read 2 bytes at a C*2 stride + if (c % 4 == 0 && 256 % cv4 == 0 && (hw >= 4096 || _lite || _ncWide)) { // two-phase: S pixel partitions → fp32 partials → mean int s = PartSplits(hw, c); diff --git a/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/Shaders/gemm_nc.comp b/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/Shaders/gemm_nc.comp index e538ff9..9aa76e0 100644 --- a/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/Shaders/gemm_nc.comp +++ b/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/Shaders/gemm_nc.comp @@ -15,6 +15,12 @@ // wider SIMD spills them. Bindings and the 16-byte push constant match // conv1x1_cm. // +// -DDIRECT (devices that cannot pin a narrow subgroup, e.g. Adreno wave64): +// each tile goes straight from global memory into shared memory, no +// register prefetch (Adreno's compiler miscomputes the prefetch form); +// tiles stay fp16 in shared memory with BK 8 (Adreno 750: 30% faster than +// fp32 tiles with BK 16), the tail stages through its own fp32 array. +// // flags: bit0 hasBias, bit1 hasResidual, bit3 dual output, bits[4:7] activation // act: 0=none 1=relu 2=gelu 3=hardswish 4=sigmoid // bit10 (1024): dual output stores the pre-activation value @@ -25,6 +31,9 @@ #ifndef BN #define BN 64u #endif +#if defined(DIRECT) && !defined(BK) +#define BK 8u +#endif #ifndef BK #define BK 16u #endif @@ -55,7 +64,15 @@ layout(push_constant) uniform PC { uint mDim; uint nDim; uint kDim; uint flags; // As = S[0, BK*BM), Bs = S[BK*BM, ...); after the K loop each thread stages // one accumulator row at S[tid*TN, +TN), so BK*(BM+BN) >= NTHR*TN +#ifdef DIRECT +shared float16_t S[BK * (BM + BN)]; +shared float St[NTHR * TN]; +#define LDS(v) float(v) +#else shared float S[BK * (BM + BN)]; +#define St S +#define LDS(v) (v) +#endif #define As(i) S[i] #define Bs(i) S[BK * BM + (i)] @@ -90,6 +107,7 @@ void main() for (uint i = 0u; i < TM * TN; i++) acc[i] = 0.0; f16vec4 z = f16vec4(float16_t(0.0)); +#ifndef DIRECT f16vec4 ra[AV]; f16vec4 rb[BV]; for (uint t = 0u; t < AV; t++) @@ -106,6 +124,7 @@ void main() uint gn = n0 + r, gk = cv * 4u; rb[t] = (e < BN * AK && gn < p.nDim && gk < p.kDim) ? w[gn * kv + (gk >> 2u)] : z; } +#endif for (uint k0 = 0u; k0 < p.kDim; k0 += BK) { @@ -115,7 +134,12 @@ void main() if (e < BM * AK) { uint r = e / AK, cv = e - r * AK; +#ifdef DIRECT + uint gm = m0 + r, gk = k0 + cv * 4u; + f16vec4 f = (gm < p.mDim && gk < p.kDim) ? x[gm * kv + (gk >> 2u)] : z; +#else vec4 f = vec4(ra[t]); +#endif As((cv * 4u + 0u) * BM + r) = f.x; As((cv * 4u + 1u) * BM + r) = f.y; As((cv * 4u + 2u) * BM + r) = f.z; @@ -128,7 +152,12 @@ void main() if (e < BN * AK) { uint r = e / AK, cv = e - r * AK; +#ifdef DIRECT + uint gn = n0 + r, gk = k0 + cv * 4u; + f16vec4 f = (gn < p.nDim && gk < p.kDim) ? w[gn * kv + (gk >> 2u)] : z; +#else vec4 f = vec4(rb[t]); +#endif Bs((cv * 4u + 0u) * BN + r) = f.x; Bs((cv * 4u + 1u) * BN + r) = f.y; Bs((cv * 4u + 2u) * BN + r) = f.z; @@ -136,6 +165,7 @@ void main() } } barrier(); +#ifndef DIRECT uint k1 = k0 + BK; if (k1 < p.kDim) { @@ -154,12 +184,13 @@ void main() rb[t] = (e < BN * AK && gn < p.nDim && gk < p.kDim) ? w[gn * kv + (gk >> 2u)] : z; } } +#endif for (uint kk = 0u; kk < BK; kk++) { float a[TM]; float b[TN]; - for (uint i = 0u; i < TM; i++) a[i] = As(kk * BM + ty * TM + i); - for (uint j = 0u; j < TN; j++) b[j] = Bs(kk * BN + tx * TN + j); + for (uint i = 0u; i < TM; i++) a[i] = LDS(As(kk * BM + ty * TM + i)); + for (uint j = 0u; j < TN; j++) b[j] = LDS(Bs(kk * BN + tx * TN + j)); for (uint i = 0u; i < TM; i++) for (uint j = 0u; j < TN; j++) acc[i * TN + j] = fma(a[i], b[j], acc[i * TN + j]); @@ -202,14 +233,14 @@ void main() // copies, which push the accumulators into spills for (uint i = 0u; i < TM; i++) { - for (uint j = 0u; j < TN; j++) S[tid * TN + j] = acc[i * TN + j]; + for (uint j = 0u; j < TN; j++) St[tid * TN + j] = acc[i * TN + j]; uint gm = mBase + i; if (gm >= p.mDim) continue; [[dont_unroll]] for (uint j = 0u; j < TN; j++) { uint gn = nBase + j; if (gn >= p.nDim) continue; - float v = S[tid * TN + j]; + float v = St[tid * TN + j]; if (hasBias) v += float(bias[gn]); if (hasRes) v += float(res[gm * p.nDim + gn]); o[gm * p.nDim + gn] = actf(v, act, p.flags); diff --git a/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/Shaders/gemm_nc_d.spv b/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/Shaders/gemm_nc_d.spv new file mode 100644 index 0000000..0a1a560 Binary files /dev/null and b/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/Shaders/gemm_nc_d.spv differ diff --git a/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/Shaders/gemm_nc_ds.spv b/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/Shaders/gemm_nc_ds.spv new file mode 100644 index 0000000..2353c8e Binary files /dev/null and b/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/Shaders/gemm_nc_ds.spv differ diff --git a/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/Vk.cs b/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/Vk.cs index 83da462..7a315ee 100644 --- a/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/Vk.cs +++ b/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/Vk.cs @@ -1,5 +1,6 @@ // Minimal Vulkan 1.1 P/Invoke for compute-only use (SimdPaddleOCR Vulkan backend). -// Loads vulkan-1.dll (Windows) / libvulkan.so.1 (Linux) via SetDllImportResolver. +// Loads vulkan-1.dll (Windows) / libvulkan.so.1 (Linux) / libvulkan.so (Android) +// via SetDllImportResolver. // Only the entry points and structs we need; all structs are blittable. using System.Runtime.InteropServices; @@ -11,6 +12,10 @@ internal static unsafe partial class Vk private static int _resolverRegistered; + // Android ships only the unversioned NDK name (/system/lib64/libvulkan.so) + private static readonly string[] s_candidates = OperatingSystem.IsWindows() + ? ["vulkan-1.dll"] : ["libvulkan.so.1", "libvulkan.so"]; + public static void RegisterResolver() { if (Interlocked.Exchange(ref _resolverRegistered, 1) != 0) @@ -19,11 +24,20 @@ public static void RegisterResolver() { if (name != LibName) return IntPtr.Zero; - string candidate = OperatingSystem.IsWindows() ? "vulkan-1.dll" : "libvulkan.so.1"; - return NativeLibrary.TryLoad(candidate, out IntPtr h) ? h : IntPtr.Zero; + foreach (string candidate in s_candidates) + if (NativeLibrary.TryLoad(candidate, out IntPtr h)) + { + LoadedLibrary = candidate; + return h; + } + throw new DllNotFoundException( + $"Vulkan loader not found (tried {string.Join(", ", s_candidates)})"); }); } + /// File name the resolver loaded, or null before the first call. + public static string? LoadedLibrary { get; private set; } + public const ulong WholeSize = ~0UL; public const uint QueueFamilyIgnored = ~0u; @@ -67,6 +81,10 @@ public uint MaxComputeSharedMemorySize { get { fixed (byte* p = LimitsAndSparse) return *(uint*)(p + 4 + 216); } } + public uint MaxStorageBufferRange + { + get { fixed (byte* p = LimitsAndSparse) return *(uint*)(p + 4 + 28); } + } } [StructLayout(LayoutKind.Sequential)] diff --git a/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/VkDevice.cs b/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/VkDevice.cs index a0519e5..e00c3f5 100644 --- a/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/VkDevice.cs +++ b/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/VkDevice.cs @@ -24,6 +24,7 @@ internal unsafe sealed class VkDevice : IDisposable public bool Coop8x16x16; // same types, MxNxK 8x16x16 (Arc) public uint SubgroupMin = 1, SubgroupMax = 128; public uint MaxSharedMemory; // maxComputeSharedMemorySize (bytes per workgroup) + public ulong MaxStorageRange = uint.MaxValue; // maxStorageBufferRange (bytes per binding) public int CoopM, CoopN, CoopK; // best fp16->fp32 subgroup coopmat config public bool Storage16Bit; // storageBuffer16BitAccess + shaderInt16 enabled public bool ShaderFloat16; // shaderFloat16 (VK_KHR_shader_float16_int8) enabled — required by fp16 shaders @@ -107,6 +108,7 @@ public static VkDevice Create(uint deviceIndex = 0) d.VendorId = props.VendorID; d.TimestampPeriodNs = props.TimestampPeriodNs; d.MaxSharedMemory = props.MaxComputeSharedMemorySize; + d.MaxStorageRange = props.MaxStorageBufferRange; uint nqf = 0; Vk.vkGetPhysicalDeviceQueueFamilyProperties(d.PhysDevice, &nqf, null); diff --git a/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/tools/build-shaders.bat b/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/tools/build-shaders.bat index 10b9624..f6ab17b 100644 --- a/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/tools/build-shaders.bat +++ b/src/Sdcb.SimdPaddleOCR/Backends/Vulkan/tools/build-shaders.bat @@ -51,3 +51,6 @@ rem nocm: subgroup-free fp16 GEMM (devices without 16x16x16 coopmat) echo glslc gemm_nc "%GLSLC%" -O --target-env=vulkan1.1 "%DIR%\gemm_nc.comp" -o "%DIR%\gemm_nc.spv" || exit /b 1 "%GLSLC%" -O --target-env=vulkan1.1 -DSIMPLE "%DIR%\gemm_nc.comp" -o "%DIR%\gemm_nc_s.spv" || exit /b 1 +rem nocm without a pinnable narrow subgroup (Adreno wave64): no register prefetch +"%GLSLC%" -O --target-env=vulkan1.1 -DDIRECT "%DIR%\gemm_nc.comp" -o "%DIR%\gemm_nc_d.spv" || exit /b 1 +"%GLSLC%" -O --target-env=vulkan1.1 -DDIRECT -DSIMPLE "%DIR%\gemm_nc.comp" -o "%DIR%\gemm_nc_ds.spv" || exit /b 1 diff --git a/src/Sdcb.SimdPaddleOCR/Properties/InternalsVisibleTo.cs b/src/Sdcb.SimdPaddleOCR/Properties/InternalsVisibleTo.cs index 6e3d50d..7dfcd06 100644 --- a/src/Sdcb.SimdPaddleOCR/Properties/InternalsVisibleTo.cs +++ b/src/Sdcb.SimdPaddleOCR/Properties/InternalsVisibleTo.cs @@ -2,3 +2,4 @@ [assembly: InternalsVisibleTo("Sdcb.SimdPaddleOCR.Tests")] [assembly: InternalsVisibleTo("Sdcb.SimdPaddleOCR.UnitTests")] [assembly: InternalsVisibleTo("Sdcb.SimdPaddleOCR.GpuBench")] +[assembly: InternalsVisibleTo("Sdcb.SimdPaddleOCR.AndroidBench")] diff --git a/test/Sdcb.SimdPaddleOCR.AndroidBench/Bench.cs b/test/Sdcb.SimdPaddleOCR.AndroidBench/Bench.cs new file mode 100644 index 0000000..fb93622 --- /dev/null +++ b/test/Sdcb.SimdPaddleOCR.AndroidBench/Bench.cs @@ -0,0 +1,49 @@ +namespace Sdcb.SimdPaddleOCR.AndroidBench; + +/// Mode dispatch. Relative paths resolve against the app's external files dir. +static class Bench +{ + internal static string OutDir = ""; + + internal static int Run(string[] args, string root, string outDir) + { + Paths.Root = root; + OutDir = outDir; + if (args.Length == 0) return Usage(); + return args[0] switch + { + "--init" => 0, + "--caps" => Caps.Run(args), + "--e2e" => E2e.Run(args), + "--detprof" or "--recprof" => Prof.Run(args), + "--detcmp" or "--reccmp" => Compare.Run(args), + "--conc" => Prof.Conc(args), + "--rawbench" => Caps.RawBench(args), + "--gemm" => GemmTest.Run(args), + "--gemmsweep" => GemmTest.Sweep(args), + "--peak" => GemmTest.Peak(args), + // --layers [first] [last] + "--layers" => LayerProbe.Run(Paths.ModelPath(args[1], args[2]), int.Parse(args[3]), int.Parse(args[4]), + int.Parse(args[5]), args.Length > 6 ? int.Parse(args[6]) : 0, + args.Length > 7 ? int.Parse(args[7]) : int.MaxValue), + _ => Usage(), + }; + } + + static int Usage() + { + Console.WriteLine(""" + --caps + --e2e --model tiny|small|medium --backend cpu|vulkan|auto [--workers 4] [--count N] [--warmup N] [--out name.json] + --detprof [reps] SIMD_OCR_GPU_PROF=1 for per-kernel times + --recprof [reps] + --detcmp [maxSide] CPU vs Vulkan prob map + boxes on the same input + --reccmp CPU vs Vulkan REC activations + CTC argmax + --conc [threads] [backend] + --gemm [flags] [tileM] [tileN] [reqSg] [reps] + --layers [first] [last] + --rawbench [pc uints...] + """); + return 2; + } +} diff --git a/test/Sdcb.SimdPaddleOCR.AndroidBench/Caps.cs b/test/Sdcb.SimdPaddleOCR.AndroidBench/Caps.cs new file mode 100644 index 0000000..1f31e10 --- /dev/null +++ b/test/Sdcb.SimdPaddleOCR.AndroidBench/Caps.cs @@ -0,0 +1,228 @@ +using System.Runtime.InteropServices; +using Sdcb.SimdPaddleOCR.Backends.Vulkan; + +namespace Sdcb.SimdPaddleOCR.AndroidBench; + +/// Device capability probe (M0) and single-dispatch timing. +static unsafe class Caps +{ + static string CompType(uint t) => t switch + { + 0 => "f16", 1 => "f32", 2 => "f64", 3 => "s8", 4 => "s16", 5 => "s32", 6 => "s64", + 7 => "u8", 8 => "u16", 9 => "u32", 10 => "u64", 1000141000 => "bf16", + _ => t.ToString(), + }; + + static string Scope(uint s) => s switch { 1 => "device", 2 => "workgroup", 3 => "subgroup", 5 => "queuefamily", _ => s.ToString() }; + + internal static int Run(string[] args) + { + Console.WriteLine($"phone: {Android.OS.Build.Manufacturer} {Android.OS.Build.Model} ({Android.OS.Build.Device}) " + + $"Android {Android.OS.Build.VERSION.Release} API {(int)Android.OS.Build.VERSION.SdkInt} soc={(OperatingSystem.IsAndroidVersionAtLeast(31) ? Android.OS.Build.SocModel : "?")}"); + Console.WriteLine($"runtime: {RuntimeInformation.FrameworkDescription} rid={RuntimeInformation.RuntimeIdentifier} " + + $"cpus={Environment.ProcessorCount} advsimd={System.Runtime.Intrinsics.Arm.AdvSimd.IsSupported} " + + $"dotprod={System.Runtime.Intrinsics.Arm.Dp.IsSupported} vector={System.Numerics.Vector.Count}x{System.Numerics.Vector.IsHardwareAccelerated} " + + $"lib={typeof(PaddleOcrAll).Assembly.GetCustomAttributes(typeof(System.Runtime.Versioning.TargetFrameworkAttribute), false).OfType().FirstOrDefault()?.FrameworkName}"); + + using var dev = VkDevice.Create(); + Console.WriteLine($"vulkan loader: {Vk.LoadedLibrary}"); + Vk.VkPhysicalDeviceProperties props; + Vk.vkGetPhysicalDeviceProperties(dev.PhysDevice, &props); + uint api = props.ApiVersion; + Console.WriteLine($"device: {dev.DeviceName} vendor=0x{props.VendorID:x} deviceId=0x{props.DeviceID:x} type={props.DeviceType} " + + $"api={api >> 22 & 0x7f}.{api >> 12 & 0x3ff}.{api & 0xfff} driver=0x{props.DriverVersion:x8}"); + + byte* lim = props.LimitsAndSparse + 4; // native VkPhysicalDeviceLimits is 8-aligned + Console.WriteLine($"limits: maxComputeSharedMemorySize={*(uint*)(lim + 216)} maxWgInvocations={*(uint*)(lim + 232)} " + + $"maxWgSize={*(uint*)(lim + 236)}x{*(uint*)(lim + 240)}x{*(uint*)(lim + 244)} " + + $"maxWgCount={*(uint*)(lim + 220)}x{*(uint*)(lim + 224)}x{*(uint*)(lim + 228)}"); + Console.WriteLine($"limits: maxStorageBufferRange={*(uint*)(lim + 28)} minStorageBufferOffsetAlignment={*(ulong*)(lim + 328)} " + + $"maxPushConstantsSize={*(uint*)(lim + 32)} maxMemoryAllocationCount={*(uint*)(lim + 36)} timestampPeriod={*(float*)(lim + 424)}ns"); + + uint nqf = 0; + Vk.vkGetPhysicalDeviceQueueFamilyProperties(dev.PhysDevice, &nqf, null); + var qf = stackalloc Vk.VkQueueFamilyProperties[(int)nqf]; + Vk.vkGetPhysicalDeviceQueueFamilyProperties(dev.PhysDevice, &nqf, qf); + for (uint i = 0; i < nqf; i++) + Console.WriteLine($"queue family {i}: flags=0x{qf[i].QueueFlags:x} count={qf[i].QueueCount} timestampValidBits={qf[i].TimestampValidBits}{(i == dev.QueueFamily ? " <- selected" : "")}"); + Console.WriteLine($"queue globalPriority={dev.QueuePriority}"); + + Console.WriteLine($"subgroup: default={dev.SubgroupSize} stages=0x{dev.SubgroupStages:x} ops=0x{dev.SubgroupOps:x} " + + $"sizeControl={dev.SubgroupSizeControl} sgRange={dev.SubgroupMin}-{dev.SubgroupMax} requiredStagesCompute={dev.ComputeSubgroupSize}"); + Console.WriteLine($"features: storage16={dev.Storage16Bit} shaderFloat16={dev.ShaderFloat16} coopMatrix={dev.CoopMatrix} " + + $"push={dev.PushDescriptors} coop16x16x16={dev.Coop16x16x16} coop8x16x16={dev.Coop8x16x16} best={dev.CoopM}x{dev.CoopN}x{dev.CoopK}"); + Console.WriteLine($"routing: Sg32Subgroup={dev.Sg32Subgroup} CoopGemm={dev.CoopGemm}"); + + for (int i = 0; i < (int)dev.MemProps.MemoryHeapCount; i++) + Console.WriteLine($"heap {i}: {dev.MemProps.HeapAt(i).Size >> 20} MB flags=0x{dev.MemProps.HeapAt(i).Flags:x}"); + for (int i = 0; i < (int)dev.MemProps.MemoryTypeCount; i++) + Console.WriteLine($"memtype {i}: flags=0x{dev.MemProps.TypeAt(i).PropertyFlags:x} heap={dev.MemProps.TypeAt(i).HeapIndex}"); + + uint next = 0; + Vk.vkEnumerateDeviceExtensionProperties(dev.PhysDevice, null, &next, null); + var exts = new Vk.VkExtensionProperties[next]; + fixed (Vk.VkExtensionProperties* pe = exts) + Vk.vkEnumerateDeviceExtensionProperties(dev.PhysDevice, null, &next, pe); + var names = new List(); + for (int i = 0; i < next; i++) + fixed (byte* n = exts[i].ExtensionName) names.Add(new string((sbyte*)n)); + Console.WriteLine($"extensions ({names.Count}): {string.Join(' ', names.OrderBy(x => x))}"); + + CoopList(dev); + SubgroupProbe(dev); + TimestampProbe(dev); + return 0; + } + + static void CoopList(VkDevice dev) + { + byte[] fn = System.Text.Encoding.ASCII.GetBytes("vkGetPhysicalDeviceCooperativeMatrixPropertiesKHR\0"); + IntPtr p; + fixed (byte* f = fn) p = Vk.vkGetInstanceProcAddr(dev.Instance, f); + if (p == IntPtr.Zero) { Console.WriteLine("coopmat: vkGetPhysicalDeviceCooperativeMatrixPropertiesKHR not exported"); return; } + var q = (delegate* unmanaged[Cdecl])p; + uint np = 0; + q(dev.PhysDevice, &np, null); + var cm = new Vk.VkCooperativeMatrixPropertiesKHR[np]; + for (int i = 0; i < np; i++) cm[i].SType = VkConst.StCooperativeMatrixPropertiesKHR; + fixed (Vk.VkCooperativeMatrixPropertiesKHR* pc = cm) q(dev.PhysDevice, &np, pc); + Console.WriteLine($"coopmat: {np} configurations"); + foreach (var c in cm) + Console.WriteLine($" coopmat {c.MSize}x{c.NSize}x{c.KSize} A={CompType(c.AType)} B={CompType(c.BType)} C={CompType(c.CType)} R={CompType(c.ResultType)} sat={c.SaturatingAccumulation} scope={Scope(c.Scope)}"); + } + + static byte[] Resource(string name) + { + using Stream s = typeof(Caps).Assembly.GetManifestResourceStream(name)!; + byte[] b = new byte[s.Length]; + s.ReadExactly(b); + return b; + } + + /// gl_SubgroupSize as seen by a pipeline built through the same + /// VkDevice.NewPipeline path the graph uses, per requiredSubgroupSize. + static void SubgroupProbe(VkDevice dev) + { + byte[] spv = Resource("sgsize.spv"); + foreach (uint req in new uint[] { 0, 16, 32, 64, 128 }) + { + if (req != 0 && !(dev.SubgroupSizeControl && dev.ComputeSubgroupSize && req >= dev.SubgroupMin && req <= dev.SubgroupMax)) + { + Console.WriteLine($"requiredSubgroupSize={req}: not requested (outside sgRange {dev.SubgroupMin}-{dev.SubgroupMax} or no compute size control; invalid usage)"); + continue; + } + try + { + var sp = dev.NewPipeline(dev.NewShaderModule(spv), 1, 4, req); + var set = dev.NewDescriptorSet(sp.SetLayout); + const int groups = 64; + var ob = dev.NewStorageBuffer(groups * 16, hostVisible: true, preferHost: true); + dev.BindBuffer(set, 0, ob); + IntPtr cmd = dev.NewCommandBuffer(); + IntPtr f = dev.NewFence(); + var begin = new Vk.VkCommandBufferBeginInfo { SType = VkConst.StCommandBufferBeginInfo }; + Vk.Check(Vk.vkBeginCommandBuffer(cmd, &begin), "begin"); + Vk.vkCmdBindPipeline(cmd, VkConst.BindPointCompute, sp.Pipeline); + Vk.vkCmdBindDescriptorSets(cmd, VkConst.BindPointCompute, sp.Layout, 0, 1, &set, 0, null); + Vk.vkCmdDispatch(cmd, groups, 1, 1); + Vk.Check(Vk.vkEndCommandBuffer(cmd), "end"); + dev.Submit(cmd, f); dev.WaitFence(f); + uint* o = (uint*)ob.Map(); + var sizes = new HashSet(); + for (int g = 0; g < groups; g++) sizes.Add($"gl_SubgroupSize={o[g * 4]} numSubgroups={o[g * 4 + 1]} lastSubgroupId={o[g * 4 + 2]} lastLane={o[g * 4 + 3]}"); + ob.Unmap(); + Console.WriteLine($"requiredSubgroupSize={req}: {string.Join(" | ", sizes)}"); + dev.DestroyFence(f); + ob.Free(); + } + catch (Exception ex) + { + Console.WriteLine($"requiredSubgroupSize={req}: FAILED {ex.Message}"); + } + } + } + + static void TimestampProbe(VkDevice dev) + { + byte[] spv = Resource("sgsize.spv"); + var sp = dev.NewPipeline(dev.NewShaderModule(spv), 1, 4, 0); + var set = dev.NewDescriptorSet(sp.SetLayout); + var ob = dev.NewStorageBuffer(1 << 20, hostVisible: false); + dev.BindBuffer(set, 0, ob); + IntPtr cmd = dev.NewCommandBuffer(); + IntPtr f = dev.NewFence(); + var qci = new Vk.VkQueryPoolCreateInfo { SType = 11, QueryType = 2, QueryCount = 2 }; + Vk.Check(Vk.vkCreateQueryPool(dev.Device, &qci, null, out IntPtr qp), "qp"); + var begin = new Vk.VkCommandBufferBeginInfo { SType = VkConst.StCommandBufferBeginInfo }; + Vk.Check(Vk.vkBeginCommandBuffer(cmd, &begin), "begin"); + Vk.vkCmdResetQueryPool(cmd, qp, 0, 2); + Vk.vkCmdBindPipeline(cmd, VkConst.BindPointCompute, sp.Pipeline); + Vk.vkCmdBindDescriptorSets(cmd, VkConst.BindPointCompute, sp.Layout, 0, 1, &set, 0, null); + Vk.vkCmdWriteTimestamp(cmd, VkConst.PipelineStageBottomOfPipe, qp, 0); + Vk.vkCmdDispatch(cmd, 16384, 1, 1); + Vk.vkCmdWriteTimestamp(cmd, VkConst.PipelineStageBottomOfPipe, qp, 1); + Vk.Check(Vk.vkEndCommandBuffer(cmd), "end"); + long t0 = System.Diagnostics.Stopwatch.GetTimestamp(); + dev.Submit(cmd, f); dev.WaitFence(f); + double wall = System.Diagnostics.Stopwatch.GetElapsedTime(t0).TotalMilliseconds; + ulong* ts = stackalloc ulong[2]; + var r = Vk.vkGetQueryPoolResults(dev.Device, qp, 0, 2, 16, ts, 8, 1 | 2); + Console.WriteLine($"timestamp probe: result={r} t0={ts[0]} t1={ts[1]} delta={(ts[1] - ts[0]) * dev.TimestampPeriodNs / 1e6:F4} ms (wall {wall:F3} ms, 16384 WGs x 256)"); + Vk.vkDestroyQueryPool(dev.Device, qp, null); + dev.DestroyFence(f); + ob.Free(); + } + + // --rawbench [pc uints...] + internal static int RawBench(string[] args) + { + using var dev = VkDevice.Create(); + byte[] spv = File.ReadAllBytes(Paths.Path_(args[1])); + int nbind = int.Parse(args[2]); + uint gx = uint.Parse(args[3]), gy = uint.Parse(args[4]); + int reps = int.Parse(args[5]); + ulong bytes = ulong.Parse(args[6]) << 20; + uint reqSg = uint.Parse(args[7]); + uint[] pcs = args.Skip(8).Select(uint.Parse).ToArray(); + int pcBytes = Math.Max(16, pcs.Length * 4); + var rp = dev.NewPipeline(dev.NewShaderModule(spv), nbind, pcBytes, reqSg); + var set = dev.NewDescriptorSet(rp.SetLayout); + var bufs = new List(); + for (int b = 0; b < nbind; b++) + { + var buf = dev.NewStorageBuffer(bytes, hostVisible: false); + dev.Zero(buf); + dev.BindBuffer(set, (uint)b, buf); + bufs.Add(buf); + } + IntPtr cmd = dev.NewCommandBuffer(); + IntPtr f = dev.NewFence(); + var qci = new Vk.VkQueryPoolCreateInfo { SType = 11, QueryType = 2, QueryCount = 2 }; + Vk.Check(Vk.vkCreateQueryPool(dev.Device, &qci, null, out IntPtr qp), "qp"); + uint[] pcv = new uint[pcBytes / 4]; + pcs.CopyTo(pcv, 0); + double best = double.MaxValue; + ulong* ts = stackalloc ulong[2]; + for (int r = 0; r < reps; r++) + { + var begin = new Vk.VkCommandBufferBeginInfo { SType = VkConst.StCommandBufferBeginInfo }; + Vk.Check(Vk.vkResetCommandBuffer(cmd, 0), "reset"); + Vk.Check(Vk.vkBeginCommandBuffer(cmd, &begin), "begin"); + Vk.vkCmdResetQueryPool(cmd, qp, 0, 2); + Vk.vkCmdBindPipeline(cmd, VkConst.BindPointCompute, rp.Pipeline); + Vk.vkCmdBindDescriptorSets(cmd, VkConst.BindPointCompute, rp.Layout, 0, 1, &set, 0, null); + fixed (uint* pp = pcv) Vk.vkCmdPushConstants(cmd, rp.Layout, VkConst.StageComputeShader, 0, (uint)pcBytes, pp); + Vk.vkCmdWriteTimestamp(cmd, VkConst.PipelineStageBottomOfPipe, qp, 0); + Vk.vkCmdDispatch(cmd, gx, gy, 1); + Vk.vkCmdWriteTimestamp(cmd, VkConst.PipelineStageBottomOfPipe, qp, 1); + Vk.Check(Vk.vkEndCommandBuffer(cmd), "end"); + dev.Submit(cmd, f); dev.WaitFence(f); + Vk.vkGetQueryPoolResults(dev.Device, qp, 0, 2, 16, ts, 8, 1 | 2); + best = Math.Min(best, (ts[1] - ts[0]) * dev.TimestampPeriodNs / 1e6); + } + Console.WriteLine($"rawbench best={best:F4} ms"); + Vk.vkDestroyQueryPool(dev.Device, qp, null); + foreach (var b in bufs) b.Free(); + return 0; + } +} diff --git a/test/Sdcb.SimdPaddleOCR.AndroidBench/Compare.cs b/test/Sdcb.SimdPaddleOCR.AndroidBench/Compare.cs new file mode 100644 index 0000000..553bcce --- /dev/null +++ b/test/Sdcb.SimdPaddleOCR.AndroidBench/Compare.cs @@ -0,0 +1,98 @@ +using Sdcb.SimdPaddleOCR.Backends.Vulkan; +using Sdcb.SimdPaddleOCR.OnnxSharp; + +namespace Sdcb.SimdPaddleOCR.AndroidBench; + +/// Same input through the CPU and Vulkan paths on the phone. +static class Compare +{ + internal static int Run(string[] args) => args[0] == "--detcmp" ? Det(args) : Rec(args); + + // --detcmp [maxSide] + static int Det(string[] args) + { + string model = args[1]; + int maxSide = args.Length > 3 ? int.Parse(args[3]) : 960; + string[] files = args[2] == "all" ? Paths.DatasetFiles() + : int.TryParse(args[2], out int n) ? Paths.DatasetFiles().Take(n).ToArray() + : [Paths.Path_(args[2])]; + var mdl = Model.Load(File.ReadAllBytes(Paths.ModelPath(model, "det"))); + var compiled = new CompiledModel(mdl, intraOpThreads: 4); + var opts = new PaddleOcrDetectorOptions(); + var scratch = new DbPostprocess.Workspace(); + using var sCpu = OcrSessionFactory.Create(compiled, OcrBackend.Cpu); + using var sGpu = OcrSessionFactory.Create(compiled, OcrBackend.Vulkan); + Console.WriteLine($"cpu session={sCpu.GetType().Name} gpu session={sGpu.GetType().Name}"); + int sameBoxes = 0; + foreach (string f in files) + { + var (bgr, w, h) = Paths.LoadBgr(f); + var size = PPOCRPreprocess.ComputeDetSize(w, h, maxSide); + int W = size.Width, H = size.Height; + float[][] maps = new float[2][]; + int[] nbox = new int[2]; + string[] boxStr = new string[2]; + IOcrSession[] ss = [sCpu, sGpu]; + for (int k = 0; k < 2; k++) + { + IOcrSession s = ss[k]; + s.Reshape([1, 3, H, W]); + PPOCRPreprocess.Det(bgr, w, h, w * 3, W, H, s.InputData, s.ResizeWorkspace, 1, s.InputIsNhwc, + ImagePixelFormat.Bgr24); + maps[k] = s.RunInternal(s.InputData).ToArray(); + var boxes = DbPostprocess.Run(maps[k], W, H, opts, w, h, size.WidthRatio, size.HeightRatio, scratch); + nbox[k] = boxes.Length; + boxStr[k] = string.Join(";", boxes.Select(b => $"{b.X1:F0},{b.Y1:F0},{b.X3:F0},{b.Y3:F0}")); + } + float[] pc = maps[0], pg = maps[1]; + double md = 0, sum = 0; int up = 0, dn = 0, nz = 0; + for (int i = 0; i < pc.Length; i++) + { + double d = Math.Abs(pc[i] - pg[i]); + md = Math.Max(md, d); sum += d; + if (pg[i] != 0) nz++; + if (pc[i] < 0.3f && pg[i] >= 0.3f) up++; + if (pc[i] >= 0.3f && pg[i] < 0.3f) dn++; + } + bool same = boxStr[0] == boxStr[1]; + if (same) sameBoxes++; + Console.WriteLine($"{Path.GetFileName(f)} {W}x{H}: maxAbs={md:F4} meanAbs={sum / pc.Length:E2} gpuNonZero={nz}/{pg.Length} flips(0.3) up={up} down={dn} boxes cpu={nbox[0]} gpu={nbox[1]} {(same ? "same" : "DIFF")} gpuAlive={((sGpu as GpuSession)?.GpuAlive.ToString() ?? "n/a")}"); + } + Console.WriteLine($"detcmp {model}: identical box lists {sameBoxes}/{files.Length}"); + return 0; + } + + // --reccmp : random input, activations before the CTC projection + static int Rec(string[] args) + { + string model = args[1]; + int nb = int.Parse(args[2]), W = int.Parse(args[3]); + var mdl = Model.Load(File.ReadAllBytes(Paths.ModelPath(model, "rec"))); + var compiled = new CompiledModel(mdl, intraOpThreads: 4); + int[] shape = [nb, 3, 48, W]; + var inp = new float[nb * 3 * 48 * W]; + var rr = new Random(1234); + for (int i = 0; i < inp.Length; i++) inp[i] = (float)(rr.NextDouble() * 2 - 1); + var cpu = new InferenceSession(compiled); + cpu.Reshape(shape); + if (!cpu.TryRunUntilCtcProjection(inp, out CtcProjectionOperands ops)) + { Console.WriteLine("cpu: no CTC projection tail"); return 2; } + float[] a = ops.Activations.ToArray(); + int mm = ops.MatMulNodeIndex; + using var dev = VkDevice.Create(); + using var gg = new GpuDetGraph(dev, compiled); + float[] g = gg.Run(shape, inp, mm, checked((int)mdl.Nodes[mm].Inputs[0])).ToArray(); + double md = 0; int nz = 0; + for (int i = 0; i < a.Length; i++) { md = Math.Max(md, Math.Abs(a[i] - g[i])); if (g[i] != 0) nz++; } + int rows = ops.RowCount; + int[] ic = new int[rows], ig = new int[rows]; + float[] sc = new float[rows], sg = new float[rows]; + Kernels.MatMul.TryArgMax(a, ops.Weights, ops.Bias, ic, sc, ops.Batch, ops.Rows, ops.Inner, ops.Columns, ops.PackedWeights, 4); + Kernels.MatMul.TryArgMax(g, ops.Weights, ops.Bias, ig, sg, ops.Batch, ops.Rows, ops.Inner, ops.Columns, ops.PackedWeights, 4); + int bad = 0, badText = 0; + for (int r = 0; r < rows; r++) + if (ic[r] != ig[r]) { bad++; if (ic[r] != 0) badText++; } + Console.WriteLine($"reccmp {model} {nb}x{W}: act n={a.Length} maxAbs={md:F4} gpuNonZero={nz}/{g.Length} argmaxDiff={bad}/{rows} (non-blank {badText})"); + return 0; + } +} diff --git a/test/Sdcb.SimdPaddleOCR.AndroidBench/E2e.cs b/test/Sdcb.SimdPaddleOCR.AndroidBench/E2e.cs new file mode 100644 index 0000000..04b6b05 --- /dev/null +++ b/test/Sdcb.SimdPaddleOCR.AndroidBench/E2e.cs @@ -0,0 +1,168 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Nodes; +using Sdcb.SimdPaddleOCR.OnnxSharp; +using Sdcb.SimdPaddleOCR.Tests; + +namespace Sdcb.SimdPaddleOCR.AndroidBench; + +/// +/// End-to-end dataset run with the desktop Tests harness's options and JSON +/// layout (rows: file / total_ms / hash / detected / texts / stage_ms ...), +/// so cmp.ps1, steady.ps1 and Tests --summarize work on the pulled file. +/// +static class E2e +{ + internal static int Run(string[] args) + { + string model = "tiny", backendName = "cpu"; + int workers = 4, warmup = 1; + int? count = null; + string? outName = null; + for (int i = 1; i < args.Length; i++) + { + string Next() => args[++i]; + switch (args[i]) + { + case "--model": model = Next(); break; + case "--backend": backendName = Next(); break; + case "--workers": workers = int.Parse(Next()); break; + case "--count": count = int.Parse(Next()); break; + case "--warmup": warmup = int.Parse(Next()); break; + case "--out": outName = Next(); break; + default: throw new ArgumentException($"unknown argument {args[i]}"); + } + } + OcrBackend backend = Paths.ParseBackend(backendName); + string metadataPath = Path.Combine(Paths.Dataset, "metadata.json"); + string[] files = Paths.DatasetFiles(); + if (files.Length != 100) throw new InvalidOperationException($"expected 100 images, got {files.Length}"); + if (count is { } c) files = files.Take(c).ToArray(); + + var decoded = files.Select(f => (Name: Path.GetFileName(f), Img: Paths.LoadBgr(f))).ToArray(); + + const int CacheEntries = 32; + using var ocr = PaddleOcrAll.Load(Paths.ModelPath(model, "det"), Paths.ModelPath(model, "cls"), + Paths.ModelPath(model, "rec"), Paths.DictPath(model), new PaddleOcrOptions + { + LineWorkerCount = workers, + Detector = new PaddleOcrDetectorOptions { MaxSessionCacheEntries = CacheEntries, Backend = backend }, + Recognizer = new PaddleOcrRecognizerOptions { AdaptiveWidth = true, TargetWidth = 320, Backend = backend }, + Classifier = new PaddleOcrClassifierOptions { Backend = backend }, + }); + PipelineProfiler.Enable(true); + var prev = PipelineProfiler.Snapshot(); + bool gpu = Backends.Vulkan.GpuBackend.UsesGpu(backend); + double wsLoaded = WorkingSetMb(); + Console.WriteLine($"loaded model={model} backend={backendName} usesGpu={gpu} workers={ocr.EffectiveLineWorkerCount}/{workers} ws={wsLoaded:F1}MB"); + + int warmupCount = Math.Min(warmup, Math.Max(0, decoded.Length - 1)); + var rows = new List(); + var boxes = new List(); + double wsPeak = wsLoaded; + for (int index = 0; index < decoded.Length; index++) + { + var (name, (bgr, w, h)) = decoded[index]; + var sw = Stopwatch.StartNew(); + PaddleOcrResult r = ocr.Run(bgr, w, h, w * 3); + sw.Stop(); + var cur = PipelineProfiler.Snapshot(); + var stageMs = new Dictionary(); + var stageCalls = new Dictionary(); + for (int s = 0; s < PipelineProfiler.StageCount; s++) + { + stageMs[PipelineProfiler.StageNames[s]] = cur[s].Milliseconds - prev[s].Milliseconds; + stageCalls[PipelineProfiler.StageNames[s]] = cur[s].Calls - prev[s].Calls; + } + prev = cur; + double ws = WorkingSetMb(); + wsPeak = Math.Max(wsPeak, ws); + rows.Add(new BenchmarkRow + { + File = name, Width = w, Height = h, Warmup = index < warmupCount, + TotalMs = sw.Elapsed.TotalMilliseconds, StageMs = stageMs, StageCalls = stageCalls, + Detected = r.DetectedCount, Lines = r.Lines.Length, Hash = $"{r.PackedTextHash:x16}", + Texts = r.Lines.Select(x => x.Text).ToArray(), + Rotations = r.Lines.Select(x => x.AppliedRotationDegrees).ToArray(), + WorkingSetMb = ws, + }); + boxes.Add(r.Lines.Select(x => Aabb(x.Box)).ToArray()); + Console.WriteLine($"{index + 1}/{decoded.Length} {name} total={sw.Elapsed.TotalMilliseconds:F3} det={stageMs["det_graph"]:F2} lines={r.Lines.Length} ws={ws:F1}MB"); + } + + var meta = new JsonObject + { + ["mode"] = backendName == "cpu" ? "sharp" : backendName, + ["benchmark"] = true, + ["benchmarkKind"] = "simd", + ["model"] = model, + ["workers"] = workers, + ["backend"] = backendName, + ["usesGpu"] = gpu, + ["effectiveWorkers"] = ocr.EffectiveLineWorkerCount, + ["cacheEntries"] = CacheEntries, + ["rid"] = RuntimeInformation.RuntimeIdentifier, + ["os"] = RuntimeInformation.OSDescription, + ["arch"] = RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant(), + ["cpu"] = Environment.ProcessorCount, + ["machine"] = $"{Android.OS.Build.Manufacturer} {Android.OS.Build.Model}", + ["timestamp"] = DateTimeOffset.Now.ToString("yyyy-MM-ddTHH:mm:sszzz"), + ["pixelFormat"] = "bgr", + ["working_set_mb_loaded"] = wsLoaded, + ["working_set_mb_peak"] = wsPeak, + ["working_set_mb_last"] = rows.Count > 0 ? rows[^1].WorkingSetMb : wsLoaded, + ["advSimdSupported"] = System.Runtime.Intrinsics.Arm.AdvSimd.IsSupported, + ["effectiveIsa"] = System.Runtime.Intrinsics.Arm.AdvSimd.IsSupported ? "advsimd" : "vector", + ["sampleCount"] = decoded.Length, + ["framework"] = RuntimeInformation.FrameworkDescription, + ["libraryTfm"] = typeof(PaddleOcrAll).Assembly + .GetCustomAttributes(typeof(System.Runtime.Versioning.TargetFrameworkAttribute), false) + .OfType().FirstOrDefault()?.FrameworkName, + ["thermal"] = Thermal(), + }; + var accuracy = BenchSummary.ComputeAccuracy(rows, metadataPath, boxes); + if (accuracy is not null) meta["accuracy"] = BenchSummary.AccuracyNode(accuracy); + JsonObject doc = BenchSummary.WrapWithMeta(rows, meta); + string outPath = Path.Combine(Bench.OutDir, outName ?? $"e2e-{model}-{backendName}.json"); + File.WriteAllText(outPath, doc.ToJsonString(new JsonSerializerOptions + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + })); + Console.WriteLine($"saved: {outPath}"); + BenchSummary.Print(BenchSummary.Parse(outPath, null, metadataPath)); + var measured = rows.Where(r => !r.Warmup).Select(r => r.TotalMs).OrderBy(x => x).ToArray(); + var tail = rows.Where(r => !r.Warmup).TakeLast(50).Select(r => r.TotalMs).OrderBy(x => x).ToArray(); + if (measured.Length > 0 && tail.Length > 0) + Console.WriteLine($"steady: all-med {measured[measured.Length / 2]:F1} last50-med {tail[tail.Length / 2]:F1} ws-peak {wsPeak:F0}MB thermal={Thermal()}"); + return 0; + } + + static float[] Aabb(PaddleOcrDetectionBox b) => + [ + Math.Min(Math.Min(b.X1, b.X2), Math.Min(b.X3, b.X4)), + Math.Min(Math.Min(b.Y1, b.Y2), Math.Min(b.Y3, b.Y4)), + Math.Max(Math.Max(b.X1, b.X2), Math.Max(b.X3, b.X4)), + Math.Max(Math.Max(b.Y1, b.Y2), Math.Max(b.Y3, b.Y4)), + ]; + + internal static double WorkingSetMb() + { + using Process p = Process.GetCurrentProcess(); + p.Refresh(); + return p.WorkingSet64 / (1024d * 1024d); + } + + /// PowerManager thermal status (0 none .. 6 shutdown) or -1. + internal static int Thermal() + { + try + { + var pm = (Android.OS.PowerManager?)Android.App.Application.Context.GetSystemService(Android.Content.Context.PowerService); + return pm is null || !OperatingSystem.IsAndroidVersionAtLeast(29) ? -1 : (int)pm.CurrentThermalStatus; + } + catch { return -1; } + } +} diff --git a/test/Sdcb.SimdPaddleOCR.AndroidBench/GemmTest.cs b/test/Sdcb.SimdPaddleOCR.AndroidBench/GemmTest.cs new file mode 100644 index 0000000..b6d6292 --- /dev/null +++ b/test/Sdcb.SimdPaddleOCR.AndroidBench/GemmTest.cs @@ -0,0 +1,246 @@ +using System.Text.RegularExpressions; +using Sdcb.SimdPaddleOCR.Backends.Vulkan; + +namespace Sdcb.SimdPaddleOCR.AndroidBench; + +/// +/// GEMM checks with the graph's conv1x1 binding layout (x, w[N,K], bias, +/// res, o, o2; pc = M, N, K, flags): one dispatch vs an fp64 reference, +/// a variant sweep over hot shapes, and an FMA peak probe. +/// +static unsafe class GemmTest +{ + // --gemm [flags] [tileM] [tileN] [reqSg] [reps] + internal static int Run(string[] args) + { + string shader = args[1]; + int M = int.Parse(args[2]), N = int.Parse(args[3]), K = int.Parse(args[4]); + uint flags = args.Length > 5 ? uint.Parse(args[5]) : 0; + uint tm = args.Length > 6 ? uint.Parse(args[6]) : 64, tn = args.Length > 7 ? uint.Parse(args[7]) : 64; + uint reqSg = args.Length > 8 ? uint.Parse(args[8]) : 0; + int reps = args.Length > 9 ? int.Parse(args[9]) : 10; + using var dev = VkDevice.Create(); + using var h = new Harness(dev, (long)M * K, (long)N * K, (long)M * N, N); + var pipe = dev.NewPipeline(dev.NewShaderModule(Spv(shader)), 6, 16, reqSg); + double best = h.Time(pipe, M, N, K, flags, tm, tn, reps); + var (maxErr, bad) = h.Verify(M, N, K, flags); + Console.WriteLine($"gemm {shader} M={M} N={N} K={K} flags={flags} tile={tm}x{tn} reqSg={reqSg}: best={best:F4} ms ({2.0 * M * N * K / (best * 1e-3) / 1e12:F3} TFLOPS) maxErr={maxErr:G4} bad={bad}/{(long)M * N}"); + return bad == 0 ? 0 : 3; + } + + static readonly (int M, int N, int K)[] s_hot = + [ + (5760, 1024, 512), (5760, 512, 1024), (2880, 768, 1536), (2880, 1536, 768), + (57600, 256, 128), (57600, 128, 256), (14400, 512, 256), (3600, 1024, 512), + (900, 896, 1792), (57600, 64, 256), (11520, 256, 512), + ]; + + // --gemmsweep [rounds] [flags]: every variant is checked on + // an odd shape against fp64, then timed on the hot shapes, variants + // interleaved per round; min per shape over rounds + internal static int Sweep(string[] args) + { + string[] shaders = args[1].Split(','); + int rounds = args.Length > 2 ? int.Parse(args[2]) : 3; + uint flags = args.Length > 3 ? uint.Parse(args[3]) : 0; + using var dev = VkDevice.Create(); + using var h = new Harness(dev, s_hot.Max(s => (long)s.M * s.K), s_hot.Max(s => (long)s.N * s.K), + s_hot.Max(s => (long)s.M * s.N), s_hot.Max(s => s.N)); + var pipes = new VkPipeline[shaders.Length]; + var tiles = new (uint, uint)[shaders.Length]; + for (int v = 0; v < shaders.Length; v++) + { + pipes[v] = dev.NewPipeline(dev.NewShaderModule(Spv(shaders[v])), 6, 16, 0); + tiles[v] = Tile(shaders[v]); + h.Time(pipes[v], 1000, 136, 264, flags | 3u, tiles[v].Item1, tiles[v].Item2, 1); + var (err, bad) = h.Verify(1000, 136, 264, flags | 3u); + Console.WriteLine($"variant {v} {shaders[v]} tile={tiles[v].Item1}x{tiles[v].Item2}: verify maxErr={err:G3} bad={bad}"); + } + var best = new double[shaders.Length, s_hot.Length]; + for (int v = 0; v < shaders.Length; v++) for (int s = 0; s < s_hot.Length; s++) best[v, s] = double.MaxValue; + for (int r = 0; r < rounds; r++) + for (int s = 0; s < s_hot.Length; s++) + for (int v = 0; v < shaders.Length; v++) + { + var (M, N, K) = s_hot[s]; + best[v, s] = Math.Min(best[v, s], h.Time(pipes[v], M, N, K, flags, tiles[v].Item1, tiles[v].Item2, 3)); + } + Console.WriteLine("shape".PadRight(18) + string.Concat(Enumerable.Range(0, shaders.Length).Select(v => $"{"v" + v,10}"))); + var sum = new double[shaders.Length]; + for (int s = 0; s < s_hot.Length; s++) + { + var (M, N, K) = s_hot[s]; + var line = new System.Text.StringBuilder($"{M}x{N}x{K}".PadRight(18)); + for (int v = 0; v < shaders.Length; v++) { line.Append($"{best[v, s],10:F3}"); sum[v] += best[v, s]; } + Console.WriteLine(line); + } + Console.WriteLine("sum ms".PadRight(18) + string.Concat(sum.Select(x => $"{x,10:F2}"))); + double flop = s_hot.Sum(s => 2.0 * s.M * s.N * s.K); + Console.WriteLine("TFLOPS".PadRight(18) + string.Concat(sum.Select(x => $"{flop / (x * 1e-3) / 1e12,10:F3}"))); + Console.WriteLine($"thermal={E2eThermal()}"); + return 0; + } + + // --peak: fp32 / fp16 FMA throughput of 16 independent vec4 chains per thread + internal static int Peak(string[] args) + { + using var dev = VkDevice.Create(); + using var h = new Harness(dev, 4096, 4096, 4096, 64); + foreach (string name in new[] { "peak.spv", "peak16.spv" }) + { + using Stream s = typeof(GemmTest).Assembly.GetManifestResourceStream(name)!; + byte[] b = new byte[s.Length]; + s.ReadExactly(b); + var pipe = dev.NewPipeline(dev.NewShaderModule(b), 6, 16, 0); + const uint groups = 4096; + double best = h.Time(pipe, 0, 0, 0, 0, 0, 0, 10, groups); + double flop = groups * 256.0 * 32768; + Console.WriteLine($"peak {name}: {best:F3} ms -> {flop / (best * 1e-3) / 1e12:F3} TFLOPS"); + } + return 0; + } + + static int E2eThermal() + { +#if ANDROID + return E2e.Thermal(); +#else + return -1; +#endif + } + + static (uint, uint) Tile(string shader) + { + var bm = Regex.Match(shader, @"bm(\d+)"); + var bn = Regex.Match(shader, @"bn(\d+)"); + return (bm.Success ? uint.Parse(bm.Groups[1].Value) : 64u, bn.Success ? uint.Parse(bn.Groups[1].Value) : 64u); + } + + static byte[] Spv(string shader) + { + if (shader.EndsWith(".spv")) return File.ReadAllBytes(shader); + using Stream s = typeof(VkDevice).Assembly.GetManifestResourceStream( + $"Sdcb.SimdPaddleOCR.Backends.Vulkan.Shaders.{shader}.spv") ?? throw new FileNotFoundException(shader); + byte[] b = new byte[s.Length]; + s.ReadExactly(b); + return b; + } + + /// Random fp16 operands sized for the largest shape, bound once. + sealed class Harness : IDisposable + { + readonly VkDevice _dev; + readonly Half[] _x, _w, _bias, _res; + readonly VkBuffer[] _binds; + readonly IntPtr _cmd, _fence, _qp; + + public Harness(VkDevice dev, long xElems, long wElems, long mn, int maxN) + { + _dev = dev; + var rr = new Random(3); + mn = Math.Max(mn, 1000 * 136); + _x = new Half[Math.Max(xElems, 1000 * 264) + 128 * 512]; + _w = new Half[Math.Max(wElems, 136 * 264) + 128 * 2048]; + _bias = new Half[Math.Max(maxN, 136) + 8]; + _res = new Half[mn]; + for (long i = 0; i < _x.Length; i++) _x[i] = (Half)(rr.NextDouble() * 2 - 1); + for (long i = 0; i < _w.Length; i++) _w[i] = (Half)((rr.NextDouble() * 2 - 1) * 0.1); + for (int i = 0; i < _bias.Length; i++) _bias[i] = (Half)(rr.NextDouble() - 0.5); + for (long i = 0; i < _res.Length; i++) _res[i] = (Half)(rr.NextDouble() - 0.5); + ulong outBytes = (ulong)mn * 2 + 4096; + _binds = [Up(_x), Up(_w), Up(_bias), Up(_res), + dev.NewStorageBuffer(outBytes, hostVisible: true, preferHost: true), + dev.NewStorageBuffer(outBytes, hostVisible: true, preferHost: true)]; + _cmd = dev.NewCommandBuffer(); + _fence = dev.NewFence(); + var qci = new Vk.VkQueryPoolCreateInfo { SType = 11, QueryType = 2, QueryCount = 2 }; + Vk.Check(Vk.vkCreateQueryPool(dev.Device, &qci, null, out _qp), "qp"); + } + + VkBuffer Up(Half[] h) + { + var b = _dev.NewStorageBuffer((ulong)Math.Max(h.Length, 64) * 2, hostVisible: false); + fixed (Half* p = h) _dev.Upload(b, p, (ulong)h.Length * 2); + return b; + } + + /// Min GPU time (ms) of dispatches. The + /// operands are laid out for the call's own (M, N, K). + public double Time(VkPipeline pipe, int M, int N, int K, uint flags, uint tm, uint tn, int reps, uint groups = 0) + { + IntPtr set = _dev.NewDescriptorSet(pipe.SetLayout); + for (uint i = 0; i < 6; i++) _dev.BindBuffer(set, i, _binds[i]); + uint gx = groups != 0 ? groups : (uint)((M + tm - 1) / tm), gy = groups != 0 ? 1 : (uint)((N + tn - 1) / tn); + uint[] pc = [(uint)M, (uint)N, (uint)K, flags]; + double best = double.MaxValue; + ulong* ts = stackalloc ulong[2]; + for (int r = 0; r < reps; r++) + { + var begin = new Vk.VkCommandBufferBeginInfo { SType = VkConst.StCommandBufferBeginInfo }; + Vk.Check(Vk.vkResetCommandBuffer(_cmd, 0), "reset"); + Vk.Check(Vk.vkBeginCommandBuffer(_cmd, &begin), "begin"); + Vk.vkCmdResetQueryPool(_cmd, _qp, 0, 2); + Vk.vkCmdBindPipeline(_cmd, VkConst.BindPointCompute, pipe.Pipeline); + Vk.vkCmdBindDescriptorSets(_cmd, VkConst.BindPointCompute, pipe.Layout, 0, 1, &set, 0, null); + fixed (uint* pp = pc) Vk.vkCmdPushConstants(_cmd, pipe.Layout, VkConst.StageComputeShader, 0, 16, pp); + Vk.vkCmdWriteTimestamp(_cmd, VkConst.PipelineStageBottomOfPipe, _qp, 0); + Vk.vkCmdDispatch(_cmd, gx, gy, 1); + Vk.vkCmdWriteTimestamp(_cmd, VkConst.PipelineStageBottomOfPipe, _qp, 1); + Vk.Check(Vk.vkEndCommandBuffer(_cmd), "end"); + _dev.Submit(_cmd, _fence); _dev.WaitFence(_fence); + Vk.vkGetQueryPoolResults(_dev.Device, _qp, 0, 2, 16, ts, 8, 1 | 2); + best = Math.Min(best, (ts[1] - ts[0]) * _dev.TimestampPeriodNs / 1e6); + } + _dev.FreeDescriptorSet(set); + return best; + } + + /// Checks the last dispatch's output (the operands as laid out + /// for this M, N, K) against fp64. + public (double MaxErr, long Bad) Verify(int M, int N, int K, uint flags) + { + Half* o = (Half*)_binds[4].Map(); + bool hasBias = (flags & 1) != 0, hasRes = (flags & 2) != 0; + uint act = (flags >> 4) & 7; + double maxErr = 0; long bad = 0; + for (int m = 0; m < M; m++) + for (int n = 0; n < N; n++) + { + double acc = 0; + for (int k = 0; k < K; k++) acc += (double)_x[(long)m * K + k] * (double)_w[(long)n * K + k]; + if (hasBias) acc += (double)_bias[n]; + if (hasRes) acc += (double)_res[(long)m * N + n]; + acc = act switch + { + 1 => Math.Max(acc, 0), + 2 => 0.5 * acc * (1 + Erf(acc / Math.Sqrt(2))), + 3 => acc * Math.Clamp(acc * (double)BitConverter.UInt16BitsToHalf((ushort)(flags >> 16)) + 0.5, 0, 1), + 4 => 1 / (1 + Math.Exp(-acc)), + _ => acc, + }; + double g = (double)o[(long)m * N + n]; + double e = double.IsNaN(g) ? double.PositiveInfinity : Math.Abs(g - acc); + maxErr = Math.Max(maxErr, e); + if (e > 0.02 + 0.01 * Math.Abs(acc)) bad++; + } + _binds[4].Unmap(); + return (maxErr, bad); + } + + public void Dispose() + { + Vk.vkDestroyQueryPool(_dev.Device, _qp, null); + _dev.DestroyFence(_fence); + foreach (var b in _binds) b.Free(); + } + } + + // Abramowitz-Stegun 7.1.26 (|err| < 1.5e-7) + static double Erf(double v) + { + double s = Math.Sign(v); v = Math.Abs(v); + double t = 1 / (1 + 0.3275911 * v); + double y = 1 - ((((1.061405429 * t - 1.453152027) * t + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t * Math.Exp(-v * v); + return s * y; + } +} diff --git a/test/Sdcb.SimdPaddleOCR.AndroidBench/LayerProbe.cs b/test/Sdcb.SimdPaddleOCR.AndroidBench/LayerProbe.cs new file mode 100644 index 0000000..9470bc1 --- /dev/null +++ b/test/Sdcb.SimdPaddleOCR.AndroidBench/LayerProbe.cs @@ -0,0 +1,53 @@ +using Sdcb.SimdPaddleOCR.Backends.Vulkan; +using Sdcb.SimdPaddleOCR.OnnxSharp; + +namespace Sdcb.SimdPaddleOCR.AndroidBench; + +/// +/// Bisection aid: truncates the GPU graph after node k and reads that node's +/// output back through the graph's own fp32 writeback, printing statistics. +/// Plain .NET, so the desktop GpuBench links it too for reference numbers. +/// +static class LayerProbe +{ + // --layers [firstNode] [lastNode] + internal static int Run(string onnx, int n, int h, int w, int first = 0, int last = int.MaxValue) + { + var mdl = Model.Load(File.ReadAllBytes(onnx)); + var compiled = new CompiledModel(mdl, intraOpThreads: 4); + int[] shape = [n, 3, h, w]; + var inp = new float[n * 3 * h * w]; + var rr = new Random(7); + for (int i = 0; i < inp.Length; i++) inp[i] = (float)(rr.NextDouble() * 2 - 1); + using var dev = VkDevice.Create(); + using var gg = new GpuDetGraph(dev, compiled); + Console.WriteLine($"device={dev.DeviceName} nodes={mdl.Nodes.Length}"); + var nodes = mdl.Nodes; + for (int k = first; k <= Math.Min(last, nodes.Length - 1); k++) + { + int t = checked((int)nodes[k].Outputs[0]); + GpuSchedule s; + try { s = gg.Prepare(shape, k + 1, t); } + catch (Exception ex) { Console.WriteLine($"n{k,-4} {nodes[k].Operator,-14} plan failed: {ex.GetBaseException().Message}"); continue; } + int p = t; + while (s.Alias[p] != p) p = s.Alias[p]; + if (s.Off[p] < 0 || s.Recs.Length < 2) + { + if (Environment.GetEnvironmentVariable("LAYERS_VERBOSE") == "1") + Console.WriteLine($"n{k,-4} {nodes[k].Operator,-14} skipped: t{t} phys{p} off={s.Off[p]} recs={s.Recs.Length}"); + continue; + } + float[] v = gg.Run(shape, inp, k + 1, t).ToArray(); + double sum = 0, sumAbs = 0; float mn = float.MaxValue, mx = float.MinValue; int nz = 0, nan = 0; + foreach (float x in v) + { + if (float.IsNaN(x) || float.IsInfinity(x)) { nan++; continue; } + mn = MathF.Min(mn, x); mx = MathF.Max(mx, x); sum += x; sumAbs += Math.Abs(x); + if (x != 0) nz++; + } + string last2 = string.Join(" | ", s.Recs.Take(s.Recs.Length - 1).TakeLast(2).Select(r => r.Tag)); + Console.WriteLine($"n{k,-4} {nodes[k].Operator,-14} numel={v.Length,-9} mean={sum / v.Length,10:G5} meanAbs={sumAbs / v.Length,10:G5} min={mn,10:G5} max={mx,10:G5} nz={nz}/{v.Length} bad={nan} recs={s.Recs.Length} [{last2}]"); + } + return 0; + } +} diff --git a/test/Sdcb.SimdPaddleOCR.AndroidBench/MainActivity.cs b/test/Sdcb.SimdPaddleOCR.AndroidBench/MainActivity.cs new file mode 100644 index 0000000..0852a78 --- /dev/null +++ b/test/Sdcb.SimdPaddleOCR.AndroidBench/MainActivity.cs @@ -0,0 +1,118 @@ +using Android.App; +using Android.OS; +using Android.Views; + +namespace Sdcb.SimdPaddleOCR.AndroidBench; + +/// +/// adb entry point. Extras: args (space-separated bench arguments), +/// env (K=V;K=V, applied before the library is touched so its +/// static env switches see them) and run (output id). Everything the +/// bench prints goes to logcat (tag SimdOcrBench) and to +/// files/out/<run>.log; <run>.done holds the exit code. +/// The process kills itself afterwards so every run starts cold. +/// +[Activity(Label = "SimdOcrBench", MainLauncher = true, Exported = true, + Name = "com.sdcb.simdocr.bench.MainActivity")] +public class MainActivity : Activity +{ + protected override void OnCreate(Bundle? savedInstanceState) + { + base.OnCreate(savedInstanceState); + Window?.AddFlags(WindowManagerFlags.KeepScreenOn); + string args = Intent?.GetStringExtra("args") ?? "--caps"; + string env = Intent?.GetStringExtra("env") ?? ""; + string run = Intent?.GetStringExtra("run") ?? "run"; + string root = GetExternalFilesDir(null)!.AbsolutePath; + string outDir = Path.Combine(root, "out"); + // app-created, so the app can read what adb pushes into them later + // (directories made by the shell user are not readable by the app) + foreach (string d in new[] { outDir, Path.Combine(root, "models"), Path.Combine(root, "dataset") }) + Directory.CreateDirectory(d); + File.Delete(Path.Combine(outDir, run + ".done")); + + foreach (string kv in env.Split(';', StringSplitOptions.RemoveEmptyEntries)) + { + int eq = kv.IndexOf('='); + if (eq > 0) System.Environment.SetEnvironmentVariable(kv[..eq], kv[(eq + 1)..]); + } + + var worker = new Thread(() => + { + int rc; + using (var log = new TeeWriter(Path.Combine(outDir, run + ".log"))) + { + Console.SetOut(log); + Console.SetError(log); + Console.WriteLine($"args: {args}"); + if (env.Length > 0) Console.WriteLine($"env: {env}"); + try + { + Directory.SetCurrentDirectory(root); + rc = Bench.Run(args.Split(' ', StringSplitOptions.RemoveEmptyEntries), root, outDir); + } + catch (Exception ex) + { + Console.WriteLine($"FATAL {ex}"); + rc = 99; + } + Console.WriteLine($"exit {rc}"); + } + File.WriteAllText(Path.Combine(outDir, run + ".done"), rc.ToString()); + Process.KillProcess(Process.MyPid()); + }, 64 << 20); + worker.Start(); + } +} + +/// Line-buffered tee to logcat and a file. +sealed class TeeWriter(string path) : TextWriter +{ + private readonly StreamWriter _file = new(path, append: false) { AutoFlush = true }; + private readonly System.Text.StringBuilder _line = new(); + private readonly object _sync = new(); + public override System.Text.Encoding Encoding => System.Text.Encoding.UTF8; + + public override void Write(char value) + { + lock (_sync) + { + if (value == '\n') { FlushLine(); return; } + if (value != '\r') _line.Append(value); + } + } + + public override void Write(string? value) + { + if (value is null) return; + lock (_sync) + foreach (char c in value) + { + if (c == '\n') FlushLine(); + else if (c != '\r') _line.Append(c); + } + } + + public override void WriteLine(string? value) + { + lock (_sync) { Write(value); FlushLine(); } + } + + private void FlushLine() + { + string s = _line.ToString(); + _line.Clear(); + Android.Util.Log.Info("SimdOcrBench", s); + _file.WriteLine(s); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + lock (_sync) if (_line.Length > 0) FlushLine(); + _file.Dispose(); + } + base.Dispose(disposing); + } +} diff --git a/test/Sdcb.SimdPaddleOCR.AndroidBench/Paths.cs b/test/Sdcb.SimdPaddleOCR.AndroidBench/Paths.cs new file mode 100644 index 0000000..0a11466 --- /dev/null +++ b/test/Sdcb.SimdPaddleOCR.AndroidBench/Paths.cs @@ -0,0 +1,42 @@ +namespace Sdcb.SimdPaddleOCR.AndroidBench; + +/// Model / dataset locations and image decoding shared by the phone +/// host and the desktop GpuBench (which links this file). +static class Paths +{ + internal static string Root = ""; + internal static string ModelsDir = "models"; + internal static string DatasetDir = "dataset"; + + internal static string Path_(string p) => Path.IsPathRooted(p) ? p : Path.Combine(Root, p); + internal static string ModelPath(string model, string kind) => + kind == "cls" ? Path.Combine(Path_(ModelsDir), "cls.onnx") : Path.Combine(Path_(ModelsDir), $"{model}-{kind}.onnx"); + internal static string DictPath(string model) => Path.Combine(Path_(ModelsDir), $"{model}-dict.txt"); + internal static string Dataset => Path_(DatasetDir); + + internal static OcrBackend ParseBackend(string s) => s.ToLowerInvariant() switch + { + "cpu" or "sharp" => OcrBackend.Cpu, + "vulkan" => OcrBackend.Vulkan, + "auto" => OcrBackend.Auto, + _ => throw new ArgumentException("backend must be cpu, vulkan or auto"), + }; + + /// Decodes like the desktop Tests harness (ImageSharp, BGR24). + internal static (byte[] Bgr, int W, int H) LoadBgr(string path) + { + using var image = SixLabors.ImageSharp.Image.Load(path); + byte[] px = new byte[checked(image.Width * image.Height * 3)]; + for (int y = 0; y < image.Height; y++) + for (int x = 0; x < image.Width; x++) + { + var p = image[x, y]; + int o = (y * image.Width + x) * 3; + px[o] = p.B; px[o + 1] = p.G; px[o + 2] = p.R; + } + return (px, image.Width, image.Height); + } + + internal static string[] DatasetFiles() => Directory.GetFiles(Dataset, "img-*.jpg") + .OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToArray(); +} diff --git a/test/Sdcb.SimdPaddleOCR.AndroidBench/Prof.cs b/test/Sdcb.SimdPaddleOCR.AndroidBench/Prof.cs new file mode 100644 index 0000000..eabc087 --- /dev/null +++ b/test/Sdcb.SimdPaddleOCR.AndroidBench/Prof.cs @@ -0,0 +1,80 @@ +using System.Diagnostics; +using Sdcb.SimdPaddleOCR.Backends.Vulkan; +using Sdcb.SimdPaddleOCR.OnnxSharp; + +namespace Sdcb.SimdPaddleOCR.AndroidBench; + +/// Pure-GPU profiles (GpuBench --detprof / --recprof) and the +/// concurrent-caller check (GpuBench --conc), on the phone. +static class Prof +{ + internal static int Run(string[] args) + { + bool det = args[0] == "--detprof"; + string model = args[1]; + int a = int.Parse(args[2]), b = int.Parse(args[3]); + int reps = args.Length >= 5 ? int.Parse(args[4]) : 20; + var mdl = Model.Load(File.ReadAllBytes(Paths.ModelPath(model, det ? "det" : "rec"))); + var compiled = new CompiledModel(mdl, intraOpThreads: 8); + int[] shape = det ? [1, 3, a, b] : [a, 3, 48, b]; + var inp = new float[shape[0] * shape[1] * shape[2] * shape[3]]; + var rr = new Random(1); + for (int i = 0; i < inp.Length; i++) inp[i] = (float)(rr.NextDouble() * 2 - 1); + int nodeLimit = int.MaxValue, outTensor = -1; + if (!det) + { + var cpu = new InferenceSession(compiled); + cpu.Reshape(shape); + cpu.TryRunUntilCtcProjection(inp, out CtcProjectionOperands ops); + nodeLimit = ops.MatMulNodeIndex; + outTensor = checked((int)mdl.Nodes[nodeLimit].Inputs[0]); + Console.WriteLine($"rec: batch={a} T={ops.Rows} C={ops.Inner} cols={ops.Columns}"); + } + using var dev = VkDevice.Create(); + using var gg = new GpuDetGraph(dev, compiled); + double best = double.MaxValue; + var walls = new List(); + for (int r = 0; r < reps; r++) + { + long t0 = Stopwatch.GetTimestamp(); + gg.Run(shape, inp, nodeLimit, outTensor); + double ms = Stopwatch.GetElapsedTime(t0).TotalMilliseconds; + walls.Add(ms); + best = Math.Min(best, ms); + } + walls.Sort(); + Console.WriteLine($"best wall {best:F2} ms (median {walls[walls.Count / 2]:F2}), dispatches={gg.DispatchCount(shape, nodeLimit, outTensor)} thermal={E2e.Thermal()}"); + gg.DumpProfile(shape, nodeLimit, outTensor); + return 0; + } + + // --conc [threads] [backend]: serial reference texts vs concurrent callers + internal static int Conc(string[] args) + { + string model = args[1]; + int threads = args.Length > 2 ? int.Parse(args[2]) : 4; + OcrBackend backend = args.Length > 3 ? Paths.ParseBackend(args[3]) : OcrBackend.Vulkan; + using var ocr = PaddleOcrAll.Load(Paths.ModelPath(model, "det"), Paths.ModelPath(model, "cls"), + Paths.ModelPath(model, "rec"), Paths.DictPath(model), new PaddleOcrOptions + { + Detector = new PaddleOcrDetectorOptions { Backend = backend }, + Recognizer = new PaddleOcrRecognizerOptions { Backend = backend }, + Classifier = new PaddleOcrClassifierOptions { Backend = backend }, + }); + var imgs = Paths.DatasetFiles().Take(24).Select(Paths.LoadBgr).ToArray(); + string Texts(int i) => string.Join("|", ocr.Run(imgs[i].Bgr, imgs[i].W, imgs[i].H).Lines.Select(l => l.Text)); + string[] reference = Enumerable.Range(0, imgs.Length).Select(Texts).ToArray(); + int bad = 0; + double wsPeak = E2e.WorkingSetMb(); + var tc = Stopwatch.StartNew(); + Parallel.For(0, imgs.Length * 3, new ParallelOptions { MaxDegreeOfParallelism = threads }, j => + { + int i = j % imgs.Length; + if (Texts(i) != reference[i]) { Interlocked.Increment(ref bad); Console.WriteLine($"MISMATCH img{i}"); } + double ws = E2e.WorkingSetMb(); + lock (reference) wsPeak = Math.Max(wsPeak, ws); + }); + Console.WriteLine($"conc model={model} backend={backend} usesGpu={GpuBackend.UsesGpu(backend)} threads={threads} runs={imgs.Length * 3} mismatches={bad} wall={tc.ElapsedMilliseconds}ms ws-peak={wsPeak:F0}MB"); + return bad == 0 ? 0 : 5; + } +} diff --git a/test/Sdcb.SimdPaddleOCR.AndroidBench/Sdcb.SimdPaddleOCR.AndroidBench.csproj b/test/Sdcb.SimdPaddleOCR.AndroidBench/Sdcb.SimdPaddleOCR.AndroidBench.csproj new file mode 100644 index 0000000..235fdec --- /dev/null +++ b/test/Sdcb.SimdPaddleOCR.AndroidBench/Sdcb.SimdPaddleOCR.AndroidBench.csproj @@ -0,0 +1,42 @@ + + + + net10.0-android + 29 + Exe + enable + enable + preview + true + false + Sdcb.SimdPaddleOCR.AndroidBench + Sdcb.SimdPaddleOCR.AndroidBench + com.sdcb.simdocr.bench + SimdOcrBench + 1 + 1.0 + android-arm64 + apk + true + false + true + + embedded + + false + + + + + + + + + + + + diff --git a/test/Sdcb.SimdPaddleOCR.AndroidBench/Shaders/peak.comp b/test/Sdcb.SimdPaddleOCR.AndroidBench/Shaders/peak.comp new file mode 100644 index 0000000..83d299e --- /dev/null +++ b/test/Sdcb.SimdPaddleOCR.AndroidBench/Shaders/peak.comp @@ -0,0 +1,38 @@ +#version 450 +#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require +#extension GL_EXT_control_flow_attributes : require + +// FMA peak probe: 8 independent vec4 chains per thread, fully unrolled +// inner body, 512 iterations: 2 * 4 * 8 * 512 = 32768 flop per thread. +// -DF16 runs the same on f16vec4. +// Rebuild: glslc -O --target-env=vulkan1.1 [-DF16] peak.comp -o peak[16].spv + +layout(local_size_x = 256) in; +layout(std430, binding = 0) buffer O { vec4 o[]; }; +layout(push_constant) uniform PC { float s; } p; + +#ifdef F16 +#define T f16vec4 +#define S(x) float16_t(x) +#else +#define T vec4 +#define S(x) (x) +#endif + +void main() +{ + T m = T(S(p.s)), c = T(S(1e-4)); + float b = float(gl_LocalInvocationID.x) * 1e-3; + T a0 = T(S(b)), a1 = T(S(b + 0.1)), a2 = T(S(b + 0.2)), a3 = T(S(b + 0.3)); + T a4 = T(S(b + 0.4)), a5 = T(S(b + 0.5)), a6 = T(S(b + 0.6)), a7 = T(S(b + 0.7)); + for (int it = 0; it < 64; it++) + { + [[unroll]] for (int u = 0; u < 8; u++) + { + a0 = fma(a0, m, c); a1 = fma(a1, m, c); a2 = fma(a2, m, c); a3 = fma(a3, m, c); + a4 = fma(a4, m, c); a5 = fma(a5, m, c); a6 = fma(a6, m, c); a7 = fma(a7, m, c); + } + } + T r = a0 + a1 + a2 + a3 + a4 + a5 + a6 + a7; + if (float(r.x) == 12345.0) o[gl_GlobalInvocationID.x] = vec4(r); +} diff --git a/test/Sdcb.SimdPaddleOCR.AndroidBench/Shaders/peak.spv b/test/Sdcb.SimdPaddleOCR.AndroidBench/Shaders/peak.spv new file mode 100644 index 0000000..af1f022 Binary files /dev/null and b/test/Sdcb.SimdPaddleOCR.AndroidBench/Shaders/peak.spv differ diff --git a/test/Sdcb.SimdPaddleOCR.AndroidBench/Shaders/peak16.spv b/test/Sdcb.SimdPaddleOCR.AndroidBench/Shaders/peak16.spv new file mode 100644 index 0000000..2d86a12 Binary files /dev/null and b/test/Sdcb.SimdPaddleOCR.AndroidBench/Shaders/peak16.spv differ diff --git a/test/Sdcb.SimdPaddleOCR.AndroidBench/ab.ps1 b/test/Sdcb.SimdPaddleOCR.AndroidBench/ab.ps1 new file mode 100644 index 0000000..9ca434f --- /dev/null +++ b/test/Sdcb.SimdPaddleOCR.AndroidBench/ab.ps1 @@ -0,0 +1,26 @@ +# End-to-end A/B on the phone: for each model, backends alternate round by +# round (cpu, vulkan, cpu, vulkan, ...) with a cool-down between runs. Prints +# median ms (all / last 50 images), thermal status and accuracy per run. +# .\ab.ps1 -tag final -models tiny,small,medium -rounds 3 +param([string]$tag = "ab", [string[]]$models = @("tiny", "small", "medium"), + [string[]]$backends = @("cpu", "vulkan"), [int]$rounds = 3, [int]$cool = 60, [string]$Env = "") +$here = $PSScriptRoot +$out = (Resolve-Path "$here\..\..").Path + "\bench-out\android" +foreach ($m in $models) { + for ($r = 1; $r -le $rounds; $r++) { + foreach ($b in $backends) { + $id = "$tag-$m-$b-r$r" + Start-Sleep -Seconds $cool + & "$here\run.ps1" -Bench "--e2e --model $m --backend $b --out $id.json" -Env $Env -Run $id -TimeoutSec 3600 | Out-Null + $log = "$out\$id.log" + $st = (Select-String -Path $log -Pattern "steady: all-med ([\d.]+) last50-med ([\d.]+) ws-peak (\d+)MB thermal=(-?\d+)").Matches + $ac = (Select-String -Path $log -Pattern "exact_lines=(\d+)/\d+ .*CER=([\d.]+)%").Matches + $gpu = (Select-String -Path $log -Pattern "usesGpu=(\w+)").Matches + if ($st) { + "{0,-7} {1,-7} r{2} med {3,7} last50 {4,7} ws {5,5}MB thermal {6} lines {7} CER {8}% gpu={9}" -f $m, $b, $r, + $st[0].Groups[1].Value, $st[0].Groups[2].Value, $st[0].Groups[3].Value, $st[0].Groups[4].Value, + $ac[0].Groups[1].Value, $ac[0].Groups[2].Value, $gpu[0].Groups[1].Value + } else { "{0,-7} {1,-7} r{2} FAILED (see $log)" -f $m, $b, $r } + } + } +} diff --git a/test/Sdcb.SimdPaddleOCR.AndroidBench/global.json b/test/Sdcb.SimdPaddleOCR.AndroidBench/global.json new file mode 100644 index 0000000..545c92c --- /dev/null +++ b/test/Sdcb.SimdPaddleOCR.AndroidBench/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.302", + "rollForward": "latestFeature" + } +} diff --git a/test/Sdcb.SimdPaddleOCR.AndroidBench/gprof.ps1 b/test/Sdcb.SimdPaddleOCR.AndroidBench/gprof.ps1 new file mode 100644 index 0000000..8afdbed --- /dev/null +++ b/test/Sdcb.SimdPaddleOCR.AndroidBench/gprof.ps1 @@ -0,0 +1,21 @@ +# Pure-GPU profile on the phone over the GpuBench gprof.ps1 case list +# (per-dispatch timestamps, min over reps). Logs land in bench-out/android/gp--*.log. +param([string]$tag = "x", [string[]]$cases = @( + "det medium 960 960", "det medium 640 960", "rec medium 8 480", "rec medium 1 320", + "det small 960 960", "rec small 8 480", "det tiny 960 960", "rec tiny 8 480"), + [int]$reps = 10, [switch]$all, [string]$Env = "") +$here = $PSScriptRoot +$out = (Resolve-Path "$here\..\..").Path + "\bench-out\android" +$envs = "SIMD_OCR_GPU_PROF=1" + $(if ($all) { ";SIMD_OCR_GPU_PROF_ALL=1" }) + $(if ($Env) { ";$Env" }) +foreach ($c in $cases) { + $k, $m, $a, $b = $c -split " " + $mode = if ($k -eq "det") { "--detprof" } else { "--recprof" } + $id = "gp-$tag-$k-$m-$a-$b" + & "$here\run.ps1" -Bench "$mode $m $a $b $reps" -Env $envs -Run $id | Out-Null + $log = "$out\$id.log" + $t = (Select-String -Path $log -Pattern "GPU profile \((\d+) dispatches, ([\d.]+) ms").Matches + $w = (Select-String -Path $log -Pattern "best wall ([\d.]+)").Matches + $th = (Select-String -Path $log -Pattern "thermal=(\d+)").Matches + "{0,-22} gpu={1,8} ms disp={2,4} wall={3} thermal={4}" -f $c, $t[0].Groups[2].Value, $t[0].Groups[1].Value, + $(if ($w) { $w[0].Groups[1].Value } else { "?" }), $(if ($th) { $th[0].Groups[1].Value } else { "?" }) +} diff --git a/test/Sdcb.SimdPaddleOCR.AndroidBench/run.ps1 b/test/Sdcb.SimdPaddleOCR.AndroidBench/run.ps1 new file mode 100644 index 0000000..68df8e1 --- /dev/null +++ b/test/Sdcb.SimdPaddleOCR.AndroidBench/run.ps1 @@ -0,0 +1,104 @@ +# One command for the phone: [build] -> [adb install] -> [push models/dataset] -> launch -> +# wait for .done -> pull files/out/* into bench-out/android/. On a crash or a +# process that dies without writing .done, logcat is saved next to the log. +# +# .\run.ps1 -Build -Install -Push -Bench "--caps" +# .\run.ps1 -Bench "--e2e --model tiny --backend vulkan --out e2e-tiny-vk.json" -Env "SIMD_OCR_GPU_TIME=1" +param( + [string]$Bench = "--caps", + [string]$Env = "", + [string]$Run = "", + [switch]$Build, + [switch]$Install, + [switch]$Push, + [int]$TimeoutSec = 3600, + [string]$Adb = $(if ($env:ADB) { $env:ADB } else { "D:\_\3rd\android-sdk\platform-tools\adb.exe" }), + [string]$AndroidSdk = $(if ($env:ANDROID_HOME) { $env:ANDROID_HOME } else { "D:\_\3rd\android-sdk" }), + [string]$Jdk = $(if ($env:JAVA_HOME -and (Test-Path "$env:JAVA_HOME\bin\javac.exe") -and ($env:JAVA_HOME -notmatch "jdk-2[2-9]")) { $env:JAVA_HOME } else { "C:\Program Files\Android\openjdk\jdk-21.0.8" }) +) +$ErrorActionPreference = "Stop" +$here = $PSScriptRoot +$root = (Resolve-Path "$here\..\..").Path +$pkg = "com.sdcb.simdocr.bench" +$remote = "/sdcard/Android/data/$pkg/files" +$local = "$root\bench-out\android" +New-Item -ItemType Directory -Force $local | Out-Null +if (-not $Run) { $Run = "r" + (Get-Date -Format "MMdd-HHmmss") } + +if ($Build) { + Push-Location $here + try { + dotnet build -c Release "-p:AndroidSdkDirectory=$AndroidSdk" "-p:JavaSdkDirectory=$Jdk" -v:q -nologo -clp:ErrorsOnly | Out-Host + if ($LASTEXITCODE -ne 0) { throw "build failed" } + } finally { Pop-Location } +} +if ($Install) { + $apk = "$here\bin\Release\net10.0-android\android-arm64\$pkg-Signed.apk" + & $Adb install -r -g $apk | Out-Host + if ($LASTEXITCODE -ne 0) { throw "adb install failed" } +} + +function Start-Bench([string]$id, [string]$argv, [string]$envs) { + # a previous run may still be exiting: an intent delivered to it would be + # swallowed by the dying activity instead of starting a fresh process + & $Adb shell "am force-stop $pkg" + for ($i = 0; $i -lt 20 -and ((& $Adb shell "pidof $pkg") -join ""); $i++) { Start-Sleep -Milliseconds 500 } + & $Adb shell "rm -f $remote/out/$id.done $remote/out/$id.log" + $cmd = "am start -S -n $pkg/$pkg.MainActivity --es run '$id' --es args '$argv'" + if ($envs) { $cmd += " --es env '$envs'" } + & $Adb shell $cmd | Out-Null +} + +if ($Push) { + # the app must create models/ dataset/ out/ itself: directories made by the + # shell user are not readable by the app + & $Adb shell "rm -rf $remote/models $remote/dataset $remote/out" + Start-Bench "init" "--init" "" + for ($i = 0; $i -lt 30 -and -not ((& $Adb shell "cat $remote/out/init.done 2>/dev/null") -join ""); $i++) { Start-Sleep 1 } + & $Adb push "$root\bench-out\models\." "$remote/models/" | Out-Host + & $Adb push "$root\dataset\." "$remote/dataset/" | Out-Host +} + +& $Adb logcat -c +Start-Bench $Run $Bench $Env +Write-Output "[$Run] $Bench $(if ($Env) { "(env $Env)" })" + +$t0 = Get-Date +$seen = 0 +$started = $false +$result = $null +while (((Get-Date) - $t0).TotalSeconds -lt $TimeoutSec) { + Start-Sleep -Seconds 2 + $done = (& $Adb shell "cat $remote/out/$Run.done 2>/dev/null") -join "" + $log = & $Adb shell "cat $remote/out/$Run.log 2>/dev/null" + if ($log) { + $lines = @($log) + for ($i = $seen; $i -lt $lines.Count; $i++) { Write-Output $lines[$i] } + $seen = $lines.Count + } + if ($done -match "^\d+$") { $result = [int]$done; break } + $procId = (& $Adb shell "pidof $pkg") -join "" + if ($procId) { $started = $true } + elseif ($started -or ((Get-Date) - $t0).TotalSeconds -gt 30) { + # the app writes .done and then kills itself: look once more + Start-Sleep -Seconds 1 + $done = (& $Adb shell "cat $remote/out/$Run.done 2>/dev/null") -join "" + $lines = @(& $Adb shell "cat $remote/out/$Run.log 2>/dev/null") + for ($i = $seen; $i -lt $lines.Count; $i++) { Write-Output $lines[$i] } + if ($done -match "^\d+$") { $result = [int]$done } + break + } +} + +& $Adb pull "$remote/out/." "$local" 2>&1 | Out-Null +if ($null -eq $result) { + $lc = "$local\$Run-logcat.txt" + & $Adb logcat -d -v time "SimdOcrBench:*" "DOTNET:*" "AndroidRuntime:*" "DEBUG:*" "libc:*" "ActivityManager:*" "lowmemorykiller:*" "*:F" "*:S" | Out-File -Encoding utf8 $lc + Write-Output "[$Run] process ended without .done — logcat saved to $lc" + exit 98 +} +if ($result -ne 0) { + & $Adb logcat -d -v time "SimdOcrBench:*" "DOTNET:*" "AndroidRuntime:*" "*:F" "*:S" | Out-File -Encoding utf8 "$local\$Run-logcat.txt" +} +Write-Output "[$Run] exit $result -> $local\$Run.log" +exit $result diff --git a/test/Sdcb.SimdPaddleOCR.GpuBench/Harness.cs b/test/Sdcb.SimdPaddleOCR.GpuBench/Harness.cs index 3b92b4d..081685f 100644 --- a/test/Sdcb.SimdPaddleOCR.GpuBench/Harness.cs +++ b/test/Sdcb.SimdPaddleOCR.GpuBench/Harness.cs @@ -1,5 +1,18 @@ namespace Sdcb.SimdPaddleOCR.GpuBench; +static class DesktopPaths +{ + internal static int Run(Func mode, string[] args) + { + string root = AppContext.BaseDirectory; + while (root.Length > 3 && !File.Exists(Path.Combine(root, "Sdcb.SimdPaddleOCR.slnx"))) + root = Path.GetDirectoryName(root.TrimEnd(Path.DirectorySeparatorChar))!; + AndroidBench.Paths.Root = root; + AndroidBench.Paths.ModelsDir = Path.Combine("bench-out", "models"); + return mode(args); + } +} + static class Harness { internal static int Usage(int code) @@ -26,6 +39,10 @@ internal static int Usage(int code) --sessiso --reciso --detmap + --detcmp [maxSide] CPU vs Vulkan DET map + boxes + --reccmp CPU vs Vulkan REC activations + --gemm [flags] [tileM] [tileN] [reqSg] [reps] GEMM vs fp64 reference + --layers [first] [last] per-node output stats of the truncated GPU graph --recreal [W] """); return code; diff --git a/test/Sdcb.SimdPaddleOCR.GpuBench/Program.cs b/test/Sdcb.SimdPaddleOCR.GpuBench/Program.cs index 9b9a2c2..4c36010 100644 --- a/test/Sdcb.SimdPaddleOCR.GpuBench/Program.cs +++ b/test/Sdcb.SimdPaddleOCR.GpuBench/Program.cs @@ -13,5 +13,11 @@ "--rec" => RecCompare.Run(args), "--recprof" or "--sessiso" or "--reciso" or "--recreal" => RecBench.Run(args), "--conc" or "--pipe" => PipelineBench.Run(args), + "--gemm" => Sdcb.SimdPaddleOCR.AndroidBench.GemmTest.Run(args), + // same checks as the phone host; models from bench-out/models, images from dataset/ + "--detcmp" or "--reccmp" => DesktopPaths.Run(Sdcb.SimdPaddleOCR.AndroidBench.Compare.Run, args), + "--layers" when args.Length >= 5 => Sdcb.SimdPaddleOCR.AndroidBench.LayerProbe.Run(args[1], int.Parse(args[2]), + int.Parse(args[3]), int.Parse(args[4]), args.Length > 5 ? int.Parse(args[5]) : 0, + args.Length > 6 ? int.Parse(args[6]) : int.MaxValue), _ => Harness.Usage(2), }; diff --git a/test/Sdcb.SimdPaddleOCR.GpuBench/Sdcb.SimdPaddleOCR.GpuBench.csproj b/test/Sdcb.SimdPaddleOCR.GpuBench/Sdcb.SimdPaddleOCR.GpuBench.csproj index 4481ab0..01aaccf 100644 --- a/test/Sdcb.SimdPaddleOCR.GpuBench/Sdcb.SimdPaddleOCR.GpuBench.csproj +++ b/test/Sdcb.SimdPaddleOCR.GpuBench/Sdcb.SimdPaddleOCR.GpuBench.csproj @@ -18,5 +18,9 @@ + + + +