AI 工具

魔搭 ModelScope:从魔搭下载模型时,最常见的问题是“下载了不需要的大文件”和“下载中断后不知道文件是否完整”

从魔搭下载模型时,最常见的问题是“下载了不需要的大文件”和“下载中断后不知道文件是否完整”。 这篇写一个 msfetch.py: - 用官方 SDK 的 snapshotdownload(allowpatterns=...) 只下载需要的文件; - 再用魔搭仓库文件列表接口返回的 Sha256 和 Size,逐个校验本地文件; - 有缺失或不一致时,退出码为

BINBHO · 图文教程

从魔搭下载模型时,最常见的问题是“下载了不需要的大文件”和“下载中断后不知道文件是否完整”。 这篇写一个 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 校验一遍才放心。