#include "mlir/Conversion/AffineToStandard/AffineToStandard.h" #include "mlir/Transforms/Passes.h" #include "llvm/Support/Error.h" #include "llvm/Support/ErrorHandling.h" #include "llvm/Support/JSON.h" #include "llvm/Support/MemoryBuffer.h" #include "llvm/Support/Path.h" #include "llvm/ADT/SmallString.h" #include #include #include #include "src/Accelerators/PIM/Common/PimCommon.hpp" #include "src/Accelerators/PIM/Compiler/PimCompilerOptions.hpp" #include "PimPipelineSelection.hpp" #include "PimInstructionTiming.hpp" #include "PimPeriodicAnalysis.hpp" #include "src/Accelerators/PIM/Compiler/PimCompilerUtils.hpp" #include "src/Accelerators/PIM/Conversion/ONNXToSpatial/ONNXToSpatialOptions.hpp" #include "src/Accelerators/PIM/Dialect/Pim/PimOps.hpp" #include "src/Accelerators/PIM/Dialect/Spatial/SpatialTargetResources.hpp" #include "src/Accelerators/PIM/Dialect/Spatial/Passes/Transforms/MergeComputeNodes/SpatialDataflowCsvExporter.hpp" #include "src/Accelerators/PIM/Dialect/Spatial/Passes/Transforms/MergeComputeNodes/Scheduling/SchedulingTarget.hpp" #include "src/Accelerators/PIM/Passes/PIMPasses.h" #include "src/Compiler/CompilerPasses.hpp" #define DEBUG_TYPE "PimCompilerUtils" using namespace mlir; using namespace onnx_mlir; namespace onnx_mlir { namespace { void setDefaultPimInterProcessorLatencies( spatial::SchedulingTarget& target) { size_t rows = static_cast( std::sqrt(static_cast(target.processorCount))); while (rows > 1 && target.processorCount % rows != 0) --rows; size_t columns = (target.processorCount + rows - 1) / rows; target.interProcessorLatencyNs.assign( target.processorCount * target.processorCount, 0); Cost latencySum = 0; size_t pairCount = 0; for (size_t source = 0; source < target.processorCount; ++source) { for (size_t destination = 0; destination < target.processorCount; ++destination) { if (source == destination) continue; size_t sourceRow = source / columns; size_t sourceColumn = source % columns; size_t destinationRow = destination / columns; size_t destinationColumn = destination % columns; size_t rowDistance = sourceRow > destinationRow ? sourceRow - destinationRow : destinationRow - sourceRow; size_t columnDistance = sourceColumn > destinationColumn ? sourceColumn - destinationColumn : destinationColumn - sourceColumn; Cost latency = static_cast(2 + rowDistance + columnDistance); target.interProcessorLatencyNs[ source * target.processorCount + destination] = latency; latencySum = checkedAdd(latencySum, latency); ++pairCount; } } target.averageInterProcessorLatencyNs = pairCount == 0 ? 0 : (latencySum + static_cast(pairCount) - 1) / static_cast(pairCount); } spatial::SchedulingTarget getDefaultPimSchedulingTarget() { spatial::SchedulingTarget target; target.processorCount = static_cast(coresCount.getValue()); target.residentWeightCapacity = crossbarCountInCore.getValue(); target.matrixRows = crossbarSize.getValue(); // The oracle charges eight input reads for each matrix operation. target.matrixInputReadMultiplier = 8; target.matrixColumns = crossbarSize.getValue(); target.synchronizationRegisterCount = kPimEventRegisterCount; target.sharedMemoryWidthBytes = 16; target.sharedMemoryReadLatencyNs = 8; target.sharedMemoryWriteLatencyNs = 10; target.zeroSplatUsesSharedMemory = false; setDefaultPimInterProcessorLatencies(target); return target; } spatial::ConvLoweringStrategy getSpatialConvLoweringStrategy(PimConvLoweringType strategy) { switch (strategy) { case PimConvLoweringAuto: return spatial::ConvLoweringStrategy::Auto; case PimConvLoweringLegacy: return spatial::ConvLoweringStrategy::Legacy; case PimConvLoweringDepthwise: return spatial::ConvLoweringStrategy::Depthwise; case PimConvLoweringPackedIm2Col: return spatial::ConvLoweringStrategy::PackedIm2Col; case PimConvLoweringStreamedPatch: return spatial::ConvLoweringStrategy::StreamedPatch; case PimConvLoweringStreamedPacked: return spatial::ConvLoweringStrategy::StreamedPacked; case PimConvLoweringOutputChannelTiled: return spatial::ConvLoweringStrategy::OutputChannelTiled; case PimConvLoweringInputKTiled: return spatial::ConvLoweringStrategy::InputKTiled; case PimConvLoweringTiled2D: return spatial::ConvLoweringStrategy::Tiled2D; } llvm_unreachable("unknown Pim Conv lowering strategy"); } spatial::SpatialDataflowExportStage getPimSpatialDataflowExportStage( PimSpatialDataflowExportType stage) { switch (stage) { case SpatialDataflowExportNone: return spatial::SpatialDataflowExportStage::None; case SpatialDataflowExportSpatial1: return spatial::SpatialDataflowExportStage::Spatial1; case SpatialDataflowExportSpatial2: return spatial::SpatialDataflowExportStage::Spatial2; case SpatialDataflowExportSpatial3: return spatial::SpatialDataflowExportStage::Spatial3; case SpatialDataflowExportSpatial4: return spatial::SpatialDataflowExportStage::Spatial4; case SpatialDataflowExportAll: return spatial::SpatialDataflowExportStage::All; } llvm_unreachable("unknown Pim Spatial dataflow export stage"); } spatial::SpatialTargetResources getPimSpatialTargetResources(const spatial::SchedulingTarget& target) { spatial::SpatialTargetResources resources; resources.matrixShape = {target.matrixRows, target.matrixColumns}; resources.matrixUnitsPerProcessor = target.residentWeightCapacity; resources.processorCount = target.processorCount; resources.vectorWidth = target.vectorWidth; if (failed(resources.verify())) llvm::report_fatal_error("Pim target resources are incomplete"); return resources; } ONNXToSpatialPlanningOptions getPimONNXToSpatialPlanningOptions() { ONNXToSpatialPlanningOptions options; options.convIm2colMaxElements = pimConvIm2colMaxElements.getValue(); options.convStreamChunkPositions = pimConvStreamChunkPositions.getValue(); options.forcedConvStrategy = getSpatialConvLoweringStrategy(pimConvLowering.getValue()); options.reportConvLowering = pimReportConvLowering.getValue(); return options; } const llvm::json::Object& requireObject(const llvm::json::Object& object, llvm::StringRef key, llvm::StringRef path) { const llvm::json::Object* nested = object.getObject(key); if (!nested) llvm::report_fatal_error("Pim target config is missing object '" + path + "." + key + "'"); return *nested; } Cost getConfigCost(const llvm::json::Object& object, llvm::StringRef key, Cost fallback, bool allowZero = false) { std::optional number = object.getNumber(key); if (!number) return fallback; if (!std::isfinite(*number) || *number < 0.0 || (!allowZero && *number == 0.0) || *number > static_cast(std::numeric_limits::max())) llvm::report_fatal_error("Pim target config field '" + key + "' must be a valid positive number"); return static_cast(std::ceil(*number)); } std::pair getConfigPair(const llvm::json::Object& object, llvm::StringRef key) { const llvm::json::Array* values = object.getArray(key); if (!values || values->size() != 2) llvm::report_fatal_error("Pim target config field '" + key + "' must contain two integers"); std::optional first = (*values)[0].getAsInteger(); std::optional second = (*values)[1].getAsInteger(); if (!first || !second || *first <= 0 || *second <= 0) llvm::report_fatal_error("Pim target config field '" + key + "' must contain two positive integers"); return {static_cast(*first), static_cast(*second)}; } void loadPimInterProcessorLatencies( PimInstructionTiming& target, const llvm::json::Object& network, std::optional memorySwitch) { std::optional filename = network.getString("net_config_file_path"); if (!filename) llvm::report_fatal_error( "Pim target config is missing network latency file path"); llvm::SmallString<256> networkPath(*filename); if (!llvm::sys::path::is_absolute(networkPath)) { llvm::SmallString<256> configDirectory(pimTargetConfig.getValue()); llvm::sys::path::remove_filename(configDirectory); llvm::sys::path::append(configDirectory, networkPath); networkPath = configDirectory; } auto buffer = llvm::MemoryBuffer::getFile(networkPath); if (!buffer) llvm::report_fatal_error( llvm::Twine("failed to read Pim network config '") + networkPath + "': " + buffer.getError().message()); auto parsed = llvm::json::parse(buffer.get()->getBuffer()); if (!parsed) llvm::report_fatal_error( llvm::Twine("failed to parse Pim network config '") + networkPath + "': " + llvm::toString(parsed.takeError())); const llvm::json::Object* root = parsed->getAsObject(); const llvm::json::Object* latencies = root ? root->getObject("latency") : nullptr; if (!latencies) llvm::report_fatal_error( "Pim network config is missing its latency matrix"); target.interProcessorLatencyNs.assign( target.processorCount * target.processorCount, 0); Cost latencySum = 0; size_t pairCount = 0; for (size_t source = 0; source < target.processorCount; ++source) { std::string sourceKey = std::to_string(source); const llvm::json::Object* row = latencies->getObject(sourceKey); if (!row) llvm::report_fatal_error( llvm::Twine("Pim network config is missing latency row ") + sourceKey); for (size_t destination = 0; destination < target.processorCount; ++destination) { if (source == destination) continue; std::string destinationKey = std::to_string(destination); std::optional latency = row->getNumber(destinationKey); if (!latency || !std::isfinite(*latency) || *latency <= 0.0) llvm::report_fatal_error( llvm::Twine("Pim network config is missing latency ") + sourceKey + " -> " + destinationKey); Cost roundedLatency = static_cast(std::ceil(*latency)); target.interProcessorLatencyNs[ source * target.processorCount + destination] = roundedLatency; latencySum = checkedAdd(latencySum, roundedLatency); ++pairCount; } } target.averageInterProcessorLatencyNs = pairCount == 0 ? 0 : (latencySum + static_cast(pairCount) - 1) / static_cast(pairCount); if (memorySwitch) { const auto *memoryRow = latencies->getObject(std::to_string(*memorySwitch)); for (size_t core = 0; core < target.processorCount; ++core) { const auto *row = latencies->getObject(std::to_string(core)); auto request = row ? row->getNumber(std::to_string(*memorySwitch)) : std::nullopt; auto response = memoryRow ? memoryRow->getNumber(std::to_string(core)) : std::nullopt; if (!request || !response || !std::isfinite(*request) || !std::isfinite(*response) || *request < 0 || *response < 0 || *request >= static_cast(std::numeric_limits::max()) || *response >= static_cast(std::numeric_limits::max())) { target.sharedMemoryRequestLatencyNs.clear(); target.sharedMemoryResponseLatencyNs.clear(); break; } target.sharedMemoryRequestLatencyNs.push_back(std::ceil(*request)); target.sharedMemoryResponseLatencyNs.push_back(std::ceil(*response)); } } } PimInstructionTiming loadPimInstructionTiming() { PimInstructionTiming target; static_cast(target) = getDefaultPimSchedulingTarget(); if (pimTargetConfig.empty()) return target; auto buffer = llvm::MemoryBuffer::getFile(pimTargetConfig); if (!buffer) llvm::report_fatal_error( llvm::Twine("failed to read Pim target config '") + pimTargetConfig.getValue() + "': " + buffer.getError().message()); auto parsed = llvm::json::parse(buffer.get()->getBuffer()); if (!parsed) llvm::report_fatal_error( llvm::Twine("failed to parse Pim target config '") + pimTargetConfig.getValue() + "': " + llvm::toString(parsed.takeError())); const llvm::json::Object* root = parsed->getAsObject(); if (!root) llvm::report_fatal_error("Pim target config must contain a JSON object"); const llvm::json::Object& chip = requireObject(*root, "chip_config", "root"); const llvm::json::Object& core = requireObject(chip, "core_config", "chip_config"); const llvm::json::Object& matrix = requireObject(core, "matrix_config", "chip_config.core_config"); const llvm::json::Object& localMemory = requireObject(core, "local_memory_config", "chip_config.core_config"); const llvm::json::Object& network = requireObject(chip, "network_config", "chip_config"); // Older target configurations omit global memory and use adapter defaults. if (chip.get("global_memory_config")) { const llvm::json::Object& sharedMemory = requireObject(chip, "global_memory_config", "chip_config"); Cost memoryPeriod = getConfigCost(sharedMemory, "period", Cost{1}); target.sharedMemoryWidthBytes = getConfigCost( sharedMemory, "data_width", target.sharedMemoryWidthBytes); target.sharedMemoryReadLatencyNs = checkedMultiply(memoryPeriod, getConfigCost(sharedMemory, "read_latency_cycle", Cost{8}, true)); target.sharedMemoryWriteLatencyNs = checkedMultiply(memoryPeriod, getConfigCost(sharedMemory, "write_latency_cycle", Cost{10}, true)); } std::optional coreCount = chip.getInteger("core_cnt"); if (!coreCount || *coreCount <= 0) llvm::report_fatal_error("Pim target config field 'core_cnt' must be a positive integer"); target.processorCount = static_cast(*coreCount); target.residentWeightCapacity = getConfigCost(matrix, "xbar_array_count", target.residentWeightCapacity); std::tie(target.matrixRows, target.matrixColumns) = getConfigPair(matrix, "xbar_size"); if (target.processorCount != static_cast(coresCount.getValue()) || target.residentWeightCapacity != crossbarCountInCore.getValue() || target.matrixRows != crossbarSize.getValue() || target.matrixColumns != crossbarSize.getValue()) llvm::report_fatal_error("Pim target config resources do not match --core-count, " "--crossbar-count, and --crossbar-size"); loadPimInterProcessorLatencies(target, network, core.getInteger("global_memory_switch_id")); target.processorPeriodNs = getConfigCost(core, "period", target.processorPeriodNs); target.signalIssueNs = target.processorPeriodNs; target.controlMessageBytes = 1; target.sharedMemoryReadReturnsPayload = false; target.peerPermissionThenSourceRead = true; target.localMemoryWidthBytes = getConfigCost(localMemory, "data_width", target.localMemoryWidthBytes); target.localMemoryPeriodNs = getConfigCost(localMemory, "period", target.localMemoryPeriodNs); target.localMemoryReadLatencyCycles = getConfigCost(localMemory, "read_latency_cycle", target.localMemoryReadLatencyCycles); target.localMemoryWriteLatencyCycles = getConfigCost(localMemory, "write_latency_cycle", target.localMemoryWriteLatencyCycles); target.transferWidthBytes = getConfigCost(network, "bus_width", target.transferWidthBytes); target.vectorWidth = getConfigCost(core, "vector_width", target.vectorWidth); target.vectorLatencyCycles = getConfigCost(core, "vector_latency_cycle", target.vectorLatencyCycles); target.matrixPeriodNs = getConfigCost(matrix, "period", target.matrixPeriodNs); target.matrixInputResolutionBits = getConfigCost(matrix, "dac_resolution", target.matrixInputResolutionBits); target.matrixInputLatencyCycles = getConfigCost(matrix, "dac_latency_cycle", target.matrixInputLatencyCycles); target.matrixInputParallelism = getConfigCost(matrix, "dac_count", target.matrixInputParallelism); target.matrixReadLatencyNs = getConfigCost(matrix, "xbar_latency", target.matrixReadLatencyNs); target.matrixSampleLatencyCycles = getConfigCost(matrix, "sample_hold_latency_cycle", target.matrixSampleLatencyCycles); target.matrixOutputLatencyCycles = getConfigCost(matrix, "adc_latency_cycle", target.matrixOutputLatencyCycles); target.matrixOutputParallelism = getConfigCost(matrix, "adc_count", target.matrixOutputParallelism); target.matrixShiftLatencyCycles = getConfigCost(matrix, "shift_adder_latency_cycle", target.matrixShiftLatencyCycles); target.matrixBufferLatencyCycles = getConfigCost(matrix, "output_buffer_latency_cycle", target.matrixBufferLatencyCycles); target.matrixInputBufferLatencyCycles = getConfigCost(matrix, "input_buffer_latency_cycle", target.matrixInputBufferLatencyCycles, /*allowZero=*/true); target.matrixPipeline = matrix.getBoolean("pipeline_mode").value_or(target.matrixPipeline); return target; } } // namespace PimInstructionTiming getPimInstructionTiming() { return loadPimInstructionTiming(); } spatial::SchedulingTarget getPimSchedulingTarget() { auto timing = getPimInstructionTiming(); auto target = static_cast(timing); configurePimScheduleCosts(target, std::move(timing)); return target; } llvm::Expected getPimPeriodicTimingContract() { auto buffer = llvm::MemoryBuffer::getFile(pimTargetConfig); if (!buffer) return llvm::createStringError("queue selection requires --pim-target-config with instruction timing"); auto parsed = llvm::json::parse(buffer.get()->getBuffer()); if (!parsed) return parsed.takeError(); const auto *root = parsed->getAsObject(); const auto *chip = root ? root->getObject("chip_config") : nullptr; const auto *core = chip ? chip->getObject("core_config") : nullptr; const auto *matrix = core ? core->getObject("matrix_config") : nullptr; if (!matrix || core->getInteger("rob_size").value_or(1) != 1) return llvm::createStringError("queue selection requires a target with ROB capacity one and matrix timing"); auto bits = matrix->getInteger("cell_precision").value_or(2); if (bits <= 0) return llvm::createStringError("matrix cell precision must be positive"); return bits; } void addPimLoweringPasses(PassManager &pm, const spatial::SpatialTargetResources &targetResources, PimEmissionTargetType target, bool reportProgress) { auto message = [&](llvm::StringRef text) { if (reportProgress) pm.addPass(createMessagePass(text.str())); }; if (target >= EmitPim) { pm.addPass(createSpatialToPimPass(targetResources)); message("Spatial lowered to Pim"); } if (target >= EmitPimBufferized) { pm.addPass(createPimBufferizationPreparationPass(pimVerifyBufferizationCopyFreedom.getValue())); pm.addPass(createPimOneShotBufferizationPass()); pm.addPass(createPimMemoryNormalizationPass()); pm.addPass(createPimBufferizationVerificationPass()); message("Pim bufferized"); } if (target >= EmitPimCodegen) { pm.addPass(mlir::createLowerAffinePass()); pm.addPass(createPimHostConstantFoldingPass()); message("Pim host constants folded"); pm.addPass(createPimInstructionSelectionPass()); message("Pim instructions selected"); pm.addPass(createPimLocalMemoryPlanningPass()); message("Pim local memory planned"); pm.addPass(createPimVerificationPass(targetResources, pimDetectCommunicationDeadlock.getValue())); message("Pim verified"); } } void addPassesPim(OwningOpRef& module, PassManager& pm, EmissionTargetType& emissionTarget, std::string outputNameNoExt) { verifyPimCompilerOptions(); spatial::SchedulingTarget schedulingTarget = getPimSchedulingTarget(); spatial::SpatialTargetResources targetResources = getPimSpatialTargetResources(schedulingTarget); if (pimOnlyCodegen) { pm.addPass(createPimInstructionSelectionPass()); pm.addPass(createPimLocalMemoryPlanningPass()); pm.addPass(createPimVerificationPass(targetResources, pimDetectCommunicationDeadlock.getValue())); pm.addPass(createEmitPimCodePass()); return; } if (emissionTarget >= EmitONNXIR) addONNXToMLIRPasses(pm, /*target CPU*/ false); if (pimEmissionTarget >= EmitSpatial) { ONNXToSpatialPlanningOptions planningOptions = getPimONNXToSpatialPlanningOptions(); spatial::SpatialDataflowExportStage exportStage = getPimSpatialDataflowExportStage(pimExportSpatialDataflow.getValue()); pm.addPass(createONNXToSpatialPass(targetResources, planningOptions)); pm.addPass(createSpatialLayoutPlanningPass( targetResources, pimDisableSpatialPlanning.getValue())); pm.addPass(createLowerSpatialPlansPass(targetResources, planningOptions, exportStage)); pm.addPass(createTrivialGraphComputeMergePass( schedulingTarget.residentWeightCapacity, exportStage)); if (pipelineStages.getValue() > 1 && pimEmissionTarget >= EmitPimCodegen) { pm.addPass(createPimPipelineSelectionPass(schedulingTarget, targetResources, pipelineStages.getValue(), exportStage)); pm.addPass(createEmitPimCodePass()); return; } else { if (pipelineStages.getValue() > 1) pm.addPass(createMessagePass("Queue-aware pipeline: intermediate emission retains the original schedule without backend evaluation")); pm.addPass(spatial::createScheduleAndRealizeSpatialPass( schedulingTarget, exportStage, pipelineStages.getValue(), pimPeriodicDiagnostics.getValue())); } pm.addPass(createMessagePass("Onnx lowered to Spatial")); } addPimLoweringPasses(pm, targetResources, pimEmissionTarget); if (pimEmissionTarget >= EmitPimCodegen) { pm.addPass(createEmitPimCodePass()); pm.addPass(createMessagePass("Pim code emitted")); } } } // namespace onnx_mlir