360M参数模型模拟终端环境,无需真实Shell

开发者微调360M参数模型,实现无需真实Shell的终端环境模拟。该模型基于Qwen2.5-Coder-0.5B,通过预测屏幕字符网格变化运作,支持创建文件、运行vim及游戏等交互,并提供约208MB的浏览器演示版本。

开发者近期微调了一个拥有 360M 参数的语言模型,使其能够模拟终端环境。该模型背后并不依赖任何外部文件系统或真实 Shell,而是通过预测每次按键后的屏幕变化来运作。它能够创建文件、运行 vim 和 tmux、玩贪吃蛇游戏,并通常能记住用户输入的命令。其核心突破在于教会模型修复自身错误,并实现屏幕行的并行解码。目前该项目已提供浏览器演示版本(约 208 MB),用户可尝试输入 python 等命令,体验非真实 Shell 的模拟效果。

行复制优化:每行仅需3个token

该演示主要面向支持 WebGPU 且配备物理键盘的桌面浏览器,推荐在 Chrome 或 Edge 中运行。虽然现代手机也能使用,但性能表现较为缓慢。

受“Genie 3”等世界模型启发,开发者设想未来的应用程序可能不再由代码编写,而是由模型根据用户输入预测下一个最可能的状态。由于缺乏大规模 GPU 集群和视频数据,开发者将目标缩小至终端模拟:终端仅是一个字符网格而非像素阵列,且可以通过运行真实程序生成大量完美的标记化训练数据。预训练的语言模型本身对 ls 或 cat 等命令的输出已有较好认知。

最终模型基于 Qwen2.5-Coder-0.5B,通过在 Docker 容器内运行真实程序记录的终端会话进行微调。

终端视角的建模方式

标准 LLM 采用提示符后逐个追加 token 的方式,这与终端行为不符。终端输出并非总是追加到底部,例如清屏操作会将光标移回顶部。因此,模型需要预测屏幕上的具体变化。

开发者将终端视为一个字符网格,每个单元格包含一个字符。模型不直接处理网格图像,而是处理描述网格内容及其变更的 token 序列。

会话结构

每个会话以快照开始,包含屏幕尺寸、内容及光标位置。

尺寸作为输入的一部分,并非固定为 32×96。在演示中,用户可在自定义起始屏幕时设置行数和列数。

BOS SCREEN_START 32 96
ROW_0 0 'simedw@laptop:~$ '
CURSOR 0 17 1
SCREEN_END

这种机制允许从任意状态启动应用。若在此模型中输入特定指令,其表现优于基础 bash 模型。

随后,每一次按键进入真实终端都会发送原始字节。字母保持原样,Enter 对应 \r,Escape 对应 0x1b,左箭头对应 ESC [D 等。

INPUT_START 'l' INPUT_END

模型需回答哪些行被重写以及光标的最终位置:

OUTPUT_START
ROW_0 17 'l'
CURSOR 0 18 1
OUTPUT_END

上述示例表示在第 0 行第 17 列写出字符 l,并将光标移至第 0 行第 18 列。全屏应用有时会隐藏光标。

无需先清除屏幕,新字符直接覆盖旧字符。

开发者未引入新的角色 token。Qwen 已为所有 256 个字节值分配了 code,这些 code 被复用于字符、坐标及其他小数字。因此,像 17 这样的坐标仅需一个 token,而非两个。

时间也是输入变量。若无按键操作,模型接收带有持续时间的 WAIT token,而非键值。

行复制优化

当用户在终端底部按下回车时,每一行需向上移动一格。若让模型重新输出每行的新位置,单次按键需消耗约 3,000 个 token。

为此,开发者引入了 COPY 操作:

ROW_0 COPY 1
ROW_1 COPY 2  
ROW_2 COPY 3
...

每个操作复制前一屏幕的行。例如,旧的第 1 行变为第 0 行,第 2 行变为第 1 行,以此类推。

滚动操作现在每行仅需 3 个 token,而非 96 个。

类似技巧也适用于清屏、vim 中的行插入与删除以及手册页导航。可以复制一行后覆盖其部分区域。

总体而言,模型使用了仅 333 个 token 的微小词汇表:256 个字节 token、64 个行 token 及少量特殊操作。每个预测均代表有效的屏幕更新。

这一微小词汇表在后续将模型压缩至浏览器时发挥了关键作用。

训练数据生成

开发者最初寻找现有的终端记录数据集(如 asciinema),但未找到所需的按键到屏幕的精确对齐数据。

解决方案是通过在 Docker 容器中运行真实命令来合成可信的终端会话。

合成配方包括:

  1. 随机用户名、主机名、提示风格及终端大小。
  2. 根据虚构人物(开发者、系统管理员等)随机选择目录和文件。
  3. 选择 6–24 项加权活动(修改文件、vim、python、tmux 等)。
  4. 大多数会话旨在打开已存在的文件。
  5. 逐键输入,包含标签补全等。错误率设定为 1.2%。
  6. 35% 的课程从中途开始,迫使模型学习任意状态下的行为。

终端尺寸主要为 32×96,部分为 24×80 和 40×120,以防止模型过拟合单一尺寸。

随着模型能力提升,数据集不断扩展,最终达到 336 万集。相比之下,基础模型在训练期间看到了约 18T token。

训练过程

起点选用 Qwen2.5-Coder-0.5B,因其较小且适合实时性能,同时作为编码模型已见过大量 CLI 使用场景。

初期采用简单的监督微调:给定屏幕历史和按键,预测真实终端产生的屏幕值。

常规打字很快被学会:字母出现、退格删除、光标移动。

但在长会话中,错误开始累积:文件名中的一个错误会在剩余会话中传播。

从自身错误中学习

为解决此问题,开发者借鉴了 DAgger 算法的思想,让模型从其访问的状态中学习,而不仅仅是理想演示。

对于每个训练示例,流程如下:

  1. 在录制的会话中选择一个点,通常靠近产生输出的命令。
  2. 让模型预测接下来的 1-4 个事件。
  3. 比较结果屏幕与实际录制屏幕。
  4. 若差异足够小,在下一次按键时修正模型的屏幕。
  5. 继续训练正确的帧及随后的真实事件。

Delta 将模型的错误屏幕转移到正确的下一屏幕。本质上,模型学会了“修复”自己的错误。

存在一个重要限制:模型必须有足够信息推断修正。通过校正引入的任何单词必须出现在之前的真实屏幕或用户输入中,而不能仅存在于模型自身的预测中。

开发者无法指望模型正确列出从未见过的文件名,也不希望增加幻觉。

约三分之一的采样会议产生了有用的校正示例。每次迭代都从最新模型中收集错误。

自我校正是最有效的训练改进之一。结合额外训练数据,它将 PresetBench(下文定义)的成绩从 int4 精度下的 24/47 提升至 38/47。

整个模型训练及后续优化在 RTX 4090 上耗时约 75 GPU-hours。

测试评估

使用了三项测试:ShellBench 包含来自 24 个手写 bash 会话的 106 条命令,按输出精确度评分。PresetBench 包含 47 个脚本案例,源自演示中遇到的失败情况。这两项测试均未出现在训练数据中。

记忆测试 包含 32 个世界。每个世界向两个文件写入两个随机值,然后在不同会话中读回。只有两次读回完全准确才算通过,总计 64 次读回。名称和值均未出现在训练数据中。

微调带来的提升显著:

Model ShellBench /106 File readbacks /13
Qwen2.5-Coder-0.5B, prompted 52 2
Qwen3-0.6B, prompted 60 3
WorldTUI 80 9

最大差异体现在记忆能力上:WorldTUI 正确完成了 9/13 的文件读回,而提示模型仅为 2/13 和 3/13。

尽管它仍远非可靠的文件系统,但对熟悉命令的处理相当不错。然而,以新方式组合命令容易出错。例如,执行 echo green >> colors 后,运行 cat colors 往往不会显示追加的行。

性能优化

起初开发者避开了自回归模型,因为完整重绘涉及约 3,000 个 token,逐个生成似乎无法满足终端的实时性要求。

但由于自回归模型性能远超其他方案,开发者最终妥协。单 token 生成时,打字速度约为 ~38 ms/key,尚可接受;但涉及全屏重绘的操作(如 vim test.txt, man, clear)在 RTX 4090 上耗时高达 12 秒。开发者希望能在 Mac 上实现近实时运行。

并行行解码

关键在于意识到一帧中的大多数行彼此独立,无需依次生成。开发者为每一行运行一个批次元素。每行获取相同的历史记录(包括 KV cache),但仅生成其自身的更新。

这将解码成本从所有行之和(最多 3,000 步)降低到最长行的长度(约 100 步)。

光标随后成为最后的一个独立步骤,可以看到所有行更新的集合。

这需要在训练期间匹配注意力掩码,但速度提升值得付出代价。大框架 p95 延迟从 6.7 秒降至 0.87 秒。

Large-frame p95 latency (RTX 4090) ↓ Sequential 6.7 s Parallel rows 0.87 s

行间的协调问题

并行解码带来了新问题:由于行之间相互独立,它们无法协调,导致 vim 可能在第 3 行而非第 0 行打开,或多行同时添加贪吃蛇的食物。

解决方案是推测解码(Speculative Decoding)。

通常推测解码使用小模型起草 token,大模型验证。得益于因果注意力,多个 token 可在单次前向传递中验证。

此处使用同一模型完成两项任务:并行行生成草稿,随后模型顺序验证草稿。

验证器保留直到第一个分歧点的 token。从那里开始,它生成自己的修正并重新提出剩余的草稿。

在 M4 Mac 的浏览器中,输入单个字符约需 60ms。较大的更新如 man test 顺序执行需 2.5s,而使用并行起草和验证则降至 0.9s。

模型小型化

初始模型 Qwen2.5-Coder-0.5B 约有 494M 参数。为实现网页演示,需大幅缩减体积。其 151,936 个 token 词汇表的嵌入层占 136M 参数。由于仅需 333 个 token,几乎全部嵌入层均可移除,使模型从 494M 减至约 360M 参数(990MB → 716MB)。

随后量化至 int4,体积进一步降至 208 MB。

浏览器演示采用自定义 JavaScript 实现的 Qwen 解码器,计算着色器使用 WGSL 编写并通过 WebGPU 运行。着色器在推理期间解包 4-bit 权重。激活值和注意力缓存使用 32-bit 浮点数。所有推理均在本地标签页中运行。

知识蒸馏实验

蒸馏至更少层数的实验揭示了内存依赖的关键层。教师模型为早期检查点,ShellBench 得分 78/106。仅移除第 7 层且不重新训练,下一 token 准确率几乎不变:99.3% 降至 98.9%。但精确内存读回从 64/64 跌至 0/64。这些测试输入真实历史,因此失败并非由模型自身输出错误的累积引起。

首个蒸馏学生模型保留了所有偶数层,但使用第 23 层而非第 22 层作为最终层。它能写出合理的终端输出,但在 64 次内存读回中仅 31 次正确。它始终打印出写入的两个值之一,但约一半时间选错。这表明它丢失了记忆功能,且在 480M 训练 token 后仍未恢复。

该学生模型丢弃了第 1、7、15 和 22 层。另一个保留这四层的 16 层学生模型通过了所有 32 个记忆测试世界,实现 64/64 精确读回。它在 4-bit 精度下也通过了所有 32 个世界,体积约 138MB。但在其他方面存在代价:ShellBench 得分为 65,低于教师模型的 78。这说明学生模型可能看似正常,却悄悄失去了记忆能力。

最终,开发者放弃了蒸馏路线。

功能表现

记忆能力

这是开发者最关注的部分。生成令人信服的目录列表很容易,难点在于创建文件、编辑、离开后再回来发现文件未被改变。

首次成功启动 tmux,在 vim 中打开文件,修改并保存,然后在不同的 tmux 窗口中使用 cat 读取更新内容时,开发者感到兴奋:“它活了!”

其他应用

为确保超越 bash 提示符的功能,训练数据涵盖了 vim、python、man pages、snake 甚至 tmux。

在 vim 中,它处理 esc 的模式切换,并允许保存、退出或返回插入模式。

可能是世界上计算强度最高的贪吃蛇。

tmux 的表现尤为惊人:可以创建多个窗格,在每个窗格中执行各种操作并在其间跳转。虽不完美,但对于一个小模型而言已属难得。

它甚至管理环境变量和变量展开。

Python 支持

鉴于基础模型是编码模型,开发者好奇其对 Python REPL 的处理能力。它做出了不错的尝试:小加法通常正确,列表推导式返回列表,尽管内容常有误。它对拼写错误略宽容:pythoon 能启动 Python,但 ython 不能。

局限性

Docker 容器未允许互联网访问,因此 lynx 等工具表现平淡。

Python 能处理非常简单的操作,但更复杂的任务会产生类似 GPT-3 时代的幻觉答案。

有时在创建嵌套目录时会迷失方向,突然处于不同路径。一致性仍有很大提升空间。

测试显示,Firefox 中的演示比 Chrome 慢约 20 倍。两者均使用 WebGPU,原因尚不明确。

此类项目通常先自行解决,再参考他人方法。早在 2022 年,人们已通过提示 ChatGPT 模拟 Linux 终端。近期,GameNGen 使用扩散模型模拟 Doom,NeuralOS 则针对桌面 GUI。WorldTUI 更接近原始的 ChatGPT 终端技巧,但专门针对按键和屏幕更新进行了训练,包括 vim 和 tmux 等全屏应用。

总结

构建该项目充满乐趣。其实用性有限,但对于一个在浏览器标签页中运行的 360M 参数模型而言,其表现远超预期。

评论 0

0/500

评论需审核后展示,请文明发言

💬
还没有评论,来说两句

相关阅读