Skip to content

Fix MLIR conv-pointwise-layout fusion splitting - #5064

Open
justinrosner wants to merge 8 commits into
developfrom
1067-cached-tuning-db-error
Open

Fix MLIR conv-pointwise-layout fusion splitting#5064
justinrosner wants to merge 8 commits into
developfrom
1067-cached-tuning-db-error

Conversation

@justinrosner

@justinrosner justinrosner commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Motivation

Some gpu::mlir_op convolution + pointwise fusions can fail to compile when the fused module is followed by layout-only operations such as reshape_lazy or transpose. In these cases, MIGraphX was unable to use the selected perfConfig for the fused module, and was also failing to properly split the module into separate conv + pointwise kernels because of the layout tail.

Technical Details

Update the GPU JIT MLIR compiler to detect layout-only tails after pointwise fusion and split the fused op into separate convolution MLIR, pointwise, and layout-copy kernels when needed. This keeps the MLIR portion compatible with the selected perf config while preserving the final output layout.

Changelog Category

Add a CHANGELOG.md entry for any option other than Not Applicable

    • Added: New functionality.
    • Changed: Changes to existing functionality.
    • Removed: Functionality or support that has been removed. (Compared to a previous release)
    • Optimized: Component performance that has been optimized or improved.
    • Resolved Issues: Known issues from a previous version that have been resolved.
    • Not Applicable: This PR is not to be included in the changelog.

Follow the LLVM AI Tool Use Policy for contributions using AI.

Copilot AI 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.

Pull request overview

This PR prevents gpu::compile_ops from blindly reusing cached MLIR tuning solutions (perfConfig strings) that no longer apply to a fused MLIR submodule, by validating cached solutions against the current fused module before reuse. This fits into the GPU backend’s tuning/benchmarking pipeline and improves robustness when problem-cache keys collide across different MLIR module structures.

Changes:

  • Add an applicability check for cached gpu::mlir_op solutions using is_module_fusible() before reusing a cached perfConfig.
  • Fall back to configured tuning candidates (and benchmarking when enabled) when a cached solution is null or inapplicable.
  • Add a verify regression that seeds MIGRAPHX_PROBLEM_CACHE with a known-bad MLIR perfConfig for a conv+pointwise fused path.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
test/verify/test_conv_add_tune.cpp Adds a regression that seeds the problem cache with a bad MLIR perfConfig and exercises a fused conv/add MLIR path.
src/targets/gpu/compile_ops.cpp Validates cached MLIR solutions against the fused submodule before reusing them; falls back to candidate selection/benchmarking otherwise.
CHANGELOG.md Documents the fix for cached gpu::mlir_op perfConfig reuse causing compilation failures.

Comment thread test/verify/test_conv_add_tune.cpp Outdated
Comment thread test/verify/test_conv_add_tune.cpp Outdated
Comment thread src/targets/gpu/compile_ops.cpp Outdated
Comment thread src/targets/gpu/compile_ops.cpp Outdated

@umangyadav umangyadav left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Changes look good to me but let's wait on someone from MIGraphX team to review

@pfultz2 pfultz2 left a comment

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.

I dont think this is a good approach. I think it would be better to just add is_module_fusible to the problem config.

Comment thread test/verify/test_conv_add_tune.cpp Outdated
Comment thread src/targets/gpu/compile_ops.cpp Outdated
@justinrosner

Copy link
Copy Markdown
Contributor Author

I dont think this is a good approach. I think it would be better to just add is_module_fusible to the problem config.

I'm not sure that I follow. How would we add is_module_fusible to the problem config?

@pfultz2

pfultz2 commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

I saw this specifically with a splitK perfConfig cached for a standalone convolution problem that was later reused for a fused convolution plus pointwise MLIR module

That shouldn't fail, we split the module when is_module_fusible is false: https://github.com/ROCm/AMDMIGraphX/blob/develop/src/targets/gpu/jit/mlir.cpp#L215

@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Additional details and impacted files
@@           Coverage Diff            @@
##           develop    #5064   +/-   ##
========================================
  Coverage    93.09%   93.09%           
========================================
  Files          623      623           
  Lines        33073    33073           
========================================
  Hits         30789    30789           
  Misses        2284     2284           
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@gh-app-migraphx-bot-pr-write

gh-app-migraphx-bot-pr-write Bot commented Jul 14, 2026

Copy link
Copy Markdown
Test Batch New Rate (7df825) Old Rate (55ca93) Diff Status
torchvision-resnet50 64 3,103.80 3,169.20 -2.06%
torchvision-resnet50_fp16 64 6,724.24 6,721.88 0.04%
torchvision-densenet121 32 699.34 2,712.99 -74.22% 🔴
torchvision-densenet121_fp16 32 434.22 4,578.90 -90.52% 🔴
torchvision-inceptionv3 32 634.48 1,805.02 -64.85% 🔴
torchvision-inceptionv3_fp16 32 1,662.07 2,855.31 -41.79% 🔴
cadene-inceptionv4 16 716.84 824.13 -13.02% 🔴
cadene-resnext64x4 16 756.21 785.46 -3.72%
slim-mobilenet 64 8,451.19 8,444.46 0.08%
slim-nasnetalarge 64 171.20 229.50 -25.40% 🔴
slim-resnet50v2 64 1,045.58 3,185.35 -67.18% 🔴
bert-mrpc-onnx 8 1,175.05 1,173.56 0.13%
bert-mrpc-tf 1 73.17 484.20 -84.89% 🔴
pytorch-examples-wlang-gru 1 116.49 472.27 -75.33% 🔴
pytorch-examples-wlang-lstm 1 99.78 386.76 -74.20% 🔴
torchvision-resnet50_1 1 154.25 754.58 -79.56% 🔴
cadene-dpn92_1 1 62.67 444.58 -85.90% 🔴
cadene-resnext101_1 1 89.50 366.32 -75.57% 🔴
onnx-taau-downsample 1 237.31 402.99 -41.11% 🔴
dlrm-criteoterabyte 1 26.02 32.58 -20.14% 🔴
dlrm-criteoterabyte_fp16 1 52.71 52.65 0.11%
agentmodel 1 9,735.87 7,784.58 25.07% 🔆
unet_fp16 2 55.59 57.48 -3.29%
resnet50v1_fp16 1 755.86 932.89 -18.98% 🔴
resnet50v1_int8 1 929.89 934.74 -0.52%
bert_base_cased_fp16 64 707.82 1,105.09 -35.95% 🔴
bert_large_uncased_fp16 32 347.56 347.51 0.01%
bert_large_fp16 1 91.76 205.61 -55.37% 🔴
distilgpt2_fp16 16 1,321.36 2,104.89 -37.22% 🔴
yolov5s 1 568.28 559.63 1.55%
tinyllama 1 25.15 46.13 -45.48% 🔴
vicuna-fastchat 1 44.24 44.16 0.18%
whisper-tiny-encoder 1 415.39 415.45 -0.02%
whisper-tiny-decoder 1 411.12 412.20 -0.26%
llama2_7b 1 20.85 21.00 -0.71%
qwen1.5-7b 1 19.32 23.73 -18.56% 🔴
phi3-3.8b 1 26.85 26.83 0.06%
llama3-8b 1 7.89 21.87 -63.94% 🔴
whisper-large-encoder 1 4.00 10.23 -60.92% 🔴
whisper-large-decoder 1 25.95 104.89 -75.26% 🔴
mistral-7b 1 23.76 23.89 -0.56%
FLUX.1-schnell 1 747.85 760.54 -1.67%

Regressions detected 🔴

@gh-app-migraphx-bot-pr-write

gh-app-migraphx-bot-pr-write Bot commented Jul 14, 2026

