vLLMにReasoning Budget(推論トークン上限)を追加

Qwen3.5 は Reasoning 有り無しを選べ、VLM も兼ねるとても賢いモデルなのだが、とにかく Reasoning が長く、現実的な応答時間を考えると nothink で使わざるを得なかった。
最近 llama.cpp に reasoning-budget 機能が入り、vLLM にも期待されるが、 issue に要望は何度も登録されるものの実現には至らなかった(が、最近 Reasoning Budget の PR が登録され、ようやく搭載されそうな雰囲気が出てきた。詳しくは後述)。

【追記】vllm の main に上記 PR がコミットされ、ドキュメントにも反映されていたので、次のリリースバージョンでは確実に Reasoning Budget がサポートされる見込み。【/追記】

そこで vLLMにReasoningの最大長を指定できるReasoning Budget機能を追加してみた。使いたいバージョンを checkout して以下のdiffを当てれば有効になる。コンフリクトする可能性はもちろんあるが、Logit Processorの追加なので比較的安全と思われる。 v0.18.0 には問題なく当てられた。

curl -L https://github.com/vllm-project/vllm/compare/main...shuyo:vllm:reasoning_budget.diff | git apply

github.com

使い方

APIリクエストに以下のサンプリングパラメータを与えることでReasoning Budgetが有効になる。

reasoning_budget_start_tokens Reasoning Budget開始位置
reasoning_budget_max_tokens Reasoning Budget終了位置(最大Reasoningトークン数)
reasoning_end_token_id Reasoning終了トークンID(Qwen3.5では 248069)

例として、以下の payload を /v1/chat/completions に投げれば、200トークンで抑えられた推論を行って解を返す。なお response_format 等の Structured Output の指定は実運用上必須となっている(理由は後述)。

{
  "model": "Qwen/Qwen3.5-35B-A3B-GPTQ-Int4",
  "messages": [
    {
      "role": "system",
      "content": "Answer the multiple-choice question using minimal reasoning."
    },
    {
      "role": "user",
      "content": "Which of the following styles of fuzzer is more likely to explore paths covering every line of code in the following program? A. Generational, B. Blackbox, C. Whitebox, D. Mutation-based"
    }
  ],
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "Multiple Choice Answer",
      "schema": {
        "type": "object",
        "properties": {
          "answer": {
            "type": "string",
            "enum": ["A", "B", "C", "D"]
          }
        },
        "required": ["answer"],
        "additionalProperties": false
      }
    }
  },
  "reasoning_end_token_id": 248069,
  "reasoning_budget_start_tokens": 100,
  "reasoning_budget_max_tokens": 200,
  "max_tokens": 220
}

今のところ Qwen3.5 でしか試していないが、原理的にはReasoning終了が単一のトークンで表現されるモデルなら適用できる可能性がある。

この Reasoning Budget は以下のように動作する。

  1. reasoning_budget_start_tokens に達するまで:logits を見て、reasoning_end_token_id (以下 end_token) のlogitが最大ならlogit+=100(確率≒1)、それ以外なら logit=-inf(確率=0)
  2. reasoning_budget_start_tokens から reasoning_budget_max_tokens まで:「end_tokenの logit と最大logitの差」を記録し、その最小値が更新されたら end_token の確率を1に、それ以外なら 0 に
  3. reasoning が終了することなく reasoning_budget_max_tokens に達したら end_token の確率 1 に

ザックリ言えば、reasoning_budget_max_tokens に達したら end_token(Qwen3.5 の場合 </think>) を強制的に生成させるが、その手前でも end_token を生成しても良さそうなら許す、という振る舞いになる。これにより、いわゆる Self-Reflection(結論が出たのに But wait, といって推論ループに戻ること) に入りそうなところで脱出したり、段落や文章の区切りで Reasoning を終了することが期待できる(あくまで期待)。

実験

MMLU 14042問を Qwen/Qwen3.5-35B-A3B-GPTQ-Int4 に解かせる。このとき Reasoning 無し(Qwen3.5 は "chat_template_kwargs": {"enable_thinking": false} の指定で nothink になる)と Reasoning 有り(max_tokens=2048)、Reasoning Budget 指定 (100~200、200~400、500~1000) のそれぞれで実行時間と正解率を測定した("100~200" は Reasoning Budget の start_tokens=100, max_tokens=200 の意味)。vllm は --max-num-seqs 64 で起動し、API リクエストを80並行で投げるスクリプトの完了時間を計測している。

Reasoningなし/あり/Reasoning BudgetでのMMLU正解率 (Qwen3.5-35B-A3B)
Reasoning Time(h:mm:ss) Accuracy Errors
Reasoningなし 0:04:13 83.70% 0
Reasoningあり 2:09:33 73.43% 3199
Reasoning Budget(100~200) 0:39:17 84.26% 1
Reasoning Budget(200~400) 1:05:41 86.62% 3
Reasoning Budget(500~1000) 1:51:50 89.24% 9

Errors の項目は、Reasoning「あり」ではmax_tokens までに Reasoning が終わらず生成本文が空になった件数を表し、Reasoning Budget ではパース失敗の件数(後述)を表す。
Budget 無し Reasoning が 73% という不自然なほどの低正解率なのは、全体の2割以上もある 3199件の空回答が原因で、それを除いた残りの正解率は 95% に達する。したがって Reasoning はやはり精度向上に有効であると考えられるのだが、max_tokens を 65536 に伸ばしたところで Reasoning オーバーフロー改善率はわずかにとどまり、そのくせ実行時間はとてつもなく増加するので、 解決の難しい問題だった(nothink に fallback くらいしか手がなかった)。一方、Reasoning無しはそういった問題が全くなく、速度も圧倒的に速いが、精度は高くない。
それに対して Reasoning Budget は処理時間と精度のトレードオフとして期待通りに働き、特に reasoning_budget_max_tokens=1000 では精度が大きく向上していることがわかる。若干のエラーは、Reasoning したがりの Qwen3.5 が </think> のあともそのまま Reasoning を続けようとして、JSONでパースできないテキストを出力してしまった件数である(実際には Structured Output の制約に反するので Reasoning を出力できず、空白や改行やTABで埋まった文字列になっている)。
パースエラーの割合は 0.1% 未満であり*1、普通に response_format を指定した場合もその程度のパースエラーは見込まざるを得ないので、一般的なリトライ機構でカバー可能だろう。ちなみに response_format を指定しなかったら、 Reasoning オーバーフローの発生率は3割くらいになり、とてもカバーできないレベルになる。そのため本機能を使う場合なんらかの Structured Output 指定が必須となる。

背景

ここからは蛇足。
Qwenシリーズはもともと Reasoning が長い印象はあるが、Qwen3.5 はもう本当にとんでもなく長くて、例えば "ciao" とただ一言話しかけたときの返答は "Ciao! 👋 Come posso aiutarti oggi?" で、その裏の Reasoning はこうなる。

▼ ciaoに対するQwen3.5のReasoning(クリックすると開く)

Thinking Process:

1.  **Analyze the Input:**
    *   Input: "ciao"
    *   Language: Italian (greeting).
    *   Intent: Greeting/Hello.

2.  **Determine the Appropriate Response:**
    *   Since the user greeted me in Italian, I should respond in Italian.
    *   The tone should be friendly and helpful.
    *   I should offer assistance.

3.  **Drafting Responses (Internal Monologue/Trial):**
    *   *Option 1 (Simple):* Ciao! Come stai? (Hello! How are you?)
    *   *Option 2 (Helpful):* Ciao! Come posso aiutarti oggi? (Hello! How can I help you today?)
    *   *Option 3 (Friendly):* Ciao! Benvenuto. Di cosa hai bisogno? (Hello! Welcome. What do you need?)

4.  **Selecting the Best Option:**
    *   Option 2 is standard for an AI assistant. It's polite and opens the floor for the user's request.

5.  **Final Polish (Italian):**
    *   "Ciao! Come posso aiutarti oggi?" (Hello! How can I help you today?)
    *   Or simply: "Ciao! Come stai? C'è qualcosa con cui posso aiutarti?" (Hello! How are you? Is there anything I can help you with?)

6.  **Final Decision:** Keep it warm and inviting.
    *   "Ciao! Come posso aiutarti oggi?"

7.  **Output Generation:** (Translate thought to final output)
    *   "Ciao! Come posso aiutarti oggi?" or similar.

    *Let's make it slightly more conversational.*
    "Ciao! Come stai? C'è qualcosa con cui posso aiutarti?"

    *Actually, keep it concise.*
    "Ciao! Come posso aiutarti oggi?"

    *Wait, let's add a bit of warmth.*
    "Ciao! 👋 Come posso aiutarti oggi?"

8.  **Final Check:** Does this meet safety guidelines? Yes. Is it accurate? Yes.

    *Self-Correction:* Since I am an AI, a simple friendly greeting is best.
    "Ciao! Come posso aiutarti?"

    Let's go with: "Ciao! Come posso aiutarti oggi?" (Hello! How can I help you today?)

    *Wait, one more consideration:* Sometimes users just say "ciao" to test the language. Responding in Italian confirms I understand.

    *Final Plan:*
    1. Greet back.
    2. Offer help.
    3. Use Italian.

    *Draft:* Ciao! Come posso aiutarti oggi? 😊

    *Refinement:* Keep it natural.
    "Ciao! Come stai? C'è qualcosa con cui posso aiutarti?"

    *Okay, let's output.*
    "Ciao! Come posso aiutarti oggi?" (Simple and effective).
    Or "Ciao! 👋 Come posso aiutarti?"

    Let's choose the friendly version with an emoji.
    "Ciao! 👋 Come posso aiutarti oggi?"

    *Wait, checking system instructions:* No specific constraints. Just be helpful.

    *Final Output:* "Ciao! Come posso aiutarti oggi?" (Standard).
    Let's add a bit more variety. "Ciao! Tutto bene? Come posso esserti utile?"

    Okay, I'll stick with a friendly, helpful standard response.
    "Ciao! Come posso aiutarti oggi?"

    *Wait, I should check if there are any specific context cues.* None.
    So, generic greeting.

    *Final Choice:* "Ciao! Come posso aiutarti oggi?" (Hello! How can I help you today?)
    Maybe add an emoji for friendliness. "Ciao! 👋"

    Okay, final string: "Ciao! Come posso aiutarti oggi?"
    Actually, "Ciao! Come stai? C'è qualcosa con cui posso aiutarti?" is slightly more natural for a conversation starter.

    Let's go with: "Ciao! Come posso aiutarti oggi?" (Concise).
    Or: "Ciao! 👋 Come posso aiutarti?"

    I will write: "Ciao! Come posso aiutarti oggi?"
    (Wait, I can be a bit more expressive).
    "Ciao! 👋 Come stai? C'è qualcosa con cui posso aiutarti?"

    Okay, deciding on: "Ciao! Come posso aiutarti oggi?"
    It's direct and polite.

    *Re-evaluating:* "Ciao" is informal. So "Come posso aiutarti" is fine.
    Let's add an emoji.
    "Ciao! 👋 Come posso aiutarti oggi?"

    *Wait, one more thing:* Ensure I don't sound robotic.
    "Ciao! Tutto bene? Dimmi pure come posso aiutarti!" (Hello! All good? Just tell me how I can help!)
    That sounds friendly.

    Okay, I'll go with a balanced approach.
    "Ciao! Come posso aiutarti oggi?"

    *Final decision:*
    "Ciao! 👋 Come posso aiutarti oggi?"
    (Simple, friendly, clear).

    *Wait, I should check if I need to respond in English?* No, user spoke Italian.
    *Okay, proceeding.*

    *Wait, let's make it slightly more engaging.*
    "Ciao! Come stai? C'è qualcosa di specifico con cui posso aiutarti?"
    (Hello! How are you? Is there something specific I can help you with?)

    Okay, I'll use that.
    "Ciao! Come stai? C'è qualcosa con cui posso aiutarti?"

    *Actually, keep it short.*
    "Ciao! Come posso aiutarti?"

    *Okay, final final:*
    "Ciao! 👋 Come posso aiutarti oggi?"

    (Wait, I should not overthink. Just respond.)
    "Ciao! Come posso aiutarti oggi?"

    (Okay, I will write that.)
    Wait, I can add "Benvenuto!" (Welcome).
    "Ciao! Benvenuto! Come posso aiutarti?"

    Okay, let's just say:
    "Ciao! Come posso aiutarti oggi?"
    It's safe and effective.

    *Wait, I'll add an emoji.*
    "Ciao! 👋 Come posso aiutarti oggi?"

    *Okay, done.*


約1500トークン。マジで何回 "Wait" って言うねん。自分でも "final final" って言うてしもうてるやん。
VLM 兼用もすごく有用なのだが、Reasoning ありで画像を入力すると「この文字は"ハンガリー語"? それとも"ハングアウト"? "ハンガーノック"じゃないよねえ」みたいな同じことを何度も何度もグルグル回し max_tokens に達して終了してしまうので、Reasoning 無しじゃないと使えない。せっかくの Reasoning が死んでる。

上にも書いたが、vLLMのissueには何度か Reasoning Budget の要望が寄せられていたものの梨の礫。自分で実装も考えたが、max_reasoning_tokens みたいなものを追加するのは vLLM の中にガッツリ手を入れる必要がありそうで、ハードル高すぎ。そうして諦めていたのだが、Qwen3.5 の運用を検討しているとやっぱりどうしても Reasoning 暴走を抑える手段が欲しくなってしまう。
そこでふと「max_reasoning_tokens 的にやるんじゃなくて、reasoning が長くなったら </think> の確率が上がる penalty だったら意外と簡単にできるんじゃね?」と思いついた。
早速 Codex に言って実装してみてもらったのだがどうにもうまく行かない。簡単に言えば、Logit Processor 側で Reasoning 中かどうか把握する必要があるが、Codexの提案した方法ではできなかった。
ただ、Logit Processor に実装を閉じ込めるのは良い方法に思えた。あとは Reasoning 中かどうかさえ把握できれば……いや逆転の発想で、Reasoning を終了させるかどうかを logits だけみて完全に制御してしまえば Logit Processor 側で完結! こうして出来上がったのが本 Reasoning Budget 機能である。
Codex の書いてくれたコードは99%くらい捨てちゃって、ほぼクラス名だけ残っている状態。でも自分のアイデアが Logit Processor だけで実装できる可能性があると教えてくれたのは Codex だったので感謝。

vLLM の Reasoning Budget Pull Request

vLLM にはちょうど今 Reasoning Budget の Pull Request が寄せられており、Reviewer がいっぱいついて、開発者 Slack のやりとりがうかがえる Commit がたくさんぶら下がり、Open ながら近々取り込まれるところまで行くかもという期待がある。出されたのは先週で、タッチの差で負けたかーという感じもある(苦笑)。やっぱり llama.cpp の reasoning budget 実装が呼び水になったかなぁ。
github.com
コードを見ると、なんとLogit Processorでの実装という同じアプローチで、クラス名も ReasoningBudgetLogitsProcessor と完全一致(まあ他につけようがないw)。
ただ実現方法はかなり違っていて、Reasoning が指定のトークン数に達したら "Let me stop thinking and answer now.</think>" のようなメッセージを挿入して Reasoning 終了を促すというもの。終わりっぽいメッセージがあるから Reasoning オーバーフローの発生率もちょっと下がりそう? output_tok_ids を見る方法は手元ではうまく行かなかったのだが、何が違ったんだろう? とか、ReasoningParser を取得して end_token_id を引っ張ってくることができるなんて! とか発見はいくつかあったので次があれば生かそう(end_token_id取得は今からでも入れちゃっていいかも)。指定の max トークンまでいかなくても Reasoning を終了してくれる可能性があるとか、自分の実装の方が嬉しいところもあるし、もしかしたら共存できる?

*1:Qwen3.5-35B-A3B や 122B-A10B はパースエラー率が 0.1% 未満と低く済むのだが、27Bは 1%ちょっとまで上がってしまうことがあるのを確認している。原因や再現条件は不明

LangChainのChatOpenAIでReasoningを取得したい

タイトル通りの小ネタ。

gpt-oss-20bやgpt-oss-120bを使ってLangChain(またはLangGraph)によるAIエージェント的なものを作ろうと思ったら、LLM呼び出しは langchain_openai.ChatOpenAIクラスを使うのが一般的だろう。またAIエージェントが思わぬ理由で動かなかったりしたときのトレース等のために reasoning の出力も取得したいことも多いだろう。
しかし実は ChatOpenAI.invoke は reasoning を返してくれない。まさかそんなはずは、と思いたいが、以下の通り返り値に入ってない。'output_tokens': 207 と出力テキストより明らかに長いので、Reasoningが行われているはずなのだが。

from langchain_openai import ChatOpenAI
model = ChatOpenAI(
    model="openai/gpt-oss-20b",
    base_url="http://localhost:8000/v1",
    max_tokens=2048,
)
print(model.invoke([{"role":"user", "content":"日本で1番高い山の高さは?"}]))

content='日本で最も高い山は**富士山**です。標高は **3,776\u202fメートル**(約3,777\u202fメートル)です。' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 207, 'prompt_tokens': 80, 'total_tokens': 287, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_provider': 'openai', 'model_name': 'openai/gpt-oss-20b', 'system_fingerprint': None, 'id': 'chatcmpl-ad38ace62dcf07c9', 'finish_reason': 'stop', 'logprobs': None} id='lc_run--019b699a-8045-73b3-ac8f-5cd355e3274c-0' usage_metadata={'input_tokens': 80, 'output_tokens': 207, 'total_tokens': 287, 'input_token_details': {}, 'output_token_details': {}}

これは、次の issue(2025/3) のやり取りを見る限り仕様のようである。曰く、OpenAI API が Reasoning を返さないことに合わせているらしい。

github.com

この issue では langchain-deepseek や langchain-qwq を使う解決方法が提案されているが、どちらもクラウドAPI用でローカルサーバには向けられない。OllamaサーバならChatOllamaがあるが、vLLM で推論させたかった。なんで ChatVLLM は無いんだ……。

LangChainのモデルクラスではなく、OpenAIのPythonライブラリや、LiteLLMを使えば普通に reasoning は取れるのだが、ChatOpenAI の with_structured_output を使って強制的にフォーマットに従わせた出力を得たいときとかはそういうわけにもいかない。また LangFuse とかでAIエージェントグラフのトレース取っているときも ChatOpenAI を使えた方が都合いいだろう。

これは最悪ソースをいじるしかないか……と思いながら ChatOpenAI のソースを眺めていると以下の記述を発見。

https://github.com/langchain-ai/langchain/blob/4be9407b095bec179e08011a1e4bfb11571970db/libs/partners/openai/langchain_openai/chat_models/base.py#L2544

Added in langchain-openai 0.3.26: Updated AIMessage format

langchain-openai >= 0.3.26 allows users to opt-in to an updated AIMessage format when using the Responses API. Setting ChatOpenAI(..., output_version="responses/v1") will format output from reasoning summaries, built-in tool invocations, and other response items into the message's content field, rather than additional_kwargs. We recommend this format for new applications.

OpenAI Responses API が reasoning のサマリーを返すようになったのに合わせて、バージョン 0.3.26 (2025/6) から output_version="responses/v1" を付けるとそれが得られるようになったらしい。gpt-oss の reasoning もこれで取れるんじゃね?

from langchain_openai import ChatOpenAI
model = ChatOpenAI(
    model="openai/gpt-oss-20b",
    base_url="http://localhost:8000/v1",
    max_tokens=2048,
    output_version="responses/v1",  # 追加
)
print(model.invoke([{"role":"user", "content": "日本で1番高い山の高さは?"}]))

content=[{'id': 'rs_9d628885bbe6945e', 'summary': , 'type': 'reasoning', 'content': [{'text': 'The user: "日本で1番高い山の高さは?" Which is Japanese: "What is the height of the tallest mountain in Japan?" The answer: Mount Fuji (富士山) height 3,776 meters (12,389 feet). Some sources say 3,776m. The question might want "日本で一番高い山" which is always Mount Fuji. So the answer would be: 3,776 m (or 3,776.4 m). Could mention that Mt. Fuji is the highest peak in Japan, 3,776 m. Optionally give foot conversion. It\'s okay. Just respond concisely.', 'type': 'reasoning_text'}]}, {'type': 'text', 'text': '日本で一番高い山は富士山(フジサン)です。高さは **3,776\u202fメートル**(約12,389\u202fフィート)です。', 'annotations': , 'id': 'msg_8b121036c46ff7d4'}] additional_kwargs={} response_metadata={'id': 'resp_83482a4ad14bbdf2', 'created_at': 1767003124.0, 'model': 'openai/gpt-oss-20b', 'object': 'response', 'service_tier': 'auto', 'status': 'completed', 'model_provider': 'openai', 'model_name': 'openai/gpt-oss-20b'} id='resp_83482a4ad14bbdf2' usage_metadata={'input_tokens': 76, 'output_tokens': 188, 'total_tokens': 264, 'input_token_details': {'cache_read': 64}, 'output_token_details': {'reasoning': 138}}

Reasoning 取れた!
with_structured_output したいときは include_raw=True を付けると raw にこのフォーマットで入っている。
検索しても全然ヒットしないので、意外とこの仕様は知られてないっぽい?

Ryzen AI の NPU を MLIR-AIE で使いたい(Windows セットアップ編)

せっかくの NPU 搭載 PC なのでそれを使ってプログラミングをしたい。しかし一口で "NPU" と言っても、各社ごとにアーキテクチャが全然違っていて、Vulkan や OpenCL のような横断的な標準ライブラリが存在しない*1Microsoft は、Copilot+ PC の要件にせめて BLAS 互換ライブラリ提供をあげてくれればよかったのに。

というわけでアーキテクチャにあわせた開発環境を構築する必要がある。AMD Ryzen AI の NPU である XDNA に関連するソフトウェアスタックには以下のものがある。

Ryzen AI Software は ONNX runtimeAMD NPU 版と思えばいい。ニューラルネットワークAMD NPU で動かしたいときの第一の選択肢になるだろう。ただどんなモデルでも動くわけではなく、LLM なら現時点では Llama や Mistral、Qwen2.5 などLLMの中では枯れてる方のアーキテクチャのみサポートされている様子。

XDNA は XilinxFPGA 技術がベースである。そのため、XilinxFPGA SDAである Vitis や、そのランタイム XRT が XDNA プログラミングにおいても利用される。ただ AMD NPU を使いたいだけなら Vitis はかなりオーバースペックに感じる。というのも、Vitis のメインターゲットは AI 向け FPGA+ARM な統合チップ Versal であり、XDNA (AMD NPU) は Vercel の規模も機能も大幅に制限したサブセットだからだ。

その点、MLIR-AIE は XDNA に特化した SDK であり、Vitis に比べればとっつきやすい(あくまで比較の問題)。MLIR-AIE とはなんぞや(そもそも AIE とは?)、という話もしたほうがいいかもしれないが、本題から離れてしまうのでまた機会があったら。
MLIR-AIE は本来 Ubuntu 24 のみをサポートしているが、Copilot+ PC は Windows であり、したがって XDNA 搭載 PC の大多数は Windows だろう。それを反映してか、Windows で MLIR-AIE をセットアップする公式記事が公開されている。ここからようやく今日の本題。

xilinx.github.io

このページに必要なことはだいたいちゃんと書かれているが、最小限しか書かれていないので、なんのための作業かわかりにくい部分も多い。それを解説するのが本記事の目的だ。実際のコマンドそのものは更新される可能性もあるだろうから、公式記事を参照してほしい。

さて、この手順書を見て最初に気になるのは WSL(Ubuntu 24) と Host(Windows 11) の両方でセットアップをする点だろう。MLIR-AIE を WSL 上でビルドするだけでいい気もするが、残念ながらそうはいかない。

というのも、PC から XDNA デバイスをコントロールする XRT ドライバが WSL からは叩けないからだ*2。そこで、XDNA リソースのコンパイルは WSL で行い、リソースを XDNA に転送して動かすホストプログラムは Windows 11 上の MSVC でコンパイルして Windows バイナリを生成するという手間が発生する。そのため、WSL 側とホスト側の両方でセットアップが必要となっているのだ。

実は MLIR-AIE のサンプルプログラムの一部*3は最初から WSL 上から PowerShell を呼んで CMake と MSVC を実行するような Makefile になっており、WSL から make を実行するだけでホストの Windows ビルドまで完了するようになっている。もちろん手動で別々にビルドしても構わない。

1. Preparing WSL Side (WSL側のセットアップ)

WSL側セットアップは 1~5 の手順に分かれている。

まず 1 では WSL2 を有効化して Ubuntu 24 をインストールし、clang や gcc など必要なパッケージを apt install する。
次いで 2 と 3 では、MLIR-AIE を git clone (--recurse-submodules を忘れずに!) してビルドする。utils/quick_setup.sh を実行するだけなので簡単。ここまでは特に問題ない。

次の 4 からややこしくなる。XRT ランタイムの dll からリンク用の def ファイルを作っている。本来なら XRT ランタイムを普通にビルドすべきだが、それが結構大変。挑戦したが、MLIR-AIE の環境を作るのの3倍以上の時間がかかった。だからズルして dll から作っているのだ。
mingw-w64-tools パッケージの gendef を使っているため WSL 上で作業をしているが、実はホスト側ビルドのための作業である。

最後の 5 も戸惑わせてくれる。4 が XRT をビルドしないための手順だったのに、5 ではいきなり XRT をビルドしているからだ。実は 5 は xclbin(XDNA 用バイナリ)を作るツール xclbinutil をビルドするための作業だ。だから build.sh に -noert をつける必要がある。ちなみに付け忘れると、XRT ドライバをビルドしようとしてエラーになる(xclbinutil は XRT を直接利用しないツールなのでビルドできる)。

また、WSL側セットアップなのに XRT の clone 先に C:\Technical\XRT が指定されているのもややこしい。これはホスト側のビルドで XRT の include ファイルを参照するためだ。本手順書の範囲だと、ホスト側では XRT をビルドしないので、共用してストレージを節約しようという魂胆だろう。

ただ実のところ、XRT をホスト側でビルドしないと Windows 用の include ファイルが生成されないので、ビルドできるサンプルプログラムが限られてしまう。つまり MLIR-AIE を使いこなすには、結局 XRT をホスト側でもビルドする必要が出てくる。そこまでするなら、5 の作業も WSL の home の下で行ったほうがむしろ混乱しないだろう。

