Skip to content

Hoist and horizontal dot#5014

Open
TedThemistokleous wants to merge 31 commits into
developfrom
hoist_and_horizontal_dot
Open

Hoist and horizontal dot#5014
TedThemistokleous wants to merge 31 commits into
developfrom
hoist_and_horizontal_dot

Conversation

@TedThemistokleous

@TedThemistokleous TedThemistokleous commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Motivation

Final piece split from #4723

Adds in a hoist pass into simplify reshapes to move point wise operations above slices that is repeated across several slices. This sets us up to perofrm a horizontal dot fusion across each of the branches.

Technical Details

Updating this after refining things further. Analysis after doing a compare with develop and some changes

Summary

Fuse the MLP prediction-tower bias + SiLU epilogue into the horizontally-fused GEMM path on the feed-gen model, eliminating per-tower loose pointwise kernels. Two root causes were fixed:

  • Cause A — heterogeneous split groups: find_splits only hoisted a pointwise/reduction above sibling slices when every slice fed the same op. A wide horizontally-fused dot whose output is sliced per tower mixes SiLU heads with a differently-consumed head (e.g. a reshape), so the group was rejected and each head's add + SiLU fell out as a separate kernel. Added a contiguous-subset partial hoist to find_splits that lifts the op of a maximal contiguous matching run over its bounding slice (supports unary ops and binary ops whose other operand is an identically-ranged slice of another wide tensor, i.e. a broadcast bias).
  • Cause B — large-K GEMMs routed to rocBLAS: is_mlir_dot skipped GEMMs with K > 1535, sending the wide concatenated-K tower GEMMs to gpu::hip_gemm (rocBLAS), which cannot take a fused epilogue. Raised the threshold to 8096 so they stay on MLIR and fuse.

Performance (feed-gen model, batch 1, driver perf -n 10000)

Metric develop baseline this PR Δ
Rate 568.31 inf/sec 625.37 inf/sec +10.0%
Median latency 1.76 ms 1.58 ms −10.2%
Total instruction time 3.38 ms 3.39 ms ~flat

The rate/latency gain with flat total-instruction-time reflects fewer kernel launches (less dispatch overhead), not less compute.

Resultant kernels

Kernel develop this PR Notes
add_sigmoid_mul_kernel (loose SiLU) 14 1 12 sliced tower-head SiLUs collapse to 1; other 2 fused via Cause B
mlir_dot_add_sigmoid_mul 19 21 2 large-K GEMMs moved to MLIR and fused their epilogue
gpu::hip_gemm (rocBLAS, no epilogue fusion) 2 0 K threshold raised
mlir_dot_add_unsqueezemlir_slice_dot_add_unsqueeze 12 12 downstream dots absorb the re-slices (benign)

Tests

  • test/simplify_algebra_test.cpp: simplify_split_partial_pointwise_subset (unary) and simplify_split_partial_binary_subset (binary bias+SiLU shape) — cover the mixed-group partial hoist.
  • test/optimize_module_test.cpp: end-to-end MLP-tower SiLU pipeline guard.

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.

Hoist a pointwise (or SiLU diamond) chain that is replicated across
several sibling slices of the same instruction above the slices: the
chain is computed once on the bounding slice and each consumer reads its
sub-range back out. This exposes a single wide pointwise op that
downstream passes (find_splits, horizontal fusion) can recombine instead
of N identical narrow ops.

trace_pw_chain handles both linear pointwise chains and the SiLU diamond
(x -> sigmoid -> mul(x, sigmoid)). Slices are only hoisted when they
exactly tile the bounding range so no redundant elements are computed.
Cover the hoist transform in isolation (simplify_reshapes + DCE only):
- relu hoisted above two sibling unit slices
- SiLU diamond (sigmoid/mul) hoisted above sibling slices
- no hoist when slices leave a gap in the bounding range
- no hoist when sibling slices feed different pointwise chains
Batch structurally-identical dot operations (same activation/weight lens
and output type, constant weights) into a single batched GEMM by stacking
activations and weights along a new leading axis, then slice and squeeze
the per-dot results back out.

Per review on #4723, only the dots are fused here; downstream elementwise
work (bias add, SiLU, ...) is left in place so the existing fusions
(find_splits in simplify_algebra and the pointwise/MLIR fusions) can
recombine it on top of the batched dot rather than duplicating that logic.
Cover the batched-dot fusion (fuse_horizontal + DCE):
- two shape-identical dots with constant weights batch into one GEMM
- dots with non-constant weights are not fused
- dots with mismatched activation/weight shapes do not group
Exercise simplify_reshapes (hoist_pointwise_above_slices) and
fuse_horizontal (dot_horizontal_fusion) in a single pipeline on a module
containing both opportunities: a SiLU replicated across sibling slices
and a set of parallel constant-weight dots. Verify the dots collapse to a
single batched GEMM and the per-slice SiLUs collapse to one sigmoid/mul.
@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.08917% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/simplify_algebra.cpp 97.56% 3 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #5014      +/-   ##
===========================================
+ Coverage    92.89%   92.92%   +0.04%     
===========================================
  Files          603      603              
  Lines        32448    32672     +224     
===========================================
+ Hits         30140    30360     +220     
- Misses        2308     2312       +4     
Files with missing lines Coverage Δ
src/fuse_horizontal.cpp 99.51% <100.00%> (+0.09%) ⬆️
src/simplify_algebra.cpp 97.51% <97.56%> (-0.01%) ⬇️

... and 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

}

EXPECT(m1.sort() == m2.sort());
}

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 already be handled by find_splits.

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.

Understood. Modified the changes so we can reuse as much of find_splits as possible, got rid of the netire tracing section so its just a case where we handle the smaller cases. Diamon pattern of ops to lift before slice should be handled like we talked about after fuse_pointwise.

Let me know what you think I'm just running some additional tests on this.

Comment thread test/simplify_reshapes_test.cpp
Comment thread src/simplify_reshapes.cpp Outdated
Comment thread src/fuse_horizontal.cpp Outdated
@gh-app-migraphx-bot-pr-write

gh-app-migraphx-bot-pr-write Bot commented Jun 26, 2026

Copy link
Copy Markdown
Test Batch New Rate (84eddd) Old Rate (c7984a) Diff Status
torchvision-resnet50 64 3,137.09 3,147.92 -0.34%
torchvision-resnet50_fp16 64 6,654.63 6,683.54 -0.43%
torchvision-densenet121 32 2,692.81 2,706.70 -0.51%
torchvision-densenet121_fp16 32 4,541.52 4,584.01 -0.93%
torchvision-inceptionv3 32 1,795.36 1,796.69 -0.07%
torchvision-inceptionv3_fp16 32 2,835.97 2,852.74 -0.59%
cadene-inceptionv4 16 821.19 823.24 -0.25%
cadene-resnext64x4 16 783.02 783.82 -0.10%
slim-mobilenet 64 8,384.93 8,427.50 -0.51%
slim-nasnetalarge 64 229.00 228.91 0.04%
slim-resnet50v2 64 3,162.04 3,173.08 -0.35%
bert-mrpc-onnx 8 1,169.59 1,166.14 0.30%
bert-mrpc-tf 1 488.92 484.48 0.92%
pytorch-examples-wlang-gru 1 479.44 473.62 1.23%
pytorch-examples-wlang-lstm 1 391.71 382.56 2.39%
torchvision-resnet50_1 1 754.09 768.62 -1.89%
cadene-dpn92_1 1 445.73 445.78 -0.01%
cadene-resnext101_1 1 364.86 365.52 -0.18%
onnx-taau-downsample 1 400.91 401.90 -0.25%
dlrm-criteoterabyte 1 32.41 32.50 -0.27%
dlrm-criteoterabyte_fp16 1 51.82 52.75 -1.76%
agentmodel 1 9,180.54 7,840.62 17.09% 🔆
unet_fp16 2 57.03 57.37 -0.61%
resnet50v1_fp16 1 928.64 931.81 -0.34%
resnet50v1_int8 1 942.08 935.43 0.71%
bert_base_cased_fp16 64 1,098.95 1,102.59 -0.33%
bert_large_uncased_fp16 32 345.58 346.63 -0.30%
bert_large_fp16 1 205.25 203.73 0.75%
distilgpt2_fp16 16 2,090.95 2,091.42 -0.02%
yolov5s 1 561.00 557.67 0.60%
tinyllama 1 45.79 45.81 -0.04%
vicuna-fastchat 1 44.12 44.12 -0.02%
whisper-tiny-encoder 1 412.75 415.14 -0.58%
whisper-tiny-decoder 1 414.01 410.13 0.95%
llama2_7b 1 20.83 20.97 -0.67%
qwen1.5-7b 1 23.50 23.60 -0.43%
phi3-3.8b 1 26.65 26.85 -0.74%
llama3-8b 1 21.66 21.83 -0.78%
whisper-large-encoder 1 10.17 10.21 -0.31%
whisper-large-decoder 1 104.20 104.58 -0.36%
mistral-7b 1 23.66 23.80 -0.60%
FLUX.1-schnell 1 758.21 746.40 1.58%

Check flagged results 🔆

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

gh-app-migraphx-bot-pr-write Bot commented Jun 26, 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

TedThemistokleous and others added 8 commits June 26, 2026 14:57
Replace the bespoke pointwise-chain hoisting engine (chain tracing, equivalence,
replay) in find_splits with a unary-only hoist_bounded_splits that reuses the
existing get_split_groups/is_fusable/split_groups_are_dependent helpers.

When sibling slices only partially tile their tensor, a single-input op
replicated across them is computed once on the bounding slice and each consumer
re-slices its sub-range. Multi-op linear chains are peeled one op per fixpoint
iteration, and SiLU/diamond patterns are handled once fuse_pointwise collapses
them into a single pointwise op, so no diamond-specific code is needed here.

Co-authored-by: Cursor <cursoragent@cursor.com>
The slim find_splits hoist only widens single-input ops, so a raw sigmoid/mul
SiLU diamond is no longer hoisted directly. Update the two SiLU tests to run
fuse_pointwise first (collapsing each per-slice SiLU into one pointwise op) and
assert on the resulting fused pointwise: hoist_silu_above_slices now compares
against a hoisted pointwise program, and the end-to-end test asserts a single
pointwise plus a single batched dot.

Co-authored-by: Cursor <cursoragent@cursor.com>
ensures we're not grabbing everything ith bias which was generated errnornous add_kernels by fusing dot early.
Comment thread test/simplify_algebra_test.cpp Outdated
Comment thread test/simplify_algebra_test.cpp
Comment thread src/simplify_algebra.cpp Outdated
Comment thread src/simplify_algebra.cpp
Comment thread src/simplify_algebra.cpp Outdated
TedThemistokleous and others added 2 commits July 1, 2026 02:31
- move tests
- Adjust changes to find_splits with partial flag.
@TedThemistokleous
TedThemistokleous requested a review from pfultz2 July 8, 2026 05:13
@pfultz2

pfultz2 commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Can you revert the refactor into helper functions and just use a NOLINT for now? It makes it harder to review the changes and the refactor needs a lot more work.

@TedThemistokleous

Copy link
Copy Markdown
Collaborator Author

Can you revert the refactor into helper functions and just use a NOLINT for now? It makes it harder to review the changes and the refactor needs a lot more work.

Sure can do that

Comment thread test/fuse_horizontal_test.cpp

@CharlieL7 CharlieL7 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.

Looks good overall, recommending some test changes

Comment thread test/fuse_horizontal_test.cpp
TedThemistokleous and others added 5 commits July 16, 2026 18:09
A horizontally-fused wide dot gets sliced per group, and each slice feeds
an optional bias add then a SiLU gate (mul(x, sigmoid(x))). find_splits
cannot hoist this because its binary path only folds a constant second
operand, while the gate's second operand is derived from the split, so the
epilogue is stranded after the slice and cannot fuse into the GEMM.

find_split_gate rewrites the (optional bias) + gate onto the un-sliced
tensor (concat the per-slice biases, add, then g/mul on the whole result,
slicing last) so MLIR can fuse dot+add+g+mul into the wide GEMM.

Co-authored-by: Cursor <cursoragent@cursor.com>
The K>1535 cutoff in is_mlir_dot routed the wide horizontally-fused GEMMs
(K=2112 and K=5536) to rocBLAS (gpu::hip_gemm), which has no epilogue
fusion, so their bias+SiLU fell out as standalone kernels. Raise the
threshold to 8096 so these GEMMs stay on the MLIR path and can fuse their
epilogue.

Co-authored-by: Cursor <cursoragent@cursor.com>
…tial groups that are skipped over within the find_splits matcher.
@TedThemistokleous
TedThemistokleous force-pushed the hoist_and_horizontal_dot branch from c4863b8 to e10b146 Compare July 17, 2026 20:08
Comment thread src/fuse_horizontal.cpp
// Fuse across distinct tables first, then same-table groups. Running same-table
// fusion first can shrink cross-table groups below their size threshold and miss it.
fuse_horizontal_ops(m, gather_horizontal_fusion{}, same_table_gather_horizontal_fusion{});
fuse_horizontal_ops(m, gather_horizontal_fusion{}, same_table_gather_horizontal_fusion{}, dot_horizontal_fusion{});

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.

[format.py] reported by reviewdog 🐶

Suggested change
fuse_horizontal_ops(m, gather_horizontal_fusion{}, same_table_gather_horizontal_fusion{}, dot_horizontal_fusion{});
fuse_horizontal_ops(m,
gather_horizontal_fusion{},
same_table_gather_horizontal_fusion{},
dot_horizontal_fusion{});

Comment thread src/simplify_algebra.cpp
Comment on lines +1715 to +1716
auto lo = any_cast<op::slice>(splits[i]->get_operator()).starts.front();
auto hi = any_cast<op::slice>(splits[j - 1]->get_operator()).ends.front();

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.

[format.py] reported by reviewdog 🐶

Suggested change
auto lo = any_cast<op::slice>(splits[i]->get_operator()).starts.front();
auto hi = any_cast<op::slice>(splits[j - 1]->get_operator()).ends.front();
auto lo = any_cast<op::slice>(splits[i]->get_operator()).starts.front();
auto hi = any_cast<op::slice>(splits[j - 1]->get_operator()).ends.front();

Comment thread src/simplify_algebra.cpp
Comment on lines +1728 to +1729
c = m.insert_instruction(
std::next(base), info.consumer->get_operator(), {base}, info.consumer->module_inputs());

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.

[format.py] reported by reviewdog 🐶

Suggested change
c = m.insert_instruction(
std::next(base), info.consumer->get_operator(), {base}, info.consumer->module_inputs());
c = m.insert_instruction(std::next(base),
info.consumer->get_operator(),
{base},
info.consumer->module_inputs());

Comment thread src/simplify_algebra.cpp
Comment on lines +1738 to +1740
info.consumer->get_operator(),
args,
info.consumer->module_inputs());

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.

[format.py] reported by reviewdog 🐶

Suggested change
info.consumer->get_operator(),
args,
info.consumer->module_inputs());
info.consumer->get_operator(),
args,
info.consumer->module_inputs());

Comment on lines +2387 to +2388
auto r2 = m1.add_instruction(migraphx::make_op("relu"), s2);
auto s3 = m1.add_instruction(

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.

[format.py] reported by reviewdog 🐶

Suggested change
auto r2 = m1.add_instruction(migraphx::make_op("relu"), s2);
auto s3 = m1.add_instruction(
auto r2 = m1.add_instruction(migraphx::make_op("relu"), s2);
auto s3 = m1.add_instruction(

Comment thread src/targets/gpu/fuse_mlir.cpp Outdated
Comment thread src/simplify_algebra.cpp
// mixed full-cover group (e.g. SiLU heads next to a reshape head) still
// fuses its heads. (The `partial` path already covers partial tiling.)
if(not partial)
hoist_partial_groups(m, ins, splits);

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 is this doing? It looks like it is reimplementing find_splits again. The comment doesnt explain it good. From the comments, it sounds like it is trying to handle the case where we have slice -> op, slice -> reshape -> op but that seems like we should have another matcher to rewrite the slice -> op, slice -> reshape -> op to slice -> op, slice -> op -> reshape, which I thought you already had a matcher to handle that so I am not sure what the issue is.

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.

We were missing some branches. Let me clean this up today - Its to catch partial splits which were originally getting missed causing things to never get matched/changed then fused correctly

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.

From the tests, is this trying to handle the case where a binary takes both inputs as splits from different tensors?

I do know the current code doesnt handle that as it requires the second argument to be constant, but we could add a check that the second argument can be constant or another split.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

high priority A PR with high priority for review and merging.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants