入力処理は速く、生成は遅い

IQuest-Coder-V1-40B-Loop-Instruct を aider で使うと、入力の処理は速い一方、生成は 0.6〜8 tok/s でした。編集を繰り返す用途では、出力を待つ時間が長くなります。

同時期の別の構成では、IQuest-Coder-V1-40B-Instruct-nvfp4 が 25〜28 tok/s を出していました。同じ 40B クラスでも結果が異なるため、編集形式や設定を分けて考えます。

編集形式と待ち時間

aider では、修正を確認して次の指示を出す作業を繰り返します。1 回の待ち時間が長いと、そのたびに作業が止まります。

当時のメモでは、次のように評価していました。

Prefill は十分速いが、decode が致命的に遅い。

vLLM の速度とキャッシュ使用量

  • Prefill(入力処理)は数百〜900 tok/s でした。
  • Decode(生成)は 0.6〜8 tok/s でした。
  • GPU や KV cache の不足を主因とは見ていません。
  • aider で編集を繰り返すには、生成の待ち時間が長い状態でした。

vLLM のログにも、生成速度の低さが出ています。

  (APIServer pid=1) INFO 01-09 19:15:06 [loggers.py:257] Engine 000: Avg prompt throughput: 0.0 tokens/s, Avg generation throughput: 8.0 tokens/s, Running: 1 reqs, Waiting: 0 reqs, GPU KV cache usage: 12.2%, Prefix cache hit rate: 6.3%
(APIServer pid=1) INFO 01-09 19:15:16 [loggers.py:257] Engine 000: Avg prompt throughput: 0.0 tokens/s, Avg generation throughput: 8.0 tokens/s, Running: 1 reqs, Waiting: 0 reqs, GPU KV cache usage: 12.5%, Prefix cache hit rate: 6.3%
(APIServer pid=1) INFO 01-09 19:16:16 [loggers.py:257] Engine 000: Avg prompt throughput: 251.4 tokens/s, Avg generation throughput: 4.0 tokens/s, Running: 1 reqs, Waiting: 0 reqs, GPU KV cache usage: 7.0%, Prefix cache hit rate: 6.4%
(APIServer pid=1) INFO 01-09 19:17:06 [loggers.py:257] Engine 000: Avg prompt throughput: 73.4 tokens/s, Avg generation throughput: 1.1 tokens/s, Running: 0 reqs, Waiting: 0 reqs, GPU KV cache usage: 0.0%, Prefix cache hit rate: 6.8%
  

KV cache usage は 7.0%〜13.8% で、キャッシュを使い切ってはいませんでした。Prefix cache hit rate は 6.3%〜6.8% です。この値だけでは速度差を説明できず、生成側を調べる必要があります。

遅さに関係する条件

メモでは、次の 4 つを原因の候補として挙げていました。

1. 40B モデルで whole edit を返す

whole edit はファイル全体を返すため、小さな修正でも生成するトークン数が増えます。生成速度が低いと、その分だけ待ち時間が長くなります。

このとき返された Django モデルの一部です。

  from django.db import models
from django.utils.translation import gettext_lazy as _
from core.models import TimeStampedModel, SoftDeleteModel


class Comment(TimeStampedModel, SoftDeleteModel):
    class Status(models.TextChoices):
        PENDING = 'pending', _('Pending')
        APPROVED = 'approved', _('Approved')
        SPAM = 'spam', _('Spam')
        TRASH = 'trash', _('Trash')

    site = models.ForeignKey('core.Site', on_delete=models.CASCADE)
    post = models.ForeignKey('content.Post', on_delete=models.CASCADE)
    parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True)
    author_user = models.ForeignKey('auth.User', on_delete=models.SET_NULL, null=True, blank=True)
    author_name = models.CharField(max_length=255)
    author_email = models.EmailField()
    author_url = models.URLField(blank=True)
    body = models.TextField()
    status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING)
    ip_hash = models.CharField(max_length=64)
    user_agent = models.TextField(blank=True)

    class Meta:
        indexes = [
            models.Index(fields=['post', 'status', 'created_at']),
        ]

    def __str__(self):
        return f"Comment by {self.author_name} on {self.post}"

    def approve(self):
        self.status = self.Status.APPROVED
        self.save(update_fields=['status'])

    def mark_as_spam(self):
        self.status = self.Status.SPAM
        self.save(update_fields=['status'])

    def move_to_trash(self):
        self.status = self.Status.TRASH
        self.save(update_fields=['status'])

    def restore(self):
        self.status = self.Status.PENDING
        self.save(update_fields=['status'])

    def is_approved(self):
        return self.status == self.Status.APPROVED
  

まとまったコードは返せています。ただし、生成速度が 1 桁 tok/s では、この長さの出力を毎回待つことになります。

2. 文脈が大きい

repo-map と複数ファイルを渡すと、モデルが参照する情報が増えます。メモでは、repo-map や /add 対象を減らす案を挙げていました。

文脈が大きいと説明や差分も長くなりやすく、生成の遅さが目立ちます。

3. サンプリングを使っている

生成設定の変更案は、temperature=0 にして greedy decoding を使うことです。毎回、確率が最も高いトークンを選ぶ方法です。コード編集では、返答の再現性と早く出力を終えることを優先する案です。

4. 40B クラスの単発生成

40B クラスは、1 リクエストずつの生成に向かない可能性もあります。ただし、別の nvfp4 構成では 25〜28 tok/s が出ています。モデルの大きさだけでなく、編集形式、文脈、生成設定をあわせて見る必要があります。

メモに挙げた変更案

変更案は次の順番でした。

  1. whole edit をやめて diff/patch 形式にする
  2. temperature=0 で回す
  3. repo-map や /add 対象を減らす
  4. max_model_len を必要最小にする
  5. 必要なら、量子化版や軽いコード特化モデル(20B〜32B)に変更する

最初の 1 と 2 は、出力を短くし、生成時の選択を単純にする案です。入力処理より、生成にかかる時間を減らすことを優先しています。

別の nvfp4 構成の結果

Firworks-IQuest-Coder-V1-40B-Instruct-nvfp4.md には、次の値が記録されていました。

  • Prompt throughput: 1100〜2300 tok/s
  • Generation throughput: 25〜28 tok/s
  • KV cache usage: 2〜12%
  • Prefix cache hit rate: 20〜45%

25〜28 tok/s なら、200 tokens の出力は数秒台です。今回の 0.6〜8 tok/s では、同じ長さでも待ち時間が大きく変わります。

この比較は、同じ条件で測ったモデル順位ではありません。aider での使いやすさは、出力形式、文脈の量、生成設定にも左右されます。

今回の結果

今回の 40B + whole edit では、低い生成速度 (t/s) と長い出力が重なりました。入力処理の速さだけでは、編集の待ち時間は決まりません。