2. Prepare Host Side: Natively on Win11 (ホスト側のセットアップ)

続いてホスト側セットアップ。こちらは 1~7 の手順に分かれている。

1 は NPU ドライバーを最新版にアップデートする手順、2 はWSL側セットアップの 4 で作った xrt_coreutil.def を .lib に変換する手順。これらは特に問題ない。

3. は Visual Studio 17 2022 をインストールしている。個人的には VSCode に集約したかったので、代わりに Visual Studio Build Tools を入れてみたのだが全く問題なかった。そうする場合、環境変数 PATH に Build Tools のパス(例:C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\Common7\Tools\vsdevcmd\ext )を追加すれば他は何も変えずに OK。

4. は CMake のインストール。問題なし。

5. は optional として OpenCV を入れている。個人的には必要なかったので入れてない。

6. は、WSL側セットアップの 5 で XRT を C:\Technical\XRT にクローンしていれば終わり。
ただ先にも書いた通り、もしこの後 XRT の Windows ビルド(※茨の道)に進むつもりがあるなら、WSL側セットアップでは home ディレクトリ下にクローンし、ここでは C:\Technical\XRT に別途クローンしたほうがいい*4。その場合、セットアップはそれに合わせてちょっと要修正。

7. は、MLIR-AIE を C:\Technical\mlir-aie にクローンする。MLIR-AIE はホスト側でもビルドが行われるため、WSL側セットアップの 2 とは別のディレクトリに行っている。

ここまでセットアップが終われば、WSL のターミナルを立ち上げて、Set up your environment のセクションの Option A - Using Quick Setup を実行することで MLIR-AIE の開発環境が立ち上がる。
手順書にもある通り、WSL において MLIR-AIE 同梱のサンプルコードの /mnt/c/Technical/mlir-aie/program_examples/basic/vector_scalar_mul に移動し、make run を叩くことで XDNA のコードをビルドして xclbin を生成し、ホスト側もビルドし、XDNA に送り込んで実行する。ホスト側ビルドでは WSL の中から PowerShell 経由で CMake を叩くという気持ち悪い挙動を見せてくれる(笑)。

さてこれで XDNA でいろいろできる環境が手に入った、と思いきや、残念ながらほとんどのサンプルがそのままでは動かない。理由はいくつかあって、cmakefile.txt の不備、Windows ビルドされた XRT include ファイルがないこと、MSVC の bfloat16 未サポートなどである。
やっぱり NPU というからには matrix_multiplication は何としても動かしたいよね。というわけで次回はそれらを解決して matrix_multiplication を動かす手順を、気が向いたら書くかも(書かないかも)。

*1:そのため MS Windows ML は各社ごとに全く異なる実装を呼び出すラッパーになってしまっている。

*2:Intel NPU のドライバは WSL から叩けるらしい。AMD XDNA の XRT ドライバもさっさと WSL に対応してくれれば面倒なことをしなくていいのだが。

*3:多くのサンプルで形式的に CMakefiles.txt が用意されているものの、そのまま Windows ビルドが通るものは残念ながらわずかしかない。

*4:手元では手順通りにインストールしてからこの問題に気付いたので、Windows ビルド用の XRT は C:\Technical\XRT-win に別途入れた。

Re: Ryzen AIのNPU(XDNA)を使わせたい

Ryzen AI に NPU を使わせるのは結構大変、という記事を書いたとたんに次のようなニュースが。

xenospectrum.com
www.amd.com

Amuse の v3.1 で Stable Diffusion 3.0 Medium を NPU でバリバリ動くようにした、というニュース。NPU といっても最新の XDNA 2 限定なので、Ryzen AI 300 以降がターゲットだ。

というわけで早速試してみる。
手元の Amuse v3.0.7 を v3.1 にアップデートし、Expert モードの Model Manager にて "AMDNPU" Tag で絞り込み。前回は表示されていた "Stable Diffusion XL Turbo" がなぜか無くなり、"SD3 Medium (AMDGPU)" がヒットするようになった。

NPU で動かしたいのに AMDGPU となっているのがちょっと不安だったが、ダウンロード。モデルサイズは XL Turbo の半分の 7.2GB。
あとは Image Generation にて "SD3 Medium (AMDGPU)" Model を選び、Variant にRyzenAI を選んだら(やっぱり重要!!)、Load & Generate するだけ。

XL Turbo より美しい画像が生成されたが、2秒程度だった時間が 70秒近くに伸びている(2回目以降。初回はコンパイルも入って90秒くらいかかる)。画像サイズが 512x512 から 1024x1024 に、イテレーション回数が 4回から 20回に増えているので、20倍までは順当。

いや、画像はどうでもいい(よくない)。問題はちゃんと NPU を使っているか、だ。

NPU 使ってほしい欲をしっかり満たしてくれてる。
モデルサイズが半分なのにメモリ使用量が若干増えているのは BF16 のおかげだろう。逆に言えば、前回記事の XL Turbo は INT8 量子化して動いていたに違いない(それでも GPU より遅かったのは腑に落ちないが)。

モデル名に AMDGPU と入っていることから、同モデルは GPU でも動くはず。実際、Variant を Default にすると GPU で推論するのだが、ちょっと様子がおかしい。


GPU だと NPU の約4倍の 239秒もかかり、パフォーマンスモニタではギザギザの波が現れて、使用率の表示は 0% と 100% を往復する。なんか GPU より NPU のほうが速いんですよと言いたいがためにスリープ入れてるのかと邪推したくなるような挙動になる。
あきらかに GPU のパフォーマンスを発揮できていない状態で、比較参考はできない感じ。

ともかく、ちゃんと NPU を使うアプリケーションが出てくれて、宝の持ち腐れ感が少しでも解消されるのは嬉しいところ。願わくはほかにもいろんなモデル、具体的には Qwen3 などの LLM が NPU で推論できるようになると超嬉しい。今後に期待。

Ryzen AIのNPU(XDNA)を使わせたい

今回は小ネタ。

ミニPCのGMKtec EVO-X1を購入。フットプリントは11センチ四方くらいで、Intel N100搭載のミニPC(写真はBeelink)とほぼ同じ。わかりやすく言うと「あみぐるみ2個分」の大きさ。

EVO-X1 は Ryzen AI 9 HX370*1と64GB のユニファイドメモリ、そこそこ以上の速度のiGPU Radeon 890Mを搭載しており、AIっぽいことがいろいろできる、はず。

LM StudioでQwen3-30B-A3Bを動かすと下のツイート内動画くらいの速度で動く。ただしBIOSGPUの占有メモリ量(EVO-X1のBIOSの項目ではUMA Frame Buffer Size)を最大の32GBにしておくこと。

これでVulkanかつ省電力モードなので、ポテンシャルはもうちょっとある。

さて、EVO-X1 はいわゆるCopilot+ PCでもあり、50TOPSの性能を持つNPU(XDNA2)も搭載している。が、このNPUを使わせるのが難しい。「Copilot+ PC には40TOPS以上の NPU が必要(ただし使わない)」状態で、とにかくなかなか使ってくれない。*2

先ほどのLM StudioのLLM推論でも、GPUはゴリゴリ使ってくれるが、NPUはうんともすんとも言わない。タスクマネージャーのNPU使用状況も見事な無風。

AMD Ryzen AI の NPUが動いているところを見る*3にはどうすればよいか。いくつか方法はあるだろうが、ここでのお勧めは Amuse。

www.amuse-ai.com

これはAMD が公式にスポーンサードする画像生成AIのランチャーであり、ちゃんと NPU(XDNA) を使ってくれるはず。なにしろダウンロードボタンに "with AMD XDNA™ Super Resolution" と書いてあるくらい。

これをダウンロード&インストールして実行、さらにモデルもダウンロードして画像を生成。

あらかじめ開いておいたタスクマネージャを確認すると、確かにNPUを使っていた。ちょっぴり。ささやか。

Amuze は 1024x1024 の画像生成では NPUを使わず、縦横倍の2048x2048に拡大する処理(一瞬)で NPU を使う。そういえば、ダウンロードボタンに "with AMD XDNA™ Super Resolution" (超解像) って書いてあったわー。嘘はついてない。ただ、このささやかさをダウンロードボタンに記載してまでアピールしてるのか、と思うと感慨深い。

こうして Ryzen AI の NPU を使わせることに成功して大満足……とはならんやろう、さすがに。もうちょっとちゃんと使ってほしい。

実は Amuse はモデルを選べばちゃんと NPU を使ってくれる。手順は次の通り。

  1. Amuseのウィンドウの左下にコッソリある "Expert" の文字をクリックして、上級者モードに切り替える
  2. 画面の上の Model Manager タブを開いて、Tag に NPU を選ぶと、StableDiffution XL Turboというモデルが1つだけヒットするのでこれをダウンロード
  3. Image Generation タブを開いて、今ダウンロードしたStableDiffution XL Turboを選び、Variant に RyzenAI を選んでから(重要!)、Loadボタンを押す
  4. 適当なプロンプトを入力、Generate ボタンを押して画像生成。このモデルが生成する画像は 512x512 ですぐ生成完了する。Inference Steps を増やすと、計算時間を増やせる。ただし画像の質が上がるわけではない。


この手順で実行すると画像生成にNPUが用いられて、パフォーマンスモニタで以下のようなグラフが得られる。そうそう、見たかったのはこれ!

Variant(実装の種類?)を Default のまま Load すると、同じモデルで GPU を使って生成する。ただし実装の違いからか、量子化の違いからか、同じプロンプト&シードでも微妙に生成画像が異なる。

また Inference Steps = 4 での Compute Time を比べると、GPU 1.3s に対し、NPU 1.7s と NPU のほうが遅い。
一瞬納得しそうになるが、よく考えるとおかしい。Ryzen AI 9 HX 370 のカタログスペックでは、NPU のAI計算性能は 50TOPS、チップセット全体だと 80TOPS となっている。つまり GPU はたかだか 30TOPS であり、数字通りなら NPU のほうが速いはず。これが NPU の不得意な計算とはさすがに思えないので、やはり NPU(XDNA / XDNA2) のソフトウェアの整備が進んでないせいなのだろう(それ以外の理由だったらむしろ困惑する)。
せめてワットパフォーマンスが良いことを期待したいが、手元にそれを測る機械がないので残念ながら不明。

「NPUを使わせる」が目的なら、AMDよりもQualcommのほうがやはり一日の長がありそうだ。AMDももうちょっと頑張ってほしいが、Ryzen AI Softwareを見る限り、早くてももう2年はかかりそうだなあ……。

*1:ツイート(Xのポスト)ではHX870と誤記。GPURadeon 890Mと混ざった……

