多 GPU 没分配上,模型只跑在卡 0

本地 LLM 只用一张卡,其余卡 0% 空闲。用 llama.cpp --split-mode、vLLM --tensor-parallel-size、Ollama 自动分布,以及 PCIe 机器需要的 NCCL 参数把它分到多卡上。

机器里插了两张 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 -mNV# = 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 接受 nonelayer(默认)、row(已弃用的张量分割)、tensor(实验性)。-ngl/--n-gpu-layers 也接受 autoall。只有在互连足够快时才用 -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 调成匹配各卡的真实显存,并预期较慢的卡会拖住整体速度(木桶效应)。实际操作中,同型号成对要省心得多。

相关阅读

标签: #local-llm #vllm #排查