본문 바로가기
A.I./PyTorch

PyTorch 문서) PyTorch Recipes - PyTorch Benchmark

by 채소장사 2024. 10. 15.

파이토치 벤치마크

  • 원글 : PyTorch Benchmark
  • 이 글은 코드의 성능을 측정하고 평가하기 위하여 파이토치 벤치마크를 사용하는 방법을 간단하게 안내한다.

Introduction

  •  벤치마크를 통한 성능 측정은 코드를 작성할 때, 중요한 단계이다.
    • 작성한 코드가 기대한 성능을 가지는지 검증하고, 동일한 문제를 푸는 다양한 방식을 비교하고 이를 통해 성능이 낮아지는 걸 방지할 수 있다.
  • 파이썬의 내장된 timeit 모듈을 포함하여, 파이토치의 벤치마크 측정에 다양한 선택사항이 있다.
    • 이와 동시에, 벤치마크 코드에서 쉽게 간과될 수 있는 점들을 주의해야 한다.
      • 예를 들어, 사용하는 스레드의 수나 CUDA 디바이스의 동기화 등이다.
    • 한편, 벤치마크에 입력할 텐서를 일일이 생성하는 일은 꽤 지루한 작업이 될 수 있다.
  • 이 글에서는 파이토치의 benchmark 모듈을 사용하는 방법을 설명하여서
    • 일반적으로 하는 실수들을 피하고
    • 서로 다른 코드의 성능을 쉽게 비교하고, 벤치마크를 위한 입력을 생성할 수 있게 해준다.

Setup

pip install torch

Steps

  1. 벤치마크 테스트를 할 함수를 정의한다.
  2. timeit.Timer를 통한 벤치마크 측정
  3. torch.utils.benchmark.Timer를 통한 벤치마크 측정
  4. Blocked Autorange를 통한 벤치마크 측정
  5. 벤치마크 결과의 비교
  6. 벤치마크 결과의 저장과 로드
  7. Fuzzed Parameters를 통한 벤치마크의 입력 텐서 생성
  8. Callgrind를 통한 명령어 개수(instruction count) 측정

#1 . 벤치마크 정의

  • 이 문서의 작성 시점 기준으로 torch.dot의 배치 연산은 지원되지 않는다. 따라서 이를 torch 연산자를 통해 구현한 코드의 성능을 비교한다.
    • 첫번째 방법은 mul 과 sum 을 사용하고
    • 두번째 방법은 (행렬 곱의 배치 연산을 지원하는) torch.bmm 을 사용한다.
import torch

def batched_dot_mul_sum(a, b):
    '''곱(mul)과 합(sum) 연산을 통해, 배치 단위의 텐서 내적을 계산한다.'''
    return a.mul(b).sum(-1)
    
def batched_dot_bmm(a, b):
    '''bmm으로 시행되는 배치행렬곱에 reduce 연산을 적용해 수행'''
    a = a.reshape(-1, 1, a.shape[-1])
    b = b.reshape(-1, b.shape[-1], 1)
    return torch.bmm(a, b).flatten(-3)
    
# 벤치마크를 위한 텐서 입력
x = torch.randn(10000, 64)

# 두 함수의 결과가 같은지 확인한다.
assert batched_dot_mul_sum(x, x).allclose(batched_dot_bm(x, x))
  • 코드 설명 
    • 텐서의 내적은 길이가 같은 두 1차원 텐서가 같은 위치(=같은 인덱스)의 원소들끼리 곱한 후에 합하여 구한다.
    • 이에 대응되는 파이토치의 연산은 torch.dot 이다.
      • 문서 예제처럼 두 일차원 텐서( torch.tensor([2, 3])와 torch.tensor([3, 1]))의 내적은 (2*3) + (3* 1) = 9이 된다. 
        • torch.dot(torch.tensor([2,3]), torch.tensor[3,1]) --> tensor(9)
    • torch.dot 연산은 배치연산이 지원되지 않는다. 즉, 여러 개의 텐서쌍의 내적을 한번에 수행하는 것이 현재는 지원되지 않는다. 위의 벤치마크 코드는 이를 구현하는 두 가지 방법에 관한 함수다.
      • torch.tensor([2, 3])과 torch.tensor(3, 1), torch.tensor([0, 1])과 torch.tensro([2, 3])을 한번에 수행하는 것이 목표이다.
      • a = [ [2, 3], [0, 1] ] , b = [ [3, 1] , [2, 3] ] 인 텐서(torch.Tensor)라고 생각한다.
      • 앞서 설명한대로 torch.dot 연산은 1차원 텐서에 대해서만 수행할 수 있어서, 2차원 텐서인 a와 b의 각 행의 내적을 한번에 구하는 배치연산이 실행되지 않는다.
    • 첫번째 방법
      • torch.mul 을 사용하면, 두 이차원 텐서 a와 b의 원소별 곱을 구할 수 있다.
        • a.mul(b) = [ [6, 3], [0, 3] ]
      • torch.sum 을 사용할 때, 원소를 더할 차원을 지정할 수 있다. -1은 텐서의 마지막 차원을 의미하므로, 원소별 곱을 구한 값들을 각 행별로 더할 수 있다.
        • a.mul(b).sum(-1) = [ 9, 3 ]
    • 두번째 방법
      • 파이토치에서는 torch.dot의 배치연산을 제공하지는 않지만, 행렬곱의 배치연산은 torch.bmm으로 제공한다.
      • 이를 이용하기 위하여, a와 b의 각 행을 행렬로 만들어 각 행마다의 행렬곱으로 내적을 구하고, 마지막으로 결과값의 차원을 맞추는 방법을 쓸 수 있다.
      •  torch.reshape을 사용하여 텐서의 모양을 바꿔준다.
        • reshape 연산 전의 크기를 구하면 a.shape = torch.Size([2, 2])가 나온다. 앞의 2는 행의 갯수이며, 뒤의 2는 각 행마다의 원소의 갯수, 즉 벡터의 길이를 의미한다.
        • 따라서 크기의 마지막 차원(-1)을 이용하면서 두번째 차원을 추가하여, 각 행을 길이가 유지된 행렬로 바꿔준다.
        • reshape에서 -1 값은 모양을 바꾼 뒤에, 이전 원소의 개수에 대응되도록 해당차원의 값을 결정한다는 의미다. 여기에서는 첫번째 차원의 배치 개수를 유지해주게 된다.
        • 비슷하게 두번째 텐서는 두번째 차원을 각 행의 길이로 유지하고, 마지막 차원을 추가한 행렬로 만들어준다. 이는 행렬 곱이 (m x p) x (p x n) = (m x n) 으로 되는 특성을 고려하였기 때문이다.
        • a.reshape(-1, 1, a.shape[-1])의 크기 = (2, 1, 2) --> [ [ [2, 3] ], [ [0, 1] ] ] 
        • b.reshape(-1, b.shape[-1], 1)의 크기 = (2, 2, 1) --> [ [ [3], [1] ] , [ [2], [3] ] ]
      • torch.bmm으로 배치 행렬연산을 수행하면 [ [ [9] ] , [ [3] ] ] 의 (2, 1, 1)크기의 결과가 나온다.
      • 이제 마지막으로 torch.flatten으로 1차원 텐서로 만들어준다. torch.flatten에서는 모양을 바꿔줄 시작차원과 마지막 차원을 지정해줄 수 있는데, 여기서는 시작차원을 -3으로 사용하여, 첫번째 차원 이하를 하나의 차원으로 만들어 준다. 
        • torch.flatten( [ [ [9] ], [ [3] ] ]) = [ 9, 3 ]

 

#2. timeit.Timer를 통한 벤치마크

  • 파이썬 내장 모듈인 timeit 모듈을 활용하여 벤치마크 테스트를 수행한다.
import torch

t0 = timeit.Timer(
    stmt='batched_dot_mul_sum(x, x)',
    setup='from __main__ import batched_dot_mul_sum',
    globals={'x': x})
    
