跳到正文
Hacker News (official RSS)(simedw.com)·· 5 小時前精選AI 評分71

WorldTUI:360M 引數模型在瀏覽器中實現終端機

WorldTUI: I trained a 360M model to pretend to be a terminal

AI 導讀

WorldTUI 是一個 360M 引數模型,經過微調後能在瀏覽器中模擬終端機,直接回應鍵盤輸入並預測螢幕變化。

\n\n- 技術概念:模型以字元格為輸出,使用 333 個 token(256 個位元組、64 行 token 及少量特殊操作)進行語法限制的螢幕更新。

推薦理由

此模型演示了在瀏覽器中實現終端機的可行性,透過 360M 引數模型與並行行列解碼,讀者可參考其技術方案與效能最佳化,並瞭解自我修正與行列並行對速度提升的貢獻,並在瀏覽器端實現 60ms 以上的回應時間。

正文 · AI 翻譯

TL;DR: 我微調了一個360M引數的語言模型,使其能作為終端機。沒有後端的 shell 或檔案系統:每一次鍵入都直接送入,模型預測畫面如何改變。它可以建立檔案、執行 vim 與 tmux、玩蛇遊戲,並且通常會記住你在幾條指令後所寫的內容。最大的成就包括教它修正自己的錯誤以及並行解碼畫面列。在瀏覽器中試試 (208 MB),並輸入 pythoon 以確認它不是一個真正的 shell。

此示範為桌面瀏覽器搭配 WebGPU 與實體鍵盤所設計。Chrome 或 Edge 是最佳試用環境。它在現代手機上也能運作,但速度相當慢。

去年第一次看到 Genie 3 時,我開始想像一個應用程式不是由程式碼寫成,而是由學習產生的未來。取代遊戲引擎渲染畫面,模型會根據使用者輸入預測下一個最可能的畫面。

自然我想訓練自己的世界模型。但我沒有龐大的 GPU 叢集或大量影片資料,所以我將目標縮小:不再是遊戲世界,而是終端機。

終端機證明是一個很好的選擇。它只是一個字元格格子,而非畫素,我可以透過執行真實 shell 產生大量標記完備的訓練資料。預訓練的語言模型已經相當瞭解 ls 或 cat 應該輸出什麼。

我真正想知道的是它能否維持一個一致的世界,還是僅僅產生令人信服的終端輸出。

最終模型基於 Qwen2.5-Coder-0.5B,並在 Docker 內執行真實程式錄製的終端會話上進行微調。 跳轉到範例影片

它如何看待終端機

標準 LLM 會接受提示,然後一次一個地在末尾追加 token。這對終端機來說不太適用。單一鍵入就可能自行完成某事,例如 vim 中的 esc,輸出也不一定會落在最底部:clear 會清除畫面並將遊標移回頂端。因此,我不再預測文字流,而是預測畫面的變化。

我把終端機視為一個字元格格子,每格一個字元。1 模型並不直接看到格子,而是接收描述其內容及變更的 token 序列。

會話

每個會話都以一個快照開始:畫面尺寸、內容以及遊標位置。

尺寸是輸入的一部分,並非固定為 32×96。在示範中,你可以在建立自訂起始畫面時設定行列。

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

這也讓我能在應用程式中途開始。給它一個 bash 提示、vim 緩衝或蛇遊戲,模型會嘗試從那裡繼續。(從 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 欄。最後的 1 表示遊標可見,因為全螢幕應用程式有時會隱藏它。

不會先清除任何東西。新字元只是覆蓋舊字元。

我不需要為字元新增 token。Qwen 已經有 256 個位元組值的 token,我把它們重複使用於字元、座標及其他小數字。因此像 17 這樣的座標只需一個 token,而非兩個。

時間也是輸入之一。若沒有按下任何鍵,模型會收到一個帶有持續時間的 WAIT 令牌,而不是鍵,這就是蛇遊戲能自動移動的原因。

複製行

當你到達終端機視窗底部並按下 Enter 時,每一行都必須上移一行。我本可以直接要求模型重新輸出每一行的新位置,但這對於一次鍵入而言大約需要 3,000 個令牌。

因此我改用 COPY 操作:

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

每一次操作都會從上一個畫面複製一行。在此範例中,舊的第 1 行變成第 0 行,第 2 行變成第 1 行,依此類推。

一次捲動現在每行只需 3 個令牌,而非約 96。

同樣的技巧也適用於清除畫面、在 vim 中插入與刪除行,以及瀏覽 man 頁面。你亦可先複製一行,再覆寫其中一部份。

總而言之,模型使用的詞彙量僅 333 個令牌:256 個位元組令牌、64 個行令牌以及少量特殊操作。解碼受語法限制,故每一次預測都會產生合法的畫面更新。

這個小小的詞彙量稍後會再次派上用場,當我將模型壓縮到瀏覽器中時。

生成訓練資料

我最初尋找現有的終端機錄製資料集,包括 asciinema,但未能找到符合我所需的精確鍵擊與畫面對齊的資料集。

幸運的是,自己建立並不困難。

基本思路是透過在 Docker 容器內執行實際指令,並記錄每一次鍵擊及其後的畫面,來生成可信的終端機會話。

合成流程如下:

  1. 隨機化使用者名稱、主機名稱、提示樣式與終端機尺寸。
  2. 根據虛構人物(開發者、系統管理員等)隨機化目錄與檔案。
  3. 挑選 6–24 種加權活動(修改檔案、vim、python、tmux 等)。
  4. 追蹤狀態,確保會話大多開啟實際存在的檔案。
  5. 像人一樣打字:一次一個鍵擊,並使用 Tab 補全等功能。我將錯字率設定為 1.2%,對我而言算是寬鬆。
  6. 35% 的會話從中途開始,迫使模型學會從任何狀態開始工作。

終端機尺寸大多為 32×96,亦有 24×80 與 40×120,這樣模型就不會過度擬合單一尺寸。

隨著模型能力提升,我持續擴充資料集,最終達到 336k 個情節,約 20 億個令牌。作為對照,基礎模型在訓練時處理了約 18 兆個令牌。

訓練

我以 Qwen2.5-Coder-0.5B 作為起點。為了接近即時表現,我需要一個小型模型。作為編碼模型,它已經接觸過大量 CLI 使用。

我先進行簡單的監督式微調:提供畫面歷史與一次鍵擊,預測真實終端機產生的畫面差異。

普通打字很快學會:字母出現、退格刪除、遊標移動。

然而,在較長的會話中,錯誤開始累積:檔名錯誤會在整個會話中傳播。

從自身錯誤中學習

為瞭解決這個問題,我參考了 DAgger,模型從自身訪問的狀態學習,而非僅僅依賴理想示範。2

對於每個訓練樣本:

  1. 挑選錄製會話中的一個點,通常靠近產生輸出的指令。
  2. 讓模型預測接下來的 1–4 個事件。
  3. 將產生的畫面與真實錄製畫面進行比較。
  4. 若差異足夠小,則構造修正,使模型在下一次鍵擊時修復畫面。
  5. 繼續在修正後的畫面與幾個後續真實事件上進行訓練。

Delta 將模型錯誤的畫面轉換為正確的下一畫面。實質上,它學會「修復」自己的錯誤。

有一個重要限制:模型必須擁有足夠資訊來推斷修正。任何由修正引入的字詞必須已出現在較早的真實畫面或使用者輸入中,而非僅在模型自身預測中出現。

例如,如果模型拼錯了先前出現過的檔名,我可以教它修正。但若它列出從未見過的檔名,我就無法期待它正確。

在這些例子上訓練實際會獎勵幻覺,而我並不需要更多此類。

約三分之一的抽樣會話產生有用的修正範例。我重複進行三輪,每輪都從最新模型收集錯誤。

自我修正是較有效的訓練變更之一。加上額外的訓練資料,它將 PresetBench(下文定義)從 24/47 提升至 38/47(int4)。

總體而言,訓練模型及其後續最佳化大約耗時 75 小時 GPU,使用 RTX 4090。

測試

我使用三項測試。ShellBench 包含 106 條指令,跨 24 個手寫 bash 會話,按精確輸出評分。PresetBench 包含 47 個指令碼案例,取自我在示範中遇到的失敗。兩項測試均未出現在訓練資料中。

記憶測試 共有 32 個世界。每個世界將兩個隨機值寫入兩個檔案,然後在不同會話中讀回。只有兩次讀回都完全相同,該世界才算通過,總共 64 次讀回。名稱與值從未出現在訓練中。3

微調到底能帶來多少幫助?

Model4 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/鍵,但任何重繪整個畫面的操作(vim test.txt、man、clear)在我的 RTX 4090 上最多耗時 12 秒,正如預期。這給了我足夠時間重新考慮是否需要手冊。我也想在 Mac 上以近乎即時的方式執行它。

行並行

我忽略的是,畫面中的大多數行互不依賴,因此沒有必要一次生成一行。我為每行執行一個批次元素。每個元素都使用相同的先前歷史(包括 KV 快取),但僅生成自己的更新。

這將解碼成本從所有行總和(高達 3,000 步)改為最長行長度,約 100 步。

遊標隨後成為最後的一個步驟,能看到整個行更新集合。

這需要在訓練時使用相符的注意力遮罩,但速度提升值得。大型畫面 p95 延遲從 6.7 秒降至 0.87 秒。

大型畫面 p95 延遲(RTX 4090)↓

Sequential

6.7 s

Parallel rows

0.87 s

無法互相通訊的列

這聽起來可能太好得不真實,實際上確實如此。 問題在於列是獨立的,無法協調,導致 vim 在第 3 列開啟而不是第 0 列,或多列同時為蛇新增食物。

解決方案是推測式解碼。

通常推測式解碼會使用小型模型草擬 tokens,並用較大型模型進行驗證。由於因果注意力,幾個 tokens 可以在一次前向傳播中完成驗證。

在此我使用同一模型完成兩者:並行列產生草稿,然後模型再以順序方式驗證。

驗證器保留到第一個不一致為止的 tokens。從那裡它生成自己的修正並重新提出剩餘草稿。

在我的 M4 Mac 瀏覽器上,輸入單個字元大約 60 毫秒。像 man test 這樣的大更新以順序方式需要 2.5 秒,或以並行草擬與驗證方式為 0.9 秒。5

縮小模型

我從 Qwen2.5-Coder-0.5B 開始,它大約有 494M 引數。我非常想要一個網頁示範,因此需要縮小它。其 151,936 個 token 詞彙的嵌入表佔 136M 引數。我只需要 333 個 token,6,所以可以捨棄大部分,將模型從 494M 引數縮減至約 360M(990MB → 716MB)。

接著我將其量化為 int4,將大小縮減至 208 MB。

瀏覽器示範使用自訂的 JavaScript 實作 Qwen 解碼器,計算著色器以 WGSL 編寫並透過 WebGPU 執行。在推理過程中,著色器會解碼 4 位元權重。啟用值與注意力快取使用 32 位元浮點數。所有推理都在標籤本地執行。

蒸餾

將其蒸餾成更少的層教會我哪些層對記憶依賴。教師模型是較早的檢查點,在 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。它仍以約 138MB 的 4 位元模型通過所有 32 個世界。然而,成本出現在其他地方:它對 ShellBench 的正確命令數為 65,低於教師的 78。因此,學生看起來沒問題,但實際上已悄悄失去記憶。

最終我放棄了蒸餾。

它能做什麼

記憶

這是我最感興趣的部分。 生成逼真的目錄列表很簡單。我想要的是建立一個檔案、編輯它、離開,再回來時發現它未被改動。

第一次我啟動 tmux,開啟 vim 的檔案,修改後儲存,然後在另一個 tmux 視窗中使用 cat 讀取更新內容,我感覺像弗蘭肯斯坦博士:它活了起來!

其他應用

對我而言,這能在 bash 提示符之外工作非常重要,所以我在 vim、python、man page、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 引數模型來說,它遠超出我的預期。

我最感興趣的是,它能在未實作任何底層規則的情況下,維持相當一致的世界。

它顯然是一個相當弱的世界模型,我很想看看如果提升一致性並改用不同的螢幕更新方式(或許使用擴散模型取代自回歸解碼),這種方法能走多遠。

接下來我想嘗試的實驗是訓練數千個隨機生成的應用程式,每個都有自己的控制與規則。目的是觀察模型在擁有足夠上下文線索時,是否能預測它從未遇過的應用程式行為。

在此試用。下載 208 MB,完全本機執行,Chrome 最佳。試著破壞它,告訴我發現了什麼。

我會盡快發布權重並在此加入下載連結。


  1. 它是單色的:只有字元,沒有顏色或文字樣式。Unicode 字元,包括 tmux 的框線,會在編碼前對映為 ASCII,因此這些框線不需要多位元組字元標記。 ↩

  2. 與我在 midi 文章 中的排程取樣相差不大。 ↩

  3. 這些是短間隔測試,介於 0–1 個指令之間。ShellBench 也測試 18 個指令的永續性。 ↩

  4. 兩個提示模型各得到三個範例。它們預測整個指令輸出,而非單一螢幕更新,故不是完全相同的任務。 ↩

  5. 在 4090 上,推測性解碼帶來的速度提升遠大於我的 M4。我的猜測是瓶頸在計算與記憶體頻寬之間移動,但我尚未進行充分調查。 ↩

  6. 填充至 384 以迎合 GPU。↩