Research digest: HIP integer-VIF kernel fault analysis (ADR-0537)¶
- Date: 2026-05-18
- Author: Claude (Anthropic), reviewed by lusoris
- Scope: Root-cause analysis of the GPU memory access fault that ADR-0530 documented but did not fix — the
vmaf_fex_integer_vif_hipextractor crashing on the first frame whenever the model-driven dispatch picked it on AMD ROCm.
Method¶
- Reproduce the fault in
vmaf-dev-mcpagainst AMD gfx1036 withAMD_LOG_LEVEL=3. Capture therocvirtual.cppargument-dump for every kernel launch. - Compare against the CUDA twin (
core/src/feature/cuda/integer_vif/{filter1d.cu,vif_statistics.cuh}) to identify structural divergence in (a) kernel signature, (b) hostargs[]array, (c) filter-coefficient handling, (d) downsample pipeline. - Comparable HIP extractors (
integer_motion_hip.c,float_psnr_hip.c) for the established HIP launch-arg idiom and per-frame HtoD staging precedent. - gdb backtrace of the post-launch SIGSEGV (after applying the first three fixes) to confirm the dispatch was now reaching
collect_fex_hiprather than faulting inside the kernel.
Findings¶
1. Filter table host-pointer (kernel arg)¶
The pre-fix kernel signature was:
__global__ void filter1d_8_vertical_kernel_uint32_t_17_9(
VifBufferHip buf,
const uint8_t *__restrict__ ref_in,
const uint8_t *__restrict__ dis_in,
int w, int h,
const uint16_t filter[18]) // <- decays to const uint16_t*
{ ... filter[k + HALF] ... }
Host launch:
vif_filter1d_table is a host-side static const uint16_t[4][18] defined in core/src/feature/integer_vif.h. vif_filter1d_table[0] decays to a host pointer. hipModuleLaunchKernel then copied that host address into the kernel argument region, and the GPU dereferenced it on the first filter[fi] load. AMD ROCm faulted with:
Memory access fault by GPU node-1 ... on address 0x55... Reason: Page
not present or supervisor privilege.
CUDA hides this defect because the CUDA twin passes the filter as a by-value struct (filter_table_stuct containing a 4×18 array) — the entire 144-byte payload is copied into the kernel argument region, so there is no pointer for the GPU to dereference. HIP module-launch does support struct-by-value args, but the existing HIP kernel chose the pointer signature.
Fix: allocate a device buffer at init, hipMemcpy the table to it once, pass &s->vif_filt_dev (address of the variable storing the device pointer) in args[].
2. Filter half-width parsed from the wrong kernel-suffix number¶
Kernel names like filter1d_16_vertical_kernel_uint2_17_9_0 encode three integers: fwidth_0=17, fwidth_1=9, scale=0. These come from the CUDA twin's templated instantiations (FILTER1D_16_VERT(uint2, 17, 9, 0)) where the second number is the downsample filter width (used to compute accum_ref_rd / accum_dis_rd), not the half-width of the main filter.
The pre-fix HIP kernel ignored that and used:
const int HALF = 9; // for the "17_9_0" kernel
for (int k = -HALF; k <= HALF; ++k) { // k = -9..9 -> 19 iterations
uint16_t coeff = filter[k + HALF]; // filter[0..18]
...
}
This reads 19 filter coefficients from an 18-entry vif_filter1d_table[0] — off-by-one OOB read on every output pixel. The actual filter widths are:
Fix: use vif_filter1d_width[scale] / 2 for the half-width.
3. Missing rd-filter downsample-write path¶
The CUDA twin's scale-0/1/2 kernels compute the *_convol channels in addition to the * channels:
if (fi >= (fwidth_0 - fwidth_1) / 2 &&
fi < (fwidth_0 - (fwidth_0 - fwidth_1) / 2)) {
accum_ref_rd[off] += fcoeff_rd * imgcoeff_ref;
accum_dis_rd[off] += fcoeff_rd * imgcoeff_dis;
}
// ...
ref[(y / 2) * rd_stride + (x / 2)] =
(uint16_t)((accum_ref_rd[off / 2] + 32768) >> 16);
dis[(y / 2) * rd_stride + (x / 2)] =
(uint16_t)((accum_dis_rd[off / 2] + 32768) >> 16);
The pre-fix HIP kernel did neither. Scales 1–3 then read from s->buf.ref / s->buf.dis (the rd half-res planes) which were uninitialised device memory — garbage in, garbage out, with crash probability undefined.
Fix: apply the rd filter in the horizontal pass (matching the CPU subsample_rd_8 / subsample_rd_16 reference); decimate-by-2 in the even-thread write path.
4. Picture buffer host pointer (separately reachable)¶
VmafPicture arrives as VMAF_PICTURE_BUFFER_TYPE_HOST for HIP (there is no HIP picture pool yet — VMAF_PICTURE_BUFFER_TYPE_HIP_DEVICE is reserved in ADR-0530 for future work). The pre-fix HIP submit passed ref_pic->data[0] directly to the kernel. Identical bug class to #1; would have faulted as soon as the filter pointer was fixed.
Fix: allocate s->ref_in_dev / s->dis_in_dev at init, run hipMemcpy2DAsync from ref_pic->data[0] into the device staging buffer before launching the scale-0 kernel. Established precedent in integer_motion_hip.c::msh_submit_inputs() at lines 372–373.
5. feature_name_dict ABI corruption (separately discovered)¶
After fixing the four kernel defects above, gdb showed a SIGSEGV inside vmaf_dictionary_get reading dict->cnt:
Thread 1 "vmaf" received signal SIGSEGV
0x00007f... in vmaf_dictionary_get (dict=0x7fff..., key="VMAF_integer_feature_vif_scale0_score") at dict.c:40
40 for (unsigned i = 0; i < d->cnt; i++) {
The dict pointer value was 0xffffffffa028f710 — a 32-bit value sign-extended to 64-bit. The cause: integer_vif_hip.c was missing #include "feature_name.h" (the header declaring vmaf_feature_name_dict_from_provided_features() as returning VmafDictionary *). Without the declaration, GCC's implicit-int rule treated the return as int, truncating the 64-bit pointer to 32 bits at the call site.
Fix: add #include "feature_name.h". This was missed because the legacy HIP scaffold copy-pasted only the includes the original scaffold-posture (-ENOSYS) needed; the real init path requires more.
Decision¶
Apply all five fixes in a single PR (ADR-0537). Re-enable VMAF_FEATURE_EXTRACTOR_HIP on vmaf_fex_integer_vif_hip.
Adjacent latent issues surfaced during the rebuild are bundled:
- Missing HSACO entries in
hip_kernel_sourcesfor the ADR-0533 registration sweep (seven extractors referenced_hsacosymbols the build never produced). - Weak-stub
_hsacosymbols for the four ADM kernels that fail to compile standalone because they depend on CUDA-helper macros (uint64_cu,warp_reduce,VMAF_CUDA_THREADS_PER_WARP). - Include path for
hipcc --gencoextended to findconfig.h(inbuild_dir) andinteger_*_hip.h(infeature/hip).
Per-frame parity numbers (Netflix golden pair, 576×324, 48 frames)¶
| Scale | HIP | CPU | Δ |
|---|---|---|---|
| 0 | 0.3630 | 0.3637 | 0.000710 |
| 1 | 0.7643 | 0.7675 | 0.003238 |
| 2 | 0.8607 | 0.8631 | 0.002458 |
| 3 | 0.9136 | 0.9157 | 0.002080 |
places=3 parity (round to 3 decimals matches). places=4 parity NOT yet achieved — the residual delta comes from boundary handling divergence: HIP uses an in-kernel clamp, CPU uses a pre-padded mirror buffer (PADDING_SQ_DATA_2). The cumulative effect compounds across the downsampling cascade — scale 0's small per-pixel mu delta becomes scale 3's larger numerator/denominator delta.
Tightening to places=4 is tracked as an ADR-0537 follow-up: port the CPU pre-pad boundary into the kernel (requires either a separate pad-and-copy kernel or a wider device staging buffer allocated at init).
References¶
- ADR-0530: HIP feature-flag promotion (un-flagged this extractor).
- ADR-0533: HIP-extractor registration sweep.
- ADR-0214: Cross-backend parity gate (places=4 target).
- AMD ROCm runtime trace: kernel launches succeed; fault address was in host-memory range (
0x55...), not GPU range (0x7f...).