-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Not clear how to expose existing C++ vector as numpy array #1042
Comments
If you don't want to copy, one solution would be to move the |
Untested code, but this should be the implementation of the non-copy approach: auto v = new std::vector<int>(some_func());
auto capsule = py::capsule(v, [](void *v) { delete reinterpret_cast<std::vector<int>*>(v); });
return py::array(v->size(), v->data(), capsule); Yes, probably more black magic than you might expect. But then again, you're not doing something simple either. You are keeping a C++ object alive to make sure you can access its internal data safely, without leaking the memory. But if you don't mind the copy, just go: auto v = some_func();
return py::array(v.size(), v.data()); |
@YannickJadoul, thank you very it really works! I don't mind doing black magic (and the magic is in fact quite logical) but currently the user is not even aware that this kind of magic exists. |
Another suggestion. Probably it makes sense to provide an easy non-copying conversion from any contiguous buffer to py::arrray? Something like:
which will create corresponding py::buffer_info and capsule internally?
Manual wrapping of each argument with py::buffer, py::capsule into the py::array becomes tedious in such cases. |
Agreed, I had to look into the actual headers to check the exact constructors, etc. But I don't know about planned documentation updates. If you feel like it, I'm sure a PR with more documentation on this would be gladly accepted ;-) Then again, I'm not always sure what's stable API and what're implementation details. |
Not sure how easy that is to do (and how much more confusing this will make the whole situation). Maybe some kind of a static function as 'named constructor' could make sense, though? By the way, |
Sure, it won't work with "input" function parameters but works for "output" when one transforms c++ signature to python function returning tuple of numpy arrays instead of bunch of ref parameters (tht's exactly my case). In any case such thing should not be automatic - the user have to make it explicit in lambda. Vector is indeed not contigous, sorry. But for example vectorEigen::Vector3f is contigous and could be returned efficiently as 2d array. Such funny structures are common when dealing with variable number of space points when Eigen::MatrixXf is not usable due to unknown dimension. |
@YannickJadoul your code works, thanks for the reference, I just wanted to point out that you are missing a parenthesis at the end of the line |
By the way, std::shared_ptr<std::vector<float>> ptr = get_data();
return py::array_t<float>{
ptr->size(),
ptr->data(),
py::capsule(ptr.get(), [](void* p){ reinterpret_cast<decltype(ptr)*>(p)->reset(); }),
}; Obviously, this will never work, because when return happens, std::shared_ptr<std::vector<float>> ptr = get_data();
return py::array_t<float>{
ptr->size(),
ptr->data(),
py::capsule([ptr](){ }), // using lambda-capture to increase lifetime of ptr
}; Worked this solution (which seems very dirty): std::shared_ptr<std::vector<float>> ptr = get_data();
return py::array_t<float>{
ptr->size(),
ptr->data(),
py::capsule(
new auto(ptr), // <- can leak
[](void* p){ delete reinterpret_cast<decltype(ptr)*>(p); }
)
}; |
@arquolo Indeed, the only data that can be stored in a Is it that dirty, though? In the end, a The one thing to note, though, is that the object doesn't need to be a |
We define the following utility functions, which have proven to be live savers :) template <typename Sequence>
inline py::array_t<typename Sequence::value_type> as_pyarray(Sequence&& seq) {
// Move entire object to heap (Ensure is moveable!). Memory handled via Python capsule
Sequence* seq_ptr = new Sequence(std::move(seq));
auto capsule = py::capsule(seq_ptr, [](void* p) { delete reinterpret_cast<Sequence*>(p); });
return py::array(seq_ptr->size(), // shape of array
seq_ptr->data(), // c-style contiguous strides for Sequence
capsule // numpy array references this parent
);
} and the copy version template <typename Sequence>
inline py::array_t<typename Sequence::value_type> to_pyarray(const Sequence& seq) {
return py::array(seq.size(), seq.data());
} |
Thanks @ferdonline template <typename Sequence,
typename = std::enable_if_t<std::is_rvalue_reference_v<Sequence&&>>>
inline py::array_t<typename Sequence::value_type> as_pyarray(Sequence&& seq) With such fix, the compiler will warn you if you calls with without |
@arquolo If you call without std::move, it will bind as an L-value reference and then inside it does the |
You will destroy the original container, then, though. That's quite unexpected if you didn't call the container with an rvalue reference. |
The function is called |
@arquolo , you might be interested in what I have found: #323 (comment) |
If anyone's interested in a version of @ferdonline's utility function without explicit/manual
Apart from avoiding |
I'm not sure this would work? The memory would be freed early as there is nothing left to hold onto the heap allocation after the Then another heap allocation could grab the same memory, and new writes could corrupt what is already there (i.e. the numpy buffer we just returned). See https://www.cplusplus.com/reference/memory/unique_ptr/get/. |
@YannickJadoul This is what I am using:
|
That's why you call
This seems quite similar (or the same?) to @ferdonline's utility function. As far as I can see, it will still leak memory when |
@YannickJadoul You are right, your code is absolutely correct. I can't help but think that the content of the capsule function is just a very complicated way of calling delete. I greatly prefer modern C++ and smart pointers, but if there is (void *) in the middle it becomes more difficult to reason about the data flow (for me at least!). Either smart pointers up and down the entire stack, or not at all? It is tricky to choose the right level of abstraction, and sometimes if one abstracts too much the intent gets obscured. I did not see @ferdonline's utility function initially (see above), the one I quoted was written from first principles. It's somewhat interesting that they are virtually identical :) |
Yes, it definitely is, but it does have the advantage of covering the corner case of exceptions in |
This issue has been resolved. @YannickJadoul has done a great job answering questions here. Further question are better suited for gitter. |
I'm thinking. Maybe we can/should add a convenience function for this to pybind11, since it seems to be such a popular issue. I'll reopen to remind ourselves. |
For the record, I have a large Python module that has zero-copy communication between Python and C++ when working with columns in a DataFrame. It is zero-copy both ways, i.e. Python >> C++ and C++ >> Python. It is blazingly fast. I usually combine it with OpenMP or TBB to do multi-threaded calculations on the column data. It is all in pybind11 and Modern C++ (except for one raw pointer reference which is wrapped in a function; see above). It's easily testable, when the function is called from C++ is accepts a templated vector, and when it is called from Python it accepts a templated span. The zero-copy C++ >> Python adapter is in my post above. This is the zero-copy Python >> C++ adapter:
|
Hi, I would like to check whether the cleanup function is really called, so wrote the following code. auto v = new std::vector<int>(some_func());
auto capsule = py::capsule(v, [](void *v) {
py::scoped_ostream_redirect output;
std::cout << "deleting int vector\n";
delete reinterpret_cast<std::vector<int>*>(v);
});
return py::array(v->size(), v->data(), capsule); However, import gc
gc.collect(2)
gc.collect(1)
gc.collect(0) Could you help me to make the cleanup function called explicitly? Thank you |
@tlsdmstn56-2 You need to delete the variable returned by the pybind11 module on the Python side, or else the memory will not be freed.
|
This is great for sharing the raw data, but how does it handle ownership? It looks like the short answer is that it doesn't, but maybe I'm missing something. Thanks! |
@cchriste mentioned:
This is great for sharing the raw data, but how does it handle ownership?
It looks like the short answer is that it doesn't, but maybe I'm missing
something. Thanks!
Short answer: it doesn't, but that's fine as the parent Python function
caller holds ownership for the duration of the call.
Remember, this is the "zero-copy Python >> C++ adapter", so Python creates
the Numpy array, C++ modifies the array contents, then returns.
Here is an example scenario:
* Python creates a Numpy array, it is the owner.
* Python calls a method written in C++/pybind11.
* The C++ uses the `toSpan` method above to obtain a reference to this
array.
* The C++ can then safely edit the contents of this array.
* The C++ returns.
* The Numpy array is now modified, without the overhead of copying the
array's contents back and forth from Python to C++ to Python.
This is *really* useful when modifying columns in a DataFrame.
It would be possible to break this if we really wanted to. The C++ side
could create another thread, and that thread could start modifying the
array behind Python's back, even after the original function call had
returned and the Python side had deallocated it. But we assume that once
the C++ function returns it does not touch that array again.
…On Tue, 1 Jun 2021, 23:20 Cameron Christensen, ***@***.***> wrote:
For the record, I have a large Python module that has zero-copy
communication between Python and C++ when working with columns in a
DataFrame. It is zero-copy both ways, i.e. Python >> C++ and C++ >> Python.
It is blazingly fast.
I usually combine it with OpenMP or TBB to do multi-threaded calculations
on the column data.
It is all in pybind11 and Modern C++ (except for one raw pointer reference
which is wrapped in a function; see above). It's easily testable, when the
function is called from C++ is accepts a templated vector, and when it is
called from Python it accepts a templated span.
The zero-copy C++ >> Python adapter is in my post above.
This is the zero-copy Python >> C++ adapter:
/**
* \brief Returns span<T> from py:array_T<T>. Efficient as zero-copy.
* \tparam T Type.
* \param passthrough Numpy array.
* \return Span<T> that with a clean and safe reference to contents of Numpy array.
*/
template<class T=float32_t>
inline std::span<T> toSpan(const py::array_t<T>& passthrough)
{
py::buffer_info passthroughBuf = passthrough.request();
if (passthroughBuf.ndim != 1) {
throw std::runtime_error("Error. Number of dimensions must be one");
}
size_t length = passthroughBuf.shape[0];
T* passthroughPtr = static_cast<T*>(passthroughBuf.ptr);
std::span<T> passthroughSpan(passthroughPtr, length);
return passthroughSpan;
}
This is great for sharing the raw data, but how does it handle ownership?
It looks like the short answer is that it doesn't, but maybe I'm missing
something. Thanks!
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#1042 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAJ3FJHLSPLXCIF4OG3VKCDTQVMHHANCNFSM4DY367GQ>
.
|
I appreciate the quick reply, and agree this is very useful. For our use case, we do in fact want to take ownership of the data. Going from C++ to Python seems safe: memory buffers are tagged with an ownership flag and, after the last reference to that memory is removed, won't be freed unless owned. Thanks for your other example demonstrating a trick to claim ownership when creating arrays for which pybind11 should simply provide a more straightforward argument. The other way around does not seem as straightforward. Even if some clever combination of PyObject_GetBuffer/PyObject_Release can be used to ensure Python doesn't delete memory out from under C++, if it's deleted by C++ then any existing Python objects will suddenly be pointing to deallocated space. Maybe if ownership transfer is achieved using a move (a |
@cchriste For Python to C++, I imagine that if the C++ wanted to take ownership of the data, the easiest and safest way would be to make a copy. I imagine that's the only way to prevent Python garbage collecting that data once You also mentioned:
... but the method above exposes the Numpy array as a |
@virtuald I am also encountering this problem. As far as I understand, returning a However, a I have figured it out. I can "lend" my data to an |
not necessarily a pybind solution, but you could allocate the std::vector on the heap with |
Thanks for sharing the code. One thing to notice is that if T is a struct but not packed (i.e., In this case, the alignment of the input buffer |
I was using this version for a while in a library, but recently I noticed it did not work anymore. It must something related to the compiler because I did not change the pybind11 version I was using (its commit is fixed a git submodule in my library). But the version of @sharpe5 works. The main difference seems to come from the constructor of
|
add individual frame counting - no support for row-dark currently - had to change ctor for vectorToPyArray, see here: pybind/pybind11#1042 (comment) . This may be a numpy 2 thing - outputs will be a SparseArray with one frame/scan shape = (1, 1) and no metadata. This is to take advantage of methods in SparseArray
This is a question of documentation rather than an issue. I can't find any example of the following very common scenario:
I don't know the answer. It would be very nice to have this explained in docs since this scenario if rather common.
The text was updated successfully, but these errors were encountered: