44# NOTE: This file has been modified by Intel Corporation.
55"""Torch checkpoint module."""
66
7- from collections import OrderedDict
7+ from collections import OrderedDict , defaultdict
88from pathlib import Path
99from typing import Any
1010
@@ -40,18 +40,16 @@ def save(
4040
4141 This method handles both individual torch modules and pipelines containing modules.
4242 For individual modules, it directly uses the module's state_dict() method.
43- For pipelines, it extracts state dictionaries from all tuned modules in the pipeline.
44-
45- The pipeline is expected to be an object with attributes that are tuned modules (WrapperModule instances).
46- Only tuned modules are saved in the state dictionary, as regular torch modules don't have tuning information.
47-
48- If no tuned modules are found in the pipeline, a ValueError is raised.
43+ For a pipeline, it saves each tuned module (WrapperModule instance) under its dotted path,
44+ e.g. `"vae.decoder"`. Modules that are not tuned are not saved.
4945
5046 Args:
5147 module_or_pipeline: The module or pipeline to save. This can be either a torch.nn.Module
52- or a pipeline object with module attributes. For pipelines, only tuned modules
53- (WrapperModule instances) will be saved.
48+ or a pipeline object with module attributes.
5449 path: The path where the module state should be saved.
50+
51+ Raises:
52+ ValueError: If the pipeline contains no tuned modules.
5553 """
5654 if isinstance (module_or_pipeline , TorchModule ):
5755 state_dict = module_or_pipeline .state_dict ()
@@ -107,39 +105,55 @@ def load(
107105 return module_or_pipeline
108106
109107 @staticmethod
110- def get_pipeline_modules (pipeline : Any ) -> dict [str , torch . nn . Module ]:
108+ def get_pipeline_modules (pipeline : Any ) -> dict [str , TorchModule | WrapperModule ]:
111109 """Get the modules from the pipeline.
112110
113111 Args:
114112 pipeline: The pipeline object to extract modules from.
115113
116114 Returns:
117- dict: A dictionary mapping attribute names to Module objects.
115+ dict: A dictionary mapping attribute names to modules. Tuned modules nested in a module
116+ attribute are added under their dotted path, e.g. `"vae.decoder"`.
118117 """
119- modules = {}
118+ modules : dict [str , TorchModule | WrapperModule ] = {}
119+
120+ def add_wrapped_children (module : TorchModule , prefix : str ) -> None :
121+ # `named_modules()` cannot be used: a wrapper forwards it to the wrapped module, which yields itself.
122+ for child_name , child in module .named_children ():
123+ child_path = f"{ prefix } .{ child_name } "
124+ if isinstance (child , WrapperModule ):
125+ modules [child_path ] = child
126+ else :
127+ add_wrapped_children (child , child_path )
120128
121129 for name , attr in vars (pipeline ).items ():
122- if isinstance (attr , ( torch . nn . Module , WrapperModule ) ):
130+ if isinstance (attr , WrapperModule ):
123131 modules [name ] = attr
132+ elif isinstance (attr , TorchModule ):
133+ modules [name ] = attr
134+ add_wrapped_children (attr , name )
124135
125136 return modules
126137
127138 @staticmethod
128139 def state_dict_from_pipeline (pipeline : Any ) -> dict [str , Any ]:
129140 """Extract the state_dict from a pipeline (e.g. HF Diffusers pipeline).
130141
142+ Each tuned module is stored once under its dotted path, also when the pipeline reaches it by several paths.
143+
131144 Args:
132145 pipeline: The pipeline to extract the state_dict from.
133146
134147 Returns:
135148 dict: The state_dict from the pipeline.
136149 """
137150 state_dict = OrderedDict ()
138- pipeline_modules = TorchCheckpoint . get_pipeline_modules ( pipeline )
151+ saved_module_ids = set ( )
139152
140- for name , module in pipeline_modules .items ():
141- if isinstance (module , WrapperModule ):
153+ for name , module in TorchCheckpoint . get_pipeline_modules ( pipeline ) .items ():
154+ if isinstance (module , WrapperModule ) and id ( module ) not in saved_module_ids :
142155 state_dict [name ] = module .state_dict ()
156+ saved_module_ids .add (id (module ))
143157 if not state_dict :
144158 raise ValueError ("No tuned modules found in the pipeline" )
145159 return state_dict
@@ -249,13 +263,35 @@ def replace_tuned_module(
249263 final_module .load_state_dict (state_dict , strict = False )
250264 return final_module
251265
266+ @staticmethod
267+ def get_pipeline_module_paths (pipeline : Any ) -> dict [int , list [tuple [Any , str ]]]:
268+ """Get every place where the pipeline holds a module.
269+
270+ Args:
271+ pipeline: The pipeline to search.
272+
273+ Returns:
274+ dict: Maps `id()` of a module to the `(owner, attribute name)` pairs that hold it.
275+ """
276+ module_paths : dict [int , list [tuple [Any , str ]]] = defaultdict (list )
277+
278+ for name , module in vars (pipeline ).items ():
279+ if isinstance (module , (TorchModule , WrapperModule )):
280+ module_paths [id (module )].append ((pipeline , name ))
281+ if isinstance (module , TorchModule ):
282+ for _ , parent in module .named_modules (remove_duplicate = False ):
283+ for child_name , child in parent .named_children ():
284+ module_paths [id (child )].append ((parent , child_name ))
285+
286+ return module_paths
287+
252288 @staticmethod
253289 def load_state_dict_for_pipeline (pipeline , state_dict , device_map : dict [str , torch .device ]):
254290 """Load a state dictionary into a pipeline.
255291
256- This method loads the state dictionary into the pipeline by matching module names
257- in the state dictionary with attributes in the pipeline. Each matched module is
258- replaced with the tuned module.
292+ This method loads the state dictionary into the pipeline by matching dotted module paths
293+ in the state dictionary with modules in the pipeline. Each matched module is replaced
294+ with the tuned module in every place where the pipeline holds it .
259295
260296 Args:
261297 pipeline: The pipeline to load the state dictionary into.
@@ -270,18 +306,30 @@ def load_state_dict_for_pipeline(pipeline, state_dict, device_map: dict[str, tor
270306 if a matched attribute is not a torch.nn.Module or WrapperModule.
271307 """
272308 matched_modules = set ()
309+ module_paths = TorchCheckpoint .get_pipeline_module_paths (pipeline )
310+ restored_module_ids = set ()
273311 for name , module_state_dict in state_dict .items ():
274- if hasattr (pipeline , name ):
275- module = getattr (pipeline , name )
276- if isinstance (module , (torch .nn .Module , WrapperModule )):
277- matched_modules .add (name )
278- setattr (
279- pipeline ,
280- name ,
281- TorchCheckpoint .load_state_dict_for_module (module , module_state_dict , device_map , name ),
282- )
283- else :
284- raise ValueError (f"Module { name } is not a torch.nn.Module or WrapperModule" )
312+ * parent_path , attribute = name .split ("." )
313+ parent = pipeline
314+ for component in parent_path :
315+ parent = getattr (parent , component , None )
316+ if parent is None or not hasattr (parent , attribute ):
317+ continue
318+
319+ module = getattr (parent , attribute )
320+ if not isinstance (module , (torch .nn .Module , WrapperModule )):
321+ raise ValueError (f"Module { name } is not a torch.nn.Module or WrapperModule" )
322+
323+ matched_modules .add (name )
324+ if id (module ) in restored_module_ids :
325+ # Older checkpoints store an aliased module under each of its paths.
326+ device_map .pop (name , None )
327+ continue
328+
329+ restored_module = TorchCheckpoint .load_state_dict_for_module (module , module_state_dict , device_map , name )
330+ for owner , child_name in module_paths [id (module )]:
331+ setattr (owner , child_name , restored_module )
332+ restored_module_ids .add (id (restored_module ))
285333
286334 unmatched_keys = set (state_dict .keys ()).difference (matched_modules )
287335 if unmatched_keys :
0 commit comments