Skip to content

Commit f2d126b

Browse files
committed
mlir: delegate block-level liveness to mlir::Liveness
1 parent 96805b2 commit f2d126b

1 file changed

Lines changed: 61 additions & 121 deletions

File tree

src/executable/mlir/Target/PythonBytecode/LiveAnalysis.hpp

Lines changed: 61 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "Dialect/EmitPythonBytecode/IR/EmitPythonBytecode.hpp"
44
#include "RegisterAllocationLogger.hpp"
55
#include "RegisterAllocationTypes.hpp"
6+
#include "mlir/Analysis/Liveness.h"
67
#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h"
78
#include "mlir/Dialect/Func/IR/FuncOps.h"
89
#include "mlir/IR/AsmState.h"
@@ -21,15 +22,25 @@
2122
namespace codegen {
2223

2324
/**
24-
* LiveAnalysis performs backward dataflow analysis to determine which values are alive
25-
* at each point in the program. This is the first step in register allocation.
25+
* LiveAnalysis determines which values are alive at each point in the program.
26+
* This is the first step in register allocation.
2627
*
27-
* Uses the standard liveness algorithm:
28-
* LiveOut[B] = union of LiveIn[S] for all successors S of B
29-
* LiveIn[B] = Use[B] ∪ (LiveOut[B] - Def[B])
30-
* Iterate until fixed point
28+
* The block-level fixed-point dataflow is delegated to mlir::Liveness — it
29+
* already implements the standard LiveOut[B] = LiveIn[S] for S in succ(B),
30+
* LiveIn[B] = Use[B] ∪ (LiveOut[B] - Def[B]) algorithm and handles loops /
31+
* back-edges. What's project-specific stays here:
3132
*
32-
* This correctly handles loops and back edges.
33+
* 1. ForwardedOutput for FOR_ITER. The body block's first argument is the
34+
* loop value produced by FOR_ITER's terminator, which MLIR can't model
35+
* natively. We track it as a synthetic Value-like entity.
36+
*
37+
* 2. alive_at_timestep — a linearised per-operation list of "what's alive
38+
* here". mlir::Liveness only exposes per-block / per-value queries; the
39+
* register allocator wants the linearised view, so we materialise it on
40+
* top of mlir::Liveness's block-level results.
41+
*
42+
* 3. block_input_mappings — for each value, the set of block arguments it
43+
* could flow into via a CFG edge. Computed from the terminator operands.
3344
*/
3445
class LiveAnalysis
3546
{
@@ -53,18 +64,20 @@ class LiveAnalysis
5364
block_input_mappings;
5465

5566
/**
56-
* Analyze the function to determine liveness information using backward dataflow
67+
* Analyze the function to determine liveness information.
5768
*/
5869
void analyse(mlir::func::FuncOp &fn)
5970
{
6071
auto logger = get_regalloc_logger();
61-
logger->info(
62-
"Starting backward dataflow live analysis for function: {}", fn.getName().str());
72+
logger->info("Starting live analysis for function: {}", fn.getName().str());
6373

6474
auto &region = fn.getRegion();
6575
auto sorted_blocks = sortBlocks(region);
6676

67-
// Build block information and Use/Def sets
77+
// Collect block operations and the project-specific bits MLIR doesn't
78+
// model natively (ForwardedOutputs from FOR_ITER, value→block-arg
79+
// edge mapping). Use/def computation is no longer needed here —
80+
// mlir::Liveness owns the dataflow.
6881
std::map<mlir::Block *, BlockInfo> block_info;
6982
std::vector<std::pair<std::variant<mlir::Value, ForwardedOutput>, mlir::BlockArgument>>
7083
block_parameters_to_args;
@@ -73,12 +86,13 @@ class LiveAnalysis
7386
build_block_info(block, block_info[block], block_parameters_to_args, logger);
7487
}
7588

76-
// Run backward dataflow to compute LiveIn/LiveOut
77-
compute_liveness(sorted_blocks, block_info, logger);
89+
// Block-level live-in / live-out via the upstream analysis.
90+
mlir::Liveness liveness(fn);
7891

79-
// Build alive_at_timestep from LiveIn/LiveOut
92+
// Materialise alive_at_timestep on top of mlir::Liveness's block
93+
// results, injecting ForwardedOutputs into the live sets as needed.
8094
std::map<mlir::Block *, std::pair<size_t, size_t>> blocks_span;
81-
build_timesteps(sorted_blocks, block_info, blocks_span);
95+
build_timesteps(sorted_blocks, block_info, liveness, blocks_span);
8296

8397
// Propagate block argument inputs through the liveness information
8498
propagate_block_arguments(block_parameters_to_args, blocks_span);
@@ -91,22 +105,13 @@ class LiveAnalysis
91105

92106
private:
93107
/**
94-
* Information about a single block for dataflow analysis
108+
* Per-block ancillary state: the operation list (in topological order,
109+
* needed because the linearised timesteps must match the order ops are
110+
* walked by the bytecode emitter) plus the ForwardedOutputs created by
111+
* this block's terminator (FOR_ITER's synthetic loop-var value).
95112
*/
96113
struct BlockInfo
97114
{
98-
// Values used in this block (before being defined)
99-
ValueSet use;
100-
101-
// Values defined in this block
102-
ValueSet def;
103-
104-
// Values live at entry to this block (computed by dataflow)
105-
ValueSet live_in;
106-
107-
// Values live at exit from this block (computed by dataflow)
108-
ValueSet live_out;
109-
110115
// Operations in this block (in order)
111116
std::vector<mlir::Operation *> operations;
112117

@@ -115,7 +120,8 @@ class LiveAnalysis
115120
};
116121

117122
/**
118-
* Build Use/Def sets for a block
123+
* Collect operations and project-specific terminator metadata for a block.
124+
* Block-level live-in/out is computed separately by mlir::Liveness.
119125
*/
120126
void build_block_info(mlir::Block *block,
121127
BlockInfo &info,
@@ -129,26 +135,12 @@ class LiveAnalysis
129135
std::abort();
130136
}
131137

132-
// Build Use/Def sets
133-
// For each operation, add operands to Use (if not already in Def), and add results to Def
134-
for (auto &op : block->getOperations()) {
135-
info.operations.push_back(&op);
136-
137-
// Add operands to Use (if not already defined)
138-
for (const auto &operand : op.getOperands()) {
139-
if (!info.def.contains(operand)) { info.use.insert(operand); }
140-
}
141-
142-
// Add results to Def
143-
for (const auto &result : op.getResults()) { info.def.insert(result); }
144-
}
138+
for (auto &op : block->getOperations()) { info.operations.push_back(&op); }
145139

146140
// Handle terminators specially
147141
if (auto *terminator = block->getTerminator()) {
148142
handle_terminator(terminator, info, block_parameters_to_args, logger);
149143
}
150-
151-
logger->debug("Block has {} uses, {} defs", info.use.size(), info.def.size());
152144
}
153145

154146
/**
@@ -217,77 +209,15 @@ class LiveAnalysis
217209
}
218210

219211
/**
220-
* Compute LiveIn/LiveOut using backward dataflow iteration
221-
*/
222-
void compute_liveness(const std::vector<mlir::Block *> &sorted_blocks,
223-
std::map<mlir::Block *, BlockInfo> &block_info,
224-
std::shared_ptr<spdlog::logger> &logger)
225-
{
226-
logger->info("Running backward dataflow iteration to compute liveness");
227-
228-
// Initialize all LiveIn and LiveOut to empty (already done by default)
229-
230-
// Iterate until fixed point
231-
bool changed = true;
232-
int iteration = 0;
233-
234-
while (changed) {
235-
changed = false;
236-
iteration++;
237-
238-
logger->debug("Dataflow iteration {}", iteration);
239-
240-
// Process blocks in reverse post-order for better convergence
241-
for (auto it = sorted_blocks.rbegin(); it != sorted_blocks.rend(); ++it) {
242-
auto *block = *it;
243-
auto &info = block_info[block];
244-
245-
// Save old LiveIn for convergence check
246-
auto old_live_in = info.live_in;
247-
248-
// LiveOut[B] = union of LiveIn[S] for all successors S
249-
info.live_out.clear();
250-
for (auto *successor : block->getSuccessors()) {
251-
const auto &succ_info = block_info[successor];
252-
info.live_out.insert(succ_info.live_in.begin(), succ_info.live_in.end());
253-
}
254-
255-
// LiveIn[B] = Use[B] ∪ (LiveOut[B] - Def[B])
256-
info.live_in = info.use;
257-
for (const auto &val : info.live_out) {
258-
if (!info.def.contains(val)) { info.live_in.insert(val); }
259-
}
260-
261-
// Add ForwardedOutputs to LiveIn (they're "defined" by the terminator but need
262-
// to be live for the successor)
263-
for (const auto &fwd : info.forwarded_outputs) { info.live_in.insert(fwd); }
264-
265-
// Check if LiveIn changed
266-
if (info.live_in != old_live_in) { changed = true; }
267-
}
268-
}
269-
270-
logger->info("Dataflow converged after {} iterations", iteration);
271-
272-
// Debug: print LiveIn/LiveOut for each block
273-
for (auto *block : sorted_blocks) {
274-
const auto &info = block_info[block];
275-
logger->debug("Block {} LiveIn: {} values, LiveOut: {} values",
276-
static_cast<void *>(block),
277-
info.live_in.size(),
278-
info.live_out.size());
279-
}
280-
}
281-
282-
/**
283-
* Build alive_at_timestep from LiveIn/LiveOut
212+
* Build alive_at_timestep from mlir::Liveness's block live-out info.
284213
*
285214
* Computes precise per-operation liveness by propagating backward within each block.
286215
* This ensures values are only marked alive when actually needed, not conservatively
287216
* throughout the entire block.
288217
*/
289218
void build_timesteps(const std::vector<mlir::Block *> &sorted_blocks,
290219
const std::map<mlir::Block *, BlockInfo> &block_info,
220+
const mlir::Liveness &liveness,
291221
std::map<mlir::Block *, std::pair<size_t, size_t>> &blocks_span)
292222
{
293223
auto logger = get_regalloc_logger();
@@ -301,8 +231,13 @@ class LiveAnalysis
301231
std::vector<ValueSet> alive_before_op;
302232
alive_before_op.resize(info.operations.size());
303233

304-
// Start from LiveOut (values alive at block exit) and work backward
305-
ValueSet alive_after = info.live_out;
234+
// Start from LiveOut (values alive at block exit) and work backward.
235+
// mlir::Liveness returns a SmallPtrSet<Value>; the project's
236+
// ValueSet is a variant<Value, ForwardedOutput> set, so we
237+
// convert. ForwardedOutputs aren't part of mlir::Liveness's view
238+
// and are added separately below.
239+
ValueSet alive_after;
240+
for (mlir::Value v : liveness.getLiveOut(block)) { alive_after.insert(v); }
306241

307242
for (size_t i = info.operations.size(); i-- > 0;) {
308243
auto *op = info.operations[i];
@@ -349,19 +284,24 @@ class LiveAnalysis
349284
}
350285
}
351286

352-
// Add ForwardedOutputs to the first operation if they're in LiveIn
353-
// (they're "defined" by the terminator but need to be live for the successor)
287+
// Add ForwardedOutputs to the first operation's alive set.
288+
// These are "defined" by the terminator but need to be live
289+
// throughout the block so the successor (the FOR_ITER body)
290+
// can pick the loop variable up via the same register. The
291+
// previous custom dataflow unconditionally injected these
292+
// into LiveIn before this point; mlir::Liveness doesn't know
293+
// about them, so they're injected here directly.
354294
if (!info.operations.empty()) {
355-
for (const auto &fwd : info.forwarded_outputs) {
356-
if (info.live_in.contains(fwd)) { alive_before_op[0].insert(fwd); }
357-
}
295+
for (const auto &fwd : info.forwarded_outputs) { alive_before_op[0].insert(fwd); }
358296
}
359297

360-
// Note: alive_before_op[0] may differ from LiveIn because the needs_tracking
361-
// pass above adds impure operation results to the timestep of their defining op.
362-
// These results are defined within this block so they cannot be in LiveIn.
363-
// This discrepancy is expected and intentional — it ensures impure ops get
364-
// proper register assignments at their definition site.
298+
// Note: alive_before_op[0] may differ from mlir::Liveness's
299+
// LiveIn(block) because the needs_tracking pass above adds
300+
// impure operation results to the timestep of their defining
301+
// op. These results are defined within this block so they
302+
// cannot be in LiveIn. The discrepancy is intentional — it
303+
// ensures impure ops get a register assignment at their
304+
// definition site.
365305

366306
// Now build timesteps in forward order using the computed liveness
367307
for (size_t i = 0; i < info.operations.size(); i++) {
@@ -382,7 +322,7 @@ class LiveAnalysis
382322
start,
383323
end,
384324
info.operations.size(),
385-
info.live_in.size());
325+
liveness.getLiveIn(block).size());
386326
if (block->getTerminator()
387327
&& mlir::isa<mlir::emitpybytecode::ForIter>(block->getTerminator())) {
388328
logger->debug(" ^ FOR_ITER block");

0 commit comments

Comments
 (0)