Repository navigation
fix(cuda): make the CudaImageData device-buffer path work against the shipped binding - #98
Merged
Merged
Conversation
CudaImageData could not hand out a device cube at all in 3.6.0.0rc1. Three defects, all in this wrapper; the native SDK and the binding are correct. - _view() called cuvis_il.cuvis_cuda_view_ptr, which no cuvis-il build exports, so to_torch() and __cuda_array_interface__ both raised AttributeError on a perfectly good device buffer. SWIG already converts the void* field to an int, so the pointer is read from the struct directly and no binding helper can go missing again. - make_ipc() called cuvis_il.cuvis_cuda_descriptor_bytes, absent the same way. The descriptor is now serialized here, to the same 184-byte layout cuvis_ipc already parses; a test pins the two copies of that layout together. - __del__ and the DLPack capsule deleter freed with cuvis_cuda_mem_free(handle), but the C API declares cuvis_cuda_mem_free(CUVIS_CUDA_MEM*), like cuvis_cuda_ipc_handle_free. Every call raised TypeError out of the binding: ignored in __del__, invisible in the deleter, so no device buffer was ever returned to the SDK's pool and a long-running consumer exhausted the card. The handle is boxed now, and the ipc free is corrected the same way. Verified against SDK 3.6.0 on an RTX 4070: to_torch() is bit-identical to the host cube, and 40 read-and-drop cycles now grow device memory by 0 MiB. make_ipc(legacy) produces a 184-byte descriptor that round-trips through cuvis_ipc's own parser with the right backend, size and geometry; the cross-process import itself is not covered, since it needs cuda-python. Why this shipped: nothing ever executed the path. test_cuda.py covers capability reporting and the enable/disable toggle, and CudaImageData appeared only in test_handle_lifecycle with a deliberately invalid argument. CI cannot close that gap, since it runs in the cuvis_pyil container on a GPU-less runner. So the new tests lead with checks that need no device: a static assertion that cube_utils references no absent cuvis_il symbol, which fails on main and names both missing functions, and the descriptor layout. The device tests skip with an actionable reason, including when the SDK is processing on the host, which since 3.6.0 is what a cuvis.init that did not ask for the GPU leaves you with.
…o CUDA CI runs against a CPU-only libcuvis.so that exports none of the cuvis_cuda_* functions, so calling one raises RuntimeError from the binding's own guard rather than the TypeError the test is about. Gated on binding.available, the same way test_cuda.py gates its device-function tests. The static symbol test is deliberately not gated and passed on that runner: SWIG defines the Python attribute either way, so it still catches a wrapper calling a function the binding never declared.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
CudaImageDatacannot hand out a device cube at all in 3.6.0.0rc1 — every consumer path raises on first use. Three defects, all in this wrapper: the native SDK and thecuvis-ilbinding are both correct.Found while wiring the same-process device path into cuvis-ai-dataloader, which currently has to repair this at runtime.
The defects
1.
_view()calls a function that does not exist.cuvis_cuda_view_ptris exported by nocuvis-ilbuild and appears nowhere incuvis.swigorcuvis.c— it was never written. Soto_torch()and__cuda_array_interface__both raiseAttributeErroron a perfectly good device buffer. SWIG already converts thevoid *field to an int, so the pointer now comes off the struct directly and there is no helper left to go missing.2.
make_ipc()calls another one.cuvis_cuda_descriptor_bytesis absent the same way, somake_ipc()/export_payload()raise too. The descriptor is now serialized here, to the same 184-byte layoutcuvis_ipcalready documents and parses. Since that layout now exists in two places, a test pins them together.3. Device buffers are never freed.
__del__and the DLPack capsule deleter both callcuvis_cuda_mem_free(self._handle), but the C API declares:a pointer, exactly like
cuvis_cuda_ipc_handle_free. Every call raisesTypeErrorout of the binding — ignored in__del__, and invisible in the deleter — so no buffer is ever returned to the SDK's pool and a long-running consumer exhausts the card. The handle is boxed now, and the IPC free is corrected the same way. (The doxygen@param[in] i_memreads as if it took the handle by value, which is probably how this happened; the signature is the authority and is consistent with its sibling.)Note
cuvis.cuda.capabilities().same_processreturns True throughout, because it probes the native symbols incuvis.dllviabinding.availableand cannot see the Python layer over them. The failure lands at first use, not at the capability check, so gating oncapabilities()does not protect a caller.Why it shipped, and what stops it recurring
Nothing ever executed this code.
test_cuda.pycovers capability reporting and theenable/disabletoggle;CudaImageDataotherwise appears only intest_handle_lifecycle.py, constructed with a deliberately invalid argument. And CI cannot close that gap — it runs in thecuvis_pyilcontainer onubuntu-latest, with no GPU.So the new tests lead with ones that need no device:
test_every_binding_symbol_the_cuda_path_uses_existswalkscube_utils.pywithastand asserts everycuvis_il.<name>it references exists on the installed binding. Checked againstmain: it fails and names bothcuvis_cuda_view_ptrandcuvis_cuda_descriptor_bytes. This is the one that would have caught the original bug, and it runs on the GPU-less runner.test_descriptor_serialises_to_the_locked_wire_layoutandtest_the_wire_layout_matches_the_consumer_halfcover the descriptor bytes and keepcube_utilsandcuvis_ipcfrom drifting apart.test_free_passes_the_handle_the_way_the_c_api_takes_itpins the calling convention, asserting that the by-value form still raises.The five device tests skip with an actionable reason.
Verification
Against native SDK 3.6.0 on an RTX 4070 Laptop, real
.cu3sdata:_view()ptr=0x2667e4e00 size=65880000 device=0, exactly the cube's bytesto_torch()(1000, 1080, 61) torch.uint8 cuda:0, bit-identical to the host cube__cuda_array_interface__make_ipc(BACKEND_LEGACY)cuvis_ipc's own parser with the right backend, size and geometryFull suite: 172 passed, 5 skipped with a GPU; 168 passed, 9 skipped without one.
ruff checkandruff format --checkclean.Not covered
cuda-pythonon the consumer side, which is not installed here; and this device reportsipc_pool=False,ipc_vmm=False, so only the legacy copy-on-export backend was exercisable at all.ptr_blobis serialized fromptr_blob_lenbytes, which the legacy backend leaves at 0. The pool backend's path through it is untested for the same reason.