• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

daisytuner / docc / 30614148740

31 Jul 2026 07:49AM UTC coverage: 64.691% (+0.2%) from 64.531%
30614148740

push

github

web-flow
Adding ProducerIntoConsumer MapFusion into the new, recursive MapFusionPass (#873)

Refactored the code of MapFusion Transform into MapFusionByAccessWorker.

The old transform still exists for its use as standalone transformation for now and also to run the old pass to cover everything not yet implemented in the new ByAccessWorker.

Refactored MapFusionByDomain into LoopFusionPass, the sequential & recursive loop-fusion logic into a base that handles the existing by-domain fusion and the by-access fusion, separately disablable.

For this, the existing per-loop caches of LoopFusionPass have been extended to contain the data needed by by-access fusion (assumptions, non-map loops and more details about subsets and locations of accesses).

LoopFusionPass still maintains these caches as fusions happen, so it never needs to clear any analysis.

For now, the MapFusionByAccessWorker has not been verified and skips any ConsumerIntoProducer fusions, as that is a greater change to how the cached data needs to be updated and its also a smaller portion of compile-time.

The MapFusionByAccessWorker was refactored to use the FusionLoopCandidate structure to access any cached data. It can be used as a basis for a new Transformation, that simply creates these per-loop structures on the fly, so that Transform and efficient pass can share code again.
The ByAccessWorker was also optimized to support per "fusion container" selection of replacing memory accesses with local transients and to consider the domain-equality checks already done by the NewMapFusionPass.

If the domains match, we can leave in the indirect writes in, which makes the fused code is a valid substitute for the original loop, avoiding the need for any loop-duplication.

Elliding same-subset indirect write-read patterns in the same block or successive blocks is a far easier problem to solve, then leaving the original loop lying around and hoping that it can be proven completely dead and removed by later ... (continued)

1388 of 1894 new or added lines in 13 files covered. (73.28%)

45088 of 69697 relevant lines covered (64.69%)

725.27 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

63.27
/opt/src/passes/loop_fusion/loop_fusion_by_access.cpp
1
#include "sdfg/passes/loop_fusion/loop_fusion_by_accesses.h"
2

3
#include <isl/ctx.h>
4
#include <isl/map.h>
5
#include <isl/options.h>
6
#include <isl/set.h>
7
#include <isl/space.h>
8

9
#include "sdfg/passes/loop_fusion/loop_fusion_pass.h"
10
#include "sdfg/symbolic/delinearization.h"
11
#include "sdfg/symbolic/utils.h"
12
#include "sdfg/transformations/map_fusion.h"
13
#include "symengine/solve.h"
14

15
namespace sdfg::passes::loop_fusion {
16

17
structured_control_flow::StructuredLoop* LoopFusionByAccessWorker::Plan::consumer_target_loop() const {
22✔
18
    if (this->init_hoist_) {
22✔
19
        return this->consumer_loops_.at(this->consumer_loops_.size() - 2);
1✔
20
    } else {
21✔
21
        return this->consumer_loops_.back();
21✔
22
    }
21✔
23
}
22✔
24

25
structured_control_flow::Sequence& LoopFusionByAccessWorker::Plan::consumer_target_sequence() const {
26✔
26
    return init_hoist_ ? *hoist_body_ : *consumer_body_;
26✔
27
}
26✔
28

29
std::vector<std::pair<symbolic::Symbol, symbolic::Expression>> LoopFusionByAccessWorker::solve_subsets(
30
    const data_flow::Subset& producer_subset,
31
    const data_flow::Subset& consumer_subset,
32
    const std::vector<structured_control_flow::StructuredLoop*>& producer_loops,
33
    const std::vector<structured_control_flow::StructuredLoop*>& consumer_loops,
34
    const symbolic::Assumptions& producer_assumptions,
35
    const symbolic::Assumptions& consumer_assumptions,
36
    bool invert_range_check
37
) {
69✔
38
    // Delinearize subsets to recover multi-dimensional structure from linearized accesses
39
    // e.g. T[i*N + j] with assumptions on bounds -> T[i, j]
40
    auto producer_sub = producer_subset;
69✔
41
    if (producer_sub.size() == 1) {
69✔
42
        auto producer_result = symbolic::delinearize(producer_sub.at(0), producer_assumptions);
55✔
43
        if (producer_result.success) {
55✔
44
            producer_sub = producer_result.indices;
50✔
45
        }
50✔
46
    }
55✔
47
    auto consumer_sub = consumer_subset;
69✔
48
    if (consumer_sub.size() == 1) {
69✔
49
        auto consumer_result = symbolic::delinearize(consumer_sub.at(0), consumer_assumptions);
55✔
50
        if (consumer_result.success) {
55✔
51
            consumer_sub = consumer_result.indices;
50✔
52
        }
50✔
53
    }
55✔
54

55
    // Subset dimensions must match
56
    if (producer_sub.size() != consumer_sub.size()) {
69✔
NEW
57
        return {};
×
NEW
58
    }
×
59
    if (producer_sub.empty()) {
69✔
NEW
60
        return {};
×
NEW
61
    }
×
62

63
    // Extract producer indvars
64
    SymEngine::vec_sym producer_vars;
69✔
65
    for (auto* loop : producer_loops) {
91✔
66
        producer_vars.push_back(SymEngine::rcp_static_cast<const SymEngine::Symbol>(loop->indvar()));
91✔
67
    }
91✔
68

69
    // Step 1: Solve the linear equation system using SymEngine
70
    // System: producer_sub[d] - consumer_sub[d] = 0, for each dimension d
71
    // Solve for producer_vars in terms of consumer_vars and parameters
72
    SymEngine::vec_basic equations;
69✔
73
    for (size_t d = 0; d < producer_sub.size(); ++d) {
162✔
74
        auto equation = symbolic::sub(producer_sub.at(d), consumer_sub.at(d));
93✔
75
        if (!symbolic::eq(equation, symbolic::zero())) {
93✔
76
            equations.push_back(equation);
91✔
77
        }
91✔
78
    }
93✔
79

80
    // Need exactly as many equations as unknowns for a unique solution.
81
    // Underdetermined systems (e.g. linearized access with multiple loop vars)
82
    // cannot be uniquely solved and would crash linsolve.
83
    if (equations.size() != producer_vars.size()) {
69✔
NEW
84
        return {};
×
NEW
85
    }
×
86

87
    SymEngine::vec_basic solution;
69✔
88
    try {
69✔
89
        solution = SymEngine::linsolve(equations, producer_vars);
69✔
90
    } catch (...) {
69✔
NEW
91
        return {};
×
NEW
92
    }
×
93
    if (solution.size() != producer_vars.size()) {
69✔
NEW
94
        return {};
×
NEW
95
    }
×
96
    // Build consumer var set for atom validation
97
    symbolic::SymbolSet consumer_var_set;
69✔
98
    for (auto* loop : consumer_loops) {
95✔
99
        consumer_var_set.insert(loop->indvar());
95✔
100
    }
95✔
101

102
    std::vector<std::pair<symbolic::Symbol, symbolic::Expression>> mappings;
69✔
103
    for (size_t i = 0; i < producer_vars.size(); ++i) {
160✔
104
        auto& sol = solution[i];
91✔
105

106
        // Check for invalid solutions
107
        if (SymEngine::is_a<SymEngine::NaN>(*sol) || SymEngine::is_a<SymEngine::Infty>(*sol)) {
91✔
NEW
108
            return {};
×
NEW
109
        }
×
110

111
        // Validate that solution atoms are consumer vars or parameters
112
        for (const auto& atom : symbolic::atoms(sol)) {
93✔
113
            if (consumer_var_set.count(atom)) {
93✔
114
                continue;
93✔
115
            }
93✔
NEW
116
            bool is_param = false;
×
NEW
117
            auto it = consumer_assumptions.find(atom);
×
NEW
118
            if (it != consumer_assumptions.end() && it->second.constant()) {
×
NEW
119
                is_param = true;
×
NEW
120
            }
×
NEW
121
            if (!is_param) {
×
NEW
122
                it = producer_assumptions.find(atom);
×
NEW
123
                if (it != producer_assumptions.end() && it->second.constant()) {
×
NEW
124
                    is_param = true;
×
NEW
125
                }
×
NEW
126
            }
×
NEW
127
            if (!is_param) {
×
NEW
128
                return {};
×
NEW
129
            }
×
NEW
130
        }
×
131

132
        mappings.push_back({symbolic::symbol(producer_vars[i]->get_name()), symbolic::expand(sol)});
91✔
133
    }
91✔
134
    // Step 2: ISL integrality validation via map composition
135
    // Build an unconstrained producer access map (no domain bounds on producer vars).
136
    // In map fusion, the producer's computation is inlined into the consumer, so
137
    // the producer's original iteration domain is irrelevant. We only need to verify
138
    // that the equation system has an INTEGER solution for every consumer point.
139
    symbolic::Assumptions unconstrained_producer;
69✔
140
    for (auto* loop : producer_loops) {
91✔
141
        symbolic::Assumption a(loop->indvar());
91✔
142
        a.constant(false);
91✔
143
        unconstrained_producer[loop->indvar()] = a;
91✔
144
    }
91✔
145
    for (const auto& [sym, assump] : producer_assumptions) {
303✔
146
        if (assump.constant() && unconstrained_producer.find(sym) == unconstrained_producer.end()) {
303✔
147
            unconstrained_producer[sym] = assump;
95✔
148
        }
95✔
149
    }
303✔
150

151
    std::string producer_map_str = symbolic::expression_to_map_str(producer_sub, unconstrained_producer);
69✔
152
    // Build consumer access map with full domain constraints
153
    std::string consumer_map_str = symbolic::expression_to_map_str(consumer_sub, consumer_assumptions);
69✔
154

155
    isl_ctx* ctx = isl_ctx_alloc();
69✔
156
    isl_options_set_on_error(ctx, ISL_ON_ERROR_CONTINUE);
69✔
157

158
    isl_map* producer_map = isl_map_read_from_str(ctx, producer_map_str.c_str());
69✔
159
    isl_map* consumer_map = isl_map_read_from_str(ctx, consumer_map_str.c_str());
69✔
160

161
    if (!producer_map || !consumer_map) {
69✔
NEW
162
        if (producer_map) isl_map_free(producer_map);
×
NEW
163
        if (consumer_map) isl_map_free(consumer_map);
×
NEW
164
        isl_ctx_free(ctx);
×
NEW
165
        return {};
×
NEW
166
    }
×
167

168
    // Align parameters between the two maps
169
    isl_space* params_p = isl_space_params(isl_map_get_space(producer_map));
69✔
170
    isl_space* params_c = isl_space_params(isl_map_get_space(consumer_map));
69✔
171
    isl_space* unified = isl_space_align_params(isl_space_copy(params_p), isl_space_copy(params_c));
69✔
172
    isl_space_free(params_p);
69✔
173
    isl_space_free(params_c);
69✔
174

175
    producer_map = isl_map_align_params(producer_map, isl_space_copy(unified));
69✔
176
    consumer_map = isl_map_align_params(consumer_map, isl_space_copy(unified));
69✔
177

178
    // Save consumer domain before consuming consumer_map in composition
179
    isl_set* consumer_domain = isl_map_domain(isl_map_copy(consumer_map));
69✔
180

181
    // Compute composition: consumer_access ∘ inverse(producer_access)
182
    // This checks whether the equation system producer_subset = consumer_subset
183
    // has an integer solution for each consumer domain point.
184
    isl_map* producer_inverse = isl_map_reverse(producer_map);
69✔
185
    isl_map* composition = isl_map_apply_range(consumer_map, producer_inverse);
69✔
186

187
    // Check single-valuedness: each consumer point maps to at most one producer point
188
    bool single_valued = isl_map_is_single_valued(composition) == isl_bool_true;
69✔
189

190
    // Check domain coverage: every consumer point has a valid integer mapping
191
    isl_set* comp_domain = isl_map_domain(composition);
69✔
192

193
    bool domain_covered = isl_set_is_subset(consumer_domain, comp_domain) == isl_bool_true;
69✔
194

195
    isl_set_free(comp_domain);
69✔
196
    isl_set_free(consumer_domain);
69✔
197

198
    // Step 3: Verify producer write range covers consumer read range.
199
    // The producer only writes a subset of the array if its loops have restricted bounds.
200
    // Fusion is invalid if the consumer reads elements the producer never writes.
201
    bool range_covered = false;
69✔
202
    if (single_valued && domain_covered) {
69✔
203
        std::string constrained_producer_map_str = symbolic::expression_to_map_str(producer_sub, producer_assumptions);
65✔
204
        isl_map* constrained_producer = isl_map_read_from_str(ctx, constrained_producer_map_str.c_str());
65✔
205
        isl_map* consumer_map_copy = isl_map_read_from_str(ctx, consumer_map_str.c_str());
65✔
206

207
        if (constrained_producer && consumer_map_copy) {
65✔
208
            constrained_producer = isl_map_align_params(constrained_producer, isl_space_copy(unified));
65✔
209
            consumer_map_copy = isl_map_align_params(consumer_map_copy, isl_space_copy(unified));
65✔
210

211
            isl_set* producer_range = isl_map_range(constrained_producer);
65✔
212
            isl_set* consumer_range = isl_map_range(consumer_map_copy);
65✔
213

214
            // When arguments are swapped (ConsumerIntoProducer), the "producer"/"consumer"
215
            // labels are inverted. Flip the subset check to always verify:
216
            // actual_consumer_read_range ⊆ actual_producer_write_range
217
            if (invert_range_check) {
65✔
218
                range_covered = isl_set_is_subset(producer_range, consumer_range) == isl_bool_true;
4✔
219
            } else {
61✔
220
                range_covered = isl_set_is_subset(consumer_range, producer_range) == isl_bool_true;
61✔
221
            }
61✔
222

223
            isl_set_free(producer_range);
65✔
224
            isl_set_free(consumer_range);
65✔
225
        } else {
65✔
NEW
226
            if (constrained_producer) isl_map_free(constrained_producer);
×
NEW
227
            if (consumer_map_copy) isl_map_free(consumer_map_copy);
×
NEW
228
        }
×
229
    }
65✔
230

231
    isl_space_free(unified);
69✔
232
    isl_ctx_free(ctx);
69✔
233

234
    if (!single_valued || !domain_covered || !range_covered) {
69✔
235
        return {};
9✔
236
    }
9✔
237

238
    return mappings;
60✔
239
}
69✔
240

241
LoopFusionByAccessWorker::FusionRegs LoopFusionByAccessWorker::
242
    find_fusion_regs(const FusionLoopCandidate& first, const FusionLoopCandidate& second) {
34✔
243
    auto& first_args = first.args;
34✔
244
    auto second_args = second.args;
34✔
245

246
    std::unordered_set<std::string> first_inputs;
34✔
247
    std::unordered_set<std::string> first_outputs;
34✔
248
    for (const auto& [name, arg] : first_args) {
117✔
249
        if (arg.arg.is_output) {
117✔
250
            first_outputs.insert(name);
34✔
251
        }
34✔
252
        if (arg.arg.is_input) {
117✔
253
            first_inputs.insert(name);
117✔
254
        }
117✔
255
    }
117✔
256

257
    std::unordered_set<std::string> second_outputs;
34✔
258
    for (const auto& [name, arg] : second_args) {
122✔
259
        if (arg.arg.is_output) {
122✔
260
            second_outputs.insert(name);
34✔
261
        }
34✔
262
    }
122✔
263

264
    // First pass: identify fusion containers (producer writes, consumer reads)
265
    std::unordered_set<std::string> fusion_containers;
34✔
266
    for (const auto& [name, arg] : second_args) {
122✔
267
        if (first_outputs.contains(name) && arg.arg.is_input) {
122✔
268
            fusion_containers.insert(name);
31✔
269
        }
31✔
270
    }
122✔
271

272
    // Second pass: check for conflicts on non-fusion containers
273
    for (const auto& [name, arg] : second_args) {
122✔
274
        bool is_fusion = fusion_containers.contains(name);
122✔
275
        if (first_outputs.contains(name) && arg.arg.is_output && !is_fusion) {
122✔
NEW
276
            return {.conflicts = true};
×
NEW
277
        }
×
278
        if (first_inputs.contains(name) && arg.arg.is_output && !is_fusion) {
122✔
279
            return {.conflicts = true};
2✔
280
        }
2✔
281
    }
122✔
282

283
    return {.fusion_regs = fusion_containers, .second_outputs = second_outputs, .conflicts = false};
32✔
284
}
34✔
285

286
std::vector<StructuredLoop*> LoopFusionByAccessWorker::collect_structured_sub_tree(StructuredLoop& top) {
67✔
287
    auto& ana = get_loop_analysis();
67✔
288
    std::vector<StructuredLoop*> loop_stack;
67✔
289
    auto it = ana.get_loop_iterator(&top);
67✔
290
    auto end = ana.get_subtree_end(&top);
67✔
291
    auto size = std::distance(it, end);
67✔
292
    loop_stack.reserve(size);
67✔
293
    while (it != end) {
158✔
294
        auto* loop = *it;
91✔
295
        if (auto structured = dyn_cast<StructuredLoop*>(loop)) {
91✔
296
            loop_stack.push_back(structured);
91✔
297
        } else {
91✔
NEW
298
            return {};
×
NEW
299
        }
×
300
        ++it;
91✔
301
    }
91✔
302
    return loop_stack;
67✔
303
}
67✔
304

305
std::vector<StructuredLoop*> LoopFusionByAccessWorker::collect_loop_parents(Sequence* sequence, StructuredLoop* loop) {
1✔
306
    auto node = sequence->get_parent();
1✔
307
    std::vector<StructuredLoop*> loop_stack;
1✔
308
    while (node != loop) {
3✔
309
        if (node == nullptr) {
2✔
NEW
310
            throw std::runtime_error(
×
NEW
311
                "The loop #" + std::to_string(loop->element_id()) + " must be parent of #" +
×
NEW
312
                std::to_string(sequence->element_id())
×
NEW
313
            );
×
NEW
314
        }
×
315
        if (auto structured = dyn_cast<StructuredLoop*>(node)) {
2✔
316
            loop_stack.push_back(structured);
1✔
317
        }
1✔
318
        node = node->get_parent();
2✔
319
    }
2✔
320
    loop_stack.push_back(loop);
1✔
321

322
    std::ranges::reverse(loop_stack);
1✔
323

324
    return loop_stack;
1✔
325
}
1✔
326

327
std::unique_ptr<LoopFusionByAccessWorker::Plan> LoopFusionByAccessWorker::
328
    try_create_fusion_by_access_plan(FusionLoopCandidate& first, FusionLoopCandidate& second, bool domains_match) {
37✔
329
    auto first_map = dyn_cast<Map*>(first.loop);
37✔
330
    if (!first_map) {
37✔
331
        return {};
1✔
332
    }
1✔
333
    auto state_ptr = std::make_unique<Plan>(*first_map, *second.loop);
36✔
334
    Plan& state = *state_ptr;
36✔
335

336
    auto& ana = get_loop_analysis();
36✔
337
    auto first_loop_info = ana.loop_info(first.loop);
36✔
338
    auto second_loop_info = ana.loop_info(second.loop);
36✔
339

340
    bool first_nested = first_loop_info.is_perfectly_nested;
36✔
341
    bool second_nested = second_loop_info.is_perfectly_nested;
36✔
342

343
    // Both non-perfectly-nested: not supported
344
    if (!first_nested && !second_nested) {
36✔
NEW
345
        return {};
×
NEW
346
    }
×
347

348
    if (!first_nested && second_nested) {
36✔
349
        // Pattern 2: Producer non-perfectly-nested, consumer perfectly nested
350
        state.direction_ = FusionDirection::ConsumerIntoProducer;
2✔
351
        DEBUG_PRINTLN(
2✔
352
            "Aborting ConsumerIntoProducer still unsupported: #" + std::to_string(first.loop->element_id()) + " <- #" +
2✔
353
            std::to_string(second.loop->element_id())
2✔
354
        );
2✔
355
        return {}; // unsupported for now. to different
2✔
356
    } else {
34✔
357
        // Pattern 1: Both perfectly nested — producer into consumer (original path)
358
        // Reverse Pattern 2: Producer perfectly nested, consumer non-perfectly-nested
359
        state.direction_ = FusionDirection::ProducerIntoConsumer;
34✔
360
    }
34✔
361

362
    // The side being inlined must be all-parallel (all Maps) so iterations can be reordered.
363
    // ProducerIntoConsumer: the producer is replicated at each consumer site and must be
364
    // reorderable, so it must be all-parallel. The consumer is normally required to be
365
    // all-parallel too, because a sequential (For) loop would re-execute the inlined producer
366
    // on every iteration (e.g. init T=0 fused into For(k){T+=A[k]} re-initializes each k).
367
    //
368
    // Reduction branch: we relax the consumer requirement when the consumer is a perfect nest
369
    // (parallel outer band + inner sequential For, i.e. a reduction). A fully-parallel producer
370
    // that is *streamed element-by-element* inside the reduction loop can still be inlined
371
    // soundly (e.g. scale -> max: max(M, A[i,j,k]/d)). The element-streaming safety conditions
372
    // are verified once the fusion candidates are known (see consumer_reduction_branch below):
373
    //   (1) the fused container must not be written by the consumer (no loop-carried
374
    //       accumulator), and
375
    //   (2) its consumer read subset must depend on an inner sequential loop indvar, so the
376
    //       inlined producer runs once per element rather than per init position.
377
    // These keep init-into-reduction (T=0 followed by For(k){T+=...}) rejected.
378
    // ConsumerIntoProducer: only the consumer (inlined side) must be all-parallel.
379
    bool consumer_reduction_branch = false;
34✔
380
    if (state.direction_ == FusionDirection::ProducerIntoConsumer) {
34✔
381
        if (!first_loop_info.is_perfectly_parallel) {
34✔
NEW
382
            return {};
×
383
        } else if (!second_loop_info.is_perfectly_parallel) {
34✔
384
            if (!second_loop_info.is_perfectly_nested) {
3✔
NEW
385
                return {};
×
NEW
386
            }
×
387
            consumer_reduction_branch = true;
3✔
388
        }
3✔
389
    } else {
34✔
NEW
390
        if (!second_loop_info.is_perfectly_parallel) {
×
NEW
391
            return {};
×
NEW
392
        }
×
NEW
393
    }
×
394

395
    // Locate producer write point
396

397
    if (first_nested) {
34✔
398
        // perfectly nested
399
        state.producer_loops_ = collect_structured_sub_tree(state.first);
34✔
400
        if (state.producer_loops_.empty()) {
34✔
401
            // sth. is invalid here, it should have been perfectly nested and at least contain the loop itself
NEW
402
            return {};
×
NEW
403
        }
×
404
        state.producer_body_ = &state.producer_loops_.back()->root();
34✔
405
        if (state.producer_body_->size() == 0) {
34✔
NEW
406
            return {};
×
NEW
407
        }
×
408

409
        if (state.producer_body_->size() == 1) {
34✔
410
            structured_control_flow::ControlFlowNode* node = &state.producer_body_->at(0);
33✔
411
            state.producer_block_ = dyn_cast<structured_control_flow::Block*>(node);
33✔
412
            if (state.producer_block_ == nullptr) {
33✔
NEW
413
                return {};
×
NEW
414
            }
×
415
        }
33✔
416
        // If the body has multiple children, then rely on write-based identification
417
    } else {
34✔
418
        // Non-perfectly-nested: search recursively for the write block
419
        // We need to know which containers to look for, but we don't know them yet.
420
        // Defer write location search until after fusion_containers are identified.
NEW
421
    }
×
422

423
    // Locate consumer read point
424
    if (second_nested) {
34✔
425
        // Perfectly nested: subtree should just be a stack of loops
426
        // Reduction patterns (e.g. Map{Map{For{T[i,j]+=...}}}) are rejected by
427
        // the is_perfectly_parallel check — For loops make it non-parallel.
428
        state.consumer_loops_ = collect_structured_sub_tree(state.second);
33✔
429
        state.consumer_body_ = &state.consumer_loops_.back()->root();
33✔
430
    } else {
33✔
431
        // Non-perfectly-nested: defer read location search until after fusion_containers are identified.
432
    }
1✔
433

434
    // Get arguments analysis to identify inputs/outputs of each loop
435
    auto [fusion_regs, second_outputs, reg_conflicts] = find_fusion_regs(first, second);
34✔
436
    if (fusion_regs.empty() || reg_conflicts) {
34✔
437
        return {};
5✔
438
    }
5✔
439

440
    // Now that we know the fusion containers, resolve deferred locations
441
    if (state.producer_block_ == nullptr) {
29✔
442
        // Non-perfectly-nested producer (or perfectly-nested with multi-block body):
443
        // find write location for the first fusion container.
444
        // All fusion containers must be written at the same block for this to work.
NEW
445
        structured_control_flow::Block* common_block = nullptr;
×
NEW
446
        for (const auto& container : fusion_regs) {
×
NEW
447
            auto& fusion_arg = first.args.at(container);
×
NEW
448
            auto& nested_common = fusion_arg.nested_access;
×
449

NEW
450
            if (nested_common.wr_block.block_conflict) {
×
NEW
451
                return {};
×
NEW
452
            }
×
NEW
453
            if (!nested_common.wr_block.common_block) {
×
NEW
454
                return {};
×
NEW
455
            }
×
NEW
456
            if (!common_block) {
×
NEW
457
                common_block = nested_common.wr_block.common_block;
×
NEW
458
            } else if (common_block != nested_common.wr_block.common_block) {
×
NEW
459
                return {};
×
NEW
460
            }
×
NEW
461
        }
×
NEW
462
        if (common_block) {
×
NEW
463
            state.producer_block_ = common_block;
×
NEW
464
            state.producer_body_ = static_cast<Sequence*>(common_block->get_parent());
×
NEW
465
            state.producer_loops_ = collect_loop_parents(state.producer_body_, first.loop);
×
NEW
466
        } else {
×
NEW
467
            return {};
×
NEW
468
        }
×
NEW
469
    }
×
470

471
    if (!second_nested) {
29✔
472
        // Non-perfectly-nested consumer: find read location for the first fusion container
473
        // All fusion containers must be read at the same sequence for this to work
474
        structured_control_flow::Sequence* common_seq = nullptr;
1✔
475
        for (const auto& container : fusion_regs) {
1✔
476
            auto& fusion_arg = second.args.at(container);
1✔
477
            auto& nested_common = fusion_arg.nested_access;
1✔
478

479
            if (nested_common.rd_block.block_conflict) {
1✔
NEW
480
                return {};
×
NEW
481
            }
×
482
            if (!nested_common.rd_block.common_block) {
1✔
NEW
483
                return {};
×
NEW
484
            }
×
485
            if (!common_seq) {
1✔
486
                common_seq = static_cast<Sequence*>(nested_common.rd_block.common_block->get_parent());
1✔
487
            } else if (common_seq != nested_common.rd_block.common_block->get_parent()) {
1✔
NEW
488
                return {};
×
NEW
489
            }
×
490
        }
1✔
491
        if (common_seq) {
1✔
492
            state.consumer_body_ = common_seq;
1✔
493
            state.consumer_loops_ = collect_loop_parents(common_seq, second.loop);
1✔
494
        } else {
1✔
NEW
495
            return {};
×
NEW
496
        }
×
497
    }
1✔
498

499
    state.producer_fusion_candidate_ = get_fuse_candidate(*state.producer_loops_.back());
29✔
500
    state.consumer_fusion_candidate_ = get_fuse_candidate(*state.consumer_loops_.back());
29✔
501

502
    if (!state.consumer_fusion_candidate_ || !state.producer_fusion_candidate_) {
29✔
NEW
503
        DEBUG_PRINTLN(
×
NEW
504
            "Aborting fusion: Missing fusion candidate state for #" +
×
NEW
505
            std::to_string(state.producer_loops_.back()->element_id()) + " - #" +
×
NEW
506
            std::to_string(state.consumer_loops_.back()->element_id())
×
NEW
507
        );
×
NEW
508
        return {};
×
NEW
509
    }
×
510

511
    // Check if producer actually reads a fusion container in the dataflow.
512
    // If so, ProducerIntoConsumer is unsafe (original producer loop mutates the array
513
    // before the inlined copy reads it). Force ConsumerIntoProducer.
514
    if (state.direction_ == FusionDirection::ProducerIntoConsumer) {
29✔
515
        bool producer_reads_fusion = false;
29✔
516
        for (auto& container : fusion_regs) {
29✔
517
            auto& arg = state.producer_fusion_candidate_->args.at(container);
29✔
518
            if (arg.arg.is_explicit_input) {
29✔
519
                producer_reads_fusion = true;
2✔
520
                break;
2✔
521
            }
2✔
522
        }
29✔
523
        if (producer_reads_fusion) {
29✔
524
            state.direction_ = FusionDirection::ConsumerIntoProducer;
2✔
525
            // Re-check: consumer must be all-parallel for ConsumerIntoProducer
526
            if (!second_loop_info.is_perfectly_parallel) {
2✔
NEW
527
                return {};
×
NEW
528
            }
×
529
        }
2✔
530
    }
29✔
531

532
    // ProducerIntoConsumer only deep-copies producer_block_ into the consumer body.
533
    // If the producer body has multiple blocks (e.g. from prior BlockFusion merging
534
    // a previous fusion's writeback + inlined blocks), the write block may depend on
535
    // intermediates produced by earlier blocks that would NOT be copied. Reject.
536
    if (state.direction_ == FusionDirection::ProducerIntoConsumer && state.producer_body_->size() > 1) {
29✔
NEW
537
        return {};
×
NEW
538
    }
×
539

540
    if (state.direction_ == FusionDirection::ConsumerIntoProducer) {
29✔
541
        DEBUG_PRINTLN(
2✔
542
            "Aborting fusion, ConsumerIntoProducer still unsupported: #" +
2✔
543
            std::to_string(state.producer_body_->element_id()) + "<- #" +
2✔
544
            std::to_string(state.consumer_body_->element_id())
2✔
545
        );
2✔
546
        return {};
2✔
547
    }
2✔
548

549
    std::unordered_map<std::string, const data_flow::Subset*> producer_subsets;
27✔
550

551

552
    // For each fusion container, find the producer memlet and collect unique consumer subsets
553
    for (const auto& container : fusion_regs) {
27✔
554
        auto& arg = state.producer_fusion_candidate_->args.at(container);
27✔
555

556
        assert(arg.saw_access_locally()); // otherwise it should have never made it into this list to begin with
27✔
557

558
        auto common_subset = &arg.local_access.common_subset;
27✔
559
        if (arg.local_access.subsets_conflict || !common_subset) {
27✔
NEW
560
            DEBUG_PRINTLN(
×
NEW
561
                "Aborting fusion, conflicting write subsets on " + container + " in #" +
×
NEW
562
                std::to_string(state.producer_fusion_candidate_->loop->element_id())
×
NEW
563
            );
×
NEW
564
            return {};
×
NEW
565
        }
×
566
        // TODO old code aborted on finding access nodes of fusion regs that are read & write.
567
        //  cannot happen, because for now we do not allow any reads
568

569
        producer_subsets.emplace(container, &arg.local_access.common_subset.value());
27✔
570
    }
27✔
571

572
    transformations::FusionConsumerSubsetVisitor consumer_visitor(producer_subsets);
27✔
573
    bool abort = consumer_visitor.dispatch(*state.consumer_body_);
27✔
574
    if (abort) {
27✔
575
        return {};
1✔
576
    }
1✔
577

578
    // Get assumptions for the resolved write/read locations
579
    // Include trivial bounds from types to help delinearization with symbolic strides
580
    // TODO for the producer into consumer cases, it has to be innermost loops. Would not be for ConsumerIntoProducer.
581
    auto& producer_assumptions = state.producer_fusion_candidate_->assumptions;
26✔
582
    auto& consumer_assumptions = state.consumer_fusion_candidate_->assumptions;
26✔
583

584
    for (auto [container, unique_subsets] : consumer_visitor.unique_subsets_per_container()) {
26✔
585
        auto& producer_subset = *producer_subsets.at(container);
26✔
586
        // For each unique consumer subset, solve index mappings and create a FusionCandidate
587
        // The direction determines which side's indvars are solved for
588
        for (const auto& consumer_subset : unique_subsets) {
30✔
589
            std::vector<std::pair<symbolic::Symbol, symbolic::Expression>> mappings;
30✔
590

591
            if (state.direction_ == FusionDirection::ProducerIntoConsumer) {
30✔
592
                // Solve producer indvars in terms of consumer indvars
593
                mappings = solve_subsets(
30✔
594
                    producer_subset,
30✔
595
                    consumer_subset,
30✔
596
                    state.producer_loops_,
30✔
597
                    state.consumer_loops_,
30✔
598
                    producer_assumptions,
30✔
599
                    consumer_assumptions
30✔
600
                );
30✔
601
            } else {
30✔
602
                // ConsumerIntoProducer: solve consumer indvars in terms of producer indvars
603
                // Arguments are swapped, so invert the range check direction
NEW
604
                mappings = solve_subsets(
×
NEW
605
                    consumer_subset,
×
NEW
606
                    producer_subset,
×
NEW
607
                    state.consumer_loops_,
×
NEW
608
                    state.producer_loops_,
×
NEW
609
                    consumer_assumptions,
×
NEW
610
                    producer_assumptions,
×
NEW
611
                    true
×
NEW
612
                );
×
NEW
613
            }
×
614

615
            if (mappings.empty()) {
30✔
616
                return {};
4✔
617
            }
4✔
618

619
            FusionRegCandidate candidate;
26✔
620
            candidate.container = container;
26✔
621
            candidate.consumer_subset = consumer_subset;
26✔
622
            candidate.index_mappings = std::move(mappings);
26✔
623

624
            state.fusion_candidates_.push_back(candidate);
26✔
625
        }
26✔
626
    }
26✔
627

628
    // Reduction-branch safety: when fusing a parallel producer into a non-parallel
629
    // (reduction) consumer, classify each fusion container into one of two sound patterns:
630
    //   Case 1 (stream):     the container is NOT a consumer output and its consumer read
631
    //                        depends on an inner sequential indvar -> it is produced and
632
    //                        consumed element-by-element, so the producer is scalarized and
633
    //                        inlined inside the reduction loop (e.g. softmax scale -> max).
634
    //   Case 2 (init-hoist): the container IS a consumer output (the reduction accumulator)
635
    //                        and its consumer read is loop-invariant w.r.t. every sequential
636
    //                        indvar -> the producer is the accumulator's initial value and is
637
    //                        hoisted to the reduction's outer parallel band, before the inner
638
    //                        sequential loop (e.g. T = -INF preceding T = max(T, x)).
639
    // Anything else (e.g. an accumulator whose read depends on the sequential indvar, or a
640
    // streamed value that the consumer also writes) is unsafe and rejected. The two patterns
641
    // require different placement in apply(), so all candidates must share one pattern.
642
    if (consumer_reduction_branch) {
22✔
643
        symbolic::SymbolSet sequential_indvars;
3✔
644
        size_t first_sequential = state.consumer_loops_.size();
3✔
645
        for (size_t li = 0; li < state.consumer_loops_.size(); ++li) {
10✔
646
            if (dyn_cast<structured_control_flow::Map*>(state.consumer_loops_[li]) == nullptr) {
7✔
647
                sequential_indvars.insert(state.consumer_loops_[li]->indvar());
3✔
648
                if (first_sequential == state.consumer_loops_.size()) {
3✔
649
                    first_sequential = li;
3✔
650
                }
3✔
651
            }
3✔
652
        }
7✔
653
        if (sequential_indvars.empty()) {
3✔
NEW
654
            return {};
×
NEW
655
        }
×
656
        bool any_stream = false;
3✔
657
        bool any_init = false;
3✔
658
        for (const auto& candidate : state.fusion_candidates_) {
3✔
659
            bool depends_on_sequential = false;
3✔
660
            for (const auto& dim : candidate.consumer_subset) {
3✔
661
                for (const auto& atom : symbolic::atoms(dim)) {
13✔
662
                    if (sequential_indvars.count(atom)) {
13✔
663
                        depends_on_sequential = true;
2✔
664
                        break;
2✔
665
                    }
2✔
666
                }
13✔
667
                if (depends_on_sequential) {
3✔
668
                    break;
2✔
669
                }
2✔
670
            }
3✔
671

672
            if (second_outputs.contains(candidate.container)) {
3✔
673
                // Case 2 candidate: must be a loop-invariant accumulator init.
674
                if (!allow_init_hoist_) {
1✔
675
                    // Init-hoisting disabled for this run (reserved for the final
676
                    // map-fusion pass so it does not fight loop distribution).
NEW
677
                    return {};
×
NEW
678
                }
×
679
                if (depends_on_sequential) {
1✔
NEW
680
                    return {};
×
NEW
681
                }
×
682
                any_init = true;
1✔
683
            } else {
2✔
684
                // Case 1 candidate: must be a streamed element.
685
                if (!depends_on_sequential) {
2✔
NEW
686
                    return {};
×
NEW
687
                }
×
688
                any_stream = true;
2✔
689
            }
2✔
690
        }
3✔
691
        // Do not mix patterns in a single fusion.
692
        if (any_init && any_stream) {
3✔
NEW
693
            return {};
×
NEW
694
        }
×
695
        if (any_init) {
3✔
696
            // Need an enclosing parallel band to host the hoisted init (the init must run
697
            // once per accumulator element, outside the sequential reduction loop).
698
            if (first_sequential == 0) {
1✔
NEW
699
                return {};
×
NEW
700
            }
×
701
            state.init_hoist_ = true;
1✔
702
            state.hoist_body_ = &state.consumer_loops_[first_sequential - 1]->root();
1✔
703
        }
1✔
704
    }
3✔
705

706
    state.domains_match = domains_match;
22✔
707
    if (!state.init_hoist_ && (!domains_match || state.direction_ != FusionDirection::ProducerIntoConsumer)) {
22✔
708
        // we will be copying a loop, so we can do our integrated RLE to simplify memory accesses
709
        for (auto& candidate : state.fusion_candidates_) {
23✔
710
            //
711
            candidate.integrated_rle = true;
23✔
712
        }
23✔
713
    }
19✔
714

715
    // Criterion: At least one valid fusion candidate
716
    if (!state.fusion_candidates_.empty()) {
22✔
717
        return std::move(state_ptr);
22✔
718
    } else {
22✔
NEW
719
        return {};
×
NEW
720
    }
×
721
}
22✔
722

723
ComplexFusionResult LoopFusionByAccessWorker::apply_fusion_by_access_plan(std::unique_ptr<Plan> plan_ptr) {
22✔
724
    auto& plan = *plan_ptr;
22✔
725

726
    if (plan.direction_ == FusionDirection::ProducerIntoConsumer) {
22✔
727
        return apply_producer_into_consumer(plan);
22✔
728
    } else {
22✔
NEW
729
        return apply_consumer_into_producer(plan);
×
NEW
730
    }
×
731
}
22✔
732

733
ComplexFusionResult LoopFusionByAccessWorker::apply_producer_into_consumer(Plan& plan) {
22✔
734
    auto& builder = this->builder();
22✔
735
    auto& sdfg = builder.subject();
22✔
736

737
    // Pattern 1 + Reverse Pattern 2: Inline producer blocks into consumer's read body
738
    auto& first_dataflow = plan.producer_block_->dataflow();
22✔
739

740
    // For each fusion candidate, create a temp and insert a producer block
741
    std::vector<std::string> candidate_temps;
22✔
742

743
    int rle_count = 0;
22✔
744
    for (size_t cand_idx = 0; cand_idx < plan.fusion_candidates_.size(); ++cand_idx) {
48✔
745
        auto& candidate = plan.fusion_candidates_[cand_idx];
26✔
746

747
        auto& container_type = sdfg.type(candidate.container);
26✔
748
        types::Scalar tmp_type(container_type.primitive_type());
26✔
749
        std::string temp_name;
26✔
750
        if (candidate.integrated_rle) {
26✔
751
            ++rle_count;
23✔
752
            // if we are forced to create a copy of the prod loop, then we can do integrated RLE
753
            // as it will reduce how many arguments we need to update
754
            // Case 1: scalarize the streamed element into a private temp.
755
            temp_name = builder.find_new_name("_fused_tmp");
23✔
756
            builder.add_container(temp_name, tmp_type);
23✔
757
            candidate_temps.push_back(temp_name);
23✔
758
        }
23✔
759

760
        // Insert the producer block at the beginning of the host sequence:
761
        //  - Case 1 (stream):     consumer_body_ = innermost sequential (reduction) loop body.
762
        //  - Case 2 (init-hoist): hoist_body_   = outer parallel-band body, before that loop.
763
        auto& host_seq = plan.consumer_target_sequence();
26✔
764
        auto& first_child = host_seq.at(0);
26✔
765
        auto& new_block = builder.add_block_before(host_seq, first_child);
26✔
766
        structured_control_flow::AssignmentBlock* init_assignment_block = nullptr;
26✔
767

768
        // Deep copy all nodes from producer block to new block
769
        std::unordered_map<const data_flow::DataFlowNode*, data_flow::DataFlowNode*> node_mapping;
26✔
770
        std::unordered_map<std::string, std::string> intermediate_renames;
26✔
771
        for (auto& node : first_dataflow.nodes()) {
85✔
772
            node_mapping[&node] = &builder.copy_node(new_block, node);
85✔
773
            auto* copied = node_mapping[&node];
85✔
774
            if (auto* access_node = dynamic_cast<data_flow::AccessNode*>(copied)) {
85✔
775
                if (access_node->data() == candidate.container && candidate.integrated_rle) {
59✔
776
                    // Case 1: redirect the producer's array write to the private scalar.
777
                    access_node->data(temp_name);
23✔
778
                } else if (access_node->data() == plan.first.indvar()->get_name()) {
36✔
779
                    // Determine the new expression for the index variable of the first map
780
                    symbolic::Expression new_expr = SymEngine::null;
1✔
781
                    for (auto& c : plan.fusion_candidates_) {
1✔
782
                        for (auto& [sym, expr] : c.index_mappings) {
1✔
783
                            if (symbolic::eq(sym, plan.first.indvar())) {
1✔
784
                                new_expr = expr;
1✔
785
                                break;
1✔
786
                            }
1✔
787
                        }
1✔
788
                        if (!new_expr.is_null()) {
1✔
789
                            break;
1✔
790
                        }
1✔
791
                    }
1✔
792

793
                    if (new_expr.is_null() || symbolic::eq(new_expr, plan.second.indvar())) {
1✔
794
                        // Simple case: The new expression is simply the index variable of the second loop
NEW
795
                        access_node->data(plan.second.indvar()->get_name());
×
796
                    } else {
1✔
797
                        // Complex case: Add AssignmentBlock before the new block (if necessary) and store the
798
                        // shifted index into a new temporary variable with an assignment. Then, replace the index
799
                        // variable with the new temporary variable
800
                        auto new_index_name = builder.find_new_name();
1✔
801
                        builder.add_container(new_index_name, builder.subject().type(plan.second.indvar()->get_name()));
1✔
802

803
                        if (!init_assignment_block) {
1✔
804
                            init_assignment_block = &builder.add_assignments_at(host_seq, 0, {});
1✔
805
                        }
1✔
806
                        init_assignment_block->assignments().insert({symbolic::symbol(new_index_name), new_expr});
1✔
807
                        access_node->data(new_index_name);
1✔
808
                    }
1✔
809
                } else if (first_dataflow.in_degree(node) > 0 && first_dataflow.out_degree(node) > 0 &&
35✔
810
                           dynamic_cast<const types::Scalar*>(&sdfg.type(access_node->data())) != nullptr) {
35✔
811
                    // SSA Dataflow required to check for non-local use of the access node's container.
812
                    // Intermediate access node (e.g. from a prior BlockFusion): clone
813
                    // its container so each inlined copy gets its own private scalar
NEW
814
                    auto it = intermediate_renames.find(access_node->data());
×
NEW
815
                    if (it == intermediate_renames.end()) {
×
NEW
816
                        std::string fresh = builder.find_new_name(access_node->data());
×
NEW
817
                        builder.add_container(fresh, sdfg.type(access_node->data()));
×
NEW
818
                        intermediate_renames[access_node->data()] = fresh;
×
NEW
819
                    }
×
NEW
820
                    access_node->data(intermediate_renames[access_node->data()]);
×
NEW
821
                }
×
822
            }
59✔
823
        }
85✔
824

825
        // Add memlets with index substitution (producer indvars → consumer expressions)
826
        for (auto& edge : first_dataflow.edges()) {
59✔
827
            auto& src_node = edge.src();
59✔
828
            auto& dst_node = edge.dst();
59✔
829

830
            const types::IType* base_type = &edge.base_type();
59✔
831
            data_flow::Subset new_subset;
59✔
832
            for (const auto& dim : edge.subset()) {
59✔
833
                auto new_dim = dim;
59✔
834
                for (const auto& [pvar, mapping] : candidate.index_mappings) {
85✔
835
                    new_dim = symbolic::subs(new_dim, pvar, mapping);
85✔
836
                }
85✔
837
                new_dim = symbolic::expand(new_dim);
59✔
838
                new_subset.push_back(new_dim);
59✔
839
            }
59✔
840

841
            // Integrated RLE: the producer's array write becomes a scalar write (empty subset).
842
            // Default: keep the remapped array subset so the init writes the accumulator.
843
            auto* dst_access = dynamic_cast<data_flow::AccessNode*>(&dst_node);
59✔
844
            if (dst_access != nullptr && dst_access->data() == candidate.container && candidate.integrated_rle &&
59✔
845
                first_dataflow.in_degree(*dst_access) > 0) {
59✔
846
                new_subset.clear();
23✔
847
                base_type = &tmp_type;
23✔
848
            }
23✔
849

850
            builder.add_memlet(
59✔
851
                new_block,
59✔
852
                *node_mapping[&src_node],
59✔
853
                edge.src_conn(),
59✔
854
                *node_mapping[&dst_node],
59✔
855
                edge.dst_conn(),
59✔
856
                new_subset,
59✔
857
                *base_type,
59✔
858
                edge.debug_info()
59✔
859
            );
59✔
860
        }
59✔
861
    }
26✔
862

863
    DEBUG_PRINTLN(
22✔
864
        "Fusing loop stack by-access (#"
22✔
865
        << plan.first.element_id() << " | #" << plan.producer_loops_.back()->element_id() << " -> #"
22✔
866
        << plan.second.element_id() << " | #" << plan.consumer_loops_.back()->element_id()
22✔
867
        << (plan.init_hoist_ ? ", redu-init" : "") << (plan.domains_match ? ", domain-match" : "") << ", "
22✔
868
        << plan.fusion_candidates_.size() << " fRegs, " << rle_count << " RLEs"
22✔
869
        << ")"
22✔
870
    );
22✔
871

872
    // Integrated RLE: rewrite consumer reads of the fused arrays to the scalar temps.
873
    if (rle_count) {
22✔
874
        size_t num_producer_blocks = plan.fusion_candidates_.size();
19✔
875
        transformations::FusionConsumerUpdateVisitor update_visitor(builder, plan.fusion_candidates_, candidate_temps);
19✔
876
        update_visitor.dispatch_partial_sequence(*plan.consumer_body_, num_producer_blocks, plan.consumer_body_->size());
19✔
877
    }
19✔
878

879
    auto& ana = get_loop_analysis();
22✔
880
    auto& first_inner_info = ana.loop_info_local(plan.producer_loops_.back());
22✔
881

882
    ana.added_local_contents(
22✔
883
        plan.consumer_loops_.back(),
22✔
884
        first_inner_info.contains_side_effects,
22✔
885
        first_inner_info.contains_non_perfectly_nested
22✔
886
    );
22✔
887

888
    // innermost loops had to be leaf
889
    auto first_current = get_fuse_candidate(*plan.producer_loops_.back());
22✔
890
    auto second_current = get_fuse_candidate(*plan.consumer_target_loop());
22✔
891

892
    update_copied_leaf_contents_from_first_to_second(plan, first_current, second_current);
22✔
893

894
    bool removed_first = false;
22✔
895
    if ((!rle_count && plan.domains_match) || plan.init_hoist_) {
22✔
896
        ana.removed_loop(&plan.first);
3✔
897

898
        // We have moved all the relevant contents of producer loop, including any potential array writes, so
899
        builder.remove_from_parent(plan.first);
3✔
900
        removed_first = true;
3✔
901
    }
3✔
902

903
    return {
22✔
904
        .pattern_result =
22✔
905
            {.removed_first = removed_first, .visit_second_body = false, .second_root_replacement = nullptr},
22✔
906
        .fused = true
22✔
907
    };
22✔
908
}
22✔
909

NEW
910
ComplexFusionResult LoopFusionByAccessWorker::apply_consumer_into_producer(Plan& plan) {
×
NEW
911
    auto& builder = this->builder();
×
NEW
912
    auto& sdfg = builder.subject();
×
913

NEW
914
    throw std::runtime_error("ConsumerIntoProducer fusion not yet supported");
×
915

NEW
916
    DEBUG_PRINTLN(
×
NEW
917
        "Fusing " << plan.first.element_id() << " - " << plan.second.element_id() << " by "
×
NEW
918
                  << static_cast<int>(plan.direction_)
×
NEW
919
    );
×
920

921
    // ConsumerIntoProducer (Pattern 2): Inline consumer blocks into the producer's write body
922
    // Modify the producer block in-place to write to a temp scalar, add a writeback block
923
    // for the original array, then copy consumer blocks reading from the temp.
924

NEW
925
    std::vector<std::string> candidate_temps;
×
NEW
926
    auto& producer_dataflow = plan.producer_block_->dataflow();
×
927

NEW
928
    for (size_t cand_idx = 0; cand_idx < plan.fusion_candidates_.size(); ++cand_idx) {
×
NEW
929
        auto& candidate = plan.fusion_candidates_[cand_idx];
×
930

NEW
931
        auto& container_type = sdfg.type(candidate.container);
×
NEW
932
        std::string temp_name = builder.find_new_name("_fused_tmp");
×
NEW
933
        types::Scalar tmp_type(container_type.primitive_type());
×
NEW
934
        builder.add_container(temp_name, tmp_type);
×
NEW
935
        candidate_temps.push_back(temp_name);
×
936

937
        // Step 1: Modify the original producer block to write to _fused_tmp
NEW
938
        data_flow::Subset original_write_subset;
×
NEW
939
        for (auto& node : producer_dataflow.nodes()) {
×
NEW
940
            auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
×
NEW
941
            if (access == nullptr || access->data() != candidate.container) continue;
×
NEW
942
            if (producer_dataflow.in_degree(*access) == 0) continue;
×
943

944
            // This is the write access node — save the original subset, then redirect
NEW
945
            for (auto& in_edge : producer_dataflow.in_edges(*access)) {
×
NEW
946
                original_write_subset = in_edge.subset();
×
NEW
947
                in_edge.set_subset({});
×
NEW
948
                in_edge.set_base_type(tmp_type);
×
NEW
949
            }
×
NEW
950
            access->data(temp_name);
×
NEW
951
            break;
×
NEW
952
        }
×
953

954
        // Step 2: Add a writeback block: container[original_subset] = _fused_tmp
NEW
955
        auto& wb_block = builder.add_block_after(*plan.producer_body_, *plan.producer_block_);
×
NEW
956
        auto& wb_src = builder.add_access(wb_block, temp_name);
×
NEW
957
        auto& wb_dst = builder.add_access(wb_block, candidate.container);
×
NEW
958
        auto& wb_tasklet = builder.add_tasklet(wb_block, data_flow::TaskletCode::assign, "_out", {"_in"});
×
NEW
959
        builder.add_computational_memlet(wb_block, wb_src, wb_tasklet, "_in", {});
×
NEW
960
        builder.add_computational_memlet(wb_block, wb_tasklet, "_out", wb_dst, original_write_subset);
×
961

962
        // Step 3: Copy consumer blocks after the writeback block
NEW
963
        structured_control_flow::ControlFlowNode* last_inserted = &wb_block;
×
964

NEW
965
        for (size_t i = 0; i < plan.consumer_body_->size(); ++i) {
×
NEW
966
            auto* consumer_block = dyn_cast<structured_control_flow::Block*>(&plan.consumer_body_->at(i));
×
NEW
967
            if (consumer_block == nullptr) {
×
NEW
968
                continue;
×
NEW
969
            }
×
970

NEW
971
            auto& consumer_dataflow = consumer_block->dataflow();
×
972

973
            // Check if this block reads from the fusion container
NEW
974
            bool reads_container = false;
×
NEW
975
            for (auto& node : consumer_dataflow.nodes()) {
×
NEW
976
                auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
×
NEW
977
                if (access != nullptr && access->data() == candidate.container &&
×
NEW
978
                    consumer_dataflow.out_degree(*access) > 0) {
×
NEW
979
                    reads_container = true;
×
NEW
980
                    break;
×
NEW
981
                }
×
NEW
982
            }
×
NEW
983
            if (!reads_container) {
×
NEW
984
                continue;
×
NEW
985
            }
×
986

987
            // Insert a new block after the last inserted block in the producer's body
NEW
988
            auto& new_block = builder.add_block_after(*plan.producer_body_, *last_inserted);
×
NEW
989
            structured_control_flow::AssignmentBlock* init_assignment_block = nullptr;
×
990

991
            // Deep copy all nodes from consumer block
NEW
992
            std::unordered_map<const data_flow::DataFlowNode*, data_flow::DataFlowNode*> node_mapping;
×
NEW
993
            std::unordered_map<std::string, std::string> intermediate_renames;
×
NEW
994
            for (auto& node : consumer_dataflow.nodes()) {
×
NEW
995
                node_mapping[&node] = &builder.copy_node(new_block, node);
×
NEW
996
                auto* copied = node_mapping[&node];
×
NEW
997
                if (auto* access_node = dynamic_cast<data_flow::AccessNode*>(copied)) {
×
NEW
998
                    if (access_node->data() == candidate.container) {
×
999
                        // Only rename read access nodes to temp; keep write access nodes
1000
                        // pointing to the original container
NEW
1001
                        if (consumer_dataflow.in_degree(node) == 0) {
×
NEW
1002
                            access_node->data(temp_name);
×
NEW
1003
                        }
×
NEW
1004
                    } else if (consumer_dataflow.in_degree(node) > 0 && consumer_dataflow.out_degree(node) > 0 &&
×
NEW
1005
                               dynamic_cast<const types::Scalar*>(&sdfg.type(access_node->data())) != nullptr) {
×
1006
                        // SSA Dataflow required to check for non-local use of the access node's container.
1007
                        // Intermediate access node (e.g. from a prior BlockFusion): clone
1008
                        // its container so each inlined copy gets its own private scalar
NEW
1009
                        auto it = intermediate_renames.find(access_node->data());
×
NEW
1010
                        if (it == intermediate_renames.end()) {
×
NEW
1011
                            std::string fresh = builder.find_new_name(access_node->data());
×
NEW
1012
                            builder.add_container(fresh, sdfg.type(access_node->data()));
×
NEW
1013
                            intermediate_renames[access_node->data()] = fresh;
×
NEW
1014
                        }
×
NEW
1015
                        access_node->data(intermediate_renames[access_node->data()]);
×
NEW
1016
                    }
×
NEW
1017
                    if (access_node->data() == plan.second.indvar()->get_name() &&
×
NEW
1018
                        consumer_dataflow.in_degree(node) == 0) {
×
1019
                        // Determine the new expression for the index variable of the second loop
NEW
1020
                        symbolic::Expression new_expr = SymEngine::null;
×
NEW
1021
                        for (auto& c : plan.fusion_candidates_) {
×
NEW
1022
                            for (auto& [sym, expr] : c.index_mappings) {
×
NEW
1023
                                if (symbolic::eq(sym, plan.second.indvar())) {
×
NEW
1024
                                    new_expr = expr;
×
NEW
1025
                                    break;
×
NEW
1026
                                }
×
NEW
1027
                            }
×
NEW
1028
                            if (!new_expr.is_null()) {
×
NEW
1029
                                break;
×
NEW
1030
                            }
×
NEW
1031
                        }
×
1032

NEW
1033
                        if (new_expr.is_null() || symbolic::eq(new_expr, plan.first.indvar())) {
×
1034
                            // Simple case: The new expression is simply the index variable of the first map
NEW
1035
                            access_node->data(plan.first.indvar()->get_name());
×
NEW
1036
                        } else {
×
1037
                            // Complex case: Add an AssignmentBlock (if necessary) and store the
1038
                            // shifted index into a new temporary variable with an assignment. Then, replace the
1039
                            // index variable with the new temporary variable
NEW
1040
                            if (!init_assignment_block) {
×
NEW
1041
                                init_assignment_block = &builder.add_assignments_at(*plan.producer_body_, 0, {});
×
NEW
1042
                            }
×
NEW
1043
                            auto new_index_name = builder.find_new_name();
×
NEW
1044
                            builder
×
NEW
1045
                                .add_container(new_index_name, builder.subject().type(plan.first.indvar()->get_name()));
×
NEW
1046
                            init_assignment_block->assignments().insert({symbolic::symbol(new_index_name), new_expr});
×
NEW
1047
                            access_node->data(new_index_name);
×
NEW
1048
                        }
×
NEW
1049
                    }
×
NEW
1050
                }
×
NEW
1051
            }
×
1052

1053
            // Add memlets with index substitution (consumer indvars → producer expressions)
NEW
1054
            for (auto& edge : consumer_dataflow.edges()) {
×
NEW
1055
                auto& src_node = edge.src();
×
NEW
1056
                auto& dst_node = edge.dst();
×
1057

NEW
1058
                const types::IType* base_type = &edge.base_type();
×
NEW
1059
                data_flow::Subset new_subset;
×
NEW
1060
                for (const auto& dim : edge.subset()) {
×
NEW
1061
                    auto new_dim = dim;
×
NEW
1062
                    for (const auto& [cvar, mapping] : candidate.index_mappings) {
×
NEW
1063
                        new_dim = symbolic::subs(new_dim, cvar, mapping);
×
NEW
1064
                    }
×
NEW
1065
                    new_dim = symbolic::expand(new_dim);
×
NEW
1066
                    new_subset.push_back(new_dim);
×
NEW
1067
                }
×
1068

1069
                // For read edges from temp scalar, use empty subset
NEW
1070
                auto* src_access = dynamic_cast<data_flow::AccessNode*>(&src_node);
×
NEW
1071
                if (src_access != nullptr && src_access->data() == candidate.container &&
×
NEW
1072
                    consumer_dataflow.in_degree(*src_access) == 0) {
×
NEW
1073
                    new_subset.clear();
×
NEW
1074
                    base_type = &tmp_type;
×
NEW
1075
                }
×
1076

NEW
1077
                builder.add_memlet(
×
NEW
1078
                    new_block,
×
NEW
1079
                    *node_mapping[&src_node],
×
NEW
1080
                    edge.src_conn(),
×
NEW
1081
                    *node_mapping[&dst_node],
×
NEW
1082
                    edge.dst_conn(),
×
NEW
1083
                    new_subset,
×
NEW
1084
                    *base_type,
×
NEW
1085
                    edge.debug_info()
×
NEW
1086
                );
×
NEW
1087
            }
×
1088

NEW
1089
            last_inserted = &new_block;
×
NEW
1090
        }
×
NEW
1091
    }
×
1092

1093
    // Remove the consumer loop
NEW
1094
    auto* parent = plan.second.get_parent();
×
NEW
1095
    auto* parent_seq = dyn_cast<structured_control_flow::Sequence*>(parent);
×
NEW
1096
    if (parent_seq != nullptr) {
×
NEW
1097
        int idx = parent_seq->index(plan.second);
×
NEW
1098
        if (idx >= 0) {
×
NEW
1099
            builder.remove_child(*parent_seq, static_cast<size_t>(idx));
×
NEW
1100
        }
×
NEW
1101
    }
×
NEW
1102
}
×
1103

1104
ComplexFusionResult LoopFusionByAccessWorker::
1105
    try_fuse_by_access(FusionLoopCandidate& first, FusionLoopCandidate& second, bool domains_match) {
37✔
1106
    auto plan = try_create_fusion_by_access_plan(first, second, domains_match);
37✔
1107
    if (plan) {
37✔
1108
        return apply_fusion_by_access_plan(std::move(plan));
22✔
1109
    } else {
22✔
1110
        return {};
15✔
1111
    }
15✔
1112
}
37✔
1113

1114
} // namespace sdfg::passes::loop_fusion
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc