8GB 显卡本地跑通千问 3.8-27B:llama.cpp Vulkan 踩坑与实测

8GB 显存的 AMD 显卡,能不能本地跑一个 27B 参数的大语言模型?
答案是:能,但只有 1.5 字/秒。这篇记录我踩过的每一个坑,以及一组证明”这个速度不是没调好”的对照实测。

📌 本文分两部:第一部(前半)记录跑通官方版的全过程;第二部(「限制从哪来」起)记录发现限制在权重层、换去审查版并横评 1bit→Q4 全家桶的后续实测。官方版已在第二部末尾删除,现役为去审查版。


📖 前言

Qwen3.8-27B 是 2026 年 8 月发布的 27B 稠密模型,Apache 2.0、原生多模态、256K 上下文,官方还直接出了 GGUF。听起来很适合本地部署——直到你看到体积:Q4_K_M 量化后仍有 17.7GB

而我手里只有一张 AMD RX 6650 XT(8GB 显存),在 Windows 上还吃不到 ROCm。这条路上我踩了三个坑(下载断线、Git Bash curl 写文件报错、速度莫名只有 1 字/秒),最后靠 llama.cpp Vulkan 版 + 自动分层跑通,并做了一组五轮对照实测来回答那个最折磨人的问题:到底是我没调好,还是这台机器就只能这么快?

答案是后者。这篇文章把结论和证据都摆出来,省得你再花几小时重复我的调参。


🎯 最终方案

项目 方案
运行框架 llama.cpp Vulkan 预编译版(b10760)
模型形态 Qwen3.8-27B Q4_K_M GGUF(17.7GB)
GPU 方案 Vulkan 后端(无需 ROCm)
显存分配 自动分层(不写 -ngl,按空闲显存自动拟合)
实测速度 生成 1.5~1.6 字/秒,读入 5~6 字/秒

核心逻辑:模型远大于显存,绝大多数层只能走内存。既然如此,就不要手动指定塞几层进显卡——交给 llama.cpp 自动拟合,既不会超售,也不用反复试参数。


🧱 方案选型:为什么是 Q4_K_M + Vulkan

为什么选 Qwen3.8-27B

特性 对本地部署的意义
混合注意力:48 层 Gated DeltaNet(线性)+ 16 层 Gated Attention KV 缓存省约 75%,16K 上下文几乎不占显存
原生多模态 不用外挂 CLIP 也能走图文
256K 上下文 理论上能吃长文档(但本机读入速度下不实用,见实测节)
Apache 2.0 商用无顾虑
官方出 GGUF(ggml-org 仓库) 不用自己转换权重,开箱即用

量化档位:8GB 卡只有 Q4 这一个活口

量化 体积 8GB 卡可行性
BF16 / FP8 55GB+ ❌ 想都别想
Q8_0 ~30GB
Q4_K_M 17.7GB ✅ 能跑(大部分在内存)
IQ4_XS / Q3_K_XL 13~14GB ⚠️ 能跑,但提速有限(见”想更快怎么办”)

💡 同系列没有”轻量款”Qwen3.8-Flash-Next 听着像小的,实际是 180GB 的巨型 MoE(Q4_K_XL 分片加起来 111GB),比 27B 还难跑。别被名字骗了。

推理后端:Vulkan 是 Windows + AMD 的最优解

路线 现状
ROCm / HIP 编译版 Windows 上官方支持长期缺位,编译折磨
Vulkan 预编译版 下载解压即用
纯 CPU 最稳,但没有硬件加速

验证命令:

1
2
llama-server.exe --list-devices
# Vulkan0: AMD Radeon RX 6650 XT (8176 MiB)

看到显卡型号的那一刻基本就成了。


📦 文件清单

⚠️ 更新(见本文第二部):以下为第一部跑通官方版时的记录,官方版权重后来已在横评后删除。现役文件为去审查版 Heretic Q4_K_M(16.55GB)+ RVN-IQ3_XXS(11.19GB)+ mmproj 视觉投影(629MB),最终清单见第二部「最终配置速查」。

文件 位置 大小 说明
Qwen3.8-27B-Q4_K_M.gguf models/ 17.7GB 主模型(ggml-org 仓库,hf-mirror 下载)
llama-server.exe llama.cpp/ 约 200MB Vulkan 预编译包
python_resume_download.py models/ 断点续传下载器(本文最可复用的一段)

启动参数(写成 bat 一键启动):

1
2
3
4
5
6
7
8
llama-server.exe ^
-m "models\Qwen3.8-27B-Q4_K_M.gguf" ^
-a "Qwen3.8-27B" ^
--ctx-size 16384 ^
--flash-attn on ^
--jinja ^
--chat-template-kwargs "{\"reasoning_effort\": \"medium\"}" ^
--host 127.0.0.1
  • -a 是模型别名。不加的话 API 和客户端里显示的是一长串文件路径,加上就是干净的 Qwen3.8-27B
  • 故意不写 -ngl,让 llama.cpp 自动按空闲显存分层——这是本文最重要的一个配置决定,原因见后面 1 字/秒那节。

📥 下载关:17.7GB 怎么不断线

这一关花的时间比装模型还多,三个拦路虎:

拦路虎一:GitHub 下不动 → 换镜像

Release 包直连失败(schannel 报错)。改用 GitHub 加速镜像即可:

1
2
curl -L -C - -o llama.zip "https://ghfast.top/https://github.com/ggml-org/llama.cpp/releases/download/b10760/llama-b10760-bin-win-vulkan-x64.zip"
# 备选:https://gh-proxy.com/https://github.com/...

拦路虎二:hf-mirror 长连接被掐断

模型源站连不上,走镜像站。但大文件下载到几百 MB 就会被掐断(连接被对端关闭),必须断点续传 + 失败重试。

先拿到真实文件大小,免得传完了不知道:

1
2
3
curl -s "https://hf-mirror.com/api/models/ggml-org/Qwen3.8-27B-GGUF/tree/main" | \
python -c "import json,sys;[print(f['path'], f['size']) for f in json.load(sys.stdin) if f['path'].endswith('.gguf')]"
# Qwen3.8-27B-Q4_K_M.gguf 18973870432 ← 记下这个数

拦路虎三:MSYS 的 curl 写文件报错 23(真坑)

套上续传循环后,每次都在写入时失败:

1
curl: (23) client returned ERROR on write of 16384 bytes

排查过:磁盘空间充足(30GB+)、文件没被占用(用 Python 往同一文件写字节能成功)。结论是 Git Bash 自带 curl 在特定写入场景下的毛病,换实现比继续查快。

最终方案:Python urllib 断点续传脚本(改成自己的 URL/路径/大小就能直接用):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import os, sys, time, urllib.request

URL = "https://hf-mirror.com/ggml-org/Qwen3.8-27B-GGUF/resolve/main/Qwen3.8-27B-Q4_K_M.gguf"
PATH = r"D:\AI-LLM\models\Qwen3.8-27B-Q4_K_M.gguf"
TARGET = 18973870432 # 上一步查到的真实大小
CHUNK = 1 << 22 # 4MB

attempt = 0
while True:
sz = os.path.getsize(PATH)
if sz >= TARGET:
print(f"COMPLETE {sz}", flush=True); break
attempt += 1
print(f"attempt {attempt}: resume at {sz} ({100*sz/TARGET:.1f}%)", flush=True)
try:
req = urllib.request.Request(URL, headers={"Range": f"bytes={sz}-"})
r = urllib.request.urlopen(req, timeout=60)
code = r.getcode()
# 服务器忽略 Range 返回 200 时,绝不写入,否则前面全白下
if code == 200 and sz > 0:
print(" server ignored Range (200); abort to avoid overwrite", flush=True)
sys.exit(2)
with open(PATH, "ab") as f:
t0, got = time.time(), 0
while True:
chunk = r.read(CHUNK)
if not chunk: break
f.write(chunk); got += len(chunk)
if got % (1 << 26) < CHUNK: # 每 ~64MB 报一次
speed = got / max(time.time() - t0, 0.1) / 1e6
print(f" {os.path.getsize(PATH)} ({100*os.path.getsize(PATH)/TARGET:.1f}%) {speed:.1f} MB/s", flush=True)
except Exception as e:
print(f" error: {e}", flush=True); time.sleep(3)
time.sleep(1)

两个关键点:

  • Range 请求 + 追加写(ab:断线后用已下载字节数做偏移,从头循环。
  • 拒绝 200 响应:服务器若忽略 Range 从头返回,追加写会把文件撑成两倍大的垃圾。直接退出让人处理,比静默损坏强

下完记得校验大小和文件头:

1
2
3
with open(PATH, "rb") as f:
print(f.read(4)) # b'GGUF'
print(os.path.getsize(PATH)) # 18973870432

💡 附带一个 Windows 小坑:用 Windows 版 Python 跑脚本时,路径必须写成 D:/xxx/yyy.py 风格。Git Bash 的 /d/xxx/yyy.py 会被解析成 C:\d\xxx\yyy.py,然后报找不到文件。


🐛 第一个真坑:1 字/秒,显存超售的锅

模型加载正常、能出字,但速度只有 1.00 字/秒。日志里有一行关键警告:

1
2
W common_fit_params: failed to fit params to free device memory:
n_gpu_layers already set by user to 20, abort

根因:我手动写死了 --n-gpu-layers 20,而此时显存被另一个常驻程序占着(只剩 3.2GB 可用),20 层的权重塞不下。

Windows 的 WDDM 驱动模型这时候不会报 OOM,而是静默把 GPU 显存溢出到”共享 GPU 内存”——也就是走 PCIe 去系统内存搬。这比直接把层放 CPU 上还慢得多,于是掉到 1.0 字/秒。

⚠️ 这个坑的阴险之处:它不报错、不崩溃,只是慢。你只会以为是”27B 本来就慢”,然后开始怀疑人生。

修法:别手写 -ngl,让 llama.cpp 自动拟合。 它启动时按当前真实空闲显存算能放几层,无论别的程序占没占显存都不会超售。


📊 实测:1.5 字/秒是物理极限,不是没调好

做了五组对照(每次都用全新 prompt,排除缓存干扰):

配置 生成速度 读入速度
手动 -ngl 20/22(显存被占用时) 1.00 字/秒 3~6 字/秒
自动分层(不写 -ngl 1.59 字/秒 5.6 字/秒
纯 CPU(-ngl 0 1.49 字/秒 4.9 字/秒
纯 CPU + 12 线程(-t 12 1.49 字/秒 6.2 字/秒
重启后干净环境(显存全空) 1.58 字/秒 5.8 字/秒

三个反直觉的结论:

① GPU 分层只带来 7% 提升。 按 Amdahl 定律,8GB 显存能放约 40% 的层,理论上限应该接近 1.6 倍,实际只有 1.07 倍。差额来自 Vulkan 在 RDNA2 上的 kernel 效率、跨设备提交开销,以及自动拟合本身很保守。

② 线程数对生成速度零影响。 12 线程只让读入快了 27%,生成纹丝不动——生成是带宽瓶颈,不是算力瓶颈

③ 关掉其他程序腾出全部显存也没用。 1.58 = 1.59,说明瓶颈根本不在显存。

算一笔账,验证是不是带宽

生成一个 token 要把 17.7GB 权重完整读一遍。本机内存有效带宽约 25~30 GB/s:

1
17.7 GB ÷ 26 GB/s ≈ 0.68 秒/字  →  约 1.5 字/秒

和实测的 1.49~1.59 完全吻合。所以这个数字不是”没调好”,是内存带宽写死的。

💡 顺带一提:上下文设太大(比如 256K)对生成速度没影响(混合注意力的 KV 很小),但读入会慢到不可用——长文档先想清楚再喂。


💡 想更快怎么办

27B 在 8GB 卡上没有提速空间,唯一出路是换更小、能整进显存的模型

方案 体积 预计速度 代价
Qwen3-8B Q4_K_M ~5GB(可全进显存) 15~25 字/秒 智力明显下降
Qwen3-14B Q4_K_M ~9GB(约七成进显存) 4~6 字/秒 智力略降
继续优化 27B 参数 上限 1.6 字/秒 无用功

一句话:交互聊天用小模型,长文/批量用 27B。27B 的正确用法是”挂后台丢任务,去干别的”,不是一问一答。


🛠️ 实用小贴士

  • 思考档位:Qwen3.8 默认 reasoning_effort=xhigh,会思考十几分钟。日常用 medium,急用 low,或者直接调 API 传参覆盖。
  • 省时间:连续提问保持同一会话,prompt 缓存命中后读入快 3~5 倍(实测 45 token 命中缓存只需 1.1 秒,冷启动要 8 秒)。
  • 冷启动:首次从磁盘读 17.7GB 约 12 分钟,第二次有系统缓存约 1030 秒。
  • 输出乱码:Vulkan 下混合注意力批处理有个已知 bug,加 -ub 256-ub 1024 规避(避开默认 512)。
  • 接入其他客户端:服务提供 OpenAI 兼容接口,Chatbox / NextChat 之类填 http://127.0.0.1:<端口>/v1、模型名 Qwen3.8-27B 即可。
  • 界面语言:llama.cpp 自带网页界面是英文且打包在 exe 里改不了。可以用 --path <目录> 指定自己的静态页面来替换。

✅ 排错清单(照着查)

现象 原因 处理
日志出现 failed to fit params ... n_gpu_layers already set 手写 -ngl 导致显存超售 删掉 -ngl,让程序自动拟合
速度 1 字/秒 同上(WDDM 共享内存溢出) 同上
下载中断、curl 报 error 23 Git Bash curl 写入问题 换 Python urllib 续传脚本
服务器忽略 Range 返回 200 镜像站问题 脚本里 sys.exit(2) 拒绝写入,别硬写
Windows Python 报找不到脚本文件 用了 /d/xxx 路径 改成 D:/xxx
生成慢到不可用 27B 在 8GB 卡的物理极限 换小模型,别再调参
回答前思考十几分钟 默认 xhigh 档位 low / medium
输出乱码 Vulkan 批处理 bug -ub 256-ub 1024

🔄 第二部:限制从哪来,去审查版怎么选,1bit→Q4 横评

跑通只是第一步。接着自然遇到一个灵魂问题:都本地部署了,为什么它还各种拒绝?
这一部给结论:限制在权重里不在配置里;换社区去审查(abliterated)权重是唯一出路;然后用一次五模型横评决定留谁删谁。

🔍 限制在权重层,不在配置层

先怀疑配置:很多部署会在 system prompt 或模板里注入限制,那种情况改提示词就能解。所以我 dump 了官方 GGUF 元数据验证:

1
2
3
GGUF v3  tensors=851  metadata_kv=39
general.architecture = qwen35 ← 没有 chat_template 键
general.name = Qwen3.8-27B ← 没有 system prompt 键

39 个键里既没有 system prompt 也没有 chat_template——llama.cpp 用的是内置模板。结论干净利落:

限制 100% 在权重里。拒绝倾向是训练阶段用 SFT + RLHF 直接写进参数的,GGUF 量化只压缩数字精度不改变模型学过什么。中文大厂模型的合规对齐尤其彻底,权重在哪,限制就在哪——这就是为什么改 system prompt 无效,唯一出路是换权重。

💡 不是所有”拒绝”都是审查:模型真不会时会拒答、推理模型天生保守、多模态还有一层图像护栏。这些换任何版本都解决不了。

📥 去审查模型怎么选

社区成品 GGUF(原模型 Apache 2.0,改版无许可问题):

仓库 量化 下载量
huihui-ai/Huihui-Qwen3.8-27B-abliterated-GGUF Q4_K_L 等 187 万
JonathanColetti/Qwen3.8-27B-Uncensored-GGUF Q4_K_M 等 214 万
0bserverx/...Heretic-Abliterated-Uncensored-GGUF Q4_K_M ~ IQ1_S 全档 121 万
orcarouter/Qwen3.8-27B-Uncensored-GGUF Q4_K_M 等 25 万

我选 0bserverx 的 Heretic 系——同一仓库把 Q4_K_M / Q3 / IQ3_XXS / IQ2 / IQ1_S 全档配齐,还带视觉投影(mmproj)与 MTP 融合版,方便一次横评。

两个必须搞清楚的概念:

  • 视觉与量化解耦:GGUF 只含文本权重,”看图”靠独立视觉投影(mmproj,约 600MB)。同一份 mmproj 配任何量化档都能看图——差别只在”看得清不清楚”。
  • MTP 头是独立零件:普通 GGUF 把 MTP 子模型剥掉了。要用 MTP 投机解码得下 MTP 焊在里面的融合版(文件名带 -mtp);外挂独立模块当前 llama.cpp 不支持。

⚖️ 全家桶横评(官方 / Q4 / 3bit / 1bit / MTP)

同题短文(”一只猫决定去海边旅行”200 字)+ 计数测速,五模型轮番上阵:

模型 体积 速度 同题短文质量
官方版 Q4_K_M 17.7GB 1.64 字/秒 好,但有审查
Heretic Q4_K_M 16.55GB 1.85 字/秒 好,无审查,可看图
Heretic IQ3_XXS(3bit) 11.19GB 2.26 字/秒 不错,与 Q4 差距很小
Heretic IQ1_S(1bit) 7.15GB 5.01 字/秒 明显劣化:干瘪浅白
Heretic IQ3_XXS + MTP 11.64GB 2.28 字/秒 与普通 IQ3 相同

IQ1_S 的 5 字/秒不是白来的——7.15GB 能塞进 8GB 显存大半。但代价肉眼可见:它写”它准备了猫粮、鱼罐头和一把小伞……不再抱怨猫粮的烦恼”;Q4 写”海浪的气味从某个缝隙钻进来,细碎的,咸的,带着一种它从未闻过的辽阔”。1bit 只适合”大概能聊”,不适合讲究文字的用途。IQ3_XXS 是性价比惊喜:小 5.4GB、快 20%,质量几乎追平 Q4。

最终保留:主力 Heretic Q4_K_M + mmproj(质量/视觉/去审查的最佳平衡);备用 Heretic IQ3_XXS + mmproj(快档)。官方版(有审查还更大)、IQ1_S(质量掉档)、MTP 融合版(实测无价值)删除,腾出 36GB。

⚡ MTP 实测:激活成功,但这台机器用不起

llama.cpp 较新构建已支持 Qwen3.8(qwen35)架构的 MTP(qwen35.cpp 里 2026 年 5-6 月就合入了相关代码),启用参数 --spec-type draft-mtp --spec-draft-n-max 3,前提是模型含 MTP 头。启动日志出现 creating MTP draft context against the target model 即激活成功。

MTP 原理是”一次前向多验几个字”,在带宽受限场景本该尤其有效。实测打脸:

配置 计数任务 中文叙述
无 MTP 基线 2.23 字/秒 ~2.2 字/秒
MTP n-max=3 2.15(更慢) 1.31(明显慢)
MTP n-max=2 2.38(+7%) 1.62(仍慢)

这台机器连主模型一次前向都靠内存带宽硬扛,MTP 草稿头与验证流程的额外内存往返抵消了省下的前向次数。MTP 是显存充裕机器的 1.5~2 倍利器,在 8GB 带宽瓶颈机上是负优化。

🛠️ 删除与”假删除”坑

横评后删除 36GB 模型,文件没了,磁盘可用空间纹丝不动。排查发现是脚本环境的 rm 被安全包装——删除实际是移进 Windows 回收站而不是真删。回收站里躺着那三个大模型,确认无其他文件后精确清除才释放空间。

💡 通用教训:在封装过的终端环境删大文件后空间没释放,先查回收站($Recycle.Bin)。删大模型文件后习惯性 df 验证一下,无声无息的”假删除”最坑。

✅ 最终配置速查(现役)

项目 内容
引擎 llama.cpp Vulkan b10760(无需 ROCm)
主力 Heretic Q4_K_M(16.55GB)+ mmproj 视觉
备用 Heretic IQ3_XXS(11.19GB)+ mmproj 视觉
启动参数 不写 -ngl 自动分层;-a 起别名;--mmproj 挂视觉
实测速度 Q4 ≈ 1.9 字/秒;IQ3 ≈ 2.3 字/秒
MTP 引擎支持但本机负优化,不启用

两个模型共用一份 mmproj,同一时刻跑一个。API 模型名 Qwen3.8-27B-Heretic-Q4_K_M / Qwen3.8-27B-Heretic-IQ3_XXS


📝 写在最后

整个过程最大的体会:本地跑大模型,瓶颈往往不在显存,而在内存带宽;而模型的价值观对齐是出厂焊死的,本地化部署只保证”没人偷看你的对话”,不保证”它什么都肯说”。显卡能帮你一点点(7%),权重里的拒绝倾向则只能靠社区去审查版解决。

如果你也是 8GB 显卡想跑 27B,记住这几句话:

  1. 别手写 -ngl:自动拟合既省事又不会踩 WDDM 静默溢出的坑
  2. 速度上限不是没调好:算一笔带宽账就明白了,别再调参
  3. 限制在权重里,改提示词没用:想放开就换 abliterated / uncensored 权重
  4. 1bit 不是速度神器:5 字/秒很诱人,但文字水平掉到没法看;3bit 才是性价比甜点
  5. MTP 是富人的玩具:带宽瓶颈的 8GB 卡上用不起,显存自由的机器才值得开
  6. 删模型记得验证空间真释放了:回收站会骗你

祝你也跑通 🎉

AMD 显卡跑通 MiniMax-H3 视频全家桶:ZLUDA + GGUF Q4_K 完整实录

8GB 显存的 AMD 显卡,能不能跑一个原始体积 41GB 的文生视频/图生视频/音视频联合生成模型?
答案是:能,而且我已经跑通了。这篇记录我踩过的每一个坑。


📖 前言

MiniMax-H3 是支持文本生成视频(t2va)、参考图生成视频(ref2va)、首帧生成视频(fl2va)三合一的音视频联合生成模型,权重高达 41GB。正常来说这是 24GB 显存显卡的玩具。

但我手里只有一张 AMD RX 6650 XT(8GB 显存),还吃不到 CUDA 生态——只能靠 ZLUDA 翻译层把 CUDA 调用翻译成 AMD 的 HIP。整个过程中我换了两次技术方案、打了 9 个补丁、排了 14 轮错,最终用 GGUF Q4_K 量化把模型压到 10.6GB,成功跑进采样循环。

这篇文章是完整的操作记录和排错路线图,希望能帮你少走弯路。


🎯 最终方案

项目 方案
运行框架 ComfyUI 0.33.0
GPU 方案 ZLUDA 翻译层(CUDA → HIP)
模型形态 GGUF Q4_K 量化(10.63GB,原模型一半体积)
编码器 Qwen3-VL 32B(nvfp4,强制 CPU 加载)
显存占用 主模型 6GB 进 GPU + 5.2GB 低显存逐层 offload

核心逻辑:Q4_K 权重只有 int8 版的一半,主模型能塞进 8GB 显存,配合 lowvram 模式逐层搬运,就能完整跑起来。


🧱 方案选型:为什么抛弃 int8 转投 GGUF

第一版方案用 int8 量化版(20.97GB),forward 时显存峰值 10.6GB,超过 8GB 物理显存,必死

方案 模型大小 显存峰值 结果
int8 safetensors 20.97GB 10.6GB > 8GB ❌ forward 即 OOM
GGUF Q4_K 10.63GB ~6GB + offload ✅ 有余量

💡 结论:8GB 显存想跑大视频模型,量化率必须到 Q4 级别,int8 只是减半,不够。


📦 模型与文件清单

文件 位置 大小 说明
minimax_h3_fl2va_pruned-Q4_K.gguf models/diffusion_models/ 10.63GB 主模型(unsloth 出品,HuggingFace 下载)
qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors models/text_encoders/强制 CPU 加载,nvfp4 张量无法 .to(cuda) 15.7GB 文本编码器
minimax_h3_video_vae_fp16.safetensors models/vae/ 5.2GB 视频 VAE(解画面)
minimax_h3_audio_vae_fp32.safetensors models/vae/ 577MB 音频 VAE(解声音,别漏)
minimax_h3_fl2v_turbo_4step_v1.0_768p_comfyui_bf16.safetensors models/loras/ 1.87GB 4 步 turbo LoRA

启动参数--auto-launch --dont-upcast-attention --preview-method auto --use-quad-cross-attention

工作流minimax-h3-t2va-gguf-av-turbo4.json(带音频 + 4 步加速,cfg 已默认 1.0);图生视频另有 minimax-h3-i2v-turbo4.json(首帧驱动)与 minimax-h3-ref2va-turbo4.json(参考图模式)

最小验证参数416×256×22 帧 × 4 步 × cfg 1,跑通后再放大。

下载技巧:HuggingFace 直连在国内基本不可用,换 hf-mirror.com 镜像(实测速度 35MB/s)。下载完务必确认文件大小与仓库标注一致,不要留 .part 残留。


🔧 前置补丁:让 ComfyUI 在 ZLUDA 上活下来

ZLUDA 环境下有 5 个补丁是缺一不可的,不补的话报错会随机出现在任何地方(因为 CUDA 上下文从启动起就坏了)。

# 补丁位置 内容 解决什么
1 comfy/cuda_malloc.py cuda_malloc_supported() 检测到 AMD/Radeon/gfx 直接返回 False 最核心:ZLUDA 下 cudaMallocAsync 分配器直接崩溃,强制改回 native 分配器
2 main.py 开头 注入 PYTORCH_CUDA_ALLOC_CONF=backend:native + CUDA_LAUNCH_BLOCKING=1 双保险
3 comfy/ops.py AMD 下把 int8 相关算子加入禁用列表 量化走 emulated 降级路径
4 comfy_kitchen/tensor/base.py 压制 get_cuda_capability() 返回 (0,0);量化权重搬 GPU 前先 CPU 反量化 见下
5 comfy/samplers.py count_nonzero 对 NestedTensor 的降级 采样链路打通

⚠️ 最大的认知坑:ZLUDA 伪造算力

ZLUDA 会让 torch.cuda.get_device_capability() 返回 (8, 8)——假装自己是 RTX 40 系!这导致 PyTorch 启用所有原生 CUDA kernel,而实际后端是 HIP,全部崩溃。

必须主动压制算力检测,强制所有”是否支持快速算子”的判断走降级路径。这是前几轮所有诡异报错的共同根源。


🐛 GGUF 专属巨坑:BF16 反序列化(本文最值钱的部分)

症状(采样第 1 步必现):

1
2
NoCapableBackendError: rms_rope_split_half_:
eager: q_scale: dtype torch.uint8 not in {bfloat16, float32, float16}

排查过程

  1. 报错在 Attention 的”融合 RMSNorm + RoPE”算子,参数 q_scale 是 RMSNorm 权重
  2. 查 GGUF 文件头:q_norm.weight 在 GGUF 里明明是 BF16 类型
  3. 为什么运行时变成 uint8?→ 打开 GGUF 加载器源码,真相大白

根因:GGUF 文件里 BF16 张量按 2 字节 uint16 小端存储,而 numpy 没有 bfloat16 类型,所以 GGUFReader 把它暴露为 uint8 原始字节。加载器只对 F32/F16 做了 reshape,漏掉了 BF16 分支——于是全模型 212 个 BF16 张量(RMSNorm 权重等)以 uint8 裸字节喂给了模型。

修复(加载器内加一个分支):

1
2
3
4
if tensor.tensor_type == BF16:
torch_tensor = torch_tensor.view(torch.uint16).view(torch.bfloat16).view(*shape)
elif tensor.tensor_type in {F32, F16}:
torch_tensor = torch_tensor.view(*shape)

验证:修复后 q_norm.weight 应为 bfloat16、形状正确、数值均值≈1(RMSNorm 权重特性)。

💡 这是所有含 BF16 张量的 GGUF 的通用 bug,不是 MiniMax 专属。如果你的 GGUF 加载后各种 dtype 报错,先检查这一处。


🧩 工作流搭建:魔改版节点的坑

运行时报”缺失节点包”,但节点明明装了——排查后发现:

  • 第三方整合包预装的 ComfyUI-GGUF 是魔改版,节点名是 LoaderGGUF、输入字段叫 gguf_name
  • 官方版本(city96)的节点名是 UnetLoaderGGUF、字段 unet_name
  • 工作流引用官方节点名 → 前端报缺失

教训:报”缺失节点包”先查运行中实例的 /object_info 接口,看实际注册的类名,再决定是改工作流还是装包,别急着装 Node Manager。

完整工作流关键节点

节点 配置
LoaderGGUF gguf_name = minimax_h3_fl2va_pruned-Q4_K.gguf
CLIPLoader 编码器 nvfp4,device 强制 cpu(nvfp4 张量无法搬到 CUDA)
VAELoader 视频 VAE fp16
MiniMaxH3SigmaShift 必需节点(AV 采样器),基础版 shift_video=12 / shift_audio=3;配 768p turbo LoRA 改为 6 / 3
KSampler 最小验证参数:416×256×22帧 × 4 步 × cfg 1(配 turbo LoRA);不配 LoRA 时 10 步 × cfg 5

🗺️ 排错路线图(判断补丁是否生效的标尺)

14 轮排错中,报错不是随机的——它按固定轨迹演进,每前进一格说明前面的补丁生效了:

1
2
3
4
5
operation not supported              ← CUDA 上下文已坏(最早期,补丁 1-4 前)
shared object initialization failed ← 物理内存枯竭(页面文件不够)
out of memory (10.6GB > 8GB) ← 显存物理极限(int8 方案的死因)
q_scale dtype uint8 ← GGUF BF16 反序列化(补丁 6 解决)
正常采样循环 ← ✅ 全链路打通

💾 内存与性能管理

  • 32GB 物理内存是极限:编码器 15GB + 主模型 offload 5GB + 各类对象,运行期间必须关掉其他大程序
  • 页面文件扩到独立盘符(系统盘放不下),48GB 起步
  • 崩溃后必查残留进程:python.exe 可能残留占 20GB+ 内存,清掉再重启

🔊 番外:能出画面,但为什么没声音?

跑通后我兴冲冲打开视频——画面正常,但一点声音都没有。这是 H3 最容易踩的静默失败:不报错,就是没音轨。

根因一:视频和音频是两套独立 VAE

MiniMax H3 的”音视频联合生成”指的是采样过程联合,但解码阶段两套 VAE 完全分开:

VAE 文件 用途
视频 VAE minimax_h3_video_vae_fp16.safetensors(5.2GB) 解画面
音频 VAE minimax_h3_audio_vae_fp32.safetensors(577MB) 解声音(DAC + BigVGAN 架构)

官方仓库两个文件分开提供,我一开始只下了视频 VAE——根本没音频解码器,当然没声音

根因二:音频 latent 被静默丢弃

采样器输出的 latent 是个嵌套张量对:

1
NestedTensor( video[B,24,T,H/16,W/16] , audio[B,32,2,T40] )

普通的 VAEDecode 只解 video,audio 分支被直接扔掉,而且不报任何错。合流节点 CreateVideoaudio 输入空着,出来的自然是哑巴视频。

解法:通用音频解码节点就够

一开始我以为要装 MiniMax 专属音频节点,翻源码才发现 ComfyUI 的通用节点 VAEDecodeAudio 内部已经处理了嵌套张量:

1
2
3
latent = samples["samples"]
if latent.is_nested:
latent = latent.unbind()[-1] # 取最后一个 = audio

所以工作流加两个节点就行:

节点 配置
VAELoader 加载音频 VAE(577MB)
VAEDecodeAudio 输入接采样器 latent + 音频 VAE
CreateVideo audio 输入接上音频解码结果

💡 避坑提示:音频 VAE 的识别键是 pre_block.attn.zero_k_bias(ComfyUI 靠它判断是不是 H3 音频 VAE)。下载后建议验证一下这个键存在,同时确认没有 decoder.model.0.weight_g——否则会被误判成 MiniMax Music3 的 DAV 模型。


⚡ 番外二:4 步加速 LoRA,与”静默失效”这个隐形杀手

10 步采样在 ZLUDA 上太慢,于是加上官方的 4 步 turbo LoRA(1.87GB)。本以为只是拖个节点的事,结果又踩了两个坑——其中一个不会报任何错

坑一:768p 版的 shift 是 6/3,不是 12/3

同一个仓库里挂着好几个 turbo LoRA,超参并不通用。官方规格表:

模型 训练分辨率 训练 shift (video/audio) 推荐推理步数
4-step v0.1 544p 12 / 3 4
8-step v1.0 544p 12 / 3 8
4-step v1.0 768p 768p 6 / 3 4
8-step v1.0 768p 768p 6 / 3 8

只看”几步”去抄参数是错的,必须同时核对版本分辨率两列。基础版工作流用的 shift 是 12/3,换上 768p 的 LoRA 后必须改成 6/3,否则 sigma 网格对不上蒸馏时学到的轨迹,步数再少也白搭。

坑二:LoRA 加载失败是静默的

这是本文第二个”最值钱”的坑。

给模型挂上 LoRA,点 Queue,没有任何报错,视频照常生成,进度条照常走完——但速度和画质一点没变。LoRA 根本没生效。

原因出在键名前缀:

1
2
主模型 state_dict :  blocks.0.attn.qkv_proj.weight            ← 无前缀
官方 turbo LoRA : diffusion_model.blocks.0.attn.qkv_proj ← 有 diffusion_model. 前缀

ComfyUI 的 comfy/lora.py 里,model_lora_keys_unet() 给 Flux、SD3、Kandinsky5、LTXV、QwenImage 等每个架构单独写了一段键名映射——但 MiniMax H3 是新增架构,还没有它的分支,于是只能落到”无前缀”的通用映射上,命中率 0 / 208

load_lora_for_models() 匹配不到键时,只会在日志里打一行 lora key not loaded,然后继续正常执行。生成结果完全合法,只是 LoRA 的贡献为零——白占 1.87GB 显存和几十秒加载时间。

怎么验证:不需要真去加载 10GB 模型,纯静态比对键名即可。

1
2
3
4
5
6
# sd_keys 取自 GGUF 的 tensor 名;mods 取自 LoRA 的 lora_A/lora_B 键去后缀
key_map = {k[:-len(".weight")]: k for k in sd_keys} # 通用映射(补丁前)
# → MATCHED: 0 MISSING: 208

key_map["diffusion_model." + k[:-7]] = k # 补上前缀映射
# → MATCHED: 208 MISSING: 0 ✅

怎么修:在 comfy/lora.pyreturn key_map 之前加一段:

1
2
3
4
5
6
if isinstance(model, comfy.model_base.MiniMaxH3):
for k in sdk:
if k.endswith(".weight"):
key_lora = k[:-len(".weight")]
key_map["diffusion_model.{}".format(key_lora)] = k
key_map["transformer.{}".format(key_lora)] = k

⚠️ 改的是核心文件,必须重启 ComfyUI 才生效。重启后跑一次,日志里不应再出现 lora key not loaded

顺带一提,这个 LoRA 用的是 PEFT 命名(lora_A.weight / lora_B.weight),不是 ComfyUI 常见的 lora_up / lora_down。这个是没问题的——ComfyUI 的 weight_adapter 已经内置了 diffusers2 分支来处理它。

参数对照

参数 基础版 4 步加速版
节点链 加载器 → SigmaShift 加载器 → LoraLoaderModelOnly → SigmaShift
strength_model 1.0
steps 10 4
shift_video 12 6
shift_audio 3 3
cfg 5.0 1.0(实测后定为本工作流默认值)
sampler / scheduler euler / simple euler / simple

注意 LoRA 必须插在 SigmaShift 之前

⚠️ cfg 这一行是实测改过的:蒸馏版 LoRA 学的是”无负向引导”的采样轨迹,cfg=5 会把采样拽离蒸馏流形——实测表现是画面糊 + 帧间闪烁,而速度只比 cfg=1 慢约 15%(见下方实测数据)。所以 cfg=1.0 不是为了省时间,是画质必需

另外两个小知识

GGUF 量化模型叠 LoRA 会不会爆显存? 不会。ComfyUI-GGUF 对量化权重走的是延迟 patch:保持量化状态,把 patch 挂上去,等到真正 forward 时才逐层反量化合并——不会一次性把 10GB 模型展开成 bf16。对 8GB 显卡相当友好。

帧数不是随便填的。 官方工作流用 max(5, round(a*24)) + (5 - (max(5, round(a*24)) % 17)) % 17 来对齐帧数,等价于要求 length % 17 == 5(H3 的时间维压缩对齐)。124 帧正好合规(124 = 7×17 + 5)。

已知取舍

  • turbo 模式下音频与运动质量略低于全步数版本(ComfyUI 官方文档口径)。
  • LoRA 训练分辨率是 768p(1344×768),当前跑 864×480 低于原生画布,增益打折;显存允许可试 1344×768。
  • 显存吃紧时把 cfg 降到 1.0 可以省掉一半前向计算实测更正:cfg=1 相对 cfg=5 只快约 15%,远不到一半。原因见下节——lowvram 模式下瓶颈是 PCIe 搬运,不是算力。降 cfg 的正确理由是画质(消除糊和闪),不是省时间。

📊 实测数据:8GB 显存的真实成绩单

以上都是”能跑”,这一节回答”多慢”。同一台机(RX 6650 XT 8GB + 32GB 内存):

配置 全程耗时 说明
416×256×22 帧 × 4 步 × cfg1 采样仅约 50 秒 冷启动总耗时 15~16 分钟,大头是模型加载 + CPU 文本编码
864×480×124 帧 × 4 步 × cfg5 1:05:01 冷启动(约 6min 模型加载 + 约 10min CPU 文本编码)
864×480×124 帧 × 4 步 × cfg1 45:49 热机 + 提示词缓存命中,几乎纯采样/解码

三个反直觉的结论:

  1. cfg=1.0 只快约 15%,不是理论上的 2 倍。 lowvram 模式下每次前向都要从内存搬约 5.3GB 权重过 PCIe,瓶颈在搬运不在算力——砍掉负向分支省不了多少墙钟时间。想提速:降分辨率/帧数 > 固定提示词吃 CLIP 缓存 > 降 cfg(最后才轮到它)。
  2. 文本编码器走 CPU 是每段视频约 10 分钟的固定税。 Qwen3-VL 32B 的 nvfp4 权重进不了显存,只能 CPU 编码。但提示词不变时命中 CLIP 缓存,10 分钟 → 约 2 分钟。所以批量出片固定提示词、只改 seed 最划算。
  3. 22 帧 ≈ 0.92 秒。 帧数换算别想当然:124 帧 @24fps 才约 5 秒,22 帧的”最小验证参数”其实不到 1 秒。

💡 顺带一个排查经验:ComfyUI 的节点缓存会让同样参数的二次运行 0.28 秒就”跑完”。做计时测试务必改 seed,否则测的是缓存不是显卡。


🎬 图生视频:让固定人物出演任意场景

H3 原生支持两条图生视频路线,都已跑通(turbo LoRA 通用,输出同样带声音):

路线 A:首帧驱动(让这张图动起来)
MiniMaxH3ImageToVideo 有可选的 first_frame 输入。图片会被拉伸为视频第 0 帧作锚点,人物主体天然不变,模型续演出动作和声音。改动最小,适合”让这张图动起来”。

路线 B:参考图模式(主体一致、场景随便换)
MiniMaxH3ReferenceToVideo 节点,参考图最多 9 张(多角度图身份更稳),prompt 里用 <Picture 1> 标签指代,每换一段 prompt 就是一个新场景视频。注意:

  • 必须把 音频 VAE 接到节点的 audio_vae 输入,否则同样没声音
  • ref_image_size=match 快(参考图缩放到生成画布面积);max 身份保真更好,但参考 token 每步参与采样,能慢数倍
  • ⚠️ 身份保持的下限取决于参考图质量:参考图过于简陋/抽象时,模型会直接回退到训练数据里的”通用动漫人物”先验,参考图形同虚设——想锁定自己的角色,必须给清晰、特征明确的人物图

✅ 排错 checklist(收藏版)

按这个顺序查,能覆盖 90% 的翻车场景:

  1. 报错位置随机 → 先怀疑 ZLUDA context 损坏,确认前置补丁全部生效
  2. OOM 且权重文件 >10GB → 换更小量化(int8 不行就 Q4_K)
  3. GGUF 加载报 dtype uint8 → BF16 反序列化缺失,补分支
  4. 出画面没声音 → 查音频 VAE 是否下载 + latent 是否接了 VAEDecodeAudio
  5. LoRA 无效 → 日志搜 lora key not loaded,查架构键名前缀映射
  6. 抄参数先核对「版本 + 分辨率」两列,只看”几步”必错
  7. 崩溃重启前先杀 python 残留进程(可占 20GB+ 内存)
  8. turbo 模型画质崩(糊 + 闪)→ 先查 cfg 是不是没设 1.0
  9. 出片太慢 → 顺序:降分辨率/帧数 > 固定提示词吃 CLIP 缓存 > 降 cfg(最后才轮到它)
  10. 参考图模式人物不像 → 参考图质量问题,换清晰、多角度的人物图,或试 ref_image_size=max

📝 写在最后

整个过程最大的体会:AMD 跑 AI 不是不行,是坑多。ZLUDA 伪造算力、分配器不兼容、GGUF 反序列化漏分支……每一个都是”看起来像玄学、查下去全是逻辑”的问题。

如果你也是 AMD 用户想跑大模型,记住这几句话:

  1. 显存不够就上 Q4 量化,int8 只是减半,不够
  2. 报错随机 ≠ 问题随机,先查 CUDA 上下文是否健康
  3. GGUF 的 BF16 张量,十有八九是反序列化问题
  4. 不报错的失败最贵:没声音、LoRA 不生效、参考图不生效,都不会报错。判断有没有生效要靠日志和 ablation,而不是”跑完了就算成功”
  5. 提速先降分辨率,别先降 cfg:8GB 卡的瓶颈是 PCIe 搬运,cfg 5→1 只快 15%,砍一档分辨率能快一倍

祝你也跑通 🎉

比较简单,打发时间的小品游戏。很适合后台挂个AI干活的时候玩。

dsh-model-fix

修复特定模型在 DeepSeek Harness(DSH)里「流式内容正常但回合必报错」的流式收尾缺陷

本仓库是 bitterSmilezzz/dsh-model-fix 的增强版:
在原版(muse-spark-1.2)基础上,加入了对 gpt-5.6-luna 的支持(同一缺陷,实测
2026-08-23:gpt-5.6-luna 流式/非流式响应的 finish_reason 均为 null,且流式不发 [DONE])。

背景:缺陷根因

opencode 聚合端点(https://opencode.ai/zen/go/v1)上的 muse-spark-1.2
gpt-5.6-luna 实现有缺陷:流式响应正常吐出内容,但从不发送 finish_reason、也不发送
[DONE]
,流直接关闭(同端点对照 deepseek-v4-flash / glm-5.2 均正常发送
finish_reason: 'stop' + [DONE])。

DSH 的 llm-pi-ai 走 pi-ai SDK(强制 stream: true 且要求流以 finish_reason 收尾),
于是每次对话的真实表现是:

  1. 内容正常流式显示;
  2. 结尾报 Stream ended without finish_reason(映射为 TRANSPORT 错误);
  3. 回合被判失败,且 TRANSPORT 在默认可重试列表里 → agent 级重试插件按
    maxRetries 反复重跑整步,烧多份 token

这与代理无关:直连与走代理的响应完全相同,换代理解决不了。

⚠️ 重要说明:无需开启 opencode 的「Allow models that train on request data」

使用 muse-spark-1.2 / gpt-5.6-luna(经 opencode-go 路由)不需要在 opencode 平台设置里
开启**「Allow models that train on request data」**。

  • 该开关是 opencode 的数据训练授权:开启后你的请求数据可能被用于模型训练,属于
    需要慎重对待的隐私授权,不要为了使用这些模型而开启它
  • 实测:不开启该开关,直连 https://opencode.ai/zen/go/v1 即可正常
    返回内容(本插件修复的只是流式收尾缺陷,与训练授权无关);
  • 若在 opencode 设置里看到该开关,保持关闭即可;本文档所述修复不依赖它。

开关位置见下图(opencode 设置页 → 提供商区域,与「启用部署在中国的模型」相邻):

opencode 设置页中的「Allow models that train on request data」开关(opencode 设置页 → 提供商区域)

本插件做什么

llm/stream waterfall 上,对匹配的模型把上述缺陷收尾为正常 stop

  • 仅当流已输出内容text-delta / reasoning-delta / tool-call-delta)且
    结尾错误代码为 TRANSPORT / STREAM_CLOSED 且错误信息明确是「缺少终止事件」
    (如 Stream ended without finish_reason)时才改写;
  • 真实传输故障(如 SocketError: other side closed不受影响,照常失败
  • 无内容的空响应也照常失败,不会被误吞。

修复后 muse-spark-1.2 / gpt-5.6-luna 直连即可正常使用,无需代理、不触发重试。

安装

1
2
3
4
5
# 从 GitHub 安装(推荐)
npx -y @deepseek-ai/dsh plugin --profile web add github:Retr67/dsh-model-fix

# 本地路径
# dsh plugin --profile web add /path/to/dsh-model-fix

安装后重启目标 profile(如 dsh web)生效。

配置

cordis.patch.ymlconfig 两个字段:

字段 默认 说明
modelPattern ^(muse-spark|gpt-5\.6-luna) 对模型 id 测试的正则;只修复匹配的模型
providers [] provider 路由键白名单(如 opencode-go3);空 = 所有 provider 中匹配的模型
1
2
3
4
5
- id: model-fix
name: dsh-model-fix
config:
modelPattern: '^(muse-spark|gpt-5\.6-luna)'
providers: []

只修 luna 可改为 modelPattern: '^gpt-5\.6-luna';只想限定某个路由可加
providers: [ 'opencode-go' ]

开发

1
2
pnpm install
pnpm verify # typecheck + build + node --test

验证矩阵

  • 单元测试:tests/fix.test.mjs 覆盖缺陷识别、内容保留、空响应放行、真实故障放行等 10 例。
  • 真实端点集成验证:用 pi-ai 同款 SDK 直连 muse-spark-1.2 / gpt-5.6-luna,内容经本插件转换后
    结尾由 error(Stream ended without finish_reason) 变为 {"kind":"stop"}

致谢

  • 上游:bitterSmilezzz/dsh-model-fix(MIT)
  • 缺陷取证(gpt-5.6-luna):2026-08-23 直连 opencode.ai/zen/go/v1 复现——流式 10 个 chunk
    finish_reason 全程 null、无 [DONE],尾部为 data: {"choices":[],"cost":"0"}
    非流式响应 finish_reason 同样为 null

dsh-plugin-manager

DeepSeek Harness (DSH) 插件管理器:在 WebUI 设置 → 插件 → 插件管理 里可视化查看、临时打开、关闭其他插件。

  • 用户插件(可开关):你在 profile/package.json 里安装的社区插件 + 你自己在 cordis.patch.yml 里加的条目(如 MCP 桥梁)。
  • 系统组件(只读):DSH 自带的 host/web 基础设施,只展示、不提供开关,避免误关导致 Harness 起不来。
  • 开关无需重启 dsh:走的是 Cordis loader 的 entry.update({ disabled })(与官方 dsh-market 主题开关同一条路)。默认在切换后自动刷新网页,让带前端界面的插件(桌宠、侧边栏等)也立即完全生效;也可在面板里切到手动刷新模式(选择保存在浏览器 localStorage)。
  • 跨重启保持:被关闭的插件 id 会记在 <profile>/.plugin-manager/state.json,下次启动时自动回放为关闭,直到你把它打开。
  • 「全部恢复开启」按钮一键清空所有保持关闭的插件。

环境要求

安装(一行命令)

在任意终端执行(默认安装到 web profile):

1
npx -y @deepseek-ai/dsh plugin --profile web add https://github.com/Retr67/dsh-plugin-manager.git

该命令会:① 安装依赖;② 检测到包声明了 dsh.bundle,自动把插件加进 dsh.profile.bundles(下次启动自动加载)。

如果你的 dsh 已全局可用,也可以直接:

1
dsh plugin --profile web add https://github.com/Retr67/dsh-plugin-manager.git

安装到其他 profile

web 换成你的 profile 名:

1
npx -y @deepseek-ai/dsh plugin --profile myprofile add https://github.com/Retr67/dsh-plugin-manager.git

使用

  1. 重启 dsh:安装后重启一次 dsh web(让插件加载),并刷新浏览器页面
  2. 打开 设置(⚙️ / Settings)→ 插件 → 插件管理
  3. 你会看到:
    • 用户插件(可开关):每个插件一行,右侧有开关,点击即可打开 / 关闭
    • 系统组件(只读):只展示,不提供开关,防止误关导致 Harness 起不来。
  4. 刷新模式:
    • 默认自动刷新:开关后页面自动刷新,带前端界面的插件也会立即完全生效。
    • 可切到手动刷新:开关后不自动刷新,点面板里的「刷新」或手动刷新网页后生效(选择会保存在浏览器 localStorage)。

卸载

1
npx -y @deepseek-ai/dsh plugin --profile web remove dsh-plugin-manager

结构

1
2
3
4
5
6
7
8
9
10
dsh-plugin-manager/
├── package.json # dsh.bundle + dsh.client 声明
├── cordis.patch.yml # 挂载 plugin-manager 主机条目
├── dsh/
│ ├── index.js # 主机端:HTTP API + loader 枚举/开关 + 状态持久化
│ └── client.js # 浏览器端:注册 设置→插件→插件管理 标签页(React)
├── install.ps1 # Windows 一键安装脚本(内部即标准 npx 命令)
├── install.sh # POSIX 一键安装脚本(内部即标准 npx 命令)
├── README.md
└── LICENSE # MIT

install.ps1 / install.sh 只是标准命令的便捷封装,默认安装源是 GitHub 仓库;不以自定义 file: 路径作为主安装方式。

与官方机制的关系

  • 不对 package.jsondsh.profile.bundles 做热增删(那需要重启生效),而是对运行中的 loader entry 做热开关——这是 dsh-market 主题切换验证过的做法。
  • 状态持久化、启动回放、自愈守卫(internal/plugin 事件)同样移植自 dsh-market。
  • group 插件(如把某个插件包包裹成 group 的宿主)无法直接置 disabled,管理器会自动改为开关其子条目。

HTTP API(同源校验)

方法 路径 说明
GET /plugin-manager/list 返回插件列表与开关状态
POST /plugin-manager/toggle { id, enabled } 热开关某个插件
POST /plugin-manager/reset 恢复所有被保持关闭的插件

POST 接口(toggle/reset)做了 Origin === Host 的同源校验;GET 列表是只读、且只暴露在回环地址绑定的 web server 上,不再要求 Origin 头(同源 GET fetch 默认不携带 Origin,要求它会导致面板报 untrusted origin)。

安全

  • 主机端仅使用 node 内置模块(fs/os/path),无网络调用、无子进程、无 eval。
  • 客户端用 React 渲染插件名(自动转义,无 XSS),所有请求都是同源 fetch。
  • 管理器自己(plugin-manager)不会被关闭;系统组件不会提供开关。

License

MIT

原因:酒馆发送的请求主体中不能有stop主体参数。

解决办法:点击API设置界面的附加参数按钮,在排除主体参数栏内填写stop,以不发送此主体参数。

Automatic Gemini API key rotation for modlens on DeepSeek Harness (dsh) — when one key hits its quota (HTTP 429), the read
automatically switches to the next key in the pool and retries, with zero
intervention. Works everywhere modlens reads an image: the dsh GUI paste flow,
the modlens_read_image tool, and the CLI.

⚠️ Unofficial: this patches the installed modlens package. The patch is
wiped by any modlens upgrade; patch.ps1 re-applies it in one command.
If modlens upstream ever ships native multi-key support, drop this project.

Why

modlens reads images through a single Gemini API key stored in ~/.modlens/config.json. Free-tier keys hit daily quota (429 RESOURCE_EXHAUSTED) / rate limits quickly, and modlens has no retry or
failover for that. This project adds:

  • a key pool (~/.modlens/api-keys.json) — list, add, rotate, inspect
  • an engine patch — 429 triggers an automatic switch to the next pool key,
    persisted to the config, then a retry of the same request (up to 8 rotations)
  • a CLI wrapper — 429 → rotate + retry; 5xx → backoff retry on the same key
  • a re-patch script — re-applies the engine patch after a modlens upgrade

Requirements

  • DeepSeek Harness (dsh) with the modlens plugin installed: npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@<version>
  • Node 22.19+ (modlens requirement)
  • PowerShell (Windows) — the scripts are .ps1

Install

Clone this repo (or copy scripts/), then:

1
2
3
4
5
6
7
8
9
# 1. copy scripts next to the modlens config
Copy-Item scripts\* "$env:USERPROFILE\.modlens\"
# or link: New-Item -ItemType SymbolicLink ... (per-file)

# 2. apply the engine patch (idempotent)
powershell -ExecutionPolicy Bypass -File "$env:USERPROFILE\.modlens\patch.ps1"

# 3. seed the key pool (one command per key)
powershell -ExecutionPolicy Bypass -File "$env:USERPROFILE\.modlens\rotate.ps1" add <GEMINI_API_KEY>

The active key is the one modlens uses: if you already configured a key via modlens config set gemini-api.apiKey, add it to the pool too so rotation can
cycle back to it.

Usage

1
2
3
4
5
6
7
8
# manage the pool
rotate.ps1 list # show pool, >> marks the active key
rotate.ps1 status # pool size + active key (masked)
rotate.ps1 rotate # switch to the next key (persists)
rotate.ps1 add <key> # add a key to the pool

# read an image with automatic rotation
ml.ps1 -Image <path-or-url>

The engine patch makes rotation automatic for every read path (GUI paste, modlens_read_image tool, CLI), so rotate.ps1 rotate is mainly for manual
override or diagnosis.

If you use a proxy

modlens itself supports it — no changes needed here:

1
modlens config set proxy http://127.0.0.1:<port>

Note: Google’s Gemini API is region-restricted. A proxy exit in an unsupported
region (e.g. mainland China, Thailand) fails with 400 User location is not supported for all keys — rotation cannot fix
that; switch the proxy node instead.

After a modlens upgrade

1
powershell -ExecutionPolicy Bypass -File "$env:USERPROFILE\.modlens\patch.ps1"

It detects whether the patch is still present; if a new version changed the
targeted code and the hunks no longer match, it reports which hunk failed and
tells you to file a new patch.

How it works

modlens ships two components in one package:

  • dsh/index.js — the dsh plugin shell: registers modlens_read_image,
    handles paste-to-path. It contains no image-reading logic; it spawns a
    child process running the CLI engine.
  • dist/main.js — the CLI engine: reads the image, calls Gemini
    (executeGeminiApi), parses structured JSON. Every read path funnels
    through this file.

The patch lives in executeGeminiApi: a single request becomes a loop that
rotates the pool on 429. See PATCH.md for the exact change.

Security

  • Keys are stored in plain text at ~/.modlens/api-keys.json (same trust
    level as modlens’s own config.json, which also stores the key in plain
    text). Do not share your home directory.
  • This repo contains no keys, no credentials, no machine-specific paths
    all paths derive from $env:USERPROFILE.

Credits & license

  • modlens by Leon Liu (liustack) — the vision engine this project wraps and patches. MIT licensed
    (Copyright © 2026 Leon Liu), see LICENSE.modlens.
    The patch hunks in this repo are a derivative modification of modlens
    source and carry its MIT notice as required.
  • This project is not endorsed by upstream — it is an independent utility
    that depends on modlens.
  • Our own scripts and docs: MIT (see LICENSE).
  • Full attribution: ACKNOWLEDGMENTS.md — please star
    the upstream project, the real work lives there.

修复 OpenViking 在 Windows + 本地向量库 + 本地 embedding 模型 组合下,记忆文件已生成但搜索不到的问题。

问题现象

  • OpenViking 服务可以正常启动
  • 记忆文件可以正常生成
  • 但执行搜索时返回空结果
1
ov find "LoopingIsle" --context-type memory

返回:

1
2
3
4
5
6
7
8
9
{
"ok": true,
"result": {
"memories": [],
"resources": [],
"skills": [],
"total": 0
}
}

服务日志出现:

1
2
openviking.storage.viking_vector_index_backend - ERROR - Error reading existing record before partial update: Strings must be encoded before hashing
openviking.storage.collection_schemas - ERROR - Failed to write to vector database: Strings must be encoded before hashing

根本原因

OpenViking 在把字符串 ID 转成 64 位整数时,使用了:

1
xxhash.xxh64(input_string)

xxhash 在 Python 3 中要求传入 bytes,不能直接传入 str,因此抛出:

1
Strings must be encoded before hashing

这导致:

  • 记忆 Markdown 文件正常写入
  • 向量没有成功写入向量库
  • 所以记忆文件存在,但语义搜索找不到

修复

修改文件:

1
openviking/storage/vectordb/utils/str_to_uint64.py

修改前:

1
2
def str_to_uint64(input_string: str) -> int:
return xxhash.xxh64(input_string).intdigest()

修改后:

1
2
def str_to_uint64(input_string: str) -> int:
return xxhash.xxh64(input_string.encode("utf-8")).intdigest()

补丁文件

文件 说明
patches/0001-fix-str_to_uint64-xxhash-encoding.patch Git 补丁,可直接 git apply
patches/patch-openviking-xxhash.ps1 Windows PowerShell 一键打补丁脚本
patches/fix-openviking-memory.ps1 重置向量库并重新索引记忆的修复脚本

使用方法

方法一:Git apply

1
git apply patches/0001-fix-str_to_uint64-xxhash-encoding.patch

方法二:PowerShell 脚本

1
powershell -ExecutionPolicy Bypass -File patches\patch-openviking-xxhash.ps1

修复后重新索引

1
2
ov reindex viking://user/default/memories --mode semantic_and_vectors --wait true
ov find "LoopingIsle" --context-type memory

环境

项目
操作系统 Windows
Python 3.14.7
OpenViking 0.4.14
向量库 local 本地向量库
Embedding 本地 GGUF 模型(512 维)
VLM OpenCode Go(deepseek-v4-flash

为什么很多人没遇到

这个 bug 主要在以下组合下出现:

  • Windows
  • Python 3.14
  • 本地向量库
  • 本地 GGUF embedding 模型

大多数用户使用的是:

  • Linux + Docker
  • 云端 embedding API
  • 云端/托管向量数据库
  • 较旧的 Python 版本

所以这条本地路径测试覆盖较少。

额外注意

如果只是移动了 OpenViking 目录导致向量库元数据路径不匹配,可以在配置中加:

1
2
3
4
5
{
"embedding": {
"allow_metadata_override": true
}
}

前提是模型没有换、维度没有变。

有始有终,值得好评。

虽然后面有点赶,但感觉把想呈现的效果都呈现了。

就像看了一段也许真的存在的冒险。人物的变化和成长也悄然地推进,不突兀挺好。

原本是单人游戏,但我是跟朋友一起用合作模式通的关。

对于多人体验而言,如果不是多人游戏荒了,不建议把这游戏作为首选项。

各种解密和动作体验中规中矩,在2026年下,没有啥新鲜内容。

各种互动还算不错,动作有些僵硬。

剧情方面中等偏上水准。

值得一提的是,在剧情的最后,当主角处在一个特殊状态下时,一开始无法正确地前往目的地,当我以为附近有什么谜题时,突然脑子里意识到了一个新的可能性,当我把这个可能性告诉我朋友,并真的成功时,当时带给我的尤里卡时刻,还是非常震撼的,并且这个设计也十分符合剧情和玩法,让游戏的机制玩法解释了剧情,让剧情和体验更上了一层楼。甚至我有点怀疑,作者是因为这个时刻,才做了这么一款游戏。

0%