M4 Maxの深層学習ベンチマーク結果を公開|PyTorch, YOLOv8, LM Studio
M4 Max搭載のMacBook Proを購入したので、ディープラーニング周りのベンチマークを行ってみました。この記事では、Resnet18によるクラス分類、YOLOv8による物体検出、そして、LM Studioによる生成AIの実行についてベンチマークした結果を紹介します。
はじめに
はじめに
新しくMacBook Proを購入したので、ディープラーニング関連のベンチマークをしてみました。購入目的自体、生成AIをローカルで動かすことでしたので、LM Studioを用いてLLMの実行速度もチェックしてみました。
環境詳細
MacBook Pro M4Max(16CPU, 40GPU) メモリ128GB, SSD 2TB
Resnet18
今回は、以下のプログラムを使わせていただきました。
Apple SiliconでPytorchを動かしてみる M1MAX vs NVIDIA GPU
https://qiita.com/satoshi_mac/items/72ecebeef459c2c47e12
上記に掲載されているプログラムを動かした結果は以下になります(注意:動作確認のため一度動かししているので、初回のダウンロードはスキップし、Files already downloaded
となっています)
Files already downloaded and verified
Epoch 1 / 5: time = 53.59[s], loss = 168.31
Epoch 2 / 5: time = 106.08[s], loss = 135.75
Epoch 3 / 5: time = 158.66[s], loss = 122.03
Epoch 4 / 5: time = 211.23[s], loss = 110.31
Epoch 5 / 5: time = 263.93[s], loss = 102.93
Train time on mps: 263.93[s] (Train loss = 102.93)
Files already downloaded and verified
Test time on mps: 46.50[s](Test loss = 19.82)
学習時間は263.93秒です。速度より気になったのは、GPUの使用履歴を見るとGPUが間欠的にしか動いていないことです。さらに、CPUの履歴を見るとCPUもサボっていました。
この手の経験は以前もしていて、そのときはDataLoaderが原因でした。
今回も「dataloaderの処理が軽いのに過度に並列化しているのが原因(num_workers=8
)かな?」と考えて、並列化しない設定(num_workers=0
)に変更してみました。
num_workers=0
にした結果が以下になります。
Files already downloaded and verified
Epoch 1 / 5: time = 7.94[s], loss = 169.37
Epoch 2 / 5: time = 15.52[s], loss = 137.06
Epoch 3 / 5: time = 23.10[s], loss = 124.06
Epoch 4 / 5: time = 30.91[s], loss = 113.45
Epoch 5 / 5: time = 38.56[s], loss = 105.84
Train time on mps: 38.56[s] (Train loss = 105.84)
Files already downloaded and verified
Test time on mps: 0.71[s](Test loss = 20.21)
やはり、データローダーが原因だったみたいで学習時間は38.56秒になりました。
さらに調査
num_workers
の件、もう少し調べてみました。
調べてみると、macのPythonのマルチプロセッシングの起動と終了処理が遅いみたいでした。
対策としては、multiprocessing_context="fork"
を設定すれば良さそうだったのでこれを設定してみました。
結果は以下のようになりました。
Files already downloaded and verified
Epoch 1 / 5: time = 4.43[s], loss = 169.16
Epoch 2 / 5: time = 8.60[s], loss = 137.19
Epoch 3 / 5: time = 12.81[s], loss = 122.96
Epoch 4 / 5: time = 16.96[s], loss = 113.19
Epoch 5 / 5: time = 21.16[s], loss = 104.72
Train time on mps: 21.16[s] (Train loss = 104.72)
Files already downloaded and verified
Test time on mps: 0.43[s](Test loss = 19.70)
なんと、学習時間は21.16秒まで短縮できました!
修正したコードは以下のようになります(21, 22行目と38行目、71行目が参考にしたコードから変更されています)
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
worker_setting = {"num_workers": 12,
"multiprocessing_context":"fork"}
# 訓練データとテストデータを用意
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, **worker_setting)
# 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()
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, **worker_setting)
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)
コードを参照した記事に、RTX3070 8GBで、96.14秒とコメントがあったのでまずまずの速度かなと思います(もしwindowsの結果であれば、同様にnum_workersの影響があるのでなんとも言えませんが)
Google Colab(T4)で実行した結果は以下の通りです。
M4Maxであれば、T4よりは高速なようです。ただ、testはこちらの方が高速でした(num_workers=2
の方が高速でしたので「2」に設定して実行しました)
Epoch 1 / 5: time = 30.08[s], loss = 171.71 Epoch 2 / 5: time = 57.33[s], loss = 137.89 Epoch 3 / 5: time = 85.71[s], loss = 123.88 Epoch 4 / 5: time = 114.34[s], loss = 114.84 Epoch 5 / 5: time = 141.67[s], loss = 106.57 Train time on cuda: 141.67[s] (Train loss = 106.57) Files already downloaded and verified Test time on cuda: 2.60[s](Test loss = 20.73)
Macで高速に学習させるには、少し工夫が必要かもしれません。
YOLOv8
YOLOv8の学習の実験を行いました。コードや手順は以下の記事と同じです。
CPUで学習
まずは、CPUでの学習結果です。以下は出力です。
Image sizes 640 train, 640 val
Using 0 dataloader workers
Logging results to runs/detect/train4
Starting training for 10 epochs...
Closing dataloader mosaic
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
1/10 0G 1.405 2.619 1.15 5 640: 100%|██████████| 36/36 [00:32<00:00, 1.09it/s]
Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 5/5 [00:04<00:00, 1.04i
all 71 100 0.0046 0.98 0.435 0.153
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
2/10 0G 1.368 1.86 1.187 6 640: 100%|██████████| 36/36 [00:33<00:00, 1.07it/s]
Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 5/5 [00:04<00:00, 1.05i
all 71 100 1 0.128 0.568 0.339
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
3/10 0G 1.341 1.683 1.144 8 640: 100%|██████████| 36/36 [00:33<00:00, 1.06it/s]
Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 5/5 [00:04<00:00, 1.07i
all 71 100 0.989 0.902 0.966 0.625
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
4/10 0G 1.291 1.486 1.138 9 640: 100%|██████████| 36/36 [00:33<00:00, 1.07it/s]
Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 5/5 [00:04<00:00, 1.06i
all 71 100 0.959 0.946 0.982 0.616
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
5/10 0G 1.273 1.351 1.126 9 640: 100%|██████████| 36/36 [00:34<00:00, 1.04it/s]
Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 5/5 [00:04<00:00, 1.04i
all 71 100 0.985 0.96 0.991 0.63
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
6/10 0G 1.257 1.251 1.131 3 640: 100%|██████████| 36/36 [00:34<00:00, 1.05it/s]
Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 5/5 [00:04<00:00, 1.05i
all 71 100 0.966 0.99 0.992 0.613
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
7/10 0G 1.221 1.16 1.085 7 640: 100%|██████████| 36/36 [00:34<00:00, 1.05it/s]
Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 5/5 [00:04<00:00, 1.03i
all 71 100 0.983 0.98 0.994 0.626
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
8/10 0G 1.212 1.107 1.105 4 640: 100%|██████████| 36/36 [00:33<00:00, 1.07it/s]
Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 5/5 [00:04<00:00, 1.04i
all 71 100 0.978 0.98 0.992 0.651
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
9/10 0G 1.165 1.05 1.074 4 640: 100%|██████████| 36/36 [00:34<00:00, 1.05it/s]
Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 5/5 [00:04<00:00, 1.06i
all 71 100 0.98 0.958 0.99 0.641
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
10/10 0G 1.157 0.9822 1.039 8 640: 100%|██████████| 36/36 [00:34<00:00, 1.05it/s]
Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 5/5 [00:04<00:00, 1.01i
all 71 100 0.989 0.95 0.99 0.642
10 epochs completed in 0.108 hours.
Optimizer stripped from runs/detect/train4/weights/last.pt, 6.2MB
Optimizer stripped from runs/detect/train4/weights/best.pt, 6.2MB
Validating runs/detect/train4/weights/best.pt...
Ultralytics 8.3.28 🚀 Python-3.12.7 torch-2.5.1 CPU (Apple M4 Max)
Model summary (fused): 168 layers, 3,005,843 parameters, 0 gradients, 8.1 GFLOPs
Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 5/5 [00:04<00:00, 1.05i
all 71 100 0.978 0.98 0.992 0.651
Speed: 0.2ms preprocess, 62.9ms inference, 0.0ms loss, 0.7ms postprocess per image
Results saved to runs/detect/train4
411.6687319278717sec.
結果は411秒でした。
処理中のCPUの履歴を見ると16コア全てが使われていることが確認できます。
GPUで学習
GPUを利用した学習結果です。以下は出力です。
Image sizes 640 train, 640 val
Using 0 dataloader workers
Logging results to runs/detect/train6
Starting training for 10 epochs...
Closing dataloader mosaic
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
1/10 2.41G 1.409 2.619 1.15 5 640: 100%|██████████| 36/36 [00:11<00:00, 3.19it/s]
Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 5/5 [00:04<00:00, 1.12i
all 71 100 0.0046 0.98 0.386 0.143
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
2/10 2.45G 1.38 1.84 1.186 6 640: 100%|██████████| 36/36 [00:09<00:00, 3.63it/s]
Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 5/5 [00:02<00:00, 1.78i
all 71 100 1 0.279 0.902 0.555
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
3/10 2.45G 1.364 1.678 1.153 8 640: 100%|██████████| 36/36 [00:10<00:00, 3.50it/s]
Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 5/5 [00:02<00:00, 2.31i
all 71 100 0.979 0.926 0.973 0.612
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
4/10 2.46G 1.346 1.58 1.159 9 640: 100%|██████████| 36/36 [00:09<00:00, 3.77it/s]
Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 5/5 [00:06<00:00, 1.28s
all 71 100 0.99 0.954 0.984 0.602
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
5/10 2.45G 1.301 1.387 1.147 9 640: 100%|██████████| 36/36 [00:09<00:00, 3.67it/s]
Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 5/5 [00:08<00:00, 1.70s
all 71 100 0.98 0.956 0.983 0.608
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
6/10 2.46G 1.263 1.288 1.149 3 640: 100%|██████████| 36/36 [00:12<00:00, 2.89it/s]
Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 5/5 [00:06<00:00, 1.22s
all 71 100 1 0.939 0.988 0.622
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
7/10 2.46G 1.224 1.186 1.108 7 640: 100%|██████████| 36/36 [00:10<00:00, 3.28it/s]
Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 5/5 [00:04<00:00, 1.20i
all 71 100 0.99 0.962 0.987 0.626
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
8/10 2.46G 1.227 1.114 1.113 4 640: 100%|██████████| 36/36 [00:11<00:00, 3.20it/s]
Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 5/5 [00:08<00:00, 1.74s
all 71 100 0.998 0.96 0.986 0.65
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
9/10 2.47G 1.195 1.057 1.09 4 640: 100%|██████████| 36/36 [00:12<00:00, 2.87it/s]
Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 5/5 [00:06<00:00, 1.31s
all 71 100 0.999 0.97 0.992 0.654
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
10/10 2.47G 1.159 0.993 1.063 8 640: 100%|██████████| 36/36 [00:12<00:00, 2.91it/s]
Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 5/5 [00:06<00:00, 1.22s
all 71 100 0.997 0.98 0.992 0.658
10 epochs completed in 0.047 hours.
Optimizer stripped from runs/detect/train6/weights/last.pt, 6.2MB
Optimizer stripped from runs/detect/train6/weights/best.pt, 6.2MB
Validating runs/detect/train6/weights/best.pt...
Ultralytics 8.3.28 🚀 Python-3.12.7 torch-2.5.1 MPS (Apple M4 Max)
Model summary (fused): 168 layers, 3,005,843 parameters, 0 gradients, 8.1 GFLOPs
Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 5/5 [00:05<00:00, 1.00s
all 71 100 0.997 0.98 0.992 0.658
Speed: 0.5ms preprocess, 9.5ms inference, 0.0ms loss, 40.1ms postprocess per image
Results saved to runs/detect/train6
186.84874892234802sec.
結果は186秒でした。
YOLOv8の結果まとめ
以前の結果にマージして記録を追加しました。M2と比べると高速ですが、思ったより早く無いという印象です(どこか、ボトルネックがあるのかもしれません)。
また、kaggleで使われているP100と比べるとまだまだ遅いという結果になりました。高速になったと言っても、ようやく使えるレベルになったと考えて良いかもしれません。
デバイス | 処理時間(秒) | |
M4 Max | device=”cpu” | 411.67 sec. |
device=”mps” | 186.85 sec. | |
M2 | device=”cpu” | 630.54 sec. |
device=”mps” | 334.40 sec. | |
M1Pro | device=”cpu” | 582.68 sec. |
device=”mps” | 372.99 sec. | |
参考:GPU=P100(kaggle notebook) | 119 sec. |
LM Studio
今回、M4 MaxのMacBook Airを購入したのは、LLMを動かすのが目的です。こちらについては、速度的な問題もありますが、メモリ量の問題でGPUより有利です(メモリが少ないGPUではそもそも動かせない)。
測定結果
LM Studioをつかって生成AIのトークン生成速度を測定してみました。今回はLlama3(3B, 70B)と、Command R+, Swallow(8B, 70B)です。Swallowの8B以外は4bitで量子化されたモデルを使いました。
モデル名 | メモリ使用量(GB) | token/s | |
Meta LLama 3(3B) | Llama-3.2-3B-Instruct-4bit | 2.20GB | 177.37 |
Meta LLama 3(70B) | Llama-3.1-Nemotron-70B-Instruct-GGUF | 40.03GB | 8.60 |
Command R+ | c4ai-command-r-plus-08-2024-gguf | 59.84GB | 5.88 |
Swallow-8B | Llama-3.1-Swallow-8B-Instruct-v0.1 | 15.34GB | 29.64 |
Swallow-70B | Llama-3.1-Swallow-70B-Instruct-v0.1-4bit | 36.72GB | 11.66 |
10token/sくらいだと読む速度に対して出力が遅すぎるという感じはあまりなかったです。
3Bのモデルは流石に高速です。今回128GBのモデルを購入したのは、Command R+などの大きなモデルを動作させるためですが、問題なく動作することが確認できました。生成速度は5.88token/sと少しかかります。
実際の変換速度の体感がわかるようにgifアニメーションにしてみました。以下のアニメーションはLlama3 70B, 8.60token/sの場合です。使える範囲の速度で動いていると思いますがいかがでしょうか。
質問内容と回答
以下は、質問と質問に対して出力された内容です。3Bの小さいモデルは、流石に使える感じはしませんが、70Bになるといい感じの回答になっている気がします。
横断歩道を渡る時に行うべき安全確認を3つ挙げてください
横断歩道を渡る際に行うべき安全確認は次の3点です。
歩き先を見つける: まず、横断歩道の向こう側にいる人や物体があるかどうかを確認する必要があります。歩き先は、人が見つかったら、実行してから渡るようにします
歩道と車道を判断する: まず、横断歩道が安全に渡ることができるかどうかを判断する必要があります。横断歩道が狭い、車の流れが激しい、またはうわばいがある場合は、横断することにはしません。
車の通行を確認する: 追補走路上などで、車がすでに通過しているかどうかを確認する必要があります。車が通行している場合は、/wait until… にはしないでください。
横断歩道を渡る際の安全確認として、以下の3点を実施することが重要です。
- 左・右・左(または右・左・右)で車両の確認:
- 左側から見て車が来ないか確認します。
- 次に、反対側(右側)を見て車が来ないか確認します。
- さらに、一度目と同じ側(左側)を見直して、急に近づいてきた車がないか最終確認を行います。(日本では右側が優先となる場合もあるため、状況に応じて「右・左・右」で確認することもあります。)
- 後ろから来る自転車やオートバイの確認:
- 車以外にも自転車やオートバイが後ろから接近してくる可能性があるため、横断歩道に入る前に後ろを確認します。これらの軽量高速移動体は予測しにくいので、特に注意が必要です。
- 信号機の確認(青信号や歩行者信号の確認):
横断歩道に信号機がある場合は、その信号が自分たちの通行を許可するものであることを確認しましょう。歩行者用の信号で「青」または「歩行者用のアイコン」が表示されているときは、車両よりも優先的に横断できます。しかし、赤信号や「止まれ」のアイコンが表示されていたら、絶対に横断しないようにします。
- 横断歩道の信号が青(または渡ってよいことを示している)であることを確認する
- 左右の車やバイクなどの車両がないか、注意深く見たり聞いたりして確かめる。特に死角から来る可能性のある車にも気を配る必要があります。
- 周囲に他の歩行者がいないかをチェックし、横断を始める
- 歩道を渡る前に、車の流れや信号機の状態を確認します。
- 歩道を渡る途中で、車が近づいているかどうかを確認します。
- 歩道を渡り終わった後も、車が近づいていないかどうかを確認します。
横断歩道を渡る時に行うべき安全確認は以下の3つです。
- 歩行者用信号機が青になっているか確認します。
- 車や自転車などが近くにいないか確認します。
- 横断歩道の両端から車や自転車などが来ないか確認します。
ちなみに、電源モードを「低電力」にしても「自動」にしてもtoken/s
はほとんど変化しませんでした。GPUにオフロードされている状態ではあまり変化しないみたいです。
まとめ
M4 Max搭載のMacBook Proを購入したので、とりあえず深層学習関連のベンチマークを行ってみました。正直なところ、期待していたほどパフォーマンスは高くありませんでした(GPUのコア数が40とM2の8と比べて多くなっているにもかかわらず、それほど速度は上がっていない)。
Bertの学習も行ってみましたが、やはりGoogle Colab(T4)より少し高速な程度でした。ただ、メモリが多いのでBatchsizeを増やして実行できるのが大きいかなと思いました。Bertなどの言語モデルの場合、16GB, 24GBではどうしてもバッチサイズが小さくなってしまいますのでその倍程度のバッチサイズで回せるのはメリットです。NVIDAのGPUの場合、48GBなどのメモリを搭載したGPUは大変高価ですので。