Skip to content

[mlir][shard,mpi] Allowing 2d-grids and simplifying lowering shard.all_gather - #180243

Merged
fschlimb merged 4 commits into
llvm:mainfrom
fschlimb:shardmpifixes
Feb 10, 2026
Merged

[mlir][shard,mpi] Allowing 2d-grids and simplifying lowering shard.all_gather#180243
fschlimb merged 4 commits into
llvm:mainfrom
fschlimb:shardmpifixes

Conversation

@fschlimb

@fschlimb fschlimb commented Feb 6, 2026

Copy link
Copy Markdown
Contributor
  • fixing incorrect assertion and related function name
  • MPI_comm_split is not pure
  • simplifying/standardizing permutation in all_gather

@llvmbot

llvmbot commented Feb 6, 2026

Copy link
Copy Markdown
Member

@llvm/pr-subscribers-mlir

Author: Frank Schlimbach (fschlimb)

Changes
  • fixing incorrect assertion and related function name
  • MPI_comm_split is not pure
  • simplifying/standardizing permutation in all_gather

Patch is 34.29 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/180243.diff

5 Files Affected:

  • (modified) mlir/include/mlir/Dialect/MPI/IR/MPIOps.td (+1-1)
  • (modified) mlir/lib/Conversion/ShardToMPI/ShardToMPI.cpp (+93-86)
  • (modified) mlir/lib/Dialect/Shard/Transforms/Partition.cpp (+20-37)
  • (modified) mlir/test/Conversion/ShardToMPI/convert-shard-to-mpi.mlir (+85-74)
  • (modified) mlir/test/Dialect/Shard/partition.mlir (+3-2)
diff --git a/mlir/include/mlir/Dialect/MPI/IR/MPIOps.td b/mlir/include/mlir/Dialect/MPI/IR/MPIOps.td
index d9e47ea3f6bfe..7e68b152fdf75 100644
--- a/mlir/include/mlir/Dialect/MPI/IR/MPIOps.td
+++ b/mlir/include/mlir/Dialect/MPI/IR/MPIOps.td
@@ -103,7 +103,7 @@ def MPI_CommSizeOp : MPI_Op<"comm_size", [Pure]> {
 // CommSplitOp
 //===----------------------------------------------------------------------===//
 
-def MPI_CommSplitOp : MPI_Op<"comm_split", [Pure]> {
+def MPI_CommSplitOp : MPI_Op<"comm_split"> {
   let summary = "Partition the group associated with the given communicator into "
                 "disjoint subgroups";
   let description = [{
diff --git a/mlir/lib/Conversion/ShardToMPI/ShardToMPI.cpp b/mlir/lib/Conversion/ShardToMPI/ShardToMPI.cpp
index c765ad5a579c8..2b77040236513 100644
--- a/mlir/lib/Conversion/ShardToMPI/ShardToMPI.cpp
+++ b/mlir/lib/Conversion/ShardToMPI/ShardToMPI.cpp
@@ -16,7 +16,7 @@
 #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/ControlFlow.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"
@@ -624,9 +624,8 @@ struct ConvertAllGatherOp : public CommOpPattern<AllGatherOp> {
   // 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 create a temporary buffer
-  // where we gather along the first dimension and then copy from that
-  // buffer to the final output along the specified gather-axis.
+  // Hence, if gather-axis != 0, we need to permute the output buffer
+  // accordingly.
 
   LogicalResult
   matchAndRewrite(AllGatherOp op, OpAdaptor adaptor,
@@ -635,8 +634,9 @@ struct ConvertAllGatherOp : public CommOpPattern<AllGatherOp> {
     FailureOr<GridOp> gridOp = checkGrid(op, symbolTableCollection);
     if (failed(gridOp))
       return failure();
-    ImplicitLocOpBuilder iBuilder(op.getLoc(), rewriter);
-    Value input = getAsMemref(adaptor.getInput(), iBuilder);
+
+    ImplicitLocOpBuilder ib(op.getLoc(), rewriter);
+    Value input = getAsMemref(adaptor.getInput(), ib);
     MemRefType inType = cast<MemRefType>(input.getType());
     if (!memref::isStaticShapeAndContiguousRowMajor(inType))
       return op.emitError(
@@ -645,94 +645,101 @@ struct ConvertAllGatherOp : public CommOpPattern<AllGatherOp> {
     if (!memref::isStaticShapeAndContiguousRowMajor(outType))
       return op.emitError(
           "Expected static shaped memref in contiguous row-major layout.");
-    int64_t gatherAxis = adaptor.getGatherAxisAttr().getInt();
-    auto ctx = op->getContext();
 
-    // Get the right communicator
-    Value comm = getComm(*gridOp, adaptor.getGridAxes(), iBuilder);
+    auto inputShape = cast<ShapedType>(adaptor.getInput().getType()).getShape();
+    auto outputShape = outType.getShape();
+    int64_t gatherAxis = adaptor.getGatherAxisAttr().getInt();
 
-    Value nRanks =
-        mpi::CommSizeOp::create(iBuilder, iBuilder.getI32Type(), comm)
-            .getSize();
-    nRanks =
-        arith::IndexCastOp::create(iBuilder, iBuilder.getIndexType(), nRanks);
+    // 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 = outputShape[gatherAxis] / inputShape[gatherAxis];
+    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.");
+    for (size_t i = 0; i < outputShape.size(); ++i)
+      assert(outputShape[i] == inputShape[i] || i == (size_t)gatherAxis);
+
+    // 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);
 
-    Value tmpOutput, gatherDimSz;
     if (gatherAxis == 0) {
-      tmpOutput = memref::AllocOp::create(iBuilder, outType);
-    } else {
-      // MPI's allgather always concatenates along the first dimension.
-      // Create a memref type for the output buffer with adjusted (expanded)
-      // shape.
-      SmallVector<int64_t> gatherShape(1, ShapedType::kDynamic);
-      llvm::append_range(gatherShape, outType.getShape());
-      gatherShape[gatherAxis + 1] = ShapedType::kDynamic;
-      MemRefType gatherType =
-          MemRefType::get(gatherShape, outType.getElementType());
-      gatherDimSz = arith::ConstantIndexOp::create(
-          iBuilder, outType.getDimSize(gatherAxis));
-      gatherDimSz = arith::DivSIOp::create(iBuilder, iBuilder.getIndexType(),
-                                           gatherDimSz, nRanks);
-      // Allocate output buffer
-      tmpOutput =
-          memref::AllocOp::create(iBuilder, gatherType, {nRanks, gatherDimSz});
-    }
-    // Create the MPI AllGather operation.
-    mpi::AllGatherOp::create(iBuilder, TypeRange(), input, tmpOutput, comm);
-
-    // If gather-axis!=0, copy from gathered buffer to output with the right
-    // layout.
-    Value finalOutput = tmpOutput;
-    if (gatherAxis != 0) {
-      int64_t nSrcDims = cast<ShapedType>(tmpOutput.getType()).getRank();
-      assert(nSrcDims == outType.getRank() + 1 &&
-             "Expected gathered type to have rank one more than output type.");
-
-      // Create affine map for copying from gathered buffer to output.
-      SmallVector<AffineExpr> dims;
-      dims.reserve(nSrcDims);
-      for (unsigned i = 0; i < nSrcDims; ++i)
-        dims.emplace_back(getAffineDimExpr(i, ctx));
-      AffineExpr s = getAffineSymbolExpr(0, ctx);
-      SmallVector<AffineExpr> results;
-      results.reserve(nSrcDims);
-      for (unsigned i = 0; i < nSrcDims - 1; ++i) {
-        if (i == gatherAxis)
-          results.emplace_back(dims[0] * s + dims[gatherAxis + 1]);
-        else
-          results.emplace_back(dims[i + 1]);
+      // If gather axis == 0, simply collapse the first 2 dims from {nRanks,
+      // dim0, ...} to {nRanks*dim0, ...}.
+      SmallVector<ReassociationIndices> reassociation;
+      reassociation.push_back({0, 1});
+      for (int64_t i = 2; i < (int64_t)gatherShape.size(); ++i) {
+        reassociation.push_back({i});
       }
-      auto affineMap = AffineMap::get(nSrcDims, /*symbols=*/1, results, ctx);
-
-      finalOutput = memref::AllocOp::create(iBuilder, outType);
-
-      // Now build a loop nest to copy from gathered buffer to finalOutput
-      // It would be nicer to just use a memref.transpose/collapse_shape op but
-      // these currently only support simpler cases.
-      Value zero = arith::ConstantIndexOp::create(iBuilder, 0);
-      SmallVector<Value> lbs(nSrcDims, zero);
-      SmallVector<Value> ubs;
-      for (int64_t d = 0; d < nSrcDims; ++d)
-        ubs.emplace_back(memref::DimOp::create(iBuilder, tmpOutput, d));
-      SmallVector<int64_t> steps(nSrcDims, 1);
-      auto emitCopy = [&](OpBuilder &builder, Location loc, ValueRange ivs) {
-        Value v = memref::LoadOp::create(iBuilder, tmpOutput, ivs);
-        // set symbol value
-        SmallVector<Value> ivss(ivs.begin(), ivs.end());
-        ivss.emplace_back(gatherDimSz);
-        affine::AffineStoreOp::create(iBuilder, v, finalOutput, affineMap,
-                                      ivss);
-      };
-      affine::buildAffineLoopNest(iBuilder, op->getLoc(), lbs, ubs, steps,
-                                  emitCopy);
+      finalOutput = memref::CollapseShapeOp::create(ib, outType, finalOutput,
+                                                    reassociation);
 
-      memref::DeallocOp::create(iBuilder, tmpOutput);
+      // 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);
     }
 
-    // If the destination is a tensor, cast it to a tensor
-    if (isa<RankedTensorType>(op.getType()))
-      finalOutput = bufferization::ToTensorOp::create(iBuilder, op.getType(),
-                                                      finalOutput, true);
     rewriter.replaceOp(op, finalOutput);
     return success();
   }
diff --git a/mlir/lib/Dialect/Shard/Transforms/Partition.cpp b/mlir/lib/Dialect/Shard/Transforms/Partition.cpp
index 62dc8f5917ab7..9f8dcd91e39d5 100644
--- a/mlir/lib/Dialect/Shard/Transforms/Partition.cpp
+++ b/mlir/lib/Dialect/Shard/Transforms/Partition.cpp
@@ -436,24 +436,32 @@ tryUpdateHaloInResharding(ImplicitLocOpBuilder &builder, GridOp grid,
                          targetSharding);
 }
 
-// Handles only resharding on a 1D shard.
 // Currently the sharded tensor axes must be exactly divisible by the single
-// grid axis size.
+// grid axis size.static TypedValue<ShapedType>
 static TypedValue<ShapedType>
-reshardOn1DGrid(ImplicitLocOpBuilder &builder, GridOp grid,
-                const Sharding &sourceSharding, const Sharding &targetSharding,
-                TypedValue<ShapedType> sourceUnshardedValue,
-                TypedValue<ShapedType> sourceShard) {
+reshard(ImplicitLocOpBuilder &builder, GridOp grid,
+        const Sharding &sourceSharding, const Sharding &targetSharding,
+        TypedValue<ShapedType> sourceUnshardedValue,
+        TypedValue<ShapedType> sourceShard) {
+  // If source and destination sharding are the same, no need to do anything.
+  if (sourceSharding == targetSharding || (isFullReplication(sourceSharding) &&
+                                           isFullReplication(targetSharding))) {
+    return sourceShard;
+  }
+
+  // Tries to handle the case where the resharding is needed because the halo
+  // sizes are different. Supports arbitrary grid dimensionality.
+  if (auto tryRes = tryUpdateHaloInResharding(
+          builder, grid, sourceSharding, targetSharding,
+          sourceUnshardedValue.getType(), sourceShard)) {
+    return std::get<0>(tryRes.value()); // targetShard
+  }
+
   assert(sourceShard.getType() ==
          shardShapedType(sourceUnshardedValue.getType(), grid, sourceSharding));
   [[maybe_unused]] ShapedType targetShardType =
       shardShapedType(sourceUnshardedValue.getType(), grid, targetSharding);
   assert(sourceShard.getType().getRank() == targetShardType.getRank());
-  assert(grid.getRank() == 1 && "Only 1D grides are currently supported.");
-
-  if (sourceSharding == targetSharding) {
-    return sourceShard;
-  }
 
   TypedValue<ShapedType> targetShard;
   Sharding actualTargetSharding;
@@ -475,38 +483,13 @@ reshardOn1DGrid(ImplicitLocOpBuilder &builder, GridOp grid,
       std::tie(targetShard, actualTargetSharding) = tryRes.value();
     }
   }
+
   assert(targetShard && "Did not find any pattern to apply.");
   assert(actualTargetSharding == targetSharding);
   assert(targetShard.getType() == targetShardType);
   return targetShard;
 }
 
-static TypedValue<ShapedType>
-reshard(ImplicitLocOpBuilder &builder, GridOp grid,
-        const Sharding &sourceSharding, const Sharding &targetSharding,
-        TypedValue<ShapedType> sourceUnshardedValue,
-        TypedValue<ShapedType> sourceShard) {
-  // If source and destination sharding are the same, no need to do anything.
-  if (sourceSharding == targetSharding || (isFullReplication(sourceSharding) &&
-                                           isFullReplication(targetSharding))) {
-    return sourceShard;
-  }
-
-  // Tries to handle the case where the resharding is needed because the halo
-  // sizes are different. Supports arbitrary grid dimensionality.
-  if (auto tryRes = tryUpdateHaloInResharding(
-          builder, grid, sourceSharding, targetSharding,
-          sourceUnshardedValue.getType(), sourceShard)) {
-    return std::get<0>(tryRes.value()); // targetShard
-  }
-
-  // Resort to handling only 1D grids since the general case is complicated if
-  // it needs to be communication efficient in terms of minimizing the data
-  // transfered between devices.
-  return reshardOn1DGrid(builder, grid, sourceSharding, targetSharding,
-                         sourceUnshardedValue, sourceShard);
-}
-
 TypedValue<ShapedType> reshard(OpBuilder &builder, GridOp grid, ShardOp source,
                                ShardOp target,
                                TypedValue<ShapedType> sourceShardValue) {
diff --git a/mlir/test/Conversion/ShardToMPI/convert-shard-to-mpi.mlir b/mlir/test/Conversion/ShardToMPI/convert-shard-to-mpi.mlir
index 4ac4a69dd5b18..fa0fa32f59ea6 100644
--- a/mlir/test/Conversion/ShardToMPI/convert-shard-to-mpi.mlir
+++ b/mlir/test/Conversion/ShardToMPI/convert-shard-to-mpi.mlir
@@ -148,35 +148,51 @@ module attributes { mpi.dlti = #dlti.map<"MPI:comm_world_rank" = 7> } {
     return %0 : memref<3x4xf64>
   }
 
+  // CHECK-LABEL: func @allgather_tensor_0
+  // CHECK-SAME: [[varg0:%.*]]: tensor<3x4xf32>
+  func.func @allgather_tensor_0(%arg0 : tensor<3x4xf32>) -> tensor<12x4xf32> {
+    // CHECK: [[vc1_i32:%.*]] = arith.constant 1 : i32
+    // CHECK: [[vc2_i32:%.*]] = arith.constant 2 : i32
+    // CHECK: [[vc4:%.*]] = arith.constant 4 : index
+    // CHECK: [[v0:%.*]] = bufferization.to_buffer [[varg0]] : tensor<3x4xf32> to memref<3x4xf32>
+    // CHECK: [[v1:%.*]] = mpi.comm_world : !mpi.comm
+    // CHECK: [[vnewcomm:%.*]] = mpi.comm_split([[v1]], [[vc2_i32]], [[vc1_i32]]) : !mpi.comm
+    // CHECK: [[vsize:%.*]] = mpi.comm_size([[vnewcomm]]) : i32
+    // CHECK: [[v2:%.*]] = arith.index_cast [[vsize]] : i32 to index
+    // CHECK: [[v3:%.*]] = arith.cmpi eq, [[v2]], [[vc4]] : index
+    // CHECK: cf.assert [[v3]]
+    // CHECK: [[valloc:%.*]] = memref.alloc() : memref<4x3x4xf32>
+    // CHECK: mpi.allgather([[v0]], [[valloc]], [[vnewcomm]]) : memref<3x4xf32>, memref<4x3x4xf32>
+    // CHECK: [[vcollapse_shape:%.*]] = memref.collapse_shape [[valloc]] {{\[\[}}0, 1], [2]] : memref<4x3x4xf32> into memref<12x4xf32>
+    // CHECK: [[v4:%.*]] = bufferization.to_tensor [[vcollapse_shape]] restrict : memref<12x4xf32> to tensor<12x4xf32>
+    %0 = shard.all_gather %arg0 on @grid0 grid_axes = [1] gather_axis = 0 : tensor<3x4xf32> -> tensor<12x4xf32>
+    // CHECK: return [[v4]] : tensor<12x4xf32>
+    return %0 : tensor<12x4xf32>
+  }
+
   // CHECK-LABEL: func @allgather_tensor
   func.func @allgather_tensor(
       // CHECK-SAME: [[varg0:%.*]]: tensor<3x4xf32>
       // CHECK-SAME: -> tensor<3x20xf32>
       %arg0 : tensor<3x4xf32>) -> tensor<3x20xf32> {
-    // CHECK-DAG: [[vc2_i32:%.*]] = arith.constant 2 : i32
-    // CHECK-DAG: [[vc1_i32:%.*]] = arith.constant 1 : i32
-    // CHECK-DAG: [[vc20:%.*]] = arith.constant 20 : index
+    // CHECK: [[vc2_i32:%.*]] = arith.constant 2 : i32
+    // CHECK: [[vc1_i32:%.*]] = arith.constant 1 : i32
+    // CHECK: [[vc5:%.*]] = arith.constant 5 : index
     // CHECK: [[v0:%.*]] = bufferization.to_buffer [[varg0]] : tensor<3x4xf32> to memref<3x4xf32>
     // CHECK: [[v1:%.*]] = mpi.comm_world : !mpi.comm
     // CHECK: [[vnewcomm:%.*]] = mpi.comm_split([[v1]], [[vc1_i32]], [[vc2_i32]]) : !mpi.comm
     // CHECK: [[vsize:%.*]] = mpi.comm_size([[vnewcomm]]) : i32
     // CHECK: [[v2:%.*]] = arith.index_cast [[vsize]] : i32 to index
-    // CHECK: [[v3:%.*]] = arith.divsi [[vc20]], [[v2]] : index
-    // CHECK: [[valloc:%.*]] = memref.alloc([[v2]], [[v3]]) : memref<?x3x?xf32>
-    // CHECK: mpi.allgather([[v0]], [[valloc]], [[vnewcomm]]) : memref<3x4xf32>, memref<?x3x?xf32>
-    // CHECK: [[valloc_0:%.*]] = memref.alloc() : memref<3x20xf32>
-    // CHECK: affine.for [[varg1:%.*]] = 0 to [[v2]] {
-      // CHECK: affine.for [[varg2:%.*]] = 0 to 3 {
-        // CHECK: affine.for [[varg3:%.*]] = 0 to [[v3]] {
-          // CHECK: [[v5:%.*]] = memref.load [[valloc]][[[varg1]], [[varg2]], [[varg3]]] : memref<?x3x?xf32>
-          // CHECK: affine.store [[v5]], [[valloc_0]][[[varg2]], [[varg1]] * symbol([[v3]]) + [[varg3]]] : memref<3x20xf32>
-        // CHECK: }
-      // CHECK: }
-    // CHECK: }
-    // CHECK: memref.dealloc [[valloc]] : memref<?x3x?xf32>
-    // CHECK: [[v4:%.*]] = bufferization.to_tensor [[valloc_0]] restrict : memref<3x20xf32> to tensor<3x20xf32>
+    // CHECK: [[v3:%.*]] = arith.cmpi eq, [[v2]], [[vc5]] : index
+    // CHECK: cf.assert [[v3]]
+    // CHECK: [[valloc:%.*]] = memref.alloc() : memref<5x3x4xf32>
+    // CHECK: mpi.allgather([[v0]], [[valloc]], [[vnewcomm]]) : memref<3x4xf32>, memref<5x3x4xf32>
+    // CHECK: [[v4:%.*]] = bufferization.to_tensor [[valloc]] restrict : memref<5x3x4xf32> to tensor<5x3x4xf32>
+    // CHECK: [[v5:%.*]] = tensor.empty() : tensor<3x5x4xf32>
+    // CHECK: [[vtransposed:%.*]] = linalg.transpose ins([[v4]] : tensor<5x3x4xf32>) outs([[v5]] : tensor<3x5x4xf32>) permutation = [1, 0, 2] 
+    // CHECK: [[vcollapsed:%.*]] = tensor.collapse_shape [[vtransposed]] {{\[\[}}0], [1, 2]] : tensor<3x5x4xf32> into tensor<3x20xf32>
     %0 = shard.all_gather %arg0 on @grid0 grid_axes = [2] gather_axis = 1 : tensor<3x4xf32> -> tensor<3x20xf32>
-    // CHECK: return [[v4]] : tensor<3x20xf32>
+    // CHECK: return [[vcollapsed]] : tensor<3x20xf32>
     return %0 : tensor<3x20xf32>
   }
 
@@ -185,28 +201,24 @@ module attributes { mpi.dlti = #dlti.map<"MPI:comm_world_rank" = 7> } {
       // CHECK-SAME: [[varg0:%.*]]: memref<3x4xf32>
       // CHECK-SAME: -> memref<3x20xf32>
       %arg0 : memref<3x4xf32>) -> memref<3x20xf32> {
-    // CHECK-DAG: [[vc1_i32:%.*]] = arith...
[truncated]

@fschlimb
fschlimb requested a review from adam-smnk February 6, 2026 17:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR expands Shard/MPI support to better handle multi-dimensional grids, fixes an incorrect purity annotation for mpi.comm_split, and simplifies shard.all_gather lowering by standardizing the transpose/collapse-based permutation.

Changes:

  • Add/adjust MLIR tests to cover 2D grids and the new all_gather lowering shape/permutation strategy.
  • Update shard partitioning/resharding logic to remove a hard 1D-grid restriction and reuse halo-update resharding when possible.
  • Revise shard.all_gather lowering to use linalg.transpose + collapse_shape and add an explicit communicator-size assertion.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
mlir/test/Dialect/Shard/partition.mlir Adds a 2D grid and renames a test function label to reflect semantics
mlir/test/Conversion/ShardToMPI/convert-shard-to-mpi.mlir Updates/extends FileCheck coverage for the simplified all_gather lowering
mlir/lib/Dialect/Shard/Transforms/Partition.cpp Refactors resharding entrypoint and allows non-1D grids (notably for halo-only changes)
mlir/lib/Conversion/ShardToMPI/ShardToMPI.cpp Simplifies all_gather lowering via transpose/collapse and adds communicator-size assert
mlir/include/mlir/Dialect/MPI/IR/MPIOps.td Removes [Pure] from mpi.comm_split since it is not pure

Comment thread mlir/lib/Dialect/Shard/Transforms/Partition.cpp Outdated
Comment thread mlir/lib/Conversion/ShardToMPI/ShardToMPI.cpp Outdated
Comment on lines +696 to +704
// 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;

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

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

For the memref-to-memref case (op.getType() is MemRefType), this lowering goes memref → tensor (to_tensor) → tensor transforms → memref (to_buffer). That round-trip can introduce unnecessary materializations/copies compared to staying in memref-land (e.g., memref.transpose + memref.collapse_shape) when shapes are static/contiguous (which you already require). Consider using a memref-only path when the result is a memref, and reserve tensor-land only for tensor results (or when memref ops can’t express the needed permutation).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think Copilot is on to something: could you avoid going back to tensors and just do the same transpose and collapse on memrefs?

Comment on lines +737 to +740
// 4. Cast back to memref if needed.
if (isa<MemRefType>(op.getType()))
finalOutput =
bufferization::ToBufferOp::create(ib, outType, finalOutput);

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

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

For the memref-to-memref case (op.getType() is MemRefType), this lowering goes memref → tensor (to_tensor) → tensor transforms → memref (to_buffer). That round-trip can introduce unnecessary materializations/copies compared to staying in memref-land (e.g., memref.transpose + memref.collapse_shape) when shapes are static/contiguous (which you already require). Consider using a memref-only path when the result is a memref, and reserve tensor-land only for tensor results (or when memref ops can’t express the needed permutation).

Copilot uses AI. Check for mistakes.
Comment thread mlir/test/Conversion/ShardToMPI/convert-shard-to-mpi.mlir Outdated
Comment thread mlir/test/Conversion/ShardToMPI/convert-shard-to-mpi.mlir Outdated

@rolfmorel rolfmorel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The changes to the tests make sense to me!

The transform changes I skimmed as I am not so familiar with this lowering path/involved dialects.

The one thing that took my notice is what Copilot (also) points out: the going to buffers and then back to tensors and then back to buffers and then back to tensors. Further lowering of that is likely to not look so pretty. Might it be possible to just do the transpose and collapse on the memrefs "in the middle"?

Comment thread mlir/lib/Conversion/ShardToMPI/ShardToMPI.cpp Outdated
Comment on lines +696 to +704
// 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think Copilot is on to something: could you avoid going back to tensors and just do the same transpose and collapse on memrefs?

@fschlimb

fschlimb commented Feb 10, 2026

Copy link
Copy Markdown
Contributor Author

@rolfmorel Thanks for your review.

The changes to the tests make sense to me!

The transform changes I skimmed as I am not so familiar with this lowering path/involved dialects.

The one thing that took my notice is what Copilot (also) points out: the going to buffers and then back to tensors and then back to buffers and then back to tensors. Further lowering of that is likely to not look so pretty. Might it be possible to just do the transpose and collapse on the memrefs "in the middle"?

The only thing that might not pretty is the pipeline itself. The resulting code should be comparable to a direct memref-based formulation.

There is some unclarity about when the ShardToMPI should be applied. In theory, it could be called after bufferization and so it would not deal with any tensor input/output. The drawback would be that cases like this would not offer tensor-level optimizations. Maybe this is another motivation for separating the communication primitives from the shard dialect.

MPI has buffer-semantics, so there is no way around going to buffer-land before calling MPI.

Right now, the shard-dialect does not support oneshot-bufferization, so it can only be applied before bufferization. Until that's solved, this seems to be the best tradeoff, since we only go to buffers to call MPI. Before and after we are in tensor-land.

The worst case you are referring to can only happen if the compiler pipeline bypasses tensor land. For shard this is not possible if it is used normally (e.g. more than plain communication operations).

Co-authored-by: Rolf Morel <rolfmorel@gmail.com>
Comment thread mlir/test/Conversion/ShardToMPI/convert-shard-to-mpi.mlir Outdated
@fschlimb
fschlimb merged commit a6929f7 into llvm:main Feb 10, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants