模型 · 它的头脑

一个编在程序里的小模型

这一页的数字不是从文档里抄的,是直接从编进 exe 的那份检查点里读出来的—— 连架构那棵树在内。想看它长什么样,回首页; 想知道它怎么用,看常见问题

01 骨架

递归 ×5,只有一套权重

Polarbear 的自进化架构只搬了推理这一半——训练与进化那部分没有进来。 它的骨架是一条权重绑定的递归:同一个块被反复用,所以"多深"不花额外的权重。

输入一个字符词嵌入17829 → 256输出投影256 → 17829下一个字符采样递归 ×5 (权重绑定 · tied)idididididblk跑 5 遍六步里只有第三步是一个真的解码块(1 头注意力、前馈宽 1024),另外五步是恒等。它被递归跑 5 遍,用的始终是同一套权重——检查点的 pool 里只躺着一个块。所以"5 层深"不花 5 份权重:整个模型才约一千万参数、40.8 MB。注意这是推理子集——Polarbear 的自进化与训练那部分没有进这个程序。
为什么是递归tied · pool = 1
同一个块反复用,深度就不必按份数付权重。代价是它没有真正的多层表示能力—— 8 次应用换来的是一套权重被反复压过 8 遍,不是 8 份不同的抽象。这是 Polarbear 那条路线的特点,不是这里省事的做法。
注意力只有一头heads = 1
d_model 256 配 1 头注意力、1024 宽前馈。这一点是从检查点的 genome 里读出来的, 不是文档里那句(文档里那句写的是另一份检查点)。
字符级vocab 17,829
词表是一个个字符:0–5 是控制符,后面是 ASCII,再后面是汉字,一共 17,829 个。 它不是按词切分的——所以它天生就在「接着写」,而不是「回答问题」。
那 10,198,949 个参数是从哪来的加起来正好是它
一个块是 4d² + 3df + 2f + 7d(注意力四组矩阵、 SwiGLU 三组、偏置与两个归一化增益),代入 d = 256、f = 1024 得 1,052,416; 词嵌入 17,829 × 256 = 4,564,224,输出投影同宽 = 4,564,224; 再加输出偏置 17,829 与最后一层增益 256。五项相加正好是 10,198,949——和检查点里报的那个数一位不差。 这也就是「只有一个块」的算术证据:五遍递归在这里一次都没多算钱。
02 训练

它是在什么上面微调的

内置那份是从一个中文维基基座出发,在 BelleGroup 的 一百万条中文样本上微调出来的。这些字段就写在检查点文件里。

检查点格式
polarbear-checkpoint-v1
数据集
BelleGroup--train_1M_CN (100 万条中文样本)
微调后 val_loss
2.263919 (基座是 3.8446)
d_model
256
词表
17,829 字 · 字符级
注意力
heads = 1 · d_ff = 1024
递归
depth 5 · tied · pool = 1
max_seq
128 (训练时的序列长度,不是 KV 缓存的硬上限)
参数量
10,198,949
权重常驻
40.8 MB (f32,没有量化)
文件大小
48.4 MB (gzip 后;解压开是 126.9 MB 的 JSON)
加载耗时
约 0.95 秒 (实测 941 / 958 ms;在后台线程,界面先出来)
吞吐
约 100–110 tok/s (实测 99.6–109.8,char 级,CPU)

微调就是 Polarbear 自己的 train --model <checkpoint>,能只用 CPU 跑。 架构与词表沿用基座,所以换检查点不用改一行推理代码。

