背景痛点:为什么“下模型”比“跑模型”还累?
第一次用 ChatTTS 做语音合成 Demo 时,我把脚本跑到服务器上,结果卡在 1.8 GB 的chattts-v1.pt整整两天——不是 502 就是下到 99 % 断线重来。
更尴尬的是,同组小伙伴用迅雷手动拖下来一份,MD5 却对不上,推理直接报size mismatch,版本号看似一致,内部权重 key 却悄悄升了级。
总结下来,常见坑位就这三点:
- 网络超时:单连接 TCP 一旦丢包,整个文件前功尽弃
- 大文件传输不稳定:>1 GB 的权重,NAS、家用宽带晚高峰速率抖动,分分钟掉线
- 版本冲突:GitHub Release 里 tag 一样,但作者偷偷 re-upload,不 checksum 根本发现不了
技术方案:三种下载姿势的权衡
场景限定在公司开发机,无外网 P2P 白名单,于是我只对比了“HTTP 直拖 vs 分块断点续传”两条路线,顺手把 P2P 思路也列出来,方便读者扩展。
- HTTP 直接下载:代码最少,适合小文件;失败成本高,大文件几乎不可用
- 分块下载(Range-request):把 1.8 GB 切成 64 MB 一块,任意一块失败只重试该块;支持多线程,能把 20 Mbps 小水管跑满
- P2P(BitTorrent/IPFS):带宽利用率高,校验自带;但需要 tracker 网络或额外网关,企业内网常被封端口
综合下来,分块断点续传是“能落地 + 改动小 + 不依赖运维”的最优解。
核心实现:带校验的断点续传
思路一句话:
“本地先建一个和远程同大小的空文件 → 按块请求写入 → 每完成一块记 checkpoint → 全部块 OK 后做 MD5 比对 → 失败块自动重试”。
关键点:
- 用
requests.get(..., headers={'Range': 'bytes=start-end'})取块 - 块大小建议 8~64 MB,过小 HTTP 握手开销大,过大失去断点意义
- 校验放在最后,避免每块 MD5 把磁盘 I/O 打满
- 重试采用“指数退避 + 最大 5 次”,超次写入日志并抛异常,防止死循环
代码时间:Python 3.8+ 可直接跑
下面这份脚本我放在项目scripts/pull_model.py,CI 里调用,Windows / Linux 行为一致。
#!/usr/bin/env python3 """ chattts_pull.py 分块下载 + 断点续传 + MD5 校验 用法: python chattts_pull.py --url https://github.com/xxx/chattts-v1.pt/releases/download/v1/chattts-v1.pt \ --md5 8f04c8bf6ea2b8e83a... \ -o ./models/chattts-v1.pt """ import os, sys, time, hashlib, requests from tqdm import tqdm CHUNK_SIZE = 32 * 1024 * 1024 # 32 MB MAX_RETRY = 5 SESSION = requests.Session() SESSION.headers.update({'User-Agent': 'chattts-pull/1.0'}) def download_chunk(url, start, end, fd, pbar): """拉取并写入单块,带重试""" headers = {'Range': f'bytes={start}-{end}'} for attempt in range(1, MAX_RETRY + 1): try: r = SESSION.get(url, headers=headers, stream=True, timeout=30) r.raise_for_status() for piece in r.iter_content(chunk_size=1024 * 64): fd.write(piece) pbar.update(end - start + 1) return except Exception as e: wait = 2 ** attempt print(f"[warn] chunk {start}-{end} fail {attempt}/{MAX_RETRY}: {e}, retry in {wait}s") time.sleep(wait) raise RuntimeError(f"chunk {start}-{end} still fail after {MAX_RETRY} retries") def already_done(checkpoint, total_size): """判断本地文件是否已完整""" return os.path.exists(checkpoint) and os.path.getsize(checkpoint) == total_size def pull(url, expect_md5, out_file): # 0. 基本路径准备 tmp_file = out_file + '.downloading' os.makedirs(os.path.dirname(out_file), exist_ok=True) # 1. 拿文件大小 head = SESSION.head(url, allow_redirects=True) total_size = int(head.headers['Content-Length']) print(f'remote size={total_size >> 20} MB') # 2. 已存在且大小对则跳过 if already_done(out_file, total_size): print('file already exists, skip download') return # 3. 建立空文件并逐块写 with open(tmp_file, 'wb') as fd, tqdm(total=total_size, unit='B', unit_scale=True) as bar: for start in range(0, total_size, CHUNK_SIZE): end = min(start + CHUNK_SIZE - 1, total_size - 1) download_chunk(url, start, end, fd, bar) # 4. MD5 校验 print('running md5 checksum ...') calc = hashlib.md5() with open(tmp_file, 'rb') as f: for chunk in iter(lambda: f.read(1024 * 1024), b''): calc.update(chunk) if calc.hexdigest() != expect_md5: os.remove(tmp_file) raise ValueError(f'md5 mismatch! expect={expect_md5} got={calc.hexdigest()}') # 5. 成功,原子替换 os.replace(tmp_file, out_file) print('download & verify done ->', out_file) if __name__ == '__main__': import argparse ap = argparse.ArgumentParser() ap.add_argument('--url', required=True, help='direct download url') ap.add_argument('--md5', required=True, help='expected md5 hex') ap.add Avenue('-o', '--output', required=True, help='local path') args = ap.parse_args() pull(args.url, args.md5, args.output)脚本依赖只有requests与tqdm,pip install即可。CI 里调用后,模型落盘路径固定,下游推理脚本直接torch.load(),不再担心“下到一半”的残片。
性能优化:把 20 Mbps 小水管跑满
- 多线程:上面代码是单线程顺序块,若带宽富余,可把
range()拆成若干任务丢进concurrent.futures.ThreadPoolExecutor。经验值:4~8 线程即可,再多会触发远程 CDN 限流 - 本地缓存:
- 对同一模型不同项目,统一软链到
/mnt/shared/models/,避免重复落盘 - 下载前先
HEAD对比Last-Modified,作者没更新就跳过,节省 100 % 流量
- 对同一模型不同项目,统一软链到
- 磁盘预分配:Linux 下
fallocate -l 1.8G file秒建空文件,防止下载中途磁盘占满导致写入失败
避坑指南:版本、空间与依赖
- 版本兼容:ChatTTS 官方 Release 页面只给 tag,不给 commit。拉取后把
config.json中的transformers_version字段与本地环境比对,不一致就新建虚拟环境,别硬怼 - 磁盘空间:记得留 2× 模型大小的余量,下载 + 解压/重命名中间会同时存在两份
- 优雅降级:如果跑 CI 的节点在海外,而模型源国内镜像更快,脚本里加
--mirror选项,先测延迟再选源,失败时自动回退主站,保证流水线稳定性
延伸思考:把套路搬到 Stable Diffusion、Llama 2
这套“分块 + 断点 + 校验”思路其实通用:
- 块大小可按文件尺寸动态算,如 1 GB 以内 8 MB,10 GB 以上 128 MB
- 校验可换成 SHA256,更安全;若仓库自带
.sha256文件,脚本里直接读列表即可 - 多线程数做成自适应:先跑 1 线程测速,逐步上调,直到带宽不再增长或出现 429 就停止
下次再遇到“大模型下载”需求,把脚本拷过去,改两行配置就能用,真正做到“一次编写,到处偷懒”。
折腾完这一圈,我最大的感受是:
让 AI 帮你写代码之前,得先让代码把 AI 的“粮草”稳稳拉到本地。
把下载流程做成可靠、可追踪、可复现的脚本,看似边角料,却能在真正落地时帮团队省下大把等待与重试时间。
希望这份小笔记也能让你的下一次部署少一点熬夜,多一点从容。