• 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

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/loop_fusion/loop_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::loop_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 allow_prod_into_cons
33
)
34
    : first_map_(first_map), second_loop_(second_loop), require_consecutive_(require_consecutive),
45✔
35
      allow_init_hoist_(allow_init_hoist), allow_prod_into_cons_(allow_prod_into_cons) {}
45✔
36

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

39
using passes::loop_fusion::LoopFusionByAccessWorker;
40

41
LoopFusionByAccessWorker::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_ = LoopFusionByAccessWorker::FusionDirection::ProducerIntoConsumer;
38✔
185
    } else if (!first_nested && second_nested) {
38✔
186
        // Pattern 2: Producer non-perfectly-nested, consumer perfectly nested
187
        direction_ = LoopFusionByAccessWorker::FusionDirection::ConsumerIntoProducer;
2✔
188
    } else {
2✔
189
        // Reverse Pattern 2: Producer perfectly nested, consumer non-perfectly-nested
190
        direction_ = LoopFusionByAccessWorker::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_ == LoopFusionByAccessWorker::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_ == LoopFusionByAccessWorker::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_ = LoopFusionByAccessWorker::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 (!allow_prod_into_cons_ && direction_ == LoopFusionByAccessWorker::FusionDirection::ProducerIntoConsumer) {
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_ == LoopFusionByAccessWorker::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_ == LoopFusionByAccessWorker::FusionDirection::ProducerIntoConsumer) {
39✔
487
                // Solve producer indvars in terms of consumer indvars
488
                mappings = passes::loop_fusion::LoopFusionByAccessWorker::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::loop_fusion::LoopFusionByAccessWorker::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_ == LoopFusionByAccessWorker::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()) {
7✔
784
                auto* access = dynamic_cast<data_flow::AccessNode*>(&node);
7✔
785
                if (access == nullptr || access->data() != candidate.container) continue;
7✔
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::loop_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