Skip to content

perf(hip): pass AdmBufferHip by pointer in ADM kernels (F3 fix, ADR-0759) - #101

Merged
lusoris merged 1 commit into
masterfrom
perf/hip-adm-buffer-by-pointer-20260529
May 29, 2026
Merged

lusoris merged 1 commit into
masterfrom
perf/hip-adm-buffer-by-pointer-20260529

Conversation

@lusoris

@lusoris lusoris commented May 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Affected kernels

File Kernel Change
adm_csf.hip adm_csf_kernel_1_4 AdmBufferHip buf → pointer
adm_csf.hip i4_adm_csf_kernel_1_4 AdmBufferHip buf → pointer
adm_cm.hip i4_adm_cm_line_kernel AdmBufferHip buf → pointer
adm_cm.hip adm_cm_line_kernel_8 AdmBufferHip buf → pointer

Build verification

meson setup core/build-hip-verify core -Denable_hipcc=true -Denable_cuda=false -Denable_sycl=false --buildtype=release
ninja -C core/build-hip-verify

Result: 35 targets, 0 errors (host: /opt/rocm/bin/hipcc, ROCm on CachyOS).

Runtime verification status

PENDING — requires AMD GPU with ROCm runtime. Smoke command when available:

docker exec vmaf-dev-mcp vmaf \
  --feature integer_adm --backend hip \
  --reference python/test/resource/yuv/src01_hrc00_576x324.yuv \
  --distorted python/test/resource/yuv/src01_hrc01_576x324.yuv \
  --width 576 --height 324 --pixel_format 420 --bitdepth 8

Expected: ADR-0214 places=4 parity vs CPU. Change is numerically transparent (pointer indirection only; all kernel arithmetic and load addresses are identical).

Deliverables checklist

  • Research digest: docs/research/research-0759-hip-adm-buffer-by-pointer.md
  • Decision matrix: ADR-0759 ## Alternatives considered (in-kernel ldg, __constant__, host-side pointer (chosen), unmodified)
  • AGENTS.md invariant: core/src/feature/hip/AGENTS.md — AdmBufferHip pass-by-pointer mandatory; future struct args follow same rule
  • Reproducer: build + smoke commands above
  • Changelog: changelog.d/perf/hip-adm-buffer-by-pointer.md
  • Rebase notes: docs/rebase-notes.md — no rebase impact (all files are fork-added)
  • Per-surface docs: no user-discoverable surface changed (internal kernel refactor)
  • docs/state.md: T-HIP-ADM-BUFFER-BY-POINTER-20260529 row added
  • docs/adr/README.md: ADR-0759 row added

No auto-merge

DRAFT — blocked on AMD hardware runtime verification before ready.

🤖 Generated with Claude Code

@lusoris
lusoris marked this pull request as ready for review May 29, 2026 09:37
…759)

Research-0755 / PR #95 HIP audit identified AdmBufferHip (~272 bytes) being
passed by value in four __global__ kernel signatures:
  - adm_csf_kernel_1_4, i4_adm_csf_kernel_1_4 (adm_csf.hip)
  - i4_adm_cm_line_kernel, adm_cm_line_kernel_8 (adm_cm.hip)

This PR replaces `AdmBufferHip buf` with `const AdmBufferHip * __restrict__ buf_ptr`
in all four kernels. A device-side copy of the struct is allocated once in
init_fex_hip via hipMalloc + hipMemcpy(hipMemcpyHostToDevice) and stored as
AdmStateHip::buf_dev. Host dispatch helpers pass &buf_dev in args[].

The struct contains only stable device pointers set at init time; no per-frame
update is required. Change is numerically transparent (pointer indirection only;
all load addresses are identical). Matches CUDA F3 fix pattern from PR #93.

Build verified: meson -Denable_hipcc=true -Denable_cuda=false succeeds (35 targets,
zero errors). Runtime verification pending — no AMD GPU on audit host.

Deliverables: ADR-0759, Research-0759, AGENTS.md invariant, changelog fragment,
rebase-notes entry, state.md row.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@lusoris
lusoris force-pushed the perf/hip-adm-buffer-by-pointer-20260529 branch from e25cff3 to 7caac96 Compare May 29, 2026 09:39
@lusoris
lusoris merged commit 31a51af into master May 29, 2026
@lusoris
lusoris deleted the perf/hip-adm-buffer-by-pointer-20260529 branch May 29, 2026 09:39
@lusoris lusoris added this to the 1.0.0 — First release milestone Sep 4, 2026
lusoris added a commit that referenced this pull request Sep 20, 2026
ADR-0759 moved the four HIP integer ADM kernels that read AdmBufferHip
(adm_csf_kernel_1_4, i4_adm_csf_kernel_1_4, i4_adm_cm_line_kernel,
adm_cm_line_kernel_8) from the struct by value to a pointer to a device copy,
in 31a51af (#101). The next merge, 92ea978 (#102, a CUDA ciede change cut
from an older base), put the by-value signatures back, and every launch since
has copied the 328-byte struct into the kernel arguments. This re-implements
the decision on today's structure instead of reverting the revert.

The design needs the struct to be the same on every launch. It is: s->buf is
written only by adm_hip_alloc_buffers(), adm_hip_slice_bands() and
adm_hip_slice_results() during init and by adm_hip_free_buffers() on
teardown. submit() passes &s->buf to every launch helper, and no launch
passes a modified copy; the scale 1-3 CSF and CM kernels get the scale as a
separate argument and use the same i4 band pointers at every scale.

adm_hip_upload_buf() allocates and uploads the copy at the end of
adm_hip_init_device(), after the band and result slices are final, and the
four launches pass &s->buf_dev. The copy is freed in close() and on both init
failure paths.

HIP output is byte-identical at %.17g before and after, with debug features,
on 8, 10, 12 and 16-bit input, odd 575x323 and 197x101 frames, bright
16-bit noise, the 1080p 8-bit checkerboard pairs and a 60-frame 1080p
10-bit clip. The kernel argument segment of each kernel shrinks by 320
bytes. Per-thread scratch and VGPRs do not change because of the pointer:
the 936 bytes on adm_cm_line_kernel_8 are VGPR spills (239 at the
128-register cap), not a copy of the struct. End-to-end 1080p 10-bit
throughput is unchanged within run-to-run noise.

adm_csf.hip had not been lint-cleaned yet, so this also takes it from 35
clang-tidy findings to 0: the device helpers move into an anonymous
namespace, locals become const, and the band select becomes a helper. That
restructure, not the pointer, lowers the two CSF kernels from 44 to 38 and
53 to 45 VGPRs. HIP tidy baseline tightened.
lusoris added a commit that referenced this pull request Sep 20, 2026
ADR-0759 moved the four HIP integer ADM kernels that read AdmBufferHip
(adm_csf_kernel_1_4, i4_adm_csf_kernel_1_4, i4_adm_cm_line_kernel,
adm_cm_line_kernel_8) from the struct by value to a pointer to a device copy,
in 31a51af (#101). The next merge, 92ea978 (#102, a CUDA ciede change cut
from an older base), put the by-value signatures back, and every launch since
has copied the 328-byte struct into the kernel arguments. This re-implements
the decision on today's structure instead of reverting the revert.

The design needs the struct to be the same on every launch. It is: s->buf is
written only by adm_hip_alloc_buffers(), adm_hip_slice_bands() and
adm_hip_slice_results() during init and by adm_hip_free_buffers() on
teardown. submit() passes &s->buf to every launch helper, and no launch
passes a modified copy; the scale 1-3 CSF and CM kernels get the scale as a
separate argument and use the same i4 band pointers at every scale.

adm_hip_upload_buf() allocates and uploads the copy at the end of
adm_hip_init_device(), after the band and result slices are final, and the
four launches pass &s->buf_dev. The copy is freed in close() and on both init
failure paths.

HIP output is byte-identical at %.17g before and after, with debug features,
on 8, 10, 12 and 16-bit input, odd 575x323 and 197x101 frames, bright
16-bit noise, the 1080p 8-bit checkerboard pairs and a 60-frame 1080p
10-bit clip. The kernel argument segment of each kernel shrinks by 320
bytes. Per-thread scratch and VGPRs do not change because of the pointer:
the 936 bytes on adm_cm_line_kernel_8 are VGPR spills (239 at the
128-register cap), not a copy of the struct. End-to-end 1080p 10-bit
throughput is unchanged within run-to-run noise.

adm_csf.hip had not been lint-cleaned yet, so this also takes it from 35
clang-tidy findings to 0: the device helpers move into an anonymous
namespace, locals become const, and the band select becomes a helper. That
restructure, not the pointer, lowers the two CSF kernels from 44 to 38 and
53 to 45 VGPRs. HIP tidy baseline tightened.
lusoris added a commit that referenced this pull request Sep 20, 2026
ADR-0759 moved the four HIP integer ADM kernels that read AdmBufferHip
(adm_csf_kernel_1_4, i4_adm_csf_kernel_1_4, i4_adm_cm_line_kernel,
adm_cm_line_kernel_8) from the struct by value to a pointer to a device copy,
in 31a51af (#101). The next merge, 92ea978 (#102, a CUDA ciede change cut
from an older base), put the by-value signatures back, and every launch since
has copied the 328-byte struct into the kernel arguments. This re-implements
the decision on today's structure instead of reverting the revert.

The design needs the struct to be the same on every launch. It is: s->buf is
written only by adm_hip_alloc_buffers(), adm_hip_slice_bands() and
adm_hip_slice_results() during init and by adm_hip_free_buffers() on
teardown. submit() passes &s->buf to every launch helper, and no launch
passes a modified copy; the scale 1-3 CSF and CM kernels get the scale as a
separate argument and use the same i4 band pointers at every scale.

adm_hip_upload_buf() allocates and uploads the copy at the end of
adm_hip_init_device(), after the band and result slices are final, and the
four launches pass &s->buf_dev. The copy is freed in close() and on both init
failure paths.

HIP output is byte-identical at %.17g before and after, with debug features,
on 8, 10, 12 and 16-bit input, odd 575x323 and 197x101 frames, bright
16-bit noise, the 1080p 8-bit checkerboard pairs and a 60-frame 1080p
10-bit clip. The kernel argument segment of each kernel shrinks by 320
bytes. Per-thread scratch and VGPRs do not change because of the pointer:
the 936 bytes on adm_cm_line_kernel_8 are VGPR spills (239 at the
128-register cap), not a copy of the struct. End-to-end 1080p 10-bit
throughput is unchanged within run-to-run noise.

adm_csf.hip had not been lint-cleaned yet, so this also takes it from 35
clang-tidy findings to 0: the device helpers move into an anonymous
namespace, locals become const, and the band select becomes a helper. That
restructure, not the pointer, lowers the two CSF kernels from 44 to 38 and
53 to 45 VGPRs. HIP tidy baseline tightened.
lusoris added a commit that referenced this pull request Sep 20, 2026
ADR-0759 moved the four HIP integer ADM kernels that read AdmBufferHip
(adm_csf_kernel_1_4, i4_adm_csf_kernel_1_4, i4_adm_cm_line_kernel,
adm_cm_line_kernel_8) from the struct by value to a pointer to a device copy,
in 31a51af (#101). The next merge, 92ea978 (#102, a CUDA ciede change cut
from an older base), put the by-value signatures back, and every launch since
has copied the 328-byte struct into the kernel arguments. This re-implements
the decision on today's structure instead of reverting the revert.

The design needs the struct to be the same on every launch. It is: s->buf is
written only by adm_hip_alloc_buffers(), adm_hip_slice_bands() and
adm_hip_slice_results() during init and by adm_hip_free_buffers() on
teardown. submit() passes &s->buf to every launch helper, and no launch
passes a modified copy; the scale 1-3 CSF and CM kernels get the scale as a
separate argument and use the same i4 band pointers at every scale.

