MultimodalFlow
← 返回博客

边缘结构化决策实测:Thor 上的 27B 准确率 0.958 反超云端 Jev 的 0.883,校准却输了

Jetson Thor约束解码结构化输出vLLMTypeSafe Jev模型校准边缘推理ECE

"结构化决策"这类活——把一段文本归到有限几个选项里、并给出置信度——正在变成一个独立的模型品类。TypeSafe 把它叫 System One,旗舰模型 Jev 只做这件事:输入状态和带选项的问题,输出类型化答案加概率,不生成散文。

那么一个自然的问题是:这种活非得上云吗?边缘设备上的开源模型配上约束解码,能不能干同样的事?

我用 Jetson AGX Thor 和 Jev 做了一次对比。结论比预期有意思,但最有价值的部分不是那张跑分表,而是我在过程中踩到的三个坑——它们都能让约束解码悄悄失效,而评测数据看起来完全正常。


测试设置

云端边缘
模型TypeSafe Jev 1.13.0Qwen3.5-27B / Qwen2.5-7B-Instruct
硬件厂商 serverlessJetson AGX Thor,122 GB 统一内存
系统JetPack R38.2.2,Linux 6.8.12-tegra
推理栈官方 HTTP APIvLLM 0.19(Jetson Thor 官方镜像)
精度未公开BF16
计费$0.042 / 1M 输入 token,输出免费一次性硬件 + 电费

任务集是 24 条工业设备故障报告,要求路由到三个维修组之一(机械 / 控制 / 安全)。参考答案由领域负责人逐条人工标注,不是用大模型共识生成的——后者等于在测"谁更像大模型",而不是"谁更对"。

公平性上做了三件事:

  • 本地模型收到的 criteria 描述文本和 Jev 逐字相同。只给裸问题不给选项描述,那是 prompt 内容上的不公平,不是能力差距。
  • 每题重复 3–5 次,准确率旁边并列报翻转率(同一题多次回答是否一致)。
  • 云端延迟包含网络往返并明确标注。这是选型时的真实数字,藏起来没意义。

校准指标一律用预测选项上的概率质量,不用厂商的 confidence 字段——这两个不是一回事。Jev 在一道势均力敌的题上返回 top 概率 0.46、confidence 只有 0.19,混用会变成拿启发式去比概率分布。


主结果

准确率翻转率ECEBrierp50 延迟
Jev 1.13.0(云)0.8830.0420.0990.1641141 ms
Qwen3.5-27B(Thor)0.9580.0000.1490.134873 ms
Qwen2.5-7B(Thor,约束生成)0.6250.0000.3670.725295 ms
Qwen2.5-7B(Thor,似然打分)0.6250.0000.1950.574415 ms

边缘的 27B 拿下准确率、Brier、延迟三项,唯独输掉 ECE

这个分布很说明问题。校准正是 TypeSafe 宣称的核心卖点,实测站住了:Jev 答对的比例更低,但它更清楚自己什么时候没把握。 边缘模型更准,却更容易在错的时候也很自信。

对选型的实际含义:

  • 如果你要做置信度分流(低置信度转人工复核),校准比准确率重要,云端仍有优势。
  • 如果你只取 argmax 直接用,边缘 27B 已经全面更优。

另外 Jev 不是随机的,但方差集中在决策边界上:同一道不含糊的题连发 12 次,12 次概率向量完全一致;换成一道势均力敌的题,12 次里出现 11 个不同的概率向量,答案在两个选项间来回翻。只报单次准确率会把这件事完全盖住。


坑一:vLLM 静默丢弃 guided_choice

这是最贵的一个教训。

vLLM 0.19 把约束生成的参数从 guided_choice 改名成了 structured_outputs旧写法照单全收,然后完全忽略。 不报错、不警告。

判决性测试很简单——给一组模型绝不可能自己说出来的选项:

# guided_choice(旧写法)
{"guided_choice": ["zorblax", "quixnar"]}
# → 'Thinking Process:\n\n1.  **Analyze'      约束没生效

# structured_outputs(新写法)
{"structured_outputs": {"choice": ["zorblax", "quixnar"]}}
# → 'quixnar'                                  生效

危险在于失效时的表现和正常时一模一样。模型只要自己听话,输出就全是合法选项、schema 合规率 100%、准确率也在合理范围。任何只检查"输出格式对不对"的验证都发现不了。

我最初在 Qwen2.5-7B 上跑了一整轮,数据看起来毫无问题。后来用正确参数重跑:

准确率ECEBrier
约束失效(旧参数)0.6250.3660.723
约束生效(新参数)0.6250.3670.725

几乎一模一样。 因为 Qwen2.5-7B 不是推理模型,prompt 让它答一个选项名它就答一个选项名,约束开不开都一样。

这推出一条对所有人都适用的结论:用小的、听话的模型验证约束解码是否生效,验不出来。 这个 bug 是换上啰嗦的推理型模型才暴露的。

现在我的脚本在跑约束模式前会先发一个哨兵请求,选项是无意义词;回来的结果不在选项里就直接中止,拒绝产出"看起来正常"的废数据。


坑二:thinking 模式和首 token 约束正面冲突

暴露坑一的那个模型,紧接着给了第二个坑。

Qwen3.5-27B 是推理型模型,chat template 默认把它置于 thinking 模式。约束生成要求第一个 token 就是选项词。这两件事直接冲突:模型被配置成"准备展开推理",却被强制立刻吐出结论。

代价:

27B 配置准确率
约束生成,thinking 开(默认)0.583
约束生成,enable_thinking: false0.958

一个 flag,37.5 个百分点。 而且不生效时不报任何错。

这个 0.958 有独立交叉验证:同一个模型经 ollama(自由文本解析、think: false)跑出来也是 0.958,两条完全不同的路径给出同一个数字。

排查过程值得记一下,因为我连续否掉了两个错误假设:

  1. "约束剥夺了推理空间" —— 否。ollama 那轮只输出 3 个 token 就拿到 0.958,它根本没在推理。
  2. "下错成基座模型了" —— 否。chat template 和 generation_config 都是指令版配置。
  3. thinking 模式与首 token 约束冲突 —— 单条验证通过,全量重跑确认。

如果你在推理型模型上做约束解码,必须显式关掉 thinking 模式。否则你会以为是模型不行。


坑三:ollama 对部分模型静默忽略 format

同一类问题的第三个版本。ollama 的 format 参数在 qwen2.5:7b 上正常工作,在 qwen3.5:9b被完全忽略——不报错,返回自由文本。

qwen2.5:7b  + format → {"department": "sales"}        生效
qwen3.5:9b  + format → "Here are a few options for..."  忽略

约束解码的支持情况是按模型算的,不是按框架算的。 换模型就得重验。


似然打分有结构性的类别先验

除了约束生成,另一条拿概率的路子是 rank classification:把每个选项接在 prompt 后面各做一次前向,取整段序列的对数似然,在选项间归一化。理论上这给出真正的 P(选项 | 状态),语义上和 Jev 的 probabilities 字段最对齐。项目一开始我认为这是"更正确"的路径。

实测把这个判断推翻了。

这个方法在 24 题、120 次推理里一次都没有预测过"安全"这个类别——6 条安全类标注,命中 0 条。

我试了三种修法,全部失败:

修法准确率预测"安全"次数
裸选项(基线)0.6250 / 24
长度归一化0.6250 / 24
把选项描述并入被打分的候选0.5421 / 24
改写 criteria 让定义更明确0.5422 / 24

第三种修法不但没用,还把偏向从"控制"换成了"机械",并且延迟涨到 3.7 倍(1538 ms)——已经比云端还慢,边缘唯一的优势被抹掉了。

机理其实很清楚:rank classification 里所有 criteria 描述都在共享前缀里,被比较的候选之间唯一的差异是那个裸选项词。 所以描述写得再清楚也进不到最终的比较里。这个方法结构上就没在用 schema。

结论反过来了:边缘侧该用引擎原生的约束生成。 它的概率是从生成 token 重构出来的、不够"干净",但一个概率干净却系统性答错的方法没有用。


schema 文本是精确率/召回率旋钮

最初的"安全"类定义是按器件归属写的(安全回路、互锁、光幕、急停链),而"机械"和"控制"是按失效机理写的。于是"安全器件上发生的机械故障"在定义上就是二义的——比如防护门开关拨杆弯了。

把安全类的定义放宽,明确覆盖"安全评级器件上的任何故障,无论失效机理":

原定义改写后变化
Jev0.8830.903+2.0
Qwen2.5-7B 约束生成0.6250.708+8.3
Qwen2.5-7B 似然打分0.6250.542−8.3

改写修好了目标那条,同时把一条相邻的"控制"类样本吸进了"安全"

所以准确的说法是:放宽一个类别的定义,会召回它的漏报,也会吸进它的邻居。这不是免费的提升。 而且它对似然打分那条路完全无效——原因见上一节。


成本:不存在平衡点

这是大多数"边缘 vs 云"对比算错的地方。

云端按输入 token 计费,而每次调用有约 344 token 的固定开销,每多问一个问题只加约 30 token。也就是说云端的正确用法是把多个决策打包进一次调用:

云端打包方式云端 $/1M 决策
1 问 / 次调用$15.71
3 问 / 次调用$6.08
10 问 / 次调用$2.70

边缘这边,单板 27B 在 873 ms/决策下的理论上限是 98,969 次/天。按 $3499 三年摊销、$0.10/kWh、实测 78.1 W 算,满负荷成本是 $34.18 / 1M 决策——这已经是边缘的理论最优。

即使跑满,边缘也比云端最贵的配置贵 2.2 倍、比最优配置贵 12.7 倍。而且加板子成本线性上涨,差距不会收敛——不存在平衡点。

瓶颈不是电费也不是硬件价格,是单板吞吐太低。

如果按"一个决策一次云端调用"计费(边缘最有利的算法),会把云端成本高估 6 倍,从而算出一个假的平衡点。

边缘的理由是延迟、隐私、离线,不是省钱。 这个结论比构造一个成本优势要诚实,也更有用。


功耗:云端没有的维度

在安静的机器上实测(Jetson 的 tegrastats,27B 约束生成 + 关闭 thinking):

  • 静默基线 21.6 W
  • 负载 78.1 W
  • 64.6 J / 决策(含整板待机)
  • 46.8 J / 决策(仅增量)

一个采样上的局限要标注:59.6 秒只拿到 47 个样本,实际间隔约 1.3 秒而非请求的 200 ms——tegrastats 经 ssh 管道有缓冲。均值可用,但不适合画瞬时功率曲线。

云端没有对应的数字可比,所以这项作为边缘独有的一栏单独呈现,不混进对比表。


Thor 上的其他运维坑

顺手记几条,都是真踩到的:

  • 统一内存在容器退出后不会立即回收。 free 报 96 GB 已用,而进程实际 RSS 只有 3 GB,导致下一个容器启动时被显存检查直接挡住。分配一块大内存再释放可以逼内核回收(可用从 25 GB 回到 60 GB)。
  • vLLM 的启动显存探测会被同机进程搅崩。 别的进程在探测过程中释放内存,就会触发 AssertionError: Error in memory profiling。重试通常能过。
  • 权重分片可能是残缺的,而且只在引擎启动时才暴露。 本地一份 Qwen2.5-7B 有个分片只有 246 MB(应为 3.86 GB),报出来是 SafetensorError: incomplete metadata。下完对一遍 model.safetensors.index.json 里的 total_size
  • 小心打到错误的端点上。 Thor 的 ollama 只监听 localhost,得开 ssh 隧道;而我的 Mac 本机 11434 跑着自己的 ollama、11435 还有个返回 gpt-4o 的 shim,两个都会像模像样地应答。跑基准前先校验模型列表确认对端是谁。
  • 本机 HTTP 代理会吞掉发往局域网的请求,返回 502。脚本里直接禁用代理最省事。

结论

回到最初的问题:边缘能不能做 System One?

能,但要看你在意什么。

  • 准确率:27B 在 Thor 上 0.958,超过云端的 0.883。
  • 延迟:873 ms vs 1141 ms,边缘赢,而且这还没算云端的网络抖动。
  • 校准:0.149 vs 0.099,云端赢。专门为结构化决策训练的模型,在"知道自己不知道"这件事上确实更好。
  • 成本:云端赢,而且不存在平衡点。
  • 隐私 / 离线:云端 API 永远给不了。

如果你的系统靠置信度做分流,校准是硬指标,云端目前仍有位置。如果你只要 argmax、且在意延迟或数据不出厂,Thor 上的 27B 已经够用甚至更好。

但真正要带走的是另一件事:约束解码有多条静默失效路径,每一条都能让你的评测数据看起来完全正常。 我在这个项目里踩了两条,都是靠交叉验证才发现的。上线前请用一组模型绝不可能自己说出来的哨兵选项,验证约束真的在生效。


没做的部分

量化一档都没跑。 最初的研究问题——"量化会不会毁掉概率校准"——在这一轮里完全没被触碰,BF16 的 27B 结果正好是它的参照基线。

这个取舍是有意的:在一个静默失效的约束解码上测量化档位,测出来的全是噪声。先把测量本身做对,比多铺几个档位重要。下一轮再说。