70Bのモデルを、4GBのGPU1枚で動かせます。しかも量子化なしで。
GitHubスター3万超えのリポジトリがそう名乗っているんですが、最初の反応は「いや、数字が合わないでしょ」でした。
重みを1ビットも削らずに35分の1のメモリへ収める理屈が本当にあるのか、ソースを開いて追いかけた結果を書きます。
量子化も蒸留も枝刈りもしないという主張から確認する
Llama 3.3 70Bのパラメータは約705億あります。
標準のbf16は1パラメータ2バイトなので、重みを全部並べると約141GB。
これを4GBに収めるということは、約35分の1に圧縮している計算になります。
一方でAirLLMがREADMEで名乗っているのは、量子化(quantization)も蒸留(distillation)も枝刈り(pruning)も使わない、という条件です。
重みそのものには一切手を触れない。
触れないまま35分の1というのは、素直に読むと物理的に無理な話です。
ネタ枠のリポジトリなら笑って閉じるところなんですが、スター30,152、フォーク3,207、直近のpushが2026年8月8日、ライセンスはApache-2.0。
現役で動いているプロジェクトです。
数字が合わないのに3万人が星を付けている。この時点でREADMEを読むだけでは判断できないと踏んで、実装を開きました。
1層だけGPUに置くという説明の裏でソースは何をしているか
公式の説明は「一度にGPUへ載せるのは1層だけ。だから必要なVRAMはモデル全体のサイズではなく1層のサイズで決まる」というものです。
日本語の解説記事もだいたいここまでで止まっています。
コードを開くと、その1層の出し入れがかなり具体的に見えてきます。
まずモデルを落とした直後に find_or_create_local_splitted_path() が走って、チェックポイントを層ごとのsafetensorsファイルに割り直します。
元の巨大なファイルのままでは1層だけ読むことができないので、先に物理的に切っておくわけです。
そのうえで、各層に2つのフックを刺します。
for idx in self._streamed_indices:
module = self.layers[idx]
module._airllm_idx = idx
module.register_forward_pre_hook(self._pre_hook)
module.register_forward_hook(self._post_hook)register_forward_pre_hook は層の計算が始まる直前、register_forward_hook は終わった直後に呼ばれるPyTorchの仕組みです。
前者で _load_streamed_layer(idx) を叩いて重みをGPUに載せ、後者で降ろす。
面白いのが降ろし方で、del でもなければファイルを消すわけでもありません。
for param_name in getattr(module, '_airllm_moved', []):
set_module_tensor_to_device(self.model, param_name, 'meta')
module._airllm_moved = []meta はPyTorchの特殊なデバイスで、テンソルの形とdtypeだけ持っていて実体のメモリを持ちません。
モデルの構造は80層すべて組み上がったまま、中身だけが空っぽになっている状態です。
「1層だけ載せて捨てる」の正体は、削除ではなくmetaデバイスへの付け替えでした。
で、肝心の1層がどれくらいのサイズなのか。
Llama 3.3 70Bの構成から計算できます。
bf16で約1.7GBです。
ここで「じゃあ4GBじゃなくて2GBあれば足りるのでは」という疑問が出るはずですが、答えもソースにあります。
self._executor = ThreadPoolExecutor(max_workers=1) if self.prefetching else Noneワーカー1本のスレッドプールを持っていて、いま計算している層の裏で次の層を先読みしています。
prefetching はデフォルトでオンです。
つまり実行中のGPU上には、計算中の層1.7GBと、読み込み中の次の層1.7GBが同時にいる。
合わせて3.4GB。残りが活性化とKVキャッシュに回って、公称の4GBにきれいに収まります。
4GBは概算の宣伝文句ではなく、1層のサイズ×2 + αという設計から出る必然の数字でした。
量子化との違いは重みを削るか置き場所を変えるかにある
同じ「省メモリ」でも、量子化とやっていることが根本的に違います。
量子化はbf16の重みを4ビット整数などに丸めて、情報を捨てる代わりに容量を稼ぎます。
AirLLMは重みをbf16のまま1バイトも変えず、ディスクとGPUの間を往復させることでVRAMの瞬間最大値だけを下げている。
出力の質という意味では、元のモデルとまったく同じものが返ってきます。
そのぶん、支払いは全部「時間」のほうに付け替えられている。
ちなみにAirLLM側にもオプションで4bitと8bitのブロック単位圧縮が用意されていて、デフォルトは無効です。
必要なら両方を重ねられます。
ボトルネックはGPUではなくディスクとRAMにある
この設計で一番きついのは、トークンを1個吐くたびに80層すべてを読み直す点です。
入力を読み終わったあとの生成フェーズでは、1トークンごとに141GB分の重みがディスクからGPUへ流れます。
Gen4のNVMeで実効5GB/s出ると仮定すると、141GB ÷ 5GB/s で約28秒。
1トークンにです。
100トークン返させたら約47分かかる計算になります。
念のため書いておくと、これは僕が実測した値ではなく設計から出る見積もりです。
ただし桁を間違えるような話ではなく、この構造である以上「重みの総バイト数 ÷ 読み込み帯域」からは逃げられません。
そして、ここから効いてくるポイントが2点あります。
- RAMが効く。141GBを載せられるだけのメモリがあればOSのページキャッシュに乗って、2回目以降の読み込みがディスクではなくRAM速度になります。GPUのVRAMより本体のRAM容量のほうが体感を左右します
- 見るべき指標が変わる。
nvidia-smiのGPU使用率が数パーセントで張り付いていても異常ではなく、それがディスク待ちの証拠です。詰まっているかどうかはiostatの読み込み帯域で判断します
READMEにある「圧縮で最大3倍速」も、この構造で読むと筋が通ります。
4bitに圧縮すると読むバイト数が141GBから約35GBまで落ちる。
速くなっているのは計算ではなく転送量です。
ただしこれはAirLLM同士の比較であって、モデルが素直にVRAMへ載る環境より速くなるという話ではありません。
常時稼働のAPIには無理でもバッチとオフラインなら戦える
遅いのは確定として、じゃあ使い道がないのかというと、そうでもありません。
この設計には効率の良い形が明確にあります。
入力を読む処理は、プロンプトが何トークンあっても各層を1回通すだけで済みます。
一方で出力は、1トークンごとに全層を読み直す。
つまり長い入力を読ませて短く答えさせる形が、圧倒的に効率がいい。
具体的には、分類、情報抽出、スコアリング、可否判定あたりです。
1万トークンのドキュメントを読ませて「該当する」「しない」だけ返させるなら、重みを読むのは数回で終わります。
逆にコードを生成させるような長文出力は、この構造では最悪の使い方になります。
実務で成立しそうな線を引くと、こうなります。
- 向く: 夜間バッチでの一括判定、オフラインでの評価データ作成、外部APIに出せない機密データのローカル処理
- 向かない: チャットボット、コード生成、レスポンスタイムが要求される常時稼働API
導入自体は pip install airllm の1行で、追加のビルドは要りません。
Apple Siliconにも対応していて、その場合は mlx と torch を入れれば同じコードが動きます。
ライセンスはApache-2.0なので、社内利用でも引っかかりません。
70Bを4GBで動かせるという見出しは、実際には「70Bを4GBのVRAMと大量の時間で動かせる」でした。
ただ、待ち時間が問題にならない使い方は思っているより多い。
手元のGPUが8GBや12GBで諦めていたモデルサイズがあるなら、常時稼働ではなく夜間バッチの枠で一度試してみる価値はあります。
💬 コメント
ログイン か 会員登録 するとコメントできます