Skip to content

Commit 2ee2978

Browse files
tordnataaron-skydio
authored andcommitted
[SymForce-External] [Caspar] GPU device selection for solver
[Caspar] GPU device selection for solver Adds device_id throughout the Caspar stack so callers can pick all GPU operations to a specific device. Default is set to device 0. Changes in Solver (solver.h/cc.jinja, solver_pybinding.h.jinja, lib.pyi.jinja): - device_id added to the constructor and stored as int device_id_. Every method calls cudaSetDevice(device_id_) at entry. - Exposed as a keyword argument in the pybind11 binding and .pyi stub Solver(*, device_id=1). - Added GetDeviceId(const py::object&), which reads the tensor pointer via cuda array interface and calls cudaPointerGetAttributes to find the owning device. This adds some overhead, but it was the least intrusive method I found. - stacked_to_caspar and caspar_to_stacked call cudaSetDevice(GetDeviceId(...)) before each kernel launch and infer device Was advised to ping @matias-christensen-skydio for best CUDA practises. Would gladly appreciate any feedback, especially on the device inference via cuda array interface for the pybindings. Blocked by #458, fixes #460. Closes #461 GitOrigin-RevId: 0c94352209d9ecefb59d0cca5dd35e846f64b9f9
1 parent f7c6ad1 commit 2ee2978

7 files changed

Lines changed: 62 additions & 7 deletions

File tree

symforce/caspar/source/runtime/pybind_array_tools.cc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,23 @@ void AssertUint2Vec(const py::object& obj) {
169169
Assert2DNxk(obj, 2);
170170
}
171171

172+
int GetDeviceId(const py::object& obj) {
173+
try {
174+
auto interface = obj.attr("__cuda_array_interface__").cast<py::dict>();
175+
auto data = interface["data"].cast<py::tuple>();
176+
void* ptr = reinterpret_cast<void*>(data[0].cast<size_t>());
177+
cudaPointerAttributes attrs;
178+
cudaError_t err = cudaPointerGetAttributes(&attrs, ptr);
179+
if (err != cudaSuccess) {
180+
cudaGetLastError();
181+
return -1;
182+
}
183+
return attrs.device;
184+
} catch (...) {
185+
return -1; // Fallback if interface or attributes aren't available
186+
}
187+
}
188+
172189
float* AsFloatPtr(const py::object& obj) {
173190
AssertFloatVec(obj);
174191
py::tuple data = GetInterface(obj)["data"].cast<py::tuple>();

symforce/caspar/source/runtime/pybind_array_tools.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ void AssertDeviceMemory(const py::object& obj);
3030
void AssertNumRowsEquals(const py::object& obj, size_t n);
3131
void AssertNumColsEquals(const py::object& obj, size_t n);
3232

33+
int GetDeviceId(const py::object& obj);
34+
3335
float* AsFloatPtr(const py::object& obj);
3436
double* AsDoublePtr(const py::object& obj);
3537
int* AsIntPtr(const py::object& obj);

symforce/caspar/source/templates/caspar_mappings_pybinding.h.jinja

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ void add_casmappings_pybindings(pybind11::module_ module) {
2727
throw std::runtime_error(
2828
"The caspar data must have at least as many columns as stacked_data has rows.");
2929
}
30+
cudaSetDevice(GetDeviceId(stacked_data));
3031
{{nodetype.__name__}}StackedToCaspar(
3132
As{{caslib.storage_t.capitalize()}}Ptr(stacked_data), As{{caslib.storage_t.capitalize()}}Ptr(cas_data), cas_stride, 0, num_objects);
3233
});
@@ -45,7 +46,7 @@ void add_casmappings_pybindings(pybind11::module_ module) {
4546
throw std::runtime_error(
4647
"The caspar data must have at least as many columns as stacked_data has rows.");
4748
}
48-
49+
cudaSetDevice(GetDeviceId(cas_data));
4950
{{nodetype.__name__}}CasparToStacked(
5051
As{{caslib.storage_t.capitalize()}}Ptr(cas_data), As{{caslib.storage_t.capitalize()}}Ptr(stacked_data), cas_stride, 0, num_objects);
5152
});

symforce/caspar/source/templates/lib.pyi.jinja

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ class {{solver.struct_name}}:
6868
{% for thing in solver.size_contributors %}
6969
{{num_arg_key(thing)}}: int = 0,
7070
{% endfor %}
71+
device_id: int = 0,
7172
): ...
7273

7374
def set_params(self, params: SolverParams) -> None:

symforce/caspar/source/templates/solver.cc.jinja

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,12 @@ namespace caspar {
6868
{{ solver.struct_name }}::{{ solver.struct_name }}(
6969
const SolverParams<double> &params,
7070
{% for thing in solver.size_contributors %}
71-
size_t {{num_arg_key(thing)}}{{ ", " if not loop.last else "" }}
71+
size_t {{num_arg_key(thing)}}{{ ", " }}
7272
{% endfor %}
73+
int device_id
7374
)
7475
: params_(params),
76+
device_id_(device_id),
7577
{% for thing in solver.size_contributors %}
7678
{{num_key(thing)}}({{num_arg_key(thing)}}),
7779
{{num_max_key(thing)}}({{num_arg_key(thing)}}){{ ", " if not loop.last else "" }}
@@ -85,6 +87,20 @@ namespace caspar {
8587
throw std::runtime_error("params.diag_init must be positive");
8688
}
8789
allocation_size_ = get_nbytes();
90+
91+
if (device_id_ < 0) {
92+
throw std::runtime_error("Invalid CUDA device id: " + std::to_string(device_id_));
93+
}
94+
if (device_id_ != 0) {
95+
int deviceCount;
96+
cudaGetDeviceCount(&deviceCount);
97+
if (deviceCount <= device_id_) {
98+
throw std::runtime_error("CUDA detected " + std::to_string(deviceCount) +
99+
" devices, but device " + std::to_string(device_id_) +
100+
" was requested (0-indexed)");
101+
}
102+
}
103+
cudaSetDevice(device_id_);
88104
cudaMalloc(&origin_ptr_, allocation_size_);
89105

90106
size_t offset = 0;
@@ -97,6 +113,7 @@ namespace caspar {
97113
}
98114

99115
{{ solver.struct_name }}::~{{ solver.struct_name }}(){
116+
cudaSetDevice(device_id_);
100117
cudaFree(origin_ptr_);
101118
}
102119

@@ -110,6 +127,7 @@ size_t {{ solver.struct_name }}::get_allocation_size(){
110127

111128

112129
SolveResult {{ solver.struct_name }}::solve(bool print_progress, bool verbose_logging) {
130+
cudaSetDevice(device_id_);
113131
SolveResult result;
114132
result.exit_reason = ExitReason::MAX_ITERATIONS;
115133
{{solver.linear_t}} score_best;
@@ -634,6 +652,7 @@ void {{ solver.struct_name }}::finish_indices() {
634652

635653
{% for nodetype in solver.node_types %}
636654
void {{ solver.struct_name }}::Set{{nodetype.__name__}}Num(const size_t num) {
655+
cudaSetDevice(device_id_);
637656
if (num > {{num_max_key(nodetype)}}) {
638657
throw std::runtime_error(std::to_string(num) + " > {{num_max_key(nodetype)}}");
639658
}
@@ -642,6 +661,7 @@ void {{ solver.struct_name }}::Set{{nodetype.__name__}}Num(const size_t num) {
642661

643662
void {{ solver.struct_name }}::Set{{nodetype.__name__}}NodesFromStackedHost(
644663
const {{solver.storage_t}}* const data, const size_t offset, const size_t num) {
664+
cudaSetDevice(device_id_);
645665
if (offset + num > {{num_key(nodetype)}}){
646666
throw std::runtime_error(std::to_string(offset + num) + " > {{num_key(nodetype)}}");
647667
}
@@ -654,6 +674,7 @@ void {{ solver.struct_name }}::Set{{nodetype.__name__}}NodesFromStackedHost(
654674

655675
void {{ solver.struct_name }}::Set{{nodetype.__name__}}NodesFromStackedDevice(
656676
const {{solver.storage_t}}* const data, const size_t offset, const size_t num) {
677+
cudaSetDevice(device_id_);
657678
if (offset + num > {{num_key(nodetype)}}){
658679
throw std::runtime_error(std::to_string(offset + num) + " > {{num_key(nodetype)}}");
659680
}
@@ -663,6 +684,7 @@ void {{ solver.struct_name }}::Set{{nodetype.__name__}}NodesFromStackedDevice(
663684

664685
void {{solver.struct_name}}::Get{{nodetype.__name__}}NodesToStackedHost(
665686
{{solver.storage_t}}* const data, const size_t offset, const size_t num) {
687+
cudaSetDevice(device_id_);
666688
if (offset + num > {{num_key(nodetype)}}){
667689
throw std::runtime_error(std::to_string(offset + num) + " > {{num_key(nodetype)}}");
668690
}
@@ -675,6 +697,7 @@ void {{solver.struct_name}}::Get{{nodetype.__name__}}NodesToStackedHost(
675697

676698
void {{solver.struct_name}}::Get{{nodetype.__name__}}NodesToStackedDevice(
677699
{{solver.storage_t}}* const data, const size_t offset, const size_t num) {
700+
cudaSetDevice(device_id_);
678701
if (offset + num > {{num_key(nodetype)}}){
679702
throw std::runtime_error(std::to_string(offset + num) + " > {{num_key(nodetype)}}");
680703
}
@@ -695,6 +718,7 @@ void {{solver.struct_name}}::Get{{nodetype.__name__}}NodesToStackedDevice(
695718
{% if fac.isnodeshared[arg] %}
696719
void {{ solver.struct_name }}::Set{{parts_to_pascal(fac.name)}}{{parts_to_pascal(arg)}}IndicesFromHost(
697720
const unsigned int* const indices, size_t num) {
721+
cudaSetDevice(device_id_);
698722
if (num != {{num_key(fac)}}){
699723
throw std::runtime_error(
700724
std::to_string(num)
@@ -708,7 +732,8 @@ void {{solver.struct_name}}::Get{{nodetype.__name__}}NodesToStackedDevice(
708732
void {{ solver.struct_name }}::Set{{parts_to_pascal(fac.name)}}{{parts_to_pascal(arg)}}IndicesFromDevice(
709733
const unsigned int* const indices, size_t num) {
710734
indices_valid_ = false;
711-
735+
cudaSetDevice(device_id_);
736+
712737
if (num != {{num_key(fac)}}){
713738
throw std::runtime_error(
714739
std::to_string(num)
@@ -737,6 +762,7 @@ void {{solver.struct_name}}::Get{{nodetype.__name__}}NodesToStackedDevice(
737762
const {{solver.storage_t}}* const data, size_t offset, size_t num
738763
{% endif %}
739764
) {
765+
cudaSetDevice(device_id_);
740766
{% if fac.isconstuniq[arg] %}
741767
const size_t offset = 0;
742768
const size_t num = 1;
@@ -769,6 +795,7 @@ void {{solver.struct_name}}::Get{{nodetype.__name__}}NodesToStackedDevice(
769795
{% elif fac.isconstindexed[arg] %}
770796
const {{solver.storage_t}}* const data, size_t offset, size_t num
771797
{% endif %} ) {
798+
cudaSetDevice(device_id_);
772799
{% if fac.isconstuniq[arg] %}
773800
const size_t offset = 0;
774801
const size_t num = 1;
@@ -791,6 +818,7 @@ void {{solver.struct_name}}::Get{{nodetype.__name__}}NodesToStackedDevice(
791818
{% if fac.isconstshared[arg] %}
792819
void {{ solver.struct_name }}::Set{{parts_to_pascal(fac.name)}}{{parts_to_pascal(arg)}}IndicesFromHost(
793820
const unsigned int* const indices, size_t num) {
821+
cudaSetDevice(device_id_);
794822
if (num != {{num_key(fac)}}){
795823
throw std::runtime_error(
796824
std::to_string(num)
@@ -804,7 +832,7 @@ void {{solver.struct_name}}::Get{{nodetype.__name__}}NodesToStackedDevice(
804832
void {{ solver.struct_name }}::Set{{parts_to_pascal(fac.name)}}{{parts_to_pascal(arg)}}IndicesFromDevice(
805833
const unsigned int* const indices, size_t num) {
806834
indices_valid_ = false;
807-
835+
cudaSetDevice(device_id_);
808836
if (num != {{num_key(fac)}}){
809837
throw std::runtime_error(
810838
std::to_string(num)
@@ -824,6 +852,7 @@ void {{solver.struct_name}}::Get{{nodetype.__name__}}NodesToStackedDevice(
824852
void {{ solver.struct_name }}::Set{{parts_to_pascal(fac.name)}}{{parts_to_pascal(arg)}}IndicesFromHost(
825853
const unsigned int* const indices, size_t num) {
826854
indices_valid_ = false;
855+
cudaSetDevice(device_id_);
827856
if (num != {{num_key(fac)}}){
828857
throw std::runtime_error(
829858
std::to_string(num)
@@ -836,6 +865,7 @@ void {{solver.struct_name}}::Get{{nodetype.__name__}}NodesToStackedDevice(
836865
void {{ solver.struct_name }}::Set{{parts_to_pascal(fac.name)}}{{parts_to_pascal(arg)}}IndicesFromDevice(
837866
const unsigned int* const indices, size_t num) {
838867
indices_valid_ = false;
868+
cudaSetDevice(device_id_);
839869
if (num != {{num_key(fac)}}){
840870
throw std::runtime_error(
841871
std::to_string(num)

symforce/caspar/source/templates/solver.h.jinja

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,9 @@ class {{ solver.struct_name }} {
5555
{{ solver.struct_name }}(
5656
const SolverParams<double> &params,
5757
{% for thing in solver.size_contributors %}
58-
size_t {{num_arg_key(thing)}}{{ ", " if not loop.last else "" }}
58+
size_t {{num_arg_key(thing)}}{{ ", " }}
5959
{% endfor %}
60+
int device_id = 0
6061
);
6162

6263
// This class is managing cuda memory and cannot be copied.
@@ -210,6 +211,7 @@ class {{ solver.struct_name }} {
210211

211212
private:
212213
SolverParams<{{solver.linear_t}}> params_;
214+
int device_id_;
213215
uint8_t* origin_ptr_;
214216
size_t scratch_inout_size_;
215217
size_t allocation_size_;

symforce/caspar/source/templates/solver_pybinding.h.jinja

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,14 @@ inline void add_solver_pybinding(pybind11::module_ module) {
4141
.def(py::init<SolverParams<double>,
4242
{% for thing in solver.size_contributors %}
4343
size_t{{ ", " if not loop.last else "" }}
44-
{% endfor %}>(),
44+
{% endfor %},
45+
int>(),
4546
py::arg("params"),
4647
py::kw_only(),
4748
{% for thing in solver.size_contributors %}
48-
py::arg("{{num_arg_key(thing)}}") = 0{{ ", " if not loop.last else "" }}
49+
py::arg("{{num_arg_key(thing)}}") = 0{{ ", " }}
4950
{% endfor %}
51+
py::arg("device_id") = 0
5052
)
5153

5254
.def("set_params", &{{solver.struct_name}}::set_params)

0 commit comments

Comments
 (0)