Knowledge/Vision

contiguous의 의미 (view, reshape, transpose, permute 이해하기)

침닦는수건 2023. 2. 21. 17:41
반응형

numpy 나 torch를 이용해서 코딩을 해본 적이 있다면 가끔 array나 tensor가 contiguous하지 않다는 오류를 본 경험이 있을 것이다. "인접한, 근접한" 이라는 뜻을 가진 contiguous 단어 자체의 뜻만 보고는 직관적으로 이 오류가 무엇을 의미하는지 이해하기가 어렵다. 나또한 그랬고 오류를 고치기 위해 한 동안 고생했던 기억이 있다. 

 

일단 글을 시작하기 전에 먼저 말하면, 규모가 큰 코드, 특히나 연산이 얽혀있는 코드, 를 작성할 때 contiguous 문제가 생기면 풀기가 쉽진 않다. 코드 레벨이 아닌 안 보이는 레벨에서 발생하는 오류이기 때문에 꽤 까다로운 문제 중 하나라고 할 수 있다. 

 

그래서 이번 글에서는 애초에 문제를 예방했으면 좋겠다는 의미에서 이 contiguous가 무엇인지 기록하고자 한다.

 

contiguous 의미

python으로 코딩에 입문했다면 처음보는 내용일 수 있겠지만 만약 C나 C++로 코딩에 입문했다면 실제 우리가 다루는 array는 메모리에 저장될 때 주소를 기반으로 저장된다는 것을 알 것이다. 우리가 코드 레벨에서 쓰는 array index는 우리가 볼 때는 단순히 0, 1, 2, 3과 같지만 메모리의 특정 주소(특정 위치)와 연결되어 있다. 단순히 그림으로 그리면 다음과 같다.

 

 

지금 32bit element를 담고 있는 2 x 3 array를 다룬다고 했을 때, 우리는 row index(파란색), column index(빨간색)만 보고 사용하지만 실제로는 메모리의 특정 주소를 다루는 것이다. 값은 하나지만 칸이 4개인 이유는 32bit (4byte)이기 때문에 8bit(1byte)씩 나뉘어 저장되는 모습을 그린 것인다. (실제로는 어떻게 쪼개냐에 따라 다르지만 나는 그냥 byte 단위로 잘랐다.)

 

이 때 만약 내가 이 array를 transpose하고 싶다고 한다면, 두 가지 방법이 있을 것이다. 첫 번째는 array index - memory address 연결은 그대로 둔 채로 저장된 값을 바꾸는 것이고, 두 번째는 저장된 값은 그대로 두고 array index - memory를 변경하는 것이다. 위 그림만 보아도 단순하게 두번째 방법에 따라 index를 바꾸는 것이 훨씬 쉽다는 것을 알 수 있다. 두번째 방법을 그림으로 그리면 다음과 같다.

 

효율성 때문에 이렇게 index-address 연결을 수정하는 식으로 데이터를 관리하는 것이 꽤 일반적인 방식이다.

 

contiguous 개념은 여기서 발생한다. 첫번째 방법이든 두번째 방법이든 우리가 코드 레벨에서 다루는 array는 전혀 변하지 않았다. 똑같은 index를 사용하면 똑같은 값을 얻을 수 있으니 말이다. 하지만 분명 memory level에서는 저장된 순서의 방향이 바뀌었다는 것을 알 수 있다. 다른 말로 memory의 row 부터 차곡차곡 채워나가는 모양에서 memory의 column부터 채워나가는 모양으로 바뀌었다. 

 

이 뒤집힌 모양은 추후 복잡한 연산을 할 때 (구체적으로 index가 아닌 memory 주소를 직접 다룰 때, c++에서 pointer를 써봤다면 무슨 이야기인지 알 것 같다.) 큰 문제를 야기할 수 있다. array level에서 0번 element 다음은 1번 element이지만 memory level에서 0번 element 다음은 3번 element이기 때문에 코드가 꼬일 수 있다. 

 

개발자가 엄청 똑똑하다면 모든 경우를 다 따져서 잘 짤 수 있겠지만 이것은 말이 안된다. 

 

따라서 array level의 순서와 memory level의 순서가 나란히 정렬되어 있을 때는 contiguous, 그렇지 않을 때를 not contiguous라고 정의하고 이를 구분하기 시작한 것이다. 

 

contiguous 문제 상황 

contiguous array는 문제가 없고 not contiguous array를 다룰 때 문제가 많이 생긴다.

 