优化器CPU 只有 SGD
CPU 那条路没有 Adam——Adam 只在 GPU 侧有。所以纯 CPU 微调是 SGD 加上全局范数裁剪;GPU 侧用的是同一套裁剪规则(同一个函数),只有更新那一步换成 Adam。
梯度裁剪GRAD_CLIP_NORM = 10
范数有限且在界内:整段缩放直接跳过,参数逐位不变。 超界:全部梯度缩到恰好落在界上。非有限:全部梯度置零, 这一步成为空转——而损失本来就是 NaN、本来就会上报。 第三种最要紧:它不是把坏数据抹平,是让它空转一次、同时把发散说出去。
为什么是 10当初的实测记录
--lr 10 --batch 1 时梯度范数冲到约 98, 裁剪让 40 步跑满;而同时期还没有这道裁剪的 CPU 在第 8 步就报非有限损失停下。 正常学习率下它从不生效(Belle 上实测峰值 0.64,约 15 倍余量)—— 这是保险丝,不是调参旋钮。
损失是怎么算的PAD 不算 · NaN 要报
填充位置的 target 被排除,否则模型会学会输出填充符;含非有限值的那一行给零梯度、但给 NaN 损失——一行坏数据不该污染权重, 但发散必须看得见,不能被 softmax 悄悄抹平。
03 证据

凭什么信这些数字

整个 Polarbear 有 16,271 行 Rust、134 个测试、30 个 CUDA 内核。 其中相当一部分不是测「功能有没有」,而是测「两条各自独立的实现会不会给出同一个答案」—— 因为一个算错的反向传播同样能让损失下降,光看损失是看不出来的。

有限差分唯一能抓出反向写错
这是唯一能发现「梯度算错」的测试:其余测试只断言「损失在下降」, 而一个方向错的梯度照样能让损失下降。它把反向循环逐行重放得到解析梯度, 再与模型自身损失的中心差分对比。覆盖 11 种拓扑(含嵌套递归、tied 与 untied)、 每个参数组的全部 16 个张量、以及输出头那 3 个。判据是绝对差 ≤ 1e-6/(2ε) 或相对误差 < 5%,步长 ε = 1e-2 —— f32 里中心差分的舍入按 1/ε 涨、截断按 ε² 涨, 1e-2 正好是两者相当的位置。
增量必须等于全序列cached == full
对话走 KV 缓存(每个 token 的重算量从 O(历史²) 降到 O(历史)),训练与校验走全序列。 这两条路必须给出同一份 logits,有一条测试逐个拓扑对拍。 任何位置偏移、旋转角度取错、或者「该不该看到某个 token」的错误,都会在这里立刻暴露。 缓存里只有当前位置之前的 token,所以增量那条路按构造就是因果的,用不着掩码。
CPU 与 GPU 是同一个函数16 种拓扑
GPU 那条路是 CPU 的一份镜像。等价性比的是走一步训练之后的损失,不是权重—— 损失对得上,才说明「前向 + 反向 + 优化器」这整条链在两边是同一个函数。 权重逐位相同反而可能是巧合:两条路完全可以在某个分支上分道扬镳。
改一处要连带改十处16 个张量的清单
一个块的那 16 个张量在源码里被逐个列出至少十处:前向、反传、梯度清零与累加、 缩放、范数、参数量、检查点校验、复杂度计费、有限差分清单、GPU 侧的上下行搬运。 少改一处的后果不是编译不过,而是「前向用它、优化器不更新它」这类静默缺陷—— 所以有限差分那张清单是照着这 16 个写的:不列,就等于没测。
这些守卫盖不到的地方
词嵌入的梯度不在有限差分清单里(它是散点累加,不是输出头反传的返回值), 这是唯一没被那条守卫覆盖的参数。
范数累加的精度两边不同:CPU 用 f64 累加平方和,GPU 侧是 f32—— 判据(10)在正常学习率下离边界很远,所以这是有意的取舍,不是待修的 bug。
构建里嵌了 sm_75(Turing)的 SASS,但手上没有那种卡, 所以那条路径只验证到「能编译、能链接」。

这一节的拓扑数、判据与常数出自 Polarbear 的实现文档,每条结论后面都跟着 文件:行号,可以直接跳过去核对;文档里那条性能记录也自己标明了 「当时实测、本次未重测」。上面 01 / 02 的数字才是从内置检查点里读出来的—— 两者的出处不一样,所以分开写。

04 局限

它还不是一个会说话的模型

这一节写在这里,是因为不说清楚会让人白期待。 下面每一条都是项目自己的实测记录,不是推测。

它每次收到的,就是这样一句话
现在你是一只爱说话、爱撒娇的马尔济斯小狗,说话短、带汪和蹭蹭。 心情:心情很好,尾巴轻轻摇~ 记得:累。 用户说:今天加班到十点,好累 小狗说:

Polarbear 是字符级续写模型,训练语料里没有角色标记——所以没法像别的模型那样按「用户 / 助手」轮次喂它。 唯一可行的一条路是:把人格、心情、记忆和你这句话拼成一条流,末尾用 小狗说: 当续写起点,让模型从这里往下写。它接的是你的话,不是回答你的话——跑偏正是从这里来的。 (模板在 src/prompt.rs,那一行续写起点是整个提示里最关键的一行。)

同一句话,它那次真的这么写
你:今天加班到十点,好累
模型:“我可以享受她的爱好。”一直在我的爱好者,我最喜欢的朋友喜爱。无
108.5 tok/s · 上下文 128 位置 · 用 maltese.exe -cli 录的
温度在采样,同一句话每次都不一样——所以这是它某一次真的写下的,不是「标准答案」。 另有三句实录可以在首页那一栏点出来听。 项目里一共量过三份检查点、五种模板、十种采样配置:二十五组里没有一组是对话。 采样不是原因——温度从保守扫到激进、top-k 从 5 扫到 40,没有哪个区间是人话。
它的产出是什么续写,不是回答
一千万参数的字符级模型。它能说出局部通顺、语气像小狗的短句—— 这正是产品要的;它不能做有逻辑的多轮问答、不能记住事实、不能听指令。 所以它经常不是在回答你,而是在接着你写。
那为什么还要装模型作者已拍板
项目文档量完证据之后,把「接受台词表是主体」标成了数据支持的选项;作者选了另一条:模型是主体,台词表退回严格的降级路径(只在没有模型、或模型加载失败时出现)。 也就是说「让它会说话」是产品主线,不是可选优化。
所以现在装上去会看到什么
装了模型(默认如此)之后,那句回复由模型自己写——你会看到上面那种续写。 但意图与记忆并没有停:它们照样在跑,侧栏「记得 累 · 吃饭」是真的会长出来的, 而且会进模型的提示词(见上面那条模板)。变的只是「那句话谁来说」。不带模型启动(--no-model)反而是更稳的那条路: 它退回台词表,用「意图 → 记忆 → 心情」三层结构回话,接得住你的话。首页那一栏可以直接试的,就是这条。
往上走只有两条路不是调 prompt
prompt 与采样参数已经试到尽头(两代检查点、五种模板、十种采样配置),不要再往 prompt 里塞技巧。要么继续找或训一份真能对话的检查点,要么接受模型是彩蛋。 作者选了前者,所以下一步是换检查点,不是改提示词。
05 换模型

用你自己的检查点

Maltese 不挑模型。只要架构与词表和它期待的一致, 换个检查点不用改一行代码。

  1. 把检查点丢进 exe 同级的 models/

    文件名以 .json.json.gz 结尾。 gzip 那份是直接可用的——内置那份就是同一个格式。

  2. 在左栏「模型」下拉里选它

    下拉会列出 models/ 里所有认得出的检查点,并写出各自的参数与加载耗时。读不出来的那个不会被「认下来」——界面上的一次失败,不该变成持久状态里的一句谎。

  3. 或者干脆不用模型

    maltese.exe --no-model:回复走内置台词表。 这是现在更稳的那条路,也是首页那一栏跑的东西。

