从魔搭下载模型时,最常见的问题是“下载了不需要的大文件”和“下载中断后不知道文件是否完整”。 这篇写一个
ms_fetch.py:
- 用官方 SDK 的
snapshot_download(allow_patterns=...)只下载需要的文件;- 再用魔搭仓库文件列表接口返回的 Sha256 和 Size,逐个校验本地文件;
- 有缺失或不一致时,退出码为 1。
先看清成品(box 上真实运行)
- 只下载 Qwen2.5-0.5B-Instruct 的配置和分词器文件(共 11.5 MB),9 个文件全部校验通过,许可证开头是
Apache License。- 也试过下载 988 MB 的权重,但 box 上的下载速度只有 80~190 kB/s,下载到 8% 时我手动中断。之后运行校验,报出
✗ 缺失 model.safetensors,退出码为 1;目录里留下了model.safetensors.incomplete。- pytest 4 项通过。
合规提醒:从魔搭下载的每个模型都有自己的许可证(本文用的 Qwen2.5-0.5B-Instruct 仓库里是 Apache License),商用前先读 LICENSE 和模型卡。访问令牌(ms- 开头)只放在环境变量里,不要共享账号或令牌,也不要用多个账号轮换来绕过限额。免费推理服务的额度、并发和使用条件以魔搭官方页面为准,平台可能随时调整,本文不写具体数字和价格。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
ms_fetch.py |
按需下载,校验 SHA-256,并显示许可证开头 |
test_ms_fetch.py |
4 项测试:跳过目录、校验通过、篡改和缺失、接口报错 |
需要准备哪些环境?
Python 3.9 以上;pip install modelscope pytest(box 上 modelscope 的版本是 1.40.2)。公开模型下载不需要令牌。
关键事实
| 事实 | 来源 |
|---|---|
snapshot_download(model_id, revision=, local_dir=, allow_patterns=, ignore_patterns=, token=, ...) |
SDK 1.40.2 的函数签名 |
GET https://www.modelscope.cn/api/v1/models/{id}/repo/files?Revision=master&Recursive=true,返回的每个文件带有 Path、Size、Sha256、Type(blob 或 tree) |
box 实测 |
模型不存在时,返回 HTTP 200,{"Code":10010205001,"Message":"获取模型目录树失败,信息:record not found","Success":false} |
box 实测 |
文件结构
61-modelscope-A-src/
├── ms_fetch.py
└── test_ms_fetch.py
第 1 步:下载和校验脚本
"""从魔搭(ModelScope)按需下载模型文件,并逐个校验 SHA-256。
- 用官方 SDK modelscope.snapshot_download 下载(allow_patterns 只取需要的文件)
- 用官方仓库文件列表接口 /api/v1/models/{id}/repo/files 取得每个文件的 Sha256 和 Size,与本地文件比对
- 下载前打印许可证文件开头,提醒先读许可证"""
import argparse, fnmatch, hashlib, json, pathlib, sys, urllib.parse, urllib.request
API = "https://www.modelscope.cn/api/v1/models/{mid}/repo/files?Revision={rev}&Recursive=true"
UA = "binbho-tutorial/1.0"
def remote_files(model_id, revision="master", opener=urllib.request.urlopen):
url = API.format(mid=model_id, rev=urllib.parse.quote(revision))
with opener(urllib.request.Request(url, headers={"User-Agent": UA}), timeout=30) as r:
d = json.load(r)
if d.get("Code") != 200 or not d.get("Success", True):
raise RuntimeError(f"文件列表接口失败:{d.get('Code')} {d.get('Message')}")
return {f["Path"]: f for f in d["Data"]["Files"] if f.get("Type") == "blob"}
def sha256(p, buf=1 << 20):
h = hashlib.sha256()
with open(p, "rb") as f:
while chunk := f.read(buf):
h.update(chunk)
return h.hexdigest()
def verify(local_dir, files, patterns):
"""返回 (ok 列表, 问题列表)。只检查匹配 patterns 的文件。"""
ok, bad = [], []
for path, meta in sorted(files.items()):
if not any(fnmatch.fnmatch(path, p) for p in patterns):
continue
p = pathlib.Path(local_dir, path)
if not p.exists():
bad.append(f"缺失 {path}"); continue
if p.stat().st_size != meta["Size"]:
bad.append(f"大小不符 {path}: 本地 {p.stat().st_size} / 远端 {meta['Size']}"); continue
if meta.get("Sha256") and sha256(p) != meta["Sha256"]:
bad.append(f"SHA-256 不符 {path}"); continue
ok.append(path)
return ok, bad
def main(argv=None):
ap = argparse.ArgumentParser()
ap.add_argument("model_id"); ap.add_argument("--dir", required=True)
ap.add_argument("--revision", default="master")
ap.add_argument("--include", nargs="+", default=["*.json", "*.txt", "LICENSE*", "README.md"])
ap.add_argument("--verify-only", action="store_true")
a = ap.parse_args(argv)
files = remote_files(a.model_id, a.revision)
total = sum(m["Size"] for p, m in files.items() if any(fnmatch.fnmatch(p, q) for q in a.include))
print(f"{a.model_id}@{a.revision}:匹配 {a.include} 的文件共 {total / 1e6:.1f} MB")
if not a.verify_only:
from modelscope import snapshot_download
snapshot_download(a.model_id, revision=a.revision, local_dir=a.dir, allow_patterns=a.include)
lic = next((pathlib.Path(a.dir, p) for p in files if p.upper().startswith("LICENSE")), None)
if lic and lic.exists():
print("许可证开头:", lic.read_text(errors="replace").strip().splitlines()[0][:80])
ok, bad = verify(a.dir, files, a.include)
for b in bad:
print("✗", b)
print(f"校验通过 {len(ok)} 个文件,问题 {len(bad)} 个")
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main())
第 2 步:测试
import hashlib, io, json
import pytest
import ms_fetch
def listing(files):
body = {"Code": 200, "Success": True, "Data": {"Files": [
{"Path": p, "Type": "blob", "Size": len(b), "Sha256": hashlib.sha256(b).hexdigest()} for p, b in files.items()]
+ [{"Path": "sub", "Type": "tree", "Size": 0, "Sha256": ""}]}}
return lambda req, timeout: io.BytesIO(json.dumps(body).encode())
FILES = {"config.json": b'{"a":1}', "tokenizer.json": b"{}", "model.safetensors": b"\0" * 10}
@pytest.fixture
def repo(tmp_path):
for p, b in FILES.items():
(tmp_path / p).write_bytes(b)
return tmp_path
def test_remote_files_skips_dirs():
fs = ms_fetch.remote_files("x/y", opener=listing(FILES))
assert set(fs) == set(FILES)
def test_verify_ok(repo):
ok, bad = ms_fetch.verify(repo, ms_fetch.remote_files("x/y", opener=listing(FILES)), ["*.json"])
assert ok == ["config.json", "tokenizer.json"] and bad == []
def test_verify_detects_tamper_and_missing(repo):
(repo / "config.json").write_bytes(b'{"a":2}') # 大小相同,内容不同
(repo / "model.safetensors").unlink()
ok, bad = ms_fetch.verify(repo, ms_fetch.remote_files("x/y", opener=listing(FILES)), ["*"])
assert "SHA-256 不符 config.json" in bad and "缺失 model.safetensors" in bad
def test_api_error_raises():
op = lambda req, timeout: io.BytesIO(json.dumps({"Code": 10010205001, "Message": "not found"}).encode())
with pytest.raises(RuntimeError, match="not found"):
ms_fetch.remote_files("x/none", opener=op)
第 3 步:运行
$ python ms_fetch.py Qwen/Qwen2.5-0.5B-Instruct --dir models/qwen25-05b
Qwen/Qwen2.5-0.5B-Instruct@master:匹配 ['*.json', '*.txt', 'LICENSE*', 'README.md'] 的文件共 11.5 MB
许可证开头: Apache License
校验通过 9 个文件,问题 0 个
$ python ms_fetch.py Qwen/Qwen2.5-0.5B-Instruct --dir models/qwen25-05b --include '*.json' '*.txt' 'LICENSE' '*.safetensors' --verify-only
Qwen/Qwen2.5-0.5B-Instruct@master:匹配 [...] 的文件共 999.6 MB
✗ 缺失 model.safetensors
校验通过 8 个文件,问题 1 个
$ python -m pytest -q
4 passed
调试示例
1. 中断的下载不会伪装成完整文件。 权重下载到 75 MB 时我手动中断,目录里只有 model.safetensors.incomplete,没有 model.safetensors,所以校验报的是“缺失”,而不是“SHA 不符”。如果你自己写下载器,直接写入最终文件名,中断后就会留下一个看起来正常、实际不完整的文件,这时大小和 SHA-256 校验就是最后一道防线。test_verify_detects_tamper_and_missing 专门测试了“大小相同、内容不同”的情况。
2. 接口出错时,HTTP 状态码仍然是 200。 查询不存在的模型时,HTTP 状态码是 200,错误信息在 JSON 的 Code 和 Success 字段里。所以 remote_files 要检查 Code == 200,不能只看 HTTP 状态码。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 下载了几十 GB 的不需要的文件 | 没有设置 allow_patterns | 先用列表接口看每个文件的大小,再设置模式 |
| 下载很慢 | 网络环境 | 换网络或时间段;只下载需要的文件 |
| 校验报“SHA-256 不符” | 文件损坏,或者 revision 不一致 | 删除该文件后重新下载,并固定 --revision |
| 私有模型返回 401 | 需要令牌 | 用 token= 或 modelscope login,令牌放在环境变量里 |
模板规则
- 先看文件列表和大小,再下载。
- 固定 revision。
- 下载后一定要做校验。
- 先读许可证,再使用模型。
项目资源
| 用途 | 地址 |
|---|---|
| 魔搭社区 | https://www.modelscope.cn |
| modelscope SDK 源码 | https://github.com/modelscope/modelscope |
| 示例模型 Qwen2.5-0.5B-Instruct | https://www.modelscope.cn/models/Qwen/Qwen2.5-0.5B-Instruct |
下载完不等于下载对了,用 SHA-256 校验一遍才放心。