Files
rapidocr/docs/PERFORMANCE.md
Cursor AgentandDaniel 3622c4598e feat: add C ABI, Python package, and CPU ISA paths
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>
2026-10-04 23:40:06 +00:00

5.0 KiB
Raw Permalink Blame History

CPU performance

The hot path is the built-in ONNX interpreter: convolutions, GEMM, and the RGB-to-NCHW front end. Post-process (DB boxes, CTC decode) is small next to those kernels. Threads come from a persistent pool (PPOCR_BENCH_THREADS, default min(16, hardware_concurrency)) plus the recognizer's outer batch pool, which is capped by the hardware thread count.

One binary, three targets

Target Where the code lives When it runs
x86 without AVX (scalar / SSE2) src/kernels.cpp, compiled with -mno-avx -mno-avx2 -mno-avx512f no AVX2
x86 AVX2 + FMA, no AVX-512 src/kernels_avx2.cpp, compiled with -mavx2 -mfma -mno-avx512f -mno-avx512bw -mno-avx512vl -mno-avx512dq avx2 and fma CPUID bits, and PPOCR_FORCE_ISA is not scalar
x86 AVX-512 src/kernels_avx512.cpp, compiled with -mavx512f -mavx512dq -mavx512bw -mavx512vl -mbmi -mbmi2 -mfma AVX-512F/DQ/BW/VL plus AVX2, FMA, BMI1, and BMI2, and OS XSAVE state, unless PPOCR_DISABLE_AVX512 or PPOCR_FORCE_ISA=avx2/scalar
AArch64 NEON src/kernels.cpp (NEON is baseline on AArch64) not forced off with PPOCR_FORCE_ISA=scalar

An AVX2-only machine never executes the AVX-512 object file. HasAvx512() returns false unless CPUID and XCR0 say the OS has enabled AVX-512, so PPOCR_FORCE_ISA=avx512 on that machine stays on AVX2 or scalar. The isa_encoding test disassembles the baseline and AVX2 objects and fails if they contain zmm or AVX-512 mask registers.

PPOCR_FORCE_ISA=scalar|avx2|avx512|neon selects a legal path for benchmarks. It cannot turn on an ISA the CPU does not have.

What changed

Recognizer attention uses GemmAccumulate. The AVX2 and AVX-512 kernels previously streamed the weight matrix once per row. They now pair two rows so each weight vector is loaded once and applied to two accumulators. Each output element still walks K in ascending order. Set PPOCR_DISABLE_AVX2_GEMM_ACC2 or PPOCR_DISABLE_AVX512_GEMM_ACC2 to restore the one-row kernel.

On AArch64 the GEMM used to reload and store C on every K step (four-wide NEON). It now keeps a 2×16 register tile, which is the same idea as the x86 kernels. PPOCR_DISABLE_NEON_GEMM_BLOCK restores the scalar loop.

tools/bench_cpu.sh and ppocr_isa_bench print the active ISA and time a 32×256×256 GEMM, the matching accumulate, a 960×544 identity RGB normalize, and optionally a full OCR.

What was measured

Host: 4 vCPU Intel Xeon (family 6, model 207) with AVX2, AVX-512F/DQ/BW/VL, BMI1, and BMI2. g++ 13.3. There is no ARM board and no AVX2-only CPU in this environment. AVX2 is measured by PPOCR_FORCE_ISA=avx2, which keeps HasAvx512() false. The AVX2 object file was disassembled and contains no zmm or AVX-512 mask registers (tests/check_isa_encoding.sh).

GEMM accumulate, 32×256×256, mean of 8 runs (ppocr_isa_bench)

Path gemm_accumulate
AVX-512, two-row (new) 0.070 ms
AVX-512, one-row (PPOCR_DISABLE_AVX512_GEMM_ACC2=1) 0.094 ms
AVX2, two-row (new) 0.082 ms
AVX2, one-row (PPOCR_DISABLE_AVX2_GEMM_ACC2=1) 0.125 ms
scalar (PPOCR_FORCE_ISA=scalar) 0.186 ms

The same-process Gemm timing (not the accumulate kernel) was about 0.044 ms on AVX-512, 0.059 ms on AVX2, and 0.089 ms on scalar. Identity RGB normalize of a 960×544 page was about 0.6–0.7 ms on every path; it is not the bottleneck.

End-to-end PP-OCRv6 tiny, 960×240 “Hello RapidOCR 123”

ppocr_bench, PPOCR_BACKEND=cpu, 1 warmup + 3 runs, one recognized line:

ISA mean
AVX-512 17.3 ms
AVX2 (PPOCR_FORCE_ISA=avx2) 136 ms
scalar 233 ms

Turning the two-row accumulate off changed this page by about 1 ms, inside the run-to-run spread. The page is convolution-bound. The accumulate win shows up on the GEMM shape above, which is the attention/GEMM kernel, not on this one short line. The large AVX-512 versus AVX2 gap is the existing AVX-512 convolution kernels, selected at runtime; it is not a claim that the two-row accumulate produced an 8× end-to-end speedup.

AArch64 NEON

No ARM CPU was available. src/kernels.cpp cross-compiles with aarch64-linux-gnu-g++ -O3 and the GEMM object contains fmla. The same 8×6906×192 recognizer-vocabulary shape was timed under qemu-aarch64-static:

  • Standalone copy of the old NEON reload loop versus the new register tile: 166 ms → 76 ms. That is the algorithm before/after. The tile result matched the reload loop (max abs error 0).
  • In-tree Gemm linked against that object: register tile 56–64 ms. Checksum and max abs matched the scalar fallback (PPOCR_DISABLE_NEON_GEMM_BLOCK=1, 31–34 ms) exactly (sum=3.171875, max_abs=1.611328). qemu-user's translator often runs scalar ARM faster than NEON, so the scalar number is not a wall-clock result for a Cortex or Neoverse core.

qemu-user is not cycle-accurate. It does execute the NEON instructions, and the NEON-versus-NEON ratio matches the drop in C-matrix traffic. Full OCR was not run under qemu.