前阵子接手一个视觉检测项目,方案是用 DINOv2 做特征基座、接 YOLO 检测头做目标定位,整体叫它“DINO-YOLO”组合。代码结构不复杂,真正卡人的反而是一开始谁都没当回事的一步:模型权重文件怎么离线准备好。目标机器完全没有外网,所有依赖、模型权重都得用移动介质带进去,我第一次直接在那台机器上跑加载脚本,结果卡在“Downloading model weights”半天没动静。后来老老实实把整套离线下载流程梳理了一遍,顺带踩了一堆缓存、校验、路径相关的坑。这篇文章就是这套流程的完整记录,适合正在做内网部署、离线环境迁移,或者单纯想搞明白模型权重到底缓存在哪的开发同学参考。
1. 先把家底摸清楚:DINO-YOLO组合的权重都放哪了
做离线下载之前,首先得知道东西在哪。DINO-YOLO这种组合不是单一模型,它至少涉及两套预训练权重,来源完全不同,默认缓存位置也各管各的。你如果直接用代码里的默认行为去联网加载,它下完之后存在哪你都不一定清楚,等到要打包带走的时候才发现目录结构一脸懵。
1.1 DINOv2的特征基座权重,藏在HuggingFace的仓库结构里
DINOv2 是自监督学习训练出来的视觉特征提取模型,常见的有vit_small、vit_base、vit_large这几个规格。官方仓库把权重托管在 HuggingFace 上,repo 的命名是facebook/dinov2,里面直接能看到.pth和.safetensors格式的权重文件。
这里有个容易忽略的点:HuggingFace 的下载机制默认不是把文件放到你指定的普通目录,而是维护一个带blobs和snapshots两层的缓存结构,路径一般在~/.cache/huggingface/hub下方。snapshots 目录里保存的是符号链接,真正的大文件实体在 blobs 里。这种机制对日常使用很友好,但对离线打包是个坑,后面我会单独说。如果你只想拿到一个干净的单文件,最好用hf_hub_download的local_dir参数,或者直接用huggingface-cli download配合--local-dir。
1.2 YOLO检测端权重,来自Ultralytics的自动缓存机制
YOLO 这边就更隐蔽了。如果你用的是 Ultralytics 生态,代码里写一行YOLO("yolov8n.pt"),第一次执行时它会自动从 GitHub Release 下载权重,然后存到~/.cache/ultralytics目录。最近几个版本默认下载yolo11n.pt、yolov8n.pt这些命名,具体下载地址是https://github.com/ultralytics/assets/releases。
这个自动下载机制平时确实省事,但它的缓存目录不会在项目里显式出现,很容易被忽略。我见过不少同事在联网机器上跑通了代码,到部署阶段才发现根本不知道.pt文件被放到了哪里。排查方法很简单:去~/.cache/ultralytics看一眼,或者用ultralytics.utils里导出的配置信息确认当前权重目录。提前把这个目录整个拷走,比在部署环境里重新下载靠谱得多。
1.3 为什么不建议在目标环境里联网下载
有人可能会想:既然权重目录都知道了,那我在部署机器上联网跑一次,让它自己下不就行了?问题是内网机器往往根本没外网权限,就算有,跨地区访问 GitHub 或 HuggingFace 的下载链路也很不稳定,大文件动不动断掉,而且每次重新下载的都是最新 Release 的权重,版本不可控。项目一旦要出问题报告或复现实验,你没法说清楚当时用的到底是哪个 commit、哪个版本的文件。
离线下载模型的核心目标应该是:一次下载,反复使用,版本固定,校验可查。所以我们需要在联网环境里把权重文件准备好,连同校验信息一起打包,再派发到内网。
2. 下载前的准备:一份离线权重包的目录约定
直接开始敲命令下载是新手做法,往往下完之后文件散落各处,等到部署才发现缺这个缺那个。我一般会在动手前先建好一套权重包目录结构,权当给别人,也给自己留一份清晰的索引。
2.1 目录骨架设计
推荐的结构类似这样:
offline_weights/ ├── dinov2/ │ ├── dinov2_vitb14_pretrain.pth │ └── README.md ├── yolo/ │ ├── yolov8n.pt │ └── README.md ├── manifest.json └── checksum.sha256manifest.json记录每个权重的原始下载地址、版本、文件大小、下载时间,checksum.sha256记录每个文件的 SHA256 值。不要觉得这一步多余——等到离线环境加载模型报错,你排查是否是文件损坏时,这份校验清单能直接帮你排除一个变量。
2.2 记录下载清单与校验值
我自己写 manifest 的习惯是每个文件一行 JSON,比如这样:
{ "files": [ { "name": "dinov2_vitb14_pretrain.pth", "source": "https://huggingface.co/facebook/dinov2/resolve/main/dinov2_vitb14_pretrain.pth", "sha256": "a1b2c3d4...", "size": 1117241506, "date": "2024-xx-xx" } ] }有了这份清单,哪怕下载工具中途换了、机器重装了,你也能随时复现下载过程。运维同事拿到这份文件也可以对照着逐项校验,不用反复问你“这个文件哪来的”。
2.3 选好下载通道:直连、镜像与代码库
下载大模型权重的通道选择,直接影响成功率和速度。对 HuggingFace 上的文件,最简单的是直接用官方命令行工具,把默认 endpoint 切到国内镜像站,比如设置环境变量HF_ENDPOINT=https://hf-mirror.com。这是官方认可的镜像方式,不涉及任何额外工具,只需要在下载前 export 这个变量即可。
对 YOLO 权重这类放到 GitHub Release 的文件,可以直接用wget或curl从 release 地址拉取。连接不稳定的时候优先用支持断点续传的下载方式,必要时引入aria2c这种多线程下载器。注意不要只依赖浏览器“右键另存为”,浏览器下载中断后通常不支持续传,一旦断掉就得从头再来,几个 GB 的文件很容易白等。
3. 实战下载:两条腿走路,一边断点续传一边校验
准备工作做完,接下来就是真正把权重文件弄下来。这一阶段我习惯“两条腿走路”:能走官方 SDK 的走 SDK,省心且有进度反馈;拿不到官方地址的用命令行工具,加断点续传参数。无论哪种方式,下载完成后必须做校验,不校验不装包。
3.1 下载DINOv2权重:镜像端点和local_dir的正确用法
DINOv2 权重我推荐用 HuggingFace 官方工具加镜像端点。先设置环境变量:
export HF_ENDPOINT=https://hf-mirror.com然后用huggingface-cli download指定 repo 和最终落地目录:
huggingface-cli download facebook/dinov2 --include "*.pth" --local-dir ./offline_weights/dinov2--local-dir这个参数很关键,它会把文件直接下载成普通文件放进指定目录,而不是走默认的缓存符号链接结构。这样离线包里的文件就是实实在在的独立文件,不会一串符号链接拷到内网后全部失效。
如果只需要某一个文件,比如只要dinov2_vitb14_pretrain.pth,用 Python 的方式更精确:
from huggingface_hub import hf_hub_download hf_hub_download( repo_id="facebook/dinov2", filename="dinov2_vitb14_pretrain.pth", local_dir="./offline_weights/dinov2", )3.2 下载YOLO权重:先跑一次让缓存热起来,再拷走
YOLO 权重有两种稳妥做法。
一种是在联网机器上用 Ultralytics 代码触发一次下载,让模型文件落到~/.cache/ultralytics,然后把这个目录整个拷贝到离线权重包:
from ultralytics import YOLO model = YOLO("yolov8n.pt")这样做的优点是版本由 Ultralytics 锁定的默认版本,和代码行为一致,不会出现你手动下载的权重文件和库要求的版本不匹配的问题。缺点是你得记住它默认下到了哪里,好在检查~/.cache/ultralytics基本一找一个准。
另一种是直接手动下载。Ultralytics 权重发布在 GitHub assets 仓库,URL 规律比较明显,比如:
wget -c https://github.com/ultralytics/assets/releases/download/v8.3.0/yolov8n.pt -O ./offline_weights/yolo/yolov8n.pt-c参数表示断点续传,如果中途网络中断,重新执行会从已下载的位置继续,不用从头再来。文件不大的情况下一次就成功了,但碰上大模型如yolov8x或yolo11x,这个参数的价值立刻体现。
3.3 完整性校验:别让坏文件混进离线包
下载完成不等于能用。我在第一阶段吃过亏:某个.bin文件下载时看似完整,解压后才发现少了几十 MB,模型加载直接报错,查了半天才发现是文件损坏。从那以后所有权重必须过 SHA256 校验。
先计算当前文件的哈希值:
sha256sum ./offline_weights/dinov2/dinov2_vitb14_pretrain.pth > checksum.sha256把计算结果和你从官方仓库看到的 SHA256 值比对。HuggingFace 页面一般能看到文件详情,GitHub Release 描述里也可能列出校验值。如果源站没直接给出,那至少保证本地生成一份清单,派发之后用于离线环境二次校验,确保搬运过程中没有损坏。
4. 搬到内网:模型加载阶段的离线适配
权重文件拷贝进内网机器之后,只完成了 50%。另一半是代码层面的加载适配。因为大多数现代深度学习代码都默认“联网自动下载”,到了离线环境,这个默认行为反而成了报错源头。
4.1 DINOv2在离线机器上的加载方式
最典型的坑是网上教程推荐的加载方式:
import torch model = torch.hub.load("facebookresearch/dinov2", "dinov2_vitb14")这行代码在联网环境没问题,但它在加载权重之前需要先去 GitHub 拉取hubconf.py,离线环境会直接超时。哪怕你把权重已经下载到了本地,torch.hub.load也可能因为拿不到配置文件而失败。
正确做法是把hubconf.py一起离线准备好,或者完全绕开 torch.hub,用本地状态字典加载。我这边习惯直接读取权重文件:
import torch state_dict = torch.load( "./offline_weights/dinov2/dinov2_vitb14_pretrain.pth", map_location="cpu", ) # 然后按官方仓库中定义模型结构的代码,实例化模型后 load_state_dict model.load_state_dict(state_dict)同理,很多封装好的算法仓库都把权重下载写死在初始化逻辑里,你需要找到那行下载调用,改成“本地路径优先”。最干净的方式是让模型初始化支持一个weights参数,传进来一个文件路径,而不是模型名。
4.2 YOLO的本地路径加载与缓存目录环境变量
YOLO 这方面其实比较友好,YOLO("./offline_weights/yolo/yolov8n.pt")直接传本地绝对路径,它就不会触发网络请求。这里要注意的是,即使传了本地路径,Ultralytics 初始化时仍可能尝试读取配置或检查更新,如果内网完全封闭,可能卡在某个超时等待上。实测中可以通过设置环境变量来改变配置目录:
export ULTRALYTICS_CONFIG_DIR=/data/offline_model_cache/ultralytics把之前拷过去的~/.cache/ultralytics整个目录放到内网机器的这个路径下。同理,HuggingFace 相关组件也可以通过HF_HOME或HUGGINGFACE_HUB_CACHE指定缓存位置:
export HF_HOME=/data/offline_model_cache/huggingface4.3 离线部署自检清单
每换一台内网机器,我都会过一遍下面这个清单,确认离线加载不会在某个角落冷不丁发起网络请求:
- 检查有没有硬编码的
http://或https://权重链接在代码里; - 确认所有
torch.hub.load都替换成了本地加载; - 确认
YOLO(...)传入的是本地绝对路径; - 确认环境变量
HF_HOME、ULTRALYTICS_CONFIG_DIR已设置且目录存在; - 用
strings或文本搜索检查模型配置文件里是否有网络地址。
这步做好了,离线机器上运行起来才能和联网环境行为一致。
5. 几轮内网部署下来攒的坑与对策
这部分是我最想说的。看似简单的拷贝文件,实际操作中至少有四个坑,每一个都让我多花过半天时间。
5.1 缓存目录里有 .incomplete 文件,加载直接就炸
HuggingFace 下载工具在文件未完成时会生成.incomplete后缀的临时文件。如果下载中断后你没清理缓存,接着重试,工具可能认为缓存命中,但实际上存的是一截残缺数据。这会导致离线加载时权重尺寸不匹配或直接读取失败。
对策是打包之前扫一遍目录,任何.incomplete文件一律删除,重新下载完整后再进离线包。
5.2 符号链接在打包迁移时最容易丢
前面提到 HuggingFace 默认缓存的snapshots目录里是符号链接。在 Linux 上直接拷贝,符号链接还在,但如果你用压缩工具,或者拷到 Windows 机器再转一道,符号链接立刻变成普通文本文件,里面对应的权重数据根本没跟过去,离线运行时要么提示文件不存在,要么读出来的内容是完全错误的。
解决思路是下载时就用local_dir参数落地成普通文件,彻底避开符号链接体系。如果已经下进了默认缓存目录,打包前用cp -rL把符号链接解引用后再拷贝,确保拿到的都是实体文件。
5.3 版本漂移:Ultralytics一升级就偷偷换权重
还有一次遇到的现象是:代码里明明写的是yolov8n.pt,在内网机器上跑起来却提示权重不存在。后来发现是升级 Ultralytics 后,新版本要求新的默认权重文件名,旧文件名不再自动下载。内网环境没有外网,这种情况基本无解,只能回退版本或者重新带一份新权重进去。
所以离线包里除了权重文件,我还建议把产生这些文件的库版本记在 manifest 里,例如ultralytics==8.3.0、torch==2.1.0。这样即使代码库升级,也能快速判断是版本不匹配还是文件缺失。
5.4 并发下载大文件时锁冲突
如果你用多个进程同时下载同一个 HuggingFace 仓库的不同文件,有时会遇到缓存目录锁冲突。表面上看是文件被占用,实际是 huggingface_hub 的并发锁机制导致某个进程等待。解决方式一是尽量串行下载,二是给不同进程指定不同的cache_dir,避免共享同一个锁目录。
这个问题在平时几乎碰不到,但如果你写了个批量脚本一口气下载十几个模型文件,大概率会撞上。提前知道有这回事,遇到提示.lock文件之类的问题就不会慌。
回过头看,离线下载模型这件事本身不复杂,难的是把“联网环境的一次性下载”变成“离线环境里可复现、可校验、可派发的静态产物”。我做这套流程之后,每次新项目部署都直接走同一个模板:建目录、写 manifest、下载、校验、打包、派发、用环境变量指过去。刚开始觉得多花了一天整理流程,后面每一台机器部署省下的时间都不止一天。最后再分享一个小技巧:所有离线权重包在派发前,找一台好机器完整跑一遍加载验证,别等到现场才发现某个文件被漏了。