• 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

67.25
/sdfg/src/data_flow/library_nodes/math/tensor/conv_node.cpp
1
#include "sdfg/data_flow/library_nodes/math/tensor/conv_node.h"
2

3
#include <map>
4
#include <sstream>
5
#include <utility>
6

7
#include "sdfg/analysis/analysis.h"
8
#include "sdfg/builder/structured_sdfg_builder.h"
9
#include "sdfg/data_flow/access_node.h"
10
#include "sdfg/data_flow/library_nodes/math/blas/blas_node.h"
11
#include "sdfg/data_flow/library_nodes/stdlib/free.h"
12
#include "sdfg/data_flow/library_nodes/stdlib/malloc.h"
13
#include "sdfg/data_flow/library_nodes/stdlib/memset.h"
14
#include "sdfg/data_flow/memlet.h"
15
#include "sdfg/data_flow/tasklet.h"
16
#include "sdfg/exceptions.h"
17
#include "sdfg/structured_control_flow/block.h"
18
#include "sdfg/structured_control_flow/map.h"
19
#include "sdfg/structured_control_flow/sequence.h"
20
#include "sdfg/symbolic/symbolic.h"
21
#include "sdfg/types/pointer.h"
22
#include "sdfg/types/scalar.h"
23
#include "sdfg/types/tensor.h"
24
#include "sdfg/types/type.h"
25

26
#include "sdfg/data_flow/library_nodes/math/blas/gemm_node.h"
27
#include "symengine/integer.h"
28
#include "symengine/symengine_rcp.h"
29

30
namespace sdfg {
31
namespace math {
32
namespace tensor {
33

34
ConvNode::ConvNode(
35
    size_t element_id,
36
    const DebugInfo& debug_info,
37
    const graph::Vertex vertex,
38
    data_flow::DataFlowGraph& parent,
39
    const std::vector<symbolic::Expression>& shape,
40
    const std::vector<symbolic::Expression>& kernel_shape,
41
    const std::vector<symbolic::Expression>& strides,
42
    const std::vector<symbolic::Expression>& pads,
43
    const std::vector<symbolic::Expression>& dilations,
44
    symbolic::Expression output_channels,
45
    symbolic::Expression group,
46
    bool with_bias,
47
    QuantizationType quantization,
48
    const data_flow::ImplementationType& impl_type
49
)
50
    : SpatialTensorNode(
47✔
51
          element_id,
47✔
52
          debug_info,
47✔
53
          vertex,
47✔
54
          parent,
47✔
55
          LibraryNodeType_Conv,
47✔
56
          {},
47✔
57
          {"Y", "X", "W"}, // X and W are required, B (bias) is optional
47✔
58
          impl_type,
47✔
59
          quantization,
47✔
60
          shape,
47✔
61
          kernel_shape,
47✔
62
          strides,
47✔
63
          pads,
47✔
64
          dilations
47✔
65
      ),
47✔
66
      output_channels_(std::move(output_channels)), group_(std::move(group)), with_bias_(with_bias) {
47✔
67
    if (with_bias) {
47✔
68
        inputs_.push_back("B");
5✔
69
    }
5✔
70
}
47✔
71

72
void ConvNode::validate(const Function& function) const {
82✔
73
    TensorNode::validate(function);
82✔
74

75
    auto& graph = this->get_parent();
82✔
76

77
    // Custom validation for ConvNode that handles optional bias input
78
    // We expect X, W as required inputs and optionally B (bias)
79

80
    // Collect all input edges by connector name
81
    std::map<std::string, const data_flow::Memlet*> input_edges;
82✔
82
    for (auto& iedge : graph.in_edges(*this)) {
250✔
83
        input_edges[iedge.dst_conn()] = &iedge;
250✔
84
    }
250✔
85

86
    // Check that required inputs X and W are present
87
    if (input_edges.find("X") == input_edges.end()) {
82✔
88
        throw InvalidSDFGException("ConvNode: Required input 'X' is not connected");
×
89
    }
×
90
    if (input_edges.find("W") == input_edges.end()) {
82✔
91
        throw InvalidSDFGException("ConvNode: Required input 'W' is not connected");
×
92
    }
×
93

94
    // Validate that parameters are not empty
95
    if (shape_.empty()) {
82✔
96
        throw InvalidSDFGException("ConvNode shape cannot be empty");
×
97
    }
×
98
    if (kernel_shape_.empty()) {
82✔
99
        throw InvalidSDFGException("ConvNode kernel_shape cannot be empty");
×
100
    }
×
101
    if (strides_.empty()) {
82✔
102
        throw InvalidSDFGException("ConvNode strides cannot be empty");
×
103
    }
×
104
    if (pads_.empty()) {
82✔
105
        throw InvalidSDFGException("ConvNode pads cannot be empty");
×
106
    }
×
107
    if (dilations_.empty()) {
82✔
108
        throw InvalidSDFGException("ConvNode dilations cannot be empty");
×
109
    }
×
110

111
    // Validate consistent dimensions
112
    size_t spatial_dims = kernel_shape_.size();
82✔
113

114
    if (shape_.size() != spatial_dims + 2) {
82✔
115
        throw InvalidSDFGException("ConvNode shape must match kernel spatial dimensions + 2");
×
116
    }
×
117

118
    if (strides_.size() != spatial_dims) {
82✔
119
        throw InvalidSDFGException("ConvNode strides must match kernel spatial dimensions");
1✔
120
    }
1✔
121

122
    if (pads_.size() != 2 * spatial_dims) {
81✔
123
        throw InvalidSDFGException("ConvNode pads must have 2 * spatial dimensions (start and end for each axis)");
1✔
124
    }
1✔
125

126
    if (dilations_.size() != spatial_dims) {
80✔
127
        throw InvalidSDFGException("ConvNode dilations must match kernel spatial dimensions");
×
128
    }
×
129

130
    // Validate groups
131
    if (SymEngine::is_a<SymEngine::Integer>(*this->group_)) {
80✔
132
        auto group_int = SymEngine::rcp_static_cast<const SymEngine::Integer>(this->group_)->as_int();
80✔
133
        if (SymEngine::is_a<SymEngine::Integer>(*this->shape_[1])) {
80✔
134
            auto input_channels_int = SymEngine::rcp_static_cast<const SymEngine::Integer>(this->shape_[1])->as_int();
80✔
135
            if (input_channels_int % group_int != 0) {
80✔
136
                throw InvalidSDFGException("ConvNode input channels must be divisible by groups");
×
137
            }
×
138
        }
80✔
139
        if (SymEngine::is_a<SymEngine::Integer>(*this->output_channels_)) {
80✔
140
            auto output_channels_int =
80✔
141
                SymEngine::rcp_static_cast<const SymEngine::Integer>(this->output_channels_)->as_int();
80✔
142
            if (output_channels_int % group_int != 0) {
80✔
143
                throw InvalidSDFGException("ConvNode output channels must be divisible by groups");
×
144
            }
×
145
        }
80✔
146
    }
80✔
147
}
80✔
148

149
blas::BLAS_Precision ConvNode::get_blas_precision(types::Scalar base_type) {
17✔
150
    switch (base_type.primitive_type()) {
17✔
151
        case types::PrimitiveType::Half:
×
152
            return blas::BLAS_Precision::h;
×
153
        case types::PrimitiveType::Float:
17✔
154
            return blas::BLAS_Precision::s;
17✔
155
        case types::PrimitiveType::Double:
×
156
            return blas::BLAS_Precision::d;
×
157
        default:
×
158
            return blas::BLAS_Precision::invalid;
×
159
    }
17✔
160
}
17✔
161

162
symbolic::MultiExpression ConvNode::get_out_shape() {
17✔
163
    size_t dims = kernel_shape_.size();
17✔
164
    symbolic::MultiExpression out_shape;
17✔
165
    out_shape.reserve(dims);
17✔
166
    // out_shape[i] = (shape[i + 2] + pads[i] + pads[dims + i] - dilations[i] * (kernel_shape[i] - 1) - 1)
167
    //                 / strides[i] + 1
168
    for (size_t i = 0; i < dims; i++) {
49✔
169
        out_shape.push_back(symbolic::add(
32✔
170
            symbolic::div(
32✔
171
                symbolic::sub(
32✔
172
                    symbolic::
32✔
173
                        sub(symbolic::add(this->shape_[i + 2], symbolic::add(this->pads_[i], this->pads_[dims + i])),
32✔
174
                            symbolic::mul(this->dilations_[i], symbolic::sub(this->kernel_shape_[i], symbolic::one()))),
32✔
175
                    symbolic::one()
32✔
176
                ),
32✔
177
                this->strides_[i]
32✔
178
            ),
32✔
179
            symbolic::one()
32✔
180
        ));
32✔
181
    }
32✔
182
    return out_shape;
17✔
183
}
17✔
184

185
bool ConvNode::has_bias() const { return with_bias_; }
×
186

187
bool ConvNode::check_expandable(data_flow::DataFlowGraph& dfg, ConvExpandPrerequisits& boundary) const {
27✔
188
    if ((dfg.nodes().size() != 4 || dfg.edges().size() != 3) && (dfg.nodes().size() != 5 || dfg.edges().size() != 4)) {
27✔
189
        return false;
4✔
190
    }
4✔
191

192
    // Get edges
193
    boundary.iedge_X = dfg.in_edge_for_connector(*this, "X");
23✔
194
    boundary.iedge_W = dfg.in_edge_for_connector(*this, "W");
23✔
195
    boundary.iedge_B = with_bias_ ? dfg.in_edge_for_connector(*this, "B") : nullptr;
23✔
196
    boundary.iedge_Y = dfg.in_edge_for_connector(*this, "Y");
23✔
197
    if (!boundary.iedge_X || !boundary.iedge_W || !boundary.iedge_Y) {
23✔
198
        return false;
×
199
    }
×
200
    boundary.has_bias = boundary.iedge_B != nullptr;
23✔
201

202
    // Get access nodes
203
    boundary.access_X = dynamic_cast<const data_flow::AccessNode*>(&boundary.iedge_X->src());
23✔
204
    boundary.access_W = dynamic_cast<const data_flow::AccessNode*>(&boundary.iedge_W->src());
23✔
205
    boundary.access_B =
23✔
206
        (boundary.has_bias ? dynamic_cast<const data_flow::AccessNode*>(&boundary.iedge_B->src()) : nullptr);
23✔
207
    boundary.access_Y = dynamic_cast<const data_flow::AccessNode*>(&boundary.iedge_Y->src());
23✔
208
    if (!boundary.access_X || !boundary.access_W || (boundary.has_bias && !boundary.access_B) || !boundary.access_Y) {
23✔
209
        return false;
×
210
    }
×
211

212
    // Get block & its parent
213
    boundary.block = dynamic_cast<structured_control_flow::Block*>(dfg.get_parent());
23✔
214
    if (!boundary.block) {
23✔
215
        return false;
×
216
    }
×
217

218
    boundary.block_parent = dynamic_cast<structured_control_flow::Sequence*>(boundary.block->get_parent());
23✔
219
    if (!boundary.block_parent) {
23✔
220
        return false;
×
221
    }
×
222

223
    boundary.block_index = boundary.block_parent->index(*boundary.block);
23✔
224
    if (boundary.block_index >= boundary.block_parent->size()) {
23✔
225
        return false;
×
226
    }
×
227

228
    return true;
23✔
229
}
23✔
230

231

232
passes::LibNodeExpander::ExpandOutcome ConvNode::
233
    expand(passes::LibNodeExpander::ExpandContext& context, structured_control_flow::Block& block) {
5✔
234
    // Validate nodes are standalone in the data flow graph
235
    auto& dfg = this->get_parent();
5✔
236
    ConvExpandPrerequisits b;
5✔
237
    if (!check_expandable(dfg, b)) {
5✔
NEW
238
        return context.unable();
×
239
    }
×
240

241
    constexpr auto Y_INPUT_IDX = 0;
5✔
242
    constexpr auto X_INPUT_IDX = 1;
5✔
243
    constexpr auto W_INPUT_IDX = 2;
5✔
244
    constexpr auto B_INPUT_IDX = 3;
5✔
245

246
    // Determine BLAS precision
247

248
    types::Scalar base_type(this->primitive_type(dfg));
5✔
249
    blas::BLAS_Precision precision = get_blas_precision(base_type);
5✔
250
    if (precision == blas::BLAS_Precision::invalid) {
5✔
NEW
251
        return context.unable();
×
NEW
252
    }
×
253

254
    using Use = passes::LibNodeExpander::InputUse;
5✔
255
    std::vector<Use> req_inputs = {Use::IndirectReadWrite, Use::IndirectRead, Use::IndirectRead};
5✔
256
    if (this->with_bias_) {
5✔
NEW
257
        req_inputs.push_back(Use::Scalar);
×
NEW
258
    }
×
259
    auto standalone = context.replacement_requires_access_nodes(req_inputs);
5✔
260

261
    if (!standalone) {
5✔
NEW
262
        return context.unable();
×
263
    }
×
264

265
    // Create new sequence for expansion
266
    auto& new_sequence = standalone->replace_with_sequence();
5✔
267
    auto& builder = standalone->builder();
5✔
268

269
    // Dimensions, i.e., 1D, 2D, 3D, ...
270
    size_t dims = this->kernel_shape_.size();
5✔
271
    symbolic::MultiExpression out_shape = get_out_shape();
5✔
272
    types::Scalar indvar_type(types::PrimitiveType::Int64);
5✔
273

274
    auto in_channels = symbolic::div(this->shape_[1], this->group_);
5✔
275
    auto out_channels = symbolic::div(this->output_channels_, this->group_);
5✔
276

277
    // Add loop over batch size
278
    auto n_container = builder.find_new_name("_n");
5✔
279
    builder.add_container(n_container, indvar_type);
5✔
280
    auto n = symbolic::symbol(n_container);
5✔
281
    auto& loop_n = builder.add_map(
5✔
282
        new_sequence,
5✔
283
        n,
5✔
284
        symbolic::Lt(n, this->shape_[0]),
5✔
285
        symbolic::zero(),
5✔
286
        symbolic::add(n, symbolic::one()),
5✔
287
        ScheduleType_Sequential::create(),
5✔
288
        {},
5✔
289
        block.debug_info()
5✔
290
    );
5✔
291

292
    // Add loop over groups
293
    auto g_container = builder.find_new_name("_g");
5✔
294
    builder.add_container(g_container, indvar_type);
5✔
295
    auto g = symbolic::symbol(g_container);
5✔
296
    auto& loop_g = builder.add_map(
5✔
297
        loop_n.root(),
5✔
298
        g,
5✔
299
        symbolic::Lt(g, this->group_),
5✔
300
        symbolic::zero(),
5✔
301
        symbolic::add(g, symbolic::one()),
5✔
302
        ScheduleType_Sequential::create(),
5✔
303
        {},
5✔
304
        block.debug_info()
5✔
305
    );
5✔
306

307
    // Add patches container with malloc
308
    symbolic::Expression patches_size = in_channels;
5✔
309
    for (size_t i = 0; i < dims; i++) {
15✔
310
        patches_size = symbolic::mul(patches_size, symbolic::mul(this->kernel_shape_[i], out_shape[i]));
10✔
311
    }
10✔
312
    types::Pointer patches_type(base_type);
5✔
313
    auto patches_container = builder.find_new_name("_patches");
5✔
314
    builder.add_container(patches_container, patches_type);
5✔
315
    auto [patches_malloc_block, patches_malloc_node] = stdlib::add_malloc_block(
5✔
316
        builder,
5✔
317
        loop_g.root(),
5✔
318
        patches_container,
5✔
319
        symbolic::mul(patches_size, symbolic::size_of_type(base_type)),
5✔
320
        patches_type,
5✔
321
        this->debug_info()
5✔
322
    );
5✔
323

324
    // Add loop over channels
325
    structured_control_flow::Sequence* current_seq = &loop_g.root();
5✔
326
    auto c_container = builder.find_new_name("_c");
5✔
327
    builder.add_container(c_container, indvar_type);
5✔
328
    auto c = symbolic::symbol(c_container);
5✔
329
    auto& loop_c = builder.add_map(
5✔
330
        *current_seq,
5✔
331
        c,
5✔
332
        symbolic::Lt(c, in_channels),
5✔
333
        symbolic::zero(),
5✔
334
        symbolic::add(c, symbolic::one()),
5✔
335
        ScheduleType_Sequential::create(),
5✔
336
        {},
5✔
337
        block.debug_info()
5✔
338
    );
5✔
339
    current_seq = &loop_c.root();
5✔
340

341
    // Add loops over kernel shape
342
    symbolic::SymbolVec ks;
5✔
343
    ks.reserve(dims);
5✔
344
    for (size_t i = 0; i < dims; i++) {
15✔
345
        auto k_container = builder.find_new_name("_k");
10✔
346
        builder.add_container(k_container, indvar_type);
10✔
347
        auto k = symbolic::symbol(k_container);
10✔
348
        ks.push_back(k);
10✔
349
        auto& loop_k = builder.add_map(
10✔
350
            *current_seq,
10✔
351
            k,
10✔
352
            symbolic::Lt(k, this->kernel_shape_[i]),
10✔
353
            symbolic::zero(),
10✔
354
            symbolic::add(k, symbolic::one()),
10✔
355
            ScheduleType_Sequential::create(),
10✔
356
            {},
10✔
357
            block.debug_info()
10✔
358
        );
10✔
359
        current_seq = &loop_k.root();
10✔
360
    }
10✔
361

362
    // Add loops over output dimensions
363
    symbolic::SymbolVec os;
5✔
364
    os.reserve(dims);
5✔
365
    for (size_t i = 0; i < dims; i++) {
15✔
366
        auto o_container = builder.find_new_name("_o");
10✔
367
        builder.add_container(o_container, indvar_type);
10✔
368
        auto o = symbolic::symbol(o_container);
10✔
369
        os.push_back(o);
10✔
370
        auto& loop_o = builder.add_map(
10✔
371
            *current_seq,
10✔
372
            o,
10✔
373
            symbolic::Lt(o, out_shape[i]),
10✔
374
            symbolic::zero(),
10✔
375
            symbolic::add(o, symbolic::one()),
10✔
376
            ScheduleType_Sequential::create(),
10✔
377
            {},
10✔
378
            block.debug_info()
10✔
379
        );
10✔
380
        current_seq = &loop_o.root();
10✔
381
    }
10✔
382

383
    // Add if/else to stay in bounds for copying
384
    symbolic::MultiExpression is;
5✔
385
    is.reserve(dims);
5✔
386
    symbolic::Condition copy_condition = symbolic::__true__();
5✔
387
    symbolic::Condition zero_condition = symbolic::__false__();
5✔
388
    for (size_t i = 0; i < dims; i++) {
15✔
389
        auto i_expr = symbolic::
10✔
390
            add(symbolic::sub(symbolic::mul(os[i], this->strides_[i]), this->pads_[i]),
10✔
391
                symbolic::mul(ks[i], this->dilations_[i]));
10✔
392
        is.push_back(i_expr);
10✔
393
        copy_condition = symbolic::
10✔
394
            And(copy_condition,
10✔
395
                symbolic::And(symbolic::Lt(i_expr, this->shape_[i + 2]), symbolic::Ge(i_expr, symbolic::zero())));
10✔
396
        zero_condition = symbolic::
10✔
397
            Or(zero_condition,
10✔
398
               symbolic::Or(symbolic::Ge(i_expr, this->shape_[i + 2]), symbolic::Lt(i_expr, symbolic::zero())));
10✔
399
    }
10✔
400
    auto& branch = builder.add_if_else(*current_seq, {}, block.debug_info());
5✔
401
    auto& copy_case = builder.add_case(branch, copy_condition, block.debug_info());
5✔
402
    auto& zero_case = builder.add_case(branch, zero_condition, block.debug_info());
5✔
403

404
    // Determine patches subset & tensor type
405
    data_flow::Subset patches_subset;
5✔
406
    patches_subset.push_back(c);
5✔
407
    patches_subset.insert(patches_subset.end(), ks.begin(), ks.end());
5✔
408
    patches_subset.insert(patches_subset.end(), os.begin(), os.end());
5✔
409
    symbolic::MultiExpression patches_shape;
5✔
410
    patches_shape.push_back(in_channels);
5✔
411
    patches_shape.insert(patches_shape.end(), this->kernel_shape_.begin(), this->kernel_shape_.end());
5✔
412
    patches_shape.insert(patches_shape.end(), out_shape.begin(), out_shape.end());
5✔
413
    types::Tensor patches_tensor_type(base_type, patches_shape);
5✔
414

415
    // Determine subset for X
416
    data_flow::Subset subset_X;
5✔
417
    subset_X.push_back(n);
5✔
418
    subset_X.push_back(symbolic::add(symbolic::mul(in_channels, g), c));
5✔
419
    subset_X.insert(subset_X.end(), is.begin(), is.end());
5✔
420

421
    // Add copy from X to patches
422
    auto& copy_block = builder.add_block(copy_case, {}, block.debug_info());
5✔
423
    {
5✔
424
        auto& X_access = standalone->add_indirect_read_access(copy_block, X_INPUT_IDX);
5✔
425
        auto& patches_access = builder.add_access(copy_block, patches_container, this->debug_info());
5✔
426
        auto& tasklet =
5✔
427
            builder.add_tasklet(copy_block, data_flow::TaskletCode::assign, "_out", {"_in"}, this->debug_info());
5✔
428
        builder.add_computational_memlet(
5✔
429
            copy_block, X_access, tasklet, "_in", subset_X, b.iedge_X->base_type(), b.iedge_X->debug_info()
5✔
430
        );
5✔
431
        builder.add_computational_memlet(
5✔
432
            copy_block, tasklet, "_out", patches_access, patches_subset, patches_tensor_type, this->debug_info()
5✔
433
        );
5✔
434
    }
5✔
435

436
    // Add zero assignment to patches
437
    auto& zero_block = builder.add_block(zero_case, {}, block.debug_info());
5✔
438
    {
5✔
439
        auto& constant_zero = builder.add_constant(zero_block, "0.0", base_type, this->debug_info());
5✔
440
        auto& patches_access = builder.add_access(zero_block, patches_container, this->debug_info());
5✔
441
        auto& tasklet =
5✔
442
            builder.add_tasklet(zero_block, data_flow::TaskletCode::assign, "_out", {"_in"}, this->debug_info());
5✔
443
        builder.add_computational_memlet(zero_block, constant_zero, tasklet, "_in", {}, base_type, this->debug_info());
5✔
444
        builder.add_computational_memlet(
5✔
445
            zero_block, tasklet, "_out", patches_access, patches_subset, patches_tensor_type, this->debug_info()
5✔
446
        );
5✔
447
    }
5✔
448

449
    // Add reference to W
450
    auto ref_W_container = builder.find_new_name("_ref_W");
5✔
451
    auto& ref_W_block = builder.add_block(loop_g.root(), {}, block.debug_info());
5✔
452
    auto& W_access = standalone->add_scalar_input_access(ref_W_block, W_INPUT_IDX);
5✔
453
    types::Scalar ref_W_base_type(builder.subject().type(W_access.data()).primitive_type());
5✔
454
    types::Pointer ref_W_type(ref_W_base_type);
5✔
455
    builder.add_container(ref_W_container, ref_W_type);
5✔
456
    auto ref_W_subset = symbolic::mul(symbolic::mul(out_channels, g), in_channels);
5✔
457
    for (size_t i = 0; i < dims; i++) {
15✔
458
        ref_W_subset = symbolic::mul(ref_W_subset, this->kernel_shape_[i]);
10✔
459
    }
10✔
460
    {
5✔
461
        auto& ref_W_access = builder.add_access(ref_W_block, ref_W_container, W_access.debug_info());
5✔
462
        builder.add_reference_memlet(ref_W_block, W_access, ref_W_access, {ref_W_subset}, ref_W_type);
5✔
463
    }
5✔
464

465
    // Add reference to Y
466
    auto& ref_Y_block = builder.add_block(loop_g.root(), {}, block.debug_info());
5✔
467
    auto& Y_access = standalone->add_scalar_input_access(ref_Y_block, Y_INPUT_IDX);
5✔
468
    auto ref_Y_container = builder.find_new_name("_ref_Y");
5✔
469
    types::Scalar ref_Y_base_type(builder.subject().type(Y_access.data()).primitive_type());
5✔
470
    types::Pointer ref_Y_type(ref_Y_base_type);
5✔
471
    builder.add_container(ref_Y_container, ref_Y_type);
5✔
472
    auto ref_Y_subset = symbolic::add(symbolic::mul(this->output_channels_, n), symbolic::mul(out_channels, g));
5✔
473
    for (size_t i = 0; i < dims; i++) {
15✔
474
        ref_Y_subset = symbolic::mul(ref_Y_subset, out_shape[i]);
10✔
475
    }
10✔
476
    {
5✔
477
        auto& ref_Y_access = builder.add_access(ref_Y_block, ref_Y_container, Y_access.debug_info());
5✔
478
        builder.add_reference_memlet(ref_Y_block, Y_access, ref_Y_access, {ref_Y_subset}, ref_Y_type);
5✔
479
    }
5✔
480

481
    // Add GEMM node
482
    auto& gemm_block = builder.add_block(loop_g.root(), {}, block.debug_info());
5✔
483
    {
5✔
484
        auto& alpha = builder.add_constant(gemm_block, "1.0", base_type, this->debug_info());
5✔
485
        auto& beta = builder.add_constant(gemm_block, "0.0", base_type, this->debug_info());
5✔
486
        auto& ref_W_access = builder.add_access(gemm_block, ref_W_container, W_access.debug_info());
5✔
487
        auto& patches_access = builder.add_access(gemm_block, patches_container, this->debug_info());
5✔
488
        auto& ref_Y_access_in = builder.add_access(gemm_block, ref_Y_container, Y_access.debug_info());
5✔
489
        symbolic::Expression gemm_m = out_channels;
5✔
490
        symbolic::Expression gemm_n = symbolic::one();
5✔
491
        symbolic::Expression gemm_k = in_channels;
5✔
492
        for (size_t i = 0; i < dims; i++) {
15✔
493
            gemm_n = symbolic::mul(gemm_n, out_shape[i]);
10✔
494
            gemm_k = symbolic::mul(gemm_k, this->kernel_shape_[i]);
10✔
495
        }
10✔
496
        auto& libnode = builder.add_library_node<blas::GEMMNode>(
5✔
497
            gemm_block,
5✔
498
            this->debug_info(),
5✔
499
            blas::ImplementationType_BLAS,
5✔
500
            precision, // precision
5✔
501
            blas::BLAS_Layout::RowMajor, // layout
5✔
502
            blas::BLAS_Transpose::No, // transA
5✔
503
            blas::BLAS_Transpose::No, // transB
5✔
504
            gemm_m, // m
5✔
505
            gemm_n, // n
5✔
506
            gemm_k, // k
5✔
507
            gemm_k, // lda
5✔
508
            gemm_n, // ldb
5✔
509
            gemm_n // ldc
5✔
510
        );
5✔
511
        builder.add_computational_memlet(gemm_block, alpha, libnode, "__alpha", {}, base_type, this->debug_info());
5✔
512
        builder.add_computational_memlet(gemm_block, beta, libnode, "__beta", {}, base_type, this->debug_info());
5✔
513
        builder
5✔
514
            .add_computational_memlet(gemm_block, ref_W_access, libnode, "__A", {}, ref_W_type, b.iedge_W->debug_info());
5✔
515
        builder
5✔
516
            .add_computational_memlet(gemm_block, patches_access, libnode, "__B", {}, patches_type, this->debug_info());
5✔
517
        builder.add_computational_memlet(
5✔
518
            gemm_block, ref_Y_access_in, libnode, "__C", {}, ref_Y_type, b.iedge_Y->debug_info()
5✔
519
        );
5✔
520
    }
5✔
521

522
    // Add bias if available
523
    if (with_bias_) {
5✔
524
        // Add loop over output channels
525
        auto l_container = builder.find_new_name("_l");
×
526
        builder.add_container(l_container, indvar_type);
×
527
        auto l = symbolic::symbol(l_container);
×
528
        auto& loop_l = builder.add_map(
×
529
            loop_g.root(),
×
530
            l,
×
531
            symbolic::Lt(l, out_channels),
×
532
            symbolic::zero(),
×
533
            symbolic::add(l, symbolic::one()),
×
534
            ScheduleType_Sequential::create(),
×
535
            {},
×
NEW
536
            block.debug_info()
×
537
        );
×
538
        current_seq = &loop_l.root();
×
539

540
        // Add loops over output dimensions (again)
541
        for (size_t i = 0; i < dims; i++) {
×
542
            auto o_container = builder.find_new_name("_o");
×
543
            builder.add_container(o_container, indvar_type);
×
544
            auto o = symbolic::symbol(o_container);
×
545
            auto& loop_o = builder.add_map(
×
546
                *current_seq,
×
547
                o,
×
548
                symbolic::Lt(o, out_shape[i]),
×
549
                symbolic::zero(),
×
550
                symbolic::add(o, symbolic::one()),
×
551
                ScheduleType_Sequential::create(),
×
552
                {},
×
NEW
553
                block.debug_info()
×
554
            );
×
555
            current_seq = &loop_o.root();
×
556
            os[i] = o;
×
557
        }
×
558

559
        // Add bias to Y
560
        data_flow::Subset Y_subset;
×
561
        Y_subset.push_back(n);
×
562
        Y_subset.push_back(symbolic::add(symbolic::mul(out_channels, g), l));
×
563
        Y_subset.insert(Y_subset.end(), os.begin(), os.end());
×
564
        auto B_subset = symbolic::add(symbolic::mul(out_channels, g), l);
×
NEW
565
        auto& bias_block = builder.add_block(*current_seq, {}, block.debug_info());
×
566
        {
×
NEW
567
            auto& B_access = standalone->add_indirect_read_access(bias_block, B_INPUT_IDX);
×
NEW
568
            auto& Y_access_in = standalone->add_indirect_read_access(bias_block, Y_INPUT_IDX);
×
NEW
569
            auto& Y_access_out = standalone->add_indirect_write_access(bias_block, Y_INPUT_IDX);
×
570
            auto& tasklet =
×
571
                builder
×
572
                    .add_tasklet(bias_block, data_flow::TaskletCode::fp_add, "_out", {"_in1", "_in2"}, this->debug_info());
×
573
            builder.add_computational_memlet(
×
574
                bias_block, Y_access_in, tasklet, "_in1", Y_subset, b.iedge_Y->base_type(), this->debug_info()
×
575
            );
×
576
            builder.add_computational_memlet(
×
577
                bias_block, B_access, tasklet, "_in2", {B_subset}, b.iedge_B->base_type(), b.iedge_B->debug_info()
×
578
            );
×
579
            builder.add_computational_memlet(
×
580
                bias_block, tasklet, "_out", Y_access_out, Y_subset, b.iedge_Y->base_type(), b.iedge_Y->debug_info()
×
581
            );
×
582
        }
×
583
    }
×
584

585
    // Add free for patches container
586
    auto& patches_free_block = builder.add_block(loop_g.root(), {}, block.debug_info());
5✔
587
    {
5✔
588
        auto& patches_access_in = builder.add_access(patches_free_block, patches_container, this->debug_info());
5✔
589
        auto& libnode = builder.add_library_node<stdlib::FreeNode>(patches_free_block, this->debug_info());
5✔
590
        builder.add_computational_memlet(
5✔
591
            patches_free_block, patches_access_in, libnode, "_ptr", {}, patches_type, this->debug_info()
5✔
592
        );
5✔
593
    }
5✔
594

595
    return standalone->successfully_expanded();
5✔
596
}
5✔
597

598
symbolic::SymbolSet ConvNode::symbols() const {
13✔
599
    auto syms = SpatialTensorNode::symbols();
13✔
600
    for (auto& atom : symbolic::atoms(output_channels_)) {
13✔
601
        syms.insert(atom);
×
602
    }
×
603
    for (auto& atom : symbolic::atoms(group_)) {
13✔
604
        syms.insert(atom);
×
605
    }
×
606

607
    return syms;
13✔
608
}
13✔
609

610
void ConvNode::replace(const symbolic::Expression old_expression, const symbolic::Expression new_expression) {
×
611
    SpatialTensorNode::replace(old_expression, new_expression);
×
612
    output_channels_ = symbolic::subs(output_channels_, old_expression, new_expression);
×
613
    group_ = symbolic::subs(group_, old_expression, new_expression);
×
614
}
×
615

616
void ConvNode::replace(const symbolic::ExpressionMapping& replacements) {
×
617
    SpatialTensorNode::replace(replacements);
×
618
    output_channels_ = symbolic::subs(output_channels_, replacements);
×
619
    group_ = symbolic::subs(group_, replacements);
×
620
}
×
621

622
std::unique_ptr<data_flow::DataFlowNode> ConvNode::
623
    clone(size_t element_id, const graph::Vertex vertex, data_flow::DataFlowGraph& parent) const {
1✔
624
    return std::unique_ptr<data_flow::DataFlowNode>(new ConvNode(
1✔
625
        element_id,
1✔
626
        this->debug_info(),
1✔
627
        vertex,
1✔
628
        parent,
1✔
629
        shape_,
1✔
630
        kernel_shape_,
1✔
631
        strides_,
1✔
632
        pads_,
1✔
633
        dilations_,
1✔
634
        output_channels_,
1✔
635
        group_,
1✔
636
        with_bias_,
1✔
637
        fixed_quantization_,
1✔
638
        implementation_type_
1✔
639
    ));
1✔
640
}
1✔
641

642
std::string ConvNode::toStr() const {
×
643
    std::stringstream result;
×
644
    result << "Conv(";
×
645
    SpatialTensorNode::operator<<(result);
×
646

647
    result << ", output_channels=" + output_channels_->__str__();
×
648
    result << ", group=" + group_->__str__() + ")";
×
649
    return result.str();
×
650
}
×
651

652
symbolic::Expression ConvNode::flop() const {
×
653
    // Total FLOPs = output_elements * K_conv (multiplications)
654
    //             + output_elements * (K_conv - 1) (additions)
655
    auto output_elems = num_output_elements();
×
656
    auto k_conv = kernel_iteration_count();
×
657

658
    auto mul_ops = symbolic::mul(output_elems, k_conv);
×
659
    auto add_ops = symbolic::mul(output_elems, symbolic::sub(k_conv, symbolic::one()));
×
660
    return symbolic::add(mul_ops, add_ops);
×
661
}
×
662

663
data_flow::PointerAccessType ConvNode::pointer_access_type(int input_idx) const {
×
664
    if (input_idx == 0) {
×
665
        return data_flow::PointerAccessMeta::create_full_write_only(symbolic::__nullptr__(), true);
×
666
    } else if (input_idx >= 1 && input_idx < inputs_.size()) {
×
667
        return data_flow::PointerAccessMeta::create_read_only(symbolic::__nullptr__(), true);
×
668
    } else {
×
669
        return TensorNode::pointer_access_type(input_idx);
×
670
    }
×
671
}
×
672

673
symbolic::Expression ConvNode::num_output_elements() const {
×
674
    // N * C_out * prod(output_spatial_dim(i))
675
    return symbolic::mul(symbolic::mul(shape_[0], output_channels_), output_spatial_volume());
×
676
}
×
677

678
symbolic::Expression ConvNode::kernel_iteration_count() const {
×
679
    // (C_in / group) * prod(kernel_shape_[i])
680
    return symbolic::mul(symbolic::div(shape_[1], group_), kernel_volume());
×
681
}
×
682

683
nlohmann::json ConvNodeSerializer::serialize(const data_flow::LibraryNode& library_node) {
×
684
    const ConvNode& conv_node = static_cast<const ConvNode&>(library_node);
×
685
    nlohmann::json j;
×
686

687
    serializer::JSONSerializer serializer;
×
688
    j["output_channels"] = serializer.expression(conv_node.output_channels());
×
689
    j["group"] = serializer.expression(conv_node.group());
×
690
    j["with_bias"] = conv_node.has_bias();
×
691

692
    fill_base_values(conv_node, j);
×
693

694
    return j;
×
695
}
×
696

697
data_flow::LibraryNode& ConvNodeSerializer::deserialize(
698
    const nlohmann::json& j, builder::StructuredSDFGBuilder& builder, structured_control_flow::Block& parent
699
) {
×
700
    assert(j.contains("kernel_shape"));
×
701

702
    auto base = deserialize_base_values(j);
×
703

704
    auto bias_it = j.find("with_bias");
×
705
    bool with_bias = false;
×
706
    if (bias_it != j.end()) {
×
707
        with_bias = bias_it->get<bool>();
×
708
    }
×
709

710
    symbolic::Expression output_channels = symbolic::one();
×
711
    if (j.contains("output_channels")) {
×
712
        output_channels = symbolic::parse(j["output_channels"].get<std::string>());
×
713
    }
×
714

715
    symbolic::Expression group = symbolic::one();
×
716
    if (j.contains("group")) {
×
717
        group = symbolic::parse(j["group"].get<std::string>());
×
718
    }
×
719

720
    return builder.add_library_node<ConvNode>(
×
721
        parent,
×
722
        base.debug_info,
×
723
        base.shape,
×
724
        base.kernel_shape,
×
725
        base.strides,
×
726
        base.pads,
×
727
        base.dilations,
×
728
        output_channels,
×
729
        group,
×
730
        with_bias,
×
731
        base.quantization
×
732
    );
×
733
}
×
734

735
} // namespace tensor
736
} // namespace math
737
} // 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