You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Power Grid Model currently uses the row-based buffer to share the data across the C-API. For example, the memory layout of a node input buffer looks like:
Where X is a meaningful byte and O is an empty byte to align the memory.
In this way we can match the input/update/output data structs exactly as we do in the calculation core. This can deliver the performance benefits as we avoid any copies and the memory layout is exactly matching.
When we need to leave some attributes unspecified in input/update, we set the pre-defined null value defined in the place so the core knows the value should be ignored.
Problem
While this design is CPU-wise very efficient, it could be memory-wise inefficient due to several reasons:
If you have many unspecified attributes, you waste the memory to have many null.
To align the data structure, the compiler will add paddings, as shown above. Those memory will not be used at all.
There is a strong case to support columnar data buffers. We give two real-world examples of this issue.
Example of update buffer
If we have an update buffer of 1000 scenarios of 1000 sym_load, the buffer size is 24 * 1000 * 1000 = 24,000,000 bytes. However, we might only need to specify id and p_specified. If we could provide these two array separately, the buffer size in total is (8 + 4) * 1000 * 1000 = 12,000,000 bytes. The reduction on memory footprint is 50%!
Example of output buffer
If we get a result buffer of 1000 scenarios of 1000 line, the buffer size is 80 * 1000 * 1000 = 80,000,000 bytes. However, we might only need to know the loading output, not even id, since we already know the id order in the input. The buffer size is 8 * 1000 * 1000 = 8,000,000 bytes. We can save 90% of memory footprint!
Proposal
We propose to support columnar data buffers across the C-API (and further in the Python API). Both the PGM core and serialization need to support that.
C-API
We already have the dataset concept in the C-API boundary. Therefore, this feature should not have breaking change in the C-API. Concretely, we add additional functions as PGM_dataset_*_add_attribute_buffer to allow user add columnar attribute buffers to the dataset. The user can call the dataset as below:
// create dataset
PGM_MutableDataset* dataset = PGM_create_dataset_mutable(handle, "input", 0, 0);
// add row-based buffer for nodePGM_dataset_const_add_buffer(handle, dataset, "node", 5, 5, nullptr, node_buffer);
// add empty buffer for line by put nullptr in data, but with size definition// decision made: do it this way, because using nullptr is a common way in C API to communicate that the buffer does not exist yetPGM_dataset_const_add_buffer(handle, dataset, "line", 5, 5, nullptr, nullptr);
// and then add individual line attributesPGM_dataset_const_add_attribute_buffer(handle, dataset, "line", "id", line_id_buffer);
PGM_dataset_const_add_attribute_buffer(handle, dataset, "line", "r1", line_r1_buffer);
// add row buffer for sym_loadPGM_dataset_const_add_buffer(handle, dataset, "sym_load", 5, 5, nullptr, sym_load_buffer);
// the following line should return error. It is not allowed to add attribute buffer if the row-buffer is set.PGM_dataset_const_add_attribute_buffer(handle, dataset, "sym_load", "p_specified", p_buffer);
// use the dataset for further actions like initialize model, put into serialization, etc.
Python API
In the Python API, four non-breaking changes are expected.
In all the places where a PGM dataset is expected, for each component, the user should be able to also supply either a numpy structured array (returned by initialize_array) or a dictionary of numpy homogeneous arrays (e.g. {"id": [1, 2], "u_rated": [150e3, 10e3]}).
In the calculate functions, the user can put desired components and/or attributes in output_component_types. The Python wrapper needs to decide whether to create a structured array or dictionary of homogeneous arrays per component. We need to figure how maintain backwards compatibility.
In the deserialization, user can specify if they want row- or column-based arrays in the returned dataset. See decision below for more information
In the serialization, if the user gives a dataset which are all column-based, the user has to provide a dataset type because there is no way to deduce it. As long as there one row-based array in the user-provided dataset, we can still deduce the dataset type.
Decision made on step 3 (deserialization):
For deserialization we support either row or column based deserialization (function argument: Enum). If a user wants to deserialize to columnar data the default is to deserialize all data present. A user can give an Optional function argument to specify the desired components and attributes. In that case, deserialization + a filter (for the specific components and attributes) is happening. Let's call this Optional function argument filter. Make sure this behavior is documented well + document that providing a filter might result in loss of data.
Make id optional for batch update dataset in columnar format
From the user's perspective, the user would definitely like to provide a columnar batch dataset in a way that the id is not provided for a certain component. In that case, it should be inferred that the elements where attributes are to be updated via columnar buffer are in the exact same sequence of the input data. This is a realistic use-case and will be appreciated by the user, to save the additional step to just assign the exactly the same id as in the input data. The following Python code should work:
In the new Dataset, add buffer control and iteration functionality. It can detect if a component buffer is row or column based, and in case of column based, generate temporary object to have the full struct for MainModel to consume.
In Serializer, it should directly read the row and column based buffer and serialize them to msgpack and json.
In Deserializer, it should write the attributes either in row- or column-based depending on what buffers are set in the WritableDataset.
Make id optional in update dataset: in the main core, we need to have special treatment in is_update_independent to make id as optional attribute in the batch update dataset.
is_update_independent should be per component instead of the whole dataset. So we can allow individual sequence for each component.
For a certain component, if the buffer is row-based
If the id of the row-based buffer is not all NaN, we use the current logic to determine if the component is independent.
If the id of the row-based buffer is all NaN
If the buffer is not uniform, or the buffer is uniform but elements_per_scenario is not the same as the number of elements in the input data (in the model). An error should be raised.
If the above check passes, we assume the component buffer is independent. And we generate a sequence from 0 to n_comp for this component. This will be consumed by the update function so the update function does not do id lookup.
For a certain component, if the buffer is columnar, we do the following:
If id attribute buffer is provided and it is not all NaN, we look at id to judge if the component is independent or not. We do not need to create proxy stuff which is waste of time. Just directly look at id buffer.
If id attribute buffer is not provided or if the id is provided but they are all NaN:
If the buffer is not uniform, or the buffer is uniform but elements_per_scenario is not the same as the number of elements in the input data (in the model). An error should be raised.
If the above check passes, we assume the component buffer is independent. And we generate a sequence from 0 to n_comp for this component. This will be consumed by the update function so the update function does not do id lookup.
Background
Power Grid Model currently uses the row-based buffer to share the data across the C-API. For example, the memory layout of a node input buffer looks like:
Where
Xis a meaningful byte andOis an empty byte to align the memory.In this way we can match the input/update/output data structs exactly as we do in the calculation core. This can deliver the performance benefits as we avoid any copies and the memory layout is exactly matching.
When we need to leave some attributes unspecified in input/update, we set the pre-defined
nullvalue defined in the place so the core knows the value should be ignored.Problem
While this design is CPU-wise very efficient, it could be memory-wise inefficient due to several reasons:
null.There is a strong case to support columnar data buffers. We give two real-world examples of this issue.
Example of update buffer
If we have an update buffer of 1000 scenarios of 1000
sym_load, the buffer size is24 * 1000 * 1000 = 24,000,000bytes. However, we might only need to specifyidandp_specified. If we could provide these two array separately, the buffer size in total is(8 + 4) * 1000 * 1000 = 12,000,000bytes. The reduction on memory footprint is 50%!Example of output buffer
If we get a result buffer of 1000 scenarios of 1000
line, the buffer size is80 * 1000 * 1000 = 80,000,000bytes. However, we might only need to know theloadingoutput, not evenid, since we already know theidorder in the input. The buffer size is8 * 1000 * 1000 = 8,000,000bytes. We can save 90% of memory footprint!Proposal
We propose to support columnar data buffers across the C-API (and further in the Python API). Both the PGM core and serialization need to support that.
C-API
We already have the
datasetconcept in the C-API boundary. Therefore, this feature should not have breaking change in the C-API. Concretely, we add additional functions asPGM_dataset_*_add_attribute_bufferto allow user add columnar attribute buffers to the dataset. The user can call the dataset as below:Python API
In the Python API, four non-breaking changes are expected.
initialize_array) or a dictionary of numpy homogeneous arrays (e.g.{"id": [1, 2], "u_rated": [150e3, 10e3]}).output_component_types. The Python wrapper needs to decide whether to create a structured array or dictionary of homogeneous arrays per component. We need to figure how maintain backwards compatibility.Decision made on step 3 (deserialization):
For deserialization we support either row or column based deserialization (function argument: Enum). If a user wants to deserialize to columnar data the default is to deserialize all data present. A user can give an Optional function argument to specify the desired components and attributes. In that case, deserialization + a filter (for the specific components and attributes) is happening. Let's call this Optional function argument
filter. Make sure this behavior is documented well + document that providing a filter might result in loss of data.Make id optional for batch update dataset in columnar format
From the user's perspective, the user would definitely like to provide a columnar batch dataset in a way that the
idis not provided for a certain component. In that case, it should be inferred that the elements where attributes are to be updated via columnar buffer are in the exact same sequence of the input data. This is a realistic use-case and will be appreciated by the user, to save the additional step to just assign the exactly the sameidas in the input data. The following Python code should work:Implementation Proposal
To make this feature possible, following implementation suggestions are proposed in the C++ core:
DatasetHandlertoDataset.Dataset.Dataset, add buffer control and iteration functionality. It can detect if a component buffer is row or column based, and in case of column based, generate temporary object to have the full struct forMainModelto consume.MainModelto use the newDataset. This also relates to Remove the Dataset logic from PGM core, use DatasetHandler for MainModel #431.Serializer, it should directly read the row and column based buffer and serialize them tomsgpackandjson.Deserializer, it should write the attributes either in row- or column-based depending on what buffers are set in theWritableDataset.is_update_independentto makeidas optional attribute in the batch update dataset.is_update_independentshould be per component instead of the whole dataset. So we can allow individualsequencefor each component.idof the row-based buffer is not allNaN, we use the current logic to determine if the component is independent.idof the row-based buffer is allNaNelements_per_scenariois not the same as the number of elements in the input data (in the model). An error should be raised.sequencefrom0ton_compfor this component. This will be consumed by the update function so the update function does not doidlookup.idattribute buffer is provided and it is not allNaN, we look atidto judge if the component is independent or not. We do not need to create proxy stuff which is waste of time. Just directly look atidbuffer.idattribute buffer is not provided or if theidis provided but they are allNaN:elements_per_scenariois not the same as the number of elements in the input data (in the model). An error should be raised.sequencefrom0ton_compfor this component. This will be consumed by the update function so the update function does not doidlookup.