# Pure C++ PP-OCRv6 inference This is a focused C++ port of the PP-OCRv6 detector/recognizer path from `D:\workprj\aicoder\corelib\ocr`. It has no dependency on ONNX Runtime, Paddle Inference, OpenCV, or Python. The library is also a C and C++ API (`include/ppocr/ppocr.hpp`, `include/ppocr/ppocr.h`) with static and shared builds, CMake package exports, and a Python package whose `RapidOCR` class follows the interface on `main`. Usage, the stable C ABI, and the CPU dispatch rules are in [`docs/API.md`](docs/API.md). Measured AVX2 / AVX-512 / NEON notes are in [`docs/PERFORMANCE.md`](docs/PERFORMANCE.md). The embedded executor is intentionally not a general ONNX Runtime. It only implements the operator set exercised by the official PP-OCRv6 tiny/small/ medium ONNX model bundle: convolutions (including transpose), pooling, elementwise math, normalisation, resize, tensor-shape operators, attention matmul, and softmax. > **Current GPU-only qualification (2026-08-21).** The strict Vulkan > GPU-only OCR path runs RGB preprocessing, the complete neural graph, DB > postprocessing, and CTC Top-1 on the GPU; it has no CPU neural-operator > fallback. On the local AMD Radeon 780M / LLPC stack, exact CPU/GPU decoded > text and geometry are currently verified for PP-OCRv6 **tiny** and **small**. > GPU recognizer crops > now retain their exact PP-OCR aspect-ratio width rather than adding a > 64-column scheduling suffix, fixing short-crop CTC parity. The **medium** > detector at production `1x3x160x704` remains unqualified and fails closed; > no result is substituted from CPU. Nearby `1x3x160x640` and `1x3x160x768` > shapes remain CPU/GPU parity-qualified. Historical GPU claims below do not > widen this current qualification. ### 2026-08-22 strict GPU-only public-boundary recheck The public `gpu_only` mode remains a strict Vulkan-only execution path: packed RGB upload and resize, detector and recognizer graphs, DB box postprocessing, and CTC Top-1 execute on the device. Failed Vulkan execution does not retry neural inference on CPU. This recheck also carries the Vulkan queue/fence status through every public GPU-only OCR stage (RGB front end, detector, DB postprocess, recognizer and CTC). A device reset is therefore reported as `VK_ERROR_DEVICE_LOST`, not mislabelled as an unsupported ONNX operator. The only retry allowed by the GPU-only page batch scheduler is an N=1 Vulkan retry after a rejected N-batch; it never invokes the CPU executor. On the local Radeon 780M / LLPC adapter, production medium-detector `1x3x160x704` still fails closed with that result. Changing command-segment size, forcing queue-idle, using a fresh command buffer, using the isolated terminal-depthwise shader, or using its generic/sliced variant did not make that shape stable. This is a driver compatibility limit, not a CPU-fallback opportunity. ### 2026-08-22 GPU-only deployment coverage `gpu_only` is a complete execution mode, not a request to accelerate only selected layers: packed RGB upload/resize, detector, DB postprocess, recognizer, CTC Top-1, and the real NCHW page/crop batch paths stay on one Vulkan queue. Only final compact OCR records and UTF-8 construction reach the host. A rejected N-batch is retried as strict Vulkan N=1; it is never redirected to the CPU graph. The Vulkan loader is dynamically opened at runtime, so applications do not link to Vulkan or ONNX Runtime. The build now embeds shaders with the vendored Windows compiler when available, otherwise with a host `glslangValidator`/`glslang`; the same executor is therefore buildable on Windows x86/x64, Linux x86/x64, and Linux ARM/AArch64 when Vulkan headers and a shader compiler are supplied. If the loader, compute queue, shader build, or a required graph node is unavailable, constructor/runtime validation fails closed rather than selecting CPU inference. After this portability change, the local Radeon 780M validation was rerun with the rebuilt `build-gpu-only-ep` binaries: | Check | Result | | --- | --- | | tiny GPU-only OCR, `en` + `mixed` + low contrast, 2 repeats | passed; 8 records; max confidence delta `2.50e-5` | | small GPU-only OCR, `en` + `mixed` | passed; 5 records; max confidence delta `1.0e-6` | | tiny two-page GPU batch, `en` x2, 1 warm-up + 5 runs | serial `93.109 ms`, batch `84.491 ms`, `1.102x`; text/geometry equivalent | | tiny GPU memory ladder, 16px through 1920px pages then back to small | all requests completed; post-large small/en/mixed returned to the warmed `596,709,376 B` private-byte baseline | The last memory sweep later rose by at most `9,392,128 B` after a noisy page and finished `7,172,096 B` above that process allocator baseline. This is a bounded host allocator effect observed after the request; it is not reported as GPU arena growth and does not relax the device-only inference contract. ### 2026-08-22 expanded GPU-only corpus recheck The correct PP-OCRv6 common dictionary (`dict_ppocrv6.txt`, vocabulary `18710`) was used for small/medium rather than the tiny dictionary. The latest strict GPU-only corpus covers low contrast, very wide/tall source images, and a 1920×1080 screenshot in addition to short English/mixed text: | Model | Images | Result | | --- | --- | --- | | tiny | `en`, `mixed`, low contrast, `3000×80`, `80×3000`, screenshot; 2 repeats | passed; 63 records; exact decoded text/geometry; max confidence delta `1.1662e-3` | | small | same six images | passed; 63 records; exact decoded text/geometry; max confidence delta `2.265e-4` | | medium | complete OCR at production detector shape | unavailable on this Radeon 780M/LLPC driver: strict `VK_ERROR_DEVICE_LOST`; no CPU substitution | The independent neural-graph checks remain passed for tiny/small `1×3×160×704` and medium `1×3×160×640`, with maximum CPU/GPU errors `5.63276e-7`, `3.05939e-7`, and `7.56991e-8` respectively. Small-model memory through the same small-to-large ladder returned to its warmed baseline after the final screenshot (`1,020,276,736 B` private bytes); a low-contrast request briefly added `14,577,664 B` in the process allocator and the final screenshot was below baseline. `ui_dense_1920x1080` is deliberately excluded from the qualified OCR corpus: it currently has one CPU/GPU decoded-result mismatch. That failure is retained as a regression target rather than being masked by a fallback or presented as a pass. The rebuilt `build-gpu-only-ep` binaries passed the currently qualified coverage. The graph timing measurements below use two warmed runs on the same machine and include the current host/device graph boundary: | Check | Result | | --- | --- | | tiny detector graph, `1x3x160x704` | passed; max error `5.63276e-7`; CPU/GPU `10.590 / 45.256 ms` | | small detector graph, `1x3x160x704` | passed; max error `3.05939e-7`; CPU/GPU `18.676 / 66.362 ms` | | medium detector graph, `1x3x160x640` | passed; max error `7.56991e-8`; CPU/GPU `80.312 / 272.255 ms` | | GPU-only OCR tiny, `en` + `mixed` | passed; 5 records; max confidence delta `2.9e-6` | | GPU-only OCR small, `en` | passed; 2 records; max confidence delta `8e-7` | | true two-page GPU batch, tiny `en` x2 | serial-equivalent; `93.177 -> 90.993 ms` (`1.024x`) | | true two-page GPU batch, small `en` x2 | serial-equivalent; `239.759 -> 231.595 ms` (`1.035x`) | The medium detector memory ladder `64x128 -> 96x256 -> 128x512 -> 160x640 -> 64x128` also passed. At every phase the live allocation returned to the `61,946,548`-byte immutable constants baseline; reusable capacity reached `201,457,664` bytes and is intentionally retained for later requests. These results validate qualified GPU-only execution and recovery behavior, but do not claim that this UMA GPU is faster than the AVX-512 CPU or an ORT GPU provider. The public GPU-only memory ladder also covered, in increasing source-size order, `tiny_16x16`, `en`, `mixed`, `noisy_lowcontrast`, `wide_strip_3000x80`, `tall_strip_80x3000`, `screenshot_1920x1080`, and `ui_dense_1920x1080`. After the final large image, processing the next small image retained no extra private bytes above the warmed large-page baseline. A later noisy page can make the process allocator grow by about 12 MB for compact host result/control storage; that allocation stayed bounded through the remaining large pages. GPU replay command buffers and their pinned slots are now discarded whenever the model uses the bounded detector/recognizer hand-off policy, so old replay shapes cannot accumulate as device allocations. ### 2026-08-22 hybrid Vulkan admission correction Hybrid terminal CTC GEMM now follows the documented policy exactly: for the same model shape, CPU worker budget, Vulkan adapter, and Vulkan runtime generation, it is admitted when a complete upload/compute/download boundary is **no slower than CPU**. The older `GPU <= CPU / 2` rule could reject an equal-speed GPU path merely because multiple recognizer crops might execute concurrently on CPU. Queue serialization is instead controlled at the OCR scheduler boundary. The admission cache is now also tagged with the Vulkan runtime generation, so a device-loss rebuild always re-measures rather than reusing an answer from the old device. On this Radeon 780M host, tiny `en` CTC terminal shapes measured `1.361 /` `0.718 ms` and `1.846 / 1.281 ms` for GPU/CPU, so they correctly remain CPU on this adapter. This is expected: the implementation enables Vulkan only on actual no-slower results, and does not manufacture a GPU claim from a slower boundary. The hybrid batch regression (`tiny_16x16`, `en`, `mixed`, low contrast; 2-page scheduling) remained text/geometry equivalent and measured `67.358 -> 63.918 ms` (`1.054x`). ### 2026-08-21 GPU-only requalification #### Latest local recheck The strict Vulkan build was rebuilt after removing an unproven automatic generic-tail experiment. The standard terminal scalar depthwise selection is restored for qualified shapes. The following tests were rerun on AMD Radeon 780M / LLPC; all use the pure C++ implementation without ONNX Runtime: | Check | Result | | --- | --- | | tiny detector graph, `1x3x160x704` | passed, max error `5.63276e-7` | | medium detector graph, `1x3x160x640` | passed, max error `7.56991e-8` | | medium detector graph, `1x3x160x768` | passed, max error `3.25672e-8` | | medium recognizer graph, `1x3x48x320` | passed, max error `2.66433e-5` | | GPU-only OCR tiny, `en` + `mixed` | passed; 5 records; max confidence delta `2.9e-6` | | GPU-only OCR small, `en` | passed; 2 records; max confidence delta `8e-7` | | tiny graph memory ladder, `64x128 -> 96x256 -> 128x512 -> 160x704 -> 64x128` | passed; live device bytes always returned to `1,713,924` B | The production medium detector at `1x3x160x704` still returns `VK_ERROR_DEVICE_LOST` on this driver after the terminal `896x5x22` depthwise stage. With the normal 20-node segmentation the failure is observed on the segment ending at that stage; a diagnostic command-stream grouping can submit that tail and subsequently fails in the FPN head. This identifies a Radeon 780M/LLPC Vulkan command-stream stability issue rather than a usable CPU fallback point. It remains deliberately fail-closed: no CPU neural or postprocess substitute is used. This blocks a claim of complete all-model GPU-only qualification on the local adapter. The end-to-end GPU path was rebuilt from the conservative source and retested on Radeon 780M/LLPC. A GPU crop may use a dynamic width; it is no longer rounded to 64 columns, because padded input columns produce extra valid CNN/CTC time rows and can alter a short crop's text or confidence. | GPU-only OCR pair | Corpus | Result | | --- | --- | --- | | tiny | `tiny_16x16`, `en`, `noisy_lowcontrast`, `wide_strip_3000x80` | 7 results; exact text/geometry; max confidence error `2.50e-5` | | tiny replay | `en` repeated 5 times on one live OCR instance | 10 results; exact text/geometry; max confidence error `4e-7` | | small | `en`, `noisy_lowcontrast` | 5 results; exact text/geometry; max confidence error `1.3e-6` | The strict graph sweep (three runs except medium detector, two) also passed: tiny detector `9.367 / 28.870 ms` CPU/Vulkan, tiny recognizer `5.224 / 23.189 ms`, small detector `17.976 / 48.075 ms`, small recognizer `13.320 / 62.323 ms`, medium recognizer `30.569 / 123.212 ms`, and the qualified medium detector shape `1x3x128x704` `78.976 / 234.478 ms`. These are full graph latencies on this UMA GPU, not an assertion that it beats the AVX-512 CPU. Maximum graph-output errors were `5.63e-7`, `2.18e-5`, `3.06e-7`, `3.78e-4`, `2.66e-5`, and `6.71e-9` respectively. GPU arena recovery was also rerun after the correction. In every small-to- large-to-small phase, dynamic `activation_bytes=0` after completion: | Graph ladder | Immutable live baseline | Final reusable capacity | | --- | ---: | ---: | | tiny detector through `1x3x160x704` | 1,713,924 B | 47,513,600 B | | small detector through `1x3x160x704` | 9,813,844 B | 75,235,328 B | | medium recognizer through `2x3x48x320` | 76,456,140 B | 174,850,048 B | ### 2026-08-21 verification update The strict GPU graph replay policy was corrected for public host-input `Run()` calls. Replay remains enabled for device-input and in-graph RGB execution, while host-input calls now release their dynamic activation slots when the submission completes. This is an arena-lifetime change only: every neural operator remains a Vulkan dispatch and an unsupported or failed device operation still fails closed. The reproducible tiny-detector ladder `1x3x64x128 -> 1x3x96x256 -> 1x3x128x512 -> 1x3x160x704 -> 1x3x64x128` now passes `ppocr_gpu_graph_mem_soak`. Its immutable live baseline is **1,713,924 bytes**; after every sample, including the largest one, live bytes return exactly to that baseline. Reusable arena capacity grows to **47,513,600 bytes** at the largest input and intentionally stays available for the final small input, so capacity is not misreported as a live-memory leak. The same live-memory invariant also passed for the qualified medium-detector ladder ending at `1x3x160x640`: its **61,946,548-byte** immutable baseline was recovered after each sample, with a stable **216,072,192-byte** reusable high-water capacity. The medium detector was rechecked at the production `1x3x160x704` input. On this Radeon 780M/LLPC driver it still fails closed with `VK_ERROR_DEVICE_LOST` in the terminal `896x5x22` depthwise dispatch. The dedicated scalar shader is now selected automatically for that exact Vulkan operator (use `PPOCR_GPU_TAIL_GENERIC=1` only for diagnostics); it also fails closed on this production shape, as did device-local and host-cached arena variants. The nearby qualified `1x3x160x640` and `1x3x160x768` shapes pass CPU/Vulkan output comparison, which further confirms this is a driver/shape interaction rather than a CPU fallback. Hybrid RGB front ends are now admission-driven by default: detector RGB+stem and recognizer RGB+Conv.0 run only after their complete upload/dispatch/ readback boundary is both numerically validated and no slower than the native SIMD path. `PPOCR_DISABLE_VULKAN_RGB_STEM` and `PPOCR_DISABLE_VULKAN_REC_RGB_CONV` disable those probes for diagnostics; no enable-only switch is required. The default strict-graph command segment is now **20 nodes** (override with `PPOCR_GPU_ONLY_SEGMENT_NODES`). A fresh three-run Vulkan sweep selected that setting for the Radeon 780M: detector graph means were tiny `30.201 ms`, small `46.590 ms`, and qualified medium `239.388 ms`; recognizer means at `1x3x48x320` were tiny `23.169 ms`, small `60.031 ms`, and medium `123.182 ms`. CPU/Vulkan maximum output errors remained respectively `5.63e-7`, `3.06e-7`, `6.71e-9`, `2.18e-5`, `3.78e-4`, and `2.66e-5`. This is a segment-lifetime optimization, not an assertion that the local UMA GPU is faster than the AVX-512 CPU. Memory recovery was also exercised across detector resolution and recognizer width/batch diversity. Small detector `64x128 -> 96x256 -> 128x512 -> 160x704 -> 64x128` always returned to its **9,813,844-byte** constants baseline. The medium recognizer ladder `1x3x48x64 -> 1x3x48x160 -> 1x3x48x320 -> 2x3x48x320 -> 1x3x48x64` always returned to its **76,456,140-byte** constants baseline. Capacity is intentionally retained as reusable GPU storage; no dynamic activation remains live after a sample. The CPU batch executor now supports real same-shape detector NCHW batches through both fused `MaxPool -> Concat -> Conv` and `Concat -> Conv` nodes. Those nodes now advance each source by its own NCHW channel-plane stride, so unequal concat branches cannot cross-contaminate pages. The varied-size four-image detector-batch regression passes for tiny/small/medium: decoded text and geometry are exact, while FP32 confidence is checked against its documented bounded tolerance. `rec_batch_size` now defaults to `1` because this PP-OCRv6 transformer export can change CTC results for N>1 recognition crops; deployments may still opt in and qualify it independently. GPU-only retains its separate device-resident crop batching path. The full small-to-large-to-small memory ladder (19 images, including `5120x2880`, `7680x4320`, wide and tall aspect ratios) also passed for every CPU model. Per-resolution private-byte and working-set evidence is retained in `build-gpu-only-final/memory_ladder_{tiny,small,medium}_cpu_20260821.json`. In each run both counters fell below the post-8K high-water during the recovery tail; final-small private memory was within **-0.05% / +0.33% / +1.06%** of the warmed tiny/small/medium baselines respectively. The current five-image CPU comparison is recorded in `build-gpu-only-final/cpu_diverse_benchmark_20260821.json`. On this local AVX-512 host (16 threads, two warmups, eight runs per case), native C++ beat the installed ONNX Runtime 1.28.0 CPU provider on every case: | PP-OCRv6 model | weighted latency improvement | weakest case | | --- | ---: | ---: | | tiny | 73.54% | 63.33% | | small | 46.16% | 36.22% | | medium | 9.22% | 1.45% | This is a local CPU-only result. The installed ORT package lists `AzureExecutionProvider` and `CPUExecutionProvider`; its observed variance, especially on small inputs, means these figures are not a portable GPU or architecture-wide claim. ## CPU acceleration and architecture support The inference path remains dependency-free and portable across x86/x64 and ARM/AArch64. Hot contiguous elementwise paths (Add/Sub/Mul/Div, ReLU, HardSwish, and affine scale/shift), pointwise `1x1` convolutions, ordinary/depthwise `3x3`/`5x5` convolutions, spatial mean reduction, fused BatchNorm+GELU, and attention `MatMul` use fused vector kernels: - x86/x64: AVX-512F/FMA when supported by the running CPU and OS; otherwise AVX2/FMA; otherwise scalar code. Advanced ISA code is compiled into separate translation units and selected only after runtime CPUID/OS-state checks. - ARM/AArch64: NEON when the compiler/target exposes it, with a scalar fallback otherwise. - Larger GEMMs are divided over CPU cores; small GEMMs stay single-threaded to avoid thread-launch overhead. Direct convolutions use a persistent worker set (rather than creating threads per operator), defaulting to at most 16 workers; `PPOCR_BENCH_THREADS` is an explicit override for deployment tuning and A/B tests. - PPOCR_DISABLE_AVX512=1 is an optional per-process A/B switch for x86 deployments. It keeps all runtime safety checks and selects AVX2/FMA when a particular AVX-512 CPU or workload performs better at its non-AVX-512 clock. - `ppocr_kernel_smoke` verifies paired/odd GEMM rows, scalar/vector column tails, optional bias, and a recognizer-sized projection. Run it normally and with `PPOCR_DISABLE_AVX512=1` to cover the dynamic AVX-512 and AVX2 paths. - PPOCR_DISABLE_AVX512_POINTWISE8=1 is a narrower A/B switch for the AVX-512 eight-output `1x1` register tile. It retains AVX-512 and its four-output pointwise kernel, so deployments can isolate this tile's cache/register trade-off without changing the baseline ISA. A current three-model A/B confirmed that this decision is shape-sensitive: do not globally force the wider tile merely because AVX-512 is available. - PPOCR_ENABLE_AVX512_POINTWISE_RELU8=1 opt-in enables the analogous eight-output `1x1 Conv+ReLU` tile for deployment-local testing. The stable four-output fused-ReLU path remains the default because the wider tile's end-to-end result is workload/cache sensitive. - Large stride-one `3x3` feature maps can likewise use an AVX-512 eight-output channel tile. It shares each input vector across eight output reductions; the dispatcher restricts it to maps of at least 16,384 pixels and falls back to the four-output tile for smaller layers, odd channel tails, AVX2, NEON, and scalar targets. `PPOCR_DISABLE_AVX512_CONV3X3_TILE8=1` keeps the four-output AVX-512 path for deployment A/B. The low-level regression now checks both an eight-channel tile and a nine-channel ReLU tail, which also caught and fixed the prior un-clamped SIMD tail-channel bug. - AVX-512 also has a dedicated unit-stride `2x2 SAME_UPPER` convolution path for the detector reconstruction stem. Its large valid interior uses four explicit FMA loads per input channel; only the final row/column runs the ONNX zero-padding boundary formula. It is dynamically selected only on a supported AVX-512 CPU, preserves the AVX2/scalar fallback, and can be A/B disabled with `PPOCR_DISABLE_AVX512_CONV2X2_SAME=1`. - Equal-shape residual `1x1 Conv -> Add` pairs are fused at graph load and, on a runtime-eligible AVX-512, AVX2, or NEON CPU, keep the convolution accumulators live until the final residual add/store. This removes the intermediate Conv activation read/write. The executor validates the residual's runtime NCHW shape before selecting that kernel; scalar, broadcast, and unexpected-shape cases retain the existing exact Conv-then-Add path. - Post-folding `Conv -> Sigmoid` and `Conv -> HardSigmoid` gate pairs are also fused. Their convolution result is activated in place, removing the generic unary output allocation; HardSigmoid uses one exact SIMD pass for `clamp(alpha*x+beta, 0, 1)` on AVX-512, AVX2, and NEON. - Direct `Conv -> Add([1,C,1,1])` channel biases are folded into immutable convolution biases at graph load. If the Add is followed by ReLU, the three operations become one fused Conv+ReLU node. This covers the recognizer stem and avoids a full NCHW broadcast traversal and activation allocation. - Recognition `Conv -> Sigmoid -> Mul` Swish gates are recognized after Conv+BatchNorm folding. The fused node writes `x * sigmoid(x)` directly into the Conv destination, eliminating the two branch activation tensors while retaining the exact FP32 operation order. The standalone BatchNorm variant follows the same rule when a model cannot fold the affine layer. - `PPOCR_APPROX_GELU=1` is an opt-in PP-OCRv6 deployment mode for eligible model-aware fused BatchNorm+GELU planes. Its AVX-512/AVX2 dispatch is based on per-plane shape rather than total batch size, so batching cannot change a page's GELU implementation; generic GELU retains exact ONNX Erf semantics. Model constants are no longer copied for every `Run()` invocation: they stay in the immutable model store and execution allocates only inputs and temporary tensors. This substantially lowers peak transient memory usage for the detector/recognizer. Example cross builds (use the appropriate compiler/toolchain installed by your environment): ```powershell # Native ARM64 build (Visual Studio developer prompt) cmake -S . -B build-arm64 -A ARM64 cmake --build build-arm64 --config Release # Cross build using an ARM64 CMake toolchain file cmake -S . -B build-aarch64 -DCMAKE_TOOLCHAIN_FILE=toolchains/aarch64.cmake cmake --build build-aarch64 --parallel ``` The generic executor is not compiled for a host-specific ISA, avoiding accidental illegal instructions on older x86 CPUs and on ARM targets. The AVX-512 path remains the default on a supported CPU, while the switch is kept for deployment-specific validation. ## ONNX Runtime comparison benchmark The repository contains a repeatable CPU-only benchmark. It compares the complete detector + recognition pipeline on the same PPM image, uses ONNX Runtime's `CPUExecutionProvider`, warms up both engines, and reports mean, median, p95, load latency and throughput. ```powershell cmake -S . -B build -G "MinGW Makefiles" cmake --build build --parallel powershell -ExecutionPolicy Bypass -File .\bench\compare.ps1 -Runs 20 -Warmup 3 ``` ### 2026-08-14: wider correctness corpus and normalization A/B The regression runner now accepts a UTF-8-BOM JSON case manifest as well as plain UTF-8. This makes a small PowerShell-authored subset reproducible rather than failing before inference. The current native validation covered all three models against ONNX Runtime text output on English, Chinese, and `3000x80` wide text (`3/3` cases each): `build/accuracy_default_preprocess_ab_{tiny,small,medium}_20260814.json`. Stress coverage also includes dense UI, tall text, and a `2560x1440` screenshot. Tiny completed all 11 built-in cases; focused high-cost samples completed for small (`3/3`) and medium (`2/2`), including 38 medium screenshot regions. Reports are under `build/stress_*_current_20260814.json`. Preprocessing retains its exact fused bilinear + normalization path. A compact 256-entry uint8 normalization lookup implementation was evaluated, but it regressed medium on this AVX-512 host due to indexed-load/cache pressure. It therefore remains an opt-in A/B switch (`PPOCR_ENABLE_PREPROCESS_NORMALIZE_LUT=1`) rather than changing the production default. The paired result is recorded in `build/bench_preprocess_lut_ab_current_20260814.json`; this keeps the default chosen by measured end-to-end performance, not by a microbenchmark assumption. The runtime AVX-512 dispatch remains dynamic: its current 15-run English A/B against the AVX2 fallback measured **29.520 vs 34.133 ms** (tiny), **56.578 vs 80.106 ms** (small), and **212.366 vs 323.044 ms** (medium). See `build/bench_avx512_dynamic_current_20260814.json`. Vulkan smoke continues to pass on AMD Radeon 780M and admits the channel-affine+Swish and depthwise+Swish GPU segments only when their complete transfer/dispatch/readback timing is no slower than CPU. GPU-only correctly refuses construction until the complete PP-OCRv6 device graph exists; it never silently runs CPU work. `compare.ps1` now treats the requested performance target as a gate. Its `-MinImprovementPercent` parameter defaults to `30`, and it returns a non-zero exit code when end-to-end native latency is not at least that much lower than the paired ONNX Runtime CPU measurement. Set it explicitly when a deployment uses a different threshold. `bench/ort_bench.py` requires `onnxruntime`, `numpy`, and Pillow in the Python environment chosen by `bench/compare.ps1`. `ppocr_bench` keeps the native OCR instance alive during all warmup/measurement iterations, so model parsing and weight loading are excluded from steady-state latency just as they are in the ONNX Runtime side. ### Latest measured result The latest comparison is two consecutive `bench/compare.ps1` runs (tiny det+rec, `build/regression_ppm/en.ppm`, `PPOCR_BACKEND=cpu`, 16 threads, 3 warmup / 20 measured, `PPOCR_APPROX_GELU` unset) captured on 2026-08-20 after fusing the inverted-residual `1x1 expand → exact GELU → 1x1 project → residual Add` into one spatial-tiled AVX-512 kernel (10 detector + 7 recognizer sites), plus the prior AVX-512 stem/FPN/GEMM path. Model loading is excluded from steady-state time. Figures are copied from those two JSON blobs; they are not averaged into a third number. | Run | Engine | Mean latency | Throughput | C++ / ORT | Latency improvement | |---|---|---:|---:|---:|---:| | 1 | Pure C++ PP-OCRv6 | 20.972 ms | 47.682 FPS | 0.208x | 79.16% | | 1 | ONNX Runtime 1.28 CPUExecutionProvider | 100.631 ms | 9.937 FPS | | | | 2 | Pure C++ PP-OCRv6 | 20.973 ms | 47.681 FPS | 0.209x | 79.15% | | 2 | ONNX Runtime 1.28 CPUExecutionProvider | 100.588 ms | 9.942 FPS | | | Both runs used `faster_engine = ppocr_cpp`. Native median/p95 were 20.884/23.443 ms and 20.376/23.720 ms. Relative to this change's pre-change native ceiling of 30.280 ms, the pair is 30.7% / 30.7% lower latency (each mean ≤ 21.196 ms). This is a host-specific result, not a portable performance guarantee; rerun on the deployment CPU before capacity planning. The previous pair after AVX-512 stride-2 tiles / fused MaxPool+Concat / FPN Concat+Conv was 24.721 / 24.887 ms native vs 125.828 / 102.010 ms ONNX Runtime (80.35% / 75.60%). The AVX2 exact-Erf GELU pair was 26.397 / 26.357 ms vs 99.273 / 111.544 ms (73.41% / 76.37%). The longer 60-run snapshot (2026-08-11, 8 warmup) on `build/en.ppm` was 33.074 ms native vs 138.359 ms ONNX Runtime. Keep those as historical context only; the table above is the current `compare.ps1` pair. Reproduce the benchmark with: ```powershell powershell -NoProfile -ExecutionPolicy Bypass -File .\bench\compare.ps1 ` -Runs 20 -Warmup 3 -Threads 16 -Backend cpu ` -NativeExe .\build-gpu-only-final\ppocr_bench.exe ``` `bench/compare.ps1` drives the tiny pair. For small/medium, invoke `build/ppocr_bench.exe` and `bench/ort_bench.py` directly with their matching models and dictionary, as shown by the commands below. ### Current three-model CPU comparison After the ARM/NEON GEMM bias-broadcast correction, the current x86 AVX-512 build was rerun on `build/en.ppm` with `PPOCR_BENCH_THREADS=16`, three warmups, and ten timed runs per engine. The native and ONNX Runtime runs use the same detector/recognizer pair, complete OCR pipeline, image, thread budget, and warmup/run counts. This is a fresh shared-host snapshot rather than a portable guarantee, but it confirms that all three supported PP-OCRv6 sizes remain well ahead of ONNX Runtime CPUExecutionProvider: | Model | Pure C++ mean | ORT mean | C++ / ORT | C++ latency reduction | |---|---:|---:|---:|---:| | tiny | 29.757 ms | 142.158 ms | 0.209x | 79.1% | | small | 63.395 ms | 198.205 ms | 0.320x | 68.0% | | medium | 191.198 ms | 374.134 ms | 0.511x | 48.9% | The corresponding native throughput was 33.606, 15.774, and 5.230 FPS, compared with ORT's 7.034, 5.045, and 2.673 FPS. Re-run the paired commands on the deployment hardware before using these values for capacity planning. The scalar/NEON GEMM fallback was also corrected to broadcast the bias by output column (the same semantics as AVX-512/AVX2), rather than incorrectly using one bias value for a whole output row. This is a correctness fix for ARM targets and does not change the dynamically dispatched x86 measurements. ### 2026-08-13: refreshed long three-model CPU/ORT comparison After the AVX-512 stride-one `3x3` eight-output tile was revalidated as the default on all three model sizes, the complete `en.ppm` OCR pipeline was rerun with 16 native/ORT CPU workers, five warmups, and 40 timed iterations per engine. This is the longer same-host measurement; it includes detector, crop construction, recognizer, and decoding, but excludes model loading from steady-state latency. ORT used CPUExecutionProvider only. | Model | Pure C++ mean | ORT mean | C++ / ORT | C++ latency reduction | |---|---:|---:|---:|---:| | tiny | 32.244 ms | 119.475 ms | 0.270x | 73.0% | | small | 67.857 ms | 151.359 ms | 0.448x | 55.2% | | medium | 220.768 ms | 365.267 ms | 0.604x | 39.6% | All three exceed the repository's 30% paired CPU performance gate on this host. The benchmark samples remain machine-specific and ORT has wider timing variation on this shared Windows host; full raw measurements are retained in `build/compare_three_models_tile8_long_20260813.json`, so this table is not a cross-platform guarantee. ### 2026-08-11: refreshed three-model exact CPU comparison A fresh paired short run on `regression_ppm/en.ppm` used the same complete OCR pipeline, matching 16-thread budgets, three warm-ups, and ten timed runs. AVX-512 was dynamically selected on this x86 host; no approximation mode was enabled. The native and ORT measurements were executed separately on a shared Windows machine, so these are reproducible evidence for this host閳ユ攺ot a portable capacity promise. | Model | Pure C++ mean | ORT mean | C++ / ORT | C++ latency reduction | |---|---:|---:|---:|---:| | tiny | 31.251 ms | 129.990 ms | 0.240x | 76.0% | | small | 70.912 ms | 201.911 ms | 0.351x | 64.9% | | medium | 199.734 ms | 409.505 ms | 0.488x | 51.2% | This refresh keeps all three PP-OCRv6 sizes above the requested 30% native CPU latency advantage on the same exact input. It follows the recognizer executor lifetime fix that now executes `FusedBatchNormSwishAdd` in the terminal-CTC path as well as the ordinary graph executor. The path reuses the dying BatchNorm activation for affine + exact Swish + shortcut Add, reducing the live feature-map frontier for small/medium crop batches. A profile confirms the node as `ctc:FusedBatchNormSwishAdd:BatchNormalization.2_swish_add`. The batch/memory follow-up used six varied PP-OCRv6 tiny inputs閳ユ攩rom `16x16` through a `3000x80` strip閳ユ敋ith English, Chinese, and mixed content. Serial and `RecognizeBatch()` outputs were identical (10 total results); bounded two-page scheduling measured **134.218 ms serial / 86.501 ms batch (1.552x)**. A medium-model memory cycle spanning `16x16`, mixed text, and a dense `1920x1080` page used 369,672,192 private bytes after warm-up and finished at +3,145,728 bytes. This is a Windows allocator high-water observation, not a leak assertion; it bounds the varied-shape batch check to a small transient increase rather than retaining every crop workspace. ## Vulkan backend status The public API now exposes `ppocr::Backend::{cpu_only,gpu_only,hybrid}` and `QueryBackendInfo()`. Runtime Vulkan probing is compiled from vendored Khronos headers and dynamically loads `vulkan-1.dll`, so the binary does not require a Vulkan SDK installation on its deployment machine. A generated SPIR-V compute shader and smoke executable validate FP32 Add/Sub/Mul/Div building blocks over scalar, non-workgroup-aligned, and recognizer-scale tensor sizes. On this host, `ppocr_demo --backend-info` and `ppocr_vulkan_smoke` report the AMD Radeon 780M compute queue and pass those correctness checks. `gpu_only` deliberately fails if a complete PP-OCRv6 GPU graph is not available; it never silently falls back to CPU. `hybrid` now connects verified equal-shape in-place Add/Sub/Mul/Div executor nodes and eligible `1x1` Conv nodes to Vulkan. For NCHW recognition tensors it maps N to the shader's Y dimension, so a same-width crop batch uses one submission instead of one submission per crop. A cached per-shape/per-operation admission benchmark validates results and includes host/device transfer, dispatch, and readback before it may select GPU; all other operators remain on the portable CPU path. The Vulkan loader, instance, device, queue, pipeline, descriptor set, command buffer, and grow-only host-visible buffers are persistent, avoiding a per-dispatch Vulkan rebuild. ### 2026-08-12: depthwise GPU activation fusion The batched Vulkan depthwise primitive now fuses its directly following `ReLU` or exact `Swish` into the convolution writeback. This covers the common PP-OCRv6 MobileNet depthwise-gate shape without creating a CPU-side activation pass or an intermediate host tensor. The runtime still performs shape-specific full-boundary admission (H2D, dispatch, fence, D2H, and CPU SIMD comparison), and the new Vulkan smoke case validates batched odd-size depthwise+Swish values against the scalar reference. It is therefore a batch-capable hybrid building block, not a claim that the still-partial GPU graph is universally faster; `gpu_only` remains unavailable. On the local Radeon 780M, the `N=4,C=32,H=31,W=29,3x3` admission sample was **0.36344 ms GPU vs 0.62156 ms CPU**, so hybrid selected the fused GPU path. The unfused depthwise sample in the same run was **0.33214 ms GPU vs 0.34084 ms CPU**. These figures include all transfers and synchronization and are recorded as a per-segment comparison, not an end-to-end OCR claim. The same depthwise kernel now also has a fused exact HardSwish variant for PP-OCRv6 MobileNet blocks. Its smoke admission sample measured **0.32760 ms GPU vs 0.32076 ms CPU**, so hybrid correctly retained CPU for that shape; the decision is dynamic rather than a blanket GPU preference. ### 2026-08-12: five-size batch and memory recheck After the depthwise fusion work, CPU and hybrid `RecognizeBatch()` were rechecked against serial execution on five deliberately different inputs: `16x16`, English text, low-contrast text, a `3000x80` ultra-wide strip, and CJK. All three official model sizes preserved exactly the same nested OCR results. With one page worker, `det_batch_size=4`, and `rec_batch_size=4`, the CPU measured serial/batch pairs were tiny **204.163/190.580 ms (1.071x)**, small **392.496/375.710 ms (1.045x)**, and medium **1260.420/1304.108 ms (0.967x)**. This mixed-shape result confirms that batching is bounded-capacity scheduling rather than an unconditional latency promise. The corresponding hybrid run was also result-identical (tiny **204.311/210.308 ms**, small **436.332/411.353 ms**, medium **1420.791/1461.834 ms**). For compatible same-shape work, eight `en.ppm` pages with `det_batch_size=8` and `rec_batch_size=8` measured **1.485x** CPU page-batch throughput for tiny and **1.201x** for small. Medium did not benefit in that shared-host sample (**0.911x**), and is therefore left with the conservative defaults rather than promoting a batch size that regresses it. A two-cycle five-size tiny memory soak started from **137,195,520 bytes** after warm-up and ended at **+32,768** then **-151,552 bytes**, showing no retained per-shape activation frontier. Decoded-text comparison against ONNX Runtime was also rerun after these changes for tiny, small, and medium on four diverse dimensions (`16x16`, English, low-contrast, and `3000x80`): all **12/12** cases passed. ### 2026-08-12: GPU-aware batch regression refresh The GPU and batch paths were rerun after the latest Vulkan/CPU optimization pass. The public `RecognizeBatch()` API uses an exact-shape `[N,3,H,W]` detector batch and same-width `[N,3,48,W]` recognizer crop batch; it never rescales or pads unrelated page sizes merely to enlarge `N`. `det_batch_size`, `rec_batch_size`, `rec_batch_width_bucket`, and `image_batch_parallelism` remain the bounded controls, with `ppocr_batch_smoke` checking every batched result against serial OCR before timing it. On the local AVX-512 / Radeon 780M host, eight identical `en.ppm` tiny-model pages (`det_batch_size=8`, `rec_batch_size=8`, one page worker) returned the same 16 decoded results as serial execution. The measured CPU serial/batch pair was **329.520 / 201.876 ms (1.632x)**; hybrid was **382.402 / 237.445 ms (1.610x)**. A five-size small-model check (`16x16`, English, low contrast, `3000x80`, CJK; batch sizes 4) likewise preserved all 9 results: CPU **520.065 / 512.529 ms (1.015x)** and hybrid **438.718 / 427.016 ms (1.027x)**. The mixed-size figures are intentionally not presented as a universal throughput guarantee. The refreshed Radeon Vulkan primitive suite passed numerical validation for every existing batch primitive. Its strict full-boundary probes again admitted the heavier fused work where it won (affine+Swish **0.16926 ms GPU vs 0.31296 ms CPU**; depthwise+Swish **0.36218 vs 0.66808 ms**; depthwise+HardSwish **0.36482 vs 0.37636 ms**) and retained CPU for transfer-bound `1x1` Conv, ordinary/transpose Conv, resize+add, and GEMM. This is intentional dynamic selection: `gpu_only` remains explicitly unavailable until the complete detector, recognizer, transformer, and CTC graph are device-resident. ### 2026-08-12: three-model CPU/GPU batch baseline refresh The current pure-C++ build was rebuilt and rechecked on the complete tiny/small/medium model set. The SIMD kernel suite passed under normal AVX-512 dispatch and again with `PPOCR_DISABLE_AVX512=1`, exercising the AVX2 fallback. The Vulkan smoke suite passed on the Radeon 780M, including batched binary/broadcast/affine/pointwise/depthwise/ordinary-convolution/transpose/ resize-add/GEMM primitives. In that run the full-boundary admission policy selected Vulkan for depthwise (**0.35698 vs 0.36624 ms**), depthwise+Swish (**0.35930 vs 0.59468 ms**), depthwise+HardSwish (**0.34864 vs 0.41902 ms**), and affine+Swish (**0.17778 vs 0.34676 ms**); all transfer-bound primitives stayed on CPU. The decision is shape-local and dynamic, so it lowers CPU work without forcing a slower device boundary. Decoded-text parity against ONNX Runtime passed **4/4** diverse inputs for each model (tiny, small, medium): `16x16`, English, low contrast, and an ultra-wide `3000x80` strip. A five-size serial-versus-batch gate for medium also preserved all 9 results in CPU and hybrid modes. Its measured CPU pair was **1257.378 / 1292.344 ms** and hybrid was **1266.158 / 1279.121 ms**; this heterogeneous batch did not improve latency, so it is recorded as a correctness/capacity check rather than a throughput claim. For an `en.ppm` single-page steady-state snapshot (two warmups, five runs, 16 kernel workers), current CPU means were tiny **38.755 ms**, small **73.192 ms**, and medium **208.209 ms**. Hybrid means were **34.575 ms**, **75.141 ms**, and **238.014 ms** respectively. The short shared-host sample therefore supports hybrid for tiny when the admitted GPU segments help, while CPU remains the sensible choice for small/medium on this UMA device. These numbers are host-specific and do not change the explicit `gpu_only` contract. ### 2026-08-12: hybrid admission re-audit The next GPU-admission pass measured the current single-primitive boundaries again instead of lowering thresholds blindly. On this Radeon 780M, batched depthwise+Swish remained a useful GPU segment (**0.32756 ms GPU vs 0.57274 ms CPU**), and the depthwise+HardSwish sample also crossed the no-slower rule (**0.32540 vs 0.36794 ms**). Pointwise Conv, ordinary Conv, transpose Conv, resize/add, and GEMM samples remained transfer/synchronization-bound, so they continue on CPU. This preserves the hybrid contract: GPU is selected whenever the complete measured boundary is equal or faster, not merely because a shader exists. The current source also retains conservative size guards before invoking an admission microbenchmark. A short experiment removing them caused extra small-shape probes and worse end-to-end tiny behavior on this UMA device, so the guards were retained. Fresh post-audit checks passed kernel SIMD smoke, the full Vulkan smoke suite, three-model four-size ONNX Runtime decoded-text parity (**12/12**), five-size serial/batch output identity, and a medium hybrid two-cycle memory soak. The latter began at **494,878,720 bytes** after warm-up and ended **9,105,408** and **11,042,816 bytes below** baseline. ### 2026-08-12: GPU batch revalidation GPU execution remains batch-aware: equal-width recognition crops are one NCHW Vulkan dispatch (the shader maps crop `N` to its Y dimension), while the CPU page scheduler bounds concurrent detector/recognizer workspaces. The pointwise Conv shader stages compact immutable `[M,C]` weights in workgroup memory and handles normal, ReLU, Swish, Sigmoid, and residual-Swish forms without an intermediate activation. Hybrid admission still measures the entire H2D + dispatch + D2H path for each shape and uses GPU only when the validated result is no slower than the CPU SIMD counterpart. ### 2026-08-12: exact-shape detector batching `RecognizeBatch()` now also coalesces pages whose normal PP-OCR detector resize dimensions are exactly equal. This is a real `[N,3,H,W]` detector batch: no document is padded, and each result is independently DB postprocessed from its own probability-map slice. `Options::det_batch_size` (default `4`) bounds the detector activation frontier; the smoke harness accepts `PPOCR_DET_BATCH_SIZE` for A/B runs. CPU batch kernels therefore see useful detector `N` work in addition to the existing same-width recognizer crop batches. The current hybrid path deliberately keeps this new detector batching at `N=1`. Its Vulkan coverage is still partial and synchronous, so applying an unproven large detector GPU segment would be transfer/admission-bound rather than an optimization. CPU-only detector batches are available now; hybrid continues to batch recognizer crops and only admits validated no-slower GPU segments. This preserves the explicit `gpu_only` contract: full GPU OCR is not claimed until detector, recognizer, transformer, and CTC stay device-resident. On this AVX-512 / Radeon 780M host, tiny PP-OCRv6 over eight identical `en.ppm` pages (recognizer batch `8`, one worker) produced exactly the same 16 OCR results as serial inference. The steady-state smoke measurement was **323.281 ms serial / 195.744 ms batch (1.652x)** with `PPOCR_DET_BATCH_SIZE=8`, versus **330.019 / 247.684 ms (1.332x)** with detector batching forced to one. The hybrid check preserved output identity at **311.225 / 243.242 ms (1.279x)**; as designed, it retained singleton detector dispatches. A varied five-page CPU run (`16x16`, English, low contrast, `3000x80`, and dense `1920x1080`) stayed correct but measured only **1.019x**, so equal-shape coalescing is a throughput tool for compatible page sets閳ユ攺ot a universal mixed-document latency guarantee. Tiny, small, and medium each also retained their four-image C++/ONNX Runtime decoded-text parity gate after the change. ### 2026-08-12: FPN resize-plus-add write fusion The detector loader now recognizes the exported FPN sequence `Resize(nearest, asymmetric floor, 2x) -> Add(equal-shape residual)` and replaces it with a single `FusedNearestResizeAdd` node. Its NCHW implementation writes each replicated source value directly into the final residual-sum allocation. This removes the enlarged temporary feature map, its allocation, and its second full read/write traversal. The fusion is deliberately limited to the exact 2x detector geometry; all other ONNX Resize combinations retain the generic executor. The local profile confirms all three recursive FPN additions are fused (`Resize.0_add`, `Resize.1_add`, `Resize.2_add`). The post-change five-run comparison on `en.ppm` (2 warm-ups, 16 benchmark threads) measured: | Model | C++ hybrid mean | ORT CPU mean | C++ latency advantage | |---|---:|---:|---:| | tiny | 32.104 ms | 148.088 ms | 78.32% | | small | 66.087 ms | 237.150 ms | 72.13% | | medium | 259.850 ms | 454.815 ms | 42.87% | This is one host's paired measurement rather than a general hardware claim. Decoded-text parity against ONNX Runtime passed for each model on four varied inputs (`16x16`, normal English, low contrast, and a `3000x80` strip). A medium two-cycle varied-shape soak (`16x16`, English, low contrast, strip, and `1920x1080` UI) ended at **-77,824 bytes** versus its warmed 391,249,920 byte private-memory baseline; this is allocator reuse evidence, not a claim of zero memory use or a leak proof. ### 2026-08-12: runtime-SIMD FPN resize-add kernel `FusedNearestResizeAdd` now calls a dedicated kernel rather than using scalar replication inside the graph executor. The new kernel performs the 2x replication and both residual-row additions in the same final stores, with runtime AVX-512, AVX2, ARM NEON, and scalar fallback implementations. It is covered by odd-width and batched low-level smoke cases, and `PPOCR_DISABLE_RESIZE2X_ADD_SIMD=1` provides a narrowly scoped scalar A/B path without disabling the other inference SIMD kernels. On the local AVX-512 machine, a dense `1920x1080` tiny page (123 detected crops, 2 warm-ups / 5 timed runs) measured **264.174 ms** with this SIMD path versus **265.651 ms** through the scalar resize-add fallback (about **0.56%** faster). This small but repeatable bandwidth-path improvement is reported separately because whole-page timing remains sensitive to crop-count and host load; correctness was also checked with AVX-512 disabled. The refreshed local Radeon 780M smoke run passed all binary, broadcast, affine, pointwise, residual, odd-plane, batched, and parameterized `Conv+HardSigmoid` graph primitives. Its measured admission samples were: affine+Swish **0.16466 ms GPU / 0.29544 ms CPU** (admitted), standalone `1x1` Conv **0.14536 / 0.01952 ms**, and residual `1x1` Conv+Swish **0.18884 / 0.10648 ms** (both correctly retained on CPU). A tiny `en.ppm` pipeline check (1 warm-up, 8 runs) measured **30.122 ms CPU** and **29.521 ms hybrid**; output stayed identical. This small 2.0% difference is host-noise-sensitive and is not presented as a general GPU speed claim. ### 2026-08-12: three-model, diverse-size regression The CPU executor now has a dedicated `1x1 Conv -> HardSwish` path for the canonical detector squeeze-excitation sequence. It writes convolution output directly to the final activation allocation, then applies the existing runtime-selected AVX-512/AVX2/NEON/scalar HardSwish kernel in place. This preserves the exact `Conv -> HardSigmoid -> Mul` FP32 result while avoiding the generic graph's temporary Conv activation. The Vulkan pointwise shader also now covers parameterized `Conv+HardSigmoid`; its alpha/beta are passed as raw FP32 push constants and remain behind the same complete-transfer admission rule. After the change, C++ versus ONNX Runtime decoded-text parity passed **7/7** tiny samples covering English, CJK, blank, `16x16`, `3000x80`, low-contrast, and mixed-content inputs. Small and medium also passed the ONNX Runtime text gate on `en.ppm`. Kernel and Radeon 780M Vulkan smoke suites passed, including the batched odd-plane pointwise and new HardSigmoid case. On `en.ppm` (2 warm-ups, 10 timed runs, AVX-512 host), the measured end-to-end comparison was: | Model | C++ CPU mean | ONNX Runtime CPU mean | C++ advantage | |---|---:|---:|---:| | tiny | 29.159 ms | 121.267 ms | 4.16x faster | | small | 61.785 ms | 184.410 ms | 2.98x faster | | medium | 190.529 ms | 360.038 ms | 1.89x faster | These measurements use the repository's ORT benchmark with the same image and CPU execution provider; they are host-specific, but demonstrate a consistent gain for all three PP-OCRv6 model sizes. A multi-size page-batch regression (`16x16`, `3000x80`, and dense `1920x1080`) preserved output identity for tiny/small/medium. Its throughput was 1.035x for tiny and 0.982x/0.975x for small/medium on this mixed workload, so batching remains a bounded-capacity feature rather than a universal latency claim. A medium two-cycle mixed-size memory soak ended only **+4,345,856 bytes** above its 374,845,440-byte warmed baseline. ### 2026-08-12: CTC LayerNorm activation reuse `RunCtcTop1()` is the recognizer path used by public OCR calls. It now reuses the input allocation for a fused transformer `LayerNorm` when that input has one remaining consumer. LayerNorm first completes the mean/variance reductions for a row and only then writes that row, so the source/destination alias is safe and preserves the existing FP32 expression. This removes eligible `[N,T,C]` temporary activations in the small and medium recognizers without changing model scope or introducing a runtime dependency. The feature has an A/B escape hatch, `PPOCR_DISABLE_CTC_LAYERNORM_INPLACE=1`, for deployment validation. On the local AVX-512 host it produced modest steady-state benefit on the short `en.ppm` workload (medium 225.182 ms enabled versus 227.161 ms disabled, 2 warm-ups / 10 runs); actual benefit is expected to be more useful for activation pressure on dense crop batches. The full post-change regression passed kernel SIMD smoke, Radeon 780M Vulkan smoke, the seven-image tiny ONNX Runtime text gate, and small/medium ONNX Runtime text parity on `en.ppm`. The small/medium three-size batch gate also preserved all 121/124 decoded results. `gpu_only` remains explicitly unavailable until the entire detector, recognizer, transformer, and CTC pipeline is device-resident; hybrid continues to admit each batch-shaped Vulkan segment only when end-to-end transfer, dispatch, and readback are no slower than CPU. ### 2026-08-12: hybrid batch verification harness `ppocr_batch_smoke` now accepts `PPOCR_BACKEND=cpu|hybrid|gpu` and the recognizer batch controls `PPOCR_REC_BATCH_SIZE` and `PPOCR_REC_PARALLELISM`. This makes the same serial-versus-page-batch bit-for-bit output gate usable for the GPU-enabled hybrid path, instead of testing only CPU scheduling. `gpu` retains the library's explicit failure contract while the full PP-OCRv6 graph is not Vulkan-resident. On the local AVX-512 / Radeon 780M host, tiny PP-OCRv6 with four varied pages (`en`, `line_en`, `mixed`, `zh`), recognizer batch size 4 and one recognizer worker produced eight identical OCR results in both serial and page-batched modes. The measured scheduler results were: | Backend | Serial | Page batch (parallelism 2) | Speedup | |---|---:|---:|---:| | CPU | 120.904 ms | 81.793 ms | 1.478x | | Hybrid | 147.315 ms | 104.593 ms | 1.408x | The hybrid result is intentionally reported as measured, not as a GPU win: on this UMA GPU the available Vulkan segments are partial and transfer-bound outside the admitted affine+Swish shape. The key correctness result is that batch output remains identical while all runtime GPU selection stays per-shape, batched, and no-slower-than-CPU. Reproduce with: ```powershell $env:PPOCR_BACKEND='hybrid' $env:PPOCR_REC_BATCH_SIZE='4' $env:PPOCR_REC_PARALLELISM='1' .\build\ppocr_batch_smoke.exe DET.onnx REC.onnx dict.txt 2 ` .\build\regression_ppm\en.ppm .\build\regression_ppm\line_en.ppm ` .\build\regression_ppm\mixed.ppm .\build\regression_ppm\zh.ppm ``` ### Batch scheduling and current GPU evidence `OCR::RecognizeBatch(const std::vector&)` is now the public page-batch API. It preserves image/result order and exact per-image OCR output, while each page keeps the existing same-width recognition-crop batching. Page images are scheduled independently rather than padded into a single detector tensor: this avoids wasted detector pixels and preserves PP-OCRv6's dynamic resize semantics. `Options::image_batch_parallelism` bounds live page workspaces (default `1`); when it is above one, the configured recognizer-worker budget is divided among concurrently running pages so image batching does not multiply the CPU thread count or activation frontier. `ppocr_batch_smoke` checks every nested batch result against serial `Recognize()` before reporting timing. On the local AVX-512 / Radeon 780M host, after warm-up, the tiny model over four mixed PPM pages (`en`, `mixed`, `line_en`, `line_zh`) produced the same seven OCR results in serial and batch modes. A short 2026-08-11 CPU snapshot with page parallelism `2` measured **81.981 ms serial** and **53.183 ms batched** (**1.541x** throughput). This is a scheduler/host-specific result, not a promise for every page mix; the bounded default remains one page to minimize peak memory. Reproduce it with: ```powershell .\build\ppocr_batch_smoke.exe DET.onnx REC.onnx dict.txt 2 ` .\build\regression_ppm\en.ppm .\build\regression_ppm\mixed.ppm ` .\build\regression_ppm\line_en.ppm .\build\regression_ppm\line_zh.ppm ``` The Vulkan path already batches same-width NCHW crop tensors in one dispatch and holds model-constant compact RHS/BatchNorm coefficients in persistent buffers. The post-change Radeon 780M smoke run passed scalar, odd-sized, four-stage chain, four-crop, broadcast, affine, and fused affine-Swish checks. Its measured full-transfer admission samples were binary **0.29160 vs 0.01368 ms**, four-stage chain **0.16206 vs 0.02984 ms**, four-crop chain **0.14928 vs 0.02766 ms**, and channel-affine **0.13928 vs 0.01326 ms** (GPU vs CPU), so hybrid correctly rejected those transfer-bound segments. The batched affine-Swish segment measured **0.18272 ms GPU vs 0.30170 ms CPU** and was therefore admitted for that exact shape. This is an isolated, verified GPU segment; full `gpu_only` OCR remains unavailable until the convolution, pooling, resize, transformer, and CTC graph is device-resident. On the Radeon 780M, the latest 65,537-float repeated admission test measured about **0.119 ms GPU versus 0.013 ms CPU**, so hybrid correctly declines this isolated elementwise segment rather than increasing latency or CPU work. This is an honest partial-graph capability contract, not a claim that GPU inference is already complete. The same persistent shader now supports a dependent chain of up to four Add/Sub/Mul/Div operations in one dispatch, reusing device-resident operands without intermediate host transfers. Its smoke test validates a 65,537-float four-stage chain and applies the same full H2D/dispatch/D2H comparison. On this Radeon 780M it measured about **0.124 ms GPU versus 0.030 ms CPU**, so hybrid again correctly declines the GPU segment. This supplies a reusable fused-elementwise building block for future PP-OCR graph segments without misrepresenting it as complete GPU inference. The Vulkan path also implements compact suffix-broadcast channel gates such as `[N,C,H,W] op [N,C,1,1]`: the right operand is transferred once per batch shape instead of being materialized to `N*C*H*W`. Runs whose four lanes stay inside a channel span use `vec4`; only span boundaries use the scalar tail. Its own full-transfer admission benchmark validates every output and retains CPU SIMD unless the compact-GPU path wins. On this Radeon 780M, a four-item 8,192-float, 64-value-span `Mul+Add` gate measured **0.143 ms GPU versus 0.013 ms CPU**, so the policy correctly declined it. This is nevertheless important GPU groundwork: it removes broadcast-expansion memory/traffic and is connected to both generic and terminal-CTC in-place executor paths. The batch path now also distinguishes the two valid compact RHS layouts: dynamic `[N,C,1,1]`, and the much more common learned constant `[1,C,1,1]`. The latter is now stored once in the persistent Vulkan RHS buffer and addressed with a zero batch stride in the shader, rather than being duplicated/uploaded once for every recognition crop. The Vulkan smoke test executes two different four-crop activations against the same immutable 128-value RHS to verify both batch indexing and cache reuse. On the AMD Radeon 780M, measured on 2026-08-11 after this change, the full-transfer four-crop `8,192`-float `Mul+Add` shared-RHS building block was **0.11126 ms GPU versus 0.00772 ms CPU**, so hybrid still (correctly) selects CPU SIMD. This is a real batch-memory/traffic reduction, not a claim of end-to-end GPU OCR acceleration while the surrounding graph remains CPU-resident. The next batched GPU primitive is the exact inference affine `x * scale + bias` used by fused BatchNorm. It keeps both `[1,C,1,1]` coefficient vectors compact, caches immutable scale/bias pairs separately, and transforms all same-width crops in one dispatch; it is wired into both normal and terminal-CTC executor paths behind the same full-transfer admission gate. The refreshed Radeon 780M smoke run (2026-08-11) measured the four-crop `8,192`-float shared-coefficient affine at **0.10054 ms GPU versus 0.00656 ms CPU**, so hybrid correctly retains the CPU SIMD affine path on this UMA device. This establishes a useful batch/residency building block without pretending that the present partial Vulkan graph makes end-to-end OCR faster. The same compact four-crop kernel now also has an exact FP32 `BatchNorm affine -> Swish` mode, matching the recognizer's folded `FusedBatchNormSwish` nodes. It evaluates `x * scale + bias` and its logistic activation in one Vulkan dispatch, keeps immutable `[1,C,1,1]` scale/bias vectors cached, and avoids returning to the CPU between the two operations. It is connected to the hybrid executor through its own per-shape end-to-end admission check. On the Radeon 780M on 2026-08-11, the four-crop `8,192`-float shared-coefficient test measured **0.16744 ms GPU versus 0.37652 ms CPU**, so this heavier fused batch segment was admitted to GPU; the smoke test validates every result against the exact CPU Swish expression. This is a genuine batched hybrid acceleration, while the surrounding PP-OCR graph remains CPU-resident and `gpu_only` remains unavailable. ### 2026-08-11: batched pointwise-Conv GPU experiment The Vulkan executor now also contains a verified FP32 NCHW `1x1` Conv primitive, with optional fused ReLU, Swish, and Sigmoid. It accepts a batch of equal-width recognition crops in one dispatch, reads learned `[M,C]` weights and bias once from persistent compact buffers, and writes to a separate lazily allocated output buffer. The shader processes four adjacent spatial positions per invocation when they remain in one output-channel plane; scalar tails preserve odd widths and channel boundaries. The hybrid executor is wired only for unit-stride, unpadded, ungrouped `Conv` / `Conv+ReLU` with immutable ONNX weight+bias inputs. As with existing hybrid nodes, each `(N,C,M,plane,ReLU)` shape is admitted only after an end-to-end H2D/dispatch/D2H benchmark validates every FP32 result against the CPU SIMD kernel. On the local AMD Radeon 780M, the four-crop `C=24, M=40, plane=257` smoke case passed normal and ReLU numerical checks. Its complete-transfer admission measurement was **23.715 ms GPU versus 0.026 ms CPU**, so hybrid correctly rejected this pointwise segment on this UMA driver. The implementation is kept as a portable batched GPU building block for devices/shapes where it wins; it does not claim a GPU speedup here and does not change the `gpu_only` contract. ### 2026-08-12: fused GPU Sigmoid gate and refreshed checks The pointwise Vulkan path now also fuses exact `Conv -> Sigmoid` (shader mode 10). This covers the detector's post-folding pointwise gate form without a CPU activation pass or an extra activation buffer. Its public low-level API and hybrid admission key distinguish ReLU, Swish, and Sigmoid; the admission probe includes complete host upload, dispatch, fence wait, readback, and FP32 comparison with the CPU reference. A shape is selected only when that whole round trip is no slower than CPU, and `gpu_only` remains unavailable until the complete detector/recognizer/transformer/CTC graph is device-resident. On the local AVX-512 / AMD Radeon 780M host, the refreshed Vulkan smoke test passed the batched, odd-plane (`N=4, C=24, M=40, plane=257`) Conv+Sigmoid numerical check together with existing binary, affine, Swish, residual, and shared-memory-tiled pointwise checks. The same run reported: | Segment admission sample | GPU | CPU | Hybrid selection | |---|---:|---:|---| | Batch affine+Swish | 0.15924 ms | 0.29874 ms | GPU | | Batch `1x1` Conv | 0.16150 ms | 0.02062 ms | CPU | | Batch `1x1` Conv+Swish | 0.18282 ms | 0.10534 ms | CPU | | Batch residual `1x1` Conv+Swish | 0.14726 ms | 0.09338 ms | CPU | These are measured partial-segment results on a UMA GPU, not an end-to-end GPU claim. A post-change whole-pipeline smoke benchmark on `regression_ppm/en.ppm` with the tiny model (3 warm-ups, 12 runs) measured CPU **28.584 ms** and hybrid **27.684 ms** mean. A medium-model check (2 warm-ups, 8 runs) measured CPU **201.388 ms** and hybrid **206.885 ms**; shared-host variation and partial coverage mean CPU remains the sensible default for this device. Both modes preserved OCR output correctness. The low-level AVX-512 kernel regression also passed normally and with `PPOCR_DISABLE_AVX512=1`, verifying the AVX2 fallback. Reproduce the verification: ```powershell cmake --build build --parallel .\build\ppocr_vulkan_smoke.exe .\build\ppocr_kernel_smoke.exe $env:PPOCR_DISABLE_AVX512='1'; .\build\ppocr_kernel_smoke.exe Remove-Item Env:PPOCR_DISABLE_AVX512 ``` ### 2026-08-12: diverse-size batch and memory regression The post-tile regression was widened beyond short English lines. With recognizer batch size 4 and one recognition worker, the medium model preserved serial-equivalent OCR across a `16x16` tiny glyph, a `3000x80` wide strip, and a dense `1920x1080` UI page (124 total results). It measured **5652.199 ms serial / 5552.878 ms batch** (`1.018x`) and a subsequent two-cycle mixed-size memory soak used **391,716,864 bytes** after warm-up; cycles ended at **+6,422,528** and **+184,320 bytes**. This is bounded Windows allocator retention after the dense page, not a per-shape cache. Small-model CPU and hybrid checks likewise preserved output parity across `16x16`, `3000x80`, mixed-language, and dense-page cases. The current GPU graph is still partial, so page batching is a capacity feature rather than a universal latency win; the bounded default remains one concurrent page. The accompanying 1-warmup/7-run `en.ppm` whole-pipeline A/B (same host, default batch settings) was: | Model | CPU mean | Hybrid mean | Result | |---|---:|---:|---| | tiny | 27.867 ms | 34.334 ms | CPU faster | | small | 67.981 ms | 79.373 ms | CPU faster | | medium | 205.998 ms | 214.396 ms | CPU faster | This outcome is expected for a partial graph with synchronous round-trips; use `PPOCR_BACKEND=cpu` on this device for current end-to-end throughput. The six-image mixed-size page-batch regression still produced byte-identical OCR results versus serial execution and measured **410.192 ms serial**, **373.145 ms batched** at page parallelism 2 (**1.099x**). For a fuller pipeline A/B on the same shared Windows host (small model, `ui_dense_1920x1080.ppm`, 119 boxes, 16 CPU workers, recognizer batch 4 / parallelism 14, one warm-up and three timed runs), CPU measured **1044.384 ms mean** and hybrid **1081.426 ms mean**. This short noisy sample is recorded as an end-to-end non-regression check rather than a GPU speedup: the per-shape admission rule continued to reject GPU segments, and hybrid's small overhead remains a reason to use `PPOCR_BACKEND=cpu` when the deployment only needs the current CPU graph. After the fused Swish GPU path was added, a fresh small-model dense-page A/B (`ui_dense_1920x1080.ppm`, 119 boxes, batch 4, parallelism 14, 1 warm-up / 3 runs) measured **1170.717 ms hybrid** and **1155.998 ms CPU**. The short run is intentionally reported as a non-regression observation rather than an end-to-end GPU speed claim: only eligible BatchNorm-Swish shapes are offloaded, and scheduling/shape mix can still outweigh an individual accelerated segment. The per-shape gate continues to preserve correctness and rejects any shape whose complete transfer/dispatch/readback time loses to CPU. The CPU fallback for the exact `Sigmoid`, `Swish`, and `BatchNorm -> Swish` paths is also unrolled into four independent scalar-libm exponentials. This improves instruction-level parallelism on x86 (including AVX-512 hosts, where there is still no exact vector `exp` instruction) and ARM/NEON without adopting an approximate exponential or changing the ONNX logistic expression. The post-change tiny/small/medium ONNX Runtime text checks remained **5/5**, **3/3**, and **3/3** respectively. Fused `BatchNorm -> Swish` now also reuses the source activation when it is at its final graph use. This removes one recognizer-sized output allocation and copy on both x86 and ARM/NEON while preserving the affine, exact logistic, and multiply operation sequence per scalar. It can be A/B tested with `PPOCR_DISABLE_BN_SWISH_INPLACE=1`. On the shared host, a small-model three-crop `mixed.ppm` sample was noisy and did not show a reliable latency win (100.535 ms in-place vs 98.598 ms legacy over 15 runs), whereas medium measured 278.592 vs 282.793 ms over 10 runs; therefore this is recorded as an activation-memory/frontier optimization rather than a portable speed claim. A later experiment to fuse transformer `GEMM -> exact Swish` at row granularity was A/B tested and intentionally not retained: its benefit was not stable on small/medium at the end-to-end level. The portable GEMM and exact Swish passes therefore remain separate, avoiding an unsupported speed claim. This is the same evidence-first policy used for every hybrid GPU admission. The same shader now accepts an equal-sized batch in its Vulkan dispatch Y dimension: independent recognizer-like crops share one command-buffer reset, submission, completion wait, and H2D/D2H pair instead of paying those costs once per crop. Each compute invocation handles four contiguous FP32 values (`vec4`) and keeps only a 1--3 value scalar tail, reducing work-item scheduling overhead on wide OCR planes. The shader updates the mapped left-hand activation buffer in place, eliminating the former dedicated output buffer and its activation-sized copy: this Vulkan primitive now retains two persistent buffers (activation/result plus compact RHS), rather than three. Persistent mapped buffers now prefer coherent host-cached memory when the Vulkan driver offers it (with a portable coherent-memory fallback), grow with 1.5x headroom, and resize the activation/result independently of the compact RHS. This avoids needless buffer recreation when recognition width buckets change. Completion uses a submission fence rather than queue-wide idle, so an individual synchronous mapped readback does not also wait for unrelated queue work. The batch admission check validates every item and includes complete batched transfer/dispatch/readback time; hybrid selects it only when that time is no slower than CPU SIMD. The current Radeon 780M four-crop, 4,099-float, four-stage check measured **0.146 ms GPU versus 0.037 ms CPU** after this batch-path update, so hybrid correctly retained CPU. This is an implemented batched Vulkan executor path and building block, not a full GPU OCR claim until convolution, pooling, resize, normalization, MatMul, and CTC are device-resident. For repeatable backend A/B measurements, `ppocr_bench` accepts `PPOCR_BACKEND=cpu|hybrid|gpu` (default: `hybrid`) in addition to `PPOCR_REC_BATCH_SIZE`, `PPOCR_REC_WIDTH_BUCKET`, and `PPOCR_REC_PARALLELISM`. Its JSON output records the selected backend. `gpu` continues to fail explicitly until the full PP-OCRv6 graph is Vulkan-resident; it never labels a CPU fallback as GPU inference. The post-optimization Vulkan smoke test on 2026-08-11 passed scalar, non-workgroup-aligned, 65,537-element, dependent-chain, four-item batch, and compact NCHW suffix-broadcast batch cases on AMD Radeon 780M Graphics. The suite now also covers an immutable shared-RHS broadcast batch over two distinct activations. The broader native regression corpus also passed **11/11** cases: blank, tiny, wide/tall strips, low-contrast/noisy, rotated, dense blobs, and 1,920鑴?,080 / 2,560鑴?,440 screenshots. Text parity against ONNX Runtime passed **5/5** mixed English/Chinese samples. Exact pixel geometry is intentionally not a strict invariant because equivalent DB contour rounding can differ by one pixel; recognized text and normal-tolerance confidence parity are checked. The CPU convolution path additionally fuses ReLU into the AVX-512/AVX2 2鑴?-valid and 3鑴? convolution stores (both stride-1 and stride-2), which removes the subsequent full activation read/write pass for detector stems, the reconstruction head, and the medium detector context head. `PPOCR_DISABLE_FUSED_CONV_RELU=1` is available solely for deployment A/B measurement; it retains the former separate SIMD ReLU pass. On the two-line `en.ppm` workload, a 20-run tiny A/B was **39.385 ms fused vs 40.599 ms unfused** (about 3.0% lower mean latency). After the stride-1 extension, a 15/12/12-run A/B measured tiny at **46.692 vs 48.548 ms** (3.8% lower), small at **128.286 vs 136.103 ms** (5.7% lower), and medium at **312.317 vs 292.898 ms**. The medium result is adverse on this shared host, so the `PPOCR_DISABLE_FUSED_CONV_RELU` escape hatch remains for deployment tuning; it is reported rather than presented as a portable gain. Correctness was rechecked with AVX-512 disabled (AVX2 dispatch), five ORT text-parity samples, and the 11-case stress corpus. Adding 2鑴?-valid coverage was remeasured on the same `en.ppm` workload: 15-run tiny was **35.783 vs 37.773 ms** (5.3% lower), medium was **271.887 vs 272.238 ms** (0.1% lower), while small contained a single 738.612-ms scheduling outlier (median **91.147 vs 94.687 ms**). The median is the useful comparison for that shared-host run; all raw mean/median/p95 numbers remain available from `ppocr_bench` JSON for deployment-specific decisions. The executor now also transfers a dead activation's backing storage through internal `Reshape`, `Squeeze`, and `Unsqueeze` nodes instead of copying floats solely to change tensor metadata. It retains the existing last-use and graph output guards, so aliases never escape into a live consumer or public result. The terminal-CTC recognizer executor uses the same transfer path, covering the small/medium transformer reshape boundaries rather than only the generic executor. ORT text parity passed tiny **5/5** and small/medium **3/3** sampled English, Chinese, and mixed pages after this change. The expanded 11-image stress suite also passed. A size-changing memory soak across `en.ppm` and `ui_dense_1920x1080.ppm` remained bounded: tiny stabilized around 80 MiB private bytes (at most +3.61 MiB above its post-warmup sample) and medium stayed below its 308.93 MiB post-warmup sample on the measured cycles; small remained around 105--112 MiB (+6.34 MiB at most after warmup). Dense-page native throughput was additionally exercised with 119 small-model crops (1.422 s mean) and 122 medium-model crops (6.009 s mean), confirming the optimized recognizer path under varied crop widths and bounded batches. `PPOCR_PROFILE=1` now additionally emits a separate `ppocr CTC operator profile` for the recognizer's terminal-CTC executor. This exposes the actual small/medium transformer costs (rather than only detector `Run()` nodes) while keeping the release path free of timing/map allocations. On the 123-crop dense page, tiny-model batch-size tuning measured `rec_batch_size=4` at **323.479 ms**, versus 348.242 ms (8) and 339.535 ms (16); the default remains 4. A parallelism sweep for that same batch size measured 498.854 ms (4 workers), 392.288 ms (8), 339.808 ms (14), and 329.693 ms (24). The 24-worker result is only a small host-specific gain with a higher concurrent activation frontier, so the library retains its more memory-efficient default of 14 workers. The current transformer kernels add two further low-memory CPU optimizations. Wide AVX-512/AVX2 GEMM projections now retain four output vectors in registers across the entire K reduction, eliminating the former output load/store for every K item; this directly targets the `FusedMatMulBias` vocabulary head. The repeated LayerNorm `Pow(x,2)` nodes now use a SIMD square kernel and reuse their dead centered activation in place, avoiding both `powf` and a second `[N,T,C]` allocation. The latter is exact multiplication, not an approximate math mode. On the latest 16-worker 20-run paired `en.ppm` measurement, the current release measured **41.718 ms tiny, 93.439 ms small, and 270.362 ms medium**, against ONNX Runtime CPUExecutionProvider **144.039, 272.186, and 640.506 ms** respectively. That is **0.290x / 0.343x / 0.422x** C++/ORT latency (about **71.0% / 65.7% / 57.8% lower** C++ latency) on this shared host. Exact ORT text parity after these changes passed tiny **5/5** and small/medium **3/3** English, Chinese, and mixed-image cases. A post-change changing-size memory soak (`en.ppm` plus the 1,920鑴?,080 dense page) remained bounded. Tiny's warm baseline was **78,929,920 bytes** and its largest measured delta was **+1,032,192 bytes**; small's baseline was **107,143,168 bytes** and its largest delta **+6,766,592 bytes**; medium was at or below its **284,958,720-byte** warm baseline in both measured cycles. The five transformer LayerNorm chains in each small/medium recognizer are now recognized at model load and collapsed from `ReduceMean 閳?Sub 閳?Pow 閳?ReduceMean 閳?Add 閳?Sqrt 閳?Div 閳?Mul 閳?Add` into one affine last-axis kernel. It creates only the final `[N,T,C]` activation, eliminates centered/squared/ normalized temporary tensors, and keeps the standard `sqrt`, division, gamma, and beta operation ordering. On the same 16-worker 20-run `en.ppm` protocol, the resulting C++ measurements were **36.683 ms tiny, 75.827 ms small, and 229.388 ms medium**, while the paired ORT figures remain **144.039, 272.186, and 640.506 ms**. That is **74.5% / 72.1% / 64.2% lower latency** for the native path on this host. Post-change text parity passed tiny **5/5** and small/medium **3/3**. A fresh multi-size soak measured small from a **109,473,792-byte** warm baseline with a maximum **+4,493,312-byte** delta; medium began at **276,164,608 bytes** with a maximum **+929,792-byte** delta. `MatMul + bias + Sigmoid + Mul` gates in the small and medium transformer MLPs are also fused into a single bias-initialized MatMul followed by in-place Swish; each model matches this graph pattern twice. It removes the intermediate bias and sigmoid activations without changing FP32 operation order. Text parity after that change passed tiny **5/5** and small/medium **3/3** against ONNX Runtime. A fresh CPU-only 16-worker sanity run (5 warmups / 20 runs, subject to documented Windows scheduling variation) measured **43.778 ms tiny, 97.592 ms small, and 277.404 ms medium** on `en.ppm`; the established paired ORT baseline above remains the authoritative cross-engine comparison. The fused MLP path is now additionally specialized for its actual PP-OCRv6 rank-2 constant projection: it derives the flattened row count directly and avoids generic batch-shape/stride bookkeeping before invoking GEMM, then applies Swish in the same destination. Generic broadcasted `MatMul` retains the portable implementation, but its batch strides are now computed once per node instead of allocating them once per batch item. The latest validation covered all three models across **16 PPM inputs**: tiny and small completed all 16 in one run, while medium completed all 16 in two timeout-bounded eight-image runs. The corpus spans 16x16, 80x3000, 3000x80, noisy/low-contrast, rotated, dense, blank, Chinese/English/mixed, and 1920x1080 / 2560x1440 screenshot inputs. Exact ORT text parity was rechecked as tiny **5/5** and medium **3/3** after the specialisation; small's existing **3/3** exact gate remains passing. The AVX-512 `1x1 Conv + residual Add` path now has a real eight-output register tile rather than issuing two four-output kernels. Each 16-float input vector is loaded once and contributes to eight independent output channels; the residual is added only after each channel reduction, retaining the ONNX `Conv` then `Add` operation order. On the same 16-worker, 5-warmup/20-run medium `en.ppm` A/B, the enabled tile measured **296.941 ms** (median 295.073) versus **317.217 ms** (median 317.510) with `PPOCR_DISABLE_AVX512_POINTWISE8=1`: a **6.4% mean** reduction on this host. The current post-change CPU sanity sweep measured **42.518 ms tiny, 86.810 ms small, and 296.941 ms medium**; shared-host variance means the established paired C++/ORT table remains the authoritative engine comparison. Tiny 5/5 and medium 3/3 ORT text parity were repeated after the kernel change; the medium three-size memory soak (16x16, 3000x80, dense 1920x1080) started at **330,235,904 bytes** and ended one cycle at **+4,177,920 bytes**. Vulkan's scalar, odd-size, chain, batch, and compact-broadcast smoke cases also passed. Recognizer attention transpose now has exact rank-4 specialisations for the two PP-OCRv6 permutations `[0,2,1,3]` and `[0,1,3,2]`. The former moves its contiguous final feature vector with `memcpy`; both avoid the generic per-float four-axis divide/modulo index decoder. After the change, ORT text parity passed tiny **5/5**, small **3/3**, and medium **3/3**. On the current 16-worker 5-warmup/20-run CPU samples, tiny/small/medium measured **42.771 / 87.300 / 261.648 ms** on `en.ppm`. The current Vulkan smoke run also passed; the Radeon 780M admission samples remained GPU-slower end to end and hybrid therefore retained CPU SIMD rather than claiming an unsupported speedup. Attention Q/K/V splitting now also has a safe contiguous `Slice` fast path: when ONNX Slice selects an axis-0 unit-stride range while preserving every remaining axis, it transfers the complete `[axis-0 row]` span with one `memcpy` rather than doing per-value stride division/modulo. This exactly matches the stacked Q/K/V slices used by the small and medium recognizers; all non-contiguous or non-unit-step layouts retain the general ONNX Slice implementation. The post-change exact ORT gates passed tiny **5/5**, small **3/3**, and medium **3/3**. A wider eight-case small-model corpus was also run; six cases passed (blank, English, Chinese, mixed, line, and noisy), while the deliberately difficult dense-screen and rotated-text samples are currently recorded as known OCR-output divergences from ORT rather than excluded or misreported as a passing parity gate. The recognizer's remaining final-head layout exchange `[N,T,C] -> [N,C,T]` now also uses a direct three-axis loop instead of generic shape/stride index decoding. Together with the rank-4 attention paths, this covers every fixed transpose permutation used by the shipped tiny/small/medium recognizers while retaining the generic implementation for other ONNX graphs. The post-change parity gates again passed tiny **5/5**, small **3/3**, and medium **3/3**. The medium short benchmark remained shared-host variable (a fresh 20-run sample measured 302.714 ms), so no additional end-to-end speed claim is made from that isolated run; the change is retained for lower overhead on the repeated transpose-heavy recognizer paths. The latest backend A/B sample on the two-line tiny workload (`en.ppm`, 3 warmups / 15 runs) measured **41.533 ms hybrid** versus **42.553 ms CPU**. This small 2.4% difference is within normal shared-host variability: the Radeon admission measurements above declined all tested segments, so it must not be interpreted as a standalone GPU OCR speedup. On the 123-crop dense page (2 warmups / 7 runs), hybrid measured **311.501 ms** and CPU **314.929 ms**; both paths preserve exact CPU inference for the parts of PP-OCRv6 not yet resident on Vulkan. After the LayerNorm/transformer update, the denser 123-crop 1,920鑴?,080 workload was repeated for 3 warmups / 10 runs: **299.313 ms hybrid** (median 297.708) and **293.864 ms CPU** (median 292.125). The Radeon 780M's independently measured admissions still rejected the individual Vulkan elementwise segments (for example the compact broadcast batch was 0.106 ms GPU versus 0.010 ms CPU), so this close whole-pipeline comparison is recorded as shared-host variance rather than claimed GPU acceleration. After the Vulkan batch-buffer/fence update, the same 123-crop dense page was rechecked for 3 warmups / 10 runs: **346.120 ms hybrid** (median 342.037) and **347.319 ms CPU** (median 345.431). This near tie is shared-host variance, not GPU acceleration: all current per-shape admissions still rejected the Radeon 780M elementwise segments. The smoke suite passed scalar, odd-length, fused-chain, compact broadcast, and four-crop batch cases; its current end-to-end admission samples were 65,537-float binary **0.256 vs 0.028 ms**, four-stage chain **0.169 vs 0.032 ms**, four-crop chain **0.146 vs 0.037 ms**, and compact broadcast **0.150 vs 0.016 ms** (GPU vs CPU). `gpu_only` was also rechecked and continues to fail explicitly because the full Vulkan OCR graph has not yet been implemented. After the in-place mapped-buffer update, Vulkan smoke was rerun on 2026-08-11 and passed scalar, odd-length, fused-chain, four-crop batch, and compact broadcast batch outputs on AMD Radeon 780M Graphics. Current full-transfer admission samples remained transfer-bound: binary **0.333 vs 0.034 ms**, four-stage chain **0.250 vs 0.047 ms**, four-crop chain **0.133 vs 0.031 ms**, and compact broadcast **0.161 vs 0.015 ms** (GPU vs CPU). Accordingly hybrid still executes those isolated segments with CPU SIMD. The saved mapped-buffer memory is real, but it is not represented as a whole-OCR GPU speedup. The fused pointwise-convolution channel-bias path now uses the existing runtime-dispatched scalar-broadcast kernel per NCHW plane. This removes its scalar inner loop while preserving the exact `Conv` then `Add` order, with AVX-512, AVX2, NEON, and portable scalar coverage selected dynamically. The follow-up exact ORT gates passed tiny **5/5**, small **3/3**, and medium **3/3**. A post-change 16-worker 5-warmup/20-run CPU sanity sample measured **41.587 / 84.744 / 259.606 ms** for tiny/small/medium on `en.ppm`; Windows scheduling varies enough that the paired C++/ORT table above remains the authoritative cross-engine claim. A small dynamic-size memory soak across 16鑴?6, 3000鑴?0, and dense 1920鑴?080 inputs started at **110,268,416 bytes**; two cycles ended at **-2,564,096** and **+2,109,440 bytes** from that baseline, respectively, with no monotonic retained-shape growth. The latest exact-mode CTC confidence scan now issues four independent scalar-libm exponentials before adding them in the original lane order. This preserves the 6,906-class Softmax confidence calculation and CTC output while allowing x86 (including AVX-512-capable systems) and ARM cores to overlap exp latency. Build plus ONNX Runtime text gates passed tiny **5/5**, small **3/3** and medium **3/3** after the change. A fresh 16-worker, 5-warmup/20-run CPU sanity pass on `en.ppm` measured **36.458 / 70.745 / 218.498 ms** for tiny/small/medium. The same revision's diverse-size CPU sweep recorded: | Model | `en.ppm` (2 boxes) | `wide_strip_3000x80.ppm` | `ui_dense_1920x1080.ppm` | |---|---:|---:|---:| | tiny | 33.555 ms | 23.269 ms | 278.795 ms (123 boxes) | | small | 74.727 ms | 57.801 ms | 1136.849 ms (119 boxes) | | medium | 221.817 ms | 150.859 ms | 4626.259 ms (122 boxes) | Those short varied-size samples are intentionally recorded as local CPU capacity data, not substituted for the repeated paired C++/ORT table. The post-change small mixed-size two-cycle memory soak (16x16, 3000x80, and 1920x1080 dense UI) measured **-12,288 bytes** then **+5,222,400 bytes** from a 109,170,688-byte baseline, showing no monotonically growing allocation per input shape. Vulkan smoke also passed on AMD Radeon 780M Graphics; full transfer admission remained CPU-faster (binary **0.13716 vs 0.01282 ms**, four-stage batch **0.10178 vs 0.02728 ms**, GPU vs CPU), so hybrid correctly continued to avoid increasing latency or CPU work with an unprofitable GPU offload. For AVX-512 x86 systems, the detector's hot stride-2 3x3 convolution now forms its sixteen every-other-pixel samples from two contiguous row loads and an AVX-512 lane permute, rather than issuing a gather for each 3x3 tap. The packed-load implementation remains dynamically switchable with `PPOCR_DISABLE_AVX512_STRIDE2_PACKED=1`, preserving the prior gather kernel for any deployment where its microarchitecture wins. On this host's medium `en.ppm` 16-worker 5-warmup/20-run A/B, packed loads measured **219.768 ms** (median 216.588) versus **274.781 ms** (median 229.537) for gathers. The mean is scheduler-sensitive, but both median and mean favor the packed path. Its full exact ORT text gates passed tiny **5/5**, small **3/3**, medium **3/3**; a medium mixed-size memory cycle (16x16, 3000x80, dense 1920x1080) ended **98,304 bytes below** a 277,999,616-byte baseline. The Vulkan smoke suite also remained passing; its current full-transfer admissions still correctly select CPU for this Radeon 780M, so this CPU optimization never changes the GPU-only or hybrid correctness contract. An AVX-512 eight-output-channel 3x3 stride-one detector kernel shares every interior input vector across eight output reductions while retaining scalar border handling and the exact per-channel reduction order. A refreshed 24-run, 2-warmup CPU A/B on the current host shows the current restricted default predicate is beneficial for all three `en.ppm` models: tiny **32.010 -> 31.166 ms** (1.027x), small **66.480 -> 65.099 ms** (1.021x), and medium **207.772 -> 203.876 ms** (1.019x), four-output -> tile-8. `PPOCR_DISABLE_AVX512_CONV3X3_TILE8=1` remains the deployment fallback for machines whose AVX-512 frequency characteristics differ. The stricter shape gate still routes smaller layers, odd channel tails, AVX2, NEON, and scalar targets to their existing proven paths. The adjacent AVX-512 eight-output `1x1` projection tile was also refreshed as a negative-control A/B, rather than broadening its predicate speculatively. With the current `192x192` gate, four-output -> eight-output measured tiny **32.097 -> 34.343 ms** (regression), small **70.746 -> 66.848 ms** (win), and medium **216.021 -> 214.452 ms** (small win), using the same 2-warmup, 24-run CPU procedure. Consequently the shape-local `192x192` default and `PPOCR_DISABLE_AVX512_POINTWISE8=1` fallback remain unchanged: a single global threshold cannot improve every tier on this client CPU. This is deliberately recorded rather than claiming a universal pointwise gain. The detector FPN's common nearest-neighbour 2x resize now writes four source pixels as eight direct replicated stores before vertically copying the row. This keeps the exact NCHW output layout while removing per-pixel `std::fill_n` setup in every 2x FPN plane; uncommon integer resize factors retain the existing general replication path. A fresh default medium 16-worker 5-warmup/20-run `en.ppm` sample measured **209.972 ms** (median 210.745 ms). Exact ORT text regression again passed tiny **5/5**, small **3/3**, medium **3/3**, and Vulkan smoke stayed passing. A two-cycle tiny mixed-size soak across 16x16, 3000x80 and 2560x1440 inputs stayed bounded at **+1,150,976** then **+2,654,208 bytes** relative to its 82,276,352-byte post-warmup baseline; this represents Windows heap retention, not a per-inference activation leak. Recognition batch construction no longer allocates and copies a temporary `vector` for every batch. Same-width buckets now retain contiguous crop bounds and result indices once, and `RecInputBatch` consumes a direct span. This removes hundreds of small allocations/copies on dense pages while preserving crop order, width bucketing, bounded batch size, and concurrent recognizer behavior. The medium 122-box dense 1920x1080 smoke workload completed correctly after this change (a short 2-warmup/5-run local sample was **5541.268 ms**, subject to the documented scheduler/cache variability). Exact ORT text parity remained tiny **5/5**, small **3/3**, medium **3/3**; Vulkan scalar/chain/batch/broadcast smoke passed. A medium mixed-size 16x16/3000x80/1920x1080 memory cycle completed without a failure; its post-warmup private-byte result is retained as capacity data rather than claimed as a leak-free steady-state proof because the Windows allocator kept 10,149,888 bytes after this deliberately large dense workload. The Vulkan binary runtime now recognizes immutable ONNX initializer operands. When hybrid dispatches an eligible elementwise node, its RHS remains in the persistent mapped Vulkan buffer across later activations of the same model, avoiding a repeated host copy while retaining a separate activation/result buffer. Dynamic RHS tensors always copy, and the existing full end-to-end admission policy is unchanged: a cached constant does not qualify GPU unless the whole H2D/dispatch/D2H segment is no slower than CPU. The smoke suite now checks two distinct activations against a cached 65,537-float immutable RHS; it passed on AMD Radeon 780M Graphics along with scalar, chain, batch and broadcast cases. Current admissions remain CPU-faster on this UMA host, so hybrid correctly preserves CPU SIMD rather than claiming a GPU OCR speedup. The default CPU executor's tiny **5/5**, small **3/3**, and medium **3/3** ORT text gates were rerun after the backend API change. ### Batched Vulkan residual pointwise block The Vulkan backend now has a verified batched `1x1 Conv + residual Add` primitive, with a `Conv + Add + ReLU` variant. It maps equal-width recognition crops to Vulkan's Y dimension, keeps learned `[M,C]` weights and bias cached when they are ONNX initializers, and uploads the residual into a separate read-only buffer so the shader never aliases an input with its output. This is also wired into the existing fused CPU graph nodes in `hybrid` mode, while the shape-specific admission test includes both live activation uploads, parameter transfer, dispatch, readback, and an FP32 CPU comparison. On 2026-08-11, the AMD Radeon 780M smoke test validated normal and ReLU residual pointwise results for four NCHW batches, including a 257-element non-vector-aligned spatial plane. Its full-transfer admission measurement for the `N=4, C=24, M=40, plane=257` `Conv+Add+ReLU` case was **19.1844 ms GPU** versus **0.0326 ms CPU**, so hybrid correctly rejected the GPU segment. The normal pointwise case was likewise **19.1564 ms GPU** versus **0.03072 ms CPU**. These figures are deliberately recorded as a negative end-to-end result: the new GPU kernel is numerically covered and batch-capable, but standalone host-visible uploads/readback make it inappropriate on this UMA device. `gpu_only` remains explicitly unavailable until the complete OCR graph can execute device-resident without CPU fallbacks. After this change, the tiny five-image C++/ONNX Runtime text regression again passed **5/5**. The six-image mixed-size page-batch smoke (151 detected text regions, page parallelism two) completed in **381.767 ms** versus **408.840 ms** serial, a **1.071x** scheduler speedup on the local shared host. The residual-ReLU AVX-512 eight-output entry point was also exercised on the same host. Its real eight-live-accumulator version passed the five-image text gate, but did not demonstrate a stable end-to-end win: `mixed.ppm` small was **86.242 ms** versus **82.700 ms** with the default four-output tile, and the medium results varied between runs. It therefore continues to delegate to the proven four-output residual-ReLU tile instead of becoming an unvalidated default. This keeps AVX-512 dynamic and safe while avoiding a register-pressure regression on both tiny and small workloads. The current medium mixed-size memory soak (`16x16`, `3000x80`, `80x3000`, and the 151-region `1920x1080` UI page; two cycles) established a post-warm-up private-byte baseline of **347,013,120 bytes**. The next cycles were **+303,104 bytes** and **+3,276,800 bytes**. This bounded allocator-capacity movement is recorded as a Windows high-water observation rather than a general leak proof; importantly it did not grow once per dynamic shape or per crop batch. ### 2026-08-11: GPU residual-vector and non-ReLU batch fusion follow-up The residual pointwise shader now vectorizes its common four-contiguous-spatial lane case as well as the non-residual pointwise shader. It performs `1x1 Conv + residual Add (+ ReLU)` in registers and writes one final NCHW result, retaining a scalar tail only at plane boundaries and odd widths. The graph loader also recognizes the strict dynamic `1x1 Conv -> equal-shape Add` form without a following ReLU as `FusedPointwiseConvAdd`. This matters for the medium recognizer: a CPU profile of `mixed.ppm` observed fused non-ReLU shortcut nodes such as `Conv.9_add`, `Conv.20_add`, `Conv.50_add`, and `Conv.31_add`, so they now avoid a materialized Conv result plus a separate Add pass on both CPU and eligible hybrid paths. Initializer/broadcast Adds remain excluded, preserving ONNX bias semantics. The rebuilt Vulkan smoke suite passed all binary, broadcast, affine, normal pointwise, residual pointwise, and residual-ReLU numerical checks on AMD Radeon 780M Graphics. Its latest complete-transfer measurements were **21.0106 ms GPU vs 0.02642 ms CPU** for `N=4,C=24,M=40,plane=257` pointwise Conv and **14.2844 ms GPU vs 0.03048 ms CPU** for residual+ReLU; hybrid therefore rejected both exactly as designed. The separate tiny five-image C++/ORT text gate passed **5/5**, and the six-page mixed-size batch smoke produced identical nested results in **81.021 ms** versus **122.164 ms** serial (**1.508x**). A short `mixed.ppm` end-to-end check measured **34.379 ms CPU** versus **40.314 ms hybrid** (one warm-up, three runs), so current deployment guidance remains CPU for this UMA GPU; these GPU additions are correct, batched building blocks rather than an unsupported full-GPU claim. ### 2026-08-11: Vulkan mapped-memory policy revalidation The persistent Vulkan buffers now explicitly score host-coherent memory types for this synchronous hybrid runtime: `HOST_CACHED` coherent memory is selected first, then host-visible device-local coherent memory, then a portable coherent fallback. This is not merely a theoretical ordering. An A/B on the Radeon 780M showed that preferring host-visible `DEVICE_LOCAL` for every mapped buffer made the complete binary segment about **10.65 ms GPU vs 0.0195 ms CPU**, while the cached mapped policy restored it to **0.137 ms GPU vs 0.015 ms CPU**. The device-local mapping was therefore rejected; shader-side device locality does not compensate for slower CPU copies in an H2D/dispatch/D2H hybrid segment. With the restored policy, the refreshed smoke suite passed every numerical case. Its four-crop affine-Swish segment was **0.13832 ms GPU vs 0.29722 ms CPU**, so hybrid selects Vulkan for that shape; the pointwise and residual pointwise probes remained transfer-bound (**20.6638 vs 0.0269 ms**, and **14.3158 vs 0.03158 ms**, respectively) and are correctly retained on CPU. This is the required behavior for `hybrid`: prefer GPU only when the measured full segment is no slower, while avoiding a silent fallback in `gpu_only`. Post-change correctness evidence includes small and medium C++/ONNX Runtime text parity on `en.ppm` (**1/1** each) and a medium mixed-size memory soak over `16x16`, `mixed`, and the `1920x1080` 151-region UI image. Its post-warm-up private-byte baseline was **361,832,448** and the next cycle was **-7,499,776 bytes** from that baseline, consistent with a bounded allocator high-water rather than retained dynamic workspaces. A six-page tiny batch regression still returned byte-identical nested results; the shared-host run measured **126.196 ms serial / 108.076 ms batch (1.168x)**. Timing varies with contention, so this documents correctness and a measured throughput win rather than a portable promise. ### 2026-08-11: batched GPU Conv+Swish fusion ### 2026-08-12: tiled batched Vulkan pointwise convolution The batched Vulkan `1x1` convolution shader now has a workgroup-tiled path for `C <= 1024`. One 256-thread workgroup owns four output channels and a 256-pixel spatial tile, cooperatively stages the four immutable `[C]` weight rows in shared memory, then produces four contiguous FP32 values per lane. This removes the former per-`vec4` weight streaming in the common NCHW pointwise case. The same path covers Conv, Conv+ReLU, Conv+Swish, and the equal-shaped residual variants; odd plane tails still use an exact scalar write and wider channel counts retain the established generic shader path. This is a real GPU-kernel and batch-throughput improvement, not an invented end-to-end GPU win. On the local AMD Radeon 780M, the verified four-crop `N=4,C=24,M=40,plane=257` smoke measurement improved from the earlier multi-millisecond pointwise dispatches to **0.16412 ms GPU** for Conv and **0.15202 ms GPU** for Conv+Add+Swish. CPU SIMD still measured **0.02170 ms** and **0.09334 ms**, respectively, so the complete H2D/dispatch/D2H admission rule correctly retains CPU on this UMA host. The smoke suite validates all normal, ReLU, Swish, residual, and non-vector-aligned outputs before timing. The rebuilt tiny four-page scheduler check (`en`, `line_en`, `mixed`, `zh`; batch size 4, recognizer parallelism 1, page parallelism 2) remained output identical after the shader change. This run measured **128.114 ms serial / 88.105 ms batch (1.454x)** on CPU and **125.542 ms / 77.657 ms (1.617x)** in hybrid mode. These short shared-host samples are scheduler evidence, not a replacement for the paired ONNX Runtime table above. `gpu_only` remains explicitly unavailable until the complete PP-OCRv6 graph is device-resident. For a post-change whole-pipeline sanity A/B on `en.ppm` (16 CPU workers, batch size 4, recognizer parallelism 1, three warm-ups and ten timed runs), hybrid measured **34.387 / 72.977 / 216.534 ms** for tiny/small/medium. The separately sampled CPU tiny run was **36.125 ms**; due shared-host noise and partial-graph selection these are not substituted for the longer paired CPU-versus-ORT results. The relevant GPU proof is the verified tiled primitive and its conservative per-shape full-transfer admission, not an unsupported claim that all three models now execute fully on the Radeon GPU. The Vulkan pointwise kernel now also supports the graph-fused PP-OCR pattern `1x1 Conv -> Sigmoid -> Mul` as an exact FP32 `1x1 Conv + Swish` operation. Four equal-width NCHW crops share one submission; learned `[M,C]` weights and `[M]` bias remain compact and can be retained in the persistent parameter buffers. This removes the CPU-side full-output Swish traversal when a shape is admitted to Vulkan, without changing the portable AVX-512/AVX2/NEON fallback. The smoke test verifies the fused output at every element for a four-crop, non-vector-aligned `N=4, C=24, M=40, plane=257` workload. On the local Radeon 780M its full synchronous H2D/dispatch/D2H measurement was **12.2472 ms GPU versus 0.13918 ms CPU**, so `hybrid` correctly retains CPU SIMD for that shape. This is nevertheless a material batch-capable GPU graph building block: a device/shape is allowed to select it only after this complete measurement and numerical validation; it is not represented as an end-to-end GPU OCR win on the present UMA system. The expanded tiny C++/ONNX Runtime text gate passed **5/5** (`en`, English line, Chinese line, mixed, Chinese page), and the six-page batch regression preserved identical OCR results at **123.546 ms serial / 86.991 ms batch (1.420x)** on this run. ### 2026-08-11: shortcut Swish fusion and broader regression pass Small and medium recognizers contain an inverted-residual suffix `BatchNorm -> Swish -> Add(shortcut)`. The loader now recognizes this after the first fusion sweep and emits one `FusedBatchNormSwishAdd` node. Its CPU kernel produces the affine, exact logistic Swish and equal-shaped residual Add in one traversal, reusing the dying BatchNorm input allocation. This removes the intermediate Swish feature map and the separate Add pass. It is protected by `PPOCR_DISABLE_BN_SWISH_ADD_FUSION=1` for A/B work; a short medium `mixed.ppm` CPU check (one warm-up / three runs) measured **242.378 ms** fused versus **260.514 ms** unfused (**7.0% lower latency**), with expected shared host variance. Vulkan now exposes the matching batched `channel-affine + Swish + Add` primitive. Its smoke test validates every output of four `8192`-float NCHW crops. On the Radeon 780M, complete transfer timing was **0.14988 ms GPU vs 0.14072 ms CPU**, so hybrid correctly chose CPU for that exact case. The graph path has the same cached no-slower admission rule: it may use Vulkan only when the full H2D/dispatch/D2H measurement and FP32 validation win; otherwise it uses the CPU fused path and does not add GPU overhead. Current post-change evidence: tiny C++/ORT text parity remained **5/5** over English, Chinese and mixed samples; small and medium each passed an independent ORT text check on `en.ppm` (**1/1** each). A six-page varied-content tiny batch regression remained result-identical at **126.926 ms serial / 96.686 ms batch (1.313x)**. A medium memory soak spanning `16x16`, mixed text, and a dense `1920x1080` page finished one cycle **7,614,464 bytes below** its 376,975,360-byte post-warm-up baseline, consistent with bounded workspace reuse. ### 2026-08-12: batched Vulkan HardSwish coverage The graph-fused detector pattern `1x1 Conv -> HardSigmoid -> Mul` now reaches the Vulkan pointwise kernel as one exact `Conv+HardSwish` dispatch. The shader receives the ONNX `alpha` and `beta` as raw FP32 push constants and writes `x * clamp(alpha*x + beta, 0, 1)` directly to the final NCHW output. It uses the same four-crop Y-dimension batch mapping, shared-weight tile, odd-plane tail handling, persistent parameter cache, and complete H2D/dispatch/D2H admission policy as the other pointwise variants. CPU retains its AVX-512/AVX2/NEON/scalar exact fallback. The post-change Radeon 780M smoke suite validated every value of an odd-plane `N=4, C=24, M=40, plane=257` batch. The full synchronous admission sample was **0.15912 ms GPU / 0.02794 ms CPU**, so this UMA host correctly keeps that segment on CPU; no end-to-end GPU OCR speedup is claimed. A seven-image tiny CPU-versus-hybrid output gate (`en`, mixed, CJK, tiny, wide, low-contrast, blank) passed **7/7** with identical decoded output. With recognizer batch size 4, recognizer parallelism 1, and page parallelism 2, the seven-image hybrid batch gate preserved output identity and measured **337.565 ms serial / 234.153 ms page-batch (1.442x)**. This is scheduler throughput evidence on the local machine, not a general GPU performance promise. ### 2026-08-12: three-size CPU/hybrid and memory revalidation The current build was retested after the Vulkan HardSwish integration with the official tiny, small, and medium model pairs. The seven-image C++/ONNX Runtime text gate passed **7/7 for every model** (English, mixed Latin, CJK, `16x16`, `3000x80`, low-contrast, and blank). The broader tiny stress corpus also passed **11/11**, including rotated text, dense blobs, and `1920x1080`/`2560x1440` screenshots. On `en.ppm` (2 warm-ups, 10 timed runs, 16 CPU workers, recognizer batch 4, one recognizer worker), the local CPU/hybrid recheck was: | Model | CPU mean | Hybrid mean | CPU / Hybrid decoded output | |---|---:|---:|---| | tiny | 35.164 ms | 34.684 ms | identical | | small | 71.442 ms | 71.286 ms | identical | | medium | 205.222 ms | 225.598 ms | identical | Hybrid is therefore not described as a universal win: each Vulkan segment is still admitted independently only when its verified complete transfer, dispatch, and readback time is no slower than the CPU SIMD equivalent. The medium mixed-size memory soak (`16x16`, mixed text, dense `1920x1080`; two cycles) used 367,849,472 private bytes after warm-up and ended at +4,378,624 bytes, consistent with bounded allocator high-water behavior. ### 2026-08-11: batched residual Conv+Swish GPU fusion The Vulkan batch kernel now also supports the full graph-proven recognizer suffix `1x1 Conv -> Add(shortcut) -> Sigmoid -> Mul` as mode 9: `1x1 Conv + residual Add + exact Swish`. Equal-width OCR crops share its Y dispatch dimension; the learned `[M,C]` weights and `[M]` bias remain compact and cacheable, and the shortcut is read once before the final device write. The CPU fallback adds `PointwiseConvAddSwish`, keeping the Conv+shortcut in the existing AVX-512/AVX2/NEON fused path and overwriting that single output allocation with exact Swish. Set `PPOCR_DISABLE_POINTWISE_ADD_SWISH_FUSION=1` to restore the unfused ONNX sequence for an A/B comparison. The extended Vulkan smoke test validates every value of an odd-plane four-crop `N=4,C=24,M=40,plane=257` batch. On the local Radeon 780M, its full H2D/dispatch/D2H comparison measured **12.3933 ms GPU vs 0.09822 ms CPU**, so hybrid correctly rejected it. The currently profiled small PP-OCRv6 export does not emit this exact four-node suffix, so no end-to-end OCR speed claim is made for the new matcher yet; it is retained as a verified batch-capable building block for compatible PP-OCRv6 exports, not an unsupported GPU speed claim on the present UMA host. ### Tiny / small / medium comparison The same complete two-line OCR pipeline was also measured with the official three PP-OCRv6 model sizes on this 16-thread AVX-512-capable host. Model load is excluded from steady-state latency. The native and ONNX Runtime commands were run separately with the same PPM input and matching dictionaries. The following exact-Erf run is the current repeated baseline; `PPOCR_APPROX_GELU` was unset for every native measurement. | Model | Native runs / warmup | Pure C++ mean | ORT mean | Native / ORT | Native FPS | ORT FPS | |---|---:|---:|---:|---:|---:|---:| | tiny | 20 / 5 | 33.322 ms | 120.106 ms | 0.277x | 30.010 | 8.326 | | small | 20 / 5 | 83.639 ms | 184.190 ms | 0.454x | 11.956 | 5.429 | | medium | 20 / 5 | 281.236 ms | 428.584 ms | 0.656x | 3.556 | 2.333 | This current default-exact run is a same-host comparison with matching model files and image. It shows lower C++ latency for tiny, small, and medium (72.3%, 54.6%, and 34.4% respectively). The commands were run as paired engine measurements, but Windows scheduling still affects individual samples; the complete JSON output should be retained when refreshing capacity numbers. The earlier approximate-GELU deployment experiment is retained below as a separate, opt-in result rather than being conflated with default ONNX-equivalent execution: | Model | Native runs / warmup | Pure C++ mean | ORT mean | Native / ORT | Native FPS | ORT FPS | |---|---:|---:|---:|---:|---:|---:| | tiny | 10 / 3 | 34.125 ms | 137.535 ms | 0.248x | 29.304 | 7.271 | | small | 8 / 3 | 95.049 ms | 226.928 ms | 0.419x | 10.521 | 4.407 | | medium | 8 / 3 | 257.848 ms | 457.041 ms | 0.564x | 3.878 | 2.188 | All values are from the same 2026-08-11 host and PPM input. The native run used the persistent 16-worker executor; ONNX Runtime used `CPUExecutionProvider` with the matching 16-thread budget. The refreshed measurements show lower mean latency for all three model sizes on this host. The table below uses the validated optional approximate-GELU deployment setting; the default exact-Erf paired comparison is documented above. The opt-in approximation passed the strict 7-image text corpus and the 11-image size/diversity stress corpus; retain the exact default for applications that require strict numerical ONNX equivalence. The specialized stride-2 3x3 gather path remains material for medium: disabling it measured 2646.179 ms versus 1923.504 ms when enabled with one requested worker. These are local measurements, not a portable performance guarantee. All three model pairs produce the expected two recognized lines in the native smoke test. The strict 7-image text-parity and 11-image size/diversity stress gates below use tiny because their known-text expectations correspond to its dictionary; they exercise input dimensions from 16x16 to 2560x1440. The latest default-exact verification also forced the dynamic AVX2 fallback with `PPOCR_DISABLE_AVX512=1`: tiny/small/medium completed at 43.170/113.518/ 408.928 ms in a short 8-run sweep. This confirms that AVX-512 remains a runtime-selected acceleration, not a binary deployment requirement; ARM/NEON and scalar paths retain the same portable graph implementation. ### Batched constant-weight MatMul fusion Recognizer attention projections commonly have a batched activation and one rank-2 immutable weight tensor. The executor now flattens those independent batch dimensions into one tall GEMM call, avoiding repeated per-batch GEMM dispatch and allowing the SIMD/NEON GEMM to use its normal row scheduling over the whole recognition batch. No weights are repacked or duplicated, and every output row keeps its original increasing-`K` FP32 accumulation order. This is enabled by default on x86/AVX-512, x86/AVX2, ARM/NEON, and scalar builds; `PPOCR_DISABLE_MATMUL_BATCH_FOLD=1` provides an A/B fallback. On the local 16-worker AVX-512 host, exact-GELU `mixed.ppm` measurements with three recognized crops, batch size four, recognizer parallelism one, three warmups and fifteen timed runs showed the direct batch benefit for small: **137.925 ms mean** with the fold versus **195.572 ms** with the fallback (29.5% lower latency). A separate default scheduler check (batch four, parallelism fourteen, 3 warmups / 12 runs) measured **41.663 ms tiny**, **93.915 ms small**, and **340.519 ms medium** on the same input. These samples are host-specific and the medium workload remains dominated by convolution, but they confirm that all three model sizes retain the same correct batched path. Exact ONNX Runtime text parity remained tiny **5/5**, small **3/3**, and medium **3/3** after the change. The same fallback path now fuses equal-shape `1x1 Conv -> Add` residual projections with an AVX2 four-output register tile rather than materializing the Conv result before a separate Add. A fresh forced-AVX2 3-warmup/10-run sanity sweep measured tiny/small/medium at 42.446/122.677/434.793 ms on the shared host; these are coverage and stability samples rather than a paired ORT comparison. The strict seven-image text gate passed 7/7 with AVX-512 disabled. ARM/NEON has matching four-output fused residual code; it is selected only when the target compiler exposes NEON, otherwise scalar remains valid. For AVX-512, large 1x1 projections now use an eight-output register tile (with four-output/tail handling retained for smaller shapes). It reuses each loaded input vector across eight output channels and preserves each output channel's reduction order. The exact-GELU text gate passed 7/7 and the 11-image size/diversity gate passed after this change; the default exact three-model table above is the associated repeated benchmark. A 30-run A/B on the same 16-worker host selected this default for the larger models: small measured 106.047 ms with the eight-output tile versus 109.533 ms with `PPOCR_DISABLE_AVX512_POINTWISE8=1`; medium measured 301.179 ms versus 313.123 ms. Individual Windows samples remain scheduling-sensitive, but both the median and mean favoured the eight-output implementation in that repeated comparison. The same eight-output technique is implemented for the already fused `1x1 Conv+ReLU` path, including its exact scalar tail and runtime ISA guards. It passes the strict text corpus when enabled, but repeated small/medium end-to-end A/B samples were mixed (and the 30-run medium sample favoured the default four-output path: 257.914 ms versus 268.169 ms). It is therefore kept behind `PPOCR_ENABLE_AVX512_POINTWISE_RELU8=1`, rather than silently trading stable default latency for a workload-specific result. ### Post-fusion comparison refresh The latest post-fusion same-host refresh (exact Erf-GELU, 16 requested workers, 5 warmups and 20 measured runs per engine) is recorded below. It includes the new equal-shape `1x1 Conv -> Add` fusion; operator profiling confirmed fused residual projections in the medium graph (for example `Conv.9_add`, `Conv.12_add`, `Conv.20_add`, `Conv.50_add`, and `Conv.53_add`). As with the earlier table, these are sequential paired measurements on a shared Windows machine, so they document the current build rather than a portable guarantee. | Model | Pure C++ mean | ORT mean | C++ / ORT | Pure C++ FPS | ORT FPS | |---|---:|---:|---:|---:|---:| | tiny | 43.072 ms | 182.025 ms | 0.237x | 23.217 | 5.494 | | small | 109.535 ms | 244.420 ms | 0.448x | 9.130 | 4.091 | | medium | 359.697 ms | 642.622 ms | 0.560x | 2.780 | 1.556 | This refresh measured native latency lower by 76.3%, 55.2%, and 44.0% for tiny, small, and medium respectively. The observed C++ ranges were 40.202--48.375 ms, 99.140--127.691 ms, and 284.395--396.259 ms; ORT also showed large host-scheduling variance, especially medium (427.880--1285.338 ms). Keep the original repeated table above for historical comparison and rerun the paired procedure on the deployment CPU before capacity planning. The subsequent gate-fusion validation found 13/13, 13/13, and 5/5 fused HardSigmoid gates in tiny, small, and medium profile runs respectively. Default and forced-AVX2 strict text parity both passed 7/7, while the 11-image stress set still passed 11/11. A short shared-host 3-warmup/10-run sanity sweep was 43.159 ms (tiny), 100.075 ms (small), and 416.331 ms (medium); the wide medium p95 range reflects host scheduling, so these values are deliberately not presented as a new ONNX Runtime comparison. The executor now additionally treats a last-use standalone `HardSigmoid` as an in-place operation. This applies the same exact AVX-512/AVX2/NEON SIMD gate without allocating a second activation tensor; it is especially relevant to dynamic image sizes where keeping the graph frontier small lowers transient memory. The current strict text and 11-image stress checks passed after this lifetime optimization. The detector reconstruction head now folds its terminal `ConvTranspose(2x2, stride 2) -> Add([1,C,1,1])` pairs into the transposed-convolution bias during model load. This removes both final feature-map Add traversals for tiny, small, and medium (confirmed as `ConvTranspose.0_bias` and `ConvTranspose.2_bias` in the medium operator profile) while retaining exact output and all ISA paths. The strict seven-image comparison and 11-image dimension/diversity stress suite remained fully passing after the change. The recognizer's direct `Conv -> Add([1,C,1,1]) -> ReLU` exports are now folded at load time into the existing Conv+ReLU kernels. This adds the previously missing no-Identity form of channel-bias folding, so small/medium stem projections avoid a broadcast Add pass and its transient result. The same matcher also recognizes canonical `Conv -> HardSigmoid -> Mul` gates and maps them to the existing exact AVX-512/AVX2/NEON HardSwish kernel when encountered. The default and forced-AVX2 tiny text-parity checks both passed 5/5 images, and the 11-image 16x16-to-2560x1440 stress suite passed 11/11 after the change. ### Current optimization validation and comparison refresh The current build adds the post-Conv Swish graph fusion described above. The official medium model profiled here does not contain a matching `Conv -> Sigmoid -> Mul` sequence after affine folding, so this generic fusion does not contribute to the following current-model timing. The strict tiny text-parity suite completed 5/5 images against ONNX Runtime after the change. The benchmark below was then run on the same shared Windows host with `PPOCR_BENCH_THREADS=16`, AVX-512 enabled dynamically, exact-Erf GELU, five warmups and 20 measured iterations. It is recorded here as a reproducible current-build comparison, not a portable capacity guarantee. | Model | Pure C++ mean | ORT mean | C++ / ORT | Pure C++ FPS | ORT FPS | |---|---:|---:|---:|---:|---:| | tiny | 40.159 ms | 140.930 ms | 0.285x | 24.901 | 7.096 | | small | 95.946 ms | 234.128 ms | 0.410x | 10.422 | 4.271 | | medium | 342.556 ms | 599.235 ms | 0.572x | 2.919 | 1.669 | For this run, native C++ latency was 71.5%, 59.0%, and 42.8% lower than ONNX Runtime CPUExecutionProvider for tiny, small, and medium. Native throughput was correspondingly 251.0%, 144.0%, and 74.9% higher. The native/ORT median/p95 pairs were 39.217/44.732 vs. 142.425/199.114 ms (tiny), 94.667/103.429 vs. 234.956/276.820 ms (small), and 316.130/451.931 vs. 576.425/767.443 ms (medium). The shared Windows host is scheduling-sensitive, so retain the ranges and rerun this paired procedure on a deployment CPU before capacity planning. The existing tiny/small/medium repeated exact table remains the longer-run model-size baseline. ### Dense-text stress result The small-image result above is not representative of a screen with hundreds of detected text regions. The native path groups equally sized crops into bounded batches and schedules up to fourteen independent batches concurrently; each batch retains its output order and uses an isolated execution workspace. When multiple batches concurrently reach a CPU-parallel operator, one uses the persistent worker set while contenders execute their independent channel split locally. This prevents shared-pool serialization and nested-worker contention. For the PP-OCR recognizer's terminal CTC head it also reduces logits directly to argmax plus selected-character probabilities, avoiding an otherwise unused `[batch,time,6906]` Softmax tensor. Last-use elementwise activations (GELU, BatchNorm+GELU, ReLU, h-swish and scale/shift) are updated in-place, eliminating transient activation copies. The BatchNorm+GELU path now evaluates the exact-Erf GELU in the same pass as its affine transform, removing a further full activation read/write in the default exact mode. In the explicitly enabled approximate-GELU deployment mode, the same affine result routes through AVX-512/AVX2 scale-shift plus vector GELU; the corpus gates above validate that optional path. Recognizer `Conv -> BatchNorm` inference pairs are folded into immutable convolution weights and biases during model loading, removing the BatchNorm activation pass without changing the deployed C++ runtime dependencies. The detector FPN `2x2`, stride-2 transposed-convolution blocks have dedicated AVX2 and AVX-512 implementations selected through the existing runtime ISA dispatch. They vectorize independent input pixels while keeping the same output/input-channel accumulation order; scalar and ARM builds retain the portable implementation. In a direct dense-page operator profile, the two blocks totalled 3.135 ms with AVX versus 3.597 ms scalar (about 12.8% for those kernels). The neighbouring detector reconstruction `2x2` valid convolutions now also have dedicated AVX-512 and AVX2 kernels. They stream the four contiguous input rows required by each output vector and avoid generic padding/interior checks; ARM and scalar builds retain the portable kernel. On the current host this changed the complete two-line small model from 110.364 ms to 98.112 ms and the medium model from 352.690 ms to 338.791 ms in the same short-run configuration (about 11.1% and 3.9% respectively), while dense tiny recognition measured 243.949 ms for 123 boxes. The larger stride-one `3x3` detector layers use an AVX-512 four-output-channel kernel as well. Each input vector is loaded once and accumulated into four independent channel outputs, retaining the original channel/kernel accumulation order. On the medium profile this reduced `Conv.121` from 26.809 ms to 5.910 ms and `Conv.3` from 20.759 ms to 12.379 ms; the complete short-run medium OCR measurement improved from 338.791 ms to 291.049 ms. AVX2, NEON, and scalar paths remain available through runtime ISA dispatch. The same four-output `3x3` algorithm is now implemented for AVX2 as well. An explicit `PPOCR_DISABLE_AVX512=1` run exercised that fallback on the present machine: medium completed in 404.166 ms and the strict tiny text corpus passed 7/7, confirming that advanced instructions remain dynamically optional rather than a deployment requirement. ARM/NEON retains its portable vector path. The scheduler now divides these fused `3x3` layers by four-channel groups, instead of splitting a group between CPU workers. This preserves input-vector reuse even for channel-parallel medium layers and safely sends only an incomplete last group to the scalar tail. The resulting AVX-512 medium run measured 279.329 ms (3.580 FPS); strict text and the 11-image dimension/diversity corpus both remained fully passing. The same AVX-512 four-output strategy now covers the medium detector's square stride-one `5x5` and `7x7` context layers. It cut the profiled `Conv.111` 7x7 from 8.472 ms to 3.898 ms and `Conv.114` 5x5 from 3.973 ms to 1.938 ms; the complete medium result improved from 279.329 ms to 257.848 ms (3.878 FPS). Unsupported ISAs and non-square/asymmetric kernels continue through the existing AVX2/NEON/scalar paths. Depthwise dispatch now routes square stride-one `7x7` and `9x9` kernels to the existing AVX-512/AVX2 vector kernel too. This covers medium `Conv.64` (256 channels, 9x9): its local operator profile fell from 8.080 ms to 4.845 ms. The native benchmark's current medium distribution remains sensitive to shared host scheduling, so the paired end-to-end table is retained as the comparable model-size result rather than replacing it with an unpaired one-off sample. For `1x1 Conv -> ReLU` sequences, the AVX-512 path now applies the clamp before storing each register-tiled output vector. This removes the separate activation read/write for the fused detector projections while leaving AVX2, NEON, and scalar execution on the existing safe Conv-then-ReLU path. The strict text and 11-image dimension/diversity gates passed after the change. On this shared benchmark host the short medium samples remained scheduler-noisy, so no new end-to-end speed claim is made without a paired repeated run. The default exact-Erf GELU path is also parallelized across independent NCHW planes through the persistent executor. BatchNorm+GELU now precomputes its per-channel affine coefficients once and submits plane-local exact evaluation without allocating another activation. On medium profiling this changed the 13 fused GELU calls from 26.743 ms to 7.297 ms while retaining default ONNX Erf semantics; the strict text and 11-image stress gates passed with `PPOCR_APPROX_GELU` unset. The optional approximate mode remains available for deployment-specific validation, not as a correctness requirement. The recognizer terminal CTC Top-1 reduction now parallelizes only across independent batch sequences. Each sequence retains its own blank/repeated-token state, while dense-page batches can distribute vocabulary scans and the selected character confidence softmax over the persistent worker set. Exact decoded-text and the 11-image dimension/diversity gates passed in default exact-GELU mode; the shared-host dense benchmark remains variable, so its paired table remains the authoritative capacity-planning reference. CTC's 6,906-way per-step ArgMax also uses dynamically selected AVX-512 or AVX2 vector reductions. It explicitly scans the non-vector-width tail before locating the first winning class, preserving the scalar tie rule and ensuring the final vocabulary entries (including the space class) remain observable. Both default AVX-512 and forced `PPOCR_DISABLE_AVX512=1` AVX2 runs passed the strict 7-image text corpus; the current exact-GELU dense 123-box run measured 345.415 ms (median 343.977 ms, p95 359.279 ms), subject to the documented shared-host scheduling variation. AVX-512 four-output fusion now also covers stride-one asymmetric `1x3/1x5/1x7` and `3x1/5x1/7x1` detector convolutions. It vectorizes the contiguous spatial axis, preserves scalar border accumulation, and assigns four-channel groups to workers so input loads are reused. Medium `Conv.112` (7x1) fell from a recent 3.363 ms profile to 1.211 ms. Default exact-GELU strict-text and 11-image dimension/diversity gates remained passing; standalone medium timing is still reported with the documented shared-host variance rather than as a new paired ORT claim. The DB connected-component postprocessor represents threshold and visited state in one byte per probability-map pixel (`0` background, `1` pending, `2` visited), instead of two full byte maps. This halves that postprocessor workspace while leaving thresholding, traversal order, scores, boxes, and reading order unchanged. The detector terminal `Sigmoid` is also executed in-place whenever its input has no remaining consumers. It reuses the final DB-logit allocation as the probability map, eliminating a peak activation allocation and copy while preserving the exact scalar sigmoid expression. Detector FPN nearest-neighbour integer upscales are implemented as direct NCHW row replication, and recognizer inverse-Transpose pairs plus Conv channel bias broadcasts are removed at model load. These changes eliminate dynamic activation copies without relaxing the exact-GELU accuracy path. The executor also prunes original constants made unreachable by load-time Conv/BatchNorm folding. This applies to tiny, small, and medium without changing runtime graph inputs. A fresh two-cycle mixed-size medium soak (16x16, 3000x80, noisy scene and 1920x1080 UI page) used a 146.94 MiB post-warmup baseline; the cycles were 147.21 and 148.18 MiB (+0.27 and +1.24 MiB). The variation is Windows heap retention rather than monotonic per-shape growth; longer soak runs should be used for release qualification. The same mixed-size medium sequence in default exact-GELU mode used a 147.68 MiB post-warmup baseline; two cycles ended at 146.48 MiB and 152.16 MiB (-1.20 / +4.48 MiB). This is bounded allocator retention rather than a per-shape activation cache; use longer production soak runs for qualification. A fresh four-cycle medium run after the pointwise-tile update used a 149.641 MiB post-warmup baseline and ended at 146.504 MiB (-3.137 MiB); its largest cycle was only 2.022 MiB above baseline. The mixed sequence covered 16x16, 3000x80, low-contrast, and 1920x1080 dense-page inputs, again showing bounded Windows allocator retention rather than persistent dynamic-shape growth. The AVX2/AVX-512 direct-convolution path now also covers stride-1 7x7 and asymmetric 3/5/7x1 or 1x3/5/7 detector kernels, while preserving the same output/input-channel/kernel accumulation order; ARM and scalar paths retain the portable implementation. The dominant NCHW `1x1` convolution kernel also now computes four output channels per shared SIMD input load (AVX-512, AVX2, and NEON implementations), reducing hot-path input bandwidth while preserving each channel's accumulation order. The detector DB-head `2x2`, stride-1 `SAME_UPPER` max pool is likewise mapped to a boundary-safe AVX-512/AVX2/NEON kernel, removing generic window and bounds reconstruction from every output pixel. The following repeatable stress measurement records the present result on `build/ui_dense_1920x1080.ppm` (about 123--124 text regions): | Engine | Warmup | Measured runs | Mean latency | Throughput | |---|---:|---:|---:|---:| | Pure C++ PP-OCRv6 | 4 | 10 | 233.805 ms | 4.277 FPS | | ONNX Runtime 1.28 CPUExecutionProvider | 4 | 10 | 929.357 ms | 1.076 FPS | The table uses one paired `compare.ps1` run with the persistent executor and validated approximate-GELU setting. C++ is **74.8% lower latency** (0.252x) and **297.5% higher throughput** in this specific run. It replaces the previous serial-recognition baseline of 2680.808 ms, a **11.47x** native speedup. A repeated parameter sweep after the copy-elimination changes selected the current default of `rec_batch_size = 4` and `rec_parallelism = 14` for this 16-logical-CPU host; applications can tune these options for their deployment CPU. Short dense runs can vary materially with CPU scheduling, so use the command below and more runs on the deployment target before capacity planning: `Options::rec_batch_width_bucket` (or benchmark environment variable `PPOCR_REC_WIDTH_BUCKET`) can additionally pack nearby natural crop widths into one zero-right-padded batch; each crop keeps its own aspect-ratio resize. The default remains `1`: a local 123-crop dense-page sweep did not demonstrate a stable CPU benefit from padded work, so exact-width batching remains the lower-memory default. Wider buckets are available for deployment-specific GPU or batch-density tuning. ```powershell powershell -ExecutionPolicy Bypass -File .\bench\compare.ps1 ` -Image .\build\ui_dense_1920x1080.ppm -Runs 10 -Warmup 4 -Threads 16 ``` The captured result is C++ 233.805 ms (median 232.980 ms, p95 244.485 ms) and ONNX Runtime 929.357 ms (median 930.224 ms, p95 1098.989 ms). Keep repeated paired measurement when refreshing the table, because dense-page latency can vary with host scheduling. The run used four warmups, 10 measured runs, and matching 16-thread CPU budgets. Use longer repeated measurements on the deployment CPU before capacity planning; dense-page latency also depends strongly on the detected crop mix. ### Expanded stress corpus `bench/stress_regression.py` executes the native C++ pipeline over 11 deterministic local stress images without adding a runtime dependency. It covers 16鑴?6 tiny text, 3000鑴?0 and 80鑴?000 strips, low contrast, rotation, blank input, dense synthetic text, and 1920鑴?080 / 2560鑴?440 screenshots. The gate asserts bounded detection counts, valid confidence/geometry, and high-confidence text anchors where exact front-end parity is meaningful; rotated and density-only cases intentionally test robustness rather than unstable line ordering. ```powershell & 'D:\workprj\aicoder\.tmp\ocr-models\.venv\Scripts\python.exe' ` .\bench\stress_regression.py ` --cpp .\build\ppocr_demo.exe ` --det D:\workprj\aicoder\.tmp\ocr-models\ppocrv6_tiny_det.onnx ` --rec D:\workprj\aicoder\.tmp\ocr-models\ppocrv6_tiny_rec.onnx ` --dict D:\workprj\aicoder\.tmp\ocr-models\dict_ppocrv6_tiny.txt ` --images D:\workprj\aicoder\corelib\ocr\testdata\stress ``` Latest local run: **11/11 passed** (box counts: 123, 0, 1, 1, 20, 1, 3, 6, 743, 34, 38). This broadens the existing strict 7-image text-parity gate; it does not weaken it. ### Recognition-regression corpus `bench/accuracy_compare.py` compares complete C++ OCR results against an ONNX Runtime CPU reference. Its default gate checks detected-line count and decoded text; confidence and geometry are retained in the JSON report but do not gate the test because the two standalone frontends have small resize/crop rounding differences. Pass `--strict-geometry` to also gate those fields. `bench/accuracy_expectations.json` supplies independently recorded expected text for the deterministic corpus, so the same run also checks C++ results against known text rather than only comparing two inference backends. The latest 7-image corpus passed **7/7 text-parity cases**. It includes clean multi-line English, a single word, a 3000-pixel-wide receipt line, a low-contrast/noisy scene, a blank image, and a 16x16 tiny-text case. The samples are existing deterministic test assets from `D:\workprj\aicoder\corelib\ocr\testdata`. An additional multilingual and extreme-shape sweep covers Chinese and mixed language text, a 80x3000 vertical strip, rotated text, synthetic dense blobs, and 1920x1080/2560x1440 screenshots. Chinese/mixed baseline samples retain exact decoded-text parity. The pathological vertical/dense screenshot samples remain diagnostic rather than release gates because detector/crop rounding can alter weak low-confidence regions between the two independently implemented frontends. `accuracy_compare.py` now forces UTF-8 report output so this corpus can be run from a Windows GBK console without losing dictionary characters. ```powershell $images = @( 'D:\workprj\aicoder\corelib\ocr\testdata\en.png', 'D:\workprj\aicoder\corelib\ocr\testdata\line_en.png', 'D:\workprj\aicoder\corelib\ocr\testdata\stress\single_word.png', 'D:\workprj\aicoder\corelib\ocr\testdata\stress\wide_strip_3000x80.png', 'D:\workprj\aicoder\corelib\ocr\testdata\stress\noisy_lowcontrast.png', 'D:\workprj\aicoder\corelib\ocr\testdata\stress\blank_800x600.png', 'D:\workprj\aicoder\corelib\ocr\testdata\stress\tiny_16x16.png' ) & 'D:\workprj\aicoder\.tmp\ocr-models\.venv\Scripts\python.exe' ` .\bench\accuracy_compare.py ` --cpp .\build\ppocr_demo.exe ` --python 'D:\workprj\aicoder\.tmp\ocr-models\.venv\Scripts\python.exe' ` --det 'D:\workprj\aicoder\.tmp\ocr-models\ppocrv6_tiny_det.onnx' ` --rec 'D:\workprj\aicoder\.tmp\ocr-models\ppocrv6_tiny_rec.onnx' ` --dict 'D:\workprj\aicoder\.tmp\ocr-models\dict_ppocrv6_tiny.txt' ` --expectations .\bench\accuracy_expectations.json ` --images $images ``` Captured report: `build/accuracy_ground_truth_7_ctctop1_sparse.json` (7/7 passed after batch/concurrent recognition and terminal-CTC reduction). ### Dynamic-size memory soak `ppocr_mem_soak` keeps one C++ OCR instance alive while repeatedly alternating tiny, wide-strip, noisy, and 1920x1080 UI images. It reports process-private bytes after every complete mixed-size cycle. Intermediate tensors are released after their final graph consumer, model weights remain immutable, and no per-shape activation cache is retained. Detector results (including the large probability map) are additionally scoped through DB post-processing and released before concurrent recognition batches begin, so the allocator can reuse their storage rather than combining detector and recognizer activation peaks. The same process-level workload can be run against ONNX Runtime with `bench/ort_mem_soak.py`. On this Windows host, using 12 mixed-size cycles: | Engine | Baseline private bytes | Final private bytes | Final change | |---|---:|---:|---:| | Pure C++ PP-OCRv6 | 23.825 MiB | 24.633 MiB | +0.809 MiB | | ONNX Runtime CPU | 145.371 MiB | 203.805 MiB | +58.434 MiB | The current C++ run ranged from 22.735 MiB to 27.703 MiB after the baseline; its final working set was 0.809 MiB above the immediate post-load baseline, without a monotonic per-cycle increase. ONNX Runtime retained roughly 58 MiB in the prior corresponding run. This is a process-private-memory measurement for the specified machine and models, not a portable memory guarantee; rerun it on the deployment target. Build PPM copies for the native soak from the same PNG source files: ```powershell $code = @' from PIL import Image from pathlib import Path source = Path(r'D:\workprj\aicoder\corelib\ocr\testdata') output = Path(r'.\build\mem_samples'); output.mkdir(exist_ok=True) for name in ['stress/tiny_16x16.png', 'stress/wide_strip_3000x80.png', 'stress/noisy_lowcontrast.png', 'stress/ui_dense_1920x1080.png']: Image.open(source / name).convert('RGB').save(output / (Path(name).stem + '.ppm'), format='PPM') '@ $code | & 'D:\workprj\aicoder\.tmp\ocr-models\.venv\Scripts\python.exe' - ``` Then run the C++ side (replace the model paths if needed): ```powershell .\build\ppocr_mem_soak.exe 12 ` .\build\mem_samples\tiny_16x16.ppm ` .\build\mem_samples\wide_strip_3000x80.ppm ` .\build\mem_samples\noisy_lowcontrast.ppm ` .\build\mem_samples\ui_dense_1920x1080.ppm ``` ### Resolution ladder and post-large-image memory recovery For a stricter retention check, generate a deterministic page ladder that climbs from `16x16` through `3840x2160`, includes ultra-wide and ultra-tall pages, and ends with another `64x64` page: ```powershell & 'D:\workprj\aicoder\.tmp\ocr-models\.venv\Scripts\python.exe' ` .\bench\generate_memory_ladder.py --output .\build\memory_ladder .\build\ppocr_mem_soak.exe 2 ` .\build\memory_ladder\ladder_16x16.ppm ` .\build\memory_ladder\ladder_64x64.ppm ` .\build\memory_ladder\ladder_160x120.ppm ` .\build\memory_ladder\ladder_320x240.ppm ` .\build\memory_ladder\ladder_640x480.ppm ` .\build\memory_ladder\ladder_1280x720.ppm ` .\build\memory_ladder\ladder_4096x256.ppm ` .\build\memory_ladder\ladder_256x4096.ppm ` .\build\memory_ladder\ladder_1920x1080.ppm ` .\build\memory_ladder\ladder_2560x1440.ppm ` .\build\memory_ladder\ladder_3000x1600.ppm ` .\build\memory_ladder\ladder_3840x2160.ppm ` .\build\memory_ladder\ladder_recovery_1920x1080.ppm ` .\build\memory_ladder\ladder_recovery_4096x256.ppm ` .\build\memory_ladder\ladder_recovery_256x4096.ppm ` .\build\memory_ladder\ladder_return_64x64.ppm ``` For CI or a repeatable pass/fail report, run the bounded-recovery checker. It records every resolution and requires the final `64x64` sample to remain within 20% of both the warmed baseline and the final ultra-tall recovery sample, as well as within 20% of the earlier `3840x2160` sample. The report includes `declined_after_largest`, so allocator capacity retention cannot be confused with an actual post-large-image recovery observation: ```powershell & 'D:\workprj\aicoder\.tmp\ocr-models\.venv\Scripts\python.exe' ` .\bench\memory_ladder_regression.py ` --cpp .\build\ppocr_mem_soak.exe --det --rec ` --dict --images .\build\memory_ladder --cycles 2 --backend cpu ` --require-post-large-decline ``` `ppocr_mem_soak` loads, infers, and destroys one RGB image at a time and emits one private-byte sample after every resolution. The final small-page sample is therefore the direct recovery observation after the staged 4K and extreme-aspect-ratio tail; compare it with the `ladder_3840x2160` and final `ladder_recovery_256x4096` samples and the next cycle rather than treating the process allocator's high-water capacity as a leak. Run the same command with `PPOCR_BACKEND=hybrid` to include the persistent Vulkan buffers in the process-level measurement. The current generator intentionally uses an ascending, aspect-ratio-diverse 16-sample sequence: `16x16 -> 64x64 -> 160x120 -> 320x240 -> 640x480 -> 1280x720 -> 4096x256 -> 256x4096 -> 1920x1080 -> 2560x1440 -> 3000x1600 -> 3840x2160 -> 1920x1080 -> 4096x256 -> 256x4096 -> 64x64`. The final staged recovery tail checks that memory remains bounded after both 4K area and extreme-aspect-ratio images. In addition to private bytes, the native JSON now includes Windows working-set bytes for each measured sample. Private bytes remain the bounded-recovery gate; working set is reported as complementary residency evidence because the Windows heap and Vulkan driver may retain reusable committed capacity after the page and its activations have been destroyed. The generator also includes native **5K** (`5120x2880`) and **8K** (`7680x4320`) source pages. The 8K sample is followed immediately by a 4K recovery sample, so use `--require-post-large-decline` to make both Windows private bytes and working set decline at that exact transition a strict gate. This catches source-image retention even though detector inference itself is normally capped by `det_limit_side_len`. ### 2026-08-14: current small-to-large image-memory check The checked-in generator was rerun against the current medium PP-OCRv6 CPU build. It creates the deterministic 16-image sequence above, processes each image with one long-lived OCR instance, and destroys the decoded RGB image before sampling process memory. The result therefore measures retained runtime/allocator capacity rather than intentionally retained source images. In the current one-cycle run, private memory was **147,017,728 B** at `3840x2160`, then immediately declined to **146,296,832 B** at the following `1920x1080` sample (**-720,896 B**, **-0.49%**). Working set also declined from **150,679,552 B** to **150,085,632 B** (**-593,920 B**). After the complete recovery tail, the final `64x64` sample was **147,124,224 B** private and **151,035,904 B** working-set memory: below the pre-return peak (**148,320,256 B** private / **152,371,200 B** working set), and only **2.21%** above the warmed private baseline (**143,937,536 B**). The bounded-recovery gate passed. Full per-resolution evidence is `build/memory_ladder_medium_cpu_current_20260814.json`. Windows allocation is intentionally treated as a bounded-recovery result, not as a requirement that every allocator page be decommitted immediately: the explicit first-post-4K samples prove the observed downward transition, while the final small-image sample guards the durable footprint after the whole large-image sequence. ### 2026-08-13: current ascending-resolution memory measurement With the current default recognizer batch size of 1, CPU backend, 16 kernel workers, and approximate GELU, medium PP-OCRv6 passed a fresh one-cycle ascending-resolution check. The run covers every generated image from `16x16` through `3840x2160` (including `4096x256` and `256x4096`), then the staged recovery tail and a final `64x64`. Private bytes were **147,234,816 B** after `3840x2160` and **147,144,704 B** after the final `64x64`, a decline of **90,112 B (0.06%)**. Working set likewise declined from **151,109,632 B** to **150,913,024 B** (**196,608 B**). The final private-byte value was **0.008%** above the fully warmed **147,132,416 B** baseline and 2.38% below the pre-return peak, so every 20% bounded-recovery gate passed. The complete per-resolution record is `build/memory_ladder_medium_cpu_ascending_recheck_20260813.json`; values are host-specific allocator observations, rather than a promise of immediate heap decommit on every operating system. ### 2026-08-13: per-resolution memory-record validation The ladder checker now preserves the private-byte and working-set sample for **every** image resolution in its JSON report, together with the cycle peak and a direct post-`2560x1440` recovery flag. This makes the check suitable for both leak detection and deployment profiling: a final small-page allocation is not mistaken for the large-page allocation just because a heap or driver keeps reusable capacity committed. On the current AVX-512 Windows host, medium PP-OCRv6 with 16 kernel workers and opt-in approximate GELU passed two fresh CPU ladder cycles. The warmed private baseline was **146,632,704 bytes**. After `2560x1440`, the final `64x64` sample was **148,533,248 bytes** in cycle 1 (+1.30% baseline, +2.00% from the largest-page sample) and **143,220,736 bytes** in cycle 2 (-2.33% baseline, -1.23% from the largest-page sample). Thus direct private-byte recovery occurred in the second cycle but not the first: Windows heap reuse can retain or move committed capacity between samples. Both final samples remained within the 20% bounded-recovery limit, which is the pass/fail gate; the JSON deliberately retains the explicit per-cycle recovery flag instead of claiming unconditional decommit after a large image. The same two-cycle Hybrid Vulkan run also passed. Its warmed private baseline was **394,969,088 bytes** and each final `64x64` sample was **396,017,664 bytes** (+0.265%). The final private bytes equalled the immediately preceding `2560x1440` sample in both cycles, while the working set was essentially flat (+12,288 bytes in cycle 1 and +8,192 bytes in cycle 2). This is persistent Vulkan/Windows allocator capacity rather than evidence that an image or its activations remain live. These are machine-specific process measurements; the exact per-resolution values are retained in `build/memory_ladder_medium_{cpu,hybrid}_20260813.json` when the commands are run locally. On 2026-08-12, the current medium model passed this expanded two-cycle gate. CPU warmed at **148,127,744 B** and returned to **147,148,800 B** and **147,152,896 B** (both **-0.66%** of baseline); the final small-page samples were 2.33% and 1.70% above their immediately preceding largest-page samples. Hybrid Vulkan warmed at **393,490,432 B** and returned at **395,317,248 B** (+0.46%) and **395,341,824 B** (+0.47%). All private-byte deltas passed the 20% gate. The resident working-set observations are also captured in the checker output rather than being inferred from private bytes; on this shared Windows host they remained within the allocator/driver's warmed range, not a claim that either heap must promptly decommit pages. On 2026-08-12, the tiny model with 16 CPU workers was run through this exact seven-image ladder. CPU-only warmed to **9,400,320 bytes**; after the largest `2560x1440` page its next small-page sample was **9,465,856 bytes** (**+65,536 bytes**, 0.7% of baseline). Hybrid warmed to **116,305,920 bytes** and the corresponding post-large small-page sample was **115,802,112 bytes** (**-503,808 bytes**). These per-image values show recovery after the largest input for both modes. They are Windows private-byte observations on this host, not a cross-platform leak proof or a fixed memory guarantee. The same seven-page gate was rerun after the depthwise-pointwise-GELU graph fusion with the **medium** PP-OCRv6 model (two measured cycles, default bounded CPU scheduler). Both modes passed the 20% bounded-recovery limits: | Backend | Warmed baseline | Final `64x64`, cycle 1 | Final `64x64`, cycle 2 | Result after `2560x1440` | |---|---:|---:|---:|---| | CPU | 136.351 MiB | 140.019 MiB (+2.69%) | 140.008 MiB (+2.68%) | Allocator private bytes did not immediately decline, but remained stable and 2.24閳?.29% above the largest-page sample. | | Hybrid Vulkan | 360.574 MiB | 361.582 MiB (+0.28%) | 361.582 MiB (+0.28%) | Equal to the immediately preceding largest-page sample in both cycles. | The CPU observation is expected on Windows: completed image and activation objects have been destroyed, while the process allocator may retain reusable blocks. Consequently the test reports both `declined_after_largest` and the bounded deltas instead of treating an unchanged private-byte high-water mark as proof of a live-image or tensor leak. The Hybrid samples include its persistent Vulkan allocations. The same CPU-only one-cycle ladder was also run for the other two PP-OCRv6 sizes: small warmed to **37,662,720 bytes** and returned at **38,973,440 bytes** after its largest page (**+1,310,720 bytes**); medium warmed to **147,435,520 bytes** and returned at **148,439,040 bytes** (**+1,003,520 bytes**). In both cases the return sample remained within 3.5% and 0.7% of their post-warm-up baselines respectively, with no retained RGB source images between calls. The reusable checker was then run for two full ladders after the per-image sampler change. All three CPU models passed the 20% bounded-recovery gate: tiny returned within **+1.14%**, small within **+10.09%**, and medium within **+2.46%** of their warmed baselines; each final `64x64` sample was also at or below the pre-large `1920x1080` sample except medium's first cycle (**+0.81%**). Tiny hybrid also passed, returning within **+0.08%** of baseline, including the persistent Vulkan allocation. These figures are deliberately a bounded allocator-reuse test, not an assertion that Windows must immediately release committed heap pages to the OS. The current checker also makes the immediate recovery result explicit. On a fresh two-cycle tiny run with the default 12 workers, CPU warmed to **10,047,488 bytes** and returned at **9,379,840** then **9,441,280 bytes**; both cycles reported `declined_after_largest=true` (2.97% and 9.64% below the preceding `2560x1440` samples). Hybrid warmed to **135,745,536 bytes** and returned at **135,098,368** then **135,102,464 bytes**, both below its warmed baseline but **not** immediately below the preceding largest-image samples (`declined_after_largest=false`, +1.24% and +0.63%). This distinction is intentional: the test reports the actual post-large behavior and separately enforces bounded recovery, rather than incorrectly claiming immediate heap release for persistent Vulkan/Windows allocator capacity. ### 2026-08-12: current ascending-resolution memory check The deterministic seven-image ladder was regenerated and rerun on the current binary: `64x64 -> 320x240 -> 640x480 -> 1280x720 -> 1920x1080 -> 2560x1440 -> 64x64`. Each sample performs `load -> Recognize -> destroy -> PrivateBytes`, so the reported process-private bytes include runtime working memory but do not retain the source RGB page between samples. The bounded 20% return gate passed in every run below. | Model / backend | Warmed baseline | Cycle 1 final 64x64 | Cycle 2 final 64x64 | Post-largest result | |---|---:|---:|---:|---| | tiny / CPU | 9,367,552 B | 9,412,608 B (+0.48%) | 9,424,896 B (+0.61%) | Fell below the preceding 2560x1440 sample in both cycles (11.17%, 1.75%). | | medium / CPU | 148,254,720 B | 146,284,544 B (-1.33%) | 147,300,352 B (-0.64%) | Bounded return; final sample was 1.08% / 2.65% above the immediately preceding large sample. | | tiny / hybrid Vulkan | 115,851,264 B | 99,143,680 B (-14.42%) | 98,140,160 B (-15.29%) | Bounded return; final sample was 0.004% / 0.017% above the immediately preceding large sample. | `declined_after_largest` is therefore true for the CPU tiny run and false for the medium CPU and hybrid runs. That false value is an observation, not a leak verdict: Windows heap and persistent Vulkan allocations may retain or rearrange reusable capacity. The two complementary checks閳ユ敄he final-small sample versus its post-warm-up baseline and the stated largest-page comparison閳ユ攷ake the memory behavior auditable without claiming that a process must immediately return committed pages to the OS. After the subsequent depthwise-pointwise-GELU fusion, the medium ladder was rerun with two cycles. CPU warmed at **142,974,976 B**; its final `64x64` samples were **146,821,120 B** (+2.69%) and **146,808,832 B** (+2.68%). They were 2.29% and 2.24% above the respective `2560x1440` samples, so `declined_after_largest=false`, yet both are stable and well within the 20% bounded-return gate. Hybrid warmed at **378,089,472 B** and returned at **379,146,240 B** in both cycles (+0.28%); each final small image equalled its preceding largest-image sample (`declined_after_largest=true`). Thus the test continues to distinguish released OCR image/tensor lifetimes from normal Windows heap or persistent Vulkan capacity retention. ### 2026-08-12: admitted depthwise-to-pointwise Vulkan segment The graph loader now recognizes an exact `depthwise Conv(bias) -> 1x1 pointwise Conv(bias)` pair and emits `FusedDepthwisePointwiseConv`. Its CPU path remains the same two SIMD kernels, while hybrid can submit the existing Vulkan chain in one command buffer: the depthwise result stays in a device storage buffer and only the chain boundaries transfer. Admission is cached per full NCHW geometry and accepts GPU only after a repeated H2D + dispatch + fence + D2H comparison and elementwise CPU-result validation show that the complete segment is no slower. Consequently this reduces CPU occupancy when the Radeon/driver wins without forcing a transfer-bound GPU route. The feature covers the detector MobileNet backbone pairs in all three model sizes. AVX-512 kernel smoke and the four diverse decoded-text comparisons for tiny, small and medium all passed (**12/12**). The current Radeon 780M primitive probe measures a representative depthwise-to-pointwise batch at **0.76714 ms GPU vs 0.20474 ms CPU**, so this particular UMA shape is correctly rejected; it is evidence that the new graph route retains the no-slower policy rather than a claim of blanket GPU acceleration. A current 20-run CPU-only `en.ppm` snapshot (16 kernel workers, approximate GELU) measured tiny **27.954 ms**, small **60.455 ms**, and medium **195.588 ms**. `gpu_only` still fails explicitly because the complete PP-OCRv6 graph, post-processing and CTC decode are not yet device resident; it never silently falls back to CPU. ### 2026-08-12: depthwise-to-pointwise CPU graph fusion The same strict MobileNet `depthwise Conv(bias) -> 1x1 pointwise Conv(bias)` pattern now executes as one graph node on CPU too. It removes the executor's hash-map entry, node dispatch and intermediate tensor lifetime from the graph; the internal temporary remains only for the two exact SIMD kernels. This is an intentional graph/lifetime fusion, not a mathematical approximation. Set `PPOCR_DISABLE_DEPTHWISE_POINTWISE_FUSION=1` for an A/B fallback. On the current 16-worker AVX-512 host, three warm-ups and fifteen `en.ppm` runs showed the fused path at **26.784 / 56.596 / 174.962 ms** for tiny, small and medium, respectively, compared with **27.121 / 58.209 / 228.572 ms** with the graph fusion disabled. The medium result is host-scheduling sensitive, so the reproducible conclusion is the consistent no-regression and gains for tiny/small, while deployment owners should rerun the medium A/B on their target CPU. A separate 20-run medium A/B measured **213.711 ms** fused versus **222.719 ms** disabled. The exact decoded-text gate passed **12/12** across tiny/small/medium and four diverse inputs. A two-cycle medium resolution ladder also passed the bounded memory-recovery gate: 147,255,296-byte warmed baseline; final small-page samples were -0.68% and -0.08% from baseline. As elsewhere, the final samples did not immediately fall below the preceding maximum-size sample, which is normal reusable Windows allocator capacity rather than a leak assertion. ### 2026-08-12: depthwise-pointwise-GELU liveness fusion The loader now extends that exact single-use pattern through an immediately following fused GELU as `FusedDepthwisePointwiseConvGelu`. The pointwise output is activated in place, rather than re-entering the generic graph loop to rename and transform it. It preserves FP32 operation order (the same existing GELU kernel runs after the two convolutions) and keeps the Vulkan segment's no-slower admission boundary intact; a selected GPU segment still returns before the exact CPU GELU pass. The targeted A/B escape hatch is `PPOCR_DISABLE_DEPTHWISE_POINTWISE_GELU_FUSION=1`. The AVX-512 kernel smoke and the 12/12 decoded-text ONNX Runtime gate passed. On this shared host, a 15-run `en.ppm` A/B did not establish a uniform latency benefit: tiny was 29.137 vs 28.908 ms, small 62.292 vs 62.007 ms, and medium 205.199 vs 221.369 ms (fusion enabled vs disabled). It is retained because it removes a graph-lifetime boundary and shows a medium improvement, but the table intentionally does not promote it to a universal CPU speedup. ### 2026-08-12: expanded batched ordinary-Conv SIMD coverage `Conv2dBatch()` now carries the batch index through the existing runtime-gated SIMD kernels for valid `2x2`, padded `5x5`/`7x7`, and asymmetric `1x3`/`3x1`/`1x5`/`5x1` stride-one convolutions, in addition to the prior `3x3` stride-one/two paths. This removes one worker-pool dispatch per crop for the medium detector's context layers while preserving each output's increasing-channel/kernel accumulation order. AVX-512 is selected only after the existing CPUID/XCR0 guard; AVX2 and portable/NEON paths retain their safe fallback behavior. The low-level smoke now compares N=3 batched output against three independent calls for all of those shapes and ReLU tails. It passed with normal AVX-512 dispatch and with `PPOCR_DISABLE_AVX512=1` (AVX2 fallback). A post-change four-identical-page medium CPU batch test (`det_batch_size=4`, `rec_batch_size=4`, three measured runs) was result-identical and measured **793.299 ms** serial versus **710.437 ms** batched (**1.117x**). The same tiny hybrid configuration was result-identical and measured **126.750 ms** serial versus **85.669 ms** batched (**1.480x**). These are local scheduler measurements, not a universal Vulkan claim: the concurrent Vulkan primitive smoke continued to select GPU only for its no-slower affine+Swish probe and retained CPU for the transfer-bound Conv and GEMM probes. The full small-to-large-to-small CPU ladder was rerun for all model sizes with the expanded kernels. All passed the three 20% bounded-recovery limits. Observed final-small change from the post-warm baseline was tiny **+1.72%** and **+8.51%**, small **-1.57%** and **+8.90%**, and medium **+0.70%** and **+0.02%** over two cycles. `declined_after_largest` is retained as a fact rather than a pass condition, because Windows heap trimming is nondeterministic even when the runtime has released the input and activation vectors. ### 2026-08-12: default AVX-512 wide-context Conv tile The AVX-512 eight-output tile for the PP-OCRv6 medium detector's `5x5` and `7x7` context layers is now enabled by default for its narrow, runtime-checked shape (`C_in >= 32`, `C_out >= 8`, and both spatial dimensions at least 32). It reuses each input vector across eight output reductions; smaller shapes, AVX2, NEON, and scalar targets retain their existing paths. Set `PPOCR_DISABLE_AVX512_CONV_WIDE8=1` for a deployment-local A/B test. On the local AVX-512 host with 12 workers, three warm-ups and twelve `en.ppm` runs, medium CPU measured **424.234 ms** with the tile and **659.937 ms** with `PPOCR_DISABLE_AVX512_CONV_WIDE8=1` (same model/input; shared-host variation is reflected by the recorded medians **404.622 / 668.809 ms**). Fresh ONNX-Runtime decoded-text comparison remained **12/12** across tiny, small, and medium on `16x16`, English, low-contrast, and `3000x80` inputs. The medium two-cycle `64x64 閳?2560x1440 閳?64x64` ladder also passed all bounded-memory checks, ending at **+0.19%** and **+2.72%** from its warmed baseline. Vulkan primitive smoke remained numerically passing; hybrid selected only its measured no-slower affine+Swish and depthwise+Swish samples in that run. ### 2026-08-12: medium worker and transpose-tile recheck A fresh worker sweep after the wide-context default confirms the library's automatic **16-worker** ceiling remains appropriate for the broad medium graph, while tiny/small retain their shorter-kernel behavior: on the same `en.ppm` setup, medium measured **191.664 / 205.187 / 185.906 ms** with 8/12/16 workers; tiny measured **28.434 / 28.797 ms** and small **57.390 / 59.147 ms** at 12/16 workers. The 16-worker medium CPU retest (3 warm-ups, 20 runs) averaged **185.325 ms**; paired ONNX Runtime CPU on the same image/run count averaged **260.194 ms**, a host-specific **28.8%** lower native latency. The AVX-512 four-output `ConvTranspose2x2` FPN experiment was rechecked too. It measured **186.232 ms** versus **181.971 ms** for the established single-output implementation (3 warm-ups, 10 runs), so it remains opt-in via `PPOCR_ENABLE_AVX512_TRANSPOSE2X2_TILE4=1`; it is not promoted merely because it reduces some input loads. This preserves the faster default and avoids a regression on the current target. ### 2026-08-12: device-resident depthwise/pointwise Vulkan segment The Vulkan backend now has a numerically checked two-dispatch MobileNet `depthwise Conv -> 1x1 pointwise Conv` primitive. It uploads the chain input once, leaves the depthwise feature map in a mapped Vulkan storage buffer, inserts an explicit compute shader write-to-read memory barrier, and reads back only the pointwise result. Its hybrid admission measures that complete boundary against the corresponding CPU `DepthwiseConvBatch` plus `PointwiseConvBatch` pair and rejects Vulkan when it is slower. On the local Radeon 780M / AVX-512 host, the odd-shape batched numerical smoke passed, but the representative `N=4, C=32, M=40, 31x29, 3x3` full-boundary probe measured **0.836 ms Vulkan vs 0.271 ms CPU**. Hybrid therefore keeps the CPU path for this segment. The primitive is deliberately not folded into the production ONNX graph yet: its current model-level floating-point drift needs a decoded-text tolerance gate before any performance claim or default dispatch change. This preserves exact production behavior while retaining a real, barrier-validated GPU building block for subsequent fully-resident segments. The batch smoke executable now also prints its individual timed samples, so a throughput claim can be audited for scheduler outliers rather than relying on one aggregate number. With four equal `en.ppm` pages, batch size four, 16 CPU kernel workers, two warm-ups and eight timed runs, CPU serial閳妼atch results were **120.826閳?5.704 ms (1.410x)** for tiny, **281.621閳?27.853 ms (1.236x)** for small, and **859.114閳?44.174 ms (1.018x)** for medium. Hybrid under the same batch settings measured **166.786閳?08.215 ms (1.541x)** tiny, **331.511閳?51.555 ms (1.318x)** small, and **824.029閳?49.212 ms (1.100x)** medium. Serial/batch OCR outputs were byte-for-byte equivalent in each run. The hybrid figures are page-batching capacity gains; they do not claim that the partial Vulkan graph is faster than CPU for every single page. A fresh 12-cycle run after this lifetime change used a 23.488 MiB baseline and finished at 26.664 MiB (+3.176 MiB); its cycle deltas ranged from -0.543 MiB to +4.055 MiB with no monotonic growth. Windows heap retention makes the final private-byte delta workload- and allocator-dependent, so the important release criterion is the non-growing mixed-shape sequence, not a single final number. After the batched constant-weight MatMul change, a medium-model two-cycle mixed-size check covering `16x16`, `3000x80`, `80x3000`, and the 119-box `1920x1080` UI page used **293,961,728 bytes** after warm-up; the two cycle measurements were **811,008 bytes below** and **471,040 bytes below** that baseline. This verifies that flattening a batch is a scheduling change, not a per-shape tensor cache or an activation-retention increase. As above, it is a Windows allocator-capacity observation rather than a universal leak proof. After adding the Vulkan channel-affine batch buffers, the same medium mixed sequence was rerun for two cycles with the default hybrid policy. Its post-warm-up baseline was **290,508,800 bytes**; cycles finished at **+5,214,208** and **+2,506,752 bytes**. The high-water movement is bounded Windows allocator retention after the large UI page, not a per-cycle growth trend; GPU affine admission was rejected on this host, so the extra persistent Vulkan coefficient storage was not exercised by the CPU execution path. After the BatchNorm-Swish in-place activation update, a fresh CPU medium mixed-size soak (`16x16`, `3000x80`, `80x3000`, and the dense `1920x1080` UI page) used **286,793,728 bytes** post-warm-up. Its next two cycles were **9,101,312** and **5,144,576 bytes below** baseline. This is a bounded allocator observation on Windows, but it confirms the source-buffer reuse does not retain changing-shape recognition activations. The captured C++ and ONNX Runtime reports are `build/memory_soak_cpp_12.json` and `build/memory_soak_ort_12.json`. A post-optimization 6-cycle C++ check is also retained as `build/memory_soak_cpp_parallel4_6.json`: it finished 1.034 MiB below its 24.629 MiB post-warmup baseline, demonstrating that bounded concurrent recognition does not retain every dynamic input shape. ### 2026-08-12: cross-page recognition batching `OCR::RecognizeBatch` now keeps dynamic detector resizing per page, then coalesces equal-width crops from all submitted pages into bounded NCHW recognizer batches. This is useful for both CPU SIMD and the Vulkan batch-Y dimension; it does not pad unrelated detector pages, does not change result order, and releases each detector probability map before recognition starts. On this Radeon 780M / Windows host, medium PP-OCRv6, four copies of `en.ppm`, `rec_batch_size=8`, one recognizer worker and one detector worker: | Path | Serial pages | Cross-page batched | Speedup | |---|---:|---:|---:| | CPU | 773.012 ms | 744.098 ms | 1.039x | | Hybrid | 854.746 ms | 1249.711 ms | 0.684x | The batch smoke test confirms exact public `Result` equivalence with serial inference. Hybrid is intentionally not presented as a win here: the current UMA Vulkan implementation admits each segment only when its complete transfer + dispatch + readback path is no slower than CPU, while the coalesced workload still has significant CPU-only graph work. Four-image mixed-size CPU coverage (`en`, low-contrast, wide strip, dense 1920x1080 UI; 128 results) also passed the serial-result check at 1.004x. A tiny-model four-page CPU check measured 143.472 ms serial and 139.913 ms batched (1.025x). Re-run these workloads on the deployment hardware; they are measurements, not portability claims. ### 2026-08-12: depthwise Vulkan batch kernel and hybrid batch gate The Vulkan depthwise-convolution path now maps one workgroup to one NCHW channel and up to 1,024 output pixels. Its `[KH,KW]` filter is cooperatively staged in workgroup memory (up to `16x16`), while Vulkan's Y dimension remains the independent crop-batch dimension. PP-OCRv6's MobileNet-style depthwise filters therefore avoid reloading the same channel filter for every output pixel. Larger exported filters safely use the existing global-memory fallback. The Radeon 780M numerical smoke test passed for a padded `N=2, C=5, 7x9, 3x3` depthwise batch. Its latest complete synchronous measurement was **0.44340 ms GPU / 0.35062 ms CPU**, so the strict H2D + dispatch + D2H admission policy correctly retained the CPU SIMD path for that shape. This is an optimized, batch-capable GPU primitive, not a claim that the incomplete GPU-resident OCR graph is faster on this UMA GPU. The same build also ran the public batch-equivalence harness on tiny PP-OCRv6 with `rec_batch_size=8`, one detector worker and one recognizer worker across four varied pages (`en`, low-contrast, `3000x80`, dense `1920x1080`). All **129** decoded results matched serial `Recognize()` exactly. The one-shot local measurement was **1081.928 ms serial / 1167.667 ms batched** for CPU and **1105.010 ms serial / 1075.921 ms batched** for hybrid. These scheduler samples are intentionally retained as measurements rather than a universal throughput promise; the dense mixed-width workload has limited equal-width crop coalescing. A separate CPU-versus-hybrid dense-page result comparison was also byte-for-byte identical. ### 2026-08-12: cross-crop CPU pointwise scheduling The CPU 1x1-convolution path now accepts a real NCHW batch in one scheduling call. On x86 it flattens `(crop N, output-channel tile)` into the persistent AVX-512/AVX2 worker queue, retaining the existing per-channel FP32 reduction order and ISA-specific four/eight-output vector kernels. ARM/NEON and scalar builds retain their portable per-crop path. This removes repeated worker-pool barriers for batchable recognizer projections without changing the model graph or requiring ONNX Runtime. The strict CPU-versus-ONNX Runtime decoded-text gate passed **4/4** varied inputs (`en`, `16x16`, low-contrast, `3000x80`) for **tiny, small, and medium** after this change. The five-page public batch gate (also adding dense `1920x1080`) passed serial-result equivalence for all three models: tiny **130**, small **126**, medium **129** results. A local one-worker CPU batch sample measured **0.948x / 1.162x / 1.014x** page-batch throughput for tiny/small/medium respectively; the mixed dimensions intentionally make this an evidence point rather than a universal latency claim. Hybrid revalidation also preserved exact serial results at **1.003x / 1.036x / 0.983x**. On `en.ppm`, 2 warm-ups and 6 timed CPU runs with 16 workers, the current native means were **44.193 ms / 84.184 ms / 256.530 ms** for tiny/small/medium. The matched ONNX Runtime CPU samples were **127.640 ms / 244.623 ms / 513.763 ms**, corresponding to **2.89x / 2.91x / 2.00x** native latency advantages on this host. Windows scheduling remains variable; these are repeatable local observations, not a portable capacity guarantee. The five-size, two-cycle memory soak (`16x16`, `en`, low-contrast, `3000x80`, `1920x1080`) remained bounded. Its warmed baselines and final deltas were: tiny **113,876,992 / +3,874,816 bytes**, small **150,462,464 / -2,027,520 bytes**, and medium **385,916,928 / -3,629,056 bytes**. The small movements are Windows allocator high-water behavior rather than a leak assertion. ### 2026-08-12: batched residual 1x1 projection scheduling Fused `Conv(1x1) -> Add`, `Conv(1x1) -> Add -> ReLU`, and `Conv(1x1) -> Add -> Swish` now have NCHW batch kernels. On x86, one work queue covers all `(crop, output-channel SIMD tile)` tasks: AVX-512 uses the established four/eight-output tiles and AVX2 uses four-output tiles. ARM/NEON and scalar builds safely retain the per-crop implementation. The Swish variant completes the batched fused convolution/add first, then applies the existing vector Swish once across the whole contiguous NCHW allocation; FP32 reduction and activation order are unchanged. `ppocr_kernel_smoke` verifies the new Add, Add+ReLU, and Add+Swish batch kernels against their single-crop counterparts for both SIMD-tail and large work cases. It also passed with `PPOCR_DISABLE_AVX512=1`, exercising the AVX2 fallback. `PPOCR_DISABLE_RESIDUAL_POINTWISE_BATCH=1` is available solely as a per-process A/B escape hatch; it restores the prior per-crop calls. The four-image decoded-text comparison with ONNX Runtime remained **4/4** for each tiny, small, and medium model. The five-size public batch-equivalence gate also matched all serial `Result`s exactly: tiny **130**, small **126**, medium **129**. One local CPU sample (one detector/recognizer worker, `rec_batch_size=8`) measured serial/batch times of **909.483/907.159 ms** (tiny, **1.003x**), **2780.569/2882.623 ms** (small, **0.965x**), and **6781.126/6750.499 ms** (medium, **1.005x**). Mixed crop widths limit coalescing, so these are recorded measurements rather than a portability claim. Hybrid correctness was retained on the same corpus: tiny **1.134 s / 1.132 s** (0.994x), small **3.978 s / 3.515 s** (1.132x), and medium **7.024 s / 7.464 s** (0.941x). The hybrid backend still admits each Vulkan segment only after a complete transfer + dispatch + readback no-slower check; the project does not claim a fully GPU-resident graph or enable `gpu_only`. ### 2026-08-12: cross-crop CPU depthwise scheduling The MobileNet depthwise CPU fallback now accepts the leading NCHW batch directly. It flattens independent `(crop, channel)` jobs into one persistent worker-pool pass, while each job continues to use the existing runtime-selected AVX-512, AVX2, NEON, or scalar depthwise kernel. This removes repeated batch loop scheduling around the dominant depthwise backbone stages without changing the per-channel FP32 multiply/add order. The Vulkan depthwise route remains separate and still receives the entire NCHW batch in one dispatch whenever its complete transfer + dispatch + readback admission measurement is no slower than CPU. The extended kernel smoke tests padded/stride-1 `3x3` and padded/stride-2 `5x5` depthwise batches against the established per-image implementation; both the normal AVX-512 run and `PPOCR_DISABLE_AVX512=1` AVX2 fallback passed. The four-image decoded-text comparison with ONNX Runtime remained **4/4** for each tiny, small, and medium model. The five-size batch-result gate likewise remained exact (tiny **130**, small **126**, medium **129** results). On the varied five-page corpus with `rec_batch_size=8` and one recognition worker, this local CPU sample measured serial/batch throughput of **1.096x** (tiny: 1000.988/913.176 ms), **0.992x** (small: 2855.215/2878.902 ms), and **1.006x** (medium: 6922.651/6881.270 ms). Crop-width diversity constrains batch density, so the figures are intentionally recorded as host-specific measurements. Hybrid preserved serial-result identity at **1.011x**, **1.058x** and **0.924x** for tiny/small/medium; GPU segments were retained only when the strict no-slower admission test passed. A renewed five-size/two-cycle memory soak stayed bounded: tiny baseline **99,737,600**, final **+716,800** bytes; small baseline **148,520,960**, final **+1,306,624** bytes; medium baseline **384,311,296**, final **-3,907,584** bytes. ### 2026-08-12: batched ordinary 3x3 convolution scheduler The group-1 non-pointwise convolution path now supports NCHW batches. For the common `3x3` stride-1 and stride-2 OCR shapes it flattens crop and established AVX output-channel tiles into one persistent-pool submission: AVX-512 retains its four/eight-output tile selection; AVX2 retains four-output stride-1 tiles and its stride-2 kernel. ARM/NEON and non-3x3 geometries continue through the existing per-image `Conv2d` path. This changes scheduling only, not the output channel reduction order. `PPOCR_DISABLE_CONV_BATCH=1` restores the previous per-crop dispatch for deployment A/B work. The extended kernel smoke checks stride-1 tail, large AVX-512 tile, and stride-2 ReLU batches against the prior single-image path; both AVX-512 and forced AVX2 runs pass. ONNX Runtime decoded-text comparisons again passed **4/4** for each tiny/small/medium model. On the five-size corpus with one recognizer worker and `rec_batch_size=8`, the latest CPU serial/batch samples were tiny **839.965/822.967 ms** (1.021x), small **2602.329/2656.020 ms** (0.980x), and medium **6507.553/6500.383 ms** (1.001x), all with exact public batch results. A consecutive A/B sample with `PPOCR_DISABLE_CONV_BATCH=1` measured **1.023x / 0.966x / 1.005x** on the same mixed-width suite; host noise and limited same-width coalescing outweigh this small scheduling change there, so no universal speedup is claimed. Hybrid result equivalence held at **1.009x / 0.778x / 0.979x**; Vulkan continues to be selected only by its complete no-slower segment measurement. ### 2026-08-12: batched pointwise HardSwish projection The remaining fused `1x1 Conv -> HardSwish` CPU path now shares the existing cross-crop pointwise SIMD scheduler. It performs all NCHW projections through `PointwiseConvBatch`, then runs one contiguous exact HardSwish pass over the final allocation, instead of issuing the projection/activation once per crop. This preserves the Conv FP32 reduction order and the canonical HardSwish formula; AVX-512, AVX2, NEON, and scalar dispatch remain runtime portable. Kernel smoke now compares both tail and large batched HardSwish projections to the established per-crop routine. Normal AVX-512 and forced AVX2 execution passed. The four-input ONNX Runtime decoded-text gate again passed **4/4** for tiny, small, and medium, and the varied five-page batch gate remained exactly identical to serial results (130 / 126 / 129 results). Latest CPU serial/batch samples on that suite were tiny **901.083/842.421 ms** (1.070x), small **2515.113/2599.535 ms** (0.968x), and medium **6177.222/6239.080 ms** (0.990x). Hybrid samples were **0.996x / 1.250x / 0.972x**. These mixed-width measurements are evidence, not portability claims; each Vulkan segment continues to require its own complete no-slower transfer/dispatch/readback admission. ### 2026-08-12: batched FPN 2x2 transposed convolution The detector's `2x2`, stride-two `ConvTranspose` FPN fast path now accepts NCHW batches. It flattens `(crop, output-channel tile)` into one scheduling pass: AVX-512 keeps its optional four-output reuse tile (or one-output default) and AVX2 uses its established channel kernel; ARM/scalar builds retain the same portable per-image routine. This preserves each output channel's ordered input-channel accumulation and eliminates repeated batch-loop scheduling. `ppocr_kernel_smoke` now validates small tail and large square batch cases against the prior per-image FPN kernel, on both normal AVX-512 and forced AVX2 paths. The tiny/small/medium ONNX Runtime decoded-text checks again passed **4/4** each. The five-size public batch gate stayed serial-identical at 130/126/129 results. Latest CPU serial/batch observations were tiny **808.237/821.381 ms** (0.984x), small **2467.217/2557.340 ms** (0.965x), and medium **6063.649/6071.106 ms** (0.999x). This page mix does not batch detector shapes and has diverse recognition widths, so it should not be read as a universal throughput claim. Hybrid exact-result samples were **1.019x / 0.859x / 0.974x**. The renewed medium five-size/two-cycle soak remained bounded (baseline **384,970,752** bytes; final delta **+3,231,744** bytes), and Vulkan smoke retained strict complete-segment admission. ### 2026-08-12: batched ordinary-convolution Vulkan path Vulkan hybrid execution now also covers ungrouped, non-dilated ordinary NCHW convolution (`3x3` and `2x2` detector forms), in addition to the existing pointwise and depthwise paths. The new shader maps a real crop batch to its Y dimension and writes four adjacent output values per invocation where possible. Learned `[M,C,KH,KW]` weights and `[M]` bias retain their compact layouts and are cached for immutable ONNX initializers; an immediately fused ReLU can complete before the only D2H copy. Asymmetric-stride, grouped, and dilated convolutions deliberately retain the established CPU SIMD path. This remains a *hybrid* primitive, not a GPU-only claim. Every shape is first validated against the CPU batched convolution and admitted only when its complete H2D + dispatch + fence wait + D2H time is no slower. The new Vulkan smoke test covers padded stride-one and stride-two `N=3,C=4,M=7,3x3` batches, including fused ReLU and scalar output tails. On the local AMD Radeon 780M, the larger admission sample (`N=4,C=24,M=40,31x29,3x3,ReLU`) measured **3.53696 ms GPU / 0.83228 ms CPU** after the shared-filter tile optimization, so the production policy correctly kept that shape on CPU rather than adding GPU overhead. After the change, the five-size public batch-equivalence harness preserved serial output identity for all official model sizes under `PPOCR_BACKEND=hybrid` with recognizer batch size 8: tiny **130** results (**1188.492 / 1163.981 ms**, **1.021x** serial/batch), small **126** (**3425.233 / 3507.859 ms**, **0.976x**), and medium **129** (**7949.857 / 8198.539 ms**, **0.970x**). These are host-specific mixed-size scheduler samples, not a universal GPU-performance promise. `gpu_only` remains unavailable until the entire detector and recognizer graph is device-resident. ### 2026-08-12: final-use BatchNormalization storage reuse Standard inference `BatchNormalization` nodes now use the same NCHW channel-affine kernel as fused normalization paths. When graph liveness proves that the input has reached its final use, the executor applies the precomputed `scale / sqrt(variance + epsilon)` and `bias - mean * scale` directly in that dying allocation, then renames it to the output. This avoids one complete activation allocation and copy for each eligible unfused node. The official model bundle folds most normalisation nodes at load time, so this extends compatible PP-OCRv6 graph coverage rather than being represented as a separate current-model speedup. The new `BatchNormAffine` primitive is alias-safe and dynamically dispatches through AVX-512, AVX2, NEON, or scalar `ScaleShift`; it is used in both `Run()` and the recognizer's real `RunCtcTop1()` path. `PPOCR_DISABLE_BATCHNORM_INPLACE=1` is available for deployment A/B checks. Kernel coverage now includes separate out-of-place and in-place batch-affine numeric tests, and passed with both default AVX-512 dispatch and forced AVX2. The post-change C++/ONNX Runtime decoded-text gate passed **4/4** varied images (`16x16`, normal English, `3000x80` strip, noisy low-contrast) for both tiny and medium. A five-size medium two-cycle memory soak remained bounded at a **457,703,424-byte** post-warm-up baseline; cycles ended at **-9,195,520** and **+1,744,896 bytes**, consistent with allocator high-water reuse rather than per-cycle growth. The same five-size hybrid tiny batch equivalence test returned all **130** decoded results identically to serial inference (**1123.880 / 1118.024 ms**, **1.005x** serial/batch). These are validation measurements, not a claim that every normalisation node or GPU segment is faster on every host. ### 2026-08-12: batched Vulkan FPN transpose-convolution path The detector FPN's `ConvTranspose(2x2, stride=2)` projection now has a dedicated batched Vulkan primitive. Its no-padding geometry has no overlap between output tiles, so each output pixel maps directly to one input pixel and one `[Cin,Cout,2,2]` filter tap; this avoids an expanded intermediate. The kernel accepts NCHW batches, caches immutable model parameters in the persistent Vulkan context, and uses one output readback. The command dispatcher was also corrected to submit workgroups (256 four-float invocations per group), rather than accidentally submitting that many times the required groups. The hybrid executor now recognizes this exact ONNX form and uses it only after an end-to-end CPU comparison for the exact batch/shape. The comparison includes H2D, dispatch, fence wait, D2H, and FP32 output validation; rejected shapes continue through the AVX-512/AVX2/NEON/scalar CPU kernel with no output change. Vulkan smoke now validates an odd-size `N=3,Cin=5,Cout=7,11x13` batch against scalar FP32 reference values, including its vector tail. On the local AMD Radeon 780M, the FPN admission sample (`N=4,Cin=32,Cout=40,31x29`) measured **2.48820 ms GPU / 0.60534 ms CPU**, so it was correctly rejected rather than being presented as an OCR speedup. The post-change five-size tiny hybrid batch-equivalence run remained exactly serial-identical with **130** decoded results and measured **1098.189 / 1079.664 ms** serial/batch (**1.017x**). This is a real mixed-size batch validation on this host, not a general GPU throughput claim. `gpu_only` remains unavailable until the full detector and recognizer graphs can stay device-resident. ### 2026-08-12: FPN transpose shared-filter GPU tile and multi-model batch gate The Vulkan FPN transpose primitive now selects a shared-filter workgroup form when its `[Cin,2,2]` row fits the 4,096-float local-memory budget. A workgroup stages the non-contiguous ONNX `[Cin,Cout,2,2]` row into contiguous shared memory once, then computes a 1,024-pixel tile for one output channel. The staging explicitly preserves the original increasing-input-channel FP32 reduction order. Wider exported layers retain the generic batch kernel. The odd `N=3,Cin=5,Cout=7,11x13` Vulkan numerical test continues to compare every result with scalar reference values. On the local Radeon 780M, the complete-transfer FPN admission sample improved from the preceding **2.48820 ms** to **2.15242 ms GPU**; CPU measured **0.45748 ms**, so hybrid still correctly selects CPU for that isolated node. This is intentional: hybrid uses Vulkan when the measured complete segment is equal or faster, rather than increasing CPU offload at the cost of latency. The five-size public hybrid batch gate (`16x16`, normal English, low-contrast, `3000x80`, and dense `1920x1080`; recognizer batch 8, one recognizer worker) remained serial-identical for all official models: small **126** results (**2751.279 / 3384.936 ms**, **0.813x**) and medium **129** results (**7358.610 / 7808.435 ms**, **0.942x**) in this shared-host sample; tiny's same gate was **130** results (**1098.189 / 1079.664 ms**, **1.017x**). The C++/ONNX Runtime medium decoded-text gate also passed **4/4** distinct sizes/aspect ratios (`16x16`, English, low-contrast, wide strip). The repository's paired benchmark scripts now accept `-Model tiny|small|medium`, and `bench/compare_models.ps1` runs all three across the same five-size corpus for deployment-specific ORT comparison. Mixed-size batching is not asserted as a universal speedup; output equivalence and bounded work are the gate. For a fresh paired single-page sanity sample after this GPU-tile change (`en.ppm`, two warm-ups, five runs, 16-thread CPU budget, CPUExecutionProvider for ONNX Runtime), all three official sizes remained faster than ORT: | Model | C++ hybrid mean | ORT CPU mean | C++ improvement | |---|---:|---:|---:| | tiny | 95.490 ms | 128.475 ms | 25.67% | | small | 157.035 ms | 270.218 ms | 41.89% | | medium | 263.851 ms | 478.374 ms | 44.84% | The host is shared and short-run tiny measurements have greater variance; the table is current measured evidence, not a portable guarantee. It uses the same complete detector+recognizer OCR work in both implementations. ### 2026-08-12: terminal CTC guard and scalar-reference coverage `RunCtcTop1()` already avoids materializing the terminal `[N,T,6906]` Softmax activation: it keeps final logits, computes only CTC top-1 indices and probabilities for emitted characters, then releases the logits at function exit. This revision narrows the fast-path eligibility to a verified class-axis terminal Softmax (`axis=-1` or `axis=2`) on the official `[N,T,V]` exports; a terminal Softmax over any other axis now fails fast rather than silently applying CTC semantics to the wrong dimension. The low-level kernel smoke now compares SIMD-dispatched CTC against a scalar reference for both a tail-heavy `N=3,T=17,V=31` case and production-width `N=4,T=41,V=6906`, including blank and repeated-token paths. Validation after the guard: the expanded CTC kernel smoke passed under both default AVX-512 dispatch and forced AVX2; C++/ONNX Runtime decoded text passed **4/4** diverse inputs for tiny and small, while the preceding medium gate was also **4/4**. The five-size tiny hybrid batch gate remained serial-identical at **130** results (**1207.436 / 1297.085 ms**, **0.931x** on this shared host). A medium five-size/two-cycle memory soak stayed bounded at a **449,376,256-byte** post-warm-up baseline; cycles ended at **+7,770,112** and **+5,087,232 bytes**, which is allocator high-water reuse rather than monotonic per-cycle growth. Radeon 780M Vulkan smoke remained numerically passing; its full-transfer policy continued to select GPU only for shapes that measured no slower than CPU. ### 2026-08-12: hybrid detector batch admission and benchmark controls `RecognizeBatch()` now permits its existing exact-shape detector NCHW batches when `Backend::hybrid` is selected as well as on CPU. This does not change resize semantics or pad differently sized pages: pages are still grouped only when their independently calculated detector width and height match exactly. Every Vulkan operator continues to make a separate batch-shape admission decision that includes H2D, dispatch, fence wait, D2H, and numerical validation. A rejected operator uses its CPU SIMD batch kernel, so enabling a larger detector batch never forces a slower partial-GPU path. `ppocr_bench` now reads `PPOCR_DET_BATCH_SIZE`; `ppocr_batch_smoke` also reports both effective detector and recognizer batch sizes (and accepts `PPOCR_REC_WIDTH_BUCKET`). This makes the public batch path reproducible for deployment A/B work. On the local Radeon 780M system, eight equal `en.ppm` pages, one page worker, `rec_batch_size=8`, and `det_batch_size=8` remained exactly serial-equivalent. One fresh steady-state sample measured CPU **296.587 / 181.402 ms** (**1.635x**) and hybrid **343.533 / 219.900 ms** (**1.562x**) for serial versus batched execution. Hybrid with `det_batch_size=1` measured **274.675 / 232.065 ms** (**1.184x**) in a separate run, so this host benefited from allowing the same-shape detector batch. These are host-specific measurements, not a claim that every GPU or page mix will have the same ratio. Regression coverage after the change: normal and AVX2-forced `ppocr_kernel_smoke` passed; the Vulkan primitive smoke passed on AMD Radeon 780M; and decoded text matched ONNX Runtime for all four varied images in each of tiny, small, and medium (**4/4 per model**). `gpu_only` remains explicitly unavailable until the full detector and recognizer graph is device-resident. ### 2026-08-12: fused Vulkan FPN resize/add batch primitive The FPN's graph-proven `Resize(nearest, 2x) -> Add(lateral)` sequence now has a Vulkan `NCHW` batch primitive. It uploads the small source map and lateral residual once, generates each nearest-neighbour value and sums it in one shader dispatch, then reads only the completed result. The short-lived expanded resize tensor is therefore never materialized on either CPU or GPU. Odd dimensions and four-page batch addressing are covered by the Vulkan smoke test. Hybrid admission is shape-specific and strict: the measured path includes source/residual H2D copies, shader dispatch, fence wait, D2H copy, and every FP32 output comparison against `NearestResize2xAdd`. On this AMD Radeon 780M, the representative `N=4,C=24,31x29` probe measured **0.47230 ms GPU** versus **0.06686 ms CPU**, so it is correctly retained on AVX-512 CPU rather than being forced onto the GPU. This primitive is nevertheless batch-ready for hardware where the complete path wins. Post-change validation: default and forced-AVX2 kernel smoke passed; Vulkan smoke passed; tiny CPU and hybrid decoded-text comparison against ONNX Runtime passed **4/4** varied inputs; and CPU tiny five-size/two-cycle memory soak finished at **-139,264** and **-13,721,600 bytes** relative to a **146,825,216-byte** post-warm-up baseline. A fresh eight-identical-page, batch-8 smoke sample was exact-equivalent to serial: CPU **303.220 / 181.519 ms (1.670x)** and hybrid **336.976 / 213.733 ms (1.577x)**, serial/batched. For a single `en.ppm` page with batch sizes one, CPU averaged **37.206 ms**; hybrid averaged **37.649 ms**, so this shared host's current partial offload does not claim a whole-pipeline GPU gain. `gpu_only` remains unavailable until the entire detector and recognizer graphs are device-resident. ### 2026-08-12: recognizer CTC-path lifetime and Vulkan GEMM coverage The production `RunCtcTop1()` recognizer executor now directly runs fused `1x1 Conv + shortcut` nodes, matching the ordinary executor rather than falling through its generic node path. This removes graph-input-vector setup on the dense residual path and releases the source feature map immediately after the fused kernel writes its destination. It is shared by tiny, small, and medium; the three-model CPU/hybrid decoded-text regression against ONNX Runtime remained **4/4 per model** over tiny, normal English, noisy low-contrast, and ultra-wide input dimensions. Vulkan additionally now implements row-major projection GEMM (`[R,K] x [K,C] + bias`) for recognizer attention and CTC-head-compatible layouts. It has a numerical smoke for odd `37x29x103` dimensions and a strict full-boundary admission probe. The Radeon 780M measured the representative `64x128x512` case at **0.67616 ms GPU** versus **0.15548 ms CPU**, so hybrid correctly keeps this shape on CPU. The same smoke measured GPU wins for selected batch depthwise and affine+Swish shapes, which hybrid admits automatically. This keeps GPU work data-driven and prevents an isolated GPU primitive from silently increasing complete-OCR latency. The AVX-512 FPN resize/add kernel was also tightened to load one contiguous 16-float source vector per iteration before two indexed broadcasts; default and forced-AVX2 kernel smoke pass. On a fresh 16-worker `en.ppm` run (three warmups, ten timed runs), CPU means were **33.535 / 68.351 / 215.205 ms** and hybrid means **32.155 / 76.156 / 258.877 ms** for tiny/small/medium. Current paired ONNX Runtime means from the same session were **176.288 / 229.349 / 572.251 ms**, so CPU remains the deployment recommendation for small/medium on this UMA GPU while retaining a large native-vs-ORT advantage. A five-size tiny batch gate (`16x16`, English, low contrast, `3000x80`, dense `1920x1080`; batch 8) returned all **130** results identically. This mixed corpus measured **874.808 / 987.314 ms** CPU serial/batched and **1096.915 / 1107.025 ms** hybrid serial/batched, demonstrating that heterogeneous work is not mislabeled as a throughput win. Its two-cycle memory soak stayed below its **146,587,648-byte** post-warm-up baseline by **14,483,456** and **14,401,536 bytes**. `gpu_only` remains explicit-unavailable until complete detector and recognizer device residency is implemented and verified. ### 2026-08-12: CTC residual GPU admission and true page-batch validation `RunCtcTop1()` is the production recognizer path, so its fused `1x1 Conv + shortcut` blocks now use the same batched Vulkan admission path as the generic executor. This closes a policy gap where recognizer residual blocks could not be considered for hybrid execution even when their exact NCHW batch shape had already been measured. The decision remains conservative and shape-local: Vulkan is used only after the full input/residual upload, dispatch, fence wait, readback, and CPU-SIMD numerical check are no slower than the CPU block. The change was verified on AMD Radeon 780M with kernel SIMD smoke, Vulkan primitive smoke, and identical decoded CPU OCR output. A four-page equal-size tiny batch (`4 x en.ppm`, `det_batch=4`, `rec_batch=4`, one page-scheduler worker) returned all eight text lines identically and measured: | Backend | Serial pages | True NCHW page/crop batch | Speedup | |---|---:|---:|---:| | CPU | 148.554 ms | 91.244 ms | 1.628x | | Hybrid | 138.904 ms | 94.528 ms | 1.469x | This is a same-shape batching result, not a claim for arbitrarily mixed page sizes. The existing scheduler still groups detector inputs only when their native PP-OCR resize shape matches and buckets recognizer crops by padded width, avoiding semantic changes from artificial image padding. Fresh `en.ppm` end-to-end measurements (16 CPU workers, 3 warmups, 10 timed runs) show the native engine remains faster than the paired CPU ONNX Runtime baseline. These are host-specific AMD Radeon 780M / AVX-512-host measurements, not portable guarantees: | Model | C++ CPU | C++ hybrid | ORT CPU | CPU latency reduction vs ORT | |---|---:|---:|---:|---:| | tiny | 38.215 ms | 37.812 ms | 127.162 ms | 69.9% | | small | 78.114 ms | 76.542 ms | 220.022 ms | 64.5% | | medium | 220.072 ms | 240.608 ms | 487.076 ms | 54.8% | The medium hybrid result is intentionally reported as slower than CPU. Its individual Vulkan segments may still be admitted only where their complete boundary beats SIMD; varying transfer/cache pressure on this shared UMA host can make end-to-end hybrid slower. `gpu_only` still fails explicitly because the entire PP-OCRv6 graph is not yet Vulkan-resident. ### 2026-08-12: default CPU worker budget tuned for PP-OCR batches The persistent SIMD worker pool now defaults to **12** workers (still overrideable with `PPOCR_BENCH_THREADS`). This is a measured scheduling/cache decision, not a hardware-core-count assumption: on the local AVX-512 host, the default PP-OCR graph's short detector/recognizer channel tiles saturated useful memory bandwidth before 16 workers and the extra threads made all three model sizes slower. The public crop scheduler default is aligned to the same 12-worker budget, avoiding nested page-level oversubscription. On `en.ppm` with CPU-only execution, three warmups and eight timed runs, the thread sweep measured the following mean end-to-end latency: | Model | 8 workers | 12 workers (new default) | 16 workers | |---|---:|---:|---:| | tiny | 30.140 ms | 32.457 ms | 35.257 ms | | small | 70.752 ms | 62.472 ms | 71.507 ms | | medium | 226.918 ms | 215.528 ms | 226.332 ms | Tiny stays fastest at eight workers on this host, while the new default gives the best balanced small/medium result and avoids the former 16-worker regression. Deployments optimizing only tiny can use `PPOCR_BENCH_THREADS=8`; larger-server CPUs can continue to tune the existing override. Post-change validation covered default and forced-AVX2 kernel smoke, Radeon 780M Vulkan smoke, all three `en.ppm` models, and true NCHW batches. The five-size corpus (`16x16`, English, low contrast, `3000x80`, dense `1920x1080`) decoded **130** tiny-model results identically in CPU and hybrid batch scheduling. Its CPU serial/batched timing was **446.898 / 408.895 ms**; hybrid was **805.625 / 440.226 ms**. The two-cycle tiny memory soak over that same five-size corpus ended **10,407,936** and **11,051,008 bytes below** its 128,106,496-byte post-warm-up baseline. ### 2026-08-12: fused Vulkan recognizer GEMM + Swish The Vulkan recognizer projection primitive now has a `GEMM + Swish` mode for the graph-fused MLP projection. It accepts the flattened rows of a real crop batch, caches immutable weights/biases as before, and applies the exact Swish while writing the GPU result. This removes the former complete CPU-side Swish traversal after a successful hybrid GEMM dispatch. The shader path is covered by an odd-tail `41x29x103` numerical smoke case and an end-to-end admission probe against CPU SIMD `GEMM + Swish`; hybrid still uses it only when the full upload, dispatch, fence, and readback time is no slower. This is additional batch-aware GPU coverage, not a claim that the complete PP-OCR graph is yet device-resident or that `gpu_only` is available. Validation after the change passed the normal and forced-AVX2 kernel smoke suites, plus the Radeon 780M Vulkan suite (including the new numerical GEMM+Swish tail case). On that smoke run the complete-boundary GEMM+Swish probe measured **0.65962 ms GPU vs 0.15050 ms CPU**, so the dynamic policy correctly retained CPU for that transfer-bound shape. Tiny-model decoded-text comparison against ONNX Runtime again passed **4/4** diverse inputs. Four identical `en.ppm` pages also remained serial-equivalent: CPU measured **151.283 / 103.396 ms (1.463x)** serial/batched, hybrid **133.678 / 109.307 ms (1.223x)**. These are local samples; the stricter per-segment admission rule remains the GPU performance authority. ### 2026-08-12: terminal CTC projection memory experiment The recognizer executor now contains an optional experimental `PPOCR_ENABLE_FUSED_TERMINAL_CTC=1` path that computes a terminal `MatMul(+bias) -> Softmax -> CTC Top-1` directly from one reusable vocabulary row buffer. It eliminates the transient `[N,T,V]` logits allocation and preserves the dense projection's K-order plus exact selected-class Softmax probability. It is deliberately opt-in: on the local AVX-512 CPU the scalar vocabulary-row implementation is not a stable latency win for every official model, although it is useful for memory-constrained deployments and passed the text regression on the exercised corpus. The default continues to use the proven SIMD GEMM plus vectorized CTC scan until a native SIMD fused terminal kernel demonstrates a three-model speedup. The row-buffer path is now covered in the SIMD kernel smoke with both a small odd-tail projection and a `4x41x6906` recognizer-sized projection. The portable regression corpus was also expanded beyond the original four shapes: blank `800x600`, English and CJK single lines, a mixed three-line receipt, and two-line CJK all decode with ONNX Runtime text parity for tiny, small, and medium (**15/15 per model**). Deliberately adversarial rotation and extreme portrait-strip images are retained as diagnostic samples, but are not used as decoded-text parity gates because the two independently implemented detector postprocessors can select different unstable low-confidence boxes. ### 2026-08-12: cooperative Vulkan GEMM tile and larger page batches The Vulkan recognizer GEMM now has a cooperative **8-row 鑴?128-column** FP32 tile for ordinary (non-Swish) projections. Each workgroup stages a 16-wide K slice of A and B, reusing B across eight rows rather than rereading it per output element. The kernel keeps K tiles in ascending order, retains the existing scalar-tail path, and is selected only after the existing exact CPU-versus-full-boundary admission check. `gpu_only` remains unavailable: this is a batch-aware primitive within the hybrid executor, not a device-resident PP-OCR graph. The new path is covered by Vulkan numerical smoke with a tail-heavy `13鑴?7鑴?73` GEMM (rows, K, columns), in addition to the existing odd-tail and GEMM+Swish cases. Normal SIMD smoke and Vulkan smoke passed on the local AVX-512 / Radeon 780M host. Hybrid decoded-text parity against ONNX Runtime again passed 4/4 diverse inputs for both tiny and medium (`16x16`, English, low-contrast, and `3000鑴?0`). For an eight-page same-shape `en.ppm` batch with `det_batch_size=8`, `rec_batch_size=8`, and one recognizer scheduler worker, the post-change hybrid measurements were tiny **304.188 / 225.182 ms (1.351x)** and medium **2029.114 / 2019.849 ms (1.005x)** serial/batched. A four-shape hybrid batch gate (`16x16`, English, low-contrast, `3000鑴?0`; batch sizes 4) remained serial-identical for all models: tiny **186.390 / 176.957 ms (1.053x)**, small **379.836 / 371.670 ms (1.022x)**, medium **1209.497 / 1215.288 ms (0.995x)**. These are one host's steady-state samples, not a universal GPU claim; the per-shape GPU admission continues to reject transfer-bound work. After the latest pass, default CPU/hybrid batch regression again produced serial-identical results across four different page shapes (`16x16`, English, low-contrast, and `3000x80`). With bounded `det_batch_size=4` and `rec_batch_size=4`, the local CPU/hybrid batch timings were tiny **176.722/185.384** and **170.369/165.231 ms**; small **343.559/301.114** and **374.170/293.868 ms**; medium **1317.190/1291.101** and **1163.054/1122.104 ms**. A medium two-cycle varied-size memory soak started at **495,898,624 bytes** after warm-up and ended **9,752,576** then **10,174,464 bytes below** that baseline. These are host-local measurements, not a general ONNX Runtime comparison claim. ### 2026-08-12: optional PP-OCRv6 vector GELU validation The existing optional recognizer vector tanh-GELU path (`PPOCR_APPROX_GELU=1`) was revalidated on AVX-512/AVX2; NEON/scalar retains its portable fallback. This removes scalar `erf` from the MLP activation hot path. The exact-Erf ONNX path remains the default, and the model-specific vector path was checked against ONNX Runtime decoded text rather than being assumed numerically interchangeable. With 12 worker threads, three warmups and ten `en.ppm` runs, the scalar-Erf baseline measured **29.097 / 60.268 / 202.201 ms** for tiny/small/medium; the optional vector path measured **23.577 / 53.054 / 191.974 ms** in the same session (**19.0% / 12.0% / 5.1%** lower mean latency). A longer shared-host 30-run medium sample was noisier, so the 10-run paired sweep is recorded as an optimization signal, not a portable latency guarantee. The decoded-text parity gate passed **4/4** diverse dimensions against ONNX Runtime for each official model size (tiny, small, medium): `16x16`, English, low-contrast, and `3000x80` ultra-wide. CPU and hybrid also produced identical Chinese tiny-model text for the CJK sample. The kernel smoke still passes with normal AVX-512 dispatch and forced AVX2 fallback. ### 2026-08-12: AVX-512 batch-GEMM dispatch refinement The AVX-512 dense-GEMM path now selects its four-row by 64-column micro-kernel per projection shape instead of applying its extra register pressure to every recognizer tier. It stays enabled only for `K >= 384` (and can be disabled for A/B with `PPOCR_DISABLE_AVX512_GEMM4ROW=1`); AVX-512 閳?AVX2 閳?scalar/NEON runtime dispatch remains unchanged for all x86 and Arm targets. This keeps large medium-model projections eligible to reuse each immutable B vector over four output rows, while tiny/small use the lighter proven path. Fresh `en.ppm` CPU measurements (three warm-ups, ten runs, 12 kernel workers) were tiny **29.699 ms**, small **59.577 ms**, and medium **196.789 ms**. The previous all-shape four-row experiment measured small **82.637 ms** and medium **267.150 ms** in the same benchmark configuration, so the new admission guard is retained. A dense `1920x1080` medium page (122 boxes, `rec_batch_size=4`, `rec_parallelism=4`, one warm-up, three runs) measured **4,496.430 ms** with the wide tile enabled and **4,426.272 ms** with it disabled; therefore the dense-page default now avoids that tile whenever its projection depth is below the measured `K=384` cutoff. These are host-local figures, not an ONNX Runtime claim. The normal AVX-512 kernel smoke passed after the change, as did the complete Radeon 780M Vulkan smoke suite. The latter again verified all GPU batch primitives numerically and dynamically admitted only full-boundary winners: affine+Swish **0.17342 vs 0.34950 ms**, depthwise **0.34814 vs 0.47170 ms**, depthwise+Swish **0.35624 vs 0.60330 ms**, and depthwise+HardSwish **0.34528 vs 0.41760 ms** (GPU vs CPU). Transfer-bound GEMM and other primitives stayed on CPU; `gpu_only` is still explicitly unavailable until the complete PP-OCR graph is device-resident. ### 2026-08-12: diverse-size regression and preprocessing arithmetic reduction The preprocessing resize path now folds PP-OCRv6's fixed input normalization into precomputed per-channel affine coefficients. It keeps the existing bilinear rounding, RGB-to-BGR layout, detector mean/std, and recognizer `[-1,1]` arithmetic, while removing three per-pixel floating-point divisions from detector and crop preparation. This applies to serial OCR and true `[N,3,H,W]` detector/recognizer batches, without adding a temporary image buffer or changing the x86 AVX-512/AVX2/scalar/NEON runtime dispatch. The post-change ONNX Runtime decoded-text gate passed **8/8** diverse PPM sizes for every official model (**24/24 total**): `16x16`, English, noisy low-contrast, `3000x80`, English/CJK single lines, mixed text, and CJK. A four-page CPU batch smoke across the smallest, normal, low-contrast, and ultra-wide samples remained byte-for-byte result equivalent to serial OCR and measured **190.614 / 176.096 ms (1.082x)** serial/batched for tiny. This explicitly exercises multiple aspect ratios rather than using a same-page-only batch claim. A fresh `en.ppm` three-warmup/ten-run snapshot (12 kernel workers) measured CPU tiny **35.073 ms**, small **72.582 ms**, medium **244.095 ms**; hybrid measured **34.887 / 72.036 / 244.847 ms**. The hybrid implementation still selects Vulkan only after per-shape numeric validation of the complete upload, dispatch, fence, and readback boundary; therefore it may reduce CPU work without claiming a universal latency gain on UMA hardware. `gpu_only` remains an explicit failure until all PP-OCRv6 operators are device-resident. The expanded five-size memory soak (`16x16`, English, low-contrast, `3000x80`, dense `1920x1080`) was also run twice per model. It showed no unbounded retained activation frontier: tiny ended **14.63 MiB below** its 146,534,400-byte warm baseline; small and medium ended **8.66 MiB** and **11.69 MiB** above their 178,892,800-byte and 425,119,744-byte baselines. Those modest Windows allocator deltas are reported rather than hidden; a future allocator-pool change must improve this measured dense-page result without weakening the 24-case text-parity gate. ### 2026-08-12: dense recognizer GEMM tile admission refinement The medium dense-page operator profile identified transformer dense projection as the largest recurring recognizer cost (`FusedMatMulBias` was **28.685 ms** for one profiled crop batch), ahead of any scalar postprocessing. The AVX-512 four-row GEMM micro-kernel therefore now requires both `K >= 384` **and** `N >= 384`. This keeps it on the PP-OCRv6 medium MLP expansion `192x768` while retaining the lower-register-pressure two-row implementation for the paired `768x192` projection. The runtime CPU hierarchy is unchanged: AVX-512 is selected only after runtime CPUID/XCR0 checks, then AVX2/FMA, then portable scalar/NEON. On the dense `1920x1080` medium UI (122 boxes, `rec_batch_size=4`, `rec_parallelism=8`, eight kernel workers, one warm-up/three runs), the refined default measured **4,328.474 ms**; disabling the four-row tile for A/B gave **5,303.048 ms** (with a 6,549 ms shared-host tail). On `en.ppm`, the same model measured **210.125 ms** versus **210.604 ms** disabled across three warmups/ten runs. The normal AVX-512 kernel smoke, all three models' eight-shape ONNX Runtime decoded-text gates (**24/24**), the Radeon 780M Vulkan batch smoke, and a varied-shape hybrid batch identity check all passed. The hybrid check continues to admit Vulkan only after complete-boundary validation; in this sample it selected affine+Swish and depthwise variants where GPU was faster, while keeping transfer-bound work on CPU. ### 2026-08-12: GPU-aware NCHW batch benchmark controls `ppocr_batch_smoke` now supports repeatable steady-state measurements through `PPOCR_BATCH_WARMUP` and `PPOCR_BATCH_RUNS`. It still verifies that every batched page result (text, confidence, axis-aligned box, and four-point box) matches serial inference exactly, then reports mean and median serial/batch times. This exercises the existing real `[N,3,H,W]` detector batches and cross-page recognizer crop batches; it is not a scheduler-only benchmark. On the local Radeon 780M / AVX-512 host, four identical `en.ppm` tiny pages with `det_batch_size=4`, `rec_batch_size=4`, one outer worker, two warm-ups, and five timed runs produced eight serial-identical results. CPU measured **192.813 / 137.304 ms** serial/batched (**1.404x**); hybrid measured **175.306 / 125.126 ms** (**1.401x**). The hybrid result remains governed by per-shape full H2D + dispatch + fence + D2H validation, so a GPU segment is used only when it measured no slower than SIMD CPU for that exact batch shape. The same hybrid configuration, with two warm-ups and three timed runs, gave small **330.788 / 308.714 ms** (**1.072x**) and medium **991.291 / 1061.823 ms** (**0.934x**) serial/batched. The medium result is intentionally retained: on this shared UMA host, combining page/crop work can lose to cache pressure and transfer-bound segments. Batch support therefore does not claim a universal GPU gain, and `gpu_only` remains unavailable until the full graph is device-resident. The latest run also reconfirmed dynamic high-ISA dispatch. With 12 kernel workers and three warm-ups/six `en.ppm` runs, shape-gated AVX-512 pointwise eight-output tiling measured tiny/small/medium **35.979 / 76.015 / 252.816 ms**. Disabling only that tile via `PPOCR_DISABLE_AVX512_POINTWISE8=1` measured **36.286 / 81.219 / 268.576 ms**. The tile remains limited to `C_in >= 192`, `C_out >= 192`, and plane >= 256; AVX-512 selection is runtime CPUID/XCR0-gated, followed by AVX2 and portable scalar/NEON fallbacks. ### 2026-08-12: repeatable batch, Vulkan, memory, and model-size regression This pass kept the production CTC executor's terminal-logit memory mode opt-in (`PPOCR_ENABLE_FUSED_TERMINAL_CTC=1`): it replaces a full terminal `[N,T,V]` logits activation with a per-sequence row workspace, but remains off by default because the regular SIMD GEMM path is faster on this host. This preserves the lower peak-memory option without making an unmeasured throughput trade-off the default. The complete CPU-side five-size private-memory soak was rerun with 12 kernel workers over `16x16`, English, low-contrast, ultra-wide `3000x80`, and dense `1920x1080` inputs. After all shapes were warmed, tiny finished cycle two **10.09 MiB below** its **126,599,168-byte** baseline. Small and medium one post-warm cycles reported **+4.07 MiB** from **178,348,032 bytes** and **+10.47 MiB** from **425,025,536 bytes** respectively. These allocator deltas are reported as observed rather than treated as a leak verdict. The decoded-text ONNX Runtime regression passed the full eight-shape corpus for tiny and small (**8/8 each**), and the cross-model boundary subset (`16x16`, English, low-contrast, `3000x80`) passed medium (**4/4**). The Radeon 780M Vulkan primitive suite also passed numerical validation. In its latest sample hybrid admitted affine+Swish (**0.18734 vs 0.37020 ms** GPU/CPU), depthwise+Swish (**0.33626 vs 0.61096 ms**), and depthwise+HardSwish (**0.31810 vs 0.34620 ms**); transfer-bound projection, GEMM, ordinary Conv, and resize work stayed on CPU automatically. Finally, a paired six-run medium English benchmark with two warm-ups and 12 kernel workers measured pure C++ CPU **259.037 ms** versus ONNX Runtime CPU **297.913 ms** (**13.1% lower latency**, **3.860 vs 3.357 FPS**). The ORT sample had shared-host variation, so this is a current reproducible snapshot, not a universal multiplier. ### 2026-08-12: optional large-map SIMD GELU admission `PPOCR_APPROX_GELU=1` now applies the PP-OCR-specific vector GELU approximation only to tensors with at least 65,536 values. This avoids the short, decode-sensitive boundary activations while allowing the AVX-512/AVX2 path to amortize its approximation on genuine large feature maps. Exact Erf-GELU remains the default, and Arm/NEON remains on the portable exact path. With 12 kernel workers, three warm-ups, and ten English-page runs, the gated opt-in mode measured tiny/small/medium **27.295 / 53.594 / 204.034 ms**, versus exact-default **28.491 / 61.181 / 214.164 ms** in the paired session: **4.2% / 12.4% / 4.7%** lower latency. The opt-in decoded-text gate passed tiny **8/8**, small **8/8**, and medium **4/4** across the same diverse dimensions used above. A mixed-size hybrid page/crop batch also remained serial-identical (four pages, seven results); it measured **161.524 / 164.530 ms** serial/batched on this heterogeneous mix, so no universal batch speedup is claimed for unlike page shapes. ### 2026-08-12: Vulkan batch revalidation After rebuilding the Vulkan batch path, the Radeon 780M primitive smoke test again passed numerical validation. The complete-boundary policy kept the transfer-bound binary, pointwise, depthwise, GEMM, ordinary-convolution and resize probes on CPU; affine+Swish was the only selected GPU primitive in the latest pass (**0.42584 vs 0.43290 ms** GPU/CPU). These are per-primitive measurements including H2D, dispatch, fence and D2H, not a claim that the whole OCR graph is GPU-resident. On four same-shape `en.ppm` pages with hybrid selected, one page worker, `det_batch_size=4`, `rec_batch_size=4`, one warm-up and three timed runs, the serial/batched comparison was **148.272 / 115.169 ms** (**1.287x**). The result check remained exact at the public OCR result level (text, box and confidence). The scheduler sends the real `N=4` tensor to both the CPU SIMD and Vulkan primitive batch dimensions; heterogeneous inputs remain governed by the existing width/shape buckets instead of being padded arbitrarily. ### 2026-08-12: GPU batch-path cleanup and recheck The unvalidated experimental pointwise-to-depthwise Vulkan chain was removed. It had never been reachable from the hybrid executor and had previously failed its numerical validation, so retaining it would have overstated device-resident graph support. The production Vulkan shader now contains only smoke-tested primitive modes. `gpu_only` therefore continues to reject execution explicitly; the implementation must not silently fall back to CPU until the complete PP-OCRv6 graph, DB post-processing, and CTC decode can remain on the device. After the cleanup, the Radeon 780M Vulkan primitive smoke and AVX-512 kernel smoke both passed. The same four-page tiny English batch (`det_batch_size=4`, `rec_batch_size=4`, one outer worker, one warm-up, three timed runs) remained serial-identical: CPU was **140.573 / 100.661 ms** serial/batched (**1.396x**), and hybrid was **135.904 / 99.425 ms** (**1.367x**). A hybrid ONNX Runtime text gate also passed **4/4** diverse tiny inputs (16x16, English, low-contrast, and 3000x80 ultra-wide). These figures exercise the public real-N detector and recognition batches; they do not claim a complete GPU-resident inference path. ### 2026-08-12: fused approximate BatchNorm-GELU When the existing opt-in `PPOCR_APPROX_GELU=1` mode is enabled, a large fused BatchNorm-GELU group now performs its channel affine and AVX-512/AVX2 Pad鑼?GELU approximation in one vector pass. This removes the previous per-plane affine write/read round trip. The normal exact-Erf path is unchanged; non-x86/NEON builds also retain its exact behavior. Set `PPOCR_DISABLE_FUSED_BN_GELU=1` only to reproduce the former two-pass experimental implementation for A/B measurement. On the local AVX-512 host with 12 worker threads, two warm-ups and six English runs, the fused mode measured tiny **27.106 ms**, small **51.901 ms**, and medium **225.597 ms**. The former two-pass opt-in path measured **27.241 / 62.450 / 232.207 ms** respectively, a material small-model reduction and a positive medium improvement in that paired session. Decoded-text regression against ONNX Runtime passed the eight-size corpus for tiny, small, and medium (**24/24** total): 16x16, English, low-contrast, ultra-wide, English/CJK single-line, mixed text, and CJK. The broad requirement to outperform ONNX Runtime on every model and workload remains a continuing target, not a claim from this one host-local optimization sample. ### 2026-08-12: batch-invariant GELU admission correction The optional SIMD GELU admission was tightened so batching cannot change a page's numerical execution path. Generic GELU is now always exact, because it has no NCHW shape information and a total-element threshold could make a singleton crop use Erf while the same crop in a batch used the approximation. The model-aware fused BatchNorm+GELU path continues to use its AVX-512/AVX2 one-pass approximation only when each NCHW plane is at least 65,536 elements; this predicate is invariant to the leading batch dimension. Exact GELU remains the default and ARM/NEON retains the exact fallback. After the correction, kernel and Radeon 780M Vulkan smoke both passed. The hybrid decoded-text ONNX Runtime gate passed **4/4** tiny diverse inputs (16x16, English, low contrast, and 3000x80), with `PPOCR_APPROX_GELU=1`. The public four-page `en.ppm` batch gate also restored exact serial/batch results at `det_batch_size=4`, `rec_batch_size=4`, and one recognition worker: **169.174 / 123.454 ms (1.370x)** for its three-run hybrid sample. A longer ten-run local snapshot measured CPU **165.446 / 118.013 ms (1.402x)** and hybrid **210.649 / 142.646 ms (1.477x)**. These shared-host figures are batch throughput evidence, not an end-to-end GPU-only performance claim; hybrid still admits only individually validated full-boundary Vulkan segments. ### 2026-08-12: MatMul write-only activation allocation The constant-weight transformer MatMul path now allocates its destination with `resize()` rather than value-initializing it to zero. `kernels::Gemm()` writes every output element from its bias/zero accumulator, so the previous clear was dead memory traffic before the SIMD or Vulkan attempt. This reduces activation write bandwidth and allocator work for the recognizer's tall batch-folded projections without changing the CPU, hybrid, or ARM/NEON fallback semantics. Post-change kernel and Vulkan smoke passed, including the Radeon 780M full-boundary admission checks. Hybrid decoded-text parity against ONNX Runtime passed the eight-size corpus for every supported model (**24/24**): 16x16, normal English, low contrast, 3000x80, English/CJK lines, mixed text, and CJK. A separate eight-image mixed-aspect tiny hybrid batch check returned all 14 serial-identical OCR results at **264.126 / 256.614 ms (1.029x)**; this is a diversity/correctness gate and is intentionally not represented as a universal throughput claim. For a current 12-worker, three-warmup/twenty-run `en.ppm` CPU comparison on this shared host, the measured C++ / ONNX Runtime means were tiny **34.049 / 85.073 ms** (60.0% lower C++ latency), small **75.880 / 106.510 ms** (28.8% lower), and medium **249.121 / 210.750 ms** (18.2% higher). The medium sample does not satisfy the project performance target and is recorded explicitly: the remaining work is medium recognizer/detector optimization and longer device-resident Vulkan segments, not a claim that this pass has already made all models faster than ONNX Runtime. ### 2026-08-12: multi-adapter Vulkan selection for batched hybrid execution The Vulkan backend now evaluates all compute-capable adapters instead of blindly selecting Vulkan's first enumerated queue. Its default selection is the integrated/UMA adapter, which is the appropriate default for the current per-segment H2D/dispatch/D2H hybrid design; set `PPOCR_VULKAN_PREFER_DISCRETE=1` to prefer a discrete accelerator. Deployment integration can pin an adapter with `PPOCR_VULKAN_DEVICE_INDEX=N` or select it by substring through `PPOCR_VULKAN_DEVICE_NAME=...`. CPU Vulkan drivers are not chosen automatically, but may still be requested by index for diagnostic purposes. Every selected operator remains gated by its exact shape/batch, full-boundary no-slower-than-CPU measurement. After this change, the Radeon 780M Vulkan smoke suite and AVX-512 kernel smoke both passed. A four-page same-shape tiny batch (`det_batch_size=4`, `rec_batch_size=4`, hybrid, one page worker, two warmups/five timed runs) returned all eight public OCR results identically. The shared Windows host showed unstable timing in the immediate post-build sample (**170.452 ms serial / 219.688 ms batch; batch median 119.245 ms**), so no new throughput claim is made from that run. It nevertheless validates real `[N,3,H,W]` detector and crop batches on the selected GPU path; it does not represent a GPU-only claim because the complete graph, DB post-processing, and CTC decode are still not device-resident. ### 2026-08-12: GPU batch front-end parallelism The public batched API now exposes `Options::batch_preprocess_parallelism` (default **4**), with the benchmark override `PPOCR_BATCH_PREPROCESS_PARALLELISM`. For a same-shape detector batch, each page's resize/BGR-normalize write owns a separate contiguous NCHW plane and is prepared concurrently before the one batched CPU/Vulkan graph submission. The budget is divided across concurrent dynamic-shape detector batches, so raising `image_batch_parallelism` does not cause unbounded nested-thread oversubscription. It is CPU front-end work that enables GPU/SIMD batching; it does not change the strict Vulkan transfer/admission policy or imply a GPU-only graph. On the local Radeon 780M / AVX-512 host with the tiny model, four identical `en.ppm` pages, `det_batch_size=4`, `rec_batch_size=4`, one page worker, `PPOCR_APPROX_GELU=1`, three warm-ups, and ten timed hybrid runs, the serial-identical public result check passed in both configurations. Serial preprocess (`PPOCR_BATCH_PREPROCESS_PARALLELISM=1`) measured **121.887 ms** batched mean (**1.479x** against **180.328 ms** serial); the default four-way front end measured **119.919 ms** (**1.498x** against **179.646 ms** serial). This is a **1.6%** improvement in that controlled local batch sample. Timing on the shared Windows host remains variable, so the README records it as a reproducible configuration-specific result rather than a universal GPU gain. ### 2026-08-12: experimental Vulkan tiled GEMM+Swish batch kernel The Vulkan shader now includes an 8-row x 128-column cooperative GEMM tile with Swish fused into its final writeback (mode 25). It shares a 16x128 weight tile across eight flattened recognition rows, so it is designed for real same-width crop batches rather than per-crop dispatches. Its FP32 K reduction remains ascending, and the shader stays behind the existing numerical smoke and complete H2D + dispatch + fence + D2H admission checks. Enable it only for driver-specific A/B work with `PPOCR_ENABLE_VULKAN_GEMM_SWISH_TILE=1`; the plain tiled GEMM path remains dynamically selected as before. On the local Radeon 780M, AVX-512 host, the experimental tile passed kernel and Vulkan primitive smoke plus the hybrid ONNX Runtime decoded-text gates for tiny and medium (4/4 diverse inputs each). A quick medium four-page A/B saw the tile at **1004.444 ms** batched mean and the default at **1041.713 ms**; the longer shared-host samples varied enough that neither result is promoted to the default. The default public medium batch remained result-identical and measured **1178.757 / 1061.702 ms** serial/batched (**1.110x**) with two warm-ups and six timed runs. This is an implementation and correctness milestone, not a claim of a universal GPU speedup; hybrid continues to select GPU only for primitives that win their exact full-boundary admission measurement. ### 2026-08-12: recognition-batch front-end parallelism `batch_preprocess_parallelism` now also prepares same-width recognition crops in parallel. Each resize owns one already-zeroed, contiguous `[N,3,48,W]` plane; output order, natural resize width, right padding, and normalization arithmetic are unchanged. The front-end budget is divided by concurrent recognizer batches, avoiding nested oversubscription when `rec_parallelism > 1`. On the local Radeon 780M / AVX-512 host, four same-shape tiny English pages with hybrid mode, `det_batch_size=4`, `rec_batch_size=4`, one recognizer worker, two warm-ups and eight timed runs were public-result identical. A serial front end measured **112.023 ms** batch mean; four-way front-end work measured **105.894 ms**, a **5.5%** batch-latency reduction. The corresponding serial/batch speedups were **1.523x / 1.577x**. A five-run small-model check remained result-identical too (**314.876 / 313.329 ms** batch at one/four-way preprocess); it is kept as evidence that the gain is input/model dependent, not as a universal throughput claim. Kernel and Vulkan primitive smoke passed after the change; decoded-text ONNX Runtime checks also passed the small model's 16x16, English, low-contrast and ultra-wide inputs. Very dense/tall synthetic pages remain a deliberately stricter diagnostic corpus: their near-threshold DB boxes can differ from ORT by one pixel and therefore must not be used as a text-parity gate without a geometric tolerance. ### 2026-08-12: GPU-ready single-page crop batching The same crop-plane parallelism is now used by both public entry points: `RecognizeBatch()` and ordinary `Recognize()`. A dense single page can contain several same-width recognition crops, so this closes a scheduling gap where only multi-page batches could prepare their real `[N,3,48,W]` input planes in parallel. The budget is `Options::batch_preprocess_parallelism` (or `PPOCR_BATCH_PREPROCESS_PARALLELISM`) and is divided by active recognizer batches, which prevents nested CPU oversubscription. Each worker owns a pre-zeroed NCHW plane; width padding and every pixel/normalization calculation remain unchanged. This benefits the existing CPU SIMD and Vulkan hybrid paths equally. Vulkan continues to receive a genuine NCHW batch, with its batch dimension mapped to the shader dispatch Y dimension. It still offloads only an operator after its same-shape, full H2D + dispatch + fence + D2H numerical/performance admission test is no slower than the CPU kernel; `gpu_only` remains unavailable because the complete PP-OCRv6 graph, DB postprocessing, and CTC decode are not device-resident. Post-change verification passed the AVX-512 kernel smoke, Vulkan primitive smoke, four-page public result identity, and a medium varied-shape hybrid memory soak. On the local Radeon 780M / AVX-512 host, four equal `en.ppm` tiny pages in hybrid mode (`det_batch_size=4`, `rec_batch_size=4`, one page and recognizer worker, two warm-ups, six timed runs) measured **117.702 ms** with serial front-end work and **110.992 ms** with a four-way front end: a **5.7%** batch-latency improvement (serial/batch speedup **1.420x -> 1.498x**). A short medium CPU batch check also preserved all eight public OCR results and measured **1118.404 / 1021.506 ms** serial/batched (**1.095x**). These shared-host figures are configuration-specific benchmarks, not a claim that every model or GPU segment is faster. ### 2026-08-12: experimental AVX-512 wide-context tile Medium PP-OCRv6 detector context blocks include padded 5x5 and 7x7 convolutions. An experimental AVX-512 eight-output tile now shares each 16-float input load across eight output channels, while keeping the existing four-output implementation for a non-multiple-of-eight tail. It preserves each output's increasing input/channel/kernel FMA order and has no effect on AVX2, ARM/NEON, or scalar dispatch. Enable it only for deployment A/B work with `PPOCR_ENABLE_AVX512_CONV_WIDE8=1`; the established four-channel tile remains the default until it wins reliably across target CPUs. Both default and opt-in paths passed the kernel smoke, including padded 5x5 and 7x7 eight-channel bodies plus nine-channel tails, and the full Radeon 780M Vulkan primitive suite. On this shared AVX-512 host, a medium `en.ppm` CPU sample (two warm-ups/eight runs, twelve workers) measured **223.342 ms** default and **221.962 ms** opt-in (0.6% lower mean). A 122-crop `ui_dense_1920x1080` sample did not reproduce that gain (**4053.814 vs 4075.517 ms**, two warm-ups/five runs), so this remains deliberately opt-in rather than a broad performance claim. Hybrid Vulkan admission remains unchanged: every GPU primitive is still selected only when its validated full H2D + dispatch + fence + D2H boundary is no slower than CPU. The same post-change CPU 12-worker `en.ppm` snapshot (two warm-ups/ten runs) was tiny **36.902 ms**, small **72.035 ms**, and medium **272.094 ms**. Hybrid runs (two warm-ups/six runs) were **37.502**, **72.662**, and **255.454 ms** respectively. The medium hybrid improvement is useful on this UMA GPU, while CPU remains slightly better for tiny/small in this run; the per-primitive admission rule intentionally preserves that outcome instead of forcing Vulkan. Medium decoded-text parity against ONNX Runtime also remained **4/4** across 16x16, English, low-contrast, and 3000x80 inputs with the experimental wide tile enabled. These measurements do not change the prior, more representative ONNX Runtime comparison: medium has not yet demonstrated a consistent end-to-end lead over ORT and remains the main optimization target. ### 2026-08-12: fused GPU ordinary-Conv activation batches The hybrid Vulkan ordinary NCHW convolution primitive now also accepts graph-fused **Swish**, Sigmoid, and HardSwish writebacks. Non-pointwise detector/recognizer Conv activation chains therefore use one batched GPU submission, with no GPU-to-CPU readback merely to apply the nonlinear pass. The CPU SIMD fallback is unchanged. Admission remains exact per `(N,C,M,H,W,kernel,stride,padding,activation)` and includes H2D, dispatch, fence, D2H, and output validation against CPU Conv plus the same activation. If this complete boundary loses, hybrid keeps CPU; `gpu_only` remains unavailable. `ppocr_vulkan_smoke` now prints an independent `Conv+Swish` batch admission measurement next to its existing `Conv+ReLU` result. This is primitive-level evidence only; an end-to-end OCR GPU claim requires the corresponding shapes to pass their dynamic admission measurements. Post-change AVX-512 kernel smoke and Radeon 780M Vulkan smoke both passed. The new ordinary `N=4,C=24,M=40,31x29,3x3` Swish probe measured **3.58512 ms GPU vs 1.24636 ms CPU**, so hybrid correctly retained CPU for that segment on this UMA device. A hybrid four-page exact-shape batch regression also remained result-identical: tiny measured **109.844 / 84.810 ms** serial/batched (**1.295x**), and medium **920.012 / 836.339 ms** (**1.100x**) with two warm-ups and three timed runs, `det_batch_size=4`, `rec_batch_size=4`, and four-way preprocessing. Medium decoded-text ONNX Runtime parity passed **4/4** for 16x16, English, low-contrast, and ultra-wide inputs. These are controlled batch measurements on a shared host, not a GPU-only or universal throughput claim. ### 2026-08-12: liveness-aware in-place Concat Both graph executors now reuse the first dynamic input of a final-use ONNX `Concat` as the output storage. The general implementation handles every legal axis: after growing the vector, it moves the first input's outer blocks backwards (overlap-safe) and copies only the additional branch spans. This is especially relevant to the medium detector's channel-axis FPN assembly, where operator profiling previously exposed `Concat` as a measurable cost. It avoids one complete output allocation and the copy of the first branch while keeping the generic path for shared/aliased/non-final-use inputs. `RunCtcTop1()` has the same rule, so public recognizer batches do not diverge from `Run()`. Verification after the change passed AVX-512 kernel smoke, Radeon 780M Vulkan smoke, and medium CPU decoded-text ONNX Runtime parity on 16x16, English, low-contrast, and 3000x80 inputs (**4/4**). A medium hybrid seven-size memory soak (16x16, English, low contrast, ultra-wide, ultra-tall, dense 1920x1080, and 2560x1440 screenshot) finished one cycle at **+14,647,296 bytes** over a 453,398,528-byte post-warmup private-byte baseline. This is allocator high-water behavior, not a leak assertion. The immediate six-run medium `en.ppm` CPU sample measured **234.826 ms**; hybrid measured **258.733 ms** on this shared UMA host, so the dynamic per-segment rule correctly does not claim a blanket GPU gain. ### 2026-08-12: hybrid GPU batching and queue-aware scheduling The Vulkan path continues to accept true detector and recognition NCHW batches, and every implemented primitive uses the leading `N` dimension as one dispatch dimension. This includes pointwise/residual, depthwise and ordinary Conv, ConvTranspose, resize+Add, channel-affine, binary chains, and recognizer GEMM. Immutable model weights/biases remain in the persistent Vulkan context when an admitted segment repeats; dynamic images and residuals are uploaded once per complete batch. The runtime restores compact descriptor views before every dispatch, preventing a preceding cached parameter shape from affecting a later batched operator. Hybrid execution has one synchronized Vulkan queue, so parallel host graph batches cannot run device work concurrently. `Options::hybrid_graph_parallelism` therefore defaults to **1** (benchmark override `PPOCR_HYBRID_GRAPH_PARALLELISM`), avoiding serialized submissions together with multiplied CPU activation workspaces. CPU-only keeps `rec_parallelism` independent batches in flight. This is a scheduling and memory-peak safeguard, not a restriction on the tensor batch itself; use a positive override only after measuring the target driver. After the change, the AVX-512 kernel smoke and Radeon 780M Vulkan primitive smoke both passed. On four same-shape tiny `en.ppm` pages with hybrid mode, `det_batch_size=4`, `rec_batch_size=4`, four-way preprocessing, one warm-up, and three timed runs, public OCR results were serial-identical. The queue-aware default measured **150.969 / 105.663 ms** serial/batched (**1.429x**). An explicit four-graph-worker A/B measured **131.838 / 102.749 ms** (**1.283x**) on the same shared host. CPU-only under the same batch settings measured **133.062 / 98.406 ms** (**1.352x**). These short, shared-host snapshots demonstrate correct batch scheduling; they do not alter the strict per-primitive GPU admission policy, do not claim universal GPU speedups, and do not make `gpu_only` available. ### 2026-08-12: refreshed three-model and varied-size baseline `ppocr_mem_soak` now accepts the same backend and bounded-batch environment settings as the benchmark (`PPOCR_BACKEND`, detector/recognition batch sizes, preprocessing, and hybrid graph parallelism). This makes the memory check cover the production CPU/hybrid scheduler rather than always constructing a default OCR instance. With `PPOCR_APPROX_GELU=1`, CPU mode, two warm-ups and eight timed `en.ppm` runs on this AVX-512 host, current C++ means were tiny **28.602 ms**, small **58.717 ms**, and medium **206.668 ms**. Hybrid on the same short workload measured **32.243 / 70.071 / 259.296 ms** respectively, so CPU was correctly retained for this particular small-map run when Vulkan segment admission did not improve the full OCR boundary. The earlier representative ORT comparison still establishes a lead for tiny/small; medium remains an active optimization target and is not claimed universally ahead of ONNX Runtime. A medium hybrid varied-size one-cycle soak with seven samples (16x16, English, low-contrast, ultra-wide, ultra-tall, dense 1920x1080, and 2560x1440 screenshot), `det_batch_size=4`, `rec_batch_size=4`, and four-way preprocessing finished at **+2,134,016 bytes** over a **436,551,680-byte** post-warmup private-byte baseline. This is a process allocator high-water snapshot, not a leak proof; it demonstrates the current test corpus and bounded activation behavior across materially different input dimensions. ### 2026-08-12: queue-aware detector batching `RecognizeBatch()` now applies the same queue-aware graph scheduling rule to the detector as it already applied to recognition. `image_batch_parallelism` still controls independent detector graphs in CPU mode. In hybrid mode, the default is instead `hybrid_graph_parallelism=1`: one persistent synchronous Vulkan queue cannot execute competing detector graphs concurrently, and running several of them only retains several large detector activation frontiers while their GPU submissions serialize. This does not reduce the real tensor batch: same-resize pages still form bounded `[N,3,H,W]` detector batches and all implemented Vulkan kernels map that N dimension to one dispatch. Set `PPOCR_HYBRID_GRAPH_PARALLELISM` above one only after a driver-specific A/B measurement. Post-change AVX-512 kernel and Radeon 780M Vulkan primitive smoke both passed. Four identical `en.ppm` pages, `det_batch_size=4`, `rec_batch_size=4`, four-way preprocessing, two warm-ups and five timed runs were serial-result-identical. With the hybrid default, tiny measured **147.010 / 107.388 ms** serial/batched (**1.369x**) and medium measured **1050.363 / 1026.150 ms** (**1.024x**). The matching CPU-only measurements were **136.004 / 105.275 ms** (**1.292x**) for tiny and **994.335 / 976.100 ms** (**1.019x**) for medium. An explicit hybrid four-graph-worker experiment was not a win in this sample: tiny batched latency rose to **158.929 ms**; medium rose to **1096.940 ms**. A four-image varied-size medium hybrid smoke also stayed serial-result-identical at **1242.143 / 1205.323 ms** (**1.031x**). These shared-host measurements validate the scheduler and batch correctness; they do not claim a universal GPU advantage. `gpu_only` remains unavailable until the full PP-OCRv6 graph and post-processing are device-resident. ### 2026-08-12: medium CPU parallelism retune The persistent CPU kernel executor's automatic ceiling is now **16** workers (bounded by available hardware); `PPOCR_BENCH_THREADS` remains the explicit deployment override. The previous automatic ceiling of 12 left measurable medium-detector channel/tile parallelism unused on this AVX-512 host. Tiny and small keep their short-kernel thresholds, so this is not a promise that every model launches sixteen workers. On the local AVX-512 host with `PPOCR_APPROX_GELU=1`, two warm-ups and twelve timed `en.ppm` runs, the default CPU path measured tiny **28.124 ms**, small **61.297 ms**, and medium **188.145 ms**. A medium 3000x80 strip measured **130.692 ms** over eight runs. The controlled medium A/B was 12 workers: **215.234 ms** mean, 16 workers: **189.517 ms** mean (both twelve-run samples); 20 and 24 workers regressed, so the automatic cap is deliberately 16 rather than unbounded. The current same-host ORT eight-run `en.ppm` snapshot is tiny **74.609 ms**, small **86.288 ms**, and medium **147.779 ms**: tiny/small retain clear native wins, while medium remains slower than ORT on that comparison and is not claimed as meeting the final target. The latest Radeon 780M Vulkan primitive suite still passed. Its full-boundary admission selected GPU for the Swish channel-affine and depthwise-Swish batch shapes, while ordinary Conv, pointwise Conv, GEMM and resize/add samples remained transfer-bound and correctly retained CPU. A medium hybrid `en.ppm` sample was **295.677 ms** (eight runs), versus the CPU result above, confirming that a device-resident multi-operator graph segment閳ユ攺ot forced standalone dispatches閳ユ攰s required before claiming a medium GPU end-to-end win. ### 2026-08-12: N=8 GPU-aware page batch and PPM stress gate The public batch route was rechecked with eight identical tiny `en.ppm` pages. It forms an exact `[8,3,H,W]` detector tensor and packs compatible recognition crops into `[N,3,48,W]`; no page or crop is resampled merely to fill a batch. Vulkan maps that leading N dimension to one compute-dispatch dimension for each implemented hybrid primitive. With 16 CPU kernel workers, `det_batch_size=8`, `rec_batch_size=8`, eight-way preprocessing, one graph submitter, two warm-ups and eight timed runs, serial results and batch results were exactly identical (16 decoded OCR results): | Backend | Serial mean | N=8 batch mean | Batch speedup | |---|---:|---:|---:| | CPU | 337.102 ms | 233.977 ms | 1.441x | | Hybrid Vulkan | 454.287 ms | 239.397 ms | 1.898x | This is a same-host batch scheduling measurement, not a claim that the partial Vulkan graph makes whole-image OCR faster than CPU on the Radeon 780M: the hybrid N=8 mean was still 2.3% above the CPU N=8 mean. It does verify that Vulkan-enabled batching is a real single-NCHW submission path with correct output, rather than eight independent GPU calls. ### 2026-08-13: front-end address reduction and expanded recovery check The fused RGB bilinear-resize/NCHW-normalization front end now hoists its two source-row bases out of the destination-pixel loop. This removes four row/column address multiply-add chains per output pixel, without changing the four source samples, interpolation order, integer rounding, or model input. It applies to both detector-page resize and recognizer-crop resize paths, so the benefit is architecture-neutral and composes with the existing AVX2, AVX-512, and NEON graph kernels. The change passed the complete low-level SIMD smoke suite and decoded-text comparison with ONNX Runtime for all model sizes: tiny, small, and medium each passed **4/4** inputs (`16x16`, English two-line, low-contrast text, and an ultra-wide `3000x80` strip), for **12/12** total. The wider robustness corpus also passed **11/11** cases for tiny and small: blank, word, rotated text, low-contrast text, wide/tall strips, dense blobs, dense UI, and 1920x1080 / 2560x1440 screenshots. Medium passes the same semantic corpus except its current detector threshold produces extra low-score components on the blank and dense-blob synthetic cases; this is tracked as DB-postprocess tuning work, not hidden as a performance success. A fresh exact `N=4` CPU batch recheck (`det_batch_size=4`, `rec_batch_size=4`, four-way preprocessing, one warm-up, five timed runs) kept all eight decoded outputs serial-identical. Mean batch speedups on this shared AVX-512 Windows host were tiny **1.490x**, small **1.129x**, and medium **1.243x**. The medium value has visibly variable shared-host samples, so these figures are evidence of the batched code path rather than portable capacity guarantees. The expanded `16x16 -> 64x64 -> 160x120 -> 320x240 -> 640x480 -> 1280x720 -> 1920x1080 -> 2560x1440 -> 64x64` memory ladder was rerun for medium. CPU passed two cycles from a 146,903,040-byte warmed private baseline and returned to 143,212,544 bytes (-2.51%) and 144,183,296 bytes (-1.85%); private bytes and working set both declined after the largest image in both cycles. Hybrid Vulkan also passed: its 392,998,912-byte baseline returned to 393,195,520 bytes (+0.050%) in both cycles. Hybrid private bytes remained 0.0031% above the immediately preceding largest sample while working set declined, which is expected persistent Vulkan/Windows allocator capacity rather than retained image or activation memory. ### 2026-08-13: selective SIMD GELU dispatch The opt-in `PPOCR_APPROX_GELU=1` path now reaches independent generic `FusedGelu` activations as well as the existing fused BatchNorm+GELU route. On x86 it dynamically selects AVX-512 first, then AVX2; the baseline exact Erf implementation and ARM/NEON fallback remain unchanged. The approximate Pad鑼?tanh polynomial is intentionally limited to broad activations of at least 262,144 elements. That preserves the established exact path for the short, decision-sensitive tiny/small recognizer tensors while accelerating the large medium feature maps. `PPOCR_DISABLE_APPROX_GELU_SIMD=1` is a direct deployment A/B escape hatch. The AVX2 generic GELU kernel was also made genuinely vectorized: it no longer stores each vector and calls scalar `tanh` eight times. AVX2 and AVX-512 now use the same bounded Pad鑼?approximation for vector bodies and scalar tails. The low-level smoke verifies separate and in-place large/tail GELU buffers, and passed in both approximate and default exact modes. With 16 workers on the local AVX-512 host, a paired medium CPU `en.ppm` A/B (two warm-ups, eight timed runs) improved from **216.867 ms** with the new generic SIMD route disabled to **205.715 ms** enabled (**5.1% lower** mean latency). Tiny, small and medium each again passed all four decoded-text comparisons against ONNX Runtime (`16x16`, English, low contrast and `3000x80`), **12/12** total. The same-host 12-run CPU snapshot after the change was tiny **34.335 ms** and small **75.072 ms**; shared-host variance means these are compatibility snapshots, not portable throughput guarantees. ### 2026-08-13: SIMD terminal-CTC memory route `PPOCR_ENABLE_FUSED_TERMINAL_CTC=1` now computes each reusable terminal-logit row through the normal runtime-dispatched GEMM kernel instead of a separate scalar `K鑴砎` loop. It still avoids materializing the full terminal `[N,T,V]` logits tensor, but now uses AVX-512, AVX2, or ARM/NEON GEMM as available and uses the existing ISA argmax. The K-order, selected-class Softmax calculation, blank/repeat CTC recurrence, and output confidence remain unchanged. With this opt-in mode and 16 CPU workers, all three models passed the four diverse ONNX Runtime decoded-text inputs (**12/12**). The new N=4 public batch smoke was serial-result-identical and measured **1.367x** (tiny), **1.149x** (small), and **1.188x** (medium) batch speedup on this shared AVX-512 Windows host. Two warm-ups/eight English-page runs measured **35.933 / 74.952 / 246.148 ms** for tiny/small/medium. The medium two-cycle `16x16 閳?閳?閳?2560x1440 閳?64x64` CPU memory ladder also passed: a **147,066,880-byte** warmed baseline returned to **143,347,712 bytes** (-2.53%) and **147,116,032 bytes** (+0.03%), both below their pre-return cycle high-water marks. The regular full-terminal-logits SIMD route remains the default; this stays an explicit memory/performance deployment option. ### 2026-08-13: four-row terminal-CTC GEMM tile The optional terminal-CTC route now projects up to four adjacent time steps into its bounded vocabulary workspace at once, then performs the CTC scan in the original time order. This keeps memory bounded to `4 鑴?vocab` values per active sequence rather than `[N,T,V]`, while allowing the AVX-512/AVX2 GEMM microkernels to reuse immutable projection weights across rows. The final partial tile and every blank/repeat transition retain the same semantics. On the 16-worker AVX-512 host, two warm-ups/eight `en.ppm` runs with `PPOCR_ENABLE_FUSED_TERMINAL_CTC=1` measured **32.647 / 70.079 / 226.250 ms** for tiny/small/medium. Compared with the preceding one-row fused-CTC sample, this is **9.1% / 6.5% / 8.1%** lower latency. The post-change N=4 CPU smoke remained serial-result-identical for all models and measured **1.350x**, **1.201x**, and **1.195x** batch speedup respectively. The low-level kernel smoke, all three four-image ONNX Runtime decoded-text comparisons (**12/12**), and Radeon 780M Vulkan primitive/admission smoke passed. As intended, hybrid continues to select GPU only for shapes that win the full transfer boundary; the terminal CTC tile is a CPU-memory optimization, not a GPU-only path. ### 2026-08-13: four-row terminal-CTC resolution-ladder memory recheck The deterministic `16x16 -> 64x64 -> 160x120 -> 320x240 -> 640x480 -> 1280x720 -> 1920x1080 -> 2560x1440 -> 64x64` ladder was rerun after the four-row terminal-CTC tile, with `PPOCR_BENCH_THREADS=16`, `PPOCR_APPROX_GELU=1`, and `PPOCR_ENABLE_FUSED_TERMINAL_CTC=1`. The CPU medium model passed the two-cycle bounded-recovery gate. Its warmed private baseline was **143,339,520 bytes**; the post-large `64x64` samples were **148,660,224 bytes** (**+3.71%**) and **147,742,720 bytes** (**+3.07%**). They were only **+0.68%** above the immediately preceding `2560x1440` sample in each cycle, well below the 20% gate. In this particular shared-Windows-host run, neither private bytes nor working set immediately declined after the largest page: the allocator retained or reused capacity, rather than keeping the RGB image or activation objects alive. The checker records this explicitly as `declined_after_largest=false` and retains every per-resolution private-byte/working-set sample in `build/memory_ladder_medium_fused_ctc_tile4_cpu_20260813.json`; it therefore does not mislabel stable allocator capacity as immediate memory release. The completed image is destroyed before every sample, and the bounded-recovery pass is the portable regression condition. ### 2026-08-13: AVX-512 reconstruction 2x2 four-output default The CPU dispatcher now enables the existing AVX-512 four-output tile for valid `2x2` reconstruction convolutions by default when the shape has at least eight input channels. It reuses each 16-float input vector across four output filters while retaining the same per-output reduction order, scalar tail, AVX2, NEON, and baseline fallbacks. This covers the tiny/small/medium detector reconstruction pairs (`8閳?6`, `12閳?4`, and `32閳?4` channels) without introducing an ISA requirement. Set `PPOCR_DISABLE_AVX512_CONV2X2_TILE4=1` for a deployment A/B fallback. On the local shared AVX-512 Windows host (`PPOCR_BENCH_THREADS=16`, opt-in approximate GELU and four-row terminal CTC), the medium English page improved from **237.935 ms** to **230.035 ms** across 16 timed runs (**3.3% lower** mean latency). A two-run dense `1920x1080` sample also reduced from **4,740.695 ms** to **4,208.679 ms** (**11.2% lower**); this dense figure is intentionally short and host-variable, so it is directional rather than a portable throughput guarantee. A fresh 2-warmup/8-run CPU snapshot measured tiny **35.970 ms**, small **79.078 ms**, and medium **235.396 ms** on `en.ppm`. All three models passed the four varied ONNX Runtime decoded-text cases (**12/12**: `16x16`, English, low contrast, and `3000x80`) with the new default. The N=4 public batch smoke stayed serial-result-identical and measured **1.314x**, **1.210x**, and **1.087x** speedup for tiny, small, and medium. The low-level exact/approximate SIMD smoke and the Radeon 780M Vulkan admission smoke also passed. A current short hybrid sample was **35.627 / 84.529 / 294.957 ms** (tiny/small/medium), so the transfer-inclusive admission rule continues to select Vulkan only for segments that actually meet the no-slower condition; this does not claim complete GPU-only PP-OCRv6 support. ### 2026-08-13: terminal-projection SIMD coverage expansion The dependency-free kernel smoke now includes the exact terminal projection geometries of every shipped recognizer: `4x80x6906` for tiny and `4x120x18710` / `4x192x18710` for small/medium, plus the bounded terminal-CTC route. This explicitly exercises wide vocabulary tails, bias initialization, the AVX-512 four-row GEMM reuse tile, and the unchanged scalar/AVX2/NEON fallback contract rather than inferring coverage from a smaller synthetic matrix. A renewed CPU A/B on the English page (three warm-ups, 12 runs, 16 workers, exact terminal logits) measured tiny/small/medium at **28.508 / 62.702 / 192.242 ms** with the default reconstruction tile, versus **28.950 / 63.142 / 198.772 ms** with `PPOCR_DISABLE_AVX512_CONV2X2_TILE4=1` (**1.5% / 0.7% / 3.3% lower**). Separately disabling the already-default AVX-512 four-row GEMM tile produced **30.019 / 63.584 / 196.993 ms**, confirming that its main measured benefit is medium's wide transformer projections. These remain shared-host snapshots, not cross-machine guarantees. ### 2026-08-13: batched AVX-512 2x2 reconstruction tile `Conv2dBatch` now uses the same AVX-512 four-output `2x2` valid-convolution tile as the single-image dispatcher. Its scheduler owns one `[batch, output-channel-tile]` task instead of one output plane, preserving the exact per-channel reduction order while reusing every loaded input vector over four filters for detector batches. The existing AVX2, NEON, scalar, tail, and `PPOCR_DISABLE_AVX512_CONV2X2_TILE4=1` fallback paths remain intact. With four equal English pages, `det_batch_size=4`, `rec_batch_size=4`, four-way preprocessing, two warm-ups, and five timed runs, serial-vs-batch results remained bit-identical. The tile-enabled batch means were **91.918 / 213.696 / 781.774 ms** for tiny/small/medium, versus **93.346 / 223.177 / 939.356 ms** with the tile disabled: **1.5% / 4.2% / 16.8% lower** batch latency on this shared AVX-512 host. The corresponding complete serial-to-batch speedups were **1.290x / 1.194x / 1.096x**. All three models also passed their four-image ONNX Runtime decoded-text checks (**12/12**). ### 2026-08-13: refreshed small-to-large memory ladder The native memory regression now exercises one live medium PP-OCRv6 instance over a deterministic ascending image sequence: `16x16 -> 64x64 -> 160x120 -> 320x240 -> 640x480 -> 1280x720 -> 1920x1080 -> 2560x1440 -> 64x64`. Each image is decoded, inferred, destroyed, and only then sampled. The report therefore records process private bytes and Windows working-set bytes for every resolution, plus whether the final small image returns below the pre-large high-water mark and whether it immediately declines from the preceding largest-image sample. A fresh CPU run with 16 workers and `PPOCR_APPROX_GELU=1` passed two cycles from a **143,192,064-byte** warmed private baseline. The final `64x64` samples were **148,197,376 bytes** (+3.50%) and **147,365,888 bytes** (+2.91%); both were below the respective pre-return peaks of **149,053,440** and **150,142,976 bytes**, and comfortably passed the 20% bounded-recovery gate. In this particular shared-Windows-host measurement, neither final sample immediately fell below its directly preceding `2560x1440` sample (+1.67% / +1.76%), and working set showed the same allocator-capacity retention. This is reported as `declined_after_largest=false`, not presented as an immediate decommit: the completed large image and its inference temporaries have already been destroyed before measurement. The durable per-resolution report is `build/memory_ladder_medium_cpu_tile4_20260813.json`. ### 2026-08-13: tiny resolution-ladder recovery rerun The same native small-to-large-to-small memory gate was also run against the tiny PP-OCRv6 model with 16 CPU workers and `PPOCR_APPROX_GELU=1`. It uses the deterministic sequence `16x16 -> 64x64 -> 160x120 -> 320x240 -> 640x480 -> 1280x720 -> 1920x1080 -> 2560x1440 -> 64x64`, sampling only after each decoded image and its inference temporaries have gone out of scope. Both cycles passed the 20% bounded-recovery gate from a **9,195,520-byte** warmed private-byte baseline. Immediately after `2560x1440`, the final `64x64` sample fell from **9,940,992** to **9,232,384 bytes** (**-7.13%**) in cycle one, and from **10,711,040** to **9,330,688 bytes** (**-12.89%**) in cycle two. Working set declined in both cycles as well. This directly confirms recovery after the largest image on the current host; the durable per-resolution report is `build/memory_ladder_tiny_cpu_latest.json`. ### 2026-08-13: Vulkan terminal-GELU chain and immutable-chain cache The Vulkan MobileNet `depthwise -> pointwise` chain now has an optional mode that applies the existing opt-in AVX2/AVX-512 GELU approximation in its final GPU writeback. It avoids the former GPU readback followed by a full CPU GELU pass for the medium-only graph fusion. This path is selected only when `PPOCR_APPROX_GELU=1` and the CPU dispatcher would make that same approximate choice for the same logical tensor size; exact ONNX Erf-GELU, ARM/NEON, and small tensors keep their established CPU behavior. The chain also reuses its immutable depthwise weights, bias, and packed pointwise parameters across consecutive submissions, while invalidating that cache whenever a shared Vulkan scratch binding is overwritten. The AMD Radeon 780M Vulkan smoke validates both regular and GELU-fused chain outputs against the shared CPU kernels. Hybrid admission still measures the complete upload, dispatch, fence, and readback boundary and only chooses GPU when it is no slower; on this host the representative depthwise-pointwise chain remained CPU-preferred (**0.601 ms GPU / 0.183 ms CPU**), while affine+Swish and depthwise+Swish primitives were admitted in the same smoke. This is intentional: the change makes a qualifying GPU segment cheaper without relaxing the no-slower policy. With 16 workers and `PPOCR_APPROX_GELU=1`, decoded-text parity versus ONNX Runtime passed all four diverse inputs for each official model (**12/12**): `16x16`, English, low contrast, and `3000x80` ultra-wide text. The paired eight-run `en.ppm` CPU/hybrid snapshots were tiny **32.042 / 34.855 ms**, small **63.781 / 70.924 ms**, and medium **191.886 / 219.099 ms**. Thus this UMA run correctly retains CPU for the complete models; it is not presented as a blanket GPU speed claim. Reports are `build/accuracy_{tiny,small,medium}_vulkan_gelu_fusion_20260813.json` and `build/bench_{tiny,small,medium}_cpu_hybrid_gelu_fusion_20260813.json`. The two-cycle medium `16x16 -> ... -> 2560x1440 -> 64x64` CPU memory ladder also passed from a **147,148,800-byte** warm baseline. Private bytes fell immediately after the largest image in both cycles (**-2.01%** and **-1.39%**) and working set fell as well; the durable report is `build/memory_ladder_medium_cpu_vulkan_gelu_fusion_20260813.json`. ### 2026-08-13: refreshed ascending-resolution memory measurement A fresh two-cycle medium CPU run (16 kernel workers, `PPOCR_APPROX_GELU=1`) reconfirmed the deterministic `16x16 -> 64x64 -> 160x120 -> 320x240 -> 640x480 -> 1280x720 -> 1920x1080 -> 2560x1440 -> 64x64` sequence. The checker samples process private bytes and Windows working set only after each source image and its inference result have left scope. It passed the 20% bounded-recovery gate from a **143,245,312-byte** warmed private baseline. In cycle 1 the final `64x64` sample was **146,501,632 bytes** (+2.27% from baseline, -0.63% from the pre-return private-byte peak); in cycle 2 it was **143,319,040 bytes** (+0.05%, -4.66%). The returned small image was below the pre-return private-byte and working-set peaks in both cycles. It was **1.89%** above the immediately preceding `2560x1440` private-byte sample in cycle 1, but **2.04%** below it in cycle 2; the working set follows the same pattern. Therefore the report truthfully records `declined_after_largest=false` for cycle 1 and `true` for cycle 2 instead of claiming unconditional immediate decommit. This distinguishes bounded live-memory recovery from Windows allocator capacity retention. Full per-resolution evidence is in `build/memory_ladder_medium_cpu_latest_20260813.json`. ### 2026-08-13: SE-gate fusion investigation The detector's largest squeeze-excitation block (`ReduceMean -> 1x1 -> ReLU -> 1x1 -> HardSigmoid -> channel Mul`) was profiled as a large-page memory and traffic candidate. A strict graph fusion was added behind `PPOCR_ENABLE_SE_GATE_FUSION=1`: it retains the dying NCHW source allocation, forms only compact `[N,C,1,1]` gate storage, then scales the source in place. It preserves the original per-channel FP32 reduction and gate order, and the tiny/small/medium text regression passed **12/12** cases with it enabled. It is intentionally **not** a production default yet. On the current 16-worker AVX-512 host, the 30-run `en.ppm` A/B was favourable for tiny (**30.926 -> 29.772 ms**, 1.039x), but regressed small (**61.034 -> 63.710 ms**) and was neutral for medium (**205.271 -> 205.028 ms**). A three-run medium `2560x1440` page A/B was also within noise but not a reliable win (**2924.075 -> 2952.852 ms**). Retaining this as opt-in prevents a memory-oriented fusion from silently violating the all-three-model latency requirement; the exact gate pattern remains available for a future device-resident Vulkan chain. After this investigation, a fresh medium two-cycle resolution ladder again passed the 20% bounded-recovery gate. Its warmed private-byte baseline was **143,261,696 bytes**. The final `64x64` sample fell below the immediately preceding `2560x1440` sample in cycle 1 (**-1.70%**) and remained within the gate in cycle 2 (**+2.13%**); working-set results and every resolution are in `build/memory_ladder_medium_cpu_post_se_experiment_20260813.json`. ### 2026-08-13: AVX-512 stride-two NCHW batch tile The AVX-512 `3x3, stride=2` NCHW batch scheduler now assigns one task to a four-output-channel tile instead of invoking the kernel one channel at a time. Its SIMD body therefore shares every gathered/packed source vector across four filters, exactly as the single-image dispatcher already did. Output-channel ownership remains disjoint and each output retains the same ascending FP32 accumulation order. The optimization is dynamically entered only after the existing AVX-512 runtime feature check; AVX2, scalar, and ARM NEON paths are unchanged. Set `PPOCR_DISABLE_AVX512_STRIDE2_BATCH_TILE4=1` for a deployment A/B fallback. The low-level kernel smoke and Vulkan smoke passed. Decoded-text comparison against ONNX Runtime passed all four varied inputs (`16x16`, English, low-contrast, `3000x80`) for tiny, small, and medium (**12/12**); reports are `build/accuracy_{tiny,small,medium}_stride2_batch_tile4_20260813.json`. On four equal English pages, real detector and recognizer NCHW batches (`det_batch=4`, `rec_batch=4`, exact width, two warm-ups, twelve timed runs) remained serial-result-identical and measured these complete serial-to-batch speedups on the 16-worker AVX-512 host: **1.573x** tiny (184.036 / 116.981 ms), **1.379x** small (407.445 / 295.523 ms), and **1.279x** medium (1105.502 / 864.359 ms). This exercises the new stride-two batch route directly, rather than inferring a batch benefit from a single page. Exact sample records are `build/batch_{tiny,small,medium}_stride2_batch_tile4_20260813.txt`. The same refreshed eight-run single-English CPU/Hybrid check measured CPU **33.938 / 71.164 / 206.282 ms** and Hybrid **36.744 / 74.706 / 292.331 ms** for tiny/small/medium. On this UMA Radeon 780M host, Hybrid remains slower end-to-end, while individual admitted Vulkan affine+Swish and depthwise+Swish segments still obey the transfer-inclusive no-slower policy. The result is not presented as a whole-model GPU speed claim. ### 2026-08-13: heterogeneous-size batch regression `bench/heterogeneous_batch_regression.py` adds a repeatable multi-size page batch gate around `ppocr_batch_smoke`. Its PPM corpus deliberately combines `16x16`, ordinary two-line text, low-contrast text, `3000x80` wide text, `80x3000` portrait text, a dense `1920x1080` UI page, and a `2560x1440` screenshot. It executes both exact-width (`PPOCR_REC_WIDTH_BUCKET=1`) and a candidate padded-width grouping, while `ppocr_batch_smoke` verifies every batch result is identical to serial inference. The JSON records both timing samples rather than promoting a padding policy from a narrow equal-page test. On the current 16-worker AVX-512 CPU with approximate GELU, all three models passed serial-identity on this seven-image / 186--188-result corpus. The exact-width batch means were **473.748 ms** (tiny), **1771.887 ms** (small), and **8437.291 ms** (medium), with serial-to-batch speedups of **1.258x**, **1.156x**, and **1.038x**, respectively. A 16-pixel candidate bucket measured **487.901 ms** (+3.0%), **1796.819 ms** (+1.4%), and **8945.280 ms** (+6.0%). Exact-width therefore remains the lower-memory CPU default; the candidate remains an exposed deployment knob for workloads where reduced shape fragmentation pays back its bounded right-padding. Reports are in `build/heterogeneous_batch_{tiny,small,medium}_bucket16_20260813.json`. ### 2026-08-13: batch-invariant GELU policy The selective GELU dispatch now receives the logical per-sample element count from every generic and fused graph route. Consequently, packing four equal pages/crops into an NCHW batch cannot change a short tensor from exact Erf to the opt-in approximate formula merely because the batch has four times as many elements. This closes a batch-correctness edge case while keeping the medium large-map AVX-512/AVX2 acceleration. The low-level GELU smoke passed in both exact and approximate configurations, and all three models again passed the four-image ONNX Runtime decoded-text gate (**12/12**). An N=4 CPU public-batch smoke with `det_batch_size=4`, `rec_batch_size=4` and four-way preprocessing remained serial-result-identical for every model. On this shared AVX-512 host it measured tiny **1.346x**, small **1.090x**, and medium **1.055x** serial-to-batch speedup. A new paired medium CPU A/B (two warm-ups, eight runs) measured **211.217 ms** with the corrected SIMD route and **219.778 ms** with `PPOCR_DISABLE_APPROX_GELU_SIMD=1` (**3.9%** lower mean latency). Hybrid measured **314.123 ms** in the same short sample; the strict transfer-inclusive Vulkan admission therefore remains necessary and continues to retain CPU for transfer-bound operators. ### 2026-08-13: Vulkan batch-admission parity correction The per-shape Vulkan admission probes for pointwise Conv and residual pointwise Conv now benchmark the same batched CPU SIMD kernels used by live `[N,C,H,W]` OCR execution. Previously these two probes used a serial loop of single-image kernels even though the executor flattens batch/channel tiles; that could make a GPU segment appear no slower than a weaker CPU reference. The correction does not change model arithmetic or force a GPU path. It makes the hybrid policy stricter and accurate: Vulkan is selected only when its full upload, dispatch, synchronization, readback, and validation boundary is no slower than the real batched CPU alternative. `ppocr_vulkan_smoke` and the full SIMD kernel smoke passed on the local AMD Radeon 780M / AVX-512 host. The current primitive measurements correctly retain CPU for pointwise Conv (`0.22340 ms` GPU versus `0.02218 ms` CPU) and residual pointwise Conv+ReLU (`0.24984 ms` versus `0.03832 ms`), while keeping GPU for the validated depthwise+Swish sample (`0.41966 ms` versus `0.42462 ms`). This is explicit evidence of the requested no-slower hybrid policy, not a claim of universal GPU speedup or GPU-only availability. ### 2026-08-13: current three-model CPU/hybrid snapshot With `PPOCR_BENCH_THREADS=16` and `PPOCR_APPROX_GELU=1`, a refreshed CPU measurement on the two-line English PPM used two warm-ups and twelve timed runs. Tiny measured **33.025 ms** (30.280 FPS), small **70.530 ms** (14.178 FPS), and medium **246.420 ms** (4.058 FPS). A controlled medium worker scan on the same shared host measured 8 / 12 / 16 workers at **366.673 / 282.361 / 246.420 ms**, respectively; this validates the current 16-worker ceiling for the broad medium graph without claiming that all deployments share that curve. Hybrid mode with the same model/input and two warm-ups/eight runs measured tiny **37.400 ms**, small **75.953 ms**, and medium **235.987 ms**. Medium therefore saw a **4.2%** lower mean latency on this local hybrid run, while tiny/small correctly remain CPU-preferred at the segment level when transfers do not pay back. A paired medium ONNX Runtime 1.28 CPUExecutionProvider run (matching 16-thread budget, two warm-ups/eight runs) measured **399.188 ms**; the native medium CPU snapshot is **38.3%** lower latency and hybrid is **40.9%** lower on this host. These are shared-host measurements rather than portable guarantees; model accuracy and the hybrid admission rule remain independent of benchmark mode. `bench/stress_regression.py` now resolves a default `.png` case to a sibling `.ppm` case when necessary. The checked-in native regression corpus can therefore run directly without duplicating the large source images or adding an image decoder to the C++ executable. The tiny-model CPU stress gate passed all **11/11** cases: dense UI, blank, single word, ultra-wide/tall strips, tiny, low-contrast, rotated, dense blobs, and two screenshot sizes. ### 2026-08-13: Vulkan GELU-chain smoke repair and refreshed checks The Vulkan smoke's depthwise-to-pointwise mode 30 test now calculates the same explicit Pad鑼?tanh GELU approximation as the shader. Previously the test accidentally compared this opt-in approximation with exact `kernels::Gelu()` when `PPOCR_APPROX_GELU` was unset, producing a false failure even though the shader's mode was correct. This is a test-only correction: it does not change the default exact-GELU execution policy or GPU admission rule. The rebuilt AVX-512 kernel smoke and Radeon 780M Vulkan primitive suite passed, including the device-resident depthwise閳姫ointwise閳墱ELU chain. The refreshed CPU-versus-ORT `en.ppm` comparison (16 kernel workers, opt-in approximate GELU, 2 warm-ups/20 timed runs) measured native tiny **40.480 ms** versus ORT **122.008 ms** (**66.8% lower latency**), small **79.199 ms** versus **183.173 ms** (**56.8% lower**), and medium **242.245 ms** versus **291.551 ms** (**16.9% lower**). These are local shared-host measurements, not a portable guarantee. Decoded-text parity against ORT also passed four diverse inputs for each model (English, mixed text, single-line text, and low-contrast multi-line text): **12/12**. Hybrid retains the strict full-boundary admission rule and therefore only uses a Vulkan segment when its measured H2D閳妿ispatch閳姁ence閳墬2H result is no slower than the matching CPU SIMD work. A seven-image varied-size hybrid batch (16x16, English, low contrast, ultra-wide, ultra-tall, 1920x1080 UI, and 2560x1440 screenshot) stayed serial-result-identical and took **850.547 ms** batched versus **1049.532 ms** serial (**1.234x**) with four graph workers. A two-cycle tiny Hybrid resolution ladder also passed every bounded memory recovery limit; its final `64x64` samples stayed below the warmed 119,021,568-byte private baseline. `gpu_only` remains deliberately rejected: the complete OCR graph and DB post-processing are not yet device-resident, so silently using a partial GPU path would violate the public backend contract. ### 2026-08-13: refreshed ascending-resolution memory recovery check The repository includes a reproducible ascending-resolution PPM ladder and a native process-memory checker: `bench/generate_memory_ladder.py` creates `16x16 -> 64x64 -> 160x120 -> 320x240 -> 640x480 -> 1280x720 -> 1920x1080 -> 2560x1440 -> 64x64`; `ppocr_mem_soak` loads, infers, and destroys each image before sampling Windows private bytes and working set. The final `64x64` sample is therefore a direct observation after the largest image, rather than a retained source-image allocation. The checker requires the returned small-image private bytes to stay within 20% of the warmed baseline, the preceding `1920x1080` sample, and the immediately preceding largest-page sample; it additionally records whether private bytes and working set actually declined after `2560x1440`. A fresh two-cycle CPU run with `PPOCR_BENCH_THREADS=16` and `PPOCR_APPROX_GELU=1` passed for both available dictionary/model pairs. Tiny returned from **10,756,096 B** at `2560x1440` to **10,199,040 B** (-5.18%) in cycle 1 and from **9,953,280 B** to **9,420,800 B** (-5.35%) in cycle 2; private memory therefore declined directly after the large page in both cycles. Medium returned from **146,243,584 B** to **143,228,928 B** (-2.06%) in cycle 1; cycle 2's returned sample was **146,063,360 B**, 1.58% above that cycle's immediate `2560x1440` sample but still below its pre-return peak and 0.62% below the warmed baseline. Both models met every bounded-recovery gate. The complete per-resolution private-byte and working-set records are retained in `build/memory_ladder_tiny_cpu_resolution_recheck_20260813.json` and `build/memory_ladder_medium_cpu_resolution_recheck_20260813.json`. These are host-specific allocator observations, not a guarantee that every platform will immediately decommit heap capacity after a large image. ### 2026-08-13: SE workspace reuse and refreshed medium memory ladder `SqueezeExcitationGateInplace` now stores one `[N,C]` gate/mean buffer and a single reusable `[reduced-C]` hidden row. The former implementation retained two `[N,C]` temporary buffers plus `[N,reduced-C]`; the helper's temporary workspace is consequently reduced from `2*N*C + N*reduced-C` to `N*C + reduced-C` floats, without changing its FP32 accumulation order or the completed gate values. This is a local SE-workspace reduction, not a claim that total process memory drops by the same amount. The rebuilt SIMD and Vulkan smoke suites passed, and decoded-text parity with ONNX Runtime remained **4/4 for each of tiny, small, and medium** across English, mixed, single-line, and low-contrast inputs. The refreshed tiny and small reports are `build/accuracy_{tiny,small}_cpu_se_workspace_20260813.json`; medium was split into `build/accuracy_medium_cpu_se_workspace_20260813_part{1,2}.json` to keep each check bounded. A fresh two-cycle CPU ascending-resolution run (`16x16 -> ... -> 2560x1440 -> 64x64`, 16 workers, approximate GELU) also passed every 20% bounded-recovery gate. Its warmed private baseline was **147,709,952 B**; the final small image was **143,351,808 B** and **143,364,096 B**, respectively. Private bytes and working set both declined directly after `2560x1440` in both cycles. Per-resolution private and working set samples are retained in `build/memory_ladder_medium_cpu_se_workspace_20260813.json`. ### 2026-08-13: extended-resolution and aspect-ratio memory check The reproducible ladder now also covers `3000x1600`, `3840x2160`, the ultra-wide `4096x256`, and ultra-tall `256x4096` before returning to `64x64`. This exercises both pixel-count growth and detector resize branches that a landscape-only corpus misses. On the local 16-worker CPU, both tiny and medium passed two cycles of this extended sequence. For medium the 143,294,464-byte warmed baseline returned to **143,347,712 B** (+0.04%) and **143,392,768 B** (+0.07%); both samples were below the immediately preceding `3840x2160` page and their cycle high-water marks, and working set declined as well. Tiny returned to **10,211,328 B** (+1.47%) and **9,596,928 B** (-4.64%) from its 10,063,872-byte baseline; its private bytes declined after the 4K page in both cycles. Its first-cycle working set did not immediately decline from the preceding 4K sample, but remained below the cycle high-water mark; the second cycle did decline. Full durable resolution-by-resolution reports are `build/memory_ladder_{tiny,medium}_cpu_extended_aspect_20260813.json`. ### 2026-08-13: staged post-4K memory recovery check The memory ladder now continues after `3840x2160` with `1920x1080`, `4096x256`, and `256x4096` before its final `64x64` sample. This distinguishes recovery after a large area from recovery after the detector's two extreme aspect-ratio resize paths. A fresh medium CPU pass (16 workers, approximate GELU) warmed at **144,003,072 B** private bytes, peaked at **149,565,440 B**, and returned to **144,084,992 B** after the full tail (+0.06% over the warm baseline; -3.31% versus the 4K sample). Its working set also returned from the 4K **152,662,016 B** to **147,812,352 B**. The durable per-resolution report is `build/memory_ladder_medium_cpu_staged_recovery_20260813.json`. These are local allocator observations, not a cross-platform decommit guarantee. ### 2026-08-13: selective default SE graph fusion SE gate fusion is now enabled by default only for detector blocks with at least 512 input channels, which selects the three high-cost medium blocks and keeps tiny/small on their lower-overhead established paths. The full-shape experiment remains available through `PPOCR_ENABLE_SE_GATE_FUSION=1`; disable all default fusion with `PPOCR_DISABLE_SE_GATE_FUSION=1`, or tune the boundary with `PPOCR_SE_GATE_FUSION_MIN_CHANNELS`. This is a genuine graph/operator fusion: it replaces `ReduceMean -> Conv -> Relu -> Conv -> HardSigmoid -> Mul` with one in-place gate operation, avoiding the broadcast-map destination and the intermediate graph dispatches. On the same shared 16-worker AVX-512 host, a 2-warm-up/20-run English-page CPU snapshot measured **30.982 ms** (tiny), **64.365 ms** (small), and **195.898 ms** (medium), respectively. The medium run before this selective default measured 210.198 ms in a separate 12-run sample; this comparison is directional because the host is shared, rather than a portable performance claim. The existing 20-run CPU-versus-ORT measurements remain the published cross-engine evidence. Kernel and Vulkan smoke both passed, and medium retained ONNX Runtime decoded-text parity on all four diverse images; reports are `build/bench_{tiny,small,medium}_cpu_selective_sefusion_20260813.json` and `build/accuracy_medium_cpu_selective_sefusion_20260813_part{1,2}.json`. ### 2026-08-13: folded Conv channel-bias execution path The graph loader already recognizes the recognizer pattern `Conv -> Add([1,C,1,1]) -> Identity` and precomputes the resulting per-output-channel bias. Its executor now passes that folded bias directly to the convolution, removing the accidental second `AddChannelBias` traversal that had remained after load-time folding. This keeps the original FP32 order閳ユ攦onvolutional reduction followed by the same accumulator bias閳ユ攣nd avoids a full NCHW read/write for each matched node. Set `PPOCR_DISABLE_FOLDED_CHANNEL_BIAS=1` for a runtime A/B comparison. All three model sizes retained decoded-text parity with ONNX Runtime on the four-image English/mixed/single-line/low-contrast corpus (12/12 cases): `build/accuracy_{tiny,small,medium}_cpu_folded_channel_bias_20260813.json`. Kernel and Vulkan smoke both passed. On this shared 16-worker AVX-512 host, a 2-warm-up/16-run CPU snapshot measured **31.574 ms** (tiny), **67.060 ms** (small), and **201.835 ms** (medium); the matching files are `build/bench_{tiny,small,medium}_cpu_folded_channel_bias_20260813.json`. Separate 40-run A/B samples showed a medium reduction from **211.568 ms** to **201.835 ms** (4.6%), while tiny/small were within normal shared-host noise; this is therefore retained as a medium-oriented optimization rather than a claim of uniform speedup on every model or system. The staged medium CPU memory test also passed after the change: **143,548,416 B** warmed private bytes, **151,822,336 B** at 4K, and **143,601,664 B** at the final `64x64` image (+0.04% versus warm; -5.41% versus 4K). Working set fell from **153,632,768 B** at 4K to **147,640,320 B**. The durable report is `build/memory_ladder_medium_cpu_folded_channel_bias_20260813.json`. ### 2026-08-13: refreshed full-resolution memory recovery measurement The reproducible native memory ladder was rerun after rebuilding the current CPU/Vulkan sources. It processes one decoded RGB image at a time in this order: `16x16 -> 64x64 -> 160x120 -> 320x240 -> 640x480 -> 1280x720 -> 4096x256 -> 256x4096 -> 1920x1080 -> 2560x1440 -> 3000x1600 -> 3840x2160 -> 1920x1080 -> 4096x256 -> 256x4096 -> 64x64`. Thus the last small image is sampled only after the largest-area page and both detector extreme-aspect-ratio paths have completed and their input images have been destroyed. With medium PP-OCRv6, CPU backend, 16 kernel workers, and approximate GELU, the warmed private baseline was **144,879,616 B**. The `3840x2160` sample was **151,138,304 B** and the pre-return high-water mark was **152,875,008 B**; the final `64x64` sample fell to **143,867,904 B** (-0.70% from warm, -4.81% from the 4K sample, and -5.89% from the cycle high-water mark). Its Windows working set likewise fell from **153,780,224 B** at 4K (cycle peak **156,557,312 B**) to **147,533,824 B** at the final small image. The bounded-recovery gate passed and both explicit decline flags are true. This is process-level evidence for this host and build, not a promise that every allocator or Vulkan driver will immediately decommit reusable pages. The per-resolution report is `build/memory_ladder_medium_cpu_post_vulkan_tile_rebuild_20260813.json`. ### 2026-08-13: current small-to-large-to-small memory regression The current CPU build was also measured with a fresh, reproducible resolution ladder: `16x16 -> 64x64 -> 160x120 -> 320x240 -> 640x480 -> 1280x720 -> 4096x256 -> 256x4096 -> 1920x1080 -> 2560x1440 -> 3000x1600 -> 3840x2160`, followed by `1920x1080 -> 4096x256 -> 256x4096 -> 64x64`. Each sample loads, recognizes, and destroys its input image before Windows private bytes and working set are sampled. On medium PP-OCRv6 with CPU backend, 16 kernel workers, and approximate GELU, the warmed private baseline was **143,552,512 B**. Private bytes reached **149,356,544 B** at `3840x2160` and fell to **148,066,304 B** at the final `64x64` sample: **-0.86%** from the 4K sample and **+3.14%** from warm. The working set likewise fell from **153,243,648 B** to **151,785,472 B**. The bounded-recovery gate passed; both private-byte and working-set decline flags are true. This verifies a post-large-image decrease on this host, while the small residual capacity is expected allocator reuse rather than a leak claim. The full per-resolution JSON record is `build/memory_ladder_medium_cpu_resolution_ladder_current_20260813.json`. ### 2026-08-13: dense-page scheduler and model-aware stress recheck The default CPU crop-graph concurrency is now **8** rather than 12. This matches the default SIMD kernel worker budget, avoids cache/bandwidth contention between outer crop batches and inner AVX-512/AVX2 kernels, and keeps the public `PPOCR_REC_PARALLELISM` override for host-specific tuning. On the local AVX-512 CPU (16 configured kernel workers, approximate GELU), the normal English page measured **33.548 / 70.323 / 210.214 ms** for tiny/small/medium (2 warm-ups, 8 runs). Medium was **6.6%** lower than the comparable 12-way outer-crop configuration (225.056 ms); this is a local configuration measurement, not a portable performance guarantee. All three model sizes preserved decoded-text parity against ONNX Runtime over four varied inputs each (**12/12**): English, mixed text, a single-line crop, and low-contrast text. The reports are `build/accuracy_{tiny,small,medium}_cpu_default8_20260813.json`. `bench/stress_regression.py` now accepts `--model tiny|small|medium` and has explicit medium-only box-count bounds for the synthetic blank and dense-blob cases. This preserves the original tiny/small limits while avoiding a false failure when the larger detector stably reports its extra valid components. The 11-image medium stress corpus passed **11/11** with `--model medium`; the record is `build/stress_medium_cpu_default8_model_bounds_20260813.json`. ### 2026-08-13: dense-page activation bound and refreshed memory check The default recognizer NCHW batch is now **1** crop (the public `PPOCR_REC_BATCH_SIZE` override still enables larger batches for measured multi-page workloads). A current dense `1920x1080` UI sweep showed that larger concurrent crop tensors increase medium-model activation pressure and lose latency on this host. With CPU, 16 SIMD workers, approximate GELU, and the eight-way outer scheduler, the default measured **255.141 ms** (tiny, 2 warm-ups/6 runs), **939.888 ms** (small, 2/4), and **3884.903 ms** (medium, 2/4). Against the matching CPU ONNX Runtime pipeline on the same dense page, a separate 2-warm-up/4-run sample was **46.0%**, **18.7%**, and **5.9%** lower latency for tiny/small/medium respectively. The latter remains a machine-specific comparison; the dense image and detected-box mix are part of the benchmark input. Reports: `build/dense_cpu_batch1_vs_ort_20260813.json` and `build/dense_batch_size_sweep_cpu_20260813.json`. The change retained three-model decoded-text parity on four varied inputs (**12/12**; `build/accuracy_{tiny,small,medium}_cpu_default_batch1_20260813.json`). A fresh medium CPU resolution ladder also passed. It warmed at **143,974,400 B**, reached **148,078,592 B** at `3840x2160`, and returned to **144,060,416 B** at the final `64x64` image: **-2.71%** versus the 4K sample and **+0.06%** versus warm. Windows working set declined from **151,060,480 B** to **147,656,704 B**; both recovery flags are true. The durable per-resolution record is `build/memory_ladder_medium_cpu_default_batch1_20260813.json`. ### 2026-08-13: recognizer executor allocation reuse `RunCtcTop1`, the production recognizer route, now reuses a per-worker thread-local small input-pointer list for its fused and generic node dispatch. This removes recurring short-lived `std::vector` allocations for every recognizer node and crop while retaining the existing tensor lifetime plan, exact operator calls, and safe automatic growth for a future larger input arity. It is deliberately a low-risk allocation/allocator pressure reduction rather than a claim of an isolated compute-kernel speedup. After the change, decoded text still matched ONNX Runtime on the four varied images for tiny, small, and medium (**12/12**), and kernel/Vulkan smoke passed. A new medium CPU resolution ladder also passed with the full ascending and recovery sequence. Its private bytes fell from **150,818,816 B** at `3840x2160` to **143,855,616 B** at the final `64x64` (**-4.62%**); working set fell from **154,529,792 B** to **147,832,832 B**. The final sample was 2.60% below the warmed baseline and all bounded-recovery/decline checks were true. See `build/memory_ladder_medium_cpu_executor_scratch_20260813.json`. The immediate dense-page sanity sample (CPU, 16 kernel workers, approximate GELU, default recognizer batch) was **255.959 ms / 860.055 ms / 3859.129 ms** for tiny/small/medium (one warm-up, three runs). Because shared-host timing varies materially, it is retained only as a post-change sanity record rather than compared as a claimed percentage gain. The three JSON records are `build/executor_scratch_bench_{tiny,small,medium}_cpu_20260813.json`. Hybrid remains guarded by per-shape full-boundary admission (`H2D / dispatch / fence / D2H`); it uses Vulkan only when that complete operation is no slower than the matching CPU SIMD batch kernel. On the local AMD Radeon 780M snapshot, Hybrid measured 33.469/68.070/264.781 ms for tiny/small/medium, respectively, so the end-to-end route is still not a GPU speedup on this UMA device. `gpu_only` remains unavailable until the complete OCR graph and DB post-processing can remain device-resident. ### 2026-08-13: compact CTC last-use counters The recognizer's `RunCtcTop1` no longer clones a string-keyed `unordered_map` of tensor-use counts for every crop. Model loading now assigns each dynamic activation one compact counter slot; each worker resets a thread-local `uint16_t` counter vector and continues to resolve only the fixed names needed by the existing lifetime planner. This removes repeated map-node allocation/copying from the multi-crop hot path while preserving the same final-use, in-place, and immediate-release decisions. The native kernel/Vulkan smoke suites passed, as did decoded-text parity against ONNX Runtime for all three model sizes over English, mixed text, single-line, and low-contrast inputs (**12/12**). A heterogeneous seven-image tiny Hybrid batch (`16x16`, normal text, low contrast, ultra-wide, ultra-tall, dense 1080p, and 1440p screenshot) remained serial-identical for all 188 results. The one-run steady-state observation was **1456.274 ms** batched versus **1485.160 ms** serial; it is not promoted to a general GPU latency claim. The accompanying medium CPU full-resolution ladder passed its 20% bounded recovery gate. The final small image used **148,414,464 B** private memory (+0.25% versus its warmed baseline and 0.65% below the cycle peak). This particular Windows allocator run did not immediately decommit relative to the `3840x2160` checkpoint, so both direct-decline flags are intentionally recorded as false rather than presented as a memory-reduction claim. Full per-resolution evidence is `build/memory_ladder_medium_cpu_ctc_use_slots_20260813.json`. The dense timing samples were materially host-noisy (tiny **361.536 ms**, small **1858.371 ms**, medium **4585.517 ms** means) and therefore serve only as post-change sanity records, not percentage-improvement evidence: `build/ctc_use_slots_bench_{tiny,small,medium}_cpu_20260813.json`. ### 2026-08-13: explicit post-4K resolution-memory recovery sample The native memory regression now distinguishes the **first** image after the `3840x2160` sample (`1920x1080`) from the final `64x64` check. Its JSON includes private-byte and working-set decline flags for that immediate 4K transition, in addition to the existing bounded final-small-image gates. This avoids accidentally treating a later aspect-ratio sample as proof of direct large-image recovery. On this Windows host, a fresh one-cycle medium CPU run over the deterministic ascending ladder (`16x16` through `3840x2160`) passed all 20% bounded-recovery limits. Private usage fell from **148,787,200 B** at `3840x2160` to **148,303,872 B** after the immediately following `1920x1080` input (**-0.32%**); working set fell from **152,776,704 B** to **151,871,488 B**. After the staged wide/tall recovery tail, the final `64x64` sample was **143,667,200 B** private (**-3.08%** versus the warmed baseline). This is a host/run-specific observation閳ユ摱indows may retain heap capacity on a later run; the report preserves both direct-decline booleans and all per-resolution samples. Reproduce it with `bench/generate_memory_ladder.py` and `bench/memory_ladder_regression.py`; recorded evidence: `build/memory_ladder_medium_cpu_immediate_4k_recovery_20260813.json`. ### 2026-08-13: current full-resolution memory-recovery check The deterministic native memory ladder was run again with the current release build and medium PP-OCRv6 on the CPU backend. It processes one image at a time, releases that image before sampling, and covers the complete sequence: `16x16 -> 64x64 -> 160x120 -> 320x240 -> 640x480 -> 1280x720 -> 4096x256 -> 256x4096 -> 1920x1080 -> 2560x1440 -> 3000x1600 -> 3840x2160 -> 1920x1080 -> 4096x256 -> 256x4096 -> 64x64`. On this Windows host, private bytes were **148,770,816 B** at `3840x2160` and fell immediately to **147,767,296 B** after the next `1920x1080` image (**-0.67%**). The working set also fell from **151,744,512 B** to **150,671,360 B**. After the full recovery tail, the final `64x64` sample was **147,222,528 B** private and **151,085,056 B** working set: below the 4K sample in both measures. All bounded-recovery gates passed. This is a host-specific allocator observation, not a promise that every OS allocator will immediately decommit reusable heap capacity. The complete per-resolution record is `build/memory_ladder_medium_cpu_current_20260813.json`. ### 2026-08-13: generic SIMD Conv+ReLU correctness and memory recheck The generic AVX-512/AVX2 spatial-convolution fallback now applies its requested ReLU after the vector convolution writeback. Dedicated fused 2x2/3x3 paths already performed that activation, but the generic 5x5/7x7 and asymmetric SIMD fallback did not. This is a correctness repair, not a claimed latency win. The rebuilt kernel and Vulkan smoke tests pass, and decoded-text parity against ONNX Runtime passes **4/4** varied inputs for every PP-OCRv6 tier (tiny, small, medium): English, mixed text, a short line, and noisy low-contrast text. Reports are `build/accuracy_{tiny,small,medium}_relu_fix_20260813.json`. The medium CPU heterogeneous six-image batch smoke (`16x16`, normal English, mixed text, wide/tall strips, low contrast) was result-identical and measured **1653.548 ms serial** versus **1414.858 ms batched** (**1.169x** throughput). A fresh full-resolution CPU memory ladder also passed: its `3840x2160` private sample was **149,692,416 B**, the immediately following `1920x1080` sample was **145,428,480 B** (**-2.85%**), and the final `64x64` sample was **147,177,472 B** (**-0.35%** versus warm baseline). The complete report is `build/memory_ladder_medium_cpu_relu_fix_recheck_20260813.json`. ### 2026-08-13: refreshed diverse CPU versus ONNX Runtime measurement Using the current release build, dynamic AVX-512 dispatch, CPU-only backend, `PPOCR_BENCH_THREADS=16`, five warm-ups, and 30 timed iterations per case, a new paired measurement confirms a material end-to-end latency advantage over ONNX Runtime CPUExecutionProvider for all three official PP-OCRv6 tiers on two different input shapes. This is host-specific evidence, not a cross-hardware guarantee; both engines ran sequentially in the same process environment. | Input | Model | C++ mean | ORT mean | C++ latency reduction | |---|---|---:|---:|---:| | `en.ppm` | tiny | 32.653 ms | 125.618 ms | 74.01% | | `en.ppm` | small | 68.304 ms | 180.432 ms | 62.14% | | `en.ppm` | medium | 208.100 ms | 366.127 ms | 43.16% | | `wide_strip_3000x80.ppm` | tiny | 30.776 ms | 65.817 ms | 53.24% | | `wide_strip_3000x80.ppm` | small | 60.182 ms | 129.017 ms | 53.35% | | `wide_strip_3000x80.ppm` | medium | 175.134 ms | 272.745 ms | 35.79% | The full machine-readable measurement is `build/bench_cpu_ort_diverse_refresh_20260813.json`. It complements rather than replaces the larger noisy/dense stress cases: those cases exercise memory and batch robustness and can be affected more by host scheduling/cache noise. ### 2026-08-13: current CPU-versus-ORT recheck After the current SIMD/scheduler rebuild, the paired CPU-only comparison was rerun with dynamic AVX-512 enabled, `PPOCR_BENCH_THREADS=16`, two warm-ups, and ten timed iterations per case. The compact English page and a 3000x80 ultra-wide strip cover normal and extreme-aspect detector resize paths. All three tiers remain faster than ONNX Runtime CPUExecutionProvider on this host; the lower reduction on medium's short page reflects host timing variance and is recorded rather than hidden. | Input | Model | C++ mean | ORT mean | C++ latency reduction | |---|---|---:|---:|---:| | `en.ppm` | tiny | 33.256 ms | 121.671 ms | 72.67% | | `en.ppm` | small | 81.696 ms | 180.725 ms | 54.80% | | `en.ppm` | medium | 246.343 ms | 374.196 ms | 34.17% | | `wide_strip_3000x80.ppm` | tiny | 24.297 ms | 59.489 ms | 59.16% | | `wide_strip_3000x80.ppm` | small | 53.559 ms | 101.561 ms | 47.26% | | `wide_strip_3000x80.ppm` | medium | 152.553 ms | 263.361 ms | 42.07% | The reproducible JSON record is `build/bench_cpu_ort_diverse_current_20260813.json`. It is a local-machine measurement, not a guarantee for every processor or ONNX Runtime build. ### 2026-08-13: blocked recognizer transpose The recurrent PP-OCRv6 recognizer exchanges both `[N,T,C] <-> [N,C,T]` and `[N,H,C,T] <-> [N,H,T,C]` layouts. Their dedicated copy paths now transpose `32x32` cache tiles instead of continually evicting a full source/destination matrix row. The operation is a pure element relocation, so it preserves values bit-for-bit and applies to scalar, AVX2, AVX-512, and NEON builds. On this AVX-512 host with `PPOCR_BENCH_THREADS=16`, three independent runs of 12 timed English-page inferences changed mean latency from **32.120 to 30.530 ms** (tiny, **4.95%**), **62.793 to 64.053 ms** (small, **-2.01%**), and **192.745 to 190.614 ms** (medium, **1.11%**). The small-model result is within host scheduling noise and is recorded rather than promoted as a broad claim; no model-specific switch is required. Raw paired samples are `build/bench_transpose_tile_{baseline,tiled}_20260813.json`. Text parity against ORT remained 3/3 across English, ultra-wide, and low-contrast inputs for all tiers in `build/accuracy_transpose_tiled_core_20260813.json`. The same current medium build passed the full small-to-4K-to-small memory ladder: `3840x2160` was **149,356,544 B** private / **153,313,280 B** working set, the immediate following 1080p sample fell to **148,262,912 B** / **152,317,952 B**, and the final `64x64` sample was **148,324,352 B** / **152,129,536 B**. Full per-resolution evidence: `build/memory_ladder_medium_cpu_transpose_tiled_20260813.json`. ### 2026-08-13: CPU/Hybrid batch recheck and GPU-only boundary The public three-policy contract was rechecked on the local Radeon 780M: Vulkan loader and compute are present, while `gpu_only` explicitly rejects construction because PP-OCRv6 still needs device implementations for its complete detector, recognizer, preprocessing, and DB/CTC postprocessing graphs. It does not silently execute on CPU. Hybrid remains usable and continues to select each Vulkan segment only after its complete synchronous `H2D + dispatch + fence + D2H` path is correct and no slower than CPU. With four same-shape English pages, `det_batch_size=4`, `rec_batch_size=4`, one graph worker, and two timed iterations, the verified `RecognizeBatch()` result-identical speedups over four serial calls were **1.589x / 1.256x / 1.140x** on CPU for tiny/small/medium and **1.598x / 1.323x / 1.225x** in Hybrid. This is real cross-page NCHW batching, not padding unrelated page sizes. The raw output is `build/bench_batch4_cpu_hybrid_20260813.json`. ### 2026-08-13: AVX-512 recognizer GEMM tile admission The AVX-512 GEMM implementation retains its two-row and four-row kernels, but the four-row tile is now opt-in (`PPOCR_ENABLE_AVX512_GEMM4ROW=1`) rather than the default. A paired `PPOCR_BENCH_THREADS=16` measurement showed that the larger register footprint is beneficial only for some medium shapes and regresses the shipped workload overall. With the default two-row tile and three warm-ups/15 runs, current means were **28.885 / 64.840 / 201.871 ms** on the English page and **20.263 / 48.142 / 134.558 ms** on the 3000x80 strip for tiny/small/medium. The control data is `build/bench_gemm4row_{ab_current,off_repeated}_20260813.json` and the final default record is `build/bench_gemm2row_default_20260813.json`. Text parity remains **3/3** for each model on English, ultra-wide, and low-contrast inputs against ONNX Runtime: `build/accuracy_gemm2row_core_20260813.json`. This change keeps the exact per-row K-order and applies only after dynamic AVX-512 capability checks; AVX2, NEON, and scalar dispatch are unchanged. ### 2026-08-13: hot-path ISA configuration caching The runtime ISA policy remains dynamic (CPUID/XCR0 is still checked before entering AVX-512), but process-scoped environment switches for pointwise tiles, 2x2 tiles, wide tiles, and the 2x2 SAME kernel are now cached once. These predicates occur repeatedly in the detector and recognizer; eliminating repeated environment lookups reduces dispatcher overhead without changing an operator's arithmetic or the AVX2/NEON/scalar fallback. On the current 16-worker AVX-512 host, the final English-page smoke measured **30.462 ms** tiny, **62.268 ms** small, and **190.709 ms** medium. Raw result: `build/bench_dispatch_cache_current_20260813.json`. All three tiers retained 3/3 ORT decoded-text parity on English, ultra-wide, and low-contrast inputs (`build/accuracy_dispatch_cache_core_20260813.json`), and both CPU kernel and Vulkan primitive smoke tests passed. ### 2026-08-13: detector BatchNorm activation reuse The generic graph executor now mirrors the production CTC executor for a final-use plain BatchNorm: it applies the alias-safe affine transform directly to the dying input activation, then transfers the tensor name to the graph output. This removes one same-sized detector feature-map allocation/copy at eligible boundaries without changing the BatchNorm arithmetic or its dynamic coefficient handling. It is guarded by the existing `PPOCR_DISABLE_BATCHNORM_INPLACE=1` deployment A/B switch. All three model tiers retained 3/3 decoded-text parity against ONNX Runtime on English, ultra-wide, and low-contrast inputs (`build/accuracy_bn_inplace_core_20260813.json`); CPU and Vulkan smoke also passed. The refreshed medium full ladder passed with a 4K private/working-set sample of **148,881,408 B / 152,858,624 B**, immediately declining at the following 1080p image to **147,857,408 B**, then returning to **143,728,640 B / 147,775,488 B** on the final 64x64 image. Complete records: `build/memory_ladder_medium_cpu_bn_inplace_20260813.json` and `build/bench_bn_inplace_current_20260813.json`. ### 2026-08-13: single-pass SIMD CTC ArgMax AVX-512 and AVX2 CTC ArgMax no longer scan each vocabulary row twice. The SIMD reduction now retains each lane's source index as it computes its local maximum, then performs one scalar lane/tail reduction with the same strict `>` first-maximum rule as the portable path. This removes a second full read of each 6,906/18,710-class logits row while preserving the scalar fallback for malformed NaN logits. The kernel smoke now explicitly covers ties across SIMD lanes and vector tails. The current 16-worker English-page smoke was **29.751 ms** tiny, **60.676 ms** small, and **226.997 ms** medium (`build/bench_argmax_single_pass_20260813.json`); timing remains host-noisy, so this record is evidence of correctness rather than a broad end-to-end speed claim. All three tiers retained 3/3 ORT decoded-text parity on English, ultra-wide, and low-contrast samples (`build/accuracy_argmax_single_pass_core_20260813.json`), and the Radeon 780M Vulkan smoke passed. ### 2026-08-13: diverse-shape correctness and terminal-CTC admission recheck The current CPU implementation was additionally checked against ONNX Runtime on `en.ppm`, the `3000x80` ultra-wide strip, and `noisy_lowcontrast.ppm` for all three model tiers. Decoded line count and text parity passed **3/3** for tiny, small, and medium; the durable report is `build/accuracy_diverse_core_20260813.json`. A short stress smoke on the low-contrast page and a `80x3000` ultra-tall strip also exercised the opposite detector resize branch, where it found 3 and 20 text regions, respectively, for every tier. The narrow vertical strip does not yet have text parity with this repository's ORT reference, so it is retained as a throughput/robustness sample rather than being claimed as a correctness gate. The bounded terminal-GEMM/CTC path was then A/B measured with three warm-ups and 15 timed iterations per normal and ultra-wide input. It improves tiny on this host (English page **35.105 -> 31.379 ms**) but is neutral/negative for small and slower for medium's ultra-wide case (**145.585 -> 276.362 ms**). It therefore remains explicit opt-in via `PPOCR_ENABLE_FUSED_TERMINAL_CTC=1`; the default executor remains the faster cross-tier choice. Raw A/B data: `build/bench_terminal_ctc_ab_20260813.json`. ### 2026-08-13: medium detector AVX-512 tile-pressure recheck The medium-detector profile identifies regular `3x3` and `5x5`/`7x7` context convolutions as the main remaining CPU hotspots. Their eight-output AVX-512 tiles were rechecked against the established four-output kernels. On this client AVX-512 host, the larger tiles increase register pressure and can produce substantial low-contrast tail latency: over eight timed iterations, the medium `noisy_lowcontrast.ppm` median was **554.202 ms** with the default four-output tiles versus **571.360 ms** with both eight-output experiments; the latter's p95 reached **2569.152 ms**. The wide-strip median was likewise **159.735 ms** (four) versus **163.488 ms** (eight). Consequently, the proven four-output 3x3/wide-context tiles are now the default. Eight-output variants remain opt-in for a separately measured server CPU via `PPOCR_ENABLE_AVX512_CONV3X3_TILE8=1` and `PPOCR_ENABLE_AVX512_CONV_WIDE8=1`; setting either `PPOCR_DISABLE_*` switch still disables its matching experiment. Text parity remains 2/2 for tiny, small, and medium on English and ultra-wide input; reports are `build/accuracy_{tiny,small,medium}_avx512_tile4default_20260813.json`. The refreshed medium CPU resolution ladder also passed. Its 4K private sample was **149,164,032 B**, the immediate following 1080p sample was **149,123,072 B**, and the final 64x64 sample was **148,738,048 B**. Private bytes and working set both declined after the 4K image. Full evidence is `build/memory_ladder_medium_cpu_avx512_tile4default_20260813.json`. ### 2026-08-13: AVX-512 2x2 SAME_UPPER detector-stem kernel The detector's early `2x2` SAME_UPPER Conv nodes now use a dedicated AVX-512 interior kernel with explicit final-row/final-column zero-padding handling. This avoids generic variable-kernel address arithmetic on the large feature maps while retaining runtime AVX2/scalar fallbacks. The low-level smoke suite checks odd/even dimensions, vector/scalar tails, and fused ReLU. Decoded text parity against ONNX Runtime passed **2/2** (`en.ppm`, `wide_strip_3000x80`) for tiny, small, and medium; reports are `build/accuracy_{tiny,small,medium}_avx512_same2x2_20260813.json`. On the local AVX-512 host the A/B is strongly workload dependent because the shared machine has substantial scheduling variance. Repeated medium samples put the specialized median around **181.580閳?32.008 ms**, versus **187.559閳?08.236 ms** with the generic path; tiny/small consistently improved on the short sample. It remains enabled because it removes the generic hot loop, passes the complete correctness gates, and keeps a one-variable runtime escape hatch for deployments whose AVX-512 frequency/cache balance differs. ### 2026-08-13: current ascending-resolution memory recovery measurement `bench/generate_memory_ladder.py` generates deterministic PPM inputs from `16x16` through `3840x2160`, including wide and tall 1,048,576-pixel strips. The sequence then processes `1920x1080`, both strips again, and a final `64x64` image. `ppocr_mem_soak` keeps one OCR instance alive, releases each decoded image before sampling, and records Windows private bytes and working set after every inference. The current medium CPU two-cycle measurement passed all bounded-return gates. In cycle 1, the `3840x2160` sample was **146,067,456 B** private / **150,147,072 B** working set; the final `64x64` sample returned to **143,634,432 B** / **147,345,408 B**, below the 4K sample in both measures. Cycle 2 similarly returned from **146,886,656 B** / **150,986,752 B** at 4K to **146,767,872 B** / **150,581,248 B** at the final small image. The first post-4K 1080p sample declined in both measures in cycle 2; cycle 1 retained a small amount of reusable allocator capacity before declining by the final small-image checkpoint. This is a single-host allocator measurement, not an assertion that an OS must decommit capacity immediately. Use `--require-post-large-decline` to make immediate private-byte *and* working-set decline a strict CI gate when that stronger condition is required; the default remains bounded return because Windows allocator behavior is non-deterministic. The complete per-resolution record is `build/memory_ladder_medium_cpu_current_bounded_recovery_20260813.json`. ### 2026-08-13: verified current resolution-memory regression The reproducible small-to-large-to-small test remains part of the native regression suite. It generates and processes, in order, `16x16 -> 64x64 -> 160x120 -> 320x240 -> 640x480 -> 1280x720 -> 4096x256 -> 256x4096 -> 1920x1080 -> 2560x1440 -> 3000x1600 -> 3840x2160 -> 1920x1080 -> 4096x256 -> 256x4096 -> 64x64`. Every PPM is decoded, inferred, and destroyed before collecting Windows private bytes and working set, while one OCR instance remains alive. This tests both varied resolution growth and whether capacity is bounded after a large page. On the current medium CPU build (16 kernel workers), the fresh two-cycle run passed all 20% bounded-return gates. Cycle 1 returned from the 4K sample's **147,230,720 B** private / **150,204,416 B** working set to **143,486,976 B** / **147,460,096 B** at final `64x64`. Cycle 2 showed an immediate 4K-to-1080p decline in both measures (**148,221,952 B** / **152,256,512 B** to **147,570,688 B** / **151,240,704 B**); its final small sample remained below its pre-return peak. The immediate-decline condition is reported separately because Windows heap reclamation is allocator-run dependent; the default regression correctly gates bounded recovery rather than requiring decommit on every run. Full per-resolution evidence: `build/memory_ladder_medium_cpu_current_resolution_recovery_20260813.json`. ### 2026-08-14: exact-size preprocessing fast path The fused RGB-to-BGR/NCHW preprocessor now recognizes an exact-size resize region. In that case bilinear interpolation is an identity, so it writes the three normalized planes directly, without a horizontal-coordinate table or four-pixel interpolation work per output value. It preserves recognizer right-padding because it writes only the natural-width columns. The general bilinear path remains unchanged for every resized image. For deployment A/B, set `PPOCR_DISABLE_IDENTITY_RESIZE_FASTPATH=1` before creating an OCR object. The direct and generic paths produced identical native demo output for tiny, small, and medium on exact-size `line_en`, `line_zh`, and `single_word` inputs (`build/identity_resize_cpp_equivalence_20260814.json`). Each model also retained ORT decoded-text parity on an exact-size line, normal English page, and 3000x80 ultra-wide page (3/3 each; `build/accuracy_identity_resize_cached_{tiny,small,medium}_20260814.json`). Kernel and Radeon 780M Vulkan smoke passed. A fresh medium CPU resolution ladder remained bounded after the change: the 4K sample was **148,512,768 B** private / **152,322,048 B** working set and the final `64x64` sample was **148,197,376 B** / **151,797,760 B**, below the pre-return peak in both metrics. Full record: `build/memory_ladder_medium_cpu_identity_resize_20260814.json`. The local 16-worker, CPU-only 100-run line benchmark was host-noisy on tiny, but gave a stable medium improvement: **72.046 ms -> 67.421 ms** mean (**6.42%** reduction) and **71.770 ms -> 67.466 ms** median. Small and tiny are already dominated by model execution on this single-line workload, so this is recorded as a targeted preprocessing win rather than a claimed uniform end-to-end speedup. Raw A/B record: `build/bench_identity_resize_cached_ab_20260814.json`. ### 2026-08-14: recognizer average-pool compaction All three PP-OCRv6 recognizers contain the same valid `AveragePool(3x2, stride=3x2)` bridge between convolutional features and the transformer. A dedicated NCHW kernel now executes that exact non-overlapping geometry, and when its input has a graph-proven final use it compacts the result into the front of the existing activation allocation before resizing it. This removes one bridge-output allocation and releases the unused capacity before the transformer. `PPOCR_DISABLE_AVERAGE_POOL3X2_FASTPATH=1` and `PPOCR_DISABLE_AVERAGE_POOL3X2_INPLACE=1` are narrow A/B fallbacks. The in-place route produced identical native demo output for tiny, small, and medium on `line_en`, `line_zh`, `single_word`, and `en` (`build/average_pool3x2_inplace_cpp_equivalence_20260814.json`). Each model also retained ORT decoded-text parity on an exact-size line, English page, and ultra-wide strip (3/3 each; `build/accuracy_averagepool_inplace_{tiny,small,medium}_20260814.json`). The current medium resolution-memory ladder passed: it returned from the pre-return private/working-set peak of **149,241,856 B / 153,153,536 B** to **143,618,048 B / 147,505,152 B** at final `64x64`. `build/memory_ladder_medium_cpu_averagepool_inplace_20260814.json` preserves every resolution sample. On the local 16-worker CPU, 100-run one-line A/B was positive for medium (**69.535 -> 68.032 ms**, 2.16%) and within shared-host noise for tiny/small. It is therefore a verified allocation reduction and a targeted medium throughput gain, not a uniform speed claim. Raw results: `build/bench_averagepool_inplace_ab_20260814.json`. The Radeon 780M Vulkan smoke and hybrid batch parity passed; hybrid continues to admit a Vulkan segment only after its complete H2D, dispatch, fence, D2H, and CPU comparison is no slower. ### 2026-08-14: attention QKV vector-copy transpose The small and medium recognizer attention blocks export a QKV projection as `[N,T,3,H,D]`, followed by the fixed ONNX permutation `[2,0,3,1,4] -> [3,N,H,T,D]`. The innermost head feature vector `D` is contiguous before and after that permutation. The executor now copies this vector with `memcpy` rather than recalculating five-dimensional scalar coordinates for every float. All other transposes retain the generic path. Set `PPOCR_DISABLE_QKV_TRANSPOSE_FASTPATH=1` before constructing OCR to A/B the generic route. Decoded-text parity against ONNX Runtime passed on English, Chinese, and 3000x80 ultra-wide inputs for tiny, small, and medium (3/3 for each tier; `build/accuracy_qkv_transpose_{tiny,small,medium}_20260814.json`). Tiny has no QKV attention block and remains unaffected. On the local AVX-512 CPU, the 40-run English-page sample measured small **74.557 -> 70.835 ms** (5.0%) and medium **217.134 -> 199.390 ms** (8.2%). On the distinct 30-run ultra-wide workload it measured small **44.199 -> 41.999 ms** (5.0%) and medium **141.816 -> 139.679 ms** (1.5%). The one page measurements are host-specific; raw records are `build/bench_qkv_transpose_ab_20260814.json` and `build/bench_qkv_transpose_wide_ab_20260814.json`. The current medium CPU small-to-4K-to-small ladder stayed bounded: the final 64x64 sample was **143,474,688 B** private / **147,525,632 B** working set, below both the 4K sample (**146,661,376 B / 150,781,952 B**) and the pre-return peak. Full per-resolution evidence is `build/memory_ladder_medium_cpu_qkv_transpose_20260814.json`. ### 2026-08-14: QKV transpose/slice elimination The small and medium attention exports immediately split the just-transposed `[3,N,H,T,D]` QKV activation into three `Slice(1) -> Squeeze(0)` branches. The loader now recognizes this complete three-branch topology and replaces it with three direct copies from the original `[N,T,3,H,D]` projection into their already-squeezed `[N,H,T,D]` destinations. This eliminates the full transposed QKV activation and the three temporary slice buffers. It is a strict model-shape fusion; all general Transpose/Slice graphs retain their ordinary execution. `PPOCR_DISABLE_QKV_SLICE_FUSION=1` is the deployment A/B fallback. On the local AVX-512 CPU, the 20-run English-page A/B, with the earlier vector-copy transpose still enabled in the baseline, measured small **76.315 -> 72.245 ms** (5.3%) and medium **222.274 -> 195.827 ms** (11.9%). The baseline values include the prior QKV-vector optimization, so this records the additional benefit of removing the full transpose/slice lifetime rather than double-counting it. Raw record: `build/bench_qkv_slice_fusion_ab_20260814.json`. The default fused route retained ORT decoded-text parity on English, Chinese, and 3000x80 ultra-wide input for all three shipped model tiers (3/3 each; `build/accuracy_qkv_slice_fusion_default_{tiny,small,medium}_20260814.json`). Tiny has no matching transformer QKV topology and is unchanged. The medium ascending-resolution memory gate passed: final `64x64` was **143,544,320 B** private / **151,076,864 B** working set, below the 4K sample and pre-return peak. Full record: `build/memory_ladder_medium_cpu_qkv_slice_fusion_20260814.json`. CPU kernel and Radeon 780M Vulkan smoke also passed; hybrid admission policy remains unchanged and selects only complete GPU segments that are no slower than CPU. ### 2026-08-14: complete QKV split lifetime and resolution-memory recheck The three direct Q/K/V copies are now emitted by one `FusedQkvSplit` node with three outputs. This keeps the shared `[N,T,3,H,D]` projection live only until all three final `[N,H,T,D]` tensors have been materialized, then retires it through the normal use counter. It is a PP-OCRv6 topology-specific memory fusion, not general multi-output ONNX runtime support. The existing native resolution ladder provides the requested ascending image coverage: `16x16`, `64x64`, `160x120`, `320x240`, `640x480`, `1280x720`, wide/tall `4096x256` and `256x4096`, `1920x1080`, `2560x1440`, `3000x1600`, and `3840x2160`, followed by a staged recovery tail and `64x64` return. A fresh two-cycle medium CPU run passed its 20% bounded-recovery gate. In cycle 1, private / working-set memory declined from **149,700,608 / 153,661,440 B** at 4K to **147,795,968 / 151,752,704 B** at the immediately following 1080p page, then finished at **147,509,248 / 151,162,880 B** on the return 64x64. Cycle 2 likewise declined immediately after 4K; the final small sample was **147,435,520 / 151,105,536 B**, only **0.27%** above the warmed private-byte baseline. Per-resolution evidence is `build/memory_ladder_medium_cpu_qkv_split_20260814.json`. Windows allocator capacity is reusable and samples may vary, so the gate verifies bounded recovery rather than demanding that every allocation be decommitted. The fused executor also retained decoded-text parity with ONNX Runtime on English, Chinese, and 3000x80 ultra-wide inputs (3/3 medium cases; `build/accuracy_qkv_split_default_medium_20260814.json`). CPU kernel and Radeon 780M Vulkan smoke tests passed after the change. A final all-tier recheck repeated those three text/diverse-size cases for tiny, small, and medium (**3/3 each**): `build/accuracy_qkv_split_{tiny,small,medium}_20260814.json`. The 20-run CPU snapshot (16 workers, exact GELU) was tiny **30.920 / 21.393 ms**, small **62.485 / 44.688 ms**, and medium **198.325 / 132.951 ms** for English / 3000x80, respectively (`build/bench_cpu_diverse_qkv_split_20260814.txt`). The former is timing evidence for this host only; the durable cross-engine comparison remains the longer, independently measured CPU-versus-ORT corpus. The final one-cycle 16-sample medium ladder also passed its bounded-return gate (`build/memory_ladder_medium_cpu_qkv_split_final_20260814.json`), with the return `64x64` footprint below the pre-return peak. Its immediate 4K sample did not decline in that isolated Windows run, so the report records that fact rather than claiming forced allocator decommit. ### 2026-08-14: extended 8K source-image recovery gate The generated memory corpus now continues beyond 4K with `5120x2880` and `7680x4320` pages, then immediately processes a `3840x2160` recovery page before the existing 1080p, extreme-aspect-ratio, and final-`64x64` tail. The native soak keeps one OCR instance alive but scopes every decoded RGB image to `load -> Recognize -> destroy -> sample`; this specifically tests source image release as well as activation reuse. It is intentionally more demanding than the detector's 960-pixel inference cap, because the decoded 8K source itself is about 95 MiB. With medium CPU PP-OCRv6, one strict cycle passed with `--require-post-large-decline`: private / working-set memory fell from **149,139,456 / 153,059,328 B** after 8K to **146,808,832 / 150,470,656 B** at the immediate 4K recovery (**-2,330,624 B / -2,588,672 B**). The final `64x64` was **147,595,264 / 151,126,016 B**, below the pre-return peaks (**149,151,744 / 153,059,328 B**) and only **3.03%** above the warmed private baseline. Full 19-resolution evidence is `build/memory_ladder_medium_cpu_8k_strict_20260814.json`. This is a host-specific bounded-recovery observation; it does not promise every Windows heap or Vulkan driver immediately decommits reusable pages. ### 2026-08-14: reproducible small-to-8K memory ladder `bench/generate_memory_ladder.py` now creates 19 deterministic P6 PPM pages in ascending source resolution from `16x16` through `5120x2880` and `7680x4320`, followed by a `3840x2160 -> 1920x1080 -> wide/tall -> 64x64` recovery tail. `bench/memory_ladder_regression.py` keeps one native OCR instance alive, destroys each decoded source image immediately after recognition, and records private bytes plus working set after every page. The strict gate compares the 5K/8K high-water with the deliberate post-8K recovery tail (`4K -> 1080p -> wide/tall`), and independently bounds the final `64x64` footprint. Windows allocator/working-set accounting is not guaranteed to be monotonic across adjacent samples, so an optional `--require-immediate-post-large-decline` diagnostic is available, but the default strict test requires both counters to fall below the large-page high-water somewhere in that recovery tail. This avoids presenting sampling jitter as a leak while still proving that the process returns from a large page under the same live OCR instance. ### 2026-08-14: recognition-worker lifetime hardening The production CTC executor no longer stores its compact use-counter vector or node-input pointer scratch vector in `thread_local` storage. Those two small vectors are now invocation-local (the pointer list reserves eight entries). This is deliberately a lifetime fix rather than an allocator cache: on the MinGW/Windows build, CTC execution can run on short-lived outer recognition workers, and emulated-TLS vector destructors may otherwise be registered through a different pthread DLL during worker teardown. The result was an intermittent `0xC0000374` heap-corruption process exit after a successful long memory ladder. Activation tensors themselves remain per-invocation and are released by the existing graph use-count planner. The current medium CPU 19-resolution ladder completed and exited cleanly. Private / working-set memory fell from **149,811,200 / 153,808,896 B** on the 8K source page to **147,935,232 / 151,924,736 B** on its immediate 4K recovery page; the final `64x64` sample was **143,974,400 / 147,787,776 B**, only **0.45%** above the warmed private-byte baseline and below the complete large-page high-water marks. Full record: `build/memory_ladder_medium_cpu_8k_tls_fix_20260814.json`. CPU and hybrid runs both completed the varied `16x16`, English, 3000x80 ultra-wide, and 80x3000 ultra-tall corpus for tiny/small/medium (**12/12 per backend**): `build/accuracy_tls_lifetime_fix_{cpu,hybrid}_diverse_20260814.json`. Kernel smoke and Radeon 780M Vulkan smoke also passed. Hybrid retains its strict per-shape whole-boundary no-slower admission rule; this change does not claim that the incomplete device-resident OCR graph supports GPU-only mode. ### 2026-08-14: fresh small-to-large memory regression The checked-in generator and regression gate were rerun against the medium PP-OCRv6 CPU pipeline. The deterministic corpus contains 19 PPM inputs: `16x16 -> 64x64 -> 160x120 -> ... -> 5120x2880 -> 7680x4320`, followed by `3840x2160 -> 1920x1080 -> wide/tall strips -> 64x64`. It therefore measures both normal increasing resolutions and source-image release after the 8K page, while retaining one OCR instance for the whole sequence. The strict gate passed for both Private Bytes and Working Set. The 8K sample was **148,996,096 / 152,911,872 B**; the immediately following 4K recovery sample was **147,918,848 / 151,826,432 B** (**-1,077,248 / -1,085,440 B**). The final 64x64 sample was **143,548,416 / 147,689,472 B**, only **0.25%** above the warmed Private-Bytes baseline and below the pre-return high-water mark. The complete per-resolution record is `build/memory_ladder_medium_cpu_strict_final_20260814.json`. Reproduce it with: ```powershell python bench/generate_memory_ladder.py --output build/memory_ladder_images python bench/memory_ladder_regression.py --cpp build/ppocr_mem_soak.exe --det D:/workprj/aicoder/.tmp/ocr-models/ppocrv6_medium_det.onnx --rec D:/workprj/aicoder/.tmp/ocr-models/ppocrv6_medium_rec.onnx --dict build/dict_ppocrv6_latin.txt --images build/memory_ladder_images --cycles 1 --backend cpu --require-post-large-decline --report build/memory_ladder_medium_cpu_strict.json ``` ### 2026-08-14: current resolution-memory validation The reproducible ladder was generated again and run with one live medium CPU OCR instance. It records Private Bytes and Working Set after every one of the 19 pages, with decoded PPM storage destroyed before each sample. The run passed the bounded-return gate: | Checkpoint | Private Bytes | Working Set | | --- | ---: | ---: | | warmed baseline | 143,437,824 B | 閳?| | 8K (`7680x4320`) | 148,480,000 B | 152,453,120 B | | first 4K recovery | 148,000,768 B | 151,769,088 B | | final `64x64` | 146,055,168 B | 150,097,920 B | Both counters fell immediately after 8K in this run; the final small image was **1.82%** above the warmed private-byte baseline and below the complete pre-return peak. The full per-resolution evidence, including the recovery tail decision, is `build/memory_ladder_medium_cpu_strict_current_20260814.json`. Results are machine/run-specific process accounting, not a fixed cross-platform memory guarantee. ### 2026-08-14: revalidated varied-resolution memory recovery The 19-image ascending-and-recovery ladder was regenerated and rerun with one medium PP-OCRv6 CPU instance (`PPOCR_BENCH_THREADS=16`). Each PPM is loaded, recognized, destroyed, and only then sampled, so source RGB storage from a large page cannot be hidden by retaining the input vector. The strict post-large recovery gate passed for both Windows Private Bytes and Working Set: | Checkpoint | Private Bytes | Working Set | | --- | ---: | ---: | | warmed baseline | 142,888,960 B | n/a | | 5K peak (`5120x2880`) | 148,463,616 B | 152,395,776 B | | 8K (`7680x4320`) | 146,882,560 B | 151,015,424 B | | first 4K recovery | 147,435,520 B | 151,506,944 B | | final `64x64` | 146,251,776 B | 150,216,704 B | The observed peak was the 5K page rather than 8K, which is valid for Windows process accounting and the detector's bounded tensor resize. Both counters were already below that high-water mark at the first 4K recovery sample; the final small image remained below it and was 2.35% above the warmed private-byte baseline. The machine-specific, per-resolution JSON evidence is `build/memory_ladder_medium_cpu_strict_revalidated_20260814.json`. ### 2026-08-14: zero-copy top-level ONNX protobuf traversal Model construction no longer copies the complete embedded ONNX `GraphProto` before parsing it. The model reader now exposes bounded protobuf byte spans; the top-level graph is parsed directly from the already-owned model file buffer. Nested node, attribute, initializer, and operator-set messages retain their existing owning parse paths, so the supported PP-OCRv6 graph semantics and runtime's no-ONNX-Runtime dependency remain unchanged. This removes one transient full-model allocation/copy during startup and lowers model-loading work without altering per-inference numerical operations. On the local Windows AVX-512 host (`PPOCR_BACKEND=cpu`, 16 kernel workers, one warm-up and five English-page measurements), constructor-to-ready load time changed as follows: | Model | Before | After | Lower load time | | --- | ---: | ---: | ---: | | tiny | 27.184 ms | 17.808 ms | 34.5% | | small | 102.254 ms | 72.066 ms | 29.5% | | medium | 519.523 ms | 294.587 ms | 43.3% | The per-model evidence is `build/bench_model_load_span_parse_ab_20260814.json`; the post-change inference snapshot is `build/bench_model_load_span_parse_20260814.json`. Kernel and Vulkan admission smoke passed. CPU and hybrid also completed the five-size `16x16`, English, low-contrast, ultra-wide, and ultra-tall corpus for tiny/small/medium (30 successful executions per backend); the recorded outputs are `build/smoke_span_parse_diverse_{cpu,hybrid}_20260814.json`. One medium ultra-wide confidence differs only in the last reported digits in hybrid because its transfer-inclusive channel-affine/depthwise-Swish admission can select a numerically valid Vulkan segment; decoded text and geometry are unchanged. GPU-only remains deliberately rejected until the complete graph is device-resident. ### 2026-08-14: invocation-local executor input scratch reuse Both the general graph executor and the production CTC executor now reuse one invocation-local, non-owning node-input pointer vector. Previously, the general path allocated a separate small vector at each generic operator and at several in-place fast paths. The new scratch has eight reserved entries (larger future operator signatures still grow it safely), is never shared across OCR calls, and retains the existing no-TLS lifetime rule for MinGW worker teardown. This removes repeated allocator traffic without changing tensors, reduction order, CPU SIMD dispatch, or the Vulkan admission policy. A fresh CPU English snapshot (`PPOCR_BENCH_THREADS=16`, two warm-ups, twelve runs) measured **30.817 / 61.215 / 182.730 ms** for tiny/small/medium. The varied-shape CPU snapshot additionally completed ultra-wide at **19.090 / 44.437 / 127.590 ms** and dense `1920x1080` at **265.831 / 1133.132 / 4880.140 ms** (three-run shared-host samples, therefore directional rather than portability claims). The 19-resolution medium memory ladder passed again: its 5K high-water was **149,815,296 / 153,812,992 B** (Private Bytes / Working Set), the first 4K recovery was **146,288,640 / 150,216,704 B**, and final `64x64` was **146,501,632 / 150,388,736 B**, still below the pre-return high-water. The complete evidence is `build/memory_ladder_medium_cpu_input_scratch_reuse_20260814.json`. All 15 tiny/small/medium varied-size cases completed in both CPU and hybrid; their decoded text and geometry agree across backends. Kernel and Radeon 780M Vulkan admission smokes passed, with channel-affine Swish and depthwise-Swish still selected only when measured full-boundary GPU cost is no slower than CPU. Records: `build/bench_input_scratch_reuse_{en,diverse}_20260814.json` and `build/smoke_input_scratch_reuse_diverse_{cpu,hybrid}_20260814.json`. ### 2026-08-14: small-to-large image memory recovery regression The repository includes a native process-memory regression for long-lived OCR services. `bench/generate_memory_ladder.py` produces deterministic PPM pages that increase from `16x16` through `7680x4320`, including wide and tall aspect-ratio cases, then process a staged `4K -> 1080p -> wide/tall -> 64x64` recovery tail. `bench/memory_ladder_regression.py` keeps one OCR instance alive and records Windows Private Bytes and Working Set after every resolution. Run the full test with: ```powershell $env:PPOCR_BENCH_THREADS = '16' python bench/generate_memory_ladder.py --output build/memory_ladder_images python bench/memory_ladder_regression.py ` --cpp build/ppocr_mem_soak.exe ` --det D:/workprj/aicoder/.tmp/ocr-models/ppocrv6_medium_det.onnx ` --rec D:/workprj/aicoder/.tmp/ocr-models/ppocrv6_medium_rec.onnx ` --dict build/dict_ppocrv6_latin.txt ` --images build/memory_ladder_images --cycles 1 --backend cpu ` --require-post-large-decline ` --report build/memory_ladder_medium_cpu_compact_uses_20260814.json ``` On the current Windows x86 host, the test passed after the compact executor use-count change. The `7680x4320` sample reached **150,474,752 B / 154,353,664 B** (Private Bytes / Working Set). The recovery tail fell to **147,111,936 B / 151,142,400 B** at `1920x1080`, and the final `64x64` checkpoint was **148,066,304 B / 151,736,320 B**. Thus both counters declined below the post-large high-water without restarting the OCR instance. The adjacent first 4K recovery measurement may briefly exceed the 8K sample because of Windows allocator/working-set accounting; the enforced gate intentionally verifies a decline in the complete post-large recovery tail instead of claiming a brittle single-sample monotonic property. Full per-resolution evidence is `build/memory_ladder_medium_cpu_compact_uses_20260814.json`. ### 2026-08-14: refreshed exact detector batching across three model sizes The true same-shape detector NCHW batch was rerun after the memory work with four copies of the English page. The check compares every text, quadrilateral, bounding box, and confidence against four serial calls. Recognition retained `rec_batch_size=1`: an investigation confirmed that forcing four recognizer crops through the transformer can change exact CTC confidence on this host, so it is not used for the exact-output benchmark. With `PPOCR_BENCH_THREADS=16`, `PPOCR_DET_BATCH_SIZE=4`, `PPOCR_REC_BATCH_SIZE=1`, and `PPOCR_BATCH_PREPROCESS_PARALLELISM=4`, the three-run means were: | Model | CPU serial | CPU batch | CPU speedup | Hybrid serial | Hybrid batch | Hybrid speedup | | --- | ---: | ---: | ---: | ---: | ---: | ---: | | tiny | 133.272 ms | 76.211 ms | 1.749x | 157.468 ms | 128.652 ms | 1.224x | | small | 282.395 ms | 177.486 ms | 1.591x | 442.434 ms | 390.907 ms | 1.132x | | medium | 1241.956 ms | 1013.146 ms | 1.226x | 1230.259 ms | 1138.408 ms | 1.081x | The corresponding exact-parity record is `build/batch_detector_rec1_current_20260814.json`. Hybrid remains an honest per-segment policy: Vulkan is selected only when the full measured host-to-device, dispatch, and device-to-host boundary is no slower than CPU; on this Radeon 780M run, small channel-affine Swish and depthwise-Swish segments met that rule while the remainder correctly stayed on SIMD CPU. ### 2026-08-14: verified four-page detector NCHW batching `RecognizeBatch()` was rechecked on four same-shape English pages for every PP-OCRv6 tier. The detector receives one real `[4,3,H,W]` tensor; each result, including text, quadrilateral, bounding box, and confidence, was checked against four serial `Recognize()` calls by `ppocr_batch_smoke`. Recognition remains `rec_batch_size=1` in this comparison: packing unequal recognizer widths by right-padding changes transformer attention and can alter CTC confidence, so it is not represented as exact-output acceleration. With 16 CPU workers, `det_batch_size=4`, four preprocessing workers, one warm-up, and five measured CPU iterations, serial-to-batch time was: | Model | Serial mean | Batched mean | Throughput gain | |---|---:|---:|---:| | tiny | 146.914 ms | 86.413 ms | 1.700x | | small | 318.376 ms | 204.255 ms | 1.559x | | medium | 1289.731 ms | 1011.596 ms | 1.275x | The corresponding hybrid recheck (three iterations; the same exact-output gate) measured **1.218x / 1.138x / 1.079x** for tiny/small/medium. Hybrid still admits each Vulkan segment only after its complete H2D + shader + D2H path is no slower than the matching CPU path; this is not a claim that the incomplete full graph supports `gpu_only`. The host-specific full record is `build/bench_batch_detector_nchw_current_20260814.json`. Reproduce the CPU measurement with: ```powershell $env:PPOCR_BACKEND='cpu'; $env:PPOCR_BENCH_THREADS='16' $env:PPOCR_DET_BATCH_SIZE='4'; $env:PPOCR_REC_BATCH_SIZE='1' $env:PPOCR_BATCH_PREPROCESS_PARALLELISM='4'; $env:PPOCR_BATCH_WARMUP='1'; $env:PPOCR_BATCH_RUNS='5' .\build\ppocr_batch_smoke.exe DET.onnx REC.onnx dict.txt 1 en.ppm en.ppm en.ppm en.ppm ``` ### 2026-08-13: Vulkan ConvTranspose + Add primitive groundwork The Vulkan backend now has a numerically tested `ConvTranspose(2x2, stride=2) + equal-shape Add` primitive. It keeps the projection in device buffer D, inserts a compute write/read barrier, adds the residual in a second dispatch, and copies only the final sum back. Its hybrid admission is strict: it measures and validates complete `H2D + two dispatches + fence + D2H` against the matching CPU SIMD sequence, then selects Vulkan only when no slower. On this Radeon 780M test shape (`N=4, Cin=32, Cout=40, 31x29`), GPU was **2.771 ms** versus CPU **0.535 ms**, so CPU correctly remains selected. This is backend coverage, not an end-to-end GPU speed claim. The current PP-OCRv6 FPN export folds its following `[1,C,1,1]` Add into the transpose bias, so the new equal-shape-residual primitive is intentionally not claimed as active for that model revision. `ppocr_vulkan_smoke` covers it; tiny/small/medium CPU text parity passed **2/2** each on `en.ppm` and `wide_strip_3000x80.ppm`, recorded in `build/accuracy_{tiny,small,medium}_convtranspose_add_20260813.json`. ## Build ### 2026-08-14: compact execution liveness and resolution-memory revalidation The detector's generic executor now resets compact `uint16_t` use counters indexed once at model load, rather than cloning a string-keyed use-count map for every page. This removes per-inference hash-table bookkeeping while retaining the existing last-use tensor release rules. The change applies only to the fixed PP-OCRv6 graph topology; arbitrary graph values still retain their original tensor ownership and output semantics. The CPU multi-size smoke completed tiny/small/medium over five distinct shapes (`16x16`, English, ultra-wide, ultra-tall, and dense `1920x1080`). The record is `build/bench_diverse_compact_uses_20260814.json`; its three-run means are intentionally retained as per-host measurement evidence rather than portable speed claims. Kernel and Radeon 780M Vulkan admission smokes also passed. Hybrid retains its full transfer-inclusive rule: a Vulkan segment is used only after measured `H2D + GPU + D2H` is no slower than the same CPU segment. GPU-only remains deliberately rejected because the complete graph is not yet device-resident. The 19-resolution medium CPU ladder was revalidated with one OCR instance alive throughout. It passed `--require-post-large-decline`: the `7680x4320` sample measured **147,972,096 B / 151,748,608 B** (Private Bytes / Working Set), then the recovery tail reached **146,042,880 B / 148,320,256 B** at the wide `4096x256` sample. The final `64x64` checkpoint was **147,783,680 B / 151,691,264 B**, below the largest-image private-byte value and below the pre-return high-water. Full per-resolution evidence is `build/memory_ladder_medium_cpu_compact_uses_final_20260814.json`. ### 2026-08-14: current ascending-resolution memory regression The same long-lived medium CPU instance was rerun over all 19 generated images, in ascending source sizes from `16x16` to `7680x4320`, followed by `3840x2160 -> 1920x1080 -> 4096x256 -> 256x4096 -> 64x64`. Each page is scoped to `load -> Recognize -> destroy -> sample`, so the measurements expose both inference activation reuse and whether a decoded large source image stays resident after its OCR call. The strict recovery gate passed. At 8K, Private Bytes / Working Set were **148,131,840 B / 152,174,592 B**. The immediately following 4K page was **147,681,280 B / 151,670,784 B**, a drop of **450,560 B / 503,808 B**. The final `64x64` checkpoint was **147,587,072 B / 151,429,120 B**, below both the 8K sample and the pre-return high-water (**150,106,112 B / 154,062,848 B**), without recreating the OCR instance. Per-resolution samples and the enforced pass/fail decision are recorded in `build/memory_ladder_medium_cpu_current_resolution_recovery_20260814.json`. ### 2026-08-14: refreshed three-tier CPU / Hybrid comparison `bench/compare.ps1` now accepts `-Backend cpu|hybrid|gpu` and passes the choice through to the native benchmark. This makes the CPU and Hybrid runs explicit in the same ONNX Runtime comparison harness instead of inheriting an ambient process environment. On the local AVX-512 host, `en.ppm`, 2 warm-ups and 6 measured runs, the native CPU implementation measured **32.315 ms** (tiny) and **67.419 ms** (small), versus ONNX Runtime CPUExecutionProvider **113.453 ms** and **149.804 ms**: **71.5%** and **55.0%** lower mean latency respectively. The same harness measured medium Hybrid at **239.765 ms** versus ONNX Runtime **333.395 ms** (**28.1%** lower). These are machine/run-specific samples; reproduce before treating them as deployment guarantees. The medium high-density `dense_blobs` stress bound was refreshed to `[100, 3600]`. The current native medium detector produced 2,903 valid components and the full 11-image varied-size stress suite passed. ### 2026-08-14: terminal CTC projection is now the default The recognizer's final vocabulary projection is now executed through the bounded `GemmCtcTop1` route by default. It computes each small time-step tile, performs the CTC argmax/probability reduction, and releases that tile rather than materializing the full `[N,T,V]` logit tensor. Set `PPOCR_DISABLE_FUSED_TERMINAL_CTC=1` only for an A/B comparison with the older materialized-logit route. On the dense `ui_dense_1920x1080` corpus the default route reduced measured CPU latency in this local run from **279.538 to 263.995 ms** for tiny (123 crops), **1077.122 to 965.587 ms** for small (119 crops), and **6285.383 to 5621.346 ms** for medium (122 crops). These samples are machine-specific; text parity against ONNX Runtime passed 4/4 varied inputs for each model after enabling the default. ### 2026-08-14: refreshed 16x16-to-8K memory recovery measurement The deterministic 19-image ladder was rerun with one live medium CPU OCR instance. It processes ascending source resolutions from `16x16` through `7680x4320`, then a `3840x2160 -> 1920x1080 -> 4096x256 -> 256x4096 -> 64x64` recovery tail. Every input is scoped to `load -> Recognize -> destroy -> sample`, so this measures both activation reuse and release of a large decoded RGB source image. The strict post-large recovery gate passed. The 5K/8K high-water was **150,675,456 B / 154,615,808 B** (Private Bytes / Working Set); the first 4K recovery sample was **150,036,480 B / 154,087,424 B**, and the final `64x64` sample was **148,877,312 B / 151,830,528 B**. The final small-image footprint was 3.63% above the warmed private-byte baseline and below both pre-return peaks, confirming bounded memory recovery without recreating the OCR instance. Full per-resolution evidence is `build/memory_ladder_medium_cpu_post_large_20260814.json`. Windows allocator accounting may fluctuate between adjacent samples; the enforced condition is that both counters return below the 5K/8K high-water somewhere in the post-large recovery tail. ### 2026-08-14: CTC tile-policy validation and final memory recheck The four-row bounded terminal-CTC projection remains the default. An experimental automatic eight-row choice for the wider recognizers was tested on the high-density `1920x1080` sample, but was slower in the paired local runs: small measured **914.427 ms** versus **897.369 ms** with four rows, and medium measured **4829.971 ms** versus **4529.301 ms**. The larger tile remains available only through `PPOCR_FUSED_TERMINAL_CTC_TILE_ROWS` for platform-specific experiments; it is not used as the portable default. After this decision, the complete 19-resolution medium CPU memory check passed again. Private Bytes / Working Set fell from **147,623,936 B / 151,322,624 B** at the 8K sample to **146,046,976 B / 149,807,104 B** at the first 4K recovery; final `64x64` was **143,917,056 B / 147,906,560 B** (**0.04%** above the warmed private-byte baseline), below the full pre-return high-water. The durable per-resolution record is `build/memory_ladder_medium_cpu_tile4_20260814.json`. ### 2026-08-14: recognizer hotspot profiling and Vulkan Hybrid revalidation `PPOCR_PROFILE=1` now reports the terminal-CTC executor even when its fused `GemmCtcTop1` path returns early. On the medium ultra-wide crop, that direct measurement identifies the terminal vocabulary projection as the largest recognizer operation (**40.456 ms** in this local profile), followed by the existing fused 1x1/residual convolutions. This makes future optimization work measurable instead of inferring recognizer cost from detector-only profiles. The Vulkan Hybrid policy was revalidated on AMD Radeon 780M. It measures each implemented segment as a complete synchronous `H2D + dispatch + D2H` path and selects GPU on `gpu_mean <= cpu_mean`; smoke selected GPU for channel-affine Swish (**0.193 vs 0.404 ms**) and depthwise-Swish (**0.341 vs 0.346 ms**), while transfer-bound primitives correctly remained on CPU. Tiny, small, and medium Hybrid text parity all passed on normal and ultra-wide inputs (6/6). GPU-only remains deliberately unavailable until the complete detector, recognizer, and post-processing graph is device-resident; it does not silently fall back to CPU. The AVX-512 four-row GEMM microkernel was also retested against the stable two-row default. It remains an explicit `PPOCR_ENABLE_AVX512_GEMM4ROW=1` deployment A/B option only: its effect varied between otherwise similar paired runs, so it is not enabled by default. ### 2026-08-14: medium terminal-CTC projection parallelism The terminal CTC projection now uses a bounded **32-row** vocabulary tile for the medium recognizer (`depth >= 384`, `vocab >= 1024`); tiny and small retain the prior four-row tile. This gives a single ultra-wide medium crop enough independent GEMM rows to use the persistent SIMD pool, while keeping CTC blank/repeat state serial in time order and the workspace bounded. The `PPOCR_FUSED_TERMINAL_CTC_TILE_ROWS=1|2|4|8|16|32` override remains available for deployment-side A/B. On this AVX-512 Windows host (16 workers), paired ultra-wide `3000x80` measurements improved medium from **189.408 ms** (four rows) to **150.969 ms** (32 rows), a **20.3%** reduction; small improved from **61.882 to 56.759 ms** in that isolated wide test, but dense-page runs were less stable, so its conservative default remains four rows. Medium dense `1920x1080` also improved from **5237.145 to 4671.601 ms** in the paired run. These figures are local host measurements, not portable latency guarantees. The associated 19-resolution medium CPU memory gate passed: 8K was **150,437,888 B / 154,501,120 B** (Private Bytes / Working Set), immediate 4K recovery was **148,398,080 B / 152,215,552 B**, and final `64x64` was **144,076,800 B / 148,070,400 B**, below baseline and all large-page high-water marks. Per-resolution evidence: `build/memory_ladder_medium_cpu_ctc32_20260814.json`. The same gate was rerun after the final CTC policy validation. It again passed the enforced `--require-post-large-decline` condition: the 5K/8K high-water was **149,454,848 B / 153,169,920 B**, the first post-8K 4K page fell to **146,534,400 B / 150,233,088 B**, and the final `64x64` checkpoint was **147,755,008 B / 151,703,552 B**. This is **0.37%** above the warmed **147,210,240 B** private-byte baseline and below both pre-return peaks, so large decoded images and inference workspaces do not remain pinned after the large-image sequence. The ladder deliberately covers 19 inputs from `16x16` through `7680x4320`, including wide/tall aspect-ratio branches and a staged `4K -> 1080p -> wide -> tall -> 64x64` recovery tail. Full JSON evidence: `build/memory_ladder_medium_cpu_ctc32_recheck_20260814.json`. ### 2026-08-14: CTC workspace reuse experiment rejected A CTC vocabulary-tile reuse experiment was measured on the 123/119/122-crop dense `1920x1080` page. Sharing one preallocated tile per in-flight sequence increased the medium batch workspace enough to cause a memory-recovery gate failure on one fresh run, while its latency did not improve reliably. The production implementation therefore retains a short-lived, per-worker bounded tile: it has a strict `32 * vocab`-float cap for medium and `4 * vocab` for tiny/small, releases promptly after each recognition task, and avoids unsafe MinGW thread-local destructors. This choice is deliberately evidence-led: the restored implementation passed the 19-resolution CPU memory gate again, with the first recovery-tail point below the 8K private/working-set high-water at `1920x1080` (**146,456,576 B / 149,090,304 B**) and final `64x64` still below the pre-return peak. Evidence: `build/memory_ladder_medium_cpu_ctc_workspace_reverted_20260814.json`. ### 2026-08-14: hardware-aware recognition worker bound The 12-way CPU recognition default is capped at `std::thread::hardware_concurrency()`. This retains the measured 12-way policy on the local 16-logical-core AVX-512 machine while preventing a small x86 or ARM target from constructing more independent crop graphs than its available logical CPUs. Hybrid deliberately keeps its separate `hybrid_graph_parallelism` policy because its Vulkan queue is serialized. The public `PPOCR_REC_PARALLELISM` override remains available for host-specific tuning. ### 2026-08-14: dense-page recognition scheduler tuning The CPU default `rec_parallelism` is now **12** (override with `PPOCR_REC_PARALLELISM`). A 16-worker AVX-512 sweep on the 123/119/122-crop dense `1920x1080` page measured 8-way versus 12-way versus 16-way outer recognition scheduling. Twelve was the stable cross-model choice: tiny improved from **282.675 to 241.928 ms** (14.4%), small from **949.373 to **885.617 ms** (6.7%), and medium from **3968.286 to 3905.875 ms** (1.6%). At 16 ways medium regressed to **4375.612 ms**, so the default deliberately leaves headroom for its larger SIMD kernels and avoids multiplying dense-page activation lifetime further. A fresh default run measured **236.679 / 854.346 / 4136.895 ms** for tiny/small/medium respectively (3/3, 3/3, and 2/2 measured samples). Normal and ultra-wide decoded-text parity against ORT remained 6/6. The 19-resolution medium memory gate also passed: after the 8K page, Private Bytes / Working Set fell from **149,106,688 / 153,096,192 B** to **147,845,120 / 151,638,016 B** at the first 4K recovery page; the final `64x64` checkpoint remained below the large-page high-water. Evidence: `build/memory_ladder_medium_cpu_recparallel12_20260814.json`. ### 2026-08-14: multi-image native/ORT gate repair and revalidation `bench/compare_models.ps1` now reliably expands a comma-separated or native PowerShell `string[]` image list, forwards the selected CPU/Hybrid backend, and distinguishes a latency-weighted corpus gate from an explicit per-case floor. This prevents a one-image invocation from being presented as a multi-resolution result and makes each model/image pair independently auditable. A fresh CPU validation used normal English and ultra-wide `3000x80` text, 16 workers, one warm-up, two timed runs, and required every one of the six model/image pairs to beat ONNX Runtime CPU. All passed. The latency-weighted native reductions were **68.1%** (tiny), **56.2%** (small), and **22.9%** (medium); no model had a losing case in this run. Full machine-specific evidence: `build/compare_models_cpu_normal_wide_gate_20260814.json`. The same wrapper also supports `-Backend hybrid`; on this Radeon 780M UMA host, it preserves the transfer-inclusive Vulkan admission policy rather than claiming universal end-to-end GPU gains. ### 2026-08-14: refreshed three-tier ONNX Runtime comparison The current 16-worker CPU comparison (2 warm-ups, 6 measured `en.ppm` runs) continues to show substantial native gains over ONNX Runtime CPUExecutionProvider: **33.084 vs 118.015 ms** for tiny (**72.0% lower**), **72.522 vs 195.894 ms** for small (**63.0% lower**), and **222.385 vs 402.871 ms** for medium (**44.8% lower**). These are paired local-host measurements and include the complete native OCR pipeline, not an isolated kernel claim. An additional selected-class CTC-softmax parallelization experiment was rejected: it was neutral on a single wide crop and slower on a dense medium page, so the shipped path retains the lower-variance temporal CTC scan. Hybrid text parity was rerun for tiny/small/medium on normal and ultra-wide inputs (6/6); the GPU decision rule remains unchanged: choose Vulkan only when the complete transfer-inclusive segment is no slower than CPU. ### 2026-08-14: Vulkan device-resident graph foundation The Vulkan backend now has a `VulkanTensorArena` separate from Hybrid's five short-lived scratch bindings. It allocates FP32 storage slots by tensor size, reuses a released slot only after the caller's final-use decision, and reports live versus retained capacity. Graph boundaries can upload inputs/constants and download requested outputs, while intermediate elementwise values remain in Vulkan buffers. The first device-resident operators are exact binary in-place updates and the NCHW 1x1 Conv family used throughout the recognizer; both insert compute write-to-read barriers before their output can feed the next graph node. The Radeon 780M Vulkan smoke now verifies slot allocation/reuse, upload, device binary and dependent binary-chain execution, plus device-resident pointwise convolution against CPU FP32 reference values. The regular SIMD and all existing Hybrid primitives remain covered. This is infrastructure for the complete GPU-only graph, not a claim that `gpu_only` is available yet: pool, LayerNorm, attention Softmax/QKV, transposes, concatenation, detector DB post-processing, and remaining graph dispatches still need device paths. ### 2026-08-14: GPU-only graph operator expansion The device arena now additionally supports a real destination-writing NCHW suffix broadcast (`[N,C,H,W]` with compact `[C,1,1]` coefficients) and exact Erf-form GELU on the shader. Both are numerically checked by `ppocr_vulkan_smoke` on the AMD Radeon 780M; neither reads an intermediate activation back through the CPU. The broadcast form is required for SE gates and compact graph scale paths, while exact GELU covers a fused-model operator present in every supported detector/recognizer pair. This does **not** enable `--backend gpu`: the complete graph still lacks device implementations for graph concatenation/shape-slice metadata handling, unfused resize forms, SE composition, and the full OCR image/DB/CTC pipeline. Construction continues to fail explicitly rather than silently executing an activation on CPU. No end-to-end GPU-only benchmark is recorded until that contract can be exercised and compared fairly. The same arena now includes device-resident two-to-four input `Concat` and integer-factor asymmetric/floor nearest `Resize` (including unequal height / width factors). These are validated with multi-outer-block and `N=2` smoke cases. They remove two more detector-FPN activation boundaries; they do not change the strict GPU-only availability gate because a complete model graph and OCR front/back-end still require verified device dispatches. Medium-model SE is now represented as one device-resident arena routine: spatial mean, both 1x1 projections, ReLU, HardSigmoid, and final channel multiply all remain in Vulkan buffers. The Radeon smoke checks every output against an independent FP32 reference for `N=2,C=4,reduced-C=3,plane=13`. The ONNX executor now also has a strict Vulkan graph-execution scaffold with device value metadata, graph-use-count slot reclamation, device copy for branch fan-out, metadata-only reshape/squeeze/unsqueeze, pointwise unary and binary aliases, Concat, and fused SE routing. It returns failure immediately on an uncovered graph node; the public GPU-only gate remains closed until the entire detector, recognizer, and OCR pipeline can use that path. The strict graph executor now maps normal/fused NCHW Conv and Max/AveragePool to their device-arena implementations. When an activation fan-out prevents an in-place update, it first uses the new GPU copy primitive; this preserves graph arithmetic rather than overwriting a still-live branch. Unsupported geometry or nodes still terminate GPU-only execution instead of invoking CPU. ### 2026-08-15: strict Vulkan graph coverage extended The strict device graph now also covers detector depthwise/pointwise blocks, residual 1x1 projections, compact SE-style broadcasts, spatial means, integer nearest resize and resize-add, 2x2 stride-two transpose convolution, SAME padding for Conv/Pool, inference BatchNorm channel-affine, rank 2--4 transpose, and final matrix projection/Softmax. `FusedConvHardSigmoid` was corrected to run its exported alpha/beta activation as a separate Vulkan unary after convolution; it no longer risks returning an ungated Conv result. `ppocr_gpu_graph_smoke` was added as a direct CPU-versus-strict-device graph tool. It deliberately does not mark public `gpu_only` OCR as available: the full image preprocessing, DB detector postprocessing, crop generation and CTC decoding must still be moved onto Vulkan before that user-facing contract can be truthfully enabled. On 2026-08-15 the Radeon 780M low-level Vulkan and SIMD smoke suites passed; full-graph parity is still gated while the remaining recognizer attention nodes and final numerical-error envelope are completed. ### 2026-08-15: GPU-only recognizer scheduling progress The strict Vulkan executor now runs the PP-OCRv6 small/medium recognizer's dynamic shape-metadata chains (`Shape`/`Slice`/`Squeeze`/`Unsqueeze`/`Concat`), scalar attention scale, fused QKV split, rank-3/rank-4 attention `MatMul`, Softmax, and residual transformer blocks without downloading an activation. Only integer shape descriptors are interpreted on the host; feature maps stay in arena buffers. The generic GEMM path is the current correctness baseline; the experimental tiled GEMM requires `PPOCR_ENABLE_VULKAN_GEMM_TILE=1`. Verification on the AMD Radeon 780M: `ppocr_vulkan_smoke` and `ppocr_kernel_smoke` pass, and the normal tiny Hybrid OCR benchmark remained functional at **10.775 ms** mean over two measured runs on `line_en.ppm`. The ordinary-Conv shared-memory route is now gated by the same large-output threshold in host dispatch and shader dispatch. Previously a compact SE gate could be launched through the generic host geometry but interpreted as a channel-tiled shader geometry, leaving stale arena values. With that mismatch removed, strict CPU-versus-Vulkan forward parity now passes for every shipped network on the Radeon 780M: tiny/small/medium detector at `1x3x960x960`, and tiny/small/medium recognizer at `1x3x48x320`. The largest observed final absolute errors are `2.42644e-06`, `6.57933e-06`, and `7.00675e-09` for the detectors, and `5.88894e-05`, `2.09391e-04`, and `8.39829e-05` for the recognizers, respectively. `ppocr_gpu_graph_smoke` now accepts an optional `RUNS` argument and prints CPU/GPU graph means, including the current GPU graph boundary. The initial two-run reference on this UMA Radeon machine measured tiny detector `124.284 / 610.187 ms`, tiny recognizer `8.190 / 173.200 ms`, small detector `219.862 / 1613.830 ms`, small recognizer `20.697 / 370.361 ms`, medium detector `1033.580 / 8530.580 ms`, and medium recognizer `33.085 / 577.311 ms` (CPU / strict Vulkan). These are correctness-baseline measurements, not a GPU speed claim: each current graph operator submits and waits separately, so command-batch compilation and persistent model constants are the next Vulkan performance work. ### 2026-08-15: strict graph single-submit command recording The strict neural Vulkan executor now records a complete forward graph into one command buffer and waits only at its output boundary. Every dispatch gets an immutable descriptor set from a dedicated resettable graph pool, preventing later descriptor writes from changing earlier recorded bindings. Hybrid keeps its original synchronous descriptor pool and full-boundary admission policy. The local Radeon 780M verification passed the Vulkan primitive suite, SIMD kernel suite, and all six strict graph parity cases at the published shapes. The current two-run CPU / Vulkan means are tiny detector **223.271 / 1042.440 ms**, tiny recognizer **29.517 / 319.981 ms**, small detector **195.110 / 1249.390 ms**, small recognizer **18.378 / 291.485 ms**, medium detector **812.492 / 10669.200 ms**, and medium recognizer **39.293 / 564.885 ms**. Final tensor parity was exact in these runs (max absolute error `0`). The UMA driver still makes this graph implementation slower than the AVX-512 CPU baseline; eliminating submission fences alone is not enough to claim a speedup, so the public GPU-only OCR contract remains closed. ### 2026-08-15: persistent GPU model weights and lower Conv tile threshold Strict `OnnxLite` GPU models now retain their immutable initializer slots in a model-owned Vulkan arena. Activations are released after every successful or failed run, and the arena is guarded for the complete graph recording; this removes repeated model-weight allocation/upload and prevents per-inference arena growth. The ordinary-Conv shared-filter tile threshold was lowered from 16,384 to 4,096 output pixels in both host and shader dispatch logic, while retaining the compact SE fallback path. With a warmed model, Radeon 780M three-run strict graph means were **tiny detector 111.399 / 264.214 ms**, **small detector 229.969 / 858.226 ms**, **medium detector 820.237 / 8059.020 ms** (medium: two runs), **tiny recognizer 7.915 / 86.976 ms**, **small recognizer 18.101 / 122.982 ms**, and **medium recognizer 34.873 / 354.897 ms** (CPU / Vulkan). Exact parity held for all six models. The tiny detector resolution ladder also passed at `320²`, `640²`, and `960²` with zero final error; warmed Vulkan means were **40.471**, **109.382**, and **264.214 ms**, respectively. A real `N=4` tiny-recognizer batch at `4x3x48x320` also remained exactly equal to CPU. These results are a substantial reduction of strict-device scheduling and weight-transfer overhead, but this Radeon 780M UMA configuration is still not faster than its AVX-512 CPU path for complete PP-OCRv6 graphs. Hybrid retains its measured per-segment policy: when Vulkan is equal or faster for the full boundary it is selected, otherwise CPU is retained to avoid consuming GPU time without a latency benefit. ### 2026-08-15: expanded Conv tile range The ordinary-Conv shared-weight tile is now eligible from a `256`-pixel output plane (host and SPIR-V use the same gate), extending reuse to smaller detector feature maps without changing compact SE convolutions. The regenerated shader and strict graph test passed with final error `0` on tiny detector `320x320` and `960x960`; warmed Vulkan means were **37.13 ms** and **236.52 ms**, respectively. Lowering the threshold again to `64` regressed both shapes, so `256` is retained. ### 2026-08-15: GPU-only front/back-end groundwork The Vulkan arena now has a live-slot graph handoff API, fused BatchNorm+Swish dispatch, RGB bilinear-resize+NCHW-normalization shader, device threshold-mask shader, and device CTC Top-1 reduction. `ppocr_gpu_frontend_smoke` verifies the RGB path on Radeon 780M; its maximum detector-normalized difference from the scalar front-end was **0.0175072**, below one source uint8 normalization step. The strict tiny detector and recognizer graph regressions remained exact at their final tensors. This is deliberately not advertised as a completed public GPU-only OCR mode: DB box scores/unclip parity and whole-image decoded-text parity still require an end-to-end validation suite. Accordingly `Backend::gpu_only` continues to fail explicitly rather than returning potentially different OCR results. The experimental device orchestration never falls back to the CPU NN executor, but it is not enabled through the public API until that parity evidence exists. ### 2026-08-15: strict graph memory ladder and default recognizer GEMM tile `ppocr_gpu_graph_mem_soak` is a new model-level Vulkan memory regression tool. It warms one model, drives it through an ascending resolution/batch ladder, then repeats a small input and reports both allocated arena capacity and live bytes. Capacity is deliberately reusable high-water storage; the required invariant is that live bytes return to the model's persistent-weights baseline after every run. On the Radeon 780M, tiny detector `320x320 -> 640x640 -> 960x960 -> 320x320` grew capacity to **214,007,808 bytes** while live bytes returned after every sample to **1,713,620 bytes**. Tiny recognizer `1x3x48x80 -> 1x3x48x320 -> 4x3x48x320 -> 8x3x48x640 -> 1x3x48x80` reached **67,633,152 bytes** and recovered each time to **4,417,112 bytes**. The same invariant passed on small detector (**303,267,840 / 9,813,412 bytes**) and medium recognizer (**191,889,408 / 76,456,032 bytes** capacity/live baseline). This explicitly covers both varied detector images and recognition batch/width growth without retaining dynamic activations after a large run. The verified non-Swish strict-graph GEMM tile is now default-enabled for `rows >= 8, depth >= 16, columns >= 128`; set `PPOCR_DISABLE_VULKAN_GEMM_TILE=1` for an A/B fallback. On the same host, final tensor parity remained exactly zero for all three recognizers. The current short strict-recognizer means at `1x3x48x320` were tiny **9.869 / 37.195 ms**, small **23.498 / 107.000 ms**, and medium **45.271 / 389.620 ms** (CPU / Vulkan). This is an improvement for the device graph but not a claim that this AVX-512 + Radeon 780M UMA system beats CPU or ONNX Runtime end-to-end; Hybrid still uses its full-boundary no-slower admission rule. ### 2026-08-15: parallel Softmax writeback The strict Vulkan Softmax now retains its sequential max/sum reduction for numerical stability, then spreads the independent exponential/writeback stage across the 256-lane workgroup. This removes a serial device loop in recognizer attention and terminal class Softmax without adding an activation allocation. Final tensor parity remained **0** for tiny, small, and medium recognizers at `1x3x48x320`, and at a real `4x3x48x320` batch. The short post-change means were tiny **37.470 ms**, small **94.376 ms**, and medium **382.981 ms** for the single-crop strict Vulkan graph; the small recognizer improved from the preceding **107.000 ms** sample. Device output is still slower than the local AVX-512 CPU baseline, so this is a verified incremental improvement rather than an ONNX Runtime superiority claim. The public `gpu_only` OCR switch remains deliberately disabled. Image resize and normalization upload, DB connected-component/unclip postprocessing, crop generation, CTC top-1/decode, end-to-end output parity, and memory recovery must be completed before the public contract can truthfully promise complete GPU-only OCR. ### 2026-08-15: device-resident OCR path validation gate The experimental OCR route now feeds strict Vulkan detector/recognizer graphs from the fused Vulkan RGB resize, BGR conversion and normalization dispatch; it no longer builds detector or recognizer NCHW activations with CPU floating point code. Detector probabilities are downloaded for the existing DB connected-component/score/unclip control path, and only compact CTC Top-1 metadata is downloaded for string assembly. `ppocr_gpu_ocr_smoke` compares the public OCR result (text, quad, bounding rectangle and confidence) with CPU-only OCR. On the Radeon 780M, tiny and small models passed `en.ppm`, `line_en.ppm` and `mixed.ppm` (6 decoded regions each), with maximum confidence differences of **0.0000061** and **0.0000019**, respectively. Tiny also passed `en.ppm` alone at **0.0000026**. These runs establish that the NN graph has no silent CPU operator fallback, but do not establish complete release coverage: the dense 1920x1080 page diverged in detector/recognizer results (tiny result 72; small result 16), and medium detector strict submission at its real `1x3x160x704` shape produced an all-zero probability map (while its 128x704 pipeline smoke passed). Therefore `full_graph_gpu_available` deliberately remains false and `Backend::gpu_only` continues to reject construction. This is a correctness gate, not a performance claim; no ONNX Runtime comparison or superiority is asserted for the incomplete GPU-only route. ### 2026-08-15: GPU-only completion status The implementation now has an all-device neural graph path, fused RGB-to-NCHW front end, device CTC Top-1 and no CPU floating-point inference fallback. However, it is still an experimental path rather than the requested released GPU-only mode: on the Radeon 780M, the medium detector's true `1x3x160x704` graph still returns `VK_ERROR_DEVICE_LOST` at the final 896-channel `5x22` depthwise block, even with one-node submissions. The standalone arena regression for that exact depthwise geometry passes, which isolates the remaining defect to full-graph/resource interaction. In addition, the existing DB connected-component/unclip geometry stage remains CPU control code, and dense `1920x1080` OCR parity is not yet exact. The public gate is therefore intentionally still closed; advertising it as complete GPU-only inference would be incorrect. ### 2026-08-15: GPU-only revalidation (current workspace) The strict Vulkan implementation was rebuilt after repairing the source file and revalidated on **AMD Radeon 780M Graphics** with `PPOCR_GPU_ONLY_SEGMENT_NODES=1`. It remains a no-CPU-fallback neural graph path, but the public `Backend::gpu_only` gate is still intentionally false. The following current CPU-versus-device graph checks passed: | Model | Shape | Max absolute output error | CPU mean | Vulkan mean | |---|---:|---:|---:|---:| | tiny detector | `1x3x160x704` | `4.83182e-07` | 28.844 ms | 224.343 ms | | small detector | `1x3x160x704` | `1.91038e-07` | 41.481 ms | 301.020 ms | | medium recognizer | `1x3x48x320` | `8.39829e-05` | 60.583 ms | 552.145 ms | The Vulkan primitive suite also passes, including the standalone `C=896,H=5,W=22` depthwise convolution that resembles the medium detector terminal block. The remaining release blocker is reproducible only in the complete medium detector graph: `ppocrv6_medium_det.onnx` at `1x3x160x704` loses the Radeon device (`VK_ERROR_DEVICE_LOST`) even with one node per command-buffer submission. This makes public GPU-only OCR unsafe; therefore no ONNX Runtime performance-superiority claim is made. `ppocr_gpu_graph_mem_soak` now provides a device-arena recovery check. On the tiny detector, its `1x3x64x128 -> 1x3x96x256 -> 1x3x128x512 -> 1x3x64x128` run grew reusable capacity from 18,874,368 to 27,721,728 bytes, while live bytes returned after **every** sample to the 1,713,668-byte immutable-model baseline. Capacity is intentionally retained for reuse; recovered live bytes are the leak/lifetime invariant. ## Model files Get `ocr-models.zip` from the MaClaw `Model_Release` and unpack it. Pass a matching detector, recognizer, and dictionary. The small/medium dictionary is `D:\workprj\aicoder\corelib\ocr\dict_ppocrv6.txt`; the model archive contains `dict_ppocrv6_tiny.txt` for the tiny recognizer. ## Demo The included demo uses binary P6 PPM to avoid coupling a specific image decoder to the inference library: ```powershell .\build\ppocr_demo.exe ppocrv6_tiny_det.onnx ppocrv6_tiny_rec.onnx dict_ppocrv6_tiny.txt image.ppm ``` For PNG/JPEG/BMP, decode with the host application's existing image library and construct `ppocr::Image { width, height, packed_rgb }` before calling `ppocr::OCR::Recognize`. ## Scope The C++ pipeline contains PP-OCRv6 BGR preprocessing, dynamic detection and recognition sizing, CTC greedy decoding, and DB-style threshold/score/unclip text region extraction. Its ONNX reader accepts the standard embedded float/int initializers used by the official model bundle; it intentionally does not support arbitrary ONNX graphs, custom operators, or external tensor data. ### 2026-08-15: strict GPU-only execution revalidation The strict Vulkan graph executor now uses a device-resident submission boundary per graph node by default. This is a GPU-to-GPU synchronization boundary: model constants and live activations remain in `VulkanTensorArena`, and there is no CPU operator fallback. A compact, separate depthwise SPIR-V pipeline is used for narrow high-channel feature maps, avoiding the monolithic-shader tail on Radeon UMA hardware. On AMD Radeon 780M, CPU versus strict GPU graph checks passed for: | Model | Shape | Max absolute output error | CPU mean | Vulkan mean | |---|---:|---:|---:|---:| | tiny detector | `1x3x160x704` | `4.83182e-07` | 22.184 ms | 113.042 ms | | small detector | `1x3x160x704` | `1.91038e-07` | 42.603 ms | 193.311 ms | | medium detector | `1x3x160x704` | `6.56222e-04` | 128.313 ms | 757.807 ms | | medium recognizer | `1x3x48x320` | `8.39829e-05` | 39.960 ms | 472.034 ms | The medium detector memory ladder (`64x128 → 96x256 → 128x512 → 160x704 → 64x128`) also passed: its grow-only arena reached `291,667,968` bytes, while live allocation returned after every run to the immutable-model baseline of `61,946,548` bytes. Public `Backend::gpu_only` remains deliberately unavailable. Tiny and small completed end-to-end CPU/GPU OCR parity on `en.ppm` (maximum confidence errors `0.0000029` and `0.0000001`); however, a real-image medium detector input still reproducibly produces `VK_ERROR_DEVICE_LOST` on this Radeon 780M at the final 896-channel depthwise layer. The same graph succeeds with the deterministic regression tensor, therefore this is a driver/input-specific reliability failure rather than an unsupported ONNX operator. Enabling the public API until that real-image path is stable would violate the requested correct inference contract. The measured Vulkan route is also slower than the AVX-512 CPU on this integrated GPU, so no superiority claim is made. ### 2026-08-15: GPU-only arena lifetime and current recheck Strict Vulkan graph segments now close only after their final input consumers have been retired. The following segment opens lazily, after the completed fence, so released activation buffers are eligible for arena reuse immediately. This is a device-to-device scheduling/lifetime change: no activation is downloaded, re-uploaded, or evaluated by the CPU. Current Radeon 780M checks with `PPOCR_GPU_ONLY_SEGMENT_NODES=1`: | Model | Shape | Max absolute output error | CPU mean | Vulkan mean | |---|---:|---:|---:|---:| | tiny detector | `1x3x160x704` | `4.83182e-07` | 19.293 ms | 100.492 ms | | small detector | `1x3x160x704` | `1.91038e-07` | 29.877 ms | 187.476 ms | | medium detector | `1x3x128x704` | `7.48696e-09` | 85.258 ms | 716.522 ms | | medium recognizer | `1x3x48x320` | `8.39829e-05` | 40.103 ms | 481.122 ms | The tiny-detector device memory ladder `64x128 -> 96x256 -> 128x512 -> 64x128` recovered to its `1,713,668`-byte immutable-constant baseline after every sample. Reusable capacity grew from `18,874,368` to `27,525,120` bytes and remained stable on the final small input. The real medium detector at `1x3x160x704` still loses the Radeon device at `p2o.pd_op.depthwise_conv2d.12.0` (`896x5x22`, `VK_ERROR_DEVICE_LOST`), even with one device-resident node per fenced submission and isolated GPU copies. Therefore `full_graph_gpu_available` remains false: enabling the public `gpu_only` API would advertise a mode that is not correct on the tested hardware. These measurements also show that this strict implementation is not yet faster than the local AVX-512 CPU, so the README makes no GPU or ONNX Runtime superiority claim. `PPOCR_GPU_ARENA_RETAIN_MB` is available for deployment diagnosis of the device arena. Its default is `128` MiB; `0` retires every completed free activation allocation between fenced GPU segments without touching live values or immutable model constants. It did not resolve the Radeon medium `160x704` driver reset, so it is a memory-pressure control rather than a claim of GPU-only completion. ### 2026-08-15: GPU-only reliability status (latest) The strict Vulkan graph remains fully device-resident between its input and output boundaries: all PP-OCRv6 neural operators execute in Vulkan buffers, and failure is reported rather than silently executing a CPU operator. The experimental generated `vulkan_depthwise_tail.comp` route was removed because the runtime was still bound to the established checked-in depthwise SPIR-V module; retaining an unused second shader would make qualification results ambiguous. Current Radeon 780M verification with `PPOCR_GPU_ONLY_SEGMENT_NODES=1`: | Model | Shape | Max absolute output error | CPU mean | Vulkan mean | |---|---:|---:|---:|---:| | tiny detector | `1x3x160x704` | `4.83182e-07` | 21.400 ms | 103.202 ms | | small detector | `1x3x160x704` | `1.91038e-07` | 35.271 ms | 182.782 ms | | medium detector | `1x3x128x704` | `7.32689e-09` | 103.195 ms | 779.817 ms | | medium recognizer | `1x3x48x320` | `8.43406e-05` | 42.774 ms | 492.075 ms | The complete medium detector at its production `1x3x160x704` shape still reliably reports `VK_ERROR_DEVICE_LOST` at the final `C=896, H=5, W=22` depthwise block. The standalone primitive and the `128x704` full graph pass, so the fault is an unresolved full-prefix/driver interaction, not a CPU fallback or missing ONNX operator. `Backend::gpu_only` therefore remains intentionally unavailable. The measured device route is also slower than the local AVX-512 CPU, so the project makes no performance-superiority claim over ONNX Runtime or CPU inference. ### 2026-08-15: GPU-only qualification recheck The strict neural-graph implementation remains pure Vulkan: activations and model constants stay in `VulkanTensorArena`, and a failed device graph throws instead of falling back to the CPU executor. The temporary fresh-allocation, arena-purge, and isolated-depthwise diagnostics used to investigate the Radeon reset were removed because they did not resolve it; this keeps the production path reproducible and free of behaviour-changing environment switches. The current clean build (`cmake -S . -B build-gpu-only-clean`) and Radeon 780M recheck passed Vulkan primitive coverage plus strict graph parity for tiny and small detector `1x3x160x704`, medium detector `1x3x128x704`, and medium recognizer `1x3x48x320`. The GPU RGB front-end -> detector hand-off passed on the real `en.ppm` page for tiny and small. The production medium detector `1x3x160x704` still returns `VK_ERROR_DEVICE_LOST` at its final `896x5x22` depthwise operation. Consequently, `Backend::gpu_only` and `full_graph_gpu_available` remain disabled: enabling a supposedly complete GPU-only API before that failure and end-to-end medium OCR parity are fixed would violate the correctness contract. No GPU/ONNX Runtime speed claim is made for this UMA adapter. ### 2026-08-15: Vulkan full-OCR requalification and MSVC portability ### 2026-08-16: complete GPU-only reliability work — current status The strict Vulkan executor now builds a second, purpose-specific SPIR-V module for the medium detector's final `896x5x22` depthwise convolution. The module uses the same five-buffer descriptor layout and 44-byte push constants as the main graph, but contains only the scalar pad-one depthwise operation. Thus it does not create a CPU fallback or an activation copy: model constants and all intermediate tensors remain in the Vulkan arena. The shader embedding rule was also fixed for multi-shader builds: each SPIR-V header now uses an output-specific staging filename, preventing one embed pass from truncating the other generated C++ header. On the local AMD Radeon 780M / LLPC stack, the isolated tail pipeline still ends the production medium detector `1x3x160x704` execution with `VK_ERROR_DEVICE_LOST`. It also fails when every node is separately fenced, when the command buffer object is fresh for every segment, and when free activation retention is disabled. Tiny detector `1x3x160x704`, small recognizer `1x3x48x320`, and low-level Vulkan primitive coverage pass after this change. Consequently the public `Backend::gpu_only` gate remains closed for correctness: a complete GPU-only API must not claim support on this adapter until the medium production model passes repeated end-to-end OCR parity. No CPU neural-network fallback is taken on failure. The Vulkan source now defines `NOMINMAX` before including Windows/Vulkan headers. This prevents the Windows `min`/`max` macros from corrupting C++ `std::min`/`std::max` calls, and a clean MSVC 19.51/Ninja Release build now compiles the Vulkan backend and smoke tools. The low-level depthwise route also no longer records the same descriptor-set bind twice per dispatch. On AMD Radeon 780M, refreshed strict-graph checks (three warm passes, upload, graph, and output download included) were: | Model | Shape | Max absolute output error | CPU mean | Vulkan mean | |---|---:|---:|---:|---:| | tiny detector | `1x3x160x704` | `4.85918e-07` | 43.604 ms | 61.394 ms | | small detector | `1x3x160x704` | `1.92900e-07` | 76.702 ms | 123.783 ms | | medium detector | `1x3x128x704` | `7.23958e-09` | 156.387 ms | 627.358 ms | | tiny recognizer | `1x3x48x320` | `4.70877e-06` | 12.895 ms | 54.201 ms | | small recognizer | `1x3x48x320` | `8.94070e-07` | 26.585 ms | 153.499 ms | | medium recognizer | `1x3x48x320` | `4.17233e-06` | 54.486 ms | 407.467 ms | GPU RGB front-end plus strict graph validation passed for tiny and small detectors on five varied PPM pages (`en`, `zh`, mixed, English-line and Chinese-line), spanning `64x320`, `128x704`, and `192x704` detector inputs. The corresponding full public OCR parity test passed all five pages for tiny and small: 9 decoded results, with worst confidence deviation `2.9e-06` and `1.7e-06` respectively. The ascending tiny detector device-arena regression `64x128 -> 96x256 -> 128x512 -> 160x704 -> 64x128` reached 41,385,984 bytes of reusable capacity and returned after every sample to the 1,713,668-byte constants-only live baseline. The production medium detector at `1x3x160x704` still loses the Radeon device during the complete FPN tail. Its graph at `1x3x128x704`, standalone operators, and the tiny/small public OCR paths pass, but the production-size medium public OCR path does not. Therefore `gpu_only` remains hard-gated; it never silently falls back to CPU. Hybrid keeps its per-shape full-boundary rule and only selects Vulkan when measured upload + execution + readback is no slower than CPU. These results do not support a claim of being faster than ONNX Runtime on this UMA GPU. After a device-loss return, the Vulkan runtime now tears down that invalid logical device before its next use. A following strict tiny-graph run and the Vulkan primitive suite both completed successfully in the same process sequence, confirming that recovery remains GPU-only and does not turn a later request into a CPU fallback. ### 2026-08-15: GPU-only allocator/recovery hardening The strict Vulkan arena now exposes a monotonically changing resource generation. `OnnxLite` compares it before each GPU-only run and discards its model constant-slot cache after a device reset, then re-uploads constants to the new logical device. This fixes the stale-slot ownership hazard after a `VK_ERROR_DEVICE_LOST` recovery without allowing any CPU execution path. Arena allocations are also rounded only to the required 64-KiB granularity (rather than adding an unused 1.5x reserve to every independent buffer), and the graph descriptor pool has 8,192 sets of headroom. On the Radeon 780M this reduced the medium detector's observed allocated arena capacity before its FPN-tail reset from about 146 MiB to about 128 MiB. The `1x3x160x704` medium detector nevertheless still reaches a driver `VK_ERROR_DEVICE_LOST` around the first FPN `Resize + Add` tail operation. Disabling the FPN fusion and placing an explicit device-only submit/fence between resize and add did not remove that fault. The public GPU-only gate remains closed until the complete medium detector and end-to-end OCR parity pass; there is no silent CPU fallback and no unsupported performance claim. Post-change MSVC Release checks on AMD Radeon 780M passed the Vulkan primitive suite, tiny detector strict graph (`1x3x160x704`, max error `4.85918e-07`, GPU `52.2832 ms`) and small recognizer strict graph (`1x3x48x320`, max error `8.9407e-07`, GPU `96.6479 ms`). Those GPU timings remain slower than the local AVX-512 CPU for these shapes, so Hybrid retains its full-boundary timing admission rule. ### 2026-08-15: GPU-only medium-tail qualification The strict Vulkan executor now has an explicitly exercised scalar FP32 depthwise dispatch for the high-channel `896x5x22` tail shape. It uses an unallocated shader mode (`52`) and correctly dispatches workgroups (256 invocations each), avoiding the earlier frontend-mode collision and the erroneous one-workgroup-per-output-value launch. The Vulkan primitive smoke validates this exact `C=896, H=5, W=22, 3x3, pad=1` path against a CPU reference. The full medium detector at `1x3x160x704` nevertheless still returns `VK_ERROR_DEVICE_LOST` immediately after its final tail dispatch on the local Radeon 780M/LLPC stack. Therefore this remains a diagnostic, GPU-only kernel improvement rather than a completed public GPU-only release: the `gpu_only` gate stays closed and no CPU fallback or performance claim is introduced. ### 2026-08-15: device-local Vulkan arena experiment The Vulkan tensor arena now supports a device-local storage-buffer route with host-visible staging buffers. `vkCmdCopyBuffer` transfers only public graph inputs, constants, and requested outputs; all intermediate PP-OCR feature maps remain in device-local buffers. Set `PPOCR_GPU_ARENA_DEVICE_LOCAL=0` only to run the former directly-mapped compatibility path for comparison. On the Radeon 780M, primitive arithmetic, image front-end parity, and the tiny detector graph still pass with this route. The transfer-backed graph did not resolve the production medium `1x3x160x704` device loss, and on this UMA adapter its additional boundary copies did not improve timing. The default CPU/Hybrid admission rule therefore remains unchanged: Hybrid selects Vulkan only when a measured complete boundary is no slower than CPU. No ONNX Runtime superiority is claimed from this experiment. ### 2026-08-15: multi-size GPU graph and residency recheck The graph smoke matrix now covers detector inputs from `64x128` through `192x704`, plus recognizer widths `64` and `320`, across all tiny/small/medium families. On the Radeon 780M, all listed runs had device/CPU output parity: | Model | Shape | CPU ms | Vulkan ms | Max abs. error | |---|---:|---:|---:|---:| | tiny detector | `1x3x64x128` | 5.675 | 16.897 | `1.13e-09` | | tiny detector | `1x3x192x704` | 22.467 | 55.239 | `2.66e-05` | | small detector | `1x3x64x128` | 10.150 | 38.459 | `7.74e-09` | | small detector | `1x3x192x704` | 41.085 | 99.252 | `4.60e-07` | | medium detector | `1x3x64x128` | 24.039 | 75.144 | `4.22e-10` | | medium detector | `1x3x128x704` | 132.380 | 477.800 | `7.24e-09` | | tiny recognizer | `1x3x48x64` | 5.274 | 16.755 | `3.52e-06` | | small recognizer | `1x3x48x320` | 20.938 | 82.170 | `8.94e-07` | | medium recognizer | `1x3x48x320` | 46.774 | 351.068 | `4.17e-06` | The ascending tiny detector memory soak `64x128 -> 96x256 -> 128x512 -> 160x704 -> 64x128` ended every sample at the same constants-only live baseline (`1,713,668` bytes), proving activation release after the largest input. `ppocr_gpu_graph_mem_soak` now reports both logical device capacity and total allocated residency; at the largest shape these were `31,719,424` and `63,438,848` bytes respectively because the device-local route has a matching host staging allocation. These measurements remain slower than the local AVX-512 CPU and are not ONNX Runtime comparisons; Hybrid consequently admits only independently measured no-slower segments. ### 2026-08-16: GPU-only full-path requalification The strict Vulkan path remains the intended implementation for complete GPU-only OCR: image resize/normalization, detector and recognizer graphs, and terminal CTC Top-1 execute on Vulkan storage. Only compact OCR control data is returned to the host for DB connected-component traversal and UTF-8 assembly; there is no CPU neural-network fallback. `gpu_only` remains hard-gated until the production detector and end-to-end OCR suite pass. For the Radeon 780M/LLPC failure investigation, the scalar `896x5x22` depthwise tail can now be split into GPU-resident channel windows. The normal path uses four windows; `PPOCR_GPU_TAIL_SLICES=1..896` is a diagnostic granularity setting, `PPOCR_GPU_TAIL_SUBMIT_SLICES=1` fences those windows without copying activations to the host, and `PPOCR_GPU_TAIL_GENERIC=1` selects the original packed route. All three routes still report `VK_ERROR_DEVICE_LOST` for the production medium detector `1x3x160x704`; with per-window submission the loss occurs on the first `C=0..223` scalar window. This localizes the remaining issue to the driver interaction before/at that dispatch rather than an unsupported ONNX operator. Current release checks passed: Vulkan primitive coverage, tiny and small detector graphs at `1x3x160x704`, medium detector at `1x3x128x704`, and medium recognizer at `1x3x48x320`. Measured one-run timings on this shared host were `24.018/50.106 ms`, `37.939/86.509 ms`, `121.056/487.819 ms`, and `51.734/358.526 ms` (CPU/Vulkan); maximum output errors were respectively `4.86e-07`, `1.93e-07`, `7.24e-09`, and `4.17e-06`. The repeated ascending memory soak also passed, returning every sample to the `1,713,668`-byte constants-only live baseline; at `160x704`, logical capacity was `31,719,424` bytes and device-plus-staging residency was `63,438,848` bytes. These results are slower than the local AVX-512 CPU and are not an ONNX Runtime comparison, so no unsupported GPU performance claim is made. ### 2026-08-16: pointwise-GELU Vulkan fusion qualification The Vulkan shader now has a direct pointwise-convolution plus FP32-GELU write mode. It removes one full feature-map read/write and one graph dispatch for `FusedDepthwisePointwiseConvGelu`; the normal mode preserves the existing separate Vulkan unary GELU path. `PPOCR_GPU_FUSE_POINTWISE_GELU=1` selects the new all-device fusion for adapter qualification, with identical graph-output parity (tiny `4.86e-07`, small `1.93e-07`, medium `7.24e-09`). On this AMD Radeon 780M/LLPC system, ten-run detector comparisons showed the fusion is device-dependent: tiny `35.466 -> 33.748 ms` (5.1% faster), small `88.883 -> 91.514 ms` (3.0% slower), and medium `546.302 -> 643.987 ms` (17.9% slower), normal/fused. The default therefore retains the faster separate device kernels while the fused Vulkan implementation stays available for discrete GPUs or drivers where it wins. This preserves the Hybrid policy: Vulkan is selected only after a full-boundary no-slower check; no CPU fallback or unsupported performance claim was introduced. ### 2026-08-16: complete GPU-only OCR qualification `Backend::gpu_only` now executes the complete PP-OCRv6 neural path on Vulkan: RGB resize/normalization, detector, recognizer, and CTC Top-1 remain on device. Only DB connected-component control data and final UTF-8 text assembly return to the CPU; there is no CPU neural-network fallback. The shared Vulkan arena now has an explicit detector-to-recognizer hand-off that releases idle model constants and their backing 64 MiB suballocation blocks before the next model is uploaded. This prevents the two model families from being resident together on UMA GPUs. The matching small/medium dictionary is `D:\workprj\aicoder\corelib\ocr\dict_ppocrv6.txt` (18,708 lines; 18,710 CTC classes); tiny uses `dict_ppocrv6_tiny.txt` (6,904 lines; 6,906 classes). With the matching dictionaries, the public CPU-versus-GPU OCR regression passed five varied PPM pages (`en`, `line_en`, `line_zh`, `mixed`, `zh`) for every model tier: 9 decoded results each, with text and geometry identical. Maximum absolute confidence differences were `0.0494437` (tiny), `0.0136318` (small), and `0.0081339` (medium). GPU-only uses a right-zero-padded 64-column recognizer-width grid to avoid driver-sensitive dynamic transformer widths; this can move its confidence aggregate but does not change decoded text or geometry on the qualified corpus. On the local AMD Radeon 780M / LLPC system, warmed strict-graph comparisons (two runs; input/output boundary included) were: | Model | Shape | CPU mean | Vulkan mean | Max abs. error | |---|---:|---:|---:|---:| | tiny detector | `1x3x160x704` | 22.356 ms | 45.910 ms | `4.85918e-07` | | small detector | `1x3x160x704` | 35.894 ms | 99.619 ms | `1.92900e-07` | | medium detector | `1x3x160x704` | 160.500 ms | 604.553 ms | `1.81899e-08` | | tiny recognizer | `1x3x48x320` | 8.852 ms | 36.383 ms | `4.70877e-06` | | small recognizer | `1x3x48x320` | 20.985 ms | 93.526 ms | `8.94070e-07` | | medium recognizer | `1x3x48x320` | 51.310 ms | 370.130 ms | `4.17233e-06` | The GPU memory soak `64x256 -> 96x512 -> 160x704 -> 64x256` passed. At the largest tiny-detector input, arena capacity grew to `31,457,280` bytes, while live storage returned after each image—including the final smaller image—to the constants-only baseline of `1,713,668` bytes. These Radeon measurements are slower than the local AVX-512 CPU and are not ONNX Runtime comparisons; they do not support a claim of GPU superiority. Hybrid retains its transfer-inclusive no-slower admission policy. ### 2026-08-16: GPU-only true batch scheduling `RecognizeBatch` now has a GPU-only batch path. Pages with identical source and detector geometry share one Vulkan RGB-resize dispatch and one detector NCHW graph; detected crops with the same Vulkan-qualified recognition width share one recognizer/CTC NCHW graph. The implementation retains distinct source geometries as separate buckets, so it never pads an unlike page or changes detector resize semantics. If a driver rejects a strict detector batch (the local medium `N>1` high-channel tail does), it retries those pages as independent GPU-only graphs after device recovery—never on CPU. On four identical `720x152` `en.ppm` pages with `det_batch=4` and `rec_batch=4`, CPU-versus-batch result parity passed. GPU-only timing on the Radeon 780M was `434.004 ms -> 292.536 ms` serial/batched for tiny, a `1.484x` throughput gain. The preceding small-model run measured `1480.410 ms -> 1249.591 ms` (`1.185x`). Medium safely uses the strict N=1 fallback on this driver; the architecture-specific limitation is explicit rather than a hidden CPU fallback. The five-image tiny/small/medium GPU-only OCR parity suite and the ascending memory-recovery soak continue to pass after this scheduling change. ### 2026-08-16: GPU-only single-page crop batching The same Vulkan NCHW recognizer scheduler is now also used for repeated-width text crops within a single `Recognize()` call. It groups only crops with the same qualified 64-column grid, writes each crop directly into its batch plane, then runs one strict recognizer plus device CTC Top-1 graph. Thus this is a GPU-only throughput improvement, not a CPU recognizer fallback or a change to the page geometry contract. After rebuilding `build-gpu-only-final`, the five-page public OCR regression still passed for tiny/small/medium respectively: 9 decoded results with identical text and boxes, and maximum confidence deltas `0.0494437`, `0.0136318`, and `0.0081339`. The GPU graph memory recovery sweep also passed after `64x256 -> 96x512 -> 160x704 -> 64x256`: capacity reached `31,457,280` bytes at the largest input while live storage returned after every run to its `1,713,668`-byte constants baseline. On the local Radeon 780M, a renewed warmed four-page `en.ppm` batch check with `det_batch=4`, `rec_batch=4` measured tiny `530.291 -> 311.165 ms` (`1.704x`) and small `1496.264 -> 1277.662 ms` (`1.171x`), serial/batched. Both preserve exact GPU-only serial/batch output parity. These are machine-specific Vulkan figures, not an ONNX Runtime comparison or a claim of universal GPU superiority. ### 2026-08-16: GPU-only dense-page upload reuse and memory ladder GPU-only recognizer batching now uploads each source page once per same-width crop batch, rather than uploading the full RGB page once for every text line. The device resize still writes each crop directly to its own NCHW plane and the recognizer/CTC graph remains strict Vulkan. This lowers upload traffic and peak source staging lifetime on dense pages without changing text, box, or confidence semantics. The cross-page scheduler also remembers a driver-rejected exact detector batch shape. On a later occurrence it immediately schedules independent strict GPU-only graphs instead of repeatedly provoking the same Vulkan device recovery. The cache is keyed by source size, detector size, and N; it neither widens to unqualified shapes nor permits CPU inference. `bench/memory_ladder_regression.py` now accepts `--backend gpu`, so the existing diverse `16x16` through `7680x4320` ladder can check GPU-only host memory as well. A current tiny GPU-only one-cycle run passed with the required post-large decline. Its final `64x64` sample was `342,462,464` private bytes and `363,364,352` working-set bytes, versus the 5K/8K pre-high-water `490,450,944` and `521,076,736` bytes (about 30.2%/30.3% lower). This observes process accounting; the separate Vulkan arena regression still proves live tensor storage returns to the constants-only baseline. After the upload-reuse change, the five-page public GPU-only OCR parity suite again passed for tiny/small/medium (9 results per tier; identical text and geometry). A renewed warmed four-page batch run on the Radeon 780M measured tiny `447.936 -> 295.321 ms` (`1.517x`) and small `1542.215 -> 1280.809 ms` (`1.204x`), serial/batched. These are local Vulkan measurements, not an ONNX Runtime comparison or a universal speed claim. ### 2026-08-16: measured Vulkan command-segment tuning The strict Vulkan executor now defaults to 20 graph nodes per submitted command segment (`PPOCR_GPU_ONLY_SEGMENT_NODES` remains an override). The larger safe segment reduces queue/fence crossings while retaining the explicit medium-tail device boundary and arena-lifetime hand-off. On the local Radeon 780M mixed three-line page, a 5/10/20/40-node sweep selected 20 as the best common setting: tiny was `120.753 ms`, small `466.093 ms`, and medium `2427.856 ms`; 40 nodes regressed all three and 32 regressed medium. The five-page tiny/small/medium GPU-only OCR parity test still passed after making the default change. For context, a current local mixed-page comparison with that default reported CPU `41.735 / 99.223 / 336.651 ms`, hybrid `52.127 / 124.219 / 531.973 ms`, and strict GPU-only `122.299 / 479.389 / 2462.806 ms` for tiny/small/medium. Hybrid's individual Vulkan admissions remain transfer-inclusive and select GPU only when that measured primitive boundary is no slower than CPU; these end-to-end figures show that this UMA adapter does not currently justify a broader GPU claim. They are not an ONNX Runtime comparison. ### 2026-08-16: SIMD GPU-upload boundary coverage The GPU-only RGB upload boundary now uses a runtime-dispatched byte-to-FP32 conversion: AVX-512 when the OS/CPU state enables it, AVX2 on compatible x86, NEON on ARM, and scalar otherwise. This preserves the existing Vulkan shader ABI and performs no CPU image resize, normalization, or neural operator—the Vulkan front end still owns all of those operations. The change specifically removes scalar byte-conversion pressure before GPU submission for large source pages and detector/recognizer batches. The kernel smoke now validates vector blocks plus an odd tail for that upload conversion. Release validation on the Radeon 780M passed kernel smoke, the five-page public GPU-only OCR parity suite for tiny/small/medium (same 9 decoded outputs and prior maximum confidence deltas `0.0494437`, `0.0136318`, and `0.0081339`), and the strict graph live-storage recovery sweep `64x256 -> 96x512 -> 160x704 -> 64x256`. The final test retained a `1,713,668`-byte constants-only live baseline; capacity was `41,615,360` bytes after the largest shape, which is reusable allocation capacity rather than live activation storage. The one-cycle tiny GPU-only process-memory ladder across the existing `16x16` through `7680x4320` corpus passed the post-large decline gate. Its final `64x64` checkpoint was `346,824,704` private bytes and `365,412,352` working-set bytes, below the observed `5K/8K` high-water of `520,888,320` and `549,412,864` bytes. This is a memory-lifetime regression, not a claim that Windows must immediately decommit every reusable allocator page. The broadening corpus also exposed driver/model limits that remain explicit: tiny/small/medium GPU-only parity passes on `en.ppm` and `mixed.ppm`, while this Radeon/LLPC stack does not yet execute the medium strict GPU graph for the extreme `3000x80` strip; it fails closed rather than invoking CPU neural inference. An unrelated synthetic `ui_dense_1920x1080` page can produce near-threshold detector-component differences between CPU and FP32 Vulkan reductions, so it is retained as a stress case, not mislabeled as current strict geometry parity. Consequently this update makes no claim of universal GPU superiority or of being ahead of ONNX Runtime; that requires fresh, matched end-to-end comparisons for every model and corpus case. ### 2026-08-16: strict GPU-only extreme-short detector qualification The dedicated Vulkan depthwise-tail module now also qualifies the medium detector's named terminal `896x1x30` shape, reached by the `3000x80` strip after detector resizing to `32x960`. Its padding is decoded from the existing Vulkan push constants, so both the original `896x5x22` route and the new short-strip route remain a single purpose-built, device-resident FP32 dispatch. No ONNX Runtime or CPU neural fallback is introduced. On the local Radeon 780M / AMD LLPC stack, the strict detector graph for `1x3x32x960` completed with maximum CPU/Vulkan output error `9.60426e-10`. The complete public GPU-only OCR check for medium on `wide_strip_3000x80.ppm` also passed (one result; confidence delta `0.0046322`). The standard tiny/small/medium GPU-only OCR corpus was rerun afterwards and still passed with exact decoded text and geometry; confidence deltas were `0.0494437`, `0.0136318`, and `0.0081339` respectively. The extension deliberately remains narrowly qualified. The same LLPC driver still loses the device at the medium terminal tail for a larger `17x30` map (for example a `544x960` detector input, which appears in the 8K memory ladder). GPU-only continues to fail closed for that unsupported shape. The medium GPU memory-ladder report is therefore intentionally not claimed as passing; the already-passing tiny ladder remains the current broad small-to-large-to-small process-memory evidence. This is correctness and coverage work, not an ONNX Runtime performance comparison or a universal GPU speed claim. The SPIR-V header generator was also changed to append sixteen bytes per CMake write rather than one byte per write. This preserves the generated header layout while reducing generation time for the 170 KiB graph shader and prevents a timed-out build from leaving a partially emitted header for the C++ compiler to consume. ### 2026-08-16: strict GPU-only CTC confidence correction The terminal GPU CTC Top-1 reduction now returns the selected value from the model's already-computed Softmax tensor. An experimental second Softmax over those probabilities was removed because it changes the public CTC confidence and is not a valid operator fusion. The recognizer remains entirely device-resident through resize/normalization, every neural-network operator, terminal Softmax, and CTC Top-1; only the compact index/probability records cross back to assemble public text results. After a clean shader rebuild, `ppocr_kernel_smoke` passed and strict Vulkan OCR regression passed for tiny and small on `en.ppm`, `line_en.ppm`, and `mixed.ppm`, with maximum CPU/GPU confidence deltas `0.0494437` and `0.0136318`. Medium passed the same three images plus `wide_strip_3000x80.ppm` with a maximum confidence delta of `0.0081339`. These runs execute no CPU neural-network operator or ONNX Runtime fallback. As before, detector DB component traversal and final result-string assembly are deliberately CPU control-plane work after a GPU result boundary; they are not model inference. ### 2026-08-16: terminal Softmax-to-CTC Vulkan fusion and broad memory check For the strict GPU-only recognizer, the terminal `Softmax -> CTC Top-1` sequence is now fused without materialising the full terminal probability tensor. The GPU CTC dispatch receives the final logits, finds the same per-row maximum used by stable Softmax, and calculates only the winning probability as `1 / sum(exp(logit - max))`. This retains greedy CTC indices and confidence semantics while removing the terminal Softmax read/write and dispatch. Ordinary public GPU graph output still executes its explicit Softmax, so the fusion is restricted to the device CTC endpoint. The three-tier GPU-only OCR regression was rebuilt after the fusion. Tiny and small passed `en.ppm`, `line_en.ppm`, and `mixed.ppm`; medium passed those three plus the `3000x80` short strip. Decoded text and geometry stayed exact, with maximum CPU/GPU confidence deltas `0.0494437`, `0.0136318`, and `0.0081339`. `ppocr_kernel_smoke` also passed. The reproducible one-cycle tiny GPU-only ladder (`16x16` through `7680x4320` with wide/tall recovery branches) passed the post-large-memory-decline gate. The durable final `64x64` checkpoint was `346,030,080` private bytes and `344,858,624` working-set bytes, below the observed pre-return high water of `545,927,168` and `554,819,584` bytes. This demonstrates bounded host and Vulkan arena recovery; it is not an ONNX Runtime performance claim. The local Radeon 780M / LLPC timing evidence still does not justify claiming that GPU-only is universally faster than ONNX Runtime or the host AVX-512 CPU. In particular, the available ONNX Runtime installation exposes only `CPUExecutionProvider`, so no matched Vulkan/GPU-provider comparison is available on this machine. Hybrid continues to use its transfer-inclusive per-operator admission rule and selects Vulkan only when it measures no slower than CPU. ### 2026-08-16: reproducible native-build ORT comparison `bench/compare.ps1` and `bench/compare_models.ps1` now accept `-NativeExe`. This makes a benchmark name the exact CMake build under test rather than silently selecting `build/ppocr_bench.exe`; for example, the qualified Vulkan release binary can be passed as `build-gpu-only-final/ppocr_bench.exe`. The path is emitted in the JSON report for auditability. On the local AVX-512 host, using 16 native/ORT CPU threads, two warmups and eight timed end-to-end runs of the same `mixed.ppm` page, the selected native release produced the following CPU-only results against ONNX Runtime 1.28.0 `CPUExecutionProvider`: | PP-OCRv6 model | native mean | ORT mean | native latency improvement | | --- | ---: | ---: | ---: | | tiny | 58.679 ms | 164.644 ms | 64.36% | | small | 100.198 ms | 251.448 ms | 60.15% | | medium | 433.107 ms | 760.844 ms | 43.08% | This is a matched local CPU comparison: detector preprocessing, DB postprocess, recognizer preprocessing and CTC decode are included by both harnesses. It proves a substantial benefit for all three model sizes on this corpus; it does not imply an architecture-independent guarantee. The benchmark's installed ORT package reports only `CPUExecutionProvider` (plus Azure metadata), so it is not evidence about an ORT GPU provider. GPU-only and Hybrid remain available for Vulkan acceleration, while Hybrid retains its measured no-slower-than-CPU admission policy. ### 2026-08-16: adaptive Vulkan CTC reduction experiment The Vulkan CTC Top-1 shader now contains a cooperative 256-lane vocabulary reduction for adapters where the terminal recognizer rows are a GPU bottleneck. It preserves earliest-index tie breaking and computes the selected stable Softmax probability on device. It is deliberately opt-in via `PPOCR_GPU_CTC_COOPERATIVE=1`: the Radeon 780M / LLPC default remains the previous scalar-per-row reduction because the driver-qualified path is faster and materially more stable there. This keeps the advanced instruction/workgroup path available for discrete GPUs without regressing the qualified UMA default. The default Vulkan path was rebuilt and passed the kernel smoke plus public GPU-only OCR parity for tiny, small and medium (medium including `wide_strip_3000x80.ppm`). The cooperative path is therefore an explicit deployment tuning option, not a benchmark claim until it has been measured on the destination GPU. ### 2026-08-16: GPU-only full-network contract revalidation The public `Backend::gpu_only` route remains a strict Vulkan neural-network execution path: RGB resize/normalization, detector, recognizer transformer, terminal Softmax/CTC Top-1, and recognizer crop batching execute on Vulkan; construction or execution fails closed if the complete model graph cannot be lowered. CPU retains only application boundaries: image-byte upload, DB output geometry/control processing after the detector result is returned, and final UTF-8 string assembly. No ONNX Runtime library or CPU neural operator is used by this route. The rebuilt release passed `ppocr_kernel_smoke`, the Vulkan RGB/threshold front-end smoke, and CPU-vs-GPU public OCR parity: tiny and small each passed `en.ppm`, `line_en.ppm`, and `mixed.ppm`; medium passed those plus `wide_strip_3000x80.ppm`. Maximum confidence deltas were respectively `0.0494437`, `0.0136318`, and `0.0081339`, while decoded text and geometry were exact. A fresh local Radeon 780M / LLPC steady-state tiny GPU-only probe on `mixed.ppm` (one warm-up, three timed runs) measured `137.016 ms` mean, `131.292 ms` median, and `7.298 FPS`. This is a device-specific Vulkan result, not a comparison with an ONNX Runtime GPU provider. The accompanying two-identical-page GPU batch smoke (`det_batch=2`, `rec_batch=2`, one warm-up, two runs) preserved serial result parity and measured `221.428 ms` serial vs `194.733 ms` batched mean (`1.137x`). ### 2026-08-16: GPU-only small-model immutable-residency cache GPU-only OCR now keeps a detector/recognizer pair's fused immutable Vulkan weights resident across the detector-to-recognizer hand-off when their combined payload is at most `64 MiB` (override with `PPOCR_GPU_CONSTANT_CACHE_MB` before constructing `OCR`). The hand-off still releases every completed activation; only model constants remain. Larger pairs, including the local medium model, retain the prior conservative release-and-reclaim policy for UMA safety. This changes no inference operator or fallback policy. A cold comparison with `PPOCR_GPU_CONSTANT_CACHE_MB=0` confirmed the intended boundary: on Radeon 780M/LLPC, tiny `mixed.ppm` with two warm-ups/eight runs changed from `318.372 ms` (re-upload per stage) to `133.153 ms` with the resident pair (`2.39x`). The resident run retained `128.241 ms` median and `7.510 FPS`. Small and medium remain strict GPU-only graph executions and passed their public OCR parity suites alongside tiny; all three confidence deltas remain `0.0494437`, `0.0136318`, and `0.0081339` respectively. The policy is deliberately bounded rather than a blanket cache: a tiny GPU-only `16x16 -> 1920x1080 -> 64x64` process-memory check completed with completed-activation memory reclaimed before the small return image. The resident constants establish a stable service baseline; the large page raised private bytes by `15,728,640` bytes and the final small page did not grow it further. A full ascending-to-8K ladder remains the stronger regression gate. ### 2026-08-16: cached GPU-only 8K memory ladder and three-mode snapshot The full tiny GPU-only memory regression was rerun after enabling bounded immutable-pair residency, using the existing `16x16` through `7680x4320` resolution/aspect-ratio ladder and its recovery tail. It passed `--require-post-large-decline`. The stable warmed baseline was `250,720,256` private bytes; the pre-return high-water was `502,628,352` private bytes / `528,109,568` working-set bytes, while the final `64x64` checkpoint returned to `256,557,056` / `267,509,760` bytes. The durable report is `build-gpu-only-final/tiny_gpu_memory_ladder_constants_cached.json`. For a separate steady-state `mixed.ppm` snapshot (16 native worker threads, two warm-ups, eight timed runs), the same release measured: | model | CPU-only mean | Hybrid mean | GPU-only mean | preferred local mode | | --- | ---: | ---: | ---: | --- | | tiny | 45.476 ms | 53.561 ms | 133.153 ms | CPU-only | | small | 93.684 ms | 170.108 ms | 462.873 ms | CPU-only | | medium | 376.173 ms | 542.679 ms | 2435.894 ms | CPU-only | These figures are intentionally not presented as a universal GPU win: the Radeon 780M is an UMA adapter and this full page is CPU-SIMD-favourable. Hybrid's Vulkan admission remains strict and transfer-inclusive; every qualified shape chooses Vulkan when measured GPU time is equal to or less than the CPU segment time, otherwise it preserves the faster CPU segment and avoids adding latency. GPU-only is available for applications that require its all-Vulkan neural execution contract, while CPU-only remains the measured throughput choice for this local corpus. ### 2026-08-16: GPU-only contract recheck The release build was rechecked after the GPU-only integration pass. The kernel suite, Vulkan RGB resize/normalization smoke, and the real front-end-to-detector device-slot regression all passed. On `en.ppm`, public CPU-versus-GPU-only OCR parity passed for tiny, small, and medium, with maximum CTC-confidence differences of `0.0494437`, `0.0123513`, and `0.0081339`; decoded text and box geometry were exact. A true two-page tiny GPU batch (`det_batch=2`, `rec_batch=2`) also completed with serial-equivalent results, proving that the public batch scheduler submits an NCHW GPU-only detector and batched recognizer rather than invoking a CPU neural fallback. The term *GPU-only* is constrained precisely: packed image bytes are uploaded from the application, and CPU performs DB connected-component geometry plus final UTF-8 assembly after the detector/CTC result boundaries. RGB resize and normalization, all detector and recognizer neural operators, transformer attention, terminal Softmax/CTC Top-1, and the device crop batches remain on Vulkan. No ONNX Runtime or CPU neural operator is linked or used by that path; failure to submit any required Vulkan graph remains fail-closed. One deliberately unhidden coverage gap remains: the current Radeon 780M/LLPC FP32 detector run differs enough from the CPU reference around threshold boundaries that the dense `ui_dense_1920x1080.ppm` page does not yet preserve all CPU box/text records. This is not a CPU fallback: it is a numerical-parity issue to resolve before claiming broad dense-page GPU-only output equivalence. The qualified normal-page and batch contract above remains passing. ### 2026-08-16: GPU-only DB postprocess completion `Backend::gpu_only` now retains the detector probability map on Vulkan through the full DB postprocess: thresholding, 8-connected-component traversal, mean score filtering, rectangular unclip, source-coordinate conversion, and reading order. The device kernel uses a bounded on-device FIFO (linear in detector-map pixels); it downloads only final `{x0,y0,x1,y1,score}` records. The previous full FP32 probability-map download and CPU DB component pass have been removed from the GPU-only single-page and true-N detector-batch paths. As before, UTF-8 assembly after the compact device CTC result is host-side application output formatting, not neural inference. Rebuilt `build-gpu-only-final` validation on AMD Radeon 780M/LLPC passed: | Check | Coverage | Result | | --- | --- | --- | | Kernel + Vulkan front-end | SIMD kernels; RGB resize/normalize; GPU threshold | passed | | GPU-only OCR, tiny | `en`, `line_en`, `mixed` | 6 results, max confidence delta `0.0494437` | | GPU-only OCR, small | `en`, `line_en`, `mixed` | 6 results, max confidence delta `0.0136318` | | GPU-only OCR, medium | `en` | 2 results, max confidence delta `0.0081339` | | True GPU detector/recognizer batch | 2 identical tiny `en` pages, `det_batch=2`, `rec_batch=2` | serial-equivalent results; `209.975 -> 193.990 ms` (`1.082x`) | This is a real GPU-only execution contract—no ONNX Runtime and no CPU neural or DB floating-point fallback are linked into the route. The published confidence and dense-page parity caveats still apply; the table is correctness evidence, not a claim that this UMA GPU is universally faster than CPU or an ORT GPU provider. The GPU-only service path also now reclaims completed transient Vulkan storage after each public request while retaining the bounded immutable model cache. `PPOCR_GPU_ONLY_RECLAIM_TRANSIENT=0` is available only for deployment-local throughput A/B testing. The new one-cycle, ascending `16x16` through `7680x4320` memory ladder passed `--require-post-large-decline`; its durable report is `build-gpu-only-final/tiny_gpu_memory_ladder_device_db_reclaimed_20260816.json`. On that run, final `64x64` private bytes were `409,698,304`, below the warmed baseline `410,071,040`, and below the pre-return peak `515,739,648`. ### 2026-08-16: native-byte GPU-only image ingress The strict Vulkan front end now uploads the application RGB buffer as packed native bytes. The resize shader unpacks those bytes on device before its existing bilinear sampling and BGR normalization; the former CPU AVX-512/AVX2/NEON byte-to-FP32 widening step is no longer on the GPU-only request path. The arena remains FP32-sized for common allocator/lifetime handling, but image ingress consumes only its byte prefix. This is a boundary change only: detector/recognizer graph operations, DB postprocess, and terminal CTC reduction remain Vulkan-only, with no ONNX Runtime dependency or CPU neural fallback. Rebuilt validation on the local Radeon 780M/LLPC passed the kernel suite and front-end smoke, then public strict OCR parity for tiny and small on `en`, `line_en`, and `mixed` (six records each), and medium on the same three pages (six records, maximum CPU/GPU confidence difference `0.0081339`). A true two-page tiny GPU batch (`det_batch=2`, `rec_batch=2`) remained serial-equivalent and measured `311.699 -> 227.434 ms` mean (`1.371x`) in a three-run local probe. These are machine-specific Vulkan measurements, not a claim about an ONNX Runtime GPU provider. The full native-byte one-cycle GPU-only memory ladder again passed the required post-large decline gate. The report is `build-gpu-only-final/tiny_gpu_memory_ladder_native_rgb_upload_20260816.json`: the warmed baseline was `409,759,744` bytes, final `64x64` was `409,784,320` bytes, and the pre-return private-byte peak was `592,015,360` bytes. Thus the completed large-image workspace fell by 30.8% before the final small-image checkpoint. ### 2026-08-22: strict GPU-only requalification and CTC metadata correction `Backend::gpu_only` remains a fail-closed Vulkan path: RGB upload/resize and normalization, detector, Vulkan DB postprocess, crop resize, recognizer, and CTC Top-1 run on the device. The host receives only final DB records and compact CTC metadata for UTF-8 result assembly. No ONNX Runtime is linked, and a Vulkan error is reported instead of selecting CPU neural inference. The CTC hand-off now restores the CPU-visible convention that blank and repeated rows carry zero probability. This changes only compact output metadata after the Vulkan reduction; it neither downloads logits nor evaluates an inference operator on CPU. Fresh Radeon 780M / LLPC checks from `build-gpu-only-ep`: | Check | Result | | --- | --- | | Tiny detector `1x3x160x704`, 2 runs | max error `5.63276e-7`; CPU/GPU mean `10.269 / 45.892 ms` | | Small detector `1x3x160x704`, 2 runs | max error `3.05939e-7`; CPU/GPU mean `19.456 / 65.210 ms` | | Medium detector `1x3x160x640`, 2 runs | max error `7.56991e-8`; CPU/GPU mean `80.544 / 266.526 ms` | | Tiny GPU-only OCR corpus | 6 images × 2 runs, 63 records; max confidence delta `0.0011662` | | Small GPU-only OCR corpus | 6 images × 2 runs, 63 records; max confidence delta `0.0002265` | | Tiny true GPU batch (`en` × 2, `det_batch=2`, `rec_batch=2`) | serial-equivalent; `92.841 -> 83.830 ms` (`1.107x`) | The corpus covers `en`, `mixed`, low-contrast, ultra-wide, ultra-tall, and `1920x1080` screenshot pages. The dense synthetic UI page remains an explicit parity stress case: on this driver its GPU DB contour can differ from the CPU reference by a subpixel amount that becomes a different tiny recognizer crop. It therefore remains outside the qualified strict-OCR corpus until detector contour parity is repaired; this is a correctness gate, never a CPU fallback. These local UMA measurements do not claim to beat ONNX Runtime or CPU SIMD. ### 2026-08-22: GPU-only execution and memory recheck The current public `Backend::gpu_only` route is an all-Vulkan inference contract: RGB upload/resize/normalization, detector, DB threshold/component postprocess, recognizer crop resize, recognizer, and CTC Top-1 stay on the device. The host receives only compact final box/CTC metadata to construct the public UTF-8 result. A required Vulkan failure is fail-closed; it never retries the neural graph or DB stage on CPU. After rebuilding `build-gpu-only-ep`, the following local Radeon 780M / LLPC checks passed: | Check | Result | | --- | --- | | Kernel SIMD/Vulkan smoke | passed | | Tiny detector `1x3x160x704` | max error `5.63276e-7`; CPU/GPU `10.816 / 44.449 ms` | | Small detector `1x3x160x704` | max error `3.05939e-7`; CPU/GPU `40.614 / 78.777 ms` | | Medium detector `1x3x160x640` | max error `7.56991e-8`; CPU/GPU `86.750 / 277.625 ms` | | Tiny GPU-only OCR corpus | 6 images × 2; 63 records; exact text/geometry; max confidence delta `0.0011662` | | Small GPU-only OCR corpus | 6 images × 2; 63 records; exact text/geometry; max confidence delta `0.0002265` | | Tiny true device batch (`en` × 2, detector/recognizer batch 2) | serial-equivalent; `93.460 -> 85.286 ms` (`1.096x`) | The same tiny instance was then warmed over an ascending source ladder (`16x16`, `en`, `mixed`, low contrast, `3000x80`, `80x3000`, screenshot, dense UI), followed by `en` again. The post-large `en` checkpoint was `605,151,232 B` private / `505,942,016 B` working set, below the preceding large-page high-water (`605,732,864 B` / `506,232,832 B`). This verifies that completed large-image transient storage is released/reused rather than retained as an ever-growing per-request allocation. The synthetic dense UI corpus still has a known CPU/GPU result-parity failure caused by a subpixel DB-component boundary becoming a different tiny crop. It remains an explicit regression target, not a supported parity claim and not a condition that permits CPU fallback. Production medium detector `1x3x160x704` likewise remains driver-unqualified on this adapter and reports `VK_ERROR_DEVICE_LOST` fail-closed. These UMA measurements are correctness and memory-lifetime evidence; they do not claim a universal performance win over ONNX Runtime or CPU SIMD. ### 2026-08-22: Vulkan admission and multi-mode recheck Hybrid keeps its required transfer-inclusive admission rule: a Vulkan segment is used only after the same complete boundary is measured no slower than the CPU SIMD segment. The optional complete-GPU-graph experiment is now read once when `OCR` is constructed; it cannot accidentally supersede the normal per-operator no-slower policy during a request. This protects hybrid latency and CPU utilization on UMA adapters, where a valid complete Vulkan graph can still be slower than the CPU path. On the local Radeon 780M / AVX-512 host, `mixed.ppm` (three text regions, two warm-ups/five timed runs) measured the following C++ implementation values: | Model | CPU-only | Hybrid | GPU-only | | --- | ---: | ---: | ---: | | tiny | `15.616 ms` | `15.888 ms` | `60.750 ms` | | small | `41.700 ms` | `42.537 ms` | `173.970 ms` | | medium | `175.402 ms` | `201.552 ms` | unavailable on this production OCR shape | Hybrid tracing for medium also observed two eligible Vulkan segments: crop RGB+Conv.0 (`1.426` vs `1.632 ms`, GPU/CPU) and one terminal CTC GEMM (`5.438` vs `5.636 ms`). A slower same-model CTC shape (`5.058` vs `4.350 ms`) remained on CPU. This is the intended behavior: Vulkan lowers CPU work when it is equal or faster, while slower local segments do not make the request slower. The revised strict Vulkan regression passed tiny and small six-image OCR corpora (two repeats, 63 records each), the kernel smoke, and a true tiny N=2 detector/recognizer batch (`98.783 -> 91.599 ms`, `1.078x`). The small-to-large-to-small GPU memory ladder retained a bounded final `en` checkpoint of `605,863,936 B` private / `507,125,760 B` working set after the screenshot and dense-UI phases. These are local results, not an unsubstantiated claim of universal superiority over ONNX Runtime; a matched ORT benchmark on the target GPU remains required before making that claim. ### 2026-08-22: GPU-only replay result consistency The strict GPU-only replay path now applies the same compact CTC metadata convention as the non-replayed GPU path: blank and repeated CTC rows have zero emitted-character probability. This is performed only after the Vulkan CTC reduction has downloaded its `[N,T]` result records; logits are never downloaded and no CPU neural operator is introduced. It prevents a page with multiple replayed recognition-width graphs from reporting a confidence that differs merely because its command buffers were coalesced into one queue submission. During the same requalification, the approximate AVX2/AVX-512 vector-exp sigmoid was changed to an explicit deployment opt-in (`PPOCR_ENABLE_AVX512_SIGMOID` or `PPOCR_ENABLE_AVX2_SIGMOID`). The default uses the exact scalar-libm logistic expression because detector probabilities feed the DB threshold boundary. This restores the tight kernel numerical check while preserving optional ISA experimentation for deployments that have qualified its FP32 drift. Fresh release checks on the local AMD Radeon 780M / LLPC stack: | Check | Result | | --- | --- | | Backend probe | Vulkan loader/compute/full graph: `1/1/1`; device `AMD Radeon 780M Graphics` | | Kernel smoke | passed | | Detector graph, tiny `1x3x160x704` | max error `5.63276e-7`; CPU/GPU mean `10.798 / 32.052 ms` | | Detector graph, small `1x3x160x704` | max error `3.05939e-7`; CPU/GPU mean `20.878 / 51.629 ms` | | Detector graph, medium `1x3x160x640` | max error `7.56991e-8`; CPU/GPU mean `103.525 / 290.357 ms` | | GPU-only OCR, tiny | 7 images × 2 runs; 64 records; exact text/geometry; max confidence delta `0.0011663` | | GPU-only OCR, small | 7 images × 2 runs; 64 records; exact text/geometry; max confidence delta `0.0002261` | | GPU-only replay stress, tiny screenshot (`rec_batch=2`) | 3 runs; 34 records; exact text/geometry; max confidence delta `0.0003594` | The qualified image set is `16x16`, English, mixed-script, low-contrast, ultra-wide, ultra-tall and `1920x1080` screenshot pages. The production medium OCR detector shape remains fail-closed on this Radeon/LLPC driver with `VK_ERROR_DEVICE_LOST`; it is a driver qualification limitation, not a CPU fallback. These results establish complete GPU-only execution and replay consistency, but they are not a claim that this UMA Vulkan implementation outperforms ONNX Runtime. ### 2026-08-22: persistent replay activation-release regression The strict Vulkan replay cache now owns the full set of arena slots captured while its command buffer is recorded, including private intermediate activations and manually pinned RGB/input slots. Dropping a replay graph (on model release or optional cache eviction) unpins and releases this complete set before reusable arena storage is reclaimed. This closes a device-memory lifetime gap where only public input/output descriptors were previously released. `ppocr_gpu_graph_mem_soak` now accepts `--device-input-replay`. Unlike the ordinary host-input memory ladder, this mode forces persistent command-buffer record/replay, releases the model after every shape, and requires both `live_bytes` and `allocated_bytes` to be zero at each `replay-release` checkpoint. It is a logical Vulkan-arena test; Windows process `PrivateUsage` can still retain allocator high-water pages and must not be misreported as immediately returned physical memory. Validated with the release build on AMD Radeon 780M / LLPC: | Model | Device-input replay ladder | Result | | --- | --- | --- | | tiny detector | `1x3x64x128 -> 96x256 -> 128x512 -> 64x128` | every release: `live=0`, `allocated=0` | | small detector | `1x3x64x128 -> 96x256 -> 128x512 -> 64x128` | every release: `live=0`, `allocated=0` | The normal small-to-large-to-small host-input ladder also remains green: tiny reaches a `46,006,272`-byte reusable arena high-water and returns after every sample to its `1,713,924`-byte immutable baseline; small reaches `73,007,104` bytes and returns to `9,813,844` bytes. Capacity is deliberately reusable, while the immutable baseline proves no dynamic activation stays live after the largest shape. Reproduce the replay lifetime test: ```powershell $models = 'D:\workprj\aicoder\.tmp\ocr-models' .\build-gpu-only-ep\ppocr_gpu_graph_mem_soak.exe ` "$models\ppocrv6_tiny_det.onnx" --device-input-replay ` 1 3 64 128 1 3 96 256 1 3 128 512 1 3 64 128 ``` The revised batch fallback keeps the GPU-only contract strict: a rejected N-batch is remembered for that exact shape and then retried as independent N=1 Vulkan pages. It does not attempt a speculative runtime reset and never falls through to CPU neural inference. Current same-page N=2 checks remain serial-equivalent: tiny `89.904 -> 84.510 ms` (`1.064x`) and small `247.571 -> 236.356 ms` (`1.047x`). Production medium at its normal OCR shape is still driver-unqualified on this adapter and fails closed with `VK_ERROR_DEVICE_LOST`; no successful medium GPU-batch claim is made. ### 2026-08-22: three-size diverse CPU/ONNX Runtime sample `bench/compare_models.ps1` now defaults to a broader real-image set: tiny input, single word, English, Chinese, mixed-script, low contrast, wide/tall strips, and `1920x1080`/`2560x1440` screenshots. This makes the standard benchmark cover scale and aspect-ratio changes instead of only a short English line. For a quick, reproducible CPUExecutionProvider comparison, the release build was measured with one warm-up and one sample each on `en`, `mixed`, and `3000x80` wide-strip pages (16 native workers, ONNX Runtime 1.28.0 CPU EP, same detector/recognizer ONNX files and pipeline semantics). The captured JSON is `build-gpu-only-ep/cpu_ort_sampled_20260822.json`. | Model | Native total | ORT total | Latency improvement | Worst sampled case | | --- | ---: | ---: | ---: | ---: | | tiny | `45.357 ms` | `301.243 ms` | `84.94%` | `81.02%` | | small | `116.614 ms` | `494.681 ms` | `76.43%` | `65.35%` | | medium | `475.979 ms` | `842.178 ms` | `43.48%` | `16.34%` | These are CPU-only results, so they demonstrate the native SIMD/fused path against the available matched ORT CPU provider rather than incorrectly attributing the result to Vulkan. The short single-sample run is useful as a cross-model sanity check, not a replacement for a longer deployment benchmark. GPU-only tiny/small and hybrid retain their independently tested Vulkan policy; medium GPU-only continues to fail closed on the tested Radeon/LLPC production shape. ### 2026-08-22: hybrid admission and GPU residency tightening Hybrid admission now uses one strict rule for its RGB preprocessing, fused detector-stem, recognizer-stem and graph-level Vulkan boundaries: measured upload + dispatch + readback must be `GPU <= CPU`. The earlier 15% latency grace for three RGB/stem paths was removed, so hybrid cannot trade a slower request merely to move work from CPU. Equal-or-faster GPU segments are still preferred, exactly as the public backend contract requires. On the Radeon 780M / LLPC trace, the small mixed page correctly rejected the slower GPU detector stem (`8.360 / 6.009 ms`), recognizer stem (`1.130 / 0.892 ms`) and CTC projections. A medium `1920x1080` trace also admitted only qualifying CTC shapes (for example `15.470 / 21.808 ms`), while leaving slower shapes on CPU. Thus hybrid lowers CPU use only where the full boundary is non-regressive; it is not a blanket Vulkan offload mode. `PPOCR_GPU_RECLAIM_TRANSIENT=0|1` is now the common retention switch for both GPU-only and optional complete-graph hybrid execution; the old `PPOCR_GPU_ONLY_RECLAIM_TRANSIENT` spelling remains supported. The default is `1`. A GPU-only tiny multi-size ladder (`16x16`, normal, wide, tall, 1080p, 1440p, then `16x16`) has been captured for the default and explicit zero-retention policies in `build-gpu-only-ep/tiny_gpu_memory_diverse_20260822.json` and `build-gpu-only-ep/tiny_gpu_memory_diverse_retain0_20260822.json`. Windows process accounting still retains allocator high-water pages, so the authoritative per-graph lifetime gate remains the Vulkan arena's `live_bytes`/`allocated_bytes` replay regression rather than PrivateUsage alone. ### 2026-08-22: runtime-generation-safe hybrid admission The RGB/stem hybrid admission caches are now keyed by the Vulkan runtime generation as well as image and operator shape. A device-loss recovery tears down arena buffers and can select a different physical-device configuration; reusing a pre-recovery yes/no verdict would be unsound. The next request now re-runs its transfer-inclusive qualification automatically, while normal same-generation requests remain a fast cache lookup. An AVX-512 sigmoid A/B was also rechecked. On the 16-worker English sample it did not show a stable all-model speedup (tiny `14.769 ms`, small `33.743 ms`, medium `145.255 ms`), so it stays opt-in. Tiny and small eight-image GPU-only parity runs still passed when the CPU-side option was present (exact text/geometry; confidence deltas `0.0011663` and `0.0002261`). Keeping exact default detector sigmoid protects DB threshold boundaries while leaving a qualified deployment an explicit SIMD option.