-
Notifications
You must be signed in to change notification settings - Fork 557
Fix indexedarray adjoint #1504
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?
Fix indexedarray adjoint #1504
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1312,6 +1312,38 @@ adj_address(const array_t<T>& buf, int i, const array_t<T>& adj_buf, int adj_i, | |
| else if (buf.grad) | ||
| adj_atomic_add(&index_grad(buf, i), adj_output); | ||
| } | ||
| // indexedarray adjoint: resolve the index indirection, then accumulate into the | ||
| // adjoint indexedarray's storage (adj_buf.arr) or the base array's embedded grad | ||
| template <typename T> | ||
| inline CUDA_CALLABLE void | ||
| adj_address(const indexedarray_t<T>& buf, int i, const indexedarray_t<T>& adj_buf, int adj_i, const T& adj_output) | ||
| { | ||
| if (i < 0) | ||
| i += buf.shape[0]; | ||
| if (buf.indices[0]) | ||
| i = buf.indices[0][i]; | ||
|
|
||
| if (adj_buf.arr.data) | ||
| adj_atomic_add(&index(adj_buf.arr, i), adj_output); | ||
| else if (buf.arr.grad) | ||
| adj_atomic_add(&index_grad(buf.arr, i), adj_output); | ||
| } | ||
| // indexedarray with a regular-array adjoint (as passed by the CUDA codegen): resolve the | ||
| // index indirection, then accumulate into the base grad or the base array's embedded grad | ||
| template <typename T> | ||
| inline CUDA_CALLABLE void | ||
| adj_address(const indexedarray_t<T>& buf, int i, const array_t<T>& adj_buf, int adj_i, const T& adj_output) | ||
| { | ||
| if (i < 0) | ||
| i += buf.shape[0]; | ||
| if (buf.indices[0]) | ||
| i = buf.indices[0][i]; | ||
|
|
||
| if (adj_buf.data) | ||
| adj_atomic_add(&index(adj_buf, i), adj_output); | ||
| else if (buf.arr.grad) | ||
| adj_atomic_add(&index_grad(buf.arr, i), adj_output); | ||
| } | ||
|
Comment on lines
+1331
to
+1346
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The comment says "as passed by the CUDA codegen", but after this PR the tape backward always passes an Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
| template <typename T> | ||
| inline CUDA_CALLABLE void | ||
| adj_address(const array_t<T>& buf, int i, int j, const array_t<T>& adj_buf, int adj_i, int adj_j, const T& adj_output) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1246,6 +1246,43 @@ def test_indexedarray_fill_struct(test, device): | |
| assert_np_equal(a4.numpy(), np.zeros(a4.shape, dtype=nptype)) | ||
|
|
||
|
|
||
| @wp.kernel | ||
| def kernel_indexedarray_grad_1d( | ||
| samples: wp.indexedarray(dtype=float), | ||
| weights: wp.array(dtype=float), | ||
| total: wp.array(dtype=float), | ||
| ): | ||
| i = wp.tid() | ||
| wp.atomic_add(total, 0, samples[i] * weights[i]) | ||
|
|
||
|
|
||
| def test_indexedarray_grad_1d(test, device): | ||
| # gradients must flow back through a differentiable indexedarray input (GH-1479): | ||
| # the adjoint follows the gather indirection and accumulates into the base array's grad | ||
| base = wp.array(np.linspace(1.0, 6.0, 6, dtype=np.float32), dtype=float, device=device, requires_grad=True) | ||
| weights_np = np.array([0.25, 0.5, 1.0], dtype=np.float32) | ||
| weights = wp.array(weights_np, dtype=float, device=device) | ||
| indices = wp.array([1, 3, 5], dtype=int, device=device) | ||
| samples = wp.indexedarray1d(base, [indices]) | ||
| total = wp.zeros(1, dtype=float, device=device, requires_grad=True) | ||
|
|
||
| tape = wp.Tape() | ||
| with tape: | ||
| wp.launch( | ||
| kernel_indexedarray_grad_1d, dim=samples.size, inputs=[samples, weights], outputs=[total], device=device | ||
| ) | ||
|
|
||
| # forward: sum of base[i] * weight over the gathered indices | ||
| assert_np_equal(total.numpy(), np.array([8.5], dtype=np.float32), tol=1e-6) | ||
|
|
||
| tape.backward(loss=total) | ||
|
|
||
| # d(total)/d(base[j]) is the matching weight at each gathered index, zero elsewhere | ||
| expected = np.zeros(6, dtype=np.float32) | ||
| expected[[1, 3, 5]] = weights_np | ||
| assert_np_equal(base.grad.numpy(), expected, tol=1e-6) | ||
|
Comment on lines
+1259
to
+1283
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Two behaviors introduced by this PR are not exercised: (1) |
||
|
|
||
|
|
||
| devices = get_test_devices() | ||
|
|
||
|
|
||
|
|
@@ -1254,6 +1291,7 @@ class TestIndexedArray(unittest.TestCase): | |
|
|
||
|
|
||
| add_function_test(TestIndexedArray, "test_indexedarray_1d", test_indexedarray_1d, devices=devices) | ||
| add_function_test(TestIndexedArray, "test_indexedarray_grad_1d", test_indexedarray_grad_1d, devices=devices) | ||
| add_function_test(TestIndexedArray, "test_indexedarray_2d", test_indexedarray_2d, devices=devices) | ||
| add_function_test(TestIndexedArray, "test_indexedarray_3d", test_indexedarray_3d, devices=devices) | ||
| add_function_test(TestIndexedArray, "test_indexedarray_4d", test_indexedarray_4d, devices=devices) | ||
|
|
||
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.
gradproperty silently permits ndim > 1 without C++ supportThe property creates a valid-looking
indexedarrayfor anyndim, but the C++adj_addressoverloads added in this PR only exist for the 1-D signature. When a 2-D+ indexed array is differentiated, the generated adjoint kernel callsadj_address(indexedarray_t, i, j, ...), which has no matching template, producing an opaque C++ compilation error at kernel-launch time. Adding a guard here would surface a clear, actionable Python error instead.