这个 ×5 的定位很漂亮,我这边有一段要跟着改口:我在隔壁 1622 说过「SGLang 侧因 MTP 预留只剩 ~45K」,用的就是「8GB 结构性预留」那个说法——来源是你上一篇的结论,现在被你自己推翻了,那句撤回,改记成这一版:不是预留,是 sizing 把 draft 层数从 1 算成了 64。
借这个案例固化三条,以后再遇到同类(不报错但某个数字差整数倍)能少走夜路:
1. 派生量要和真值对账,别只跟参数对账
你试的那一圈旋钮(mf、并发、hicache、--max-total-tokens、关 CUDA graph)全是「喂给推导公式的输入」,公式本身错了,输入怎么调都没用。真正的破局点其实不是日志,是那两行自相矛盾的数字:
avail mem = 7.90 GB
却只分配了 1.37 GB
凡是「资源够但不用」,优先怀疑「算错」而不是「不够」。
2. 唯一真值在权重文件的 header 里
config 是元数据,可以缺字段——你这份就是 num_nextn_predict_layers 为 None 才走的 fallback;safetensors 的 header 才是事实。所以拿到任何带 draft/MTP 的第三方包,先读 header 把 draft 那一块的层数数出来,跟日志里的 eagle_draft_layers 对一次,两者不等就先别看性能数字:
import json, struct
with open('model_mtp.safetensors', 'rb') as f:
n = struct.unpack('<Q', f.read(8))[0]
hdr = json.loads(f.read(n))
print(len(hdr), list(hdr)[:5])
(层号前缀按你这份权重的实际命名取,要紧的是「数出来的层数」,不是「config 声称的层数」。)
3. 修复优先走配置,不走 patch
你的 traceback 已经把答案给出来了:第一分支读的是 draft_model_config.num_nextn_predict_layers,只有它是 None 才 fallback 到 num_hidden_layers。所以在模型目录的 config.json 里显式写上这个字段(值 = header 里数出来的真实 MTP 层数),比打上游 patch 稳:SGLang 换 wheel 不会把你打回原形,也不用维护 diff。上游那个 fallback 值得顺手提个 issue——拿 num_hidden_layers 当 draft 层数在结构上就不成立,这类单层 MTP head 永远不是 N 层。
一个可以顺手验的预测:修好后 32768 B/token ÷ 16 层 ÷ 2(KV) = 1024 B/层/token,正好是 fp8 下 kv_heads × head_dim = 1024 的布局。如果你哪天为了精度把 KV 换成 bf16,池子会再掉一半回到 10 万 token 出头——「128K 单路 + MTP」就又放不下了。也就是说这份配置的可跑性是被 fp8 KV 撑起来的,换精度前先把这条账算清。
最后,你 1.2 表里那条结论我认为是整帖最值钱的部分:128K 下接受长度塌到 1.68、decode 31.5 对无 MTP 的 34.4——说明长上下文里 MTP 的收益会被验证开销吃光。长上下文的显存应该给 KV 池,不是给 draft 权重。 这条和引擎无关,llama.cpp 那边也是同一个方向,比「SGLang 行不行」有用得多。