Skip to content

Commit 62c9532

Browse files
Fix gh-18459: pre-build Sequential sublayers not reached through call()
Root cause: when a Sequential sublayer is only used inside a custom `train_step` (not in `call()`), it is not built during `_symbolic_build`, which only traces `call()`. For the TF backend without a distribute strategy, `_maybe_symbolic_build()` previously returned early without calling `_symbolic_build` at all. This caused the Sequential sublayer to be built lazily during the first `tf.function` trace of `train_step`. `Sequential.build()` creates KerasTensors and calls `compute_output_spec`, which for TF creates a nested `FuncGraph` inside the already-active `tf.function` context. When TF then tries to use a KerasTensor as a real tensor, it hits `KerasTensor.__tf_tensor__` which raises: "A KerasTensor cannot be used as input to a TensorFlow function." Fix (two parts): 1. `_symbolic_build` (trainer.py): after tracing `call()`, iterate over all sublayers and pre-build any that are still unbuilt by running `compute_output_spec` on the symbolic input `x`. 2. TF `_maybe_symbolic_build` (tensorflow/trainer.py): instead of always deferring when no distribute strategy is set, only defer when all layers are already built. If unbuilt sublayers exist, call `_symbolic_build` so they are built before `tf.function` traces `train_step`. Also updates the regression test docstrings to accurately describe the root cause (gh-18459).
1 parent 316d355 commit 62c9532

3 files changed

Lines changed: 35 additions & 7 deletions

File tree

keras/src/backend/tensorflow/trainer.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,15 @@ def _maybe_symbolic_build(self, iterator=None, data_batch=None):
715715
# When no distribution strategy is set, defer building
716716
# to when the train/test/predict function gets traced.
717717
# This maximizes backwards compatibility.
718+
# Exception: if the model has unbuilt sublayers that won't be
719+
# reached through `call()` (e.g. a Sequential sublayer only used
720+
# in a custom `train_step`), we must pre-build them here.
721+
# Otherwise `Sequential.build()` runs lazily inside `tf.function`
722+
# tracing, where it creates a nested `FuncGraph` that is
723+
# incompatible with the active `tf.function` context (gh-18459).
724+
if all(layer.built for layer in self._flatten_layers()):
725+
return
726+
self._symbolic_build(iterator=iterator, data_batch=data_batch)
718727
return
719728

720729
# Unlike jax/torch iterator, tf iterator returns an iterator instead

keras/src/trainers/trainer.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,19 @@ def to_symbolic_input(v):
11301130
"Exception encountered:\n"
11311131
f"'{e}'"
11321132
)
1133+
# Pre-build any sublayers that were not reached through `call()`
1134+
# (e.g. a Sequential sublayer only used in a custom `train_step`).
1135+
# Without this, such layers would be built lazily during the first
1136+
# `tf.function` trace of `train_step`, where `Sequential.build()`
1137+
# creates a nested `FuncGraph` inside an active `tf.function`
1138+
# context, causing "A KerasTensor cannot be used as input to a
1139+
# TensorFlow function" (gh-18459).
1140+
for layer in self._flatten_layers():
1141+
if not layer.built:
1142+
try:
1143+
backend.compute_output_spec(layer, x)
1144+
except Exception:
1145+
pass
11331146
if compile_metrics_unbuilt:
11341147
# Build all metric state with `backend.compute_output_spec`.
11351148
backend.compute_output_spec(

keras/src/trainers/trainer_test.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,15 @@ def test_step(self, state, data):
8282
class SequentialSublayerInTrainStepModel(Trainer, layers.Layer):
8383
"""Model that calls a Sequential sublayer only inside train_step.
8484
85-
Used as a regression test for GitHub issue #18459, which reported
86-
that calling a Sequential model from a custom train_step during
87-
model.fit() incorrectly received KerasTensors instead of real
88-
tensors, causing: "A KerasTensor cannot be used as input to a
89-
TensorFlow function."
85+
Used as a regression test for GitHub issue #18459. The bug: a
86+
Sequential sublayer used only in train_step (not in call()) is not
87+
built during _symbolic_build, which only traces call(). For the TF
88+
backend without a distribute strategy, _maybe_symbolic_build() used
89+
to exit early entirely, so the sublayer was built lazily during the
90+
first tf.function trace of train_step. Sequential.build() creates
91+
KerasTensors and calls compute_output_spec() which for TF creates a
92+
nested FuncGraph inside the active tf.function context, causing:
93+
"A KerasTensor cannot be used as input to a TensorFlow function."
9094
"""
9195

9296
def __init__(self, units):
@@ -791,8 +795,10 @@ def test_fit_with_custom_train_step(self):
791795
def test_sequential_sublayer_in_custom_train_step(self):
792796
"""Regression test for GitHub issue #18459.
793797
794-
Calling a Sequential sublayer from a custom train_step during
795-
model.fit() must not raise a KerasTensor-related error.
798+
A Sequential sublayer used only in train_step must be pre-built by
799+
_symbolic_build before tf.function traces train_step. Without the
800+
fix, Sequential.build() ran lazily inside the tf.function context,
801+
creating a nested FuncGraph that caused a KerasTensor error.
796802
"""
797803
if backend.backend() == "jax":
798804
self.skipTest(

0 commit comments

Comments
 (0)