Sariel00 commited on
Commit
3706f1c
·
verified ·
1 Parent(s): 022e6e4

Keep MTP opt-in: default-off build, isolated experimental package and measurements

Browse files

Add explicit experimental MTP compile gating and resident probe tools/results. Normal terminal/API inference does not initialize MTP. Keep final v6 model and portable binary unchanged; exclude MTP build artifacts.

.gitignore ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Build and local caches
2
+ build/
3
+ build-mtp*/
4
+ engine/source/build/
5
+ **/__pycache__/
6
+ *.pyc
7
+
8
+ # Runtime and benchmark output
9
+ *.log
10
+ *.jsonl
11
+ board-test-result.json
12
+ *-result.json
13
+ artifacts/benchmarks/
14
+ docs/measurements/**/*.f32
15
+
16
+ # Large model/checkpoint/release files; publish through Release or model hosting
17
+ models/**/*.safetensors
18
+ models/**/*.l3r
19
+ models/**/*.gguf
20
+ models/local-final/**/*.l3r
21
+ models/local-final/**/mindnano-infer
22
+ models/local-experimental/**/ling3-rknn
23
+ releases/
24
+
25
+ # The final model is published separately through Hugging Face / Git LFS.
26
+ !models/local-final/
27
+ !models/local-final/v6-board-test/
28
+ !models/local-final/v6-board-test/ling3-tiny-w4.l3r
29
+ models/local-final/v6-board-test/mindnano-infer
30
+
31
+ # Generated quantization payloads
32
+ calibration/assets/quantized-assets/
33
+ work/
CMakeLists.txt CHANGED
@@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.20)
3
  project(ling3_rknn VERSION 0.1.0 LANGUAGES CXX)
4
 
5
  option(LING3_WITH_RKNN "Build the direct RKNN backend" OFF)
 
6
  option(LING3_TUNE_A76 "Tune release builds for Cortex-A76" ON)
7
  option(LING3_WITH_ICU "Use ICU for exact tokenizer normalization and splitting" ON)
8
  set(RKNN_ROOT "" CACHE PATH "Local RKNPU2 root from https://github.com/airockchip/rknpu2 (contains include/ and lib/)")
@@ -28,6 +29,7 @@ target_include_directories(ling3_core PUBLIC include)
28
  target_compile_features(ling3_core PUBLIC cxx_std_20)
29
  target_compile_options(ling3_core PRIVATE -Wall -Wextra -Wpedantic)
30
  target_compile_definitions(ling3_core PUBLIC LING3_WITH_RKNN=$<BOOL:${LING3_WITH_RKNN}>)
 
31
 
32
  if(LING3_WITH_ICU)
33
  find_package(ICU COMPONENTS uc i18n QUIET)
 
3
  project(ling3_rknn VERSION 0.1.0 LANGUAGES CXX)
4
 
5
  option(LING3_WITH_RKNN "Build the direct RKNN backend" OFF)
6
+ option(LING3_EXPERIMENTAL_MTP "Build experimental NEXTN/MTP probes (not production decoding)" OFF)
7
  option(LING3_TUNE_A76 "Tune release builds for Cortex-A76" ON)
8
  option(LING3_WITH_ICU "Use ICU for exact tokenizer normalization and splitting" ON)
9
  set(RKNN_ROOT "" CACHE PATH "Local RKNPU2 root from https://github.com/airockchip/rknpu2 (contains include/ and lib/)")
 
29
  target_compile_features(ling3_core PUBLIC cxx_std_20)
30
  target_compile_options(ling3_core PRIVATE -Wall -Wextra -Wpedantic)
31
  target_compile_definitions(ling3_core PUBLIC LING3_WITH_RKNN=$<BOOL:${LING3_WITH_RKNN}>)
32
+ target_compile_definitions(ling3_core PUBLIC LING3_EXPERIMENTAL_MTP=$<BOOL:${LING3_EXPERIMENTAL_MTP}>)
33
 
34
  if(LING3_WITH_ICU)
35
  find_package(ICU COMPONENTS uc i18n QUIET)
README.md CHANGED
@@ -70,6 +70,8 @@ Ling-3.0-rknn 是在 RK3588/RK3588S 上的专用 C++ 推理工程。
70
 
71
  当前限制:一次只执行一个推理请求,忙时可能返回 429;接口没有用户鉴权和 TLS;KV 缓存是进程内资源,不是跨重启的持久聊天记录;128K/256K 只提供容量选项,尚未完成完整板端质量和性能认证;MTP 未启用。
72
 
 
 
73
  ## 板端性能
74
 
75
  下表来自 NanoPi M6(RK3588、16GB)2026-09-22 实测。CPU、NPU、DMC 测试期间固定最高频率;模型为 `ling3-tiny-w4.l3r`,BF16 KV,32K 初始化,关闭 thinking,temperature=0,每次冷启动会话输入后生成 64 tokens,未命中 KV 缓存。TTFT 不包括服务启动时间。
@@ -86,6 +88,8 @@ Ling-3.0-rknn 是在 RK3588/RK3588S 上的专用 C++ 推理工程。
86
 
87
  TTFT 随输入长度增加,解码速度也会因每个新 token 需要访问更长的历史 KV 而下降;因此 128/64 的结果不能代表长上下文表现。完整原始结果和测试环境见 [BOARD_BENCHMARK_AND_INT4_20260922.md](docs/BOARD_BENCHMARK_AND_INT4_20260922.md)。频率、散热和后台负载改变时应重新测量。
88
 
 
 
89
  ## 量化与数值损失
90
 
91
  当前发布模型不是全 W4:部分线性层和专家使用 W4A8,其他指定投影使用 W8A8。默认 KV 为 BF16,KDA 状态保留 FP32。量化存在损失,以下是固定 teacher-forcing 序列、4,322 个预测位置、完整 157,184 词表,以 BF16 为参考的 logits 分布指标:
@@ -137,6 +141,7 @@ python3 tools/pack_model.py \
137
  cmake -S . -B build \
138
  -DCMAKE_BUILD_TYPE=Release \
139
  -DLING3_WITH_RKNN=ON \
 
140
  -DRKNN_ROOT=./vendor/rknpu2
141
  cmake --build build -j4 --target mindnano-infer ling3-rknn
142
  ```
 
70
 
71
  当前限制:一次只执行一个推理请求,忙时可能返回 429;接口没有用户鉴权和 TLS;KV 缓存是进程内资源,不是跨重启的持久聊天记录;128K/256K 只提供容量选项,尚未完成完整板端质量和性能认证;MTP 未启用。
72
 
73
+ MTP 仅作为实验保留,编译开关为 `LING3_EXPERIMENTAL_MTP`,默认 `OFF`。默认构建不包含 MTP 前向实现、实验 API 和 `mtp-benchmark` 命令,不初始化 MTP 权重或 KV。即使显式编译为 `ON`,终端对话和 OpenAI API 也不自动启用 MTP;只有实验探针调用 `EnableMtp()` 后才初始化。实验模型不加入正常部署流程,启用方法见 [MTP 实验说明](docs/MTP_LATEST_20261002.md)。
74
+
75
  ## 板端性能
76
 
77
  下表来自 NanoPi M6(RK3588、16GB)2026-09-22 实测。CPU、NPU、DMC 测试期间固定最高频率;模型为 `ling3-tiny-w4.l3r`,BF16 KV,32K 初始化,关闭 thinking,temperature=0,每次冷启动会话输入后生成 64 tokens,未命中 KV 缓存。TTFT 不包括服务启动时间。
 
88
 
89
  TTFT 随输入长度增加,解码速度也会因每个新 token 需要访问更长的历史 KV 而下降;因此 128/64 的结果不能代表长上下文表现。完整原始结果和测试环境见 [BOARD_BENCHMARK_AND_INT4_20260922.md](docs/BOARD_BENCHMARK_AND_INT4_20260922.md)。频率、散热和后台负载改变时应重新测量。
90
 
91
+ 2026-10-02 保留最新混合模型+MTP 的[实验驻留前向测试](docs/MTP_LATEST_20261002.md):统一 32K 初始化,主模型约 18.69 token/s;MTP 每步追加约 10.54 ms,驻留 RSS 增加约 497 MiB。四条不同序列的下一 token Top1 命中为 17/252,尚未证明投机加速。这些 MTP 指标只适用于显式启用的实验探针,不是默认引擎的开销。实验模型位于 `models/local-experimental/mtp/`,正式 v6 模型、便携发布二进制和默认服务未替换。
92
+
93
  ## 量化与数值损失
94
 
95
  当前发布模型不是全 W4:部分线性层和专家使用 W4A8,其他指定投影使用 W8A8。默认 KV 为 BF16,KDA 状态保留 FP32。量化存在损失,以下是固定 teacher-forcing 序列、4,322 个预测位置、完整 157,184 词表,以 BF16 为参考的 logits 分布指标:
 
141
  cmake -S . -B build \
142
  -DCMAKE_BUILD_TYPE=Release \
143
  -DLING3_WITH_RKNN=ON \
144
+ -DLING3_EXPERIMENTAL_MTP=OFF \
145
  -DRKNN_ROOT=./vendor/rknpu2
146
  cmake --build build -j4 --target mindnano-infer ling3-rknn
147
  ```
