見出し画像

GPU版 mini_llm を追っていたら、結局戦っていたのは CUDA そのものではなく API と責務の整合性だった

※この記事は個人開発しているC++製ミニLLMの開発ログです。
※AIの仕組み理解を目的としてフルスクラッチで実装しています。
※実用レベルのLLMとは規模・学習データ・計算資源が大きく異なります。
※この記事は個人の学習・研究目的の開発ログです。
※紹介しているLLMは教育・実験用の小型モデルであり、実用的なAIシステムとは異なります。


今日は mini_llm の GPU版をかなり本気で追い込みました。
しかも、ただ「CUDA を触った日」ではありませんでした。

むしろ今日やっていたのは、GPU版 mini_llm を本当に前へ進めるための土台を掘り当てる作業だったと思います。
表面的には GPU 化の話に見えて、実際に詰まっていたのはもっと手前――API の食い違いと設計の分岐でした。

今日はかなり「ほんちゃん」でした。
単なる試行錯誤ではなく、どこが詰まりどころなのか、何を揃えれば train から chat まで一本線になるのかが、かなりはっきり見えた一日でした。


最初はもっと単純な話だと思っていた

最初の想定は、かなり素直なものでした。

train_llm4d
  ↓
vocab.bin / weights4d.bin
  ↓
chat_llm4d

CPU版では、すでにこの流れがうっすら見えていました。
だから GPU版でも、やることは同じだと考えていました。

  • train_llm4d.cu を通す

  • chat_llm4d.cu を通す

  • vocab.bin と weights4d.bin の保存・読み込みを繋ぐ

要するに、

「CUDA 化した train と chat を通せばいい」

最初は本気でそう思っていました。
やることだけ見ると、そこまで複雑には見えませんでした。

でも、実際にビルドを進めて中を掘ると、話は全然違いました。


本当に詰まっていたのは CUDA そのものではなかった

今回ぶつかったのは、「GPU が難しい」というより、CPU版 API と GPU版 API の食い違いでした。

たとえば、こういうズレが次々に出てきました。

  • Tensor4D に at() がある前提のコードがある

  • でも実際の Tensor4D は、data を持つ版と d_data を持つ版が混在している

  • Embedding4D::forward() が std::vector<int> を受ける前提の場所がある

  • でも別の実装では int* を受ける想定になっている

  • TransformerBlock4D::forward() が Tensor4D を返す前提のコードがある

  • でも別の版では void 扱いになっている

  • parameters() を持つ前提で学習系が書かれている

  • なのに実装側にはそれが無い版もある

つまり何が起きていたかというと、
CPU版から GPU版へ素直に置き換える段階ではなく、途中で設計が分岐してしまっていたということでした。

ここが本質でした。

GPU 化で詰まっているように見えて、実際には
同じ名前のクラスや関数が、場所によって別物になっていた

この状態では、CUDA カーネルを書く以前に、部品同士が噛み合いません。
今日いちばん重かったのは、まさにそこでした。


今日の時点で見えたこと その1

train 側の核はかなり見えている

かなり追ってみて、少なくとも train_llm4d 側は、完全にゼロではないことが見えました。

  • loss 計算

  • 構造指定

  • 保存処理を差し込む位置

このあたりは、少なくとも
どこに何を入れれば形になるかが見える状態でした。

全部が完成しているわけではありません。
でも「何もない」状態でもありませんでした。

これはかなり大きいです。

つまり train 側は、まだ粗くても、
進めるための土台はかなり見えている

今日の作業でそこが確認できたのは、大きな収穫でした。


今日の時点で見えたこと その2

chat 側は train 側に追いついていなかった

今回いちばん大きかったのはここでした。

最初は「GPU版そのものがダメなのか」と思っていたのですが、実際には少し違っていて、
chat 側が train 側に追いついていなかったのが大きかったです。

本来、LLM として当たり前の流れはこうです。

  1. train で学習する

  2. 学習済みの重みを保存する

  3. chat でその重みを読む

  4. 推論する

でも GPU版では、この一連の流れがまだ途中でした。

つまり問題は「CUDA の演算が弱い」ではなく、

train で吐いたものを chat で受ける経路が未完成だった

ということでした。

ここが繋がらないと、どれだけ GPU カーネルを書いても、
「学習したモデルで会話する」ところまで行けません。

今日の作業で、そこがかなりはっきり見えました。


本当の敵は CUDA そのものではなく、整合性だった

今日かなり実感したのはこれです。

GPU化というと、ついこう考えがちです。

  • カーネルを書くのが大変

  • CUDA メモリ管理が難しい

  • CPU / GPU 転送が面倒

もちろん、それも大変です。
でも今回いちばん厄介だったのは、そこではありませんでした。

本当に詰まっていたのは、

  • どのクラスが何を返すのか

  • どのメモリを持っているのか

  • CPU版と GPU版で同じ名前の関数が同じ意味なのか

  • 保存形式と読み込み形式が一致しているのか

そういう設計の整合性でした。

要するに、

「GPU 化」より先に「API の統一」が必要だった

という話です。

これは逆に言えば、いままで CUDA が悪いように見えていた部分の一部は、
実は設計の揺れが表面化しただけだった、ということでもあります。


GPU は動いていた

次の課題は「使い切ること」と「安定させること」

さらに確認していく中で、もうひとつ大きなことが見えてきました。

GPU そのものは、少なくとも動いていました。

nvidia-smi を見ると、RTX3060 はきちんと反応していて、GPU Util も高い状態が出ていました。
つまり「GPU版なのに GPU が回っていない」という段階ではありません。

少なくとも、CUDA 側へ処理が流れていないわけではない。
これはかなり大きいです。

ただし同時に、別の課題も見えました。

それが、

VRAM をまだ使い切れていない

という点です。

RTX3060 は 12GB ありますが、実際の使用量はまだかなり少ない。
つまり GPU は動いているけれど、GPU に十分な仕事量を渡せていない可能性があるということです。

ここで関わってくるのが、

  • batch

  • sequence length

  • hidden size

  • layers

  • 実行スケジュール

といったパラメータです。

今日の時点で見えているのはこういうことです。

  • GPU は動いている

  • でも VRAM はまだ余っている

  • つまり GPU版としての実装だけでなく、実行パラメータの詰めも必要

ここまで来ると、話は
「CUDA 化できたら終わり」
ではありません。

むしろ、GPU 化できてからが調整の始まりでした。


GPU化したら終わりではなく、そこから先に「調整」の世界がある

最初はどうしても、

  • CUDA 化できれば速くなる

  • 全部 GPU に乗せれば勝ち

という感覚を持ちやすいです。

でも実際には、GPU 化は入口にすぎませんでした。

その先に必要なのは、

  • どれくらいの batch を積むか

  • どれくらいの seq にするか

  • 小型モデルなら CPU のほうが速くないか

  • CPU / GPU ハイブリッドにする意味はどこにあるか

  • GPU を遊ばせないスケジューリングはどうするか

という、実行効率の設計です。

ここまで来ると、話は単なる CUDA ではなくなります。

mini_llm の実装は、モデル本体だけではなく、

  • CPU版

  • CPU-GPU ハイブリッド版

  • GPU専用版

をどう住み分けるか、というランタイム設計の話にも入ってきます。


小型は CPU が速く感じることがある

今日改めて実感したのはここでした。

モデルが小さいうちは、GPU を使っていても、

  • 転送

  • カーネル起動オーバーヘッド

  • 細かい同期

のせいで、CPU のほうが体感で速く見えることがある。

逆に、モデルが大きくなってくると GPU の意味が出てきます。

だから単純に「GPU が正義」という話ではなく、

  • 小型構成は CPU 向き

  • 中型以上は GPU 向き

  • 前処理や制御は CPU

  • 学習本体は GPU

というように、役割を整理していく必要があります。

このあたりまで見えてくると、PyTorch が裏でやっていたことの重さもよくわかります。

ブラックボックスに見えていたものは、実際には

  • テンソル管理

  • デバイス管理

  • 最適化

  • 整合性維持

  • 実行効率調整

をまとめて引き受けていたわけです。

C++ で 4D テンソルを持ち、自前で CPU版と GPU版を持ち、さらに train と chat を繋げようとすると、そこを全部自分で引き受けることになります。

ここが本当に重い。


学習が回ることと、学習が安定することは別

もうひとつ大きかったのは、loss の動きです。

学習自体は進んでいるように見えても、loss が途中で跳ねたり、大きく荒れたりすることがあります。
この状態は「まったく動いていない」わけではありません。

むしろ、

計算は回っているが、学習条件がまだ整っていない

可能性が高いです。

ここで効いてくるのは、

  • learning rate

  • warmup

  • gradient clipping

  • batch size

  • precision

  • データの質

  • 保存 / 読み込みの整合性

あたりです。

つまり、GPU 化できたあとに待っているのは、
性能調整と学習安定化です。


ここまで見えて、ようやく「次にやること」が整理できた

今日の作業で、どこが詰まりどころなのかはかなりはっきりしました。
次にやるべきことも、かなり明確です。

まずは一本線を作ることです。

  • train_llm4d.cu に保存処理を入れる

  • chat_llm4d.cu に読み込み処理を入れる

  • Tensor4D

  • Embedding4D

  • TransformerBlock4D

このあたりの API を揃える

ここを揃えれば、ようやく

train
  ↓
保存
  ↓
chat
  ↓
推論

という、LLM として最低限の一本線になります。

そのうえでさらに、

  • VRAM を使い切る方向の調整

  • batch / seq / hidden の見直し

  • CPU / GPU / ハイブリッドの住み分け

  • 学習安定化のためのパラメータ調整

に進めるようになります。


今日の結論

今日やっていたのは、表面上は
「GPU版を通す作業」
でした。

でも実際には、

GPU化の問題を追っていたら、設計の分岐を掘り当てた

という一日でした。

CUDA そのものより先に、CPU版と GPU版で揺れてしまった API と責務を揃えないと前に進めない。
これは逆に言えば、詰まりの原因がようやく見えたということでもあります。

何が悪いのかわからない状態がいちばんつらい。
でも今日はそこから一歩進んで、

  • train 側はどこまであるのか

  • chat 側は何が足りないのか

  • 何を統一すれば前に進むのか

  • GPU は本当に動いているのか

  • 次に詰めるべきなのは実装か、パラメータか、スケジューラか

このあたりが見えました。

これはかなり大きいです。

GPU化しているつもりで、実は戦っていたのは
CUDA そのものだけではなく、API と責務の整合性でした。

でも逆に言えば、そこさえ揃えば、ようやく本当に GPU版 mini_llm を前へ進められる。

今日はそのための、詰まりどころの特定ができた日でした。

そして今はもう、単に

「GPU版 mini_llm を動かしたい」

ではなく、

「train から chat までを一本で繋ぎ、GPU をちゃんと使い切り、学習を安定させる」

という次の段階に入ってきています。

たぶん今日の話を一言でまとめるなら、こうです。

GPU版 mini_llm を追っていたら、結局戦っていたのは CUDA そのものだけではなく、API と責務の整合性だった。
でも、詰まりどころが見えた今なら、ようやく本当に前へ進める。


いいなと思ったら応援しよう!