P40 24G 显卡本地大模型部署全流程:llama.cpp + llama-swap 多模型热切换(含 Qwen3.8-27B-Uncensored 实战)
关键词:P40 24G · llama.cpp · llama-swap · Qwen3.8-27B-Uncensored · 多模型热切换 · 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,挂载 /) |
| 数据盘 | 1TB NVMe SSD(nvme0n1,可用 938G,挂载 /data) |
| 推理引擎 | llama.cpp(源码编译,CUDA 12.8) |
| 代理层 | llama-swap v255(多模型自动加载 / 切换 / 卸载) |
| NVIDIA 驱动 | 580.178.04(apt-mark hold 锁定版本) |
| 模型数量 | 5 个(Qwen3.8-27B 系 ×4 + Qwen3-VL-8B 视觉) |
| WebUI | Open-WebUI(Docker 部署) |
常驻服务如下,全部配置了开机自启:
| 服务 | 端口 | 说明 |
|---|---|---|
| llama-swap | 8080 | OpenAI 兼容 API 代理,负责模型的加载 / 卸载 |
| frpc | — | 内网穿透客户端,把 8080 / 22 映射到公网服务器 |
| p40-fan | — | 显卡风扇温控脚本(P40 被动散热,必须自己控风) |
| open-webui | 3000 | Docker 容器,图形化聊天界面 |
| nct6687 | — | 主板风扇传感器驱动(内核模块) |
二、多模型方案
llama-swap 管理 5 个模型,全部放在同一个 exclusive 互斥组里 —— 一次只跑一个,切换时自动卸载旧模型释放显存。
| 模型 ID | 底层模型 | 模型文件 | 上下文 | 显存 |
|---|---|---|---|---|
| qwen3.8-27b-uncensored-q4km-p40 | Qwen3.8-27B-Uncensored(非思考) | Qwen3.8-27B-Uncensored-Q4_K_M.gguf | 192K(KV q4_0) | 实测约 20G |
| qwen3.8-27b-uncensored-q4km-think-p40 | Qwen3.8-27B-Uncensored(思考) | Qwen3.8-27B-Uncensored-Q4_K_M.gguf(同一文件) | 192K(KV q4_0) | 实测约 20G |
| qwen3.8-27b-q4km-think-p40 | Qwen3.8-27B(思考) | Qwen3.8-27B-Q4_K_M.gguf | 192K(KV q4_0) | 约 20G |
| qwen3.8-27b-ud-q4km-think-p40 | Qwen3.8-27B-UD(思考) | Qwen3.8-27B-UD-Q4_K_M.gguf | 192K(KV q4_0) | 约 20G |
| qwen3-vl-8b-instruct-p40 | Qwen3-VL-8B-Instruct(视觉) | Qwen3-VL-8B-Instruct-Q5_K_M.gguf + mmproj | 128K(KV q8_0) | 约 16G |
注意:
- 27B 全系跑 192K(196608)上下文,KV cache 量化到 q4_0,实测总占用约 20G/24G。
- Qwen3.8-27B 原生上下文已覆盖 192K,不需要 YaRN / rope 扩展(对照:早期 Qwen3-14B 要 40K→64K 必须
--rope-scale 1.6,scale 用错输出会乱)。 - 非思考 / 思考变体用同一份权重文件,只差
--chat-template-kwargs(enable_thinking / reasoning_effort)。 - 24G 显存一次只能跑一个大模型,所以放进 exclusive 组自动 swap。
三、llama-swap 配置
配置文件:/opt/llama-swap/config.yaml(属主 llamauser)
listen: 0.0.0.0:8080
logLevel: info
globalTTL: 300
healthCheckTimeout: 300
metricsMaxInMemory: 50
apiKeys:
- "你的API-Key"
groups:
main-gpu:
exclusive: true
models:
- qwen3.8-27b-uncensored-q4km-p40
- qwen3.8-27b-uncensored-q4km-think-p40
- qwen3.8-27b-q4km-think-p40
- qwen3.8-27b-ud-q4km-think-p40
models:
# Qwen3-VL-8B-Instruct (unsloth Q5_K_M 5.9G + mmproj-F16 1.2G) —— 本机视觉/图片识别
qwen3-vl-8b-instruct-p40:
group: main-gpu
ttl: 900
env:
- CUDA_VISIBLE_DEVICES=0
cmd: >
/usr/local/bin/llama-server
--model /data/models/Qwen3-VL-8B-Instruct-Q5_K_M.gguf
--mmproj /data/models/mmproj-Qwen3-VL-8B-Instruct-F16.gguf
--gpu-layers 99
--ctx-size 131072
--n-predict 4096
--parallel 1
--cache-type-k q8_0
--cache-type-v q8_0
--jinja
--host 127.0.0.1
--port ${PORT}
--timeout 1200
qwen3.8-27b-uncensored-q4km-p40:
group: main-gpu
ttl: 900
env:
- CUDA_VISIBLE_DEVICES=0
cmd: >
/usr/local/bin/llama-server
--model /data/models/Qwen3.8-27B-Uncensored-Q4_K_M.gguf
--gpu-layers 99
--ctx-size 196608
--n-predict 49152
--parallel 1
--cache-type-k q4_0
--cache-type-v q4_0
--batch-size 2048
--jinja
--chat-template-kwargs '{"enable_thinking": false}'
--kv-unified
--host 127.0.0.1
--port ${PORT}
--timeout 1200
qwen3.8-27b-uncensored-q4km-think-p40:
group: main-gpu
ttl: 900
env:
- CUDA_VISIBLE_DEVICES=0
cmd: >
/usr/local/bin/llama-server
--model /data/models/Qwen3.8-27B-Uncensored-Q4_K_M.gguf
--gpu-layers 99
--ctx-size 196608
--n-predict 49152
--parallel 1
--cache-type-k q4_0
--cache-type-v q4_0
--batch-size 2048
--jinja
--chat-template-kwargs '{"enable_thinking": true, "reasoning_effort": "low"}'
--kv-unified
--host 127.0.0.1
--port ${PORT}
--timeout 1800
qwen3.8-27b-q4km-think-p40:
group: main-gpu
ttl: 900
env:
- CUDA_VISIBLE_DEVICES=0
cmd: >
/usr/local/bin/llama-server
--model /data/models/Qwen3.8-27B-Q4_K_M.gguf
--gpu-layers 99
--ctx-size 196608
--n-predict 49152
--parallel 1
--cache-type-k q4_0
--cache-type-v q4_0
--batch-size 2048
--jinja
--chat-template-kwargs '{"enable_thinking": true, "reasoning_effort": "low"}'
--kv-unified
--host 127.0.0.1
--port ${PORT}
--timeout 1800
qwen3.8-27b-ud-q4km-think-p40:
group: main-gpu
ttl: 900
env:
- CUDA_VISIBLE_DEVICES=0
cmd: >
/usr/local/bin/llama-server
--model /data/models/Qwen3.8-27B-UD-Q4_K_M.gguf
--gpu-layers 99
--ctx-size 196608
--n-predict 49152
--parallel 1
--cache-type-k q4_0
--cache-type-v q4_0
--batch-size 2048
--jinja
--chat-template-kwargs '{"enable_thinking": true, "reasoning_effort": "low"}'
--kv-unified
--host 127.0.0.1
--port ${PORT}
--timeout 1800
上面就是当前 /opt/llama-swap/config.yaml 的完整内容(API Key 已打码)。4 个 27B 参数已统一:192K(196608)上下文、KV q4_0、n-predict 49152,只差模型文件与 thinking 的 --chat-template-kwargs;VL-8B 是视觉模型(Q5_K_M + mmproj,128K / q8_0)。
关键点:
- 层数参数用长名
--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.8-27B-Uncensored(192K 上下文)
后来把主力模型升级成了 Qwen3.8-27B-Uncensored(Q4_K_M 量化):两个变体都跑 192K(196608)上下文、KV cache q4_0 量化,实测总占用约 20G/24G,decode 约 10 t/s。非思考版日常用,思考版做推理任务(当前加载的就是思考版)。实际拉起 llama-server 的命令行如下(当前运行中的思考版):
/usr/local/bin/llama-server \
--model /data/models/Qwen3.8-27B-Uncensored-Q4_K_M.gguf \
--gpu-layers 99 \
--ctx-size 196608 \
--n-predict 49152 \
--parallel 1 \
--cache-type-k q4_0 \
--cache-type-v q4_0 \
--batch-size 2048 \
--jinja \
--chat-template-kwargs '{"enable_thinking": true, "reasoning_effort": "low"}' \
--kv-unified \
--host 127.0.0.1 --port 5804 --timeout 1800
非思考版只改 --chat-template-kwargs '{"enable_thinking": false}' 与 --timeout 1200。端口由 llama-swap 自动分配(示例中 5804 是当前加载的思考版端口),客户端只打 llama-swap 的 8080。
要点:
--cache-type-k/v q4_0是能把 192K 上下文塞进 24G 的关键,KV cache 量化后省下大量显存(实测总占用约 20G)。--parallel 1单并发最稳。早期 Qwen3.5 用 --parallel 2 配--kv-unified-per-slot 65536分槽;Qwen3.8 降到单并发,去掉了 per-slot。--jinja启用模型自带的 chat template,工具调用(function calling)必须开。--chat-template-kwargs控制 thinking:非思考版"enable_thinking": false(日常最快);思考版"enable_thinking": true, "reasoning_effort": "low"(慢一些但推理质量更好)。--n-predict 49152限定最大输出长度;--batch-size 2048提升 prompt 读取速度。
五、API 访问信息
请求需带鉴权头:Authorization: Bearer 你的API-Key,或 x-api-key: 你的API-Key。
| 位置 | 地址 |
|---|---|
| 本机 | http://127.0.0.1:8080/v1 |
| 局域网 | http://内网IP:8080/v1 |
| 公网(走 frp 转发) | https://vanys.xyz/v1(走 vanys 主机 nginx 反代 → frp 转发;对外地址不带端口号,避免暴露端口) |
| 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 = "vanys.xyz"
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
九、米家开机卡远程电源管理(优雅关机方案)
P40 主机放在家里,人不在时需要远程开关机 / 关机。用的是米家「Jiuhao 电脑开机卡 Pro」,接主板 PWRBTN 排针,WiFi 连家庭光猫,米家 APP 远程控电。配合一条 systemd 配置,把三种操作的行为定下来:
| 操作 | 行为 | 说明 |
|---|---|---|
| 关机 | 优雅关机 | 卡片模拟短按电源键 → Linux ACPI 事件 → systemd-logind 走 poweroff,正常走 systemd 关机流程(约 1 分钟,干净) |
| 开机 | 干净上电 | 卡片给电源上电,正常开机 |
| 重启 | 硬件 RESET | 卡片发 RESET 信号(不断电复位),非优雅;偶尔用没问题,实测未伤文件系统 |
关键配置(让"关机"走优雅流程):/etc/systemd/logind.conf 里设 HandlePowerKey=poweroff,改完 systemctl restart systemd-logind。这样米家 APP 点的"关机"(模拟短按电源键)会被 systemd-logind 解释成 poweroff,而不是默认的 suspend / ignore。
重刷系统后复现(两步):
echo 'HandlePowerKey=poweroff' >> /etc/systemd/logind.conf
systemctl restart systemd-logind
实测(2026-10-18):关机 → 优雅关机(08:27:11,干净);开机 → 正常(08:28:27);重启 → 硬件 RESET(08:30:23,ext4 干净挂载、无文件系统损伤)。日常用"关机 / 开机"就是安全的优雅关机,"重启"留作偶尔用(相当于断电重启)。
优雅重启(未实施):想让"重启也走 systemd 优雅流程",需装一个守护进程订阅 MQTT p40/cmd,收到 reboot 再执行 systemctl reboot;触发端可以是 vanys 主机(本文的"公网服务器 / 外网服务器",8.138.235.178)的网页按钮或手机 MQTT app。
十、客户端接入配置
因为是标准的 OpenAI 兼容接口,任何支持自定义 Base URL 的客户端都能直接用。以 Qt Creator 的 QodeAssist 插件为例:
| 设置项 | 值 |
|---|---|
| Provider | OpenAI-compatible |
| Base URL | https://vanys.xyz/v1 |
| API Key | 你的API-Key |
| Model | qwen3.8-27b-uncensored-q4km-p40(或你的模型 ID) |
| Prompt Template | Chat Completions (OpenAI) |
| Tools / Function calling | 开启 |
| Context Window | 196608 |
| 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)才是对着显卡吹的那个,别写错通道。
十四、待办事项
- vanys 主机安全组放行 8082(nginx 反代到 frp 转发的 llama-swap,对外用 https://vanys.xyz/v1,不暴露端口号);如需外网访问 Open-WebUI 再放行 3000。
- 开机自启后模型不会预加载(按需加载,闲置 5 分钟自动卸载),如需常驻可自行调整 TTL。
十五、小结
整套方案的成本就是一张二手 P40 加一台普通主机,换来的是一个完全自控、数据不出门、带 OpenAI 兼容接口的大模型服务。24G 显存配合 KV cache 量化,跑 27B 甚至更大的量化模型都还有余量。缺点是 Pascal 老架构不支持新的量化格式和部分加速特性,速度比不上新卡,但胜在便宜且稳定。
有同样想法的可以照着配,遇到问题欢迎在楼下讨论。