hf-mem 0.0.3__py3-none-any.whl
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/__init__.py +0 -0
- hf_mem/cli.py +195 -0
- hf_mem/print.py +290 -0
- hf_mem/py.typed +0 -0
- hf_mem-0.0.3.dist-info/METADATA +36 -0
- hf_mem-0.0.3.dist-info/RECORD +9 -0
- hf_mem-0.0.3.dist-info/WHEEL +4 -0
- hf_mem-0.0.3.dist-info/entry_points.txt +3 -0
- hf_mem-0.0.3.dist-info/licenses/LICENSE +25 -0
hf_mem/__init__.py
ADDED
|
File without changes
|
hf_mem/cli.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
import json
|
|
4
|
+
import math
|
|
5
|
+
import os
|
|
6
|
+
import struct
|
|
7
|
+
import sys
|
|
8
|
+
from functools import reduce
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from hf_mem.print import print_report_for_diffusers, print_report_for_transformers
|
|
14
|
+
|
|
15
|
+
# NOTE: Defines the bytes that will be fetched per safetensors file, but the metadata
|
|
16
|
+
# can indeed be larger than that
|
|
17
|
+
MAX_METADATA_SIZE = 100_000
|
|
18
|
+
REQUEST_TIMEOUT = float(os.getenv("REQUEST_TIMEOUT", 10.0))
|
|
19
|
+
MAX_CONCURRENCY = int(os.getenv("MAX_WORKERS", min(32, (os.cpu_count() or 1) + 4)))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# NOTE: Return type-hint set to `Any`, but it will only be a JSON-compatible object
|
|
23
|
+
async def get_json_file(
|
|
24
|
+
client: httpx.AsyncClient, url: str, headers: Optional[Dict[str, str]] = None
|
|
25
|
+
) -> Any:
|
|
26
|
+
response = await client.get(url, headers=headers, timeout=REQUEST_TIMEOUT)
|
|
27
|
+
response.raise_for_status()
|
|
28
|
+
return response.json()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def fetch_safetensors_metadata(
|
|
32
|
+
client: httpx.AsyncClient, url: str, headers: Optional[Dict[str, str]] = None
|
|
33
|
+
) -> Dict[str, Any]:
|
|
34
|
+
headers = {"Range": f"bytes=0-{MAX_METADATA_SIZE}", **(headers or {})}
|
|
35
|
+
response = await client.get(url, headers=headers, timeout=REQUEST_TIMEOUT)
|
|
36
|
+
response.raise_for_status()
|
|
37
|
+
|
|
38
|
+
metadata = response.read()
|
|
39
|
+
# NOTE: Parse the first 8 bytes as a little-endian uint64 (size of the metadata)
|
|
40
|
+
metadata_size = struct.unpack("<Q", metadata[:8])[0]
|
|
41
|
+
|
|
42
|
+
if metadata_size < MAX_METADATA_SIZE:
|
|
43
|
+
metadata = metadata[8 : metadata_size + 8]
|
|
44
|
+
return json.loads(metadata)
|
|
45
|
+
|
|
46
|
+
# NOTE: Given that by default we just fetch the first 100_000 bytes, if the content is larger
|
|
47
|
+
# then we simply fetch the remainder again
|
|
48
|
+
metadata = metadata[8 : MAX_METADATA_SIZE + 8]
|
|
49
|
+
headers["Range"] = f"bytes={MAX_METADATA_SIZE + 1}-{metadata_size + 7}"
|
|
50
|
+
|
|
51
|
+
response = await client.get(url, headers=headers, timeout=REQUEST_TIMEOUT)
|
|
52
|
+
response.raise_for_status()
|
|
53
|
+
|
|
54
|
+
metadata += response.read()
|
|
55
|
+
return json.loads(metadata)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def run(model_id: str, revision: str) -> None:
|
|
59
|
+
headers = {}
|
|
60
|
+
if token := os.getenv("HF_TOKEN", None):
|
|
61
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
62
|
+
|
|
63
|
+
client = httpx.AsyncClient(
|
|
64
|
+
limits=httpx.Limits(
|
|
65
|
+
max_keepalive_connections=MAX_CONCURRENCY,
|
|
66
|
+
max_connections=MAX_CONCURRENCY,
|
|
67
|
+
),
|
|
68
|
+
timeout=httpx.Timeout(REQUEST_TIMEOUT),
|
|
69
|
+
http2=True,
|
|
70
|
+
follow_redirects=True,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# TODO: `recursive=true` shouldn't really be required unless it's a Diffusers
|
|
74
|
+
# models... I don't think this adds extra latency anyway
|
|
75
|
+
url = f"https://huggingface.co/api/models/{model_id}/tree/{revision}?recursive=true"
|
|
76
|
+
|
|
77
|
+
files = await get_json_file(client=client, url=url, headers=headers)
|
|
78
|
+
|
|
79
|
+
file_paths = [
|
|
80
|
+
f["path"]
|
|
81
|
+
for f in files
|
|
82
|
+
if f.get("path", None) is not None and f.get("type", None) == "file"
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
if "model.safetensors" in file_paths:
|
|
86
|
+
url = f"https://huggingface.co/{model_id}/resolve/{revision}/model.safetensors"
|
|
87
|
+
metadata = await fetch_safetensors_metadata(
|
|
88
|
+
client=client, url=url, headers=headers
|
|
89
|
+
)
|
|
90
|
+
print_report_for_transformers(model_id, revision, metadata)
|
|
91
|
+
elif "model.safetensors.index.json" in file_paths:
|
|
92
|
+
# TODO: We could eventually skip this request in favour of a greedy approach on trying to pull all the
|
|
93
|
+
# files following the formatting `model-00000-of-00000.safetensors`
|
|
94
|
+
url = f"https://huggingface.co/{model_id}/resolve/{revision}/model.safetensors.index.json"
|
|
95
|
+
files_index = await get_json_file(client=client, url=url, headers=headers)
|
|
96
|
+
|
|
97
|
+
urls = {
|
|
98
|
+
f"https://huggingface.co/{model_id}/resolve/{revision}/{f}"
|
|
99
|
+
for f in set(files_index["weight_map"].values())
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
semaphore = asyncio.Semaphore(MAX_CONCURRENCY)
|
|
103
|
+
|
|
104
|
+
async def fetch_semaphore(url: str) -> Dict[str, Any]:
|
|
105
|
+
async with semaphore:
|
|
106
|
+
return await fetch_safetensors_metadata(
|
|
107
|
+
client=client, url=url, headers=headers
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
tasks = [asyncio.create_task(fetch_semaphore(url)) for url in urls]
|
|
111
|
+
metadata_list: List[Dict[str, Any]] = await asyncio.gather(
|
|
112
|
+
*tasks, return_exceptions=False
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
metadata = reduce(lambda acc, metadata: acc | metadata, metadata_list, {})
|
|
116
|
+
print_report_for_transformers(model_id, revision, metadata)
|
|
117
|
+
elif "model_index.json" in file_paths:
|
|
118
|
+
url = f"https://huggingface.co/{model_id}/resolve/{revision}/model_index.json"
|
|
119
|
+
files_index = await get_json_file(client=client, url=url, headers=headers)
|
|
120
|
+
paths = {k for k, _ in files_index.items() if not k.startswith("_")}
|
|
121
|
+
|
|
122
|
+
path_urls: Dict[str, List[str]] = {}
|
|
123
|
+
for path in paths:
|
|
124
|
+
if f"{path}/diffusion_pytorch_model.safetensors" in file_paths:
|
|
125
|
+
path_urls[path] = [
|
|
126
|
+
f"https://huggingface.co/{model_id}/resolve/{revision}/{path}/diffusion_pytorch_model.safetensors"
|
|
127
|
+
]
|
|
128
|
+
elif f"{path}/model.safetensors" in file_paths:
|
|
129
|
+
path_urls[path] = [
|
|
130
|
+
f"https://huggingface.co/{model_id}/resolve/{revision}/{path}/model.safetensors"
|
|
131
|
+
]
|
|
132
|
+
elif f"{path}/diffusion_pytorch_model.safetensors.index.json" in file_paths:
|
|
133
|
+
url = f"https://huggingface.co/{model_id}/resolve/{revision}/{path}/diffusion_pytorch_model.safetensors.index.json"
|
|
134
|
+
files_index = await get_json_file(
|
|
135
|
+
client=client, url=url, headers=headers
|
|
136
|
+
)
|
|
137
|
+
path_urls[path] = list({
|
|
138
|
+
f"https://huggingface.co/{model_id}/resolve/{revision}/{path}/{f}"
|
|
139
|
+
for _, f in files_index["weight_map"].items() # type: ignore
|
|
140
|
+
})
|
|
141
|
+
elif f"{path}/model.safetensors.index.json" in file_paths:
|
|
142
|
+
url = f"https://huggingface.co/{model_id}/resolve/{revision}/{path}/model.safetensors.index.json"
|
|
143
|
+
files_index = await get_json_file(
|
|
144
|
+
client=client, url=url, headers=headers
|
|
145
|
+
)
|
|
146
|
+
path_urls[path] = list({
|
|
147
|
+
f"https://huggingface.co/{model_id}/resolve/{revision}/{path}/{f}"
|
|
148
|
+
for _, f in files_index["weight_map"].items() # type: ignore
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
semaphore = asyncio.Semaphore(MAX_CONCURRENCY)
|
|
152
|
+
|
|
153
|
+
async def fetch_semaphore(url: str) -> Dict[str, Any]:
|
|
154
|
+
async with semaphore:
|
|
155
|
+
return await fetch_safetensors_metadata(
|
|
156
|
+
client=client, url=url, headers=headers
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
metadata = {}
|
|
160
|
+
for path, urls in path_urls.items():
|
|
161
|
+
tasks = [asyncio.create_task(fetch_semaphore(url)) for url in urls]
|
|
162
|
+
metadata_list: List[Dict[str, Any]] = await asyncio.gather(
|
|
163
|
+
*tasks, return_exceptions=False
|
|
164
|
+
)
|
|
165
|
+
metadata[path] = reduce(
|
|
166
|
+
lambda acc, metadata: acc | metadata, metadata_list, {}
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
print_report_for_diffusers(
|
|
170
|
+
model_id=model_id, revision=revision, metadata=metadata
|
|
171
|
+
)
|
|
172
|
+
else:
|
|
173
|
+
raise RuntimeError(
|
|
174
|
+
"NONE OF `model.safetensors`, `model.safetensors.index.json`, `model_index.json` HAS BEEN FOUND"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
sys.exit(0)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def main() -> None:
|
|
181
|
+
parser = argparse.ArgumentParser()
|
|
182
|
+
parser.add_argument(
|
|
183
|
+
"--model-id", required=True, help="Model ID on the Hugging Face Hub"
|
|
184
|
+
)
|
|
185
|
+
parser.add_argument(
|
|
186
|
+
"--revision",
|
|
187
|
+
default="main",
|
|
188
|
+
help="Model revision on the Hugging Face Hub",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
args = parser.parse_args()
|
|
192
|
+
model_id = args.model_id
|
|
193
|
+
revision = args.revision
|
|
194
|
+
|
|
195
|
+
asyncio.run(run(model_id, revision))
|
hf_mem/print.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from typing import Any, Dict, Literal, Optional
|
|
3
|
+
|
|
4
|
+
MIN_NAME_LEN = 5
|
|
5
|
+
MAX_NAME_LEN = 13
|
|
6
|
+
MIN_DATA_LEN = 20
|
|
7
|
+
MAX_DATA_LEN = 32
|
|
8
|
+
BORDERS_AND_PADDING = 7
|
|
9
|
+
|
|
10
|
+
BOX = {
|
|
11
|
+
"tl": "┌",
|
|
12
|
+
"tr": "┐",
|
|
13
|
+
"bl": "└",
|
|
14
|
+
"br": "┘",
|
|
15
|
+
"ht": "─",
|
|
16
|
+
"vt": "│",
|
|
17
|
+
"tsep": "┬",
|
|
18
|
+
"bsep": "┴",
|
|
19
|
+
"lm": "├",
|
|
20
|
+
"rm": "┤",
|
|
21
|
+
"mm": "┼",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _print_with_color(content: str) -> None:
|
|
26
|
+
print(f"\x1b[38;2;244;183;63m{content}\x1b[0m")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _print_header(current_len: int) -> None:
|
|
30
|
+
length = current_len + MAX_NAME_LEN + BORDERS_AND_PADDING
|
|
31
|
+
top = BOX["tl"] + (BOX["tsep"] * (length - 2)) + BOX["tr"]
|
|
32
|
+
_print_with_color(top)
|
|
33
|
+
|
|
34
|
+
bottom = BOX["lm"] + (BOX["bsep"] * (length - 2)) + BOX["rm"]
|
|
35
|
+
_print_with_color(bottom)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _print_centered(text: str, current_len: int) -> None:
|
|
39
|
+
max_len = current_len + MAX_NAME_LEN - BORDERS_AND_PADDING
|
|
40
|
+
total_width = max_len + 12
|
|
41
|
+
text_len = len(text)
|
|
42
|
+
pad_left = (total_width - text_len) // 2
|
|
43
|
+
pad_right = total_width - text_len - pad_left
|
|
44
|
+
_print_with_color(f"{BOX['vt']}{' ' * pad_left}{text}{' ' * pad_right}{BOX['vt']}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _print_divider(
|
|
48
|
+
current_len: int,
|
|
49
|
+
side: Optional[Literal["top", "top-continue", "bottom", "bottom-continue"]] = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
match side:
|
|
52
|
+
case "top":
|
|
53
|
+
left, mid, right = BOX["lm"], BOX["tsep"], BOX["rm"]
|
|
54
|
+
case "top-continue":
|
|
55
|
+
left, mid, right = BOX["lm"], BOX["bsep"], BOX["rm"]
|
|
56
|
+
case "bottom":
|
|
57
|
+
left, mid, right = BOX["bl"], BOX["bsep"], BOX["br"]
|
|
58
|
+
case "bottom-continue":
|
|
59
|
+
left, mid, right = BOX["lm"], BOX["bsep"], BOX["rm"]
|
|
60
|
+
case _:
|
|
61
|
+
left, mid, right = BOX["lm"], BOX["mm"], BOX["rm"]
|
|
62
|
+
|
|
63
|
+
name_col_inner = MAX_NAME_LEN + 2
|
|
64
|
+
data_col_inner = current_len + 1
|
|
65
|
+
|
|
66
|
+
line = left
|
|
67
|
+
line += BOX["ht"] * name_col_inner
|
|
68
|
+
line += mid
|
|
69
|
+
line += BOX["ht"] * data_col_inner
|
|
70
|
+
line += right
|
|
71
|
+
_print_with_color(line)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _format_name(name: str) -> str:
|
|
75
|
+
if len(name) < MIN_NAME_LEN:
|
|
76
|
+
return f"{name:<{MIN_NAME_LEN}}"
|
|
77
|
+
if len(name) > MAX_NAME_LEN:
|
|
78
|
+
return name[: MAX_NAME_LEN - 3] + "..."
|
|
79
|
+
return f"{name:<{MAX_NAME_LEN}}"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _print_row(name: str, text: str, current_len: int) -> None:
|
|
83
|
+
name_fmt = _format_name(name)
|
|
84
|
+
data_fmt = f"{str(text):<{current_len}}"
|
|
85
|
+
_print_with_color(f"{BOX['vt']} {name_fmt} {BOX['vt']} {data_fmt} {BOX['vt']}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _make_bar(used: float, total: float, width: int) -> str:
|
|
89
|
+
if total <= 0:
|
|
90
|
+
return "░" * width
|
|
91
|
+
frac = min(max(used / total, 0.0), 1.0)
|
|
92
|
+
filled = int(round(frac * width))
|
|
93
|
+
filled = max(0, min(width, filled))
|
|
94
|
+
return "█" * filled + "░" * (width - filled)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _format_short_number(n: float) -> Optional[str]:
|
|
98
|
+
n = float(n)
|
|
99
|
+
for unit in ("", "K", "M", "B", "T"):
|
|
100
|
+
if abs(n) < 1000.0:
|
|
101
|
+
return f"{int(n)}" if unit == "" else f"{n:.1f}{unit}"
|
|
102
|
+
n /= 1000.0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _bytes_to_gb(nbytes: int) -> float:
|
|
106
|
+
return nbytes / (1024**3)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def print_report_for_transformers(
|
|
110
|
+
model_id: str, revision: str, metadata: Dict[str, Any]
|
|
111
|
+
) -> None:
|
|
112
|
+
ppdt = {}
|
|
113
|
+
for key, value in metadata.items():
|
|
114
|
+
if key in {"__metadata__"}:
|
|
115
|
+
continue
|
|
116
|
+
if value["dtype"] not in ppdt:
|
|
117
|
+
ppdt[value["dtype"]] = (0, 0)
|
|
118
|
+
|
|
119
|
+
match value["dtype"]:
|
|
120
|
+
case "F64" | "I64" | "U64":
|
|
121
|
+
dtype_b = 8
|
|
122
|
+
case "F32" | "I32" | "U32":
|
|
123
|
+
dtype_b = 4
|
|
124
|
+
case "F16" | "BF16" | "I16" | "U16":
|
|
125
|
+
dtype_b = 2
|
|
126
|
+
case "F8_E5M2" | "F8_E4M3" | "I8" | "U8":
|
|
127
|
+
dtype_b = 1
|
|
128
|
+
case _:
|
|
129
|
+
raise RuntimeError(f"DTYPE={value['dtype']} NOT HANDLED")
|
|
130
|
+
|
|
131
|
+
current_shape = math.prod(value["shape"])
|
|
132
|
+
current_shape_bytes = current_shape * dtype_b
|
|
133
|
+
|
|
134
|
+
ppdt[value["dtype"]] = (
|
|
135
|
+
ppdt[value["dtype"]][0] + current_shape,
|
|
136
|
+
ppdt[value["dtype"]][1] + current_shape_bytes,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
rows = [
|
|
140
|
+
"INFERENCE MEMORY ESTIMATE FOR",
|
|
141
|
+
f"https://hf.co/{model_id} @ {revision}",
|
|
142
|
+
"TOTAL MEMORY",
|
|
143
|
+
"REQUIREMENTS",
|
|
144
|
+
]
|
|
145
|
+
for dt, (params, nbytes) in ppdt.items():
|
|
146
|
+
rows.append(f"{dt} {params} {nbytes}")
|
|
147
|
+
|
|
148
|
+
max_len = 0
|
|
149
|
+
for r in rows:
|
|
150
|
+
max_len = max(max_len, len(str(r)))
|
|
151
|
+
|
|
152
|
+
current_len = min(max_len, MAX_DATA_LEN)
|
|
153
|
+
|
|
154
|
+
total_bytes = sum(nbytes for _, nbytes in ppdt.values())
|
|
155
|
+
total_params = sum(params for params, _ in ppdt.values())
|
|
156
|
+
total_gb = _bytes_to_gb(total_bytes)
|
|
157
|
+
|
|
158
|
+
_print_header(current_len)
|
|
159
|
+
_print_centered("INFERENCE MEMORY ESTIMATE FOR", current_len)
|
|
160
|
+
_print_centered(f"https://hf.co/{model_id} @ {revision}", current_len)
|
|
161
|
+
_print_divider(current_len + 1, "top")
|
|
162
|
+
|
|
163
|
+
total_text = f"{_bytes_to_gb(total_bytes):.2f} GB ({_format_short_number(total_params)} params)"
|
|
164
|
+
_print_row("TOTAL MEMORY", total_text, current_len)
|
|
165
|
+
|
|
166
|
+
total_bar = _make_bar(total_bytes, total_bytes, current_len)
|
|
167
|
+
_print_row("REQUIREMENTS", total_bar, current_len)
|
|
168
|
+
_print_divider(current_len + 1)
|
|
169
|
+
|
|
170
|
+
max_length = max([
|
|
171
|
+
len(f"{_format_short_number(params)} PARAMS") for params, _ in ppdt.values()
|
|
172
|
+
])
|
|
173
|
+
for i, (dtype, (params, nbytes)) in enumerate(ppdt.items()):
|
|
174
|
+
dtype_name = dtype.upper()
|
|
175
|
+
dtype_gb = _bytes_to_gb(nbytes)
|
|
176
|
+
|
|
177
|
+
gb_text = f"{dtype_gb:.1f} / {total_gb:.1f} GB"
|
|
178
|
+
_print_row(
|
|
179
|
+
dtype_name + " " * (max_length - len(dtype_name)),
|
|
180
|
+
gb_text,
|
|
181
|
+
current_len,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
bar = _make_bar(dtype_gb, total_gb, current_len)
|
|
185
|
+
_print_row(f"{_format_short_number(params)} PARAMS", bar, current_len)
|
|
186
|
+
|
|
187
|
+
if i < len(ppdt) - 1:
|
|
188
|
+
_print_divider(current_len + 1)
|
|
189
|
+
|
|
190
|
+
_print_divider(current_len + 1, "bottom")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def print_report_for_diffusers(
|
|
194
|
+
model_id: str, revision: str, metadata: Dict[str, Dict[str, Any]]
|
|
195
|
+
) -> None:
|
|
196
|
+
components_ppdt: Dict[str, Dict[str, tuple[int, int]]] = {}
|
|
197
|
+
total_bytes = 0
|
|
198
|
+
total_params = 0
|
|
199
|
+
|
|
200
|
+
for path, path_metadata in metadata.items():
|
|
201
|
+
ppdt: Dict[str, tuple[int, int]] = {}
|
|
202
|
+
for key, value in path_metadata.items():
|
|
203
|
+
if key in {"__metadata__"}:
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
dtype = value["dtype"]
|
|
207
|
+
match dtype:
|
|
208
|
+
case "F64" | "I64" | "U64":
|
|
209
|
+
dtype_b = 8
|
|
210
|
+
case "F32" | "I32" | "U32":
|
|
211
|
+
dtype_b = 4
|
|
212
|
+
case "F16" | "BF16" | "I16" | "U16":
|
|
213
|
+
dtype_b = 2
|
|
214
|
+
case "F8_E5M2" | "F8_E4M3" | "I8" | "U8":
|
|
215
|
+
dtype_b = 1
|
|
216
|
+
case _:
|
|
217
|
+
raise RuntimeError(f"DTYPE={dtype} NOT HANDLED")
|
|
218
|
+
|
|
219
|
+
current_shape = math.prod(value["shape"])
|
|
220
|
+
current_shape_bytes = current_shape * dtype_b
|
|
221
|
+
|
|
222
|
+
if dtype not in ppdt:
|
|
223
|
+
ppdt[dtype] = (0, 0)
|
|
224
|
+
ppdt[dtype] = (
|
|
225
|
+
ppdt[dtype][0] + current_shape,
|
|
226
|
+
ppdt[dtype][1] + current_shape_bytes,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
components_ppdt[path] = ppdt
|
|
230
|
+
for params, nbytes in ppdt.values():
|
|
231
|
+
total_params += params
|
|
232
|
+
total_bytes += nbytes
|
|
233
|
+
|
|
234
|
+
rows = [
|
|
235
|
+
"INFERENCE MEMORY ESTIMATE FOR",
|
|
236
|
+
f"https://hf.co/{model_id} @ {revision}",
|
|
237
|
+
"TOTAL MEMORY",
|
|
238
|
+
"REQUIREMENTS",
|
|
239
|
+
]
|
|
240
|
+
for path, ppdt in components_ppdt.items():
|
|
241
|
+
rows.append(path)
|
|
242
|
+
for dt, (params, nbytes) in ppdt.items():
|
|
243
|
+
rows.append(f"{dt} {params} {nbytes}")
|
|
244
|
+
|
|
245
|
+
max_len = 0
|
|
246
|
+
for r in rows:
|
|
247
|
+
max_len = max(max_len, len(str(r)))
|
|
248
|
+
current_len = min(max_len, MAX_DATA_LEN)
|
|
249
|
+
|
|
250
|
+
_print_header(current_len)
|
|
251
|
+
_print_centered("INFERENCE MEMORY ESTIMATE FOR", current_len)
|
|
252
|
+
_print_centered(f"https://hf.co/{model_id} @ {revision}", current_len)
|
|
253
|
+
_print_divider(current_len + 1, "top")
|
|
254
|
+
|
|
255
|
+
total_text = f"{_bytes_to_gb(total_bytes):.2f} GB ({_format_short_number(total_params)} params)"
|
|
256
|
+
_print_row("TOTAL MEMORY", total_text, current_len)
|
|
257
|
+
|
|
258
|
+
total_bar = _make_bar(total_bytes, total_bytes, current_len)
|
|
259
|
+
_print_row("REQUIREMENTS", total_bar, current_len)
|
|
260
|
+
|
|
261
|
+
for path, ppdt in components_ppdt.items():
|
|
262
|
+
_print_divider(current_len + 1, "top-continue")
|
|
263
|
+
|
|
264
|
+
path_bytes = sum(nbytes for _, nbytes in ppdt.values())
|
|
265
|
+
path_gb = _bytes_to_gb(path_bytes)
|
|
266
|
+
|
|
267
|
+
_print_centered(f"{path.upper()} ({path_gb:.2f} GB)", current_len)
|
|
268
|
+
_print_divider(current_len + 1, "top")
|
|
269
|
+
|
|
270
|
+
max_length = max([
|
|
271
|
+
len(f"{_format_short_number(params)} PARAMS") for params, _ in ppdt.values()
|
|
272
|
+
])
|
|
273
|
+
for i, (dtype, (params, nbytes)) in enumerate(ppdt.items()):
|
|
274
|
+
dtype_name = dtype.upper()
|
|
275
|
+
dtype_gb = _bytes_to_gb(nbytes)
|
|
276
|
+
|
|
277
|
+
gb_text = f"{dtype_gb:.1f} / {_bytes_to_gb(total_bytes):.1f} GB"
|
|
278
|
+
_print_row(
|
|
279
|
+
dtype_name + " " * (max_length - len(dtype_name)),
|
|
280
|
+
gb_text,
|
|
281
|
+
current_len,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
bar = _make_bar(dtype_gb, _bytes_to_gb(total_bytes), current_len)
|
|
285
|
+
_print_row(f"{_format_short_number(params)} PARAMS", bar, current_len)
|
|
286
|
+
|
|
287
|
+
if i < len(ppdt) - 1:
|
|
288
|
+
_print_divider(current_len + 1, "bottom-continue")
|
|
289
|
+
|
|
290
|
+
_print_divider(current_len + 1, "bottom")
|
hf_mem/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hf-mem
|
|
3
|
+
Version: 0.0.3
|
|
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
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: httpx[http2]>=0.28.1
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# `hf-mem`
|
|
14
|
+
|
|
15
|
+
`hf-mem` is an experimental CLI to estimate inference memory requirements for Hugging Face models, written in Python. `hf-mem` is lightweight, only depends on `httpx`. It's recommended to run with [`uv`](https://github.com/astral-sh/uv) for a better experience.
|
|
16
|
+
|
|
17
|
+
`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.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uvx hf-mem --model-id MiniMaxAI/MiniMax-M2
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
<img width="899" height="648" src="https://github.com/user-attachments/assets/530f8b14-a415-4fd6-9054-bcd81cafae09" />
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uvx hf-mem --model-id Qwen/Qwen-Image
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
<img width="894" height="872" src="https://github.com/user-attachments/assets/cd4234ec-bdcc-4db4-8b01-0ac9b5cd390c" />
|
|
32
|
+
|
|
33
|
+
## References
|
|
34
|
+
|
|
35
|
+
- [Safetensors Metadata parsing](https://huggingface.co/docs/safetensors/en/metadata_parsing)
|
|
36
|
+
- [usgraphics - TR-100 Machine Report](https://github.com/usgraphics/usgc-machine-report)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
hf_mem/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
hf_mem/cli.py,sha256=m6W32NBWYvfqU_Odh8N0YAQHQ53P3HIhCdw90aICHAI,7781
|
|
3
|
+
hf_mem/print.py,sha256=G3bDy2ND1WY8LyGTYf07E4zFZE54TJZu1uhVWGFnFkg,9269
|
|
4
|
+
hf_mem/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
hf_mem-0.0.3.dist-info/licenses/LICENSE,sha256=TUO4cSlmwlYsHZfepsmo1zLoYhmT0RImXoRihVYNHhw,1060
|
|
6
|
+
hf_mem-0.0.3.dist-info/WHEEL,sha256=M6du7VZflc4UPsGphmOXHANdgk8zessdJG0DBUuoA-U,78
|
|
7
|
+
hf_mem-0.0.3.dist-info/entry_points.txt,sha256=p6o2afl5zELLNtkKwQxfLdOpal2nRlcshDZSfV02pOY,44
|
|
8
|
+
hf_mem-0.0.3.dist-info/METADATA,sha256=n9wvVCPHGzSWC86iVJIH2jSS8hvovUAvjC-mB9o43Ac,1512
|
|
9
|
+
hf_mem-0.0.3.dist-info/RECORD,,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Copyright (c) 2025 Alvaro Bartolome
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any
|
|
4
|
+
person obtaining a copy of this software and associated
|
|
5
|
+
documentation files (the "Software"), to deal in the
|
|
6
|
+
Software without restriction, including without
|
|
7
|
+
limitation the rights to use, copy, modify, merge,
|
|
8
|
+
publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software
|
|
10
|
+
is furnished to do so, subject to the following
|
|
11
|
+
conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice
|
|
14
|
+
shall be included in all copies or substantial portions
|
|
15
|
+
of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
|
18
|
+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
|
19
|
+
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
20
|
+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
|
21
|
+
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
22
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
23
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
|
24
|
+
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
25
|
+
DEALINGS IN THE SOFTWARE.
|