コーディング AI は「写し書き」で何倍速くなる? AgSpec の仕組みを半分だけ実装して確かめる

AIエージェント

はじめに:コーディング AI の待ち時間は「同じ文章の書き直し」に使われている

Claude Code や Codex のようなコーディングエージェント(自分でファイルを読み、コードを直し、テストを動かす AI)を使っていると、修正パッチやテスト結果が 1 文字ずつ流れてくるのを眺める時間があります。よく見ると、その中身の多くはさっき読んだコードや、さっき出たログの書き写しです。

2026 年 10 月 1 日に arXiv で公開された論文「AgSpec」(韓国 KAIST のチーム)は、この「書き写し」に目をつけました。過去の文章から続きを先回りして写し、モデルにまとめて確かめさせることで、コーディングエージェントの生成速度を通常の最大 4.37 倍(同時に 1 件処理)、4.76 倍(同時に 16 件処理)にしたと報告しています。

この記事では、AgSpec の考え方を専門用語をかみくだきながら説明し、そのうえで仕組みの半分ほどを自分で実装して、Mac の上で本当に速くなるのかを確かめた結果を紹介します。

左は1トークンずつ書くので13回の計算が必要。右はさっき開いたファイルから同じ並びを探して下書きとして貼り、モデルが1回で採点するので2〜3回で済む

図1:左がふつうの生成、右が AgSpec の考え方。写せる部分は過去の文章から写し、モデルは「自分でもそう書く」かを確かめるだけにする

この記事で分かることは次の 4 つです。

  • 大規模言語モデル(LLM)の生成が遅い理由と、それを速くする「投機的デコーディング」の基本
  • コーディングエージェントならではの「写し書き」と、AgSpec がそれをどう利用するか
  • 論文が報告した数値と、どの工夫がどれだけ効いたか
  • 手元で半分だけ実装して測った結果と、実際に導入するときの注意点

想定している読者は、AI ツールを使ってはいるけれど推論の仕組みには詳しくない方から、推論サーバーを運用しているエンジニアまでです。前半は仕組みの説明、後半は検証と実務の話になっています。

この記事の確認範囲

  • 論文:Sumin Lee ほか「AgSpec: Pushing the Limits of Retrieval-Based Speculative Decoding in Coding Agent Pipelines」arXiv:2610.01108v1(2026-10-01 投稿)。本文と付録を読んで確認しました。執筆時点で公式のコードは見つかりませんでした
  • 検証:2026-10-04 に MacBook Pro(Apple M5 Max、メモリ 128GB)で実施。モデルは論文でも使われた Devstral-Small-2507 の MLX 4bit 版です
  • 論文の数値は論文の記載、手元の数値は筆者の計測として、文中で区別して書きます

LLM はなぜ 1 トークンずつしか進めないのか

1 回の計算で 1 トークン

LLM は、文章を「トークン」という小さなかたまり(英単語なら 1 語前後、日本語なら 1〜数文字)に分けて扱います。そしてそれまでの文章をすべて見て、次の 1 トークンを決めることをくり返して文章を作ります。この進め方を自己回帰(じこかいき)生成と呼びます。

ここで困るのが、次のトークンを決めるには前のトークンが決まっていないといけないことです。つまり 500 トークンの修正パッチを書くには、モデル全体の計算を 500 回、順番に行う必要があります。

しかも 1 回の計算の大部分は、モデルの重み(数十 GB)をメモリから読み出す時間です。1 トークン分の計算でも 10 トークン分の計算でも、重みを読む手間はほとんど同じです(データセンター向け GPU で 1 件ずつ処理する場合。Mac での事情は後の検証で見ます)。「1 回の計算で 1 トークンしか進まない」のがもったいない、というのが出発点になります。

投機的デコーディング:下書きをまとめて採点する

そこで考えられたのが投機的デコーディング(Speculative Decoding)です。何か安い方法で「たぶん次はこう続く」という下書きを数トークン分作り、本物のモデル(論文では「ターゲットモデル」)に1 回の計算でまとめて採点してもらいます。

ふつうの生成は5トークンに5回の計算。投機的デコーディングでは下書き5つをまとめて採点し、一致した3つとモデル自身の1つで4トークンが1回で進む

図2:下書きのうち、先頭から連続して一致した部分だけを採用する。外れた部分は捨てる

採点のしかたはシンプルです。モデルは下書きの各位置で「自分ならここに何を書くか」を一度に計算します。下書きと一致していれば採用、最初に食い違ったところで打ち切り、その位置にはモデル自身の答えを使います。そのため、下書きが全部外れても最低 1 トークンは進みます。

この方法の大事な性質は、出力の中身が変わらないことです。採用されるのはモデル自身が選ぶトークンと一致したものだけなので、1 トークンずつ書いた場合と同じ文章になります(最も確率の高いトークンを選ぶ「貪欲法」の場合。確率に従って選ぶ場合も、受理の規則を工夫して分布を保てます)。ただし後で書くとおり、コンピューターの数値計算の細かな事情で、実際には完全には一致しないことがあります。

下書きの作り方は 3 系統

下書きの作り方には大きく 3 つの系統があります。

系統下書きの作り方長所短所
小さな別モデル同じ系列の小型モデルに先に書かせるどんな文章にも使える小型モデルを動かすコストとメモリが要る
予測ヘッド(EAGLE、MTP など)本体に付けた小さな部品で数トークン先を予測当たりやすいモデルごとに学習が必要
検索型(Retrieval-based)すでにある文章から、同じ並びの続きを写す追加のモデルが要らず、ほぼタダ写せる文章がないと当たらない

AgSpec は 3 番目の検索型です。検索型の代表例には、プロンプトの中から続きを探す Prompt Lookup Decoding(PLD)や、過去の回答を木構造にためて探す SuffixDecoding などがあります。

コーディングエージェントは「写し書き」だらけ

