Skip to content

feat: GPU-accelerated downconvert and correlate via KernelAbstractions.jl#99

Open
zsoerenm wants to merge 40 commits into
masterfrom
ss/ka-downconvert-and-correlate
Open

feat: GPU-accelerated downconvert and correlate via KernelAbstractions.jl#99
zsoerenm wants to merge 40 commits into
masterfrom
ss/ka-downconvert-and-correlate

Conversation

@zsoerenm

@zsoerenm zsoerenm commented Mar 5, 2026

Copy link
Copy Markdown
Member

Summary

Add a portable GPU implementation of the GNSS downconvert-and-correlate pipeline using KernelAbstractions.jl. The kernel fuses carrier wipe-off, code lookup, and correlation into a single pass with in-kernel workgroup reduction via shared memory.

The journey

v1: Naive GPU kernel (baseline)

Started with a straightforward port: per-sample sincos() for carrier, Float64 accumulators, per-thread partial sums transferred back to CPU for reduction. This was slower than CPU for all configurations due to the massive GPU→CPU transfer of partial arrays and expensive FP64 sincos on GPU.

v2: In-kernel reduction + ComplexF64 results

Replaced CPU-side reduction with in-kernel tree reduction using shared memory (@LocalMem). Only a compact ComplexF64 result array (sats × ants × taps) is transferred back. Combined the separate Float64 re/im arrays into a single ComplexF64 array, halving the number of GPU→CPU copies. Also introduced Val{num_taps} for compile-time kernel specialization, letting @Private allocate exact-sized accumulators and enabling loop unrolling.

v3: Cross-system batching + per-system kernel launches

Added support for batching multiple GNSS systems (e.g., GPSL1 + GalileoE1B) into the same struct. Initially tried a single kernel with a tuple of code tables and per-satellite system_idx — but the @generated dispatch overhead caused a 25-34% regression. Switched to per-system kernel launches, each specialized at compile time for modulation type (LOC/BOC/CBOC), code length, and num_taps. This recovered the regression and added GalileoE1B (CBOC) support.

v4: Carrier rotation + FP32 accumulation

Replaced per-sample sincos() with incremental carrier rotation using FP32 multiply-add (Givens rotation). FP32 accumulators during the inner loop, promoting to FP64 only at the final reduction step. On RDNA 4 (Radeon 8060S): ~1.7x kernel speedup at 25K samples.

v5: Combined-tap reduction

For ≤8 correlator taps (the common case), replaced sequential per-tap reduction (num_taps × 8 barriers) with a single combined pass storing all taps in shared memory simultaneously (8 barriers total). ~22% kernel speedup for EarlyPromptLate (3 taps).

v6: Subcarrier LUT + tap phase hoisting

Precomputed subcarrier values (BOC/CBOC) into a lookup table indexed by sub-chip phase. Hoisted per-tap code phase offsets out of the inner loop. LUT size as compile-time Val eliminates dead subcarrier code for LOC signals (GPSL1). GalileoE1B CBOC: 214→127μs (1.69x).

v7: CPU-generated code replicas (dead end)

Tried pre-generating code replicas on CPU via gen_code_replica! and uploading per call. The kernel became trivially fast (pure indexed reads, no floor/mod) — GPSL1 72→39μs, GalE1B 127→55μs — but the PCIe transfer of code replicas dominated: 175μs for E1B 16sat (47% of total time), plus 79μs for CPU generation (21%). Together: 68% overhead. This approach only won at very high satellite counts.

v8: Fixed-point LUT on GPU (the winner)

Returned to GPU-resident code tables with subcarrier baked into expanded LUTs (e.g., E1B: 4092 chips × 12 sub-per-chip = 49104 entries/PRN), but replaced the expensive float floor+mod code lookup from v6 with fixed-point integer arithmetic.

The key insight: encode code phase as a fixed-point integer (fractional bits after the radix point). Code chip index = phase >> fractional_bits. Sub-chip index for the expanded LUT comes naturally from the integer phase. No floor(), no float-to-int conversion.

Two modes via parametric phase type:

  • Int32 (18 fractional bits): Fast, ~4% sub-chip quantization error. Max expanded phase ~1.6B fits Int32.
  • Int64 (32 fractional bits): Default. Zero quantization errors across 100K samples (verified against BigFloat reference). Eliminates the 3/100K errors that even the CPU accumulator approach produces.

v9: Accumulate+wrap (final optimization)

Profiling showed the kernel was 86% of total time, and within the kernel, Int64 mod (emulated as ~20 ALU instructions on AMD GPU) was the bottleneck. Microbenchmarked four alternatives:

  • mod(Int64): 28.4μs (baseline)
  • conditional subtract: 18.7μs
  • mod(Int32) truncated: 22.5μs
  • accumulate+wrap: 15.1μs (1.88x faster)

The accumulate+wrap pattern tracks code phase as a running accumulator, initialized once with mod(), then advanced by delta×stride per grid step with a branchless conditional subtract for wrapping.

Final benchmark results (AMD Radeon 8060S, RDNA 4)

Config GPU (μs) CPU (μs) GPU/CPU
L1 4sat/5K 34.9 9.0 3.88x
L1 16sat/5K 36.9 36.7 1.01x
E1B 4sat/25K 58.3 47.6 1.22x
E1B 16sat/25K 61.3 187.8 0.33x
E1B 4sat/100K 129.7 228.2 0.57x
E1B 16sat/100K 131.2 919.8 0.14x
4L1+4E1B/25K 86.3 84.5 1.02x
8L1+8E1B/25K 87.9 167.8 0.52x
8L1+8E1B/100K 157.5 515.1 0.31x

GPU/CPU < 1.0 means GPU is faster. The GPU wins decisively for E1B with ≥16 satellites (3-7x faster) and for multi-system configurations (2-3x faster). L1 with few satellites still favors CPU due to the ~35μs fixed GPU launch overhead.

CUDA (NVIDIA A100-PCIE-40GB MIG 1g.5gb)

Config CPU (median) KA-CUDA (median) Speedup
8L1+8E1B/25K 727.825 μs 143.529 μs 5.1x
E1B 8sat/100K 1.575 ms 199.019 μs 7.9x
E1B 8sat/25K 399.538 μs 99.840 μs 4.0x
L1 1sat/5K 8.869 μs 65.569 μs 0.1x
L1 8sat/5K 70.500 μs 69.470 μs 1.0x

Test plan

  • All existing tests pass (CPU backend via KernelAbstractions)
  • GPSL1 KA vs CPU: atol=25
  • GalileoE1B KA vs CPU: Int64 (default) rtol=0.01, Int32 rtol=0.1
  • Multi-system GPSL1+GalileoE1B: KA vs CPU
  • Manual GPU benchmark on AMD hardware

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Mar 5, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results