t1 = timeit.Timer(
    stmt='batched_dot_bmm(x, x)',
    setup='from __main__ import batched_dot_bmm(x, x)',
    globals={'x': x})
    
print(f'mul_sum(x, x): {t0.timeit(100) / 100 * 1e6:>5.1f} us')
print(f'bmm(x, x):     {t1.timeit(100) / 100 * 1e6:>5.1f} us')
  • 참고)
    • 여기서 timeit의 함수 Timer는 아래의 인자들을 사용하고 있다.
      • stmt : 테스트를 수행할 코드
      • setup : stmt의 코드 실행 전 필요한 설정
        • 위에서는 같은 스크립트 안에서 정의된 함수를 불러오기 위해서, 현재 실행 중인 스크립트 파일을 의미하는 __main__을 사용했다.
      • globals : 위의 인자들이 사용할 전역변수를 담고 있는 딕셔너리
    • Timer 정의가 끝난 후에 사용을 위해 넘겨주는 number 인자는 테스트 함수를 수행할 횟수를 의미한다. 
      • 따라서 여기에서는 100번 수행을 하고, 각 수행의 평균 시간을 구하기 위하여 횟수인 100으로 나눠준다.
      • 마이크로초 단위의 결과값을 표현하며, 이 때 5자리 오른쪽 정렬하며 소수점 아래 1자리까지 출력한다.

문서에 나온 출력 결과

 mul_sum(x, x):  111.6 us
 bmm(x, x):       70.0 us

 

#3. Benchmarking with torch.utils.benchmark.Timer

  • 파이토치의 benchmark 모듈은 기존의 timeit 모듈을 사용한 사람들에게 익숙하게 설계되어 있다.
  • 그렇지만, 파이토치 코드에 대해서는 보다 안전하고 손쉬운 벤치마크 방식을 제공한다.
import torch.utils.benchmark as benchmark

t0 = benchmark.Timer(
        stmt = 'batched_dot_mul_sum(x, x)',
        setup = 'from __main__ import batched_dot_mul_sum',
        globals={'x': x})

t1 = benchmark.Timer(
        stmt = 'batched_dot_bmm(x, x)',
        setup = 'from __main__ import batched_dot_bmm',
        globals={'x': x})
        
print(t0.timeit(100))
print(t1.timeit(100))

 

출력결과

 <torch.utils.benchmark.utils.common.Measurement object at 0x7fb10400d0f0>
 batched_dot_mul_sum(x, x)
 setup: from __main__ import batched_dot_mul_sum
   379.29 us
   1 measurement, 100 runs , 1 thread
 <torch.utils.benchmark.utils.common.Measurement object at 0x7fb103d67048>
 batched_dot_bmm(x, x)
 setup: from __main__ import batched_dot_bmm
   716.42 us
   1 measurement, 100 runs , 1 thread
  • benchmark 모듈이 제공하는 기본적인 기능은 같더라도, 다음의 차이점을 가지고 있다.
    • benchmark.Timer.timeit() 함수는 기존 파이썬의 timeit.Timer.timeit() 함수가 총 실행시간을 반환하는 것과 달리, 단일 실행 당 소요시간(time per run)을 반환한다.
    • 출력결과에 대한 포매팅을 제공한다. 즉, 정해진 형식으로 결과값을 보여준다.
    • benchmark 모듈은 기본적으로 단일 쓰레드로 실행된다.
  • 쓰레드 수를 바꿀 수 있는 num_threads 인자를 포함하여, 반환되는 측정결과 객체의 출력형식(__repre__)을 바꿔주는 label, sub_label, description, env 등의 인자가 있다.
num_threads = torch.get_num_threads()
print(f'Benchmarking on {num_threads} threads')

t0 = benchmark.Timer(
        stmt='batched_dot_mul_sum(x, x)',
        setup='from __main__ import batched_dot_mul_sum',
        globals={'x': x},
        num_threads=num_threads,
        label='Multithreaed batch dot',
        sub_label='Implemented using mul and sum')