検索型は「写せる文章」がある場面で強く、コーディングエージェントはまさにその場面です。論文は、エージェントの作業をセッション(1 つの依頼を片付けるまでのやり取り全体)として見ています。1 つのセッションの中で、役割の違う複数のエージェントが順番に呼ばれます。

場所を探す係、コードを書く係、実行する係、確認する係が順に動き、テストが落ちたらコード係からやり直す。それぞれファイル名、変えない行、ログ、パッチの一部を写す

図3:役割ごとに写すものが違い、やり直しを重ねるほど写しが増える

たとえばコードを書く係は、読んだファイルを unified diff(行頭に「␣」「-」「+」を付けた修正パッチの書式)で書き直します。変えない行や消す行は、元のファイルの写しです。実行する係はテストのログを引用し、確認する係はパッチやログの一部を引用します。テストが落ちてやり直すと、2 回目のパッチは 1 回目とほとんど同じになります。

ところが、従来の検索型にはこの写しを取りこぼす理由が 2 つありました。

  • 探す範囲が狭い:多くは「いま処理中の 1 回の呼び出し」の入力と出力しか探さない。前の呼び出しの出力(1 回目のパッチやログ)は、呼び出しが終わった時点で探せなくなる。ファイルが長くてプロンプトから切り落とされた場合も同じ
  • 形が違う:ファイルは原文のまま登録されるが、エージェントは diff の形で書き出す。行頭に「␣」が 1 文字付くだけで、トークンの並びが変わって一致しなくなる

さらに、何トークン先まで下書きするかも問題です。論文の調べでは、受理できる長さは役割ごとに違う範囲に分かれ、ターンを重ねるほど変わっていきました。固定の長さでは、写す場面では短すぎ、自由に書く場面では長すぎることになります。

AgSpec の工夫①:どこから写すか

探す場所を「出どころ」で 3 つに分ける

AgSpec は、探す対象(コーパス=文章の集まり)を出どころ別に 3 つ持ちます。

セッション、ワークスペース、グローバルの3つのコーパスを同じ末尾で探し、一致の長さを比べる。SとWは長い方、GはS・Wの長い方より5以上長いときだけ使う

図4:3 つの探し場所と、どれを使うかの規則

コーパス中身いつまで残すか
S:セッションこの依頼での全呼び出しの入力・ツール出力・生成文。役割をまたいで共有依頼が終わったら捨てる
W:ワークスペースこの依頼で開いたリポジトリのファイル(原文+書き出す形)依頼が終わったら捨てる
G:グローバル別の作業で集めたエージェントの出力記録事前に作って固定

下書きを作るときは、いま書いている文章の末尾と同じ並びを 3 か所で探し、何トークン前まで一致しているか(一致の長さ)を比べます。長く一致しているほど、その続きも当たりやすいと考えるからです。

選び方の規則は次のとおりです。S と W は長い方を使い、同じ長さなら S を使います。G は量が多くて無関係な文章とたまたま一致しやすいので、S と W の長い方より δ(デルタ、論文では 5 トークン)を超えて長いときだけ使います。

同じ一致がコーパス内に何か所もあるときは、S と G では「続く 3 トークン」が同じものをまとめて一番多いグループを選び(同数なら新しい方)、W では最初に出てくる箇所を選びます。

ファイルを「書き出す形」でも登録する

2 つ目の工夫は、ワークスペースのファイルをエージェントが書き出す形でも登録することです。

開いたファイルの原文と、コード係が書くdiffを並べる。diffでは写した行の先頭に␣や-が付くので、AgSpecは全行に␣を付けた写しと全行に-を付けた写しも登録する

図5:原文のままでは行頭の 1 文字の差で一致が切れる。書き出す形の写しを足すと、どの行を写しても一致する

ファイルを最初に開いたとき、AgSpec は「原文」「全行の先頭に␣を付けた写し」「全行の先頭に-を付けた写し」の 3 通りを W に入れます。diff の変えない行は␣付き、消す行は-付きなので、どちらかの写しと必ず一致します。ファイル全体を書き直す形式のツールなら、改行や引用符をエスケープした写しを 1 つ足す、と論文は説明しています。

AgSpec の工夫②:何トークン先まで写すか

外れた下書きは「みんなの時間」を使う

下書きが当たれば得をしますが、外れた分の採点は無駄になります。1 件だけ処理しているときは、その無駄はほとんど目立ちません。ところが推論サーバーが同時に何件もまとめて処理(バッチ処理)していると、外れた下書きの採点もバッチ全体の計算に乗るので、全員が少しずつ遅くなります。

論文はこの損得を「損益分岐点 λ(ラムダ)」で表します。λ は「下書きを 1 トークン増やして採点する手間が、何トークン分の生成に相当するか」です。下書きの k 番目が当たる確率が λ より高ければ、その位置まで下書きする価値があります。

長さはK=clip(round(α×m),1,Kmax)。Kmaxは過去の記録から係ごとに事前に決め、αは採点結果を見て毎回調整する。論文の例では固定32だと外れが多く、αで81%減った

図6:下書きの長さは「一致の長さ × 倍率 α」を、係ごとの上限 Kmax で抑えて決める

上限は事前に、倍率はその場で

AgSpec の長さの決め方は、次の 1 行の式です。

K = clip( round(α × m), 1, Kmax[係] )
  m     : いま見つかった一致の長さ
  α     : 係ごとの倍率(最初は 1.0)
  Kmax  : 係ごとの上限(事前に決める)

上限 Kmax は、グローバルコーパスを作るのに使った過去の記録を再生して決めます。記録の各場面で「検索した続きが、実際の出力と何トークン先まで一致していたか(R)」を数え、係ごとに「k 個目まで当たる確率 Pr(R ≥ k)」を出します。この確率が λ を下回る手前が上限です。論文の例では、λ = 0.1 のとき 11 個目が当たる確率 0.12、12 個目が 0.09 なら上限は 11 になります。

倍率 α は、採点のたびに更新します。

α ← α + η × ( τ − [1 つでも外れたら 1、全部当たったら 0] )
  η = 0.5(更新の幅)、τ = 1 − λ

τ = 0.95 なら、全部当たると +0.475、1 つでも外れると −0.025 です。外れが続く場面(自由に文章を書いている場面)では少しずつ縮み、写しが始まって全部当たるようになると一気に伸びます。論文によると、この更新は「R/m の τ 分位点(上位 5% の位置)」を追いかける形になっていて、上限と同じ損益分岐点の考え方を、その場の状況に合わせて当てはめたものだと説明されています。

論文の結果:どの工夫がどれだけ効いたか

論文は、vLLM(推論サーバーのソフトウェア)の上に AgSpec を実装し、NVIDIA RTX PRO 6000 Blackwell(96GB)1 枚で測っています。モデルは Devstral-24B、Gemma3-27B、Qwen3.6-27B の 3 つ(いずれも 4bit 量子化)、ベンチマークは SWE-bench Verified(実在の GitHub の不具合を直す課題)と TeamBench(役割分担したチームでの課題)です。

AgSpec は、下書きを探す「エンジン」は既存のもの(SAM-Decoding の接尾辞オートマトン、SuffixDecoding の接尾辞木)をそのまま使い、何を探すか・何トークン写すかという方針だけを入れ替える作りです。それぞれ AgSpec-SAM、AgSpec-Suffix と呼ばれています。

全体の速さ

条件通常の生成に対する速さ(AgSpec)補足
SWE-bench・TeamBench、バッチ 12.27〜4.37 倍最大は Devstral・TeamBench の 4.37 倍
同上、バッチ 161.08〜4.76 倍最大は Qwen3.6・SWE-bench の 4.76 倍
既存の最速手法との比較平均 18.0% 速い12 条件すべてで 1 位か 2 位
LiveCodeBench(リポジトリなし)4.87〜7.94 倍ワークスペースなしでも効く
Terminal-Bench(エージェント 1 体)既存の最速より 9.3〜27.2% 速い役割の区別なしでも効く

比較対象には、PLD、REST、FastCoder、SuffixDecoding、SAM-Decoding という 5 つの検索型と、予測ヘッド型の EAGLE-3 が入っています。エンジンごとに見ると、SuffixDecoding に AgSpec を載せると平均 16.5% 速く、SAM-Decoding に載せると平均 64.6% 速くなりました。SuffixDecoding はもともと過去の回答をためて長さも一致に合わせて変えるので、伸びしろが小さかったと論文は説明しています。

一方で、バッチ 16 の TeamBench では Qwen3.6 で 1.08 倍にとどまり、どの手法もほぼ速くなっていない条件もあります。「いつでも 4 倍」ではない点は押さえておきたいところです。

工夫ごとの効き目

論文は、記録した会話を再生するシミュレーションで、コーパスを 1 つずつ足したときの受理長(1 回の採点で進んだトークン数)も調べています。

工夫論文が報告した効果
呼び出しごとのコーパス → セッション全体のコーパス受理長 1.6〜2.1 倍(一番大きい)
ワークスペースを足す単独で +26〜47%、セッションに重ねても +2.2〜7.0%
書き出す形での登録コード係の 1 ターン目で +13〜39%、5 ターン目には 3% 以下
全部入りグローバルだけの場合の 2.9〜4.8 倍の受理長
下書きの長さの制御(Devstral、SWE-bench)固定 8 に対しバッチ 1 で +25%、バッチ 16 で +16%
固定 32 に倍率 α だけ足す(バッチ 16)速さ +66%、不採用の割合 78% → 50%

特に面白いのが長さの制御の結果です。固定 32 トークンは、バッチ 1 では固定 8 より 14% 速いのに、バッチ 16 では 30% 遅くなりました。外れた下書きの採点が、バッチ全体の計算を食うためです。AgSpec は不採用を固定 8 並みに抑えつつ、固定 32 の 83〜89% のトークンを受理していました。

手元で「半分だけ」実装してみる

論文のコードは公開されていないので、AgSpec の考え方を自分で小さく組み直し、Mac の上で本当に速くなるかを確かめました。目標は「論文と同じ数字を出すこと」ではなく、どの工夫が、どんな場面で、どれくらい効くのかを自分の目で確かめることです。

何を実装し、何を省いたか

論文の要素この記事の実装備考
検索エンジン(接尾辞オートマトン/接尾辞木)2 トークン組の索引から後ろへ一致を伸ばす簡易版一致長の測り方は同じ。速さの工夫は省略
S:セッションコーパス実装前の呼び出しの入力・出力をすべて登録(論文は入力の末尾 2,048 トークン+出力)
W:ワークスペース(原文)実装開いたファイルを登録
W:書き出す形(␣付き・-付き)実装論文どおり 2 通りの写しを追加
G:グローバル実装(小規模)評価に使わない 1 タスクの出力記録だけ。論文は 70〜213 タスク分
コーパスの選び方(δ = 5)実装同じ一致長が複数あるときは「新しい方」を選ぶ簡易版(論文は続く 3 トークンの多数決)
上限 Kmax の事前プロファイル実装別タスクの記録を再生して係ごとに決める
倍率 α のオンライン更新実装η = 0.5、α は 0.05〜12 に制限
バッチ処理での評価省略生成はバッチ 1 のみ。採点の重さだけバッチ 1・4・16 で測定
vLLM への組み込み省略Apple の MLX(mlx-lm)で動かす

「半分」と言っているのは、方針の部分(どこから探すか・何トークン写すか)は一通り実装した一方で、エンジンの高速化と、サーバーとしての同時処理を省いたからです。

検証の環境

項目内容
マシンMacBook Pro(Apple M5 Max、メモリ 128GB)、macOS 26
ソフトPython 3.12、mlx 0.32.3、mlx-lm 0.32.0
モデルmlx-community/Devstral-Small-2507-4bit(論文と同じ Devstral-Small-2507 の 4bit 版、約 13GB)
生成貪欲法(毎回いちばん確率の高いトークンを選ぶ)
注意計測中、同じ Mac で別の作業(別モデルの評価、動画の書き出しなど)が GPU を使っていた

モデルには、論文で使われていた Devstral を選びました。最初は手元にあった Qwen 系の新しい 27B モデルを使うつもりでしたが、設定を見ると「線形注意」という層が混ざった構成でした。この種の層は過去の情報を 1 つの状態にまとめて持つため、外れた下書きの分だけ巻き戻す、という投機的デコーディングに必須の操作が簡単にはできません。そこで、ふつうの注意(attention)だけでできたモデルに切り替えています。

題材:小さなリポジトリを 4 つ用意した

エージェントに解かせる題材として、バグのある小さな Python リポジトリを 4 つ作りました。どれも 40〜60 行ほどのモジュールとテストの組です。

タスク中身使いみち
t0_textstats単語数を数える関数が句読点を数えてしまう事前プロファイルとグローバルコーパス用(評価には使わない)
t1_inventory在庫がマイナスになる・合計金額の丸め評価
t2_config設定ファイルの行内コメント・空白・真偽値の表記ゆれ評価
t3_cart割引が商品ごとに何度もかかる・送料無料の判定ミス評価

エージェントは、論文の SWE-bench 構成を 4 役割に縮めたものです。場所特定係がファイルを答え、コード係が unified diff を書き、実際に patch で当ててから pytest を動かし、実行係がテスト結果を報告し、確認係が判定します。テストが通らなければ、報告と指摘を付けてコード係からやり直します(最大 3 ターン)。

計測の流れ

計測は 2 段階に分けました。

  1. 記録:エージェントを通常の生成で 1 回走らせ、各呼び出しの入力・出力・開いたファイルを JSON に保存する(agent.py)
  2. 再生:記録を先頭から順に再生し、呼び出しごとに 8 つの方式で同じ入力から生成し直して、速さを測る(bench.py)

再生方式にしたのは、すべての方式を「同じ会話の同じ時点」で比べるためです。セッションのコーパスには記録どおりの入出力を積み上げるので、方式によって会話の流れが変わることはありません。また、同じ呼び出しの中で 8 方式を続けて実行するので、Mac で別の作業が動いて速さが揺れても、その影響が特定の方式に偏りにくくなります。時間は入力の読み込み(プレフィル)を除いた、生成部分だけを測っています。論文と同じ考え方です。

比べた方式は次の 8 つです。①〜⑤は探す場所を 1 つずつ増やし、⑥⑦は下書きの長さの決め方を比べます。

方式探す場所下書きの長さ
通常の生成なし下書きなし
① 呼び出し内だけ(PLD 相当)今回の呼び出しの入力と出力固定 10
② +セッション①+前の呼び出しすべて固定 10
③ +開いたファイル(原文)②+W(原文)固定 10
④ +書き出す形③+W(␣付き・-付き)固定 10
⑤ +グローバル④+G(別タスクの出力)固定 10
⑥ 長さ固定 32⑤と同じ固定 32
⑦ AgSpec 式⑤と同じK = clip(round(α·m), 1, Kmax[係])

実装の要点①:一致の長さを測って続きを取り出す

検索の中心は、コーパスの中で「いま書いている文章の末尾」と最も長く一致する場所を探す処理です。直前 2 トークンの組で候補の位置を引き、そこから後ろ向きに何トークン一致するかを数えます。

class Corpus:
    """トークン列+2-gram 索引。prefer='recent' なら同じ一致長で新しい方を選ぶ。"""

    def search(self, ctx, min_match=2):
        cands = self.index.get((ctx[-2], ctx[-1]))  # 直前 2 トークンが同じ位置
        if not cands:
            return 0, -1
        t, best_m, best_p = self.tokens, 0, -1
        for p in reversed(cands[-256:]):             # 新しい方から最大 256 か所
            if p >= len(t):                           # 続きが無い位置は使えない
                continue
            m = 2
            while m < 64 and m < len(ctx) and p - m - 1 >= 0 \
                    and t[p - m - 1] == ctx[-m - 1]:
                m += 1                                # 後ろ向きに一致を伸ばす
            if m > best_m:
                best_m, best_p = m, p
        return best_m, best_p                         # 一致長と、続きの開始位置

3 つのコーパスの使い分けは、論文の規則をそのまま書いています。

best = max(live, key=lambda x: x[0])        # S と W のうち一致が長い方(同点は S)
if use_global:
    mg, pg = glob.search(ctx)
    if mg > best[0] + delta:                 # G は δ を超えて長いときだけ
        best = (mg, pg, glob, "global")

書き出す形の写しは、ファイルの全行に「␣」または「-」を付けるだけです。

def emission_forms(text):
    lines = text.splitlines()
    return ["\n".join(" " + l for l in lines) + "\n",   # 変えない行の形
            "\n".join("-" + l for l in lines) + "\n"]   # 消す行の形

実装の要点②:下書きをまとめて採点する

採点では、直前のトークンと下書きをつなげてモデルに 1 回だけ通し、各位置の「モデルならこう書く」を取り出します。先頭から一致した数だけ採用し、外れた分の KV キャッシュ(過去のトークンの計算結果の置き場)を捨てます。

inp = mx.array([y] + draft, mx.uint32)              # 直前のトークン+下書き
logits = model(inp[None], cache=cache)              # 1 回の計算でまとめて採点
pred = mx.argmax(logits[0], axis=-1).tolist()       # 各位置でモデルが選ぶトークン
n = 0
while n < len(draft) and draft[n] == pred[n]:       # 先頭から一致した数
    n += 1
new = draft[:n] + [pred[n]]                         # 採用分+モデル自身の 1 つ
trim_prompt_cache(cache, len(draft) - n)            # 外れた分の KV を巻き戻す

trim_prompt_cache は mlx-lm に入っている関数で、KV キャッシュの末尾を指定した数だけ削ります。先ほどの「線形注意の層があると巻き戻せない」というのは、この操作ができないという意味です。

実装の要点③:長さの制御

下書きの長さは、論文の式 (1)〜(3) をそのまま書きました。

@dataclass
class LengthController:
    tau: float                      # τ = 1 − λ
    eta: float = 0.5
    kmax: dict = field(default_factory=dict)   # 係ごとの上限(事前プロファイル)
    alpha: dict = field(default_factory=dict)  # 係ごとの倍率(最初は 1.0)

    def k(self, role, m):
        a = self.alpha.get(role, 1.0)
        return max(1, min(round(a * m), self.kmax.get(role, 32)))

    def update(self, role, accepted, drafted):
        cap = self.kmax.get(role, 32)
        if accepted == drafted == cap:     # 上限で打ち切られた観測は使わない
            return
        a = self.alpha.get(role, 1.0)
        a += self.eta * (self.tau - (1.0 if accepted < drafted else 0.0))
        self.alpha[role] = min(12.0, max(0.05, a))

上限 Kmax は、評価に使わない t0_textstats の記録から事前に決めます(profile.py)。モデルを動かさず、記録された出力に対して「検索した続きが何トークン先まで当たっていたか」を数え、係ごとに Pr(R ≥ k) が λ を下回る手前を上限にします。λ は本来、採点するトークンを 1 つ増やしたときの時間の増え方から計算します(論文の付録)。ただし今回は後で書く事情で手元の λ を安定して測れなかったため、主な比較では論文の SWE-bench 設定と同じ τ = 0.95(λ = 0.05)を使い、別に手元の推定値 λ = 0.2 でも比べました。

再現する手順

検証に使ったコードは、上で紹介した部分を含めて 6 本のスクリプト(specdec.py・agent.py・bench.py・profile.py・cost.py・numerics.py)とグラフ用の plots.py、題材の 4 リポジトリです。同じ構成で試す場合の流れは次のとおりです。

# 作業フォルダと Python 環境(uv を使う例)
mkdir agspec-lab && cd agspec-lab
uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python mlx-lm matplotlib pytest

# モデル(約 13GB)。途中で止まるときは curl -C - で再開できる取り方に切り替える
.venv/bin/hf download mlx-community/Devstral-Small-2507-4bit

# 記録 → 採点の重さ → 事前プロファイル → 8 方式の比較 → λ=0.2 版 → 数値の切り分け → グラフ
MODEL=mlx-community/Devstral-Small-2507-4bit ./run_all.sh

M5 Max では、記録に約 25 分、8 方式の比較に約 40 分かかりました(別の作業と GPU を共有した状態)。Devstral を transformers のトークナイザーで読むと分割規則の警告が出るので、読み込み時に fix_mistral_regex=True を付けています(specdec.py の load_model)。

検証結果:何が効き、何が効かなかったか

エージェントの記録

まず、エージェントを通常の生成で走らせた記録です。評価に使ったのは t1〜t3 の 3 タスク、合わせて 21 回の呼び出しで、モデルが書いた出力は 2,490 トークンでした。

タスク結果ターン数呼び出し出力トークン
t0_textstats(プロファイル用)3 ターン目で全テスト合格3101,440
t1_inventory2 ターン目で全テスト合格27834
t2_config3 ターンでは直しきれず(5 件中 4 件合格)3101,357
t3_cart1 ターン目で全テスト合格14299

最初の記録では、モデルの書いた diff が patch でも git apply でも当たらず、3 ターンとも同じ失敗をくり返していました。中身の修正は正しいのに、関数の間の空行が 1 行足りない、新しく足した行に「+」を付け忘れて文脈行として書く、といった細かな誤りがあったためです。そこで、行番号や空行のずれ、ファイルにない文脈行を許して当てる方法を併用し、記録を取り直しました。実際のエージェント基盤でも、モデルの diff をそのまま厳密に当てることは少なく、似た工夫が入っています。

結果①:探す場所を増やすほど、1 回で進むトークンが増えた

8 つの方式で、21 回の呼び出しを生成し直した結果です。ここで示す「1 回の採点で進んだトークン数」(論文の受理長=AL)と「外れた下書きの数」は、モデルを何回呼んだかを数えたもので、マシンの混み具合に左右されません。

探す場所を増やすと1回で進むトークン数が1.78から2.52まで増える。長さ固定32とAgSpec式はどちらも2.52だが、外れた下書きは3.76と1.17で大きく違う

図7:方式ごとの「1 回の採点で進んだトークン数」(左)と「外れた下書きの数」(右)。括弧内は、通常の生成に対してモデルを呼んだ回数の割合

方式1 回で進んだトークン数モデルを呼んだ回数外れた下書き(出力 1 トークンあたり)
通常の生成1.00100%0
① 呼び出し内だけ(PLD 相当)1.7856%1.37
② +セッション2.1646%1.36
③ +開いたファイル(原文)2.1646%1.36
④ +書き出す形の写し2.4142%1.15
⑤ +グローバル2.4341%1.21
⑥ 長さ固定 322.5240%3.76
⑦ AgSpec 式2.5240%1.17

読み取れることは 4 つあります。

  • セッションの記録が一番効く:①→②で 1.78 → 2.16。論文でもセッション全体のコーパスが最大の効果でした
  • 原文のファイルは効かず、書き出す形の写しは効く:③は②と同じ値で、原文のワークスペースからの下書きは 1 回も選ばれませんでした。今回はコード係のプロンプトにファイル全文が入っているので、原文の一致は②の時点ですでに見つかるからです。一方、␣付き・-付きの写しを足した④では 2.41 に伸び、ワークスペース由来の下書き 76 回で 479 トークンが受理されました(1 回あたり 6.3 トークン)
  • グローバルはほとんど使われない:⑤で G が選ばれたのは 13 回だけでした。別タスク 1 つ分の小さな記録で、しかも δ = 5 の差を付けているので、出番が少ないのは想定どおりです
  • 長さ制御は「同じ成果を少ない外れで」:⑥長さ固定 32 と⑦AgSpec 式は、どちらも 2.52 トークン進みました。ところが下書きの数は 1 回あたり 10.97 個と 4.48 個、外れの割合は 86% と 66% です。⑦は⑥の 4 割ほどの下書きで、同じだけ当てています

結果②:コード係と実行係の 2 ターン目以降で大きく伸びる

係とターンに分けると、論文の図 1(b) と同じく、係ごとに受理長の範囲がはっきり分かれました。

場所特定と確認は1.3〜1.5でほぼ伸びない。コード係は1ターン目2.4、2ターン目以降4.4。実行報告は2ターン目以降4.2まで伸びる

図8:係とターンごとの受理長。①呼び出し内だけ(青)、②+セッション(緑)、⑦AgSpec 式(橙)

係・ターン① 呼び出し内だけ② +セッション⑦ AgSpec 式
コード 1 ターン目1.841.842.42
コード 2 ターン目以降1.883.514.37
実行報告 1 ターン目3.242.603.15
実行報告 2 ターン目以降2.733.154.16
場所特定・確認1.2〜1.41.3〜1.51.3〜1.5

コード係の 1 ターン目は、セッションを足しても変わりません(まだ自分のパッチが無いため)。ここを押し上げたのが書き出す形の写しで、1.84 → 2.42(+32%)でした。論文の「1 ターン目で +13〜39%」とほぼ同じ範囲です。2 ターン目以降は前回の自分のパッチがセッションにあるので、②だけで 3.51、⑦で 4.37 まで伸びました。

一方で、実行係の 1 ターン目は②の方が①より低くなりました(3.24 → 2.60)。テストのログを写す場面で、セッション側の「より長く一致した別の箇所」(プロンプトに入っていたコードなど)を選んでしまい、続きが外れたためです。長く一致した場所が正しい続きとは限らない、という検索型の弱点で、論文が同じ一致の中から「続く 3 トークンの多数決」で選んでいるのは、こうした外れを減らすためだと考えられます(今回の簡易版は「新しい方」を選んでいます)。

場所特定と確認の係は、どの方式でもほとんど伸びませんでした。自分の言葉で短い判断を書く係なので、写せる部分が少ないのです。事前プロファイルで決まった上限も、場所特定 4・コード 20・実行報告 16・確認 4 と、係による違いがはっきり出ました。

結果③:長さ制御は、外れそうな場面で下書きを縮める

長さ固定 32 と AgSpec 式で、1 回の呼び出しの中の動きを並べたのが次の図です(t2_config のコード係、1 ターン目)。

長さ固定32は外れが多い場面でも毎回最大32個下書きし外れが1063トークン。AgSpec式は外れが続く場面で下書きを数個に縮め、当たり始めると伸ばし、外れは464トークン

図9:採点ごとの「下書きした数」(薄い青)と「当たった数」(濃い青)。上が長さ固定 32、下が AgSpec 式

採点の回数は 115 回と 119 回でほぼ同じ、受理したトークンも 132 と 128 でほぼ同じです。それでいて、外れた下書きは 1,063 トークンから 464 トークンへ 56% 減りました。倍率 α は呼び出しをまたいで学習され、最後には コード係 6.3、実行係 6.4、場所特定 1.8、確認 1.7 と、係ごとに大きく分かれていました。

結果④:Mac では「まとめて確かめる」のがタダではなかった

ここまでの指標は混み具合に左右されない数え方でしたが、最後に実際の時間も見ます。ただし今回は、同じ Mac で別の作業(別のモデルの評価や動画の書き出し)が GPU を使い続けていて、時間の測定はかなり揺れました。呼び出しごとに全方式を続けて測って偏りはならしていますが、ここからの数値は参考値として読んでください。

実測では通常の生成に対し、呼び出し内だけは0.83倍とかえって遅く、AgSpec式は1.51倍。1回の採点に約180〜250msかかり、通常の1歩106msより重い

図10:実測の生成速度(通常の生成=1)。括弧内は 1 回の採点にかかった時間と、確かめた平均トークン数

方式実測の速さ1 回の採点の時間1 回に確かめた平均トークン数
通常の生成1.00 倍(毎秒 9.4 トークン)106 ms1.0
① 呼び出し内だけ0.83 倍229 ms4.2
② +セッション1.16 倍198 ms5.1
④ +書き出す形の写し1.29 倍199 ms5.2
⑥ 長さ固定 321.08 倍247 ms12.0
⑦ AgSpec 式1.51 倍178 ms5.5

いちばん意外だったのは、①呼び出し内だけ(PLD 相当)が 通常の生成より遅くなったことです。原因は、Mac の上では「まとめて確かめる」のがタダではないことでした。

21 回の呼び出しの記録から、1 回の採点の時間を「固定の部分」と「下書き 1 トークンあたりの部分」に分けて推定すると、下書き 1 トークンにつき約 15.5 ms かかっていました。通常の 1 歩が 64〜119 ms(混み具合で変動)なので、下書きを 1 つ足すごとに 1 歩の 13〜25% ずつ時間が延びる計算です。念のため、モデル全体ではなく 4bit の行列の掛け算 1 つだけを測っても、1 行で 0.50 ms、4 行で 0.91 ms、16 行で 1.06 ms と、1 行のときの 2 倍ほどかかりました。

確かめるトークン数を増やすと1回の時間が延びる。同時1件でも16トークンで数倍、同時16件では64トークンで12倍まで延びる

図11:まとめて確かめるトークン数と 1 回の時間(1 トークンのときを 1 とした倍率)。GPU を別の作業と共有した状態で 15 回測った最小値なので、細かな凸凹は誤差を含む目安

データセンター向けの GPU でバッチ 1 なら、数トークン増やしても時間はほとんど変わらない、というのが投機的デコーディングの前提です。ところが今回の Mac(MLX の 4bit 計算)では、この前提が成り立っていませんでした。そのため、1 回で 1.78 トークンしか進まない①は、採点の重さに負けて遅くなったのです。

裏を返すと、採点が重い環境ほど、AgSpec の 2 つの工夫がそのまま速さの差になります。②〜⑤で写せる量を増やすと 1.1〜1.3 倍まで回復し、⑦で外れを減らすと 1.5 倍に届きました。⑥長さ固定 32 は、⑦と同じだけ進んでいるのに、1 回に平均 12 トークンも確かめるので 1.08 倍にとどまります。論文がバッチ 16 で示した「長い固定の下書きは、外れの採点で遅くなる」という現象が、Mac ではバッチ 1 から起きていた、と言えます。

結果⑤:損益分岐点 λ を手元の値にすると

論文の式では、損益分岐点 λ が上限 Kmax と倍率の目標 τ を決めます。今回の主な比較では、GPU の混雑で λ を安定して測れなかったため、論文の SWE-bench 設定と同じ τ = 0.95(λ = 0.05)を使いました。ところが上の推定からすると、この Mac の λ は 0.2〜0.3 程度です。

そこで λ = 0.2(τ = 0.8)で上限を決め直し、別の回にもう一度比べました。上限は 場所特定 2・コード 9・実行報告 7・確認 2 と、λ = 0.05 のとき(4・20・16・4)の半分ほどになります。

設定1 回で進んだトークン数外れた下書き(1 トークンあたり)1 回の下書き数実測の速さ(同じ回の通常の生成比)
⑦ λ = 0.052.521.174.481.51 倍
⑧ λ = 0.22.290.492.411.43 倍(同じ回の⑤は 1.13 倍)

外れはさらに 6 割減りましたが、1 回で進むトークン数も少し下がり、実測の速さはほぼ同じでした。混み具合の揺れを考えると、この 2 つの差ははっきりしません。言えるのは、λ を変えると「どれだけ攻めて下書きするか」が素直に変わり、どちらも固定長の⑤⑥より速いということです。λ は環境ごとに違うので、使う機材で測ってから決めるのが筋です。

結果⑥:出力は通常の生成と一致したか

投機的デコーディングは原理上は出力を変えないはずですが、実際には 21 回の呼び出しのうち、通常の生成と最後まで完全に一致したのは方式ごとに 13〜15 回でした。残りは途中のどこかで 1 トークン食い違い、その後の文章が少し変わっています。

これがバグなのか数値の問題なのかを切り分けるため、下書きを使わずに「記録された正解の出力を、k トークンずつまとめてモデルに通し、各位置でモデルが選ぶトークンが記録と同じか」を調べました(numerics.py)。

まとめる数 k食い違った位置(2,490 トークン中)
1(通常の生成と同じ計算)0
410
168

下書きの仕組みを一切通さなくても、まとめて計算するだけで 0.3〜0.4% の位置で選ばれるトークンが入れ替わりました。1 トークンずつと複数トークンまとめてでは、GPU の計算の順序が変わり、丸め誤差がわずかに違います。最有力の候補と 2 番手の確率がほぼ同じ場所では、その差で順位が入れ替わるのです。

つまり、今回のずれは投機のバグではなく数値の揺れです。論文は vLLM の貪欲法で同じ出力が保たれる前提で書かれていますが、実際の環境では、まとめて計算することによるわずかな揺れはあり得ます。厳密な再現性が必要な用途では、この点を自分の環境で確かめておくと安心です。

実務への示唆:どこで、どう使えるか

いま使える検索型の投機的デコーディング

AgSpec そのものはまだ公開されていませんが、土台になっている検索型の仕組みは、主要な推論サーバーですでに使えます。たとえば vLLM の公式ドキュメント(Speculative Decoding のページ、2026-10-04 確認)には、プロンプトから続きを探す ngram と、過去の回答を木構造にためて探す suffix(SuffixDecoding)が載っています。どちらも追加のモデルなしで、起動時の設定 1 つで有効にできます。

# 例1:プロンプト内の n-gram から下書きを作る(PLD と同じ考え方)
vllm serve <TARGET_MODEL> \
  --speculative-config '{"method": "ngram", "num_speculative_tokens": 4,
                         "prompt_lookup_min": 2, "prompt_lookup_max": 5}'

# 例2:過去の回答もためて探す SuffixDecoding(AgSpec-Suffix の土台)
vllm serve <TARGET_MODEL> \
  --speculative-config '{"method": "suffix", "num_speculative_tokens": 8,
                         "suffix_decoding_max_tree_depth": 24,
                         "suffix_decoding_max_spec_factor": 1.0}'

設定のキーや既定値はバージョンによって変わることがあるので、使う前に手元の vLLM のドキュメントで確認してください。この記事では vLLM での動作は確かめていません。

AgSpec の論文が示しているのは、こうした「エンジン」はそのままに、セッション単位の記録と、開いたファイルの書き出す形の写しを渡し、係ごとに下書きの長さを変えるだけで、さらに速くなる余地があるということです。エージェントの基盤側(どのファイルを開いたか、どの係の呼び出しか)と、推論サーバー側(下書きを作る仕組み)をつなぐ情報の受け渡しが、実装上の鍵になりそうです。

量子化・キャッシュとは削るものが違う

量子化はモデルの重み、KVキャッシュとプレフィックスキャッシュは入力の読み直し、検索型の投機は出力の1トークンずつの往復を削る。セッション記録の持ち方と、他のセッションから見えないようにする注意を右に示す

図12:推論の速さを上げる手段は、削るものがそれぞれ違うので重ねて使える

コーディングエージェントを大量に動かす立場から見ると、速さを上げる手段は 3 層に分けて考えると整理しやすくなります。

  • 量子化:重みを 4bit などに小さくし、1 回の計算で読む量を減らす。品質とのかね合いがある
  • KV キャッシュ・プレフィックスキャッシュ:同じ前置き(システムプロンプトや前の会話)の計算結果を使い回し、入力の読み込み(プレフィル)を省く
  • 検索型の投機:出力のうち写しの部分を先回りして、1 回の計算で何トークンも進める

