chrono_batch_render/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
66 / 194
34.0%
Branches:
40 / 226
17.7%
Functions:
7 / 16, 1 excl
43.8%

python/src/dlpack.cpp
Line Branch Exec Source
1 // SPDX-License-Identifier: BSD-3-Clause
2 #include <Python.h>
3 #include <fmt/format.h>
4
5 #include <algorithm>
6 #include <cstddef>
7 #include <cstdint>
8 #include <memory>
9 #include <optional>
10 #include <vector>
11
12 #include "bindings.h"
13 #include "chrono_batch_render/device/commands.h"
14 #include "chrono_batch_render/device/queue.h"
15
16 namespace nb = nanobind;
17 using namespace nb::literals;
18
19 namespace chrono::batch_render::python {
20
21 namespace {
22
23 constexpr const char* kVersioned = "dltensor_versioned";
24 constexpr const char* kUnversioned = "dltensor";
25
26 ✗ struct Export {
27 DLManagedTensorVersioned managed{};
28 device::Tensor tensor;
29 std::vector<std::byte> host;
30 };
31
32 1170 struct Producer {
33 DLManagedTensorVersioned wrapper{};
34 DLManagedTensorVersioned* versioned = nullptr;
35 DLManagedTensor* unversioned = nullptr;
36 };
37
38 1170 void ReleaseProducer(DLManagedTensorVersioned* self) {
39 1170 auto* producer = static_cast<Producer*>(self->manager_ctx);
40
1/2
✓ Branch 1 taken 1170 times.
✗ Branch 2 not taken.
1170 if (Py_IsInitialized()) {
41 1170 nb::gil_scoped_acquire gil;
42
2/4
✓ Branch 0 taken 1170 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1170 times.
✗ Branch 3 not taken.
1170 if (producer->versioned != nullptr && producer->versioned->deleter != nullptr) {
43
1/1
✓ Branch 1 taken 1170 times.
1170 producer->versioned->deleter(producer->versioned);
44 }
45
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 1170 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
1170 if (producer->unversioned != nullptr && producer->unversioned->deleter != nullptr) {
46
0/1
✗ Branch 1 not taken.
✗ producer->unversioned->deleter(producer->unversioned);
47 }
48 1170 }
49
1/2
✓ Branch 0 taken 1170 times.
✗ Branch 1 not taken.
1170 delete producer;
50 1170 }
51
52 ✗ DLManagedTensorVersioned* CreateExport(const device::Tensor& tensor, bool toHost) {
53
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (!toHost) {
54 ✗ return tensor.Export();
55 }
56 ✗ auto held = std::make_unique<Export>();
57
0/1
✗ Branch 1 not taken.
✗ held->tensor = tensor;
58
0/1
✗ Branch 1 not taken.
✗ held->host = tensor.ToHostBytes();
59 ✗ DLManagedTensorVersioned& managed = held->managed;
60 ✗ DLTensor& exported = managed.dl_tensor;
61 ✗ exported.data = held->host.data();
62 ✗ exported.device = {kDLCPU, 0};
63 ✗ exported.ndim = int32_t(tensor.Shape().size());
64 ✗ exported.dtype = DataType(tensor.Type());
65 ✗ exported.shape = const_cast<int64_t*>(held->tensor.Shape().data());
66 ✗ managed.flags = DLPACK_FLAG_BITMASK_IS_COPIED;
67 ✗ managed.version = {DLPACK_MAJOR_VERSION, DLPACK_MINOR_VERSION};
68 ✗ managed.manager_ctx = held.get();
69 ✗ managed.deleter = [](DLManagedTensorVersioned* self) {
70
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ delete static_cast<Export*>(self->manager_ctx);
71 ✗ };
72 ✗ return &held.release()->managed;
73 ✗ }
74
75 ✗ device::Tensor CopyOnDevice(const device::Tensor& tensor) {
76 ✗ const Span<const int64_t> shape = tensor.Shape();
77 ✗ const Span<const int64_t> strides = tensor.Strides();
78 ✗ int64_t last = 0;
79 ✗ int64_t packed = 1;
80
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ for (size_t axis = shape.size(); axis-- > 0;) {
81
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ last += (shape[axis] - 1) * (strides.empty() ? packed : strides[axis]);
82 ✗ packed *= shape[axis];
83 }
84 ✗ const uint64_t bytes =
85
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ tensor.Elements() == 0 ? 0 : uint64_t(last + 1) * device::ElementBytes(tensor.Type());
86
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ device::Device& device = tensor.GetBuffer().GetDevice();
87 ✗ const Ref<device::Buffer> copied = device.CreateBuffer({.bytes = std::max<uint64_t>(bytes, 4),
88 .usage = device::BufferUsage::Interop,
89
0/1
✗ Branch 1 not taken.
✗ .label = "dlpack copy"});
90 ✗ if (bytes > 0) {
91
0/1
✗ Branch 1 not taken.
✗ device::Queue& queue = device.GetQueue();
92
0/1
✗ Branch 1 not taken.
✗ device::CommandEncoder encoder = queue.CreateEncoder();
93
0/3
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ Branch 7 not taken.
✗ encoder.Copy(tensor.GetBuffer().Slice(tensor.Offset(), bytes), copied->Slice(0, bytes));
94
0/2
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ queue.Submit(encoder.Finish());
95 ✗ }
96
0/1
✗ Branch 1 not taken.
✗ return device::Tensor(copied, 0, tensor.Type(), {shape.begin(), shape.end()},
97
0/7
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ Branch 8 not taken.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ {strides.begin(), strides.end()});
98 ✗ }
99
100 ✗ void CheckStream(int type, int64_t stream) {
101
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (type == kDLCUDA && stream == 0) {
102
0/1
✗ Branch 2 not taken.
✗ throw nb::value_error("CUDA consumers name the default stream as 1 or 2, never 0");
103 }
104
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ if (type == kDLROCM && (stream == 1 || stream == 2)) {
105
0/1
✗ Branch 2 not taken.
✗ throw nb::value_error("ROCm consumers name the default stream as 0, never 1 or 2");
106 }
107 ✗ }
108
109 ✗ PyObject* VersionedCapsule(DLManagedTensorVersioned* managed) {
110 ✗ return PyCapsule_New(managed, kVersioned, [](PyObject* self) {
111
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ if (PyCapsule_IsValid(self, kVersioned)) {
112 ✗ auto* tensor =
113 ✗ static_cast<DLManagedTensorVersioned*>(PyCapsule_GetPointer(self, kVersioned));
114 ✗ tensor->deleter(tensor);
115 }
116 ✗ });
117 }
118
119 ✗ PyObject* UnversionedCapsule(DLManagedTensorVersioned* managed) {
120 ✗ auto* legacy = new DLManagedTensor();
121 ✗ legacy->dl_tensor = managed->dl_tensor;
122 ✗ legacy->manager_ctx = managed;
123 ✗ legacy->deleter = [](DLManagedTensor* self) {
124 ✗ auto* inner = static_cast<DLManagedTensorVersioned*>(self->manager_ctx);
125 ✗ inner->deleter(inner);
126
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ delete self;
127 ✗ };
128 ✗ return PyCapsule_New(legacy, kUnversioned, [](PyObject* self) {
129
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ if (PyCapsule_IsValid(self, kUnversioned)) {
130 ✗ auto* tensor = static_cast<DLManagedTensor*>(PyCapsule_GetPointer(self, kUnversioned));
131 ✗ tensor->deleter(tensor);
132 }
133 ✗ });
134 }
135
136 1170 std::optional<int64_t> RequestedStream(nb::handle source, std::optional<uintptr_t> stream) {
137
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1170 times.
1170 if (!nb::hasattr(source, "__dlpack_device__")) {
138 ✗ return std::nullopt;
139 }
140
1/1
✓ Branch 1 taken 1170 times.
2340 const nb::tuple device = nb::cast<nb::tuple>(source.attr("__dlpack_device__")());
141
2/4
✗ Branch 0 not taken.
✓ Branch 1 taken 1170 times.
✓ Branch 2 taken 1170 times.
✗ Branch 3 not taken.
2340 const int type = nb::cast<int>(device[0]);
142 1170 const bool cuda = type == kDLCUDA || type == kDLCUDAHost || type == kDLCUDAManaged;
143 1170 const bool rocm = type == kDLROCM || type == kDLROCMHost;
144
1/2
✓ Branch 0 taken 1170 times.
✗ Branch 1 not taken.
1170 if (!cuda && !rocm) {
145 1170 return std::nullopt;
146 }
147
0/6
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ if (stream && (*stream != 0 || !cuda)) {
148 ✗ return int64_t(*stream);
149 }
150
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ return cuda ? 1 : 0;
151 1170 }
152
153 1170 nb::object RequestCapsule(nb::handle source, std::optional<int64_t> stream) {
154
1/1
✓ Branch 1 taken 1170 times.
1170 const nb::object request = source.attr("__dlpack__");
155 1170 try {
156
3/12
✗ Branch 0 not taken.
✓ Branch 1 taken 1170 times.
✗ Branch 3 not taken.
✗ Branch 6 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1170 times.
✗ Branch 10 not taken.
✓ Branch 11 taken 1170 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
2340 return stream ? request("stream"_a = *stream, "max_version"_a = nb::make_tuple(1, 0))
157
6/17
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✓ Branch 7 taken 1170 times.
✓ Branch 10 taken 1170 times.
✓ Branch 13 taken 1170 times.
✓ Branch 15 taken 1170 times.
✗ Branch 16 not taken.
✓ Branch 17 taken 1170 times.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✓ Branch 20 taken 1170 times.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
4680 : request("max_version"_a = nb::make_tuple(1, 0));
158
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ } catch (nb::python_error& error) {
159
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ if (!error.matches(PyExc_TypeError)) {
160 ✗ throw;
161 }
162
0/5
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✗ Branch 6 not taken.
✗ Branch 9 not taken.
✗ return stream ? request("stream"_a = *stream) : request();
163 ✗ }
164 1170 }
165
166 }
167
168 222 DLDataType DataType(device::DType type) {
169 222 const uint8_t bits = uint8_t(device::ElementBytes(type) * 8);
170
2/3
✓ Branch 0 taken 215 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
222 switch (type) {
171 215 case device::DType::Float32:
172 215 case device::DType::Float64:
173 215 return {kDLFloat, bits, 1};
174 ✗ case device::DType::Int32:
175 ✗ case device::DType::Int64:
176 ✗ return {kDLInt, bits, 1};
177 7 default:
178 7 return {kDLUInt, bits, 1};
179 }
180 }
181
182 2 nb::tuple TensorDevice(const device::Tensor& tensor) {
183
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (tensor.Empty()) {
184
0/1
✗ Branch 2 not taken.
✗ throw nb::buffer_error("An empty tensor has no memory to share");
185 }
186
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
2 if (const std::optional<DLDevice> shared = tensor.GetBuffer().GetDevice().SharedDevice()) {
187 ✗ return nb::make_tuple(int(shared->device_type), shared->device_id);
188 }
189 2 return nb::make_tuple(int(kDLVulkan), 0);
190 }
191
192 2 nb::object ExportTensor(const device::Tensor& tensor, std::optional<int64_t> stream,
193 std::optional<nb::tuple> maxVersion, std::optional<nb::tuple> dlDevice,
194 std::optional<bool> copy) {
195 2 const nb::tuple own = TensorDevice(tensor);
196
2/4
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
4 const int type = nb::cast<int>(own[0]);
197
2/4
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
4 const int id = nb::cast<int>(own[1]);
198 2 bool toHost = false;
199
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (dlDevice) {
200
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ const int wanted = nb::cast<int>((*dlDevice)[0]);
201
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ const int wantedId = nb::cast<int>((*dlDevice)[1]);
202 ✗ toHost = wanted == kDLCPU;
203
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ if (toHost && copy == false) {
204 ✗ throw nb::buffer_error("This tensor reaches the host only as a copy, and copy=False "
205
0/1
✗ Branch 2 not taken.
✗ "forbids one");
206 }
207
0/6
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ if (!toHost && (wanted != type || wantedId != id)) {
208
0/1
✗ Branch 1 not taken.
✗ throw nb::buffer_error(fmt::format("This tensor is on device ({}, {}); it's exported "
209 "there or copied to the host, not to ({}, {})",
210 type, id, wanted, wantedId)
211
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ .c_str());
212 }
213 }
214
1/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 if (!toHost && type == kDLVulkan) {
215 2 throw nb::buffer_error("This tensor's memory isn't shared with CUDA or HIP; ask for "
216
1/1
✓ Branch 2 taken 2 times.
2 "dl_device=(1, 0) to copy it to the host");
217 }
218
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ if (!toHost && stream) {
219
0/1
✗ Branch 1 not taken.
✗ CheckStream(type, *stream);
220 }
221
222 ✗ DLManagedTensorVersioned* managed = nullptr;
223 ✗ try {
224 ✗ nb::gil_scoped_release release;
225
0/7
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 5 not taken.
✗ Branch 8 not taken.
✗ Branch 11 not taken.
✗ managed = CreateExport(copy == true && !toHost ? CopyOnDevice(tensor) : tensor, toHost);
226
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (copy == true) {
227 ✗ managed->flags |= DLPACK_FLAG_BITMASK_IS_COPIED;
228 }
229
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ if (!toHost && stream != -1) {
230
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ const int64_t handle = stream.value_or(type == kDLCUDA ? 1 : 0);
231
0/2
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ tensor.GetBuffer().GetDevice().GetQueue().Mark().Wait(
232 Stream::Compute(reinterpret_cast<void*>(uintptr_t(handle))));
233 }
234
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ } catch (const UnsupportedError& error) {
235
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (managed != nullptr) {
236
0/1
✗ Branch 1 not taken.
✗ managed->deleter(managed);
237 }
238
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ throw nb::buffer_error(error.what());
239 ✗ } catch (...) {
240
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (managed != nullptr) {
241
0/1
✗ Branch 1 not taken.
✗ managed->deleter(managed);
242 }
243 ✗ throw;
244 ✗ }
245
0/6
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
2 const bool versioned = maxVersion && nb::cast<int>((*maxVersion)[0]) >= 1;
246
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✗ Branch 6 not taken.
✗ PyObject* capsule = versioned ? VersionedCapsule(managed) : UnversionedCapsule(managed);
247
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (capsule == nullptr) {
248
0/1
✗ Branch 1 not taken.
✗ managed->deleter(managed);
249 ✗ throw nb::python_error();
250 }
251 ✗ return nb::steal(capsule);
252 ✗ }
253
254 1170 TakenTensor TakeTensor(nb::handle source, std::optional<uintptr_t> stream) {
255 1170 const std::optional<int64_t> requested = RequestedStream(source, stream);
256 1170 const nb::object capsule = RequestCapsule(source, requested);
257
1/1
✓ Branch 1 taken 1170 times.
1170 auto* producer = new Producer();
258
2/3
✓ Branch 1 taken 1170 times.
✓ Branch 3 taken 1170 times.
✗ Branch 4 not taken.
1170 if (PyCapsule_IsValid(capsule.ptr(), kVersioned)) {
259
1/1
✓ Branch 1 taken 1170 times.
1170 producer->versioned =
260
1/1
✓ Branch 1 taken 1170 times.
1170 static_cast<DLManagedTensorVersioned*>(PyCapsule_GetPointer(capsule.ptr(), kVersioned));
261
1/1
✓ Branch 1 taken 1170 times.
1170 PyCapsule_SetName(capsule.ptr(), "used_dltensor_versioned");
262 1170 producer->wrapper.flags = producer->versioned->flags;
263 1170 producer->wrapper.dl_tensor = producer->versioned->dl_tensor;
264
0/3
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ } else if (PyCapsule_IsValid(capsule.ptr(), kUnversioned)) {
265
0/1
✗ Branch 1 not taken.
✗ producer->unversioned =
266
0/1
✗ Branch 1 not taken.
✗ static_cast<DLManagedTensor*>(PyCapsule_GetPointer(capsule.ptr(), kUnversioned));
267
0/1
✗ Branch 1 not taken.
✗ PyCapsule_SetName(capsule.ptr(), "used_dltensor");
268 ✗ producer->wrapper.dl_tensor = producer->unversioned->dl_tensor;
269 } else {
270
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ delete producer;
271
0/1
✗ Branch 2 not taken.
✗ throw nb::type_error("__dlpack__ returned no DLPack capsule");
272 }
273
274 1170 producer->wrapper.version = {DLPACK_MAJOR_VERSION, DLPACK_MINOR_VERSION};
275 1170 producer->wrapper.manager_ctx = producer;
276 1170 producer->wrapper.deleter = ReleaseProducer;
277
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1170 times.
1170 const Stream followed = requested
278
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1170 times.
1170 ? Stream::Compute(reinterpret_cast<void*>(uintptr_t(*requested)))
279 1170 : ToStream(stream);
280 1170 return {&producer->wrapper, followed};
281 1170 }
282
283 }
284