admin 管理员
积分160
帖子0
注册

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」的组合。

项目配置
GPUTesla 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 视觉)
WebUIOpen-WebUI(Docker 部署)

常驻服务如下,全部配置了开机自启:

服务端口说明
llama-swap8080OpenAI 兼容 API 代理,负责模型的加载 / 卸载
frpc—内网穿透客户端,把 8080 / 22 映射到公网服务器
p40-fan—显卡风扇温控脚本(P40 被动散热,必须自己控风)
open-webui3000Docker 容器,图形化聊天界面
nct6687—主板风扇传感器驱动(内核模块)

二、多模型方案

llama-swap 管理 5 个模型,全部放在同一个 exclusive 互斥组里 —— 一次只跑一个,切换时自动卸载旧模型释放显存。

模型 ID底层模型模型文件上下文显存
qwen3.8-27b-uncensored-q4km-p40Qwen3.8-27B-Uncensored(非思考)Qwen3.8-27B-Uncensored-Q4_K_M.gguf192K(KV q4_0)实测约 20G
qwen3.8-27b-uncensored-q4km-think-p40Qwen3.8-27B-Uncensored(思考)Qwen3.8-27B-Uncensored-Q4_K_M.gguf(同一文件)192K(KV q4_0)实测约 20G
qwen3.8-27b-q4km-think-p40Qwen3.8-27B(思考)Qwen3.8-27B-Q4_K_M.gguf192K(KV q4_0)约 20G
qwen3.8-27b-ud-q4km-think-p40Qwen3.8-27B-UD(思考)Qwen3.8-27B-UD-Q4_K_M.gguf192K(KV q4_0)约 20G
qwen3-vl-8b-instruct-p40Qwen3-VL-8B-Instruct(视觉)Qwen3-VL-8B-Instruct-Q5_K_M.gguf + mmproj128K(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-WebUIhttp://内网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°C80约 2100 RPM
50 - 59°C120约 3000 RPM
60 - 69°C170约 3900 RPM
70 - 77°C220约 4700 RPM
78°C 及以上2555128 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 插件为例:

设置项值
ProviderOpenAI-compatible
Base URLhttps://vanys.xyz/v1
API Key你的API-Key
Modelqwen3.8-27b-uncensored-q4km-p40(或你的模型 ID)
Prompt TemplateChat Completions (OpenAI)
Tools / Function calling开启
Context Window196608
Max tokens8192
Temperature0.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 条)

  1. GPU 完全认不到(BAR1 = 0M):BIOS 里只开 Above 4G Decoding 不够,必须开 Re-Size BAR Support,否则驱动装上也识别不了 P40。
  2. 驱动 535 装不上:apt 源已迁移到 580 分支,直接用 580 驱动 + CUDA 12.8,装好后 apt-mark hold 锁死版本,否则某次 apt upgrade 会把驱动作废。
  3. llama.cpp 报 --ngl: invalid argument:新版已改用 --gpu-layers,还用旧短参数会直接启动失败。
  4. KV cache 参数失效:新版是 --cache-type-k/v,不再是 --kv-cache-type。
  5. safetensors 转 GGUF:需要 torch + transformers 环境,用官方 convert_hf_to_gguf.py 转换。
  6. Qwen3 原生只有 40K:要 64K 得开 YaRN,且 scale 是 1.6(不是按 32K 算出来的 2.0),用错输出会乱。
  7. nct6687 不是内核自带:要自己编译社区驱动,而且内核升级后需重新编译,建议锁内核版本或做成 DKMS。
  8. llama-swap 下载包名:v255 的资产名是 llama-swap_255_linux_amd64.tar.gz,别下错。
  9. Open-WebUI 卡在 embedding 下载:P40 无法访问 HuggingFace,必须设 HF_HUB_OFFLINE=1 并把 RAG embedding model 留空。
  10. Open-WebUI 端口:容器内 uvicorn 监听 8080,所以映射要写 3000:8080,不是 3000:3000。
  11. 显卡风扇通道:nct6687 编译加载后,fan4(SYS_FAN2)才是对着显卡吹的那个,别写错通道。

十四、待办事项

  1. vanys 主机安全组放行 8082(nginx 反代到 frp 转发的 llama-swap,对外用 https://vanys.xyz/v1,不暴露端口号);如需外网访问 Open-WebUI 再放行 3000。
  2. 开机自启后模型不会预加载(按需加载,闲置 5 分钟自动卸载),如需常驻可自行调整 TTL。

十五、小结

整套方案的成本就是一张二手 P40 加一台普通主机,换来的是一个完全自控、数据不出门、带 OpenAI 兼容接口的大模型服务。24G 显存配合 KV cache 量化,跑 27B 甚至更大的量化模型都还有余量。缺点是 Pascal 老架构不支持新的量化格式和部分加速特性,速度比不上新卡,但胜在便宜且稳定。

有同样想法的可以照着配,遇到问题欢迎在楼下讨论。



This is a test signature
暂无回复,快来抢沙发吧!
请 登录 后发表回复
hljs.highlightAll();