Frameworks/PyTorch

[PyTorch] 데이터의 텐서 변환 시 as_tensor, from_array 속도 비교

Jonghyuk Baek 2022. 6. 7. 23:20

Introduction

파이토치 사용 중에 listnumpy array데이터 copy 없이 torch tensor로 view를 변환해야 하는 일이 굉장히 자주 있다. 파이토치에서는 이런 종류의 변환 작업을 위한 메소드를 이것저것 제공하고 있는데, 문득 어떤 데이터에 어떤 방식을 사용해야 가장 속도가 빠를지 궁금해졌다.

 

이번 글에서는 간단한 실험을 통해 어떤 상황에서 어떤 도구가 가장 효율적인지 알아보려고 한다.

 

결론만 말하자면,

 

  • 꽤 큰 사이즈 의 list라면 as_tensor를 쓰는 게 더 빠르고,
  • np.array에는 from_numpy를 사용하는 쪽이 더 좋다.

List to Tensor

먼저 리스트를 CPU 텐서로 변환하는 상황을 살펴보자. 직접 as_tensor 를 통해 리스트에서 텐서로 갈 수도 있고, numpy.array로 view 변환을 한번 거친 후 from_numpy 로 변환할 수도 있다. 파이토치 프로파일링을 통해 성능 비교를 해 보자.

import torch
import numpy as np
import torch.autograd.profiler as profiler

NUM_TRIAL = 10000

test_sample_list = [float(n) for n in range(2048)]

with profiler.profile(with_stack=True, profile_memory=True) as prof:
    for _ in range(NUM_TRIAL):
        with profiler.record_function("LIST_AS_TENSOR"):
            _ = torch.as_tensor(test_sample_list)

    for _ in range(NUM_TRIAL):
        with profiler.record_function("LIST_TO_NUMPY_TO_TENSOR"):
            test_sample_np = np.asarray(test_sample_list)
            _ = torch.from_numpy(test_sample_np)

print(prof.key_averages().table(sort_by="cpu_time_total", row_limit=10))

파이토치 프로파일링의 사용법에 대해서는 이전 글을 참고하자. 결과는 아래와 같다.

---------------------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  
                       Name    Self CPU %      Self CPU   CPU total %     CPU total  CPU time avg       CPU Mem  Self CPU Mem    # of Calls  
---------------------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  
    LIST_TO_NUMPY_TO_TENSOR        45.43%        1.403s        49.46%        1.528s     152.751us     -39.06 Kb    -195.31 Kb         10000  
             LIST_AS_TENSOR        27.51%     849.780ms        32.75%        1.012s     101.152us      78.09 Mb    -195.31 Kb         10000  
                aten::zeros        10.95%     338.204ms        17.79%     549.398ms      27.470us      78.12 Kb           0 b         20000  
                aten::empty        10.84%     334.634ms        10.84%     334.634ms       5.577us      78.51 Mb      78.51 Mb         60000  
                aten::zero_         3.15%      97.419ms         3.15%      97.419ms       4.871us           0 b           0 b         20000  
                 aten::set_         1.06%      32.836ms         1.06%      32.836ms       3.284us           0 b           0 b         10000  
                   aten::to         1.06%      32.625ms         1.06%      32.625ms       3.262us           0 b           0 b         10000  
                   [memory]         0.00%       0.000us         0.00%       0.000us       0.000us     -78.12 Mb     -78.12 Mb         10000  
---------------------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  
Self CPU time total: 3.088s

대상 데이터가 리스트인 경우에는 numpy로의 view 변환을 거치지 않고 바로 as_tensor 를 사용해 텐서로 변환하는 것이 1024 사이즈 float 텐서 기준으로 약 30% 가량 더 빨랐다 (1.528s -> 1.012s).

 

모든 텐서 사이즈 대역에서도 비슷한가 싶어서 텐서 사이즈를 바꾸어 가며 추가적인 테스트를 진행해 보았다.

특이한 결과가 나왔는데, 512 이하의 작은 사이즈의 벡터에 대해서는 np array로 뷰를 변환하고 from_array를 사용하는 게 더 빠르고 그 이상의 사이즈에서는 as_tensor를 이용하는 것이 더 빨랐다. 그리고 텐서의 사이즈가 커질수록 둘 사이의 시간 비율은 1:2의 비율에 점차 수렴하는 것을 확인할 수 있었다.

 

참고로 np.array를 바로 캐스팅하는 방식은 메모리 카피를 한번 거치기 때문에 복사를 하지 않는 두 방식을 공정하게 비교하기 위해 np.asarray를 대신 사용했다.

 

Numpy array to Tensor

그렇다면 이미 데이터가 numpy array 형태인 경우에는 어떤 방식이 더 좋을까? 동일한 방식으로 한번 테스트해 보자.

import torch
import numpy as np
import torch.autograd.profiler as profiler

NUM_TRIAL = 10000

test_sample_list = [float(n) for n in range(2048)]
test_sample_np = np.asarray(test_sample_list)

with profiler.profile(with_stack=True, profile_memory=True) as prof:
    with profiler.record_function("NUMPY_AS_TENSOR"):
        for _ in range(NUM_TRIAL):
            _ = torch.as_tensor(test_sample_np)

    with profiler.record_function("FROM_NUMPY"):
        for _ in range(NUM_TRIAL):
            _ = torch.from_numpy(test_sample_np)

print(prof.key_averages().table(sort_by="cpu_time_total", row_limit=10))
-------------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  
               Name    Self CPU %      Self CPU   CPU total %     CPU total  CPU time avg       CPU Mem  Self CPU Mem    # of Calls  
-------------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  
    NUMPY_AS_TENSOR        21.86%      76.860ms        61.83%     217.352ms     217.352ms          -4 b         -20 b             1  
         FROM_NUMPY        21.12%      74.251ms        38.15%     134.110ms     134.110ms          -4 b         -20 b             1  
           aten::to        22.82%      80.216ms        22.82%      80.216ms       8.022us           0 b           0 b         10000  
         aten::set_        17.12%      60.190ms        17.12%      60.190ms       3.009us           0 b           0 b         20000  
        aten::empty        17.06%      59.974ms        17.06%      59.974ms       2.998us          40 b          40 b         20004  
        aten::zeros         0.01%      51.000us         0.03%      96.400us      48.200us           8 b           0 b             2  
        aten::zero_         0.00%      17.200us         0.00%      17.200us       8.600us           0 b           0 b             2  
-------------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  
Self CPU time total: 351.558ms

대상 데이터가 np.array인 경우에는 from_array를 이용해 텐서로 변환하는 것이 1024 사이즈 float 텐서 기준으로 약 38% 가량 더 빨랐다 (217.352ms -> 134.110ms).

다양한 사이즈 세팅에서 실험했을 때에는 두 방법론 모두 대상 텐서의 사이즈에 대해 소요 시간이 크게 영향받지 않고 비슷한 수준을 유지하는 것을 확인했다. 두 방법의 시간 소요 비율도 1.6:1.0 정도로 비슷한 수준을 유지했다. 차트엔 없지만 8192 사이즈의 벡터를 사용해도 위와 유사한 결과가 나오는 것을 확인했다.

 

Notes

아래에서는 텐서 변환에 관련해 모아둔 잡지식들을 정리해 놓았다.

  • torch.Tensortorch.tensor로 캐스팅해 텐서를 만드는 방법도 있는데, 이들은 위에서 소개한 방법들과는 달리 데이터를 복사해 새로 텐서를 만드는 방식이다. 반면 위에서 소개한 방법들은 데이터 복사 없이 데이터의 view만을 바꾸어 주는 방법들이다.
  • 위처럼 메모리 복사를 통해 새 텐서를 생성하는 경우엔 리스트에서는 소개한 방법들에 비해 40%가량 더 빨랐지만 반대로 np.array 상에서는 더 느렸던 방법과 비교해 2배 이상 느린 현상을 확인했다.
  • 사실 as_tensor는 같은 디바이스 상의 numpy array가 들어오면 from_numpy를 내부적으로 호출한다고 하는데, 실제로는 두 방법 사이에 유의미한 소요 시간 차이가 발생한다. 만약 디바이스만 다른 텐서가 들어오면 to 메소드를 호출한다고 한다.
  • as_tensor를 사용하면 autograd 히스토리를 되도록 유지하면서 주어진 dtype, device의 텐서를 만들어 준다. 반면 torch.tensor는 history 유지 없이 복사된 leaf variable을 만들어 준다.

 


적고 보니 뭘 한 건지 모르겠는데, 아무튼 결론은 벡터 기준 512 이상의 큰 사이즈 리스트에는 as_tensor를 쓰고, np array에는 from_numpy 를 쓰는 쪽이 시간 측면에서 더 효율적인 것 같다. 그리고 이번 기회에 텐서 변환에 관련해 산발적으로 알고 있던 지식들을 한번 정리해 보았다.

 

아래는 참고할 만한 페이지들이다.

References

Best way to convert a list to a tensor?

https://discuss.pytorch.org/t/best-way-to-convert-a-list-to-a-tensor/59949/6

[PyTorch] 파이토치 프로파일링 (PyTorch profiler API)

https://jh-bk.tistory.com/20

 

반응형