offload.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. # Copyright 2022 The HuggingFace Team. All rights reserved.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import json
  15. import os
  16. from collections.abc import Mapping
  17. from typing import Optional, Union
  18. import numpy as np
  19. import torch
  20. from safetensors import safe_open
  21. def offload_weight(weight, weight_name, offload_folder, index=None):
  22. dtype = None
  23. # Check the string instead of the dtype to be compatible with versions of PyTorch that don't have bfloat16.
  24. if str(weight.dtype) == "torch.bfloat16":
  25. # Need to reinterpret the underlined data as int16 since NumPy does not handle bfloat16s.
  26. weight = weight.view(torch.int16)
  27. dtype = "bfloat16"
  28. array = weight.cpu().numpy()
  29. tensor_file = os.path.join(offload_folder, f"{weight_name}.dat")
  30. if index is not None:
  31. if dtype is None:
  32. dtype = str(array.dtype)
  33. index[weight_name] = {"dtype": dtype, "shape": list(array.shape)}
  34. if array.ndim == 0:
  35. array = array[None]
  36. file_array = np.memmap(tensor_file, dtype=array.dtype, mode="w+", shape=array.shape)
  37. file_array[:] = array[:]
  38. file_array.flush()
  39. return index
  40. def load_offloaded_weight(weight_file, weight_info):
  41. shape = tuple(weight_info["shape"])
  42. if shape == ():
  43. # NumPy memory-mapped arrays can't have 0 dims so it was saved as 1d tensor
  44. shape = (1,)
  45. dtype = weight_info["dtype"]
  46. if dtype == "bfloat16":
  47. # NumPy does not support bfloat16 so this was saved as a int16
  48. dtype = "int16"
  49. weight = np.memmap(weight_file, dtype=dtype, shape=shape, mode="r")
  50. if len(weight_info["shape"]) == 0:
  51. weight = weight[0]
  52. weight = torch.tensor(weight)
  53. if weight_info["dtype"] == "bfloat16":
  54. weight = weight.view(torch.bfloat16)
  55. return weight
  56. def save_offload_index(index, offload_folder):
  57. if index is None or len(index) == 0:
  58. # Nothing to save
  59. return
  60. offload_index_file = os.path.join(offload_folder, "index.json")
  61. if os.path.isfile(offload_index_file):
  62. with open(offload_index_file, encoding="utf-8") as f:
  63. current_index = json.load(f)
  64. else:
  65. current_index = {}
  66. current_index.update(index)
  67. with open(offload_index_file, "w", encoding="utf-8") as f:
  68. json.dump(current_index, f, indent=2)
  69. def offload_state_dict(save_dir: Union[str, os.PathLike], state_dict: dict[str, torch.Tensor]):
  70. """
  71. Offload a state dict in a given folder.
  72. Args:
  73. save_dir (`str` or `os.PathLike`):
  74. The directory in which to offload the state dict.
  75. state_dict (`Dict[str, torch.Tensor]`):
  76. The dictionary of tensors to offload.
  77. """
  78. os.makedirs(save_dir, exist_ok=True)
  79. index = {}
  80. for name, parameter in state_dict.items():
  81. index = offload_weight(parameter, name, save_dir, index=index)
  82. # Update index
  83. save_offload_index(index, save_dir)
  84. class PrefixedDataset(Mapping):
  85. """
  86. Will access keys in a given dataset by adding a prefix.
  87. Args:
  88. dataset (`Mapping`): Any map with string keys.
  89. prefix (`str`): A prefix to add when trying to access any element in the underlying dataset.
  90. """
  91. def __init__(self, dataset: Mapping, prefix: str):
  92. self.dataset = dataset
  93. self.prefix = prefix
  94. def __getitem__(self, key):
  95. return self.dataset[f"{self.prefix}{key}"]
  96. def __iter__(self):
  97. return iter([key for key in self.dataset if key.startswith(self.prefix)])
  98. def __len__(self):
  99. return len(self.dataset)
  100. class OffloadedWeightsLoader(Mapping):
  101. """
  102. A collection that loads weights stored in a given state dict or memory-mapped on disk.
  103. Args:
  104. state_dict (`Dict[str, torch.Tensor]`, *optional*):
  105. A dictionary parameter name to tensor.
  106. save_folder (`str` or `os.PathLike`, *optional*):
  107. The directory in which the weights are stored (by `offload_state_dict` for instance).
  108. index (`Dict`, *optional*):
  109. A dictionary from weight name to their information (`dtype`/ `shape` or safetensors filename). Will default
  110. to the index saved in `save_folder`.
  111. """
  112. def __init__(
  113. self,
  114. state_dict: Optional[dict[str, torch.Tensor]] = None,
  115. save_folder: Optional[Union[str, os.PathLike]] = None,
  116. index: Optional[Mapping] = None,
  117. device=None,
  118. ):
  119. if state_dict is None and save_folder is None and index is None:
  120. raise ValueError("Need either a `state_dict`, a `save_folder` or an `index` containing offloaded weights.")
  121. self.state_dict = {} if state_dict is None else state_dict
  122. self.save_folder = save_folder
  123. if index is None and save_folder is not None:
  124. with open(os.path.join(save_folder, "index.json")) as f:
  125. index = json.load(f)
  126. self.index = {} if index is None else index
  127. self.all_keys = list(self.state_dict.keys())
  128. self.all_keys.extend([key for key in self.index if key not in self.all_keys])
  129. self.device = device
  130. def __getitem__(self, key: str):
  131. # State dict gets priority
  132. if key in self.state_dict:
  133. return self.state_dict[key]
  134. weight_info = self.index[key]
  135. if weight_info.get("safetensors_file") is not None:
  136. device = "cpu" if self.device is None else self.device
  137. tensor = None
  138. try:
  139. with safe_open(weight_info["safetensors_file"], framework="pt", device=device) as f:
  140. tensor = f.get_tensor(weight_info.get("weight_name", key))
  141. except TypeError:
  142. # if failed to get_tensor on the device, such as bf16 on mps, try to load it on CPU first
  143. with safe_open(weight_info["safetensors_file"], framework="pt", device="cpu") as f:
  144. tensor = f.get_tensor(weight_info.get("weight_name", key))
  145. if "dtype" in weight_info:
  146. tensor = tensor.to(getattr(torch, weight_info["dtype"]))
  147. if tensor.device != torch.device(device):
  148. tensor = tensor.to(device)
  149. return tensor
  150. weight_file = os.path.join(self.save_folder, f"{key}.dat")
  151. return load_offloaded_weight(weight_file, weight_info)
  152. def __iter__(self):
  153. return iter(self.all_keys)
  154. def __len__(self):
  155. return len(self.all_keys)
  156. def extract_submodules_state_dict(state_dict: dict[str, torch.Tensor], submodule_names: list[str]):
  157. """
  158. Extract the sub state-dict corresponding to a list of given submodules.
  159. Args:
  160. state_dict (`Dict[str, torch.Tensor]`): The state dict to extract from.
  161. submodule_names (`List[str]`): The list of submodule names we want to extract.
  162. """
  163. result = {}
  164. for module_name in submodule_names:
  165. # We want to catch module_name parameter (module_name.xxx) or potentially module_name, but not any of the
  166. # submodules that could being like module_name (transformers.h.1 and transformers.h.10 for instance)
  167. result.update(
  168. {
  169. key: param
  170. for key, param in state_dict.items()
  171. if key == module_name or key.startswith(module_name + ".")
  172. }
  173. )
  174. return result