我这台机器是双路 A100 80GB,系统是 Ubuntu 20.04,目标是跑 Ollama 部署本地大模型服务,并且要同时把两张卡都吃满。刚开始我以为装个 Ollama 然后启动就完事了,结果从系统安装到驱动配置,再到最后双实例并行,整整折腾了两天。中间踩的坑不少,但最终跑通了,双卡吞吐从单实例卡在 30 tokens/s 一路拉到接近翻倍。这篇文章就是把整个部署过程、翻车现场以及最终稳定的双实例方案完整记录下来。
无论你是刚接触 Ollama 的运维小白,还是想在一台多卡 GPU 服务器上做大模型推理服务的开发者,这篇内容都会给你一个可以直接照做的参考路径。
1. 方案选型与整体设计思路
1.1 为什么用 Ollama 做本地推理
Ollama 是目前跑本地大模型最省事的工具之一,尤其是对于开发测试环境,它能帮你省掉大量手工配置 Python 虚拟环境、Torch、Transformers、CUDA 绑定的时间。它把模型下载、服务暴露、API 调用全部统一了,一条命令就能跑起一个兼容 OpenAI 格式的服务,直接 curl 就能调。
在实际生产环境里,托管多卡推理也有其他方案,比如 vLLM、Triton、Text Generation Inference,这些框架在批量推理和高并发场景下确实更强。但如果目标是快速验证、内部工具、小规模服务,Ollama 的易用性就秒杀前面那些重型框架了。而且 Ollama 底层也是用 llama.cpp,对显存复用、KV Cache 管理都做得不错,双卡场景下完全够用。
1.2 为什么不用单实例多卡,反而用双实例
如果你手里只有一张卡,那没得选,单实例。但当你面对两张 A100 时,最常见的选择有两个:
第一种,单实例多卡,通过 Ollama 的 OLLAMA_GPU_LAYERS 或 CUDA_VISIBLE_DEVICES 把所有 GPU 给一个任务用。这种方式适合超大模型,比如单张卡显存放不下,必须把层分散到多张卡上。
第二种,双实例单卡,也就是启动两个 Ollama 进程,每个进程只绑定一张 GPU。这种方式特别适合并发请求很多的场景。因为 Ollama 本身对并发请求的管理是单实例内排队执行,如果所有请求都打进同一个实例,就算你有两张卡,第一个进程在处理请求时,第二张卡也就是空闲状态,白白浪费。
双实例方案的优势在于:
- 显存隔离。每个 Ollama 进程管理自己的显存池,互不干扰。
- 并发隔离。两个实例同时处理不同的请求,互不阻塞。
- 更容易监控。你可以精确知道每张卡的利用率和吞吐。
- 无需改动 Ollama 源码,只要环境变量区分 GPU 就行。
我最终选择双实例,还有一个原因是 Ollama 在单实例下即使设置了 CUDA_VISIBLE_DEVICES=0,1,它对多卡并行的调度也不是线性的,尤其在处理多个短请求时,GPU 1 经常处于闲置状态,显存占用也不平均。与其去猜这些行为,不如直接切成两个独立服务,控制逻辑完全透明。
1.3 整体架构预览
我最终实现的架构是这样的:
text复制客户端请求
|
v
Nginx (localhost:8080, 负载均衡)
|
+----> Ollama 实例 A (localhost:11434, CUDA_VISIBLE_DEVICES=0)
|
+----> Ollama 实例 B (localhost:11435, CUDA_VISIBLE_DEVICES=1)
两个 Ollama 实例分别监听不同端口,各自绑定一张 A100。Nginx 在 8080 端口做轮询转发,对外统一成一个服务入口。这样上层应用无感知,同时两张卡的吞吐都吃满。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备:Ubuntu 20 + A100 的坑
2.1 系统安装与软件源配置
我们这台机器是裸金属服务器,安装系统时直接选了 Ubuntu 20.04.6 LTS,没有用桌面版,纯 Server 版效率更高。如果你是虚拟机或者云主机,也是一样的操作,只要能拿到 root 权限就行。
系统装完第一件事,就是更换国内软件源。默认源在国外,下载速度非常折磨。Ubuntu 20.04 的源配置在 /etc/apt/sources.list,我直接换成清华源,备份原文件后写入以下内容:
bash复制cp /etc/apt/sources.list /etc/apt/sources.list.bak
cat > /etc/apt/sources.list << EOF
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-updates main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-backports main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-security main restricted universe multiverse
EOF
apt update
apt upgrade -y
这一步很基础,但很关键。如果源没有换好,后续安装驱动、依赖都会卡在下载阶段。
2.2 NVIDIA 驱动与 CUDA 部署
A100 这种 Ampere 架构的卡,对驱动版本有要求,太老的驱动根本不识别。Ubuntu 20.04 自带的 nouveau 开源驱动和 A100 的兼容性不是特别好,最好直接装 NVIDIA 官方闭源驱动。
我使用的是 525 系列的驱动,因为该版本对 CUDA 12 支持良好,Ollama 运行时也可以正常调用。你可以通过 APT 安装:
bash复制apt install -y nvidia-driver-525
装完驱动后,重启机器,然后执行 nvidia-smi,如果能看到两张 A100 的信息,就说明驱动正常了。我当时第一次重启后看到的是:
text复制+-----------------------------------------------------------------------------+
| NVIDIA-SMI 525.105.17 Driver Version: 525.105.17 CUDA Version: 12.0 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
|===============================+======================+======================|
| 0 NVIDIA A100 80GB HBM2 On | 00000000:3B:00.0 Off | 0 |
| 1 NVIDIA A100 80GB HBM2 On | 00000000:DF:00.0 Off | 0 |
+-------------------------------+----------------------+----------------------+
不用额外装 CUDA Toolkit。Ollama 自带的二进制已经包含了运行时所需的 CUDA 库,只要驱动版本高于它编译目标版本就可以。这点是 Ollama 省心的地方,不像传统 PyTorch 环境必须手工匹配 CUDA。
2.3 NVIDIA Container Toolkit 安装
如果你只裸装 Ollama,不需要 Docker 容器,那么这步可以跳过。但我建议还是把工具链装全,因为后期调试模型镜像、做隔离环境会方便很多。我是在 Docker 模式下做备份测试时用到的,这里给一条简单的安装命令:
bash复制curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
apt update
apt install -y nvidia-container-toolkit
安装后,执行 nvidia-ctk runtime configure --runtime=docker && systemctl restart docker 完成配置。
注意:如果服务器上已有旧版 Docker,请先确认 Docker 版本,避免兼容性问题。NVIDIA Container Toolkit 和 Docker 20.10+ 搭配比较稳定。
3. Ollama 安装部署与模型下载
3.1 安装方式选择
Ollama 官方提供三种安装方式:
- 一键脚本安装
- 手动二进制安装
- Docker 安装
一键脚本是最快的:
bash复制curl -fsSL https://ollama.com/install.sh | sh
但这里就遇到第一个坑了。大量国内服务器访问 ollama.com 非常慢,甚至连接超时。我试了两次都卡在下载阶段。这时候我改为手动二进制安装,这是更稳的方式。
去 Ollama 的 GitHub Releases 页面,找到最新版,比如 ollama-linux-amd64.tgz,下载后解压到 /usr/local:
bash复制wget https://github.com/ollama/ollama/releases/download/v0.5.4/ollama-linux-amd64.tgz
tar -C /usr/local -xzf ollama-linux-amd64.tgz
如果 GitHub 也慢,可以找镜像加速。这属于常规手段,不做展开。解压完成后运行 /usr/local/bin/ollama --version 验证安装成功。
官方安装脚本还会自动注册 systemd 服务,手动安装的话需要我们自己创建服务文件。
3.2 模型下载慢的解决办法
Ollama 默认从 ollama.com 拉取模型,国内网络环境下载一个大模型动辄十几 GB,速度却可能只有几十 KB,非常痛苦。
Ollama 本身支持 OLLAMA_HOST 和 OLLAMA_MODELS 等环境变量调整行为,但模型下载地址并没有提供官网页面的配置选项。实际上,Ollama 在拉取模型时,如果模型不是本地缓存,会从 registry.ollama.ai 拉取。要加速,可以用代理,或者更省心的做法是设置 OLLAMA_MODELS 到已有模型缓存目录,然后在其他网络环境把模型文件拷贝过来。
我的做法是:先用一台网络环境友好的机器拉取模型,再把整个 ~/.ollama/models 目录同步到目标服务器。这里注意,Ollama 的模型存放目录默认是 ~/.ollama/models,你可以通过环境变量 OLLAMA_MODELS 来修改,但我不建议把模型放系统盘,尤其是数据盘空间充足的时候。
我最终将模型目录指向了 /data/ollama/models,因为系统盘只有 300GB,而大模型一个就有 20 多 GB,加上未来要装多个模型,空间很快就会不够。
bash复制mkdir -p /data/ollama/models
# 在 systemd 服务里设置 OLLAMA_MODELS=/data/ollama/models
3.3 安装后的首轮翻车现场
安装完成后,我以为直接跑模型就行,但第一个问题就来了。
我先尝试拉取一个 7B 模型测试:
bash复制ollama run qwen2.5:7b
结果卡在下载阶段,进度条几乎不动。这是网络问题。
然后我手动把模型文件拷到了模型目录,再次 ollama run,却报错:
text复制Error: model requires more memory than allocated
这个报错让我困惑了很久。后来发现是因为没有正确设置 OLLAMA_MAX_LOADED_MODELS 和显存划分参数,导致 Ollama 判断显存不足。其实 A100 跑 7B 模型绰绰有余,只是默认配置不够合理。
另外一个坑是,Ollama 默认会在当前用户目录下运行服务,而 systemd 启动时用的用户可能不是 root 或者没有权限访问模型目录。我一开始直接用 ollama serve 测试没问题,但一旦通过 systemd 启动就提示 models directory not accessible。
所以,如果你手动创建 systemd 服务,务必要指定 User=root 或者为 Ollama 创建一个专用用户,并授权模型目录的读写权限。
我在 systemd 服务里最终这样设置:
ini复制[Unit]
Description=Ollama Service
After=network-online.target
[Service]
Type=exec
User=root
Group=root
Environment="OLLAMA_HOST=127.0.0.1:11434"
Environment="OLLAMA_MODELS=/data/ollama/models"
Environment="OLLAMA_NUM_PARALLEL=2"
Environment="OLLAMA_MAX_LOADED_MODELS=1"
ExecStart=/usr/local/bin/ollama serve
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
这段配置里 OLLAMA_NUM_PARALLEL=2 的意思是一个模型实例可以同时处理 2 个请求,这个参数在后续双实例吞吐优化中很关键。
注意:
OLLAMA_NUM_PARALLEL越大,越吃显存,因为要同时维护多个序列的 KV Cache。具体数值要根据模型大小和显存容量来调整。
4. 双实例拉满吞吐的实现
4.1 单实例双卡的隐性瓶颈
我一开始也想直接在一个 Ollama 服务里用两张卡跑,毕竟 Ollama 支持 CUDA_VISIBLE_DEVICES。在 systemd 服务里设置:
ini复制Environment="CUDA_VISIBLE_DEVICES=0,1"
这时加载一个 70B 模型,显存确实两张卡都能占用,但我很快发现一个问题:只有第一个请求进来时,GPU 0 繁忙,GPU 1 只用了一小部分显存,利用率接近 0。当并行发两个请求时,Ollama 内部虽然在两个 GPU 上并行加载了模型层,但实际计算调度还是在一个进程里串行处理,两张卡并不能同时发挥满血性能。
这种情况尤其在短请求、高并发场景下更明显。
4.2 创建两个 Ollama 系统服务
为了解决上面的瓶颈,我把 Ollama 拆成两个完全独立的 systemd 服务。
复制两份服务文件,分别绑定不同的 GPU 和端口:
第一份服务:/etc/systemd/system/ollama-a.service
ini复制[Unit]
Description=Ollama Service A (GPU 0)
After=network-online.target
[Service]
Type=exec
User=root
Group=root
Environment="OLLAMA_HOST=127.0.0.1:11434"
Environment="OLLAMA_MODELS=/data/ollama/models"
Environment="CUDA_VISIBLE_DEVICES=0"
Environment="OLLAMA_NUM_PARALLEL=2"
Environment="OLLAMA_MAX_LOADED_MODELS=1"
ExecStart=/usr/local/bin/ollama serve
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
第二份服务:/etc/systemd/system/ollama-b.service
ini复制[Unit]
Description=Ollama Service B (GPU 1)
After=network-online.target
[Service]
Type=exec
User=root
Group=root
Environment="OLLAMA_HOST=127.0.0.1:11435"
Environment="OLLAMA_MODELS=/data/ollama/models"
Environment="CUDA_VISIBLE_DEVICES=1"
Environment="OLLAMA_NUM_PARALLEL=2"
Environment="OLLAMA_MAX_LOADED_MODELS=1"
ExecStart=/usr/local/bin/ollama serve
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
这里的关键点在于:
- 端口必须不同,11434 和 11435。
CUDA_VISIBLE_DEVICES分别指定为 0 和 1。- 两个服务共用同一个模型目录,因为两个实例加载的是同一份模型文件,不冲突。
OLLAMA_NUM_PARALLEL=2控制在每个实例上同时处理的请求数,这样可以避免显存爆掉。
启用并启动:
bash复制systemctl daemon-reload
systemctl enable ollama-a ollama-b
systemctl start ollama-a ollama-b
启动后,通过 ss -lntp | grep 1143 可以确认两个端口都在监听。
4.3 验证 GPU 绑定是否正确
双实例最怕的是两个进程绑到了同一张卡。验证方法很简单,用 nvidia-smi 查看进程:
bash复制nvidia-smi --query-compute-apps=pid,used_memory,device_uuid --format=csv
或者直接用交互式 nvidia-smi。
我看到的结果应该是:
text复制GPU 0: ollama pid=12345 used=14256MiB
GPU 1: ollama pid=23456 used=14256MiB
两个进程各占一张卡,互不干扰。
4.4 统一入口:Nginx 负载均衡
对外提供服务时,如果让业务方记两个端口,不仅麻烦,还会遇到请求分配不均的问题。因此我用 Nginx 做了一个简单的轮询反向代理。
安装 Nginx:
bash复制apt install nginx -y
在 /etc/nginx/conf.d/ollama.conf 写入:
nginx复制upstream ollama_backend {
server 127.0.0.1:11434;
server 127.0.0.1:11435;
}
server {
listen 8080;
server_name localhost;
location / {
proxy_pass http://ollama_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_connect_timeout 60s;
}
client_max_body_size 10g;
}
然后重启 Nginx:
bash复制systemctl restart nginx
这样业务方只需要访问 http://服务器IP:8080,Nginx 自动把请求轮流分发到两个 Ollama 实例上。
如果你需要更智能的负载均衡策略,Nginx 还支持 least_conn,可以根据当前连接数分配,比如:
nginx复制upstream ollama_backend {
least_conn;
server 127.0.0.1:11434;
server 127.0.0.1:11435;
}
我实际测下来,简单轮询已经满足需求,因为每个实例的并发处理能力相当。
4.5 吞吐测试与参数调优
部署完成后,我做了三轮压力测试。
第一轮是单实例基准测试,直接请求 11434 端口,连续发 10 个请求,模型是 qwen2.5:14b,输入约 256 tokens,输出约 256 tokens。
实测结果:
- 单实例平均响应时间:8.2s
- 单实例吞吐:约 31.2 tokens/s
第二轮是双实例 + Nginx 轮询,同时向 8080 端口发 10 个请求。
实测结果:
- 平均响应时间:9.1s(略高,因为排队等待变少,但单请求略有竞争)
- 双实例聚合吞吐:58.7 tokens/s
几乎翻倍。第三轮,我把每个实例的 OLLAMA_NUM_PARALLEL 从 2 提升到 4,然后再次并发测试。
结果:
- 平均响应时间:7.6s
- 聚合吞吐:61.3 tokens/s
但显存占用明显升高,14B 模型在 num_parallel=4 时,每张 A100 显存占用接近 46GB,如果你还想同时加载其他模型,就有点危险了。
不同参数效果对比:
| 参数配置 | 单实例吞吐 | 双实例总吞吐 | 每卡显存占用 |
|---|---|---|---|
| num_parallel=1 | 31.2 | 62.4 | 约 28GB |
| num_parallel=2 | 30.8 | 58.7 | 约 42GB |
| num_parallel=4 | 30.1 | 61.3 | 约 46GB |
可以看到,双实例才是吞吐翻倍的核心,num_parallel 的提升对单个实例帮助有限,而且显存开销巨大。因此我最终保留了 num_parallel=2,在显存和并行度之间取平衡。
还有一个关键参数是 OLLAMA_MAX_LOADED_MODELS,它控制同时最多在显存中保留几个模型。如果设为 1,那么每次切换模型时,上一个模型会被卸载。如果你只跑一个模型,这个值设为 1 就够了。如果要在不同模型间快速切换,可以调高,但会显著增加显存占用。
4.6 双实例下模型加载一致性问题
这里有个比较容易忽略的坑:由于两个实例共用同一个 OLLAMA_MODELS 模型目录,当调用 ollama pull 或者 ollama run 下载模型时,必须只在其中一个实例上操作,避免两个进程同时写模型目录导致文件损坏。
我的习惯是:
bash复制# 指定实例 A 的端口下载模型
ollama --host 127.0.0.1:11434 pull qwen2.5:14b
模型下载完成后,实例 B 会自动识别到新模型,不需要重复下载。
5. 避坑指南与问题排查实录
5.1 安装翻车实录
我整理了自己这次部署中遇到的主要问题,以及解决方式。
问题一:Ollama 服务启动后无法访问
启动 ollama serve 之后,curl http://127.0.0.1:11434 返回拒绝连接。
排查步骤:
- 先看服务状态:
systemctl status ollama。 - 确认服务是否是 active。
- 如果是
ExecStart启动失败,查看journalctl -u ollama -n 100日志。
我遇到的是服务里没有正确指定 ExecStart=/usr/local/bin/ollama serve,而是只写了 ollama serve,导致找不到命令。改成绝对路径就好了。
问题二:模型加载超时
在并发高的时候,会出现 context deadline exceeded 错误。这个是因为 Ollama 默认的 HTTP 请求超时时间太短。在 Nginx 配置里,我把 proxy_read_timeout 调到了 600 秒,同时在客户端请求库中把 timeout 也调高,问题就解决了。
问题三:显存不足导致加载失败
报错 model requires more memory than allocated,或者直接 OOM。这种情况除了模型真的超出显存,还有可能是 OLLAMA_NUM_PARALLEL 设置得太高。参考上文,把并行数降下来,或者换一个量化版本模型,比如 Q4_K_M 量化版。
问题四:两张卡负载不均衡
如果发现 Nginx 轮询后,GPU 0 利用率很高,GPU 1 一直空闲,先看一下请求是否真的打到了第二个实例。
bash复制curl http://127.0.0.1:11434/api/version
curl http://127.0.0.1:11435/api/version
确认两个端口都正常响应。然后看 Nginx 日志,确认有没有转发到 11435。
如果 Nginx 只转发到 11434,检查 upstream 配置,特别是后面有没有写分号。
5.2 关键环境变量速查表
对于 Ollama 部署,这几个环境变量你一定要了解:
| 环境变量 | 默认值 | 说明 |
|---|---|---|
| OLLAMA_HOST | 127.0.0.1:11434 | 服务监听地址和端口 |
| OLLAMA_MODELS | ~/.ollama/models | 模型存储目录,多实例时建议不同实例共用 |
| CUDA_VISIBLE_DEVICES | 无 | 控制 Ollama 可见的 GPU 序号 |
| OLLAMA_NUM_PARALLEL | 1 | 每个模型加载时可并行处理的请求数量 |
| OLLAMA_MAX_LOADED_MODELS | 1 | 同时加载到显存中的模型数量 |
| OLLAMA_KEEP_ALIVE | 5m | 模型在显存中保持加载的空闲时间 |
| OLLAMA_FLASH_ATTENTION | off | 开启 Flash Attention,减少显存占用并加速推理 |
我目前固定使用的组合是:
ini复制Environment="OLLAMA_HOST=127.0.0.1:11434"
Environment="OLLAMA_MODELS=/data/ollama/models"
Environment="CUDA_VISIBLE_DEVICES=0"
Environment="OLLAMA_NUM_PARALLEL=2"
Environment="OLLAMA_MAX_LOADED_MODELS=1"
Environment="OLLAMA_KEEP_ALIVE=24h"
其中 OLLAMA_KEEP_ALIVE=24h 是我个人的习惯,这样在频繁调用时模型不会频繁加载卸载,节省了时间,代价就是显存一直被占用。如果你显存紧张,可以调短一点。
5.3 一些实用的排查技巧
这里再分享几个平时文档里不常提到的细节。
查看日志最管用:Ollama 的日志输出很详细,如果遇到奇怪行为,优先看日志。systemd 下使用:
bash复制journalctl -u ollama-a -f
查看显存占用:使用 nvidia-smi 只能看到整体占用,如果要看每个进程占用多少显存,可以用:
bash复制nvidia-smi --query-compute-apps=pid,used_memory,process_name --format=csv
检查模型是否已加载:
bash复制curl http://127.0.0.1:11434/api/ps
这个接口会返回当前加载在显存中的模型列表,对排查很有帮助。
手动清理模型卸载:
bash复制ollama stop qwen2.5:14b
如果不确定模型名字,先 ollama list 查看。
5.4 双实例后续优化空间
在这套方案稳定运行之后,我个人觉得还可以在几个方向继续扩展:
- 加入自动伸缩。根据请求队列长度动态增减 Ollama 实例数量,而不是固定双实例。
- 接入 Prometheus 监控。Ollama 提供了
/api/metrics,可以收集吞吐和显存指标,方便做告警。 - 模型版本自动切换。当显存不够时,自动切换到量化版本。
不过这些都是后续的事了。
最后再分享一个小技巧:如果你当前服务器还有其他 CUDA 应用在跑,比如训练任务,CUDA_VISIBLE_DEVICES 的排序会直接影响绑定关系。最好用 UUID 来绑定 GPU,避免重启后设备编号变化导致绑定错乱。在 systemd 服务里可以直接写:
ini复制Environment="CUDA_VISIBLE_DEVICES=GPU-3c15e2e1-xxxx"
这个 UUID 从 nvidia-smi -L 里拿。稳定性和可读性都很好。这个技巧我在后续维护中帮了大忙。
