-
-
Notifications
You must be signed in to change notification settings - Fork 226
Pass custom_data through assemblers to kernel functions. #4013
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
base: main
Are you sure you want to change the base?
Conversation
|
Instead of |
|
I say probably because it's a performance critical part of the code - might need a bit of thought. |
|
The void* approach should have minimal overhead because:
Potential overhead is: Storing the pointer in integral_data struct (+8 bytes per integral) I can run some performance benchmarks of this branch vs 0.10 release. If you have any existing performance benchmarks that you trust let me know. |
|
I didn't explain myself very well. In modern C++ it's canonical to use std::optional rather than a pointer and nullptr to represent a value or absence of value. This also tends to wrap nicely into Python. For example: |
|
How about |
|
I have changed it to std::optional<void*> for now. Open for suggestions. |
648f439 to
cf52114
Compare
Replace void* with nullptr pattern with std::optional<void*> for custom_data in the Form class and assembly functions. This is the canonical modern C++ approach for representing optional values. Changes: - Form.h: integral_data::custom_data now uses std::optional<void*> - Form.h: custom_data() returns std::optional<void*> - Form.h: set_custom_data() accepts std::optional<void*> - assemble_*_impl.h: Function parameters use std::optional<void*> - assemble_*_impl.h: Kernel calls use .value_or(nullptr) - Python bindings: Handle std::optional with None support - Test: Update to expect None instead of 0 for unset custom_data Benefits: - Clearer intent: "optional value" vs "magic nullptr" - Type safety: Distinguishes "no value" from "null pointer value" - Pythonic: Returns None instead of 0 when not set - Zero-cost: .value_or(nullptr) compiles to same code
cf52114 to
8101685
Compare
jhale
left a comment
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.
Looks pretty good - a few small changes to make.
| nb::arg("type"), nb::arg("id"), nb::arg("kernel_idx"), | ||
| nb::arg("data").none(), | ||
| "Set custom data pointer for an integral. The data pointer is " | ||
| "passed to the kernel. Pass None to clear.") |
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.
I think the last part "Pass None to clear" is unnecessary. Maybe instead explain that None means no data.
|
|
||
|
|
||
| # Helper intrinsic to cast void* to a typed pointer for custom_data | ||
| @numba.extending.intrinsic |
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.
Do we already have this intrinsic in FFCx?
| b = numba.carray(b_, (3), dtype=dtype) | ||
| coordinate_dofs = numba.carray(coords_, (3, 3), dtype=xdtype) | ||
|
|
||
| # Cast void* to float64* and read the scale value |
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.
Nice!
| coordinate_dofs = numba.carray(coords_, (3, 3), dtype=xdtype) | ||
|
|
||
| # Cast void* to float64* and read the scale value | ||
| typed_ptr = voidptr_to_float64_ptr(custom_data) |
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.
Do we have any more complex examples? For example, an array of float64s index by the current cell. This is what @chrisrichardson mentioned before "a kernel might need to know which cell it's running on".
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.
I saw the complex examples/tests below with a length-two region of memory, but they don't quite hit the point just made.
| } | ||
|
|
||
| /// @brief Set the custom data pointer for an integral. | ||
| /// |
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.
I would like to add a warning here:
"void-pointers are inherently unsafe and cannot be type or bounds checked. Incorrect usage of this feature can and will lead to undefined behaviour and crashes."
| [](dolfinx::fem::Form<T, U>& self, dolfinx::fem::IntegralType type, | ||
| int id, int kernel_idx, std::optional<std::uintptr_t> data) | ||
| { | ||
| self.set_custom_data(type, id, kernel_idx, |
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.
set_custom_data doesn't quite fit the design of the rest of the class - the custom data pointer should be passed up (or not) at creation time, and then it is immutable (at least, the pointer is - the user could modify the pointed memory of course).
| "custom_data", | ||
| [](const dolfinx::fem::Form<T, U>& self, | ||
| dolfinx::fem::IntegralType type, int id, | ||
| int kernel_idx) -> std::optional<std::uintptr_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.
Why are we returning std::uintptr_t and not void*? If there is a good reason there needs to be a comment.
|
|
||
| # Verify the assembly used our custom values | ||
| # The offset should contribute to each DOF | ||
| assert la.norm(b) > 0 |
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.
I suspect this assert always comes up true. Can you do something more precise?
I don't think nanobind supports I'm not sure about the argument regarding |
Add void* custom_data member to integral_data struct and pass it to assembly functions (matrix, vector, scalar, lifting). Custom_data defaults back to nullptr if no custom_data is set explicitly (with set_custom_data()). The required changes are minimal
This enables integration kernels to receive additional per-cell user data which is essential for example for runtime quadrature rules. With this extensions users are no longer required to completely rewrite all assembly routines for customised C-kernels.
For an example library that relies on custom_data and its integration into the assembler see
https://github.com/sclaus2/runintgen