adm_hip_upload_buf() allocates and uploads the copy at the end of
adm_hip_init_device(), after the band and result slices are final, and the
four launches pass &s->buf_dev. The copy is freed in close() and on both init
failure paths.

HIP output is byte-identical at %.17g before and after, with debug features,
on 8, 10, 12 and 16-bit input, odd 575x323 and 197x101 frames, bright
16-bit noise, the 1080p 8-bit checkerboard pairs and a 60-frame 1080p
10-bit clip. The kernel argument segment of each kernel shrinks by 320
bytes. Per-thread scratch and VGPRs do not change because of the pointer:
the 936 bytes on adm_cm_line_kernel_8 are VGPR spills (239 at the
128-register cap), not a copy of the struct. End-to-end 1080p 10-bit
throughput is unchanged within run-to-run noise.

adm_csf.hip had not been lint-cleaned yet, so this also takes it from 35
clang-tidy findings to 0: the device helpers move into an anonymous
namespace, locals become const, and the band select becomes a helper. That
restructure, not the pointer, lowers the two CSF kernels from 44 to 38 and
53 to 45 VGPRs. HIP tidy baseline tightened.
lusoris added a commit that referenced this pull request Sep 20, 2026
ADR-0759 moved the four HIP integer ADM kernels that read AdmBufferHip
(adm_csf_kernel_1_4, i4_adm_csf_kernel_1_4, i4_adm_cm_line_kernel,
adm_cm_line_kernel_8) from the struct by value to a pointer to a device copy,
in 31a51af (#101). The next merge, 92ea978 (#102, a CUDA ciede change cut
from an older base), put the by-value signatures back, and every launch since
has copied the 328-byte struct into the kernel arguments. This re-implements
the decision on today's structure instead of reverting the revert.

The design needs the struct to be the same on every launch. It is: s->buf is
written only by adm_hip_alloc_buffers(), adm_hip_slice_bands() and
adm_hip_slice_results() during init and by adm_hip_free_buffers() on
teardown. submit() passes &s->buf to every launch helper, and no launch
passes a modified copy; the scale 1-3 CSF and CM kernels get the scale as a
separate argument and use the same i4 band pointers at every scale.

adm_hip_upload_buf() allocates and uploads the copy at the end of
adm_hip_init_device(), after the band and result slices are final, and the
four launches pass &s->buf_dev. The copy is freed in close() and on both init
failure paths.

HIP output is byte-identical at %.17g before and after, with debug features,
on 8, 10, 12 and 16-bit input, odd 575x323 and 197x101 frames, bright
16-bit noise, the 1080p 8-bit checkerboard pairs and a 60-frame 1080p
10-bit clip. The kernel argument segment of each kernel shrinks by 320
bytes. Per-thread scratch and VGPRs do not change because of the pointer:
the 936 bytes on adm_cm_line_kernel_8 are VGPR spills (239 at the
128-register cap), not a copy of the struct. End-to-end 1080p 10-bit
throughput is unchanged within run-to-run noise.

adm_csf.hip had not been lint-cleaned yet, so this also takes it from 35
clang-tidy findings to 0: the device helpers move into an anonymous
namespace, locals become const, and the band select becomes a helper. That
restructure, not the pointer, lowers the two CSF kernels from 44 to 38 and
53 to 45 VGPRs. HIP tidy baseline tightened.
lusoris added a commit that referenced this pull request Sep 20, 2026
ADR-0759 moved the four HIP integer ADM kernels that read AdmBufferHip
(adm_csf_kernel_1_4, i4_adm_csf_kernel_1_4, i4_adm_cm_line_kernel,
adm_cm_line_kernel_8) from the struct by value to a pointer to a device copy,
in 31a51af (#101). The next merge, 92ea978 (#102, a CUDA ciede change cut
from an older base), put the by-value signatures back, and every launch since
has copied the 328-byte struct into the kernel arguments. This re-implements
the decision on today's structure instead of reverting the revert.

