How to Write a GPU Module
This guide explains how to write Vistle modules that can be run on the GPU. It assumes you are familiar with the basics on how to write a Vistle module.
Vistle makes use of the portable toolkit VTK-m which allows running scientific visualization algorithms on various devices, including GPUs, and is designed to keep data transfers between devices at a minimum.
Overview
The VtkmModule Class
The class VtkmModule
is the base class for VTK-m modules in Vistle. It is designed to make adding new VTK-m algorithms, so-called filters, as simple as possible by providing core functionality for handling the input data, passing it to the filter implemented by the derived class, and writing the filter result to the output ports. At the same time, it is meant to be flexible, allowing the derived class to customize and extend these processes, if desired.
In order to implement a module wrapping a filter provided by or relying on VTK-m, one implements a class deriving from VtkmModule
.
Note: The class VtkmModule
derives from Module
, i.e., many of its features, like the functionality for adding module parameters, can be used by VTK-m modules, too.
The Constructor
The VtkmModule
constructor, which the derived class should call in its own constructor, creates the input and output ports of the VTK-m module.
VtkmModule(const std::string &name, int moduleID, mpi::communicator comm, int numPorts = 1, bool requireMappedData = true);
By default, a VTK-m module provides of one input port and one output port, but additional ports can be added by specifying the desired number in the base constructor (numPorts
). Note that all data on the input ports must be defined on the same grid, as the module will throw an error, otherwise.
Many VTK-m filters work on data fields, so, by default, a VTK-m module expects the data at the input ports to contain mapped data in addition to a grid and will throw an error if there is none. If desired, the derived class can remove this requirement by setting requireMappedData
to false
.
Defining the VTK-m Filter
The setUpFilter
method, which must be implemented by the derived class, creates, sets up and returns the desired VTK-m filter that will be called on the input data.
virtual std::unique_ptr<vtkm::filter::Filter> setUpFilter() const = 0;
If the VTK-m module has multiple input ports, the filter will only be applied to the data on the first input port, i.e., the filter’s active field is set to the field on the first port. The fields on the remaining ports will be mapped to the resulting output grid.
Preparing the Input Data
The prepareInputGrid
transforms the input grid into a VTK-m cellset and adds it to the VTK-m dataset dataset
. Similarly, prepareOutputGrid
, which is called once per field, transforms the input fields into VTK-m array handles and adds them to dataset
as well. The filter will, subsequently, be applied to dataset
.
virtual ModuleStatusPtr prepareInputGrid(const vistle::Object::const_ptr &grid, vtkm::cont::DataSet &dataset) const;
virtual ModuleStatusPtr prepareInputField(const vistle::Port *port, const vistle::Object::const_ptr &grid, const vistle::DataBase::const_ptr &field, std::string &fieldName, vtkm::cont::DataSet &dataset) const;
A VTK-m module only performs very basic checks on the input ports while reading in the data, i.e., in the readInPorts
method: It ensures each input port contains data as long as its corresponding output port is connected. Additionally, it makes sure that at least one input grid provides an input grid and that all data fields are defined on the same grid.
Some filters might, however, require additional checks on the input data. These can be added by overriding prepareInputGrid
and/or prepareInputField
as needed.
Preparing the Output Data
prepareOutputGrid
and prepareOutputField
, which is called once per field, transform the filter results that will be added to the output ports back into the Vistle format. The output data field, which contains the output grid, is added to the ports. If requireMappedData
is false
, only the output grid is added.
virtual vistle::Object::ptr prepareOutputGrid(const vtkm::cont::DataSet &dataset, const vistle::Object::const_ptr &inputGrid) const;
virtual vistle::DataBase::ptr prepareOutputField(const vtkm::cont::DataSet &dataset, const vistle::Object::const_ptr &inputGrid, const vistle::DataBase::const_ptr &inputField, const std::string &fieldName, const vistle::Object::ptr &outputGrid) const;
By default, the VTK-m module simply copies the attributes from the input grid and fields to the output grid and fields, respectively. It also sets the output field’s grid to the output grid. To account for possible attribute changes after applying the filter, e.g., when the filter changes the field’s mapping from element- to cell-based, the derived class can override these two methods as needed.
Example 1: Basic usage
This first example illustrates how to use the base functionalities of the VtkmModule
class. It will create a VTK-m module MyIsosurfaceVtkm which calls VTK-m’s Contour filter to generate an isosurface.
The Header
Let’s first inspect the new module’s header file:
#ifndef VISTLE_MYISOSURFACEVTKM_MYISOSURFACEVTKM_H
#define VISTLE_MYISOSURFACEVTKM_MYISOSURFACEVTKM_H
#include <vistle/vtkm/vtkm_module.h>
class MyIsosurfaceVtkm: public VtkmModule {
public:
MyIsosurfaceVtkm(const std::string &name, int moduleID, mpi::communicator comm);
~MyIsosurfaceVtkm();
private:
vistle::FloatParameter *m_isovalue;
std::unique_ptr<vtkm::filter::Filter> setUpFilter() const override;
};
#endif
As the goal is to use VTK-m to run the algorithm on the GPU, the module inherits from the VtkmModule
class which is defined in vistle/vtkm/vtkm_module.h
:
class MyIsosurfaceVtkm: public VtkmModule {
Because of inheriting from said class, MyIsosurfaceVtkm
must override the setUpFilter()
method to prepare the desired filter to be applied to the input dataset:
std::unique_ptr<vtkm::filter::Filter> setUpFilter() const override;
The Contour filter needs an isovalue. Since VtkmModule
inherits from the Module
class, we can simply define a float parameter for this purpose:
vistle::FloatParameter *m_isovalue;
The Source File
Next, the corresponding source file will be discussed:
#include <vtkm/filter/contour/Contour.h>
#include "MyIsosurfaceVtkm.h"
MODULE_MAIN(MyIsosurfaceVtkm)
using namespace vistle;
MyIsosurfaceVtkm::MyIsosurfaceVtkm(const std::string &name, int moduleID, mpi::communicator comm)
: VtkmModule(name, moduleID, comm, 2)
{
m_isovalue = addFloatParameter("isovalue", "isovalue", 0.0);
}
MyIsosurfaceVtkm::~MyIsosurfaceVtkm()
{}
std::unique_ptr<vtkm::filter::Filter> MyIsosurfaceVtkm::setUpFilter() const
{
auto filter = std::make_unique<vtkm::filter::contour::Contour>();
filter->SetIsoValue(m_isovalue->getValue());
return filter;
}
Like any Vistle module, VTK-m modules must also call the MODULE_MAIN
function to make sure it is integrated correctly into the software:
MODULE_MAIN(MyIsosurfaceVtkm)
The constructor must call the base constructor:
MyIsosurfaceVtkm::MyIsosurfaceVtkm(const std::string &name, int moduleID, mpi::communicator comm)
: VtkmModule(name, moduleID, comm, 2)
{
m_isovalue = addFloatParameter("isovalue", "isovalue", 0.0);
}
The base constructor lets us choose the number of ports. Here, the number of ports is 2. This means that the Contour filter will use the data field on the first input port to create the isosurface. The data on the second port will then simply be mapped to the resulting geometry.
The constructor can, e.g., be used to define module parameters like the isovalue.
Finally, we create a Contour filter in the setUpFilter
method, pass the isovalue to it and return it:
std::unique_ptr<vtkm::filter::Filter> MyIsosurfaceVtkm::setUpFilter() const
{
auto filter = std::make_unique<vtkm::filter::contour::Contour>();
filter->SetIsoValue(m_isovalue->getValue());
return filter;
}
Adding the Module to Vistle
Adding a VTK-m module to Vistle is very similar to adding a regular module to Vistle. In the module’s CMakeLists.txt
, we call the add_vtkm_module
target which makes sure the correct VTK-m libraries are linked in addition to all necessary Vistle libraries:
add_vtkm_module(MyIsosurfaceVtkm "Basic GPU module using VTK-m's Contour filter" MyIsosurfaceVtkm.h MyIsosurfaceVtkm.cpp)
Then, we must choose the module category which fits best, and add the new subdirectory to the corresponding CMakeLists.txt
file:
add_subdirectory(MyIsosurfaceVtkm)
The Result
The code above produces a Vistle module MyIsosurfaceVtkm which consists of two input and output ports as well as a float parameter to set the isovalue and which calculates the isosurface using VTK-m’s Contour filter.
The data field on the first input port is used to calculate the isosurface. In the following example workflow, MyIsosurfaceVtkm reads in the data field named scalar and uses it to create an isosurface of isovalue 1.1:
Since scalar is the data field on the first input port and was thus used as the filter’s active field, the data field on the first output port is uniform on the resulting output geometry.
For comparison, a second input field called vector_z
is connected to IsosurfaceVtkm in the following example workflow:
The resulting geometry remains the same because the data on the first input port and the isovalue have not changed. The second output port returns the data field vector_z
mapped to the output grid, which leads to a different coloring of the geometry.
Example 2: Extending the Core Functionality
In this second example, the derived class will change the methods for handling its input and output data. The new class MyCertToVellVtkm will call the Point Average filter to transform a cell-based data field into an equivalent vertex-based field. It, however, only applies the filter if the input data is cell-based. If not, it simply adds the input field to the output port.
The Header File
To implement the desired behaviour, MyCellToVertVtkm
overrides the prepareInputField
, prepareOutputGrid
and prepareOutputField
methods:
#ifndef VISTLE_MYCELLTOVERTVTKM_MYCELLTOVERTVTKM_H
#define VISTLE_MYCELLTOVERTVTKM_MYCELLTOVERTVTKM_H
#include <array>
#include <vistle/vtkm/vtkm_module.h>
class MyCellToVertVtkm: public VtkmModule {
public:
MyCellToVertVtkm(const std::string &name, int moduleID, mpi::communicator comm);
~MyCellToVertVtkm();
private:
ModuleStatusPtr prepareInputField(const vistle::Port *port, const vistle::Object::const_ptr &grid, const vistle::DataBase::const_ptr &field, std::string &fieldName,vtkm::cont::DataSet &dataset) const override;
std::unique_ptr<vtkm::filter::Filter> setUpFilter() const override;
vistle::Object::ptr prepareOutputGrid(const vtkm::cont::DataSet &dataset, vistle::Object::const_ptr &inputGrid) const override;
vistle::DataBase::ptr prepareOutputField(const vtkm::cont::DataSet &dataset, const vistle::Object::const_ptr &inputGrid, const vistle::DataBase::const_ptr &inputField, const std::string &fieldName, const vistle::Object::ptr &outputGrid) const override;
};
#endif
The Source File
The following is MyCellToVertVtkm’s complete source file. In this section, the overridden methods will be explained one by one.
#include <vtkm/filter/contour/Contour.h>
#include <vtkm/filter/field_conversion/PointAverage.h>
#include "MyCellToVertVtkm.h"
MODULE_MAIN(MyCellToVertVtkm)
using namespace vistle;
MyCellToVertVtkm::MyCellToVertVtkm(const std::string &name, int moduleID, mpi::communicator comm)
: VtkmModule(name, moduleID, comm)
{}
MyCellToVertVtkm::~MyCellToVertVtkm()
{}
std::unique_ptr<vtkm::filter::Filter> MyCellToVertVtkm::setUpFilter() const
{
return std::make_unique<vtkm::filter::field_conversion::PointAverage>();
}
ModuleStatusPtr MyCellToVertVtkm::prepareInputField(const Port *port, const Object::const_ptr &grid,
const DataBase::const_ptr &field, std::string &fieldName,
vtkm::cont::DataSet &dataset) const
{
if (field->guessMapping(grid) == DataBase::Element) {
return VtkmModule::prepareInputField(port, grid, field, fieldName, dataset);
}
return Info("No need to apply filter to port " + port->getName());
}
Object::ptr MyCellToVertVtkm::prepareOutputGrid(const vtkm::cont::DataSet &dataset,
const Object::const_ptr &inputGrid) const
{
return nullptr;
}
DataBase::ptr MyCellToVertVtkm::prepareOutputField(const vtkm::cont::DataSet &dataset,
const Object::const_ptr &inputGrid,
const DataBase::const_ptr &inputField, const std::string &fieldName,
const Object::ptr &outputGrid) const
{
// if filter was applied ...
if (dataset.HasField(fieldName)) {
// ... add its output to the output port
auto outputField = VtkmModule::prepareOutputField(dataset, inputGrid, inputField, fieldName, outputGrid);
outputField->setMapping(DataBase::Vertex);
outputField->setGrid(inputGrid);
return outputField;
} else {
// ... otherwise just copy the input field
auto ndata = inputField->clone();
ndata->setGrid(inputGrid);
updateMeta(ndata);
return ndata;
}
}
Like any VTK-m module, MyCellToVertVtkm must define and set up its desired filter in the setUpFilter()
method:
std::unique_ptr<vtkm::filter::Filter> MyCellToVertVtkm::setUpFilter() const
{
return std::make_unique<vtkm::filter::field_conversion::PointAverage>();
}
Before transforming the input field into a VTK-m field, MyCellToVertVtkm first determines the field’s mapping using the guessMapping
method. If the field is element-based (=cell-based), the prepareInputField
method of the base class is called to add the field to the VTK-m dataset that will be passed to the VTK-m filter. Otherwise, nothing happens, only an informational message will be printed to the GUI.
ModuleStatusPtr MyCellToVertVtkm::prepareInputField(const Port *port, const Object::const_ptr &grid,
const DataBase::const_ptr &field, std::string &fieldName,
vtkm::cont::DataSet &dataset) const
{
if (field->guessMapping(grid) == DataBase::Element) {
return VtkmModule::prepareInputField(port, grid, field, fieldName, dataset);
}
return Info("No need to apply filter to port " + port->getName());
}
Note: ModuleStatusPtr
is used to pass module states to VtkmModule
which handles the states through its isValid
method. Currently, there are four states: Success()
, Info(const std::string &message)
, Warning(const std::string &message)
, Error(const std::string &message)
. Returning the latter three, results in VtkmModule
printing message
to the GUI’s Vistle Console. Returning an Error
state will stop the execution of the module, but not cause Vistle to crash.
In this example, the output grid is the same as the input grid. As there is no reason to convert the filter’s output grid back to Vistle, we can skip this step:
Object::ptr MyCellToVertVtkm::prepareOutputGrid(const vtkm::cont::DataSet &dataset,
const Object::const_ptr &inputGrid) const
{
return nullptr;
}
The field we return in the prepareOutputField
method is the field that will be passed to the output port (as long as it is not a nullptr, in that case outputGrid
will be added to the port). We can use this to achieve the desired behavior: If the filter was applied, i.e., the input data field was cell-based, we want to add the filter’s result to the output port.
If the filter was not applied, i.e., the input field was vertex-based, we copy simply add the input field to the output port.
DataBase::ptr MyCellToVertVtkm::prepareOutputField(const vtkm::cont::DataSet &dataset,
const Object::const_ptr &inputGrid,
const DataBase::const_ptr &inputField, const std::string &fieldName,
const Object::ptr &outputGrid) const
{
// if filter was applied ...
if (dataset.HasField(fieldName)) {
// ... add its output to the output port
auto outputField = VtkmModule::prepareOutputField(dataset, inputGrid, inputField, fieldName, outputGrid);
outputField->setMapping(DataBase::Vertex);
outputField->setGrid(inputGrid);
return outputField;
} else {
// ... otherwise just copy the input field
auto ndata = inputField->clone();
ndata->setGrid(inputGrid);
updateMeta(ndata);
return ndata;
}
}
By default, the output field’s grid is the output grid. Since we skipped calculating outputGrid
and a field’s grid cannot be nullptr
, we set the output field’s grid to the input grid instead using setGrid
. VtkmModule::prepareOutputField
additionally copies the input field’s attributes to the output field. As the filter changed the field’s mapping, we must set it to vertex-based using setMapping
.
The Result
The code above creates the MyCellToVertVtkm module which consists of one input and one output port. It checks if it makes sense to apply the Point Average filter to the input field, i.e., it checks if the input field is cell-based. If so, the filter is applied. If not, an informational message is printed to the Vistle Console:
Custom VTK-m Filters
For simplicity, predefined VTK-m filters have been used for the two examples above. Please note, that VtkmModule
can, of course, also be used to add custom VTK-m filters to Vistle. To learn more about implementing custom VTK-m filters, check out VTK-m’s user guide.
How to Configure Vistle to Run VTK-m Modules on the GPU
VTK-m is an open-source software that can be obtained through Kitware’s Gitlab. It was added as submodule to the Vistle repository, so that Vistle can handle configuring VTK-m appropriately for the user.
Only the CUDA version of VTK-m is supported by Vistle, although, we are currently working on adding support for the Kokkos version as well.
To compile Vistle with the CUDA version of VTK-m, run the following commands from your build directory:
cmake -DVISTLE_USE_CUDA=ON ..
make
Note: If VTK-m is already installed on your system, which is usually the case if VTK is installed, Vistle will not compile its own VTK-m, but use the system VTK-m instead. In that case, make sure that the system VTK-m was compiled to use CUDA (or Kokkos), otherwise, the modules will be run on the CPU.