エージェントは入力も出力も長く、しかも前の呼び出しと重なりが大きいので、入力側のキャッシュと出力側の投機の両方が効きやすい仕事です。どちらも「同じセッションの記録を賢く使い回す」という点で発想が共通しています。

記録の扱いには注意が要る

セッションやワークスペースのコーパスには、ユーザーのプロンプトやリポジトリの中身、ツールの出力がそのまま入ります。論文も倫理に関する記述の中で、各セッションのコーパスへのアクセスを制限し、他のセッションに漏れないようにし、終わったら捨てるべきだと書いています。

特にグローバルコーパスは、全ユーザーで共有する前提の仕組みです。投機的デコーディングは出力の中身を変えないので、別のユーザーの文章が出力に紛れ込むことはありません。ただ、下書きが当たるかどうかで返答の速さが変わるため、「ある文章がコーパスに入っているか」を速さの差から推し量られる余地はあると筆者は考えています(論文はこの点を扱っていません)。社内のコードや顧客のデータを含む記録は、共有のコーパスに入れない方が安全です。論文がワークスペースを G に混ぜずに分けている理由の 1 つも、ユーザーごとにリポジトリが違う実運用を想定したものでした。

つまずきやすい点:自分の環境で試す前に

Q1 出力に写しが多いか、Q2 KVキャッシュを巻き戻せるモデルか、Q3 前の呼び出しの記録を探せるか、Q4 バッチが大きいか・採点が重い環境か、を順に確認し、いいえの場合の理由と対処を右に示す

図13:検索型の投機が効きそうかを確かめる順番

今回の検証で実際に詰まった点と、その対処をまとめます。

症状原因対処
下書きの仕組みを入れても「巻き戻せない」と言われる線形注意などの層を含む混成モデルは、KV キャッシュを末尾から削れないふつうの注意だけのモデルを使う。または状態を保存して戻す仕組みのあるエンジンを使う
出力が通常の生成と少し違うまとめて計算したときと 1 トークンずつ計算したときで、数値の丸めが変わる想定どおりの揺れか、1 トークンずつ/まとめての比較で確かめる(結果⑥)
Devstral で「正規表現が誤っている」と警告が出る配布されているトークナイザー設定の分割規則の不具合transformers で読み込むときに fix_mistral_regex=True を付ける
モデルの書いた diff が patch でも git apply でも当たらない関数の間の空行が 1 行足りない、行番号の見出しがずれている、追加行に「+」を付け忘れる、など細かな誤り行番号・空行のずれと、ファイルにない文脈行を許して当てる方法を併用(agent.py の lenient_apply)
ダウンロードが途中で止まる回線や同時ダウンロードの影響curl -C - で再開できる取り方に切り替える
書き出す形の写しがほとんど使われないプロンプトにファイル全文が入っていて、セッション側で先に一致してしまう正常。ファイルがプロンプトから切り落とされる長いリポジトリで効く

まとめ

AgSpec は、「コーディングエージェントの出力は、どこかで見た文章の写しが多い」という観察を、推論の速さに変える研究でした。要点を振り返ります。

  • 投機的デコーディングは、下書きをまとめて採点することで、出力を変えずに生成を速くする仕組み。検索型は、既にある文章から下書きを写す
  • AgSpec の工夫は 2 つ。どこから写すか(セッション・ワークスペース・グローバルの 3 つ、ファイルは diff の書き出す形でも登録)と、何トークン写すか(係ごとの上限と、採点結果で毎回調整する倍率)
  • 論文では、通常の生成の最大 4.37 倍(バッチ 1)・4.76 倍(バッチ 16)。効果の大半はセッション全体の記録を探せるようにしたことから来ていた
  • 手元の半分実装でも同じ傾向が出た。セッションの記録で 1 回あたり 1.78 → 2.16 トークン、書き出す形の写しでコード係の 1 ターン目が +32%、長さ制御で外れた下書きを 3 分の 1 に減らした
  • ただし Mac(MLX)では採点がタダではなく、呼び出し内だけを探す方式はかえって遅くなった。写せる量を増やし、外れを減らす AgSpec の 2 つの工夫が、そのまま速さの差になった

次に試すなら、vLLM の suffix や ngram の投機を GPU サーバーで有効にして、自分のエージェントの記録で受理長を測ってみるのがおすすめです。今回の bench.py のように「記録を再生して比べる」形にしておくと、条件をそろえた比較がしやすくなります。

参考リソース

  • Sumin Lee, Sukmin Cho, Suengjae Lim, Youngjin Kwon「AgSpec: Pushing the Limits of Retrieval-Based Speculative Decoding in Coding Agent Pipelines」arXiv:2610.01108(2026-10-01)https://arxiv.org/abs/2610.01108
  • vLLM ドキュメント「Speculative Decoding」https://docs.vllm.ai/en/latest/features/speculative_decoding/
  • Prompt Lookup Decoding(Apoorv Saxena)https://github.com/apoorvumang/prompt-lookup-decoding
  • SuffixDecoding(Oliaro ほか、NeurIPS 2025)、SAM-Decoding(Hu ほか、ACL 2025)、REST(He ほか、NAACL 2024)。いずれも論文の参考文献より
  • mlx-lm(Apple の MLX で LLM を動かすライブラリ)https://github.com/ml-explore/mlx-lm
  • 使ったモデル:mlx-community/Devstral-Small-2507-4bit https://huggingface.co/mlx-community/Devstral-Small-2507-4bit

PR

生成AIを体系的に学びたい方へ

「DMM 生成AI CAMP 学び放題」は、ChatGPTなどの生成AIを学べる月額制のオンライン学習サービスです。仕事への活用に向けて継続的に学びたい方は、公式サイトでコース内容や入会条件をご確認ください。

DMM 生成AI CAMP 学び放題

リンク先は公式サイトです。

AIエージェントAI最新情報LLM
Takuyaをフォローする