The design needs the struct to be the same on every launch. It is: s->buf is
written only by adm_hip_alloc_buffers(), adm_hip_slice_bands() and
adm_hip_slice_results() during init and by adm_hip_free_buffers() on
teardown. submit() passes &s->buf to every launch helper, and no launch
passes a modified copy; the scale 1-3 CSF and CM kernels get the scale as a
separate argument and use the same i4 band pointers at every scale.

adm_hip_upload_buf() allocates and uploads the copy at the end of
adm_hip_init_device(), after the band and result slices are final, and the
four launches pass &s->buf_dev. The copy is freed in close() and on both init
failure paths.

HIP output is byte-identical at %.17g before and after, with debug features,
on 8, 10, 12 and 16-bit input, odd 575x323 and 197x101 frames, bright
16-bit noise, the 1080p 8-bit checkerboard pairs and a 60-frame 1080p
10-bit clip. The kernel argument segment of each kernel shrinks by 320
bytes. Per-thread scratch and VGPRs do not change because of the pointer:
the 936 bytes on adm_cm_line_kernel_8 are VGPR spills (239 at the
128-register cap), not a copy of the struct. End-to-end 1080p 10-bit
throughput is unchanged within run-to-run noise.

adm_csf.hip had not been lint-cleaned yet, so this also takes it from 35
clang-tidy findings to 0: the device helpers move into an anonymous
namespace, locals become const, and the band select becomes a helper. That
restructure, not the pointer, lowers the two CSF kernels from 44 to 38 and
53 to 45 VGPRs. HIP tidy baseline tightened.
lusoris added a commit that referenced this pull request Sep 22, 2026
ADR-0759 moved the four HIP integer ADM kernels that read AdmBufferHip
(adm_csf_kernel_1_4, i4_adm_csf_kernel_1_4, i4_adm_cm_line_kernel,
adm_cm_line_kernel_8) from the struct by value to a pointer to a device copy.
It landed in 31a51af (#101); the next merge, 92ea978 (#102, an unrelated
CUDA ciede change squashed from an older base), put the by-value signatures
back the same day. The invariant note in core/src/feature/hip/AGENTS.md
survived the revert, so the file has since carried both a "P1 known issue:
struct-by-value" section and a "MUST be passed by pointer - Resolved" section,
with the code matching the first. Documentation that contradicts the code is
what the next rebase trusts, which is worse than either half alone.

This re-implements the decision on today's structure rather than reverting the
revert, and rewrites the AGENTS.md section to say what is true: the pointer
convention, the revert history so it is recognised if it happens again, the
host-side contract, and the fact that AdmFixedParametersHip (248 bytes) is
still by value by decision, not by oversight.

The design needs s->buf to be identical on every launch. It is: the pointer
fields are written only by the allocation and the two slicing blocks in
init_fex_hip() and cleared in close_fex_hip(), and submit() passes &s->buf to
the launch helpers without modifying it. AdmStateHip gains a void *buf_dev
device copy, uploaded at the end of init_fex_hip() once every pointer inside
s->buf is final, passed as args[0] by address, and freed in close_fex_hip()
and on the new fail_buf_dev init path. The upload shares the failure exit the
feature-name dictionary already used, so the file gains no goto and no
oversized block: praetorctl reports 1063 total HISS infractions before and
after.

Measured with hipcc --genco (ROCm 7.2.53211), reading each kernel's metadata
with clang-offload-bundler --unbundle and llvm-readelf --notes. Identical on
gfx1036, gfx1100 and gfx90a:

  adm_csf_kernel_1_4          kernarg 856 -> 536
  i4_adm_csf_kernel_1_4       kernarg 856 -> 536
  i4_adm_cm_line_kernel       kernarg 904 -> 584
  adm_cm_line_kernel_8        kernarg 968 -> 648
  adm_cm_reduce_line_kernel_4 kernarg 296 -> 296 (does not read the struct)

