ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

vLLM卡在modelscope锁等待?一文搞懂原理与五种解决策略

vLLM卡在modelscope锁等待?一文搞懂原理与五种解决策略 你第一次跑vllm serve从 ModelScope 拉权重日志刷到modelscope - INFO - Still waiting to acquire lock就卡住不动一等就是十几分钟甚至更久。相信我这不是你网络的问题也不是 ModelScope 服务端挂了几乎可以断定就是本地文件锁互斥导致的后台进程在互相等待。这篇就把这个报错拆开揉碎讲清楚它的机制、排查思路和根治办法顺便把 vllm 和 ModelScope 配合使用时的那些隐蔽坑也一并解决了。这个报错的核心是 ModelScope 下载组件内置了一个基于文件锁的并发保护机制。它的本意是防止多个 Python 进程同时往同一个缓存目录写同一个模型文件结果现实里却经常因为残留锁、多进程下载同一模型、缓存目录权限问题而卡死。适合正在用 vllm 本地部署 DeepSeek、Qwen 这类模型尤其是习惯用MODELSCOPE_CACHE自定义缓存路径的人参考。1. 报错现象与锁机制背后的设计逻辑1.1 “Still waiting to acquire lock”到底在等什么ModelScope 的 Python SDK 在下载模型前会先在本地缓存目录里创建一个锁文件。锁文件是特殊的空文件路径通常在模型缓存目录下命名类似.lock或者.modelscope_lock。进程拿到锁之后才允许写入当前模型的快照文件下载完成后释放锁再删除锁文件。如果另一个下载同一模型的任务撞上这个场景它会进入一个循环等待modelscope - INFO - Still waiting to acquire lock每过一段时间刷一条日志的时间戳和内容完全一致。看起来像死循环实际上是在等前一个持锁进程结束。这个设计思路本身没问题和数据库行锁、Redis 分布式锁是同一个逻辑核心目的是保证同一个模型目录不会同时被两个进程写入不然下载一半的文件和索引会被彼此覆盖下一次加载就直接 load 出脏数据。问题出在实现层锁是绑定到进程的本地文件锁进程非正常退出无法自动释放锁于是后到的进程只能无限期等待。vllm 的启动方式又经常是多进程或者带ray后端多个 worker 同时拉取同一个模型时锁竞争的概率远高于普通场景。1.2 为什么 vllm 部署大模型时更容易触发vllm 启动时默认会初始化一个模型加载阶段。如果你用pip install vllm且模型没有提前下载到本地vllm 内部会调用snapshot_download这类接口去拉权重这就会走 ModelScope SDK 的下载管线。你会发现 CPU 没跑满、网卡流量几乎为零但日志就是卡在 waiting lock。有几个典型触发场景同一个缓存目录下上一次下载任务被 CtrlC 强制中断锁文件没有被清理。并行启动两个 vllm 实例加载同一个模型两个进程竞争同一个锁。使用 docker 部署容器重建后旧容器的锁文件残留在宿主机挂载的缓存目录里。不同用户跑同一台机器A 用户下载模型时留下了 root 权限的锁文件B 用户没有权限删除锁文件所以永远等不到锁。还有一个隐藏场景ModelScope SDK 的锁文件路径是根据MODELSCOPE_CACHE环境变量动态生成的。如果你这次启动的环境变量和上次不一致比如默认缓存目录为~/.cache/modelscope这次额外赋予了MODELSCOPE_CACHE/data/modelscope那么 locks 和真正模型的存放位置会错位锁文件和缓存内容不匹配就会发生新进程等一个根本不存在的持锁进程。2. 排查定位三步找出是“谁”在持锁2.1 先看锁文件的真实位置不相名不要直接去下载目录里肉眼找ModelScope 不同版本锁文件命名和路径都不完全一样。先打开 Python 实际确认你的缓存路径和锁文件路径python -c from modelscope.hub.file_download import MODEL_SNAPSHOT_METADATA; print(MODEL_SNAPSHOT_METADATA)或者直接查环境变量echo $MODELSCOPE_CACHE如果环境变量为空默认走~/.cache/modelscope/hub。找到模型对应的目录后用ls -la列出全部隐藏文件重点看是否有.lock、.lockfile或类似*.lock结尾的文件。我实测下来老版本 SDK0.11.x 及更早会把锁文件塞在~/.cache/modelscope/hub/.locks/下新版本则直接写在模型目录同级的隐藏文件里。确认锁文件名之后查看锁文件里的内容有时会写入持锁进程的 PID 或者主机名这是最快的定位方式。cat ~/.cache/modelscope/hub/.locks/*.lock如果文件内容是空的也没关系继续下一步。2.2 用 lsof 和 ps 找到持锁进程如果锁文件里没有 PID使用lsof查看哪些进程打开了这个锁文件lsof ~/.cache/modelscope/hub/.locks/模型名.lock正常输出会有一行进程名比如python或python3后面对应的是 PID。拿到 PID 之后ps -ef | grep PID确认这个 PID 是否还活着。如果进程存在且是正常的下载进程那就让它继续下载你只需要等待。如果进程已经不存在但锁文件还躺在那里这就是典型的 stale lock直接删掉锁文件即可。2.3 确认是不是多进程并发下载同一模型vllm 和 ModelScope 的锁冲突另一个常见原因是你在同一台机器上起了多个 vllm 实例或者跑了多个python脚本。用下面的命令查看所有涉及模型下载的进程ps -ef | grep -E python.*(vllm|modelscope|snapshot_download) | grep -v grep如果你看到两个以上的进程同时存在且它们在同一个缓存目录下工作那 lock 的等待是必然的。解决办法是只保留一个进程其他全部 kill。这里不要犹豫等待并不能解决问题进程间锁冲突不会因为时间推移自动解除。3. 逐个击破五种方案彻底解决锁等待3.1 方案一手动删除残留锁文件最快确认没有其他下载进程在运行后直接删除锁文件即可。优先使用绝对路径rm -f ~/.cache/modelscope/hub/.locks/*.lock rm -f ~/.cache/modelscope/hub/models/模型名称/.lock如果.lock文件是隐藏的用ls -la确认。删除锁文件后重新启动 vllm正常情况下会跳过等待锁直接进入下载流程。注意删除锁文件之前务必确认没有任何进程正在写模型资源。如果某个下载进程还在后台运行强制删除锁文件会导致它与新进程同时写入模型文件可能损坏。判断当前有没有活动下载进程最稳的办法是连续跑两次lsof间隔 2 秒如果输出变化且文件被某个 PID 持有说明下载仍在进行这时不要删锁。3.2 方案二kill 掉僵死的旧进程如果你发现锁文件被一个已变成僵尸进程或者无法响应的旧进程持有先 kill 再删锁kill -9 PID如果kill -9都不生效检查进程状态是 D 状态不可中断睡眠还是 Z 状态僵尸进程。D 状态往往是磁盘 I/O 卡死需要重启机器或者等待磁盘恢复。Z 状态说明进程已经结束只是父进程没有回收资源这种情况杀掉父进程即可ps -ef | grep 父PID kill -9 父PID实际上进程已经结束但锁未释放是一个很经典的坑。ModelScope 的锁是基于fcntl.flock的这个锁有个特性进程退出或文件描述符关闭时内核会自动释放锁。但这只适用于持有锁的进程还活着的情形。如果是文件锁记录在了磁盘的元数据里进程被kill -9后内核理论上是会释放掉锁的。你在现实中看到“进程没了但锁还在”的情况多半是锁文件本身没有被 SDK 清理或者你看到的是另一个挂载目录里的文件又或者锁文件其实是符号链接指向了一个已经无效的路径。3.3 方案三统一缓存目录并加环境变量如果频繁因为多实例起冲突与其每次去删锁不如从架构上解决将 ModelScope 缓存目录固化到一个路径并在 vllm 启动前显式声明。export MODELSCOPE_CACHE/data/modelscope export MODELSCOPE_TIMEOUT60 export VLLM_USE_MODELSCOPETrue vllm serve Qwen/Qwen2.5-7B-Instruct-GPTQ-Int4 \ --port 8000 \ --tensor-parallel-size 2单机上只需要一个 vllm 实例时缓存目录统一之后锁冲突概率会大幅下降。但也注意如果两个 vllm 实例是同时启动、同时抢同一模型锁还是会冲突。所以更推荐的做法是“先下载后启动”一步到位绕开运行时下载的锁竞争问题python -c from modelscope import snapshot_download; snapshot_download(Qwen/Qwen2.5-7B-Instruct-GPTQ-Int4, local_dir/data/modelscope/qwen2.5-7b)模型显式放到本地路径后vllm 启动时直接用vllm serve /data/modelscope/qwen2.5-7b --port 8000这类做法的好处是vllm 即便内部再去调 ModelScope也会因为文件已存在且 snapshot 版本与远端一致而快速跳过。文件锁的等待最多持续几秒不会卡死。3.4 方案四解决权限导致的死锁多用户机器上经常遇到。一个用户以 root 身份下载模型后另一个普通用户再次启动 vllm 拉同一模型锁文件可能在 root 的目录下普通用户没有写权限无法创建自己的锁也无法删除 root 的锁只能反复等待。检查当前用户对缓存目录的实际写权限namei -m /root/.cache/modelscope/hub如果权限不足要么给目录添加写权限要么切换到对应用户操作chmod -R 777 /root/.cache/modelscope/hub但不建议直接 chmod 777 到多用户共享目录安全风险高。更合理的方式是把模型下载到公共的可写目录比如/opt/models再设置组权限chown -R modeladmin:modelgroup /opt/models chmod -R 775 /opt/models后续所有用户统一使用这个缓存路径锁文件也会落到这个目录谁都可以管理。3.5 方案五给 SDK 打补丁缩短锁等待时间进阶如果你排查过一轮发现锁文件被一个永远等不到释放的进程持有但进程又删不掉比如进入了 D 状态可以考虑临时修改 ModelScope SDK 的超时等待逻辑。找到modelscope/hub/file_download.py或 modelscope/hub/api/...这个方案我一般不建议除非你项目工期紧张。修改 SDK 源码里的waiting_lock相关参数核心是给等待加一个超时比如超过 10 秒直接报错退出而不是无限循环。但注意升级 SDK 后修改会失效属于紧急规避手段不适合长期使用。想用可以找到 SDK 的file_download.py里类似while True的地方拉一个timeout计数器。最干净的根治方式是下载模型和启动 vllm 解耦永远不要让 vllm 在运行时临时去拉大权重。模型一次性下载完毕后vllm 直接加载本地路径这样锁竞争的触发面就被完全切断了。4. vllm 与 ModelScope 集成的四个关键细节4.1 环境变量VLLM_USE_MODELSCOPE的开关逻辑vllm 本身默认是从 Hugging Face Hub 拉模型只有设置了这个环境变量才会走 ModelScope 源export VLLM_USE_MODELSCOPETrue这个环境变量在实际使用中只要设置了就会生效不区分大小写也可以。但在最新版本的 vllm 里这个开关已经逐步被--model-impl modelscope这种 CLI 参数替代。建议在启动命令里同时保留环境变量和参数避免版本升级后的兼容性问题。4.2 “先下载后启动”是最省心的路径在本地部署 DeepSeek R1、Qwen3 这类大参数模型时权重文件动辄几十 GB。vllm 运行时下载的特点是一边下载一边加载网络抖动和锁冲突都会被放大。我强烈建议先执行独立的下载脚本python -c from modelscope import snapshot_download; snapshot_download(deepseek-ai/DeepSeek-R1-Distill-Qwen-7B, local_dir/data/models/deepseek-7b)这个脚本跑完后再启动 vllm 时你甚至都不需要再依赖VLLM_USE_MODELSCOPETrue直接给本地路径即可vllm serve /data/models/deepseek-7b --tensor-parallel-size 2 --max-model-len 8192两段命令解耦后即便中间一次下载失败了重新执行下载脚本也不会和 vllm 的启动进程产生锁竞争。4.3 多实例部署时不要共享同一个 local_dir有些人一台机器上要起两个 vllm 服务分别服务不同业务线但模型是同一个。图省事两个服务都指向同一个模型目录。这不会马上报错但锁竞争的概率会明显上升而且在模型热加载或者版本出快照时可能互相干扰。建议每个实例使用独立快照目录vllm serve /data/models/qwen3-8b-instance1 --port 8001 vllm serve /data/models/qwen3-8b-instance2 --port 8002或者用local_dir分别下载两份权重磁盘够就分开不够就接受启动时的互斥等待用脚本串行启动。4.4 镜像源仓库名的正确写法一个很容易踩的小坑ModelScope 上的模型 id 与 Hugging Face 不完全一致。deepseek-ai/DeepSeek-R1-Distill-Qwen-7B在 ModelScope 上是同一个 id但Qwen/Qwen2.5-7B-Instruct-GPTQ-Int4这类带有后缀的量化模型在 ModelScope 上有时会拆成不同的仓库。用 vllm 加载时模型 id 写错会直接报 404但日志不会显示 lock 等待而是显示下载失败。如果你是从 Hugging Face 切到 ModelScope先到 modelScope 官网或调用 search 接口确认完整 id再填入 vllm。python -c from modelscope.hub.api import HubApi; apiHubApi(); print(api.get_model(Qwen/Qwen2.5-7B-Instruct-GPTQ-Int4))5. 常见问题速查表与最终避坑心得下面把这些年折腾 vllm 和 ModelScope 最常见的现象汇总成表你可以对照排查。现象直接原因解决方案日志反复输出 Still waiting to acquire lock残留锁文件确认无下载进程后删除锁文件锁文件有记录但找不到进程父进程未回收或僵尸进程查找父进程 kill 后删除锁文件同一模型多实例启动多进程锁竞争预下载本地目录串行启动普通用户提示锁文件权限不够缓存目录被其他用户占用统一缓存目录并授权Docker 重建后卡等待宿主机挂载目录残留旧锁挂载前清理锁文件vllm 启动后长时间不下载模型 id 错误或者镜像源没生效检查 VLLM_USE_MODELSCOPE 和模型 id下载到一半断网恢复后卡住SDK 缓存索引损坏删除该模型整个快照目录重新下载还有个很容易被忽略的问题ModelScope SDK 版本不同锁行为也会有差异。如果你的环境里装了很老的版本可以直接先升级pip install -U modelscope升级后锁文件的路径可能发生变化之前残留的旧锁文件需要手动清理干净升级后第一次启动往往是最容易出错的清完锁再启动。最后说点实在的。我一开始遇到这个报错时第一反应也是怀疑网络和镜像源折腾了大半小时最后才发现是上次一个下载脚本被中断锁文件残留。后来我给自己定了一条规矩凡是多实例加载同一个大模型绝不靠 vllm 自动去拉权重而是先用snapshot_download把它落盘再启动服务。这个习惯帮我省掉了后面无数次的锁等待问题。如果你后续需要反复调整模型或做微调测试也强烈建议在缓存目录上做软链而不是直接复制权重模型占用空间小则无所谓大模型几十 GB 复制时间浪费在锁上就更不值得了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进