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)