Time benchmarks (vs base branch)
ss/cpu-threade... fef711d... ss/cpu-threade... / fef711d...
downconvert and correlate/CPU/Float32 2.46 ± 0.02 μs 2.46 ± 0.02 μs 0.996 ± 0.011
downconvert and correlate/CPU/Float32 4ant 4.64 ± 0.031 μs 4.65 ± 0.03 μs 0.998 ± 0.0093
downconvert and correlate/CPU/Float64 2.83 ± 0.02 μs 2.79 ± 0.03 μs 1.01 ± 0.013
downconvert and correlate/CPU/Int16 2.6 ± 0.02 μs 2.58 ± 0.02 μs 1.01 ± 0.011
downconvert and correlate/CPU/Int16 4ant 5.29 ± 0.031 μs 5.33 ± 0.031 μs 0.992 ± 0.0082
downconvert and correlate/CPU/Int32 2.5 ± 0.02 μs 2.5 ± 0.021 μs 0.996 ± 0.012
fused kernel/1-ant dynamic taps 2.18 ± 0.01 μs 2.37 ± 0.011 μs 0.92 ± 0.006
fused kernel/1-ant static taps 2.03 ± 0.01 μs 2.02 ± 0.01 μs 1 ± 0.007
fused kernel/4-ant dynamic taps 5.35 ± 0.03 μs 5.97 ± 0.041 μs 0.896 ± 0.0079
fused kernel/4-ant static taps 4.17 ± 0.02 μs 4.17 ± 0.02 μs 1 ± 0.0068
multi-sat/8L1+8E1B/25K 0.386 ± 0.099 ms 0.385 ± 0.099 ms 1 ± 0.36
multi-sat/E1B 4sat/25K 0.0953 ± 0.0037 ms 0.0966 ± 0.0036 ms 0.987 ± 0.053
multi-sat/L1 8sat/5K 0.0364 ± 0.0023 ms 0.0366 ± 0.0022 ms 0.997 ± 0.086
track/Float32 2.71 ± 0.058 μs 2.76 ± 0.06 μs 0.985 ± 0.03
time_to_load 1.11 ± 0.014 s 1.1 ± 0.01 s 1 ± 0.016
Memory benchmarks (vs base branch)
ss/cpu-threade... fef711d... ss/cpu-threade... / fef711d...
downconvert and correlate/CPU/Float32 2 allocs: 0.359 kB 2 allocs: 0.359 kB 1
downconvert and correlate/CPU/Float32 4ant 2 allocs: 0.641 kB 2 allocs: 0.641 kB 1
downconvert and correlate/CPU/Float64 2 allocs: 0.359 kB 2 allocs: 0.359 kB 1
downconvert and correlate/CPU/Int16 2 allocs: 0.359 kB 2 allocs: 0.359 kB 1
downconvert and correlate/CPU/Int16 4ant 2 allocs: 0.641 kB 2 allocs: 0.641 kB 1
downconvert and correlate/CPU/Int32 2 allocs: 0.359 kB 2 allocs: 0.359 kB 1
fused kernel/1-ant dynamic taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/1-ant static taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/4-ant dynamic taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/4-ant static taps 0 allocs: 0 B 0 allocs: 0 B
multi-sat/8L1+8E1B/25K 0.275 k allocs: 16.2 kB 0.275 k allocs: 16.2 kB 1
multi-sat/E1B 4sat/25K 0.077 k allocs: 5.42 kB 0.077 k allocs: 5.42 kB 1
multi-sat/L1 8sat/5K 0.13 k allocs: 7.05 kB 0.13 k allocs: 7.05 kB 1
track/Float32 12 allocs: 1.67 kB 12 allocs: 1.67 kB 1
time_to_load 0.145 k allocs: 11 kB 0.145 k allocs: 11 kB 1
Time benchmarks (vs master)
master fef711d... master / fef711d...
downconvert and correlate/CPU/Float32 3.03 ± 0.03 μs 2.46 ± 0.02 μs 1.23 ± 0.016
downconvert and correlate/CPU/Float32 4ant 5.82 ± 0.12 μs 4.65 ± 0.03 μs 1.25 ± 0.027
downconvert and correlate/CPU/Float64 3.5 ± 0.03 μs 2.79 ± 0.03 μs 1.25 ± 0.017
downconvert and correlate/CPU/Int16 3.06 ± 0.029 μs 2.58 ± 0.02 μs 1.19 ± 0.015
downconvert and correlate/CPU/Int16 4ant 5.9 ± 0.081 μs 5.33 ± 0.031 μs 1.11 ± 0.017
downconvert and correlate/CPU/Int32 3.06 ± 0.029 μs 2.5 ± 0.021 μs 1.22 ± 0.015
multi-sat/8L1+8E1B/25K 0.6 ± 0.011 ms 0.385 ± 0.099 ms 1.56 ± 0.4
multi-sat/E1B 4sat/25K 0.163 ± 0.0045 ms 0.0966 ± 0.0036 ms 1.68 ± 0.078
multi-sat/L1 8sat/5K 0.0563 ± 0.00022 ms 0.0366 ± 0.0022 ms 1.54 ± 0.091
track/Float32 3.31 ± 0.07 μs 2.76 ± 0.06 μs 1.2 ± 0.036
fused kernel/4-ant static taps 4.17 ± 0.02 μs
fused kernel/1-ant static taps 2.02 ± 0.01 μs
fused kernel/1-ant dynamic taps 2.37 ± 0.011 μs
fused kernel/4-ant dynamic taps 5.97 ± 0.041 μs
time_to_load 1.02 ± 0.018 s 1.1 ± 0.01 s 0.926 ± 0.018
Memory benchmarks (vs master)
master fef711d... master / fef711d...
downconvert and correlate/CPU/Float32 2 allocs: 0.359 kB 2 allocs: 0.359 kB 1
downconvert and correlate/CPU/Float32 4ant 4 allocs: 0.766 kB 2 allocs: 0.641 kB 1.2
downconvert and correlate/CPU/Float64 2 allocs: 0.359 kB 2 allocs: 0.359 kB 1
downconvert and correlate/CPU/Int16 2 allocs: 0.359 kB 2 allocs: 0.359 kB 1
downconvert and correlate/CPU/Int16 4ant 4 allocs: 0.766 kB 2 allocs: 0.641 kB 1.2
downconvert and correlate/CPU/Int32 2 allocs: 0.359 kB 2 allocs: 0.359 kB 1
multi-sat/8L1+8E1B/25K 6 allocs: 5.52 kB 0.275 k allocs: 16.2 kB 0.34
multi-sat/E1B 4sat/25K 2 allocs: 1.62 kB 0.077 k allocs: 5.42 kB 0.3
multi-sat/L1 8sat/5K 3 allocs: 2.45 kB 0.13 k allocs: 7.05 kB 0.347
track/Float32 12 allocs: 1.67 kB 12 allocs: 1.67 kB 1
fused kernel/4-ant static taps 0 allocs: 0 B
fused kernel/1-ant static taps 0 allocs: 0 B
fused kernel/1-ant dynamic taps 0 allocs: 0 B
fused kernel/4-ant dynamic taps 0 allocs: 0 B
time_to_load 0.145 k allocs: 11 kB 0.145 k allocs: 11 kB 1

@codecov

codecov Bot commented Mar 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.32258% with 21 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.46%. Comparing base (f48f594) to head (fef711d).
⚠️ Report is 2 commits behind head on ss/cpu-threaded-downconvert-and-correlate.

Files with missing lines Patch % Lines
src/downconvert_and_correlate_ka.jl 90.18% 21 Missing ⚠️
Additional details and impacted files
@@                              Coverage Diff                              @@
##           ss/cpu-threaded-downconvert-and-correlate      #99      +/-   ##
=============================================================================
+ Coverage                                      86.80%   94.46%   +7.65%     
=============================================================================
  Files                                             24       23       -1     
  Lines                                            796      940     +144     
=============================================================================
+ Hits                                             691      888     +197     
+ Misses                                           105       52      -53     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@zsoerenm
zsoerenm force-pushed the ss/ka-downconvert-and-correlate branch 5 times, most recently from 27a042a to 4d39905 Compare March 9, 2026 12:21
@zsoerenm
zsoerenm changed the base branch from master to ss/cpu-threaded-downconvert-and-correlate March 9, 2026 12:23
@zsoerenm
zsoerenm force-pushed the ss/ka-downconvert-and-correlate branch 3 times, most recently from b1d66f2 to 153ed5a Compare March 9, 2026 12:41
@zsoerenm
zsoerenm force-pushed the ss/cpu-threaded-downconvert-and-correlate branch from 6d675ec to 73abb76 Compare March 9, 2026 12:47
@zsoerenm
zsoerenm force-pushed the ss/ka-downconvert-and-correlate branch from 153ed5a to 574c27d Compare March 9, 2026 12:48
@zsoerenm
zsoerenm force-pushed the ss/cpu-threaded-downconvert-and-correlate branch 2 times, most recently from b8b08a7 to fa0ae1c Compare March 9, 2026 12:59
zsoerenm and others added 13 commits March 9, 2026 14:05
Replace separate carrier replica generation and downconversion with a
fused approach that generates the carrier on-the-fly during downconversion.
This eliminates the carrier replica buffer, saving memory bandwidth.

- Add @generated _fused_downconvert_unrolled! that unrolls the antenna
  dimension at compile time via NumAnts, allowing @avx to vectorize purely
  along the sample dimension while computing sincos once per sample
- Update downconvert_and_correlate! to use fused downconvert! with NumAnts
- Remove carrier_replica buffer allocation from @no_escape block
- Add 4-antenna benchmark to benchmark/benchmarks.jl

~11-18% faster across all antenna configurations (1-4 antennas).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…eneration

- Eliminate carrier replica buffer by generating carrier on-the-fly during
  downconversion using FastSinCos.fast_sincos_u100k (SIMD)
- Use @nexprs for 4x loop unrolling, _deinterleave_load with shufflevector
  for interleaved complex signal loading
- Auto-detect SIMD width via VectorizationBase.pick_vector_width (supports
  AVX2, AVX-512, ARM NEON)
- Merge single/multi-antenna into one method (compiler unrolls for j in 1:1)
- Remove unused StructArray fused downconvert methods
- ~15% faster than master, zero allocations in downconvert!, 56% fewer lines

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Single-pass kernel that generates the carrier on-the-fly, downconverts,
and immediately accumulates against shifted code replicas — keeping
downconverted samples in SIMD registers instead of writing to an
intermediate buffer. Supports both single-antenna and multi-antenna
correlators with 4xW unrolled, 1xW cleanup, and scalar remainder loops.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use @generated to fully unroll M×NC accumulator dimensions, keeping
  all accumulators in named local variables (registers) instead of
  heap-allocated arrays
- Precompute per-tap code pointers (p_code_k) before the loop, removing
  repeated sample_shifts[k] - min_shift from the hot path
- Use muladd-based phase computation: precompute per-vector initial
  phases and broadcast a single muladd per iteration (drift-free)
- Remove downconvert signal buffer allocation from CPU path since the
  fused kernel keeps downconverted samples in registers
- Wire fused kernel into downconvert_and_correlate! replacing the
  separate downconvert! + correlate calls
- Add test comparing fused vs split path results

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The fused downconvert+correlate kernel replaces all @avx-based split
paths (gen_carrier_replica!, downconvert!, correlate), eliminating
the need for LoopVectorization and VectorizationBase. StructArrays
is no longer used since the fused kernel works directly on interleaved
complex arrays. Also removes reshape allocations in the 1-antenna
dynamic path (0 allocs across all variants), cleans up benchmarks
by removing legacy PACKAGE_VERSION branches, and adds fused kernel
microbenchmarks for 1-ant/4-ant static/dynamic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use VectorizationBase.pick_vector_width(T) instead of hardcoded
256-bit assumption, so the SIMD width adapts to the actual CPU
features (AVX-512, etc.).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
[sources] in Project.toml is not supported in Julia 1.10, which is
needed for the unregistered FastSinCos.jl dependency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrap fused kernel microbenchmarks in isdefined check so the
benchmark script works on master where the function doesn't exist.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Test fused kernel against independent scalar reference (rtol=1e-4)
  instead of only comparing dynamic vs static paths
- Add strict cross-path comparison (rtol=1e-6)
- Rename benchmark labels to "static taps"/"dynamic taps"
- Fix benchmark CI by using AirspeedVelocity fork with [sources] support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SIMD codegen differs between Julia 1.11 and 1.12, causing larger
floating-point accumulation differences (~1e-5) between the static
and dynamic paths on 1.11.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… head

Add multi-antenna (M=2) test for the fused downconvert+correlate kernel,
covering the _add_antenna SVector path and multi-antenna branches in both
static (@generated) and dynamic (AbstractVector) code paths.

Fix benchmark CI by using PR HEAD SHA for --bench-on instead of default
branch, since master's benchmarks.jl still has `using CUDA`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@zsoerenm
zsoerenm force-pushed the ss/cpu-threaded-downconvert-and-correlate branch from fa0ae1c to 20e0bb2 Compare March 9, 2026 13:05
Use non-SIMD-aligned signal length (5003) to exercise the scalar
remainder loop (lines 310-320) in the dynamic-shifts multi-antenna
branch of the fused kernel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
zsoerenm and others added 4 commits March 9, 2026 15:10
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…al config

Add test for signal_samples_to_integrate == 0 branch in
CPUThreadedDownconvertAndCorrelator to achieve full patch coverage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@zsoerenm
zsoerenm force-pushed the ss/cpu-threaded-downconvert-and-correlate branch from 46924de to dbe91a9 Compare March 9, 2026 14:10
@zsoerenm
zsoerenm force-pushed the ss/ka-downconvert-and-correlate branch from 574c27d to 9779aa1 Compare March 9, 2026 14:53
zsoerenm and others added 19 commits March 9, 2026 17:41
_to_vec and _make_offset used ntuple() to construct SIMD vectors, which
Julia failed to stack-allocate for N=16 (AVX-512). This caused 149K heap
allocations per call on AVX-512 machines, making the CPU path ~275x
slower than necessary.

Fix: convert both to @generated functions that emit literal tuple
constructors, ensuring the compiler sees constant N at generation time.

Before (AVX-512): L1 8sat/5K = 3,522 μs, 149K allocs
After  (AVX-512): L1 8sat/5K = 12.8 μs, 3 allocs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…s.jl

Add a portable GPU implementation of the GNSS downconvert-and-correlate
pipeline using KernelAbstractions.jl. The kernel fuses carrier wipe-off,
code lookup, and correlation into a single pass with in-kernel workgroup
reduction via shared memory.

Started with a straightforward port: per-sample sincos() for carrier,
Float64 accumulators, per-thread partial sums transferred back to CPU
for reduction. This was slower than CPU for all configurations due to
the massive GPU→CPU transfer of partial arrays and expensive FP64
sincos on GPU.

Replaced CPU-side reduction with in-kernel tree reduction using shared
memory (@LocalMem). Only a compact ComplexF64 result array (sats × ants
× taps) is transferred back. Combined the separate Float64 re/im arrays
into a single ComplexF64 array, halving the number of GPU→CPU copies.
Also introduced Val{num_taps} for compile-time kernel specialization,
letting @Private allocate exact-sized accumulators and enabling loop
unrolling.

Added support for batching multiple GNSS systems (e.g., GPSL1 +
GalileoE1B) into the same struct. Initially tried a single kernel with
a tuple of code tables and per-satellite system_idx — but the @generated
dispatch overhead caused a 25-34% regression. Switched to per-system
kernel launches, each specialized at compile time for modulation type
(LOC/BOC/CBOC), code length, and num_taps. This recovered the
regression and added GalileoE1B (CBOC) support.

Replaced per-sample sincos() with incremental carrier rotation using
FP32 multiply-add (Givens rotation). FP32 accumulators during the inner
loop, promoting to FP64 only at the final reduction step. On RDNA 4
(Radeon 8060S): ~1.7x kernel speedup at 25K samples.

For ≤8 correlator taps (the common case), replaced sequential per-tap
reduction (num_taps × 8 barriers) with a single combined pass storing
all taps in shared memory simultaneously (8 barriers total). ~22%
kernel speedup for EarlyPromptLate (3 taps).

Precomputed subcarrier values (BOC/CBOC) into a lookup table indexed by
sub-chip phase. Hoisted per-tap code phase offsets out of the inner
loop. LUT size as compile-time Val eliminates dead subcarrier code for
LOC signals (GPSL1). GalileoE1B CBOC: 214→127μs (1.69x).

