Magnitudeの2倍という主張は、自分の環境でも再現できるか。

村井
村井

@murai72 ・ 1本

サムネイル

「llama.cppの最大2倍速い」と聞いて、手元のマシンでも同じ数字が出るのか確かめたくなっていませんか? ただ、2倍と書いてあっても、何と比べての2倍なのかを先に見たいんですよね。推論エンジンMagnitudeの公式の表をばらしてみると、2倍近くまで伸びているのは4項目のうち1項目だけでした。

Magnitudeは、YC S25のスタートアップが作るオープンソースのローカル推論エンジンです。GPUで行列計算をする小さなプログラム(カーネル)を、モデルを動かす前にそのマシンの上で調整するのが特徴で、Apple Silicon、NVIDIA、AMD、CPUに対応しています。ライセンスはApache 2.0で、デスクトップアプリに magnitude CLIが同梱されています。

Magnitudeの2倍は、何と比べた数字か?

公式サイトの表を並べ直すと、次のとおりです。比較相手はどちらもllama.cpp、モデルはQwen3.6 35B-A3Bの4bit量子化、コンテキストは64k、投機的デコードは使っていません。

環境
処理
llama.cpp
Magnitude
差
Metal M4 Pro 48GB
プリフィル
466 tok/s
507 tok/s
+9%
Metal M4 Pro 48GB
デコード
30 tok/s
57 tok/s
+92%
CUDA DGX Spark
プリフィル
2,033 tok/s
2,507 tok/s
+23%
CUDA DGX Spark
デコード
49 tok/s
58 tok/s
+19%

「最大2倍」の中身は、Metalのデコードが30から57に伸びた1項目です。残りの3項目は1.09〜1.23倍に収まっています。

プリフィルは入力を読み込む処理、デコードは1トークンずつ文章を書き出す処理です。エージェントはファイルや会話履歴を毎回大量に読むので、待ち時間がプリフィル側に寄りやすくなります。エージェント用途なら、デコードの92%と同じ重さでプリフィルの9%を見ておくべきだと自分は読んでいます。

あなたが普段待たされているのは、読み込みと書き出しのどちらでしょうか?

llama.cpp側の条件は、どこまで書いてあるか

基地局の機器でも、カタログの最大値は測定条件の欄とセットで読むのが普通です。同じ目で、公式サイトとLaunch HNのスレッドでの開発側の説明から条件を拾うと、こうなります。右の列は自分の環境を書き込む欄です。

項目
公式の条件
自分の環境
比較相手
llama.cpp
記入
llama.cppのバージョン
記載なし
記入
llama.cppの設定
FlashAttention有効、プリフィルのバッチは既定値
記入
モデルと量子化
Qwen3.6 35B-A3B、4bit
記入
KVキャッシュ
Magnitudeはキー8bit・値4bit、llama.cppは既定のf16とみられる
記入
コンテキスト
64k、入力を64k埋めたかは記載なし
記入
投機的デコード
使わない
記入
マシン
M4 Pro 48GB、DGX Spark
記入
Magnitudeのバージョン
記載なし
記入

気になるのはKVキャッシュの行です。KVキャッシュは読んだ文脈を覚えておくメモリで、開発側はHNで、ここを量子化してメモリを半分以下にし、デコードも速くしていると説明しています。llama.cpp側で同じ量子化を試したらデコードが大きく落ちたので外した、とも書いていました。公式の92%は、エンジンの作りの差とKVキャッシュの持ち方の差が混ざった数字だと読んでいます。

バージョンの空欄も軽くありません。9月30日の0.2.0で、それまでllama.cppベースだった中身を自社エンジンに入れ替え、10月2日には0.2.4まで進んでいます。バージョンのない数字は、あとで誰とも突き合わせられません。

Qwen3.6 35B-A3Bは、自分の機体に載るか?

Magnitudeのモデル一覧では、Qwen3.6 35B-A3Bの必要メモリはQ4で24.2GB、Q8で40.4GBです。64kのコンテキストを使えば、その上にKVキャッシュが乗ります。

公式ドキュメントによると、Macは本体のメモリをOSやほかのアプリと分け合うので、32GBのMacでも32GBすべてをモデルに回せるわけではありません。GPUを挿した機体で効くのはGPU側のメモリで、メインメモリと足し算はできません。NVIDIAはAmpere世代以降が対象です。

自分の押し入れのGPUサーバーで比べるなら、相手はCUDAの行です。期待値は+19%と+23%で、Metalの92%を持ち込む理由はありません。しかもDGX Sparkは、CPUとGPUで128GBのメモリを共有する機体で、帯域は273GB/sです。デコードはメモリ帯域に引っ張られやすいので、VRAMが独立したGPUなら公式と違う比率が出ても不思議ではないと思っています。

16GBの機体では、この条件はそもそも組めません。Qwen3.5 9B(Q4で7.1GB)のような小さいモデルで測るなら、公式とは別の条件の数字として記録します。

あなたの機体は、Metalの行とCUDAの行のどちらに近いでしょうか?

llama.cppとMagnitudeを同じ物差しで測る手順

llama.cppには llama-bench という測定用コマンドがありますが、MagnitudeのCLIリファレンスには同じ役割のコマンドが見当たりませんでした。道具が違えば数字の意味も揃わないので、両方をOpenAI互換のAPIサーバーとして立て、同じスクリプトで測ります。

  1. magnitude --version と llama-server --version の出力を、そのまま記録に貼る
  2. llama.cpp側を公式に近い設定で起動する(llama-server -m ./qwen3.6-35b-a3b-q4.gguf -c 65536 -ngl 99 -fa on --port 8080、ファイル名は手元のものに置き換える)
  3. Magnitude側は別のターミナルで magnitude serve を起動し、magnitude catalog list で確認したモデルIDを magnitude catalog pull と magnitude models load に渡す
  4. 初回はカーネルの調整が走るので、1回空打ちしてから測る(0.2.4で、調整は長くても1分ほどに短縮された)
  5. 同じプロンプトを両方に5回ずつ投げ、中央値を取る

スクリプトは標準ライブラリだけで書いてあります。

# bench.py  使い方: python3 bench.py <URL> <モデルID> <プロンプトファイル>
import json, sys, time, urllib.request

url, model, path = sys.argv[1:4]
body = json.dumps({
    "model": model,
    "messages": [{"role": "user", "content": open(path, encoding="utf-8").read()}],
    "max_tokens": 256,
    "temperature": 0,
    "stream": True,
}).encode()
req = urllib.request.Request(url, body, {"Content-Type": "application/json"})

start = time.perf_counter()
first, chunks = None, 0
with urllib.request.urlopen(req) as res:
    for raw in res:
        line = raw.decode("utf-8").strip()
        if not line.startswith("data:") or line[5:].strip() == "[DONE]":
            continue
        choices = json.loads(line[5:]).get("choices") or []
        delta = choices[0].get("delta", {}) if choices else {}
        if any(delta.get(k) for k in ("content", "reasoning_content", "reasoning")):
            chunks += 1
            first = first or time.perf_counter()
end = time.perf_counter()

print(f"最初の出力まで: {first - start:.2f}秒")
print(f"書き出し: {(chunks - 1) / (end - first):.1f} チャンク/秒 ({chunks}チャンク)")

URLは、llama.cppなら http://127.0.0.1:8080/v1/chat/completions、Magnitudeなら http://127.0.0.1:10100/inference/v1/chat/completions です。最初の出力までの秒数がプリフィル、それ以降の速さがデコードの目安になります。

プロンプトは、1,000トークン前後の短いものと、6万トークン前後の長いものを2本用意します。公式の64kがどちらの状態を指すのか分からない以上、両方の数字を持っておけば後で照らし合わせられます。

注意が1つあります。このスクリプトが数えるのは、トークンではなくサーバーが送ってくる塊の数です。Magnitudeが1回に何トークンずつ送るかは、公式ドキュメントでは確認できませんでした。llama.cppは最後に timings という集計を返すので、そのtok/sとチャンク/秒がほぼ一致するかを先に見ておくと安心です。

Magnitudeで2倍が出ないのは、故障か?

NVIDIAの機体でデコードが+20%前後なら、公式のCUDAの行どおりです。2倍が出ないこと自体は、故障を疑う理由になりません。

Macは世代で事情が変わります。M5世代以降はMetal 4の行列演算をまだ使い切れていないと開発側がHNで認めていて、M5 MaxではMLX系のほうが速かったという報告も出ています。M1やM2でモデルが読み込めない件は0.2.4で、「Assessing models」で止まる件は0.2.2で直っているので、まず magnitude update で最新にしてから測り直します。

HNでは「llama.cppに速度で勝つのは低いハードルだ」という指摘もあり、Macでより速い選択肢として挙がっていたのがMLXでした。Macで測るなら、MLX系の実行環境を3列目に足しておくと、Magnitudeを選ぶ理由が数字で残ります。

公式サイトの「エージェント1つあたりメモリ27%減」も、比較相手とモデルが書かれていません。HNの投稿ではMetalで28%、CUDAで27%と書き分けられていたので、条件が埋まるまでは参考値として扱います。

ローカルLLMのベンチマークは、条件の表から

公式の92%は嘘ではありません。M4 Pro 48GB、Qwen3.6 35B-A3Bの4bit、64k、KVキャッシュの持ち方まで含めた条件で出た、デコードだけの数字です。条件が1つずれれば、比率も変わるかもしれません。

あなたの条件表は、右の列がいくつ埋まりそうですか? 測る前に、まずそこを埋めるところから始めます。埋まらない行が残ったまま出た数字は、公式と並べずに自分の記録として取っておけばいい、と自分は考えています。