GPU

Colab / GPGPU / Gemini / 機械学習 / 強化学習

概要

Colab の A100 を機械学習の勉強に使うときの判断材料をまとめる。手元のパソコン(RTX 3070 Ti)では無理で A100 なら成り立つのは何か、速さはどれだけ変わるか、GPU の性能をどう比べるかを扱う。2026-10-20 時点の調査と、手元での実測にもとづく。

結論は次の3つ。

  • A100 でしかできないことは主に2つ:GPU メモリが 8GB に収まらない計算(大きなモデルの追加学習など)と、倍精度(FP64)の計算。
  • 大きく速くなるのは、大きな行列積が中心の計算。行列積を BF16・FP16 で回すと公称値で約7倍、メモリを読み書きする速さは約2.6倍。普通の単精度(FP32)の計算は公称値では 3070 Ti のほうが少し上だが、実測では A100 が 1.2倍速かった(実測)。
  • MPPI とオセロ(8×8)の機械学習は手元で足りる。計算が小さく、A100 の大きさを使い切れない。

用語

用語意味
GPU メモリ(VRAM)GPU に付いているメモリ。モデルの重み・学習途中の値・入力データがここに収まらないと計算できない
メモリ帯域GPU メモリを1秒間に読み書きできる量(GB/s)。要素ごとの足し算のように、計算量が少なくデータ量が多い処理の速さを決める
TFLOPS1秒間に何兆回の浮動小数点演算ができるか。公称値は理論上の最大で、実際のプログラムはこれより遅い
FP64 / FP3264ビット(倍精度)・32ビット(単精度)の浮動小数点数
FP16 / BF1616ビットの浮動小数点数。BF16 は FP32 と同じ範囲の大きさを表せる代わりに桁数が少ない。機械学習の学習でよく使う
TF32行列積の中だけ FP32 を19ビットに落として速く計算する方式。プログラムからは FP32 のまま見える
Tensor コア行列積専用の計算器。FP16・BF16・TF32 などの行列積はここで計算される。通常の計算器(FP32)との差は機種で違い、A100 の BF16 は 16倍(312 対 19.5 TFLOPS)、3070 Ti の BF16 は 2倍、3070 Ti の TF32 は 1倍
混合精度学習の大部分の計算を BF16・FP16 で行い、誤差が問題になる部分だけ FP32 で行う方法。PyTorch では torch.autocast で切り替える。Tensor コアの速さはこれを使ったときに出る
CUColab の計算ユニット。GPU を接続している時間と機種に応じて減る

2つの GPU の仕様

公称値は製品の最大値。A100 は SXM 版(サーバーの専用端子に挿す型)の値を載せた。2026-10-20 に Colab で割り当てられたのは NVIDIA A100-SXM4-40GB だった(nvidia-smi で確認)。

項目RTX 3070 Ti(手元)A100 40GBA100 80GBA100 40GB ÷ 3070 Ti
GPU メモリ8GB40GB80GB5倍
メモリ帯域608 GB/s1,555 GB/s2,039 GB/s2.6倍
FP32(通常の計算器)21.7 TFLOPS19.5 TFLOPS19.5 TFLOPS0.9倍
TF32 行列積21.7 TFLOPS156 TFLOPS156 TFLOPS7.2倍
FP16・BF16 行列積(FP32 で足し合わせ)43.5 TFLOPS312 TFLOPS312 TFLOPS7.2倍
FP640.34 TFLOPS9.7 TFLOPS9.7 TFLOPS28.5倍
FP64 行列積(Tensor コア)なし19.5 TFLOPS19.5 TFLOPS―
光線追跡の計算器(RT コア)ありなしなし―

数値の出どころ:

  • A100 は NVIDIA のデータシートの値(原文: "FP64 9.7 TFLOPS", "FP64 Tensor 19.5 TFLOPS", "FP32 19.5 TFLOPS", "FP16 Tensor 312 TFLOPS | 624 TFLOPS*", "* With sparsity" — A100 データシート)。表には「* With sparsity」が付かない側(行列の半分が0という条件を使わない値)を載せた。
  • 3070 Ti は NVIDIA の製品ページの CUDA コア 6,144・ブーストクロック 1.77GHz・256ビット GDDR6X から計算した。FP32 = 6,144 × 2 × 1.77GHz。帯域は、手元の nvidia-smi が示すメモリクロック最大 9,501MHz(転送速度 19Gbps)× 256ビット ÷ 8。RTX 3070 Ti 製品ページ
  • 3070 Ti の行列積と FP64 の比率は、同じ世代の RTX 3070 の公式表から取った。家庭用の Ampere は FP16 行列積を FP32 で足し合わせると速さが半分になる(原文: RTX 3070 で "Peak FP16 Tensor TFLOPS with FP16 Accumulate 81.3"、"with FP32 Accumulate 40.6")。FP64 は FP32 の 1/64(原文: "The FP64 TFLOP rate is 1/64th the TFLOP rate of FP32 operations.")。GA102 ホワイトペーパー
  • A100 は FP16・BF16 の行列積を FP32 で足し合わせても 312 TFLOPS のまま(原文: "Peak BF16 Tensor TFLOPS with FP32 Accumulate 312/624" — Ampere アーキテクチャ ホワイトペーパー 表1)。

手元でしかできないことと A100 でしかできないこと

A100 でしかできないこと

できること理由
7〜8B(70〜80億個の重み)級の公開 LLM を、重みを BF16 のまま LoRA で追加学習するBF16 の重みだけで約 16GB(8B × 2バイト)で、8GB には載らない。重みを4ビットに量子化する QLoRA なら重みは約 5GB になり、手元でも入力を短くすれば学習できる。A100 は量子化せず、長い入力のまま学習できる
画像分類・物体検出などのモデルを、論文と同じバッチサイズ(1回にまとめて計算する入力の数)で学習するバッチサイズに比例して学習途中の値がメモリを使う。8GB では小さいバッチにするか、小さいバッチの勾配を何回分か足してから更新する方法で代用する。後者でも BatchNorm(バッチ内の平均と分散で値をそろえる層)はバッチごとに統計を取るので小さいバッチのまま計算され、論文と結果がずれる原因になる
倍精度が要る数値計算(流体・構造・量子化学など)を GPU で回す3070 Ti の FP64 は 0.34 TFLOPS。手元の CPU(Core i9-11900K、AVX-512)は 8コア × 1サイクル16回 × 約3.5GHz で約0.45 TFLOPS と計算でき(推測、未計測)、GPU のほうが遅い可能性がある。A100 は 9.7(行列積は 19.5)TFLOPS で、行列積の実測は 18.3 TFLOPS(3070 Ti の63倍)

A100 でもできないこと

できないこと理由
Isaac Sim・Isaac Lab を動かすRT コアの無い GPU は対象外(原文: "GPUs without RT Cores (A100, H100) are not supported." — Isaac Sim Requirements、2026-10-14 更新版)。Colab で選べる機種では G4(RTX PRO 6000 Blackwell)・L4・T4 が RT コアを持つ。同じ資料は 16GB の GeForce RTX 4080 を最低ラインとしており、16GB の T4 は足りない場合がある。Colab で Isaac Sim が動くかは未確認
長時間の連続計算FAQ は無料版で最大12時間、Colab Pro+ で最大24時間としている(原文: "Colab Pro+ supports continuous code execution for up to 24 hours if you have sufficient compute units." — Colab FAQ)。Google AI Pro 経由の Colab にどちらが当てはまるかは未確認。GPU の確保も保証されない。途中経過を Google Drive に保存して再開できる形で書く

手元のほうが向くこと

  • CPU が速さを決める処理。手元の CPU は Core i9-11900K(8コア16スレッド)。2026-10-20 に Colab で割り当てられた A100 の VM は、CPU が Intel(R) Xeon(R) CPU @ 2.20GHz の 12個(nproc)、メモリ 83GB だった。オセロの自己対戦で木探索(MCTS)を CPU で回す場合は、A100 にしても速くならない。
  • GPU の処理1回が小さい計算。手元で、64個の数に1を足す処理を連続で投げると1回あたり 5.7µs かかった(実測)。この時間は主に CPU とドライバが GPU へ命令を出す時間で、小さいネットワークや少ない並列数では計算量よりこちらが速さを決める。GPU を大きくしても縮まず、実測では Colab の A100 のほうが 8.5µs で手元より遅かった。減らすには、処理をまとめて1回で投げる仕組み(PyTorch の torch.compile・CUDA Graphs、JAX の jit と lax.scan)を使う。
  • 何度も書き直して試す作業。Colab は放置すると接続が切れ、CU は待機中も減る。

速さはどれだけ上がるか

計算の種類によって、効く仕様が違う。

計算の種類速さを決める仕様A100 40GB ÷ 3070 Ti(公称値の比。実測があれば併記)例
大きな行列積(BF16・FP16)Tensor コアの TFLOPS7.2倍(実測 6.1〜6.4倍)Transformer・大きな CNN の学習
大きな行列積(FP32 のまま)FP32 の TFLOPS(TF32 を許すと Tensor コアの TFLOPS)TF32 なら公称値で 7.2倍・実測 6.1倍、許さなければ公称値で 0.9倍・実測 1.2倍PyTorch は既定で行列積に TF32 を使わず、畳み込みには使う(手元の PyTorch 2.10 で torch.backends.cudnn.allow_tf32 が True)。行列積は torch.set_float32_matmul_precision("high") で TF32 になる
要素ごとの計算・データのコピーメモリ帯域2.6倍(実測 2.8倍)活性化関数・正規化・物理シミュレーションの1ステップ
倍精度の行列積FP64 の Tensor コアの TFLOPS(3070 Ti は通常の計算器)57倍(実測 63倍)数値計算の連立方程式・行列分解
倍精度の行列積以外の計算FP64 TFLOPS28.5倍数値計算
小さな処理を何度も呼ぶ命令を出す時間(CPU 側)実測 0.67倍(A100 のほうが遅い)MPPI、1局ずつ進める 8×8 のオセロ、小さな MLP(オセロの強化学習で数千局を同時に進めた場合は、A100 の同時に進める対局数 16,384 と手元の 8,192 の比較で 1.8倍。内訳は未計測)

BF16・FP16 の約7倍は、混合精度(torch.autocast)を使ったときだけ出る。3070 Ti も BF16 の Tensor コアを持つので、混合精度の書き方は手元で覚えられる。

実際のプログラムは複数の種類が混ざるので、全体の速さは公称値の比より小さくなる。どれだけ上がるかは、次の節の方法で同じプログラムを両方で走らせて測る。

性能の比べ方

3段階で比べる。下の段ほど自分の用途に近い。

  1. 仕様表:上の表の公称値を比べる。自分の計算がどの種類かを決めないと、どの行を見るかが決まらない。
  2. 小さな計測プログラム:行列積・コピー・小さな処理の速さを測る。公称値に対して実際にどれだけ出るかが分かる。
  3. 自分のプログラム:同じコード・同じ設定で、決まった回数の学習にかかる時間を測る。最初の1回はコンパイルや準備で遅いので除く。

計測プログラム

PyTorch が入った環境(Colab は最初から入っている)でそのまま動く。Colab では A100 に接続してからセルに貼って実行する。各項目を1秒以上くり返して測るので、実行は1分前後。接続から切断まで5分なら、A100 40GB の消費(1時間あたり 5.4 CU 前後、Gemini のページ)で約0.5 CU。

import torch
dev = "cuda"
p = torch.cuda.get_device_properties(0)
print(torch.cuda.get_device_name(0), f"{p.total_memory/2**30:.1f} GiB")

def timeit(fn, seconds=1.0):
    """fn を1秒以上くり返し、1回あたりの秒数を返す"""
    for _ in range(3): fn()            # 準備運転(初回の遅さを除く)
    torch.cuda.synchronize()
    n = 1
    while True:
        start, end = torch.cuda.Event(enable_timing=True), torch.cuda.Event(enable_timing=True)
        start.record()
        for _ in range(n): fn()
        end.record(); torch.cuda.synchronize()
        s = start.elapsed_time(end) / 1000
        if s >= seconds: return s / n
        n *= 2

old_tf32 = torch.backends.cuda.matmul.allow_tf32
# 行列積の速さ(TFLOPS)。n×n の行列積は 2n^3 回の演算
for name, dtype, tf32 in [("FP32", torch.float32, False), ("TF32", torch.float32, True),
                          ("FP16", torch.float16, False), ("BF16", torch.bfloat16, False),
                          ("FP64", torch.float64, False)]:
    torch.backends.cuda.matmul.allow_tf32 = tf32
    n = 8192 if dtype != torch.float64 else 4096
    a = torch.randn(n, n, device=dev, dtype=dtype); b = torch.randn(n, n, device=dev, dtype=dtype)
    s = timeit(lambda: a @ b)
    print(f"matmul {name:4s}: {2*n**3/s/1e12:7.2f} TFLOPS")
    del a, b
torch.backends.cuda.matmul.allow_tf32 = old_tf32

# メモリ帯域(GB/s):1GiB を読み、1GiB を書く
x = torch.empty(2**28, device=dev); y = torch.empty_like(x)
s = timeit(lambda: y.copy_(x))
print(f"copy bandwidth: {2*x.numel()*4/s/1e9:7.1f} GB/s")
del x, y

# 小さな処理を連続で投げたときの1回あたりの時間(µs)
z = torch.randn(64, device=dev)
s = timeit(lambda: z.add_(1))
print(f"small kernel: {s*1e6:7.1f} us")

GPU の命令は CPU から投げた時点で戻ってくるので、CPU の時計で測ると投げた時間だけを測ってしまう。torch.cuda.Event は GPU 上で時刻を記録し、torch.cuda.synchronize() は GPU の計算が終わるまで待つ。

実測

2026-10-20 に上のプログラムを走らせた結果。手元の RTX 3070 Ti(ドライバ 580.178.04、PyTorch 2.10.0+CUDA 12.8)は2回走らせ、2回の差は 2% 以内だった。画面表示にも同じ GPU を使っている状態で測った。Colab の A100(NVIDIA A100-SXM4-40GB、ドライバ 580.82.07、PyTorch 2.11.0+CUDA 13.0)は1回走らせた。A100 は、Colab をコマンドから使うの colab run で動かした。

項目3070 Ti 実測3070 Ti 公称値に対する割合A100 実測A100 公称値に対する割合A100 ÷ 3070 Ti(実測)
行列積 FP3215.6 TFLOPS72%19.1 TFLOPS98%1.2倍
行列積 TF3221.1 TFLOPS97%129.5 TFLOPS83%6.1倍
行列積 FP1642.3 TFLOPS97%259.2 TFLOPS83%6.1倍
行列積 BF1642.1 TFLOPS97%267.4 TFLOPS86%6.4倍
行列積 FP640.29 TFLOPS85%18.3 TFLOPS94%(FP64 行列積の 19.5 に対して)63倍
コピーの帯域495 GB/s81%1,379 GB/s89%2.8倍
小さな処理1回5.7µs―8.5µs―0.67倍(A100 のほうが遅い)

FP32 のまま(TF32 なし)の行列積だけ公称値の72%にとどまった。理由は未確認。推測では、家庭用の Ampere は1つの SM(計算器のまとまり)にある FP32 の計算器128個のうち半分が整数計算と兼用で(GA102 ホワイトペーパー)、公称値はその全部が FP32 を計算した場合の値なので届きにくい。3070 Ti の Tensor コアを使う行列積は公称値の97%出ている。A100 の FP32 以外の行列積は公称値の83〜94%だった。

Colab をコマンドから使う

Google の公式 CLI(コマンドで操作する道具)google-colab-cli で、端末から Colab の GPU を起動し、手元のスクリプトを走らせ、終わったら止められる。google-colab-cli

uv tool install google-colab-cli      # 入れる(pip install google-colab-cli でも可)
colab usage                           # CU の残高と消費率
colab run --gpu A100 bench.py         # A100 を起動し、bench.py を走らせ、終わったら止める
colab new -s work --gpu A100          # 止めずに使い続けるセッションを作る
colab exec -s work -f train.py        # そのセッションで手元の train.py を走らせる
colab download -s work /content/out.pt ./out.pt   # 成果物を手元へ
colab stop -s work                    # 止める。止めないと CU が減り続ける
  • --gpu には T4・L4・G4・H100・A100 を指定できる。綴りを間違えると A100 として扱われる(colab skill で表示される説明書に記載)。
  • 認証は既定で gcloud の ADC(Application Default Credentials、gcloud auth application-default login で作る認証情報)を使う。環境変数 GOOGLE_APPLICATION_CREDENTIALS がサービスアカウントの鍵を指していると、そのアカウントの CU(0)を見てしまうので、外して実行する。
  • colab run は終わると自動で止まる。colab new で作ったセッションは colab stop まで残る。

勉強の題材

MPPI

MPPI(Model Predictive Path Integral)は、制御入力の候補を数千〜数万本ランダムに作り、それぞれの未来をシミュレーションして、結果の良い候補ほど重く平均する制御法。候補ごとの計算は独立なので GPU に向く。

ただし1本の計算は小さい。候補 10,000 本 × 先読み 100 ステップで動力学の計算は 100 万回。1回 100 演算と仮定すると 1億演算で、3070 Ti の FP32 21.7 TFLOPS なら約5µs 分にすぎない。先読みの各ステップは前のステップの結果を待つので、100 回の小さな処理が順に並び、命令を出す時間(1回 5.7µs)のほうが効く(速さはどれだけ上がるかの表の最終行)。速くするには先読みのループを jit・lax.scan や CUDA Graphs で1つの処理にまとめるのが先で、GPU を大きくしても効かない。手元の GPU で足りる。A100 にする理由は無い。

オセロ(8×8)の機械学習

AlphaZero の方式(自己対戦で集めた棋譜で、局面の評価と次の一手を出すネットワークを学習し、木探索と組み合わせる)が代表。時間の大半は自己対戦で、木探索を CPU で回すなら CPU が速さを決める。ネットワークは 8×8 の盤面を入力にする小さい CNN で、8GB に十分収まる。手元で足りる。 木探索まで GPU で一括計算する作り(JAX の mctx など)にすると、同時に進める対局の数を数千に増やして A100 を埋めることもできる。ただしネットワークが小さく1手あたりの計算が少ないので、手元でも学習は回る(負けを目指すオセロ AI の実測)。

負けを目指すオセロ AI の実測

2026-10-20 に、終局で自分の石が相手より少なくなること(負け)を目指す AI を、ランダムに打つ相手との対局で学習させた。学習法は方策勾配法(REINFORCE。勝敗の結果が平均より良かった対局の手を出やすくする)、ネットワークは 3層の全結合(入力は自分の石・相手の石・合法手の 3×64)。数千〜1万局を GPU で同時に進め、学習時間は 15秒に固定した。盤面の処理は、素直に書いた参照実装と 200局(11,997手)で一致することを確かめた。

機種同時に進める対局数15秒で学習した対局数1秒あたり学習後、ランダム相手に負けた割合学習後、貪欲(最も多く返す手を打つ)相手に負けた割合
学習前(参考)―――34.9〜36.0%50.5〜50.7%
RTX 3070 Ti4,096299,00819,70698.0%99.0%
RTX 3070 Ti8,192335,87222,13598.3%99.0%
RTX 3070 Ti16,384メモリ不足で停止―――
A100 40GB16,384606,20839,83998.4%99.2%

負けた割合は、学習後に毎回最も確率の高い手を打たせ、相手ごとに 8,192 局で数えた。A100 は 1秒あたりの対局数が 1.8倍で、手元では入らない同時対局数を使えた。一方で負けた割合は手元でも 98% に達しており、1回ずつの実行では、この題材は A100 が無くても同じ結果になった。A100 の利用は、起動から停止まで 34秒だった。

学習プログラム(`colab run --gpu A100 othello_lose.py 15 16384` で実行。引数は学習秒数と同時対局数)
"""負けを目指すオセロ AI を、ランダムに打つ相手との対局で方策勾配(REINFORCE)により学習する。
盤面は「手番側から見た向き」で持つ(+1=手番側の石、-1=相手の石、0=空き)。数千局を GPU で同時に進める。
"""
import sys, time, torch, torch.nn as nn

T0 = time.time()
TRAIN_SECONDS = float(sys.argv[1]) if len(sys.argv) > 1 else 15.0
B = int(sys.argv[2]) if len(sys.argv) > 2 else 4096
dev = "cuda"
torch.manual_seed(0)

# 各マス s・方向 d・距離 k の先にあるマスの番号(盤外は 64 = 常に 0 の番兵)
DIRS = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
ray = torch.full((64, 8, 7), 64, dtype=torch.long)
for s in range(64):
    for d, (dy, dx) in enumerate(DIRS):
        for k in range(7):
            y, x = s // 8 + dy * (k + 1), s % 8 + dx * (k + 1)
            if 0 <= y < 8 and 0 <= x < 8:
                ray[s, d, k] = y * 8 + x
ray = ray.to(dev)

def init(n):
    b = torch.zeros(n, 64, dtype=torch.int8, device=dev)
    b[:, [27, 36]] = -1; b[:, [28, 35]] = 1   # 黒(先手)から見た向き
    return b

def analyze(b):
    """合法手 (n,64) と、各マス・各方向で返せる石の数 (n,64,8)"""
    ext = torch.cat([b, torch.zeros(b.shape[0], 1, dtype=b.dtype, device=dev)], 1)
    r = ext[:, ray]                                   # (n,64,8,7)
    stop = torch.cat([r != -1, torch.ones_like(r[..., :1], dtype=torch.bool)], 3)
    nrun = stop.to(torch.int8).argmax(3)              # 連続する相手の石の数(最初に相手の石でなくなる位置)
    end = torch.gather(r, 3, nrun.clamp(max=6).unsqueeze(3)).squeeze(3)
    ok = (nrun > 0) & (nrun < 7) & (end == 1)
    flips = torch.where(ok, nrun, torch.zeros_like(nrun))
    legal = (b == 0) & ok.any(2)
    return legal, flips

def play(b, move, flips):
    """手番側が move に打ち、盤面を相手の向きに裏返して返す"""
    n = b.shape[0]; idx = torch.arange(n, device=dev)
    f = flips[idx, move]                              # (n,8)
    k = torch.arange(7, device=dev)
    sq = ray[move]                                    # (n,8,7)
    mask = k.view(1, 1, 7) < f.unsqueeze(2)
    sq = torch.where(mask, sq, torch.full_like(sq, 64))
    ext = torch.cat([b, torch.zeros(n, 1, dtype=b.dtype, device=dev)], 1)
    ext.scatter_(1, sq.view(n, -1), 1)
    ext[idx, move] = 1
    return -ext[:, :64]

def features(b, legal):
    return torch.cat([(b == 1).float(), (b == -1).float(), legal.float()], 1)

net = nn.Sequential(nn.Linear(192, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, 64)).to(dev)
opt = torch.optim.Adam(net.parameters(), lr=1e-3)

def opponent_move(b, legal, kind):
    if kind == "random":
        return torch.multinomial(legal.float() + 1e-9 * (~legal.any(1, keepdim=True)).float(), 1).squeeze(1)
    # greedy: 返せる石が最も多い手(同数は乱数で選ぶ)
    _, flips = analyze(b)
    score = flips.sum(2).float() + torch.rand_like(flips[..., 0].float()) * 0.5
    return score.masked_fill(~legal, -1).argmax(1)

def run_games(n, opp="random", train=True, greedy_agent=False):
    b = init(n)
    agent_black = torch.rand(n, device=dev) < 0.5
    agent_turn = agent_black.clone()                  # 黒が先手
    passes = torch.zeros(n, dtype=torch.int8, device=dev)
    done = torch.zeros(n, dtype=torch.bool, device=dev)
    obs, masks, acts, valid = [], [], [], []
    for _ in range(130):
        legal, flips = analyze(b)
        has = legal.any(1)
        passes = torch.where(has, torch.zeros_like(passes), passes + 1)
        done = done | (passes >= 2)
        if bool(done.all()):
            break
        move_now = has & ~done
        x = features(b, legal)
        with torch.no_grad():
            logits = net(x).masked_fill(~legal, -1e9)
            if greedy_agent:
                a_agent = logits.argmax(1)
            else:
                a_agent = torch.distributions.Categorical(logits=logits).sample()
        a_opp = opponent_move(b, legal, opp)
        a = torch.where(agent_turn, a_agent, a_opp)
        if train:
            obs.append(x); masks.append(legal); acts.append(a); valid.append(agent_turn & move_now)
        nb = play(b, a.clamp(0, 63), flips)
        b = torch.where(move_now.unsqueeze(1), nb, torch.where(done.unsqueeze(1), b, -b))
        agent_turn = torch.where(done, agent_turn, ~agent_turn)
    # 終局:手番側から見た石の差を、AI から見た差に直す
    diff = b.float().sum(1) * torch.where(agent_turn, 1.0, -1.0)
    lose = (diff < 0).float() - (diff > 0).float()   # AI が負け=+1、勝ち=-1
    return lose, (obs, masks, acts, valid)

def train_step():
    reward, (obs, masks, acts, valid) = run_games(B)
    X = torch.stack(obs); M = torch.stack(masks); A = torch.stack(acts); V = torch.stack(valid).float()
    logits = net(X).masked_fill(~M, -1e9)
    logp = torch.log_softmax(logits, -1).gather(2, A.unsqueeze(2)).squeeze(2)
    adv = (reward - reward.mean()).unsqueeze(0)
    loss = -(adv * logp * V).sum() / V.sum()
    opt.zero_grad(); loss.backward(); opt.step()
    return reward

def evaluate(n=8192):
    out = {}
    for opp in ["random", "greedy"]:
        with torch.no_grad():
            r, _ = run_games(n, opp, train=False, greedy_agent=True)
        out[opp] = ((r > 0).float().mean().item(), (r == 0).float().mean().item(), (r < 0).float().mean().item())
    return out

print(torch.cuda.get_device_name(0), f"B={B}", f"train={TRAIN_SECONDS}s")
before = evaluate()
t_train = time.time(); it = 0; games = 0
while time.time() - t_train < TRAIN_SECONDS:
    r = train_step(); it += 1; games += B
    if it % 10 == 0:
        print(f"  {time.time()-t_train:5.1f}s iter {it:4d} games {games:7d} lose_rate {(r>0).float().mean().item():.3f}", flush=True)
torch.cuda.synchronize()
train_time = time.time() - t_train
after = evaluate()
print(f"trained {it} iters, {games} games in {train_time:.1f}s ({games/train_time:.0f} games/s)")
for name, res in [("before", before), ("after", after)]:
    for opp, (l, d, w) in res.items():
        print(f"{name:6s} vs {opp:6s}: AI lost {l*100:5.1f}%  draw {d*100:4.1f}%  AI won {w*100:5.1f}%")
print(f"total {time.time()-T0:.1f}s")
# 学習した重みを残す(Colab では colab download で手元へ取る)
torch.save(net.state_dict(), "othello_lose_weights.pt")
print("saved othello_lose_weights.pt")

ロボットの強化学習

MuJoCo Playground(物理演算ごと GPU に載せ、環境を数千本並列に回す)で、手元の 3070 Ti は四足歩行ロボット(Unitree Go1)の歩行を 16.7 分で学習できた(2026-09 の手元の実測、記録は非公開)。手元で成り立つので、A100 は必須ではない。A100 でどれだけ速くなるか、どの仕様が効くかは未計測。実測のとおり小さな処理1回は A100 のほうが遅いので、並列数が少ない設定では手元より遅くなりうる。Isaac Lab は A100 では動かない(A100 でもできないこと)。

A100 を使う価値がある題材

題材A100 を使う理由手元でやる場合
公開 LLM の追加学習(LoRA・QLoRA)GPU メモリ。8B 級を BF16 のまま扱える1〜3B 級に小さくするか、4ビット量子化して入力を短くする
画像モデルの学習を論文の設定で再現する(例:ResNet-50 を ImageNet 全体、論文のバッチ256で)GPU メモリとバッチサイズ。混合精度の行列積で約7倍バッチを小さくする。BatchNorm のため結果が論文とずれる
同じプログラムを 3070 Ti と A100 で走らせ、どこが速くなりどこがならないかを測る計測そのものが勉強になる。計測プログラムから始め、自分の学習コードへ広げる片側だけでは比較にならない
倍精度の数値計算FP64 が28.5倍(行列積は57倍、実測63倍)CPU より遅い可能性がある

画像の学習では、データを Google Drive から直接読むと読み込みが遅く、GPU が待つ。接続後にランタイムのディスクへコピーしてから学習するが、コピーの時間も CU が減る。ImageNet 全体は数百万枚の画像で、コピーの時間とディスクの容量の両方が問題になるので、まず小さいデータで試す。

文章・画像を生成する公開モデルを動かすだけの用途は、外部のサービスが提供しているモデルを使えば足り、A100 を借りて自分で動かす理由が薄いので、この表に入れていない。自分のデータで追加学習する場合だけ載せた。

CU の目安

2026-10-20 の CU 残高は 400。A100 で計測プログラムを走らせた直後も、colab usage の残高表示は 400.00 のままだった。Google AI Pro では毎月 200 CU が付き、付いてから90日で失効する前提で集計している。400 CU のうちいつ何 CU 失効するかは未確認。A100 40GB の消費を 1時間あたり 5.40 CU(2026-03 の米国での実測例、Gemini のページ)とすると約74時間分。実際の消費率は接続後の「リソースを表示」で確かめる。CU は待機中も減るので、データの準備やコードの書き直しは手元か CPU のランタイムで済ませてから A100 に接続し、終わったら成果物を Drive に保存して切断する。

確認できていない点

  • Colab の A100 の CPU 数・メモリ量・型番が毎回同じか。2026-10-20 の1回だけ確かめた。
  • 3070 Ti の FP32 行列積が公称値の72%にとどまった理由。
  • Google AI Pro 経由の Colab の連続実行の上限(12時間か24時間か)。
  • 400 CU の失効時期の内訳。

参考