Skip to content

fix(amdgpu): forward custom op domains from the hip backend too - #102

Open
AMDmoore wants to merge 2 commits into
mainfrom
fix/amdgpu-forward-hip-custom-op-domains
Open

fix(amdgpu): forward custom op domains from the hip backend too#102
AMDmoore wants to merge 2 commits into
mainfrom
fix/amdgpu-forward-hip-custom-op-domains

Conversation

@AMDmoore

@AMDmoore AMDmoore commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • GetNumCustomOpDomains/GetCustomOpDomains on the AMDGPU umbrella EP factory only forwarded to the DirectML backend factory, never to the HIP backend factory.
  • Custom op schema registration happens once, at Model::Load, before any session/EP exists and before the profile provider option selects a backend. The union of every built backend's custom op domains must therefore be reported here, regardless of which backend a given model eventually routes to.
  • Without this fix, a graph containing a HIP-only custom op (e.g. com.amd::QMoE, used by fused Nemotron-H MoE models) fails schema resolution at Model::Load even though the HIP backend is built and would otherwise handle the node.
  • Extends both functions to also forward to hip_ep_factory_ (guarded by USE_HIP, matching the existing USE_DML guard), tracking a byte offset across backends in GetCustomOpDomains so each backend's domains are written into its own slice of the caller-provided buffer.

