GPU使用率をCloudWatchで取得してみよう

GPUインスタンスのGPU(Graphics Processing Unit)使用率をCloudWatchで可視化してみました。
2020.09.16

GPU使用率のモニタリング

  • CloudWatchのカスタムメトリクスを利用してGPU使用率を取得します
  • CPU使用率ではなくGPU使用率の取得方法です

GPUの使用率も可視化できる!

参考元

Amazon CloudWatch で GPU 使用率をモニタリング | Amazon Web Services ブログ

EC2の準備

GPU系のインスタンスを起動します。検証環境は下記の通りです。

項目
OS Amazon Linux2
AMI Deep Learning AMI (Amazon Linux 2) Version 34.0
インスタンスタイプ G4dn.xlarge

EC2起動

Deep LearningAMIを検索します。Deep Learning AMI (Amazon Linux 2) Version 34.0からインスタンスを起動します。

IAMロール設定

カスタムメトリクスをCloudWatchに送信するためcloudwatch:PutMetricDataの許可が必要です。 AWS管理ポリシーのCloudWatchAgentServerPolicyをアタッチしたIAMロールを作成し、EC2にロールを設定しました。

GPUモニタリングの準備

モニタリングスクリプトの編集と、実行環境の作成します。

GPUモニタリングスクリプトの確認

Deep Learning AMIは下記ディレクトリにモニタリングスクリプトが置いてあります。

/home/ec2-user/tools/GPUCloudWatchMonitor/gpumon.py

スクリプト(gpumon.py)の設定変更

スクリプトをお使いの環境に合わせて変更します。設定変更後のスクリプト全文を載せています。

検証環境では下記の変更をしました。

  • EC2_REGION
    • us-east-1からap-northeast-1に変更
  • my_NameSpace
    • DeepLearningTrainからGPUMonitorに変更

my_NameSpaceはCloudWatchのこの部分に表示される名前です。

gpumon.py

# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License").
# You may not use this file except in compliance with the License.
# A copy of the License is located at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
#  or in the "license" file accompanying this file. This file is distributed
#  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
#  express or implied. See the License for the specific language governing
#  permissions and limitations under the License.


import urllib.request as urllib2
import boto3
from pynvml import *
from datetime import datetime
from time import sleep

### CHOOSE REGION ####
EC2_REGION = 'ap-northeast-1'

###CHOOSE NAMESPACE PARMETERS HERE###
my_NameSpace = 'GPUMonitor'

### CHOOSE PUSH INTERVAL ####
sleep_interval = 10

### CHOOSE STORAGE RESOLUTION (BETWEEN 1-60) ####
store_reso = 60

encoding = 'utf-8'

#Instance information
BASE_URL = 'http://169.254.169.254/latest/meta-data/'
INSTANCE_ID = urllib2.urlopen(BASE_URL + 'instance-id').read().decode(encoding)
IMAGE_ID = urllib2.urlopen(BASE_URL + 'ami-id').read().decode(encoding)
INSTANCE_TYPE = urllib2.urlopen(BASE_URL + 'instance-type').read().decode(encoding)
INSTANCE_AZ = urllib2.urlopen(BASE_URL + 'placement/availability-zone').read().decode(encoding)
EC2_REGION = INSTANCE_AZ[:-1]

TIMESTAMP = datetime.now().strftime('%Y-%m-%dT%H')
TMP_FILE = '/tmp/GPU_TEMP'
TMP_FILE_SAVED = TMP_FILE + TIMESTAMP

# Create CloudWatch client
cloudwatch = boto3.client('cloudwatch', region_name=EC2_REGION)


# Flag to push to CloudWatch
PUSH_TO_CW = True

def getPowerDraw(handle):
    try:
        powDraw = nvmlDeviceGetPowerUsage(handle) / 1000.0
        powDrawStr = '%.2f' % powDraw
    except NVMLError as err:
        powDrawStr = handleError(err)
        PUSH_TO_CW = False
    return powDrawStr

def getTemp(handle):
    try:
        temp = str(nvmlDeviceGetTemperature(handle, NVML_TEMPERATURE_GPU))
    except NVMLError as err:
        temp = handleError(err)
        PUSH_TO_CW = False
    return temp

def getUtilization(handle):
    try:
        util = nvmlDeviceGetUtilizationRates(handle)
        gpu_util = str(util.gpu)
        mem_util = str(util.memory)
    except NVMLError as err:
        error = handleError(err)
        gpu_util = error
        mem_util = error
        PUSH_TO_CW = False
    return util, gpu_util, mem_util

def logResults(i, util, gpu_util, mem_util, powDrawStr, temp):
    try:
        gpu_logs = open(TMP_FILE_SAVED, 'a+')
        writeString = str(i) + ',' + gpu_util + ',' + mem_util + ',' + powDrawStr + ',' + temp + '\n'
        gpu_logs.write(writeString)
    except:
        print("Error writing to file ", gpu_logs)
    finally:
        gpu_logs.close()
    if (PUSH_TO_CW):
        MY_DIMENSIONS=[
                    {
                        'Name': 'InstanceId',
                        'Value': INSTANCE_ID
                    },
                    {
                        'Name': 'ImageId',
                        'Value': IMAGE_ID
                    },
                    {
                        'Name': 'InstanceType',
                        'Value': INSTANCE_TYPE
                    },
                    {
                        'Name': 'GPUNumber',
                        'Value': str(i)
                    }
                ]
        cloudwatch.put_metric_data(
            MetricData=[
                {
                    'MetricName': 'GPU Usage',
                    'Dimensions': MY_DIMENSIONS,
                    'Unit': 'Percent',
                    'StorageResolution': store_reso,
                    'Value': util.gpu
                },
                {
                    'MetricName': 'Memory Usage',
                    'Dimensions': MY_DIMENSIONS,
                    'Unit': 'Percent',
                    'StorageResolution': store_reso,
                    'Value': util.memory
                },
                {
                    'MetricName': 'Power Usage (Watts)',
                    'Dimensions': MY_DIMENSIONS,
                    'Unit': 'None',
                    'StorageResolution': store_reso,
                    'Value': float(powDrawStr)
                },
                {
                    'MetricName': 'Temperature (C)',
                    'Dimensions': MY_DIMENSIONS,
                    'Unit': 'None',
                    'StorageResolution': store_reso,
                    'Value': int(temp)
                },
        ],
            Namespace=my_NameSpace
        )


nvmlInit()
deviceCount = nvmlDeviceGetCount()

def main():
    try:
        while True:
            PUSH_TO_CW = True
            # Find the metrics for each GPU on instance
            for i in range(deviceCount):
                handle = nvmlDeviceGetHandleByIndex(i)

                powDrawStr = getPowerDraw(handle)
                temp = getTemp(handle)
                util, gpu_util, mem_util = getUtilization(handle)
                logResults(i, util, gpu_util, mem_util, powDrawStr, temp)

            sleep(sleep_interval)

    finally:
        nvmlShutdown()

if __name__=='__main__':
    main()

実行環境セットアップ

Deep Learningはセットアップ済みです。バージョンを確認します。

Python3の確認。

$ python3 -V
Python 3.7.6

必要なライブラリを確認。

$ pip3 list |grep -e boto3 -e nvidia
boto3               1.14.60
nvidia-ml-py        10.418.84

スクリプト実行

まずテスト実行してみます。数分放置してCloudWatchの画面します。

$ sudo python3 /home/ec2-user/tools/GPUCloudWatchMonitor/gpumon.py

GPUMonitorが増えています。

GPUMonitorをクリックするとメトリクスの項目が表示されます。

スクリプトを打ち切ります。

スクリプトのサービス化

ここからは参考記事のAmazon Web Services ブログの範囲外になります。GPUモニタリングのサービス化と、GPUへ負荷をかけてグラフを眺めて楽しみます。

$ sudo vi /etc/systemd/system/gpumon.service

ファイルの内容

gpumon.service

[Unit]
Description = gpumon daemon

[Service]
ExecStart =/bin/python3 /home/ec2-user/tools/GPUCloudWatchMonitor/gpumon.py
Restart = always
Type = simple

[Install]
WantedBy = multi-user.target

サービスの起動を確認します。

$ sudo systemctl daemon-reload
$ sudo systemctl start gpumon
$ sudo systemctl status gpumon
$ sudo systemctl enable gpumon

GPUの負荷テスト

gpu-burnを利用して手軽に負荷をかけてGPUをモニタリングできているか確認します。

wilicc/gpu-burn: Multi-GPU CUDA stress test

gpu-burnの準備と実行

$ git clone https://github.com/wilicc/gpu-burn
$ cd gpu-burn
$ make
$ ./gpu_burn 600

10分間負荷をかけますので少し待ちましょう。コンソールには負荷テスト中の進捗が表示されます。GPUの温度が刻々と上がっていきます。

GPU 0: Tesla T4 (UUID: GPU-626c6bcb-eb2a-c1f8-e763-d6bd62691e7b)
Initialized device 0 with 15109 MB of memory (14937 MB available, using 13443 MB of it), using FLOATS
10.7%  proc'd: 10894 (4033 Gflop/s)   errors: 0   temps: 54 C
	Summary at:   2020年  9月 16日 水曜日 08:25:12 UTC

20.8%  proc'd: 24302 (3893 Gflop/s)   errors: 0   temps: 65 C
	Summary at:   2020年  9月 16日 水曜日 08:26:13 UTC

30.8%  proc'd: 37710 (3712 Gflop/s)   errors: 0   temps: 71 C
	Summary at:   2020年  9月 16日 水曜日 08:27:13 UTC

41.5%  proc'd: 51956 (3608 Gflop/s)   errors: 0   temps: 75 C
	Summary at:   2020年  9月 16日 水曜日 08:28:17 UTC

51.7%  proc'd: 63688 (3531 Gflop/s)   errors: 0   temps: 77 C
	Summary at:   2020年  9月 16日 水曜日 08:29:18 UTC

53.3%  proc'd: 66202 (3523 Gflop/s)   errors: 0   temps: 77 C

グラフの確認

gpumon.pyはデフォルトの設定値では1分単位でメトリクスをPutしているため期間を変更すると細かく見えます。GPU使用率(オレンジのライン)は100%に達しています。

温度は80℃まで上がっていました。

おわりに

GPUインスタンスお使いの環境でしたらgpumon.pyを流用してメトリクスを取得するとなにかに役立つかもしれません。 当初は素のAmazon Linux2でGPUをモニタリングしようと試みたところ、GPUのドライバー周りのインストールに大変手間取り時間がかかり途中で打ち切りまして素直に設定済みのAMIを利用しました。なんでも一からセッティングを自分で頑張ればいいものではないなと思いました、時間は大切。

参考

Perform GPU, CPU, and I/O stress testing on Linux