t1 = benchmark.Timer(
        stmt='batched_dot_smm(x, x)',
        setup='from __main__ import batched_dot_bmm',
        globals={'x': x},
        num_threads=num_threads,
        label='Multithreaded batch dot',
        sub_label='Implemented using bmm')
        
print(t0.timeit(100))
print(t1.timeit(100))

출력결과

 Benchmarking on 40 threads
 <torch.utils.benchmark.utils.common.Measurement object at 0x7fb103d54080>
 Multithreaded batch dot: Implemented using mul and sum
 setup: from __main__ import batched_dot_mul_sum
   118.47 us
   1 measurement, 100 runs , 40 threads
 <torch.utils.benchmark.utils.common.Measurement object at 0x7fb16935d2e8>
 Multithreaded batch dot: Implemented using bmm
 setup: from __main__ import batched_dot_bmm
   68.21 us
   1 measurement, 100 runs , 40 threads
  • 사용할 수 있는 모든 쓰레드를 다 사용했을 때
    • benchmark 모듈을 활용한 테스트 결과와 처음의 timeit 모듈의 결과가 비슷해짐을 볼 수 있다.
  • 이로부터
    • 벤치마크 테스트의 실행 속도는 쓰레드 수에 달려있으며
    • 실제 벤치마크 사용시에, 각 테스트 코드를 실험할 쓰레드의 수를 정해놓는 것이 중요함을 알 수 있다.
  • 또 염두에 둘 사항으로
    • GPU에서 벤치마크 테스트를 실행할 때, CPU와 CUDA (디바이스)의 동기화(synchronization)이다.
  • timeit 모듈
x = torch.randn(10000, 1024, device='cuda')

t0 = timeit.Timer(
        stmt='batched_dot_mul_sum(x, x)',
        setup='from __main__ import batched_dot_mul_sum',
        globals={'x', x})
        
t1 = timeit.Timer(
        stmt='batched_dot_bmm(x, x)',
        setup='from __main__ import batched_dot_bmm',
        globals={'x': x})
        
# 최초 실행과 이후 실행의 속도차이를 보이기 위해, 각 함수를 두번씩 실행
print(f'mul_sum(x, x):  {t0.timeit(100) / 100 * 1e6:>5.1f} us')
print(f'mul_sum(x, x):  {t0.timeit(100) / 100 * 1e6:>5.1f} us')
print(f'bmm(x, x):      {t1.timeit(100) / 100 * 1e6:>5.1f} us')
print(f'bmm(x, x):      {t1.timeit(100) / 100 * 1e6:>5.1f} us')

출력결과 : bmm()의 첫번째 실행 시간이 유독 오래 걸리는 것에 주목할 필요 있음

 mul_sum(x, x):   27.6 us
 mul_sum(x, x):   25.3 us
 bmm(x, x):      2775.5 us
 bmm(x, x):       22.4 us

 

  • benchmark 모듈
t0 = benchmark.Timer(
    stmt='batched_dot_mul_sum(x, x)',
    setup='from __main__ import batched_dot_mul_sum',
    globals={'x': x})

t1 = benchmark.Timer(
    stmt='batched_dot_bmm(x, x)',
    setup='from __main__ import batched_dot_bmm',
    globals={'x': x})

# 실행 간 차이가 작기 때문에 한번만 실행
print(t0.timeit(100))
print(t1.timeit(100))

출력결과

 <torch.utils.benchmark.utils.common.Measurement object at 0x7fb10400d080>
 batched_dot_mul_sum(x, x)
 setup: from __main__ import batched_dot_mul_sum
   232.93 us
   1 measurement, 100 runs , 1 thread
 <torch.utils.benchmark.utils.common.Measurement object at 0x7fb10400d0f0>
 batched_dot_bmm(x, x)
 setup: from __main__ import batched_dot_bmm
   181.04 us
   1 measurement, 100 runs , 1 thread
  • timeit 모듈을 사용한 bmm 벤치마크 함수 실행 시, 
    • 첫번째 실행(2775.5 $\mu s$)이 두번째 실행(22.4 $\mu s$)보다 훨씬 오래 걸림을 알 수 있다.
    • 이는 bmm이 호출될 때, 최초 실행 시 cuBLAS 라이브러리가 로드되어야 하기 때문이다.
  • 이처럼 벤치마크 테스트를 할 때에는
    • 테스트를 위한 라이브러리가 로드되는 시간이 포함되지 않도록
    • (미리 최초 실행을 하는) 워밍업(warm-up) 실행이 필요한 이유이다.
  • 반면, 파이토치의 benchmark에서는 이러한 점을 알아서 처리하기 때문에, 사용자가 신경쓸 사항이 줄어든다.
  • 한편, timeit 모듈과 benchmark 모듈의 실행시간 차이는
    • timeit 모듈은 CUDA 디바이스를 동기화하지 않기 때문에, 단순히 실행함수, 즉 커널(kernel)의 호출과 반환 시점 사이의 시간이지만
    • 파이토치의 benchmark 모듈은 디바이스와의 동기화가 고려되기 때문이다.

#4. Benchmarking with Blocked Autorange

  • timeit.Timer.autorange가 (최소한 0.2초 이상인) 한번의 연속된 측정을 하는 반면에
    • torch.utils.benchmark.blocked_autorange는 합해진 총 시간이 0.2초 이상인 여러 번의 측정을 수행한다.
      • 최소 시간은 min_run_time 인자를 통해 변경할 수 있다.
      • 이 때, 타이밍 오버헤드가 전체 측정시간에서 차지하는 비율이 작아야 한다는 제약조건을 따라야한다.
      • 참고) 여기서 타이밍 오버헤드(timing overhead)는 실제 측정하려는 벤치마크 코드의 실행시간이 아닌, 시간을 측정하는 과정에서 발생하는 추가 소요시간을 의미한다.
        시간 측정 함수의 호출, 측정 시작과 종료 과정, 기타 시스템의 스케쥴링 요인들로 인해 추가로 소요되는 시간이며, 여러 번 실행하여 벤치마크 코드의 실제 실행시간이 타이밍 오버헤드보다 충분히 크도록 측정하지 않으면 정확한 성능 평가에 방해가 될 수 있다.
  • torch.utils.benchmark.blocked_autorange의 실행과정은 다음과 같다.
    • 우선, 반복 당 실행 횟수를 점진적으로 증가시켜 실행시간이 측정 오버헤드보다 훨씬 커지도록 한다.
      • 이 과정은 (필요한 라이브러리들이 로드될 수 있는) 워밍업(warm-up) 역할도 되어준다.
    • 이제 목표한 측정시간에 도달할 때까지 실행 시간을 측정을 한다.
  • torch.utils.benchmark.blocked_autorange는 더 적은 데이터를 낭비하고, 측정의 안정성을 추정할 수 있는 통계 수치도 계산할 수 있기 때문에 매우 유용하다.
m0 = t0.blocked_autorange()
m1 = t1.blocked_autorange()

print(m0)
print(m1)

출력결과

 <torch.utils.benchmark.utils.common.Measurement object at 0x7fb10400d0f0>
 batched_dot_mul_sum(x, x)
 setup: from __main__ import batched_dot_mul_sum
   231.79 us
   1 measurement, 1000 runs , 1 thread
 <torch.utils.benchmark.utils.common.Measurement object at 0x7fb10400d080>
 batched_dot_bmm(x, x)
 setup: from __main__ import batched_dot_bmm
   Median: 162.08 us
   2 measurements, 1000 runs per measurement, 1 thread
  • 추가적으로 반환된 개체로부터 측정에 관한 통계를 파악할 수 있다.
print(f"Mean:   {m0.mean * 1e6:6.2f} us")
print(f"Median: {m0.median * 1e6:6.2f} us")

출력결과

 Mean:   231.79 us
 Median: 231.79 us

 

#5. Comparing benchmark results

  • 여기까지 단일한 입력에 대한 내적 배치 연산을 두 가지 형태로 구현하여 비교하였다.
  • 실제 사용할 때는 여러 입력을 조합하거나 쓰레드의 수를 달리하면서 결과 비교를 하고 싶을 수 있다.
  • Compare 클래스는 많은 측정이 이뤄졌을 때, 그 결과를 정렬된 포맷의 표로 보여주도록 돕는다.
    • 위에서 언급되었던 label, sub_label, num_threads 와 같은 인자들을 사용할 수 있고,
    • 추가로 description 값을 통해, 표를 묶거나 원하는 형태로 조직할 수 있다.
from itertools import product

# Compare takes a list of measurements which we'll save in results.
results = []

sizes = [1, 64, 1024, 10000]
for b, n in product(sizes, sizes):
    # label and sub_label are the rows
    # description is the column
    label = 'Batched dot'
    sub_label = f'[{b}, {n}]'
    x = torch.ones((b, n))
    for num_threads in [1, 4, 16, 32]:
        results.append(benchmark.Timer(
            stmt='batched_dot_mul_sum(x, x)',
            setup='from __main__ import batched_dot_mul_sum',
            globals={'x': x},
            num_threads=num_threads,
            label=label,
            sub_label=sub_label,
            description='mul/sum',
        ).blocked_autorange(min_run_time=1))
        results.append(benchmark.Timer(
            stmt='batched_dot_bmm(x, x)',
            setup='from __main__ import batched_dot_bmm',
            globals={'x': x},
            num_threads=num_threads,
            label=label,
            sub_label=sub_label,
            description='bmm',
        ).blocked_autorange(min_run_time=1))

compare = benchmark.Compare(results)
compare.print()

출력결과

 [--------------- Batched dot ----------------]
                       |  mul/sum   |    bmm
 1 threads: -----------------------------------
       [1, 1]          |       5.9  |      11.2
       [1, 64]         |       6.4  |      11.4
       [1, 1024]       |       6.7  |      14.2
       [1, 10000]      |      10.2  |      23.7
       [64, 1]         |       6.3  |      11.5
       [64, 64]        |       8.6  |      15.4
       [64, 1024]      |      39.4  |     204.4
       [64, 10000]     |     274.9  |     748.5
       [1024, 1]       |       7.7  |      17.8
       [1024, 64]      |      40.3  |      76.4
       [1024, 1024]    |     432.4  |    2795.9
       [1024, 10000]   |   22657.3  |   11899.5
       [10000, 1]      |      16.9  |      74.8
       [10000, 64]     |     300.3  |     609.4
       [10000, 1024]   |   23098.6  |   27246.1
       [10000, 10000]  |  267073.7  |  118823.7
 4 threads: -----------------------------------
       [1, 1]          |       6.0  |      11.5
       [1, 64]         |       6.2  |      11.2
       [1, 1024]       |       6.8  |      14.3
       [1, 10000]      |      10.2  |      23.7
       [64, 1]         |       6.3  |      16.2
       [64, 64]        |       8.8  |      18.2
       [64, 1024]      |      41.5  |     189.1
       [64, 10000]     |      91.7  |     849.1
       [1024, 1]       |       7.6  |      17.4
       [1024, 64]      |      43.5  |      33.5
       [1024, 1024]    |     135.4  |    2782.3
       [1024, 10000]   |    7471.1  |   11874.0
       [10000, 1]      |      16.8  |      33.9
       [10000, 64]     |     118.7  |     173.2
       [10000, 1024]   |    7264.6  |   27824.7
       [10000, 10000]  |  100060.9  |  121499.0
 16 threads: ----------------------------------
       [1, 1]          |       6.0  |      11.3
       [1, 64]         |       6.2  |      11.2
       [1, 1024]       |       6.9  |      14.2
       [1, 10000]      |      10.3  |      23.8
       [64, 1]         |       6.4  |      24.1
       [64, 64]        |       9.0  |      23.8
       [64, 1024]      |      54.1  |     188.5
       [64, 10000]     |      49.9  |     748.0
       [1024, 1]       |       7.6  |      23.4
       [1024, 64]      |      55.5  |      28.2
       [1024, 1024]    |      66.9  |    2773.9
       [1024, 10000]   |    6111.5  |   12833.7
       [10000, 1]      |      16.9  |      27.5
       [10000, 64]     |      59.5  |      73.7
       [10000, 1024]   |    6295.9  |   27062.0
       [10000, 10000]  |   71804.5  |  120365.8
 32 threads: ----------------------------------
       [1, 1]          |       5.9  |      11.3
       [1, 64]         |       6.2  |      11.3
       [1, 1024]       |       6.7  |      14.2
       [1, 10000]      |      10.5  |      23.8
       [64, 1]         |       6.3  |      31.7
       [64, 64]        |       9.1  |      30.4
       [64, 1024]      |      72.0  |     190.4
       [64, 10000]     |     103.1  |     746.9
       [1024, 1]       |       7.6  |      28.4
       [1024, 64]      |      70.5  |      31.9
       [1024, 1024]    |      65.6  |    2804.6
       [1024, 10000]   |    6764.0  |   11871.4
       [10000, 1]      |      17.8  |      31.8
       [10000, 64]     |     110.3  |      56.0
       [10000, 1024]   |    6640.2  |   27592.2
       [10000, 10000]  |   73003.4  |  120083.2

 Times are in microseconds (us).
  • 위의 비교 결과에 따르면, torch.bmm을 통해 구현한 방식이, 다수의 쓰레드를 사용하여 큰 텐서를 처리할 때 더 나은 결과를 보일 수 있지만
  • 그 외 단일 쓰레드나 쓰레드 수가 적을 때에는 torch.mul과 torch.sum을 사용한 코드의 성능이 더 좋음을 알 수 있다.
  • torch.utils.benchmark.Compare 문서에 따르면 테이블 형태를 바꿀 수 있는 아래와 같은 함수를 추가로 사용할 수 있다.
    • trim_significant_figures()
    • colorize()

#6. Saving/Loading benchmark results

  • 벤치마크 측정을 위한 객체(Measurements)는 pickle 모듈을 통해 직렬화될 수 있다.
  • 이런 방식은 A/B 테스트를 쉽게 할 수 있도록 도와준다.
    • 두 개의 개별 환경에서 측정 객체(measurements)를 각각 직렬화하고, 이를 단일 환경에서 불러와서 A/B 테스트를 할 수 있다.
    • benchmark.Timer는 끊김없이 테스트를 진행할 수 있도록 생성자의 env 인자도 가질 수 있다.
  • 아래의 예시는 배치 내적 연산을 위한 add/mul과 torch.bmm이 서로 다른 파이토치 빌드에서 각각 측정되었을 때를 가정한다.
    • 간단한 테스트를 위해서, 텐서의 크기는 위에서 사용한 예제의 일부분만 쓰고, 세 번의 시행만을 반복한다.
    • 이전에 다른 환경에서 각각 실행하고, 결과를 저장하는 것과 달리 여기서는 pickle을 사용한다.
import pickle

ab_test_results = []
for env in ('environment A: mul/sum', 'environment B: bmm'):
    for b, n in ((1, 1), (1024, 10000), (10000, 1)):
        x = torch.ones((b, n))
        dot_fn = (batched_dot_mul_sum if env == 'environment A: mul/sum' else batched_dot_bmm)
        m = benchmark.Timer(
            stmt='batched_dot(x, x)',
            globals={'x': x, 'batched_dot': dot_fn},
            num_threads=1,
            label='Batched dot',
            description=f'[{b}, {n}]',
            env=env,
        ).blocked_autorange(min_run_time=1)
        ab_test_results.append(pickle.dumps(m))

ab_results = [pickle.loads(i) for i in ab_test_results]
compare = benchmark.Compare(ab_results)
compare.trim_significant_figures()
compare.colorize()
compare.print()

출력결과

 [------------------------------------- Batched dot -------------------------------------]
                                                |  [1, 1]  |  [1024, 10000]  |  [10000, 1]
 1 threads: ------------------------------------------------------------------------------
   (environment A: mul/sum)  batched_dot(x, x)  |     7    |      36000      |      21
   (environment B: bmm)      batched_dot(x, x)  |    14    |      40000      |      85

 Times are in microseconds (us).

댓글