Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
441cac8
Charge nested loop iterations in shared SCHEMATIC energy propagation
byeongjee Aug 22, 2026
8e9072c
Charge the restore cost in the SCHEMATIC loop iteration budget
byeongjee Aug 23, 2026
656dfc2
Treat promoted allocas as dead at function entry in MILP liveness
byeongjee Aug 23, 2026
4505eaa
Charge FRAM stack accesses in assembly energy for SCHEMATIC and RockC…
byeongjee Aug 23, 2026
df73f3f
Convert estimator block costs to SCHEMATIC's all-NVM convention
byeongjee Aug 23, 2026
898e266
Count only memory-accessing stack operands in the assembly estimator
byeongjee Aug 23, 2026
8c8c013
Charge FRAM stack accesses in RockClimb's machine-level energy pass
byeongjee Aug 23, 2026
1ac1d2b
Unify compiler-split annotated loops before MILP chunking
byeongjee Aug 24, 2026
055f371
Update results
byeongjee Aug 24, 2026
94eed2b
Declare nvm_access_penalty in the RockClimb preprocess test configs
byeongjee Aug 24, 2026
1b9bf53
Scope the promoted-alloca liveness rule to main
byeongjee Aug 24, 2026
97c2403
Fail SCHEMATIC when a split loop body still does not fit
byeongjee Aug 24, 2026
b62d3be
Charge the operand access of stack push and pop instructions
byeongjee Aug 24, 2026
0d030bb
Initialize logging in UnifyAnnotatedLoopPass
byeongjee Aug 24, 2026
a09e70d
Re-measure milp_debug.csv in a single run
byeongjee Aug 24, 2026
89a9b93
Unify compiler-split annotated loops in the SCHEMATIC pipeline
byeongjee Aug 24, 2026
f583c8a
Re-measure schematic_debug.csv in a single run
byeongjee Aug 24, 2026
fa640fa
Re-measure schematicO3_debug.csv in a single run
byeongjee Aug 24, 2026
796ba83
Re-measure milp.csv in a single run
byeongjee Aug 24, 2026
8c6667b
Re-measure schematic.csv in a single run
byeongjee Aug 24, 2026
6691ffa
Re-measure schematicO3.csv in a single run
byeongjee Aug 24, 2026
98494be
Update result plots
byeongjee Aug 24, 2026
46ff13a
Repeat harvesting traces 320 times
byeongjee Aug 24, 2026
2c776d1
Update intermittent results
byeongjee Aug 24, 2026
bc328bc
Add intermittent result plot script
byeongjee Aug 24, 2026
abc40a5
Rename intermittent sha256_fixed results to sha256
byeongjee Aug 24, 2026
7af00b1
Restyle intermittent plot axes
byeongjee Aug 24, 2026
64b06f4
Label intermittent bars with their values
byeongjee Aug 24, 2026
7cb25bb
Drop the intermittent bar plot and label the box plot
byeongjee Aug 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30,240 changes: 30,240 additions & 0 deletions benchmarks/traces/1.csv

Large diffs are not rendered by default.

47,280 changes: 47,280 additions & 0 deletions benchmarks/traces/10.csv

Large diffs are not rendered by default.

47,040 changes: 47,040 additions & 0 deletions benchmarks/traces/2.csv

Large diffs are not rendered by default.

47,280 changes: 47,280 additions & 0 deletions benchmarks/traces/3.csv

Large diffs are not rendered by default.

31,440 changes: 31,440 additions & 0 deletions benchmarks/traces/4.csv

Large diffs are not rendered by default.

37,200 changes: 37,200 additions & 0 deletions benchmarks/traces/5.csv

Large diffs are not rendered by default.

61,920 changes: 61,920 additions & 0 deletions benchmarks/traces/6.csv

Large diffs are not rendered by default.

41,760 changes: 41,760 additions & 0 deletions benchmarks/traces/7.csv

Large diffs are not rendered by default.

55,440 changes: 55,440 additions & 0 deletions benchmarks/traces/8.csv

Large diffs are not rendered by default.