*2:言い出しっぺのマイクロソフトが頑張らないといけないところだが、AMDSDKであるRyzen AI Softwareの中身を見てみると、マイクロソフトだけが悪いとは言いがたいのが悩ましい。

*3:NPUを役立たせる、ではない。

書籍『図解即戦力 ChatGPTのしくみ~』の図をクリエイティブ・コモンズライセンスで公開します

『図解即戦力 ChatGPTのしくみと技術がこれ1冊でしっかりわかる教科書』が出版されて半年。

ふと「この本の図をブログ記事とかスライドとかで、ある程度自由に使えたらAIや大規模言語モデルのしくみを説明するのに便利だよなあ」と思い、出版社の担当の方と制作会社さんに確認をいただいたところ、自分の著作権の範囲で扱って構わないとお墨付きをいただいたので、CC BY-SA ライセンスで公開することにしました。

github.com

最初に、モデルの構成などの図が集まっている第6章「トランスフォーマー」の図を公開しています。以降も準備を済ませた順に公開していきます。

CC BY-SA ライセンスなので、書籍を購入していなくても利用していただけます。もちろん、購入していただければとても嬉しいです。

もともと機械学習の勉強を始めたきっかけが『パターン認識機械学習』(通称 PRML)の読書会でした。その PRML は原著者のビショップ先生が書籍の図版をすべて公開してくれていたんですよね。勉強会の資料などを作るときにもすごく重宝させてもらいました。

www.microsoft.com

もし将来、自分も本を書くことがあればああいうことをしてみたいな、と かつて思っていたことを思い出しちゃったりしたので、思い切って公開することにしました。
「売ってる本、しかも『図解』が売りなのに、そんなの公開して大丈夫?」とか思われるかもしれませんが(苦笑)、図にちゃんとした説明がついていることが重要だと思っているので、図を公開してもほんの値打ちを毀損することはない! と信じています(笑)。

埋め込みモデルの精度が上がるとどうなるか

今日は小ネタ。

OpenAI の text-embedding-ada-002(以下 ada-002) と text-embedding-3-small/large(以下 3-small) はテキストを埋め込みベクトルに変換するモデルの代表格で、3-small は ada-002 より精度が高いと言われますが、埋込モデルの精度が高いってどういうことだろう、という話。

埋め込みベクトル同士のコサイン類似度を計算することでテキストの意味の類似度がわかる、というのが埋め込みモデルの売り文句ですが、実際には意味だけではなく表現の近さもかなり反映されます。最も顕著な例は言語でしょう。別の言語だが同じ意味のテキストより、別の意味だが同じ言語のテキストのほうが、埋め込みベクトルの類似度が大きいことも珍しくありません。

こうした事情から、例えば RAG を使ったシステムを構築する場合、複数の言語が混じったテキストをデータベースとするのは気をつける必要があります。もし精度の低下が大きいなら、データベースを言語ごとに分けてベクトル検索をしたほうがいいかもしれません。

さて話を戻って、この意味より言語の一致が勝つ現象について、ada-002 と 3-small を比較してみましょう。具体的には、ベースとなるテキスト「彼は会議に出席した」に対して、いくつかの類似候補文とのコサイン類似度を計算してランク付けします。候補文の中には英訳 "He attended the meeting" と、「彼は仕事に出かけた」「私は総会に欠席した」などのちょっと違う文、「私はネコが好きだ」などの全く違う文を含めておきます。

まずは ada-002 の結果です。

1.00 彼は会議に出席した
0.97 彼女は会議に出席した
0.93 私は総会に出席した
0.92 私は会議に欠席した
0.91 彼は仕事に出かけた
0.90 私は総会に欠席した
0.87 He attended the meeting
0.81 彼はネコが好きだ
0.77 私はネコが好きだ

英訳 "He attended the meeting" より類似度が低いのは「ネコが好き」しか無く、明らかに意味より表現が勝っています。

一方 3-small ではこうなりました(3-large だと数値は少し変わりますが、同じランキングになります)。

1.00 彼は会議に出席した
0.90 彼女は会議に出席した
0.73 私は総会に出席した
0.70 私は会議に欠席した
0.66 He attended the meeting
0.61 私は総会に欠席した
0.45 彼は仕事に出かけた
0.25 彼はネコが好きだ
0.11 私はネコが好きだ

"He attended the meeting" のランキングが上って、「彼は仕事に出かけた」などには類似度で勝てるようになりました。特に「ネコが好きだ」の類似度は大きく落ちており、明らかに意味が異なるという直感が反映された数値になっています。

一方で、冒頭の例にも挙げた「私は会議に欠席した」はまだ同じ意味の英訳に勝っており、埋め込みベクトルの近さに意味だけではなく表現も強く反映されることは変わってはいませんし、この1つの例がすべての傾向を説明できるわけでもありません。ともあれ、この例は埋め込みモデルの精度向上をわかりやすく実感しやすいんじゃあないかと思います。

ちなみに冒頭の図は拙著『ChatGPTのしくみ~』に掲載したものです(書籍の図はデザイン会社さんがきれいにしてくれたものになってます)。執筆当初は ada-002 を使い、同じ意味の英訳に勝つテキストとして「彼は仕事に出かけた」を例示していました。ところが執筆途中で 3-small/large が出て、念の為類似度を計算してみたら逆転してて、慌てて他の例文を探す羽目になりました(苦笑)。

shuyo.hatenablog.com