Part 1: Create a Plugin
A plugin mechanism in OpenAirInterface will allow us to isolate functionality originally implemented in the main source code and modularize it, so that multiple implementations and extensions to the functionality can be done. Using dynamic libraries, we will be able to select across multiple modules implementing the functionality based on the use case needs.
To keep the implementation of new functions flexible, we use OpenAirInterface’s dynamic module loader [OAILib] which allows to load different implementations at runtime. This requires creating a plugin, but also adding the required hooks or entry points in the original code base to initialize, make them available and use the plugin. Further, modifications to the build system and the container images created are needed.
The global modifications are already integrated in the patched OpenAirInterface code base. We will focus on the tasks required to add a plugin in this added infrastructure.
For this tutorial, we will show the procedure how the QAM demapper is modularized. The first part will describe the different parts of the plugin, and the second part will extend it with a second version that captures data from the selected function. Further, the Integration of a Neural Demapper in the next tutorial will be implemented as a plugin and can be used as a drop-in replacement for the existing OAI demapper.
Note
The current plugin mechanism works with L1 code. Other parts of the OAI code, like L2, require some modifications and are still work in progress.
Architecture Overview
There is a set of global changes to the OpenAirInterface needed to compile and expose the plugins in the code base. These changes involve exposing the plugin headers to the codebase, connecting the initialization functions, as well as changes to the build system. These changes are not covered by the tutorial.
The global files plugins.h, plugins.c and CMakeLists.txt represent the entry points of this infrastructure that are modified when adding a new plugin.
Plugin specific files cover the following parts:
Type definitions. Data types and function signatures exposed by the plugin to OAI.
Public interface. Set of functions exposed by the dynamic library, as well as the global entry point of the interface.
Init functions. Functions called during the initialization of OAI that loads the dynamic library and populates the global entry point with the corresponding function pointers. Additional functions for proper deinitialization as well as additional setups.
Plugin entry point. A globally accessible structure (a singleton) that contains the function pointers to the functions of the plugin to be used inside the OAI codebase.
Plugin code. The code that implements the plugin functionality.
File |
Description |
---|---|
plugins.h |
Header entry point, all plugins headers added here |
plugins.c |
Plugin entry point, wires loading, unloading and init of all plugins |
_defs.h |
Types definitions and function signatures |
_extern.h |
Public interface of the plugin |
_load.c |
Load / Unload / Init functions |
.c |
Plugin compilation units |
CMakeLists.txt |
CMake build files |
Select Functions
We focus on modularizing the demapper function nr_ulsch_compute_llr
into a separate module. This function, found in openair1/PHY/NR_TRANSPORT/nr_ulsch_llr_computation.c
, handles the demapping of received QAM symbols to log-likelihood ratios (LLRs) based on the modulation scheme used. It has a clear and well-defined interface, making it an ideal candidate for modularization. The function’s signature will serve as the basis for our plugin’s interface requirements.
1void nr_ulsch_compute_llr(int32_t *rxdataF_comp,
2 int32_t *ul_ch_mag,
3 int32_t *ul_ch_magb,
4 int32_t *ul_ch_magc,
5 int16_t *ulsch_llr,
6 uint32_t nb_re,
7 uint8_t symbol,
8 uint8_t mod_order)
9{
10 switch(mod_order){
11 case 2:
12 nr_ulsch_qpsk_llr(rxdataF_comp,
13 ulsch_llr,
14 nb_re,
15 symbol);
16 break;
17 case 4:
18 nr_ulsch_16qam_llr(rxdataF_comp,
19 ul_ch_mag,
20 ulsch_llr,
21 nb_re,
22 symbol);
23 break;
24 case 6:
25 nr_ulsch_64qam_llr(rxdataF_comp,
26 ul_ch_mag,
27 ul_ch_magb,
28 ulsch_llr,
29 nb_re,
30 symbol);
31 break;
32 case 8:
33 nr_ulsch_256qam_llr(rxdataF_comp,
34 ul_ch_mag,
35 ul_ch_magb,
36 ul_ch_magc,
37 ulsch_llr,
38 nb_re,
39 symbol);
40 break;
41 default:
42 AssertFatal(1==0,"nr_ulsch_compute_llr: invalid Qm value, symbol = %d, Qm = %d\n",symbol, mod_order);
43 break;
44 }
45}
Define Module Interface
Next, we define the module interface that specifies the function we want to extend, along with additional functions for module initialization and cleanup during loading and unloading.
1#include "nr_demapper_defs.h"
2
3typedef struct demapper_interface_s {
4 demapper_initfunc_t *init;
5 demapper_initfunc_t *init_thread;
6 demapper_shutdownfunc_t *shutdown;
7 demapper_compute_llrfunc_t *compute_llr;
8} demapper_interface_t;
9
10// global access point for the plugin interface
11extern demapper_interface_t demapper_interface;
12
13int load_demapper_lib( char *version, demapper_interface_t * );
14int free_demapper_lib( demapper_interface_t *demapper_interface );
15
16demapper_initfunc_t demapper_init;
17demapper_shutdownfunc_t demapper_shutdown;
18demapper_compute_llrfunc_t demapper_compute_llr;
The structure demapper_interface_t
defines the functions that each dynamically loaded library will export, making them available for dynamic linking at runtime. The global instance demapper_interface
is the actual mapping loaded and active in the system - this serves as the entry point that makes the module available throughout the codebase. The functions load_demapper_lib
and free_demapper_lib
are called during initialization to select and load the library, utilizing the module loader functionality.
The actual function signatures are defined in a separate file nr_demapper_defs.h
given as:
1typedef int32_t(demapper_initfunc_t)(void);
2typedef int32_t(demapper_shutdownfunc_t)(void);
3
4typedef int(demapper_compute_llrfunc_t)( int32_t *rxdataF_comp,
5 int32_t *ul_ch_mag,
6 int32_t *ul_ch_magb,
7 int32_t *ul_ch_magc,
8 int16_t *ulsch_llr,
9 uint32_t nb_re,
10 uint8_t symbol,
11 uint8_t mod_order );
Loading the Module
Loading the module happens during the initialization of the application. We leverage OAI’s config utility, which provides flexibility in specifying the libraries to load through configuration options. This can be done in either a file or as command line arguments.
The load function performs dynamic linking of the selected module and retrieves the function pointers that the module exports. These function pointers are then mapped to the corresponding elements of the global entry point interface, making them available throughout the application.
The unload function handles the graceful shutdown of the module according to the module’s own cleanup specifications.
1static int32_t demapper_no_thread_init() {
2 return 0;
3}
4
5int load_demapper_lib( char *version, demapper_interface_t *interface )
6{
7 char *ptr = (char*)config_get_if();
8 char libname[64] = "demapper";
9
10 if (ptr == NULL) { // config module possibly not loaded
11 uniqCfg = load_configmodule( 1, demapper_arg, CONFIG_ENABLECMDLINEONLY );
12 logInit();
13 }
14
15 // function description array for the shlib loader
16 loader_shlibfunc_t shlib_fdesc[] = { {.fname = "demapper_init" },
17 {.fname = "demapper_init_thread", .fptr = &demapper_no_thread_init },
18 {.fname = "demapper_shutdown" },
19 {.fname = "demapper_compute_llr" }};
20
21 int ret;
22 ret = load_module_version_shlib( libname, version, shlib_fdesc, sizeofArray(shlib_fdesc), NULL );
23 AssertFatal((ret >= 0), "Error loading demapper library");
24
25 // assign loaded functions to the interface
26 interface->init = (demapper_initfunc_t *)shlib_fdesc[0].fptr;
27 interface->init_thread = (demapper_initfunc_t *)shlib_fdesc[1].fptr;
28 interface->shutdown = (demapper_shutdownfunc_t *)shlib_fdesc[2].fptr;
29 interface->compute_llr = (demapper_compute_llrfunc_t *)shlib_fdesc[3].fptr;
30
31 AssertFatal( interface->init() == 0, "Error starting Demapper library %s %s\n", libname, version );
32
33 return 0;
34}
35
36int free_demapper_lib( demapper_interface_t *demapper_interface )
37{
38 return demapper_interface->shutdown();
39}
To integrate these functions into the executables, we need to make the module interface visible and initialize it when the application starts. We accomplish this by adding them to the two global entry points:
tutorials/plugins.h
: Contains the required module interface headerstutorials/plugins.c
: Implements the initialization calls
These files provide a single, clean entry point for module integration. They are incorporated into both the gNB and UE executables of OpenAirInterface, as well as the build system. Additional plugins only need to added to these two files to be exposed to OAI.
1#include "neural_demapper/nr_demapper_extern.h"
2
3void init_plugins();
4void free_plugins();
5
6void worker_thread_plugin_init();
1// global entry points for the tutorial plugins
2// any global structures defined here.
3
4demapper_interface_t demapper_interface = {0};
5
6
7void init_plugins()
8{
9 // insert your plugin init here.
10
11 load_demapper_lib( NULL, &demapper_interface);
12}
13
14void free_plugins()
15{
16 // insert your plugin release/free here.
17
18 free_demapper_lib(&demapper_interface);
19}
20
21void worker_thread_plugin_init()
22{
23 // insert your plugin per thread initialization here.
24
25 if (demapper_interface.init_thread)
26 demapper_interface.init_thread();
27}
Use Module Functions
At this point, the module is loaded and its interface is available in the OpenAirInterface code. The next step is to use this interface. The demapper function selects between several modulation schemes and calls the corresponding implementation. Since we only want to accelerate specific modulation schemes, we will rename the original function nr_ulsch_compute_llr
to nr_ulsch_compute_llr_default
and implement a wrapper that falls back to this default implementation when the module does not handle a particular case.
1// will include the definition for demapper_interface singleton
2#include <tutorials/plugins.h>
3
4void nr_ulsch_compute_llr(int32_t *rxdataF_comp,
5 int32_t *ul_ch_mag,
6 int32_t *ul_ch_magb,
7 int32_t *ul_ch_magc,
8 int16_t *ulsch_llr,
9 uint32_t nb_re,
10 uint8_t symbol,
11 uint8_t mod_order)
12{
13 int handled = demapper_interface.compute_llr(rxdataF_comp,
14 ul_ch_mag,
15 ul_ch_magb,
16 ul_ch_magc,
17 ulsch_llr,
18 nb_re,
19 symbol,
20 mod_order);
21 if (!handled)
22 nr_ulsch_compute_llr_default(rxdataF_comp,
23 ul_ch_mag,
24 ul_ch_magb,
25 ul_ch_magc,
26 ulsch_llr,
27 nb_re,
28 symbol,
29 mod_order);
30}
Module Implementation
Now we can implement the actual functionality of the module. The implementation consists of two parts: a header file defining the interface and a source file containing its actual implementation. In this example, we take a minimal approach where the module simply returns zero, indicating that it does not handle any cases itself. Instead, it relies on the wrapper function to call the default behavior. This design pattern helps reduce code duplication while allowing the plugin to gradually take over specific cases as needed, with unhandled cases automatically falling back to the original implementation.
1int32_t demapper_init( void )
2{
3 printf("Original/pass-through demapping initialized\n");
4 // do nothing
5 return 0;
6}
7
8int32_t demapper_shutdown( void )
9{
10 // do nothing
11 return 0;
12}
13
14// No custom code, trigger default handling by returning 0 for unhandled
15
16int demapper_compute_llr(int32_t *rxdataF_comp,
17 int32_t *ul_ch_mag,
18 int32_t *ul_ch_magb,
19 int32_t *ul_ch_magc,
20 int16_t *ulsch_llr,
21 uint32_t nb_re,
22 uint8_t symbol,
23 uint8_t mod_order) {
24 // do nothing
25 return 0;
26}
Compiling
Finally, we need to compile our new plugin. This is done by creating a new CMakeLists.txt
file in our module directory and including it from the main tutorials/CMakeLists.txt
file. This setup allows CMake to properly build and link our plugin as a shared library. A minimal version looks as follows:
1set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
2
3# demapper libraries
4
5# original
6set(PHY_DEMAPPER_ORIG_SRC
7 nr_demapper_orig.c
8)
9
10add_library(demapper_orig MODULE ${PHY_DEMAPPER_ORIG_SRC})
11add_library(demapper MODULE ${PHY_DEMAPPER_ORIG_SRC})
12
13add_dependencies(nr-softmodem demapper_orig demapper)
14add_dependencies(nr-uesoftmodem demapper_orig demapper)
15add_dependencies(demapper generate_T)
16add_dependencies(demapper_orig generate_T)
Incremental Builds
Rebuilding the full ran-build-cuda
OAI image every time to rebuild only the module is too time consuming. You can however use the image to rebuild it quickly.
# Create temp directory
mkdir /tmp/demapper
# Launch a container, mounting the tutorials directory with the newer code.
# If you made changes to openairinterface5g code, you also need to mount those.
# for example, for openair1, add -v ext/openairinterface5g/openair1:/oai-ran/openair1
docker run -v /tmp/demapper:/mnt -v ./tutorials:/oai-ran/tutorials --rm -it ran-build-cuda:latest bash
# Inside the container
cmake -S . -B cmake_targets/ran_build/build/ -GNinja
cmake --build cmake_targets/ran_build/build/ --target demapper
cmake --build cmake_targets/ran_build/build/ --target demapper_orig
# Check updated libraries
ls -lh cmake_targets/ran_build/build/libdemapper*
# Copy resulting libraries to host
cp cmake_targets/ran_build/build/libdemapper* /mnt/
Container Changes
As a final step, you need to modify the gNB and UE Dockerfiles to include the newly created libraries in your containers. Here are the corresponding lines for the demapper:
# ...
COPY --from=gnb-build \
/oai-ran/cmake_targets/ran_build/build/libdemapper*.so \
# ...
ldd /usr/local/lib/liboai_eth_transpro.so \
/usr/local/lib/libdemapper*.so \
# ...
# ...
COPY --from=nr-ue-build \
/oai-ran/cmake_targets/ran_build/build/libdemapper*.so \
# ...
ldd /usr/local/lib/liboai_eth_transpro.so \
/usr/local/lib/libdemapper*.so \
# ...
We can now rebuild the source tree with the demapper functions modularized into a dynamically linked library. If multiple modules are available, they can be selected at runtime using --loader.demapper.shlibversion _orig
for the original version. Alternatively, you can add the following line to the .env
file in your configuration directory:
GNB_EXTRA_OPTIONS="--loader.demapper.shlibversion _orig"
When the library is loaded, you will see a message like this in the gNB logs:
[LOADER] library libdemapper_orig.so successfully loaded