侧边栏壁纸
博主头像
龍騰博客 博主等级

行动起来,活在当下

  • 累计撰写 175 篇文章
  • 累计创建 31 个标签
  • 累计收到 7 条评论

目 录CONTENT

文章目录

本地大模型部署:从 qwythos 迁移到三模型手动切换方案

管理员
2026-08-20 / 0 评论 / 0 点赞 / 2 阅读 / 0 字

记录时间:2026年8月19日-20日 涉及范围:qwythos 迁移到 Ollama、新增 GLM-4.7-Flash 和 Qwen3-30B-A3B-Instruct-2507、内存 OOM 排查、最终三模型手动切换架构落地


一、背景与目标

硬件环境

  • 主机:fnOS-Ser8(Ryzen AI 处理器 + Radeon 780M 核显)

  • 实际可用内存:38GB(不是宣传的48GB,iGPU通过BIOS UMA划走约10GB,free -h 可实测确认)

  • GPU加速:Vulkan 后端,/dev/dri 直通

起始状态

  • llama.cpp 跑两个常驻模型:qwythos-9B(8g)+ Qwen3.6-35B-A3B-Uncensored(26g)

  • Ollama 独立运行,服务HA语音助手,8个模型(mistral-nemo、bge-m3、qwen2.5vl、llama3.1、deepseek-r1、qwen3:14b、gemma4、qwen2.5:14b)

  • Open WebUI 两套:一套接llama.cpp(8088),一套接Ollama(8081)

  • SearXNG(8091):被 Hermes + HermesII 两个Agent实例共用做联网搜索

最终目标

  • qwythos 迁移到 Ollama 管理(强迫症式的"llama.cpp只放Hermes在用的大模型,其他都归Ollama"分类逻辑)

  • llama.cpp 新增两个模型:GLM-4.7-Flash、Qwen3-30B-A3B-Instruct-2507,凑成三个大模型

  • 三个大模型手动切换,同一时刻只跑一个,避免内存超支


二、第一阶段:qwythos 迁移到 Ollama

2.1 关键认知:GGUF文件不能直接扔进Ollama目录

踩坑点:一开始以为把 gguf 文件拷贝到 /vol3/1000/docker/ollama/models 目录下就行,这是错的。Ollama 内部是 manifest + blob 存储结构,必须通过 ollama create -f Modelfile 建立索引,Ollama才认得这个模型。

2.2 正确的导入流程

第一步:给Ollama容器加只读挂载,指向llama.cpp的模型目录:

  ollama:
    volumes:
      - /vol3/1000/docker/ollama/models:/root/.ollama
      - /vol3/1000/docker/llama/models:/import-models:ro    # 新增
cd /vol3/1000/docker/ollama
docker compose up -d --force-recreate ollama

第二步:在宿主机上写Modelfile,再用docker cp复制进容器(踩坑:直接用 docker exec sh -c 'cat > ... << EOF' 这种heredoc方式在容器内经常静默失败,文件写不对或写不进去,改用宿主机写好再cp更可靠):

cat > /tmp/qwythos.Modelfile << 'EOF'
FROM /import-models/Qwythos-9B-Claude-Mythos-5-1M-Q4_K_M.gguf
PARAMETER temperature 0.6
PARAMETER top_p 0.95
PARAMETER top_k 20
PARAMETER repeat_penalty 1.05
PARAMETER num_ctx 32768
EOF
docker cp /tmp/qwythos.Modelfile ollama:/tmp/qwythos.Modelfile
docker exec -it ollama ollama create qwythos-9b -f /tmp/qwythos.Modelfile

第三步:验证导入成功

docker exec -it ollama ollama list
docker exec -it ollama ollama run qwythos-9b "你好,简单自我介绍一下"

2.3 确认无误后,删除llama.cpp那边的原始文件(满足"两边不重复"的洁癖需求)

docker stop llama-qwythos
docker rm llama-qwythos
rm -f /vol3/1000/docker/llama/models/Qwythos-9B-Claude-Mythos-5-1M-Q4_K_M.gguf

注意顺序:一定是"先导入确认能用 → 再删原文件",不要反过来,万一导入过程出问题(权限、模板解析等)还有原文件可以重试。

2.4 磁盘空间提醒

ollama create 会把模型内容完整拷贝进它自己的blob存储,不是引用原文件。如果两边都保留,会占用近乎双倍磁盘空间。及时清理不需要的那一份。

2.5 权限问题(本次未真正踩坑,但要留意)

llama.cpp目录下gguf文件权限是 -rwx------(属主 ubuntu:1001,group/other无权限)。经确认 Ollama容器内进程是root身份跑的docker exec ollama ps aux 验证),root能无视权限位读取,所以本次没有真正遇到permission denied。但如果换成非root运行的场景,需要 chmod o+r 补读权限。

2.6 查看/导出已导入模型的Modelfile

# 查看单个
docker exec -it ollama ollama show --modelfile qwythos-9b

# 批量导出成宿主机文本文件, 方便存档/纳入Git版本管理
mkdir -p /vol3/1000/docker/ollama/modelfiles-exported
for m in qwythos-9b qwen2.5:14b; do
  safe_name=$(echo "$m" | tr ':' '_')
  docker exec ollama ollama show --modelfile "$m" > "/vol3/1000/docker/ollama/modelfiles-exported/${safe_name}.Modelfile"
done

注意ollama show --modelfile 是Ollama现场根据内部manifest重新生成的展示内容,不是去读某个物理文件,跟你手写喂给 ollama create 的原始输入是两回事。


三、第二阶段:下载两个新模型

3.1 命令行工具改名坑

huggingface-cli 已废弃,会报错提示改用 hf

pip install huggingface_hub --break-system-packages   # 如果还没装
hf download unsloth/Qwen3-30B-A3B-Instruct-2507-GGUF \
  Qwen3-30B-A3B-Instruct-2507-Q4_K_M.gguf --local-dir .
hf download unsloth/GLM-4.7-Flash-GGUF \
  GLM-4.7-Flash-Q4_K_M.gguf --local-dir .

3.2 SSH断线导致下载中断的处理

hf download 默认是前台进程,SSH断开可能连带被杀。建议一律用 nohup 挂后台

nohup hf download unsloth/GLM-4.7-Flash-GGUF \
  GLM-4.7-Flash-Q4_K_M.gguf --local-dir . \
  > /tmp/glm_download.log 2>&1 &
tail -f /tmp/glm_download.log     # Ctrl+C退出不影响后台下载

若已经中断,重新执行原命令即可,hf download 支持断点续传,不会从头重来。用文件大小判断是否完整下载完毕(GLM-4.7-Flash Q4_K_M约18.3GB,Qwen3-30B-A3B-2507 Q4_K_M约18.6GB)。

3.3 模型选型简要说明

模型

架构

体量(Q4_K_M)

备注

Qwen3.6-35B-A3B-Uncensored(原有)

MoE, 3B激活

21.2GB

HauhauCS去审查微调版

Qwen3-30B-A3B-Instruct-2507(新增)

MoE, 3B激活

18.6GB

官方稳定版,256K原生上下文

GLM-4.7-Flash(新增)

MoE, 3.6B激活, MLA混合注意力

18.3GB

Z.ai出品,代码/Agent能力强,默认输出较长思维链

三者体量接近,MoE架构对iGPU友好(激活参数少,实际算力消耗低),是选型的核心原则——避免选稠密(Dense)模型,稠密模型全参数激活,同等文件大小下在弱算力iGPU上会明显更慢。


四、中间插曲:一次真实的OOM排查(重要,值得完整记录)

4.1 症状

Ollama报错:llama-server process has terminated: signal: killed

4.2 排查步骤(这套方法论可以复用到以后任何OOM场景)

第一步:确认是不是OOM杀的

dmesg | tail -50 | grep -i -E "kill|oom"
journalctl -k --since "10 minutes ago" | grep -i -E "out of memory|killed process"

看到 Out of memory: Killed process ... (llama-server) 字样即实锤。

第二步:核对内存基线

free -h

这一步发现了关键事实:总内存38GB,不是预想的48GB。之前所有内存预算的计算都要按这个下修。

第三步:排查是不是有容器一直空占内存没释放

docker ps    # 注意STATUS列, 特别留意 "unhealthy" 和运行时长异常长的容器

本次发现 llama-qwen36 已经跑了5小时且标记unhealthy,docker stop 它之后:

之前: Mem 38Gi total, used 28Gi, available 10Gi
之后: Mem 38Gi total, used 6.8Gi, available 32Gi

内存立刻释放出22GB,证实它是内存黑洞的直接原因。

4.3 "unhealthy"是假警报,真正的坑是healthcheck端口写错了

docker inspect llama-qwen36 | grep -A 20 "Healthcheck"

发现健康检查探测的是 http://localhost:8080/health,但模型实际监听端口是 8083LLAMA_ARG_PORT=8083)。健康检查每次都探测错误端口,连不上,所以一直被标记unhealthy——跟内存、模型本身毫无关系,是compose文件里健康检查配置没有跟着端口号一起改

查看容器日志 docker logs --tail 100 llama-qwen36 确认模型本身其实一直在正常处理请求(20-25 tok/s,符合预期),没有任何崩溃迹象。

教训:以后写healthcheck,一定要确认测试的端口跟 LLAMA_ARG_PORT 一致;最好在compose文件里直接写死为curl真实端口,不要用容器默认的8080。

4.4 真正的OOM根因:静态内存预留严重超支

三个大模型体量加总:

GLM-4.7-Flash: 18.3GB
Qwen3-30B-A3B-2507: 18.6GB
Qwen3.6-35B-A3B: 21.2GB
-------------------------
合计: 58GB

而实际可用内存只有38GB。如果三个模型都走"常驻并行",物理上放不下,这不是调优能解决的问题。

同理,qwythos(8g) + qwen36(26g) + Ollama(不设限时无限抢占) + 其他常驻服务(Frigate/Immich/CompreFace/Hermes/Syncthing基线约6-7GB) 叠加起来同样会超出38GB,这是当时那次OOM的系统性成因。

4.5 解决方案:从"常驻并行"改为"手动切换,同一时刻只跑一个"

这是本次架构调整的核心决策,详见下一节。


五、架构决策过程:llama-swap vs 手动切换

中间讨论过用 llama-swap(一个透明代理,能根据请求自动加载/卸载模型,做到"按需加载")来管理三个大模型,配置模板如下(供以后参考,最终没有采用):

# llama-swap/config.yaml 示例
models:
  "glm-4.7-flash":
    cmd: |
      /app/llama-server
      --model /models/GLM-4.7-Flash-Q4_K_M.gguf
      --port ${PORT}
      --host 0.0.0.0
      -ngl 99
      -c 65536
      --flash-attn
    ttl: 600
  # ... 其他模型同理

也讨论过一个进阶需求:"常驻一个模型,切换到另一个时自动退出,另一个空闲后自动切回常驻模型"——这个功能llama-swap官方目前不支持(查证GitHub Discussion #490,维护者明确表示"on_unload"钩子容易引发"swap风暴",还没有满意的设计方案)。

最终决策:抛弃自动化方案,改用最直接的手动切换——三个模型各自独立service,restart: "no"(不自动启动),你需要哪个就手动 docker compose up -d <服务名>,用完手动stop

理由:可预测性最高,不用担心自动化逻辑判断失误,符合"自己心里有数"的使用习惯,也彻底避免了内存计算的复杂性(永远只需要保证"最大的那个模型 + 其他常驻服务"不超过38GB即可)。


六、最终架构:完整配置文件

6.1 端口分配原则

  • qwen36 保持原有 8083 端口不变——因为Hermes/HermesII的配置文件(config.yamlprovider-model-catalog.json等)里已经硬编码引用了这个端口,改动会牵连一大片,能不动就不动

  • qwythos 原来的 8082 端口空出来后,给新增的 GLM-4.7-Flash 复用,不浪费

  • qwen3-25078084(保持之前的规划)

验证外部引用是否有硬编码端口的方法(避免改动遗漏):

grep -ril "8083\|8084\|8085" /vol3/1000/docker/hermes*/ 2>/dev/null | grep -v "/data/logs\|/data/home\|/data/lazy-packages\|/data/my-packages"

6.2 /vol3/1000/docker/llama/docker-compose.yml 完整内容

services:

  qwen36:
    image: ghcr.io/ggml-org/llama.cpp:server-vulkan
    container_name: llama-qwen36
    restart: "no"
    ports:
      - "8083:8083/tcp"
    volumes:
      - /vol3/1000/docker/llama/models:/models
      - /etc/localtime:/etc/localtime:ro
    devices:
      - /dev/dri:/dev/dri
    group_add:
      - video
    mem_limit: 26g
    memswap_limit: 26g
    environment:
      - TZ=Asia/Shanghai
      - LLAMA_ARG_MODEL=/models/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf
      - LLAMA_ARG_HOST=0.0.0.0
      - LLAMA_ARG_PORT=8083
      - LLAMA_ARG_N_GPU_LAYERS=99
      - LLAMA_ARG_CTX_SIZE=65536
      - LLAMA_ARG_FLASH_ATTN=1
      - LLAMA_ARG_TEMP=0.6
      - LLAMA_ARG_TOP_P=0.95
      - LLAMA_ARG_TOP_K=20
      - LLAMA_ARG_REPEAT_PENALTY=1.1
      - LLAMA_ARG_CACHE_TYPE_K=q8_0
      - LLAMA_ARG_CACHE_TYPE_V=q4_0
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8083/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  glm47:
    image: ghcr.io/ggml-org/llama.cpp:server-vulkan
    container_name: llama-glm47
    restart: "no"
    ports:
      - "8082:8082/tcp"
    volumes:
      - /vol3/1000/docker/llama/models:/models
      - /etc/localtime:/etc/localtime:ro
    devices:
      - /dev/dri:/dev/dri
    group_add:
      - video
    mem_limit: 24g
    memswap_limit: 24g
    environment:
      - TZ=Asia/Shanghai
      - LLAMA_ARG_MODEL=/models/GLM-4.7-Flash-Q4_K_M.gguf
      - LLAMA_ARG_HOST=0.0.0.0
      - LLAMA_ARG_PORT=8082
      - LLAMA_ARG_N_GPU_LAYERS=99
      - LLAMA_ARG_CTX_SIZE=65536
      - LLAMA_ARG_FLASH_ATTN=1
      - LLAMA_ARG_TEMP=0.6
      - LLAMA_ARG_TOP_P=0.95
      - LLAMA_ARG_TOP_K=20
      - LLAMA_ARG_REPEAT_PENALTY=1.05
      - LLAMA_ARG_CACHE_TYPE_K=q8_0
      - LLAMA_ARG_CACHE_TYPE_V=q4_0
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8082/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  qwen3-2507:
    image: ghcr.io/ggml-org/llama.cpp:server-vulkan
    container_name: llama-qwen3-2507
    restart: "no"
    ports:
      - "8084:8084/tcp"
    volumes:
      - /vol3/1000/docker/llama/models:/models
      - /etc/localtime:/etc/localtime:ro
    devices:
      - /dev/dri:/dev/dri
    group_add:
      - video
    mem_limit: 24g
    memswap_limit: 24g
    environment:
      - TZ=Asia/Shanghai
      - LLAMA_ARG_MODEL=/models/Qwen3-30B-A3B-Instruct-2507-Q4_K_M.gguf
      - LLAMA_ARG_HOST=0.0.0.0
      - LLAMA_ARG_PORT=8084
      - LLAMA_ARG_N_GPU_LAYERS=99
      - LLAMA_ARG_CTX_SIZE=65536
      - LLAMA_ARG_FLASH_ATTN=1
      - LLAMA_ARG_TEMP=0.6
      - LLAMA_ARG_TOP_P=0.95
      - LLAMA_ARG_TOP_K=20
      - LLAMA_ARG_REPEAT_PENALTY=1.05
      - LLAMA_ARG_CACHE_TYPE_K=q8_0
      - LLAMA_ARG_CACHE_TYPE_V=q4_0
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8084/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  searxng:
    image: searxng/searxng:latest
    container_name: searxng
    restart: unless-stopped
    ports:
      - "8091:8080/tcp"
    volumes:
      - /vol3/1000/docker/llama/searxng:/etc/searxng
    mem_limit: 1g
    memswap_limit: 1g
    environment:
      - TZ=Asia/Shanghai

networks:
  default:
    driver: bridge

注意:这份配置里不再包含 open-webui(llama.cpp那套的,8088端口),因为已改用 MacBook Pro 上的 WorkBuddy + Obsidian 作为客户端,该实例已删除。searxng 保留,因为Hermes/HermesII两个Agent实例都在用它做联网搜索(见下节说明)。

6.3 /vol3/1000/docker/ollama/docker-compose.yml 完整内容

services:
  ollama:
    image: ollama/ollama:latest
    container_name: ollama
    restart: always
    ports:
      - "11434:11434"
    volumes:
      - /vol3/1000/docker/ollama/models:/root/.ollama
    devices:
      - /dev/dri
    mem_limit: 16g
    memswap_limit: 16g
    environment:
      - OLLAMA_VULKAN=1
      - OLLAMA_IGPU_ENABLE=1
      - OLLAMA_CONTEXT_LENGTH=65536
      - OLLAMA_KEEP_ALIVE=5m
      - OLLAMA_MAX_LOADED_MODELS=1
    group_add:
      - "44"
      - "105"

  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    container_name: open-webui
    restart: always
    ports:
      - "8081:8080"
    volumes:
      - /vol3/1000/docker/ollama/webui:/app/backend/data
    environment:
      - OLLAMA_BASE_URL=http://ollama:11434
    depends_on:
      - ollama

关键新增:

  • mem_limit: 16g + memswap_limit: 16g:给Ollama设内存硬上限,防止它跟其他服务无限制抢内存(这是修复OOM的关键一环)

  • OLLAMA_MAX_LOADED_MODELS=1:强制Ollama一次只保留一个模型在内存里,不会叠加加载多个模型导致内存超支

  • 导入qwythos用的 /import-models:ro 只读挂载,导入完成、原文件删除后已撤掉,保持配置干净

6.4 便捷切换脚本

cat > /vol3/1000/docker/llama/switch-model.sh << 'EOF'
#!/bin/bash
# 用法: ./switch-model.sh qwen36 | qwen3-2507 | glm47
if [ -z "$1" ]; then
  echo "用法: $0 [qwen36|qwen3-2507|glm47]"
  exit 1
fi
cd /vol3/1000/docker/llama
echo "停止所有大模型..."
docker compose stop qwen36 qwen3-2507 glm47
echo "启动 $1 ..."
docker compose up -d "$1"
echo "完成, 当前状态:"
docker ps --filter "name=llama-" --format "table {{.Names}}\t{{.Status}}"
EOF
chmod +x /vol3/1000/docker/llama/switch-model.sh

日常切换只需要一行:

./switch-model.sh glm47

七、清理Open WebUI时的关键教训:删东西前先grep确认依赖关系

背景:以为SearXNG只是给Open WebUI用的联网搜索后端,Open WebUI都不用了(改用WorkBuddy+Obsidian),一度想把SearXNG也一起删掉。

正确做法:先搜索确认有没有别的服务也在依赖它,再动手删:

grep -ri "searxng\|8091" /vol3/1000/docker/hermes*/. 2>/dev/null

结果发现 hermeshermesII 两个Agent实例的日志里持续到当天都有 Plugin 'web-searxng' registered web provider: searxng 的记录——SearXNG其实是Hermes在用的公共基础设施,跟Open WebUI是完全独立的两条使用路径。如果直接删掉,会连带砍断Hermes的联网搜索能力。

教训:任何"看起来不用了"的服务,删除前务必 grep 一下其他容器的配置和日志目录,确认没有别的地方在依赖它,避免"拔线拔错了"的连锁故障。

最终结论:只删 open-webui-llamacpp(8088端口),searxng(8091)保留。


八、容器名冲突的通用处理套路

本次过程中反复遇到 Error response from daemon: Conflict. The container name "/xxx" is already in use 报错(改compose文件后重新up时),处理套路固定:

# 1. 先看这个残留容器是什么状态
docker ps -a --filter "name=容器名"

# 2a. 如果是 Exited(已停止)—— 直接删掉重建
docker rm 容器名
docker compose up -d 服务名

# 2b. 如果是 Up(正在运行)—— 用force-recreate让compose接管
docker compose up -d --force-recreate 服务名

九、最终验证结果(三个模型实测数据)

启动、验证的标准流程(每个模型都跑一遍):

./switch-model.sh <模型名>
docker ps --filter "name=llama-"                    # 确认healthy
curl http://localhost:<端口>/health                  # 确认 {"status":"ok"}
curl http://localhost:<端口>/v1/chat/completions \    # 确认真实对话正常
  -H "Content-Type: application/json" \
  -d '{"model":"<模型标识>","messages":[{"role":"user","content":"你好,简单介绍一下你自己"}]}'

实测速度对比

模型

端口

生成速度(tok/s)

备注

Qwen3-30B-A3B-Instruct-2507

8084

32.31

最快,回复简洁,无冗余思维链

GLM-4.7-Flash

8082

23.51

Vulkan对MLA混合注意力架构支持良好,符合预期速度,但默认输出大段思维链(reasoning_content),实测149字回复配了1166 token的思考过程,耗时49.6秒

Qwen3.6-35B-A3B-Uncensored(qwen36)

8083

~20-25

基准值,原有模型

关键结论

  1. GLM-4.7-Flash 的 MLA 新架构在 Vulkan 后端完全兼容,之前担心的"新算子支持不完善导致变慢"的顾虑没有应验,速度甚至优于基准

  2. GLM-4.7-Flash 默认思维链过长,如果后续接入Hermes做日常对话,需要想办法在请求参数里抑制/精简reasoning输出,否则每次响应都会因为思考过程拖慢、且消耗更多token——这是一个待办事项,还没解决

  3. Qwen3-30B-A3B-Instruct-2507 是三个模型里综合表现最好的(速度快、无冗余输出、官方对齐稳定),适合作为"求快求稳"场景的首选


十、待办事项 / 后续优化方向

  1. GLM-4.7-Flash 思维链精简:需要研究 llama-server 是否支持类似 reasoning_effort 的参数,或者在应用层(Hermes接入时)过滤掉 reasoning_content 字段,避免拖慢响应

  2. Hugging Face MCP工具已装好hf_whoamihub_repo_searchhub_repo_detailshf_fs),后续找模型/查GGUF量化版本可以直接让Hermes调用,减少手动web搜索确认的环节

  3. 官方原版 Qwen3.6-35B-A3B 是否引入:社区推荐过,是当前 qwen36(HauhauCS去审查微调版)的官方基座原版。功能上会跟 qwen3-2507(同样"官方+受限对齐"定位)有一定重叠,暂未决定是否要替换现有qwen36这个位置,取决于对"去审查"这个特性的实际需求权衡

  4. 内存监控:建议以后定期跑一次 free -h 检查,尤其是长时间挂机后,防止类似qwen36曾经因healthcheck误判长期占用不释放的情况再次发生(虽然这次是误报,但监控习惯要养成)


十一、本次经验总结(给未来自己的备忘)

  1. 算内存预算前先确认真实可用总量,不要用宣传值(本机是38GB不是48GB),iGPU统一内存架构会占走一部分

  2. 多个大模型不可能无脑常驻并行,必须先算总账(各模型体量加总 vs 实际可用内存),放不下就老老实实做按需切换/手动切换,没有配置技巧能绕开物理限制

  3. healthcheck配置的端口必须跟服务实际监听端口一致,端口写错只会导致误报"unhealthy",具有很强的迷惑性,容易被误判为模型本身有问题

  4. OOM排查标准流程dmesg/journalctl 确认是否OOM → free -h 看真实内存状况 → docker ps 看有没有异常占用不释放的容器 → 找到根因后针对性加 mem_limit/MAX_LOADED_MODELS 这类硬限制

  5. 删除任何"看起来没用"的公共服务前,先 grep 搜一遍其他容器的配置和日志,确认没有别的地方在依赖,避免连锁故障

  6. Ollama的模型导入必须走 ollama create -f Modelfile 流程,不能指望把gguf文件直接扔进目录就能用

  7. hf 命令替代了废弃的 huggingface-cli,长时间下载任务务必配合 nohup 防止SSH断线中断

0

评论区