docs/MTP_LATEST_20261002.md ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 最新混合量化模型与 MTP 组合测试
2
+
3
+ 2026-10-02,在 NanoPi M6 / RK3588 / 16GB(`10.1.20.33`)测试。
4
+ 结论:最新主模型与 MTP 可以一起加载并连续前向,未改变本组主模型生成
5
+ token;但预测命中偏低,尚未证明投机解码收益。
6
+
7
+ ## 实验开关(默认关闭)
8
+
9
+ `LING3_EXPERIMENTAL_MTP=OFF` 是 CMake 默认值。关闭时,MTP 层、状态、
10
+ 前向函数、实验 API 和 `mtp-benchmark` 命令均不编译;正常推理只运行原主干,
11
+ 不初始化 MTP 权重和 KV。追加了 MTP 的模型文件不会自动激活它。
12
+
13
+ 开启编译也不会把 MTP 加入终端或 OpenAI API 对话流程。`Decoder` 构造函数
14
+ 不再自动加载 sidecar,只有实验探针显式调用 `EnableMtp()` 才会初始化。
15
+ 现有正常生成、KV 复用和服务流程没有调用此接口。
16
+
17
+ 以下性能和内存数据为之前显式运行实验探针的记录,不代表默认构建开销。
18
+ 实验模型保留在 `models/local-experimental/mtp/`,不进入正式 v6 发布包。
19
+ 本地及板端的独立 MTP 编译目录、旧实验二进制已清理;源码、模型和测试记录
20
+ 保留,可按下方命令重新构建。当前源码复现实验时必须单独开启编译开关;默认二进制的
21
+ 帮助信息不包含 `mtp-benchmark`,测试脚本会在锁频前提示编译开关未开启。
22
+
23
+ ## 版本与口径
24
+
25
+ 主干使用本工作区 `models/local-final/v6-board-test/ling3-tiny-w4.l3r`,
26
+ SHA256 为 `80f0086247328fe77b2f939df460b4955287b6f75f6aaada85512c56e652f435`。
27
+ 它是混合 W4A8/W8A8 包,包含 91 个 BF16 源线性权重张量,在运行时走 W8A8;
28
+ 不是上一轮 `minimind-o/Ling3RKNN` 的旧 W4 实验主干。
29
+
30
+ MTP 来自 `inclusionAI/Ling-3.0-tiny-base` 的 layer 24,使用此前已转换的
31
+ W4A8 资产。仅追加 798 个 MTP 张量,原主干 18,029 个张量保留原数据并逐项
32
+ 验证 SHA256;不重新量化主模型。MTP 使用主模型的共享 W8A8 输出头。
33
+
34
+ 组合包:`models/local-experimental/mtp/ling3-tiny-v6-mtp-experimental.l3r`,
35
+ 5,188,395,008 bytes,约 4.832 GiB。
36
+ SHA256 为 `edfc670a0d27d9e5db1a49e18584adb1f47af1120b2060cfa5042ec9332ce8a9`。
37
+ 正式 v6 模型和便携发布二进制未替换。
38
+
39
+ 使用本工作区根目录源码的实验 `HasMtp` / `EvalMtp` 与 `mtp-benchmark`,
40
+ 在板端重新编译。该源码与 README 中 2026-09-22 的 `engine/source/`
41
+ 历史性能测试版本不同,不把新旧结果视为同成本优化对比。
42
+
43
+ 两个模型均初始化 **32K 容量**,实际输入为 128、44、42、39 token,
44
+ 输出均为 64 token;这是短请求测试,不是 32K 实际输入测试。每题在同一个
45
+ 驻留进程中预热一次,再测三轮;切换题目时重置对话状态,保留权重和 RKNN
46
+ context。固定中文性能题、故事题、办公题、解释题均关闭 thinking,贪心解码。
47
+
48
+ CPU 大核 2.304/2.352 GHz、NPU 1 GHz、DMC 2.4 GHz,固定最高频率。
49
+ 启用专家预加载、三核平衡调度、共享输入、SIMD/vector math。
50
+ MLA attention 使用 CPU,GDN 使用 FP32 CPU 路径;线性层和专家矩阵使用 NPU。
51
+ 采样未发现降频,最高温度 58.23°C;推理进程未使用 swap,测试后频率已恢复。
52
+
53
+ ## 连续速度与内存
54
+
55
+ 速度取预热后三轮合计解码时间,均计入首 token 后的 63 次计算。
56
+
57
+ | 项目 | 最新主模型 | 加载 MTP、只运行主模型 | 主模型逐步追加 MTP |
58
+ |---|---:|---:|---:|
59
+ | 四题合计解码速度 | 18.69 token/s | 18.71 token/s | 15.54 token/s |
60
+ | 固定 128/64 题速度 | 18.43 token/s | 18.56 token/s | 15.34 token/s |
61
+ | 主模型每步平均 | 53.35 ms | 53.32 ms | 53.54 ms |
62
+ | MTP 每步平均 | — | — | 10.54 ms |
63
+ | MTP 层 / 输出头 | — | — | 2.03 / 8.51 ms |
64
+ | 预热后进程 RSS | 6,516~6,524 MiB | 7,012~7,021 MiB | 7,012~7,021 MiB |
65
+ | 进程峰值 RSS | 6,952 MiB | 7,021 MiB | 7,021 MiB |
66
+
67
+ RSS 约从 **6.37 GiB 增至 6.86 GiB**,同题稳定态增量约 **497 MiB**。
68
+ MTP 增加一个 MLA 层,32K BF16 KV 本体增加 320 MiB;其他增量包括专家与
69
+ 投影权重、RKNN context 和工作内存。共享输出头没有另复制一整套权重。
70
+ 峰值还包含构造期间临时映射,不能直接用两次冷启动峰值差计算 MTP 的内存增量。
71
+ RSS 包含 NPU 共享映射,是进程统计,不等于整个系统内存差值。
72
+
73
+ 输出头约占 MTP 前向时间的 **81%**。上一轮旧 W4 主干的 MTP 为 6.85 ms、
74
+ 输出头 4.76 ms;最新模型使用共享 W8A8 输出头,不能沿用旧包的成本估算。
75
+
76
+ 本探针逐 token 填满 MTP 缓存:固定 128-token 题的主模型填充约 6.8 秒,
77
+ 追加 MTP 后约 8.1 秒。**这不是正式批量 prefill 的 TTFT**,不能与 README
78
+ 的 753 ms 直接比较。
79
+
80
+ ## 预测与主模型一致性
81
+
82
+ | 问题 | 输入 token | MTP 下一 token Top1 命中 |
83
+ |---|---:|---:|
84
+ | 固定工作安排性能题 | 128 | 3/63,4.76% |
85
+ | 中文故事 | 44 | 6/63,9.52% |
86
+ | 项目会议议程 | 42 | 3/63,4.76% |
87
+ | 长上下文原理解释 | 39 | 5/63,7.94% |
88
+ | 四条不同序列合计 | — | **17/252,6.75%** |
89
+
90
+ 每题三轮相同,重复轮次用于测速度,不能当作更多独立质量样本。
91
+ 加入 MTP 后每轮主模型 Top1 均为 64/64 一致;四题的生成 token hash
92
+ 也与不含 MTP 的原模型一致。这只证明本组生成序列一致,没有完整 logits
93
+ 数值等价认证,也不是通用问答正确率。
94
+
95
+ 目前测的是使用真实主干 hidden 和正确历史的单步 NEXTN;**不是连续预测
96
+ 四个候选后的实际投机接受率**。主模型批量验证、接受前缀提交、拒绝回滚及
97
+ MTP 会话状态恢复尚未实现。直接顺序追加 MTP 会降低速度,当前命中表现
98
+ 也不足以支持“预测四个、接受两个”的假设。
99
+
100
+ 下一步应先对照桌面参考定位预测偏差,包括 MTP 量化损失、主干与 sidecar
101
+ 兼容性、位置和缓存对齐。已核对上游 HF 实现确实传入主模型最终归一化后的
102
+ hidden;不应在没有数值对照时擅自去掉这一归一化。先修预测质量,再做完整
103
+ 投机验证和输出头优化。
104
+
105
+ ## 使用与复现
106
+
107
+ 在 RK3588 上显式开启实验编译,使用独立构建目录:
108
+
109
+ ```bash
110
+ cmake -S . -B build-mtp -DCMAKE_BUILD_TYPE=Release \
111
+ -DLING3_WITH_RKNN=ON -DRKNN_ROOT=./vendor/rknpu2 \
112
+ -DLING3_EXPERIMENTAL_MTP=ON
113
+ cmake --build build-mtp -j4 --target ling3-rknn
114
+ ```
115
+
116
+ 普通发布使用 README 中的默认 `OFF` 配置。CLI 参数示例如下,依赖匹配的
117
+ RKNN/ICU 用户态运行库;定量复现请使用后面的脚本设置优化环境。正式便携
118
+ 二进制尚未加入这个实验 CLI。模型头中的 4096 是原包默认容量,本测试显式
119
+ 传入 32768 初始化,不代表实际输入被限制在 4096。
120
+
121
+ ```bash
122
+ ./build-mtp/ling3-rknn mtp-benchmark \
123
+ models/local-experimental/mtp/ling3-tiny-v6-mtp-experimental.l3r \
124
+ 128 64 3 32768 tests/data/mtp_probe_questions.txt
125
+ ```
126
+
127
+ `tools/mtp_resident_benchmark.py` 会设置优化环境、临时锁频、采样 CPU/NPU、
128
+ 内存及温度,正常结束和 SIGINT/SIGTERM 时恢复原设置:
129
+
130
+ ```bash
131
+ sudo python3 tools/mtp_resident_benchmark.py \
132
+ ./build-mtp/ling3-rknn \
133
+ models/local-experimental/mtp/ling3-tiny-v6-mtp-experimental.l3r \
134
+ --runtime /path/to/rknn/lib --output ./results/mtp-combined-new \
135
+ --context 32768 --seqlen 128 --tokens 64 --rounds 3 \
136
+ --cases tests/data/mtp_probe_questions.txt
137
+ ```
138
+
139
+ 对照时将模型路径换成 `models/local-final/v6-board-test/ling3-tiny-w4.l3r`,
140
+ 使用新的输出目录。脚本拒绝覆盖旧结果,一次只运行一个模型进程。
141
+
142
+ `tools/append_mtp_package.py` 可从含量化 MTP 张量的 `.l3r` 追加 layer 24,
143
+ 不要求原始主模型或重新量化主干:
144
+
145
+ ```bash
146
+ python3 tools/append_mtp_package.py \
147
+ --base models/local-final/v6-board-test/ling3-tiny-w4.l3r \
148
+ --mtp-package /path/to/package-containing-layer24.l3r \
149
+ --output ./work/ling3-tiny-v6-mtp-new.l3r
150
+ ```
151
+
152
+ MTP 缓存目前要求连续填满。跳过前缀、重复提交同一个位置、只恢复主模型
153
+ 缓存后直接调用 MTP 都会被接口检查拒绝;不能把主模型状态复用直接视为
154
+ MTP 状态复用。
155
+
156
+ 主机侧 8 项机制测试通过,包含新增的 MTP 精度族分类检查。板端两组测试
157
+ 均 exit 0,本地生成的组合模型 SHA256 与板端一致。
158
+
159
+ 整理实验开关后,主机侧默认 `OFF` 与显式 `ON` 两种构建分别通过全部
160
+ 8 项测试;检查默认静态库不含 `EnableMtp`、`HasMtp`、`EvalMtp` 导出符号,
161
+ 默认 CLI 不含 `mtp-benchmark`,禁用的实验命令不会打开模型文件。此轮没有
162
+ 重跑板端性能测试,上面的数值仍是此前保存的实测记录。
163
+
164
+ [结果摘要](measurements/mtp-latest-20261002/summary.json)与
165
+ [合包审计](measurements/mtp-latest-20261002/ling3-tiny-v6-mtp-experimental.manifest.json)
166
+ 保存在工作区。完整前向日志、频率元数据和占用采样保留在同目录的
167
+ `baseline-32k/`、`combined-32k/`,以及板端
168
+ `/home/serial/ling3-latest-mtp-20261002/results/`。
docs/README.md CHANGED
@@ -4,6 +4,7 @@
4
 
5
  ## 核心报告
6
 
 
7
  - [BOARD_BENCHMARK_AND_INT4_20260922.md](BOARD_BENCHMARK_AND_INT4_20260922.md):当前板端 128/64 至 16K/64 性能与官方 INT4 数值对比。
8
  - [INT4_BOARD_ACCURACY_20260922.md](INT4_BOARD_ACCURACY_20260922.md):固定 teacher-forcing 条件下的 logits、概率分布和 Top1 一致率。
9
  - [KV_CACHE_BENCHMARK_20260922.md](KV_CACHE_BENCHMARK_20260922.md):BF16/INT8 KV 的容量、内存和长输入性能。
 
4
 
5
  ## 核心报告
6
 
7
+ - [MTP_LATEST_20261002.md](MTP_LATEST_20261002.md):默认不编译的 MTP 实验开关,以及统一 32K 容量的驻留速度、内存和预测命中测试。
8
  - [BOARD_BENCHMARK_AND_INT4_20260922.md](BOARD_BENCHMARK_AND_INT4_20260922.md):当前板端 128/64 至 16K/64 性能与官方 INT4 数值对比。
9
  - [INT4_BOARD_ACCURACY_20260922.md](INT4_BOARD_ACCURACY_20260922.md):固定 teacher-forcing 条件下的 logits、概率分布和 Top1 一致率。
10
  - [KV_CACHE_BENCHMARK_20260922.md](KV_CACHE_BENCHMARK_20260922.md):BF16/INT8 KV 的容量、内存和长输入性能。
docs/measurements/mtp-latest-20261002/baseline-32k/metadata.json ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "started_utc": "2026-10-02T14:32:07.142908+00:00",
3
+ "command": [
4
+ "/home/serial/ling3-latest-mtp-20261002/build/ling3-rknn",
5
+ "mtp-benchmark",
6
+ "/home/serial/ling3-v6-workspace-test/ling3-tiny-w4.l3r",
7
+ "128",
8
+ "64",
9
+ "3",
10
+ "32768",
11
+ "/home/serial/ling3-latest-mtp-20261002/source/tests/data/mtp_probe_questions.txt"
12
+ ],
13
+ "original_clocks": {
14
+ "/sys/devices/system/cpu/cpufreq/policy0/scaling_governor": "ondemand",
15
+ "/sys/devices/system/cpu/cpufreq/policy0/scaling_min_freq": "1800000",
16
+ "/sys/devices/system/cpu/cpufreq/policy4/scaling_governor": "ondemand",
17
+ "/sys/devices/system/cpu/cpufreq/policy4/scaling_min_freq": "408000",
18
+ "/sys/devices/system/cpu/cpufreq/policy6/scaling_governor": "ondemand",
19
+ "/sys/devices/system/cpu/cpufreq/policy6/scaling_min_freq": "408000",
20
+ "/sys/class/devfreq/fdab0000.npu/governor": "rknpu_ondemand",
21
+ "/sys/class/devfreq/fdab0000.npu/min_freq": "300000000",
22
+ "/sys/class/devfreq/dmc/governor": "dmc_ondemand",
23
+ "/sys/class/devfreq/dmc/min_freq": "534000000"
24
+ },
25
+ "meminfo_before": "MemTotal: 16342628 kB\nMemFree: 260916 kB\nMemAvailable: 14314584 kB\nBuffers: 87808 kB\nCached: 13925596 kB\nSwapCached: 0 kB\nActive: 7125980 kB\nInactive: 7658296 kB\nActive(anon): 23304 kB\nInactive(anon): 830208 kB\nActive(file): 7102676 kB\nInactive(file): 6828088 kB\nUnevictable: 14400 kB\nMlocked: 0 kB\nSwapTotal: 8171312 kB\nSwapFree: 8119344 kB\nZswap: 0 kB\nZswapped: 0 kB\nDirty: 316 kB\nWriteback: 0 kB\nAnonPages: 785292 kB\nMapped: 276836 kB\nShmem: 82640 kB\nKReclaimable: 383648 kB\nSlab: 541800 kB\nSReclaimable: 383648 kB\nSUnreclaim: 158152 kB\nKernelStack: 9184 kB\nPageTables: 18632 kB\nSecPageTables: 0 kB\nNFS_Unstable: 0 kB\nBounce: 0 kB\nWritebackTmp: 0 kB\nCommitLimit: 16342624 kB\nCommitted_AS: 4934684 kB\nVmallocTotal: 133143592960 kB\nVmallocUsed: 104164 kB\nVmallocChunk: 0 kB\nPercpu: 7776 kB\nCmaTotal: 786432 kB\nCmaAllocated: 8780 kB\nCmaReleased: 777652 kB\nCmaFree: 588 kB\nHugePages_Total: 0\nHugePages_Free: 0\nHugePages_Rsvd: 0\nHugePages_Surp: 0\nHugepagesize: 2048 kB\nHugetlb: 0 kB",
26
+ "applied_clocks": {
27
+ "/sys/devices/system/cpu/cpufreq/policy0/scaling_governor": "performance",
28
+ "/sys/devices/system/cpu/cpufreq/policy0/scaling_min_freq": "1800000",
29
+ "/sys/devices/system/cpu/cpufreq/policy4/scaling_governor": "performance",
30
+ "/sys/devices/system/cpu/cpufreq/policy4/scaling_min_freq": "2304000",
31
+ "/sys/devices/system/cpu/cpufreq/policy6/scaling_governor": "performance",
32
+ "/sys/devices/system/cpu/cpufreq/policy6/scaling_min_freq": "2352000",
33
+ "/sys/class/devfreq/fdab0000.npu/governor": "performance",
34
+ "/sys/class/devfreq/fdab0000.npu/min_freq": "1000000000",
35
+ "/sys/class/devfreq/dmc/governor": "performance",
36
+ "/sys/class/devfreq/dmc/min_freq": "2400000000"
37
+ },
38
+ "profile": {
39
+ "LING3_PREWARM_EXPERTS": "1",
40
+ "LING3_EXPERT_BALANCED": "1",
41
+ "LING3_EXPERT_ZERO_COPY": "1",
42
+ "LING3_GDN_CPU_PREFILL": "1",
43
+ "LING3_GDN_CPU_FP32_STATE": "1",
44
+ "LING3_GDN_CPU_DECODE": "1",
45
+ "LING3_GDN_FULL_FP32": "1",
46
+ "LING3_MLA_SIMD": "1",
47
+ "LING3_VECTOR_MATH": "1",
48
+ "LING3_MLA_BACKEND": "cpu"
49
+ },
50
+ "exit_code": 0,
51
+ "elapsed_s": 138.48842622793745,
52
+ "restored_clocks": {
53
+ "/sys/class/devfreq/dmc/min_freq": "534000000",
54
+ "/sys/class/devfreq/dmc/governor": "dmc_ondemand",
55
+ "/sys/class/devfreq/fdab0000.npu/min_freq": "300000000",
56
+ "/sys/class/devfreq/fdab0000.npu/governor": "rknpu_ondemand",
57
+ "/sys/devices/system/cpu/cpufreq/policy6/scaling_min_freq": "408000",
58
+ "/sys/devices/system/cpu/cpufreq/policy6/scaling_governor": "ondemand",
59
+ "/sys/devices/system/cpu/cpufreq/policy4/scaling_min_freq": "408000",
60
+ "/sys/devices/system/cpu/cpufreq/policy4/scaling_governor": "ondemand",
61
+ "/sys/devices/system/cpu/cpufreq/policy0/scaling_min_freq": "1800000",
62
+ "/sys/devices/system/cpu/cpufreq/policy0/scaling_governor": "ondemand"
63
+ }
64
+ }
docs/measurements/mtp-latest-20261002/combined-32k/metadata.json ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "started_utc": "2026-10-02T14:35:58.845242+00:00",
3
+ "command": [
4
+ "/home/serial/ling3-latest-mtp-20261002/build/ling3-rknn",
5
+ "mtp-benchmark",
6
+ "/home/serial/ling3-latest-mtp-20261002/artifacts/ling3-tiny-v6-mtp-experimental.l3r",
7
+ "128",
8
+ "64",
9
+ "3",
10
+ "32768",
11
+ "/home/serial/ling3-latest-mtp-20261002/source/tests/data/mtp_probe_questions.txt"
12
+ ],
13
+ "original_clocks": {
14
+ "/sys/devices/system/cpu/cpufreq/policy0/scaling_governor": "ondemand",
15
+ "/sys/devices/system/cpu/cpufreq/policy0/scaling_min_freq": "1800000",
16
+ "/sys/devices/system/cpu/cpufreq/policy4/scaling_governor": "ondemand",
17
+ "/sys/devices/system/cpu/cpufreq/policy4/scaling_min_freq": "408000",
18
+ "/sys/devices/system/cpu/cpufreq/policy6/scaling_governor": "ondemand",
19
+ "/sys/devices/system/cpu/cpufreq/policy6/scaling_min_freq": "408000",
20
+ "/sys/class/devfreq/fdab0000.npu/governor": "rknpu_ondemand",
21
+ "/sys/class/devfreq/fdab0000.npu/min_freq": "300000000",
22
+ "/sys/class/devfreq/dmc/governor": "dmc_ondemand",
23
+ "/sys/class/devfreq/dmc/min_freq": "534000000"
24
+ },
25
+ "meminfo_before": "MemTotal: 16342628 kB\nMemFree: 7260096 kB\nMemAvailable: 14319844 kB\nBuffers: 52036 kB\nCached: 7005492 kB\nSwapCached: 0 kB\nActive: 3938108 kB\nInactive: 3889344 kB\nActive(anon): 28320 kB\nInactive(anon): 830228 kB\nActive(file): 3909788 kB\nInactive(file): 3059116 kB\nUnevictable: 14400 kB\nMlocked: 0 kB\nSwapTotal: 8171312 kB\nSwapFree: 8113968 kB\nZswap: 0 kB\nZswapped: 0 kB\nDirty: 0 kB\nWriteback: 0 kB\nAnonPages: 784344 kB\nMapped: 232044 kB\nShmem: 88876 kB\nKReclaimable: 351588 kB\nSlab: 506672 kB\nSReclaimable: 351588 kB\nSUnreclaim: 155084 kB\nKernelStack: 9104 kB\nPageTables: 18624 kB\nSecPageTables: 0 kB\nNFS_Unstable: 0 kB\nBounce: 0 kB\nWritebackTmp: 0 kB\nCommitLimit: 16342624 kB\nCommitted_AS: 4947628 kB\nVmallocTotal: 133143592960 kB\nVmallocUsed: 104052 kB\nVmallocChunk: 0 kB\nPercpu: 7712 kB\nCmaTotal: 786432 kB\nCmaAllocated: 8780 kB\nCmaReleased: 777652 kB\nCmaFree: 402076 kB\nHugePages_Total: 0\nHugePages_Free: 0\nHugePages_Rsvd: 0\nHugePages_Surp: 0\nHugepagesize: 2048 kB\nHugetlb: 0 kB",
26
+ "applied_clocks": {
27
+ "/sys/devices/system/cpu/cpufreq/policy0/scaling_governor": "performance",
28
+ "/sys/devices/system/cpu/cpufreq/policy0/scaling_min_freq": "1800000",
29
+ "/sys/devices/system/cpu/cpufreq/policy4/scaling_governor": "performance",
30
+ "/sys/devices/system/cpu/cpufreq/policy4/scaling_min_freq": "2304000",
31
+ "/sys/devices/system/cpu/cpufreq/policy6/scaling_governor": "performance",
32
+ "/sys/devices/system/cpu/cpufreq/policy6/scaling_min_freq": "2352000",
33
+ "/sys/class/devfreq/fdab0000.npu/governor": "performance",
34
+ "/sys/class/devfreq/fdab0000.npu/min_freq": "1000000000",
35
+ "/sys/class/devfreq/dmc/governor": "performance",
36
+ "/sys/class/devfreq/dmc/min_freq": "2400000000"
37
+ },
38
+ "profile": {
39
+ "LING3_PREWARM_EXPERTS": "1",
40
+ "LING3_EXPERT_BALANCED": "1",
41
+ "LING3_EXPERT_ZERO_COPY": "1",
42
+ "LING3_GDN_CPU_PREFILL": "1",
43
+ "LING3_GDN_CPU_FP32_STATE": "1",
44
+ "LING3_GDN_CPU_DECODE": "1",
45
+ "LING3_GDN_FULL_FP32": "1",
46
+ "LING3_MLA_SIMD": "1",
47
+ "LING3_VECTOR_MATH": "1",
48
+ "LING3_MLA_BACKEND": "cpu"
49
+ },
50
+ "exit_code": 0,
51
+ "elapsed_s": 262.595832146937,
52
+ "restored_clocks": {
53
+ "/sys/class/devfreq/dmc/min_freq": "534000000",
54
+ "/sys/class/devfreq/dmc/governor": "dmc_ondemand",
55
+ "/sys/class/devfreq/fdab0000.npu/min_freq": "300000000",
56
+ "/sys/class/devfreq/fdab0000.npu/governor": "rknpu_ondemand",
57
+ "/sys/devices/system/cpu/cpufreq/policy6/scaling_min_freq": "408000",
58
+ "/sys/devices/system/cpu/cpufreq/policy6/scaling_governor": "ondemand",
59
+ "/sys/devices/system/cpu/cpufreq/policy4/scaling_min_freq": "408000",
60
+ "/sys/devices/system/cpu/cpufreq/policy4/scaling_governor": "ondemand",
61
+ "/sys/devices/system/cpu/cpufreq/policy0/scaling_min_freq": "1800000",
62
+ "/sys/devices/system/cpu/cpufreq/policy0/scaling_governor": "ondemand"
63
+ }
64
+ }
docs/measurements/mtp-latest-20261002/ling3-tiny-v6-mtp-experimental.manifest.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "base": "ling3-tiny-w4.l3r",
3
+ "base_sha256": "80f0086247328fe77b2f939df460b4955287b6f75f6aaada85512c56e652f435",
4
+ "mtp_package": "ling3-tiny-w4-mtp-experimental.l3r",
5
+ "mtp_source_sha256": "27911ca182d5a16d3c8e80e069e60a893ceb77f6ddfa3ac3da4182e5ed2e0485",
6
+ "output": "ling3-tiny-v6-mtp-experimental.l3r",
7
+ "output_sha256": "edfc670a0d27d9e5db1a49e18584adb1f47af1120b2060cfa5042ec9332ce8a9",
8
+ "bytes": 5188395008,
9
+ "flags": 513,
10
+ "trunk_tensors_unchanged": 18029,
11
+ "appended_mtp_tensors": 798,
12
+ "recomputed_quantization": false
13
+ }
docs/measurements/mtp-latest-20261002/summary.json ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "test": "resident NEXTN forward, not speculative decoding",
3
+ "context_capacity": 32768,
4
+ "new_tokens": 64,
5
+ "measured_rounds": 3,
6
+ "cases": [
7
+ {
8
+ "case": 0,
9
+ "label": "fixed_128_token_work_prompt",
10
+ "input_tokens": 128,
11
+ "baseline": {
12
+ "decode_wall_tokens_per_second": 18.429956132608858,
13
+ "target_mean_ms": 54.117866666666664,
14
+ "rss_mib_min": 6515.7969,
15
+ "rss_mib_max": 6515.7969,
16
+ "process_hwm_mib": 6952.4688
17
+ },
18
+ "combined_target_only": {
19
+ "decode_wall_tokens_per_second": 18.564440680217608,
20
+ "target_mean_ms": 53.7246,
21
+ "rss_mib_min": 7011.7422,
22
+ "rss_mib_max": 7011.7422,
23
+ "process_hwm_mib": 7011.7422
24
+ },
25
+ "target_plus_mtp": {
26
+ "decode_wall_tokens_per_second": 15.343605430662127,
27
+ "target_mean_ms": 54.265033333333335,
28
+ "rss_mib_min": 7011.7422,
29
+ "rss_mib_max": 7011.7422,
30
+ "process_hwm_mib": 7011.7422,
31
+ "mtp_mean_ms": 10.624833333333333,
32
+ "mtp_layers_mean_ms": 2.1126666666666667,
33
+ "mtp_head_mean_ms": 8.5121,
34
+ "mtp_p95_ms": 10.869200000000001
35
+ },
36
+ "teacher_hash": "658009ec40b8a851",
37
+ "predictions_unchanged": true,
38
+ "draft_top1_hits": 3,
39
+ "draft_positions": 63
40
+ },
41
+ {
42
+ "case": 1,
43
+ "label": "story",
44
+ "input_tokens": 44,
45
+ "baseline": {
46
+ "decode_wall_tokens_per_second": 18.7953734859991,
47
+ "target_mean_ms": 53.06296666666667,
48
+ "rss_mib_min": 6519.8555,
49
+ "rss_mib_max": 6519.8555,
50
+ "process_hwm_mib": 6952.4688
51
+ },
52
+ "combined_target_only": {
53
+ "decode_wall_tokens_per_second": 18.82802610460991,
54
+ "target_mean_ms": 52.9705,
55
+ "rss_mib_min": 7016.4453,
56
+ "rss_mib_max": 7016.4453,
57
+ "process_hwm_mib": 7016.4453
58
+ },
59
+ "target_plus_mtp": {
60
+ "decode_wall_tokens_per_second": 15.614778103593295,
61
+ "target_mean_ms": 53.24966666666667,
62
+ "rss_mib_min": 7016.4453,
63
+ "rss_mib_max": 7016.4453,
64
+ "process_hwm_mib": 7016.4453,
65
+ "mtp_mean_ms": 10.5083,
66
+ "mtp_layers_mean_ms": 2.0058,
67
+ "mtp_head_mean_ms": 8.502466666666667,
68
+ "mtp_p95_ms": 10.743
69
+ },
70
+ "teacher_hash": "871f43b0902ff7a0",
71
+ "predictions_unchanged": true,
72
+ "draft_top1_hits": 6,
73
+ "draft_positions": 63
74
+ },
75
+ {
76
+ "case": 2,
77
+ "label": "office_meeting",
78
+ "input_tokens": 42,
79
+ "baseline": {
80
+ "decode_wall_tokens_per_second": 18.730715539100714,
81
+ "target_mean_ms": 53.2465,
82
+ "rss_mib_min": 6521.8945,
83
+ "rss_mib_max": 6521.8945,
84
+ "process_hwm_mib": 6952.4688
85
+ },
86
+ "combined_target_only": {
87
+ "decode_wall_tokens_per_second": 18.6898446583178,
88
+ "target_mean_ms": 53.36313333333333,
89
+ "rss_mib_min": 7018.6602,
90
+ "rss_mib_max": 7018.6602,
91
+ "process_hwm_mib": 7018.6602
92
+ },
93
+ "target_plus_mtp": {
94
+ "decode_wall_tokens_per_second": 15.613767923149531,
95
+ "target_mean_ms": 53.2464,
96
+ "rss_mib_min": 7018.6602,
97
+ "rss_mib_max": 7018.6602,
98
+ "process_hwm_mib": 7018.6602,
99
+ "mtp_mean_ms": 10.515466666666667,
100
+ "mtp_layers_mean_ms": 2.0067666666666666,
101
+ "mtp_head_mean_ms": 8.5087,
102
+ "mtp_p95_ms": 10.748233333333333
103
+ },
104
+ "teacher_hash": "34da8f0ff021a5e",
105
+ "predictions_unchanged": true,
106
+ "draft_top1_hits": 3,
107
+ "draft_positions": 63
108
+ },
109
+ {
110
+ "case": 3,
111
+ "label": "long_context_explanation",
112
+ "input_tokens": 39,
113
+ "baseline": {
114
+ "decode_wall_tokens_per_second": 18.826060646809655,
115
+ "target_mean_ms": 52.97593333333333,
116
+ "rss_mib_min": 6524.0742,
117
+ "rss_mib_max": 6524.0742,
118
+ "process_hwm_mib": 6952.4688
119
+ },
120
+ "combined_target_only": {
121
+ "decode_wall_tokens_per_second": 18.744271096191156,
122
+ "target_mean_ms": 53.207766666666664,
123
+ "rss_mib_min": 7020.9922,
124
+ "rss_mib_max": 7020.9922,
125
+ "process_hwm_mib": 7020.9922
126
+ },
127
+ "target_plus_mtp": {
128
+ "decode_wall_tokens_per_second": 15.580993868400766,
129
+ "target_mean_ms": 53.38976666666667,
130
+ "rss_mib_min": 7020.9922,
131
+ "rss_mib_max": 7020.9922,
132
+ "process_hwm_mib": 7020.9922,
133
+ "mtp_mean_ms": 10.506966666666667,
134
+ "mtp_layers_mean_ms": 2.0087,
135
+ "mtp_head_mean_ms": 8.498233333333333,
136
+ "mtp_p95_ms": 10.7371
137
+ },
138
+ "teacher_hash": "d5dafb8e2a4e1f3f",
139
+ "predictions_unchanged": true,
140
+ "draft_top1_hits": 5,
141
+ "draft_positions": 63
142
+ }
143
+ ],
144
+ "baseline-32k": {
145
+ "initialization_ms": 26056.0407,
146
+ "clocks_restored": true,
147
+ "maximum_temperature_C": 57.307,
148
+ "minimum_system_mem_available_mib": 7443.515625,
149
+ "maximum_process_swap_kib": 0
150
+ },
151
+ "combined-32k": {
152
+ "initialization_ms": 21645.1046,
153
+ "clocks_restored": true,
154
+ "maximum_temperature_C": 58.23,
155
+ "minimum_system_mem_available_mib": 6909.07421875,
156
+ "maximum_process_swap_kib": 0
157
+ },
158
+ "distinct_draft_positions": 252,
159
+ "distinct_draft_hits": 17,
160
+ "draft_top1_agreement": 0.06746031746031746,
161
+ "package": {
162
+ "base": "ling3-tiny-w4.l3r",
163
+ "base_sha256": "80f0086247328fe77b2f939df460b4955287b6f75f6aaada85512c56e652f435",
164
+ "mtp_package": "ling3-tiny-w4-mtp-experimental.l3r",
165
+ "mtp_source_sha256": "27911ca182d5a16d3c8e80e069e60a893ceb77f6ddfa3ac3da4182e5ed2e0485",
166
+ "output": "ling3-tiny-v6-mtp-experimental.l3r",
167
+ "output_sha256": "edfc670a0d27d9e5db1a49e18584adb1f47af1120b2060cfa5042ec9332ce8a9",
168
+ "bytes": 5188395008,
169
+ "flags": 513,
170
+ "trunk_tensors_unchanged": 18029,
171
+ "appended_mtp_tensors": 798,
172
+ "recomputed_quantization": false
173
+ },
174
+ "aggregate": {
175
+ "baseline-32k/target_only": {
176
+ "decode_wall_tokens_per_second": 18.69419444496819,
177
+ "target_mean_ms": 53.35081666666667,
178
+ "rss_mib_min": 6515.7969,
179
+ "rss_mib_max": 6524.0742,
180
+ "process_hwm_mib": 6952.4688
181
+ },
182
+ "combined-32k/target_only": {
183
+ "decode_wall_tokens_per_second": 18.70615503663274,
184
+ "target_mean_ms": 53.3165,
185
+ "rss_mib_min": 7011.7422,
186
+ "rss_mib_max": 7020.9922,
187
+ "process_hwm_mib": 7020.9922
188
+ },
189
+ "combined-32k/target_plus_mtp": {
190
+ "decode_wall_tokens_per_second": 15.537454676617868,
191
+ "target_mean_ms": 53.53771666666667,
192
+ "rss_mib_min": 7011.7422,
193
+ "rss_mib_max": 7020.9922,
194
+ "process_hwm_mib": 7020.9922,
195
+ "mtp_mean_ms": 10.538891666666666,
196
+ "mtp_layers_mean_ms": 2.0334833333333333,
197
+ "mtp_head_mean_ms": 8.505375
198
+ }
199
+ }
200
+ }
include/ling3/decoder.h CHANGED
@@ -39,6 +39,14 @@ public:
39
  std::size_t RestoreState(const DecoderState & state);
