File size: 8,685 Bytes
4afe981
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
217
218
219
220
221
222
#!/usr/bin/env python3
"""一键验证:拿到包的人跑这一条命令,就知道包是不是完整、能不能用、数字对不对。

  python verify_package.py            # 完整验证(含 100 条迷你协议复算,需下载视觉塔约 600MB)
  python verify_package.py --quick    # 只查校验和 / 参数量 / 推理连通(不下载视觉塔)

判据只用**客观项**:文件校验和、参数量、权重结构、推理可跑通、同输入结果稳定。
"模型答对了几条"只打印、不参与 PASS/FAIL —— 小模型命中率本来就低,拿它当判据会把好包判死。
"""
from __future__ import annotations

import argparse
import hashlib
import json
import re
import sys
import time
from pathlib import Path

HERE = Path(__file__).resolve().parent
PKG = HERE.parent
sys.path.insert(0, str(HERE))

EXPECT_PARAMS = 39_335_936
EXPECT_LLM = 38_023_680
EXPECT_CONNECTOR = 1_312_256
EXPECT_STEP = 2474
# 公布值(logs/eval_vqa_s3.json,1,812 条 test):迷你集只是同分布抽样,用于对照、不判失败
PUBLISHED_TEST_ACC = 0.2953
PUBLISHED_TEST_N = 1812

ok_all = True


def check(name: str, ok: bool, detail: str = "", hard: bool = True) -> bool:
    global ok_all
    mark = "PASS" if ok else ("FAIL" if hard else "WARN")
    if not ok and hard:
        ok_all = False
    print(f"  [{mark}] {name}" + (f"  {detail}" if detail else ""), flush=True)
    return ok


def sha256(p: Path) -> str:
    h = hashlib.sha256()
    with open(p, "rb") as f:
        for b in iter(lambda: f.read(1 << 20), b""):
            h.update(b)
    return h.hexdigest()


def step_sums() -> None:
    print("\n[1/5] 文件校验和 (SHA256SUMS)")
    sf = PKG / "SHA256SUMS"
    if not sf.is_file():
        check("SHA256SUMS 存在", False)
        return
    bad = miss = 0
    lines = 0
    for line in sf.read_text().splitlines():
        m = re.match(r"^([0-9a-f]{64})\s+(.+)$", line.strip())
        if not m:
            continue
        lines += 1
        want, rel = m.group(1), m.group(2)
        p = PKG / rel
        if not p.is_file():
            print(f"       缺文件: {rel}")
            miss += 1
        elif sha256(p) != want:
            print(f"       校验不符: {rel}")
            bad += 1
    check(f"校验 {lines} 个文件", bad == 0 and miss == 0,
          f"不符 {bad} / 缺失 {miss}" + ("" if bad == 0 and miss == 0 else " → 包被改过或传输损坏"))


def step_weights() -> None:
    print("\n[2/5] 权重结构与参数量")
    w = PKG / "weights" / "duovlm-s3-final.pth"
    if not w.is_file():
        check("权重文件存在", False, str(w))
        return
    import torch

    d = torch.load(w, map_location="cpu")
    check("顶层键 llm/connector/step/extra", all(k in d for k in ("llm", "connector", "step", "extra")),
          str(list(d.keys())))
    check("step == 公布值", int(d["step"]) == EXPECT_STEP, f"{int(d['step'])} (期望 {EXPECT_STEP})")
    llm = d["llm"]
    con = d["connector"]
    tied = torch.equal(llm["lm_head.weight"], llm["transformer.wte.weight"])
    n_raw = sum(v.numel() for v in llm.values() if hasattr(v, "numel"))
    # lm_head 与词表 embedding 共享同一份存储 → 可训练参数量要扣掉一次
    n_llm = n_raw - llm["lm_head.weight"].numel() if tied else n_raw
    n_con = sum(v.numel() for v in con.values() if hasattr(v, "numel"))
    check("LLM 参数量", n_llm == EXPECT_LLM,
          f"{n_llm:,} (期望 {EXPECT_LLM:,})"
          + (";权重表条目含共享的 lm_head,已按绑定关系扣除" if tied else ""))
    check("connector 参数量", n_con == EXPECT_CONNECTOR, f"{n_con:,} (期望 {EXPECT_CONNECTOR:,})")
    check("合计可训练参数", n_llm + n_con == EXPECT_PARAMS, f"{n_llm + n_con:,} (期望 {EXPECT_PARAMS:,})")
    check("权重绑定 lm_head == wte", tied, "lm_head 与词表 embedding 共享(省 4.19M 参数)")


