Skip to content

perf(backend): O(1) ALLOWED_DTYPES membership and a fast path for canonical dtype strings - #23182

Merged
hertschuh merged 11 commits into
keras-team:masterfrom
pctablet505:perf-P1-dtype-fastpath
Sep 10, 2026
Merged

hertschuh merged 11 commits into
keras-team:masterfrom
pctablet505:perf-P1-dtype-fastpath

Conversation

@pctablet505

@pctablet505 pctablet505 commented Jun 27, 2026 •

Copy link
Copy Markdown
Contributor

Closes #23272

Summary

standardize_dtype runs a chain of hasattr probes and a linear membership test against the ALLOWED_DTYPES tuple on every call, even when it is handed a dtype string that is already canonical โ€” which is the overwhelmingly common case.

Two changes:

  • dtypes.ALLOWED_DTYPES_SET, a frozenset derived from ALLOWED_DTYPES, so the membership test is O(1). ALLOWED_DTYPES itself stays an ordered tuple: several test suites build parametrized cases by iterating it, and pytest-xdist needs a deterministic collection order across workers.
  • An early return for a str that is already in that set, skipping the normalization chain entirely.

Everything else is unchanged.

Effect

Single process, interleaved with an A/A control, 20k iterations ร— 11 rounds:

input master this PR
"float32" (canonical string) 110.0 ns 32.3 ns
A/A control 109.7 ns 32.2 ns
torch.float32 (non-string path) 208.0 ns 214.4 ns

3.4ร— on the common path. The non-string path pays one extra isinstance check and is ~3% slower; the A/A control puts the noise floor under 1%, so that cost is real but small.

End to end, standardize_dtype is called 71 times per forward pass on a small CNN and 297 times on a 20-layer MLP (counted with cProfile, which is immune to from โ€ฆ import rebinding). Measured PR vs master, CPU, medians of interleaved runs:

model master this PR
MLP, 20 layers 1.3170 ms 1.2992 ms โˆ’1.4% (master spread 0.4%)
CNN, 2 blocks 3.2308 ms 3.1950 ms โˆ’1.1% (master spread 5.0%, so within noise)

The MLP figure is outside its noise band and agrees with the arithmetic (297 ร— 77.7 ns = 23 ยตs on a 1.32 ms step). The CNN does proportionally more real compute per layer call, so the effect disappears into noise there.

Equivalence

A differential probe of 48 inputs โ€” every ALLOWED_DTYPES string, every PYTHON_DTYPES_MAP key, None, Python scalar types, numpy dtypes and scalar types, torch dtypes, "torch.*" / "jax.numpy.*" strings, "torch.float32.extra", and invalid inputs including 1, True, [], {} and object() โ€” returns identical results and identical exception types and messages on both trees. The only differing byte is the memory address inside the object() error message, which differs between any two processes.

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.

@google-cla

google-cla Bot commented Jun 27, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@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 optimizes the standardize_dtype function by converting ALLOWED_DTYPES to a frozenset for O(1) lookups, introducing fast paths for common dtype formats, and implementing a module-level cache (_DTYPE_CACHE) for resolved non-string dtypes. However, the new string fast path introduces a regression where framework-prefixed dtype strings (such as "torch.float32" or "jax.numpy.float32") are no longer correctly resolved and instead raise a ValueError. The feedback suggests updating the string fast path to handle these prefixed strings and adding corresponding test cases to prevent future regressions.

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/backend/common/variables.py Resolved
@pctablet505
pctablet505 force-pushed the perf-P1-dtype-fastpath branch 2 times, most recently from 0e93741 to 1343b06 Compare June 27, 2026 15:17
@codecov-commenter

codecov-commenter commented Jun 27, 2026 •

Copy link
Copy Markdown

Codecov Report

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

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #23182      +/-   ##
==========================================
- Coverage   86.88%   84.31%   -2.58%     
==========================================
  Files         477      478       +1     
  Lines       71493    71543      +50     
  Branches    11825    11833       +8     
==========================================
- Hits        62120    60318    -1802     
- Misses       6239     8209    +1970     
+ Partials     3134     3016     -118     
Flag Coverage ฮ”
keras 84.13% <100.00%> (-2.57%) โฌ‡๏ธ
keras-cpu 84.13% <100.00%> (-1.92%) โฌ‡๏ธ
keras-gpu ?
keras-jax 58.37% <100.00%> (-2.18%) โฌ‡๏ธ
keras-numpy 54.09% <100.00%> (-0.17%) โฌ‡๏ธ
keras-openvino 59.74% <100.00%> (-0.17%) โฌ‡๏ธ
keras-tensorflow 60.07% <100.00%> (-2.09%) โฌ‡๏ธ
keras-torch 59.54% <100.00%> (-2.21%) โฌ‡๏ธ
keras-tpu ?
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
pctablet505 force-pushed the perf-P1-dtype-fastpath branch from 1343b06 to 3d8ce24 Compare June 27, 2026 16:03
@pctablet505
pctablet505 force-pushed the perf-P1-dtype-fastpath branch from 3d8ce24 to b191894 Compare June 30, 2026 17:11
@pctablet505
pctablet505 force-pushed the perf-P1-dtype-fastpath branch 3 times, most recently from 167cf39 to ee95f3c Compare July 1, 2026 16:37
@hertschuh

Copy link
Copy Markdown
Collaborator

Is this still in draft? Or is this ready for review?

@pctablet505

Copy link
Copy Markdown
Contributor Author

/gemini review

@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 14, 2026
tf.DType's __hash__/__eq__ are coerced to match plain Python ints
(hash(tf.float32) == hash(1) == 1, tf.float32 == 1 is True). Once a
tf.DType populated _DTYPE_CACHE or the result_type lru_cache, a later
unrelated invalid int/bool argument would silently return the wrong
cached dtype string instead of raising ValueError. Both caches are now
keyed on (type(dtype), dtype) tuples, which defeats the coercing
equality. Adds regression tests pinning the fix, extends the
standardize_dtype contract test with a complex-dtype case, and adds a
backend-agnostic test for the cache fast path.
The tf.DType coercion rationale was fully re-explained in three
places across two files; keep one full explanation per file and
shorten the others to what's not already said nearby. Drop a comment
that only restated the line below it.
Moves the lru_cache-decorated resolution core above result_type, its
sole call site, matching the house convention of defining private
helpers before the public function that uses them.
Matches the house style used elsewhere in the codebase (e.g.
backend/jax/rnn.py) instead of embedding an arrow mid-sentence.
Per review, this PR should cover `ALLOWED_DTYPES` and `standardize_dtype`
only. The `result_type` lru_cache and its two tests are removed here and
move to their own PR; `result_type` is restored to its master form.

`ALLOWED_DTYPES_SET` stays because `standardize_dtype` is what needs the
O(1) membership test.
variables.py had 14 comment lines on ~50 added lines, denser than the
file's own standardize_shape (its closest sibling). Cut the "Fast
path N" labels that just restated the branch below them, and folded
the 6-line _DTYPE_CACHE block down to match the phrasing already
settled on for the same explanation in dtypes.py.

variables_test.py's new tests carried 6-line docstrings against a
file where docstrings, when present, are one line. Trimmed to that
convention and dropped inline comments that only restated the line
below them.
Comment thread keras/src/backend/common/variables.py Outdated
Comment on lines +580 to +582
if isinstance(dtype, str):
if dtype in dtypes.ALLOWED_DTYPES_SET:
return dtype

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.

How much speed-up to you get if you undo everything, but keep ALLOWED_DTYPES_SET, add these 3 lines of code...

Comment thread keras/src/backend/common/variables.py Resolved
Per review, undo the dtype cache and the restructured branches, keeping
only the O(1) membership set and an early return for an already-canonical
dtype string.
@pctablet505 pctablet505 changed the title perf(backend): O(1) ALLOWED_DTYPES membership + cached standardize_dtype perf(backend): O(1) ALLOWED_DTYPES membership and a fast path for canonical dtype strings Sep 9, 2026

@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!

@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
Removed comment about O(1) membership set for ALLOWED_DTYPES.
@google-ml-butler google-ml-butler Bot removed the ready to pull Ready to be merged into the codebase label Sep 10, 2026
@hertschuh
hertschuh merged commit 39a78cf into keras-team:master Sep 10, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

standardize_dtype / result_type do uncached O(n) dtype lookups

5 participants