fix(onnx-to-hip): take multi-dynamic expand_shape extents from the shape operand (-64% TTFT on Gemma-4 26B-A4B) - #700
Draft
BoarQing wants to merge 2 commits into
Draft
Conversation
BoarQing
requested review from
amd-bqiao,
amd-mingw,
edelaye,
fhanuman,
qianglin-amd and
wcy123
as code owners
August 12, 2026 12:12
|
Thanks for opening a PR! This project follows LLVM's incremental-development and AI-tool-use Before requesting review, please check that:
Reviewers are assigned through |
BoarQing
marked this pull request as draft
August 12, 2026 12:23
L2 Accuracy Results (EP vs CPU)
Threshold: 0.01 | Run: 4598 - Commit: |
MorphiZen EP Performance Results
EPContext Export Performance
EPContext Import Performance
OGA Benchmark Results
OGA Wheel Smoke (Python benchmark_e2e.py)
Run: 4598 - Commit: |
BoarQing
force-pushed
the
fix/reshape-multi-dyn-expand-shape
branch
from
August 13, 2026 02:59
993ef57 to
8b68f4c
Compare
Closed
Contributor
Author
BoarQing
force-pushed
the
fix/reshape-multi-dyn-expand-shape
branch
from
August 18, 2026 07:58
8b68f4c to
70b9e6b
Compare
…ape operand buildExpandShapeOutputShape gave every dynamic output dim in a reassociation group the full source-dim extent. That holds only while a group covers at most one dynamic dim; when a Reshape splits one dynamic source dim into several dynamic output dims the source extent is their product and says nothing about the split, so each output dim was assigned the whole product. On Gemma-4 26B-A4B the per-layer Reshape([bs*ss, 2816] -> [bs, ss, 2816]) therefore claimed [ss, ss, 2816], and the 30 layer input norms consuming it ran over ss^2 rows: 5,036 ms of the 6,486 ms decoder prefill at 2,049 tokens, and 88 GB of shared memory at 4,097 tokens, which took the machine down. ReshapeShapeFold already folds Reshape(_, Shape(x)) into a host-visible tensor.from_elements holding the correct per-dim values, precisely so this can be recovered, but the consuming branch was never written and the conversion never read operand 1. Add it, and return nullopt when the shape operand is not host-readable so the caller falls back to tensor.reshape rather than emit an expand_shape it cannot justify. Single-dynamic groups keep the existing derivation, leaving the other expand_shape sites in the model untouched. Also make the failure loud. verifyNormAxisMatchesScale rejects at conversion time any norm whose axis does not span exactly the scale's element count, which the runtime cannot check because its ABI carries element counts rather than shapes; and the two norm wrappers that lacked the positivity and divisibility preconditions already present in wrap_layer_normalization now have them. Gemma-4 26B-A4B FP16W4-qmoe on gfx1151, TTFT at 2,049 tokens 7,865 -> 2,859 ms, decoder prefill 6,486 -> 1,520 ms, layernorm within it 5,036 -> 50 ms. 4,097 tokens now completes in 4,983 ms instead of rebooting the host. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Made-with: Cursor Co-authored-by: Cursor <cursoragent@cursor.com>
Re-wrap the countDynOutDims and buildExpandShapeOutputShape declarations and the expand_shape call site to satisfy the pinned clang-format 16.0.1. No code change; the whitespace-stripped token stream is identical to the parent commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Made-with: Cursor Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
buildExpandShapeOutputShapederived every dynamic output dimension of atensor.expand_shapefrom the source dimension it came from. That is correct only while a reassociation group covers at most one dynamic dimension. When aReshapesplits one dynamic source dimension into several dynamic output dimensions, the source extent is their product, so each of those output dimensions was assigned the whole product.This makes the split silently wrong for any
[bs*ss, H] -> [bs, ss, H]reshape, and every consumer inherits the inflated shape. On Gemma-4 26B-A4B it cost 78% of decoder prefill and made 4K prompts unrunnable.Related issue or design
None.
Why
The per-layer
Reshape([bs*ss, 2816] -> [bs, ss, 2816])in Gemma-4 26B-A4B lowered toWith
bs = 1the source dimension already equalsss, so the expansion claimed[ss, ss, 2816]rather than[1, ss, 2816]. The 30 layer input norms consuming it ran overss^2rows instead ofss.It stayed silent because the norm runtime ABI passes element counts, not shapes:
simplified_layer_normrecoversnum_rowsasinput_num_elements / scale_num_elements, which divides evenly for anyss, so anss-times-too-large buffer looks like a well-formed larger batch. At 2,049 tokens that was 5,036 ms of the 6,486 ms decoder prefill; at 4,097 tokens the N-squared tensor needed 88 GB against 63.6 GB of shared APU memory, and the host rebooted.ReshapeShapeFoldalready rewritesReshape(_, Shape(x))into a host-visibletensor.from_elementscarrying the correct per-dimension values, and its comment said this was so the conversion could consume them — but the consuming branch was never written and the conversion never read operand 1. The fix is to finish that path rather than add a new mechanism.What
ReshapeConversion.cpp:buildExpandShapeOutputShapenow detects groups with more than one dynamic output dimension and takes those extents from the shape operand viaresolveShapeOperandExtents, which walkstensor.from_elementsthrougharith.index_castand constants. Groups with zero or one dynamic dimension keep the existingtensor.dimderivation, so the other expand-shape sites in the model are bit-for-bit unchanged.std::nulloptwhen a multi-dynamic group's extents are not host-readable, and all three call sites (Reshapeexpand, same-rank split/combine,Unsqueeze) fall back totensor.reshapeinstead of emitting anexpand_shapewhoseoutput_shapecannot be justified.NormConversion.cpp:verifyNormAxisMatchesScalerejects at conversion time anySimplifiedLayerNormalizationorRMSNormalizationwhose axis does not span exactly the scale's element count, when the dimensions involved are static. This is the layer that can still see shapes.simplified_layer_norm.cpp,skip_simplified_layer_norm.cpp: add thescale_num_elements > 0and even-divisibility preconditions thatwrap_layer_normalizationalready had, so a mismatch fails instead of truncating.ReshapeShapeFold.cpp: correct the stale comment that claimed the operand was already consumed.A same-shape guard is deliberately not added, since
[ss, ss, 2816]is a legal shape and only the value is wrong.Test plan
test/lit/Conversion/onnx-to-hip/test_reshape.mlir:test_reshape_expand_multi_dynpins the[?x2816, 2816] -> [?, ?, 2816]split to distinct extents from the shape operand;test_reshape_expand_multi_dyn_opaquepins thetensor.reshapefallback when the operand is opaque.test/lit/Conversion/hip-to-llvm/test_rms_norm.mlir:test_rms_norm_dynamic_batch_seq_3dpinsinput_num_elementsfor a[?, ?, 2816]norm to the product of three distinct dimensions.HIPDNN_EP_DEBUGconfirms all 181 rank-3 norms report2049x2816with no remaining4198401.Performance
Gemma-4 26B-A4B FP16W4-qmoe, gfx1151 (Strix Halo, 63.6 GB shared), Release build, 2,049-token prompt, median of 5 after warmup.
TTFT at other lengths: 1,905 ms at 513, 2,183 ms at 1,025, and 4,983 ms at 4,097. Prefill is now near-linear in prompt length; 4,097 tokens previously exhausted shared memory and rebooted the host.
Notes for reviewers
tensor.reshape, which is correct but blocks fusion. I found no such case in the models tested, so the branch is exercised only by the lit test.Checklist
AI assistance: the diagnosis (IR dumps, roofline check confirming the kernel was at 91% of achievable bandwidth and so not the problem), the patch, the tests, and the benchmark runs were produced with Cursor. All measurements above are from real runs on the machine named, and I reviewed the change.