なぜこの記事を書いたのか
手元のPCでMoEモデルを動かそうとして、llama.cppの --n-gpu-layers を何度も書き換えては速度を測り直す、という時間を筆者は何度も溶かしました。困ったのは、うまくいく設定が機械ごとに違うことです。同じ数字が別のマシンでは最悪だった。何を根拠に決めればいいのかが分からないまま、勘で回していました。
2026年8月17日にarXivへ投稿された FreeToken の論文は、その「勘」を式に落としています。読んでほしいのは結論ではなく判断の形のほうです。エッジ推論の速度は、GPUの強さではなく 2本の帯域の比 で決まる。この見方が正しいなら、私たちがチューニングすべき対象は設定ファイルではなく、自分のマシンの実測値になります。
なお筆者の検証環境は Xeon 1コア・メモリ3GB という貧弱なコンテナです。GPUはありません。だからこそ「ホスト側の帯域をどう測り損なうか」がはっきり見えました。それも書きます。
この記事の対象読者
- ローカルでLLMを動かしたことがあり、VRAMに収まらないモデルで速度が出ずに困っている人
この記事で得られること
- 自分のPCで転送帯域とホスト帯域を実測し、その比からエキスパートのミスを転送とCPU実行にどう振り分けるか、そしてどの推論エンジンを使うべきかを決められるようになります
- 上記の判断に使う閉形式と、それを整数に丸めるときの落とし穴
- LRUエキスパートキャッシュが効く条件と、効かない条件
動画で見たい人向け
YouTube版を公開しました。
この記事で扱わないこと
- MoEの学習・ルータの設計(推論側の話に絞ります)
- FreeTokenのインストール手順の逐一(公式ドキュメントが十分です)
1. FreeTokenは何を解いたのか
このセクションで分かること:FreeTokenが誰の何を対象にしたシステムか。
FreeToken は、2026年8月17日に arXiv へ投稿された論文(arXiv:2608.16157)で提案された、個人所有のマシン向けMoE推論エンジンです。著者11名にはvLLMやSGLangの中心人物が並び、連絡先はバークレーとテキサス大オースティンのアドレスになっています。実装は Apache-2.0 で公開済みで、2026年8月時点でGitHubのスター数は4.3k、Windows/Linux向けのデスクトップアプリも配布されています。
論文の主張は要約すると3行です。
- フロンティア級のオープンウェイトモデルは公開されているが、動かす側の設備がデータセンター前提のままである
- 個人のマシンを「小さなGPU」ではなく「GPU・CPU・ホストメモリ・相互接続をまとめた1つの推論基盤」として扱えば、8GBのノートPCで35Bクラス、32GBのゲーミングPCで284Bクラスが実用速度で動く
- そのために必要なのは新しいハードではなく、実測帯域に合わせて実行を割り振るサービングソフトである
原文(Abstract):"treats a personal machine not as a small GPU"
和訳:個人のマシンを、小さなGPUとしてではなく扱う(原文はこの後、統一された弾性的な推論基盤として扱う、と続きます)
論文が扱うのは推論のサービング層です。モデルそのものは一切変更せず、量子化やエキスパートの間引きによる近似も行いません。出力はビット単位で通常実行と一致します。
2. 前提:MoEは「計算は疎、重みは密」
前節で、FreeTokenが個人マシン向けのMoE推論エンジンだと確認しました。ではなぜMoEだけが特別扱いされるのか。ここを押さえないと、以降の設計がすべて場当たりに見えてしまいます。
このセクションで分かること:MoEがエッジ推論にとって好都合であり、同時に厄介である理由。
この記事では最後まで、相談窓口と倉庫というたとえで通します。
- エキスパート(専門家ネットワーク)= 相談員
- GPUのVRAM = 窓口カウンターの席。数が少ない
- ホストメモリ(RAM)= 別棟の倉庫。相談員は全員ここに常駐している
- PCIe = 倉庫と窓口をつなぐ通路。台車で相談員を運ぶ
- ルータ = 「この案件はこの相談員」と指名する受付係
MoEレイヤは相談員を数百人抱えていますが、1トークンが呼ぶのはそのうち数人だけです。LLMの推論コストを決めるのは呼ばれた人数のほうなので、計算量は劇的に小さくなります。
論文によれば DeepSeek-V4-Flash は43層それぞれで256人の相談員から6人を選び、284Bのうち13Bだけが1トークンの計算に参加します。この13Bという活性パラメータ量は、RTX5090の32GBというVRAM容量に収まる大きさです。
問題はここからです。計算に参加しない271B分の相談員も、いつ指名されるか分からない以上、どこかに待機していなければならない。倉庫は必要なのです。つまりMoEは、計算は疎になるのに、確保すべきメモリは疎にならない。
3. エッジ推論が詰まる3か所
前節で、MoEは計算を軽くする代わりに「全員分の待機場所」を要求すると分かりました。待機場所が窓口の外にある以上、次に問題になるのは誰をいつ運ぶかです。論文はここが3か所で詰まると整理しています。うち1か所目のプリフィルは、転送と再計算という別々の成分を含みます。
このセクションで分かること:これから読む対策が、それぞれどの詰まりに対応しているか。
| 詰まる場所 | 何が起きるか | 論文が挙げる数字 |
|---|---|---|
| プリフィル(最初の一括処理) | 長いプロンプトはほぼ全相談員を指名する。倉庫からの大量搬送が発生 | FP4のDeepSeek-V4-Flashで約140GBの転送 |
| プリフィルの再計算 | エージェントがツール呼び出しのたびに文脈を編集し、状態チェックポイントが無効化される | 数千トークンの再処理 |
| デコード(1トークンずつの生成) | 席にいない相談員をどう処理するかの方針がない | CPU片寄せだと帯域が1桁足りない |
| リソース変動 | ブラウザやゲームがVRAMを奪う。専有できない | 起動のたびに使える量が違う |
とくに厄介なのが2行目です。エージェント用途では、ツール実行と結果の差し込みのたびにプリフィルが走ります。単発のベンチマークではこの負荷が現れないため、単発測定はエージェント実運用の性能を過大評価します。論文もこの点を明示しています。
4. プリフィル側の答え:二重バッファとセマンティックアンカー
前節で、プリフィルは「ほぼ全員を倉庫から運ぶ」処理だと分かりました。全員運ぶこと自体は避けられません。ならば残る手は、運搬時間を計算時間の裏に隠すことだけです。
このセクションで分かること:プリフィルを転送律速まで押し切る2つの仕掛け。
全レイヤ二重バッファリング
FreeTokenは席の一部を2レイヤ分のバッファとして確保し、GPUが第 $l$ レイヤの相談員と話している間に、第 $l+1$ レイヤの相談員全員を台車で運びます。全員運ぶので、そのレイヤのルータがまだ誰を指名するか決めていなくても搬送を開始できるのがポイントです。
論文の実測では、8,192トークンごとのプリフィルが1.19〜1.22秒で完了しています。これは64.4GBのエキスパート重みを52.7GB/sで1回流し切る時間そのもので、つまり計算時間が転送の裏に完全に隠れた状態です。二重バッファを切ると16kトークンで26%スループットが落ちたと報告されています。
セマンティックアンカー
もうひとつが再計算対策です。Qwen3.6やKimi-K3のような再帰層を持つモデルでは、過去の文脈が1つの状態に圧縮されます。この状態は途中から部分的に再利用できないため、チェックポイントを取っておくしかありません。
FreeTokenはチェックポイントを、思考ブロックの終端やツール呼び出しの境界といった特殊トークンの位置に打ちます。理由は明快で、エージェントのフレームワークが文脈を削るとき、削る単位がまさにそこだからです。OpenCodeは古いツール出力をプレースホルダに置き換え、OpenClawは最新以外の思考ブロックを落とします。境界にチェックポイントを置いておけば、編集を受けても生き残る確率が高い。会議の議事録に「議題の切れ目」でしおりを挟むのと同じ発想です。
5. デコード側の答え その1:グローバルLRUキャッシュ
前節で、プリフィルは転送律速まで押し切れると分かりました。デコードはその真逆で、1トークンが呼ぶのは数人だけです。全員を運ぶ必要はない。代わりに、誰を席に残すかが問題になります。
このセクションで分かること:LRUキャッシュがどんな条件で効き、どんな条件で効かないか。
llama.cppはモデルのロード時にどのテンソルをどのデバイスに置くかを決め、KTransformersはプリフィル時に「よく使う」相談員を席に固定します。どちらも一度決めたら変えない静的配置です。対してFreeTokenは、全MoEレイヤで1つのLRU(最近使われていないものから追い出す)キャッシュを共有し、ルータが指名した相談員をその都度席に迎え入れます。
本当に差が出るのかを、自分で確かめました。合成したルーティングトレースを両方式に流し、ミス率を比べます。局所性パラメータ $p$ は「直前のトークンと同じ相談員を呼び続ける確率」です。
"""ルーティング局所性のもとで LRU キャッシュと静的配置のミス率を比較する."""
from collections import OrderedDict, Counter
import numpy as np
E, K, STEPS, SEED = 256, 8, 20_000, 20260825
def make_trace(locality: float, rng: np.random.Generator) -> np.ndarray:
"""locality の確率で直前のワーキングセットを再利用するトレースを作る."""
work = rng.choice(E, size=K, replace=False)
out = np.empty((STEPS, K), dtype=np.int32)
for t in range(STEPS):
swap = rng.random(K) > locality # 局所性が低いほど入れ替わる
work = np.where(swap, rng.integers(0, E, K), work)
out[t] = work
return out
def _accesses(trace: np.ndarray) -> int:
return sum(len(np.unique(s)) for s in trace)
def miss_rate_lru(trace: np.ndarray, cap: int) -> float:
cache, miss = OrderedDict(), 0
for step in trace:
for e in np.unique(step):
if e in cache:
cache.move_to_end(e)
else:
miss, cache[e] = miss + 1, None
if len(cache) > cap:
cache.popitem(last=False)
return miss / _accesses(trace)
def miss_rate_static(trace: np.ndarray, cap: int, warm: int = 512) -> float:
"""プリフィル相当の先頭 warm ステップで頻度上位 cap 個を固定し、以後変えない."""
resident = {e for e, _ in Counter(trace[:warm].ravel().tolist()).most_common(cap)}
hit = sum(int(e in resident) for s in trace for e in np.unique(s))
return 1 - hit / _accesses(trace)
E=256 人の相談員、1ステップあたり K=8 人を指名、20,000ステップ。席数を変えながら測った結果が次の表です。
| 席数(/256) | 局所性 p=0.95 のLRU | 同・静的配置 | 局所性 p=0.00 のLRU | 同・静的配置 |
|---|---|---|---|---|
| 16 | 4.7% | 92.5% | 94.8% | 93.6% |
| 32 | 4.4% | 85.6% | 88.6% | 87.2% |
| 64 | 3.8% | 72.7% | 76.0% | 74.6% |
| 96 | 3.2% | 60.5% | 63.4% | 62.2% |
| 128 | 2.6% | 48.9% | 50.7% | 49.7% |
左半分は劇的です。席が全体の25%(64席)しかなくても、局所性があればLRUのミス率は3.8%まで落ちます。静的配置は72.7%。論文がRTX 5090の実容量で報告した「FreeToken 16%対llama.cpp 62%」という数字と、方向も桁も一致しました。
意外だったのは右半分です。局所性をゼロにすると、LRUは静的配置に負けました(94.8%対93.6%)。理由を考えると当たり前で、ランダムなアクセスでは「最近使った」という情報に予測価値がなく、LRUは追い出しの入れ替えコストだけを払うことになります。
ここから読み取るべきことは1つです。LRUの優位はハードウェアが生んでいるのではなく、モデルのルーティング局所性が生んでいる。局所性の弱いモデルでは、この設計の利得はそのまま消えます。
ここまでのまとめ
- MoEは計算が疎になるが、全相談員分のメモリは依然として必要(§2)
- プリフィルはほぼ全員を運ぶので、二重バッファで転送を計算の裏に隠す(§4)
- デコードは疎なので、LRUで「今の作業集合」を席に追随させる(§5)
- ただしLRUはミスをゼロにはできない。次はその残りミスの話(§6)
6. デコード側の答え その2:q政策
前節で、LRUはミス率を大きく下げるものの、ゼロにはできないと分かりました。コールドスタート、作業集合の急な移動、そもそもの容量不足。残ったミスをどう処理するかが、デコード速度を直接決めます。
このセクションで分かること:ミスを「運ぶ」と「その場で計算する」に振り分ける閉形式。
席にいない相談員に対して、取れる手は2つです。台車で窓口まで運んでGPUで実行するか、倉庫まで出向いてCPUに実行させるか。ここで使う記号を、使う直前に1つずつ導入します。
まず $m$(ミス数、整数)です。1レイヤで指名されたのに席にいなかった相談員の人数を指します。次に $S$(エキスパートサイズ、スカラー・バイト)。相談員1人分の重みの大きさです。
そして2本の帯域です。$B_{\mathrm{P}}$(PCIe転送帯域、GB/s)は台車が通路を進む速さ。$B_{\mathrm{H}}$(ホスト側処理帯域、GB/s)はCPUが倉庫の中で重みを読みながら計算する速さです。
ここが本題の核心なのですが、この2つは同じ倉庫のドアを共有しています。台車で運び出すのも、倉庫の中で読むのも、どちらもホストメモリから同じバイト列を引っ張る動作だからです。したがってPCIeが飽和しているとき、CPU実行に使える帯域は残りかすしかありません。
B_{\mathrm{R}} = \max(B_{\mathrm{H}} - B_{\mathrm{P}}, 0)
この式が言っているのは、ドアの幅 $B_{\mathrm{H}}$ から台車が占有する分 $B_{\mathrm{P}}$ を引いた残りが、その場で相談できる余力 $B_{\mathrm{R}}$(残余帯域、GB/s)だということです。台車が広すぎるとドアが塞がり、倉庫内での作業は止まります。
ここで $q$(転送に回す人数、整数)を置きます。$m$ 人のミスのうち $q$ 人を台車で運び、残る $m-q$ 人を倉庫で処理する。それぞれの所要時間は次のようになります。
T_{\mathrm{fill}}(q) \approx \frac{qS}{B_{\mathrm{P}}}, \qquad
T_{\mathrm{cpu}}(m-q) \approx \frac{(m-q)S}{B_{\mathrm{H}} - B_{\mathrm{P}}}
この2式が言っているのは、運搬時間は台車の速さで、倉庫内作業の時間は残余帯域で割って求まる、ということです。そして両者は同時進行するので、レイヤが露出する遅延は遅いほうです。両者が等しくなる点で全体が最小になるので、
\frac{q}{m-q} \approx \frac{B_{\mathrm{P}}}{B_{\mathrm{H}} - B_{\mathrm{P}}}, \qquad
q^{\star} \approx m\,\frac{B_{\mathrm{P}}}{B_{\mathrm{H}}}
最終的に残るのは、帯域の比に人数を掛けるだけという形です。台車が速いマシンほど多くを運び、CPU側が強いマシンほど倉庫に残す。$B_{\mathrm{H}}$ が $B_{\mathrm{P}}$ に近づくと $q^{\star}$ は $m$ に近づき、CPU併走は自然に消滅します。分岐を書かずに全ハードウェアを1本の式で覆えているのが、この定式化のうまいところです。
記号と比喩の対応表
| 記号 | 読み | 相談窓口のたとえでの意味 | 型 |
|---|---|---|---|
| $E$ | エキスパート総数 | 倉庫にいる相談員の全人数 | 整数 |
| $k$ | 活性エキスパート数 | 1トークンが指名する人数 | 整数 |
| $m$ | ミス数 | 指名されたが席にいなかった人数 | 整数 |
| $S$ | エキスパートサイズ | 相談員1人分の重みのバイト数 | スカラー |
| $B_{\mathrm{P}}$ | PCIe転送帯域 | 台車が通路を進む速さ | GB/s |
| $B_{\mathrm{H}}$ | ホスト処理帯域 | 倉庫のドアの広さ | GB/s |
| $B_{\mathrm{R}}$ | 残余帯域 | 台車に取られずに残ったドアの幅 | GB/s |
| $q^{\star}$ | 最適な転送人数 | 台車で運ぶべき人数 | 整数 |
7. 自分のマシンのホスト帯域を測る
前節で、判断に必要なのは $B_{\mathrm{P}}$ と $B_{\mathrm{H}}$ の2つだけだと分かりました。逆に言えば、この2つを測らないと式は使えません。しかもカタログスペックからは読めない、と論文は明言しています。
このセクションで分かること:ホスト帯域の測り方と、素朴に測ると外す理由。
次のスクリプトを筆者の環境で走らせました。
"""ホストメモリ帯域 B_H の実測(CPU側エキスパート実行の上限を決める量)."""
import time
import numpy as np
MiB = 1024 ** 2
def _timed(fn) -> float:
t0 = time.perf_counter()
fn()
return time.perf_counter() - t0
def measure(fn, nbytes: int, repeat: int = 7) -> float:
"""fn を repeat 回まわし、最速試行から GB/s を返す."""
fn() # ウォームアップ
return nbytes / min(_timed(fn) for _ in range(repeat)) / 1e9
def main() -> None:
n = 64 * MiB // 4 # 64MiB 分の float32
src, dst = np.ones(n, dtype=np.float32), np.empty(n, dtype=np.float32)
w = np.ones((2048, 2048), dtype=np.float32) # 16MiB のエキスパート重みを模す
x = np.ones(2048, dtype=np.float32)
rows = [
("逐次読み出し(sum)", lambda: src.sum(), src.nbytes),
("コピー(read+write)", lambda: np.copyto(dst, src), src.nbytes * 2),
("行列ベクトル積(GEMV)", lambda: w @ x, w.nbytes),
]
for label, fn, nbytes in rows:
print(f"{label:<24}{nbytes / MiB:>10.0f} MiB{measure(fn, nbytes):>10.2f} GB/s")
if __name__ == "__main__":
main()
計測環境は Ubuntu 24.04 / Intel Xeon 2.10GHz 1コア / メモリ3GB / Python 3.12.3 / NumPy 2.4.4 のコンテナです。3回走らせた結果の範囲を載せます。
| 操作 | 転送量 | 測定値(GB/s) | この値が代表するもの |
|---|---|---|---|
| 逐次読み出し(sum) | 64 MiB | 15.9〜16.3 | 単純な読み出し。NumPyの縮約が単スレッドALU律速 |
| コピー(read+write) | 128 MiB | 24.0〜26.0 | memcpy相当。読み書き合計 |
| 行列ベクトル積(GEMV) | 16 MiB | 23.3〜27.4 | 実際のエキスパート実行に最も近い |
素朴に「読み出し帯域を測ればいい」と考えて sum の値を採ると、16 GB/s という数字が出ます。しかし実際のCPUエキスパート実行に相当するGEMVは27 GB/s まで出ています。約1.7倍の過小評価です。これをそのまま $B_{\mathrm{H}}$ に入れると、$q^{\star} = m \cdot B_{\mathrm{P}}/B_{\mathrm{H}}$ が実際より大きく出て、CPUに回せたはずの仕事まで台車に積んでしまいます。
論文が「すべての帯域はプラットフォーム仕様ではなく、実際に配置されるテンソル形状で測定した」とわざわざ書いている理由が、ここで腑に落ちました。測る対象は帯域ではなく、そのカーネルの帯域なのです。
$B_{\mathrm{P}}$ 側はPyTorchがあれば数行で測れます。ページロックしたメモリからの非同期転送でないと実効値が出ないので、pin_memory=True を忘れないでください。
import torch, time
src = torch.empty(1 << 28, dtype=torch.uint8, pin_memory=True) # 256MiB
dst = torch.empty_like(src, device="cuda")
torch.cuda.synchronize(); t0 = time.perf_counter()
dst.copy_(src, non_blocking=True); torch.cuda.synchronize()
print(f"B_P = {src.nbytes / (time.perf_counter() - t0) / 1e9:.1f} GB/s")
8. 四捨五入で27%損をした話
前節で、2本の帯域を実測する方法が固まりました。ところが式は連続値を返すのに、運ぶ人数は整数でしか指定できません。この丸め方が実害を出すかどうかを確かめないまま使うのは危ないので、検算しました。
このセクションで分かること:閉形式を整数化するときの安全な手順。
論文のTable 1に載っている実測帯域を式に入れ、四捨五入した $q^{\star}$ と、$q$ を1から $m$ まで総当たりして得た真の最適値を突き合わせます。
"""q* = m * B_P / B_H の閉形式を、総当たり探索の最適解と突き合わせて検算する."""
import math
MACHINES = [ # 論文 Table 1 の実測帯域 (GB/s)
("RTX 4060 ノート", 11.8, 47.5), ("RTX 4090", 25.1, 63.2),
("RTX 3090", 25.3, 56.7), ("RTX PRO 6000", 51.5, 178.0),
("RTX 5090 サーバ", 52.7, 77.3), ("RTX 5090 デスクトップ", 49.0, 53.8),
]
M = 8 # 1 レイヤあたりのキャッシュミス数
def latency(q: int, m: int, bp: float, bh: float) -> float:
"""露出するレイヤ遅延。転送と CPU 実行は同時に走るので遅い方が支配する."""
t_fill = q / bp
if q == m:
return t_fill
residual = bh - bp
return math.inf if residual <= 0 else max(t_fill, (m - q) / residual)
def pick(m: int, bp: float, bh: float) -> int:
"""閉形式の floor / ceil と「全部転送」の3候補だけを評価して選ぶ."""
exact = m * bp / bh
cands = {max(1, math.floor(exact)), min(m, math.ceil(exact)), m}
return min(cands, key=lambda q: latency(q, m, bp, bh))
for name, bp, bh in MACHINES:
naive = max(1, min(M, round(M * bp / bh)))
best = min(range(1, M + 1), key=lambda q: latency(q, M, bp, bh))
loss = latency(naive, M, bp, bh) / latency(best, M, bp, bh) - 1
print(f"{name:<22}{bp / bh:>6.2f}{naive:>4}{pick(M, bp, bh):>4}{best:>4}{loss:>8.1%}")
| マシン | $B_{\mathrm{P}}/B_{\mathrm{H}}$ | 四捨五入 | 3候補評価 | 総当たり | 四捨五入の損失 |
|---|---|---|---|---|---|
| RTX 4060 ノート | 0.25 | 2 | 2 | 2 | 0.0% |
| RTX 4090 | 0.40 | 3 | 3 | 3 | 0.0% |
| RTX 3090 | 0.45 | 4 | 4 | 4 | 0.0% |
| RTX PRO 6000 | 0.29 | 2 | 2 | 2 | 0.0% |
| RTX 5090 サーバ | 0.68 | 5 | 6 | 6 | 7.1% |
| RTX 5090 デスクトップ | 0.91 | 7 | 8 | 8 | 27.6% |
最下行が予想外でした。RTX 5090のデスクトップ構成、つまり PCIe 5.0 に対して DDR5 の2チャネルというもっとも一般的な自作PCの形で、四捨五入が27.6%も損をしています。
原因は残余帯域です。$B_{\mathrm{P}} = 49.0$ に対し $B_{\mathrm{H}} = 53.8$ なので、$B_{\mathrm{R}}$ はわずか4.8 GB/s。ここに1人でもCPU側へ回すと、その1人だけで $1/4.8$ の時間がかかり、8人全員を台車で運ぶ $8/49.0$ より遅くなります。帯域比が1に近いマシンでは、CPU併走は足を引っ張るだけになるわけです。
対策は単純でした。$m \cdot B_{\mathrm{P}}/B_{\mathrm{H}}$ の切り捨て・切り上げ・そして「全部転送($q = m$)」の3候補だけを遅延式に入れて比べる。$m$ を4・8・16・32、帯域比を0.01刻みで振って総当たり解と照合したところ、この3候補評価は全ケースで最適解と一致しました。追加コストは掛け算3回です。
この結果は、論文がRTX 5090のサーバ構成とデスクトップ構成を分けて評価している理由も説明します。GPUのシリコンは同じでも、ホスト側がメニーチャネルのサーバか2チャネルの一般PCかで、最適な振り分けはまったく別物になります。GPUの型番だけでは判断できません。
9. リソース変動への答え:弾性メモリと高速起動
前節までで、席数が固定という前提のもとでの最適な振り分けが決まりました。しかし個人のマシンでその前提は成り立ちません。ブラウザもゲームも同じVRAMを奪いに来ます。
このセクションで分かること:実行中に席数が変わっても壊れない仕組み。
FreeTokenの設計は、ひとつの性質にすべて乗っています。ホストメモリ側のエキスパートプール、つまり倉庫が正本であるということです。席に何人いるかは速度にしか影響せず、正しさには一切影響しません。
この性質があるため、スケジューラの安全点で、エンジンを再起動せずに、倉庫を読み直さずに、席の総数とKVキャッシュとの配分を組み替えられます。エージェントのセッションが長引いてKVキャッシュが膨らんだら、席を減らしてKVに回せばよい。
起動時間についても工夫があります。FTW(FreeToken Weight)という独自形式で、実行時のレイアウトのままディスクに置いておく。読み込み時はテンソルの探索も並べ替えも不要で、そのまま流し込むだけです。しかもページロックは中身を埋めたあとに行います。空のバッファを先にロックすると、上書きするだけのページをOSがゼロ埋めするために数GB分の無駄が出るからです。GPUのウォームアップも行いません。最初のリクエストは冷えたキャッシュのまま§6のミス処理経路で処理され、使いながら温まります。
10. 実測が示す到達点
前節までで、FreeTokenの設計要素が出そろいました。では実際に何がどこまで動くのか。判断の材料として、論文が報告した数字を整理します。
このセクションで分かること:どのクラスのマシンで、どのクラスのモデルが実用速度に届くか。
| ハードウェア | 動かしたモデル | デコード速度 | 最良の既存エンジン比 |
|---|---|---|---|
| RTX 4060 ノート(8GB, PCIe 4.0 x8) | Qwen3.6-35B-A3B(NVFP4) | 39.3 tok/s | 1.8倍 |
| RTX 3090 / 4090(24GB) | Qwen3.6-35B-A3B(BF16) | ─ | 1.3倍 |
| RTX 5090 デスクトップ(32GB) | Qwen3.6-35B-A3B(BF16) | ─ | 2.1倍 |
| RTX 5090(32GB) | DeepSeek-V4-Flash 284B(MXFP4) | 22〜25 tok/s | 1.5〜1.9倍 |
| RTX PRO 6000(96GB) | GLM-5.2 753B(NVFP4, 433GB) | 14.9 tok/s | 2.0倍 |
8GBのノートPCで35Bが39.3 tok/s という数字は、論文が引用している実運用トレース上のCodexのデコード速度の中央値33 tok/s を上回っています。手元の非力なGPUが、商用コーディングエージェントと同等の体感速度に届いているということです。
もうひとつ見ておくべきなのがテール遅延です。FreeTokenの最悪TTFT(最初のトークンまでの時間)は全ワークロードで44秒未満に収まった一方、比較対象は少なくとも1つの条件で150秒を超え、KTransformersは946秒に達しています。OpenClawには120秒のアイドル監視、Claude Codeには約10分のリクエストタイムアウトがあるため、これは遅いという話ではなく、エージェントが動かないという話です。平均値だけを見ていると見落とします。
11. 明日の判断:自分のマシンをどう分類するか
前節の数字は、あくまで論文の6台での結果です。手元の1台をどこに当てはめるかは、§7で測った2つの値が決めます。
このセクションで分かること:測った帯域比から、使うべきエンジンと設定を選ぶ手順。
導入自体は次の1行です。
uv pip install "freetoken[accel]"
Windows/Linux向けにはGUI付きのデスクトップアプリも配布されています。RTX 30/40/50シリーズに対応し、Anthropic互換・OpenAI互換のAPIエンドポイントを持つため、Claude Code や OpenCode といったエージェントからそのまま向けられます。
12. トラブルシューティング
| 症状 | 考えられる原因 | 対処 |
|---|---|---|
| CPU併走を有効にしても速くならない | 帯域比 $B_{\mathrm{P}}/B_{\mathrm{H}}$ が1に近く、残余帯域が枯れている | §8の3候補評価で $q = m$ を選ぶ。CPUスレッド数を増やしても効果はない |
| $q^{\star}$ が実測と合わない | $B_{\mathrm{H}}$ をmemcpyや sum で測っている |
GEMVなど実際のカーネル形状で測り直す(§7で1.7倍の差) |
| 起動が異常に遅い | 重みの探索・並べ替えが毎回走っている | FTW形式に事前変換しておく |
| プリフィルは速いのにデコードが遅い | 席数が足りずLRUが機能していない | KVキャッシュとの配分を見直す。エージェント用途では文脈増加でKV側が膨らんでいる場合がある |
| エージェントが途中で応答を打ち切る | TTFTのテールがクライアントのタイムアウトを超えている | 平均ではなく最悪値を測る。OpenClawは120秒、Claude Codeは約10分が目安 |
| ホストメモリをページロックできない | OSやドライバの制約 | 純CPUバックエンドに退避する。転送帯域は諦めることになる |
13. 用語集
| 用語 | 定義 | 相談窓口のたとえでの対応 |
|---|---|---|
| MoE | 多数の部分ネットワークから一部だけを選んで実行する構造 | 大勢の相談員から数人だけを指名する受付 |
| エキスパート | MoEレイヤ内の個々の部分ネットワーク | 相談員1人 |
| ルータ | どのエキスパートを使うかを決める小さなネットワーク | 指名を出す受付係 |
| プリフィル | プロンプト全体を一括処理して初期状態を作る段階 | 開店前に全案件を一気にさばく時間 |
| デコード | 1トークンずつ生成する段階 | 来客ごとに1件ずつ相談する時間 |
| TTFT | 最初のトークンが返るまでの時間 | 来客が最初の返答をもらうまでの待ち時間 |
| 残余帯域 | PCIe転送に取られた後にホスト側へ残る帯域 | 台車に塞がれた後のドアの隙間 |
| セマンティックアンカー | 特殊トークン境界に打つ状態チェックポイント | 議事録の議題の切れ目に挟むしおり |
| 弾性メモリ管理 | 実行中にVRAM配分を組み替える仕組み | 混雑に応じて窓口の席を増減させる運用 |
| FTW | 実行時レイアウトのまま重みを保存する独自形式 | 相談員を配置順どおりに並べた倉庫 |
14. 学習ロードマップ
- Transformer と Self-Attention で、KVキャッシュがなぜ必要かを押さえる
- GPU と VRAM で、容量と帯域が別物であることを確認する
- llama.cpp と GGUF で、静的なレイヤ配置を実際に触ってみる
- vLLM でページドKVキャッシュとプレフィックス再利用を理解する
- 本記事の§7のスクリプトで、自分のマシンの2本の帯域を測る
- 状態空間モデル側の系譜(SSM → Mamba-2)で、再帰層のチェックポイントが必要な理由まで降りる
まとめ
- FreeTokenは、個人のマシンを GPU・CPU・ホストメモリ・PCIeをまとめた1つの推論基盤として扱うMoEサービングエンジンです
- 中心にあるのは $q^{\star} = m \cdot B_{\mathrm{P}}/B_{\mathrm{H}}$ という1本の式で、判断材料は実測した2本の帯域だけです
- 自分で検算した結果、LRUの優位はルーティング局所性に完全に依存しており、局所性がなければ静的配置に負けました
- 同じく検算の結果、閉形式の四捨五入は帯域比が1に近いマシンで最大27.6%損をしました。切り捨て・切り上げ・全部転送の3候補評価で解消できます
- カタログスペックからは何も決まりません。測ってから決めるというのが、この論文がいちばん強く言っていることだと受け取りました
筆者にとって最大の収穫は、「GPUを買い替えるべきか」という問いが「ホスト側のメモリチャネルはいくつか」という問いに置き換わったことでした。RTX 5090のシリコンが同じでも、サーバとデスクトップで最適解が変わる。それは、次に買うべきものがGPUとは限らないという意味でもあります。
参考文献
- Yang, S. ほか, FreeToken: Efficient Edge-Native MoE Serving with Bandwidth-Adaptive Execution, arXiv:2608.16157, 2026年8月17日投稿(邦題訳:FreeToken ── 帯域適応実行による効率的なエッジネイティブMoEサービング)。本記事の設計・実測値の出典。§3.2に $q^{\star}$ 政策、§5.1のTable 1に6台の実測帯域が載っています
- FlashML-org/FreeToken(GitHub, Apache-2.0)。実装本体。README冒頭の宣言は "Unlock datacenter-class intelligence on the hardware you already own"(和訳:すでに所有しているハードウェアでデータセンター級の知能を解き放つ)。導入手順・対応モデル一覧・CLIリファレンスは
docs/にあります
- Liang ほか, Local Routing Consistency of Mixture-of-Experts Language Models, arXiv:2505.16056(邦題訳:MoE言語モデルの局所的ルーティング一貫性)。§5でLRUが前提とするルーティング局所性を、複数のモデル系列で測定した研究
- Kamahori ほか, Fiddler: CPU-GPU Orchestration for Fast Inference of Mixture-of-Experts Models, arXiv:2402.07033(邦題訳:Fiddler ── MoEモデルの高速推論のためのCPU-GPU協調)。ミスしたエキスパートを「運ぶデータ」ではなく「その場で実行できる仕事」として初めて扱った先行研究
検証に使ったスクリプトや続報はXで流しています。