雑談:这两周有六个数字骗了我


这两周我给家里那台跑本地 35B 的旧台式机加了两个能力:作曲和画图。都跑通了,手机上跟 Telegram 里的助手说一句就出东西。

但这篇不打算写怎么装。装的过程没多少可说的 —— 下权重、写个 MCP 工具、注册进 agent,就完了。

值钱的是另一件事。我数了一下,这两周至少有六个数字骗过我。 它们都看起来很权威:官方默认值、系统自报的硬件规格、别人转述的限制。但每一个都以不同的方式不可信。

202 秒

先说最打脸的。

我装了 FLUX.2 [klein],一个 40 亿参数的图像模型,能生成也能按指令改图。第一次测试跑通了两条路:生成一张图 22.6 秒,改一张图 202 秒。

我当时的反应是“改图比生成贵九倍,合理” —— 改图要先理解原图嘛。

然后我看日志,才发现原因蠢得多:

生成    我传了 num_inference_steps=4     4 步
改图    我没传这个参数,用了默认值        50 步

我只在生成那次写了步数,改图那次忘了写。默认值 50 是从上游库继承的。

于是我把 4 步和 8 步的改图结果跟 50 步的放在一起看。看不出差别。 猫的毛色、姿势、桌面阴影,三张图里完全一致。

 4 步   ~22 秒
 8 步   ~37 秒
50 步   202 秒     ← 默认值

这个模型是蒸馏过的 —— 它的设计点就是 4 步。日志里其实一直在提醒我:Guidance scale 3.5 is ignored for step-wise distilled models。它连引导强度参数都不接受,我却以为它会认真跑 50 步做出更好的图。

默认值不是为你的模型准备的,它是为上游那个通用库准备的。

72 小时

过了几天,我发现服务全挂了。

Ollama 没了,网页版没了,编程 agent 也没了。但机器没重启 —— 开机时间显示 84 小时。日志里没有崩溃痕迹,没有 OOM,没有堆栈。

这个症状很有迷惑性:服务在同一时刻集体消失,而机器活得好好的。 我第一反应是内存不够被杀了。

查任务状态才看到真相:

OllamaServe      lastRun=9/3 22:10:03   lastResult=267014
OpenWebUIServe   lastRun=9/3 22:10:03   lastResult=267014
DshServe         lastRun=9/3 22:10:03   lastResult=267014

267014 是 SCHED_S_TASK_TERMINATED —— 被终止,不是异常退出。三个任务的启动时间完全相同,说明不是各自崩溃,是同一个外部计时器到点了。

开机 9/3 22:10,加 72 小时 = 9/6 22:10。

原因在任务定义里:我用 schtasks /create 建的任务,没有显式设置 ExecutionTimeLimit,这个字段就根本不会写进 XML。于是 Windows 套用默认值 —— 3 天。到点掐掉。

<!-- 我以为的:没写就是没限制 -->
<!-- 实际的:没写就用默认 P3D -->

这是第二种骗法:“没有配置”不等于“没有限制”。 缺失的字段有它自己的默认值,而那个默认值可能是个定时炸弹。

顺带说,当时我第一个怀疑的是另一行设置:

<StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>

“切到电池就停止任务”。听起来很像。但查到这台机器无电池设备、机箱类型是台式机,这条就排除了 —— 它不可能触发。

这次排除很重要。 如果我没排除就动手改它,服务照样会在 72 小时后死,而我会以为自己修好了。

(不过我还是把它关了。因为我打算买 UPS —— UPS 接上之后 Windows 会把它识别成电池,停电那一刻这个默认值会主动停掉所有服务,正好是最不该停的时候。)

64 GB

然后我想加内存。先问机器自己能吃多少:

Win32_PhysicalMemoryArray.MaxCapacity = 67108864 KB = 64 GB

清楚了,64GB 上限。我就照这个给自己做了规划,还挺认真地对比了 4×16GB 和 2×32GB 两种买法。

结果是错的。联想官方的产品规格书(PSREF)写的是:

Max Memory: Up to 128GB DDR4-3200 Memory Slots: Four DDR4 UDIMM slots

128GB,是我查到的数字的两倍。

为什么会差这么多?因为那个 WMI 查询的原理不是“探测内存控制器的能力”,而是读 BIOS 里 SMBIOS 表的 Type 16 记录 —— 一个厂商写死的静态数值。

品牌机厂商在这里填的是“我验证过并且愿意承诺的容量”,不是“这块板子的物理上限”。2021 年这台机器出厂时最高配可能就是 32GB,研发测到 64GB 就停了,于是 BIOS 里填 64GB,规避未验证容量的售后风险。

这是第三种骗法:它报的是承诺,不是能力。 而我把一个保守的商业承诺当成了物理约束。

这个错误的代价是实际的 —— 差别正好卡在我想做的事上:

35B + 图像模型 + 系统   42.5 GB   → 64GB 够
再加视频模型(权重 36GB) → 只有 128GB 撑得住

我前面因为内存不够放弃了视频模型。基于 64GB 这个假数,那个放弃是“没办法”;基于真实的 128GB,那是“可以重新考虑”。

300 秒

作曲那边也有一个。

MiniMax Music 3 的参数里有 duration_sec,我请求 300 秒。跑了 11 分钟,出来的音频是 144.6 秒。

不是失败,退出码 0,文件完整。就是只有一半长。

原因:duration_sec 是上限,不是目标。歌词唱完歌就结束,参数只管“最多多长”。我给了三段主歌三段副歌加一个桥段,它唱了 144.6 秒就没词了。

这是第二次撞上同一件事 —— 之前请求 60 秒出了 23.4 秒,我当时以为是模型不稳定。

参数名不会告诉你它是 cap 还是 target。 现在我的工具里改成了读输出文件的真实时长回报,而不是把请求值抄回去 —— 因为抄回去就是在对用户说谎。

120 秒

这个不是机器骗我,是人。

有人告诉我 MCP 协议的工具调用上限是 120 秒,超过会被截断或者不稳定。听起来很合理 —— 我的音乐生成动辄好几分钟,正好该担心这个。

我去读了源码:

_DEFAULT_TOOL_TIMEOUT = 300          # 默认 5 分钟,不是 2 分钟

def _resolve_tool_timeout(config):
    per_server = config.get("timeout")
    if per_server is not None:
        return per_server            # 直接返回,没有任何封顶

默认 300 秒,而且每服务的配置值原样生效不裁剪。全文搜不到 120,也没有任何截断逻辑。

而且实测直接证伪:那次 300 秒的音乐任务跑了 664 秒才返回,一切正常 —— 是 120 的五倍半。

有意思的是,“过长会不稳定”这半句其实有道理,但原因完全不同:真正的瓶颈是显存,不是超时。我之前遇到的中断是模型没卸载就生成导致的爆显存。

一个正确的现象配上一个错误的解释,比纯粹的错误更难纠正。 因为你会拿现象去印证解释,然后觉得自己验证过了。

2667 MT/s

最后一个,还没完全查清。

我的四条内存标称都是 3200,实际跑 2667。我当时的推论是:这是品牌机,BIOS 锁了,没有 XMP 选项。

后来看到联想官方文档明确提到 BIOS 里有 XMP 配置文件,还说:

若使用非 XMP 内存或 BIOS 中未开启内存超频选项,系统将默认以 2133MHz 或 2400MHz 等基础频率运行

2133 或 2400。我的机器跑的是 2667,两个都不是。

所以 2667 不是“XMP 没开”的特征。回头看内存配置:

ChannelA-DIMM0   Corsair  ← 一套
ChannelA-DIMM1   Lenovo   ← 另一套
ChannelB-DIMM0   Corsair
ChannelB-DIMM1   Lenovo

两套不同品牌混插,而且每通道插了两条。这两个条件各自都会压频率:混插导致 XMP 配置文件不一致无法套用,每通道两条(2DPC)则是内存控制器本身的降频。

我原来的推论 —— “读不到 BIOS 设置,所以大概没有这个功能” —— 是个很糟的推理。读不到只说明我用的那个接口是空壳(Lenovo 的 WMI 类在消费级主板上返回 255 个一模一样的占位记录),不说明功能不存在。

顺便:一个不是数字的教训

写工具的时候还撞到一件事,跟数字无关但更有意思。

音乐和图像是同一个问题:模型太大,和常驻的 35B 不能共存,必须轮换。我给音乐工具写的方案是每次调用都卸载 35B,算完再装回来 —— 切换要 28 秒,但音乐生成本身两分钟以上,这点开销无所谓。

给图像工具我就直接照搬了。然后发现很荒唐:

出一张图      22 秒
卸载+重载 35B  28 秒     ← 比干活本身还贵

连出十张图要付十次 28 秒。

所以图像这边必须反过来:常驻一个 worker 持有模型,空闲三分钟后自己退出并把 GPU 还给 35B。 连续出图只付一次切换。

同一个约束,同一台机器,两个相反的正确答案。差别只在一个比值:干活的时间 vs 切换的时间。

我当时差点没注意到,因为音乐那套“跑通了”。能跑通的方案是最难被质疑的方案。

所以

六个数字,五种骗法:

数字 它假装是 它其实是
50 步 这个模型的推荐值 上游通用库的默认值
72 小时 (不存在的限制) 缺失字段的默认值
64 GB 硬件物理上限 厂商愿意承诺的容量
300 秒 目标时长 上限
120 秒 协议规定 别人的转述
2667 XMP 缺失的证据 混插 + 2DPC 的结果

要说有什么共同点,大概是:这些数字都不是测出来的。

默认值是别人替我填的,自报规格是厂商替我承诺的,协议上限是别人替我转述的,而 2667 是我替自己解释的。六个里面没有一个来自“我在这台机器上跑了一遍看结果”。

我这两周真正做对的事只有一件:每次都去测了。4 步和 50 步的图放一起看,300 秒的请求去读输出文件的真实时长,120 秒的说法去读源码再跑一次 664 秒的任务。

测一次的成本通常很低 —— 对比两张图花了不到一分钟。而不测的代价是把一个错的数字写进配置,然后在它上面做几周的决策。