Files
Raptor/test/PIM/PimAbstractExecutionTest.cpp
T

865 lines
47 KiB
C++

#include "TestCheck.hpp"
#include <limits>
#include "src/Accelerators/PIM/Compiler/PimPeriodicAnalysis.hpp"
#include "src/Accelerators/PIM/Compiler/PimInstructionTiming.hpp"
#include "src/Accelerators/PIM/Compiler/PimSpatialCost.hpp"
#include "src/Accelerators/PIM/Compiler/PimBinaryFormat.hpp"
#include "src/Accelerators/PIM/Compiler/PimDependencyTokens.hpp"
#include "mlir/Dialect/Func/IR/FuncOps.h"
#include "mlir/Dialect/SCF/IR/SCF.h"
#include "mlir/Dialect/Tensor/IR/Tensor.h"
#include "mlir/Dialect/Linalg/IR/Linalg.h"
#include "mlir/IR/Verifier.h"
#include "src/Accelerators/PIM/Dialect/Spatial/SpatialOps.hpp"
using namespace onnx_mlir;
using namespace mlir;
using E = PimExecutionEvent;
using O = pim_binary::Opcode;
static PimInstructionTiming hardware(int64_t period) {
PimInstructionTiming target;
target.processorCount = 2;
target.processorPeriodNs = period;
target.localMemoryPeriodNs = 3;
target.peerPermissionThenSourceRead = true;
target.signalIssueNs = period;
target.controlMessageBytes = 1;
target.sharedMemoryWidthBytes = 64;
target.sharedMemoryReadLatencyNs = 80;
target.sharedMemoryWriteLatencyNs = 120;
target.sharedMemoryRequestLatencyNs = {2, 3};
target.sharedMemoryResponseLatencyNs = {2, 3};
target.interProcessorLatencyNs = {0, 2, 2, 0};
target.synchronizationRegisterCount = 4;
return target;
}
static std::string binary(const std::vector<pim_binary::InstructionRecord> &records) {
std::string result(12, '\0');
result.replace(0, 4, "PIMB");
llvm::support::endian::write32le(result.data() + 4, 1);
llvm::support::endian::write32le(result.data() + 8, records.size());
for (const auto &record : records) {
auto encoded = pim_binary::encodeInstructionRecord(record);
result.append(encoded.data(), encoded.size());
}
return result;
}
static void testTypedResult(PimInstructionTiming target) {
PimPeriodicOptions options;
options.verifyPeriodicState = true;
PimExecutionProgram program{{{E::Work, 29}, {E::Load, 65}}};
auto estimate = evaluatePimExecution(program, target, options);
TEST_CHECK(estimate.cost.available() && estimate.cost.convergenceProved() == true);
TEST_CHECK(estimate.report.getString("status") == "estimated");
TEST_CHECK(estimate.cost.loweringSupport == spatial::StaticLoweringSupport::Unknown);
TEST_CHECK(!estimate.cost.eligible() && !estimate.cost.selectionMargin);
auto *queue = estimate.report.getObject("memory_controller");
for (size_t policy = 0; policy < 2; ++policy) {
auto *state = queue->getObject(policy ? "alternative_periodic_state" : "periodic_state");
const auto &proof = estimate.cost.recurrence[policy];
TEST_CHECK(proof.proved() && state->getString("status") == "proved");
TEST_CHECK(proof.intervalNs == state->getNumber("interval_ns"));
TEST_CHECK(proof.periodGenerations == state->getInteger("period_generations"));
}
auto primary = estimate.cost.primary, alternative = estimate.cost.alternative;
estimate.report = llvm::json::Object{{"status", "unavailable"}, {"convergence_proved", false}};
TEST_CHECK(estimate.cost.primary == primary && estimate.cost.alternative == alternative);
TEST_CHECK(estimate.cost.convergenceProved() == true);
options.verifyPeriodicState = false;
estimate = evaluatePimExecution(program, target, options);
TEST_CHECK(estimate.cost.available() && !estimate.cost.convergenceProved().has_value());
TEST_CHECK(estimate.report.getString("status") == "estimated");
TEST_CHECK(!estimate.report.getObject("memory_controller")->getObject("periodic_state"));
options.intervalWindowEventBudget = 0;
for (bool detailed : {false, true}) {
options.detailed = detailed;
estimate = evaluatePimExecution(program, target, options);
TEST_CHECK(estimate.queue && !estimate.cost.available());
TEST_CHECK(!estimate.cost.convergenceProved().has_value());
TEST_CHECK(estimate.report.getString("status") == "unavailable");
TEST_CHECK(estimate.report.getString("reason") == estimate.cost.reason);
TEST_CHECK(estimate.report.getObject("memory_controller")->getString("status") == "unavailable");
TEST_CHECK(!estimate.report.getObject("memory_controller")->getObject("periodic_state"));
}
options.intervalWindowEventBudget.reset();
options.detailed = false;
options.verifyPeriodicState = true;
estimate = evaluatePimExecution({{{E::Wait, 0, 0, 0, 1}, {E::Load, 65}}}, target, options);
TEST_CHECK(!estimate.cost.available() && estimate.cost.convergenceProved() == false);
TEST_CHECK(!estimate.cost.reason.empty() && !estimate.cost.eligible());
target.sharedMemoryWidthBytes = 0;
estimate = evaluatePimExecution(program, target, options);
TEST_CHECK(!estimate.cost.available() && !estimate.cost.reason.empty());
TEST_CHECK(estimate.report.getString("status") == "unavailable");
TEST_CHECK(estimate.report.getString("reason") == estimate.cost.reason);
}
static void testLateRecurrenceAuthority(PimInstructionTiming target) {
PimPeriodicOptions options;
options.verifyPeriodicState = true;
options.intervalWindowEventBudget = 1;
PimExecutionProgram program{{{E::Work, 29}, {E::Load, 65}}};
auto late = evaluatePimExecution(program, target, options);
TEST_CHECK(late.queue && late.cost.available() && late.cost.convergenceProved() == true);
for (const auto &policy : late.queue->policies) {
TEST_CHECK(policy.legacy.termination == PimQueueTermination::EventBudget);
TEST_CHECK(!policy.legacy.intervalNs && policy.recurrence.proved());
TEST_CHECK(policy.proofProcessedEvents > policy.legacy.progress.processedEvents);
TEST_CHECK(policy.intervalNs() == policy.recurrence.intervalNs);
TEST_CHECK(policy.termination == PimQueueTermination::RecurrenceProved);
}
auto first = serializePimQueueEvaluation(*late.queue);
auto second = serializePimQueueEvaluation(*late.queue);
TEST_CHECK(first.getString("status") == "estimated" && !first.getString("reason"));
TEST_CHECK(first.getString("alternative_tie_status") == "estimated");
TEST_CHECK(first.getNumber("interval_ns") == late.cost.primary);
TEST_CHECK(first.getNumber("alternative_tie_interval_ns") == late.cost.alternative);
TEST_CHECK(first.getObject("legacy_interval_window")->getString("status") == "unavailable");
TEST_CHECK(first.getObject("legacy_interval_window")->getString("termination") == "event-budget");
TEST_CHECK(llvm::json::Value(std::move(first)) == llvm::json::Value(std::move(second)));
auto blocked = evaluatePimExecution({{{E::Wait, 0, 0, 0, 1}, {E::Signal, 0, 1}},
{{E::Wait, 0, 0, 0, 1}, {E::Signal, 0, 0}}}, target, options);
TEST_CHECK(blocked.queue && blocked.cost.convergenceProved() == false);
TEST_CHECK(blocked.queue->policies[1].termination == PimQueueTermination::NoReadyEvents);
for (size_t failedPolicy = 0; failedPolicy < 2; ++failedPolicy) {
auto asymmetric = *late.queue;
asymmetric.policies[failedPolicy] = blocked.queue->policies[failedPolicy];
auto cost = asymmetric.cost();
cost.loweringSupport = spatial::StaticLoweringSupport::Supported;
cost.selectionMargin = 0.021;
TEST_CHECK(cost.primary.has_value() == (failedPolicy != 0));
TEST_CHECK(cost.alternative.has_value() == (failedPolicy != 1));
TEST_CHECK(cost.convergenceProved() == false && !cost.eligible());
TEST_CHECK(cost.reason == blocked.queue->policies[failedPolicy].recurrence.reason);
auto report = serializePimQueueEvaluation(asymmetric);
TEST_CHECK(report.getString("status") == (failedPolicy == 0 ? "unavailable" : "estimated"));
TEST_CHECK(report.getString("alternative_tie_status") == (failedPolicy == 1 ? "unavailable" : "estimated"));
TEST_CHECK(report.getObject(failedPolicy == 0 ? "periodic_state" : "alternative_periodic_state")
->getString("status") == "unproved");
}
options.intervalWindowEventBudget.reset();
auto normal = evaluatePimExecution(program, target, options);
options.workBreakdown = true;
auto detailed = evaluatePimExecution(program, target, options);
TEST_CHECK(late.cost.primary == normal.cost.primary && normal.cost.primary == detailed.cost.primary);
TEST_CHECK(late.cost.alternative == normal.cost.alternative && normal.cost.alternative == detailed.cost.alternative);
TEST_CHECK(normal.queue->policies[0].progress.processedEvents
<= detailed.queue->policies[0].progress.processedEvents);
}
static void testSharedEngine(PimInstructionTiming target) {
auto record = [](O op, int32_t peer = 0, int32_t reg = 0, int32_t count = 0, int32_t bytes = 0) {
pim_binary::InstructionRecord r;
r.opcode = op; r.r2OrImm = peer; r.generic1 = reg; r.generic2 = count; r.generic3 = bytes;
return r;
};
auto vectorWork = getPimVectorWorkNs(target, 65, 1, 1, true);
PimInstructionProgram instructions;
instructions.instructions = {
binary({record(O::setbw, 0, 8, 8), record(O::ld, 0, 0, 0, 65),
record(O::vvadd, 0, 0, 0, 65), record(O::send, 1, 0, 0, 65), record(O::sync, 1, 0)}),
binary({record(O::recv, 0, 0, 0, 65), record(O::wait, 0, 0, 1), record(O::st, 0, 0, 0, 33)})};
instructions.arrayGroups.resize(2);
PimExecutionProgram abstract(2);
abstract[0] = {{E::Work, target.processorPeriodNs}, {E::Load, 65}, {E::Work, vectorWork},
{E::Send, 65, 1}, {E::Signal, 0, 1, 0}};
abstract[1] = {{E::Receive, 65, 0}, {E::Wait, 0, 0, 0, 1}, {E::Store, 33}};
PimPeriodicOptions details;
details.workBreakdown = true;
PimExecutionProgram trace;
details.executionTrace = &trace;
auto reference = analyzePimInstructions(instructions, target, 2, details);
TEST_CHECK(reference.getString("status") == "estimated");
TEST_CHECK(!reference.getObject("memory_controller")->getObject("periodic_state"));
details.executionTrace = nullptr;
auto roundTrip = analyzePimExecution(trace, target, details);
for (const char *field : {"interval_ns", "alternative_tie_interval_ns"})
TEST_CHECK(roundTrip.getObject("memory_controller")->getNumber(field)
== reference.getObject("memory_controller")->getNumber(field));
for (size_t resource = 0; resource < 7; ++resource)
TEST_CHECK((*roundTrip.getArray("resource_service_ns"))[resource].getAsInteger()
== (*reference.getArray("resource_service_ns"))[resource].getAsInteger());
details.verifyPeriodicState = true;
auto proof = analyzePimExecution(trace, target, details);
TEST_CHECK(proof.getObject("memory_controller")->getString("status")
== reference.getObject("memory_controller")->getString("status"));
TEST_CHECK(proof.getObject("memory_controller")->getObject("legacy_interval_window")->getNumber("interval_ns")
== reference.getObject("memory_controller")->getNumber("interval_ns"));
TEST_CHECK(proof.getObject("memory_controller")->getNumber("interval_ns")
== proof.getObject("memory_controller")->getObject("periodic_state")->getNumber("interval_ns"));
TEST_CHECK(proof.getObject("memory_controller")->getObject("periodic_state"));
details.verifyPeriodicState = false;
auto actual = analyzePimExecution(abstract, target, details);
for (size_t core = 0; core < 2; ++core)
TEST_CHECK((*actual.getArray("request_order"))[core].getAsObject()->getString("identity")
== (*reference.getArray("request_order"))[core].getAsObject()->getString("identity"));
for (size_t core = 0; core < 2; ++core)
TEST_CHECK((*actual.getArray("request_order"))[core].getAsObject()->getString("blocking_identity")
== (*reference.getArray("request_order"))[core].getAsObject()->getString("blocking_identity"));
for (const char *field : {"global_read_bytes", "global_write_bytes", "global_read_requests",
"global_write_requests", "global_memory_service_ns"})
TEST_CHECK(actual.getInteger(field) == reference.getInteger(field));
auto *a = actual.getObject("memory_controller"), *b = reference.getObject("memory_controller");
TEST_CHECK(a && b && a->getString("status") == "estimated");
TEST_CHECK(a->getNumber("interval_ns") == b->getNumber("interval_ns"));
TEST_CHECK(a->getNumber("alternative_tie_interval_ns") == b->getNumber("alternative_tie_interval_ns"));
abstract[1].clear();
abstract[0] = {{E::Load, 1}, {E::Load, 65}};
auto ordered = analyzePimExecution(abstract, target, details);
abstract[0] = {{E::Load, 65}, {E::Load, 1}};
auto reversed = analyzePimExecution(abstract, target, details);
TEST_CHECK(ordered.getInteger("global_read_bytes") == reversed.getInteger("global_read_bytes"));
TEST_CHECK((*ordered.getArray("request_order"))[0].getAsObject()->getString("identity")
!= (*reversed.getArray("request_order"))[0].getAsObject()->getString("identity"));
// Preserve repeated request order, not a single larger request.
abstract[0] = {{E::Repeat, 2, 0, 0, 0, {{E::Load, 1}, {E::Work, 4}}}};
abstract[1].clear();
actual = analyzePimExecution(abstract, target);
TEST_CHECK(actual.getInteger("global_read_requests") == 2);
TEST_CHECK(actual.getInteger("global_memory_service_ns") == 160);
abstract[0] = {{E::Repeat, 1000000000000ull, 0, 0, 0, {{E::Load, 1}}}};
actual = analyzePimExecution(abstract, target);
TEST_CHECK(actual.getString("status") == "unavailable");
abstract[0] = {{E::Repeat, 1000000000000ull, 0, 0, 0, {{E::Work, 4}}}};
actual = analyzePimExecution(abstract, target);
TEST_CHECK(actual.getObject("memory_controller")->getNumber("interval_ns") == 4000000000000.0);
abstract[0] = {{E::Signal, 0, int64_t(1) << 32}};
TEST_CHECK(analyzePimExecution(abstract, target).getString("status") == "unavailable");
TEST_CHECK(getPimMatrixOutputBytes(target, 2, pim_binary::kMatrixWeightPrecisionBits, 3, 1)
== target.matrixColumns * 2 / pim_binary::kMatrixWeightPrecisionBits * 3);
E copy{E::Work, 4000};
copy.workClass = E::LocalCopy;
abstract[0] = {copy, {E::Load, 1}};
auto charged = analyzePimExecution(abstract, target);
PimPeriodicOptions sensitivity;
sensitivity.ignoreLocalCopies = true;
auto omitted = analyzePimExecution(abstract, target, sensitivity);
TEST_CHECK(*charged.getObject("memory_controller")->getNumber("interval_ns")
- *omitted.getObject("memory_controller")->getNumber("interval_ns") == 4000);
}
static void testAccountingAndOverflow(PimInstructionTiming target) {
auto record = [](O op, int64_t bytes) {
pim_binary::InstructionRecord r;
r.opcode = op; r.generic3 = bytes;
return r;
};
PimPeriodicOptions options;
options.scalarIssue = false;
options.workBreakdown = true;
auto setbw = record(O::setbw, 0);
setbw.generic1 = setbw.generic2 = 8;
auto matrixInstruction = record(O::mvmul, 0);
matrixInstruction.r2OrImm = pim_binary::kMatrixWeightPrecisionBits;
PimInstructionProgram reference;
reference.instructions = {binary({setbw, matrixInstruction, record(O::vvadd, 65), record(O::vmv, 3), record(O::lmv, 65), record(O::lldi, 65)})};
reference.arrayGroups = {{3}};
E matrix{E::Work, getPimMatrixWorkNs(target, 8,
getPimMatrixOutputBytes(target, 2, pim_binary::kMatrixWeightPrecisionBits, 3, 1))};
matrix.workClass = E::Matrix;
E vector{E::Work, getPimVectorWorkNs(target, 65, 1, 1, true)};
vector.workClass = E::Vector;
E vectorCopy{E::Work, getPimVectorWorkNs(target, 3, 1, 1, false)};
vectorCopy.workClass = E::VectorCopy;
E copy{E::Work, target.getLocalMemoryServiceNs(65, false)
+ target.getLocalMemoryServiceNs(65, true) + target.processorPeriodNs};
copy.workClass = E::LocalCopy;
E fill{E::Work, target.getLocalMemoryServiceNs(65, true) + target.processorPeriodNs};
fill.workClass = E::Fill;
auto exact = analyzePimInstructions(reference, target, 2, options);
auto abstract = analyzePimExecution({{matrix, vector, vectorCopy, copy, fill}}, target, options);
TEST_CHECK(*abstract.getArray("resource_service_ns") == *exact.getArray("resource_service_ns"));
TEST_CHECK(abstract.getObject("memory_controller")->getNumber("interval_ns")
== exact.getObject("memory_controller")->getNumber("interval_ns"));
auto repeated = analyzePimExecution({{{E::Repeat, 7, 0, 0, 0, {matrix, vector, vectorCopy, copy, fill}}}}, target, options);
for (size_t i = 0; i < 7; ++i)
TEST_CHECK(*(*repeated.getArray("resource_service_ns"))[i].getAsInteger()
== 7 * *(*abstract.getArray("resource_service_ns"))[i].getAsInteger());
auto nested = analyzePimExecution({{{E::Repeat, 1000000, 0, 0, 0,
{{E::Repeat, 1000000, 0, 0, 0, {fill}}}}}}, target, options);
TEST_CHECK(*(*nested.getArray("resource_service_ns"))[2].getAsInteger()
== static_cast<int64_t>(1000000000000ull * fill.amount));
TEST_CHECK(nested.getInteger("expanded_graph_events").value() < 10);
PimExecutionProgram pattern(2), expanded(2);
pattern[0] = {{E::Repeat, 3, 0, 0, 0, {{E::Load, 65},
{E::Repeat, 5, 0, 0, 0, {matrix, vector, copy, fill}}}}};
for (size_t repeat = 0; repeat < 3; ++repeat) {
expanded[0].push_back({E::Load, 65});
for (size_t work = 0; work < 5; ++work)
expanded[0].insert(expanded[0].end(), {matrix, vector, copy, fill});
}
pattern[1] = expanded[1] = {{E::Work, 311}, {E::Load, 257}};
auto compactCost = analyzePimExecution(pattern, target, options);
auto expandedCost = analyzePimExecution(expanded, target, options);
for (const char *policy : {"interval_ns", "alternative_tie_interval_ns"})
TEST_CHECK(compactCost.getObject("memory_controller")->getNumber(policy)
== expandedCost.getObject("memory_controller")->getNumber(policy));
TEST_CHECK(*compactCost.getArray("resource_service_ns") == *expandedCost.getArray("resource_service_ns"));
std::vector<E> body{{E::Load, 1}};
for (size_t i = 0; i < 10000; ++i) {
E event{E::Work, 1}; event.workClass = i % 2 ? E::Vector : E::Fill;
body.push_back(event);
}
auto cachedWork = analyzePimExecution({{{E::Repeat, 10000, 0, 0, 0, body}}}, target, options);
TEST_CHECK(cachedWork.getInteger("global_read_requests") == 10000);
TEST_CHECK(cachedWork.getInteger("expanded_graph_events").value() < 100000);
TEST_CHECK((*cachedWork.getArray("resource_service_ns"))[1].getAsInteger() == 50000000);
TEST_CHECK((*cachedWork.getArray("resource_service_ns"))[2].getAsInteger() == 50000000);
uint64_t maximum = std::numeric_limits<uint64_t>::max();
for (auto program : std::vector<PimExecutionProgram>{
{{{E::Work, maximum}}}, {{{E::Load, maximum}}},
{{{E::Repeat, maximum, 0, 0, 0, {fill}}}},
{{{E::Work, uint64_t(1) << 62}, {E::Work, uint64_t(1) << 62}}}})
TEST_CHECK(analyzePimExecution(program, target).getString("status") == "unavailable");
target.sharedMemoryReadLatencyNs = maximum;
TEST_CHECK(analyzePimExecution({{{E::Load, 65}}}, target).getString("status") == "unavailable");
}
static PimPeriodicOptions spatialDetails() {
PimPeriodicOptions options;
options.workBreakdown = true;
return options;
}
static void testCoreAssignments() {
MLIRContext context;
context.loadDialect<func::FuncDialect, spatial::SpatialDialect>();
OwningOpRef<ModuleOp> module = ModuleOp::create(UnknownLoc::get(&context));
OpBuilder builder(module->getBodyRegion());
auto loc = module->getLoc();
auto function = func::FuncOp::create(builder, loc, "test", builder.getFunctionType({}, {}));
builder.setInsertionPointToStart(function.addEntryBlock());
auto empty = [&](std::optional<int32_t> id) {
auto compute = spatial::SpatScheduledCompute::create(builder, loc, TypeRange{}, ValueRange{}, ValueRange{});
if (id) compute->setAttr("coreId", builder.getI32IntegerAttr(*id));
compute.getBody().push_back(new Block);
builder.setInsertionPointToStart(&compute.getBody().front());
spatial::SpatYieldOp::create(builder, loc, ValueRange{});
builder.setInsertionPointAfter(compute);
return compute;
};
auto first = empty(0);
auto second = empty(1);
func::ReturnOp::create(builder, loc);
TEST_CHECK(succeeded(verify(*module)));
auto target = hardware(1);
auto reason = [&]() {
auto report = analyzePimSpatialSchedule(*module, target);
return report.getString("reason").value_or("").str();
};
TEST_CHECK(reason().find("physical core") == std::string::npos);
second->setAttr("coreId", builder.getI32IntegerAttr(0));
TEST_CHECK(reason() == "duplicate physical core assignment: 0");
TEST_CHECK(evaluatePimSpatialSchedule(*module, target).cost.loweringSupport
== spatial::StaticLoweringSupport::Unsupported);
second->removeAttr("coreId");
TEST_CHECK(reason() == "missing physical core");
TEST_CHECK(evaluatePimSpatialSchedule(*module, target).cost.loweringSupport
== spatial::StaticLoweringSupport::Unknown);
for (int32_t id : {-1, 2}) {
second->setAttr("coreId", builder.getI32IntegerAttr(id));
TEST_CHECK(reason() == "physical core ID is outside the target: " + std::to_string(id));
}
second.erase();
builder.setInsertionPoint(function.getBody().front().getTerminator());
auto batch = spatial::SpatScheduledComputeBatch::create(builder, loc, TypeRange{},
builder.getI32IntegerAttr(2), ValueRange{}, ValueRange{});
batch.getBody().push_back(new Block);
batch.getBody().front().addArgument(builder.getIndexType(), loc);
builder.setInsertionPointToStart(&batch.getBody().front());
spatial::SpatYieldOp::create(builder, loc, ValueRange{});
TEST_CHECK(reason() == "missing physical batch cores");
batch->setAttr("coreIds", builder.getDenseI32ArrayAttr({1, 1}));
TEST_CHECK(reason() == "duplicate physical core assignment: 1");
batch->setAttr("coreIds", builder.getDenseI32ArrayAttr({1, 0}));
TEST_CHECK(reason() == "duplicate physical core assignment: 0");
TEST_CHECK(evaluatePimSpatialSchedule(*module, target).cost.loweringSupport
== spatial::StaticLoweringSupport::Unsupported);
first.erase();
batch->setAttr("coreIds", builder.getDenseI32ArrayAttr({1, 2}));
TEST_CHECK(reason() == "physical core ID is outside the target: 2");
}
static void testDependencyTokens() {
PimDependencyTokens fresh, drained;
TEST_CHECK(fresh.empty() && fresh.remaining().empty());
drained.initialize(1);
TEST_CHECK(drained.consume() == 0);
TEST_CHECK(drained.empty() && drained.remaining().empty());
// Empty queues have identical future behavior, regardless of their capacity.
for (uint64_t time : {0, 7, 7, 123}) {
fresh.push(time); drained.push(time);
TEST_CHECK(fresh.remaining() == drained.remaining());
TEST_CHECK(fresh.consume() == drained.consume());
TEST_CHECK(fresh.empty() && drained.empty());
}
// Consumed prefixes and compaction cannot change FIFO contents or readiness.
drained.initialize(3);
TEST_CHECK(drained.consume() == 0);
drained.push(11);
TEST_CHECK(drained.consume() == 0);
TEST_CHECK(drained.consume() == 0);
drained.push(11);
drained.push(12);
for (uint64_t time : {11, 11, 12}) TEST_CHECK(drained.consume() == time);
TEST_CHECK(drained.empty());
drained.initialize(0);
TEST_CHECK(drained.empty() && drained.remaining().empty());
}
static void testReadOnlySpatial(PimInstructionTiming target, bool batched) {
MLIRContext context;
context.loadDialect<arith::ArithDialect, func::FuncDialect, scf::SCFDialect,
tensor::TensorDialect, linalg::LinalgDialect, spatial::SpatialDialect>();
auto module = ModuleOp::create(UnknownLoc::get(&context));
OpBuilder builder(module.getBodyRegion());
auto loc = module.getLoc();
auto type = RankedTensorType::get({1, 64}, builder.getF32Type());
auto function = func::FuncOp::create(builder, loc, "test", builder.getFunctionType({}, {}));
builder.setInsertionPointToStart(function.addEntryBlock());
Value constant = arith::ConstantOp::create(builder, loc,
DenseElementsAttr::get(type, builder.getF32FloatAttr(2)));
Operation *compute;
if (batched) {
compute = spatial::SpatScheduledComputeBatch::create(builder, loc, TypeRange{},
builder.getI32IntegerAttr(2), ValueRange{}, ValueRange{constant});
compute->setAttr("coreIds", builder.getDenseI32ArrayAttr({0, 1}));
} else {
compute = spatial::SpatScheduledCompute::create(builder, loc, TypeRange{}, ValueRange{}, ValueRange{constant});
compute->setAttr("coreId", builder.getI32IntegerAttr(0));
}
Block *body = new Block;
compute->getRegion(0).push_back(body);
if (batched) body->addArgument(builder.getIndexType(), loc);
Value input = body->addArgument(type, loc);
builder.setInsertionPointToStart(body);
Value zero = arith::ConstantIndexOp::create(builder, loc, 0);
Value upper = arith::ConstantIndexOp::create(builder, loc, 28);
Value one = arith::ConstantIndexOp::create(builder, loc, 1);
Value local = tensor::EmptyOp::create(builder, loc, type.getShape(), type.getElementType());
auto loop = scf::ForOp::create(builder, loc, zero, upper, one);
builder.setInsertionPointToStart(loop.getBody());
auto add = spatial::SpatVAddOp::create(builder, loc, type, local, input);
builder.setInsertionPointAfter(loop);
spatial::SpatWaitOp::create(builder, loc, zero, zero);
spatial::SpatYieldOp::create(builder, loc, ValueRange{});
builder.setInsertionPointAfter(compute);
func::ReturnOp::create(builder, loc);
TEST_CHECK(succeeded(verify(module)));
std::string before, after;
llvm::raw_string_ostream os(before); module.print(os);
auto report = analyzePimSpatialSchedule(module, target, spatialDetails());
auto fast = analyzePimSpatialSchedule(module, target);
PimPeriodicOptions proofOptions;
proofOptions.verifyPeriodicState = true;
auto typed = evaluatePimSpatialSchedule(module, target, proofOptions);
TEST_CHECK(typed.cost.available() && typed.cost.convergenceProved() == true);
TEST_CHECK(typed.cost.loweringSupport == spatial::StaticLoweringSupport::Supported);
TEST_CHECK(typed.report.getBoolean("lowering_supported") == typed.cost.loweringSupported());
TEST_CHECK(typed.report.getBoolean("convergence_proved") == typed.cost.convergenceProved());
TEST_CHECK(!fast.getArray("request_order") && !fast.getArray("resource_service_ns"));
TEST_CHECK(!fast.getObject("without_local_copy_work") && !fast.getArray("work_class_ns"));
for (const char *field : {"interval_ns", "alternative_tie_interval_ns"})
TEST_CHECK(fast.getObject("memory_controller")->getNumber(field)
== report.getObject("memory_controller")->getNumber(field));
TEST_CHECK(report.getBoolean("safe_for_selection") == false);
TEST_CHECK(report.getInteger("global_read_requests") == (batched ? 2 : 28));
TEST_CHECK(report.getInteger("global_read_bytes") == (batched ? 2 : 28) * 64);
TEST_CHECK(report.getInteger("constant_read_requests") == (batched ? 2 : 28));
llvm::raw_string_ostream out(after); module.print(out);
TEST_CHECK(before == after);
add->setAttr("test.annotation", builder.getUnitAttr());
report = analyzePimSpatialSchedule(module, target, spatialDetails());
TEST_CHECK(report.getInteger("global_read_requests") == (batched ? 2 : 28));
builder.setInsertionPoint(body->getTerminator());
Value source = tensor::EmptyOp::create(builder, loc, ArrayRef<int64_t>{1, 3, 1, 224}, builder.getF32Type());
Value destination = tensor::EmptyOp::create(builder, loc, ArrayRef<int64_t>{1, 1, 224, 3}, builder.getF32Type());
linalg::TransposeOp::create(builder, loc, source, destination, ArrayRef<int64_t>{0, 2, 3, 1});
TEST_CHECK(succeeded(verify(module)));
report = analyzePimSpatialSchedule(module, target, spatialDetails());
auto expectedCopyWork = 224 * getPimVectorWorkNs(target, 3, 1, 1, false) * (batched ? 2 : 1);
auto *classes = report.getArray("work_class_ns");
auto *columns = report.getArray("work_class_columns");
TEST_CHECK(classes && columns && classes->size() == 6 && columns->size() == 6);
const char *names[] = {"matrix", "vector", "local_copy", "fill", "vector_copy", "scalar_issue"};
uint64_t totalWork = 0;
for (size_t i = 0; i < classes->size(); ++i) {
TEST_CHECK((*columns)[i].getAsString() == names[i]);
totalWork += *(*classes)[i].getAsInteger();
}
TEST_CHECK((*classes)[E::VectorCopy].getAsInteger() == expectedCopyWork);
TEST_CHECK((*classes)[E::LocalCopy].getAsInteger() == 0);
TEST_CHECK((*classes)[E::ScalarIssue].getAsInteger() == 0);
TEST_CHECK(totalWork == report.getInteger("compute_and_local_work_ns"));
auto softmax = spatial::SpatSoftmaxOp::create(builder, loc, type, input);
TEST_CHECK(succeeded(verify(module)));
TEST_CHECK(analyzePimSpatialSchedule(module, target, spatialDetails()).getString("status") == "unavailable");
softmax.erase();
source = tensor::EmptyOp::create(builder, loc, ArrayRef<int64_t>{1, 0, 1, 224}, builder.getF32Type());
destination = tensor::EmptyOp::create(builder, loc, ArrayRef<int64_t>{1, 1, 224, 0}, builder.getF32Type());
linalg::TransposeOp::create(builder, loc, source, destination, ArrayRef<int64_t>{0, 2, 3, 1});
TEST_CHECK(succeeded(verify(module)));
report = analyzePimSpatialSchedule(module, target, spatialDetails());
TEST_CHECK(report.getString("reason") == "zero-element transpose has no abstract timing contract");
module.erase();
}
static void testHostSlice(PimInstructionTiming target) {
MLIRContext context;
context.loadDialect<arith::ArithDialect, func::FuncDialect, scf::SCFDialect,
tensor::TensorDialect, spatial::SpatialDialect>();
auto module = ModuleOp::create(UnknownLoc::get(&context));
OpBuilder builder(module.getBodyRegion());
auto loc = module.getLoc();
auto type = RankedTensorType::get({1, 3, 4, 4}, builder.getF32Type());
auto function = func::FuncOp::create(builder, loc, "test", builder.getFunctionType({type}, {}));
builder.setInsertionPointToStart(function.addEntryBlock());
auto batch = spatial::SpatScheduledComputeBatch::create(builder, loc, TypeRange{},
builder.getI32IntegerAttr(2), ValueRange{}, ValueRange{function.getArgument(0)});
batch->setAttr("coreIds", builder.getDenseI32ArrayAttr({0, 1}));
Block *body = new Block;
batch.getBody().push_back(body);
body->addArgument(builder.getIndexType(), loc);
Value input = body->addArgument(type, loc);
builder.setInsertionPointToStart(body);
auto offsets = getAsOpFoldResult(builder.getI64ArrayAttr({0, 0, 1, 0}));
auto sizes = getAsOpFoldResult(builder.getI64ArrayAttr({1, 3, 1, 4}));
auto strides = getAsOpFoldResult(builder.getI64ArrayAttr({1, 1, 1, 1}));
auto slice = tensor::ExtractSliceOp::create(builder, loc, input, offsets, sizes, strides);
spatial::SpatReluOp::create(builder, loc, slice.getType(), slice.getResult());
spatial::SpatYieldOp::create(builder, loc, ValueRange{});
builder.setInsertionPointAfter(batch);
func::ReturnOp::create(builder, loc);
TEST_CHECK(succeeded(verify(module)));
auto report = analyzePimSpatialSchedule(module, target, spatialDetails());
TEST_CHECK(report.getInteger("global_read_requests") == 6);
TEST_CHECK(report.getInteger("global_read_bytes") == 24);
module.erase();
}
static void testCapturedVectorWeights(PimInstructionTiming target) {
MLIRContext context;
context.loadDialect<arith::ArithDialect, func::FuncDialect, tensor::TensorDialect,
spatial::SpatialDialect>();
auto module = ModuleOp::create(UnknownLoc::get(&context));
OpBuilder builder(module.getBodyRegion());
auto loc = module.getLoc();
auto type = RankedTensorType::get({1, 64}, builder.getF32Type());
auto function = func::FuncOp::create(builder, loc, "test", builder.getFunctionType({}, {}));
builder.setInsertionPointToStart(function.addEntryBlock());
Value zero = arith::ConstantOp::create(builder, loc,
DenseElementsAttr::get(type, builder.getF32FloatAttr(0)));
auto compute = spatial::SpatScheduledCompute::create(builder, loc, TypeRange{},
ValueRange{zero}, ValueRange{});
compute->setAttr("coreId", builder.getI32IntegerAttr(0));
Block *body = new Block;
compute.getBody().push_back(body);
Value weight = body->addArgument(type, loc);
builder.setInsertionPointToStart(body);
Value local = tensor::EmptyOp::create(builder, loc, type.getShape(), type.getElementType());
spatial::SpatVAddOp::create(builder, loc, type, local, weight);
spatial::SpatYieldOp::create(builder, loc, ValueRange{});
builder.setInsertionPointAfter(compute);
func::ReturnOp::create(builder, loc);
TEST_CHECK(succeeded(verify(module)));
auto report = analyzePimSpatialSchedule(module, target, spatialDetails());
// Captured weight operands stay block arguments; codegen cannot fold this load to LLDI.
TEST_CHECK(report.getInteger("global_read_requests") == 1);
TEST_CHECK(report.getInteger("constant_read_bytes") == 64);
module.erase();
}
static void testPublicationAndOutput(PimInstructionTiming target) {
MLIRContext context;
context.loadDialect<arith::ArithDialect, func::FuncDialect, tensor::TensorDialect,
spatial::SpatialDialect>();
auto module = ModuleOp::create(UnknownLoc::get(&context));
OpBuilder builder(module.getBodyRegion());
auto loc = module.getLoc();
auto inputType = RankedTensorType::get({1, 2, 3, 5}, builder.getF32Type());
auto outputType = RankedTensorType::get({1, 4, 3, 5}, builder.getF32Type());
auto function = func::FuncOp::create(builder, loc, "test", builder.getFunctionType({}, {outputType}));
builder.setInsertionPointToStart(function.addEntryBlock());
auto compute = spatial::SpatScheduledCompute::create(builder, loc, TypeRange{outputType},
ValueRange{}, ValueRange{});
compute->setAttr("coreId", builder.getI32IntegerAttr(0));
compute.getBody().push_back(new Block);
builder.setInsertionPointToStart(&compute.getBody().front());
Value zero = arith::ConstantIndexOp::create(builder, loc, 0);
Value one = arith::ConstantIndexOp::create(builder, loc, 1);
Value input = tensor::EmptyOp::create(builder, loc, inputType.getShape(), inputType.getElementType());
auto concat = spatial::SpatConcatOp::create(builder, loc, outputType,
builder.getI64IntegerAttr(1), ValueRange{input, input});
auto slice = tensor::ExtractSliceOp::create(builder, loc, concat.getOutput(),
getAsOpFoldResult(builder.getI64ArrayAttr({0, 0, 1, 0})),
getAsOpFoldResult(builder.getI64ArrayAttr({1, 4, 1, 5})),
getAsOpFoldResult(builder.getI64ArrayAttr({1, 1, 1, 1})));
spatial::SpatHostStoreSyncOp::create(builder, loc, zero, one, zero, zero, slice.getResult());
spatial::SpatWaitOp::create(builder, loc, one, one);
spatial::SpatYieldOp::create(builder, loc, ValueRange{concat.getOutput()});
builder.setInsertionPointAfter(compute);
auto reader = spatial::SpatScheduledCompute::create(builder, loc, TypeRange{}, ValueRange{}, ValueRange{});
reader->setAttr("coreId", builder.getI32IntegerAttr(1));
reader.getBody().push_back(new Block);
builder.setInsertionPointToStart(&reader.getBody().front());
zero = arith::ConstantIndexOp::create(builder, loc, 0);
one = arith::ConstantIndexOp::create(builder, loc, 1);
spatial::SpatWaitOp::create(builder, loc, zero, one);
spatial::SpatSyncOp::create(builder, loc, zero, one);
spatial::SpatYieldOp::create(builder, loc, ValueRange{});
builder.setInsertionPointAfter(reader);
func::ReturnOp::create(builder, loc, compute.getResults());
TEST_CHECK(succeeded(verify(module)));
auto report = analyzePimSpatialSchedule(module, target, spatialDetails());
TEST_CHECK(report.getInteger("global_write_requests") == 5);
TEST_CHECK(report.getInteger("global_write_bytes") == 80);
auto *runs = (*report.getArray("request_order"))[0].getAsObject()->getArray("runs");
TEST_CHECK(runs->size() == 2);
TEST_CHECK((*(*runs)[0].getAsArray())[1].getAsInteger() == 60);
TEST_CHECK((*(*runs)[1].getAsArray())[1].getAsInteger() == 5);
TEST_CHECK((*(*runs)[1].getAsArray())[2].getAsInteger() == 4);
module.erase();
}
static void testUnrepresentableSpatial(PimInstructionTiming target) {
MLIRContext context;
context.loadDialect<arith::ArithDialect, func::FuncDialect, tensor::TensorDialect,
scf::SCFDialect, spatial::SpatialDialect>();
for (bool loopOverflow : {false, true}) {
auto module = ModuleOp::create(UnknownLoc::get(&context));
OpBuilder builder(module.getBodyRegion());
auto loc = module.getLoc();
auto function = func::FuncOp::create(builder, loc, "test", builder.getFunctionType({}, {}));
builder.setInsertionPointToStart(function.addEntryBlock());
auto compute = spatial::SpatScheduledCompute::create(builder, loc, TypeRange{}, ValueRange{}, ValueRange{});
compute->setAttr("coreId", builder.getI32IntegerAttr(0));
compute.getBody().push_back(new Block);
builder.setInsertionPointToStart(&compute.getBody().front());
if (loopOverflow) {
Value lower = arith::ConstantIndexOp::create(builder, loc, std::numeric_limits<int64_t>::min());
Value upper = arith::ConstantIndexOp::create(builder, loc, std::numeric_limits<int64_t>::max());
Value step = arith::ConstantIndexOp::create(builder, loc, 1);
scf::ForOp::create(builder, loc, lower, upper, step);
} else {
auto type = RankedTensorType::get({std::numeric_limits<int64_t>::max(), 2}, builder.getF32Type());
Value input = tensor::EmptyOp::create(builder, loc, type.getShape(), type.getElementType());
spatial::SpatReluOp::create(builder, loc, type, input);
}
spatial::SpatYieldOp::create(builder, loc, ValueRange{});
builder.setInsertionPointAfter(compute);
func::ReturnOp::create(builder, loc);
TEST_CHECK(succeeded(verify(module)));
auto report = analyzePimSpatialSchedule(module, target, spatialDetails());
TEST_CHECK(report.getString("status") == "unavailable");
TEST_CHECK(!report.getBoolean("safe_for_selection").value());
module.erase();
}
}
static void testMatrixGeometry(PimInstructionTiming target) {
MLIRContext context;
context.loadDialect<arith::ArithDialect, func::FuncDialect, tensor::TensorDialect,
spatial::SpatialDialect>();
target.matrixCellBits = 2;
for (bool encoded : {false, true})
for (int64_t rows : {64, 128, 192})
for (int64_t columns : {64, 128, 192, 256}) {
auto module = ModuleOp::create(UnknownLoc::get(&context));
OpBuilder builder(module.getBodyRegion());
auto loc = module.getLoc();
auto inputType = RankedTensorType::get({1, rows}, builder.getF32Type());
auto weightType = RankedTensorType::get({rows, columns}, builder.getF32Type(),
encoded ? builder.getStringAttr("unsupported-layout") : Attribute{});
auto resultType = RankedTensorType::get({1, columns}, builder.getF32Type());
auto function = func::FuncOp::create(builder, loc, "test", builder.getFunctionType({}, {}));
builder.setInsertionPointToStart(function.addEntryBlock());
Value weight = arith::ConstantOp::create(builder, loc,
DenseElementsAttr::get(weightType, builder.getF32FloatAttr(1)));
auto compute = spatial::SpatScheduledCompute::create(builder, loc, TypeRange{},
ValueRange{weight}, ValueRange{});
compute->setAttr("coreId", builder.getI32IntegerAttr(0));
compute.getBody().push_back(new Block);
Value captured = compute.getBody().front().addArgument(weightType, loc);
builder.setInsertionPointToStart(&compute.getBody().front());
Value input = tensor::EmptyOp::create(builder, loc, inputType.getShape(), inputType.getElementType());
spatial::SpatVMMOp::create(builder, loc, resultType, captured, input);
spatial::SpatYieldOp::create(builder, loc, ValueRange{});
builder.setInsertionPointAfter(compute);
func::ReturnOp::create(builder, loc);
TEST_CHECK(succeeded(verify(module)));
auto report = analyzePimSpatialSchedule(module, target);
if (encoded || columns % target.matrixColumns || static_cast<uint64_t>(rows) > target.matrixRows)
TEST_CHECK(report.getString("reason") == "unavailable physical matrix weight geometry");
else TEST_CHECK(report.getString("status") != "unavailable");
module.erase();
}
}
static void testOutputMultiplicity(PimInstructionTiming target) {
MLIRContext context;
context.loadDialect<arith::ArithDialect, func::FuncDialect, tensor::TensorDialect,
spatial::SpatialDialect>();
for (bool repeatedResult : {false, true}) {
auto module = ModuleOp::create(UnknownLoc::get(&context));
OpBuilder builder(module.getBodyRegion());
auto loc = module.getLoc();
auto type = RankedTensorType::get({1, 64}, builder.getF32Type());
auto function = func::FuncOp::create(builder, loc, "test", builder.getFunctionType({}, {type, type}));
builder.setInsertionPointToStart(function.addEntryBlock());
SmallVector<Type> results(repeatedResult ? 1 : 2, type);
auto compute = spatial::SpatScheduledCompute::create(builder, loc, results, ValueRange{}, ValueRange{});
compute->setAttr("coreId", builder.getI32IntegerAttr(0));
compute.getBody().push_back(new Block);
builder.setInsertionPointToStart(&compute.getBody().front());
Value input = tensor::EmptyOp::create(builder, loc, type.getShape(), type.getElementType());
Value output = spatial::SpatReluOp::create(builder, loc, type, input);
SmallVector<Value> yielded(results.size(), output);
spatial::SpatYieldOp::create(builder, loc, yielded);
builder.setInsertionPointAfter(compute);
func::ReturnOp::create(builder, loc, ValueRange{compute.getResult(0), compute.getResult(repeatedResult ? 0 : 1)});
TEST_CHECK(succeeded(verify(module)));
auto report = analyzePimSpatialSchedule(module, target, spatialDetails());
if (repeatedResult)
TEST_CHECK(report.getString("reason") == "unavailable indirect compute output collection");
else {
TEST_CHECK(report.getInteger("global_write_requests") == 2);
TEST_CHECK(report.getInteger("global_write_bytes") == 128);
auto *runs = (*report.getArray("request_order"))[0].getAsObject()->getArray("runs");
TEST_CHECK((*(*runs)[0].getAsArray())[2].getAsInteger() == 2);
}
module.erase();
}
}
static void testPeriodicStateProof() {
auto target = hardware(1);
target.processorCount = 3;
target.sharedMemoryReadReturnsPayload = false;
target.localMemoryPeriodNs = 1;
target.localMemoryReadLatencyCycles = 20;
target.localMemoryWriteLatencyCycles = 30;
target.transferWidthBytes = target.sharedMemoryWidthBytes = 32;
target.sharedMemoryReadLatencyNs = 40;
target.sharedMemoryWriteLatencyNs = 50;
target.sharedMemoryRequestLatencyNs = target.sharedMemoryResponseLatencyNs = {6, 6, 6};
target.interProcessorLatencyNs = {0, 3, 4, 3, 0, 3, 4, 3, 0};
PimExecutionProgram program{
{{E::Work, 377}, {E::Store, 875}, {E::Work, 545}, {E::Load, 367},
{E::Work, 333}, {E::Store, 80}, {E::Signal, 0, 1}, {E::Wait, 0, 0, 0, 1}},
{{E::Work, 924}, {E::Load, 312}, {E::Work, 70}, {E::Store, 434},
{E::Work, 337}, {E::Load, 275}, {E::Signal, 0, 2}, {E::Wait, 0, 0, 0, 1}},
{{E::Work, 158}, {E::Store, 565}, {E::Work, 126}, {E::Load, 245},
{E::Work, 1124}, {E::Store, 367}, {E::Signal, 0, 0}, {E::Wait, 0, 0, 0, 1}}};
auto baseline = analyzePimExecution(program, target);
PimPeriodicOptions options;
options.verifyPeriodicState = true;
options.workBreakdown = true;
auto verified = analyzePimExecution(program, target, options);
auto *queue = verified.getObject("memory_controller");
auto *legacy = queue->getObject("legacy_interval_window");
TEST_CHECK(legacy);
for (const char *field : {"interval_ns", "alternative_tie_interval_ns"})
TEST_CHECK(legacy->getNumber(field) == baseline.getObject("memory_controller")->getNumber(field));
auto *state = queue->getObject("periodic_state");
TEST_CHECK(state && state->getString("status") == "proved");
TEST_CHECK(state->getInteger("period_generations") == 3);
TEST_CHECK(state->getNumber("interval_ns") == 17485.0 / 3);
TEST_CHECK(queue->getNumber("interval_ns") == state->getNumber("interval_ns"));
TEST_CHECK(legacy->getNumber("interval_ns") != state->getNumber("interval_ns"));
// A fast writer builds signal tokens. Repeated local intervals are insufficient.
program = {{{E::Signal, 0, 1}, {E::Work, 10}},
{{E::Wait, 0, 0, 0, 1}, {E::Work, 100}}};
verified = analyzePimExecution(program, target, options);
state = verified.getObject("memory_controller")->getObject("periodic_state");
TEST_CHECK(state && state->getString("status") == "unproved");
}
static void testCompleteSchedulerState() {
auto target = hardware(1);
PimPeriodicOptions options;
options.verifyPeriodicState = true;
auto verify = [&](const PimExecutionProgram &program, bool expected) {
auto baseline = analyzePimExecution(program, target);
auto typed = evaluatePimExecution(program, target, options);
TEST_CHECK(typed.cost.convergenceProved() == expected);
auto detailed = std::move(typed.report);
auto *queue = detailed.getObject("memory_controller");
TEST_CHECK(queue);
TEST_CHECK(detailed.getString("status") == (typed.cost.available() ? "estimated" : "unavailable"));
if (!typed.cost.available()) TEST_CHECK(detailed.getString("reason") == typed.cost.reason);
if (expected) {
TEST_CHECK(queue->getNumber("interval_ns") == typed.cost.primary);
TEST_CHECK(queue->getNumber("alternative_tie_interval_ns") == typed.cost.alternative);
} else {
for (const char *field : {"interval_ns", "alternative_tie_interval_ns"})
TEST_CHECK(queue->getNumber(field) == baseline.getObject("memory_controller")->getNumber(field));
}
size_t policy = 0;
for (const char *field : {"periodic_state", "alternative_periodic_state"}) {
auto *state = queue->getObject(field);
TEST_CHECK(state && (state->getString("status") == "proved") == expected);
const auto &evidence = typed.cost.recurrence[policy++];
TEST_CHECK(evidence.proved() == expected);
TEST_CHECK(evidence.intervalNs == state->getNumber("interval_ns"));
if (expected) TEST_CHECK(state->getBoolean("complete_scheduler_state") == true);
}
return detailed;
};
verify({{{E::Work, 0}}}, false);
auto steady = verify({{{E::Work, 100}}}, true);
auto *state = steady.getObject("memory_controller")->getObject("periodic_state");
TEST_CHECK(state->getInteger("period_generations") == 1);
TEST_CHECK(state->getNumber("interval_ns") == 100);
verify({{{E::Work, uint64_t{1} << 60}}}, true);
// Simultaneous signals, drained token queues, and controller arrivals must
// recur under each deterministic arbitration policy.
verify({{{E::Work, 10}, {E::Signal, 0, 1}, {E::Wait, 0, 0, 0, 1}},
{{E::Work, 10}, {E::Signal, 0, 0}, {E::Wait, 0, 0, 0, 1}}}, true);
verify({{{E::Load, 65}, {E::Signal, 0, 1}, {E::Wait, 0, 0, 0, 1}},
{{E::Load, 65}, {E::Signal, 0, 0}, {E::Wait, 0, 0, 0, 1}}}, true);
// Repeated output intervals do not prove uniform progress on every core.
auto unequal = verify({{{E::Work, 7}}, {{E::Work, 11}}}, false);
TEST_CHECK(unequal.getObject("memory_controller")->getString("status") == "estimated");
// The complete state retains growing dependency-token queues.
auto backlog = verify({{{E::Signal, 0, 1}, {E::Work, 10}},
{{E::Wait, 0, 0, 0, 1}, {E::Work, 100}}}, false);
TEST_CHECK(backlog.getObject("memory_controller")->getString("status") == "estimated");
// Both endpoints exist, but neither WAIT can receive its signal.
auto deadlock = verify({{{E::Wait, 0, 0, 0, 1}, {E::Signal, 0, 1}},
{{E::Wait, 0, 0, 0, 1}, {E::Signal, 0, 0}}}, false);
TEST_CHECK(deadlock.getObject("memory_controller")->getString("status") == "unavailable");
verify({{{E::Send, 65, 1}, {E::Receive, 65, 1}},
{{E::Send, 65, 0}, {E::Receive, 65, 0}}}, false);
// An independent active core must not hide an inactive dependency cycle.
verify({{{E::Work, 10}}, {{E::Wait, 0, 0, 0, 1}, {E::Signal, 0, 1}}}, false);
auto overflow = analyzePimExecution({{{E::Work, uint64_t{1} << 62}}}, target, options);
TEST_CHECK(overflow.getString("status") == "unavailable");
}
int main() {
testDependencyTokens();
testCoreAssignments();
testPeriodicStateProof();
testCompleteSchedulerState();
for (int64_t period : {1, 5}) {
testLateRecurrenceAuthority(hardware(period));
testTypedResult(hardware(period));
testSharedEngine(hardware(period));
testAccountingAndOverflow(hardware(period));
testMatrixGeometry(hardware(period));
testOutputMultiplicity(hardware(period));
testHostSlice(hardware(period));
testCapturedVectorWeights(hardware(period));
testPublicationAndOutput(hardware(period));
testUnrepresentableSpatial(hardware(period));
testReadOnlySpatial(hardware(period), false);
testReadOnlySpatial(hardware(period), true);
}
}