Repository navigation
[AIROCMLIR-1246] Ignore negligible anchors when resolving layout conflicts - #473
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Minor but actionable correctness/maintainability issues were found in the updated Triton transform implementation (see PR comments).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR improves Triton’s tritongpu-remove-layout-conversions conflict resolution by adding a traffic-based tie-breaker so “negligible” layout anchors (e.g., narrow 128x1 loads that don’t meaningfully describe downstream tile layouts) don’t inadvertently force expensive shared-memory round trips in dot epilogues. It also adds targeted regression tests to cover both the Triton-side heuristic and the rocMLIR↔Triton interaction with Rock’s load-narrowing pass.
Changes:
- Track per-encoding “anchor traffic” during layout propagation and use it as a tie-break in
LayoutPropagation::resolveConflicts. - Add a Triton GPU dialect test validating that narrow-load anchors don’t win layout conflicts when their traffic is negligible.
- Add a rocMLIR Rock integration test ensuring narrowed loads remain narrow through Triton’s layout passes without contaminating epilogue layouts.
File summaries
| File | Description |
|---|---|
| triton-patches/triton-patch-content.txt | Registers the new downstream Triton patch and documents its motivation/tests. |
| triton-patches/patch-negligible-anchor-layout-conflict.patch | Downstream patch recording the Triton transform + test additions. |
| external/triton/lib/Dialect/TritonGPU/Transforms/RemoveLayoutConversions.cpp | Implements traffic propagation and the negligible-anchor tie-break in conflict resolution. |
| external/triton/test/TritonGPU/amd/remove-layout-conversions-negligible-anchor.mlir | New regression test for the negligible-anchor conflict behavior. |
| mlir/test/Dialect/Rock/narrow-redundant-loads-through-triton.mlir | New end-to-end test covering Rock narrowing interacting with Triton layout passes. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| static int64_t getByteCount(Value result, int64_t minElementCount = 0, | ||
| int64_t minBitWidth = 0) { | ||
| int64_t elementCount = 0; | ||
| int64_t dtypeBitWidth = 0; | ||
| if (auto tensorTy = dyn_cast<RankedTensorType>(result.getType())) { |
| // Snapshot the source candidates and their traffic, as writing to `layouts` | ||
| // below can grow it and invalidate `info`, which aliases one of its entries. |
| @@ -0,0 +1,80 @@ | |||
| // RUN: triton-opt %s -split-input-file -tritongpu-remove-layout-conversions -cse | FileCheck %s | |||
There was a problem hiding this comment.
This new lit test does not appear to be run by any rocmlirTriton CI target. external/triton/CMakeLists.txt:543 guards add_subdirectory(test) behind TRITON_BUILD_UT, and cmake/triton.cmake:154 sets TRITON_BUILD_UT OFF; cmake/triton.cmake:249-266 then adds only ${TRITON_PROJECT_DIR}/test/lib, not test/ itself. So the external/triton/test/ lit suite is never configured here and this file is dead coverage in this repo, even though the behavior it pins is exactly what the PR changes. Please either confirm how it gets executed in our CI, or move/duplicate the two cases under mlir/test/ where check-rocmlir picks them up. This maps to the checklist's Major bullet on a pass change without both positive and negative Lit coverage that actually runs.
There was a problem hiding this comment.
we don't run triton CI in our CI. That's is a known issue that needs to be fixed in another PR. I've verified the new triton tests pass manually.
| /// combined with, which makes it a poor choice for the values downstream of it. | ||
| static int64_t getAnchorTraffic(Value anchor) { | ||
| // Block arguments are anchored too, and have no op to inspect. | ||
| int64_t traffic = getByteCount(anchor); |
There was a problem hiding this comment.
getAnchorTraffic relies on getByteCount, which leaves dtypeBitWidth at 0 unless the element type satisfies isIntOrFloat(). A tensor<...x!tt.ptr<T>> fails that check, so every pointer-tensor anchor scores 0 bytes and is therefore unconditionally classified negligible, regardless of its real size. That matters here because initAnchorLayout explicitly anchors function arguments (lines 288-293), and a pointer-tensor argument such as tensor<128x64x!tt.ptr<f32>> will now always lose the tie-break to any other candidate — silently inverting the intended preference on pointer-heavy kernels. Suggest special-casing triton::PointerType in getAnchorTraffic (e.g. use the pointee's bit width, or element count times the access width) so pointer tensors are scored by the data they actually move. This is the checklist's "silently falls through for unhandled types" Major case.
| // RUN: | rocmlir-driver -c --arch=gfx1100 --mlir-disable-threading -o /dev/null \ | ||
| // RUN: --perf-config=gemm:mPerBlock=128,nPerBlock=64,kPerBlock=32,kpack=1,numCTAs=1,numWaves=4,matrixInstrNonkdim=0,splitKFactor=1,numStages=2,wavesPerEU=0,gridGroupSize=0 \ | ||
| // RUN: --mlir-print-ir-after=tritonamdgpu-optimize-epilogue 2>&1 \ | ||
| // RUN: | FileCheck %s --implicit-check-not='convert_layout {{.*}}tensor<128x64xf32' |
There was a problem hiding this comment.
The --implicit-check-not='convert_layout {{.*}}tensor<128x64xf32' guard only covers f32 tiles, but the epilogue this test is protecting is f16 — the CHECK lines at 32-34 match tensor<128x64xf16 operations. A regression that reintroduces a full-tile convert_layout on the f16 add/broadcast would pass this test unnoticed. Consider broadening the guard to convert_layout {{.*}}tensor<128x64x so it covers both element types, after confirming no legitimate 128x64 conversion survives in the printed IR for this config.
There was a problem hiding this comment.
Verdict: COMMENT · Findings: 4 (0 Critical, 2 Major, 2 Minor)
Scope
Adds a traffic-based tie-break to Triton's LayoutPropagation::resolveConflicts so that an anchor whose layout governs no more than one dword per thread loses to one governing more. Motivated by rock-narrow-redundant-loads rewriting a broadcast-invariant bias tile into a 128x1 load, which then anchored the dot epilogue's layout and cost up to 1.6x on gfx1100. Touches the vendored Triton pass ([EXTERNAL] commit), its downstream patch record, and two new lit tests.
Findings
external/triton/test/TritonGPU/amd/remove-layout-conversions-negligible-anchor.mlir:1(Major) — the new Triton-level lit test does not appear to be executed by any rocmlirTriton CI target.external/triton/lib/Dialect/TritonGPU/Transforms/RemoveLayoutConversions.cpp:64(Major) —getByteCountreturns 0 for pointer-element tensors, so any pointer-tensor anchor is unconditionally scored negligible.mlir/test/Dialect/Rock/narrow-redundant-loads-through-triton.mlir:25(Minor) — the--implicit-check-notguard only covers f32 tiles while the epilogue it protects is f16.triton-patches/triton-patch-content.txt:356(Minor) — the new patch record omits the "re-evaluate / drop once upstreamed" exit criterion its neighbours carry.
Notes
Verified and clean: the vendored-tree edits are isolated in a correctly [EXTERNAL]-prefixed commit; triton-patches/patch-negligible-anchor-layout-conflict.patch matches that commit's diff exactly and is indexed in triton-patch-content.txt; TritonGPUDialect::getThreadsPerWarp(ModuleOp) is declared in the vendored TritonGPUDialect.td, so the new call compiles; the rewritten selection loop preserves the previous "first encoding wins" fallback, and the propagation fixpoint still terminates since traffic is monotone and bounded.
The rocMLIR back-port check does not fire — the touched paths are confined to external/triton/, triton-patches/, and mlir/test/, none of which are in the shared path list.
Neither new .mlir file carries a license header, but sibling first-party tests (mlir/test/Dialect/Rock/narrow-redundant-loads.mlir, triton-to-hsaco-denormal-mode.mlir) do not either, so this reads as established repo practice rather than a defect this PR introduces; worth settling repo-wide instead of here.
Perf-wise, the extra SmallDenseMap<Attribute, int64_t, 8> per LayoutInfo plus traffic changes feeding hasChanged can add propagation iterations and memory on large kernels. A compile-time spot-check on a big fusion would be reassuring.
CI status
No checks are in a fail or cancel state. Jenkins, "Build and Test", MIGraphX, and ml-ci-internal.amd.com are still pending, so the change has not yet been validated by the full suite.
pabloantoniom
left a comment
There was a problem hiding this comment.
Could you double-check that this PR does not lose the performance gains achieved by #445 and/or the original NarrowLoad PR? I just want to make sure that we are not losing performance by changing the layout in unexpected ways.
Also, would be good to check on gfx942/gfx950 as suggested in my comment
Lastly, I don't know the code well but I assume you will upstream this to Triton so they should provide proper feedback on that. The fact that we are replacing Hacky resolve and TODO: add a proper heuristic with a proper heuristic makes me think the should be interested in something like this (hopefully)
| // Counted in bytes rather than in elements as isExpensiveLoadOrStore does, | ||
| // since what a layout can express of an access depends on how wide it is. | ||
| constexpr int64_t bytesPerDword = 4; | ||
| int64_t negligibleTraffic = bytesPerDword * lookupNumWarps(funcOp) * |
There was a problem hiding this comment.
Can you explain the reason for this heuristic? Assume:
lookupNumWarps = 8
getThreadsPerWarp = 64 (gfx942)
Then negligibleTraffic = 2048 bytes. Meaning that tensor<32x32xf16> would count as negligible. Not sure if that would work well?
There was a problem hiding this comment.
sure, the idea is that a tensor is negligible if it's a single register per thread. In this case, there are 8*64=512 threads, so a 32x32 f16 tensor would fit into 1 register per thread.
The existing isExpensiveLoadOrStore has a similar heuristic. Just without taking into account datatypes and register size.
| // Counted in bytes rather than in elements as isExpensiveLoadOrStore does, | ||
| // since what a layout can express of an access depends on how wide it is. | ||
| constexpr int64_t bytesPerDword = 4; | ||
| int64_t negligibleTraffic = bytesPerDword * lookupNumWarps(funcOp) * |
There was a problem hiding this comment.
This may work well for gfx1101 but did you test on gfx942/gfx950?
There was a problem hiding this comment.
I'll check this, but the logic is the same IMO.
There was a problem hiding this comment.
The threshold is different depending on threadsPerWarp so I'm not sure if it needs some adjustment to take that into account
justinrosner
left a comment
There was a problem hiding this comment.
Just a few small questions. Also some of the AI review comments look valid
bc3573d to
a11f529
Compare
Checked, perf stays flat for the mlir that made us open that ticket.
Yes, I'll open an upstream PR. |
justinrosner
left a comment
There was a problem hiding this comment.
Changes look mostly good to me now. Just the one comment. Also make sure that you update the commit structure when it's time to merge in (i.e., squashing to two commits)
| Attribute encoding; | ||
| std::tuple<bool, bool, bool> best; | ||
| for (Attribute e : info.encodings) { | ||
| if ((isLoadOrStore && isa<BlockedEncodingAttr>(e)) || | ||
| (!isLoadOrStore && isa<MmaEncodingTrait>(e))) { | ||
| if (auto candidate = rank(e); !encoding || best < candidate) { | ||
| best = candidate; |
There was a problem hiding this comment.
!encoding now has double meaning as both "no candidate chosen yet" and "the chosen candidate is null", and since rank() now runs on every element instead of breaking at the first preferred-kind match, a null Attribute (which I believe initAnchorLayout can insert) would assert inside isa<BlockedEncodingAttr> and rank itself above real candidates?
7a2d384 to
63e6706
Compare
bbccf60 to
6566a0a
Compare
justinrosner
left a comment
There was a problem hiding this comment.
Changes look good to me now. It would be good to also run the Triton LIT test suite to make sure that no other tests need to be updated now that we will soon be enabling those in the Nightly CI (see here: #494)
Motivation
rock-narrow-redundant-loadsrewrites a broadcast-invariant load (e.g. a 128x64 bias tile) into a 128x1 load plus a broadcast. That narrow load still anchors a layout inremove-layout-conversions, and since no candidate is an mma layout yet beforeaccelerate-matmul, iteration order handed the dot's whole epilogue the load'ssizePerThread = [1, 1]layout plus a full-tile shared-memory round trip: up to 1.6x slower on gfx1100 convolution+reduce fusions.Technical Details
Adds the tie-break
LayoutPropagation::resolveConflictsasked for in its "hacky resolve" comment. Each anchor carries the traffic its layout generates, the figure propagates with the layout, and a candidate whose anchor generates no more than one dword per thread loses to one that governs more. The existing type-based preference still decides first, so this only settles what it leaves tied.Perf numbers
Results of develop vs this branch:
Test Plan
PR CI.
Test Result
All tests pass.
Submission Checklist