Skip to content

perf(layers): fast path for single-InputSpec assert_input_compatibility - #23184

Merged
hertschuh merged 13 commits into
keras-team:masterfrom
pctablet505:perf-P5-input-spec
Oct 13, 2026
Merged

hertschuh merged 13 commits into
keras-team:masterfrom
pctablet505:perf-P5-input-spec

Conversation

@pctablet505

@pctablet505 pctablet505 commented Jun 27, 2026 •

Copy link
Copy Markdown
Contributor

Closes #23275

Summary

assert_input_compatibility flattens both the spec and the inputs and runs a full input-count check before validating anything, even for the common case of one InputSpec against one tensor. This adds a fast path for that case and extracts the per-spec validation into _check_input_spec, which the general path also uses.

_check_input_spec is master's per-input checks, moved without change to the bottom of the file, so the diff reads as a change to assert_input_compatibility plus a new helper.

Behaviour change

One: an out-of-range spec.axes key now raises ValueError naming the axis, the input rank and the full shape. Previously it raised a bare IndexError from the tuple lookup.

The allow_last_axis_squeeze change from earlier revisions is no longer part of this PR. It changes which inputs Functional models accept and does not depend on the fast path, so it will be sent as a separate PR, as requested in review.

Performance

assert_input_compatibility(spec, x, name) with spec = InputSpec(min_ndim=2, axes={-1: 64}), which is what a built Dense sets, and a (32, 64) tensor. Per call:

backend bare spec, master bare spec, this PR change list of one spec, master list of one spec, this PR change
torch 1.21 ยตs 0.96 ยตs โˆ’21% 1.33 ยตs 1.42 ยตs +7%
TensorFlow 1.58 ยตs 1.19 ยตs โˆ’25% 1.75 ยตs 1.75 ยตs 0%
JAX 1.56 ยตs 1.15 ยตs โˆ’26% 1.69 ยตs 1.72 ยตs +2%
NumPy 1.04 ยตs 0.69 ยตs โˆ’33% 1.16 ยตs 1.21 ยตs +4%

The bare spec takes the fast path and saves 0.25 to 0.40 ยตs per call. A list of one spec stays on the general path, which now pays for the call into the shared helper: up to 0.09 ยตs.

A full Dense(64) call on a (32, 64) input, which calls assert_input_compatibility once with a bare spec:

backend master this PR change master measured twice
torch, CPU 40.9 ยตs 40.3 ยตs โˆ’1.5% 0.3%
torch, CUDA 40.7 ยตs 39.4 ยตs โˆ’3.1% 0.1%
TensorFlow 137.2 ยตs 135.0 ยตs โˆ’1.6% 0.1%
JAX 39.6 ยตs 39.4 ยตs โˆ’0.4% 0.6%
NumPy 52.2 ยตs 51.3 ยตs โˆ’1.8% 0.8%

Timed in place inside the Dense call, the check takes about twice as long as in an isolated loop, and the fast path saves 0.2 to 0.5 ยตs there (2.65 ยตs to 2.14 ยตs on torch). That is 0.2% to 1.4% of the call, so the saving on a whole layer call is about 1%.

Method: one process per build, master and this PR interleaved over 5 rounds, the best of 7 timeit repeats per round, median over rounds. The last column is the difference between two master builds measured in the same runs; on the per-call table that difference is at most 2.3%. CPU rows are Python 3.11; the CUDA row is Python 3.14 on a shared GPU.

Coverage

input_spec_test.py adds 28 tests. Twenty-three call assert_input_compatibility directly and cover the squeeze cases that master accepts, the out-of-bounds axis, a None axis value, wrong axes, wrong shape, wrong dtype, min_ndim/ndim/max_ndim, non-tensor inputs, the optional-spec and empty-spec early returns, and that a bare spec and a one-element list spec behave identically, so the fast path and the general path agree. Two of them cover the guard on the fast path: nested inputs (a one-element list, a dict, two tensors) must take the general path, and None with a required spec must raise. The remaining five go through Dense and through Functional models.

Contributor Agreement

  • This PR is linked to an issue that has been assigned to me (see Closes #23275 above).
  • 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 introduces a fast-path optimization in assert_input_compatibility for the common case of a single InputSpec and a single tensor input. This optimization bypasses tree.flatten and the generic loop, reducing Python-level overhead during layer forward passes. Additionally, a comprehensive test suite has been added to verify that this fast path behaves identically to the general path across various input shapes, dimensions, and configurations. There are no review comments, and I have no feedback to provide.

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.

@codecov-commenter

codecov-commenter commented Jun 27, 2026 •

Copy link
Copy Markdown

Codecov Report

โŒ Patch coverage is 94.87179% with 2 lines in your changes missing coverage. Please review.
โœ… Project coverage is 84.51%. Comparing base (63e75ba) to head (595c713).

Files with missing lines Patch % Lines
keras/src/layers/input_spec.py 94.87% 0 Missing and 2 partials โš ๏ธ
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #23184      +/-   ##
==========================================
- Coverage   85.33%   84.51%   -0.83%     
==========================================
  Files         480      480              
  Lines       63874    63885      +11     
  Branches    10798    10802       +4     
==========================================
- Hits        54510    53993     -517     
- Misses       6697     7236     +539     
+ Partials     2667     2656      -11     
Flag Coverage ฮ”
keras 84.33% <94.87%> (-0.80%) โฌ‡๏ธ
keras-cpu 84.33% <94.87%> (+0.02%) โฌ†๏ธ
keras-gpu ?
keras-jax 66.02% <94.87%> (-0.34%) โฌ‡๏ธ
keras-numpy 61.67% <94.87%> (+0.03%) โฌ†๏ธ
keras-tensorflow 67.92% <94.87%> (-0.27%) โฌ‡๏ธ
keras-torch 67.44% <94.87%> (-0.40%) โฌ‡๏ธ
keras-tpu ?

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-P5-input-spec branch 3 times, most recently from d9e5db3 to 81c6825 Compare July 1, 2026 16:37
@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 refactors input compatibility checking in Keras by extracting a single-spec validation helper _check_input_spec and introducing a fast path for single-spec, single-tensor inputs to bypass flattening overhead. It also adds a comprehensive test suite in input_spec_test.py. The review feedback highlights critical edge cases in the validation logic: first, when allow_last_axis_squeeze is active, a rank mismatch can lead to silent truncation in zip and bypass shape checks; second, out-of-bounds axes in spec.axes can trigger unhandled IndexError exceptions. Suggestions are provided to explicitly validate rank equality and axis bounds, along with a corresponding test case.

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/layers/input_spec.py Resolved
Comment thread keras/src/layers/input_spec.py Resolved
Comment thread keras/src/layers/input_spec_test.py Resolved
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

Address the gemini-code-assist review on this PR and fix two latent
validation bugs. Both predate this PR on master (the code was moved
verbatim into `_check_input_spec`); fixing them here fixes the fast
path and the general path uniformly.

- Raise a `ValueError` instead of an unhandled `IndexError` when
  `spec.axes` refers to an axis that does not exist on the input.

- Only squeeze one side, and only for rank mismatches of exactly one,
  when `allow_last_axis_squeeze=True`. The previous unconditional
  double-squeeze compared misaligned dimensions and silently accepted
  incompatible inputs such as shape `(2, 3, 99, 1)` against spec
  `(None, 3, 1)`, or `(2, 1)` against spec `(None, 3)`.

Rank mismatches that squeezing cannot resolve intentionally remain
non-raising here, diverging from the review suggestion: `Functional`
(the only user of `allow_last_axis_squeeze=True`) validates ranks right
after in `_adjust_input_rank` with a more informative error that
includes the input path, a behavior pinned by
`functional_test.py::test_rank_standardization_failure`. The suggested
strict rank check would also reject documented-legal inputs such as
shape `(1,)` against spec `(None, 1)`.

Also align the new test file with keras test conventions (no module
docstring, no section dividers, house naming) and expand regression
coverage: out-of-bounds axes, squeeze dimension mismatches, and the
Functional rank-mismatch error path. The new regression tests fail
before the fix and pass after, on all four backends.
pctablet505 and others added 6 commits September 2, 2026 19:06
Per review, so the diff reads as a modification of
assert_input_compatibility plus an appended helper rather than a
rewrite of the whole file, and shorten the fast-path comment to the
suggested two lines.
The squeeze rebinds the local `shape`, so a rank N+1 input that failed the
squeezed comparison was reported with the squeezed shape: `(2, 3, 99, 1)`
against spec `(None, 3, 1)` said `found shape=(2, 3, 99)`, naming a shape the
caller never used. Keep the original for the message.

The comment above the zip claimed rank validation is left to
`Functional._adjust_input_rank`. It is not: `assert_input_compatibility` runs
earlier in `Layer.__call__`, so that error is unreachable for these inputs.
State what the code actually does instead.
@pctablet505

Copy link
Copy Markdown
Contributor Author

Thanks for the review @hertschuh! All requested changes have been applied in 7a2a5d980:

  1. Moved _check_input_spec to the bottom of keras/src/layers/input_spec.py as a module-level private helper.
  2. Shortened the try / except AttributeError comment to the exact 2-line wording requested.
  3. Restored the original squeezed shape (batch, ..., 1) in the allow_last_axis_squeeze error message when rank mismatches after squeezing the last axis.
  4. Added unit tests covering allow_last_axis_squeeze valid rank-mismatch inputs (e.g. (4, 28, 28, 1) into expected (4, 28, 28)) and error cases ((4, 28, 28, 1) into (4, 32)). Note that equal-rank inputs with a mismatched last axis (e.g. (4, 28, 28, 3) into expected (None, 28, 28, 1)) now correctly raise ValueError on axis -1, whereas previously allow_last_axis_squeeze only validated axes inside the len(shape) - 1 == spec.ndim branch.

@JyotinderSingh

Copy link
Copy Markdown
Collaborator

Please fix the failing CI checks

keras.src.backend no longer exports convert_to_tensor since keras-team#23642, so the
new tests raised AttributeError on every backend.
@pctablet505

Copy link
Copy Markdown
Contributor Author

Please fix the failing CI checks

fixed it

@JyotinderSingh JyotinderSingh 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.

Please see the inline comments.

Additionally, please add a measurement to the PR description

The description does not say what the fast path saves. Please add a before and after measurement for assert_input_compatibility with a bare spec, and for a full Dense call. The saving on a full layer call is small, we need the number to decide if the new branch is worth it.

spec_shape = spec.shape
# Report the shape the caller passed, not the squeezed one.
input_shape = shape
if spec.allow_last_axis_squeeze:

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.

Please move the squeeze change out of this PR and into its own PR.

Functional.input_spec returns a list, so no Functional model reaches the new fast path. The fast path serves layers with a bare InputSpec. This change applies only to Functional models and to layers that set allow_last_axis_squeeze=True. The two changes do not depend on each other.

This is a breaking change. model(x) now raises for inputs that master accepts.

Examples: a (28, 28, 1) model called with a (4, 28, 28, 3) input, or a (3,) model called with a (2, 1) input. The new rule is correct. The PR never rejects an input that the docstring allows, and it never accepts an input that master rejects. But how many users it breaks is something we should investigate - which is why it should be in a separate PR.

Here, please keep master's squeeze logic (a verbatim move into _check_input_spec). The ValueError for an out-of-bounds axis can stay.

In the separate PR, add a Functional test for the same-rank case. No test calls a Functional model with an input of the same rank and the wrong last axis.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

#23920
Opened a new PR and fixed the other actionable.

Comment thread keras/src/layers/input_spec.py Resolved
Comment thread keras/src/layers/input_spec.py Resolved
Comment thread keras/src/layers/input_spec.py Resolved
Comment thread keras/src/layers/input_spec_test.py Resolved
Comment thread keras/src/layers/input_spec_test.py Resolved
Comment thread keras/src/layers/input_spec_test.py Resolved
Comment thread keras/src/layers/input_spec_test.py Resolved
The allow_last_axis_squeeze change is a behaviour change for Functional
models and does not depend on the fast path, so it moves to its own PR.
_check_input_spec is now master's per-input checks moved verbatim, plus
the ValueError for an out-of-bounds axis.

Add tests for the inputs that must leave the fast path and for a None
input with a required spec, and tighten three assertions.
@JyotinderSingh

Copy link
Copy Markdown
Collaborator

Thanks for addressing the reviews. The gain is small, on a full Dense call it is about 1%. I'm unsure if it adds a lot of value outside of the additional test coverage.

I'm okay either way. @hertschuh WDYT?

@hertschuh

Copy link
Copy Markdown
Collaborator

Thanks for addressing the reviews. The gain is small, on a full Dense call it is about 1%. I'm unsure if it adds a lot of value outside of the additional test coverage.

I'm okay either way. @hertschuh WDYT?

To me, part of the value is the refactor extracting _check_input_spec out of assert_input_compatibility, which was really long.

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

@hertschuh
hertschuh merged commit 156a613 into keras-team:master Oct 13, 2026
26 checks passed
pctablet505 added a commit to pctablet505/keras that referenced this pull request Oct 13, 2026
Conflict in keras/src/layers/input_spec.py: master moved the per-input
checks into _check_input_spec (keras-team#23184). The shape check there now takes
this branch's version, without the squeeze of both shapes.
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.

assert_input_compatibility re-validates the same static InputSpec every call

6 participants