Tool Useが効かないDevstralでコーディングエージェントを作る

Mistal.aiからMistral 3.1 Smallをベースにしたコーディング専用モデルDevstralが出ていたので、これを使ってエージェントを作ろうと思ったのです。
Devstral | Mistral AI

Devstralは、24Bというサイズで他の大きなオープンウェイトモデルも凌駕するコーディング性能を出しています。

ベンチマークは当てにならないことも多いですが、使い比べた実感としても確かにQwen3 235B-A22BやDeepSeekよりもコードを書くという印象です。

これを、以前作った、Tool Use(Function Calling)を使った雑なコーディングエージェントで使ってみようと思ったわけです。
LangChain4Jで雑なAIコーディングエージェントを作る - きしだのHatena

けど、DevstralがTool Useに対応してないようで、うまく動きませんでした。 でもRoo Codeは動いてるな、とシステムプロンプトを見てると、独自にXML形式を定義して出力させています。
ということで、XML形式を指定して自力でパースするのを試してみました。

システムプロンプトはこんな感じ。CDATA使うなというのはGemma3のために書いてるんですが、ここまで書いてもGemma3はCDATAを使うので、ちょっとあきらめ。

you are coding agent.
generate source codes following user instruction.
you must generate whole code to run the user intended system including configuration and build script.
you must make readme.md file including the feature, file structure, how to build, how to run.

all generated source codes including readme.md must be in the xml format below.
code tag start and end must be separated line.
<source>
<filename>path of code</filename>
<code>
the code that must use entity reference. not use use CDATA tag.
</code>
</source>

そうするとだいたいこんな出力になります。

出てきた出力を、こんな感じのパーサーで解析します。

    void consume(char ch) {
        buf.append(ch);
        if (state == State.PLAIN) {
            handler.plain(ch);
        }
        if (ch != RET) {
            return;
        }
        String str = buf.toString();
        buf.setLength(0);
        switch (state) {
            case PLAIN -> {
                if (str.startsWith("```")) {
                    formatted = !formatted;
                } else if (str.trim().equals("<source>")) {
                    state = State.IN_XML;
                    handler.sourceStarted();
                }
            }
            case IN_XML -> {
                if (str.startsWith("<filename>")) {
                    String name = str.substring("<filename>".length(), 
                            str.length() - "</filename>\n".length());
                    handler.filename(name);
                } else if (str.trim().equals("<code>")) {
                    state = State.IN_CODE;
                } else if (str.trim().equals("</source>")) {
                    state = State.PLAIN;
                    handler.sourceEnded();
                }else {
                    System.out.println(str);
                }
            }
            case IN_CODE -> {
                if (str.trim().equals("</code>")) {
                    state = State.IN_XML;
                } else {
                    String code = str.replaceAll("&lt;", "<").replaceAll("&gt;", ">"
                            .replaceAll("&quot;", "\"").replaceAll("&amp;", "&"));
                    handler.code(code);
                }
            }
        }            
    }

なんか いい感じに動きました。6倍速で、実際は3分くらいで生成が終わってます。

ここではcreate tableしてなくてエラーになるので「テーブル定義のSQLがないよ」と指示してあげると、schema.sqlを作ってくれました。

エラーが出たときにエラーログを渡すと修正してくれたりもします。
簡単なコードだけど、想像以上にちゃんと動いてくれました。

完全なコードはこちら。
https://gist.github.com/kishida/bc7cec2d036c906111a5be93f1159870

OuteTTSでテキストの音声化を試す

OuteTTSというののv1.0が出てたので試してみました。

前回のブログ内の文章を適当に読ませてみました。
風邪ひいてるときに読んだマンガ - きしだのHatena

「勇者」「美女」を読めなかったり「平和」が「ピンフ」になったりするので、書き換えています。あと、英語女性話者のプロファイルしかないので、英語訛りになってますね。

ということで、「勇者を暗殺するために・・・」の一文を読み上げて食わせてみたら、なんか訛りが入りつつそれっぽく話しています。

gradioとoutettsが必要です。

pip install gradio outetts
import gradio as gr
import tempfile
import os

from outetts import Interface, ModelConfig, GenerationConfig, Backend, InterfaceVersion, Models, GenerationType
from outetts import LlamaCppQuantization

interface = Interface(
    ModelConfig.auto_config(
        model=Models.VERSION_1_0_SIZE_1B,
        # backend=Backend.HF,
        backend=Backend.LLAMACPP,
        quantization=LlamaCppQuantization.Q4_K_S,
    )
)

speaker = interface.load_default_speaker("EN-FEMALE-1-NEUTRAL")

# speaker = interface.create_speaker("yusha.wav")
# interface.save_speaker(speaker, "yusha.json")

# speaker = interface.load_speaker("yusha.json")

def text_to_speech(text):
    output = interface.generate(
        GenerationConfig(
            text= text,
            speaker=speaker,
        )
    )
    with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as fp:
        output.save(fp.name)

        return fp.name

# Gradioインターフェース
iface = gr.Interface(
    fn=text_to_speech,
    inputs=gr.Textbox(label="テキストを入力"),
    outputs=gr.Audio(label="生成された音声"),
    title="OuteTTS",
    description="テキストを入力すると音声(WAV)を生成して再生します"
)

iface.launch()

風邪ひいてるときに読んだマンガ

風邪ひいて土日は寝てたりして、だいぶマンガを雑に読んでたので面白かったののメモ。
異世界多し。

気絶勇者と暗殺姫

勇者を暗殺するために美女3人がパーティーを組んで暗殺を狙うという話だけど、裏切りとかもなく平和。
6/14まで2巻まで無料で読める。

凶乱令嬢ニア・リストン

死にかけ病弱令嬢に転移して、最強令嬢で悪い人をのしていくやつ。
強すぎていい。

ところで、異世界に生まれなおすのが転生で、異世界にそのまま若返ったりしつつ移動するのが転移なんだけど、死んだ人の肉体に魂だけ入るのなんていうんだろう?今回は転移と書いてます。

黒岩メダカに私の可愛いが通じない

話もかわいいけど、絵がかわいい。
表紙でわかるけど、チェックが手書きで、衣服の立体感がすごい。
6/5まで3巻まで無料で読める

最強で最速の無限レベルアップ

俺ツエーものなんだけど、話が平和でよい。
6/10まで2巻まで無料で読める。

あたしメリーさん。いま異世界にいるの……。

捨てた人形から電話がかかってくるやつ。
なぜかメリーさんは異世界に飛ばされて、魔王をたおして元の世界にもどって主人公の後ろにたつことを目指して戦う。

偽聖女クソオブザイヤー

これも悪役聖女に転移して、悪い人をのしていくやつ。
強すぎていい。
6/10まで2巻まで無料でよめる

ずっと青春ぽいですよ

アイドル研究部でわちゃわちゃするやつ。平和でよい。 キラキラしてない「2.5次元の誘惑」という感じ。それかドタバタしてない「究極超人あ~る」、つまり地味部活青春コメディ。

念願の悪役令嬢の身体を手に入れたぞ!

病弱だった女の子が、名前のとおり悪役令嬢に転生して、魔物とかをのしていくやつ。
つよすぎるし、かわいくていい。
6/3まで1巻が無料で読める

追放された転生重騎士はゲーム知識で無双する

不遇職である重騎士にゲーム内転生して成果をあげていくやつ。

モブ高生の俺でも冒険者になればリア充になれますか?

現代にダンジョンができて、冒険者になると高校のクラス内カーストで上位になれる、という世界。 で、冒険者になるけど、クラス内カーストとか関係なく仲間と強くなっていく。
6/5まで2巻まで無料で読める

MIX

親が再婚して兄弟になったピッチャーキャッチャーと妹の話。
あだち充のよくあるやつ。
そしてやっぱり人が死ぬ・・・

魔術ギルド総帥

暗殺された魔術ギルド総帥が自殺したいじめられっこに転移して、俺ツエーやるやつ。
教科書通りの俺ツエーをやっててよい。

母という呪縛 娘という牢獄

教育虐待から抜け出すため、母を殺した娘の、その母を殺すまでの過程。
平和ではない。

AIエージェントの流れはAGI(汎用人口知能)から一旦離れる流れ

AIコーディングエージェントが流行りだしてますね。 AIコーディングエージェントでは、いろいろなロジカルな処理でLLMを制御することで、プログラミングの計画をたて実装してテスト、修正といった流れを実行します。

このAIコーディングエージェントを病院の診察室に持っていっても、うまく診療したりしませんね。 診療のためのエージェントは診療エージェントとして特別に実装する必要があります。

つまり、AIエージェントって独自実装で専門家していきます。

