NUC9 + fnOS + RX 6600 部署 llama.cpp 本地大模型推理服务

前言

事情是这样的。

家里那台 NUC9 幽灵峡谷,装了飞牛 fnOS,插着一张 RX 6600 独显,日常跑 Docker 用 llama.cpp + ROCm 做本地推理。前不久刚经历了一次显卡凭空消失的诡异故障 —— 排查了半天发现是金手指接触不良,重新插拔解决了。那次折腾完之后我就想:既然硬件都搞定了,不如把整个部署过程完整记录下来。

坦率讲,在 NAS 上跑本地大模型不是刚需,但 GPU 闲着也是闲着。RX 6600 有 7.8GB 显存,跑个 9B 的量化模型绰绰有余。局域网内搭一个兼容 OpenAI API 的推理服务,macOS 上的 Hermes Agent 和浏览器的 Page Assist 都能直连,比每个月给云服务交 API 费用舒服多了。

这篇文章记录了从零开始在 NUC9 + fnOS + RX 6600 上,用 Docker 部署 llama.cpp ROCm 推理服务,最终接入日常使用的 AI 工具链的完整过程。包含硬件配置、Dockerfile 编写、docker-compose.yml 逐字段解释、模型选型、性能实测,以及几个容易踩的坑。

硬件配置

项目参数
主机Intel NUC9 幽灵峡谷 (Ghost Canyon)
CPUi9-9980HK,8 核 16 线程
GPUAMD Radeon RX 6600,7.8GB VRAM,gfx1032
内存32GB DDR4
系统飞牛 fnOS(基于 Debian)
存储NVMe 固态,模型放 /vol4/1000/ai-models

这套配置跑本地推理有几个好处:NUC9 体积小、功耗低(整机满载不到 150W),放角落 7x24 开着不心疼电费。RX 6600 虽然不算高端卡,但在消费级 AMD 显卡里 ROCm 支持算不错的,7.8GB 显存刚好能跑 9B 参数的 Q4 量化模型。i9-9980HK 虽然是移动端 CPU,但在这种场景下主要看 GPU,CPU 基本摸鱼。

前置准备

AMD 容器工具包

fnOS 的应用中心里搜「AMD」,装 AMD 容器工具包。这一步本质上是把 ROCm 运行时和 Docker 的 GPU 支持打通,让容器能访问宿主机的 /dev/kfd/dev/dri

装完之后验证一下设备节点:

Terminal window
ls -la /dev/kfd
ls -la /dev/dri/

期望看到 /dev/kfd 存在,/dev/dri/ 下有 card0(核显)、card1(RX 6600)、renderD128renderD129 等渲染节点。如果没有,说明系统没认出显卡 —— 先跑一遍 lspci | grep -i amd 排查。

模型选型

选了 Qwen3.5-9B 的 Q4_K_M 量化版。几个理由:

  • 显存刚好:Q4_K_M 大约 5.5GB,加上推理时的 KV Cache 开销,7.8GB 显存不会爆
  • 中英文都好:Qwen 系列对中文的支持在开源模型里是第一梯队,日常写作、翻译、问答都够用
  • 9B 这个尺寸很甜:比 7B 聪明一截,比 14B 省显存,个人部署的甜蜜点

模型文件从 HuggingFace 下载:

Terminal window
huggingface-cli download Qwen/Qwen3.5-9B-GGUF Qwen3.5-9B-Q4_K_M.gguf \
--local-dir /vol4/1000/ai-models/

如果没有装 huggingface-cli,直接 wget 也行:

Terminal window
wget -O /vol4/1000/ai-models/Qwen3.5-9B-Q4_K_M.gguf \
"https://huggingface.co/Qwen/Qwen3.5-9B-GGUF/resolve/main/Qwen3.5-9B-Q4_K_M.gguf"

部署步骤

项目结构

/vol4/1000/ai-models/
├── docker-compose.yml
├── models/
│ └── Qwen3.5-9B-Q4_K_M.gguf
└── logs/

Dockerfile

官方提供了预构建的 rocm/llama.cpp 镜像,省去自己编译 ROCm 版本的痛苦(说真的,从源码编译 ROCm 环境比部署本身还折腾)。直接用:

FROM rocm/llama.cpp:latest
EXPOSE 8000
ENTRYPOINT ["/llama-server"]
CMD ["-m", "/models/Qwen3.5-9B-Q4_K_M.gguf", \
"--host", "0.0.0.0", \
"--port", "8000", \
"-ngl", "99"]

-ngl 99 的意思是「把所有层都卸载到 GPU」。RX 6600 有 7.8GB 显存,Q4_K_M 模型大约 5.5GB,99 层全卸毫无压力。如果你的模型比显存大,需要调小这个数字,部分层留在 CPU 上跑。

docker-compose.yml

services:
llama-server:
build: .
image: llama-rocm:latest
container_name: llama-server
restart: always
ports:
- "11890:8000"
devices:
- /dev/kfd:/dev/kfd
- /dev/dri:/dev/dri
volumes:
- ./models:/models:ro
- ./logs:/logs
environment:
8 collapsed lines
- HSA_OVERRIDE_GFX_VERSION=10.3.0
cap_add:
- SYS_PTRACE
security_opt:
- seccomp=unconfined
group_add:
- video
- render

字段拆解:

  • ports: 11890
    — 宿主机 11890 映射容器 8000。选 11890 没别的原因,自己记着方便
  • devices/dev/kfd 是 ROCm 计算接口,/dev/dri 是渲染节点,两个缺一个 GPU 就不干活
  • volumes — 模型文件只读挂载(:ro),防止容器里误删;日志目录可读写
  • HSA_OVERRIDE_GFX_VERSION=10.3.0 — RX 6600 是 gfx1032,ROCm 官方支持列表里没有它,这行环境变量告诉 ROCm「把我当 gfx1030 处理」。没这行,llama.cpp 启动时会报找不到 GPU
  • cap_add + security_opt — 给容器足够的权限访问 GPU 硬件。SYS_PTRACE 是 ROCm 调试用的,seccomp=unconfined 放开系统调用限制
  • group_add: video, render — 让容器内的进程有权限访问 /dev/dri 设备

构建与启动

Terminal window
cd /vol4/1000/ai-models
docker compose build
docker compose up -d

第一次 build 会拉 rocm / llama.cpp 镜像(大概 3-4GB),后面就快了。启动后看日志确认 GPU 被识别:

Terminal window
docker compose logs -f

看到类似 ggml_cuda_init: found 1 ROCm devices 或者 llama_model_load: using ROCm 就说明 GPU 在工作。如果日志里出现 ggml_cuda_init: no ROCm devices found,回去检查 HSA_OVERRIDE_GFX_VERSION 和 devices 挂载。

验证

用 curl 测试一下 API 是否正常:

Terminal window
# 查模型列表
curl http://localhost:11890/v1/models
# 发一条对话请求
curl http://localhost:11890/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3.5-9b",
"messages": [{"role": "user", "content": "用一句话介绍你自己"}]
}'

模型名随便填,llama.cpp server 默认不校验。返回 JSON 带着 choices[0].message.content 就是通了。

性能实测

Qwen3.5-9B - Q4_K_M 在 RX 6600 上的表现:

指标速度
提示词处理 (prompt eval)~136 tokens/s
文本生成 (token generation)~32 tokens/s

32 tok / s 是什么概念?差不多人眼阅读速度的两倍。日常问答、写作辅助、翻译这种场景完全够用 —— 你发一段话过去,一两秒就开始流式出字,体感几乎没有等待。

对比一下纯 CPU 推理:同一台机器用 i9-9980HK 跑,生成速度大概 5-8 tok / s,GPU 加速后有 4-6 倍的提升。7.8GB 显存是瓶颈,但在这个尺寸下已经够用了。

接入前端

Page Assist

Page Assist 是个浏览器扩展,可以在任何网页上划词调用本地模型。安装后在设置里添加:

  • Provider: OpenAI API Compatible
  • API Base URL: http://192.168.3.166:11890/v1
  • Model: qwen3.5-9b
  • API Key: 随便填(llama.cpp 本地服务不校验)

配置完就能在浏览器里选中文字直接问本地大模型了,翻译、总结、改写都行。

Hermes Agent

~/.hermes/config.yamlcustom_providers 里加一段:

custom_providers:
- name: fnos-qwen3.5-9b
base_url: http://192.168.3.166:11890/v1
model: qwen3.5-9b

然后在 Hermes 里切换 Provider 到 fnos-qwen3.5-9b 就行了。这就是我现在在用的方案 ——Hermes 的对话模型跑在自己的 NUC 上,延迟低、不要钱、数据不离开局域网。

踩坑记录

坑一:容器里看不到 GPU

日志报 no ROCm devices found

排查:进容器跑 rocminfo,如果也报错,99% 是 devices 没挂对或者 HSA_OVERRIDE_GFX_VERSION 没设。确认 devices: /dev/kfd:/dev/kfd/dev/dri:/dev/dri 都在 compose 文件里,环境变量也别漏。

坑二:容器启动几秒后自动退出

docker compose ps 显示 Exited (1)

通常是模型路径不对。确认 volumes 挂载路径和 CMD 里的 -m 参数一致。进容器手动跑一次 /llama-server -m /models/xxx.gguf 看报什么错。

坑三:GPU OOM

模型太大,显存放不下。

两个办法:一是换更小的量化版本(Q3 甚至 Q2),二是调低 -ngl 参数,把部分层留在 CPU 内存里跑。比如 -ngl 50 就只卸载一半层。性能会降,但至少能跑。

坑四:重启后 /dev/kfd 权限不对

有时候系统重启后 /dev/kfd 的权限会变成 root:root,容器里的非 root 进程访问不了。

检查:

Terminal window
ls -la /dev/kfd
# 期望: crw-rw-rw- 1 root render

如果 group 不是 render,执行:

Terminal window
sudo chmod 666 /dev/kfd
sudo usermod -aG render,video $USER

做了这些之后,下次重启应该就正常了。