30,000 changes: 30,000 additions & 0 deletions benchmarks/traces/9.csv

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions benchmarks/traces/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ The `*.csv` files here are the replay-ready versions: time-compressed 10x
benchmarks run; at 10x the replays fail 0.2-2/s), block-averaged to 50 Hz
(each sample is the mean of a 200 ms window of the recording, which acts as
the anti-aliasing filter), clipped to 3.6 V, the MSP430 operating maximum,
and written as 80 back-to-back repetitions. Reproduce with:
and written as 320 back-to-back repetitions. Reproduce with:

```bash
uv run python scripts/otii/preprocess_traces.py benchmarks/traces/original/[0-9]*.txt \
-o benchmarks/traces --vmax 3.6 --speedup 10 --repeat 80
-o benchmarks/traces --vmax 3.6 --speedup 10 --repeat 320
```

Trace 3 peaks at 3.13 V, below the target's operating range, so it is scaled up
Expand Down
1 change: 1 addition & 0 deletions passes/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ src/common/LoopTripCount.cpp
src/milp/EnergyModel.cpp
src/milp/EnergyPathUtils.cpp
src/milp/AllocaToGlobalPass.cpp
src/milp/UnifyAnnotatedLoopPass.cpp
src/milp/LoopStripMiningPass.cpp
src/milp/AbstractCFG.cpp
src/milp/MILPCheckpointPass.cpp
Expand Down
26 changes: 26 additions & 0 deletions passes/bb-energy-analyzer/BBEnergyAnalyzer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <iostream>
#include <nlohmann/json.hpp>
#include <optional>
#include <regex>

using namespace llvm;
using namespace bbanalyzer;
Expand Down Expand Up @@ -97,6 +98,11 @@ static cl::opt<std::string>
static cl::opt<std::string> OutputFile("o", cl::init("-"),
cl::desc("Output JSON file (default: stdout)"));

static cl::opt<double> StackAccessPenalty(
"stack-access-penalty", cl::init(0.0),
cl::desc("Extra energy per stack memory access (for builds whose stack lives in FRAM; "
"the energy parameters are calibrated with the stack in SRAM)"));

static cl::opt<bool> DumpLineMap("dump-line-map", cl::init(false),
cl::desc("Dump resolved address->BB line map and exit"));

