世界を動かす技術を、日本語で。

Flash-MoE: ラップトップでの397Bパラメータモデルの実行

概要

  • Flash-MoE は3970億パラメータのMixture-of-Expertsモデルを MacBook Pro で高速動作させる技術
  • C/Metal のみで実装、フレームワークやPython未使用
  • SSDから専門家重みを ストリーミング し、4.4トークン/秒を達成
  • 生産品質の出力 とツールコールを実現
  • OSキャッシュ 活用によるシンプルかつ効率的なパイプライン設計

Flash-MoE: 397Bパラメータモデルをラップトップで実行

  • Qwen3.5-397B-A17B (3970億パラメータMixture-of-Expertsモデル)を MacBook Pro (48GB RAM)で動作
  • 4.4トークン/秒 以上の生成速度、 ツールコール対応 の高品質出力
  • モデル全体(209GB)は SSDからストリーミング、Metalカスタムパイプラインで処理
  • C/Objective-C/Metal のみで構築、Pythonや機械学習フレームワークは不使用

実験結果・構成

| 設定 | tok/s | 品質 | 備考 | |----------------------------|-------|-----------|----------------------------------------------| | 4bitエキスパート + FMA | 4.36 | 優秀 | 現行ベスト、209GB、ツールコール可 | | 4bitエキスパート | 3.90 | 優秀 | FMA最適化前 | | 2bitエキスパート | 5.74 | 良い* | 120GB、JSON/ツールコール不可 | | 2bitピーク | 7.05 | 良い* | キャッシュバースト、ツール用途不可 | | *2bitはJSON出力に不具合発生、4bitが本番推奨 |

ハードウェア構成

  • MacBook Pro (Apple M3 Max, 16-core CPU, 40-core GPU, 48GB unified memory)
  • SSD: 1TB, 17.5GB/sシーケンシャルリード
  • macOS: 26.2 (Darwin 25.2.0)

モデルアーキテクチャ

  • 60層トランスフォーマー: 45 GatedDeltaNet(線形アテンション)+ 15標準フルアテンション
  • 各層 512エキスパート、トークン毎にK=4のみ活性化(+共有エキスパート)
  • 隠れ次元4096

主要技術

  • SSDエキスパートストリーミング

    • エキスパート重み(209GB/4bit)を NVMe SSD から 必要分のみ並列読み込み
    • K=4のみロード(1層あたり約6.75MB)、OSページキャッシュに完全依存
    • Apple "LLM in a Flash" 論文に着想
  • FMA最適化デクオンカーネル

    • 4bitデクオン+行列積を FMA(積和演算) で1命令化、12%高速化
  • Metal手書きカーネル

    • 4bit/2bitデクオン行列積、SwiGLU活性化、RMS正規化、GPUアテンション、RoPE、MoE合成等を GPUで融合処理
  • Deferred GPUエキスパート計算

    • エキスパート前向き計算を 非同期送信、CPUは次層準備と並行
    • 合成・正規化もGPUで完結し、次層アテンションへ直接渡す
  • Accelerate BLASによる線形アテンション

    • GatedDeltaNetの状態更新を cblas_sscal/sgemv/sger で64%高速化
  • OSキャッシュ信頼設計

    • カスタムキャッシュ不要、 OSページキャッシュ(約35GB) に一任
    • 独自キャッシュは GPUメモリ圧迫やオーバーヘッドで逆効果

パイプライン概要(1層平均4.28ms/4bit)

  • CMD3(prev)CMD1 (アテンション投影+デルタネット, 1.22ms GPU)
    • → CPU: 結果フラッシュ(0.01ms)
    • CMD2 (o_proj+正規化+ルーティング+共有, 0.55ms GPU)
    • → CPU: softmax/topKルーティング(0.003ms)
    • → I/O: K=4エキスパート並列pread(2.41ms SSD)
    • CMD3 (エキスパート前向き+合成+正規化, 0.04ms, Deferred)

Apple Siliconにおけるメモリ制約

  • SSD DMAとGPU計算は同一メモリコントローラ を共有し、並列化は逆効果
  • GPUのデクオンカーネルは帯域飽和 (418GiB/s)
  • SSD DMAの背景動作でも GPUレイテンシ急増、直列パイプラインが最適

クイックスタート

  • cd metal_infer && make
  • 4bit推論: ./infer --prompt "Explain quantum computing" --tokens 100
  • 2bit推論(高速/ツール不可): ./infer --prompt "Explain quantum computing" --tokens 100 --2bit
  • インタラクティブチャット: ./chat
  • 層ごとのタイミング計測: ./infer --prompt "Hello" --tokens 20 --timing

プロジェクト構成

  • metal_infer/
    • infer.m:推論エンジン本体(約7000行)
    • shaders.metal:Metalカーネル(約1200行)
    • chat.m:チャットTUI/ツールコール
    • tokenizer.h:C BPEトークナイザ(449行)
    • main.m:MoEベンチマーク
    • Makefile:ビルド
    • extract_weights.py:重み抽出
    • repack_experts_2bit.py:2bit化
    • train_predictor.py:ルーティング分析
    • model_weights.bin:非エキスパート重み(5.5GB)
    • model_weights.json:テンソルマニフェスト
    • vocab.bin:語彙
    • tokenizer.bin:BPEトークナイザ
    • repack_experts.py:4bitパッキング
    • progress.py:可視化
    • results.tsv:実験ログ

試行錯誤と最適化

  • 維持した手法

    • FMAデクオンカーネル:トークン生成速度+12%
    • OSページキャッシュ信頼:独自キャッシュより38%高速
    • GPU合成+正規化融合:CPUラウンドトリップ排除
    • BLASデルタネット:アテンション64%高速化
    • Deferred CMD3:GPU/CPU重複
    • 小型Cトークナイザ:起動20倍高速化
    • 2bit用F_NOCACHE:ページスラッシング回避で+3%
    • GPU融合アテンション(RoPE):フルアテンション層+2%
  • 破棄した手法(抜粋)

    • LZ4圧縮:デコードオーバーヘッドで-13%
    • F_RDADVISEプリフェッチ:SSD DMAでGPU-73%
    • 時系列予測ルーティング:SSD帯域浪費で-18%
    • MLPルーティング予測:精度31%、時系列未満
    • GPU LUTデクオン:間接参照で-2%
    • プライベートバッファ圧縮:パイプライン-20%
    • mmapエキスパートファイル:ページフォルトで5倍遅延
    • 早期投機ルーティング:キャッシュ汚染で-38%
    • その他:NVMeの7MB粒度、dispatch_io/dispatch_dataのオーバーヘッド等

セーフティ設計

  • 開発端末での安全性重視
    • 非エキスパート重みは 5.5GBのみmmap (読み取り専用)
    • Metalバッファ約200MB、合計6GB以下
    • 残り42GBは OS+ページキャッシュ 専用
    • OOMリスクなし、エキスパート重みはSSDからオンデマンドストリーム
    • 独自キャッシュなし、OS信頼方針

この技術により、 巨大言語モデル省リソース・高効率推論 が個人端末でも現実的になりつつある。

Hackerたちの意見

/r/localllamaのディスカッション: https://old.reddit.com/r/LocalLLaMA/comments/1rxmmu5/running...

すごいね!LinuxでもSSDの代わりにシステムメモリを使う似たような方法があるのかな?もしかしたら、重みのROMみたいなのが復活する可能性もあるかも?

まさにその通り!こういうのがあれば、3090が2枚とかの控えめなGPUとたくさんのRAMで、個人のラボでも大きな推論がもっと実用的になるかも。

システムメモリへのエキスパートのロードは、ほとんどのローカルAIフレームワークでサポートされてるよ。でも、デコード部分をGPUで動かしてもあまりメリットはないかな。デコードは計算制限がないし、CPUとGPUの間の転送にはオーバーヘッドがあるからね。モデルの共有部分を加速するためにGPUを使うのがベストだと思う。

もちろん!人気のエンジンは、これを行うための広範なサポートがあって、どの重みがどこに行くかを正確に制御できるよ(llama.cpp: https://github.com/ggml-org/llama.cpp/blob/master/tools/cli/... , vllm: https://docs.vllm.ai/en/stable/configuration/engine_args/#of... , sglang(これは試してないけど): https://docs.sglang.io/advanced_features/server_arguments.ht...)。MoEモデルでも、比較的小さな部分の重みを移動させる必要があるけど、帯域幅の制約はかなりあるよ。

システムメモリとCPUを使って、GPUメモリに収まらないレイヤーを処理するのは、一般的なツールで既にサポートされてるよ。ミクスチャーオブエキスパートモデルには使えるけど、モデルがGPUを超えてシステムRAMに移ると、パフォーマンスが急激に落ちるんだ。ディスクからモデルを毎回取得しなきゃならない場合も、また別のパフォーマンスの崖があるね。

GitHubのページによると、単純なmmapアプローチはページごとのオーバーヘッドでボトルネックになってるみたい。これって、明示的な「巨大」ページを設定することで改善できるのかな?(「ネイティブ」ページサイズが16kの場合はCONT PTE機能を使って2M、PMDレベルのブロックマッピングで32M、CONT PMD機能を使って1Gとか。)macOSはこれをそのままサポートしてるの?それとも、シンプルなmmapを使ってposix_fadviseみたいのでデータのプリフェッチを設定するって手もあるかも。

これは消費者デバイスでQwen 3.5 397Bを動かす唯一の方法じゃないからね。128Gデバイス向けに優れた約2.5 BPWのクォンタイズがあるから、実用的だよ。M1 Ultraで256kコンテキストの余裕を持って実行したら、約20 t/sの成功を収めた。ここにいくつかのlm-evaluation-harnessの結果を載せておくね:mmlu: 87.86% gpqa diamond: 82.32% gsm8k: 86.43% ifeval: 75.90% 体験の詳細はここに: - https://huggingface.co/ubergarm/Qwen3.5-397B-A17B-GGUF/discu... - https://huggingface.co/ubergarm/Qwen3.5-397B-A17B-GGUF/discu... - https://gist.github.com/simonw/67c754bbc0bc609a6caedee16fef8... オフライン推論用に持っておくと素晴らしいモデルだよ。

このリンクの方法は、もう2ビットの量子化を使ってるよ。さらに、トークンごとのエキスパート数を10から4に減らしてるから、品質がまた落ちてる。私の経験では、2ビットの量子化は短いプロンプトには意味のある出力を生成できるけど、長いセッションには役に立たないんだ。このプロジェクトは、モデルから有用なJSONを得ることすらできなかった。引用符のための正しいトークンを生成できないからね。> *2ビット量子化はJSON出力で「\name\」を生成するから、「"name"」にならず、ツール呼び出しが信頼できなくなる。

消費電力って何?もしかしたら https://www.coconut-flavour.com/coconutbattery/ で推定できるかも?

最近のトークン/秒はどれくらい?もっとコンテキストを使うと実際にうまくいく?ところで、最後にあなたのユーザー名を見たのはずいぶん前だね。あなたはNeovimを立ち上げた人だよね!すごい成功だね。確実に、私がちょっと関わったKickstarter/Bountysourceの中で一番の結果だったよ。毎日使ってる。

ありがとう、個人の自動化にクレジットを使いすぎてたよ。

ただのM1ウルトラ1台だけ?

詳細を読むと、彼は2ビットの量子化を使って、トークンごとのエキスパートの数を10から4に減らして5トークン/秒を達成してるみたい。面白いコンセプトだけど、通常の397Bモデルの品質やパフォーマンスには程遠いね。エキスパートの数を減らすのは特に誤解を招く。これは興味深い作業だけど、LLMに極端な手段を適用すると品質がひどく落ちる。彼は品質の損失は無視できると言ってるけど、私の経験では2ビットの量子化は実際の作業には全く役に立たない。プロンプトに反応させることはできるけど、知能を失ってぐるぐる回ってしまう。彼は5-6トークン/秒も示してるけど、大きなモデルを限られたハードウェアで動かすには印象的だけど、すごく遅い。モデルの能力がひどく低下して、出力も非常に遅いから、397Bの結果は技術的に動くことを証明する試みとして考えるべきで、良い結果が得られる証拠ではないと思う。彼は自分の変更に関する明らかな問題も言及してるしね。 > *2ビットの量子化はJSON出力で「name」の代わりに\name\を生成するから、ツール呼び出しが信頼できなくなる。だから、何かをするには全く役に立たない。彼はもっと小さなモデルや量子化を少なくして、実際に役に立つ出力を得ることを試みることができたけど、そうすると印象的には見えないだろうね。正直言って、こういうAIコード(リンクで認められてる)やAIが書いた論文を読むのはちょっと疲れてきた。印象的な見出しを得るためのトリックを使うよりも、役に立つモデルを動かすためにこの作業を適用してほしかったな。

2ビットでの品質劣化は本当に問題だよ。実際の作業タスクでは、私の経験上、4ビットでよく調整された30Bモデルが、2ビットの70B+モデルよりも通常は優れてる。さらにエキスパートを減らすことで、状況が悪化する - 実質的にかなり異なるモデルを動かしてることになるからね。それでも、消費者ハードウェアがどこまで挑戦できるかを見るのは興味深いよ。結果が商業用に準備されてなくてもね。

要するに、Danの仕事を活用してもっと実用的にしようと試みたよ: https://github.com/matt-k-wong/mlx-flash 2ビットの量子化はモデルを劣化させるけど、やっぱりすごい!いつかはインテリジェントな2ビット量子化ができるようになるのかな…私のバージョンは、4ビット量子化、ハイブリッドストリーミング(ディスク + RAM)、任意のモデル互換性をサポートしてる。Mamba2でテスト済みで、LM Studioとの統合のためのフレームワークも整えてる。この作業を活用した(Danveloperに感謝)けど、もっと実用的なモデルや量子化で動かすための真っ最中だよ。フラッシュストリーミングはまだ使ってるけど、どれだけRAMを使うか選べるコントロールノブが付いてる。最も極端なケースでは、可能な限り少ないRAMを使うけど、すごく遅い。でも、バランスの取れたケースでは、少しRAMを使うとかなり速くなる。インテリジェンス密度の高いNemotron 3 Nano 30BとNemotron Cascade 2 30Bモデルを中心に設計してて、低スペックの16GBマシンでも動かせるけど、大きなマシンでは任意の大きさのモデルも動かせるように設計してる(非常に低スペック向けだけど、高スペックも対応可能)。

LLMを使うのに、最初から全てを揮発性メモリに収めるしかないって、ちょっと変じゃない?映画を作る時は、コンピュータが光の反射を計算するのを、何時間も、時には何日も待つのに。なんでAIでも同じことをしないの?大きなモデルに大きな質問を投げて、宇宙の答えを明日もらうっていうのはどう?

研究みたいな長時間かかるタスクには確かに使い道があるけど、一般的な使い方には常に監視とインタラクションが必要すぎるよね。

ターンアラウンドタイムを気にしないなら、それも可能だね。でもほとんどのLLMの使い方は、ワークフローを加速させることだから。もし返事をもらうのに一晩待って、しかも間違った方向に進んでたり、意図を誤解されたり、プロンプトに重要な情報が抜けてたら、また最初からやり直しだよね。私はLLMにコードを書かせることはしないけど、コードベースの探索やレビュー、試作はたくさんやってる。毎日何百回、下手したら何千回もLLMとやり取りしてるから、もし10倍や100倍も待たなきゃいけなかったら、全然役に立たないよ。遅いLLMを無視して、自分でやった方がずっと生産的だと思う。

Metal Compute Shaders — 手書きのMetalカーネル これって、GPTが手書きしたの? ;)

彼は、それがAIによって書かれたってことをはっきり言ってるね。

これ、すごいね、ダン!