エージェントの核になるLLMは、どのような要件にも使える汎用の知能部品です。LLMが賢くなる流れは、AGI(汎用知能)に近づくものだったと思います。

けど、LLMの性能は上限が見えてきて、例えばthinkingの過程を入れて性能向上するようになってきています。

LLMの性能が頭打ちしてくる中で、このままLLMの性能をあげるだけではタスクを実行完了できるようにならないということで、ロジックでLLMを制御して全体処理の制御をやるというのがエージェントのひとつの形です。 こういって細分化専門家したAIエージェントを統合というのは難しいとおもうので、これはAGIから一旦離れる流れだと思います。

AGIも、ロジックをもったゲートキーバーが専門のAIエージェントに処理を振り分ける仕組みになるかもしれないですが、それはそれで作りこみが必要そうです。ただ、その場合に必要とされるAIエージェントは多岐にわたりそう。

ということで、まあしばらくは、ひとつの人口知能がなんでもできるというものから、作りこまれた専用エージェントが特定タスクをこなすという、AGIから少し離れていくのですね。

そうやってそれぞれの分野について専門家して適応が進む裏で、なにかTransformerに手がはいって、今と違うDiffusion LLMだったりRWKVだったりが進化してAGIに近づいてた、ってなるんですかね。

関係ないけどお話がかわいいので

LLMの日本知識を測るのに山口県について聞くのがよかった

山口県の特徴は?」でLLMの日本語知識が割と測れる気がしたので16GB VRAMで動く範囲でいくつかオープンモデルを試しました。

結論としては、日本語でのチャットなど日本語表現力が必要なら、オープンモデルではGemma3一択。
法律や商慣習に関わる処理や観光地での案内に使うなど、確実な日本知識が必要な場合、また1GB程度のサイズで日本語応対する場合などはLLM-jp-3がおすすめです。

Gemma3、Qwen3、ABEMA Qwen2.5 32B Japanese、LLM-jp-3、Sarashina2.2を試していきます。

Gemma3

まずGemma3。27Bで見てみます。
石見銀山」「津和野の石州和紙」と、島根がちょっと混じっていますが、どちらも「萩・石見空港」だったり「萩・津和野」と山口県萩市知名度を借りたマーケティングが行われがちなので、ちょっと仕方ない。

他は変なところはないのと、津和野の和紙を「津和野和紙」と呼ばず「石州和紙」と呼ぶなど知識あることも示されていて、普通に日本に関して対話するくらいだと問題なさそうな知識をもっていそうです。

あと、表現力を見るために「山口県の特徴をギャルっぽく紹介して」とやってみます。

性格よさそう。これはきっとオタクにやさしいギャル。
そういった感じで、言葉遣いが変わるだけじゃなくポジティブさも出てたり、すごく表現力あります。
ただし「まじ卍」が好きすぎるので、実際に使うときにはシステムプロンプトで禁止したほうがよさそう。 12Bでも結構使えます。

ただ、4Bになると知識はかなり怪しいので、知識が問われる用途は避けたほうがいいです。

Qwen3

一般的な常識やコーディングに強いQwen3は、日本固有の知識には弱いです。
最大のモデルであるQwen3-235B-A22Bでも次のようになりました。

津和野町が入ってますが、これはまあヨシ。

阿武火山群国定公園・・・そんな国定公園は ない・・・
しかし、阿武火山群というのがあるというのは、これで初めて知りました。ということで国定公園ではないものの実在は している。
https://www.data.jma.go.jp/vois/data/fukuoka/512_Abu/512_index.html

しかし、長崎の軍艦島を大島郡上関町というところに持ってきたり(上関町は熊毛郡)、石川県の日本酒「白山」があったり。

碧城という酒はなさそうなんだけど、Googleさんもしっかりだましてくれました。福岡に小野酒造なさそう。

ついでに「山口県の特徴をギャルっぽく紹介して」をやってみます。

それっぽくはあるけど、語尾を「~」で伸ばせばギャルになると思っている節がある。

ということで、もっと小さいモデルについては推して知るべしという感じで、知識は間違いだらけになっていきます。
日本固有のあまり世界的に有名ではない情報を扱うときにはQwen3は避けたほうがよさそう。
また、「角島大桥」のような漢字が出てしまうので、日本語での自然な対話を目的とするときには避けたほうがいいですね。

ABEJA Qwen-2.5 32B Japanese v1.0

そんなことを書いているところにちょうどABEJAから日本語継続学習を行ったモデルが出ていたので試してみます。
https://huggingface.co/abeja/ABEJA-Qwen2.5-32b-Japanese-v1.0

観光はちょっと怪しく、周防大島青海苔は特に有名ではなく、安芸の島は実在しなさそう。大筋あってる、というところ。

2bit量子化モデルで試しているので、8bitや4bitだともう少しマシかもと思うと、悪くもなさそう。

そして「山口県の特徴をギャルっぽく紹介して」

これはいいですね。Gemma3よりはおとなしい。

知識に関してはそこまで期待しないほうがいいけど、表現力はかなりあるのがわかります。
これは、開発が日本語読解や要約など日本語操作力をあげるという方向性だったことも現れているように思います。
ただ、回答が短くまとまる傾向がありそう。

ちなみに、ベースになったQwen2.5 32B Instructの2bit量子化で見てみると未知の情報にあふれているので、かなり日本語知識も改善されていることがわかります。

ギャルというより、ふつうのフレンドリーな若い人って感じですね。

LLM-jp-3

日本知識や日本語能力となれば、最初から日本語情報を学習したLLMが強いはず。 ということで、大規模言語モデル研究開発センターが開発したLLM-jp-3を試してみます。
LLM-jp-3 172B: オープンかつ日本語に強いGPT-3級大規模言語モデル | 国立情報学研究所 大規模言語モデル研究開発センター
LLM-jp-3 MoE シリーズ の公開 | 国立情報学研究所 大規模言語モデル研究開発センター
LLM-jp-3 1.8B・3.7B・13B の公開 | 国立情報学研究所 大規模言語モデル研究開発センター

大規模言語モデル研究開発センターは昨年4月に国立情報学研究所が解説した研究所です。
国立情報学研究所に「大規模言語モデル研究開発センター」新設~国産LLMを構築し、生成AIモデルの透明性・信頼性を確保する研究開発を加速〜 - 国立情報学研究所 / National Institute of Informatics

13B

13Bを4bit量子化で試してみます。
香月泰男、知らなかったけど山口県出身。宇部科学技術館は違いそう。だけど、かなり詳しく具体的な記述。

ギャルかな?回天記念館は渋い。

3.7Bの4bit量子化もかなりまとも。マツダ工場があるのは防府市で、山陽小野田市マツダの部品を作ってる会社の工場、という細かい指摘くらい。

そして方言ギャルか

MoE 8x1.8B

次にMoEモデルの8x1.8Bを試します。
内容に誤りなし。毛利氏庭園のようなマイナーどころも。

「ギャルっぽく」はちょっとガラが悪いかな。「日本三名 bridge 」はちょっと残念。1.8Bベースなので4bit化の悪影響が強いかも。

980m

980m(0.98B)でここまで答えるのはすごい。200tok/s出るし。
宮島(広島)が入ったりするのはご愛敬。

まとめ

ただ、絵文字はかなり苦手そう。ちょっと山口弁使ってるのはいいとこ。

山口弁で試すと、大きく外れてはなくて、山口弁っぽい表現もあるけど、「ぶち」とかは使われていないな。

全体に見て、日本知識はかなり高く、表現はそこまで得意ではない感じ。

ただ、コーディングには使えないですね。あまりコーディング能力は高くない。

1.8Bはちょっとバグ?

あと、8x1.8Bは、こんな感じで指示を突っぱねられることが割とありました。

と思ったら、ベースになってる1.8Bがよくわからない理由で弾いてきたりするので、この挙動を引き継いでるみたい。

1.8Bは「ギャルっぽく」も弾かれたりしているので、無用にリクエスト却下しがちかもしれない。

Sarashina 2.2

もうひとつ最初から日本語で学習したモデルとして、Sarashina 2.2 Instructを。
Sarashina2.2-Instruct:コンパクトかつ性能の高い日本語Instructモデル - SB Intuitions TECH BLOG

日本語表現力としては、英語入りがちということがあって期待できないですけど、日本語知識はかなり高そうです。

3B

内容かなりしっかりしてます。

でもなんか英語使いすぎ。

1B

これも問題なし。

やはり英語使いたがりですね。

0.5B

ちょっと怪しいですが、モデルサイズ500MBでここまで答えるのすごい。

怪しい風習が英語まじりで紹介されています。

古いコードを捨てて1から書き直したからこそ続いているソフトウェア

Joel on SoftwareにNetScapeを例に、古いプログラムを捨てて1から書き直したくなるのは戦略ミスだって書いてあるけど、あのとき書き直してなかったら続いてないんではって思ったので、1から書き直して続いてるソフトウェアを挙げてみる。

Firefox

NetScapeからMozillaに移行するときに、新バージョンのリリースがなくなって、そこで致命的にシェアを落としたというのは確かにそうだと思う。
けど、そこで書き換えていなかったら、2005年のAJAXから始まるWebアプリの高度化についていけなかったと思う。
あそこで書き換えたからこそ、いまこの記事をFirefox上で書けてるんじゃなかろうか。

Windows

Windowsは、MS DOS上で動いていた3.1に継ぎ足すような形で32bit対応してWindows 95など続いていたけど、やはり無理が出ていて、ビジネス用にWindows NTを1から作り直していました。
そして一般向けにもNTを使おうと計画してたけど開発が遅れてWindows 98をアップデートしたWindows MEが出たりして、それでもWindows XPからはNTベースになって今も続いています。

macOS

MacのOSは、古いMac OSを捨ててmacOS(当時はMac OS X)に置き換えられました。開発は遅れて、いつまでもリリースされなかったのを覚えてます。当初、次期OSとしてはCoplandの開発が進んでいましたが、いつまでもリリースされなかったのはコレ。結局Coplandはキャンセルされて、NeXTを買収し、OPENSTEPをベースにMac OS Xが開発されたのでした。(その意味では1から開発されたわけではなかったので失敗例かもしれない)
それでUNIXベースになったからこそエンジニアの支持を得ているし、またそこでアーキテクチャが整理されたからiPhoneなどへの端末展開ができたんじゃないかと思います。

はてなブログ

いまこの記事が置かれてる はてなブログも古いコードを捨てて書き直されたサービスですね。
はてなダイアリーから はてなブログへの移行期に、おそらくなんらかのビジネスチャンスを失ったとは思うけど、それでも、あのときに刷新してなかったら、gooブログなどサービス終了するブログたちに名前を連ねてたかもしれない。

失敗例

業務システムは定期的に更新されてるけど、最近は失敗が目立つようになってますね。今までも失敗はあったけど、影響がでかくなってるような。

新システムに移行したらプリンが出荷できなくなったグリコ基幹システム。
グリコ純利益43%減 24年12月期決算、基幹システム障害で「プッチンプリン」など出荷停止 - 日本経済新聞

システム更新しようとしてたら隠れ仕様がたくさん出て来て訴訟問題になってるNHK受信料システム。
IBM Japan Newsroom - お知らせ

今、JavaはCで書かれているのだけど、JavaJavaで書くというのは夢で、JavaバイトコードをネイティブコンパイルするコンパイラJavaで書こうという話がありました。 で、Cで書かれた今のC2コンパイラのメンテが難しすぎるので置き換えようということでJavaで書かれたGraalコンパイラが開発されたのだけど、C2を置き換えるには至りませんでしたね。
とはいえ消えたわけじゃなく、GraalはGraalVMでネイティブコンパイルに使われていて、また標準化の流れも来ているけど。
JEP 317: Experimental Java-Based JIT Compiler

自社開発プロダクトは、移行失敗したら静かに消えていくので目立ちにくいんだろう。なんかあった気がするけど。

まとめ

Joel on Softwareには、移行のための3年というのはインターネットの世界では非常に長いと書かれていたけど、それから20年たって動きも落ち着いて、3年はそう長くないようにも見える。
AIに関わらないプロダクトだと、3年のギャップはそこまで問題にならないんでは。逆にAIに関するプロダクトは3ヵ月の遅れが致命的になってる。

ただ、見返してみると、FirefoxWindowsmacOSも、20世紀のコードを21世紀に入って捨てたもので、それ以降は安定して書き換えるという話になってないですね。
はてなブログも、2012年くらいに稼働してると思うけど、それ以降はコードベースが古いという話になっていないです。
企業システムも、メインフレームからオープンシステム、自前サーバーからクラウドのようなアーキテクチャ変更がモチベーションにあるように思います。
インターネットの世界も落ち着いて、16bitから32bit、クラウドスマホのような実行基盤の変更も起きないように見えるので、今後は新しい実行環境で動かすために1から書き直すということは減っていきそう。
創業期につくったシステムが成長が一段落したときにユースケースが変わっているので作り変える、といったことはあるのかもしれないけど。

例えばPerlで書かれている はてなブログPerl以外で書き直すということは なさそうで、新しい機能をいれるときにマイクロサービスとして切り出して別言語で実装というふうになると思う。既存部分を置き換えるにしても、同様にマイクロサービスとして切り出して置き換えていく形になりそう。

まあ、なんらか1から書き直す必然性は残るだろうけど、それは単にコードがダサいからとかではなく、今後の発展につながる形である必要があると思う。

CPUが得意なことをCPUにまかせて少ないVRAMでも大きめのLLMを速く動かす

Redditに「VRAM足りないとき一部のレイヤーをCPUに任せるんではなく、レイヤー全部をGPUに載せてレイヤー内部のFFNだけCPUに持っていったら速くなった、なんでこれが標準じゃないんだ」というのがあったので、おうちのRTX 4060 Ti 16GBで試してみたら微妙に速くなりました。
https://www.reddit.com/r/LocalLLaMA/comments/1ki7tg7/dont_offload_gguf_layers_offload_tensors_200_gen/

Qwen3 30B A3Bで試してみる

こういった指定がOllamaやLM Studioではできないので、今回はKoboldCPPというので試してます。
https://github.com/LostRuins/koboldcpp

KoboldCPPでは実用が厳しいので、llama.cppで試すほうがよさそう。

とりあえず、LM StudioでQwen3 30B A3Bのq3_k_xlを動かしたときは15.58tok/sec

48中38レイヤーをGPUに割り当てています。

ということで、koboldcppの実行。ダウンロードした実行ファイルに--overridetensors--model--gpulayersを指定して起動します。

koboldcpp.exe --overridetensors "blk\.([0-9]*[05])\.ffn_.*_exps\.=CPU" --model "D:\dev\gguf\unsloth\Qwen3-30B-A3B-GGUF\Qwen3-30B-A3B-UD-Q3_K_XL.gguf" --gpulayers 48

--overridetensors "blk\.([0-9]*[05])\.ffn_.*_exps\.=CPU"という指定が肝ですね。

0と5で終わるffn内の層がCPUに乗ります。

今回はRedditに書いてあった指定を使っているのだけど、層の名前を確認したいときは正規表現.*を指定すれば全部CPUに乗るので確認できそう。

http://localhost:5001にアクセスして「bertとgptの違いは」と聞いてみます。

17.55tok/sec!12%速くなりましたね。

メモリ消費はこのくらい。

落としたときに2.2GB使っていたので、11.4GBほど消費してます。これはLM Studioで36レイヤー読み込んだときと同じ。

Llama4 ScoutのQ2_KをLM Studioで16レイヤーをGPUにオフロードした場合とKoboldCPPで--overridetensors "blk\.([0-9]*[0124578])\.ffn_.*_exps\.=CPU"としてFFNだけ2/3ほどCPUに残した場合では、4.1tok/secだったのが4.9tok/secと20%速くなりました。

ただ、思ったより効果がでてないのは、うちのCPUがちょっと弱いからではないかと。強いCPUならもっと効果が出ると思います。
Qwen3 32Bで試したら性能向上できなかったけど、CPUが強ければそれなりに効果が出そう。

何をやっているのか

では何をやっているのか見るためにLLMの基本構造を確認してみましょう。

いまのLLMはトランスフォーマという構造をベースにして、だいたいこんな感じになってる。位置エンコーディング(Posional Encoding)からFeed Forwardまでで一層で、 それがQwen 30B A3Bなら48層、Qwen 32Bなら64層という風になってる。

で、LM Studioをはじめ、LLMの実行系の設定では、層単位でGPUにどれだけ乗せるか、逆にCPUにどれだけ残すかというのを設定するようになってる。
でも、層全体で決めるんじゃなくて、層のなかの役割によってCPUでも効率化できるか、GPUじゃないとだめかって決まるんで、CPUでも効率化できるところはCPUに残して、GPUのメリットあるところはなるべくGPUに乗せたほうがいいんでは、って話ですね。

なぜそれがいいのか

じゃあなぜそれがいいのか、って見るのには、実際のコード見るのがいいと思います。

ということで、llama2.cをJavaで書き直したやつをベースに。
https://gist.github.com/kishida/05656bfcbe840f269784f7dbbee5928e

LLMの処理を見るのはforwardメソッド。
https://gist.github.com/kishida/05656bfcbe840f269784f7dbbee5928e#file-llama-java-L300

まず後段になるFeedForwardを見てみます。今回CPUに乗せようというのはこの部分です。

rmsnorm(s.xb, x, w.rms_ffn_weight[l], dim);

// Now for FFN in PyTorch we have: self.w2(F.silu(self.w1(x)) * self.w3(x))
// first calculate self.w1(x) and self.w3(x)
matmul(s.hb, s.xb, w.w1[l], dim, hidden_dim);
matmul(s.hb2, s.xb, w.w3[l], dim, hidden_dim);

// SwiGLU non-linearity
for (int i = 0; i < hidden_dim; i++) {
  // 省略
}

// final matmul to get the output of the ffn
matmul(s.xb, s.hb, w.w2[l], hidden_dim, dim);

SwiGLUのところは省略してますが1重ループです。rmsnormも1重ループになってます。1重ループは基本的に時間がかからないので、高速化の必要性も薄いです。 あとはmatmulです。FFNの処理時間はmatmul部分にかかります。

そのmatmulはこんな感じ。

static void matmul(float[] xout, float[] x, FloatBuffer ws, int n, int d) {
    MemorySegment w = MemorySegment.ofBuffer(ws);
    IntStream.range(0, d).parallel().forEach(i -> {
        FloatVector val = FloatVector.zero(SPECIES);
        for (int j = 0; j < n; j+=SIMD_SIZE) {
            FloatVector a = FloatVector.fromMemorySegment(
               SPECIES, w, (i * n + j + SIMD_SIZE) * FLOAT_SIZE, ByteOrder.LITTLE_ENDIAN);
            FloatVector b = FloatVector.fromArray(SPECIES, x, j + 0*SIMD_SIZE);
            val = a.fma(b, val);
        }
        xout[i] = val.reduceLanes(VectorOperators.ADD);
    });
}

細かいところは置いておいて、IntStreamでparallelとしてマルチスレッド化してるところと、その中にループがあってFloatVectorを使ってAVXなどSIMD命令を使うようにしてることだけ見てください。

つまり、スレッドを動かすコア数がそれなりにあってAVXのように1命令で複数のデータを処理できれば、CPUでも速く処理ができます。

一方でマルチヘッドアテンションはこんな感じですね。

// multihead attention. iterate over all heads
final var fl = l;
IntStream.range(0, p.n_heads).parallel().forEach(h -> {
    int qpos = h * head_size;
    int kvpos = h / kv_mul * head_size;
    float[] att = s.att[h];
    for (int t = 0; t <= pos; t++) {
        float score = 0;
        FloatVector val = FloatVector.zero(SPECIES);
        for (int i = 0; i < head_size; i+=SIMD_SIZE) {
            FloatVector a = FloatVector.fromArray(SPECIES, s.q, qpos + i);
            FloatVector b = FloatVector.fromArray(SPECIES, s.key_cache[fl][t], kvpos + i);
            val = a.fma(b, val);
        }
        score = val.reduceLanes(VectorOperators.ADD);
        score /= head_aqrt;
        // save the score to the attention buffer
        att[t] = score;
    }
    ・・・

IntStreamのparallelでマルチスレッド化して、内部にFloatVectorを使ったループがあるのはmatmulと似てるのだけど、FloatVectorを使ったループがループで囲まれて、全体で3重ループになってます。

そして、真ん中のループは特にハードウェアでの高速化がされてないです。CPUだとこれを高速化する仕組みがない。

Intel AMXとかあるけど4世代Xeonにようやく搭載されたところで、普及してない。使えるとLLMが速くなるようです。
インテルの AI 対応 AMX CPU アクセラレータのテスト結果について | Google Cloud 公式ブログ

一方でGPUだと3重ループを速くすることができます。
GPU処理の共通フレームワークであるOpenCLの説明に次のように書いてます。

解きたい問題には全て、直線状やキューブ状や平面状のようにある程度の次元性が存在している。 OpenCLでは最大3次元までを指定してカーネルを展開する。

ここで、サッと3重ループをGPUで効率よく処理したソースが出せるといいんだけど、ディープラーニングGPU使って速くしようとした処理では、ちゃんと3重ループの処理が書けてなくて高速化できてなかった。
https://github.com/kishida/neuralnet/blob/use_jocl/src/main/resources/kernels/convolution_forward.cl#L15

次のようにiのループとjのループもGPUの並列化に任せるようにすると速くなるはず。

int fxy = get_global_id(0);
int i = get_global_id(1);
int j = get_global_id(2);