哪些检查点能用
检查点里带自己的 genome 与词表,是自包含的。但词表必须对得上—— 内置那份是 17,829 字。项目里留下的几份对照里,有一份词表只有 2,158 字(中文覆盖差很多), 那种换进来会明显吃力。选之前先看一眼它的词表大小。
载入时会逐项校验七步,顺序不能调
格式串 → 架构(头数能不能整除、层数会不会溢出)→ 参数组数 →逐组比对形状 → 词嵌入与输出头 → 每个张量的 shape与数据长度是否自洽 → 词表大小。每一步都对应一个真实发生过的故障,所以顺序是钉死的: 早先一个不能整除的头数会让所有命令以裸 abort 结束,而不是报出问题。
为什么要比形状,不只比组数数字对、描述错
保存的权重会替换物化出来的那一份,于是「跑的是保存的权重, 而所有读架构的地方描述的是另一个架构」。实测过一份基因组被改成 ff=128、权重仍是 ff=96 的检查点:载入退出码 0,inspect Block(h=4,ff=128),而参数量(29,633)与验证损失(2.1899) 一个都没变——数字是对的,只有描述是错的。只比组数抓不到这个。
还有一类文件会被拦下被截断的
序列化会绕过长度断言,所以文件被截断也能通过前面那些检查。实测后果是 sampleindex out of bounds 处崩掉,而 inspect(不跑前向)退出码 0—— 所以载入时会显式把每个张量的形状与长度对一遍,而不是信任文件。
换架构时要留意的两个坑
一、head 维度等于 1 时位置编码是恒等操作——那种架构完全没有位置信息 (heads == d_model 就会这样)。内置这份是 1 头、 每个 head 256 维,不在这里。二、只含注意力或只含前馈的块,另外半边照样分配: attention-only 的块里约四分之三的前馈参数从不被训练、也从不被更新。省不掉, 这是已知的实现现状,不是配置问题。
它认得出文件名里的信息
产物名是 polarbear-<kind>-<dataset>-<arch>[-tag]-<stamp>.jsonkind trained / finetuned / champion / genome / manual / snapshot 之一,arch 形如 d256-l5-h1; 旁边还有一份 .polarbear-index.json 侧车索引。 认不出 kind 的一律归入「其他」——宁可承认不知道,也不猜一个
自己训一个检查点

上面那些检查点的来路就是它。这张卡与下载页上那张读的是清单里的同一份数据 —— 换文件、改地址只动 versions.json 一处,两页一起变。

Polarbear 训练端自己训一只,或者改它的架构
2,882,048 B2.9 MBWindows 10 / 11 · 绿色免安装
SHA-256D36C47E3 E6CDF468 9F6EA728 FCBF3554 C3429122 42CE9E3D 158800C6 05E1BEA2
  • evolve 架构进化(不训练权重,改结构)· train 训练 / 继续训练 · chat 交互对话(多轮记忆 + 流式输出)
  • sample 从检查点一次性生成 · inspect 查看基因组与检查点内容 · models / datasets / gpu · demo 内置快速演示
  • 双击进交互菜单,也可以直接下命令:polarbear train --data ‹数据集› --steps 300
  • 它自带 demo,装完立刻能看polarbear demo 会当场从零进化一个小架构给你看(实测 8 个体 × 4 代、d_model 32,几秒跑完,不写任何文件)。
  • 正经训练要两个目录:data/(语料,放进去会被自动发现,支持 JSONL / JSON 数组 / 纯文本)与 models/(检查点)。目录空着时它会直接告诉你怎么放,不会报一串错。
  • 继续训练用 --model FILE,架构与词表沿用那份检查点——Maltese 内置那份就是这么微调出来的(BelleGroup 1M 中文样本,val_loss 3.8446 → 2.2639)。

它的脑子说完了

想让它现在就说话,最快的一条路是不带模型启动——台词表那条。

下载Windows

离线 · 只存本地 · 不联网