Expand Down Expand Up @@ -194,6 +200,22 @@ int main(int argc, char **argv) {
funcOutput["bb_count"] = funcMap.bbToAddresses.size();
funcOutput["bb_energy"] = json::object();

// r4 addresses the stack only when the prologue makes it the frame
// pointer (mov r1, r4); otherwise it is a general-purpose register.
bool fpIsR4 = false;
if (StackAccessPenalty > 0.0) {
static const std::regex fpPrologue(R"(r1,\s*r4)");
for (const auto &[bbIndex, ranges] : funcMap.bbToAddresses) {
for (const auto &range : ranges) {
for (const auto &insn : instructions) {
if (insn.address >= range.start && insn.address < range.end &&
insn.mnemonic == "mov" && std::regex_match(insn.operands, fpPrologue))
fpIsR4 = true;
}
}
}
}

for (const auto &[bbIndex, ranges] : funcMap.bbToAddresses) {
double bbEnergy = 0.0;
int instrCount = 0;
Expand All @@ -218,6 +240,10 @@ int main(int argc, char **argv) {
} else {
bbEnergy += model.getEnergy(insn.mnemonic, insn.addrMode);
}
if (StackAccessPenalty > 0.0) {
bbEnergy += StackAccessPenalty *
MSP430Disassembler::countStackAccesses(insn, fpIsR4);
}
instrCount++;
instructionMapped[i] = true;
}
Expand Down
41 changes: 41 additions & 0 deletions passes/bb-energy-analyzer/MSP430Disassembler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -366,4 +366,45 @@ std::string MSP430Disassembler::parseOperandMode(const std::string &operand) {
return "symbolic";
}

unsigned MSP430Disassembler::countStackAccesses(const Instruction &instr, bool fpIsR4) {
const std::string &m = instr.mnemonic;
if (m == "pushm" || m == "popm") {
static const std::regex countPattern(R"(#(\d+))");
std::smatch match;
if (std::regex_search(instr.operands, match, countPattern))
return static_cast<unsigned>(std::stoul(match[1]));
return 1;
}

static const std::regex spOrFpOperand(R"(@[rR][14]\+?|.*\([rR][14]\))");
static const std::regex spOperand(R"(@[rR]1\+?|.*\([rR]1\))");
const std::regex &pattern = fpIsR4 ? spOrFpOperand : spOperand;

unsigned count = 0;
std::string current;
auto flush = [&]() {
current.erase(0, current.find_first_not_of(" \t"));
if (!current.empty()) {
current.erase(current.find_last_not_of(" \t") + 1);
if (std::regex_match(current, pattern))
count++;
}
current.clear();
};
for (char ch : instr.operands) {
if (ch == ',')
flush();
else
current += ch;
}
flush();

// These push or pop one word on top of whatever their operand addresses,
// so a stack-relative operand costs an access of its own.
if (m == "push" || m == "pop" || m == "call" || m == "calla" || m == "ret" || m == "reta" ||
m == "reti")
return count + 1;
return count;
}

} // namespace bbanalyzer
5 changes: 5 additions & 0 deletions passes/bb-energy-analyzer/MSP430Disassembler.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ class MSP430Disassembler {
/// @return List of instructions with addresses
std::vector<Instruction> disassemble(const std::string &elfPath);

/// Number of stack memory accesses an instruction performs: operands
/// addressed through SP (r1) or, when *fpIsR4*, the frame pointer (r4),
/// plus the implicit push/pop of push, pop, call, ret and pushm/popm.
static unsigned countStackAccesses(const Instruction &instr, bool fpIsR4);

/// Parse instruction and relocation lines from msp430-elf-objdump -d -r output.
/// Exposed separately so parsing can be tested without invoking the external tool.
/// Mnemonic width suffixes (e.g., .b/.w) are normalized away because the
Expand Down
4 changes: 4 additions & 0 deletions passes/include/milp/AllocaToGlobalPass.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

namespace checkpoint {

/// Metadata kind marking a global that AllocaToGlobalPass created from an
/// alloca; its contents are undefined at function entry.
inline constexpr const char *PromotedAllocaMD = "ckpt.promoted_alloca";

/// Rewrites main's allocas into internal globals in .fram.
///
/// MILP links with the stack in SRAM, so a live stack object is copied to
Expand Down
14 changes: 14 additions & 0 deletions passes/include/milp/UnifyAnnotatedLoopPass.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#pragma once

#include "llvm/IR/PassManager.h"

namespace checkpoint {

/// Restores a single latch when LLVM split one annotated source loop into a
/// nested pair. Only the form that needs no PHI synthesis is changed.
class UnifyAnnotatedLoopPass : public llvm::PassInfoMixin<UnifyAnnotatedLoopPass> {
public:
llvm::PreservedAnalyses run(llvm::Function &F, llvm::FunctionAnalysisManager &AM);
};

} // namespace checkpoint
10 changes: 10 additions & 0 deletions passes/src/common/PassRegistry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "milp/AllocaToGlobalPass.h"
#include "milp/LoopStripMiningPass.h"
#include "milp/MILPCheckpointPass.h"
#include "milp/UnifyAnnotatedLoopPass.h"
#include "rockclimb/RockClimbLoopUnrollPass.h"
#include "schematic/CallIsolation.h"
#include "schematic/SchematicPass.h"
Expand Down Expand Up @@ -80,12 +81,17 @@ extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo llvmGetPassPluginIn
return {LLVM_PLUGIN_API_VERSION, "CheckpointPasses", LLVM_VERSION_STRING, [](PassBuilder &PB) {
PB.registerPipelineParsingCallback([](StringRef Name, FunctionPassManager &FPM,
ArrayRef<PassBuilder::PipelineElement>) {
if (Name == "milp-unify-annotated-loops") {
FPM.addPass(checkpoint::UnifyAnnotatedLoopPass());
return true;
}
if (Name == "checkpoint") {
FPM.addPass(checkpoint::AllocaToGlobalPass());
FPM.addPass(LoopSimplifyPass());
FPM.addPass(LCSSAPass());
FPM.addPass(createFunctionToLoopPassAdaptor(LoopRotatePass()));
FPM.addPass(createFunctionToLoopPassAdaptor(IndVarSimplifyPass()));
FPM.addPass(checkpoint::UnifyAnnotatedLoopPass());
FPM.addPass(checkpoint::LoopStripMiningPass());
FPM.addPass(checkpoint::MILPCheckpointPass());
return true;
Expand All @@ -100,6 +106,7 @@ extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo llvmGetPassPluginIn
FPM.addPass(LCSSAPass());
FPM.addPass(createFunctionToLoopPassAdaptor(LoopRotatePass()));
FPM.addPass(createFunctionToLoopPassAdaptor(IndVarSimplifyPass()));
FPM.addPass(checkpoint::UnifyAnnotatedLoopPass());
FPM.addPass(checkpoint::LoopStripMiningPass());
FPM.addPass(checkpoint::MILPCheckpointPass());
return true;
Expand All @@ -116,6 +123,7 @@ extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo llvmGetPassPluginIn
FPM.addPass(LCSSAPass());
FPM.addPass(createFunctionToLoopPassAdaptor(LoopRotatePass()));
FPM.addPass(createFunctionToLoopPassAdaptor(IndVarSimplifyPass()));
FPM.addPass(checkpoint::UnifyAnnotatedLoopPass());
FPM.addPass(checkpoint::LoopStripMiningPass());
FPM.addPass(checkpoint::BBFreqCollectorPass());
return true;
Expand All @@ -126,6 +134,7 @@ extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo llvmGetPassPluginIn
FPM.addPass(LCSSAPass());
FPM.addPass(createFunctionToLoopPassAdaptor(LoopRotatePass()));
FPM.addPass(createFunctionToLoopPassAdaptor(IndVarSimplifyPass()));
FPM.addPass(checkpoint::UnifyAnnotatedLoopPass());
FPM.addPass(checkpoint::LoopStripMiningPass());
return true;
}
Expand Down Expand Up @@ -165,6 +174,7 @@ extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo llvmGetPassPluginIn
FunctionPassManager FPM;
FPM.addPass(LoopSimplifyPass());
FPM.addPass(LCSSAPass());
FPM.addPass(checkpoint::UnifyAnnotatedLoopPass());
MPM.addPass(createModuleToFunctionPassAdaptor(std::move(FPM)));
MPM.addPass(checkpoint::SchematicPass());
return true;
Expand Down
4 changes: 4 additions & 0 deletions passes/src/milp/AllocaToGlobalPass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "llvm/IR/GlobalVariable.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/IntrinsicInst.h"
#include "llvm/IR/Metadata.h"
#include "llvm/IR/Module.h"
#include "llvm/TargetParser/Triple.h"

Expand Down Expand Up @@ -73,6 +74,9 @@ PreservedAnalyses AllocaToGlobalPass::run(Function &F, FunctionAnalysisManager &
if (useFramSection)
GV->setSection(".fram");
GV->setAlignment(AI->getAlign());
// The alloca's contents are undefined at function entry, so liveness
// can treat the global as dead there instead of charging a restore.
GV->setMetadata(PromotedAllocaMD, MDNode::get(M.getContext(), {}));

eraseLifetimeMarkers(AI);
AI->replaceAllUsesWith(GV);
Expand Down
10 changes: 10 additions & 0 deletions passes/src/milp/LivenessAnalysis.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "milp/LivenessAnalysis.h"

#include "milp/AllocaToGlobalPass.h"

#include "llvm/IR/CFG.h"
#include "llvm/IR/DataLayout.h"
#include "llvm/IR/Instructions.h"
Expand Down Expand Up @@ -71,6 +73,14 @@ computeEligibleLiveness(llvm::Function &F, llvm::AAResults &AA, const CFGAnalysi
}
}

// A promoted alloca holds no defined value at main's entry, so
// nothing before the entry block's first full store needs saving.
if (&BB == &F.getEntryBlock() && F.getName() == "main" &&
GV->hasMetadata(PromotedAllocaMD)) {
info.loadBeforeMustStore = false;
info.hasMustStore = true;
}

blockGVInfo[key] = info;
}
}
Expand Down
101 changes: 101 additions & 0 deletions passes/src/milp/UnifyAnnotatedLoopPass.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#include "milp/UnifyAnnotatedLoopPass.h"

#include "common/Logger.h"

#include "llvm/Analysis/LoopInfo.h"
#include "llvm/Analysis/ValueTracking.h"
#include "llvm/IR/CFG.h"
#include "llvm/IR/Dominators.h"
#include "llvm/IR/Instructions.h"
#include "llvm/Transforms/Utils/BasicBlockUtils.h"

namespace checkpoint {

using namespace llvm;

namespace {

Loop *findSplitInnerLoop(Loop *Outer) {
MDNode *LoopID = Outer->getLoopID();
BasicBlock *OuterLatch = Outer->getLoopLatch();
if (!LoopID || !Outer->getLoopPreheader() || !OuterLatch)
return nullptr;

BasicBlock *OuterHeader = Outer->getHeader();
auto *HeaderBranch = dyn_cast<UncondBrInst>(OuterHeader->getTerminator());
if (!HeaderBranch)
return nullptr;

for (Instruction &I : *OuterHeader) {
if (!isa<PHINode>(I) && !I.isTerminator() && !isSafeToSpeculativelyExecute(&I))
return nullptr;
}

BasicBlock *InnerHeader = HeaderBranch->getSuccessor(0);
if (isa<PHINode>(InnerHeader->front()))
return nullptr;

for (Loop *Inner : Outer->getSubLoops()) {
if (Inner->getHeader() != InnerHeader || Inner->getLoopID() != LoopID)
continue;
BasicBlock *InnerLatch = Inner->getLoopLatch();
if (!InnerLatch)
return nullptr;
for (BasicBlock *Pred : predecessors(InnerHeader)) {
if (Pred != OuterHeader && Pred != InnerLatch)
return nullptr;
}
return Inner;
}
return nullptr;
}

void unifyPair(Loop *Outer, Loop *Inner) {
BasicBlock *OuterHeader = Outer->getHeader();
BasicBlock *OuterLatch = Outer->getLoopLatch();
BasicBlock *InnerHeader = Inner->getHeader();
BasicBlock *InnerLatch = Inner->getLoopLatch();
MDNode *LoopID = Outer->getLoopID();

InnerLatch->getTerminator()->replaceSuccessorWith(InnerHeader, OuterHeader);
for (PHINode &PN : OuterHeader->phis())
PN.addIncoming(&PN, InnerLatch);

SmallVector<BasicBlock *, 2> Latches = {OuterLatch, InnerLatch};
BasicBlock *Latch = SplitBlockPredecessors(OuterHeader, Latches, ".latch",
static_cast<DominatorTree *>(nullptr));
OuterLatch->getTerminator()->setMetadata(LLVMContext::MD_loop, nullptr);
InnerLatch->getTerminator()->setMetadata(LLVMContext::MD_loop, nullptr);
Latch->getTerminator()->setMetadata(LLVMContext::MD_loop, LoopID);
}

} // namespace

PreservedAnalyses UnifyAnnotatedLoopPass::run(Function &F, FunctionAnalysisManager &) {
checkpoint::initLogging();

bool Changed = false;
while (true) {
DominatorTree DT(F);
LoopInfo LI(DT);
Loop *Outer = nullptr;
Loop *Inner = nullptr;
for (Loop *L : LI.getLoopsInPreorder()) {
Inner = findSplitInnerLoop(L);
if (Inner) {
Outer = L;
break;
}
}
if (!Outer)
break;

PLOGD << "UnifyAnnotatedLoopPass: " << F.getName() << " unified "
<< Outer->getHeader()->getName() << " and " << Inner->getHeader()->getName();
unifyPair(Outer, Inner);
Changed = true;
}
return Changed ? PreservedAnalyses::none() : PreservedAnalyses::all();
}

} // namespace checkpoint
Loading
Loading