Copy link
Copy Markdown
Test Status Result
bert-mrpc-onnx PASSED: MIGraphX meets tolerance
bert-mrpc-tf PASSED: MIGraphX meets tolerance
pytorch-examples-wlang-gru PASSED: MIGraphX meets tolerance
pytorch-examples-wlang-lstm PASSED: MIGraphX meets tolerance
dlrm-criteoterabyte PASSED: MIGraphX meets tolerance
agentmodel PASSED: MIGraphX meets tolerance
unet PASSED: MIGraphX meets tolerance
resnet50v1 PASSED: MIGraphX meets tolerance
bert_base_cased_fp16 PASSED: MIGraphX meets tolerance
bert_large_uncased_fp16 🔴 FAILED: MIGraphX is not within tolerance - check verbose output
bert_large PASSED: MIGraphX meets tolerance
yolov5s PASSED: MIGraphX meets tolerance
tinyllama PASSED: MIGraphX meets tolerance
vicuna-fastchat PASSED: MIGraphX meets tolerance
whisper-tiny-encoder PASSED: MIGraphX meets tolerance
whisper-tiny-decoder PASSED: MIGraphX meets tolerance
distilgpt2_fp16 PASSED: MIGraphX meets tolerance
llama2_7b PASSED: MIGraphX meets tolerance
qwen1.5-7b PASSED: MIGraphX meets tolerance
phi3-3.8b PASSED: MIGraphX meets tolerance
llama3-8b PASSED: MIGraphX meets tolerance
whisper-large-encoder PASSED: MIGraphX meets tolerance
whisper-large-decoder PASSED: MIGraphX meets tolerance
mistral-7b PASSED: MIGraphX meets tolerance
FLUX.1-schnell PASSED: MIGraphX meets tolerance

@justinrosner justinrosner changed the title Validate cached MLIR tuning solutions before reuse Fix MLIR conv-pointwise-layout fusion splitting Jul 15, 2026
@justinrosner
justinrosner requested a review from pfultz2 July 15, 2026 01:07
@justinrosner

Copy link
Copy Markdown
Contributor Author

I saw this specifically with a splitK perfConfig cached for a standalone convolution problem that was later reused for a fused convolution plus pointwise MLIR module

That shouldn't fail, we split the module when is_module_fusible is false: https://github.com/ROCm/AMDMIGraphX/blob/develop/src/targets/gpu/jit/mlir.cpp#L215

@pfultz2 I believe I tracked down the root cause to the selected MLIR perfConfig rejecting the full conv + pointwise + layout fused module, and then the existing fallback only knew how to split into MLIR kernel + pointwise kernel. That two-way fallback assumes the pointwise portion can write directly into the original final output buffer. Once there is a layout-only tail, that assumption breaks, because the pointwise output and final output layout/shape are separate stages.

I am still worried about overall performance when trying to use the same perfConfigs for problemConfigs that index back to both fused and non-fused kernels. I know this is something that the rocMLIR team has talked about in the past before, so maybe we can try to come up with a better solution for this now. @dhernandez0 @umangyadav @pabloantoniom

@justinrosner
justinrosner requested a review from umangyadav July 15, 2026 01:31
Comment thread test/verify/test_conv_add_tune.cpp Outdated
return *it;
}

static optional<instruction_ref> find_layout_tail_split(instruction_ref pointwise_ins)

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.

I dont think you need to write another function for this. reshape_lazy just needs to be added to the list in find_final_split.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I saw your comment down here: #5064 (comment). Is the current function approach that I have the proper way to go about solving this? Or is this a different underlying root cause where there should never be a reshape_lazy in the first place?

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.

Ok I looked into more. The problem happens with reshape as well as reshape_lazy. I dont think we need to replace the find_final_split function, we can just add reshape_lazy to the list. It doesnt completely fix it though.

The problem is that we cant compile the pointwise module with reshapes in them, so we need to split the tail module and compile the pointwise and then insert the last half.

Comment thread test/verify/test_conv_add_tune.cpp Outdated
Comment thread test/verify/test_conv_add_tune.cpp Outdated
Comment thread test/verify/test_conv_add_tune.cpp Outdated
@pfultz2

pfultz2 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

I saw this specifically with a splitK perfConfig cached for a standalone convolution problem that was later reused for a fused convolution plus pointwise MLIR module

That shouldn't fail, we split the module when is_module_fusible is false: https://github.com/ROCm/AMDMIGraphX/blob/develop/src/targets/gpu/jit/mlir.cpp#L215

@pfultz2 I believe I tracked down the root cause to the selected MLIR perfConfig rejecting the full conv + pointwise + layout fused module, and then the existing fallback only knew how to split into MLIR kernel + pointwise kernel. That two-way fallback assumes the pointwise portion can write directly into the original final output buffer. Once there is a layout-only tail, that assumption breaks, because the pointwise output and final output layout/shape are separate stages.

We already check for these "layout"-ops(which it more accurately called shape transforms as these ops do not change the memory layout) when we do the split, but we missed the check for reshape_lazy. We do check for reshape though.

I am still worried about overall performance when trying to use the same perfConfigs for problemConfigs that index back to both fused and non-fused kernels. I know this is something that the rocMLIR team has talked about in the past before, so maybe we can try to come up with a better solution for this now. @dhernandez0 @umangyadav @pabloantoniom

It needs to add is_module_fusible to the problem key so we treat them as different problems to tune for.

@pfultz2

pfultz2 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Actually, we shouldn't be adding reshape_lazy to the mlir submodule in the first place. This can cause problems if the layout changes. I wonder where the reshape_lazy is coming from as we dont insert reshape_lazy until lowering which happens after fuse_mlir.

@justinrosner

Copy link
Copy Markdown
Contributor Author

It needs to add is_module_fusible to the problem key so we treat them as different problems to tune for.

I think we should be careful with that because is_module_fusible depends on the selected perfConfig (the function’s solution param), not just the problem key/problem config. The same problem key can return either true or false depending on which perfConfig is being tested. For example, one config may fit the fused op into LDS while another does not. So adding that boolean to the problem key would encode a perfConfig-level property as if it were a problemConfig-level property (i.e., a property of the kernel itself).

I do see the issue you’re trying to solve, though. Would it make more sense to add a problemConfig-level capability, like supportsSplitK, instead of using is_module_fusible directly? That check is independent of the chosen perfConfig, so it seems much safer to include in the problem key than the result of is_module_fusible.

@causten

causten commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

There are now models and usecases where this PR would resolve failures. @pfultz2 please work with @justinrosner to resolve

@justinrosner
justinrosner force-pushed the 1067-cached-tuning-db-error branch from 7df8254 to d118e6f Compare August 11, 2026 15:06
@justinrosner

Copy link
Copy Markdown
Contributor Author

Actually, we shouldn't be adding reshape_lazy to the mlir submodule in the first place. This can cause problems if the layout changes. I wonder where the reshape_lazy is coming from as we dont insert reshape_lazy until lowering which happens after fuse_mlir.

It looks like they are being inserted by fuse_mlir in the find_channel_slice_convolution logic,

}))
return nullopt;
return *std::prev(it);
}

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.

This doesnt split after an add or mul like find_final_split does which prevents fusing these operators with the gemm.

Why arent you using find_final_split and just add reshape_lazy to the list?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated to use find_final_split again

std::array<module_with_inputs, 2> mods = {std::move(mod_splits3[0]),
std::move(mod_splits3[1])};
return insert(cops, mods, ins, split_ins);
}

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.

Whats the reason for changing all this code? What is this trying to fix?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is modified slightly in the most recent version, but my intention with this was to handle the case where after splitting off the trailing shape transform, the pointwise kernel can no longer write directly to the final output allocation (because of the transpose/reshapes)

@justinrosner
justinrosner requested a review from pfultz2 August 17, 2026 14:59
{
auto tail_path = get_output_path(tail_split.value());
if(std::find(tail_path.begin(), tail_path.end(), split_ins) != tail_path.end())
split_ins = tail_split.value();

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.

The tail_split should not replace the split_ins. You need both split instructions. The split_ins is the barrier between the mlir and pointwise kernel and the tail_split is the barrier between the pointwise and "reshapes".

if(std::find(tail_path.begin(), tail_path.end(), split_ins) != tail_path.end())
split_ins = tail_split.value();
}
std::array<module_with_inputs, 2> mod_splits = smod->split(input_args, {split_ins});

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.

This should be std::array<module_with_inputs, 3> mod_splits = smod->split(input_args, {split_ins}, tail_split) since we are now splitting into 3 modules.

}))
return nullopt;
return *std::prev(it);
}

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.

This should traverse up from the returns to skip over the layouts. This also probably needs to return a vector instead of a single instruction because of multiple returns.

auto copy_input_shape = copy_op.expected_inputs.front();
auto copy_input = m.insert_instruction(
ins,
migraphx::make_op("as_shape", {{"shape", to_value(copy_input_shape)}}),

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.

Inserting as_shape is not correct.

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.

6 participants