MIOpen → hipDNN Forwarding Wrapper#
Contributors: Nolan Hanna, Mitch Ousdahl
Table of Contents#
1. Executive Summary#
Why#
Frameworks like PyTorch, TensorFlow, ONNX Runtime, and JAX/XLA already have substantial integration with MIOpen — call sites, tuning DBs, build infrastructure, CI coverage. There is external pushback against going all-in on a hipDNN backend at this juncture: the migration work for those framework teams is non-trivial, the timing is bad for several of them, and hipDNN is still maturing. At the same time, we want MIOpen consumers to start benefiting from hipDNN’s engine ecosystem (performance work, fusion graphs, new architectures) without waiting for that migration. A forwarding wrapper lets us route selected calls through hipDNN behind the existing MIOpen API, decoupling the rollout of hipDNN-backed execution from any framework-side rework.
What#
This RFC proposes a thin wrapper layer in front of MIOpen that preserves the existing MIOpen C API verbatim from the consumer’s point of view, while internally choosing — on a per-entry-point basis — whether to forward the call to the original MIOpen implementation or to hipDNN. The public/private library split is always built; behavior is governed at runtime by an env var that defaults to pure pass-through. With the default in effect, MIOpen behaves functionally as it does today: every call lands on the original MIOpen implementation, just via one extra function-call hop measured at ≈1 ns/call (§4.3.2). Opting in (per op family, via env var) routes selected calls to hipDNN instead. The split is:
MIOpen Private — the existing MIOpen implementation, with the public C entry points renamed in source to a
_implsuffix (e.g.miopenConvolutionForward_impl). Functionally identical to today’s library, just relocated behind a private symbol surface. Ships aslibMIOpen_private.so.MIOpen Public — a new lightweight shared library that re-exports the original public symbol names (e.g.
miopenConvolutionForward) and dispatches each call either to MIOpen Private or to hipDNN. Ships aslibMIOpen.so(the filename consumers link against today, SONAME preserved).
The wrapper reaches the private library via direct link-time linkage (libMIOpen.so → -lMIOpen_private). Because the public API and SONAME do not change, this work is invisible to existing MIOpen consumers — they continue to call miopenConvolutionForward and link against libMIOpen.so exactly as today. The forwarding decision happens behind that boundary.
hipDNN dependency posture#
Because forwarding is opt-in and off by default, libMIOpen.so links hipDNN weakly during the rollout window: an install that has MIOpen but not hipDNN loads and runs exactly as today, and only a consumer who turns forwarding on needs hipDNN present. Turning forwarding on when hipDNN is unavailable is a loud error, not a silent no-op. When the forwarding default eventually flips to enabled (§9), hipDNN becomes a hard runtime dependency. The wrapper also pins to a single hipDNN major version and uses only hipDNN’s stable base-graph API, so a consumer upgrading hipDNN underneath the wrapper cannot break linking. Full detail in §4.9.
Investigation status#
A first-cut Phase 1 prototype has been built and exercised; raw findings are catalogued in 0001a_InvestigationReference.md (referred to throughout this RFC as “the investigation reference”). At a high level, the prototype confirms that the public/private split builds, links, runs, and is performance-neutral; surfaces one wrapper-induced test breakage with a clear fix path; and exposes a wiring gap in the hipDNN MIOpen-provider plugin that this RFC now addresses in Phase 1 by installing a parallel _impl-form public header alongside the regular MIOpen headers. Specific findings are cited inline below (investigation reference §1–§5 are this Phase 1 evidence).
A separate, later Phase 2 forwarding prototype (investigation reference §6, ticket ALMIOPEN-1965) goes one step further and actually redirects miopenConvolutionForward into hipDNN end-to-end through MIOpenDriver, building the hipDNN backend-descriptor graph by hand. It establishes that the redirect path is viable and provides a concrete argument-translation worked example, and it initially surfaced what looked like a headline cold-start regression: a fresh process took ~4 s vs. native’s ~0.2 s warm-process wall time. Follow-up investigation has since re-attributed this to a one-time runtime load of the GPU device libraries on the forwarded path, not to a missing persistent plan cache. Because forwarding ultimately executes through the same MIOpen Private code, it shares MIOpen’s existing kernel cache — there is no double compilation and no per-process plan rebuild beyond that one-time device-library load. This substantially de-risks the concern relative to the original prototype reading; it remains a Phase 1 exit-criterion-4 measurement (§7) to confirm in the wrapper build, and is reflected in the risks table (§6). (The raw prototype numbers in investigation reference §6 predate this re-attribution and should be read together with it.)
Pivot summary#
The design pivoted away from an earlier draft that used a build-time CMake flag (MIOPEN_ENABLE_HIPDNN_WRAPPER) to gate the entire wrapper, and from a Phase 4 framing of the provider’s direct private linkage. The pivoted design (1) always builds the public/private split and gates routing behavior at runtime, (2) renames the public-API symbols directly in the MIOpen source (no -include macro header) so consumers and providers see a single, stable source representation, (3) installs the _impl-form header in Phase 1 so the hipDNN MIOpen-provider plugin can be rewired against libMIOpen_private.so early, and (4) moves the provider’s direct private linkage from Phase 4 into Phase 1. Full rationale, affected sections, concerns addressed, and new risks are documented in RFC-pivot-summary.md.
Phased approach#
The work is broken into four phases (full detail in §7). Phase 1 is gated on two prerequisites, both outside this RFC. First, the unmerged MIOpen layering refactor that lets the driver and tests stop depending on internal symbols (and therefore lets the same release disable the long-standing leakage of MIOpen internals into libMIOpen.so) must land first. Without it, the wrapper would bake today’s leaked-internals surface into its public contract and external consumers known to depend on those internals (e.g. MIGraphX) would have no clean migration window. Second, the hipDNN frontend — which the wrapper consumes to reach hipDNN — must gain a runtime (dlopen) backend-loading mode, because as a header-only library it otherwise forces a hard hipDNN-backend dependency onto libMIOpen.so that would violate §4.9’s weak-linkage posture even with forwarding off. See the §7 Phase 1 prerequisite notes and the corresponding risk rows in §6.
Pass-through wrapper + provider direct private linkage. Establish the two-library split, dispatch plumbing, and the consumer-side
_implheader install. Rewire hipDNN’s MIOpen provider to call MIOpen Private directly (link-time, againstlibMIOpen_private.so) so the loop hazard is structurally removed before any forwarding lands. Every public entry point still forwards to MIOpen Private. Validate that overhead is negligible.Selective hipDNN forwarding. Forward the convolution op family — the chosen first and, within this RFC’s scope, only forwarded op family — to hipDNN via the compile-time forwarding set when
MIOPEN_HIPDNN_FORWARDING=enabled. Convolution is already realized end-to-end by the Phase 2 forwarding prototype (investigation reference §6); subsequent op families (batchnorm likely next) are future work (§9).Env var and logging mapping. Translate MIOpen’s debug/tuning/logging surface to hipDNN’s, scoped to the variables that frameworks actually use in production.
Performance baselining. Publish end-to-end performance numbers across the wrapper-pass-through and wrapper-forwarding configurations now that the provider’s direct private linkage is already in place from Phase 1.
Open question — wrapper ownership. Long-term ownership of the wrapper layer (the new
MIOpen Publicartifact, the routing policy module, and the env-var translation work in Phase 3) is not yet assigned. Candidates include an integration team or the FDE function. Needs alignment with @BradPepersAMD before Phase 1 exit so that on-call rotations, bug triage, and the eventual default-flip decision (§9) have a clear DRI.
2. Problem Statement#
hipDNN is being positioned as the next-generation graph-execution surface for AMD’s deep-learning stack, but MIOpen still has a large installed base and a stable C API that frameworks (PyTorch, TensorFlow, ONNX Runtime, JAX/XLA, etc.) depend on directly. We want a path for MIOpen consumers to benefit from hipDNN’s engine ecosystem — performance work, new architectures, fusion graphs — without requiring those consumers to migrate their integration code or rebuild against a new header.
This wrapper is an explicitly temporary measure. The longer-term goal is for frameworks to consume hipDNN features directly so that we no longer need to add new functionality to MIOpen. Eventually MIOpen itself will be deprecated and the public API exposed by this wrapper will go away — though that endpoint is expected to be a long way off, is out of scope for this RFC, and is not blocked or driven by the work proposed here. In the meantime, the wrapper gives us a way to put framework calls in front of hipDNN without forcing framework teams to do migration work first.
Concretely, we want the ability to:
Redirect specific MIOpen API calls to hipDNN at runtime, on a per-entry-point basis.
Roll the redirection out incrementally — one operation, or one path within an operation, at a time — and roll it back just as easily.
Prove that the forwarding layer itself adds negligible overhead before we migrate any production traffic onto it.
Default the wrapper to a pure pass-through so consumers who don’t opt into hipDNN forwarding see today’s MIOpen behavior. Existing consumers (no env-var change, no code change) get today’s library semantics plus an ≈1 ns/call wrapper hop measured to be in the noise (§4.3.2).
Let hipDNN’s existing MIOpen provider bypass the wrapper to avoid the round-trip cost when hipDNN is already in the call stack — wired up in Phase 1 so the loop hazard is structurally removed before any forwarding lands.
What we are not trying to do in this RFC:
Change the MIOpen public API.
Rewrite MIOpen on top of hipDNN.
Deprecate or remove any MIOpen entry points.
3. Current System Overview#
MIOpen exposes a flat C API through include/miopen/miopen.h. Each public entry point is implemented in a corresponding *_api.cpp file under src/ (e.g. convolution_api.cpp, batch_norm_api.cpp, layernorm_api.cpp). The API layer translates handle/descriptor objects, builds a ProblemDescription, calls into the solver framework, and returns a miopenStatus_t.
Consumers link against libMIOpen.so and call these symbols directly. There is no shim layer between the public symbol and the implementation; miopenConvolutionForward in the consumer binary resolves to the function defined in convolution_api.cpp.
hipDNN already ships with a MIOpen provider plugin (dnn-providers/miopen-provider/) that calls into MIOpen the normal way. Today, the call direction is exclusively caller → MIOpen and hipDNN → MIOpen — never MIOpen → hipDNN.
Today’s call graph:

4. Proposed Design#
4.1 Runtime feature flag#
The public/private split is always built in every MIOpen configuration. Behavior is governed at runtime by an env var, tentatively MIOPEN_HIPDNN_FORWARDING (name TBD; default: disabled).
disabled(default): Every public entry point dispatches to MIOpen Private. The wrapper hop is ≈1 ns/call (§4.3.2), measured to be in the noise on any real op. Functionally equivalent to today’s MIOpen from the consumer’s perspective.enabled: The per-entry-point routing policy in §4.4 is consulted; selected ops are forwarded to hipDNN, others remain on Private. A companion env var (MIOPEN_DISABLE_HIPDNN_FOR) provides finer-grained opt-out control.
Why a runtime flag rather than the build-time CMake flag from earlier drafts:
One artifact to build, test, and ship. CI does not need to maintain a wrapper-off matrix in parallel with a wrapper-on matrix. The single artifact is the one that ships, which removes the divergence risk between configurations.
Operator-controlled rollback. Any consumer who hits a regression can set
MIOPEN_HIPDNN_FORWARDING=disabledwithout rebuilding or replacing the library. This is the practical recovery lever during the rollout window.Cleaner default flip. When the time comes (§9), changing the default is a one-line change to the env-var parser. Consumers who have already deployed the library pick up the new default on next process start; no rebuild required.
Same surface area either way. The public/private split, the rename, and the wrapper code are present in every build, so there is no “the wrapper-off path is the one that works because that’s the one anyone actually ships.” Every consumer is exercising the same code paths.
4.2 Symbol surface split (architecture)#
The public API symbols are split across two libraries — a thin Public wrapper and the existing implementation, which becomes Private. After Phase 1, the wrapper is a pure pass-through to Private by default; from Phase 2 onward the wrapper may also forward to hipDNN when the runtime env var opts in. The hipDNN MIOpen-provider plugin reaches MIOpen Private directly via link-time direct private linkage from Phase 1 onward (see §4.5 and the diagram in §7 Phase 1).
MIOpen Private: contains the entire current MIOpen library, with each public-API entry point renamed in source from
miopenFootomiopenFoo_impl. The rename is applied directly to the function definitions and to internal headers, so the existing implementation TUs compile against the_implnames. All other MIOpen symbols (internal helpers, solver classes, etc.) are unchanged.MIOpen Public: a new, small shared library. For each public entry point, it defines a wrapper that decides where to dispatch (Private vs. hipDNN, governed by §4.1) and forwards the arguments. The exported symbol set is identical to today’s
libMIOpen.so, so no consumer code change is required.
How the two libraries are physically packaged, named on disk, and bound together is the subject of §4.3.
4.3 Implementation#
The wrapper ships as a thin shared library that reuses the existing MIOpen .so filename; the implementation library is renamed.
4.3.1 Packaging and link surface#
Aspect |
Value |
|---|---|
Public wrapper artifact |
|
Private implementation artifact |
|
Symbol rename in Private |
each public entry point → |
Wrapper → Private binding |
direct link at build time ( |
Wrapper → hipDNN binding |
weak link during the opt-in rollout (missing hipDNN does not block |
Consumer impact |
none — same |
User runtime opt-in |
none for pass-through (default); env-var opt-in for hipDNN forwarding (§4.1, §4.4) |
The call path is a plain function call (one indirection), the wrapper is a normal library that can be packaged and shipped through the existing channels, and consumers don’t need to know the wrapper exists. The Phase 1 provider direct private linkage (§4.5) is a straightforward link-line change in dnn-providers/miopen-provider/CMakeLists.txt.
Investigation status (see investigation reference §1, §2, §4 for full data):
Confirmed viable. The prototype produces
libMIOpen.so.1as the wrapper andlibMIOpen_private.so.1as the implementation, with the wrapper holding 263 public exports and the private library holding 263_implsymbols (4 non-renamed survivors, none implicated in any collision).MIOpenDriverlinks and runs correctly against the split; gtest results show 261/265 binaries clean. The prototype used a-include miopen_private_rename.hmacro-rename mechanism, which has since been replaced in this design by a direct source rename (§4.6); the prototype’s symbol-surface findings still apply because the resulting symbol set is identical.Symbol-rename collisions. No collisions observed in the prototype against the renamed surface.
Still to be validated.
SONAME / SOVERSION inheritance, and a public-symbol-superset check: the wrapper-built
libMIOpen.somust export a superset of the public entry points implemented inlibMIOpen_private.so(everymiopenFoo_implin Private has a matchingmiopenFooexport in the wrapper) and must export no_implsymbols itself. This is derived from the two libraries in the same build, so it needs no pre-wrapper baseline snapshot. The prototype builds reuse.so.1; the formal CI gates listed in §6 and §8 are not yet wired up.Packaging implications for ROCm distributors: shipping a second
.soand ensuring both end up in the right paths.Coordinator: TBD (named owner required before Phase 1 exit). Stakeholders to contact before Phase 1 exit: TheRock superbuild maintainers, ROCm release-engineering, distro packagers (Ubuntu, RHEL, SUSE), container-image maintainers (
rocm/dev-*images), Conda-forge ROCm, and the framework wheel teams (PyTorch ROCm, TensorFlow ROCm). Sign-off must be captured in a tracking ticket. Each of these consumers has a build/packaging assumption that “MIOpen ships as one.so”; surfacing this late turns into a release-readiness blocker. Note: under the runtime-flag design, every ROCm release ships the split, so this coordination is unconditional rather than gated on a “wrapper-enabled distro” subset.Co-versioning invariant (load-bearing for packagers).
libMIOpen.so.<X>andlibMIOpen_private.so.<X>are produced from the same source tree, released together, and must be installed at the same version. Distros and other downstream packagers must package them together with an exact-version dependency, never split them into independently-upgradable packages, and never allow one to upgrade ahead of the other. This is the practical consequence of §4.7’s lockstep SOVERSION policy:libMIOpen.so’s hardDT_NEEDEDonlibMIOpen_private.someans a version-skewed install either fails at load (different SOVERSION → loader rejects the bind) or silently misbehaves (same SOVERSION but a wrapper-vN call lands on a private-vM_implwith mismatched signature). Quotable forms for packagers:RPM:
Requires: libMIOpen-private = %{version}-%{release}(exact-equals, not>=).dpkg / Debian:
Depends: libmiopen-private (= ${binary:Version}).Conda: pin
libmiopen-privateto the same=<version>=<build>aslibmiopenin the recipe.Wheels / single-archive bundles (PyTorch ROCm, TensorFlow ROCm, container images): both
.sofiles are bundled from the same release artifact set; no per-file substitution.
CI gate (new Phase 1 sub-task — see §7 Phase 1 task 8). Before a release artifact is built, CI extracts the embedded version metadata from
libMIOpen.so.<X>andlibMIOpen_private.so.<X>and fails the build if they disagree. Same gate also asserts the wrapper’sDT_NEEDEDentry for the private library resolves to the same<X>the wrapper was built at.
Interaction with the existing
MIOpen_with_pluginsCMake target. The proxy bundles two classes of consumer that reach past the public C API into MIOpen internals — the CK grouped-conv plugins and the internal-symbol-using tests — which must bindlibMIOpen_private.soonce the public wrapper stops re-exporting internals. The design for this is in §4.10 (CK plugins rewired toMIOpen_privatein Phase 1; driver and tests handled by the §7 layering-refactor prerequisite). What remains is the Phase 1 implementation and its validation (tasks 12–13), not an open feasibility question.
4.3.2 Findings from the prototype#
A prototype split build was stood up; full data is in the investigation reference (§1–§4). Headlines:
Build and link. The split configuration produces the expected
libMIOpen.so.1wrapper +libMIOpen_private.so.1implementation.MIOpenDriverand the gtest binaries link and run; smoke tests show identical solver selection and verification numerics between the split and a non-split baseline (investigation reference §1).Runtime path proof. With per-stub
fprintfinstrumentation, a singleMIOpenDriver convfp16invocation produced 45 wrapper-traversal lines on the split build and 0 on the baseline — every public call in the run went through the wrapper hop, no hidden bypasses (investigation reference §2).Test suite. 261 of 265 gtest binaries clean on the split build. Of the 4 case failures: 2 are wrapper-induced (
test_smoke_tuning_policyasserts on capturedMIOPEN_LOG_FUNCTIONstderr that now contains_implnames) and 2 are environmental (test_db_syncmissing a.kdbfile). 2 timeouts (test_lrn,test_soft_max) were progressing when the per-binary 1200 s budget fired — not failures (investigation reference §3). The wrapper-induced fix is tracked as a Phase 1 follow-up (see §7 Phase 1 task list).Performance. Aggregate GPU kernel time differs by ≤2% between configurations (signed direction varies by phase — i.e., noise); wall-clock is the same or slightly faster on the split build. Direct microbenchmark of the worst-case no-work API (
miopenGetVersion) puts the wrapper hop at ≈1 ns/call (upper bound) in steady state; cold-load cost is +6 major page faults and ≤1 MB RSS (investigation reference §4). The negligible-overhead premise that justifies the whole design is empirically supported.
Taken together, the prototype clears the feasibility bar for Phase 1. Outstanding items (SONAME/SOVERSION CI gates, packaging coordination) remain as noted in §4.3.1; the MIOpen_with_plugins interaction is resolved by the design in §4.10; and the wiring gap that blocks the provider’s direct private linkage is addressed in §4.5.
4.3.3 Considered alternative: dlopen/dlsym + LD_PRELOAD#
An earlier draft proposed a dynamic-loader-based wrapper (dlopen("libMIOpen.so") + dlsym("miopenFoo"), opted in via LD_PRELOAD) as a fallback in case the public/private split turned out to be infeasible. Investigation found that the dynamic-loader path carries real correctness, operational, and portability costs that the direct-linkage design does not — symbol resolution ordering with RTLD_NEXT, static-initializer ordering, fork/exec safety of cached function pointers, thread-safety of first-call resolution, lack of link-time validation of the hipDNN ABI, opaque static-analysis (nm/ldd/readelf -d don’t see hipDNN as a dependency), environment-dependent library discovery (LD_LIBRARY_PATH/RPATH/ld.so.cache), POSIX-only API (Windows needs a separate LoadLibrary shim), and a function-pointer indirection on every forwarded call that defeats inlining. The previously open edge cases — RTLD_DEEPBIND interactions, static-linked consumers where LD_PRELOAD does not interpose, setuid binaries where the loader strips LD_PRELOAD — each compound that surface area.
Because the prototype (§4.3.2) cleared the split-build path’s feasibility bar, the dynamic-loader path is not pursued. It is recorded here so that if a future hard blocker forces a revisit, the prior analysis is available rather than re-derived from scratch.
4.4 Per-entry-point routing policy#
Each wrapper function consults a routing decision: “should this call go to hipDNN or to MIOpen Private?” In Phase 1 the answer is always “Private”. As later phases add hipDNN coverage, the policy becomes more nuanced.
Routing inputs that the policy may consider:
The entry point itself (some are forwarded, some are not).
Argument shape (e.g. forward only certain layouts/dtypes).
A compile-time list (the forwarding set) for entry points that are known-good and forwarded when
MIOPEN_HIPDNN_FORWARDING=enabled. This list is owned and updated by MIOpen developers as each op is qualified in Phase 2 and beyond; consumers do not opt individual ops in at runtime, they get whatever the compile-time list says when they enable forwarding.An opt-out environment variable (
MIOPEN_DISABLE_HIPDNN_FOR=...) so that a single op family can be force-routed back to MIOpen Private without rebuilding. This is the recovery lever when a forwarded op misbehaves in production and is the documented rollback path called out in §9 (the “loud” default-flip communication promises this knob will exist).MIOPEN_DISABLE_HIPDNN_FOR=*disables forwarding for every op while leavingMIOPEN_HIPDNN_FORWARDING=enabledin effect.
The routing decision is centralized in one place (a small routing.cpp or equivalent) so that adding/removing a forwarded op is a one-line change.
Why no opt-in env var. An earlier draft also proposed an opt-in env var (
MIOPEN_USE_HIPDNN_FOR=<ops>) so that operators could ad-hoc-enable individual ops without rebuilding. We dropped it: the compile-time forwarding set already gives MIOpen developers the surface they need to stage ops, and combining an opt-in env var with the opt-out env var creates interaction-rule complexity (“which wins when both are set, and why”) that does not pay for itself. The opt-out alone gives consumers everything they need for emergency rollback, which is the only runtime knob the rollout actually depends on.
4.5 hipDNN MIOpen-provider direct private linkage (Phase 1)#
Today, hipDNN’s MIOpen provider plugin calls into libMIOpen.so. Once the wrapper exists, that call would land in MIOpen Public, which from Phase 2 onward might in turn forward back into hipDNN — a loop that, even if guarded against actual recursion, adds an unnecessary hop. The provider is therefore wired to call MIOpen’s private API directly in Phase 1, before any forwarded ops land, so the loop hazard is structurally removed rather than relied on a thread-local recursion guard during the rollout window.
The MIOpen provider is changed to link directly against libMIOpen_private.so and call the _impl symbols. This routes hipDNN-originated calls straight to the private implementation when hipDNN is the original caller, so the wrapper only sits in the path when an external consumer (e.g. PyTorch) called MIOpen.
Wiring gap from the investigation, and the fix. The provider’s CMakeLists already conditionally links MIOpen_private when that target is exported, and the prototype confirms the link selection works as intended — but the resulting plugin links and then fails to load because the provider’s translation units #include <miopen/miopen.h> and therefore reference the unrenamed names (miopenCreate), while libMIOpen_private.so.1 only exports _impl-suffixed names (miopenCreate_impl). The dlopen step aborts at the plugin’s first MIOpen relocation. Additionally, libMIOpen.so.1 carries a hard DT_NEEDED on libMIOpen_private.so.1, so the install always loads both files transitively — direct private linkage changes which exports bind, not which files load. Full evidence in investigation reference §5.
The chosen fix (selected from the three options sketched in investigation reference §5.7) is to install a parallel _impl-form public header, <miopen/miopen_impl.h>, alongside the regular <miopen/miopen.h>. The header declares the same API surface but with each function declared under its _impl name. The exported MIOpen_private CMake target carries INTERFACE_INCLUDE_DIRECTORIES pointing at the install location of this header, so any consumer that links MIOpen_private automatically gets the _impl declarations on its include path. The MIOpen provider plugin opts in by including <miopen/miopen_impl.h> in place of <miopen/miopen.h> for the TUs that talk to MIOpen Private.
Why this option over the alternatives:
Option 1 from §5.7 (install the macro-rename header + auto-apply
-DMIOPEN_BUILDING_PRIVATE -include miopen_private_rename.hvia theMIOpen_privatetarget) carries the same fragility surface called out in earlier drafts: any TU that mentions amiopenFoosymbol as a string (debug-tracing macros, logging metadata, embedded format strings) gets silently rewritten, and the forced-include order is load-bearing. Acceptable inside MIOpen’s own build where the surface area is auditable; not acceptable to push onto consumers.Option 3 (export private-side aliases so
libMIOpen_private.so.1publishes bothmiopenFooandmiopenFoo_impl) re-creates the exact risk the split was trying to remove — the private library becomes reachable via the public name surface, and any future static-analysis or symbol-leak check has to special-case it.
The parallel _impl header keeps the public include path untouched, keeps macro rewriting out of consumer TUs, and keeps the private library exporting only _impl names. The cost is one extra installed header file and a small amount of conditional compilation in the MIOpen provider.
Phase 1 verification. The rewired provider plugin must dlopen and run correctly against libMIOpen_private.so, with its MIOpen calls resolving to the _impl symbols in the private library rather than to the wrapper. This is covered functionally by the provider tests (§7 Phase 1) and statically by the provider-side object-file check in §4.7 (built object files reference only _impl symbols from libMIOpen_private.so).
4.6 Header story#
Three concerns the header story has to satisfy:
The public header
<miopen/miopen.h>is unchanged. Consumers see the same declarations, the same names, and the same include path as today.MIOpen Private’s source compiles against the
_implnames directly. The public-API function definitions insrc/*_api.cppare renamed in source frommiopenFootomiopenFoo_impl, and the internal private-side declarations they consume are likewise renamed. There is no-includemacro header, no forced rewrite at preprocess time, noMIOPEN_BUILDING_PRIVATEdefine. The renamed names are what is actually in the source tree.Consumers that need to call the
_implnames (the MIOpen provider plugin, §4.5) get a parallel<miopen/miopen_impl.h>header that declares the API in_implform. This header is installed alongside<miopen/miopen.h>and exposed viaMIOpen_private’sINTERFACE_INCLUDE_DIRECTORIES, so any consumer that linksMIOpen_privatepicks it up automatically.
Why direct source rename rather than the prototype’s macro-include mechanism. The prototype used -DMIOPEN_BUILDING_PRIVATE -include miopen_private_rename.h to #define each miopenFoo to miopenFoo_impl across every Private-side TU. The prototype showed that the mechanism works — 263/263 entry points renamed cleanly with no observed collisions (investigation reference §1, §5) — but the macro approach carries two fragility modes that direct source rename does not:
Any third-party header included in the same TU that mentions a
miopenFoosymbol as a string (debug-tracing macros, logging facility metadata, embedded format strings) gets silently rewritten.The forced-include order is load-bearing; any TU that subverts it picks up unrenamed signatures and confusing link errors.
Neither surfaced in the prototype, but with the direct rename the surface area collapses to zero: there is no rewrite happening, so neither fragility mode is reachable. The cost is the one-time edit to rename the public-API function definitions in the source tree; that edit is mechanical (the public API surface is enumerated in <miopen/miopen.h>) and the resulting source is what every reader of MIOpen Private’s source from that point onward sees, which is its own readability win.
<miopen/miopen_impl.h> generation and maintenance. The parallel _impl header is generated from <miopen/miopen.h> at build configuration time (or hand-maintained with a CI check that the two stay in sync — pick deferred to implementation), so it cannot drift from the public API. Including it from inside MIOpen’s own build is forbidden (the rename is already applied in source, and re-including would only add confusion); a CI check asserts no MIOpen TU includes the _impl header.
Invariant: the _impl header is not in MIOpen’s normal public include path. It is installed under the same prefix but is only added to a consumer’s include path via the MIOpen_private target. Consumers that find MIOpen (the public target) get only <miopen/miopen.h>. This keeps the _impl names invisible to consumers who have no business calling them. The leakage check from §6 (nm of libMIOpen.so shows no _impl exports, and no _impl appears in the staged public headers) catches violations.
4.7 MIOpen_private ABI policy#
Once the hipDNN MIOpen-provider plugin links libMIOpen_private.so.1 and calls _impl symbols directly (§4.5), MIOpen_private stops being a build-time-only convenience and becomes a stable, versioned ABI consumed by a separate build artifact — the plugin lives at dnn-providers/miopen-provider/ in this same rocm-libraries monorepo (a sibling location to MIOpen at projects/miopen/ and to hipDNN core at projects/hipdnn/), but its build, install, and release artifact are distinct from MIOpen’s. This section states the SOVERSION policy, the version-skew matrix, and the runtime behavior on mismatch so the plugin team has something to build against and packagers have something to pin.
SOVERSION policy: lockstep with libMIOpen.so.
libMIOpen_private.soSOVERSION bumps in lockstep withlibMIOpen.soSOVERSION, on the same release cadence, out of the same source tree. They are never released independently. The two libraries’<X>and<Y>in.so.<X>.<Y>always match.The SOVERSION bumps whenever either (a) the public C API in
<miopen/miopen.h>has an ABI-incompatible change (the existing MIOpen policy) or (b) any_implsignature in<miopen/miopen_impl.h>has an ABI-incompatible change. Because the two headers are kept in sync (§4.6), in practice these reduce to the same trigger.Adding new entry points (e.g.
miopenGetLastForwardedError()from §6.1) is an additive change and does not bump SOVERSION — same policy the public library already follows.
Version-skew matrix for {wrapper, private, provider plugin}.
Scenario |
Behavior |
|---|---|
Wrapper vN, Private vN (in-tree case) |
Supported, the only configuration MIOpen CI exercises directly. |
Wrapper vN, Private vM (M ≠ N) |
Unsupported. Concern 5’s packaging-coupling invariant (§4.3.1) makes this not-occur in correctly-packaged installs. If it does occur (broken package split, manual install error), the loader will surface it as either a missing |
Provider plugin built against Private vN, loads Private vN |
Supported, the configuration the in-tree provider tests exercise. |
Provider plugin built against Private vN, loads Private vM (M > N, same SOVERSION) |
Supported. New |
Provider plugin built against Private vN, loads Private vM (different SOVERSION) |
The dynamic loader rejects the bind at plugin |
Cross-project coordination. The MIOpen-provider plugin at dnn-providers/miopen-provider/ and hipDNN core at projects/hipdnn/ both live in the same rocm-libraries monorepo as MIOpen itself — not in separate repositories. The three projects share a source tree but have distinct build targets, distinct install artifacts, and (potentially) distinct downstream packaging and release-tag cadences. The lockstep SOVERSION policy bounds the coordination cost: the plugin only has to rebuild against a new Private SOVERSION when it bumps, on the same trigger it would already rebuild against a new public-MIOpen SOVERSION. The monorepo layout makes build-time coordination easy (all three projects’ sources move together; CI can rebuild the plugin against MIOpen Private in the same pipeline that bumped the SOVERSION) but does not by itself solve deployment-time skew, since downstream packagers may still split the installs into separate packages — the co-versioning packaging invariant in §4.3.1 covers that side. The investigation reference §5.3 records one prior occurrence of the skew failure mode (a TheRock-bundled pre-built provider linked against an older MIOpen install that lacked MIOpen_private); after this policy lands, the same situation surfaces at plugin dlopen time with a clear SOVERSION-mismatch message rather than a missing-target build failure.
Internal symbols visible to MIOpen_private consumers. The provider plugin links libMIOpen_private.so directly, which means the loader resolves any symbol the plugin references against Private’s full export set — not just the _impl C API. The _impl rename only covered the public-API surface; MIOpen’s internal C++ symbols (solver classes, helpers) remain exported by libMIOpen_private.so exactly as they are by today’s libMIOpen.so. The provider plugin is expected to consume only the _impl C API declared in <miopen/miopen_impl.h>; reaching into Private’s internal C++ symbols is unsupported and is covered by the same internal-symbol-leakage cleanup the Phase 1 prerequisite (§7) addresses. A CI check on the provider side asserts that its built object files reference only _impl symbols from libMIOpen_private.so. (The CK grouped-conv plugins are a deliberate internal-symbol consumer — unlike the provider plugin they legitimately bind Private’s internal C++ types — and are handled separately in §4.10.)
4.8 Tuning DB behavior under forwarding#
MIOpen maintains on-disk tuning state (.kdb kernel databases, find-db, perf-db) that is written and read by the public API. When the wrapper forwards an op to hipDNN, that state interacts with a backend (hipDNN) that did not produce it. This section states the chosen behavior, the rollback implication, and the compatibility commitment for pre-wrapper on-disk files.
Chosen behavior: bypass. When an op is forwarded to hipDNN, the wrapper does not consult MIOpen’s tuning DB for that call, and the hipDNN execution does not write back into MIOpen’s tuning DB. Any pre-existing tuned entries for that op in .kdb / find-db / perf-db are silently inert for the duration the forwarding flag is on. hipDNN’s own internal tuning (if any) lives entirely on the hipDNN side at hipDNN-managed paths; the two DBs do not interact.
Calls that fall through to MIOpen Private (forwarding disabled for the call, or the op not in the forwarding set) consult and update the MIOpen tuning DB exactly as today. The wrapper does not change the read/write semantics on the MIOpen-Private side.
The wrapper does not introduce any new on-disk format or new paths under MIOpen’s tuning-DB roots.
Rollback implication. Because forwarded calls do not write to MIOpen’s tuning DB, flipping MIOPEN_HIPDNN_FORWARDING=disabled (or unsetting it) returns control to MIOpen Private with the same tuning DB state it had before forwarding was enabled — modulo entries written by non-forwarded calls during the forwarding window. There is no “two diverging DBs at different versions” failure mode to clean up. An operator who flips forwarding off after a rollout regression sees their original tuned entries take effect immediately on the next call, with no cache or file-system reset needed.
Pre-wrapper .kdb compatibility. .kdb files written by pre-wrapper MIOpen remain readable by the wrapper-shipped MIOpen Private without modification. The split into Public + Private libraries does not change the on-disk format, the file paths, or the kernel-database schema — MIOpen Private inherits MIOpen’s existing tuning-DB code unchanged. Phase 1 exit criterion test_db_sync (which already exists and exercises .kdb loading) covers this assertion; no new test is required.
Out of scope. Passing MIOpen tuning hints into hipDNN as a translated “configuration suggestion” is not pursued in this RFC. The two backends’ tuning representations are structurally different and there is no hipDNN-side surface today for accepting MIOpen-shaped hints; revisiting this is left to a future RFC if forwarding becomes the long-term path for an op family.
4.9 hipDNN dependency posture and API-version pinning#
This section states how libMIOpen.so depends on hipDNN at link and load time, how that posture changes when the forwarding default flips (§9), and how the wrapper pins to a hipDNN API version so that version skew on the installed hipDNN cannot break the wrapper. It resolves the two questions left open in earlier drafts: “is hipDNN now a mandatory dependency?” and “what happens when the installed hipDNN is present but the wrong version?”
Linking posture: weak now, hard at the default-flip.
During the rollout (default
disabled): weak linkage.libMIOpen.sodoes not carry a hardDT_NEEDEDonlibhipdnn_backend.so. The hipDNN backend symbols the wrapper forwards into are referenced weakly, so an install that has MIOpen but not hipDNN still loads and runslibMIOpen.sonormally — every call lands on MIOpen Private exactly as today. This is the deliberate choice over option 7(a)’s “hipDNN is mandatory from day one”: the overwhelming majority of installs during the rollout window will never turn forwarding on, and forcing a hard hipDNN dependency on all of them just to enable an opt-in feature would be a gratuitous packaging disruption. Weak linkage here is distinct from thedlopen-as-wrapper-mechanism rejected in §4.3.3 — that rejection was about how the wrapper reaches MIOpen Private; this is about how the wrapper optionally reaches the hipDNN backend. Achieving this weak posture depends on a hipDNN-side prerequisite: because the wrapper reaches hipDNN through hipDNN’s header-only frontend, which today hard-links the backend and references its C-API symbols directly, the frontend must first gain a runtime (dlopen) backend-loading mode beforelibMIOpen.socan consume it without inheriting a hardDT_NEEDEDonlibhipdnn_backend.so— see the §7 Phase 1 hipDNN-frontend prerequisite note.Enabling forwarding without hipDNN present is a hard, loud error. If a consumer sets
MIOPEN_HIPDNN_FORWARDING=enabledon a system where the weakly-linked hipDNN backend symbols did not resolve, the wrapper does not silently degrade to pass-through. It emits a clear diagnostic (e.g. “hipDNN forwarding requested but libhipdnn_backend.so is unavailable”) and fails the affected call(s), so the misconfiguration is obvious rather than masked as “forwarding silently did nothing.” Pure pass-through with the flag left atdisabledis unaffected and never touches hipDNN.At the default-flip (§9): hard dependency. When
MIOPEN_HIPDNN_FORWARDINGdefaults toenabledfor a validated op set,libhipdnn_backend.sobecomes a hard runtime requirement oflibMIOpen.so— the weak references are promoted to a normalDT_NEEDED, and an install that lacks hipDNN fails to loadlibMIOpen.soat process start. This is acceptable at that point precisely because the flip is gated on hipDNN being a shipped, validated part of the stack; it is not acceptable during the opt-in rollout window. Adding the hard packaging dependency in the same release that flips the default is part of the successor “default-flip” RFC’s deliverables (§9).
API-version pinning: link against hipDNN’s stable base-graph API.
The wrapper is tied to a single hipDNN major version and deliberately uses only hipDNN’s stable base-graph surface (the “V1.0” frontend API), not cutting-edge backend features.
The wrapper compiles against the stable base-graph API only; backend symbols outside that subset are excluded from compilation, so they cannot become link-time dependencies. The practical effect: “we only use the base graph at the V1.0 API; every other backend symbol is removed from compilation, so no link errors can happen even when swapping a V2 hipDNN backend down to V1.” There are no references to newer-than-V1.0 symbols that could fail to resolve, so the wrapper can link and run against an older hipDNN backend even when a newer one is installed (and vice-versa within the supported range).
Pinning to the stable subset is what makes version skew tractable: because the wrapper never reaches for a symbol that exists only in a newer hipDNN, a present-but-different-version hipDNN does not break the wrapper as long as the stable base-graph API is intact across that range. Adopting any newer hipDNN feature is a deliberate, separately-versioned decision — it does not happen implicitly when a consumer upgrades hipDNN underneath.
Runtime behavior on a present-but-wrong hipDNN version. Resolved against hipDNN’s own versioning model (hipDNN RFC 0005_Versioning.md); see the confirmation note below.
Primary mechanism — init-time version query (matches hipDNN’s compatibility model). hipDNN handles version differences by dynamic version query + graceful failure, not by SONAME-based loader rejection: the backend exposes a runtime version-reporting API (hipDNN RFC 0005 §4.5, §7.2) and consumers are expected to query it and degrade gracefully on an unsupported version (§4.6.3). The wrapper follows that model — at init it queries the hipDNN backend’s reported version and asserts the major matches the one it was built against; an unexpected major is treated as unavailable for forwarding (pass-through when
disabled, loud error whenenabled, §6.1). Because hipDNN does not commit to bumpinglibhipdnn_backend.so’s SONAME per major, this query — not the loader — is the reliable gate against a silently-incompatible major, so it is load-bearing and is kept.Within-major skew is safe by hipDNN guarantee. Within a single major, hipDNN guarantees the consumer-facing API is forward-compatible and that minor releases only add — nothing is removed (RFC 0005 §4.2 “Minor version”, §4.7). The wrapper pins to the base V1.0 API precisely to stay in that guaranteed direction: an older consumer (built against 1.0) running against an equal-or-newer 1.x backend is exactly hipDNN’s forward-compatible case. (hipDNN’s backward compatibility — a newer consumer against an older backend — is still future work in RFC 0005 §4.6.3, a further reason to pin to the oldest API in the major rather than a newer minor.)
SONAME / weak-link behavior (secondary). A missing or unresolved
libhipdnn_backend.sois handled by the weak-link posture above (pass-through whendisabled, loud error whenenabled; hard load failure post-flip). This catches “absent” cleanly; it is not relied upon to catch “present-but-wrong-major,” which is the version query’s job.
Confirmed against hipDNN RFC
0005_Versioning.md(2026-06-16).
(b) stable API preserved within a major — confirmed. Minor versions add only, never remove, and the consumer-facing API is forward-compatible within a major (§4.2, §4.7).
© queryable runtime version — confirmed. The backend is a dynamic-version-query component and “add api functions for reporting library version” is planned work (§4.5, §7.2); the frontend already places dynamic version guards that fail gracefully on unsupported backend versions (§4.6.3).
(a) SONAME bumped per major — not committed. RFC 0005 does not specify ELF SONAME behavior and instead builds cross-version handling on dynamic version queries plus graceful failure. Consequence: the wrapper must not rely on the loader to reject a wrong-major hipDNN; the init-time version query is the load-bearing gate (resolving the earlier decision gate in favor of keeping it).
Terminology. In hipDNN’s component model the runtime
.sothe wrapper depends on is the backend (the C API,libhipdnn_backend.so;hipdnn_backend+hipdnn_backend_privateshare one version, §4.6.2); the header-only frontend is compiled into the wrapper. “V1.0 API” here means hipDNN major-version-1’s stable backend C API. All hipDNN components share a single major version (§4.2), so “the pinned hipDNN major” is unambiguous.
4.10 MIOpen_with_plugins and internal-symbol consumers#
MIOpen_with_plugins (src/CMakeLists.txt) is an INTERFACE target that links MIOpen and adds build-order dependencies on the CK grouped-conv plugin shared libraries (MIOpenCKGroupedConv_*.so). MIOpen’s own internal executables — MIOpenDriver, the gtest binaries, the legacy Boost.Test binaries, and the speedtests — link this proxy instead of MIOpen directly, so that building any one of them also builds the CK plugins it dlopens at runtime. When CK is disabled the proxy is a plain pass-through to MIOpen.
The proxy itself is not the concern: target_link_libraries(MIOpen_with_plugins INTERFACE MIOpen) keeps pointing at whatever MIOpen resolves to — after the split, the public wrapper — and the build-order dependency on the CK .so files is orthogonal to the symbol split. The concern is the two classes of consumer the proxy bundles that reach past the public C API into MIOpen’s internal C++ symbols (ProblemDescription, ConvSolution, InvokerFactory, etc., pulled in via src/include). Once libMIOpen.so becomes the thin public wrapper that exports only the public C API, those consumers can no longer resolve the internals against it — they must bind libMIOpen_private.so.
1. CK grouped-conv plugins. Each MIOpenCKGroupedConv_*.so links MIOpen and includes MIOpen’s internal headers (src/ck_impl/CMakeLists.txt) for the shared solver types. After the split those are internal C++ symbols that live only in libMIOpen_private.so. The plugins are therefore rewired to link MIOpen_private directly — the same direct-private-linkage seam the hipDNN provider uses (§4.5), except the CK plugins consume internal C++ symbols rather than the _impl C API, so they do not include <miopen/miopen_impl.h>. This is in-scope Phase 1 work (§7 Phase 1 task 12). The §4.7 ABI note already establishes that libMIOpen_private.so keeps exporting these internal symbols exactly as today’s libMIOpen.so does, so the plugins’ needs are met without any new export contract.
2. Internal executables (driver and tests). These are handled by the layering refactor that §7 already names as a Phase 1 prerequisite:
MIOpenDriver is refactored to drop its private-symbol dependency entirely and becomes a pure public-API consumer, so it links the public wrapper cleanly.
The gtest / ctest binaries that genuinely need internal access are moved into the private layer by the refactor and continue to link
libMIOpen_private.so, retaining their internal-symbol access.As this RFC is implemented, the tests that need no private access are moved back onto the public surface. These public-API tests are precisely the ones used to exercise forwarding flag-on vs flag-off behavior through the public API (
MIOPEN_HIPDNN_FORWARDING=disabledvsenabled), since by construction they touch only the public surface the wrapper routes. This relayering is §7 Phase 1 task 13.
Net effect: after the prerequisite refactor and the Phase 1 CK-plugin rewire, no consumer bundled by MIOpen_with_plugins depends on internal symbols re-exported by the public wrapper — the CK plugins and the internal-needing tests bind libMIOpen_private.so, while the driver and the public-API tests bind the wrapper. The MIOpen_with_plugins interaction that earlier drafts left open (§4.3.1) is resolved by these two moves rather than remaining a feasibility unknown.
5. Key Design Decisions#
Each decision below ties back to one or more bullets in the problem statement (§2). If a decision can’t be tied back, that’s a signal either to drop the decision or to widen the problem statement.
5.1 Split into Public + Private libraries instead of a single library with two symbol sets#
Ties to: “Roll the redirection out incrementally … and roll it back just as easily” and “Eventually let hipDNN’s existing MIOpen provider bypass the wrapper.”
A single shared object that exports both miopenConvolutionForward and miopenConvolutionForward_impl would also work, but a split keeps the dependency direction one-way, which is what the incremental-rollout and provider-bypass goals depend on.
Pros |
Cons |
|
|---|---|---|
Split (chosen) |
Explicit one-way dependency: Public depends on Private; Private has no knowledge of the wrapper. hipDNN’s provider can link Private directly (Phase 4). Wrapper symbol footprint stays small and inspectable. Wrapper can be stripped/optimized independently of MIOpen proper. |
Two artifacts to package and ship. Slightly more build-system complexity. |
Single |
One artifact. No SONAME juggling. |
The |
5.2 Runtime flag instead of a build-time flag#
Ties to: “Default the wrapper to a pure pass-through so consumers who don’t opt into hipDNN forwarding see today’s MIOpen behavior” and “Roll the redirection out incrementally … and roll it back just as easily.”
An earlier draft used a build-time CMake flag (MIOPEN_ENABLE_HIPDNN_WRAPPER, default OFF) to gate the entire mechanism. The flag-off build was bit-identical to today’s MIOpen. The pivot replaces that with a runtime env var that defaults to disabled; the public/private split, the rename, and the wrapper code are always built. The justification for the change is that the build-time approach had three downstream problems that compounded over the rollout window:
CI doubled. Maintaining a wrapper-off and a wrapper-on matrix in parallel was expected to add ~4–6.5 hours per full-arch pipeline run, based on the most recent successful MIOpen CI build (~6 hours) and TheRock Release portable Linux package builds (~4–6.5 hours). Parallelizing the matrix kept wall-clock flat but doubled compute.
Configuration divergence risk. Two artifacts means two code paths that can diverge. The wrapper-off artifact is the one anyone runs in production until the default flips, so it becomes the artifact that “is supposed to work,” and the wrapper-on artifact gets exercised less. The split incentive structure is exactly the wrong one for catching regressions in the path that is supposed to eventually become the default.
Default flip becomes a packaging migration. Flipping the default under the build-time scheme requires every downstream distro/wheel/container to rebuild and re-publish before consumers see the new behavior. Under the runtime scheme it is a one-line change to the env-var parser; consumers pick up the new default on the next process start with no rebuild.
Pros |
Cons |
|
|---|---|---|
Runtime flag (chosen) |
Single artifact: one build to test, one binary to ship. Operator-controlled rollback ( |
Wrapper is always present in the binary and in the call path; consumers cannot get a literally byte-identical-to-today’s-MIOpen artifact. Mitigation: the ≈1 ns/call wrapper hop is in the noise on any real op (§4.3.2); a public-symbol-superset check (§6, §7 Phase 1 task 8) asserts the wrapper exports every public entry point the private library implements (and no |
Build-time flag |
Flag-off artifact is bit-identical to today’s MIOpen; clean A/B comparison for perf regression detection. |
Doubled CI, configuration divergence risk, packaging migration on default flip. The bit-equivalence guarantee was load-bearing in the prior draft but is replaced here by the wrapper-hop overhead measurement plus the public ABI check. |
5.3 Direct linkage rather than a dlopen/dlsym + LD_PRELOAD wrapper#
Ties to: “Prove that the forwarding layer itself adds negligible overhead” and “Let hipDNN’s existing MIOpen provider bypass the wrapper.”
The chosen design ships libMIOpen.so as a wrapper that link-time depends on libMIOpen_private.so and (weakly, during the opt-in rollout — §4.9) on libhipdnn_backend.so. The dispatch is a plain function call. An earlier draft kept a dlopen/dlsym + LD_PRELOAD design as a fallback in case the direct-linkage path turned out to be infeasible; the prototype (§4.3.2) cleared the feasibility bar, so the dynamic-loader fallback is dropped from the live design and documented in §4.3.3 as a considered alternative.
Why direct linkage:
No user-side opt-in. No
LD_PRELOADto set, no per-process initialization to remember.Plain function-call dispatch. ≈1 ns/call upper bound (§4.3.2); easiest to argue is overhead-free.
Provider direct private linkage is a link-line change (§4.5).
SONAME is preserved, so consumers see no change.
Link-time ABI validation. hipDNN ABI breaks surface as build failures in CI rather than
dlsymreturningNULLdeep inside a production workload. (Weak linkage during the rollout — §4.9 — preserves this build-time validation against the hipDNN the wrapper was built against; it only changes the runtime-missing case from a hard load failure into a clean fall-through.)Works the same on Windows and Linux (modulo SONAME / DLL naming) — no
LoadLibraryshim.
The downsides of direct linkage are limited to the build-system mechanics for producing a split (renaming the implementation library, packaging two .so files), which the prototype confirmed are tractable. See §4.3.3 for the full catalog of why the dynamic-loader path was not pursued.
5.4 Phase env vars and logging separately#
Ties to: “Roll the redirection out incrementally — one operation, or one path within an operation, at a time.”
MIOpen has a substantial set of debug / tuning / logging environment variables and a logging subsystem with its own conventions. hipDNN has a different set. Mapping these requires care — some MIOpen knobs have no hipDNN analog and vice versa, some affect the call path before any forwarding decision can be made, and some affect kernel selection inside MIOpen Private in ways the wrapper can’t influence. Gating the forwarding work on resolving the mapping would block incremental rollout. Phase 2 explicitly defers it (by only forwarding ops insensitive to env vars), Phase 3 takes it on as the focus.
Pros |
Cons |
|
|---|---|---|
Phase separately (chosen) |
Forwarding rollout starts immediately on insensitive ops. Env-var mapping gets the design attention it deserves rather than being rushed. |
A subset of ops is off-limits for forwarding until Phase 3 lands. |
Block on env-var/logging mapping first |
Single, complete forwarding story at launch. |
Pushes any user-visible benefit out by months for a problem that doesn’t affect early targets. |
5.5 Direct private linkage for the MIOpen provider in Phase 1 rather than Phase 4#
Ties to: “Let hipDNN’s existing MIOpen provider call the private MIOpen API directly to avoid the round-trip cost when hipDNN is already in the call stack — wired up in Phase 1 so the loop hazard is structurally removed before any forwarding lands.”
An earlier draft deferred the direct private linkage to Phase 4. The pivot moves it to Phase 1 because (a) Phase 2 is the first phase that can actually create the loop (hipDNN provider → wrapper → hipDNN), and structurally removing the loop before that point eliminates a class of recursion-guard bugs and reasoning load entirely, and (b) the consumer-side _impl header that the direct linkage needs (§4.5) is also the mechanism the provider plugin needs to load successfully against libMIOpen_private.so, so the two changes are coupled and don’t benefit from being split across phases.
Pros |
Cons |
|
|---|---|---|
Phase 1 (chosen) |
Loop hazard removed structurally before any forwarding lands; no thread-local recursion-guard needed during Phase 2. Couples the |
Couples the wrapper rollout to a hipDNN-side build change; Phase 1’s blast radius is larger. Mitigation: the change is contained to |
Phase 4 |
Phase 1 changes stay minimal. The wrapper is validated end-to-end before hipDNN’s build is touched. |
Until Phase 4, hipDNN-originated calls go through the wrapper unnecessarily, and once Phase 2 lands, the loop is gated by a recursion guard rather than by structure. |
6. Risks#
Risk |
Likelihood |
Impact |
Mitigation |
|---|---|---|---|
Per-call overhead from the wrapper is non-negligible for short-running ops |
Low |
High |
Largely mitigated by the prototype: microbenchmark of the worst-case no-work API ( |
Renaming public symbols to |
Low |
Medium |
The renaming is mechanical and applied directly in source (§4.6); collisions surface at compile or link time. We grep the source for |
Behavioral divergence between MIOpen and hipDNN for a forwarded op (precision, edge cases, error codes) |
Medium |
High |
Phase 2 forwards a small, well-tested set first under |
Environment variables silently change meaning when forwarding is on |
High if unaddressed |
Medium |
The default of |
hipDNN ABI churn breaks the wrapper |
Low |
Medium |
Likelihood reduced by API-version pinning (§4.9): the wrapper links only hipDNN’s stable base-graph (V1.0) API and excludes other backend symbols from compilation, so a newer hipDNN cannot introduce an unresolved symbol and a consumer upgrading hipDNN within the pinned major does not break linking. Any remaining breakage (a change to the stable base-graph API itself) surfaces at link time in CI and is coordinated with the wrapper’s release cadence. |
Consumers that statically link MIOpen lose the wrapper indirection |
Low–Medium (assumption — needs confirmation) |
Low |
Currently flagged as low-likelihood, but this is an assumption that needs confirmation: before Phase 1 exit, reach out to the framework and downstream-consumer teams (PyTorch ROCm, TensorFlow ROCm, internal AMD consumers) to inventory whether anyone actually static-links MIOpen today. If any production consumer does, the risk likelihood and the mitigation both need to be re-scoped. Until then, document that the wrapper requires the shared-library build; if a static-link consumer surfaces and matters, a separate mechanism (likely link-time symbol substitution) is needed. |
Loop / recursion if the MIOpen provider in hipDNN re-enters the wrapper |
Low |
High |
The Phase 1 provider direct private linkage (§4.5) structurally prevents the loop: the provider links |
Operator sets |
Medium |
Medium |
The opt-in is intentional and per-process; the failure mode (forwarded op misbehaves) is recoverable via |
|
Low |
High |
The rename is applied directly in source; there is no rewrite macro to leak. The |
Pre-existing internal-symbol leakage in |
High (today) |
High |
Today’s |
hipDNN frontend’s header-only structure forces a hard hipDNN-backend dependency on |
High (today) |
High |
The wrapper reaches hipDNN through hipDNN’s header-only frontend, whose |
hipDNN is missing, broken, or fails at runtime |
Medium |
High |
Behavior is defined per failure class (unavailable / present-wrong-version / per-call). During the rollout, weak linkage (§4.9) keeps a missing or incompatible-major hipDNN from blocking |
Cold-start wall time on the forwarded path is higher than native on a fresh process |
Low–Medium (re-attributed after investigation) |
Medium |
The Phase 2 forwarding prototype initially showed a fresh process at ~4 s vs native’s ~0.2 s warm-process wall time, which first read as a missing-persistent-cache regression (investigation reference §6). Follow-up investigation re-attributes the bulk of this to a one-time runtime load of the GPU device libraries on the forwarded path, not to a missing plan cache. Because forwarding executes through the same MIOpen Private code, it shares MIOpen’s existing kernel cache — no double compilation, no per-process plan rebuild beyond that one-time device-library load — so a dedicated cross-process plan cache is likely not required. Phase 2 confirms this by measuring against Phase 1 exit criterion 4 (cold-start ≤ 1.10× of |
6.1 Failure-mode behavior for hipDNN forwarding#
Three failure classes, each with explicit defined behavior:
hipDNN unavailable (missing, or its symbols don’t resolve) — during the rollout window
libMIOpen.solinks hipDNN weakly (§4.9), so a missing or SONAME-unresolvedlibhipdnn_backend.sodoes not preventlibMIOpen.sofrom loading; the wrapper’s hipDNN references simply do not resolve. (A hipDNN that is present but is the wrong major — which may still resolve at the loader, since hipDNN does not guarantee a per-major SONAME bump — is caught separately by the init-time version query in the next bullet.) The wrapper detects this at init, logs once to stderr, sets an internalhipdnn_unavailableflag, and continues. WithMIOPEN_HIPDNN_FORWARDING=disabled(the default) this is invisible — every call lands on MIOpen Private as usual. With forwarding requested, the affected calls fail loudly (§4.9) rather than silently degrading, so the misconfiguration is obvious. (This corrects the earlier draft: under a hard-link assumption the loader rejectedlibMIOpen.sobefore any wrapper code could run, so this defensive measure could never execute. Weak linkage during the rollout is what makes it reachable. Once the default flips and hipDNN becomes a hard dependency — §4.9, §9 — missing hipDNN reverts to a clean load-time failure at process start, acceptable at that point because hipDNN is then a validated, shipped part of the stack.)Present-but-wrong hipDNN version — caught by the init-time version query in §4.9, which is the load-bearing gate because hipDNN does not guarantee a per-major SONAME bump (hipDNN RFC 0005). The wrapper queries the backend’s reported version at init and, on an unexpected major, routes into the same “unavailable” handling (pass-through when
disabled, loud error whenenabled) rather than letting the mismatch surface inside a forwarded call. A same-major hipDNN is safe by hipDNN’s within-major forward-compatibility guarantee — which is exactly why the wrapper pins to the base V1.0 API (§4.9).Per-call hipDNN failure — a forwarding attempt returns a hipDNN-side error. The failure is propagated to the caller as a translated
miopenStatus_t. We do not silently fall back to MIOpen Private mid-execution, because silent fallback would mask correctness regressions (hipDNN returned a wrong-but-not-error result, MIOpen would have returned right) and hide perf regressions. Operators retainMIOPEN_DISABLE_HIPDNN_FOR=<op>(§4.4) as the kill switch for a flaking op.Error-code distinguishability. A translated
miopenStatus_talone is ambiguous — on-call cannot tell fromMIOPEN_STATUS_INTERNAL_ERRORwhether to read MIOpen source or hipDNN source. The wrapper provides:a thread-local accessor
miopenGetLastForwardedError()returning the hipDNN-side status (if any) for the most recent forwarded call on the current thread, anda
[hipDNN-forwarded]prefix on themiopenGetErrorStringmessage for any forwarded-error code, so log scraping is unambiguous.A new dedicated error code (
MIOPEN_STATUS_FORWARDED_ERROR) was considered but rejected for Phase 2 because it would change the visible status set for any consumer that opts into forwarding; the thread-local accessor is additive and does not. To be re-evaluated if forwarded-error patterns turn out to warrant their own bucket.
6.2 hipDNN as an upstream dependency — change communication#
With forwarding enabled, hipDNN is in MIOpen’s runtime dependency chain, so changes on the hipDNN side can now reach MIOpen consumers who never integrated hipDNN directly. A full supply-chain / advisory policy is out of scope for this RFC, but the basic commitment is noted here for visibility: critical hipDNN changes that affect forwarded behavior — breaking changes, security fixes (CVEs), or ABI bumps — must be surfaced to MIOpen users through MIOpen’s existing release notes and changelog channels, so consumers are not blindsided by behavior originating in a dependency they did not knowingly add. The concrete advisory/coordination process is left to a follow-up.
7. Execution Plan#
The work is broken into four phases. Each phase ends with the existing MIOpen test suite green in the default (MIOPEN_HIPDNN_FORWARDING=disabled) configuration; phases that forward to hipDNN additionally run the suite with the relevant forwarding env vars set.
Estimates. Rough per-phase effort estimates, in person-sprints:
Phase 1: 2
Phase 2: 2–4
Phase 3: 2–4
Phase 4: 1
Phase 1 is the best understood — the prototype already covers most of it (investigation reference §1–§4) and the remaining work (direct source rename,
_implheader install, provider rewire, env-var plumbing, CI gates, wrapper-induced test fix) is well-scoped. Phase 2 onward should still be treated as quite rough: the actual cost depends on which op families end up in the forwarding set, on hipDNN’s coverage at the time, on what the framework-env-var audit (§7 Phase 3 task 1) surfaces, and on how much of the existing MIOpen env-var surface turns out to be production-relevant. Re-scope these numbers as each phase enters planning.
Phase 1 — Pass-through wrapper, source rename, and provider direct private linkage#
Prerequisite — internal-symbol leakage must be addressed before Phase 1 starts. Today’s
libMIOpen.soexports a large set of internal symbols (solver/internal classes, helpers) that are not part of the public C API. This has two downstream consequences that block Phase 1 from achieving its stated goal of a clean public/private split:
MIOpen’s own internal consumers (MIOpenDriver, gtest binaries) reach into these internals directly, so they cannot run today against a library whose only exported surface is the public C API. As long as that’s true, the wrapper’s
libMIOpen.socannot stop re-exporting internals without breaking MIOpen’s own driver and tests.External libraries are known to depend on the leaked internals. MIGraphX, in particular, binds against MIOpen internals that are not part of the public API. If the wrapper inherits today’s leaky surface, those external consumers continue to depend on symbols that the wrapper has no contract over and that hipDNN forwarding cannot honor.
There is an unmerged refactor that splits MIOpen along cleaner layers so that the driver and (to a large degree) the tests no longer need access to hidden internal symbols. That refactor must land before Phase 1 begins, so that:
The symbol-superset and header-leakage checks (§4.6, §6, §7 Phase 1 task 8) run against a library whose exported surface is the public C API rather than the public C API plus accumulated internals.
The same MIOpen release that introduces the wrapper can also disable the remaining internal symbol leaks — giving external consumers (notably MIGraphX) a single, clearly-flagged release in which to migrate off the leaked symbols. Bundling the symbol-leak fix with the wrapper rollout means downstream consumers absorb one disruption rather than two.
The header-leakage and symbol-superset CI gates added in Phase 1 task 8 are checking the intended surface (public C API only) rather than locking in today’s polluted one.
The layering split is what lets MIOpenDriver become a pure public-API consumer with no private-symbol dependency, and it moves the gtest/ctest binaries that need internal access into the private layer, where they keep that access by binding
libMIOpen_private.so. The complementary moves owned by this RFC — rewiring the CK plugins toMIOpen_privateand relayering the no-private-access tests back onto the public surface (where they serve as the forwarding flag-on/flag-off tests) — are described in §4.10 and scheduled as Phase 1 tasks 12–13.The refactor is large and is owned outside this RFC; the Phase 1 kickoff is gated on its merge. If the refactor slips, Phase 1 slips with it — proceeding without it would either bake the leakage into the wrapper’s public contract or force a second disruptive consumer-migration release later.
Prerequisite — the hipDNN frontend must support runtime (
dlopen) backend loading before Phase 1 starts. The wrapper reaches hipDNN through hipDNN’s header-only frontend. As that frontend is structured today, merely consuming it forces a hard dependency on the hipDNN backend, which is incompatible with §4.9’s weak-linkage posture (libMIOpen.somust load and run with no hipDNN present whileMIOPEN_HIPDNN_FORWARDING=disabled). Two structural facts cause this:
The frontend’s CMake
INTERFACEtarget hard-links the backend.projects/hipdnn/frontend/CMakeLists.txtdoestarget_link_libraries(hipdnn_frontend INTERFACE hipdnn_data_sdk hipdnn_backend), so every frontend consumer — including the wrapper — inherits aDT_NEEDEDonlibhipdnn_backend.so.The frontend’s backend shim calls the backend C API directly.
HipdnnBackendWrapper(projects/hipdnn/frontend/include/hipdnn_frontend/detail/BackendWrapper.hpp) forwards eachIHipdnnBackendmethod straight tohipdnnCreate,hipdnnBackendExecute, etc., so those backend symbols are baked as undefined references into the consumer’s own translation units. Because the frontend is header-only, there is no separate.soseam at which the backend could be weak-linked ordlopen’d after the fact — the usual weak-linkage trick is unavailable.The fix is hipDNN-side: add a second
IHipdnnBackendimplementation —HipdnnDynamicWrapper— that resolves each backend entry point throughdlopen/dlsymat first use instead of referencing it directly, so a frontend consumer carries no link-time or load-time dependency on the backend and loads it only when forwarding actually runs. A new CMake flag (theRUNTIME_LOAD_BACKENDoption — the concept is anticipated in the frontend but the flag and the dynamic wrapper do not exist yet) selects which wrapper the frontend compiles in and, when on, dropshipdnn_backendfrom the frontend’sINTERFACElink line; a matching compile define (modelled on cuDNN’sNV_CUDNN_FRONTEND) keeps consumer source identical across both modes, so the wrapper code looks the same whether the backend is direct-linked or runtime-loaded.Note the two-flag separation this sets up, which matches the intended rollout (§9): every shipped MIOpen build compiles the dynamic backend-load mode on (
RUNTIME_LOAD_BACKENDenabled), so the backend is never a hard dependency; whether the backend is actually loaded and called is then governed entirely at runtime byMIOPEN_HIPDNN_FORWARDING. A static check must confirm that, with the compile flag on, no backend symbol is referenced directly anywhere in the frontend — all backend access goes through the wrapper — so a dynamic-mode consumer has zero unresolved references tolibhipdnn_backend.sountil forwarding is turned on.This work is owned in hipDNN and will be tracked by an ALMIOPEN ticket filed alongside the rest of this RFC’s implementation tickets. The Phase 1 kickoff is gated on its merge: without it, §4.9’s weak-linkage requirement cannot be met and
libMIOpen.sowould acquire a hard hipDNN dependency the moment the wrapper consumes the frontend, regardless of theMIOPEN_HIPDNN_FORWARDINGdefault.
A prototype covering the build/link mechanics, runtime path, wrapper stubs, and performance characterization has been built (investigation reference §1–§4). The Phase 1 increment over the prototype is: convert the macro-based rename to a direct source rename (task 3), install the parallel _impl header (task 4), rewire the MIOpen provider for direct private linkage (task 5), add the runtime env var (task 7), stand up the CI gates (task 8), confirm existing PyTorch-on-ROCm CI covers the wrapper build (task 9), the wrapper-induced test fix (task 11), rewire the CK grouped-conv plugins for direct private linkage (task 12), and relayer the no-private-access tests onto the public surface (task 13).
Goal: establish the two-library split, the dispatch plumbing, the runtime env-var control surface, and the provider’s direct private linkage, with all public entry points dispatching to MIOpen Private when MIOPEN_HIPDNN_FORWARDING=disabled (the default). From the consumer’s perspective with the default in effect, behavior is functionally identical to today’s MIOpen.

Tasks:
Add the
MIOpen_privateCMake target and wire itsINTERFACE_INCLUDE_DIRECTORIESto expose<miopen/miopen_impl.h>(see task 4). The build always produces bothlibMIOpen.so(wrapper) andlibMIOpen_private.so(implementation). CMake should emit a clear status message at configure time announcing the build state (“MIOpen wrapper: built — runtime default is pass-through; hipDNN forwarding controlled by MIOPEN_HIPDNN_FORWARDING env var”) so that downstream packagers cannot miss the structural change.Confirm build-system mechanics, SONAME and SOVERSION inheritance, and
MIOpen_with_pluginsinteraction. (Status: build-system mechanics and runtime path confirmed by the prototype — investigation reference §1, §4. SONAME/SOVERSION CI gate is task 8; theMIOpen_with_pluginsinteraction is resolved by the §4.10 design and implemented in tasks 12–13.)Direct source rename. Rename the public-API function definitions in
src/*_api.cpp(and the corresponding internal headers undersrc/include/miopen/) frommiopenFootomiopenFoo_impl. Mechanical edit driven by the public API surface enumerated in<miopen/miopen.h>. (Status: prototype used a macro-based-includemechanism (§4.6 historical), produced 263/263 renamed entry points. Direct source rename replaces that and is new Phase 1 work.)Install
<miopen/miopen_impl.h>. Generate or hand-maintain the parallel_impl-form public header alongside<miopen/miopen.h>(§4.6). Install it under the private include subtree and add it toMIOpen_private’sINTERFACE_INCLUDE_DIRECTORIES. CI check: a TU built againstMIOpen_privateand including<miopen/miopen_impl.h>resolves the_impldeclarations; a TU built againstMIOpenonly and including<miopen/miopen.h>resolves only the public names.Rewire the MIOpen provider plugin for direct private linkage (Phase 1). In
dnn-providers/miopen-provider/, swap the link target toMIOpen_privateand the include from<miopen/miopen.h>to<miopen/miopen_impl.h>for the TUs that talk to MIOpen. Verifydlopenof the resulting plugin succeeds and that the provider’s MIOpen calls resolve to the_implsymbols inlibMIOpen_private.so(covered by the provider tests and the §4.7 object-file check).Generate the wrapper source file. Each entry point has a stub that forwards the call to the corresponding
_implsymbol via a plain function call. (Status: 263 stubs in place; runtime-traversal proof in investigation reference §2.)Add the
MIOPEN_HIPDNN_FORWARDINGenv var (§4.1) and the wrapper’s runtime dispatcher. Phase 1 only needs thedisabledpath to be load-bearing;enabledis parsed and accepted but is functionally equivalent todisabledbecause no op is in the forwarding set yet. A configuration banner is emitted to stderr once at first wrapper call summarizing the resolved mode.Add the header-leakage, symbol-superset, and co-versioning CI checks described in §6, §4.6, and §4.3.1: (a)
grep -r '_impl' <staged public headers under MIOpen install root>returns no matches; (b)nmof the builtlibMIOpen.soshows no_implexports; © public-symbol-superset check: the wrapper’s exported public symbols are a superset of the public entry points inlibMIOpen_private.so— for everymiopenFoo_implexported by Private there is a matchingmiopenFooexported by the wrapper — so no entry point is silently dropped. Checks (b) and © read both libraries from the same build, so no pre-wrapper baseline snapshot is required; wrapper-only additions such asmiopenGetLastForwardedError()(§6.1) are permitted by construction. (d) co-versioning check (§4.3.1 invariant): extract embedded version metadata fromlibMIOpen.so.<X>andlibMIOpen_private.so.<X>and fail the build if they disagree, and assert the wrapper’sDT_NEEDED libMIOpen_private.so.<X>entry resolves to the same<X>the wrapper was built at. (Status: not yet wired into CI.)Confirm existing PyTorch-on-ROCm CI exercises the wrapper build. Because the wrapper preserves the public API and ABI, PyTorch-on-ROCm consumes the wrapper build exactly as it consumes MIOpen today — no new sample, harness, or integration work is required, and the existing PyTorch ROCm regression coverage (§8.2) exercises the pass-through path automatically once the wrapper build is what ships. The only Phase 1 action is to confirm the wrapper build is the MIOpen under test, not to stand up new coverage. (Status: expected automatic; confirm during Phase 1.)
Run the MIOpen test suite and the MIOpen provider tests (in
dnn-providers/miopen-provider/) with the default env var setting (disabled); all must be green. (Status: gtest pass on the prototype — 261/265 binaries clean, with 2 wrapper-induced case failures intest_smoke_tuning_policy(see task 11), 2 environmental case failures intest_db_sync(missinggfx942.kdb), and 2 timeouts intest_lrn/test_soft_maxthat were still progressing at the 1200 s per-binary budget — investigation reference §3.)Wrapper-induced test fix (known-issue follow-up surfaced by task 10).
test_smoke_tuning_policyasserts on captured stderr fromMIOPEN_LOG_FUNCTION, which now contains_impl-suffixed function names rather than the public names the test was written against. Fix: relax the test assertion to accept the_implform. The alternative of normalizing the wrapper’s log output back to the public name was considered but rejected: under direct source rename, the function names are genuinely_implin source, so normalizing the log would be hiding the actual control flow from the test rather than reflecting it. Investigation reference §3 has the failure details.Rewire the CK grouped-conv plugins for direct private linkage (§4.10). In
src/ck_impl/CMakeLists.txt, switch eachMIOpenCKGroupedConv_*.sofrom linkingMIOpento linkingMIOpen_private, so the plugins resolve MIOpen’s internal C++ symbols (ProblemDescription,ConvSolution,InvokerFactory) againstlibMIOpen_private.sorather than the public wrapper. The plugins consume internal C++ symbols, not the_implC API, so they do not include<miopen/miopen_impl.h>. Verify each plugin still builds, loads viadlopen, and is exercised by the driver/tests that bundle it throughMIOpen_with_plugins. (Status: new Phase 1 work; depends on task 1.)Relayer the no-private-access tests onto the public surface (§4.10). The layering refactor (§7 prerequisite) moves the gtest/ctest binaries that need internal symbols into the private layer (linking
libMIOpen_private.so). As part of this RFC, move the tests that need no private access back onto the publicMIOpentarget so they exercise the wrapper, and use them to test flag-on vs flag-off execution through the public API (MIOPEN_HIPDNN_FORWARDING=disabledvsenabled). (Status: new work; the flag-on coverage becomes load-bearing from Phase 2 onward, when ops are actually forwarded.)Capture the pre-wrapper baseline and run the one-time pass-through overhead comparison (§8.1). Before the Phase 1 split lands, snapshot the pre-wrapper
libMIOpen.so, then run a manual, one-time performance comparison of the pass-through wrapper (MIOPEN_HIPDNN_FORWARDING=disabled) against that snapshot on representative hardware and document the results to confirm the wrapper hop is negligible (Phase 1 exit criteria 2 and 3). This is a one-off characterization, not a CI job — there is no per-merge wrapper-off artifact to compare against. Establishing this equivalence is what licensesdisabledto become the comparison baseline for Phase 2 onward (§8.1). (Status: characterized on the prototype — wall-clock same or slightly faster vs baseline, GPU kernel-time deltas ≤ 2%, dispatch hop ≈1 ns/call; investigation reference §4. Remaining work is to repeat and document this against the Phase 1 wrapper build.)
Exit criteria (Phase 1):
Functional parity (default mode). Wrapper build passes the same MIOpen test suite + MIOpen provider tests with
MIOPEN_HIPDNN_FORWARDING=disabled(the default) as the pre-wrapper baseline. Phase 1 explicitly does not exercise the env-var or logging mapping — the forwarded op set is empty, so env-var-sensitive paths are out of scope for this phase. Tasks deliberately select ops whose pass-through behavior is insensitive to env-var state. Env-var coverage is the focus of Phase 3. (Status: provisionally met on the prototype for the gtest suite — 261/265 binaries clean — pending the test fix in task 11 and the environmentaltest_db_syncsetup gap. Investigation reference §3.)Steady-state wall-clock overhead. Wrapper adds < 1% wall-clock overhead on a representative end-to-end workload, measured against the pre-wrapper baseline snapshot. This is the gate for the wrapper itself being acceptable as a permanent piece of the call path that every consumer now pays. (Status: met on the prototype. End-to-end conv workload shows wall-clock the same or slightly faster vs baseline; aggregate GPU kernel time deltas ≤ 2% with signed direction varying by phase. Investigation reference §4.)
Per-call overhead breakdown (gate, not a target). Measure overhead from each of: parameter translation, hipDNN graph building (where applicable in later phases — measured here so the Phase 2 number can be compared cleanly), dispatch indirection. If any single component dominates the < 1% budget, escalate before Phase 2 starts. Mitigation strategies that are pre-approved if translation overhead is a problem: caching translated graphs, caching descriptor conversions, lazy materialization of hipDNN handles. (Status: dispatch indirection measured at ≈1 ns/call upper bound. Parameter translation and hipDNN graph building are Phase 2 measurements — there is no forwarded op to translate against in Phase 1.)
Cold-start vs warm-start compilation time. Measure first-call latency separately from steady-state. The risk we are gating against is that hipRTC compilation (or any other one-time JIT work on either side) happens twice — once on a MIOpen Private path and once on a hipDNN path — for a workload that, before this work, would have compiled it only once. The exit criterion is that cold-start latency for any forwarded op family with
MIOPEN_HIPDNN_FORWARDING=enabledis ≤ 1.10× cold-start with=disabled, and warm-start is within the < 1% steady-state budget above. (Status: pass-through cold-load characterized — +6 major page faults, ≤1 MB RSS, no detectable wall-clock change (investigation reference §4). The Phase 2 forwarding prototype initially showed a fresh process at ~4 s vs ~0.2 s warm-process native, which first read as a cold plan-build regression (investigation reference §6 concern 1); follow-up investigation re-attributes the bulk of that to a one-time runtime load of the GPU device libraries, not a missing plan cache. Because forwarding executes through the same MIOpen Private code, MIOpen’s kernel cache is shared and there is no double compilation, so the gate is expected to be satisfiable once the one-time device-library load is accounted for — it no longer requires a dedicated cross-process plan cache. The gate stays open as a Phase 2 measurement to confirm this in the wrapper build; the unstable plan-build time across cold runs (§6 concern 2) remains to be characterized.)PyTorch-on-ROCm CI signal. Existing PyTorch-on-ROCm CI is green against the wrapper build (pass-through default). Because the public API/ABI is unchanged, this requires no new coverage — only confirmation that the wrapper build is what their CI exercises. (Status: expected automatic; confirm during Phase 1.)
Header-leakage, symbol-superset, and co-versioning CI gates green. The checks added in task 8 are wired into CI and pass. (Status: not yet wired up.)
Provider direct private linkage verified (§4.5).
dlopenof the rewired MIOpen provider plugin succeeds, the provider tests pass againstlibMIOpen_private.so, and the §4.7 object-file check (provider references only_implsymbols) passes. (Status: not yet wired up — depends on tasks 4 and 5.)CK plugins and test relayering verified (§4.10). The CK grouped-conv plugins link
libMIOpen_private.so, build, anddlopencorrectly; the driver/tests that bundle them viaMIOpen_with_pluginspass; MIOpenDriver links the public wrapper with no private-symbol dependency; and the no-private-access tests run against the public wrapper (providing the flag-on/flag-off harness used from Phase 2 onward). (Status: not yet wired up — depends on tasks 12–13 and the §7 layering-refactor prerequisite.)
Phase 2 — hipDNN forwarding for selected entry points#
Goal: actually forward the convolution op family to hipDNN — the chosen first and, within this RFC’s scope, only forwarded op family. Defer environment-variable and logging concerns.
Tasks:
Set the initial compile-time forwarding set to the convolution op family (§4.4). This is the decided first target for this RFC: hipDNN already covers convolution, the behavior is well-understood, and the Phase 2 forwarding prototype already redirects
miopenConvolutionForwardend-to-end (investigation reference §6), so it is the lowest-risk op family to qualify first. Convolution forward is what the prototype proves out; the remaining convolution entry points (backward-data, backward-weights, and the bias/activation-fused convolution paths) are qualified during this phase against the same argument-translation and numerical-verification gates. The forwarding set lives in the routing policy module and determines which ops are forwarded whenMIOPEN_HIPDNN_FORWARDING=enabled; it stays scoped to convolution for the duration of this RFC. (Batchnorm is the expected next op family but is out of scope here — see §9.)Implement the routing policy module — a single source file that the wrapper consults to decide Private vs. hipDNN per call (consulting the compile-time forwarding set and
MIOPEN_DISABLE_HIPDNN_FOR).Implement the hipDNN call paths: argument translation from MIOpen descriptors to hipDNN graph + variant pack, hipDNN execution, result translation back to
miopenStatus_t. (The Phase 2 forwarding prototype already realizes this formiopenConvolutionForward— it builds the hipDNN backend-descriptor conv graph by hand, finalizes/caches an execution plan, and runs end-to-end viaMIOpenDriver; it also enumerates the descriptor fields with no 1:1 hipDNN analog today, declining alpha/beta scaling,groupCount != 1, and > 5 spatial dims. Investigation reference §6 is the worked example to generalize from.)Wire the opt-out env var (
MIOPEN_DISABLE_HIPDNN_FOR=..., §4.4) into the routing policy.MIOPEN_DISABLE_HIPDNN_FOR=*forces every op back to MIOpen Private while leavingMIOPEN_HIPDNN_FORWARDING=enabledin effect (this is the “wrapper enabled but every op back on MIOpen” rollback path documented in §9).Add routing tracing. When
MIOPEN_LOG_LEVEL(or a wrapper-specific equivalent) is set, every wrapper invocation logs a single line containing the entry-point name, the resolvedMIOPEN_HIPDNN_FORWARDINGmode, the routing decision (Private vs. hipDNN), and the reason (in the compile-time forwarding set, env-var opt-out, policy fall-through). This is the on-call instrument for “why did this call go where I didn’t expect” investigations and is a prerequisite for confidently flipping theMIOPEN_HIPDNN_FORWARDINGdefault in any environment.Implement the failure-mode behavior defined in §4.9 and §6.1: the init-time hipDNN version query (query the hipDNN backend’s reported runtime version via its version-reporting API, treat an unexpected major as unavailable-for-forwarding) — load-bearing per §4.9 because hipDNN gates compatibility on dynamic version queries rather than SONAME (hipDNN RFC 0005 §4.5–§4.7) — hipDNN-unavailable handling under weak linkage (pass-through when
disabled; a loud error when forwarding is requested but hipDNN did not resolve — not a silent no-op), per-call hipDNN errors propagated (not silently absorbed), and themiopenGetLastForwardedError()thread-local +[hipDNN-forwarded]error-string prefix. Add tests for the missing-hipDNN-with-forwarding-enabled failure class (weak-link case) and the wrong-major-version assertion. (Note: the Phase 2 forwarding prototype currently falls back silently on any hipDNN failure (investigation reference §6 concern 4), which is broader than this design. Reconciling the prototype’s blanket fallback with §6.1’s “decline-unsupported-up-front but propagate genuine per-call errors” split is part of this task.)Confirm cold-start behavior on the forwarded path against exit criterion 4. The prototype’s ~4 s fresh-process wall time (vs native’s ~0.2 s warm-process) initially read as a missing-cross-process-cache regression, but follow-up investigation re-attributes the bulk of it to a one-time runtime load of the GPU device libraries; because forwarding executes through the same MIOpen Private code, MIOpen’s kernel cache is shared and there is no double compilation (investigation reference §6 concern 1, as re-attributed). Measure cold-start with
MIOPEN_HIPDNN_FORWARDING=enabledagainst=disabled, accounting for the one-time device-library load, and confirm it clears Phase 1 exit criterion 4 (cold-start ≤ 1.10×). Only if that measurement still shows a per-process plan rebuild beyond the device-library load is dedicated cross-process plan persistence (or enabling/sharing hipDNN’s own on-disk kernel cache) needed. Also investigate the unstable plan-build time (1.3 ms vs 3.9 s across cold runs, §6 concern 2).Document the explicit non-coverage of env-var / logging mapping for this phase. Forwarded ops in this phase should be ones whose behavior is not sensitive to MIOpen env vars or logging-related state.
Wire the public gtest suite into CI as a standing dual-configuration job, run with numerical verification. Configure CI to run the public-layer gtests (the no-private-access tests relayered onto the wrapper in Phase 1 task 13) twice against the same wrapper build — once with
MIOPEN_HIPDNN_FORWARDING=disabledand once with=enabled— with both runs required green for the job to pass. This makes the flag-off/flag-on coverage a permanent, automatically-enforced gate (rather than a per-phase manual run), so every subsequent change is checked against both the pass-through path and the convolution-forwarding path. The=enabledrun uses numerical verification (-V 1on driver-level tests, tolerance checks on gtest): the prototype validated execution but not correctness (it ran-V 0— investigation reference §6 concern 5), so confirming the redirected output matches native MIOpen is a Phase 2 gate, not an assumption. Add targeted tests for any forwarded-op edge case not already covered. Phase 2 is where this gate becomes load-bearing because it is the first phase with a non-empty forwarding set (in Phase 1 the=enabledrun is functionally identical to=disabled). (Status: new Phase 2 work; depends on tasks 1 and Phase 1 task 13.)
Exit criteria: the convolution op family forwards to hipDNN under MIOPEN_HIPDNN_FORWARDING=enabled and passes the MIOpen test suite with numerical verification; routing tracing is wired up; failure-mode behavior matches §6.1; the public gtest suite runs green in CI in both the disabled and enabled configurations as a standing dual-configuration gate (task 9); and cold-start on the forwarded path clears Phase 1 exit criterion 4 (per task 7, accounting for the one-time device-library load now understood to be the main contributor — investigation reference §6 as re-attributed).
Phase 3 — Environment variable and logging mapping#
Goal: build a translation layer for the cross-cutting concerns deferred in Phase 2 — scoped to the variables that frameworks actually use in production, rather than an exhaustive port of the full MIOpen env-var surface. The exhaustive approach was considered and rejected: the long-tail MIOpen-only variables are largely diagnostic/tuning-debug knobs whose users are MIOpen developers (who have other means of investigation) rather than framework consumers, and trying to port all of them would blow up Phase 3’s scope and timeline without delivering proportional consumer-facing value.
Tasks:
Audit framework usage first. Survey the actual MIOpen env-var usage in PyTorch ROCm, TensorFlow ROCm, ONNX Runtime, JAX/XLA, and any internal AMD-consumer build/CI to identify which MIOpen env vars are set in production environments. This is the input to the mapping prioritization — variables nobody outside MIOpen actually sets are deprioritized.
From the audit output, classify each variable that does see production use: (a) directly maps to a hipDNN env var, (b) maps to a hipDNN concept under a different name, © no hipDNN equivalent — document behavior when forwarding is on. The remaining MIOpen-specific variables (the long tail not surfaced by the audit) are documented as incompatible with hipDNN forwarding: when one of them is set, the wrapper either refuses to forward the affected op family or emits a one-time warning, as decided per variable.
Inventory logging conventions on both sides; decide whether the wrapper translates MIOpen log calls to hipDNN log calls, leaves them alone, or emits both. The routing-tracing log added in Phase 2 stays on the MIOpen side regardless — it is wrapper instrumentation, not a backend log.
Implement the env-var translation at wrapper init time (and on relevant per-call boundaries for variables that change kernel selection).
Document the mapping in user-facing docs, including the explicit list of MIOpen-only variables that are not honored when forwarding is enabled.
Re-run the test suite, including any tests that exercise env-var-sensitive behavior.
Exit criteria: env-var-sensitive ops that the audit (task 1) identified as production-relevant can be safely forwarded; user-facing documentation explains the mapping and lists the MIOpen-only variables that are incompatible with hipDNN forwarding.
Phase 4 — Performance baselining#
Goal: produce a definitive performance comparison between the two rollout configurations now that the env-var and logging mapping (Phase 3) is in place and the provider’s direct private linkage (Phase 1) has already been validated.
The pre-wrapper baseline is not re-compared here. Wrapper-on-vs-pre-wrapper was the Phase 1 exit gate, and the pre-wrapper snapshot was taken on long-ago hardware/driver state — re-using it months later would mix wrapper overhead with unrelated drift in the rest of the stack. From Phase 2 onward, disabled is the comparison baseline (see §8 Testing Plan for the rationale).

Tasks:
Run the broader benchmark suite (MIOpenDriver, framework-level workloads) in the two runtime configurations against the same wrapper-built binary:
MIOPEN_HIPDNN_FORWARDING=disabled(the today-equivalent reference) andMIOPEN_HIPDNN_FORWARDING=enabledwith hipDNN forwarding for the validated set from Phase 2. Report deltas. The reference isdisabledmeasured on the same build and the same hardware/driver asenabled, not the pre-wrapper snapshot.Re-verify the Phase 1 provider direct private linkage (§4.5) against the current build:
dlopenof the MIOpen provider plugin succeeds, the provider tests pass, and the §4.7 object-file check still holds. This is a regression sentinel — the direct linkage lived from Phase 1, but Phase 2/3 changes to the wrapper or to the_implheader could break it.Decide, based on the data, whether to flip the
MIOPEN_HIPDNN_FORWARDINGdefault fromdisabledtoenabled(with the validated op set). The decision criteria are out of scope for this RFC — see §9.Write an op-onboarding playbook capturing the end-to-end convolution learnings. Now that the convolution op family has been carried through Phases 1–4, distill the experience into a step-by-step playbook for adding a new op family end-to-end. This is explicitly the on-ramp for the subsequent op families (§9 — batchnorm next) and is written to be executed by an AI agent: concrete, ordered, with exact file locations, the commands/gates to run at each step, and the decision points called out as checkable conditions rather than prose the agent has to infer. At minimum it covers: (a) adding the op to the compile-time forwarding set in the routing policy module (§4.4); (b) the argument-translation pattern from MIOpen descriptors to a hipDNN graph + variant pack, generalized from the convolution worked example (investigation reference §6), including how to enumerate descriptor fields with no 1:1 hipDNN analog and decline the unsupported cases up front; © wiring the failure-mode behavior (§6.1) and confirming the tuning-DB bypass semantics (§4.8) hold for the new op; (d) adding the op’s tests to the standing dual-configuration CI gate (Phase 2 task 9) and running them with numerical verification (
-V 1); (e) the env-var / logging-mapping checklist (Phase 3) for any variable the new op is sensitive to; (f) re-baselining performancedisabledvsenabled(Phase 4 task 1) for the new op. Each section should link the canonical example commit/PR from the convolution work so the agent has a concrete reference implementation to mirror. The playbook lives in the MIOpen docs tree (not under the RFC folder, since it is developer documentation rather than an RFC). (Status: new Phase 4 work; the convolution rollout through Phases 1–4 is the source material.)
Exit criteria: performance numbers published for both runtime configurations (disabled and enabled); provider direct private linkage still verified (§4.5, §4.7); the op-onboarding playbook (task 4) is published in the docs tree and is concrete enough for an AI agent to follow end-to-end; informed default-flip decision delegated to the successor RFC named in §9.
8. Testing Plan#
The existing MIOpen test suites are the acceptance gate. There is only one shipped artifact — the wrapper build — so CI runs the suite against that artifact with the relevant MIOPEN_HIPDNN_FORWARDING setting per phase.
Unit and gtest layer (
test/gtest/) — every test that passes today must pass withMIOPEN_HIPDNN_FORWARDING=disabled(the default), no exceptions. Any divergence is a bug in the wrapper or in the routing policy, not an excuse to skip a test. From Phase 2 onward, the suite is additionally run withMIOPEN_HIPDNN_FORWARDING=enabled(which exercises whatever ops the Phase 2 compile-time forwarding set contains). This dual run is not a manual per-phase step: §7 Phase 2 task 9 wires the public-layer gtests into CI as a standing dual-configuration job that runs the same wrapper build once each way (disabledandenabled) with both runs required green, so the flag-off and flag-on paths are gated automatically on every change from Phase 2 onward.Legacy Boost.Test layer (
test/) — same rule.MIOpenDriver — the standalone driver is run with both env-var settings on representative shapes for each forwarded op (from Phase 2 onward).
CTest aggregate (
make check) — must be green in the default mode before merging each phase, and additionally in the enabled mode for any phase that introduces or modifies a forwarded op.
8.1 Perf comparison baseline policy#
The prior draft included a wrapper-off vs wrapper-on A/B comparison built into the testing plan as the perf regression detector. That comparison is dropped: with the runtime flag, there is no wrapper-off artifact to compare against on a per-merge basis. The replacement is a two-stage baseline policy:
Phase 1: pre-wrapper baseline snapshot vs
disabled. A snapshot of the pre-wrapperlibMIOpen.sois captured once before the Phase 1 split lands, and a manual, one-time performance comparison ofdisabledagainst that snapshot is run on representative hardware and its results documented, to demonstrate that the wrapper hop is in the noise (Phase 1 exit-criteria 2 and 3; §7 Phase 1 task 14). This is a one-off characterization, not a CI job — there is no per-merge wrapper-off artifact to compare against, so the comparison is conducted and recorded by hand rather than wired into CI. (This snapshot is for the perf comparison only; the symbol-superset and leakage checks in §7 Phase 1 task 8 derive their baseline from the private library in the same build and need no pre-wrapper snapshot.) This is the only point at which the pre-wrapper baseline is the comparison reference.Phase 2 onward:
disabledis the comparison reference. Once Phase 1 has established that wrapper-on (disabled) is performance-equivalent to the pre-wrapper baseline, the snapshot stops being useful as a comparison target. Other MIOpen work is continuing in parallel during this rollout, so a perf measurement taken weeks or months after the snapshot was captured will mix wrapper effects with unrelated drift in the rest of the stack. From Phase 2 onward,MIOPEN_HIPDNN_FORWARDING=enabledis measured againstMIOPEN_HIPDNN_FORWARDING=disabledon the same build, same hardware, same driver, same day — the only valid attribution for “what did forwarding change.”
This two-stage policy is sufficient for the rollout decisions and avoids the maintenance cost of a second build configuration in CI.
New tests added by this work:
A “wrapper disabled” smoke test that asserts, with
MIOPEN_HIPDNN_FORWARDING=disabled, that a call tomiopenConvolutionForwardproduces the same output as the pre-wrapper baseline within tolerance. (Phase 1.)A “wrapper routes to hipDNN” test that asserts, for each forwarded op, the wrapper’s hipDNN path returns the same result as the wrapper’s Private path within tolerance. (Phase 2 onward.)
A microbenchmark target measuring wrapper overhead on a no-op-equivalent entry point. (Phase 1.)
A symbol-set check that runs
nmon the builtlibMIOpen.soandlibMIOpen_private.soto verify the wrapper exports a superset of the private library’s public entry points and exports no_implsymbols. This replaces a dedicated sample-consumer build — MIOpen is consumed the same way as today, so no new sample program is needed. (Phase 1 — guards against dropped entry points and symbol/header leakage from §6.)A provider-link smoke test that compiles a small program against the installed
MIOpen_privateCMake target, includes<miopen/miopen_impl.h>, and verifies that the resulting binary resolves_implsymbols againstlibMIOpen_private.sowith zero unresolved references. (Phase 1.)A provider object-file check (§4.7) verifying the rewired MIOpen provider references only
_implsymbols fromlibMIOpen_private.so— i.e. hipDNN-originated calls reach MIOpen Private directly, not via the wrapper. (Phase 1, re-verified in Phase 4.)
8.2 Leveraging PyTorch tests and benchmarks#
PyTorch is the largest in-tree consumer of MIOpen and exercises the C API along paths that MIOpen’s own test suite does not — real model graphs, autograd-driven shape combinations, mixed-dtype workloads, and the MIOpen tuning DB integration. Because the wrapper preserves the public ABI, PyTorch’s own coverage is a high-value, no-extra-implementation source of integration testing.
PyTorch unit tests — run the convnet, batchnorm, and RNN test modules from PyTorch’s test suite (e.g.
test_nn.py,test_cudnn.pypaths that map to MIOpen on ROCm) against the wrapper build with the default env-var setting (disabled); from Phase 2 onward additionally withMIOPEN_HIPDNN_FORWARDING=enabledand the validated op allow-list. These catch behavioral regressions that MIOpen-only tests miss.TorchBench / model zoo — run a handful of representative workloads (ResNet, BERT, a small transformer training step) under both env-var settings. These provide a real-world signal on overhead and on any subtle numerical regressions introduced by forwarded ops.
PyTorch microbenchmarks —
torch.utils.benchmarkop-level scripts on the ops being forwarded in Phase 2, to spot per-call overhead before it shows up in end-to-end numbers.
Because the wrapper preserves the public API and ABI, this PyTorch coverage is exercised automatically by the existing PyTorch-on-ROCm regression CI once the wrapper build is what ships — no new coverage needs to be stood up. The only Phase 1 action is to confirm the wrapper build is the MIOpen their CI tests against. The same coverage is reused throughout Phases 2–4 (with enabled added from Phase 2).
9. Future Considerations#
Rollout lifecycle. The flag-based design is meant to roll out in three stages, each gating the next on evidence:
Opt-in (Phases 1–3).
MIOPEN_HIPDNN_FORWARDINGdefaults todisabled; the hipDNN backend is compiled in dynamic-load mode (§7 Phase 1 prerequisite) and is loaded only if a consumer opts in. No change for consumers who do not opt in, and no hard hipDNN dependency.Default-on, still reversible. Once benchmarks and downstream projects show no perf or coverage regression across a broad validation pass, the default flips to
enabled(see “Default flip” below). This is expected to be a long stage with wide testing across projects and benchmarks. A regression in the field is recoverable same-day by settingMIOPEN_HIPDNN_FORWARDING=disabledper process or, if severe, by resetting the shipped default back todisableduntil the issue is resolved — so the weak/optional-dependency posture (§4.9) is retained through this stage to keep that rollback path working.Hard dependency, flag retired. Only after the default has been stable at
enabledfor an extended period does the final step makelibhipdnn_backend.soa hard runtime dependency and remove the feature flag(s), retiring the dynamic-load and pass-through machinery. This is the terminal exit gate of the rollout and, like the default flip, is owned by the successor RFC.
Granular feature flags. A single
MIOPEN_HIPDNN_FORWARDINGswitch may be too coarse for the default-on stage. The design anticipates possibly splitting it into a primary hipDNN-backend flag plus per-operation-category flags (e.g. convolution-API routing, normalization-API routing) so forwarding can be enabled or rolled back for one op family without affecting the others. The runtimeMIOPEN_DISABLE_HIPDNN_FOR=<op>opt-out (§4.4) already provides per-op rollback within a single enabled switch; whether to promote that to first-class per-category enable flags is a rollout-time decision for the successor RFC, informed by how validation proceeds op family by op family.Subsequent op families. This RFC’s forwarding scope is the convolution op family only (§7 Phase 2). The expected next target is batchnorm / the normalization API, but it is deliberately out of scope here and is likely to be taken up only after Phase 4 — as follow-on work, potentially folded into the successor RFC that owns the default-flip decision or handled as a dedicated op-onboarding effort. Each additional op family follows the same Phase 2 pattern: add it to the compile-time forwarding set, implement and validate its argument translation with numerical verification, then re-baseline performance. The op-onboarding playbook produced in Phase 4 (§7 Phase 4 task 4) captures this end-to-end procedure from the convolution work and is written to be executed by an AI agent, so it is the intended starting point for batchnorm and every op family after it. The per-op-category rollback knobs (§4.4, and the granular-flag note below) exist so that onboarding a new family later cannot destabilize convolution forwarding already in the field.
Default flip. Whether and when
MIOPEN_HIPDNN_FORWARDINGdefaults toenabled(with some validated op set) depends entirely on Phase 4 measurements. Even after a flip, thedisabledmode stays supported indefinitely. The actual bar for performing the flip — what perf/regression/coverage criteria must be hit, who signs off, what notice period downstream consumers get — is out of scope for this RFC and will be the subject of a successor RFC so that the decision criteria get the design attention they deserve rather than being inferred from this document. Three non-negotiables for the eventual flip, called out here so the implementation lands with the necessary affordances:Communicate loudly. Release notes, ROCm SDK changelog, framework-team direct outreach, and a deprecation-style banner on first wrapper process startup (when the resolved mode is
enabledby default rather than by explicit opt-in) for at least one release cycle. The Phase 1 configuration banner (§7 Phase 1 task 7) provides the hook.Documented disable path. A consumer must be able to opt out of the new default in one step at runtime, without rebuilding. Three layered opt-outs: (a)
MIOPEN_HIPDNN_FORWARDING=disabledat the process level, which forces pure pass-through and never goes away; (b)MIOPEN_DISABLE_HIPDNN_FOR=*for the “wrapper is enabled but I want every op back on MIOpen” case; © per-opMIOPEN_DISABLE_HIPDNN_FOR=<op>for surgical opt-outs. All three are picked up on next process start; no rebuild or library replacement required.Reversible by env var alone. The runtime-flag design makes the flip itself a one-line change in the env-var parser, and it makes a rollback (if the flipped default causes regressions in the field) a same-day operation rather than a re-release of the library.
hipDNN becomes a hard dependency at the terminal step, not the flip itself. To preserve same-day reversibility through the default-on stage, the weak/optional-dependency posture (§4.9) is retained when the default first flips to
enabled:libMIOpen.sostill loads withoutlibhipdnn_backend.so, so a consumer can fall back todisabledeven on an install where hipDNN is absent or broken. Promoting hipDNN to a hard runtime requirement oflibMIOpen.so— and removing the feature flag(s) and the dynamic-load/pass-through machinery — is the terminal step (rollout lifecycle stage 3 above), taken only after theenableddefault has been stable for an extended period. Packaging adds the hard dependency in that terminal release, and the successor RFC owns the coordination. (Note the terminal step is a one-way disruption for installs that previously carried MIOpen without hipDNN; it is the cost of making forwarding permanent and is part of why it is gated behind its own RFC.)
Removing the wrapper later. If hipDNN ever subsumes MIOpen entirely, the wrapper could be retired in favor of consumers calling hipDNN directly. The phased approach keeps that door open.
Static-linking story. Consumers who static-link MIOpen are out of scope here. If they become important, we would need a separate mechanism — likely link-time symbol substitution rather than runtime dispatch.
Windows / ROCm-on-Windows. Direct linkage works the same on both platforms (modulo SONAME / DLL naming), so the design itself is not Windows-specific. The hipDNN-backend dynamic-loading prerequisite (§7 Phase 1) must also be cross-platform —
dlopen/dlsymon Linux,LoadLibrary/GetProcAddresson Windows — so the weak/optional-dependency posture holds on Windows too rather than being a Linux-only affordance. Not blocking for the Linux rollout but called out so the dynamic wrapper is not built Linux-only.
We deliberately do not plan a third routing-heuristics layer (per shape / dtype / arch) on top of the wrapper. hipDNN already has its own routing heuristics, and when hipDNN can’t handle a problem it is expected to fall through to its MIOpen provider — which has MIOpen’s existing heuristics. Adding a third decision layer at the wrapper level would mostly duplicate work already happening one level down. The wrapper’s routing policy stays minimal: hand-curated allow/deny per entry point, driven by what hipDNN supports.
10. Glossary#
MIOpen Public — the new wrapper shared library. Exports the original public API symbol set. Ships as
libMIOpen.so(the filename consumers link against today). Always built.MIOpen Private — the existing MIOpen library, built with its public-API entry points renamed in source to a
_implsuffix, so the wrapper can call them without symbol collision. Ships aslibMIOpen_private.so. Always built._implsuffix — the rename applied in source to each public-API entry point inside MIOpen Private (e.g.miopenConvolutionForward→miopenConvolutionForward_impl).<miopen/miopen_impl.h>— the parallel_impl-form public header (§4.6) that declares the API under its_implnames. Installed alongside<miopen/miopen.h>but exposed only viaMIOpen_private’sINTERFACE_INCLUDE_DIRECTORIES. Used by the rewired MIOpen provider plugin.MIOPEN_HIPDNN_FORWARDING— runtime env var controlling wrapper behavior (§4.1). Values:disabled(default, every call to MIOpen Private) andenabled(per-op routing policy consulted).MIOPEN_DISABLE_HIPDNN_FOR— opt-out env var (§4.4) listing op families that must not be forwarded even whenMIOPEN_HIPDNN_FORWARDING=enabled.=*disables forwarding for every op. This is the documented runtime rollback knob.Compile-time forwarding set — the list, owned by MIOpen developers in the routing policy module, of ops that get forwarded to hipDNN when
MIOPEN_HIPDNN_FORWARDING=enabled. There is no opt-in env var for individual ops; the compile-time list is the only way an op enters the forwarding set, andMIOPEN_DISABLE_HIPDNN_FORis the only way to remove one at runtime.Routing policy — the centralized decision of, per entry point and per call, whether to dispatch to MIOpen Private or to hipDNN. Consulted only when
MIOPEN_HIPDNN_FORWARDING=enabled. Inputs: the compile-time forwarding set,MIOPEN_DISABLE_HIPDNN_FOR, and optionally argument shape.libhipdnn_backend.so— the hipDNN backend shared library the wrapper forwards into. Linked weakly during the rollout (forwarding is opt-in; a missing hipDNN does not blocklibMIOpen.soload) and promoted to a hard dependency when the forwarding default flips (§4.9, §9). The wrapper links only its stable base-graph (V1.0) API surface.MIOpen provider — hipDNN’s existing engine plugin that calls into MIOpen. From Phase 1 onward, it uses direct private linkage: it links
libMIOpen_private.sodirectly and calls_implsymbols rather than going through the wrapper.Direct private linkage (provider) — the Phase 1 wiring change (§4.5) that has the hipDNN MIOpen-provider plugin link and call MIOpen’s private API directly, structurally preventing the wrapper → hipDNN → wrapper loop hazard.
Pass-through / forwarding — shorthand for the wrapper’s two functional modes. Pass-through: every call lands on MIOpen Private. Forwarding: selected ops are routed to hipDNN per the routing policy.