双卡 A100 + Ollama 生产部署从安装、踩坑、调优到最终可上线方案

张开发
2026/4/8 6:25:30 15 分钟阅读

分享文章

双卡 A100 + Ollama 生产部署从安装、踩坑、调优到最终可上线方案
一、为什么这套方案值得认真做很多人一看到双卡 A100就会下意识觉得这还不简单吗两张卡性能肯定够装上就能飞。现实恰恰相反。因为硬件越强你越容易犯一个错误以为资源足够大所以很多问题都可以被“蛮力掩盖”。实际上生产环境里最容易出问题的从来不是“卡不够强”而是这些更基础的地方服务治理不清晰端口管理混乱模型加载策略不稳定参数没有收敛调用层分流做得很烂日志、探活、重试都没做结果就是机器配置看着很豪华服务偶尔也确实很快但一上压测、一上并发、一上真实流量就开始抖所以这套方案真正的目标不是“让双卡亮起来”而是把双卡 A100 变成一套可长期稳定提供本地推理服务的生产节点。二、项目目标不是能跑而是能上线这次方案的目标最终收敛成了四句话1. 服务要稳定开机自启异常自动拉起日志可追踪进程状态可观测2. 模型要稳定模型目录权限正确模型支持常驻避免反复冷启动支持开机自动预热3. 双卡要真正吃起来不是一张卡忙、一张卡闲不是“理论双卡”而是“实际双实例分流”请求要能均匀打到两个端口4. 参数要可收敛吞吐优先而不是参数堆满并发、上下文、KV Cache、队列要一起看最终形成可解释、可复制的配置说白了这套方案追求的不是“演示”而是“交付”。三、最终结论先说双卡 A100 更适合双实例方案这次实践里一个非常重要的结论就是对于能单卡放下的模型双卡 A100 上更适合用“双实例双端口分流”而不是指望一个实例自动把两张卡优雅吃满。所以最终架构是这样的实例一GPU0绑定CUDA_VISIBLE_DEVICES0监听11434实例二GPU1绑定CUDA_VISIBLE_DEVICES1监听11435上层Python 客户端做轮询分发支持健康检查支持失败切换支持预热感知这样做的好处非常直接每张卡各自维护一份模型副本两张卡可同时接流量单实例状态更清晰调优边界更明确故障影响更可控一句话概括就是把复杂调度问题拆成两个简单实例问题。四、从“安装成功”到“服务可用”中间至少隔了五个坑这一段很重要因为很多文章都喜欢直接跳到“最终配置”但真正有价值的恰恰是这些坑。坑一systemd服务文件根本没创建好最开始的报错并不复杂Failed toenableunit: Unitfileollama-gpu0.service does not exist.这说明不是 Ollama 坏了而是服务文件没创建成功文件名写错了或者放错目录了这个坑看起来初级但实际非常常见。因为一旦你开始用双实例服务就不再是默认的ollama.service而是你自己维护的ollama-gpu0.serviceollama-gpu1.service也就是说从这一步开始你就已经进入“自己要对 systemd 配置负责”的阶段了。坑二服务文件存在但进程一启动就退出后面更进一步进入了这种状态ExecStart/usr/local/bin/ollama serve(codeexited,status1/FAILURE)这时候最容易误判是不是路径不对是不是 Ollama 装坏了是不是 CUDA 有问题但真正的正确动作不是猜而是先确认路径再看日志。最终确认结果是whichollama /usr/local/bin/ollama也就是说这台机器上ExecStart/usr/local/bin/ollama serve是对的不需要盲目改成/usr/bin/ollama。这一步其实给了我们一个很实用的经验文档路径不是机器路径机器路径才是真路径。坑三11434 端口被占用后面日志里又出现了一个很经典的问题Error: listen tcp 0.0.0.0:11434: bind: address already in use这说明什么说明gpu0起不来不是因为 GPU不是因为配置也不是因为权限而是11434 已经被别的进程占了。而这种情况在 Ollama 场景里特别常见因为默认ollama.service很可能已经在跑。也就是说你一边想搞双实例一边默认实例还在默默占着11434那gpu0永远起不来。这一步最后得出的治理原则非常明确既然你决定走双实例就不要再保留默认单实例服务。坑四目录明明写了为什么还是没权限端口问题解决之后最关键的报错终于出现了Error: mkdir /data/ollama: permission denied: ensure path elements are traversable这个错误特别有迷惑性。很多人第一反应是哦/data/ollama/models没创建我建一下就好了。其实不够。因为这里最关键的不是mkdir而是后面这个词traversable它的意思是运行服务的ollama用户不仅要对目标目录有权限还必须能穿过整条上级目录链。也就是说只要下面任意一级目录没有可进入权限就会报错//data/data/ollama/data/ollama/models这也是这次排障里最有代表性的一个收获Linux 目录权限问题很多时候不是目标目录本身而是父目录链路权限不通。坑五双实例起来了不代表双卡真的工作了就算服务正常起来也不代表双卡就真的在吃流量。因为如果 Python 调用层写成这样base_urlhttp://127.0.0.1:11434那11435那个实例就只是“存在”而不是“在工作”。这一点非常关键。很多双卡部署失败不是败在服务层而是败在调用层服务拆成两份了但业务代码永远只调一个端口结果就是一张卡累死一张卡养老所以真正让双卡工作起来的不只是两个systemd而是请求分流机制。五、最终生产版架构长什么样复盘到最后整套方案最终定型成这样1. 服务层ollama-gpu0.serviceollama-gpu1.service2. 模型层自定义模型目录/data/ollama/modelsollama用户拥有目录读写权限模型常驻支持预热3. 参数层OLLAMA_KEEP_ALIVE-1OLLAMA_FLASH_ATTENTION1OLLAMA_KV_CACHE_TYPEq8_0OLLAMA_MAX_LOADED_MODELS1OLLAMA_NUM_PARALLEL4OLLAMA_MAX_QUEUE1024OLLAMA_CONTEXT_LENGTH81924. 调用层Python 实例池轮询分发/api/version轻探活/api/ps检查运行模型失败自动切换预热感知5. 观测层systemctl statusjournalctlollama psnvidia-smi/api/generate返回指标这套结构不是“看起来复杂”而是“职责清晰”服务负责活着模型负责热着调用层负责分流观测层负责解释问题六、最终版 systemd 配置/etc/systemd/system/ollama-gpu0.service[Unit] DescriptionOllama GPU0 Service Afternetwork-online.target [Service] ExecStart/usr/local/bin/ollama serve Userollama Groupollama Restartalways RestartSec3 EnvironmentPATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin EnvironmentOLLAMA_HOST0.0.0.0:11434 EnvironmentCUDA_VISIBLE_DEVICES0 EnvironmentOLLAMA_MODELS/data/ollama/models EnvironmentOLLAMA_KEEP_ALIVE-1 EnvironmentOLLAMA_FLASH_ATTENTION1 EnvironmentOLLAMA_KV_CACHE_TYPEq8_0 EnvironmentOLLAMA_MAX_LOADED_MODELS1 EnvironmentOLLAMA_NUM_PARALLEL4 EnvironmentOLLAMA_MAX_QUEUE1024 EnvironmentOLLAMA_CONTEXT_LENGTH8192 [Install] WantedBymulti-user.target/etc/systemd/system/ollama-gpu1.service[Unit] DescriptionOllama GPU1 Service Afternetwork-online.target [Service] ExecStart/usr/local/bin/ollama serve Userollama Groupollama Restartalways RestartSec3 EnvironmentPATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin EnvironmentOLLAMA_HOST0.0.0.0:11435 EnvironmentCUDA_VISIBLE_DEVICES1 EnvironmentOLLAMA_MODELS/data/ollama/models EnvironmentOLLAMA_KEEP_ALIVE-1 EnvironmentOLLAMA_FLASH_ATTENTION1 EnvironmentOLLAMA_KV_CACHE_TYPEq8_0 EnvironmentOLLAMA_MAX_LOADED_MODELS1 EnvironmentOLLAMA_NUM_PARALLEL4 EnvironmentOLLAMA_MAX_QUEUE1024 EnvironmentOLLAMA_CONTEXT_LENGTH8192 [Install] WantedBymulti-user.target这一版配置的核心思想很简单每张卡一个实例每个实例一个主模型模型尽量常驻并发适度放开上下文控制在吞吐友好区间队列留一点缓冲但不指望它创造性能七、权限这件事必须一次性处理到位这一步是上线前的必做项sudomkdir-p/data/ollama/modelssudochown-Rollama:ollama /data/ollamasudochmod755/datasudochmod755/data/ollamasudochmod755/data/ollama/models再验证namei-l/data/ollama/modelssudo-uollamabash-lccd /data/ollama/models touch .perm_test rm -f .perm_test echo ok为什么我这里特别强调这一块因为很多问题看起来像模型目录不存在服务权限有问题systemd 用户不对本质上最终往往都能归结为一句话目录链路没打通。而这一类问题如果你不一次性彻底处理好它会在每次重启、每次换目录、每次换用户时反复找你。八、自动预热是真正让服务变“顺滑”的关键一步服务起来了不代表第一批请求就好用。如果你不做预热常见现象就是刚启动后第一批请求很慢两个实例首包时延不一致压测前几轮数据很丑用户第一波请求体验不好所以最终加入了一个预热脚本/usr/local/bin/ollama-prewarm.sh#!/usr/bin/env bashset-eMODEL${1:-gemma3}forportin1143411435;doecho[prewarm] checking${port}curl-sfhttp://127.0.0.1:${port}/api/version/dev/nullecho[prewarm] loading${MODEL}on${port}curl-sfhttp://127.0.0.1:${port}/api/generate\-d{\model\:\${MODEL}\,\keep_alive\:-1}/dev/nullecho[prewarm] verifying${MODEL}on${port}curl-sfhttp://127.0.0.1:${port}/api/psdone然后再把它接入systemd/etc/systemd/system/ollama-prewarm.service[Unit] DescriptionPrewarm Ollama Models Afterollama-gpu0.service ollama-gpu1.service Wantsollama-gpu0.service ollama-gpu1.service [Service] Typeoneshot ExecStart/usr/local/bin/ollama-prewarm.sh gemma3 [Install] WantedBymulti-user.target这样做完以后整条链路就闭环了服务启动模型预热模型常驻再开始接流量这时第一批用户就不再是你的“免费预热器”。九、吞吐调优的真正核心不是调参数而是在做显存预算分配这是整套方案里最值得记住的一句话吞吐调优本质上是在做显存预算分配。你在分配的其实是这几项1. 给并发多少预算对应OLLAMA_NUM_PARALLEL2. 给单请求上下文多少预算对应OLLAMA_CONTEXT_LENGTH3. 给模型驻留多少预算对应keep_alive/OLLAMA_KEEP_ALIVE4. 给 KV Cache 压缩多少预算对应OLLAMA_KV_CACHE_TYPE5. 给高峰排队多少预算对应OLLAMA_MAX_QUEUE6. 给多模型竞争多少预算对应OLLAMA_MAX_LOADED_MODELS这些参数不是彼此独立的。尤其这一条必须牢牢记住并发和上下文是乘法关系。也就是说实例真正承受的压力不是4 并发8192 上下文这么简单而是4 × 8192 这个级别的上下文预算。所以很多调优失败并不是因为参数不够大而是因为上下文太大并发太高两个一起把显存预算吃爆了最终表现为时延抖动队列堆积503 overloadedCPU offload双卡看起来很忙但整体吞吐不高十、真正会看压测结果的人看的是“慢在哪里”很多人压测只看一句平均耗时多少秒这不够。真正有价值的是接口返回里的这些字段total_durationload_durationprompt_eval_durationeval_duration它们分别能告诉你1. 是不是模型没热好看load_duration2. 是不是输入太长看prompt_eval_duration3. 是不是生成本身慢看eval_duration4. 是不是整体链路在抖看total_duration你只有把这几个值一起看才能知道慢是因为没预热慢是因为 prompt 太大慢是因为生成长度太长慢是因为实例过载否则你只知道“慢”却不知道“为什么慢”。十一、ollama ps和nvidia-smi一个都不能少很多人喜欢只看nvidia-smi。它当然重要但它只能告诉你显存占多少GPU 利用率怎么样它不能直接告诉你模型是不是 100% GPU有没有偷偷 offload 到 CPU当前实例实际拿到的上下文是多少这时候就必须结合ollamaps和watch-n1nvidia-smi一起看。你真正应该关心的是ollama ps里模型是不是100% GPUCONTEXT是多少两个实例是不是都挂了模型两张卡是不是都在稳定出力真正的调优不是单看一个指标而是把 API 指标、运行状态、GPU 状态拼起来看。十二、Python 调用层决定了双卡到底是不是双卡服务层解决的是“实例存在”。调用层解决的是“实例工作”。最终的 Python 调用方案要具备四个能力1. 轮询分流不能永远打一个端口2. 探活先看/api/version3. 失败切换一个实例失败时自动切到另一个4. 预热感知两个实例都要先热起来最终思路其实很朴素准备两个客户端轮询拿客户端先探活失败标记不健康过一段时间再恢复探测这套东西写出来不复杂但价值非常大。因为它让双实例真正变成了一个可调度的本地推理池。十三、上线前的最终检查清单如果要把这套方案交付上线我建议最后按下面这张清单走一遍。服务层ollama-gpu0.service正常运行ollama-gpu1.service正常运行ollama-prewarm.service可正常执行默认ollama.service已停用目录层/data/ollama/models已创建ollama用户对目录有读写权限父目录链路可遍历端口层11434正常监听11435正常监听没有额外进程占端口模型层模型可正常拉取两个实例都能预热/api/ps能看到运行模型模型保持常驻性能层ollama ps显示100% GPU两张 A100 都有稳定利用率load_duration在预热后明显下降没有持续性 503 overloaded调用层Python 客户端支持轮询Python 客户端支持失败切换Python 客户端支持探活压测时两个实例都能吃到请求只要这一套检查清单都过了这套方案基本就已经脱离“实验环境”进入“可上线环境”了。十四、这次实践最重要的五个收获最后把整次方案压成五句话。收获一默认服务和自定义双实例不能混用否则端口冲突几乎是必然的。收获二目录权限问题很多时候卡在父目录不是目标目录不存在而是ollama用户过不去。收获三双卡要真正工作调用层必须做分流服务拆两份没意义请求不分流一样是一张卡干活。收获四吞吐调优本质是显存预算分配不是把参数调大而是让每一份显存花得值。收获五生产化的关键不是服务能起而是服务能一直稳systemd、常驻、预热、探活、重试、压测、观测缺一个都可能让“看起来能跑”的方案在真实流量里变得不好用。十五、结尾这套方案到底算不算完成如果只是从“把 Ollama 安装到服务器上”这个角度看这套方案早就该结束了。但如果你从“能不能作为一套真正可用的本地推理服务上线”这个角度看到这一篇才算真正收官。因为我们最终交付的已经不是一条命令、一个模型、一个端口。而是一整套完整能力双实例服务治理双卡资源利用模型常驻与预热目录权限治理Python 调用封装轮询分流与失败切换压测与性能判读最终上线检查清单这才是一套像样的生产方案。说到底真正的技术能力不是“你会不会装 Ollama”而是当它不顺的时候你能不能把它一步一步拉回到正确的位置。而这次双卡 A100 Ollama 的完整实践本质上就是这样一场从“能装”走到“能打”的过程。

更多文章