MIOpen hipDNN Shim — Investigation Reference#
Companion document to the MIOpen / hipDNN shim RFC. Consolidates every test report under projects/miopen/test-results/ so the data backing every RFC decision is in one place.
Common context#
Date range: 2026-05-27
Branch:
users/nhanna/miopen-hipdnn-shim-investigation-1Hardware: AMD Instinct MI300X (gfx942), ROCm 7.13, MIOpen 3.5.2
Two builds under test, differing only in one CMake flag:
Build |
|
Library layout |
|---|---|---|
|
|
single |
|
|
|
The wrapper variant inserts a thin C-ABI forwarding layer: each of MIOpen’s 263 public entry points becomes a stub <name> in libMIOpen.so.1 that calls <name>_impl in libMIOpen_private.so.1. The rename is achieved during the private-library build via -DMIOPEN_BUILDING_PRIVATE -include miopen_private_rename.h, so the same source compiles to _impl-suffixed symbols.
Document map#
Section |
Source file |
What it answers |
|---|---|---|
|
Does the flag change basic MIOpen driver behavior or correctness? |
|
|
Can we prove every public call actually traverses the wrapper at runtime? |
|
|
Does the upstream MIOpen test suite still pass with the wrapper enabled? |
|
|
What is the runtime cost of the wrapper? |
|
|
Does the hipDNN provider plugin correctly link to (and route through) |
|
|
Can the wrapper actually redirect a call into hipDNN and run it end-to-end, and at what cost? |
§1–§5 are Phase 1 evidence (the library split). §6 is the first Phase 2 evidence (the forwarding path itself) and was produced in a separate, later investigation on different hardware — it carries its own context block rather than the common context below.
§1 MIOpenDriver smoke test#
Source: MIOpenDriverTest.md
MIOpenDriver was built in both trees via make -j32 MIOpenDriver; both linked successfully.
Linkage verification#
ldd confirms the flag produces the expected library split:
flagon —
MIOpenDriverlinks against bothlibMIOpen.so.1andlibMIOpen_private.so.1(private interface split out for the hipDNN wrapper).flagoff —
MIOpenDriverlinks againstlibMIOpen.so.1only (no private library produced).
No hipdnn* symbols are exported from libMIOpen.so in either build, which is expected — the shim lives in a separate target.
Smoke tests#
The same four invocations were run against each build. Verification was enabled (-V 1) in all cases.
1. Forward + Backward Convolution (verify only)#
MIOpenDriver conv -n 1 -c 3 -H 32 -W 32 -k 16 -y 3 -x 3 -p 1 -q 1 -V 1
Build |
Forward |
Bwd Data |
Bwd Weights |
|---|---|---|---|
flagon |
OK (3.08e-08) |
OK (4.46e-08) |
OK (7.87e-08) |
flagoff |
OK (3.08e-08) |
OK (4.46e-08) |
OK (7.91e-08) |
2. Convolution with Timing (-t 1)#
MIOpenDriver conv -n 1 -c 3 -H 32 -W 32 -k 16 -y 3 -x 3 -p 1 -q 1 -V 1 -t 1
Same solver selections on both builds:
Forward: solution 84 /
ConvBinWinogradRxSf2x3g1Bwd Data: solution 84 /
ConvBinWinogradRxSf2x3g1Bwd Weights: solution 110 /
ConvAsmImplicitGemmGTCDynamicWrwXdlopsNHWC
Stage |
flagon (ms) |
flagoff (ms) |
|---|---|---|
Fwd |
0.01046 |
0.01052 |
BwdD |
0.01259 |
0.01204 |
BwdW |
0.02244 |
0.02333 |
Timing variance is within noise; all stages verified OK on both builds.
3. GEMM#
MIOpenDriver gemm -m 64 -n 64 -k 64 -V 1
Both builds: Forward GEMM Verifies on CPU and GPU (err=0.000000).
4. Activation#
MIOpenDriver activ -n 1 -c 3 -H 32 -W 32 -V 1
Both builds: forward and backward activation verify on CPU and GPU.
Result#
Both builds pass every smoke test with identical correctness results. The MIOPEN_ENABLE_HIPDNN_WRAPPER=ON build adds the private library split (visible in ldd) without altering MIOpen driver behavior or solver selection on the tested workloads — i.e., enabling the hipDNN shim has not regressed the underlying MIOpen public API.
§2 Runtime wrapper logging#
Source: RuntimeWrapperLogging.md
Smoke and gtest results show flagon behaves like flagoff, but they don’t directly prove the wrapper layer is being used. This section adds explicit per-call instrumentation that prints whenever a wrapper stub is invoked, so the wrapper-traversal claim is observable in stderr.
What was changed#
src/private/wrapper.cpp was instrumented so each of the 263 forwarding stubs announces itself on stderr before delegating to its *_impl counterpart. One #include <cstdio> was added and one line was inserted as the first statement of every stub:
extern "C" miopenStatus_t miopenCreate(miopenHandle_t* handle)
{
fprintf(stderr, "[MIOPEN_HIPDNN_WRAPPER] miopenCreate\n");
return miopenCreate_impl(handle);
}
Only the MIOpen CMake target in build-flagon was rebuilt; build-flagoff was not touched (it does not compile wrapper.cpp). The original wrapper file was backed up to /tmp/wrapper.cpp.bak for revert.
Test command#
A single MIOpenDriver invocation was issued against each build:
MIOpenDriver convfp16 -n 1 -c 1 -H 8 -W 8 -k 1 -y 3 -x 3 -p 1 -q 1 -u 1 -v 1 \
-l 1 -j 1 -m conv -g 1 -F 1 -t 1 -i 1 -V 0
stdout and stderr were captured separately so the wrapper trace is trivially isolated from normal driver output. Both runs exited 0 and selected the same solver (85/ConvDirectNaiveConvFwd).
Logs: perf-results/wrapper-proof/{flagon,flagoff}.instr.{stdout,stderr}.
Full output — flagon#
flagon.instr.stdout (7 lines — same shape as flagoff):
MIOpenDriver convfp16 -n 1 -c 1 -H 8 -W 8 -k 1 -y 3 -x 3 -p 1 -q 1 -u 1 -v 1 -l 1 -j 1 -m conv -g 1 -F 1 -t 1 -i 1 -V 0
PRNG seed: 12345678
Timestamp: 2026-05-27 14:49:25 UTC; Host Name: 684148a436c9; Operating System: Linux 5.15.0-173-generic; ROCm: 7.13.61040; MIOpen Driver: 3.5.2; CPU Vendor: Intel; CPU Model: 2 x Intel(R) Xeon(R) Platinum 8480C; RAM Size: 2015 GB; GPU Model: 8 x AMD Instinct MI300X; AMDGPU Driver: 6.16.13
MIOpen Forward Conv. Algorithm: 1, Solution: 85/ConvDirectNaiveConvFwd
GPU Kernel Time Forward Conv. Elapsed: 0.006776 ms (average)
stats: name, n, c, ho, wo, y, x, k, flopCnt, bytesRead, bytesWritten, GFLOPs, GB/s, timeMs
stats: fwd-conv3x3u1, 1, 1, 8, 8, 3, 3, 1, 1152, 146, 128, 0, 0, 0.006776
flagon.instr.stderr (45 lines — every line is wrapper proof):
[MIOPEN_HIPDNN_WRAPPER] miopenCreateWithStream
[MIOPEN_HIPDNN_WRAPPER] miopenGetStream
[MIOPEN_HIPDNN_WRAPPER] miopenCreateTensorDescriptor
[MIOPEN_HIPDNN_WRAPPER] miopenCreateTensorDescriptor
[MIOPEN_HIPDNN_WRAPPER] miopenCreateTensorDescriptor
[MIOPEN_HIPDNN_WRAPPER] miopenCreateTensorDescriptor
[MIOPEN_HIPDNN_WRAPPER] miopenCreateTensorDescriptor
[MIOPEN_HIPDNN_WRAPPER] miopenCreateTensorDescriptor
[MIOPEN_HIPDNN_WRAPPER] miopenCreateConvolutionDescriptor
[MIOPEN_HIPDNN_WRAPPER] miopenCreateTensorDescriptor
[MIOPEN_HIPDNN_WRAPPER] miopenCreateTensorDescriptor
[MIOPEN_HIPDNN_WRAPPER] miopenCreateTensorDescriptor
[MIOPEN_HIPDNN_WRAPPER] miopenCreateConvolutionDescriptor
[MIOPEN_HIPDNN_WRAPPER] miopenEnableProfiling
[MIOPEN_HIPDNN_WRAPPER] miopenSetTensorDescriptorV2
[MIOPEN_HIPDNN_WRAPPER] miopenSetTensorDescriptorV2
[MIOPEN_HIPDNN_WRAPPER] miopenInitConvolutionNdDescriptor
[MIOPEN_HIPDNN_WRAPPER] miopenSetConvolutionGroupCount
[MIOPEN_HIPDNN_WRAPPER] miopenSetConvolutionAttribute
[MIOPEN_HIPDNN_WRAPPER] miopenGetConvolutionNdForwardOutputDim
[MIOPEN_HIPDNN_WRAPPER] miopenSetTensorDescriptorV2
[MIOPEN_HIPDNN_WRAPPER] miopenGetTensorDescriptorSize
[MIOPEN_HIPDNN_WRAPPER] miopenGetTensorDescriptorSize
[MIOPEN_HIPDNN_WRAPPER] miopenGetTensorDescriptorSize
[MIOPEN_HIPDNN_WRAPPER] miopenGetTensorDescriptorSize
[MIOPEN_HIPDNN_WRAPPER] miopenGetTensorDescriptorSize
[MIOPEN_HIPDNN_WRAPPER] miopenGetTensorDescriptorSize
[MIOPEN_HIPDNN_WRAPPER] miopenConvolutionForwardGetWorkSpaceSize
[MIOPEN_HIPDNN_WRAPPER] miopenGetVersion
[MIOPEN_HIPDNN_WRAPPER] miopenFindConvolutionForwardAlgorithm
[MIOPEN_HIPDNN_WRAPPER] miopenConvolutionForwardGetSolution
[MIOPEN_HIPDNN_WRAPPER] miopenConvolutionForward
[MIOPEN_HIPDNN_WRAPPER] miopenGetKernelTime
[MIOPEN_HIPDNN_WRAPPER] miopenDestroyTensorDescriptor
[MIOPEN_HIPDNN_WRAPPER] miopenDestroyTensorDescriptor
[MIOPEN_HIPDNN_WRAPPER] miopenDestroyTensorDescriptor
[MIOPEN_HIPDNN_WRAPPER] miopenDestroyTensorDescriptor
[MIOPEN_HIPDNN_WRAPPER] miopenDestroyTensorDescriptor
[MIOPEN_HIPDNN_WRAPPER] miopenDestroyTensorDescriptor
[MIOPEN_HIPDNN_WRAPPER] miopenDestroyConvolutionDescriptor
[MIOPEN_HIPDNN_WRAPPER] miopenDestroyTensorDescriptor
[MIOPEN_HIPDNN_WRAPPER] miopenDestroyTensorDescriptor
[MIOPEN_HIPDNN_WRAPPER] miopenDestroyTensorDescriptor
[MIOPEN_HIPDNN_WRAPPER] miopenDestroyConvolutionDescriptor
[MIOPEN_HIPDNN_WRAPPER] miopenDestroy
Full output — flagoff#
flagoff.instr.stdout (7 lines — same shape, same result):
MIOpenDriver convfp16 -n 1 -c 1 -H 8 -W 8 -k 1 -y 3 -x 3 -p 1 -q 1 -u 1 -v 1 -l 1 -j 1 -m conv -g 1 -F 1 -t 1 -i 1 -V 0
PRNG seed: 12345678
Timestamp: 2026-05-27 14:49:27 UTC; Host Name: 684148a436c9; Operating System: Linux 5.15.0-173-generic; ROCm: 7.13.61040; MIOpen Driver: 3.5.2; CPU Vendor: Intel; CPU Model: 2 x Intel(R) Xeon(R) Platinum 8480C; RAM Size: 2015 GB; GPU Model: 8 x AMD Instinct MI300X; AMDGPU Driver: 6.16.13
MIOpen Forward Conv. Algorithm: 1, Solution: 85/ConvDirectNaiveConvFwd
GPU Kernel Time Forward Conv. Elapsed: 0.006936 ms (average)
stats: name, n, c, ho, wo, y, x, k, flopCnt, bytesRead, bytesWritten, GFLOPs, GB/s, timeMs
stats: fwd-conv3x3u1, 1, 1, 8, 8, 3, 3, 1, 1152, 146, 128, 0, 0, 0.006936
flagoff.instr.stderr (0 lines):
[empty]
The lines that show the difference#
The full contrast is the entire stderr stream — 45 wrapper hits on flagon vs. 0 on flagoff. The structurally important hits inside the flagon trace are the ones that map to the actual conv operation rather than per-tensor bookkeeping:
Line in |
What it proves |
|---|---|
|
Very first MIOpen API call traverses the wrapper |
|
Conv descriptor setup goes through the wrapper |
|
Solver search routed through the wrapper |
|
The actual GPU enqueue routed through the wrapper |
|
Timing readback routed through the wrapper |
|
Final teardown traverses the wrapper |
The flagoff side prints none of these because its libMIOpen.so.1 contains the real functions directly — there is no stub layer to print from. Stdout is byte-shape-identical between the two runs; the entire behavioral delta is on stderr, and it is exactly what the wrapper design predicts.
Reverting the instrumentation#
cp /tmp/wrapper.cpp.bak src/private/wrapper.cpp
cmake --build build-flagon --target MIOpen -- -j$(nproc)
§3 MIOpen gtest results — build-flagon#
Source: MIOpenGtestResults.md
Run completed 2026-05-27, 15:31:21 → 20:25:25 UTC (≈4h 54m, single-threaded sequential).
The build uses MIOPEN_TEST_DISCRETE=ON, so there is no single miopen_gtest binary — gtest content is split across 265 discrete test_* executables in build-flagon/bin/. Source for each lives under test/gtest/*.cpp. All 265 were executed (7 in the first pass with the v1 runner, 258 in the resume pass with the v2 runner).
Top-line numbers#
Metric |
Value |
|---|---|
Binaries executed |
265 / 265 |
Binaries clean (exit 0, no failures) |
261 |
Binaries with failures |
2 |
Binaries hit the 1200 s timeout |
2 |
Individual gtest cases run |
58,039 |
Cases passed |
53,516 |
Cases skipped |
4,520 |
Cases failed |
4 |
The 4,520 skips are expected — they’re guards in the test harness for unsupported architectures / dtypes / configs (e.g. test_rnn_seq_api skips 2,560 of 4,608 cases on gfx942, test_bad_fusion_plan skips all 8).
Failures (4 cases across 2 binaries)#
test_smoke_tuning_policy — 2 failures (likely wrapper-induced)#
Both failing tests capture stderr from a miopen{Get,Set}TuningPolicy() call and assert the captured text contains the public function name. The build under test routes through the wrapper, so the impl’s auto-generated function-entry log emits _impl-suffixed names — the substring check misses by exactly that suffix:
test/gtest/smoke_tuning_policy.cpp:105: Failure
Expected: has substring " miopenGetTuningPolicy("
Actual: "MIOpen(HIP): miopenStatus_t miopenGetTuningPolicy_impl(miopenHandle_t, miopenTuningPolicy_t *){\n…"
Same shape for TestSetApiLogged. This is a real wrapper-related test breakage, not noise — the test was written against the un-wrapped API name and the wrapper’s pass-through changes the logged symbol. Worth fixing as part of the shim work (either rename in the wrapper’s _impl symbols’ MIOPEN_LOG_FUNCTION output, or relax the assertion to accept the _impl form).
The other 4 tests in this binary pass.
Timeouts (2 binaries, runner killed at 1200 s)#
Both tests were actively making progress when the timeout fired — they’re not hung, they’re just enormous suites:
Binary |
Tests started |
Tests passed before kill |
|---|---|---|
|
123 |
122 (still running #123) |
|
267 |
266 (still running #267) |
Neither produced a [ FAILED ] line. To get clean numbers either raise the per-binary timeout (e.g. timeout 3600) or filter to a subset via --gtest_filter.
Largest suites (top 10 by test count)#
Binary |
Ran |
Pass |
Fail |
Skip |
|---|---|---|---|---|
|
7,452 |
7,452 |
0 |
0 |
|
7,200 |
7,200 |
0 |
0 |
|
5,143 |
5,143 |
0 |
0 |
|
4,608 |
2,048 |
0 |
2,560 |
|
3,270 |
3,270 |
0 |
0 |
|
3,042 |
3,042 |
0 |
0 |
|
1,944 |
1,944 |
0 |
0 |
|
1,870 |
1,870 |
0 |
0 |
|
1,625 |
701 |
0 |
924 |
|
1,308 |
1,308 |
0 |
0 |
Slowest binaries (top 10 by wall time)#
Binary |
Duration |
|---|---|
|
1200 s (timeout) |
|
1200 s (timeout) |
|
1171 s |
|
901 s |
|
856 s |
|
815 s |
|
744 s |
|
734 s |
|
602 s |
|
572 s |
Artifacts#
All under perf-results/gtest-flagon/:
_all_gtests.txt— canonical list of all 265 binaries._remaining_at_resume.txt— the 258 binaries fed to the v2 runner._run_gtests.sh/_progress.log/_summary.tsv— v1 runner (first 7 binaries). The v1 summary’s pass/fail/skip columns are bogus (the--gtest_brief=1flag suppressed the per-test lines its grep relied on)._run_gtests_v2.sh/_progress_v2.log/_summary_v2.tsv— v2 runner (remaining 258). Drops--gtest_brief=1and parses the[==========]/[ PASSED ]/[ SKIPPED ]/[ FAILED ]summary lines for accurate counts. This is the source of truth for runner-side counts.test_<name>.log— raw stdout+stderr for each of the 265 binaries.
The numbers in this section were re-aggregated directly from the 265 test_*.log files (not from _summary*.tsv), so they’re consistent regardless of which runner produced a given log.
Re-run hints#
Just the failing binaries:
build-flagon/bin/test_smoke_tuning_policyandbuild-flagon/bin/test_db_sync(each <2 s).Just the timeout victims with a larger budget:
timeout 3600 build-flagon/bin/test_lrnand the same fortest_soft_max.Same suite against
build-flagoff(to confirmtest_smoke_tuning_policypasses there and conclusively pin it on the wrapper): swap the BINDIR in_run_gtests_v2.shtobuild-flagoff/binand re-run.
§4 Performance comparison#
Source: PerformanceComparison.md
Script: ~/test-data/model_f_short.sh (64 convbfp16 configs, NHWC layout). Only MIOPEN_ENABLE_HIPDNN_WRAPPER differs between the two builds.
Methodology#
For each build, in order: clear ~/.cache/miopen and ~/.config/miopen, then:
OOTB —
--iter 10(cold cache, default find).Tuning —
--iter 1withMIOPEN_FIND_ENFORCE=4(full exhaustive tuning).Tuned —
--iter 10 -S 0(re-use the find-db produced in step 2).
All raw logs are in perf-results/. Every one of the 64 configs verified OK against the GPU reference in all six runs (no correctness regressions).
Wall-Clock Duration#
Phase |
flagoff |
flagon |
Δ (flagon − flagoff) |
|---|---|---|---|
OOTB |
558 s |
528 s |
−30 s (−5.4%) |
Tuning |
728 s |
668 s |
−60 s (−8.2%) |
Tuned |
533 s |
464 s |
−69 s (−12.9%) |
flagon is consistently the same or slightly faster end-to-end. The wall-clock numbers are dominated by per-config CPU setup (kernel compilation, JIT, find-db lookups) so they reflect process overhead more than GPU work.
Aggregate GPU Kernel Time (sum across 64 configs)#
Phase |
flagoff |
flagon |
total Δ |
|---|---|---|---|
OOTB |
6.9436 ms |
6.8860 ms |
−0.83% |
Tuning |
6.4445 ms |
6.5752 ms |
+2.03% |
Tuned |
6.0500 ms |
6.0534 ms |
+0.06% |
GPU work is statistically identical between builds. Sub-1% differences in OOTB/tuned are well within the noise floor for --iter 10 runs at this scale.
Per-Config Variability#
Phase |
mean per-config Δ |
max regression |
max improvement |
|---|---|---|---|
OOTB |
+0.09% |
+13.94% (idx 64) |
−13.93% (idx 44) |
Tuned |
−0.53% |
+46.65% (idx 46) |
−57.43% (idx 44) |
The outliers do not track the flag — they track solver selection. In the tuned run, diff of MIOpen … Algorithm: lines shows the two builds picked different “best” solvers on 2 of 64 configs (3 of 64 in OOTB). For example:
idx 34 (TUNED): flagoff →
108/ConvAsmImplicitGemmGTCDynamicBwdXdlopsNHWC; flagon →155/ConvHipImplicitGemmGroupBwdXdlops.
This is normal find/tuning non-determinism (timing-driven tie-breaking on a cold cache) and is the dominant source of per-config swings. Aggregate kernel time absorbs these into noise.
Tuned vs. OOTB (validates the tuning flow)#
Build |
OOTB sum |
Tuned sum |
Improvement |
|---|---|---|---|
flagoff |
6.9436 ms |
6.0500 ms |
−12.9% |
flagon |
6.8860 ms |
6.0534 ms |
−12.1% |
Tuning provides ≈12% kernel-time reduction on this workload for both builds, confirming the find-db path is exercised identically.
Convolution-level conclusion#
Enabling the hipDNN wrapper (MIOPEN_ENABLE_HIPDNN_WRAPPER=ON) has no measurable performance impact on MIOpen itself:
GPU kernel time totals differ by ≤2% in every phase, with the signed direction varying by phase — i.e., noise.
Wall-clock duration is the same or slightly better for flagon (≤13% faster on the tuned run), well within run-to-run variance for cache/JIT-dominated work.
All 64 configs verify in every run; the tuning flow yields the same ≈12% improvement on both builds.
The few large per-config swings are explained by find/tuning picking different but performance-equivalent solvers, not by the flag.
The shim is performance-neutral for the public MIOpen path on this benchmark.
Wrapper microbenchmark — per-call API overhead (warm & cold)#
The convolution benchmarks above are dominated by GPU kernel time; per-call CPU overhead in the C ABI is essentially invisible against milliseconds of GPU work. To isolate the wrapper cost itself, a small C harness (perf-results/wrapper-bench/wrapper_bench.c) calls the cheapest public MIOpen entry point (miopenGetVersion, no GPU work) and a moderate one (miopenCreate + miopenDestroy) in a tight loop, then reports wall/CPU time, getrusage page faults, RSS, and context switches.
Both builds use the same source and same compile flags; the only difference is whether MIOPEN_ENABLE_HIPDNN_WRAPPER=ON was set at CMake time, which inserts a forwarding stub (<symbol> in libMIOpen.so → <symbol>_impl in libMIOpen_private.so) for every public API.
Method#
Two binaries built against
build-flagoff/libandbuild-flagon/lib; rpath pins each to its own MIOpen.Warm runs: prime the page cache, then loop the API call 100M times (getversion) or 1k times (create/destroy). 10 reps each.
Cold runs:
sync && echo 3 > /proc/sys/vm/drop_cachesplus explicitdd iflag=nocacheeviction oflibMIOpen.so.1(andlibMIOpen_private.so.1for flagon) before each invocation, then run a single iteration so process startup / dynamic-loader cost dominates. 5 reps each.Noop baseline (warm only): same loop structure without the API call, to estimate harness cost.
Driver:
perf-results/wrapper-bench/run_bench.sh; raw output:out.tsv,raw.log.
Results — warm steady-state (per-call cost)#
Mode |
Build |
n |
mean (ns/call) |
median |
stdev |
min |
max |
|---|---|---|---|---|---|---|---|
noop (baseline) |
flagoff |
10 |
0.85 |
0.74 |
0.22 |
0.72 |
1.26 |
noop (baseline) |
flagon |
10 |
0.85 |
0.75 |
0.22 |
0.72 |
1.28 |
|
flagoff |
10 |
3.23 |
3.24 |
0.87 |
2.36 |
4.06 |
|
flagon |
10 |
4.41 |
4.44 |
1.14 |
3.29 |
5.53 |
|
flagoff |
10 |
4,190,048 |
4,069,811 |
316,862 |
3,773,061 |
4,729,323 |
|
flagon |
10 |
4,241,539 |
4,066,200 |
421,505 |
3,775,337 |
5,066,603 |
Notes:
The noop loop (no API call) shows the same bimodal jitter as
getversion(0.72 vs 1.28 ns/iter), which is CPU DVFS / P-state hop on this host, not the wrapper. Subtracting the noop baseline isolates the API cost.miopenGetVersionis a 3-pointer-store function: useful as a worst-case relative measurement of wrapper hop cost.miopenCreate/Destroyallocates a HIP stream and reads driver state (~4 ms/call); the wrapper hop is in the noise.
Net per-call cost (mean, harness-subtracted)#
API |
flagoff |
flagon |
Δ (flagon − flagoff) |
|---|---|---|---|
|
2.38 ns |
3.56 ns |
+1.18 ns / call |
|
4.190 ms |
4.242 ms |
+0.05 ms / call (≈ 0.0012 ×, within stdev) |
The ≈1 ns delta on miopenGetVersion is the upper bound on wrapper hop cost — it is an indirect call through the PLT into the private library plus one extra ret. For any real MIOpen API (which invariably touches the GPU), this is unmeasurable.
Results — cold start (dynamic-loader cost)#
These runs do a single API call after dropping caches, so the dominant cost is the kernel paging in the .so files from disk. The interesting columns are wall time and major page faults (majflt).
Mode |
Build |
n |
mean wall (ms) |
mean majflt |
mean minflt |
mean RSS (KB) |
|---|---|---|---|---|---|---|
|
flagoff |
5 |
0.0023 |
1,438 |
5,491 |
186,978 |
|
flagon |
5 |
0.0023 |
1,444 |
5,500 |
187,909 |
|
flagoff |
5 |
1.503 |
1,658 |
11,131 |
600,926 |
|
flagon |
5 |
1.564 |
1,664 |
11,158 |
602,040 |
Cold-load delta is +6 major page faults, identical for both APIs. That is the cost of mapping the additional
libMIOpen_private.so.1ELF header / dynamic section into the process — every other page that has to be paged in (the actual code/data of MIOpen, ROCm-comgr, hipBLASLt, MIOpenTensile, etc.) is identical between builds because the same total amount of code is loaded; flagon just splits it across two files.Wall-clock cold-startup difference is ≈60 µs for create/destroy (≈4%), within the run-to-run stdev of ~70 µs. For
getversionthe wall time is dominated byclock_gettimeresolution and is indistinguishable between builds.Resident-set size is the same to within ~1 MB; the wrapper adds no measurable memory footprint beyond a second ELF file’s overhead.
Microbenchmark conclusion#
Direct measurement of the wrapper hop confirms the convolution-level findings:
Steady-state CPU overhead: ≈1 ns per call on a worst-case (no-work) API, well below the per-call cost of any function that actually launches a kernel.
Cold-load overhead: ≈6 extra major page faults (one extra ELF mapped), with no detectable change in wall time, RSS, or minor page faults.
The split into
libMIOpen.so+libMIOpen_private.sois not a meaningful runtime cost — it is a structural change with essentially-zero measurable impact on the public API surface.
§5 MIOpen hipDNN provider plugin — linkage & runtime verification#
Source: MIOpenPluginResults.md
Synthetic harness: perf-results/plugin-test/.
5.1 Purpose#
The hipDNN MIOpen provider plugin selects its MIOpen link target at CMake time:
# dnn-providers/miopen-provider/CMakeLists.txt:149-163
find_package(miopen CONFIG REQUIRED)
# RFC 0001 Phase 4 short-circuit: when the MIOpen install was built with
# MIOPEN_ENABLE_HIPDNN_WRAPPER=ON, it exports a MIOpen_private target whose
# libMIOpen_private.so contains the renamed _impl symbols. Linking the plugin
# against MIOpen_private bypasses the wrapper, so this provider's calls don't
# round-trip back through hipDNN. When the MIOpen install is the legacy
# single-library build, MIOpen_private won't exist and we fall back to MIOpen.
if(TARGET MIOpen_private)
set(_MIOPEN_PROVIDER_LINK_TARGET MIOpen_private)
message(STATUS "miopen-provider: linking MIOpen_private (Phase 4 short-circuit)")
else()
set(_MIOPEN_PROVIDER_LINK_TARGET MIOpen)
message(STATUS "miopen-provider: linking MIOpen (legacy single-library install)")
endif()
The intent: when MIOpen is installed in wrapper mode (flagon), the provider plugin links directly to libMIOpen_private.so.1 so its calls don’t take the public-API wrapper hop and don’t risk re-entering hipDNN. When MIOpen is installed in legacy mode (flagoff), the provider links to libMIOpen.so.1 like any normal MIOpen consumer.
This section verifies, via structural ELF inspection, symbol resolution, dynamic-linker tracing, and a negative test, that this wiring selects the correct library at link time and that the loader routes the runtime calls as designed — and documents one gap that surfaced during the runtime test.
5.2 Test artifacts#
Because no pre-built provider plugin in the working tree is linked against libMIOpen_private.so.1 (the canonical one in /data/jlichtne/TheRock/therock-build/lib/hipdnn_plugins/engines/libmiopen_plugin.so was built against an older MIOpen install that lacked the MIOpen_private target — see §5.3), a synthetic plugin that mirrors the real plugin’s link wiring was built:
perf-results/plugin-test/synthetic_plugin.c— calls three public MIOpen entry points (miopenGetVersion,miopenCreate,miopenDestroy). Compiled with-fvisibility=hidden, exports onlyplugin_runvia a linker version script — matches the real plugin’s symbol-export discipline.perf-results/plugin-test/build.sh— produces two variants from the same source, againstbuild-flagon/lib:libsynth_plugin_public.so→ linked-lMIOpen(legacy / fallback target)libsynth_plugin_private.so→ linked-lMIOpen_private(Phase-4 target)
perf-results/plugin-test/host.c— a genericdlopen(RTLD_NOW | RTLD_LOCAL) + dlsym("plugin_run")host, mimicking how hipDNN loads engine plugins at runtime. The host does not link MIOpen itself; all MIOpen exposure comes from the loaded plugin’sDT_NEEDED.
5.3 Structural verification (readelf -d)#
DT_NEEDED / DT_RUNPATH of each artifact:
Artifact |
NEEDED MIOpen entry |
RUNPATH |
|---|---|---|
|
|
|
|
|
|
TheRock pre-built |
|
|
|
|
|
Findings:
The two synthetic variants differ only in their
NEEDEDMIOpen entry — exactly the difference the Phase-4 short-circuit produces. This is the structural fingerprint we’d see on any real provider plugin built from the same CMakeLists.libMIOpen.so.1itself has a hardNEEDEDonlibMIOpen_private.so.1. This is by design — the wrapper stubs inlibMIOpen.so.1call<name>_implsymbols defined in the private library — but it means that in a flagon install every consumer oflibMIOpen.so.1transitively loadslibMIOpen_private.so.1whether they want to or not.The TheRock pre-built
libmiopen_plugin.soisNEEDED libMIOpen.so.1(no MIOpen_private), confirming it was built against a non-flagon MIOpen install. It is not useful for validating Phase 4 and cannot be substituted for the synthetic test.
5.4 Symbol cross-check#
How many miopen* symbols each library exports, and what form (nm -D --defined-only | grep " T miopen"):
Library |
Plain |
|
Total |
|---|---|---|---|
|
263 |
0 |
263 |
|
4 † |
263 |
267 |
† The 4 non-renamed survivors in libMIOpen_private.so.1 are: miopenConvolutionABBackwardWeightsGetWorkSpaceSize, miopenHiddenGetConvolutionFindMode, miopenHiddenSetConvolutionFindMode, miopen_sqlite3_memvfs_init. They are not in the rename header and are unrelated to the wrapper hop.
Undefined miopen* symbols in each synthetic plugin (nm -D --undefined-only):
Plugin |
Undefined |
|---|---|
|
|
|
|
Note the identical undefined-symbol set: the plugin source uses the unrenamed public names (#include <miopen/miopen.h>) regardless of which library it is linked against. The linker tolerates this for the PRIVATE variant because shared-object linking is lazy — undefined symbols in a .so are not errors at link time; they are recorded as a request the dynamic loader will fulfill later.
Where each undefined symbol resolves:
Plugin |
|
|
|
|---|---|---|---|
PUBLIC |
|
|
|
PRIVATE |
|
only |
only |
The PRIVATE variant has no way to bind its undefined symbols at load time — the names it asks for are not the names the private library exports. This is the gap explored in §5.5 and §5.7.
5.5 Runtime verification — LD_DEBUG=bindings#
Captures: perf-results/plugin-test/ld_public.<pid>, ld_private.<pid>.
PUBLIC variant — ./host ./libsynth_plugin_public.so#
plugin_run returned 10 # = 3 + 5 + 2 (MIOpen 3.5.2)
Bindings of interest (counts from the LD_DEBUG output):
Source library |
Target library |
Bindings |
Notes |
|---|---|---|---|
|
|
3 |
exactly the three API calls in |
|
|
263 |
every wrapper stub binds its matching |
The three explicit plugin-side bindings:
binding file ./libsynth_plugin_public.so [0]
to .../libMIOpen.so.1 [0]: normal symbol `miopenGetVersion'
binding file ./libsynth_plugin_public.so [0]
to .../libMIOpen.so.1 [0]: normal symbol `miopenCreate'
binding file ./libsynth_plugin_public.so [0]
to .../libMIOpen.so.1 [0]: normal symbol `miopenDestroy'
This is the canonical wrapper-hop trace: plugin code calls miopenCreate, which is a forwarding stub in libMIOpen.so.1, which calls miopenCreate_impl in libMIOpen_private.so.1. The 263 wrapper→private bindings are the entire C-ABI surface — they get resolved up front because the host uses RTLD_NOW, but only three of them are actually called during this run.
PRIVATE variant — ./host ./libsynth_plugin_private.so#
dlopen failed: ./libsynth_plugin_private.so: undefined symbol: miopenGetVersion
Bindings of interest:
Source library |
Target library |
Bindings |
Notes |
|---|---|---|---|
|
|
6 |
only C runtime / unwind glue |
|
any |
0 |
dlopen aborted before any MIOpen binding could be attempted |
(The trace does record ~2.8k bindings internal to libMIOpen_private.so.1 and from ROCm helpers into it — those are part of loading the library itself and would happen before the plugin’s own relocations are processed.)
This is the runtime expression of the §5.4 gap: the linker happily built libsynth_plugin_private.so with U miopenGetVersion, but the only thing libMIOpen_private.so.1 exports under that name discipline is miopenGetVersion_impl, so dlopen fails at the plugin’s relocation step. The Phase-4 short-circuit, as currently written in the provider’s CMakeLists, produces a .so that links but does not load.
5.6 Negative test — hide libMIOpen_private.so.1#
To confirm that the loader is actually pulling libMIOpen_private.so.1 (not picking up a stale copy from /opt/rocm/lib or similar):
$ mv build-flagon/lib/libMIOpen_private.so.1{,.hidden_for_test}
$ ./host ./libsynth_plugin_public.so
dlopen failed: libMIOpen_private.so.1: cannot open shared object file: No such file or directory
$ ./host ./libsynth_plugin_private.so
dlopen failed: libMIOpen_private.so.1: cannot open shared object file: No such file or directory
$ mv build-flagon/lib/libMIOpen_private.so.1{.hidden_for_test,}
$ ./host ./libsynth_plugin_public.so
plugin_run returned 10
Both variants fail with the same “cannot open shared object” error, then PUBLIC recovers immediately after the file is restored. This confirms:
The PUBLIC plugin’s runtime dependency on
libMIOpen_private.so.1is real and transitive (vialibMIOpen.so.1’sDT_NEEDED) — there is no possible code path through the wrapper variant that avoids loading the private library.The PRIVATE plugin’s runtime dependency on
libMIOpen_private.so.1is real and direct.Both plugins resolve
libMIOpen_private.so.1frombuild-flagon/libvia theDT_RUNPATHbaked into each plugin’s ELF (and intolibMIOpen.so.1’s ELF for the PUBLIC case) — nothing else on the system is being silently substituted.
5.7 Gap finding — Phase 4 short-circuit is incomplete#
§5.4–§5.6 together establish that the provider’s CMake snippet chooses the link target correctly (the PRIVATE variant does link libMIOpen_private.so.1 rather than libMIOpen.so.1), but the resulting plugin cannot be loaded because the symbol names visible to the plugin’s compilation unit don’t match the symbol names exported by libMIOpen_private.so.1.
The mechanism the wrapper build uses to make MIOpen’s own translation units call _impl names is a build-time include:
# projects/miopen/src/CMakeLists.txt — MIOpen Private build options
target_compile_options(MIOpen_private PRIVATE
-DMIOPEN_BUILDING_PRIVATE
-include ${CMAKE_CURRENT_SOURCE_DIR}/private/miopen_private_rename.h)
The header (projects/miopen/src/private/miopen_private_rename.h) #defines every miopenFoo to miopenFoo_impl and is gated by MIOPEN_BUILDING_PRIVATE — and the comment at the top reads:
This header is intentionally NOT installed (RFC 0001 §4.6, Q5).
So the rename header is not in the install tree; the provider plugin, being a downstream consumer, has no way to apply it as written today. The PRIVATE link target therefore only delivers value if something on the consumer side also rewrites the names. Options:
Install the rename header under
private/in the MIOpen install tree, and havefind_package(miopen)set up theMIOpen_privateimported target’sINTERFACE_INCLUDE_DIRECTORIES/INTERFACE_COMPILE_OPTIONSto apply-DMIOPEN_BUILDING_PRIVATE -include miopen_private_rename.hautomatically. The provider plugin would then transparently get the renamed names whenever it links againstMIOpen_private. This is the lowest-friction option for plugin authors.Ship a parallel public header that declares the API in
_implform, and have the provider’s source code include that header when targetingMIOpen_private. This avoids the macro-rename trick but requires the provider to be conditionally compiled.Keep the rename header private, change the private library’s exports so it also publishes the unrenamed names as additional symbols (aliases). This eliminates the need for any consumer-side rename, at the cost of
libMIOpen_private.so.1having two symbols per API. It re-creates the risk Phase 4 was trying to remove (the private library being callable through the public name path).
Option (1) most closely matches the existing build’s mechanism and was the assumption the provider’s CMake snippet was written against.
The runtime evidence collected here can be used as the regression bar for any chosen fix: once it’s in, ./host ./libsynth_plugin_private.so must succeed, and LD_DEBUG=bindings must show plugin→libMIOpen_private.so.1 bindings on miopenCreate_impl / miopenDestroy_impl / miopenGetVersion_impl with zero bindings into libMIOpen.so.1.
5.8 Plugin summary#
Verification |
PUBLIC variant |
PRIVATE variant |
|---|---|---|
|
✓ |
✓ |
|
✓ |
✓ |
Symbols exist in target lib |
✓ all 3 found |
✗ only |
|
✓ |
✗ undefined symbol: |
|
✓ returns 10 (MIOpen 3.5.2) |
✗ never reached |
Wrapper hop observed (LD_DEBUG) |
3 plugin→wrapper + 263 wrapper→private |
n/a |
Negative test (hide private lib) |
fails with cannot-open (transitive) |
fails with cannot-open (direct) |
The wrapper path (PUBLIC plugin against flagon MIOpen) is the legacy/fallback case and works end-to-end. The wrapper-hop overhead is the ≈1 ns per call documented in §4.
The short-circuit path (PRIVATE plugin) selects the correct library at link time but its plugin source can’t call any API in that library because the consumer has no access to the rename mechanism. This is a gap in Phase 4’s wiring, not a bug in the test setup — every API call in the synthetic plugin is also present in the real plugin and would fail the same way.
The negative test confirms
libMIOpen_private.so.1is always loaded in a flagon install, even when only the public library is linked. Wrapper mode and short-circuit mode do not differ in which files are mapped, only in which library’s exports the plugin’s relocations bind to.
5.9 Plugin artifact list#
perf-results/plugin-test/synthetic_plugin.c,host.c,build.sh— sourcesperf-results/plugin-test/libsynth_plugin_{public,private}.so,host— built artifactsperf-results/plugin-test/ld_public.<pid>,ld_private.<pid>— raw LD_DEBUG bindings traces
Cross-cutting takeaways#
Pulled from the five sections above for use as the RFC’s evidence checklist:
Claim the RFC needs to support |
Evidence section |
|---|---|
The wrapper build produces the expected library split and links cleanly. |
|
Every public API call traverses the wrapper at runtime (no hidden bypass). |
|
The wrapper does not regress MIOpen correctness on the upstream gtest suite (4 case failures, all explained: 2 wrapper-aware test fixes needed, 2 environmental). |
|
Per-call CPU overhead of the wrapper is ≈1 ns (upper bound, on a no-work API); aggregate GPU work is statistically identical (≤2% phase-level deltas, signed direction varies). |
|
Cold-load overhead of the second ELF is +6 major page faults, ≤1 MB RSS, no detectable wall-clock change. |
|
The provider plugin’s CMake snippet correctly selects |
|
|
|
The Phase 4 short-circuit as currently wired produces a plugin that links but cannot be loaded, because the consumer-side rename mechanism is missing. Three remediation options are sketched in §5.7. |
§6 hipDNN redirect feasibility — Phase 2 forwarding prototype#
Source: hipdnn-redirect-investigation.md — ticket ALMIOPEN-1965 (“Investigation that MIOpen shim can successfully redirect to hipDNN”)
Where §1–§5 characterize the library split — does the wrapper build, link, run, and stay performance-neutral on the pass-through path — this section characterizes the forwarding path itself: can the wrapper actually redirect a MIOpen call into hipDNN and execute it end-to-end, and what does that cost. It is the first Phase 2 evidence; §1–§5 are Phase 1 evidence.
The redirect is entirely opt-in via environment variables and always falls back to native MIOpen on any failure, so default behavior is unchanged. Implementation lives in projects/miopen/src/private/wrapper.cpp.
Context (differs from the common context above)#
Hardware: 1× AMD Instinct MI308X (gfx942) — not the MI300X used in §1–§5
ROCm 7.14.60850, MIOpen 3.5.2
Native solution selected for the test conv:
ConvBinWinogradRxSf2x3g1(GPU kernel ~0.017 ms)
Feature flags (all off by default)#
Variable |
Effect |
|---|---|
|
Master switch; pairs a hipDNN handle to each MIOpen handle |
|
Routes |
|
Emits |
|
Emits HIP free/total memory around handle init |
Handle lifecycle#
miopenCreate / miopenCreateWithStream open a paired hipDNN handle via hipdnnCreate and stash it in an unordered_map<miopenHandle_t, hipdnnHandle_t>; miopenDestroy releases it via hipdnnDestroy. hipDNN open failure is non-fatal — the MIOpen handle is still returned (a stderr line is emitted; per RFC §4.4 this becomes a routing-policy decision in a later phase). miopenCreateWithStream also has a reverse-order init mode (hipDNN before MIOpen) to isolate init-order / memory effects.
Convolution forwarding — a worked argument-translation example#
miopenConvolutionForward dispatches to try_forward_conv_to_hipdnn when both the forwarding and conv flags are set. That helper builds an equivalent hipDNN backend descriptor convolution graph by hand (the same cuDNN-style API the handle path uses — no frontend dependency), finalizes an execution plan, caches it per problem shape, and executes through the paired handle.
The guards below are the operational definition of “fields with no clean hipDNN analog in this prototype” — each returns miopenStatusUnsupportedOp, which routes the call back to native MIOpen:
Unsupported case |
Wrapper behavior |
|---|---|
non-identity alpha/beta scaling |
|
|
|
spatial dim > 5 |
|
any hipDNN API failure |
|
The plan cache is an in-process unordered_map keyed on tensor dims/strides/types + conv params — process-lifetime only, not persisted to disk.
This is a concrete, end-to-end realization of the descriptor → hipDNN graph + variant-pack translation that the umbrella review’s concern #1 asked for as a worked example: it names exactly which descriptor fields have no 1:1 hipDNN mapping in this prototype (alpha/beta scaling, groups, > 5 spatial dims) and states what the wrapper does for them (decline and fall back). See the concern #1 entry in review-decisions-2026-06-08.md for how this bears on closing that concern.
Full field-by-field mapping (descriptor → hipDNN backend graph)#
The worked example above names the declined fields; this section records the full translation the prototype performs for the fields that do map, so the mapping is documented rather than living only in wrapper.cpp. The wrapper builds the graph from hipDNN backend descriptors (the cuDNN-style API — no frontend dependency), in this order:
Three
HIPDNN_BACKEND_TENSOR_DESCRIPTORs — one each for X, W, Y.One
HIPDNN_BACKEND_OPERATION_CONVOLUTION_FORWARD_DESCRIPTORreferencing the three tensors and carrying the conv parameters.One
HIPDNN_BACKEND_OPERATIONGRAPH_DESCRIPTOR(paired hipDNN handle + the single op).ENGINEHEUR→ENGINECFG→EXECUTION_PLAN(cached per problem shape; queried for workspace size).A
HIPDNN_BACKEND_VARIANT_PACK_DESCRIPTORbinding tensor UIDs to device pointers, thenhipdnnBackendExecute.
Fields that map 1:1 (miopenConvolutionForward(handle, alpha, xDesc, x, wDesc, w, convDesc, algo, beta, yDesc, y, workSpace, workSpaceSize)):
MIOpen input |
hipDNN backend target |
Notes |
|---|---|---|
|
paired |
Paired at |
|
|
Copied directly. |
|
|
MIOpen encodes layout (NCHW/NHWC/NCDHW/NDHWC) as strides; hipDNN takes dims+strides, so layout maps by copying strides — no separate layout enum. Vectorized layouts (NCHWc4/c8, CHWNc4/c8) have no plain dims+strides form → decline. |
tensor |
|
1:1: |
tensor identity |
|
Wrapper assigns stable UIDs (X/W/Y); these link the graph to the variant pack. |
|
|
MIOpen padding is symmetric (one array); set hipDNN pre = post = |
|
|
Direct. |
|
|
Direct. |
|
|
|
|
length of the four arrays above |
Supported for ≤ 5 (see decline table above). |
(derived) compute/accumulation type |
|
Derived from IO dtypes (e.g. fp32 accumulate for half/bf16). |
|
|
Pointers live in the variant pack, not the graph. |
Variant pack: UNIQUE_IDS = {X, W, Y}, DATA_POINTERS = {x, w, y}, WORKSPACE = wrapper-owned buffer sized from the execution plan’s HIPDNN_ATTR_EXECUTION_PLAN_WORKSPACE_SIZE.
Behavioral differences — fields that map but not faithfully (these are not declines; the call proceeds, but a consumer could observe a difference):
algo(miopenConvFwdAlgorithm_t) is ignored — hipDNN selects its own engine viaENGINEHEUR, so the caller’s algorithm choice is not honored.workSpace/workSpaceSizeare not reused — hipDNN computes its own workspace requirement and the wrapper owns that buffer; the caller-provided workspace is left untouched.
Why the declined fields have no mapping (the decline table above, with the underlying reason): alpha/beta ≠ identity — the hipDNN conv-forward op exposes no alpha/beta scaling attribute (there is no HIPDNN_ATTR_OPERATION_CONVOLUTION_FORWARD_ALPHA/BETA), so only α=1, β=0 is expressible; groupCount ≠ 1 — no group-count attribute on the hipDNN conv-forward op in this API; c_mode = miopenTranspose — deconvolution is not a forward-conv op (maps to backward-data); spatial dim > 5 — prototype boundary.
Status translation: any hipdnnStatus_t failure during build/finalize/execute currently returns miopenStatusUnsupportedOp (native fallback). Per RFC §6.1 this becomes a translated miopenStatus_t carrying the [hipDNN-forwarded] marker and exposed via miopenGetLastForwardedError(); reconciling the prototype’s blanket fallback with that design is RFC §7 Phase 2 task 6.
How it was tested#
Single conv repeated 100 iterations (-i 100) via MIOpenDriver:
MIOpenDriver conv -n 16 -c 16 -H 16 -W 16 -k 16 -y 3 -x 3 \
-p 1 -q 1 -u 1 -v 1 -l 1 -j 1 -F 1 -V 0 -i 100 -t 1
Four runs (FourRunComparison.txt): flag off then on, each run twice (cold process / warm process) to expose cross-process caching behavior.
Results#
Per-phase overhead (redirect on, from the [MIOpen->hipDNN] timers):
Phase |
Cold |
Warm |
|---|---|---|
|
~2.1 ms |
n/a (once per process) |
conv plan build |
1.3 ms – 3.9 s (highly variable) |
cached (0) |
conv execute |
~1.4 ms |
~27 µs / call |
|
~1 µs |
n/a |
Wall-clock (real), cold vs. warm process:
1st run (cold) |
2nd run (warm) |
|
|---|---|---|
Redirect off (native) |
4.860 s |
0.213 s |
Redirect on (hipDNN) |
4.863 s |
4.121 s |
Native MIOpen drops to 0.213 s on the second process because it reuses its persistent on-disk kernel cache. The redirect path has only an in-process plan cache, so every fresh process re-pays the cold plan build (dominated by the multi-second conv plan build), and warm-process wall time stays ~4 s. The warm per-call execute overhead itself is small (~27 µs).
Acceptance criteria#
Open/close of hipDNN handle in the shim — MET. Paired
hipdnnCreateon handle creation,hipdnnDestroyon destroy; cold create ~2.1 ms, destroy ~1 µs.Timing/benchmarking overhead, cold + warm — MET. Instrumented via
MIOPEN_HIPDNN_FORWARDING_TIMING; numbers above. The driver’s own GPU kernel timing does not flow through the redirect (it reports 0.000 ms / inf GFLOPs), so the shim’s env-var timers are the usable measurement path.Stretch: full operation + run via MIOpenDriver — LARGELY MET.
miopenConvolutionForwardis fully implemented over the hipDNN backend API and runs end-to-end throughMIOpenDriverwith[MIOpen->hipDNN]trace lines. Caveats: numerical verification was not enabled in these runs (-V 0), and hipDNN’s own internal logging was not captured (only the shim’s trace).
Concerns found#
No persistent cache for the redirect (highest impact). The hipDNN plan is cached per process only. Native MIOpen persists compiled kernels to disk and so collapses repeat runs to 0.2 s; the redirect re-pays cold setup every process and stays ~4 s. Cross-process persistence (or enabling/sharing hipDNN’s own kernel cache) is needed before the redirect is competitive on repeated invocations. This is the headline regression and the gating item for RFC §7 Phase 1 exit criterion 4 (cold-start vs warm-start) and Phase 4 perf.
Plan-build time is unstable (1.3 ms on one cold run, 3.9 s on another). The multi-second case implies kernel compilation / autotuning happening inside plan finalize on some runs; the attribution/trigger needs to be understood.
Driver benchmarking blind spot. GPU kernel time / GFLOPs report 0.000 / inf under the redirect, so standard driver perf reporting is unusable; only the shim timers work today.
Limited coverage. Conv forward only, identity scaling, group=1. Because the path falls back silently on any unsupported case, “redirect enabled” does not guarantee the redirect was actually taken — future work should surface when fallback occurs. (Note: this prototype’s silent fallback on any hipDNN failure is broader than the RFC §6.1 design, which propagates per-call hipDNN errors rather than absorbing them; Phase 2 must reconcile the two.)
Correctness not yet verified. These runs used
-V 0(no numerical verification), so the hipDNN path was only proven to execute, not to produce correct results. Follow-up: re-run with verification enabled (-V 1) before relying on the path.
Performance impact (summary)#
Per handle: +~2.1 ms cold create, +~1 µs destroy; negligible thereafter.
Per conv (warm): ~27 µs host-side overhead per call (native GPU kernel is ~17 µs; not directly comparable, but the redirect adds modest steady-state cost).
Cold start / repeated processes: dominated by the non-persisted plan build (up to ~3.9 s), keeping warm-process wall time at ~4 s vs. native 0.2 s. This is the headline regression for the redirect as currently implemented and the main item to resolve in the RFC.