첫번째는 앞서 잠시 설명한 것처럼 memory address를 직접 사용하는 경우다. python이면 이것이 흔치 않으므로 문제가 안되는데 c++ 사용 시 이는 자주 문제가 된다. 따라서 cython을 쓰거나 python + prebuilt c++ 를 섞어서 쓸 경우, array가 contiguous한지 확인해야 한다.

 

두번째는 pytorch를 쓸 때다. 딥러닝 네트워크를 구현하는 과정에서 논문에 제시된 복잡한 loss function을 구현해야 하는데 이 때 수많은 tensor들을 view, reshape, transpose, permute하는 연산이 낀다. 이 연산들은 contiguous 상태를 개발자가 트래킹하기 어렵게 만드는 연산들인데 어떤 tensor가 contiguous한지 안한지 놓친 상황에서 구현을 계속하다보면 오류를 보게 된다. 오류가 안나더라도 학습이 잘 안되는 모습을 자주 본다. 

 

세번째는 cuda 코드를 같이 사용할 경우다. 이는 c++ 코드와 혼용할 때와 비슷한데 cuda 코드는 특히나 더 메모리 주소 관리에 민감하기 때문에 무조건적으로 contiguous tensor 상태를 유지하는 것이 좋다. 이를 놓칠 경우, 오류 폭탄을 만나게 된다.

 

contiguous 다루기 

간단하다. numpy를 쓸 경우,

YOUR_ARRAY = np.ascontiguousarray(YOUR_ARRAY)

torch를 쓸 경우,

YOUR_TENSOR = YOUR_TENSOR.contiguous()

contiguous 상태를 확인하는 함수들도 있는데, 일단 상태를 확인해야 한다는 것 자체가 위험한 상태이니 꼭 array 상태를 알고 있길 추천한다.

 

contiguous 관점에서 보는 view vs reshape / transpose vs permute

contiguous를 알아야 되는 대부분의 이유는 torch 사용 때문에 torch 사용 시 알고 있어야 하는 4가지 함수를 정리하고자 한다.

view vs reshape (안전함)

view와 reshape 모두 torch tensor의 모양을 재배열하는 함수다. 둘의 기능은 같지만 단 하나의 차이가 있다.

 

먼저 view는 contiguous array를 재배열된 contiguous array로 만들어 준다. 즉, 입력도 contiguous, 출력도 contiguous이다. 따라서 입출력 모두 contiguous가 명확한 상태일 때 사용한다. 아니라면 애초에 오류가 나기 때문에 view == contiguous 보장이라고 생각하면 된다.

 

reshape는 입력은 아무거나, 출력은 contiguous이다. 여기서 중요한 것은 출력은 어쨌거나 contiguous라는 것이다. 따라서 reshape도 contiguous를 보장받을 수 있다. 앞서 말한 단 하나의 차이는 입력이 not contiguous일 때도 동작한다는 것인데, 입력이 contiguous 상태라면 입력 원본 그대로를 재배열해서 return하고, 입력이 not contiguous라면 contiguous copy를 만든 뒤, 복사본을 재배열해서 return한다. 즉, reshape() == contiguous().view()와 같다고 볼 수 있다.

 

torch gradient 측면에서는 이러나 저러나 graph가 연결되어 있으니 걱정 안해도 된다.

 

transpose vs permute (위험함)

transpose와 permute 모두 torch tensor의 dimension을 뒤바꾸는 함수다. 둘의 기능은 같고 transpose는 2개의 dimension만, permute는 여러개의 dimension을 동시에 다룰 수 있다는 차이가 있다. 

 

기본적으로 transpose와 permute는 둘 다 not contiguous array를 return한다. 즉, array index - memory address 연결을 뒤 바꾸어 dimension을 재정렬하는 함수이므로 사용에 꼭 유의해야 하는 함수다. 

 

그러므로 입출력 tensor의 contiguous 여부를 잘 트래킹하면서 쓰거나, 모르겠으면 그냥 contiguous 상태로 강제 변환하고 사용하는 것을 추천한다. 무조건 contiguous로 바꾸게 되면 약간의 시간 증가나 메모리 추가 사용이 발생할 수는 있지만 애초에 메모리가 부족하면 GPU 더 쓰고 더 기다리겠단 마인드로 딥러닝을 하고 있다면 그냥 .contiguous() 쓰는 것을 추천한다.

반응형