Skip to content

Add support for indexed arrays in structs and related kernels#1328

Open
FabienPean-Virtonomy wants to merge 1 commit intoNVIDIA:mainfrom
Virtonomy:FabienPean/indexedarray_in_struct
Open

Add support for indexed arrays in structs and related kernels#1328
FabienPean-Virtonomy wants to merge 1 commit intoNVIDIA:mainfrom
Virtonomy:FabienPean/indexedarray_in_struct

Conversation

@FabienPean-Virtonomy
Copy link
Copy Markdown

@FabienPean-Virtonomy FabienPean-Virtonomy commented Mar 30, 2026

Description

This PR adds support for indexedarray within struct.

Closes #1327

Checklist

  • I am familiar with the Contributing Guidelines.
  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

Test plan

Tests were added in warp/tests/test_indexedarray.py

New feature / enhancement

This now works

import warp as wp

@wp.struct
class Foo:
    arr: wp.indexedarray(dtype=wp.float32)

Summary by CodeRabbit

  • Bug Fixes

    • Proper support for indexedarray fields inside structs: assignment, device transfers (including cross-device/CUDA behavior), and NumPy structured-value conversion; works for nested structs and arrays-of-structs.
  • Tests

    • Added tests exercising indexedarrays in structs, nested structs, struct arrays, NumPy conversion, and device-transfer scenarios.
  • Documentation

    • Changelog updated to document indexedarray support in struct operations.

@copy-pr-bot
Copy link
Copy Markdown

copy-pr-bot bot commented Mar 30, 2026

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds support for using wp.indexedarray as a wp.struct field: construction and setters, device transfers via StructInstance.to, NumPy structured dtype/value serialization, native atomic overload for indexed arrays, and tests for kernel access, nesting, struct arrays, NumPy interop, and device transfer.

Changes

Cohort / File(s) Summary
Changelog
CHANGELOG.md
Added an [Unreleased] entry documenting support for wp.indexedarray fields in structs.
Struct marshalling & codegen
warp/_src/codegen.py
Recognize indexedarray field types in constructors/setters; added set_indexedarray_value() setter (accepts None or validated indexedarray), include indexedarray_t in ctypes _fields_, preserve indexedarray across StructInstance.to(device), and serialize indexedarray in Struct.numpy_dtype(), StructInstance.numpy_value(), and Struct.from_ptr().
Type system & serialization
warp/_src/types.py
Added indexedarray_t._numpy_dtype_, indexedarray_t.numpy_dtype() classmethod, and indexedarray_t.numpy_value() instance method; adjusted indexedarray_t.__init__ handling when data is None to construct an empty array_t and ensure NumPy structured-dtype/value interoperability.
Native kernel support
warp/native/array.h
Added a CUDA-callable atomic_add overload for indexedarray_t<T>* / indexedarray_t<T> to enable atomic-add resolution when indexed arrays are embedded in structs (no-op stub).
Tests
warp/tests/test_indexedarray.py
Added IndexedArrayStruct and NestedIndexedArrayStruct; kernels reading and mutating indexedarray fields (direct, nested, array-of-structs) with atomic updates; tests for kernel behavior, nested/array-of-structs, NumPy dtype/value, and CUDA .to(device) transfer.

Sequence Diagram

sequenceDiagram
    participant User
    participant StructDef as Struct Definition
    participant Setter as Field Setter/Constructor
    participant Marshall as Marshalling Layer
    participant Device as Device (CPU/CUDA)
    participant Native as Native Kernel Layer

    User->>StructDef: define `@wp.struct` with `indexedarray` field
    StructDef->>Setter: build constructor & setter for `indexedarray`
    User->>Marshall: instance.to(device)
    Marshall->>Device: call member.value.to(device)
    Device-->>Marshall: return device-bound array/array-like
    Marshall->>Marshall: wrap/ensure `indexedarray(...)` on mismatch

    User->>Marshall: instance.numpy_value() / Struct.numpy_dtype()
    Marshall->>Device: call indexedarray_t.numpy_dtype() / indexedarray.numpy_value()
    Device-->>Marshall: return NumPy-compatible structured dtype/value

    User->>Native: launch kernel accessing indexedarray in struct
    Native->>Native: resolve `atomic_add` overload for `indexedarray`
    Native-->>User: kernel executes and updates indexedarray
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding support for indexed arrays as struct fields with kernel integration.
Linked Issues check ✅ Passed All coding requirements from issue #1327 are addressed: indexedarray can be struct fields, assignment/device transfer works, numpy interop is supported, and kernel integration is tested.
Out of Scope Changes check ✅ Passed All changes are directly related to supporting indexedarray in structs: codegen updates, type system additions, native function stubs, tests, and changelog documentation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@FabienPean-Virtonomy FabienPean-Virtonomy force-pushed the FabienPean/indexedarray_in_struct branch from a9690f0 to 06e268d Compare March 30, 2026 12:19
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 30, 2026

Greptile Summary

This PR adds wp.indexedarray support in @wp.struct fields by extending all struct code paths in codegen.py and types.py to handle the indexed-array type alongside regular arrays, and by adding a no-op atomic_add stub in array.h for the backward-pass. Cross-device transfer via StructInstance.to() converts an indexed array to a contiguous identity indexed array (preserving data values at the indexed positions but dropping the index selectors), which is the intended design documented in comments and validated by a new device-transfer test. All previously raised review concerns — the unreachable else branch, the ndim/Any assertion edge case, the missing device-transfer test, and the same-device string comparison — are addressed in this revision.

Confidence Score: 5/5

Safe to merge — all prior review concerns are addressed and the implementation matches existing patterns.

No P0 or P1 issues remain. All previously raised concerns (unreachable branch, ndim/Any annotation edge case, missing device-transfer test, same-device string comparison) have been addressed. The new code is consistent with pre-existing array handling patterns and is covered by five new targeted tests.

No files require special attention.

Important Files Changed

Filename Overview
warp/_src/codegen.py Extends all struct code paths (field constructor, setter, to(), numpy_value(), numpy_dtype(), from_ptr(), ctype fields) to handle indexedarray fields alongside array; device transfer converts indexed arrays to identity indexed arrays by design.
warp/_src/types.py Fixes indexedarray_t.init null-path to derive ndim from len(shape) instead of calling array().ctype(); adds numpy_dtype(), numpy_value(), and numpy_dtype to indexedarray_t, mirroring the array_t pattern.
warp/native/array.h Adds a no-op atomic_add(indexedarray_t*, indexedarray_t) stub matching the pre-existing no-op for array_t, satisfying backward-pass codegen when an indexed array field appears in a differentiable struct.
warp/tests/test_indexedarray.py Adds five new tests covering plain struct, nested struct, struct array, NumPy round-trip, and CPU-to-CUDA device transfer scenarios for indexed array struct fields.
CHANGELOG.md Adds a single Unreleased entry for indexedarray struct support with the correct GH-1327 issue reference.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[StructInstance.to(device)] --> B[for each field in struct]
    B --> C{field type?}
    C -->|array| D[value.to(device)]
    D --> E[setattr dst field]
    C -->|indexedarray| F[cloned = value.to(device)]
    F --> G{isinstance cloned\nindexedarray?}
    G -->|Yes\nsame device, returned self| H[setattr dst field = cloned]
    G -->|No\ndiff device, warp.clone returned array| I[setattr dst field = indexedarray cloned\nidentity indexed array]
    C -->|nested struct| J[recurse via stack]
    C -->|other| K[setattr dst field = value]
Loading

Reviews (6): Last reviewed commit: "Add support for indexed arrays in struct..." | Re-trigger Greptile

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
warp/tests/test_indexedarray.py (1)

92-158: Add a regression test for nullable struct fields + .to(device)

The linked objective includes assigning None to indexedarray struct fields and moving structs across devices. Adding one focused test for s.iarr = None followed by s.to(device) would lock this behavior and prevent regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/tests/test_indexedarray.py` around lines 92 - 158, Add a regression test
that assigns None to an IndexedArrayStruct field and then calls .to(device) to
ensure nullable struct fields survive device moves: create an instance of
IndexedArrayStruct, set s.iarr = None, call s.to(device) (or
struct_arr.to(device) if testing arrays), and assert the field remains None (or
numpy_dtype/ numpy_value reflect null) and no exceptions are raised; reference
IndexedArrayStruct, s.iarr, and the .to(device) call in the new test (e.g.,
test_indexedarray_nullable_field_to_device).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CHANGELOG.md`:
- Line 80: Update the changelog entry for the fix "Fix `@wp.struct` packing to
support `wp.indexedarray` fields (assignment, device transfer, and NumPy
structured values)" to include the issue reference that this PR closes (GH-1327)
by appending the issue link reference format (e.g.
"([GH-1327](https://github.com/<org>/<repo>/issues/1327))") to that line so it
complies with the changelog guideline; ensure the exact entry text remains
unchanged except for adding the issue reference.

In `@warp/_src/codegen.py`:
- Around line 333-335: StructInstance.to() currently calls value.to(device) for
fields detected by matches_array_class(var.type, indexedarray) without checking
for None; change that branch to guard nullable indexedarray fields by checking
if value is not None before calling value.to(device) and otherwise set the
destination attribute to None (i.e., replace the unconditional setattr(dst,
name, value.to(device)) with a conditional that calls value.to(device) only when
value is not None and sets dst.name to None when it is).

In `@warp/_src/types.py`:
- Around line 2207-2218: The numpy_value method currently forwards
self.data.numpy_value(), but when self.data is None the fallback ends up
creating a 1-D array which loses the original ndim; update numpy_value so that
if self.data is None you construct a placeholder numpy/ctype array with ndim
equal to len(self.shape) (or the stored ndim if available) and the same shape
(use self.shape to derive dimensions) before calling its numpy_value(), so
serialized tuples preserve the original ndim for wp.indexedarray fields; keep
the existing indices handling and return signature (self.data.numpy_value(),
indices, list(self.shape)).

In `@warp/tests/test_indexedarray.py`:
- Line 104: After each wp.launch call in the test functions (e.g., the
wp.launch(kernel_indexedarray_in_struct, dim=iarr.size, inputs=[s,
expected_arr], device=device) invocation), add an explicit
wp.synchronize_device(device) immediately after the launch so kernel-side
assertions (wp.expect_eq) are flushed and failures surface deterministically;
apply the same change for the other wp.launch calls referenced (the launches at
the other test locations around lines 122 and 138) to ensure all launches are
followed by wp.synchronize_device(device).

---

Nitpick comments:
In `@warp/tests/test_indexedarray.py`:
- Around line 92-158: Add a regression test that assigns None to an
IndexedArrayStruct field and then calls .to(device) to ensure nullable struct
fields survive device moves: create an instance of IndexedArrayStruct, set
s.iarr = None, call s.to(device) (or struct_arr.to(device) if testing arrays),
and assert the field remains None (or numpy_dtype/ numpy_value reflect null) and
no exceptions are raised; reference IndexedArrayStruct, s.iarr, and the
.to(device) call in the new test (e.g.,
test_indexedarray_nullable_field_to_device).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: b9dd14c8-7838-40c4-8118-767015ef7d55

📥 Commits

Reviewing files that changed from the base of the PR and between 63e1840 and 06e268d.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • warp/_src/codegen.py
  • warp/_src/types.py
  • warp/native/array.h
  • warp/tests/test_indexedarray.py

Comment on lines +333 to +335
elif matches_array_class(var.type, indexedarray):
# indexedarray_t
setattr(dst, name, value.to(device))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard nullable indexedarray fields before device transfer

StructInstance.to() currently calls value.to(device) unconditionally for indexedarray fields. If the field is set to None (which your setter supports), this raises at runtime.

💡 Proposed fix
-            if matches_array_class(var.type, array):
-                # array_t
-                setattr(dst, name, value.to(device))
-            elif matches_array_class(var.type, indexedarray):
-                # indexedarray_t
-                setattr(dst, name, value.to(device))
+            if matches_array_class(var.type, array) or matches_array_class(var.type, indexedarray):
+                # array_t / indexedarray_t
+                setattr(dst, name, None if value is None else value.to(device))
🧰 Tools
🪛 Ruff (0.15.7)

[error] 333-333: matches_array_class may be undefined, or defined from star imports

(F405)


[error] 333-333: indexedarray may be undefined, or defined from star imports

(F405)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/codegen.py` around lines 333 - 335, StructInstance.to() currently
calls value.to(device) for fields detected by matches_array_class(var.type,
indexedarray) without checking for None; change that branch to guard nullable
indexedarray fields by checking if value is not None before calling
value.to(device) and otherwise set the destination attribute to None (i.e.,
replace the unconditional setattr(dst, name, value.to(device)) with a
conditional that calls value.to(device) only when value is not None and sets
dst.name to None when it is).

@shi-eric shi-eric requested a review from nvlukasz March 30, 2026 15:21
Copy link
Copy Markdown
Contributor

@nvlukasz nvlukasz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this looks pretty good. Just a few minor comments worth addressing.

setattr(dst, name, value.to(device))
elif matches_array_class(var.type, indexedarray):
# indexedarray_t
setattr(dst, name, value.to(device))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that indexedarray.to() will create a regular array if it needs to be copied to a different device.

We could consider doing a full clone of data + indices to create an indexedarray, but I think that's beyond the scope of this PR. So in the meantime, we could raise an exception if the device does not match.

Copy link
Copy Markdown
Author

@FabienPean-Virtonomy FabienPean-Virtonomy Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah indeed, and the test only test same device copy. I find the shrinking to an array a good approach in general, I would prefer to leave it up to the user to consider where they store the backing array if they care about reuse or optimization.

An easy way around would be to use the passthrough property of indexedarray_t when indices=None as it would behave like an array_t if I read right.
So it means modifying either at codegen point above, or adding method to on python side indexedarray, or modifying noncontiguous base to method. Would any of the three be ok with you, are there issues I did not foresee?

Copy link
Copy Markdown
Author

@FabienPean-Virtonomy FabienPean-Virtonomy Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The different possibilities:

Option 1: at codegen, coerce to indexedarray only in struct packing

setattr(dst, name, value.to(device) if value.device is device else wp.indexedarray(value.to(device))

Option 2: in indexedarray, always return an indexedarray and discards return array path, which means a pointer check for index access compared to array path

    def to(self, device):
        device = warp.get_device(device)
        if self.device == device:
            return self

        dense = warp.clone(self, device=device)
        return indexedarray(data=dense, indices=None)

Option 3: in noncontiguousbase, not applicable due to other child class indexedfabricarray

# types.py L4437
return type(self)(warp.clone(self, device=device))

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
warp/_src/codegen.py (1)

333-344: ⚠️ Potential issue | 🔴 Critical

Guard nullable indexedarray fields before dereferencing value.device.

Line 338 assumes every field holds a concrete object, but Lines 442-453 explicitly allow None. s.iarr = None; s.to(device) will currently fail with AttributeError instead of preserving the nullable field.

💡 Suggested fix
             elif matches_array_class(var.type, indexedarray):
                 # indexedarray_t
+                if value is None:
+                    setattr(dst, name, None)
+                    continue
                 # only support same-device no-op transfers for now
                 # cross-device transfers `iarr.to` returns an array_t
                 target_device = warp.get_device(device)
                 if value.device != target_device:
                     raise RuntimeError(

Also applies to: 442-453

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/codegen.py` around lines 333 - 344, The nullable indexedarray field
case must be guarded before dereferencing value.device: in the block that
handles matches_array_class(var.type, indexedarray) check if value is None (or
uses a safe attribute access) and if so set dst's field to None (preserving the
nullable) instead of accessing value.device; otherwise proceed to get
target_device, compare devices and call value.to(target_device) and setattr(dst,
name, ...) as before. Ensure the same null-check pattern is applied to the
duplicate block around lines 442-453 that also handles indexedarray fields.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@warp/_src/codegen.py`:
- Around line 333-344: The nullable indexedarray field case must be guarded
before dereferencing value.device: in the block that handles
matches_array_class(var.type, indexedarray) check if value is None (or uses a
safe attribute access) and if so set dst's field to None (preserving the
nullable) instead of accessing value.device; otherwise proceed to get
target_device, compare devices and call value.to(target_device) and setattr(dst,
name, ...) as before. Ensure the same null-check pattern is applied to the
duplicate block around lines 442-453 that also handles indexedarray fields.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 57fc8d8b-a7b8-4d73-97cb-38576bf9bf32

📥 Commits

Reviewing files that changed from the base of the PR and between 06e268d and f26c05b.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • warp/_src/codegen.py
  • warp/tests/test_indexedarray.py
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md

@FabienPean-Virtonomy FabienPean-Virtonomy force-pushed the FabienPean/indexedarray_in_struct branch from f26c05b to 5cf0b56 Compare April 1, 2026 09:18
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
warp/_src/types.py (1)

2206-2218: ⚠️ Potential issue | 🟠 Major

Preserve ndim when serializing null indexed arrays.

numpy_value() still forwards self.data.numpy_value(), but the null-data path in indexedarray_t.__init__() seeds self.data with array().__ctype__(), which hard-codes ndim == 1. A wp.indexedarray(..., ndim>1) struct field set to None will therefore round-trip through the new structured NumPy path with the wrong dimensionality.

💡 Proposed fix
 class indexedarray_t(ctypes.Structure):
     def __init__(self, data, indices, shape):
         if data is None:
-            self.data = array().__ctype__()
+            ndim = len(shape)
+            self.data = array_t(data=0, grad=0, ndim=ndim, shape=(0,) * ndim, strides=(0,) * ndim)
             for i in range(ARRAY_MAX_DIMS):
                 self.indices[i] = ctypes.c_void_p(None)
                 self.shape[i] = 0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/types.py` around lines 2206 - 2218, The numpy_value() serializer
for indexedarray_t should preserve the original ndim when self.data was seeded
with a default placeholder (array().__ctype__()) in indexedarray_t.__init__;
currently it forwards self.data.numpy_value() which hard-codes ndim==1 for
null-data cases. Update numpy_value() to detect the placeholder/null-data case
(the default array ctype created in indexedarray_t.__init__) and synthesize a
numpy_value for an empty array that uses self.ndim (or otherwise set the correct
ndim) instead of blindly using self.data.numpy_value(); keep the rest of the
returned tuple (indices, shape) intact so a None-valued indexedarray(...,
ndim>1) round-trips with the correct dimensionality.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@warp/_src/types.py`:
- Around line 2206-2218: The numpy_value() serializer for indexedarray_t should
preserve the original ndim when self.data was seeded with a default placeholder
(array().__ctype__()) in indexedarray_t.__init__; currently it forwards
self.data.numpy_value() which hard-codes ndim==1 for null-data cases. Update
numpy_value() to detect the placeholder/null-data case (the default array ctype
created in indexedarray_t.__init__) and synthesize a numpy_value for an empty
array that uses self.ndim (or otherwise set the correct ndim) instead of blindly
using self.data.numpy_value(); keep the rest of the returned tuple (indices,
shape) intact so a None-valued indexedarray(..., ndim>1) round-trips with the
correct dimensionality.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 227bd925-f201-4056-943e-f9a3667833a1

📥 Commits

Reviewing files that changed from the base of the PR and between f26c05b and 5cf0b56.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • warp/_src/codegen.py
  • warp/_src/types.py
  • warp/native/array.h
  • warp/tests/test_indexedarray.py
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • warp/native/array.h
👮 Files not reviewed due to content moderation or server errors (2)
  • warp/_src/codegen.py
  • warp/tests/test_indexedarray.py

@FabienPean-Virtonomy FabienPean-Virtonomy force-pushed the FabienPean/indexedarray_in_struct branch from 5cf0b56 to 02d0e90 Compare April 1, 2026 10:10
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
warp/_src/codegen.py (1)

320-324: ⚠️ Potential issue | 🔴 Critical

Handle None before moving indexedarray fields.

Line 323 still calls value.to(device) unconditionally. The new constructor/setter path allows indexedarray fields to be None, so StructInstance.to() now raises for a valid struct state.

🐛 Proposed fix
             elif matches_array_class(var.type, indexedarray):
                 # indexedarray_t
-                # `.to` returns an array if on different device, force to identity indexedarray
-                cloned = value.to(device)
-                setattr(dst, name, cloned if isinstance(cloned, indexedarray) else indexedarray(cloned))
+                if value is None:
+                    setattr(dst, name, None)
+                else:
+                    # `.to()` returns an array if on different device, force to identity indexedarray
+                    cloned = value.to(device)
+                    setattr(dst, name, cloned if isinstance(cloned, indexedarray) else indexedarray(cloned))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/codegen.py` around lines 320 - 324, The code calls value.to(device)
unconditionally for fields matched by matches_array_class(var.type,
indexedarray), but fields may be None; update the block in codegen.py (the
branch handling indexedarray) to first check if value is None and if so set
setattr(dst, name, None) and skip the .to() call, otherwise perform cloned =
value.to(device) and set setattr(dst, name, cloned if isinstance(cloned,
indexedarray) else indexedarray(cloned)); this prevents StructInstance.to() from
raising when indexedarray fields are None.
🧹 Nitpick comments (1)
warp/tests/test_indexedarray.py (1)

154-160: Exercise the structured NumPy round-trip, not just the wrapper shape.

This still passes if numpy_dtype() returns incompatible formats/offsets or if numpy_value() serializes the wrong payload. Materializing a structured NumPy array from both values will catch the interop breakage this test is meant to cover.

🧪 Proposed enhancement
     # Just ensure these are functional for structs embedding indexedarray_t
     dtype = IndexedArrayStruct.numpy_dtype()
     value = s.numpy_value()
+    structured = np.array([value], dtype=np.dtype(dtype))
 
     test.assertIsInstance(dtype, dict)
     test.assertEqual(dtype["names"], ["iarr"])
     test.assertEqual(len(value), 1)
+    test.assertEqual(structured.dtype.names, ("iarr",))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/tests/test_indexedarray.py` around lines 154 - 160, The test currently
only checks dtype names and wrapper shape; instead materialize a structured
NumPy array using the dtype returned by IndexedArrayStruct.numpy_dtype() and the
value returned by s.numpy_value() to verify they interoperate correctly: call
numpy.array(value, dtype=dtype) (or create a zero-length structured array and
set the field) and then assert the resulting array has the expected shape and
contents (e.g., len == 1 and the field "iarr" yields the same data as
s.numpy_value()); update assertions to validate that the constructed structured
array round-trips rather than only checking dtype["names"] and len(value).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@warp/_src/codegen.py`:
- Around line 422-433: set_indexedarray_value is missing the grad-buffer
keepalive that set_array_value uses; mirror the same logic from set_array_value:
after writing the ctype payload (the setattr(inst._ctype, field, ...)) store or
clear the strong reference to the source grad buffer in the instance's
grad-keepalive storage (the same attribute set_array_value uses, e.g.
inst._grad_keepalive[field] = value._grad_keepalive or similar) when value is
not None, and remove/clear that entry when value is None, then leave the
cls.__setattr__(inst, field, value) call as-is.

---

Duplicate comments:
In `@warp/_src/codegen.py`:
- Around line 320-324: The code calls value.to(device) unconditionally for
fields matched by matches_array_class(var.type, indexedarray), but fields may be
None; update the block in codegen.py (the branch handling indexedarray) to first
check if value is None and if so set setattr(dst, name, None) and skip the .to()
call, otherwise perform cloned = value.to(device) and set setattr(dst, name,
cloned if isinstance(cloned, indexedarray) else indexedarray(cloned)); this
prevents StructInstance.to() from raising when indexedarray fields are None.

---

Nitpick comments:
In `@warp/tests/test_indexedarray.py`:
- Around line 154-160: The test currently only checks dtype names and wrapper
shape; instead materialize a structured NumPy array using the dtype returned by
IndexedArrayStruct.numpy_dtype() and the value returned by s.numpy_value() to
verify they interoperate correctly: call numpy.array(value, dtype=dtype) (or
create a zero-length structured array and set the field) and then assert the
resulting array has the expected shape and contents (e.g., len == 1 and the
field "iarr" yields the same data as s.numpy_value()); update assertions to
validate that the constructed structured array round-trips rather than only
checking dtype["names"] and len(value).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 2cbe8274-d019-4e67-9fe0-e61a048ab4d4

📥 Commits

Reviewing files that changed from the base of the PR and between 5cf0b56 and 02d0e90.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • warp/_src/codegen.py
  • warp/_src/types.py
  • warp/native/array.h
  • warp/tests/test_indexedarray.py
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • warp/native/array.h
  • warp/_src/types.py

@FabienPean-Virtonomy FabienPean-Virtonomy force-pushed the FabienPean/indexedarray_in_struct branch from 02d0e90 to 70361ac Compare April 7, 2026 15:07
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
warp/_src/codegen.py (2)

342-361: ⚠️ Potential issue | 🟠 Major

Clear indexedarray grad keepalive when assigning None

Lines 352-359 clear keepalive only for non-None assignments. If the field previously held a grad-enabled value, assigning None leaves a stale reference on the instance.

💡 Proposed fix
     def set_indexedarray_value(inst, value):
+        grad_attr = "_" + field + "_grad"
         if value is None:
             setattr(inst._ctype, field, var_type.__ctype__())
+            cls.__setattr__(inst, grad_attr, None)
         else:
             assert isinstance(value, indexedarray)
             assert types_equal(value.dtype, var_type.dtype), (
                 f"assign to struct member variable {field} failed, expected type {type_repr(var_type.dtype)}, got type {type_repr(value.dtype)}"
             )
             setattr(inst._ctype, field, value.__ctype__())
 
             # workaround to prevent gradient buffers being garbage collected
             # (indexedarray_t embeds an array_t)
-            grad_attr = "_" + field + "_grad"
             if value.data is not None and value.data.requires_grad:
                 cls.__setattr__(inst, grad_attr, value.data.grad)
             else:
                 # clear any previous keepalive
                 cls.__setattr__(inst, grad_attr, None)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/codegen.py` around lines 342 - 361, set_indexedarray_value
currently only clears the grad keepalive when assigning a non-None value; if
value is None a stale grad reference can remain — modify the None branch inside
set_indexedarray_value to compute grad_attr = "_" + field + "_grad" and call
cls.__setattr__(inst, grad_attr, None) (mirroring the else branch) so any
previous keepalive is cleared when assigning None; ensure you still set the C
struct field and then call cls.__setattr__(inst, field, value).

240-244: ⚠️ Potential issue | 🔴 Critical

Guard nullable indexedarray fields in StructInstance.to()

Line 243 unconditionally calls value.to(device). Since the setter allows None, this path can raise at runtime for nullable fields.

💡 Proposed fix
             elif matches_array_class(var.type, indexedarray):
                 # indexedarray_t
                 # `.to` returns an array if on different device, force to identity indexedarray
-                cloned = value.to(device)
-                setattr(dst, name, cloned if isinstance(cloned, indexedarray) else indexedarray(cloned))
+                if value is None:
+                    setattr(dst, name, None)
+                else:
+                    cloned = value.to(device)
+                    setattr(dst, name, cloned if isinstance(cloned, indexedarray) else indexedarray(cloned))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/codegen.py` around lines 240 - 244, In StructInstance.to(), the
branch handling indexedarray fields (uses matches_array_class and indexedarray)
calls value.to(device) unconditionally; guard against nullable fields by
checking if value is None before calling value.to(device) and set dst.{name} to
None in that case, otherwise perform the existing cloned = value.to(device) and
assign cloned if it's an indexedarray else indexedarray(cloned); update the
logic around variables value, cloned, dst and name in the
matches_array_class/indexedarray branch of StructInstance.to().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@warp/_src/codegen.py`:
- Around line 342-361: set_indexedarray_value currently only clears the grad
keepalive when assigning a non-None value; if value is None a stale grad
reference can remain — modify the None branch inside set_indexedarray_value to
compute grad_attr = "_" + field + "_grad" and call cls.__setattr__(inst,
grad_attr, None) (mirroring the else branch) so any previous keepalive is
cleared when assigning None; ensure you still set the C struct field and then
call cls.__setattr__(inst, field, value).
- Around line 240-244: In StructInstance.to(), the branch handling indexedarray
fields (uses matches_array_class and indexedarray) calls value.to(device)
unconditionally; guard against nullable fields by checking if value is None
before calling value.to(device) and set dst.{name} to None in that case,
otherwise perform the existing cloned = value.to(device) and assign cloned if
it's an indexedarray else indexedarray(cloned); update the logic around
variables value, cloned, dst and name in the matches_array_class/indexedarray
branch of StructInstance.to().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 17590270-8692-4e53-8002-0355d866d666

📥 Commits

Reviewing files that changed from the base of the PR and between 02d0e90 and 70361ac.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • warp/_src/codegen.py
  • warp/_src/types.py
  • warp/native/array.h
  • warp/tests/test_indexedarray.py
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • warp/native/array.h
  • warp/_src/types.py

Signed-off-by: Fabien Péan <pean@virtonomy.io>
@FabienPean-Virtonomy FabienPean-Virtonomy force-pushed the FabienPean/indexedarray_in_struct branch from 70361ac to 974f270 Compare April 7, 2026 16:11
@shi-eric
Copy link
Copy Markdown
Contributor

shi-eric commented Apr 7, 2026

@FabienPean-Virtonomy: @nvlukasz is on travel this week so to set your expectations, I wouldn't expect a re-review until next week. Thanks for your patience.

@FabienPean-Virtonomy
Copy link
Copy Markdown
Author

Thanks for the heads-up ! The PR is stabilized, and will be awaiting for further feedback.

I went on enabling indexedarray to be transferred to different device via struct, by wrapping the resulting array into a default passthrough indexedarray (Option 1 in comment above #1328 (comment))

About the remaining comment from coderabbits:

Guard nullable indexedarray fields in StructInstance.to()

It was not done for array just above so I don't think it is applicable.

Clear indexedarray grad keepalive when assigning None

Not familiar enough with the AD system about the need for this section or requirement to clear the attributes (as it seems not done for array above):
https://github.com/NVIDIA/warp/pull/1328/changes#diff-4509963cccfb2a792b7af2b720eeec5e8a3a87991a12e605522b73fbd752c2f1R352-R359

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support indexedarray fields in struct

3 participants