Skip to content

Commit 6d23c71

Browse files
matthiasrichtersawenzel
authored andcommitted
Adding helper function to create parallel pipelines from a pre-defined workflow
This DPL helper function creates a workflow with parallel pipelines, each serving multiples of data channels of the same origin and description but different sub specification. If total number of parallel channels is larger than the number of pipelines, the channels are distributed among the pipelines.
1 parent c4db45a commit 6d23c71

4 files changed

Lines changed: 250 additions & 0 deletions

File tree

Framework/Core/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ set(TEST_SRCS
276276
test/test_Variants.cxx
277277
test/test_WorkflowHelpers.cxx
278278
test/test_DeviceSpecHelpers.cxx
279+
test/test_ParallelPipeline.cxx
279280
)
280281

281282
set(BENCH_SRCS

Framework/Core/include/Framework/WorkflowSpec.h

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,44 @@ WorkflowSpec parallel(WorkflowSpec specs,
3737
size_t maxIndex,
3838
std::function<void(DataProcessorSpec&, size_t id)> amendCallback);
3939

40+
/// create parallel pipelines of processors from a template sequence for a number of
41+
/// parallel sub specification IDs. The sub specifications are distributed among the
42+
/// pipelines.
43+
/// serves the case where each input id (subspec) corresponds to outputs amended with the
44+
/// same subspec. Two callback functions allow two configure the list of subspecs for the
45+
/// call.
46+
///
47+
/// Schematic workflow illustration:
48+
/// template pipeline parallel pipelines
49+
/// ---- ----- ----
50+
/// | A0|---->|A0 B0|---->|B0 |
51+
/// | | | C0|---->|C0 |
52+
/// | A1|---->|A1 B1|---->|B1 |
53+
/// | | | C1|---->|C1 |
54+
/// ---- ----- ----
55+
///
56+
/// ---- ----- ----
57+
/// ---- ---- ---- | A2|---->|A2 B2|---->|B2 |
58+
/// | A|---->|A B|---->|B | becomes | | | C2|---->|C2 |
59+
/// | | | C|---->|C | ======> | A3|---->|A3 B3|---->|B3 |
60+
/// ---- ---- ---- | | | C3|---->|C3 |
61+
/// ---- ----- ----
62+
/// .
63+
/// .
64+
/// ---- ----- ----
65+
/// | An|---->|An Bn|---->|Bn |
66+
/// | | | Cn|---->|Cn |
67+
/// ---- ----- ----
68+
///
69+
/// @param specs the template to be multiplied
70+
/// @param nPipelines number of pipelines
71+
/// @param getNumberOfSubspecs callback function to return the number of subspecs
72+
/// @param getSubSpec callback function to return the subspecs at index
73+
WorkflowSpec parallelPipeline(const WorkflowSpec& specs,
74+
size_t nPipelines,
75+
std::function<size_t()> getNumberOfSubspecs,
76+
std::function<size_t(size_t)> getSubSpec);
77+
4078
/// The purpose of this helper is to duplicate an InputSpec @a original
4179
/// as many times as specified in maxIndex and to amend each instance
4280
/// by invoking amendCallback on them with their own @a id. This can be

Framework/Core/src/WorkflowSpec.cxx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include "Framework/WorkflowSpec.h"
1111
#include "Framework/DataProcessorSpec.h"
1212
#include "Framework/DataDescriptorQueryBuilder.h"
13+
#include "Framework/DataSpecUtils.h"
1314

1415
#include <cstddef>
1516
#include <functional>
@@ -49,6 +50,63 @@ WorkflowSpec parallel(WorkflowSpec specs,
4950
return results;
5051
}
5152

53+
WorkflowSpec parallelPipeline(const WorkflowSpec& specs,
54+
size_t nPipelines,
55+
std::function<size_t()> getNumberOfSubspecs,
56+
std::function<size_t(size_t)> getSubSpec)
57+
{
58+
WorkflowSpec result;
59+
size_t numberOfSubspecs = getNumberOfSubspecs();
60+
if (numberOfSubspecs < nPipelines) {
61+
// no need to create more pipelines than the number of parallel Ids, in that case
62+
// each pipeline serves one id
63+
nPipelines = numberOfSubspecs;
64+
}
65+
for (auto process : specs) {
66+
size_t index = 0;
67+
size_t inputMultiplicity = numberOfSubspecs / nPipelines;
68+
if (numberOfSubspecs % nPipelines) {
69+
inputMultiplicity += 1;
70+
}
71+
auto amendProcess = [numberOfSubspecs, nPipelines, &index, &inputMultiplicity, getSubSpec](DataProcessorSpec& spec, size_t pipeline) {
72+
auto inputs = std::move(spec.inputs);
73+
auto outputs = std::move(spec.outputs);
74+
spec.inputs.reserve(inputMultiplicity);
75+
spec.outputs.reserve(inputMultiplicity);
76+
for (size_t inputNo = 0; inputNo < inputMultiplicity; ++inputNo) {
77+
for (auto& input : inputs) {
78+
spec.inputs.push_back(input);
79+
spec.inputs.back().binding += std::to_string(inputNo);
80+
DataSpecUtils::updateMatchingSubspec(spec.inputs.back(), getSubSpec(index + inputNo));
81+
}
82+
for (auto& output : outputs) {
83+
spec.outputs.push_back(output);
84+
spec.outputs.back().binding.value += std::to_string(inputNo);
85+
spec.outputs.back().subSpec = getSubSpec(index + inputNo);
86+
}
87+
}
88+
index += inputMultiplicity;
89+
if (inputMultiplicity > numberOfSubspecs / nPipelines &&
90+
((numberOfSubspecs - index) % (nPipelines - (pipeline + 1))) == 0) {
91+
// if the remaining ids can be distributed equally among the remaining pipelines
92+
// we can decrease multiplicity
93+
inputMultiplicity = numberOfSubspecs / nPipelines;
94+
}
95+
};
96+
97+
if (nPipelines > 1) {
98+
// add multiple processes and distribute inputs among them
99+
auto amendedProcessors = parallel(process, nPipelines, amendProcess);
100+
result.insert(result.end(), amendedProcessors.begin(), amendedProcessors.end());
101+
} else if (nPipelines == 1) {
102+
// add one single process with all the inputs
103+
amendProcess(process, 0);
104+
result.push_back(process);
105+
}
106+
}
107+
return result;
108+
}
109+
52110
Inputs mergeInputs(InputSpec original,
53111
size_t maxIndex,
54112
std::function<void(InputSpec &, size_t)> amendCallback) {
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Copyright CERN and copyright holders of ALICE O2. This software is
2+
// distributed under the terms of the GNU General Public License v3 (GPL
3+
// Version 3), copied verbatim in the file "COPYING".
4+
//
5+
// See http://alice-o2.web.cern.ch/license for full licensing information.
6+
//
7+
// In applying this license CERN does not waive the privileges and immunities
8+
// granted to it by virtue of its status as an Intergovernmental Organization
9+
// or submit itself to any jurisdiction.
10+
11+
#include "Framework/InputSpec.h"
12+
#include "Framework/DataProcessorSpec.h"
13+
#include "Framework/DataSpecUtils.h"
14+
#include "Framework/ParallelContext.h"
15+
#include "Framework/runDataProcessing.h"
16+
#include "Framework/ControlService.h"
17+
#include "Framework/ParallelContext.h"
18+
#include <iostream>
19+
#include <algorithm>
20+
#include <memory>
21+
#include <unordered_map>
22+
23+
#define ASSERT_ERROR(condition) \
24+
if ((condition) == false) { \
25+
LOG(ERROR) << R"(Test condition ")" #condition R"(" failed)"; \
26+
}
27+
28+
using DataHeader = o2::header::DataHeader;
29+
using namespace o2::framework;
30+
31+
size_t nPipelines = 4;
32+
size_t nParallelChannels = 6;
33+
size_t nRolls = 1;
34+
35+
std::vector<DataProcessorSpec> defineDataProcessing(ConfigContext const&)
36+
{
37+
// define a template workflow with processors to be executed in a pipeline
38+
std::vector<DataProcessorSpec> workflowSpecs{
39+
{ "processor1",
40+
Inputs{
41+
{ "input", "TST", "TRIGGER", 0, Lifetime::Timeframe } },
42+
Outputs{
43+
{ { "output" }, "TST", "PREPROC", 0, Lifetime::Timeframe } },
44+
AlgorithmSpec{ [](ProcessingContext& ctx) {
45+
for (auto const& input : ctx.inputs()) {
46+
auto const& parallelContext = ctx.services().get<ParallelContext>();
47+
std::cout << "instance " << parallelContext.index1D() << " of " << parallelContext.index1DSize() << ": "
48+
<< *input.spec << ": " << *((int*)input.payload) << std::endl;
49+
auto const* dataheader = DataRefUtils::getHeader<o2::header::DataHeader*>(input);
50+
//auto data& = ctx.outputs().make<int>(OutputRef{"output", dataheader->subSpecification});
51+
auto& data = ctx.outputs().make<int>(Output{ "TST", "PREPROC", dataheader->subSpecification, Lifetime::Timeframe });
52+
ASSERT_ERROR(ctx.inputs().get<int>(input.spec->binding.c_str()) == parallelContext.index1D());
53+
data = parallelContext.index1D();
54+
}
55+
} } },
56+
{ "processor2",
57+
Inputs{
58+
{ "input", "TST", "PREPROC", 0, Lifetime::Timeframe } },
59+
Outputs{
60+
{ { "output" }, "TST", "DATA", 0, Lifetime::Timeframe },
61+
{ { "metadt" }, "TST", "META", 0, Lifetime::Timeframe } },
62+
AlgorithmSpec{ [](ProcessingContext& ctx) {
63+
for (auto const& input : ctx.inputs()) {
64+
auto const& parallelContext = ctx.services().get<ParallelContext>();
65+
std::cout << "instance " << parallelContext.index1D() << " of " << parallelContext.index1DSize() << ": "
66+
<< *input.spec << ": " << *((int*)input.payload) << std::endl;
67+
ASSERT_ERROR(ctx.inputs().get<int>(input.spec->binding.c_str()) == parallelContext.index1D());
68+
auto const* dataheader = DataRefUtils::getHeader<o2::header::DataHeader*>(input);
69+
// TODO: there is a bug in the API for using OutputRef, returns an rvalue which can not be bound to
70+
// lvalue reference
71+
//auto& data = ctx.outputs().make<int>(OutputRef{"output", dataheader->subSpecification});
72+
auto& data = ctx.outputs().make<int>(Output{ "TST", "DATA", dataheader->subSpecification, Lifetime::Timeframe });
73+
data = ctx.inputs().get<int>(input.spec->binding.c_str());
74+
//auto meta& = ctx.outputs().make<int>(OutputRef{"metadt", dataheader->subSpecification});
75+
auto& meta = ctx.outputs().make<int>(Output{ "TST", "META", dataheader->subSpecification, Lifetime::Timeframe });
76+
meta = dataheader->subSpecification;
77+
}
78+
} } },
79+
};
80+
81+
// create parallel pipelines from the template workflow, the number of parallel channel is defined by
82+
// nParallelChannels and is distributed among the pipelines
83+
std::vector<o2::header::DataHeader::SubSpecificationType> subspecs(nParallelChannels);
84+
std::generate(subspecs.begin(), subspecs.end(), [counter = std::make_shared<int>(0)]() { return 0x1 << (*counter)++; });
85+
workflowSpecs = parallelPipeline(workflowSpecs, nPipelines,
86+
[&subspecs]() { return subspecs.size(); },
87+
[&subspecs](size_t index) { return subspecs[index]; });
88+
89+
// define a producer process with outputs for all subspecs
90+
auto producerOutputs = [&subspecs]() {
91+
Outputs outputs;
92+
for (auto const& subspec : subspecs) {
93+
outputs.emplace_back("TST", "TRIGGER", subspec, Lifetime::Timeframe);
94+
}
95+
return outputs;
96+
};
97+
98+
// we keep the correspondence between the subspec and the instance which serves this particular subspec
99+
// this is checked in the final consumer
100+
auto checkMap = std::make_shared<std::unordered_map<o2::header::DataHeader::SubSpecificationType, int>>();
101+
workflowSpecs.emplace_back(DataProcessorSpec{
102+
"trigger",
103+
Inputs{},
104+
producerOutputs(),
105+
AlgorithmSpec{ [subspecs, checkMap, counter = std::make_shared<int>(0)](ProcessingContext& ctx) {
106+
if (*counter < nRolls) {
107+
size_t multiplicity = subspecs.size() / nPipelines;
108+
if (subspecs.size() % nPipelines) {
109+
multiplicity++;
110+
}
111+
size_t instance = 0;
112+
size_t inputRank = 0;
113+
for (size_t index = 0, end = subspecs.size(); index < end; index++) {
114+
ctx.outputs().make<int>(Output{ "TST", "TRIGGER", subspecs[index], Lifetime::Timeframe }) = instance;
115+
(*checkMap)[subspecs[index]] = instance;
116+
if (++inputRank == multiplicity) {
117+
inputRank = 0;
118+
instance++;
119+
if (instance < nPipelines && ((subspecs.size() - index - 1) % (nPipelines - instance)) == 0) {
120+
multiplicity = subspecs.size() / nPipelines;
121+
}
122+
}
123+
}
124+
(*counter)++;
125+
}
126+
if (*counter == nRolls) {
127+
ctx.services().get<ControlService>().readyToQuit(false);
128+
}
129+
} } });
130+
131+
// the final consumer
132+
workflowSpecs.emplace_back(DataProcessorSpec{
133+
"consumer",
134+
mergeInputs({ { "datain", "TST", "DATA", 0, Lifetime::Timeframe },
135+
{ "metain", "TST", "META", 0, Lifetime::Timeframe } },
136+
subspecs.size(),
137+
[&subspecs](InputSpec& input, size_t index) {
138+
DataSpecUtils::updateMatchingSubspec(input, subspecs[index]);
139+
}),
140+
Outputs(),
141+
AlgorithmSpec{ [checkMap](ProcessingContext& ctx) {
142+
for (auto const& input : ctx.inputs()) {
143+
std::cout << "consuming : " << *input.spec << ": " << *((int*)input.payload) << std::endl;
144+
auto const* dataheader = DataRefUtils::getHeader<o2::header::DataHeader*>(input);
145+
if (input.spec->binding.compare(0, 6, "datain") == 0) {
146+
ASSERT_ERROR((*checkMap)[dataheader->subSpecification] == ctx.inputs().get<int>(input.spec->binding.c_str()));
147+
}
148+
}
149+
ctx.services().get<ControlService>().readyToQuit(true);
150+
} } });
151+
152+
return workflowSpecs;
153+
}

0 commit comments

Comments
 (0)