Expose the PP-OCRv6 engine through a stable C ABI and installable shared and static libraries, plus a ctypes Python package that follows the RapidOCR call shape. Keep AVX-512 out of the AVX2 object file and speed up GEMM accumulate and the AArch64 GEMM tile. Co-authored-by: Daniel <znsoft@163.com>
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. Measured AVX2 / AVX-512 / NEON notes are in
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
1x3x160x704remains unqualified and fails closed; no result is substituted from CPU. Nearby1x3x160x640and1x3x160x768shapes 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_THREADSis 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_smokeverifies paired/odd GEMM rows, scalar/vector column tails, optional bias, and a recognizer-sized projection. Run it normally and withPPOCR_DISABLE_AVX512=1to 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
1x1register 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+ReLUtile 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
3x3feature 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=1keeps 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_UPPERconvolution 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 withPPOCR_DISABLE_AVX512_CONV2X2_SAME=1. - Equal-shape residual
1x1 Conv -> Addpairs 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 -> SigmoidandConv -> HardSigmoidgate pairs are also fused. Their convolution result is activated in place, removing the generic unary output allocation; HardSigmoid uses one exact SIMD pass forclamp(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 -> MulSwish gates are recognized after Conv+BatchNorm folding. The fused node writesx * 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=1is 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):
# 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.
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 -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:
$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<Image>&) 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:
.\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:
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<CropBounds> 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 -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.
& '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.
$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:
$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):
.\build\ppocr_mem_soak.exe <det.onnx> <rec.onnx> <dict.txt> 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:
& 'D:\workprj\aicoder\.tmp\ocr-models\.venv\Scripts\python.exe' `
.\bench\generate_memory_ladder.py --output .\build\memory_ladder
.\build\ppocr_mem_soak.exe <det.onnx> <rec.onnx> <dict.txt> 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:
& 'D:\workprj\aicoder\.tmp\ocr-models\.venv\Scripts\python.exe' `
.\bench\memory_ladder_regression.py `
--cpp .\build\ppocr_mem_soak.exe --det <det.onnx> --rec <rec.onnx> `
--dict <dict.txt> --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 Results 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<const Tensor*> 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:
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:
$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:
$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:
.\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:
$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.