hf-mem 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
hf_mem-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: hf-mem
3
+ Version: 0.0.1
4
+ Summary: A CLI to estimate inference memory requirements for Hugging Face models, written in Python
5
+ Author: Alvaro Bartolome
6
+ Author-email: Alvaro Bartolome <36760800+alvarobartt@users.noreply.github.com>
7
+ License-Expression: MIT
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+
11
+ # `hf-mem`
12
+
13
+ > [!WARNING]
14
+ > `hf-mem` works, but is still experimental. Ideally there should be a minor release anytime soon making things a bit more stable (and aesthetic).
15
+
16
+ `hf-mem` is a CLI to estimate inference memory requirements for Hugging Face models, written in Python. `hf-mem` is lightweight and free for third-party dependencies. It's recommended to run with [`uv`](https://github.com/astral-sh/uv) for a better experience.
17
+
18
+ `hf-mem` lets you estimate the inference requirements to run any model from the Hugging Face Hub, including Transformers, Diffusers and Sentence Transformers models, as well as any model that contains [Safetensors](https://github.com/huggingface/safetensors) compatible weights.
19
+
20
+ ## Usage
21
+
22
+ ```bash
23
+ uvx hf-mem --model-id HuggingFaceTB/SmolLM3-3B
24
+ ```
25
+
26
+ ## References
27
+
28
+ - [Safetensors Metadata parsing](https://huggingface.co/docs/safetensors/en/metadata_parsing)
hf_mem-0.0.1/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # `hf-mem`
2
+
3
+ > [!WARNING]
4
+ > `hf-mem` works, but is still experimental. Ideally there should be a minor release anytime soon making things a bit more stable (and aesthetic).
5
+
6
+ `hf-mem` is a CLI to estimate inference memory requirements for Hugging Face models, written in Python. `hf-mem` is lightweight and free for third-party dependencies. It's recommended to run with [`uv`](https://github.com/astral-sh/uv) for a better experience.
7
+
8
+ `hf-mem` lets you estimate the inference requirements to run any model from the Hugging Face Hub, including Transformers, Diffusers and Sentence Transformers models, as well as any model that contains [Safetensors](https://github.com/huggingface/safetensors) compatible weights.
9
+
10
+ ## Usage
11
+
12
+ ```bash
13
+ uvx hf-mem --model-id HuggingFaceTB/SmolLM3-3B
14
+ ```
15
+
16
+ ## References
17
+
18
+ - [Safetensors Metadata parsing](https://huggingface.co/docs/safetensors/en/metadata_parsing)
@@ -0,0 +1,18 @@
1
+ [project]
2
+ name = "hf-mem"
3
+ version = "0.0.1"
4
+ description = "A CLI to estimate inference memory requirements for Hugging Face models, written in Python"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Alvaro Bartolome", email = "36760800+alvarobartt@users.noreply.github.com" }
8
+ ]
9
+ license = "MIT"
10
+ requires-python = ">=3.10"
11
+ dependencies = []
12
+
13
+ [build-system]
14
+ requires = ["uv_build>=0.9.5,<0.10.0"]
15
+ build-backend = "uv_build"
16
+
17
+ [project.scripts]
18
+ hf-mem = "hf_mem.__init__:main"
@@ -0,0 +1,281 @@
1
+ import argparse
2
+ import json
3
+ import logging
4
+ import math
5
+ import os
6
+ import struct
7
+ import sys
8
+ from concurrent.futures import ThreadPoolExecutor, as_completed
9
+ from functools import reduce
10
+ from typing import Any, Dict, List
11
+ from urllib.request import Request, urlopen
12
+
13
+ # NOTE: Defines the bytes that will be fetched per safetensors file, but the metadata
14
+ # can indeed be larger than that
15
+ MAX_METADATA_SIZE = 100_000
16
+ REQUEST_TIMEOUT = float(os.getenv("REQUEST_TIMEOUT", 30.0))
17
+ MAX_WORKERS = int(os.getenv("MAX_WORKERS", min(32, (os.cpu_count() or 1) + 4)))
18
+
19
+
20
+ logging.basicConfig(
21
+ format="[%(levelname)s] @ %(filename)s:%(lineno)d :: %(message)s",
22
+ level=logging.INFO,
23
+ )
24
+
25
+
26
+ def fetch_safetensors_metadata(url: str) -> Dict[str, Any]:
27
+ headers = {"Range": f"bytes=0-{MAX_METADATA_SIZE}"}
28
+ if token := os.getenv("HF_TOKEN", None):
29
+ headers["Authorization"] = f"Bearer {token}"
30
+
31
+ request = Request(url, headers=headers, method="GET")
32
+ with urlopen(request, timeout=REQUEST_TIMEOUT) as response:
33
+ if response.status not in {200, 206}:
34
+ logging.error(f"REQUEST FAILED WITH {response.status}")
35
+ sys.exit(3)
36
+
37
+ metadata = response.read()
38
+ # NOTE: Parse the first 8 bytes as a little-endian uint64 (size of the metadata)
39
+ metadata_size = struct.unpack("<Q", metadata[:8])[0]
40
+
41
+ if metadata_size < MAX_METADATA_SIZE:
42
+ metadata = metadata[8 : metadata_size + 8]
43
+ return json.loads(metadata)
44
+
45
+ # NOTE: Given that by default we just fetch the first 100_000 bytes, if the content is larger
46
+ # then we simply fetch the remainder again
47
+ metadata = metadata[8 : MAX_METADATA_SIZE + 8]
48
+ headers["Range"] = f"bytes={MAX_METADATA_SIZE}-{metadata_size + 7}"
49
+
50
+ request = Request(url, headers=headers, method="GET")
51
+ with urlopen(request, timeout=REQUEST_TIMEOUT) as response:
52
+ if response.status not in {200, 206}:
53
+ logging.error(f"REQUEST FAILED WITH {response.status}")
54
+ sys.exit(3)
55
+
56
+ metadata += response.read()
57
+ return json.loads(metadata)
58
+
59
+
60
+ def main() -> None:
61
+ parser = argparse.ArgumentParser()
62
+ parser.add_argument(
63
+ "--model-id", required=True, help="Model ID on the Hugging Face Hub"
64
+ )
65
+ parser.add_argument(
66
+ "--revision",
67
+ default="main",
68
+ help="Model revision on the Hugging Face Hub",
69
+ )
70
+
71
+ args = parser.parse_args()
72
+ model_id = args.model_id
73
+ revision = args.revision
74
+
75
+ url = f"https://huggingface.co/api/models/{model_id}/tree/{revision}?recursive=true"
76
+
77
+ headers = {}
78
+ if token := os.getenv("HF_TOKEN", None):
79
+ headers["Authorization"] = f"Bearer {token}"
80
+
81
+ request = Request(url, headers=headers, method="GET")
82
+ with urlopen(request, timeout=REQUEST_TIMEOUT) as response:
83
+ if response.status != 200:
84
+ logging.error(f"REQUEST FAILED WITH {response.status}")
85
+ sys.exit(3)
86
+
87
+ files = [
88
+ f["path"]
89
+ for f in json.loads(response.read())
90
+ if f.get("path", None) is not None and f.get("type", None) == "file"
91
+ ]
92
+
93
+ if "model.safetensors" in files:
94
+ url = f"https://huggingface.co/{model_id}/resolve/{revision}/model.safetensors"
95
+ metadata = fetch_safetensors_metadata(url=url)
96
+ elif "model.safetensors.index.json" in files:
97
+ # TODO: We could eventually skip this request in favour of a greedy approach on trying to pull all the
98
+ # files following the formatting `model-00000-of-00000.safetensors`
99
+ url = f"https://huggingface.co/{model_id}/resolve/{revision}/model.safetensors.index.json"
100
+ request = Request(url, headers=headers, method="GET")
101
+ with urlopen(request, timeout=REQUEST_TIMEOUT) as response:
102
+ if response.status != 200:
103
+ logging.error(f"REQUEST FAILED WITH {response.status}")
104
+ sys.exit(3)
105
+
106
+ urls = {
107
+ f"https://huggingface.co/{model_id}/resolve/{revision}/{f}"
108
+ for _, f in json.loads(response.read())["weight_map"].items()
109
+ }
110
+
111
+ metadatas = []
112
+ with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
113
+ futures = {
114
+ executor.submit(fetch_safetensors_metadata, url): url for url in urls
115
+ }
116
+ for future in as_completed(futures):
117
+ try:
118
+ metadatas.append(future.result())
119
+ except Exception as e:
120
+ logging.error(f'ANY OF THE FUTURES FAILED "{e}"')
121
+ sys.exit(4)
122
+
123
+ metadata = reduce(lambda acc, metadata: acc | metadata, metadatas, {})
124
+ elif "model_index.json" in files:
125
+ url = f"https://huggingface.co/{model_id}/resolve/{revision}/model_index.json"
126
+ request = Request(url, headers=headers, method="GET")
127
+ with urlopen(request, timeout=REQUEST_TIMEOUT) as response:
128
+ if response.status != 200:
129
+ logging.error(f"REQUEST FAILED WITH {response.status}")
130
+ sys.exit(3)
131
+
132
+ paths = {
133
+ k
134
+ for k, _ in json.loads(response.read()).items()
135
+ if not k.startswith("_") # e.g. `_class_name` or `_diffusers_version`
136
+ }
137
+
138
+ path_urls: Dict[str, List[str]] = {}
139
+ for path in paths:
140
+ if path not in path_urls:
141
+ path_urls[path] = []
142
+
143
+ if f"{path}/diffusion_pytorch_model.safetensors" in files:
144
+ path_urls[path].append(
145
+ f"https://huggingface.co/{model_id}/resolve/{revision}/{path}/diffusion_pytorch_model.safetensors"
146
+ )
147
+ elif f"{path}/model.safetensors" in files:
148
+ path_urls[path].append(
149
+ f"https://huggingface.co/{model_id}/resolve/{revision}/{path}/model.safetensors"
150
+ )
151
+ elif f"{path}/diffusion_pytorch_model.safetensors.index.json" in files:
152
+ url = f"https://huggingface.co/{model_id}/resolve/{revision}/{path}/diffusion_pytorch_model.safetensors.index.json"
153
+ request = Request(url, headers=headers, method="GET")
154
+ with urlopen(request, timeout=REQUEST_TIMEOUT) as response:
155
+ if response.status != 200:
156
+ logging.error(f"REQUEST FAILED WITH {response.status}")
157
+ sys.exit(3)
158
+
159
+ path_urls[path] = list({
160
+ f"https://huggingface.co/{model_id}/resolve/{revision}/{path}/{f}"
161
+ for _, f in json.loads(response.read())[
162
+ "weight_map"
163
+ ].items()
164
+ })
165
+ elif f"{path}/model.safetensors.index.json" in files:
166
+ url = f"https://huggingface.co/{model_id}/resolve/{revision}/{path}/model.safetensors.index.json"
167
+ request = Request(url, headers=headers, method="GET")
168
+ with urlopen(request, timeout=REQUEST_TIMEOUT) as response:
169
+ if response.status != 200:
170
+ logging.error(f"REQUEST FAILED WITH {response.status}")
171
+ sys.exit(3)
172
+
173
+ path_urls[path] = list({
174
+ f"https://huggingface.co/{model_id}/resolve/{revision}/{path}/{f}"
175
+ for _, f in json.loads(response.read())[
176
+ "weight_map"
177
+ ].items()
178
+ })
179
+
180
+ path_metadatas: Dict[str, List[Dict[str, Any]]] = {}
181
+ with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
182
+ futures = {
183
+ executor.submit(fetch_safetensors_metadata, url): path
184
+ for path, urls in path_urls.items()
185
+ for url in urls
186
+ if len(urls) > 0
187
+ }
188
+ for future in as_completed(futures):
189
+ path = futures[future]
190
+ try:
191
+ if path not in path_metadatas:
192
+ path_metadatas[path] = []
193
+ path_metadatas[path].append(future.result())
194
+ except Exception as e:
195
+ logging.error(f'ANY OF THE FUTURES FAILED "{e}"')
196
+ sys.exit(4)
197
+
198
+ metadatas: Dict[str, Dict[str, Any]] = {
199
+ path: reduce(lambda acc, metadata: acc | metadata, metadatas, {})
200
+ for path, metadatas in path_metadatas.items()
201
+ }
202
+
203
+ for path, metadata in metadatas.items():
204
+ ppdt = {}
205
+ for key, value in metadata.items():
206
+ if key in {"__metadata__"}:
207
+ continue
208
+ if value["dtype"] not in ppdt:
209
+ ppdt[value["dtype"]] = 0
210
+ ppdt[value["dtype"]] += math.prod(value["shape"])
211
+
212
+ total = 0
213
+ for dtype, count in ppdt.items():
214
+ logging.info(f"[PATH={path}] DTYPE={dtype}, COUNT={count}\n")
215
+
216
+ match dtype:
217
+ case "F64" | "I64" | "U64":
218
+ dtype_b = 8
219
+ case "F32" | "I32" | "U32":
220
+ dtype_b = 4
221
+ case "F16" | "BF16" | "I16" | "U16":
222
+ dtype_b = 2
223
+ case "F8_E5M2" | "F8_E4M3" | "I8" | "U8":
224
+ dtype_b = 1
225
+ case _:
226
+ logging.error(f"DTYPE={dtype} NOT HANDLED")
227
+ sys.exit(2)
228
+
229
+ total += count * dtype_b
230
+
231
+ logging.info(f"[PATH={path}] TOTAL VRAM={total} (IN BYTES)")
232
+ logging.info(
233
+ f"[PATH={path}] TOTAL VRAM={total / (1024**2):.2f} (IN MEGABYTES)"
234
+ )
235
+ logging.info(
236
+ f"[PATH={path}] TOTAL VRAM={total / (1024**3):.2f} (IN GIGABYTES)\n"
237
+ )
238
+
239
+ sys.exit(0)
240
+ else:
241
+ logging.error(
242
+ "NONE OF `model.safetensors`, `model.safetensors.index.json`, `model_index.json` HAS BEEN FOUND"
243
+ )
244
+ sys.exit(1)
245
+
246
+ ppdt = {}
247
+ for key, value in metadata.items():
248
+ if key in {"__metadata__"}:
249
+ continue
250
+ if value["dtype"] not in ppdt:
251
+ ppdt[value["dtype"]] = 0
252
+ ppdt[value["dtype"]] += math.prod(value["shape"])
253
+
254
+ total = 0
255
+ for dtype, count in ppdt.items():
256
+ logging.info(f"DTYPE={dtype}, COUNT={count}\n")
257
+
258
+ match dtype:
259
+ case "F64" | "I64" | "U64":
260
+ dtype_b = 8
261
+ case "F32" | "I32" | "U32":
262
+ dtype_b = 4
263
+ case "F16" | "BF16" | "I16" | "U16":
264
+ dtype_b = 2
265
+ case "F8_E5M2" | "F8_E4M3" | "I8" | "U8":
266
+ dtype_b = 1
267
+ case _:
268
+ logging.error(f"DTYPE={dtype} NOT HANDLED")
269
+ sys.exit(2)
270
+
271
+ bytes_count = count * dtype_b
272
+ total += bytes_count
273
+ logging.info(f"VRAM={bytes_count} (IN BYTES)")
274
+ logging.info(f"VRAM={bytes_count / (1024**2):.2f} (IN MEGABYTES)")
275
+ logging.info(f"VRAM={bytes_count / (1024**3):.2f} (IN GIGABYTES)\n")
276
+
277
+ logging.info(f"TOTAL VRAM={total} (IN BYTES)")
278
+ logging.info(f"TOTAL VRAM={total / (1024**2):.2f} (IN MEGABYTES)")
279
+ logging.info(f"TOTAL VRAM={total / (1024**3):.2f} (IN GIGABYTES)")
280
+
281
+ sys.exit(0)
File without changes