Skip to content

samples: add OpenVX graph pipelining CPU+GPU sample - #7

Open
simonCatBot wants to merge 18 commits into
developfrom
feature/pipelining-sample
Open

samples: add OpenVX graph pipelining CPU+GPU sample#7
simonCatBot wants to merge 18 commits into
developfrom
feature/pipelining-sample

Conversation

@simonCatBot

Copy link
Copy Markdown
Owner

Summary

Adds a new C++ sample under samples/c_samples/pipelining that demonstrates the OpenVX vx_khr_pipelining extension on a mixed CPU+GPU workload.

The sample runs the same vision graph in two modes:

  • --pipeline 0 — synchronous vxProcessGraph loop (baseline).
  • --pipeline 1QUEUE_AUTO pipelined enqueue/dequeue with multiple input/output buffers.

Graph: RGB -> ColorConvert -> ChannelExtract(Y) -> Box3x3 -> U8

Both paths compute an identical per-frame checksum aggregate, and the pipelined path reports a higher fps so the benefit is immediately visible.

What was added

  • samples/c_samples/pipelining/pipelining.cpp — the sample application.
  • samples/c_samples/pipelining/CMakeLists.txt — build file matching the existing canny sample.
  • Updated samples/c_samples/README.md with build/run instructions.

Verification performed

Built and tested locally on a Radeon gfx1151 GPU and on the CPU-only backend:

Mode Backend fps (60 frames @ 640x480) checksum aggregate
sync HIP ~447 2309715656
pipe HIP ~503 2309715656
sync CPU ~352 2304694854
pipe CPU ~531 2304694854

Both backends pass the checksum-equality test and show measurable speed-up when pipelining is enabled.

Checklist

  • Builds on HIP backend.
  • Builds on CPU-only backend.
  • --pipeline 0 and --pipeline 1 produce the same aggregate checksum.
  • --pipeline 1 reports higher fps than --pipeline 0.
  • Does not touch any existing library code; only adds a new sample.

@simonCatBot
simonCatBot force-pushed the feature/pipelining-sample branch from 8b51ead to ab8c164 Compare August 6, 2026 17:38
@daniellegillai

Copy link
Copy Markdown

Tested on Radeon RX 9070 XT / Ryzen 9 9950X 16-Core Processor, using TheRock Nightly 10.1.0 2026-08-05:

Mode Backend fps (60 frames @ 640x480) checksum aggregate
sync HIP ~422 2309715656
pipe HIP ~456 2309715656
sync CPU ~450 2304694854
pipe CPU ~498 2304694854

Both backends pass the checksum-equality test and show speed-up when pipelining is enabled. One note is that CPU outperformed HIP backend, @kiritigowda is that reasonable?

@simonCatBot
simonCatBot force-pushed the feature/pipelining-sample branch 3 times, most recently from 4b11c3c to 00322f5 Compare August 6, 2026 22:06
Add samples/c_samples/pipelining, a self-contained demonstration of the
vx_khr_pipelining extension on a mixed CPU+GPU workload. The same vision
graph (RGB -> ColorConvert -> ChannelExtract(Y) -> Box3x3 -> U8) is run
synchronously with vxProcessGraph (--pipeline 0) and asynchronously with
QUEUE_AUTO enqueue/dequeue (--pipeline 1).

Both paths report identical per-frame checksums so correctness can be
verified, while the pipelined path prints a higher fps to make the
performance benefit visible.

Also update samples/c_samples/README.md with build/run instructions.
@simonCatBot
simonCatBot force-pushed the feature/pipelining-sample branch from 00322f5 to 7f75313 Compare August 6, 2026 23:30
simonCatBot and others added 17 commits August 7, 2026 16:02
… to fix non-deterministic HIP pipe checksums

The heavy preset previously chained two GPU Box3x3 nodes through a shared
virtual intermediate. In QUEUE_AUTO pipelined mode on the HIP backend this
produced slightly different aggregate checksums on every run, while sync and
the CPU backend were stable.

Replace the heavy graph with a single Gaussian3x3 node. It keeps the preset
compute-heavy enough to show pipelining speed-up, and the sync and pipelined
paths now produce identical, deterministic checksums on both HIP and CPU
backends. The README is updated to describe the new heavy graph and to remove
the caveat about sync/pipe checksum differences.
… preset change

- Update pipelining --help to describe the heavy preset as Gaussian3x3
  instead of the old Box3x3 -> Box3x3 chain.
- Remove the misleading compare-mode message that claimed heavy sync/pipe
  checksum differences were expected.
- Fix pipelining_hybrid header comment: the CPU stage is Threshold (U8)
  -> Box3x3, not a U1 conversion.
- README: mention both Box3x3 and Gaussian3x3 as the heavier convolutions.
These GDF files demonstrate the proposed GDF syntax for vx_khr_pipelining
support in runvx. They are not executable today because runvx does not yet
implement QUEUE_AUTO, graph parameters, or enqueue/dequeue primitives.

Files:
- utilities/runvx/gdf/pipelining/README.md
- utilities/runvx/gdf/pipelining/pipelining_box3x3.gdf
- utilities/runvx/gdf/pipelining/pipelining_gaussian3x3.gdf
- utilities/runvx/gdf/pipelining/pipelining_hybrid.gdf

See ROCm#1734 for the enhancement request.
…EUE_MANUAL over-scheduling in graph pipelining (ROCm#1729)

* Fix cross-target hand-off, queued OpenCL args, and QUEUE_MANUAL over-scheduling

Three defects kept a pipelined graph from being usable once its work was
split across the CPU and the GPU.

A CPU node only recorded that it had written a buffer when that buffer
already had device memory. A graph that hands its output to another graph
usually writes an image no GPU node has bound yet, so the record was
skipped, and the device memory reserved later by the consuming graph was
read without ever being uploaded. The CPU node now records the write
regardless, and a device buffer reserved for a queued reference starts out
needing an upload.

On OpenCL, buffer kernel arguments are bound when the graph is verified.
That holds for an object that stays put, and the code already re-bound
delay slots, but a queued graph parameter names a different reference on
every execution. Every execution after the first therefore wrote the
reference chosen at verification time. Queued references are now re-bound
the same way delay slots are.

In QUEUE_MANUAL, vxScheduleGraph runs every complete set of enqueued
references it finds rather than a single one, so a request could carry out
work that later requests were made for; those requests then found empty
queues and reported an error for work that had in fact been done. The
extra executions are credited and spent by the requests that follow, which
keeps the error for the case it was written for - an application that
scheduled the graph without enqueueing a reference everywhere.

Two tests cover the parts that were silently wrong: a two-graph GPU-after-
CPU chain whose result is only exact if the hand-off uploads, and a
QUEUE_MANUAL run that checks each enqueued set runs once, with correct
data and no failure logged.

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

* Make the QUEUE_MANUAL execution accounting explicit

Review pointed out that crediting the executions a request performs beyond
the one it asked for leaves credits behind that nothing is waiting for, so
a later request with empty queues reports success instead of the missing
references.

A request cannot be limited to a single execution instead. The conformance
suite's GraphPipeline.testManualSchedule enqueues one set per buffer,
schedules the graph once, and requires every one of those sets to have run
by the time it dequeues; one execution per request fails 7 of the 109
pipelining tests.

With the number of executions decoupled from the number of requests that
way, an application that enqueues N sets and schedules once and then N-1
times, and one that enqueues and schedules N times where a request ran
several sets, make exactly the same calls in the same order. Nothing in the
queues separates them, so what separates a request whose work an earlier
one already did from a request the application enqueued nothing for is
whether an execution is there to claim. The counter now says that directly:
every request claims one execution, the executions no request has claimed
are carried, and reconfiguring the graph starts the count again.

Test 27 now covers both directions, a request per enqueued set and one
request for every enqueued set, and checks that a request with nothing
enqueued for it is still reported as an error.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
* HIP: add RGB/RGBX Remap nearest and bilinear kernels

Add GPU Remap support for packed RGB and RGBX images in the HIP backend.
Previously Remap only had a GPU implementation for VX_DF_IMAGE_U8.
This change adds:
- VX_KERNEL_AMD_REMAP_U24_U24_NEAREST
- VX_KERNEL_AMD_REMAP_U32_U32_NEAREST

Implementation details:
- Added enum IDs, kernel list entries, and API declarations.
- Implemented per-channel nearest and bilinear interpolation HIP kernels
  for 3-byte RGB and 4-byte RGBX in hipvx/geometric_kernels.cpp.
- Wired validate and hip_execute in ago_kernel_api.cpp for the existing
  Remap_U24_U24_Bilinear and Remap_U32_U32_Bilinear kernels plus the new
  nearest variants.
- Updated agoDramaDivideRemapNode to dispatch the new kernels for RGB/RGBX
  with undefined borders and nearest or bilinear interpolation.
- Added GDF test cases under tests/amd_openvx_gdfs/geometric/.

Tested on gfx1151 (Radeon 8060S) with runvx GPU affinity.

* HIP Remap U8: use round-half-up packing to match CPU/OpenCV

The CPU SSE path computes bilinear interpolation with fixed-point weights
and rounds with `+32 >> 6` (round-half-up).  The HIP U8 remap kernel used
`__builtin_amdgcn_cvt_pk_u8_f32`, which rounds to nearest-even, so .5 ties
(e.g. 62.5 -> 62) could differ from CPU by one.

Add a `hip_pack_half_up()` helper that converts each float with
`(uint)(x + 0.5f)` and clamps to [0,255], then wire it into the U8 bilinear
remap kernels.  RGB/RGBX bilinear kernels already used per-channel
`(uchar)(x + 0.5f)` and are unaffected.

Validation on 16x16 probes and the OpenVX 1280x720 demo mirror map shows
CPU/HIP match exactly for U8, RGB, and RGBX.

* HIP Remap RGB: fix output store to write all 24 bytes

The RGB bilinear kernel packed 8 pixels into two uint3 registers (24 bytes)
but then wrote only 20 bytes: out[0].x/out[0].y (8 bytes), out[1].x/out[1].y
(8 bytes), and out[1].z (4 bytes).  This dropped out[0].z (bytes 8-11),
which contains the blue channel of pixel 2 and all of pixel 3, causing
systematic corruption visible on the demo map.

Write all six uints out[0].x/y/z and out[1].x/y/z to the destination so the
24 RGB bytes are preserved exactly.  After the fix the HIP GPU output matches
the CPU reference on U8, RGB, and RGBX bilinear remap.

* OpenVX CPU: add scalar, SSE, and AVX2 RGB/RGBX Remap fallbacks

Add CPU implementations for the new RGB/RGBX Remap kernels so the PR
provides both CPU and HIP coverage for U8/RGB/RGBX remap:

- HafCpu_Remap_U24_U24_Bilinear / Nearest
- HafCpu_Remap_U32_U32_Bilinear / Nearest

Includes SSE and AVX2 fast paths plus a scalar tail, source-coordinate
clamping to keep the 2x2 neighborhood in-bounds, and wiring in
ago_kernel_api.cpp so CPU affinity dispatches correctly.

Validation: all Remap_* GDFs pass on both CPU and GPU affinity for both
the CPU-only and HIP builds; CPU and HIP outputs match exactly on the
1280x720 demo map.

* Remap RGB/RGBX: add constant-border CPU and HIP kernels

Extend the PR so RGB and RGBX remap support VX_BORDER_MODE_CONSTANT, not
just VX_BORDER_MODE_UNDEFINED.  This brings them in line with the existing
U8 remap kernels and lets them match OpenCV-style BORDER_CONSTANT behavior.

Changes:
- Added HIP kernels: Hip_Remap_RGB*_Bilinear_Constant / Nearest_Constant,
  plus constant-border bilinear sampling helpers.
- Added CPU scalar fallbacks: HafCpu_Remap_U24/U32 *_Constant.
- Registered new kernel IDs, list entries, and API declarations.
- Wired CPU and HIP execute paths and drama divide dispatch.
- Added GDF tests: Remap_RGB*_Bilinear_Constant and *_Nearest_Constant.

Validation:
- All 12 remap GDFs pass on CPU-only and HIP builds (CPU/GPU affinity).
- CPU and HIP outputs match on the 1280x966 dewarp demo for U8 and RGB
  constant-border bilinear.

* Remap RGB/RGBX: fix OOB stores, misaligned loads, and clamp; address review

- hip_pack_half_up: clamp in the float domain before the uint cast, so a
  negative round-off value pins to 0 instead of underflowing to 255
- RGB/RGBX Remap kernels stored a full 8-pixel block unconditionally, writing
  past the row end and overflowing the row stride when dstWidth is not a
  multiple of 8; store partial tail blocks pixel-wise
- RGBX bilinear read source pixels via 16-byte uint4 loads off 4-byte-aligned
  addresses (misaligned/UB); sample byte-wise through hip_bilinear_sample_RGBX
- RGB bilinear dropped two unused uint2 loads that were also misaligned
- GDF tests: RGB (RGB2) uniform-image fills use 24-bit values per repo
  convention, not 32-bit
- CHANGELOG: add the RGB/RGBX Remap GPU support entry

Co-Authored-By: Claude <noreply@anthropic.com>

* Remap RGB/RGBX CPU: fix boundary over-reads in SSE/AVX bilinear paths

The SIMD bilinear paths clamped the sampled coordinates but not the width of
the loads, so near the allocation edge they read past the buffer:

- RGB 4-pixel block packed each 3-byte pixel with a 16-byte loadu; at the
  bottom-right pixel this read up to 15 bytes past the image. Pack exactly
  three bytes via AgoLoadRGB24.
- RGB 2-pixel tail read pixels through vx_uint32*, taking a 4th byte off the
  final 3-byte pixel; same three-byte packing applied.
- RGB and RGBX 2-pixel tails decoded the remap through AgoRemapComputeOffsets_SSE,
  which loads four entries (and pMapY_X+1 a further four); with fewer than four
  entries left in a row this over-read the remap table. Decode one entry at a
  time via AgoRemapComputeOffset1.

Behaviour is unchanged for in-bounds pixels; only the out-of-allocation reads
are removed. Verified with a clean HIP build of libopenvx.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Kiriti Gowda <kiriti.gowda@gmail.com>
Co-authored-by: Kiriti Gowda <kiritigowda@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
* CI - Use rocm 10.1

* CI - update comments
…Cm#1750)

The software emulation of amd_bytealign (used on platforms that do not
support cl_amd_media_ops, e.g. non-AMD OpenCL devices) masked the byte
offset argument with & 31 instead of & 3. The hardware amd_bytealign
uses only the low 2 bits of the third argument to select a byte offset
(0-3) into the concatenated {src0:src1} pair; masking with & 31 allows
shifts of up to 31 bytes, reading the wrong bytes whenever the offset is
>= 4.

This produced incorrect results in kernels that call amd_bytealign with
a full (non-masked) byte index -- e.g. the ORB Gaussian scale kernel used
by vxGaussianPyramidNode -- when running on a non-AMD device. The native
hardware path is unaffected because the emulation is only injected when
cl_amd_media_ops is unavailable.

amd_bitalign (bit funnel shift, valid range 0-31 bits) correctly keeps
its & 31 mask.
…m#1752)

Bumps [rocm-docs-core](https://github.com/ROCm/rocm-docs-core) from 1.39.0 to 1.40.0.
- [Release notes](https://github.com/ROCm/rocm-docs-core/releases)
- [Changelog](https://github.com/ROCm/rocm-docs-core/blob/develop/CHANGELOG.md)
- [Commits](ROCm/rocm-docs-core@v1.39.0...v1.40.0)

---
updated-dependencies:
- dependency-name: rocm-docs-core
  dependency-version: 1.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
The OpenCL codegen for the ORB Gaussian scale kernel (used by
vxGaussianPyramidNode with VX_SCALE_PYRAMID_ORB) performed the horizontal
filter in place: each work-item read neighbouring image pixels from the
local buffer (lbuf_ptr[ilx & ~3]) and, in the same pass with no barrier,
wrote its packed 16-bit horizontal sums back to the same local buffer
(((uint2*)lbuf_ptr)[lx]).

Because the read footprint of one work-item overlaps the write footprint of
its neighbours (e.g. lx=2 reads local bytes [8..19] while lx=1 writes
[8..15]), a work-item could read sums another work-item had already written
instead of the original pixels. On a lock-step SIMD device the reads happen
to complete before the writes, so the result was correct; on a scalar
device where work-items are not executed in lock-step (e.g. a CPU OpenCL
driver such as PoCL) the writes raced ahead and corrupted the result -- the
right-hand output columns were wrong.

Fix: compute both the main-row and the +16-row horizontal sums into private
registers (isum, isum2), then barrier, then store. All reads of the shared
local buffer now complete before any work-item writes to it. This also
removes the previous reuse of lbuf_ptr across the two stores.

No behavioural change on AMD hardware (verified: GaussianPyramid ORB still
12/12 on gfx1151). Fixes the 8 GaussianPyramid ORB VX_BORDER_UNDEFINED
OpenVX-CTS cases that failed on PoCL; full vision conformance now passes
5824/5824 on a PoCL CPU device.
…ROCm#1747)

Follow-up to ROCm#1723. Six of seven memcpy(x, desc, s - desc) sites in the
GDF data-descriptor parser were bounds-checked; this closes the gaps in
the same class that were left unfixed:

- ago_util.cpp agoGetDataFromDescription (threshold:): add the missing
  `(size_t)(s - desc) >= sizeof(thresh_type)` guard before the memcpy,
  matching the six sibling sites.
- ago_interface.cpp agoUpdateLine: was expanding $VAR / $! values into the
  caller's fixed char line[2048] via unbounded strcpy; add a line_size
  capacity parameter and bound both expansions (callers pass sizeof(line)),
  mirroring the agoUpdateN hardening.
- ago_interface.cpp def-macro: strncpy(macro_name, arg[1], sizeof) left the
  256-byte buffer unterminated; copy sizeof-1 and NUL-terminate.
- ago_util.cpp agoGetDataName: char tmp[512] strcpy of a name written up to
  MAX_MODULE_NAME_SIZE (1024); size tmp to MAX_MODULE_NAME_SIZE and bound
  the copy.

Verified: full build + ctest 100% (32/32) against ROCm 7.2, gfx1100.

Co-authored-by: Kiriti Gowda <kiritigowda@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants