Writing Python Blocks with CuPy
Relevant source files
The following files were used as context for generating this wiki page:
Writing GPU-accelerated blocks in Python using the gr-cuda framework allows developers to leverage CuPy's NumPy-like interface while maintaining performance levels within 5% of hand-written CUDA C++. The system handles memory transfers and CUDA-event synchronization automatically, enabling blocks to receive device pointers directly in their processing methods.
This module provides drop-in replacements for standard GNU Radio blocks (e.g., gr.sync_block, gr.basic_block), which automatically allocate CUDA buffers, synchronize them before and after execution, and convert input/output arrays to CuPy objects without unnecessary host-device copies.
Sources: README.md:12-14, python/cuda/cuda_block.py:16-24
Core Architecture and Inheritance
The architecture relies on the cuda_block mixin, which integrates a non-blocking CUDA stream and automated synchronization into the standard GNU Radio block lifecycle. Users do not inherit from gr base classes directly but instead use the cuda namespace equivalents.
Class Hierarchy
The following diagram illustrates the relationship between the base CUDA mixin and the specialized GNU Radio block types.
The diagram shows how cuda_block acts as a mixin to provide CUDA capabilities to all standard GNU Radio block archetypes.
Sources: python/cuda/cuda_block.py:106-160
Available Base Classes
| Class | Base Type | Use Case |
|---|---|---|
cuda.sync_block | gr.sync_block | 1:1 input to output ratio (e.g., multiply_const). |
cuda.decim_block | gr.decim_block | N:1 fixed decimation (e.g., downsample). |
cuda.interp_block | gr.interp_block | 1:M fixed interpolation (e.g., upsample). |
cuda.basic_block | gr.basic_block | Variable rates requiring general_work, consume, and produce. |
Sources: python/cuda/cuda_block.py:149-160, python/cuda/qa_cuda_block.py:79-135
Automatic Synchronization and Data Flow
The gr-cuda framework uses a specialized decorator, _wrap_work, to manage the complexities of CUDA synchronization. This decorator ensures that data is ready on the GPU before processing begins and that output events are recorded after processing ends.
The _wrap_work Execution Flow
When work() or general_work() is called, the following automated sequence occurs:
This flowchart represents the internal synchronization logic handled by the _wrap_work decorator to maintain a zero-copy GPU pipeline.
Sources: python/cuda/cuda_block.py:46-103
Key Synchronization Mechanics
- Wait for Work: Calls
cuda.wait_for_workto ensure upstream data is ready and downstream blocks have finished reading the previous output. - CuPy Conversion: Uses
cuda.as_cupy()to wrap raw device pointers into CuPy ndarrays, allowing standard NumPy-style operations (e.g.,cp.multiply,cp.fft). - Manual Production: If
self.produce()is called inside abasic_block, the system intercepts the call to triggercuda.mark_work_donebefore the write pointer advances. This prevents downstream blocks from seeing stale device-ready events.
Sources: python/cuda/cuda_block.py:53-85
Implementation Patterns
Synchronous Processing (sync_block)
For 1:1 processing, the implementation focuses on the work method. Data is manipulated using CuPy operations, and the number of items processed is returned.
class my_cupy_block(cuda.sync_block):
def __init__(self, factor):
cuda.sync_block.__init__(self, "my_cupy_block",
[np.complex64], [np.complex64])
self.factor = factor
def work(self, input_items, output_items):
# Arrays are already CuPy on GPU
cp.multiply(input_items[0], self.factor, out=output_items[0])
return len(output_items[0])
Sources: README.md:46-59, python/cuda/multiply_const_cupy.py:28-49
Variable Rate Processing (basic_block)
In a basic_block, the user must manage sample consumption and production manually. The framework ensures mark_work_done is called even if produce is used explicitly.
class my_resampler(cuda.basic_block):
def general_work(self, input_items, output_items):
# ... perform GPU compute with CuPy ...
self.consume_each(n_consumed)
self.produce(0, n_produced)
return -2 # WORK_CALLED_PRODUCE
Sources: README.md:65-71, python/cuda/qa_cuda_block.py:101-114
Performance Considerations
The zero-copy pipeline provided by cuda_buffer handles host-device transfers and synchronization. Benchmarks show that Python-based CuPy FFT implementations achieve throughput very close to C++ implementations.
| Benchmark | cuFFT (C++) | CuPy FFT (Python) |
|---|---|---|
| DGX Spark (GB10) | 13.9 Gsps | 13.5 Gsps |
| RTX PRO 6000 | 92.8 Gsps | 88.3 Gsps |
Sources: README.md:104-114, apps/benchmark_gr_cuda_fft.py:180-210
Summary
Writing Python blocks with CuPy in the gr-cuda project significantly reduces boilerplate code for GPU signal processing. By inheriting from cuda.sync_block or cuda.basic_block, developers gain automatic CUDA stream management, zero-copy buffer handling, and seamless integration with the GNU Radio scheduler, all while maintaining high execution performance through the CuPy library.
Sources: README.md:12-18, python/cuda/cuda_block.py:16-37