P40 24G 显卡本地大模型部署全流程:llama.cpp + llama-swap 多模型热切换(含 Qwen3.5-27B 实战)
关键词:P40 24G · llama.cpp · llama-swap · Qwen3.5-27B · 多模型热切换 · OpenAI 兼容接口 · 本地大模型部署
用一张二手的 Tesla P40(24G 显存)在一台普通主机上搭起一套可对外提供 OpenAI 兼容接口的本地大模型推理服务。整套方案的核心是 llama.cpp(推理引擎)+ llama-swap(多模型热切换代理),配合 frp 内网穿透对外发布,再挂一个 Open-WebUI 当聊天前端。
本文记录完整的部署过程、全部配置文件、关键路径、客户端接入方式以及踩过的坑。文中涉及的 API Key、frp token、公网 IP、内网 IP 均已做脱敏处理,请替换为你自己的值。
一、当前部署架构
P40 主机上运行的核心是一套「llama-swap 代理 + 多模型 + Open-WebUI」的组合。
| 项目 | 配置 |
|---|---|
| GPU | Tesla P40 24GB(Pascal 架构, sm_61) |
| 操作系统 | Ubuntu 22.04.5,内核 5.15.0-191 |
| 系统盘 | 476.9G NVMe(nvme1n1,挂载 /) |
| 数据盘 | 223.6G NVMe(nvme0n1,挂载 /data) |
| 推理引擎 | llama.cpp(源码编译,CUDA 12.8) |
| 代理层 | llama-swap v255(多模型自动加载 / 切换 / 卸载) |
| NVIDIA 驱动 | 580.178.04(apt-mark hold 锁定版本) |
| 模型数量 | 3 个(Qwen3-14B / Qwen2.5-14B / DeepSeek-R1-8B) |
| WebUI | Open-WebUI(Docker 部署) |
常驻服务如下,全部配置了开机自启:
| 服务 | 端口 | 说明 |
|---|---|---|
| llama-swap | 8080 | OpenAI 兼容 API 代理,负责模型的加载 / 卸载 |
| frpc | — | 内网穿透客户端,把 8080 / 22 映射到公网服务器 |
| p40-fan | — | 显卡风扇温控脚本(P40 被动散热,必须自己控风) |
| open-webui | 3000 | Docker 容器,图形化聊天界面 |
| nct6687 | — | 主板风扇传感器驱动(内核模块) |
二、多模型方案
llama-swap 管理 3 个模型,全部放在同一个 exclusive 互斥组里 —— 一次只跑一个,切换时自动卸载旧模型释放显存。
| 模型 ID | 底层模型 | 模型文件 | 上下文 | 显存 |
|---|---|---|---|---|
| qwen3-14b-64k-q4km-p40 | Qwen3-14B-Instruct(OpenPipe 转换) | Qwen3-14B-Instruct.Q4_K_M.gguf | 64K(YaRN 1.6) | 约 14G |
| qwen2.5-14b-32k-q4km-p40 | Qwen2.5-14B-Instruct | qwen2.5-14b-instruct-q4_k_m.gguf | 32K | 约 13G |
| deepseek-r1-8b-q4km-p40 | DeepSeek-R1-0528-Qwen3-8B | DeepSeek-R1-0528-Qwen3-8B-Q4_K_M.gguf | 32K | 约 8G |
注意:
- Qwen3-14B 原生上下文只有 40960(40K),要上 64K 必须开 YaRN:
--rope-scale 1.6 --yarn-orig-ctx 40960。注意这里不是常见的 2.0,用错会导致输出崩坏。 - Qwen2.5-14B 与 DeepSeek-R1 原生就是 32K,无需 YaRN。
- 24G 显存一次只能跑一个大模型,所以放进 exclusive 组自动 swap。
三、llama-swap 配置
配置文件:/opt/llama-swap/config.yaml(属主 llamauser)
listen: 0.0.0.0:8080
logLevel: info
globalTTL: 300
healthCheckTimeout: 300
apiKeys:
- "你的API-Key"
groups:
main-gpu:
exclusive: true
models:
- qwen3-14b-64k-q4km-p40
- qwen2.5-14b-32k-q4km-p40
- deepseek-r1-8b-q4km-p40
models:
qwen3-14b-64k-q4km-p40:
group: main-gpu
ttl: 300
env:
- CUDA_VISIBLE_DEVICES=0
cmd: >
/usr/local/bin/llama-server
--model /data/models/Qwen3-14B-Instruct.Q4_K_M.gguf
--gpu-layers 99
--ctx-size 65536
--n-predict 8192
--cache-type-k q8_0
--cache-type-v q8_0
--rope-scaling yarn
--rope-scale 1.6
--yarn-orig-ctx 40960
--host 127.0.0.1
--port ${PORT}
qwen2.5-14b-32k-q4km-p40:
group: main-gpu
ttl: 300
env:
- CUDA_VISIBLE_DEVICES=0
cmd: >
/usr/local/bin/llama-server
--model /data/models/qwen2.5-14b-instruct-q4_k_m.gguf
--gpu-layers 99
--ctx-size 32768
--n-predict 8192
--cache-type-k q8_0
--cache-type-v q8_0
--host 127.0.0.1
--port ${PORT}
deepseek-r1-8b-q4km-p40:
group: main-gpu
ttl: 300
env:
- CUDA_VISIBLE_DEVICES=0
cmd: >
/usr/local/bin/llama-server
--model /data/models/DeepSeek-R1-0528-Qwen3-8B-Q4_K_M.gguf
--gpu-layers 99
--ctx-size 32768
--n-predict 8192
--cache-type-k q8_0
--cache-type-v q8_0
--host 127.0.0.1
--port ${PORT}
关键点:
- 层数参数用长名
--gpu-layers,不要用短名--ngl(新版 llama.cpp 短参数存在解析 bug)。 - KV cache 量化参数是
--cache-type-k/--cache-type-v,旧写法--kv-cache-type已失效。 - 改完配置重启:
systemctl restart llama-swap。 - 上生产前先校验:
llama-swap -config /opt/llama-swap/config.yaml -validate。
四、当前生产模型:Qwen3.5-27B(131K 上下文)
后来把主力模型换成了 Qwen3.5-27B(Q3_K_M 量化),靠 KV cache 量化 + 统一 KV 池,把 131K 上下文塞进 24G 显存,实测可用。实际拉起 llama-server 的命令行如下:
/usr/local/bin/llama-server \
--model /data/models/Qwen3.5-27B-Q3_K_M.gguf \
--gpu-layers 99 \
--ctx-size 131072 \
--n-predict 8192 \
--parallel 2 \
--cache-type-k q8_0 \
--cache-type-v q8_0 \
--jinja \
--kv-unified \
--kv-unified-per-slot 65536 \
--reasoning on \
--temp 1.0 --top-k 20 --top-p 0.95 \
--presence-penalty 1.5 --repeat-penalty 1.15 --repeat-last-n 512 \
--host 127.0.0.1 --port 5801
要点:
--cache-type-k q8_0 --cache-type-v q8_0是能把 131K 塞进 24G 的关键,KV cache 量化后省下大量显存。--parallel 2配合--kv-unified让两个并发槽共享 KV 池,--kv-unified-per-slot 65536限定单槽上限。--jinja启用模型自带的 chat template,工具调用(function calling)必须开。--reasoning on打开思维链模式(Qwen3.5 系列支持)。- 采样参数是官方推荐值,别乱改,尤其是
--temp 1.0配--top-p 0.95。
五、API 访问信息
请求需带鉴权头:Authorization: Bearer 你的API-Key,或 x-api-key: 你的API-Key。
| 位置 | 地址 |
|---|---|
| 本机 | http://127.0.0.1:8080/v1 |
| 局域网 | http://内网IP:8080/v1 |
| 公网(走 frp 转发) | http://公网服务器IP:8082/v1(需在云服务器安全组放行 8082) |
| Open-WebUI | http://内网IP:3000 |
| llama-swap 调试面板 | http://内网IP:8080/ui |
六、systemd 服务
# /etc/systemd/system/llama-swap.service
[Unit]
Description=llama-swap proxy for llama.cpp P40-24G
After=network.target
[Service]
Type=simple
User=llamauser
Group=llamauser
ExecStart=/usr/local/bin/llama-swap --config /opt/llama-swap/config.yaml
Restart=on-failure
RestartSec=5
Environment="CUDA_VISIBLE_DEVICES=0"
Environment="LD_LIBRARY_PATH=/usr/local/lib/ollama"
OOMScoreAdjust=-50
MemoryHigh=28G
[Install]
WantedBy=multi-user.target
# /etc/systemd/system/frpc.service
[Unit]
Description=frp client - connect to public frps server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/frpc -c /etc/frp/frpc.toml
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
# /etc/frp/frpc.toml (token 已脱敏)
serverAddr = "你的公网服务器IP"
serverPort = 7000
auth.method = "token"
auth.token = "你的-frp-token"
[[proxies]]
name = "p40-llamaswap"
type = "tcp"
localIP = "127.0.0.1"
localPort = 8080
remotePort = 8082
[[proxies]]
name = "p40-ssh"
type = "tcp"
localIP = "127.0.0.1"
localPort = 22
remotePort = 6001
# /etc/systemd/system/p40-fan.service
[Unit]
Description=P40 GPU fan control (nct6687 pwm4)
After=multi-user.target
[Service]
Type=simple
ExecStart=/usr/local/bin/p40-fan-control.sh
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
风扇服务跑在 root 下(写 hwmon 需要权限),Restart=always 保证脚本异常退出后 5 秒自动拉起。开机自启用 systemctl enable p40-fan.service。
七、关键路径速查表
| 用途 | 路径 |
|---|---|
| llama.cpp 源码 | /data/llama.cpp |
| llama-server 可执行文件 | /usr/local/bin/llama-server(依赖库在 /usr/local/lib/ollama) |
| llama-swap 可执行文件 | /usr/local/bin/llama-swap |
| llama-swap 配置 | /opt/llama-swap/config.yaml |
| 模型文件目录 | /data/models/ |
| 模型转换 venv | /data/ggufenv |
| 风扇控制脚本 | /usr/local/bin/p40-fan-control.sh |
| nct6687 内核模块 | /lib/modules/$(uname -r)/kernel/drivers/hwmon/nct6687.ko |
| Open-WebUI 数据目录 | /data/open-webui |
| frpc 配置 | /etc/frp/frpc.toml |
| 运行推理的用户 | llamauser(uid 1001) |
八、风扇温控(P40 必看)
Tesla P40 是被动散热卡,本身不带风扇。装进普通机箱后必须靠机箱风扇吹,而且得能按 GPU 温度自动调速,否则分分钟过热降频甚至关机。
做法:编译社区驱动 nct6687(Fred78290/nct6687d)读取主板风扇传感器,再由 p40-fan.service 根据 GPU 温度写 pwm4(对应 SYS_FAN2,即朝显卡吹的那个机箱风扇):
| GPU 温度 | PWM 值 | 实测转速 |
|---|---|---|
| 低于 50°C | 80 | 约 2100 RPM |
| 50 - 59°C | 120 | 约 3000 RPM |
| 60 - 69°C | 170 | 约 3900 RPM |
| 70 - 77°C | 220 | 约 4700 RPM |
| 78°C 及以上 | 255 | 5128 RPM(满转) |
实测升温(45 → 68 → 74°C)时风扇逐级加速(80 → 170 → 220),降温后自动回落,映射完全正确。满转时噪音不小,建议主机放得离人远一点。
控制脚本 /usr/local/bin/p40-fan-control.sh 的完整内容(每 3 秒读一次 GPU 温度,PWM 值变化时写入并记 syslog,方便用 journalctl -t p40-fan 查调速历史):
# /usr/local/bin/p40-fan-control.sh
#!/bin/bash
# P40 GPU 温度控制显卡风扇 (nct6687 pwm4) - 自动检测hwmon路径
# 读取 nvidia-smi 的 GPU 温度, 映射 PWM(0-255) 控制显卡风扇(SYS_FAN2/pwm4)
# 由 systemd 服务 p40-fan.service 开机自启
# 自动检测 nct6687 的 hwmon 路径(重启后编号可能变化)
HWMON_PATH=""
for h in /sys/class/hwmon/hwmon*; do
if [ "$(cat $h/name 2>/dev/null)" = "nct6687" ]; then
HWMON_PATH="$h"
break
fi
done
# 若未检测到, 回退到默认(手动指定)
[ -z "$HWMON_PATH" ] && HWMON_PATH="/sys/class/hwmon/hwmon5"
PWM_CH="pwm4" # 显卡风扇对应通道 (实测 fan4 = System Fan#2)
PWM_ENABLE="${HWMON_PATH}/${PWM_CH}_enable"
PWM_VAL="${HWMON_PATH}/${PWM_CH}"
# 手动模式接管 (enable: 1=manual)
echo 1 > "${PWM_ENABLE}" 2>/dev/null
LAST_PWM=""
# 温度 -> PWM 映射 (P40被动卡需主动散热)
temp_to_pwm() {
local t=$1
if [ "$t" -lt 50 ]; then echo 80 # <50°C 低转速待机
elif [ "$t" -lt 60 ]; then echo 120 # 50-59 中低
elif [ "$t" -lt 70 ]; then echo 170 # 60-69 中高
elif [ "$t" -lt 78 ]; then echo 220 # 70-77 高
else echo 255 # >=78 全速
fi
}
while true; do
GPU_TEMP=$(nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader,nounits 2>/dev/null | head -1)
if [ -z "$GPU_TEMP" ]; then
PWM=120
sleep 5
else
PWM=$(temp_to_pwm "$GPU_TEMP")
sleep 3
fi
if [ "$PWM" != "$LAST_PWM" ]; then
echo "$PWM" > "$PWM_VAL" 2>/dev/null
LAST_PWM="$PWM"
logger -t p40-fan "GPU ${GPU_TEMP}C PWM ${PWM}"
fi
done
九、客户端接入配置
因为是标准的 OpenAI 兼容接口,任何支持自定义 Base URL 的客户端都能直接用。以 Qt Creator 的 QodeAssist 插件为例:
| 设置项 | 值 |
|---|---|
| Provider | OpenAI-compatible |
| Base URL | http://公网服务器IP:8082/v1 |
| API Key | 你的API-Key |
| Model | qwen3-14b-64k-q4km-p40(或你的模型 ID) |
| Prompt Template | Chat Completions (OpenAI) |
| Tools / Function calling | 开启 |
| Context Window | 65536 |
| Max tokens | 8192 |
| Temperature | 0.2 |
其他客户端(Continue、Cherry Studio、LobeChat、Open-WebUI 等)同理,只要填 Base URL + Key + Model 三项即可。
十、Open-WebUI
- 访问地址:http://内网IP:3000
- 首次打开需注册管理员账号。
- 添加模型连接:设置 → 连接 → OpenAI,Base URL 填
http://host.docker.internal:8080/v1,Key 填你的 API-Key。 - 容器名
open-webui,镜像ghcr.io/open-webui/open-webui:main。 - 数据卷:
/data/open-webui挂到容器内/app/backend/data。 - 端口映射:
3000:8080(注意容器内 uvicorn 监听的是 8080,不是 3000)。 - 必须设
HF_HUB_OFFLINE=1:P40 无法访问 HuggingFace,不设会导致 RAG embedding 下载卡死。 - 重启容器:
docker restart open-webui。
十一、验证清单
- GPU 识别正常(Re-Size BAR 修复后)
- 驱动 580 + CUDA 12.8 正常
- 多模型加载 / 切换正常
- Function Calling 可用
- API Key 鉴权生效
- TTL 空闲卸载(300 秒后显存归零)
- 风扇温控升温 / 降温响应正确
- 重启后所有服务自动拉起
- Open-WebUI 可正常对话
十二、踩坑记录(11 条)
- GPU 完全认不到(BAR1 = 0M):BIOS 里只开 Above 4G Decoding 不够,必须开 Re-Size BAR Support,否则驱动装上也识别不了 P40。
- 驱动 535 装不上:apt 源已迁移到 580 分支,直接用 580 驱动 + CUDA 12.8,装好后
apt-mark hold锁死版本,否则某次 apt upgrade 会把驱动作废。 - llama.cpp 报
--ngl: invalid argument:新版已改用--gpu-layers,还用旧短参数会直接启动失败。 - KV cache 参数失效:新版是
--cache-type-k/v,不再是--kv-cache-type。 - safetensors 转 GGUF:需要 torch + transformers 环境,用官方
convert_hf_to_gguf.py转换。 - Qwen3 原生只有 40K:要 64K 得开 YaRN,且 scale 是 1.6(不是按 32K 算出来的 2.0),用错输出会乱。
- nct6687 不是内核自带:要自己编译社区驱动,而且内核升级后需重新编译,建议锁内核版本或做成 DKMS。
- llama-swap 下载包名:v255 的资产名是
llama-swap_255_linux_amd64.tar.gz,别下错。 - Open-WebUI 卡在 embedding 下载:P40 无法访问 HuggingFace,必须设
HF_HUB_OFFLINE=1并把 RAG embedding model 留空。 - Open-WebUI 端口:容器内 uvicorn 监听 8080,所以映射要写
3000:8080,不是3000:3000。 - 显卡风扇通道:nct6687 编译加载后,
fan4(SYS_FAN2)才是对着显卡吹的那个,别写错通道。
十三、待办事项
- 云服务器安全组放行 8082(公网访问 llama-swap),如需外网访问 Open-WebUI 再放行 3000。
- 开机自启后模型不会预加载(按需加载,闲置 5 分钟自动卸载),如需常驻可自行调整 TTL。
十四、小结
整套方案的成本就是一张二手 P40 加一台普通主机,换来的是一个完全自控、数据不出门、带 OpenAI 兼容接口的大模型服务。24G 显存配合 KV cache 量化,跑 27B 甚至更大的量化模型都还有余量。缺点是 Pascal 老架构不支持新的量化格式和部分加速特性,速度比不上新卡,但胜在便宜且稳定。
有同样想法的可以照着配,遇到问题欢迎在楼下讨论。