前作 1LM-Blackwell は「1文字ずつ」予測する ミニ言語モデルでした。このリポジトリでは トークナイザをサブワード(SentencePiece)に変え、 コーパスを 441万文字から 948万文字へ増やして、日本語として読める返答が返ってくる ところまで鍛えます。モデル構造は1つも変えていません。変えたのはトークナイザとデータだけです。
- フレームワーク: PyTorch 2.13 + CUDA 13.0(Blackwell / sm_120 対応 wheel)
- モデル: ミニGPT / 6層 / 384次元 / 6ヘッド / 文脈 256トークン / 13.81M パラメータ
- トークナイザ: SentencePiece unigram / 語彙 8,000 /
byte_fallbackあり - データ: 日本語会話 4データセット(すべて Apache-2.0)を整形した 58,092 会話 / 948万文字
- 学習時間: RTX 5090 で 78.5 秒(3,600ステップ / 最終 val loss 3.6118)
- 評価: bits/char 2.586 / 主題保持率 0.733 / 破綻率 0.000
Apple Silicon / MLX 版 2LM-MLX の移植です。 Mac の最終 val loss 3.6029 に対して 3.6118(差 +0.0089)、bits/char は 2.584 対 2.586 まで 一致しました。M1 Max の 1,620.2 秒に対して 78.5 秒なので 20.65 倍です。
| 1LM-Blackwell | 2LM-Blackwell | |
|---|---|---|
| トークナイザ | 文字レベル / 語彙 2,077 | サブワード / 語彙 8,000 |
| コーパス | oasst1 のみ / 441万文字 | 4データセット / 948万文字 |
| パラメータ | 11.53M | 13.81M(埋め込みが増えたぶん) |
| 1トークンあたり | 1 文字 | 2.00 文字(マーカーを除いた実測) |
| 文脈 256 で入る量 | 256 文字 | 約 513 文字 |
| 評価 | なし | 4指標を固定条件で採点 |
| モデル構造・学習設定 | — | 完全に同じ |
「同じ 256 トークンで2倍の文章が入る」のがサブワード化の効きどころです。 学習設定を1つも変えていないので、結果の差はトークナイザとデータだけに帰属できます。
| インターフェース | コマンド |
|---|---|
| 環境診断(最初にこれ) | python check_env.py |
| CLI チャット | python src\chat_cli.py |
| Web GUI(Liquid Glass 風) | python server.py --open |
| 単発生成 | python src\generate.py --prompt "こんにちは" |
| 固定評価セットで採点 | python eval\run.py --tag v1 --compare-mac |
| 語彙サイズの実測 | python tools\pick_vocab.py |
必要なもの: Windows 10/11 / NVIDIA GPU / uv。
前作と同じ仮想環境を使えます(sentencepiece だけ増えます)。
# 1. 環境変数(ユーザースコープ。設定後にターミナルを開き直す)
setx PYTHONUTF8 1
setx PYTHONIOENCODING utf-8
setx HF_HOME E:\hf_cache # 空きの大きいドライブへ
setx TORCHINDUCTOR_CACHE_DIR E:\ti_cache
setx CUDA_MODULE_LOADING LAZY
# 2. 仮想環境(3リポジトリで共用する想定なので親ディレクトリに置く)
uv venv C:\LLM\.venv-blackwell --python 3.13
C:\LLM\.venv-blackwell\Scripts\Activate.ps1
# 3. 依存(torch は PyPI ではなく NVIDIA のインデックスから)
uv pip install -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cu130
uv pip install -r requirements-dev.txt # 作図ツールを使う場合のみ
# 4. 診断
python check_env.pycheck_env.py が 13 項目すべてを通してから先に進んでください。
torch.cuda.is_available() が True でも、wheel の CUDA が古いと最初の行列積で
no kernel image is available for execution on the device になります。
Windows 固有の「エラーを出さずに 3 倍遅くなる」共有GPUメモリ対策
(CUDA - Sysmem Fallback Policy / runtime.configure() / MemoryGuard)については、
前作 1LM-Blackwell の README に
実測付きで書いてあります。runtime.py はそのまま持ち込んでいます。
python data\prepare.py --exclude eval\holdout.txt--exclude は必須です。eval/holdout.txt は採点に使う固定検証セットなので、
これを訓練データに残すと「暗記したモデル」を高く採点してしまいます。
実測値(2026-08 時点)。
| 項目 | 実測 | Mac 版 |
|---|---|---|
| 取得した会話 | 341,694 件 | — |
| フィルタ後 | 58,092 会話 | — |
| 文字数 | 9,484,834 文字 | 949万文字 |
| 文字種 | 2,573 | 2,574 |
差は 0.1% 未満なので、Hugging Face 側のデータは動いていないと判断できます。
この照合を学習より先にやってください。 ここがずれていると
val loss を比べても意味がありません。data/corpus.meta.json に自動で残ります。
使ったデータセット(python data\prepare.py --list-sources で確認できます)。
| キー | リポジトリ | ライセンス |
|---|---|---|
| oasst1 | kunishou/oasst1-89k-ja |
Apache-2.0 |
| oasst2 | llm-jp/oasst2-33k-ja |
Apache-2.0 |
| magpie | llm-jp/magpie-sft-v1.0 |
Apache-2.0 |
| tanuki | Aratako/Magpie-Tanuki-8B-97k |
Apache-2.0 |
ShareAlike(継承)条件を持つデータは意図的に入れていません。 学習した重みを 配布する前提なので、継承条件が重みに及ぶかが不明なデータは避けています。
python tools\pick_vocab.py # 語彙サイズを実測して選ぶ948万文字のコーパスを実際に符号化して測った表です(外挿していません)。
| 語彙 | 文字/トークン | 総トークン数 | 埋め込み | 1語彙あたり出現回数 |
|---|---|---|---|---|
| 4,000 | 1.944 | 5,746,096 | 1.54M | 1,437 |
| 8,000 | 2.358 | 4,736,014 | 3.07M | 592 |
| 12,000 | 2.558 | 4,367,028 | 4.61M | 364 |
| 16,000 | 2.691 | 4,151,039 | 6.14M | 259 |
| 24,000 | 2.869 | 3,893,802 | 9.22M | 162 |
| 32,000 | 2.986 | 3,740,009 | 12.29M | 117 |
Mac 版は 24,000 まででしたが、5090 なら埋め込みが増えても VRAM は余るので 32,000 まで伸ばしました。結論は変わりませんでした。
- 圧縮率の伸びは 24,000 → 32,000 で +0.117 だけ(4,000 → 8,000 は +0.414)
- 埋め込みは +3.07M。増分だけで前作のモデル1個ぶんに近い
- 1語彙あたりの平均出現回数は 117 回まで痩せる(4,000 のときの 12分の1)
足りないのは VRAM ではなくデータです。 語彙を増やせば1トークンあたりの 情報量は増えますが、同じコーパスでは1語彙あたりの学習回数が反比例で減ります。 GPU が強くなっても、この計算だけは変わりません。
注意: この表の「文字/トークン」はマーカー(
<|user|>など)を含む ファイル長で割った値です。マーカーを除くと 8,000 で 2.003 文字/トークン に なります(src/train.pyの起動時に表示されます)。Mac 版の資料が言う「約2.36文字」は前者です。 どちらを使うか決めて、混ぜないでください。
学習時のトークナイザは3通りに切り替えられます。
python src\train.py # 既定: data/tokenizer/tokenizer.model を流用
python src\train.py --tokenizer-model "" # コーパスから語彙8,000を学習し直す
python src\train.py --vocab-size 0 # 文字レベル(前作と同じ方式。対照実験用)既定が「流用」なのは意図的です。 語彙を学習し直すとトークンIDの対応が変わり、 同梱のチェックポイントが読めなくなり、Mac 版との loss 比較も成立しません。
python src\train.py # 3,600ステップ(RTX 5090 で約80秒)
python src\train.py --equivalence-run # Mac 版と同条件で回して差を出す
python src\train.py --tokens 30_000_000 # トークン予算で止める
python src\train.py --resume # 中断したところから再開
python src\train.py --init-from checkpoints\final # 追加学習(次作で使う)実測値(Windows 11 / RTX 5090 / torch 2.13.0+cu130)。
| 項目 | 値 |
|---|---|
| 学習時間 | 78.5 秒 / 3,600ステップ |
| 消費トークン | 59.0M(4.69M トークンを約12.6周) |
| 最終 train loss | 3.4325(直近50ステップ平均) |
| 最終 val loss | 3.6118 |
| 定常スループット | 1,027k tok/s |
| 専用VRAM ピーク | 1.88 GB |
| 共有GPUメモリ増分ピーク | +0.01 GB |
| 項目 | M1 Max (MLX) | RTX 5090 (PyTorch) |
|---|---|---|
| 3,600ステップ | 1,620.2 秒 | 77.9 秒(20.80 倍) |
| 最終 train loss | 3.4205 | 3.4325(差 +0.0120) |
| 最終 val loss | 3.6029 | 3.6118(差 +0.0089) |
python tools\compare_mac.py --mac-csv E:\ref\2LM-MLX\runs\loss.csv秒数が上の表(78.5 秒)と 0.6 秒違うのは、図が runs/loss.csv の最終ステップ時点、
本文が train.py の合計値(最後のサンプル生成を含む)だからです。
前作は 31.4 倍でしたが今回は 20.80 倍です。Windows 側が遅くなったのではありません。 5090 側は語彙が 3.85 倍になって1ステップ 14.4ms → 16.0ms(11% 悪化)に留まっており、 比が縮んだのは主に Mac 側の所要時間が 1,907 秒 → 1,620 秒と短いためです。 詳しくは docs/notes.md の第5節に書きました。
python eval\run.py --tag v1 --compare-macサンプリング条件は Mac 版と同一に固定しています (temperature 0.8 / top_k 40 / repetition_penalty 1.15 / seed 777)。
| 指標 | Windows (RTX 5090) | Mac (M1 Max) | 見方 |
|---|---|---|---|
| bits/char | 2.586 | 2.584 | 低いほど良い。乱数を使わないので直接比較できる |
| 主題保持率 | 0.733 | 0.733 | 高いほど良い。質問の主題語が返答に出た割合 |
| 反復率 | 0.150 | 0.100 | 低いほど良い。設問1問で 0.05 動く |
| 破綻率 | 0.000 | 0.000 | 低いほど良い |
| 平均返答長 | 60.2 | 64.1 | — |
反復率の +0.05 を「悪化した」と読んではいけません。 設問は20問しかないので 1問の当たり外れで 0.05 動きます。同じシードを指定しても MLX と PyTorch では 乱数生成器が別物なので、同じ文章は出ません。乱数に依存する指標を、 乱数生成器が違う環境間で比べても意味がないというのがここでの教訓です。
比較の土台に使えるのは bits/char です。分母を文字数にしてあるので、
文字レベル(前作)とサブワード(今回)を並べられます。
| 語彙 | bits/char | |
|---|---|---|
| Mac 対照実験A(405万文字・文字レベル) | 2,578 | 3.282 |
| Mac 対照実験B(949万文字・文字レベル) | 2,578 | 2.809 |
| 本リポジトリ(948万文字・サブワード) | 8,000 | 2.586 |
nats/token のまま比べてはいけません。語彙が違うと単位が違います
(ランダム予測の損失が ln(2578)=7.85 と ln(8000)=8.99 で別物)。
python src\chat_cli.py生成速度は CLI と Web GUI の実測で 411〜1,015 文字/秒(Mac 版は 320〜800 文字/秒)。
1回目の生成だけは CUDA カーネルの初期化を含むので遅く出ます(上の画像の 20 文字/秒)。
KVキャッシュは実装していないので、文脈が伸びるほど遅くなります。
--max-new-tokens 200 は「200文字」ではなく 約400文字ぶんです。
python server.py --openサブワードでは1トークンが複数バイトに割れることがあるため、
ストリーミングは1トークンずつ復号してはいけません。累積したトークン列を
毎回まとめて復号し、末尾が U+FFFD のときは次を待ちます
(src/generate.py の decode_incrementally)。
これを忘れると絵文字や一部の漢字が ???? に化けます。
python tools\equivalence.py # 段階0〜2
python src\train.py --equivalence-run # 段階3| 段 | 見るもの | 判定基準 | 実測 |
|---|---|---|---|
| 0 | Mac のチェックポイントを strict=True で読む |
64テンソルの形が一致 | 通過 |
| 1 | fp32 の logits を NumPy の独立実装と比較 | max|diff| < 1e-4 |
1.98e-05 |
| 1c | トークナイザ(マーカー / 往復 / byte_fallback) | 3つすべて | 通過 |
| 2 | step 0 の loss | ln(8000) + 0.5 = 9.487 ± 0.1 |
9.474 |
| 3 | 3,600ステップ後の val loss | 3.6029 ± 0.05 |
3.6118 |
段階 1c は前作には無かった段です。今回は「同じ文章が同じIDに割れること」が 比較の土台なので、ここを確かめずに先へ進むと、loss の水準そのものが変わって しまい原因を切り分けられません。
移植でずれる場所(埋め込み初期化 / AdamW のバイアス補正 / erf GELU / bias の非対称 /
step 0 の loss が ln(V)+0.5 になる理由)は前作と同じです。
src/model.py と src/optim.py のコメントに全部書いてあります。
13.81M パラメータに対して、経験則(Chinchilla の D ≈ 20N)が求めるトークン数は 2億7,600万。手元にあるのは 469万トークン、つまり 1.7% です。
| 値 | |
|---|---|
| 非埋め込みパラメータ | 10.64M |
| 全パラメータ | 13.81M |
| 経験則が求めるトークン数 | 276M |
| 実際の学習トークン | 4.69M(1.7%) |
| 学習で消費したトークン | 59.0M(同じデータを12.6周) |
文字数で数えると 948万文字あるので「4%」と言えてしまいますが、 モデルが何回の予測を経験したかで数えるなら 1.7% が正しい見方です。 「データが2桁足りない」——これがこのモデルの限界の正体で、 5090 に替えても解決しません。GPU が速くなって解決するのは待ち時間だけです。
次作 2LM-Blackwell-GAL では、 足りないデータを自分で作る側に回ります。
flowchart LR
A["文字列<br/>おすすめの本は"] --> B["トークナイザ<br/>SentencePiece unigram<br/>1トークン≒2文字"]
B --> C[埋め込み<br/>+ 位置埋め込み]
C --> D["Transformer Block x6<br/>因果マスク付き自己注意 + MLP"]
D --> E["次の1トークンの確率<br/>語彙8,000次元"]
E --> F[サンプリング<br/>temperature / top_k]
F --> A
| ファイル | 役割 |
|---|---|
| check_env.py | 環境診断13項目。最初に通す |
| runtime.py | VRAM / 共有GPUメモリの監視、バッチ計画、MemoryGuard |
| src/tokenizer.py | 文字レベルとサブワードの2種。同じインタフェースで差し替えられる |
| src/model.py | ミニGPT本体。前作から変更なし |
| src/optim.py | MLX と同じ挙動の AdamW(バイアス補正なし) |
| src/train.py | 学習ループ。--init-from で追加学習もできる |
| src/generate.py | サンプリングと、byte_fallback を壊さない逐次復号 |
| src/chat_cli.py | CLIチャット |
| server.py / web/ | FastAPI + SSE / Liquid Glass 風 GUI |
| data/prepare.py | コーパス整形(4データセット対応) |
| eval/run.py | 固定評価セットで4指標を採点 |
| tools/pick_vocab.py | 語彙サイズの実測 |
| tools/equivalence.py | 同値確認(段階0〜2) |
| tools/check_bytefallback.py | 絵文字・珍しい漢字がストリーミングで化けないかの確認 |
| tools/compare_runs.py | 複数の学習ログを重ねて比べる |
前作で書いたもの(cu130 / torch.compile の 260 文字 / 共有GPUメモリ / \r 混入 /
PowerShell の文字化け)に加えて、今回だけのものを挙げます。
- 語彙を学習し直したら同梱モデルが読めなくなった →
--tokenizer-modelを空にすると SentencePiece を再学習します。IDの対応が変わるので、既存の重みとは噛み合いません。data/tokenizer/tokenizer.modelを消さないでください。 - 絵文字や一部の漢字が
????になる → 1トークンずつ復号している。byte_fallbackは未知の文字をバイト列に割るので、まとめて復号する必要があります (🐱は4トークン = 4バイト)。python tools/check_bytefallback.pyで確認できます。 - cosine の分母を前作と同じ 4,300 にした → 今回の Mac 版は 3,600 を走り切っている (ログ最終行の lr が下限 3e-5 まで落ちている)ので、分母は 3,600 です。 前作は時間打ち切りで途中終了していたため 4,300 でした。ログの lr から逆算できます。
Set-Content -Encoding utf8でソースを書き換えた → PowerShell 5.1 のutf8は BOM 付きです。Python は BOM 付きソースを受け付けてしまうのでエラーが出ず、 diff が全行変更になって初めて気付きます。ソースの編集にリダイレクトを使わないこと。- 評価指標が Mac と一致しない →
bits/char以外は乱数に依存します。 MLX と PyTorch では同じシードでも別の文章が出ます。 - 「文字/トークン」が 2.358 と 2.003 の2つある → マーカーを含めるかどうかの違いです。 誤記ではありません。
checkpoints/final/ に学習済みの重みを同梱しています(13.81M パラメータ fp32 で約56MB)。
クローンすればすぐ会話できます。
checkpoints/final/
├── model.safetensors # 重み
├── config.json # モデル構成
├── tokenizer.model # SentencePiece の語彙(再学習しないこと)
└── tokenizer.json # 方式と語彙数
コードは MIT License です(LICENSE)。 Apple Silicon / MLX 版 2LM-MLX の移植で、 原典も同じ作者・同じ MIT License です。
同梱の学習済みモデルは、上記4つの Apache-2.0 データセットから学習しています。 同梱のトークナイザは 2LM-MLX が学習したものをそのまま流用しています(再学習していません)。 前処理で加えた変更点とライセンス全文は NOTICE と licenses/Apache-2.0.txt にあります。
生成される文章は、コーパスの統計から次の1トークンを予測し続けた結果にすぎません。 事実性は一切保証されず、実在の人物や団体について誤った内容を出力することがあります。 出力を公開の場に掲載する場合は、機械生成物である旨を明記してください。