def step_infer(quick: bool) -> None:
    print("\n[3/5] 推理连通性")
    from duovlm_infer import DuoVLMInfer

    t0 = time.time()
    v = DuoVLMInfer(verbose=False)
    check("模型载入", True, f"{v.n_params:,} 参数 / {v.dev} / {time.time()-t0:.1f}s")
    check("参数量与权重自洽", v.n_params == EXPECT_PARAMS, f"{v.n_params:,}")
    imgs = sorted((PKG / "protocol" / "images").glob("*.jpg"))
    if not imgs:
        check("包内样图存在", False)
        return
    r = v.ask(str(imgs[0]), "Render a clear and concise summary of the photo.")
    check("看图问答可跑通", bool(r["answer"]), f"『{r['answer']}』 {r['ms']}ms source={r['source']}")
    r2 = v.ask(str(imgs[0]), "Render a clear and concise summary of the photo.")
    check("同输入两次结果一致(解码稳定)", r["answer"] == r2["answer"], f"『{r2['answer']}』")


def step_protocol() -> tuple[int, int]:
    print("\n[4/5] 迷你协议复算(100 条,全部图都在包内)")
    from duovlm_infer import DuoVLMInfer

    pf = PKG / "protocol" / "mini_vqa_100.jsonl"
    if not pf.is_file():
        check("迷你协议文件存在", False, str(pf))
        return 0, 0
    rows = [json.loads(x) for x in pf.read_text().splitlines() if x.strip()]
    v = DuoVLMInfer(verbose=False)
    hit = 0
    out = []
    t0 = time.time()
    for r in rows:
        img = PKG / r["image"]
        got = v.ask(str(img), r["question"], max_new=12, ngram=3)["answer"]
        good = int(norm(got) == norm(r["answer"]))
        hit += good
        out.append({**r, "pred": got, "correct": bool(good)})
    acc = hit / max(len(rows), 1)
    out_path = Path.cwd() / "mini_vqa_100_result.json"     # 写在包外,避免污染包与校验和
    out_path.write_text(
        json.dumps({"n": len(rows), "hit": hit, "acc": round(acc, 4),
                    "seconds": round(time.time() - t0, 1), "rows": out},
                   ensure_ascii=False, indent=1))
    print(f"        本机复算: {hit}/{len(rows)} = {acc*100:.1f}%   ({time.time()-t0:.0f}s,逐条结果见 {out_path})")
    print(f"        公布值  : {PUBLISHED_TEST_N} 条 test 上 {PUBLISHED_TEST_ACC*100:.2f}% "
          f"(本迷你集为同分布抽样,仅作对照)")
    check("复算流程可跑通(不看命中率)", len(out) == len(rows), f"{len(out)}/{len(rows)} 条完成")
    return hit, len(rows)


def norm(s: str) -> str:
    s = str(s).lower().strip()
    for a, b in (("'s", " s"), ("n't", " nt")):
        s = s.replace(a, b)
    s = re.sub(r"[;,!?]", "", s)
    s = re.sub(r"\.", "", s)
    s = re.sub(r"\b(a|an|the)\b", " ", s)
    return " ".join(s.split())


def dep_check() -> None:
    """先查依赖,给一句能照做的报错,而不是一串 ModuleNotFoundError 栈。"""
    missing = []
    for m in ("torch", "transformers", "litgpt", "numpy", "PIL"):
        try:
            __import__(m)
        except ImportError:
            missing.append(m)
    if missing:
        print(f"缺依赖: {', '.join(missing)}\n"
              f"当前解释器: {sys.executable}\n\n"
              f"请先装依赖(建议独立虚拟环境):\n"
              f"  python -m venv .venv && . .venv/bin/activate\n"
              f"  pip install -r requirements.txt\n"
              f"然后用它跑:  .venv/bin/python code/verify_package.py")
        sys.exit(2)
    from importlib.metadata import PackageNotFoundError, version

    import torch
    import transformers

    try:
        lt = version("litgpt")
    except PackageNotFoundError:
        lt = "未安装"
    print(f"解释器: {sys.executable}")
    print(f"  torch {torch.__version__} / transformers {transformers.__version__} / "
          f"litgpt {lt} / cuda {torch.cuda.is_available()}"
          + ("" if lt == "0.5.13" else "   ⚠ 期望 litgpt==0.5.13,其它版本可能接口不兼容"))


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("--quick", action="store_true", help="跳过迷你协议复算(不下载视觉塔)")
    a = ap.parse_args()

    print("=" * 78)
    print("DuoVLM-40M 分发包自检")
    print(f"包目录: {PKG}")
    print("=" * 78)
    dep_check()

    step_sums()
    step_weights()
    if not a.quick:
        step_infer(a.quick)
        step_protocol()
    else:
        print("\n[3/5] 推理连通性 —— --quick 跳过(需要视觉塔)")
    print("\n[5/5] 汇总")
    print("=" * 78)
    print(("全部客观判据通过:包完整、参数对、能跑。" if ok_all else "有检查未通过,见上面 [FAIL]。"))
    print("注:模型答对几条只是参考,不参与判定;要复算头条数字请用完整 VQAv2 test 集。")
    sys.exit(0 if ok_all else 1)


if __name__ == "__main__":
    main()