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

daisytuner / docc / 30476914782

29 Jul 2026 05:47PM UTC coverage: 64.787% (+0.5%) from 64.324%
30476914782

Pull #873

github

web-flow
Merge 9d147ddd2 into 3eb01880e
Pull Request #873: Map fusion migration

1388 of 1889 new or added lines in 13 files covered. (73.48%)

195 existing lines in 6 files now uncovered.

44816 of 69174 relevant lines covered (64.79%)

730.43 hits per line

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

80.25
/opt/src/transformations/map_fusion.cpp
1
#include "sdfg/transformations/map_fusion.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
#include <symengine/solve.h>
9
#include "sdfg/analysis/arguments_analysis.h"
10
#include "sdfg/analysis/loop_analysis.h"
11

12
#include "sdfg/analysis/assumptions_analysis.h"
13
#include "sdfg/control_flow/interstate_edge.h"
14
#include "sdfg/data_flow/data_flow_graph.h"
15
#include "sdfg/passes/map_fusion/map_fusion_by_accesses.h"
16
#include "sdfg/structured_control_flow/block.h"
17
#include "sdfg/structured_control_flow/for.h"
18
#include "sdfg/symbolic/delinearization.h"
19
#include "sdfg/symbolic/utils.h"
20
#include "sdfg/visitor/structured_sdfg_visitor.h"
21

