Skip to content

Add vendor-agnostic wgpu compute backend - #44

Open
naitikpahwa18 wants to merge 20 commits into
IOES-Lab:ros2from
naitikpahwa18:wgpu_integration
Open

naitikpahwa18 wants to merge 20 commits into
IOES-Lab:ros2from
naitikpahwa18:wgpu_integration

Conversation

@naitikpahwa18

Copy link
Copy Markdown

Summary

This PR removes the CUDA dependency from the multibeam sonar implementation and introduces a modular compute backend architecture. A new WGPU-based compute backend is added along with a CPU reference backend used as a fallback when GPU execution is unavailable.

The sonar sensor was refactored to use this backend interface instead of calling CUDA kernels directly. The WGPU implementation runs the sonar compute stages on the GPU using WGSL shaders, while the CPU implementation preserves the existing physics model for deterministic results.

The build system was updated to remove CUDA requirements and integrate a Rust-based WGPU vendor package. Runtime backend selection is also supported.

Validation

The WGPU backend has been tested on Vulkan; support for Metal and DirectX backends is expected via WGPU but remains untested.

Signed-off-by: Naitik Pahwa <naitikpahwa11@gmail.com>
Copilot AI review requested due to automatic review settings March 16, 2026 20:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the CUDA-dependent multibeam sonar compute pipeline with a modular backend architecture. A WGPU-based GPU backend (using Rust + WGSL shaders) and a CPU reference backend are introduced, selectable at runtime via the DAVE_SONAR_COMPUTE_BACKEND environment variable. The sonar sensor is refactored to use a ComputeBackend interface, and computation is moved to a background thread to avoid blocking the rendering pipeline.

Changes:

  • New ComputeBackend abstraction with WGPU (GPU via WGSL shaders + Rust FFI) and CPU implementations, replacing direct CUDA kernel calls
  • Background compute thread in MultibeamSonarSensor with snapshot-based decoupling from the render thread, plus consistent timestamping for ROS messages
  • Build system migrated from CUDA to a Rust-based wgpu_vendor package with CMake integration, and runtime backend selection via launch arguments

Reviewed changes

Copilot reviewed 26 out of 27 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
multibeam_sonar/sonar_compute_backend.hh New abstract backend interface and data structures
multibeam_sonar/sonar_compute_cpu.cc CPU backend implementation and backend factory
multibeam_sonar/sonar_compute_wgpu.cc/hh WGPU backend: C++ wrapper calling Rust FFI with CPU fallback
multibeam_sonar/MultibeamSonarSensor.cc/hh Background compute thread, snapshot-based pipeline, backend integration
multibeam_sonar/CMakeLists.txt Removed CUDA, added wgpu_vendor dependency
wgpu_vendor/sonar_wgpu_rust/src/lib.rs Rust FFI entry point: GPU dispatch, staging readback, CPU FFT fallback
wgpu_vendor/sonar_wgpu_rust/src/pipeline.rs GPU context singleton: device init, buffer management, pipeline compilation
wgpu_vendor/sonar_wgpu_rust/src/fft.rs CPU FFT (Cooley-Tukey + Bluestein)
wgpu_vendor/sonar_wgpu_rust/src/shaders/*.wgsl WGSL compute shaders: backscatter, convert, matmul, FFT
wgpu_vendor/CMakeLists.txt Rust cargo build integration with CMake
multibeam_sonar_system/CMakeLists.txt Simplified: removed CUDA conditionals
multibeam_sonar_demo/launch/multibeam_sonar_demo.launch.py Added compute_backend launch argument
multibeam_sonar_demo/scripts/plotdata.py Save to file instead of plt.show()
dave_interfaces/CMakeLists.txt Removed gz-cmake3/gz-msgs10 dependencies
models/.../model.sdf Debug comment for non-power-of-2 FFT testing

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread gazebo/dave_gz_multibeam_sonar/wgpu_vendor/sonar_wgpu_rust/src/lib.rs Outdated
Comment thread gazebo/dave_gz_multibeam_sonar/wgpu_vendor/sonar_wgpu_rust/src/lib.rs Outdated
Comment thread gazebo/dave_gz_multibeam_sonar/wgpu_vendor/sonar_wgpu_rust/src/lib.rs Outdated
Comment thread gazebo/dave_gz_multibeam_sonar/wgpu_vendor/sonar_wgpu_rust/src/pipeline.rs Outdated
Comment thread gazebo/dave_gz_multibeam_sonar/wgpu_vendor/CMakeLists.txt Outdated
Comment thread gazebo/dave_gz_multibeam_sonar/wgpu_vendor/sonar_wgpu_rust/src/fft.rs Outdated
Comment thread gazebo/dave_gz_multibeam_sonar/multibeam_sonar/sonar_compute_wgpu.cc Outdated
Comment thread gazebo/dave_gz_multibeam_sonar/wgpu_vendor/sonar_wgpu_rust/src/pipeline.rs Outdated
Comment thread gazebo/dave_gz_multibeam_sonar/multibeam_sonar/CMakeLists.txt
Comment thread gazebo/dave_gz_multibeam_sonar/wgpu_vendor/sonar_wgpu_rust/src/fft.rs Outdated
naitikpahwa18 and others added 2 commits March 19, 2026 00:45
naitikpahwa18 and others added 2 commits April 13, 2026 23:57
@woensug-choi

Copy link
Copy Markdown

@naitikpahwa18 Could you provide quick how-to test document?

Signed-off-by: Naitik Pahwa <naitikpahwa11@gmail.com>
@naitikpahwa18

Copy link
Copy Markdown
Author

Hi @woensug-choi, I've added a quick demo guide in the latest commit. Please let me know if anything else needs to be added.

@kickflipin2

Copy link
Copy Markdown

@naitikpahwa18 Thank you for the quick response! I will give it a spin sometime next week on MacOS

@woensug-choi

woensug-choi commented May 22, 2026 •

Copy link
Copy Markdown

@naitikpahwa18 I've tested on MacOSX with some changes. Note at https://github.com/naitikpahwa18/dave/blob/wgpu_integration/gazebo/DEMO_GUIDE_AppleSilicon_MacOSX.md. Dave currently assumes ROS2 Jazzy and Gazebo Harmonic.

Multibeam Sonar Demo

@naitikpahwa18

Copy link
Copy Markdown
Author

Thanks for testing on MacOSX and updating the docs! (I'm on ROS2 Rolling + Gazebo Jetty on my end)

woensug-choi and others added 2 commits May 25, 2026 23:38
Signed-off-by: Naitik Pahwa <naitikpahwa11@gmail.com>
@naitikpahwa18

Copy link
Copy Markdown
Author

@woensug-choi I have tested the current wgpu implementation with rays=3000, raySkips=10 - it works, though the output is slightly more distorted compared to before.
cuda.webm
wgpu.webm
Perf-wise, CUDA sonar frame calc time is ~36ms vs wgpu's ~60ms.

@woensug-choi

Copy link
Copy Markdown

@naitikpahwa18 Apologies for the delayed response. Although I'm aware of the late ping-pong, I would be delighted to see this agnostic sonar plugin upstreamed. Lack of rays is especially visible when grazing the seabed (e.g. 4. Local Search Scenario at Wiki Doc showing stripe pattern). What do you mean by slightly more distorted?

@naitikpahwa18

Copy link
Copy Markdown
Author

Agreed, increasing rays from 300 to 9000 significantly reduces the stripe pattern, consistent with what you described. Please ignore the "slightly more distorted" comment, I was misinterpreting the RViz visualization at the time.
I’d be happy to keep improving the implementation and work toward getting it upstreamed.

WGPU

Signed-off-by: Yeseol Gwon <172019512+yeseorizi@users.noreply.github.com>
Signed-off-by: Yeseol Gwon <172019512+yeseorizi@users.noreply.github.com>
@yeseorizi

Copy link
Copy Markdown
Collaborator

Hi @naitikpahwa18,

I validated the WGPU multibeam-sonar backend on Apple M2 / Metal and found a possible range-axis mismatch in the non-power-of-two FFT path. I pushed two separate signed-off commits for review:

  • 9a08fa9: candidate fft.wgsl writeback correction
  • 050244d: validation report, CSV results, and figures

Why I changed it

For the default n_freq = 399, the WGPU path zero-pads to padded_n = 512. The original shader then copied smem[i] directly into the original 399-bin output. Because the published ROS range vector still represents 399 physical bins, the measured peak followed approximately the 512 / 399 scale and the far-range tail approached clipping near 10 * 399 / 512 = 7.793 m.

Candidate change

The candidate keeps the 512-point FFT, but during writeback samples the complex padded spectrum at i * padded_n / n_freq and linearly interpolates it back onto the original 399-bin physical range grid.

Controlled validation

  • 2, 4, 6, and 8 m planar targets
  • 0, 15, 30, and 45 degree incidence angles
  • Five frames per backend-condition
  • Independent PointCloud range used to check target placement
Backend Range RMSE
PR #44 CPU 0.0134 m
Original PR #44 WGPU 1.4532 m
Candidate WGPU 0.0540 m

This is a 96.28% RMSE reduction relative to the original WGPU writeback in the 16-condition matrix.

WGPU range-axis validation

Full evidence: WGPU padded-FFT range-axis validation

I also checked n_freq = 301, 511, 512, and 513. The large scaling error was reproduced for 301 -> 512 and 513 -> 1024, while the unmodified 512 -> 512 control remained accurate. The 511 -> 512 case is an important edge case: linear interpolation did not improve its already small error, so an adaptive or no-op policy near a unity padding ratio may be preferable.

NVIDIA CUDA equivalence has not been tested yet, so I am not claiming complete backend equivalence. Could you please review whether this range-axis interpretation matches the intended FFT/writeback design? I would also appreciate your opinion on the 511 -> 512 edge case and any CUDA comparison you think should be run before merging.

We are considering preparing a paper from this numerical analysis and validation. I plan to lead the remaining experiments, analysis, and initial draft; once the technical direction is agreed, I would like to discuss separately how you would like to contribute.

@naitikpahwa18

Copy link
Copy Markdown
Author

Hi @yeseorizi,

I reproduced your matrix on NVIDIA (RTX 4060, Gazebo Jetty): same panel setup, 2/4/6/8 m x 0/15/30/45 deg, 5 frames, peak from the median profile, PointCloud for placement.

1. Range-axis interpretation: confirmed. CUDA runs an exact n = nFreq transform, so the padded output at i * padded_n / n_freq is the exact n_freq-point result at bin i. Your src mapping is the right target.

2. CUDA equivalence:

Backend Platform RMSE (m) MAE (m) Max abs err (m)
Original WGPU Apple M2 (yours) 1.4532 1.3587 1.9582
Candidate WGPU Apple M2 (yours) 0.0540 0.0449 0.1271
CUDA RTX 4060 0.0552 0.0410 0.1455
Original WGPU RTX 4060 / Vulkan 1.4801 1.3775 1.9833
Candidate WGPU RTX 4060 / Vulkan 0.0574 0.0470 0.1522

Candidate matches CUDA to 0.0022 m RMSE, so I'd consider NVIDIA equivalence covered on this matrix. Your Metal numbers reproduce on Vulkan to within a few percent.

3. 511 -> 512: swept six ratios, 4 distances, 0 deg.

n_freq ratio original RMSE candidate RMSE CUDA RMSE
512 1.0000 0.0114 0.0114 0.0083
511 1.0020 0.0191 0.0126 0.0191
505 1.0139 0.0835 0.0166 0.0123
490 1.0449 0.2575 0.0201 0.0390
450 1.1378 0.7599 0.0439 0.0160
399 1.2832 1.4616 0.0327 0.0125

At 511 the candidate comes out slightly better here, where yours came out slightly worse. I don't think either result means much: both differences are smaller than one range bin (~0.02 m), so they sit below what the sonar can resolve.

Everywhere else the candidate is clearly better, by more as the ratio grows. At ratio 1.0 your formula already reduces to a plain copy (src = i, alpha = 0), so the unpadded case needs no guard either. I'd apply the correction unconditionally rather than add a threshold, since any cutoff value would have to be picked from differences we can't measure.

4. On the paper: This is something I'd really like to see happen. The cross-platform angle in particular seems worth writing up properly, and with both sets of validation there's a solid base to build on. Worth talking through what it would cover first. Happy to take that to a separate thread or a call whenever suits you.

@yeseorizi

Copy link
Copy Markdown
Collaborator

Hi @naitikpahwa18,

Thank you for reproducing the experiment and for the detailed comparison. Your NVIDIA results address the main open point from my earlier validation: the candidate WGPU result on RTX 4060/Vulkan (0.0574 m RMSE) is within 0.0022 m of CUDA (0.0552 m), while the original WGPU path retains the large range error. It is also reassuring that the Apple Metal result reproduced on Vulkan within a few percent.

I agree with your interpretation of the 511 -> 512 case. Since the observed differences are below one range bin and the mapping reduces to a plain copy when the padding ratio is 1.0, an additional threshold or no-op branch does not appear necessary. I will keep the correction unconditional.

As a separate follow-up, we also evaluated an exact-length Bluestein WGPU FFT prototype. This is not the implementation in PR #44 and is not an end-to-end DAVE/Gazebo/ROS 2 sonar result, but it provides evidence for a possible future arbitrary-length FFT path:

  • N = {301, 399, 511, 513} with 30 fixed seeds per length (120 cases per platform)
  • NVIDIA RTX 5060 / Vulkan: 120/120 passed, mean complex NRMSE 8.85e-7
  • Apple M2 / Metal: 120/120 passed, mean complex NRMSE 7.75e-7
  • Peak-bin agreement: 120/120 on both platforms; NaN/Inf: 0

These standalone results support the numerical feasibility of an exact-length alternative, but they do not replace the end-to-end validation of the current linear-interpolation correction. For this PR, your CUDA/Vulkan reproduction and sweep provide the directly relevant evidence.

Thank you as well for being open to the paper discussion. I would be happy to discuss the technical scope, remaining experiments, and contributions in a separate thread or call.

@yeseorizi

Copy link
Copy Markdown
Collaborator

Hi @naitikpahwa18,

I have now synchronized the latest ros2 branch (492e926, including the Lyrical / Jetty transition from PR #65) into wgpu_integration. The two CMake conflicts in the multibeam sonar packages were resolved while preserving both sides:

  • Gazebo Jetty dependency targets from the current ros2 branch
  • the WGPU backend and runtime backend selection from this PR
  • optional CUDA support when a CUDA toolkit is available

I also added the macOS dynamic-library search paths required by the sonar and sonar-system plugins. During runtime validation, cv_bridge introduced unnecessary OpenCV ABI coupling for two byte-for-byte BGR8 / RGB8 image copies, so those conversions were replaced with a small local ROS image-message copy and the unused dependency was removed.

Validation completed on Apple M2 / arm64 with ROS 2 Lyrical and Gazebo Jetty:

  • clean wgpu_vendor build completed
  • multibeam_sonar_system and the required demo/runtime packages built successfully
  • DAVE_SONAR_COMPUTE_BACKEND=wgpu selected the Apple M2 Metal adapter
  • the default WGPU multibeam demo ran through 300 GPU frames without a crash
  • pre-commit CI now passes
  • GitHub reports that the branch conflict is resolved and the PR is mergeable

The synchronization is in 06a786c, the macOS runtime portability follow-up is in 61d79f6, and pre-commit applied its formatting-only follow-up in a83a91a.

I updated only the PR branch; PR #44 remains open and has not been merged into ros2.

yeseorizi and others added 3 commits September 23, 2026 12:29
Signed-off-by: Yeseol Gwon <172019512+yeseorizi@users.noreply.github.com>
Publish macOS loader paths for both sonar libraries and replace the two byte-copy cv_bridge conversions with a local ROS image copy. This avoids unnecessary OpenCV ABI coupling while preserving BGR8 and RGB8 message output.

Signed-off-by: Yeseol Gwon <172019512+yeseorizi@users.noreply.github.com>

This branch has not been deployed

No deployments
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.

6 participants