-
Notifications
You must be signed in to change notification settings - Fork 2
updating dataclasses.replace and register_jax_tree #10
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
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 |
|---|---|---|
|
|
@@ -10,24 +10,57 @@ | |
|
|
||
|
|
||
| def _register_jax_tree(cls_: type[T]) -> type[T]: | ||
| """Registers a class as a JAX Pytree, safely preventing double-registration.""" | ||
| """Registers a class as a JAX Pytree, safely preventing double-registration. | ||
|
|
||
| Field classification: | ||
| - ``jax_static=True`` + ``init=True`` -> aux bucket 1 (restored via __init__) | ||
| - ``jax_static=False`` + ``init=True`` -> traced leaves (restored via __init__) | ||
| - any + ``init=False`` -> aux bucket 2 (restored via object.__setattr__) | ||
|
|
||
| Private (``init=False``) fields must travel in aux rather than as leaves so | ||
| that JAX round-trips (jit, vmap, grad, etc.) can restore them. They are | ||
| written back with ``object.__setattr__`` in ``unflatten``, which bypasses the | ||
| DataClass frozen guard safely because the object is still being initialised. | ||
| """ | ||
| # Guard: If already registered (e.g., by __init_subclass__), skip re-registering | ||
| if getattr(cls_, "_jax_tree_registered", False): | ||
| return cls_ | ||
|
|
||
| static_fields = [f.name for f in fields(cls_) if f.metadata.get("jax_static")] | ||
| dynamic_fields = [f.name for f in fields(cls_) if not f.metadata.get("jax_static")] | ||
| all_fields = fields(cls_) | ||
| # Traced leaves: init=True and not jax_static | ||
| dynamic_fields = [ | ||
| f.name for f in all_fields if not f.metadata.get("jax_static") and f.init | ||
| ] | ||
| # Aux bucket 1: explicitly static (jax_static=True) and init=True | ||
| static_init_fields = [ | ||
| f.name for f in all_fields if f.metadata.get("jax_static") and f.init | ||
| ] | ||
| # Aux bucket 2: private (init=False), regardless of jax_static — must ride in aux | ||
| private_fields_names = [f.name for f in all_fields if not f.init] | ||
|
|
||
| def flatten_with_keys(obj): | ||
| keyed_leaves = [ | ||
| (jax.tree_util.GetAttrKey(f), getattr(obj, f)) for f in dynamic_fields | ||
| ] | ||
| aux = tuple(getattr(obj, f) for f in static_fields) | ||
| aux = ( | ||
| tuple(getattr(obj, f) for f in static_init_fields), | ||
| tuple(getattr(obj, f) for f in private_fields_names), | ||
| ) | ||
| return keyed_leaves, aux | ||
|
|
||
| def unflatten(aux, leaves): | ||
| kwargs = {**dict(zip(static_fields, aux)), **dict(zip(dynamic_fields, leaves))} | ||
| return cls_(**kwargs) | ||
| static_init_values, private_values = aux | ||
| init_kwargs = { | ||
| **dict(zip(static_init_fields, static_init_values)), | ||
| **dict(zip(dynamic_fields, leaves)), | ||
| } | ||
| obj = cls_(**init_kwargs) | ||
| # Stamp private fields back after construction. object.__setattr__ bypasses | ||
| # DataClass.__setattr__'s frozen guard, which is safe here because we are | ||
| # restoring the exact values the object was flattened from. | ||
| for name, value in zip(private_fields_names, private_values): | ||
| object.__setattr__(obj, name, value) | ||
| return obj | ||
|
Comment on lines
+29
to
+63
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. 🧩 Analysis chain🌐 Web query:
💡 Result: Yes, in JAX custom pytree registration with register_pytree_with_keys (and similarly register_pytree_node), aux_data is treated as static tree metadata. It is stored in the treedef, must be hashable and immutable, and is used for structure reconstruction during unflatten. It does not participate in transformations like jit, vmap, or tree_map—those operate on the dynamic children/leaves. Array-valued data that must participate in jit (i.e., be traced and potentially compiled differently based on value), vmap (vectorized), or tree_map (mapped over) should be returned as children/leaves, not in aux_data. Including arrays in aux_data leads to unsupported, poorly-defined behavior, such as unexpected tracing under jit (e.g., jnp arrays may become Traced unexpectedly, while np arrays do not). Official JAX documentation examples explicitly state: "children must contain arrays & pytrees" and "aux_data must contain static, hashable data." StackOverflow answer citing JAX docs confirms: "aux_data in a pytree flattening must contain static elements... Neither np.ndarray nor jax.Array satisfy this, so they should not be included in aux_data." Citations:
🏁 Script executed: #!/bin/bash
# Find definitions of private_field and static_private_field
rg "def (private_field|static_private_field)" -A 5Repository: ymahlau/drinx Length of output: 668 🏁 Script executed: #!/bin/bash
# Search for uses of jax_static metadata in the fields
rg "jax_static" -B 2 -A 2 | head -100Repository: ymahlau/drinx Length of output: 4940 🏁 Script executed: #!/bin/bash
# Check the structure of fields and metadata in base.py
fd "base.py" | xargs cat -n | head -150Repository: ymahlau/drinx Length of output: 6448 Split private fields by
Keep 🧰 Tools🪛 Ruff (0.15.10)[warning] 54-54: Add explicit value for parameter (B905) [warning] 55-55: Add explicit value for parameter (B905) [warning] 61-61: Add explicit value for parameter (B905) 🤖 Prompt for AI Agents |
||
|
|
||
| jax.tree_util.register_pytree_with_keys(cls_, flatten_with_keys, unflatten) | ||
| cls_._jax_tree_registered = True # ty:ignore[unresolved-attribute] | ||
|
|
||
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.
_dataclass_replacecurrently both exposes and drops private fields.known_namesletsinit=Falsenames through, soupdated_copy(_cache=...)/aset("_cache", ...)now bypass the existing guard thattests/test_base.py:1302-1311andtests/test_base.py:1400-1410assert. At the same time, onlyf.name in changesgets stamped back, soupdated_copy(x=...)still resets any existing private state instead of preserving it.Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents