File size: 8,808 Bytes
9c5d0ee | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | #!/usr/bin/env python3
"""
Đếm số tham số (Parameters) trực tiếp từ file model.safetensors của
Diffusion Policy (LeRobot).
Chỉ đọc header JSON (vài KB đầu file) — KHÔNG cần load toàn bộ model vào
RAM/VRAM, KHÔNG cần cài PyTorch. An toàn với file nhiều GB.
Thêm so với bản gốc (đồng bộ với count_params_act.py):
- Phân nhóm tham số theo module (vision encoder / U-Net down-mid-up / FiLM ...)
- Ước tính VRAM do THAM SỐ đóng góp (inference và training)
- Tách riêng tham số học được và buffer (BatchNorm running stats) — vì
safetensors lưu cả hai, nên "tổng phần tử" > "số tham số thực"
Cách dùng:
python3 count_params_from_file.py /duong/dan/toi/model.safetensors
python3 count_params_from_file.py dp_v1.safetensors dp_v2.safetensors # so sánh
"""
import json
import struct
import sys
from math import prod
# Số byte mỗi phần tử theo dtype của safetensors — dùng để ước tính VRAM
# cho đúng độ chính xác mà model đang được lưu (FP32 / FP16 / BF16).
DTYPE_BYTES = {
"F64": 8, "F32": 4, "F16": 2, "BF16": 2,
"I64": 8, "I32": 4, "I16": 2, "I8": 1, "U8": 1, "BOOL": 1,
}
# Thứ tự nhóm cũng là thứ tự hiển thị trong bảng kết quả.
# Mỗi rule là (tên_nhóm, hàm_kiểm_tra(tên_tensor) -> bool). Rule ở trên được
# ưu tiên trước — vd. "rgb_encoder ... backbone" phải khớp trước "rgb_encoder" chung.
GROUP_RULES = [
("Vision encoder — backbone ResNet-18", lambda k: "rgb_encoder" in k and "backbone" in k),
("Vision encoder — SpatialSoftmax + projection", lambda k: "rgb_encoder" in k),
("U-Net — diffusion_step_encoder", lambda k: "diffusion_step_encoder" in k),
("U-Net — down_modules (nhánh nén)", lambda k: "down_modules" in k),
("U-Net — mid_modules (bottleneck)", lambda k: "mid_modules" in k),
("U-Net — up_modules (nhánh giãn)", lambda k: "up_modules" in k),
("U-Net — final_conv (đầu ra action)", lambda k: "final_conv" in k),
("Normalization buffers (mean/std/min/max)", lambda k: "normalize" in k.lower()),
]
GROUP_OTHER = "Khác (chưa phân loại)"
# Nhóm cắt ngang: FiLM conditioning nằm rải trong mọi residual block của U-Net,
# nên không thể tách bằng GROUP_RULES (sẽ trùng với down/mid/up). Thống kê riêng.
def is_film(name: str) -> bool:
return "cond_encoder" in name
def is_buffer(name: str, dtype: str) -> bool:
"""Buffer = tensor được lưu trong file nhưng KHÔNG phải tham số học được."""
return (
"running_mean" in name
or "running_var" in name
or "num_batches_tracked" in name
or dtype in ("I64", "I32", "BOOL")
)
def group_of(tensor_name: str) -> str:
for group_name, match in GROUP_RULES:
if match(tensor_name):
return group_name
return GROUP_OTHER
def load_header(path: str) -> dict:
with open(path, "rb") as f:
# 8 byte đầu = độ dài (uint64, little-endian) của phần header JSON
header_len = struct.unpack("<Q", f.read(8))[0]
header = json.loads(f.read(header_len))
# "__metadata__" không phải tensor, phải loại ra trước khi đếm
header.pop("__metadata__", None)
return header
def count_params(path: str) -> dict:
header = load_header(path)
total_elems = 0 # mọi phần tử được lưu trong file
learnable = 0 # tham số học được (đã loại buffer)
buffers = 0 # BN running stats, num_batches_tracked, ...
total_bytes = 0
film_params = 0
dtype_count: dict = {}
group_params: dict = {}
group_tensor_count: dict = {}
for name, info in header.items():
shape = info["shape"]
n_elem = prod(shape) if shape else 1 # tensor 0-chiều (scalar) vẫn tính là 1
dt = info["dtype"]
total_elems += n_elem
if is_buffer(name, dt):
buffers += n_elem
else:
learnable += n_elem
if is_film(name):
film_params += n_elem
start, end = info["data_offsets"]
total_bytes += end - start
dtype_count[dt] = dtype_count.get(dt, 0) + 1
g = group_of(name)
group_params[g] = group_params.get(g, 0) + n_elem
group_tensor_count[g] = group_tensor_count.get(g, 0) + 1
# dtype chiếm đa số — dùng để ước tính VRAM đúng với độ chính xác đang lưu
dominant_dtype = max(dtype_count, key=dtype_count.get) if dtype_count else "F32"
bytes_per_elem = DTYPE_BYTES.get(dominant_dtype, 4)
return {
"path": path,
"n_tensors": len(header),
"dtype_count": dtype_count,
"dominant_dtype": dominant_dtype,
"bytes_per_elem": bytes_per_elem,
"total_elems": total_elems,
"learnable": learnable,
"buffers": buffers,
"film_params": film_params,
"total_bytes": total_bytes,
"group_params": group_params,
"group_tensor_count": group_tensor_count,
}
def print_report(r: dict) -> None:
print(f"File: {r['path']}")
print(f"Số tensor: {r['n_tensors']}")
print(f"Kiểu dữ liệu: {r['dtype_count']}")
print(f"Tổng phần tử: {r['total_elems']:,}")
print(f" ├─ tham số học được: {r['learnable']:,}")
print(f" └─ buffer (BN stats): {r['buffers']:,}")
print(f"Kích thước data: {r['total_bytes']:,} byte "
f"({r['total_bytes']/1024**2:.1f} MiB / {r['total_bytes']/1e9:.3f} GB)")
print()
print("--- Phân theo module ---")
head = f"{'Module':46s}{'#tensor':>9s}{'Tham số':>14s}{'%':>7s}"
print(head)
print("-" * len(head))
ordered = [g for g, _ in GROUP_RULES] + [GROUP_OTHER]
for g in ordered:
p = r["group_params"].get(g, 0)
if p == 0:
continue
n = r["group_tensor_count"].get(g, 0)
pct = 100 * p / r["total_elems"] if r["total_elems"] else 0
print(f"{g:46s}{n:9d}{p:14,d}{pct:6.1f}%")
if r["film_params"]:
pct = 100 * r["film_params"] / r["total_elems"]
print("-" * len(head))
print(f"{'(cắt ngang) FiLM cond_encoder':46s}"
f"{'':>9s}{r['film_params']:14,d}{pct:6.1f}%")
print(" ^ nằm rải trong down/mid/up ở trên — KHÔNG cộng thêm vào tổng")
print()
# Ước tính VRAM lúc train: trọng số + gradient + 2 hệ số Adam (m, v) = 4x.
# Đây chỉ là phần do THAM SỐ đóng góp — activations (phụ thuộc batch_size,
# số camera, độ phân giải ảnh) thường lớn hơn nhiều và KHÔNG được tính ở đây.
b = r["bytes_per_elem"]
w_mib = r["total_elems"] * b / 1024**2
print(f"--- Ước tính VRAM do THAM SỐ đóng góp ({r['dominant_dtype']}, chưa tính activations) ---")
print(f"Trọng số: {w_mib:8.1f} MiB (~{w_mib/1024:.2f} GiB)")
print(f"Inference (chỉ trọng số): {w_mib:8.1f} MiB (~{w_mib/1024:.2f} GiB)")
print(f"Training (+grad +2x Adam): {4*w_mib:8.1f} MiB (~{4*w_mib/1024:.2f} GiB)")
print()
print("Lưu ý khi đọc con số trên:")
print(" - Activations (ảnh camera qua ResNet-18) thường lớn hơn khoản này")
print(" nhiều lần khi train; đây là sàn dưới, KHÔNG phải tổng VRAM.")
print(" - Isaac Sim chạy song song cũng chiếm VRAM đáng kể (render 4 camera).")
print(" - Diffusion Policy lặp U-Net num_inference_steps lần mỗi bước inference:")
print(" không tăng đỉnh VRAM nhưng gây phân mảnh bộ nhớ -> OOM sớm hơn lý thuyết.")
def print_comparison(reports: list) -> None:
print("=== So sánh nhiều file ===")
head = f"{'File':30s}{'Tổng phần tử':>16s}{'MiB':>10s}"
print(head)
print("-" * len(head))
for r in reports:
name = r["path"].split("/")[-1]
print(f"{name:30s}{r['total_elems']:16,d}{r['total_bytes']/1024**2:10.1f}")
if len(reports) == 2:
a, b = reports
if b["total_elems"]:
print(f"\nTỉ lệ {a['path'].split('/')[-1]} / {b['path'].split('/')[-1]}: "
f"{a['total_elems']/b['total_elems']:.3f}x")
print()
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Dùng: python3 count_params_from_file.py <model1.safetensors> [model2.safetensors ...]")
sys.exit(1)
all_reports = []
for p in sys.argv[1:]:
rpt = count_params(p)
all_reports.append(rpt)
print_report(rpt)
print("=" * 70)
print()
if len(all_reports) > 1:
print_comparison(all_reports)
|