Merge remote-tracking branch 'origin/vrtsim-convolution-acceleration' into integration_2026_w14 (!3696)

Acceleration of channel convolution

Added two versions of channel convolution:
- accelerated via threadpool
- accelerated using CUDA
This commit is contained in:
Jaroslava Fiedlerova
2026-04-02 17:11:31 +02:00
27 changed files with 1379 additions and 352 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: LicenseRef-CSSL-1.0
add_subdirectory(TOOLS)
if(ENABLE_PHYSIM_TESTS)
add_subdirectory(tests)
add_subdirectory(TOOLS)
endif()
+14
View File
@@ -1,5 +1,18 @@
# SPDX-License-Identifier: LicenseRef-CSSL-1.0
add_library(noise_device noise_device.c)
target_link_libraries(noise_device PRIVATE log_headers SIMU)
target_include_directories(noise_device PUBLIC ./)
set(CHANNEL_PIPELINE_SOURCES channel_pipeline.c)
if (CUDA_ENABLE)
list(APPEND CHANNEL_PIPELINE_SOURCES channel_pipeline_v2.cu)
endif()
add_library(channel_pipeline ${CHANNEL_PIPELINE_SOURCES})
target_link_libraries(channel_pipeline PUBLIC thread-pool noise_device)
if (CUDA_ENABLE)
target_link_libraries(channel_pipeline PUBLIC CUDA::toolkit)
endif()
if(CUDA_ENABLE)
add_library(oai_cuda_lib STATIC
multipath_channel.cu
@@ -12,4 +25,5 @@ if(CUDA_ENABLE)
target_include_directories(oai_cuda_lib PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(channel_pipeline PUBLIC oai_cuda_lib)
endif()
@@ -0,0 +1,185 @@
/*
* SPDX-License-Identifier: LicenseRef-CSSL-1.0
*/
#include <math.h>
#include "utils.h"
#include "channel_pipeline.h"
#include "task_ans.h"
#include "noise_device.h"
#include "thread-pool.h"
typedef struct {
const c16_t **tx_sig0;
const c16_t **tx_sig1;
int num_samples_tx_sig0;
int nb_tx;
int num_samples_rx_sig0;
int num_samples;
int channel_length;
int num_jobs;
task_ans_t *task_ans;
float noise_power;
} job_common_args_t;
typedef struct {
job_common_args_t *common;
const cf_t **channel;
c16_t *rx_sig0;
c16_t *rx_sig1;
int job_index;
} job_args_t;
void do_convolution_and_noise(int nb_tx,
int channel_length,
const cf_t **channel,
const c16_t **tx_sig0,
const c16_t **tx_sig1,
int num_samples_tx_sig0,
c16_t *rx_sig0,
c16_t *rx_sig1,
int num_samples_rx_sig0,
int num_samples,
int job_index,
int num_jobs,
float noise_power)
{
const int batch_size = 4000;
cf_t work_buffer[batch_size] __attribute__((aligned(64)));
for (int batch_start = job_index * batch_size; batch_start < num_samples; batch_start += batch_size * num_jobs) {
int batch_end = min(batch_start + batch_size, num_samples);
if (noise_power > 0.0f) {
get_noise_vector((float *)work_buffer, batch_size * 2);
}
for (int i = batch_start; i < batch_end; i++) {
float rx_r = 0.0f;
float rx_i = 0.0f;
for (int tx_ant = 0; tx_ant < nb_tx; tx_ant++) {
for (int l = 0; l < channel_length; l++) {
int idx = i + (channel_length - 1) - l;
c16_t in_sample;
if (idx < num_samples_tx_sig0) {
in_sample = tx_sig0[tx_ant][idx];
} else {
in_sample = tx_sig1[tx_ant][idx - num_samples_tx_sig0];
}
cf_t tx_sample = {(float)in_sample.r, (float)in_sample.i};
cf_t ch = channel[tx_ant][l];
rx_r += tx_sample.r * ch.r - tx_sample.i * ch.i;
rx_i += tx_sample.r * ch.i + tx_sample.i * ch.r;
}
}
if (noise_power > 0.0f) {
work_buffer[i - batch_start].r += rx_r;
work_buffer[i - batch_start].i += rx_i;
} else {
work_buffer[i - batch_start].r = rx_r;
work_buffer[i - batch_start].i = rx_i;
}
}
for (int i = batch_start; i < batch_end; i++) {
if (i < num_samples_rx_sig0) {
rx_sig0[i].r = work_buffer[i - batch_start].r;
rx_sig0[i].i = work_buffer[i - batch_start].i;
} else {
rx_sig1[i - num_samples_rx_sig0].r = work_buffer[i - batch_start].r;
rx_sig1[i - num_samples_rx_sig0].i = work_buffer[i - batch_start].i;
}
}
}
}
void channel_job(void *args)
{
job_args_t *job_args = (job_args_t *)args;
job_common_args_t *common = job_args->common;
do_convolution_and_noise(common->nb_tx,
common->channel_length,
job_args->channel,
common->tx_sig0,
common->tx_sig1,
common->num_samples_tx_sig0,
job_args->rx_sig0,
job_args->rx_sig1,
common->num_samples_rx_sig0,
common->num_samples,
job_args->job_index,
common->num_jobs,
common->noise_power);
completed_task_ans(common->task_ans);
}
void channel_pipeline(void *tpool,
const cf_t **channel,
const c16_t **tx_sig0,
const c16_t **tx_sig1,
int num_samples_tx_sig0,
c16_t **rx_sig0,
c16_t **rx_sig1,
int num_samples_rx_sig0,
int num_samples,
int channel_length,
int nb_tx,
int nb_rx,
float noise_power)
{
AssertFatal(nb_tx > 0, "nb_tx must be positive (%d)\n", nb_tx);
AssertFatal(nb_rx > 0, "nb_rx must be positive (%d)\n", nb_rx);
AssertFatal(nb_tx <= 64, "Number of TX antennas is too large (%d)\n", nb_tx);
AssertFatal(nb_rx <= 64, "Number of RX antennas is too large (%d)\n", nb_rx);
tpool_t *thread_pool = (tpool_t *)tpool;
size_t num_tpool_threads = thread_pool->len_thr;
job_common_args_t common_args;
common_args.tx_sig0 = tx_sig0;
if (tx_sig1) {
common_args.tx_sig1 = tx_sig1;
} else {
common_args.tx_sig1 = NULL;
}
// At least 1 job per RX antenna. Attempt to saturate the threadpool
size_t num_jobs_per_rx_antenna = (num_tpool_threads + nb_rx - 1) / nb_rx;
common_args.num_samples_tx_sig0 = num_samples_tx_sig0;
common_args.num_samples_rx_sig0 = num_samples_rx_sig0;
common_args.num_samples = num_samples;
common_args.channel_length = channel_length;
common_args.nb_tx = nb_tx;
common_args.num_jobs = num_jobs_per_rx_antenna;
common_args.noise_power = noise_power;
task_ans_t task_ans;
init_task_ans(&task_ans, nb_rx * num_jobs_per_rx_antenna);
common_args.task_ans = &task_ans;
job_args_t job_args_array[nb_rx][num_jobs_per_rx_antenna];
for (int aarx = 0; aarx < nb_rx; aarx++) {
for (int job_idx = 0; job_idx < num_jobs_per_rx_antenna; job_idx++) {
job_args_t *job_args = &job_args_array[aarx][job_idx];
job_args->common = &common_args;
job_args->channel = &channel[nb_tx * aarx];
job_args->rx_sig0 = rx_sig0[aarx];
if (rx_sig1) {
job_args->rx_sig1 = rx_sig1[aarx];
} else {
job_args->rx_sig1 = NULL;
}
job_args->job_index = job_idx;
task_t task;
task.args = job_args;
task.func = channel_job;
pushTpool(tpool, task);
}
}
join_task_ans(&task_ans);
}
void channel_pipeline_init(float noise_power) {
init_noise_device(noise_power);
}
void channel_pipeline_shutdown(void) {
free_noise_device();
}
@@ -0,0 +1,49 @@
/*
* SPDX-License-Identifier: LicenseRef-CSSL-1.0
*/
#ifndef _CHANNEL_CONVOLUTION_H_
#define _CHANNEL_CONVOLUTION_H_
#ifdef __cplusplus
extern "C" {
#endif
#include "common/platform_types.h"
#ifdef CUDA_ENABLE
void *cuda_channel_pipeline_init(int max_samples);
void cuda_channel_pipeline_shutdown(void *context_handle);
void cuda_channel_pipeline(void *context_handle,
const cf_t **channel,
const c16_t **tx_sig0,
const c16_t **tx_sig1,
int num_samples_tx_sig0,
c16_t **rx_sig0,
c16_t **rx_sig1,
int num_samples_rx_sig0,
int num_samples,
int channel_length,
int nb_tx,
int nb_rx,
float noise_power);
#endif
void channel_pipeline_init(float noise_power);
void channel_pipeline_shutdown(void);
void channel_pipeline(void *tpool,
const cf_t **channel,
const c16_t **tx_sig0,
const c16_t **tx_sig1,
int num_samples_tx_sig0,
c16_t **rx_sig0,
c16_t **rx_sig1,
int num_samples_rx_sig0,
int num_samples,
int channel_length,
int nb_tx,
int nb_rx,
float noise_power);
#ifdef __cplusplus
}
#endif
#endif
@@ -0,0 +1,210 @@
/*
* SPDX-License-Identifier: LicenseRef-CSSL-1.0
*/
#include "oai_cuda.h"
#include <cstdint>
#include <cstdio>
#include <cuda_runtime.h>
#include "common/platform_types.h"
#include "common/utils/assertions.h"
#define CHECK_CUDA(val) \
{ \
if (val != cudaSuccess) { \
fprintf(stderr, "CUDA Error at %s:%d: %s\n", __FILE__, __LINE__, cudaGetErrorString(val)); \
exit(EXIT_FAILURE); \
} \
}
__device__ __forceinline__ cf_t complex_mul(cf_t a, cf_t b)
{
return cf_t{a.r * b.r - a.i * b.i, a.r * b.i + a.i * b.r};
}
__device__ __forceinline__ cf_t complex_add(cf_t a, cf_t b)
{
return cf_t{a.r + b.r, a.i + b.i};
}
/**
* @brief CUDA kernel for performing multipath channel convolution and noise generation
*
* This kernel simulates the effect of a multipath channel by convolving the transmitted
* signal with the channel impulse response. It supports multiple transmit and receive
* antennas (MIMO). The input signal can be split across two buffers (tx_sig0 and tx_sig1),
* and the output is similarly written to two buffers (rx_sig0 and rx_sig1).
*
* @param channel Pointer to the channel impulse response coefficients for each link.
* @param tx_sig0 Pointer to the first part of the transmitted signal buffers.
* @param tx_sig1 Pointer to the second part of the transmitted signal buffers (optional).
* @param num_samples_tx_sig0 Number of samples in the first transmit buffer NOTE: The total input
samples provided to the kernel should be equal to num_samples + channel_length - 1. They
can be arbitrarily split between tx_sig0 and tx_sig1 via this parameter
* @param rx_sig0 Pointer to the first part of the received signal buffers.
* @param rx_sig1 Pointer to the second part of the received signal buffers (optional).
* @param num_samples_rx_sig0 Number of samples in the first receive buffer. NOTE: The total output
samples privided to the kernel should be equal to num_samples. They can be arbitrarily split
between rx_sig0 and rx_sig1 via this parameter
* @param num_samples Total number of output samples to compute.
* @param channel_length Number of taps in the multipath channel.
* @param nb_tx Number of transmit antennas.
* @param nb_rx Number of receive antennas.
*/
__global__ void channel_convolution_and_noise(const cf_t **__restrict__ channel,
const c16_t **__restrict__ tx_sig0,
const c16_t **__restrict__ tx_sig1,
int num_samples_tx_sig0,
c16_t **__restrict__ rx_sig0,
c16_t **__restrict__ rx_sig1,
int num_samples_rx_sig0,
int num_samples,
int channel_length,
int nb_tx,
int nb_rx,
float noise_power,
curandState_t *curand_states)
{
extern __shared__ cf_t tx_sig[];
const int i = blockIdx.x * blockDim.x + threadIdx.x;
const int rx_ant_idx = blockIdx.y;
const int padding_len = channel_length - 1;
cf_t rx_tmp = cf_t{0.0f, 0.0f};
for (int aatx = 0; aatx < nb_tx; aatx++) {
const int tid = threadIdx.x;
const int block_start_idx = blockIdx.x * blockDim.x;
const int shared_mem_size = blockDim.x + padding_len;
for (int k = tid; k < shared_mem_size; k += blockDim.x) {
int load_idx = block_start_idx + k;
if (load_idx < num_samples + padding_len) {
c16_t val;
if (load_idx < num_samples_tx_sig0) {
val = tx_sig0[aatx][load_idx];
} else {
val = tx_sig1[aatx][load_idx - num_samples_tx_sig0];
}
tx_sig[k] = cf_t{(float)val.r, (float)val.i};
} else {
tx_sig[k] = cf_t{0.0f, 0.0f};
}
}
__syncthreads();
if (i < num_samples) {
for (int l = 0; l < channel_length; l++) {
cf_t tx_sample = tx_sig[tid + (channel_length - 1) - l];
int chan_link_idx = aatx + (rx_ant_idx * nb_tx);
cf_t chan_weight = channel[chan_link_idx][l];
rx_tmp = complex_add(rx_tmp, complex_mul(tx_sample, chan_weight));
}
}
__syncthreads();
}
if (i < num_samples) {
if (noise_power > 0.0f) {
curandState_t local_state = curand_states[rx_ant_idx * num_samples + i];
float2 awgn = curand_normal2(&local_state);
if (i < num_samples_rx_sig0) {
rx_sig0[rx_ant_idx][i].r = rx_tmp.r + awgn.x * noise_power;
rx_sig0[rx_ant_idx][i].i = rx_tmp.i + awgn.y * noise_power;
} else {
rx_sig1[rx_ant_idx][i - num_samples_rx_sig0].r = rx_tmp.r + awgn.x * noise_power;
rx_sig1[rx_ant_idx][i - num_samples_rx_sig0].i = rx_tmp.i + awgn.y * noise_power;
}
curand_states[rx_ant_idx * num_samples + i] = local_state;
} else {
if (i < num_samples_rx_sig0) {
rx_sig0[rx_ant_idx][i].r = rx_tmp.r;
rx_sig0[rx_ant_idx][i].i = rx_tmp.i;
} else {
rx_sig1[rx_ant_idx][i - num_samples_rx_sig0].r = rx_tmp.r;
rx_sig1[rx_ant_idx][i - num_samples_rx_sig0].i = rx_tmp.i;
}
}
}
}
struct GpuContext {
cudaStream_t stream;
curandState_t *curand_states;
size_t curand_states_size;
};
extern "C" void *cuda_channel_pipeline_init(int max_samples)
{
GpuContext *ctx = new GpuContext();
int dev = 0;
struct cudaDeviceProp prop;
CHECK_CUDA(cudaGetDeviceProperties(&prop, dev));
int pageable;
int integrated;
cudaDeviceGetAttribute(&pageable, cudaDevAttrPageableMemoryAccess, dev);
cudaDeviceGetAttribute(&integrated, cudaDevAttrIntegrated,dev);
if (!(pageable)) {
return NULL;
}
CHECK_CUDA(cudaStreamCreate(&ctx->stream));
ctx->curand_states = (curandState_t *)create_and_init_curand_states_cuda(max_samples, time(NULL));
ctx->curand_states_size = max_samples;
return (void *)ctx;
}
extern "C" void cuda_channel_pipeline_shutdown(void *context_handle)
{
if (context_handle == nullptr) {
return;
}
GpuContext *ctx = (GpuContext *)context_handle;
CHECK_CUDA(cudaStreamDestroy(ctx->stream));
destroy_curand_states_cuda((void *)ctx->curand_states);
delete ctx;
}
extern "C" void cuda_channel_pipeline(void *context_handle,
const cf_t **channel,
const c16_t **tx_sig0,
const c16_t **tx_sig1,
int num_samples_tx_sig0,
c16_t **rx_sig0,
c16_t **rx_sig1,
int num_samples_rx_sig0,
int num_samples,
int channel_length,
int nb_tx,
int nb_rx,
float noise_power)
{
AssertFatal(context_handle, "No context handle provided\n");
AssertFatal(channel, "No channel provided\n");
AssertFatal(tx_sig0, "No tx_sig0 provided\n");
AssertFatal(num_samples_tx_sig0 == (num_samples + channel_length - 1) || tx_sig1, "No tx_sig1 provided\n");
AssertFatal(rx_sig0, "No rx_sig0 provided\n");
AssertFatal(num_samples_rx_sig0 == num_samples || rx_sig1, "No rx_sig1 provided\n");
GpuContext *ctx = (GpuContext *)context_handle;
if (num_samples * nb_rx > ctx->curand_states_size) {
destroy_curand_states_cuda((void *)ctx->curand_states);
ctx->curand_states_size = num_samples * nb_rx;
ctx->curand_states = (curandState_t *)create_and_init_curand_states_cuda(ctx->curand_states_size, time(NULL));
}
dim3 threadsPerBlock(512, 1);
dim3 numBlocks((num_samples + threadsPerBlock.x - 1) / threadsPerBlock.x, nb_rx);
size_t sharedMemSize = (threadsPerBlock.x + channel_length - 1) * sizeof(cf_t);
channel_convolution_and_noise<<<numBlocks, threadsPerBlock, sharedMemSize, ctx->stream>>>(channel,
tx_sig0,
tx_sig1,
num_samples_tx_sig0,
rx_sig0,
rx_sig1,
num_samples_rx_sig0,
num_samples,
channel_length,
nb_tx,
nb_rx,
noise_power,
ctx->curand_states);
CHECK_CUDA(cudaStreamSynchronize(ctx->stream));
}
@@ -5,6 +5,7 @@
#include <stdio.h>
#include <cuda_runtime.h>
#include "oai_cuda.h"
#include "common/platform_types.h"
#define CHECK_CUDA(val) checkCuda((val), #val, __FILE__, __LINE__)
static void checkCuda(cudaError_t result, const char *const func, const char *const file, const int line)
+75
View File
@@ -0,0 +1,75 @@
/*
* SPDX-License-Identifier: LicenseRef-CSSL-1.0
*/
#include "assertions.h"
#include <pthread.h>
#include <errno.h>
#include "noise_device.h"
// Use block size equal to cache size
#define BLOCK_SIZE (64 / sizeof(float)) // 4 bytes per float
// Use a power of 2 for block size length
#define NUM_BLOCKS (8192)
#define NOISE_VECTOR_LENGTH (BLOCK_SIZE * NUM_BLOCKS)
static struct {
pthread_t noise_device_thread;
float noise[NOISE_VECTOR_LENGTH] __attribute__((aligned(64)));
float noise_power;
bool running;
} state;
static uint32_t jz, jsr = 123456789;
static uint32_t random_uint(void)
{
return SHR3;
}
static void generate_one_block_noise(int write_block_index, float noise_power)
{
// Generate noise vector
float block[BLOCK_SIZE];
for (int i = 0; i < BLOCK_SIZE; i++) {
block[i] = noise_power * gaussZiggurat(0, 1);
}
// Write block to noise vector
memcpy(state.noise + write_block_index * BLOCK_SIZE, block, sizeof(block));
}
void *noise_device_thread_function(void *arg)
{
while (state.running) {
// Generate noise vector
generate_one_block_noise(random_uint() % NUM_BLOCKS, state.noise_power);
usleep(10000);
}
return NULL;
}
void init_noise_device(float noise_power)
{
for (int i = 0; i < NUM_BLOCKS; i++) {
generate_one_block_noise(i, noise_power);
}
int ret = pthread_create(&state.noise_device_thread, NULL, noise_device_thread_function, NULL);
AssertFatal(ret == 0, "pthread_create failed: %d, errno %d (%s)\n", ret, errno, strerror(errno));
}
void free_noise_device(void)
{
state.running = false;
int ret = pthread_join(state.noise_device_thread, NULL);
AssertFatal(ret == 0, "pthread_join failed: %d, errno %d (%s)\n", ret, errno, strerror(errno));
}
void get_noise_vector(float *noise_vector, int length)
{
int start_block = random_uint() % NUM_BLOCKS;
while (length > 0) {
int copy_size = min(length, (NUM_BLOCKS - start_block) * BLOCK_SIZE);
memcpy(noise_vector, &state.noise[start_block * BLOCK_SIZE], copy_size * sizeof(float));
start_block = 0;
length -= copy_size;
noise_vector += copy_size;
}
}
+14
View File
@@ -0,0 +1,14 @@
/*
* SPDX-License-Identifier: LicenseRef-CSSL-1.0
*/
#ifndef NOISE_DEVICE_H
#define NOISE_DEVICE_H
#include "SIMULATION/TOOLS/sim.h"
void init_noise_device(float noise_power);
void free_noise_device(void);
void get_noise_vector(float *noise_vector, int length);
#endif
+1 -4
View File
@@ -8,10 +8,7 @@
#include <stdint.h>
#ifdef __NVCC__
typedef struct complex16 {
int16_t r;
int16_t i;
} c16_t;
#include "common/platform_types.h"
#else
#include "PHY/TOOLS/tools_defs.h"
#endif
-2
View File
@@ -440,8 +440,6 @@ endif()
####################################################################################
if (CUDA_ENABLE)
add_physim_test(cuda.5g.channelsim.test_multipath "unitary test designed specifically to validate the multipath_channel_cuda" test_multipath)
add_physim_test(cuda.5g.channelsim.test_noise "unitary test that focuses on a single component: the add_noise_cuda function" test_noise)
# --- Test Suite 1: 61440 samples, 2/4/8/32 TX - 2/4/8/32 RX ---
add_physim_test(cuda.5g.channelsim.test_channel_scalability.test1 "16 channels 61440 samples 2T2R Batch" test_channel_scalability -c 16 -t 2 -r 2 -s 61440 -l 32 -m batch -n 12)