//===- OpenACCCGOps.td - OpenACC intermediate operations ---*- tablegen -*-===// // // Part of the MLIR Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// // // Defines MLIR OpenACC intermediate operations used to represent semantics // when decomposing and lowering `acc` dialect operations. // These operations are placed in a separate file because they do not represent // direct mappings of OpenACC language constructs. // //===----------------------------------------------------------------------===// #ifndef OPENACC_CG_OPS #define OPENACC_CG_OPS include "mlir/Interfaces/InferTypeOpInterface.td" include "mlir/Interfaces/ViewLikeInterface.td" include "mlir/IR/CommonAttrConstraints.td" // This file is intended to be included from OpenACCOps.td, which provides // the necessary includes and definitions. The operations defined here use // types and definitions from that file. //===----------------------------------------------------------------------===// // acc.reduction_init //===----------------------------------------------------------------------===// def OpenACC_ReductionInitOp : OpenACC_Op<"reduction_init", [SameOperandsAndResultType, RecursiveMemoryEffects, DeclareOpInterfaceMethods, SingleBlockImplicitTerminator<"YieldOp">]> { let summary = "Allocate and initialize a reduction variable from a recipe"; let description = [{ This operation provides materialized allocation and initialization for a private reduction variable from an OpenACC reduction recipe. The region contains the recipe's init code and must yield a single value (the private reduction storage) via `acc.yield`. The var operand is the original/shared reduction variable. The reduction_operator specifies the reduction kind (e.g. add, mul). }]; let arguments = (ins OpenACC_AnyPointerOrMappableType:$var, OpenACC_ReductionOperatorAttr:$reductionOperator); let results = (outs OpenACC_AnyPointerOrMappableType:$result); let regions = (region AnyRegion:$region); let assemblyFormat = [{ $var $reductionOperator `:` type($result) $region attr-dict }]; let hasVerifier = 1; } //===----------------------------------------------------------------------===// // acc.reduction_combine_region //===----------------------------------------------------------------------===// def OpenACC_ReductionCombineRegionOp : OpenACC_Op<"reduction_combine_region", [SameTypeOperands, RecursiveMemoryEffects, DeclareOpInterfaceMethods, SingleBlockImplicitTerminator<"YieldOp">]> { let summary = "Combine a reduction private value with its original (recipe)"; let description = [{ This operation provides materialized reduction combine code from an OpenACC reduction recipe. The region takes the partially reduced value(s) from the private reduction variable and combines them with the current value(s) in the original/shared reduction variable. The region is terminated by `acc.yield` with no operands. The destVar operand is the original/shared reduction variable. The srcVar operand is typically the result of acc.reduction_init. }]; let arguments = (ins OpenACC_AnyPointerOrMappableType:$destVar, OpenACC_AnyPointerOrMappableType:$srcVar); let results = (outs); let regions = (region AnyRegion:$region); let assemblyFormat = [{ $srcVar `into` $destVar `:` type($srcVar) $region attr-dict }]; let hasVerifier = 1; } //===----------------------------------------------------------------------===// // acc.reduction_combine //===----------------------------------------------------------------------===// def OpenACC_ReductionCombineOp: OpenACC_Op<"reduction_combine", [SameTypeOperands, DeclareOpInterfaceMethods]> { let summary = "Combine a reduction partial sum with its original value"; let description = [{ This operation is a composite to do a typical update of a reduction variable. The intention of this operator is to facilitate codegen decisions (such as generate an atomic update). E.g. ``` acc.reduction_combine %src into %dest : memref ``` Might lower to something similar to ``` %loadSrc = memref.load %src[] : memref %loadDest = memref.load %dest[] : memref %combine = arith.addi %loadSrc, %loadDest : i32 memref.store %combine, %dest[] : memref ``` The $destMemref operand is a "pointer" to the original reduction variable (typically shared). The $srcMemref operand is a "pointer" to the partial sum of the reduction (typically private). The $kind is the OpenACC reduction operator that determines how to accumulate the two values. }]; let arguments = (ins OpenACC_AnyPointerOrMappableType:$destMemref, OpenACC_AnyPointerOrMappableType:$srcMemref, OpenACC_ReductionOperatorAttr:$reductionOperator); let assemblyFormat = [{ $srcMemref `into` $destMemref $reductionOperator `:` type($destMemref) attr-dict }]; } //===----------------------------------------------------------------------===// // acc.reduction_accumulate //===----------------------------------------------------------------------===// def OpenACC_ReductionAccumulateOp : OpenACC_Op<"reduction_accumulate", []> { let summary = "Accumulate an SSA value into a reduction variable"; let description = [{ Accumulates a scalar SSA value into a pointer-like reduction variable. Example: ```mlir %private = memref.alloca() {acc.par_dims = #acc} : memref memref.store %c0, %private[] : memref %partial = scf.parallel (%iv) = (%c0) to (%cN) step (%c1) init (%c0) -> i32 { %v = memref.load %data[%iv] : memref scf.reduce(%v : i32) { ^bb0(%lhs: i32, %rhs: i32): %sum = arith.addi %lhs, %rhs : i32 scf.reduce.return %sum : i32 } } {acc.par_dims = #acc} acc.reduction_accumulate %partial to %private : i32 -> memref {par_dims = #acc} acc.reduction_combine %private into %shared : memref {acc.par_dims = #acc} ``` }]; let arguments = (ins AnyTypeOf<[AnyInteger, AnyFloat, AnyComplex]>:$value, Arg:$memref, OpenACC_ReductionOperatorAttr:$reductionOperator, OpenACC_GPUParallelDimsAttr:$par_dims); let assemblyFormat = [{ $value `to` $memref $reductionOperator `:` type($value) `->` type($memref) attr-dict }]; let hasVerifier = 1; } //===----------------------------------------------------------------------===// // acc.kernel_environment //===----------------------------------------------------------------------===// def OpenACC_KernelEnvironmentOp : OpenACC_Op<"kernel_environment", [AttrSizedOperandSegments, RecursiveMemoryEffects, SingleBlock, NoTerminator, DeclareOpInterfaceMethods, MemoryEffects<[MemWrite, MemRead]>]> { let summary = "Decomposition of compute constructs to capture data mapping " "and asynchronous behavior information"; let description = [{ The `acc.kernel_environment` operation represents a decomposition of any OpenACC compute construct (acc.kernels, acc.parallel, or acc.serial) that captures data mapping and asynchronous behavior: - data clause operands - async clause operands - wait clause operands This allows kernel execution parallelism and privatization to be handled separately, facilitating eventual lowering to GPU dialect where kernel launching and compute offloading are handled separately. }]; let arguments = (ins Variadic:$dataClauseOperands, Optional:$asyncOperand, UnitAttr:$asyncOnly, Optional:$waitDevnum, Variadic:$waitOperands, UnitAttr:$waitOnly); let regions = (region SizedRegion<1>:$region); let assemblyFormat = [{ oilist( `dataOperands` `(` $dataClauseOperands `:` type($dataClauseOperands) `)` | `async` `` custom($asyncOperand, type($asyncOperand), $asyncOnly) | `wait_devnum` `(` $waitDevnum `:` type($waitDevnum) `)` | `wait` `` custom($waitOperands, type($waitOperands), $waitOnly) ) $region attr-dict }]; let hasCanonicalizer = 1; let hasVerifier = 1; let extraClassDeclaration = [{ /// Create a `KernelEnvironmentOp` populated with data mapping and the /// async/wait clauses for `deviceType` extracted from the given ACC compute /// construct. Emplaces a block in the region and sets the rewriter's /// insertion point to the start of that block so callers can create /// operations inside it (e.g., `acc.compute_region`). template static KernelEnvironmentOp createAndPopulate( ComputeConstructT computeConstruct, ::mlir::acc::DeviceType deviceType, ::mlir::OpBuilder &builder); }]; } //===----------------------------------------------------------------------===// // acc.firstprivate_map //===----------------------------------------------------------------------===// def OpenACC_FirstprivateMapInitialOp : OpenACC_DataEntryOp< "firstprivate_map", "mlir::acc::DataClause::acc_firstprivate", "", [], (ins Arg:$var)> { let summary = "Represents the mapping of the initial value for firstprivate " "semantics."; let description = [{ The `acc.firstprivate_map` operation is an intermediate representation used during the decomposition of `acc.firstprivate` operations. It represents the mapping of the initial value from the host to the device, which is then used to initialize per-thread private copies. This operation is distinct from `acc.copyin` because: - `acc.copyin` includes present counter updates, but private variables do not impact reference counters - The mapped value is used to initialize private copies rather than being accessed directly }]; let results = (outs Arg:$accVar); let extraClassDeclaration = extraClassDeclarationBase; } //===----------------------------------------------------------------------===// // acc.privatize //===----------------------------------------------------------------------===// def OpenACC_PrivatizeOp : OpenACC_Op<"privatize", []> { let summary = "Create a handle for privatized storage along parallel dimensions"; let description = [{ Introduces a privatization handle for storage that varies across the active parallel dimensions (for example OpenACC `private` / `firstprivate` after recipe materialization). The handle type is `acc.private_type` where `T` is the logical storage type (commonly a `memref`). Optional `index` operands supply dynamic sizes when the privatized shape depends on SSA values (for example `memref` row lengths). The optional `acc.par_dims` attribute records which GPU parallel dimensions participate in the privatization. }]; let arguments = (ins Variadic:$dynamicSizes, OptionalAttr:$par_dims); let results = (outs OpenACC_PrivateType:$result); let assemblyFormat = [{ (`(` $dynamicSizes^ `)`)? (`[` qualified($par_dims)^ `]`)? attr-dict `:` functional-type(operands, results) }]; let builders = [ OpBuilder<(ins "::mlir::Type":$resultType, CArg<"::mlir::ValueRange", "{}">:$dynamicSizes), [{ $_state.addTypes(resultType); $_state.addOperands(dynamicSizes); }]> ]; } //===----------------------------------------------------------------------===// // acc.unwrap_private //===----------------------------------------------------------------------===// def OpenACC_UnwrapPrivateOp : OpenACC_Op<"unwrap_private", [NoMemoryEffect, AlwaysSpeculatable, ViewLikeOpInterface]> { let summary = "Unwrap acc.private_type handle to pointer-like storage"; let description = [{ Converts a privatization handle (`acc.private_type`) to a pointer-like view of the underlying private storage. }]; let arguments = (ins OpenACC_PrivateType:$handle); let results = (outs OpenACC_PointerLikeType:$result); let assemblyFormat = [{ $handle attr-dict `:` qualified(type($handle)) `to` type($result) }]; let extraClassDeclaration = [{ ::mlir::Value getViewSource() { return getHandle(); } }]; } //===----------------------------------------------------------------------===// // acc.private_local //===----------------------------------------------------------------------===// def OpenACC_PrivateLocalOp : OpenACC_Op<"private_local", [NoMemoryEffect, AlwaysSpeculatable]> { let summary = "Materialize privatized storage for the current parallelism context"; let description = [{ Given a value of type `acc.private_type`, materializes the underlying storage (typically a `memref`) for the current thread / iteration. Which slice of the privatized allocation is selected is determined by surrounding parallelism assigned in the context. The result type is usually `T` (often a `memref` that matches the logical storage type). The result may instead use a different surface type that still aliases the same underlying storage as `T`; in that case passes that consume this operation must treat the handle type and the result type as describing the same storage layout. }]; let arguments = (ins OpenACC_PrivateType:$privatized); let results = (outs AnyType:$output); let assemblyFormat = [{ $privatized attr-dict `:` functional-type(operands, results) }]; let builders = [ OpBuilder<(ins "::mlir::Type":$resultType, "::mlir::Value":$privatized), [{ $_state.addOperands(privatized); $_state.addTypes(resultType); }]> ]; } //===----------------------------------------------------------------------===// // acc.par_width //===----------------------------------------------------------------------===// def OpenACC_ParWidthOp : OpenACC_Op<"par_width", [NoMemoryEffect, AlwaysSpeculatable]> { let summary = "Specify parallel width for a GPU dimension"; let description = [{ The `acc.par_width` operation specifies the parallel width for a given GPU parallel dimension. It is used as an input to `acc.compute_region` to define the launch configuration. The optional `launchArg` operand provides a known width value. When absent, the width is unknown and must be determined later (either at compile time by analysis or at runtime). Examples: ```mlir // Known width from SSA value %w1 = acc.par_width %vector_len {par_dim = #acc.par_dim} // Unknown width (to be computed later) %w2 = acc.par_width {par_dim = #acc.par_dim} ``` }]; let arguments = (ins Optional:$launchArg, OpenACC_GPUParallelDimAttr:$par_dim); let results = (outs Index:$output); let assemblyFormat = [{ ($launchArg^)? attr-dict }]; } //===----------------------------------------------------------------------===// // acc.compute_region //===----------------------------------------------------------------------===// // Local type constraint for gpu::AsyncTokenType. def OpenACC_GPUAsyncTokenType : Type< CPred<"::llvm::isa<::mlir::gpu::AsyncTokenType>($_self)">, "GPU async token type">; def OpenACC_ComputeRegionOp : OpenACC_Op<"compute_region", [OffloadRegionOpInterface, AffineScope, RecursiveMemoryEffects, SingleBlockImplicitTerminator<"YieldOp">, IsolatedFromAbove, AttrSizedOperandSegments]> { let summary = "Compute region for GPU execution"; let description = [{ The `acc.compute_region` operation wraps a region of code that will be compiled and executed on a GPU. It is typically produced by lowering OpenACC compute constructs (`acc.parallel`, `acc.kernels`, `acc.serial`) but can also be targeted directly by other frontends or lowered from other constructs that benefit from the automatic parallelization and data mapping facilities that the `acc` dialect provides. It serves as the bridge between the high-level representation and the `gpu.launch` operation. The operation is `IsolatedFromAbove`: all values used inside the region must be explicitly captured. Values are captured in two ways: - Launch arguments (`launch`): Results of `acc.par_width` operations that define the parallel launch configuration. These become `index`-typed block arguments representing the parallel width for each dimension. - Input arguments (`ins`): Arbitrary values captured from outside the region (data pointers, scalars, etc.). These become block arguments with their original types. The `origin` attribute records which construct produced this compute region (e.g., `"acc.parallel"`, `"acc.kernels"`). This is intended to be solely informational. Canonicalization may simplify `ins` captures: duplicate `ins` operands (same SSA value threaded more than once) are merged by reusing the first block argument, and unused `ins` operands (block arguments with no uses) are removed. `launch` operands are never merged or dropped. Example: ```mlir %w0 = acc.par_width %c128 {par_dim = #acc.par_dim} %w1 = acc.par_width %c8 {par_dim = #acc.par_dim} acc.compute_region launch(%arg0 = %w0, %arg1 = %w1) ins(%arg2 = %data) : (memref<1024xf32>) { %c0 = arith.constant 0 : index %c1 = arith.constant 1 : index %c1024 = arith.constant 1024 : index scf.parallel (%iv) = (%c0) to (%c1024) step (%c1) { %v = memref.load %arg2[%iv] : memref<1024xf32> scf.reduce } {acc.par_dims = #acc} acc.yield } {origin = "acc.parallel"} ``` }]; let arguments = (ins Variadic:$launchArgs, Variadic:$inputArgs, Optional:$stream, StrAttr:$origin, OptionalAttr:$kernel_func_name, OptionalAttr:$kernel_module_name); let results = (outs Variadic:$results); let regions = (region AnyRegion:$region); let hasCanonicalizer = 1; let extraClassDeclaration = [{ /// Look up the par_width op for the given dimension among launch args. std::optional getLaunchArg( ::mlir::acc::GPUParallelDimAttr parDim); /// Get the known (non-empty) launch value for a dimension. std::optional getKnownLaunchArg( ::mlir::acc::GPUParallelDimAttr parDim); /// Get the known constant launch value for a dimension. std::optional getKnownConstantLaunchArg( ::mlir::acc::GPUParallelDimAttr parDim); /// Add a new input argument, appending to both the operand list and /// the region block arguments. Returns the new block argument. ::mlir::BlockArgument appendInputArg(::mlir::Value); /// After hoisting `value`'s defining op, wire it into this region: append it /// as an `ins` operand, add the matching body entry argument, and replace /// uses under this region with that argument (excluding this op's own `ins` /// operand uses). /// /// Requires that `value` is defined outside `getRegion()` and is still used /// inside the region. Otherwise returns `std::nullopt`. std::optional<::mlir::BlockArgument> wireHoistedValueThroughIns(::mlir::Value value); /// Check whether all parallel dimensions have width 1. bool isEffectivelySerial(); /// Get the block argument representing the width for a given dimension. ::mlir::BlockArgument parDimToWidth( ::mlir::acc::GPUParallelDimAttr parDim); /// Get the block argument for a specific gpu::Processor. ::mlir::BlockArgument gpuParWidth(::mlir::gpu::Processor); /// Collect all GPU parallel dimensions present in the launch config. llvm::SmallVector<::mlir::acc::GPUParallelDimAttr> getLaunchParDims(); /// Get the body block of the compute region. ::mlir::Block *getBody() { return &getRegion().front(); } /// Get the terminator of the compute region. ::mlir::Operation *getTerminator() { return &getRegion().back().back(); } /// Return the `launch` or `ins` operand threaded to `blockArg`, or a null /// `Value` if `blockArg` is not an argument of `getBody()` or its index is /// out of range for this op's `launch` and `ins` operands. ::mlir::Value getOperand(::mlir::BlockArgument blockArg); /// If `value` is a launch or input operand, return the body block argument /// it is threaded through; otherwise `std::nullopt`. If `value` matches /// more than one `ins` operand, the first match is returned (canonicalization /// may merge duplicate `ins` values). Duplicate `launch` operands are not /// folded. std::optional<::mlir::BlockArgument> getBlockArg(::mlir::Value value); }]; let hasVerifier = 1; let hasCustomAssemblyFormat = 1; } //===----------------------------------------------------------------------===// // acc.predicate_region //===----------------------------------------------------------------------===// def OpenACC_PredicateRegionOp : OpenACC_Op<"predicate_region", [RecursiveMemoryEffects, NoTerminator]> { let summary = "Groups operations at intermediate loop-nest points"; let description = [{ Groups statements within an `acc.compute_region` that sit at an intermediate point in a loop nest (outside a partitioned loop body, between or around nested loops). This grouping marks code whose execution scope differs from that of surrounding partitioned loops, so predication and synchronization can be applied correctly during lowering. Corresponds to OpenACC single or redundant execution at nest transitions. Example: ```mlir // !$acc parallel num_gangs(NG) vector_length(VL) // !$acc loop gang // !$acc atomic update // !$acc loop vector // !$acc atomic update %w_gang = acc.par_width %cNG {par_dim = #acc.par_dim} %w_vector = acc.par_width %cVL {par_dim = #acc.par_dim} acc.compute_region launch(%ng = %w_gang, %vl = %w_vector) ins(%arg_c1 = %c1, %arg_c2 = %c2) : (memref, memref) { scf.parallel (%i) = (%c1) to (%cN) step (%c1) { acc.predicate_region { acc.atomic.update %arg_c1 : memref { ^bb0(%old: i32): %one = arith.constant 1 : i32 %sum = arith.addi %old, %one : i32 acc.yield %sum : i32 } } scf.parallel (%j) = (%c1) to (%cN) step (%c1) { acc.atomic.update %arg_c2 : memref { ^bb0(%old: i32): %one = arith.constant 1 : i32 %sum = arith.addi %old, %one : i32 acc.yield %sum : i32 } scf.reduce } {acc.par_dims = #acc} scf.reduce } {acc.par_dims = #acc} acc.yield } {origin = "acc.parallel"} ``` }]; let regions = (region AnyRegion:$region); let assemblyFormat = "$region attr-dict"; let hasVerifier = 1; } #endif // OPENACC_CG_OPS