Files
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

1226 lines
52 KiB
C++

//===- ShardToMPI.cpp - Shard to MPI dialect conversion -----------------===//
//
// Part of the LLVM 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
//
//===----------------------------------------------------------------------===//
//
// This file implements a translation of Shard communication ops to MPI ops.
//
//===----------------------------------------------------------------------===//
#include "mlir/Conversion/ShardToMPI/ShardToMPI.h"
#include "mlir/Dialect/Shard/Transforms/Transforms.h"
#include "mlir/Dialect/Affine/IR/AffineOps.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/Bufferization/IR/Bufferization.h"
#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h"
#include "mlir/Dialect/Func/IR/FuncOps.h"
#include "mlir/Dialect/Func/Transforms/FuncConversions.h"
#include "mlir/Dialect/Linalg/IR/Linalg.h"
#include "mlir/Dialect/MPI/IR/MPI.h"
#include "mlir/Dialect/MemRef/IR/MemRef.h"
#include "mlir/Dialect/MemRef/Utils/MemRefUtils.h"
#include "mlir/Dialect/SCF/IR/SCF.h"
#include "mlir/Dialect/Shard/IR/ShardDialect.h"
#include "mlir/Dialect/Shard/IR/ShardOps.h"
#include "mlir/Dialect/Shard/Transforms/Simplify.h"
#include "mlir/Dialect/Shard/Transforms/Transforms.h"
#include "mlir/Dialect/Tensor/IR/Tensor.h"
#include "mlir/Dialect/Utils/StaticValueUtils.h"
#include "mlir/IR/AffineMap.h"
#include "mlir/IR/Builders.h"
#include "mlir/IR/BuiltinAttributes.h"
#include "mlir/IR/BuiltinTypes.h"
#include "mlir/IR/PatternMatch.h"
#include "mlir/IR/SymbolTable.h"
#include "mlir/Transforms/DialectConversion.h"
#include "mlir/Transforms/GreedyPatternRewriteDriver.h"
#define DEBUG_TYPE "shard-to-mpi"
namespace mlir {
#define GEN_PASS_DEF_CONVERTSHARDTOMPIPASS
#include "mlir/Conversion/Passes.h.inc"
} // namespace mlir
using namespace mlir;
using namespace shard;
namespace {
/// Converts a vector of OpFoldResults (ints) into vector of Values of the
/// provided type.
static SmallVector<Value> getMixedAsValues(OpBuilder b, const Location &loc,
llvm::ArrayRef<int64_t> statics,
ValueRange dynamics,
Type type = Type()) {
SmallVector<Value> values;
auto dyn = dynamics.begin();
Type i64 = b.getI64Type();
if (!type)
type = i64;
assert((i64 == type || b.getIndexType() == type) &&
"expected an i64 or an intex type");
for (auto s : statics) {
if (s == ShapedType::kDynamic) {
values.emplace_back(*(dyn++));
} else {
TypedAttr val = type == i64 ? b.getI64IntegerAttr(s) : b.getIndexAttr(s);
values.emplace_back(arith::ConstantOp::create(b, loc, type, val));
}
}
return values;
}
/// Create operations converting a linear index to a multi-dimensional index.
[[maybe_unused]] static SmallVector<Value>
linearToMultiIndex(Location loc, OpBuilder b, Value linearIndex,
ValueRange dimensions) {
int n = dimensions.size();
SmallVector<Value> multiIndex(n);
for (int i = n - 1; i >= 0; --i) {
multiIndex[i] = arith::RemSIOp::create(b, loc, linearIndex, dimensions[i]);
if (i > 0)
linearIndex = arith::DivSIOp::create(b, loc, linearIndex, dimensions[i]);
}
return multiIndex;
}
/// Create operations converting a multi-dimensional index to a linear index.
Value multiToLinearIndex(Location loc, OpBuilder b, ValueRange multiIndex,
ValueRange dimensions) {
Value linearIndex = arith::ConstantIndexOp::create(b, loc, 0);
Value stride = arith::ConstantIndexOp::create(b, loc, 1);
for (int i = multiIndex.size() - 1; i >= 0; --i) {
Value off = arith::MulIOp::create(b, loc, multiIndex[i], stride);
linearIndex = arith::AddIOp::create(b, loc, linearIndex, off);
stride = arith::MulIOp::create(b, loc, stride, dimensions[i]);
}
return linearIndex;
}
/// Replace GetShardingOp with related/dependent ShardingOp.
struct ConvertGetShardingOp : public OpConversionPattern<GetShardingOp> {
using OpConversionPattern::OpConversionPattern;
LogicalResult
matchAndRewrite(GetShardingOp op, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
auto shardOp = adaptor.getSource().getDefiningOp<ShardOp>();
if (!shardOp)
return failure();
auto shardingOp = shardOp.getSharding().getDefiningOp<ShardingOp>();
if (!shardingOp)
return failure();
rewriter.replaceOp(op, shardingOp.getResult());
return success();
}
};
/// Convert a sharding op to a tuple of tensors of its components
/// (SplitAxes, HaloSizes, ShardedDimsOffsets)
/// as defined by type converter.
struct ConvertShardingOp : public OpConversionPattern<ShardingOp> {
using OpConversionPattern::OpConversionPattern;
LogicalResult
matchAndRewrite(ShardingOp op, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
auto splitAxes = op.getSplitAxes().getAxes();
int64_t maxNAxes = 0;
for (auto axes : splitAxes)
maxNAxes = std::max<int64_t>(maxNAxes, axes.size());
// To hold the split axes, create empty 2d tensor with shape
// {splitAxes.size(), max-size-of-split-groups}.
// Set trailing elements for smaller split-groups to -1.
Location loc = op.getLoc();
auto i16 = rewriter.getI16Type();
auto i64 = rewriter.getI64Type();
std::array<int64_t, 2> shape = {static_cast<int64_t>(splitAxes.size()),
maxNAxes};
Value resSplitAxes = tensor::EmptyOp::create(rewriter, loc, shape, i16);
auto attr = IntegerAttr::get(i16, -1);
Value fillValue = arith::ConstantOp::create(rewriter, loc, i16, attr);
resSplitAxes =
linalg::FillOp::create(rewriter, loc, fillValue, resSplitAxes)
.getResult(0);
// explicitly write values into tensor row by row
std::array<int64_t, 2> strides = {1, 1};
int64_t nSplits = 0;
ValueRange empty = {};
for (auto [i, axes] : llvm::enumerate(splitAxes)) {
int64_t size = axes.size();
if (size > 0)
++nSplits;
std::array<int64_t, 2> offs = {(int64_t)i, 0};
std::array<int64_t, 2> sizes = {1, size};
auto tensorType = RankedTensorType::get({size}, i16);
auto attrs = DenseIntElementsAttr::get(tensorType, axes.asArrayRef());
auto vals = arith::ConstantOp::create(rewriter, loc, tensorType, attrs);
resSplitAxes = tensor::InsertSliceOp::create(rewriter, loc, vals,
resSplitAxes, empty, empty,
empty, offs, sizes, strides);
}
// To hold halos sizes, create 2d Tensor with shape {nSplits, 2}.
// Store the halo sizes in the tensor.
SmallVector<Value> haloSizes =
getMixedAsValues(rewriter, loc, adaptor.getStaticHaloSizes(),
adaptor.getDynamicHaloSizes());
auto type = RankedTensorType::get({nSplits, 2}, i64);
Value resHaloSizes =
haloSizes.empty()
? tensor::EmptyOp::create(rewriter, loc,
std::array<int64_t, 2>{0, 0}, i64)
.getResult()
: tensor::FromElementsOp::create(rewriter, loc, type, haloSizes)
.getResult();
// To hold sharded dims offsets, create Tensor with shape {nSplits,
// maxSplitSize+1}. Store the offsets in the tensor but set trailing
// elements for smaller split-groups to -1. Computing the max size of the
// split groups needs using collectiveProcessGroupSize (which needs the
// GridOp)
Value resOffsets;
if (adaptor.getStaticShardedDimsOffsets().empty()) {
resOffsets = tensor::EmptyOp::create(rewriter, loc,
std::array<int64_t, 2>{0, 0}, i64);
} else {
SymbolTableCollection symbolTableCollection;
auto gridOp = getGrid(op, symbolTableCollection);
int64_t maxSplitSize = 0;
for (auto axes : splitAxes) {
int64_t splitSize =
collectiveProcessGroupSize(axes.asArrayRef(), gridOp.getShape());
assert(splitSize != ShapedType::kDynamic);
maxSplitSize = std::max<int64_t>(maxSplitSize, splitSize);
}
assert(maxSplitSize);
++maxSplitSize; // add one for the total size
resOffsets = tensor::EmptyOp::create(
rewriter, loc, std::array<int64_t, 2>{nSplits, maxSplitSize}, i64);
Value zero = arith::ConstantOp::create(
rewriter, loc, i64, rewriter.getI64IntegerAttr(ShapedType::kDynamic));
resOffsets =
linalg::FillOp::create(rewriter, loc, zero, resOffsets).getResult(0);
SmallVector<Value> offsets =
getMixedAsValues(rewriter, loc, adaptor.getStaticShardedDimsOffsets(),
adaptor.getDynamicShardedDimsOffsets());
int64_t curr = 0;
for (auto [i, axes] : llvm::enumerate(splitAxes)) {
int64_t splitSize =
collectiveProcessGroupSize(axes.asArrayRef(), gridOp.getShape());
assert(splitSize != ShapedType::kDynamic && splitSize < maxSplitSize);
++splitSize; // add one for the total size
ArrayRef<Value> values(&offsets[curr], splitSize);
Value vals = tensor::FromElementsOp::create(rewriter, loc, values);
std::array<int64_t, 2> offs = {static_cast<int64_t>(i), 0};
std::array<int64_t, 2> sizes = {1, splitSize};
resOffsets = tensor::InsertSliceOp::create(rewriter, loc, vals,
resOffsets, empty, empty,
empty, offs, sizes, strides);
curr += splitSize;
}
}
// return a tuple of tensors as defined by type converter
SmallVector<Type> resTypes;
if (failed(getTypeConverter()->convertType(op.getResult().getType(),
resTypes)))
return failure();
resSplitAxes =
tensor::CastOp::create(rewriter, loc, resTypes[0], resSplitAxes);
resHaloSizes =
tensor::CastOp::create(rewriter, loc, resTypes[1], resHaloSizes);
resOffsets = tensor::CastOp::create(rewriter, loc, resTypes[2], resOffsets);
rewriter.replaceOpWithNewOp<UnrealizedConversionCastOp>(
op, TupleType::get(op.getContext(), resTypes),
ValueRange{resSplitAxes, resHaloSizes, resOffsets});
return success();
}
};
class ConvertProcessLinearIndexOp
: public OpConversionPattern<ProcessLinearIndexOp> {
public:
using OpConversionPattern::OpConversionPattern;
LogicalResult
matchAndRewrite(ProcessLinearIndexOp op, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
// Create mpi::CommRankOp
Location loc = op.getLoc();
auto *ctx = op.getContext();
Value commWorld =
mpi::CommWorldOp::create(rewriter, loc, mpi::CommType::get(ctx));
auto rank = mpi::CommRankOp::create(
rewriter, loc,
TypeRange{mpi::RetvalType::get(ctx), rewriter.getI32Type()},
commWorld)
.getRank();
rewriter.replaceOpWithNewOp<arith::IndexCastOp>(op, rewriter.getIndexType(),
rank);
return success();
}
};
struct ConvertNeighborsLinearIndicesOp
: public OpConversionPattern<NeighborsLinearIndicesOp> {
using OpConversionPattern::OpConversionPattern;
LogicalResult
matchAndRewrite(NeighborsLinearIndicesOp op, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
// Computes the neighbors indices along a split axis by simply
// adding/subtracting 1 to the current index in that dimension.
// Assigns -1 if neighbor is out of bounds.
auto axes = adaptor.getSplitAxes();
// For now only single axis sharding is supported
if (axes.size() != 1)
return failure();
Location loc = op.getLoc();
SymbolTableCollection symbolTableCollection;
auto gridOp = getGrid(op, symbolTableCollection);
auto mIdx = adaptor.getDevice();
auto orgIdx = mIdx[axes[0]];
SmallVector<Value> dims;
llvm::transform(
gridOp.getShape(), std::back_inserter(dims), [&](int64_t i) {
return arith::ConstantIndexOp::create(rewriter, loc, i).getResult();
});
Value dimSz = dims[axes[0]];
Value one = arith::ConstantIndexOp::create(rewriter, loc, 1);
Value minus1 = arith::ConstantIndexOp::create(rewriter, loc, -1);
Value atBorder =
arith::CmpIOp::create(rewriter, loc, arith::CmpIPredicate::sle, orgIdx,
arith::ConstantIndexOp::create(rewriter, loc, 0));
auto down = scf::IfOp::create(
rewriter, loc, atBorder,
[&](OpBuilder &builder, Location loc) {
scf::YieldOp::create(builder, loc, minus1);
},
[&](OpBuilder &builder, Location loc) {
SmallVector<Value> tmp = mIdx;
tmp[axes[0]] =
arith::SubIOp::create(rewriter, op.getLoc(), orgIdx, one)
.getResult();
scf::YieldOp::create(builder, loc,
multiToLinearIndex(loc, rewriter, tmp, dims));
});
atBorder = arith::CmpIOp::create(
rewriter, loc, arith::CmpIPredicate::sge, orgIdx,
arith::SubIOp::create(rewriter, loc, dimSz, one).getResult());
auto up = scf::IfOp::create(
rewriter, loc, atBorder,
[&](OpBuilder &builder, Location loc) {
scf::YieldOp::create(builder, loc, minus1);
},
[&](OpBuilder &builder, Location loc) {
SmallVector<Value> tmp = mIdx;
tmp[axes[0]] =
arith::AddIOp::create(rewriter, op.getLoc(), orgIdx, one);
scf::YieldOp::create(builder, loc,
multiToLinearIndex(loc, rewriter, tmp, dims));
});
rewriter.replaceOp(op, ValueRange{down.getResult(0), up.getResult(0)});
return success();
}
};
struct ConvertShardShapeOp : public OpConversionPattern<ShardShapeOp> {
using OpConversionPattern::OpConversionPattern;
LogicalResult
matchAndRewrite(ShardShapeOp op, OneToNOpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
auto sharding = op.getSharding().getDefiningOp<ShardingOp>();
if (!sharding) {
return op->emitError()
<< "Expected ShardingOp as defining op for sharding"
<< " but found " << adaptor.getSharding()[0].getDefiningOp();
}
// Compute the sharded shape by applying the sharding to the input shape.
// If shardedDimsOffsets is not defined in the sharding, the shard shape is
// computed by dividing the dimension size by the number of shards in that
// dimension (which is given by the size of the grid axes provided in
// split-axes). Odd elements get distributed to trailing shards. If a
// shardedDimsOffsets is provided, the shard shape is computed by
// subtracting the offset of the current shard from the offset of the next
// shard.
Location loc = op.getLoc();
Type index = rewriter.getIndexType();
// This is a 1:N conversion because the sharding op is a 1:3 conversion.
// The operands in the adaptor are a vector<ValeRange>. For dims and device
// we have a 1:1 conversion.
// For simpler access fill a vector with the dynamic dims.
SmallVector<Value> dynDims, dynDevice;
for (auto dim : adaptor.getDimsDynamic()) {
// type conversion should be 1:1 for ints
dynDims.emplace_back(llvm::getSingleElement(dim));
}
// same for device
for (auto device : adaptor.getDeviceDynamic()) {
dynDevice.emplace_back(llvm::getSingleElement(device));
}
// To keep the code simple, convert dims/device to values when they are
// attributes. Count on canonicalization to fold static values.
SmallVector<Value> shape =
getMixedAsValues(rewriter, loc, op.getDims(), dynDims, index);
SmallVector<Value> multiIdx =
getMixedAsValues(rewriter, loc, adaptor.getDevice(), dynDevice, index);
// Get the GridOp, the grid shape is needed to compute the sharded shape.
SymbolTableCollection symbolTableCollection;
auto gridOp = getGrid(sharding, symbolTableCollection);
// For now we only support static grid shapes
if (ShapedType::isDynamicShape(gridOp.getShape()))
return failure();
auto splitAxes = sharding.getSplitAxes().getAxes();
// shardedDimsOffsets are optional and might be Values (not attributes).
// Also, the shardId might be dynamic which means the position in the
// shardedDimsOffsets is not statically known. Create a tensor of the
// shardedDimsOffsets and later extract the offsets for computing the
// local shard-size.
Value shardedDimsOffs;
{
SmallVector<Value> tmp = getMixedAsValues(
rewriter, loc, sharding.getStaticShardedDimsOffsets(),
sharding.getDynamicShardedDimsOffsets(), index);
if (!tmp.empty())
shardedDimsOffs = tensor::FromElementsOp::create(
rewriter, loc, RankedTensorType::get({(int64_t)tmp.size()}, index),
tmp);
}
// With static grid shape the sizes of the split axes are known.
// Hence the start/pos for each split axes in shardDimsOffsets can be
// computed statically.
int64_t pos = 0;
SmallVector<Value> shardShape;
Value zero =
arith::ConstantOp::create(rewriter, loc, rewriter.getZeroAttr(index));
Value one =
arith::ConstantOp::create(rewriter, loc, rewriter.getOneAttr(index));
// Iterate over the dimensions of the tensor shape, get their split Axes,
// and compute the sharded shape.
for (auto [i, dim] : llvm::enumerate(shape)) {
// Trailing dimensions might not be annotated.
if (i < splitAxes.size() && !splitAxes[i].empty()) {
auto axes = splitAxes[i];
// The current dimension might not be sharded.
// Create a value from the static position in shardDimsOffsets.
Value posVal = arith::ConstantOp::create(rewriter, loc,
rewriter.getIndexAttr(pos));
// Get the index of the local shard in the grid axis.
Value idx = multiIdx[axes[0]];
auto numShards =
collectiveProcessGroupSize(axes.asArrayRef(), gridOp.getShape());
if (shardedDimsOffs) {
// If sharded dims offsets are provided, use them to compute the
// sharded shape.
if (axes.size() > 1) {
return op->emitError() << "Only single axis sharding is "
<< "supported for each dimension.";
}
idx = arith::AddIOp::create(rewriter, loc, posVal, idx);
// Compute size = shardedDimsOffs[idx+1] - shardedDimsOffs[idx].
Value off =
tensor::ExtractOp::create(rewriter, loc, shardedDimsOffs, idx);
idx = arith::AddIOp::create(rewriter, loc, idx, one);
Value nextOff =
tensor::ExtractOp::create(rewriter, loc, shardedDimsOffs, idx);
Value sz = arith::SubIOp::create(rewriter, loc, nextOff, off);
shardShape.emplace_back(sz);
} else {
Value numShardsVal = arith::ConstantOp::create(
rewriter, loc, rewriter.getIndexAttr(numShards));
// Compute shard dim size by distributing odd elements to trailing
// shards:
// sz = dim / numShards
// + (idx >= (numShards - (dim % numShards)) ? 1 : 0)
Value sz = arith::DivSIOp::create(rewriter, loc, dim, numShardsVal);
Value sz1 = arith::RemSIOp::create(rewriter, loc, dim, numShardsVal);
sz1 = arith::SubIOp::create(rewriter, loc, numShardsVal, sz1);
auto cond = arith::CmpIOp::create(
rewriter, loc, arith::CmpIPredicate::sge, idx, sz1);
Value odd = arith::SelectOp::create(rewriter, loc, cond, one, zero);
sz = arith::AddIOp::create(rewriter, loc, sz, odd);
shardShape.emplace_back(sz);
}
pos += numShards + 1; // add one for the total size.
} // else no sharding if split axis is empty or no split axis
// If no size was added -> no sharding in this dimension.
if (shardShape.size() <= i)
shardShape.emplace_back(dim);
}
assert(shardShape.size() == shape.size());
rewriter.replaceOp(op, shardShape);
return success();
}
};
static mpi::MPI_ReductionOpEnumAttr getMPIReductionOp(ReductionKindAttr kind) {
auto *ctx = kind.getContext();
auto getReductionOp = [ctx](mpi::MPI_ReductionOpEnum redOp) {
return mpi::MPI_ReductionOpEnumAttr::get(ctx, redOp);
};
switch (kind.getValue()) {
case ReductionKind::Sum:
return getReductionOp(mpi::MPI_ReductionOpEnum::MPI_SUM);
case ReductionKind::Product:
return getReductionOp(mpi::MPI_ReductionOpEnum::MPI_PROD);
case ReductionKind::Min:
return getReductionOp(mpi::MPI_ReductionOpEnum::MPI_MIN);
case ReductionKind::Max:
return getReductionOp(mpi::MPI_ReductionOpEnum::MPI_MAX);
case ReductionKind::BitwiseAnd:
return getReductionOp(mpi::MPI_ReductionOpEnum::MPI_BAND);
case ReductionKind::BitwiseOr:
return getReductionOp(mpi::MPI_ReductionOpEnum::MPI_BOR);
case ReductionKind::BitwiseXor:
return getReductionOp(mpi::MPI_ReductionOpEnum::MPI_BXOR);
default:
llvm_unreachable("Unknown/unsupported reduction kind");
}
}
template <typename CommOp>
struct CommOpPattern : public OpConversionPattern<CommOp> {
using OpConversionPattern<CommOp>::OpConversionPattern;
MemRefType getMemrefType(ShapedType tensorType) const {
return MemRefType::get(tensorType.getShape(), tensorType.getElementType());
}
Value getAsMemref(Value input, ImplicitLocOpBuilder &iBuilder,
bool readOnly) const {
auto itype = input.getType();
// If the source is a tensor, materialize a memref for it.
if (isa<RankedTensorType>(itype)) {
auto memrefType = getMemrefType(cast<ShapedType>(itype));
input = bufferization::ToBufferOp::create(iBuilder, memrefType, input,
readOnly);
} else {
assert(isa<MemRefType>(itype) &&
"expected input to be of MemRefType or TensorType");
}
return input;
}
FailureOr<GridOp> checkGrid(CommOp op,
SymbolTableCollection &symbolTableCollection,
bool allowDynamic = false) const {
GridOp gridOp = getGrid(op, symbolTableCollection);
if (!gridOp)
return op->emitError() << "Missing grid symbol.";
if (!allowDynamic && ShapedType::isDynamicShape(gridOp.getShape()))
return op->emitError() << "Dynamic grid shape not supported.";
return gridOp;
}
// Get an MPI_Comm_split for a given grid and axes.
// The color is the linear index of the process in the grid along the
// non-'grid-axes'. The key is the linear index of the process in the grid
// along the grid-axes.
Value getComm(GridOp &gridOp, ::llvm::ArrayRef<int16_t> gridAxes,
ImplicitLocOpBuilder &iBuilder) const {
size_t gridDims = gridOp.getShape().size();
auto commType = mpi::CommType::get(gridOp->getContext());
Value commWorld = mpi::CommWorldOp::create(iBuilder, commType);
if (gridAxes.empty() || gridAxes.size() >= gridDims) {
return commWorld;
}
SmallVector<GridAxis> otherAxes;
for (GridAxis i = 0; i < static_cast<GridAxis>(gridDims); ++i) {
if (!llvm::is_contained(gridAxes, i))
otherAxes.emplace_back(i);
}
SmallVector<Type> indexResultTypes(otherAxes.size(),
iBuilder.getIndexType());
Value color =
createProcessLinearIndex(iBuilder, gridOp.getSymName(), otherAxes);
color = arith::IndexCastOp::create(iBuilder, iBuilder.getI32Type(), color);
Value key =
createProcessLinearIndex(iBuilder, gridOp.getSymName(), gridAxes);
key = arith::IndexCastOp::create(iBuilder, iBuilder.getI32Type(), key);
// Finally split the communicator
return mpi::CommSplitOp::create(iBuilder, commType, commWorld, color, key)
.getNewcomm();
}
};
struct ConvertAllReduceOp : public CommOpPattern<AllReduceOp> {
using CommOpPattern::CommOpPattern;
LogicalResult
matchAndRewrite(AllReduceOp op, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
SymbolTableCollection symbolTableCollection;
FailureOr<GridOp> gridOp = checkGrid(op, symbolTableCollection);
if (failed(gridOp))
return failure();
ImplicitLocOpBuilder iBuilder(op.getLoc(), rewriter);
Value input = getAsMemref(adaptor.getInput(), iBuilder, true);
MemRefType inType = cast<MemRefType>(input.getType());
if (!memref::isStaticShapeAndContiguousRowMajor(inType))
return op.emitError(
"Expected static shaped memref in contiguous row-major layout.");
MemRefType outType = getMemrefType(cast<ShapedType>(op.getType()));
if (!memref::isStaticShapeAndContiguousRowMajor(outType))
return op.emitError(
"Expected static shaped memref in contiguous row-major layout.");
// Allocate buffer and copy input to buffer.
Value buffer = memref::AllocOp::create(iBuilder, outType);
linalg::CopyOp::create(iBuilder, input, buffer);
// Get the right communicator
Value comm = getComm(*gridOp, adaptor.getGridAxes(), iBuilder);
// Create the MPI AllReduce operation.
mpi::AllReduceOp::create(iBuilder, TypeRange(), buffer, buffer,
getMPIReductionOp(adaptor.getReductionAttr()),
comm);
// If the destination is a tensor, cast it to a tensor
if (isa<RankedTensorType>(op.getType()))
buffer = bufferization::ToTensorOp::create(iBuilder, op.getType(), buffer,
true);
rewriter.replaceOp(op, buffer);
return success();
}
};
struct ConvertReduceScatterOp : public CommOpPattern<ReduceScatterOp> {
using CommOpPattern::CommOpPattern;
// shard.reduce_scatter reduces and then scatters along a specified
// scatter-dim. mpi.reduce_scatter_block always scatters along the first
// dimension. Hence, if scatter-dim != 0, we need to rearrange the input
// data by expanding the scatter-dim into {nRanks, output_scatter_dim} and
// transposing nRanks to the first dimension.
LogicalResult
matchAndRewrite(ReduceScatterOp op, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
auto gridAxes = adaptor.getGridAxes();
int64_t scatterDim = adaptor.getScatterDimAttr().getInt();
SymbolTableCollection symbolTableCollection;
FailureOr<GridOp> gridOp = checkGrid(op, symbolTableCollection);
if (failed(gridOp))
return failure();
ImplicitLocOpBuilder ib(op.getLoc(), rewriter);
Value rawInput = adaptor.getInput();
auto inShapedType = cast<ShapedType>(rawInput.getType());
MemRefType outType = getMemrefType(cast<ShapedType>(op.getType()));
auto elemType = outType.getElementType();
auto inputShape = inShapedType.getShape();
auto outputShape = outType.getShape();
int64_t inputDimOnAxis = inputShape[scatterDim];
int64_t outputDimOnAxis = outputShape[scatterDim];
for (size_t i = 0; i < outputShape.size(); ++i)
if (outputShape[i] != inputShape[i] &&
i != static_cast<size_t>(scatterDim))
return op.emitError(
"Result and input shapes must match along non-scatter axes.");
if (outputDimOnAxis == 0)
return op.emitError(
"Output size along the scatter axis must be non-zero.");
if (inputDimOnAxis % outputDimOnAxis != 0)
return op.emitError(
"Input size along the scatter axis must be an exact "
"multiple of the output size along the scatter axis.");
if (!memref::isStaticShapeAndContiguousRowMajor(outType))
return op.emitError("Result must be a statically shaped memref in "
"contiguous row-major layout.");
int64_t nRanks = inputDimOnAxis / outputDimOnAxis;
// Verify that nRanks matches the number of devices along the grid axes.
int64_t gridGroupSize =
collectiveProcessGroupSize(gridAxes, gridOp->getShape());
if (nRanks != gridGroupSize)
return op.emitError()
<< "Expected the scatter factor (" << nRanks
<< ") to match the number of devices along grid_axes ("
<< gridGroupSize << ").";
// Get the right communicator.
Value comm = getComm(*gridOp, gridAxes, ib);
Value mpiInput;
if (scatterDim == 0) {
// scatter_dim == 0 maps directly to MPI_Reduce_scatter_block.
// Input must be contiguous for MPI.
Value input = getAsMemref(rawInput, ib, true);
MemRefType inType = cast<MemRefType>(input.getType());
if (!memref::isStaticShapeAndContiguousRowMajor(inType))
return op.emitError("Input must be a statically shaped memref in "
"contiguous row-major layout.");
mpiInput = input;
} else {
// For scatter_dim != 0 we rearrange the input so the scatter factor
// becomes the first dimension.
//
// 1. Get a tensor representation of the input (avoid memref->tensor
// round-trip if the input is already a tensor).
Value tensorInput = rawInput;
if (!isa<RankedTensorType>(rawInput.getType())) {
auto inTensorType = RankedTensorType::get(inputShape, elemType);
tensorInput =
bufferization::ToTensorOp::create(ib, inTensorType, rawInput, true);
}
// 2. Expand the scatter dim from {d0, ..., d_sd, ..., dN} to
// {d0, ..., nRanks, o_sd, ..., dN}.
SmallVector<int64_t> expandedShape;
SmallVector<ReassociationIndices> expandReassociation;
int64_t expandedIdx = 0;
for (int64_t i = 0; i < static_cast<int64_t>(inputShape.size()); ++i) {
if (i == scatterDim) {
expandedShape.push_back(nRanks);
expandedShape.push_back(outputDimOnAxis);
expandReassociation.push_back({expandedIdx, expandedIdx + 1});
expandedIdx += 2;
} else {
expandedShape.push_back(inputShape[i]);
expandReassociation.push_back({expandedIdx});
expandedIdx += 1;
}
}
auto expandedType = RankedTensorType::get(expandedShape, elemType);
tensorInput = tensor::ExpandShapeOp::create(ib, expandedType, tensorInput,
expandReassociation);
// 3. Transpose to move nRanks (at position scatterDim) to position 0:
// {d0, ..., nRanks, o_sd, ..., dN} -> {nRanks, d0, ..., o_sd, ..., dN}
SmallVector<int64_t> permutation, transposedShape;
permutation.emplace_back(scatterDim);
for (int64_t i = 0; i < scatterDim; ++i)
permutation.emplace_back(i);
for (int64_t i = scatterDim + 1; i < (int64_t)expandedShape.size(); ++i)
permutation.emplace_back(i);
for (auto p : permutation)
transposedShape.emplace_back(expandedShape[p]);
Value permOutput = tensor::EmptyOp::create(ib, transposedShape, elemType);
tensorInput =
linalg::TransposeOp::create(ib, tensorInput, permOutput, permutation)
->getResult(0);
// 4. Materialize as contiguous memref for MPI by copying into a
// freshly allocated buffer.
auto mpiInType = MemRefType::get(transposedShape, elemType);
Value transposedBuf =
bufferization::ToBufferOp::create(ib, mpiInType, tensorInput, true);
mpiInput = memref::AllocOp::create(ib, mpiInType);
linalg::CopyOp::create(ib, transposedBuf, mpiInput);
}
// Allocate output buffer.
Value output = memref::AllocOp::create(ib, outType);
// Create the MPI ReduceScatter operation.
mpi::ReduceScatterBlockOp::create(
ib, TypeRange(), mpiInput, output,
getMPIReductionOp(adaptor.getReductionAttr()), comm);
// If the destination is a tensor, cast it to a tensor.
if (isa<RankedTensorType>(op.getType()))
output =
bufferization::ToTensorOp::create(ib, op.getType(), output, true);
else if (scatterDim != 0) // Deallocate the temporary input buffer
memref::DeallocOp::create(ib, mpiInput);
// Notice: If this is called from tensor-world, then we assume an extra pass
// will take care of deallocating the intermediate buffers.
rewriter.replaceOp(op, output);
return success();
}
};
struct ConvertAllGatherOp : public CommOpPattern<AllGatherOp> {
using CommOpPattern::CommOpPattern;
// shard.allgather concatenates along a specified gather-axis.
// mpi.allgather always concatenates along the first dimension and
// there is no MPI operation that allows gathering along an arbitrary axis.
// Hence, if gather-axis != 0, we need to permute the output buffer
// accordingly.
LogicalResult
matchAndRewrite(AllGatherOp op, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
SymbolTableCollection symbolTableCollection;
FailureOr<GridOp> gridOp = checkGrid(op, symbolTableCollection);
if (failed(gridOp))
return failure();
ImplicitLocOpBuilder ib(op.getLoc(), rewriter);
Value input = getAsMemref(adaptor.getInput(), ib, true);
MemRefType inType = cast<MemRefType>(input.getType());
MemRefType outType = getMemrefType(cast<ShapedType>(op.getType()));
auto inputShape = inType.getShape();
auto outputShape = outType.getShape();
int64_t gatherAxis = adaptor.getGatherAxisAttr().getInt();
int64_t inputDimOnAxis = inputShape[gatherAxis];
int64_t outputDimOnAxis = outputShape[gatherAxis];
for (size_t i = 0; i < outputShape.size(); ++i)
if (outputShape[i] != inputShape[i] && i != (size_t)gatherAxis)
return op.emitError(
"Result and input shapes must match along non-gather axes.");
if (inputDimOnAxis == 0)
return op.emitError("Input size along the gather axis must be non-zero.");
if (inputDimOnAxis == 1) {
assert(outputDimOnAxis == inputDimOnAxis);
rewriter.replaceOp(op, adaptor.getInput());
return success();
}
if (outputDimOnAxis % inputDimOnAxis != 0)
return op.emitError("Result size along the gather axis must be an exact "
"multiple of the input size along the gather axis.");
if (!memref::isStaticShapeAndContiguousRowMajor(inType) ||
!memref::isStaticShapeAndContiguousRowMajor(outType))
return op.emitError("Input/result must be statically shaped memrefs in "
"contiguous row-major layout.");
// Get the right communicator.
Value comm = getComm(*gridOp, adaptor.getGridAxes(), ib);
Value nRanksV =
mpi::CommSizeOp::create(ib, ib.getI32Type(), comm).getSize();
nRanksV = arith::IndexCastOp::create(ib, ib.getIndexType(), nRanksV);
int64_t nRanks = outputDimOnAxis / inputDimOnAxis;
Value nRanksC = arith::ConstantIndexOp::create(ib, nRanks);
Value notError =
arith::CmpIOp::create(ib, arith::CmpIPredicate::eq, nRanksV, nRanksC);
cf::AssertOp::create(ib, notError,
"Expected number of ranks in the communicator to "
"match the output size along the gather axis divided "
"by the input size along the gather axis.");
// mpi.allgather always concatenates along the first dimension, so
// get a output buffer of shape {nRanks, dim0, ...}.
SmallVector<int64_t> gatherShape;
gatherShape.emplace_back(nRanks);
gatherShape.append(inputShape.begin(), inputShape.end());
auto gatherType = MemRefType::get(gatherShape, outType.getElementType());
Value finalOutput = memref::AllocOp::create(ib, gatherType);
// Create the MPI AllGather operation.
mpi::AllGatherOp::create(ib, TypeRange(), input, finalOutput, comm);
if (gatherAxis == 0) {
// If gather axis == 0, simply collapse the first 2 dims from {nRanks,
// dim0, ...} to {nRanks*dim0, ...}.
SmallVector<ReassociationIndices> reassociation;
reassociation.push_back({0, 1});
int64_t numGatherDims = gatherShape.size();
for (int64_t i = 2; i < numGatherDims; ++i)
reassociation.push_back({i});
finalOutput = memref::CollapseShapeOp::create(ib, outType, finalOutput,
reassociation);
// If the op's result is a tensor, cast it to a tensor.
if (isa<RankedTensorType>(op.getType()))
finalOutput = bufferization::ToTensorOp::create(ib, op.getType(),
finalOutput, true);
} else {
// 1. Enter tensor-land.
auto inType =
RankedTensorType::get(gatherShape, outType.getElementType());
finalOutput =
bufferization::ToTensorOp::create(ib, inType, finalOutput, true);
// 2. Permute the output buffer from {nRanks, dim0, ..., gatherAxis, ...}
// to {dim0, ..., nRanks, dim1,...}.
SmallVector<int64_t> outShapePermuted, permutation;
for (int i = 1; i <= gatherAxis; ++i) {
outShapePermuted.emplace_back(gatherShape[i]);
permutation.emplace_back(i);
}
outShapePermuted.emplace_back(gatherShape[0]);
permutation.emplace_back(0);
for (size_t i = gatherAxis + 1; i < gatherShape.size(); ++i) {
outShapePermuted.emplace_back(gatherShape[i]);
permutation.emplace_back(i);
}
Value permOutput = tensor::EmptyOp::create(ib, outShapePermuted,
outType.getElementType());
finalOutput =
linalg::TransposeOp::create(ib, finalOutput, permOutput, permutation)
->getResult(0);
// 3. Collapse the output buffer from {dim0, ..., nRanks, gatherAxis, ...}
// to {dim0, ..., nRanks*gatherAxis, ...}.
SmallVector<ReassociationIndices> reassociation;
for (int64_t i = 0; i < gatherAxis; ++i) {
reassociation.push_back({i});
}
reassociation.push_back({gatherAxis, gatherAxis + 1});
for (int64_t i = gatherAxis + 2; i < (int64_t)outShapePermuted.size();
++i) {
reassociation.push_back({i});
}
auto outTType =
RankedTensorType::get(outputShape, outType.getElementType());
finalOutput = tensor::CollapseShapeOp::create(ib, outTType, finalOutput,
reassociation);
// 4. Cast back to memref if needed.
if (isa<MemRefType>(op.getType()))
finalOutput =
bufferization::ToBufferOp::create(ib, outType, finalOutput, false);
}
rewriter.replaceOp(op, finalOutput);
return success();
}
};
struct ConvertUpdateHaloOp : public OpConversionPattern<UpdateHaloOp> {
using OpConversionPattern::OpConversionPattern;
LogicalResult
matchAndRewrite(UpdateHaloOp op, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
// The input/output memref is assumed to be in C memory order.
// Halos are exchanged as 2 blocks per dimension (one for each side: down
// and up). For each haloed dimension `d`, the exchanged blocks are
// expressed as multi-dimensional subviews. The subviews include potential
// halos of higher dimensions `dh > d`, no halos for the lower dimensions
// `dl < d` and for dimension `d` the currently exchanged halo only.
// By iterating form higher to lower dimensions this also updates the halos
// in the 'corners'.
// memref.subview is used to read and write the halo data from and to the
// local data. Because subviews and halos can have mixed dynamic and static
// shapes, OpFoldResults are used whenever possible.
auto haloSizes = getMixedValues(adaptor.getStaticHaloSizes(),
adaptor.getHaloSizes(), rewriter);
if (haloSizes.empty()) {
// no halos -> nothing to do
rewriter.replaceOp(op, adaptor.getDestination());
return success();
}
SymbolTableCollection symbolTableCollection;
Location loc = op.getLoc();
// convert a OpFoldResult into a Value
auto toValue = [&rewriter, &loc](OpFoldResult &v) -> Value {
if (auto value = dyn_cast<Value>(v))
return value;
return arith::ConstantOp::create(
rewriter, loc,
rewriter.getIndexAttr(
cast<IntegerAttr>(cast<Attribute>(v)).getInt()));
};
auto dest = adaptor.getDestination();
auto dstShape = cast<ShapedType>(dest.getType()).getShape();
Value array = dest;
if (isa<RankedTensorType>(array.getType())) {
// If the destination is a memref, we need to cast it to a tensor
auto mmemrefType = MemRefType::get(
dstShape, cast<ShapedType>(array.getType()).getElementType());
array =
bufferization::ToBufferOp::create(rewriter, loc, mmemrefType, array);
}
auto rank = cast<ShapedType>(array.getType()).getRank();
auto opSplitAxes = adaptor.getSplitAxes().getAxes();
auto grid = adaptor.getGrid();
auto gridOp = getGrid(op, symbolTableCollection);
// subviews need Index values
for (auto &sz : haloSizes) {
if (auto value = dyn_cast<Value>(sz))
sz = arith::IndexCastOp::create(rewriter, loc, rewriter.getIndexType(),
value)
.getResult();
}
// most of the offset/size/stride data is the same for all dims
SmallVector<OpFoldResult> offsets(rank, rewriter.getIndexAttr(0));
SmallVector<OpFoldResult> strides(rank, rewriter.getIndexAttr(1));
SmallVector<OpFoldResult> shape(rank), dimSizes(rank);
auto currHaloDim = -1; // halo sizes are provided for split dimensions only
// we need the actual shape to compute offsets and sizes
for (auto i = 0; i < rank; ++i) {
auto s = dstShape[i];
if (ShapedType::isDynamic(s))
shape[i] = memref::DimOp::create(rewriter, loc, array, s).getResult();
else
shape[i] = rewriter.getIndexAttr(s);
if ((size_t)i < opSplitAxes.size() && !opSplitAxes[i].empty()) {
++currHaloDim;
// the offsets for lower dim sstarts after their down halo
offsets[i] = haloSizes[currHaloDim * 2];
// prepare shape and offsets of highest dim's halo exchange
Value _haloSz = arith::AddIOp::create(
rewriter, loc, toValue(haloSizes[currHaloDim * 2]),
toValue(haloSizes[currHaloDim * 2 + 1]));
// the halo shape of lower dims exlude the halos
dimSizes[i] =
arith::SubIOp::create(rewriter, loc, toValue(shape[i]), _haloSz)
.getResult();
} else {
dimSizes[i] = shape[i];
}
}
auto tagAttr = rewriter.getI32IntegerAttr(91); // we just pick something
auto tag = arith::ConstantOp::create(rewriter, loc, tagAttr);
auto zeroAttr = rewriter.getI32IntegerAttr(0); // for detecting v<0
auto zero = arith::ConstantOp::create(rewriter, loc, zeroAttr);
SmallVector<Type> indexResultTypes(gridOp.getShape().size(),
rewriter.getIndexType());
auto myMultiIndex =
ProcessMultiIndexOp::create(rewriter, loc, indexResultTypes, grid)
.getResult();
// traverse all split axes from high to low dim
for (ssize_t dim = opSplitAxes.size() - 1; dim >= 0; --dim) {
auto splitAxes = opSplitAxes[dim];
if (splitAxes.empty())
continue;
assert(currHaloDim >= 0 && (size_t)currHaloDim < haloSizes.size() / 2);
// Get the linearized ids of the neighbors (down and up) for the
// given split
auto tmp = NeighborsLinearIndicesOp::create(rewriter, loc, grid,
myMultiIndex, splitAxes)
.getResults();
// MPI operates on i32...
Value neighbourIDs[2] = {
arith::IndexCastOp::create(rewriter, loc, rewriter.getI32Type(),
tmp[0]),
arith::IndexCastOp::create(rewriter, loc, rewriter.getI32Type(),
tmp[1])};
auto lowerRecvOffset = rewriter.getIndexAttr(0);
auto lowerSendOffset = toValue(haloSizes[currHaloDim * 2]);
auto upperRecvOffset =
arith::SubIOp::create(rewriter, loc, toValue(shape[dim]),
toValue(haloSizes[currHaloDim * 2 + 1]));
auto upperSendOffset = arith::SubIOp::create(
rewriter, loc, upperRecvOffset, toValue(haloSizes[currHaloDim * 2]));
Value commWorld = mpi::CommWorldOp::create(
rewriter, loc, mpi::CommType::get(op->getContext()));
// Make sure we send/recv in a way that does not lead to a dead-lock.
// The current approach is by far not optimal, this should be at least
// be a red-black pattern or using MPI_sendrecv.
// Also, buffers should be re-used.
// Still using temporary contiguous buffers for MPI communication...
// Still yielding a "serialized" communication pattern...
auto genSendRecv = [&](bool upperHalo) {
auto orgOffset = offsets[dim];
dimSizes[dim] = upperHalo ? haloSizes[currHaloDim * 2 + 1]
: haloSizes[currHaloDim * 2];
// Check if we need to send and/or receive
// Processes on the grid borders have only one neighbor
auto to = upperHalo ? neighbourIDs[0] : neighbourIDs[1];
auto from = upperHalo ? neighbourIDs[1] : neighbourIDs[0];
auto hasFrom = arith::CmpIOp::create(
rewriter, loc, arith::CmpIPredicate::sge, from, zero);
auto hasTo = arith::CmpIOp::create(rewriter, loc,
arith::CmpIPredicate::sge, to, zero);
auto buffer = memref::AllocOp::create(
rewriter, loc, dimSizes,
cast<ShapedType>(array.getType()).getElementType());
// if has neighbor: copy halo data from array to buffer and send
scf::IfOp::create(
rewriter, loc, hasTo, [&](OpBuilder &builder, Location loc) {
offsets[dim] = upperHalo ? OpFoldResult(lowerSendOffset)
: OpFoldResult(upperSendOffset);
auto subview = memref::SubViewOp::create(
builder, loc, array, offsets, dimSizes, strides);
memref::CopyOp::create(builder, loc, subview, buffer);
mpi::SendOp::create(builder, loc, TypeRange{}, buffer, tag, to,
commWorld);
scf::YieldOp::create(builder, loc);
});
// if has neighbor: receive halo data into buffer and copy to array
scf::IfOp::create(
rewriter, loc, hasFrom, [&](OpBuilder &builder, Location loc) {
offsets[dim] = upperHalo ? OpFoldResult(upperRecvOffset)
: OpFoldResult(lowerRecvOffset);
mpi::RecvOp::create(builder, loc, TypeRange{}, buffer, tag, from,
commWorld);
auto subview = memref::SubViewOp::create(
builder, loc, array, offsets, dimSizes, strides);
memref::CopyOp::create(builder, loc, buffer, subview);
scf::YieldOp::create(builder, loc);
});
memref::DeallocOp::create(rewriter, loc, buffer);
offsets[dim] = orgOffset;
};
auto doSendRecv = [&](int upOrDown) {
OpFoldResult &v = haloSizes[currHaloDim * 2 + upOrDown];
Value haloSz = dyn_cast<Value>(v);
if (!haloSz)
haloSz = arith::ConstantOp::create(
rewriter, loc,
rewriter.getI32IntegerAttr(
cast<IntegerAttr>(cast<Attribute>(v)).getInt()));
auto hasSize = arith::CmpIOp::create(
rewriter, loc, arith::CmpIPredicate::sgt, haloSz, zero);
scf::IfOp::create(rewriter, loc, hasSize,
[&](OpBuilder &builder, Location loc) {
genSendRecv(upOrDown > 0);
scf::YieldOp::create(builder, loc);
});
};
doSendRecv(0);
doSendRecv(1);
// the shape for lower dims include higher dims' halos
dimSizes[dim] = shape[dim];
// -> the offset for higher dims is always 0
offsets[dim] = rewriter.getIndexAttr(0);
// on to next halo
--currHaloDim;
}
if (isa<MemRefType>(op.getResult().getType())) {
rewriter.replaceOp(op, array);
} else {
assert(isa<RankedTensorType>(op.getResult().getType()));
rewriter.replaceOp(op, bufferization::ToTensorOp::create(
rewriter, loc, op.getResult().getType(), array,
/*restrict=*/true, /*writable=*/true));
}
return success();
}
};
struct ConvertShardToMPIPass
: public impl::ConvertShardToMPIPassBase<ConvertShardToMPIPass> {
using Base::Base;
/// Run the dialect converter on the module.
void runOnOperation() override {
auto *ctxt = &getContext();
RewritePatternSet patterns(ctxt);
ConversionTarget target(getContext());
// Define a type converter to convert shard::ShardingType,
// mostly for use in return operations.
TypeConverter typeConverter;
typeConverter.addConversion([](Type type) { return type; });
// convert shard::ShardingType to a tuple of RankedTensorTypes
typeConverter.addConversion(
[](ShardingType type,
SmallVectorImpl<Type> &results) -> std::optional<LogicalResult> {
auto i16 = IntegerType::get(type.getContext(), 16);
auto i64 = IntegerType::get(type.getContext(), 64);
std::array<int64_t, 2> shp = {ShapedType::kDynamic,
ShapedType::kDynamic};
results.emplace_back(RankedTensorType::get(shp, i16));
results.emplace_back(RankedTensorType::get(shp, i64)); // actually ?x2
results.emplace_back(RankedTensorType::get(shp, i64));
return success();
});
// To 'extract' components, a UnrealizedConversionCastOp is expected
// to define the input
typeConverter.addTargetMaterialization(
[&](OpBuilder &builder, TypeRange resultTypes, ValueRange inputs,
Location loc) {
// Expecting a single input.
if (inputs.size() != 1 || !isa<TupleType>(inputs[0].getType()))
return SmallVector<Value>();
auto castOp = inputs[0].getDefiningOp<UnrealizedConversionCastOp>();
// Expecting an UnrealizedConversionCastOp.
if (!castOp)
return SmallVector<Value>();
// Fill a vector with elements of the tuple/castOp.
SmallVector<Value> results;
for (auto oprnd : castOp.getInputs()) {
if (!isa<RankedTensorType>(oprnd.getType()))
return SmallVector<Value>();
results.emplace_back(oprnd);
}
return results;
});
// No shard dialect should left after conversion...
target.addIllegalDialect<shard::ShardDialect>();
// ...except the global GridOp. GridShapeOp which will get folded later.
target.addLegalOp<shard::GridOp, shard::GridShapeOp>();
// Allow all the stuff that our patterns will convert to
target.addLegalDialect<BuiltinDialect, mpi::MPIDialect, scf::SCFDialect,
arith::ArithDialect, tensor::TensorDialect,
bufferization::BufferizationDialect,
linalg::LinalgDialect, memref::MemRefDialect,
affine::AffineDialect, cf::ControlFlowDialect>();
// Make sure the function signature, calls etc. are legal
target.addDynamicallyLegalOp<func::FuncOp>([&](func::FuncOp op) {
return typeConverter.isSignatureLegal(op.getFunctionType());
});
target.addDynamicallyLegalOp<func::CallOp, func::ReturnOp>(
[&](Operation *op) { return typeConverter.isLegal(op); });
patterns.add<ConvertUpdateHaloOp, ConvertNeighborsLinearIndicesOp,
ConvertGetShardingOp, ConvertShardingOp, ConvertShardShapeOp,
ConvertAllGatherOp, ConvertAllReduceOp, ConvertReduceScatterOp,
ConvertProcessLinearIndexOp>(typeConverter, ctxt);
SymbolTableCollection stc;
populateProcessMultiIndexOpLoweringPatterns(patterns, stc);
populateAllSliceOpLoweringPatterns(patterns, stc);
populateFunctionOpInterfaceTypeConversionPattern<func::FuncOp>(
patterns, typeConverter);
populateCallOpTypeConversionPattern(patterns, typeConverter);
populateReturnOpTypeConversionPattern(patterns, typeConverter);
(void)applyPartialConversion(getOperation(), target, std::move(patterns));
// Folding patterns cannot be mixed with conversion patterns -> extra pass.
patterns.clear();
SymbolTableCollection symbolTableCollection;
mlir::shard::populateFoldingPatterns(patterns, symbolTableCollection);
(void)applyPatternsGreedily(getOperation(), std::move(patterns));
}
};
} // namespace