Skip to content

[DataOriented] Fix ndarrays on data oriented (reland of #704)#723

Merged
hughperkins merged 7 commits into
mainfrom
hp/704-min
Jun 5, 2026
Merged

[DataOriented] Fix ndarrays on data oriented (reland of #704)#723
hughperkins merged 7 commits into
mainfrom
hp/704-min

Conversation

@hughperkins

@hughperkins hughperkins commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Relands #704 (reverted in #719 due to Genesis RecursionError + MatrixField crashes) with only the minimum fixes needed to make Genesis unit tests and benchmarks green. Built directly on top of the original #704 squash commit; no unrelated changes.

What's in the branch

Doc changes compared to #704

one stale claim in compound_types.md → "Restrictions" section. The original #704 said the ndarray-path cache is per class and shipped a documented caveat about polymorphic-instance silent-stale-kernel reuse. My per-instance cache fix (commit fbfa65d) makes it per instance and removes that caveat. The bullet now correctly describes per-instance caching and reformulates the remaining edge cases (delete-on-instance, add-and-reassign-after-first-launch) in per-instance terms.

Details

Four commits on top of main:

  1. Revert of the revert — restores the original [DataOriented] Fix ndarrays on data oriented #704 code verbatim.
  2. Walker robustness — cycle-safe _build_struct_nd_paths / _walk_obj (Genesis attribute graphs have back-pointers) and MRO-safe is_data_oriented / is_dataclass_instance (Pydantic's ModelMetaclass.__getattr__ recursed infinitely on probe-style getattr of RigidOptions instances).
  3. Per-instance ndarray-path cache (Codex [Build] Linux x86 runner #3 follow-up from the original PR review) — fixes the 'MatrixField' object has no attribute 'element_type' crash on ~60 Genesis ndarray-backend tests. The cache is stored off-instance in a weakref.finalize-cleaned dict so it isn't visible to the fastcache args walker (which rejects list children).
  4. Perf optTemplateMapper.lookup iterates only template_slot_locations and uses a per-class precomputed dispatch (_arg_nd_paths_or_none), recovering the ~15% FPS that [DataOriented] Fix ndarrays on data oriented #704's unconditional per-call walk cost on small-step CPU benches.

Verification

Cluster, 8-GPU rtx-mid, container genesis-v1_22.sqsh, Genesis main, pytest -v -ra --backend gpu --dev --forked ./tests:

  • GS_ENABLE_NDARRAY=1: 742 passed, 0 failed, 4 skipped, 6 xfailed
  • GS_ENABLE_NDARRAY=0: 743 passed, 0 failed, 2 skipped, 7 xfailed

Genesis speed benchmarks (bench_cluster_wandb.py, branch vs main from WandB CI):

backend CPU bs=0 mean GPU mean worst
field +0.1% -2% anymal_uniform GPU -5.3%
ndarray -2% -1.5% shadow_hand_cubes_sparse CPU -5.1%

All within typical noise margin. No regression worse than -5.3%.

Supersedes #721.

Made with Cursor

…_oriented

PR #704 introduced two ndarray-walkers (the hotpath ``_build_struct_nd_paths``
in ``_template_mapper_hotpath.py`` and the compile-time ``_walk_obj`` in
``function_def_transformer.py``) which together caused Genesis unit tests to
``RecursionError`` and crash on first kernel launch. Two independent bugs:

1. **No cycle protection.** Real Genesis containers form attribute graphs with
   back-pointers (e.g. ``sim.rigid_solver.sim is sim``). Both walkers recursed
   infinitely on the back-edge. Fixed by threading an ``id(obj)``-keyed seen-set
   through each walk.

2. **Pydantic metaclass blew the stack.** Both walkers used
   ``dataclasses.is_dataclass(child) and not isinstance(child, type)`` (which
   calls ``hasattr(type(obj), '__dataclass_fields__')``) and
   ``is_data_oriented`` (which called
   ``getattr(type(obj), '_data_oriented', False)``). Pydantic's
   ``ModelMetaclass.__getattr__`` recurses infinitely on arbitrary missing
   attribute names, so probing a ``RigidOptions`` instance for either flag blew
   the stack. Fixed by adding ``is_dataclass_instance`` in ``util.py`` and
   rewriting ``is_data_oriented`` to walk ``type(obj).__mro__`` and probe each
   class's ``__dict__`` directly, bypassing any descriptor / ``__getattr__``
   machinery. Both walkers now route through these MRO-safe helpers.
The original #704 cached ndarray-attribute paths per CLASS, which is incorrect
under polymorphic instance structure: ``@qd.data_oriented`` and ``qd.Tensor``
members can have different leaf-types across instances of the same class —
Genesis ``DataManager`` only allocates ``*_adjoint_cache`` ndarrays when
``requires_grad=True``, and a ``qd.Tensor`` field can wrap an ``Ndarray`` on one
instance and a ``MatrixField`` on another (e.g. ndarray-vs-field backend).
``_collect_struct_nd_descriptors`` then crashed reading ``element_type`` /
``shape`` off the ``MatrixField`` (``'MatrixField' object has no attribute
'element_type'``), affecting ~60 Genesis unit tests under the ndarray backend.

Fix: cache the path list on the *instance* via ``object.__setattr__`` (so
frozen dataclasses work too); ``__slots__`` classes without a ``__dict__``
silently fall back to the class-level cache (Genesis containers don't use
slots). Also add defensive ``isinstance(v, Ndarray)`` and
``shape is None`` guards in ``_collect_struct_nd_descriptors`` for the case
where a leaf changes type *within* an instance's lifetime.
…-class

The args_hash data_oriented walker that #704 added ran unconditionally for every
arg of every kernel call. Even with the per-class attribute-path cache, the per-
call ``is_data_oriented(arg)`` + ``type(arg).__dict__.get`` chain still cost
~15% FPS on small-step CPU benches (anymal_zero CPU bs=0: -17.6% vs the pre-#704
reference) and several other Genesis CPU/GPU workloads were also significantly
regressed (anymal_uniform GPU -11.5%, franka_random CPU -17.5%, …).

Two coordinated optimisations:

1. Only iterate ``self.template_slot_locations`` instead of all args. A
   data_oriented class is never a dataclass and typed-dataclass args carry a
   specific dataclass type by construction, so the only positions where a
   data_oriented container can appear are the ``qd.template()`` annotated ones —
   already tracked by the kernel decorator. Genesis main ``kernel_step_1`` has 4
   template positions of 16 args; reduces the per-call work proportionally.

2. Per-``type(arg)`` precomputed dispatch: ``_arg_nd_paths_or_none`` maps each
   seen type to either the cached path list to walk, or ``None`` (skip — covers
   primitive templates, non-data_oriented composites, and data_oriented with
   zero ndarrays). One ``dict.get`` per candidate per call after warmup,
   replacing the previous chain.

Brings the post-#704 Genesis bench numbers from ~ -8 to -18% on CPU and ~ -10%
on GPU anymal_uniform back to within noise margin (target: matching pre-#704).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 42a31e213f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

ann = arg_meta.annotation
if ann is not template and type(ann) is not template:
continue
if dataclasses.is_dataclass(val) and not isinstance(val, type) and not is_data_oriented(val):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid unsafe dataclass probes in template warning

For a qd.template() argument whose class uses a metaclass with a problematic __getattr__ (the same Genesis/Pydantic case this commit fixes elsewhere), this warning path calls dataclasses.is_dataclass(val) before the safe is_data_oriented check, so the first compile can still recurse or crash instead of reaching the new MRO-safe helpers. This should use the new is_dataclass_instance() helper here as well, otherwise non-dataclass template objects can reintroduce the RecursionError on first materialization.

Useful? React with 👍 / 👎.

Comment on lines +123 to +124
_arg_nd_paths_or_none[cls] = paths
if paths is None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not cache ndarray-path absence by class

Caching None for the whole class means the first observed @qd.data_oriented instance with no ndarray members disables the ndarray-id walk for every later instance of that type. If a later same-type instance has ndarray members and then reassigns one to a different dtype/ndim, args_hash remains just id(arg) and lookup() can return the stale spec key without calling extract(), reusing a kernel specialized for the old ndarray. This conflicts with the per-instance polymorphism handled in _struct_nd_paths_for; the skip decision needs to be per instance or rechecked when paths may appear.

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown

The fix in fbfa65d ([Fix] Per-instance ndarray-path cache from PR #704)
changes the granularity of the template-mapper's ndarray-attribute-path cache
from per-class to per-instance. Update the "Restrictions" bullet under the
``@qd.data_oriented`` section to reflect the new behaviour:

- Per-instance caching: two instances of the same class can carry different
  ndarray attribute sets (e.g. ``DataManager`` with vs without
  ``requires_grad`` and its optional ``*_adjoint_cache`` members).
- The polymorphic-instance / stale-kernel caveat that the original #704
  shipped no longer applies — the per-instance cache disambiguates them.
- The remaining edge cases (deleting an attribute on an instance after its
  first launch; adding then reassigning a new attribute on an instance
  post-first-launch) are now phrased per-instance rather than per-class.
@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown

@hughperkins

hughperkins commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator Author

Genesis unit testes result:

Screenshot 2026-06-05 at 17 04 05

@hughperkins

Copy link
Copy Markdown
Collaborator Author

Gensis benchmarks result:

20260605_704b

…on test

Three CI complaints on #723:

1. **pyright**: ``_template_mapper.py:125`` ``"object" is not iterable``. The
   ``_arg_nd_paths_or_none.get(cls, _UNCLASSIFIED)`` sentinel pattern widened
   ``paths`` to ``list[tuple] | None | object``, and the ``if paths is None``
   narrowing didn't drop the ``object`` branch. Replaced with a ``try / except
   KeyError`` lookup so the cached value is unambiguously ``list[tuple] | None``.

2. **Check line wrapping**: prose lines wrapped well below the project's 120c
   target in ``test_ad_dataclass.py`` (lines 8, 10, 11, 226-227, 266-268),
   ``test_data_oriented_mixed_combos.py`` (lines 3-4),
   ``test_data_oriented_ndarray.py`` (lines 510-511, 536-537, 721-722), and
   ``test_py_dataclass.py`` (lines 2884-2887). Reflowed to ~120c per the
   ``find_underwrapped`` skill.

3. **Check test coverage for changes**: no regression test exercised the
   ``_seen`` cycle-detection guard in ``_build_struct_nd_paths`` /
   ``_walk_obj``. Added ``test_data_oriented_attribute_cycle_does_not_recurse
   _infinitely`` covering the canonical ``sim.solver.sim is sim`` shape.

The fourth failure (arm64 wheel test ``test_no_premature_shutdown[none]``) is
unrelated to this PR — it tests pytest's xdist worker retirement and is flaky
on arm64.
@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown

@duburcqa

duburcqa commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

ok to merge

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown

@hughperkins
hughperkins merged commit 821147a into main Jun 5, 2026
56 checks passed
@hughperkins
hughperkins deleted the hp/704-min branch June 5, 2026 23:54
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.

2 participants