SunnyNet 工程化实录:一个 O(N) 架构如何长出自己的 C 推理运行时
项目地址:https://github.com/SunnyClaw-cx/SunnyNet (Apache-2.0)
本文所有数字均来自本机实测,原始数据在仓库experiments/results/*.json,报告在docs/EXPERIMENT_REPORT.md。
测试机:i7-8750H(Coffee Lake,6C12T,AVX2,无 AVX-512 / 无 VNNI / 无 AMX),torch 2.14.1+cpu。
一、新增的运行时能力
SunnyNet 是一个不基于 Transformer / RNN / Mamba / CNN 的原创序列架构,设计目标是 CPU 训练,序列复杂度严格 O(N)。此前它是一个训练框架,现在带上了独立的 C 推理运行时:
| 新增 | 位置 | 关键数字 |
|---|---|---|
| 通用一阶递推 C 内核 | runtime/recur.c |
对 torch 向量化基线中位 9.90× |
| 整模型单步解码进 C | runtime/sunnynet.c |
B=1 单线程 1.53–2.05× |
| RWKV-7 整模型 C 内核 | runtime/rwkv7.c |
对 numpy 参考 5.39×,8 线程 7.49× |
| INT4 分组权重量化 | runtime/quant.c |
端到端 2.83×,体积 729→269 MB |
| 外部权重接入 | runtime/mamba2.py deltanet.py gla.py |
逐 token 单步,不经 PyTorch 前向 |
| 稳定 C ABI | runtime/sn_abi.h |
版本化 + 能力位 + 错误码,SN_ABI_VERSION 1.0.0 |
| OpenAI 兼容服务 | serve.py |
标准库零依赖,SSE 流式 |
| Ollama / LM Studio 接入 | integrate.py |
RWKV-7 GGUF 真实导入成功 |
| SIMD 后端 | kernels/scan_avx512.c scan_neon.c |
交叉编译 + 反汇编产物验证 |
二、通用一阶递推内核:六种架构收敛到两种形式
2.1 观察
把主流线性架构的状态更新式摆在一起看,会发现它们都能写成两种形式之一:
s_t = a_t * s_{t-1} + b_t (FMA)
s_t = a_t * s_{t-1} + (1 - a_t) * u_t (门控)
RWKV 的 wkv、Mamba/S6 与 H3 的对角 SSM、S4、线性注意力的 KV 累加、以及 SunnyNet 自己的凸组合滚动状态,全部归约为其中之一。于是可以只写一个 C 内核,用参数映射适配不同架构。
2.2 映射与数值验证
experiments/_unified_ir_map.py 把各架构的递推映射到内核参数,与各自的 PyTorch 递推逐元素比对(B=2 / T=128 / N=128 / fp32):
| 架构递推形式 | 映射 | max\ | diff\ | |
|---|---|---|---|---|
| RWKV-4 wkv | a=p, b=k*v |
9.5e-07 | ||
| Mamba / S6 SSM | a=decay, u=bu |
2.4e-07 | ||
| H3 对角 SSM | a=decay, u=v |
1.2e-07 | ||
| 线性注意力累积 KV | a=1, b=k v^T |
0.0 | ||
| SunnyNet 滚动状态 | a=p, u=u |
1.9e-06 | ||
| S4 对角 SSM | a=A_bar, b=B_bar x |
7.2e-07 |
Hyena 是长卷积(FIR)递推,不属一阶状态递推,需要独立的卷积后端,没有硬塞进来。
2.3 速度
experiments/_runtime_bench.py,每格独立子进程、单线程、B=1、10 次取中位。基线是 torch 的向量化分块扫描(ops.gated_scan,cumsum 闭式解),不是 Python 循环:
| T | N | 模式 | 向量化 ms | 内核 ms | 倍数 |
|---|---|---|---|---|---|
| 256 | 128 | FMA | 0.391 | 0.043 | 9.08× |
| 1024 | 128 | FMA | 1.256 | 0.132 | 9.54× |
| 4096 | 128 | GATED | 8.500 | 0.806 | 10.55× |
| 4096 | 640 | FMA | 26.944 | 7.574 | 3.56× |
中位 9.90×,范围 3.56–19.29×。N=640 时掉到 3.6–4.0×,是单线程内存带宽限制。内核三分支与 torch 的最大数值差 7.63e-06。
2.4 一个必须写出来的边界
上表是批量跨段扫描口径(B=1、整段 T)。把它接到 B=1 单步解码上,收益直接消失:四格比值 1.023 / 1.072 / 0.897 / 1.073,方向都不一致,8 线程 T=256 反而慢 10.3%。
原因是模型侧 T=1、B=1 时张量太小,跨语言调用(ctypes FFI)与状态搬运的成本和单个算子同量级。所以这个内核不能拿来声称"端到端解码加速"。这也是后面把整模型搬进 C 的动机——只有一次调用覆盖整步,FFI 开销才摊得开。
三、整模型解码搬进 C
单步算子内核解决不了 FFI 摊销问题,于是把整个前向也写进 C:嵌入查表、逐层 norm、深度卷积历史、门控竞争、状态递推、读出、终态归一、LM head,全在一条没有中间张量的路径上完成。
权重由 experiments/_export_runtime.py 从 checkpoint 导出为打包 fp32 blob,两个细节:矩阵乘权重预转置成 (in, out) 以配合 GEMV,circ ∘ W_s 折成单个矩阵。
3.1 数值一致性
experiments/_runtime_model_equiv.py,1.4M checkpoint,48 token 逐步:
| 口径 | 结果 | ||
|---|---|---|---|
| 单步 logits max\ | diff\ | 1.24e-05 | |
| argmax 一致 | 48 / 48 | ||
| prefill 末 token 与 torch 整段前向 | 3.81e-06 |
3.2 端到端延迟
experiments/_runtime_decode.py,B=1 单序列、单线程、每格独立子进程、先走满 prefill 不计时再测单步、3 轮交错:
| prefill T | torch ms/step | C ms/step | 比值 | 逐轮占优 |
|---|---|---|---|---|
| 256 | 3.796 | 2.487 | 1.53× | 3/3 |
| 1024 | 3.802 | 1.851 | 2.05× | 3/3 |
| 2048 | 4.309 | 2.189 | 1.97× | 3/3 |
中位比值和逐轮符号检验一致(四格全 3/3),可以判定为真实提升而不是轮间漂移。
提速来源不是算子融合,而是消灭了每步约 1100 次 aten 调用 / 21 次 GEMV / 20 次 cat。torch 单步剖析的占比是 aten::mm 16.4%、aten::mul 10.2%、aten::select 8.5%、aten::cat 8.0%——单看每个算子都不贵,加起来就是这 1.5–2 倍。
3.3 口径边界
上表是单线程 B=1。CLI 默认 8 线程时,C 后端 4.40 ms/tok vs torch 4.56 ms/tok,差距被线程调度摊平。B=1 小张量并行是负收益(1 线程比 8 线程快 1.42×),所以多线程下不要按上表线性外推。
四、外部权重接入:Mamba-2 / DeltaNet / GLA
同一套 Runtime 接口直接加载别人的官方权重,逐 token 单步递推,全程不经 PyTorch 前向。三个模型都做了分段续算验证——把序列切成两段分别算,再对比全程递推,结果均为逐位 0.0,说明状态传递没有隐藏的额外初始化。
4.1 Mamba-2(state-spaces/mamba2-130m)
d_model 768 / 24 层 / 24 head × 64。表面上每个 head 的状态是 (64, 128) 矩阵,但衰减 dA = exp(dt·A) 对所有 (p, n) 全同,外积也可分解,实际退化成元素级一阶递推:
dA = exp(dt_h · A_h) # 标量
S_t = S_{t-1} ⊙ dA + (dt_h · B_g) ⊗ x_h # 不需要矩阵-向量乘积
- 与
transformers的Mamba2ForCausalLM整段前向 max|diff| 5.9e-04、argmax 24/24。 - 这个差异是单精度累积不是公式错误:官方 fp32 自身相对 fp64 偏离 3.8e-04,本实现偏离 2.5e-04,反而更接近 fp64 真值。
- 贪心续写英文与代码连贯。
4.2 DeltaNet(fla-org DeltaNet 340M)
hidden 1024 / 24 层 / 8 head × 128,短卷积 4。这是真 delta rule,状态 (k_dim=128, v_dim=128),更新含 rank-1 校正:
δ_t = (v_t − S_{t-1}ᵀ k_t) · β_t
S_t = S_{t-1} + k_t ⊗ δ_t
o_t = S_tᵀ q_t
踩了个坑值得单独说:head_k_dim == head_v_dim 时,把 S 乘错一侧不会报维度错误,只会静默算成另一个式子。所以秩 1 形式和显式 (I − β k kᵀ) S + β k vᵀ 写法必须分别实现、分别验证,两边 max|diff| 4.6e-05。
贪心输出:"<s>The capital of France is the city of Paris. It is the largest city in Europe…"。
4.3 GLA(fla-hub/gla-340M-15B)
hidden 1024 / 24 层 / 4 head × 128,gate_rank 16。GLA 的门控 gk = logsigmoid(·) / 16 只作用在 key 维,衰减可分解为按行缩放,状态递推同样退化成元素级:
S_t = diag(exp(gk_t)) · S_{t-1} + k_t ⊗ v_t
o_t = scale · S_tᵀ q_t, scale = head_k_dim^{-1/2}
读出后接 RMSNorm(o) · silu(g)(先归一后乘门)。
与独立参考实现 max|diff| 2.05e-05、argmax 12/12。测试里直接断言了"衰减是元素级可分解的"(test_gla_decay_is_elementwise_decomposable),防止后面有人误把它当成矩阵值递推处理。
4.4 速度与层级缺口
单线程 numpy 参考路径,每格独立子进程 × 5 轮:
| 架构 | median ms/token | 递推层级 |
|---|---|---|
| mamba2 | 45.68 | 元素级,可复用 recur.c |
| gla | 85.1 | 元素级,可复用 recur.c |
| deltanet | 91.46 | 矩阵值 rank-1,现有内核表达不了 |
这是参考路径的绝对耗时,不与任何基线比倍数。两者单步的安全上界都是 O(heads × head_dim × state_size),与上下文长度无关。
矩阵值递推是目前唯一还没进 C 的层级:RWKV-7 和 DeltaNet 的状态更新都含 S ← S + k ⊗ (·) 这类外积累加,SN_FMA / SN_GATED 的逐通道形式表达不了,需要新增一个 rank-1 递推内核。Mamba-2 与 GLA 因为衰减外积可分解,不需要。
五、RWKV-7:从 GGUF 读取到 C 内核到 INT4
选 RWKV-7 191M(BlinkDL/rwkv-7-world)作为第一条打通外部权重的完整链路,因为它同时包含"读别人的权重格式"和"矩阵值递推"两件难事。
5.1 GGUF 只读路径(numpy)
sunnynet/runtime/gguf.py 读 GGUF(f16/bf16/f32 + 量化反量化),rwkv7.py 逐 token 自回归生成。张量命名与递推形式取自 llama.cpp 的 src/models/rwkv7*.cpp,参考数学取自 BlinkDL/RWKV-LM 的 rwkv_v7_numpy.py。
experiments/_rwkv7_gguf_equiv.py:
| 口径 | 结果 | ||
|---|---|---|---|
GGUF 反量化 vs 官方 .pth |
逐位一致(max\ | diff\ | = 0) |
| 单步 logits max\ | diff\ | vs 独立参考 | 4.79e-06(bf16 精度内) |
| argmax 一致 | 12 / 12 |
生成样例(温度 0.7 / top-k 20 / seed 42):
The Eiffel Tower is located in the city of
-> Paris, France. The Eiffel Tower was designed by Gustave Eiffel in 1887 as a monument to
def fibonacci(n):
-> \n if n < 2:\n return n\n return fibonacci(n - 1) + fibonacci(n - 2)
这条路径是 numpy,解码约 388 ms/step,只证明"能读外部权重并算对",不作为速度成果。
这里有一条与常见叙事不同的技术事实必须记录:RWKV-7 的状态是逐 head 的矩阵 S ∈ R^{64×64},更新为
S_t = S_{t-1} ⊙ w_t − (S_{t-1} k_t) k_tᵀ + v_t k_tᵀ
含 rank-1 去除项,不是元素级 s = a ⊙ s + b。统一 Runtime 要覆盖它,IR 必须在元素级递推之外补一个矩阵值递推层级。
5.2 整模型 C 内核(runtime/rwkv7.c)
把整模型单步搬进 C:嵌入查表、逐层 norm / time-mix(含 LoRA 与 tanh/sigmoid)、逐 head 矩阵状态递推(rank-1 校正)、group-norm、channel-mix、终态归一、LM head 全在 C 内,一步一次调用。权重从 GGUF 导出为打包 fp32 blob,矩阵存 (in, out) 配合 GEMV。
experiments/_rwkv7_c_equiv.py:
| 口径 | 结果 | ||
|---|---|---|---|
| 单步 logits max\ | diff\ | 7.25e-05(bf16 精度内) | |
| argmax 一致 | 12 / 12 | ||
| 120 步长程 argmax 一致 | 120 / 120 |
experiments/_rwkv7_c_bench.py,单序列、已建状态后单步、每格独立子进程、5 轮交错、60 步取中位:
| 实现 | ms/step | 相对 numpy |
|---|---|---|
| numpy 参考 | 366.36 | 1.00× |
| C 内核 1 线程 | 67.92 | 5.39×(5/5 逐轮占优) |
| C 内核 4 线程 | 52.26 | 7.01× |
| C 内核 8 线程 | 48.88 | 7.49× |
C 内核 8 线程对 1 线程再快 1.39×(5/5 逐轮占优)。单线程方差偏大(50–84 ms),绝对值随机器负载浮动,比值与逐轮符号检验始终一致。
5.3 INT4 分组权重量化(runtime/quant.c)
方案是分组对称 INT4:每 64 个连续权重共享一个 fp32 scale,量化值域 [-8, 7],两值一字节。量化 GEMV 把激活也按同样分组量化到 INT8,用 maddubs 做点积:
Σ(qᵢ − 8)xᵢ = Σqᵢxᵢ − 8Σxᵢ
权重保持无符号 [0,15] 直接进第一操作数。整模型 C 内核按矩阵标志位混用 fp32 / INT4,向量与嵌入表保持 fp32。
| 口径 | fp32 | INT4 |
|---|---|---|
| blob 体积 | 729.1 MB | 269.0 MB |
| 单步解码(单线程中位) | 49.16 ms | 17.35 ms |
端到端 2.83×(5/5 逐轮占优),体积压缩 2.7×。整数路径与显式整数公式逐位一致(tests/test_quant_i4.py);对 fp32 GEMV 的相对误差约 0.11,在 4-bit 预期内。
生成质量:英文保持连贯,中文与代码有退化。4-bit 想保质量必须配校准或混合精度,这里只发布"能跑且快"的结论,不声称质量无损。
六、稳定 C ABI
此前的 C 接口是内部用的,改结构就破坏调用方。加一层 sn_abi.h 隔离:
- 符号统一
sn_abi_前缀,与内部符号分离 - 句柄不透明(
SnAbiRwkv7*),调用方不依赖任何结构体布局 - 版本号打包成
(MAJOR << 16) | (MINOR << 8) | PATCH,当前 1.0.0 - 能力位
SN_CAP_*、错误码SN_OK=0/SN_E_INVAL=-1/SN_E_NOMEM=-2/SN_E_IO=-3/SN_E_FORMAT=-4/SN_E_UNSUPPORTED=-5 sn_status固定为int32_t,避免int宽度差异;数组维度参数同样固定int32_t
调用方在编译期拿 SN_ABI_VERSION_MAJOR 传给 sn_abi_check(),返回 SN_E_UNSUPPORTED 就是主版本不匹配,应当拒绝加载。未知能力位必须忽略,这是前向兼容的前提。
#include "sn_abi.h"
if (sn_abi_check(SN_ABI_VERSION_MAJOR) != SN_OK) {
/* 主版本不兼容 */
}
uint32_t caps = sn_abi_caps();
if (caps & SN_CAP_RWKV7) {
SnAbiRwkv7* m = sn_abi_rwkv7_load("model.runtime.bin");
int32_t V = sn_abi_rwkv7_vocab(m);
float* logits = malloc(sizeof(float) * V);
sn_abi_rwkv7_step(m, token, logits);
sn_abi_rwkv7_free(m);
free(logits);
}
经 ABI 与经内部句柄的 RWKV-7 logits 逐位一致(max|diff| = 0.0)。tests/test_abi.py 的 9 项契约测试覆盖版本协商、能力位、错误码路径、递推内核逐位数值、INT8 往返误差。
七、服务化:OpenAI 兼容 + Ollama / LM Studio
7.1 OpenAI 兼容 HTTP 服务(serve.py)
零依赖实现,用标准库 http.server.ThreadingHTTPServer,不引入 fastapi/flask。任何 OpenAI SDK 或客户端可以直接指向:
sunnynet-serve --ckpt <checkpoint.pt> --host 127.0.0.1 --port 8080
sunnynet-serve --gguf <model.gguf> --gguf-backend c --port 8080 # RWKV-7
sunnynet-serve --hf-dir <模型目录> --hf-arch gla --port 8080 # GLA
| 端点 | 说明 |
|---|---|
GET /v1/models |
模型列表(object=model、owned_by=sunnynet) |
POST /v1/chat/completions |
支持 stream: true(SSE data: {...} + data: [DONE]) |
POST /v1/completions |
传统补全 |
GET /health |
存活探针 |
后端选择走 Engine 抽象:HFEngine / GGUFEngine / GGUFCEngine / TorchEngine,采样(temperature / top-k / top-p)在服务侧统一处理。本地实测官方 openai SDK 的 models.list、chat 非流式、chat 流式、legacy completions 四种调用全通过;400(缺字段)/ 404(未知路径)错误处理正常。
7.2 Ollama / LM Studio 接入(integrate.py)
两者都跑 llama.cpp,前提是 GGUF 且架构被内置运行时支持。RWKV-7 GGUF 的 general.architecture=rwkv7、tokenizer.ggml.model=rwkv,Ollama 直接识别(family=rwkv7、parameter_size=191.13M、runner=llamacpp)。
sunnynet-integrate ollama <model.gguf> --name sunnynet-rwkv7 --run --smoke
# [smoke] OK: 'The capital of France is Paris.'
sunnynet-integrate lmstudio --gguf <model.gguf> --models-dir <LM Studio models>
踩了四个坑,都是文档里没有、只有真跑才会遇到的:
- 必须显式写 TEMPLATE。Ollama 内置的
rwkv-world模板会在 prompt 外再包一层User: / Assistant:,输出退化成复读(-of-the-world-of-the-world-...)。写TEMPLATE """User: { { .Prompt }}\n\nAssistant:"""后输出恢复成" The capital of France is Paris.",与自家 C runtime 逐字一致。 Error: could not locate ollama app的真实含义是 server 没运行。ollama create在 server 离线时会尝试拉起本地 app 并失败,报错文本具有误导性。工具改成先探活GET /api/version,离线时直接给ollama serve提示。OLLAMA_MODELS必须与 server 侧一致。只设OLLAMA_HOST时create会把 blob 写进默认模型目录,和运行中的 server 目录错位。- 本地探活须绕开系统 HTTP 代理。企业代理会把
127.0.0.1拦成 502,代码内用ProxyHandler({})构建 opener。
八、SIMD 后端与产物验证方法
AVX2/FMA 之外补了两条 SIMD 后端源码,都含前向扫描、反向扫描、单步三个入口。
AVX-512(F/BW/VL)用 __mmask16 做掩码尾处理,导出 CPUID 探测(leaf 7 EBX bit16)与 ISA 字符串。本机 CPU 实测 AVX512F = 0(i7-8750H 是 Coffee Lake),所以只能做编译与产物验证,不能做执行验证:
| 验证项 | 结果 |
|---|---|
| 编译 | gcc -O3 -mavx512f -mavx512bw -mavx512vl -shared 通过 |
| 反汇编 | 198 处 zmm 寄存器、16 处 vfmadd132ps |
| 加载保护 | 不支持时 load_avx512() 直接抛错,避免非法指令 |
NEON 这条更麻烦:本机没有 ARM 工具链,也没有 QEMU(wsl.exe 被安全策略拦),改用 zig cc 做真交叉编译:
pip install ziglang
python -c "from sunnynet.kernels.simd import cross_compile_neon; cross_compile_neon('<out.so>')"
| 验证项 | 结果 |
|---|---|
| 产物 | AArch64 ELF64(e_machine=183,zig cc -target aarch64-linux-gnu) |
| 反汇编 | capstone 确认 4-lane fmla v3.4s, v2.4s, v4.4s 及 fmul / fsub / fadd |
本机无法执行 NEON,真实执行验证放在 CI 的 ubuntu-24.04-arm runner(原生 aarch64)。这里的方法论是:没有硬件就验证产物——编译通过 + 反汇编确认指令正确 + 加载时做 ISA 保护,比"没测就不写"更能推进项目。
九、CPU 优化还剩多少空间
数据来自 docs/CPU_HEADROOM.md,结论是本机架构下的训练速度已接近结构性上限。
9.1 训练步构成
T=256 / B=8 / dim=80 / L=4,8 线程:
前向 49.2 ms (24.8%) | CE 14.5 ms (7.3%) | 反向 113.1 ms (56.9%) | 完整步 198.7 ms
aten 算子调用:4524 次/步
反向占 56.9%,是最大单块。热点 top:aten::mm 35.2%(63 次调用)、_log_softmax_backward_data 7.1%、aten::mul 6.6%(232 次)、_log_softmax 6.0%、aten::cat 4.6%。mm 已经是高效核,剩下的是 4524 次分散小算子。所以减算子数是唯一有效方向。
9.2 矩阵效率没有空间
实际训练尺寸(8192×80×160,B=32 折叠)已达 237.9 GFLOPS,方阵 1024³ 峰值 295.8——小形状只损失 20%。判据是 ms/token 对 batch 不敏感:B=4/8/16/32 分别是 0.10803 / 0.09049 / 0.10358 / 0.10507 ms/token。如果是被小形状拖累,加大 batch 应该显著降低 per-token 成本,实测没有。
9.3 量化在这台机器上是反向优化
| 精度 | matmul GFLOPS | 相对 fp32 |
|---|---|---|
| fp32 | 295.8 | 1× |
| fp16 | 0.593 | 慢约 477× |
| bf16 | 0.569 | 慢约 520× |
int8(torch._int_mm) |
1.443 | 慢约 205× |
原因是 AVX2 无原生 FP16/BF16 运算、Coffee Lake 无 VNNI,torch 走模拟或回退路径。数值上也不可行:扫描累积的 fp32 相对误差是 4.7e-07(T=2048 仍为 4.5e-07,分块递推稳定),bf16 达 2.75%,量化会同时击穿收敛和架构不变量。
注意这里的结论只对 AVX2-only 的 2018 年移动 CPU 成立。现代服务器(AVX-512 / VNNI / AMX)上低精度与量化的结论会不同。
9.4 线程数:12 线程最差
真实训练步:4 线程 168.5 ms、6 线程 193.4 ms、8 线程 183.3 ms、12 线程 225.4 ms。12 线程明确最差(+23%),和纯 matmul 的结论相反(方阵 matmul 12 线程 GFLOPS 最高)。训练步由小张量主导,多线程只增同步开销。4 线程名义比 8 快 8.8%,但落在 ±15% 轮间极差内,不足以判定;改默认值会让既有全部对照数据需要重测,所以没动。
十、测量方法论
测量方法比代码更值得复用。
进程隔离是硬要求。 同一模型在同一进程里"构建过多个模型之后"再测,耗时可达干净进程的 2 倍以上(T=4096: 325ms vs 776–822ms),而且放大倍数随架构变化。修正做法是每个 (架构, 序列长度) 单元格单独起子进程,进程内只构建一个模型。未做隔离时的前向加速比是 12.63×、交叉点 T≈834;隔离后是 22.69×、交叉点 T≈384。
参数量对齐用 meta device 计数,不用手推公式。 手推公式漏项严重,RWKV 偏差达 34%。
速度结论用逐轮配对,不用单一中位数。 9 轮配对,每轮两架构在相邻时刻各跑一次,抵消轮间漂移。sunny80 vs rwkv 三个指标互相矛盾(中位比、配对比值、符号检验),所以写作"统计持平"而不是"超越"。
微基准不得单独作为采纳依据。 出现过两次:index_select 改写竞争门控在微基准里明显更快(cpu 0.891× / cuda 0.717×),端到端两平台均无差异;块内转置扫描在隔离大维度下快 3.4×,真实 dim=128 下反慢 26.2%——扫描基准必须用真实块维度,两点结论可以完全相反。
十一、边界与局限
工程能力变了,局限没变,照旧列出:
- 短序列是结构性劣势。O(N) 优势只在长序列兑现。batch=1 前向交叉点 T≈384(6 层)/ T≈600(4 层),短于此慢于标准 Transformer。训练态(batch=8)交叉点下移到 T<256,T=256 快 1.5–2.4×,但 T=128 仍慢。
- 解码不是强项。与 O(N) 同行同场对照,rwkv 全程第 1、快 1.9–2.6×;SunnyNet 排名随 T 从第 5 追到第 2(T=4096 时比 Transformer 快 2.18×),但从未追上 rwkv。
- 不存在"领先全部同行"的长度。前向无稳定领先区间,随 T 从第 6 名追到第 3 名,全程未登顶。
- INT4 只有"能跑且快"。英文保持连贯,中文与代码退化,未配校准。
- 0.5B 无质量数据。6GB 卡上 AdamW 需 7.6GB 装不下;SGD 单步勉强通过(5004 MB,仅剩 9% 余量),多步 loss 无下降趋势。
- "GPU 友好"不是结论。8.0–15.2× 是未做 CUDA 侧优化的下界,且无同行 GPU 对照。
- 质量对照未收敛。七架构对比仅 600 步,未逐架构调优学习率,多为单种子,架构间小差距落在种子噪声内(种子间 ppl 极差 6.32 > 架构间差距 3.53)。
- scaling 趋势非单调。1.4M 第 2 → 20M 第 1 → 100M 退回第 2,不能声称"规模越大越有优势"。
不能写的表述:全面更快、规模越大越强、长距离强于 Transformer、泛化能力强。模型是实验科研项目,未经过安全对齐流程。
十二、快速上手
# 安装(发布名 sunnynet-cx,导入名仍是 sunnynet)
pip install torch --index-url https://download.pytorch.org/whl/cpu
pip install sunnynet-cx
# 训练
python prepare_data.py
python build_corpus.py --vocab-size 8192
python train/train.py --preset small --steps 800 --batch-size 8 --seq-len 256
# 推理:torch 后端
python -m sunnynet.cli --ckpt runs/small.pt --prompt "子曰:" --max-new 80 --stats
# 推理:纯 C 后端(先导出权重 blob)
python -X utf8 experiments/_export_runtime.py runs/small.pt runs/small.runtime.bin
python -m sunnynet.cli --ckpt runs/small.pt --backend c --prompt "子曰:" --max-new 80 --stats
# 外部权重:GGUF / HF
python -m sunnynet.cli --gguf <model.gguf> --gguf-backend c --prompt "The Eiffel Tower is located in"
python -m sunnynet.cli --hf-dir <模型目录> --hf-arch mamba2 --prompt "def fibonacci(n):" --stats
python -m sunnynet.cli --hf-dir <模型目录> --hf-arch deltanet --prompt "The capital of France is"
python -m sunnynet.cli --hf-dir <模型目录> --hf-arch gla --prompt "The capital of France is"
# 服务化
sunnynet-serve --ckpt runs/small.pt --port 8080
sunnynet-integrate ollama <model.gguf> --name sunnynet-rwkv7 --run --smoke
# 权重发布导出
sunnynet-release runs/small.pt --out-dir dist/release
# 完整实验
bash experiments/battery.sh
bash experiments/battery2.sh
python experiments/long_sequence.py --only latency
Python 侧加载入口也可以直接调用:
from sunnynet.runtime import load_model
m = load_model("deltanet", "<模型目录>") # 或 "mamba2" / "gla" / "rwkv7"
st = m.reset(m.init_state())
logits = m.step(token, st) # 每步 O(1)
手写内核需要 C 编译器(w64devkit / GCC):把 SUNNY_W64 指向 w64devkit 根目录,或把 gcc 加入 PATH。
结语
比那 9.90× 或 2.83× 更值得留下的是几个工程判断。
六种架构的递推收敛到两种形式,一个内核适配全部——先找共性再写代码,省下的工作量远超优化本身。
没有目标 CPU 也能推进代码:编译通过 + 反汇编确认 + 加载保护,AVX-512 与 NEON 两条路径都做到了"只差执行验证"。
口径比数字重要。9.90× 是批量跨段扫描口径,接到 B=1 单步就归零;1.53–2.05× 是单线程口径,多线程下被摊平。每个数字都带测量条件,结论才站得住。
项目地址:https://github.com/SunnyClaw-cx/SunnyNet
CPU 优化空间分析:docs/CPU_HEADROOM.md
稳定 ABI 说明:docs/ABI.md
完整实验报告:docs/EXPERIMENT_REPORT.md(含全部负面结论)