PyTorch 내부 구조 03 - Contiguous, Memory Format, Hidden Copy
shape가 같아도 memory layout이 다르면 operator 선택과 성능이 달라지고 때로는 보이지 않는 복사가 생긴다
contiguous는 왜 자꾸 등장할까
많은 PyTorch 연산은 non-contiguous tensor도 처리할 수 있다. 하지만 처리 방식은 연산마다 다르다.
- 어떤 연산은 stride를 따라 직접 접근한다
- 어떤 연산은 내부에서 contiguous copy를 만든다
- 어떤 custom kernel은 아예 contiguous만 가정한다
즉 contiguous 여부는 correctness뿐 아니라 성능과 메모리에도 영향을 준다.
memory format은 단순한 flag가 아니다
이미지 연산에서는 channels_last 같은 format이 있고, 일반 dense tensor에서는 전통적인 row-major contiguous layout이 흔하다. 중요한 것은 operator가 어떤 layout에서 더 잘 동작하는지, 그리고 layout 변환 비용이 연산 이득보다 작은지다.
hidden copy가 위험한 이유
숨은 복사는 다음 문제를 만든다.
- 예상보다 메모리를 더 많이 쓴다
- operator 자체보다 layout 변환이 더 비싸진다
- profiling에서 "왜 여기서 시간이 많이 들지?"라는 혼란을 준다
특히 custom op를 만들 때 입력을 무조건 contiguous로 바꾸면 구현은 쉬워도 상위 시스템에서는 불필요한 비용이 누적될 수 있다.
좋은 습관
- 입력 tensor의 stride와 is_contiguous를 먼저 본다
- kernel이 어떤 layout을 기대하는지 분명히 한다
- 변환이 필요하면 어디서 한 번만 할지 결정한다
다음 글에서는 dispatcher를 본다. PyTorch가 같은 연산 이름에 대해 CPU, CUDA, Autograd, Meta 같은 여러 구현을 어떻게 고르는지가 그 층에서 결정된다.