Skip to content

feat(hipdnn): add the generic kernel ingestor foundation behind a default-OFF flag (ALMIOPEN-2402) - #10606

Merged
BrianHarrisonAMD merged 42 commits into
developfrom
users/bharriso/almiopen-2402-ingestor-skeleton
Aug 16, 2026
Merged

BrianHarrisonAMD merged 42 commits into
developfrom
users/bharriso/almiopen-2402-ingestor-skeleton

Conversation

@BrianHarrisonAMD

@BrianHarrisonAMD BrianHarrisonAMD commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds the foundation of the generic kernel ingestor (RFC 0017), behind the default-OFF HIPDNN_ENABLE_KERNEL_INGESTOR flag. Every descriptor kind (UED, KMD, UMD, UDD, UHD, UKD, KDP) exists as a real type cross-referenced by id, and KernelIngestorStateManager answers the three questions hipDNN's four host calls reduce to.

Two reference engines ship on it — hipkernel:PointwiseAdd and hipkernel:PointwiseSub — described by descriptor data plus native symbols resolved by name. Nothing is loaded from a file yet: ALMIOPEN-2401 replaces one function body with a directory scan and the rest keeps working.

JIRA ID : ALMIOPEN-2402

What a second engine cost

PointwiseSub was added last, after the seam was finished, to measure the claim. Three new files plus 12 lines across three shared files — one row in IngestorPacks.cpp's table, two declarations in its header, two CMake lines, one kernel-embedding entry. No matcher, state manager, container, or engine factory changed.

A pack is two .cpp files: <Op>Native.cpp (graph matcher, kernel matcher, scorer, dispatch handler, registration function) and <Op>Descriptors.cpp (the descriptor set). No per-pack header.

The test side cost more — the shared graph-builder header became pack-parametric and three test files followed — so "two files" describes the production surface only.

Risk Assessment

Risk level 2. Every new file and include site is behind #ifdef HIPDNN_ENABLE_KERNEL_INGESTOR, default OFF. Verified with the flag off: 428 provider tests pass, no ingestor test in any binary, no ingestor/ path installed, touching an ingestor source rebuilds nothing.

Two changes reach shared code: the new header-only SDK surface under plugin_sdk/include/hipdnn_plugin_sdk/ingestor/, which is what a second provider would adopt; and an additive arch-name constructor overload on KernelCompileOptions.

ASIC Coverage

Passing PR CI is not sufficient on its own. Every numeric-correctness assertion is SKIP_IF_NO_DEVICES()-gated and both superbuild CI runners are device-less, so CI produces zero signal on correctness. Local and cluster runs are the only proof.

Coverage is gfx90a (MI210) locally and gfx942 (MI300X), both green. No full multi-arch sweep is required: nothing here is architecture-dependent beyond a hipRTC compile of a trivial elementwise kernel through the provider's existing IKernelCompiler path.

Design

Layout. The SDK half is header-only, so another provider can adopt a piece without the whole ingestor. No new engine or plugin ABI interface. Provider half at engines/kernel_ingestor_engine/.

Two mechanisms with opposite lifetimes. Native symbol registration is permanent — the escape hatch survives the loader. Descriptor provenance is temporary scaffolding standing in for files.

A pack declares its architectures. KernelDescriptorPack::arch is checked ahead of every matcher.

Symbols resolve at construction, not first use. A descriptor naming a symbol this build does not ship would otherwise survive load and isApplicable and throw at plan build, past the point applicability is a binding promise. Resolving in the constructor makes it a load-time failure naming the descriptor.

Interfaces worth a review opinion.

  • IKernelDispatchHandler splits into workspaceBytes, prepare, and launch: the workspace query arrives before a kernel is chosen, and a plan outlives the call that built it.
  • KernelSource is a tagged union, not two strings: the kind is the adapter dispatch point. Only EMBEDDED_SOURCE has an adapter; any other kind is a load error.
  • ingestor::DeviceProperties replaces hipDeviceProp_t on IDeviceResolver and MatchContext, making $device.* a closed vocabulary instead of ~90 implied ones and keeping HIP out of a header another provider adopts.

Testing Summary

Plugin SDK and provider unit tests, a concurrency suite (eight threads, also under ThreadSanitizer), frontend E2E against the CPU reference executor, and a flag-off pass covering build, tests, install surface, and incremental rebuild.

Nine behaviours are mutation-verified — the fix reverted and the test confirmed to catch it — including the cache-thrash fix and both packs' refusal guards.

The refusal matrices are the load-bearing pack-layer tests: an under-specified refusal accepts a graph the kernel cannot serve, which is silently wrong rather than a missed optimization.

