Skip to content

feat: add mtp draft models for speculative decoding - #588

Open
accelerator-llc wants to merge 7 commits into
InfiniTensor:InfiniLM-v0.2.9cfrom
accelerator-llc:feat/mtp-draft-models
Open

accelerator-llc wants to merge 7 commits into
InfiniTensor:InfiniLM-v0.2.9cfrom
accelerator-llc:feat/mtp-draft-models

Conversation

@accelerator-llc

@accelerator-llc accelerator-llc commented Sep 20, 2026 •

Copy link
Copy Markdown

MTP draft models for InfiniLM speculative decoding

结论摘要

  • 做了什么与意义:给 InfiniLM 的投机解码框架新增一类 MTP draft(qwen3_5_mtp:Qwen3.5,混合线性注意力;mimo_mtp:MiMo-7B-Base,纯全注意力)——draft 直接用 target checkpoint 内嵌的 MTP 头,不需要另行训练、不需要额外 draft 权重文件,第二个家族作为独立证据接入。基线只支持 EAGLE(独立 draft checkpoint)这一条硬编码路径,本 PR 把它改成描述驱动的注册表,使"checkpoint 内嵌 MTP 头"成为仓库可直接接入的一类 draft:新家族只需一份描述与一个注册项(该步 runner 文件改动 0 行),并由仓库的结构性检查长期维持;交付的是一条可复用的接入通道加两个真实家族的验证,而不只是单个模型的适配。
  • 结论:K=1 且 batch=1(一次只处理一个请求)下,开启投机与关闭投机的输出逐 token 一致——平台与模型的对应关系为:Qwen3.5-0.8B 在本地 4060、2B 在 4090D 与 C500、9B 与 MiMo-7B 在 C500,均已实测通过(逐平台状态见 §5)。加速比各 K 档均 <1.0(最高约 0.93×,C500 9B K=1),故不承诺加速收益;EAGLE 的判定结果未被改动(静态等价 + 运行期 A/B 佐证,见 §4.3)。
  • 两项对本 PR 不利的读数,其成因都不在本项工作的接入方式上:① 收益为负归因于多 token 验证的形状成本与接受率/验证开销——纯全注意力 target 去掉形状成本后仍为负,残余不在接入方式(机制解释,非逐位置内核级归因,见 §4.2 与 §6);② K≥2 的逐 token 翻转来自模型自身的 bf16 精确并列与多 token 验证内核路径——关闭投机的纯贪心解码在两个平台上本身就取了不同分支,且该单点差异与状态一致性无关(见 §4.1)。K≥2 不在无损主张范围内,其数据只作机制性能参考。这些读数都取自单卡上的小规模载体(本地 8 GB 的 0.8B、云端的 2B / 9B / MiMo-7B):前向本身快、显存受限,rollout 与验证的固定开销因此压不过收益。
  • 反例对照与数值对齐的实测证据:反例对照已实测——语义错例与映射对调全部按预期偏离参考、装载表项缺失时装载失败、真实多层 draft checkpoint(发布 2 个深度而块只组合 1 个)在构造期被拒绝并给出具名原因、未进入生成;C500 上真权重 MiMo-7B 的权重位精确比对 max_abs_diff = 0.0。逐项明细与证据出处(含本机重跑 / C500 实测的划分)见 §4.1 与 §5。
  • 变更构成与改动面:相对上游基线 41 文件 / +11,244 −27——功能代码 21 文件 / +2021 −27、文档 2 文件 / +340、测试附件 18 文件 / +8,883(清单与运行方式见附录 A、明细见 §1bis)。含一处改变既有路径的框架级判定逻辑(runner 的 draft 准入判据由硬编码改为注册表描述)、3 处既有模型文件的 C++ 改动(其中一处改变 forward 的输出接口)与一个新接入的 target 模型架构(MiMo-7B)。
  • 可测范围:8 个 CPU 测试文件(136 条测试)无需权重与 GPU、可直接执行;0.8B 端到端与基准、2B 前向数值对齐需对应权重与单卡 GPU;C500 与 4090D 的读数需要对应平台;云端批次读数与 EAGLE 零影响的运行期 A/B 只有我方归档(产生该读数的脚本不在本 PR 内)。分类与前提见 §1bis。
  • 两条最容易被误读的边界:① CPU 上没有端到端投机(paged KV cache 无 CPU 后端);② deepseek_v3 / deepseek_v32 在本构建里不可构造(无对应 C++ 块),只落到描述层。
  • 术语:checkpoint = 模型权重目录(含 config 与分片权重文件);MTP = multi-token prediction,checkpoint 内嵌的多 token 预测头,本项目把它当 draft 用;K = 每个验证轮一次投出的 draft token 数;自然档 / 强制档 = 让模型自行决定接受与否 / 由测试固定接受结果(含部分接受、全接受两条子路径)。强制档在真权重上是默认运行自带的一段(该脚本没有 --deterministic-partial 一类需要显式打开的开关);组件级 / 端到端 = 只测 draft 前向、权重映射与描述解析 / 跑完整的生成流程;同时驻留 = target 与 draft 在同一张卡上同时驻留显存;target / draft = 被验证的主模型 / 提出候选 token 的投机模型;验证轮 = 一次把本轮 draft token 交给 target 验证的过程;接受率 = 被接受的 draft token 数 ÷ 投出的 draft token 数。

1. 项目内容

  • 做成的事:MTP draft 模型与内嵌权重加载;speculative_runner 接线;家族接入机制(一份 MTP 头描述 + 一个注册项);与模型无关的状态一致性基础设施;测试与基准设施(含仓库自带 examples/bench.py);接入文档(MODELS.md §5、§6;MODELS_ZH.md 声明英文版为权威版本)。
  • 验证载体与新增范围:两个真实家族——Qwen3.5(0.8B / 2B / 9B,混合线性注意力)与 MiMo-7B-Base(纯全注意力),平台覆盖 NVIDIA(本地 RTX 4060 8 GB、云端 4090D 24 GB)与沐曦 C500 64 GB,另含 CPU 侧组件级验证;MiMo-7B 是本 PR 新接入的 target 模型架构(不是 draft 侧工作,见 1bis)。
  • 明确不做:不为"所有带 MTP 头的模型"背书(各家族头结构确有差异,实测只覆盖这两个家族与平台);未实测项、加速收益与 EAGLE 零影响的边界见 §6、§4.2 与 §4.3。

1bis. 变更构成与可测范围