Test plan

  • Built amdgpu-ep.dll + hip-backend.dll (fresh, from this branch) plus directx-backend.dll/migraphx-backend.dll on Windows/gfx1151 (TheRock ROCm SDK), deployed alongside a pre-existing known-good hipgpu.dll/hip-compiler.dll/custom kernels build.
  • Ran an OGA model_benchmark end-to-end pass against a real NVIDIA-Nemotron-3-Super-120B QMoE-fused model (genai_config.json provider = AMDGPU, profile = hip) with HIPDNN_EP_STRICT=1 (no silent CPU fallback):
    • Model::Load succeeded (the com.amd::QMoE schema resolved through the AMDGPU factory's forwarded HIP domains).
    • Graph optimization and full MLIR compilation completed successfully.
    • Execution failed only at hipMalloc failed for constants blob (68000755968 bytes) inside InferenceState::create -- the model's 68GB of constants exceed the 34GB VRAM on the test GPU (gfx1151). This is an orthogonal hardware-capacity limitation, not a schema/domain-forwarding regression, and matches the exact known failure signature from prior HIP-EP-direct validation of the same model.
  • This confirms the domain-forwarding fix is necessary and sufficient for HIP-only custom ops to be visible during graph loading through the AMDGPU umbrella EP.

@CLAassistant

CLAassistant commented Aug 24, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Comment thread src/amdgpu/gpu_factory.cc Outdated
RETURN_IF_ERROR(hip_ep_factory_->GetNumCustomOpDomains(hip_ep_factory_, &hip_num_domains));
hip_num_domains = std::min(hip_num_domains, num_domains - offset);
if (hip_num_domains > 0) {
RETURN_IF_ERROR(hip_ep_factory_->GetCustomOpDomains(hip_ep_factory_, domains + offset, hip_num_domains));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What happens if HIP EP returns custom op domains on an unsupported GPU? HIP EP is only built to support gfx1151 at the moment.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good question. Whether the custom op or standard op, I think hip-ep has the same behavior for an unsupported GPU. If profiler is configured as hip, ORT can recognize the custom ops that hip-ep registers, then hip-ep compiler can compile successfully but fails at runtime due to unsupported GPU, either hipmalloc error or kernel launch failure.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Have you tested on non-gfx1151 hardware with a gpu ep produced build that only has gfx1151 tensile/custom_operator files? We have to make sure there is good fallback behavior that doesn't crash.

@AMDmoore AMDmoore Aug 31, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Hi @tperry-amd, do you know if anyone has tested this base case before? a GPU EP produced build that only has gfx1151 standard ops on non-gfx1151 hardware, what's the base behavior?

As you requested, I did a test with prebuilt gfx1151 package running on gfx1150 GPU and gfx1150 theRock for win.
case1: profile as auto, EP for Hipep backend is not created.
(1) standard op model like Llama-3.1-8B, fallback to CPU EP and run successfully.
(2) custom op model like com.amd::QMoE, fallback to CPU EP but ORT throws exception, message like "hip-ep-registered custom op node 'qmoe_amd_L0' fell back to CPU: this op is implemented only as a GPU kernel in the hip-ep backend and has no CPU kernel."
case2: profile as hip, only Hipep backend is selected.
(1) standard op model like Llama-3.1-8B, ORT session initialization fails.
(2) custom op model like com.amd::QMoE, also ORT session initialization fails.
These two cases both fails like [2026-08-31 01:59:39.8219620 [E:onnxruntime:, inference_session.cc:2812 onnxruntime::InferenceSession::Initialize::<lambda_7>::operator ()] Exception during initialization: ].

So, as I replied some days ago, the amdgpu behavior has no difference between standard op and custom op on an unsupported GPU platform, it will fall back to CPUEP for case1 and throw exception for case2. By this PR, it only changes the exception from ORT model load (op not supported) to the other places.
For case1-(2), I think there should be more effort to ease the arch restrictions from gfx1151.

cc: @amd-mingw @zz002

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hi, @tperry-amd which fallback path is expected? Thanks! cc @AMDmoore

Have you tested on non-gfx1151 hardware with a gpu ep produced build that only has gfx1151 tensile/custom_operator files? We have to make sure there is good fallback behavior that doesn't crash.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hi @amd-mingw and @AMDmoore, I am mostly referring to a GPU that HIP EP doesn't support today like Navi 31 or Navi 48. Does the hip ep register ops in those cases? Does it only register ops for compatible hardware?

I'm wondering if we need to gate this behind a similar heuristic that we have today. Currently we gate HIP EP on gfx1151 + LLM model arch. (Obviously the gfx1151 will expand in the future)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hi @tperry-amd , Custom OP register only has implementation for HIP EP now, If heuristic not route to hip-ep, then it will fallback to migraphx EP? DML EP? CPU EP? If want the fallback EP not crash, they will need support this custom op, too, but that wasn’t the original design intent. The intent was only for the HIP EP to support this custom op.

@amd-mingw amd-mingw Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@tperry-amd , Custom OP register happen before ORT complete register EP,
for example, VitisAI EP register 2 custom_ops, and the OP register happen before VitisAI EP register completed:
image

for our AMDGPU EP, the OP register will happen before AMDGPU EP register completed:
so for your question:
Does the hip ep register ops in those cases? --Yes
Does it only register ops for compatible hardware? --No

for a model with custom op, If not register it, it will report error and exit. If no this PR, for the fallback EP(migraphx EP/DML EP/CPU EP), it will report error and exit, too, the behavior is similar with/without this PR.

any suggest approach to support this custom op feature? Thanks!

@tperry-amd tperry-amd Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

My main concern is here that this change doesn't break cert on non-hipep use-cases (like Stable Diffusion) and non-hipep hardware (like Navi4x).

Based your answer "Does it only register ops for compatible hardware? --No", it seems to be that you register ops for incompatible hardware which may result in hardware crashes, TDR, etc.

Please understand that HIP EP is not the only provider, other providers need to function as normal. If they don't, then the entire software cannot ship (including HIP EP) since it will fail cert.

I think the question I need answered. Do the ops get registered if hip backend isn't selected?

@AMDmoore
AMDmoore marked this pull request as ready for review August 26, 2026 11:19
@AMDmoore
AMDmoore requested a review from tperry-amd August 27, 2026 02:49
Comment thread src/amdgpu/gpu_factory.cc Outdated
#ifdef USE_DML
if (dml_ep_factory_ != nullptr && dml_ep_factory_->GetCustomOpDomains != nullptr) {
RETURN_IF_ERROR(dml_ep_factory_->GetCustomOpDomains(dml_ep_factory_, domains, num_domains));
size_t dml_num_domains{};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can you reduce the code duplication here? It should just be a function that accepts a factory pointer since the logic between the dml and hip ep factory are identical it seems, it's just the factory pointer that's different.

Comment thread src/amdgpu/gpu_factory.cc Outdated
RETURN_IF_ERROR(hip_ep_factory_->GetNumCustomOpDomains(hip_ep_factory_, &hip_num_domains));
hip_num_domains = std::min(hip_num_domains, num_domains - offset);
if (hip_num_domains > 0) {
RETURN_IF_ERROR(hip_ep_factory_->GetCustomOpDomains(hip_ep_factory_, domains + offset, hip_num_domains));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Have you tested on non-gfx1151 hardware with a gpu ep produced build that only has gfx1151 tensile/custom_operator files? We have to make sure there is good fallback behavior that doesn't crash.

@AMDmoore
AMDmoore force-pushed the fix/amdgpu-forward-hip-custom-op-domains branch from dddde34 to 4911385 Compare September 1, 2026 04:25
GetNumCustomOpDomains/GetCustomOpDomains only forwarded to the DirectML

backend factory. Schema registration happens once, at Model::Load, before

any session/EP or profile selection exists, so the union of every built

backend's domains must be reported regardless of which backend a given

model eventually routes to. Without this, a graph containing a HIP-only

custom op (e.g. com.amd::QMoE) fails schema resolution during Model::Load

even though the HIP backend is built and would otherwise handle the node.

Extend both functions to also forward to hip_ep_factory_ (guarded by

USE_HIP, matching the existing USE_DML guard), tracking an offset across

backends in GetCustomOpDomains so each backend's domains are written into

its own slice of the caller's buffer.

Co-authored-by: Cursor <cursoragent@cursor.com>
@AMDmoore
AMDmoore force-pushed the fix/amdgpu-forward-hip-custom-op-domains branch from 4911385 to 288144b Compare September 1, 2026 06:17
GetNumCustomOpDomains and GetCustomOpDomains each repeated the same null-check, count, clamp and append sequence once per backend, with only the factory pointer differing, behind their own USE_DML/USE_HIP guards.

Collect the backends that can contribute custom op schemas into custom_op_backends_ as each factory is created, and iterate that member in both functions. The push_back sites sit inside the USE_DML/USE_HIP blocks the constructor already has, so no new preprocessor guard is introduced, and the build with neither backend no longer produces unused variables needing (void) casts. Both functions now walk the same list in the same order, so the offset-sliced layout GetCustomOpDomains writes cannot drift from what GetNumCustomOpDomains counted. Adding a backend is one push_back at its load site.

No behavior change. Verified warning-free at /W4 with USE_DML+USE_HIP, USE_HIP only, USE_DML only, and neither backend. Checked end to end through the amdgpu EP on an 8-layer nemotron submodel carrying com.amd::QMoE: it loads and compiles when built with USE_HIP, and Model::Load fails with 'com.amd:QMoE(-1) is not a registered function/op' when built without it -- before backend routing runs, which is why the union has to be reported at Model::Load. Load, compile and run results are identical to the pre-refactor build.

Co-authored-by: Cursor <cursoragent@cursor.com>
@tperry-amd
tperry-amd requested a review from KenLagos September 3, 2026 05:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants