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

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

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

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

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

二、多模型方案

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

模型 ID底层模型模型文件上下文显存
qwen3-14b-64k-q4km-p40Qwen3-14B-Instruct(OpenPipe 转换)Qwen3-14B-Instruct.Q4_K_M.gguf64K(YaRN 1.6)约 14G
qwen2.5-14b-32k-q4km-p40Qwen2.5-14B-Instructqwen2.5-14b-instruct-q4_k_m.gguf32K约 13G
deepseek-r1-8b-q4km-p40DeepSeek-R1-0528-Qwen3-8BDeepSeek-R1-0528-Qwen3-8B-Q4_K_M.gguf32K约 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-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 = "你的公网服务器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°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

九、客户端接入配置

因为是标准的 OpenAI 兼容接口,任何支持自定义 Base URL 的客户端都能直接用。以 Qt Creator 的 QodeAssist 插件为例:

设置项
ProviderOpenAI-compatible
Base URLhttp://公网服务器IP:8082/v1
API Key你的API-Key
Modelqwen3-14b-64k-q4km-p40(或你的模型 ID)
Prompt TemplateChat Completions (OpenAI)
Tools / Function calling开启
Context Window65536
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. 云服务器安全组放行 8082(公网访问 llama-swap),如需外网访问 Open-WebUI 再放行 3000。
  2. 开机自启后模型不会预加载(按需加载,闲置 5 分钟自动卸载),如需常驻可自行调整 TTL。

十四、小结

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

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


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