Apple SiliconでPytorchを動かしてみる M1MAX vs NVIDIA GPU

こんにちは、ドイです。
Macでディープラーニングの勉強をすべく記事を書きためていこうと思っています。
今回はPytorchでのMacのGPU利用と、性能確認を行います。

## PytorchでMacのGPUを利用する
PytorchがM1チップなどApple Silicon MacでGPUの利用が可能となりました。

Introducing Accelerated PyTorch Training on Mac
https://pytorch.org/blog/introducing-accelerated-pytorch-training-on-mac/


>PyTorch v1.12 release, developers and researchers can take advantage of Apple silicon GPUs for significantly faster model training.

DeepLでの翻訳
>PyTorch v1.12では、開発者や研究者はAppleのシリコンGPUを活用し、大幅に高速なモデルトレーニングを行えるようになります。

（以下、引用部分はDeep Lで翻訳した内容を引用）

AppleのMetal Performance Shaders（MPS）を利用することでMacのGPUを利用することができるようです。
これまではCPUしか利用できませんでしたが、M1やM2などのApple シリコンチップであれば内蔵GPUを使うことができます。

>Apple製Macは、ユニファイドメモリアーキテクチャを採用しており、
GPUがすべてのメモリストアに直接アクセスできるようになっています。
このため、Macは機械学習のための優れたプラットフォームとなり、
ユーザーはより大きなネットワークやバッチサイズをローカルでトレーニングすることができます。
これにより、クラウドベースの開発やローカルGPUの追加に関連するコストを削減できます。

環境はmacOS 12.3以降
すべてMacでとありますがIntelCPUのMacを持ち合わせていないので他の方で試した結果を教えてくださいね。
一応、、Appleシリコンシリーズだけの話かなと思って書いています。


mpsをデバイスとして選べるかどうかの判断は、
torch.backends.mps.is_available()
を用いて調べることができます。

結果がゼロになればOKです。

## M1MAXでCPU vs GPU（mps）やってみた
PytorchでResnet18を用いたディープラーニングで学習時間を検証します。
用いるMacは、M1MAX 14インチMacBook Proになります。
10coreCPU 24core GPU メモリ32GBです。

プログラムはこちらを使わせていただきました。
https://github.com/HidetoshiKawaguchi/tech-blog-codes/tree/main/20220731_pytorch-m1-macbook-gpu


以下、デバイスの指定のところだけちょこっといじりました。
```
import torch
from torch.nn import CrossEntropyLoss
from torch.optim import SGD
from torch.utils.data import DataLoader
from torchvision.datasets import CIFAR10
from torchvision import transforms as tt
from torchvision.models import resnet18

import os
from argparse import ArgumentParser
import time

def main(device):
    # ResNetのハイパーパラメータ
    n_epoch = 5            # エポック数
    batch_size = 512       # ミニバッチサイズ
    momentum = 0.9         # SGDのmomentum
    lr = 0.01              # 学習率
    weight_decay = 0.00005 # weight decay

    # 訓練データとテストデータを用意
    mean = (0.491, 0.482, 0.446)
    std = (0.247, 0.243, 0.261)
    train_transform = tt.Compose([
        tt.RandomHorizontalFlip(p=0.5),
        tt.RandomCrop(size=32, padding=4, padding_mode='reflect'),
        tt.ToTensor(),
        tt.Normalize(mean=mean, std=std)
    ])
    test_transform = tt.Compose([tt.ToTensor(), tt.Normalize(mean, std)])
    root = os.path.dirname(os.path.abspath(__file__))
    train_set = CIFAR10(root=root, train=True,
                        download=True, transform=train_transform)
    train_loader = DataLoader(train_set, batch_size=batch_size,
                              shuffle=True, num_workers=8)

    # ResNetの準備
    resnet = resnet18()
    resnet.fc = torch.nn.Linear(resnet.fc.in_features, 10)

    # 訓練
    criterion = CrossEntropyLoss()
    optimizer = SGD(resnet.parameters(), lr=lr,
                    momentum=momentum, weight_decay=weight_decay)
    train_start_time = time.time()
    resnet.to(device)
    resnet.train()
    for epoch in range(1, n_epoch+1):
        train_loss = 0.0
        for inputs, labels in train_loader:
            inputs = inputs.to(device)
            optimizer.zero_grad()
            outputs = resnet(inputs)
            labels = labels.to(device)
            loss = criterion(outputs, labels)
            loss.backward()
            train_loss += loss.item()
            del loss  # メモリ節約のため
            optimizer.step()
        print('Epoch {} / {}: time = {:.2f}[s], loss = {:.2f}'.format(
            epoch, n_epoch, time.time() - train_start_time, train_loss))
    print('Train time on {}: {:.2f}[s] (Train loss = {:.2f})'.format(
        device, time.time() - train_start_time, train_loss))

    # 評価
    test_set = CIFAR10(root=root, train=False, download=True,
                       transform=test_transform)
    test_loader = DataLoader(test_set, batch_size=batch_size,
                             shuffle=False, num_workers=8)
    test_loss = 0.0
    test_start_time = time.time()
    resnet.eval()
    for inputs, labels in test_loader:
        inputs = inputs.to(device)
        outputs = resnet(inputs)
        labels = labels.to(device)
        loss = criterion(outputs, labels)
        test_loss += loss.item()
    print('Test time on {}: {:.2f}[s](Test loss = {:.2f})'.format(
        device, time.time() - test_start_time, test_loss))


if __name__ == '__main__':
    parser = ArgumentParser()
    parser.add_argument('--device', type=str, default='mps',
                        choices=['cpu', 'mps', 'cuda'])
    args = parser.parse_args()
    device = torch.device(args.device)
    main(device)

```

### M1MAXのCPUを指定
![ScreenShot 2022-08-23 22.04.45.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565773/ce05a64b-b8bb-6a7b-9518-e1663c937cc7.jpeg)
思ったほどCPU負荷が上がっていませんね。
もっと全力出してほしい。

```
% python pytorch_m1_macbook.py --device cpu
Downloading https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz to /Users/satoshi/git/cifar-10-python.tar.gz
100%|█████████████████████████████████████████████████████████████████████████████████████████████| 170498071/170498071 [00:53<00:00, 3205505.20it/s]
Extracting /Users/satoshi/git/cifar-10-python.tar.gz to /Users/satoshi/git
Epoch 1 / 5: time = 416.14[s], loss = 171.38
Epoch 2 / 5: time = 837.20[s], loss = 138.99
Epoch 3 / 5: time = 1257.28[s], loss = 124.96
Epoch 4 / 5: time = 1672.27[s], loss = 113.21
Epoch 5 / 5: time = 2122.82[s], loss = 104.77
Train time on cpu: 2122.82[s] (Train loss = 104.77)
Files already downloaded and verified

```
学習時間は2122秒。そこそこかかりましたね。

### M1MAXのGPUを指定
![ScreenShot 2022-08-23 22.22.29.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565773/e00c03d9-8932-0b0c-b9b0-a7ef00a0a9c2.jpeg)

GPUも目一杯使ってるわけではなさそうですね。
もしかしたらブースターかける方法があるのかも？？？
```
% python pytorch_m1_macbook.py --device mps
Files already downloaded and verified
Epoch 1 / 5: time = 68.32[s], loss = 173.48
Epoch 2 / 5: time = 128.38[s], loss = 138.95
Epoch 3 / 5: time = 188.78[s], loss = 124.67
Epoch 4 / 5: time = 248.96[s], loss = 114.29
Epoch 5 / 5: time = 309.02[s], loss = 106.39
Train time on mps: 309.02[s] (Train loss = 106.39)
Files already downloaded and verified
```

309秒！！

ここから参考に開発用ハイスペックマシンとの比較を行います。
### Win10のCPUを指定
Windows 10 Pro
Intel(R) Core(TM) i9-10980XE CPU @ 3.00GHz   3.00 GHz
```
Epoch 1 / 5: time = 77.13[s], loss = 168.85
Epoch 2 / 5: time = 152.89[s], loss = 136.54
Epoch 3 / 5: time = 228.79[s], loss = 123.56
Epoch 4 / 5: time = 304.61[s], loss = 113.26
Epoch 5 / 5: time = 379.44[s], loss = 104.18
Train time on cpu: 379.44[s] (Train loss = 104.18)
Files already downloaded and verified
Test time on cpu: 23.96[s](Test loss = 21.25)
```
学習時間は379.44秒
あら？？？M1MAXのGPUとさほど変わらない？？

### Win10のNVIDIA GPUを指定
NVIDIA Quadro RTX 8000
```
Epoch 1 / 5: time = 40.68[s], loss = 170.00
Epoch 2 / 5: time = 74.70[s], loss = 135.34
Epoch 3 / 5: time = 108.20[s], loss = 120.69
Epoch 4 / 5: time = 142.11[s], loss = 111.18
Epoch 5 / 5: time = 176.35[s], loss = 104.24
Train time on cuda: 176.35[s] (Train loss = 104.24)
Files already downloaded and verified
Test time on cuda: 23.13[s](Test loss = 20.66)
```

学習時間は176秒　圧倒的！！
NVIDIA Quadro RTX 8000はめちゃんこ高いGPUですもんね。
まだまだ性能的にはNVIDIAのGPUの方が良さそうですが、
Macの健闘しましたね（と思いたい）

これからもMacに特化して勉強しながら記事を書きたいと思います。

参考記事
https://zenn.dev/hidetoshi/articles/20220731_pytorch-m1-macbook-gpu