| |
| """ |
| Đế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 |
|
|
|
|
| |
| |
| DTYPE_BYTES = { |
| "F64": 8, "F32": 4, "F16": 2, "BF16": 2, |
| "I64": 8, "I32": 4, "I16": 2, "I8": 1, "U8": 1, "BOOL": 1, |
| } |
|
|
| |
| |
| |
| 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)" |
|
|
|
|
| |
| |
| 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: |
| |
| header_len = struct.unpack("<Q", f.read(8))[0] |
| header = json.loads(f.read(header_len)) |
| |
| header.pop("__metadata__", None) |
| return header |
|
|
|
|
| def count_params(path: str) -> dict: |
| header = load_header(path) |
|
|
| total_elems = 0 |
| learnable = 0 |
| buffers = 0 |
| 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 |
| 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 |
|
|
| |
| 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() |
|
|
| |
| |
| |
| 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) |
|
|