Skip to content

[MLIR][OpenMP] Add omp.loop_nest operation #87083

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 65 additions & 1 deletion mlir/include/mlir/Dialect/OpenMP/OpenMPOps.td
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,70 @@ def SingleOp : OpenMP_Op<"single", [AttrSizedOperandSegments]> {
let hasVerifier = 1;
}

//===----------------------------------------------------------------------===//
// Loop Nest
//===----------------------------------------------------------------------===//

def LoopNestOp : OpenMP_Op<"loop_nest", [SameVariadicOperandSize,
AllTypesMatch<["lowerBound", "upperBound", "step"]>,
ParentOneOf<["DistributeOp", "SimdLoopOp", "TaskloopOp",
"WsloopOp"]>,
RecursiveMemoryEffects]> {
let summary = "rectangular loop nest";
let description = [{
This operation represents a collapsed rectangular loop nest. For each
rectangular loop of the nest represented by an instance of this operation,
lower and upper bounds, as well as a step variable, must be defined.

The lower and upper bounds specify a half-open range: the range includes the
lower bound but does not include the upper bound. If the `inclusive`
attribute is specified then the upper bound is also included.

The body region can contain any number of blocks. The region is terminated
by an `omp.yield` instruction without operands. The induction variables,
represented as entry block arguments to the loop nest operation's single
region, match the types of the `lowerBound`, `upperBound` and `step`
arguments.

```mlir
omp.loop_nest (%i1, %i2) : i32 = (%c0, %c0) to (%c10, %c10) step (%c1, %c1) {
%a = load %arrA[%i1, %i2] : memref<?x?xf32>
%b = load %arrB[%i1, %i2] : memref<?x?xf32>
%sum = arith.addf %a, %b : f32
store %sum, %arrC[%i1, %i2] : memref<?x?xf32>
omp.yield
}
```

This is a temporary simplified definition of a loop based on existing OpenMP
loop operations intended to serve as a stopgap solution until the long-term
representation of canonical loops is defined. Specifically, this operation
is intended to serve as a unique source for loop information during the
transition to making `omp.distribute`, `omp.simdloop`, `omp.taskloop` and
`omp.wsloop` wrapper operations. It is not intended to help with the
addition of support for loop transformations, non-rectangular loops and
non-perfectly nested loops.
}];

let arguments = (ins Variadic<IntLikeType>:$lowerBound,
Variadic<IntLikeType>:$upperBound,
Variadic<IntLikeType>:$step,
UnitAttr:$inclusive);

let regions = (region AnyRegion:$region);

let extraClassDeclaration = [{
/// Returns the number of loops in the loop nest.
unsigned getNumLoops() { return getLowerBound().size(); }

/// Returns the induction variables of the loop nest.
ArrayRef<BlockArgument> getIVs() { return getRegion().getArguments(); }
}];

let hasCustomAssemblyFormat = 1;
let hasVerifier = 1;
}

//===----------------------------------------------------------------------===//
// 2.9.2 Workshare Loop Construct
//===----------------------------------------------------------------------===//
Expand Down Expand Up @@ -743,7 +807,7 @@ def SimdLoopOp : OpenMP_Op<"simdloop", [AttrSizedOperandSegments,

def YieldOp : OpenMP_Op<"yield",
[Pure, ReturnLike, Terminator,
ParentOneOf<["WsloopOp", "DeclareReductionOp",
ParentOneOf<["LoopNestOp", "WsloopOp", "DeclareReductionOp",
"AtomicUpdateOp", "SimdLoopOp", "PrivateClauseOp"]>]> {
let summary = "loop yield and termination operation";
let description = [{
Expand Down
71 changes: 71 additions & 0 deletions mlir/lib/Dialect/OpenMP/IR/OpenMPDialect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1862,6 +1862,77 @@ LogicalResult TaskloopOp::verify() {
return success();
}

//===----------------------------------------------------------------------===//
// LoopNestOp
//===----------------------------------------------------------------------===//

ParseResult LoopNestOp::parse(OpAsmParser &parser, OperationState &result) {
// Parse an opening `(` followed by induction variables followed by `)`
SmallVector<OpAsmParser::Argument> ivs;
SmallVector<OpAsmParser::UnresolvedOperand> lbs, ubs;
Type loopVarType;
if (parser.parseArgumentList(ivs, OpAsmParser::Delimiter::Paren) ||
parser.parseColonType(loopVarType) ||
// Parse loop bounds.
parser.parseEqual() ||
parser.parseOperandList(lbs, ivs.size(), OpAsmParser::Delimiter::Paren) ||
parser.parseKeyword("to") ||
parser.parseOperandList(ubs, ivs.size(), OpAsmParser::Delimiter::Paren))
return failure();

for (auto &iv : ivs)
iv.type = loopVarType;

// Parse "inclusive" flag.
if (succeeded(parser.parseOptionalKeyword("inclusive")))
result.addAttribute("inclusive",
UnitAttr::get(parser.getBuilder().getContext()));

// Parse step values.
SmallVector<OpAsmParser::UnresolvedOperand> steps;
if (parser.parseKeyword("step") ||
parser.parseOperandList(steps, ivs.size(), OpAsmParser::Delimiter::Paren))
return failure();

// Parse the body.
Region *region = result.addRegion();
if (parser.parseRegion(*region, ivs))
return failure();

// Resolve operands.
if (parser.resolveOperands(lbs, loopVarType, result.operands) ||
parser.resolveOperands(ubs, loopVarType, result.operands) ||
parser.resolveOperands(steps, loopVarType, result.operands))
return failure();

// Parse the optional attribute list.
return parser.parseOptionalAttrDict(result.attributes);
}

void LoopNestOp::print(OpAsmPrinter &p) {
Region &region = getRegion();
auto args = region.getArguments();
p << " (" << args << ") : " << args[0].getType() << " = (" << getLowerBound()
<< ") to (" << getUpperBound() << ") ";
if (getInclusive())
p << "inclusive ";
p << "step (" << getStep() << ") ";
p.printRegion(region, /*printEntryBlockArgs=*/false);
}

LogicalResult LoopNestOp::verify() {
if (getLowerBound().size() != getIVs().size())
return emitOpError() << "number of range arguments and IVs do not match";

for (auto [lb, iv] : llvm::zip_equal(getLowerBound(), getIVs())) {
if (lb.getType() != iv.getType())
return emitOpError()
<< "range argument type does not match corresponding IV type";
}

return success();
}

//===----------------------------------------------------------------------===//
// WsloopOp
//===----------------------------------------------------------------------===//
Expand Down
37 changes: 37 additions & 0 deletions mlir/test/Dialect/OpenMP/invalid.mlir
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,43 @@ func.func @proc_bind_once() {

// -----

func.func @invalid_parent(%lb : index, %ub : index, %step : index) {
// expected-error@+1 {{op expects parent op to be one of 'omp.distribute, omp.simdloop, omp.taskloop, omp.wsloop'}}
omp.loop_nest (%iv) : index = (%lb) to (%ub) step (%step) {
omp.yield
}
}

// -----

func.func @type_mismatch(%lb : index, %ub : index, %step : index) {
// TODO Remove induction variables from omp.wsloop.
omp.wsloop for (%iv) : index = (%lb) to (%ub) step (%step) {
// expected-error@+1 {{range argument type does not match corresponding IV type}}
"omp.loop_nest" (%lb, %ub, %step) ({
^bb0(%iv2: i32):
omp.yield
}) : (index, index, index) -> ()
omp.yield
}
}

// -----

func.func @iv_number_mismatch(%lb : index, %ub : index, %step : index) {
// TODO Remove induction variables from omp.wsloop.
omp.wsloop for (%iv) : index = (%lb) to (%ub) step (%step) {
// expected-error@+1 {{number of range arguments and IVs do not match}}
"omp.loop_nest" (%lb, %ub, %step) ({
^bb0(%iv1 : index, %iv2 : index):
omp.yield
}) : (index, index, index) -> ()
omp.yield
}
}

// -----

func.func @inclusive_not_a_clause(%lb : index, %ub : index, %step : index) {
// expected-error @below {{expected 'for'}}
omp.wsloop nowait inclusive
Expand Down
79 changes: 79 additions & 0 deletions mlir/test/Dialect/OpenMP/ops.mlir
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,85 @@ func.func @omp_parallel_pretty(%data_var : memref<i32>, %if_cond : i1, %num_thre
return
}

// CHECK-LABEL: omp_loop_nest
func.func @omp_loop_nest(%lb : index, %ub : index, %step : index) -> () {
// TODO Remove induction variables from omp.wsloop.
omp.wsloop for (%iv) : index = (%lb) to (%ub) step (%step) {
Comment on lines +138 to +139
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[remark] I fear there will be inconcistencies if potentially both omp.loop_nest and omp.wsloop both define potentially inconsistent loop bounds and intermediate passes not considering the wrapper passes. Since we already discussed this, only a remark here.

Copy link
Member Author

@skatrak skatrak Apr 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is true. However, the transition of omp.wsloop to be a wrapper rather than defining the whole loop will be part of a separate patch. So, after this patch alone, it's not expected to use omp.loop_nest inside of omp.wsloop. These tests just needed to add a "future-wrapper" parent operation so that the new op could be tested.

// CHECK: omp.loop_nest
// CHECK-SAME: (%{{.*}}) : index =
// CHECK-SAME: (%{{.*}}) to (%{{.*}}) step (%{{.*}})
"omp.loop_nest" (%lb, %ub, %step) ({
^bb0(%iv2: index):
omp.yield
}) : (index, index, index) -> ()
omp.yield
}

// TODO Remove induction variables from omp.wsloop.
omp.wsloop for (%iv) : index = (%lb) to (%ub) step (%step) {
// CHECK: omp.loop_nest
// CHECK-SAME: (%{{.*}}) : index =
// CHECK-SAME: (%{{.*}}) to (%{{.*}}) inclusive step (%{{.*}})
"omp.loop_nest" (%lb, %ub, %step) ({
^bb0(%iv2: index):
omp.yield
}) {inclusive} : (index, index, index) -> ()
omp.yield
}

// TODO Remove induction variables from omp.wsloop.
omp.wsloop for (%iv) : index = (%lb) to (%ub) step (%step) {
// CHECK: omp.loop_nest
// CHECK-SAME: (%{{.*}}, %{{.*}}) : index =
// CHECK-SAME: (%{{.*}}, %{{.*}}) to (%{{.*}}, %{{.*}}) step (%{{.*}}, %{{.*}})
"omp.loop_nest" (%lb, %lb, %ub, %ub, %step, %step) ({
^bb0(%iv2: index, %iv3: index):
omp.yield
}) : (index, index, index, index, index, index) -> ()
omp.yield
}

return
}

// CHECK-LABEL: omp_loop_nest_pretty
func.func @omp_loop_nest_pretty(%lb : index, %ub : index, %step : index) -> () {
// TODO Remove induction variables from omp.wsloop.
omp.wsloop for (%iv) : index = (%lb) to (%ub) step (%step) {
// CHECK: omp.loop_nest
// CHECK-SAME: (%{{.*}}) : index =
// CHECK-SAME: (%{{.*}}) to (%{{.*}}) step (%{{.*}})
omp.loop_nest (%iv2) : index = (%lb) to (%ub) step (%step) {
omp.yield
}
omp.yield
}

// TODO Remove induction variables from omp.wsloop.
omp.wsloop for (%iv) : index = (%lb) to (%ub) step (%step) {
// CHECK: omp.loop_nest
// CHECK-SAME: (%{{.*}}) : index =
// CHECK-SAME: (%{{.*}}) to (%{{.*}}) inclusive step (%{{.*}})
omp.loop_nest (%iv2) : index = (%lb) to (%ub) inclusive step (%step) {
omp.yield
}
omp.yield
}

// TODO Remove induction variables from omp.wsloop.
omp.wsloop for (%iv) : index = (%lb) to (%ub) step (%step) {
// CHECK: omp.loop_nest
// CHECK-SAME: (%{{.*}}) : index =
// CHECK-SAME: (%{{.*}}, %{{.*}}) to (%{{.*}}, %{{.*}}) step (%{{.*}}, %{{.*}})
omp.loop_nest (%iv2, %iv3) : index = (%lb, %lb) to (%ub, %ub) step (%step, %step) {
omp.yield
}
omp.yield
}

return
}

// CHECK-LABEL: omp_wsloop
func.func @omp_wsloop(%lb : index, %ub : index, %step : index, %data_var : memref<i32>, %linear_var : i32, %chunk_var : i32) -> () {

Expand Down