Skip to content

Commit 957b4d1

Browse files
authored
Canonicalization of triton reduce with select to arith min/max (#21)
This PR introduces a pattern rewrite that simplifies triton::ReduceOp bodies containing arith.select operations based on floating-point comparisons. The transformation replaces select-based logic with equivalent min/max operations, improving IR canonicalization and enabling further optimization. Supported Transformations: select(cmpf ogt a, b), a, b → arith.maxf(a, b) select(cmpf olt a, b), a, b → arith.minf(a, b) select((cmpf ogt a, b) || cmpf une a, a), a, b → arith.maximumf(a, b) select((cmpf olt a, b) || cmpf une a, a), a, b → arith.minimumf(a, b) Applying this transformation before the ReduceOp converter runs allows the natural lowering of tt.reduce to linalg.reduce; the motivational use case was from a torch-inductor spit out Triton kernel which consisted of this complex pattern in the reduction body and did not allow the ReduceOp to get lowered to the linalg dialect. PR cloned from microsoft/triton-shared#361
1 parent 1850a7b commit 957b4d1

2 files changed

Lines changed: 186 additions & 21 deletions

File tree

include/triton-shared/Conversion/TritonArithToLinalg/ConversionPatterns.hpp

Lines changed: 153 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1760,49 +1760,181 @@ template <typename CmpOp>
17601760
struct MinMaxConverter : public OpRewritePattern<CmpOp> {
17611761
using OpRewritePattern<CmpOp>::OpRewritePattern;
17621762

1763+
template <typename T> static T getOnlyUserOfType(Value val) {
1764+
if (!val || !val.hasOneUse()) {
1765+
return nullptr;
1766+
}
1767+
return dyn_cast<T>(*val.getUsers().begin());
1768+
}
1769+
1770+
// Only handle the Cmp + OrIOp + Select pattern here.
1771+
static arith::SelectOp findSelectThroughOr(Value cond) {
1772+
if (auto ori = getOnlyUserOfType<arith::OrIOp>(cond)) {
1773+
return getOnlyUserOfType<arith::SelectOp>(ori.getResult());
1774+
}
1775+
return nullptr;
1776+
}
1777+
17631778
MinMaxConverter(MLIRContext *context)
17641779
: OpRewritePattern<CmpOp>(context, /*benefit=*/10) {}
17651780

1781+
/// Helper that maps a floating-point compare predicate to the
1782+
/// corresponding min/max operation. THis is parametrized by
1783+
/// whether we want NaN-aware operations (MaximumFOp/MinimumFOp) or
1784+
/// numeric operations (MaxNumFOp/MinNumFOp).
1785+
FailureOr<Value> foldCmpToMinMax(PatternRewriter &rewriter, Location loc,
1786+
Value lhs, Value rhs,
1787+
arith::CmpFPredicate pred,
1788+
bool useNaNOps) const {
1789+
switch (pred) {
1790+
case arith::CmpFPredicate::OGT:
1791+
case arith::CmpFPredicate::OGE:
1792+
if (useNaNOps) {
1793+
return arith::MaximumFOp::create(rewriter, loc, lhs, rhs).getResult();
1794+
} else {
1795+
return arith::MaxNumFOp::create(rewriter, loc, lhs, rhs).getResult();
1796+
}
1797+
return success();
1798+
case arith::CmpFPredicate::OLT:
1799+
case arith::CmpFPredicate::OLE:
1800+
if (useNaNOps) {
1801+
return arith::MinimumFOp::create(rewriter, loc, lhs, rhs).getResult();
1802+
} else {
1803+
return arith::MinNumFOp::create(rewriter, loc, lhs, rhs).getResult();
1804+
}
1805+
default:
1806+
return failure();
1807+
}
1808+
}
1809+
17661810
LogicalResult matchAndRewrite(CmpOp cmpOp,
17671811
PatternRewriter &rewriter) const final {
1768-
if (!cmpOp.getResult().hasOneUse()) {
1812+
Value result = cmpOp.getResult();
1813+
if (!result.hasOneUse()) {
17691814
return failure();
17701815
}
1771-
auto selectOp =
1772-
dyn_cast<arith::SelectOp>(*cmpOp.getResult().getUsers().begin());
1816+
1817+
// 1. Simple pattern: cmpf + select.
1818+
if (auto selectOp = dyn_cast<arith::SelectOp>(*result.getUsers().begin())) {
1819+
if (!(result == selectOp.getCondition() &&
1820+
(cmpOp.getLhs() == selectOp.getTrueValue() &&
1821+
cmpOp.getRhs() == selectOp.getFalseValue()))) {
1822+
return failure();
1823+
}
1824+
1825+
rewriteOpWithMinMax(rewriter, cmpOp, selectOp, cmpOp.getPredicate());
1826+
rewriter.eraseOp(cmpOp);
1827+
return success();
1828+
}
1829+
1830+
// 2. NaN-aware pattern: cmpf + or + select.
1831+
auto selectOp = findSelectThroughOr(result);
17731832
if (!selectOp) {
17741833
return failure();
17751834
}
17761835

1777-
if (!(cmpOp.getResult() == selectOp.getCondition() &&
1778-
cmpOp.getLhs() == selectOp.getTrueValue() &&
1779-
cmpOp.getRhs() == selectOp.getFalseValue())) {
1836+
if (failed(foldCmpSelectToMinMax(rewriter, selectOp))) {
17801837
return failure();
17811838
}
1839+
return success();
1840+
}
17821841

1783-
rewriteOpWithMinMax(rewriter, cmpOp, selectOp, cmpOp.getPredicate());
1784-
rewriter.eraseOp(cmpOp);
1842+
/// foldCmpSelectToMinMax performs an optimization pattern that matches
1843+
/// 'arith.select' operations based on a floating-point comparison
1844+
/// and rewrites them into equivalent numeric min/max operations.
1845+
///
1846+
/// This pattern handles the following case:
1847+
///
1848+
/// ** NaN-Aware Min/Max Reduction **
1849+
/// - select (cmpf ogt a, b) || cmpf une a, a), a, b --> arith.maximumf(a,
1850+
/// b)
1851+
/// - select (cmpf olt a, b) || cmpf une a, a), a, b --> arith.minimumf(a,
1852+
/// b)
1853+
///
1854+
/// These transformations not only improve IR canonicalization but also
1855+
/// allow the successful lowering of tt.reduce operations to linalg
1856+
/// operations, which is already supported in the triton-shared dialect
1857+
/// conversion pipeline.
1858+
1859+
LogicalResult foldCmpSelectToMinMax(PatternRewriter &rewriter,
1860+
arith::SelectOp sel) const {
1861+
1862+
if (!isa<FloatType>(sel.getType())) {
1863+
return failure();
1864+
}
17851865

1786-
return success();
1866+
Operation *condOp = sel.getCondition().getDefiningOp();
1867+
if (!condOp) {
1868+
return failure();
1869+
}
1870+
1871+
Value trueVal = sel.getTrueValue();
1872+
Value falseVal = sel.getFalseValue();
1873+
1874+
// NaN-Aware Min/Max Reduction.
1875+
auto ori = dyn_cast<arith::OrIOp>(condOp);
1876+
if (!ori)
1877+
return failure();
1878+
// Extract both sides of the OR condition.
1879+
auto cmp1 = ori.getLhs().getDefiningOp<arith::CmpFOp>();
1880+
auto cmp2 = ori.getRhs().getDefiningOp<arith::CmpFOp>();
1881+
if (!cmp1 || !cmp2)
1882+
return failure();
1883+
1884+
// Helper lambdas to identify comparison patterns.
1885+
auto isOGT = [&](arith::CmpFOp cmp) {
1886+
return cmp.getPredicate() == arith::CmpFPredicate::OGT &&
1887+
trueVal == cmp.getLhs() && falseVal == cmp.getRhs();
1888+
};
1889+
auto isOLT = [&](arith::CmpFOp cmp) {
1890+
return cmp.getPredicate() == arith::CmpFPredicate::OLT &&
1891+
trueVal == cmp.getLhs() && falseVal == cmp.getRhs();
1892+
};
1893+
auto isNaN = [&](arith::CmpFOp cmp) {
1894+
return cmp.getPredicate() == arith::CmpFPredicate::UNE &&
1895+
trueVal == cmp.getLhs() && trueVal == cmp.getRhs();
1896+
};
1897+
1898+
// Match: select ((ogt(a, b) || une(a, a)), a, b) -> arith.maximumf(a, b).
1899+
if ((isOGT(cmp1) && isNaN(cmp2)) || (isOGT(cmp2) && isNaN(cmp1))) {
1900+
PatternRewriter::InsertionGuard guard(rewriter);
1901+
rewriter.setInsertionPoint(sel);
1902+
FailureOr<Value> foldResult = foldCmpToMinMax(
1903+
rewriter, sel.getLoc(), trueVal, falseVal, arith::CmpFPredicate::OGT,
1904+
/*useNaNOps=*/true);
1905+
if (failed(foldResult)) {
1906+
return failure();
1907+
}
1908+
rewriter.replaceOp(sel, *foldResult);
1909+
return success();
1910+
}
1911+
1912+
// Match: select ((olt(a, b) || une(a, a)), a, b) -> arith.minimumf(a, b).
1913+
if ((isOLT(cmp1) && isNaN(cmp2)) || (isOLT(cmp2) && isNaN(cmp1))) {
1914+
PatternRewriter::InsertionGuard guard(rewriter);
1915+
rewriter.setInsertionPoint(sel);
1916+
FailureOr<Value> foldResult = foldCmpToMinMax(
1917+
rewriter, sel.getLoc(), trueVal, falseVal, arith::CmpFPredicate::OLT,
1918+
/*useNaNOps=*/true);
1919+
if (failed(foldResult)) {
1920+
return failure();
1921+
}
1922+
rewriter.replaceOp(sel, *foldResult);
1923+
return success();
1924+
}
1925+
return failure();
17871926
}
17881927

17891928
void rewriteOpWithMinMax(PatternRewriter &rewriter, arith::CmpFOp cmpOp,
17901929
arith::SelectOp selectOp,
17911930
arith::CmpFPredicate pred) const {
1792-
switch (pred) {
1793-
case arith::CmpFPredicate::OGT:
1794-
case arith::CmpFPredicate::OGE:
1795-
rewriter.replaceOpWithNewOp<arith::MaximumFOp>(selectOp, cmpOp.getLhs(),
1796-
cmpOp.getRhs());
1797-
break;
1798-
case arith::CmpFPredicate::OLT:
1799-
case arith::CmpFPredicate::OLE:
1800-
rewriter.replaceOpWithNewOp<arith::MinimumFOp>(selectOp, cmpOp.getLhs(),
1801-
cmpOp.getRhs());
1802-
break;
1803-
default:
1931+
FailureOr<Value> foldedResult =
1932+
foldCmpToMinMax(rewriter, selectOp.getLoc(), cmpOp.getLhs(),
1933+
cmpOp.getRhs(), pred, /*useNaNOps=*/true);
1934+
if (failed(foldedResult)) {
18041935
llvm_unreachable("Unhandled predicate");
18051936
}
1937+
rewriter.replaceOp(selectOp, *foldedResult);
18061938
}
18071939

18081940
void rewriteOpWithMinMax(PatternRewriter &rewriter, arith::CmpIOp cmpOp,

test/Conversion/TritonArithToLinalg/convert_minmax_reduce.mlir

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,36 @@ module {
135135
// CHECK: tt.store [[PARAM_0_]], [[VAR_extracted_]] : !tt.ptr<i32>
136136
// CHECK: return
137137
// CHECK: }
138+
139+
140+
// -----
141+
142+
module {
143+
tt.func public @nan_aware_max(%arg0: tensor<1024xf32>, %arg_out: !tt.ptr<f32>) {
144+
%res = "tt.reduce"(%arg0) <{axis = 0 : i32}> ({
145+
^bb0(%lhs: f32, %rhs: f32):
146+
%cmp_gt = arith.cmpf ogt, %lhs, %rhs : f32
147+
%lhs_nan = arith.cmpf une, %lhs, %lhs : f32
148+
%pred = arith.ori %cmp_gt, %lhs_nan : i1
149+
%sel = arith.select %pred, %lhs, %rhs : f32
150+
tt.reduce.return %sel : f32
151+
}) : (tensor<1024xf32>) -> f32
152+
tt.store %arg_out, %res : !tt.ptr<f32>
153+
tt.return
154+
}
155+
}
156+
157+
// CHECK-LABEL: func.func @nan_aware_max
158+
// CHECK-SAME: ([[PARAM_0_:%.+]]: tensor<1024xf32>, [[PARAM_1_:%.+]]: !tt.ptr<f32>, [[PARAM_2_:%.+]]: i32, [[PARAM_3_:%.+]]: i32, [[PARAM_4_:%.+]]: i32, [[PARAM_5_:%.+]]: i32, [[PARAM_6_:%.+]]: i32, [[PARAM_7_:%.+]]: i32) {
159+
// CHECK-DAG: [[CST_nan_:%.+]] = arith.constant 0xFF800000 : f32
160+
// CHECK-DAG: [[VAR_0_:%.+]] = bufferization.alloc_tensor() : tensor<f32>
161+
// CHECK: [[VAR_inserted_:%.+]] = tensor.insert [[CST_nan_]] into [[VAR_0_]][] : tensor<f32>
162+
// CHECK: [[VAR_reduced_:%.+]] = linalg.reduce ins([[PARAM_0_]] : tensor<1024xf32>) outs([[VAR_inserted_]] : tensor<f32>) dimensions = [0]
163+
// CHECK: ([[in_:%.+]]: f32, [[in_]]it: f32) {
164+
// CHECK: [[CMP_gt_:%.+]] = arith.maximumf [[in_]], [[in_]]it : f32
165+
// CHECK: linalg.yield [[CMP_gt_]] : f32
166+
// CHECK: }
167+
// CHECK: [[VAR_extracted_:%.+]] = tensor.extract [[VAR_reduced_]][] : tensor<f32>
168+
// CHECK: tt.store [[PARAM_1_]], [[VAR_extracted_]] : !tt.ptr<f32>
169+
// CHECK: return
170+
// CHECK: }

0 commit comments

Comments
 (0)