记录时间: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 模型选型简要说明
三者体量接近,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,但模型实际监听端口是 8083(LLAMA_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.yaml、provider-model-catalog.json等)里已经硬编码引用了这个端口,改动会牵连一大片,能不动就不动qwythos原来的 8082 端口空出来后,给新增的 GLM-4.7-Flash 复用,不浪费qwen3-2507用 8084(保持之前的规划)
验证外部引用是否有硬编码端口的方法(避免改动遗漏):
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
结果发现 hermes 和 hermesII 两个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":"你好,简单介绍一下你自己"}]}'
实测速度对比
关键结论
GLM-4.7-Flash 的 MLA 新架构在 Vulkan 后端完全兼容,之前担心的"新算子支持不完善导致变慢"的顾虑没有应验,速度甚至优于基准
GLM-4.7-Flash 默认思维链过长,如果后续接入Hermes做日常对话,需要想办法在请求参数里抑制/精简reasoning输出,否则每次响应都会因为思考过程拖慢、且消耗更多token——这是一个待办事项,还没解决
Qwen3-30B-A3B-Instruct-2507 是三个模型里综合表现最好的(速度快、无冗余输出、官方对齐稳定),适合作为"求快求稳"场景的首选
十、待办事项 / 后续优化方向
GLM-4.7-Flash 思维链精简:需要研究 llama-server 是否支持类似
reasoning_effort的参数,或者在应用层(Hermes接入时)过滤掉reasoning_content字段,避免拖慢响应Hugging Face MCP工具已装好(
hf_whoami、hub_repo_search、hub_repo_details、hf_fs),后续找模型/查GGUF量化版本可以直接让Hermes调用,减少手动web搜索确认的环节官方原版 Qwen3.6-35B-A3B 是否引入:社区推荐过,是当前
qwen36(HauhauCS去审查微调版)的官方基座原版。功能上会跟qwen3-2507(同样"官方+受限对齐"定位)有一定重叠,暂未决定是否要替换现有qwen36这个位置,取决于对"去审查"这个特性的实际需求权衡内存监控:建议以后定期跑一次
free -h检查,尤其是长时间挂机后,防止类似qwen36曾经因healthcheck误判长期占用不释放的情况再次发生(虽然这次是误报,但监控习惯要养成)
十一、本次经验总结(给未来自己的备忘)
算内存预算前先确认真实可用总量,不要用宣传值(本机是38GB不是48GB),iGPU统一内存架构会占走一部分
多个大模型不可能无脑常驻并行,必须先算总账(各模型体量加总 vs 实际可用内存),放不下就老老实实做按需切换/手动切换,没有配置技巧能绕开物理限制
healthcheck配置的端口必须跟服务实际监听端口一致,端口写错只会导致误报"unhealthy",具有很强的迷惑性,容易被误判为模型本身有问题
OOM排查标准流程:
dmesg/journalctl确认是否OOM →free -h看真实内存状况 →docker ps看有没有异常占用不释放的容器 → 找到根因后针对性加mem_limit/MAX_LOADED_MODELS这类硬限制删除任何"看起来没用"的公共服务前,先
grep搜一遍其他容器的配置和日志,确认没有别的地方在依赖,避免连锁故障Ollama的模型导入必须走
ollama create -f Modelfile流程,不能指望把gguf文件直接扔进目录就能用hf命令替代了废弃的huggingface-cli,长时间下载任务务必配合nohup防止SSH断线中断
评论区