Sarek_execute.Fusion_epilogueEpilogue fusion: an element-wise consumer inlined after the producer's stores (specs/epilogue-fusion.md).
Producer P stores elements of an intermediate array I; consumer C is element-wise (let i = global_thread_id in body) and reads I only at its own index i. In the fused kernel, every store I[e] <- v of P becomes
let t = e in
I[t] <- v ;
if t < tc then (let i = t in body)where tc is the number of threads C was launched with. The thread that stored I[t] then computes C's element t, reading I[t] back from memory just after writing it: the same operations in the same order, with the same values. What makes this equal to running P then C is checked elsewhere: P stores each element of C's domain exactly once (Index_slice, per launch), C reads nothing else P writes, and C writes nothing P or C reads elsewhere (Launch_window). The one remaining difference is that the device compiler now sees P's store and C's load in one function: a compiler allowed to contract a*b+c across them could fuse P's last multiply into C's first add. It can also share a subexpression of P and C that the unfused C contracts. That is why this fusion is on by default only where contraction is off in every kernel (-ffp-contract=off on HIP).
Names: P's are prefixed p_, C's c_, the new ones ep_, so no name can capture another. C's parameters bound to the same vector as one of P's are mapped onto P's parameter (in particular I itself).
type consumer = {index : Sarek_ir_types.var;C's i
body : Sarek_ir_types.stmt;the body under let i = global id in
reads_at_index : string list;array params that C reads only at index i (or not at all)
written : string list;array params C writes (always at i)
}val accesses_stmt :
(string * Sarek_ir_types.expr option * bool) list ->
Sarek_ir_types.stmt ->
(string * Sarek_ir_types.expr option * bool) listval accesses_expr :
(string * Sarek_ir_types.expr option * bool) list ->
Sarek_ir_types.expr ->
(string * Sarek_ir_types.expr option * bool) listval is_index : Sarek_ir_types.var -> Sarek_ir_types.expr option -> boolval consumer_shape : Sarek_ir_types.kernel -> consumerval consumer : Sarek_ir_types.kernel -> (consumer, string) Stdlib.resultval ep_var : int -> string -> Sarek_ir_types.elttype -> Sarek_ir_types.varval ids :
producer:Sarek_ir_types.kernel ->
consumer:Sarek_ir_types.kernel ->
idsval producer_names : Ir_rename.ctxval consumer_names :
shared:(string * string) list ->
c_shift:int ->
Ir_rename.ctxval insert :
array:string ->
tmp_id:int ->
after:(Sarek_ir_types.expr -> Sarek_ir_types.stmt) ->
Sarek_ir_types.stmt ->
Sarek_ir_types.stmtval fused_body :
producer:Sarek_ir_types.kernel ->
p_array:string ->
c:consumer ->
cn:Ir_rename.ctx ->
ids:ids ->
tc:Sarek_ir_types.var ->
Sarek_ir_types.stmtval merged_declarations :
Sarek_ir_types.kernel ->
Sarek_ir_types.kernel ->
(string * (string * Sarek_ir_types.elttype) list) list
* (string * (string * Sarek_ir_types.elttype list) list) list
* Sarek_ir_types.helper_func listval fuse :
name:string ->
producer:Sarek_ir_types.kernel ->
p_array:string ->
consumer:Sarek_ir_types.kernel ->
shared:(string * string) list ->
Sarek_ir_types.kernelfuse ~name ~producer ~p_array ~consumer ~shared inlines consumer after producer's stores to p_array. shared maps every consumer array parameter bound to the same vector as a producer parameter (in particular the one bound to p_array's vector) to that producer parameter. Parameters: P's, then C's that are not shared, then ep_tc.