Skip to content

Commit 897abb4

Browse files
committed
Add optional CUDA mapping backend (experimental)
The mapping update can now run on the CUDA implementation released as CuOneMap (https://github.com/finnBsch/CuOneMap) instead of the PyTorch one. Selected with MappingConf.use_cuda_backend or ONEMAP_BACKEND=cuda, and falls back to the PyTorch implementation with a warning when the cuonemap package is not installed, so this changes nothing for existing setups. create_onemap() picks the backend; CUDAOneMap in mapping/cuda_feature_map.py adapts cuonemap.OneMap to the interface of the OneMap class, handling the (F, H, W) to (H, W, F) layout change and host/device transfers. Verified against the PyTorch class over a trajectory: every map agrees to float tolerance, the derived boolean maps agree exactly, and metric_to_px matches. The backend intentionally omits one preprocessing step (the depth-gradient max-pool), so results are close but not bit-identical. The README section and the CuOneMap README document that and the other known differences. Also ignore profiling artifacts (*.nsys-rep, *.ncu-rep, *.sqlite).
1 parent 0b565fd commit 897abb4

6 files changed

Lines changed: 264 additions & 5 deletions

File tree

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@ results*/
88
*.pt
99
yolov7
1010
weights
11-
datasets
11+
datasets
12+
# Profiling artifacts
13+
*.nsys-rep
14+
*.ncu-rep
15+
*.sqlite

README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,38 @@ In summary we open-source:
2929
- The multi-object navigation dataset generation code, such that you can generate your own datasets
3030

3131
## Changes
32+
- [29/07/2026]: Released the CUDA port of the mapping backend as [CuOneMap](https://github.com/finnBsch/CuOneMap), usable from here as an experimental drop-in backend. See [CUDA backend](#cuda-backend-experimental).
3233
- [28/10/2025]: Docker build to CUDA 12.8 for RTX 50 series support. Fixed issues with reading the results for multi-object nav.
3334

3435
## Upcoming Changes
3536
- Change annotation format for multi-object nav to match paper naming, see below.
36-
- Release full CUDA port of onemap.
37+
38+
## CUDA backend (experimental)
39+
40+
The mapping update in `mapping/feature_map.py` is also available as a CUDA implementation,
41+
[CuOneMap](https://github.com/finnBsch/CuOneMap). It is a drop-in replacement that reproduces the same maps to
42+
float tolerance: roughly 3x faster at the map settings shipped here, and up to 40x on large maps with wide blur
43+
kernels. It can keep the feature map in system RAM, so GPU memory stays available for the vision models.
44+
45+
Install it without cloning:
46+
47+
```
48+
pip install git+https://github.com/finnBsch/CuOneMap.git
49+
```
50+
51+
Then enable it in your mapping config:
52+
53+
```yaml
54+
MappingConf:
55+
use_cuda_backend: True
56+
```
57+
58+
or set `ONEMAP_BACKEND=cuda`. If the module is not installed, OneMap falls back to the PyTorch implementation.
59+
60+
This backend is **experimental**. One preprocessing step differs from the PyTorch version on purpose, so results
61+
are close but not bit-identical — the [CuOneMap README](https://github.com/finnBsch/CuOneMap#differences-from-the-reference)
62+
lists every known difference, along with benchmarks and scripts to verify it against this implementation. We would
63+
love to hear how it works for you: please open an issue with feedback or problems.
3764

3865
## Abstract
3966
The capability to efficiently search for objects in complex environments is fundamental for many real-world robot

config/mapping_conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,5 @@ class MappingConf:
2323
floor_level: float
2424
floor_threshold: float
2525

26+
use_cuda_backend: bool = False
27+

mapping/__init__.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
__all__ = [
22
'OneMap',
3+
'create_onemap',
34
'Navigator',
45
'FusionType',
56
'DenseProjectionType',
@@ -16,6 +17,9 @@
1617
'cluster_thermal_image'
1718
]
1819

20+
import os
21+
import warnings
22+
1923
from mapping.nav_goals.frontier import detect_frontiers, get_frontier_midpoint, Frontier
2024

2125
from mapping.nav_goals.navigation_goals import NavGoal
@@ -27,7 +31,49 @@
2731

2832
from .feature_map import OneMap, FusionType, DenseProjectionType
2933

30-
from .navigator import Navigator
3134

35+
def _cuda_available():
36+
try:
37+
import cuonemap
38+
return True
39+
except ImportError:
40+
return False
41+
42+
43+
def create_onemap(feature_dim, config,
44+
dense_projection=DenseProjectionType.INTERPOLATE,
45+
fusion_type=FusionType.EMA,
46+
map_device="cuda",
47+
use_cuda=None):
48+
"""Factory function to create a OneMap instance with the appropriate backend.
49+
50+
Args:
51+
use_cuda: If None, auto-detect from config.use_cuda_backend or
52+
ONEMAP_BACKEND=cuda env var. If True/False, force that backend.
53+
"""
54+
if use_cuda is None:
55+
use_cuda = (
56+
getattr(config, 'use_cuda_backend', False)
57+
or os.environ.get("ONEMAP_BACKEND", "").lower() == "cuda"
58+
)
3259

60+
if use_cuda:
61+
if not _cuda_available():
62+
warnings.warn(
63+
"CUDA backend requested but the 'cuonemap' package is not installed. "
64+
"Falling back to the PyTorch implementation. Install with: "
65+
"pip install git+https://github.com/finnBsch/CuOneMap.git"
66+
)
67+
use_cuda = False
3368

69+
if use_cuda:
70+
from .cuda_feature_map import CUDAOneMap
71+
return CUDAOneMap(feature_dim, config, dense_projection,
72+
fusion_type, map_device)
73+
74+
return OneMap(feature_dim, config, dense_projection,
75+
fusion_type, map_device)
76+
77+
78+
# Navigator imported last since it depends on create_onemap from this module
79+
from .navigator import Navigator

mapping/cuda_feature_map.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
"""
2+
CUDA backend adapter for OneMap. Wraps cuonemap.OneMap to present the same interface as
3+
the Python OneMap class in feature_map.py, so the two are interchangeable.
4+
5+
See https://github.com/finnBsch/CuOneMap for the backend itself, including the known
6+
differences from the PyTorch implementation.
7+
"""
8+
import numpy as np
9+
import torch
10+
11+
import cuonemap as _onemap_cuda
12+
13+
14+
class CUDAOneMap:
15+
"""Drop-in replacement for OneMap using the CUDA backend.
16+
17+
Accepts the same constructor arguments and exposes the same properties/methods
18+
as the Python OneMap class, translating between the Python and CUDA interfaces
19+
internally.
20+
"""
21+
22+
def __init__(self,
23+
feature_dim,
24+
config,
25+
dense_projection=None,
26+
fusion_type=None,
27+
map_device="cuda"):
28+
assert feature_dim == 768, (
29+
f"CUDA OneMap backend requires feature_dim=768 (CLIP), got {feature_dim}"
30+
)
31+
cuda_conf = _onemap_cuda.MappingConf()
32+
cuda_conf.n_cells = config.n_points
33+
cuda_conf.size = float(config.size)
34+
cuda_conf.agent_radius = config.agent_radius
35+
cuda_conf.blur_kernel_size = config.blur_kernel_size
36+
cuda_conf.obstacle_map_threshold = config.obstacle_map_threshold
37+
cuda_conf.fully_explored_threshold = config.fully_explored_threshold
38+
cuda_conf.checked_map_threshold = config.checked_map_threshold
39+
cuda_conf.depth_factor = config.depth_factor
40+
cuda_conf.gradient_factor = config.gradient_factor
41+
cuda_conf.optimal_object_distance = config.optimal_object_distance
42+
cuda_conf.optimal_object_factor = config.optimal_object_factor
43+
cuda_conf.obstacle_min = config.obstacle_min
44+
cuda_conf.obstacle_max = config.obstacle_max
45+
cuda_conf.filter_stairs = config.filter_stairs
46+
cuda_conf.floor_level = config.floor_level
47+
cuda_conf.floor_threshold = config.floor_threshold
48+
cuda_conf.use_cpu_storage = (map_device == "cpu")
49+
50+
self._impl = _onemap_cuda.OneMap(cuda_conf)
51+
self.map_device = map_device
52+
self.feature_dim = feature_dim
53+
self.n_cells = config.n_points
54+
self.size = float(config.size)
55+
self.cell_size = self.size / self.n_cells
56+
center = self._impl.map_center_cells
57+
self._map_center_cells = torch.tensor(
58+
[center[0], center[1]], dtype=torch.int32
59+
).to("cuda")
60+
61+
@property
62+
def map_center_cells(self):
63+
return self._map_center_cells
64+
65+
@property
66+
def camera_initialized(self):
67+
return self._impl.camera_initialized
68+
69+
@property
70+
def fx(self):
71+
return self._impl.fx
72+
73+
@property
74+
def fy(self):
75+
return self._impl.fy
76+
77+
@property
78+
def cx(self):
79+
return self._impl.cx
80+
81+
@property
82+
def cy(self):
83+
return self._impl.cy
84+
85+
# --- Map properties ---
86+
87+
@property
88+
def feature_map(self):
89+
return self._impl.feature_map
90+
91+
@property
92+
def confidence_map(self):
93+
return self._impl.confidence_map
94+
95+
@property
96+
def checked_conf_map(self):
97+
return self._impl.checked_conf_map
98+
99+
@property
100+
def obstacle_map(self):
101+
return self._impl.obstacle_map
102+
103+
@property
104+
def updated_mask(self):
105+
return self._impl.updated_mask
106+
107+
@property
108+
def navigable_map(self):
109+
t = self._impl.navigable_map
110+
if isinstance(t, np.ndarray):
111+
return t
112+
return t.cpu().numpy() if hasattr(t, 'cpu') else np.asarray(t)
113+
114+
@property
115+
def fully_explored_map(self):
116+
t = self._impl.fully_explored_map
117+
if isinstance(t, np.ndarray):
118+
return t
119+
return t.cpu().numpy() if hasattr(t, 'cpu') else np.asarray(t)
120+
121+
@property
122+
def checked_map(self):
123+
t = self._impl.checked_map
124+
if isinstance(t, np.ndarray):
125+
return t
126+
return t.cpu().numpy() if hasattr(t, 'cpu') else np.asarray(t)
127+
128+
@property
129+
def occluded_map(self):
130+
t = self._impl.occluded_map
131+
if isinstance(t, np.ndarray):
132+
return t
133+
return t.cpu().numpy() if hasattr(t, 'cpu') else np.asarray(t)
134+
135+
# --- Methods ---
136+
137+
def set_camera_matrix(self, camera_matrix):
138+
self._impl.set_camera_matrix(camera_matrix)
139+
140+
def update(self, values, depth, tf_camera_to_episodic, artifical_obstacles=None):
141+
# values: the Python implementation is given (F, H, W); the backend wants (H, W, F)
142+
if len(values.shape) == 3 and values.shape[0] == self.feature_dim:
143+
values = values.permute(1, 2, 0).contiguous()
144+
if not values.is_cuda:
145+
values = values.to("cuda")
146+
147+
# depth: numpy or CPU tensor -> float32 CUDA tensor
148+
if isinstance(depth, np.ndarray):
149+
depth = torch.from_numpy(depth.astype(np.float32)).to("cuda")
150+
elif isinstance(depth, torch.Tensor):
151+
if not depth.is_cuda:
152+
depth = depth.to(torch.float32).to("cuda")
153+
else:
154+
depth = torch.as_tensor(depth, dtype=torch.float32).to("cuda")
155+
156+
# tf stays on the host: the binding takes a CPU array
157+
if isinstance(tf_camera_to_episodic, torch.Tensor):
158+
tf_camera_to_episodic = tf_camera_to_episodic.cpu().numpy()
159+
tf_camera_to_episodic = tf_camera_to_episodic.astype(np.float32)
160+
161+
obstacles = []
162+
if artifical_obstacles:
163+
obstacles = [(float(o[0]), float(o[1])) for o in artifical_obstacles]
164+
165+
self._impl.update(values, depth, tf_camera_to_episodic, obstacles)
166+
167+
def reset(self):
168+
self._impl.reset()
169+
170+
def reset_updated_mask(self):
171+
self._impl.reset_updated_mask()
172+
173+
def reset_checked_map(self):
174+
self._impl.reset_checked_map()
175+
176+
def metric_to_px(self, x, y):
177+
return self._impl.metric_to_px(x, y)
178+
179+
def px_to_metric(self, px, py):
180+
return self._impl.px_to_metric(px, py)

mapping/navigator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"""
55
import time
66

7-
from mapping import (OneMap, detect_frontiers, get_frontier_midpoint,
7+
from mapping import (create_onemap, detect_frontiers, get_frontier_midpoint,
88
cluster_high_similarity_regions, find_local_maxima,
99
watershed_clustering, gradient_based_clustering, cluster_thermal_image,
1010
Cluster, NavGoal, Frontier)
@@ -143,7 +143,7 @@ def __init__(self,
143143
self.sam.eval()
144144
self.sam_predictor = SamPredictor(self.sam)
145145

146-
self.one_map = OneMap(self.model.feature_dim, config.mapping, map_device="cpu")
146+
self.one_map = create_onemap(self.model.feature_dim, config.mapping, map_device="cpu")
147147

148148
self.query_text = ["Other."]
149149
self.query_text_features = self.model.get_text_features(self.query_text).to(self.one_map.map_device)

0 commit comments

Comments
 (0)