Tried pre-generating code replicas on CPU via gen_code_replica! and
uploading per call. The kernel became trivially fast (pure indexed reads,
no floor/mod) — GPSL1 72→39μs, GalE1B 127→55μs — but the PCIe transfer
of code replicas dominated: 175μs for E1B 16sat (47% of total time),
plus 79μs for CPU generation (21%). Together: 68% overhead. This
approach only won at very high satellite counts.

Returned to GPU-resident code tables with subcarrier baked into expanded
LUTs (e.g., E1B: 4092 chips × 12 sub-per-chip = 49104 entries/PRN),
but replaced the expensive float floor+mod code lookup from v6 with
fixed-point integer arithmetic.

The key insight: encode code phase as a fixed-point integer (fractional
bits after the radix point). Code chip index = phase >> fractional_bits.
Sub-chip index for the expanded LUT comes naturally from the integer
phase. No floor(), no float-to-int conversion.

Two modes via parametric phase type:
- Int32 (18 fractional bits): Fast, ~4% sub-chip quantization error.
  Max expanded phase ~1.6B fits Int32.
- Int64 (32 fractional bits): Default. Zero quantization errors across
  100K samples (verified against BigFloat reference). Eliminates the
  3/100K errors that even the CPU accumulator approach produces.

Profiling showed the kernel was 86% of total time, and within the
kernel, Int64 mod (emulated as ~20 ALU instructions on AMD GPU) was
the bottleneck. Microbenchmarked four alternatives:
- mod(Int64): 28.4μs (baseline)
- conditional subtract: 18.7μs
- mod(Int32) truncated: 22.5μs
- accumulate+wrap: 15.1μs (1.88x faster)

The accumulate+wrap pattern tracks code phase as a running accumulator,
initialized once with mod(), then advanced by delta×stride per grid
step with a branchless conditional subtract for wrapping. Per-tap
offsets use branchless wrap for negative phases (early correlator).

| Config           | GPU (μs) | CPU (μs) | GPU/CPU |
|------------------|----------|----------|---------|
| L1 4sat/5K       |     34.9 |      9.0 |   3.88x |
| L1 16sat/5K      |     36.9 |     36.7 |   1.01x |
| E1B 4sat/25K     |     58.3 |     47.6 |   1.22x |
| E1B 16sat/25K    |     61.3 |    187.8 |   0.33x |
| E1B 4sat/100K    |    129.7 |    228.2 |   0.57x |
| E1B 16sat/100K   |    131.2 |    919.8 |   0.14x |
| 4L1+4E1B/25K     |     86.3 |     84.5 |   1.02x |
| 8L1+8E1B/25K     |     87.9 |    167.8 |   0.52x |
| 8L1+8E1B/100K    |    157.5 |    515.1 |   0.31x |

GPU/CPU < 1.0 means GPU is faster. The GPU wins decisively for E1B
with ≥16 satellites (3-7x faster) and for multi-system configurations
(2-3x faster). L1 with few satellites still favors CPU due to the
~35μs fixed GPU launch overhead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Compares the old CUDA extension (texture memory), the new KA implementation
(Int32 and Int64 modes), and CPU across GPSL1, GalileoE1B, and multi-system
configurations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Detects available backends (CUDA, AMDGPU) at runtime and benchmarks
KernelAbstractions.jl (Int32/Int64) alongside CPU. Also benchmarks the
old CUDA texture memory extension when available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Split benchmarks.jl into:
- bench_cpu.jl: CPU downconvert-and-correlate + track suite
- bench_gpu_vs_cpu.jl: GPU (CUDA-ext, KA+CUDA, KA+AMDGPU) vs CPU suite

benchmarks.jl now includes both and merges their suites. The GPU
benchmark gracefully skips KA when not available (e.g., on master).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The BenchmarkTools UUID was wrong (copy-paste error), causing
Pkg.instantiate() to fail on CI. Also fix BenchmarkGroup composition
in benchmarks.jl — merge! is not supported, use iteration instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Int64 with accumulate+wrap is both faster and more accurate than Int32:
- E1B 8sat/25K: 60μs (Int64) vs 85μs (Int32)
- E1B 8sat/100K: 132μs (Int64) vs 236μs (Int32)

Removes ~400 lines: Int32 kernels, param packing, phase_type kwarg,
and associated tests/benchmarks. The struct drops the P type parameter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Merge ka_dc_kernel! and ka_dc_multi_ant_kernel! into a single kernel
that takes num_ants as a parameter. For single-antenna (num_ants=1),
signal[i, 1] works for both vectors and matrices in Julia.

Removes ~170 lines of duplication with zero performance regression
(benchmarked on AMD Radeon 8060S).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verify that KADownconvertAndCorrelator (CPU backend) converges to
correct code phase and carrier phase over 2000 tracking iterations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GPUDownconvertAndCorrelator requires homogeneous NTuple type, so
multi-system (GPSL1+GalileoE1B) benchmarks can't use the old extension.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…enchmarks

Replace the texture-memory CUDA extension (TrackingCUDAExt) with
KernelAbstractions.jl-based GPU support throughout. Add CUDA-conditional
tests for downconvert_and_correlate and tracking. Update Buildkite to
run KA+CUDA tests and GPU benchmarks with annotated results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace @Spawn with @threads for zero-allocation threading
- Add fused downconvert! that generates carrier on-the-fly, eliminating
  carrier_replica buffer and saving memory bandwidth
- Use @generated to unroll antenna dimension at compile time via NumAnts,
  giving optimal SIMD for all antenna counts (1.3x faster at 1 ant,
  1.25x at 2 ant, ~equal at 4-8 ant vs unfused)
- Remove carrier_replica_re/im fields from CPUThreadedDownconvertAndCorrelator
- Add max_num_samples kwarg to constructor to fix DimensionMismatch
- Detect low-CU iGPUs and limit GPU benchmark configs to avoid display freeze
- Access Dictionary internals directly (states.values[i]) to avoid allocation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Wrap CPUThreadedDownconvertAndCorrelator and KADownconvertAndCorrelator
  usage with isdefined guards so benchmarks don't fail on master where
  these types don't exist.
- Set JULIA_NUM_THREADS=auto in Buildkite benchmark step so CPU-Threaded
  actually uses multiple cores.
- Restructure GPU benchmark table: single table per GPU backend showing
  GPU, CPU-Threaded, CPU columns with speedup ratios (vs CPU, vs Threaded).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GPU benchmarks require a GPU runner and are run separately via Buildkite
(benchmark/run_gpu_benchmarks.jl). Including them in the AirspeedVelocity
SUITE caused errors on master where the multi-system CPU correlator
buffer allocation failed with size(::Val{...}, ::Int64).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The benchmark suite covers all backends (CPU, CPU-Threaded, KA-CUDA,
KA-AMDGPU) for multi-satellite/multi-system workloads, not just GPU.

Renames:
- bench_gpu_vs_cpu.jl → bench_multi_sat.jl
- run_gpu_benchmarks.jl → run_multi_sat_benchmarks.jl
- gpu_suite() → multi_sat_suite()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Julia's soft scope rules treat assignment inside a for loop as a new
local variable. Add explicit `global` to update the outer variable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@zsoerenm
zsoerenm force-pushed the ss/ka-downconvert-and-correlate branch from 4c5aafc to 091934b Compare March 9, 2026 16:42
Remove [sources] section now that FastSinCos is in the registry.
Lower minimum Julia version from 1.11 to 1.10 across Project.toml,
CI, and Buildkite. Add Julia latest to Buildkite GPU test matrix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@zsoerenm
zsoerenm force-pushed the ss/cpu-threaded-downconvert-and-correlate branch 2 times, most recently from 83f89dc to 62f4e8f Compare March 25, 2026 08:39
Base automatically changed from ss/cpu-threaded-downconvert-and-correlate to master March 25, 2026 10:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant