ハードウェアとテンソル 入門
このトラックの他の primer はネットワークが何をしているかを扱った。 この primer はネットが乗っている基盤を扱う。短い 4 トピック:GPU vs CPU、なぜ現代モデルはみな GPU に住むのか;VRAM、いま「どれだけ大きいモデルを動かせるか」の最も固い制約;テンソル、深層学習の基本データ単位 —— 多次元配列;そしてバッチ・パディング・マスク、現実の凸凹したテキストを GPU フレンドリーな矩形に詰め直す実践技。実務ではこの primer は飛ばせない —— この先見る「CUDA out of memory」のエラーは、すべてこの語彙で書かれている。
GPU vs CPU
CPU は賢いが少数。GPU は単純だが多数。深層学習は後者向きだ。
現代の CPU は「コンピュータ」のプラトン的理想形だ。少数(ノート PC で 8 個、 ワークステーションで 64 個)の速くて柔軟なコア —— どれも分岐ロジック、複雑な命令、 雑多な記帳を高速にこなす。最適化されているのは「1 スレッドが多様な タスクの長い列を順番にこなす」こと。OS、データベース、Web サーバー —— ここは CPU の領土だ。
現代の GPU は逆だ。数千個の小さくて単純なコア —— NVIDIA H100 は 16,896 個の CUDA コアを持つ。各コアは分岐が苦手、一般的なロジックも苦手、 「隣のコアたちと同じ単純な演算をする」ときだけ速い。実行モデルは SIMD (単一命令、複数データ)あるいはより細粒な SIMT。アーキテクチャ全体が「1 つの演算を数千個のデータに並列適用」に最適化されている。
その第 2 のパターンはまさにニューラルネットそのものだ:
- 行列積。Transformer の最下層の演算。4096×4096 行列同士の積は 670 億回の乗算 —— そのすべてを並列に走らせられる。
- 要素ごとの活性化関数。ReLU、GELU、softmax —— 同じ関数を数百万個の値に独立に適用。
- 正規化層。LayerNorm と RMSNorm は 1 軸で reduce してから 各要素を rescale —— これも「気まずいほど並列」。
数字で感じたいなら:4096 × 4096 の行列積は、速い CPU で約 50 ms、H100 で 1 ms 未満。 この差はサイズに対して超線形に開く ——4k × 4k では GPU はまだ暖機すらしておらず、 CPU は既に飽和している。最先端 LLM の訓練は通常、数千枚の GPU を数か月 並列で回す。CPU でやれば数世紀かかる。だから現代モデルはみな GPU に住む。
GPU が苦手なこと、簡潔に:
- 分岐が多いコード。異なる「コア」が異なる経路を取るなら、順番に こなすしかない —— 速いコアが遅いコアを待つ。深層学習側の対策は「分岐なしで書く」 こと:パディングとマスクで、どこも同じ演算を当てる。
- 小さなワークロード。GPU 起動には費用がかかる。32 × 32 のような 小さな行列積は GPU の立ち上げが支配的になり、CPU の方が速い。実深層学習では 作業を束ねて起動コストを薄める。
- CPU↔GPU のメモリ転送。システム RAM と GPU の VRAM(§2)の間は PCIe バスを通り、GPU 内部の帯域より大幅に遅い。データは GPU に留める。
簡単に:TPU は Google の専用テンソル加速器 —— GPU よりさらに特化、 「matmul + 活性化」パターンに最適化されている。Google 内のモデル訓練 (Gemini など)の大半はこの上で回る。機能的には同じアイデアのより過激な版: 単純で並列な算術と、制御されたメモリアクセスパターン。
Transformer では:各層は行列積(Q·K、attention·V、FFN 重み)と いくつかの要素演算(softmax、GELU、残差加算、LayerNorm)。順伝播全体は 「矩形のテンソル仕事を GPU に流す」。アーキテクチャを設計する人は暗黙に このハードウェア現実のために設計し、訓練する人は暗黙に「どれだけ GPU 時間を 確保できるか」を競う。
VRAM —— 最も固いボトルネック
これまで見た「CUDA out of memory」のすべては、ここで書かれた物語だ。
GPU は CPU が使うシステム RAM とは別の、自分専用のメモリを持つ。NVIDIA はそれを VRAM(新しいチップでは HBM)と呼ぶ。H100 で 80 GB、A100 で 40 か 80、4090 で 24、 4070 で 12。この数字が「そのカードでどれだけ大きいモデルを訓練 / 実行できるか」の 硬い制約だ。モデルと作業領域が入らなければ訓練できない。それだけだ。
70 億パラメータ、fp16(数 1 つで 2 バイト)のモデルで計算してみる:
推論(モデルだけ):
重み 7 × 10⁹ × 2 バイト = 14 GB
──────────────────
24 GB のコンシューマ GPU に余裕で収まる
訓練(モデル + 訓練の足場すべて):
重み 14 GB
勾配 14 GB ← パラメータ 1 個に 1 つ
Adam モーメンタム 14 GB ← パラメータ 1 個に 1 つ
Adam 分散 14 GB ← パラメータ 1 個に 1 つ
活性 ~30 GB ← バッチと seq_len 依存
─────────
≈ 86 GB → 80 GB H100 に入らないここに 2 つの驚きがある。1 つ目、同じモデルでも訓練は推論の約 5–10 倍の VRAMを使う —— オプティマイザが各パラメータに複数コピーを持ち、順伝播の活性が逆伝播のために 保存されるからだ。2 つ目、推論なら 24 GB のコンシューマカードに余裕で乗る 7B モデルが、訓練では 80 GB の H100 1 枚にすら収まらない。70B モデルは重みだけで 140 GB —— 確実にマルチ GPU 領域。
緩和策、おおむね人が手を伸ばす順:
- 量子化(Quantization)。重みを低精度で保存する —— int8(バイト半減)、int4(1/4)、さらには 3-bit や 2-bit のスキーム。 fp16 で 14 GB の 7B モデルは int4 では 3.5 GB。推論では一般的;訓練では勾配が 高精度を望むため厄介。
- 混合精度訓練。活性と勾配は fp16 か bf16、オプティマイザの 「マスタコピー」は安定性のため fp32 で保持。活性メモリはおよそ半減。
- 勾配 / 活性チェックポイント。順伝播のすべての活性を 保存しない —— 数層ごとに保存し、逆伝播時に間を再計算する。 計算量とメモリのトレードオフ。
- モデル分割 / テンソル並列。モデル自身を複数 GPU に分割する。 ZeRO、FSDP、DeepSpeed、Megatron —— 分散訓練フレームワークはどれもこの変種だ。
- Flash attention。アテンションを再実装し、n × n の アテンション行列を VRAM に実体化しない。長文脈訓練で 20–80 GB 節約;今やすべての 現代 Transformer 実装に組み込まれている。
帯域は「メモリ」の物語のもう半分で、踏むまで誰も言わない。H100 の VRAM 帯域は 3 TB/s —— 巨大だが、それでも有限。多くのニューラルネット演算はメモリ律速: GPU は計算より VRAM から届くデータを待つ時間の方が長い。効率的なカーネル (FlashAttention、fused MLP など)の仕事の一部は「VRAM への往復を減らす」こと。 GPU 内部にもメモリ階層がある —— レジスタ、shared memory / L1 キャッシュ(SM ごと 256 KB)、L2 キャッシュ(全体で 50 MB)。VRAM よりずっと速いが、ずっと小さい。 パフォーマンス最適化の多くは「データを速いメモリに長く留める」こと。
Transformer では:あらゆるアーキテクチャ選択に VRAM の側面がある。KV キャッシュ(自己回帰デコードのために残された過去トークンの key と value) は batch × seq_len × n_layers × d_kv でスケールし、長文脈モデルでは 重みより大きいことも多い。KV キャッシュを縮めること(Grouped-Query Attention、 Multi-Query Attention、スライディング窓アテンション)は、現代 LLM が 128k や 1M トークンの文脈を扱える理由の半分だ。
テンソル —— 多次元配列
深層学習の基本データ単位。ネットワークを流れる値はすべてこれ。
フレームワーク、モデル、抽象を全部はがすと、残るのはテンソルだけ。 テンソルは数値の多次元配列。現代の ML スタック(PyTorch、JAX、TensorFlow、NumPy) は本質的に「テンソルを効率的に生成・整形・結合する関数の集まり」だ。 テンソルを理解すれば、LLM のコードがやっていることの 90% を理解できる —— どの行も基本テンソル操作だ。
次元のはしご:
- 0-D(スカラ) —— 数 1 つ。
shape = ()。 順伝播の最後に出る損失、学習率、確率。 - 1-D(ベクトル) —— 数の 1 列。
shape = (n,)。 1 単語の埋め込み(例:768 次元ベクトル)、層のバイアス。 - 2-D(行列) —— 行 × 列。
shape = (m, n)。 重み行列、1 文の埋め込み(系列長 × d_model)、共分散行列。 - 3-D(テンソル) —— 行列の積み重ね。
shape = (a, b, c)。 Transformer の日常形:(batch, seq, d_model)。 - より高次(n-D) —— 軸を増やす。マルチヘッドアテンションは 4-D (batch, heads, seq, head_dim)、動画データは 5-D (batch, time, channel, height, width)。
テンソルに何かする前に必ず知っておくべき 3 つ:
- Shape(形状)。整数のタプル ——
(32, 512, 768)は 32 サンプル、各 512 トークン、トークンごとに 768 次元の隠れ状態。 ニューラルネットのバグはほぼすべて形状不一致だ。 - Dtype(データ型)。精度:
float32(4 バイト)、float16/bfloat16(2 バイト)、int8(1 バイト)、bool。dtype の選択は半分 VRAM(§2)の判断、 半分は数値安定性の判断だ。 - Device(デバイス)。バイトがどこに住むか ——
cpu、cuda:0、cuda:1など。違うデバイス上のテンソル同士の 演算はエラー;先に.to(device)しないといけない。
時間の 95% を占める演算:
- 要素ごと:
+、*、tanh、relu。セルごとに適用、出力形状 = 入力形状。 - リダクション:軸に沿って
sum、mean、max。(32, 512, 768).mean(dim=-1)→(32, 512)—— 隠れ次元が潰れる。 - Reshape:
view、reshape、permute、transpose。データを変えずに軸を並べ替える。 Transformer の中の batch / seq / head 軸の入れ替えは全部これ。 - 行列積:PyTorch / NumPy では
@。(32, 512, 768) @ (768, 768) → (32, 512, 768)。最大の演算; GPU はそのために作られている。 - インデックス / スライス:
x[:, 0, :]で各サンプルの 最初のトークンを取り出す。x[mask]でmaskが真の位置だけ。
ブロードキャストは初心者を混乱させ、その後は見えなくなる演算だ。(32, 512, 768) + (768,) と書くと、小さい方のテンソルが欠けている 次元に沿って自動的に「引き伸ばされ」、同じ (768,) のバイアス ベクトルがすべてのサンプルのすべての位置に足される。メモリはコピーされない、 ブロードキャストは仮想的だ。ニューラルネットコードのほぼすべての行が ブロードキャストで簡潔に書かれる。
Transformer では:入力トークンが(batch, seq, d_model) の形状のテンソルに埋め込まれる。 この形状はネットワーク内で保たれる —— 各層の出力は同じ形状の別のテンソルで、 数値が違うだけ。アテンション内部では、テンソルが一時的に(batch, heads, seq, d_head) に reshape されてマルチヘッドの内積を 並列計算、その後戻る。最終層が (batch, seq, vocab_size) に射影する —— 各位置で語彙上のロジットベクトル。すべてがテンソル演算。
バッチ、パディング、マスク
可変長テキストが実際にどう GPU フレンドリーな矩形に詰め直されるか。
現実のテキストはバラバラな長さの系列の集まり。現実の GPU は矩形のテンソルが 欲しい。3 つの実用的な仕掛けがその橋渡しをする —— Transformer 訓練ループの どの行にも出てくる。テキスト primer §1 で導入したアイデアを、ここでは仕組みごと 固める。
バッチ(Batch)。
1 文ずつ与えるのではなく、N 文を一度に与え、形状 (N, max_len)のテンソルに積み、GPU に N 個を並列処理させる。N ——バッチサイズ—— は深層学習で最も重要なハイパーパラメータの 1 つ。 バッチが大きいほど VRAM を消費するが、GPU を忙しく保ち、勾配をより平均し、 (適切な範囲で)訓練を速くする。典型値:数枚の GPU で 70B モデルを微調整するときは 32、 GPT-4 級モデルの事前訓練では数百万トークンのバッチ。具体的な数値は VRAM に 収まる範囲で決まる。
パディング(Padding)。
同じバッチ内の文は長さが違う。形状 (N, max_len) の矩形テンソルに積むには、 全文を同じ長さに揃える必要がある。短い文を特別な [PAD] トークンでパディングする —— 慣習として語彙の id 0。バッチで最も長い文がmax_len を決め、他はそこまで PAD トークンを追加する。
パディングまわりの実用的なつまみが 2 つある。1 つ目、バッチごとの max_lenの選び方:データセット全体の最大長までパディングするのは無駄。各ミニバッチ内の 最大長まで(「動的パディング」)ならずっと節約できる。2 つ目、どちら側に詰めるか: 現代の Transformer の多くは訓練時に右パディング、自己回帰生成時に左パディング。 理由は繊細だが影響は大きい —— 間違えるとモデルは静かに意味のないものを学ぶ。
マスク(Mask)。
パディングは形状を直すが、新しい問題を生む:アテンションは既定で、本物トークンと PAD トークン間のアテンション重みを平気に計算する。モデルはパディング経由で情報を回すことを学んでしまう —— 無意味だ。修正はマスク —— 0/1 の並列テンソル(よく bool)で、モデルにどの位置が 本物かを伝える:
A 文: ["hi", ".", PAD, PAD, PAD, PAD, PAD] A mask: [ 1, 1, 0, 0, 0, 0, 0] B 文: ["the", "dog", "runs", "fast", ".", PAD, PAD] B mask: [ 1, 1, 1, 1, 1, 0, 0]
アテンションブロック内で、マスクは softmax の前に適用される:PAD 位置の アテンションスコアが −∞ に設定され、softmax 後の重みは正確に 0。 PAD トークンの出力への寄与は 0;モデルは「パディングなしの文だけで走らせた」のと 同じ挙動(無駄な計算を除いて)。
Transformer で一般的なマスクの 2 種類:
- Padding mask —— 今説明したもの。本物 vs PAD を示す。 ほぼすべての Transformer に存在する。
- 因果(autoregressive)マスク —— GPT のような decoder-only モデルで 使う。位置 t は位置 ≤ t しか参照できない。
−∞の 三角行列をアテンションスコアに加える。padding mask とは論理 AND で合成。
Transformer では:すべてのアテンション呼び出しがマスクを受け取る。 訓練時の典型バッチは両方を持つ:自己回帰構造のための因果マスクと、可変長のための padding mask。出力は正しく、GPU はきれいな矩形テンソルを見て、損失は本物トークン だけを数える(損失関数自体もマスクする —— PAD 位置の予測でモデルを罰しない)。 この目立たない配管こそが、「バッチ並列訓練」を成立させる基盤すべてだ。