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

daisytuner / docc / 28806128926

06 Jul 2026 04:16PM UTC coverage: 62.96%. First build
28806128926

push

github

web-flow
New LibNodeExpansion pass (#740)

* Switched expansion "pipeline" to new LibNodeExpansionPass that can recursively expand using a LibNodeExpander impl.
- removed access to analysis_manager from expansion methods, as those would currently not be appropriately maintained between nodes
+ New expansion API handles more of the boilerplate code (checking for standalone, creating boundary access nodes, removing old elements)
* Also migrated sdfg-json-to-c.cpp to not using the expansion pipeline anymore
* toStr() for ReduceNodes and giving PyStructuredSDFG access to the output_dir for additional dumping
~ DotVisualizer : Fix on nested sequences.
+ DotVisualizer: visualize empty sequences
* DotVisualizer default-enabled show block/loop ids
 * while MathNodes still have an expand-method, the new infrastructure is based upon "Expander" classes. The MathNodeExpander just redirects to the method for now
 ~ Broadcast node still used old ptr-output semantics
 * updated tensor & blas node expand to new expand API, that handles more of the boilerplate code (checking, removing old nodes, creating standalone-replacement nodes)
 - removed Transpose Node. Was unused and on old ptr-semantics
 + StructuredSDFGBuilder.add_sequence_at, add_for_at, add_map_at
 * switched StructuredSDFGBuilder internally to use ptr of Assignments to express using default assignments when needed
 * updated tests to use the new expand_single_math_node helper function, instead of the method directly
 + pass and expand-single helper functions are ready to use other expanders
 + EinsumExpansionPass is basically the legacy ExpansionPass, only restricted to EinsumNodes. It still needs to run in a pipeline to ensure finding multiple EinsumNodes per block. This is temporary, because Einsum is the only node that already supported splitting a block, which the new expansion disallows, as it should handle it itself when needed (but that part is not yet implemented)

594 of 962 new or added lines in 31 files covered. (61.75%)

40554 of 64412 relevant lines covered (62.96%)

963.11 hits per line

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

79.62
/sdfg/src/data_flow/library_nodes/math/tensor/einsum_node.cpp
1
#include "sdfg/data_flow/library_nodes/math/tensor/einsum_node.h"
2

3
#include <cstddef>
4
#include <memory>
5
#include <nlohmann/json_fwd.hpp>
6
#include <sstream>
7
#include <string>
8
#include <unordered_map>
9
#include <unordered_set>
10
#include <vector>
11

12
#include "sdfg/analysis/analysis.h"
13
#include "sdfg/builder/structured_sdfg_builder.h"
14
#include "sdfg/data_flow/access_node.h"
15
#include "sdfg/data_flow/data_flow_graph.h"
16
#include "sdfg/data_flow/data_flow_node.h"
17
#include "sdfg/data_flow/library_node.h"
18
#include "sdfg/data_flow/library_nodes/math/math_node.h"
19
#include "sdfg/data_flow/memlet.h"
20
#include "sdfg/data_flow/tasklet.h"
21
#include "sdfg/element.h"
22
#include "sdfg/exceptions.h"
23
#include "sdfg/function.h"
24
#include "sdfg/graph/graph.h"
25
#include "sdfg/serializer/json_serializer.h"
26
#include "sdfg/structured_control_flow/block.h"
27
#include "sdfg/structured_control_flow/control_flow_node.h"
28
#include "sdfg/structured_control_flow/map.h"
29
#include "sdfg/structured_control_flow/sequence.h"
30
#include "sdfg/structured_control_flow/structured_loop.h"
31
#include "sdfg/symbolic/symbolic.h"
32
#include "sdfg/types/scalar.h"
33
#include "sdfg/types/type.h"
34
#include "symengine/symbol.h"
35

36
namespace sdfg {
37
namespace math {
38
namespace tensor {
39

40
EinsumNode::EinsumNode(
41
    size_t element_id,
42
    const DebugInfo& debug_info,
43
    const graph::Vertex vertex,
44
    data_flow::DataFlowGraph& parent,
45
    const std::vector<std::string>& inputs,
46
    const std::vector<EinsumDimension>& dims,
47
    const data_flow::Subset& out_indices,
48
    const std::vector<data_flow::Subset>& in_indices,
49
    bool rename_indvars
50
)
51
    : math::MathNode(
54✔
52
          element_id,
54✔
53
          debug_info,
54✔
54
          vertex,
54✔
55
          parent,
54✔
56
          LibraryNodeType_Einsum,
54✔
57
          {"__einsum_out"},
54✔
58
          inputs,
54✔
59
          data_flow::ImplementationType_NONE
54✔
60
      ),
54✔
61
      dims_(dims), out_indices_(out_indices), in_indices_(in_indices) {
54✔
62
    // Check list sizes
63
    if (inputs.size() != in_indices.size()) {
54✔
64
        throw InvalidSDFGException("EinsumNode: Number of input containers != number of input indices");
×
65
    }
×
66

67
    // Rename indvars to internal symbols (only for fresh construction, not clone/deserialize)
68
    if (rename_indvars) {
54✔
69
        // Build mapping from original indvars to internal symbols
70
        // Format: _einsum_node_{element_id}_{original_indvar_name}
71
        std::string prefix = "_einsum_node_" + std::to_string(element_id) + "_";
40✔
72
        std::vector<std::pair<symbolic::Symbol, symbolic::Symbol>> indvar_renames;
40✔
73
        for (const auto& dim : this->dims_) {
40✔
74
            auto old_indvar = dim.indvar;
22✔
75
            auto old_name = SymEngine::rcp_static_cast<const SymEngine::Symbol>(old_indvar)->get_name();
22✔
76
            auto new_indvar = symbolic::symbol(prefix + old_name);
22✔
77
            indvar_renames.push_back({old_indvar, new_indvar});
22✔
78
        }
22✔
79

80
        // Apply all substitutions
81
        for (size_t idx = 0; idx < indvar_renames.size(); idx++) {
62✔
82
            auto old_indvar = indvar_renames[idx].first;
22✔
83
            auto new_indvar = indvar_renames[idx].second;
22✔
84

85
            // Replace in all dims' init, bound, and indvar
86
            for (auto& d : this->dims_) {
56✔
87
                if (symbolic::eq(d.indvar, old_indvar)) {
56✔
88
                    d.indvar = new_indvar;
22✔
89
                }
22✔
90
                d.init = symbolic::subs(d.init, old_indvar, new_indvar);
56✔
91
                d.bound = symbolic::subs(d.bound, old_indvar, new_indvar);
56✔
92
            }
56✔
93

94
            // Replace in out_indices
95
            for (size_t i = 0; i < this->out_indices_.size(); i++) {
57✔
96
                this->out_indices_[i] = symbolic::subs(this->out_indices_[i], old_indvar, new_indvar);
35✔
97
            }
35✔
98

99
            // Replace in in_indices
100
            for (size_t i = 0; i < this->in_indices_.size(); i++) {
62✔
101
                for (size_t j = 0; j < this->in_indices_[i].size(); j++) {
111✔
102
                    this->in_indices_[i][j] = symbolic::subs(this->in_indices_[i][j], old_indvar, new_indvar);
71✔
103
                }
71✔
104
            }
40✔
105
        }
22✔
106
    }
40✔
107

108
    // Append output at the end
109
    this->inputs_.push_back("__einsum_out");
54✔
110
    this->in_indices_.push_back(this->out_indices_);
54✔
111
}
54✔
112

113
const std::vector<EinsumDimension>& EinsumNode::dims() const { return this->dims_; }
165✔
114

115
const EinsumDimension& EinsumNode::dim(size_t index) const { return this->dims_.at(index); }
7✔
116

117
const symbolic::Symbol& EinsumNode::indvar(size_t index) const { return this->dims_.at(index).indvar; }
127✔
118

119
const symbolic::Expression& EinsumNode::init(size_t index) const { return this->dims_.at(index).init; }
40✔
120

121
const symbolic::Expression& EinsumNode::bound(size_t index) const { return this->dims_.at(index).bound; }
58✔
122

123
const data_flow::Subset& EinsumNode::out_indices() const { return this->out_indices_; }
137✔
124

125
const symbolic::Expression& EinsumNode::out_index(size_t index) const { return this->out_indices_.at(index); }
37✔
126

127
const std::vector<data_flow::Subset>& EinsumNode::in_indices() const { return this->in_indices_; }
77✔
128

129
const data_flow::Subset& EinsumNode::in_indices(size_t index) const { return this->in_indices_.at(index); }
96✔
130

131
const symbolic::Expression& EinsumNode::in_index(size_t index1, size_t index2) const {
82✔
132
    return this->in_indices_.at(index1).at(index2);
82✔
133
}
82✔
134

135
symbolic::SymbolSet EinsumNode::internal_symbols() const {
4✔
136
    symbolic::SymbolSet result;
4✔
137
    for (auto& dim : this->dims()) {
9✔
138
        result.insert(dim.indvar);
9✔
139
    }
9✔
140
    return result;
4✔
141
}
4✔
142

143
passes::LibNodeExpander::ExpandOutcome EinsumNode::
NEW
144
    expand(passes::LibNodeExpander::ExpandContext& context, structured_control_flow::Block& block) {
×
NEW
145
    return context.unapplicable();
×
NEW
146
}
×
147

148
bool EinsumNode::expand(builder::StructuredSDFGBuilder& builder, analysis::AnalysisManager& analysis_manager) {
6✔
149
    // Get data flow graph and block
150
    auto& dfg = this->get_parent();
6✔
151
    auto* block = dynamic_cast<structured_control_flow::Block*>(dfg.get_parent());
6✔
152
    if (!block) {
6✔
153
        return false;
×
154
    }
×
155

156
    // Get parent sequence
157
    auto* sequence = dynamic_cast<structured_control_flow::Sequence*>(block->get_parent());
6✔
158
    if (!sequence) {
6✔
159
        return false;
×
160
    }
×
161

162
    // Create block after this block
163
    auto& block_after = builder.add_block_after(*sequence, *block, {}, block->debug_info());
6✔
164

165
    // Collect and transfer nodes after the EinsumNode
166
    bool before = true;
6✔
167
    std::unordered_map<data_flow::DataFlowNode*, data_flow::DataFlowNode*> nodes_after;
6✔
168
    for (auto* node : dfg.topological_sort()) {
34✔
169
        if (before) {
34✔
170
            if (node == this) {
23✔
171
                before = false;
6✔
172
            }
6✔
173
            continue;
23✔
174
        }
23✔
175
        data_flow::DataFlowNode* node_after = nullptr;
11✔
176
        if (auto* constant_node = dynamic_cast<data_flow::ConstantNode*>(node)) {
11✔
177
            node_after =
×
178
                &builder
×
179
                     .add_constant(block_after, constant_node->data(), constant_node->type(), constant_node->debug_info());
×
180
        } else if (auto* access_node = dynamic_cast<data_flow::AccessNode*>(node)) {
11✔
181
            if (dfg.out_degree(*access_node) == 0 && dfg.in_degree(*access_node) == 1 &&
9✔
182
                &(*dfg.in_edges(*access_node).begin()).src() == this) {
9✔
183
                continue;
5✔
184
            }
5✔
185
            node_after = &builder.add_access(block_after, access_node->data(), access_node->debug_info());
4✔
186
        } else if (auto* code_node = dynamic_cast<data_flow::CodeNode*>(node)) {
4✔
187
            node_after = &builder.copy_node(block_after, *code_node);
2✔
188
        } else {
2✔
189
            return false;
×
190
        }
×
191
        nodes_after.insert({node, node_after});
6✔
192
        if (dynamic_cast<data_flow::Tasklet*>(node) || dynamic_cast<data_flow::LibraryNode*>(node)) {
6✔
193
            for (auto& iedge : dfg.in_edges(*node)) {
3✔
194
                if (!nodes_after.contains(&iedge.src())) {
3✔
195
                    if (auto* constant_node = dynamic_cast<data_flow::ConstantNode*>(&iedge.src())) {
×
196
                        nodes_after.insert(
×
197
                            {constant_node,
×
198
                             &builder.add_constant(
×
199
                                 block_after, constant_node->data(), constant_node->type(), constant_node->debug_info()
×
200
                             )}
×
201
                        );
×
202
                    } else if (auto* access_node = dynamic_cast<data_flow::AccessNode*>(&iedge.src())) {
×
203
                        nodes_after.insert(
×
204
                            {access_node,
×
205
                             &builder.add_access(block_after, access_node->data(), access_node->debug_info())}
×
206
                        );
×
207
                    } else {
×
208
                        return false;
×
209
                    }
×
210
                }
×
211
            }
3✔
212
        }
2✔
213
    }
6✔
214

215
    // Transfer memlets after the EinsumNode
216
    for (auto& edge : dfg.edges()) {
28✔
217
        if (!nodes_after.contains(&edge.src()) || !nodes_after.contains(&edge.dst())) {
28✔
218
            continue;
23✔
219
        }
23✔
220
        builder.add_memlet(
5✔
221
            block_after,
5✔
222
            *nodes_after[&edge.src()],
5✔
223
            edge.src_conn(),
5✔
224
            *nodes_after[&edge.dst()],
5✔
225
            edge.dst_conn(),
5✔
226
            edge.subset(),
5✔
227
            edge.base_type(),
5✔
228
            edge.debug_info()
5✔
229
        );
5✔
230
    }
5✔
231

232
    // Delete transferred data flow in the original block
233
    std::unordered_set<data_flow::Memlet*> edges_for_removal;
6✔
234
    for (auto& edge : dfg.edges()) {
28✔
235
        if (nodes_after.contains(&edge.src()) && nodes_after.contains(&edge.dst())) {
28✔
236
            edges_for_removal.insert(&edge);
5✔
237
        }
5✔
238
    }
28✔
239
    for (auto* edge : edges_for_removal) {
6✔
240
        builder.remove_memlet(*block, *edge);
5✔
241
    }
5✔
242
    std::unordered_set<data_flow::DataFlowNode*> nodes_for_removal;
6✔
243
    for (auto& node : dfg.nodes()) {
34✔
244
        if (dfg.in_degree(node) == 0 && dfg.out_degree(node) == 0) {
34✔
245
            nodes_for_removal.insert(&node);
5✔
246
        }
5✔
247
    }
34✔
248
    for (auto* node : nodes_for_removal) {
6✔
249
        builder.remove_node(*block, *node);
5✔
250
    }
5✔
251

252
    // Add containers for loop induction variables (symbols already renamed in constructor)
253
    for (size_t i = 0; i < this->dims().size(); i++) {
17✔
254
        auto indvar = this->indvar(i);
11✔
255
        auto indvar_name = SymEngine::rcp_static_cast<const SymEngine::Symbol>(indvar)->get_name();
11✔
256
        if (builder.subject().exists(indvar_name)) {
11✔
257
            continue;
5✔
258
        }
5✔
259
        builder.add_container(indvar_name, types::Scalar(types::PrimitiveType::Int64));
6✔
260
    }
6✔
261

262
    // Add loops
263
    structured_control_flow::Sequence* current_sequence = nullptr;
6✔
264
    bool map = true;
6✔
265
    for (size_t i = 0; i < this->dims().size(); i++) {
17✔
266
        if (map) {
11✔
267
            if (i >= this->out_indices().size() || !symbolic::uses(this->out_index(i), this->indvar(i))) {
11✔
268
                map = false;
6✔
269
            } else {
6✔
270
                for (size_t j = 0; j < i; j++) {
7✔
271
                    if (symbolic::uses(this->init(i), this->indvar(j)) ||
2✔
272
                        symbolic::uses(this->bound(i), this->indvar(j))) {
2✔
273
                        map = false;
×
274
                        break;
×
275
                    }
×
276
                }
2✔
277
            }
5✔
278
        }
11✔
279
        auto indvar = this->indvar(i);
11✔
280
        auto condition = symbolic::Lt(indvar, this->bound(i));
11✔
281
        auto init = this->init(i);
11✔
282
        auto update = symbolic::add(indvar, symbolic::one());
11✔
283
        if (current_sequence) {
11✔
284
            structured_control_flow::StructuredLoop* loop;
5✔
285
            if (map) {
5✔
286
                loop = &builder.add_map(
2✔
287
                    *current_sequence,
2✔
288
                    indvar,
2✔
289
                    condition,
2✔
290
                    init,
2✔
291
                    update,
2✔
292
                    ScheduleType_Sequential::create(),
2✔
293
                    {},
2✔
294
                    this->debug_info()
2✔
295
                );
2✔
296
            } else {
3✔
297
                loop = &builder.add_for(*current_sequence, indvar, condition, init, update, {}, this->debug_info());
3✔
298
            }
3✔
299
            current_sequence = &loop->root();
5✔
300
        } else {
6✔
301
            structured_control_flow::StructuredLoop* loop;
6✔
302
            if (map) {
6✔
303
                loop = &builder.add_map_after(
3✔
304
                    *sequence,
3✔
305
                    *block,
3✔
306
                    indvar,
3✔
307
                    condition,
3✔
308
                    init,
3✔
309
                    update,
3✔
310
                    ScheduleType_Sequential::create(),
3✔
311
                    {},
3✔
312
                    this->debug_info()
3✔
313
                );
3✔
314
            } else {
3✔
315
                loop =
3✔
316
                    &builder.add_for_after(*sequence, *block, indvar, condition, init, update, {}, this->debug_info());
3✔
317
            }
3✔
318
            current_sequence = &loop->root();
6✔
319
        }
6✔
320
    }
11✔
321

322
    // Add new block
323
    structured_control_flow::Block* new_block;
6✔
324
    if (current_sequence) {
6✔
325
        new_block = &builder.add_block(*current_sequence);
6✔
326
    } else {
6✔
327
        new_block = &builder.add_block_after(*sequence, *block, {}, this->debug_info());
×
328
    }
×
329

330
    // Transfer the access nodes of the EinsumNode
331
    std::unordered_map<std::string, data_flow::AccessNode*> new_in_accesses;
6✔
332
    std::unordered_map<std::string, const types::IType&> in_types;
6✔
333
    for (auto& iedge : dfg.in_edges(*this)) {
15✔
334
        in_types.insert({iedge.dst_conn(), iedge.base_type()});
15✔
335
        if (auto* constant_node = dynamic_cast<data_flow::ConstantNode*>(&iedge.src())) {
15✔
336
            new_in_accesses.insert(
×
337
                {iedge.dst_conn(),
×
338
                 &builder
×
339
                      .add_constant(*new_block, constant_node->data(), constant_node->type(), constant_node->debug_info())
×
340
                }
×
341
            );
×
342
        } else if (auto* access_node = dynamic_cast<data_flow::AccessNode*>(&iedge.src())) {
15✔
343
            data_flow::AccessNode* new_access_node = nullptr;
15✔
344
            for (auto [conn, other_access_node] : new_in_accesses) {
15✔
345
                if (access_node->data() == other_access_node->data()) {
13✔
346
                    new_access_node = other_access_node;
×
347
                    break;
×
348
                }
×
349
            }
13✔
350
            if (!new_access_node) {
15✔
351
                new_access_node = &builder.add_access(*new_block, access_node->data(), access_node->debug_info());
15✔
352
            }
15✔
353
            new_in_accesses.insert({iedge.dst_conn(), new_access_node});
15✔
354
        } else {
15✔
355
            return false;
×
356
        }
×
357
    }
15✔
358
    data_flow::AccessNode* new_out_access;
6✔
359
    const types::IType* out_type;
6✔
360
    {
6✔
361
        auto& oedge = *dfg.out_edges(*this).begin();
6✔
362
        out_type = &oedge.base_type();
6✔
363
        if (auto* access_node = dynamic_cast<data_flow::AccessNode*>(&oedge.dst())) {
6✔
364
            new_out_access = &builder.add_access(*new_block, access_node->data(), access_node->debug_info());
6✔
365
        } else {
6✔
366
            return false;
×
367
        }
×
368
    }
6✔
369

370
    // Add computations to the block
371
    if (this->inputs().size() == 1) {
6✔
372
        auto& tasklet =
×
373
            builder.add_tasklet(*new_block, data_flow::TaskletCode::assign, {"_out"}, {"_in0"}, this->debug_info());
×
374
        builder.add_memlet(
×
375
            *new_block,
×
376
            *new_in_accesses.at(this->input(0)),
×
377
            "void",
×
378
            tasklet,
×
379
            "_in0",
×
380
            this->in_indices(0),
×
381
            in_types.at(this->input(0)),
×
382
            this->debug_info()
×
383
        );
×
384
        builder.add_memlet(
×
385
            *new_block, tasklet, "_out", *new_out_access, "void", this->out_indices(), *out_type, this->debug_info()
×
386
        );
×
387
    } else if (this->inputs().size() == 2) {
6✔
388
        auto& tasklet =
4✔
389
            builder
4✔
390
                .add_tasklet(*new_block, data_flow::TaskletCode::fp_add, {"_out"}, {"_in0", "_in1"}, this->debug_info());
4✔
391
        builder.add_memlet(
4✔
392
            *new_block,
4✔
393
            *new_in_accesses.at(this->input(0)),
4✔
394
            "void",
4✔
395
            tasklet,
4✔
396
            "_in0",
4✔
397
            this->in_indices(0),
4✔
398
            in_types.at(this->input(0)),
4✔
399
            this->debug_info()
4✔
400
        );
4✔
401
        builder.add_memlet(
4✔
402
            *new_block,
4✔
403
            *new_in_accesses.at(this->input(1)),
4✔
404
            "void",
4✔
405
            tasklet,
4✔
406
            "_in1",
4✔
407
            this->in_indices(1),
4✔
408
            in_types.at(this->input(1)),
4✔
409
            this->debug_info()
4✔
410
        );
4✔
411
        builder.add_memlet(
4✔
412
            *new_block, tasklet, "_out", *new_out_access, "void", this->out_indices(), *out_type, this->debug_info()
4✔
413
        );
4✔
414
    } else {
4✔
415
        // Build a mapping from original connector names to internal names and indices
416
        std::unordered_map<std::string, data_flow::Subset> in_indices;
2✔
417
        std::unordered_map<std::string, std::string> conn_to_internal;
2✔
418
        for (size_t i = 0; i < this->inputs().size(); i++) {
9✔
419
            in_indices.insert({this->input(i), this->in_indices(i)});
7✔
420
            conn_to_internal.insert({this->input(i), "_in" + std::to_string(i)});
7✔
421
        }
7✔
422
        long long inp;
2✔
423
        for (inp = 0; inp < (long long) this->inputs().size() - 3; inp++) {
3✔
424
            auto tmp = builder.find_new_name();
1✔
425
            auto& tmp_type = builder.add_container(tmp, types::Scalar(in_types.at(this->input(inp)).primitive_type()));
1✔
426
            auto& tmp_access = builder.add_access(*new_block, tmp);
1✔
427
            std::string int_conn0 = conn_to_internal.at(this->input(inp));
1✔
428
            std::string int_conn1 = conn_to_internal.at(this->input(inp + 1));
1✔
429
            auto& tasklet = builder.add_tasklet(
1✔
430
                *new_block, data_flow::TaskletCode::fp_mul, {"_out"}, {int_conn0, int_conn1}, this->debug_info()
1✔
431
            );
1✔
432
            builder.add_memlet(
1✔
433
                *new_block,
1✔
434
                *new_in_accesses.at(this->input(inp)),
1✔
435
                "void",
1✔
436
                tasklet,
1✔
437
                int_conn0,
1✔
438
                in_indices.at(this->input(inp)),
1✔
439
                in_types.at(this->input(inp)),
1✔
440
                this->debug_info()
1✔
441
            );
1✔
442
            builder.add_memlet(
1✔
443
                *new_block,
1✔
444
                *new_in_accesses.at(this->input(inp + 1)),
1✔
445
                "void",
1✔
446
                tasklet,
1✔
447
                int_conn1,
1✔
448
                in_indices.at(this->input(inp + 1)),
1✔
449
                in_types.at(this->input(inp + 1)),
1✔
450
                this->debug_info()
1✔
451
            );
1✔
452
            builder.add_memlet(*new_block, tasklet, "_out", tmp_access, "void", {}, tmp_type, this->debug_info());
1✔
453
            new_in_accesses[this->input(inp + 1)] = &tmp_access;
1✔
454
            in_indices[this->input(inp + 1)].clear();
1✔
455
            in_types.erase(this->input(inp + 1));
1✔
456
            in_types.insert({this->input(inp + 1), tmp_type});
1✔
457
        }
1✔
458
        std::string int_conn0 = conn_to_internal.at(this->input(inp));
2✔
459
        std::string int_conn1 = conn_to_internal.at(this->input(inp + 1));
2✔
460
        std::string int_conn2 = conn_to_internal.at(this->input(inp + 2));
2✔
461
        auto& tasklet = builder.add_tasklet(
2✔
462
            *new_block, data_flow::TaskletCode::fp_fma, {"_out"}, {int_conn0, int_conn1, int_conn2}, this->debug_info()
2✔
463
        );
2✔
464
        builder.add_memlet(
2✔
465
            *new_block,
2✔
466
            *new_in_accesses.at(this->input(inp)),
2✔
467
            "void",
2✔
468
            tasklet,
2✔
469
            int_conn0,
2✔
470
            in_indices.at(this->input(inp)),
2✔
471
            in_types.at(this->input(inp)),
2✔
472
            this->debug_info()
2✔
473
        );
2✔
474
        builder.add_memlet(
2✔
475
            *new_block,
2✔
476
            *new_in_accesses.at(this->input(inp + 1)),
2✔
477
            "void",
2✔
478
            tasklet,
2✔
479
            int_conn1,
2✔
480
            in_indices.at(this->input(inp + 1)),
2✔
481
            in_types.at(this->input(inp + 1)),
2✔
482
            this->debug_info()
2✔
483
        );
2✔
484
        builder.add_memlet(
2✔
485
            *new_block,
2✔
486
            *new_in_accesses.at(this->input(inp + 2)),
2✔
487
            "void",
2✔
488
            tasklet,
2✔
489
            int_conn2,
2✔
490
            in_indices.at(this->input(inp + 2)),
2✔
491
            in_types.at(this->input(inp + 2)),
2✔
492
            this->debug_info()
2✔
493
        );
2✔
494
        builder.add_memlet(
2✔
495
            *new_block, tasklet, "_out", *new_out_access, "void", this->out_indices(), *out_type, this->debug_info()
2✔
496
        );
2✔
497
    }
2✔
498

499
    // Remove EinsumNode and its access nodes and memlets
500
    std::unordered_set<data_flow::AccessNode*> old_accesses;
6✔
501
    while (dfg.in_edges(*this).begin() != dfg.in_edges(*this).end()) {
21✔
502
        auto& iedge = *dfg.in_edges(*this).begin();
15✔
503
        old_accesses.insert(dynamic_cast<data_flow::AccessNode*>(&iedge.src()));
15✔
504
        builder.remove_memlet(*block, iedge);
15✔
505
    }
15✔
506
    while (dfg.out_edges(*this).begin() != dfg.out_edges(*this).end()) {
12✔
507
        auto& oedge = *dfg.out_edges(*this).begin();
6✔
508
        old_accesses.insert(dynamic_cast<data_flow::AccessNode*>(&oedge.dst()));
6✔
509
        builder.remove_memlet(*block, oedge);
6✔
510
    }
6✔
511
    for (auto* old_access : old_accesses) {
21✔
512
        if (dfg.in_degree(*old_access) == 0 && dfg.out_degree(*old_access) == 0) {
21✔
513
            builder.remove_node(*block, *old_access);
20✔
514
        }
20✔
515
    }
21✔
516
    builder.remove_node(*block, *this);
6✔
517

518
    // Remove block before loops if empty
519
    size_t block_index = sequence->index(*block);
6✔
520
    if (dfg.nodes().size() == 0 && sequence->at(block_index).second.empty()) {
6✔
521
        builder.remove_child(*sequence, sequence->index(*block));
5✔
522
    }
5✔
523

524
    // Remove block after loops if empty
525
    if (block_after.dataflow().nodes().size() == 0) {
6✔
526
        builder.remove_child(*sequence, sequence->index(block_after));
5✔
527
    }
5✔
528

529
    return true;
6✔
530
}
6✔
531

532
symbolic::SymbolSet EinsumNode::symbols() const {
4✔
533
    symbolic::SymbolSet result;
4✔
534
    symbolic::SymbolSet internal = this->internal_symbols();
4✔
535

536
    // Collect only external symbols from bounds and init expressions
537
    for (auto& dim : this->dims()) {
9✔
538
        for (auto& symbol : symbolic::atoms(dim.init)) {
9✔
539
            if (!internal.count(symbol)) {
×
540
                result.insert(symbol);
×
541
            }
×
542
        }
×
543
        for (auto& symbol : symbolic::atoms(dim.bound)) {
9✔
544
            if (!internal.count(symbol)) {
9✔
545
                result.insert(symbol);
9✔
546
            }
9✔
547
        }
9✔
548
    }
9✔
549

550
    // Note: indices only contain internal indvars, so skip them
551

552
    return result;
4✔
553
}
4✔
554

555
void EinsumNode::replace(const symbolic::Expression old_expression, const symbolic::Expression new_expression) {
×
556
    // Skip if old_expression is an internal symbol (indvar)
557
    for (auto& dim : this->dims()) {
×
558
        if (symbolic::eq(dim.indvar, old_expression)) {
×
559
            return; // Internal symbol - do not replace
×
560
        }
×
561
    }
×
562

563
    // Only replace external symbols in bounds/init expressions
564
    for (auto& dim : this->dims_) {
×
565
        dim.init = symbolic::subs(dim.init, old_expression, new_expression);
×
566
        dim.bound = symbolic::subs(dim.bound, old_expression, new_expression);
×
567
    }
×
568

569
    // Note: indices only contain internal indvars, so no substitution needed
570
}
×
571

572
void EinsumNode::replace(const symbolic::ExpressionMapping& replacements) {
×
573
    // Filter out replacements whose key is an internal symbol (indvar)
574
    symbolic::ExpressionMapping filtered;
×
575
    for (auto& pair : replacements) {
×
576
        bool is_internal = false;
×
577
        for (auto& dim : this->dims_) {
×
578
            if (symbolic::eq(dim.indvar, pair.first)) {
×
579
                is_internal = true;
×
580
                break;
×
581
            }
×
582
        }
×
583
        if (!is_internal) {
×
584
            filtered[pair.first] = pair.second;
×
585
        }
×
586
    }
×
587

588
    if (filtered.empty()) {
×
589
        return;
×
590
    }
×
591

592
    // Only replace external symbols in bounds/init expressions
593
    for (auto& dim : this->dims_) {
×
594
        dim.init = symbolic::subs(dim.init, filtered);
×
595
        dim.bound = symbolic::subs(dim.bound, filtered);
×
596
    }
×
597

598
    // Note: indices only contain internal indvars, so no substitution needed
599
}
×
600

601
std::string EinsumNode::toStr() const {
4✔
602
    std::stringstream stream;
4✔
603

604
    stream << this->output(0);
4✔
605
    for (auto& index : this->out_indices()) {
5✔
606
        stream << "[" << index->__str__() << "]";
5✔
607
    }
5✔
608
    stream << " = ";
4✔
609
    size_t num_inputs = this->inputs().size();
4✔
610
    if (num_inputs > 1) {
4✔
611
        for (size_t i = 0; i < num_inputs - 1; i++) {
10✔
612
            if (i > 0) {
6✔
613
                stream << " * ";
2✔
614
            }
2✔
615
            stream << this->input(i);
6✔
616
            for (auto& index : this->in_indices(i)) {
11✔
617
                stream << "[" << index->__str__() << "]";
11✔
618
            }
11✔
619
        }
6✔
620
        stream << " + ";
4✔
621
    }
4✔
622
    stream << this->input(num_inputs - 1);
4✔
623
    for (auto& index : this->in_indices(num_inputs - 1)) {
5✔
624
        stream << "[" << index->__str__() << "]";
5✔
625
    }
5✔
626

627
    for (auto& dim : this->dims()) {
9✔
628
        stream << " for " << dim.indvar->__str__() << " = " << dim.init->__str__() << " : " << dim.bound->__str__();
9✔
629
    }
9✔
630

631
    return stream.str();
4✔
632
}
4✔
633

634
symbolic::Expression EinsumNode::flop() const {
4✔
635
    symbolic::SymbolMap dim_map;
4✔
636
    symbolic::Expression result = symbolic::one();
4✔
637

638
    for (size_t i = 0; i < this->dims().size(); i++) {
13✔
639
        symbolic::Expression dim_expr = symbolic::sub(this->bound(i), this->init(i));
9✔
640
        for (size_t j = 0; j < i; j++) {
16✔
641
            for (auto& symbol : symbolic::atoms(dim_expr)) {
7✔
642
                if (symbolic::eq(symbol, this->indvar(j))) {
7✔
643
                    dim_expr =
×
644
                        symbolic::subs(dim_expr, symbol, symbolic::div(dim_map.at(symbol), symbolic::integer(2)));
×
645
                }
×
646
            }
7✔
647
        }
7✔
648
        dim_map.insert({this->indvar(i), dim_expr});
9✔
649
        result = symbolic::mul(result, dim_expr);
9✔
650
    }
9✔
651

652
    return symbolic::mul(result, symbolic::integer(this->inputs().size() - 1));
4✔
653
}
4✔
654

655
std::unique_ptr<data_flow::DataFlowNode> EinsumNode::
656
    clone(size_t element_id, const graph::Vertex vertex, data_flow::DataFlowGraph& parent) const {
×
657
    return std::make_unique<EinsumNode>(
×
658
        element_id,
×
659
        this->debug_info(),
×
660
        vertex,
×
661
        parent,
×
662
        std::vector<std::string>(this->inputs().begin(), this->inputs().end() - 1),
×
663
        this->dims(),
×
664
        this->out_indices(),
×
665
        std::vector<data_flow::Subset>(this->in_indices().begin(), this->in_indices().end() - 1),
×
666
        false // skip renaming - already internal symbols
×
667
    );
×
668
}
×
669

670
void EinsumNode::validate(const Function& function) const {
22✔
671
    // Check inputs
672
    size_t inputs_size = this->inputs().size();
22✔
673
    if (inputs_size == 0) {
22✔
674
        throw InvalidSDFGException("EinsumNode: Inputs of EinsumNode must not be empty");
×
675
    }
×
676
    for (size_t i = 0; i < inputs_size - 1; i++) {
60✔
677
        if (this->input(i) == "__einsum_out") {
38✔
678
            throw InvalidSDFGException("EinsumNode: Input '__einsum_out' at wrong position");
×
679
        }
×
680
    }
38✔
681
    if (this->input(inputs_size - 1) != "__einsum_out") {
22✔
682
        throw InvalidSDFGException("EinsumNode: Last input of EinsumNode must be '__einsum_out'");
×
683
    }
×
684

685
    // Check last in indices
686
    if (this->out_indices().size() != this->in_indices(inputs_size - 1).size()) {
22✔
687
        throw InvalidSDFGException("EinsumNode: Out indices and last in indices have different sizes");
×
688
    }
×
689
    for (size_t i = 0; i < this->out_indices().size(); i++) {
40✔
690
        if (!symbolic::eq(this->out_index(i), this->in_index(inputs_size - 1, i))) {
18✔
691
            throw InvalidSDFGException("EinsumNode: Out indices and last in indices do not match");
×
692
        }
×
693
    }
18✔
694

695
    // Check input containers
696
    auto& dfg = this->get_parent();
22✔
697
    auto& oedge = *dfg.out_edges(*this).begin();
22✔
698
    std::string out_container = dynamic_cast<const data_flow::AccessNode&>(oedge.dst()).data();
22✔
699
    for (auto& iedge : dfg.in_edges(*this)) {
60✔
700
        auto& src = dynamic_cast<const data_flow::AccessNode&>(iedge.src());
60✔
701
        if (src.data() != out_container && iedge.dst_conn() == "__einsum_out") {
60✔
702
            throw InvalidSDFGException("EinsumNode: Out container must occur as a summation in the inputs");
×
703
        }
×
704
    }
60✔
705

706
    // Check if dimensions index variables occur at least once as in/out indices
707
    for (size_t i = 0; i < this->dims().size(); i++) {
45✔
708
        bool unused = true;
23✔
709
        for (auto& index : this->out_indices()) {
30✔
710
            for (auto& symbol : symbolic::atoms(index)) {
30✔
711
                if (symbolic::eq(this->indvar(i), symbol)) {
30✔
712
                    unused = false;
13✔
713
                    break;
13✔
714
                }
13✔
715
            }
30✔
716
            if (!unused) {
30✔
717
                break;
13✔
718
            }
13✔
719
        }
30✔
720
        if (!unused) {
23✔
721
            continue;
13✔
722
        }
13✔
723
        for (auto& indices : this->in_indices()) {
12✔
724
            for (auto& index : indices) {
17✔
725
                for (auto& symbol : symbolic::atoms(index)) {
17✔
726
                    if (symbolic::eq(this->indvar(i), symbol)) {
17✔
727
                        unused = false;
10✔
728
                        break;
10✔
729
                    }
10✔
730
                }
17✔
731
                if (!unused) {
17✔
732
                    break;
10✔
733
                }
10✔
734
            }
17✔
735
            if (!unused) {
12✔
736
                break;
10✔
737
            }
10✔
738
        }
12✔
739
        if (unused) {
10✔
740
            throw InvalidSDFGException(
×
741
                "EinsumNode: Dimension indvar does not occur in the in/out indices: " + this->indvar(i)->__str__()
×
742
            );
×
743
        }
×
744
    }
10✔
745
}
22✔
746

747
nlohmann::json EinsumSerializer::serialize(const data_flow::LibraryNode& libnode) {
1✔
748
    if (libnode.code() != LibraryNodeType_Einsum) {
1✔
749
        throw InvalidSDFGException("EinsumSerializer: Invalid library node type");
×
750
    }
×
751

752
    const auto& einsum_node = static_cast<const EinsumNode&>(libnode);
1✔
753
    serializer::JSONSymbolicPrinter printer;
1✔
754

755
    nlohmann::json j;
1✔
756
    j["type"] = "library_node";
1✔
757
    j["code"] = std::string(LibraryNodeType_Einsum.value());
1✔
758
    j["side_effect"] = einsum_node.side_effect();
1✔
759

760
    j["output"] = einsum_node.output(0);
1✔
761

762
    j["inputs"] = nlohmann::json::array();
1✔
763
    for (auto& input : einsum_node.inputs()) {
3✔
764
        j["inputs"].push_back(input);
3✔
765
    }
3✔
766

767
    j["dims"] = nlohmann::json::array();
1✔
768
    for (auto& dim : einsum_node.dims()) {
3✔
769
        nlohmann::json dimj;
3✔
770
        dimj["indvar"] = printer.apply(dim.indvar);
3✔
771
        dimj["init"] = printer.apply(dim.init);
3✔
772
        dimj["bound"] = printer.apply(dim.bound);
3✔
773
        j["dims"].push_back(dimj);
3✔
774
    }
3✔
775

776
    j["out_indices"] = nlohmann::json::array();
1✔
777
    for (auto& index : einsum_node.out_indices()) {
2✔
778
        j["out_indices"].push_back(printer.apply(index));
2✔
779
    }
2✔
780

781
    j["in_indices"] = nlohmann::json::array();
1✔
782
    for (auto& indices : einsum_node.in_indices()) {
3✔
783
        nlohmann::json indicesj = nlohmann::json::array();
3✔
784
        for (auto& index : indices) {
6✔
785
            indicesj.push_back(printer.apply(index));
6✔
786
        }
6✔
787
        j["in_indices"].push_back(indicesj);
3✔
788
    }
3✔
789

790
    return j;
1✔
791
}
1✔
792

793
data_flow::LibraryNode& EinsumSerializer::deserialize(
794
    const nlohmann::json& j, builder::StructuredSDFGBuilder& builder, structured_control_flow::Block& parent
795
) {
1✔
796
    assert(j.contains("type"));
1✔
797
    assert(j["type"].is_string());
1✔
798
    assert(j.contains("code"));
1✔
799
    assert(j["code"].is_string());
1✔
800
    assert(j.contains("side_effect"));
1✔
801
    assert(j["side_effect"].is_boolean());
1✔
802
    assert(j.contains("output"));
1✔
803
    assert(j["output"].is_string());
1✔
804
    assert(j.contains("inputs"));
1✔
805
    assert(j["inputs"].is_array());
1✔
806
    assert(j.contains("dims"));
1✔
807
    assert(j["dims"].is_array());
1✔
808
    assert(j.contains("out_indices"));
1✔
809
    assert(j["out_indices"].is_array());
1✔
810
    assert(j.contains("in_indices"));
1✔
811
    assert(j["in_indices"].is_array());
1✔
812
    assert(j["inputs"].size() == j["in_indices"].size());
1✔
813

814
    auto type = j["type"].get<std::string>();
1✔
815
    if (type != "library_node") {
1✔
816
        throw InvalidSDFGException("EinsumSerializer: Invalid library node type");
×
817
    }
×
818

819
    auto code = j["code"].get<std::string>();
1✔
820
    if (code != LibraryNodeType_Einsum.value()) {
1✔
821
        throw InvalidSDFGException("EinsumSerializer: Invalid library node code");
×
822
    }
×
823

824
    auto side_effect = j["side_effect"].get<bool>();
1✔
825
    if (side_effect) {
1✔
826
        throw InvalidSDFGException("EinsumSerializer: EinsumNodes must be free of side effects");
×
827
    }
×
828

829
    auto output = j["output"].get<std::string>();
1✔
830
    if (output != "__einsum_out") {
1✔
831
        throw InvalidSDFGException("EinsumSerializer: Output of EinsumNode must be '__einsum_out'");
×
832
    }
×
833

834
    auto inputs = j["inputs"].get<std::vector<std::string>>();
1✔
835
    size_t inputs_size = inputs.size();
1✔
836
    if (inputs_size == 0) {
1✔
837
        throw InvalidSDFGException("EinsumSerializer: Inputs of EinsumNode must not be empty");
×
838
    }
×
839
    if (inputs[inputs_size - 1] != "__einsum_out") {
1✔
840
        throw InvalidSDFGException("EinsumSerializer: Last input of EinsumNode must be '__einsum_out'");
×
841
    }
×
842

843
    std::vector<EinsumDimension> dims;
1✔
844
    for (size_t i = 0; i < j["dims"].size(); i++) {
4✔
845
        auto& dimj = j["dims"][i];
3✔
846
        assert(dimj.is_object());
3✔
847
        assert(dimj.contains("indvar"));
3✔
848
        assert(dimj["indvar"].is_string());
3✔
849
        assert(dimj.contains("init"));
3✔
850
        assert(dimj["init"].is_string());
3✔
851
        assert(dimj.contains("bound"));
3✔
852
        assert(dimj["bound"].is_string());
3✔
853

854
        EinsumDimension dim;
3✔
855
        dim.indvar = symbolic::symbol(dimj["indvar"]);
3✔
856
        dim.init = symbolic::parse(dimj["init"]);
3✔
857
        dim.bound = symbolic::parse(dimj["bound"]);
3✔
858
        dims.push_back(dim);
3✔
859
    }
3✔
860

861
    data_flow::Subset out_indices;
1✔
862
    auto out_indices_str = j["out_indices"].get<std::vector<std::string>>();
1✔
863
    for (auto& index_str : out_indices_str) {
2✔
864
        out_indices.push_back(symbolic::parse(index_str));
2✔
865
    }
2✔
866

867
    std::vector<data_flow::Subset> in_indices;
1✔
868
    for (size_t i = 0; i < j["in_indices"].size(); i++) {
4✔
869
        assert(j["in_indices"][i].is_array());
3✔
870

871
        data_flow::Subset indices;
3✔
872
        auto indices_str = j["in_indices"][i].get<std::vector<std::string>>();
3✔
873
        for (auto& index_str : indices_str) {
6✔
874
            indices.push_back(symbolic::parse(index_str));
6✔
875
        }
6✔
876
        in_indices.push_back(indices);
3✔
877
    }
3✔
878
    if (out_indices.size() != in_indices[inputs_size - 1].size()) {
1✔
879
        throw InvalidSDFGException("EinsumSerializer: Out indices and last in indices have different sizes");
×
880
    }
×
881
    for (size_t i = 0; i < out_indices.size(); i++) {
3✔
882
        if (!symbolic::eq(out_indices[i], in_indices[inputs_size - 1][i])) {
2✔
883
            throw InvalidSDFGException("EinsumSerializer: Out indices and last in indices do not match");
×
884
        }
×
885
    }
2✔
886

887
    auto& einsum_node = builder.add_library_node<
1✔
888
        EinsumNode,
1✔
889
        const std::vector<std::string>&,
1✔
890
        const std::vector<EinsumDimension>&,
1✔
891
        const data_flow::Subset&,
1✔
892
        const std::vector<data_flow::Subset>&,
1✔
893
        bool>(
1✔
894
        parent,
1✔
895
        DebugInfo(),
1✔
896
        std::vector<std::string>(inputs.begin(), inputs.end() - 1),
1✔
897
        dims,
1✔
898
        out_indices,
1✔
899
        std::vector<data_flow::Subset>(in_indices.begin(), in_indices.end() - 1),
1✔
900
        false // skip renaming - already internal symbols from serialization
1✔
901
    );
1✔
902

903
    return einsum_node;
1✔
904
}
1✔
905

906
} // namespace tensor
907
} // namespace math
908
} // namespace sdfg
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc