Skip to content

Fix Layer.__delattr__ to untrack sub-layers (#23761) - #23781

Open
Pushpak731 wants to merge 1 commit into
keras-team:masterfrom
Pushpak731:fix-layer-delattr-untrack
Open

Pushpak731 wants to merge 1 commit into
keras-team:masterfrom
Pushpak731:fix-layer-delattr-untrack

Conversation

@Pushpak731

Copy link
Copy Markdown

Description

Fixes #23761

This PR fixes an issue where deleting a sub-layer attribute via \del layer.attr\ did not untrack the layer, leaving its weights and parameters tracked in \layer.weights, \layer.trainable_weights, and \layer.count_params().

Changes Made

  1. **\keras/src/layers/layer.py**:
    • Added _untrack_layer(self, layer)\ to safely unlock _tracker, untrack the sub-layer from the tracker store, and restore lock state.
    • Updated \Layer.delattr\ to call \self._untrack_layer(obj)\ when \isinstance(obj, Layer).
  2. **\keras/src/layers/layer_test.py**:
    • Added \ est_delattr_untrack_layer\ verifying that dynamically attaching a sub-layer to a model and subsequently running \del model.attr\ properly restores \len(model.weights)\ and \model.count_params(), and removes the layer from \model.layers.

Contributor Agreement

Please review our PR Contribution Policy and AI-Assisted Contribution Policy and check all boxes below before submitting your PR for review:

  • This PR is linked to an issue that has been assigned to me (see \Fixes #xxx\ 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.

@github-actions
github-actions Bot marked this pull request as draft September 29, 2026 05:35
@github-actions

Copy link
Copy Markdown

❌ Approved issue check failed. This PR has been placed in draft until it links an approved issue assigned to @Pushpak731.

Please review the Keras Pull Request Policy. To resolve this:

  1. Find or open an issue for this change and ask a maintainer to approve it and assign it to you.
  2. Link the issue in this PR's description (e.g. Fixes #xxx).
  3. Once this check passes, mark the PR Ready for review when it is ready.

Referenced issue(s) not assigned to @Pushpak731: #23761.

@google-cla

google-cla Bot commented Sep 29, 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.

@Pushpak731
Pushpak731 marked this pull request as ready for review September 29, 2026 05:36

@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 mechanism to untrack sub-layers when they are deleted from a parent layer or model via __delattr__. It adds the _untrack_layer helper method to manage tracker locking state during untracking and updates __delattr__ to invoke this helper when deleting a Layer attribute. A corresponding unit test is added to verify that weights, parameters, and the layer itself are correctly untracked upon deletion. The review feedback suggests expanding the unit test to verify that sub-layers nested inside container attributes (such as lists) are also correctly untracked when the container attribute is deleted.

Comment thread keras/src/layers/layer_test.py Outdated
Comment on lines +2111 to +2126
def test_delattr_untrack_layer(self):
inputs = Input((4,))
model = Model(inputs, layers.Dense(4)(inputs))
initial_num_weights = len(model.weights)
initial_params = model.count_params()

helper = layers.Dense(3)
helper.build((None, 4))
model.helper = helper
self.assertGreater(len(model.weights), initial_num_weights)
self.assertGreater(model.count_params(), initial_params)

del model.helper
self.assertEqual(len(model.weights), initial_num_weights)
self.assertEqual(model.count_params(), initial_params)
self.assertNotIn(helper, model.layers)

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.

medium

Update the test to also verify that sub-layers nested inside container attributes (like lists) are correctly untracked when the container attribute is deleted.

    def test_delattr_untrack_layer(self):
        inputs = Input((4,))
        model = Model(inputs, layers.Dense(4)(inputs))
        initial_num_weights = len(model.weights)
        initial_params = model.count_params()

        helper = layers.Dense(3)
        helper.build((None, 4))
        model.helper = helper
        self.assertGreater(len(model.weights), initial_num_weights)
        self.assertGreater(model.count_params(), initial_params)

        del model.helper
        self.assertEqual(len(model.weights), initial_num_weights)
        self.assertEqual(model.count_params(), initial_params)
        self.assertNotIn(helper, model.layers)

        # Test untracking layers inside a list container
        helper_list = [layers.Dense(3), layers.Dense(2)]
        for h in helper_list:
            h.build((None, 4))
        model.helper_list = helper_list
        self.assertGreater(len(model.weights), initial_num_weights)

        del model.helper_list
        self.assertEqual(len(model.weights), initial_num_weights)
        for h in helper_list:
            self.assertNotIn(h, model.layers)

@github-actions
github-actions Bot marked this pull request as draft September 29, 2026 05:37
@github-actions

Copy link
Copy Markdown

❌ Approved issue check failed. This PR has been placed in draft until it links an approved issue assigned to @Pushpak731.

Please review the Keras Pull Request Policy. To resolve this:

  1. Find or open an issue for this change and ask a maintainer to approve it and assign it to you.
  2. Link the issue in this PR's description (e.g. Fixes #xxx).
  3. Once this check passes, mark the PR Ready for review when it is ready.

Referenced issue(s) not assigned to @Pushpak731: #23761.

@codecov-commenter

codecov-commenter commented Oct 3, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.66667% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.83%. Comparing base (df66bff) to head (a9062f7).

Files with missing lines Patch % Lines
keras/src/layers/layer.py 66.66% 3 Missing and 5 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #23781      +/-   ##
==========================================
- Coverage   87.70%   84.83%   -2.88%     
==========================================
  Files         481      481              
  Lines       64218    64242      +24     
  Branches    10802    10811       +9     
==========================================
- Hits        56322    54497    -1825     
- Misses       5135     7100    +1965     
+ Partials     2761     2645     -116     
Flag Coverage Δ
keras 84.65% <66.66%> (-2.86%) ⬇️
keras-cpu 84.65% <66.66%> (-2.14%) ⬇️
keras-gpu ?
keras-jax 66.27% <66.66%> (-2.41%) ⬇️
keras-numpy 62.16% <62.50%> (-0.17%) ⬇️
keras-tensorflow 68.13% <66.66%> (-2.33%) ⬇️
keras-torch 67.77% <66.66%> (-2.46%) ⬇️
keras-tpu ?
keras.applications ?
keras.applications-jax ?
keras.applications-numpy ?
keras.applications-tensorflow ?
keras.applications-torch ?
keras.wrappers ?
keras.wrappers-jax ?
keras.wrappers-numpy ?
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.

@laxmareddyp
laxmareddyp marked this pull request as ready for review October 5, 2026 23:38
@goyaladitya05

Copy link
Copy Markdown
Collaborator

@Pushpak731 could you please sign the CLA?

@Pushpak731

Copy link
Copy Markdown
Author

Thanks @goyaladitya05! Signing the individual CLA now via https://cla.developers.google.com with the GitHub account. The commits on this PR are authored as [email protected], so once the CLA covers that email the check should turn green — will confirm here right after.

@Pushpak731

Copy link
Copy Markdown
Author

@goyaladitya05 The CLA is signed — the check just hadn't re-evaluated since it last ran on Sep 29. Pushed a commit to re-trigger it, and cla/google now reports ✅ All contributors are covered under a CLA with Google (see the latest head). The zizmor-output failure is the repo-wide ref-version-mismatch in scorecard.yml being fixed in #23881 — this PR doesn't touch any workflows. Thanks!

@keerthanakadiri keerthanakadiri self-assigned this Oct 12, 2026
@keerthanakadiri keerthanakadiri added the stat:awaiting keras-eng Awaiting response from Keras engineer label Oct 12, 2026
Pushpak731 added a commit to Pushpak731/keras that referenced this pull request Oct 14, 2026
…_delattr__

Address code review feedback on keras-team#23781: deleting a container attribute
(e.g. a list of sub-layers) previously fell through to the default
attribute deletion, leaving every nested sub-layer and variable tracked.
Untrack tracked elements recursively before deleting the attribute, and
extend test_delattr_untrack_layer with list, dict and nested-container
cases.

Guards against properties (e.g. `weights`, `layers`) that return
containers: only instance attributes stored in `__dict__` are unwound.
@Pushpak731

Copy link
Copy Markdown
Author

Addressed the remaining code-review feedback in bf3c7d6: __delattr__ now also untracks sub-layers and variables nested inside container attributes (lists, dicts, nested containers) before the attribute is deleted, and test_delattr_untrack_layer gained list / dict / nested-container cases. Verified locally with the torch backend (layer_test + tracking_test: 81 passed) and confirmed the new assertions fail without the implementation change.

@laxmareddyp

Copy link
Copy Markdown
Collaborator

HI @Pushpak731 Could you please rebase your branch with master and fix the test failures? You can ignore the zizmor failure, as it will go away automatically once you rebase. Thanks!

@Pushpak731
Pushpak731 force-pushed the fix-layer-delattr-untrack branch from bf3c7d6 to a9062f7 Compare November 2, 2026 17:00
@Pushpak731

Copy link
Copy Markdown
Author

Thanks for the nudge! Rebased onto master and force-pushed as a single clean commit (a9062f7). The four torch attention failures were gone after the rebase — they came from the branch being ~100 commits behind, not from this change (they pass on the rebased head locally, 4/4). The format failure is fixed too; ruff and ruff-format are clean on both files now, and the delattr tests were moved into LayerTest where they belong (the rebase had landed them in the int4 setter class). Full layer_test.py passes on the torch backend (86 passed). Zizmor left alone as suggested.

@laxmareddyp

Copy link
Copy Markdown
Collaborator

Thanks for the nudge! Rebased onto master and force-pushed as a single clean commit (a9062f7). The four torch attention failures were gone after the rebase — they came from the branch being ~100 commits behind, not from this change (they pass on the rebased head locally, 4/4). The format failure is fixed too; ruff and ruff-format are clean on both files now, and the delattr tests were moved into LayerTest where they belong (the rebase had landed them in the int4 setter class). Full layer_test.py passes on the torch backend (86 passed). Zizmor left alone as suggested.

Really thanks for quick response, will take a look.

Comment thread keras/src/layers/layer.py Outdated
Comment on lines +1993 to +1995
elif isinstance(obj, Layer):
self._untrack_layer(obj)
super().__delattr__(name)

@gaga1313 gaga1313 Nov 3, 2026 •

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.

Tracker stores each object once by id, so del of one attribute untracks the object even if another attribute still references it.

For example: self.encoder = enc; self.blocks = [enc, dec] is a normal pattern. Because the store holds enc once, del self.encoder now removes enc from _layers even though self.blocks[0] still uses it in call.

Pontential fix: only untrack when no other attribute still reaches the object. tree.flatten already understands TrackedList/TrackedDict/tuples and treats layers/variables as leaves

+    def _is_referenced_elsewhere(self, obj, skip_name):
+        others = {k: v for k, v in self.__dict__.items() if k != skip_name}
+        return any(leaf is obj for leaf in tree.flatten(others))
+
     def __delattr__(self, name):
         obj = getattr(self, name)
         ...
-        elif isinstance(obj, Layer):
-            self._untrack_layer(obj)
+        elif isinstance(obj, Layer):
+            if not self._is_referenced_elsewhere(obj, name):
+                self._untrack_layer(obj)
             super().__delattr__(name)

unlike the container branch, the Layer branch has no name in self.dict guard; a read-only property returning a layer would be untracked and then raise AttributeError. The generic version above puts the guard on both.

Comment thread keras/src/layers/layer.py Outdated
Comment on lines +2016 to +2021
if isinstance(element, backend.Variable):
self._untrack_variable(element)
elif isinstance(element, Layer):
self._untrack_layer(element)
elif isinstance(element, (list, tuple, dict, set)):
self._untrack_tracked_elements(element)

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.

Layer._initialize_tracker has five stores (trainable_variables, non_trainable_variables, metrics, layers, seed_generators). The PR's isinstance chain handles two of them; del self.my_metric / del self.seed_gen still leave the object in _metrics / _seed_generators

-    def _untrack_layer(self, layer):
-        if hasattr(self, "_tracker"):
-            previous_lock_state = self._tracker.locked
-            self._tracker.unlock()
-            self._tracker.untrack(layer)
-            if previous_lock_state is True:
-                self._tracker.lock()
+    def _untrack(self, obj, skip_name):
+        """Untracks `obj` (any tracked type) unless another attribute
+        still references it. Recurses into containers."""
+        if not hasattr(self, "_tracker"):
+            return
+        if isinstance(obj, dict):
+            obj = list(obj.values())
+        if isinstance(obj, (list, tuple, set)):
+            for element in obj:
+                self._untrack(element, skip_name)
+        elif not self._is_referenced_elsewhere(obj, skip_name):
+            if isinstance(obj, backend.Variable):
+                self._untrack_variable(obj)
+            else:
+                self._tracker.untrack(obj)

     def __delattr__(self, name):
         obj = getattr(self, name)
         if isinstance(obj, backend.Variable):
             import gc
             self._untrack_variable(obj)
             super().__delattr__(name)
             gc.collect()
-        elif isinstance(obj, Layer):
-            self._untrack_layer(obj)
-            super().__delattr__(name)
-        elif (
-            isinstance(obj, (list, tuple, dict, set)) and name in self.__dict__
-        ):
-            ...
-            self._untrack_tracked_elements(obj)
-            super().__delattr__(name)
         else:
+            if name in self.__dict__:
+                self._untrack(obj, name)
             super().__delattr__(name)
-
-    def _untrack_tracked_elements(self, container):
-        ...

# they are still mean 0 / variance 1.
self.assertAllClose(y, x / np.sqrt(1.0 + 1e-3), atol=1e-3)

def test_delattr_untrack_layer(self):

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.

Fix for codecov 66%:

one 45-line test covering four scenarios with shared initial_* state; the Keras suite usually splits these (test_delattr_untrack_layer, _container, _alias, _metric) so a failure names the case. Also uses a Model as the parent; a plain layers.Layer subclass (the class under test) would exercise the locked-tracker path, since Model/Functional override _lock_state and never lock (confirmed: m._tracker.locked is False in the test fixture, so the "restore lock state" lines are effectively untested.

@gaga1313 gaga1313 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! I have added two inline comments for avoiding deletion of object referenced multiple times and for extending the coverage to untrack metrics and seed_gen.

Deleting an attribute that held a sub-layer left the layer tracked: the
parent still exposed its weights and count_params() kept counting them,
so the removed layer kept influencing saving/tracking semantics.

__delattr__ now untracks whatever the deleted attribute held: layers,
variables, metrics and seed generators, plus the elements of container
attributes (list/tuple/dict/set), which were tracked element-wise on
assignment. Untracking goes through Tracker.untrack, which removes the
object from every store, so all tracked types are covered. Since the
tracker stores each object once by id, the object is only untracked
when no other attribute still reaches it (checked with tree.flatten),
so aliases of a deleted attribute keep the object tracked.

Only attributes that were actually stored on assignment are untracked:
they live in __dict__, or in torch.nn.Module's _modules on the torch
backend. Properties like weights or layers keep the default behavior.
The tracker's own store lists and the torch module registries are
skipped when looking for remaining references, since they mirror what
has been tracked rather than references the layer still holds.
@Pushpak731
Pushpak731 force-pushed the fix-layer-delattr-untrack branch from a9062f7 to 0cdb24e Compare November 5, 2026 16:42

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S stat:awaiting keras-eng Awaiting response from Keras engineer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Layer.__delattr__ does not untrack sub-layers

6 participants