This document describes the work required to upgrade LLTFI from LLVM 15 to a current LLVM release (17–20). Tasks are split between those that require a human and those that Claude Code can handle autonomously once the build environment is ready.
Migrated from: LLVM 15.0 (/usr/lib/llvm-15)
Current LLVM version: LLVM 20.1 (/usr/lib/llvm-20)
The most difficult part of a major LLVM upgrade — migrating from the legacy
pass manager (PM) to the new PM — is already done for all core LLTFI
passes. RegisterPasses.cpp exposes llvmGetPassPluginInfo(), every core
pass has a PassInfoMixin wrapper, and instrument.py already uses
-load-pass-plugin. What remains is a set of API-level fixups and one
incomplete pass migration.
Estimated time: 2–4 hours
This is the only hard prerequisite. Claude Code cannot change what is installed on the machine, so the compile-verify-fix loop that de-risks the code changes cannot begin until this is done.
Steps:
- Install a current LLVM release via the LLVM apt repository:
wget https://apt.llvm.org/llvm.sh && chmod +x llvm.sh sudo ./llvm.sh 20 # or 17/18/19
- Update the build configuration to point at the new installation:
# Delete the old build root first rm -rf /home/karthik/Programs/LLTFI-build ./setup -LLFI_BUILD_ROOT /home/karthik/Programs/LLTFI-build \ -LLVM_SRC_ROOT /home/karthik/Programs/llvm-project \ -LLVM_DST_ROOT /usr/lib/llvm-20 \ -LLVM_GXX_BIN_DIR /usr/lib/llvm-20/bin - Attempt an initial build (
cd LLTFI-build && make) and record the errors. The output of this first build attempt is the input for Claude Code's work.
Estimated time: 2–3 hours
The instruction-construction API fixes (task C-1 below) replace
new AllocaInst/StoreInst/LoadInst calls with IRBuilder equivalents. These
compile cleanly whether correct or not, but an incorrect insertion point places
an instruction in the wrong basic block or wrong position, producing
miscompiled IR. A human who understands the intended pass semantics should
review these diffs specifically before merging. The key files to scrutinise
are FaultInjectionPass.cpp and InstTracePass.cpp.
Claude Code preliminary review (2026-04-14): Both files were inspected and the insertion points appear semantically correct:
FaultInjectionPass.cpplines 229–230, 266: uses theBasicBlock*insertAtEnd form (new AllocaInst(type, 0, name, block)), which inserts at the end of the given block. Allocas go in the entry block (standard practice); the store and load go in the respective exit block immediately after the injected value is computed. This is the same logical placement as before the API change.InstTracePass.cpplines 141–142, 144, 152, 170: usesBasicBlock::iteratorfromgetFirstNonPHIOrDbgOrLifetime()for alloca insertion (correct: before any non-PHI/dbg/lifetime instruction in the entry block), andinsertPoint->getIterator()for stores (correct: inserts immediately before the trace call). All 21 tests pass with these changes.
The remaining human task is to verify the logical placement makes sense for the pass's intended semantics, not just that it compiles and passes tests.
Estimated time: 1–2 hours
The only step that requires a human is making onnx-mlir available:
- Install onnx-mlir or set
ONNX_MLIR_BUILDto point at an existing build. - Run
compile.shinsample_programs/ml_sample_programs/vision_models/mnist/to producemodel.ll. - Build
SIDHelperFunctions.ll(needed for end-to-end numerical run):cd llvm_passes/instruction_duplication/shared_lib sh compile_shrd_lib.sh
Once model.ll and SIDHelperFunctions.ll exist, Claude Code (or the test
suite) takes over automatically. Two tests in test_instruction_duplication.py
cover the rest:
real_model_structural— applies the pass to the real onnx-mlir IR and verifies thatcompareFloatValuescalls are inserted and arithmetic instructions are duplicated. This proves the pass handles genuine onnx-mlir IR patterns, not just the synthetic fixtures used in the seven other tests.real_model_end_to_end— runs both the baselinemodel.lland the duplicated+inlined model throughlli, then asserts their outputs are identical. BecausecompareFloatValues(x, x) == x(bitwise AND of equal floats is the float itself), the outputs must match when no fault is injected. Any divergence indicates a pass transformation bug.
Both tests SKIP gracefully when their prerequisites are absent, so the suite continues to report 0 failures even before onnx-mlir is set up. Run them via:
cd /path/to/LLTFI-build/test_suite
python3 SCRIPTS/test_instruction_duplication.pyRun the full test suite against the new LLVM version and confirm no regressions:
cd /path/to/LLTFI-build/test_suite
python3 SCRIPTS/llfi_test --all_cpp # expect 21/21
python3 SCRIPTS/llfi_test --all_ml # expect all non-SKIP to passResult: 21/21 PASS. All hardware fault, trace tool, makefile
generation tests pass against LLVM 20.1. The --all_ml tests that
require optional dependencies (onnx-mlir, TensorFlow, PyTorch) are reported as
SKIP (not FAIL) on machines where those are not installed.
Estimated time: 1 day
LLVM 16/17 removed the InsertBefore Instruction-pointer parameter from
AllocaInst, StoreInst, and LoadInst constructors. All 9 affected sites
must be replaced with IRBuilder equivalents.
Affected files and sites:
| File | Sites | Pattern |
|---|---|---|
llvm_passes/core/FaultInjectionPass.cpp |
3 | new AllocaInst(...), new StoreInst(...), new LoadInst(...) |
llvm_passes/core/InstTracePass.cpp |
6 | new AllocaInst(...) ×3, new StoreInst(...) ×3 |
Example replacement:
// Before (LLVM 15)
AllocaInst *tmploc = new AllocaInst(fitype, 0, "tmploc", entryblock);
// After (LLVM 17+)
IRBuilder<> builder(&entryblock->front());
AllocaInst *tmploc = builder.CreateAlloca(fitype, nullptr, "tmploc");Note: CallInst::Create(func, args, "", insertPoint) with a raw Instruction *
as the last argument may also need updating to use a BasicBlock::iterator;
check each call site after the build reveals errors.
Estimated time: 2–3 hours
Two methods changed their return type from Instruction * to
BasicBlock::iterator in LLVM 17:
getFirstNonPHIOrDbgOrLifetime()— used inInstTracePass.cpp:123getNextNonDebugInstruction()— used inUtils.cpp:125andInstructionDuplication.cpp:440–443
At each call site, either dereference the iterator (&*iter) or update the
surrounding code to work with iterators directly.
Estimated time: 2–3 days
InstructionDuplication.cpp is the only remaining pass still using the legacy
PM. It needs to be migrated to PassInfoMixin to match all other LLTFI passes.
Changes required:
-
InstructionDuplication.cpp: Change class base fromFunctionPasstoPassInfoMixin<InstructionDuplicationPass>. RenamerunOnFunction(Function &F)torun(Function &F, FunctionAnalysisManager &AM)returningPreservedAnalyses. Removestatic char IDandRegisterPass<>. The five existingPassInfoMixinpasses in the codebase serve as templates. -
RegisterPasses.cpp: Add aregisterPipelineParsingCallbackentry forInstructionDuplicationPass, following the existing pattern for the other passes. Decide whetherSEDPasses.soshould be merged into the mainllfi-passes.soor kept separate; keeping it separate is simpler. -
llvm_passes/instruction_duplication/CMakeLists.txt: Update to add thellvmGetPassPluginInfoexport ifSEDPasses.sostays separate, or adjust linking if merged. -
shared_lib/build.shandcompile_shrd_lib.sh: Updateoptinvocation to remove-load/--enable-new-pm=0and use-load-pass-pluginwith--passes=InstructionDuplicationPass. -
README.md(ininstruction_duplication/): Update exampleoptcommand. -
test_suite/SCRIPTS/test_instruction_duplication.py: Update_run_passto use-load-pass-pluginand--passes=InstructionDuplicationPassinstead of-load/--InstructionDuplicationPass/--enable-new-pm=0.
Estimated time: 30 minutes
getGlobalList() was removed in LLVM 17. The one call site in
Utils.cpp:206 creates a GlobalVariable and appends it to the module.
Replace with the GlobalVariable constructor overload that takes a Module *
directly (which inserts the variable into the module automatically):
// Before (LLVM 15)
nameStr = new GlobalVariable(name_c->getType(), true,
GlobalVariable::InternalLinkage, name_c, gv_nameStr.c_str());
M.getGlobalList().push_back(nameStr);
// After (LLVM 17+)
nameStr = new GlobalVariable(M, name_c->getType(), true,
GlobalVariable::InternalLinkage, name_c, gv_nameStr.c_str());Estimated time: 1 week (wall clock; most of this is Claude Code running builds)
After applying C-1 through C-5, run make and address any remaining compiler
errors introduced by LLVM 17–20 API changes not captured above. LLVM releases
between 17 and 20 introduce additional deprecations (e.g. changes to
Value::use_iterator, DebugLoc APIs, MDNode helpers) that may surface
depending on the exact target version. Claude Code can drive this loop:
read the error, identify the fix, apply it, rebuild.
After the build was clean, clang-format-20 and clang-tidy-20 were run
across all hand-written C++ sources under llvm_passes/. In addition to style
issues, clang-tidy surfaced several real bugs:
| Bug | File | Fix |
|---|---|---|
| Double-free in singleton destructor | Controller.cpp |
Removed delete ctrl from ~Controller() — object is not heap-allocated by the time the destructor runs |
| File stream leak | LLFIDotGraphPass.cpp |
Added missing fclose(outputFile) |
Null dereference via unchecked fopen |
GenLLFIIndexPass.cpp |
Moved fclose inside the if (outputFile) block |
Uninitialized field isChainDuplication |
InstructionDuplicationPass constructor |
Added explicit isChainDuplication = false initializer |
Null getCalledFunction() dereference |
ProfilingPass.cpp, InstructionDuplication.cpp, CustomTensorOperatorInstSelector.cpp |
Added null checks before name comparison |
Unchecked null dyn_cast results |
Utils.cpp, multiple |
Changed to cast<> (asserting) where type is guaranteed by a prior opcode check; added null checks elsewhere |
Style fixes applied across 26 files: override on all overriding methods,
virtual ~Base() = default on abstract base classes, .empty() replacing
.size() == 0, initialized-at-declaration for all local pointers,
strncpy/strncat replacing unbounded strcpy/strcat, const T& in
range-for loops, and cl::opt<T>::getValue() to avoid slicing.
Infrastructure added:
.clang-tidy— project tidy config with intentionally disabled checks documentedlint.sh— unified C++ and Python lint runner (./lint.sh --fixauto-formats)CODING_GUIDELINES.md— expanded withoverride, variable initialisation, container emptiness, andcast<>vsdyn_cast<>sections
ML fault injection and instruction duplication passes:
| File | Fix |
|---|---|
ProfilingPass.cpp |
dyn_cast<CallInst> → cast<CallInst> after isa<> check in insertCallForMLFIStats() |
InstructionDuplication.cpp |
for (auto insVector : → for (const auto& insVector : to avoid copying inner vectors; removed dead return false; after exhaustive if/else |
Documentation fixes:
| File | Fix |
|---|---|
caveats.txt |
LLVM version references updated 15 → 20; duplicate item number fixed |
llvm_passes/instruction_duplication/README.md |
Final opt invocation updated from legacy PM -always-inline to --passes=always-inline (legacy PM removed in LLVM 17) |
llvm_passes/instruction_duplication/shared_lib/build.sh |
Uses LLVM_GXX_BIN_DIR env var to find versioned clang (fixes failure on Ubuntu with apt-installed LLVM where only clang-20 exists) |
llvm_passes/instruction_duplication/shared_lib/compile_shrd_lib.sh |
Same fix for clang++ |
H-1 Install LLVM 17+ and attempt initial build ✅ DONE
└─> C-1 Fix instruction-construction API ✅ DONE
└─> C-2 Fix iterator return-type changes ✅ DONE
└─> C-4 Fix getGlobalList ✅ DONE
└─> C-5 Iterative build-fix loop ✅ DONE
└─> C-6 C++ static analysis and formatting cleanup ✅ DONE
└─> C-7 ML/SID secondary pass, doc fixes ✅ DONE
H-2 Review IRBuilder insertion-point diffs Pending
└─> C-3 Migrate InstructionDuplication pass ✅ DONE
H-3 Validate InstructionDuplication on onnx-mlir IR Pending
H-4 Final test sign-off ✅ DONE (21/21)
| Task | Owner | Status | Estimated time |
|---|---|---|---|
| H-1: Install LLVM 20 and update build config | Human | ✅ Done | 2–4 hours |
| H-2: Review IRBuilder insertion-point correctness | Human | Pending | 2–3 hours |
| H-3: Provide onnx-mlir environment (install + compile.sh) | Human | Pending | 1–2 hours |
| H-4: Final test sign-off | Human | ✅ Done (21/21) | — |
| Total human time remaining | ~3–5 hours | ||
| C-1: Deprecated instruction-construction API | Claude Code | ✅ Done | — |
| C-2: Iterator return-type fixes | Claude Code | ✅ Done | — |
| C-3: InstructionDuplication new PM migration | Claude Code | ✅ Done | — |
C-4: getGlobalList fix |
Claude Code | ✅ Done | — |
| C-5: Iterative build-fix loop | Claude Code | ✅ Done | — |
| C-6: C++ static analysis and formatting cleanup | Claude Code | ✅ Done | — |
| C-7: ML/SID, doc fixes | Claude Code | ✅ Done | — |
| Total Claude Code time remaining | None — all done |
Without Claude Code, a human developer would need approximately 2–3 weeks of active work. With Claude Code handling the mechanical fixes and the build-fix loop, human involvement drops to roughly 1.5–2 days of active effort, with Claude Code running largely autonomously in between.
- Subtle IRBuilder insertion-point bugs: compile successfully but produce miscompiled IR. Mitigated by H-2 (human review) and the existing test suite.
- New PM semantics differences: the new PM runs passes in a different order
and does not support inter-pass mutable state in the same way. The
Controllersingleton used by the FI selectors should be checked for thread-safety assumptions that the new PM may violate. - onnx-mlir compatibility: onnx-mlir targets a specific LLVM version internally. If the onnx-mlir build and the LLTFI build target different LLVM versions, llvm-link may refuse to link the bitcode. This is an environment concern, not a code concern, but it could block H-3.