-
Notifications
You must be signed in to change notification settings - Fork 195
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
Eigen type caster improvements #215
Conversation
… scalars that it can actually handle. - de-bug: correct handling of Eigen/ndarray strides - enhanced: fully support Eigen's dynamic strides; request contiguous memory from ndarray only if really needed. - de-bug: prevent nb::cast<Eigen::Map> and nb::cast<Eigen::Ref<mutable>> from pointing to invalid memory due to conversions. - enhanced: renamed a few test functions to better reflect their signature.
…und function argument, and ensure that it owns the data it maps. - re-format to project coding style.
(FYI, I am working towards a conference deadline and will get back to reviewing PRs in a few days -- sorry for the delay) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A first set of feedback from a quick pass over the PR.
include/nanobind/eigen/dense.h
Outdated
@@ -28,6 +28,28 @@ NAMESPACE_BEGIN(detail) | |||
template <typename T> | |||
constexpr int NumDimensions = bool(T::IsVectorAtCompileTime) ? 1 : 2; | |||
|
|||
template<typename T> | |||
struct StrideExtr |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
very minor: in nanobind, opening braces are on the same line.
include/nanobind/eigen/dense.h
Outdated
using Type = Eigen::Stride<0, 0>; | ||
}; | ||
|
||
template <typename T, int Options, typename StrideType> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
include/nanobind/eigen/dense.h
Outdated
template<typename T> | ||
using Stride = typename StrideExtr<T>::Type; | ||
|
||
template<typename T> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add a comment explaining the different cases being considered here. I looked at it and couldn't understand the whole thing.
Please rename top-level constexpr to snake_case (requires_contig_memory
)
> | ||
>; | ||
f_contig>, | ||
any_contig>>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part wasn't clear to me. Why any_contig
if requiresContigMemory==false
?
tests/test_eigen.cpp
Outdated
@@ -150,4 +178,18 @@ NB_MODULE(test_eigen_ext, m) { | |||
nb::class_<ClassWithEigenMember>(m, "ClassWithEigenMember") | |||
.def(nb::init<>()) | |||
.def_rw("member", &ClassWithEigenMember::member); | |||
|
|||
m.def("castToMapVXi", [](nb::object obj) -> Eigen::Map<Eigen::VectorXi> | |||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fix indentation/brace, ditto for lines below
include/nanobind/eigen/dense.h
Outdated
|
||
if constexpr (T::IsRowMajor) | ||
std::swap(inner, outer); | ||
|
||
if constexpr (IS == 0) { | ||
assert(inner == 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't include cassert
, so I don't think it's generally valid to call that here. Do you think it's really necessary that these assertions are part of the type caster, or were these merely helpful to debug all cases on your end?
include/nanobind/eigen/dense.h
Outdated
if (std::max<int64_t>(1, StrideType::InnerStrideAtCompileTime) != (NumDimensions<T> == 1 || !T::IsRowMajor ? caster.value.stride(0) : caster.value.stride(1))) | ||
return false; | ||
if constexpr (NumDimensions<T> == 2 && StrideType::OuterStrideAtCompileTime != Eigen::Dynamic) { | ||
int64_t outerStrideExpected; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
camel_case.
include/nanobind/eigen/dense.h
Outdated
return false; | ||
if constexpr (!requiresContigMemory<Map>) { | ||
if constexpr (StrideType::InnerStrideAtCompileTime != Eigen::Dynamic) | ||
if (std::max<int64_t>(1, StrideType::InnerStrideAtCompileTime) != (NumDimensions<T> == 1 || !T::IsRowMajor ? caster.value.stride(0) : caster.value.stride(1))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't include <algorithm>
in type casters, so you can't call std::max
(even if convenient). The fact that it happens to compile is probably not good to rely on.
include/nanobind/eigen/dense.h
Outdated
if (!caster.from_python(src, flags, cleanup)) | ||
return false; | ||
if constexpr (!requiresContigMemory<Map>) { | ||
if constexpr (StrideType::InnerStrideAtCompileTime != Eigen::Dynamic) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic of this function is quite intricate -- as with the constexpr expression at the top, it would be good to document the cases in readable language.
Comments to clarify the type_casters for Eigen types. Formatting.
Hi @WKarel — I saw that you posted some changes. Please let me know once I should re-review. |
Dear @wjakob , I had prepared this comment, but forgot to post it :-D I hope to have addressed all your remarks in my latest commit. I'm a friend of comments in code, but I noticed that they are rather sparse in nanobind, and so I initially did not introduce any. Concerning the formatting rules, are they documented somewhere? I was confused by some pre-existing code like constexpr int NumDimensions ... right in dense.h, and the rules are still a bit unclear to me. I have commented out the assert statements. But once more out of curiosity, what is the motivation for not including Along the way, I discovered a bug when mapping Eigen::RowVector, which I resolved, together with introducing a respective test. The failed tests on Ubuntu should be gone.
What is your opinion on the above? |
Hi Karel, a few comments:
I agree that it is confusing and would be willing to support such a change. |
Dear Wenzel, thanks for your explanations. I do not expect any of the assertions to fail under any circumstances, unless there is a not yet discovered bug, of course. It seems that I had not inspected Since you agree that Cheers, |
type_caster<Eigen::Ref<T>>: prohibit conversions if T is mutable.
As discussed, |
Hi @WKarel, I did a pass over the PR (great work!) with some minor stylistic changes and plan to merge it shortly with those edits. However, there was one part of your changes that I did not understand. The type caster for What confuses me is that I don't understand how Could the following could happen?
Maybe I am understanding some of the details of how Unrelated: On top of your changes, I will be adding one more tweak to potentially create |
(updated my above comment with a more precise question) |
- support scalar type conversions only if a cleanup_list is passed, or if Ref will surely map its own data. - support dynamic strides only if they can surely map Ref's contiguous memory.
Dear @wjakob , thanks! The documentation on But indeed, there was still a flaw when scalar conversion took place, but no stride conversion! I have therefore pushed another commit that supports scalar type conversions only if either a cleanup_list is passed, or if the Ref is guaranteed to own the data it maps. Along the way, I have discovered a feature/behaviour/bug in Eigen that makes Ref fail silently if it cannot map its own data. Therefore, stride conversions are now restricted to the common case that Ref's StrideType can actually describe the contiguous memory of Ref's member variable. I've tried to document all the considerations while not making them too lengthy. Cheers, |
…eness, without affecting existing code. - avoid MSVC compiler warnings about unreachable code.
Hi @WKarel, thank you for those changes. I am confused about one last thing: The trait Should this not be a precondition for calling I also don't understand how this flag is combined with the presence of a cleanup list. I remembered that the ndarray caster doesn't even use the cleanup stack. |
(Here is my current diff for this PR. It's mostly a consistency/naming pass, and I shortened the commentary as well -- having the detailed version there was really useful to figure out what's going on though.) |
The only thing that still confuses me is the role of the |
Dear @wjakob , The constructors of However, it is true that the construction of a
So far, It may be necessary for If no cleanup_list is passed, then Cheers, |
Dear @wjakob, great, and you are welcome. Looking at dense.h, I think that https://github.com/wjakob/nanobind/blob/08b363aaeac0f487261e06d1d2437dec88a06cc2/include/nanobind/eigen/dense.h#LL400C5-L401C69
should be replaced with
|
Good catch, thanks! |
is_ndarray_scalar_v
, used to limittype_caster<Eigen>
to those scalars that it can actually handle. This gives users the chance to add their own specializations for other scalars.nb::cast<Eigen::Map>
andnb::cast<Eigen::Ref<mutable>>
from pointing to invalid memory due to conversions.As discussed,
type_caster<Eigen::Map>
andtype_caster<Eigen::Ref<mutable>>
do not support conversions any longer, unless a cleanup_list is passed. This preventsnb::cast
from returning Eigen types that point into destroyed memory.Still, I suggest these 2 type casters to not support conversions at all, even if they handle bound function arguments. I believe that the intention of binding a function that expects an
Eigen::Map
is to modify the passed memory (Map<mutable>
) or to avoid copies (Map<const>
). Silent conversions may hide errors and show surprising behaviour instead, see e.g. https://github.com/WKarel/nanobind/blob/f339ec0df6ce0d2d185e82df03e4e1905c04a183/tests/test_eigen.py#L99 where the argument remains unmodified.