开篇:别让一张好卡只跑一个模型
最近这段时间,我一直在折腾 MindIE 推理框架下的多实例部署。最开始的原因很简单——手里那张 80G 的卡只跑一个 7B 模型,显存用了不到一半,剩下的空间闲着也是闲着。后来尝试在同一张卡上同时拉起两个、三个不同模型的推理实例,踩了不少坑,也摸出了一套比较顺手的配置方法。
先说结论:MindIE 是支持单卡多实例部署的,核心入口就是 config.json 里那几个关键字段。把这个文件玩明白了,你就能在一张卡上同时跑不同模型、不同精度的多个实例,把显存和算力利用率拉上去,推理任务也能错峰调度,不用再傻等一个任务跑完才轮到下一个。
这篇文章就是围绕“用 config.json 玩转单卡多模型实例”这件事,把我的配置思路、参数取舍、踩坑实录一起写出来。适合已经在用 MindIE 跑推理、想把硬件压榨得更狠的朋友,也适合刚接触多实例部署、想搞清楚 config.json 到底怎么改的人。我会尽量把每一步为什么这么做讲清楚,而不是丢给你一份“照着抄就行”的配置模板。
1. 多实例部署的整体设计思路
1.1 为什么要在单卡上跑多个模型实例
先聊聊动机。很多人第一次听到“单卡多实例”的第一反应是:一个模型不是已经能处理并发请求了吗,为什么还要再拉一个实例?
这个问题的关键在于,模型推理的并发能力和实例数量是两码事。一个模型实例在同一时刻能处理的请求数,取决于它的 batch size、显存占用和计算流水线深度。当你的请求类型差异很大时——比如一个模型主要做短文本生成,另一个模型做长文档摘要——把它们塞进同一个实例里互相争抢资源,不如拆成两个独立实例,各自按需调度。
更现实的情况是显存余量问题。假设一张 80G 的卡跑一个 7B 的 BF16 模型,模型权重加 KV cache 占用可能只有 40G 左右,剩下 40G 全空着。如果你另外有几个轻量模型(比如 1.5B 或 3B 的 embedding 模型、分类模型),完全可以把它们一起放上来,互不干扰地服务不同业务线。
从工程部署角度讲,多实例还有一个隐性好处:隔离性。一个实例出问题(比如 OOM 崩溃)不会影响其他实例,这在生产环境里非常关键。单实例方案一旦崩了就是全军覆没,多实例至少能保证核心服务不挂。
1.2 config.json 在整个部署体系里的位置
MindIE 的部署体系里,config.json 是实例配置的核心入口。它不是唯一配置文件,但绝对是最关键的那一个——模型路径、设备分配、精度设置、KV cache 策略、并发参数,全都汇总在这里。
我的理解是,config.json 就好比每个实例的“房间钥匙”。你有多套 config.json,就能拉起多个实例;每套配置里的 device_id 和显存相关参数,决定了这个实例住在那张卡、能占多少空间、能用多少算力。
所以多实例部署的本质,就是为每个模型实例准备一份独立且互不冲突的 config.json,然后逐一启动。听起来简单,实际操作时很容易在显存分配、设备绑定、共享内存这几个地方出问题。
1.3 方案选型:多进程多实例 vs 单进程多模型
在具体配置之前,先明确一个架构选择。MindIE 下实现单卡多模型,主要有两种路线:
- 多进程多实例:每个模型实例对应一个独立进程,各自加载一份 config.json,进程之间互不感知。这是最稳妥、最隔离的方案,也是我这篇文章主要讲的。
- 单进程多模型:在一个进程内加载多个模型,通过框架内部的路由逻辑分发请求。这个方案对显存更友好(可以共用一部分基础资源),但配置复杂度高,调试难度也大,而且一旦崩溃就是全部不可用。
我个人的建议是:能上多进程就多进程。运维成本低,出问题好排查,配置文件的逻辑也清晰。单进程多模型更适合对显存极度敏感、连几十 MB 都要省的场景,普通业务没必要这么激进。
2. 核心配置细节解析——读懂 config.json 是关键
2.1 一个基础 config.json 的骨架
先看一份最基础的 config.json 长什么样。我以 MindIE 常见的参数集为例,后面会逐步解释每个字段的用途:
{ "model_dir": "/data/models/Qwen2-7B-Instruct", "device_id": [0], "tokenizer_dir": "/data/models/Qwen2-7B-Instruct", "block_nums": 100, "max_seq_len": 8192, "dtype": "bfloat16", "multi_instance_mode": false, "custom_engine": false }这里每个字段都不是随便写的:
- model_dir:模型权重所在目录。每个实例必须指向不同的模型路径(或者相同路径的不同副本),否则会撞车。
- device_id:设备编号,数组格式。单卡就是 [0],如果要用多卡就是 [0, 1]。
- tokenizer_dir:分词器路径,一般和模型目录一致,但也可以单独指定。
- block_nums:KV cache 的 block 数量,这个直接决定能跑多长的序列、多少并发。
- max_seq_len:最大序列长度,要和 block_nums 匹配,否则运行时会报错。
- dtype:权重精度,常见的有 bfloat16、float16、int8 等。
- multi_instance_mode:是否开启多实例模式。单卡多模型时,这个字段的配置方式容易踩坑,下面会细说。
这份配置是单实例的基线,后面要做的所有多实例调整,都是在这个基础上改出来的。
2.2 多实例场景下最关键的几个字段
做完基础配置后,多实例部署主要改这几个地方:
第一是 device_id。多实例部署时,每个实例仍然绑定同一张卡(比如 device_id [0]),但框架需要感知到同一张卡上有多个实例在跑。这时候 multi_instance_mode 字段就要认真对待了。
我自己实测的经验是:当 multi_instance_mode 保持默认 false 时,后面启动的同卡实例可能会覆盖前面的实例,导致前一个实例的服务端口被抢占或直接异常退出。把这项设为 true 后,每个实例会分配独立的通信资源,互相之间不再干扰。
第二个关键字段是显存控制类参数,不同版本 MindIE 叫法略有差异,但核心思路一致。你需要为每个实例设置显存上限或者预留值,避免多个实例抢显存导致 OOM。类似常见的字段包括 kv_cache_dtype、gpu_memory_fraction、free_gpu_memory_fraction 等。我目前用的版本支持用 free_gpu_memory_fraction 来预留空闲显存比例,多实例时这个值要调小,比如 0.3 左右,意思是最多占用约 70% 的显存,留一点余量给后启动的实例或系统开销。
第三是端口和通信配置。每个实例需要独立的服务端口和通信端口。如果两个实例用同一个端口,后启动的必然起不来。常见做法是给每个实例的 serve 参数指定不同的 port,比如实例 A 用 8000,实例 B 用 8001。
下面是我整理的一份多实例配置字段速查表:
| 字段 | 单实例场景 | 多实例场景 | 说明 |
|---|---|---|---|
| device_id | [0] | [0](同卡) | 多个实例绑定同一张卡时,必须配合多实例模式 |
| multi_instance_mode | false | true | 开启后允许同卡多实例共享设备资源 |
| free_gpu_memory_fraction | 0.1 | 0.3 | 多实例时建议预留更多空间,避免 OOM |
| block_nums | 按需 | 下调 | 多实例时要为每个实例分配更少的 KV cache block |
| port | 默认 | 唯一 | 每实例独立端口,不可冲突 |
| model_dir | 模型 A | 模型 A/B/C | 每个实例对应各自模型路径 |
这张表是我踩了很多次坑之后总结出来的,基本涵盖了你从单实例迁移到多实例时需要改动的全部关键点。
2.3 KV cache 分配:多实例最容易忽略的瓶颈
如果说多实例部署里哪个参数最容易被忽略,我一定投 KV cache 的分配。很多人只关注模型权重占的显存,忽略了 KV cache 是按 block 动态分配的。每个 block 的大小由 hidden_size、层数、精度共同决定,一旦你给 A 实例分多了,B 实例能用的就少了。
block_nums 的计算逻辑大概是这样的:每个 block 能缓存多少 token,取决于模型结构。比如 hidden_size 是 4096、40 层、BF16 精度,每个 block 缓存 16 个 token,那么一个 block 占用的显存大约是:
单 block 显存 = 2(K 和 V) × 40(层数) × 4096(hidden_size) × 16(token 数) × 2(BF16 字节数)
算下来大约 40 MB 左右。如果你只有 80G 显存,模型权重占了 40G,剩下 40G 给 KV cache,那么 block_nums 大概可以设到 900 左右,这是保守估算,实际还要留出激活值、通信缓冲等空间。
多实例时,这个资源池要拆成几份。两个实例的话,建议先把总可用 KV 显存算出来,再按业务比例分。比如总可用 40G,A 实例分 60%,B 实例分 40%,那 A 的 block_nums 就设 500 左右,B 设 350 左右。宁可留出一点余量,也不要卡得太死。
一个我常用的实操习惯是:先用一个极小的示例请求测试每个实例的 max_seq_len 和 block_nums 是否匹配,如果运行时报 “out of memory for block” 之类的错,就说明分配少了,逐步往上调。
3. 实操过程:从单实例到双实例的完整步骤
3.1 第一步:先跑通一个基线实例
不要一上来就试多实例。先确保单个实例能够稳定运行,再去叠加第二个。我自己的流程是先全默认参数跑一个小模型,确认 MindIE 环境本身没有硬伤,再换到目标模型调优。
假设我已经有了一份可以正常启动的单实例配置,比如 7B 模型,80G 单卡,正常生成没有报错。那么这就是我的基线。用基线去测一遍推理效果、延迟、吞吐,记录数据。后续加第二个实例后,再对比数据,你就能判断多实例是否真的带来了收益,还是反而拖慢了整体性能。
启动命令一般是:
mindie serve --config config_qwen.json这个命令会拉起一个 HTTP 服务,监听默认端口或你在配置里指定的端口。先不要急着配置多实例,跑几次请求确认没问题再说。
3.2 第二步:准备第二个实例的 config.json
有了基线实例后,准备第二个实例的配置文件。假设第二个模型是 3B 的轻量模型,结构比 7B 简单,显存占用也小很多。
我一般会在同一个目录下建一个 instances 文件夹,里面按模型或业务线命名子目录,每个子目录放自己的 config.json,这样后续好维护。比如:
instances/ ├── qwen7b/ │ └── config.json ├── qwen3b/ │ └── config.json第二个实例的 config.json 长这样:
{ "model_dir": "/data/models/Qwen2.5-3B-Instruct", "device_id": [0], "tokenizer_dir": "/data/models/Qwen2.5-3B-Instruct", "block_nums": 150, "max_seq_len": 4096, "dtype": "bfloat16", "multi_instance_mode": true, "free_gpu_memory_fraction": 0.3 }注意一点:我的 7B 主实例并没有设置 multi_instance_mode 为 true,但第二个实例设置了。这个组合看起来不对称,但实际上没问题——关键在于先启动的实例占用了端口和部分显存资源,后启动的实例会感知到已有实例的存在,从而主动调整自己的通信地址。
当然,更规范的做法是两个实例的配置里都开启 multi_instance_mode,这样框架能更早地进入多实例协作状态。我之所以先只改第二个,是想验证框架的兼容性,属于排查问题时的“最小改动”思路,生产环境建议两边都开。
3.3 第三步:确认端口与通信配置不冲突
有了两份 config.json,接下来就是启动顺序和端口分配的问题。
启动第一个实例(7B 模型):
mindie serve --config instances/qwen7b/config.json --port 8000 --master-port 8001然后启动第二个实例(3B 模型):
mindie serve --config instances/qwen3b/config.json --port 8002 --master-port 8003注意我用了两组端口,master-port 是 MindIE 实例间通信用的端口,也是一个实例一个,不能共用。如果你不清楚 master-port 具体绑定的是什么服务,有一个简单的排查方法:启动后看日志,日志会显示当前实例监听的具体端口,如果提示端口被占用,就换一个。
启动完成后,检查两个服务是否都注册成功:
curl http://localhost:8000/v1/models curl http://localhost:8002/v1/models如果两个请求都正常返回模型列表,说明两个实例已经同时跑起来了。这时候再用一张卡上同一时刻有两个模型在服务。
3.4 第四步:验证多实例的资源分配是否合理
实例能起来只是第一步,关键是资源分配是否合理。我一般会做三个维度的检查:
- 显存占用情况。用 nvidia-smi 观察两张卡的显存占用。如果第二个实例启动后,第一个实例的显存占用被大幅挤压甚至出现显存溢出,说明 free_gpu_memory_fraction 或 block_nums 需要调整。
- 请求响应时间。分别向两个实例发一组标准请求,对比它们的首 token 延迟和平均生成速度。如果某个实例的延迟明显劣化,可能是它的 KV cache 分配不足,或者和另一个实例抢算力。
- 并发压力。同时向两个实例发并发请求,看系统是否会报错或某个实例无响应。这能检验多实例是否真的做到了隔离。
我自己的一个经验是,如果两个模型的计算量差异很大(7B 和 3B),资源分配要偏向大模型,因为它对 KV cache 的敏感度更高。小模型即使 block_nums 少一点,影响也不至于太大。所以实际配置里,7B 的 block_nums 我给 400,3B 给 150,整体跑下来还算均衡。
3.5 不同场景下的配置参数参考
这里给一份基于实际测试的参考配置,适合单卡 80G、同时跑一个 7B 和一个 3B 模型的场景:
| 参数 | 实例 A(7B) | 实例 B(3B) | 备注 |
|---|---|---|---|
| model_dir | /models/Qwen2-7B | /models/Qwen2-3B | 各自独立 |
| device_id | [0] | [0] | 同卡 |
| multi_instance_mode | true | true | 两边都开 |
| block_nums | 400 | 150 | 按业务量比例分配 |
| max_seq_len | 8192 | 4096 | 对齐模型能力 |
| dtype | bfloat16 | bfloat16 | 尽量一致 |
| port | 8000 | 8002 | 避免冲突 |
| master-port | 8001 | 8003 | 避免冲突 |
如果你的卡是 48G 或者 96G,按比例缩放即可。核心思想是权重占一部分、KV cache 按需分配、留 20% 到 30% 余量。
4. 常见问题与排查技巧实录
4.1 启动时报端口冲突
这个问题几乎人人都遇到。现象是第二个实例启动时提示端口被占用,或者第一个实例无法启动。原因很简单,两个实例用了相同的端口。
排查方法其实很直接:启动前先看端口监听情况,比如用 netstat 或 lsof 查一下 8000、8001、8002、8003 这些端口是否被占用。如果端口被其他进程占用,换端口是最省事的。
但有一种隐藏情况是,MindIE 内部除了 HTTP 服务端口,还有一些内部通信端口,你可能在命令行只指定了 port,没指定 master-port,导致默认值撞了。两个实例同时用默认 master-port 时,就可能出现“HTTP 端口不同但集群通信端口相同”的诡异问题。解决方法是显式指定每实例独立的端口和 master-port。
4.2 第二实例启动后第一个实例显存溢出或崩溃
这属于多实例部署里最头疼的问题。根因通常是第一个实例没有预留足够的显存空间。它的 KV cache 是按单实例场景配置的,占满了几乎所有空闲显存,第二个实例加载模型时发现显存不够,直接把第一个实例的显存挤爆。
我遇到这种情况时的处理顺序:
- 先停掉所有实例。
- 重新计算两个模型总共需要的显存,包括权重和 KV cache。
- 给第一个实例调小 block_nums,并显式设置 free_gpu_memory_fraction 为 0.3 左右。
- 修改后再启动第一个实例,确认它启动后的显存占用不超过预期的 70%——这一步可以通过 nvidia-smi 实时监控。
- 接着启动第二个实例,观察两个实例的显存总和是否在卡容量内。
这里有个重要的细节:不要只靠 nvidia-smi 的当前显存占用来估算,因为推理过程中的 KV cache 是动态分配的。你看到的显存占用可能比实际有较大的富余,一旦并发请求来了,KV cache 会暴涨。所以 block_nums 一定要按最大可能并发和最长序列来算,而不是按当前测试请求的量来算。
4.3 模型推理速度明显下降
多实例跑起来后,推理速度比单实例慢是正常的,毕竟算力和显存带宽是共享的。但如果慢得离谱,比如首 token 延迟从 50ms 变成 500ms,那就不是正常现象了。
我复盘这类问题后发现,最常见的原因是两个实例频繁争抢显存带宽,尤其是当两个模型的序列长度都很长、并发很高时,HBM 带宽会成为瓶颈。这个层面靠 config.json 很难完全解决,只能从业务侧做削峰填谷,比如错峰调度。
另一个原因是 KV cache 太小,触发了频繁的缓存淘汰或重新计算。当你看到日志里出现大量 cache miss 或重复 prefill 的记录时,基本可以判断是 block_nums 给少了。这时候适当增大 block_nums,或者降低 max_seq_len,让 KV cache 能覆盖更多的历史 token。
还有一个容易忽略的坑:自定义引擎与 multi_instance_mode 的兼容性。有些模型版本需要打开 custom_engine 使用特定优化,但开启后可能与多实例模式冲突,导致推理走回低效路径。遇到性能问题时,先试试关掉 custom_engine 或改回默认引擎,看性能是否恢复。
4.4 显存碎片化导致的多实例无法同时启动
这个问题比较隐蔽,出在多次启动、停止实例之后。第一个实例加载时占了一块显存,释放后没有完全还给驱动,第二个实例再启动时,看起来总显存足够,但实际可用的连续显存块不够,导致分配失败。
处理办法有几个:
- 重启容器或宿主机上的 GE(Graph Engine)管理进程,让显存重新整合。
- 在启动脚本里显式加上显存释放与同步等待逻辑,确保前一个实例退出后显存真正归还。
- 把两个实例的启动顺序固定下来,先启动显存需求大的实例,再启动小的,减少碎片化的概率。
这个问题的核心是环境层面的,不是 config.json 能直接解决的,但很多人在排查时会误以为是自己配置参数写错了,浪费不少时间。
4.5 常见报错信息速查表
结合这段时间的实操,我整理了一张个人向的报错速查表,贴出来供参考:
| 报错关键词 | 可能原因 | 解决方向 |
|---|---|---|
| address already in use | 端口冲突 | 检查 port 和 master-port,更换独立端口 |
| device memory insufficient | 显存不足 | 调小 block_nums 或调整 free_gpu_memory_fraction |
| block num exceeds limit | KV cache 超限 | 降低 block_nums 或 max_seq_len |
| multi-instance not enabled | 多实例模式未开 | 设置 multi_instance_mode 为 true |
| engine initialization failed | 自定义引擎冲突 | 关闭 custom_engine,或检查引擎版本 |
| cache miss rate too high | KV cache 过小 | 增大 block_nums,或降低并发 |
这张表不完全全面,但覆盖了多实例部署里 80% 以上的问题。如果你遇到的不在这张表里,优先看完整日志,重点关注前 100 行的报错信息,那里面通常会直接告诉你问题出在哪个模块。
4.6 一个值得养成的启动前检查习惯
最后分享一个我自己的固定动作。每次多实例部署前,我会先画一张简单的资源清单,类似下面这样:
- 模型 A:7B,BF16,权重约 15G,目标 KV cache 约 30G,一共约 45G。
- 模型 B:3B,BF16,权重约 7G,目标 KV cache 约 15G,一共约 22G。
- 合计 67G,80G 卡剩余 13G 留给系统开销和激活值。
然后我按照这个清单去配置每个实例的 block_nums 和显存预留参数。这样做的好处是,每一步改动都有明确的目标,而不是靠猜。多实例部署的难点不在于某个参数有多难理解,而在于多个参数之间的联动效应。先把资源账算清楚,后面排查问题会轻松很多。
我实际跑下来的体会是,多实例部署这件事,40% 的功夫在 config.json 的参数调优上,60% 的功夫在启动顺序、资源规划、日志监控这些“软技能”上。把配置文件改得再漂亮,如果启动时机不对、资源预算没算清,一样会翻车。
另外想提醒一点:如果你部署的是需要加载自定义引擎的模型,务必先验证它在多实例模式下是否正常工作。我遇到过好几次模型单实例跑得很溜,开了多实例后反而更慢的情况,最后定位到就是自定义引擎和多实例模式打架。遇到这种问题,不要恋战,先切回默认引擎验证,再考虑优化方案。
目前这个多实例方案我已经用在日常实验环境里跑了一周多,两张卡分别承担不同的模型组合,整体稳定性和单实例相差不大。后续我打算把三个小模型合在一张卡上试试,进一步压榨显存和算力。如果你也在折腾 MindIE 多实例,卡在某一步过不去,不妨对照着 config.json 逐项查一遍,大概率能解决问题。