40
  const std::string & StateSignature() const;
41
  DecodeTimings Eval(std::uint32_t token, std::span<float> logits);
 
 
 
 
 
 
 
 
42
  DecodeTimings EvalBatch(
43
  std::span<const std::uint32_t> tokens,
44
  std::span<float> logits);
 
39
  std::size_t RestoreState(const DecoderState & state);
40
  const std::string & StateSignature() const;
41
  DecodeTimings Eval(std::uint32_t token, std::span<float> logits);
42
+ #if LING3_EXPERIMENTAL_MTP
43
+ // Only the explicit experiment initializes MTP; normal chat does not.
44
+ bool EnableMtp();
45
+ // Experimental NEXTN probe; requires a contiguous MTP prefix. Restored
46
+ // trunk states and batch prefill alone do not populate the MTP cache.
47
+ bool HasMtp() const noexcept;
48
+ DecodeTimings EvalMtp(std::uint32_t next_token, std::span<float> logits);
49
+ #endif
50
  DecodeTimings EvalBatch(
51
  std::span<const std::uint32_t> tokens,
52
  std::span<float> logits);
include/ling3/precision_policy.h CHANGED
@@ -25,9 +25,19 @@ inline std::string W8MatrixFamily(const std::string& base) {
25
  constexpr std::string_view prefix="model.layers.";
26
  if(!base.starts_with(prefix))throw std::invalid_argument("invalid attention matrix name");
27
  int layer=-1;auto [end,ec]=std::from_chars(base.data()+prefix.size(),base.data()+base.size(),layer);
28
- if(ec!=std::errc{}||end==base.data()+base.size()||*end!='.'||layer<0||layer>=24)
 
 
 
 
 
29
  throw std::invalid_argument("invalid attention layer");
 
 
 
 
30
  return std::string((layer+1)%4==0?"mla_":"kda_")+(base.ends_with(".o_proj")?"out":"in");
 
31
  }
32
  if(base.find(".mlp.shared_experts.")!=std::string::npos)return "shared";
33
  if(base=="model.layers.0.mlp.gate_up"||base=="model.layers.0.mlp.down_proj")return "dense";
 
25
  constexpr std::string_view prefix="model.layers.";
26
  if(!base.starts_with(prefix))throw std::invalid_argument("invalid attention matrix name");
27
  int layer=-1;auto [end,ec]=std::from_chars(base.data()+prefix.size(),base.data()+base.size(),layer);
28
+ #if LING3_EXPERIMENTAL_MTP
29
+ constexpr int maximum_layer=24;
30
+ #else
31
+ constexpr int maximum_layer=23;
32
+ #endif
33
+ if(ec!=std::errc{}||end==base.data()+base.size()||*end!='.'||layer<0||layer>maximum_layer)
34
  throw std::invalid_argument("invalid attention layer");
35
+ #if LING3_EXPERIMENTAL_MTP
36
+ // Optional layer 24 is MTP MLA, outside the main 24-layer trunk.
37
+ return std::string((layer==24||(layer+1)%4==0)?"mla_":"kda_")+(base.ends_with(".o_proj")?"out":"in");
38
+ #else
39
  return std::string((layer+1)%4==0?"mla_":"kda_")+(base.ends_with(".o_proj")?"out":"in");
40
+ #endif
41
  }
42
  if(base.find(".mlp.shared_experts.")!=std::string::npos)return "shared";
43
  if(base=="model.layers.0.mlp.gate_up"||base=="model.layers.0.mlp.down_proj")return "dense";
models/local-experimental/mtp/README.md ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MTP 实验模型
2
+
3
+ 本目录仅供 NEXTN/MTP 驻留前向测试,不属于默认部署流程。尚未实现完整的投机验证、接受前缀提交和拒绝回滚;当前测试未证明加速收益。
4
+
5
+ 组合模型为 `ling3-tiny-v6-mtp-experimental.l3r`(5,188,395,008 bytes):
6
+
7
+ - 主干来自正式 v6 混合 W4A8/W8A8 模型,原 18,029 个张量数据保持不变。
8
+ - MTP 来自 [inclusionAI/Ling-3.0-tiny-base](https://huggingface.co/inclusionAI/Ling-3.0-tiny-base) 的 layer 24,追加 798 个转换张量。
9
+ - SHA256:`edfc670a0d27d9e5db1a49e18584adb1f47af1120b2060cfa5042ec9332ce8a9`。
10
+
11
+ 本项目转换模型资产使用 [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/),许可边界见 [LICENSES.md](../../../LICENSES.md)。上游来源及其原有许可须保留。
12
+
13
+ 需要单独使用 `-DLING3_EXPERIMENTAL_MTP=ON` 编译;编译产物不随本目录发布。默认 `OFF` 不编译 MTP,终端和 OpenAI API 对话不会自动启用它。详细命令、内存、速度和预测命中见 [实验报告](../../../docs/MTP_LATEST_20261002.md)。
14
+
15
+ 此包包含完整主干,约 4.832 GiB。普通推理只需下载正式 v6 模型,无需下载本目录。
models/local-experimental/mtp/ling3-tiny-v6-mtp-experimental.manifest.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "base": "ling3-tiny-w4.l3r",
3
+ "base_sha256": "80f0086247328fe77b2f939df460b4955287b6f75f6aaada85512c56e652f435",
4
+ "mtp_package": "ling3-tiny-w4-mtp-experimental.l3r",
5
+ "mtp_source_sha256": "27911ca182d5a16d3c8e80e069e60a893ceb77f6ddfa3ac3da4182e5ed2e0485",
6
+ "output": "ling3-tiny-v6-mtp-experimental.l3r",
7
+ "output_sha256": "edfc670a0d27d9e5db1a49e18584adb1f47af1120b2060cfa5042ec9332ce8a9",
8
+ "bytes": 5188395008,
9
+ "flags": 513,
10
+ "trunk_tensors_unchanged": 18029,
11
+ "appended_mtp_tensors": 798,
12
+ "recomputed_quantization": false
13
+ }
src/decoder.cpp CHANGED
@@ -133,10 +133,19 @@ std::unique_ptr<Linear> MakeLinear(
133
  const auto begin = base.data() + layer_prefix.size();
134
  const auto end = base.data() + base.size();
135
  const auto [parsed_end, error] = std::from_chars(begin, end, layer);
136
- if (error != std::errc {} || parsed_end == begin || layer < 0 || layer >= 24) {
 
 
 
 
 
137
  throw std::runtime_error("cannot derive W4 IOMMU domain from " + base);
138
  }
 
 
 
139
  iommu_domain_id = 2 + layer / 2;
 
140
  } else if (base == "lm_head") {
141
  iommu_domain_id = 14;
142
  }
@@ -1414,6 +1423,62 @@ private:
1414
  bool reuse_batch_input_ = false;
1415
  };
1416
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1417
  class DecoderLayer {
1418
  public:
1419
  DecoderLayer(
@@ -1543,6 +1608,12 @@ struct Decoder::Impl {
1543
  std::vector<float> final_norm;
1544
  DecoderScratch scratch; // Declared before layers: outlives all borrowers.
1545
  std::vector<std::unique_ptr<DecoderLayer>> layers;
 
 
 
 
 
 
1546
  std::unique_ptr<Linear> lm_head;
1547
  std::vector<float> hidden;
1548
  std::vector<float> normalized;
@@ -1595,11 +1666,31 @@ struct Decoder::Impl {
1595
  lm_head = MakeLinear(package, "lm_head");
1596
  }
1597
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1598
  void Reset() {
1599
  for (auto & weak : checkpoints) if (auto checkpoint = weak.lock()) checkpoint->valid = false;
1600
  checkpoints.clear();
1601
  current_position = 0;
1602
  for (auto & layer : layers) layer->Reset();
 
 
 
 
 
1603
  }
1604
 
1605
  void InvalidateOverwrittenCheckpoints() {
@@ -1649,6 +1740,9 @@ struct Decoder::Impl {
1649
  throw std::invalid_argument("stale or foreign decoder checkpoint");
1650
  for (std::size_t i = 0; i < layers.size(); ++i) layers[i]->RestoreCheckpoint(checkpoint.layers[i]);
1651
  current_position = checkpoint.position;
 
 
 
1652
  return current_position;
1653
  }
1654
 
@@ -1678,6 +1772,9 @@ struct Decoder::Impl {
1678
  lm_head->Run(normalized, logits);
1679
  const auto end = Clock::now();
1680
  ++current_position;
 
 
 
1681
  return {
1682
  Milliseconds(begin, layers_end),
1683
  Milliseconds(layers_end, end),
@@ -1727,6 +1824,9 @@ struct Decoder::Impl {
1727
  }
1728
  const auto end = Clock::now();
1729
  current_position += rows;
 
 
 
1730
  return {
1731
  Milliseconds(begin, layers_end),
1732
  Milliseconds(layers_end, end),
@@ -1734,6 +1834,28 @@ struct Decoder::Impl {
1734
  };
1735
  }
1736
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1737
  void PrepareBatch(std::size_t rows) {
1738
  if (rows < 1 || rows > kMaxBatch) {
1739
  throw std::invalid_argument("decoder batch rows must be in [1, 128]");
@@ -1756,6 +1878,14 @@ MlaBackendStats Decoder::AttentionStats() const { return impl_->scratch.mla.npu.
1756
  DecodeTimings Decoder::Eval(std::uint32_t token, std::span<float> logits) {
1757
  return impl_->Eval(token, logits);
1758
  }
 
 
 
 
 
 
 
 
1759
  DecodeTimings Decoder::EvalBatch(
1760
  std::span<const std::uint32_t> tokens,
1761
  std::span<float> logits) {
 
133
  const auto begin = base.data() + layer_prefix.size();
134
  const auto end = base.data() + base.size();
135
  const auto [parsed_end, error] = std::from_chars(begin, end, layer);
136
+ #if LING3_EXPERIMENTAL_MTP
137
+ constexpr int maximum_layer = 24;
138
+ #else
139
+ constexpr int maximum_layer = 23;
140
+ #endif
141
+ if (error != std::errc {} || parsed_end == begin || layer < 0 || layer > maximum_layer) {
142
  throw std::runtime_error("cannot derive W4 IOMMU domain from " + base);
143
  }
144
+ #if LING3_EXPERIMENTAL_MTP
145
+ iommu_domain_id = layer == 24 ? 15 : 2 + layer / 2;
146
+ #else
147
  iommu_domain_id = 2 + layer / 2;
148
+ #endif
149
  } else if (base == "lm_head") {
150
  iommu_domain_id = 14;
151
  }
 
1423
  bool reuse_batch_input_ = false;
1424
  };
1425
 
1426
+ #if LING3_EXPERIMENTAL_MTP
1427
+ // Layer 24 from the optional Ling NEXTN/MTP sidecar. It is deliberately
1428
+ // separate from DecoderLayer: the main trunk has KDA/MLA grouping and its
1429
+ // state/checkpoints, while MTP consumes the trunk's normalized hidden state
1430
+ // together with the embedding of the candidate token.
1431
+ class MtpLayer {
1432
+ public:
1433
+ MtpLayer(const ModelPackage & package, std::size_t max_context, DecoderScratch & scratch)
1434
+ : input_norm_(DecodeBf16(package.tensor("model.layers.24.input_layernorm.weight"), kHidden)),
1435
+ post_norm_(DecodeBf16(package.tensor("model.layers.24.post_attention_layernorm.weight"), kHidden)),
1436
+ embedding_norm_(DecodeBf16(package.tensor("model.layers.24.enorm.weight"), kHidden)),
1437
+ hidden_norm_(DecodeBf16(package.tensor("model.layers.24.hnorm.weight"), kHidden)),
1438
+ final_norm_(DecodeBf16(package.tensor("model.layers.24.final_layernorm.weight"), kHidden)),
1439
+ embedding_hidden_(kHidden * 2), projected_(kHidden), residual_(kHidden),
1440
+ normalized_(kHidden), attention_output_(kHidden), ffn_output_(kHidden),
1441
+ eh_projection_(MakeLinear(package, "model.layers.24.eh_proj")),
1442
+ attention_(std::make_unique<MlaAttention>(package, 24, max_context, scratch.mla)),
1443
+ feed_forward_(std::make_unique<SparseFeedForward>(package, 24, scratch.sparse)) {}
1444
+
1445
+ void Reset() { attention_->Reset(); }
1446
+
1447
+ void Run(std::span<const float> token_embedding, std::span<const float> trunk_hidden,
1448
+ std::size_t position, std::span<float> output) {
1449
+ if (token_embedding.size() != kHidden || trunk_hidden.size() != kHidden ||
1450
+ output.size() != kHidden) {
1451
+ throw std::invalid_argument("MTP hidden shape mismatch");
1452
+ }
1453
+ RmsNorm(token_embedding.data(), embedding_norm_.data(), embedding_hidden_.data(), kHidden, kEpsilon);
1454
+ RmsNorm(trunk_hidden.data(), hidden_norm_.data(), embedding_hidden_.data() + kHidden, kHidden, kEpsilon);
1455
+ eh_projection_->Run(embedding_hidden_, projected_);
1456
+ std::copy(projected_.begin(), projected_.end(), residual_.begin());
1457
+ RmsNorm(residual_.data(), input_norm_.data(), normalized_.data(), kHidden, kEpsilon);
1458
+ attention_->Run(normalized_, position, attention_output_);
1459
+ for (int i = 0; i < kHidden; ++i) residual_[i] += attention_output_[i];
1460
+ RmsNorm(residual_.data(), post_norm_.data(), normalized_.data(), kHidden, kEpsilon);
1461
+ feed_forward_->Run(normalized_, ffn_output_);
1462
+ for (int i = 0; i < kHidden; ++i) residual_[i] += ffn_output_[i];
1463
+ RmsNorm(residual_.data(), final_norm_.data(), output.data(), kHidden, kEpsilon);
1464
+ }
1465
+
1466
+ void PrepareBatch(std::size_t rows) {
1467
+ eh_projection_->PrepareBatch(rows);
1468
+ attention_->PrepareBatch(rows);
1469
+ feed_forward_->PrepareBatch(rows);
1470
+ }
1471
+
1472
+ private:
1473
+ std::vector<float> input_norm_, post_norm_, embedding_norm_, hidden_norm_, final_norm_;
1474
+ std::vector<float> embedding_hidden_, projected_, residual_, normalized_, attention_output_, ffn_output_;
1475
+ std::unique_ptr<Linear> eh_projection_;
1476
+ std::unique_ptr<MlaAttention> attention_;
1477
+ std::unique_ptr<SparseFeedForward> feed_forward_;
1478
+ };
1479
+
1480
+ #endif
1481
+
1482
  class DecoderLayer {
1483
  public:
1484
  DecoderLayer(
 
1608
  std::vector<float> final_norm;
1609
  DecoderScratch scratch; // Declared before layers: outlives all borrowers.
1610
  std::vector<std::unique_ptr<DecoderLayer>> layers;
1611
+ #if LING3_EXPERIMENTAL_MTP
1612
+ std::unique_ptr<MtpLayer> mtp;
1613
+ std::vector<float> mtp_hidden, mtp_embedding;
1614
+ std::size_t mtp_position = 0;
1615
+ bool mtp_trunk_ready = false;
1616
+ #endif
1617
  std::unique_ptr<Linear> lm_head;
1618
  std::vector<float> hidden;
1619
  std::vector<float> normalized;
 
1666
  lm_head = MakeLinear(package, "lm_head");
1667
  }
1668
 
1669
+ #if LING3_EXPERIMENTAL_MTP
1670
+ bool EnableMtp() {
1671
+ if (current_position != 0)
1672
+ throw std::invalid_argument("MTP must be enabled before processing the prefix");
1673
+ if (mtp) return true;
1674
+ if (std::any_of(package.tensors().begin(), package.tensors().end(),
1675
+ [](const TensorView & t) { return t.name == "model.layers.24.eh_proj.weight"; })) {
1676
+ mtp = std::make_unique<MtpLayer>(package, context_capacity, scratch);
1677
+ mtp_hidden.resize(kHidden);
1678
+ mtp_embedding.resize(kHidden);
1679
+ }
1680
+ return mtp != nullptr;
1681
+ }
1682
+ #endif
1683
+
1684
  void Reset() {
1685
  for (auto & weak : checkpoints) if (auto checkpoint = weak.lock()) checkpoint->valid = false;
1686
  checkpoints.clear();
1687
  current_position = 0;
1688
  for (auto & layer : layers) layer->Reset();
1689
+ #if LING3_EXPERIMENTAL_MTP
1690
+ if (mtp) mtp->Reset();
1691
+ mtp_position = 0;
1692
+ mtp_trunk_ready = false;
1693
+ #endif
1694
  }
1695
 
1696
  void InvalidateOverwrittenCheckpoints() {
 
1740
  throw std::invalid_argument("stale or foreign decoder checkpoint");
1741
  for (std::size_t i = 0; i < layers.size(); ++i) layers[i]->RestoreCheckpoint(checkpoint.layers[i]);
1742
  current_position = checkpoint.position;
1743
+ #if LING3_EXPERIMENTAL_MTP
1744
+ mtp_trunk_ready = false;
1745
+ #endif
1746
  return current_position;
1747
  }
1748
 
 
1772
  lm_head->Run(normalized, logits);
1773
  const auto end = Clock::now();
1774
  ++current_position;
1775
+ #if LING3_EXPERIMENTAL_MTP
1776
+ mtp_trunk_ready = true;
1777
+ #endif
1778
  return {
1779
  Milliseconds(begin, layers_end),
1780
  Milliseconds(layers_end, end),
 
1824
  }
1825
  const auto end = Clock::now();
1826
  current_position += rows;
1827
+ #if LING3_EXPERIMENTAL_MTP
1828
+ mtp_trunk_ready = compute_logits;
1829
+ #endif
1830
  return {
1831
  Milliseconds(begin, layers_end),
1832
  Milliseconds(layers_end, end),
 
1834
  };
1835
  }
1836
 
1837
+ #if LING3_EXPERIMENTAL_MTP
1838
+ DecodeTimings EvalMtp(std::uint32_t token, std::span<float> logits) {
1839
+ if (!mtp) throw std::runtime_error("model package has no MTP layer");
1840
+ if (token >= package.header().vocab_size || logits.size() != package.header().vocab_size ||
1841
+ current_position == 0 || !mtp_trunk_ready || mtp_position != current_position - 1)
1842
+ throw std::invalid_argument("MTP requires a contiguous, aligned prefix and valid token/logits");
1843
+ const auto begin = Clock::now();
1844
+ const auto embedding = embeddings.subspan(static_cast<std::size_t>(token) * kHidden, kHidden);
1845
+ for (int i = 0; i < kHidden; ++i) mtp_embedding[i] = BFloat16ToFloat(embedding[i]);
1846
+ // Upstream passes the main model's final normalized hidden state.
1847
+ // Keep MTP output separate so probing cannot overwrite trunk scratch.
1848
+ mtp->Run(mtp_embedding, normalized, mtp_position, mtp_hidden);
1849
+ const auto layers_end = Clock::now();
1850
+ lm_head->Run(mtp_hidden, logits);
1851
+ const auto end = Clock::now();
1852
+ ++mtp_position;
1853
+ mtp_trunk_ready = false;
1854
+ return {Milliseconds(begin, layers_end), Milliseconds(layers_end, end), Milliseconds(begin, end)};
1855
+ }
1856
+
1857
+ #endif
1858
+
1859
  void PrepareBatch(std::size_t rows) {
1860
  if (rows < 1 || rows > kMaxBatch) {
1861
  throw std::invalid_argument("decoder batch rows must be in [1, 128]");
 
1878
  DecodeTimings Decoder::Eval(std::uint32_t token, std::span<float> logits) {
1879
  return impl_->Eval(token, logits);
1880
  }
1881
+ #if LING3_EXPERIMENTAL_MTP
1882
+ bool Decoder::EnableMtp() { return impl_->EnableMtp(); }
1883
+ bool Decoder::HasMtp() const noexcept { return impl_->mtp != nullptr; }
1884
+ DecodeTimings Decoder::EvalMtp(std::uint32_t token, std::span<float> logits) {
1885
+ return impl_->EvalMtp(token, logits);
1886
+ }
1887
+ #endif
1888
+
1889
  DecodeTimings Decoder::EvalBatch(
1890
  std::span<const std::uint32_t> tokens,
1891
  std::span<float> logits) {
src/main.cpp CHANGED
@@ -64,6 +64,9 @@ void PrintUsage(const char * program) {
64
  << " " << program << " logits-smoke MODEL.l3r TOKEN [REF.f32]\n"
65
  << " " << program << " generate MODEL.l3r USER_TEXT [MAX_NEW_TOKENS]\n"
66
  << " " << program << " benchmark MODEL.l3r SEQLEN NEW_TOKENS [ITERATIONS]\n"
 
 
 
67
  << " " << program << " sequence-check MODEL.l3r SEQLEN NEW_TOKENS OUT_PREFIX [REFERENCE_PREFIX [CORPUS.txt]]\n"
68
  << " " << program << " serve MODEL.l3r [PORT]\n";
69
  }
@@ -1209,6 +1212,130 @@ struct BenchmarkSample {
1209
  ProcessMemory memory;
1210
  };
1211
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1212
  int RunBenchmark(
1213
  const std::string & package_path,
1214
  std::size_t sequence_length,
@@ -1449,6 +1576,13 @@ int main(int argc, char ** argv) {
1449
  const std::size_t iterations = argc == 6 ? ParseSize(argv[5], "iterations") : 3;
1450
  return RunBenchmark(argv[2], sequence_length, new_tokens, iterations);
1451
  }
 
 
 
 
 
 
 
1452
  if (command == "sequence-check" && argc >= 6 && argc <= 8) {
1453
  return RunSequenceCheck(argv[2], ParseSize(argv[3], "sequence length"),
1454
  ParseSize(argv[4], "new tokens"), argv[5],
 
64
  << " " << program << " logits-smoke MODEL.l3r TOKEN [REF.f32]\n"
65
  << " " << program << " generate MODEL.l3r USER_TEXT [MAX_NEW_TOKENS]\n"
66
  << " " << program << " benchmark MODEL.l3r SEQLEN NEW_TOKENS [ITERATIONS]\n"
67
+ #if LING3_EXPERIMENTAL_MTP
68
+ << " " << program << " mtp-benchmark MODEL.l3r SEQLEN NEW_TOKENS ITERATIONS CONTEXT [CASES.txt]\n"
69
+ #endif
70
  << " " << program << " sequence-check MODEL.l3r SEQLEN NEW_TOKENS OUT_PREFIX [REFERENCE_PREFIX [CORPUS.txt]]\n"
71
  << " " << program << " serve MODEL.l3r [PORT]\n";
72
  }
 
1212
  ProcessMemory memory;
1213
  };
1214
 
1215
+ #if LING3_EXPERIMENTAL_MTP
1216
+ // Same resident decoder for baseline and MTP. Populate every MTP prefix slot
1217
+ // sequentially, and teacher-force baseline tokens so the work and histories
1218
+ // stay comparable. This measures forward overhead, not speculative speedup.
1219
+ int RunMtpBenchmark(const std::string & path, std::size_t length,
1220
+ std::size_t steps, std::size_t repeats, std::size_t capacity,
1221
+ const std::string & cases_path) {
1222
+ ConfigureInferenceProcess();
1223
+ const ling3::ModelPackage package(path);
1224
+ if (!length || steps < 2 || !repeats || length + steps > capacity || capacity > 262144)
1225
+ throw std::invalid_argument("invalid MTP benchmark lengths");
1226
+ const auto tokenizer = PackageTokenizer(package);
1227
+ std::vector<std::vector<std::uint32_t>> prompts{BuildBenchmarkPrompt(tokenizer, length)};
1228
+ if (!cases_path.empty()) {
1229
+ std::ifstream file(cases_path);
1230
+ if (!file) throw std::runtime_error("cannot open MTP cases file");
1231
+ std::string question;
1232
+ while (std::getline(file, question)) {
1233
+ if (question.empty()) continue;
1234
+ auto ids = tokenizer.Encode("<role>SYSTEM</role>detailed thinking off<|role_end|>"
1235
+ "<role>HUMAN</role>" + question + "<|role_end|><role>ASSISTANT</role>\n<think></think>");
1236
+ if (ids.empty() || ids.size() + steps > capacity)
1237
+ throw std::invalid_argument("case does not fit the requested context");
1238
+ prompts.push_back(std::move(ids));
1239
+ }
1240
+ }
1241
+ const auto started = std::chrono::steady_clock::now();
1242
+ ling3::Decoder decoder(package, capacity);
1243
+ decoder.EnableMtp();
1244
+ const std::vector<bool> modes = decoder.HasMtp() ? std::vector<bool>{false, true} : std::vector<bool>{false};
1245
+ std::vector<float> logits(package.header().vocab_size), draft(logits.size());
1246
+ const auto milliseconds = [](auto a, auto b) {
1247
+ return std::chrono::duration<double, std::milli>(b - a).count();
1248
+ };
1249
+ std::cout << std::fixed << std::setprecision(4)
1250
+ << "benchmark=resident_mtp_forward_not_speculative\n"
1251
+ << "context_capacity=" << capacity << " cases=" << prompts.size()
1252
+ << " mtp_enabled=" << decoder.HasMtp() << " new_tokens=" << steps << " iterations=" << repeats << '\n'
1253
+ << "initialization_ms=" << milliseconds(started, std::chrono::steady_clock::now()) << std::endl;
1254
+ const auto initial_memory = ReadProcessMemory();
1255
+ std::cout << "initial_rss_mib=" << initial_memory.rss_kb / 1024.0
1256
+ << " initial_hwm_mib=" << initial_memory.hwm_kb / 1024.0 << std::endl;
1257
+ for (std::size_t case_index = 0; case_index < prompts.size(); ++case_index) {
1258
+ const auto & prompt = prompts[case_index];
1259
+ length = prompt.size();
1260
+ for (std::size_t round = 0; round <= repeats; ++round) {
1261
+ std::vector<std::uint32_t> teacher;
1262
+ teacher.reserve(steps);
1263
+ for (bool with_mtp : modes) {
1264
+ decoder.Reset();
1265
+ const auto attention_before = decoder.AttentionStats();
1266
+ const auto begin = std::chrono::steady_clock::now();
1267
+ double trunk = 0, mtp = 0, mtp_layers = 0, mtp_head = 0;
1268
+ for (std::size_t i = 0; i < length; ++i) {
1269
+ decoder.Eval(prompt[i], logits);
1270
+ if (with_mtp && i + 1 < length) decoder.EvalMtp(prompt[i + 1], draft);
1271
+ }
1272
+ std::uint32_t token = GreedyToken(logits);
1273
+ std::size_t target_agreement = 0, draft_hits = 0;
1274
+ if (!with_mtp) teacher.push_back(token);
1275
+ else {
1276
+ target_agreement += token == teacher[0];
1277
+ token = teacher[0];
1278
+ }
1279
+ const auto first = std::chrono::steady_clock::now();
1280
+ std::vector<double> trunk_samples, mtp_samples;
1281
+ for (std::size_t i = 1; i < steps; ++i) {
1282
+ std::uint32_t proposed = 0;
1283
+ if (with_mtp) {
1284
+ const auto t = decoder.EvalMtp(token, draft);
1285
+ mtp += t.total_ms; mtp_layers += t.layers_ms; mtp_head += t.output_head_ms;
1286
+ mtp_samples.push_back(t.total_ms);
1287
+ proposed = GreedyToken(draft);
1288
+ }
1289
+ const auto t = decoder.Eval(token, logits);
1290
+ trunk += t.total_ms; trunk_samples.push_back(t.total_ms);
1291
+ token = GreedyToken(logits);
1292
+ if (!with_mtp) teacher.push_back(token);
1293
+ else {
1294
+ target_agreement += token == teacher[i];
1295
+ draft_hits += proposed == teacher[i];
1296
+ token = teacher[i];
1297
+ }
1298
+ }
1299
+ const auto end = std::chrono::steady_clock::now();
1300
+ const auto percentile = [](std::vector<double> values, double q) {
1301
+ std::sort(values.begin(), values.end());
1302
+ return values[static_cast<std::size_t>(q * (values.size() - 1))];
1303
+ };
1304
+ const auto memory = ReadProcessMemory();
1305
+ const auto attention = decoder.AttentionStats();
1306
+ std::cout << "phase=" << (round == 0 ? "warmup" : "measured")
1307
+ << " case=" << case_index << " seqlen=" << length
1308
+ << " round=" << round << " mode=" << (with_mtp ? "target_plus_mtp" : "target_only")
1309
+ << " sequential_prefill_ms=" << milliseconds(begin, first)
1310
+ << " decode_wall_ms=" << milliseconds(first, end)
1311
+ << " tokens_per_second=" << 1000.0 * (steps - 1) / milliseconds(first, end)
1312
+ << " target_mean_ms=" << trunk / (steps - 1)
1313
+ << " target_p50_ms=" << percentile(trunk_samples, 0.5)
1314
+ << " target_p95_ms=" << percentile(trunk_samples, 0.95);
1315
+ if (with_mtp)
1316
+ std::cout << " mtp_mean_ms=" << mtp / (steps - 1)
1317
+ << " mtp_p50_ms=" << percentile(mtp_samples, 0.5)
1318
+ << " mtp_p95_ms=" << percentile(mtp_samples, 0.95)
1319
+ << " mtp_layers_mean_ms=" << mtp_layers / (steps - 1)
1320
+ << " mtp_head_mean_ms=" << mtp_head / (steps - 1)
1321
+ << " target_agreement=" << target_agreement << '/' << steps
1322
+ << " draft_top1_hits=" << draft_hits << '/' << (steps - 1);
1323
+ std::cout << " mla_npu_calls=" << attention.npu_calls - attention_before.npu_calls
1324
+ << " mla_cpu_calls=" << attention.cpu_calls - attention_before.cpu_calls
1325
+ << " mla_fallbacks=" << attention.fallbacks - attention_before.fallbacks
1326
+ << " rss_mib=" << memory.rss_kb / 1024.0
1327
+ << " hwm_mib=" << memory.hwm_kb / 1024.0
1328
+ << " teacher_hash=" << std::hex << TokenHash(teacher) << std::dec << std::endl;
1329
+ if (with_mtp && target_agreement != steps)
1330
+ throw std::runtime_error("MTP probe changed target predictions for the same teacher-forced tokens");
1331
+ }
1332
+ }
1333
+ }
1334
+ return 0;
1335
+ }
1336
+
1337
+ #endif
1338
+
1339
  int RunBenchmark(
1340
  const std::string & package_path,
1341
  std::size_t sequence_length,
 
1576
  const std::size_t iterations = argc == 6 ? ParseSize(argv[5], "iterations") : 3;
1577
  return RunBenchmark(argv[2], sequence_length, new_tokens, iterations);
1578
  }
1579
+ #if LING3_EXPERIMENTAL_MTP
1580
+ if (command == "mtp-benchmark" && (argc == 7 || argc == 8)) {
1581
+ return RunMtpBenchmark(argv[2], ParseSize(argv[3], "sequence length"),
1582
+ ParseSize(argv[4], "new tokens"), ParseSize(argv[5], "iterations"),
1583
+ ParseSize(argv[6], "context capacity"), argc == 8 ? argv[7] : "");
1584
+ }
1585
+ #endif
1586
  if (command == "sequence-check" && argc >= 6 && argc <= 8) {
1587
  return RunSequenceCheck(argv[2], ParseSize(argv[3], "sequence length"),
1588
  ParseSize(argv[4], "new tokens"), argv[5],
tests/data/mtp_probe_questions.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ 请用中文讲一个完整的小故事,主角是一个第一次出海的年轻人,故事要有起因、转折和结尾。
2
+ 我明天要召开一个项目进度会,请帮我设计会议议程,并说明如何跟踪会议后的任务。
3
+ 请解释为什么长上下文会影响大语言模型的解码速度,用通俗语言举一个例子。
tests/precision_policy_test.cpp CHANGED
@@ -16,6 +16,21 @@ int main()try{
16
  if(!SelectW8(small,"lm_head")||!SelectW8(small,"model.layers.0.mlp.down_proj")||
17
  !SelectW8(small,"model.layers.1.mlp.shared_experts.gate_up")||SelectW8({},"lm_head"))
18
  throw std::runtime_error("nonattention precision selection mismatch");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  for(auto bad:{"kda","head,","head,,shared"}){
20
  bool rejected=false;try{ParseW8Families(bad);}catch(const std::invalid_argument&){rejected=true;}
21
  if(!rejected)throw std::runtime_error("invalid selector accepted");
 
16
  if(!SelectW8(small,"lm_head")||!SelectW8(small,"model.layers.0.mlp.down_proj")||
17
  !SelectW8(small,"model.layers.1.mlp.shared_experts.gate_up")||SelectW8({},"lm_head"))
18
  throw std::runtime_error("nonattention precision selection mismatch");
19
+ #if LING3_EXPERIMENTAL_MTP
20
+ if(W8MatrixFamily("model.layers.24.attention.qkv_gate_a")!="mla_in" ||
21
+ W8MatrixFamily("model.layers.24.attention.o_proj")!="mla_out" ||
22
+ SelectW8(all,"model.layers.24.mlp.experts.0.gate_up"))
23
+ throw std::runtime_error("MTP precision family mismatch");
24
+ #else
25
+ bool disabled_mtp_rejected=false;
26
+ try{W8MatrixFamily("model.layers.24.attention.o_proj");}
27
+ catch(const std::invalid_argument&){disabled_mtp_rejected=true;}
28
+ if(!disabled_mtp_rejected)throw std::runtime_error("disabled MTP layer accepted");
29
+ #endif
30
+ bool invalid_mtp_layer=false;
31
+ try{W8MatrixFamily("model.layers.25.attention.o_proj");}
32
+ catch(const std::invalid_argument&){invalid_mtp_layer=true;}
33
+ if(!invalid_mtp_layer)throw std::runtime_error("invalid MTP layer accepted");
34
  for(auto bad:{"kda","head,","head,,shared"}){
35
  bool rejected=false;try{ParseW8Families(bad);}catch(const std::invalid_argument&){rejected=true;}
36
  if(!rejected)throw std::runtime_error("invalid selector accepted");
tools/append_mtp_package.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Append quantized layer-24 tensors without requantizing any trunk weight."""
3
+ import argparse
4
+ import hashlib
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ import zlib
9
+
10
+ from pack_model import HEADER, ENTRY, align, pad_to
11
+
12
+
13
+ def index(path):
14
+ with path.open("rb") as stream:
15
+ header = list(HEADER.unpack(stream.read(HEADER.size)))
16
+ if (header[0] != b"L3RKNN1\0" or header[1:4] != [1, HEADER.size, 0x01020304]
17
+ or header[5] != ENTRY.size or header[12] != path.stat().st_size):
18
+ raise ValueError("invalid package header: " + str(path))
19
+ crc = header[14]
20
+ header[14] = 0
21
+ if zlib.crc32(HEADER.pack(*header)) & 0xFFFFFFFF != crc:
22
+ raise ValueError("header CRC mismatch")
23
+ header[14] = crc
24
+ stream.seek(header[8])
25
+ names = stream.read(header[9])
26
+ stream.seek(header[7])
27
+ result = []
28
+ for _ in range(header[4]):
29
+ entry = list(ENTRY.unpack(stream.read(ENTRY.size)))
30
+ name = names[entry[0]:entry[0] + entry[1]].decode("utf-8")
31
+ if not name or entry[3] > 4:
32
+ raise ValueError("invalid tensor entry")
33
+ for offset, size in ((entry[12], entry[13]), (entry[14], entry[15])):
34
+ if size and (offset < header[10] or offset + size > header[12]):
35
+ raise ValueError("out-of-range tensor: " + name)
36
+ result.append((path, name, entry))
37
+ if len({name for _, name, _ in result}) != len(result):
38
+ raise ValueError("duplicate tensor names")
39
+ return header, result
40
+
41
+
42
+ def copy_range(source, output, offset, size):
43
+ source.seek(offset)
44
+ digest = hashlib.sha256()
45
+ remaining = size
46
+ while remaining:
47
+ chunk = source.read(min(remaining, 8 * 1024 * 1024))
48
+ if not chunk:
49
+ raise IOError("short tensor payload")
50
+ output.write(chunk)
51
+ digest.update(chunk)
52
+ remaining -= len(chunk)
53
+ return digest.digest()
54
+
55
+
56
+ def file_hash(path):
57
+ with path.open("rb") as stream:
58
+ return hashlib.file_digest(stream, "sha256").hexdigest()
59
+
60
+
61
+ def main():
62
+ parser = argparse.ArgumentParser(description=__doc__)
63
+ parser.add_argument("--base", type=Path, required=True)
64
+ parser.add_argument("--mtp-package", type=Path, required=True)
65
+ parser.add_argument("--output", type=Path, required=True)
66
+ args = parser.parse_args()
67
+ if args.output.exists():
68
+ parser.error("refusing to overwrite an existing package")
69
+ header, trunk = index(args.base)
70
+ other_header, other = index(args.mtp_package)
71
+ if header[15:35] != other_header[15:35]:
72
+ parser.error("MTP architecture differs from trunk")
73
+ mtp = [row for row in other if row[1].startswith("model.layers.24.")]
74
+ if len(mtp) != 798 or any(row[1].startswith("model.layers.24.") for row in trunk):
75
+ parser.error("expected 798 MTP tensors and a trunk without MTP")
76
+ tensors = trunk + mtp
77
+ names = bytearray()
78
+ for _, name, entry in tensors:
79
+ entry[0] = len(names)
80
+ encoded = name.encode("utf-8")
81
+ entry[1] = len(encoded)
82
+ names.extend(encoded)
83
+ header[4] = len(tensors)
84
+ header[7] = HEADER.size
85
+ header[8] = HEADER.size + len(tensors) * ENTRY.size
86
+ header[9] = len(names)
87
+ header[10] = align(header[8] + len(names))
88
+ args.output.parent.mkdir(parents=True, exist_ok=True)
89
+ sources = {}
90
+ entries = []
91
+ try:
92
+ with args.output.open("xb") as output:
93
+ output.write(bytes(HEADER.size + len(tensors) * ENTRY.size))
94
+ output.write(names)
95
+ pad_to(output, header[10])
96
+ for path, name, entry in tensors:
97
+ if path not in sources:
98
+ sources[path] = path.open("rb")
99
+ source = sources[path]
100
+ data_offset = align(output.tell())
101
+ pad_to(output, data_offset)
102
+ if copy_range(source, output, entry[12], entry[13]) != entry[22]:
103
+ raise ValueError("tensor SHA256 mismatch: " + name)
104
+ aux_offset = align(output.tell()) if entry[15] else 0
105
+ if entry[15]:
106
+ pad_to(output, aux_offset)
107
+ copy_range(source, output, entry[14], entry[15])
108
+ entry[12], entry[14] = data_offset, aux_offset
109
+ entries.append(ENTRY.pack(*entry))
110
+ header[12] = align(output.tell())
111
+ pad_to(output, header[12])
112
+ header[11] = header[12] - header[10]
113
+ header[14] = 0
114
+ header[14] = zlib.crc32(HEADER.pack(*header)) & 0xFFFFFFFF
115
+ output.seek(0)
116
+ output.write(HEADER.pack(*header))
117
+ output.writelines(entries)
118
+ output.flush()
119
+ os.fsync(output.fileno())
120
+ finally:
121
+ for source in sources.values():
122
+ source.close()
123
+ record = {"base": args.base.name, "base_sha256": file_hash(args.base),
124
+ "mtp_package": args.mtp_package.name, "mtp_source_sha256": file_hash(args.mtp_package),
125
+ "output": args.output.name, "output_sha256": file_hash(args.output),
126
+ "bytes": header[12], "flags": header[6], "trunk_tensors_unchanged": len(trunk),
127
+ "appended_mtp_tensors": len(mtp), "recomputed_quantization": False}
128
+ args.output.with_suffix(".manifest.json").write_text(json.dumps(record, indent=2) + "\n")
129
+ print(json.dumps(record, indent=2), flush=True)
130
+
131
+
132
+ if __name__ == "__main__":
133
+ main()
tools/mtp_resident_benchmark.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Resident MTP forward probe; temporarily lock clocks and restore on exit."""
3
+ import argparse
4
+ import datetime
5
+ import glob
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ import signal
10
+ import subprocess
11
+ import time
12
+
13
+
14
+ def read(path):
15
+ try:
16
+ return Path(path).read_text().strip()
17
+ except OSError:
18
+ return None
19
+
20
+
21
+ def main():
22
+ parser = argparse.ArgumentParser(description=__doc__)
23
+ parser.add_argument("binary")
24
+ parser.add_argument("model")
25
+ parser.add_argument("--output", required=True)
26
+ parser.add_argument("--runtime", required=True)
27
+ parser.add_argument("--seqlen", type=int, default=128)
28
+ parser.add_argument("--tokens", type=int, default=64)
29
+ parser.add_argument("--rounds", type=int, default=3)
30
+ parser.add_argument("--context", type=int, default=32768)
31
+ parser.add_argument("--cases")
32
+ args = parser.parse_args()
33
+ if os.geteuid() != 0:
34
+ parser.error("run with sudo for temporary clock locking and NPU debugfs sampling")
35
+ help_env = os.environ.copy()
36
+ help_env["LD_LIBRARY_PATH"] = args.runtime + ":" + help_env.get("LD_LIBRARY_PATH", "")
37
+ probe = subprocess.run([args.binary], stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
38
+ text=True, env=help_env, timeout=15)
39
+ if "Usage:" not in probe.stdout:
40
+ parser.error("cannot query engine CLI: " + probe.stdout.strip())
41
+ if "mtp-benchmark" not in probe.stdout:
42
+ parser.error("MTP is not compiled; rebuild with -DLING3_EXPERIMENTAL_MTP=ON")
43
+ output = Path(args.output)
44
+ output.mkdir(parents=True, exist_ok=False)
45
+ original = {}
46
+ applied = {}
47
+ child = None
48
+
49
+ def write_clock(path, value):
50
+ original[path] = read(path)
51
+ if original[path] is None:
52
+ raise RuntimeError("cannot snapshot " + path)
53
+ Path(path).write_text(str(value))
54
+ applied[path] = read(path)
55
+
56
+ def stop(signum, frame):
57
+ raise KeyboardInterrupt
58
+
59
+ signal.signal(signal.SIGTERM, stop)
60
+ signal.signal(signal.SIGINT, stop)
61
+ try:
62
+ for policy in sorted(glob.glob("/sys/devices/system/cpu/cpufreq/policy*")):
63
+ write_clock(policy + "/scaling_governor", "performance")
64
+ write_clock(policy + "/scaling_min_freq", read(policy + "/scaling_max_freq"))
65
+ for device in ("fdab0000.npu", "dmc"):
66
+ base = "/sys/class/devfreq/" + device
67
+ frequencies = read(base + "/available_frequencies")
68
+ maximum = max(map(int, frequencies.split())) if frequencies else int(read(base + "/max_freq"))
69
+ write_clock(base + "/governor", "performance")
70
+ write_clock(base + "/min_freq", maximum)
71
+ env = os.environ.copy()
72
+ for key in ("LING3_GDN_PREFILL_DIR", "LING3_PREFILL_W4A4", "LING3_EXPERT_ALL_CORES",
73
+ "LING3_MLA_DECODE_NPU", "LING3_DISABLE_BATCH_INPUT_REUSE",
74
+ "LING3_DISABLE_PARALLEL_GATHER"):
75
+ env.pop(key, None)
76
+ for key in ("LING3_PREWARM_EXPERTS", "LING3_EXPERT_BALANCED", "LING3_EXPERT_ZERO_COPY",
77
+ "LING3_GDN_CPU_PREFILL", "LING3_GDN_CPU_FP32_STATE", "LING3_GDN_CPU_DECODE",
78
+ "LING3_GDN_FULL_FP32", "LING3_MLA_SIMD", "LING3_VECTOR_MATH"):
79
+ env[key] = "1"
80
+ env["LING3_MLA_BACKEND"] = "cpu"
81
+ env["LD_LIBRARY_PATH"] = args.runtime + ":" + env.get("LD_LIBRARY_PATH", "")
82
+ command = [args.binary, "mtp-benchmark", args.model, str(args.seqlen), str(args.tokens), str(args.rounds), str(args.context)]
83
+ if args.cases:
84
+ command.append(args.cases)
85
+ metadata = {"started_utc": datetime.datetime.now(datetime.timezone.utc).isoformat(),
86
+ "command": command, "original_clocks": original, "meminfo_before": read("/proc/meminfo"), "applied_clocks": applied,
87
+ "profile": {k: v for k, v in env.items() if k.startswith("LING3_")}}
88
+ (output / "metadata.json").write_text(json.dumps(metadata, indent=2))
89
+ with (output / "benchmark.log").open("w") as log, (output / "telemetry.jsonl").open("w") as telemetry:
90
+ child = subprocess.Popen(command, stdout=log, stderr=subprocess.STDOUT, env=env)
91
+ print("pid=" + str(child.pid) + " output=" + str(output), flush=True)
92
+ start = time.monotonic()
93
+ while child.poll() is None:
94
+ row = {"elapsed_s": time.monotonic() - start,
95
+ "process_stat": read(f"/proc/{child.pid}/stat"),
96
+ "process_status": read(f"/proc/{child.pid}/status"),
97
+ "npu_load": read("/sys/kernel/debug/rknpu/load"),
98
+ "meminfo": read("/proc/meminfo")}
99
+ row["frequencies"] = {p: read(p) for p in glob.glob("/sys/devices/system/cpu/cpufreq/policy*/scaling_cur_freq")
100
+ + ["/sys/class/devfreq/fdab0000.npu/cur_freq", "/sys/class/devfreq/dmc/cur_freq"]}
101
+ row["temperatures"] = {p: read(p) for p in glob.glob("/sys/class/thermal/thermal_zone*/temp")}
102
+ telemetry.write(json.dumps(row) + "\n")
103
+ telemetry.flush()
104
+ time.sleep(0.5)
105
+ metadata["exit_code"] = child.returncode
106
+ metadata["elapsed_s"] = time.monotonic() - start
107
+ print("exit_code=" + str(child.returncode), flush=True)
108
+ finally:
109
+ if child is not None and child.poll() is None:
110
+ child.terminate()
111
+ try:
112
+ child.wait(timeout=15)
113
+ except subprocess.TimeoutExpired:
114
+ child.kill()
115
+ child.wait()
116
+ restored = {}
117
+ for path, value in reversed(list(original.items())):
118
+ try:
119
+ Path(path).write_text(value)
120
+ restored[path] = read(path)
121
+ except OSError as error:
122
+ restored[path] = str(error)
123
+ final = locals().get("metadata", {"original_clocks": original, "applied_clocks": applied})
124
+ final["restored_clocks"] = restored
125
+ (output / "metadata.json").write_text(json.dumps(final, indent=2))
126
+ if "SUDO_UID" in os.environ:
127
+ for path in [output] + list(output.iterdir()):
128
+ os.chown(path, int(os.environ["SUDO_UID"]), int(os.environ["SUDO_GID"]))
129
+ print("clocks_restored", flush=True)
130
+ return child.returncode if child else 1
131
+
132
+
133
+ if __name__ == "__main__":
134
+ raise SystemExit(main())