-
Notifications
You must be signed in to change notification settings - Fork 1
Basic Kernel Developer Guide
In FleXR, a kernel is a unit for parallel expansion and offloading. FleXR kernels are implemented on RaftLib kernel, and it is possible to activate kernel ports for remote/local communication and duplicate output ports for multiple downstream kernels at runtime. The FleXR system consists of pipelined kernels, but the FleXR design makes kernel development and deployment phases completely transparent to each other. So, developers do not need to consider how their kernels are used in distributed pipelines at the deployment phase.
Kernels are classified into Source, Sink, and Intermediate by their input/output ports. Source and Sink are the kernels for Device I/O; the source kernel is only with output ports while the sink kernel has only input ports without any output. The intermediate kernel is a kernel having both input and output ports. In FleXR, all kernels to be offloaded are intermediate kernels.
Basically, FleXR can be installed on the host system, and it is possible to write kernels separately. However, for simplicity, this guide describes how to add and build a new kernel in the FleXR directory.
A kernel should be added under the directory flexr/flexr_kernels, and the directory structure of flexr_kernels is below.
flexr_kernels
|-- include
| |-- intermediate
| |-- sink
| |-- source
| `-- yaml
| |-- intermediate
| |-- sink
| |-- source
| `-- yaml_parser.h
`-- source
|-- intermediate
|-- sink
|-- source
`-- yaml
|-- intermediate
|-- sink
`-- source
/* flexr_kernels/include/intermediate/example_kernel.h */
# pragma once
#ifdef __FLEXR_KERNEL_EXAMPLE_KERNEL__
#include <flexr_core/include/core.h>
namespace flexr
{
namespace kernels
{
class ExampleKernel: public FleXRKernel
{
private:
int defaultValue;
public:
ExampleKernel(std::string id, int defaultValue);
raft::kstatus run() override;
}
}
}
#endif
__FLEXR_KERNEL_EXAMPLE_KERNEL__ is a macro to selectively build the kernel with its dependencies. The initial setup to run the kernel is done in the constructor, and the kernel execution logic is implemented in the run function.
/* flexr_kernels/source/intermediate/example_kernel.cc */
#ifdef __FLEXR_KERNEL_EXAMPLE_KERNEL__
#include <flexr_kernels/include/intermediate/example_kernel.h>
namespace flexr
{
namespace kernels
{
ExampleKernel::ExampleKernel(std::string id, int defaultValue): FleXRKernel(id), defaultValue(defaultValue)
{
setName("ExampleKernel");
portManager.registerInPortTag("input_a", flexr::PortDependency::BLOCKING);
portManager.registerInPortTag("input_b", flexr::PortDependency::NONBLOCKING);
portManager.registerOutPortTag("output_c", utils::sendLocalBasicCopy<int>);
}
raft::kstatus ExampleKernel::run()
{
types::Message<int> *inputA = portManager.getInput<types::Message<int>>("input_a");
types::Message<int> *inputB = portManager.getInput<types::Message<int>>("input_b");
types::Message<int> *outputC = portManager.getOutputPlaceholder<types::Message<int>>("output_c");
if(inputB != nullptr) outputC->data = inputA->data + inputB->data;
else outputC->data = inputA->data + defaultValue;
portManager.sendOutput("output_c", outputC);
portManager.freeInput("input_a", inputA);
portManager.freeInput("input_b", inputB);
return raft::proceed;
}
}
}
#endif
All kernels are implemented by inheriting FleXRKernel. The constructor must have an id parameter at least, which is used to differentiate kernels of the same name. In the constructor of ExampleKernel, two inputs and one output tag are registered by portManager. input_a is of the BLOCKING dependency and input_b has NONBLOCKING. These dependencies of BLOCKING and NONBLOCKING indicate the primary and optional relationship of input ports. output_c is registered with sendLocalBasicCopy<int>, a function copying the output message for the case where the output of kernel is sent to multiple downstream kernels.
In the run function of ExampleKernel, two inputs, inputA and inputB of the flexr::types::Message<int> type, are retrieved. outputC is the memory buffer of the output message as a placeholder to send. The kernel logic is C = A+B when optional inputB is available, or C = A+defaultValue when optional inputB is not received. outputC is sent via portManager, and input messages are freed as the kernel logic is done.
FleXR kernels are used via FleXR's YAML interfaces. In this section, we connect ExampleKernel to the interfaces. The processes are following.
- Add a new YAML interface kernel corresponding to ExampleKernel
- Update YAML parser with the new interface kernel
- Add the new kernels to the build system.
/* flexr_kernels/include/yaml/intermediate/yaml_example_kernel.h */
# pragma once
#ifdef __FLEXR_KERNEL_EXAMPLE_KERNEL__
#include <flexr_core/include/core.h>
#include <flexr_kernels/include/intermediate/example_kernel.h>
namespace flexr
{
namespace yaml
{
class YamlExampleKernel: public YamlFleXRKernel
{
private:
int defaultValue;
public:
void parseExampleKernel(const YAML::Node &node);
void parseExampleKernelSpecific(const YAML::Node &node);
void* make();
}
}
}
#endif
The interface kernel needs a parsing function parseExampleKernel. parseExampleKernelSpecific is only for parsing ExampleKernel-specific parameters. The make function is used by the YAML parser to instantiate ExampleKernel.
/* flexr_kernels/source/yaml/intermediate/yaml_example_kernel.cc */
#ifdef __FLEXR_KERNEL_EXAMPLE_KERNEL__
#include <flexr_kernels/include/yaml/intermediate/yaml_example_kernel.h>
namespace flexr
{
namespace yaml
{
void YamlExampleKernel::parseExampleKernel(const YAML::Node &node)
{
parseBase(node);
parseExampleKernelSpecific(node);
}
void YamlExampleKernel::parseExampleKernelSpecific(const YAML::Node &node)
{
specificSet = true;
YAML::Node others = node["others"][0];
defaultValue = others["default_value"].as<int>();
}
void* YamlExampleKernel::make()
{
kernels::ExampleKernel *newKenrel = new kernels::ExampleKernel(id, defaultValue);
newKernel->setFrequency(frequency);
newKernel->setLogger(loggerId, loggerFileName);
for(int i = 0; i < inPorts.size(); i++)
{
if(inPorts[i].portName == "input_a" || inPorts[i].portName == "input_b")
{
if(inPorts[i].connectionType == "local")
newKernel->activateInPortAsLocal<types::Message<int>>(inPorts[i].portName);
else if(inPorts[i].connectionType == "remote")
newKernel->activateInPortAsRemote<types::Message<int>>(inPorts[i].portName,
inPorts[i].protocol,
inPorts[i].bindingPortNum);
}
}
for(int i = 0; i < outPorts.size(); i++)
{
if(outPorts[i].portName == "output_c")
{
if(outPorts[i].connectionType == "local")
newKernel->activateOutPortAsLocal<types::Message<int>>(outPorts[i].portName);
else if(outPorts[i].connectionType == "remote")
newKernel->activateOutPortAsRemote<types::Message<int>>(outPorts[i].portName,
outPorts[i].protocol,
outPorts[i].connectingAddr,
outPorts[i].connectingPortNum);
}
else
{
// Port Duplication
if(outPorts[i].duplicatedFrom == "output_c")
{
if(outPorts[i].connectionType == "local")
newKernel->duplicateOutPortAsLocal<types::Message<int>>(outPorts[i].duplicatedFrom,
outPorts[i].portName);
else if(outPorts[i].connectionType == "remote")
newKernel->duplicateOutPortAsRemote<types::Message<int>>(outPorts[i].duplicatedFrom, outPorts[i].portName,
outPorts[i].protocol, outPorts[i].connectingAddr,
outPorts[i].connectingPortNum);
}
}
}
}
}
}
#endif
In the parseExampleKernel function, parseBase and parseExampleKernelSpecific are called. parseBase is the function parsing the basic kernel info of FleXRKernel. Then, ExampleKernel specific parameters are parsed by parseExampleKernelSpecific; in this example, it's only defaultValue.
Then, in the make function, ExampleKernel is instantiated and set. The base FleXRKernel has a frequency manager and logger. Then, the activation interfaces of input and output ports are implemented. In FleXR, each port can be set by local or remote at runtime. When it is remote, it is also possible to specify the protocol to use: TCP or UDP-based RTP. For the output port, along with the port activation interface, the duplication interface is also implemented. The port duplication is for sending output to multiple downstream kernels, and it is possible to duplicate an output to multiple local/remote outputs at runtime.
/* flexr_kernels/include/yaml/yaml_parser.h */
#pragma once
#include <flexr_kernels/include/yaml/intermediate/yaml_example_kernel.h>
namespace flexr
{
namespace yaml
{
class YamlFleXRParser
{
/* some codes... */
public:
void destroyKernel(flexr::kernels::FleXRKernel *kernel)
{
#ifdef __FLEXR_KERNEL_EXAMPLE_KERNEL__
if(kernel->getName() == "ExampleKernel") delete (flexr::kernels::ExampleKernel*) kernel;
#endif
}
void initKernels()
{
for(int i = 0; i < doc.size(); i++)
{
if(doc[i]["kernel"].IsDefined())
{
kernels::FleXRKernel* temp = nullptr;
if(doc[i]["kernel"].as<std::string>() == "ExampleKernel")
{
#ifdef __FLEXR_KERNEL_EXAMPLE_KERNEL__
YamlExampleKernel yamlExampleKernel;
yamlExampleKernel.parseExampleKernel(doc[i]);
temp = (kernels::FleXRKernel*)yamlExampleKernel.make();
#endif
}
}
}
}
/* some codes... */
}
}
}
For updating the existing YAML parser, it is required to add codes to two functions: destroyKernel and initKernels. destroyKernel is the function to free the instantiated kernel, and initKernels is for instantiation. These codes should be with macro.
# flexr/cmake/kernels/intermediate/example_kernel.cmake #
macro(kernel_example_kernel)
if(${KERNEL_EXAMPLE_KERNEL})
add_definitions(-D__FLEXR_KERNEL_EXAMPLE_KERNEL__)
message("\t [Intermediate] ExampleKernel")
endif()
endmacro()
To build ExampleKernel with the interface, it is required to add an example_kernel.cmake file of a function defining the macro __FLEXR_KERNEL_EXAMPLE_KERNEL__. If there are other dependencies of the kernel, their compile linker and cxx flags must be appended to FLEXR_KERNEL_CXX_FLAGS, FLEXR_KERNEL_LINKER_FLAGS, and FLEXR_KERNEL_LINKER_LIBS.
# flexr/cmake/kernels/kernels.cmake #
set(FLEXR_KERNEL_CXX_FLAGS "")
set(FLEXR_KERNEL_LINKER_FLAGS "")
set(FLEXR_KERNEL_LINKER_LIBS "")
include(cmake/kernels/intermediate/example_kernel.cmake)
macro(kernel_setup_dependencies)
message(STATUS "[Enabled Kernels]")
kernel_example_kernel()
endmacro()
In kernels.cmake, include the kernel's cmake file and call the setup function.
# flexr/CmakeLists.txt #
util_flexr_option(KERNEL_EXAMPLE_KERNEL "Use ExampleKernel Intermediate kernel" ON)
In CMakeLists.txt under the flexr directory, set the option for enabling ExampleKernel.
That's all. We implement a new FleXR kernel, ExampleKernel, with the YAML interface YamlExampleKernel. Then, we also added it to the existing build system and allowed the kernel deployers to selectively use ExampleKernel.