机器里插了两张 RTX 3090(合计 48 GB 显存),加载一个需要约 42 GB 的 70B Q4_K_M 模型。推理时 nvidia-smi 显示 GPU 0 占用 100%、约 24 GB 显存,而 GPU 1 是 0% 和 0 MB。没有任何报错,模型就是只跑在一张卡上。
最快的修法: 推理引擎几乎不会自己决定去拆分模型,你得明确告诉它用哪种分割模式。
- llama.cpp / llama-server: 加
-sm layer(默认,把各层分散到多卡)或-sm row(张量并行),再配--tensor-split 1,1和-ngl 99。 - vLLM: 加
--tensor-parallel-size 2(该值必须能整除模型的注意力头数)。 - Ollama: 先确认两张卡都可见(
CUDA_VISIBLE_DEVICES=0,1);当前版本的 Ollama 会对装不下单卡的模型自动分布。想强制分布用OLLAMA_SCHED_SPREAD=1。
如果模型本来就能装进单卡,那只跑一张卡是正确的——在没有 NVLink 的消费级硬件上拆分通常会更慢而不是更快。对照下面的分类表找到你属于哪种情况。
先判断你属于哪一类
| 现象 | 最可能的原因 | 跳转 |
|---|---|---|
| 只有 GPU 0 占显存,模型很小(小于单卡显存) | 不需要分割,单卡是对的 | 原因 1 |
nvidia-smi -L 列出的卡比实际少 | CUDA_VISIBLE_DEVICES / Docker 把卡藏了 | 原因 2 |
| 引擎能启动、模型很大,但仍只用一张卡 | 没设分割模式 / 参数 | 原因 3 |
| vLLM 崩溃:“attention heads must be divisible” | --tensor-parallel-size 不能整除头数 | 原因 4 |
| vLLM 启动时卡住、无任何报错 | 消费级卡的 PCIe P2P 失效 | 原因 5 |
| 多卡分上了,但比单卡还慢 | 没 NVLink,PCIe 带宽成瓶颈 | 原因 6 |
| 均分时较小的卡 OOM | 显存不等,需要按比例分割 | 原因 7 |
常见原因
1. 模型能装进 GPU 0,本来就不需要分割
Ollama、llama.cpp、vLLM 在模型装得下时都优先用单卡,这是正确选择。一个 Q4_K_M 的 7B 模型(约 4.4 GB)在两张 24 GB 卡上会全部跑在 GPU 0——因为把 4.4 GB 的模型拆到两张卡只会增加卡间通信,却没有任何容量收益。只有当模型大于单卡的可用显存时,自动分布(或需要强制分布)才有意义。
怎么判断: 把模型的显存占用和单卡可用显存比一比。能装进一张卡,单卡运行就是预期行为,不是 bug。
2. CUDA_VISIBLE_DEVICES(或 Docker)把引擎限制在一张卡
CUDA_VISIBLE_DEVICES=0——可能来自 shell 配置、systemd 单元、conda 激活钩子、CI 脚本或父进程——会隐藏除 GPU 0 以外的所有卡。引擎确实只看得到一张卡,无法分配到看不见的卡上。Docker 里的对应情况是用 --gpus '"device=0"' 启动,而不是 --gpus all。
怎么判断: 运行 echo $CUDA_VISIBLE_DEVICES。若输出 0 或单个 GPU UUID,就是这个原因。用 nvidia-smi -L 确认系统层面两张卡都在。在容器里要在容器内跑 nvidia-smi -L,而不是宿主机。
3. 没设置分割模式 / 参数
这是大模型最常见的真实原因。每个引擎都有自己的开关,而且谁都不会在没被告知方式的情况下去拆分一个需要多卡的模型:
- vLLM 默认
--tensor-parallel-size 1(单卡)。把它设成你的 GPU 数量。 - llama.cpp / llama-server 用
--split-mode(-sm):layer(默认,类似流水线,每张卡持有一段连续的层)、row(张量并行,每层把权重拆到多卡)、tensor(实验性的跨后端张量并行)、none(单卡)。在默认layer模式下,模型太大时它确实会自动把层分散开——但只会分到它看得见的卡上,而且要-ngl设得够高,先把层推到 GPU 上才行。 - Ollama 会对装不下单张可见卡的模型自动分布;如果想让它在能装下单卡时也分布,设
OLLAMA_SCHED_SPREAD=1(截至 2026 年 6 月,这应被当作高级覆盖项,而非默认设置)。
怎么判断: 检查启动命令。vLLM 没有 --tensor-parallel-size?llama.cpp 的 -ngl/-sm 没起到作用?Ollama 前面挂着限制性的 CUDA_VISIBLE_DEVICES?任何一条都会把你钉死在 GPU 0。
4. vLLM:tensor-parallel-size 不能整除注意力头数
vLLM 的张量并行是把注意力头拆到各卡,所以模型的注意力头数必须能被 --tensor-parallel-size 整除。否则 vLLM 会在加载时直接报错退出:
Total number of attention heads must be divisible by tensor parallel size
一个有 64 个头的模型,--tensor-parallel-size 取 2、4、8 都可以,但取 3 或 5 不行。
怎么判断: 在启动 traceback 里找上面这行。命中了就改成能整除头数的 TP 值,或者改用 --pipeline-parallel-size(它拆的是层,不是头)。
5. vLLM 在 NCCL 初始化时卡住(无报错、无进度)
在没有 NVLink 的消费级卡上,PCIe 上的 GPU 间点对点访问(P2P)经常被 IOMMU/ACS 或驱动问题搞坏。vLLM 会在 NCCL 初始化阶段(pynccl.py 附近)卡住,没有 traceback,或打印 peer access is not supported between these two devices。
怎么判断 / 修复: 启动时加 NCCL_P2P_DISABLE=1(若没有 InfiniBand,再加 NCCL_IB_DISABLE=1)。如果现在能启动完成,就是 P2P 的问题。它能跑但会损失吞吐,所以这只是诊断手段而非长久之计——真正的修复是关掉 IOMMU/ACS 或更新驱动。
6. 没有 NVLink——PCIe 带宽让分割比单卡还慢
张量并行每层都要在各卡间做一次 all-reduce。在消费级机器上(两张 3090 走 PCIe、没有 NVLink),这个 all-reduce 走 PCIe 总线,就成了瓶颈。截至 2026 年 6 月的经验法则是:对于追求低延迟的单条请求,拆分往往比单卡更慢;张量并行只有在高并发(大致 10 个以上同时请求)时才明显占优,此时 PCIe 开销被摊薄到大量请求上。PCIe 上的单流任务,优先用 pipeline / layer 分割(卡间通信更少),或者干脆把模型放在一张卡上。
怎么判断: 运行 nvidia-smi topo -m。NV# = NVLink(快);PIX/PXB = 经交换机的 PCIe;PHB/SYS = 经主机桥(最慢)。没有 NV# 就是纯 PCIe,相应地调整预期。
7. 显存大小不一——均分会让较小的卡 OOM
如果 GPU 0 是 24 GB、GPU 1 是 16 GB,按 50/50(--tensor-split 1,1)拆分会让 GPU 1 OOM。要按各卡可用显存的比例来分。
怎么判断: 运行 nvidia-smi --query-gpu=memory.total --format=csv,noheader。两个数值不同,就需要按比例的 --tensor-split,而不是 1,1。
最短修复路径
Step 1:确认每张卡都可见
# 列出所有 GPU 及其编号
nvidia-smi -L
# GPU 0: NVIDIA GeForce RTX 3090 (UUID: GPU-abc123)
# GPU 1: NVIDIA GeForce RTX 3090 (UUID: GPU-def456)
# 先清掉任何限制,再让所有卡可见
unset CUDA_VISIBLE_DEVICES
export CUDA_VISIBLE_DEVICES=0,1
如果你有两张卡但 nvidia-smi -L 只列出一张,先解决驱动/插槽问题——操作系统都看不到的卡,任何软件参数都没法分配上去。
Step 2:Ollama——确认两张卡,必要时强制分布
当前的 Ollama 会对装不下单张可见卡的模型自动分布,所以通常你只需要让两张卡都可见。
# 不要限制 Ollama 能看到的 GPU
unset CUDA_VISIBLE_DEVICES # 或:export CUDA_VISIBLE_DEVICES=0,1
# 查看 Ollama 探测到了哪些 GPU
OLLAMA_DEBUG=1 ollama serve 2>&1 | grep -i "gpu\|cuda"
# 期望每张卡一行 "inference compute id=GPU-..."
# 可选:即使模型能装进单卡,也强制分布
export OLLAMA_SCHED_SPREAD=1
如果 Ollama 由 systemd 托管,把变量写进单元文件,而不是只在 shell 里设:
sudo systemctl edit ollama
# 在 [Service] 下加:
# Environment="CUDA_VISIBLE_DEVICES=0,1"
# Environment="OLLAMA_SCHED_SPREAD=1"
sudo systemctl daemon-reload
sudo systemctl restart ollama
# 加载大模型后,ollama ps 应报告它分布在两张卡上
ollama ps
Step 3:llama-server——选好分割模式和比例
# 在两张相同的 24 GB 卡上做默认的 layer 分割
./llama-server \
-m models/llama-3.1-70b-Q4_K_M.gguf \
-sm layer \
--tensor-split 1,1 \
-ngl 99 \
--port 8080
# 张量并行(row)分割——有 NVLink 时延迟更低
./llama-server -m models/llama-3.1-70b-Q4_K_M.gguf -sm row --tensor-split 1,1 -ngl 99 --port 8080
# 显存不等的卡(24 GB + 16 GB)——按比例分割
./llama-server -m models/llama-3.1-70b-Q4_K_M.gguf -sm layer --tensor-split 1.5,1 -ngl 99 --port 8080
截至 2026 年 6 月,-sm/--split-mode 接受 none、layer(默认)、row(已弃用的张量分割)、tensor(实验性)。-ngl/--n-gpu-layers 也接受 auto 和 all。只有在互连足够快时才用 -sm row/tensor;走 PCIe 就老老实实用 layer。
Step 4:vLLM——设置张量(或流水线)并行规模
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Meta-Llama-3.1-70B-Instruct \
--tensor-parallel-size 2 \
--max-model-len 16384 \
--gpu-memory-utilization 0.90 \
--port 8000
--tensor-parallel-size 必须能整除模型的注意力头数(64 个头 → 2/4/8 可以)。如果撞到 Total number of attention heads must be divisible by tensor parallel size,或者你在没有 NVLink 的 PCIe 上想减少卡间通信,改用流水线并行:
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Meta-Llama-3.1-70B-Instruct \
--pipeline-parallel-size 2 \
--gpu-memory-utilization 0.90 \
--port 8000
如果 vLLM 在消费级卡上启动时悄无声息地卡住,在命令前加 NCCL_P2P_DISABLE=1(见原因 5)。
Step 5:确认分割确实生效了
# 生成 token 时实时监控每张卡
watch -n 1 "nvidia-smi --query-gpu=index,utilization.gpu,memory.used,memory.free --format=csv,noheader"
怎么确认已修好: 生成期间,每张卡都应有非零的显存占用,并且(模型已分布时)随着 token 流动会有不止一张卡出现 GPU 利用率的尖峰。如果始终只有 GPU 0 在动,说明分割没生效——回头检查可见性(Step 1)和你所用引擎的分割参数。
预防建议
- 把
CUDA_VISIBLE_DEVICES=0,1(所有相关编号)写进 Ollama 的systemd单元,而不是只写在交互 shell 里。 - 每次驱动或内核更新后重跑
nvidia-smi -L,确认所有卡仍能被枚举。 - 在显存大小不一的机器上,
--tensor-split要按各卡可用显存成比例,绝不要无脑1,1。 - vLLM 只用能整除模型注意力头数的
--tensor-parallel-size值;否则改用--pipeline-parallel-size。 - 纯 PCIe(无 NVLink)的机器,只拆分单卡装不下的模型,并在信任多卡前先和单卡跑个基准对比——单流延迟场景下它往往更慢。
- 把分割模式和
--tensor-split比例写在启动脚本旁的注释里,省得下一个人再去摸索。
常见问答 (FAQ)
Q:多 GPU 是让推理更快,还是只是能跑更大的模型? A:在没有 NVLink 的消费级硬件上,主要是后者。对于能装进一张卡的模型,拆分会增加卡间 all-reduce 通信,单条请求通常更慢。明确的收益在于容量(跑单卡装不下的模型)和高并发下的吞吐,而不是单流速度。
Q:张量并行和流水线并行有什么区别?
A:张量并行(vLLM 的 --tensor-parallel-size、llama.cpp 的 -sm row/tensor)把每个权重矩阵拆到各卡,每层都通信——有 NVLink 时很快,在 PCIe 上受带宽限制。流水线并行(vLLM 的 --pipeline-parallel-size、llama.cpp 的 -sm layer)把整层放在各卡上,通信少得多,代价是”流水线气泡”造成的空闲时间。纯 PCIe 的机器上,pipeline / layer 通常是更稳妥的默认选择。
Q:Ollama 的 ollama ps 里两张卡都列出来了,但只有 GPU 0 有利用率,为什么?
A:ollama ps 显示的是哪些卡持有模型权重,而不是此刻哪张卡在算。持有靠后层的卡在 token 到达那些层之前利用率接近 0%,所以各卡利用率天然不均。只要两张卡都有显存占用、且随时间都会出现尖峰,分割就是在正常工作。
Q:vLLM 在我的两张 RTX 卡上启动时无报错地卡住,怎么办?
A:这几乎总是 PCIe 上的 GPU 点对点访问坏了。启动时加 NCCL_P2P_DISABLE=1(没有 InfiniBand 就再加 NCCL_IB_DISABLE=1)。如果能启动了,就是 P2P 的问题——长久的修复是在 BIOS 里关掉 IOMMU/ACS 或更新驱动。
Q:能不能两张卡各跑一个不同的模型,而不是拆分一个?
A:可以,而且对各自都能装进一张卡的模型往往更好。用 CUDA_VISIBLE_DEVICES=0 在 11434 端口起一个 Ollama 实例,再用 CUDA_VISIBLE_DEVICES=1 在 11435 端口起另一个。两者各用自己的卡,没有任何卡间通信。
Q:能在两张不同型号的卡(比如 4090 + 3090)上做张量并行吗?
A:技术上可以,但要把 --tensor-split 调成匹配各卡的真实显存,并预期较慢的卡会拖住整体速度(木桶效应)。实际操作中,同型号成对要省心得多。
相关阅读
- Ollama 探测不到 GPU,全跑在 CPU
- vLLM 报 context length exceeded
- LM Studio 加载模型时显存 OOM
- vLLM 启动报 CUDA 版本不匹配
- 本地模型冷启动后首 token 极慢
标签: #local-llm #vllm #排查