22
namespace sdfg {
23
namespace transformations {
24

25
using passes::map_fusion::FusionRegCandidate;
26

27
MapFusion::MapFusion(
28
    structured_control_flow::Map& first_map,
29
    structured_control_flow::StructuredLoop& second_loop,
30
    bool require_consecutive,
31
    bool allow_init_hoist,
32
    bool cons_into_prod_only
33
)
34
    : first_map_(first_map), second_loop_(second_loop), require_consecutive_(require_consecutive),
45✔
35
      allow_init_hoist_(allow_init_hoist), cons_into_prod_only_(cons_into_prod_only) {}
45✔
36

37
std::string MapFusion::name() const { return "MapFusion"; }
2✔
38

39
using passes::map_fusion::MapFusionByAccessWorker;
40

41
MapFusionByAccessWorker::FusionDirection MapFusion::last_fusion_direction() const { return direction_; }
1✔
42

43
bool MapFusion::find_write_location(
44
    structured_control_flow::StructuredLoop& loop,
45
    const std::string& container,
46
    std::vector<structured_control_flow::StructuredLoop*>& loops,
47
    structured_control_flow::Sequence*& body,
48
    structured_control_flow::Block*& block
49
) {
4✔
50
    loops.push_back(&loop);
4✔
51
    auto& seq = loop.root();
4✔
52

53
    for (size_t i = 0; i < seq.size(); ++i) {
10✔
54
        auto& child = seq.at(i);
6✔
55

56
        if (auto* blk = dyn_cast<structured_control_flow::Block*>(&child)) {
6✔
57
            // Check if this block writes to the container
58
            auto& dataflow = blk->dataflow();
4✔
59
            for (auto& node : dataflow.nodes()) {
14✔
60
                auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
14✔
61
                if (access == nullptr || access->data() != container) {
14✔
62
                    continue;
12✔
63
                }
12✔
64
                // Write access: has incoming edges (sink node)
65
                if (dataflow.in_degree(*access) > 0 && dataflow.out_degree(*access) == 0) {
2✔
66
                    if (block != nullptr) {
2✔
67
                        // Multiple write blocks found — ambiguous
68
                        return false;
×
69
                    }
×
70
                    body = &seq;
2✔
71
                    block = blk;
2✔
72
                }
2✔
73
            }
2✔
74
        } else if (auto* nested_loop = dyn_cast<structured_control_flow::StructuredLoop*>(&child)) {
4✔
75
            if (!find_write_location(*nested_loop, container, loops, body, block)) {
2✔
76
                return false;
×
77
            }
×
78
            // If we didn't find the write in this subtree, pop the loop back off
79
            if (loops.back() != &loop && block == nullptr) {
2✔
80
                // The recursive call already popped — but we need to check
81
            }
×
82
        }
2✔
83
    }
6✔
84

85
    // If we didn't find the write in this subtree, remove this loop from the chain
86
    if (block == nullptr) {
4✔
87
        loops.pop_back();
×
88
    }
×
89

90
    return true;
4✔
91
}
4✔
92

93
bool MapFusion::find_read_location(
94
    structured_control_flow::StructuredLoop& loop,
95
    const std::string& container,
96
    std::vector<structured_control_flow::StructuredLoop*>& loops,
97
    structured_control_flow::Sequence*& body
98
) {
2✔
99
    loops.push_back(&loop);
2✔
100
    auto& seq = loop.root();
2✔
101

102
    for (size_t i = 0; i < seq.size(); ++i) {
5✔
103
        auto& child = seq.at(i);
3✔
104

105
        if (auto* blk = dyn_cast<structured_control_flow::Block*>(&child)) {
3✔
106
            // Check if this block reads from the container
107
            auto& dataflow = blk->dataflow();
2✔
108
            for (auto& node : dataflow.nodes()) {
8✔
109
                auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
8✔
110
                if (access == nullptr || access->data() != container) {
8✔
111
                    continue;
7✔
112
                }
7✔
113
                // Read access: has outgoing edges (source node)
114
                if (dataflow.in_degree(*access) == 0 && dataflow.out_degree(*access) > 0) {
1✔
115
                    if (body != nullptr && body != &seq) {
1✔
116
                        // Reads at different sequence levels — ambiguous
117
                        return false;
×
118
                    }
×
119
                    body = &seq;
1✔
120
                }
1✔
121
            }
1✔
122
        } else if (auto* nested_loop = dyn_cast<structured_control_flow::StructuredLoop*>(&child)) {
2✔
123
            if (!find_read_location(*nested_loop, container, loops, body)) {
1✔
124
                return false;
×
125
            }
×
126
        }
1✔
127
    }
3✔
128

129
    // If we didn't find any reads in this subtree, remove this loop from the chain
130
    if (body == nullptr) {
2✔
131
        loops.pop_back();
×
132
    }
×
133

134
    return true;
2✔
135
}
2✔
136

137
bool MapFusion::can_be_applied(builder::StructuredSDFGBuilder& builder, analysis::AnalysisManager& analysis_manager) {
43✔
138
    fusion_candidates_.clear();
43✔
139

140
    // no use in fusing empty loops. Also presumed to not be empty further down
141
    if (first_map_.root().size() == 0 || second_loop_.root().size() == 0) {
43✔
142
        return false;
×
143
    }
×
144

145
    // Criterion: Get parent scope and verify both loops are sequential children
146
    auto* first_parent = first_map_.get_parent();
43✔
147
    auto* second_parent = second_loop_.get_parent();
43✔
148
    if (first_parent == nullptr || second_parent == nullptr) {
43✔
149
        return false;
×
150
    }
×
151
    if (first_parent != second_parent) {
43✔
152
        return false;
×
153
    }
×
154

155
    auto* parent_sequence = dyn_cast<structured_control_flow::Sequence*>(first_parent);
43✔
156
    if (parent_sequence == nullptr) {
43✔
157
        return false;
×
158
    }
×
159

160
    int first_index = parent_sequence->index(first_map_);
43✔
161
    int second_index = parent_sequence->index(second_loop_);
43✔
162
    if (first_index == -1 || second_index == -1) {
43✔
163
        return false;
×
164
    }
×
165
    if (require_consecutive_ && second_index != first_index + 1) {
43✔
166
        return false;
1✔
167
    }
1✔
168

169
    // Determine fusion pattern based on nesting properties
170
    auto& loop_analysis = analysis_manager.get<analysis::LoopAnalysis>();
42✔
171
    auto first_loop_info = loop_analysis.loop_info(&first_map_);
42✔
172
    auto second_loop_info = loop_analysis.loop_info(&second_loop_);
42✔
173

174
    bool first_nested = first_loop_info.is_perfectly_nested;
42✔
175
    bool second_nested = second_loop_info.is_perfectly_nested;
42✔
176

177
    // Both non-perfectly-nested: not supported
178
    if (!first_nested && !second_nested) {
42✔
179
        return false;
1✔
180
    }
1✔
181

182
    if (first_nested && second_nested) {
41✔
183
        // Pattern 1: Both perfectly nested — producer into consumer (original path)
184
        direction_ = MapFusionByAccessWorker::FusionDirection::ProducerIntoConsumer;
38✔
185
    } else if (!first_nested && second_nested) {
38✔
186
        // Pattern 2: Producer non-perfectly-nested, consumer perfectly nested
187
        direction_ = MapFusionByAccessWorker::FusionDirection::ConsumerIntoProducer;
2✔
188
    } else {
2✔
189
        // Reverse Pattern 2: Producer perfectly nested, consumer non-perfectly-nested
190
        direction_ = MapFusionByAccessWorker::FusionDirection::ProducerIntoConsumer;
1✔
191
    }
1✔
192

193
    // The side being inlined must be all-parallel (all Maps) so iterations can be reordered.
194
    // ProducerIntoConsumer: the producer is replicated at each consumer site and must be
195
    // reorderable, so it must be all-parallel. The consumer is normally required to be
196
    // all-parallel too, because a sequential (For) loop would re-execute the inlined producer
197
    // on every iteration (e.g. init T=0 fused into For(k){T+=A[k]} re-initializes each k).
198
    //
199
    // Reduction branch: we relax the consumer requirement when the consumer is a perfect nest
200
    // (parallel outer band + inner sequential For, i.e. a reduction). A fully-parallel producer
201
    // that is *streamed element-by-element* inside the reduction loop can still be inlined
202
    // soundly (e.g. scale -> max: max(M, A[i,j,k]/d)). The element-streaming safety conditions
203
    // are verified once the fusion candidates are known (see consumer_reduction_branch below):
204
    //   (1) the fused container must not be written by the consumer (no loop-carried
205
    //       accumulator), and
206
    //   (2) its consumer read subset must depend on an inner sequential loop indvar, so the
207
    //       inlined producer runs once per element rather than per init position.
208
    // These keep init-into-reduction (T=0 followed by For(k){T+=...}) rejected.
209
    // ConsumerIntoProducer: only the consumer (inlined side) must be all-parallel.
210
    bool consumer_reduction_branch = false;
41✔
211
    if (direction_ == MapFusionByAccessWorker::FusionDirection::ProducerIntoConsumer) {
41✔
212
        if (!first_loop_info.is_perfectly_parallel) {
39✔
213
            return false;
×
214
        } else if (!second_loop_info.is_perfectly_parallel) {
39✔
215
            if (!second_loop_info.is_perfectly_nested) {
2✔
216
                return false;
×
217
            }
×
218
            consumer_reduction_branch = true;
2✔
219
        }
2✔
220
    } else {
39✔
221
        if (!second_loop_info.is_perfectly_parallel) {
2✔
222
            return false;
×
223
        }
×
224
    }
2✔
225

226
    // Locate producer write point
227
    producer_loops_.clear();
41✔
228
    producer_body_ = nullptr;
41✔
229
    producer_block_ = nullptr;
41✔
230

231
    if (first_nested) {
41✔
232
        // Perfectly nested: walk the at(0).first chain
233
        producer_loops_.push_back(&first_map_);
39✔
234
        producer_body_ = &first_map_.root();
39✔
235
        structured_control_flow::ControlFlowNode* node = &first_map_.root().at(0);
39✔
236
        while (auto* nested = dyn_cast<structured_control_flow::StructuredLoop*>(node)) {
51✔
237
            producer_loops_.push_back(nested);
13✔
238
            producer_body_ = &nested->root();
13✔
239
            if (nested->root().size() == 0) return false;
13✔
240
            node = &nested->root().at(0);
12✔
241
        }
12✔
242
        producer_block_ = dyn_cast<structured_control_flow::Block*>(node);
38✔
243
        if (producer_block_ == nullptr) {
38✔
244
            return false;
×
245
        }
×
246
        // If the body has multiple children, the at(0) walk does not guarantee
247
        // we found the correct (or unique) write block. Fall back to deferred
248
        // find_write_location resolution.
249
        if (producer_body_->size() != 1) {
38✔
250
            producer_block_ = nullptr;
×
251
            // Keep producer_loops_ and producer_body_ from the walk — they are
252
            // still valid for the loop chain. find_write_location will re-resolve
253
            // the block within producer_body_.
254
        }
×
255
    } else {
38✔
256
        // Non-perfectly-nested: search recursively for the write block
257
        // We need to know which containers to look for, but we don't know them yet.
258
        // Defer write location search until after fusion_containers are identified.
259
    }
2✔
260

261
    // Locate consumer read point
262
    consumer_loops_.clear();
40✔
263
    consumer_body_ = nullptr;
40✔
264

265
    if (second_nested) {
40✔
266
        // Perfectly nested: walk the at(0).first chain through all loop types.
267
        // Reduction patterns (e.g. Map{Map{For{T[i,j]+=...}}}) are rejected by
268
        // the is_perfectly_parallel check — For loops make it non-parallel.
269
        consumer_loops_.push_back(&second_loop_);
39✔
270
        consumer_body_ = &second_loop_.root();
39✔
271
        structured_control_flow::ControlFlowNode* node = &second_loop_.root().at(0);
39✔
272
        while (auto* nested = dyn_cast<structured_control_flow::StructuredLoop*>(node)) {
53✔
273
            consumer_loops_.push_back(nested);
15✔
274
            consumer_body_ = &nested->root();
15✔
275
            if (nested->root().size() == 0) return false;
15✔
276
            node = &nested->root().at(0);
14✔
277
        }
14✔
278
    } else {
39✔
279
        // Non-perfectly-nested: defer read location search until after fusion_containers are identified.
280
    }
1✔
281

282
    // Get arguments analysis to identify inputs/outputs of each loop
283
    auto& arguments_analysis = analysis_manager.get<analysis::ArgumentsAnalysis>();
39✔
284
    auto first_args = arguments_analysis.arguments(analysis_manager, first_map_);
39✔
285
    auto second_args = arguments_analysis.arguments(analysis_manager, second_loop_);
39✔
286

287
    std::unordered_set<std::string> first_inputs;
39✔
288
    std::unordered_set<std::string> first_outputs;
39✔
289
    for (const auto& [name, arg] : first_args) {
131✔
290
        if (arg.is_output) {
131✔
291
            first_outputs.insert(name);
41✔
292
        }
41✔
293
        if (arg.is_input) {
131✔
294
            first_inputs.insert(name);
131✔
295
        }
131✔
296
    }
131✔
297

298
    std::unordered_set<std::string> second_outputs;
39✔
299
    for (const auto& [name, arg] : second_args) {
137✔
300
        if (arg.is_output) {
137✔
301
            second_outputs.insert(name);
40✔
302
        }
40✔
303
    }
137✔
304

305
    // First pass: identify fusion containers (producer writes, consumer reads)
306
    std::unordered_set<std::string> fusion_containers;
39✔
307
    for (const auto& [name, arg] : second_args) {
137✔
308
        if (first_outputs.contains(name) && arg.is_input) {
137✔
309
            fusion_containers.insert(name);
39✔
310
        }
39✔
311
    }
137✔
312
    if (fusion_containers.empty()) {
39✔
313
        return false;
1✔
314
    }
1✔
315

316
    // Second pass: check for conflicts on non-fusion containers
317
    for (const auto& [name, arg] : second_args) {
132✔
318
        bool is_fusion = fusion_containers.contains(name);
132✔
319
        if (first_outputs.contains(name) && arg.is_output && !is_fusion) {
132✔
320
            return false;
×
321
        }
×
322
        if (first_inputs.contains(name) && arg.is_output && !is_fusion) {
132✔
323
            return false;
1✔
324
        }
1✔
325
    }
132✔
326

327
    // Now that we know the fusion containers, resolve deferred locations
328
    if (producer_block_ == nullptr) {
37✔
329
        // Non-perfectly-nested producer (or perfectly-nested with multi-block body):
330
        // find write location for the first fusion container.
331
        // All fusion containers must be written at the same block for this to work.
332
        for (const auto& container : fusion_containers) {
2✔
333
            std::vector<structured_control_flow::StructuredLoop*> write_loops;
2✔
334
            structured_control_flow::Sequence* write_body = nullptr;
2✔
335
            structured_control_flow::Block* write_block = nullptr;
2✔
336

337
            if (!find_write_location(first_map_, container, write_loops, write_body, write_block)) {
2✔
338
                return false;
×
339
            }
×
340
            if (write_block == nullptr) {
2✔
341
                return false;
×
342
            }
×
343

344
            if (producer_block_ == nullptr) {
2✔
345
                // First container: set the locations
346
                producer_loops_ = write_loops;
2✔
347
                producer_body_ = write_body;
2✔
348
                producer_block_ = write_block;
2✔
349
            } else {
2✔
350
                // Subsequent containers must be in the same block
351
                if (write_block != producer_block_) {
×
352
                    return false;
×
353
                }
×
354
            }
×
355
        }
2✔
356
    }
2✔
357

358
    if (!second_nested) {
37✔
359
        // Non-perfectly-nested consumer: find read location for the first fusion container
360
        // All fusion containers must be read at the same sequence for this to work
361
        for (const auto& container : fusion_containers) {
1✔
362
            std::vector<structured_control_flow::StructuredLoop*> read_loops;
1✔
363
            structured_control_flow::Sequence* read_body = nullptr;
1✔
364

365
            if (!find_read_location(second_loop_, container, read_loops, read_body)) {
1✔
366
                return false;
×
367
            }
×
368
            if (read_body == nullptr) {
1✔
369
                return false;
×
370
            }
×
371

372
            if (consumer_body_ == nullptr) {
1✔
373
                // First container: set the locations
374
                consumer_loops_ = read_loops;
1✔
375
                consumer_body_ = read_body;
1✔
376
            } else {
1✔
377
                // Subsequent containers must be at the same sequence
378
                if (read_body != consumer_body_) {
×
379
                    return false;
×
380
                }
×
381
            }
×
382
        }
1✔
383
    }
1✔
384

385
    // Check if producer actually reads a fusion container in the dataflow.
386
    // If so, ProducerIntoConsumer is unsafe (original producer loop mutates the array
387
    // before the inlined copy reads it). Force ConsumerIntoProducer.
388
    // We check the dataflow directly rather than ArgumentsAnalysis, because the latter
389
    // conservatively marks written containers as also read.
390
    if (direction_ == MapFusionByAccessWorker::FusionDirection::ProducerIntoConsumer) {
37✔
391
        auto& first_dataflow_check = producer_block_->dataflow();
35✔
392
        bool producer_reads_fusion = false;
35✔
393
        for (const auto& container : fusion_containers) {
36✔
394
            for (auto& node : first_dataflow_check.nodes()) {
124✔
395
                auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
124✔
396
                if (access != nullptr && access->data() == container && first_dataflow_check.out_degree(*access) > 0) {
124✔
397
                    producer_reads_fusion = true;
2✔
398
                    break;
2✔
399
                }
2✔
400
            }
124✔
401
            if (producer_reads_fusion) break;
36✔
402
        }
36✔
403
        if (producer_reads_fusion) {
35✔
404
            direction_ = MapFusionByAccessWorker::FusionDirection::ConsumerIntoProducer;
2✔
405
            // Re-check: consumer must be all-parallel for ConsumerIntoProducer
406
            if (!second_loop_info.is_perfectly_parallel) {
2✔
407
                return false;
×
408
            }
×
409
        }
2✔
410
    }
35✔
411

412
    if (cons_into_prod_only_ && direction_ != MapFusionByAccessWorker::FusionDirection::ConsumerIntoProducer) {
37✔
413
        // THe new mapfusion can handle all the other cases, so don't waste time doing those
NEW
414
        return false;
×
NEW
415
    }
×
416

417
    // ProducerIntoConsumer only deep-copies producer_block_ into the consumer body.
418
    // If the producer body has multiple blocks (e.g. from prior BlockFusion merging
419
    // a previous fusion's writeback + inlined blocks), the write block may depend on
420
    // intermediates produced by earlier blocks that would NOT be copied. Reject.
421
    if (direction_ == MapFusionByAccessWorker::FusionDirection::ProducerIntoConsumer && producer_body_->size() > 1) {
37✔
422
        return false;
×
423
    }
×
424

425
    std::unordered_map<std::string, const data_flow::Subset*> producer_subsets;
37✔
426

427
    // For each fusion container, find the producer memlet and collect unique consumer subsets
428
    auto& first_dataflow = producer_block_->dataflow();
37✔
429
    for (const auto& container : fusion_containers) {
38✔
430
        // Find unique producer write in first map
431
        data_flow::Memlet* producer_memlet = nullptr;
38✔
432

433
        for (auto& node : first_dataflow.nodes()) {
131✔
434
            auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
131✔
435
            if (access == nullptr || access->data() != container) {
131✔
436
                continue;
91✔
437
            }
91✔
438
            // Skip read-only access nodes (producer reads the fusion container)
439
            if (first_dataflow.in_degree(*access) == 0) {
40✔
440
                continue;
2✔
441
            }
2✔
442
            // Write access: must have exactly one incoming edge and no outgoing
443
            if (first_dataflow.in_degree(*access) != 1 || first_dataflow.out_degree(*access) != 0) {
38✔
444
                return false;
×
445
            }
×
446
            auto& iedge = *first_dataflow.in_edges(*access).begin();
38✔
447
            if (iedge.type() != data_flow::MemletType::Computational) {
38✔
448
                return false;
×
449
            }
×
450
            if (producer_memlet != nullptr) {
38✔
451
                return false;
×
452
            }
×
453
            producer_memlet = &iedge;
38✔
454
        }
38✔
455
        if (producer_memlet == nullptr) {
38✔
456
            return false;
×
457
        }
×
458

459
        const auto& producer_subset = producer_memlet->subset();
38✔
460
        if (producer_subset.empty()) {
38✔
461
            return false;
×
462
        } else {
38✔
463
            producer_subsets.emplace(container, &producer_subset);
38✔
464
        }
38✔
465
    }
38✔
466

467
    FusionConsumerSubsetVisitor consumer_visitor(producer_subsets);
37✔
468
    bool abort = consumer_visitor.dispatch(*consumer_body_);
37✔
469
    if (abort) {
37✔
470
        return false;
1✔
471
    }
1✔
472

473
    // Get assumptions for the resolved write/read locations
474
    // Include trivial bounds from types to help delinearization with symbolic strides
475
    auto& assumptions_analysis = analysis_manager.get<analysis::AssumptionsAnalysis>();
36✔
476
    auto& producer_assumptions = assumptions_analysis.get(*producer_block_, true);
36✔
477
    auto& consumer_assumptions = assumptions_analysis.get(consumer_body_->at(0), true);
36✔
478

479
    for (auto [container, unique_subsets] : consumer_visitor.unique_subsets_per_container_) {
37✔
480
        auto& producer_subset = *producer_subsets.at(container);
37✔
481
        // For each unique consumer subset, solve index mappings and create a FusionCandidate
482
        // The direction determines which side's indvars are solved for
483
        for (const auto& consumer_subset : unique_subsets) {
39✔
484
            std::vector<std::pair<symbolic::Symbol, symbolic::Expression>> mappings;
39✔
485

486
            if (direction_ == MapFusionByAccessWorker::FusionDirection::ProducerIntoConsumer) {
39✔
487
                // Solve producer indvars in terms of consumer indvars
488
                mappings = passes::map_fusion::MapFusionByAccessWorker::solve_subsets(
35✔
489
                    producer_subset,
35✔
490
                    consumer_subset,
35✔
491
                    producer_loops_,
35✔
492
                    consumer_loops_,
35✔
493
                    producer_assumptions,
35✔
494
                    consumer_assumptions
35✔
495
                );
35✔
496
            } else {
35✔
497
                // ConsumerIntoProducer: solve consumer indvars in terms of producer indvars
498
                // Arguments are swapped, so invert the range check direction
499
                mappings = passes::map_fusion::MapFusionByAccessWorker::solve_subsets(
4✔
500
                    consumer_subset,
4✔
501
                    producer_subset,
4✔
502
                    consumer_loops_,
4✔
503
                    producer_loops_,
4✔
504
                    consumer_assumptions,
4✔
505
                    producer_assumptions,
4✔
506
                    true
4✔
507
                );
4✔
508
            }
4✔
509

510
            if (mappings.empty()) {
39✔
511
                return false;
5✔
512
            }
5✔
513

514
            FusionRegCandidate candidate{
34✔
515
                .container = container,
34✔
516
                .consumer_subset = consumer_subset,
34✔
517
                .index_mappings = std::move(mappings),
34✔
518
                .integrated_rle = true
34✔
519
            };
34✔
520

521
            fusion_candidates_.push_back(candidate);
34✔
522
        }
34✔
523
    }
37✔
524

525
    // Reduction-branch safety: when fusing a parallel producer into a non-parallel
526
    // (reduction) consumer, classify each fusion container into one of two sound patterns:
527
    //   Case 1 (stream):     the container is NOT a consumer output and its consumer read
528
    //                        depends on an inner sequential indvar -> it is produced and
529
    //                        consumed element-by-element, so the producer is scalarized and
530
    //                        inlined inside the reduction loop (e.g. softmax scale -> max).
531
    //   Case 2 (init-hoist): the container IS a consumer output (the reduction accumulator)
532
    //                        and its consumer read is loop-invariant w.r.t. every sequential
533
    //                        indvar -> the producer is the accumulator's initial value and is
534
    //                        hoisted to the reduction's outer parallel band, before the inner
535
    //                        sequential loop (e.g. T = -INF preceding T = max(T, x)).
536
    // Anything else (e.g. an accumulator whose read depends on the sequential indvar, or a
537
    // streamed value that the consumer also writes) is unsafe and rejected. The two patterns
538
    // require different placement in apply(), so all candidates must share one pattern.
539
    if (consumer_reduction_branch) {
31✔
540
        symbolic::SymbolSet sequential_indvars;
2✔
541
        size_t first_sequential = consumer_loops_.size();
2✔
542
        for (size_t li = 0; li < consumer_loops_.size(); ++li) {
8✔
543
            if (dyn_cast<structured_control_flow::Map*>(consumer_loops_[li]) == nullptr) {
6✔
544
                sequential_indvars.insert(consumer_loops_[li]->indvar());
2✔
545
                if (first_sequential == consumer_loops_.size()) {
2✔
546
                    first_sequential = li;
2✔
547
                }
2✔
548
            }
2✔
549
        }
6✔
550
        if (sequential_indvars.empty()) {
2✔
551
            return false;
×
552
        }
×
553
        bool any_stream = false;
2✔
554
        bool any_init = false;
2✔
555
        for (const auto& candidate : fusion_candidates_) {
2✔
556
            bool depends_on_sequential = false;
2✔
557
            for (const auto& dim : candidate.consumer_subset) {
2✔
558
                for (const auto& atom : symbolic::atoms(dim)) {
8✔
559
                    if (sequential_indvars.count(atom)) {
8✔
560
                        depends_on_sequential = true;
1✔
561
                        break;
1✔
562
                    }
1✔
563
                }
8✔
564
                if (depends_on_sequential) {
2✔
565
                    break;
1✔
566
                }
1✔
567
            }
2✔
568

569
            if (second_outputs.contains(candidate.container)) {
2✔
570
                // Case 2 candidate: must be a loop-invariant accumulator init.
571
                if (!allow_init_hoist_) {
1✔
572
                    // Init-hoisting disabled for this run (reserved for the final
573
                    // map-fusion pass so it does not fight loop distribution).
574
                    return false;
×
575
                }
×
576
                if (depends_on_sequential) {
1✔
577
                    return false;
×
578
                }
×
579
                any_init = true;
1✔
580
            } else {
1✔
581
                // Case 1 candidate: must be a streamed element.
582
                if (!depends_on_sequential) {
1✔
583
                    return false;
×
584
                }
×
585
                any_stream = true;
1✔
586
            }
1✔
587
        }
2✔
588
        // Do not mix patterns in a single fusion.
589
        if (any_init && any_stream) {
2✔
590
            return false;
×
591
        }
×
592
        if (any_init) {
2✔
593
            // Need an enclosing parallel band to host the hoisted init (the init must run
594
            // once per accumulator element, outside the sequential reduction loop).
595
            if (first_sequential == 0) {
1✔
596
                return false;
×
597
            }
×
598
            init_hoist_ = true;
1✔
599
            hoist_body_ = &consumer_loops_[first_sequential - 1]->root();
1✔
600
            for (auto& candidate : fusion_candidates_) {
1✔
601
                candidate.integrated_rle = false;
1✔
602
            }
1✔
603
        }
1✔
604
    }
2✔
605

606
    // Criterion: At least one valid fusion candidate
607
    return !fusion_candidates_.empty();
31✔
608
}
31✔
609

610
void MapFusion::apply(builder::StructuredSDFGBuilder& builder, analysis::AnalysisManager& analysis_manager) {
22✔
611
    auto& sdfg = builder.subject();
22✔
612

613
    DEBUG_PRINTLN(
22✔
614
        "Fusing " << this->first_map_.element_id() << " - " << this->second_loop_.element_id() << " by "
22✔
615
                  << static_cast<int>(direction_)
22✔
616
    );
22✔
617

618
    if (direction_ == MapFusionByAccessWorker::FusionDirection::ProducerIntoConsumer) {
22✔
619
        // Pattern 1 + Reverse Pattern 2: Inline producer blocks into consumer's read body
620
        auto& first_dataflow = producer_block_->dataflow();
19✔
621

622
        // For each fusion candidate, create a temp and insert a producer block
623
        std::vector<std::string> candidate_temps;
19✔
624

625
        for (size_t cand_idx = 0; cand_idx < fusion_candidates_.size(); ++cand_idx) {
40✔
626
            auto& candidate = fusion_candidates_[cand_idx];
21✔
627

628
            auto& container_type = sdfg.type(candidate.container);
21✔
629
            types::Scalar tmp_type(container_type.primitive_type());
21✔
630
            std::string temp_name;
21✔
631
            if (!init_hoist_) {
21✔
632
                // Case 1: scalarize the streamed element into a private temp.
633
                temp_name = builder.find_new_name("_fused_tmp");
20✔
634
                builder.add_container(temp_name, tmp_type);
20✔
635
                candidate_temps.push_back(temp_name);
20✔
636
            }
20✔
637

638
            // Insert the producer block at the beginning of the host sequence:
639
            //  - Case 1 (stream):     consumer_body_ = innermost sequential (reduction) loop body.
640
            //  - Case 2 (init-hoist): hoist_body_   = outer parallel-band body, before that loop.
641
            auto& host_seq = init_hoist_ ? *hoist_body_ : *consumer_body_;
21✔
642
            auto& first_child = host_seq.at(0);
21✔
643
            auto& new_block = builder.add_block_before(host_seq, first_child);
21✔
644
            structured_control_flow::AssignmentBlock* init_assignment_block = nullptr;
21✔
645

646
            // Deep copy all nodes from producer block to new block
647
            std::unordered_map<const data_flow::DataFlowNode*, data_flow::DataFlowNode*> node_mapping;
21✔
648
            std::unordered_map<std::string, std::string> intermediate_renames;
21✔
649
            for (auto& node : first_dataflow.nodes()) {
72✔
650
                node_mapping[&node] = &builder.copy_node(new_block, node);
72✔
651
                auto* copied = node_mapping[&node];
72✔
652
                if (auto* access_node = dynamic_cast<data_flow::AccessNode*>(copied)) {
72✔
653
                    if (!init_hoist_ && access_node->data() == candidate.container) {
50✔
654
                        // Case 1: redirect the producer's array write to the private scalar.
655
                        access_node->data(temp_name);
20✔
656
                    } else if (access_node->data() == first_map_.indvar()->get_name()) {
30✔
657
                        // Determine the new expression for the index variable of the first map
658
                        symbolic::Expression new_expr = SymEngine::null;
2✔
659
                        for (auto& c : fusion_candidates_) {
2✔
660
                            for (auto& [sym, expr] : c.index_mappings) {
2✔
661
                                if (symbolic::eq(sym, first_map_.indvar())) {
2✔
662
                                    new_expr = expr;
2✔
663
                                    break;
2✔
664
                                }
2✔
665
                            }
2✔
666
                            if (!new_expr.is_null()) {
2✔
667
                                break;
2✔
668
                            }
2✔
669
                        }
2✔
670

671
                        if (new_expr.is_null() || symbolic::eq(new_expr, second_loop_.indvar())) {
2✔
672
                            // Simple case: The new expression is simply the index variable of the second loop
673
                            access_node->data(second_loop_.indvar()->get_name());
1✔
674
                        } else {
1✔
675
                            // Complex case: Add AssignmentBlock before the new block (if necessary) and store the
676
                            // shifted index into a new temporary variable with an assignment. Then, replace the index
677
                            // variable with the new temporary variable
678
                            auto new_index_name = builder.find_new_name();
1✔
679
                            builder
1✔
680
                                .add_container(new_index_name, builder.subject().type(second_loop_.indvar()->get_name()));
1✔
681

682
                            if (!init_assignment_block) {
1✔
683
                                init_assignment_block = &builder.add_assignments_at(host_seq, 0, {});
1✔
684
                            }
1✔
685
                            init_assignment_block->assignments().insert({symbolic::symbol(new_index_name), new_expr});
1✔
686
                            access_node->data(new_index_name);
1✔
687
                        }
1✔
688
                    } else if (first_dataflow.in_degree(node) > 0 && first_dataflow.out_degree(node) > 0 &&
28✔
689
                               dynamic_cast<const types::Scalar*>(&sdfg.type(access_node->data())) != nullptr) {
28✔
690
                        // SSA Dataflow required to check for non-local use of the access node's container.
691
                        // Intermediate access node (e.g. from a prior BlockFusion): clone
692
                        // its container so each inlined copy gets its own private scalar
693
                        auto it = intermediate_renames.find(access_node->data());
×
694
                        if (it == intermediate_renames.end()) {
×
695
                            std::string fresh = builder.find_new_name(access_node->data());
×
696
                            builder.add_container(fresh, sdfg.type(access_node->data()));
×
697
                            intermediate_renames[access_node->data()] = fresh;
×
698
                        }
×
699
                        access_node->data(intermediate_renames[access_node->data()]);
×
700
                    }
×
701
                }
50✔
702
            }
72✔
703

704
            // Add memlets with index substitution (producer indvars → consumer expressions)
705
            for (auto& edge : first_dataflow.edges()) {
51✔
706
                auto& src_node = edge.src();
51✔
707
                auto& dst_node = edge.dst();
51✔
708

709
                const types::IType* base_type = &edge.base_type();
51✔
710
                data_flow::Subset new_subset;
51✔
711
                for (const auto& dim : edge.subset()) {
51✔
712
                    auto new_dim = dim;
46✔
713
                    for (const auto& [pvar, mapping] : candidate.index_mappings) {
64✔
714
                        new_dim = symbolic::subs(new_dim, pvar, mapping);
64✔
715
                    }
64✔
716
                    new_dim = symbolic::expand(new_dim);
46✔
717
                    new_subset.push_back(new_dim);
46✔
718
                }
46✔
719

720
                // Case 1: the producer's array write becomes a scalar write (empty subset).
721
                // Case 2: keep the remapped array subset so the init writes the accumulator.
722
                auto* dst_access = dynamic_cast<data_flow::AccessNode*>(&dst_node);
51✔
723
                if (!init_hoist_ && dst_access != nullptr && dst_access->data() == candidate.container &&
51✔
724
                    first_dataflow.in_degree(*dst_access) > 0) {
51✔
725
                    new_subset.clear();
20✔
726
                    base_type = &tmp_type;
20✔
727
                }
20✔
728

729
                builder.add_memlet(
51✔
730
                    new_block,
51✔
731
                    *node_mapping[&src_node],
51✔
732
                    edge.src_conn(),
51✔
733
                    *node_mapping[&dst_node],
51✔
734
                    edge.dst_conn(),
51✔
735
                    new_subset,
51✔
736
                    *base_type,
51✔
737
                    edge.debug_info()
51✔
738
                );
51✔
739
            }
51✔
740
        }
21✔
741

742
        // Case 1 only: rewrite consumer reads of the fused arrays to the scalar temps.
743
        // Case 2 leaves the reduction body untouched (it keeps reading/writing the accumulator,
744
        // now pre-initialized by the hoisted init block).
745
        if (!init_hoist_) {
19✔
746
            size_t num_producer_blocks = fusion_candidates_.size();
18✔
747
            FusionConsumerUpdateVisitor update_visitor(builder, fusion_candidates_, candidate_temps);
18✔
748
            update_visitor.dispatch_partial_sequence(*consumer_body_, num_producer_blocks, consumer_body_->size());
18✔
749
        } else {
18✔
750
            // Case 2: the hoisted init copy fully overwrites the accumulator before the
751
            // reduction reads it, so the original init producer map is redundant. Unlike
752
            // Case 1, the accumulator array stays live, so DCE would not reclaim it — remove
753
            // the producer explicitly (mirrors how ConsumerIntoProducer removes its loop).
754
            auto* parent = first_map_.get_parent();
1✔
755
            auto* parent_seq = dyn_cast<structured_control_flow::Sequence*>(parent);
1✔
756
            if (parent_seq != nullptr) {
1✔
757
                int idx = parent_seq->index(first_map_);
1✔
758
                if (idx >= 0) {
1✔
759
                    builder.remove_child(*parent_seq, static_cast<size_t>(idx));
1✔
760
                }
1✔
761
            }
1✔
762
        }
1✔
763

764
    } else {
19✔
765
        // ConsumerIntoProducer (Pattern 2): Inline consumer blocks into the producer's write body
766
        // Modify the producer block in-place to write to a temp scalar, add a writeback block
767
        // for the original array, then copy consumer blocks reading from the temp.
768

769
        std::vector<std::string> candidate_temps;
3✔
770
        auto& producer_dataflow = producer_block_->dataflow();
3✔
771

772
        for (size_t cand_idx = 0; cand_idx < fusion_candidates_.size(); ++cand_idx) {
6✔
773
            auto& candidate = fusion_candidates_[cand_idx];
3✔
774

775
            auto& container_type = sdfg.type(candidate.container);
3✔
776
            std::string temp_name = builder.find_new_name("_fused_tmp");
3✔
777
            types::Scalar tmp_type(container_type.primitive_type());
3✔
778
            builder.add_container(temp_name, tmp_type);
3✔
779
            candidate_temps.push_back(temp_name);
3✔
780

781
            // Step 1: Modify the original producer block to write to _fused_tmp
782
            data_flow::Subset original_write_subset;
3✔
783
            for (auto& node : producer_dataflow.nodes()) {
6✔
784
                auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
6✔
785
                if (access == nullptr || access->data() != candidate.container) continue;
6✔
786
                if (producer_dataflow.in_degree(*access) == 0) continue;
3✔
787

788
                // This is the write access node — save the original subset, then redirect
789
                for (auto& in_edge : producer_dataflow.in_edges(*access)) {
3✔
790
                    original_write_subset = in_edge.subset();
3✔
791
                    in_edge.set_subset({});
3✔
792
                    in_edge.set_base_type(tmp_type);
3✔
793
                }
3✔
794
                access->data(temp_name);
3✔
795
                break;
3✔
796
            }
3✔
797

798
            // Step 2: Add a writeback block: container[original_subset] = _fused_tmp
799
            auto& wb_block = builder.add_block_after(*producer_body_, *producer_block_);
3✔
800
            auto& wb_src = builder.add_access(wb_block, temp_name);
3✔
801
            auto& wb_dst = builder.add_access(wb_block, candidate.container);
3✔
802
            auto& wb_tasklet = builder.add_tasklet(wb_block, data_flow::TaskletCode::assign, "_out", {"_in"});
3✔
803
            builder.add_computational_memlet(wb_block, wb_src, wb_tasklet, "_in", {});
3✔
804
            builder.add_computational_memlet(wb_block, wb_tasklet, "_out", wb_dst, original_write_subset);
3✔
805

806
            // Step 3: Copy consumer blocks after the writeback block
807
            structured_control_flow::ControlFlowNode* last_inserted = &wb_block;
3✔
808

809
            for (size_t i = 0; i < consumer_body_->size(); ++i) {
6✔
810
                auto* consumer_block = dyn_cast<structured_control_flow::Block*>(&consumer_body_->at(i));
3✔
811
                if (consumer_block == nullptr) {
3✔
812
                    continue;
×
813
                }
×
814

815
                auto& consumer_dataflow = consumer_block->dataflow();
3✔
816

817
                // Check if this block reads from the fusion container
818
                bool reads_container = false;
3✔
819
                for (auto& node : consumer_dataflow.nodes()) {
11✔
820
                    auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
11✔
821
                    if (access != nullptr && access->data() == candidate.container &&
11✔
822
                        consumer_dataflow.out_degree(*access) > 0) {
11✔
823
                        reads_container = true;
3✔
824
                        break;
3✔
825
                    }
3✔
826
                }
11✔
827
                if (!reads_container) {
3✔
828
                    continue;
×
829
                }
×
830

831
                // Insert a new block after the last inserted block in the producer's body
832
                auto& new_block = builder.add_block_after(*producer_body_, *last_inserted);
3✔
833
                structured_control_flow::AssignmentBlock* init_assignment_block = nullptr;
3✔
834

835
                // Deep copy all nodes from consumer block
836
                std::unordered_map<const data_flow::DataFlowNode*, data_flow::DataFlowNode*> node_mapping;
3✔
837
                std::unordered_map<std::string, std::string> intermediate_renames;
3✔
838
                for (auto& node : consumer_dataflow.nodes()) {
11✔
839
                    node_mapping[&node] = &builder.copy_node(new_block, node);
11✔
840
                    auto* copied = node_mapping[&node];
11✔
841
                    if (auto* access_node = dynamic_cast<data_flow::AccessNode*>(copied)) {
11✔
842
                        if (access_node->data() == candidate.container) {
8✔
843
                            // Only rename read access nodes to temp; keep write access nodes
844
                            // pointing to the original container
845
                            if (consumer_dataflow.in_degree(node) == 0) {
4✔
846
                                access_node->data(temp_name);
3✔
847
                            }
3✔
848
                        } else if (consumer_dataflow.in_degree(node) > 0 && consumer_dataflow.out_degree(node) > 0 &&
4✔
849
                                   dynamic_cast<const types::Scalar*>(&sdfg.type(access_node->data())) != nullptr) {
4✔
850
                            // SSA Dataflow required to check for non-local use of the access node's container.
851
                            // Intermediate access node (e.g. from a prior BlockFusion): clone
852
                            // its container so each inlined copy gets its own private scalar
853
                            auto it = intermediate_renames.find(access_node->data());
×
854
                            if (it == intermediate_renames.end()) {
×
855
                                std::string fresh = builder.find_new_name(access_node->data());
×
856
                                builder.add_container(fresh, sdfg.type(access_node->data()));
×
857
                                intermediate_renames[access_node->data()] = fresh;
×
858
                            }
×
859
                            access_node->data(intermediate_renames[access_node->data()]);
×
860
                        }
×
861
                        if (access_node->data() == second_loop_.indvar()->get_name() &&
8✔
862
                            consumer_dataflow.in_degree(node) == 0) {
8✔
863
                            // Determine the new expression for the index variable of the second loop
864
                            symbolic::Expression new_expr = SymEngine::null;
×
865
                            for (auto& c : fusion_candidates_) {
×
866
                                for (auto& [sym, expr] : c.index_mappings) {
×
867
                                    if (symbolic::eq(sym, second_loop_.indvar())) {
×
868
                                        new_expr = expr;
×
869
                                        break;
×
870
                                    }
×
871
                                }
×
872
                                if (!new_expr.is_null()) {
×
873
                                    break;
×
874
                                }
×
875
                            }
×
876

877
                            if (new_expr.is_null() || symbolic::eq(new_expr, first_map_.indvar())) {
×
878
                                // Simple case: The new expression is simply the index variable of the first map
879
                                access_node->data(first_map_.indvar()->get_name());
×
880
                            } else {
×
881
                                // Complex case: Add an AssignmentBlock (if necessary) and store the
882
                                // shifted index into a new temporary variable with an assignment. Then, replace the
883
                                // index variable with the new temporary variable
884
                                if (!init_assignment_block) {
×
885
                                    init_assignment_block = &builder.add_assignments_at(*producer_body_, 0, {});
×
886
                                }
×
887
                                auto new_index_name = builder.find_new_name();
×
888
                                builder.add_container(
×
889
                                    new_index_name, builder.subject().type(first_map_.indvar()->get_name())
×
890
                                );
×
891
                                init_assignment_block->assignments().insert({symbolic::symbol(new_index_name), new_expr}
×
892
                                );
×
893
                                access_node->data(new_index_name);
×
894
                            }
×
895
                        }
×
896
                    }
8✔
897
                }
11✔
898

899
                // Add memlets with index substitution (consumer indvars → producer expressions)
900
                for (auto& edge : consumer_dataflow.edges()) {
8✔
901
                    auto& src_node = edge.src();
8✔
902
                    auto& dst_node = edge.dst();
8✔
903

904
                    const types::IType* base_type = &edge.base_type();
8✔
905
                    data_flow::Subset new_subset;
8✔
906
                    for (const auto& dim : edge.subset()) {
9✔
907
                        auto new_dim = dim;
9✔
908
                        for (const auto& [cvar, mapping] : candidate.index_mappings) {
13✔
909
                            new_dim = symbolic::subs(new_dim, cvar, mapping);
13✔
910
                        }
13✔
911
                        new_dim = symbolic::expand(new_dim);
9✔
912
                        new_subset.push_back(new_dim);
9✔
913
                    }
9✔
914

915
                    // For read edges from temp scalar, use empty subset
916
                    auto* src_access = dynamic_cast<data_flow::AccessNode*>(&src_node);
8✔
917
                    if (src_access != nullptr && src_access->data() == candidate.container &&
8✔
918
                        consumer_dataflow.in_degree(*src_access) == 0) {
8✔
919
                        new_subset.clear();
3✔
920
                        base_type = &tmp_type;
3✔
921
                    }
3✔
922

923
                    builder.add_memlet(
8✔
924
                        new_block,
8✔
925
                        *node_mapping[&src_node],
8✔
926
                        edge.src_conn(),
8✔
927
                        *node_mapping[&dst_node],
8✔
928
                        edge.dst_conn(),
8✔
929
                        new_subset,
8✔
930
                        *base_type,
8✔
931
                        edge.debug_info()
8✔
932
                    );
8✔
933
                }
8✔
934

935
                last_inserted = &new_block;
3✔
936
            }
3✔
937
        }
3✔
938

939
        // Remove the consumer loop
940
        auto* parent = second_loop_.get_parent();
3✔
941
        auto* parent_seq = dyn_cast<structured_control_flow::Sequence*>(parent);
3✔
942
        if (parent_seq != nullptr) {
3✔
943
            int idx = parent_seq->index(second_loop_);
3✔
944
            if (idx >= 0) {
3✔
945
                builder.remove_child(*parent_seq, static_cast<size_t>(idx));
3✔
946
            }
3✔
947
        }
3✔
948
    }
3✔
949

950
    analysis_manager.invalidate_all();
22✔
951
    applied_ = true;
22✔
952
}
22✔
953

954
void MapFusion::to_json(nlohmann::json& j) const {
1✔
955
    j["transformation_type"] = this->name();
1✔
956
    j["parameters"] = nlohmann::json::object();
1✔
957

958
    serializer::JSONSerializer ser_flat(false);
1✔
959
    j["subgraph"] = nlohmann::json::object();
1✔
960
    j["subgraph"]["0"] = nlohmann::json::object();
1✔
961
    ser_flat.serialize_node(j["subgraph"]["0"], first_map_);
1✔
962

963
    j["subgraph"]["1"] = nlohmann::json::object();
1✔
964
    ser_flat.serialize_node(j["subgraph"]["1"], second_loop_);
1✔
965
}
1✔
966

967
MapFusion MapFusion::from_json(builder::StructuredSDFGBuilder& builder, const nlohmann::json& desc) {
1✔
968
    auto first_map_id = desc["subgraph"]["0"]["element_id"].get<size_t>();
1✔
969
    auto second_loop_id = desc["subgraph"]["1"]["element_id"].get<size_t>();
1✔
970

971
    auto first_element = builder.find_element_by_id(first_map_id);
1✔
972
    auto second_element = builder.find_element_by_id(second_loop_id);
1✔
973

974
    if (first_element == nullptr) {
1✔
975
        throw InvalidTransformationDescriptionException("Element with ID " + std::to_string(first_map_id) + " not found.");
×
976
    }
×
977
    if (second_element == nullptr) {
1✔
978
        throw InvalidTransformationDescriptionException(
×
979
            "Element with ID " + std::to_string(second_loop_id) + " not found."
×
980
        );
×
981
    }
×
982

983
    auto* first_map = dyn_cast<structured_control_flow::Map*>(first_element);
1✔
984
    auto* second_loop = dyn_cast<structured_control_flow::StructuredLoop*>(second_element);
1✔
985

986
    if (first_map == nullptr) {
1✔
987
        throw InvalidTransformationDescriptionException(
×
988
            "Element with ID " + std::to_string(first_map_id) + " is not a Map."
×
989
        );
×
990
    }
×
991
    if (second_loop == nullptr) {
1✔
992
        throw InvalidTransformationDescriptionException(
×
993
            "Element with ID " + std::to_string(second_loop_id) + " is not a StructuredLoop."
×
994
        );
×
995
    }
×
996

997
    return MapFusion(*first_map, *second_loop);
1✔
998
}
1✔
999

NEW
1000
bool ::sdfg::transformations::FusionConsumerSubsetVisitor::visit(IfElse& node) {
×
NEW
1001
    for (int i = 0; i < node.size(); ++i) {
×
NEW
1002
        if (visit(node.at(i).first)) {
×
NEW
1003
            return true;
×
NEW
1004
        }
×
NEW
1005
    }
×
1006

NEW
1007
    return false;
×
NEW
1008
}
×
1009

1010
const std::unordered_map<std::string, std::vector<data_flow::Subset>>& FusionConsumerSubsetVisitor::
1011
    unique_subsets_per_container() {
26✔
1012
    return unique_subsets_per_container_;
26✔
1013
}
26✔
1014

1015
FusionConsumerUpdateVisitor::FusionConsumerUpdateVisitor(
1016
    builder::StructuredSDFGBuilder& builder,
1017
    const std::vector<passes::map_fusion::FusionRegCandidate>& fusion_candidates,
1018
    const std::vector<std::string>& candidate_temps
1019
)
1020
    : builder_(builder), fusion_candidates_(fusion_candidates), candidate_temps_(candidate_temps) {}
37✔
1021

1022
bool FusionConsumerUpdateVisitor::dispatch_partial_sequence(Sequence& node, size_t first, size_t end) {
37✔
1023
    for (int i = first; i < end; ++i) {
78✔
1024
        if (dispatch(node.at(i))) {
41✔
NEW
1025
            return true;
×
NEW
1026
        }
×
1027
    }
41✔
1028

1029
    return false;
37✔
1030
}
37✔
1031

1032
bool FusionConsumerUpdateVisitor::visit(sdfg::structured_control_flow::Block& block) {
41✔
1033
    auto& dataflow = block.dataflow();
41✔
1034

1035
    // Snapshot access nodes before mutation: adding new access nodes below
1036
    // would rehash dataflow.nodes_ and invalidate the range iterator.
1037
    std::vector<data_flow::AccessNode*> access_nodes;
41✔
1038
    for (auto& node : dataflow.nodes()) {
148✔
1039
        auto* an = dynamic_cast<data_flow::AccessNode*>(&node);
148✔
1040
        if (an != nullptr && dataflow.out_degree(*an) > 0) {
148✔
1041
            access_nodes.push_back(an);
60✔
1042
        }
60✔
1043
    }
148✔
1044

1045
    for (auto* access : access_nodes) {
60✔
1046
        std::string original_container = access->data();
60✔
1047

1048
        // Match each out-edge against a fusion candidate.
1049
        struct Match {
60✔
1050
            data_flow::Memlet* memlet;
60✔
1051
            size_t cand_idx;
60✔
1052
        };
60✔
1053
        std::vector<Match> matches;
60✔
1054
        for (auto& memlet : dataflow.out_edges(*access)) {
64✔
1055
            if (memlet.type() != data_flow::MemletType::Computational) {
64✔
NEW
1056
                continue;
×
NEW
1057
            }
×
1058
            const auto& memlet_subset = memlet.subset();
64✔
1059
            for (size_t cand_idx = 0; cand_idx < fusion_candidates_.size(); ++cand_idx) {
90✔
1060
                auto& candidate = fusion_candidates_[cand_idx];
73✔
1061
                if (original_container != candidate.container) {
73✔
1062
                    continue;
19✔
1063
                }
19✔
1064
                if (!candidate.integrated_rle) {
54✔
NEW
1065
                    continue;
×
NEW
1066
                }
×
1067
                if (memlet_subset.size() != candidate.consumer_subset.size()) {
54✔
NEW
1068
                    continue;
×
NEW
1069
                }
×
1070
                bool subset_matches = true;
54✔
1071
                for (size_t d = 0; d < memlet_subset.size(); ++d) {
109✔
1072
                    if (!symbolic::eq(memlet_subset[d], candidate.consumer_subset[d])) {
62✔
1073
                        subset_matches = false;
7✔
1074
                        break;
7✔
1075
                    }
7✔
1076
                }
62✔
1077
                if (subset_matches) {
54✔
1078
                    matches.push_back({&memlet, cand_idx});
47✔
1079
                    break;
47✔
1080
                }
47✔
1081
            }
54✔
1082
        }
64✔
1083
        if (matches.empty()) {
60✔
1084
            continue;
17✔
1085
        }
17✔
1086

1087
        // Group matches by candidate index.
1088
        std::unordered_set<size_t> distinct_cands;
43✔
1089
        for (auto& m : matches) {
47✔
1090
            distinct_cands.insert(m.cand_idx);
47✔
1091
        }
47✔
1092

1093
        if (distinct_cands.size() == 1) {
43✔
1094
            // Fast path: all matched out-edges resolve to the same candidate.
1095
            // Mutate the shared access node in place — this preserves the
1096
            // existing semantics for the single-read-per-container case.
1097
            size_t cand_idx = *distinct_cands.begin();
41✔
1098
            const auto& temp_name = candidate_temps_[cand_idx];
41✔
1099
            auto& temp_type = builder_.subject().type(temp_name);
41✔
1100

1101
            access->data(temp_name);
41✔
1102

1103
            for (auto& m : matches) {
43✔
1104
                m.memlet->set_subset({});
43✔
1105
                m.memlet->set_base_type(temp_type);
43✔
1106
            }
43✔
1107

1108
            for (auto& in_edge : dataflow.in_edges(*access)) {
41✔
NEW
1109
                in_edge.set_subset({});
×
NEW
1110
                in_edge.set_base_type(temp_type);
×
NEW
1111
            }
×
1112
        } else {
41✔
1113
            // Stencil-like case: a single access node feeds reads at
1114
            // multiple distinct subsets (e.g. T[j-1] and T[j+1] sharing
1115
            // one AccessNode). Each must be rewired to its own
1116
            // candidate-specific temp scalar — otherwise mutating
1117
            // `access->data()` once per candidate makes all reads
1118
            // collapse onto the last temp, e.g. T[j+1]-T[j] becomes
1119
            // tmp-tmp == 0.
1120
            //
1121
            // Fix: for each distinct candidate, create one fresh
1122
            // AccessNode for its temp scalar and redirect the matched
1123
            // edges from the shared access node to the fresh nodes.
1124
            struct PendingRedirect {
2✔
1125
                data_flow::DataFlowNode* dst;
2✔
1126
                std::string src_conn;
2✔
1127
                std::string dst_conn;
2✔
1128
                DebugInfo debug_info;
2✔
1129
                size_t cand_idx;
2✔
1130
                const data_flow::Memlet* memlet_to_remove;
2✔
1131
            };
2✔
1132
            std::vector<PendingRedirect> pending;
2✔
1133
            pending.reserve(matches.size());
2✔
1134
            for (auto& m : matches) {
4✔
1135
                pending.push_back(
4✔
1136
                    {&m.memlet->dst(),
4✔
1137
                     m.memlet->src_conn(),
4✔
1138
                     m.memlet->dst_conn(),
4✔
1139
                     m.memlet->debug_info(),
4✔
1140
                     m.cand_idx,
4✔
1141
                     m.memlet}
4✔
1142
                );
4✔
1143
            }
4✔
1144

1145
            std::unordered_map<size_t, data_flow::AccessNode*> per_cand_node;
2✔
1146
            for (auto& p : pending) {
4✔
1147
                auto it = per_cand_node.find(p.cand_idx);
4✔
1148
                if (it == per_cand_node.end()) {
4✔
1149
                    auto& fresh = builder_.add_access(block, candidate_temps_[p.cand_idx]);
4✔
1150
                    it = per_cand_node.emplace(p.cand_idx, &fresh).first;
4✔
1151
                }
4✔
1152
                auto& temp_type = builder_.subject().type(candidate_temps_[p.cand_idx]);
4✔
1153
                builder_.remove_memlet(block, *p.memlet_to_remove);
4✔
1154
                builder_.add_memlet(block, *it->second, p.src_conn, *p.dst, p.dst_conn, {}, temp_type, p.debug_info);
4✔
1155
            }
4✔
1156

1157
            // If the original shared access node now has no edges at all
1158
            // it is dangling and should be removed. Keep it if it still
1159
            // has out-edges (unmatched reads of the original container)
1160
            // or in-edges (writes to the original container).
1161
            if (dataflow.out_degree(*access) == 0 && dataflow.in_degree(*access) == 0) {
2✔
1162
                builder_.remove_node(block, *access);
2✔
1163
            }
2✔
1164
        }
2✔
1165
    }
43✔
1166
    return false;
41✔
1167
}
41✔
1168

NEW
1169
bool FusionConsumerUpdateVisitor::visit(sdfg::structured_control_flow::Sequence& node) {
×
NEW
1170
    for (int i = 0; i < node.size(); ++i) {
×
NEW
1171
        if (dispatch(node.at(i))) {
×
NEW
1172
            return true;
×
NEW
1173
        }
×
NEW
1174
    }
×
1175

NEW
1176
    return false;
×
NEW
1177
}
×
1178

NEW
1179
bool FusionConsumerUpdateVisitor::visit(IfElse& node) {
×
NEW
1180
    for (int i = 0; i < node.size(); ++i) {
×
NEW
1181
        if (visit(node.at(i).first)) {
×
NEW
1182
            return true;
×
NEW
1183
        }
×
NEW
1184
    }
×
1185

NEW
1186
    return false;
×
NEW
1187
}
×
1188

1189
bool ::sdfg::transformations::FusionConsumerSubsetVisitor::visit(sdfg::structured_control_flow::Sequence& node) {
64✔
1190
    for (int i = 0; i < node.size(); ++i) {
129✔
1191
        if (dispatch(node.at(i))) {
67✔
1192
            return true;
2✔
1193
        }
2✔
1194
    }
67✔
1195

1196
    return false;
62✔
1197
}
64✔
1198

1199
bool ::sdfg::transformations::FusionConsumerSubsetVisitor::visit(sdfg::structured_control_flow::Block& block) {
67✔
1200
    auto& dataflow = block.dataflow();
67✔
1201
    for (auto& node : dataflow.nodes()) {
240✔
1202
        auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
240✔
1203
        if (access == nullptr) {
240✔
1204
            continue;
72✔
1205
        }
72✔
1206
        auto& container = access->data();
168✔
1207

1208
        auto target_it = target_containers_.find(container);
168✔
1209
        if (target_it == target_containers_.end()) {
168✔
1210
            continue;
88✔
1211
        }
88✔
1212
        auto& producer_subset = *target_it->second;
80✔
1213
        auto& unique_subsets = unique_subsets_per_container_[container]; // Ensures entry exists
80✔
1214

1215
        // Skip write-only access nodes (consumer also writes the fusion container)
1216
        if (dataflow.in_degree(*access) > 0 && dataflow.out_degree(*access) == 0) {
80✔
1217
            continue;
9✔
1218
        }
9✔
1219
        if (dataflow.in_degree(*access) != 0 || dataflow.out_degree(*access) == 0) {
71✔
NEW
1220
            return abort();
×
NEW
1221
        }
×
1222

1223
        // Check all read memlets from this access
1224
        for (auto& memlet : dataflow.out_edges(*access)) {
75✔
1225
            if (memlet.type() != data_flow::MemletType::Computational) {
75✔
NEW
1226
                return abort();
×
NEW
1227
            }
×
1228

1229
            auto& consumer_subset = memlet.subset();
75✔
1230
            if (consumer_subset.size() != producer_subset.size()) {
75✔
1231
                return abort();
2✔
1232
            }
2✔
1233

1234
            // Check if this subset is already in unique_subsets
1235
            bool found = false;
73✔
1236
            for (const auto& existing : unique_subsets) {
73✔
1237
                if (existing.size() != consumer_subset.size()) continue;
14✔
1238
                bool match = true;
14✔
1239
                for (size_t d = 0; d < existing.size(); ++d) {
18✔
1240
                    if (!symbolic::eq(existing[d], consumer_subset[d])) {
14✔
1241
                        match = false;
10✔
1242
                        break;
10✔
1243
                    }
10✔
1244
                }
14✔
1245
                if (match) {
14✔
1246
                    found = true;
4✔
1247
                    break;
4✔
1248
                }
4✔
1249
            }
14✔
1250
            if (!found) {
73✔
1251
                unique_subsets.push_back(consumer_subset);
69✔
1252
            }
69✔
1253
        }
73✔
1254
    }
71✔
1255
    return false;
65✔
1256
}
67✔
1257

1258
::sdfg::transformations::FusionConsumerSubsetVisitor::FusionConsumerSubsetVisitor(std::unordered_map<
1259
                                                                                  std::string,
1260
                                                                                  const data_flow::Subset*>&
1261
                                                                                      target_containers)
1262
    : target_containers_(target_containers) {}
64✔
1263

1264
} // namespace transformations
1265
} // namespace sdfg
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