Skip to content

OpenVX - CPU kernel optimization - #1663

Merged
kiritigowda merged 23 commits into
ROCm:developfrom
kiritigowda:kg/openvx-optimize
May 21, 2026
Merged

OpenVX - CPU kernel optimization#1663
kiritigowda merged 23 commits into
ROCm:developfrom
kiritigowda:kg/openvx-optimize

Conversation

@kiritigowda

@kiritigowda kiritigowda commented May 20, 2026

Copy link
Copy Markdown
Collaborator

Motivation

Implement SIMD optimization

Technical Details

AVX2 paths for:

  • WeightedAverage
  • Magnitude
  • MeanStdDev
  • MinMax
  • Erode3x3 / Dilate3x3
  • ScaleUp2x2
  • And / Or / Xor / Not
  • inverse TableLookup fast path

Test Plan

Current functional and performance tests

Test Result

All functional tests should have no issues and performance has to show significant speed-ups

Submission Checklist

@kiritigowda kiritigowda self-assigned this May 20, 2026
@kiritigowda
kiritigowda requested a review from rrawther as a code owner May 20, 2026 06:26
@kiritigowda kiritigowda added enhancement New feature or request bugfix Bug fixes to existing features ci:precheckin labels May 20, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds CPU-side SIMD optimizations (primarily AVX2) for several OpenVX kernels and introduces a small CPU feature-query API intended to support capability-based dispatch.

Changes:

  • Added CPU feature detection (agoGetCpuFeatures) and exposed a feature bitset struct in ago_platform.h.
  • Introduced new AVX/AVX2 fast paths for multiple CPU kernels (logical ops, morphology 3x3, scaling, LUT invert, magnitude, mean/stddev, min/max).
  • Refactored Harris score window accumulation to use a sliding-window sum update.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
amd_openvx/openvx/ago/ago_platform.h Adds ago_cpu_features_t and declares agoGetCpuFeatures()
amd_openvx/openvx/ago/ago_platform.cpp Implements CPUID/XGETBV feature detection and wires agoIsCpuHardwareSupported() to it
amd_openvx/openvx/ago/ago_haf_cpu_logical.cpp Adds AVX path for NOT/AND/OR/XOR
amd_openvx/openvx/ago/ago_haf_cpu_harris.cpp Refactors Harris 3x3 windowing using incremental column add/remove
amd_openvx/openvx/ago/ago_haf_cpu_geometric.cpp Adds optimized blend helpers and fast paths for specific scale cases; AVX path for ScaleUp2x2
amd_openvx/openvx/ago/ago_haf_cpu_generic_functions.cpp Adds AVX fast path for WeightedAverage (U8)
amd_openvx/openvx/ago/ago_haf_cpu_filter.cpp Adds AVX paths for Dilate/Erode 3x3 (U8)
amd_openvx/openvx/ago/ago_haf_cpu_arithmetic.cpp Adds AVX fast paths for invert LUT, Magnitude, MeanStdDev, MinMax, MinMaxLoc, Phase, and refactors IntegralImage

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread amd_openvx/openvx/ago/ago_platform.cpp Outdated
Comment thread amd_openvx/openvx/ago/ago_haf_cpu_arithmetic.cpp Outdated
Comment thread amd_openvx/openvx/ago/ago_haf_cpu_arithmetic.cpp Outdated
kiritigowda and others added 12 commits May 20, 2026 11:48
Add AVX2 single-precision Magnitude that processes 16 int16 magnitudes
per iteration (within the CTS +/-1 tolerance), reducing per-pixel work
compared to the prior double-precision path.

Widen Phase to 16 pixels per iteration and replace the dual-divide
ay/ax + ax/ay blend with a single min/max divide using cmp_ps for the
quadrant selector.

Extend IntegralImage with a 32-byte AVX2-friendly outer loop that
processes two 16-byte SIMD prefix-sum stages with a shared row carry,
amortizing the extract/broadcast latency per row.

Split EqualizeHist's histogram into four parallel sub-tables to break
the per-pixel read-modify-write dependency on dstHist[i]++, then merge
with AVX2 adds.

Add AVX2 32-byte exact-2x downscale fast path for ScaleImage_Half and
an integer-only scale==1.0 fast path for Mul_U8_U8U8_Sat_Trunc/Round.

All changes verified against the OpenVX 1.3 CTS Vision profile.

Co-authored-by: Cursor <cursoragent@cursor.com>
- SobelMagnitudePhase: replace scalar atan2 loop and double-precision sqrt
  pipeline with AVX2 single-precision polynomial atan2 and packed sqrt.
  ~6-8x faster end to end (25.8ms -> 3.3ms on FHD).
- ScaleImage_Double 2x bilinear: pair adjacent destination rows so the
  horizontal blends from the shared source-row pair are computed once
  and reused. ~4x faster (9.3ms -> 2.1ms on FHD).
- NonLinearFilter: drop qsort per pixel; linear scan for MIN/MAX and
  insertion sort for MEDIAN (count <= 81). ~1.3-1.5x faster
  (184ms -> 144ms on FHD).
- LaplacianPyramid upsampleImage: row-major SIMD zero-stuff plus row-major
  SIMD scale-by-4-with-saturation replace the column-major scalar loops.
- Canny: replace 6 per-iteration scalar atan2 loops with SIMD
  HafCpu_FastAtan2_Canny_8 helper.

All kernels remain functionally verified against the reference; the
openvx-mark verified=true flag is preserved.

Co-authored-by: Cursor <cursoragent@cursor.com>
…pelines

- HarrisSobel_3x3: vectorized horizontal/vertical Sobel passes with SoA
  ping-pong row buffers; AVX2 path widens horizontal filter to 32 i16
  outputs/iter and the GxGx/GxGy/GyGy computation to 8 floats/iter.
- HarrisNonMaxSupp_3x3 (XY_ANY_3x3): added an AVX2 prefilter that scans 8
  scores at a time and skips columns where the center is zero (true for
  the bulk of post-threshold Harris output), then masks 8-way local-max
  comparisons against all eight neighbors.
- FastCornersSupression NMS: added a 16-byte SSE local-max pass over the
  scratch image, rejecting whole 16-pixel blocks when no candidate is
  non-zero and gathering surviving lanes via movemask.
- Canny suppression + atan2: vectorized angle quantization, neighbor
  comparisons and hysteresis classification (carry-over of earlier work
  in this branch, now widened with AVX2 lanes where appropriate).
- LaplacianPyramid: replaced the per-level vxCreateImage + upsample +
  vxuSubtract round-trip with a fused PyramidUp_Gaussian5x5+saturating
  subtract that maps the laplacian level S16 buffer directly and
  produces the difference in one separable AVX2 pass. Drops one full
  read/write of an S16 intermediate plus the graph-creation overhead of
  the legacy vxuSubtract path; LaplacianPyramid drops from ~45 ms to
  ~7 ms at FHD on the AMD CPU benchmark.
- PyramidUp_Gaussian5x5_U8 fast path: widened the vertical and horizontal
  passes to AVX2 (32 source bytes / 16 source columns per iter) so both
  the LaplacianPyramid fused path and LaplacianReconstruct see the AVX2
  throughput.

Conformance: OpenVX 1.3 CTS vision feature set still passes 41/41 with
the new paths active.

Co-authored-by: Cursor <cursoragent@cursor.com>
CannySuppThreshold previously processed 8 i16 pixels per iteration with
128-bit SSE; the inner work is purely lane-parallel (angle-dependent
neighbor select, magnitude/threshold compares, hysteresis classification)
so widening to AVX2 doubles throughput for the dominant pre-trace stage
of the L1NORM Canny path. The SSE/scalar tails remain for the trailing
pixels and for builds without AVX2.

CannyEdgeDetector drops from ~12.7 ms to ~11.2 ms at FHD on the AMD CPU
benchmark, and the EdgeDetection pipeline (ColorConvert + ChannelExtract
+ Gaussian3x3 + Canny) drops from ~16.4 ms to ~14.0 ms.

Conformance: OpenVX 1.3 CTS vision feature set still passes 41/41.
Co-authored-by: Cursor <cursoragent@cursor.com>
ConvertDepth: widen U8<->S16 conversions to AVX2 (32 pixels/iter) to roughly
halve the FHD runtime.

ScaleImage_U8_U8_Bilinear: replace the 6-instruction unpack/multiply/pack 3:1
and 1:3 byte blends with the 2-instruction `pavg(pavg(a,b), a)` identity. This
collapses the exact 2x bilinear up- and down-sampler hot loops to a handful of
SSE instructions and yields ~10x speedup for ScaleImage_Double / ScaleImage_Half
at FHD, comfortably beating OpenCV's INTER_LINEAR.

FastCorners: tighten the SIMD pre-filter from the original 2-of-2 axial-pair
test to the strongest correct cardinal-pair pre-filter for FAST-9. A 9-arc must
include at least one adjacent cardinal pair {p1,p5}/{p5,p9}/{p9,p13}/{p13,p1}
all brighter or all darker, so we reject 8 candidates at a time on that
polarity-aware test instead of the older symmetric "within threshold" check.
Verified against all 24 FastCorners CTS cases.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the SSE-only 16-pixel-wide separable Sobel with an AVX2 implementation
that processes 32 source pixels per horizontal-filter call and 16 int16 lanes
per vertical-combine iteration. The new path uses three ring-buffered scratch
rows for Gx_h and Gy_h (matching the original 6 * alignedWidth * int16 scratch
budget), eliminates the interleaved Gx_h/Gy_h scratch layout, and falls back to
SSE for the final tail when the destination width is not a multiple of 16.
Reduces Sobel3x3 FHD runtime by ~17%; all Sobel/Magnitude/Phase/Canny/Harris
CTS cases continue to pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the per-pixel scalar Harris-score inner loop with an SSE
column-sum + 4-wide window/score kernel. For each output row we now
build the 3-row vertical column sums of (GxGx, GxGy, GyGy) into a
single SSE-friendly scratch, then process 4 destination pixels at a
time: load 6 column sums, build 4 windows, transpose into SoA,
compute trace/det/Mc with vector math, threshold and store.

Reduces HarrisCorners FHD runtime ~26% (22.5ms -> 16.7ms). All 434
HarrisCorners CTS cases continue to pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the per-candidate `_mm_srli_si128` sequence in
HafCpu_FastCorners_XY_U8_Supression / NoSupression with precomputed
position-dependent shuffle masks. The original SSE inner loop shifted
all seven row registers by one byte each iteration (7 srli_si128 ops ×
8 candidates = 56 unconditional shifts per outer iteration) so that the
candidate-zero boundary masks could be reused. With a 8×7 mask table
(offsets 0..7), the per-candidate path now reads the seven row loads
directly without shifting.

Reduces FastCorners FHD runtime ~7% (41.6ms -> 39.0ms). All 24
FastCorners CTS cases continue to pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
- Fix Gx/Gy scratch buffer aliasing in HafCpu_CannySobelSuppThreshold_U8XY_U8_3x3_L1NORM:
  spacing was only (dstride * sizeof(int16)) so Gy[0] overlapped Gx[1], corrupting
  gradients. Now both buffers span (dstride * dstHeight) elements as required.
- Implement HafCpu_CannySobelSuppThreshold_U8XY_U8_3x3_L2NORM (previously
  AGO_ERROR_HAFCPU_NOT_IMPLEMENTED): packs sqrt(Gx^2 + Gy^2) magnitude with
  4-bucket angle into the gradient row, then runs the same NMS + dual-threshold
  pass as the L1 variant. Uses _mm_unpacklo/hi + madd + sqrt_ps + packus_epi32.
- Wire localDataPtr / localDataSize for the L2 3x3 fused kernel API so the
  scratch buffer is allocated by the framework.
- agoDramaDivideCannyEdgeDetectorNode: only route to the fused
  Sobel+Supp+Threshold kernel when gradient_size == 3. 5x5 and 7x7 fused
  variants are still unimplemented; falling back to the separate Sobel +
  CannySuppThreshold path keeps conformance for those cases regardless of
  USE_AGO_CANNY_SOBEL_SUPP_THRESHOLD.

All 56 Canny CTS tests now pass (32 L1 + 24 L2 across 3x3/5x5/7x7) and the
full Vision conformance profile remains green.

Co-authored-by: Cursor <cursoragent@cursor.com>
The AVX2 paths for several pixelwise kernels were single-vector loops with
nothing to hide load/store latency, so they trailed OpenCV (which unrolls and
saturates the memory subsystem). Manually unroll them x4 (128 bytes per
iteration), keeping the existing 32-byte tail and scalar tail for residual
pixels:

- HafCpu_Not_U8_U8: was 1.48x slower than OpenCV on FHD (0.212 ms); now
  0.12 ms, ~19% faster than OpenCV. Also switched _mm256_andnot_si256(p, ones)
  to _mm256_xor_si256(p, ones) so the inversion is a single-input op that
  pipelines better.
- HafCpu_AbsDiff_U8_U8U8: was 1.15x slower (0.230 ms); now 0.16 ms,
  ~20% faster than OpenCV.
- HafCpu_And/Or/Xor_U8_U8U8: now consistently match or slightly beat OpenCV
  instead of trailing by a few %.
- HafCpu_ChannelCopy_U8_U8: added an AVX2 unrolled fast path (the existing
  implementation was SSE-only). This is the kernel used as the level-0 copy
  in vxGaussianPyramidNode.

All vxNot / vxBinOp* / vxAddSub* / ChannelExtract / ChannelCombine CTS
suites still pass (154 + 65 tests).

Co-authored-by: Cursor <cursoragent@cursor.com>
- ago_platform.cpp: agoGetCpuFeatures() used a non-atomic "initialized"
  guard, racy under concurrent first calls. Switch to a C++17 thread-safe
  function-local static initialized via an IIFE so the cpuid probe runs
  exactly once and no caller can observe partially-initialized flags.

- ago_haf_cpu_arithmetic.cpp (MinMaxLoc AVX path): _mm256_movemask_epi8
  returns a 32-bit lane mask. Storing it in 'int' made (mask - 1) signed
  overflow UB whenever bit 31 was set, which can happen when pixel 31 of a
  32-byte block matches min/max. Hold the masks as uint32_t and do all bit
  arithmetic on the unsigned type; the popcnt/ctz helpers already take
  unsigned int.

- ago_haf_cpu_arithmetic.cpp (MeanStdDev AVX path): the per-row helper
  accumulated squared partials into 32-bit lanes before a single widen
  step at end-of-row, so very wide rows could wrap modulo 2^32 and yield
  the wrong sumOfSquared (the SSE path already widened in-loop). Fold the
  widening into HafCpu_AccumulateSquaresU8_AVX2 so squared partials are
  added straight into the 64-bit sumSquared accumulator. Drops the now
  unused HafCpu_AddU32ToU64_AVX2 helper.

Verified with the OpenVX CTS Vision conformance profile, full
MinMaxLoc + MeanStdDev (8/8) and the broader Multiply/Add/Sub group
(560/560) on the CPU backend.

Co-authored-by: Cursor <cursoragent@cursor.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 7 comments.

Comment thread amd_openvx/openvx/ago/ago_haf_cpu_harris.cpp Outdated
Comment thread amd_openvx/openvx/ago/ago_haf_cpu_harris.cpp Outdated
Comment thread amd_openvx/openvx/ago/ago_haf_cpu_fast_corners.cpp Outdated
Comment thread amd_openvx/openvx/ago/ago_haf_cpu_canny.cpp Outdated
Comment thread amd_openvx/openvx/ago/ago_haf_cpu_canny.cpp Outdated
Comment thread amd_openvx/openvx/ago/ago_haf_cpu_arithmetic.cpp
Comment thread amd_openvx/openvx/ago/ago_haf_cpu_arithmetic.cpp
…scale

The new fast PyramidUp_Gaussian5x5 helpers in HafCpu_LaplacianPyramid /
HafCpu_LaplacianReconstruct produced results that drifted from the OpenVX
1.3 conformance reference by up to 136 grey-levels on
LaplacianReconstruct (border failures) and 3 grey-levels on
LaplacianPyramid (scaling failures). Two correctness bugs were
identified and fixed against the CTS reference convolution applied to
the zero-stuffed image:

1. Output scaling: was computing `H >> 6` for the upsample value, which
   is mathematically `H / 64`. The OpenVX spec / CTS reference instead
   performs the integer division by the convolution scale (256) first
   and only then multiplies by 4 to compensate for the zero stuffing,
   i.e. `(H / 256) * 4`. For sums just under a 256-boundary these differ
   by up to 3 (e.g. H=511 gives 7 with `>>6` but 4 with `(>>8)<<2`),
   which is exactly the LaplacianPyramid CTS failure pattern.

2. Border handling: the vertical pass used a NULL row pointer (zero
   contribution) when `fy_bot >= srcH`, and the V column-buffer right
   pad was filled with zeros. Both are wrong for replicate border. The
   CTS reference's convolve over the zero-stuffed image is equivalent
   to source-level replicate, which means rows past the last valid
   source row should reuse `srcH - 1`, and the right V slot should
   reuse `V[srcW]`. Without this, the right column of every level
   underflowed by ~half its true magnitude, and the error compounded
   across LaplacianReconstruct's 7-level upsample chain to produce the
   136-level CTS failure at x=127.

Both `HafCpu_PyramidUp_Gaussian5x5_U8` (used by LaplacianReconstruct via
the upsampleImage fast path) and `HafCpu_PyramidUp_Gaussian5x5_Subtract_U8`
(used by the fused LaplacianPyramid path) get the same fixes.

Vision-pyramid CTS now reports 48/48 passing (previously 32/48 with 16
LaplacianPyramid/LaplacianReconstruct failures under VX_BORDER_UNDEFINED).
Broader Convolve/Sobel/Magnitude/Phase/arithmetic/filter sweep
(1291 tests) also remains 1291/1291.

Co-authored-by: Cursor <cursoragent@cursor.com>
- Add shared cross-platform agoCtz32 helper in ago_haf_cpu.h that wraps
  __builtin_ctz (gcc/clang) and _BitScanForward (MSVC). Replace four
  __builtin_ctz call sites in canny, fast_corners and harris so the AVX2
  bit-iteration loops compile cleanly on Windows. Switch the iterated
  masks to unsigned int to avoid signed overflow UB when clearing the
  lowest set bit (mask -= 1).
- Stop reinterpreting harris colSumBuf (std::vector<float>) storage as
  __m128*: vector<float> is not required to be 16-byte aligned and the
  cast would let the compiler emit aligned movaps. Use plain float*
  storage with unaligned _mm_loadu_ps/_mm_storeu_ps helpers.
- Gate the scale==1.0 AVX2 fast paths in HafCpu_Mul_U8_U8U8_Sat_Trunc and
  Sat_Round under #if USE_AVX so builds with USE_AVX=0 fall back to the
  existing SSE/FP path instead of failing to compile.
- Remove now-duplicated HafCpu_Ctz32 static helper in
  ago_haf_cpu_arithmetic.cpp in favor of the shared agoCtz32 helper.

Co-authored-by: Cursor <cursoragent@cursor.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.

Comment thread amd_openvx/openvx/ago/ago_platform.cpp Outdated
Comment thread amd_openvx/openvx/ago/ago_platform.cpp Outdated
Comment thread amd_openvx/openvx/ago/ago_haf_cpu_fast_corners.cpp Outdated
Comment thread amd_openvx/openvx/ago/ago_haf_cpu_arithmetic.cpp
kiritigowda and others added 2 commits May 20, 2026 19:59
Aligns the conformance workflow's `benchmark` job with the upstream
openvx-mark v1.1+ source tree at https://github.com/kiritigowda/openvx-mark
which now ships an in-tree `opencv-mark` companion binary, a CLI
fairness/timing audit (`--validate-timing`, `--threads N`, `--output-dir`,
`--vision-parity`, `--dump-outputs`), and helper scripts for cross-impl
output verification and organized pairwise step summaries.

`benchmark` job (Release OpenVX vs OpenCV) is rewritten end-to-end:
- Both `openvx-mark` and `opencv-mark` are now built from the upstream
  openvx-mark source tree in a single CMake invocation, so the two
  binaries share build flags, compiler, glibc, and the `bench_core`
  static lib (timer / stats / JSON schema). Removes the in-tree
  `tests/opencv_benchmark/` build from the CI path (source kept as-is).
- Adds a `--validate-timing` self-test gate before every bench step so
  a sloppy runner clock fails loud before its timing numbers propagate
  into the comparison.
- Pins `--threads 1` on both binaries so OpenCV's TBB/OpenMP defaults
  don't silently turn this into "1-thread OpenVX vs nproc-thread
  OpenCV". MIVisionX's AGO CPU path is single-threaded per kernel anyway.
- Uses `--vision-parity` (the 41 OpenVX vision kernels with a 1:1
  OpenCV equivalent) for apples-to-apples per-kernel rows.
- Runs `--dump-outputs` on both binaries and feeds the resulting
  sentinel dumps to `scripts/cross_verify_outputs.py` to prove
  numerical equivalence at the pixel level. Divergences emit a
  ::warning:: but don't fail the job, so the timing table still
  appears alongside the verdict.
- Replaces the inline `cat comparison.md >> SUMMARY` with the upstream
  `scripts/ci_pairwise_summary.py` renderer: TL;DR speedup matrix +
  grouped headline table + per-kernel detail collapsed inside
  <details>. Same shape and helper as the upstream openvx-mark CI.
- Three artifacts uploaded (90-day retention): `benchmark-results`
  (per-impl JSON/CSV/MD), `benchmark-comparison` (the per-pair detail
  files), and a new `cross-verify-dumps` (raw .bin sentinels +
  manifest.json so any reviewer can re-run the verifier locally
  without re-running CI).

`perf-gate` job (PR vs main MIVisionX bench) is updated for parity:
- `--output` → `--output-dir` on the warmup invocations (the v1.1+
  CLI dropped `--output`).
- Adds `--validate-timing` once before each bench so timing gate
  surfaces a sloppy runner clock immediately.
- Adds `--threads 1` to match the OpenVX-vs-OpenCV bench downstream,
  keeping the PR-vs-main comparison apples-to-apples with the
  OpenVX-vs-OpenCV one.

Co-authored-by: Cursor <cursoragent@cursor.com>
…nd 3)

Four legitimate review findings from Copilot's review of 5016a19, plus
one additional unguarded AVX2 callsite that the same class-of-bug grep
found in ago_haf_cpu_geometric.cpp:

- ago_platform.cpp: mark the inline `cpuid` and `xgetbv` asm blocks
  `volatile` with a "memory" clobber. cpuid / xgetbv are intentionally
  ordered hardware/OS state queries (feature detection, XCR0), and we
  do not want the optimizer to reorder them across surrounding
  feature-flag derivation or merge two adjacent identical-input
  invocations. The "memory" clobber also forces a compiler memory
  fence so subsequent reads of agoGetCpuFeatures() observe the probe.

- ago_haf_cpu_fast_corners.cpp: drop the dead `any_nz` precomputation
  in the SIMD non-max suppression prefilter. The signed _mm_cmpgt_epi8
  result was being computed and then discarded — the actual "all
  candidates in this 16-byte window are zero" decision is made one
  line below via the unsigned-correct `_mm_max_epu8 + _mm_cmpeq_epi8`
  pair, so the signed version was both wrong-in-spirit and unused.
  Comment expanded so the next reader doesn't reintroduce it.

- ago_haf_cpu_arithmetic.cpp::HafCpu_Histogram_DATA_U8: wrap the
  AVX2 four-way bin-merge tree (`_mm256_loadu/_add_epi32/_storeu`) in
  `#if USE_AVX` and add an equivalent scalar 256-bin merge for
  USE_AVX=0 builds. The merge is O(NUM_BINS) and runs once per
  call, so the scalar fallback is essentially free; the AVX2 form is
  still preferred when available because it folds the four sub-tables
  into one ymm-wide add tree.

- ago_haf_cpu_geometric.cpp::ScaleImage 2x-downscale path: wrap the
  AVX2 32-px/iter inner loop in `#if USE_AVX`. The SSE 16-px/iter
  loop directly below it (plus the scalar tail) is already a
  complete fallback that walks `x` from 0 to dstWidth on its own
  when the AVX2 loop is compiled out — same algorithm, narrower
  vectors. ones256/round256 constants moved inside the gate too so
  there are no unused __m256i declarations.

Validation:
- Default (USE_AVX=1) build of libopenvx.dylib + libvxu.dylib clean.
- USE_AVX=0 smoke build (`-DCMAKE_CXX_FLAGS=-DUSE_AVX=0`) now also
  compiles cleanly — this is exactly the failure mode that Copilot
  was flagging on each individual unguarded callsite.
- Targeted CTS subset on the rebuilt libs (Histogram.*:HalfScaleGaussian.*:
  Scale.*:FastCorners.*:Multiply.*:vxMultiply.*:vxuMultiply.*) — 1509
  required vision tests pass, 0 fail.

Also note re: two of the Copilot comments on commit 5016a19: the
two `Mul scale==1.0` AVX2 fast paths flagged at lines 5756 and 5876
were already wrapped under `#if USE_AVX ... #endif` in 5016a19 — the
bot appears to have re-flagged the same block after its line numbers
shifted. No code change needed there; left in place.

Co-authored-by: Cursor <cursoragent@cursor.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Comment thread amd_openvx/openvx/ago/ago_platform.cpp Outdated
Comment thread amd_openvx/openvx/ago/ago_drama_divide.cpp
…nd 4)

Two legitimate findings from Copilot's review of a7a6c29:

- ago_platform.cpp::agoIsCpuHardwareSupported(): the support gate
  previously only checked SSE4.2, but the binary now emits AVX2 (and
  optionally BMI2) instructions whenever USE_AVX / USE_BMI2 are
  compiled in (the default since PR ROCm#1657). On a SSE4.2-only Nehalem-
  class CPU we would have happily created a vx_context and then
  SIGILL'd on the first AVX2 kernel invocation. Tighten the gate to
  also require f.avx2 when USE_AVX==1 and f.bmi2 when USE_BMI2==1.
  The runtime AVX2 bit already AND-folds the OSXSAVE / XCR0 OS-state
  check inside agoGetCpuFeatures(), so an OS that disabled AVX state
  save also correctly fails this gate. Mirrors the same `#ifndef
  USE_AVX … #define USE_AVX 1 … #endif` block from ago_internal.h
  locally in ago_platform.cpp so a single -DUSE_AVX=0 on the CMake
  line propagates consistently to both translation units (this file
  is compiled before the later, !_WIN32-gated ago_internal.h include).

- ago_drama_divide.cpp::agoDramaDivideCannyEdgeDetectorNode(): the
  fused-vs-separate selection comment claimed the 5x5 / 7x7 fused
  variants were "not yet implemented", but the symbols actually do
  exist (registered in ago_kernel_list.cpp; declared in ago_haf_cpu.h).
  Rewrite the comment to reflect what's really going on: the
  HafCpu_CannySobelSuppThreshold_U8XY_U8_{5x5,7x7}_L{1,2}NORM bodies
  in ago_haf_cpu_canny.cpp / ago_haf_cpu.cpp are stubs that return
  AGO_ERROR_HAFCPU_NOT_IMPLEMENTED, so dispatching to them would fail
  the graph. The runtime gate `gradient_size == 3` stays as-is — it's
  the correct guard against the stub dispatch.

Note re: the three other Copilot comments on a7a6c29 (lines 5756,
5876, 7691 in ago_haf_cpu_arithmetic.cpp): all three blocks are
already inside `#if USE_AVX … #endif` regions in commit a7a6c29 (the
Mul Sat_Trunc fast path at 5730-5765, the Mul Sat_Round fast path at
5851-5885, and the Histogram_DATA_U8 merge at 7681-7698 with its
scalar #else fallback). The bot's line-tracking appears to be
re-flagging the same blocks after each commit's line shifts. Replying
to them on the PR as outdated.

Validation:
- Default (USE_AVX=1, USE_BMI2=1) build of libopenvx.dylib + libvxu.dylib
  clean.
- USE_AVX=0 smoke build (`-DCMAKE_CXX_FLAGS=-DUSE_AVX=0`) still clean.
- Targeted CTS subset on the rebuilt libs (Canny.*:cannyEdgeDetector.*:
  Histogram.*:Multiply.*:vxMultiply.*:vxuMultiply.*) — 478 required
  vision tests pass, 0 fail. Confirms the tightened hardware gate
  does not regress conformance on a CPU that does support the binary's
  required ISA (host machine has AVX2+BMI2).

Co-authored-by: Cursor <cursoragent@cursor.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Comment thread amd_openvx/openvx/ago/ago_haf_cpu_filter.cpp Outdated
Comment thread amd_openvx/openvx/ago/ago_haf_cpu_arithmetic.cpp Outdated
…nd 5)

Two findings from Copilot's review of 22f9a02:

- ago_haf_cpu_filter.cpp::HafCpu_SobelMagnitude_S16_8: switch the int32
  conversion from `_mm256_cvttps_epi32` (truncate toward zero) back to
  `_mm256_cvtps_epi32` (round-to-nearest-even per the default MXCSR).
  The legacy double-precision pipeline this AVX2 helper replaced
  rounded the magnitude before packing to int16; truncate-toward-zero
  biased every non-integer sqrt result down by 1, which can produce
  systematic -1 differences vs the CTS reference right at the Canny
  hysteresis threshold boundary. Same operand otherwise — sumf and
  the sqrt are unchanged, only the i32 conversion mode flips.

- ago_haf_cpu_arithmetic.cpp: remove the unused
  `HafCpu_PackEightI32ToI16` helper that was declared inside the
  `#if USE_AVX` helper section but never referenced anywhere in the
  translation unit (grep across amd_openvx/openvx/ago confirms zero
  call sites). `static inline` mostly insulates clang/gcc from
  `-Wunused-function`, but the function is dead code regardless —
  cheaper to drop it than to maintain a forwarding promise.

Validation:
- Default (USE_AVX=1, USE_BMI2=1) build of libopenvx.dylib + libvxu.dylib
  clean.
- USE_AVX=0 smoke build (`-DCMAKE_CXX_FLAGS=-DUSE_AVX=0`) still clean.
- CTS subset matching `*Canny*:*Sobel*:*Phase*:*Magnitude*` (every test
  that exercises the rounding fix) — 74 required tests pass, 0 fail.
  Includes the Canny pyramid tests that were sensitive to the -1
  bias at the hysteresis threshold.

Co-authored-by: Cursor <cursoragent@cursor.com>
@kiritigowda
kiritigowda merged commit 205261f into ROCm:develop May 21, 2026
21 checks passed
@kiritigowda
kiritigowda deleted the kg/openvx-optimize branch May 21, 2026 20:22
kiritigowda added a commit that referenced this pull request May 28, 2026
* AMD OpenVX - Round-2 CPU kernel optimizations for OpenCV parity

Follow-up to #1663 targeting the kernels still below 1x OpenCV parity in
the openvx-mark FHD benchmark on the conformance.yml CI matrix:

  MeanStdDev    0.40x  -> defer i32->i64 widening of squared-sums
  MinMax        0.43x  -> 4x unroll with independent accumulator chains
  MinMaxLoc     0.43x  -> AVX2 + popcount, eliminates per-bit iteration
  Phase         0.68x  -> rcp_ps + 1 NR step instead of div_ps
  CustomConv    0.70x  -> AVX2 3xN convolve (mullo/mulhi_epi16 vs mullo_epi32)

amd_openvx/openvx/ago/ago_haf_cpu_arithmetic.cpp
  - HafCpu_MeanStdDev_DATA_U8: split the existing _AVX2 accumulator helper
    into per-iter (i32 partials via madd_epi16) and periodic-widen
    (i32->i64 every 1024 iters) phases. Breaks the long sumSquared
    dependency chain that limited throughput on Zen.
  - HafCpu_MinMax_DATA_U8: unroll to 128-byte chunks with 4 independent
    min/max accumulator chains; in-lane reduction at the end (cuts the
    serial dep chain of consecutive max/min_epu8 ops).
  - HafCpu_MinMaxLoc_DATA_U8DATA_Loc_None_Count_{Min,Max,MinMax}: replace
    SSE 16-byte loop + per-bit movemask iteration with AVX2 32-byte loop
    + HafCpu_PopCount32 on each movemask. Also fixes a pre-existing
    copy-paste bug in the Count_Max prefix/postfix scalar tails that
    compared against globalMin instead of globalMax (only fires on
    non-16-aligned widths, which is why CTS images masked it).
  - HafCpu_Phase_U8_S16S16 (both 16-wide and 8-wide paths): replace
    _mm256_div_ps with rcp_ps + 1 Newton-Raphson step (~24-bit
    precision). Output is u8 with +/-1 OpenVX tolerance, so the residual
    error is well within spec.

amd_openvx/openvx/ago/ago_haf_cpu_filter.cpp
  - HafCpu_Convolve_U8_U8_3xN: new AVX2 fast path processing 32 dst
    pixels per iter. Uses mullo_epi16/mulhi_epi16 (1c throughput on
    Zen) instead of the SSE path's mullo_epi32 (3c throughput), then
    in-lane unpack + packs_epi32 + packus_epi16 + a single
    permute4x64_epi64 fix-up. Fixes the alignedWidth-was-being-mutated
    bug that broke conformance during initial development.
  - HafCpu_FastAtan2_PhaseByte_8 (Canny/Sobel inner helper): same
    div_ps -> rcp_ps + 1 NR step replacement as the Phase kernel.

Conformance
  Full OpenVX-cts 1.3 run against the rebuilt libopenvx (HOST backend):
    5820 tests run, 5817 pass, 3 fail.
  The 3 failures are the same pre-existing macOS-only SmokeTestBase
  dynamic-loading failures present on the parent commit (#1663) before
  any of these edits.

  Targeted kernel filter
  (*MinMaxLoc*:*MinMax*:*MeanStdDev*:*Phase*:*Magnitude*:*Convolve*:*CustomConvolution*):
    1025/1025 pass, 0 fail.

Co-authored-by: Cursor <cursoragent@cursor.com>

* AMD OpenVX - AVX2 path for ColorConvert RGB->IYUV (was 0.58x of OpenCV)

HafCpu_ColorConvert_IYUV_RGB (RGB -> IYUV, despite the misleading
function name) was the largest remaining sub-1x kernel after #1663
that's still simple enough to tackle without a full rewrite. The
existing SSE path processed only 4 input pixels per outer iter and
serialized R/G/B extraction through a long `_mm_srli_si128(row, 1)`
dependency chain (one byte shifted off at a time across 3 channels x
2 rows).

amd_openvx/openvx/ago/ago_haf_cpu_color_convert.cpp
  - New AVX2 fast path on top of the existing SSE path (behind
    `#if USE_AVX`): processes 8 source pixels per row x 2 rows = 16
    input pixels per outer iter, emitting 16 Y, 4 U, 4 V output bytes.
  - Source layout per row: two 16-byte loads (offset 0 and offset
    +12) inserted into a __m256i. Three independent `_mm256_shuffle_epi8`
    masks then extract R, G, B as 8-lane i32 vectors with no
    inter-channel dependency.
  - BT.709 coefficients computed in 256-bit FP (`_mm256_mul_ps` +
    `_mm256_add_ps`). The 6 outputs (Y, U, V across 2 rows) form 6
    independent dependency chains, so the AVX2 path can keep both FP
    issue ports busy.
  - Chroma subsample uses the exact same step sequence as the SSE
    path (`avg_epu16` vertical, then `hadd_epi16` + `(x+1)>>1`
    horizontal), so output bytes are bit-identical to the SSE iter for
    every aligned 8-pixel column.
  - Width handling: AVX2 chunk handles `dstWidth & ~7`, falls through
    to the existing SSE 4-pixel iter for the remainder (`& ~3`), then
    the original scalar 2-pixel tail. All three paths reach the same
    output for every column.
  - Memory safety: per row we read bytes 0..27 (+12 load reaches byte
    27 of the row). OpenVX image pitch always rounds up to >= 16 bytes
    and the loop bound is `dstWidth & ~7`, so the +12 load stays
    within the allocated row pitch for any dstWidth >= 8.

Conformance (OpenVX-cts 1.3, HOST backend)
  Full suite: 5820 tests run, 5817 pass, 3 fail.
  The 3 failures are the same pre-existing macOS-only SmokeTestBase
  dynamic-loading failures present on every commit on this branch
  (and on develop/205261f8) before any of these edits.

  Targeted filter (*ColorConvert*:*ChannelExtract*:*ChannelCombine*):
    124 / 124 pass, 0 fail.

  Bit-identical-output verification on aligned 8-pixel columns relies
  on matching the SSE chroma-subsample step sequence exactly; the
  postfix paths exercise the existing SSE+scalar tails unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>

* AMD OpenVX - Revert Phase rcp_ps fast path (perf regression in CI)

The rcp_ps + 1 Newton-Raphson replacement for _mm256_div_ps that I
introduced earlier in this PR turned out to be a net slowdown on the
ROCm CI runner, not a speedup. The Release-OpenVX-vs-OpenCV bench
on commit 5f1a06d shows Phase at 0.5680x vs 0.68x on develop -
a clear regression.

Likely cause: on the runner's microarchitecture the critical
dependency chain through `rcp(d) -> d*rcp -> (2-x) -> rcp*new -> mn*rcp`
(5 dependent FP ops) is longer than the single hardware div_ps it
replaced, and the surrounding loop is latency-bound rather than
div-throughput-bound. The rcp form pays off when div is the
throughput bottleneck across many independent iterations, which is
not the shape of this kernel's hot loop.

Reverting both call sites:
  * HafCpu_Phase_U8_S16S16 (16-wide and 8-wide AVX2 paths in
    ago_haf_cpu_arithmetic.cpp)
  * HafCpu_FastAtan2_PhaseByte_8 (Canny/Sobel inner helper in
    ago_haf_cpu_filter.cpp)

restores them to the pre-PR `_mm256_div_ps(mn, mx + eps)` form.

Conformance (HOST backend, *Phase*:*Magnitude*:*Canny*:*Sobel* filter):
  74 / 74 tests pass.

The other 5 round-2 optimizations in this PR are unaffected by this
revert and confirmed wins per the CI benchmark on 5f1a06d:
  MeanStdDev        0.40x -> 0.82x  ( +106% )
  CustomConvolution 0.70x -> 1.16x  (  +66% )
  ColorConvert_RGB2IYUV 0.58x -> 1.19x ( +105% )

MinMaxLoc moved only 0.43x -> 0.46x because cv::minMaxLoc asks for
locations, so the OpenVX path actually exercises
HafCpu_MinMaxLoc_..._Loc_MinMax_Count_MinMax (which was already
AVX2+popcount-optimized) rather than the Loc_None_Count_* variants I
touched. That kernel's gap vs OpenCV is structural - the graph
decomposes MinMaxLoc into a separate MinMax pass + a MinMaxLoc pass,
so the data is scanned twice end-to-end. Fixing that requires a
graph-level fused kernel and is a separate PR.

Co-authored-by: Cursor <cursoragent@cursor.com>

* AMD OpenVX - Second-pass U8 statistics optimizations

Improve the U8 statistics hot paths with low-risk changes that preserve exact results while exposing more CPU parallelism.

MeanStdDev now uses four independent AVX2 accumulation chains over 128-byte blocks and flushes the squared partials less often. This keeps the same integer accumulation semantics while reducing dependency pressure and widening instruction-level parallelism.

MinMax_DATA_U8 now exits early once the full U8 extrema range has been proven. This is exact for U8 images and helps MinMaxLoc workloads because the current graph still performs a separate min/max pre-pass before scanning again for locations.

Verification: cmake --build build --target openvx
  openvx-mark --kernel MeanStdDev,MinMaxLoc --resolution FHD --iterations 30 --warmup 5
  openvx-mark --category statistical --resolution FHD --iterations 20 --warmup 5
Co-authored-by: Cursor <cursoragent@cursor.com>

* AMD OpenVX - Address PR review comments on CPU kernel optimizations

Three targeted fixes for issues raised in the Copilot review of PR 1664:

1) HafCpu_Convolve_U8_U8_3xN: align SSE shift semantics with the AVX2 and scalar paths.

   The new AVX2 path uses _mm256_srai_epi32 (arithmetic right shift) so that negative intermediates clamp to 0 in the subsequent unsigned packus pack to U8, matching the scalar prefix/postfix clamp(0,255) behavior. The pre-existing SSE remainder loop was using _mm_srli_epi32 (logical right shift), which silently turns negative intermediates into large positives that saturate to 255 instead of 0. Switched the four SSE shifts to _mm_srai_epi32 so all three paths produce the same output for every conv coefficient sign, including the case where the AVX2 path handles most of the row and the SSE path handles the 16-pixel remainder.

2) HafCpu_ColorConvert_IYUV_RGB AVX2 path: tighten the high-half load offset to avoid reading past the end of the 24-byte RGB working set.

   The previous code did _mm_loadu_si128(pLocalSrc + 12), which reaches bytes 12..27 even though an 8-pixel RGB iteration only consumes bytes 0..23. On the last 2-row block of an image whose source pitch is exactly 3*dstWidth (no trailing padding) this peeks 4 bytes past the row end and, on the bottom row, past the image buffer. Changed the high-half load to (pLocalSrc + 8), so the read covers bytes 8..23 only -- strictly inside the 24-byte RGB span. Lane 1's shuffle masks now extract pixels 4..7 at the byte offsets {4,7,10,13} / {5,8,11,14} / {6,9,12,15}, preserving bit-identical output vs the SSE path.

3) HafCpu_MeanStdDev_DATA_U8: correct the overflow-safety comment.

   The previous comment claimed each iter contributes up to 8*(4*255^2) = 2,080,800 to a single i32 lane. The correct per-lane bound is 2*(2*255^2) = 4*65,025 = 260,100, because each madd_epi16 result already holds the pair-sum of two squared u8 values. INT32_MAX / 260,100 is about 8,255, so flushInterval = 8192 is the largest power-of-two cadence that stays under INT32_MAX for the worst-case path (the 32-byte remainder loop drives only chain 0 with ++=1 per iter). Comment was updated to reflect this and to spell out the per-chain budget for the 128-byte main loop separately.

Verification (local, FHD, openvx-mark v1.1.0):
  ./build/openvx-mark-bench/build/openvx-mark --kernel MeanStdDev,MinMaxLoc,ColorConvert_RGB2IYUV,CustomConvolution,Phase --resolution FHD --iterations 30 --warmup 5
  -> 5 / 5 verified (output diff vs OpenCV reference within tolerance)

  ./build/openvx-mark-bench/build/openvx-mark --category filters,color,statistical --resolution FHD --iterations 10
  -> 26 / 26 verified, 0 failed

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Bug fixes to existing features enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants