blazingly faster
Validate Operations / validate-operations (push) Waiting to run

This commit is contained in:
NiccoloN
2026-07-19 09:59:49 +02:00
parent 5f42da36ae
commit ab54243fda
76 changed files with 4363 additions and 4323 deletions
+1 -1
View File
@@ -11,4 +11,4 @@ build_*
compile.sh
pimcomp_utils/*
**/__*
**/__pycache__/
+5 -7
View File
@@ -99,15 +99,13 @@ Pass these to `onnx-mlir` when compiling for PIM:
- `--core-count=<N>` - required positive core count for PIM compilation.
- `--crossbar-size=<N>` - crossbar width/height. Default in code is `2`.
- `--crossbar-count=<N>` - crossbars per core. Default in code is `256`.
- `--pim-merge-scheduler=peft` - merge scheduler. `peft` is the only accepted
value in the current code.
- `--pim-only-codegen` - assume input is already bufferized PIM IR and only run
the codegen tail.
- `--pim-emit-json` - also emit `core_*.json` instruction files alongside
`core_*.pim`.
- `--pim-export-spatial-dataflow=<none|spatial1|spatial2|spatial3|all>` - control Spatial
dataflow CSV reports. The default `all` emits graph, scheduled, and realized
snapshots under `reports/`.
- `--pim-export-spatial-dataflow=<none|spatial1|spatial2|spatial3|spatial4|all>` - control Spatial
dataflow CSV reports for the graph, trivially merged graph, scheduled, and
realized snapshots under `reports/`.
- `--use-experimental-conv-impl` - use the alternate convolution lowering.
- `--ignore-concat-error` - soft-fail a ConcatOp corner case.
@@ -170,8 +168,8 @@ Each validation run writes artifacts in the model workspace, for example under
- `simulation/out.bin` - raw simulator output used for comparison.
The compiler currently dumps dialect snapshots such as `spatial0.mlir`,
`spatial1_graph.mlir`, `spatial2_scheduled_no_comm.mlir`,
`spatial3_scheduled.mlir`, `pim0.mlir`, `pim1_buff.mlir`,
`spatial1_graph.mlir`, `spatial2_trivial_merged.mlir`,
`spatial3_scheduled_no_comm.mlir`, `spatial4_scheduled.mlir`, `pim0.mlir`, `pim1_buff.mlir`,
`pim2_coalesced.mlir`, and `pim3_folded.mlir` when an output directory is
available.
+4
View File
@@ -0,0 +1,4 @@
colorama>=0.4.6,<1
numpy>=1.26.4,<3
onnx>=1.17,<2
-e ./tools/raptor_graph_explorer[dev]
+1
View File
@@ -10,6 +10,7 @@ add_pim_library(OMPimCommon
IR/ShapeUtils.cpp
IR/ShapingUtils.cpp
IR/StaticIntSequence.cpp
IR/StaticIntGrid.cpp
IR/SubviewUtils.cpp
IR/TensorSliceUtils.cpp
IR/WeightUtils.cpp
+7
View File
@@ -1,3 +1,4 @@
#include "mlir/Dialect/Affine/IR/AffineOps.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/MemRef/IR/MemRef.h"
#include "mlir/Dialect/SCF/IR/SCF.h"
@@ -7,6 +8,7 @@
#include <limits>
#include "src/Accelerators/PIM/Common/IR/AddressAnalysis.hpp"
#include "src/Accelerators/PIM/Common/IR/AffineUtils.hpp"
#include "src/Accelerators/PIM/Common/IR/ShapeUtils.hpp"
#include "src/Accelerators/PIM/Dialect/Pim/PimOps.hpp"
@@ -391,6 +393,11 @@ llvm::FailureOr<int64_t> resolveIndexValueImpl(mlir::Value value, const StaticVa
if (!definingOp)
return mlir::failure();
if (auto affineApplyOp = mlir::dyn_cast<mlir::affine::AffineApplyOp>(definingOp))
return evaluateAffineApply(affineApplyOp, [&](mlir::Value operand) {
return resolveIndexValueImpl(operand, knowledge);
});
if (auto indexCastOp = mlir::dyn_cast<mlir::arith::IndexCastOp>(definingOp))
return resolveIndexValueImpl(indexCastOp.getIn(), knowledge);
+3 -1
View File
@@ -1,3 +1,4 @@
#include "mlir/Dialect/Affine/IR/AffineOps.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/MemRef/IR/MemRef.h"
#include "mlir/Dialect/SCF/IR/SCF.h"
@@ -10,7 +11,8 @@
namespace onnx_mlir {
bool isCoreStaticAddressOp(mlir::Operation* op) {
if (mlir::isa<mlir::arith::ConstantOp,
if (mlir::isa<mlir::affine::AffineApplyOp,
mlir::arith::ConstantOp,
mlir::arith::AddIOp,
mlir::arith::SubIOp,
mlir::arith::MulIOp,
+223
View File
@@ -0,0 +1,223 @@
#include "StaticIntGrid.hpp"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "AffineUtils.hpp"
#include "ConstantUtils.hpp"
#include <algorithm>
#include <limits>
using namespace mlir;
namespace onnx_mlir {
namespace {
static std::optional<size_t> cellCount(size_t rows, size_t columns) {
if (!rows || !columns ||
rows > static_cast<size_t>(std::numeric_limits<int64_t>::max()) / columns)
return std::nullopt;
return rows * columns;
}
static bool checkedFlatIndex(
size_t row, size_t columns, size_t column, size_t &flat) {
bool overflow;
flat = llvm::SaturatingMultiplyAdd(row, columns, column, &overflow);
return !overflow &&
flat <= static_cast<size_t>(std::numeric_limits<int64_t>::max());
}
static bool affineValue(int64_t base, int64_t rowStep, int64_t columnStep,
size_t row, size_t column, int64_t &result) {
if (row > static_cast<size_t>(std::numeric_limits<int64_t>::max()) ||
column > static_cast<size_t>(std::numeric_limits<int64_t>::max()))
return false;
int64_t rowValue, columnValue;
return !llvm::MulOverflow(rowStep, static_cast<int64_t>(row), rowValue)
&& !llvm::MulOverflow(columnStep, static_cast<int64_t>(column),
columnValue)
&& !llvm::AddOverflow(base, rowValue, result)
&& !llvm::AddOverflow(result, columnValue, result);
}
} // namespace
FailureOr<StaticIntGrid> StaticIntGrid::fromSequences(
ArrayRef<StaticIntSequence> input, bool columnsInput,
int64_t sparseBase) {
if (input.empty() || !input.front().size())
return failure();
size_t rowCount = columnsInput ? input.front().size() : input.size();
size_t columnCount = columnsInput ? input.size() : input.front().size();
auto cells = cellCount(rowCount, columnCount);
if (!cells ||
llvm::any_of(input, [&](const StaticIntSequence &sequence) {
return sequence.size() != input.front().size();
}))
return failure();
StaticIntGrid result(rowCount, columnCount, input.front().valueAt(0));
if (llvm::all_equal(input)) {
if (input.front().getKind() == StaticIntSequenceKind::Uniform)
return result;
result.kind = columnsInput ? Kind::ActionOnly : Kind::LaneOnly;
result.values = input.front();
return result;
}
SmallVector<int64_t> outerBases;
for (const StaticIntSequence &sequence : input)
outerBases.push_back(sequence.valueAt(0));
if (llvm::all_of(input, [](const StaticIntSequence &sequence) {
return sequence.getKind() == StaticIntSequenceKind::Uniform;
})) {
result.kind = columnsInput ? Kind::LaneOnly : Kind::ActionOnly;
result.values = StaticIntSequence::fromValues(outerBases);
return result;
}
auto innerStep = input.front().getAffineStep();
StaticIntSequence bases = StaticIntSequence::fromValues(outerBases);
auto outerStep = bases.getAffineStep();
if (innerStep && outerStep &&
llvm::all_of(input, [&](const StaticIntSequence &sequence) {
return sequence.getAffineStep() == innerStep;
}))
return affine2D(result.base,
columnsInput ? *innerStep : *outerStep,
columnsInput ? *outerStep : *innerStep, rowCount, columnCount);
SmallVector<int64_t> values;
values.reserve(*cells);
for (size_t row = 0; row < rowCount; ++row)
for (size_t column = 0; column < columnCount; ++column)
values.push_back(columnsInput ? input[column].valueAt(row)
: input[row].valueAt(column));
result.values = StaticIntSequence::fromValues(values);
for (size_t index = 0; index < *cells; ++index)
if (values[index] != sparseBase)
result.overrideKeys.push_back(static_cast<int64_t>(index));
if (result.overrideKeys.size() <= *cells / 4) {
result.kind = Kind::SparseLaneOverrides;
result.base = sparseBase;
} else {
result.kind = Kind::Dense;
result.overrideKeys.clear();
}
return result;
}
FailureOr<StaticIntGrid> StaticIntGrid::fromRows(
ArrayRef<StaticIntSequence> rows) {
if (rows.empty() || !rows.front().size())
return failure();
return fromSequences(rows, false, rows.front().valueAt(0));
}
FailureOr<StaticIntGrid> StaticIntGrid::fromColumns(
size_t rowCount, ArrayRef<StaticIntSequence> columnSequences,
int64_t defaultValue) {
if (!cellCount(rowCount, columnSequences.size()))
return failure();
SmallVector<StaticIntSequence> padded;
padded.reserve(columnSequences.size());
for (const StaticIntSequence &sequence : columnSequences) {
if (sequence.size() > rowCount)
return failure();
if (sequence.size() == rowCount) {
padded.push_back(sequence);
continue;
}
SmallVector<int64_t> values(rowCount, defaultValue);
for (size_t row = 0; row < sequence.size(); ++row)
values[row] = sequence.valueAt(row);
padded.push_back(StaticIntSequence::fromValues(values));
}
return fromSequences(padded, true, defaultValue);
}
FailureOr<StaticIntGrid> StaticIntGrid::affine2D(
int64_t base, int64_t rowStep, int64_t columnStep,
size_t rows, size_t columns) {
int64_t last;
if (!cellCount(rows, columns) ||
!affineValue(base, rowStep, columnStep, rows - 1, columns - 1, last))
return failure();
StaticIntGrid result(rows, columns, base);
result.kind = rowStep || columnStep ? Kind::Affine : Kind::Uniform;
result.rowStep = rowStep;
result.columnStep = columnStep;
return result;
}
FailureOr<StaticIntGrid> StaticIntGrid::laneIntervals(
size_t columns, ArrayRef<std::pair<size_t, size_t>> intervals,
int64_t insideValue, int64_t outsideValue) {
if (!columns)
return failure();
SmallVector<int64_t> values(columns, outsideValue);
for (auto [begin, end] : intervals) {
if (begin > end || end > columns)
return failure();
std::fill(values.begin() + begin, values.begin() + end, insideValue);
}
StaticIntSequence row = StaticIntSequence::fromValues(values);
return fromRows(ArrayRef<StaticIntSequence>(row));
}
int64_t StaticIntGrid::valueAt(size_t row, size_t column) const {
assert(row < rows && column < columns);
if (kind == Kind::Uniform)
return base;
if (kind == Kind::ActionOnly)
return values->valueAt(row);
if (kind == Kind::LaneOnly)
return values->valueAt(column);
if (kind == Kind::Affine) {
int64_t result;
bool valid = affineValue(base, rowStep, columnStep, row, column, result);
assert(valid);
return result;
}
size_t flat;
bool valid = checkedFlatIndex(row, columns, column, flat);
assert(valid);
if (kind == Kind::SparseLaneOverrides) {
auto found = llvm::lower_bound(overrideKeys, static_cast<int64_t>(flat));
if (found == overrideKeys.end() || *found != static_cast<int64_t>(flat))
return base;
}
assert(kind == Kind::Dense || kind == Kind::SparseLaneOverrides);
return values->valueAt(flat);
}
Value StaticIntGrid::emitLookup(Value row, Value column,
Operation *constantAnchor,
ConstantPool &constants, OpBuilder &builder,
Location loc) const {
if (kind == Kind::Uniform)
return constants.getIndex(base);
if (kind == Kind::ActionOnly || kind == Kind::LaneOnly)
return emitStaticIntLookup(
*values, kind == Kind::ActionOnly ? row : column,
constantAnchor, constants, builder, loc);
Value flat = affineMulConst(builder, loc, row, columns, constantAnchor);
flat = arith::AddIOp::create(builder, loc, flat, column);
if (kind == Kind::Affine) {
Value rowValue = affineMulConst(
builder, loc, row, rowStep, constantAnchor);
Value columnValue = affineMulConst(
builder, loc, column, columnStep, constantAnchor);
Value result = arith::AddIOp::create(builder, loc, rowValue, columnValue);
return affineAddConst(builder, loc, result, base, constantAnchor);
}
return emitStaticIntLookup(
*values, flat, constantAnchor, constants, builder, loc);
}
OpFoldResult StaticIntGrid::emitFoldedLookup(
Value row, Value column, Operation *constantAnchor,
ConstantPool &constants, OpBuilder &builder, Location loc) const {
return kind == Kind::Uniform
? OpFoldResult(builder.getIndexAttr(base))
: OpFoldResult(emitLookup(
row, column, constantAnchor, constants, builder, loc));
}
} // namespace onnx_mlir
+61
View File
@@ -0,0 +1,61 @@
#pragma once
#include "StaticIntSequence.hpp"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/SmallVector.h"
#include <utility>
namespace onnx_mlir {
class ConstantPool;
class StaticIntGrid {
public:
static mlir::FailureOr<StaticIntGrid> fromColumns(
size_t rows, llvm::ArrayRef<StaticIntSequence> columns,
int64_t defaultValue);
static mlir::FailureOr<StaticIntGrid> fromRows(
llvm::ArrayRef<StaticIntSequence> rows);
static mlir::FailureOr<StaticIntGrid> affine2D(
int64_t base, int64_t rowStep, int64_t columnStep,
size_t rows, size_t columns);
static mlir::FailureOr<StaticIntGrid> laneIntervals(
size_t columns,
llvm::ArrayRef<std::pair<size_t, size_t>> intervals,
int64_t insideValue, int64_t outsideValue);
int64_t valueAt(size_t row, size_t column) const;
mlir::Value emitLookup(mlir::Value row, mlir::Value column,
mlir::Operation *constantAnchor,
ConstantPool &constants, mlir::OpBuilder &builder,
mlir::Location loc) const;
mlir::OpFoldResult emitFoldedLookup(
mlir::Value row, mlir::Value column, mlir::Operation *constantAnchor,
ConstantPool &constants, mlir::OpBuilder &builder,
mlir::Location loc) const;
private:
enum class Kind { Uniform, ActionOnly, LaneOnly, Affine,
SparseLaneOverrides, Dense };
StaticIntGrid(size_t rows, size_t columns, int64_t base)
: rows(rows), columns(columns), base(base) {}
static mlir::FailureOr<StaticIntGrid> fromSequences(
llvm::ArrayRef<StaticIntSequence> sequences, bool columns,
int64_t sparseBase);
Kind kind = Kind::Uniform;
size_t rows = 0;
size_t columns = 0;
int64_t base = 0;
int64_t rowStep = 0;
int64_t columnStep = 0;
llvm::SmallVector<int64_t> overrideKeys;
std::optional<StaticIntSequence> values;
};
} // namespace onnx_mlir
+6
View File
@@ -36,6 +36,12 @@ public:
StaticIntSequence slice(size_t begin, size_t count) const;
StaticIntSequence remap(llvm::ArrayRef<unsigned> indices) const;
StaticIntSequenceKind getKind() const { return kind; }
std::optional<int64_t> getAffineStep() const {
if (kind == StaticIntSequenceKind::Uniform)
return 0;
return kind == StaticIntSequenceKind::Affine
? std::optional<int64_t>(step) : std::nullopt;
}
bool operator==(const StaticIntSequence& other) const;
llvm::hash_code hash() const;
+4 -3
View File
@@ -22,7 +22,7 @@ Value extractAxisSlice(
.getResult();
}
Value extractStaticSliceOrIdentity(RewriterBase& rewriter,
Value extractStaticSliceOrIdentity(OpBuilder& rewriter,
Location loc,
Value source,
RankedTensorType resultType,
@@ -52,7 +52,8 @@ Value extractStaticSliceOrIdentity(RewriterBase& rewriter,
if (isIdentitySlice)
return source;
return tensor::ExtractSliceOp::create(rewriter, loc, resultType, source, offsets, sizes, strides).getResult();
return rewriter.createOrFold<tensor::ExtractSliceOp>(
loc, resultType, source, offsets, sizes, strides);
}
Value insertStaticSlice(
@@ -68,7 +69,7 @@ Value insertStaticSlice(
.getResult();
}
Value extractMixedSliceOrIdentity(RewriterBase &rewriter,
Value extractMixedSliceOrIdentity(OpBuilder &rewriter,
Location loc,
Value source,
RankedTensorType resultType,
+2 -2
View File
@@ -17,7 +17,7 @@ struct MixedSliceGeometry {
mlir::Value extractAxisSlice(
mlir::PatternRewriter& rewriter, mlir::Location loc, mlir::Value source, int64_t axis, int64_t offset, int64_t size);
mlir::Value extractStaticSliceOrIdentity(mlir::RewriterBase& rewriter,
mlir::Value extractStaticSliceOrIdentity(mlir::OpBuilder& rewriter,
mlir::Location loc,
mlir::Value source,
mlir::RankedTensorType resultType,
@@ -31,7 +31,7 @@ mlir::Value insertStaticSlice(mlir::PatternRewriter& rewriter,
mlir::Value dest,
llvm::ArrayRef<mlir::OpFoldResult> offsets);
mlir::Value extractMixedSliceOrIdentity(mlir::RewriterBase &rewriter,
mlir::Value extractMixedSliceOrIdentity(mlir::OpBuilder &rewriter,
mlir::Location loc,
mlir::Value source,
mlir::RankedTensorType resultType,
+1
View File
@@ -26,6 +26,7 @@ add_pim_library(OMPimCompilerUtils
${PIM_COMPILER_INCLUDE_DIRS}
LINK_LIBS PUBLIC
MLIRAffineToStandard
OMPimCompilerOptions
OMPimCommon
OMPimBufferization
+4 -9
View File
@@ -15,13 +15,6 @@ llvm::cl::opt<PimEmissionTargetType> pimEmissionTarget(
llvm::cl::init(EmitPimCodegen),
llvm::cl::cat(OnnxMlirOptions));
llvm::cl::opt<PimMergeSchedulerType>
pimMergeScheduler("pim-merge-scheduler",
llvm::cl::desc("Scheduler used by the Spatial merge-compute-nodes pass"),
llvm::cl::values(clEnumValN(MergeSchedulerPeft, "peft", "Use PEFT scheduling")),
llvm::cl::init(MergeSchedulerPeft),
llvm::cl::cat(OnnxMlirOptions));
llvm::cl::opt<PimMemoryReportLevel> pimMemoryReport(
"pim-memory-report",
llvm::cl::desc("Emit a human-readable PIM memory planning report"),
@@ -64,9 +57,11 @@ llvm::cl::opt<PimSpatialDataflowExportType> pimExportSpatialDataflow(
llvm::cl::values(
clEnumValN(SpatialDataflowExportSpatial1, "spatial1", "Emit spatial1 graph dataflow CSV reports")),
llvm::cl::values(
clEnumValN(SpatialDataflowExportSpatial2, "spatial2", "Emit spatial2 scheduled dataflow CSV reports")),
clEnumValN(SpatialDataflowExportSpatial2, "spatial2", "Emit spatial2 trivially merged graph dataflow CSV reports")),
llvm::cl::values(
clEnumValN(SpatialDataflowExportSpatial3, "spatial3", "Emit spatial3 realized dataflow CSV reports")),
clEnumValN(SpatialDataflowExportSpatial3, "spatial3", "Emit spatial3 scheduled dataflow CSV reports")),
llvm::cl::values(
clEnumValN(SpatialDataflowExportSpatial4, "spatial4", "Emit spatial4 realized dataflow CSV reports")),
llvm::cl::values(clEnumValN(SpatialDataflowExportAll, "all", "Emit all Spatial dataflow CSV reports")),
llvm::cl::init(SpatialDataflowExportNone),
llvm::cl::cat(OnnxMlirOptions));
+2 -6
View File
@@ -20,10 +20,6 @@ typedef enum {
EmitPimCodegen = 3
} PimEmissionTargetType;
typedef enum {
MergeSchedulerPeft = 0,
} PimMergeSchedulerType;
typedef enum {
PimMemoryReportNone = 0,
PimMemoryReportSummary = 1,
@@ -47,12 +43,12 @@ typedef enum {
SpatialDataflowExportSpatial1 = 1,
SpatialDataflowExportSpatial2 = 2,
SpatialDataflowExportSpatial3 = 3,
SpatialDataflowExportAll = 4,
SpatialDataflowExportSpatial4 = 4,
SpatialDataflowExportAll = 5,
} PimSpatialDataflowExportType;
extern llvm::cl::OptionCategory OnnxMlirOptions;
extern llvm::cl::opt<PimEmissionTargetType> pimEmissionTarget;
extern llvm::cl::opt<PimMergeSchedulerType> pimMergeScheduler;
extern llvm::cl::opt<PimMemoryReportLevel> pimMemoryReport;
extern llvm::cl::opt<PimConvLoweringType> pimConvLowering;
extern llvm::cl::opt<PimSpatialDataflowExportType> pimExportSpatialDataflow;
+3
View File
@@ -1,3 +1,4 @@
#include "mlir/Conversion/AffineToStandard/AffineToStandard.h"
#include "mlir/Transforms/Passes.h"
#include "src/Accelerators/PIM/Compiler/PimCompilerOptions.hpp"
@@ -31,6 +32,7 @@ void addPassesPim(OwningOpRef<ModuleOp>& module,
pm.addPass(createONNXToSpatialPass());
pm.addPass(createSpatialLayoutPlanningPass());
pm.addPass(createLowerSpatialPlansPass());
pm.addPass(createTrivialGraphComputeMergePass());
pm.addPass(createMergeComputeNodesPass());
pm.addPass(createMessagePass("Onnx lowered to Spatial"));
}
@@ -46,6 +48,7 @@ void addPassesPim(OwningOpRef<ModuleOp>& module,
}
if (pimEmissionTarget >= EmitPimCodegen) {
pm.addPass(mlir::createLowerAffinePass());
pm.addPass(createPimHostConstantFoldingPass());
pm.addPass(createMessagePass("Pim host constants folded"));
if (!pimDisableMemoryCoalescing)
+4 -1
View File
@@ -154,7 +154,10 @@ static OperationOrdering buildOperationOrdering(Operation* coreLikeOp) {
}
static bool isSupportedAliasOp(Operation* op) {
return isa<memref::SubViewOp, memref::CastOp, memref::CollapseShapeOp, memref::ExpandShapeOp>(op);
return isa<memref::SubViewOp,
memref::CastOp,
memref::CollapseShapeOp,
memref::ExpandShapeOp>(op);
}
static bool isRuntimeMemoryTouchOp(Operation* op) {
@@ -13,6 +13,7 @@
#include <utility>
#include "src/Accelerators/PIM/Common/Support/CheckedArithmetic.hpp"
#include "src/Accelerators/PIM/Common/IR/TensorSliceUtils.hpp"
#include "src/Accelerators/PIM/Conversion/ONNXToSpatial/CompileTime.hpp"
#include "src/Accelerators/PIM/Dialect/Spatial/SpatialOps.hpp"
@@ -374,9 +375,6 @@ extractGraphBatchPhysicalFragment(mlir::PatternRewriter& rewriter,
auto physicalType = mlir::dyn_cast<mlir::RankedTensorType>(physicalBatch.getType());
if (!physicalType || physicalType.getRank() != fragmentType.getRank() + 1)
return mlir::failure();
mlir::SmallVector<int64_t> selectedShape {1};
llvm::append_range(selectedShape, fragmentType.getShape());
auto selectedType = mlir::RankedTensorType::get(selectedShape, fragmentType.getElementType(), fragmentType.getEncoding());
mlir::SmallVector<mlir::OpFoldResult> offsets {slot};
mlir::SmallVector<mlir::OpFoldResult> sizes {rewriter.getIndexAttr(1)};
mlir::SmallVector<mlir::OpFoldResult> strides {rewriter.getIndexAttr(1)};
@@ -385,11 +383,8 @@ extractGraphBatchPhysicalFragment(mlir::PatternRewriter& rewriter,
sizes.push_back(rewriter.getIndexAttr(dim));
strides.push_back(rewriter.getIndexAttr(1));
}
mlir::Value selected = mlir::tensor::ExtractSliceOp::create(rewriter, loc, selectedType, physicalBatch, offsets, sizes, strides);
mlir::SmallVector<mlir::ReassociationIndices> reassociation {{0, 1}};
for (int64_t dim = 2; dim <= fragmentType.getRank(); ++dim)
reassociation.push_back({dim});
return mlir::tensor::CollapseShapeOp::create(rewriter, loc, fragmentType, selected, reassociation).getResult();
return extractMixedSliceOrIdentity(
rewriter, loc, physicalBatch, fragmentType, {offsets, sizes, strides});
}
template <typename BodyFn>
@@ -3,6 +3,7 @@
#include "mlir/Dialect/Tensor/IR/Tensor.h"
#include "src/Accelerators/PIM/Conversion/ONNXToSpatial/Common/Common.hpp"
#include "src/Accelerators/PIM/Conversion/ONNXToSpatial/Common/ComputeRegionBuilder.hpp"
#include "src/Accelerators/PIM/Dialect/Spatial/SpatialOps.hpp"
using namespace mlir;
@@ -38,6 +39,30 @@ Value createPaddedInputCompute(Value input,
if (inputType == paddedInputType)
return input;
auto producer = inputType.getRank() == 2 && paddedInputType.getRank() == 2
? input.getDefiningOp<spatial::SpatGraphComputeBatch>()
: spatial::SpatGraphComputeBatch();
auto inputFragmentType = producer
? spatial::getGraphBatchFragmentType(inputType, producer.getLaneCount())
: FailureOr<RankedTensorType>(failure());
auto paddedFragmentType = producer
? spatial::getGraphBatchFragmentType(paddedInputType, producer.getLaneCount())
: FailureOr<RankedTensorType>(failure());
if (producer && succeeded(inputFragmentType) && succeeded(paddedFragmentType)) {
auto batch = createSpatComputeBatch(rewriter, loc, TypeRange {paddedInputType}, producer.getLaneCount(), {}, input,
[&](detail::SpatComputeBatchBodyArgs args) -> LogicalResult {
auto fragment = extractGraphBatchPhysicalFragment(
rewriter, loc, args.inputs.front(), args.lane, *inputFragmentType);
if (failed(fragment))
return failure();
Value padded = createZeroPaddedTensor(*fragment, *paddedFragmentType, rewriter, loc);
publishGraphBatchPhysicalFragment(rewriter, loc, padded, args.outputs.front(), args.lane);
return success();
});
if (succeeded(batch))
return batch->getResult(0);
}
auto computeOp = createSpatCompute<1>(rewriter, loc, TypeRange {paddedInputType}, {}, input, [&](Value computeInput) {
Value paddedInput = createZeroPaddedTensor(computeInput, paddedInputType, rewriter, loc);
spatial::SpatYieldOp::create(rewriter, loc, paddedInput);
@@ -79,6 +79,57 @@ materializeRowStripToDense(const RowStripPhysicalValue& rowStripValue, Location
return createRowStripAssemblyBlueprint(rowStripValue.storage, rowStripValue.logicalType, rewriter, loc);
}
static FailureOr<Value> lowerDenseBatchBiasAdd(Value input, Value bias, RankedTensorType resultType,
PatternRewriter& rewriter, Location loc) {
auto producer = input.getDefiningOp<spatial::SpatGraphComputeBatch>();
auto inputType = dyn_cast<RankedTensorType>(input.getType());
auto biasType = dyn_cast<RankedTensorType>(bias.getType());
if (!producer || !inputType || !biasType || !inputType.hasStaticShape() || !biasType.hasStaticShape()
|| !resultType.hasStaticShape() || inputType.getDimSize(0) != producer.getLaneCount()
|| biasType.getDimSize(0) != producer.getLaneCount() || resultType.getDimSize(0) != producer.getLaneCount())
return failure();
auto inputFragmentType = spatial::getGraphBatchFragmentType(inputType, producer.getLaneCount());
auto outputFragmentType = spatial::getGraphBatchFragmentType(resultType, producer.getLaneCount());
if (failed(inputFragmentType) || failed(outputFragmentType) || inputFragmentType->getRank() != biasType.getRank()
|| inputFragmentType->getDimSize(0) != 1 || inputFragmentType->getShape().drop_front() != biasType.getShape().drop_front()
|| inputFragmentType->getRank() != outputFragmentType->getRank() + 1)
return failure();
for (auto [inputDim, outputDim] : llvm::zip(inputFragmentType->getShape().drop_front(), outputFragmentType->getShape()))
if (outputDim > inputDim)
return failure();
auto batch = createSpatComputeBatch(rewriter, loc, TypeRange {resultType}, producer.getLaneCount(), {}, ValueRange {input, bias},
[&](detail::SpatComputeBatchBodyArgs args) -> LogicalResult {
FailureOr<Value> fragment = extractGraphBatchPhysicalFragment(rewriter, loc, args.inputs[0], args.lane, *inputFragmentType);
if (failed(fragment))
return failure();
MixedSliceGeometry biasSlice;
for (int64_t dim : inputFragmentType->getShape()) {
biasSlice.offsets.push_back(biasSlice.offsets.empty() ? OpFoldResult(args.lane) : rewriter.getIndexAttr(0));
biasSlice.sizes.push_back(rewriter.getIndexAttr(dim));
biasSlice.strides.push_back(rewriter.getIndexAttr(1));
}
Value biasFragment = extractMixedSliceOrIdentity(rewriter, loc, args.inputs[1], *inputFragmentType, biasSlice);
if (!biasFragment)
return failure();
Value added = spatial::SpatVAddOp::create(rewriter, loc, *inputFragmentType, *fragment, biasFragment);
MixedSliceGeometry outputSlice;
outputSlice.offsets.assign(inputFragmentType->getRank(), rewriter.getIndexAttr(0));
outputSlice.sizes.push_back(rewriter.getIndexAttr(1));
outputSlice.strides.assign(inputFragmentType->getRank(), rewriter.getIndexAttr(1));
for (int64_t dim : outputFragmentType->getShape())
outputSlice.sizes.push_back(rewriter.getIndexAttr(dim));
Value output = extractMixedSliceOrIdentity(rewriter, loc, added, *outputFragmentType, outputSlice);
if (!output)
return failure();
publishGraphBatchPhysicalFragment(rewriter, loc, output, args.outputs.front(), args.lane);
return success();
});
if (failed(batch))
return failure();
return batch->getResult(0);
}
struct LowerSpatialPlansPass final : PassWrapper<LowerSpatialPlansPass, OperationPass<ModuleOp>> {
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(LowerSpatialPlansPass)
@@ -236,6 +287,13 @@ struct LowerSpatialPlansPass final : PassWrapper<LowerSpatialPlansPass, Operatio
signalPassFailure();
return;
}
if (planOp.getInput().getDefiningOp<spatial::SpatGraphComputeBatch>()) {
FailureOr<Value> lowered = lowerDenseBatchBiasAdd(planOp.getInput(), *denseBias, resultType, rewriter, planOp.getLoc());
if (succeeded(lowered)) {
rewriter.replaceOp(planOp, *lowered);
continue;
}
}
auto computeOp = createSpatCompute<2>(rewriter,
planOp.getLoc(),
planOp.getOutput().getType(),
@@ -20,6 +20,7 @@
#include "src/Accelerators/PIM/Common/IR/AffineUtils.hpp"
#include "src/Accelerators/PIM/Common/IR/LoopUtils.hpp"
#include "src/Accelerators/PIM/Common/IR/TensorSliceUtils.hpp"
#include "src/Accelerators/PIM/Common/Support/Diagnostics.hpp"
#include "src/Accelerators/PIM/Common/Support/ReportUtils.hpp"
#include "src/Accelerators/PIM/Compiler/PimCompilerOptions.hpp"
@@ -1355,20 +1356,11 @@ static Value createWeightTile(Value packedWeights,
SmallVector<OpFoldResult> sizes {rewriter.getIndexAttr(1),
rewriter.getIndexAttr(tiling.tileInputRows),
rewriter.getIndexAttr(tiling.tileOutputChannels)};
auto sliceType =
RankedTensorType::get({1, tiling.tileInputRows, tiling.tileOutputChannels}, packedWeightType.getElementType());
Value slice = tensor::ExtractSliceOp::create(
rewriter, loc, sliceType, packedWeights, offsets, sizes, getUnitStrides(rewriter, 3));
auto collapsedType =
RankedTensorType::get({tiling.tileInputRows, tiling.tileOutputChannels}, packedWeightType.getElementType());
return tensor::CollapseShapeOp::create(rewriter,
loc,
collapsedType,
slice,
SmallVector<ReassociationIndices> {
{0, 1},
{2}
});
return extractMixedSliceOrIdentity(
rewriter, loc, packedWeights, collapsedType,
{offsets, sizes, getUnitStrides(rewriter, 3)});
}
static Value createBiasTile(
@@ -1676,8 +1668,6 @@ struct ConvGemmPlan {
int64_t effectiveMaxParallelPixels;
int64_t packedNumRows;
RankedTensorType im2colType;
RankedTensorType im2colRowType;
RankedTensorType gemmInputRowsType;
RankedTensorType wFlatType;
RankedTensorType wTransType;
@@ -1718,52 +1708,6 @@ static PreparedConvInput prepareInputForIm2Col(const ConvLoweringState& state,
return {paddedInputOp.getResult(0), paddedType};
}
static Value createPaddedRows(Value rows,
RankedTensorType rowsType,
int64_t paddedRows,
PatternRewriter& rewriter,
Location loc) {
if (rowsType.getDimSize(0) == paddedRows)
return rows;
auto paddedType =
RankedTensorType::get({paddedRows, rowsType.getDimSize(1)}, rowsType.getElementType(), rowsType.getEncoding());
return createZeroPaddedTensor(
rows, paddedType, {0, 0}, {paddedRows - rowsType.getDimSize(0), 0}, rewriter, loc);
}
static Value packRowsForParallelGemm(
Value rows, RankedTensorType rowsType, int64_t packFactor, PatternRewriter& rewriter, Location loc) {
if (packFactor == 1)
return rows;
const int64_t paddedNumRows = ceilIntegerDivide(rowsType.getDimSize(0), packFactor) * packFactor;
const int64_t packedNumRows = paddedNumRows / packFactor;
const int64_t rowWidth = rowsType.getDimSize(1);
auto groupedType =
RankedTensorType::get({packedNumRows, packFactor, rowWidth}, rowsType.getElementType(), rowsType.getEncoding());
auto packedType =
RankedTensorType::get({packedNumRows, packFactor * rowWidth}, rowsType.getElementType(), rowsType.getEncoding());
Value padded = createPaddedRows(rows, rowsType, paddedNumRows, rewriter, loc);
Value grouped = tensor::ExpandShapeOp::create(rewriter,
loc,
groupedType,
padded,
SmallVector<ReassociationIndices> {
{0, 1},
{2}
});
return tensor::CollapseShapeOp::create(rewriter,
loc,
packedType,
grouped,
SmallVector<ReassociationIndices> {
{0},
{1, 2}
});
}
static Value unpackRowsFromParallelGemm(Value packedRows,
RankedTensorType packedRowsType,
int64_t unpackedRows,
@@ -2166,8 +2110,6 @@ buildConvGemmPlan(const ConvLoweringState& state,
auto elemType = state.xType.getElementType();
auto outElemType = state.outType.getElementType();
plan.im2colType = RankedTensorType::get({plan.chunkNumPatches, plan.patchSize}, elemType);
plan.im2colRowType = RankedTensorType::get({1, plan.patchSize}, elemType);
plan.gemmInputRowsType =
RankedTensorType::get({plan.packedNumRows, plan.effectiveMaxParallelPixels * plan.patchSize}, elemType);
plan.wFlatType = RankedTensorType::get({state.numChannelsOut, plan.patchSize}, state.wType.getElementType());
@@ -2185,44 +2127,99 @@ static Value createIm2colRows(const ConvLoweringState& state,
const ConvGemmPlan& plan,
PatternRewriter& rewriter,
Location loc) {
constexpr size_t numInputs = 1;
auto im2colComputeOp =
createSpatCompute<numInputs>(rewriter, loc, TypeRange {plan.gemmInputRowsType}, {}, preparedInput.value, [&](Value xArg) {
auto elemType = preparedInput.type.getElementType();
// Keep the standard im2col view of convolution, flipped so filters sit in
// B / crossbar columns:
// A (im2col): [numPatches, patchSize] -- one row per output spatial position
// B (weights): [patchSize, cOut]
// Gemm output: [numPatches, cOut]
Value im2colInit = tensor::EmptyOp::create(rewriter, loc, plan.im2colType.getShape(), elemType);
if (plan.gemmInputRowsType.getDimSize(1) > crossbarSize.getValue()) {
assert(plan.effectiveMaxParallelPixels == 1 && "multi-crossbar im2col rows cannot pack pixels");
auto compute = createSpatCompute<1>(
rewriter, loc, TypeRange {plan.gemmInputRowsType}, {}, preparedInput.value, [&](Value input) {
auto elemType = preparedInput.type.getElementType();
Value empty = tensor::EmptyOp::create(rewriter, loc, plan.gemmInputRowsType.getShape(), elemType);
Operation *anchor = rewriter.getInsertionBlock()->getParentOp();
Value c0 = getOrCreateIndexConstant(rewriter, anchor, 0);
Value c1 = getOrCreateIndexConstant(rewriter, anchor, 1);
Value upper = getOrCreateIndexConstant(rewriter, anchor, plan.chunkNumPatches);
auto patchType = RankedTensorType::get(
{1, state.numChannelsIn, state.wHeight, state.wWidth}, elemType);
auto rowType = RankedTensorType::get({plan.patchSize}, elemType);
auto loop = buildNormalizedScfFor(
rewriter, loc, c0, upper, c1, ValueRange {empty},
[&](OpBuilder &, Location nestedLoc, Value patchIndex, ValueRange iterArgs,
SmallVectorImpl<Value> &yielded) {
Value batchIndex = affineAddFloorDivConst(
rewriter, nestedLoc, patchIndex, plan.chunkStart, plan.numPatchesPerBatch, anchor);
Value batchPatchIndex = affineAddModConst(
rewriter, nestedLoc, patchIndex, plan.chunkStart, plan.numPatchesPerBatch, anchor);
Value outHeight = affineFloorDivConst(
rewriter, nestedLoc, batchPatchIndex, state.outWidth, anchor);
Value outWidth = affineModConst(
rewriter, nestedLoc, batchPatchIndex, state.outWidth, anchor);
Value patch = createConvInputPatch(
input, patchType, batchIndex, c0,
affineMulConst(rewriter, nestedLoc, outHeight, state.strideHeight, anchor),
affineMulConst(rewriter, nestedLoc, outWidth, state.strideWidth, anchor),
state.dilationHeight, state.dilationWidth, rewriter, nestedLoc);
Value row = tensor::CollapseShapeOp::create(
rewriter, nestedLoc, rowType, patch,
SmallVector<ReassociationIndices> {{0, 1, 2, 3}});
Value next = tensor::InsertSliceOp::create(
rewriter, nestedLoc, row, iterArgs.front(),
SmallVector<OpFoldResult> {patchIndex, rewriter.getIndexAttr(0)},
SmallVector<OpFoldResult> {rewriter.getIndexAttr(1), rewriter.getIndexAttr(plan.patchSize)},
getUnitStrides(rewriter, 2));
yielded.push_back(next);
return success();
});
if (failed(loop))
return failure();
spatial::SpatYieldOp::create(rewriter, loc, loop->results.front());
return success();
});
assert(succeeded(compute) && "Conv im2col compute construction must succeed");
return compute->getResult(0);
}
auto elemType = preparedInput.type.getElementType();
auto packedRowType = RankedTensorType::get(
{plan.effectiveMaxParallelPixels * plan.patchSize}, elemType, plan.gemmInputRowsType.getEncoding());
auto zeroAttr = DenseElementsAttr::get(packedRowType, rewriter.getZeroAttr(elemType));
Value zeroRow = getOrCreateConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(), zeroAttr, packedRowType);
auto im2colComputeOp = createSpatComputeBatch(
rewriter,
loc,
TypeRange {plan.gemmInputRowsType},
plan.packedNumRows,
{},
ValueRange {preparedInput.value, zeroRow},
[&](detail::SpatComputeBatchBodyArgs args) {
Operation* anchorOp = rewriter.getInsertionBlock()->getParentOp();
Value c0 = getOrCreateIndexConstant(rewriter, anchorOp, 0);
Value c1 = getOrCreateIndexConstant(rewriter, anchorOp, 1);
Value cPack = getOrCreateIndexConstant(rewriter, anchorOp, plan.effectiveMaxParallelPixels);
Value cNumPatches = getOrCreateIndexConstant(rewriter, anchorOp, plan.chunkNumPatches);
Value laneStart = affineMulConst(rewriter, loc, args.lane, plan.effectiveMaxParallelPixels, anchorOp);
Value remaining = arith::SubIOp::create(rewriter, loc, cNumPatches, laneStart);
Value isPartial = arith::CmpIOp::create(rewriter, loc, arith::CmpIPredicate::ult, remaining, cPack);
Value lanePatches = arith::SelectOp::create(rewriter, loc, isPartial, remaining, cPack);
auto patchType = RankedTensorType::get({1, state.numChannelsIn, state.wHeight, state.wWidth}, elemType);
auto patchRowType = RankedTensorType::get({plan.patchSize}, elemType);
auto im2colLoop = buildNormalizedScfFor(
auto rowLoop = buildNormalizedScfFor(
rewriter,
loc,
c0,
cNumPatches,
lanePatches,
c1,
ValueRange {im2colInit},
[&](OpBuilder&, Location nestedLoc, Value patchIndex, ValueRange iterArgs, SmallVectorImpl<Value>& yielded) {
Value im2colAcc = iterArgs.front();
ValueRange {args.inputs[1]},
[&](OpBuilder&, Location nestedLoc, Value copyIndex, ValueRange iterArgs, SmallVectorImpl<Value>& yielded) {
Value patchIndex = arith::AddIOp::create(rewriter, nestedLoc, laneStart, copyIndex);
Value batchIndex =
affineAddFloorDivConst(rewriter, nestedLoc, patchIndex, plan.chunkStart, plan.numPatchesPerBatch, anchorOp);
Value batchPatchIndex =
affineAddModConst(rewriter, nestedLoc, patchIndex, plan.chunkStart, plan.numPatchesPerBatch, anchorOp);
Value outHeightIndex = affineFloorDivConst(rewriter, nestedLoc, batchPatchIndex, state.outWidth, anchorOp);
Value outWidthIndex = affineModConst(rewriter, nestedLoc, batchPatchIndex, state.outWidth, anchorOp);
Value inputHeightOffset =
affineMulConst(rewriter, nestedLoc, outHeightIndex, state.strideHeight, anchorOp);
Value inputWidthOffset =
affineMulConst(rewriter, nestedLoc, outWidthIndex, state.strideWidth, anchorOp);
auto patchType =
RankedTensorType::get({1, state.numChannelsIn, state.wHeight, state.wWidth}, elemType);
Value patch = createConvInputPatch(xArg,
Value inputHeightOffset = affineMulConst(rewriter, nestedLoc, outHeightIndex, state.strideHeight, anchorOp);
Value inputWidthOffset = affineMulConst(rewriter, nestedLoc, outWidthIndex, state.strideWidth, anchorOp);
Value patch = createConvInputPatch(args.inputs.front(),
patchType,
batchIndex,
c0,
@@ -2232,34 +2229,27 @@ static Value createIm2colRows(const ConvLoweringState& state,
state.dilationWidth,
rewriter,
nestedLoc);
Value row = tensor::CollapseShapeOp::create(rewriter,
nestedLoc,
plan.im2colRowType,
patch,
SmallVector<ReassociationIndices> {
{0},
{1, 2, 3}
Value patchRow = tensor::CollapseShapeOp::create(rewriter,
nestedLoc,
patchRowType,
patch,
SmallVector<ReassociationIndices> {
{0, 1, 2, 3}
});
SmallVector<OpFoldResult> rowOffsets {patchIndex, rewriter.getIndexAttr(0)};
SmallVector<OpFoldResult> rowSizes {rewriter.getIndexAttr(1), rewriter.getIndexAttr(plan.patchSize)};
Value next = tensor::InsertSliceOp::create(
rewriter, nestedLoc, row, im2colAcc, rowOffsets, rowSizes, getUnitStrides(rewriter, 2));
Value rowOffset = affineMulConst(rewriter, nestedLoc, copyIndex, plan.patchSize, anchorOp);
Value next = tensor::InsertSliceOp::create(rewriter,
nestedLoc,
patchRow,
iterArgs.front(),
SmallVector<OpFoldResult> {rowOffset},
SmallVector<OpFoldResult> {rewriter.getIndexAttr(plan.patchSize)},
getUnitStrides(rewriter, 1));
yielded.push_back(next);
return success();
});
if (failed(im2colLoop))
if (failed(rowLoop))
return failure();
Value gemmInputRows = im2colLoop->results.front();
// Pack N old im2col rows into one longer row so one GEMM can cover N
// pixels in parallel. The corresponding packed weight matrix contains N
// block-diagonal copies of W^T, and the packed output must be unpacked
// back to one row per spatial patch.
if (plan.effectiveMaxParallelPixels != 1)
gemmInputRows = packRowsForParallelGemm(gemmInputRows, plan.im2colType, plan.effectiveMaxParallelPixels, rewriter, loc);
spatial::SpatYieldOp::create(rewriter, loc, gemmInputRows);
publishGraphBatchPhysicalFragment(rewriter, loc, rowLoop->results.front(), args.outputs.front(), args.lane);
return success();
});
@@ -17,6 +17,7 @@
#include "src/Accelerators/PIM/Common/IR/AffineUtils.hpp"
#include "src/Accelerators/PIM/Common/IR/LoopUtils.hpp"
#include "src/Accelerators/PIM/Common/IR/ShapeUtils.hpp"
#include "src/Accelerators/PIM/Common/IR/TensorSliceUtils.hpp"
#include "src/Accelerators/PIM/Common/PimCommon.hpp"
#include "src/Accelerators/PIM/Common/Support/Diagnostics.hpp"
#include "src/Accelerators/PIM/Conversion/ONNXToSpatial/Common/Common.hpp"
@@ -501,9 +502,9 @@ static Value extractReductionPiece(Value partialPiecesArg,
SmallVector<OpFoldResult> pieceSizes {rewriter.getIndexAttr(numOutRows), rewriter.getIndexAttr(1), rewriter.getIndexAttr(crossbarSize.getValue())};
SmallVector<OpFoldResult> pieceOffsets {
createPartialGroupOffset(hSlice, kSlice, numKSlices, numOutRows, rewriter, loc), rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)};
auto selectedType = RankedTensorType::get({numOutRows, 1, static_cast<int64_t>(crossbarSize.getValue())}, pieceType.getElementType());
Value selected = tensor::ExtractSliceOp::create(rewriter, loc, selectedType, partialPiecesArg, pieceOffsets, pieceSizes, unitStrides);
return tensor::CollapseShapeOp::create(rewriter, loc, pieceType, selected, SmallVector<ReassociationIndices> {{0, 1}, {2}});
return extractMixedSliceOrIdentity(
rewriter, loc, partialPiecesArg, pieceType,
{pieceOffsets, pieceSizes, unitStrides});
}
static Value reducePartialPiecesForHSlice(Value partialPiecesArg,
@@ -9,6 +9,7 @@
#include "src/Accelerators/PIM/Common/IR/AffineUtils.hpp"
#include "src/Accelerators/PIM/Common/IR/LoopUtils.hpp"
#include "src/Accelerators/PIM/Common/IR/TensorSliceUtils.hpp"
#include "src/Accelerators/PIM/Conversion/ONNXToSpatial/Common/Common.hpp"
#include "src/Accelerators/PIM/Conversion/ONNXToSpatial/CompileTime.hpp"
#include "src/Accelerators/PIM/Conversion/ONNXToSpatial/Patterns.hpp"
@@ -299,22 +300,14 @@ static Value extractBatchedATile(Value a,
RankedTensorType aTileType,
PatternRewriter& rewriter,
Location loc) {
auto aSliceType = RankedTensorType::get({1, 1, aTileType.getDimSize(1)}, aTileType.getElementType());
Value sourceBatchIndex =
mapOutputBatchIndexToSourceBatchIndex(outputBatchIndex, sourceBatchShape, outputBatchShape, rewriter, loc);
SmallVector<OpFoldResult> offsets {OpFoldResult(sourceBatchIndex), row, kOffset};
SmallVector<OpFoldResult> sizes {
rewriter.getIndexAttr(1), rewriter.getIndexAttr(1), rewriter.getIndexAttr(aTileType.getDimSize(1))};
auto slice =
tensor::ExtractSliceOp::create(rewriter, loc, aSliceType, a, offsets, sizes, getUnitStrides(rewriter, 3));
return tensor::CollapseShapeOp::create(rewriter,
loc,
aTileType,
slice,
SmallVector<ReassociationIndices> {
{0, 1},
{2}
});
return extractMixedSliceOrIdentity(
rewriter, loc, a, aTileType,
{offsets, sizes, getUnitStrides(rewriter, 3)});
}
static Value extractBatchedBTile(Value b,
@@ -326,24 +319,15 @@ static Value extractBatchedBTile(Value b,
RankedTensorType bTileType,
PatternRewriter& rewriter,
Location loc) {
auto bSliceType =
RankedTensorType::get({1, bTileType.getDimSize(0), bTileType.getDimSize(1)}, bTileType.getElementType());
Value sourceBatchIndex =
mapOutputBatchIndexToSourceBatchIndex(outputBatchIndex, sourceBatchShape, outputBatchShape, rewriter, loc);
SmallVector<OpFoldResult> offsets {OpFoldResult(sourceBatchIndex), kOffset, hOffset};
SmallVector<OpFoldResult> sizes {rewriter.getIndexAttr(1),
rewriter.getIndexAttr(bTileType.getDimSize(0)),
rewriter.getIndexAttr(bTileType.getDimSize(1))};
auto slice =
tensor::ExtractSliceOp::create(rewriter, loc, bSliceType, b, offsets, sizes, getUnitStrides(rewriter, 3));
return tensor::CollapseShapeOp::create(rewriter,
loc,
bTileType,
slice,
SmallVector<ReassociationIndices> {
{0, 1},
{2}
});
return extractMixedSliceOrIdentity(
rewriter, loc, b, bTileType,
{offsets, sizes, getUnitStrides(rewriter, 3)});
}
static Value getBatchLaneIndex(
@@ -448,22 +432,14 @@ static Value extractDynamicBatchedRowVector(Value matrix,
RankedTensorType vectorType,
PatternRewriter& rewriter,
Location loc) {
auto rowSliceType = RankedTensorType::get({1, 1, vectorType.getDimSize(1)}, vectorType.getElementType());
Value sourceBatchIndex =
mapOutputBatchIndexToSourceBatchIndex(outputBatchIndex, sourceBatchShape, outputBatchShape, rewriter, loc);
SmallVector<OpFoldResult> offsets {OpFoldResult(sourceBatchIndex), row, rewriter.getIndexAttr(0)};
SmallVector<OpFoldResult> sizes {
rewriter.getIndexAttr(1), rewriter.getIndexAttr(1), rewriter.getIndexAttr(vectorType.getDimSize(1))};
auto rowSlice =
tensor::ExtractSliceOp::create(rewriter, loc, rowSliceType, matrix, offsets, sizes, getUnitStrides(rewriter, 3));
return tensor::CollapseShapeOp::create(rewriter,
loc,
vectorType,
rowSlice,
SmallVector<ReassociationIndices> {
{0, 1},
{2}
});
return extractMixedSliceOrIdentity(
rewriter, loc, matrix, vectorType,
{offsets, sizes, getUnitStrides(rewriter, 3)});
}
static FailureOr<spatial::SpatComputeBatch> createBatchedVvdmulBatch(Value a,
@@ -519,7 +495,6 @@ static FailureOr<Value> createBatchedDynamicOutputCompute(Value scalarPieces,
const int64_t numOutRows = outType.getDimSize(1);
const int64_t numOutCols = outType.getDimSize(2);
auto scalarType = RankedTensorType::get({1, 1}, outType.getElementType());
auto outputScalarType = RankedTensorType::get({1, 1, 1}, outType.getElementType());
auto computeOp = createSpatCompute<1>(
rewriter, loc, TypeRange {outType}, {}, ValueRange {scalarPieces}, [&](Value pieces) -> LogicalResult {
@@ -545,20 +520,12 @@ static FailureOr<Value> createBatchedDynamicOutputCompute(Value scalarPieces,
FailureOr<Value> scalar = extractGraphBatchPhysicalFragment(rewriter, nestedLoc, pieces, lane, scalarType);
if (failed(scalar))
return failure();
Value expanded = tensor::ExpandShapeOp::create(rewriter,
nestedLoc,
outputScalarType,
*scalar,
SmallVector<ReassociationIndices> {
{0},
{1, 2}
});
SmallVector<OpFoldResult> outputOffsets {batch, row, column};
SmallVector<OpFoldResult> outputSizes = {
rewriter.getIndexAttr(1), rewriter.getIndexAttr(1), rewriter.getIndexAttr(1)};
Value next =
tensor::InsertSliceOp::create(
rewriter, nestedLoc, expanded, outputAcc, outputOffsets, outputSizes, getUnitStrides(rewriter, 3))
rewriter, nestedLoc, *scalar, outputAcc, outputOffsets, outputSizes, getUnitStrides(rewriter, 3))
.getResult();
yielded.push_back(next);
return success();
@@ -591,9 +558,9 @@ static Value extractBatchedReductionPiece(Value partialPiecesArg,
Value pieceOffset = arith::AddIOp::create(rewriter, loc, batchAndHSlice, kOffset);
SmallVector<OpFoldResult> offsets {pieceOffset, rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)};
SmallVector<OpFoldResult> sizes {rewriter.getIndexAttr(numOutRows), rewriter.getIndexAttr(1), rewriter.getIndexAttr(crossbarSize.getValue())};
auto selectedType = RankedTensorType::get({numOutRows, 1, static_cast<int64_t>(crossbarSize.getValue())}, pieceType.getElementType());
Value selected = tensor::ExtractSliceOp::create(rewriter, loc, selectedType, partialPiecesArg, offsets, sizes, getUnitStrides(rewriter, 3));
return tensor::CollapseShapeOp::create(rewriter, loc, pieceType, selected, SmallVector<ReassociationIndices> {{0, 1}, {2}});
return extractMixedSliceOrIdentity(
rewriter, loc, partialPiecesArg, pieceType,
{offsets, sizes, getUnitStrides(rewriter, 3)});
}
static Value reduceBatchedPartialPiecesForHSlice(Value partialPiecesArg,
@@ -640,8 +607,6 @@ static FailureOr<Value> createBatchedReductionCompute(Value partialPieces,
const int64_t numOutHSlices = ceilIntegerDivide(outType.getDimSize(2), crossbarSize.getValue());
auto pieceType = RankedTensorType::get({numOutRows, static_cast<int64_t>(crossbarSize.getValue())},
partialPiecesType.getElementType());
auto outputSliceType = RankedTensorType::get({1, numOutRows, static_cast<int64_t>(crossbarSize.getValue())},
partialPiecesType.getElementType());
Value outputInit =
tensor::EmptyOp::create(rewriter, loc, paddedOutType.getShape(), paddedOutType.getElementType()).getResult();
@@ -671,14 +636,6 @@ static FailureOr<Value> createBatchedReductionCompute(Value partialPieces,
Value outputAcc = hIterArgs.front();
Value reduced = reduceBatchedPartialPiecesForHSlice(
partialPiecesArg, batch, hSlice, pieceType, numKSlices, numOutHSlices, numOutRows, rewriter, hLoc);
Value expandedReduced = tensor::ExpandShapeOp::create(rewriter,
hLoc,
outputSliceType,
reduced,
SmallVector<ReassociationIndices> {
{0, 1},
{2}
});
Value hOffset = affineMulConst(
rewriter, hLoc, hSlice, crossbarSize.getValue(), rewriter.getInsertionBlock()->getParentOp());
SmallVector<OpFoldResult> outputOffsets {batch, rewriter.getIndexAttr(0), hOffset};
@@ -687,7 +644,7 @@ static FailureOr<Value> createBatchedReductionCompute(Value partialPieces,
rewriter.getIndexAttr(crossbarSize.getValue())};
Value next =
tensor::InsertSliceOp::create(
rewriter, hLoc, expandedReduced, outputAcc, outputOffsets, outputSizes, getUnitStrides(rewriter, 3))
rewriter, hLoc, reduced, outputAcc, outputOffsets, outputSizes, getUnitStrides(rewriter, 3))
.getResult();
hYielded.push_back(next);
return success();
@@ -1,7 +1,8 @@
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/Tensor/IR/Tensor.h"
#include "src/Accelerators/PIM/Conversion/SpatialToPim/Common.hpp"
#include "src/Accelerators/PIM/Conversion/SpatialToPim/Patterns.hpp"
#include "src/Accelerators/PIM/Common/IR/ShapeUtils.hpp"
#include "src/Accelerators/PIM/Dialect/Pim/PimOps.hpp"
#include "src/Accelerators/PIM/Dialect/Spatial/SpatialOps.hpp"
@@ -18,6 +19,30 @@ static void copyRaptorDebugAttrs(Operation* source, Operation* target) {
}
}
static Value createDestinationByteOffset(PatternRewriter& rewriter,
tensor::InsertSliceOp insert) {
auto destinationType = cast<RankedTensorType>(insert.getDestType());
SmallVector<int64_t> strides = computeRowMajorStrides(destinationType.getShape());
int64_t elementBytes = getElementTypeSizeInBytes(destinationType.getElementType());
Value total = arith::ConstantIndexOp::create(rewriter, insert.getLoc(), 0);
for (auto [dimension, offset] : llvm::enumerate(insert.getMixedOffsets())) {
int64_t scale = strides[dimension] * elementBytes;
Value component;
if (auto attribute = dyn_cast<Attribute>(offset)) {
component = arith::ConstantIndexOp::create(
rewriter, insert.getLoc(), cast<IntegerAttr>(attribute).getInt() * scale);
} else {
component = cast<Value>(offset);
if (scale != 1)
component = arith::MulIOp::create(
rewriter, insert.getLoc(), component,
arith::ConstantIndexOp::create(rewriter, insert.getLoc(), scale));
}
total = arith::AddIOp::create(rewriter, insert.getLoc(), total, component);
}
return total;
}
struct ChannelSendLowering : OpRewritePattern<spatial::SpatChannelSendOp> {
using OpRewritePattern::OpRewritePattern;
@@ -40,7 +65,21 @@ struct ChannelReceiveLowering : OpRewritePattern<spatial::SpatChannelReceiveOp>
rewriter.eraseOp(op);
return success();
}
auto outputType = cast<ShapedType>(op.getResult().getType());
auto outputType = cast<RankedTensorType>(op.getResult().getType());
tensor::InsertSliceOp destinationInsert;
if (op->hasOneUse()) {
auto insert = dyn_cast<tensor::InsertSliceOp>(*op->getUsers().begin());
auto destinationType = insert
? dyn_cast<RankedTensorType>(insert.getDestType()) : RankedTensorType();
if (insert && insert.getSource() == op.getOutput()
&& insert.getSourceType() == outputType
&& insert->getBlock() == op->getBlock() && destinationType
&& destinationType.hasStaticShape()
&& isContiguousSubviewWithDynamicOffsets(
destinationType.getShape(), insert.getMixedOffsets(),
insert.getStaticSizes(), insert.getStaticStrides()))
destinationInsert = insert;
}
Value outputBuffer =
tensor::EmptyOp::create(rewriter, op.getLoc(), outputType.getShape(), outputType.getElementType()).getResult();
auto sizeAttr = getTensorSizeInBytesAttr(rewriter, op.getOperation(), op.getResult());
@@ -50,7 +89,19 @@ struct ChannelReceiveLowering : OpRewritePattern<spatial::SpatChannelReceiveOp>
rewriter, op.getLoc(), op.getResult().getType(), outputBuffer, *sizeAttr, op.getSourceCoreId());
copyRaptorDebugAttrs(op.getOperation(), receive.getOperation());
Value received = receive.getOutput();
rewriter.replaceOp(op, received);
if (!destinationInsert) {
rewriter.replaceOp(op, received);
return success();
}
rewriter.setInsertionPoint(destinationInsert);
Value targetOffset = createDestinationByteOffset(rewriter, destinationInsert);
Value zero = arith::ConstantIndexOp::create(rewriter, op.getLoc(), 0);
auto copy = pim::PimMemCopyOp::create(
rewriter, op.getLoc(), destinationInsert.getDestType(), targetOffset, zero,
destinationInsert.getDest(), received, *sizeAttr);
rewriter.replaceOp(destinationInsert, copy.getOutput());
rewriter.eraseOp(op);
return success();
}
};
@@ -1,4 +1,3 @@
#include "mlir/Conversion/AffineToStandard/AffineToStandard.h"
#include "mlir/Dialect/Affine/IR/AffineOps.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/Bufferization/IR/Bufferization.h"
@@ -175,11 +174,11 @@ void onnx_mlir::raptor::SpatialToPimPass::runOnOperation() {
RewritePatternSet coreBodyPatterns(ctx);
populateCoreBodyPatterns(coreBodyPatterns);
populateAffineToStdConversionPatterns(coreBodyPatterns);
FrozenRewritePatternSet frozenCoreBodyPatterns(std::move(coreBodyPatterns));
ConversionTarget coreBodyTarget(*ctx);
coreBodyTarget.addLegalDialect<PimDialect,
coreBodyTarget.addLegalDialect<affine::AffineDialect,
PimDialect,
tensor::TensorDialect,
arith::ArithDialect,
bufferization::BufferizationDialect,
@@ -226,7 +225,8 @@ void onnx_mlir::raptor::SpatialToPimPass::runOnOperation() {
eraseUnusedTensorPackingOps(funcOp, rewriter);
ConversionTarget communicationTarget(*ctx);
communicationTarget.addLegalDialect<PimDialect,
communicationTarget.addLegalDialect<affine::AffineDialect,
PimDialect,
tensor::TensorDialect,
arith::ArithDialect,
bufferization::BufferizationDialect,
@@ -3,8 +3,6 @@
#include "src/Accelerators/PIM/Common/IR/AddressAnalysis.hpp"
#include "src/Accelerators/PIM/Common/PimCommon.hpp"
#include "src/Accelerators/PIM/Common/Support/CheckedArithmetic.hpp"
#include "src/Accelerators/PIM/Dialect/Pim/PimOps.hpp"
#include "src/Accelerators/PIM/Dialect/Pim/Transforms/Bufferization/BufferizationUtils.hpp"
#include "src/Accelerators/PIM/Dialect/Pim/Transforms/Bufferization/Common.hpp"
@@ -25,25 +23,8 @@ FailureOr<Value> materializeContiguousInputMemRef(Value memrefValue,
auto shapedType = cast<ShapedType>(memrefValue.getType());
auto contiguousType = MemRefType::get(shapedType.getShape(), shapedType.getElementType());
Value contiguousBuffer = memref::AllocOp::create(rewriter, loc, contiguousType);
auto sizeInBytes =
getCheckedShapedTypeSizeInBytes(shapedType, contiguousBuffer.getDefiningOp(), "contiguous copy byte size");
if (failed(sizeInBytes))
return failure();
Value zeroOffset = getOrCreateIndexConstant(rewriter, contiguousBuffer.getDefiningOp(), 0);
auto sizeAttr =
getCheckedI32Attr(rewriter, contiguousBuffer.getDefiningOp(), *sizeInBytes, "contiguous copy byte size");
if (failed(sizeAttr))
return failure();
if (isHostBackedPimAddress(memrefValue, knowledge)) {
return PimMemCopyHostToDevOp::create(
rewriter, loc, contiguousType, zeroOffset, zeroOffset, contiguousBuffer, memrefValue, *sizeAttr)
.getOutput();
}
return PimMemCopyOp::create(
rewriter, loc, contiguousType, zeroOffset, zeroOffset, contiguousBuffer, memrefValue, *sizeAttr)
.getOutput();
memref::CopyOp::create(rewriter, loc, memrefValue, contiguousBuffer);
return contiguousBuffer;
}
Value allocateContiguousResultMemRefLike(Value memrefValue, Location loc, RewriterBase& rewriter) {
@@ -1,14 +1,19 @@
#include "mlir/Dialect/Bufferization/IR/Bufferization.h"
#include "mlir/Dialect/Bufferization/Transforms/OneShotAnalysis.h"
#include "mlir/Dialect/Bufferization/Transforms/OneShotModuleBufferize.h"
#include "mlir/Dialect/Bufferization/Transforms/Transforms.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/Func/IR/FuncOps.h"
#include "mlir/Dialect/MemRef/IR/MemRef.h"
#include "mlir/Dialect/SCF/IR/SCF.h"
#include "mlir/Dialect/Tensor/IR/Tensor.h"
#include "mlir/IR/Dominance.h"
#include "mlir/IR/PatternMatch.h"
#include "mlir/Pass/Pass.h"
#include "mlir/Rewrite/PatternApplicator.h"
#include "llvm/ADT/SmallPtrSet.h"
#include "llvm/ADT/SmallSet.h"
#include "llvm/Support/Casting.h"
#include "Common/PimCommon.hpp"
@@ -116,33 +121,57 @@ lowerMemRefCopyToPimCopy(memref::CopyOp copyOp, PatternRewriter& rewriter, const
return success();
}
static LogicalResult verifyLoweredPimCopy(pim::PimMemCopyHostToDevOp copyOp, const StaticValueKnowledge& knowledge) {
static LogicalResult verifyLoweredPimCopy(pim::PimMemCopyHostToDevOp copyOp,
const StaticValueKnowledge& knowledge,
bool emitDiagnostic) {
bool sourceIsHost = isHostBackedPimAddress(copyOp.getHostSource(), knowledge);
bool targetIsHost = isHostBackedPimAddress(copyOp.getDeviceTarget(), knowledge);
bool sourceIsDevice = isDeviceLocalPimAddress(copyOp.getHostSource(), knowledge);
bool targetIsDevice = isDeviceLocalPimAddress(copyOp.getDeviceTarget(), knowledge);
if (!sourceIsHost || !targetIsDevice || targetIsHost || sourceIsDevice)
return copyOp.emitOpError("pim.memcp_hd requires a host-backed source and a device-local target");
if (!sourceIsHost || !targetIsDevice || targetIsHost || sourceIsDevice) {
if (emitDiagnostic)
copyOp.emitOpError() << "pim.memcp_hd requires a host-backed source and a device-local target: source="
<< copyOp.getHostSource() << " host=" << sourceIsHost << " device=" << sourceIsDevice
<< ", target=" << copyOp.getDeviceTarget() << " host=" << targetIsHost
<< " device=" << targetIsDevice;
return failure();
}
return success();
}
static LogicalResult verifyLoweredPimCopy(pim::PimMemCopyDevToHostOp copyOp, const StaticValueKnowledge& knowledge) {
static LogicalResult verifyLoweredPimCopy(pim::PimMemCopyDevToHostOp copyOp,
const StaticValueKnowledge& knowledge,
bool emitDiagnostic) {
bool sourceIsHost = isHostBackedPimAddress(copyOp.getDeviceSource(), knowledge);
bool targetIsHost = isHostBackedPimAddress(copyOp.getHostTarget(), knowledge);
bool sourceIsDevice = isDeviceLocalPimAddress(copyOp.getDeviceSource(), knowledge);
bool targetIsDevice = isDeviceLocalPimAddress(copyOp.getHostTarget(), knowledge);
if (!targetIsHost || !sourceIsDevice || sourceIsHost || targetIsDevice)
return copyOp.emitOpError("pim.memcp_dh requires a device-local source and a host-backed target");
if (!targetIsHost || !sourceIsDevice || sourceIsHost || targetIsDevice) {
if (emitDiagnostic)
copyOp.emitOpError() << "pim.memcp_dh requires a device-local source and a host-backed target: source="
<< copyOp.getDeviceSource() << " host=" << sourceIsHost << " device=" << sourceIsDevice
<< ", target=" << copyOp.getHostTarget() << " host=" << targetIsHost
<< " device=" << targetIsDevice;
return failure();
}
return success();
}
static LogicalResult verifyLoweredPimCopy(pim::PimMemCopyOp copyOp, const StaticValueKnowledge& knowledge) {
static LogicalResult verifyLoweredPimCopy(pim::PimMemCopyOp copyOp,
const StaticValueKnowledge& knowledge,
bool emitDiagnostic) {
bool sourceIsHost = isHostBackedPimAddress(copyOp.getSource(), knowledge);
bool targetIsHost = isHostBackedPimAddress(copyOp.getTarget(), knowledge);
bool sourceIsDevice = isDeviceLocalPimAddress(copyOp.getSource(), knowledge);
bool targetIsDevice = isDeviceLocalPimAddress(copyOp.getTarget(), knowledge);
if (!sourceIsDevice || !targetIsDevice || sourceIsHost || targetIsHost)
return copyOp.emitOpError("pim.memcp requires device-local source and target operands");
if (!sourceIsDevice || !targetIsDevice || sourceIsHost || targetIsHost) {
if (emitDiagnostic)
copyOp.emitOpError() << "pim.memcp requires device-local source and target operands: source="
<< copyOp.getSource() << " host=" << sourceIsHost << " device=" << sourceIsDevice
<< ", target=" << copyOp.getTarget() << " host=" << targetIsHost
<< " device=" << targetIsDevice;
return failure();
}
return success();
}
@@ -170,6 +199,109 @@ static LogicalResult applyPatternsOnce(Operation* op, PatternApplicator& applica
return applicator.matchAndRewrite(op, rewriter);
}
static void materializeWritableConstantDestinations(func::FuncOp funcOp) {
SmallVector<OpOperand*> constantBackedRoots;
llvm::SmallPtrSet<OpOperand*, 8> seenRoots;
auto collect = [&](Operation* coreOp) {
coreOp->walk([&](tensor::InsertSliceOp insert) {
OpOperand* root = &insert.getDestMutable();
Value value = root->get();
llvm::SmallDenseSet<Value, 4> visited;
while (visited.insert(value).second) {
if (value.getDefiningOp<arith::ConstantOp>()) {
if (seenRoots.insert(root).second)
constantBackedRoots.push_back(root);
break;
}
auto argument = dyn_cast<BlockArgument>(value);
auto loop = argument
? dyn_cast_or_null<scf::ForOp>(argument.getOwner()->getParentOp())
: scf::ForOp();
if (!loop || argument.getArgNumber() == 0)
break;
auto initArgs = loop.getInitArgsMutable();
root = &*(initArgs.begin() + argument.getArgNumber() - 1);
value = root->get();
}
});
};
funcOp.walk([&](pim::PimCoreOp coreOp) { collect(coreOp); });
funcOp.walk([&](pim::PimCoreBatchOp coreOp) { collect(coreOp); });
for (OpOperand* root : constantBackedRoots) {
OpBuilder builder(root->getOwner());
auto allocation = bufferization::AllocTensorOp::create(
builder, root->getOwner()->getLoc(), cast<RankedTensorType>(root->get().getType()),
ValueRange {}, root->get());
root->set(allocation.getResult());
}
}
static LogicalResult verifyConflictFreePimCoreWrites(
func::FuncOp funcOp, const bufferization::OneShotBufferizationOptions& options) {
bufferization::AnalysisState analysisState(options);
DominanceInfo dominance(funcOp);
size_t violationCount = 0;
auto verifyCore = [&](Operation* coreOp) {
coreOp->walk([&](Operation* writeOp) {
for (OpOperand& write : writeOp->getOpOperands()) {
if (!isa<TensorType>(write.get().getType()) || !analysisState.bufferizesToMemoryWrite(write))
continue;
SmallVector<Value, 8> worklist {write.get()};
llvm::SmallDenseSet<Value, 8> visited;
bool hasConflict = false;
Value conflictingAlias;
Operation* conflictingUse = nullptr;
while (!worklist.empty() && !hasConflict) {
Value alias = worklist.pop_back_val();
if (!visited.insert(alias).second)
continue;
for (OpOperand& use : alias.getUses()) {
bool usePrecedesWrite = false;
for (Operation* ancestor = use.getOwner(); ancestor; ancestor = ancestor->getParentOp())
if (ancestor == writeOp || dominance.properlyDominates(ancestor, writeOp)) {
usePrecedesWrite = true;
break;
}
if (usePrecedesWrite || analysisState.insideMutuallyExclusiveRegions(use.getOwner(), writeOp))
continue;
hasConflict = true;
conflictingAlias = alias;
conflictingUse = use.getOwner();
break;
}
if (alias.getDefiningOp<tensor::ExtractSliceOp>()
&& llvm::all_of(alias.getUses(), [&](OpOperand& use) {
return use.getOwner() == writeOp;
}))
continue;
if (isa<OpResult>(alias))
for (bufferization::AliasingOpOperand tied : analysisState.getAliasingOpOperands(alias).getAliases())
worklist.push_back(tied.opOperand->get());
}
if (!hasConflict)
continue;
if (violationCount++ == 0)
writeOp->emitOpError() << "PIM core tensor write may modify an alias used later: operand #"
<< write.getOperandNumber() << " (" << write.get() << "), alias="
<< conflictingAlias << ", later use=" << *conflictingUse;
}
});
};
funcOp.walk([&](pim::PimCoreOp coreOp) { verifyCore(coreOp); });
funcOp.walk([&](pim::PimCoreBatchOp coreOp) { verifyCore(coreOp); });
if (violationCount != 0)
funcOp.emitError() << "found " << violationCount
<< " non-linear PIM tensor write(s); the first is reported above";
return success(violationCount == 0);
}
} // namespace
void PimBufferizationPass::runOnOperation() {
@@ -180,9 +312,27 @@ void PimBufferizationPass::runOnOperation() {
options.allowUnknownOps = true;
options.bufferizeFunctionBoundaries = true;
options.setFunctionBoundaryTypeConversion(bufferization::LayoutMapOption::IdentityLayoutMap);
bufferization::BufferizationState state;
if (failed(bufferization::runOneShotModuleBufferize(moduleOp, options, state))) {
materializeWritableConstantDestinations(funcOp);
if (failed(verifyConflictFreePimCoreWrites(funcOp, options))) {
signalPassFailure();
return;
}
auto hostOptions = options;
hostOptions.opFilter.denyOperation([](Operation *op) {
return op->getParentOfType<pim::PimCoreOp>()
|| op->getParentOfType<pim::PimCoreBatchOp>();
});
bufferization::BufferizationState state;
if (failed(bufferization::insertTensorCopies(
moduleOp, hostOptions, state))) {
moduleOp.emitError("Failed to bufferize PIM and Spatial ops");
signalPassFailure();
return;
}
if (failed(bufferization::bufferizeModuleOp(
moduleOp, options, state))) {
moduleOp.emitError("Failed to bufferize PIM and Spatial ops");
signalPassFailure();
return;
@@ -393,18 +543,19 @@ LogicalResult PimBufferizationPass::verifyContiguousRuntimeOperands(ModuleOp mod
}
LogicalResult PimBufferizationPass::verifyPimCopyAddressSpaces(ModuleOp moduleOp) const {
bool hasFailure = false;
size_t failureCount = 0;
auto verifyWithKnowledge = [&](auto coreLikeOp, const StaticValueKnowledge& initialKnowledge) {
(void) walkPimCoreBlockStructurally(
coreLikeOp.getBody().front(), initialKnowledge, [&](Operation& op, const StaticValueKnowledge& knowledge) {
if (auto copyOp = dyn_cast<pim::PimMemCopyOp>(&op); copyOp && failed(verifyLoweredPimCopy(copyOp, knowledge)))
hasFailure = true;
if (auto copyOp = dyn_cast<pim::PimMemCopyOp>(&op);
copyOp && failed(verifyLoweredPimCopy(copyOp, knowledge, failureCount == 0)))
++failureCount;
if (auto copyOp = dyn_cast<pim::PimMemCopyHostToDevOp>(&op);
copyOp && failed(verifyLoweredPimCopy(copyOp, knowledge)))
hasFailure = true;
copyOp && failed(verifyLoweredPimCopy(copyOp, knowledge, failureCount == 0)))
++failureCount;
if (auto copyOp = dyn_cast<pim::PimMemCopyDevToHostOp>(&op);
copyOp && failed(verifyLoweredPimCopy(copyOp, knowledge)))
hasFailure = true;
copyOp && failed(verifyLoweredPimCopy(copyOp, knowledge, failureCount == 0)))
++failureCount;
return success();
});
};
@@ -414,7 +565,10 @@ LogicalResult PimBufferizationPass::verifyPimCopyAddressSpaces(ModuleOp moduleOp
StaticValueKnowledge knowledge = seedCoreBatchKnowledge(coreBatchOp, 0);
verifyWithKnowledge(coreBatchOp, knowledge);
});
return success(!hasFailure);
if (failureCount != 0)
moduleOp.emitError() << "found " << failureCount
<< " PIM copy address-space violation(s); the first is reported above";
return success(failureCount == 0);
}
std::unique_ptr<Pass> createPimBufferizationPass() { return std::make_unique<PimBufferizationPass>(); }
+2
View File
@@ -20,11 +20,13 @@ add_pim_library(SpatialOps
Transforms/MergeComputeNodes/DeferredCommunicationRealization.cpp
Transforms/MergeComputeNodes/MergeComputeNodesPass.cpp
Transforms/MergeComputeNodes/ScheduledComputeMaterialization.cpp
Transforms/MergeComputeNodes/ScheduledComputePlanning.cpp
Transforms/MergeComputeNodes/ScheduledComputeReport.cpp
Transforms/MergeComputeNodes/ScheduledComputeVerification.cpp
Transforms/MergeComputeNodes/SpatialDataflowCsvExporter.cpp
Transforms/MergeComputeNodes/Scheduling/MergeSchedulingAnalysis.cpp
Transforms/MergeComputeNodes/Scheduling/PeftScheduler.cpp
Transforms/TrivialGraphComputeMergePass.cpp
EXCLUDE_FROM_OM_LIBS
+19 -1
View File
@@ -49,6 +49,7 @@ class SpatComputeLikeBase<string mnemonic> : SpatOp<mnemonic,
}
def SpatGraphCompute : SpatComputeLikeBase<"graph_compute"> {
let hasCanonicalizer = 1;
let extraClassDeclaration = [{
std::optional<::mlir::BlockArgument> getWeightArgument(unsigned idx);
std::optional<::mlir::BlockArgument> getInputArgument(unsigned idx);
@@ -186,7 +187,8 @@ def SpatDeferredCommunicationOp : SpatOp<"deferred_communication", [SingleBlock]
let summary = "Temporary scheduled payload derivation placeholder";
let arguments = (ins
Variadic<SpatTensor>:$sources
Variadic<SpatTensor>:$sources,
OptionalAttr<I64Attr>:$specialization_count
);
let results = (outs
@@ -199,6 +201,22 @@ def SpatDeferredCommunicationOp : SpatOp<"deferred_communication", [SingleBlock]
let hasCustomAssemblyFormat = 1;
}
def SpatDeferredSourceSelectOp : SpatOp<"deferred_source_select", []> {
let summary = "Select a deferred tensor source with a statically analyzable index";
let arguments = (ins
Index:$selector,
Variadic<SpatTensor>:$sources
);
let results = (outs
SpatTensor:$output
);
let hasVerifier = 1;
let hasCustomAssemblyFormat = 1;
}
def SpatExtractRowsOp : SpatOp<"extract_rows", []> {
let summary = "Extract every row of a rank-2 tensor as separate rank-2 row tensors";
+51 -10
View File
@@ -457,39 +457,80 @@ void SpatDeferredCommunicationOp::print(OpAsmPrinter& printer) {
printCompressedValueSequence(printer, getSources());
printer.printOptionalAttrDict((*this)->getAttrs());
printer << " : ";
printer.printFunctionalType(getSources().getTypes(), getOperation()->getResultTypes());
printCompressedTypeList(
printer, getSources().getTypes(), ListDelimiter::Paren);
printer << " -> ";
printCompressedTypeSequence(printer, getOperation()->getResultTypes());
printer << " ";
printer.printRegion(getBody(), /*printEntryBlockArgs=*/false);
}
ParseResult SpatDeferredCommunicationOp::parse(OpAsmParser& parser, OperationState& result) {
SmallVector<OpAsmParser::UnresolvedOperand> sources;
Type functionTypeStorage;
SmallVector<Type> sourceTypes, outputTypes;
if (parseCompressedOperandSequence(parser, sources) || parser.parseOptionalAttrDict(result.attributes)
|| parser.parseColon() || parser.parseType(functionTypeStorage))
|| parser.parseColon()
|| parseCompressedRepeatedList(
parser, ListDelimiter::Paren, sourceTypes,
[&](Type& type) { return parser.parseType(type); })
|| parser.parseArrow()
|| parseCompressedTypeSequence(
parser, outputTypes, /*allowEmpty=*/false))
return failure();
auto functionType = dyn_cast<FunctionType>(functionTypeStorage);
if (!functionType)
return parser.emitError(parser.getCurrentLocation(), "expected deferred communication function type");
if (sources.size() != functionType.getNumInputs())
if (sources.size() != sourceTypes.size())
return parser.emitError(parser.getCurrentLocation(), "number of sources and source types must match");
if (parser.resolveOperands(sources, functionType.getInputs(), parser.getCurrentLocation(), result.operands))
if (parser.resolveOperands(sources, sourceTypes, parser.getCurrentLocation(), result.operands))
return failure();
result.addTypes(functionType.getResults());
result.addTypes(outputTypes);
Region* body = result.addRegion();
SmallVector<OpAsmParser::Argument> bodyArgs;
for (Type type : functionType.getInputs()) {
for (Type type : sourceTypes) {
OpAsmParser::Argument argument;
argument.type = type;
bodyArgs.push_back(argument);
}
if (auto count = dyn_cast_or_null<IntegerAttr>(
result.attributes.get("specialization_count"));
count && count.getInt() > 1) {
OpAsmParser::Argument argument;
argument.type = parser.getBuilder().getIndexType();
bodyArgs.push_back(argument);
}
return parser.parseRegion(*body, bodyArgs);
}
void SpatDeferredSourceSelectOp::print(OpAsmPrinter& printer) {
printer << " " << getSelector() << " of ";
printCompressedValueSequence(printer, getSources());
printer.printOptionalAttrDict((*this)->getAttrs());
printer << " : " << getOutput().getType();
}
ParseResult SpatDeferredSourceSelectOp::parse(
OpAsmParser& parser, OperationState& result) {
OpAsmParser::UnresolvedOperand selector;
SmallVector<OpAsmParser::UnresolvedOperand> sources;
Type outputType;
if (parser.parseOperand(selector) || parser.parseKeyword("of")
|| parseCompressedOperandSequence(parser, sources)
|| parser.parseOptionalAttrDict(result.attributes)
|| parser.parseColon() || parser.parseType(outputType))
return failure();
if (parser.resolveOperand(selector, parser.getBuilder().getIndexType(),
result.operands))
return failure();
SmallVector<Type> sourceTypes(sources.size(), outputType);
if (parser.resolveOperands(sources, sourceTypes,
parser.getCurrentLocation(), result.operands))
return failure();
result.addTypes(outputType);
return success();
}
void SpatExtractRowsOp::print(OpAsmPrinter& printer) {
printer << " ";
printer.printOperand(getInput());
@@ -42,6 +42,33 @@ LogicalResult SpatGraphCompute::fold(FoldAdaptor adaptor, ::llvm::SmallVectorImp
return foldComputeLike(*this, results);
}
struct RemoveUnusedGraphComputeInputsPattern : OpRewritePattern<SpatGraphCompute> {
using OpRewritePattern::OpRewritePattern;
LogicalResult matchAndRewrite(SpatGraphCompute compute, PatternRewriter& rewriter) const override {
SmallVector<unsigned> unusedInputs;
for (unsigned index = 0; index < compute.getInputs().size(); ++index) {
auto argument = compute.getInputArgument(index);
if (argument && argument->use_empty())
unusedInputs.push_back(index);
}
if (unusedInputs.empty())
return failure();
rewriter.modifyOpInPlace(compute, [&] {
for (unsigned index : llvm::reverse(unusedInputs)) {
compute.getBody().front().eraseArgument(compute.getWeights().size() + index);
compute.getInputsMutable().erase(index);
}
});
return success();
}
};
void SpatGraphCompute::getCanonicalizationPatterns(RewritePatternSet& results, MLIRContext* context) {
results.add<RemoveUnusedGraphComputeInputsPattern>(context);
}
LogicalResult SpatScheduledCompute::fold(FoldAdaptor adaptor, ::llvm::SmallVectorImpl<::mlir::OpFoldResult>& results) {
return foldComputeLike(*this, results);
}
+61 -39
View File
@@ -225,33 +225,6 @@ static LogicalResult verifyOnlyConstantExternalValues(Operation* ownerOp, Region
return success(!hasFailure);
}
static LogicalResult verifyYieldTypes(Operation* op, Region& region, TypeRange resultTypes, StringRef kind) {
if (region.empty())
return op->emitOpError() << kind << " requires a body block";
Block& block = region.front();
auto yield = dyn_cast_or_null<SpatYieldOp>(block.getTerminator());
if (!yield)
return op->emitOpError() << kind << " body must terminate with spat.yield";
if (yield.getOutputs().size() != resultTypes.size())
return op->emitOpError() << kind << " yield operand count must match result count";
for (auto [yieldType, resultType] : llvm::zip(yield.getOutputs().getTypes(), resultTypes))
if (yieldType != resultType)
return op->emitOpError() << kind << " yield operand types must match result types";
return success();
}
static LogicalResult verifyRegionArguments(Operation* op, Region& region, ValueRange operands, StringRef kind) {
if (region.empty())
return op->emitOpError() << kind << " requires a body block";
Block& block = region.front();
if (block.getNumArguments() != operands.size())
return op->emitOpError() << kind << " body argument count must match operand count";
for (auto [arg, operand] : llvm::zip(block.getArguments(), operands))
if (arg.getType() != operand.getType())
return op->emitOpError() << kind << " body argument types must match operand types";
return success();
}
template <typename ComputeBatchOpTy>
static LogicalResult verifyBatchBody(ComputeBatchOpTy batchOp, Block& block, bool verifyLaneSliceOffsets = true) {
if (batchOp.getNumResults() == 0) {
@@ -741,11 +714,12 @@ LogicalResult verifyComputeLikeOp(ComputeOpTy compute, StringRef opName) {
bool isScheduled = isa<SpatScheduledCompute>(compute.getOperation());
if (compute.getBody().empty())
return compute.emitOpError("compute body must have at least one block");
if (isScheduled && !compute.getBody().hasOneBlock())
return compute.emitOpError("scheduled compute must have exactly one block");
SmallVector<Type> yieldedTypes;
for (Block& block : compute.getBody()) {
if ((!isScheduled && block.getNumArguments() != expectedArgCount)
|| (isScheduled && block.getNumArguments() < expectedArgCount))
for (Block &block : compute.getBody()) {
if (block.getNumArguments() != expectedArgCount)
return compute.emitOpError("compute body must have weight and input block arguments");
for (auto [weightIndex, weight] : llvm::enumerate(compute.getWeights()))
@@ -757,17 +731,13 @@ LogicalResult verifyComputeLikeOp(ComputeOpTy compute, StringRef opName) {
Operation* terminator = block.getTerminator();
if (auto yieldOp = dyn_cast_or_null<SpatYieldOp>(terminator)) {
auto realized = compute->template getAttrOfType<BoolAttr>("scheduled.realized");
if (isScheduled && (!realized || !realized.getValue() || !compute.getBody().hasOneBlock()))
return compute.emitOpError("scheduled compute blocks must terminate with spat.block_yield");
llvm::append_range(yieldedTypes, yieldOp->getOperandTypes());
continue;
}
auto blockYield = dyn_cast_or_null<SpatBlockYieldOp>(terminator);
if (!blockYield || !isScheduled)
if (!blockYield || isScheduled)
return compute.emitOpError("ComputeOp must have a single yield operation");
if (blockYield->getNumSuccessors() == 0)
llvm::append_range(yieldedTypes, blockYield->getOperandTypes());
llvm::append_range(yieldedTypes, blockYield->getOperandTypes());
}
auto resultTypes = compute.getResultTypes();
@@ -832,6 +802,11 @@ LogicalResult SpatBlockYieldOp::verify() {
LogicalResult SpatDeferredCommunicationOp::verify() {
if (getSources().empty())
return emitOpError("requires at least one source");
auto specialization = (*this)->getAttrOfType<IntegerAttr>(
"specialization_count");
int64_t specializationCount = specialization ? specialization.getInt() : 1;
if (specializationCount <= 0)
return emitOpError("specialization_count must be positive");
static constexpr StringLiteral staleAttributes[] = {
"exchangeId", "logicalProducer", "logicalConsumer", "sourceClass", "targetClass", "sourceCore",
"targetCore", "sourceLane", "targetLane", "transferKind", "resultIndex", "projectedTransfer",
@@ -842,9 +817,56 @@ LogicalResult SpatDeferredCommunicationOp::verify() {
if (getOperation()->hasAttr(name))
return emitOpError() << "does not accept stale routing attribute '" << name
<< "'; source selection and shaping belong in the body and routing is derived in Phase 2";
if (failed(verifyRegionArguments(getOperation(), getBody(), getSources(), "spat.deferred_communication")))
return failure();
return verifyYieldTypes(getOperation(), getBody(), getOperation()->getResultTypes(), "spat.deferred_communication");
if (getBody().empty())
return emitOpError("spat.deferred_communication requires a body block");
Block &body = getBody().front();
unsigned expectedArguments = getSources().size()
+ (specializationCount > 1 ? 1 : 0);
if (body.getNumArguments() != expectedArguments)
return emitOpError("body argument count must match sources plus the grouped specialization argument");
for (auto [argument, source] : llvm::zip(
body.getArguments().take_front(getSources().size()), getSources()))
if (argument.getType() != source.getType())
return emitOpError("body source argument types must match source operand types");
if (specializationCount > 1
&& !body.getArguments().back().getType().isIndex())
return emitOpError("grouped specialization argument must have index type");
auto yield = dyn_cast_or_null<SpatYieldOp>(body.getTerminator());
if (!yield || yield.getOutputs().size() != 1)
return emitOpError("body must yield exactly one fragment");
Type fragmentType = yield.getOutputs().front().getType();
Type outputType = getOutput().getType();
if (specializationCount == 1)
return fragmentType == outputType
? success()
: emitOpError("ordinary deferred yield type must match its output type");
auto fragmentTensor = dyn_cast<RankedTensorType>(fragmentType);
auto outputTensor = dyn_cast<RankedTensorType>(outputType);
if (!fragmentTensor || !outputTensor || !fragmentTensor.hasStaticShape()
|| !outputTensor.hasStaticShape())
return emitOpError("grouped specialization requires static ranked tensor types");
if (outputTensor.getRank() != fragmentTensor.getRank() + 1
|| outputTensor.getDimSize(0) != specializationCount
|| outputTensor.getShape().drop_front() != fragmentTensor.getShape()
|| outputTensor.getElementType() != fragmentTensor.getElementType())
return emitOpError("grouped output must have shape specialization_count x fragment shape");
return success();
}
LogicalResult SpatDeferredSourceSelectOp::verify() {
if (getSources().empty())
return emitOpError("requires at least one source");
if (!getSelector().getType().isIndex())
return emitOpError("requires an index selector");
if (llvm::any_of(getSources(), [&](Value source) {
return source.getType() != getOutput().getType();
}))
return emitOpError("source and output types must match");
if (!getOperation()->getParentOfType<SpatDeferredCommunicationOp>()
&& !getOperation()->getParentOfType<SpatScheduledCompute>()
&& !getOperation()->getParentOfType<SpatScheduledComputeBatch>())
return emitOpError("must be nested in deferred or scheduled computation");
return success();
}
template <typename ComputeBatchOpTy>
@@ -1,709 +1,313 @@
#include "mlir/Dialect/SCF/IR/SCF.h"
#include "llvm/ADT/MapVector.h"
#include "DeferredBoundaryPlanning.hpp"
#include "DeferredCommunicationScheduling.hpp"
#include "DeferredProjectionAnalysis.hpp"
#include "DeferredTransferPlanning.hpp"
namespace onnx_mlir::spatial {
using namespace mlir;
namespace {
enum class BoundaryEventKind { Send, Receive };
static BoundaryProgram &getBoundary(
SmallVectorImpl<BoundaryProgram> &boundaries,
DenseMap<BoundaryKey, unsigned> &indices, BoundaryKey key) {
auto [it, inserted] = indices.try_emplace(key, boundaries.size());
if (inserted)
boundaries.push_back({key, {}});
return boundaries[it->second];
}
struct BoundaryEvent {
BoundaryEventKind kind = BoundaryEventKind::Send;
ScheduledTransferSlice slice;
LaneSet activeLanes;
TransferEmissionSignature emission;
};
static LaneSet getReceiveLanes(const ScheduledTransferSlice &slice) {
LaneInterval family = slice.family->targetLanes.intervals().front();
unsigned begin = family.begin + slice.familyOffset;
return LaneSet::range(begin, begin + slice.transferCount);
}
struct SequenceNode {
unsigned previous = 0;
unsigned instruction = 0;
};
static void appendSend(BoundaryProgram &boundary,
const ScheduledTransferSlice &slice) {
unsigned lane = slice.family->requirement->producer->scheduledLane;
LaneSet lanes = LaneSet::range(lane, lane + 1);
if (!boundary.instructions.empty())
if (auto *run = std::get_if<EmitSendRun>(&boundary.instructions.back());
run && haveSameTransferEmissionSignature(
*run->slices.back().family, *slice.family)) {
run->slices.push_back(slice);
run->lanes = run->lanes.unite(lanes);
return;
}
boundary.instructions.push_back(EmitSendRun {{slice}, lanes});
}
struct IntervalClass {
LaneSet lanes;
unsigned sequence = 0;
};
static LogicalResult addCoverage(
RequirementFamily &requirement, const LaneSet &lanes,
DenseMap<RequirementFamily *, LaneSet> &coverage) {
LaneSet &covered = coverage[&requirement];
if (!covered.intersect(lanes).empty())
return requirement.exchange->deferred.emitOpError(
"deferred availability covers a target lane more than once");
covered = covered.unite(lanes);
return success();
}
enum class SemanticKind {
Send,
Availability,
Result
};
static bool canGroupLocalAvailability(RequirementFamily &lhs,
RequirementFamily &rhs) {
if (!(lhs.coordinate == rhs.coordinate)
|| lhs.publicationFragmentType != rhs.publicationFragmentType
|| lhs.producer->payload != rhs.producer->payload
|| lhs.producerProjection.has_value()
!= rhs.producerProjection.has_value())
return false;
if (!lhs.producerProjection)
return true;
auto ranks = [](const DeferredStaticSliceGeometry &geometry) {
return std::tuple(geometry.offsets.size(), geometry.sizes.size(),
geometry.strides.size());
};
return ranks(*lhs.producerProjection) == ranks(*rhs.producerProjection);
}
struct SemanticKey {
SemanticKind kind = SemanticKind::Send;
TransferEmissionSignature emission;
DeferredExchangePlan* exchange = nullptr;
RequirementCoordinate coordinate;
Type fragmentType;
bool operator==(const SemanticKey& other) const {
if (kind != other.kind)
return false;
if (kind == SemanticKind::Send)
return emission == other.emission;
if (kind == SemanticKind::Result)
return exchange == other.exchange;
return exchange == other.exchange && coordinate == other.coordinate
&& fragmentType == other.fragmentType;
}
};
struct PendingToken {
LaneSet lanes;
unsigned semantic = 0;
std::variant<unsigned, LocalAvailabilityFamily*, DeferredExchangePlan*> value;
};
struct SequenceCursor {
LaneSet lanes;
struct CollectionTarget {
const FragmentCollectionPlan *collection = nullptr;
unsigned position = 0;
};
struct CanonicalAction {
SemanticKey key;
SmallVector<ScheduledTransferSlice> slices;
SmallVector<LocalAvailabilityFamily*> locals;
LaneSet instructionLanes;
LaneSet receiveLanes;
LaneSet localLanes;
DeferredExchangePlan* result = nullptr;
};
struct BoundaryWork {
BoundaryProgram program;
SmallVector<BoundaryEvent> events;
};
static unsigned getBoundaryIndex(SmallVectorImpl<BoundaryWork>& boundaries,
DenseMap<BoundaryKey, unsigned>& indices,
BoundaryKey key) {
if (auto it = indices.find(key); it != indices.end())
return it->second;
unsigned index = boundaries.size();
indices[key] = index;
BoundaryWork work;
work.program.key = key;
boundaries.push_back(std::move(work));
return index;
}
static size_t hashSemanticKey(const SemanticKey& key) {
if (key.kind == SemanticKind::Send)
return llvm::hash_combine(key.kind,
key.emission.scheduled,
key.emission.payload.getAsOpaquePointer(),
key.emission.fragmentType.getAsOpaquePointer(),
key.emission.hasGraphLane,
key.emission.hasProducerProjection,
key.emission.sourceIsBatch);
if (key.kind == SemanticKind::Result)
return llvm::hash_combine(key.kind, key.exchange);
return llvm::hash_combine(key.kind, key.exchange,
key.coordinate.leafIndex,
key.coordinate.selectedPosition,
key.fragmentType.getAsOpaquePointer());
}
static unsigned internSemantic(const SemanticKey& key,
SmallVectorImpl<SemanticKey>& keys,
DenseMap<size_t, SmallVector<unsigned>>& byHash) {
size_t hash = hashSemanticKey(key);
for (unsigned candidate : byHash.lookup(hash))
if (keys[candidate] == key)
return candidate;
unsigned id = keys.size();
keys.push_back(key);
byHash[hash].push_back(id);
return id;
}
static SemanticKey getEventKey(const BoundaryEvent& event) {
if (event.kind == BoundaryEventKind::Send) {
SemanticKey key;
key.kind = SemanticKind::Send;
key.emission = event.emission;
return key;
}
RequirementFamily& requirement = *event.slice.family->requirement;
SemanticKey key;
key.kind = SemanticKind::Availability;
key.exchange = requirement.exchange;
key.coordinate = requirement.coordinate;
key.fragmentType = requirement.publicationFragmentType;
return key;
}
static SemanticKey getLocalKey(LocalAvailabilityFamily& local) {
RequirementFamily& requirement = *local.requirement;
SemanticKey key;
key.kind = SemanticKind::Availability;
key.exchange = requirement.exchange;
key.coordinate = requirement.coordinate;
key.fragmentType = requirement.publicationFragmentType;
return key;
}
static SmallVector<ScheduledTransferSlice> intersectReceiveSlice(const ScheduledTransferSlice& slice,
const LaneSet& lanes) {
LaneInterval family = slice.family->targetLanes.intervals().front();
unsigned sliceBegin = family.begin + slice.familyOffset;
LaneSet active = LaneSet::range(sliceBegin, sliceBegin + slice.transferCount).intersect(lanes);
SmallVector<ScheduledTransferSlice> result;
for (LaneInterval selected : active.intervals()) {
ScheduledTransferSlice part = slice;
part.familyOffset += selected.begin - sliceBegin;
part.transferCount = selected.end - selected.begin;
result.push_back(part);
}
return result;
}
static void mergeSequenceCursors(SmallVectorImpl<SequenceCursor>& states) {
DenseMap<unsigned, unsigned> byPosition;
SmallVector<SequenceCursor> merged;
for (SequenceCursor& state : states) {
auto [it, inserted] = byPosition.try_emplace(state.position, merged.size());
if (inserted)
merged.push_back(std::move(state));
else
merged[it->second].lanes = merged[it->second].lanes.unite(state.lanes);
}
states = std::move(merged);
}
static LogicalResult appendReplayToken(const PendingToken& token,
const LaneSet& lanes,
CanonicalAction& action,
ArrayRef<BoundaryEvent> events) {
if (auto eventId = std::get_if<unsigned>(&token.value)) {
const BoundaryEvent& event = events[*eventId];
if (event.kind == BoundaryEventKind::Send) {
LaneSet active = event.activeLanes.intersect(lanes);
if (!active.empty())
action.slices.push_back(event.slice);
action.instructionLanes =
action.instructionLanes.unite(active);
}
else {
llvm::append_range(action.slices, intersectReceiveSlice(event.slice, lanes));
action.receiveLanes = action.receiveLanes.unite(lanes);
}
return success();
}
if (auto local = std::get_if<LocalAvailabilityFamily*>(&token.value)) {
if (!llvm::is_contained(action.locals, *local))
action.locals.push_back(*local);
action.localLanes = action.localLanes.unite(lanes);
return success();
}
action.result = *std::get_if<DeferredExchangePlan*>(&token.value);
return success();
}
static FailureOr<SmallVector<CanonicalAction, 0>> collectCanonicalActions(
ArrayRef<unsigned> sequence, ArrayRef<PendingToken> tokens,
ArrayRef<SemanticKey> semantics, ArrayRef<BoundaryEvent> events,
const LaneSet& lanes) {
SmallVector<CanonicalAction, 0> actions;
for (unsigned semantic : sequence) {
CanonicalAction action;
action.key = semantics[semantic];
actions.push_back(std::move(action));
}
SmallVector<SequenceCursor> states {
{lanes, 0}
};
for (const PendingToken& token : tokens) {
SmallVector<SequenceCursor> next;
for (const SequenceCursor& state : states) {
LaneSet intersection = state.lanes.intersect(token.lanes);
LaneSet difference = state.lanes.subtract(token.lanes);
if (!difference.empty())
next.push_back({difference, state.position});
if (intersection.empty())
continue;
if (state.position == sequence.size()) {
next.push_back({intersection, state.position});
continue;
}
if (state.position >= sequence.size() || sequence[state.position] != token.semantic
|| failed(appendReplayToken(token, intersection, actions[state.position], events)))
return failure();
next.push_back({intersection, state.position + 1});
}
mergeSequenceCursors(next);
states = std::move(next);
}
if (llvm::any_of(states, [&](const SequenceCursor& state) { return state.position != sequence.size(); }))
return failure();
return actions;
}
static bool matchesAssembly(ArrayRef<CanonicalAction> actions, size_t begin, SmallVectorImpl<unsigned>& entryOrder) {
if (begin >= actions.size())
static bool sameCollectionEmissionContract(
const CollectionTarget &lhs, const CollectionTarget &rhs) {
if (lhs.collection != rhs.collection)
return false;
const CanonicalAction& first = actions[begin];
DeferredExchangePlan* exchange = first.key.exchange;
auto& assembly = exchange->program.insertAssembly;
if (first.key.kind != SemanticKind::Availability || !first.locals.empty()
|| !assembly || assembly->entries.empty()
|| begin + assembly->entries.size() > actions.size())
return false;
SmallVector<bool> matched(assembly->entries.size());
for (size_t offset = 0; offset < assembly->entries.size(); ++offset) {
const CanonicalAction& action = actions[begin + offset];
if (action.key.kind != SemanticKind::Availability || !action.locals.empty()
|| action.key.exchange != exchange || action.slices.empty())
return false;
std::optional<unsigned> entry;
for (auto [entryIndex, candidate] : llvm::enumerate(assembly->entries))
if (!matched[entryIndex] && action.key.coordinate == candidate.coordinate) {
entry = entryIndex;
break;
}
if (!entry)
return false;
matched[*entry] = true;
entryOrder.push_back(*entry);
}
if (assembly->entries.size() > 1
&& llvm::any_of(ArrayRef(assembly->entries).drop_front(), [&](const DeferredInsertAssemblyEntryTemplate& entry) {
return entry.sourceTransform != assembly->entries.front().sourceTransform
|| entry.sourceType != assembly->entries.front().sourceType;
}))
return false;
return true;
if (lhs.collection->key.kind != FragmentCollectionKind::InsertAssembly)
return true;
const auto &entries =
lhs.collection->key.exchange->program.insertAssembly->entries;
const auto &left = entries[lhs.position];
const auto &right = entries[rhs.position];
return left.sourceTransform == right.sourceTransform
&& left.sourceType == right.sourceType;
}
static bool matchesProjectionAssembly(ArrayRef<CanonicalAction> actions,
size_t begin,
const LaneSet& lanes,
unsigned& leafIndex,
SmallVectorImpl<unsigned>& positions) {
if (begin >= actions.size() || lanes.empty())
return false;
const CanonicalAction& first = actions[begin];
DeferredExchangePlan* exchange = first.key.exchange;
if (first.key.kind != SemanticKind::Availability || !first.locals.empty()
|| !exchange || exchange->program.insertAssembly
|| first.key.coordinate.leafIndex >= exchange->program.leaves.size())
return false;
leafIndex = first.key.coordinate.leafIndex;
const DeferredProjectionLeafTemplate& leaf = exchange->program.leaves[leafIndex];
if (leaf.form != DeferredLeafForm::DirectSource)
return false;
unsigned representative = lanes.intervals().front().begin;
unsigned positionCount = 0;
unsigned requirementCount = 0;
Type fragmentType;
for (RequirementFamily& requirement : exchange->requirements) {
if (requirement.coordinate.leafIndex != leafIndex || !requirement.targetLanes.contains(representative))
continue;
++requirementCount;
positionCount = std::max(positionCount, requirement.coordinate.selectedPosition + 1);
if (fragmentType && fragmentType != requirement.publicationFragmentType)
return false;
fragmentType = requirement.publicationFragmentType;
static std::optional<EmitLocalCollectionRun> buildLocalConcat(
const FragmentCollectionPlan &collection,
const DenseMap<RequirementFamily *, LocalAvailabilityFamily *> &locals,
unsigned targetLaneCount) {
RankedTensorType type = collection.collectionType;
if (collection.key.kind != FragmentCollectionKind::Leaf
|| targetLaneCount != 1 || type.getRank() == 0
|| collection.positionCount == 0
|| type.getDimSize(0) != collection.positionCount)
return std::nullopt;
SmallVector<RequirementFamily *> requirements(collection.positionCount);
for (const auto &entry : collection.requirements) {
if (entry.position >= requirements.size() || requirements[entry.position])
return std::nullopt;
requirements[entry.position] = entry.family;
}
auto fragment = dyn_cast<RankedTensorType>(fragmentType);
if (positionCount < 2 || requirementCount != positionCount || !fragment
|| leaf.reconstructedType.getRank() != fragment.getRank() + 1
|| leaf.reconstructedType.getDimSize(0) != positionCount
|| leaf.reconstructedType.getShape().drop_front() != fragment.getShape())
return false;
SmallVector<bool> seen(positionCount);
for (size_t offset = 0; begin + offset < actions.size(); ++offset) {
const CanonicalAction& action = actions[begin + offset];
if (action.key.kind != SemanticKind::Availability || !action.locals.empty()
|| action.key.exchange != exchange
|| action.key.coordinate.leafIndex != leafIndex || action.key.fragmentType != fragmentType)
break;
unsigned position = action.key.coordinate.selectedPosition;
if (action.slices.empty() || position >= positionCount || seen[position])
return false;
seen[position] = true;
positions.push_back(position);
}
if (positions.size() < 2)
return false;
for (RequirementFamily& requirement : exchange->requirements) {
if (requirement.coordinate.leafIndex != leafIndex || !requirement.targetLanes.contains(representative)
|| seen[requirement.coordinate.selectedPosition])
continue;
bool local = llvm::any_of(exchange->local, [&](const LocalAvailabilityFamily& availability) {
return availability.requirement == &requirement && availability.targetLanes.contains(representative);
});
if (!local)
return false;
}
return true;
}
static size_t getReceiveBundleLength(ArrayRef<CanonicalAction> actions,
size_t begin,
const LaneSet& lanes) {
if (begin >= actions.size())
return 0;
const CanonicalAction& first = actions[begin];
if (first.key.kind != SemanticKind::Availability)
return 0;
Type fragmentType = first.key.fragmentType;
auto rankedFragment = dyn_cast<RankedTensorType>(fragmentType);
if (!rankedFragment || !rankedFragment.hasStaticShape())
return 0;
Value firstOutput = first.key.exchange->deferred.getOutput();
if (!firstOutput.hasOneUse())
return 0;
Operation* firstUser = *firstOutput.getUsers().begin();
auto selection = firstUser->getParentOfType<scf::IndexSwitchOp>();
if (!selection)
return 0;
size_t end = begin;
while (end < actions.size()) {
const CanonicalAction& action = actions[end];
Value output = action.key.exchange
? action.key.exchange->deferred.getOutput()
: Value();
Operation* user = output && output.hasOneUse()
? *output.getUsers().begin()
: nullptr;
if (action.key.kind != SemanticKind::Availability || !action.locals.empty()
|| action.slices.empty() || !(action.receiveLanes == lanes)
|| action.key.fragmentType != fragmentType
|| !user || user->getParentOfType<scf::IndexSwitchOp>() != selection)
break;
++end;
}
return end - begin >= 2 ? end - begin : 0;
}
static BoundaryInstructionList materializeInstructions(ArrayRef<CanonicalAction> actions,
const LaneSet& lanes) {
BoundaryInstructionList result;
auto& instructions = result.instructions;
for (size_t index = 0; index < actions.size();) {
const CanonicalAction& action = actions[index];
if (action.key.kind == SemanticKind::Send) {
EmitSendRun run;
run.lanes = action.instructionLanes;
do {
llvm::append_range(run.slices, actions[index].slices);
run.lanes = run.lanes.unite(actions[index].instructionLanes);
++index;
}
while (index < actions.size() && actions[index].key.kind == SemanticKind::Send
&& actions[index].key.emission == action.key.emission);
if (!run.slices.empty())
instructions.push_back(std::move(run));
continue;
LaneSet all = LaneSet::all(targetLaneCount);
EmitLocalCollectionRun run {&collection, 0, {}, all, true};
Value payload;
int64_t payloadBegin = 0;
int64_t payloadEnd = 0;
for (auto [position, requirement] : llvm::enumerate(requirements)) {
if (!requirement)
return std::nullopt;
LocalAvailabilityFamily *local = locals.lookup(requirement);
if (!local || !(requirement->targetLanes == all)
|| !(local->targetLanes == all) || !requirement->graphLanes
|| requirement->graphLanes->size() != 1
|| requirement->graphLanes->valueAt(0) != static_cast<int64_t>(position)
|| !requirement->producerLocalOffsets
|| requirement->producerLocalOffsets->size() != 1)
return std::nullopt;
ProducedValue *producer = requirement->producer;
int64_t payloadOffset =
requirement->producerLocalOffsets->valueAt(0);
if (static_cast<int64_t>(position) == payloadEnd) {
if (!producer)
return std::nullopt;
auto payloadType = dyn_cast<RankedTensorType>(producer->payload.getType());
if (!payloadType || payloadOffset != 0
|| payloadType.getRank() != type.getRank()
|| payloadType.getElementType() != type.getElementType()
|| payloadType.getShape().drop_front() != type.getShape().drop_front())
return std::nullopt;
payloadBegin = position;
payloadEnd = payloadBegin + payloadType.getDimSize(0);
if (payloadEnd > collection.positionCount)
return std::nullopt;
payload = producer->payload;
run.families.push_back(local);
}
SmallVector<unsigned> assemblyEntries;
if (matchesAssembly(actions, index, assemblyEntries)) {
EmitReceiveAssemblyRun run;
run.lanes = lanes;
run.assemblyEntries = assemblyEntries;
run.entryOffsets.push_back(0);
for (size_t offset = 0; offset < assemblyEntries.size(); ++offset) {
llvm::append_range(run.slices, actions[index + offset].slices);
run.entryOffsets.push_back(run.slices.size());
}
instructions.push_back(std::move(run));
index += assemblyEntries.size();
continue;
}
unsigned projectionLeaf = 0;
SmallVector<unsigned> projectionPositions;
if (matchesProjectionAssembly(actions, index, lanes, projectionLeaf, projectionPositions)) {
EmitReceiveAssemblyRun run;
run.lanes = lanes;
run.projectionLeaf = projectionLeaf;
run.assemblyEntries = projectionPositions;
run.entryOffsets.push_back(0);
for (size_t offset = 0; offset < projectionPositions.size(); ++offset) {
llvm::append_range(run.slices, actions[index + offset].slices);
run.entryOffsets.push_back(run.slices.size());
}
instructions.push_back(std::move(run));
index += projectionPositions.size();
continue;
}
if (size_t bundleLength = getReceiveBundleLength(actions, index, lanes)) {
EmitReceiveBundle bundle;
for (size_t offset = 0; offset < bundleLength; ++offset) {
const CanonicalAction& entry = actions[index + offset];
EmitReceiveRun receive;
receive.slices = entry.slices;
receive.entryOffsets = {0, receive.slices.size()};
receive.lanes = entry.receiveLanes;
bundle.entries.push_back(std::move(receive));
}
instructions.push_back(std::move(bundle));
index += bundleLength;
continue;
}
if (action.key.kind == SemanticKind::Availability) {
ResolveAvailability availability;
availability.exchange = action.key.exchange;
availability.coordinate = action.key.coordinate;
availability.fragmentType = action.key.fragmentType;
if (!action.slices.empty()) {
EmitReceiveRun receive;
receive.slices = action.slices;
receive.entryOffsets = {0, receive.slices.size()};
receive.lanes = action.receiveLanes;
availability.alternatives.push_back(
{receive.lanes, AvailabilitySource(std::move(receive))});
}
if (!action.locals.empty()) {
MaterializeLocalFamily local {action.locals, action.localLanes};
availability.alternatives.push_back(
{local.lanes, AvailabilitySource(std::move(local))});
}
instructions.push_back(std::move(availability));
++index;
continue;
}
instructions.push_back(ProduceDeferredResult {action.result, lanes});
++index;
if (producer->payload != payload
|| payloadOffset != static_cast<int64_t>(position) - payloadBegin)
return std::nullopt;
}
return result;
return payloadEnd == collection.positionCount
? std::optional<EmitLocalCollectionRun>(std::move(run)) : std::nullopt;
}
static void addTokenToClasses(const LaneSet& active,
unsigned semantic,
SmallVectorImpl<IntervalClass>& classes,
SmallVectorImpl<SequenceNode>& nodes,
DenseMap<std::pair<unsigned, unsigned>, unsigned>& interned) {
SmallVector<IntervalClass> next;
for (const IntervalClass& current : classes) {
LaneSet intersection = current.lanes.intersect(active);
LaneSet difference = current.lanes.subtract(active);
if (!difference.empty())
next.push_back({difference, current.sequence});
if (intersection.empty())
continue;
auto key = std::make_pair(current.sequence, semantic);
auto [it, inserted] = interned.try_emplace(key, nodes.size());
if (inserted)
nodes.push_back({current.sequence, semantic});
next.push_back({intersection, it->second});
}
classes = std::move(next);
}
struct SequenceClass {
LaneSet lanes;
SmallVector<unsigned> sequence;
};
static SmallVector<SequenceClass>
buildSequenceClasses(unsigned laneCount, ArrayRef<PendingToken> tokens) {
SmallVector<IntervalClass> classes {
{LaneSet::all(laneCount), 0}
};
SmallVector<SequenceNode> nodes {
{0, 0}
};
DenseMap<std::pair<unsigned, unsigned>, unsigned> interned;
for (const PendingToken& token : tokens)
addTokenToClasses(token.lanes, token.semantic, classes, nodes, interned);
SmallVector<SequenceClass> result;
DenseMap<unsigned, unsigned> classBySequence;
SmallVector<unsigned> classSequences;
for (const IntervalClass& interval : classes) {
auto [it, inserted] = classBySequence.try_emplace(
interval.sequence, result.size());
if (inserted) {
result.push_back({interval.lanes, {}});
classSequences.push_back(interval.sequence);
static void appendReceive(BoundaryProgram &boundary,
const ScheduledTransferSlice &slice,
CollectionTarget target) {
RequirementFamily *requirement = slice.family->requirement;
LaneSet lanes = getReceiveLanes(slice);
if (!boundary.instructions.empty())
if (auto *run = std::get_if<EmitReceiveAssemblyRun>(
&boundary.instructions.back())) {
RequirementFamily *previous = run->slices[
run->entryOffsets[run->entryOffsets.size() - 2]].family->requirement;
CollectionTarget previousTarget {run->collection, run->positions.back()};
bool sameEntry = previous == requirement;
if (sameEntry
|| (sameCollectionEmissionContract(previousTarget, target)
&& previous->publicationFragmentType
== requirement->publicationFragmentType)) {
run->slices.push_back(slice);
if (sameEntry) {
run->entryOffsets.back() = run->slices.size();
run->entryLanes.back() = run->entryLanes.back().unite(lanes);
} else {
run->entryOffsets.push_back(run->slices.size());
run->positions.push_back(target.position);
run->entryLanes.push_back(lanes);
}
run->lanes = run->lanes.unite(lanes);
return;
}
}
else
result[it->second].lanes = result[it->second].lanes.unite(interval.lanes);
}
for (auto [behavior, sequence] : llvm::zip_equal(result, classSequences)) {
for (unsigned node = sequence; node != 0; node = nodes[node].previous)
behavior.sequence.push_back(nodes[node].instruction);
std::reverse(behavior.sequence.begin(), behavior.sequence.end());
}
return result;
}
static SmallVector<DeferredExchangePlan*>
getProducedExchanges(const BoundaryInstructionList& list) {
SmallVector<DeferredExchangePlan*> result;
for (const BoundaryInstruction& instruction : list.instructions)
if (auto produced = std::get_if<ProduceDeferredResult>(&instruction))
result.push_back(produced->exchange);
return result;
}
static LogicalResult buildCanonicalBoundary(
BoundaryProgram& boundary, ArrayRef<BoundaryEvent> events,
ArrayRef<PendingToken> tokens, ArrayRef<SemanticKey> semantics) {
unsigned laneCount = boundary.key.scheduled->cores.size();
SmallVector<SequenceClass> classes =
buildSequenceClasses(laneCount, tokens);
if (classes.empty())
return failure();
size_t prefix = classes.front().sequence.size();
for (const SequenceClass& behavior : ArrayRef(classes).drop_front()) {
prefix = std::min(prefix, behavior.sequence.size());
size_t index = 0;
while (index < prefix
&& behavior.sequence[index] == classes.front().sequence[index])
++index;
prefix = index;
}
auto prefixActions = collectCanonicalActions(
ArrayRef(classes.front().sequence).take_front(prefix), tokens, semantics,
events, LaneSet::all(laneCount));
if (failed(prefixActions))
return failure();
boundary.root = materializeInstructions(*prefixActions, LaneSet::all(laneCount));
if (classes.size() == 1)
return success();
auto dispatch = std::make_unique<LaneDispatch>();
SmallVector<int64_t> classIds(laneCount);
for (auto [classId, behavior] : llvm::enumerate(classes)) {
for (LaneInterval interval : behavior.lanes.intervals())
for (unsigned lane = interval.begin; lane < interval.end; ++lane)
classIds[lane] = classId;
auto actions = collectCanonicalActions(
behavior.sequence, tokens, semantics, events, behavior.lanes);
if (failed(actions))
return failure();
dispatch->branches.push_back(materializeInstructions(
ArrayRef(*actions).drop_front(prefix), behavior.lanes));
auto produced = getProducedExchanges(dispatch->branches.back());
if (classId == 0)
dispatch->producedExchanges = std::move(produced);
else if (produced != dispatch->producedExchanges)
return failure();
}
dispatch->branchByLane = StaticIntSequence::fromValues(classIds);
boundary.root.instructions.push_back(std::move(dispatch));
return success();
boundary.instructions.push_back(EmitReceiveAssemblyRun {
target.collection, {slice}, {0, 1}, {target.position}, {lanes}, lanes});
}
} // namespace
FailureOr<DeferredBoundaryPlan> buildDeferredBoundaryPlan(DeferredTransferPlan& transfers,
const ScheduledCommunicationPlan& schedule) {
FailureOr<DeferredBoundaryPlan> buildDeferredBoundaryPlan(
DeferredTransferPlan &transfers,
const ScheduledCommunicationPlan &schedule) {
DeferredBoundaryPlan result;
SmallVector<BoundaryWork> boundaries;
DenseMap<BoundaryKey, unsigned> boundaryIndices;
DenseMap<DeferredExchangePlan*, unsigned> resultBoundarySteps;
for (const ScheduledTransferSlice& slice : schedule.slices) {
ExternalTransferFamily& family = *slice.family;
auto [resultStep, inserted] =
resultBoundarySteps.try_emplace(family.requirement->exchange, slice.targetInsertionStep);
if (!inserted)
resultStep->second = std::max(resultStep->second, slice.targetInsertionStep);
unsigned sourceIndex =
getBoundaryIndex(boundaries, boundaryIndices,
{family.sourceScheduled, slice.sourceInsertionStep});
unsigned targetIndex =
getBoundaryIndex(boundaries, boundaryIndices,
{family.targetScheduled, slice.targetInsertionStep});
unsigned sourceLane = family.requirement->producer->scheduledLane;
boundaries[sourceIndex].events.push_back({BoundaryEventKind::Send,
slice,
LaneSet::range(sourceLane, sourceLane + 1),
getTransferEmissionSignature(family)});
LaneInterval familyLanes = family.targetLanes.intervals().front();
unsigned targetBegin = familyLanes.begin + slice.familyOffset;
boundaries[targetIndex].events.push_back({BoundaryEventKind::Receive,
slice,
LaneSet::range(targetBegin, targetBegin + slice.transferCount),
getTransferEmissionSignature(family)});
SmallVector<BoundaryProgram> boundaries;
DenseMap<BoundaryKey, unsigned> indices;
DenseMap<DeferredExchangePlan *, unsigned> resultSteps;
DenseMap<RequirementFamily *, LaneSet> coverage;
for (const std::unique_ptr<DeferredExchangePlan> &exchange :
transfers.exchanges) {
auto plan = buildDeferredResultPlan(*exchange);
if (failed(plan))
return exchange->deferred.emitOpError(
"cannot evaluate deferred result lane functions"), failure();
result.results.push_back(std::move(*plan));
}
DenseMap<RequirementFamily *, CollectionTarget> collections;
for (const DeferredResultPlan &plan : result.results)
for (const FragmentCollectionPlan &collection : plan.collections)
for (const FragmentCollectionPlan::Requirement &requirement :
collection.requirements)
if (!collections.try_emplace(
requirement.family,
CollectionTarget{&collection, requirement.position}).second)
return requirement.family->exchange->deferred.emitOpError(
"deferred requirement is owned by multiple fragment collections"),
failure();
for (const ScheduledTransferSlice &slice : schedule.slices) {
ExternalTransferFamily &family = *slice.family;
BoundaryProgram &source = getBoundary(
boundaries, indices,
{family.sourceScheduled, slice.sourceInsertionStep});
appendSend(source, slice);
BoundaryProgram &target = getBoundary(
boundaries, indices,
{family.targetScheduled, slice.targetInsertionStep});
CollectionTarget collection = collections.lookup(family.requirement);
if (!collection.collection)
return family.requirement->exchange->deferred.emitOpError(
"deferred requirement has no complete result-owned collection"),
failure();
appendReceive(target, slice, collection);
resultSteps[family.requirement->exchange] = std::max(
resultSteps.lookup(family.requirement->exchange),
slice.targetInsertionStep);
if (failed(addCoverage(*family.requirement, getReceiveLanes(slice),
coverage)))
return failure();
}
DenseMap<BoundaryKey, SmallVector<LocalAvailabilityFamily*>> locals;
DenseMap<BoundaryKey, SmallVector<DeferredExchangePlan*>> exchanges;
for (const std::unique_ptr<DeferredExchangePlan>& exchange : transfers.exchanges) {
auto resultStep = resultBoundarySteps.find(exchange.get());
unsigned step = resultStep == resultBoundarySteps.end() ? exchange->consumerStep : resultStep->second;
BoundaryKey key {exchange->target, step};
getBoundaryIndex(boundaries, boundaryIndices, key);
for (LocalAvailabilityFamily& local : exchange->local)
locals[key].push_back(&local);
exchanges[key].push_back(exchange.get());
auto resultPlan = buildDeferredResultPlan(*exchange);
if (failed(resultPlan))
return exchange->deferred.emitOpError("cannot evaluate deferred result lane functions"), failure();
result.results.push_back(std::move(*resultPlan));
}
for (const std::unique_ptr<DeferredExchangePlan> &exchange :
transfers.exchanges) {
unsigned resultStep = resultSteps.lookup(exchange.get());
for (LocalAvailabilityFamily &local : exchange->local)
resultStep = std::max(
resultStep, local.requirement->producer->step + 1);
if (exchange->requirements.empty())
resultStep = exchange->consumerStep;
if (resultStep > exchange->consumerStep)
return exchange->deferred.emitOpError(
"deferred result boundary is later than its consumer"), failure();
DenseMap<ScheduledInfo*, unsigned> scheduledOrder;
for (auto [index, scheduled] : llvm::enumerate(transfers.scheduled))
scheduledOrder[&scheduled] = index;
llvm::stable_sort(boundaries, [&](const BoundaryWork& lhs, const BoundaryWork& rhs) {
return std::tie(scheduledOrder[lhs.program.key.scheduled], lhs.program.key.insertionStep)
< std::tie(scheduledOrder[rhs.program.key.scheduled], rhs.program.key.insertionStep);
});
for (BoundaryWork& work : boundaries) {
BoundaryProgram& boundary = work.program;
SmallVector<PendingToken> tokens;
SmallVector<SemanticKey> semantics;
DenseMap<size_t, SmallVector<unsigned>> semanticsByHash;
DenseMap<unsigned, SmallVector<LocalAvailabilityFamily*>> localsBySemantic;
SmallPtrSet<LocalAvailabilityFamily*, 8> emittedLocals;
for (LocalAvailabilityFamily* local : locals[boundary.key]) {
unsigned semantic =
internSemantic(getLocalKey(*local), semantics, semanticsByHash);
localsBySemantic[semantic].push_back(local);
BoundaryProgram &boundary = getBoundary(
boundaries, indices, {exchange->target, resultStep});
DenseMap<RequirementFamily *, LocalAvailabilityFamily *> localByRequirement;
for (LocalAvailabilityFamily &local : exchange->local) {
auto [it, inserted] = localByRequirement.try_emplace(local.requirement, &local);
if (!inserted)
it->second = nullptr;
}
SmallVector<unsigned> eventSemantics;
for (BoundaryEvent& event : work.events) {
unsigned semantic =
internSemantic(getEventKey(event), semantics, semanticsByHash);
eventSemantics.push_back(semantic);
}
llvm::SmallDenseSet<unsigned, 8> emittedAvailabilities;
for (auto [eventId, event] : llvm::enumerate(work.events)) {
unsigned semantic = eventSemantics[eventId];
if (event.kind == BoundaryEventKind::Send) {
tokens.push_back({LaneSet::all(
boundary.key.scheduled->cores.size()),
semantic, static_cast<unsigned>(eventId)});
DenseMap<const FragmentCollectionPlan *, std::optional<EmitLocalCollectionRun>> concatRuns;
llvm::SmallPtrSet<const FragmentCollectionPlan *, 4> emittedConcats;
SmallVector<EmitLocalCollectionRun> localUpdates;
for (LocalAvailabilityFamily &local : exchange->local) {
CollectionTarget target = collections.lookup(local.requirement);
if (!target.collection)
return exchange->deferred.emitOpError(
"local availability has no complete result-owned collection"),
failure();
auto [concat, inserted] = concatRuns.try_emplace(target.collection);
if (inserted)
concat->second = buildLocalConcat(*target.collection,
localByRequirement,
exchange->targetLaneCount);
if (concat->second) {
if (emittedConcats.insert(target.collection).second)
localUpdates.push_back(std::move(*concat->second));
if (failed(addCoverage(*local.requirement, local.targetLanes, coverage)))
return failure();
continue;
}
tokens.push_back(
{event.activeLanes, semantic, static_cast<unsigned>(eventId)});
if (emittedAvailabilities.insert(semantic).second) {
for (LocalAvailabilityFamily* local : localsBySemantic[semantic]) {
tokens.push_back({local->targetLanes, semantic, local});
emittedLocals.insert(local);
}
localsBySemantic.erase(semantic);
auto grouped = llvm::find_if(localUpdates, [&](EmitLocalCollectionRun &update) {
return update.lanes.intersect(local.targetLanes).empty()
&& update.collection == target.collection
&& update.collectionPosition == target.position
&& canGroupLocalAvailability(*update.families.front()->requirement,
*local.requirement);
});
if (grouped == localUpdates.end()) {
localUpdates.push_back(EmitLocalCollectionRun {
target.collection, target.position, {&local}, local.targetLanes,
false});
} else {
grouped->families.push_back(&local);
grouped->lanes = grouped->lanes.unite(local.targetLanes);
}
if (failed(addCoverage(*local.requirement, local.targetLanes, coverage)))
return failure();
}
for (LocalAvailabilityFamily* local : locals[boundary.key])
if (!emittedLocals.contains(local)) {
unsigned semantic =
internSemantic(getLocalKey(*local), semantics, semanticsByHash);
tokens.push_back({local->targetLanes, semantic, local});
}
for (DeferredExchangePlan* exchange : exchanges[boundary.key]) {
SemanticKey key;
key.kind = SemanticKind::Result;
key.exchange = exchange;
unsigned semantic = internSemantic(key, semantics, semanticsByHash);
tokens.push_back({LaneSet::all(exchange->targetLaneCount), semantic, exchange});
}
if (failed(buildCanonicalBoundary(boundary, work.events, tokens, semantics)))
return boundary.key.scheduled->op->emitOpError("cannot construct canonical boundary program"), failure();
result.boundaries.push_back(std::move(boundary));
for (EmitLocalCollectionRun &update : localUpdates)
boundary.instructions.push_back(std::move(update));
for (RequirementFamily &requirement : exchange->requirements)
if (!(coverage.lookup(&requirement) == requirement.targetLanes))
return exchange->deferred.emitOpError(
"deferred availability does not cover every target lane exactly once"),
failure();
boundary.instructions.push_back(ProduceDeferredResult {exchange.get()});
}
DenseMap<ScheduledInfo *, unsigned> scheduledOrder;
for (auto [index, scheduled] : llvm::enumerate(transfers.scheduled))
scheduledOrder[&scheduled] = index;
llvm::stable_sort(boundaries, [&](const BoundaryProgram &lhs,
const BoundaryProgram &rhs) {
return std::tie(scheduledOrder[lhs.key.first], lhs.key.second)
< std::tie(scheduledOrder[rhs.key.first], rhs.key.second);
});
result.boundaries = std::move(boundaries);
return result;
}
@@ -7,72 +7,37 @@ namespace onnx_mlir::spatial {
struct DeferredTransferPlan;
struct BoundaryKey {
ScheduledInfo* scheduled = nullptr;
unsigned insertionStep = 0;
bool operator==(const BoundaryKey& other) const {
return scheduled == other.scheduled
&& insertionStep == other.insertionStep;
}
};
using BoundaryKey = std::pair<ScheduledInfo *, unsigned>;
struct EmitSendRun {
llvm::SmallVector<ScheduledTransferSlice> slices;
LaneSet lanes;
};
struct EmitReceiveRun {
llvm::SmallVector<ScheduledTransferSlice> slices;
llvm::SmallVector<size_t> entryOffsets;
LaneSet lanes;
};
struct EmitReceiveBundle {
llvm::SmallVector<EmitReceiveRun> entries;
};
struct EmitReceiveAssemblyRun {
llvm::SmallVector<ScheduledTransferSlice> slices;
llvm::SmallVector<size_t> entryOffsets;
llvm::SmallVector<unsigned> assemblyEntries;
std::optional<unsigned> projectionLeaf;
LaneSet lanes;
};
struct MaterializeLocalFamily {
struct EmitLocalCollectionRun {
const FragmentCollectionPlan* collection = nullptr;
unsigned collectionPosition = 0;
llvm::SmallVector<LocalAvailabilityFamily*> families;
LaneSet lanes;
bool concatenatePayloads = false;
};
using AvailabilitySource =
std::variant<EmitReceiveRun, MaterializeLocalFamily>;
struct AvailabilityAlternative {
struct EmitReceiveAssemblyRun {
const FragmentCollectionPlan* collection = nullptr;
llvm::SmallVector<ScheduledTransferSlice> slices;
llvm::SmallVector<size_t> entryOffsets;
llvm::SmallVector<unsigned> positions;
llvm::SmallVector<LaneSet> entryLanes;
LaneSet lanes;
AvailabilitySource source;
};
struct ResolveAvailability {
DeferredExchangePlan* exchange = nullptr;
RequirementCoordinate coordinate;
mlir::Type fragmentType;
llvm::SmallVector<AvailabilityAlternative, 2> alternatives;
};
struct ProduceDeferredResult {
DeferredExchangePlan* exchange = nullptr;
LaneSet lanes;
};
struct BoundaryInstructionList;
struct LaneDispatch;
using BoundaryInstruction = std::variant<EmitSendRun, ResolveAvailability,
EmitReceiveBundle, EmitReceiveAssemblyRun, ProduceDeferredResult,
std::unique_ptr<LaneDispatch>>;
struct BoundaryInstructionList {
llvm::SmallVector<BoundaryInstruction, 0> instructions;
};
struct LaneDispatch {
StaticIntSequence branchByLane = StaticIntSequence::uniform(0, 1);
llvm::SmallVector<BoundaryInstructionList> branches;
llvm::SmallVector<DeferredExchangePlan*> producedExchanges;
};
using BoundaryInstruction =
std::variant<EmitSendRun, EmitLocalCollectionRun, EmitReceiveAssemblyRun,
ProduceDeferredResult>;
struct BoundaryProgram {
BoundaryKey key;
BoundaryInstructionList root;
llvm::SmallVector<BoundaryInstruction, 0> instructions;
};
struct DeferredBoundaryPlan {
@@ -84,22 +49,3 @@ mlir::FailureOr<DeferredBoundaryPlan> buildDeferredBoundaryPlan(DeferredTransfer
const ScheduledCommunicationPlan& schedule);
} // namespace onnx_mlir::spatial
namespace llvm {
template <>
struct DenseMapInfo<onnx_mlir::spatial::BoundaryKey> {
static onnx_mlir::spatial::BoundaryKey getEmptyKey() {
return {DenseMapInfo<onnx_mlir::spatial::ScheduledInfo*>::getEmptyKey(), 0};
}
static onnx_mlir::spatial::BoundaryKey getTombstoneKey() {
return {DenseMapInfo<onnx_mlir::spatial::ScheduledInfo*>::getTombstoneKey(), 0};
}
static unsigned getHashValue(const onnx_mlir::spatial::BoundaryKey& key) {
return hash_combine(key.scheduled, key.insertionStep);
}
static bool isEqual(const onnx_mlir::spatial::BoundaryKey& lhs,
const onnx_mlir::spatial::BoundaryKey& rhs) {
return lhs == rhs;
}
};
} // namespace llvm
File diff suppressed because it is too large Load Diff
@@ -9,6 +9,23 @@
namespace onnx_mlir::spatial {
struct FragmentCollectionKeyInfo {
static FragmentCollectionKey getEmptyKey() {
return {llvm::DenseMapInfo<DeferredExchangePlan*>::getEmptyKey()};
}
static FragmentCollectionKey getTombstoneKey() {
return {llvm::DenseMapInfo<DeferredExchangePlan*>::getTombstoneKey()};
}
static unsigned getHashValue(const FragmentCollectionKey& key) {
return llvm::hash_combine(
key.exchange, key.kind, key.leafIndex);
}
static bool isEqual(const FragmentCollectionKey& lhs,
const FragmentCollectionKey& rhs) {
return lhs == rhs;
}
};
struct DeferredEmissionContext {
DeferredEmissionContext(mlir::IRRewriter& rewriter,
ConstantPool& constants)
@@ -16,10 +33,8 @@ struct DeferredEmissionContext {
mlir::IRRewriter& rewriter;
ConstantPool& constants;
llvm::DenseMap<RequirementFamily*, mlir::Value> receives;
llvm::DenseMap<DeferredExchangePlan*, mlir::Value> assemblies;
llvm::DenseMap<std::pair<DeferredExchangePlan*, unsigned>, mlir::Value>
projectionAssemblies;
llvm::DenseMap<FragmentCollectionKey, mlir::Value,
FragmentCollectionKeyInfo> fragmentCollections;
};
using DeferredReplacementMap =
@@ -5,8 +5,6 @@
#include "llvm/ADT/DenseMap.h"
#include <map>
namespace onnx_mlir::spatial {
using namespace mlir;
namespace {
@@ -126,7 +124,6 @@ static LogicalResult simulatePlanned(
}
struct RealizedOperation {
Operation *op = nullptr;
bool send = false;
StaticIntSequence channels;
StaticIntSequence parents;
@@ -135,27 +132,6 @@ struct RealizedOperation {
StaticIntSequence targets;
};
static FailureOr<size_t> getBatchTransferCount(Operation *op) {
if (auto count = op->getAttrOfType<IntegerAttr>(
"raptor.batch_transfer_count")) {
if (count.getInt() > 0)
return count.getInt();
return op->emitOpError("has invalid compact transfer count"), failure();
}
if (op->hasAttr("raptor.batch_channel_ids_encoding"))
return op->emitOpError("is missing compact transfer count"), failure();
Attribute channels = op->getAttr("raptor.batch_channel_ids");
if (auto array = dyn_cast_or_null<DenseI64ArrayAttr>(channels))
return array.empty()
? FailureOr<size_t>(failure())
: FailureOr<size_t>(array.size());
if (auto elements = dyn_cast_or_null<DenseIntElementsAttr>(channels);
elements && elements.getNumElements() > 0)
return elements.getNumElements();
return op->emitOpError("has invalid legacy compact transfer metadata"),
failure();
}
static FailureOr<RealizedOperation> parseRealizedOperation(Operation *op) {
bool scalar = op->hasAttr("raptor.channel_id");
bool batch = op->hasAttr("raptor.batch_channel_ids");
@@ -164,11 +140,14 @@ static FailureOr<RealizedOperation> parseRealizedOperation(Operation *op) {
"must have exactly one scalar or compact metadata form");
return failure();
}
auto batchCount = scalar ? FailureOr<size_t>(1)
: getBatchTransferCount(op);
if (failed(batchCount))
return failure();
size_t size = *batchCount;
size_t size = 1;
if (batch) {
auto count = op->getAttrOfType<IntegerAttr>(
"raptor.batch_transfer_count");
if (!count || count.getInt() <= 0)
return op->emitOpError("has invalid compact transfer count"), failure();
size = count.getInt();
}
auto channels = getStaticIntSequenceAttr(
op, scalar ? "raptor.channel_id" : "raptor.batch_channel_ids", size);
auto parents = getStaticIntSequenceAttr(
@@ -191,37 +170,10 @@ static FailureOr<RealizedOperation> parseRealizedOperation(Operation *op) {
return failure();
}
}
return RealizedOperation {op, isa<SpatChannelSendOp>(op),
return RealizedOperation {isa<SpatChannelSendOp>(op),
*channels, *parents, *counts, *sources, *targets};
}
struct CoreTransferSequences {
DenseMap<int64_t, StaticIntSequenceChain> sends;
DenseMap<int64_t, StaticIntSequenceChain> receives;
DenseMap<int64_t, StaticIntSequenceChain> events;
};
struct ExpectedFamily {
ExternalTransferFamily *family = nullptr;
int64_t firstChannel = 0;
int64_t endChannel = 0;
};
static void appendByCore(DenseMap<int64_t, StaticIntSequenceChain> &result,
const StaticIntSequence &channels,
const StaticIntSequence &cores, size_t begin,
size_t count) {
size_t end = begin + count;
cores.forEachEqualRun(
[&](int64_t core, size_t runBegin, size_t runCount) {
size_t selectedBegin = std::max(begin, runBegin);
size_t selectedEnd = std::min(end, runBegin + runCount);
if (selectedBegin < selectedEnd)
result[core].append(
channels, selectedBegin, selectedEnd - selectedBegin);
});
}
static void appendEventsByCore(
DenseMap<int64_t, StaticIntSequenceChain> &result,
const StaticIntSequence &channels, const StaticIntSequence &cores,
@@ -241,39 +193,6 @@ static void appendEventsByCore(
});
}
static LogicalResult compareSequences(
func::FuncOp funcOp,
const DenseMap<int64_t, StaticIntSequenceChain> &expected,
const DenseMap<int64_t, StaticIntSequenceChain> &actual, StringRef kind) {
if (expected.size() != actual.size())
return funcOp.emitOpError()
<< "realized " << kind << " stream set differs from plan";
for (const auto &[core, sequence] : expected) {
auto found = actual.find(core);
if (found == actual.end())
return funcOp.emitOpError() << "realized " << kind
<< " stream is missing on core " << core;
StaticIntSequenceChainCursor expectedCursor(sequence);
StaticIntSequenceChainCursor actualCursor(found->second);
uint64_t ordinal = 0;
while (!expectedCursor.done() && !actualCursor.done()
&& expectedCursor.value() == actualCursor.value()) {
expectedCursor.advance();
actualCursor.advance();
++ordinal;
}
if (expectedCursor.done() && actualCursor.done())
continue;
return funcOp.emitOpError()
<< "realized " << kind << " logical order differs on core "
<< core << " at ordinal " << ordinal << ": expected channel "
<< (expectedCursor.done() ? -1 : expectedCursor.value())
<< ", actual channel "
<< (actualCursor.done() ? -1 : actualCursor.value());
}
return success();
}
static LogicalResult compareEventSequences(
func::FuncOp funcOp,
const DenseMap<int64_t, StaticIntSequenceChain> &expected,
@@ -320,6 +239,37 @@ static LogicalResult compareEventSequences(
LogicalResult verifyPlannedCommunicationDeadlockFree(
Operation *anchor, ArrayRef<unsigned> stepCounts,
const ScheduledCommunicationPlan &plan) {
SmallVector<std::pair<int64_t, int64_t>> familyChannels;
DenseMap<ExternalTransferFamily *, unsigned> familyIndex;
for (const ScheduledTransferSlice &slice : plan.slices) {
ExternalTransferFamily *family = slice.family;
if (!familyIndex.try_emplace(family, familyIndex.size()).second)
continue;
size_t count = family->channelIds.size();
if (count == 0)
return anchor->emitError(
"planned communication family has no channels");
int64_t first = family->channelIds.valueAt(0);
for (size_t index = 1; index < count; ++index)
if (family->channelIds.valueAt(index)
!= first + static_cast<int64_t>(index))
return anchor->emitError(
"planned communication family has non-consecutive channels");
familyChannels.emplace_back(
first, first + static_cast<int64_t>(count));
}
llvm::sort(familyChannels);
int64_t nextChannel = 0;
for (auto [firstChannel, endChannel] : familyChannels) {
if (firstChannel != nextChannel)
return anchor->emitError(
"planned communication channels are not exactly contiguous");
nextChannel = endChannel;
}
if (static_cast<uint64_t>(nextChannel) != plan.logicalTransferCount)
return anchor->emitError(
"planned communication channel count is inconsistent");
for (const ScheduledTransferSlice &slice : plan.slices) {
ExternalTransferFamily &family = *slice.family;
for (size_t offset = 0; offset < slice.transferCount; ++offset) {
@@ -341,55 +291,27 @@ LogicalResult verifyPlannedCommunicationDeadlockFree(
LogicalResult verifyRealizedCommunicationDeadlockFree(
func::FuncOp funcOp, const ScheduledCommunicationPlan &plan) {
SmallVector<ExpectedFamily> families;
SmallVector<ExternalTransferFamily *> familyByChannel(
plan.logicalTransferCount);
DenseMap<ExternalTransferFamily *, unsigned> familyIndex;
for (const ScheduledTransferSlice &slice : plan.slices) {
ExternalTransferFamily *family = slice.family;
if (familyIndex.count(family))
if (!familyIndex.try_emplace(family, familyIndex.size()).second)
continue;
size_t count = family->channelIds.size();
int64_t first = family->channelIds.valueAt(0);
for (size_t index = 1; index < count; ++index)
if (family->channelIds.valueAt(index)
!= first + static_cast<int64_t>(index))
return funcOp.emitOpError(
"planned communication family has non-consecutive channels");
familyIndex[family] = families.size();
families.push_back({family, first, first + static_cast<int64_t>(count)});
for (size_t index = 0; index < family->channelIds.size(); ++index)
familyByChannel[family->channelIds.valueAt(index)] = family;
}
llvm::sort(families, [](const ExpectedFamily &lhs,
const ExpectedFamily &rhs) {
return lhs.firstChannel < rhs.firstChannel;
});
familyIndex.clear();
std::map<int64_t, unsigned> familyByFirstChannel;
int64_t nextChannel = 0;
for (auto [index, expected] : llvm::enumerate(families)) {
if (expected.firstChannel != nextChannel)
return funcOp.emitOpError(
"planned communication channels are not exactly contiguous");
nextChannel = expected.endChannel;
familyIndex[expected.family] = index;
familyByFirstChannel.emplace(expected.firstChannel, index);
}
if (static_cast<uint64_t>(nextChannel) != plan.logicalTransferCount)
return funcOp.emitOpError(
"planned communication channel count is inconsistent");
CoreTransferSequences expected;
DenseMap<int64_t, StaticIntSequenceChain> expected;
for (const ScheduledTransferSlice &slice : plan.slices) {
ExternalTransferFamily &family = *slice.family;
appendByCore(expected.sends, family.channelIds, family.sourceCores,
slice.familyOffset, slice.transferCount);
appendByCore(expected.receives, family.channelIds, family.targetCores,
slice.familyOffset, slice.transferCount);
appendEventsByCore(expected.events, family.channelIds, family.sourceCores,
appendEventsByCore(expected, family.channelIds, family.sourceCores,
slice.familyOffset, slice.transferCount, true);
appendEventsByCore(expected.events, family.channelIds, family.targetCores,
appendEventsByCore(expected, family.channelIds, family.targetCores,
slice.familyOffset, slice.transferCount, false);
}
CoreTransferSequences actual;
DenseMap<int64_t, StaticIntSequenceChain> actual;
SmallVector<std::unique_ptr<StaticIntSequence>> actualChannels;
bool invalid = false;
funcOp.walk([&](Operation *op) {
@@ -405,29 +327,28 @@ LogicalResult verifyRealizedCommunicationDeadlockFree(
: cast<SpatChannelReceiveOp>(op).getOutput().getType();
for (size_t index = 0; index < realized->channels.size(); ++index) {
int64_t channel = realized->channels.valueAt(index);
auto upper = familyByFirstChannel.upper_bound(channel);
if (upper == familyByFirstChannel.begin()) {
if (channel < 0
|| static_cast<uint64_t>(channel) >= familyByChannel.size()) {
op->emitOpError("references an unknown logical channel");
invalid = true;
return;
}
ExpectedFamily &expected = families[std::prev(upper)->second];
if (channel >= expected.endChannel) {
ExternalTransferFamily *family = familyByChannel[channel];
if (!family) {
op->emitOpError("references an unknown logical channel");
invalid = true;
return;
}
ExternalTransferFamily &family = *expected.family;
size_t familyOffset = channel - expected.firstChannel;
RequirementFamily &requirement = *family.requirement;
size_t familyOffset = channel - family->channelIds.valueAt(0);
RequirementFamily &requirement = *family->requirement;
if (realized->parents.valueAt(index)
!= static_cast<int64_t>(requirement.exchange->exchangeId)
|| realized->counts.valueAt(index)
!= requirement.exchange->externalTransferCount
|| realized->sources.valueAt(index)
!= family.sourceCores.valueAt(familyOffset)
!= family->sourceCores.valueAt(familyOffset)
|| realized->targets.valueAt(index)
!= family.targetCores.valueAt(familyOffset)
!= family->targetCores.valueAt(familyOffset)
|| payloadType != requirement.publicationFragmentType) {
op->emitOpError(
"logical transfer metadata differs from its symbolic family");
@@ -439,24 +360,13 @@ LogicalResult verifyRealizedCommunicationDeadlockFree(
return;
actualChannels.push_back(
std::make_unique<StaticIntSequence>(std::move(realized->channels)));
appendByCore(realized->send ? actual.sends : actual.receives,
*actualChannels.back(),
realized->send ? realized->sources : realized->targets,
0, actualChannels.back()->size());
appendEventsByCore(actual.events, *actualChannels.back(),
appendEventsByCore(actual, *actualChannels.back(),
realized->send ? realized->sources : realized->targets,
0, actualChannels.back()->size(), realized->send);
});
if (invalid)
return failure();
if (failed(compareSequences(
funcOp, expected.sends, actual.sends, "send"))
|| failed(compareSequences(
funcOp, expected.receives, actual.receives, "receive"))
|| failed(compareEventSequences(
funcOp, expected.events, actual.events)))
return failure();
return success();
return compareEventSequences(funcOp, expected, actual);
}
} // namespace onnx_mlir::spatial
@@ -104,11 +104,14 @@ private:
};
struct RequirementCoordinate {
unsigned specializationIndex = 0;
unsigned leafIndex = 0;
unsigned selectedPosition = 0;
bool operator==(const RequirementCoordinate& other) const {
return leafIndex == other.leafIndex && selectedPosition == other.selectedPosition;
return specializationIndex == other.specializationIndex
&& leafIndex == other.leafIndex
&& selectedPosition == other.selectedPosition;
}
};
@@ -139,10 +142,10 @@ struct DeferredProjectionLeafTemplate {
DeferredLeafForm form = DeferredLeafForm::DirectSource;
mlir::Value sourceRoot;
mlir::Value replacementRoot;
mlir::tensor::ExtractSliceOp leadingProjection;
DeferredSliceTemplate leadingGeometry;
DeferredSliceTemplate innerGeometry;
mlir::RankedTensorType reconstructedType;
bool leadingRankReduced = false;
};
struct DeferredInsertAssemblyEntryTemplate {
@@ -160,6 +163,9 @@ struct DeferredInsertAssemblyTemplate {
struct DeferredProgramTemplate {
SpatDeferredCommunicationOp deferred;
unsigned specializationCount = 1;
mlir::Value specializationArgument;
mlir::RankedTensorType specializationFragmentType;
mlir::Value scheduledLane;
mlir::Value yieldedValue;
llvm::SmallVector<DeferredProjectionLeafTemplate, 0> leaves;
@@ -189,9 +195,7 @@ struct ScheduledInfo {
llvm::SmallVector<mlir::Block*> blocks;
llvm::SmallVector<mlir::Operation*> stepAnchors;
llvm::SmallVector<int64_t> cores;
llvm::SmallVector<int64_t> stepSourceIds;
llvm::SmallVector<int64_t> resultOffsets;
llvm::SmallVector<int64_t> resultCounts;
unsigned stepCount = 0;
llvm::SmallVector<ProducedValue*> produced;
llvm::SmallVector<unsigned> streamIds;
@@ -10,6 +10,8 @@
#include "src/Accelerators/PIM/Common/IR/AffineUtils.hpp"
#include "src/Accelerators/PIM/Common/IR/ShapingUtils.hpp"
#include "src/Accelerators/PIM/Common/IR/StaticIntSequence.hpp"
#include "src/Accelerators/PIM/Common/IR/TensorSliceUtils.hpp"
#include "src/Accelerators/PIM/Conversion/ONNXToSpatial/CompileTime.hpp"
namespace onnx_mlir::spatial {
@@ -77,7 +79,6 @@ static FailureOr<Value> buildBlueprintReconstruction(
sliceOffsets.push_back(builder.getIndexAttr((*sourceSlots)[fragmentIndex]));
sliceSizes.push_back(builder.getIndexAttr(1));
sliceStrides.push_back(builder.getIndexAttr(1));
SmallVector<int64_t> selectedShape {1};
for (int64_t dim = 0; dim < rank; ++dim) {
int64_t index = fragmentIndex * rank + dim;
int64_t size = sizes[index];
@@ -87,21 +88,16 @@ static FailureOr<Value> buildBlueprintReconstruction(
sliceOffsets.push_back(builder.getIndexAttr(sourceCoordinates[dim]));
sliceSizes.push_back(builder.getIndexAttr(size));
sliceStrides.push_back(builder.getIndexAttr(1));
selectedShape.push_back(size);
}
auto selectedType = RankedTensorType::get(selectedShape, resultType.getElementType());
Value selected = tensor::ExtractSliceOp::create(
builder, loc, selectedType, sourceBlockArgs[operandIndex], sliceOffsets,
sliceSizes, sliceStrides);
SmallVector<int64_t> fragmentResultShape(selectedShape.begin() + 1,
selectedShape.end());
auto fragmentType = RankedTensorType::get(fragmentResultShape,
resultType.getElementType());
SmallVector<ReassociationIndices> reassociation {{0, 1}};
for (int64_t dim = 1; dim < rank; ++dim)
reassociation.push_back({dim + 1});
Value fragment = tensor::CollapseShapeOp::create(
builder, loc, fragmentType, selected, reassociation);
SmallVector<int64_t> selectedFragmentShape;
selectedFragmentShape.reserve(rank);
for (int64_t dim = 0; dim < rank; ++dim)
selectedFragmentShape.push_back(sizes[fragmentIndex * rank + dim]);
auto fragmentType = RankedTensorType::get(
selectedFragmentShape, resultType.getElementType());
Value fragment = extractMixedSliceOrIdentity(
builder, loc, sourceBlockArgs[operandIndex], fragmentType,
{sliceOffsets, sliceSizes, sliceStrides});
SmallVector<OpFoldResult> targetOffsets, targetSizes, targetStrides;
for (int64_t dim = 0; dim < rank; ++dim) {
int64_t index = fragmentIndex * rank + dim;
@@ -116,56 +112,52 @@ static FailureOr<Value> buildBlueprintReconstruction(
return result;
}
static FailureOr<Value> buildIndexSwitchSelection(OpBuilder &builder, Location loc,
Value selector, ValueRange candidates,
Operation *diagnosticOwner) {
if (candidates.empty())
return diagnosticOwner->emitOpError("direct selection requires at least one candidate"), failure();
Type type = candidates.front().getType();
if (llvm::any_of(candidates, [&](Value candidate) { return candidate.getType() != type; }))
return diagnosticOwner->emitOpError("direct selection requires identical candidate types"), failure();
if (candidates.size() == 1)
return candidates.front();
SmallVector<int64_t> cases;
for (int64_t index = 0; index < static_cast<int64_t>(candidates.size()) - 1; ++index)
cases.push_back(index);
auto selection = scf::IndexSwitchOp::create(
builder, loc, TypeRange {type}, selector, cases, cases.size());
auto buildYield = [&](Region &region, Value candidate) {
OpBuilder::InsertionGuard guard(builder);
Block *block = builder.createBlock(&region);
builder.setInsertionPointToEnd(block);
scf::YieldOp::create(builder, loc, candidate);
};
for (auto [region, candidate] : llvm::zip(selection.getCaseRegions(), candidates.drop_back()))
buildYield(region, candidate);
// The scheduled-lane verifier guarantees an in-range selector, so default is
// the final lane without an otherwise-unreachable extra branch.
buildYield(selection.getDefaultRegion(), candidates.back());
return selection.getResult(0);
}
static FailureOr<Value> buildSelectedDeferredSource(OpBuilder &builder, Location loc,
SpatDeferredCommunicationOp transfer,
Operation *diagnosticOwner,
Value scheduledLane,
ValueRange sourceBlockArgs,
ArrayRef<int64_t> sourceOperandForScheduledLane) {
if (sourceBlockArgs.size() == 1)
return sourceBlockArgs.front();
if (!scheduledLane || sourceOperandForScheduledLane.empty())
return transfer.emitOpError("multiple deferred sources require the enclosing scheduled lane"), failure();
auto scheduled = transfer->getParentOfType<SpatScheduledComputeBatch>();
if (!scheduled || sourceOperandForScheduledLane.size() != static_cast<size_t>(scheduled.getLaneCount()))
return transfer.emitOpError("deferred source mapping must cover every scheduled lane"), failure();
SmallVector<Value> candidates;
candidates.reserve(sourceOperandForScheduledLane.size());
return diagnosticOwner->emitOpError("multiple deferred sources require the enclosing scheduled lane"), failure();
for (int64_t sourceIndex : sourceOperandForScheduledLane) {
if (sourceIndex < 0 || sourceIndex >= static_cast<int64_t>(sourceBlockArgs.size()))
return transfer.emitOpError("deferred source mapping operand is out of range"), failure();
candidates.push_back(sourceBlockArgs[sourceIndex]);
return diagnosticOwner->emitOpError("deferred source mapping operand is out of range"), failure();
}
return buildIndexSwitchSelection(builder, loc, scheduledLane, candidates, transfer.getOperation());
StaticIntSequence sourceIndices = StaticIntSequence::fromValues(
sourceOperandForScheduledLane);
if (sourceIndices.getKind() == StaticIntSequenceKind::Uniform)
return sourceBlockArgs[sourceIndices.valueAt(0)];
Value selector;
if (sourceIndices.getKind() == StaticIntSequenceKind::Affine) {
int64_t step = sourceIndices.valueAt(1) - sourceIndices.valueAt(0);
selector = affineMulConst(
builder, loc, scheduledLane, step, diagnosticOwner);
selector = affineAddConst(
builder, loc, selector, sourceIndices.valueAt(0),
diagnosticOwner);
} else {
struct Run { int64_t value; size_t end; };
SmallVector<Run> runs;
sourceIndices.forEachEqualRun(
[&](int64_t value, size_t begin, size_t count) {
runs.push_back({value, begin + count});
});
selector = arith::ConstantIndexOp::create(
builder, loc, runs.back().value);
for (const Run &run : llvm::reverse(ArrayRef(runs).drop_back())) {
Value end = arith::ConstantIndexOp::create(builder, loc, run.end);
Value before = arith::CmpIOp::create(
builder, loc, arith::CmpIPredicate::ult, scheduledLane, end);
Value value = arith::ConstantIndexOp::create(builder, loc, run.value);
selector = arith::SelectOp::create(
builder, loc, before, value, selector);
}
}
return SpatDeferredSourceSelectOp::create(
builder, loc, sourceBlockArgs.front().getType(), selector,
sourceBlockArgs).getOutput();
}
static bool isDeferredPayloadCandidateOp(Operation *op) {
@@ -202,6 +194,10 @@ static FailureOr<Value> clonePayloadRoot(Value root, Block &body, const Deferred
std::function<FailureOr<Value>(Value)> cloneScheduledLane = [&](Value value) -> FailureOr<Value> {
if (mapping.contains(value)) return mapping.lookup(value);
if (value == plan.scheduledLane) return value;
if (auto argument = dyn_cast<BlockArgument>(value);
argument && argument.getOwner() == &transfer.getBody().front()
&& argument.getArgNumber() >= transfer.getSources().size())
return value;
if (isa<BlockArgument>(value))
return transfer.emitOpError("phase 1 payload shaping captures an unsupported block argument"), failure();
Operation *op = value.getDefiningOp();
@@ -279,7 +275,8 @@ bool isDeferredFragmentAssemblyInput(
LogicalResult prepareSingleCpuInput(OpBuilder &, Location loc, Value input, BlockArgument graphInput,
const ComputeInstance &consumerInstance, const MergeScheduleResult &,
ValueRange scheduledInputs, Block &block, unsigned firstInputArgument,
ArrayRef<ProducerValueKey> carriedKeys, Value graphLane, Value scheduledGraphLane,
const DenseMap<ProducerValueKey, MaterializedProducerRef> &availableValues,
Value graphLane, Value scheduledGraphLane,
DeferredInputPlan &plan) {
plan = {graphInput, {}, {}, {}, graphLane, scheduledGraphLane, {}, {}, {}, {}, 1, nullptr};
if (isDeferredFragmentAssemblyInput(input, consumerInstance)) {
@@ -290,9 +287,14 @@ LogicalResult prepareSingleCpuInput(OpBuilder &, Location loc, Value input, Bloc
auto producer = getProducerValueRef(input, &consumerInstance);
if (!producer) { plan.availableValue = getBlockOperand(block, scheduledInputs, input, firstInputArgument); return success(); }
ProducerValueKey key {producer->instance, producer->resultIndex};
auto carried = llvm::find(carriedKeys, key);
if (carried != carriedKeys.end()) {
plan.availableValue = block.getArgument(firstInputArgument + scheduledInputs.size() + std::distance(carriedKeys.begin(), carried));
auto batch = dyn_cast<SpatComputeBatch>(producer->instance.op);
bool hasCompleteValue = !batch
|| (producer->instance.laneStart == 0
&& producer->instance.laneCount
== static_cast<uint32_t>(batch.getLaneCount()));
if (auto available = availableValues.find(key);
hasCompleteValue && available != availableValues.end()) {
plan.availableValue = available->second.payload;
return success();
}
auto source = getOriginalProducerValue(*producer);
@@ -361,6 +363,29 @@ LogicalResult materializeDeferredPayloadDemands(OpBuilder &builder, Location loc
if (needsIdentity) roots.push_back(plan.graphInput);
llvm::sort(roots, [](Value a, Value b) { return a.getAsOpaquePointer() < b.getAsOpaquePointer(); });
roots.erase(std::unique(roots.begin(), roots.end()), roots.end());
Value sharedSource;
if (!plan.blueprint) {
OpBuilder::InsertPoint restore = builder.saveInsertionPoint();
if (plan.scalarizedGraphLaneBase && plan.originalSources.size() > 1) {
Operation *loop = builder.getInsertionBlock()->getParentOp();
if (loop && !isa<scf::ForOp>(loop))
loop = loop->getParentOfType<scf::ForOp>();
if (loop)
builder.setInsertionPoint(loop);
else if (plan.scalarizedHoistBlock)
builder.setInsertionPointToEnd(plan.scalarizedHoistBlock);
else
return emitError(loc) << "phase 1 scalarized deferred source is missing a hoist point";
}
auto selected = buildSelectedDeferredSource(
builder, loc, plan.graphInput.getOwner()->getParentOp(),
plan.scheduledLane, plan.originalSources,
plan.sourceOperandForScheduledLane);
if (failed(selected))
return failure();
sharedSource = *selected;
builder.restoreInsertionPoint(restore);
}
for (Value root : roots) {
llvm::SmallPtrSet<Operation *, 16> laneDependencies;
bool scalarize = plan.scalarizedGraphLaneBase
@@ -378,40 +403,68 @@ LogicalResult materializeDeferredPayloadDemands(OpBuilder &builder, Location loc
else
return emitError(loc) << "phase 1 scalarized deferred payload is missing a hoist point";
}
SmallVector<Value> payloads;
unsigned count = scalarize ? plan.scalarizedLaneCount : 1;
for (unsigned offset = 0; offset < count; ++offset) {
auto transfer = SpatDeferredCommunicationOp::create(builder, loc, root.getType(), plan.originalSources);
bool grouped = count > 1;
auto fragmentType = dyn_cast<RankedTensorType>(root.getType());
if (!fragmentType || !fragmentType.hasStaticShape())
return emitError(loc) << "phase 1 deferred payload requires a static ranked tensor";
Type outputType = fragmentType;
if (grouped) {
SmallVector<int64_t> groupedShape {static_cast<int64_t>(count)};
llvm::append_range(groupedShape, fragmentType.getShape());
outputType = RankedTensorType::get(groupedShape,
fragmentType.getElementType());
}
auto specialization = scalarize
? builder.getI64IntegerAttr(count)
: IntegerAttr();
SmallVector<Value> transferSources;
if (plan.blueprint)
llvm::append_range(transferSources, plan.originalSources);
else
transferSources.push_back(sharedSource);
auto transfer = SpatDeferredCommunicationOp::create(
builder, loc, outputType, transferSources, specialization);
SmallVector<Type> bodyTypes(transfer.getSources().getTypes());
if (grouped)
bodyTypes.push_back(builder.getIndexType());
Block *deferred = builder.createBlock(&transfer.getBody(), transfer.getBody().end(),
TypeRange {transfer.getSources().getTypes()}, SmallVector<Location>(transfer.getSources().size(), loc));
bodyTypes, SmallVector<Location>(bodyTypes.size(), loc));
builder.setInsertionPointToStart(deferred);
ValueRange sourceArguments = deferred->getArguments().take_front(
transfer.getSources().size());
auto selected = plan.blueprint
? buildBlueprintReconstruction(builder, loc, plan.blueprint,
deferred->getArguments())
: buildSelectedDeferredSource(builder, loc, transfer,
plan.scheduledLane,
deferred->getArguments(),
plan.sourceOperandForScheduledLane);
sourceArguments)
: FailureOr<Value>(sourceArguments.front());
if (failed(selected)) return failure();
Value boundGraphLane;
if (scalarize) {
boundGraphLane = affineAddConst(
builder, loc, plan.scalarizedGraphLaneBase, offset, transfer.getOperation());
}
if (grouped)
boundGraphLane = arith::AddIOp::create(
builder, loc, plan.scalarizedGraphLaneBase,
deferred->getArguments().back());
else if (scalarize)
boundGraphLane = plan.scalarizedGraphLaneBase;
auto payload = clonePayloadRoot(root, body, plan, builder, transfer, *selected, boundGraphLane);
if (failed(payload)) return failure();
SpatYieldOp::create(builder, loc, *payload);
payloads.push_back(transfer.getOutput());
builder.setInsertionPointAfter(transfer);
}
if (scalarize) {
if (grouped) {
builder.restoreInsertionPoint(restore);
auto selected = buildIndexSwitchSelection(
builder, loc, plan.scalarizedLocalLane, payloads, root.getDefiningOp());
if (failed(selected)) return failure();
mapper.map(root, *selected);
SmallVector<OpFoldResult> offsets {
plan.scalarizedLocalLane};
SmallVector<OpFoldResult> sizes {builder.getIndexAttr(1)};
SmallVector<OpFoldResult> strides {builder.getIndexAttr(1)};
for (int64_t dimension : fragmentType.getShape()) {
offsets.push_back(builder.getIndexAttr(0));
sizes.push_back(builder.getIndexAttr(dimension));
strides.push_back(builder.getIndexAttr(1));
}
mapper.map(root, extractMixedSliceOrIdentity(
builder, loc, transfer.getOutput(), fragmentType,
{offsets, sizes, strides}));
} else {
mapper.map(root, payloads.front());
mapper.map(root, transfer.getOutput());
}
collectClosure(root, body, plan, absorbed);
}
@@ -31,7 +31,7 @@ LogicalResult prepareSingleCpuInput(OpBuilder &builder, Location loc, Value inpu
const MergeScheduleResult &schedule,
ValueRange scheduledInputs, Block &block,
unsigned firstInputArgument,
ArrayRef<ProducerValueKey> carriedKeys,
const DenseMap<ProducerValueKey, MaterializedProducerRef> &availableValues,
Value graphLane, Value scheduledGraphLane,
DeferredInputPlan &plan);
@@ -12,141 +12,6 @@ namespace onnx_mlir::spatial {
using namespace mlir;
namespace {
static LogicalResult validateScalarLinearization(ScheduledInfo &info) {
auto scheduled = cast<SpatScheduledCompute>(info.op);
for (unsigned index = 1; index < info.blocks.size(); ++index) {
auto previous = dyn_cast<SpatBlockYieldOp>(
info.blocks[index - 1]->getTerminator());
if (!previous
|| previous.getOutputs().size()
!= info.blocks[index]->getNumArguments())
return scheduled.emitOpError(
"phase 2 cannot linearize malformed scalar scheduled blocks");
for (auto [argument, value] : llvm::zip(
info.blocks[index]->getArguments(), previous.getOutputs())) {
if (argument.getType() == value.getType())
continue;
for (Operation *user : argument.getUsers())
if (!isa<SpatBlockYieldOp>(user))
return scheduled.emitOpError(
"phase 2 cannot linearize a live mismatched carried value");
}
}
return success();
}
static LogicalResult linearizeScalar(ScheduledInfo &info,
IRRewriter &rewriter) {
auto scheduled = cast<SpatScheduledCompute>(info.op);
if (failed(validateScalarLinearization(info)))
return failure();
Block *first = info.blocks.front();
SmallVector<SmallVector<Value>> incoming(info.blocks.size());
for (unsigned index = 1; index < info.blocks.size(); ++index) {
auto previous = cast<SpatBlockYieldOp>(
info.blocks[index - 1]->getTerminator());
incoming[index].assign(previous.getOutputs().begin(),
previous.getOutputs().end());
}
IRMapping carried;
for (unsigned index = 1; index < info.blocks.size(); ++index)
for (auto [argument, value] : llvm::zip(
info.blocks[index]->getArguments(), incoming[index])) {
Value resolved = carried.lookupOrDefault(value);
if (argument.getType() == resolved.getType()) {
carried.map(argument, resolved);
argument.replaceAllUsesWith(resolved);
}
}
for (unsigned index = 1; index < info.blocks.size(); ++index)
for (Operation &op : llvm::make_early_inc_range(
info.blocks[index]->without_terminator()))
op.moveBefore(first->getTerminator());
for (Block *block : info.blocks)
cast<SpatBlockYieldOp>(block->getTerminator()).erase();
SmallVector<Value> outputs(scheduled.getNumResults());
for (ProducedValue *produced : info.produced) {
unsigned result = info.resultOffsets[produced->step]
+ produced->resultIndex;
outputs[result] = produced->payload;
}
if (llvm::any_of(outputs, [](Value output) { return !output; }))
return scheduled.emitOpError(
"phase 2 cannot recover every scheduled scalar result");
rewriter.setInsertionPointToEnd(first);
SpatYieldOp::create(rewriter, scheduled.getLoc(), outputs);
scheduled->setAttr("scheduled.realized", rewriter.getBoolAttr(true));
for (unsigned index = 1; index < info.blocks.size(); ++index) {
if (llvm::any_of(info.blocks[index]->getArguments(),
[](BlockArgument argument) {
return !argument.use_empty();
}))
return scheduled.emitOpError(
"phase 2 scalar linearization left a live block argument");
info.blocks[index]->erase();
}
info.blocks.assign(1, first);
return success();
}
static LogicalResult linearizeBatch(ScheduledInfo &info,
IRRewriter &rewriter) {
auto scheduled = cast<SpatScheduledComputeBatch>(info.op);
Block *first = info.blocks.front();
SmallVector<SpatInParallelOp> terminators;
for (Block *block : info.blocks) {
auto parallel = dyn_cast<SpatInParallelOp>(block->getTerminator());
if (!parallel)
return scheduled.emitOpError(
"phase 2 cannot linearize a batch block without spat.in_parallel");
terminators.push_back(parallel);
}
for (unsigned index = 1; index < info.blocks.size(); ++index)
for (auto [argument, firstArgument] : llvm::zip(
info.blocks[index]->getArguments(), first->getArguments())) {
if (argument.getType() != firstArgument.getType())
return scheduled.emitOpError(
"phase 2 cannot linearize incompatible batch block arguments");
argument.replaceAllUsesWith(firstArgument);
}
for (unsigned index = 1; index < info.blocks.size(); ++index)
for (Operation &op : llvm::make_early_inc_range(
info.blocks[index]->without_terminator()))
op.moveBefore(first->getTerminator());
rewriter.setInsertionPoint(first->getTerminator());
auto combined = SpatInParallelOp::create(rewriter, scheduled.getLoc());
Block &combinedBlock = combined.getRegion().front();
for (SpatInParallelOp parallel : terminators)
for (Operation &op : llvm::make_early_inc_range(
parallel.getRegion().front()))
op.moveBefore(&combinedBlock, combinedBlock.end());
for (SpatInParallelOp parallel : terminators)
parallel.erase();
scheduled->setAttr("scheduled.realized", rewriter.getBoolAttr(true));
for (unsigned index = 1; index < info.blocks.size(); ++index) {
if (llvm::any_of(info.blocks[index]->getArguments(),
[](BlockArgument argument) {
return !argument.use_empty();
}))
return scheduled.emitOpError(
"phase 2 batch linearization left a live block argument");
info.blocks[index]->erase();
}
info.blocks.assign(1, first);
return success();
}
static LogicalResult linearizeScheduled(
DeferredTransferPlan &plan, IRRewriter &rewriter) {
for (ScheduledInfo &info : plan.scheduled) {
LogicalResult result = info.isBatch()
? linearizeBatch(info, rewriter) : linearizeScalar(info, rewriter);
if (failed(result))
return failure();
}
return success();
}
static LogicalResult replaceFinalGraphPublications(
func::FuncOp funcOp, DeferredTransferPlan &plan) {
for (Operation &op : funcOp.getOps()) {
@@ -157,15 +22,30 @@ static LogicalResult replaceFinalGraphPublications(
continue;
for (auto [resultIndex, result] : llvm::enumerate(op.getResults())) {
SmallVector<OpOperand *> externalUses;
for (OpOperand &use : result.getUses())
if (!isa<SpatGraphCompute, SpatGraphComputeBatch>(use.getOwner()))
externalUses.push_back(&use);
for (OpOperand &use : result.getUses()) {
Operation *user = use.getOwner();
if (isa<SpatGraphCompute, SpatGraphComputeBatch,
SpatDeferredCommunicationOp>(user))
continue;
if (auto blueprint = dyn_cast<SpatBlueprintOp>(user)) {
bool blueprintEscapes = llvm::any_of(
blueprint.getOutput().getUses(), [](OpOperand &blueprintUse) {
return !isa<SpatGraphCompute, SpatGraphComputeBatch,
SpatDeferredCommunicationOp>(
blueprintUse.getOwner());
});
if (!blueprintEscapes)
continue;
}
externalUses.push_back(&use);
}
if (externalUses.empty())
continue;
SmallVector<Value> exact;
for (ProducedValue *produced :
plan.producedByGraph.lookup(graphId.getInt()))
if (produced->resultIndex == resultIndex
&& produced->published
&& produced->published.getType() == result.getType()
&& !llvm::is_contained(exact, produced->published))
exact.push_back(produced->published);
@@ -187,23 +67,36 @@ static LogicalResult replaceFinalGraphPublications(
static LogicalResult eraseOldGraph(func::FuncOp funcOp,
IRRewriter &rewriter) {
SmallVector<Operation *> graphOps;
SmallVector<Operation *> oldGraph;
for (Operation &op : funcOp.getOps())
if (isa<SpatGraphCompute, SpatGraphComputeBatch>(op))
graphOps.push_back(&op);
for (Operation *op : llvm::reverse(graphOps)) {
if (isa<SpatGraphCompute, SpatGraphComputeBatch, SpatBlueprintOp>(op))
oldGraph.push_back(&op);
for (Operation *op : llvm::reverse(oldGraph)) {
if (auto blueprint = dyn_cast<SpatBlueprintOp>(op)) {
if (blueprint.getOutput().use_empty())
rewriter.eraseOp(blueprint);
continue;
}
if (!op->use_empty())
return op->emitOpError(
"phase 2 cannot erase an old graph compute with live results");
rewriter.eraseOp(op);
}
SmallVector<SpatBlueprintOp> deadBlueprints;
funcOp.walk([&](SpatBlueprintOp blueprint) {
if (blueprint.getOutput().use_empty())
deadBlueprints.push_back(blueprint);
return success();
}
static LogicalResult eraseDeferredSourceSelectors(
func::FuncOp funcOp, IRRewriter &rewriter) {
SmallVector<SpatDeferredSourceSelectOp> selectors;
funcOp.walk([&](SpatDeferredSourceSelectOp selector) {
selectors.push_back(selector);
});
for (SpatBlueprintOp blueprint : deadBlueprints)
rewriter.eraseOp(blueprint);
for (SpatDeferredSourceSelectOp selector : llvm::reverse(selectors)) {
if (!selector.getOutput().use_empty())
return selector.emitOpError(
"phase 2 left a live deferred source selection");
rewriter.eraseOp(selector);
}
return success();
}
@@ -223,8 +116,10 @@ static LogicalResult verifyDominance(func::FuncOp funcOp) {
} // namespace
LogicalResult realizeDeferredCommunication(func::FuncOp funcOp) {
auto transfers = buildDeferredTransferPlan(funcOp);
LogicalResult realizeDeferredCommunication(
func::FuncOp funcOp,
const ScheduledComputeMaterializationResult &materialization) {
auto transfers = buildDeferredTransferPlan(funcOp, materialization);
if (failed(transfers))
return funcOp.emitOpError(
"phase 2 failed to build symbolic transfer families");
@@ -240,7 +135,8 @@ LogicalResult realizeDeferredCommunication(func::FuncOp funcOp) {
"phase 2 failed to build sparse boundary programs");
IRRewriter rewriter(funcOp.getContext());
if (failed(linearizeScheduled(*transfers, rewriter)))
if (failed(retargetDeferredPublications(funcOp, *transfers))
|| failed(replaceFinalGraphPublications(funcOp, *transfers)))
return failure();
ConstantPool constants(funcOp, rewriter);
DeferredEmissionContext context(rewriter, constants);
@@ -258,8 +154,7 @@ LogicalResult realizeDeferredCommunication(func::FuncOp funcOp) {
"phase 2 cannot erase deferred communication with live uses");
rewriter.eraseOp(op);
}
if (failed(retargetDeferredPublications(funcOp, *transfers))
|| failed(replaceFinalGraphPublications(funcOp, *transfers))
if (failed(eraseDeferredSourceSelectors(funcOp, rewriter))
|| failed(eraseOldGraph(funcOp, rewriter))
|| failed(verifyDominance(funcOp))
|| failed(verifyRealizedCommunicationDeadlockFree(funcOp, *schedule)))
@@ -4,6 +4,10 @@
namespace onnx_mlir::spatial {
mlir::LogicalResult realizeDeferredCommunication(mlir::func::FuncOp funcOp);
struct ScheduledComputeMaterializationResult;
mlir::LogicalResult realizeDeferredCommunication(
mlir::func::FuncOp funcOp,
const ScheduledComputeMaterializationResult &materialization);
} // namespace onnx_mlir::spatial
@@ -10,6 +10,20 @@ namespace onnx_mlir::spatial {
using namespace mlir;
namespace {
using TransferEmissionSignature =
std::tuple<ScheduledInfo*, Value, Type, bool, bool, bool>;
static TransferEmissionSignature getTransferEmissionSignature(
const ExternalTransferFamily& family) {
ProducedValue* producer = family.requirement->producer;
return {producer->scheduled,
producer->payload,
family.requirement->publicationFragmentType,
family.requirement->graphLanes.has_value(),
family.requirement->producerProjection.has_value(),
producer->scheduled->isBatch()};
}
struct StreamThreshold {
unsigned stream = 0;
unsigned completedStep = 0;
@@ -20,11 +34,10 @@ struct TransferGroup {
SmallVector<ScheduledTransferSlice> ordered;
SmallVector<StreamThreshold> dependencies;
unsigned unsatisfied = 0;
bool ready = false;
bool scheduled = false;
BoundaryCost cost;
std::tuple<unsigned, unsigned, unsigned, uint64_t> originalPriority;
std::optional<TransferEmissionSignature> firstSignature;
std::tuple<unsigned, unsigned, unsigned,
std::tuple<unsigned, unsigned, unsigned, uint64_t>> staticPriority;
};
struct StreamProgress {
@@ -35,28 +48,16 @@ struct StreamProgress {
};
static size_t hashSignature(const TransferEmissionSignature& signature) {
return llvm::hash_combine(signature.scheduled,
signature.payload.getAsOpaquePointer(),
signature.fragmentType.getAsOpaquePointer(),
signature.hasGraphLane,
signature.hasProducerProjection,
signature.sourceIsBatch);
return llvm::hash_combine(std::get<0>(signature),
std::get<1>(signature).getAsOpaquePointer(),
std::get<2>(signature).getAsOpaquePointer(),
std::get<3>(signature),
std::get<4>(signature),
std::get<5>(signature));
}
static bool betterStaticPriority(const TransferGroup& lhs, const TransferGroup& rhs) {
auto left = std::tuple(lhs.cost.instructionCount,
lhs.cost.branchRegions,
std::numeric_limits<unsigned>::max()
- lhs.cost.absorbedTransfers,
lhs.cost.lookupEntries,
lhs.originalPriority);
auto right = std::tuple(rhs.cost.instructionCount,
rhs.cost.branchRegions,
std::numeric_limits<unsigned>::max()
- rhs.cost.absorbedTransfers,
rhs.cost.lookupEntries,
rhs.originalPriority);
return left < right;
return lhs.staticPriority < rhs.staticPriority;
}
static bool orderPermutationCycles(SmallVectorImpl<ScheduledTransferSlice>& slices) {
@@ -148,9 +149,29 @@ static void orderGroupSlices(TransferGroup& group) {
});
llvm::append_range(group.ordered, slicesBySignature[id]);
}
group.cost = estimateCanonicalBoundaryCost({}, group.ordered);
if (!group.ordered.empty())
group.firstSignature = getTransferEmissionSignature(*group.ordered.front().family);
unsigned instructionCount = 0;
unsigned absorbedTransfers = 0;
unsigned transferCount = 0;
std::optional<TransferEmissionSignature> previousSignature;
RequirementFamily* previousRequirement = nullptr;
for (const ScheduledTransferSlice& slice : group.ordered) {
TransferEmissionSignature signature =
getTransferEmissionSignature(*slice.family);
if (!previousSignature || *previousSignature != signature)
++instructionCount;
else
absorbedTransfers += slice.transferCount;
previousSignature = signature;
if (previousRequirement != slice.family->requirement)
++instructionCount;
previousRequirement = slice.family->requirement;
transferCount += slice.transferCount;
}
group.staticPriority =
{instructionCount,
std::numeric_limits<unsigned>::max() - absorbedTransfers,
transferCount,
group.originalPriority};
}
static SmallVector<TransferGroup> buildGroups(DeferredTransferPlan& plan) {
@@ -213,39 +234,9 @@ static unsigned tailExtension(const TransferGroup& group,
} // namespace
TransferEmissionSignature getTransferEmissionSignature(const ExternalTransferFamily& family) {
ProducedValue* producer = family.requirement->producer;
return {producer->scheduled,
producer->payload,
family.requirement->publicationFragmentType,
family.requirement->graphLanes.has_value(),
family.requirement->producerProjection.has_value(),
producer->scheduled->isBatch()};
}
BoundaryCost estimateCanonicalBoundaryCost(
ArrayRef<ScheduledTransferSlice> existingTail,
ArrayRef<ScheduledTransferSlice> candidate) {
BoundaryCost cost;
std::optional<TransferEmissionSignature> previous;
RequirementFamily* previousRequirement = nullptr;
if (!existingTail.empty()) {
previous = getTransferEmissionSignature(*existingTail.back().family);
previousRequirement = existingTail.back().family->requirement;
}
for (const ScheduledTransferSlice& slice : candidate) {
TransferEmissionSignature signature = getTransferEmissionSignature(*slice.family);
if (!previous || !(*previous == signature))
++cost.instructionCount;
else
cost.absorbedTransfers += slice.transferCount;
previous = signature;
if (previousRequirement != slice.family->requirement)
++cost.instructionCount;
previousRequirement = slice.family->requirement;
cost.lookupEntries += slice.transferCount;
}
return cost;
bool haveSameTransferEmissionSignature(
const ExternalTransferFamily& lhs, const ExternalTransferFamily& rhs) {
return getTransferEmissionSignature(lhs) == getTransferEmissionSignature(rhs);
}
FailureOr<ScheduledCommunicationPlan> scheduleDeferredCommunication(func::FuncOp funcOp, DeferredTransferPlan& plan) {
@@ -269,16 +260,12 @@ FailureOr<ScheduledCommunicationPlan> scheduleDeferredCommunication(func::FuncOp
DenseMap<size_t, std::unique_ptr<Heap>> bySignature;
auto addReady = [&](unsigned index) {
TransferGroup& group = groups[index];
if (group.ready || group.scheduled)
return;
group.ready = true;
ready.push(index);
if (group.firstSignature) {
auto& bucket = bySignature[hashSignature(*group.firstSignature)];
if (!bucket)
bucket = std::make_unique<Heap>(compare);
bucket->push(index);
}
auto& bucket = bySignature[hashSignature(
getTransferEmissionSignature(*group.ordered.front().family))];
if (!bucket)
bucket = std::make_unique<Heap>(compare);
bucket->push(index);
};
for (auto [index, group] : llvm::enumerate(groups))
if (group.unsatisfied == 0)
@@ -331,7 +318,7 @@ FailureOr<ScheduledCommunicationPlan> scheduleDeferredCommunication(func::FuncOp
unsigned candidate = bucket->second->top();
bucket->second->pop();
TransferGroup& group = groups[candidate];
if (!group.ready || group.scheduled)
if (group.scheduled)
continue;
inspected.push_back(candidate);
unsigned extension = tailExtension(
@@ -350,12 +337,11 @@ FailureOr<ScheduledCommunicationPlan> scheduleDeferredCommunication(func::FuncOp
while (!chosen && !ready.empty()) {
unsigned candidate = ready.top();
ready.pop();
if (groups[candidate].ready && !groups[candidate].scheduled)
if (!groups[candidate].scheduled)
chosen = candidate;
}
if (chosen) {
TransferGroup& group = groups[*chosen];
group.ready = false;
group.scheduled = true;
++finishedGroups;
for (ScheduledTransferSlice slice : group.ordered) {
@@ -21,35 +21,8 @@ struct ScheduledCommunicationPlan {
uint64_t logicalTransferCount = 0;
};
struct TransferEmissionSignature {
ScheduledInfo* scheduled = nullptr;
mlir::Value payload;
mlir::Type fragmentType;
bool hasGraphLane = false;
bool hasProducerProjection = false;
bool sourceIsBatch = false;
bool operator==(const TransferEmissionSignature& other) const {
return scheduled == other.scheduled && payload == other.payload
&& fragmentType == other.fragmentType
&& hasGraphLane == other.hasGraphLane
&& hasProducerProjection == other.hasProducerProjection
&& sourceIsBatch == other.sourceIsBatch;
}
};
struct BoundaryCost {
unsigned instructionCount = 0;
unsigned branchRegions = 0;
unsigned absorbedTransfers = 0;
unsigned lookupEntries = 0;
};
TransferEmissionSignature getTransferEmissionSignature(const ExternalTransferFamily& family);
BoundaryCost estimateCanonicalBoundaryCost(
mlir::ArrayRef<ScheduledTransferSlice> existingTail,
mlir::ArrayRef<ScheduledTransferSlice> candidate);
bool haveSameTransferEmissionSignature(
const ExternalTransferFamily& lhs, const ExternalTransferFamily& rhs);
mlir::FailureOr<ScheduledCommunicationPlan> scheduleDeferredCommunication(mlir::func::FuncOp funcOp,
DeferredTransferPlan& plan);
@@ -1,6 +1,7 @@
#include "DeferredProjectionAnalysis.hpp"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/Linalg/IR/Linalg.h"
#include "mlir/Dialect/SCF/IR/SCF.h"
#include "mlir/IR/Matchers.h"
@@ -109,27 +110,20 @@ static FailureOr<std::optional<unsigned>> sourceArgument(
&& argument.getArgNumber() < deferred.getSources().size())
return std::optional<unsigned>(argument.getArgNumber());
auto result = dyn_cast<OpResult>(value);
auto selection = result
? dyn_cast<scf::IndexSwitchOp>(result.getOwner()) : scf::IndexSwitchOp();
if (!selection || result.getResultNumber() != 0
|| selection.getNumResults() != 1)
return std::optional<unsigned>();
auto selector = evaluateDeferredIndex(selection.getArg(), environment);
if (failed(selector))
return failure();
Region *selected = &selection.getDefaultRegion();
for (auto [caseValue, region] :
llvm::zip(selection.getCases(), selection.getCaseRegions()))
if (caseValue == *selector) {
selected = &region;
break;
}
auto yield = selected->hasOneBlock()
? dyn_cast<scf::YieldOp>(selected->front().getTerminator())
: scf::YieldOp();
return yield && yield.getResults().size() == 1
? sourceArgument(yield.getResults().front(), deferred, environment)
: FailureOr<std::optional<unsigned>>(failure());
auto compactSelection = result
? dyn_cast<SpatDeferredSourceSelectOp>(result.getOwner())
: SpatDeferredSourceSelectOp();
if (compactSelection && result.getResultNumber() == 0) {
auto selector = evaluateDeferredIndex(
compactSelection.getSelector(), environment);
if (failed(selector) || *selector < 0
|| *selector >= static_cast<int64_t>(
compactSelection.getSources().size()))
return failure();
return sourceArgument(
compactSelection.getSources()[*selector], deferred, environment);
}
return std::optional<unsigned>();
}
static void collectSourceArguments(Value value,
@@ -144,17 +138,13 @@ static void collectSourceArguments(Value value,
return;
}
auto result = dyn_cast<OpResult>(value);
auto selection = result
? dyn_cast<scf::IndexSwitchOp>(result.getOwner()) : scf::IndexSwitchOp();
if (!selection || result.getResultNumber() != 0)
return;
for (Region &region : selection.getCaseRegions())
collectSourceArguments(
cast<scf::YieldOp>(region.front().getTerminator()).getResults().front(),
deferred, indices);
collectSourceArguments(
cast<scf::YieldOp>(selection.getDefaultRegion().front().getTerminator())
.getResults().front(), deferred, indices);
auto compactSelection = result
? dyn_cast<SpatDeferredSourceSelectOp>(result.getOwner())
: SpatDeferredSourceSelectOp();
if (compactSelection && result.getResultNumber() == 0) {
for (Value source : compactSelection.getSources())
collectSourceArguments(source, deferred, indices);
}
}
static Value getEnclosingScheduledLane(
@@ -168,34 +158,6 @@ static Value getEnclosingScheduledLane(
return block && !block->empty() ? block->getArgument(0) : Value();
}
static bool isAllowedStaticIndexExpression(
Value value, Value scheduledLane,
llvm::SmallDenseSet<Value, 16> &visiting) {
if (value == scheduledLane)
return true;
Attribute constant;
if (matchPattern(value, m_Constant(&constant)))
return true;
if (isa<BlockArgument>(value) || !value.getDefiningOp()
|| !visiting.insert(value).second)
return false;
Operation *op = value.getDefiningOp();
bool allowed = op->getNumRegions() == 0
&& (isPureIndexComputationOp(op) || isCompileTimeOp(op))
&& llvm::all_of(op->getOperands(), [&](Value operand) {
return isAllowedStaticIndexExpression(
operand, scheduledLane, visiting);
});
visiting.erase(value);
return allowed;
}
static bool isAllowedStaticIndexExpression(Value value,
Value scheduledLane) {
llvm::SmallDenseSet<Value, 16> visiting;
return isAllowedStaticIndexExpression(value, scheduledLane, visiting);
}
static bool originatesFromDeferredSource(
Value value, SpatDeferredCommunicationOp deferred,
llvm::SmallDenseSet<Value, 16> &visited) {
@@ -205,10 +167,9 @@ static bool originatesFromDeferredSource(
return argument.getOwner() == &deferred.getBody().front()
&& argument.getArgNumber() < deferred.getSources().size();
Operation *op = value.getDefiningOp();
return op && (isa<scf::IndexSwitchOp>(op)
|| llvm::any_of(op->getOperands(), [&](Value operand) {
return originatesFromDeferredSource(operand, deferred, visited);
}));
return op && llvm::any_of(op->getOperands(), [&](Value operand) {
return originatesFromDeferredSource(operand, deferred, visited);
});
}
static bool originatesFromDeferredSource(
@@ -217,43 +178,6 @@ static bool originatesFromDeferredSource(
return originatesFromDeferredSource(value, deferred, visited);
}
static LogicalResult verifyCanonicalSourceSelector(
scf::IndexSwitchOp selection, SpatDeferredCommunicationOp deferred,
Value scheduledLane, int64_t laneCount) {
if (selection->getBlock() != &deferred.getBody().front()
|| selection.getNumResults() != 1
|| !selection.getArg().getType().isIndex()
|| !isAllowedStaticIndexExpression(selection.getArg(), scheduledLane))
return selection.emitOpError(
"is not a canonical deferred source selector");
if (laneCount < 2
|| selection.getCases().size() != static_cast<size_t>(laneCount - 1))
return selection.emitOpError(
"must cover every non-default scheduled lane");
for (auto [index, caseValue] : llvm::enumerate(selection.getCases()))
if (caseValue != static_cast<int64_t>(index))
return selection.emitOpError(
"must use consecutive scheduled-lane cases starting at zero");
auto verifyRegion = [&](Region &region) -> LogicalResult {
auto yield = region.hasOneBlock()
? dyn_cast<scf::YieldOp>(region.front().getTerminator()) : scf::YieldOp();
if (!yield || yield.getResults().size() != 1)
return selection.emitOpError(
"source-selector region must yield one result");
for (Operation &op : region.front().without_terminator())
if (!isa<tensor::CastOp>(op))
return selection.emitOpError(
"source-selector regions may contain only tensor.cast");
llvm::SmallSet<unsigned, 4> indices;
collectSourceArguments(yield.getResults().front(), deferred, indices);
return success(indices.size() == 1);
};
for (Region &region : selection.getCaseRegions())
if (failed(verifyRegion(region)))
return failure();
return verifyRegion(selection.getDefaultRegion());
}
static bool isInsideDeferredLoop(Operation *op,
SpatDeferredCommunicationOp deferred) {
for (Operation *parent = op->getParentOp(); parent && parent != deferred;
@@ -263,6 +187,83 @@ static bool isInsideDeferredLoop(Operation *op,
return false;
}
static FailureOr<SmallVector<unsigned>>
getPossibleDeferredSourceOperandIndices(
Value sourceRoot, SpatDeferredCommunicationOp deferred) {
llvm::SmallSet<unsigned, 4> indices;
collectSourceArguments(sourceRoot, deferred, indices);
if (indices.empty())
return failure();
return SmallVector<unsigned>(indices.begin(), indices.end());
}
static LogicalResult validateDeferredProgram(
SpatDeferredCommunicationOp deferred, Block &body, Value scheduledLane,
Value specialization, int64_t laneCount, int64_t specializationCount) {
auto specializes = [&](Value value, auto &&accept) {
for (int64_t specializationIndex = 0;
specializationIndex < specializationCount; ++specializationIndex)
for (int64_t lane = 0; lane < laneCount; ++lane) {
StaticIndexEnvironment environment;
if (scheduledLane)
environment.bindings[scheduledLane] = lane;
if (specialization)
environment.bindings[specialization] = specializationIndex;
auto result = evaluateDeferredIndex(value, environment);
if (failed(result) || !accept(*result))
return false;
}
return true;
};
WalkResult result = body.walk([&](Operation *op) -> WalkResult {
auto reject = [&](StringRef message) {
op->emitOpError(message);
return WalkResult::interrupt();
};
if (isa<SpatYieldOp, scf::YieldOp>(op)
|| (isa<linalg::YieldOp>(op)
&& isa<linalg::TransposeOp>(op->getParentOp())))
return WalkResult::advance();
if (auto selection = dyn_cast<SpatDeferredSourceSelectOp>(op)) {
if (selection->getBlock() != &body)
return reject("must be a top-level deferred source selector");
if (selection.getSources().empty()
|| !selection.getSelector().getType().isIndex()
|| llvm::any_of(selection.getSources(), [&](Value source) {
return source.getType() != selection.getOutput().getType()
|| failed(getPossibleDeferredSourceOperandIndices(
source, deferred));
}))
return reject("has invalid deferred source operands or types");
if (!specializes(selection.getSelector(), [&](int64_t index) {
return index >= 0
&& index < static_cast<int64_t>(selection.getSources().size());
}))
return reject("source selector does not specialize to a valid source");
return WalkResult::advance();
}
if (auto loop = dyn_cast<scf::ForOp>(op)) {
for (Value bound : {loop.getLowerBound(), loop.getUpperBound()})
if (!specializes(bound, [](int64_t) { return true; }))
return reject("has a loop bound that does not specialize");
if (!specializes(loop.getStep(), [](int64_t step) { return step > 0; }))
return reject("has a non-positive or non-static deferred loop step");
return WalkResult::advance();
}
if (op->getNumRegions() != 0 && !isa<linalg::TransposeOp>(op))
return reject("contains an unsupported region operation");
if (!isShapingOnlyOp(op) && !isCompileTimeOp(op)
&& !isPureIndexComputationOp(op))
return reject("contains an unsupported deferred operation");
for (Value operand : op->getOperands())
if (isInsideDeferredLoop(op, deferred)
&& originatesFromDeferredSource(operand, deferred))
return reject("projects a deferred source inside a residual loop");
return WalkResult::advance();
});
return failure(result.wasInterrupted());
}
static bool haveLeadingUnitDifference(RankedTensorType larger,
RankedTensorType smaller) {
return larger && smaller && larger.hasStaticShape() && smaller.hasStaticShape()
@@ -314,7 +315,7 @@ analyzeInsertAssembly(const DeferredProgramTemplate &program) {
if (!transform)
return std::optional<DeferredInsertAssemblyTemplate>();
DeferredInsertAssemblyEntryTemplate entry;
entry.coordinate = {leaf->second, 0};
entry.coordinate = {0, leaf->second, 0};
entry.sourceTransform = *transform;
entry.sourceType = insertedType;
entry.targetGeometry = {SmallVector<OpFoldResult>(insert.getMixedOffsets()),
@@ -345,66 +346,6 @@ analyzeInsertAssembly(const DeferredProgramTemplate &program) {
} // namespace
LogicalResult verifyDeferredProgramContract(
SpatDeferredCommunicationOp deferred) {
if (!deferred.getBody().hasOneBlock())
return deferred.emitOpError(
"deferred program must have exactly one body block");
Block &body = deferred.getBody().front();
auto terminator = dyn_cast<SpatYieldOp>(body.getTerminator());
if (!terminator || terminator.getOutputs().size() != 1)
return deferred.emitOpError(
"deferred program must have exactly one yielded value");
Value scheduledLane;
int64_t laneCount = 1;
if (auto scheduled = deferred->getParentOfType<SpatScheduledComputeBatch>()) {
scheduledLane = getEnclosingScheduledLane(deferred, scheduled);
laneCount = scheduled.getLaneCount();
}
bool invalid = false;
deferred.getBody().walk([&](Operation *op) {
auto reject = [&](StringRef message) {
op->emitOpError(message);
invalid = true;
};
if (invalid || isa<SpatYieldOp, scf::YieldOp>(op))
return;
if (auto selection = dyn_cast<scf::IndexSwitchOp>(op)) {
if (failed(verifyCanonicalSourceSelector(
selection, deferred, scheduledLane, laneCount)))
invalid = true;
return;
}
if (auto loop = dyn_cast<scf::ForOp>(op)) {
for (Value bound : {loop.getLowerBound(), loop.getUpperBound(),
loop.getStep()})
if (!isAllowedStaticIndexExpression(bound, scheduledLane))
return reject("has a non-static deferred loop bound");
for (int64_t lane = 0; lane < laneCount; ++lane) {
StaticIndexEnvironment environment;
if (scheduledLane)
environment.bindings[scheduledLane] = lane;
auto lower = evaluateDeferredIndex(loop.getLowerBound(), environment);
auto upper = evaluateDeferredIndex(loop.getUpperBound(), environment);
auto step = evaluateDeferredIndex(loop.getStep(), environment);
if (failed(lower) || failed(upper) || failed(step) || *step <= 0)
return reject("has a loop bound that does not specialize");
}
return;
}
if (op->getNumRegions() != 0)
return reject("contains an unsupported region operation");
if (!isShapingOnlyOp(op) && !isCompileTimeOp(op)
&& !isPureIndexComputationOp(op))
return reject("contains an unsupported deferred operation");
for (Value operand : op->getOperands())
if (isInsideDeferredLoop(op, deferred)
&& originatesFromDeferredSource(operand, deferred))
return reject("projects a deferred source inside a residual loop");
});
return success(!invalid);
}
FailureOr<int64_t> evaluateDeferredIndex(
Value value, const StaticIndexEnvironment &environment) {
llvm::SmallDenseSet<Value, 16> visiting;
@@ -421,18 +362,11 @@ FailureOr<int64_t> evaluateDeferredIndex(
return failure();
}
FailureOr<SmallVector<unsigned>> getPossibleDeferredSourceOperandIndices(
Value sourceRoot, SpatDeferredCommunicationOp deferred) {
llvm::SmallSet<unsigned, 4> indices;
collectSourceArguments(sourceRoot, deferred, indices);
if (indices.empty())
return failure();
return SmallVector<unsigned>(indices.begin(), indices.end());
}
DeferredLaneValueEvaluator::DeferredLaneValueEvaluator(
const DeferredProgramTemplate &program, unsigned laneCount)
: program(program), laneCount(laneCount) {}
const DeferredProgramTemplate &program, unsigned laneCount,
unsigned specializationIndex)
: program(program), laneCount(laneCount),
specializationIndex(specializationIndex) {}
FailureOr<StaticIntSequence> DeferredLaneValueEvaluator::evaluate(Value value) {
if (auto it = values.find(value); it != values.end())
@@ -448,6 +382,9 @@ FailureOr<StaticIntSequence> DeferredLaneValueEvaluator::evaluate(Value value) {
StaticIndexEnvironment environment;
if (program.scheduledLane)
environment.bindings[program.scheduledLane] = lane;
if (program.specializationArgument)
environment.bindings[program.specializationArgument] =
specializationIndex;
auto result = evaluateDeferredIndex(value, environment);
if (failed(result))
return failure();
@@ -483,6 +420,9 @@ DeferredLaneValueEvaluator::resolveSourceOperandIndices(Value sourceRoot) {
StaticIndexEnvironment environment;
if (program.scheduledLane)
environment.bindings[program.scheduledLane] = lane;
if (program.specializationArgument)
environment.bindings[program.specializationArgument] =
specializationIndex;
auto index = sourceArgument(sourceRoot, program.deferred, environment);
if (failed(index) || !*index)
return failure();
@@ -495,6 +435,9 @@ DeferredLaneValueEvaluator::resolveSourceOperandIndices(Value sourceRoot) {
FailureOr<DeferredProgramTemplate> analyzeDeferredProgramTemplate(
SpatDeferredCommunicationOp deferred) {
if (!deferred.getBody().hasOneBlock())
return deferred.emitOpError(
"deferred program must have exactly one body block"), failure();
Block &body = deferred.getBody().front();
auto yield = dyn_cast<SpatYieldOp>(body.getTerminator());
if (!yield || yield.getOutputs().size() != 1)
@@ -503,8 +446,29 @@ FailureOr<DeferredProgramTemplate> analyzeDeferredProgramTemplate(
DeferredProgramTemplate program;
program.deferred = deferred;
program.yieldedValue = yield.getOutputs().front();
auto specialization = deferred->getAttrOfType<IntegerAttr>(
"specialization_count");
program.specializationCount = specialization
? specialization.getInt()
: 1;
if (program.specializationCount > 1) {
program.specializationArgument = body.getArguments().back();
program.specializationFragmentType = dyn_cast<RankedTensorType>(
program.yieldedValue.getType());
if (!program.specializationFragmentType)
return deferred.emitOpError(
"grouped deferred fragment must be a ranked tensor"), failure();
}
if (auto scheduled = deferred->getParentOfType<SpatScheduledComputeBatch>())
program.scheduledLane = getEnclosingScheduledLane(deferred, scheduled);
int64_t laneCount = 1;
if (auto scheduled = deferred->getParentOfType<SpatScheduledComputeBatch>())
laneCount = scheduled.getLaneCount();
if (failed(validateDeferredProgram(
deferred, body, program.scheduledLane,
program.specializationArgument, laneCount,
program.specializationCount)))
return failure();
llvm::SmallDenseSet<Value, 32> visited;
std::function<LogicalResult(Value)> visit = [&](Value value) -> LogicalResult {
@@ -529,7 +493,6 @@ FailureOr<DeferredProgramTemplate> analyzeDeferredProgramTemplate(
: DeferredLeafForm::ScalarProjection;
leaf.sourceRoot = slice.getSource();
leaf.replacementRoot = value;
leaf.leadingProjection = slice;
leaf.leadingGeometry = {
SmallVector<OpFoldResult>(slice.getMixedOffsets()),
SmallVector<OpFoldResult>(slice.getMixedSizes()),
@@ -543,6 +506,19 @@ FailureOr<DeferredProgramTemplate> analyzeDeferredProgramTemplate(
SmallVector<OpFoldResult>(
ArrayRef(slice.getMixedStrides()).drop_front())};
leaf.reconstructedType = cast<RankedTensorType>(value.getType());
if (graphProjection
&& slice.getSourceType().getRank()
== leaf.reconstructedType.getRank() + 1
&& slice.getMixedSizes().size()
== static_cast<size_t>(slice.getSourceType().getRank())) {
ArrayRef<OpFoldResult> innerSizes =
ArrayRef(slice.getMixedSizes()).drop_front();
leaf.leadingRankReduced = llvm::equal(
innerSizes, leaf.reconstructedType.getShape(),
[](OpFoldResult size, int64_t dimension) {
return getConstantIntValue(size) == dimension;
});
}
program.leaves.push_back(std::move(leaf));
return success();
}
@@ -553,7 +529,7 @@ FailureOr<DeferredProgramTemplate> analyzeDeferredProgramTemplate(
return deferred.emitOpError(
"deferred source is not a ranked tensor");
program.leaves.push_back({DeferredLeafForm::DirectSource, value, value,
{}, {}, {}, type});
{}, {}, type});
return success();
}
if (value.getType().isIndex() || isa<IntegerType>(value.getType()))
@@ -586,7 +562,7 @@ FailureOr<DeferredProgramTemplate> analyzeDeferredProgramTemplate(
FailureOr<const GraphBatchPublicationMap *> getGraphBatchPublicationMap(
SpatGraphComputeBatch graphBatch, unsigned resultIndex,
GraphBatchPublicationCache &cache) {
GraphBatchPublicationKey key {graphBatch.getOperation(), resultIndex};
std::pair<Operation *, unsigned> key {graphBatch.getOperation(), resultIndex};
if (auto it = cache.find(key); it != cache.end())
return &it->second;
auto resultType = dyn_cast<RankedTensorType>(
@@ -15,16 +15,14 @@ mlir::FailureOr<int64_t> evaluateDeferredIndex(
mlir::FailureOr<int64_t> evaluateDeferredIndex(
mlir::OpFoldResult value, const StaticIndexEnvironment &environment);
mlir::LogicalResult verifyDeferredProgramContract(
SpatDeferredCommunicationOp deferred);
mlir::FailureOr<DeferredProgramTemplate> analyzeDeferredProgramTemplate(
SpatDeferredCommunicationOp deferred);
class DeferredLaneValueEvaluator {
public:
DeferredLaneValueEvaluator(const DeferredProgramTemplate &program,
unsigned laneCount);
unsigned laneCount,
unsigned specializationIndex = 0);
mlir::FailureOr<StaticIntSequence> evaluate(mlir::Value value);
mlir::FailureOr<StaticIntSequence> evaluate(mlir::OpFoldResult value);
@@ -34,14 +32,11 @@ public:
private:
const DeferredProgramTemplate &program;
unsigned laneCount;
unsigned specializationIndex;
llvm::DenseMap<mlir::Value, StaticIntSequence> values;
llvm::DenseMap<mlir::Value, StaticIntSequence> sourceOperands;
};
mlir::FailureOr<llvm::SmallVector<unsigned>>
getPossibleDeferredSourceOperandIndices(
mlir::Value sourceRoot, SpatDeferredCommunicationOp deferred);
struct GraphBatchPublicationMap {
mlir::RankedTensorType physicalResultType;
mlir::RankedTensorType publicationFragmentType;
@@ -49,39 +44,12 @@ struct GraphBatchPublicationMap {
llvm::SmallVector<int64_t> physicalSlotToGraphLane;
};
struct GraphBatchPublicationKey {
mlir::Operation *graphBatch = nullptr;
unsigned resultIndex = 0;
bool operator==(const GraphBatchPublicationKey &other) const {
return graphBatch == other.graphBatch && resultIndex == other.resultIndex;
}
};
using GraphBatchPublicationCache =
llvm::DenseMap<GraphBatchPublicationKey, GraphBatchPublicationMap>;
llvm::DenseMap<std::pair<mlir::Operation *, unsigned>,
GraphBatchPublicationMap>;
mlir::FailureOr<const GraphBatchPublicationMap *> getGraphBatchPublicationMap(
SpatGraphComputeBatch graphBatch, unsigned resultIndex,
GraphBatchPublicationCache &cache);
} // namespace onnx_mlir::spatial
namespace llvm {
template <> struct DenseMapInfo<onnx_mlir::spatial::GraphBatchPublicationKey> {
static onnx_mlir::spatial::GraphBatchPublicationKey getEmptyKey() {
return {DenseMapInfo<mlir::Operation *>::getEmptyKey(), 0};
}
static onnx_mlir::spatial::GraphBatchPublicationKey getTombstoneKey() {
return {DenseMapInfo<mlir::Operation *>::getTombstoneKey(), 0};
}
static unsigned getHashValue(
const onnx_mlir::spatial::GraphBatchPublicationKey &key) {
return hash_combine(key.graphBatch, key.resultIndex);
}
static bool isEqual(
const onnx_mlir::spatial::GraphBatchPublicationKey &lhs,
const onnx_mlir::spatial::GraphBatchPublicationKey &rhs) {
return lhs == rhs;
}
};
} // namespace llvm
@@ -1,318 +1,338 @@
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/Tensor/IR/Tensor.h"
#include "mlir/IR/IRMapping.h"
#include "DeferredResultRealization.hpp"
#include "DeferredBoundaryRealization.hpp"
#include "DeferredProjectionAnalysis.hpp"
#include "src/Accelerators/PIM/Common/IR/AffineUtils.hpp"
#include "src/Accelerators/PIM/Common/IR/StaticIntSequence.hpp"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/SCF/IR/SCF.h"
#include "mlir/Dialect/Tensor/IR/Tensor.h"
#include "mlir/IR/IRMapping.h"
#include "llvm/ADT/DenseSet.h"
#include <array>
#include "src/Accelerators/PIM/Common/IR/LoopUtils.hpp"
#include "src/Accelerators/PIM/Common/IR/TensorSliceUtils.hpp"
namespace onnx_mlir::spatial {
using namespace mlir;
namespace {
static Value getSequencePosition(
const LaneSet& familyLanes, Value lane, Operation* anchor, DeferredEmissionContext& context, Location loc) {
unsigned begin = familyLanes.intervals().front().begin;
if (!lane)
return context.constants.getIndex(0);
return affineAddConst(context.rewriter, loc, lane, -static_cast<int64_t>(begin), anchor);
}
static OpFoldResult materializeGeometryValue(
const StaticIntSequence& sequence, Value lane, Operation* anchor, DeferredEmissionContext& context, Location loc) {
if (sequence.getKind() == StaticIntSequenceKind::Uniform)
return context.rewriter.getIndexAttr(sequence.valueAt(0));
return emitStaticIntLookup(sequence, lane, anchor, context.constants, context.rewriter, loc);
}
static MixedSliceGeometry materializeGeometry(const DeferredResultPlan::SliceGeometry& geometry,
Value lane,
Operation* anchor,
DeferredEmissionContext& context,
Location loc) {
MixedSliceGeometry result;
for (const StaticIntSequence& value : geometry.offsets)
result.offsets.push_back(materializeGeometryValue(value, lane, anchor, context, loc));
for (const StaticIntSequence& value : geometry.sizes)
result.sizes.push_back(materializeGeometryValue(value, lane, anchor, context, loc));
for (const StaticIntSequence& value : geometry.strides)
result.strides.push_back(materializeGeometryValue(value, lane, anchor, context, loc));
return result;
}
static RequirementFamily*
findRequirement(const DeferredResultPlan& plan, RequirementCoordinate coordinate, unsigned representativeLane) {
RequirementFamily* match = nullptr;
for (RequirementFamily* requirement : plan.requirements)
if (requirement->coordinate == coordinate && requirement->targetLanes.contains(representativeLane)) {
assert(!match && "requirement coordinate is not unique");
match = requirement;
}
return match;
}
static bool isIdentityGeometry(const DeferredResultPlan::SliceGeometry& geometry, RankedTensorType type) {
if (geometry.offsets.size() != static_cast<size_t>(type.getRank()))
return false;
for (auto [dimension, values] :
llvm::enumerate(llvm::zip_equal(geometry.offsets, geometry.sizes, geometry.strides))) {
const StaticIntSequence& offset = std::get<0>(values);
const StaticIntSequence& size = std::get<1>(values);
const StaticIntSequence& stride = std::get<2>(values);
if (offset.getKind() != StaticIntSequenceKind::Uniform || size.getKind() != StaticIntSequenceKind::Uniform
|| stride.getKind() != StaticIntSequenceKind::Uniform || offset.valueAt(0) != 0
|| size.valueAt(0) != type.getDimSize(dimension) || stride.valueAt(0) != 1)
return false;
}
return true;
}
static FailureOr<Value> applyInnerGeometry(Value fragment,
const DeferredProjectionLeafTemplate& leaf,
const DeferredResultPlan::SliceGeometry& geometry,
Value lane,
DeferredEmissionContext& context) {
if (leaf.form != DeferredLeafForm::GraphBatchProjection)
return fragment;
auto fragmentType = dyn_cast<RankedTensorType>(fragment.getType());
if (!fragmentType || geometry.offsets.size() != static_cast<size_t>(fragmentType.getRank()))
return failure();
if (isIdentityGeometry(geometry, fragmentType))
return fragment;
SmallVector<int64_t> shape(leaf.reconstructedType.getShape().drop_front());
auto resultType = RankedTensorType::get(shape, fragmentType.getElementType());
Value sourceRoot = leaf.sourceRoot;
MixedSliceGeometry mixed =
materializeGeometry(geometry, lane, sourceRoot.getParentBlock()->getParentOp(), context, sourceRoot.getLoc());
return extractMixedSliceOrIdentity(context.rewriter, leaf.sourceRoot.getLoc(), fragment, resultType, mixed);
}
static FailureOr<Value> reconstructLeaf(const DeferredResultPlan& plan,
unsigned leafIndex,
const LaneSet& activeLanes,
Value lane,
DeferredEmissionContext& context) {
DeferredExchangePlan& exchange = *plan.exchange;
const DeferredProjectionLeafTemplate& leaf = exchange.program.leaves[leafIndex];
unsigned representative = activeLanes.intervals().front().begin;
if (Value assembled = context.projectionAssemblies.lookup({&exchange, leafIndex})) {
for (RequirementFamily* requirement : plan.requirements) {
if (requirement->coordinate.leafIndex != leafIndex || !requirement->targetLanes.contains(representative))
continue;
Value local = context.receives.lookup(requirement);
if (!local)
continue;
auto shaped = applyInnerGeometry(local, leaf, plan.innerGeometry[leafIndex], lane, context);
auto source = succeeded(shaped)
? addLeadingUnitTensorDimension(context.rewriter, exchange.deferred.getLoc(), *shaped)
: FailureOr<Value>(failure());
if (failed(source))
return failure();
auto sourceType = cast<RankedTensorType>(source->getType());
MixedSliceGeometry geometry;
geometry.offsets.assign(sourceType.getRank(), context.rewriter.getIndexAttr(0));
geometry.offsets.front() = context.rewriter.getIndexAttr(requirement->coordinate.selectedPosition);
for (int64_t dimension : sourceType.getShape())
geometry.sizes.push_back(context.rewriter.getIndexAttr(dimension));
geometry.strides.assign(sourceType.getRank(), context.rewriter.getIndexAttr(1));
assembled = insertMixedSlice(context.rewriter, exchange.deferred.getLoc(), *source, assembled, geometry);
}
return assembled;
}
unsigned positionCount = 0;
for (RequirementFamily* requirement : plan.requirements)
if (requirement->coordinate.leafIndex == leafIndex && requirement->targetLanes.contains(representative))
positionCount = std::max(positionCount, requirement->coordinate.selectedPosition + 1);
if (positionCount == 0)
return failure();
SmallVector<Value> expanded;
for (unsigned position = 0; position < positionCount; ++position) {
RequirementFamily* requirement = findRequirement(plan, {leafIndex, position}, representative);
if (!requirement)
return failure();
auto fragment = materializeDeferredRequirement(*requirement, activeLanes, lane, context);
if (failed(fragment))
return failure();
auto shaped = applyInnerGeometry(*fragment, leaf, plan.innerGeometry[leafIndex], lane, context);
if (failed(shaped))
return failure();
if (positionCount == 1 && shaped->getType() == leaf.reconstructedType)
return *shaped;
auto value = addLeadingUnitTensorDimension(context.rewriter, exchange.deferred.getLoc(), *shaped);
if (failed(value))
return failure();
expanded.push_back(*value);
}
if (expanded.size() == 1 && expanded.front().getType() == leaf.reconstructedType)
return expanded.front();
constexpr size_t maxConcatInputs = 64;
while (expanded.size() > 1) {
SmallVector<Value> next;
next.reserve((expanded.size() + maxConcatInputs - 1) / maxConcatInputs);
for (size_t index = 0; index < expanded.size(); index += maxConcatInputs) {
ValueRange inputs = ValueRange(expanded).slice(index, std::min(maxConcatInputs, expanded.size() - index));
if (inputs.size() == 1) {
next.push_back(inputs.front());
continue;
}
auto first = cast<RankedTensorType>(inputs.front().getType());
SmallVector<int64_t> shape(first.getShape());
shape.front() = 0;
for (Value input : inputs)
shape.front() += cast<RankedTensorType>(input.getType()).getDimSize(0);
auto resultType = RankedTensorType::get(shape, first.getElementType());
next.push_back(
tensor::ConcatOp::create(context.rewriter, exchange.deferred.getLoc(), resultType, 0, inputs).getResult());
}
expanded = std::move(next);
}
return expanded.front().getType() == leaf.reconstructedType ? FailureOr<Value>(expanded.front())
: FailureOr<Value>(failure());
}
} // namespace
FailureOr<DeferredResultPlan>
buildDeferredResultPlan(DeferredExchangePlan& exchange) {
DeferredResultPlan result;
result.exchange = &exchange;
for (RequirementFamily& requirement : exchange.requirements)
result.requirements.push_back(&requirement);
DeferredLaneValueEvaluator evaluator(
exchange.program, exchange.targetLaneCount);
auto buildGeometry = [&](const DeferredSliceTemplate& source,
DeferredResultPlan::SliceGeometry& target) {
auto append = [&](ArrayRef<OpFoldResult> values,
SmallVectorImpl<StaticIntSequence>& sequences) {
for (OpFoldResult value : values) {
auto sequence = evaluator.evaluate(value);
if (failed(sequence))
return failure();
sequences.push_back(*sequence);
}
return success();
};
return success(succeeded(append(source.offsets, target.offsets))
&& succeeded(append(source.sizes, target.sizes))
&& succeeded(append(source.strides, target.strides)));
};
for (const DeferredProjectionLeafTemplate& leaf : exchange.program.leaves) {
DeferredResultPlan::SliceGeometry geometry;
if (failed(buildGeometry(leaf.innerGeometry, geometry)))
return failure();
result.innerGeometry.push_back(std::move(geometry));
}
if (exchange.program.insertAssembly)
for (const DeferredInsertAssemblyEntryTemplate& entry :
exchange.program.insertAssembly->entries) {
DeferredResultPlan::SliceGeometry geometry;
if (failed(buildGeometry(entry.targetGeometry, geometry)))
return failure();
result.assemblyGeometry.push_back(std::move(geometry));
}
for (Operation* op : exchange.program.residualOps)
op->walk([&](Operation* nested) {
for (Value operand : nested->getOperands()) {
if ((!operand.getType().isIndex()
&& !isa<IntegerType>(operand.getType()))
|| result.residualValues.count(operand))
continue;
if (auto sequence = evaluator.evaluate(operand); succeeded(sequence))
result.residualValues.try_emplace(operand, *sequence);
}
});
return result;
}
FailureOr<Value> materializeDeferredRequirement(RequirementFamily& requirement,
const LaneSet& activeLanes,
Value lane,
DeferredEmissionContext& context) {
if (Value received = context.receives.lookup(&requirement))
return received;
ProducedValue& producer = *requirement.producer;
Value payload = producer.payload;
Location loc = requirement.exchange->deferred.getLoc();
Value position = getSequencePosition(
requirement.targetLanes, lane, requirement.exchange->deferred, context, loc);
if (requirement.producerProjection) {
auto fragmentType = dyn_cast<RankedTensorType>(
requirement.publicationFragmentType);
if (!fragmentType)
return failure();
MixedSliceGeometry geometry = materializeGeometry(
*requirement.producerProjection, position,
requirement.exchange->deferred, context, loc);
return extractMixedSliceOrIdentity(
context.rewriter, loc, payload, fragmentType, geometry);
}
if (payload.getType() == requirement.publicationFragmentType)
return payload;
auto payloadType = dyn_cast<RankedTensorType>(payload.getType());
auto fragmentType = dyn_cast<RankedTensorType>(requirement.publicationFragmentType);
if (!payloadType || !fragmentType || !requirement.producerLocalOffsets
|| payloadType.getRank() != fragmentType.getRank() + 1)
return failure();
Value offset = emitStaticIntLookup(*requirement.producerLocalOffsets,
position,
requirement.exchange->deferred,
context.constants,
context.rewriter,
loc);
using namespace mlir; namespace {
static MixedSliceGeometry leadingSlice(OpBuilder &builder, RankedTensorType type, OpFoldResult position) {
MixedSliceGeometry geometry;
geometry.offsets.assign(payloadType.getRank(), context.rewriter.getIndexAttr(0));
geometry.sizes.push_back(context.rewriter.getIndexAttr(1));
geometry.strides.assign(payloadType.getRank(), context.rewriter.getIndexAttr(1));
geometry.offsets.front() = offset;
for (int64_t dimension : fragmentType.getShape())
geometry.sizes.push_back(context.rewriter.getIndexAttr(dimension));
SmallVector<int64_t> unitShape {1};
llvm::append_range(unitShape, fragmentType.getShape());
auto unitType = RankedTensorType::get(unitShape, fragmentType.getElementType());
Value unit = extractMixedSliceOrIdentity(context.rewriter, loc, payload, unitType, geometry);
return removeLeadingUnitTensorDimension(context.rewriter, loc, unit, fragmentType);
geometry.offsets.assign(type.getRank() + 1, builder.getIndexAttr(0));
geometry.offsets.front() = position;
geometry.sizes.push_back(builder.getIndexAttr(1));
for (int64_t dimension : type.getShape()) geometry.sizes.push_back(builder.getIndexAttr(dimension));
geometry.strides.assign(type.getRank() + 1, builder.getIndexAttr(1));
return geometry;
}
static LogicalResult verifyCollectionCoverage(
DeferredExchangePlan &exchange, FragmentCollectionPlan &collection,
unsigned slotCount) {
LaneSet all = LaneSet::all(exchange.targetLaneCount);
SmallVector<LaneSet, 0> coverage(slotCount);
for (const FragmentCollectionPlan::Requirement &entry :
collection.requirements) {
if (entry.position >= coverage.size()
|| !coverage[entry.position]
.intersect(entry.family->targetLanes).empty())
return exchange.deferred.emitOpError(
"fragment collection has overlapping coverage at slot ")
<< entry.position;
coverage[entry.position] =
coverage[entry.position].unite(entry.family->targetLanes);
}
for (auto [position, lanes] : llvm::enumerate(coverage))
if (!(lanes == all))
return exchange.deferred.emitOpError(
"fragment collection has incomplete coverage at slot ")
<< position;
return success();
}
FailureOr<Value> realizeDeferredResult(const DeferredResultPlan& plan,
const LaneSet& activeLanes,
Value lane,
DeferredEmissionContext& context) {
DeferredExchangePlan& exchange = *plan.exchange;
if (Value assembled = context.assemblies.lookup(&exchange))
return assembled;
static LogicalResult buildLeafCollections(DeferredExchangePlan &exchange,
DeferredResultPlan &plan) {
unsigned specializationCount = exchange.program.specializationCount;
for (auto [leafIndex, leaf] : llvm::enumerate(exchange.program.leaves)) {
SmallVector<RequirementFamily *> requirements;
unsigned positionCount = 0;
for (RequirementFamily &requirement : exchange.requirements)
if (requirement.coordinate.leafIndex == leafIndex) {
requirements.push_back(&requirement);
positionCount = std::max(
positionCount, requirement.coordinate.selectedPosition + 1);
}
if (requirements.empty())
return exchange.deferred.emitOpError(
"cannot form fragment collection for leaf ")
<< leafIndex << ": no requirements";
auto fragmentType = dyn_cast<RankedTensorType>(
requirements.front()->publicationFragmentType);
if (!fragmentType || llvm::any_of(requirements,
[&](RequirementFamily *item) {
return item->publicationFragmentType != fragmentType;
}))
return exchange.deferred.emitOpError(
"cannot form fragment collection for leaf ")
<< leafIndex
<< ": publication fragment types differ or are unranked";
RankedTensorType normalized = fragmentType;
if (leaf.form == DeferredLeafForm::GraphBatchProjection)
normalized = leaf.leadingRankReduced
? leaf.reconstructedType
: RankedTensorType::get(
leaf.reconstructedType.getShape().drop_front(),
leaf.reconstructedType.getElementType());
bool direct = positionCount == 1 && normalized == leaf.reconstructedType;
bool leading = leaf.reconstructedType.getRank() == normalized.getRank() + 1
&& leaf.reconstructedType.getDimSize(0) == positionCount
&& leaf.reconstructedType.getShape().drop_front()
== normalized.getShape();
if (!direct && !leading)
return exchange.deferred.emitOpError(
"cannot form fragment collection for leaf ")
<< leafIndex << ": " << positionCount << " fragments of "
<< normalized << " do not reconstruct " << leaf.reconstructedType;
FragmentCollectionKind kind = specializationCount == 1
? FragmentCollectionKind::Leaf
: FragmentCollectionKind::GroupedLeaf;
SmallVector<int64_t> shape;
if (specializationCount > 1)
shape.push_back(specializationCount);
llvm::append_range(shape, leaf.reconstructedType.getShape());
FragmentCollectionPlan collection;
collection.key = {&exchange, kind, static_cast<unsigned>(leafIndex)};
collection.collectionType = RankedTensorType::get(
shape, leaf.reconstructedType.getElementType());
collection.positionCount = positionCount;
for (RequirementFamily *requirement : requirements)
collection.requirements.push_back({
requirement,
requirement->coordinate.specializationIndex * positionCount
+ requirement->coordinate.selectedPosition});
if (failed(verifyCollectionCoverage(
exchange, collection, specializationCount * positionCount)))
return failure();
plan.collections.push_back(std::move(collection));
}
return success();
}
static bool supportsAssemblyTransform(
Type publicationType, const DeferredInsertAssemblyEntryTemplate &entry) {
auto publication = dyn_cast<RankedTensorType>(publicationType);
RankedTensorType source = entry.sourceType;
if (!publication || !source)
return false;
switch (entry.sourceTransform) {
case DeferredAssemblySourceTransform::Identity:
return publication == source;
case DeferredAssemblySourceTransform::AddLeadingUnitDimension:
return source.getRank() == publication.getRank() + 1
&& source.getDimSize(0) == 1
&& source.getShape().drop_front() == publication.getShape();
case DeferredAssemblySourceTransform::RemoveLeadingUnitDimension:
return publication.getRank() == source.getRank() + 1
&& publication.getDimSize(0) == 1
&& publication.getShape().drop_front() == source.getShape();
}
llvm_unreachable("unknown deferred assembly source transform");
}
static LogicalResult buildInsertAssemblyCollection(
DeferredExchangePlan &exchange, DeferredResultPlan &plan) {
if (exchange.program.specializationCount != 1)
return exchange.deferred.emitOpError(
"grouped specialization insert assembly is unsupported");
const DeferredInsertAssemblyTemplate &assembly =
*exchange.program.insertAssembly;
FragmentCollectionPlan collection;
collection.key = {&exchange, FragmentCollectionKind::InsertAssembly, 0};
collection.collectionType = assembly.resultType;
collection.positionCount = assembly.entries.size();
llvm::DenseSet<RequirementFamily *> collected;
for (auto [position, entry] : llvm::enumerate(assembly.entries)) {
bool found = false;
for (RequirementFamily &requirement : exchange.requirements) {
if (!(requirement.coordinate == entry.coordinate))
continue;
if (!supportsAssemblyTransform(
requirement.publicationFragmentType, entry))
return exchange.deferred.emitOpError(
"insert assembly source transform does not match publication type at entry ")
<< position;
if (!collected.insert(&requirement).second)
return exchange.deferred.emitOpError(
"insert assembly requirement is owned by multiple entries at entry ")
<< position;
collection.requirements.push_back(
{&requirement, static_cast<unsigned>(position)});
found = true;
}
if (!found)
return exchange.deferred.emitOpError(
"insert assembly entry has no requirement at entry ")
<< position;
}
if (collected.size() != exchange.requirements.size())
return exchange.deferred.emitOpError(
"insert assembly does not own every deferred requirement");
if (failed(verifyCollectionCoverage(
exchange, collection, assembly.entries.size())))
return failure();
plan.collections.push_back(std::move(collection));
return success();
}
using TemplateGeometryMember = SmallVector<OpFoldResult> DeferredSliceTemplate::*;
static constexpr std::array<TemplateGeometryMember, 3> templateGeometryMembers{
&DeferredSliceTemplate::offsets, &DeferredSliceTemplate::sizes,
&DeferredSliceTemplate::strides};
template <typename GetValue>
static FailureOr<StaticIntGrid> buildGrid(
const DeferredProgramTemplate &program, unsigned laneCount,
unsigned rowCount, bool specializeRows, GetValue getValue) {
SmallVector<StaticIntSequence> rows;
for (unsigned row = 0; row < rowCount; ++row) {
DeferredLaneValueEvaluator evaluator(
program, laneCount, specializeRows ? row : 0);
auto sequence = evaluator.evaluate(getValue(row));
if (failed(sequence)) return failure();
rows.push_back(std::move(*sequence));
}
return StaticIntGrid::fromRows(rows);
}
template <typename GetGeometry>
static FailureOr<DeferredGridSliceGeometry> buildGeometryGrids(
const DeferredProgramTemplate &program, unsigned laneCount,
unsigned rowCount, bool specializeRows, GetGeometry getGeometry) {
DeferredGridSliceGeometry result;
const DeferredSliceTemplate &first = getGeometry(0);
for (auto [group, sourceMember] : llvm::enumerate(templateGeometryMembers)) {
TemplateGeometryMember member = sourceMember;
for (unsigned dimension = 0;
dimension < (first.*member).size(); ++dimension) {
auto grid = buildGrid(program, laneCount, rowCount, specializeRows,
[&](unsigned row) { return (getGeometry(row).*member)[dimension]; });
if (failed(grid)) return failure();
result[group].push_back(std::move(*grid));
}
}
return result;
}
static Value cloneResidual(DeferredExchangePlan &exchange, IRMapping &mapping, DeferredEmissionContext &context) {
for (Operation *op : exchange.program.residualOps) {
Operation *copy = context.rewriter.clone(*op, mapping);
for (auto [oldValue, newValue] : llvm::zip(op->getResults(), copy->getResults())) mapping.map(oldValue, newValue);
}
return mapping.lookupOrDefault(exchange.program.yieldedValue);
}
static FailureOr<Value> realizeOne(const DeferredResultPlan &plan, Value lane, DeferredEmissionContext &context) {
DeferredExchangePlan &exchange = *plan.exchange;
if (exchange.program.insertAssembly) {
Value collection = context.fragmentCollections.lookup(
{&exchange, FragmentCollectionKind::InsertAssembly, 0});
return collection && collection.getType() == exchange.program.insertAssembly->resultType
? FailureOr<Value>(collection) : FailureOr<Value>(failure());
}
IRMapping mapping;
if (exchange.program.scheduledLane)
mapping.map(exchange.program.scheduledLane, lane);
for (auto [value, sequence] : plan.residualValues) {
if (mapping.contains(value))
continue;
Value selected = emitStaticIntLookup(sequence,
lane ? lane : context.constants.getIndex(0),
exchange.deferred,
context.constants,
context.rewriter,
exchange.deferred.getLoc());
if (!value.getType().isIndex())
selected = arith::IndexCastOp::create(context.rewriter, exchange.deferred.getLoc(), value.getType(), selected);
if (exchange.program.scheduledLane) mapping.map(exchange.program.scheduledLane, lane);
for (auto &[value, grid] : plan.residualValues) {
if (mapping.contains(value)) continue;
Value selected = grid.emitLookup(
context.constants.getIndex(0),
lane ? lane : context.constants.getIndex(0), exchange.deferred,
context.constants, context.rewriter, exchange.deferred.getLoc());
if (!value.getType().isIndex()) selected = arith::IndexCastOp::create(context.rewriter, exchange.deferred.getLoc(), value.getType(), selected);
mapping.map(value, selected);
}
for (unsigned leaf = 0; leaf < exchange.program.leaves.size(); ++leaf) {
auto value = reconstructLeaf(plan, leaf, activeLanes, lane, context);
if (failed(value))
for (unsigned index = 0; index < exchange.program.leaves.size(); ++index) {
Value value = context.fragmentCollections.lookup(
{&exchange, FragmentCollectionKind::Leaf, index});
if (!value || value.getType() != exchange.program.leaves[index].reconstructedType) {
exchange.deferred.emitOpError("failed to reconstruct deferred result leaf ") << index;
return failure();
mapping.map(exchange.program.leaves[leaf].replacementRoot, *value);
}
mapping.map(exchange.program.leaves[index].replacementRoot, value);
}
for (Operation* op : exchange.program.residualOps) {
Operation* copy = context.rewriter.clone(*op, mapping);
for (auto [oldValue, newValue] : llvm::zip(op->getResults(), copy->getResults()))
mapping.map(oldValue, newValue);
}
Value result = mapping.lookupOrDefault(exchange.program.yieldedValue);
return result && result.getType() == exchange.deferred.getOutput().getType() ? FailureOr<Value>(result)
: FailureOr<Value>(failure());
Value result = cloneResidual(exchange, mapping, context);
Type expected = exchange.program.specializationCount > 1 ? Type(exchange.program.specializationFragmentType) : exchange.deferred.getOutput().getType();
return result && result.getType() == expected ? FailureOr<Value>(result) : FailureOr<Value>(failure());
}
} // namespace
FailureOr<DeferredResultPlan> buildDeferredResultPlan(DeferredExchangePlan &exchange) {
DeferredResultPlan plan;
plan.exchange = &exchange;
if (failed(exchange.program.insertAssembly
? buildInsertAssemblyCollection(exchange, plan)
: buildLeafCollections(exchange, plan))) return failure();
unsigned specializations = exchange.program.specializationCount;
for (const auto &leaf : exchange.program.leaves) {
auto geometry = buildGeometryGrids(exchange.program,
exchange.targetLaneCount, specializations, true,
[&](unsigned) -> const DeferredSliceTemplate & {
return leaf.innerGeometry;
});
if (failed(geometry)) return failure();
plan.innerGeometry.push_back(std::move(*geometry));
}
if (exchange.program.insertAssembly) {
const auto &entries = exchange.program.insertAssembly->entries;
auto geometry = buildGeometryGrids(exchange.program,
exchange.targetLaneCount, entries.size(), false,
[&](unsigned row) -> const DeferredSliceTemplate & {
return entries[row].targetGeometry;
});
if (failed(geometry)) return failure();
plan.assemblyGeometry = std::move(*geometry);
}
llvm::DenseSet<Value> residualValues;
for (Operation *op : exchange.program.residualOps)
op->walk([&](Operation *nested) {
for (Value operand : nested->getOperands())
if (operand.getType().isIndex() || isa<IntegerType>(operand.getType()))
residualValues.insert(operand);
});
for (Value value : residualValues) {
auto grid = buildGrid(exchange.program, exchange.targetLaneCount,
specializations, true,
[&](unsigned) { return OpFoldResult(value); });
if (succeeded(grid))
plan.residualValues.try_emplace(value, std::move(*grid));
}
return plan;
}
FailureOr<Value> realizeDeferredResult(const DeferredResultPlan &plan, Value lane, DeferredEmissionContext &context) {
DeferredExchangePlan &exchange = *plan.exchange;
if (exchange.program.specializationCount == 1) return realizeOne(plan, lane, context);
auto outputType = dyn_cast<RankedTensorType>(exchange.deferred.getOutput().getType());
RankedTensorType fragmentType = exchange.program.specializationFragmentType;
if (!outputType || !fragmentType || outputType.getRank() != fragmentType.getRank() + 1 || outputType.getDimSize(0) != exchange.program.specializationCount || outputType.getShape().drop_front() != fragmentType.getShape()) return failure();
if (exchange.program.insertAssembly) return failure();
SmallVector<Value> leafStacks;
for (auto [index, leaf] : llvm::enumerate(exchange.program.leaves)) {
FragmentCollectionKey key{&exchange, FragmentCollectionKind::GroupedLeaf, static_cast<unsigned>(index)};
Value collection = context.fragmentCollections.lookup(key);
SmallVector<int64_t> shape{exchange.program.specializationCount};
llvm::append_range(shape, leaf.reconstructedType.getShape());
if (!collection || collection.getType() != RankedTensorType::get(shape, leaf.reconstructedType.getElementType())) return failure();
leafStacks.push_back(collection);
}
Location loc = exchange.deferred.getLoc();
Value initial = tensor::EmptyOp::create(context.rewriter, loc, outputType.getShape(), outputType.getElementType());
auto loop = buildNormalizedScfFor(context.rewriter, loc, context.constants.getIndex(0), context.constants.getIndex(exchange.program.specializationCount), context.constants.getIndex(1), ValueRange{initial},
[&](OpBuilder &, Location, Value specialization, ValueRange iterArgs, SmallVectorImpl<Value> &yielded) -> LogicalResult {
IRMapping mapping;
if (exchange.program.scheduledLane) mapping.map(exchange.program.scheduledLane, lane);
if (exchange.program.specializationArgument) mapping.map(exchange.program.specializationArgument, specialization);
Value runtimeLane = lane ? lane : context.constants.getIndex(0);
for (auto &[value, grid] : plan.residualValues) {
Value selected = grid.emitLookup(specialization, runtimeLane, exchange.deferred, context.constants, context.rewriter, loc);
if (!value.getType().isIndex()) selected = arith::IndexCastOp::create(context.rewriter, loc, value.getType(), selected);
mapping.map(value, selected);
}
for (auto [index, leaf] : llvm::enumerate(exchange.program.leaves)) { Value selected = extractMixedSliceOrIdentity(context.rewriter, loc, leafStacks[index], leaf.reconstructedType, leadingSlice(context.rewriter, leaf.reconstructedType, specialization)); if (!selected) return failure(); mapping.map(leaf.replacementRoot, selected); }
Value fragment = cloneResidual(exchange, mapping, context);
if (!fragment || fragment.getType() != fragmentType) return failure();
yielded.push_back(insertMixedSlice(context.rewriter, loc, fragment, iterArgs.front(), leadingSlice(context.rewriter, fragmentType, specialization)));
return success();
});
return failed(loop) || loop->results.size() != 1 ? FailureOr<Value>(failure()) : FailureOr<Value>(loop->results.front());
}
} // namespace onnx_mlir::spatial
@@ -1,31 +1,57 @@
#pragma once
#include "DeferredCommunicationModel.hpp"
#include "src/Accelerators/PIM/Common/IR/StaticIntGrid.hpp"
#include <array>
namespace onnx_mlir::spatial {
struct DeferredEmissionContext;
enum class FragmentCollectionKind {
Leaf,
GroupedLeaf,
InsertAssembly
};
struct FragmentCollectionKey {
DeferredExchangePlan *exchange = nullptr;
FragmentCollectionKind kind = FragmentCollectionKind::Leaf;
unsigned leafIndex = 0;
bool operator==(const FragmentCollectionKey &other) const {
return exchange == other.exchange && kind == other.kind
&& leafIndex == other.leafIndex;
}
};
struct FragmentCollectionPlan {
FragmentCollectionKey key;
mlir::RankedTensorType collectionType;
unsigned positionCount = 0;
struct Requirement {
RequirementFamily *family = nullptr;
unsigned position = 0;
};
llvm::SmallVector<Requirement> requirements;
};
using DeferredGridSliceGeometry =
std::array<llvm::SmallVector<StaticIntGrid, 0>, 3>;
struct DeferredResultPlan {
DeferredExchangePlan* exchange = nullptr;
llvm::SmallVector<RequirementFamily*> requirements;
using SliceGeometry = DeferredStaticSliceGeometry;
llvm::SmallVector<SliceGeometry, 0> innerGeometry;
llvm::SmallVector<SliceGeometry, 0> assemblyGeometry;
llvm::DenseMap<mlir::Value, StaticIntSequence> residualValues;
llvm::SmallVector<FragmentCollectionPlan, 0> collections;
llvm::SmallVector<DeferredGridSliceGeometry, 0> innerGeometry;
DeferredGridSliceGeometry assemblyGeometry;
llvm::DenseMap<mlir::Value, StaticIntGrid> residualValues;
};
mlir::FailureOr<DeferredResultPlan>
buildDeferredResultPlan(DeferredExchangePlan& exchange);
mlir::FailureOr<mlir::Value> realizeDeferredResult(const DeferredResultPlan& plan,
const LaneSet& activeLanes,
mlir::Value lane,
DeferredEmissionContext& context);
mlir::FailureOr<mlir::Value> materializeDeferredRequirement(RequirementFamily& requirement,
const LaneSet& activeLanes,
mlir::Value lane,
DeferredEmissionContext& context);
} // namespace onnx_mlir::spatial
@@ -10,190 +10,44 @@ namespace onnx_mlir::spatial {
using namespace mlir;
namespace {
static FailureOr<SmallVector<int64_t>> getI64Array(Operation* op, StringRef name) {
auto attr = op->getAttrOfType<DenseI64ArrayAttr>(name);
if (!attr)
return op->emitOpError() << "phase 2 requires '" << name << "' metadata";
return SmallVector<int64_t>(attr.asArrayRef());
static FailureOr<unsigned> getStepIndex(
ScheduledInfo& info, SpatDeferredCommunicationOp deferred) {
Operation *position = deferred;
Block *block = info.blocks.front();
while (position && position->getBlock() != block)
position = position->getParentOp();
if (!position)
return failure();
for (unsigned step : llvm::reverse(
llvm::seq<unsigned>(0, info.stepAnchors.size())))
if (info.stepAnchors[step] == position
|| info.stepAnchors[step]->isBeforeInBlock(position))
return step;
return failure();
}
static FailureOr<SmallVector<int64_t>> getLaneTable(Operation* op, StringRef name, size_t expected) {
if (auto array = op->getAttrOfType<DenseI64ArrayAttr>(name)) {
if (array.size() != static_cast<int64_t>(expected))
return op->emitOpError() << "phase 2 metadata '" << name << "' has the wrong size";
return SmallVector<int64_t>(array.asArrayRef());
}
auto elements = op->getAttrOfType<DenseIntElementsAttr>(name);
if (!elements || elements.getNumElements() != static_cast<int64_t>(expected))
return op->emitOpError() << "phase 2 requires a correctly-sized '" << name << "' lane table";
SmallVector<int64_t> values;
for (APInt value : elements.getValues<APInt>())
values.push_back(value.getSExtValue());
return values;
}
static Block* getScheduledBlock(SpatDeferredCommunicationOp deferred, Operation* scheduled) {
Block* block = deferred->getBlock();
while (block && block->getParentOp() != scheduled) {
Operation* parent = block->getParentOp();
block = parent ? parent->getBlock() : nullptr;
}
return block;
}
static FailureOr<unsigned> getStepIndex(ScheduledInfo& info, Block* block) {
auto it = llvm::find(info.blocks, block);
return it == info.blocks.end() ? FailureOr<unsigned>(failure())
: FailureOr<unsigned>(std::distance(info.blocks.begin(), it));
}
static FailureOr<Value>
getScalarStepResult(SpatScheduledCompute scheduled, Block& block, unsigned resultIndex, unsigned resultCount) {
auto yield = dyn_cast<SpatBlockYieldOp>(block.getTerminator());
if (!yield || resultIndex >= resultCount || yield.getOutputs().size() < resultCount)
return scheduled.emitOpError("phase 2 cannot recover a scalar scheduled step result"), failure();
return yield.getOutputs()[yield.getOutputs().size() - resultCount + resultIndex];
}
struct BatchPublication {
Value payload;
tensor::ParallelInsertSliceOp insertion;
};
struct ProducedValueGeometry {
int64_t laneStart = 0;
int64_t laneCount = 1;
int64_t publishedSlotStart = 0;
int64_t publishedSlotCount = 1;
Value payload;
Value published;
};
static FailureOr<BatchPublication>
getBatchStepResult(SpatScheduledComputeBatch scheduled, Block& block, unsigned globalResultIndex) {
auto parallel = dyn_cast<SpatInParallelOp>(block.getTerminator());
if (!parallel)
return scheduled.emitOpError("phase 2 cannot recover a batched scheduled step result"), failure();
unsigned resultBase = 1 + scheduled.getWeights().size() + scheduled.getInputs().size();
for (Operation& op : parallel.getRegion().front()) {
auto insert = dyn_cast<tensor::ParallelInsertSliceOp>(op);
auto destination = insert ? dyn_cast<BlockArgument>(insert.getDest()) : BlockArgument();
if (destination && destination.getOwner() == &block && destination.getArgNumber() == resultBase + globalResultIndex)
return BatchPublication {insert.getSource(), insert};
}
return scheduled.emitOpError("phase 2 cannot find the batched scheduled result insertion"), failure();
}
static FailureOr<ProducedValueGeometry> verifyScheduledPublicationGeometry(
ScheduledInfo& info, unsigned step, unsigned globalResult, unsigned lane,
int64_t laneStart, int64_t laneCount, Value payload,
tensor::ParallelInsertSliceOp insertion = {}) {
ProducedValueGeometry result;
result.laneStart = laneStart;
result.laneCount = info.isBatch() ? laneCount : std::max<int64_t>(laneCount, 1);
result.payload = payload;
result.published = info.op->getResult(globalResult);
auto payloadType = dyn_cast<RankedTensorType>(payload.getType());
auto publishedType = dyn_cast<RankedTensorType>(result.published.getType());
if (laneStart < 0 || result.laneCount <= 0)
return info.op->emitOpError("phase 2 scheduled publication has an invalid source-lane range"), failure();
if (!info.isBatch()) {
result.publishedSlotCount = result.laneCount;
if (payload.getType() != result.published.getType()
|| (result.laneCount > 1
&& (!publishedType || !publishedType.hasStaticShape()
|| publishedType.getRank() == 0
|| publishedType.getDimSize(0) != result.laneCount)))
return info.op->emitOpError("phase 2 scalar scheduled publication is incompatible with its result"), failure();
return result;
}
if (!insertion || !payloadType || !payloadType.hasStaticShape()
|| !publishedType || !publishedType.hasStaticShape()
|| insertion.getDest() != info.blocks[step]->getArgument(
1 + info.op->getNumOperands() + globalResult))
return info.op->emitOpError("phase 2 batched publication is malformed"), failure();
unsigned rank = publishedType.getRank();
if (rank == 0 || payloadType.getRank() != rank
|| insertion.getMixedOffsets().size() != rank
|| insertion.getMixedSizes().size() != rank
|| insertion.getMixedStrides().size() != rank)
return info.op->emitOpError("phase 2 batched publication geometry rank is invalid"), failure();
StaticIndexEnvironment environment;
environment.bindings[info.blocks[step]->getArgument(0)] = lane;
SmallVector<int64_t> offsets, sizes, strides;
for (OpFoldResult value : insertion.getMixedOffsets()) {
auto evaluated = evaluateDeferredIndex(value, environment);
if (failed(evaluated))
return info.op->emitOpError("phase 2 batched publication offset is not static"), failure();
offsets.push_back(*evaluated);
}
for (OpFoldResult value : insertion.getMixedSizes()) {
auto evaluated = evaluateDeferredIndex(value, environment);
if (failed(evaluated))
return info.op->emitOpError("phase 2 batched publication size is not static"), failure();
sizes.push_back(*evaluated);
}
for (OpFoldResult value : insertion.getMixedStrides()) {
auto evaluated = evaluateDeferredIndex(value, environment);
if (failed(evaluated))
return info.op->emitOpError("phase 2 batched publication stride is not static"), failure();
strides.push_back(*evaluated);
}
if (offsets.front() < 0 || sizes.front() <= 0 || sizes.front() != result.laneCount
|| strides.front() != 1
|| offsets.front() + sizes.front() > publishedType.getDimSize(0))
return info.op->emitOpError("phase 2 batched publication leading geometry is invalid"), failure();
for (unsigned dimension = 1; dimension < rank; ++dimension)
if (offsets[dimension] != 0 || strides[dimension] != 1)
return info.op->emitOpError("phase 2 batched publication inner geometry is invalid"), failure();
for (unsigned dimension = 0; dimension < rank; ++dimension)
if (sizes[dimension] != payloadType.getDimSize(dimension))
return info.op->emitOpError("phase 2 batched publication payload shape is incompatible"), failure();
result.publishedSlotStart = offsets.front();
result.publishedSlotCount = sizes.front();
return result;
}
static LogicalResult collectScheduledOperations(func::FuncOp funcOp, DeferredTransferPlan& plan) {
static LogicalResult collectScheduledOperations(
const ScheduledComputeMaterializationResult &materialization,
DeferredTransferPlan &plan) {
unsigned nextStream = 0;
for (Operation& op : funcOp.getOps()) {
if (!isa<SpatScheduledCompute, SpatScheduledComputeBatch>(op))
continue;
for (const ScheduledMaterializationRecord &record :
materialization.materializedSchedules) {
Operation &op = *record.scheduledOp;
ScheduledInfo info;
info.op = &op;
Region& body = isa<SpatScheduledCompute>(op) ? cast<SpatScheduledCompute>(op).getBody()
: cast<SpatScheduledComputeBatch>(op).getBody();
for (Block& block : body) {
info.blocks.push_back(&block);
info.stepAnchors.push_back(&block.front());
}
auto sourceIds = getI64Array(&op, "scheduled.step_source_ids");
auto offsets = getI64Array(&op, "scheduled.step_result_offsets");
auto counts = getI64Array(&op, "scheduled.step_result_counts");
if (failed(sourceIds) || failed(offsets) || failed(counts))
return failure();
info.stepSourceIds = std::move(*sourceIds);
info.resultOffsets = std::move(*offsets);
info.resultCounts = std::move(*counts);
if (info.blocks.size() != info.stepSourceIds.size() || info.blocks.size() != info.resultOffsets.size()
|| info.blocks.size() != info.resultCounts.size())
return op.emitOpError("phase 2 scheduled metadata does not match its block count");
if (auto scalar = dyn_cast<SpatScheduledCompute>(op)) {
auto core = scalar->getAttrOfType<IntegerAttr>(kCoreIdAttrName);
if (!core)
return scalar.emitOpError("phase 2 requires scalar coreId metadata");
info.cores.push_back(core.getInt());
}
else {
auto cores = op.getAttrOfType<DenseI32ArrayAttr>(kCoreIdsAttrName);
if (!cores)
return op.emitOpError("phase 2 requires batch coreIds metadata");
for (int32_t core : cores.asArrayRef())
info.cores.push_back(core);
}
if (!body.hasOneBlock())
return op.emitOpError("phase 2 requires canonical one-block scheduled IR");
info.blocks.push_back(&body.front());
info.stepCount = record.runs.empty() ? record.stepPlans.size()
: record.runs.size();
info.stepAnchors = record.stepAnchors;
if (llvm::any_of(info.stepAnchors,
[](Operation *anchor) { return !anchor; }))
return op.emitOpError("phase 2 scheduled step anchor is missing");
for (size_t core : record.cpus)
info.cores.push_back(core);
for (size_t lane = 0; lane < info.cores.size(); ++lane)
info.streamIds.push_back(nextStream++);
plan.scheduled.push_back(std::move(info));
@@ -201,67 +55,47 @@ static LogicalResult collectScheduledOperations(func::FuncOp funcOp, DeferredTra
plan.stepCounts.resize(nextStream);
for (ScheduledInfo& info : plan.scheduled)
for (unsigned stream : info.streamIds)
plan.stepCounts[stream] = info.blocks.size();
plan.stepCounts[stream] = info.stepCount;
return success();
}
static LogicalResult collectProducedValues(DeferredTransferPlan& plan) {
for (ScheduledInfo& info : plan.scheduled) {
SmallVector<int64_t> laneStarts, laneCounts;
size_t tableSize = info.blocks.size() * info.cores.size();
auto starts = info.isBatch() ? getLaneTable(info.op, "scheduled.source_lane_starts", tableSize)
: getI64Array(info.op, "scheduled.source_lane_starts");
auto counts = info.isBatch() ? getLaneTable(info.op, "scheduled.source_lane_counts", tableSize)
: getI64Array(info.op, "scheduled.source_lane_counts");
if (failed(starts) || failed(counts))
return failure();
laneStarts = std::move(*starts);
laneCounts = std::move(*counts);
for (unsigned step = 0; step < info.blocks.size(); ++step) {
if (info.resultOffsets[step] < 0 || info.resultCounts[step] < 0)
return info.op->emitOpError("phase 2 scheduled result metadata must be non-negative");
for (unsigned result = 0; result < static_cast<unsigned>(info.resultCounts[step]); ++result) {
unsigned globalResult = info.resultOffsets[step] + result;
if (!info.isBatch()) {
auto payload = getScalarStepResult(
cast<SpatScheduledCompute>(info.op), *info.blocks[step], result, info.resultCounts[step]);
if (failed(payload))
return failure();
auto geometry = verifyScheduledPublicationGeometry(
info, step, globalResult, 0, laneStarts[step], laneCounts[step], *payload);
if (failed(geometry))
return failure();
auto produced = std::make_unique<ProducedValue>(ProducedValue {
&info, step, result, info.stepSourceIds[step], info.cores.front(),
geometry->laneStart, geometry->laneCount, 0,
geometry->publishedSlotStart, geometry->publishedSlotCount,
geometry->payload, geometry->published});
info.produced.push_back(produced.get());
plan.producedByGraph[produced->graphId].push_back(produced.get());
plan.producedStorage.push_back(std::move(produced));
continue;
}
auto publication =
getBatchStepResult(cast<SpatScheduledComputeBatch>(info.op), *info.blocks[step], globalResult);
if (failed(publication))
return failure();
for (unsigned lane = 0; lane < info.cores.size(); ++lane) {
size_t laneIndex = step * info.cores.size() + lane;
auto geometry = verifyScheduledPublicationGeometry(
info, step, globalResult, lane, laneStarts[laneIndex],
laneCounts[laneIndex], publication->payload, publication->insertion);
if (failed(geometry))
return failure();
auto produced = std::make_unique<ProducedValue>(ProducedValue {
&info, step, result, info.stepSourceIds[step], info.cores[lane],
geometry->laneStart, geometry->laneCount, lane,
geometry->publishedSlotStart, geometry->publishedSlotCount,
geometry->payload, geometry->published});
info.produced.push_back(produced.get());
plan.producedByGraph[produced->graphId].push_back(produced.get());
plan.producedStorage.push_back(std::move(produced));
}
}
static LogicalResult collectProducedValues(
const ScheduledComputeMaterializationResult &materialization,
DeferredTransferPlan &plan) {
if (plan.scheduled.size() != materialization.materializedSchedules.size())
return failure();
for (auto [recordIndex, record] :
llvm::enumerate(materialization.materializedSchedules)) {
ScheduledInfo &info = plan.scheduled[recordIndex];
DenseMap<ComputeInstance, std::pair<unsigned, unsigned>> coordinates;
if (record.runs.empty()) {
for (auto [step, stepPlan] : llvm::enumerate(record.stepPlans))
for (auto [lane, instance] :
llvm::enumerate(stepPlan.stepTuple.instances))
coordinates[instance] = {step, lane};
} else {
for (auto [step, run] : llvm::enumerate(record.runs))
for (const ComputeInstance &instance : run.instances)
coordinates[instance] = {step, 0};
}
for (const MaterializedStepValue &value : record.stepValues) {
auto coordinate = coordinates.find(value.instance);
if (coordinate == coordinates.end()
|| coordinate->second.second >= info.cores.size()
|| value.graphId < 0 || value.laneStart < 0
|| value.laneCount <= 0 || !value.payload)
return info.op->emitOpError(
"phase 2 received an invalid scheduled materialization record");
unsigned step = coordinate->second.first;
unsigned lane = coordinate->second.second;
auto produced = std::make_unique<ProducedValue>(ProducedValue {
&info, step, value.resultIndex, value.graphId, info.cores[lane],
value.laneStart, value.laneCount, lane, value.payloadLaneStart,
value.payloadLaneCount, value.payload, value.published});
info.produced.push_back(produced.get());
plan.producedByGraph[produced->graphId].push_back(produced.get());
plan.producedStorage.push_back(std::move(produced));
}
}
return success();
@@ -293,206 +127,176 @@ static FailureOr<ProducedValue*> findProducer(DeferredTransferPlan& plan,
return match;
}
struct RequirementPoint {
struct SliceGeometry {
SmallVector<int64_t> offsets;
SmallVector<int64_t> sizes;
SmallVector<int64_t> strides;
};
ProducedValue* producer = nullptr;
Type fragmentType;
std::optional<int64_t> graphLane;
std::optional<int64_t> localOffset;
std::optional<SliceGeometry> producerProjection;
bool sameFamily(const RequirementPoint& other) const {
return producer == other.producer && fragmentType == other.fragmentType;
}
};
static FailureOr<SmallVector<int64_t>> evaluateGeometryValues(
ArrayRef<OpFoldResult> values,
DeferredLaneValueEvaluator& evaluator,
unsigned lane) {
SmallVector<int64_t> result;
result.reserve(values.size());
for (OpFoldResult value : values) {
auto sequence = evaluator.evaluate(value);
if (failed(sequence))
return failure();
result.push_back(sequence->valueAt(lane));
}
return result;
}
static FailureOr<std::optional<RequirementPoint>>
resolveRequirementPoint(DeferredTransferPlan& plan,
DeferredExchangePlan& exchange,
const DeferredProjectionLeafTemplate& leaf,
DeferredLaneValueEvaluator& evaluator,
unsigned lane,
unsigned position,
GraphBatchPublicationCache& publicationCache) {
auto sourceIndices = evaluator.resolveSourceOperandIndices(leaf.sourceRoot);
if (failed(sourceIndices))
return failure();
unsigned sourceIndex = sourceIndices->valueAt(lane);
auto source = dyn_cast<OpResult>(exchange.deferred.getSources()[sourceIndex]);
if (!source)
return exchange.deferred.emitOpError("phase 2 requires graph-result deferred sources"), failure();
auto graphId = source.getOwner()->getAttrOfType<IntegerAttr>("scheduled.graph_id");
if (!graphId)
return exchange.deferred.emitOpError("phase 2 cannot identify graph producer"), failure();
RequirementPoint point;
if (auto batch = dyn_cast<SpatGraphComputeBatch>(source.getOwner())) {
auto publication = getGraphBatchPublicationMap(batch, source.getResultNumber(), publicationCache);
if (failed(publication))
return failure();
int64_t physicalSlot = position;
if (leaf.form == DeferredLeafForm::GraphBatchProjection) {
auto offset = evaluator.evaluate(leaf.leadingGeometry.offsets.front());
auto size = evaluator.evaluate(leaf.leadingGeometry.sizes.front());
auto stride = evaluator.evaluate(leaf.leadingGeometry.strides.front());
if (failed(offset) || failed(size) || failed(stride))
return failure();
if (position >= static_cast<unsigned>(size->valueAt(lane)))
return std::optional<RequirementPoint>();
physicalSlot = offset->valueAt(lane) + static_cast<int64_t>(position) * stride->valueAt(lane);
}
else if (position >= (*publication)->physicalSlotToGraphLane.size()) {
return std::optional<RequirementPoint>();
}
if (physicalSlot < 0 || physicalSlot >= static_cast<int64_t>((*publication)->physicalSlotToGraphLane.size()))
return exchange.deferred.emitOpError("projection physical slot is outside publication map"), failure();
point.graphLane = (*publication)->physicalSlotToGraphLane[physicalSlot];
point.fragmentType = (*publication)->publicationFragmentType;
auto producer = findProducer(plan, exchange.deferred, graphId.getInt(), source.getResultNumber(), point.graphLane);
if (failed(producer))
return failure();
point.producer = *producer;
point.localOffset = *point.graphLane - point.producer->laneStart;
}
else {
if (position != 0 || !isa<SpatGraphCompute>(source.getOwner()))
return std::optional<RequirementPoint>();
point.fragmentType = leaf.form == DeferredLeafForm::ScalarProjection
? Type(leaf.reconstructedType)
: source.getType();
auto producer = findProducer(plan, exchange.deferred, graphId.getInt(), source.getResultNumber(), std::nullopt);
if (failed(producer))
return failure();
point.producer = *producer;
if (leaf.form == DeferredLeafForm::ScalarProjection) {
RequirementPoint::SliceGeometry geometry;
auto offsets = evaluateGeometryValues(leaf.leadingGeometry.offsets, evaluator, lane);
auto sizes = evaluateGeometryValues(leaf.leadingGeometry.sizes, evaluator, lane);
auto strides = evaluateGeometryValues(leaf.leadingGeometry.strides, evaluator, lane);
if (failed(offsets) || failed(sizes) || failed(strides))
return failure();
geometry.offsets = std::move(*offsets);
geometry.sizes = std::move(*sizes);
geometry.strides = std::move(*strides);
point.producerProjection = std::move(geometry);
}
}
return std::optional<RequirementPoint>(point);
}
static void appendRequirementFamily(DeferredExchangePlan& exchange,
RequirementCoordinate coordinate,
unsigned begin,
ArrayRef<RequirementPoint> points) {
RequirementFamily family;
family.exchange = &exchange;
family.coordinate = coordinate;
family.targetLanes = LaneSet::range(begin, begin + points.size());
family.producer = points.front().producer;
family.publicationFragmentType = points.front().fragmentType;
auto sequence = [&](auto member) -> std::optional<StaticIntSequence> {
if (!(points.front().*member))
return std::nullopt;
SmallVector<int64_t> values;
for (const RequirementPoint& point : points)
values.push_back(*(point.*member));
return StaticIntSequence::fromValues(values);
};
family.graphLanes = sequence(&RequirementPoint::graphLane);
family.producerLocalOffsets = sequence(&RequirementPoint::localOffset);
if (points.front().producerProjection) {
family.producerProjection.emplace();
auto appendGeometry = [&](auto member,
SmallVectorImpl<StaticIntSequence>& target) {
for (size_t dimension = 0;
dimension < ((*points.front().producerProjection).*member).size();
++dimension) {
SmallVector<int64_t> values;
values.reserve(points.size());
for (const RequirementPoint& point : points)
values.push_back(((*point.producerProjection).*member)[dimension]);
target.push_back(StaticIntSequence::fromValues(values));
}
};
appendGeometry(&RequirementPoint::SliceGeometry::offsets,
family.producerProjection->offsets);
appendGeometry(&RequirementPoint::SliceGeometry::sizes,
family.producerProjection->sizes);
appendGeometry(&RequirementPoint::SliceGeometry::strides,
family.producerProjection->strides);
}
exchange.requirements.push_back(std::move(family));
}
static LogicalResult buildRequirementFamilies(DeferredTransferPlan& plan,
DeferredExchangePlan& exchange,
GraphBatchPublicationCache& publicationCache) {
DeferredLaneValueEvaluator evaluator(exchange.program, exchange.targetLaneCount);
for (auto leafItem : llvm::enumerate(exchange.program.leaves)) {
unsigned leafIndex = leafItem.index();
const DeferredProjectionLeafTemplate& leaf = leafItem.value();
unsigned positionCount = 1;
if (leaf.form == DeferredLeafForm::GraphBatchProjection) {
auto sizes = evaluator.evaluate(leaf.leadingGeometry.sizes.front());
if (failed(sizes))
for (unsigned specialization = 0;
specialization < exchange.program.specializationCount;
++specialization) {
DeferredLaneValueEvaluator evaluator(
exchange.program, exchange.targetLaneCount, specialization);
for (auto leafItem : llvm::enumerate(exchange.program.leaves)) {
unsigned leafIndex = leafItem.index();
const DeferredProjectionLeafTemplate &leaf = leafItem.value();
auto sourceIndices = evaluator.resolveSourceOperandIndices(leaf.sourceRoot);
if (failed(sourceIndices))
return failure();
for (unsigned lane = 0; lane < exchange.targetLaneCount; ++lane)
positionCount = std::max<unsigned>(positionCount, sizes->valueAt(lane));
}
else {
auto sources = evaluator.resolveSourceOperandIndices(leaf.sourceRoot);
if (failed(sources))
return failure();
for (unsigned lane = 0; lane < exchange.targetLaneCount; ++lane) {
auto source = dyn_cast<OpResult>(exchange.deferred.getSources()[sources->valueAt(lane)]);
auto type = source ? dyn_cast<RankedTensorType>(source.getType()) : RankedTensorType();
if (source && isa<SpatGraphComputeBatch>(source.getOwner()) && type)
positionCount = std::max<unsigned>(positionCount, type.getDimSize(0));
}
}
for (unsigned position = 0; position < positionCount; ++position) {
unsigned runBegin = 0;
SmallVector<RequirementPoint> run;
auto flush = [&] {
if (!run.empty())
appendRequirementFamily(exchange, {static_cast<unsigned>(leafIndex), position}, runBegin, run);
run.clear();
};
for (unsigned lane = 0; lane < exchange.targetLaneCount; ++lane) {
auto point = resolveRequirementPoint(plan, exchange, leaf, evaluator, lane, position, publicationCache);
if (failed(point))
return failure();
if (!*point) {
flush();
continue;
DeferredStaticSliceGeometry geometry;
auto evaluate = [&](ArrayRef<OpFoldResult> values,
SmallVectorImpl<StaticIntSequence> &target) {
for (OpFoldResult value : values) {
auto sequence = evaluator.evaluate(value);
if (failed(sequence))
return failure();
target.push_back(std::move(*sequence));
}
return success();
};
if (failed(evaluate(leaf.leadingGeometry.offsets, geometry.offsets))
|| failed(evaluate(leaf.leadingGeometry.sizes, geometry.sizes))
|| failed(evaluate(leaf.leadingGeometry.strides, geometry.strides)))
return failure();
struct Source {
OpResult value;
int64_t graphId;
const GraphBatchPublicationMap *publication = nullptr;
ProducedValue *scalarProducer = nullptr;
};
SmallVector<Source> sources;
unsigned positionCount = 1;
for (unsigned lane = 0; lane < exchange.targetLaneCount; ++lane) {
Value value = exchange.deferred.getSources()[sourceIndices->valueAt(lane)];
while (auto selection = value.getDefiningOp<SpatDeferredSourceSelectOp>()) {
auto selectors = evaluator.evaluate(selection.getSelector());
if (failed(selectors))
return exchange.deferred.emitOpError(
"phase 2 cannot evaluate deferred source selection"), failure();
int64_t selected = selectors->valueAt(lane);
if (selected < 0 || selected >= static_cast<int64_t>(selection.getSources().size()))
return exchange.deferred.emitOpError(
"phase 2 deferred source selection is out of range"), failure();
value = selection.getSources()[selected];
}
auto source = dyn_cast<OpResult>(value);
if (!source)
return exchange.deferred.emitOpError(
"phase 2 requires graph-result deferred sources"), failure();
auto graphId = source.getOwner()->getAttrOfType<IntegerAttr>("scheduled.graph_id");
if (!graphId)
return exchange.deferred.emitOpError(
"phase 2 cannot identify graph producer"), failure();
Source resolved {source, graphId.getInt()};
if (auto batch = dyn_cast<SpatGraphComputeBatch>(source.getOwner())) {
auto publication = getGraphBatchPublicationMap(
batch, source.getResultNumber(), publicationCache);
if (failed(publication))
return failure();
if (leaf.form != DeferredLeafForm::GraphBatchProjection)
positionCount = std::max<unsigned>(
positionCount, (*publication)->physicalSlotToGraphLane.size());
}
else if (isa<SpatGraphCompute>(source.getOwner())) {
auto producer = findProducer(plan, exchange.deferred, graphId.getInt(),
source.getResultNumber(), std::nullopt);
if (failed(producer))
return failure();
resolved.scalarProducer = *producer;
}
sources.push_back(resolved);
}
for (Source &source : sources)
if (auto batch = dyn_cast<SpatGraphComputeBatch>(source.value.getOwner()))
source.publication = &publicationCache.find(
{batch, source.value.getResultNumber()})->second;
if (leaf.form == DeferredLeafForm::GraphBatchProjection)
for (unsigned lane = 0; lane < exchange.targetLaneCount; ++lane)
positionCount = std::max<unsigned>(
positionCount, geometry.sizes.front().valueAt(lane));
for (unsigned position = 0; position < positionCount; ++position) {
SmallVector<ProducedValue *> producers(exchange.targetLaneCount);
SmallVector<Type> fragmentTypes(exchange.targetLaneCount);
SmallVector<int64_t> graphLanes(exchange.targetLaneCount, -1);
SmallVector<int64_t> localOffsets(exchange.targetLaneCount, -1);
for (unsigned lane = 0; lane < exchange.targetLaneCount; ++lane) {
Source source = sources[lane];
if (source.publication) {
int64_t slot = position;
if (leaf.form == DeferredLeafForm::GraphBatchProjection) {
if (position >= geometry.sizes.front().valueAt(lane))
continue;
slot = geometry.offsets.front().valueAt(lane)
+ static_cast<int64_t>(position)
* geometry.strides.front().valueAt(lane);
}
else if (position >= source.publication->physicalSlotToGraphLane.size())
continue;
if (slot < 0 || slot >= static_cast<int64_t>(
source.publication->physicalSlotToGraphLane.size()))
return exchange.deferred.emitOpError(
"projection physical slot is outside publication map"),
failure();
graphLanes[lane] = source.publication->physicalSlotToGraphLane[slot];
fragmentTypes[lane] = source.publication->publicationFragmentType;
auto producer = findProducer(
plan, exchange.deferred, source.graphId,
source.value.getResultNumber(),
graphLanes[lane]);
if (failed(producer))
return failure();
producers[lane] = *producer;
localOffsets[lane] = graphLanes[lane] - (*producer)->laneStart;
if (!(*producer)->scheduled->isBatch())
localOffsets[lane] += (*producer)->publishedSlotStart;
}
else if (position == 0 && source.scalarProducer) {
fragmentTypes[lane] = leaf.form == DeferredLeafForm::ScalarProjection
? Type(leaf.reconstructedType)
: source.value.getType();
producers[lane] = source.scalarProducer;
}
}
StaticIntSequence graphLaneSequence =
StaticIntSequence::fromValues(graphLanes);
StaticIntSequence localOffsetSequence =
StaticIntSequence::fromValues(localOffsets);
for (unsigned begin = 0; begin < exchange.targetLaneCount;) {
if (!producers[begin]) {
++begin;
continue;
}
unsigned end = begin + 1;
while (end < exchange.targetLaneCount
&& producers[end] == producers[begin]
&& fragmentTypes[end] == fragmentTypes[begin])
++end;
RequirementFamily family;
family.exchange = &exchange;
family.coordinate = {specialization, leafIndex, position};
family.targetLanes = LaneSet::range(begin, end);
family.producer = producers[begin];
family.publicationFragmentType = fragmentTypes[begin];
if (graphLanes[begin] >= 0) {
family.graphLanes = graphLaneSequence.slice(begin, end - begin);
family.producerLocalOffsets =
localOffsetSequence.slice(begin, end - begin);
}
else if (leaf.form == DeferredLeafForm::ScalarProjection) {
family.producerProjection.emplace();
auto slice = [&](ArrayRef<StaticIntSequence> source,
SmallVectorImpl<StaticIntSequence> &target) {
for (const StaticIntSequence &sequence : source)
target.push_back(sequence.slice(begin, end - begin));
};
slice(geometry.offsets, family.producerProjection->offsets);
slice(geometry.sizes, family.producerProjection->sizes);
slice(geometry.strides, family.producerProjection->strides);
}
exchange.requirements.push_back(std::move(family));
begin = end;
}
if (!run.empty() && !run.front().sameFamily(**point))
flush();
if (run.empty())
runBegin = lane;
run.push_back(**point);
}
flush();
}
}
return success();
@@ -563,7 +367,8 @@ static LogicalResult buildExchanges(func::FuncOp funcOp, DeferredTransferPlan& p
if (!targetOp)
targetOp = deferred->getParentOfType<SpatScheduledComputeBatch>();
ScheduledInfo* target = scheduledByOp.lookup(targetOp);
auto step = target ? getStepIndex(*target, getScheduledBlock(deferred, targetOp)) : FailureOr<unsigned>(failure());
auto step = target ? getStepIndex(*target, deferred)
: FailureOr<unsigned>(failure());
auto program = analyzeDeferredProgramTemplate(deferred);
if (!target || failed(step) || failed(program))
return deferred.emitOpError("phase 2 cannot normalize deferred communication");
@@ -586,6 +391,13 @@ static LogicalResult
retargetBlueprint(DeferredTransferPlan& plan, SpatBlueprintOp blueprint, GraphBatchPublicationCache& publicationCache) {
if (blueprint.getMode() != "fragment_assembly")
return success();
bool escapesScheduledGraph = llvm::any_of(
blueprint.getOutput().getUses(), [](OpOperand &use) {
return !isa<SpatGraphCompute, SpatGraphComputeBatch,
SpatDeferredCommunicationOp>(use.getOwner());
});
if (!escapesScheduledGraph)
return success();
auto operandIndices = blueprint.getFragmentOperandIndices();
auto sourceSlots = blueprint.getFragmentSourceSlots();
if (!operandIndices || !sourceSlots)
@@ -608,6 +420,9 @@ retargetBlueprint(DeferredTransferPlan& plan, SpatBlueprintOp blueprint, GraphBa
auto producer = findProducer(plan, blueprint, graphId.getInt(), result.getResultNumber(), graphLane);
if (failed(producer))
return failure();
if (!(*producer)->published)
return blueprint.emitOpError(
"phase 2 Blueprint source has no scheduled publication"), failure();
source = (*producer)->published;
slot = (*producer)->publishedSlotStart + graphLane - (*producer)->laneStart;
if (slot < (*producer)->publishedSlotStart
@@ -636,9 +451,12 @@ retargetBlueprint(DeferredTransferPlan& plan, SpatBlueprintOp blueprint, GraphBa
} // namespace
FailureOr<DeferredTransferPlan> buildDeferredTransferPlan(func::FuncOp funcOp) {
FailureOr<DeferredTransferPlan> buildDeferredTransferPlan(
func::FuncOp funcOp,
const ScheduledComputeMaterializationResult &materialization) {
DeferredTransferPlan plan;
if (failed(collectScheduledOperations(funcOp, plan)) || failed(collectProducedValues(plan))
if (failed(collectScheduledOperations(materialization, plan))
|| failed(collectProducedValues(materialization, plan))
|| failed(buildExchanges(funcOp, plan)))
return failure();
return std::move(plan);
@@ -3,6 +3,7 @@
#include "mlir/Dialect/Func/IR/FuncOps.h"
#include "DeferredCommunicationModel.hpp"
#include "ScheduledComputeMaterialization.hpp"
namespace onnx_mlir::spatial {
@@ -14,7 +15,9 @@ struct DeferredTransferPlan {
llvm::SmallVector<unsigned> stepCounts;
};
mlir::FailureOr<DeferredTransferPlan> buildDeferredTransferPlan(mlir::func::FuncOp funcOp);
mlir::FailureOr<DeferredTransferPlan>
buildDeferredTransferPlan(mlir::func::FuncOp funcOp,
const ScheduledComputeMaterializationResult &materialization);
mlir::LogicalResult retargetDeferredPublications(mlir::func::FuncOp funcOp, DeferredTransferPlan& plan);
@@ -44,11 +44,9 @@ struct MergeComputeNodesPass final : PassWrapper<MergeComputeNodesPass, Operatio
signalPassFailure();
return;
}
// Phase 1 is intentionally dumped before its verifier: malformed deferred
// payloads must be diagnosed from the producer-owned body.
dumpModule(moduleOp, "spatial2_scheduled_no_comm", /*assumeVerified=*/true);
dumpModule(moduleOp, "spatial3_scheduled_no_comm", /*assumeVerified=*/true);
if (failed(verifyMaterializedScheduleMapping(funcOp,
schedule,
materialization->peftClassPlans,
@@ -68,14 +66,6 @@ struct MergeComputeNodesPass final : PassWrapper<MergeComputeNodesPass, Operatio
signalPassFailure();
return;
}
if (failed(verifyPeftMaterializationReportSummary(funcOp,
schedule,
materialization->peftClassPlans,
materialization->materializedSchedules))) {
moduleOp.emitError("scheduled Spatial report verification failed");
signalPassFailure();
return;
}
if (failed(verifyScheduledSpatialInvariants(funcOp))) {
moduleOp.emitError("scheduled Spatial phase 1 verification failed");
signalPassFailure();
@@ -83,30 +73,35 @@ struct MergeComputeNodesPass final : PassWrapper<MergeComputeNodesPass, Operatio
}
SpatialDataflowExportStage exportMode = getSpatialDataflowExportStage();
if (shouldExportSpatialDataflowStage(exportMode, SpatialDataflowExportStage::Spatial2)
&& failed(exportSpatialDataflowCsvScheduled(funcOp, "spatial2_scheduled_no_comm", "spatial2"))) {
if (shouldExportSpatialDataflowStage(exportMode, SpatialDataflowExportStage::Spatial3)
&& failed(exportSpatialDataflowCsvScheduled(
funcOp, materialization->materializedSchedules,
"spatial3_scheduled_no_comm", "spatial3"))) {
signalPassFailure();
return;
}
dumpScheduledComputeReportAndModule(moduleOp,
funcOp,
schedule,
materialization->peftClassPlans,
materialization->materializedSchedules);
if (failed(realizeDeferredCommunication(funcOp))) {
dumpScheduledComputeReport(moduleOp,
funcOp,
schedule,
materialization->peftClassPlans,
materialization->materializedSchedules);
if (failed(realizeDeferredCommunication(funcOp, *materialization))) {
moduleOp.emitError("MergeComputeNodes phase 2 communication realization failed");
signalPassFailure();
return;
}
dumpModule(moduleOp, "spatial3_scheduled", /*assumeVerified=*/true);
if (failed(verifyScheduledSpatialInvariants(funcOp))) {
dumpModule(moduleOp, "spatial4_scheduled", /*assumeVerified=*/true);
if (failed(verifyScheduledResultsLive(materialization->materializedSchedules))
|| failed(verifyScheduledSpatialInvariants(funcOp))) {
moduleOp.emitError("scheduled Spatial phase 2 verification failed");
signalPassFailure();
return;
}
if (shouldExportSpatialDataflowStage(exportMode, SpatialDataflowExportStage::Spatial3)
&& failed(exportSpatialDataflowCsvScheduled(funcOp, "spatial3_scheduled", "spatial3"))) {
if (shouldExportSpatialDataflowStage(exportMode, SpatialDataflowExportStage::Spatial4)
&& failed(exportSpatialDataflowCsvScheduled(
funcOp, materialization->materializedSchedules,
"spatial4_scheduled", "spatial4"))) {
signalPassFailure();
}
}
@@ -6,6 +6,7 @@
#include "src/Accelerators/PIM/Common/IR/AffineUtils.hpp"
#include "src/Accelerators/PIM/Common/IR/ConstantUtils.hpp"
#include "src/Accelerators/PIM/Common/IR/LoopUtils.hpp"
#include "src/Accelerators/PIM/Common/IR/TensorSliceUtils.hpp"
#include "src/Accelerators/PIM/Common/PimCommon.hpp"
#include <map>
@@ -18,11 +19,6 @@ namespace spatial {
using namespace mlir;
namespace {
struct BatchFragmentSpec {
RankedTensorType resultType;
RankedTensorType sourceSliceType;
};
static SmallVector<OpFoldResult> remapMixedOffsets(ArrayRef<OpFoldResult> mixedOffsets, IRMapping &mapper) {
SmallVector<OpFoldResult> remapped;
remapped.reserve(mixedOffsets.size());
@@ -35,135 +31,34 @@ static SmallVector<OpFoldResult> remapMixedOffsets(ArrayRef<OpFoldResult> mixedO
return remapped;
}
static void appendUnique(SmallVectorImpl<Value> &values, Value value) {
if (!llvm::is_contained(values, value))
values.push_back(value);
}
static Value getBlockOperand(Block &block, ValueRange operands, Value value, unsigned firstArgument = 0) {
auto it = llvm::find(operands, value);
assert(it != operands.end() && "missing scheduled operand");
return block.getArgument(firstArgument + std::distance(operands.begin(), it));
}
static Value getScheduledComputeOutputArgument(Block &block, ValueRange scheduledWeights, ValueRange scheduledInputs,
ArrayRef<ProducerValueKey> carriedKeys, ProducerValueKey key) {
unsigned base = scheduledWeights.size() + scheduledInputs.size();
auto it = llvm::find(carriedKeys, key);
assert(it != carriedKeys.end() && "missing carried output");
return block.getArgument(base + std::distance(carriedKeys.begin(), it));
}
static unsigned getScheduledComputeResultArgBase(SpatScheduledCompute scheduled) {
return scheduled.getWeights().size() + scheduled.getInputs().size();
}
static void appendComputeBlockArguments(SmallVectorImpl<Type> &argTypes,
SmallVectorImpl<Location> &argLocs,
ValueRange weights,
ValueRange inputs,
ArrayRef<ProducerValueKey> carriedKeys,
Location loc) {
for (Value weight : weights)
argTypes.push_back(weight.getType());
for (Value input : inputs)
argTypes.push_back(input.getType());
for (ProducerValueKey key : carriedKeys) {
auto outputs = getComputeInstanceOutputValues(key.instance);
assert(key.resultIndex < outputs.size() && "missing carried result");
argTypes.push_back(outputs[key.resultIndex].getType());
}
argLocs.append(argTypes.size(), loc);
}
static Block *createScheduledComputeBlock(PatternRewriter &rewriter,
SpatScheduledCompute scheduled,
ArrayRef<ProducerValueKey> carriedKeys,
Location loc) {
SmallVector<Type> argTypes;
SmallVector<Location> argLocs;
appendComputeBlockArguments(argTypes, argLocs, scheduled.getWeights(), scheduled.getInputs(), carriedKeys, loc);
appendComputeBlockArguments(argTypes, argLocs, scheduled.getWeights(),
scheduled.getInputs(), loc);
return rewriter.createBlock(&scheduled.getBody(), scheduled.getBody().end(), TypeRange(argTypes), argLocs);
}
static void appendBlockYieldBaseAndCarriedOperands(Block &block,
unsigned baseArgCount,
size_t carriedCount,
SmallVectorImpl<Value> &operands) {
for (unsigned index = 0; index < baseArgCount; ++index)
operands.push_back(block.getArgument(index));
for (size_t index = 0; index < carriedCount; ++index)
operands.push_back(block.getArgument(baseArgCount + index));
}
static void createBlockYield(PatternRewriter &rewriter, Location loc, ValueRange outputs, Block *next = nullptr) {
OperationState state(loc, SpatBlockYieldOp::getOperationName());
state.addOperands(outputs);
if (next)
state.addSuccessors(next);
rewriter.create(state);
}
static FailureOr<BatchFragmentSpec> getBatchFragmentSpec(SpatComputeBatch batch,
unsigned resultIndex,
uint32_t fragmentLaneCount) {
auto inParallel = dyn_cast<SpatInParallelOp>(batch.getBody().front().getTerminator());
if (!inParallel)
return batch.emitOpError("scheduled materialization only supports resultful spat.graph_compute_batch");
auto outputArg = batch.getOutputArgument(resultIndex);
if (!outputArg)
return batch.emitOpError("scheduled materialization could not locate batch output block argument");
for (Operation &op : inParallel.getRegion().front()) {
auto insert = dyn_cast<tensor::ParallelInsertSliceOp>(&op);
if (!insert)
return batch.emitOpError("scheduled materialization only supports regular leading-lane graph_compute_batch fragments");
if (insert.getDest() != *outputArg)
continue;
RankedTensorType destType = insert.getDestType();
RankedTensorType sourceType = insert.getSourceType();
if (!destType || !sourceType || !destType.hasStaticShape() || !sourceType.hasStaticShape())
return batch.emitOpError("scheduled materialization only supports regular leading-lane graph_compute_batch fragments");
if (destType.getRank() != sourceType.getRank() + 1 || destType.getDimSize(0) != batch.getLaneCount()
|| destType.getElementType() != sourceType.getElementType())
return batch.emitOpError("graph_compute_batch result must be a leading physical-slot dimension followed by its fragment");
if (!llvm::equal(destType.getShape().drop_front(), sourceType.getShape()))
return batch.emitOpError("graph_compute_batch result trailing shape must match its published fragment");
if (!insert.hasUnitStride())
return batch.emitOpError("scheduled materialization only supports regular leading-lane graph_compute_batch fragments");
auto offsets = insert.getMixedOffsets();
auto sizes = insert.getMixedSizes();
auto strides = insert.getMixedStrides();
if (offsets.size() != static_cast<size_t>(destType.getRank()) || sizes.size() != static_cast<size_t>(destType.getRank())
|| strides.size() != static_cast<size_t>(destType.getRank()))
return batch.emitOpError("scheduled materialization only supports regular leading-lane graph_compute_batch fragments");
if (!isa<Value>(offsets.front()) || !valueTransitivelyDependsOn(cast<Value>(offsets.front()), *batch.getLaneArgument()))
return batch.emitOpError("graph_compute_batch publication must select its physical slot in dimension zero");
for (unsigned dim = 1; dim < offsets.size(); ++dim) {
auto offset = dyn_cast<Attribute>(offsets[dim]);
auto integer = dyn_cast_or_null<IntegerAttr>(offset);
if (!integer || integer.getInt() != 0)
return batch.emitOpError("graph_compute_batch publication must have zero trailing offsets");
}
auto staticIndex = [](OpFoldResult value) -> std::optional<int64_t> {
auto attr = dyn_cast<Attribute>(value);
auto integer = dyn_cast_or_null<IntegerAttr>(attr);
return integer ? std::optional<int64_t>(integer.getInt()) : std::nullopt;
};
if (staticIndex(sizes.front()) != 1)
return batch.emitOpError("graph_compute_batch publication sizes must be [1] plus the fragment shape");
for (auto [size, dim] : llvm::zip_equal(ArrayRef<OpFoldResult>(sizes).drop_front(), sourceType.getShape()))
if (staticIndex(size) != dim)
return batch.emitOpError("graph_compute_batch publication sizes must be [1] plus the fragment shape");
return BatchFragmentSpec {spatial::getGraphBatchPhysicalResultType(fragmentLaneCount, sourceType), sourceType};
}
return batch.emitOpError("scheduled materialization only supports regular leading-lane graph_compute_batch fragments");
}
static SourceLaneSelector buildSourceLaneSelector(PatternRewriter &rewriter,
const ComputeStepTuple &stepTuple,
Operation *constantAnchor,
@@ -223,123 +118,6 @@ static FailureOr<Value> buildSourceLaneStartForScheduledLane(OpBuilder &builder,
return arith::IndexCastOp::create(builder, loc, builder.getIndexType(), sourceLaneStartI64).getResult();
}
static LogicalResult verifyPeftClassPlan(Operation *diagnosticAnchor,
const PeftClassPlan &peftClassPlan,
const MergeScheduleResult &schedule) {
if (peftClassPlan.cpus.empty())
return diagnosticAnchor->emitOpError("PEFT materialization class has no CPUs");
SmallVector<const SmallVector<ComputeInstance> *> schedules;
for (size_t cpu : peftClassPlan.cpus) {
auto it = peftClassPlan.instancesByCpu.find(cpu);
if (it == peftClassPlan.instancesByCpu.end())
return diagnosticAnchor->emitOpError("PEFT materialization class is missing a per-CPU schedule");
schedules.push_back(&it->second);
for (const ComputeInstance &instance : it->second)
if (!schedule.computeToCpuSlotMap.count(instance))
return diagnosticAnchor->emitOpError("PEFT materialization class references a compute instance without a scheduler position");
}
if (peftClassPlan.cpus.size() == 1)
return success();
auto emitNonIso = [&](size_t stepPosition) -> LogicalResult {
std::string cpus;
llvm::raw_string_ostream os(cpus);
llvm::interleaveComma(peftClassPlan.cpus, os, [&](size_t cpu) { os << cpu; });
diagnosticAnchor->emitOpError("PEFT equivalence class has non-isomorphic per-CPU schedules")
<< " class " << peftClassPlan.canonicalClassId << " cpus [" << os.str() << "] step " << stepPosition;
return failure();
};
size_t tupleCount = schedules.front()->size();
for (const SmallVector<ComputeInstance> *cpuSchedule : schedules)
if (cpuSchedule->size() != tupleCount)
return emitNonIso(0);
for (size_t stepPosition = 0; stepPosition < tupleCount; ++stepPosition) {
const ComputeInstance &reference = (*schedules.front())[stepPosition];
bool refIsScalar = isa<SpatCompute>(reference.op);
for (size_t cpuIndex = 1; cpuIndex < schedules.size(); ++cpuIndex) {
const ComputeInstance &instance = (*schedules[cpuIndex])[stepPosition];
if (instance.op != reference.op || instance.laneCount != reference.laneCount)
return emitNonIso(stepPosition);
if (isa<SpatCompute>(instance.op) != refIsScalar)
return emitNonIso(stepPosition);
}
}
return success();
}
static LogicalResult collectPeftClassOperandsAndResults(PeftClassPlan &peftClassPlan,
const MergeScheduleResult &schedule) {
peftClassPlan.weights.clear();
peftClassPlan.inputs.clear();
peftClassPlan.resultTypes.clear();
if (peftClassPlan.cpus.size() == 1) {
size_t cpu = peftClassPlan.cpus.front();
for (const ComputeInstance &instance : peftClassPlan.instancesByCpu.lookup(cpu)) {
if (auto compute = dyn_cast<SpatCompute>(instance.op)) {
llvm::append_range(peftClassPlan.resultTypes, compute.getResultTypes());
} else {
auto batch = cast<SpatComputeBatch>(instance.op);
for (unsigned resultIndex = 0; resultIndex < batch.getNumResults(); ++resultIndex) {
auto spec = getBatchFragmentSpec(batch, resultIndex, instance.laneCount);
if (failed(spec))
return failure();
peftClassPlan.resultTypes.push_back(spec->resultType);
}
}
for (Value weight : getComputeInstanceWeights(instance))
appendUnique(peftClassPlan.weights, weight);
for (Value input : getComputeInstanceInputs(instance))
if (!getProducerValueRef(input, &instance) &&
!isDeferredFragmentAssemblyInput(input, instance))
appendUnique(peftClassPlan.inputs, input);
}
return success();
}
for (const ScheduledStepPlan &stepPlan : buildScheduledStepPlans(peftClassPlan)) {
const ComputeStepTuple &stepTuple = stepPlan.stepTuple;
const ComputeInstance &representative = stepTuple.instances.front();
if (auto compute = dyn_cast<SpatCompute>(representative.op)) {
for (Type type : compute.getResultTypes()) {
auto tensorType = dyn_cast<RankedTensorType>(type);
if (!tensorType || !tensorType.hasStaticShape())
return compute.emitOpError("scheduled materialization only supports static ranked tensor scalar results");
SmallVector<int64_t> shape;
shape.push_back(static_cast<int64_t>(peftClassPlan.cpus.size()));
llvm::append_range(shape, tensorType.getShape());
peftClassPlan.resultTypes.push_back(RankedTensorType::get(shape, tensorType.getElementType()));
}
} else {
auto batch = cast<SpatComputeBatch>(representative.op);
uint32_t totalLanes = static_cast<uint32_t>(peftClassPlan.cpus.size()) * representative.laneCount;
for (unsigned resultIndex = 0; resultIndex < batch.getNumResults(); ++resultIndex) {
auto spec = getBatchFragmentSpec(batch, resultIndex, totalLanes);
if (failed(spec))
return failure();
peftClassPlan.resultTypes.push_back(spec->resultType);
}
}
for (const ComputeInstance &instance : stepTuple.instances) {
for (Value weight : getComputeInstanceWeights(instance))
appendUnique(peftClassPlan.weights, weight);
for (Value input : getComputeInstanceInputs(instance))
if (!getProducerValueRef(input, &instance) &&
!isDeferredFragmentAssemblyInput(input, instance))
appendUnique(peftClassPlan.inputs, input);
}
}
return success();
}
static void cloneComputeBody(OpBuilder &builder, Block &source, IRMapping &mapper,
SmallVectorImpl<Value> &yieldedValues,
const llvm::SmallPtrSetImpl<Operation *> &absorbed) {
@@ -351,18 +129,22 @@ static void cloneComputeBody(OpBuilder &builder, Block &source, IRMapping &mappe
yieldedValues.push_back(mapper.lookup(output));
}
static LogicalResult materializeResultfulBatchChunkAsScalar(PatternRewriter &rewriter,
SpatComputeBatch batch,
const ComputeInstance &instance,
ValueRange scheduledWeights,
ValueRange scheduledInputs,
Block &block,
const MergeScheduleResult &schedule,
SmallVectorImpl<Value> &yieldedValues) {
static LogicalResult materializeResultfulBatchRun(
PatternRewriter &rewriter, SpatComputeBatch batch,
const ScheduledInstanceRun &run, ValueRange scheduledWeights,
ValueRange scheduledInputs, Block &block,
const MergeScheduleResult &schedule,
const DenseMap<ProducerValueKey, MaterializedProducerRef> &availableValues,
SmallVectorImpl<Value> &yieldedValues) {
const ComputeInstance &first = run.instances.front();
const ComputeInstance &last = run.instances.back();
uint32_t runLaneCount = last.laneStart + last.laneCount - first.laneStart;
ComputeInstance runInstance {batch.getOperation(), first.laneStart,
runLaneCount};
SmallVector<Value> initResults;
SmallVector<BatchFragmentSpec> fragmentSpecs;
for (unsigned resultIndex = 0; resultIndex < batch.getNumResults(); ++resultIndex) {
auto spec = getBatchFragmentSpec(batch, resultIndex, instance.laneCount);
auto spec = getBatchFragmentSpec(batch, resultIndex, runLaneCount);
if (failed(spec))
return failure();
fragmentSpecs.push_back(*spec);
@@ -372,8 +154,8 @@ static LogicalResult materializeResultfulBatchChunkAsScalar(PatternRewriter &rew
initResults.push_back(*empty);
}
Value lower = getOrCreateIndexConstant(rewriter, batch.getOperation(), instance.laneStart);
Value upper = getOrCreateIndexConstant(rewriter, batch.getOperation(), instance.laneStart + instance.laneCount);
Value lower = getOrCreateIndexConstant(rewriter, batch.getOperation(), first.laneStart);
Value upper = getOrCreateIndexConstant(rewriter, batch.getOperation(), first.laneStart + runLaneCount);
Value step = getOrCreateIndexConstant(rewriter, batch.getOperation(), 1);
auto loop = buildNormalizedScfFor(
rewriter,
@@ -386,11 +168,12 @@ static LogicalResult materializeResultfulBatchChunkAsScalar(PatternRewriter &rew
IRMapping mapper;
mapper.map(*batch.getLaneArgument(), originalLane);
Value localLane = arith::SubIOp::create(builder,
bodyLoc,
originalLane,
getOrCreateIndexConstant(rewriter, batch.getOperation(), instance.laneStart))
.getResult();
Value localLane = runLaneCount == 1
? getOrCreateIndexConstant(rewriter, batch.getOperation(), 0)
: arith::SubIOp::create(
builder, bodyLoc, originalLane,
getOrCreateIndexConstant(
rewriter, batch.getOperation(), first.laneStart));
for (auto [index, weight] : llvm::enumerate(batch.getWeights()))
mapper.map(*batch.getWeightArgument(index), getBlockOperand(block, scheduledWeights, weight));
SmallVector<DeferredInputPlan> inputPlans;
@@ -400,19 +183,19 @@ static LogicalResult materializeResultfulBatchChunkAsScalar(PatternRewriter &rew
input.getLoc(),
input,
*batch.getInputArgument(index),
instance,
runInstance,
schedule,
scheduledInputs,
block,
scheduledWeights.size(),
ArrayRef<ProducerValueKey> {},
availableValues,
*batch.getLaneArgument(),
originalLane,
plan)))
return failure();
plan.scalarizedLocalLane = localLane;
plan.scalarizedGraphLaneBase = lower;
plan.scalarizedLaneCount = instance.laneCount;
plan.scalarizedLaneCount = runLaneCount;
plan.scalarizedHoistBlock = &block;
inputPlans.push_back(std::move(plan));
}
@@ -474,19 +257,25 @@ static LogicalResult materializeSingleCpuPeftClass(
auto instancesIt = peftClassPlan.instancesByCpu.find(cpu);
assert(instancesIt != peftClassPlan.instancesByCpu.end() && "missing single-cpu schedule");
const SmallVector<ComputeInstance> &instances = instancesIt->second;
Block *block = createScheduledComputeBlock(
rewriter, scheduled, instances.front().op->getLoc());
DenseMap<ProducerValueKey, MaterializedProducerRef> availableValues;
DenseMap<ProducerValueKey, unsigned> publicationIndices;
for (const ScheduledPublication &publication : record.publications)
publicationIndices[publication.producer] = publication.scheduledResultIndex;
SmallVector<Value> publishedOutputs(scheduled.getNumResults());
record.runs = buildScheduledInstanceRuns(instances);
for (const ScheduledInstanceRun &run : record.runs) {
const ComputeInstance &instance = run.instances.front();
for (const ComputeInstance &member : run.instances) {
GraphComputeBlockKey key = getGraphComputeBlockKey(member);
graphComputeToBlockMap[key] = block;
record.computeKeys.push_back(key);
record.blocks.push_back(block);
}
SmallVector<ProducerValueKey> carriedKeys;
Block *block = nullptr;
for (auto [ordinal, instance] : llvm::enumerate(instances)) {
if (!block)
block = createScheduledComputeBlock(rewriter, scheduled, carriedKeys, instance.op->getLoc());
GraphComputeBlockKey key = getGraphComputeBlockKey(instance);
graphComputeToBlockMap[key] = block;
record.computeKeys.push_back(key);
record.blocks.push_back(block);
rewriter.setInsertionPointToStart(block);
Operation *previous = block->empty() ? nullptr : &block->back();
rewriter.setInsertionPointToEnd(block);
SmallVector<Value> yieldedValues;
if (auto compute = dyn_cast<SpatCompute>(instance.op)) {
IRMapping mapper;
@@ -504,7 +293,7 @@ static LogicalResult materializeSingleCpuPeftClass(
scheduled.getInputs(),
*block,
scheduled.getWeights().size(),
carriedKeys,
availableValues,
{},
{},
plan)))
@@ -517,45 +306,76 @@ static LogicalResult materializeSingleCpuPeftClass(
cloneComputeBody(rewriter, compute.getBody().front(), mapper, yieldedValues, absorbed);
} else {
auto batch = cast<SpatComputeBatch>(instance.op);
if (failed(materializeResultfulBatchChunkAsScalar(rewriter,
batch,
instance,
scheduled.getWeights(),
scheduled.getInputs(),
*block,
schedule,
yieldedValues)))
if (failed(materializeResultfulBatchRun(rewriter, batch, run,
scheduled.getWeights(),
scheduled.getInputs(), *block,
schedule, availableValues,
yieldedValues)))
return failure();
}
SmallVector<ProducerValueKey> currentKeys;
for (size_t index = 0; index < yieldedValues.size(); ++index)
currentKeys.push_back({instance, index});
unsigned baseArgCount = getScheduledComputeResultArgBase(scheduled);
SmallVector<Value> blockYieldOperands;
bool hasNextBlock = ordinal + 1 < instances.size();
if (hasNextBlock) {
SmallVector<ProducerValueKey> nextCarriedKeys(carriedKeys);
llvm::append_range(nextCarriedKeys, currentKeys);
Block *nextBlock = createScheduledComputeBlock(rewriter, scheduled, nextCarriedKeys, instance.op->getLoc());
appendBlockYieldBaseAndCarriedOperands(*block, baseArgCount, carriedKeys.size(), blockYieldOperands);
llvm::append_range(blockYieldOperands, yieldedValues);
rewriter.setInsertionPointToEnd(block);
createBlockYield(rewriter, instance.op->getLoc(), blockYieldOperands, nextBlock);
carriedKeys = std::move(nextCarriedKeys);
block = nextBlock;
} else {
for (ProducerValueKey carried : carriedKeys)
blockYieldOperands.push_back(getScheduledComputeOutputArgument(*block,
scheduled.getWeights(),
scheduled.getInputs(),
carriedKeys,
carried));
llvm::append_range(blockYieldOperands, yieldedValues);
rewriter.setInsertionPointToEnd(block);
createBlockYield(rewriter, instance.op->getLoc(), blockYieldOperands);
if (yieldedValues.size()
!= getComputeInstanceResultValueCount(instance))
return scheduled.emitOpError(
"scheduled scalar step produced an unexpected result count");
for (auto [resultIndex, value] : llvm::enumerate(yieldedValues)) {
int64_t runLaneStart = instance.laneStart;
const ComputeInstance &last = run.instances.back();
int64_t runLaneCount = last.laneStart + last.laneCount - runLaneStart;
availableValues[{{instance.op, static_cast<uint32_t>(runLaneStart),
static_cast<uint32_t>(runLaneCount)}, resultIndex}] =
{value, 0, runLaneCount};
for (const ComputeInstance &member : run.instances) {
ProducerValueKey key {member, resultIndex};
int64_t payloadLaneStart = member.laneStart - runLaneStart;
availableValues[key] = {value, payloadLaneStart, member.laneCount};
Value published;
if (auto it = publicationIndices.find(key);
it != publicationIndices.end()) {
Value publication = value;
if (run.instances.size() != 1) {
auto spec = getBatchFragmentSpec(
cast<SpatComputeBatch>(member.op), resultIndex,
member.laneCount);
if (failed(spec))
return failure();
MixedSliceGeometry geometry;
auto payloadType = cast<RankedTensorType>(value.getType());
geometry.offsets.assign(payloadType.getRank(),
rewriter.getIndexAttr(0));
geometry.offsets.front() =
rewriter.getIndexAttr(payloadLaneStart);
geometry.sizes.reserve(payloadType.getRank());
for (int64_t dimension : spec->resultType.getShape())
geometry.sizes.push_back(rewriter.getIndexAttr(dimension));
geometry.strides.assign(payloadType.getRank(),
rewriter.getIndexAttr(1));
publication = extractMixedSliceOrIdentity(
rewriter, member.op->getLoc(), value, spec->resultType,
geometry);
}
publishedOutputs[it->second] = publication;
published = scheduled.getResult(it->second);
}
auto graphId = member.op->getAttrOfType<IntegerAttr>(
"scheduled.graph_id");
if (!graphId)
return member.op->emitOpError(
"scheduled materialization requires graph identity metadata");
record.stepValues.push_back(
{member, static_cast<unsigned>(resultIndex), value,
graphId.getInt(), member.laneStart, member.laneCount,
payloadLaneStart, member.laneCount, published});
}
}
Operation *anchor = previous ? previous->getNextNode() : &block->front();
record.stepAnchors.push_back(anchor);
}
if (llvm::any_of(publishedOutputs, [](Value value) { return !value; }))
return scheduled.emitOpError(
"scheduled scalar materialization did not produce every declared publication");
rewriter.setInsertionPointToEnd(block);
SpatYieldOp::create(rewriter, scheduled.getLoc(), publishedOutputs);
return success();
}
@@ -566,7 +386,7 @@ static SmallVector<OpFoldResult> buildScheduledOutputInsertOffsets(OpBuilder &bu
Location loc,
Value scheduledLane,
int64_t lanesPerScheduledLane,
RankedTensorType localFragmentType,
int64_t destinationRank,
Operation *constantAnchor) {
SmallVector<OpFoldResult> offsets;
Value scheduledOutputLane = scheduledLane;
@@ -575,10 +395,82 @@ static SmallVector<OpFoldResult> buildScheduledOutputInsertOffsets(OpBuilder &bu
builder, loc, scheduledLane, lanesPerScheduledLane, constantAnchor);
}
offsets.push_back(scheduledOutputLane);
offsets.append(localFragmentType.getRank() - 1, OpFoldResult(builder.getIndexAttr(0)));
offsets.append(destinationRank - 1, OpFoldResult(builder.getIndexAttr(0)));
return offsets;
}
struct BatchPublication {
Value fragment;
BlockArgument destination;
SmallVector<OpFoldResult> offsets;
SmallVector<OpFoldResult> sizes;
SmallVector<OpFoldResult> strides;
};
static LogicalResult collectMultiCpuPublications(
PatternRewriter &rewriter,
SpatScheduledComputeBatch scheduled,
const ComputeStepTuple &stepTuple,
const ComputeInstance &representative,
ArrayRef<Value> localFragments,
const DenseMap<ProducerValueKey, unsigned> &publicationIndices,
Block &block,
ScheduledMaterializationRecord &record,
SmallVectorImpl<BatchPublication> &publications) {
Value scheduledLane = block.getArgument(0);
for (auto [resultIndex, localFragment] : llvm::enumerate(localFragments)) {
auto publicationIt = publicationIndices.find({representative, resultIndex});
Value published = publicationIt == publicationIndices.end()
? Value()
: scheduled.getResult(publicationIt->second);
for (auto [lane, instance] : llvm::enumerate(stepTuple.instances)) {
auto instancePublication = publicationIndices.find({instance, resultIndex});
if ((instancePublication == publicationIndices.end()) != !published
|| (instancePublication != publicationIndices.end()
&& instancePublication->second != publicationIt->second))
return scheduled.emitOpError("scheduled batch tuple has inconsistent publication ownership");
auto graphId = instance.op->getAttrOfType<IntegerAttr>("scheduled.graph_id");
if (!graphId)
return instance.op->emitOpError("scheduled materialization requires graph identity metadata");
record.stepValues.push_back({instance, static_cast<unsigned>(resultIndex),
localFragment, graphId.getInt(),
instance.laneStart, instance.laneCount,
static_cast<int64_t>(lane) * instance.laneCount,
instance.laneCount, published});
}
if (!published)
continue;
auto localType = cast<RankedTensorType>(localFragment.getType());
auto destination = block.getArgument(
getScheduledBatchResultArgBase(scheduled) + publicationIt->second);
auto destinationType = cast<RankedTensorType>(destination.getType());
if (destinationType.getRank() != localType.getRank()
&& destinationType.getRank() != localType.getRank() + 1)
return scheduled.emitOpError(
"scheduled publication source must match or rank-reduce into its destination");
int64_t lanesPerScheduledLane = isa<SpatCompute>(representative.op)
? 1 : representative.laneCount;
SmallVector<OpFoldResult> offsets = buildScheduledOutputInsertOffsets(
rewriter, scheduled.getLoc(), scheduledLane, lanesPerScheduledLane,
destinationType.getRank(), scheduled.getOperation());
SmallVector<OpFoldResult> sizes;
SmallVector<OpFoldResult> strides;
if (destinationType.getRank() == localType.getRank() + 1) {
sizes.push_back(rewriter.getIndexAttr(1));
strides.push_back(rewriter.getIndexAttr(1));
}
for (int64_t dim : localType.getShape()) {
sizes.push_back(rewriter.getIndexAttr(dim));
strides.push_back(rewriter.getIndexAttr(1));
}
publications.push_back({localFragment, destination, std::move(offsets),
std::move(sizes), std::move(strides)});
}
return success();
}
static LogicalResult materializeMultiCpuPeftClass(
PatternRewriter &rewriter,
SpatScheduledComputeBatch scheduled,
@@ -588,25 +480,31 @@ static LogicalResult materializeMultiCpuPeftClass(
ScheduledMaterializationRecord &record) {
std::map<std::vector<uint32_t>, Value> laneStartTableCache;
ArrayRef<ScheduledStepPlan> stepPlans = record.stepPlans;
DenseMap<ProducerValueKey, unsigned> publicationIndices;
for (const ScheduledPublication &publication : record.publications)
publicationIndices[publication.producer] = publication.scheduledResultIndex;
SmallVector<Type> blockArgTypes {rewriter.getIndexType()};
SmallVector<Location> blockArgLocs {scheduled.getLoc()};
for (Value weight : scheduled.getWeights()) {
blockArgTypes.push_back(weight.getType());
blockArgLocs.push_back(weight.getLoc());
}
for (Value input : scheduled.getInputs()) {
blockArgTypes.push_back(input.getType());
blockArgLocs.push_back(input.getLoc());
}
for (Type resultType : scheduled.getResultTypes()) {
blockArgTypes.push_back(resultType);
blockArgLocs.push_back(scheduled.getLoc());
}
Block *block = rewriter.createBlock(
&scheduled.getBody(), scheduled.getBody().end(), blockArgTypes,
blockArgLocs);
SmallVector<BatchPublication> publications;
for (const ScheduledStepPlan &stepPlan : stepPlans) {
const ComputeStepTuple &stepTuple = stepPlan.stepTuple;
SourceLaneSelector sourceLaneSelector =
buildSourceLaneSelector(rewriter, stepTuple, scheduled.getOperation(), laneStartTableCache);
SmallVector<Type> blockArgTypes {rewriter.getIndexType()};
SmallVector<Location> blockArgLocs {scheduled.getLoc()};
for (Value weight : scheduled.getWeights()) {
blockArgTypes.push_back(weight.getType());
blockArgLocs.push_back(weight.getLoc());
}
for (Value input : scheduled.getInputs()) {
blockArgTypes.push_back(input.getType());
blockArgLocs.push_back(input.getLoc());
}
for (Type resultType : scheduled.getResultTypes()) {
blockArgTypes.push_back(resultType);
blockArgLocs.push_back(scheduled.getLoc());
}
Block *block = rewriter.createBlock(&scheduled.getBody(), scheduled.getBody().end(), blockArgTypes, blockArgLocs);
for (const ComputeInstance &instance : stepTuple.instances) {
GraphComputeBlockKey key = getGraphComputeBlockKey(instance);
graphComputeToBlockMap[key] = block;
@@ -614,7 +512,8 @@ static LogicalResult materializeMultiCpuPeftClass(
record.blocks.push_back(block);
}
rewriter.setInsertionPointToStart(block);
Operation *previous = block->empty() ? nullptr : &block->back();
rewriter.setInsertionPointToEnd(block);
Value scheduledLane = block->getArgument(0);
const ComputeInstance &representative = stepTuple.instances.front();
SmallVector<Value> finalLocalFragments;
@@ -654,18 +553,7 @@ static LogicalResult materializeMultiCpuPeftClass(
auto tensorType = dyn_cast<RankedTensorType>(yielded.getType());
if (!tensorType || !tensorType.hasStaticShape() || tensorType.getRank() == 0)
return compute.emitOpError("scheduled materialization only supports static ranked tensor scalar step results");
SmallVector<ReassociationIndices> reassociation;
reassociation.push_back({0, 1});
for (int64_t dim = 1; dim < tensorType.getRank(); ++dim)
reassociation.push_back({static_cast<int64_t>(dim + 1)});
SmallVector<int64_t> expandedShape {1};
llvm::append_range(expandedShape, tensorType.getShape());
finalLocalFragments.push_back(tensor::ExpandShapeOp::create(rewriter,
scheduled.getLoc(),
RankedTensorType::get(expandedShape, tensorType.getElementType()),
yielded,
reassociation)
.getResult());
finalLocalFragments.push_back(yielded);
}
} else {
auto batch = cast<SpatComputeBatch>(representative.op);
@@ -779,45 +667,30 @@ static LogicalResult materializeMultiCpuPeftClass(
finalLocalFragments.assign(loop->results.begin(), loop->results.end());
}
struct Publication {
Value fragment;
SmallVector<OpFoldResult> offsets;
SmallVector<OpFoldResult> sizes;
SmallVector<OpFoldResult> strides;
};
SmallVector<Publication> publications;
for (auto [resultIndex, localFragment] : llvm::enumerate(finalLocalFragments)) {
auto localFragmentType = cast<RankedTensorType>(localFragment.getType());
int64_t lanesPerScheduledLane = isa<SpatCompute>(representative.op) ? 1 : representative.laneCount;
SmallVector<OpFoldResult> offsets = buildScheduledOutputInsertOffsets(
rewriter,
scheduled.getLoc(),
scheduledLane,
lanesPerScheduledLane,
localFragmentType,
scheduled.getOperation());
SmallVector<OpFoldResult> sizes;
SmallVector<OpFoldResult> strides;
for (int64_t dim : localFragmentType.getShape()) {
sizes.push_back(rewriter.getIndexAttr(dim));
strides.push_back(rewriter.getIndexAttr(1));
}
publications.push_back(
{localFragment, std::move(offsets), std::move(sizes),
std::move(strides)});
}
auto inParallel = SpatInParallelOp::create(rewriter, scheduled.getLoc());
rewriter.setInsertionPointToStart(&inParallel.getRegion().front());
for (auto [resultIndex, publication] : llvm::enumerate(publications))
tensor::ParallelInsertSliceOp::create(
rewriter,
scheduled.getLoc(),
publication.fragment,
block->getArgument(getScheduledBatchResultArgBase(scheduled) + stepPlan.resultOffset + resultIndex),
publication.offsets,
publication.sizes,
publication.strides);
if (failed(collectMultiCpuPublications(
rewriter, scheduled, stepTuple, representative,
finalLocalFragments, publicationIndices, *block, record,
publications)))
return failure();
Operation *anchor = previous ? previous->getNextNode()
: (block->empty() ? nullptr : &block->front());
if (!anchor)
return scheduled.emitOpError(
"scheduled batch step did not materialize a body operation");
record.stepAnchors.push_back(anchor);
}
rewriter.setInsertionPointToEnd(block);
if (publications.empty()) {
SpatYieldOp::create(rewriter, scheduled.getLoc(), ValueRange {});
return success();
}
auto inParallel = SpatInParallelOp::create(rewriter, scheduled.getLoc());
rewriter.setInsertionPointToEnd(&inParallel.getRegion().front());
for (const BatchPublication &publication : publications)
tensor::ParallelInsertSliceOp::create(
rewriter, scheduled.getLoc(), publication.fragment,
publication.destination, publication.offsets, publication.sizes,
publication.strides);
return success();
}
@@ -878,6 +751,7 @@ materializeScheduledCompute(func::FuncOp funcOp,
record.canonicalPeftClassId = peftClassPlan.canonicalClassId;
record.cpus = peftClassPlan.cpus;
record.stepPlans = buildScheduledStepPlans(peftClassPlan);
record.publications = peftClassPlan.publications;
if (peftClassPlan.cpus.size() == 1) {
auto scheduled = SpatScheduledCompute::create(
@@ -886,39 +760,8 @@ materializeScheduledCompute(func::FuncOp funcOp,
TypeRange(peftClassPlan.resultTypes),
peftClassPlan.weights,
peftClassPlan.inputs);
scheduled->setAttr("scheduled.realized", rewriter.getBoolAttr(true));
scheduled->setAttr(kCoreIdAttrName, rewriter.getI32IntegerAttr(static_cast<int32_t>(peftClassPlan.cpus.front())));
scheduled->setAttr("scheduled.peft_cpus", rewriter.getDenseI64ArrayAttr(toI64Array(peftClassPlan.cpus)));
SmallVector<Attribute> stepSources;
SmallVector<Attribute> sourceLaneSelectors;
SmallVector<int64_t> stepResultOffsets;
SmallVector<int64_t> stepResultCounts;
SmallVector<int64_t> sourceLaneStarts;
SmallVector<int64_t> sourceLaneCounts;
SmallVector<int64_t> stepSourceIds;
size_t resultOffset = 0;
for (const ComputeInstance &instance : peftClassPlan.instancesByCpu.lookup(peftClassPlan.cpus.front())) {
stepSources.push_back(rewriter.getStringAttr(getInstanceName(instance)));
stepSourceIds.push_back(graphIds.lookup(instance.op));
sourceLaneSelectors.push_back(rewriter.getStringAttr(isa<SpatCompute>(instance.op) ? "scalar" : "affine"));
size_t resultCount = getComputeInstanceResultValueCount(instance);
stepResultOffsets.push_back(static_cast<int64_t>(resultOffset));
stepResultCounts.push_back(static_cast<int64_t>(resultCount));
resultOffset += resultCount;
if (isa<SpatCompute>(instance.op)) {
sourceLaneStarts.push_back(0);
sourceLaneCounts.push_back(0);
} else {
sourceLaneStarts.push_back(instance.laneStart);
sourceLaneCounts.push_back(instance.laneCount);
}
}
scheduled->setAttr("scheduled.step_sources", rewriter.getArrayAttr(stepSources));
scheduled->setAttr("scheduled.step_source_ids", rewriter.getDenseI64ArrayAttr(stepSourceIds));
scheduled->setAttr("scheduled.step_result_offsets", rewriter.getDenseI64ArrayAttr(stepResultOffsets));
scheduled->setAttr("scheduled.step_result_counts", rewriter.getDenseI64ArrayAttr(stepResultCounts));
scheduled->setAttr("scheduled.source_lane_starts", rewriter.getDenseI64ArrayAttr(sourceLaneStarts));
scheduled->setAttr("scheduled.source_lane_counts", rewriter.getDenseI64ArrayAttr(sourceLaneCounts));
scheduled->setAttr("scheduled.source_lane_selector", rewriter.getArrayAttr(sourceLaneSelectors));
record.scheduledOp = scheduled.getOperation();
scheduledComputes[peftClassPlan.canonicalClassId] = scheduled;
} else {
@@ -928,36 +771,8 @@ materializeScheduledCompute(func::FuncOp funcOp,
rewriter.getI32IntegerAttr(static_cast<int32_t>(peftClassPlan.cpus.size())),
peftClassPlan.weights,
peftClassPlan.inputs);
scheduled->setAttr("scheduled.realized", rewriter.getBoolAttr(true));
scheduled->setAttr(kCoreIdsAttrName, rewriter.getDenseI32ArrayAttr(toI32Array(peftClassPlan.cpus)));
scheduled->setAttr("scheduled.peft_cpus", rewriter.getDenseI64ArrayAttr(toI64Array(peftClassPlan.cpus)));
SmallVector<Attribute> stepSources;
SmallVector<Attribute> sourceLaneSelectors;
SmallVector<int64_t> resultOffsets;
SmallVector<int64_t> resultCounts;
SmallVector<int64_t> sourceLaneStarts;
SmallVector<int64_t> sourceLaneCounts;
SmallVector<int64_t> stepSourceIds;
for (const ScheduledStepPlan &stepPlan : record.stepPlans) {
stepSources.push_back(rewriter.getStringAttr(getInstanceName(stepPlan.stepTuple.instances.front())));
stepSourceIds.push_back(graphIds.lookup(stepPlan.stepTuple.instances.front().op));
sourceLaneSelectors.push_back(rewriter.getStringAttr(usesAffineSourceLaneMapping(stepPlan.stepTuple) ? "affine" : "table"));
resultOffsets.push_back(static_cast<int64_t>(stepPlan.resultOffset));
resultCounts.push_back(static_cast<int64_t>(stepPlan.resultCount));
for (const ComputeInstance &instance : stepPlan.stepTuple.instances) {
sourceLaneStarts.push_back(instance.laneStart);
sourceLaneCounts.push_back(instance.laneCount);
}
}
RankedTensorType sourceLaneTableType = RankedTensorType::get(
{static_cast<int64_t>(record.stepPlans.size()), static_cast<int64_t>(peftClassPlan.cpus.size())},
rewriter.getI64Type());
scheduled->setAttr("scheduled.step_sources", rewriter.getArrayAttr(stepSources));
scheduled->setAttr("scheduled.step_source_ids", rewriter.getDenseI64ArrayAttr(stepSourceIds));
scheduled->setAttr("scheduled.step_result_offsets", rewriter.getDenseI64ArrayAttr(resultOffsets));
scheduled->setAttr("scheduled.step_result_counts", rewriter.getDenseI64ArrayAttr(resultCounts));
scheduled->setAttr("scheduled.source_lane_starts", DenseElementsAttr::get(sourceLaneTableType, ArrayRef<int64_t>(sourceLaneStarts)));
scheduled->setAttr("scheduled.source_lane_counts", DenseElementsAttr::get(sourceLaneTableType, ArrayRef<int64_t>(sourceLaneCounts)));
scheduled->setAttr("scheduled.source_lane_selector", rewriter.getArrayAttr(sourceLaneSelectors));
record.scheduledOp = scheduled.getOperation();
scheduledComputeBatches[peftClassPlan.canonicalClassId] = scheduled;
}
@@ -5,12 +5,29 @@
namespace onnx_mlir {
namespace spatial {
struct BatchFragmentSpec {
RankedTensorType resultType;
RankedTensorType sourceSliceType;
};
struct ScheduledComputeMaterializationResult {
llvm::MapVector<size_t, PeftClassPlan> peftClassPlans;
std::vector<ScheduledMaterializationRecord> materializedSchedules;
DenseMap<GraphComputeBlockKey, Block *> graphComputeToBlockMap;
};
FailureOr<BatchFragmentSpec>
getBatchFragmentSpec(SpatComputeBatch batch,
unsigned resultIndex,
uint32_t fragmentLaneCount);
LogicalResult verifyPeftClassPlan(Operation *diagnosticAnchor,
const PeftClassPlan &peftClassPlan,
const MergeScheduleResult &schedule);
LogicalResult collectPeftClassOperandsAndResults(
PeftClassPlan &peftClassPlan, const MergeScheduleResult &schedule);
FailureOr<ScheduledComputeMaterializationResult>
materializeScheduledCompute(func::FuncOp funcOp,
const MergeScheduleResult &schedule,
@@ -3,7 +3,6 @@
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/Func/IR/FuncOps.h"
#include "mlir/Dialect/Tensor/IR/Tensor.h"
#include "mlir/IR/AsmState.h"
#include "mlir/IR/IRMapping.h"
#include "llvm/ADT/DenseMap.h"
@@ -20,6 +19,7 @@
#include <vector>
#include "Scheduling/ComputeInstanceUtils.hpp"
#include "src/Accelerators/PIM/Common/IR/StaticIntSequence.hpp"
#include "Scheduling/MergeSchedulingAnalysis.hpp"
#include "src/Accelerators/PIM/Dialect/Spatial/SpatialOps.hpp"
#include "src/Accelerators/PIM/Common/IR/ConstantUtils.hpp"
@@ -38,6 +38,16 @@ struct ProducerValueKey {
}
};
struct ScheduledPublication {
ProducerValueKey producer;
unsigned scheduledResultIndex = 0;
bool operator==(const ScheduledPublication &other) const {
return producer == other.producer
&& scheduledResultIndex == other.scheduledResultIndex;
}
};
struct GraphComputeBlockKey {
Operation *op = nullptr;
uint32_t laneStart = 0;
@@ -56,6 +66,7 @@ struct PeftClassPlan {
SmallVector<Value> weights;
SmallVector<Value> inputs;
SmallVector<Type> resultTypes;
SmallVector<ScheduledPublication> publications;
};
struct ComputeStepTuple {
@@ -65,10 +76,38 @@ struct ComputeStepTuple {
struct ScheduledStepPlan {
ComputeStepTuple stepTuple;
size_t stepIndex = 0;
size_t resultOffset = 0;
size_t resultCount = 0;
};
struct ScheduledInstanceRun {
Operation *sourceOp = nullptr;
size_t firstStep = 0;
size_t stepCount = 0;
SmallVector<ComputeInstance> instances;
StaticIntSequence laneStarts;
StaticIntSequence laneCounts;
bool scalarSource = false;
bool resultfulBatchSource = false;
};
struct MaterializedProducerRef {
Value payload;
int64_t payloadLaneStart = 0;
int64_t payloadLaneCount = 1;
};
struct MaterializedStepValue {
ComputeInstance instance;
unsigned resultIndex = 0;
Value payload;
int64_t graphId = -1;
int64_t laneStart = 0;
int64_t laneCount = 1;
int64_t payloadLaneStart = 0;
int64_t payloadLaneCount = 1;
Value published;
};
struct SourceLaneAffineMapping {
uint32_t baseLaneStart = 0;
uint32_t laneCount = 1;
@@ -86,14 +125,12 @@ struct ScheduledMaterializationRecord {
size_t canonicalPeftClassId = 0;
SmallVector<size_t> cpus;
SmallVector<ScheduledStepPlan> stepPlans;
SmallVector<ScheduledInstanceRun, 0> runs;
SmallVector<GraphComputeBlockKey> computeKeys;
SmallVector<Block *> blocks;
};
struct ScheduledComputePrintContext {
mlir::AsmState asmState;
explicit ScheduledComputePrintContext(ModuleOp module, const OpPrintingFlags &flags = OpPrintingFlags())
: asmState(module.getOperation(), flags) {}
SmallVector<Operation *> stepAnchors;
SmallVector<MaterializedStepValue> stepValues;
SmallVector<ScheduledPublication> publications;
};
inline GraphComputeBlockKey getGraphComputeBlockKey(const ComputeInstance &instance) {
@@ -133,22 +170,6 @@ inline size_t getScheduledCpuForComputeInstance(const ComputeInstance &instance,
return it->second;
}
inline std::string getInstanceName(const ComputeInstance &instance) {
return llvm::formatv("{0}[lanes={1}:{2}]",
instance.op->getName().getStringRef(),
instance.laneStart,
instance.laneStart + instance.laneCount)
.str();
}
inline SmallVector<int64_t> toI64Array(ArrayRef<size_t> values) {
SmallVector<int64_t> converted;
converted.reserve(values.size());
for (size_t value : values)
converted.push_back(static_cast<int64_t>(value));
return converted;
}
inline SmallVector<int32_t> toI32Array(ArrayRef<size_t> values) {
SmallVector<int32_t> converted;
converted.reserve(values.size());
@@ -209,16 +230,49 @@ inline size_t getComputeInstanceResultValueCount(const ComputeInstance &instance
inline SmallVector<ScheduledStepPlan> buildScheduledStepPlans(const PeftClassPlan &peftClassPlan) {
SmallVector<ScheduledStepPlan> stepPlans;
size_t resultOffset = 0;
for (auto [stepIndex, stepTuple] : llvm::enumerate(buildComputeStepTuples(peftClassPlan))) {
assert(!stepTuple.instances.empty() && "expected non-empty step tuple");
size_t resultCount = getComputeInstanceResultValueCount(stepTuple.instances.front());
stepPlans.push_back(ScheduledStepPlan {stepTuple, stepIndex, resultOffset, resultCount});
resultOffset += resultCount;
stepPlans.push_back(ScheduledStepPlan {stepTuple, stepIndex, resultCount});
}
return stepPlans;
}
inline SmallVector<ScheduledInstanceRun, 0> buildScheduledInstanceRuns(
ArrayRef<ComputeInstance> instances) {
SmallVector<ScheduledInstanceRun, 0> runs;
for (auto [step, instance] : llvm::enumerate(instances)) {
auto batch = dyn_cast<SpatComputeBatch>(instance.op);
bool resultfulBatch = batch && batch.getNumResults() != 0;
bool append = resultfulBatch && !runs.empty()
&& runs.back().resultfulBatchSource
&& runs.back().sourceOp == instance.op
&& runs.back().instances.back().laneStart
+ runs.back().instances.back().laneCount == instance.laneStart;
if (!append) {
ScheduledInstanceRun run;
run.sourceOp = instance.op;
run.firstStep = step;
run.scalarSource = isa<SpatCompute>(instance.op);
run.resultfulBatchSource = resultfulBatch;
runs.push_back(std::move(run));
}
ScheduledInstanceRun &run = runs.back();
run.instances.push_back(instance);
run.stepCount = run.instances.size();
}
for (ScheduledInstanceRun &run : runs) {
SmallVector<int64_t> starts, counts;
for (const ComputeInstance &instance : run.instances) {
starts.push_back(instance.laneStart);
counts.push_back(instance.laneCount);
}
run.laneStarts = StaticIntSequence::fromValues(starts);
run.laneCounts = StaticIntSequence::fromValues(counts);
}
return runs;
}
inline bool valueTransitivelyDependsOn(Value value, Value dependency) {
SmallVector<Value> worklist {value};
DenseSet<Value> visited;
@@ -255,10 +309,6 @@ inline std::optional<SourceLaneAffineMapping> getSourceLaneAffineMapping(const C
return SourceLaneAffineMapping {reference.laneStart, reference.laneCount};
}
inline bool usesAffineSourceLaneMapping(const ComputeStepTuple &stepTuple) {
return getSourceLaneAffineMapping(stepTuple).has_value();
}
inline SmallVector<uint32_t> collectSourceLaneStarts(const ComputeStepTuple &stepTuple) {
SmallVector<uint32_t> sourceLaneStarts;
sourceLaneStarts.reserve(stepTuple.instances.size());
@@ -0,0 +1,233 @@
#include "ScheduledComputeMaterialization.hpp"
#include "DeferredCommunicationPlanning.hpp"
#include "mlir/Dialect/Tensor/IR/Tensor.h"
#include "src/Accelerators/PIM/Common/IR/TensorSliceUtils.hpp"
#include "src/Accelerators/PIM/Common/PimCommon.hpp"
namespace onnx_mlir::spatial {
using namespace mlir;
namespace {
void appendUnique(SmallVectorImpl<Value> &values, Value value) {
if (!llvm::is_contained(values, value))
values.push_back(value);
}
bool requiresScheduledPublication(Value value, DenseSet<Value> &visited) {
if (!visited.insert(value).second)
return false;
return llvm::any_of(value.getUses(), [&](OpOperand &use) {
Operation *user = use.getOwner();
if (isa<SpatGraphCompute, SpatGraphComputeBatch,
SpatDeferredCommunicationOp>(user))
return false;
auto blueprint = dyn_cast<SpatBlueprintOp>(user);
return !blueprint || blueprint.getMode() != "fragment_assembly"
|| requiresScheduledPublication(blueprint.getOutput(), visited);
});
}
bool requiresScheduledPublication(Value value) {
DenseSet<Value> visited;
return requiresScheduledPublication(value, visited);
}
} // namespace
FailureOr<BatchFragmentSpec>
getBatchFragmentSpec(SpatComputeBatch batch,
unsigned resultIndex,
uint32_t fragmentLaneCount) {
auto inParallel = dyn_cast<SpatInParallelOp>(batch.getBody().front().getTerminator());
if (!inParallel)
return batch.emitOpError("scheduled materialization only supports resultful spat.graph_compute_batch");
auto outputArg = batch.getOutputArgument(resultIndex);
if (!outputArg)
return batch.emitOpError("scheduled materialization could not locate batch output block argument");
for (Operation &op : inParallel.getRegion().front()) {
auto insert = dyn_cast<tensor::ParallelInsertSliceOp>(&op);
if (!insert)
return batch.emitOpError("scheduled materialization only supports regular leading-lane graph_compute_batch fragments");
if (insert.getDest() != *outputArg)
continue;
RankedTensorType destType = insert.getDestType();
RankedTensorType sourceType = insert.getSourceType();
if (!destType || !sourceType || !destType.hasStaticShape() || !sourceType.hasStaticShape())
return batch.emitOpError("scheduled materialization only supports regular leading-lane graph_compute_batch fragments");
if (destType.getRank() != sourceType.getRank() + 1 || destType.getDimSize(0) != batch.getLaneCount()
|| destType.getElementType() != sourceType.getElementType())
return batch.emitOpError("graph_compute_batch result must be a leading physical-slot dimension followed by its fragment");
if (!llvm::equal(destType.getShape().drop_front(), sourceType.getShape()))
return batch.emitOpError("graph_compute_batch result trailing shape must match its published fragment");
if (!insert.hasUnitStride())
return batch.emitOpError("scheduled materialization only supports regular leading-lane graph_compute_batch fragments");
auto offsets = insert.getMixedOffsets();
auto sizes = insert.getMixedSizes();
auto strides = insert.getMixedStrides();
if (offsets.size() != static_cast<size_t>(destType.getRank()) || sizes.size() != static_cast<size_t>(destType.getRank())
|| strides.size() != static_cast<size_t>(destType.getRank()))
return batch.emitOpError("scheduled materialization only supports regular leading-lane graph_compute_batch fragments");
if (!isa<Value>(offsets.front()) || !valueTransitivelyDependsOn(cast<Value>(offsets.front()), *batch.getLaneArgument()))
return batch.emitOpError("graph_compute_batch publication must select its physical slot in dimension zero");
for (unsigned dim = 1; dim < offsets.size(); ++dim) {
auto offset = dyn_cast<Attribute>(offsets[dim]);
auto integer = dyn_cast_or_null<IntegerAttr>(offset);
if (!integer || integer.getInt() != 0)
return batch.emitOpError("graph_compute_batch publication must have zero trailing offsets");
}
auto staticIndex = [](OpFoldResult value) -> std::optional<int64_t> {
auto attr = dyn_cast<Attribute>(value);
auto integer = dyn_cast_or_null<IntegerAttr>(attr);
return integer ? std::optional<int64_t>(integer.getInt()) : std::nullopt;
};
if (staticIndex(sizes.front()) != 1)
return batch.emitOpError("graph_compute_batch publication sizes must be [1] plus the fragment shape");
for (auto [size, dim] : llvm::zip_equal(ArrayRef<OpFoldResult>(sizes).drop_front(), sourceType.getShape()))
if (staticIndex(size) != dim)
return batch.emitOpError("graph_compute_batch publication sizes must be [1] plus the fragment shape");
return BatchFragmentSpec {spatial::getGraphBatchPhysicalResultType(fragmentLaneCount, sourceType), sourceType};
}
return batch.emitOpError("scheduled materialization only supports regular leading-lane graph_compute_batch fragments");
}
LogicalResult verifyPeftClassPlan(Operation *diagnosticAnchor,
const PeftClassPlan &peftClassPlan,
const MergeScheduleResult &schedule) {
if (peftClassPlan.cpus.empty())
return diagnosticAnchor->emitOpError("PEFT materialization class has no CPUs");
SmallVector<const SmallVector<ComputeInstance> *> schedules;
for (size_t cpu : peftClassPlan.cpus) {
auto it = peftClassPlan.instancesByCpu.find(cpu);
if (it == peftClassPlan.instancesByCpu.end())
return diagnosticAnchor->emitOpError("PEFT materialization class is missing a per-CPU schedule");
schedules.push_back(&it->second);
for (const ComputeInstance &instance : it->second)
if (!schedule.computeToCpuSlotMap.count(instance))
return diagnosticAnchor->emitOpError("PEFT materialization class references a compute instance without a scheduler position");
}
if (peftClassPlan.cpus.size() == 1)
return success();
auto emitNonIso = [&](size_t stepPosition) -> LogicalResult {
std::string cpus;
llvm::raw_string_ostream os(cpus);
llvm::interleaveComma(peftClassPlan.cpus, os, [&](size_t cpu) { os << cpu; });
diagnosticAnchor->emitOpError("PEFT equivalence class has non-isomorphic per-CPU schedules")
<< " class " << peftClassPlan.canonicalClassId << " cpus [" << os.str() << "] step " << stepPosition;
return failure();
};
size_t tupleCount = schedules.front()->size();
for (const SmallVector<ComputeInstance> *cpuSchedule : schedules)
if (cpuSchedule->size() != tupleCount)
return emitNonIso(0);
for (size_t stepPosition = 0; stepPosition < tupleCount; ++stepPosition) {
const ComputeInstance &reference = (*schedules.front())[stepPosition];
bool refIsScalar = isa<SpatCompute>(reference.op);
for (size_t cpuIndex = 1; cpuIndex < schedules.size(); ++cpuIndex) {
const ComputeInstance &instance = (*schedules[cpuIndex])[stepPosition];
if (instance.op != reference.op || instance.laneCount != reference.laneCount)
return emitNonIso(stepPosition);
if (isa<SpatCompute>(instance.op) != refIsScalar)
return emitNonIso(stepPosition);
}
}
return success();
}
LogicalResult collectPeftClassOperandsAndResults(
PeftClassPlan &peftClassPlan, const MergeScheduleResult &schedule) {
peftClassPlan.weights.clear();
peftClassPlan.inputs.clear();
peftClassPlan.resultTypes.clear();
peftClassPlan.publications.clear();
if (peftClassPlan.cpus.size() == 1) {
size_t cpu = peftClassPlan.cpus.front();
for (const ComputeInstance &instance : peftClassPlan.instancesByCpu.lookup(cpu)) {
if (auto compute = dyn_cast<SpatCompute>(instance.op)) {
for (auto [resultIndex, result] : llvm::enumerate(compute.getResults()))
if (requiresScheduledPublication(result)) {
peftClassPlan.publications.push_back(
{{instance, resultIndex}, static_cast<unsigned>(peftClassPlan.resultTypes.size())});
peftClassPlan.resultTypes.push_back(result.getType());
}
} else {
auto batch = cast<SpatComputeBatch>(instance.op);
for (unsigned resultIndex = 0; resultIndex < batch.getNumResults(); ++resultIndex) {
if (!requiresScheduledPublication(batch.getResult(resultIndex)))
continue;
auto spec = getBatchFragmentSpec(batch, resultIndex, instance.laneCount);
if (failed(spec))
return failure();
peftClassPlan.publications.push_back(
{{instance, resultIndex}, static_cast<unsigned>(peftClassPlan.resultTypes.size())});
peftClassPlan.resultTypes.push_back(spec->resultType);
}
}
for (Value weight : getComputeInstanceWeights(instance))
appendUnique(peftClassPlan.weights, weight);
for (Value input : getComputeInstanceInputs(instance))
if (!getProducerValueRef(input, &instance) && !isDeferredFragmentAssemblyInput(input, instance))
appendUnique(peftClassPlan.inputs, input);
}
return success();
}
for (const ScheduledStepPlan &stepPlan : buildScheduledStepPlans(peftClassPlan)) {
const ComputeStepTuple &stepTuple = stepPlan.stepTuple;
const ComputeInstance &representative = stepTuple.instances.front();
if (auto compute = dyn_cast<SpatCompute>(representative.op)) {
for (auto [resultIndex, result] : llvm::enumerate(compute.getResults())) {
if (!requiresScheduledPublication(result))
continue;
auto tensorType = dyn_cast<RankedTensorType>(result.getType());
if (!tensorType || !tensorType.hasStaticShape())
return compute.emitOpError("scheduled materialization only supports static ranked tensor scalar results");
SmallVector<int64_t> shape {static_cast<int64_t>(peftClassPlan.cpus.size())};
llvm::append_range(shape, tensorType.getShape());
unsigned scheduledResultIndex = peftClassPlan.resultTypes.size();
peftClassPlan.resultTypes.push_back(RankedTensorType::get(shape, tensorType.getElementType()));
for (const ComputeInstance &instance : stepTuple.instances)
peftClassPlan.publications.push_back({{instance, resultIndex}, scheduledResultIndex});
}
} else {
auto batch = cast<SpatComputeBatch>(representative.op);
uint32_t totalLanes = static_cast<uint32_t>(peftClassPlan.cpus.size()) * representative.laneCount;
for (unsigned resultIndex = 0; resultIndex < batch.getNumResults(); ++resultIndex) {
if (!requiresScheduledPublication(batch.getResult(resultIndex)))
continue;
auto spec = getBatchFragmentSpec(batch, resultIndex, totalLanes);
if (failed(spec))
return failure();
unsigned scheduledResultIndex = peftClassPlan.resultTypes.size();
peftClassPlan.resultTypes.push_back(spec->resultType);
for (const ComputeInstance &instance : stepTuple.instances)
peftClassPlan.publications.push_back({{instance, resultIndex}, scheduledResultIndex});
}
}
for (const ComputeInstance &instance : stepTuple.instances) {
for (Value weight : getComputeInstanceWeights(instance))
appendUnique(peftClassPlan.weights, weight);
for (Value input : getComputeInstanceInputs(instance))
if (!getProducerValueRef(input, &instance) && !isDeferredFragmentAssemblyInput(input, instance))
appendUnique(peftClassPlan.inputs, input);
}
}
return success();
}
} // namespace onnx_mlir::spatial
@@ -1,304 +1,161 @@
#include "ScheduledComputeReport.hpp"
#include "llvm/Support/raw_os_ostream.h"
#include "mlir/IR/AsmState.h"
#include <fstream>
#include "src/Accelerators/PIM/Common/Support/DebugDump.hpp"
#include "src/Accelerators/PIM/Common/Support/Diagnostics.hpp"
namespace onnx_mlir {
namespace spatial {
using namespace mlir;
namespace {
static std::string formatValueLabel(Value value, AsmState &asmState) {
std::string storage;
llvm::raw_string_ostream os(storage);
value.printAsOperand(os, asmState);
return storage;
}
static std::string formatOperationLabel(Operation *op, AsmState &asmState) {
if (op->getNumResults() == 0)
return op->getName().getStringRef().str();
std::string storage;
llvm::raw_string_ostream os(storage);
llvm::interleaveComma(op->getResults(), os, [&](Value result) { os << formatValueLabel(result, asmState); });
return os.str();
}
static std::string formatGraphComputeBlockKey(const GraphComputeBlockKey &key, AsmState &asmState) {
return llvm::formatv("{0} {1}", formatOperationLabel(key.op, asmState), key.op->getName().getStringRef()).str();
}
static std::string formatComputeInstanceForReport(const ComputeInstance &instance, AsmState &asmState) {
std::string opLabel = formatGraphComputeBlockKey(getGraphComputeBlockKey(instance), asmState);
if (isa<SpatCompute>(instance.op))
return opLabel;
return llvm::formatv("{0} sourceLanes [{1}:{2}]",
opLabel,
instance.laneStart,
instance.laneStart + instance.laneCount)
.str();
op->getResult(0).printAsOperand(os, asmState);
if (op->getNumResults() == 1)
return storage;
if (StringRef(storage).ends_with("#0")) storage.resize(storage.size() - 2);
return llvm::formatv("{0}:{1}", storage, op->getNumResults()).str();
}
template <typename T>
static void printIndexedList(raw_ostream &os, ArrayRef<T> values) {
os << "[";
llvm::interleaveComma(llvm::enumerate(values), os, [&](auto indexedValue) {
os << indexedValue.index() << ":" << indexedValue.value();
});
for (size_t begin = 0; begin < values.size();) {
size_t end = begin + 1;
if (end < values.size()) {
int64_t step = static_cast<int64_t>(values[end]) - static_cast<int64_t>(values[begin]);
while (end + 1 < values.size()
&& static_cast<int64_t>(values[end + 1]) - static_cast<int64_t>(values[end]) == step)
++end;
}
bool compressed = end - begin + 1 >= 3;
if (begin) os << ", ";
os << begin << ":" << values[begin];
if (compressed) os << ".." << end << ":" << values[end];
begin = (compressed ? end : begin) + 1;
}
os << "]";
}
struct PeftMaterializationReportSummary {
size_t scalarGraphCompute = 0;
size_t graphComputeBatchOps = 0;
size_t scalarGraphComputeInstances = 0;
size_t graphComputeBatchInstances = 0;
size_t peftClasses = 0;
size_t singleCpuClasses = 0;
size_t multiCpuClasses = 0;
size_t scheduledCompute = 0;
size_t scheduledComputeBatch = 0;
size_t deferredCommunication = 0;
size_t deferredCommunicationMultiSourcePayloads = 0;
};
static PeftMaterializationReportSummary buildPeftMaterializationReportSummary(
func::FuncOp funcOp,
const MergeScheduleResult &schedule,
const llvm::MapVector<size_t, PeftClassPlan> &peftClassPlans,
ArrayRef<ScheduledMaterializationRecord> materializedSchedules) {
PeftMaterializationReportSummary summary;
for (Operation &op : funcOp.getOps()) {
if (isa<SpatGraphCompute>(op))
summary.scalarGraphCompute++;
else if (isa<SpatGraphComputeBatch>(op)) {
summary.graphComputeBatchOps++;
static void printSingleCpuSteps(raw_ostream &os, ArrayRef<ComputeInstance> instances,
const ScheduledMaterializationRecord &record,
AsmState &asmState) {
for (size_t begin = 0; begin < instances.size();) {
const ComputeInstance &first = instances[begin];
size_t resultCount = getComputeInstanceResultValueCount(first);
size_t end = begin + 1;
uint32_t laneEnd = first.laneStart + first.laneCount;
if (!isa<SpatCompute>(first.op)) {
while (end < instances.size()) {
const ComputeInstance &next = instances[end];
if (isa<SpatCompute>(next.op) || next.op != first.op || next.laneStart != laneEnd
|| next.laneCount != first.laneCount || getComputeInstanceResultValueCount(next) != resultCount)
break;
laneEnd += next.laneCount;
++end;
}
}
size_t runResults = (end - begin) * resultCount;
size_t publications = llvm::count_if(record.publications, [&](const ScheduledPublication &publication) {
return llvm::is_contained(instances.slice(begin, end - begin), publication.producer.instance);
});
os << " " << (end - begin == 1 ? "step " : "steps [") << begin;
if (end - begin != 1) os << ":" << end << "]";
os << " payloads=" << runResults << " publications=" << publications
<< " " << formatOperationLabel(first.op, asmState);
if (!isa<SpatCompute>(first.op)) os << " lanes [" << first.laneStart << ":" << laneEnd << "]";
if (end - begin != 1) os << " each(results=" << resultCount << ", lanes=" << first.laneCount << ")";
os << "\n";
begin = end;
}
for (const ComputeInstance &instance : schedule.dominanceOrderCompute)
(isa<SpatCompute>(instance.op) ? summary.scalarGraphComputeInstances : summary.graphComputeBatchInstances)++;
summary.peftClasses = peftClassPlans.size();
for (const auto &entry : peftClassPlans)
(entry.second.cpus.size() == 1 ? summary.singleCpuClasses : summary.multiCpuClasses)++;
for (const ScheduledMaterializationRecord &record : materializedSchedules) {
if (isa<SpatScheduledCompute>(record.scheduledOp))
summary.scheduledCompute++;
else
summary.scheduledComputeBatch++;
}
funcOp.walk([&](SpatDeferredCommunicationOp transfer) {
summary.deferredCommunication++;
if (transfer.getSources().size() > 1)
summary.deferredCommunicationMultiSourcePayloads++;
});
return summary;
}
} // namespace
LogicalResult verifyPeftMaterializationReportSummary(func::FuncOp funcOp,
const MergeScheduleResult &schedule,
const llvm::MapVector<size_t, PeftClassPlan> &peftClassPlans,
ArrayRef<ScheduledMaterializationRecord> materializedSchedules) {
PeftMaterializationReportSummary summary =
buildPeftMaterializationReportSummary(funcOp, schedule, peftClassPlans, materializedSchedules);
pim::CappedDiagnosticReporter diagnostics;
if (summary.peftClasses != peftClassPlans.size())
diagnostics.report(funcOp.getOperation(), [&](Operation *illegalOp) {
illegalOp->emitOpError() << "phase-check report PEFT total " << summary.peftClasses
<< " does not match classes.size() " << peftClassPlans.size();
});
if (summary.scalarGraphComputeInstances + summary.graphComputeBatchInstances != schedule.dominanceOrderCompute.size())
diagnostics.report(funcOp.getOperation(), [&](Operation *illegalOp) {
illegalOp->emitOpError() << "phase-check report total compute instances "
<< (summary.scalarGraphComputeInstances + summary.graphComputeBatchInstances)
<< " does not match schedule size " << schedule.dominanceOrderCompute.size();
});
if (summary.scheduledCompute + summary.scheduledComputeBatch != materializedSchedules.size())
diagnostics.report(funcOp.getOperation(), [&](Operation *illegalOp) {
illegalOp->emitOpError() << "phase-check report scheduled total "
<< (summary.scheduledCompute + summary.scheduledComputeBatch)
<< " does not match materialized scheduled ops " << materializedSchedules.size();
});
diagnostics.emitSuppressedSummary(funcOp, "scheduled Spatial report verification failed");
return success(!diagnostics.hasFailure());
}
namespace {
static std::string formatStepResultRange(size_t resultOffset, size_t resultCount) {
if (resultCount == 1)
return llvm::formatv("result[{0}]", resultOffset).str();
return llvm::formatv("result[{0}:{1}]", resultOffset, resultOffset + resultCount).str();
}
static void printMultiSourceDeferredInputs(raw_ostream &os, Block &block) {
unsigned deferredInputIndex = 0;
for (Operation &op : block.getOperations()) {
auto transfer = dyn_cast<SpatDeferredCommunicationOp>(&op);
if (!transfer)
continue;
auto multiSourcePayload = transfer->getAttrOfType<BoolAttr>("multi_source_payload");
auto sourceOperandForScheduledLane =
transfer->getAttrOfType<DenseI64ArrayAttr>("source_operand_for_scheduled_lane");
if (multiSourcePayload && multiSourcePayload.getValue() && sourceOperandForScheduledLane) {
SmallVector<size_t> sourceOperandIndexes;
for (int64_t sourceOperandIndex : sourceOperandForScheduledLane.asArrayRef())
sourceOperandIndexes.push_back(static_cast<size_t>(sourceOperandIndex));
os << " deferred input " << deferredInputIndex << ": multi-source uniqueSources="
<< transfer.getSources().size() << " sourceOperandForScheduledLane=";
printIndexedList(os, ArrayRef<size_t>(sourceOperandIndexes));
os << "\n";
}
deferredInputIndex++;
}
}
static void dumpPeftMaterializationReport(ModuleOp moduleOp,
func::FuncOp funcOp,
const MergeScheduleResult &schedule,
const llvm::MapVector<size_t, PeftClassPlan> &peftClassPlans,
ArrayRef<ScheduledMaterializationRecord> materializedSchedules,
ScheduledComputePrintContext &printContext) {
std::fstream file = openDialectDumpFileWithExtension("spatial2_scheduled_no_comm", "/reports", "txt");
if (!file.is_open())
return;
void dumpScheduledComputeReport(ModuleOp moduleOp, func::FuncOp funcOp, const MergeScheduleResult &schedule,
const llvm::MapVector<size_t, PeftClassPlan> &peftClassPlans,
ArrayRef<ScheduledMaterializationRecord> records) {
std::fstream file = openDialectDumpFileWithExtension("spatial3_scheduled_no_comm", "/reports", "txt");
if (!file.is_open()) return;
llvm::raw_os_ostream os(file);
AsmState &asmState = printContext.asmState;
PeftMaterializationReportSummary summary =
buildPeftMaterializationReportSummary(funcOp, schedule, peftClassPlans, materializedSchedules);
os << "Summary\n";
os << "=======\n";
os << "Graph computes:\n";
os << " total: " << (summary.scalarGraphCompute + summary.graphComputeBatchOps) << "\n";
os << " scalar graph_compute: " << summary.scalarGraphCompute << "\n";
os << " graph_compute_batch: " << summary.graphComputeBatchOps << "\n";
os << "Compute instances:\n";
os << " total: " << (summary.scalarGraphComputeInstances + summary.graphComputeBatchInstances) << "\n";
os << " scalar graph_compute instances: " << summary.scalarGraphComputeInstances << "\n";
os << " graph_compute_batch instances: " << summary.graphComputeBatchInstances << "\n";
os << "PEFT classes:\n";
os << " total: " << summary.peftClasses << "\n";
os << " single-cpu: " << summary.singleCpuClasses << "\n";
os << " multi-cpu: " << summary.multiCpuClasses << "\n";
os << "Scheduled ops:\n";
os << " total: " << (summary.scheduledCompute + summary.scheduledComputeBatch) << "\n";
os << " scheduled_compute: " << summary.scheduledCompute << "\n";
os << " scheduled_compute_batch: " << summary.scheduledComputeBatch << "\n";
os << "Deferred communications:\n";
os << " total: " << summary.deferredCommunication << "\n";
os << " multi-source payloads: " << summary.deferredCommunicationMultiSourcePayloads << "\n\n";
os << "PEFT Classes\n";
os << "============\n";
for (const auto &entry : peftClassPlans) {
const PeftClassPlan &peftClassPlan = entry.second;
os << "C" << peftClassPlan.canonicalClassId << " "
<< (peftClassPlan.cpus.size() == 1 ? "single-cpu" : "multi-cpu") << " PEFT class\n";
if (peftClassPlan.cpus.size() == 1) {
size_t cpu = peftClassPlan.cpus.front();
os << " cpu: " << cpu << "\n";
os << " steps: " << peftClassPlan.instancesByCpu.lookup(cpu).size() << "\n";
for (auto [stepIndex, instance] : llvm::enumerate(peftClassPlan.instancesByCpu.lookup(cpu)))
os << " step " << stepIndex << ": " << formatComputeInstanceForReport(instance, asmState) << "\n";
} else {
os << " scheduled lanes: " << peftClassPlan.cpus.size() << "\n";
os << " steps: " << peftClassPlan.instancesByCpu.lookup(peftClassPlan.cpus.front()).size() << "\n";
os << " cpus by scheduled lane:\n";
os << " ";
printIndexedList(os, ArrayRef<size_t>(peftClassPlan.cpus));
os << "\n";
os << " step sources:\n";
for (auto [stepIndex, stepTuple] : llvm::enumerate(buildComputeStepTuples(peftClassPlan)))
os << " step " << stepIndex << ": "
<< formatGraphComputeBlockKey(getGraphComputeBlockKey(stepTuple.instances.front()), asmState) << "\n";
}
os << "\n";
}
os << "Materialized Scheduled Ops\n";
os << "=========================\n";
for (const ScheduledMaterializationRecord &record : materializedSchedules) {
os << "C" << record.canonicalPeftClassId << " -> " << formatOperationLabel(record.scheduledOp, asmState) << " "
<< record.scheduledOp->getName().getStringRef() << "\n";
os << " kind: "
<< (isa<SpatScheduledCompute>(record.scheduledOp) ? "single-cpu scheduled_compute"
: "multi-cpu scheduled_compute_batch")
<< "\n";
if (isa<SpatScheduledCompute>(record.scheduledOp))
os << " cpu: " << record.cpus.front() << "\n";
else
os << " scheduled lanes: " << record.cpus.size() << "\n";
os << " results: " << record.scheduledOp->getNumResults() << "\n";
os << " steps: "
<< (isa<SpatScheduledCompute>(record.scheduledOp)
? peftClassPlans.lookup(record.canonicalPeftClassId).instancesByCpu.lookup(record.cpus.front()).size()
: record.stepPlans.size())
<< "\n";
if (isa<SpatScheduledComputeBatch>(record.scheduledOp)) {
os << " cpus by scheduled lane:\n";
os << " ";
printIndexedList(os, ArrayRef<size_t>(record.cpus));
os << "\n\n";
}
if (isa<SpatScheduledCompute>(record.scheduledOp)) {
const PeftClassPlan &peftClassPlan = peftClassPlans.lookup(record.canonicalPeftClassId);
size_t cpu = peftClassPlan.cpus.front();
size_t resultOffset = 0;
for (auto [stepIndex, instance] : llvm::enumerate(peftClassPlan.instancesByCpu.lookup(cpu))) {
size_t resultCount = getComputeInstanceResultValueCount(instance);
os << " step " << stepIndex << " " << formatStepResultRange(resultOffset, resultCount) << " "
<< formatComputeInstanceForReport(instance, asmState) << "\n";
resultOffset += resultCount;
}
} else {
auto scheduledBatch = cast<SpatScheduledComputeBatch>(record.scheduledOp);
for (auto [stepIndex, stepPlan] : llvm::enumerate(record.stepPlans)) {
const ComputeInstance &representative = stepPlan.stepTuple.instances.front();
SmallVector<uint32_t> sourceLaneStarts = collectSourceLaneStarts(stepPlan.stepTuple);
os << " step " << stepIndex << " " << formatStepResultRange(stepPlan.resultOffset, stepPlan.resultCount) << " "
<< formatGraphComputeBlockKey(getGraphComputeBlockKey(representative), asmState)
<< " lanesPerScheduledLane=" << representative.laneCount << " sourceLaneSelector="
<< (usesAffineSourceLaneMapping(stepPlan.stepTuple) ? "affine" : "table") << "\n";
os << " source lanes by scheduled lane:\n";
os << " ";
printIndexedList(os, ArrayRef<uint32_t>(sourceLaneStarts));
os << "\n";
Block &stepBlock = *std::next(scheduledBatch.getBody().begin(), stepIndex);
printMultiSourceDeferredInputs(os, stepBlock);
}
}
os << "\n";
}
}
} // namespace
void dumpScheduledComputeReportAndModule(ModuleOp moduleOp,
func::FuncOp funcOp,
const MergeScheduleResult &schedule,
const llvm::MapVector<size_t, PeftClassPlan> &peftClassPlans,
ArrayRef<ScheduledMaterializationRecord> materializedSchedules) {
OpPrintingFlags flags;
flags.elideLargeElementsAttrs().enableDebugInfo(false, false).assumeVerified();
ScheduledComputePrintContext printContext(moduleOp, flags);
dumpPeftMaterializationReport(moduleOp, funcOp, schedule, peftClassPlans, materializedSchedules, printContext);
AsmState asmState(moduleOp.getOperation(), flags);
std::fstream file = openDialectDumpFileWithExtension("spatial2_scheduled_no_comm", "/dialects", "mlir");
if (!file.is_open())
return;
llvm::raw_os_ostream os(file);
moduleOp.getOperation()->print(os, printContext.asmState);
os.flush();
size_t scalarGraph = 0, batchGraph = 0;
for (Operation &op : funcOp.getOps())
isa<SpatGraphCompute>(op) ? ++scalarGraph : batchGraph += isa<SpatGraphComputeBatch>(op);
size_t deferred = 0;
funcOp.walk([&](SpatDeferredCommunicationOp) { ++deferred; });
size_t scalarizedBatchInstances = 0, materializedRuns = 0;
size_t instancesCompacted = 0, largestRun = 0;
for (const ScheduledMaterializationRecord &record : records)
for (const ScheduledInstanceRun &run : record.runs) {
if (!run.resultfulBatchSource)
continue;
scalarizedBatchInstances += run.instances.size();
if (run.instances.size() == 1)
continue;
++materializedRuns;
instancesCompacted += run.instances.size() - 1;
largestRun = std::max(largestRun, run.instances.size());
}
os << "Summary\n"
<< " graph computes: " << scalarGraph + batchGraph << " (scalar=" << scalarGraph << ", batch=" << batchGraph << ")\n"
<< " compute instances: " << schedule.dominanceOrderCompute.size() << "\n"
<< " PEFT classes: " << peftClassPlans.size() << "\n"
<< " scheduled ops: " << records.size() << "\n"
<< " deferred communications: " << deferred << "\n"
<< " original scalarized batch instances: " << scalarizedBatchInstances << "\n"
<< " materialized homogeneous runs: " << materializedRuns << "\n"
<< " largest run: " << largestRun << "\n"
<< " instances compacted: " << instancesCompacted << "\n"
<< " compatible runs rejected: 0\n\n"
<< "Materialized scheduled ops\n";
for (const ScheduledMaterializationRecord &record : records) {
bool batch = isa<SpatScheduledComputeBatch>(record.scheduledOp);
const PeftClassPlan &plan = peftClassPlans.lookup(record.canonicalPeftClassId);
size_t steps = batch ? record.stepPlans.size() : plan.instancesByCpu.lookup(record.cpus.front()).size();
os << "C" << record.canonicalPeftClassId << " -> " << formatOperationLabel(record.scheduledOp, asmState) << " "
<< record.scheduledOp->getName() << " " << (batch ? "cpus=" : "cpu=");
if (batch) printIndexedList(os, ArrayRef<size_t>(record.cpus));
else os << record.cpus.front();
os << " results=" << record.scheduledOp->getNumResults() << " steps=" << steps << "\n";
if (!batch) {
printSingleCpuSteps(os, plan.instancesByCpu.lookup(record.cpus.front()), record, asmState);
continue;
}
for (auto [stepIndex, step] : llvm::enumerate(record.stepPlans)) {
const ComputeInstance &source = step.stepTuple.instances.front();
llvm::SmallDenseSet<unsigned, 4> publishedResults;
for (const ScheduledPublication &publication : record.publications)
if (llvm::is_contained(step.stepTuple.instances, publication.producer.instance))
publishedResults.insert(publication.scheduledResultIndex);
os << " step " << stepIndex << " payloads=" << step.resultCount
<< " publications=" << publishedResults.size() << " "
<< formatOperationLabel(source.op, asmState) << " lanes=" << source.laneCount << " sources=";
printIndexedList(os, ArrayRef<uint32_t>(collectSourceLaneStarts(step.stepTuple)));
os << "\n";
unsigned inputIndex = 0;
Operation *end = stepIndex + 1 < record.stepAnchors.size()
? record.stepAnchors[stepIndex + 1]
: nullptr;
for (Operation *op = record.stepAnchors[stepIndex]; op && op != end; op = op->getNextNode()) {
auto transfer = dyn_cast<SpatDeferredCommunicationOp>(op);
if (!transfer) continue;
auto sources = transfer->getAttrOfType<DenseI64ArrayAttr>("source_operand_for_scheduled_lane");
auto multiSource = transfer->getAttrOfType<BoolAttr>("multi_source_payload");
if (multiSource && multiSource.getValue() && sources) {
os << " deferred" << inputIndex << " sources=" << transfer.getSources().size() << " laneSources=";
printIndexedList(os, sources.asArrayRef());
os << "\n";
}
++inputIndex;
}
}
}
}
} // namespace spatial
} // namespace onnx_mlir
@@ -5,17 +5,11 @@
namespace onnx_mlir {
namespace spatial {
LogicalResult verifyPeftMaterializationReportSummary(
func::FuncOp funcOp,
const MergeScheduleResult &schedule,
const llvm::MapVector<size_t, PeftClassPlan> &peftClassPlans,
ArrayRef<ScheduledMaterializationRecord> materializedSchedules);
void dumpScheduledComputeReportAndModule(ModuleOp moduleOp,
func::FuncOp funcOp,
const MergeScheduleResult &schedule,
const llvm::MapVector<size_t, PeftClassPlan> &peftClassPlans,
ArrayRef<ScheduledMaterializationRecord> materializedSchedules);
void dumpScheduledComputeReport(ModuleOp moduleOp,
func::FuncOp funcOp,
const MergeScheduleResult &schedule,
const llvm::MapVector<size_t, PeftClassPlan> &peftClassPlans,
ArrayRef<ScheduledMaterializationRecord> materializedSchedules);
} // namespace spatial
} // namespace onnx_mlir
@@ -84,11 +84,20 @@ LogicalResult verifyDeferredTransferPhase1Invariants(func::FuncOp funcOp) {
funcOp.walk([&](SpatDeferredCommunicationOp transfer) {
bool ownershipValid = true;
for (Value source : transfer.getSources()) {
auto result = dyn_cast<OpResult>(source);
if (!result || !isa<SpatGraphCompute, SpatGraphComputeBatch>(result.getOwner())) {
SmallVector<Value> originalSources;
if (auto selection = source.getDefiningOp<SpatDeferredSourceSelectOp>())
llvm::append_range(originalSources, selection.getSources());
else
originalSources.push_back(source);
for (Value originalSource : originalSources) {
auto result = dyn_cast<OpResult>(originalSource);
if (result
&& isa<SpatGraphCompute, SpatGraphComputeBatch>(
result.getOwner()))
continue;
ownershipValid = false;
diagnostics.report(transfer.getOperation(), [&](Operation *illegalOp) {
illegalOp->emitOpError("phase-check deferred communication source operand must be an original graph SSA result");
illegalOp->emitOpError("phase-check deferred communication source must resolve to original graph SSA results");
});
}
}
@@ -101,18 +110,23 @@ LogicalResult verifyDeferredTransferPhase1Invariants(func::FuncOp funcOp) {
}
if (!ownershipValid)
return;
if (failed(verifyDeferredProgramContract(transfer))) {
if (failed(analyzeDeferredProgramTemplate(transfer))) {
diagnostics.report(transfer.getOperation(), [&](Operation *) {});
return;
}
for (Value source : transfer.getSources()) {
auto result = dyn_cast<OpResult>(source);
auto batch = result
? dyn_cast<SpatGraphComputeBatch>(result.getOwner())
: SpatGraphComputeBatch();
if (batch && failed(getGraphBatchPublicationMap(
batch, result.getResultNumber(), publicationCache)))
diagnostics.report(transfer.getOperation(), [&](Operation *) {});
SmallVector<Value> originalSources;
if (auto selection = source.getDefiningOp<SpatDeferredSourceSelectOp>())
llvm::append_range(originalSources, selection.getSources());
else
originalSources.push_back(source);
for (Value originalSource : originalSources) {
auto result = cast<OpResult>(originalSource);
auto batch = dyn_cast<SpatGraphComputeBatch>(result.getOwner());
if (batch && failed(getGraphBatchPublicationMap(
batch, result.getResultNumber(), publicationCache)))
diagnostics.report(transfer.getOperation(), [&](Operation *) {});
}
}
});
@@ -120,34 +134,42 @@ LogicalResult verifyDeferredTransferPhase1Invariants(func::FuncOp funcOp) {
return success(!diagnostics.hasFailure());
}
LogicalResult verifyMultiCpuStepResultRouting(SpatScheduledComputeBatch scheduled,
ArrayRef<ScheduledStepPlan> stepPlans) {
static LogicalResult verifyStepAnchors(
const ScheduledMaterializationRecord &record) {
Region &body = record.scheduledOp->getRegion(0);
size_t materializedStepCount = record.runs.empty()
? record.stepPlans.size() : record.runs.size();
if (!body.hasOneBlock()
|| record.stepAnchors.size() != materializedStepCount)
return record.scheduledOp->emitOpError(
"scheduled compute requires one block and one anchor per step");
Block &block = body.front();
for (auto [index, anchor] : llvm::enumerate(record.stepAnchors)) {
if (!anchor || anchor->getBlock() != &block
|| (index && !record.stepAnchors[index - 1]->isBeforeInBlock(anchor)))
return record.scheduledOp->emitOpError()
<< "scheduled step " << index << " has an invalid anchor";
}
return success();
}
LogicalResult verifyMultiCpuStepResultRouting(
SpatScheduledComputeBatch scheduled) {
pim::CappedDiagnosticReporter diagnostics;
unsigned resultArgBase = getScheduledBatchResultArgBase(scheduled);
if (scheduled.getBody().getBlocks().size() != stepPlans.size()) {
diagnostics.report(scheduled.getOperation(), [&](Operation *illegalOp) {
illegalOp->emitOpError() << "scheduled batch step routing expected " << stepPlans.size()
<< " blocks but saw " << scheduled.getBody().getBlocks().size();
});
diagnostics.emitSuppressedSummary(scheduled.getOperation(),
"scheduled batch step routing verification failed");
if (!scheduled.getBody().hasOneBlock())
return failure();
}
Block &block = scheduled.getBody().front();
if (scheduled.getNumResults() == 0)
return success(isa<SpatYieldOp>(block.getTerminator()));
SmallVector<unsigned> globalResultWrites(scheduled.getNumResults(), 0);
size_t stepIndex = 0;
for (Block &block : scheduled.getBody().getBlocks()) {
const ScheduledStepPlan &stepPlan = stepPlans[stepIndex++];
SmallVector<bool> localWrites(stepPlan.resultCount, false);
auto inParallel = dyn_cast<SpatInParallelOp>(block.getTerminator());
if (!inParallel) {
diagnostics.report(scheduled.getOperation(), [&](Operation *illegalOp) {
illegalOp->emitOpError() << "scheduled batch step " << stepPlan.stepIndex
<< " is missing spat.in_parallel";
});
continue;
}
auto inParallel = dyn_cast<SpatInParallelOp>(block.getTerminator());
if (!inParallel)
diagnostics.report(scheduled.getOperation(), [&](Operation *illegalOp) {
illegalOp->emitOpError("scheduled batch is missing spat.in_parallel");
});
else
for (Operation &op : inParallel.getRegion().front()) {
auto insert = dyn_cast<tensor::ParallelInsertSliceOp>(&op);
if (!insert)
@@ -155,41 +177,23 @@ LogicalResult verifyMultiCpuStepResultRouting(SpatScheduledComputeBatch schedule
auto dest = dyn_cast<BlockArgument>(insert.getDest());
if (!dest || dest.getOwner() != &block) {
diagnostics.report(scheduled.getOperation(), [&](Operation *illegalOp) {
illegalOp->emitOpError() << "scheduled batch step " << stepPlan.stepIndex
<< " writes to a non-block result destination";
illegalOp->emitOpError(
"scheduled batch writes to a non-block result destination");
});
continue;
}
unsigned resultIndex = dest.getArgNumber() - resultArgBase;
if (dest.getArgNumber() < resultArgBase || resultIndex >= scheduled.getNumResults()) {
diagnostics.report(scheduled.getOperation(), [&](Operation *illegalOp) {
illegalOp->emitOpError() << "scheduled batch step " << stepPlan.stepIndex
<< " writes to invalid result block argument " << dest.getArgNumber();
illegalOp->emitOpError()
<< "scheduled batch writes to invalid result block argument "
<< dest.getArgNumber();
});
continue;
}
if (resultIndex < stepPlan.resultOffset
|| resultIndex >= stepPlan.resultOffset + stepPlan.resultCount) {
diagnostics.report(scheduled.getOperation(), [&](Operation *illegalOp) {
illegalOp->emitOpError() << "scheduled batch step " << stepPlan.stepIndex
<< " expected result range [" << stepPlan.resultOffset << ":"
<< (stepPlan.resultOffset + stepPlan.resultCount)
<< ") but wrote result " << resultIndex;
});
continue;
}
localWrites[resultIndex - stepPlan.resultOffset] = true;
globalResultWrites[resultIndex]++;
}
for (size_t index = 0; index < localWrites.size(); ++index)
if (!localWrites[index])
diagnostics.report(scheduled.getOperation(), [&](Operation *illegalOp) {
illegalOp->emitOpError() << "scheduled batch step " << stepPlan.stepIndex
<< " did not write expected result " << (stepPlan.resultOffset + index);
});
}
for (size_t resultIndex = 0; resultIndex < globalResultWrites.size(); ++resultIndex)
if (globalResultWrites[resultIndex] != 1)
diagnostics.report(scheduled.getOperation(), [&](Operation *illegalOp) {
@@ -204,27 +208,25 @@ LogicalResult verifyMultiCpuStepResultRouting(SpatScheduledComputeBatch schedule
LogicalResult verifyMultiCpuLocalFragmentOffsets(SpatScheduledComputeBatch scheduled) {
pim::CappedDiagnosticReporter diagnostics;
unsigned resultArgBase = getScheduledBatchResultArgBase(scheduled);
for (auto enumeratedBlock : llvm::enumerate(scheduled.getBody().getBlocks())) {
size_t stepIndex = enumeratedBlock.index();
Block &block = enumeratedBlock.value();
Value scheduledLane = block.getArgument(0);
auto inParallel = dyn_cast<SpatInParallelOp>(block.getTerminator());
if (!inParallel) {
diagnostics.report(scheduled.getOperation(), [&](Operation *illegalOp) {
illegalOp->emitOpError() << "phase-check scheduled batch step " << stepIndex
<< " is missing spat.in_parallel";
});
continue;
}
auto isFinalScheduledOutputInsert = [&](Operation *op) {
if (!scheduled.getBody().hasOneBlock())
return scheduled.emitOpError(
"scheduled batch local fragment verification requires one block");
Block &block = scheduled.getBody().front();
if (scheduled.getNumResults() == 0)
return success(isa<SpatYieldOp>(block.getTerminator()));
Value scheduledLane = block.getArgument(0);
auto inParallel = dyn_cast<SpatInParallelOp>(block.getTerminator());
if (!inParallel)
return scheduled.emitOpError("scheduled batch is missing spat.in_parallel");
auto isFinalScheduledOutputInsert = [&](Operation *op) {
auto insert = dyn_cast<tensor::ParallelInsertSliceOp>(op);
if (!insert || op->getParentOp() != inParallel.getOperation())
return false;
auto dest = dyn_cast<BlockArgument>(insert.getDest());
return dest && dest.getOwner() == &block && dest.getArgNumber() >= resultArgBase;
};
};
block.walk([&](Operation *op) {
block.walk([&](Operation *op) {
if (op == block.getTerminator())
return;
if (isFinalScheduledOutputInsert(op)) {
@@ -266,13 +268,11 @@ LogicalResult verifyMultiCpuLocalFragmentOffsets(SpatScheduledComputeBatch sched
diagnostics.report(insertSlice.getOperation(), [&](Operation *illegalOp) {
illegalOp->emitOpError()
<< "phase-check scheduled batch local fragment insert offset must use the source-instance inner lane, not the scheduled lane"
<< " step " << stepIndex;
<< " in the shared scheduled block";
});
break;
}
});
}
});
diagnostics.emitSuppressedSummary(scheduled.getOperation(),
"scheduled batch local fragment offset verification failed");
return success(!diagnostics.hasFailure());
@@ -281,10 +281,49 @@ LogicalResult verifyMultiCpuLocalFragmentOffsets(SpatScheduledComputeBatch sched
LogicalResult verifyScheduledMaterializationRecords(ArrayRef<ScheduledMaterializationRecord> materializedSchedules) {
for (const ScheduledMaterializationRecord &record : materializedSchedules) {
if (failed(verifyStepAnchors(record)))
return failure();
size_t expectedValues = 0;
for (const ScheduledStepPlan &step : record.stepPlans)
expectedValues += step.stepTuple.instances.size() * step.resultCount;
if (record.stepValues.size() != expectedValues)
return record.scheduledOp->emitOpError(
"scheduled materialization record has the wrong step-value count");
DenseMap<ProducerValueKey, const MaterializedStepValue *> values;
for (const MaterializedStepValue &value : record.stepValues) {
if (!value.payload || value.graphId < 0 || value.laneStart < 0
|| value.laneCount <= 0 || value.payloadLaneStart < 0
|| value.payloadLaneCount <= 0
|| !values.try_emplace({value.instance, value.resultIndex}, &value).second)
return record.scheduledOp->emitOpError(
"scheduled materialization record has an invalid or duplicate step value");
}
SmallVector<unsigned> publicationCounts(record.scheduledOp->getNumResults());
for (const ScheduledPublication &publication : record.publications) {
auto value = values.find(publication.producer);
if (value == values.end()
|| publication.scheduledResultIndex >= publicationCounts.size()
|| value->second->published
!= record.scheduledOp->getResult(publication.scheduledResultIndex))
return record.scheduledOp->emitOpError(
"scheduled publication does not map to its recorded step value");
++publicationCounts[publication.scheduledResultIndex];
}
if (llvm::any_of(publicationCounts,
[](unsigned count) { return count == 0; }))
return record.scheduledOp->emitOpError(
"scheduled result has no declared publication owner");
for (const MaterializedStepValue &value : record.stepValues)
if (value.published
&& !llvm::is_contained(record.publications,
ScheduledPublication{{value.instance, value.resultIndex},
cast<OpResult>(value.published).getResultNumber()}))
return record.scheduledOp->emitOpError(
"scheduled step value has an undeclared publication");
auto scheduled = dyn_cast<SpatScheduledComputeBatch>(record.scheduledOp);
if (!scheduled)
continue;
if (failed(verifyMultiCpuStepResultRouting(scheduled, record.stepPlans)))
if (failed(verifyMultiCpuStepResultRouting(scheduled)))
return failure();
if (failed(verifyMultiCpuLocalFragmentOffsets(scheduled)))
return failure();
@@ -292,5 +331,22 @@ LogicalResult verifyScheduledMaterializationRecords(ArrayRef<ScheduledMaterializ
return success();
}
LogicalResult verifyScheduledResultsLive(
ArrayRef<ScheduledMaterializationRecord> materializedSchedules) {
pim::CappedDiagnosticReporter diagnostics;
for (const ScheduledMaterializationRecord &record : materializedSchedules)
for (OpResult result : record.scheduledOp->getResults())
if (result.use_empty())
diagnostics.report(record.scheduledOp, [&](Operation *op) {
op->emitOpError() << "scheduled result " << result.getResultNumber()
<< " has no use after communication realization";
});
if (!materializedSchedules.empty())
diagnostics.emitSuppressedSummary(
materializedSchedules.front().scheduledOp,
"scheduled publication liveness verification failed");
return success(!diagnostics.hasFailure());
}
} // namespace spatial
} // namespace onnx_mlir
@@ -13,10 +13,10 @@ LogicalResult verifyMaterializedScheduleMapping(
ArrayRef<ScheduledMaterializationRecord> materializedSchedules);
LogicalResult verifyDeferredTransferPhase1Invariants(func::FuncOp funcOp);
LogicalResult verifyMultiCpuStepResultRouting(SpatScheduledComputeBatch scheduled,
ArrayRef<ScheduledStepPlan> stepPlans);
LogicalResult verifyMultiCpuStepResultRouting(SpatScheduledComputeBatch scheduled);
LogicalResult verifyMultiCpuLocalFragmentOffsets(SpatScheduledComputeBatch scheduled);
LogicalResult verifyScheduledMaterializationRecords(ArrayRef<ScheduledMaterializationRecord> materializedSchedules);
LogicalResult verifyScheduledResultsLive(ArrayRef<ScheduledMaterializationRecord> materializedSchedules);
} // namespace spatial
} // namespace onnx_mlir
@@ -16,13 +16,6 @@ namespace spatial {
namespace {
MergeSchedulerKind getSchedulerKind() {
switch (pimMergeScheduler.getValue()) {
case MergeSchedulerPeft: return MergeSchedulerKind::Peft;
}
llvm_unreachable("unknown merge scheduler kind");
}
void verifySchedule(const ComputeGraph& graph,
const MergeScheduleResult& result,
unsigned long crossbarCapacity,
@@ -107,22 +100,19 @@ MergeScheduleResult MergeSchedulingAnalysis::run() {
if (!verifyAcyclic(graph))
llvm::report_fatal_error("merge scheduling: compute graph is cyclic");
MergeSchedulingOptions options;
options.kind = getSchedulerKind();
size_t processorCount = 0;
if (coresCount.getValue() > 0)
options.processorCount = static_cast<size_t>(coresCount.getValue());
processorCount = static_cast<size_t>(coresCount.getValue());
MergeScheduleResult schedule;
if (options.kind == MergeSchedulerKind::Peft) {
schedule = runPeftScheduler(graph,
PeftScheduleOptions {options.processorCount,
static_cast<unsigned long>(crossbarCountInCore.getValue()),
entryOp->getContext()});
}
MergeScheduleResult schedule = runPeftScheduler(
graph, PeftScheduleOptions {
processorCount,
static_cast<unsigned long>(crossbarCountInCore.getValue()),
entryOp->getContext()});
verifySchedule(graph,
schedule,
static_cast<unsigned long>(crossbarCountInCore.getValue()),
options.processorCount);
processorCount);
return schedule;
}
@@ -2,22 +2,11 @@
#include "mlir/IR/Operation.h"
#include <cstddef>
#include "MergeSchedule.hpp"
namespace onnx_mlir {
namespace spatial {
enum class MergeSchedulerKind {
Peft,
};
struct MergeSchedulingOptions {
MergeSchedulerKind kind = MergeSchedulerKind::Peft;
size_t processorCount = 0;
};
class MergeSchedulingAnalysis {
public:
explicit MergeSchedulingAnalysis(mlir::Operation* op);
@@ -418,53 +418,34 @@ resolveProducerSourcesForCsv(const ResolvedProducer& producer,
return sources;
}
FailureOr<SmallVector<int64_t>> getIntegerValues(Operation* op, StringRef name) {
Attribute attr = op->getAttr(name);
if (auto array = dyn_cast_or_null<DenseI64ArrayAttr>(attr))
return SmallVector<int64_t>(array.asArrayRef());
if (auto elements = dyn_cast_or_null<DenseIntElementsAttr>(attr))
return SmallVector<int64_t>(elements.getValues<int64_t>());
return op->emitOpError() << "expected " << name << " integer array for Spatial dataflow report";
}
FailureOr<ScheduledNodeByGraphLane>
buildScheduledNodesByGraphLane(const DenseMap<Operation*, TopLevelOpInfo>& topLevelInfo,
const DenseMap<std::pair<Operation*, uint32_t>, ExpandedNodeInfo>& expandedNodes,
const DenseMap<int64_t, Operation*>& graphOpsById) {
ArrayRef<ScheduledMaterializationRecord> records) {
ScheduledNodeByGraphLane nodesByGraphLane;
for (const auto& entry : topLevelInfo) {
Operation* scheduledOp = entry.first;
auto sourceIds = getIntegerValues(scheduledOp, "scheduled.step_source_ids");
auto sourceStarts = getIntegerValues(scheduledOp, "scheduled.source_lane_starts");
auto sourceCounts = getIntegerValues(scheduledOp, "scheduled.source_lane_counts");
if (failed(sourceIds) || failed(sourceStarts) || failed(sourceCounts))
return failure();
uint32_t scheduledLaneCount = 1;
if (auto batch = dyn_cast<SpatScheduledComputeBatch>(scheduledOp))
scheduledLaneCount = static_cast<uint32_t>(batch.getLaneCount());
size_t expectedEntries = sourceIds->size() * scheduledLaneCount;
if (sourceStarts->size() != expectedEntries || sourceCounts->size() != expectedEntries)
return scheduledOp->emitOpError("inconsistent scheduling provenance arrays for Spatial dataflow report");
for (auto [step, graphId] : llvm::enumerate(*sourceIds)) {
auto graphIt = graphOpsById.find(graphId);
if (graphIt == graphOpsById.end())
return scheduledOp->emitOpError() << "references unknown scheduled graph id " << graphId;
bool graphIsBatch = isa<SpatGraphComputeBatch>(graphIt->second);
for (uint32_t scheduledLane = 0; scheduledLane < scheduledLaneCount; ++scheduledLane) {
auto nodeIt = expandedNodes.find({scheduledOp, scheduledLane});
for (const ScheduledMaterializationRecord &record : records) {
if (!topLevelInfo.count(record.scheduledOp))
return record.scheduledOp->emitOpError(
"missing scheduled node for Spatial dataflow report");
for (const ScheduledStepPlan &step : record.stepPlans)
for (auto [scheduledLane, instance] :
llvm::enumerate(step.stepTuple.instances)) {
auto nodeIt = expandedNodes.find(
{record.scheduledOp, static_cast<uint32_t>(scheduledLane)});
if (nodeIt == expandedNodes.end())
continue;
size_t index = step * scheduledLaneCount + scheduledLane;
int64_t start = graphIsBatch ? (*sourceStarts)[index] : 0;
int64_t count = graphIsBatch ? (*sourceCounts)[index] : 1;
if (start < 0 || count < 0)
return scheduledOp->emitOpError("negative scheduling provenance range for Spatial dataflow report");
for (int64_t lane = start; lane < start + count; ++lane)
nodesByGraphLane[{graphId, static_cast<uint32_t>(lane)}] = nodeIt->second;
auto graphId = instance.op->getAttrOfType<IntegerAttr>(
"scheduled.graph_id");
if (!graphId)
return instance.op->emitOpError(
"missing graph identity for Spatial dataflow report");
uint32_t laneStart = isa<SpatGraphComputeBatch>(instance.op)
? instance.laneStart : 0;
uint32_t laneCount = isa<SpatGraphComputeBatch>(instance.op)
? instance.laneCount : 1;
for (uint32_t lane = laneStart; lane < laneStart + laneCount; ++lane)
nodesByGraphLane[{graphId.getInt(), lane}] = nodeIt->second;
}
}
}
return nodesByGraphLane;
}
@@ -489,13 +470,10 @@ emitScheduledPlanningEdges(std::fstream& edgesFile,
func::FuncOp func,
const DenseMap<Operation*, TopLevelOpInfo>& topLevelInfo,
const DenseMap<std::pair<Operation*, uint32_t>, ExpandedNodeInfo>& expandedNodes,
ArrayRef<ScheduledMaterializationRecord> records,
StringRef stage) {
DenseMap<int64_t, Operation*> graphOpsById;
for (Operation& op : func.getBody().front())
if (auto graphId = op.getAttrOfType<IntegerAttr>("scheduled.graph_id"))
graphOpsById[graphId.getInt()] = &op;
auto nodesByGraphLane = buildScheduledNodesByGraphLane(topLevelInfo, expandedNodes, graphOpsById);
auto nodesByGraphLane = buildScheduledNodesByGraphLane(
topLevelInfo, expandedNodes, records);
if (failed(nodesByGraphLane))
return failure();
@@ -734,7 +712,7 @@ LogicalResult emitExplicitChannelEdges(std::fstream& edgesFile,
return success();
}
LogicalResult exportGraph(func::FuncOp func, StringRef reportName) {
LogicalResult exportGraph(func::FuncOp func, StringRef reportName, StringRef stage) {
std::fstream nodesFile = openDialectDumpFileWithExtension((reportName + ".nodes").str(), "/reports", "csv");
std::fstream edgesFile = openDialectDumpFileWithExtension((reportName + ".edges").str(), "/reports", "csv");
if (!nodesFile.is_open() || !edgesFile.is_open())
@@ -772,10 +750,12 @@ LogicalResult exportGraph(func::FuncOp func, StringRef reportName) {
addBatchNodeRows(nodesFile, expandedNodes, *info, batch, laneCoreIds, &asmState);
}
return emitDataEdges<SpatGraphCompute, SpatGraphComputeBatch>(edgesFile, topLevelInfo, expandedNodes, "spatial1");
return emitDataEdges<SpatGraphCompute, SpatGraphComputeBatch>(edgesFile, topLevelInfo, expandedNodes, stage);
}
LogicalResult exportScheduled(func::FuncOp func, StringRef reportName, StringRef stage) {
LogicalResult exportScheduled(func::FuncOp func,
ArrayRef<ScheduledMaterializationRecord> records,
StringRef reportName, StringRef stage) {
std::fstream nodesFile = openDialectDumpFileWithExtension((reportName + ".nodes").str(), "/reports", "csv");
std::fstream edgesFile = openDialectDumpFileWithExtension((reportName + ".edges").str(), "/reports", "csv");
if (!nodesFile.is_open() || !edgesFile.is_open())
@@ -820,8 +800,9 @@ LogicalResult exportScheduled(func::FuncOp func, StringRef reportName, StringRef
addBatchNodeRows(nodesFile, expandedNodes, *info, batch, laneCoreIds, &asmState);
}
if (stage == "spatial2")
return emitScheduledPlanningEdges(edgesFile, func, topLevelInfo, expandedNodes, stage);
if (stage == "spatial3")
return emitScheduledPlanningEdges(
edgesFile, func, topLevelInfo, expandedNodes, records, stage);
if (failed(
emitDataEdges<SpatScheduledCompute, SpatScheduledComputeBatch>(edgesFile, topLevelInfo, expandedNodes, stage)))
return failure();
@@ -870,6 +851,7 @@ SpatialDataflowExportStage getSpatialDataflowExportStage() {
case SpatialDataflowExportSpatial1: return SpatialDataflowExportStage::Spatial1;
case SpatialDataflowExportSpatial2: return SpatialDataflowExportStage::Spatial2;
case SpatialDataflowExportSpatial3: return SpatialDataflowExportStage::Spatial3;
case SpatialDataflowExportSpatial4: return SpatialDataflowExportStage::Spatial4;
case SpatialDataflowExportAll: return SpatialDataflowExportStage::All;
}
llvm_unreachable("unknown spatial dataflow export mode");
@@ -881,17 +863,20 @@ bool shouldExportSpatialDataflowStage(SpatialDataflowExportStage mode, SpatialDa
case SpatialDataflowExportStage::Spatial1: return stage == SpatialDataflowExportStage::Spatial1;
case SpatialDataflowExportStage::Spatial2: return stage == SpatialDataflowExportStage::Spatial2;
case SpatialDataflowExportStage::Spatial3: return stage == SpatialDataflowExportStage::Spatial3;
case SpatialDataflowExportStage::Spatial4: return stage == SpatialDataflowExportStage::Spatial4;
case SpatialDataflowExportStage::All: return stage != SpatialDataflowExportStage::None;
}
return false;
}
LogicalResult exportSpatialDataflowCsvGraph(func::FuncOp func, StringRef reportName) {
return exportGraph(func, reportName);
LogicalResult exportSpatialDataflowCsvGraph(func::FuncOp func, StringRef reportName, StringRef stage) {
return exportGraph(func, reportName, stage);
}
LogicalResult exportSpatialDataflowCsvScheduled(func::FuncOp func, StringRef reportName, StringRef stage) {
return exportScheduled(func, reportName, stage);
LogicalResult exportSpatialDataflowCsvScheduled(
func::FuncOp func, ArrayRef<ScheduledMaterializationRecord> records,
StringRef reportName, StringRef stage) {
return exportScheduled(func, records, reportName, stage);
}
} // namespace spatial
@@ -5,6 +5,8 @@
#include "llvm/ADT/StringRef.h"
#include "ScheduledComputeMaterialization.hpp"
namespace onnx_mlir {
namespace spatial {
@@ -13,14 +15,20 @@ enum class SpatialDataflowExportStage {
Spatial1,
Spatial2,
Spatial3,
Spatial4,
All,
};
SpatialDataflowExportStage getSpatialDataflowExportStage();
mlir::LogicalResult exportSpatialDataflowCsvGraph(mlir::func::FuncOp func, llvm::StringRef reportName);
mlir::LogicalResult exportSpatialDataflowCsvGraph(mlir::func::FuncOp func,
llvm::StringRef reportName,
llvm::StringRef stage = "spatial1");
mlir::LogicalResult
exportSpatialDataflowCsvScheduled(mlir::func::FuncOp func, llvm::StringRef reportName, llvm::StringRef stage);
exportSpatialDataflowCsvScheduled(mlir::func::FuncOp func,
llvm::ArrayRef<ScheduledMaterializationRecord> records,
llvm::StringRef reportName,
llvm::StringRef stage);
bool shouldExportSpatialDataflowStage(SpatialDataflowExportStage mode, SpatialDataflowExportStage stage);
@@ -0,0 +1,508 @@
#include "mlir/Dialect/Affine/IR/AffineOps.h"
#include "mlir/Dialect/SCF/IR/SCF.h"
#include "mlir/Dialect/Tensor/IR/Tensor.h"
#include "mlir/IR/IRMapping.h"
#include "mlir/Pass/Pass.h"
#include "mlir/Transforms/GreedyPatternRewriteDriver.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SetVector.h"
#include "llvm/Support/FormatVariadic.h"
#include "llvm/Support/raw_os_ostream.h"
#include <fstream>
#include "src/Accelerators/PIM/Common/IR/TensorSliceUtils.hpp"
#include "src/Accelerators/PIM/Common/PimCommon.hpp"
#include "src/Accelerators/PIM/Common/Support/DebugDump.hpp"
#include "src/Accelerators/PIM/Compiler/PimCompilerOptions.hpp"
#include "src/Accelerators/PIM/Conversion/ONNXToSpatial/Common/ComputeRegionBuilder.hpp"
#include "src/Accelerators/PIM/Dialect/Spatial/SpatialOps.hpp"
#include "src/Accelerators/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/Scheduling/ComputeGraph.hpp"
#include "src/Accelerators/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/SpatialDataflowCsvExporter.hpp"
#include "src/Accelerators/PIM/Pass/PIMPasses.h"
using namespace mlir;
namespace onnx_mlir {
namespace spatial {
namespace {
template <typename ComputeOp>
static bool hasOnlyStructuralAttrs(ComputeOp op) {
return llvm::all_of(op->getAttrs(), [&](NamedAttribute attr) {
return attr.getName() == op.getOperandSegmentSizesAttrName()
|| (isa<SpatGraphComputeBatch>(op.getOperation())
&& attr.getName() == cast<SpatGraphComputeBatch>(op.getOperation()).getLaneCountAttrName());
});
}
static bool hasCapacityFor(Operation* producer, Operation* consumer) {
CrossbarUsage producerWeights = collectDistinctCrossbarWeights(producer);
CrossbarUsage consumerWeights = collectDistinctCrossbarWeights(consumer);
return getCrossbarUnionSize(producerWeights, consumerWeights) <= static_cast<size_t>(crossbarCountInCore.getValue());
}
struct TrivialGraphMergeStats {
size_t scalarBefore = 0;
size_t batchBefore = 0;
size_t scalarAfter = 0;
size_t batchAfter = 0;
size_t scalarProducerConsumerMerges = 0;
size_t batchProducerConsumerMerges = 0;
size_t leadingUnitNormalizationFolds = 0;
};
static std::pair<size_t, size_t> countGraphComputes(ModuleOp module) {
size_t scalar = 0, batch = 0;
module.walk([&](Operation *op) {
scalar += isa<SpatGraphCompute>(op);
batch += isa<SpatGraphComputeBatch>(op);
});
return {scalar, batch};
}
static void dumpTrivialMergeReport(const TrivialGraphMergeStats &stats,
size_t largestBatchLaneCount) {
std::fstream file = openDialectDumpFileWithExtension("spatial2_trivial_merged", "/reports", "txt");
if (!file.is_open())
return;
llvm::raw_os_ostream os(file);
size_t before = stats.scalarBefore + stats.batchBefore;
size_t after = stats.scalarAfter + stats.batchAfter;
size_t removed = before - after;
double percentage = before ? 100.0 * removed / before : 0.0;
os << "Summary\n"
<< " graph computes: " << before << " -> " << after << "\n"
<< " removed: " << removed << " (" << llvm::formatv("{0:F2}", percentage) << "%)\n"
<< " scalar: " << stats.scalarBefore << " -> " << stats.scalarAfter << "\n"
<< " batch: " << stats.batchBefore << " -> " << stats.batchAfter << "\n"
<< " transformations:\n"
<< " scalar producer-consumer merges: " << stats.scalarProducerConsumerMerges << "\n"
<< " batch producer-consumer merges: " << stats.batchProducerConsumerMerges << "\n"
<< " leading-unit normalization folds: " << stats.leadingUnitNormalizationFolds << "\n\n"
<< "Resulting graph\n"
<< " graph computes: " << after << "\n"
<< " scalar: " << stats.scalarAfter << "\n"
<< " batch: " << stats.batchAfter << "\n"
<< " largest batch lane count: " << largestBatchLaneCount << "\n";
}
template <typename ProducerOp, typename ConsumerOp>
static bool isExclusivelyConsumedBy(ProducerOp producer, ConsumerOp consumer) {
bool hasDependency = false;
for (Value result : producer.getResults()) {
for (OpOperand& use : result.getUses()) {
if (use.getOwner() != consumer.getOperation() || !llvm::is_contained(consumer.getInputs(), result))
return false;
hasDependency = true;
}
}
return hasDependency;
}
static bool hasNoNestedArgumentCaptures(SpatGraphCompute compute) {
Block &body = compute.getBody().front();
return llvm::all_of(body.getArguments(), [&](BlockArgument argument) {
return llvm::all_of(argument.getUsers(), [&](Operation *user) { return user->getBlock() == &body; });
});
}
template <typename ProducerOp, typename ConsumerOp>
static void collectExternalOperands(ProducerOp producer,
ConsumerOp consumer,
llvm::SetVector<Value>& weights,
llvm::SetVector<Value>& inputs) {
weights.insert(producer.getWeights().begin(), producer.getWeights().end());
weights.insert(consumer.getWeights().begin(), consumer.getWeights().end());
auto appendInput = [&](Value value) {
if (value.getDefiningOp() != producer.getOperation() && !weights.contains(value))
inputs.insert(value);
};
llvm::for_each(producer.getInputs(), appendInput);
llvm::for_each(consumer.getInputs(), appendInput);
}
template <typename OldOp, typename NewOp>
static void mapExternalArguments(OldOp oldOp, NewOp newOp, IRMapping& mapper, bool mapForwardedInputs = true) {
for (auto [index, operand] : llvm::enumerate(oldOp.getWeights())) {
auto oldArg = oldOp.getWeightArgument(index);
auto newOperand = llvm::find(newOp.getWeights(), operand);
assert(oldArg && newOperand != newOp.getWeights().end());
mapper.map(*oldArg, *newOp.getWeightArgument(std::distance(newOp.getWeights().begin(), newOperand)));
}
for (auto [index, operand] : llvm::enumerate(oldOp.getInputs())) {
auto oldArg = oldOp.getInputArgument(index);
assert(oldArg);
if (mapper.contains(operand)) {
if (mapForwardedInputs)
mapper.map(*oldArg, mapper.lookup(operand));
continue;
}
auto newOperand = llvm::find(newOp.getInputs(), operand);
assert(oldArg && newOperand != newOp.getInputs().end());
mapper.map(*oldArg, *newOp.getInputArgument(std::distance(newOp.getInputs().begin(), newOperand)));
}
}
struct MergeTrivialScalarComputes : OpRewritePattern<SpatGraphCompute> {
MergeTrivialScalarComputes(MLIRContext *context, TrivialGraphMergeStats *stats)
: OpRewritePattern(context), stats(stats) {}
LogicalResult matchAndRewrite(SpatGraphCompute consumer, PatternRewriter& rewriter) const override {
SpatGraphCompute producer;
for (Value input : consumer.getInputs()) {
auto candidate = input.getDefiningOp<SpatGraphCompute>();
if (candidate && candidate->getBlock() == consumer->getBlock() && hasOnlyStructuralAttrs(candidate)
&& hasOnlyStructuralAttrs(consumer) && isExclusivelyConsumedBy(candidate, consumer)
&& hasCapacityFor(candidate, consumer) && hasNoNestedArgumentCaptures(candidate)
&& hasNoNestedArgumentCaptures(consumer)) {
producer = candidate;
break;
}
}
if (!producer)
return failure();
llvm::SetVector<Value> weights, inputs;
collectExternalOperands(producer, consumer, weights, inputs);
rewriter.setInsertionPoint(consumer);
SpatGraphCompute merged = createEmptySpatGraphCompute(
rewriter, consumer.getLoc(), consumer.getResultTypes(), weights.getArrayRef(), inputs.getArrayRef());
IRMapping mapper;
mapExternalArguments(producer, merged, mapper);
for (Operation& op : producer.getBody().front().without_terminator())
rewriter.clone(op, mapper);
auto producerYield = cast<SpatYieldOp>(producer.getBody().front().getTerminator());
for (auto [result, yielded] : llvm::zip(producer.getResults(), producerYield.getOutputs()))
mapper.map(result, mapper.lookupOrDefault(yielded));
mapExternalArguments(consumer, merged, mapper);
for (Operation& op : consumer.getBody().front())
rewriter.clone(op, mapper);
rewriter.replaceOp(consumer, merged.getResults());
rewriter.eraseOp(producer);
++stats->scalarProducerConsumerMerges;
return success();
}
private:
TrivialGraphMergeStats *stats;
};
static bool isLaneIndex(Value value, Value lane, int64_t laneCount) {
if (!value)
return false;
if (value == lane)
return true;
auto apply = value.getDefiningOp<affine::AffineApplyOp>();
if (!apply || apply.getMapOperands().size() != 1 || apply.getMapOperands().front() != lane)
return false;
AffineMap map = apply.getAffineMap();
if (map.getNumDims() != 1 || map.getNumSymbols() != 0 || map.getNumResults() != 1)
return false;
AffineExpr expression = map.getResult(0);
if (auto dim = dyn_cast<AffineDimExpr>(expression))
return dim.getPosition() == 0;
auto modulo = dyn_cast<AffineBinaryOpExpr>(expression);
if (!modulo || modulo.getKind() != AffineExprKind::Mod)
return false;
auto dim = dyn_cast<AffineDimExpr>(modulo.getLHS());
auto divisor = dyn_cast<AffineConstantExpr>(modulo.getRHS());
return dim && dim.getPosition() == 0
&& divisor && divisor.getValue() >= laneCount;
}
static bool isDirectLaneSlot(ArrayRef<OpFoldResult> offsets,
ArrayRef<OpFoldResult> sizes,
ArrayRef<OpFoldResult> strides,
Value lane,
RankedTensorType physicalType) {
size_t rank = physicalType.getRank();
if (offsets.size() != rank || sizes.size() != rank || strides.size() != rank
|| !isLaneIndex(dyn_cast<Value>(offsets.front()), lane, physicalType.getDimSize(0)))
return false;
for (size_t dim = 0; dim < rank; ++dim) {
int64_t expectedOffset = 0;
int64_t expectedSize = dim == 0 ? 1 : physicalType.getDimSize(dim);
if ((dim != 0 && getConstantIntValue(offsets[dim]) != expectedOffset)
|| getConstantIntValue(sizes[dim]) != expectedSize || getConstantIntValue(strides[dim]) != 1)
return false;
}
return true;
}
static FailureOr<SmallVector<Value>> collectPublishedFragments(SpatGraphComputeBatch producer) {
auto terminator = dyn_cast<SpatInParallelOp>(producer.getBody().front().getTerminator());
auto lane = producer.getLaneArgument();
if (!terminator || !lane)
return failure();
SmallVector<Value> fragments(producer.getNumResults());
for (Operation& op : terminator.getRegion().front()) {
auto insert = dyn_cast<tensor::ParallelInsertSliceOp>(op);
auto destination = insert ? dyn_cast<BlockArgument>(insert.getDest()) : BlockArgument();
if (!insert || !destination)
return failure();
unsigned resultIndex = 0;
while (resultIndex < producer.getNumResults() && producer.getOutputArgument(resultIndex) != destination)
++resultIndex;
if (resultIndex == producer.getNumResults() || fragments[resultIndex])
return failure();
auto physicalType = dyn_cast<RankedTensorType>(producer.getResult(resultIndex).getType());
auto fragmentType = dyn_cast<RankedTensorType>(insert.getSource().getType());
auto expectedFragmentType = getGraphBatchFragmentType(physicalType, producer.getLaneCount());
if (!physicalType || !fragmentType || failed(expectedFragmentType) || *expectedFragmentType != fragmentType
|| !isDirectLaneSlot(
insert.getMixedOffsets(), insert.getMixedSizes(), insert.getMixedStrides(), *lane, physicalType))
return failure();
fragments[resultIndex] = insert.getSource();
}
if (!llvm::all_of(fragments, [](Value value) { return value; }))
return failure();
return fragments;
}
static bool matchLeadingUnitNormalization(SpatGraphComputeBatch producer, SpatGraphCompute consumer) {
if (producer.getNumResults() != 1 || !consumer.getWeights().empty()
|| consumer.getInputs().size() != 1 || consumer.getInputs().front() != producer.getResult(0)
|| consumer.getNumResults() != 1)
return false;
auto yield = dyn_cast<SpatYieldOp>(consumer.getBody().front().getTerminator());
auto loopResult = yield && yield.getOutputs().size() == 1
? dyn_cast<OpResult>(yield.getOutputs().front())
: OpResult();
auto loop = loopResult ? dyn_cast<scf::ForOp>(loopResult.getOwner()) : scf::ForOp();
auto targetType = dyn_cast<RankedTensorType>(consumer.getResult(0).getType());
if (!loop || llvm::range_size(consumer.getBody().front().without_terminator()) != 2
|| loop->getBlock() != &consumer.getBody().front() || loopResult.getResultNumber() != 0
|| loop.getNumResults() != 1 || loop.getResult(0).getType() != targetType
|| !targetType || !targetType.hasStaticShape()
|| getConstantIntValue(loop.getLowerBound()) != 0
|| getConstantIntValue(loop.getUpperBound()) != producer.getLaneCount()
|| getConstantIntValue(loop.getStep()) != 1
|| llvm::range_size(loop.getBody()->without_terminator()) != 2)
return false;
auto empty = loop.getInitArgs().front().getDefiningOp<tensor::EmptyOp>();
auto bodyIt = loop.getBody()->begin();
auto extract = dyn_cast<tensor::ExtractSliceOp>(&*bodyIt++);
auto insert = dyn_cast<tensor::InsertSliceOp>(&*bodyIt);
auto loopYield = dyn_cast<scf::YieldOp>(loop.getBody()->getTerminator());
auto sourceType = dyn_cast<RankedTensorType>(producer.getResult(0).getType());
auto sourceFragmentType = sourceType
? getGraphBatchFragmentType(sourceType, producer.getLaneCount())
: FailureOr<RankedTensorType>(failure());
auto targetFragmentType = getGraphBatchFragmentType(targetType, producer.getLaneCount());
return empty && empty->getBlock() == &consumer.getBody().front() && empty.getType() == targetType
&& extract && insert && loopYield && loopYield.getResults().size() == 1
&& extract.getSource() == consumer.getInputArgument(0)
&& insert.getSource() == extract.getResult() && insert.getDest() == loop.getRegionIterArgs().front()
&& loopYield.getResults().front() == insert.getResult()
&& succeeded(sourceFragmentType) && succeeded(targetFragmentType)
&& extract.getType() == *sourceFragmentType
&& sourceFragmentType->getRank() == targetFragmentType->getRank() + 1
&& sourceFragmentType->getDimSize(0) == 1
&& sourceFragmentType->getShape().drop_front() == targetFragmentType->getShape()
&& sourceFragmentType->getElementType() == targetFragmentType->getElementType()
&& isDirectLaneSlot(extract.getMixedOffsets(), extract.getMixedSizes(), extract.getMixedStrides(),
loop.getInductionVar(), sourceType)
&& isDirectLaneSlot(insert.getMixedOffsets(), insert.getMixedSizes(), insert.getMixedStrides(),
loop.getInductionVar(), targetType);
}
struct FoldBatchLeadingUnitNormalization : OpRewritePattern<SpatGraphCompute> {
FoldBatchLeadingUnitNormalization(MLIRContext *context, TrivialGraphMergeStats *stats)
: OpRewritePattern(context), stats(stats) {}
LogicalResult matchAndRewrite(SpatGraphCompute consumer, PatternRewriter& rewriter) const override {
auto producer = consumer.getInputs().empty()
? SpatGraphComputeBatch()
: consumer.getInputs().front().getDefiningOp<SpatGraphComputeBatch>();
if (!producer || producer->getBlock() != consumer->getBlock() || !hasOnlyStructuralAttrs(producer)
|| !hasOnlyStructuralAttrs(consumer) || !isExclusivelyConsumedBy(producer, consumer))
return failure();
auto fragments = collectPublishedFragments(producer);
if (!matchLeadingUnitNormalization(producer, consumer) || failed(fragments))
return failure();
rewriter.setInsertionPoint(consumer);
auto folded = createEmptySpatGraphComputeBatch(rewriter, consumer.getLoc(), consumer.getResultTypes(),
producer.getLaneCount(), producer.getWeights(), producer.getInputs());
if (failed(folded))
return failure();
IRMapping mapper;
mapper.map(*producer.getLaneArgument(), *folded->getLaneArgument());
mapExternalArguments(producer, *folded, mapper);
for (Operation& op : producer.getBody().front().without_terminator())
rewriter.clone(op, mapper);
auto outputType = cast<RankedTensorType>(consumer.getResult(0).getType());
auto fragmentType = *getGraphBatchFragmentType(outputType, producer.getLaneCount());
auto fragment = removeLeadingUnitTensorDimension(
rewriter, consumer.getLoc(), mapper.lookup(fragments->front()), fragmentType);
assert(succeeded(fragment) && "normalization fragment types were prechecked");
publishGraphBatchPhysicalFragment(rewriter, consumer.getLoc(), *fragment,
*folded->getOutputArgument(0), *folded->getLaneArgument());
rewriter.replaceOp(consumer, folded->getResults());
rewriter.eraseOp(producer);
++stats->leadingUnitNormalizationFolds;
return success();
}
private:
TrivialGraphMergeStats *stats;
};
static bool hasDirectLaneConsumers(SpatGraphComputeBatch producer, SpatGraphComputeBatch consumer) {
auto lane = consumer.getLaneArgument();
if (!lane)
return false;
for (auto [inputIndex, input] : llvm::enumerate(consumer.getInputs())) {
if (input.getDefiningOp() != producer.getOperation())
continue;
auto inputArg = consumer.getInputArgument(inputIndex);
auto physicalType = dyn_cast<RankedTensorType>(input.getType());
if (!inputArg || !physicalType || inputArg->use_empty())
return false;
for (Operation* user : inputArg->getUsers()) {
auto extract = dyn_cast<tensor::ExtractSliceOp>(user);
if (!extract || extract.getSource() != *inputArg
|| !isDirectLaneSlot(
extract.getMixedOffsets(), extract.getMixedSizes(), extract.getMixedStrides(), *lane, physicalType))
return false;
auto resultType = dyn_cast<RankedTensorType>(extract.getType());
auto fragmentType = getGraphBatchFragmentType(physicalType, producer.getLaneCount());
if (failed(fragmentType) || !resultType
|| (resultType != *fragmentType
&& (resultType.getRank() != fragmentType->getRank() + 1 || resultType.getDimSize(0) != 1
|| resultType.getShape().drop_front() != fragmentType->getShape()
|| resultType.getElementType() != fragmentType->getElementType())))
return false;
}
}
return true;
}
struct MergeTrivialBatchComputes : OpRewritePattern<SpatGraphComputeBatch> {
MergeTrivialBatchComputes(MLIRContext *context, TrivialGraphMergeStats *stats)
: OpRewritePattern(context), stats(stats) {}
LogicalResult matchAndRewrite(SpatGraphComputeBatch consumer, PatternRewriter& rewriter) const override {
SpatGraphComputeBatch producer;
FailureOr<SmallVector<Value>> fragments = failure();
for (Value input : consumer.getInputs()) {
auto candidate = input.getDefiningOp<SpatGraphComputeBatch>();
if (candidate && candidate->getBlock() == consumer->getBlock()
&& candidate.getLaneCount() == consumer.getLaneCount() && hasOnlyStructuralAttrs(candidate)
&& hasOnlyStructuralAttrs(consumer) && isExclusivelyConsumedBy(candidate, consumer)
&& hasCapacityFor(candidate, consumer) && hasDirectLaneConsumers(candidate, consumer)
&& succeeded(fragments = collectPublishedFragments(candidate))) {
producer = candidate;
break;
}
}
if (!producer)
return failure();
llvm::SetVector<Value> weights, inputs;
collectExternalOperands(producer, consumer, weights, inputs);
rewriter.setInsertionPoint(consumer);
SpatGraphComputeBatch merged = *createEmptySpatGraphComputeBatch(rewriter, consumer.getLoc(), consumer.getResultTypes(),
consumer.getLaneCount(), weights.getArrayRef(), inputs.getArrayRef());
IRMapping mapper;
mapper.map(*producer.getLaneArgument(), *merged.getLaneArgument());
mapExternalArguments(producer, merged, mapper);
for (Operation& op : producer.getBody().front().without_terminator())
rewriter.clone(op, mapper);
for (auto [result, fragment] : llvm::zip(producer.getResults(), *fragments))
mapper.map(result, mapper.lookupOrDefault(fragment));
mapper.map(*consumer.getLaneArgument(), *merged.getLaneArgument());
mapExternalArguments(consumer, merged, mapper, /*mapForwardedInputs=*/false);
for (auto [index, output] : llvm::enumerate(consumer.getResults()))
mapper.map(*consumer.getOutputArgument(index), *merged.getOutputArgument(index));
for (Operation& op : consumer.getBody().front().without_terminator()) {
auto extract = dyn_cast<tensor::ExtractSliceOp>(op);
auto sourceArg = extract ? dyn_cast<BlockArgument>(extract.getSource()) : BlockArgument();
unsigned firstInputArg = 1 + consumer.getWeights().size();
if (!sourceArg || sourceArg.getOwner() != &consumer.getBody().front() || sourceArg.getArgNumber() < firstInputArg
|| sourceArg.getArgNumber() >= firstInputArg + consumer.getInputs().size()) {
rewriter.clone(op, mapper);
continue;
}
unsigned inputIndex = sourceArg.getArgNumber() - firstInputArg;
Value input = consumer.getInputs()[inputIndex];
if (input.getDefiningOp() != producer.getOperation()) {
rewriter.clone(op, mapper);
continue;
}
Value fragment = mapper.lookup(input);
if (fragment.getType() != extract.getType()) {
auto expanded = addLeadingUnitTensorDimension(rewriter, extract.getLoc(), fragment);
assert(succeeded(expanded) && expanded->getType() == extract.getType()
&& "prechecked physical fragment type must be forwardable");
fragment = *expanded;
}
mapper.map(extract.getResult(), fragment);
}
rewriter.clone(*consumer.getBody().front().getTerminator(), mapper);
rewriter.replaceOp(consumer, merged.getResults());
rewriter.eraseOp(producer);
++stats->batchProducerConsumerMerges;
return success();
}
private:
TrivialGraphMergeStats *stats;
};
struct TrivialGraphComputeMergePass final : PassWrapper<TrivialGraphComputeMergePass, OperationPass<ModuleOp>> {
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TrivialGraphComputeMergePass)
StringRef getArgument() const override { return "pim-trivial-graph-compute-merge"; }
StringRef getDescription() const override {
return "Inline graph computes with exclusive direct dependencies before PEFT scheduling.";
}
void runOnOperation() override {
ModuleOp module = getOperation();
TrivialGraphMergeStats stats;
std::tie(stats.scalarBefore, stats.batchBefore) = countGraphComputes(module);
RewritePatternSet patterns(&getContext());
patterns.add<MergeTrivialScalarComputes, MergeTrivialBatchComputes>(&getContext(), &stats);
if (failed(applyPatternsGreedily(module, std::move(patterns)))) {
signalPassFailure();
return;
}
RewritePatternSet normalization(&getContext());
normalization.add<FoldBatchLeadingUnitNormalization>(&getContext(), &stats);
if (failed(applyPatternsGreedily(module, std::move(normalization)))) {
signalPassFailure();
return;
}
std::tie(stats.scalarAfter, stats.batchAfter) = countGraphComputes(module);
size_t largestBatchLaneCount = 0;
module.walk([&](SpatGraphComputeBatch batch) {
largestBatchLaneCount = std::max(largestBatchLaneCount, static_cast<size_t>(batch.getLaneCount()));
});
dumpTrivialMergeReport(stats, largestBatchLaneCount);
dumpModule(module, "spatial2_trivial_merged");
SpatialDataflowExportStage exportMode = getSpatialDataflowExportStage();
if (shouldExportSpatialDataflowStage(exportMode, SpatialDataflowExportStage::Spatial2)) {
auto entryFunc = getPimEntryFunc(module);
if (failed(entryFunc)
|| failed(exportSpatialDataflowCsvGraph(*entryFunc, "spatial2_trivial_merged", "spatial2")))
signalPassFailure();
}
}
};
} // namespace
} // namespace spatial
std::unique_ptr<Pass> createTrivialGraphComputeMergePass() {
return std::make_unique<spatial::TrivialGraphComputeMergePass>();
}
} // namespace onnx_mlir
+2
View File
@@ -19,6 +19,8 @@ std::unique_ptr<mlir::Pass> createPimMemoryCoalescingPass();
std::unique_ptr<mlir::Pass> createMergeComputeNodesPass();
std::unique_ptr<mlir::Pass> createTrivialGraphComputeMergePass();
std::unique_ptr<mlir::Pass> createPimHostConstantFoldingPass();
std::unique_ptr<mlir::Pass> createPimVerificationPass();
+1
View File
@@ -77,6 +77,7 @@ void PimAccelerator::registerPasses(int optLevel) const {
registerPass(createSpatialToPimPass);
registerPass(createPimBufferizationPass);
registerPass(createPimMemoryCoalescingPass);
registerPass(createTrivialGraphComputeMergePass);
registerPass(createMergeComputeNodesPass);
registerPass(createPimHostConstantFoldingPass);
registerPass(createPimVerificationPass);
+3 -3
View File
@@ -70,7 +70,7 @@ The server defaults to `127.0.0.1:8765`. It has no authentication and is intende
## Supported CSV reports
Files are paired using the complete prefix before `.nodes.csv` and `.edges.csv`. That prefix is the stable `report_id`; `spatial2_scheduled_no_comm.nodes.csv` therefore pairs only with `spatial2_scheduled_no_comm.edges.csv`.
Files are paired using the complete prefix before `.nodes.csv` and `.edges.csv`. That prefix is the stable `report_id`; `spatial3_scheduled_no_comm.nodes.csv` therefore pairs only with `spatial3_scheduled_no_comm.edges.csv`.
Node columns:
@@ -116,7 +116,7 @@ Nodes group by `(report_id, node_kind)`. This deliberately small diagnostic view
### Raw instances
Raw instances displays every `raw_nodes` row exactly once and every valid `raw_edges` row exactly once. Nodes are queried independently from edges, so degree-zero nodes remain visible. A spatial3 report starts in this view automatically; Operation, Core, and Node kind remain selectable.
Raw instances displays every `raw_nodes` row exactly once and every valid `raw_edges` row exactly once. Nodes are queried independently from edges, so degree-zero nodes remain visible. A spatial4 report starts in this view automatically; Operation, Core, and Node kind remain selectable.
Spatial3 CSVs record explicit communication only and may legitimately contain many isolated nodes. The explorer never fabricates dependencies from semicolon-separated SSA names or other node text.
@@ -155,7 +155,7 @@ Motifs are not inferred from rendered geometry. For each operation graph the too
## Viewer and API
Except for spatial3, the viewer initially fetches an aggregate operation graph unless the browser has a saved view choice. Sigma renders the graph with WebGL. Controls cover report/view/metric selection, text and tensor search, mapping filters, self edges, relayout, fitting, and motif selection. Raw nodes and edges have their own selection details and never call aggregate-only detail or mapping endpoints. Mapping panels remain available for operation aggregate edges.
Except for spatial4, the viewer initially fetches an aggregate operation graph unless the browser has a saved view choice. Sigma renders the graph with WebGL. Controls cover report/view/metric selection, text and tensor search, mapping filters, self edges, relayout, fitting, and motif selection. Raw nodes and edges have their own selection details and never call aggregate-only detail or mapping endpoints. Mapping panels remain available for operation aggregate edges.
Operation expansion is a deterministic projection of the source graph. Expand selected, Expand all operations, Collapse selected operation, and Collapse all rebuild the complete display from the current expanded-operation set. An expanded operation's raw nodes replace its aggregate node, including isolated raw nodes. An aggregate edge is retained only when both endpoint operations are collapsed; otherwise its raw edges replace it with endpoints calculated from the complete expansion set. Expansion order therefore cannot leave stale endpoints or simultaneous aggregate/raw representations.
@@ -84,7 +84,7 @@ The server defaults to `127.0.0.1:8765`. It has no authentication and is intende
## Supported CSV reports
Files are paired using the complete prefix before `.nodes.csv` and `.edges.csv`. That prefix is the stable `report_id`; `spatial2_scheduled_no_comm.nodes.csv` therefore pairs only with `spatial2_scheduled_no_comm.edges.csv`.
Files are paired using the complete prefix before `.nodes.csv` and `.edges.csv`. That prefix is the stable `report_id`; `spatial3_scheduled_no_comm.nodes.csv` therefore pairs only with `spatial3_scheduled_no_comm.edges.csv`.
Node columns:
@@ -130,7 +130,7 @@ Nodes group by `(report_id, node_kind)`. This deliberately small diagnostic view
### Raw instances
Raw instances displays every `raw_nodes` row exactly once and every valid `raw_edges` row exactly once. Nodes are queried independently from edges, so degree-zero nodes remain visible. A spatial3 report starts in this view automatically; Operation, Core, and Node kind remain selectable.
Raw instances displays every `raw_nodes` row exactly once and every valid `raw_edges` row exactly once. Nodes are queried independently from edges, so degree-zero nodes remain visible. A spatial4 report starts in this view automatically; Operation, Core, and Node kind remain selectable.
Spatial3 CSVs record explicit communication only and may legitimately contain many isolated nodes. The explorer never fabricates dependencies from semicolon-separated SSA names or other node text.
@@ -169,7 +169,7 @@ Motifs are not inferred from rendered geometry. For each operation graph the too
## Viewer and API
Except for spatial3, the viewer initially fetches an aggregate operation graph unless the browser has a saved view choice. Sigma renders the graph with WebGL. Controls cover report/view/metric selection, text and tensor search, mapping filters, self edges, relayout, fitting, and motif selection. Raw nodes and edges have their own selection details and never call aggregate-only detail or mapping endpoints. Mapping panels remain available for operation aggregate edges.
Except for spatial4, the viewer initially fetches an aggregate operation graph unless the browser has a saved view choice. Sigma renders the graph with WebGL. Controls cover report/view/metric selection, text and tensor search, mapping filters, self edges, relayout, fitting, and motif selection. Raw nodes and edges have their own selection details and never call aggregate-only detail or mapping endpoints. Mapping panels remain available for operation aggregate edges.
Operation expansion is a deterministic projection of the source graph. Expand selected, Expand all operations, Collapse selected operation, and Collapse all rebuild the complete display from the current expanded-operation set. An expanded operation's raw nodes replace its aggregate node, including isolated raw nodes. An aggregate edge is retained only when both endpoint operations are collapsed; otherwise its raw edges replace it with endpoints calculated from the complete expansion set. Expansion order therefore cannot leave stale endpoints or simultaneous aggregate/raw representations.
@@ -1,7 +1,6 @@
README.md
pyproject.toml
raptor_graph_explorer/__init__.py
raptor_graph_explorer/__main__.py
raptor_graph_explorer/aggregate.py
raptor_graph_explorer/api.py
raptor_graph_explorer/cli.py
@@ -0,0 +1 @@
__version__ = "0.1.0"
@@ -18,7 +18,7 @@ function definition(object, keys) {
function status(message, error = false) { $("status").textContent = message; $("status").className = error ? "error" : ""; }
function selectedReport() { return state.reports.find(report => report.report_id === $("report").value); }
function defaultViewForReport(report) {
if (report?.stage === "spatial3") return "raw";
if (report?.stage === "spatial4") return "raw";
const saved = localStorage.getItem("raptor-graph-view");
return ["operation", "raw", "core", "node_kind"].includes(saved) ? saved : "operation";
}
@@ -1,2 +1,2 @@
Source,Target,Weight,Type,stage,source_lane,target_lane,channel_id
scb:1:5,sc:0,512,tensor<1x8x1x16xf32>,spatial3,5,,0
scb:1:5,sc:0,512,tensor<1x8x1x16xf32>,spatial4,5,,0
1 Source Target Weight Type stage source_lane target_lane channel_id
2 scb:1:5 sc:0 512 tensor<1x8x1x16xf32> spatial3 spatial4 5 0
@@ -79,8 +79,8 @@ def test_collapse_reconstructs_every_boundary(regression_output: Path):
assert all(edge["representation"] == "raw" for edge in graph["edges"])
def test_spatial3_includes_isolated_nodes_and_has_distinct_positions(regression_output: Path):
graph = project(regression_output, "spatial3_scheduled", view="raw")
def test_spatial4_includes_isolated_nodes_and_has_distinct_positions(regression_output: Path):
graph = project(regression_output, "spatial4_scheduled", view="raw")
assert graph["counts"] == {
"nodes": 16, "edges": 1, "isolated_nodes": 14,
"raw_nodes": 16, "aggregate_nodes": 0, "raw_edges": 1, "aggregate_edges": 0,
@@ -100,20 +100,20 @@ def test_header_only_edges_preserve_all_nodes(regression_output: Path):
def test_projection_cap_is_actionable_and_never_partial(regression_output: Path):
with pytest.raises(DisplayGraphTooLarge, match="Use an aggregate view or increase"):
project(regression_output, "spatial3_scheduled", view="raw", cap=16)
project(regression_output, "spatial4_scheduled", view="raw", cap=16)
client = TestClient(create_app(regression_output, expansion_cap=16))
response = client.get("/api/reports/spatial3_scheduled/display-graph?view=raw")
response = client.get("/api/reports/spatial4_scheduled/display-graph?view=raw")
assert response.status_code == 400
assert "safety cap" in response.json()["detail"] and "aggregate view" in response.json()["detail"]
def test_display_api_and_static_raw_interactions(regression_output: Path):
client = TestClient(create_app(regression_output))
assert client.get("/api/reports/spatial3_scheduled/display-graph?view=raw").json()["counts"]["nodes"] == 16
assert client.get("/api/reports/spatial4_scheduled/display-graph?view=raw").json()["counts"]["nodes"] == 16
index = client.get("/").text
script = client.get("/app.js").text
assert '<option value="raw">Raw instances</option>' in index
assert 'report?.stage === "spatial3"' in script
assert 'report?.stage === "spatial4"' in script
assert 'local.representation === "raw"' in script
reducer = script[script.index("function reduceEdge"):script.index("function applyFilters")]
assert "state.graph.source(id)" not in reducer
+1 -2
View File
@@ -40,7 +40,7 @@ def _format_command(cmd):
def compile_with_raptor(network_path, raptor_onnx_path: Path, output_base: Path,
crossbar_size, crossbar_count, core_count=None, pim_merge_scheduler="peft",
crossbar_size, crossbar_count, core_count=None,
pim_memory_report="none", raptor_extra_args=None, cwd=None, verbose=False,
reporter=None, timeout_sec=None):
# Define the arguments, with the possibility to set crossbar size and count
@@ -52,7 +52,6 @@ def compile_with_raptor(network_path, raptor_onnx_path: Path, output_base: Path,
"--EmitPimCodegen",
f"--crossbar-size={crossbar_size}",
f"--crossbar-count={crossbar_count}",
f"--pim-merge-scheduler={pim_merge_scheduler}",
]
if core_count is not None:
args.append(f"--core-count={core_count}")
+1 -3
View File
@@ -76,8 +76,6 @@ def main():
ap.add_argument("--crossbar-count", type=int, default=8)
ap.add_argument("--core-count", type=int, default=None,
help="Core count to pass to Raptor. Required for PIM validation.")
ap.add_argument("--pim-merge-scheduler", choices=("peft"), default="peft",
help="Scheduler used by the Spatial merge-compute-nodes pass.")
ap.add_argument("--pim-memory-report", choices=("none", "summary", "full"), default="none",
help="Emit a human-readable PIM memory planning report during codegen.")
ap.add_argument("--raptor-extra-arg", action="append", default=[],
@@ -149,7 +147,7 @@ def main():
result = validate_network(
onnx_path, a.raptor_path, a.onnx_include_dir, simulator_dir,
crossbar_size=a.crossbar_size, crossbar_count=a.crossbar_count, core_count=a.core_count,
pim_merge_scheduler=a.pim_merge_scheduler, pim_memory_report=a.pim_memory_report,
pim_memory_report=a.pim_memory_report,
raptor_extra_args=a.raptor_extra_arg,
command_timeout_seconds=a.command_timeout_seconds,
threshold=a.threshold,
+42 -23
View File
@@ -1,4 +1,5 @@
import json
import os
import numpy as np
import shutil
import sys
@@ -61,32 +62,37 @@ class ProgressReporter:
self.passed_models = 0
self.failed_models = 0
self.current_label = ""
self.enabled = sys.stdout.isatty() if enabled is None else enabled
self.enabled = (
sys.stdout.isatty() and "CODEX_CI" not in os.environ
if enabled is None else enabled
)
self.verbose = verbose
self.columns = shutil.get_terminal_size((100, 20)).columns
self.columns = max(1, shutil.get_terminal_size((100, 20)).columns)
self.suspended = False
self.rendered_width = 0
self.rendered_rows = 0
def _clear(self):
if self.enabled:
sys.stdout.write("\r" + (" " * self.rendered_width) + "\r")
if self.enabled and self.rendered_rows:
columns = max(1, shutil.get_terminal_size((100, 20)).columns)
rows = max(self.rendered_rows, (self.rendered_width + columns - 1) // columns)
sys.stdout.write("\r\033[2K")
for _ in range(rows - 1):
sys.stdout.write("\033[1A\r\033[2K")
sys.stdout.flush()
self.rendered_width = 0
self.rendered_rows = 0
def _render(self):
if not self.enabled or self.suspended:
return
bar_width = 24
self.columns = max(1, shutil.get_terminal_size((100, 20)).columns)
bar_width = min(24, max(4, self.columns - 24))
filled = int(bar_width * self.completed_steps / self.total_steps)
counts_text = f"P:{self.passed_models} F:{self.failed_models}"
prefix_text = f"[{'#' * filled}{'-' * (bar_width - filled)}] {self.completed_steps}/{self.total_steps}"
if len(prefix_text) > self.columns:
prefix_text = f"{self.completed_steps}/{self.total_steps}"
if prefix_text.startswith("["):
bar = Fore.GREEN + ("#" * filled) + Fore.CYAN + ("-" * (bar_width - filled))
prefix = Fore.CYAN + f"[{bar}{Fore.CYAN}] {self.completed_steps}/{self.total_steps}" + Style.RESET_ALL
else:
prefix = Fore.CYAN + prefix_text + Style.RESET_ALL
bar = Fore.GREEN + ("#" * filled) + Fore.CYAN + ("-" * (bar_width - filled))
prefix = Fore.CYAN + f"[{bar}{Fore.CYAN}] {self.completed_steps}/{self.total_steps}" + Style.RESET_ALL
counts = (
" "
@@ -109,14 +115,28 @@ class ProgressReporter:
elif self.current_label:
label = f" {self.current_label}"
available_label_width = max(0, self.columns - len(prefix_text) - len(model_counter) - len(counts_text) - 3)
fixed_width = len(prefix_text) + len(model_counter) + len(counts_text) + 2
if fixed_width > self.columns:
model_counter = ""
fixed_width = len(prefix_text) + len(counts_text) + 2
if fixed_width > self.columns:
prefix_text = f"{self.completed_steps}/{self.total_steps}"
prefix = Fore.CYAN + prefix_text + Style.RESET_ALL
fixed_width = len(prefix_text) + len(counts_text) + 2
if fixed_width > self.columns:
counts = ""
counts_text = ""
fixed_width = len(prefix_text) + 1
available_label_width = max(0, self.columns - fixed_width)
label = label[:available_label_width]
plain_line = prefix_text + model_counter + f" P:{self.passed_models} F:{self.failed_models}" + label
plain_counts = f" {counts_text}" if counts_text else ""
plain_line = prefix_text + model_counter + plain_counts + label
rendered_line = prefix + model_counter + counts + label + Style.RESET_ALL
padded_width = max(self.rendered_width, len(plain_line))
sys.stdout.write("\r" + rendered_line + (" " * max(0, padded_width - len(plain_line))))
self._clear()
sys.stdout.write(rendered_line)
sys.stdout.flush()
self.rendered_width = len(plain_line)
self.rendered_rows = max(1, (self.rendered_width + self.columns - 1) // self.columns)
def log(self, message="", color=None):
if not self.verbose:
@@ -158,7 +178,6 @@ class ProgressReporter:
if self.enabled:
self.suspended = True
self._clear()
self.rendered_width = 0
def run_command(cmd, cwd=None, reporter=None, timeout_sec=None):
@@ -293,7 +312,7 @@ def validate_outputs(sim_arrays, runner_out_dir, outputs_descriptor, threshold=1
def validate_network(network_onnx_path, raptor_path, onnx_include_dir,
simulator_dir, crossbar_size=64, crossbar_count=8, core_count=None,
pim_merge_scheduler="peft", pim_memory_report="none", raptor_extra_args=None,
pim_memory_report="none", raptor_extra_args=None,
threshold=1e-3, rtol=1e-5,
seed=0, reporter=None, model_index=1, model_total=1, verbose=False,
command_timeout_seconds=60.0, mode=MODE_FULL):
@@ -346,8 +365,8 @@ def validate_network(network_onnx_path, raptor_path, onnx_include_dir,
if mode == MODE_COMPILE_ONLY:
print_stage(reporter, model_index, model_total, network_onnx_path.name, "Compile PIM")
pim_pass_timings = compile_with_raptor(
network_mlir_path, raptor_path, pim_output_base, crossbar_size, crossbar_count,
core_count=core_count, pim_merge_scheduler=pim_merge_scheduler,
network_onnx_path, raptor_path, pim_output_base, crossbar_size, crossbar_count,
core_count=core_count,
pim_memory_report=pim_memory_report, raptor_extra_args=raptor_extra_args,
cwd=raptor_dir, verbose=verbose, reporter=reporter, timeout_sec=command_timeout_seconds)
print_info(reporter, f"PIM artifacts saved to {raptor_dir / 'pim'}")
@@ -386,8 +405,8 @@ def validate_network(network_onnx_path, raptor_path, onnx_include_dir,
if mode != MODE_RUN_ONLY:
print_stage(reporter, model_index, model_total, network_onnx_path.name, "Compile PIM")
pim_pass_timings = compile_with_raptor(
network_mlir_path, raptor_path, pim_output_base, crossbar_size, crossbar_count,
core_count=core_count, pim_merge_scheduler=pim_merge_scheduler,
network_onnx_path, raptor_path, pim_output_base, crossbar_size, crossbar_count,
core_count=core_count,
pim_memory_report=pim_memory_report, raptor_extra_args=raptor_extra_args,
cwd=raptor_dir, verbose=verbose, reporter=reporter, timeout_sec=command_timeout_seconds)
print_info(reporter, f"PIM artifacts saved to {raptor_dir / 'pim'}")