变更记录
2026-08-20(夜,再续)— h3-console 任务列表加删除/回收站
结果:任务列表每行右侧加「⋯」菜单(含「删除」),左下角加「🗑 回收站」入口,点开是删除任务的
列表,每条带「恢复」按钮。软删除——数据库里加一列 deleted_at,删除/恢复就是给它写时间戳/清空,
不是真的从 sqlite 里 DELETE,也不碰 gateway/api.py 上的任务本身(该跑的还继续跑,只是从用户
看到的列表里挪走)。
server.py:jobs 表加 deleted_at REAL 列(ALTER TABLE 包一层 try/except 兼容老库,
不会破坏已有数据);GET /v1/videos 加 ?trash=true 参数切换"主列表"和"回收站"两个视图
(分别过滤 deleted_at IS NULL / IS NOT NULL);新增 DELETE /v1/videos/{id}(软删除)和
POST /v1/videos/{id}/restore(恢复),都是幂等的 UPDATE ... WHERE + rowcount 判 404。
_pending_counts()(弹性调度器读的排队/运行中计数)没有跟着按 deleted_at 过滤——删除只是
从用户列表里隐藏,任务如果还在 gateway/api.py 上真跑着,该占的显存和该算的调度需求不变,这里
过滤了反而会让统计和实际资源占用对不上。
前端:renderSidebar() 加了 viewingTrash 模式分支(回收站视图复用同一个 #job-list 容器,
不是新开一个面板),任务行右侧「⋯」按钮弹出小菜单(点击外部自动收起),回收站行是「恢复」按钮。
纯新增 UI 状态和几个 fetch 调用,没碰现有的 refreshDetail/prefillCreate 等逻辑。
验证:浏览器里跑了一遍完整链路(删除任务 → 从主列表消失、出现在回收站,带"删除于 X 秒前"
时间 → 点恢复 → 回收站清空、任务原样回到主列表,创建时间没被恢复操作重置),以及命令行测过
DELETE/restore 的幂等性(重复调用 404,不是 500)。改的是 server.py(有状态的后端逻辑),
按安全重启法用 ss 按端口找 PID kill、重启,重启前确认了没有正在轮询详情页的关键操作会被打断
(console 是独立进程,重启不影响 gateway.py/api.py 上正在跑的生成任务)。
2026-08-20(夜,续)— console 修复"任务卡在假运行中":后端重启丢任务时正确判失败
背景:用户报告任务 b515f90547be 详情页显示"运行中",但 /v1/backends 明明
4/4 ready · 排队 0 · 运行中 0,两边对不上。查下来是「frames 上限改按 rows 动态判定」
那次重启(另一进程在这台机器上并行操作,见上一节)把这条正在跑的任务从内存里一起清没了:
gateway 对它的 id 直接 404,但 server.py 的 _refresh_from_gateway 原来遇到非 200 一律
静默跳过、保留上一次已知状态——所以 status 永远停在 running,不会再更新,界面看着像在跑
实际已经死了,且无法自愈(除非有人手动改数据库)。
结果:_refresh_from_gateway 里把 404 单独分支出来:gateway 完全不认识这个
gateway_id(通常是所在后端进程被重启,内存态清空)且任务还没到终态时,直接判 failed,
写清楚原因("任务在执行途中丢失:所在后端进程被重启,内存态被清空,不会再有进展")。
其它非 200(后端暂时不可达之类)继续保留原状态、下一轮再试,不受影响。
验证:改完重启 console 进程后,b515f90547be 立刻从假"运行中"变成 failed 并带上错误信息。
只改了 h3-console/server.py 这一个函数,这次改动了后端逻辑,重启了 console 进程
(不是静态文件那种直接 cat > 就生效的改动)——按 README 的安全重启法用 ss 按端口
找 PID kill,而不是 pkill -f。重启前确认了四张卡都 ready 且 queued=0,running=0,
不会打断正在跑的任务。
2026-08-20(夜)— frames 上限改按 rows 动态判定 + fl2va/ref2va 默认基线改 2/2
背景:任务 8755c5600378(832×480×362帧)失败,表面看像"弹性调度没生效",查下来是两件
没关系的事叠加:调度本身是好的(/v1/backends 一直 ready:4/4),是 api.py 的
frames: Field(124, ge=8, le=124) 硬顶把这条请求拒了——跟分辨率无关,一刀切。但上一节
「15 秒时长实测」已经测出 448×256/576×320 这两档能扛住 362 帧,api.py 的校验一直没跟上。
改动一:frames 上限从硬编码 124 改成按 rows(分辨率×帧数)动态判定
api.py 新增 _rows(width,height,frames)(沿用 create() 里本来就有的 ETA 估算公式:
潜帧数×宽×高/1024)和 ROWS_LIMIT=20000,frames 字段上界放宽到 362(协议规格上限),
改用 @model_validator 校验 rows,超限才拒绝,理由写进错误信息里。
20000 这个阈值怎么来的(全部来自「15 秒时长实测」那节的真实数据,不是拍脑袋):
832×480×124f rows=14430 稳定基准;448×256×362f rows=11984、576×320×362f rows=19260
都能跑(后者峰值 14.5G,吃紧但没崩);640×384×362f rows=25680 OOM(撞到 20.6G)。
20000 卡在 19260(能跑)和 25680(OOM)之间。
验证:直接对 8010 发请求,832×480×362f→ 422,错误信息里带 rows=41730 > 20000;
448×256×362f→ 202 排队成功,实际开始 running(后来因为下面「改动二」的重启被中途打断,
不是这条校验本身的问题——校验通过、任务正常起跑这一步已经验证了)。
同时把 console 前端(h3-console/static/index.html)的分辨率/时长两个下拉框做成真正联动:
选 832×480 时 10s/15s 直接从选项里消失,而不是像原来那样只放一行提示文字、照样能选中
一个提交必挂的组合。前端判定逻辑(rows()/ROWS_LIMIT)和 api.py 手动保持一致,
注释里写明了"两边数字要一起改"——纯静态文件,cat > 覆盖即生效,没重启 console。
改动二:fl2va/ref2va 默认基线从「4 张全 ref2va」改成 2/2 固定分裂
结果:start-all.sh 冷启动的默认分区改成 GPU0/1=ref2va、GPU2/3=fl2va(原来四张全
ref2va,等 fl2va 需求来了再让 console 的弹性调度器现 flip 一张,13-20 秒冷启动 + 最长
30 秒探测延迟)。改动只在 start-all.sh 里两行 ref2va→fl2va(对应端口 8011/8013),
GPU/端口映射本身没变。调度器代码一行没动——h3-console/server.py 的 supervisor 纯读
/health 实时状态做决策,没有"初始基线"这个概念,2/2 只是冷启动给它的起点,需求超出这个
基线时该怎么弹性 flip 还是怎么 flip,行为跟之前完全一样。已重启验证 ready:4/4 且分区
如预期(gpu1: ref2va, gpu2: fl2va, gpu0: ref2va, gpu3: fl2va)。
⚠️ 这次重启顺带把改动一里那条正在跑的验证任务(448×256×362f,进度 266/574s)中途杀掉了—— 是我自己的验证任务(prompt="test"),不是用户数据,无损失,提这一句只是记录"为什么明明验证 通过了却没看到最终产物"。
2026-08-20(晚)— h3-console 任务详情页加「重跑」按钮
结果:/task.html/<id> 详情页状态行旁加了「重跑」按钮。第一版直接后台再 POST /v1/videos
一次,用户反馈不对——应该是把参数带回新建任务表单让人看一眼再手动提交,不是静默重新提交。
改成:点击后把这条任务当初的 req(prompt/分辨率/帧数/turbo/steps/seed/首尾帧或参考图 URL)
灌回对应的 tab(文生/首尾帧/参考图,按 req 里有没有 first_frame_url/reference_image_urls
自动判断)、跳到 /newtask.html,用户确认无误后自己点「提交」——跟手动新建任务走的是同一条
路径、同一次人工确认。没改后端——server.py 的 POST /v1/videos 还是原来那个入口,
只是前端多了一步"回填表单"而不是直接调用。只改了 static/index.html(新增
prefillCreate()/setThumbs() + detail 模板里的按钮),纯静态文件,
直接 cat > .../console/static/index.html 覆盖就生效,不需要重启 console 进程。
⚠️ 用这个按钮验证时顺手发现任务 8755c5600378 失败是参数校验拒绝,不是 bug:
width=832,height=480 配 frames=362 被 api.py 拒了(frames 上限 124,跟上一节
「15 秒时长实测」结论一致——832×480 这个分辨率撑不住 362 帧)。重跑会用同样的参数、
同样会失败,这是预期行为(按用户原话"一样的配置"重跑),不是这个按钮该吞掉或改的错误;
真要让这条任务跑通,需要换成 448×256 或降帧数,属于用户自己改参数重新提交的范畴。
2026-08-20(下午)— h3-console 部署上线,弹性 fl2va/ref2va 调度全链路验证通过
结果:h3-console 从本地实现推进到部署(服务器 /data/models/minimax-h3/console/,端口 8080),
用真实 GPU 完整验证了一遍"提交 fl2va 任务 → 排队 → 自动 flip 一张空闲卡 → 任务自动提交并跑完",
过程中揪出并修好两个真实 bug(细节见 h3-console/README.md「部署过程中揪出的两个真实 bug」):
- PID 提取从来没匹配上过:
ss -lptn输出里 pid 嵌在users:(("python",pid=NNNN,...))里, 不是独立空格分隔 token,tok.startswith("pid=")永远不命中,kill从来没真的执行——旧进程 全须没被杀,新进程因端口冲突立刻退出。改用正则re.findall(r"pid=(\d+)", ...)。 - 调度器不知道"已经在切换中":api.py 冷启动 13-20 秒跨了不止一个 10 秒 tick,中间几轮
误判需求没满足,一个
pending=1的请求把三张卡都切了过去。加flipping字典记账修复。 - (操作事故,非代码 bug)
pkill -f "<pattern>"杀了自己的 SSH 会话——命令行本身包含被 搜索的文本,start-all.sh注释早警告过这个坑,这次真踩了一遍。以后一律用ss按端口找 PID。
四卡已恢复到干净的全 ref2va 基线状态(测试期间临时 flip 的那张卡已经切回去、验证过 ready:4/4)。
2026-08-20(上午)— 15 秒时长实测 + 新增 h3-console 管理台(设计+本地实现)
结果:两件事都完成,console 本地跑通并冒烟测试通过(部署见上一节)。
15 秒时长边界实测
MiniMax-H3 官方规格 4–15 秒(362 帧/24fps)不是脚本瞎设的限制——t2va.py/api.py 里的
124 帧上限是我们自己测出的显存边界,不是抄错官方示例(API.md 早就写明这点)。
实测 362 帧在不同分辨率下:
| 分辨率 | 结果 | 峰值显存 | 单步耗时 |
|---|---|---|---|
| 448×256 | ✅ 正常 | 12.0G | ~90s |
| 576×320 | ✅ 但吃紧(震荡换入换出) | 14.5G | 217s(慢 2.4×) |
| 640×384 | ❌ OOM | 撞到 20.6G 才崩 | — |
| 832×480(线上默认) | ❌ OOM(空卡也不行) | 撞到 20.65G 才崩 | — |
结论:15 秒能做到,但线上默认的 832×480 做不到,实用上限在 448×256~576×320 之间。
测试过程中临时停了 GPU3 的 ref2va 后端腾显存(这是测试机,CLAUDE.md 里明确允许),
测完已恢复,四卡 ready: 4/4。
被纠正的两个误解(用户转述 ChatGPT 的建议)
- "该先怀疑 Turbo 4-step 加速而不是 NF4"——方向对,我们自己的 A/B 数据支持:同一套 NF4 权重,只关 Turbo、步数 4→50,帧间差异从 16.99 降到 7.366,音频从 −34dB 升到 −27dB, 量化没变、画质肉眼可判断变好,说明 Turbo/步数是比 NF4 量化更大的画质杠杆。
- "该下载原版 BF16/FP16 权重,88GB 显存够跑更高精度"——这条路已经被昨天的实验堵死,
不是没试过:bf16 在 sm_75 上无 attention kernel(
RuntimeError: No available kernel), fp16 会让模型输出溢出变黑屏(已用 A/B 实验证伪)。这台硬件上 compute dtype 只能是 fp32, 跟权重存储精度是 NF4 还是 BF16 无关——bitsandbytes NF4 本来就是反量化到 fp32 再算。 "88GB=4×22G 独立显存,H3 不支持模型并行"这条误解也再次被纠正(README.md第四节早有结论)。 下载 BF16 权重(约 NF4 的 3-4 倍体积)在 5.9MB/s 出口带宽下要 4-5 小时,换来的只是去掉 NF4 约 9% 的量化误差,性价比远不如已经验证过的"关 Turbo、加步数"。
新增 h3-console/(设计文档 + 本地实现,未部署)
需求:给 minimax-h3 的四后端 API 加 Web 管理台(文生/首尾帧/参考图三个界面 + 上传 + 任务列表 +
按需 fl2va/ref2va 弹性分卡)。设计文档 h3-console/DESIGN.md、实现见 h3-console/README.md。
架构上不重做 gateway.py 已经做好的调度/限流/健康聚合,只加它没有的 Web UI/上传/持久化/
弹性分卡决策。本地端到端冒烟测试通过(上传→提交→轮询→下载全链路 + 浏览器 UI 手测),
弹性 partition 调度器(kill+restart GPU 后端切分区)只能在服务器上验证,还没测。
2026-08-18/19 — MiniMax-H3 文生视频从零到出片
结果:成功。两档都出片并通过 ffprobe + 逐像素 + 音频样本验收。 但环境未恢复——见文末「遗留」。
最重要的一条:minimax-h3/README.md 原第一节是错的
原文写「torch.bfloat16 → torch.float16(致命)」,把改用 fp16 当作必须的修正。
实测相反:改成 fp16 才是这条线一直出不了片的原因。
对照实验(448×256×28f,唯一变量是 torch_dtype):
| fp32 | fp16 | |
|---|---|---|
| 视频像素 | mean 140.9 std 50.6 | mean 0 std 0 |
| 音频 | finite | NaN |
| 结果 | 正常出片 ✅ | 124 帧纯黑 ❌ |
根因:模型按 bf16 训练(bf16 有 fp32 的动态范围),sm_75 无 bf16,
fp16 的 65504 上限装不下——dit.blocks.0 输入 absmax 89 就放大到 38176,50 层残差累积必爆。
被推翻的假设(六条,都实测过)
| 原以为 | 实际 |
|---|---|
| fp16 是 Turing 上的正确选择 | fp16 出全黑视频,fp32 才行 |
--fp32-accum 是降级第一档 |
开关生效但输出逐字节相同,对此故障完全不对症 |
| NF4 合成 probe 通过 ⇒ 真实模型可用 | 单层不溢出推不出 50 层残差不溢出 |
condition_proj 的 inf 是根因 |
修好后问题依旧——它只是同一个 bug 的一个实例(库里有 100 处 .to(x.dtype)) |
| 钳位到 fp16 上限可规避溢出 | 反而更糟:残差 x + attn(...) 中 x 恰为最大可表示值,再加任何正数立刻 inf |
| m3e / sd-forge 无 systemd ⇒ 手工拉起、恢复有风险 | 它们由 PM2(God Daemon PID 1467)托管,kill 会被 60 秒内拉回,必须 pm2 stop |
实测数据(fp32,单任务)
| 分辨率 | 步数 | 耗时 | 每步 | 峰值显存 | 像素 std |
|---|---|---|---|---|---|
| 448×256×28f | 4 turbo | 62 s | 15.5 s | 16.3 G | 50.6 |
| 640×384×61f | 4 turbo | 171 s | 42.8 s | 11.4 G | 51.7 |
| 832×480×124f | 4 turbo | 556 s | 107 s | 12.3 G | 57.85 |
| 832×480×124f | 50 全质量 | 5364 s | 107.3 s | 9.1 G | 59.68 |
每步 107 s 在 turbo 与 50 步下完全一致,是这台机器 fp32 全分辨率的稳定值。 加载:首次从机械盘读 fp32 权重 238 s;page cache 命中后 12–20 s。 fp16 是 36.8 s/步(快 2.9×)但根本出不了片——不是速度取舍。
产物验收
- Turbo:7,859,869 字节 · 帧间差异 16.99 · 音频 mean −34.0 dB / rms 0.0199 / 非零 100%
- 50 步:6,726,194 字节 · 帧间差异 7.366 · 音频 mean −27.2 dB / max −9.2 dB
- 两者均 832×480 · 24 fps · 124 帧 · 5.17 s · 含 aac 32 kHz 立体声
顺带修掉的
t2va.py退出码不可信:原脚本只查 audio,从不检查 video,全黑视频照样打印[done]并返回 0。 已补 video 有限性 + 纯色检查(std 退出码 3。--vram-reserve按 dtype 自适应:fp32=14 / fp16=5。实测vram_limit7.3 G 时全分辨率跑通,19.3 G 时 OOM。PYTORCH_ALLOC_CONF=expandable_segments:True写进run.sh——22 G 卡上碎片曾达 2.49 G 并直接压垮运行。- 4 个 probe-*.py 补齐到服务器:README 第九节要求「换硬件后先跑 probe-* 四个」,但服务器上原本只有 3 个脚本。
新增
minimax-h3/api.py+api-run.sh:异步任务式 HTTP API(提交拿 id → 轮询 → 下载)。 worker 在写文件前自验像素 std 与音频有限性,不会返回"成功但没用"的产物。minimax-h3/API.md:调用说明、参数范围与理由、错误码、Python 轮询示例。HANDOFF.md:交接文档,CLAUDE.md已加入口指引。deploy-scripts/RESTORE-GPU.md:停机/恢复 runbook(含 PM2 那条更正)。
方法教训
- 诊断配置必须与故障配置逐项一致:前几轮用中文 prompt 诊断、英文默认 prompt 复现, 溢出点不同,定位出的路径在真实故障里不触发,白跑几轮。
- 自检要守在会失败的位置:查
.to(fp16)之前的 fp32 张量永远显示"正常",溢出恰在转换处。 cmd1 && cmd2 &的&作用于整条链:cat > file && ... &会让文件写入与执行竞争, 曾因此跑了一个 0 字节脚本还以为它成功。上传与启动要分两步。pgrep -f "gunicorn.*7590"会匹配到自己那条 SSH 命令行,照着 kill 会误杀; 用nvidia-smi --query-compute-apps=pid反查。ffmpeg -af volumedetect输出易被 grep 漏掉,误报"无音频流";用 PyAV 解码算 rms 更可靠。- prompt 必须从文件读:三层 shell 会吃掉引号,传进去是空串,报错是
cannot reshape tensor of 0 elements——看着像 transformers 的 bug,实际是输入为空。
⚠️ 遗留(未完成)
四个服务仍是停的→ 已决策,不是遗留项。2026-08-19 16:40 用户明确「停止 llm38」「只留着 h3 即可」, 所以llm38/whisper/m3e/sd-forge停着即为目标状态;仅 H3 API(8010 + 8011)保留运行。 四者的恢复命令备在deploy-scripts/RESTORE-GPU.md,需要时再起。- llm38 与 H3 共存的显存约束(将来恢复时才需处理):llm38 跑 256K 需四卡各约 14.3 G,
而 GPU1 现被 8010 实例占着只剩 8.95 G。共存需
--parallel 1 --ctx-size 262144(单请求仍 256K,并发降为 1,省 16 G),或先停一个 H3 实例。 t2va.py:47的max(6.0, ...)地板值:空闲显存- API 无鉴权,与这台机器上其他服务一致;任务状态存在内存,进程重启即丢(本次已实际发生)。
2026-08-18 — 单请求上下文 32K → 256K(llm38)
结果:成功。中断 11 秒(17:47:14 停 → 17:47:25 就绪,远快于预估的 2 分钟,因为 CUDA JIT 缓存已热)。
用户问的那个问题:不,上限不是 128K
GGUF 元数据里 qwen35.context_length = 262144——模型原生 256K,之前所有文档写的 128K 都是低估。
python3 /home/admin/qwen38-deploy/ggufmeta.py /data/models/Qwen/Qwen3.8-27B-GGUF/Qwen3.8-27B-Q5_K_M.gguf
llm-setup.md 旧表里只列到 --parallel 2 --ctx-size 262144(128K/请求),那不是硬件极限,也不是模型极限,只是当时没查元数据。
改了什么
只动两个参数,其余(模型、镜像、采样参数、健康检查、挂载)逐字未变:
| 改前 | 改后 | |
|---|---|---|
--ctx-size(总池) |
131072 | 524288 |
--parallel(槽位) |
4 | 2 |
| 单请求上限 | 32K | 256K |
| 并发槽位 | 4 | 2 |
不需要 YaRN:524288 是总池,被 2 槽均分后每槽 262144 = 正好等于 n_ctx_train,没有外插。llm-setup.md 第 65 行「524288 (YaRN 2x)」把总池和单请求上限混为一谈了,已订正。
实测数据
| 指标 | 改前(32K×4) | 改后(256K×2) |
|---|---|---|
n_ctx_slot |
32768 | 262144 |
| 显存(llm38 自身) | 29.4 GB | 57.3 GB |
| 四卡合计占用(含 whisper) | 32.7 GB | 61.4 GB / 88 GB |
| 生成速度(短上下文) | 23.4 tok/s | 23.4 tok/s(无变化) |
| 生成速度(150K 上下文) | — | 16.7 tok/s |
| 提示处理(11K 提示) | 1491 tok/s | 1491 tok/s |
| 提示处理(150K 提示) | — | 979 tok/s |
四卡分布:GPU0 15264 / GPU1 14310 / GPU2 16620 / GPU3 15254 MiB。GPU2 只剩 5.4 GB 最紧,起新负载时避开它。
验收:大海捞针(150K token)
把一条真信息埋在 4000 条填充记录的第 1800 条,提示总长 149,880 token——这个长度连改前的 131072 总池都放不下,是硬证明而非软验证。
truncated = 0(关键:没有静默截断)- 精确取出「QX-7741-KM / 周敏」,thinking 里明确引用了「条目1800」
- 中文连贯无乱码
- 3 路并发全部正常返回,healthcheck
healthy/ FailingStreak=0
代价,明说
- 并发从 4 降到 2。 第 3 个请求不会被拒,会排队。公网
qwen.wudaima.com若真有多人同时用,排队会变明显。 - 长提示很贵。 256K 提示光 prefill 就约 4.5 分钟(按 979 tok/s 算),且期间独占一个槽位。150K 那次端到端 2m43s。
- 显存余量从 55 GB 掉到 26 GB。 minimax-h3 是按「空闲显存 − 2 GB」自适应的,这条线的可用显存被同步压缩了,两边共用一台机器时要注意。
回滚
旧容器改名保留没删:
docker rm -f llm38 && docker rename llm38_prev llm38 && docker start llm38 # 约 15 秒
切换脚本 /home/admin/qwen38-deploy/ctx256k.sh 自带失败自动回滚(容器起不来或 8 分钟不健康就自动退回)。确认稳定后再 docker rm llm38_prev——/ 分区只剩 29 GB。
想要 256K × 4 并发?可以,但没做
-ctk q8_0 -ctv q8_0 把 KV 量化到 8 bit,KV 从 64 GiB 降到 32 GiB,256K×4 就放得下。没做的原因:用户要的是最大上下文不是最大并发,而量化 KV 会引入质量变化,不值得在一次变更里叠两个变量。要做的话单独做,单独测。
2026-08-18 — MiniMax-H3 文生视频部署(进行中)
结果:环境就绪并通过全部兼容性实测;模型下载中(33 GB / 6 MB/s),尚未完成首次出片。
详见 minimax-h3/README.md。
服务器侧:/data/models/minimax-h3/。系统级零改动——没装 CUDA、没动驱动、
没碰 /usr、没改 ~/.local。venv 用 --system-site-packages 复用已有的
torch 2.9.0+cu128(其 arch_list 含 sm_75),只额外装了 bitsandbytes 0.50.1 和 diffsynth。
被推翻的假设
| 原以为 | 实际 |
|---|---|
| 照抄官方 NF4 示例即可 | ❌ 三处必改:全篇 torch.bfloat16 → float16;vram_limit 取的是 mem_get_info()[1](总显存),四张卡都有常驻服务,必须改 [0](空闲);low-vram 示例的 offload_device="disk" 在 7200rpm 机械盘上是 87 秒/步的 I/O 灾难,改 CPU 内存 offload。 |
| bf16 只是慢一点 | ❌ sm_75 上 bf16 没有任何高效 attention kernel(EFFICIENT 和 FLASH 都报 No available kernel),PyTorch 不报错、静默掉到 MATH 后端物化整个注意力矩阵。14k token 序列必 OOM,且症状伪装成显存不足而非 dtype 问题。光测 bf16 matmul 会得出错误结论——它是能跑的(cuBLAS 升位模拟)。 |
| 4×22 GB 可以合起来跑一个大任务 | ❌ DiffSynth 的序列并行(utils/xfuser/)只被 wan_video.py 和 mova_audio_video.py 引用,H3 pipeline 零引用。叠加四卡无 P2P + 跨 NUMA,只能做独立 worker 并发,且并发数由内存(每 worker 约 32 GB / 共 128 GB → 2–3 个)而非显存卡死。 |
| Turing 上只能走 llama.cpp | ❌ 那是 LLM 的结论。PyTorch + bitsandbytes NF4 在 sm_75 上实测可用(bnb 4-bit 的最低要求正好是 sm_75)。 |
nohup 足够让下载脱离 SSH |
❌ 不够,SSH 通道关闭仍会带走进程,必须 setsid(手册第三节其实写了,没照做)。另外开两个下载进程会互相破坏同一个 ._____temp 分片——出口带宽是瓶颈,并行毫无收益。 |
实测数据(RTX 2080 Ti / sm_75 / torch 2.9.0+cu128)
SDPA bfloat16 EFFICIENT → No available kernel SDPA float16 EFFICIENT → OK
SDPA bfloat16 FLASH → No available kernel SDPA float16 FLASH → 仅 sm80–sm121
SDPA bfloat16 MATH → OK(朴素实现,会 OOM)
真实 H3 形状 S=14100 H=56 D=128 的 fp16 SDPA EFFICIENT → 0.21s
fp16 GEMM 有效算力 → 54.3 TFLOPS(张量核满血)
fp16 深链数值稳定性 → finite
NF4 fp16 前向 → finite,rel_err 0.0913;prequantized 重载 → match=True
DiT 是稠密 30.9B(50 层 / hidden 5376 / 56×128 heads / ffn 14336,无 MoE),
480×832×124 帧约 14,100 token、830 TFLOP/步。故 50 步估 20–35 分钟,
Turbo 4-step LoRA 估 2–4 分钟——LoRA 可叠在 NF4 上(AutoWrappedQuantizedModule
继承 LoRAHotLoadMixin,在量化输出之后做加法,不需反量化基座)。
环境里两个坑
~/.local的 bitsandbytes 0.45.1 是坏的('NoneType' object has no attribute 'cquantize_blockwise_fp16_nf4'),venv 内装 0.50.1 覆盖。- 不要设
PYTHONNOUSERSITE=1:这台机器的torch/transformers/av都在~/.local,屏蔽掉会连 torch 一起丢。venv 的 site-packages 本就排在它前面。
容器变更(用户授权,可逆)
停了 llm(旧 llama.cpp,unhealthy 5 天,已被 llm38 取代)——恢复 docker start llm。
whisper 停后已恢复(只用单卡,停它零收益)。llm38 全程未碰。
启动参数备份在 /data/models/minimax-h3/backup/。
2026-08-18 — Qwen3.6-27B → Qwen3.8-27B
结果:成功。中断 1 分 51 秒(17:03:14 停 → 17:05:05 就绪)。
起因
用户想部署 Qwen/Qwen3.8-27B-FP8。结论是 FP8 版本装不了,改用 GGUF。
为什么 FP8 不行(三道坎,第一道是硬的)
- 2080 Ti 是 Turing,compute capability 7.5。FP8 原生需 Ada(8.9)/Hopper(9.0);vLLM 的
fp8_marlin回退内核要求 Ampere(8.0+)。7.5 连回退路径都够不着——不是配置能绕的,是芯片里没有那个电路。 - Turing 不支持 bf16,而 FP8 权重的 scale 用 bf16 存。(机器上现有 vLLM compose 全带
--dtype=half就是这堵墙留下的痕迹。) - 即便前两条不存在,机器上最新的 vLLM 镜像是 v0.10.2(11 个月前),不认识 Qwen3.8 的 Gated DeltaNet + Gated Attention 混合架构。
最终方案
bartowski/Qwen3.8-27B-GGUF 的 Q5_K_M(19.32 GiB / 20,752,787,040 字节),llama.cpp b10481 镜像,容器名 llm38,端口 8000。
选 Q5_K_M 而非平移 IQ4_XS:四卡 88 GB 只用了 29 GB,余量充足,质量提升是白拿的。
过程中被推翻的三个假设
对抗性验证阶段推翻了三条原本打算照做的事,都值得记住:
| 原计划 | 为什么错 |
|---|---|
| 新旧模型并存在 8000/8001 上 A/B 对比 | ❌ GPU3 只剩 10.7 GB(whisper 常驻 3.2 GB),而 llama.cpp 默认 tensor-split 按总显存而非剩余显存均分,不加 -ts 必 OOM。更要命的是现有服务的 context checkpoint 是惰性分配的(149.626 MiB × 4 slot × 8,上限约 4.7 GB),新容器吃光余量后公网服务会在某次多轮长对话时突然 OOM 挂掉。改为串行切换。 |
| 沿用 Qwen3.6 的采样参数 | ❌ Qwen3.8 thinking 模式官方推荐 temp 1.0 / top-k 20 / min-p 0.0;照抄 3.6 的 0.7/40/0.01 会让 thinking 质量下降。且 llama.cpp 默认是 top_k=40 / min_p=0.05,不显式写就是错的。 |
| 靠 MTP 推测解码提速 | ❌ --spec-type draft-mtp 在 sm_75 上对 hybrid 模型根本不激活(llama.cpp issue #24670)。网上「+33~39%」的说法在这台机器上吃不到。而且 MTP 头是独立 sidecar 文件,bartowski 仓库根本没有。 |
差点踩中的静默陷阱
llama.cpp build 必须 ≥ b10450(DeltaNet CUDA 修复 commit ece963f4)。b10419~b10450 的症状是加载正常、显存正常、tok/s 正常、输出全是乱码——机器上原有的 b9570 正好在雷区里。
上线前验证:docker run --rm --entrypoint /app/llama-server <镜像> --version,实得 build 10481, commit 25ae3a9b3。
实测数据
| 指标 | Qwen3.6-27B IQ4_XS | Qwen3.8-27B Q5_K_M |
|---|---|---|
| 权重 | 15.77 GB | 19.32 GB |
| 生成速度 | 28 tok/s | 23.4 tok/s |
| 显存(四卡合计) | 29.3 GB | 29.4 GB |
| 上下文 | 131072 | 131072 |
权重大了 3.5 GB 显存却几乎没变:Qwen3.8 的 64 层里只有 16 层是标准注意力(full_attention_interval=4),48 层是 Gated DeltaNet,KV cache 只有同规格纯注意力模型的约 27%。
四卡分布:GPU0 7448 / GPU1 7336 / GPU2 8110 / GPU3 6532 MiB。
验证结果
- SHA256
e731e180460b906f373294a4e2de10541e80ee676af7f8c949a84dbb6ed3caa8与官方逐字节一致 - 中文输出语义连贯,无乱码(本次最大风险,已排除)
- healthcheck
healthy/ FailingStreak=0 - 5 轮连续请求显存恒定 29426 MiB,无 CUDA graphs 泄漏
- 下游硬编码旧模型名
Qwen_Qwen3.6-27B-IQ4_XS.gguf仍返回 200 + 正常内容(服务端不校验 model 字段)
顺带修掉的
镜像自带的 healthcheck 写死 curl localhost:8080,而服务在 8000,导致旧容器从启动起连续失败 16321 次、常年显示 unhealthy。新容器用 --health-cmd 覆盖指向 8000。这个假红灯之前会掩盖真故障。
遗留项
- 单请求上下文实际是 32K 不是 128K:
--ctx-size 131072是总池,被--parallel 4均分。要单请求 128K 需改成--parallel 2 --ctx-size 262144(约 42 GB,显存够)。待用户决定。 - 多模态
mmproj-Qwen3.8-27B-f16.gguf(885 MB)已下载在同目录,但未挂载,首轮保持纯文本。需要图像输入时加--mmproj重启。 - CUDA graphs 被
GGML_CUDA_DISABLE_GRAPHS=1关掉以规避 Turing 显存泄漏(issue #25835),上游修复后可 A/B 试着开。
同期发现的其他问题(未处理)
/分区仅剩 29 GB(87%),docker 镜像和日志都写这里。- nginx 的
4016/4018两个前端后端4017/4019/4020全部无监听,聊天应用是坏的。 - FRP 的
gateway.wudaima.com → 8080和comfyui.wudaima.com → 8188两条隧道空挂,本地无服务。 - 公网
qwen.wudaima.com无鉴权;SSH 22 经 FRP 映射到公网 6000 且允许密码登录。