Skip to content

Commit b05171d

Browse files
ADIOS2: Flush to disk within a step (#1207)
* Add backendConfig parameter to flush call Parse string JSON config into nlohmann representation * ADIOS2 implementation: preferred_flush_target parameter * Testing * Documentation * Add override parameters, update tests and docs Also, use fstat in tests rather than <filesystem> * Flush attributes before PerformDataWrites() * Don't write buffered attributes upon PerformDataWrites There is no advantage from doing this, readers will only see the data after EndStep anyway, but there's the disadvantage that attributes cannot be overwritten within this step anymore * Use images from PR thread
1 parent ca451b5 commit b05171d

35 files changed

Lines changed: 769 additions & 55 deletions

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,10 +469,12 @@ set(CORE_SOURCE
469469
src/benchmark/mpi/OneDimensionalBlockSlicer.cpp
470470
src/helper/list_series.cpp)
471471
set(IO_SOURCE
472+
src/IO/AbstractIOHandler.cpp
472473
src/IO/AbstractIOHandlerImpl.cpp
473474
src/IO/AbstractIOHandlerHelper.cpp
474475
src/IO/DummyIOHandler.cpp
475476
src/IO/IOTask.cpp
477+
src/IO/FlushParams.cpp
476478
src/IO/HDF5/HDF5IOHandler.cpp
477479
src/IO/HDF5/ParallelHDF5IOHandler.cpp
478480
src/IO/HDF5/HDF5Auxiliary.cpp

docs/source/backends/adios2.rst

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,18 @@ This buffer is drained to storage only at specific times:
130130

131131
The usage pattern of openPMD, especially the choice of iteration encoding influences the memory use of ADIOS2.
132132
The following graphs are created from a real-world application using openPMD (PIConGPU) using KDE Heaptrack.
133-
Ignore the 30GB initialization phases.
133+
134+
BP4 file engine
135+
***************
136+
137+
The internal data structure of BP4 is one large buffer that holds all data written by a process.
138+
It is drained to the disk upon ending a step or closing the engine (in parallel applications, data will usually be aggregated at the node-level before this).
139+
This approach enables a very high IO performance by requiring only very few, very large IO operations, at the cost of a high memory consumption and some common usage pitfalls as detailed below:
134140

135141
* **file-based iteration encoding:** A new ADIOS2 engine is opened for each iteration and closed upon ``Iteration::close()``.
136142
Each iteration has its own buffer:
137143

138-
.. image:: ./memory_filebased.png
144+
.. figure:: https://user-images.githubusercontent.com/14241876/181477396-746ee21d-6efe-450b-bb2f-f53d49945fb9.png
139145
:alt: Memory usage of file-based iteration encoding
140146

141147
* **variable-based iteration encoding and group-based iteration encoding with steps**:
@@ -147,19 +153,100 @@ Ignore the 30GB initialization phases.
147153
These memory spikes can easily lead to out-of-memory (OOM) situations, motivating that the ``InitialBufferSize`` should not be chosen too small.
148154
Both behaviors are depicted in the following two pictures:
149155

150-
.. image:: ./memory_variablebased.png
156+
.. figure:: https://user-images.githubusercontent.com/14241876/181477405-0439b017-256b-48d6-a169-014b3fe3aeb3.png
151157
:alt: Memory usage of variable-based iteration encoding
152158

153-
.. image:: ./memory_variablebased_initialization.png
159+
.. figure:: https://user-images.githubusercontent.com/14241876/181477406-f6e2a173-2ec1-48df-a417-0cb97a160c91.png
154160
:alt: Memory usage of variable-based iteration encoding with bad ``InitialBufferSize``
155161

156162
* **group-based iteration encoding without steps:**
157163
This encoding **should be avoided** in ADIOS2.
158164
No data will be written to disk before closing the ``Series``, leading to a continuous buildup of memory, and most likely to an OOM situation:
159165

160-
.. image:: ./memory_groupbased_nosteps.png
166+
.. figure:: https://user-images.githubusercontent.com/14241876/181477397-4d923061-7051-48c4-ae3a-a9efa10dcac7.png
161167
:alt: Memory usage of group-based iteration without using steps
162168

169+
SST staging engine
170+
******************
171+
172+
Like the BP4 engine, the SST engine uses one large buffer as an internal data structure.
173+
174+
Unlike the BP4 engine, however, a new buffer is allocated for each IO step, leading to a memory profile with clearly distinct IO steps:
175+
176+
.. figure:: https://user-images.githubusercontent.com/14241876/181477403-7ed7810b-dedf-48b8-b17b-8ce89fd3c34a.png
177+
:alt: Ideal memory usage of the SST engine
178+
179+
The SST engine performs all IO asynchronously in the background and releases memory only as soon as the reader is done interacting with an IO step.
180+
With slow readers, this can lead to a buildup of past IO steps in memory and subsequently to an out-of-memory condition:
181+
182+
.. figure:: https://user-images.githubusercontent.com/14241876/181477400-f342135f-612e-464f-b0e7-c1978ef47a94.png
183+
:alt: Memory congestion in SST due to a slow reader
184+
185+
This can be avoided by specifying the `ADIOS2 parameter <https://adios2.readthedocs.io/en/latest/engines/engines.html#bp5>`_ ``QueueLimit``:
186+
187+
.. code:: cpp
188+
189+
std::string const adios2Config = R"(
190+
{"adios2": {"engine": {"parameters": {"QueueLimit": 1}}}}
191+
)";
192+
Series series("simData.sst", Access::CREATE, adios2Config);
193+
194+
By default, the openPMD-api configures a queue limit of 2.
195+
Depending on the value of the ADIOS2 parameter ``QueueFullPolicy``, the SST engine will either ``"Discard"`` steps or ``"Block"`` the writer upon reaching the queue limit.
196+
197+
BP5 file engine
198+
***************
199+
200+
The BP5 file engine internally uses a linked list of equally-sized buffers.
201+
The size of each buffer can be specified up to a maximum of 2GB with the `ADIOS2 parameter <https://adios2.readthedocs.io/en/latest/engines/engines.html#bp5>`_ ``BufferChunkSize``:
202+
203+
.. code:: cpp
204+
205+
std::string const adios2Config = R"(
206+
{"adios2": {"engine": {"parameters": {"BufferChunkSize": 2147381248}}}}
207+
)";
208+
Series series("simData.bp5", Access::CREATE, adios2Config);
209+
210+
This approach implies a sligthly lower IO performance due to more frequent and smaller writes, but it lets users control memory usage better and avoids out-of-memory issues when configuring ADIOS2 incorrectly.
211+
212+
The buffer is drained upon closing a step or the engine, but draining to the filesystem can also be triggered manually.
213+
In the openPMD-api, this can be done by specifying backend-specific parameters to the ``Series::flush()`` or ``Attributable::seriesFlush()`` calls:
214+
215+
.. code:: cpp
216+
217+
series.flush(R"({"adios2": {"preferred_flush_target": "disk"}})")
218+
219+
The memory consumption of this approach shows that the 2GB buffer is first drained and then recreated after each ``flush()``:
220+
221+
.. figure:: https://user-images.githubusercontent.com/14241876/181477392-7eff2020-7bfb-4ddb-b31c-27b9937e088a.png
222+
:alt: Memory usage of BP5 when flushing directly to disk
223+
224+
.. note::
225+
226+
KDE Heaptrack tracks the **virtual memory** consumption.
227+
While the BP4 engine uses ``std::vector<char>`` for its internal buffer, BP5 uses plain ``malloc()`` (hence the 2GB limit), which does not initialize memory.
228+
Memory pages will only be allocated to physical memory upon writing.
229+
In applications with small IO sizes on systems with virtual memory, the physical memory usage will stay well below 2GB even if specifying the BufferChunkSize as 2GB.
230+
231+
**=> Specifying the buffer chunk size as 2GB as shown above is a good idea in most cases.**
232+
233+
Alternatively, data can be flushed to the buffer.
234+
Note that this involves data copies that can be avoided by either flushing directly to disk or by entirely avoiding to flush until ``Iteration::close()``:
235+
236+
.. code:: cpp
237+
238+
series.flush(R"({"adios2": {"preferred_flush_target": "buffer"}})")
239+
240+
With this strategy, the BP5 engine will slowly build up its buffer until ending the step.
241+
Rather than by reallocation as in BP4, this is done by appending a new chunk, leading to a clearly more acceptable memory profile:
242+
243+
.. figure:: https://user-images.githubusercontent.com/14241876/181477384-ce4ea8ab-3bde-4210-991b-2e627dfcc7c9.png
244+
:alt: Memory usage of BP5 when flushing to the engine buffer
245+
246+
The default is to flush to disk, but the default ``preferred_flush_target`` can also be specified via JSON/TOML at the ``Series`` level.
247+
248+
249+
163250

