[PyTorch] 데이터의 텐서 변환 시 as_tensor, from_array 속도 비교
Introduction
파이토치 사용 중에 list나 numpy 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.Tensor나 torch.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)