Skip to content

Commit ce4c37b

Browse files
emil-martens-skydioaaron-skydio
authored andcommitted
[Caspar] Fixed bugs and added example.
Fixed bugs related to nodes of size 1 and running with multiple factor types. Added a multi-factor example. GitOrigin-RevId: 300646d24ac07cdc156ca752330283df342c93e4
1 parent 99105e1 commit ce4c37b

File tree

19 files changed

+288
-62
lines changed

19 files changed

+288
-62
lines changed

symforce/experimental/caspar/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,10 @@ __global__ void __launch_bounds__(1024, 1)
8484
```
8585
8686
### Factors
87-
TODO
87+
88+
The `examples/multiple_factors` directory demonstrates how to create a graph with various node and factor types.
89+
90+
The `examples/bal` directory provides an example of how Caspar can be utilized to optimize Bundle Adjustment in the Large (BAL) problems.
8891
8992
### Memory Accessors
9093
Correct memory management is key to generating efficient CUDA kernels.

symforce/experimental/caspar/code_formulation/compute_graph_optimizer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,14 @@ def __init__(
5959
):
6060
expr_map: dict[sf.Basic, Var] = {}
6161

62-
for arg in inputs:
62+
for arg_id, arg in enumerate(inputs):
6363
storage = Ops.to_storage(arg.storage)
6464

6565
for i, indices in enumerate(arg.chunk_indices):
6666
func = ftypes.READ_FUNCS[len(indices) - 1](
6767
data=(arg.name, i),
6868
custom_code=arg.read_template(i),
69+
unique_id=arg_id,
6970
)
7071
for el, var in zip(indices, func.outs):
7172
expr_map[storage[el]] = var

symforce/experimental/caspar/code_formulation/compute_graph_sorter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def __init__(self, funcs: list[Func]):
4343
for func in funcs:
4444
for out in func.outs:
4545
if not out.vod.missing_contribs:
46-
logging.warning("Missing contribs: " + str(out))
46+
logging.debug("Missing contribs: " + str(out))
4747

4848
if func.is_accumulator():
4949
assert len(func.args) > 1

symforce/experimental/caspar/code_formulation/function_types.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -422,32 +422,37 @@ def assign_code(self, outs: list[str], args: list[str]) -> str:
422422
# TRIGONOMETRIC FUNCTIONS
423423
class Cos(Func):
424424
def assign_code(self, outs: list[str], args: list[str]) -> str:
425-
return f"{outs[0]} = cos({args[0]});"
425+
return f"{outs[0]} = cosf({args[0]});"
426426

427427

428428
class Sin(Func):
429429
def assign_code(self, outs: list[str], args: list[str]) -> str:
430-
return f"{outs[0]} = sin({args[0]});"
430+
return f"{outs[0]} = sinf({args[0]});"
431431

432432

433433
class Tan(Func):
434434
def assign_code(self, outs: list[str], args: list[str]) -> str:
435-
return f"{outs[0]} = tan({args[0]});"
435+
return f"{outs[0]} = tanf({args[0]});"
436436

437437

438438
class ACos(Func):
439439
def assign_code(self, outs: list[str], args: list[str]) -> str:
440-
return f"{outs[0]} = acos({args[0]});"
440+
return f"{outs[0]} = acosf({args[0]});"
441441

442442

443443
class ASin(Func):
444444
def assign_code(self, outs: list[str], args: list[str]) -> str:
445-
return f"{outs[0]} = asin({args[0]});"
445+
return f"{outs[0]} = asinf({args[0]});"
446446

447447

448448
class ATan(Func):
449449
def assign_code(self, outs: list[str], args: list[str]) -> str:
450-
return f"{outs[0]} = atan({args[0]});"
450+
return f"{outs[0]} = atanf({args[0]});"
451+
452+
453+
class ATan2(Func):
454+
def assign_code(self, outs: list[str], args: list[str]) -> str:
455+
return f"{outs[0]} = atan2f({args[0]}, {args[1]});"
451456

452457

453458
class SinCos(Func):
@@ -561,6 +566,7 @@ def assign_code(self, outs: list[str], args: list[str]) -> str:
561566
symengine_wrapper.acos: ACos,
562567
symengine_wrapper.asin: ASin,
563568
symengine_wrapper.atan: ATan,
569+
symengine_wrapper.atan2: ATan2,
564570
symengine_wrapper.sign: Sign,
565571
sf.Min: Min,
566572
sf.Max: Max,

symforce/experimental/caspar/code_generation/factor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def from_diagonal_and_lower_triangle(diag_data: sf.Matrix, ltril_data: sf.Matrix
4444
Reconstructs a matrix from its diagonal and lower triangular part.
4545
"""
4646
n = len(diag_data)
47-
vec_loc = [v for v in ltril_data]
47+
vec_loc = ltril_data.to_storage()
4848
mat = sf.Matrix(n, n)
4949
for i in range(n):
5050
for j in range(i + 1, mat.shape[0]):

symforce/experimental/caspar/code_generation/solver.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -172,17 +172,15 @@ def add_solver_data(self, fields: dict[str, MemDesc]) -> None:
172172
fields[fac_key(fac, "res_tot")] = MemDesc(1, num_blocks_key(fac), alignment=alignment)
173173
fields["marker__res_tot_end_"] = MemDesc(0, 0, alignment=4)
174174

175-
for thing in [
176-
"z",
177-
"p_k_a",
178-
"p_k_b",
179-
"step_k_a",
180-
"step_k_b",
181-
"w",
182-
]:
175+
for thing in ["z", "p_k_a", "p_k_b", "step_k_a", "step_k_b"]:
183176
for typ in caslib.node_types:
184177
fields[node_key(typ, thing)] = MemDesc(Ops.tangent_dim(typ), num_key(typ))
185178

179+
fields["marker__w_start_"] = MemDesc(0, 0)
180+
for typ in caslib.node_types:
181+
fields[node_key(typ, "w")] = MemDesc(Ops.tangent_dim(typ), num_key(typ))
182+
fields["marker__w_end_"] = MemDesc(0, 0, alignment=4)
183+
186184
# USED IN THE njtr_precond
187185
fields["marker__r_k_a_start_"] = MemDesc(0, 0)
188186
for typ in caslib.node_types:

symforce/experimental/caspar/examples/bal/gen_and_run.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def fac_reprojection(
9494

9595
solver.set_fac_reprojection_pixel_data_from_stacked_host(pixels)
9696

97-
solver.solve()
97+
solver.solve(print_progress=True)
9898

9999
pointdata_out = np.empty_like(pointdata)
100100
solver.get_Point_nodes_to_stacked_host(pointdata_out)

symforce/experimental/caspar/examples/bal_deployed/run.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,4 @@
4040
solver.set_fac_reprojection_point_indices_from_host(point_ids)
4141
solver.finish_indices()
4242
print("starting")
43-
solver.solve()
43+
solver.solve(print_progress=True)
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
# ----------------------------------------------------------------------------
2+
# SymForce - Copyright 2025, Skydio, Inc.
3+
# This source code is under the Apache 2.0 license found in the LICENSE file.
4+
# ----------------------------------------------------------------------------
5+
6+
from pathlib import Path
7+
8+
import numpy as np
9+
10+
import symforce
11+
12+
symforce.set_epsilon_to_number(float(10 * np.finfo(np.float32).eps))
13+
import torch
14+
15+
import symforce.symbolic as sf
16+
from symforce import typing as T
17+
from symforce.experimental.caspar import CasparLibrary
18+
from symforce.experimental.caspar import memory as mem
19+
20+
"""
21+
We need unique classes to distinguish between values and expectations
22+
"""
23+
24+
25+
class Landmark(sf.V2): ...
26+
27+
28+
class Odometry(sf.V3): ...
29+
30+
31+
class Lidar(sf.V2): ...
32+
33+
34+
class Gnss(sf.V2): ...
35+
36+
37+
caslib = CasparLibrary()
38+
39+
40+
@caslib.add_kernel
41+
def make_poses(
42+
angle: T.Annotated[sf.Symbol, mem.ReadSequential],
43+
) -> T.Annotated[sf.Pose2, mem.WriteSequential]:
44+
x = sf.cos(angle) * 10
45+
y = sf.sin(angle) * 10
46+
47+
return sf.Pose2.from_tangent([angle, x, y])
48+
49+
50+
@caslib.add_kernel
51+
def get_odometry(
52+
pose_k: T.Annotated[sf.Pose2, mem.ReadIndexed],
53+
pose_kp1: T.Annotated[sf.Pose2, mem.ReadIndexed],
54+
) -> T.Annotated[Odometry, mem.WriteSequential]:
55+
return Odometry(pose_k.local_coordinates(pose_kp1))
56+
57+
58+
@caslib.add_kernel
59+
def get_lidar(
60+
pose: T.Annotated[sf.Pose2, mem.ReadShared],
61+
landmark: T.Annotated[Landmark, mem.ReadShared],
62+
) -> T.Annotated[Lidar, mem.WriteSequential]:
63+
landmark_body = pose.inverse() * landmark
64+
return Lidar(landmark_body)
65+
66+
67+
@caslib.add_kernel
68+
def get_gnss(
69+
pose: T.Annotated[sf.Pose2, mem.ReadIndexed],
70+
) -> T.Annotated[Gnss, mem.WriteSequential]:
71+
return Gnss(pose.t)
72+
73+
74+
@caslib.add_factor
75+
def fac_lidar(
76+
pose: T.Annotated[sf.Pose2, mem.Tunable],
77+
landmark: T.Annotated[Landmark, mem.Tunable],
78+
observed_lidar: T.Annotated[Lidar, mem.Constant],
79+
) -> sf.V2:
80+
return pose.inverse() * landmark - observed_lidar
81+
82+
83+
@caslib.add_factor
84+
def fac_position(
85+
pose: T.Annotated[sf.Pose2, mem.Tunable],
86+
observed_gnss: T.Annotated[Lidar, mem.Constant],
87+
) -> sf.V2:
88+
return pose.t - observed_gnss
89+
90+
91+
@caslib.add_factor
92+
def fac_odometry(
93+
pose_k: T.Annotated[sf.Pose2, mem.Tunable],
94+
pose_kp1: T.Annotated[sf.Pose2, mem.Tunable],
95+
observed_odom: T.Annotated[Odometry, mem.Constant],
96+
) -> sf.V3:
97+
return sf.V3(pose_k.local_coordinates(pose_kp1)) - observed_odom
98+
99+
100+
out_dir = Path(__file__).resolve().parent / "generated"
101+
caslib.generate(out_dir)
102+
caslib.compile(out_dir)
103+
104+
105+
# Can also be imported using: lib = caslib.import_lib(out_dir)
106+
from generated import caspar_lib as lib # type: ignore[import-not-found]
107+
108+
torch.set_default_device("cuda")
109+
N_POSE = 1024
110+
N_LANDMARK = 300
111+
INTERVAL_GNSS = 1
112+
MATCH_PER_LIDAR = 8
113+
114+
arange_pose = torch.arange(N_POSE, dtype=torch.int32)
115+
116+
angles = torch.linspace(0, 2 * np.pi, N_POSE, device="cuda")[None, :]
117+
pose_caspar = torch.empty(mem.caspar_size(sf.Pose2), N_POSE)
118+
lib.make_poses(angles, pose_caspar, N_POSE)
119+
120+
landmarks_caspar = (torch.rand(mem.caspar_size(Landmark), N_LANDMARK) - 0.5) * 10
121+
122+
odometry_caspar = torch.empty(mem.caspar_size(Odometry), N_POSE - 1)
123+
odom_k_indices = arange_pose[:-1]
124+
odom_kp1_indices = arange_pose[1:]
125+
lib.get_odometry(
126+
pose_caspar, odom_k_indices, pose_caspar, odom_kp1_indices, odometry_caspar, N_POSE - 1
127+
)
128+
129+
gnss_caspar = torch.empty(mem.caspar_size(Gnss), N_POSE // INTERVAL_GNSS)
130+
gnss_indices = torch.arange(0, N_POSE, INTERVAL_GNSS, dtype=torch.int32)
131+
lib.get_gnss(pose_caspar, gnss_indices, gnss_caspar, N_POSE // INTERVAL_GNSS)
132+
133+
134+
lidar_caspar = torch.empty(mem.caspar_size(Lidar), N_POSE * MATCH_PER_LIDAR)
135+
lidar_pose_indices = arange_pose.repeat_interleave(MATCH_PER_LIDAR)
136+
lidar_pose_indices_shared = torch.empty(N_POSE * MATCH_PER_LIDAR, 2, dtype=torch.int32)
137+
lib.shared_indices(lidar_pose_indices, lidar_pose_indices_shared)
138+
139+
_indices_tmp = torch.arange(MATCH_PER_LIDAR)[None:] + torch.arange(N_POSE)[:, None]
140+
lidar_landmark_indices = _indices_tmp.to(torch.int32).ravel() % N_LANDMARK
141+
lidar_landmark_indices_shared = torch.empty(N_POSE * MATCH_PER_LIDAR, 2, dtype=torch.int32)
142+
lib.shared_indices(lidar_landmark_indices, lidar_landmark_indices_shared)
143+
lib.get_lidar(
144+
pose_caspar,
145+
lidar_pose_indices_shared,
146+
landmarks_caspar,
147+
lidar_landmark_indices_shared,
148+
lidar_caspar,
149+
N_POSE * MATCH_PER_LIDAR,
150+
)
151+
152+
pose_stacked = torch.empty(N_POSE, mem.stacked_size(sf.Pose2))
153+
landmarks_stacked = torch.empty(N_LANDMARK, mem.stacked_size(Landmark))
154+
odometry_stacked = torch.empty(N_POSE - 1, mem.stacked_size(Odometry))
155+
gnss_stacked = torch.empty(N_POSE // INTERVAL_GNSS, mem.stacked_size(Gnss))
156+
lidar_stacked = torch.empty(N_POSE * MATCH_PER_LIDAR, mem.stacked_size(Lidar))
157+
lib.Pose2_caspar_to_stacked(pose_caspar, pose_stacked)
158+
lib.Landmark_caspar_to_stacked(landmarks_caspar, landmarks_stacked)
159+
lib.Odometry_caspar_to_stacked(odometry_caspar, odometry_stacked)
160+
lib.Gnss_caspar_to_stacked(gnss_caspar, gnss_stacked)
161+
lib.Lidar_caspar_to_stacked(lidar_caspar, lidar_stacked)
162+
163+
164+
params = lib.SolverParams()
165+
params.diag_init = 1.0
166+
params.solver_iter_max = 200
167+
params.pcg_iter_max = 50
168+
params.pcg_rel_error_exit = 1e-6
169+
170+
solver = lib.GraphSolver(
171+
params,
172+
Pose2_num_max=N_POSE,
173+
Landmark_num_max=N_LANDMARK,
174+
fac_lidar_num_max=N_POSE * MATCH_PER_LIDAR,
175+
fac_position_num_max=N_POSE // INTERVAL_GNSS,
176+
fac_odometry_num_max=N_POSE - 1,
177+
)
178+
179+
pose_stacked_noisy = pose_stacked.clone()
180+
pose_stacked_noisy[:, 2:4] += torch.randn_like(pose_stacked_noisy[:, 2:4])
181+
landmarks_stacked_noisy = landmarks_stacked + torch.randn_like(landmarks_stacked)
182+
solver.set_Pose2_nodes_from_stacked_device(pose_stacked_noisy)
183+
solver.set_Landmark_nodes_from_stacked_device(landmarks_stacked_noisy)
184+
185+
solver.set_fac_lidar_observed_lidar_data_from_stacked_device(lidar_stacked)
186+
solver.set_fac_lidar_pose_indices_from_device(lidar_pose_indices)
187+
solver.set_fac_lidar_landmark_indices_from_device(lidar_landmark_indices)
188+
189+
solver.set_fac_position_observed_gnss_data_from_stacked_device(gnss_stacked)
190+
solver.set_fac_position_pose_indices_from_device(gnss_indices)
191+
192+
solver.set_fac_odometry_observed_odom_data_from_stacked_device(odometry_stacked)
193+
solver.set_fac_odometry_pose_k_indices_from_device(odom_k_indices)
194+
solver.set_fac_odometry_pose_kp1_indices_from_device(odom_kp1_indices)
195+
196+
solver.finish_indices()
197+
solver.solve(print_progress=True)

symforce/experimental/caspar/memory/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from .accessors import WriteIndexed
1717
from .accessors import WriteSequential
1818
from .layouts import caspar_size
19+
from .layouts import stacked_size
1920

2021
"""
2122
This module contains classes for defining argument types for memory access patterns in CUDA kernels.

0 commit comments

Comments
 (0)