164251
Known Issues
165252
------------
-13.1 KB
Binary file not shown.
-27.9 KB
Binary file not shown.
-11.3 KB
Binary file not shown.
-13.4 KB
Binary file not shown.

docs/source/details/backendconfig.rst

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ The configuration string may refer to the complete ``openPMD::Series`` or may ad
3737
This reflects the fact that certain backend-specific parameters may refer to the whole Series (such as storage engines and their parameters) and others refer to actual datasets (such as compression).
3838
Dataset-specific configurations are (currently) only available during dataset creation, but not when reading datasets.
3939

40-
A JSON/TOML configuration may either be specified as an inline string that can be parsed as a JSON/TOML object, or alternatively as a path to a JSON/TOML-formatted text file (only in the constructor of ``openPMD::Series``):
40+
Additionally, some backends may provide different implementations to the ``Series::flush()`` and ``Attributable::flushSeries()`` calls.
41+
JSON/TOML strings may be passed to these calls as optional parameters.
42+
43+
A JSON/TOML configuration may either be specified as an inline string that can be parsed as a JSON/TOML object, or alternatively as a path to a JSON/TOML-formatted text file (only in the constructor of ``openPMD::Series``, all other API calls that accept a JSON/TOML specification require in-line datasets):
4144

4245
* File paths are distinguished by prepending them with an at-sign ``@``.
4346
JSON and TOML are then distinguished by the filename extension ``.json`` or ``.toml``.
@@ -119,6 +122,16 @@ Explanation of the single keys:
119122
Please refer to the `official ADIOS2 documentation <https://adios2.readthedocs.io/en/latest/engines/engines.html>`_ for the available engine parameters.
120123
The openPMD-api does not interpret these values and instead simply forwards them to ADIOS2.
121124
* ``adios2.engine.usesteps``: Described more closely in the documentation for the :ref:`ADIOS2 backend<backends-adios2>` (usesteps).
125+
* ``adios2.engine.preferred_flush_target`` Only relevant for BP5 engine, possible values are ``"disk"`` and ``"buffer"`` (default: ``"disk"``).
126+
127+
* If ``"disk"``, data will be moved to disk on every flush.
128+
* If ``"buffer"``, then only upon ending an IO step or closing an engine.
129+
130+
This behavior can be overridden on a per-flush basis by specifying this JSON/TOML key as an optional parameter to the ``Series::flush()`` or ``Attributable::seriesFlush()`` methods.
131+
132+
Additionally, specifying ``"disk_override"`` or ``"buffer_override"`` will take precedence over options specified without the ``_override`` suffix, allowing to invert the normal precedence order.
133+
This way, a data producing code can hardcode the preferred flush target per ``flush()`` call, but users can e.g. still entirely deactivate flushing to disk in the ``Series`` constructor by specifying ``preferred_flush_target = buffer_override``.
134+
This is useful when applying the asynchronous IO capabilities of the BP5 engine.
122135
* ``adios2.dataset.operators``: This key contains a list of ADIOS2 `operators <https://adios2.readthedocs.io/en/latest/components/components.html#operator>`_, used to enable compression or dataset transformations.
123136
Each object in the list has two keys:
124137

docs/source/usage/workflow.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,12 @@ Attributes are (currently) unaffected by this:
6363

6464
* In writing, attributes are stored internally by value and can afterwards not be accessed by the user.
6565
* In reading, attributes are parsed upon opening the Series / an iteration and are available to read right-away.
66+
67+
.. attention::
68+
69+
Note that the concrete implementation of ``Series::flush()`` and ``Attributable::seriesFlush()`` is backend-specific.
70+
Using these calls does neither guarantee that data is moved to storage/transport nor that it can be accessed by independent readers at this point.
71+
72+
Some backends (e.g. the BP5 engine of ADIOS2) have multiple implementations for the openPMD-api-level guarantees of flush points.
73+
For user-guided selection of such implementations, ``Series::flush`` and ``Attributable::seriesFlush()`` take an optional JSON/TOML string as a parameter.
74+
See the section on :ref:`backend-specific configuration <backendconfig>` for details.

include/openPMD/IO/ADIOS/ADIOS1IOHandler.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class OPENPMDAPI_EXPORT ADIOS1IOHandler : public AbstractIOHandler
5050
return "ADIOS1";
5151
}
5252

53-
std::future<void> flush(internal::FlushParams const &) override;
53+
std::future<void> flush(internal::ParsedFlushParams &) override;
5454

5555
void enqueue(IOTask const &) override;
5656

@@ -72,7 +72,7 @@ class OPENPMDAPI_EXPORT ADIOS1IOHandler : public AbstractIOHandler
7272
return "DUMMY_ADIOS1";
7373
}
7474

75-
std::future<void> flush(internal::FlushParams const &) override;
75+
std::future<void> flush(internal::ParsedFlushParams &) override;
7676

7777
private:
7878
std::unique_ptr<ADIOS1IOHandlerImpl> m_impl;

include/openPMD/IO/ADIOS/ADIOS2IOHandler.hpp

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include "openPMD/IO/AbstractIOHandler.hpp"
2727
#include "openPMD/IO/AbstractIOHandlerImpl.hpp"
2828
#include "openPMD/IO/AbstractIOHandlerImplCommon.hpp"
29+
#include "openPMD/IO/FlushParametersInternal.hpp"
2930
#include "openPMD/IO/IOTask.hpp"
3031
#include "openPMD/IO/InvalidatableFile.hpp"
3132
#include "openPMD/IterationEncoding.hpp"
@@ -140,7 +141,7 @@ class ADIOS2IOHandlerImpl
140141

141142
~ADIOS2IOHandlerImpl() override;
142143

143-
std::future<void> flush(internal::FlushParams const &);
144+
std::future<void> flush(internal::ParsedFlushParams &);
144145

145146
void
146147
createFile(Writable *, Parameter<Operation::CREATE_FILE> const &) override;
@@ -209,6 +210,16 @@ class ADIOS2IOHandlerImpl
209210
*/
210211
adios2::Mode adios2AccessMode(std::string const &fullPath);
211212

213+
enum class FlushTarget : unsigned char
214+
{
215+
Buffer,
216+
Buffer_Override,
217+
Disk,
218+
Disk_Override
219+
};
220+
221+
FlushTarget m_flushTarget = FlushTarget::Disk;
222+
212223
private:
213224
adios2::ADIOS m_ADIOS;
214225
/*
@@ -412,6 +423,7 @@ namespace ADIOS2Defaults
412423
constexpr const_str str_type = "type";
413424
constexpr const_str str_params = "parameters";
414425
constexpr const_str str_usesteps = "usesteps";
426+
constexpr const_str str_flushtarget = "preferred_flush_target";
415427
constexpr const_str str_usesstepsAttribute = "__openPMD_internal/useSteps";
416428
constexpr const_str str_adios2Schema =
417429
"__openPMD_internal/openPMD2_adios2_schema";
@@ -927,6 +939,8 @@ namespace detail
927939
friend struct BufferedGet;
928940
friend struct BufferedPut;
929941

942+
using FlushTarget = ADIOS2IOHandlerImpl::FlushTarget;
943+
930944
BufferedActions(BufferedActions const &) = delete;
931945

932946
/**
@@ -1039,10 +1053,26 @@ namespace detail
10391053
template <typename BA>
10401054
void enqueue(BA &&ba, decltype(m_buffer) &);
10411055

1056+
struct ADIOS2FlushParams
1057+
{
1058+
/*
1059+
* Only execute performPutsGets if UserFlush.
1060+
*/
1061+
FlushLevel level;
1062+
FlushTarget flushTarget = FlushTarget::Disk;
1063+
1064+
ADIOS2FlushParams(FlushLevel level_in) : level(level_in)
1065+
{}
1066+
1067+
ADIOS2FlushParams(FlushLevel level_in, FlushTarget flushTarget_in)
1068+
: level(level_in), flushTarget(flushTarget_in)
1069+
{}
1070+
};
1071+
10421072
/**
10431073
* Flush deferred IO actions.
10441074
*
1045-
* @param level Flush Level. Only execute performPutsGets if UserFlush.
1075+
* @param flushParams Flush level and target.
10461076
* @param performPutsGets A functor that takes as parameters (1) *this
10471077
* and (2) the ADIOS2 engine.
10481078
* Its task is to ensure that ADIOS2 performs Put/Get operations.
@@ -1057,7 +1087,7 @@ namespace detail
10571087
*/
10581088
template <typename F>
10591089
void flush(
1060-
FlushLevel level,
1090+
ADIOS2FlushParams flushParams,
10611091
F &&performPutsGets,
10621092
bool writeAttributes,
10631093
bool flushUnconditionally);
@@ -1067,7 +1097,7 @@ namespace detail
10671097
* and does not flush unconditionally.
10681098
*
10691099
*/
1070-
void flush(FlushLevel, bool writeAttributes = false);
1100+
void flush(ADIOS2FlushParams, bool writeAttributes = false);
10711101

10721102
/**
10731103
* @brief Begin or end an ADIOS step.
@@ -1265,7 +1295,8 @@ class ADIOS2IOHandler : public AbstractIOHandler
12651295
// we must not throw in a destructor
12661296
try
12671297
{
1268-
this->flush(internal::defaultFlushParams);
1298+
auto params = internal::defaultParsedFlushParams;
1299+
this->flush(params);
12691300
}
12701301
catch (std::exception const &ex)
12711302
{
@@ -1304,6 +1335,6 @@ class ADIOS2IOHandler : public AbstractIOHandler
13041335
return "ADIOS2";
13051336
}
13061337

1307-
std::future<void> flush(internal::FlushParams const &) override;
1338+
std::future<void> flush(internal::ParsedFlushParams &) override;
13081339
}; // ADIOS2IOHandler
13091340
} // namespace openPMD

0 commit comments

Comments
 (0)