Skip to content

Commit 4a3cc60

Browse files
authored
Introduce Rewriter abstraction to cache use lookups (#235)
As observed in #233, compilation of large kernels is slow. The culprit turned out to be the user/use lookup, which walks the entire IR every time since our IR doesn't encode use-def chains (as opposed to LLVM or MLIR). To avoid this cost, introduce a Rewriter abstraction that caches these lookups while invalidating them upon insertion, mutation, etc. Once again modeled after MLIR.
1 parent 5578859 commit 4a3cc60

4 files changed

Lines changed: 373 additions & 85 deletions

File tree

src/compiler/transform/pipeline.jl

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,25 +95,25 @@ function commute_arith_transparent(sci, block, inst, match, driver)
9595
# Insert broadcast of the scalar to x's shape and register as constant
9696
x_shape = size(xT)
9797
bc_type = Tile{eltype(xT), Tuple{x_shape...}}
98-
bc = insert_before!(block, val, Expr(:call, Intrinsics.broadcast, scalar, x_shape), bc_type;
98+
bc = insert_before!(driver.rewriter, block, val,
99+
Expr(:call, Intrinsics.broadcast, scalar, x_shape), bc_type;
99100
flag=inferred_flags(Intrinsics.broadcast))
100-
notify_insert!(driver, block, bc)
101101
# Side-inject the freshly synthesized constant into the dataflow result so
102102
# downstream pattern matches see it. Bypasses tmerge (this is a brand-new
103103
# SSA value, not a merge).
104104
driver.constants[SSAValue(bc)] = convert(eltype(xT), scalar)
105105

106106
# Insert op(x, broadcast) with x's type
107-
op = insert_before!(block, val, Expr(:call, root_func, x, SSAValue(bc)), xT;
107+
op = insert_before!(driver.rewriter, block, val,
108+
Expr(:call, root_func, x, SSAValue(bc)), xT;
108109
flag=inferred_flags(root_func))
109-
notify_insert!(driver, block, op)
110110

111111
# Replace root with transparent_op(op_result, s). Func changes
112112
# (subi/addi → reshape/broadcast), so recompute the flag from the new
113113
# func's declared effects — the inferred bits describe the OLD op.
114-
block[val.id] = (stmt=Expr(:call, transparent_func, SSAValue(op), match.bindings[:s]),
115-
flag=inferred_flags(transparent_func))
116-
driver.defs[val] = DefEntry(block, val, transparent_func)
114+
replace_stmt!(driver.rewriter, block, val,
115+
Expr(:call, transparent_func, SSAValue(op), match.bindings[:s]);
116+
flag=inferred_flags(transparent_func))
117117
push!(driver.worklist, val)
118118
add_users_to_worklist!(driver, val)
119119
return true

src/compiler/transform/rewrite.jl

Lines changed: 86 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ function def_operands(entry::DefEntry)
217217
end
218218

219219
mutable struct RewriteDriver
220-
sci::StructuredIRCode
220+
rewriter::Rewriter
221221
defs::Dict{SSAValue, DefEntry}
222222
dispatch::Dict{Any, Vector{RewriteRule}}
223223
worklist::Worklist
@@ -226,47 +226,66 @@ mutable struct RewriteDriver
226226
max_rewrites::Int
227227
end
228228

229-
"""Compute fresh use count for an SSA value."""
230-
use_count(driver::RewriteDriver, val::SSAValue) =
231-
length(uses(driver.sci.entry, val))
232-
233229
#=============================================================================
234-
Notifications
230+
Rewriter listener: driver-side bookkeeping reacts to IR mutations
235231
=============================================================================#
236232

237-
"""Add operand-producing instructions to the worklist (enables cascading)."""
238-
function add_operands_to_worklist!(driver::RewriteDriver, entry::DefEntry)
239-
for op in def_operands(entry)
240-
op isa SSAValue || continue
241-
haskey(driver.defs, op) && push!(driver.worklist, op)
242-
end
233+
# The driver has its own state on top of the Rewriter's index: a `defs` map
234+
# keyed by func, the `worklist`, and a `modified` set for cascading. These
235+
# `notify_*` hooks let the Rewriter keep that state in sync, so the driver
236+
# doesn't need to wrap every mutation callsite. Same pattern as MLIR's
237+
# `RewriterBase::Listener::notifyOperation{Inserted,Modified,Erased}`.
238+
239+
function notify_inserted!(d::RewriteDriver, block::Block, inst::Instruction)
240+
stmt = inst[:stmt]
241+
call = resolve_call(block, stmt)
242+
call === nothing && return
243+
func, _ = call
244+
val = SSAValue(inst)
245+
d.defs[val] = DefEntry(block, val, func)
246+
push!(d.worklist, val)
243247
end
244248

245-
"""Add instructions that use `val` to the worklist (their operand changed)."""
246-
function add_users_to_worklist!(driver::RewriteDriver, val::SSAValue)
247-
for inst in users(driver.sci.entry, val)
248-
push!(driver.worklist, SSAValue(inst))
249+
function notify_modified!(d::RewriteDriver, block::Block, val::SSAValue,
250+
@nospecialize(old_stmt), @nospecialize(new_stmt))
251+
# Refresh the def entry so worklist dispatch picks the new func.
252+
call = resolve_call(block, new_stmt)
253+
if call !== nothing
254+
func, _ = call
255+
d.defs[val] = DefEntry(block, val, func)
249256
end
257+
# Worklist cascading is done by the callers (`apply_rewrite!`,
258+
# `apply_inplace_rewrite!`, `commute_arith_transparent`), which re-seed
259+
# `val` and its users themselves.
250260
end
251261

252-
"""Erase an instruction and notify the worklist."""
253-
function erase_op!(driver::RewriteDriver, entry::DefEntry)
254-
add_operands_to_worklist!(driver, entry)
255-
if haskey(entry.block, entry.val.id)
256-
delete!(entry.block, entry.val.id)
262+
function notify_erased!(d::RewriteDriver, ::Block, val::SSAValue,
263+
@nospecialize(old_stmt))
264+
# Operand-defs may now be dead; cascade them to the worklist.
265+
if old_stmt isa Expr
266+
for_expr_operands(old_stmt) do op
267+
op isa SSAValue || return
268+
haskey(d.defs, op) && push!(d.worklist, op)
269+
end
257270
end
258-
delete!(driver.defs, entry.val)
259-
remove!(driver.worklist, entry.val)
271+
delete!(d.defs, val)
272+
remove!(d.worklist, val)
273+
delete!(d.modified, val)
260274
end
261275

262-
"""Register a newly inserted instruction."""
263-
function notify_insert!(driver::RewriteDriver, block::Block, inst::Instruction)
264-
val = SSAValue(inst)
265-
call = resolve_call(block, inst)
266-
call === nothing && return
267-
func, _ = call
268-
driver.defs[val] = DefEntry(block, val, func)
269-
push!(driver.worklist, val)
276+
#=============================================================================
277+
Driver-side query helpers (thin proxies to the Rewriter)
278+
=============================================================================#
279+
280+
users_of(driver::RewriteDriver, val::SSAValue) = users(driver.rewriter, val)
281+
282+
use_count(driver::RewriteDriver, val::SSAValue) = use_count(driver.rewriter, val)
283+
284+
"""Add instructions that use `val` to the worklist (their operand changed)."""
285+
function add_users_to_worklist!(driver::RewriteDriver, val::SSAValue)
286+
for u in users_of(driver, val)
287+
push!(driver.worklist, u)
288+
end
270289
end
271290

272291
#=============================================================================
@@ -291,7 +310,7 @@ function merge_bindings!(dest::Dict{Symbol,Any}, src::Dict{Symbol,Any})
291310
end
292311

293312
function pattern_match(driver::RewriteDriver, @nospecialize(val), pat::PCall,
294-
block::Block=driver.sci.entry)
313+
block::Block=driver.rewriter.sci.entry)
295314
val isa SSAValue || return nothing
296315
entry = get(driver.defs, val, nothing)
297316
entry === nothing && return nothing
@@ -322,19 +341,19 @@ function pattern_match(driver::RewriteDriver, @nospecialize(val), pat::PCall,
322341
return nothing
323342
end
324343

325-
pattern_match(driver::RewriteDriver, @nospecialize(val), pat::PBind, block::Block=driver.sci.entry) =
344+
pattern_match(driver::RewriteDriver, @nospecialize(val), pat::PBind, block::Block=driver.rewriter.sci.entry) =
326345
MatchResult(Dict{Symbol,Any}(pat.name => val), SSAValue[])
327346

328347
function pattern_match(driver::RewriteDriver, @nospecialize(val), pat::PTypedBind,
329-
block::Block=driver.sci.entry)
348+
block::Block=driver.rewriter.sci.entry)
330349
T = value_type(block, val)
331350
T === nothing && return nothing
332351
CC.widenconst(T) <: pat.type || return nothing
333352
MatchResult(Dict{Symbol,Any}(pat.name => val), SSAValue[])
334353
end
335354

336355
function pattern_match(driver::RewriteDriver, @nospecialize(val), pat::POneUse,
337-
block::Block=driver.sci.entry)
356+
block::Block=driver.rewriter.sci.entry)
338357
val isa SSAValue && use_count(driver, val) == 1 || return nothing
339358
pattern_match(driver, val, pat.inner, block)
340359
end
@@ -343,7 +362,7 @@ end
343362
# For non-SSA operands (enum constants, predicates): checks ===.
344363
# For SSA operands: routed through const_value on the ConstantAnalysis result.
345364
function pattern_match(driver::RewriteDriver, @nospecialize(val), pat::PLiteral,
346-
block::Block=driver.sci.entry)
365+
block::Block=driver.rewriter.sci.entry)
347366
val === pat.val && return MatchResult(Dict{Symbol,Any}(), SSAValue[])
348367
if val isa SSAValue
349368
c = const_value(driver.constants, val)
@@ -384,9 +403,8 @@ function resolve_rhs(driver::RewriteDriver, block, ref, op::RCall, bindings, roo
384403
typ = CC.widenconst(t)
385404
break
386405
end
387-
inst = insert_before!(block, ref, Expr(:call, op.func, operands...), typ;
406+
inst = insert_before!(driver.rewriter, block, ref, Expr(:call, op.func, operands...), typ;
388407
flag=inferred_flags(op.func))
389-
notify_insert!(driver, block, inst)
390408
SSAValue(inst)
391409
end
392410

@@ -406,12 +424,8 @@ function apply_inplace_rewrite!(driver::RewriteDriver, block, val::SSAValue, rul
406424
# the flag from the new func's declared effects (`efunc` overrides),
407425
# mirroring inference's `flags_for_effects` — the inferred bits on the
408426
# old call describe the OLD op and don't carry over.
409-
if rule.rhs.func === driver.defs[val].func
410-
block[val.id] = (stmt=new_stmt,)
411-
else
412-
block[val.id] = (stmt=new_stmt, flag=inferred_flags(rule.rhs.func))
413-
end
414-
driver.defs[val] = DefEntry(block, val, rule.rhs.func)
427+
flag = rule.rhs.func === driver.defs[val].func ? nothing : inferred_flags(rule.rhs.func)
428+
replace_stmt!(driver.rewriter, block, val, new_stmt; flag)
415429
push!(driver.worklist, val)
416430
add_users_to_worklist!(driver, val)
417431
return true
@@ -438,8 +452,8 @@ function resolve_inplace_rhs(driver, bindings, op::RCall, lhs_op::PCall)
438452
for (sub_rhs, sub_lhs) in zip(op.operands, lhs_op.operands)]
439453
# Sub-call func is enforced equal to the matched lhs func above
440454
# (`op.func === lhs_op.func`), so the flag is still valid — only operands
441-
# changed. Partial-NamedTuple setindex preserves type and flag.
442-
entry.block[matched_ssa.id] = (stmt=Expr(:call, op.func, new_ops...),)
455+
# changed.
456+
replace_stmt!(driver.rewriter, entry.block, matched_ssa, Expr(:call, op.func, new_ops...))
443457
push!(driver.worklist, matched_ssa)
444458
return matched_ssa
445459
end
@@ -454,25 +468,22 @@ The matched_ssas in MatchResult are ordered root-first, but we need to find
454468
the specific SSA for a sub-pattern. We do this by looking up the first operand's
455469
binding and finding the op that defines it."""
456470
function find_matched_ssa(driver, pat::PCall, bindings)
457-
entry = driver.sci.entry
458471
for sub in pat.operands
459472
if sub isa PBind
460473
bound = get(bindings, sub.name, nothing)
461474
bound isa SSAValue || continue
462-
for inst in users(entry, bound)
463-
call = resolve_call(entry, inst)
464-
call === nothing && continue
465-
func, _ = call
466-
func === pat.func && return SSAValue(inst)
475+
for u in users_of(driver, bound)
476+
def_entry = get(driver.defs, u, nothing)
477+
def_entry === nothing && continue
478+
def_entry.func === pat.func && return u
467479
end
468480
elseif sub isa PCall
469481
inner_ssa = find_matched_ssa(driver, sub, bindings)
470482
if inner_ssa !== nothing
471-
for inst in users(entry, inner_ssa)
472-
call = resolve_call(entry, inst)
473-
call === nothing && continue
474-
func, _ = call
475-
func === pat.func && return SSAValue(inst)
483+
for u in users_of(driver, inner_ssa)
484+
def_entry = get(driver.defs, u, nothing)
485+
def_entry === nothing && continue
486+
def_entry.func === pat.func && return u
476487
end
477488
end
478489
end
@@ -491,20 +502,21 @@ function apply_rewrite!(driver::RewriteDriver, block, val::SSAValue, rule, match
491502
# Look up live instruction for RFunc interface
492503
haskey(block, val.id) || return false
493504
inst = block[val.id]
494-
rule.rhs.func(driver.sci, block, inst, match, driver) || return false
505+
rule.rhs.func(driver.rewriter.sci, block, inst, match, driver) || return false
495506
return true
496507
elseif rule.rhs isa RBind
497508
# Forwarding: replace all uses of root with the bound value, delete root.
498509
# Mark immediate users as modified — their operands are about to change.
499510
# When these are later popped from the worklist without a match, the
500511
# driver propagates to THEIR users (see modified check in main loop).
501512
# This gives MLIR-style notifyOperationModified cascading.
502-
for inst in users(driver.sci.entry, val)
503-
push!(driver.modified, SSAValue(inst))
513+
new_val = match.bindings[rule.rhs.name]
514+
for u in users_of(driver, val)
515+
push!(driver.modified, u)
516+
push!(driver.worklist, u)
504517
end
505-
add_users_to_worklist!(driver, val)
506-
replace_uses!(driver.sci.entry, val, match.bindings[rule.rhs.name])
507-
erase_op!(driver, entry)
518+
replace_uses!(driver.rewriter, val, new_val)
519+
erase!(driver.rewriter, entry.block, val)
508520
else
509521
# Substitution: replace root in-place, clean up dead intermediates.
510522
# Only delete intermediates with no remaining uses — transparent-op
@@ -514,7 +526,7 @@ function apply_rewrite!(driver::RewriteDriver, block, val::SSAValue, rule, match
514526
dead_entry = get(driver.defs, dead_val, nothing)
515527
dead_entry === nothing && continue
516528
use_count(driver, dead_val) == 0 || continue
517-
erase_op!(driver, dead_entry)
529+
erase!(driver.rewriter, dead_entry.block, dead_val)
518530
end
519531
typ = block[val.id][:type]
520532
# Build operands, flattening RSplat nodes into multiple operands
@@ -531,13 +543,8 @@ function apply_rewrite!(driver::RewriteDriver, block, val::SSAValue, rule, match
531543
# describe the OLD op; recompute from the new func's `efunc` effects
532544
# so downstream gates (CSE, LICM) see fresh, correct information.
533545
new_stmt = Expr(:call, rule.rhs.func, operands...)
534-
if rule.rhs.func === driver.defs[val].func
535-
block[val.id] = (stmt=new_stmt,)
536-
else
537-
block[val.id] = (stmt=new_stmt, flag=inferred_flags(rule.rhs.func))
538-
end
539-
# Update defs, re-add self and users to worklist (statement changed)
540-
driver.defs[val] = DefEntry(block, val, rule.rhs.func)
546+
flag = rule.rhs.func === driver.defs[val].func ? nothing : inferred_flags(rule.rhs.func)
547+
replace_stmt!(driver.rewriter, block, val, new_stmt; flag)
541548
push!(driver.worklist, val)
542549
add_users_to_worklist!(driver, val)
543550
end
@@ -584,7 +591,10 @@ function rewrite_patterns!(sci::StructuredIRCode, rules::Vector{RewriteRule};
584591
end
585592
end
586593

587-
driver = RewriteDriver(sci, defs, dispatch, wl, constants, Set{SSAValue}(), max_rewrites)
594+
rewriter = Rewriter(sci)
595+
driver = RewriteDriver(rewriter, defs, dispatch, wl, constants, Set{SSAValue}(),
596+
max_rewrites)
597+
rewriter.listener = driver
588598

589599
num_rewrites = 0
590600
while !isempty(driver.worklist) && num_rewrites < driver.max_rewrites
@@ -605,7 +615,7 @@ function rewrite_patterns!(sci::StructuredIRCode, rules::Vector{RewriteRule};
605615
if use_count(driver, val) == 0
606616
stmt = entry.block[val.id][:stmt]
607617
if !must_keep(entry.block, stmt)
608-
erase_op!(driver, entry)
618+
erase!(driver.rewriter, entry.block, val)
609619
continue
610620
end
611621
end
@@ -634,10 +644,9 @@ function rewrite_patterns!(sci::StructuredIRCode, rules::Vector{RewriteRule};
634644
# cascade continues through the fixpoint.
635645
if !matched && val in driver.modified
636646
delete!(driver.modified, val)
637-
for inst in users(driver.sci.entry, val)
638-
uv = SSAValue(inst)
639-
push!(driver.modified, uv)
640-
haskey(driver.defs, uv) && push!(driver.worklist, uv)
647+
for u in users_of(driver, val)
648+
push!(driver.modified, u)
649+
haskey(driver.defs, u) && push!(driver.worklist, u)
641650
end
642651
end
643652
end

0 commit comments

Comments
 (0)