#include #ifdef NDEBUG #error "Spatial scheduling tests require assertions" #endif #include #include #include #include "mlir/IR/BuiltinOps.h" #include "mlir/Dialect/Tensor/IR/Tensor.h" #include "mlir/Dialect/Linalg/IR/Linalg.h" #include "mlir/Dialect/Func/IR/FuncOps.h" #include "mlir/Dialect/SCF/IR/SCF.h" #include "mlir/Dialect/MemRef/IR/MemRef.h" #include "mlir/IR/Verifier.h" #include "mlir/Pass/PassManager.h" #include "src/Accelerators/PIM/Passes/PIMPasses.h" #include "src/Accelerators/PIM/Dialect/Pim/PimOps.hpp" #include "src/Accelerators/PIM/Dialect/Spatial/Passes/Transforms/MergeComputeNodes/DeferredCommunicationScheduling.hpp" #include "src/Accelerators/PIM/Dialect/Spatial/Passes/Transforms/MergeComputeNodes/DeferredBoundaryPlanning.hpp" #include "src/Accelerators/PIM/Dialect/Spatial/Passes/Transforms/MergeComputeNodes/DeferredTransferPlanning.hpp" #include "src/Accelerators/PIM/Dialect/Spatial/Passes/Transforms/MergeComputeNodes/ScheduledComputeMaterialization.hpp" #include "src/Accelerators/PIM/Dialect/Spatial/Passes/Transforms/MergeComputeNodes/DeferredCommunicationRealization.hpp" #include "src/Accelerators/PIM/Conversion/ONNXToSpatial/Common/ComputeRegionBuilder.hpp" #include "src/Accelerators/PIM/Dialect/Spatial/Passes/Transforms/MergeComputeNodes/Scheduling/PeftScheduler.hpp" #include "src/Accelerators/PIM/Dialect/Spatial/Passes/Transforms/MergeComputeNodes/Scheduling/MergeSchedulingAnalysis.hpp" #include "src/Accelerators/PIM/Dialect/Spatial/Passes/Transforms/MergeComputeNodes/Scheduling/PipelineScheduling.hpp" #include "src/Accelerators/PIM/Dialect/Spatial/Passes/Transforms/MergeComputeNodes/Scheduling/ResidentWeightPacking.hpp" #include "src/Accelerators/PIM/Dialect/Spatial/Passes/Transforms/MergeComputeNodes/Scheduling/ScheduleEvaluation.hpp" #include "src/Accelerators/PIM/Dialect/Spatial/Passes/Transforms/MergeComputeNodes/SpatialDataflowCsvExporter.hpp" using namespace onnx_mlir::spatial; static ComputeGraph buildCheckedComputeGraph(mlir::Operation *operation, const SchedulingTarget &target, size_t partitions = 0) { auto graph = buildComputeGraph(operation, target, partitions); assert(mlir::succeeded(graph)); return std::move(*graph); } static MergeScheduleResult runCheckedPeftScheduler(const ComputeGraph &graph, const PeftScheduleOptions &options) { std::string error; auto schedule = runPeftScheduler(graph, options, error); assert(mlir::succeeded(schedule)); return std::move(*schedule); } static void testLocalFragmentInsertion() { using namespace mlir; MLIRContext context(MLIRContext::Threading::DISABLED); context.loadDialect(); OwningOpRef module = ModuleOp::create(UnknownLoc::get(&context)); OpBuilder builder(module->getBodyRegion()); auto loc = module->getLoc(); auto check = [&](ArrayRef sourceShape, ArrayRef destShape, ArrayRef offsets, ArrayRef sizes, ArrayRef strides, bool view) { Value source = tensor::EmptyOp::create(builder, loc, sourceShape, builder.getF32Type()); Value dest = tensor::EmptyOp::create(builder, loc, destShape, builder.getF32Type()); auto mixed = [&](ArrayRef values) { SmallVector result; for (int64_t value : values) result.push_back(builder.getIndexAttr(value)); return result; }; Value result = insertLocalFragment(builder, loc, source, dest, mixed(offsets), mixed(sizes), mixed(strides)); assert(isa(result.getDefiningOp()) == view); assert(succeeded(verify(result.getDefiningOp()))); }; check({4}, {1, 4}, {0, 0}, {1, 4}, {1, 1}, true); check({2}, {1, 4}, {0, 0}, {1, 2}, {1, 1}, false); check({2}, {1, 4}, {0, 2}, {1, 2}, {1, 1}, false); check({2}, {1, 4}, {0, 0}, {1, 2}, {1, 2}, false); check({4}, {2, 4}, {1, 0}, {1, 4}, {1, 1}, false); check({}, {1}, {0}, {1}, {1}, true); check({}, {}, {}, {}, {}, false); check({1, 4}, {1, 4}, {0, 0}, {1, 4}, {1, 1}, false); } static void testBatchedReceiveForwarding() { using namespace mlir; using namespace onnx_mlir; for (bool genericCopy : {false, true}) for (unsigned scenario : {0, 1, 2}) { bool barrier = scenario == 1; bool nonzeroReceive = scenario == 2; if (genericCopy && nonzeroReceive) continue; MLIRContext context(MLIRContext::Threading::DISABLED); context.loadDialect(); OwningOpRef module = ModuleOp::create(UnknownLoc::get(&context)); OpBuilder builder(module->getBodyRegion()); auto loc = module->getLoc(); auto function = func::FuncOp::create(builder, loc, "main_graph", builder.getFunctionType({}, {})); builder.setInsertionPointToStart(function.addEntryBlock()); auto core = pim::PimCoreOp::create(builder, loc, ValueRange{}, builder.getI32IntegerAttr(0)); auto *body = new Block; core.getBody().push_back(body); builder.setInsertionPointToStart(body); auto type = MemRefType::get({4}, builder.getF32Type()); Value target = memref::AllocOp::create(builder, loc, MemRefType::get({8}, builder.getF32Type())); Value temporary = memref::AllocOp::create(builder, loc, nonzeroReceive ? MemRefType::get({8}, builder.getF32Type()) : type); Value table = memref::AllocOp::create(builder, loc, MemRefType::get({1}, builder.getIndexType())); Value zero = arith::ConstantIndexOp::create(builder, loc, 0); Value receiveOffset = arith::ConstantIndexOp::create( builder, loc, nonzeroReceive ? 4 : 0); auto received = pim::PimReceiveOp::create(builder, loc, temporary.getType(), temporary, receiveOffset, builder.getI32IntegerAttr(16), zero); Value offset = memref::LoadOp::create(builder, loc, table, ValueRange{zero}); if (barrier) pim::PimSyncOp::create(builder, loc, zero, zero); if (genericCopy) { auto destination = memref::SubViewOp::create(builder, loc, target, SmallVector{offset}, SmallVector{builder.getIndexAttr(4)}, SmallVector{builder.getIndexAttr(1)}); memref::CopyOp::create(builder, loc, received.getOutput(), destination); } else pim::PimMemCopyOp::create(builder, loc, target.getType(), offset, zero, target, received.getOutput(), builder.getI32IntegerAttr(16)); pim::PimSendOp::create(builder, loc, target, builder.getI32IntegerAttr(16), zero); pim::PimHaltOp::create(builder, loc); builder.setInsertionPointAfter(core); func::ReturnOp::create(builder, loc); assert(succeeded(verify(*module))); PassManager manager(&context); manager.addPass(createPimMemoryNormalizationPass()); assert(succeeded(manager.run(*module))); unsigned copies = 0, receives = 0; core.walk([&](pim::PimMemCopyOp) { ++copies; }); core.walk([&](pim::PimReceiveOp receive) { ++receives; assert(receive.getOutputBuffer() == (barrier || nonzeroReceive ? temporary : target)); if (!barrier && !nonzeroReceive && !genericCopy) assert(receive.getOutputOffset() == offset); }); assert(receives == 1 && copies == static_cast(barrier || nonzeroReceive)); } } static void testResidentWeightViewCost() { mlir::MLIRContext context(mlir::MLIRContext::Threading::DISABLED); context.loadDialect(); mlir::Block operands; mlir::OwningOpRef owner = mlir::ModuleOp::create(mlir::UnknownLoc::get(&context)); mlir::OpBuilder builder(&context); auto loc = owner->getLoc(); auto weightType = mlir::RankedTensorType::get({128, 128}, builder.getF32Type()); auto inputType = mlir::RankedTensorType::get({1, 128}, builder.getF32Type()); auto weight = operands.addArgument(weightType, loc); auto input = operands.addArgument(inputType, loc); auto wideType = mlir::RankedTensorType::get({1, 256}, builder.getF32Type()); auto wideInput = operands.addArgument(wideType, loc); auto makeCompute = [&](bool weightView, unsigned inputView, int64_t iterations = 0) { builder.setInsertionPointToEnd(owner->getBody()); auto compute = SpatGraphCompute::create(builder, loc, mlir::TypeRange{inputType}, mlir::ValueRange{weight}, mlir::ValueRange{inputView == 2 ? wideInput : input}); auto *block = new mlir::Block(); compute.getBody().push_back(block); mlir::Value w = block->addArgument(weightType, loc); mlir::Value x = block->addArgument(inputView == 2 ? wideType : inputType, loc); builder.setInsertionPointToStart(block); auto slice = [&](mlir::Value value, mlir::RankedTensorType type, bool strided = false) { llvm::SmallVector offsets(2, builder.getIndexAttr(0)); llvm::SmallVector strides(2, builder.getIndexAttr(1)); if (strided) strides.back() = builder.getIndexAttr(2); llvm::SmallVector sizes; for (int64_t size : type.getShape()) sizes.push_back(builder.getIndexAttr(size)); return mlir::tensor::ExtractSliceOp::create( builder, loc, type, value, offsets, sizes, strides).getResult(); }; if (weightView) w = slice(w, weightType); if (inputView) x = slice(x, inputType, inputView == 2); mlir::scf::ForOp loop; if (iterations) { auto lower = mlir::arith::ConstantIndexOp::create(builder, loc, 0); auto upper = mlir::arith::ConstantIndexOp::create(builder, loc, iterations); auto step = mlir::arith::ConstantIndexOp::create(builder, loc, 1); loop = mlir::scf::ForOp::create(builder, loc, lower, upper, step, mlir::ValueRange{x}); builder.setInsertionPointToStart(loop.getBody()); x = loop.getRegionIterArg(0); } mlir::Value result = SpatVMMOp::create(builder, loc, inputType, w, x); if (loop) { mlir::scf::YieldOp::create(builder, loc, mlir::ValueRange{result}); builder.setInsertionPointAfter(loop); result = loop.getResult(0); } SpatYieldOp::create(builder, loc, mlir::ValueRange{result}); return ComputeInstance{compute, 0, 1}; }; auto direct = makeCompute(false, false); auto weightView = makeCompute(true, false); auto inputView = makeCompute(false, true); auto stridedInput = makeCompute(false, 2); auto invariantLoop = makeCompute(false, false, 1000000000); assert(collectDistinctResidentWeights(invariantLoop.op, 0).size() == 1); Cost loopBodyCost = getComputeInstanceCost(invariantLoop, SchedulingTarget{}) / 1000000000; auto loop = *mlir::cast(invariantLoop.op) .getBody().front().getOps().begin(); builder.setInsertionPoint(loop); auto lower = mlir::arith::ConstantIndexOp::create(builder, loc, std::numeric_limits::min()); auto upper = mlir::arith::ConstantIndexOp::create(builder, loc, std::numeric_limits::max()); auto step = mlir::arith::ConstantIndexOp::create(builder, loc, std::numeric_limits::max()); loop.getLowerBoundMutable().assign(lower.getResult()); loop.getUpperBoundMutable().assign(upper.getResult()); loop.getStepMutable().assign(step.getResult()); assert(getComputeInstanceCost(invariantLoop, SchedulingTarget{}) == 3 * loopBodyCost); for (Cost readLatency : {1, 20}) { SchedulingTarget target; target.localMemoryReadLatencyCycles = readLatency; target.localMemoryWriteLatencyCycles = 3; target.localMemoryWidthBytes = 8; target.processorPeriodNs = 1; target.computeBitwidth = 8; assert(getComputeInstanceCost(weightView, target) == getComputeInstanceCost(direct, target)); assert(getComputeInstanceCost(inputView, target) == getComputeInstanceCost(direct, target)); assert(getComputeInstanceCost(stridedInput, target) == getComputeInstanceCost(direct, target) + 16 * (readLatency + 3)); Cost singleRead = getComputeInstanceCost(direct, target); target.matrixInputReadMultiplier = 8; assert(getComputeInstanceCost(direct, target) == singleRead + 112 * readLatency); } } static void testTransposeCost() { using namespace mlir; MLIRContext context(MLIRContext::Threading::DISABLED); context.loadDialect(); OwningOpRef module = ModuleOp::create(UnknownLoc::get(&context)); OpBuilder builder(module->getBodyRegion()); auto loc = module->getLoc(); for (int64_t rows : {1, 4, 8}) { builder.setInsertionPointToEnd(module->getBody()); auto inputType = RankedTensorType::get({rows, 16}, builder.getF32Type()); auto outputType = RankedTensorType::get({16, rows}, builder.getF32Type()); auto function = func::FuncOp::create(builder, loc, "transpose_" + std::to_string(rows), builder.getFunctionType({inputType}, {outputType})); builder.setInsertionPointToStart(function.addEntryBlock()); auto compute = onnx_mlir::createEmptySpatGraphCompute(builder, loc, TypeRange{outputType}, ValueRange{}, ValueRange{function.getArgument(0)}); Value init = tensor::EmptyOp::create(builder, loc, outputType.getShape(), outputType.getElementType()); Value result = linalg::TransposeOp::create(builder, loc, *compute.getInputArgument(0), init, SmallVector{1, 0}).getResult()[0]; SpatYieldOp::create(builder, loc, ValueRange{result}); builder.setInsertionPointAfter(compute); func::ReturnOp::create(builder, loc, compute.getResults()); assert(succeeded(verify(function))); for (Cost period : {1, 5}) { SchedulingTarget target; target.computeBitwidth = period == 1 ? 8 : 16; target.localMemoryWidthBytes = 8; target.localMemoryReadLatencyCycles = 2; target.localMemoryWriteLatencyCycles = 3; target.processorPeriodNs = period; target.vectorWidth = 16; target.vectorLatencyCycles = 4; Cost runBytes = rows * target.computeBitwidth / 8; Cost expected = rows == 1 ? 16 * target.computeBitwidth / 8 / 8 * (2 + 3) * period : 16 * ((runBytes + 7) / 8 * (2 + 3) + 4) * period; assert(getComputeInstanceCost({compute, 0, 1}, target) == expected); Cost bytes = rows * 16 * target.computeBitwidth / 8; assert(getComputeInstanceCost({compute, 0, 1}, target, ComputeCostKind::VolumePriority) == bytes / 8 * (2 + 3) * period); target.processorCount = 4; target.residentWeightCapacity = 1; target.interProcessorLatencyNs.assign(16, 0); auto graph = buildCheckedComputeGraph(function, target); MergeScheduleResult schedule; std::string error; assert(succeeded(runPipelineScheduler(graph, schedule, 2, target, error))); assert(schedule.costs.totalWorkNs == expected); assert(schedule.costs.maximumCoreWorkNs == expected); } } } static void testConcatCost() { using namespace mlir; MLIRContext context(MLIRContext::Threading::DISABLED); context.loadDialect(); OwningOpRef module = ModuleOp::create(UnknownLoc::get(&context)); OpBuilder builder(module->getBodyRegion()); auto loc = module->getLoc(); for (int64_t axis : {0, 1, 2}) { SmallVector shape{2, 3, 5}; auto inputType = RankedTensorType::get(shape, builder.getF32Type()); shape[axis] *= 2; auto outputType = RankedTensorType::get(shape, builder.getF32Type()); builder.setInsertionPointToEnd(module->getBody()); auto function = func::FuncOp::create(builder, loc, "concat_" + std::to_string(axis), builder.getFunctionType({inputType, inputType}, {outputType})); builder.setInsertionPointToStart(function.addEntryBlock()); auto compute = onnx_mlir::createEmptySpatGraphCompute(builder, loc, TypeRange{outputType}, ValueRange{}, function.getArguments()); Value result = SpatConcatOp::create(builder, loc, outputType, axis, ValueRange{*compute.getInputArgument(0), *compute.getInputArgument(1)}); SpatYieldOp::create(builder, loc, ValueRange{result}); builder.setInsertionPointAfter(compute); func::ReturnOp::create(builder, loc, compute.getResults()); assert(succeeded(verify(function))); for (Cost period : {1, 5}) { SchedulingTarget target; target.computeBitwidth = period == 1 ? 8 : 16; target.localMemoryWidthBytes = 8; target.localMemoryReadLatencyCycles = 2; target.localMemoryWriteLatencyCycles = 3; target.processorPeriodNs = period; Cost blocks = axis == 0 ? 1 : axis == 1 ? 2 : 6; Cost bytes = 30 / blocks * target.computeBitwidth / 8; Cost expected = 2 * blocks * ((bytes + 7) / 8) * (2 + 3) * period; assert(getComputeInstanceCost({compute, 0, 1}, target) == expected); } } } static void testScalarProjectedTransferCost() { using namespace mlir; MLIRContext context(MLIRContext::Threading::DISABLED); context.loadDialect(); OwningOpRef module = ModuleOp::create(UnknownLoc::get(&context)); OpBuilder builder(module->getBodyRegion()); auto loc = module->getLoc(); auto imageType = RankedTensorType::get({1024}, builder.getF32Type()); auto fragment = RankedTensorType::get({1}, builder.getF32Type()); auto function = func::FuncOp::create(builder, loc, "scalar_projection", builder.getFunctionType({imageType}, {})); auto *body = function.addEntryBlock(); builder.setInsertionPointToStart(body); auto scalar = onnx_mlir::createEmptySpatGraphCompute(builder, loc, TypeRange{imageType}, ValueRange{}, ValueRange{body->getArgument(0)}); builder.setInsertionPointToEnd(&scalar.getBody().front()); Value image = SpatReluOp::create(builder, loc, imageType, *scalar.getInputArgument(0)); SpatYieldOp::create(builder, loc, ValueRange{image}); builder.setInsertionPointAfter(scalar); for (int64_t trips : {1, 3}) { auto batch = onnx_mlir::createEmptySpatGraphComputeBatch(builder, loc, TypeRange{RankedTensorType::get({2, 1}, builder.getF32Type())}, 2, ValueRange{}, ValueRange{scalar.getResult(0)}); assert(succeeded(batch)); builder.setInsertionPointToEnd(&batch->getBody().front()); Value zero = arith::ConstantIndexOp::create(builder, loc, 0); Value one = arith::ConstantIndexOp::create(builder, loc, 1); Value limit = arith::ConstantIndexOp::create(builder, loc, trips); Value initial = tensor::EmptyOp::create(builder, loc, fragment.getShape(), fragment.getElementType()); auto loop = scf::ForOp::create(builder, loc, zero, limit, one, ValueRange{initial}, [&](OpBuilder &nested, Location nestedLoc, Value iv, ValueRange) { Value offset = arith::AddIOp::create(nested, nestedLoc, iv, *batch->getLaneArgument()); Value selected = tensor::ExtractSliceOp::create(nested, nestedLoc, fragment, *batch->getInputArgument(0), SmallVector{offset}, SmallVector{nested.getIndexAttr(1)}, SmallVector{nested.getIndexAttr(1)}); Value result = SpatReluOp::create(nested, nestedLoc, fragment, selected); scf::YieldOp::create(nested, nestedLoc, ValueRange{result}); }); auto parallel = SpatInParallelOp::create(builder, loc); builder.setInsertionPointToEnd(¶llel.getRegion().front()); tensor::ParallelInsertSliceOp::create(builder, loc, loop.getResult(0), *batch->getOutputArgument(0), SmallVector{*batch->getLaneArgument(), builder.getIndexAttr(0)}, SmallVector(2, builder.getIndexAttr(1)), SmallVector(2, builder.getIndexAttr(1))); builder.setInsertionPointAfter(*batch); } func::ReturnOp::create(builder, loc); assert(succeeded(verify(*module))); SchedulingTarget target; target.processorCount = 2; for (size_t partitions : {1, 2}) { ComputeGraph graph = buildCheckedComputeGraph(function, target, partitions); for (const auto &edge : graph.edges) { auto instance = graph.nodes[edge.target].instance; if (instance.op == scalar) continue; auto batch = cast(instance.op); auto loop = *batch.getBody().front().getOps().begin(); Cost trips = static_cast( loop.getUpperBound().getDefiningOp().value()); assert(edge.transferCost.fixed == 2 * trips * instance.laneCount); assert(edge.transferCost.networkFlits == 3 * trips * instance.laneCount); } } } static void testStridedPublicationMemory() { using namespace mlir; MLIRContext context(MLIRContext::Threading::DISABLED); context.loadDialect(); auto module = ModuleOp::create(UnknownLoc::get(&context)); OpBuilder builder(module.getBodyRegion()); auto loc = module.getLoc(); auto image = RankedTensorType::get({2, 16}, builder.getF32Type()); auto fragment = RankedTensorType::get({2, 8}, builder.getF32Type()); auto function = func::FuncOp::create(builder, loc, "strided_publication", builder.getFunctionType({image}, {})); builder.setInsertionPointToStart(function.addEntryBlock()); auto producer = onnx_mlir::createEmptySpatGraphCompute(builder, loc, TypeRange{image}, ValueRange{}, ValueRange{function.getArgument(0)}); builder.setInsertionPointToEnd(&producer.getBody().front()); Value value = SpatReluOp::create(builder, loc, image, *producer.getInputArgument(0)); SpatYieldOp::create(builder, loc, ValueRange{value}); builder.setInsertionPointAfter(producer); for (unsigned reader = 0; reader < 2; ++reader) { auto consumer = onnx_mlir::createEmptySpatGraphCompute(builder, loc, TypeRange{fragment}, ValueRange{}, ValueRange{producer.getResult(0)}); builder.setInsertionPointToEnd(&consumer.getBody().front()); value = tensor::ExtractSliceOp::create(builder, loc, fragment, *consumer.getInputArgument(0), SmallVector(2, builder.getIndexAttr(0)), SmallVector{builder.getIndexAttr(2), builder.getIndexAttr(8)}, SmallVector(2, builder.getIndexAttr(1))); value = SpatReluOp::create(builder, loc, fragment, value); SpatYieldOp::create(builder, loc, ValueRange{value}); builder.setInsertionPointAfter(consumer); } func::ReturnOp::create(builder, loc); for (Cost width : {4, 32}) { SchedulingTarget target; target.processorCount = 3; target.sharedMemoryWidthBytes = width; target.sharedMemoryReadLatencyNs = 7; target.sharedMemoryWriteLatencyNs = 11; auto graph = buildCheckedComputeGraph(function, target); auto demand = getPipelineBoundaryMemoryDemand(graph, {0, 1, 1}, target); assert(demand.readBytes == 32 && demand.writeBytes == 32); assert(demand.readRequests == 2 && demand.writeRequests == 4); assert(demand.serviceNs == (width == 4 ? 144 : 58)); } module.erase(); } static void testProducerChunkTransferCost() { using namespace mlir; MLIRContext context(MLIRContext::Threading::DISABLED); context.loadDialect(); OwningOpRef module = ModuleOp::create(UnknownLoc::get(&context)); OpBuilder builder(module->getBodyRegion()); auto loc = module->getLoc(); for (unsigned bits : {1, 3, 4, 8, 16}) for (int64_t elements : {0, 1, 3, 8}) { builder.setInsertionPointToEnd(module->getBody()); auto fragment = RankedTensorType::get({elements}, builder.getIntegerType(bits)); auto physical = getGraphBatchPhysicalResultType(3, fragment); auto function = func::FuncOp::create(builder, loc, "transfer_" + std::to_string(bits) + "_" + std::to_string(elements), builder.getFunctionType({fragment}, {})); builder.setInsertionPointToStart(function.addEntryBlock()); auto producer = onnx_mlir::createEmptySpatGraphComputeBatch(builder, loc, TypeRange{physical}, 3, ValueRange{}, ValueRange{function.getArgument(0)}); assert(succeeded(producer)); auto parallel = SpatInParallelOp::create(builder, loc); builder.setInsertionPointToEnd(¶llel.getRegion().front()); tensor::ParallelInsertSliceOp::create(builder, loc, *producer->getInputArgument(0), *producer->getOutputArgument(0), SmallVector{*producer->getLaneArgument(), builder.getIndexAttr(0)}, SmallVector{builder.getIndexAttr(1), builder.getIndexAttr(elements)}, SmallVector(2, builder.getIndexAttr(1))); builder.setInsertionPointAfter(*producer); auto consumer = onnx_mlir::createEmptySpatGraphCompute(builder, loc, TypeRange{physical}, ValueRange{}, ValueRange{producer->getResult(0)}); SpatYieldOp::create(builder, loc, ValueRange{*consumer.getInputArgument(0)}); builder.setInsertionPointAfter(consumer); func::ReturnOp::create(builder, loc); SchedulingTarget target; target.processorCount = 3; target.computeBitwidth = 16; target.localMemoryWidthBytes = target.transferWidthBytes = 1; if (elements == 0) { // MLIR rejects physical insertions into a zero-sized fragment dimension. ScopedDiagnosticHandler handler(&context, [](Diagnostic &) { return success(); }); assert(failed(verify(function))); auto empty = getTransferCostFromBytes(0, target); assert(empty.fixed == 0 && empty.networkFlits == 2); function.erase(); continue; } assert(succeeded(verify(function))); for (size_t partitions : {1, 2, 3}) { auto graph = buildCheckedComputeGraph(function, target, partitions); const auto &edges = graph.predecessors[graph.instanceToIndex.lookup({consumer, 0, 1})]; assert(edges.size() == partitions); for (const auto &[task, transfer] : edges) { Cost selectedBits = elements * bits * graph.nodes[task].instance.laneCount; Cost bytes = (selectedBits + 7) / 8; assert(transfer.fixed == 2 * bytes); assert(transfer.networkFlits == 2 + bytes); } } } } static void testPipelineStageSpan() { using namespace mlir; MLIRContext context(MLIRContext::Threading::DISABLED); context.loadDialect(); OwningOpRef owner = ModuleOp::create(UnknownLoc::get(&context)); OpBuilder builder(owner->getBodyRegion()); ComputeGraph graph; graph.successors.resize(12); graph.predecessors.resize(12); for (size_t group = 0; group < 6; ++group) { auto batch = SpatGraphComputeBatch::create(builder, owner->getLoc(), TypeRange{}, builder.getI32IntegerAttr(2), ValueRange{}, ValueRange{}); for (uint32_t lane = 0; lane < 2; ++lane) { size_t task = graph.nodes.size(); ComputeInstance instance{batch, lane, 1}; graph.nodes.push_back({instance, group == 0 ? 40ULL : 10ULL, {}, task}); graph.instanceToIndex[instance] = task; if (group != 0) for (size_t predecessor = task - lane - 2; predecessor < task - lane; ++predecessor) { graph.predecessors[task].push_back({predecessor, {}}); graph.successors[predecessor].push_back({task, {}}); } } } for (size_t coreCount : {6, 12, 20}) { SchedulingTarget target; target.processorCount = coreCount; target.residentWeightCapacity = 1; target.interProcessorLatencyNs.assign(coreCount * coreCount, 0); MergeScheduleResult schedule; std::string error; assert(succeeded(runPipelineScheduler(graph, schedule, 2, target, error))); MergeScheduleResult repeated; assert(succeeded(runPipelineScheduler(graph, repeated, 2, target, error))); assert(schedule.processorStages == repeated.processorStages); assert(schedule.computeToCpuMap == repeated.computeToCpuMap); assert(schedule.computeToCpuSlotMap == repeated.computeToCpuSlotMap); for (size_t task = 0; task < graph.nodes.size(); ++task) for (const auto &[parent, transfer] : graph.predecessors[task]) { const auto &source = graph.nodes[parent].instance; const auto &destination = graph.nodes[task].instance; size_t from = schedule.computeToCpuMap.lookup(source); size_t to = schedule.computeToCpuMap.lookup(destination); assert(schedule.processorStages[from] <= schedule.processorStages[to]); assert(schedule.processorStages[to] <= schedule.processorStages[from] + 1); if (from == to) assert(schedule.computeToCpuSlotMap.lookup(source) < schedule.computeToCpuSlotMap.lookup(destination)); } Cost stageCosts[2] = {}; Cost stageWork[2] = {}; for (size_t task = 0; task < graph.nodes.size(); ++task) { size_t core = schedule.computeToCpuMap.lookup(graph.nodes[task].instance); size_t stage = schedule.processorStages[core]; stageWork[stage] += graph.nodes[task].cost; if (task % 2 == 0) stageCosts[stage] += graph.nodes[task].cost; } // Both 40/50 and 50/40 are optimal cuts for this 90-cycle chain. assert(std::max(stageCosts[0], stageCosts[1]) == 50); if (coreCount == 20) { assert(!schedule.equivalentClass.empty()); for (const auto &[core, peers] : schedule.equivalentClass) for (size_t peer : peers) { assert(peer != core); assert(schedule.processorStages[core] == schedule.processorStages[peer]); } for (size_t stage = 0; stage < 2; ++stage) assert(stageWork[stage] <= 10 * static_cast( llvm::count(schedule.processorStages, stage))); } } } static void testPipelinePackingCapacity() { using namespace mlir; MLIRContext context(MLIRContext::Threading::DISABLED); context.loadDialect(); OwningOpRef owner = ModuleOp::create(UnknownLoc::get(&context)); OpBuilder builder(owner->getBodyRegion()); auto batch = SpatGraphComputeBatch::create(builder, owner->getLoc(), TypeRange{}, builder.getI32IntegerAttr(4), ValueRange{}, ValueRange{}); ComputeGraph graph; graph.successors.resize(4); graph.predecessors.resize(4); for (uint32_t task = 0; task < 4; ++task) { ResidentWeightSet weights; for (uint32_t index = 0; index < (task == 3 ? 3u : 1u); ++index) { ResidentWeight weight; weight.opaqueLane = task == 3 ? index + 1 : 0; weights.push_back(weight); } ComputeInstance instance{batch, task, 1}; graph.nodes.push_back({instance, 10, weights, task}); graph.instanceToIndex[instance] = task; } SchedulingTarget target; target.processorCount = 4; target.residentWeightCapacity = 3; target.interProcessorLatencyNs.assign(16, 0); MergeScheduleResult schedule; std::string error; assert(succeeded(runPipelineScheduler(graph, schedule, 2, target, error))); std::vector loads(4); for (const auto &node : graph.nodes) { size_t core = schedule.computeToCpuMap.lookup(node.instance); assert(schedule.processorStages[core] == 0); loads[core] += node.cost; } assert(*std::max_element(loads.begin(), loads.end()) == 20); } static void testRepeatedResidentWeightPacking() { using namespace mlir; MLIRContext context(MLIRContext::Threading::DISABLED); context.loadDialect(); OwningOpRef owner = ModuleOp::create(UnknownLoc::get(&context)); OpBuilder builder(owner->getBodyRegion()); auto batch = SpatGraphComputeBatch::create(builder, owner->getLoc(), TypeRange{}, builder.getI32IntegerAttr(12), ValueRange{}, ValueRange{}); ComputeGraph graph; graph.predecessors.resize(12); graph.successors.resize(12); std::vector tasks; std::vector costs(12, 10); for (uint32_t task = 0; task < 12; ++task) { ResidentWeight weight; weight.opaqueLane = task % 2; graph.nodes.push_back({{batch, task, 1}, 10, {weight}, task}); tasks.push_back(task); } IndexedResidentWeights weights(graph); assert(weights.getPackCountLowerBound(tasks, 1) == 2); auto packing = weights.pack(tasks, costs, 1, 2); assert(packing); for (size_t task : tasks) assert((*packing)[task] == (*packing)[task % 2]); assert(!weights.pack(tasks, costs, 1, 1)); assert(weights.pack({}, costs, 1, 0)->empty()); for (size_t stages : {2, 4, 8}) for (Cost linkLatency : {0, 10}) { SchedulingTarget target; target.processorCount = stages + 1; target.residentWeightCapacity = 1; target.interProcessorLatencyNs.assign( target.processorCount * target.processorCount, linkLatency); for (size_t core = 0; core < target.processorCount; ++core) target.interProcessorLatencyNs[core * target.processorCount + core] = 0; MergeScheduleResult schedule; std::string error; assert(succeeded(runPipelineScheduler(graph, schedule, stages, target, error))); assert(schedule.dominanceOrderCompute.size() == graph.nodes.size()); std::vector residents(target.processorCount); for (const auto &node : graph.nodes) { size_t cpu = schedule.computeToCpuMap.lookup(node.instance); insertResidentWeights(residents[cpu], node.residentWeights); assert(residents[cpu].size() <= target.residentWeightCapacity); assert(schedule.processorStages[cpu] == 0); } } graph.nodes[0].residentWeights.push_back(graph.nodes[1].residentWeights.front()); IndexedResidentWeights oversized(graph); assert(oversized.getPackCountLowerBound(tasks, 1) == std::numeric_limits::max()); assert(!oversized.pack(tasks, costs, 1, 12)); } static void testResidentWeightPackingRecovery() { ComputeGraph graph; for (const auto &ids : std::vector>{ {0, 3}, {0, 1}, {0, 2}, {3, 4}}) { ResidentWeightSet residents; for (uint32_t id : ids) { ResidentWeight weight; weight.opaqueLane = id; residents.push_back(weight); } graph.nodes.push_back({{}, 10, residents, graph.nodes.size()}); } IndexedResidentWeights weights(graph); std::vector tasks{0, 1, 2, 3}; std::vector costs(4, 10); assert(weights.getPackCountLowerBound(tasks, 3) == 2); auto assignment = weights.pack(tasks, costs, 3, 2); assert(assignment && assignment == weights.pack(tasks, costs, 3, 2)); assert(*assignment == std::vector({0, 1, 1, 0})); assert((*assignment)[0] == (*assignment)[3]); assert((*assignment)[1] == (*assignment)[2]); assert((*assignment)[0] != (*assignment)[1]); std::vector residents(2); for (size_t task : tasks) { insertResidentWeights(residents[(*assignment)[task]], graph.nodes[task].residentWeights); assert(residents[(*assignment)[task]].size() <= 3); } assert(!weights.pack(tasks, costs, 3, 1)); } static void testResidentWeightPackingRepair() { ComputeGraph graph; for (const auto &ids : std::vector>{ {1, 3}, {3, 4}, {4, 5}, {2, 3}}) { ResidentWeightSet residents; for (uint32_t id : ids) { ResidentWeight weight; weight.opaqueLane = id; residents.push_back(weight); } graph.nodes.push_back({{}, 10, residents, graph.nodes.size()}); } IndexedResidentWeights weights(graph); std::vector tasks{0, 1, 2, 3}; std::vector costs(4, 10); auto assignment = weights.pack(tasks, costs, 3, 2); assert(assignment); assert((*assignment)[0] == (*assignment)[3]); assert((*assignment)[1] == (*assignment)[2]); assert((*assignment)[0] != (*assignment)[1]); assert(assignment == weights.pack(tasks, costs, 3, 2)); assert(!weights.pack(tasks, costs, 2, 2)); mlir::MLIRContext context(mlir::MLIRContext::Threading::DISABLED); mlir::OwningOpRef owner = mlir::ModuleOp::create(mlir::UnknownLoc::get(&context)); graph.predecessors.resize(graph.nodes.size()); graph.successors.resize(graph.nodes.size()); for (auto [task, node] : llvm::enumerate(graph.nodes)) node.instance = {owner->getOperation(), static_cast(task), 1}; SchedulingTarget target; target.processorCount = 2; target.residentWeightCapacity = 3; target.interProcessorLatencyNs.assign(4, 0); auto schedule = runCheckedPeftScheduler(graph, {target, &context}); assert(schedule.computeToCpuMap.lookup(graph.nodes[0].instance) == schedule.computeToCpuMap.lookup(graph.nodes[3].instance)); assert(schedule.computeToCpuMap.lookup(graph.nodes[1].instance) == schedule.computeToCpuMap.lookup(graph.nodes[2].instance)); assert(schedule.costs.maximumCoreWorkNs == 20); std::string error; target.residentWeightCapacity = 2; assert(mlir::failed(runPeftScheduler(graph, {target, &context}, error))); assert(error.find("lower bound exceeds physical capacity") != std::string::npos); // The union lower bound fits, but two cores cannot hold five distinct two-weight spokes. graph.nodes.clear(); for (uint32_t task = 0; task < 5; ++task) { ResidentWeight common, spoke; common.opaqueLane = 0; spoke.opaqueLane = task + 1; graph.nodes.push_back({{owner->getOperation(), task, 1}, 10, {common, spoke}, task}); } graph.predecessors.resize(5); graph.successors.resize(5); target.residentWeightCapacity = 3; IndexedResidentWeights impossible(graph); std::vector spokes{0, 1, 2, 3, 4}; assert(impossible.getPackCountLowerBound(spokes, 3) == 2); assert(!impossible.pack(spokes, std::vector(5, 10), 3, 2)); MergeSchedulingAnalysis analysis(graph, target, &context, error); assert(mlir::failed(analysis.getResult())); assert(error.find("heuristic search exhausted its bounded repair") != std::string::npos); target.residentWeightCapacity = 0; assert(mlir::failed(runPeftScheduler(graph, {target, &context}, error))); assert(error.find("must be positive") != std::string::npos); } static void testPhysicalPeft() { TransferCost transfer {.fixed = 50, .networkFlits = 4}; SchedulingTarget line; line.processorCount = 3; line.interProcessorLatencyNs = { 0, 1, 10, 1, 0, 1, 10, 1, 0, }; line.averageInterProcessorLatencyNs = 4; assert(getTransferTime(transfer, 0, 1, line) == 54); assert(getTransferTime(transfer, 0, 2, line) == 90); SchedulingTarget reversed = line; reversed.interProcessorLatencyNs = {0, 10, 1, 10, 0, 1, 1, 1, 0}; assert(getTransferTime(transfer, 0, 1, reversed) == 90); assert(getTransferTime(transfer, 0, 2, reversed) == 54); mlir::MLIRContext context(mlir::MLIRContext::Threading::DISABLED); mlir::OwningOpRef owner = mlir::ModuleOp::create(mlir::UnknownLoc::get(&context)); ComputeGraph chain; chain.successors.resize(3); chain.predecessors.resize(3); for (uint32_t task = 0; task < 3; ++task) { ComputeInstance instance {owner->getOperation(), task, 1}; ResidentWeight weight; weight.opaqueLane = task; chain.nodes.push_back({instance, 10, {weight}, task}); if (task != 0) { chain.successors[task - 1].push_back({task, transfer}); chain.predecessors[task].push_back({task - 1, transfer}); } } line.residentWeightCapacity = reversed.residentWeightCapacity = 1; for (const SchedulingTarget &target : {line, reversed}) { auto scheduled = runCheckedPeftScheduler(chain, {target, &context}); size_t firstCore = scheduled.computeToCpuMap.lookup(chain.nodes[0].instance); assert(firstCore == (target.interProcessorLatencyNs == line.interProcessorLatencyNs ? 1 : 2)); for (size_t task = 1; task < chain.nodes.size(); ++task) { auto previous = chain.nodes[task - 1].instance; auto current = chain.nodes[task].instance; size_t source = scheduled.computeToCpuMap.lookup(previous); size_t destination = scheduled.computeToCpuMap.lookup(current); assert(source != destination); assert(scheduled.computeToAestMap.lookup(current) == scheduled.computeToAestMap.lookup(previous) + 10 + getTransferTime(transfer, source, destination, target)); } } } static void testPeftReservationPassFailure() { using namespace mlir; MLIRContext context(MLIRContext::Threading::DISABLED); context.loadDialect(); OwningOpRef module = ModuleOp::create(UnknownLoc::get(&context)); OpBuilder builder(module->getBodyRegion()); auto loc = module->getLoc(); auto weight = RankedTensorType::get({128, 128}, builder.getF32Type()); auto vector = RankedTensorType::get({1, 128}, builder.getF32Type()); auto function = func::FuncOp::create(builder, loc, "main_graph", builder.getFunctionType({vector}, {vector})); builder.setInsertionPointToStart(function.addEntryBlock()); SmallVector weights; for (float value : {1.f, 2.f}) weights.push_back(arith::ConstantOp::create(builder, loc, DenseElementsAttr::get(weight, ArrayRef{value}))); auto compute = onnx_mlir::createEmptySpatGraphCompute(builder, loc, TypeRange{vector}, weights, ValueRange{function.getArgument(0)}); Value result = *compute.getInputArgument(0); for (unsigned index : {0, 1}) result = SpatVMMOp::create(builder, loc, vector, *compute.getWeightArgument(index), result); SpatYieldOp::create(builder, loc, ValueRange{result}); builder.setInsertionPointAfter(compute); func::ReturnOp::create(builder, loc, compute.getResults()); assert(succeeded(verify(*module))); SchedulingTarget target; target.processorCount = target.residentWeightCapacity = 1; target.interProcessorLatencyNs = {0}; std::string diagnostics; ScopedDiagnosticHandler handler(&context, [&](Diagnostic &diagnostic) { diagnostics += diagnostic.str(); return success(); }); PassManager manager(&context); manager.addPass(createScheduleAndRealizeSpatialPass(target, SpatialDataflowExportStage::None)); assert(failed(manager.run(*module))); assert(diagnostics.find("resident-weight lower bound exceeds physical capacity") != std::string::npos); } static void testPeftReservationCapacity() { mlir::MLIRContext context(mlir::MLIRContext::Threading::DISABLED); auto owner = mlir::ModuleOp::create(mlir::UnknownLoc::get(&context)); ComputeGraph graph; graph.successors.resize(4); graph.predecessors.resize(4); for (uint32_t task = 0; task < 4; ++task) { ResidentWeightSet weights; for (uint32_t index = 0; index < (task == 3 ? 4u : 1u); ++index) { ResidentWeight weight; weight.opaqueLane = task == 3 ? index + 1 : 0; weights.push_back(weight); } graph.nodes.push_back({{owner, task, 1}, 10, weights, task}); } SchedulingTarget target; target.processorCount = 3; target.residentWeightCapacity = 4; target.interProcessorLatencyNs.assign(9, 0); auto schedule = runCheckedPeftScheduler(graph, {target, &context}); size_t largeCore = schedule.computeToCpuMap.lookup(graph.nodes[3].instance); for (size_t task : {0, 1, 2}) assert(schedule.computeToCpuMap.lookup(graph.nodes[task].instance) != largeCore); assert(schedule.costs.maximumCoreWorkNs == 20); assert(schedule.costs.completionNs == 20); owner.erase(); } static void testPeftGapOrder() { mlir::MLIRContext context(mlir::MLIRContext::Threading::DISABLED); mlir::OwningOpRef owner = mlir::ModuleOp::create(mlir::UnknownLoc::get(&context)); ComputeGraph graph; graph.successors.resize(3); graph.predecessors.resize(3); for (uint32_t task = 0; task < 3; ++task) { ResidentWeight weight; weight.opaqueLane = task == 0 ? 0 : 1; graph.nodes.push_back({{owner->getOperation(), task, 1}, task == 2 ? 20u : 10u, {weight}, task}); } TransferCost transfer {.fixed = 100}; graph.successors[0].push_back({1, transfer}); graph.predecessors[1].push_back({0, transfer}); SchedulingTarget target; target.processorCount = 2; target.residentWeightCapacity = 1; target.interProcessorLatencyNs.assign(4, 0); auto schedule = runCheckedPeftScheduler(graph, {target, &context}); assert(schedule.computeToCpuSlotMap.lookup(graph.nodes[2].instance) == 0); assert(schedule.computeToCpuSlotMap.lookup(graph.nodes[1].instance) == 1); assert(schedule.computeToAestMap.lookup(graph.nodes[2].instance) == 0); } static void testMaterializedPeftGapOrder() { using namespace mlir; MLIRContext context(MLIRContext::Threading::DISABLED); context.loadDialect(); OwningOpRef owner = ModuleOp::create(UnknownLoc::get(&context)); OpBuilder builder(owner->getBodyRegion()); auto loc = owner->getLoc(); auto type = RankedTensorType::get({1}, builder.getF32Type()); auto function = func::FuncOp::create( builder, loc, "gap_order", builder.getFunctionType({type}, {})); Block *body = function.addEntryBlock(); builder.setInsertionPointToStart(body); ComputeGraph graph; graph.successors.resize(3); graph.predecessors.resize(3); for (uint32_t task = 0; task < 3; ++task) { Value input = body->getArgument(0); if (task == 1) input = graph.nodes[0].instance.op->getResult(0); auto compute = SpatGraphCompute::create( builder, loc, TypeRange{type}, ValueRange{}, ValueRange{input}); auto *block = new Block(); compute.getBody().push_back(block); Value argument = block->addArgument(type, loc); OpBuilder nested(block, block->begin()); Value result = SpatReluOp::create(nested, loc, type, argument); SpatYieldOp::create(nested, loc, ValueRange{result}); ResidentWeight weight; weight.opaqueLane = task == 0 ? 0 : 1; graph.nodes.push_back({{compute, 0, 1}, task == 2 ? 20u : 10u, {weight}, task}); } func::ReturnOp::create(builder, loc); TransferCost transfer{.fixed = 100}; graph.successors[0].push_back({1, transfer}); graph.predecessors[1].push_back({0, transfer}); SchedulingTarget target; target.processorCount = 2; target.residentWeightCapacity = 1; target.interProcessorLatencyNs.assign(4, 0); auto schedule = runCheckedPeftScheduler(graph, {target, &context}); assert(schedule.computeToCpuSlotMap.lookup(graph.nodes[2].instance) == 0); assert(schedule.computeToCpuSlotMap.lookup(graph.nodes[1].instance) == 1); PatternRewriter rewriter(&context); auto materialized = materializeScheduledCompute(function, schedule, rewriter); assert(succeeded(materialized)); size_t core = schedule.computeToCpuMap.lookup(graph.nodes[2].instance); const auto &instances = materialized->peftClassPlans[core].instancesByCpu[core]; assert(instances.size() == 2); assert(instances[0] == graph.nodes[2].instance); assert(instances[1] == graph.nodes[1].instance); for (const auto &record : materialized->materializedSchedules) if (record.cpus.front() == core) { assert(record.runs[0].instances.front() == graph.nodes[2].instance); assert(record.stepAnchors[0]->isBeforeInBlock(record.stepAnchors[1])); } } static void checkPublicationMemoryTraffic(mlir::func::FuncOp function, const ComputeGraph &graph, const SchedulingTarget &target, mlir::PatternRewriter &builder) { MergeScheduleResult schedule; schedule.processorCount = target.processorCount; schedule.processorStages = {0, 0, 1, 1}; std::vector> tasks(4); std::vector stages; for (auto [task, node] : llvm::enumerate(graph.nodes)) { bool producer = node.instance.op == graph.nodes.front().instance.op; size_t core = producer ? node.instance.laneStart % 2 : 2 + task % 2; stages.push_back(producer ? 0 : 1); schedule.dominanceOrderCompute.push_back(node.instance); schedule.computeToCpuMap[node.instance] = core; schedule.computeToCpuSlotMap[node.instance] = tasks[core].size(); tasks[core].push_back(task); } auto expected = getPipelineBoundaryMemoryDemand(graph, stages, target); populateScheduleEquivalenceClasses(graph, tasks, schedule); auto materialized = materializeScheduledCompute(function, schedule, builder); assert(mlir::succeeded(materialized)); auto plan = buildDeferredTransferPlan(function, *materialized, 2, 4); assert(mlir::succeeded(plan)); SharedMemoryDemand actual; llvm::DenseSet stored; for (const auto &exchange : plan->exchanges) for (const auto &transfer : exchange->external) if (transfer.hostRouted) { auto type = mlir::cast(transfer.requirement->publicationFragmentType); Cost bytes = (type.getNumElements() * target.computeBitwidth + 7) / 8; Cost requests = transfer.targetLanes.size(); actual += target.getSharedMemoryDemand(bytes, requests, false); for (size_t index = 0; index < requests; ++index) if (stored.insert(transfer.hostOffsets.valueAt(index)).second) actual += target.getSharedMemoryDemand(bytes, 1, true); } assert(actual.readBytes == expected.readBytes && actual.writeBytes == expected.writeBytes); assert(actual.readRequests == expected.readRequests && actual.writeRequests == expected.writeRequests); assert(actual.serviceNs == expected.serviceNs); for (unsigned registers : {3u, 16u}) { auto shared = buildDeferredTransferPlan(function, *materialized, 2, 4); assert(mlir::succeeded(shared)); auto communication = scheduleDeferredCommunication(function, *shared); assert(mlir::succeeded(communication)); auto boundaries = buildDeferredBoundaryPlan(*shared, *communication, registers); assert(mlir::succeeded(boundaries)); size_t stores = 0, signals = 0, readyWaits = 0, freeWaits = 0, releases = 0; llvm::DenseSet published; for (const auto &boundary : boundaries->boundaries) for (const auto &instruction : boundary.instructions) { if (auto send = std::get_if(&instruction)) for (const auto &slice : send->slices) if (slice.family->hostRouted) for (size_t offset = 0; offset < slice.transferCount; ++offset) { size_t index = slice.familyOffset + offset; bool first = published.insert(slice.family->hostOffsets.valueAt(index)).second; assert(bool(slice.family->hostStoreFlags.valueAt(index)) == first); stores += first; ++signals; } if (auto wait = std::get_if(&instruction)) for (auto interval : wait->lanes.intervals()) for (unsigned lane = interval.begin; lane < interval.end; ++lane) { assert(wait->eventRegisters.valueAt(lane) < registers); freeWaits += wait->waitValues.valueAt(lane); } } for (const auto &exchange : shared->exchanges) for (const auto &family : exchange->external) if (family.hostRouted) for (size_t index = 0; index < family.targetCores.size(); ++index) { readyWaits += family.waitValues.valueAt(index); assert(family.eventRegisters.valueAt(index) < registers); } for (const auto &[reader, releaseSignals] : shared->hostReleaseSignals) { llvm::DenseSet writers; for (const auto &release : releaseSignals) { assert(writers.insert(release.writerCore).second); assert(release.eventRegister < registers); ++releases; } } assert(stores == expected.writeRequests && signals == expected.readRequests); assert(readyWaits == signals && freeWaits == releases && releases == 4); } auto insufficient = buildDeferredTransferPlan(function, *materialized, 2, 4); assert(mlir::succeeded(insufficient)); auto communication = scheduleDeferredCommunication(function, *insufficient); assert(mlir::succeeded(communication)); mlir::ScopedDiagnosticHandler suppress(function.getContext(), [](mlir::Diagnostic &) { return mlir::success(); }); assert(mlir::failed(buildDeferredBoundaryPlan(*insufficient, *communication, 2))); } static void testPhysicalPublicationDependencies() { using namespace mlir; MLIRContext context(MLIRContext::Threading::DISABLED); context.loadDialect(); OwningOpRef owner = ModuleOp::create(UnknownLoc::get(&context)); PatternRewriter builder(&context); builder.setInsertionPointToEnd(owner->getBody()); auto loc = owner->getLoc(); auto fragmentType = RankedTensorType::get({1}, builder.getF32Type()); auto function = func::FuncOp::create( builder, loc, "publications", builder.getFunctionType({fragmentType}, {})); Block *body = function.addEntryBlock(); builder.setInsertionPointToStart(body); auto makeBatch = [&](Value input, uint32_t lanes, int64_t projectionSize, bool reverse, int64_t iterations = 0, bool invariantProjection = false) { auto inputType = cast(input.getType()); auto fragment = projectionSize ? RankedTensorType::get({projectionSize, 1}, builder.getF32Type()) : inputType; auto physical = getGraphBatchPhysicalResultType(lanes, fragment); auto batch = onnx_mlir::createEmptySpatGraphComputeBatch( builder, loc, TypeRange{physical}, lanes, ValueRange{}, ValueRange{input}); assert(succeeded(batch)); Value lane = *batch->getLaneArgument(); Value value = *batch->getInputArgument(0); scf::ForOp loop; if (iterations) { Value lower = arith::ConstantIndexOp::create(builder, loc, 0); Value upper = arith::ConstantIndexOp::create(builder, loc, iterations); Value step = arith::ConstantIndexOp::create(builder, loc, 1); Value empty = tensor::EmptyOp::create(builder, loc, fragment.getShape(), builder.getF32Type()); loop = scf::ForOp::create(builder, loc, lower, upper, step, ValueRange{empty}); builder.setInsertionPointToStart(loop.getBody()); lane = invariantProjection ? lower : loop.getInductionVar(); } if (projectionSize) { Value size = arith::ConstantIndexOp::create(builder, loc, projectionSize); Value offset = arith::MulIOp::create(builder, loc, lane, size); value = tensor::ExtractSliceOp::create( builder, loc, fragment, value, SmallVector{offset, builder.getIndexAttr(0)}, SmallVector{builder.getIndexAttr(projectionSize), builder.getIndexAttr(1)}, SmallVector(2, builder.getIndexAttr(1))); } value = SpatReluOp::create(builder, loc, fragment, value); if (loop) { scf::YieldOp::create(builder, loc, ValueRange{value}); builder.setInsertionPointAfter(loop); value = loop.getResult(0); lane = *batch->getLaneArgument(); } Value slot = lane; if (reverse) { Value last = arith::ConstantIndexOp::create(builder, loc, lanes - 1); slot = arith::SubIOp::create(builder, loc, last, lane); } auto parallel = SpatInParallelOp::create(builder, loc); builder.setInsertionPointToEnd(¶llel.getRegion().front()); SmallVector offsets(fragment.getRank() + 1, builder.getIndexAttr(0)); offsets.front() = slot; SmallVector sizes{builder.getIndexAttr(1)}; for (int64_t size : fragment.getShape()) sizes.push_back(builder.getIndexAttr(size)); tensor::ParallelInsertSliceOp::create(builder, loc, value, *batch->getOutputArgument(0), offsets, sizes, SmallVector(offsets.size(), builder.getIndexAttr(1))); builder.setInsertionPointAfter(*batch); return *batch; }; auto producer = makeBatch(body->getArgument(0), 4, 0, true); auto projected = makeBatch(producer.getResult(0), 2, 2, false); auto conservative = makeBatch(producer.getResult(0), 2, 0, false); auto looped = makeBatch(producer.getResult(0), 1, 2, false, 2); auto repeated = makeBatch(producer.getResult(0), 1, 2, false, 2, true); SmallVector entries{ {0, 0, {0}, {1}}, {2, 0, {1}, {1}}}; auto logicalType = RankedTensorType::get({2}, builder.getF32Type()); auto blueprint = onnx_mlir::createFragmentAssemblyBlueprint( producer.getResult(0), logicalType, entries, "dense_nchw", "fragment_assembly", builder, loc); assert(succeeded(blueprint)); auto scalar = SpatGraphCompute::create( builder, loc, TypeRange{logicalType}, ValueRange{}, ValueRange{*blueprint}); auto *block = new Block(); scalar.getBody().push_back(block); Value argument = block->addArgument(logicalType, loc); OpBuilder nested(block, block->begin()); Value value = SpatReluOp::create(nested, loc, logicalType, argument); SpatYieldOp::create(nested, loc, ValueRange{value}); auto projectedBlueprint = onnx_mlir::createEmptySpatGraphComputeBatch( builder, loc, TypeRange{getGraphBatchPhysicalResultType(2, fragmentType)}, 2, ValueRange{}, ValueRange{*blueprint}); assert(succeeded(projectedBlueprint)); Value projectedLane = *projectedBlueprint->getLaneArgument(); Value selectedBlueprint = tensor::ExtractSliceOp::create(builder, loc, fragmentType, *projectedBlueprint->getInputArgument(0), SmallVector{projectedLane}, SmallVector{builder.getIndexAttr(1)}, SmallVector{builder.getIndexAttr(1)}); selectedBlueprint = SpatReluOp::create(builder, loc, fragmentType, selectedBlueprint); auto projectedParallel = SpatInParallelOp::create(builder, loc); builder.setInsertionPointToEnd(&projectedParallel.getRegion().front()); tensor::ParallelInsertSliceOp::create(builder, loc, selectedBlueprint, *projectedBlueprint->getOutputArgument(0), SmallVector{projectedLane, builder.getIndexAttr(0)}, SmallVector(2, builder.getIndexAttr(1)), SmallVector(2, builder.getIndexAttr(1))); builder.setInsertionPointAfter(*projectedBlueprint); func::ReturnOp::create(builder, loc); for (Cost period : {1, 3}) { SchedulingTarget target; target.processorCount = 4; target.processorPeriodNs = period; target.sharedMemoryWidthBytes = 32; target.sharedMemoryReadLatencyNs = 7; target.sharedMemoryWriteLatencyNs = 11; ComputeGraph graph = buildCheckedComputeGraph(function, target); std::vector memoryStages(graph.nodes.size(), 1); for (size_t task = 0; task < graph.nodes.size(); ++task) if (graph.nodes[task].instance.op == producer) memoryStages[task] = 0; auto memory = getPipelineBoundaryMemoryDemand(graph, memoryStages, target); assert(memory.readBytes == 26 && memory.writeBytes == 4); assert(memory.readRequests == 26 && memory.writeRequests == 4); assert(memory.serviceNs == 26 * 7 + 4 * 11); assert(graph.baselineMemory.readBytes == 4 && graph.baselineMemory.readRequests == 4); auto predecessorLanes = [&](ComputeInstance instance) { std::vector lanes; for (const auto &[task, cost] : graph.predecessors[graph.instanceToIndex.lookup(instance)]) lanes.push_back(graph.nodes[task].instance.laneStart); llvm::sort(lanes); return lanes; }; assert((predecessorLanes({projected, 0, 1}) == std::vector{2, 3})); assert((predecessorLanes({projected, 1, 1}) == std::vector{0, 1})); for (uint32_t lane : {0, 1}) assert((predecessorLanes({conservative, lane, 1}) == std::vector{0, 1, 2, 3})); assert((predecessorLanes({looped, 0, 1}) == std::vector{0, 1, 2, 3})); assert((predecessorLanes({repeated, 0, 1}) == std::vector{2, 3})); assert((predecessorLanes({scalar, 0, 1}) == std::vector{1, 3})); // Two local fragment copies and the vector operation use the same ns unit. assert(graph.nodes[graph.instanceToIndex.lookup({scalar, 0, 1})].cost == 10 * period); for (uint32_t lane : {0, 1}) assert(graph.nodes[graph.instanceToIndex.lookup( {projectedBlueprint->getOperation(), lane, 1})].cost == 10 * period); for (const auto &[task, transfer] : graph.predecessors[graph.instanceToIndex.lookup({scalar, 0, 1})]) assert(transfer.fixed == 0); assert(verifyAcyclic(graph)); ComputeGraph chunked = buildCheckedComputeGraph(function, target, 1); const auto &dependencies = chunked.predecessors[ chunked.instanceToIndex.lookup({projected, 0, 2})]; assert(dependencies.size() == 1); assert(dependencies.front().second.fixed == 8 * period); assert(dependencies.front().second.networkFlits == 12); if (period == 3) checkPublicationMemoryTraffic(function, graph, target, builder); } builder.setInsertionPointToEnd(owner->getBody()); auto largeLoop = func::FuncOp::create(builder, loc, "invariant_loop", builder.getFunctionType({fragmentType}, {})); builder.setInsertionPointToStart(largeLoop.addEntryBlock()); auto loopProducer = makeBatch(largeLoop.getArgument(0), 4, 0, false); makeBatch(loopProducer.getResult(0), 1, 2, false, 1000000000, true); func::ReturnOp::create(builder, loc); SchedulingTarget loopTarget; loopTarget.processorCount = 4; auto compactGraph = buildCheckedComputeGraph(largeLoop, loopTarget); assert(compactGraph.publicationTransfers.size() == 2); for (const auto &transfer : compactGraph.publicationTransfers) assert(transfer.count == 1000000000); builder.setInsertionPointToEnd(owner->getBody()); auto independent = func::FuncOp::create(builder, loc, "batch_order", builder.getFunctionType({fragmentType}, {})); body = independent.addEntryBlock(); builder.setInsertionPointToStart(body); auto first = makeBatch(body->getArgument(0), 2, 0, false); auto second = makeBatch(body->getArgument(0), 2, 0, false); func::ReturnOp::create(builder, loc); SchedulingTarget target; target.processorCount = 2; ComputeGraph graph = buildCheckedComputeGraph(independent, target); MergeScheduleResult schedule; schedule.processorCount = 2; std::vector> tasksByCore(2); for (const auto &node : graph.nodes) schedule.dominanceOrderCompute.push_back(node.instance); for (uint32_t core : {0, 1}) for (auto [slot, batch] : llvm::enumerate(std::vector{second, first})) { ComputeInstance instance{batch, 1 - core, 1}; schedule.computeToCpuMap[instance] = core; schedule.computeToCpuSlotMap[instance] = slot; tasksByCore[core].push_back(graph.instanceToIndex.lookup(instance)); } schedule.processorStages = {0, 1}; populateScheduleEquivalenceClasses(graph, tasksByCore, schedule); assert(schedule.equivalentClass.empty()); schedule.processorStages.clear(); populateScheduleEquivalenceClasses(graph, tasksByCore, schedule); assert((schedule.equivalentClass[0] == SmallVector{1})); auto materialized = materializeScheduledCompute(independent, schedule, builder); assert(succeeded(materialized)); assert(materialized->materializedSchedules.size() == 1); const auto &record = materialized->materializedSchedules.front(); assert(isa(record.scheduledOp)); assert(record.stepPlans.size() == 2); assert(record.stepPlans[0].stepTuple.instances.front().op == second); assert(record.stepPlans[1].stepTuple.instances.front().op == first); assert(record.cpus.size() == 2); assert(record.cpus[0] == 1 && record.cpus[1] == 0); unsigned fragmentViews = 0; record.scheduledOp->walk([&](tensor::ExpandShapeOp) { ++fragmentViews; }); record.scheduledOp->walk([&](tensor::InsertSliceOp insert) { assert(insert.getDestType().getRank() != insert.getSourceType().getRank() + 1 || insert.getDestType().getDimSize(0) != 1); }); assert(fragmentViews == 2); builder.setInsertionPointToEnd(owner->getBody()); auto forwarded = func::FuncOp::create(builder, loc, "physical_forwarding", builder.getFunctionType({fragmentType}, {fragmentType})); body = forwarded.addEntryBlock(); builder.setInsertionPointToStart(body); auto permuted = makeBatch(body->getArgument(0), 4, 0, true); auto consumer = onnx_mlir::createEmptySpatGraphCompute(builder, loc, TypeRange{fragmentType}, ValueRange{}, ValueRange{permuted.getResult(0)}); Value selected = tensor::ExtractSliceOp::create(builder, loc, fragmentType, *consumer.getInputArgument(0), SmallVector(2, builder.getIndexAttr(0)), SmallVector(2, builder.getIndexAttr(1)), SmallVector(2, builder.getIndexAttr(1))); auto relu = SpatReluOp::create(builder, loc, fragmentType, selected); relu->setAttr("test.physical_consumer", builder.getUnitAttr()); SpatYieldOp::create(builder, loc, ValueRange{relu.getResult()}); builder.setInsertionPointAfter(consumer); func::ReturnOp::create(builder, loc, ValueRange{consumer.getResult(0)}); target.processorCount = 4; graph = buildCheckedComputeGraph(forwarded, target); for (const auto &[task, transfer] : graph.predecessors[ graph.instanceToIndex.lookup({consumer, 0, 1})]) { assert(transfer.fixed == 2); assert(transfer.networkFlits == 3); } schedule = MergeScheduleResult(); schedule.processorCount = 4; for (auto [slot, node] : llvm::enumerate(graph.nodes)) { schedule.dominanceOrderCompute.push_back(node.instance); schedule.computeToCpuMap[node.instance] = 0; schedule.computeToCpuSlotMap[node.instance] = slot; } materialized = materializeScheduledCompute(forwarded, schedule, builder); assert(succeeded(materialized)); assert(succeeded(realizeDeferredCommunication(forwarded, *materialized, target, 1))); bool checked = false; forwarded.walk([&](SpatReluOp op) { if (!op->hasAttr("test.physical_consumer")) return; auto slice = op.getInput().getDefiningOp(); assert(slice && getConstantIntValue(slice.getMixedOffsets().front()) == 3); checked = true; }); assert(checked && succeeded(verify(forwarded))); } static void testBlueprintSourceProjections() { using namespace mlir; MLIRContext context(MLIRContext::Threading::DISABLED); context.loadDialect(); OwningOpRef owner = ModuleOp::create(UnknownLoc::get(&context)); PatternRewriter builder(&context); builder.setInsertionPointToEnd(owner->getBody()); auto loc = owner->getLoc(); auto fragment = RankedTensorType::get({1}, builder.getF32Type()); auto physical = getGraphBatchPhysicalResultType(4, fragment); auto function = func::FuncOp::create(builder, loc, "blueprint_views", builder.getFunctionType({fragment}, {})); Block *body = function.addEntryBlock(); builder.setInsertionPointToStart(body); auto producer = onnx_mlir::createEmptySpatGraphComputeBatch( builder, loc, TypeRange{physical}, 4, ValueRange{}, ValueRange{body->getArgument(0)}); assert(succeeded(producer)); Value lane = *producer->getLaneArgument(); Value value = SpatReluOp::create(builder, loc, fragment, *producer->getInputArgument(0)); auto parallel = SpatInParallelOp::create(builder, loc); builder.setInsertionPointToEnd(¶llel.getRegion().front()); auto publication = tensor::ParallelInsertSliceOp::create(builder, loc, value, *producer->getOutputArgument(0), SmallVector{lane, builder.getIndexAttr(0)}, SmallVector(2, builder.getIndexAttr(1)), SmallVector(2, builder.getIndexAttr(1))); builder.setInsertionPointAfter(*producer); Value casted = tensor::CastOp::create(builder, loc, physical, producer->getResult(0)); auto flat = RankedTensorType::get({4}, builder.getF32Type()); Value collapsed = tensor::CollapseShapeOp::create(builder, loc, flat, casted, SmallVector{{0, 1}}); Value expanded = tensor::ExpandShapeOp::create(builder, loc, physical, collapsed, SmallVector{{0, 1}}); auto makeSlice = [&](int64_t size) -> Value { return tensor::ExtractSliceOp::create(builder, loc, RankedTensorType::get({size, 1}, builder.getF32Type()), producer->getResult(0), SmallVector{builder.getIndexAttr(2), builder.getIndexAttr(0)}, SmallVector{builder.getIndexAttr(size), builder.getIndexAttr(1)}, SmallVector(2, builder.getIndexAttr(1))); }; Value sliced = makeSlice(2), single = makeSlice(1); auto makeConsumer = [&](Value source) { int64_t count = cast(source.getType()).getDimSize(0); auto logical = RankedTensorType::get({count}, builder.getF32Type()); SmallVector entries; for (int64_t slot = 0; slot < count; ++slot) entries.push_back({slot, 0, {slot}, {1}}); auto blueprint = onnx_mlir::createFragmentAssemblyBlueprint(source, logical, entries, "dense_nchw", "fragment_assembly", builder, loc); assert(succeeded(blueprint)); auto consumer = onnx_mlir::createEmptySpatGraphCompute(builder, loc, TypeRange{logical}, ValueRange{}, ValueRange{*blueprint}); Value relu = SpatReluOp::create(builder, loc, logical, *consumer.getInputArgument(0)); SpatYieldOp::create(builder, loc, ValueRange{relu}); builder.setInsertionPointAfter(consumer); return consumer; }; auto direct = makeConsumer(producer->getResult(0)); auto viaCast = makeConsumer(casted); auto viaShape = makeConsumer(expanded); auto viaSlice = makeConsumer(sliced); auto one = makeConsumer(single); func::ReturnOp::create(builder, loc); assert(succeeded(verify(function))); SchedulingTarget target; target.processorCount = 4; auto graph = buildCheckedComputeGraph(function, target); for (auto consumer : {direct, viaCast, viaShape, viaSlice}) assert(graph.predecessors[graph.instanceToIndex.lookup({consumer, 0, 1})].size() == 4); const auto &parents = graph.predecessors[graph.instanceToIndex.lookup({one, 0, 1})]; assert(parents.size() == 1 && graph.nodes[parents[0].first].instance.laneStart == 2); // A malformed physical publication must diagnose and fail, not abort. builder.setInsertionPointToStart(&producer->getBody().front()); Value zero = arith::ConstantIndexOp::create(builder, loc, 0); publication.getOffsetsMutable().assign(ValueRange{zero}); std::string diagnostics; ScopedDiagnosticHandler handler(&context, [&](Diagnostic &diagnostic) { diagnostics += diagnostic.str(); return success(); }); assert(failed(buildComputeGraph(function, target))); assert(diagnostics.find("graph publication leading geometry is invalid") != std::string::npos); } static void testScheduleEvaluation() { ComputeGraph graph; graph.nodes = {{{nullptr, 0, 1}, 10, {}, 0}, {{nullptr, 1, 1}, 20, {}, 1}, {{nullptr, 2, 1}, 5, {}, 2}}; graph.predecessors.resize(3); graph.predecessors[2] = {{0, {}}, {1, {.fixed = 5, .networkFlits = 1}}}; for (Cost link : {2, 20}) { SchedulingTarget target; target.processorCount = 2; target.interProcessorLatencyNs = {0, link, link, 0}; std::string error; auto costs = evaluateSchedule(graph, {{0, 2}, {1}}, target, {}, error); assert(mlir::succeeded(costs)); assert(costs->totalWork == 35); assert(costs->coreWork == std::vector