estimate.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. #!/usr/bin/env python
  2. # Copyright 2023 The HuggingFace Team. All rights reserved.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. from typing import Optional
  16. import torch
  17. from huggingface_hub import model_info
  18. from huggingface_hub.utils import GatedRepoError, RepositoryNotFoundError
  19. from accelerate import init_empty_weights
  20. from accelerate.commands.utils import CustomArgumentParser
  21. from accelerate.utils import (
  22. calculate_maximum_sizes,
  23. convert_bytes,
  24. is_timm_available,
  25. is_transformers_available,
  26. )
  27. if is_transformers_available():
  28. import transformers
  29. from transformers import AutoConfig, AutoModel
  30. if is_timm_available():
  31. import timm
  32. def verify_on_hub(repo: str, token: Optional[str] = None):
  33. "Verifies that the model is on the hub and returns the model info."
  34. try:
  35. return model_info(repo, token=token)
  36. except (OSError, GatedRepoError):
  37. return "gated"
  38. except RepositoryNotFoundError:
  39. return "repo"
  40. def check_has_model(error):
  41. """
  42. Checks what library spawned `error` when a model is not found
  43. """
  44. if is_timm_available() and isinstance(error, RuntimeError) and "Unknown model" in error.args[0]:
  45. return "timm"
  46. elif (
  47. is_transformers_available()
  48. and isinstance(error, OSError)
  49. and "does not appear to have a file named" in error.args[0]
  50. ):
  51. return "transformers"
  52. else:
  53. return "unknown"
  54. def create_empty_model(
  55. model_name: str, library_name: str, trust_remote_code: bool = False, access_token: Optional[str] = None
  56. ):
  57. """
  58. Creates an empty model in full precision from its parent library on the `Hub` to calculate the overall memory
  59. consumption.
  60. Args:
  61. model_name (`str`):
  62. The model name on the Hub
  63. library_name (`str`):
  64. The library the model has an integration with, such as `transformers`. Will be used if `model_name` has no
  65. metadata on the Hub to determine the library.
  66. trust_remote_code (`bool`, `optional`, defaults to `False`):
  67. Whether or not to allow for custom models defined on the Hub in their own modeling files. This option
  68. should only be set to `True` for repositories you trust and in which you have read the code, as it will
  69. execute code present on the Hub on your local machine.
  70. access_token (`str`, `optional`, defaults to `None`):
  71. The access token to use to access private or gated models on the Hub. (for use on the Gradio app)
  72. Returns:
  73. `torch.nn.Module`: The torch model that has been initialized on the `meta` device.
  74. """
  75. model_info = verify_on_hub(model_name, access_token)
  76. # Simplified errors
  77. if model_info == "gated":
  78. raise GatedRepoError(
  79. f"Repo for model `{model_name}` is gated. You must be authenticated to access it. Please run `huggingface-cli login`."
  80. )
  81. elif model_info == "repo":
  82. raise RepositoryNotFoundError(
  83. f"Repo for model `{model_name}` does not exist on the Hub. If you are trying to access a private repo,"
  84. " make sure you are authenticated via `huggingface-cli login` and have access."
  85. )
  86. if library_name is None:
  87. library_name = getattr(model_info, "library_name", False)
  88. if not library_name:
  89. raise ValueError(
  90. f"Model `{model_name}` does not have any library metadata on the Hub, please manually pass in a `--library_name` to use (such as `transformers`)"
  91. )
  92. if library_name == "transformers":
  93. if not is_transformers_available():
  94. raise ImportError(
  95. f"To check `{model_name}`, `transformers` must be installed. Please install it via `pip install transformers`"
  96. )
  97. print(f"Loading pretrained config for `{model_name}` from `transformers`...")
  98. if model_info.config is None:
  99. raise RuntimeError(f"Tried to load `{model_name}` with `transformers` but it does not have any metadata.")
  100. auto_map = model_info.config.get("auto_map", False)
  101. config = AutoConfig.from_pretrained(model_name, trust_remote_code=trust_remote_code, token=access_token)
  102. with init_empty_weights():
  103. # remote code could specify a specific `AutoModel` class in the `auto_map`
  104. constructor = AutoModel
  105. if isinstance(auto_map, dict):
  106. value = None
  107. for key in auto_map.keys():
  108. if key.startswith("AutoModelFor"):
  109. value = key
  110. break
  111. if value is not None:
  112. constructor = getattr(transformers, value)
  113. # we need to pass the dtype, otherwise it is going to use the torch_dtype that is saved in the config
  114. model = constructor.from_config(config, torch_dtype=torch.float32, trust_remote_code=trust_remote_code)
  115. elif library_name == "timm":
  116. if not is_timm_available():
  117. raise ImportError(
  118. f"To check `{model_name}`, `timm` must be installed. Please install it via `pip install timm`"
  119. )
  120. print(f"Loading pretrained config for `{model_name}` from `timm`...")
  121. with init_empty_weights():
  122. model = timm.create_model(model_name, pretrained=False)
  123. else:
  124. raise ValueError(
  125. f"Library `{library_name}` is not supported yet, please open an issue on GitHub for us to add support."
  126. )
  127. return model
  128. def create_ascii_table(headers: list, rows: list, title: str):
  129. "Creates a pretty table from a list of rows, minimal version of `tabulate`."
  130. sep_char, in_between = "│", "─"
  131. column_widths = []
  132. for i in range(len(headers)):
  133. column_values = [row[i] for row in rows] + [headers[i]]
  134. max_column_width = max(len(value) for value in column_values)
  135. column_widths.append(max_column_width)
  136. formats = [f"%{column_widths[i]}s" for i in range(len(rows[0]))]
  137. pattern = f"{sep_char}{sep_char.join(formats)}{sep_char}"
  138. diff = 0
  139. def make_row(left_char, middle_char, right_char):
  140. return f"{left_char}{middle_char.join([in_between * n for n in column_widths])}{in_between * diff}{right_char}"
  141. separator = make_row("├", "┼", "┤")
  142. if len(title) > sum(column_widths):
  143. diff = abs(len(title) - len(separator))
  144. column_widths[-1] += diff
  145. # Update with diff
  146. separator = make_row("├", "┼", "┤")
  147. initial_rows = [
  148. make_row("┌", in_between, "┐"),
  149. f"{sep_char}{title.center(len(separator) - 2)}{sep_char}",
  150. make_row("├", "┬", "┤"),
  151. ]
  152. table = "\n".join(initial_rows) + "\n"
  153. column_widths[-1] += diff
  154. centered_line = [text.center(column_widths[i]) for i, text in enumerate(headers)]
  155. table += f"{pattern % tuple(centered_line)}\n{separator}\n"
  156. for i, line in enumerate(rows):
  157. centered_line = [t.center(column_widths[i]) for i, t in enumerate(line)]
  158. table += f"{pattern % tuple(centered_line)}\n"
  159. table += f"└{'┴'.join([in_between * n for n in column_widths])}┘"
  160. return table
  161. def estimate_command_parser(subparsers=None):
  162. if subparsers is not None:
  163. parser = subparsers.add_parser("estimate-memory")
  164. else:
  165. parser = CustomArgumentParser(description="Model size estimator for fitting a model onto CUDA memory.")
  166. parser.add_argument("model_name", type=str, help="The model name on the Hugging Face Hub.")
  167. parser.add_argument(
  168. "--library_name",
  169. type=str,
  170. help="The library the model has an integration with, such as `transformers`, needed only if this information is not stored on the Hub.",
  171. choices=["timm", "transformers"],
  172. )
  173. parser.add_argument(
  174. "--dtypes",
  175. type=str,
  176. nargs="+",
  177. default=["float32", "float16", "int8", "int4"],
  178. help="The dtypes to use for the model, must be one (or many) of `float32`, `float16`, `int8`, and `int4`",
  179. choices=["float32", "float16", "int8", "int4"],
  180. )
  181. parser.add_argument(
  182. "--trust_remote_code",
  183. action="store_true",
  184. help="""Whether or not to allow for custom models defined on the Hub in their own modeling files. This flag
  185. should only be used for repositories you trust and in which you have read the code, as it will execute
  186. code present on the Hub on your local machine.""",
  187. default=False,
  188. )
  189. if subparsers is not None:
  190. parser.set_defaults(func=estimate_command)
  191. return parser
  192. def estimate_training_usage(bytes: int, mixed_precision: str, msamp_config: Optional[str] = None) -> dict:
  193. """
  194. Given an amount of `bytes` and `mixed_precision`, calculates how much training memory is needed for a batch size of
  195. 1.
  196. Args:
  197. bytes (`int`):
  198. The size of the model being trained.
  199. mixed_precision (`str`):
  200. The mixed precision that would be ran.
  201. msamp_config (`str`):
  202. The msamp config to estimate the training memory for if `mixed_precision` is set to `"fp8"`.
  203. """
  204. memory_sizes = {"model": -1, "optimizer": -1, "gradients": -1, "step": -1}
  205. fp32_size = bytes
  206. fp16_size = bytes // 2
  207. if mixed_precision == "float32":
  208. memory_sizes["model"] = fp32_size
  209. memory_sizes["gradients"] = fp32_size
  210. memory_sizes["optimizer"] = fp32_size * 2
  211. memory_sizes["step"] = fp32_size * 4
  212. elif mixed_precision in ("float16", "bfloat16") or (mixed_precision == "fp8" and msamp_config is None):
  213. # With native `TransformersEngine`, there is no memory savings with FP8
  214. # With mixed precision training, the model has weights stored
  215. # in FP16 and FP32
  216. memory_sizes["model"] = fp32_size
  217. # 1.5 from weight gradient + computation (GEMM)
  218. memory_sizes["gradients"] = fp32_size + fp16_size
  219. # 2x from optimizer states
  220. memory_sizes["optimizer"] = fp32_size * 2 # Optimizer states
  221. memory_sizes["step"] = memory_sizes["optimizer"]
  222. return memory_sizes
  223. def gather_data(args):
  224. "Creates an empty model and gathers the data for the sizes"
  225. try:
  226. model = create_empty_model(
  227. args.model_name, library_name=args.library_name, trust_remote_code=args.trust_remote_code
  228. )
  229. except (RuntimeError, OSError) as e:
  230. library = check_has_model(e)
  231. if library != "unknown":
  232. raise RuntimeError(
  233. f"Tried to load `{args.model_name}` with `{library}` but a possible model to load was not found inside the repo."
  234. )
  235. raise e
  236. total_size, largest_layer = calculate_maximum_sizes(model)
  237. data = []
  238. for dtype in args.dtypes:
  239. dtype_total_size = total_size
  240. dtype_largest_layer = largest_layer[0]
  241. dtype_training_size = estimate_training_usage(dtype_total_size, dtype)
  242. if dtype == "float16":
  243. dtype_total_size /= 2
  244. dtype_largest_layer /= 2
  245. elif dtype == "int8":
  246. dtype_total_size /= 4
  247. dtype_largest_layer /= 4
  248. elif dtype == "int4":
  249. dtype_total_size /= 8
  250. dtype_largest_layer /= 8
  251. data.append([dtype, dtype_largest_layer, dtype_total_size, dtype_training_size])
  252. return data
  253. def estimate_command(args):
  254. data = gather_data(args)
  255. for row in data:
  256. for i, item in enumerate(row):
  257. if isinstance(item, (int, float)):
  258. row[i] = convert_bytes(item)
  259. elif isinstance(item, dict):
  260. training_usage = max(item.values())
  261. row[i] = convert_bytes(training_usage) if training_usage != -1 else "N/A"
  262. headers = ["dtype", "Largest Layer", "Total Size", "Training using Adam"]
  263. title = f"Memory Usage for loading `{args.model_name}`"
  264. table = create_ascii_table(headers, data, title)
  265. print(table)
  266. def main():
  267. parser = estimate_command_parser()
  268. args = parser.parse_args()
  269. estimate_command(args)
  270. if __name__ == "__main__":
  271. main()