320 bytes = sizeof(AdmBufferHip) - sizeof(void *), off every launch. The
by-value copy was also spilling: adm_cm_line_kernel_8's per-thread scratch
drops 920 -> 608 bytes on gfx1036, 932 -> 616 on gfx90a and 664 -> 352 on
gfx1100, and its SGPR count 86 -> 78 on gfx1036. ADR-0759 and the earlier
notes quote the struct as ~272 bytes and the parameter struct as ~244; a
harness compiled against the real header prints 328 and 248. The frozen ADR
body keeps its numbers; the correction is recorded in Research-0759.

Verified on the host's real AMD device (gfx1036, the Raphael iGPU, ROCm 7.2):
a differential harness loaded both HSACO variants into one process, gave them
byte-identical randomised band contents and parameters, and compared every
output buffer - csf_f bands 1-3, i4_csf_f bands 1-3, the tmp_accum per-thread
scratch and the three int64 adm_cm accumulators. All four kernels are
bit-identical, over three shape/seed configurations (98x50 stride 128,
33x17 stride 36, 160x90 stride 160), with non-zero output throughout. Not
run: an end-to-end vmaf --backend hip score. The workstation was saturated by
sibling agents and no full enable_hipcc build was made.

Refs: ADR-0759, Research-0759.
lusoris added a commit that referenced this pull request Sep 22, 2026
A pull request can remove work from master with nothing in its review diff
looking like a removal of somebody else's commit. `92ea978a4` (#102, a CUDA
ciede change) reset three HIP ADM files to the exact blobs they held before
`31a51afb2` (#101, ADR-0759), merged 47 seconds earlier: the post-#102 blob
of `core/src/feature/hip/integer_adm_hip.c` is `baf2b339`, byte-identical to
the pre-#101 blob, and the 50 lines #101 added are the 50 lines #102 removed.
Every check stayed green. Because #101, its ADR, its AGENTS.md note and its
state row all stayed in tree, four months of audits read the change as
shipped; it was restored only by PR #1481, and the read-only audit of
master's first-parent history found dozens more in the same class.

`scripts/ci/check-silent-revert.py` measures the real merge result
(`git merge-tree --write-tree`, not the branch tree in isolation) and fails
when it removes target work the branch never set out to touch: a file rewound
to an older blob it held on the target's history, a target commit undone
hunk-for-hunk, or lines dropped or resurrected by a conflict resolution
rather than by any non-merge commit of the branch. The first two cover the
rebased-branch shape that produced #102, where intent and effect are the same
diff and only history can show the content is old; the last two lift the
principle of the existing post-restack `check-resurrected.py` to a base/head
pair.

It runs as the required context `Silent-Revert Guard` on every non-draft PR,
resolving the live target tip rather than the PR's recorded base because the
defect class is master moving after the branch was cut, and locally as
`make silent-revert-check`. A deliberate revert is declared where a reviewer
sees it (`revert:` title, `reverts: #N`, `intentional revert: <reason>`);
there is no in-tree suppression. The gate fails closed on an unresolvable
ref, unrelated histories, a conflicting merge, or a git below 2.38.

Replayed over master's last 30 first-parent commits it fires twice, both
correctly: `5a467e629` re-lands a migration the `c2a3c7e0f` mega-squash had
reverted, and `1beb3b8a9` removes an earlier fix along with its changelog
fragment. Both need a one-line declaration, which is the intended workflow.
A third case, a comment rewrite in `core/tools/vmaf.cpp`, was a false
positive and tightened the predicate out of existence.

`dropped` and `resurrected` check the surviving tree before reporting, so a
line still present in the merged file was relocated, not lost. Dogfooding the
gate against a 48-commit stack showed why: without that check both fire on a
diff-alignment artefact, where inserting text above a line makes the
cumulative diff re-pair it as a delete plus an add although no commit's own
diff shows the pair.

12 fixture tests cover the reproduced revert, the merge-commit resolution
case, four must-stay-clean shapes, the declaration path, the rejected
template placeholder, every fail-closed exit, and a replay of the real
`31a51afb2` / `92ea978a4` pair that skips rather than passes when those
commits are not in the clone.
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.

1 participant