One branch is deliberately untested and marked so: HandleDeviceResolver falling back when hipGetDevice itself fails needs a broken HIP runtime to reach.

Testing Checklist

  • Plugin SDK unit tests - ninja hipdnn_plugin_sdk_tests && ./bin/hipdnn_plugin_sdk_tests - Status: Passed (237 tests)
  • Plugin SDK tests under ThreadSanitizer - clang++ -fsanitize=thread build of the concurrency suite - Status: Passed (zero race reports)
  • Provider unit tests - ninja hip_kernel_provider_tests && ./bin/hip_kernel_provider_tests - Status: Passed (509 tests, 1 pre-existing arch skip)
  • Provider integration tests - ninja hip_kernel_provider_integration_tests && ./bin/hip_kernel_provider_integration_tests - ASICs: gfx90a - Status: Passed (16 tests, process exit 0)
  • Flag-off build and tests - same targets configured without HIPDNN_ENABLE_KERNEL_INGESTOR - Status: Passed (428 provider tests, no ingestor test present in any binary)
  • Flag-off install surface - cmake --install with the flag off - Status: Passed (0 ingestor/ paths installed)
  • pre-commit - pre-commit run --files $(git diff --name-only) - Status: Passed
  • Cluster verification - ASICs: gfx942 - Status: Passed (237 SDK, 510 provider, 16 integration, exit 0; No skips on gfx942: the arch-gated TestContainer.GetApplicableEngineIdsSdpaGraph that skips on the gfx90a dev box runs here.
  • PR CI - GitHub PR checks - Status: Pending

Technical Changes

  • New SDK surface under plugin_sdk/include/hipdnn_plugin_sdk/ingestor/: the seven descriptor types, KernelIngestorStateManager, GenericEngine/GenericPlanBuilder/GenericPlan, NativeRegistry, SymbolScope, IKernelHeuristic, IKernelDispatchHandler, IDeviceResolver, DeviceProperties, MatchContext, Catalog, LruCache, MakeEngine.
  • Provider engine at engines/kernel_ingestor_engine/: pack table, symbol-registration sweep, discoverDescriptorSets(), run-time engine-name registration, shared device resolver.
  • Two reference packs, two .cpp files each, plus their kernels.
  • Container loops over discovered descriptor sets instead of naming an engine.
  • CI: hipdnn-superbuild-ci.yml configures both jobs with the flag ON. The flag is declared in TheRock (feat(flags): add HIPDNN_ENABLE_KERNEL_INGESTOR build flag TheRock#7194).

@BrianHarrisonAMD BrianHarrisonAMD changed the title feat(hipdnn): add generic kernel ingestor skeleton behind a default-OFF flag (ALMIOPEN-2402) feat(hipdnn): add the generic kernel ingestor foundation behind a default-OFF flag (ALMIOPEN-2402) Aug 10, 2026
@therock-pr-bot

therock-pr-bot Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

✅ All Checks Passed — Ready for Review

Check Status Details
📝 PR Description ✅ Pass —
⛔ Forbidden Files ✅ Pass —
🧪 Unit Test ✅ Pass —
🔎 pre-commit ✅ Pass —
🚫 Draft PR 🔜 To Be Enabled —
🚩 Feature Flag 🔜 To Be Enabled —
📊 Code Coverage 🔜 To Be Enabled —
🤖 therock-pr-bot ✅ Pass —

🎉 All checks passed! This PR is ready for review.

📖 Need help? See the Policy FAQ for details on every check and how to fix failures.

🙋 Wish to Override Policy?

@therock-pr-bot

Copy link
Copy Markdown

🎉 All checks passed! This PR is ready for review.

Base automatically changed from users/sareeder/graph-uuid-identity to develop August 13, 2026 14:27
@SamuelReeder
SamuelReeder force-pushed the users/bharriso/almiopen-2402-ingestor-skeleton branch from da6f632 to 6c30b18 Compare August 13, 2026 14:27
@codecov

codecov Bot commented Aug 13, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Additional details and impacted files
@@           Coverage Diff            @@
##           develop   #10606   +/-   ##
========================================
  Coverage    69.29%   69.29%           
========================================
  Files         2777     2777           
  Lines       457793   457793           
  Branches     67512    67512           
========================================
  Hits        317194   317194           
  Misses      117467   117467           
  Partials     23132    23132           
Flag Coverage Δ *Carryforward flag
TensileLite-CPP 38.07% <ø> (ø) Carriedforward from 86e41e6
TensileLite-Unit 76.15% <ø> (ø) Carriedforward from 86e41e6
hipBLAS 90.62% <ø> (ø) Carriedforward from 86e41e6
hipBLASLt 34.89% <ø> (ø) Carriedforward from 86e41e6
hipCUB 82.68% <ø> (ø) Carriedforward from 86e41e6
hipDNN 86.76% <ø> (ø)
hipFFT 43.60% <ø> (ø) Carriedforward from 86e41e6
hipRAND 76.12% <ø> (ø) Carriedforward from 86e41e6
hipSOLVER 69.03% <ø> (ø) Carriedforward from 86e41e6
hipSPARSE 86.99% <ø> (ø) Carriedforward from 86e41e6
rocBLAS 48.26% <ø> (ø) Carriedforward from 86e41e6
rocFFT 47.64% <ø> (ø) Carriedforward from 86e41e6
rocRAND 57.04% <ø> (ø) Carriedforward from 86e41e6
rocSOLVER 77.10% <ø> (ø) Carriedforward from 86e41e6
rocSPARSE 74.58% <ø> (ø) Carriedforward from 86e41e6
rocThrust 91.60% <ø> (ø) Carriedforward from 86e41e6

*This pull request uses carry forward flags. Click here to find out more.
see 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@BrianHarrisonAMD
BrianHarrisonAMD marked this pull request as ready for review August 13, 2026 23:29
@BrianHarrisonAMD
BrianHarrisonAMD requested review from a team as code owners August 13, 2026 23:29
A pack whose kernel-scoped matchers rejected every kernel logged
"admitted 0 of N kernel(s) after kernel-scoped matching", reading as a
participant that happened to score zero rather than a pack that is out.
The two declines above it, arch and graph-scoped matcher, both say
plainly that the pack is excluded.

Give zero admissions its own message and an explicit continue. The
continue is a no-op today, being the last statement in the loop body; it
is there so the skip sits where the decision is made and so a future
per-pack step inherits it.

mergeBound stays ahead of the admission check deliberately. It is the
only detector for two matchers binding one token to disagreeing values,
which is a descriptor authoring bug, invalid whether or not the pack
admits a kernel on this particular graph. Deferring it would make
detection depend on the workload under match.

No behaviour a caller can observe changes.
The ingestor test fixtures leaked native-registry state between tests, so
the suite only passed in declaration order. Under --gtest_shuffle one seed
produced 147 failures, and most individual tests failed under
--gtest_filter because they depended on a symbol another test registered.

Three fixture bugs, all introduced with these fixtures in this PR:

- makeStateManager() named "test.dispatch" without registering it, unlike
  makeTestDispatches(). It worked only once some other test in the binary
  had registered the symbol.

- ensureNoopDispatchRegistered() documented itself as idempotent but used
  replaceSymbol(), clobbering a handler a test had installed through
  ScopedDispatchRegistration. It now registers only when the symbol is
  absent, which is what the comment always claimed.

- makeStubDispatches() registered through a one-shot function-local
  static. A ScopedDispatchRegistration constructed before its first call
  captures a null previous entry and unregisters on destruction, so the
  static never re-ran and every later caller saw an unresolved symbol.
  That cascade was the source of the remaining shuffle failures.

Two tests also unregistered by hand after their assertions, which leaks
whenever the body throws, feeding the same cascade. They now hold the
shared ScopedResource<T> guard instead, via a scopedGraphMatcher() helper
beside the existing ScopedBlockSizeScore.

Verified: 30/30 shuffle seeds green, and all 239 tests pass individually
under --gtest_filter.
The skip added in af9a0c5 had no test. The log is the whole point of
that change, and SharedLogRecorder makes it assertable, so cover it
rather than leave the branch to a one-off manual check.

Two tests: a pack whose kernel matchers reject everything must say it
admitted no kernel, and must not fall through to the "admitted 0 of N"
contributor message; a pack that admits some still reports the count.

Mutation-verified: with the skip reverted the zero-admit test fails and
the contributor test still passes.
@BrianHarrisonAMD
BrianHarrisonAMD force-pushed the users/bharriso/almiopen-2402-ingestor-skeleton branch from b3f0a09 to 1d02966 Compare August 14, 2026 16:31

@DarylHawkinsAMD DarylHawkinsAMD left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

…(ALMIOPEN-2402)

registerEngineName() throws on a hash collision, and it was called from the
initializer of getEngineDefinitions()'s function-local static. An escaping throw
costs HIP_MLOPS and ASM_SDPA their rows even though neither is implicated, and
C++ leaves the static uninitialized, so the next call reruns the whole
initializer and throws again -- measured with a probe, not assumed: the
initializer ran on all three of three calls after throwing on the first.

The try/catch is per set rather than around the loop, so a collision costs only
the set that caused it and every other engine still registers.

Reported in review. Samuel's ALMIOPEN-2401 removes provider-side registration
entirely (the loader interns the name and this becomes engineNameToId), but the
fix stands on its own until that lands.
…state (ALMIOPEN-2402)

EngineRegistrar's constructor writes the name into the process-wide registry and
the object has no members, so s_registrars was keeping alive something with
nothing to keep. Nothing read it back -- it was the only reference to the type in
the repo. It arrived alongside the string-interning fix and got carried along
with it; the interning is what was load-bearing.

s_names stays: the registry holds string_view, so a name built at run time still
needs storage outliving it, and it stays a deque because reallocation would move
a registered view's string.

Checked bugprone-unused-raii, which is enabled in both .clang-tidy files: it does
not fire on this, since EngineRegistrar has no destructor to discard.

Reported in review.
…g again (ALMIOPEN-2402)

GenericEngine::initializeExecutionContext populates the settings from
engineConfig and stores them on the context immediately before calling
buildPlan, which then parsed the same config a second time. Beyond the wasted
work it was a second place for the two paths to disagree: getMaxWorkspaceSize
already reads the stored filter, so the two knob consumers derived the same
answer by two routes. buildPlan now reads executionContext.executionSettings()
and engineConfig is unused there, matching SdpaBwdPlanBuilder.

That turns TContext into a real requirement, so it is documented on the class
alongside the TSettings one.

The two test stub contexts had no-op setExecutionSettings and no accessor, which
is why nothing caught this. Both now store the settings, and the two tests that
called buildPlan directly run the sequence the engine runs --
initializeExecutionSettings, setExecutionSettings, buildPlan -- instead of a
state the engine never produces.

Mutation-verified: dropping the setExecutionSettings call makes
HonorsAnExplicitKnobSettingOverTheHeuristicDefault fail, so the knob path is
genuinely covered rather than passing incidentally.

Reported in review.
…der (ALMIOPEN-2402)

A UHD was mandatory: DescriptorSet::heuristic was a value, and the state manager
rejected a null scorer outright. An engine with no ranking model yet had nothing
to supply, so the skeleton's two packs each invented one.

heuristic and EngineDescriptor::heuristicId are now optional. When absent,
makeKernelHeuristic returns DeclaredOrderKernelHeuristic and logs a warning
naming the engine.

The fallback adds no ordering rule of its own; it scores every kernel alike and
lets rank()'s existing tie-break decide, which is already priority descending
then descriptor id ascending. So "no heuristic" means declared order, and
ranking stays total, stable across runs, and deterministic -- the absence of a
model costs selection quality, never reproducibility. A fallback that ranked on
kernel metadata would instead outrank priority, which is the one signal an
engine without a model still has; the tests pin that by giving the losing kernel
the larger block size, so a metadata-sensitive scorer fails them.

The warning is the reason this is not silent. An engine that means to declare
its order and one still waiting on a model are otherwise indistinguishable from
the outside, which is what jdcampbe asked for in review. It names the engine,
since an unnamed warning cannot tell an operator where to look; makeEngine
therefore describes the engine before moving it out of the set, or the name
would be read from a moved-from string.

The state manager's null-heuristic rejection stays. Absent and null are
different: absent is a descriptor that declined to name a model, null is a
caller that failed to build one, and only the first is legitimate.

Verified: 246 plugin SDK, 509 provider (1 pre-existing arch skip), 16
integration on gfx90a, 428 flag-off. Mutation-verified: the warning test fails
when the message stops naming the engine, and all three ordering tests fail
when the fallback scores on kernel metadata instead of a constant.
…ph (ALMIOPEN-2402)

Three review nits in the state manager, all on the same path.

buildCatalog called completeMetadata() for every kernel of every pack on every
uncached graph, rebuilding a KernelDefinition whose every field -- id, pack,
dispatch, source, completed metadata, priority -- is context-independent. The
definitions are now built once in validateAndIndexPacks, which was already
computing the same completed tuples to check uniqueness and then discarding
them, and buildCatalog copies them. Held parallel to _packs rather than keyed by
kernel id, since nothing validates that ids are unique within an engine and a
map would have quietly assumed it.

The duplicate-tuple check scanned a vector, so validation was quadratic in an
engine's kernel count. It is a std::set now; MetadataValues is an ordered map,
so it already orders and no comparator is needed.

The catalog cache hit returned *cached, copying a Catalog that LruCache::get had
already copied out. It moves out of that local instead.

Mutation-verified: disabling the duplicate-tuple guard fails
RejectsTwoKernelsSharingAMetadataTuple, so the set conversion is covered rather
than merely compiling. The integration suite is the check that matters for the
precompute, since it runs real dispatch through the matcher path: 16 passed on
gfx90a, alongside 246 plugin SDK, 509 provider, 428 flag-off.
…iopen-2402-ingestor-skeleton

# Conflicts:
#	.github/workflows/hipdnn-superbuild-ci.yml

@SamuelReeder SamuelReeder left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

…2402)

score() is supplied by the pack and resolved by symbol, so its value is outside
rank()'s control and NaN is reachable. NaN compares false against everything, so
it read as equivalent to every kernel while real scores stayed ordered among
themselves -- not a strict weak ordering, which stable_sort requires, and
therefore undefined behaviour.

The visible damage is to the other kernels, not the NaN one: a probe of the exact
comparator over 3 NaN scores reports 60 transitivity violations and sorts 7 after
1, so the worst kernel can be selected first. libstdc++ tolerates it; MSVC's
iterator debug level asserts on it.

Map non-finite to -infinity as the score enters the sort. The order stays total,
NaN-scoring kernels sink below every finite one, and the existing priority ->
kernelId tie-break still orders them, so a pack returning NaN loses selection
quality without costing determinism. Infinities are already well-ordered and pass
through untouched.

Mutation-verified: with the normalization reverted, all three NaN tests fail and
the infinity test correctly still passes.

Note the determinism-only assertion this started as was too weak to keep --
stable_sort against a broken comparator is reliably wrong rather than random, so
ranking twice and comparing passes even with the bug present. The test now asserts
the finite subsequence is still descending, which is the invariant that breaks.
…IOPEN-2402)

Every ingestor header is wholly inside #ifdef HIPDNN_ENABLE_KERNEL_INGESTOR, and
all 16 are installed whenever the flag is set. The exported hipdnn_plugin_sdk
INTERFACE target carried include directories and link dependencies but no compile
definitions, so a consumer doing find_package(hipdnn_plugin_sdk CONFIG REQUIRED)
received the headers on disk and they preprocessed to nothing -- the types simply
absent, with no diagnostic naming the cause.

In-tree this was masked by two side doors: add_compile_definitions scoped to
projects/hipdnn, and a target_compile_definitions on the provider's object
library. Neither reaches an out-of-tree consumer, and
samples/example_engine_plugin is exactly that consumer.

INTERFACE propagates to both the build-tree export and the installed
hipdnn_plugin_sdkTargets.cmake, so in-tree and out-of-tree agree.

Verified against a real install tree: a TU naming ingestor::Catalog through
find_package compiles and runs. Mutation-verified by stripping
INTERFACE_COMPILE_DEFINITIONS from the installed targets file, which reproduces
the reported failure exactly -- "use of undeclared identifier
'hipdnn_plugin_sdk'". Flag-off still installs 0 ingestor paths and exports the
definition 0 times.
hipdnn_backend_logging_shutdown_tests fails intermittently on the Windows
superbuild with exit code 0xC0000409. That code reads as STATUS_STACK_BUFFER_OVERRUN
but is misleading: MSVC routes std::abort() through __fastfail, and the abort here
is the test's own, taken when a callback worker ends with zero invocations.

The test asserted a guarantee the library never made. It documents null-before-drain
as the intended shutdown contract, and dropping queued callbacks during static
destruction is correct -- the user's handle may already be gone. But the readiness
gate claimed it ensured callbacks "have been invoked at least once" while only ever
proving they were ENQUEUED: it signalled as soon as the API call returned. Delivery
happens later, on the shared logger's single background thread, behind two callbacks
that sleep 1ms per message. So main() could return -- starting shutdown, which
disables every callback before draining the queue -- while a worker's only message
was still queued. That message is then dropped and the worker ends at zero.

Signal readiness only once the callback has actually fired. Plain workers register
no callback and keep today's enqueue-only behaviour, which is correct for them.

Reproduced deterministically by widening the slow callback 1ms -> 40ms and pinning
to 2 CPUs: 15/15 failures before, 15/15 passes after, with the same victim
distribution as CI (later-registered workers starve first). With the real 1ms sleep
restored, 40/40 pass. No production code changes, so the library's shutdown
semantics are untouched.

Whether loggerShutdown() should drain the queue before nulling the holders rather
than after is a separate question about logging semantics at exit, and is
deliberately not answered here.
@BrianHarrisonAMD
BrianHarrisonAMD merged commit 8bb6c74 into develop Aug 16, 2026
65 checks passed
@BrianHarrisonAMD
BrianHarrisonAMD deleted the users/bharriso/almiopen-2402-ingestor-skeleton branch August 16, 2026 22:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants