PyTorch GPU / CUDA 高速化
ディープラーニングの中核となる操作は、大規模な行列乗算と要素演算です。CPU の設計目標は複雑なシリアルロジックを処理することで、コア数は通常 8〜64 個です。一方 GPU は数千の単純な並列コアを備えており、このような高度に並列な数値計算に自然に適しています。PyTorch は NVIDIA のCUDA(Compute Unified Device Architecture)フレームワークを通じて GPU を呼び出し、トレーニング速度を数十倍から百倍にまで向上させることができます。
1. CPU と GPU の違い
GPU トレーニング速度の向上は主に 2 つの要因に由来します。1 つは大量の同一計算を並列実行すること、もう 1 つは高帯域幅の VRAM によりデータ転送速度が CPU メモリよりもはるかに高速になることです。
行列乗算のような演算集約型の操作では、高速化の効果が特に顕著です。
| 比較項目 | CPU | GPU(NVIDIA) |
|---|---|---|
| コア数 | 8〜64 個の大コア | 数千個の小コア(CUDA Cores) |
| 設計目標 | 低遅延シリアル処理 | 高スループット並列計算 |
| メモリ帯域幅 | ~50~100 GB/s | ~500~3000 GB/s |
| 行列乗算速度 | ベンチマーク | 10x〜100x 高速 |
| PyTorch インターフェース | "cpu" |
"cuda" |
Apple Silicon(M1/M2/M3)を使用する場合、PyTorch はmpsバックエンドを通じて Metal GPU 高速化をサポートします。使用方法は CUDA とほぼ完全に同じで、デバイスを変更するだけです。"mps"。
2. CUDA 環境の確認
GPU を使用する前に、現在の環境に CUDA 対応の PyTorch がインストールされているかどうかを確認する必要があります。
また、システム上に利用可能な GPU デバイスが存在するかどうかを確認する必要があります。
例
# CUDA 対応かどうか
print("CUDA 使用可能:", torch.cuda.is_available())
# GPU デバイスの数
print("GPU の数:", torch.cuda.device_count())
# 現在のデフォルト GPU のインデックス
print("現在の GPU:", torch.cuda.current_device())
# GPU モデル名
print("GPU モデル:", torch.cuda.get_device_name(0))
# PyTorch のバージョンと、コンパイル時にバンドルされた CUDA バージョン
print("PyTorch バージョン:", torch.__version__)
print("CUDA バージョン:", torch.version.cuda)
出力例:
CUDA 可用: True GPU 数量: 1 当前 GPU: 0 GPU 型号: NVIDIA GeForce RTX 4090 PyTorch 版本: 2.3.0+cu121 CUDA 版本: 12.1
2.1 デバイスの動的選択(推奨コード)
コード内にハードコードすると、"cuda"GPU のないマシンでは直接エラーが発生します。
例
# 方法1: 古典的な書き方で、互換性が最も高い
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 方法2: PyTorch 2.0+ 推奨、CUDA / MPS / CPU を自動サポート
device = (
"cuda" if torch.cuda.is_available()
else "mps" if torch.backends.mps.is_available()
else "cpu"
)
print(f"使用デバイス: {device}")
3. テンソルのデバイス間移動
PyTorch のテンソルはデフォルトで CPU 上に作成されます。
GPU 計算を使用するには、テンソルを明示的に GPU へ移動するか、GPU 上で直接作成する必要があります。
3.1 基本的な移動方法
例
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# CPU テンソルの作成
cpu_tensor = torch.tensor([1.0, 2.0, 3.0])
print(cpu_tensor.device) # cpu
# 方法1: .to(device) —— 推奨、汎用性が最も高い
gpu_tensor = cpu_tensor.to(device)
# 方法2: .cuda() —— CUDA 環境のみ
gpu_tensor = cpu_tensor.cuda()
# 方法3: 作成時にデバイスを直接指定
gpu_tensor = torch.tensor([1.0, 2.0, 3.0], device=device)
gpu_tensor = torch.randn(3, 4, device=device)
# CPU に戻す(表示、numpy 変換、保存などの操作に使用)
back_to_cpu = gpu_tensor.cpu()
print(gpu_tensor.device) # cuda:0
print(back_to_cpu.device) # cpu
GPU テンソルを numpy に変換する場合、先に CPU に戻す必要があります。また、テンソルに勾配が付いている場合は、最初に detach する必要があります。
例
arr = gpu_tensor.cpu().numpy()
# 勾配付き GPU テンソルを numpy に変換
arr = gpu_tensor.detach().cpu().numpy()
3.2 デバイス一貫性の制約
異なるデバイス上のテンソルは、同じ演算に直接参加できません。
そうしないとエラーがスローされます。RuntimeError:
例
b = torch.randn(3) # CPU 上
# c = a + b # RuntimeError: Expected all tensors to be on the same device
# 正しい方法: まずデバイスを統一する
c = a + b.to("cuda")
テンソルが存在するデバイスを確認する:
例
print(x.device) # cuda:0
print(x.is_cuda) # True
print(x.get_device()) # 0(GPU インデックス)
3.3 速度比較の検証
例
import time
device = torch.device("cuda")
n = 5000
# CPU 行列乗算
a_cpu = torch.randn(n, n)
b_cpu = torch.randn(n, n)
start = time.time()
c_cpu = torch.matmul(a_cpu, b_cpu)
print(f"CPU 所要時間: {time.time() - start:.3f}s")
# GPU 行列乗算
a_gpu = a_cpu.to(device)
b_gpu = b_cpu.to(device)
torch.cuda.synchronize() # データ転送が完了してから計測を開始
start = time.time()
c_gpu = torch.matmul(a_gpu, b_gpu)
torch.cuda.synchronize() # GPU の実行完了を待ってから計測を停止
print(f"GPU 所要時間: {time.time() - start:.3f}s")
出力例:
CPU 耗时: 1.847s GPU 耗时: 0.021s
GPU 計算は非同期実行であり、Python の呼び出しが戻った後も、GPU 上の演算が完了していない可能性があります。計測時には必ず呼び出して、
torch.cuda.synchronize()GPU の実際の終了を待つ必要があります。そうしないと、測定結果が不正確になります。
4. モデルの GPU への移動
モデルのすべてのパラメータ(weight、bias)も本質的にはテンソルです。
これらのパラメータも GPU 上で順伝播と逆伝播を実行するには、GPU へ移動する必要があります。
モデル全体に対して呼び出す.to(device)だけで、PyTorch が自動的にすべての内部パラメータを走査して移動します。
例
import torch.nn as nn
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
class SimpleNet(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(784, 256),
nn.ReLU(),
nn.Linear(256, 128),
nn.ReLU(),
nn.Linear(128, 10),
)
def forward(self, x):
return self.net(x)
# モデルを GPU へ移動するには、1 回呼び出すだけでよい
model = SimpleNet().to(device)
# パラメータがすべて GPU 上にあるか確認
for name, param in model.named_parameters():
print(f"{name}: {param.device}")
# net.0.weight: cuda:0
# net.0.bias: cuda:0
# ...
# 入力データも同じデバイス上にある必要がある
x = torch.randn(32, 784).to(device)
output = model(x)
print(output.shape) # torch.Size([32, 10])
モデルが GPU 上にあっても、入力データが CPU 上にある場合、順伝播でエラーが発生します。DataLoader が各バッチを読み込んだ後、必ず
inputsとlabelsすべて .to(device) を呼び出してください。.to(device)。
5. 完全なトレーニングフロー
以下は、データ読み込み、モデルトレーニング、検証評価を含む標準的な GPU トレーニングテンプレートです。
例
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
# 1. デバイス設定
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"トレーニングデバイス: {device}")
# 2. データ読み込み
# pin_memory=True: データをメモリに固定し、CPU -> GPU 転送を高速化
# num_workers: マルチプロセスによるプリロードで、データ待機時間を短縮
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,)),
])
train_dataset = datasets.MNIST(root="./data", train=True,
download=True, transform=transform)
test_dataset = datasets.MNIST(root="./data", train=False,
download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=256, shuffle=True,
num_workers=4, pin_memory=True)
test_loader = DataLoader(test_dataset, batch_size=256, shuffle=False,
num_workers=4, pin_memory=True)
# 3. モデルを定義
class CNN(nn.Module):
def __init__(self):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(1, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(64 * 7 * 7, 256), nn.ReLU(), nn.Dropout(0.5),
nn.Linear(256, 10),
)
def forward(self, x):
return self.classifier(self.features(x))
model = CNN().to(device) # モデルを GPU へ移動
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3)
# 4. トレーニング関数
def train_epoch(model, loader, optimizer, criterion):
model.train()
total_loss, correct = 0.0, 0
for inputs, labels in loader:
# non_blocking=True:非同期転送、CPUは次のバッチデータを準備し続けることができる
inputs = inputs.to(device, non_blocking=True)
labels = labels.to(device, non_blocking=True)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
total_loss += loss.item() * inputs.size(0)
correct += (outputs.argmax(1) == labels).sum().item()
n = len(loader.dataset)
return total_loss / n, correct / n
# 5. 検証関数
def eval_epoch(model, loader, criterion):
model.eval()
total_loss, correct = 0.0, 0
with torch.no_grad():
for inputs, labels in loader:
inputs = inputs.to(device, non_blocking=True)
labels = labels.to(device, non_blocking=True)
outputs = model(inputs)
loss = criterion(outputs, labels)
total_loss += loss.item() * inputs.size(0)
correct += (outputs.argmax(1) == labels).sum().item()
n = len(loader.dataset)
return total_loss / n, correct / n
# 6. メインのトレーニングループ
for epoch in range(1, 11):
train_loss, train_acc = train_epoch(model, train_loader, optimizer, criterion)
val_loss, val_acc = eval_epoch(model, test_loader, criterion)
print(f"Epoch {epoch:02d} | "
f"Train Loss: {train_loss:.4f}, Acc: {train_acc:.4f} | "
f"Val Loss: {val_loss:.4f}, Acc: {val_acc:.4f}")
重要なポイント:
device変数はコードの最上部で宣言し、グローバルに統一参照する。ハードコーディングしない。"cuda"- モデル
.to(device)一度だけ呼び出す - 各バッチの
inputsとlabelsすべて.to(device) - DataLoaderの設定
pin_memory=Trueとnon_blocking=Trueデータ転送の高速化 - 検証段階では使用
torch.no_grad()勾配をオフにし、GPUメモリを節約
6. マルチ GPU トレーニング
単一GPUのメモリが不足する場合、複数GPUでの並列トレーニングを検討できます。
さらに、複数GPUはトレーニング速度をさらに向上させることができます。PyTorchには主に2つの方法があります。
6.1 DataParallel
これは最も簡単な複数GPU方式です。
シングルプロセスで、各バッチを各GPUに均等に分割し、順伝播を並列実行し、勾配をメインGPUで集約して更新します。使いやすいですが、メインGPUの負担が重く、複数GPUの利用率が不均衡で、クイックスタートに適しています。
例
import torch.nn as nn
model = CNN()
if torch.cuda.device_count() > 1:
print(f"使用 {torch.cuda.device_count()} 個のGPU")
model = nn.DataParallel(model)
# 使用するGPUを指定することもできます
# model = nn.DataParallel(model, device_ids=[0, 1])
model = model.to("cuda")
# 以降のトレーニングコードは単一GPUと完全に同じです
# DataParallel はバッチを各GPUに自動で均等分割し、結果を集約します
元のモデルの属性(カスタムメソッドなど)にアクセスする必要がある場合は、以下を使用する必要があります.moduleアクセス:
例
print(model.module.classifier)
# モデル保存時は model.module を保存することをお勧めします。単一GPUでの読み込みが容易になります
torch.save(model.module.state_dict(), "model.pth")
6.2 DistributedDataParallel
これはマルチプロセス方式です。
各プロセスが1つのGPUにバインドされ、各GPUが完全なモデルを保持し、All-Reduceで勾配を同期します。本番環境や大規模トレーニングに推奨される方法で、DataParallelよりもはるかに効率的です。
例
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data.distributed import DistributedSampler
def main(rank, world_size):
# プロセスグループを初期化。nccl バックエンドは NVIDIA GPU 向けに最適化されています
dist.init_process_group(backend="nccl", rank=rank, world_size=world_size)
# 各プロセスを対応するGPUにバインド
torch.cuda.set_device(rank)
device = torch.device(f"cuda:{rank}")
# モデルを対応するGPUに移動後、DDPでラップ
model = CNN().to(device)
model = DDP(model, device_ids=[rank])
# DataLoader で DistributedSampler を使用し、各GPUのデータが重複しないようにします
sampler = DistributedSampler(train_dataset,
num_replicas=world_size, rank=rank)
loader = DataLoader(train_dataset, batch_size=64,
sampler=sampler, pin_memory=True)
# トレーニングロジックは単一GPUと完全に同じ
# ...
dist.destroy_process_group()
# 起動方法(torchrun を推奨):
# torchrun --nproc_per_node=4 train_ddp.py
| 比較項目 | DataParallel | DistributedDataParallel |
|---|---|---|
| プロセス数 | シングルプロセス | マルチプロセス(GPUごとに1つ) |
| 通信バックエンド | Python GIL 制限 | NCCL(高効率) |
| メインGPUの負担 | 重い(勾配の集約) | 均衡(All-Reduce) |
| コード変更 | 極めて少ない | 中程度 |
| 適用シーン | 高速実験 | 本番トレーニング |
7. 混合精度トレーニング AMP
デフォルトのトレーニングは float32(FP32)精度を使用します。
自動混合精度(Automatic Mixed Precision、AMP)一部の計算で float16(FP16)または bfloat16(BF16)を使用することで、精度をほとんど損なうことなく大きなメリットを得られます:
- GPUメモリ使用量が約50%削減
- トレーニング速度が2倍~3倍向上(Tensor Core ハードウェアアクセラレーション)
- コード変更は極めて少なく、3箇所の修正のみ
例
model = CNN().to(device)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
scaler = GradScaler() # FP16 勾配スケーラー。勾配のアンダーフローによるゼロ化を防ぎます
for epoch in range(num_epochs):
model.train()
for inputs, labels in train_loader:
inputs = inputs.to(device, non_blocking=True)
labels = labels.to(device, non_blocking=True)
optimizer.zero_grad()
# 変更1:autocast 領域内で FP16/FP32 を自動選択
with autocast(device_type="cuda"):
outputs = model(inputs)
loss = criterion(outputs, labels)
# 変更2:scaler で loss をスケーリングしてから逆伝播
scaler.scale(loss).backward()
# 変更3:scaler がパラメータを更新。内部で勾配スケーリングを自動処理
scaler.step(optimizer)
scaler.update()
7.1 FP16 と BF16 の選択
| 形式 | 精度ビット | 数値範囲ビット | 対応ハードウェア | 特徴 |
|---|---|---|---|---|
| float16 | 10 ビット | 5 ビット | RTX 20/30/40 シリーズ | GradScaler によるオーバーフロー防止が必要 |
| bfloat16 | 7 ビット | 8 ビット | A100 / H100 / RTX 4090 | FP32 と同じ範囲で、トレーニングがより安定 |
GPU が BF16 をサポートしている場合、優先的に使用し、GradScaler は不要です:
例
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
8. パフォーマンス最適化のテクニック
このセクションでは、GPUメモリとトレーニング速度を最適化するさまざまなテクニックを紹介します。
8.1 VRAM 管理
例
print(f"割り当て済み: {torch.cuda.memory_allocated() / 1024**2:.1f} MB")
print(f"キャッシュ済み: {torch.cuda.memory_reserved() / 1024**2:.1f} MB")
# 詳細なGPUメモリレポートを出力
print(torch.cuda.memory_summary())
# キャッシュプールを解放(割り当て済みのGPUメモリは解放しない)
torch.cuda.empty_cache()
# 推論時は inference_mode を使用(no_grad より速く、勾配エンジンを完全に無効化)
with torch.inference_mode():
output = model(x)
# 勾配チェックポイント:再計算と引き換えにGPUメモリを節約(大規模モデルのトレーニングでよく使用)
from torch.utils.checkpoint import checkpoint
output = checkpoint(model_block, x)
8.2 DataLoader の最適化
例
dataset,
batch_size=256,
num_workers=4, # マルチプロセス事前読み込み。CPUコア数の50%に設定することを推奨
pin_memory=True, # ページロックメモリ。CPU -> GPU 転送を高速化
persistent_workers=True, # プロセスを常駐させ、各エポックでプロセスを再作成するのを防ぐ
prefetch_factor=2, # 各ワーカーが事前取得するバッチ数
)
8.3 torch.compile によるモデルコンパイル(PyTorch 2.0+)
1行のコードで計算グラフをコンパイル最適化し、速度が30%〜200%向上します(モデル構造によります):
例
model = torch.compile(model)
# 異なるコンパイルモードのトレードオフ
model = torch.compile(model, mode="default") # バランスが取れており、ほとんどのシーンに適しています
model = torch.compile(model, mode="reduce-overhead") # スケジューリングオーバーヘッドを低減
model = torch.compile(model, mode="max-autotune") # 最大最適化。コンパイル時間が長い
8.4 勾配累積(大きなバッチをシミュレート)
GPUメモリが不足する場合、勾配累積により実際のGPUメモリ使用量を増やさずに、より大きなバッチサイズをシミュレートできます:
例
for i, (inputs, labels) in enumerate(train_loader):
inputs = inputs.to(device)
labels = labels.to(device)
outputs = model(inputs)
loss = criterion(outputs, labels) / accumulation_steps # loss を均等に分割
loss.backward() # 勾配累積。ゼロにしない
if (i + 1) % accumulation_steps == 0:
optimizer.step()
optimizer.zero_grad() # パラメータ更新後にのみゼロにする
8.5 VRAM 不足時の対処戦略
| 戦略 | 方法 | 効果 |
|---|---|---|
| バッチサイズを小さくする | 256 -> 64 | GPUメモリが線形に減少 |
| 混合精度 | AMP + FP16/BF16 | GPUメモリが約50%削減 |
| 勾配累積 | Nステップごとに更新 | 大きいバッチをシミュレートし、GPUメモリを増やさない |
| 勾配チェックポイント | torch.utils.checkpoint |
GPUメモリを大幅に削減するが、トレーニング速度は低下 |
| 一部のパラメータを凍結 | 転移学習でバックボーンを凍結 | 逆伝播のGPUメモリ使用量を削減 |
9. よくあるエラーとトラブルシューティング
このセクションでは、GPUトレーニングで最もよくあるエラーとその解決策をまとめています。
9.1 RuntimeError: Expected all tensors to be on the same device
原因:演算に参加するテンソルが異なるデバイスに配置されている(一方がCPU、もう一方がGPU)。
例
print(inputs.device, labels.device, next(model.parameters()).device)
# 解決策:各バッチで .to(device) を実行していることを確認
inputs = inputs.to(device)
labels = labels.to(device)
9.2 RuntimeError: CUDA out of memory
原因:GPUメモリ不足。バッチサイズが大きすぎる、モデルが大きすぎる、計算グラフが意図せず蓄積される場合によく発生します。
例
# 誤った書き方
total_loss += loss # loss はテンソルであり、計算グラフ全体を保持する
# 正しい書き方
total_loss += loss.item() # .item() で Python スカラーを取り出し、計算グラフを解放
# その他の調査手順:
# 1. batch_size を小さくする
# 2. 検証ループで torch.no_grad() を使用していることを確認
# 3. torch.cuda.empty_cache() を呼び出してキャッシュをクリアする
# 4. torch.cuda.memory_summary() を使用してVRAM使用量の原因を特定する
9.3 Can't call numpy() on Tensor that requires grad
原因:勾配を持つテンソルは直接numpy配列に変換できません。
例
arr = gpu_tensor.numpy()
# 正しい書き方
arr = gpu_tensor.detach().cpu().numpy()
9.4 CUDA error: device-side assert triggered
原因:通常はラベル値が範囲外(例:10クラス分類なのにlabel値が10)、または配列インデックスが範囲外です。エラー情報はGPU上で発生するため、デフォルトの表示位置は正確ではありません。
例
import os
os.environ["CUDA_LAUNCH_BLOCKING"] = "1"
9.5 トレーニング速度が向上しない(GPU 使用率が低い)
原因:データ読み込みがボトルネックとなり、GPUはほとんどの時間CPUのデータ準備を待っています。
調査手順:
- 実行
nvidia-smi或watch -n 1 nvidia-smiGPU使用率を確認- 使用率が継続的に80%未満の場合はデータのボトルネックが存在します
- DataLoaderのnum_workersを増やす
- pin_memory=True を有効にする
- データ前処理をGPUに移す(torchvision.transforms はGPU操作をサポート)
- 小さいファイルのデータセットをメモリに事前に読み込む
9.6 PyTorch Profiler を使用してボトルネックを正確に特定する
例
with profile(
activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],
record_shapes=True,
) as prof:
for i, (inputs, labels) in enumerate(train_loader):
if i >= 10:
break
inputs = inputs.to(device)
labels = labels.to(device)
loss = criterion(model(inputs), labels)
loss.backward()
optimizer.step()
optimizer.zero_grad()
# CUDAの実行時間順にソートし、上位15件を出力
print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=15))
10. API クイックリファレンス
以下は、よく使われるPyTorch GPU関連APIの早見表です。
10.1 デバイス管理
| 操作 | コード |
|---|---|
| CUDAの利用可能性を確認 | torch.cuda.is_available() |
| デバイスを選択 | torch.device("cuda" if ... else "cpu") |
| GPUの数 | torch.cuda.device_count() |
| GPU名 | torch.cuda.get_device_name(0) |
| 現在のGPUを設定 | torch.cuda.set_device(0) |
| GPUの完了を待機 | torch.cuda.synchronize() |
10.2 テンソル操作
| 操作 | コード |
|---|---|
| テンソルをGPUに移動 | tensor.to(device) 或 tensor.cuda() |
| テンソルをCPUに移動 | tensor.cpu() |
| デバイスを確認 | tensor.device |
| GPU上にあるか | tensor.is_cuda |
| GPUテンソルをnumpyに変換 | tensor.detach().cpu().numpy() |
| 非同期転送 | tensor.to(device, non_blocking=True) |
10.3 モデル操作
| 操作 | コード |
|---|---|
| モデルをGPUに移動 | model.to(device) |
| トレーニングモードに設定 | model.train() |
| 推論モードに設定 | model.eval() |
| コンパイルによる高速化 | torch.compile(model) |
| 勾配を無効化 | with torch.no_grad(): |
| 推論モード | with torch.inference_mode(): |
10.4 VRAM 管理
| 操作 | コード |
|---|---|
| 割り当て済みVRAM | torch.cuda.memory_allocated() |
| キャッシュ済みVRAM | torch.cuda.memory_reserved() |
| VRAMレポート | torch.cuda.memory_summary() |
| キャッシュをクリア | torch.cuda.empty_cache() |
10.5 核となる原則の早見メモ
1. 用 device 变量统一管理,不要硬编码 "cuda" 2. 模型和数据必须在同一设备上,每个 batch 都要 .to(device) 3. 验证和推理时一定使用 torch.no_grad() 或 torch.inference_mode() 4. 生产训练推荐开启 AMP 混合精度,几乎免费获得 2x 加速 5. DataLoader 设置 pin_memory=True 和 num_workers >= 4 减少数据瓶颈 6. PyTorch 2.0+ 可以用 torch.compile(model) 一行提速 30%+ 7. GPU 计算是异步的,精确计时需要调用 torch.cuda.synchronize()その他の拡張