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

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