I use vtkm a lot for all kinds of projects, this article includes some tips and thoughts about using the vtkm from the perspective of software design.
The original paper of vtkm is this one
Pattens that the algorithm follows for parallel running is this one:
Most of the topics can be found under the change log repo here.
One important thing is to figure out how to set the
ExecutionSignature and parameters of operator function in the worklet type. It seems that all worklet in vtkm style follows this style.
There are some detailed explanatio in the vtkm userguide 17.1-17.4, the parameters specified by exeutionSignature shows which parameters are chosen to execute in the exec env. Parameters specified in the operator function should match with the type of parameters specified in exec env (including the type of the template parameter). It seems that the template parameters can be reused? Not sure how it works here, but it works. If two parameters use the same type, we only need one input template parameter.
If we just use the worklet based on specific data sets or array, the things described about can satisfy our requirments, and we need to create the dispatcher separately. Then call the invoker function of based on worklet.
Another level of abstraction is the filter. we need to send the data in and return the data and hidden the details. There are multiple filters in vtkm, the base class is sth like
vtkm::filter::FilterField, we can call the invoke function direactly if the filter is inherited from that base class, this is an example. We inherite from that base class and call
The filter base class provids more simple things to do the dispatcher, invoke and detect the type of input and output type.
The case of multiple input and the multiple output.
The core idea is the type tranform here is to let the code can work on different types. The strategies used in vtkm is the runtime transform. The idea is that the deduce the type of a particular field, and then they use that field as the template parameter.
How to deduce the type of a specific array in runtime? The idea is specify a type list and then compare your type with that list to see which is accurate one. In this array handle there are all kinds of functions start with
CastAndCallFor... this can be used at different scenarios. The deduced array type is usually set as an input variable of a lambda expression.
There are different versions of the castandcall function, it will cast the input into the specific type. There is actually a tradeoff between known types and the actual type. If we do not put limitation on input type, there might be too many template generated when creating the actual implementation. Different cast and call type can add these assumptions. For example,
CastAndCallScalarField is only works for the types in
If you really want your function to work with all types, you can use a different version of CastAndCall that will check for all types. It would look something like this:
We could always refer to implementaitons of vtkm filters to see how the cast and call are used in different scenarios.
If there are multiple variable, the solution of the vtkm is to use the type of specific variable to infer the type of other type. They have an function called
ArrayCopyShallowIfPossible, the typical scenario is sth like this:
//use decay_t to remove the const qulifier
If they have the same type, the do the shallow copy, the
T is the deduced type.
Another commonly used function to convert the array type is the
AsArrayHandle, if we search this keyword on the gitlab page of the vtkm, there are all kinds of examples and test case
In this file that defines the ArrayHandle, it defines specific behaviours for the
AsArrayHandle, the idea is to use the
decltype to get the target type and to check if it is ok to do the conversion, if it is ok, we then use the
reinterpret_cast to do the conversion. For the cellset, there are also some function such as
AsCellSet to cast the unknown cellset into the specific one.
This question discusses some details regarding why use this kind of general type. The interface want to be desgined as general as possible, for example such as get cell set in vtkm, it can be desgined to have a get cell set function for each pecific type of cell set, it needs to have some kind of generality. We may imagine the unknown cell set or array type is a kind of interface for the concrete member.
There are multiple options to do this type conversion in VTK-m, the better ones depend on the situation.
CastAndCallForTypes needs to consider both the element type and storage type of the arrayhandle. Just looking at the Section about the customized array handle. For arrayHandle, it is generalized in two aspects, one is the basic type such as int, double etc. Another is underlying storage type such as basic and SOA array. The StorageList.h in vtkm repo contains associated types.
ListForEach just need to get the base component type of the array. We do not need to consider the storage type for this. It needs to be used together with the ExtractComponent. One specific documentation can be found here (https://gitlab.kitware.com/vtk/vtk-m/-/blob/master/docs/changelog/1.6/release-notes.md).
CompositeVector is a good use case for thie ExtractComponent. It is really flexible and the output array can be put into a dynamic array handle. But I am still not quite sure about how to use these things properly.
One thing that contains a steep learning curve is the arrayhandle, since vtkm maintain there own version of arrayhandle, and all other datasets are built over this arary handle, getting familiar with its API is important to use vtkm related operations flexibally.
Write data and get data. getting and writting data need to through the WritePortal and GetPortal, maybe the goal of this is to hide the different underlying data structure, such as CPU array and GPU array. Be careful, it is time consuming to get get/write portal multiple times in the for loop.
The arrayHandle is just a pointer essentially, so the shallow copy is eveywhere (even for the dataset based on the arrayhandle) in vtkm unless the explicit copy operation is called.
Checking the type of the value stored in the unknown array handle :
UnknownArrayHandle::ExtractComponent will return an
ArrayHandleStride for a particular component. This allows you to get the data, usually without a copy, by just looking at the base component type (check
UnknownArrayHandle::IsBaseComponentType<T>()). For this, you only have to check the basic C types of the components (i.e.,
int, etc. a.k.a.
vtkm::Int32, etc.). That is a tractable set of types to check, which is why we have this feature.
AOS or SOA array.
The idea is the combination between the lambda expression and the type deduction.
This is a typical exmaple
CastAndCallVecField can get the correct type.
Another case is that we do not know the type. It will change the concrete type in the lambda expression. such as this example, the
CastAndCallForTypesWithFloatFallback will do the actual cast tranform and it will try to compare with the list defined in the first parameter and to see wich inner type can match. Since when we call the GetData, it returns the unknown type. The common use case scenarios is to use the input data type to deduct the output type.
In the lambda expression, it usually declares a dispatcher and then use the Invoke function of the dispatcher to call the worklet, the code in this part follows a similar structure.
More detialed about the type case is described in the previous section. Be careful about the dynamic type conversion here. In one previous code, I do things like this:
auto inField1 = inData.GetField("field1")
In this way, we did not actually cast the field to specific concrete type, the field will do some innter operation to compare at least 10 types in vtkm, so there are totally 10^3 combinations, which cause really long compiling time. The good practive is always to use cast and call to convert the field into the fixed type based on some assumptions. Or at least we compare the type with all possible type for one field, and then for other field, we can assume they have same field with the first one based on
ShallowCopyIfPossible, the example can be found in the previous paragraph.
Although there are flexible libraries in vtkm to imput and output data based on the vtk format, sometimes it is conveneint to build data direactly for simple testing, such as building a structured data. The assocaited code is at the vtkm/cont/DataSetBuilderxx, there are all kinds of exmaples such as build uniform and rectilinear example. We can refer to these examples to see how to add coordinates and cellset when we need to create a new vtkm dataset for specific goals.
Or so called parallel skeletons:
The general types of the gpu primitives:
Map (scan operation or add operation, stencil is also a kind of operator)
Reduce (output is a single value, sum)
Scan (stor intermidiate results)
Sort (reordering operation)
Search (find index)
One paper regarding the VTKM design, the idea of using the parallel primitives (https://cdux.cs.uoregon.edu/pubs/MorelandPARCO.pdf)
If you try to find the array function in vtkm data set, you might doing the wrong things. Most of the function can be achieved by just doing the shallow copy. The array handle copy is also the shallow copy case. Always trying to ask yourself, if the deep copy is necessary when you want to use them.
Still not sure how it works, what are connection between these parallel primitives and the upper level operations
Classifiation different filter types from different categories is located in this table:
field_transform transform fields from/to the same entity
field_conversion does sth cell to pointer or pointer to cell converion
There is no direact way to do this things, we need to getdata firstly, then use the AsArrayHanle to transfer to dedicated handle then get the ReadPortal, then based on this ReadPortal or write Portal, we can read or write data. This may looks like a little bit redoundant, but it is how a framework works, you need to do things following its convention.
Using DataSet::GetField with field name and association instead of GetPoint or GetField when getting a specific array.
The initilization is interesting for vtkm, it needs to be at the place before the real operation to parse the argument in the program. The vtkm will remove the arguments started with the
--vtkm-sth, so we need to set the initilization before the actual operation to parse the argument in the program. The log level can be set based on the
--vtkm-log-level, different name of the log are specified here, one useful things I know recently if the
Kern log, it will print out a lot of kernel details, such as the cuda call etc, which is helpful for understanding these parallel details.
There currently is no direct way to query on which device something ran on. One way you can do this is to turn on the logging level to Perf, but that will print out a lot of info that will be difficult to discern.
What I usually do is force a particular device. That way VTK-m will run on that device or else it will raise an error. The easiest way to force a device is to add a
--vtkm-device=openmp command line argument (that gets passed to vtkm::cont::Initialize). Alternately, you can use ScopedRuntimeDeviceTracker to force a particular device (https://docs-m.vtk.org/latest/structvtkm_1_1cont_1_1ScopedRuntimeDeviceTracker.html).
Essentially, the vtkm is just a library for other dedicated service. Its location is a service which can be integrated into other service such as paraview.
Another way is just try to use the ForceDevice to set the backend as the expected one, such as this (https://github.com/Kitware/VTK-m/blob/master/examples/multi_backend/MultiDeviceGradient.cxx#L60)
This is a example to use the initialization in vtkm (
Pay attention, if we set the backend as the openmp, and use the mpirun to run the program, the default case is that there will be one core in the program even for local env, this is a good we to bind multiple physical cores to one process/task
mpirun -np ncores --bind-to none -x OMP_NUM_THREADS=nthreads ./program
Same with the device adaptor, it is recommended to use the vtkm timer when measure the performance of specific filter. Otherwise, the timing is not accurate, maybe force the timer to be set as the default pattern when initilize the timer. Especially for the cuda device.
In parallel programming, the debug operations is a little bit tricky. In vtkm, we can always start from the serial backend to do the debug operation. If the data set is large, there are also two strategeis, one is to generate small synthetic data set, another is to print out the results of the communication for specific thread. We can use the inner type workindex to index the thread. Each thread has a unique id. For the case of visitCellByPoints, the
WorkIndex can label the spefic position of the cell, for example, the 0th id process the first cell, so on so forth. another convenient tag is the
InputIndex, it produces a vtkm::Id that identifies the index of the input element, which can differ from the
WorkIndex in a worklet with a scatter.
In addition, if we want to access the coordinates of the points in the worklet, the point coordinates can be made available by having a
FieldInPoint input and passing the point coordinates array.
When we set the vtk as debug mode, it might decrease the performance since it will add extra assertion operations for arrayhandle, check this place (https://github.com/Kitware/VTK-m/blob/master/CMakeLists.txt#L137) to get more details.
Here we can see that the performance can increase a lot if we do not use extra VTKM assert
This is also really common for the c programming, if we use the assert operation in the program, these assert operation only works when we use the DEBUG mode to complie the program.
In CUDA, a function or method must be declared with the
__device__ modifier to run on the CUDA device. This tells the compiler to generate the machine code that the GPU understands. This
__device__ is hidden in the
VTKM_EXEC_CONT in VTK-m. A function running on a device can only call other functions that run on the device. The worklet, which runs on the device, is trying to call functions/methods in the Eigen library that are not marked
For example, there is
hidden attribute without using the exec tag.
These configurations are recomended to be set in the initilization function of the vtkm call. There are other operations to set the backend and the log levels, such as the ForceDevice function.
The project management thing is maintained in this repo
Since this project includes multiple organizations, they use the open issue and associated labels to show the key functionality and the working progress, it is really clear. However, it might be good to link each specific item with specific gitlab issue.
Another good thing is the CI/CD operation, once there is a new commit, the cdash is triggered and there are detailed results checking, such as the compiling warnings, the memory leak checking (compiler on different platforms have different warning complains) It is good to fix all these kind of issues before code merging.
Originally, the vtkm only focuses on the parallel primitives on single node. There is a vtkh library calles the vtkm library and in charge of MPI program based on MPI program. It turns out that there introduce lots of depedency issue, some cases such as streamline need to handle all level of parallalism. So for the currnet vtkm, when designing a filter from vtkm’s perspective, both the parallelism from the single node and multiple nodes’s perspective need to be considered.
The vtkm’s compiling process adopts similar pattern with the vtk’s pattern it adopts a moduler system to assist the compiling and linking process. There are several key word under the vtkm.module. Details can be found here (https://gitlab.kitware.com/vtk/vtk-m/-/blob/master/docs/Modules.md)
Once time, I try to use the Threshold and ThresholdPoints filter in vtkm. However, the output data set is not same with what I expected. The reason is that I forget to use the
SetCompactPoints(true) without setting this parameter, the results are not the one we want.
The filter base class provides a lot of common mechanisms used by filter. The common thing is set active field, that can set which field to apply on. The filter selection mechanism is also useful, it can control how to map the original data set to the final data set. It can control which field need to be skiped.
Coordinates system is just a common filter. We can then label that filter as a coordinates. When we call the addCoordinates system, we just label a specific field into a coordinates system.
At somepoints, we need to understand the array handle in detail from scratch to write the filter algorithm in more efficient way. TODO
Remember setting export label
Remember adding .cxx file into the CMakeLists of current filter, that may cause the vtable error which shows that some virtual function is not implemented, the core issue is that the cxx file is not compiled by cmake!!!
Remember update CMakeLists in testing dir
Remember to update vtkm.module
multiple worket and dispathers
Key interface of filters
Doexecution, input is a data set, return a data sets. input is multiple data sets, return a partitioned data sets. This can executed in a distributed manners.
Set some key properties of filter, such as the filed that the filter applied on or if the filter will be passed in the output results.
Key interface of the backends/device adaptors
Addeing tag that labels assocaited device
Query the associated device information, if the device exist on the current system.
Memory management, such as Allocate memory on device, copy data from device to host and copy data from device to host.
Set assocaited parameters for the runtime, such as number of threads and other associated information such as block number in grid and thread number in block etc.
Algorithm can be the most important part, such as the ScheduleKernel1d or 3d etc in a strided for loop
Other associated implementation such as the timer system, start time or stop time etc.
the dispather is the core part that bridges control env with the execution env. We have a array in control env, then use dispatcher to call on worklet to make the worklet code execute on device with specific patterns.
Management of worklet (more details in the tutorial)
Type checking, checking if the type of input parameter of worklet is compatible with the types specified by the ControlSignature defined in the worklet.
Transportation, dispatch mechanism plays a role here, it needs to load the data into the execution env before schduling a job. This is usually a part of dispatcher code. The transport class must contain an operator that turns the assocaited control argument into execution environment object. The ExecObjectType is the the type of the data after it is moved to execution env. It contains some information associated to devices.
Typical workflow, MapField, VisitCellByPoints, VisitPointsByCell, ReducedByKey
Key interface regarding the worklet and dispather(more details can be found in tutorial):
create data on device
schedule corresponding algorithm
When visiting the cell by points, if the result cares about the sequence of visiting the point in cells, we may need to map the index output by vtkm to the correct one. For example, if the sequence of visiting each point of a cell is different with what provided by VTK-m, we need to add another function to map the point id to the one we wanted