Skip to content

[DataOriented] Fix ndarrays on data oriented#704

Open
hughperkins wants to merge 66 commits into
mainfrom
hp/data-oriented-ndarray-fix
Open

[DataOriented] Fix ndarrays on data oriented#704
hughperkins wants to merge 66 commits into
mainfrom
hp/data-oriented-ndarray-fix

Conversation

@hughperkins
Copy link
Copy Markdown
Collaborator

Issue: #

Brief Summary

copilot:summary

Walkthrough

copilot:walkthrough

…pre-declaring struct ndarrays

``_predeclare_struct_ndarrays._walk_obj`` only recursed into ``dataclasses.is_dataclass`` children of
a dataclass root; for non-dataclass roots (the ``@qd.data_oriented`` case) it didn't recurse at all.
That meant an ndarray held by a nested ``@qd.data_oriented`` (or a ``dataclasses.dataclass`` reached
through a ``@qd.data_oriented`` attribute, or vice versa) was never registered as a kernel arg, and
``state.inner.x[i] = ...`` raised ``QuadrantsCompilationError`` with "Ndarray ... used in kernel
scope but not registered as a kernel parameter".

Extend both branches to recurse on either a dataclass instance or an ``is_data_oriented(child)``
value. Pure superset of the prior walk — same shape, just more permissive on which children to
descend into.

Bug pinned by ``tests/python/test_data_oriented_ndarray.py::test_data_oriented_nested`` and the new
nesting / cross-container tests in the same file.
…rs, not just non-frozen dataclasses

``launch_kernel`` folds the live id(s) of struct-held ndarrays into ``args_hash`` only when the host
container is "mutable", and used ``type(args[idx]).__hash__ is None`` as the predicate. Python sets
``__hash__ = None`` for non-frozen dataclasses (the common ``eq=True, frozen=False`` default), so
that arm fires correctly for them. But ``@qd.data_oriented`` classes inherit ``object.__hash__``,
which is never ``None``, so the guard missed them entirely. Consequence: reassigning ``state.x =
other_ndarray`` on the same data_oriented instance left ``args_hash`` unchanged, hit the
launch-context cache, and re-launched the kernel against the stale ndarray binding (the old ``x1``).

Extend the predicate with an explicit ``is_data_oriented(args[idx])`` arm. The launch-context cache
is a perf optimisation so widening its invalidation predicate is safe.

Bug pinned by ``tests/python/test_data_oriented_ndarray.py::test_data_oriented_ndarray_reassign_same_shape``
and ``::test_data_oriented_nested_ndarray_reassign``.
…esting, deep nesting, mutation through chain, multi-kernel, sub-func

Adds tests 12-17 to the file added in 06b7c6a:
- data_oriented holding (frozen) dataclass that holds ndarray
- dataclass holding data_oriented that holds ndarray (kernel-arg via qd.template())
- 3-level data_oriented nesting
- mutation through 2-level chain (outer.inner.x reassign)
- two kernels sharing the same data_oriented instance
- ndarray access via @qd.func sub-call

The dataclass-of-data_oriented case uses qd.template() rather than typed dataclass kernel arg
because the typed-dataclass-arg form goes through ``_transform_kernel_arg`` which does not currently
recurse on data_oriented field types — tracked as a separate follow-up.

Also tightens the xfail reason on test_data_oriented_ndarray_reassign_different_dtype to call out
that the remaining failure is the template-mapper spec-key gap, not the launch-cache gap (latter
fixed by the kernel.py change in this PR).
Update compound_types.md to reflect what landed in #561 [Type] Tensor 24 (which added
``_predeclare_struct_ndarrays``) and what's fixed in this PR (the nested + mutation cases). The
old "no" cell predated the Tensor 24 infrastructure by ~6 weeks and was already inconsistent with
the in-tree error message in ``python/quadrants/lang/impl.py`` which lists "@qd.data_oriented /
frozen-dataclass template" as the supported route for ndarrays inside structs.

Add an ndarray-member example under the @qd.data_oriented section.
@hughperkins hughperkins marked this pull request as draft May 16, 2026 15:29
Copy link
Copy Markdown

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

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: 9bdeca5439

ℹ️ 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".

(idx, chain) for _, idx, chain in struct_nd_info if type(args[idx]).__hash__ is None
(idx, chain)
for _, idx, chain in struct_nd_info
if type(args[idx]).__hash__ is None or is_data_oriented(args[idx])
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 Include mutable nested containers in the cache guard

When a frozen dataclass template contains a mutable @qd.data_oriented child, this PR now discovers ndarray chains such as ('inner', 'x'), but this predicate only examines the top-level argument. In that case type(args[idx]).__hash__ is None is false and is_data_oriented(args[idx]) is also false, so reassigning outer.inner.x between launches keeps the same args_hash and the cached launch context continues to bind the old ndarray. The guard needs to account for mutability along the recorded attr_chain, not just the root argument.

Useful? React with 👍 / 👎.


Mixing `qd.field` and `qd.ndarray` members in the same class is also supported. Nested `@qd.data_oriented` (or nested `dataclasses.dataclass`) containers with ndarrays inside are walked recursively.

Note: as with `dataclasses.dataclass`, reassigning an ndarray member between kernel calls (`state.x = other_ndarray`) is allowed; the kernel re-binds against the live value on the next launch.
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 Qualify ndarray rebinds by compatible specialization

This note overstates the new behavior for @qd.data_oriented: rebinding the same instance to an ndarray with a different dtype or dimensionality does not change the template specialization key, because data-oriented template args are keyed by object identity rather than their ndarray children. The commit's own xfailed test documents that this can silently reuse the old compiled kernel, so public docs should either restrict rebinds to compatible ndarray type/ndim/layout or the implementation should re-specialize before advertising arbitrary other_ndarray rebinds as supported.

Useful? React with 👍 / 👎.

…rray members

``_extract_arg`` returned ``weakref.ref(arg)`` for any ``is_data_oriented(arg)``, which over-shared
the compiled kernel when ``state.x`` was reassigned to an ndarray of a different dtype or ndim on
the same instance — the second launch re-used the kernel specialised for the original shape and
silently corrupted the new-shape buffer.

Walk the reachable ``Ndarray`` members (recursively through nested data_oriented and dataclass
children) and prepend their ``(path, element_type, ndim, needs_grad, layout)`` descriptors to the
spec key. Same memory-leak avoidance — the descriptors are values, no strong reference to the
ndarray itself, and the weakref to the container is preserved for the per-instance identity tail.

Containers with *no* ndarrays (the genesis field-backend ``@qd.data_oriented`` workload) take the
existing short path unchanged — ``_collect_struct_nd_descriptors`` returns an empty list and we
return ``weakref.ref(arg)`` as before. So this is a no-op for the existing hot path, and the
overhead is paid only by containers that actually hold ndarrays.

Pinned by ``test_data_oriented_ndarray_reassign_different_dtype`` (was xfail, now passes),
``::reassign_different_ndim``, ``::nested_ndarray_reassign_different_dtype``, and
``::field_only_no_speckey_change`` (no-regression case).
- Unmark test_data_oriented_ndarray_reassign_different_dtype as xfail (passes now).
- Add ::reassign_different_ndim to cover the 1D->2D shape change case.
- Add ::nested_ndarray_reassign_different_dtype to confirm the recursive walker reaches a leaf
  ndarray through a nested @qd.data_oriented chain.
- Add ::field_only_no_speckey_change to pin the no-regression case (data_oriented with only field
  members still uses the original weakref short-path).
…y member is reassigned

The spec-key fix in dc7997b (``_extract_arg`` descends into ``is_data_oriented(arg)`` to emit
ndarray shape descriptors) was being silently bypassed for the same-instance case: ``TemplateMapper.
lookup`` has a fast-path ``_mapping_cache_tracker`` keyed only on ``tuple(id(arg) for arg in args)``,
which short-circuits ``extract()`` whenever the same instance is passed again. So
``run(state)``-then-``state.x = other``-then-``run(state)`` re-used the cached spec key from the
first call and the kernel kept its original compile-time dtype/ndim.

Fold the ids of all ndarrays reachable through any ``is_data_oriented(arg)`` (recursively, via
nested data_oriented and dataclass children) into ``args_hash``. Reassigning a member ndarray
changes its id, which changes the hash, which forces ``extract()`` and (when warranted) a fresh
compilation. No-op for data_oriented containers with no ndarrays.

Mirror at this cache layer of the launch-context stale-guard fix from 97afa6d.

Pinned by ``test_data_oriented_ndarray_reassign_different_dtype`` — was failing under just the
``_extract_arg`` change because of this cache layer; now passes.
…lass kernel arg

Annotating a kernel arg as a dataclass whose field type is a ``@qd.data_oriented`` class mixes two
incompatible kernel-arg patterns:

  - Typed-dataclass args are flattened into per-leaf kernel args using the field type annotations at
    compile time (``_transform_kernel_arg`` recurses on ``field.type``).
  - ``@qd.data_oriented`` containers don't carry per-attribute type annotations — their ndarray and
    field members are walked at kernel-compile time from the *value* (``vars(self)``) via
    ``_predeclare_struct_ndarrays``, which only fires for ``qd.template()`` / ``qd.Tensor`` outer
    annotations.

Before this commit, the data_oriented field type fell through ``_transform_kernel_arg``'s else
branch and bubbled up a confusing ``Invalid data type`` error from ``cook_dtype``. Now we raise a
``QuadrantsSyntaxError`` naming the offending field and pointing users at the recommended fix
(``s: qd.template()``).

Pinned by ``test_typed_dataclass_with_data_oriented_field_raises_clear_error``.
…ap A

The unconditional ``vars(arg).items()`` recursion that the Gap A fix added to both ``_extract_arg``
and ``TemplateMapper.lookup`` was paid once per kernel call per data_oriented arg. For the genesis
field-backend, where the ``@qd.data_oriented`` Solver is passed as ``self`` to every kernel and
holds dozens of attributes, this cost ~150 FPS/env on anymal_c (B=4096) — measured ~14% regression
in paired runs.

Cache the attribute paths to ndarrays per class (``type(arg) -> list[tuple[str, ...]]``). First
call for a class walks once via ``_build_struct_nd_paths``; subsequent calls do a dict lookup +
``getattr`` chains for the (typically zero or one or two) cached paths. For solvers with no ndarray
members (genesis field backend), the cached list is empty and the per-call cost collapses to a
single dict lookup.

Trades freshness for speed: assumes the *set* of ndarray-holding attribute paths is stable across
instances of the same class. Genesis Solver and similar data_oriented containers declare members
in ``__init__`` and don't add new ones later, so this is safe. Documented in the docstring for
``_struct_nd_paths_for``.

Shared between ``_template_mapper.py`` (id collection for args_hash) and
``_template_mapper_hotpath.py`` (shape descriptors for spec key) — same paths, different payload.
Documents what combinations of `dataclasses.dataclass`, `@qd.data_oriented`, `@qd.struct`,
`qd.ndarray`, and `qd.field` work as nested members, after the data_oriented + ndarray fix series.

Three additions:

1. Per-container × per-member-type matrix replacing the previous text-only claim that
   ``@qd.data_oriented`` could not contain ndarrays.

2. Outer kernel-arg annotation rules: when to use ``qd.template()`` vs a typed-dataclass
   annotation, including the ``frozen=True`` requirement for a dataclass passed via
   ``qd.template()`` and the rejection of ``@qd.data_oriented`` field types inside a typed-dataclass
   kernel arg (matches the error from c9598ad).

3. Reassignment + restrictions: documents that ndarray reassignment with different dtype/ndim is
   supported (Gap A), and that the ndarray-bearing attribute set on a data_oriented class is
   assumed stable across instances (path-cache caveat from 93893e5).

Plus three spot tests in ``test_data_oriented_mixed_combos.py`` that empirically pin the more
involved matrix claims:

- ``test_data_oriented_with_ndarray_field_and_nested_data_oriented``: single data_oriented holding
  ndarray + field + nested data_oriented + primitive simultaneously.
- ``test_dataclass_with_data_oriented_via_template``: frozen dataclass holding a data_oriented
  holding an ndarray, passed via ``qd.template()``.
- ``test_data_oriented_with_dataclass_and_ndarray_sibling``: data_oriented holding both a direct
  ndarray AND a dataclass-with-ndarray sibling.

All three pass on cluster.
@github-actions
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown

``@qd.struct`` does not exist as an exported symbol — ``dir(qd)`` has only ``Struct``,
``StructField``, and ``dataclass``. The original doc claimed ``@qd.struct`` / ``@qd.dataclass``
as a legacy decorator pair, but only ``@qd.dataclass`` exists. The function-form equivalent
``qd.types.struct(name1=type1, ...)`` produces the same ``StructType``.

Replace all ``@qd.struct`` references with ``@qd.dataclass`` (with a parenthetical note pointing
to the function-form factory ``qd.types.struct``). No semantic change — the row's "field-only,
no ndarrays" classification was already correct; only the name was wrong.
Belt-and-braces tests for the case the user explicitly requires: fastcache should work when a
@qd.data_oriented contains ndarrays (with or without primitives or nested data_oriented
children), and should *correctly fall back* (not error, not silently miscompile) when the
container holds a qd.field.

Pattern adapted from ``test_cache.test_fastcache``: call ``qd_init_same_arch`` twice with the
same ``offline_cache_file_path`` to simulate two processes. Monkeypatch ``launch_kernel`` to
capture ``compiled_kernel_data`` per call: ``None`` on the cold init (compile) and a non-None
``CompiledKernelData`` on the warm init (loaded from disk fastcache).

New tests:

- ``test_data_oriented_ndarray_fastcache_cross_init`` — single ndarray member, second init loads
  from disk.
- ``test_data_oriented_nested_ndarray_fastcache_cross_init`` — nested @qd.data_oriented + ndarray
  member, second init loads from disk. Exercises the args_hasher recursion.
- ``test_data_oriented_ndarray_fastcache_dtype_key_distinct`` — two different ndarray dtypes on
  the same data_oriented produce two distinct cache entries; both load from disk on warm init.
  Pins the ``[nd-{dtype}-{ndim}]`` repr in args_hasher.
- ``test_data_oriented_field_disables_fastcache_but_runs`` — data_oriented + qd.field documented
  fallback: ``cache_key_generated`` is False, but the kernel still runs correctly.

The pre-existing ``test_data_oriented_ndarray_fastcache_eligible`` (kept) checks the in-process
``cache_key_generated`` flag; these four add cross-init disk-cache verification.
…otguns

The existing fastcache.md mentions @qd.data_oriented in the constraint table and in a one-line
note next to the dataclass section, but doesn't give a worked example or spell out the
behavioural semantics. This commit adds a focused subsection covering:

- A worked Simulation example: __init__ allocates state once, @qd.kernel(fastcache=True) method
  consumes it via self.
- Primitive members of @qd.data_oriented are *implicitly templated* — their values are folded
  into the fastcache key without needing add_value_to_cache_key or qd.static(...). This is the
  property that lets the cache differentiate between Simulation(n=8) and Simulation(n=64).
- Tensor contents vs reassignment: a per-operation table showing which mutations share the cache
  entry (element writes, same-dtype/ndim reassignment) and which produce a new entry (dtype or
  ndim change).
- dataclasses.dataclass nesting works, but has the inverse default for primitives — types only,
  not values. Spell out the silent-miscompile risk if you put a qd.static-baked value in a
  dataclass field without FIELD_METADATA_CACHE_VALUE.
- What disables fastcache on a data_oriented arg: any qd.field child anywhere in the tree, with
  a pointer to the perso_hugh follow-up doc.

Also adds a short "Fastcache interaction" cross-reference in compound_types.md so a reader who
lands there is pointed at the fastcache subsection.

No code changes — purely user-facing documentation of behaviour that already exists on the
hp/data-oriented-ndarray-fix branch (data_oriented + ndarray + fastcache works end-to-end across
processes, verified in the investigation doc).
@hughperkins hughperkins force-pushed the hp/data-oriented-ndarray-fix branch from 3e1f89e to ee5fbbb Compare May 16, 2026 17:48
… for compound-type keying

- Main body now covers only: how to enable fastcache + the constraints
  for enabling it.
- Move all container-specific behaviour (data_oriented primitive value
  folding, dataclasses.dataclass FIELD_METADATA_CACHE_VALUE opt-in,
  qd.field disables fastcache) into a single tight
  "Advanced -> Compound-type cache keying" subsection.
- Drop @qd.data_oriented description from fastcache.md (lives in
  compound_types.md). Drop qd.static <-> fastcache conflation: the two
  mechanisms are orthogonal.
- compound_types.md retains a single cross-link to the new
  fastcache.md#compound-type-cache-keying anchor.
…uous bare 'field'

In fastcache.md and compound_types.md, several places used the bare word
'field' to mean 'attribute of a dataclasses.dataclass / @qd.data_oriented
container'. Because qd.field is itself a documented Quadrants type
(listed in the same parameter-types table that disables fastcache when
it appears), bare 'field' was ambiguous. Standardise on 'member' for
compound-type members. Keep:

- 'qd.field' / 'ScalarField' / 'MatrixField' / 'qd.dataclass' /
  'StructType' references unchanged (these are the Quadrants types).
- 'dataclasses.field(...)' unchanged (Python stdlib API).
- 'attribute' only where it means Python attribute-access syntax
  (`s.foo`) or the `src_ll_cache_observations` Python instance
  attribute.

Also clean up the purity-constraint closure-list example to drop
'fields' (it was unrelated to the qd.field/dataclass-field distinction
and was just listing examples of external state).
…ers ('baked into kernel')

Replace 'folded into the cache key' jargon (which was undefined and
ambiguous: ndarray dtype info is just keyed, whereas data_oriented
primitive children are also Template-style specialised). Mirror the
existing qd.Template row: primitive member values are 'baked into
kernel'. Use 'included in the cache key' for type-only contributions
(ndarray dtype/ndim/layout, dataclass member types).
…are delete + late-reassign-with-different-dtype

Previous wording said 'don't add new members after the first kernel
launch'. Empirical results show this is overly broad: adding new ndarray
attributes on later instances of the same class is safe (each instance
gets its own spec entry via per-instance weakref; the compile-time
walker registers all reachable ndarrays). The actual failure modes are:

  (a) Deleting an ndarray attribute that was present on the first
      launch -> AttributeError on the next launch (the cached path
      still does getattr on the missing attribute).

  (b) Reassigning a post-first-walk ndarray attribute (a member that
      wasn't on the first instance walked, was added later, and is now
      re-assigned) to one with a different dtype/ndim -> not detected
      by the id-augmented args_hash invalidation tracker; stale
      compiled kernel is silently reused -> bit-reinterpretation of
      the new storage.

Verified empirically via ~/ais/deskai9/tmp/check_path_cache_stability.py
on cluster (cases A/B safe; C errors; D safe via per-instance weakref;
E silent miscompile - f32 array reassigned over i32 displays the i32
bit pattern as ~4e-45).
…rnel name

test_data_oriented_ndarray_fastcache_cross_init was asserting on the LAST
launch_kernel call, but state.x.to_numpy() between run(state) and the
assertion launches an internal ndarray_to_ext_arr kernel that is
is_pure=False and so always has compiled_kernel_data=None. The assertion
captured the wrong launch and the test failed even though the actual
fastcache load for the user kernel worked correctly (verified via
src_ll_cache_observations.cache_loaded=True in a debug repro).

Filter the captured list to only the user kernel ('run'). Applied the
same filter to the other two cross-init fastcache tests (which happened
to pass because their assertions came before .to_numpy(), but the filter
makes the pattern robust against future test edits).
@hughperkins hughperkins marked this pull request as ready for review May 16, 2026 18:31
Copy link
Copy Markdown

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

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: 6667ba6383

ℹ️ 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".

Comment on lines +130 to +132
if type(v) in _TENSOR_WRAPPER_TYPES:
v = v._unwrap()
type_id = id(v.element_type)
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 Handle cached ndarray paths that now point at fields

If the first @qd.data_oriented instance of a class has x as a qd.Tensor/ndarray, _struct_nd_paths_cache records ('x',) for the whole class. A later instance of the same class with x as a field-backed qd.Tensor (or a field assigned at that path) will still walk that cached path, unwrap to a ScalarField, and then unconditionally read ndarray-only attributes like element_type/grad, raising before the kernel can run. This breaks backend-polymorphic data-oriented containers depending on which backend instance is seen first; revalidate that the resolved value is still an Ndarray before emitting a descriptor.

Useful? React with 👍 / 👎.

@github-actions
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown

…ctType capability)

Replaces the misleading 'SOA-style' comment. AOS is the default for
StructType-backed fields, but make it explicit with layout=qd.Layout.AOS
to highlight that an AOS-of-N-cells allocation is the capability that
distinguishes @qd.dataclass / qd.types.struct from the other compound
types (@qd.data_oriented and dataclasses.dataclass cannot be the
element type of a tensor).
…(verified empirically)

Old cell said 'no [*1]' with no footnote defined anywhere in the doc.
Empirically verified with ~/ais/deskai9/tmp/check_dataclass_diff.py
on cluster: both typed-dataclass kernel-arg annotation (def k(s: State))
and qd.template() annotation produce correct gradients matching the
non-dataclass baseline for kernels operating on qd.ndarray members.
…ensor members

Pins that gradients flow correctly when kernel arguments are wrapped in plain
Python dataclasses across the tensor types Quadrants exposes:

- qd.ndarray via typed-dataclass annotation + qd.template() path (kernel.grad()).
- qd.field via qd.template() path (qd.ad.Tape).
- qd.tensor(backend=NDARRAY) — same path as qd.ndarray.
- qd.tensor(backend=FIELD) — xfail (pre-existing TensorWrapper.__getitem__ not
  unwrapped through dataclass member access; forward kernels fail identically;
  unrelated to AD).
- Mixed dataclass holding both qd.ndarray and qd.field members; ndarray-side
  gradient verified via kernel.grad() while a field is written in the same
  kernel.

Backs the 'supports differentiation? yes' cell for dataclasses.dataclass in
the compound_types.md overview table (06d2e86).
…role

Replaces the prior "recommended / for kernel methods / legacy" framing with
objective one-liners: dataclasses.dataclass = lightweight container that can
hold ndarrays; @qd.data_oriented = self-style objects with @qd.kernel methods;
@qd.dataclass = embedded-in-kernel structures (no ndarrays).
…ble for consistency

The two other entries (@qd.data_oriented, @qd.dataclass) already use the @
decorator prefix; aligning dataclasses.dataclass with the same form.
…Tensor

Empirical follow-up to f7dd090: the failure was not a TensorWrapper-in-kernel
limitation, it was using `object` as the dataclass-member annotation. With
`qd.Tensor` (or `qd.template()`) member annotations, populate_global_vars_from_
dataclass + FlattenAttributeNameTransformer unwraps the wrapper and rewrites
s.a to the flat name bound to the underlying impl, so kernel-side s.a[i] hits
impl.subscript with a bare Field/Ndarray. The previous xfail was wrong; this
combination is fully supported. qd.ad.Tape(loss) requires the bare impl, so
unwrap loss at the Tape boundary (Tape's API contract is field/scalar).
One short paragraph per type describing the compile-time mechanism:

- dataclasses.dataclass: walked via dataclasses.fields, members flattened into
  kernel globals + AST rewritten; ndarrays registered as kernel params.
- @qd.data_oriented: walked via vars(self), no annotations needed; primitives
  baked into IR; per-class path cache keeps the walk cheap.
- @qd.dataclass / qd.types.struct: real StructType, members by value, can be
  element type of a field / tensor; @qd.func methods inlined.
…r level

Drop internal-implementation references (dataclasses.fields, vars(self), flat
names, AST rewrite, etc.). Just the high-level story per type:

- dataclasses.dataclass: Python-only; compiler flattens members into kernel
  params; container has no kernel-side representation; members read-only.
- @qd.data_oriented: same flatten-into-params story; no annotations needed;
  primitive members baked in as constants (one kernel per distinct value).
- @qd.dataclass: real kernel-side type with fixed memory layout; by-value
  storage; can be the element type of a tensor; explains the no-ndarrays
  rule (heap-allocated, dynamic shape).
Old table was all-yes across most columns and didn't help the reader pick
between the three types. New table has 5 rows that actually distinguish:

- kernel-side representation (flattened-away vs real type)
- tensor-element-type eligibility
- ndarray support
- @qd.kernel methods on self
- member declaration style

The per-member-type nesting matrix lower in the doc still covers the
detailed allowances.
@github-actions
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown

NamedTuples (decorated as ``@qd.data_oriented``) have no instance ``__dict__``,
so ``obj.__dict__.items()`` raises ``AttributeError: 'Geom' object has no
attribute '__dict__'``. Fall back to ``_asdict()`` first, mirroring the same
fallback already used in ``args_hasher.stringify_obj_type``'s data_oriented
branch.

Pins ``test_args_hasher_named_tuple`` (added in this branch).
@github-actions
Copy link
Copy Markdown

Two tests using default_fp=qd.f64 were missing the data64 extension
requirement, causing SPIR-V crashes on Vulkan/Metal backends.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown

Three module/function docstring paragraphs in the new test files were wrapped near 80-100c,
flagged by the AI-based wrap checker. Reflow at 120c to match the repo convention.
@github-actions
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown

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