Skip to content

Commit 498e11f

Browse files
Fix ModelPruning iterative reuse RuntimeError on non-leaf tensors
When ModelPruning with use_lottery_ticket_hypothesis=True is reused across multiple trainer.fit() calls, setup() is called again each run. After the first pass, _copy_param sets dst.data = src.data, making the parameters non-leaf tensors. The subsequent deepcopy(module) then raises: RuntimeError: Only Tensors created explicitly by the user (graph leaves) support the deepcopy protocol at the moment Fix by adding _deepcopy_for_pruning(), a helper that temporarily replaces non-leaf parameters with detached clones before deepcopy and restores the originals afterward. Also explicitly set _original_layers to None before re-populating in setup() so the previous run's tensor references are released before new copies are allocated. Fixes #8542
1 parent 88352b7 commit 498e11f

2 files changed

Lines changed: 67 additions & 1 deletion

File tree

src/lightning/pytorch/callbacks/pruning.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,31 @@ def make_pruning_permanent(self, module: nn.Module) -> None:
275275
hook.remove(module)
276276
del module._forward_pre_hooks[k]
277277

278+
@staticmethod
279+
def _deepcopy_for_pruning(module: nn.Module) -> nn.Module:
280+
"""Deep-copy a module safely when parameters may be non-leaf tensors.
281+
282+
After a pruning pass with ``use_lottery_ticket_hypothesis=True``, the
283+
module's parameters are rewritten via ``_copy_param`` (``dst.data =
284+
src.data``). This makes them non-leaf tensors, and a plain
285+
``deepcopy`` raises ``RuntimeError: Only Tensors created explicitly by
286+
the user (graph leaves) support the deepcopy protocol``.
287+
288+
This helper temporarily replaces every non-leaf parameter with a
289+
detached clone, performs the deep-copy, then restores the originals so
290+
the live model is unchanged.
291+
"""
292+
non_leaf: dict[str, Tensor] = {}
293+
for param_name, param in list(module._parameters.items()):
294+
if param is not None and not param.is_leaf:
295+
non_leaf[param_name] = param
296+
module._parameters[param_name] = param.detach().clone()
297+
try:
298+
return deepcopy(module)
299+
finally:
300+
for param_name, original in non_leaf.items():
301+
module._parameters[param_name] = original
302+
278303
@staticmethod
279304
def _copy_param(new: nn.Module, old: nn.Module, name: str) -> None:
280305
# Check if the parameter has been pruned (has _orig suffix)
@@ -376,12 +401,18 @@ def setup(self, trainer: "pl.Trainer", pl_module: LightningModule, stage: str) -
376401
self._parameters_to_prune = self.filter_parameters_to_prune(parameters_to_prune)
377402

378403
if self._use_lottery_ticket_hypothesis:
404+
# Release references from any previous run so their tensors can be
405+
# garbage-collected before we allocate the new copies.
406+
self._original_layers = None
379407
# group modules by id. Each entry has a copy of the initial data
380408
# and a list of the associated parameter names to prune
381409
self._original_layers = {}
382410
for i, (module, name) in enumerate(self._parameters_to_prune):
383411
id_ = id(module)
384-
self._original_layers.setdefault(id_, _LayerRef(data=deepcopy(module), names=[]))
412+
# Use the detach-safe helper so that iterative pruning (where
413+
# parameters may already be non-leaf tensors from a previous
414+
# pruning cycle) does not raise a RuntimeError.
415+
self._original_layers.setdefault(id_, _LayerRef(data=self._deepcopy_for_pruning(module), names=[]))
385416
self._original_layers[id_]["names"].append((i, name))
386417

387418
def _run_pruning(self, current_epoch: int) -> None:

tests/tests_pytorch/callbacks/test_pruning.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,3 +551,38 @@ def forward(self, x):
551551
expected_pruned_count = int(expected_total_params * pruning_amount)
552552
pruned_tolerance = max(1, int(expected_total_params * 0.05))
553553
assert abs(pruned_count - expected_pruned_count) <= pruned_tolerance
554+
555+
556+
def test_iterative_pruning_no_runtime_error(tmp_path):
557+
"""Reusing a ModelPruning callback with use_lottery_ticket_hypothesis across multiple trainer.fit()
558+
calls must not raise RuntimeError due to non-leaf tensors.
559+
560+
Regression test for https://github.com/Lightning-AI/pytorch-lightning/issues/8542
561+
"""
562+
seed_everything(42)
563+
564+
model = BoringModel()
565+
pruning_callback = ModelPruning(
566+
"l1_unstructured",
567+
use_lottery_ticket_hypothesis=True,
568+
use_global_unstructured=True,
569+
make_pruning_permanent=False,
570+
amount=0.2,
571+
)
572+
573+
for _ in range(3):
574+
trainer = Trainer(
575+
default_root_dir=tmp_path,
576+
enable_progress_bar=False,
577+
enable_model_summary=False,
578+
enable_checkpointing=False,
579+
logger=False,
580+
limit_train_batches=2,
581+
limit_val_batches=1,
582+
max_epochs=1,
583+
accelerator="cpu",
584+
callbacks=[pruning_callback],
585+
)
586+
# Must not raise RuntimeError: "Only Tensors created explicitly by the
587+
# user (graph leaves) support the deepcopy protocol"
588+
trainer.fit(model)

0 commit comments

Comments
 (0)