Skip to content

fix(torch): canonicalize nested dict key order in the torch tree implementation - #23201

Merged
hertschuh merged 9 commits into
keras-team:masterfrom
pctablet505:perf-P18-pytree-leaf
Sep 10, 2026
Merged

hertschuh merged 9 commits into
keras-team:masterfrom
pctablet505:perf-P18-pytree-leaf

Conversation

@pctablet505

@pctablet505 pctablet505 commented Jun 30, 2026 •

Copy link
Copy Markdown
Contributor

Closes #23281

Summary

In the torch tree implementation, _dict_to_ordered_dict only sorted the keys of the outermost dict. func(structure) returns a dict for any dict input, so the if ret is None: return traverse_children() branch below it was unreachable for dicts and the values were never visited.

Nested dicts therefore kept their insertion order, which makes two structurally identical inputs flatten differently:

>>> keras.tree.flatten({"b": {"d": 40, "c": 30}, "a": 10})
[10, 40, 30]
>>> keras.tree.flatten({"a": 10, "b": {"c": 30, "d": 40}})
[10, 30, 40]
>>> keras.tree.pack_sequence_as({"b": {"d": 40, "c": 30}, "a": 10}, [10, 30, 40])
{'a': 10, 'b': {'d': 30, 'c': 40}}     # 30 lands on 'd'

The last one is the damaging case: values are silently paired with the wrong keys.

_dict_to_ordered_dict now recurses, so key order is canonical at every level. traverse canonicalizes once at the root and walks the already-canonical structure, so the recursion is not repeated per level.

Cost

Sorting at every level is work master skips, so dict-heavy structures get slower. But the rewrite also stops round-tripping every structure through torch_tree.tree_flatten/tree_unflatten, and an internal node is now rebuilt only when a dict below it actually changed, so non-dict structures get faster. Measured against master, median of 9 rounds:

case master this PR
flatten list of 12 13.03 ยตs 3.67 ยตs 0.28x
map_structure nested 7.16 ยตs 7.25 ยตs 1.01x
pack_sequence_as nested 11.43 ยตs 13.58 ยตs 1.19x
flatten nested dict 4.48 ยตs 5.53 ยตs 1.23x
flatten flat dict of 16 3.50 ยตs 4.86 ยตs 1.39x
flatten leaf 0.121 ยตs 0.163 ยตs 1.35x

Tests

tree_test.py covers nested flatten order, pack_sequence_as round-tripping, and equality of the two structurally identical dicts above. The nested-order test fails on master and passes here.

Contributor Agreement

  • I am a human, and not a bot.
  • I will be responsible for responding to review comments in a timely manner.
  • I will work with the maintainers to push this PR forward until submission.

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces a short-circuit optimization in the PyTorch tree implementation's _dict_to_ordered_dict function to return leaves immediately, avoiding unnecessary round-trips. It also adds corresponding unit tests for various data structures. The feedback suggests extending the test suite to include a nested dictionary test case to ensure recursive, deterministic sorting is thoroughly verified.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread keras/src/tree/tree_test.py Resolved
@codecov-commenter

codecov-commenter commented Jun 30, 2026 •

Copy link
Copy Markdown

Codecov Report

โœ… All modified and coverable lines are covered by tests.
โœ… Project coverage is 84.15%. Comparing base (7c06497) to head (7c08128).
โš ๏ธ Report is 17 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #23201      +/-   ##
==========================================
- Coverage   86.88%   84.15%   -2.74%     
==========================================
  Files         477      478       +1     
  Lines       71493    77360    +5867     
  Branches    11825    13535    +1710     
==========================================
+ Hits        62120    65099    +2979     
- Misses       6239     9216    +2977     
+ Partials     3134     3045      -89     
Flag Coverage ฮ”
keras 83.96% <100.00%> (-2.74%) โฌ‡๏ธ
keras-cpu 84.14% <100.00%> (-1.91%) โฌ‡๏ธ
keras-gpu 70.06% <100.00%> (+<0.01%) โฌ†๏ธ
keras-jax 58.36% <0.00%> (-2.19%) โฌ‡๏ธ
keras-numpy 54.09% <0.00%> (-0.17%) โฌ‡๏ธ
keras-openvino 59.73% <0.00%> (-0.17%) โฌ‡๏ธ
keras-tensorflow 60.28% <0.00%> (-1.88%) โฌ‡๏ธ
keras-torch 60.24% <100.00%> (-1.51%) โฌ‡๏ธ
keras-tpu 57.53% <0.00%> (-0.04%) โฌ‡๏ธ
keras.applications ?
keras.applications-jax ?
keras.applications-numpy ?
keras.applications-openvino ?
keras.applications-tensorflow ?
keras.applications-torch ?
keras.wrappers ?
keras.wrappers-jax ?
keras.wrappers-numpy ?
keras.wrappers-openvino ?
keras.wrappers-tensorflow ?
keras.wrappers-torch ?

Flags with carried forward coverage won't be shown. Click here to find out more.

โ˜” View full report in Codecov by Harness.
๐Ÿ“ข Have feedback on the report? Share it here.

๐Ÿš€ New features to boost your workflow:
  • โ„๏ธ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@pctablet505

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request adds a short-circuit optimization to _dict_to_ordered_dict in torchtree_impl.py to return leaf structures immediately, avoiding an unnecessary round-trip. A comprehensive set of unit tests has been added in tree_test.py to validate this behavior across various structures such as tensors, lists, dicts, and nested structures. No review comments were provided, so there is no additional feedback.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

pctablet505 added a commit to pctablet505/keras that referenced this pull request Jul 20, 2026
โ€ฆile-fix fold

Reworked eager A/B charts for keras-team#23189 (P4+C1), keras-team#23201 (P18+C2), keras-team#23198
(P13+C3) post-fold, plus the combined-13-base cumulative C1/C1+C2/
C1+C2+C3 graph-break and torch.compile-speedup decomposition chart.
pctablet505 added a commit to pctablet505/keras that referenced this pull request Jul 20, 2026
pctablet505 added a commit to pctablet505/keras that referenced this pull request Jul 20, 2026
@github-actions

Copy link
Copy Markdown

This PR is stale because it has been open for 14 days with no activity. It will be closed if no further activity occurs. Thank you.

@github-actions github-actions Bot added the stale label Aug 19, 2026
test_dict_to_ordered_dict_leaf_shortcircuit previously only checked a
top-level dict and a nested dict-of-lists whose keys happened to already
be sorted, so it would not catch a sorting/reassociation regression at
a nested level. Add a case with a dict-of-dict where keys are out of
order at both the outer and inner level, flatten and repack it, and
verify each leaf lands back at its correct nested key, matching an
equivalent structure whose keys are already sorted at every level.
_dict_to_ordered_dict only sorted the outermost dict level: a dict
match short-circuited before the recursive traversal that would have
processed a nested dict value, so nested dicts kept insertion order
instead of the deterministic order other tree implementations
(optree, dmtree) provide. This produced different leaf orders for
structurally-equivalent nested dicts with different insertion order,
and pack_sequence_as could silently swap values when packing one
structure's leaves into a differently-ordered equivalent structure.
Values are now recursed into while building the sorted dict, so every
level is canonicalized, not just the top one.
The dict-canonicalization comment in _dict_to_ordered_dict had a
duplicated "that" across the line wrap and joined two clauses with a
bare hyphen instead of a full sentence. The leaf-shortcircuit test's
comment referenced an internal root-cause tracking id that isn't
meaningful outside that tracker.
โ€ฆuit test

test_dict_to_ordered_dict_leaf_shortcircuit's dict-of-dict block checked
that each of two differently-ordered but structurally-equivalent nested
dicts round-trips correctly on its own, which holds even when a nested
dict level is left unsorted, since flatten and pack_sequence_as apply
the same canonicalization consistently within a single round-trip. Add
a direct comparison of the two structures' flattened leaf lists, which
is the property that actually depends on every nesting level being
canonicalized and not just the outermost one.
torch.compile/dynamo cannot trace id() comparisons, so the one-level
flatten in _dict_to_ordered_dict and traverse compared child identity
against the root's id() and graph-broke under compilation. Replace it
with a root-flag closure that tracks whether the root node has been
visited yet, with no id() compare at all. The closure is deduplicated
into a single module-level factory, _make_root_is_leaf, used by both
traverse_children call sites (previously each defined its own copy).

Each call to the factory returns a fresh closure instance, so nested/
repeated traversal calls do not leak seen_root state across each
other -- semantics are unchanged from the per-call id(structure)
scoping this replaces.

Adds regression coverage in tree_test.py: traverse/
_dict_to_ordered_dict equivalence over nested dict/list/namedtuple
structures, and a root-visited-exactly-once check guarding against
the root being mistakenly re-split into its own children.
@pctablet505 pctablet505 changed the title perf(torch): short-circuit _dict_to_ordered_dict for leaf inputs fix(torch): canonicalize nested dict key order in the torch tree implementation Sep 9, 2026
โ€ฆpile fix

_dict_to_ordered_dict now recurses into dict values, so key order is
canonical at every level rather than only at the root. traverse
canonicalizes once at the root and walks the already-canonical structure,
so the recursion is not repeated per level.

The id()-comparison change for torch.compile is removed from this branch
and will be sent as its own PR, as requested.
@pctablet505

Copy link
Copy Markdown
Contributor Author

cannot mark the PR ready for review due to new limits enforced on number of open pull requests.

@hertschuh hertschuh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the fix!

@google-ml-butler google-ml-butler Bot added kokoro:force-run ready to pull Ready to be merged into the codebase labels Sep 10, 2026
@hertschuh
hertschuh marked this pull request as ready for review September 10, 2026 20:00
@hertschuh
hertschuh merged commit e9112ec into keras-team:master Sep 10, 2026
22 of 23 checks passed
@google-ml-butler google-ml-butler Bot removed awaiting review ready to pull Ready to be merged into the codebase kokoro:force-run labels Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

_dict_to_ordered_dict does a full pytree traversal even for a plain leaf tensor

4 participants