Soft-Output Guessing Random Additive Noise Decoding (SOGRAND) for a cubic (3-dimensional) product code, with a reference C (CPU) implementation and a CUDA (GPU) implementation.
The pipeline generates random data, encodes it with a 3D product code, passes it through a simulated AWGN channel to produce log-likelihood ratios (LLRs), decodes the LLRs with SOGRAND, and compares the result against the original data.
Licensed for non-commercial academic research use — see
GRAND Codebase Non-Commercial Academic Research Use License 021722.pdf.
SOGRAND/
├── SOGRAND_C/ # Reference CPU implementation (C)
│ ├── generate_bin.c # random source-data generator
│ ├── cubic_encoder.c # 3D product-code encoder
│ ├── channel_sim.c # AWGN channel + LLR generation
│ ├── cubic_decoder*.c # SOGRAND decoders (variants)
│ ├── comparator.c # bit-error comparison of input vs. output
│ └── cubic_flow.sh # end-to-end pipeline driver
│
└── SOGRAND_CUDA/ # GPU implementation (CUDA)
├── generate_bin.c # random source-data generator
├── cubic_encoder.c # 3D product-code encoder (CPU)
├── channel_sim.c # AWGN channel + LLR generation (CPU)
├── cubic_decoder1.cu # GPU SOGRAND decoder ← built & run by cubic_flow.sh
├── cubic_decoder.cu # earlier multi-stream GPU variant (not built by default)
├── comparator.c # bit-error comparison of input vs. output
└── cubic_flow.sh # end-to-end pipeline driver
Which decoder is canonical?
SOGRAND_CUDA/cubic_flow.shcompilescubic_decoder1.cu(the "highly optimized" single-launch kernel) into the executable namedcubic_decoder.cubic_decoder.cuis an earlier stream/batch-based variant kept for reference.
| Parameter | Value | Meaning |
|---|---|---|
n |
16 | codeword length per dimension |
k |
8 | message length per dimension |
| Code | (16,8)³ | cubic product code, rate (k/n)³ = 1/8 |
L |
3 | SOGRAND list size |
Imax |
30 | max product-code iterations |
Tmax |
UINT64_MAX (tunable via TMAX_PER_COMPONENT) |
max guesses per component decode (see Performance notes) |
| block | 16³ = 4096 bits/codeword, 8³ = 512 info bits/codeword |
- C toolchain:
gccwith-lm - GPU build: NVIDIA CUDA Toolkit (
nvcc) and a CUDA-capable GPU - Linux (the driver scripts are bash)
cd SOGRAND_CUDA
./cubic_flow.shThis compiles every stage, generates data, runs encode → channel → GPU-decode → compare, and prints decoder statistics (iterations, guesses, and throughput).
To build the GPU decoder on its own:
nvcc -O3 -arch=native -use_fast_math -Xptxas -v \
-Xcompiler -march=native -Xcompiler -O3 \
-o cubic_decoder cubic_decoder1.cu
./cubic_decoder corrupted_llrs_cubic.bin decoded_cubic.bin
-arch=nativetargets the GPU on the build machine. To run the binary on a different GPU, replace it with the appropriate-arch=sm_XX(e.g.sm_80,sm_86,sm_90).
cd SOGRAND_C
./cubic_flow.shEdit the SNR_DB variable at the top of the relevant cubic_flow.sh:
SNR_DB="2.5" # Eb/N0 in dBcubic_decoder1.cu has had the following throughput fixes applied:
- Accurate timing. The Throughput: … blocks/sec figure now comes from
cudaEventtimers around the kernel launch(es), i.e. real device wall-clock. It previously usedclock(), which measures host CPU time and under-counts while the host is blocked incudaDeviceSynchronize()— inflating the number. - No idle threads.
THREADS_PER_BLOCKis now256(=n*n). The component decode only ever runs fortid < n*n, so the previous value of512left half the block idle through the most expensive phase. - Tunable abandonment cap.
Tmaxis exposed as theTMAX_PER_COMPONENTmacro. It defaults toUINT64_MAX(exact C-reference behavior). Setting a finite value (e.g.-DTMAX_PER_COMPONENT=100000) caps worst-case per-line enumeration, which reduces warp divergence and improves throughput at the cost of changed decoded output / BER — benchmark BER before relying on it.
Remaining known limiters (not yet changed, since they need restructuring and GPU profiling to do safely):
- Low occupancy from large per-thread local memory. Each active thread holds
several 16-element
doublevectors plus a 512-byte workspace, and the SOGRAND helper allocates further per-thread arrays — over ~2 KB/thread, which spills registers to local memory (notecudaLimitStackSizeis raised to 16 KB). - Convergence checked after every one of the 3 phases per iteration (up to ~90 full-cube re-encodes with barriers per codeword).
Profile with nvcc -Xptxas -v (register/local-memory usage) and Nsight Compute
(achieved occupancy) before and after any further changes. For reference,
ptxas -v reports a 3744-byte stack frame and 100 registers per thread for
the decode kernel on sm_89 — the local-memory pressure that caps occupancy.
1000 codewords, corrupted_llrs_cubic.bin @ SNR 2.0 dB, all runs BER = 0:
Tmax (TMAX_PER_COMPONENT) |
GPU time | Throughput | BER |
|---|---|---|---|
UINT64_MAX (default) |
75.8 s | 13.19 blocks/s | 0 |
100000 |
75.8 s | 13.19 blocks/s | 0 |
1000 |
68.4 s | 14.61 blocks/s | 0 |
Note that capping Tmax at 100000 has no effect: the average is only
~526 guesses per component decode (≈1.96 M guesses/block over ≈3725 component
decodes), so a generous cap never binds. Only a tight cap (≤1000) helps, and
only by ~11% here. The dominant cost is the sheer volume of serial enumeration
plus the low occupancy above — not the abandonment cap.