本地 fastchat 大语言模型这套东西,我前后帮人搭过七八次,每次遇到的问题都不重样。很多人以为难点在pip install,装完就万事大吉,结果一道CUDA out of memory就把人钉在原地。这套框架的本质是把“加载模型、切分显存、调度多实例、暴露接口”这几件琐事封装成几个独立进程,你只要按顺序把它们串起来就能用;但每一步都有隐含假设,踩空了就是几小时的排查。这篇内容适合三类人:想在自己机器上跑通第一个对话模型的初学者、要给团队搭一套内部可共享推理服务的开发者、以及已经把模型跑起来但卡在并发和显存上的进阶用户。下面我按实际动手顺序,从环境准备一路讲到服务化部署和报错排查,把中间那些文档里不会写的细节全部摊开。
1. 先搞清楚 FastChat 到底在解决什么问题
1.1 它不是一个模型,而是一套服务框架
这是新人最容易混淆的一点。FastChat 本身不包含任何权重,它是一个开源项目,做的事情是“把别人的模型装进一个统一的服务壳子里”。你下载的 Vicuna、LLaMA 系列、以及各种基于 LLaMA 微调出来的对话模型,才是真正干活的脑子;FastChat 负责的是加载、切分、调度和对外提供接口。
理解这层关系很关键,因为它决定了你排查问题的方向。跑不起来的时候,先分清是“模型加载环节炸了”还是“服务调度环节炸了”,两者的日志位置、报错形态、解决路径完全不同。模型加载阶段的报错通常和 transformers 版本、权重文件完整性、tokenizer 配置有关;服务调度阶段的报错则多半是端口占用、controller 地址写错、worker 注册失败。我见过有同行花了半天时间调 gradio 的界面参数,最后发现根本没起 worker,WebUI 里空空如也——方向错了,力气全白费。
它内部按职责拆成了几个可独立启动的模块:fastchat.serve.cli是单机命令行入口,fastchat.serve.controller是调度中心,fastchat.serve.model_worker是模型进程,fastchat.serve.gradio_web_server是网页界面,fastchat.serve.openai_api_server提供兼容 OpenAI 协议的 HTTP 接口。这五块可以任意组合,用一块还是用五块,取决于你的场景。
1.2 三类典型场景,选型判断其实很快
我把常见需求归成三类,对应的最小方案差别很大,先想清楚自己属于哪一类,能省掉一半无用功。
第一类是“我自己试试这个模型效果怎么样”。这种场景完全不需要 controller 和 worker,一条python -m fastchat.serve.cli命令就够,模型直接加载到当前进程,终端里回车对话。优点是零配置,缺点是关掉终端就没了,也没法给别人用。
第二类是“团队几个人共用一个模型服务”。这时就要上 controller + worker + WebUI 的组合。controller 维护一份已注册 worker 的清单,WebUI 把用户请求转发给 controller,controller 再挑一个空闲的 worker 去执行。好处是一个模型进程能服务多个浏览器窗口,并且后续可以横向加 worker 做负载分担。
第三类是“我要把它接进自己的业务系统”。这时候 WebUI 就不需要了,直接用openai_api_server暴露一个/v1/chat/completions接口,你现有的、按 OpenAI 协议写好的代码几乎改个 base_url 就能跑。这个方案的生产价值最高,也是大多数企业内训、客服辅助、文档问答类项目的实际形态。
提示:一开始就按最重的方案搭,是新手常见的过度设计。先用 cli 把模型跑通,确认权重没问题、显存够用,再往上叠服务层,出错时排查范围会小得多。
1.3 硬件门槛与显存估算,别凭感觉买卡
经常有人问“我这机器能不能跑”,这个问题的答案取决于模型规模和精度,可以粗略算出来。
fp16 精度下,模型权重大约占用“参数量 × 2 字节”。7B 参数约 14GB,13B 约 26GB,30B 约 60GB。但这只是权重,实际运行时还要加上 KV cache(缓存注意力计算的中间结果)和框架自身的开销,通常要再预留 20% 到 40%。所以 7B 在 fp16 下,24GB 显存的卡是比较舒服的;13B 在 fp16 下 24GB 会非常紧张,基本要靠量化或者双卡切分。
| 模型规模 | fp16 权重占用 | 建议显存(含缓存) | 24GB 单卡可行性 |
|---|---|---|---|
| 7B | 约 14GB | 18GB 至 20GB | 可以,较宽裕 |
| 13B | 约 26GB | 32GB 以上 | 不行,需 8bit 量化或双卡 |
| 30B | 约 60GB | 70GB 以上 | 不行,需多卡加量化 |
| 7B(8bit 量化) | 约 7GB | 10GB 至 12GB | 可以,很宽裕 |
上表里的“建议显存”是给对话场景估的,如果你的业务要处理长文档,单次输入几千个 token,KV cache 会明显膨胀。粗略估算方式是:每层每 token 的 KV 缓存约占2 × 隐藏维度 × 2 字节,7B 模型 32 层、隐藏维度 4096,一个 token 大约 0.5MB,4096 个 token 就是 2GB 左右,再乘以并发数。所以长上下文加高并发是显存杀手,这一点在规划阶段就要想明白。
至于纯 CPU 推理,我的建议是能上显卡就上显卡。7B 模型在普通桌面 CPU 上生成速度大概每秒一两个 token,读起来像卡带,体验上没有任何实用价值,只适合验证权重是否能加载。
2. 环境准备,版本对齐比装什么包更重要
2.1 显卡驱动、CUDA 与 PyTorch 的三方对齐
这一环节是问题高发区,绝大多数“跑起来就报错”的根源都在这里。三者是有依赖链的:显卡驱动决定你最高能用哪个版本的 CUDA Runtime,CUDA Runtime 决定你能装哪个版本的 PyTorch,而 FastChat 依赖的 transformers、accelerate 又对 PyTorch 有版本要求。
实操上的判断顺序是:先nvidia-smi看右上角显示的 CUDA Version,那是驱动支持的上限,不是你必须装的版本。然后去 PyTorch 官方安装页选对应版本的安装命令。我通常的做法是不追求最新,选一个稳定组合:驱动版本较新时用 CUDA 11.8 或 12.1 对应的 PyTorch 2.x,兼容性最好。
nvidia-smi # 关注右上角 Driver Version 与 CUDA Version python -c "import torch; print(torch.__version__, torch.version.cuda, torch.cuda.is_available())"最后那行检查必须输出True,否则后面所有 GPU 相关的参数都是白搭。如果输出 False,八成是装成了 CPU 版 PyTorch,或者驱动版本太低,重新按官方命令装一次就行。
注意:不要用系统自带的 Python 装这些包。系统 Python 被各种系统工具依赖,一旦你把 torch 装进去搞出冲突,修复成本很高。老老实实用虚拟环境隔离。
2.2 用 conda 建一个干净的环境
我个人偏好 conda 而不是 venv,原因是 conda 能顺带管理一些非 Python 的二进制依赖,出问题时更容易回滚。Python 版本选 3.10 比较稳妥,3.11 和 3.12 在部分依赖上还会遇到兼容性问题。
conda create -n fastchat python=3.10 -y conda activate fastchat # 先按官方命令装好对应 CUDA 版本的 torch pip install torch torchvision torchaudio --index-url <官方对应的索引地址>装完 torch 之后确认一次torch.cuda.is_available()为 True,再装 FastChat 本体。
pip install "fschat[model_worker,webui]"这里有个细节值得展开:fschat后面那个方括号是 extras 语法,表示额外安装模型 worker 和 webui 需要的依赖(比如 gradio、accelerate、peft、bitsandbytes 等)。如果你只装了pip install fschat,跑 cli 可能没问题,但一启 model_worker 就报缺模块。我建议一次性装全,除非你明确知道自己只需要命令行推理。
装完之后检查几个关键依赖的版本:
pip list | grep -E "transformers|accelerate|bitsandbytes|gradio|protobuf|torch"经验值上,transformers 不要追求最新大版本,太新的版本有时会调整模型加载接口的行为,导致 FastChat 里写好的适配逻辑对不上。protobuf 也是个高频冲突源,gradio 对它的版本比较敏感,遇到Descriptors cannot be created directly这类报错,基本可以锁定是 protobuf 版本过高。这种情况不需要改代码,降版本就行。
2.3 模型权重的获取与目录规划
权重文件动辄十几 GB,存放位置要提前想好。我建议单独挂一块盘或者用一个固定目录,比如/data/models/,每个模型一个子目录,目录名带上模型名和版本号,比如vicuna-7b-v1.5。这样后面启动命令里的--model-path一眼就能看懂,多模型共存时也不会乱。
一个标准的模型目录长这样:
/data/models/vicuna-7b-v1.5/ ├── config.json ├── generation_config.json ├── tokenizer.model ├── tokenizer_config.json └── model-0000X-of-0000Y.safetensors判断权重是否完整,最简单的办法是看文件大小是否和发布页标注的一致,以及config.json能不能正常解析。有些下载工具中途断了会留下半截文件,加载时报的错往往是Unable to load weights或者解析 safetensors 头部失败,这时候不要怀疑框架,先核对文件完整性。
如果你的模型是从别的格式转换过来的,还要注意 tokenizer 文件是否配套。LLaMA 系模型用的是 sentencepiece 的tokenizer.model,如果这个文件和权重不是同一个来源,跑起来会出现输出乱码或者无限重复的情况——这是很典型的“模型能加载但效果不对劲”。
3. 命令行推理,先把最小链路跑通
3.1 一条命令验证模型能不能用
这一步的价值在于做减法。所有服务化的复杂度都建立在一个前提上:模型本身能正常加载、正常生成。先用最简命令验证这个前提,能帮你把绝大部分问题挡在服务层之外。
python -m fastchat.serve.cli --model-path /data/models/vicuna-7b-v1.5不加任何额外参数时,它会尝试用默认设备加载模型,进入一个交互式对话界面。这一步需要注意几个参数的实际含义,很多人是照着博客抄命令,出问题就不知道从哪改。
--device指定运行设备,可选cuda、cpu、mps。不写时框架会自动探测,但在多卡机器上自动探测有时会挑错卡,我一般显式写上。--num-gpus指定使用的显卡数量,这个参数的影响后面会详细讲,它不是简单的“多卡加速”。--max-gpu-memory用来限制单卡最大显存占用,值写成20GiB这种带单位的字符串。--load-8bit开启 8 位量化加载,用 bitsandbytes 实现,是显存不够时最省事的方案。
首次加载会比较慢,因为要把十几 GB 的权重从磁盘读进显存,机械盘上可能要几分钟。这也是为什么我强调把模型放在 SSD 上,反复调试时这个等待时间的差异非常影响效率。
3.2 多卡切分的真相,别指望线性加速
很多同行看到--num-gpus 2就默认速度会翻倍,实际跑下来发现并没有,然后开始怀疑配置写错了。这里需要说清楚原理。
FastChat 加载模型时借助的是 accelerate 的device_map机制,做的是按层切分,也就是把模型的不同层分配到不同显卡上。这种分配方式本质上是流水线式的:数据先经过第一张卡上的若干层,再把中间结果传给第二张卡继续算,同一时刻基本上只有一张卡在真正做计算,其余的卡在等或者只占用显存。
python -m fastchat.serve.cli --model-path /data/models/vicuna-7b-v1.5 --num-gpus 2所以这个参数的真实作用是“把显存池子拼起来,让更大的模型能装下”,而不是“提升吞吐”。想要真正的计算并行和吞吐提升,得换后端,比如用 vLLM 作为 worker 的实现(FastChat 提供了fastchat.serve.vllm_worker),它用的是 PagedAttention 那套显存管理,批处理效率高一个量级。选型上我的判断标准是:单用户交互式使用,多卡切分够用;多用户并发访问,直接上 vLLM worker,别在原生 worker 上硬扛。
3.3 显存不够时的三条路,按代价排序
显存不够是最常见的拦路虎,解决路径有三条,代价依次递增,我建议按这个顺序试。
第一条是限制单卡显存占用,让框架自己想办法腾挪。加--max-gpu-memory 20GiB,框架会尽量把部分层放到 CPU 内存里,算到那里再搬过去。代价是速度下降,但至少能跑起来。这条路的适用场景是“显存只差一点点”。
第二条是 8 位量化加载:
python -m fastchat.serve.cli --model-path /data/models/vicuna-7b-v1.5 --load-8bit权重量化到 8 位后显存占用大概减半,7B 模型从 14GB 降到 7GB 左右。质量损失在对话场景下几乎感知不到,我认为是性价比最高的方案。前提是 bitsandbytes 装好了,并且和 CUDA 版本匹配——这个包对版本很挑,装错了会在加载时抛CUDA error或者直接 import 失败。
第三条是 CPU offload,把相当一部分层放到内存里。7B 模型配 16GB 内存勉强能跑,但速度会掉到难以忍受的程度,只适合做功能验证。再往下就是纯 CPU 推理了,前面说过,基本没有实用价值。
| 方案 | 显存节省 | 速度影响 | 适用判断 |
|---|---|---|---|
| 限制 max-gpu-memory | 小幅 | 中等下降 | 显存差一点点时首选 |
| 8bit 量化 | 约减半 | 轻微下降 | 最推荐的通用方案 |
| CPU offload | 大幅 | 严重下降 | 仅用于功能验证 |
| 换更小的模型 | 大幅 | 取决于模型 | 效果可接受时的最优解 |
最后补充一个容易被忽略的选项:换模型。很多人死磕 13B,其实一个训练得更充分的 7B 模型在实际任务上未必差,显存压力却小一半。这个取舍要结合实际效果评测来定,不要只看参数量。
4. 服务化部署,三个进程怎么串起来
4.1 每个进程各管一摊事
进入服务化阶段,先建立一张清晰的心智地图,比抄命令重要得多。
controller 是整个架构的注册中心和调度器。它自己不加载模型,只维护一份“当前有哪些 worker 在线、各自能处理什么模型、现在忙不忙”的清单。它监听一个端口,默认 21001。
model_worker 是真正干活的进程,加载模型权重,向 controller 注册自己,然后监听另一个端口等待任务。一个 controller 下可以挂多个 worker,挂的可以是同一个模型,也可以是不同模型。
gradio_web_server 是面向人的界面层,它连到 controller 拿模型列表,用户选一个模型发消息,它负责转发并流式接收结果展示出来。
openai_api_server 是面向程序的接口层,同样连 controller,对外暴露一套按照 OpenAI 协议组织的 REST 接口。
理解这个结构之后,很多问题就自解释了。比如“网页上模型下拉框是空的”,说明 WebUI 连上了 controller,但 controller 上没有任何 worker 注册,问题在 worker 那边,不在 WebUI。再比如“请求发出去没反应”,可能是 worker 正在处理别的请求,排队了。
4.2 启动顺序与端口规划
启动有严格顺序:controller 先起,worker 后起,界面层最后起。顺序错了 worker 会因为连不上 controller 而退出。
# 终端一:调度中心 python -m fastchat.serve.controller --host 0.0.0.0 --port 21001 # 终端二:模型进程 python -m fastchat.serve.model_worker \ --model-path /data/models/vicuna-7b-v1.5 \ --model-name vicuna-7b \ --controller http://localhost:21001 \ --worker http://localhost:21002 \ --host 0.0.0.0 \ --port 21002--model-name建议显式指定,起一个短小好记的名字。它会作为模型标识出现在 WebUI 下拉框和接口的model字段里。不指定时框架会用模型路径推断,路径长的时候名字会很难看,接口调用也麻烦。
--worker参数容易被漏掉。这个参数告诉 controller“我这个 worker 可以通过这个地址被访问到”。如果不写,框架会用默认值,在单机部署时通常也能跑,但在容器或跨机部署时会出问题——controller 记下的地址从它那个网络视角访问不到。
# 终端三:网页界面 python -m fastchat.serve.gradio_web_server \ --controller http://localhost:21001 \ --host 0.0.0.0 \ --port 7860--model-list-mode reload这个参数值得单独说。默认情况下 WebUI 在启动时拉一次模型列表就固定了,之后你新起 worker,页面上看不到。加上这个参数后,页面每次刷新都会重新拉列表,调试阶段非常实用,能省掉反复重启界面的时间。
端口规划上,我习惯固定用 21001 / 21002 起步,每多一个 worker 往后顺延 21003、21004,WebUI 用 7860,接口用 8000。这些数字本身不重要,重要的是写进一份启动脚本里固定下来,别每次靠记忆。
4.3 OpenAI 兼容接口,接入业务系统的关键
这是我认为 FastChat 最有价值的部分。它把本地模型包装成了一套标准的对话接口,你原本对接云端服务的代码,改成指向http://localhost:8000/v1,模型名换成你注册时起的名字,基本就能直接跑。
python -m fastchat.serve.openai_api_server \ --controller-address http://localhost:21001 \ --host 0.0.0.0 \ --port 8000验证接口是否通,用 curl 最快:
curl http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "vicuna-7b", "messages": [{"role": "user", "content": "用一句话解释什么是向量数据库"}], "temperature": 0.7, "max_tokens": 256, "stream": false }'在业务代码里调用时,用官方 SDK 只需改两处:
from openai import OpenAI client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed") resp = client.chat.completions.create( model="vicuna-7b", messages=[{"role": "user", "content": "帮我总结这段会议记录"}], temperature=0.3, ) print(resp.choices[0].message.content)注意api_key那一栏。本地服务默认不校验密钥,随便填个字符串占位就行,但很多 SDK 会检查这个字段是否存在,留空可能直接报参数错误。这个坑我踩过,当时的报错信息非常隐晦,排查了半天才定位到是客户端参数校验,跟服务端一点关系没有。
流式输出的支持也要提前确认。stream=true时返回的是 SSE 格式的增量数据,如果你的业务要做到打字机效果,就得按增量方式解析。有些客户端库对这个格式的处理有差异,接之前先用 curl 确认服务端输出格式正常,再往业务里接。
4.4 对外提供服务时的并发与鉴权
把服务从“自己用”变成“给别人用”,是质变的开始,有两个问题必须提前处理。
并发方面,FastChat 的原生 worker 默认是单请求串行的,一个请求进来,其他的排队等着。--limit-worker-concurrency参数可以调整这个上限,但调大的前提是显存撑得住。并发数每增加一个,KV cache 就多占一份显存,7B 模型在 24GB 卡上,并发开到 4 到 8 大概是合理区间,再往上就要看上下文长度了。真正的做法是通过“多 worker 实例”来扩展:起三个 model_worker,分别监听 21002、21003、21004,都注册到同一个 controller,controller 会把请求分给空闲的那个。
# worker 二 python -m fastchat.serve.model_worker \ --model-path /data/models/vicuna-7b-v1.5 \ --model-name vicuna-7b \ --controller http://localhost:21001 \ --worker http://localhost:21002 \ --host 0.0.0.0 --port 21002 --limit-worker-concurrency 4注意这种多实例共享 controller 的方式,本质是请求级的分发,不是显存共享。每个 worker 都完整加载一份模型,显存占用是叠加的。所以能否这么做,取决于是“一块大卡切着用”还是“多块卡各管一个”。前者靠--num-gpus切分,后者才是多 worker。
鉴权方面,FastChat 的接口层默认没有访问控制。如果服务只在内网、只给自己团队用,问题不大;一旦要暴露到更广的范围,就必须在前面加一层网关做密钥校验和限流。我通常的做法是把 openai_api_server 只绑定在内网地址上,前面放一个轻量的反向代理做鉴权和日志,这样既不用改框架代码,也便于统一管理。
注意:不要为了省事把接口直接绑定到
0.0.0.0就丢在公网上。本地模型服务没有任何访问控制能力,暴露出去意味着任何人只要知道地址就能消耗你的算力。
5. 报错排查,几种高频问题的定位方法
5.1 显存相关报错怎么快速定位
torch.cuda.OutOfMemoryError是最常见的报错,但它有几种不同形态,对应的原因不一样,要看报错里那句Tried to allocate xxx MiB的数值。
如果这个数值很大(好几个 GB),说明模型权重装载阶段就爆了,属于“显存根本不够装下模型”,直接上量化或者多卡切分。如果数值不大(几百 MB),说明权重装得下,是运行过程中 KV cache 或者中间激活值撑爆了,这时候该做的是缩短max_new_tokens、减小批大小、或者降低并发。这个区分很实用,能避免你在一开始就把方案往最重的方向改。
还有一个隐蔽的变体是“跑着跑着突然爆显存”。这通常是因为输入长度超出了预期,或者模型被不同长度的请求反复冲击导致缓存碎片化。排查时可以固定一个短输入先测稳定性,逐步加长,找到临界点。
5.2 版本冲突类报错的特征识别
版本问题有个很明显的特征:报错信息里出现你不认识的模块名,或者提示某个函数签名不对。这类问题我的处理原则是“优先降版本,不要试图改源码”。
Descriptors cannot be created directly基本等于 protobuf 版本过高,降下来就好。cannot import name 'xxx' from 'transformers'通常是 transformers 版本和 FastChat 要求的对不上,去看 FastChat 项目里声明的依赖范围,装个范围内的版本。undefined symbol这类报错指向的是编译层面的不匹配,多半是 bitsandbytes 或者 torch 和 CUDA 版本没对齐,重装对应版本。
我自己维护了一份“已知可用组合”的记录,每次成功搭好一套环境就把各包版本记下来,下次直接照抄。这看起来笨,但比每次撞运气强得多。环境这个东西,能复现比能更新重要。
5.3 服务层连不通的排查顺序
服务层的问题,排查要按链路顺序走,从下往上。
第一步看 worker 进程是否还活着。它如果启动后很快自己退出,去看它的日志末尾,通常是加载模型失败或者连不上 controller。第二步看 controller 的日志里有没有register记录,没有就说明 worker 根本没连上,检查--controller地址和网络连通性。第三步用curl直接打 controller 的模型列表接口,确认它能返回模型名。第四步才是看 WebUI 或接口层。
curl http://localhost:21001/list_models这个接口能返回模型名,说明前面三层都通了,问题在界面层。返回空,说明 worker 注册失败。这个二分法能帮你把排查范围快速砍半。
还有一个常见现象是“模型列表有,但发消息没反应”。这通常是 worker 正在处理别的长请求,或者卡在生成里出不来。看看 GPU 占用是否还在动,如果占用一直满着但没有输出,可能是生成了重复内容陷入死循环,把temperature和repetition_penalty调一下试试。
| 现象 | 最可能的原因 | 处理方向 |
|---|---|---|
| 启动即报 CUDA OOM,分配量很大 | 显存装不下模型 | 量化、多卡、换小模型 |
| 运行中报 OOM,分配量很小 | 缓存或并发撑爆 | 缩短生成长度、降并发 |
| 提示模块缺失或函数签名错 | 包版本冲突 | 按项目声明降版本 |
| WebUI 模型下拉框为空 | worker 未注册成功 | 查 controller 地址与 worker 日志 |
| 模型加载成功但输出乱码 | tokenizer 与权重不配套 | 核对 tokenizer 文件来源 |
| 接口调用返回 404 | 模型名写错或路径不对 | 核对--model-name与接口字段 |
5.4 性能调优的两个方向
调优之前先明确你要优化的是“首字延迟”还是“整体吞吐”,这两个目标的手段是冲突的。
追求首字延迟,也就是用户按下回车到看到第一个字的时间,方向是减少预填充阶段的计算量。做法包括缩短系统提示词、降低上下文长度、避免过长的历史对话累积。多人共用一个模型时,这种优化效果明显。
追求整体吞吐,也就是单位时间能处理多少请求,方向是提高批处理效率。原生 worker 在这方面的能力有限,因为它逐条处理请求。真要提升吞吐,换 vLLM 后端是更彻底的办法,它把不同请求的计算拼成一批一起算,并且用分页机制管理缓存,显存利用率高很多。代价是部署复杂度上升,需要额外装 vLLM 并且确认它支持你的模型结构。
实际参数层面,几个值得调的:temperature影响生成的随机性,业务场景通常设低一点更稳定;max_tokens直接决定最长生成时间,设一个合理上限能避免单请求占住 worker 太久;repetition_penalty高于 1 能抑制重复,但设太高会让语言变得生硬。这些参数没有通用最优值,得拿你自己的实际输入去试。
6. 我在实际配置中踩过的坑
6.1 模型格式与权重文件的那些细节
safetensors 和 bin 两种格式都支持,但前者加载更快也更安全,有条件优先选。有些发布页只提供其中一种,遇到不支持的格式不要自己瞎折腾转换,先确认框架版本是否支持,很多时候升级一下就解决了。
另一个坑是分片权重。大模型会被切成多个文件,比如model-00001-of-00004.safetensors。这些文件的命名和索引文件必须完整,缺一个就会报“找不到某个分片”。下载工具中断后再续传,有时会出现索引文件还是旧的、分片数量对不上,这时候删掉索引文件重新下载最省事。
还有 tokenizer 的配套问题,前面提过一次,这里再强调:如果你用的是别人转换过的模型,务必确认 tokenizer 文件和权重是同一套。混用不同来源的 tokenizer,模型能加载、能生成,但输出的文字质量会很差,甚至出现大量特殊符号,这种“看起来能用但效果不对”的问题最难查,因为没有任何报错。
6.2 多卡配置里那些反直觉的地方
多卡时最容易踩的坑是“指定了数量和没指定效果一样”。检查命令里有没有真的带上--num-gpus,以及CUDA_VISIBLE_DEVICES环境变量有没有把卡屏蔽掉。后者很隐蔽:如果环境变量只暴露了一张卡,你写--num-gpus 2也没用,框架只看得到一张。
另一个反直觉的点是显存分布不均匀。按层切分时,第一张卡往往还要承担输入嵌入,最后一张卡要承担输出层,两边会比中间卡多占一些。所以如果看到某张卡爆了而其他卡还空着,不是配置有问题,是切分方式导致的固有现象。缓解办法是给第一张卡分配少一点的层,或者用--max-gpu-memory给各卡设置不同的上限。
还有就是多卡启动比单卡慢不少。因为要做层分配和设备间的数据搬运,首次加载时间可能翻倍。调试阶段如果频繁重启,建议先用单卡加少量层的方式验证逻辑,最后再上多卡。
6.3 让它稳定长跑的几个习惯
调试通了不等于能长期跑,我总结了几条实践习惯。
进程管理上,别用前台终端跑。用 tmux 或者 systemd 托起来,断线不影响服务,崩溃了能自动重启。启动脚本里把每个进程的日志重定向到独立文件,出问题时不用翻终端历史。
显存监控上,定期看一眼nvidia-smi,关注的是占用趋势而不是瞬时值。如果发现占用缓慢爬升不下降,那可能是缓存没释放,重启进程能缓解,但根本解法是排查是不是有异常长的请求一直没结束。
上下文长度上,在配置里明确设一个上限,不要用模型默认的最大值。默认值往往很大,意味着一个恶意或误操作的长输入就能把显存吃满。设一个业务上够用的值,比如 2048 或 4096,稳定性会好很多。
最后一条是我自己的体会:这套东西的价值不在于“跑起来”,而在于“跑稳”。跑起来可能只需要一个下午,跑稳需要你把上面这些边角情况都过一遍。我现在的习惯是每次搭完一套,都写一份包含版本号、启动命令、端口分配的记录,下次再搭或者换机器时直接照着来,比重新摸索省下的时间远多于写这份记录的时间。