本 PR 的改动相对上游基线:41 文件 / +11,244 −27(git diff --numstat)。按类别:测试 18 文件 / +8,883(占 +11,244 的 79%)、代码 21 文件 / +2021 −27(csrc/** +583 −13、python/** +1437 −14、examples/bench.py +1)、文档 2 文件 / +340(MODELS.md +338、MODELS_ZH.md +2)。测试是随 PR 提供的附件:其中 8 个文件(4,429 行)可在无权重、无 GPU 的条件下直接执行(共 136 条测试,秒级),其余 10 个需要 GPU(其中两族的合成 checkpoint 档自建权重、不需外部权重;其余需真实权重,部分还需 target+draft 同时驻留,即 ≥12 GB 显存)——清单、覆盖与运行方式见附录 A。功能改动集中在新增的 draft 描述层(python/infinilm/draft_spec.py +1046)与 5 个共享文件的接线(合计 +391 −14)。

类别 内容
新增(target/draft 模型) csrc/models/qwen3_5_mtp/(+174)、csrc/models/mimo_mtp/(+304);csrc/models/mimo/(+63,新的 target 架构,基线不存在)
新增(机制) python/infinilm/draft_spec.py(+1046):家族描述、注册表、权重映射与 target 侧派生
新增(测试附件) test/models/qwen3_5_mtp/ 10 个 + test/models/mimo_mtp/ 8 个 = 18 个文件(清单与覆盖见附录 A)
修改(共享文件与既有 C++ 文件,共 13 个) Python 5 个:python/infinilm/llm/model_runner/speculative_runner.py +288 −10(含准入判据改写与告警措辞)、python/infinilm/modeling_utils.py +47 −2、python/infinilm/llm/cache_manager.py +33、python/infinilm/llm/scheduler.py +13 −2、python/infinilm/infer_engine.py +10。csrc/ 下既有文件 8 个:engine/infer_engine.cpp +6 −2、pybind11/engine/engine.hpp +8 −8、models/qwen3_next/qwen3_next_gated_deltanet.cpp +17 −1、global_state/forward_context.hpp +3、models/infinilm_model.hpp +3、engine/rank_worker.hpp +3、models/qwen3_5/qwen3_5_for_causal_lm.cpp +1 −1、models/qwen3_5/qwen3_5_attention.cpp +1 −1(后三个的性质见下条)
修改(文档与示例) MODELS.md +338、MODELS_ZH.md +2(英文版被声明为权威版本);examples/bench.py +1(补传已配置的 dtype,见 4.2)

唯一一处被改写的框架级判定逻辑:speculative_runner.py 里 draft 类型的准入判据,由硬编码的白名单(draft_model_type != "minicpm_eagle" 即 RuntimeError)改为注册表描述查询,minicpm_eagle 也随之成为一条描述;其行为等价性由静态等价与运行期 A/B 佐证(见 4.3)。

既有模型侧的 C++ 改动(3 个文件,本 PR 侵入面的其余部分):① csrc/models/qwen3_5/qwen3_5_for_causal_lm.cpp:forward 返回值由 {logits} 改为 {logits, hidden_states} —— 既有模型的输出接口变化(该双输出约定基线已由 minicpm4 / minicpm_eagle / kimi_k3 使用;非投机路径同样走这条 forward);② csrc/models/qwen3_5/qwen3_5_attention.cpp:static attention 路径的 sigmoid(gate) 补 ->view(attn_output->shape()),即把 static 路径对齐到同文件 paged 路径早已有的写法;③ csrc/models/qwen3_next/qwen3_next_gated_deltanet.cpp:形状判据改为可显式声明的 is_single_token_per_request(...)(默认 false → nullopt → 沿用原判据 ⇒ 默认路径行为不变,属接口扩展)。②③ 的逐位回归对照未归档(本描述不含这两条路径的回归读数)。

本 PR 的改动性质:全部改动都落在上游基线之上,不依赖任何未合入的运行时、框架或构建改动,可独立合入——改动文件集只有 csrc/**、python/**、test/**、examples/bench.py 与两份 MODELS.md,不含构建脚本、依赖清单、子模块与 CI 配置,也不新增任何第三方依赖(见 §3.1)。除上条列明的 3 处既有模型侧 C++ 改动以外,其余共享文件的改动都是默认值=原行为的接口扩展:普通(非投机)推理路径不变;加载器表里既有的 11 条条目仍然优先命中、解析结果不变(本次只新增 qwen3_5_mtp 与 mimo 两条);KV cache 与状态行的既有分配策略不变(新增的是状态行的借出 / 归还 / 交换接口);调度器与引擎侧的改动是把已有信息多传一个参数,不改变既有调度与执行策略。例外一处,如实列出:deepseek_v3 / deepseek_v32 在基线加载器表里本就没有条目,本次为它们启用了注册表回退(装载时丢弃 checkpoint 内嵌的 draft 张量);该识别与映射层已扩展、未在真实权重上验证(见 §2 第 4 行与 §6 第 2 条)。

可测范围(第三方按 §3 能做到哪一步):可直接执行与验证——上句所列 8 个 CPU 测试文件(136 条,无权重、无 GPU,清单与命令见附录 A),以及 Qwen3.5-0.8B 的端到端无损与基准、2B 的 draft 前向数值对齐(需对应权重与单卡 GPU);需要对应平台——C500 的 2B / 9B / MiMo-7B 端到端与 batch、长序列、prefix caching、显存读数,4090D 的各项读数;只有我方归档、第三方无法独立重跑——云端各批读数(实例已释放、本描述不给出实例级标识)、EAGLE 零影响的运行期 A/B(产生该读数的脚本不在本 PR 内)、部分权重清单聚合值(见 §3.2),以及下列无随 PR 工件的读数与结论:C500 的 20 项基线对照条目与逐项判定、MiMo 合成权重的 f32 前向差值、K≥2 分歧的 1-ULP 定位与覆盖该现象的 21 次运行判据、同步口径低约 8% 的对比。前提:本机 8 GB 显存装不下 2B 的 target+draft 同时驻留(需权重或 ≥12 GB 显存的测试在本机不通过)。

2. 实现思路

两个家族的接入方式:

两个家族的 checkpoint 均内置 MTP 头,因此无需训练,也不需要额外的 draft 权重目录。主要工程量在于避免为每个家族单独编写代码:把该头的结构用描述表达——一份描述给出三项信息:结构组成、键名规范、如何依据 checkpoint 自身的 config 元数据识别该头——target 侧复用同一份描述的反向信息(内嵌 draft 层在正常加载时丢弃哪些键,同样由描述派生);代码中不出现任何家族名。混合线性注意力 target 的验证路径维护的是一份循环状态、无法像 KV cache 那样回滚,验证因此采用只向前推进的形式(机制见下表第 5 行)。

# 机制 要点
1 MTP 作为一种新的 draft 类型 结构由 checkpoint 自身的 config 元数据驱动(按发布的深度字段识别、层类型据此构造),不依赖模型名;draft 的词嵌入与输出头支持三种来源——与 target 共享、由 draft 自带、或来自独立的 draft checkpoint(每深度各备一份嵌入的形态会被显式拒绝并给出具名原因)
2 家族接入 = 描述 + 注册 接入一个新家族的 draft 侧只需新增一份 DraftModelSpec 描述 + 一个注册项;家族判定与权重映射全部由描述派生——接入第二个家族这一步,runner 文件改动为 0 行;runner 文件整体并非零改动(本 PR 相对基线 +288 −10,含下面第 3 条的准入判据改写与更早的两步接线);draft 解码块也只在本构建无法组合时才需要新增
3 准入判据改为描述驱动 基线的 runner 用硬编码的白名单判 draft 类型(非 minicpm_eagle 即 RuntimeError);本 PR 改为查注册表描述。minicpm_eagle 的判据与行为逐条不变(静态等价 + 运行期 A/B,见 4.3)——这是本 PR 唯一一处被改写的框架级判定逻辑(既有模型侧的 3 处 C++ 改动见 1bis)
4 target 侧:同一条描述作答 内嵌族的 target 装载由描述自带的键映射回答——把该族 draft 命名空间的键,以及这些键在本 shard 内定位到的 draft 层(整层,含该层复用的普通命名空间张量)一并列入丢弃集合,其余张量原样返回;不再需要为每个家族手写装载表项,表中已有条目仍绝对优先(既有家族行为因此不变)。装载逐 shard 派生,故定位粒度是 per-shard:某 shard 含 draft 层张量却不含该族任何特征键时,该次装载直接报错退出而非静默放行。deepseek_v3 / deepseek_v32 属这类内嵌族:识别与映射层已扩展,但本构建没有对应 C++ 块、无法构造,也未在真实权重上验证
5 状态一致性基础设施 混合 target 的线性注意力层维护循环状态、不能像 KV cache 那样回滚:显式声明批次形状 + 单 scratch 状态槽(验证读已提交行、写 scratch 行)+ 全接受槽号交换(O(1))+ 部分接受批形状重放。机制只消费 (K, accepted, 前缀),与 draft 类型、target 模型解耦;对没有循环状态池的 target 自动不启用,路径与不投机一致

MODELS.md §6 给出两个可复用示例(qwen3_5_mtp、mimo_mtp)与资格判据;英文版为权威版本这一声明写在 MODELS_ZH.md(该译本自述可能滞后于英文版)。仓库另有两项结构性检查长期维持这套约定(清单见附录 A)。

命名注:qwen3_5_mtp / mimo_mtp 既是 draft 描述与注册表的键、也是对应 C++ 目录名;而 target 侧的权重映射表键用的是 target 类型名(mimo、qwen3_5)。

3. 复现流程

命令与前提见 3.1、3.3,平台差异见 3.4,可核验数值见 §4。代码版本 = PR 页面给出的 head(其父提交链为上游基线状态;该标识由 PR 平台给出)。本机已实际执行的输出以 # → 标出;标 📋 的需对应平台。非一键脚本。

3.1 环境前置

项 NVIDIA(本地 4060 / 云端 4090D) 沐曦 C500
OS / 编译链 Ubuntu 24.04(x86_64)、gcc / g++ 13.3 Ubuntu 24.04.1 LTS、gcc / g++ 13.3
GPU 工具链与架构 本地:CUDA 13.3(/usr/local/cuda)、驱动 595.84、sm_89(RTX 4060 Laptop 8 GB);4090D:CUDA 12.8、驱动 570.124.06、sm_89(RTX 4090 D 24 GB) MACA 3.3.0.15 + KMD 3.8.30(无 CUDA toolkit、无 nvcc);MXC500 64 GB(mx-smi -L 枚举 8 个槽位,通常只有第一个 Available)
xmake 本地 3.0.9;4090D 3.1.1 v3.0.9(自包含 bundle;root 下需 XMAKE_ROOT=y),rpath 需 patchelf(视镜像而定,见 3.4 已知问题 6)
系统工具 patchelf(第 3 步重打 rpath 必需;获取:apt install patchelf,或 pip install patchelf(本项目把它列在第 0b 步的 PKGS 里)。装完须确认 which patchelf 非空 —— 本机实测 .venv/bin 内并没有它) 同需(配方见 3.1 末与 3.4 已知问题 6)
建 venv 的工具 uv 0.12.1(获取:curl -LsSf https://astral.sh/uv/install.sh | sh,装到 ~/.local/bin)。替代做法:python -m venv + pip(需 root 与网络,本项目未在本机验证该路径;命令见下方第 0b 步注释) 不需要(镜像自带 /opt/conda 与 torch;该镜像无 uv,也不必建 venv)
Python / torch CPython 3.12 + torch 2.14.0+cu130(本地)/ 2.14.0+cu126(4090D) /opt/conda/bin/python(CPython 3.10)+ torch 2.8.0+metax3.3.0.2
Python 依赖 必须显式安装(仓库不声明任何第三方依赖,editable 安装不会带入):torch、对照参考前向用的 transformers(5.17.0)/numpy/tqdm/safetensors/pillow,引擎与服务侧的 janus==2.0.0/msgspec/psutil/fastapi/uvicorn/pyzmq/pydantic/httpx/xxhash/urllib3,rpath 用的 patchelf,以及下载工具 modelscope(见 3.2)。安装命令见下方第 0b 步 镜像自带 transformers 4.57.3;必须补装 janus==2.0.0(仓库 4 处顶层 import janus,pyproject.toml 未声明)

本 PR 不新增任何第三方依赖;上表列的是运行本流程所需的既有依赖(含两处仓库打包缺口,见 3.4 已知问题 7 与本节 C500 行)。

# 0) 代码:clone 后切到本 PR 的内容(repo = 上游仓库;PR 目标分支 InfiniLM-v0.2.9c)
git clone https://github.com/InfiniTensor/InfiniLM.git && cd InfiniLM
git checkout <this PR's head commit>        # 即 PR 页面上给出的 head 提交

# 0b) Python 环境(必须先于第 1 步:InfiniCore 的构建要用 torch 自带的 cudnn 头/库;本项目用 uv 建 venv,见 3.1 表)
uv venv .venv
# 依赖必须显式安装(仓库不声明任何第三方依赖;包名/版本见 3.1 表"Python 依赖"行;国内网络用 `export UV_DEFAULT_INDEX=https://mirrors.aliyun.com/pypi/simple/`,直连 PyPI 拉 nvidia 大 wheel 实测会停滞)
PKGS="torch transformers numpy tqdm safetensors pillow janus==2.0.0 msgspec psutil fastapi uvicorn pyzmq pydantic httpx xxhash urllib3 modelscope patchelf"
uv pip install --python .venv/bin/python $PKGS
#     替代做法(无 uv 时;需 root 与网络,本项目未在本机验证该路径):
#     sudo apt install python3.12-venv && python -m venv .venv && .venv/bin/pip install $PKGS
export PATH="$PWD/.venv/bin:$PATH"                    # 之后所有 `python` 都指向该 venv(`patchelf` 见上表)
export CUDNN_INC="$PWD/.venv/lib/python3.12/site-packages/nvidia/cudnn/include"

# 1) InfiniCore v0.2.9(CPU + CUDA 后端;NVIDIA 侧)
git clone https://github.com/InfiniTensor/InfiniCore.git ../InfiniCore && cd ../InfiniCore
git checkout v0.2.9
git submodule update --init --depth 1 third_party/spdlog third_party/cutlass
export NVCC_APPEND_FLAGS="-gencode=arch=compute_89,code=sm_89"   # 见 3.4 已知问题 3
export LIBRARY_PATH="$HOME/.infini/lib${LIBRARY_PATH:+:$LIBRARY_PATH}"   # 不覆盖既有值:新路径在前,既有值保留在后
xmake f --cpu=y --nv-gpu=true --cuda=/usr/local/cuda --cuda_arch=sm_89 \
        --cuflags="-gencode=arch=compute_89,code=sm_89 -I$CUDNN_INC" \
        --includedirs=$CUDNN_INC -cv -y
rm -rf build                          # 见 3.4 已知问题 4:改配置必须强制重编
xmake -y && xmake install -y          # → ~/.infini/lib/ 下 4 个 .so
xmake build -y _infinicore && xmake install -y _infinicore
# ★ 必需:上面的 install 只把 pybind 模块放进**源树**(python/infinicore/lib/);让 `import infinicore` 生效的是 editable 安装(与上游 InfiniCore README 的配方一致):
uv pip install --python .venv/bin/python -e .
# 预期:build ok / install ok;架构自检
cuobjdump --list-elf ~/.infini/lib/libinfiniop.so | grep -c sm_89   # → ≥1

# 2) InfiniLM 构建与安装(`pip install -e .` 本身会触发 xmake build/install `_infinilm`)
cd ../InfiniLM
xmake build -y _infinilm && xmake install -y _infinilm
# 预期:`build ok`(首次数分钟)、`install ok!`;产物 python/infinilm/lib/_infinilm*.so
uv pip install --python .venv/bin/python -e .
# 替代做法(无 uv 时):.venv/bin/pip install -e .

# 3) ★ 运行期库路径(最高频问题,见 3.4 已知问题 1):每次 install / editable 安装之后都要重打;前提:`patchelf` 在 PATH 上(见 3.1 表"系统工具"行)
.venv/bin/python -c "import infinicore, infinilm; print('imports ok')"
# 预期:`imports ok`;若报 `libinfinicore_cpp_api.so: cannot open shared object file` → 执行下一条
for so in libinfinicore_cpp_api.so libinfiniop.so libinfinirt.so libinfiniccl.so; do
  patchelf --set-rpath '$ORIGIN' ~/.infini/lib/$so; done
patchelf --set-rpath "$HOME/.infini/lib:\$ORIGIN" \
  $(ls python/infinilm/lib/_infinilm*.so) $(ls ../InfiniCore/python/infinicore/lib/_infinicore*.so)

该步必需,不可省略:抹掉 pybind 模块的 RUNPATH 后 import infinicore 立即报 libinfinicore_cpp_api.so: cannot open shared object file,恢复后 imports ok。

沐曦 C500 的差异步骤(平台预置栈,勿改动):source /data/c500-env.sh(导出 MACA_PATH、XMAKE_ROOT=y、PATH 前置 /opt/conda/bin、追加 LD_LIBRARY_PATH);InfiniCore 配置改为 xmake f --cpu=y --metax-gpu=y --use-mc=y(禁开 --aten/--infiniops/--flash-attn);rpath 配方必须含 /opt/conda/lib:patchelf --set-rpath '/root/.infini/lib:/opt/conda/lib:$ORIGIN' python/infinilm/lib/_infinilm*.so,校验 ldd python/infinilm/lib/_infinilm*.so | grep -c "not found" → 0;示例入口用 --device metax。

3.2 模型获取与完整性校验

角色 公开模型名 用途
target + draft(同一目录) Qwen/Qwen3.5-0.8B、Qwen/Qwen3.5-2B、Qwen/Qwen3.5-9B Qwen3.5 家族(checkpoint 内含 mtp.* 头)
target + draft(同一目录) XiaomiMiMo/MiMo-7B-Base MiMo 家族(checkpoint 内含 model.mtp_layers.0.* 头,16 张量)
上游 EAGLE 对照(非本项目工作) openbmb/MiniCPM4-8B + openbmb/MiniCPM4-8B-Eagle-vLLM 独立 draft checkpoint 路径
mkdir -p ~/models
# 下载工具(二选一;modelscope 已含在第 0b 步的依赖安装里):ModelScope 用 `uv pip install --python .venv/bin/python modelscope`,HuggingFace CLI 用 `uv pip install --python .venv/bin/python "huggingface_hub[cli]"`(下面用 ModelScope 演示)
python -c "
import os
from modelscope import snapshot_download
snapshot_download('Qwen/Qwen3.5-0.8B', local_dir=os.path.expanduser('~/models/Qwen3.5-0.8B'))"
# 备选:hf-mirror(xet 协议不支持,须关掉)
# HF_ENDPOINT=https://hf-mirror.com HF_HUB_DISABLE_XET=1 hf download Qwen/Qwen3.5-0.8B --local-dir ~/models/Qwen3.5-0.8B

本流程需要 0.8B 与 2B 两份权重(端到端对照用 0.8B,数值对齐用 2B);只下 0.8B 时跳过 §3.3 中需要 2B 的三条(两条显式传 --model ~/models/Qwen3.5-2B,另有 test/models/qwen3_5_mtp/test_verify_handoff.py——它的 --model 默认值就是 2B)。

完整性校验(逐模型目录执行;model.safetensors.index.json 给出逐张量 → 分片映射与 metadata.total_size)。脚本会跳过未下载与缺分片的目录并打印原因(判据是 model.safetensors.index.json 是否存在:单文件 model.safetensors 且无 index 的 checkpoint 会被报成 skipped),只下部分模型也能直接跑:

python - <<'PY'
import json, os
for d in ["Qwen3.5-0.8B", "Qwen3.5-2B", "Qwen3.5-9B", "MiMo-7B-Base"]:
    root = os.path.expanduser(f"~/models/{d}")
    index = os.path.join(root, "model.safetensors.index.json")
    if not os.path.isfile(index):
        print(f"{d}: skipped (not downloaded)"); continue
    idx = json.load(open(index))
    shards = sorted(set(idx["weight_map"].values()))
    missing = [f for f in shards if not os.path.isfile(os.path.join(root, f))]
    if missing:
        print(f"{d}: INCOMPLETE, missing shards: {missing}"); continue
    sizes = {f: os.path.getsize(os.path.join(root, f)) for f in shards}
    total = idx["metadata"]["total_size"]
    print(f"{d}: tensors={len(idx['weight_map'])} shards={len(shards)} "
          f"shard_bytes={sum(sizes.values())} index_total_size={total} "
          f"delta={sum(sizes.values()) - total}")
    print("   draft keys:", sum(1 for k in idx["weight_map"]
                                if k.startswith(("mtp.", "model.mtp_layers."))))
PY

预期:已下载目录打印 tensors=… shards=… shard_bytes=… index_total_size=… delta=…;未下载目录打印 skipped;缺分片打印 INCOMPLETE。delta 应为正的小量(safetensors 头开销,实测约 5e4–1e5 B 量级);delta 为负或出现 INCOMPLETE → 下载不完整。逐文件 sha256 用 sha256sum ~/models/Qwen3.5-0.8B/* 与模型仓库发布值对账。

已知实测值:Qwen3.5-0.8B 488 张量 / 1 分片 / 1,746,942,600 B(delta 59,848)/ 15 个 mtp.*;Qwen3.5-2B 632 张量 / 1 分片 / 4,548,221,488 B(delta 76,656)/ 15 个 mtp.*。后两份(Qwen3.5-9B 17 个文件、合计 19,329,394,438 B;MiMo-7B-Base 16 个文件、合计 15,678,418,387 B、16 个 model.mtp_layers.0.*)来自清单聚合口径、不是当次实测,且清单校验结论随实例而变——复现时须对当次副本重跑上面的脚本。注意:清单与下载渠道不同源时,差异通常只在非权重文件上,不得据此判定权重损坏。

3.3 逐条命令与预期输出

两族组件级(无需权重、无需 GPU)的测试清单、命令与预期输出见附录 A;下列命令需要 GPU 或对应平台,输出为本机实测或标注平台的归档读数。

# ── 合成权重下的投机无损对照(MiMo tiny checkpoint,需要 GPU 与 paged 缓存)──
python test/models/mimo_mtp/test_speculative_lossless.py
# → K=1..4 各 6/6 prompt 逐 token 一致;强制部分接受 144 轮全部部分接受(138 轮超出截断)
# → 强制全接受 96 轮 {2: 96}
# → target has_mamba_cache=False, runner state rows=None   (无循环状态池 → 机制自动不启用)
# → ✓ Losslessness passed;EXIT=0。该测试自建合成 checkpoint,不依赖任何外部权重或云端。
#   (自然档在这份随机初始化权重上每档只有 1 个验证轮 ⇒ 判别力有限,真实覆盖来自强制档;见 4.1)

# ── GPU:真权重端到端(0.8B;**需独占 GPU**:同卡有其它作业时可能在引擎构造期失败)──────
python test/models/qwen3_5_mtp/test_speculative_lossless.py \
    --model ~/models/Qwen3.5-0.8B --device cuda --num-draft-tokens 1
# → 2.1 段(K=1 自然档):✓ 6/6 逐 token 一致;accepted 126/157 drafted tokens (80.3% acceptance);
#   verification rounds 126, accepted per round {1:126};✓ verified draft budget: requested 1, runner reports 1
# → 3.2 段(强制部分接受 K=2,rejected tail +7 / +11 两次运行;该段是默认运行自带的一段,无需额外开关):
#   判据全 ✓(预算=2、有轮投满预算、真实部分接受 137 / 138 轮、无状态槽耗尽回退),两次运行的 rejected-tail 比较 ✓ 6/6;两个分母是两条 rejected tail 各自的账:tail +7 为 142 个 `accepted<2` 轮中 137 轮真实部分接受 + 5 轮被截断,tail +11 为 144 轮中 138 + 6(见 §4.1)
# → 该段对关闭投机的比较**是报告项、不是判据**(`report: 1/6 prompt(s) diverge`;'def fibonacci(n):' 自位置 11 起分歧,
#   K>1 边界内的确定性现象,见 4.1 与 §6 第 1 条);→ 末段 ✓ target has_mamba_cache=True、state rows held=True、fall-backs=0 ⇒ ✓ Losslessness passed for the claimed setting: K=1, batch size 1、K=1 段不受影响、**EXIT=0**

# ── GPU:draft 前向数值对齐(真权重 2B;容差 rtol=1e-2, atol=1.0;--model 默认 ~/models/Qwen3.5-2B,只下了 0.8B 时请跳过或改传)──
python test/models/qwen3_5_mtp/test_forward_validation.py --device cuda --model ~/models/Qwen3.5-2B
# → ✓ logits: max_abs_diff=0.343750;✓ hidden_states: max_abs_diff=0.500000
# → ✓ 负控(交换拼接顺序):logits max_abs_diff=28.342773 / hidden 47.521484 ⇒ 容差有判别力
# → ✓ C++ draft forward matches the torch reference

# ── GPU:多 token 验证的状态交接(状态一致性机制;只加载 target、无需 draft;**脚本内置 K=2/3/4 三项检查,K 不是命令行参数**;需 2B 权重)──
python test/models/qwen3_5_mtp/test_verify_handoff.py --device cuda
# → [PASS] K=2: 2 batch predictions, 1-token replay and full-accept hand-off all match plain decoding
# → [PASS] K=3: 3 batch predictions, 2-token replay and full-accept hand-off all match plain decoding
# → [PASS] K=4: 4 batch predictions, 3-token replay and full-accept hand-off all match plain decoding
# → [PASS] batch shape stated: single-token batch -> decode shape, two-token and mixed batches -> multi-token shape
# → [PASS] single-token verification produces the decode step's token: [13]
# → [INFO] borrow + swap + release: 0.22 us per accepted round (2000 rounds);K=1 形状声明/推断 17.50 / 17.47 ms (n=15)
# → ✓ Mechanism checks passed: verification, partial-accept replay and full-accept hand-off predict identically
#   to sequential decoding;EXIT=0(**8 GB 本机亦可完整复跑**;上列时延为本机读数,云端 4090D 归档的同一机制检查为 1.87 us/round)

# ── 拒绝 / 边界校验(可核的负例)───────────────────────────────────────
python test/models/qwen3_5_mtp/test_forward_validation.py --device cpu --model ~/models/Qwen3.5-2B
# → EXIT=1:infiniopCreateMRoPEDescriptor(...) failed with error: Device Type Not Supported
#   (Qwen3.5 的 mrope 算子无 CPU 内核 —— 既有边界,非本项目引入,见 3.4;该串出现在 RankWorker
#    线程日志里,抛到顶层的异常是 `RuntimeError: RankWorker stopped during run`)

# ── 基准(同进程 baseline vs 投机;输出接受率与加速比)──────────────────
python test/models/qwen3_5_mtp/test_speculative_latency.py \
    --model ~/models/Qwen3.5-0.8B --device cuda --num-draft-tokens 1 --rounds 3
# → acceptance stats (cumulative over warmup + timed rounds): accepted 400/496 drafted tokens (80.6% acceptance)
# → Baseline round total: median 2422.4 ms, 288 tokens, 118.9 tokens/s
# → Speculative round total: median 2834.8 ms, 288 tokens, 101.6 tokens/s
# → Speedup at K=1 (round-total median, baseline / speculative): 0.855x
#   (**单次运行样例**:本机读数约 0.85 量级;见 4.2)

云端(📋 需对应平台):K=1 无损与强弱接受路径的 2B/9B 运行见 test/models/qwen3_5_mtp/test_speculative_lossless.py(2B 需 target+draft 同时驻留 ≥12 GB;9B 需 ≈27 GB,仅 C500 满足);MiMo-7B 真机为

# 沐曦 C500
python examples/test_infer.py --device metax --model ~/models/MiMo-7B-Base \
    --prompt "The capital of France is" --max-new-tokens 16 \
    --enable-paged-attn --disable-prefix-caching
# → EXIT=0;===Response=== Paris is the capital of France.

# EAGLE 上游对照路径(非本项目工作,本 PR 不含产生该读数的脚本;须 paged,static 会退化回不投机)
# 调用引擎时传入的关键参数(模型 = MiniCPM4-8B + MiniCPM4-8B-Eagle-vLLM 作为 draft):
#   cache_type="paged", attn_backend="paged-attn", enable_prefix_caching=False,
#   max_tokens=48, temperature=1.0, top_p=1.0, top_k=1

3.4 平台差异、CPU 可用范围与已知问题

NVIDIA 沐曦 C500 CPU
--device 测试脚本一律 cuda(引擎构造白名单只收 cpu/cuda/mlu/musa/npu);nvidia 只在 examples/* 入口可用(那里会归一化为 cuda),见 3.4 已知问题 12 metax(示例入口)或 cuda(测试脚手架) cpu
paged attention 支持(投机路径必需) 支持 ❌ 未验证:无后端 → 任何 paged 路径报 Device Type Not Supported
端到端投机 K=1 已验证(0.8B / 2B;MiMo 在 C500) K=1 已验证(2B / 9B / MiMo-7B) ❌ 未验证(paged 无 CPU 后端;Qwen3.5 另受 mrope 无 CPU 内核限制)
组件级(前向 / 映射 / 描述) ✅ 已验证 ✅ 已验证 ✅ 已验证(MiMo;Qwen3.5 只到描述与映射层,前向受 mrope 限制)

9B 只在 C500 侧跑过:RTX 4090D 的 24 GB 装不下 9B 的 target+draft 同时驻留(≈27.3–27.7 GB > 24.6 GB,见 3.4 已知问题 10)。

# 现象 处置
1 ImportError: libinfinicore_cpp_api.so: cannot open shared object file 运行期库路径未设:每次 xmake install / pip install -e 之后重打 rpath(见 3.1 第 3 步),或临时 export LD_LIBRARY_PATH="$HOME/.infini/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"(不覆盖容器既有的库路径:新路径在前,既有值保留在后)
2 沐曦上 ImportError: libpython3.10.so.1.0 rpath 缺 /opt/conda/lib(配方见 3.1 末)
3 运行期 NoKernelImageForDevice CUDA devlink 未透传架构:必须保留 NVCC_APPEND_FLAGS 与 --cuflags 里的 -gencode arch=compute_89,code=sm_89
4 改了 xmake 配置但没重编 xmake 配置变更不触发重编:rm -rf build 后重跑
5 沐曦首次 xmake f 报 invalid cuda location: /usr/local/cuda 同一命令行复跑即通过(命令行并未传 --cuda=);不要创建 /usr/local/cuda、不要装 CUDA toolkit
6 沐曦镜像可能缺 patchelf / unzip(视镜像而定) 先检查;缺则 pip install patchelf(+软链到 /usr/local/bin);unzip 用 Python 垫片补齐。无 patchelf 时 set-rpath 静默不生效
7 LLM(...) 路径 import janus 失败 仓库打包缺口:pip install --no-deps janus==2.0.0
8 --draft-model 传了却完全不投机 cache_type="static" 下调度器不发放验证槽位(引擎打印告警);投机一律用 cache_type="paged" + enable_prefix_caching=False
9 设备字符串拼错却仍能执行 设备名归一化对未识别字符串会静默落到 cpu:不得以"能够执行"作为后端生效的证据,用 mx-smi -L / nvidia-smi -L 与 torch.cuda.get_device_name() 核对
10 本机 8 GB 显存跑 2B 同时驻留 OOM target+draft ≈9 GB;改用 0.8B(需独占 GPU:同卡有其它作业时构造期仍可能失败),或到 ≥12 GB 的平台(4090D 侧 9B 同时驻留需约 27–28 GB(估算、未实测),超 24.6 GB)
11 沐曦上对 Qwen3.5 开 prefix caching 引擎按设计拒绝(Prefix caching is not supported for Mamba-cache model 'qwen3_5' yet):Qwen3.5 是混合线性注意力 target,必须 --disable-prefix-caching;MiMo 等纯全注意力 target 可以开
13 复用既有 checkout 时 .so 与源码不同步 → Unsupported model config type: …(族名在冒号后)或 Input got an unexpected keyword argument 'mamba_multi_token_batch' 陈旧/不匹配的 python/infinilm/lib/_infinilm*.so:重跑 §3.1 第 2 步(必要时先 rm -rf build)
12 --device nvidia 在测试脚手架里报 Unsupported device 两套口径:examples/* 会把 nvidia 归一化成 cuda,test/models/** 的引擎构造白名单只收 cpu/cuda/mlu/musa/npu ⇒ 测试脚本一律写 --device cuda

4. 结果

4.1 正确性

统一测量口径(本节各表适用):6 条固定 prompt × 48 token、贪心(temperature=1.0, top_p=1.0, top_k=1)、bf16、paged attention 且关闭 prefix caching;无损对照在同一进程内比较开启投机与关闭投机的逐请求 token id 序列;除 batch 一栏外,均为单请求(一次只处理一个请求);batch 一栏用的是其中 2 条 prompt。加速比与接受率另跑 3 轮取 round-total 中位,计时为 perf_counter 包住单次 engine.generate、不含设备同步。设备与栈版本见 3.1,实例级标识不随本描述给出(实例多已释放,第三方无法独立核验)。两处接受率统计量不同:本表为单次运行的 accepted/drafted,§4.2 为含预热的 3 轮累计值。

项 结果 备注
K=1 逐 token 一致(无损主张) ✅ Qwen3.5-0.8B 本地 6/6(接受 126/157 = 80.3%);2B C500 6/6(117/166 = 70.5%)、2B 4090D 6/6(118/166 = 71.1%);9B C500 6/6(120/165 = 72.7%);MiMo-7B C500 6/6(99/184 = 53.8%) 0.8B 本次本机重跑;其余为对应平台上的实测归档;接受率 K=1 约 54%–80%
合成权重(MiMo tiny checkpoint) ⚠️ 自然档 K=1/2/3/4 各 6/6 prompt 逐 token 一致,但随机初始化权重下接受率仅 0.1%–0.4%,每档只发生 1 个验证轮(接受 1/281、1/556、1/825、1/1088)⇒ 该档对验证路径判别力有限;真实路径覆盖来自强制档:K=2 部分接受 144 轮(全部 accepted<2:其中 6 轮因本轮投出数少于预算而被截断解释、138 轮为真实部分接受)+ 全接受 96 轮,state rows 借还 0 次 fallback 该测试自建 checkpoint、不需要外部权重;K=3/K=4 的强制路径在本合成测试中未覆盖(强制档只有 K=2);无循环状态池的 target 上机制自动不启用(runner state rows=None)
部分接受路径真实触发 ✅ 强制部分接受路径在本地 0.8B(K=2)真实触发:142 个 accepted<2 轮中 137 轮为真实部分接受、5 轮被截断解释;但该档的逐 token 比较在位置 11 出现确定性分歧(连续三次运行一致)⇒ 本地这一档不计入无损主张(K≥2 边界,见下行与本行备注);C500 2B(K=2 115/117、K=3/K=4 117/117)与 9B(K=2 107/116、K=3 107/114、K=4 108/113)触发档 6/6;4090D K=2 117/119、K=3/K=4 119/119 触发率 92.2%–100%(云端归档档),verify_scratch_exhausted 全程为 0(未退化为 fallback);本地 0.8B 该档的分歧点在 K≥2 边界之外不构成主张(该档本就不在无损范围内)
draft 前向数值对齐 ✅ MiMo 合成权重 f32(rtol=1e-4, atol=1e-4):logits 4.8e-07 / hidden 9.5e-07(归档值:该测试仅在断言失败时打印 max_abs_diff,成功路径不打印,按 §3.3 的命令无法复现这两个数);Qwen3.5-2B 真权重 bf16:logits 0.343750 / hidden 0.500000(rtol=1e-2, atol=1.0) Qwen3.5-2B 两数为本次本机重跑(命令与预期输出见 §3.3);MiMo 两数为内部测量脚本的归档、第三方不可复现。bf16 下 0.34/0.50 的量级与 atol=1.0 同阶,判定依据是与容差比较而非绝对小量
"能失败"的对照 ✅ 反例对照已实测:MiMo 6 条语义错例 + 1 条映射对调全部按预期偏离参考;装载表项缺失 → 16 个 unexpected key 装载失败;真实多层 draft checkpoint(MiMo-7B-MTPs,发布 2 个深度而块只组合 1 个)在构造期被拒绝并给出具名原因,未进入生成;CPU 上 Qwen3.5 前向报 infiniopCreateMRoPEDescriptor ... Device Type Not Supported 前两项为本次本机重跑的测试级证据,末项本次本机复现;第三项为 C500 实测
长序列、块边界与 prefix caching ✅ 600 token 生成(跨 3 个 KV block)逐位一致;2,109 token prompt(跨 9 个 block)生成 64 token 逐位一致;MiMo 开 prefix caching 时第二个请求命中 512 token 且逐 token 一致 C500 实测;Qwen3.5 开 prefix caching 会被引擎按设计拒绝(混合 target 需关)
batch>1(探索性,不计入无损主张) 2B C500,batch=2:K=1、K=2 强制部分接受、K=4 强制部分接受三档逐请求 token 一致。MiMo batch=2:一个请求在单点位出现分歧(K=1 位置 42、K=2 位置 5) 该分歧已定位到上游内核的批布局 1-ULP 差 + 该步 top-2 精确并列,不可归因于本项目改动;因此 K=1 无损主张限定 batch=1(一次只处理一个请求)
K≥2 的边界 ⚠️ 不在无损主张范围内:真实权重下模型自身的 top-1/top-2 候选会落进同一个 bf16 量化步,K≥2 走多 token 验证内核路径时 argmax 可翻转。已做 fp32 复算的例上,候选间距均 <1 个 bf16 步:4090D 2B 为 0.47–0.73 个 bf16 量化步(绝对间距 0.0147–0.0227,logits 单位),C500 2B 为 0.41–0.98 个 bf16 量化步(绝对间距 0.0128–0.0306)、C500 9B 为 0.42 个 bf16 量化步(0.0132)。在覆盖该现象的判据里(21 个 2B 运行、840 对比较),只有两条分支、分叉点唯一(位置 12)、各分支逐位可复现、无第三种形态 该分歧不是标签约定:关闭投机的纯贪心解码路径本身在两个平台上就取了不同分支(NVIDIA → 位置 12 取 20618,C500 → 取 5759);验证路径的分支归属构成角色×模式矩阵——NVIDIA 的自然档与强制档均为 decode 取 A、verify 取 B;C500 自然档为 decode 取 B、verify 取 A;C500 强制档由"6/6 无分歧"反推 verify 与 decode 同支(B),属推断。因此两平台的通过计数真实不同:C500 强制档 6/6,4090D 强制档 5/6;C500 2B 自然档 K=3/K=4 为 5/6(位置 12)、9B K=4 自然档为 5/6、4090D 自然档 K=2/K=3 各 5/6——9B 的首次分歧在位置 30,与位置 12 那条同属"近似并列 + 多 token 验证路径"引起的确定性翻转,但位置与 token 对不同,且未纳入上述 21 次运行的判据。K≥2 数据因此只作机制性能参考。该单点差异与状态一致性无关——状态机制由 test/models/qwen3_5_mtp/test_verify_handoff.py 覆盖 K=2..4;未做的是内核级根因定位(见 §6 第 1 条)。

4.2 实测收益

负收益的归因:两族各档加速比均 <1.0,归因于多 token 验证的形状成本与接受率/验证开销;纯全注意力 target(MiMo-7B)在同平台上的形状跳变仅 +0.410 ms(比混合架构小约一个数量级),去掉形状成本后仍为负 ⇒ 残余来自验证开销与接受率(两者未作定量拆分),而非接入方式(两族由各自独立的 target/draft 实现与代码块接入);该归因不涉及无损主张本身(K=1 逐 token 一致已逐平台验证,见 §4.1)。以上归因均为机制解释,非逐位置内核级归因(见 §6)。

target 平台 加速比(K=1/2/3/4) 接受率 归因
Qwen3.5-2B C500 0.910 / 0.638(较晚读数);更早读数 0.906 / 0.641 / 0.610 / 0.553 K=1 69.7%、K=2 54.9%(累计口径) 混合架构的 multi-token 形状成本:单次前向 1→2 token 的形状跳变 +15.760 ms,验证步耗时 / 解码步耗时 = 3.14×(机制解释,非逐位置内核级归因,见 §6)
Qwen3.5-9B C500 0.932 / 0.807(较晚读数);更早读数 0.931 / 0.887 / 0.699 / 0.689 K=1 71.8%、K=2 64.9% 同上(1→2 token 形状跳变 +20.453 ms,验证步耗时 / 解码步耗时 = 2.12×;机制解释,见 §6)
Qwen3.5-0.8B 本地 4060 约 0.85 量级(K=1;单机 run-to-run 波动) K=1 80.6%(3 轮累计口径) 显存受限条件下的最小可运行配置;K=1 无 multi-token 验证形状,仍为负 ⇒ 推断:单步验证开销本身即高于收益(无本机相位分解归档,见 §6)
Qwen3.5-2B 4090D 各 K 档均 <1.0;该批的逐次读数与同实例另一批的读数见下方的读数批次表 43.4%–70.7% 同上(目标前向本身快,rollout 与验证开销无法抵消;机制解释,见 §6)
MiMo-7B C500 0.926 / 0.870 / 0.816 / 0.766 53.8 / 28.4 / 19.2 / 14.5% 去掉形状成本仍为负:同平台纯全注意力 target 的验证批形状 1→4 token(对应 K=3)跳变仅 +0.410 ms(验证步耗时 / 解码步耗时 = 1.026×),比混合架构小约一个数量级 → 接受率与验证开销是第二道门槛(机制解释,见 §6)

两个基准入口的分工:仓库自带的 examples/bench.py 可用(本 PR 只补传了它已配置的 dtype,+1 行),本描述中的各平台读数是用上方的 test/models/qwen3_5_mtp/test_speculative_latency.py 采的。

不承诺加速。官方 MiMo 资料称 MTP 首层接受率 about 90%:模型仓库与模型卡(https://github.com/XiaomiMiMo/MiMo);技术报告的测量口径为 AIME24——第一层 about 90%、第三层 above 75%(https://arxiv.org/abs/2505.07608,§2.2 MTP Inference Speedup)。该口径(AIME24 长推理输出,第一层 MTP)与本项目的 6 条固定短 prompt × 48 token 不同,仅作引用,不构成本项目读数的对照。

口径与读数边界:加速比 = 关闭投机 round-total 中位 ÷ 开启投机 round-total 中位(3 轮计时 + 1 轮不计时预热);计时不含设备同步,该口径有利于投机侧,实测同步口径低约 8% 且偏置逐平台可能不同 ⇒ 跨平台比值不构成设备侧结论,只作相对参考。4090D 的 K=2 行(0.757×,三遍 0.737 / 0.754 / 0.757)同样只作相对参考。云端读数按批次分列(同一实例与实例间的差异见下表):

批次 读数
C500 性能批 C500 的 2B/9B K=1 与 K=2 强制部分读数、20 项基线对照(仅我方归档)、batch / 长序列 / 缓存 / 显存各项
EAGLE A/B 批 EAGLE 零影响的运行期 A/B(上游基线树 vs 本 PR 树,两次构建的二进制不同)
最新批 2B/9B 的 20 项逐位回归(仅我方归档)与全部本机读数(构建 / 测试 / 0.8B 端到端 / 基准 / CPU 边界);该批读数与逐位回归取自同一次代码状态,其后仅文档改动
4090D 新实例 K=1 118/166、K=2 自然 152/267、K=4 自然 179/412、强制档 117/119(98.3%)与 119/119(100%)、K=2 加速比 0.757×(三遍 0.737 / 0.754 / 0.757,接受 481/845)
4090D 更早实例(与上一行并非同一台机器) K=3 自然 173/327、该批加速比各 K 档均 <1.0;该批的 K≥2 接受计数经新实例逐位复现

同一实例另有一批 latency 归档,读数不同,两批数据不可合并使用。

4.3 对 EAGLE 的零影响

我方对共享文件的改动对 EAGLE 路径为:描述层的检查对 EAGLE 的求值逐位不变(短路条件仍为 False)、装载映射纯新增(modeling_utils 的按模型类型索引的映射表,MiMo 阶段新增 1 条;相对上游基线本 PR 共新增 2 条:qwen3_5_mtp 与 mimo;EAGLE 的查找结果不变)、描述解析无交叉(minicpm_eagle 的描述随本次注册表一并新增,是基线白名单的等价改写,判据读法逐条相同)、EAGLE 的实现与测试在本 PR 中零改动(csrc/models/minicpm_eagle/ 下无任何文件被改动)。

运行期 A/B 跑的是真实的 EAGLE 配置:target = MiniCPM4-8B、draft = MiniCPM4-8B-Eagle-vLLM(独立 draft checkpoint,非 MTP 侧的内嵌 draft,因此它测的是 EAGLE 自身路径),paged + 关闭 prefix caching、贪心、6 条 prompt × 48 token、K=1 与 K=2;两次运行在同一节点、同一权重、两棵各自构建的代码树上执行(上游基线树 vs 本 PR 树,两次构建的二进制不同)。读数为:关闭投机基线 6×48 token 逐 token 相同;K=1 无损 6/6;K=2 的序列与接受计数(148/286,即该 K 下接受的 draft token 数 / 投出的 draft token 数)及首个分歧位置(1 + 1 = 在 index 6,上游既有行为,不做归因)逐字段相同;两次运行确定性一致——两棵树给出同一组读数,即改动未进入该路径的证据。该 A/B 的读数取自上述两棵树。其后的零影响由静态等价性与最晚一批读数状态的回归承接:C500 的 2B/9B 对照 20 项全部命中基线(2B/9B 各 10 项:逐 prompt 无损、accepted/drafted、验证轮直方图、强制部分接受触发与计数、未退化计数、退出码;条目清单与逐项判定仅存我方归档,第三方无法独立重跑);本地 0.8B 端到端复跑仍 6/6、接受计数 126/157 不变。

边界:只覆盖"关闭投机基线 / K=1 / K=2"三条运行期路径与描述层、装载层的静态等价,不含任何 EAGLE 自身的性能或收益结论。

5. 逐平台状态

平台 组件级(前向 / 数值 / 映射 / 描述) 端到端投机解码 边界与未做
NVIDIA 本地 RTX 4060 Laptop 8GB(sm_89) ✅ 已验证:八个 CPU 测试文件全绿(136 条,清单见附录 A)、Qwen3.5-2B 真权重前向对齐(logits 0.344 / hidden 0.500)、真实 checkpoint 的家族归属与深度解析 ✅ 已验证(真权重 Qwen3.5-0.8B):K=1 6/6 逐 token 一致;K=2 强制部分接受路径在同一次运行内真实触发(142 个 accepted<2 轮、137 轮真实部分接受),但该档逐 token 比较在位置 11 出现确定性分歧,故本地这一档不计入无损主张(见 §3.3、4.1);✅ 已验证(合成权重 MiMo tiny):自然档 K=1–4 各 6/6 逐 token 一致(判别力有限,真实覆盖来自 K=2 强制档)且状态机制自动不启用 ⚠️ 本机 8 GB 装不下 2B 的 target+draft 同时驻留(≈9 GB);9B 需 ≈27 GB,本机与 4090D 均不满足;K≥2 自然路径的无损性不在本档主张
NVIDIA 云端 RTX 4090D 24GB(sm_89) ✅ 已验证:真实 2B 权重的 draft rollout 与状态交接机制检查 ✅ 已验证:K=1 自然 6/6(接受 118/166);K=4 自然 6/6(接受 179/412);强制部分接受真实触发(K=2 117/119 = 98.3%、K=3 与 K=4 均 119/119 = 100%) ⚠️ 部分验证:K≥2 自然档 K=2(152/267,新实例)与 K=3(173/327,更早实例)各 5/6、强制档 K=2/K=3/K=4 各 5/6——唯一分歧为位置 12 的确定性近平局点,逐位可复现;9B 同时驻留(≈27.3–27.7 GB)超 24.6 GB 未测;加速比各 K 档均 <1.0(见 4.2)
沐曦 C500 64GB(MetaX MXC500) ✅ 已验证:真权重 MiMo-7B 完整生成(Paris is the capital of France.,exit=0)、设备归因(mx-smi -L 当次 UUID)、权重位精确比对(5 张量 max_abs_diff = 0.0);运行期显存(设备侧采样)idle 826 MiB → 生成期峰值 24,798 MiB(总 65,536),target 单独 21,644 MiB(draft 单独足迹不可分列,不做减法估算) ✅ 已验证:K=1 逐 token 一致(MiMo-7B 6/6、Qwen3.5-2B 6/6、9B 6/6);强制部分接受 2B(K=2/K=3/K=4)与 9B(K=2/K=3/K=4)各 6/6,触发率 92.2%–100%;2B batch=2 的 K=1/K=2/K=4 逐请求一致(探索性,不计入无损主张);600 token 与 2,109 token prompt 逐位一致;MiMo 开 prefix caching 命中 512 token 且逐 token 一致 ⚠️ 部分验证:K≥2 自然档 2B 的 K=2(更早读数)/K=3/K=4 与 9B 的 K=4 各 5/6(确定性近平局点);MiMo batch=2 单点位分歧(上游批布局 1-ULP,登记不改);MoE 变体(平台无 metax MoE 算子)、27B 同时驻留、真实多层 draft checkpoint 运行(构造期拒绝,属设计)
CPU(x86_64) ✅ 已验证:组件级(两族)——八个 CPU 测试文件全绿(136 条,清单见附录 A) ❌ 未验证:Qwen3.5 / MiMo 的端到端投机解码均需 paged KV cache,其无 CPU 后端(实测报 Device Type Not Supported) ⚠️ Qwen3.5 家族 CPU 端到端不可用属既有边界:其 mrope 算子无 CPU 内核(实测 infiniopCreateMRoPEDescriptor ... Device Type Not Supported),本项目未引入也未修复

6. 已知边界与未做项

一句话读完的「不支持的组合」:实测只覆盖 Qwen3.5(0.8B / 2B / 9B)与 MiMo-7B 两个家族与本描述列出的平台;这两族之外的家族——MoE draft 块、量化 draft 头、需读取 target KV 的 draft 类、独立 draft checkpoint——均未实测;deepseek_v3 / deepseek_v32 在本构建里不可构造;K≥2 与 batch>1 不在无损主张内;CPU 上没有端到端投机,真实 EOS 早停未观测到;27B 级同时驻留、MoE 变体、昇腾与多卡 TP 受硬件或算子前提限制未做;第三方小体量 checkpoint(Kimi-K3-0.40B)因取不到权重未做。

# 项 状态
1 K≥2 的逐位置内核级归因(decode 与多 token 验证哪个先翻转、并列打破方向的算子级原因);本机 0.8B 的相位分解 未做(需"同一前缀、两种 batch 形状"专项对照);§4.2 的相关归因均为机制解释,非内核级归因;现有结论停在"两条分支、分叉点唯一、逐位可复现"(在该判据覆盖的 21 个 2B 运行内)
2 上述两个家族之外的家族:MoE draft 块、量化 draft 头、需读取 target KV 的 draft 类、独立 draft checkpoint 未实测;描述中已声明缺口并在构造期报明确错误(deepseek_v3/deepseek_v32 的识别与映射层已扩展但未在真实权重上验证,且本构建不可构造)
3 真实 EOS 早停路径 未观测到,且两族原因不同:Qwen3.5-2B 因 eos_ids 为空结构上不可达(EOS 只写在 text_config 里、上游读取链不回溯);MiMo 有 EOS id,但 4 条 prompt 在 64 token 内未自然产出。不写成"已验证"
4 batch>1 的无损性(MiMo 侧单点位分歧已定位到上游批布局) 不计入无损主张;对外只声明 batch=1(一次只处理一个请求) 逐 token 一致
5 27B 级同时驻留(Qwen3.5-27B 需 ≈67.5 GB > C500 的 64 GB)、MoE 变体(平台无算子)、昇腾(无硬件)、多卡 TP 未做(硬件或算子前提不满足)
6 第三方小体量 MTP checkpoint(Kimi-K3-0.40B):原计划用作"第三个家族"的轻量对照,但其发布渠道无法取得权重 未做,已记录在案
7 本 PR 的自动 CI 只跑格式检查(pull_request 事件下运行 check-format;另有一个与本 PR 内容无关的 Ruff workflow 只检查 scripts/format.py);仓库的硬件 CI 需维护者手动触发,且其命令不传 --draft-model ⇒ 投机这条线在 CI 上无覆盖 结论依赖人工执行记录

附录 A:测试附件

测试是随 PR 提供的附件:用于支撑正文的结论与上面的实测读数,不是本项工作的主体。本节只列有哪些、各自覆盖什么、怎么跑。

A.1 无需权重与 GPU(8 个文件 / 136 条,秒级)

# ── CPU(无需 GPU、无需权重;两族组件级测试共 8 个文件 / 136 条)────────────────
python test/models/mimo_mtp/test_adaptation.py
# → Ran 14 tests ... OK            (MiMo target 侧装载:内嵌 draft 头被丢弃、其余张量不变)

python test/models/mimo_mtp/test_draft_spec.py
# → Ran 4 tests ... OK             (MiMo 侧描述解析与闸门)

python test/models/mimo_mtp/test_draft_weight_load.py
# → Ran 16 tests ... OK            (权重映射、双布局、描述字段、解析边界、闸门负例)

python test/models/mimo_mtp/test_forward_validation.py
# → Ran 9 tests ... OK             (draft 前向 vs torch 参考 + 6 条"必须失败"的语义错例)

python test/models/qwen3_5_mtp/test_adaptation.py
# → Ran 12 tests ... OK            (Qwen3.5 target 侧装载:改名、零中心 norm 偏移、tied 补齐、丢弃)

python test/models/qwen3_5_mtp/test_draft_spec.py
# → Ran 48 tests ... OK            (描述/注册一致性、家族解析、重命名、target 侧派生、runner 不含家族名、不可用家族字面集合)

python test/models/qwen3_5_mtp/test_draft_weight_load.py
# → Ran 7 tests ... OK             (Qwen3.5 侧权重映射)

python test/models/qwen3_5_mtp/test_documentation_gate.py
# → Ran 26 tests ... OK            (文档一致性检查:MODELS.md 家族表 vs 注册表、判据引用可解、中文版权威声明)

# 按族汇总(仓库文档给出的两条;两个目录存在同名模块,故 discover 必须一次只跑一个目录)
python -m unittest discover -s test/models/qwen3_5_mtp -t test/models/qwen3_5_mtp
# → Ran 93 tests ... OK            (12 + 48 + 7 + 26)
python -m unittest discover -s test/models/mimo_mtp -t test/models/mimo_mtp
# → Ran 43 tests ... OK            (14 + 4 + 16 + 9)

A.2 需要 GPU 或真实权重(10 个文件)

文件 命令与实测输出
test/models/qwen3_5_mtp/test_speculative_lossless.py(合成档 + 真权重档) §3.3
test/models/mimo_mtp/test_speculative_lossless.py(合成档,自建 checkpoint) §3.3
test/models/qwen3_5_mtp/test_forward_validation.py §3.3
test/models/qwen3_5_mtp/test_verify_handoff.py §3.3
test/models/qwen3_5_mtp/test_speculative_latency.py §3.3
test/models/mimo_mtp/test_speculative_latency.py —
test/models/qwen3_5_mtp/test_draft_rollout_validation.py —
test/models/mimo_mtp/test_draft_rollout_validation.py —
test/models/qwen3_5_mtp/test_speculative_static_cache.py —
test/models/mimo_mtp/test_speculative_static_cache.py —

前提沿用 §1bis:需要 GPU 与 paged attention;需真实权重的条目另需对应 checkpoint,部分还需 target+draft 同时驻留(≥12 GB 显存)。


PR 模板对应段

本节按仓库 .github/PULL_REQUEST_TEMPLATE.md 的字段组织(该模板要求 Checklist 段不可删、每一项都要填)。勾选以检查已真实执行为准;未执行的项保持未勾选并写明原因,不适用的项按模板要求以 N/A 替换复选框并说明。

Summary / Motivation

内容与动机见上文的「结论摘要」与 §1 项目内容:本 PR 让投机解码框架直接接受 checkpoint 内嵌的 MTP 头作为 draft,接入一个新家族只需一份描述与一个注册项,并在两个真实家族与本描述列出的平台上给出无损性与开销读数。

Closes # —— 没有可关联的上游 issue:本项工作此前未在上游登记为 issue。若维护者要求关联跟踪 issue,可随时补开一个并在本描述里回填编号。

Type of Change

  • feat —— 新增 MTP draft 类型、家族接入机制与一个新 target 架构
  • fix / [ ] perf / [ ] refactor / [ ] test / [ ] docs / [ ] build / ci / [ ] chore
  • Breaking change —— 无:InfinilmModel::Output 在基线就带有 hidden_states 字段(其注释写明供 MTP/Eagle draft 使用),本次只是让 qwen3_5 的 forward 填充它,结构体与调用签名不变;其余共享文件改动均为默认值=原行为的接口扩展(见 §1bis「本 PR 的改动性质」)。

Test Results of Involved Models on Supported Platforms

模板要求附截图。本 PR 不附截图:判定依据是文本输出(命令、逐条退出码、逐 token 比较),截图不额外承载信息;下表与 §3.3/附录 A 给出可直接照抄复跑的命令与真实输出。

模板指定的入口 NVIDIA(本地 4060 / 4090D) 沐曦 C500 CPU
单请求 examples/test_infer.py 跑过一次:Qwen3.5-0.8B + MTP draft(K=1),EXIT=0,输出 The capital of France is **Paris**. It is the largest city in France 跑过一次:MiMo-7B,EXIT=0,输出 Paris is the capital of France.(§3.3) 不适用(无端到端投机路径,见 §5)
离线性能 examples/bench.py 跑过两次(batch 1 / input 128 / output 64,bf16):关闭投机 total_time 910.83 ms、开启投机(K=1)1025.06 ms,两次 EXIT=0;单次样例,不作结论(§4.2 的平台读数采自 latency 脚本) 未跑该入口 不适用
sanity test/bench/test_benchmark.py 未跑:该入口经基准数据集与 Transformers / VLLM 后端驱动,需要数据集与对应平台 未跑(同因) 未跑(同因)
服务 python/infinilm/server/inference_server.py + scripts/test_perf.py 未跑:需要以服务方式部署模型 未跑(同因) 不适用

本 PR 对这四类入口的等价覆盖是无损逐 token 比较、状态交接、draft 前向对齐与端到端基准(§3.3、§4)。上表中仍未跑的入口及其平台前提见 §5 的逐平台状态表;未测平台需要复审人时,在本 PR 页面上 tag 即可。

Benchmark / Performance Impact

非 perf PR。本 PR 仍给出了测量:各平台各 K 档加速比均 <1.0(最高约 0.93×,C500 9B K=1),不承诺加速收益;归因、逐档读数与计时口径见 §4.2。

Notes for Reviewers

  • 三处既有模型侧的 C++ 改动与一处判定逻辑改写见 §1bis;其中 qwen3_5_for_causal_lm.cpp 的 forward 现在也返回 hidden states(Output 的第二字段在基线即存在)。
  • 一处如实列出的例外:deepseek_v3 / deepseek_v32 在基线加载器表里没有条目,本次为它们启用了注册表回退;该识别与映射层已扩展、未在真实权重上验证(本构建不可构造)。
  • 读数分两类:可复跑(§3.3 与附录 A 的命令)与仅我方归档、第三方无法独立重跑(清单见 §1bis)。

CI / ChatOps

CI 未触发:按模板说明,硬件 CI 不会在 PR 事件上自动运行,需在 Actions 页手动触发 CI workflow(本仓库该作业带 workflow_dispatch 条件),或请维护者评论 /test 或 /retest(chatops 仅对 OWNER/MEMBER 生效)。PR 事件上自动运行的只有格式检查(另有一个只检查 scripts/format.py 的 ruff workflow,本 PR 未改该文件);格式检查的本地等价复现为 scripts/format.py --ref 270feb3 --py ruff --c clang-format --check → EXIT=0(被检文件 41 个,clang-format 21.1.8、ruff 0.15.20,与 CI pinned 一致)。

Checklist

Title, Branch, and Commits

  • PR title follows Conventional Commits。
  • Branch name follows <type>/xxx-yyyy-zzzz。
  • Each commit message follows Conventional Commits。
  • Large PR's commits are meaningful and independently reviewable。
  • No stray merge commits from main。
  • No fixup! / squash! / wip commits。
  • N/A Existing PR/branch/commit with the legacy issue format —— 全新工作,无此类对象。

Scope and Design

  • Changes are minimal。
  • No dead code, commented-out blocks, debug prints, or TODO without an owner。
  • No unrelated formatting churn —— engine.hpp 有 8 行格式化器重排,语义不变。
  • Public API changes are intentional, documented, and reflected in affected callers/tests。

General Code Hygiene

  • Comments only where the why is non-obvious。
  • Every modified or added file ends with a single trailing newline。
  • No trailing whitespace, tab/space mixing, or stray BOMs。
  • Identifiers in comments and error messages use backticks。
  • All comments and error messages are in English。
  • Comments and error messages are complete sentences。

C++ Specific

  • Google C++ Style Guide。
  • Error and warning wording follows the LLVM standard。
  • Constructor initializer list order matches member declaration order。
  • No raw new/delete。
  • Changed files are formatted by scripts/format.py。
  • No changes/reference to csrc/models/llama_legacy/。

Python Specific

  • PEP 8。
  • Comments are complete English sentences with backticks for code references。
  • Docstrings follow PEP 257。
  • Changed files are formatted by scripts/format.py。
  • No changes/reference to python/infinilm/auto_config.py。

Testing

  • Untested platforms have an explicit reason and a tagged reviewer —— 未测平台原因已在状态表写明;复审人未 tag。
  • Single request test (examples/test_infer.py)。
  • Offline performance test (examples/bench.py)。
  • Sanity test (test/bench/test_benchmark.py) —— 需基准数据集与对照后端,未执行。
  • Service test (python/infinilm/server/inference_server.py + scripts/test_perf.py) —— 需以服务方式部署模型,未执行。

Build, CI, and Tooling

  • The project builds cleanly on at least one affected platform。
  • CI has been triggered manually, or /retest was requested —— 仅维护者可触发,未执行。

Documentation

  • Docs updated。
  • N/A User-visible breaking change —— 无破坏性变更。

Security and Safety

  • No secrets, tokens, internal URLs, customer data, or personal hardware identifiers。
  • Third-party code is license-compatible and attributed。
  • No unsafe pointer arithmetic, uninitialized reads, or missing bounds checks。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant