Skip to content

Commit 624d596

Browse files
danbevliparetejas
authored andcommitted
model-conversion : merge inspect-org-model.py with tensor-info.py (ggml-org#19823)
This commit replaces/merges the inspect-org-model.py script with the contents tensor-info.py script. The merged script has also been updated to also print tensor sizes which was the only thing that was not done before (by tensor-info.py that is). The motivation for this is that tensor-info.py does not load the tensor weights which can be time consuming for larger models. And also now that both are doing almost the same thing it makes sense to just have one and not two scripts to maintain.
1 parent 16a329f commit 624d596

3 files changed

Lines changed: 289 additions & 237 deletions

File tree

examples/model-conversion/Makefile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ causal-verify-embeddings: causal-run-original-embeddings causal-run-converted-em
7777
@./scripts/causal/compare-embeddings-logits.sh
7878

7979
causal-inspect-original-model:
80-
@./scripts/utils/inspect-org-model.py
80+
@./scripts/utils/inspect-org-model.py --list-all -s
81+
82+
causal-list-original-model-tensors:
83+
@./scripts/utils/inspect-org-model.py --list-all-short -s
8184

8285
causal-inspect-converted-model:
8386
@./scripts/utils/inspect-converted-model.sh
@@ -153,7 +156,7 @@ embedding-verify-logits-st: embedding-run-original-model-st embedding-run-conver
153156

154157
embedding-inspect-original-model:
155158
$(call validate_embedding_model_path,embedding-inspect-original-model)
156-
@EMBEDDING_MODEL_PATH="$(EMBEDDING_MODEL_PATH)" ./scripts/utils/inspect-org-model.py -m ${EMBEDDING_MODEL_PATH}
159+
@EMBEDDING_MODEL_PATH="$(EMBEDDING_MODEL_PATH)" ./scripts/utils/inspect-org-model.py -m ${EMBEDDING_MODEL_PATH} --list-all -s
157160

158161
embedding-inspect-converted-model:
159162
@CONVERTED_EMBEDDING_MODEL="$(CONVERTED_EMBEDDING_MODEL)" ./scripts/utils/inspect-converted-model.sh ${CONVERTED_EMBEDDING_MODEL}
Lines changed: 284 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,290 @@
11
#!/usr/bin/env python3
22

33
import argparse
4-
import os
54
import json
5+
import os
6+
import re
7+
import struct
8+
import sys
9+
from pathlib import Path
10+
from typing import Optional
611
from safetensors import safe_open
7-
from collections import defaultdict
8-
9-
parser = argparse.ArgumentParser(description='Process model with specified path')
10-
parser.add_argument('--model-path', '-m', help='Path to the model')
11-
args = parser.parse_args()
12-
13-
model_path = os.environ.get('MODEL_PATH', args.model_path)
14-
if model_path is None:
15-
parser.error("Model path must be specified either via --model-path argument or MODEL_PATH environment variable")
16-
17-
# Check if there's an index file (multi-file model)
18-
index_path = os.path.join(model_path, "model.safetensors.index.json")
19-
single_file_path = os.path.join(model_path, "model.safetensors")
20-
21-
if os.path.exists(index_path):
22-
# Multi-file model
23-
print("Multi-file model detected")
24-
25-
with open(index_path, 'r') as f:
26-
index_data = json.load(f)
27-
28-
# Get the weight map (tensor_name -> file_name)
29-
weight_map = index_data.get("weight_map", {})
30-
31-
# Group tensors by file for efficient processing
32-
file_tensors = defaultdict(list)
33-
for tensor_name, file_name in weight_map.items():
34-
file_tensors[file_name].append(tensor_name)
35-
36-
print("Tensors in model:")
37-
38-
# Process each shard file
39-
for file_name, tensor_names in file_tensors.items():
40-
file_path = os.path.join(model_path, file_name)
41-
print(f"\n--- From {file_name} ---")
42-
43-
with safe_open(file_path, framework="pt") as f:
44-
for tensor_name in sorted(tensor_names):
45-
tensor = f.get_tensor(tensor_name)
46-
print(f"- {tensor_name} : shape = {tensor.shape}, dtype = {tensor.dtype}")
47-
48-
elif os.path.exists(single_file_path):
49-
# Single file model (original behavior)
50-
print("Single-file model detected")
51-
52-
with safe_open(single_file_path, framework="pt") as f:
53-
keys = f.keys()
54-
print("Tensors in model:")
55-
for key in sorted(keys):
56-
tensor = f.get_tensor(key)
57-
print(f"- {key} : shape = {tensor.shape}, dtype = {tensor.dtype}")
58-
59-
else:
60-
print(f"Error: Neither 'model.safetensors.index.json' nor 'model.safetensors' found in {model_path}")
61-
print("Available files:")
62-
if os.path.exists(model_path):
63-
for item in sorted(os.listdir(model_path)):
64-
print(f" {item}")
12+
13+
14+
MODEL_SAFETENSORS_FILE = "model.safetensors"
15+
MODEL_SAFETENSORS_INDEX = "model.safetensors.index.json"
16+
17+
DTYPE_SIZES = {
18+
"F64": 8, "I64": 8, "U64": 8,
19+
"F32": 4, "I32": 4, "U32": 4,
20+
"F16": 2, "BF16": 2, "I16": 2, "U16": 2,
21+
"I8": 1, "U8": 1, "BOOL": 1,
22+
"F8_E4M3": 1, "F8_E5M2": 1,
23+
}
24+
25+
SIZE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB']
26+
27+
28+
def get_weight_map(model_path: Path) -> Optional[dict[str, str]]:
29+
index_file = model_path / MODEL_SAFETENSORS_INDEX
30+
31+
if index_file.exists():
32+
with open(index_file, 'r') as f:
33+
index = json.load(f)
34+
return index.get("weight_map", {})
35+
36+
return None
37+
38+
39+
def get_all_tensor_names(model_path: Path) -> list[str]:
40+
weight_map = get_weight_map(model_path)
41+
42+
if weight_map is not None:
43+
return list(weight_map.keys())
44+
45+
single_file = model_path / MODEL_SAFETENSORS_FILE
46+
if single_file.exists():
47+
try:
48+
with safe_open(single_file, framework="pt", device="cpu") as f:
49+
return list(f.keys())
50+
except Exception as e:
51+
print(f"Error reading {single_file}: {e}")
52+
sys.exit(1)
53+
54+
print(f"Error: No safetensors files found in {model_path}")
55+
sys.exit(1)
56+
57+
58+
def find_tensor_file(model_path: Path, tensor_name: str) -> Optional[str]:
59+
weight_map = get_weight_map(model_path)
60+
61+
if weight_map is not None:
62+
return weight_map.get(tensor_name)
63+
64+
single_file = model_path / MODEL_SAFETENSORS_FILE
65+
if single_file.exists():
66+
return single_file.name
67+
68+
return None
69+
70+
71+
def read_safetensors_header(file_path: Path) -> dict:
72+
with open(file_path, 'rb') as f:
73+
header_size = struct.unpack('<Q', f.read(8))[0]
74+
return json.loads(f.read(header_size))
75+
76+
77+
def get_tensor_size_bytes(tensor_meta: dict) -> int:
78+
offsets = tensor_meta.get("data_offsets")
79+
if offsets and len(offsets) == 2:
80+
return offsets[1] - offsets[0]
81+
n_elements = 1
82+
for d in tensor_meta.get("shape", []):
83+
n_elements *= d
84+
return n_elements * DTYPE_SIZES.get(tensor_meta.get("dtype", "F32"), 4)
85+
86+
87+
def format_size(size_bytes: int) -> str:
88+
val = float(size_bytes)
89+
for unit in SIZE_UNITS[:-1]:
90+
if val < 1024.0:
91+
return f"{val:.2f} {unit}"
92+
val /= 1024.0
93+
return f"{val:.2f} {SIZE_UNITS[-1]}"
94+
95+
96+
def get_all_tensor_metadata(model_path: Path) -> dict[str, dict]:
97+
weight_map = get_weight_map(model_path)
98+
99+
if weight_map is not None:
100+
file_to_tensors: dict[str, list[str]] = {}
101+
for tensor_name, file_name in weight_map.items():
102+
file_to_tensors.setdefault(file_name, []).append(tensor_name)
103+
104+
all_metadata: dict[str, dict] = {}
105+
for file_name, tensor_names in file_to_tensors.items():
106+
try:
107+
header = read_safetensors_header(model_path / file_name)
108+
for tensor_name in tensor_names:
109+
if tensor_name in header:
110+
all_metadata[tensor_name] = header[tensor_name]
111+
except Exception as e:
112+
print(f"Warning: Could not read header from {file_name}: {e}", file=sys.stderr)
113+
return all_metadata
114+
115+
single_file = model_path / MODEL_SAFETENSORS_FILE
116+
if single_file.exists():
117+
try:
118+
header = read_safetensors_header(single_file)
119+
return {k: v for k, v in header.items() if k != "__metadata__"}
120+
except Exception as e:
121+
print(f"Error reading {single_file}: {e}")
122+
sys.exit(1)
123+
124+
print(f"Error: No safetensors files found in {model_path}")
125+
sys.exit(1)
126+
127+
128+
def normalize_tensor_name(tensor_name: str) -> str:
129+
normalized = re.sub(r'\.\d+\.', '.#.', tensor_name)
130+
normalized = re.sub(r'\.\d+$', '.#', normalized)
131+
return normalized
132+
133+
134+
def list_all_tensors(
135+
model_path: Path,
136+
short: bool = False,
137+
show_sizes: bool = False,
138+
):
139+
tensor_names = get_all_tensor_names(model_path)
140+
141+
metadata: Optional[dict[str, dict]] = None
142+
if show_sizes:
143+
metadata = get_all_tensor_metadata(model_path)
144+
145+
total_bytes = 0
146+
147+
if short:
148+
seen: dict[str, str] = {}
149+
for tensor_name in sorted(tensor_names):
150+
normalized = normalize_tensor_name(tensor_name)
151+
if normalized not in seen:
152+
seen[normalized] = tensor_name
153+
display_pairs = list(sorted(seen.items()))
154+
name_width = max((len(n) for n, _ in display_pairs), default=0)
155+
for normalized, first_name in display_pairs:
156+
if metadata and first_name in metadata:
157+
m = metadata[first_name]
158+
size = get_tensor_size_bytes(m)
159+
total_bytes += size
160+
print(f"{normalized:{name_width}} {m.get('dtype', '?'):6s} {str(m.get('shape', '')):30s} {format_size(size)}")
161+
else:
162+
print(normalized)
163+
else:
164+
name_width = max((len(n) for n in tensor_names), default=0)
165+
for tensor_name in sorted(tensor_names):
166+
if metadata and tensor_name in metadata:
167+
m = metadata[tensor_name]
168+
size = get_tensor_size_bytes(m)
169+
total_bytes += size
170+
print(f"{tensor_name:{name_width}} {m.get('dtype', '?'):6s} {str(m.get('shape', '')):30s} {format_size(size)}")
171+
else:
172+
print(tensor_name)
173+
174+
if show_sizes:
175+
print(f"\nTotal: {format_size(total_bytes)}")
176+
177+
178+
def print_tensor_info(model_path: Path, tensor_name: str, num_values: Optional[int] = None):
179+
tensor_file = find_tensor_file(model_path, tensor_name)
180+
181+
if tensor_file is None:
182+
print(f"Error: Could not find tensor '{tensor_name}' in model index")
183+
print(f"Model path: {model_path}")
184+
sys.exit(1)
185+
186+
file_path = model_path / tensor_file
187+
188+
try:
189+
header = read_safetensors_header(file_path)
190+
tensor_meta = header.get(tensor_name, {})
191+
dtype_str = tensor_meta.get("dtype")
192+
193+
with safe_open(file_path, framework="pt", device="cpu") as f:
194+
if tensor_name in f.keys():
195+
tensor_slice = f.get_slice(tensor_name)
196+
shape = tensor_slice.get_shape()
197+
print(f"Tensor: {tensor_name}")
198+
print(f"File: {tensor_file}")
199+
print(f"Shape: {shape}")
200+
if dtype_str:
201+
print(f"Dtype: {dtype_str}")
202+
if tensor_meta:
203+
print(f"Size: {format_size(get_tensor_size_bytes(tensor_meta))}")
204+
if num_values is not None:
205+
tensor = f.get_tensor(tensor_name)
206+
if not dtype_str:
207+
print(f"Dtype: {tensor.dtype}")
208+
flat = tensor.flatten()
209+
n = min(num_values, flat.numel())
210+
print(f"Values: {flat[:n].tolist()}")
211+
else:
212+
print(f"Error: Tensor '{tensor_name}' not found in {tensor_file}")
213+
sys.exit(1)
214+
215+
except FileNotFoundError:
216+
print(f"Error: The file '{file_path}' was not found.")
217+
sys.exit(1)
218+
except Exception as e:
219+
print(f"An error occurred: {e}")
220+
sys.exit(1)
221+
222+
223+
def main():
224+
parser = argparse.ArgumentParser(
225+
description="Print tensor information from a safetensors model"
226+
)
227+
parser.add_argument(
228+
"tensor_name",
229+
nargs="?",
230+
help="Name of the tensor to inspect"
231+
)
232+
parser.add_argument(
233+
"-m", "--model-path",
234+
type=Path,
235+
help="Path to the model directory (default: MODEL_PATH environment variable)"
236+
)
237+
parser.add_argument(
238+
"-l", "--list-all-short",
239+
action="store_true",
240+
help="List unique tensor patterns (layer numbers replaced with #)"
241+
)
242+
parser.add_argument(
243+
"-la", "--list-all",
244+
action="store_true",
245+
help="List all tensor names with actual layer numbers"
246+
)
247+
parser.add_argument(
248+
"-n", "--num-values",
249+
nargs="?",
250+
const=10,
251+
default=None,
252+
type=int,
253+
metavar="N",
254+
help="Print the first N values of the tensor flattened (default: 10 if flag is given without a number)"
255+
)
256+
parser.add_argument(
257+
"-s", "--sizes",
258+
action="store_true",
259+
help="Show dtype, shape, and size for each tensor when listing"
260+
)
261+
262+
args = parser.parse_args()
263+
264+
model_path = args.model_path
265+
if model_path is None:
266+
model_path_str = os.environ.get("MODEL_PATH")
267+
if model_path_str is None:
268+
print("Error: --model-path not provided and MODEL_PATH environment variable not set")
269+
sys.exit(1)
270+
model_path = Path(model_path_str)
271+
272+
if not model_path.exists():
273+
print(f"Error: Model path does not exist: {model_path}")
274+
sys.exit(1)
275+
276+
if not model_path.is_dir():
277+
print(f"Error: Model path is not a directory: {model_path}")
278+
sys.exit(1)
279+
280+
if args.list_all_short or args.list_all:
281+
list_all_tensors(model_path, short=args.list_all_short, show_sizes=args.sizes)
65282
else:
66-
print(f" Directory {model_path} does not exist")
67-
exit(1)
283+
if args.tensor_name is None:
284+
print("Error: tensor_name is required when not using --list-all-short or --list-all")
285+
sys.exit(1)
286+
print_tensor_info(model_path, args.tensor_name, args.num_values)
287+
288+
289+
if __name__ == "__main__":
290+
main()

0 commit comments

Comments
 (0)