
PyTorch를 활용해 복잡한 딥러닝 아키텍처를 설계하다 보면 RuntimeError: input is not contiguous라는 메시지를 마주하게 됩니다. 이는 텐서의 형태 변환이나 차원 교환(Transpose, Permute) 직후에 주로 발생하는데, 초보 개발자들에게는 가장 이해하기 어려운 하드웨어 수준의 제약 사항 중 하나입니다. 본 포스팅에서는 텐서의 물리적 메모리 구조를 심층 분석하고, 왜 특정 시점에 반드시 contiguous()를 호출해야 하는지 그 결정적인 차이점을 실무 관점에서 다룹니다.
1. 메모리 연속성(Contiguity)의 개념과 발생 원인
PyTorch 텐서는 데이터를 물리적 메모리(RAM 또는 VRAM) 상의 1차원 배열로 저장합니다. 우리가 보는 다차원 텐서는 이 1차원 메모리 블록 위에서 Stride(보폭)라는 메타데이터를 사용하여 논리적으로 재구성된 결과물입니다.
예를 들어 $3 \times 3$ 행렬이 있을 때, 가로 방향으로 데이터가 인접해 있다면 이는 Contiguous 상태입니다. 하지만 transpose() 연산을 수행하면 PyTorch는 실제 데이터를 물리적으로 재배치하지 않고 Stride 값만 바꿉니다. 결과적으로 논리적으로는 행렬이 뒤집힌 것처럼 보이지만, 물리적 메모리 주소 상으로는 데이터가 띄엄띄엄 떨어져 있는 Non-contiguous 상태가 됩니다.
2. 비교 분석: Contiguous vs Non-contiguous 텐서의 특성
두 상태의 텐서가 시스템 내부적으로 어떻게 다르게 취급되는지 요약한 비교표입니다.
| 비교 항목 | Contiguous (연속적) | Non-contiguous (불연속적) |
|---|---|---|
| 물리적 저장 방식 | 인접한 메모리 주소에 순차적 저장 | 데이터 간 메모리 주소 간격이 불규칙함 |
| view() 연산 가능 여부 | 항상 가능 | 불가능 (RuntimeError 발생) |
| 연산 최적화 | SIMD 및 하드웨어 가속기 최적화 용이 | 메모리 접근 병목 현상 발생 가능 |
| 주요 발생 함수 | torch.randn(), narrow(), expand() | transpose(), permute(), select() |
3. 실무 해결을 위한 7가지 Sample Example (Best Practices)
실제 딥러닝 프로젝트 파이프라인에서 contiguous()를 적재적소에 배치하여 에러를 해결하는 예제입니다.
Example 1: Transpose 후 view()를 사용해야 할 때의 에러 해결
가장 고전적인 에러 상황으로, 텐서의 축을 바꾼 뒤 모양을 변경할 때 필수입니다.
import torch
x = torch.randn(10, 20)
y = x.transpose(0, 1) # Non-contiguous 상태
# 1. 에러 발생 코드: y.view(-1) -> RuntimeError
# 2. 해결 방법:
y_fixed = y.contiguous().view(-1)
print(f"Is contiguous: {y_fixed.is_contiguous()}")
Example 2: Permute를 활용한 멀티헤드 어텐션(Attention) 가공
Transformer 구조에서 헤드 차원을 병합할 때 자주 사용됩니다.
# (Batch, Heads, Seq, Dim) -> (Batch, Seq, Heads * Dim)
attention_out = torch.randn(16, 8, 50, 64)
# permute 후에는 반드시 contiguous()를 불러줘야 안전하게 view()가 작동함
merged = attention_out.permute(0, 2, 1, 3).contiguous().view(16, 50, -1)
Example 3: 텐서가 이미 연속적인지 확인하고 조건부 호출하기
불필요한 메모리 복사를 방지하기 위해 텐서 상태를 체크하는 효율적인 방법입니다.
def ensure_contiguous(t):
if not t.is_contiguous():
return t.contiguous()
return t
Example 4: 특정 차원을 선택(Select)한 후의 슬라이싱 최적화
large_tensor = torch.randn(100, 100, 100)
# 차원 선택 후에도 메모리 레이아웃이 깨질 수 있음
sub_tensor = large_tensor[50].contiguous()
Example 5: GPU 메모리 상의 연산 성능 병목 해결 방법
CUDA 커널은 연속적인 메모리 접근(Coalesced Access) 시 성능이 극대화됩니다.
# 매우 큰 텐서의 축을 바꾼 후 연산을 반복해야 한다면
# 매번 stride를 계산하는 것보다 한 번 contiguous()를 해주는 것이 속도 면에서 유리함
big_data = torch.randn(10000, 10000).cuda().t().contiguous()
Example 6: Custom C++ Extension 연동 시 안정성 확보
직접 작성한 C++ 또는 CUDA 커널은 텐서가 연속적이라고 가정하고 포인터 연산을 수행하는 경우가 많습니다.
# C++ 확장 모듈에 전달하기 전 데이터 강제 정렬
input_for_cpp = user_tensor.contiguous()
Example 7: narrow() 연산과 함께 사용하는 메모리 해제 팁
# 텐서의 일부분만 사용하고 원본의 큰 메모리를 해제하고 싶을 때
# .clone() 혹은 .contiguous()를 하면 원본과 링크가 끊긴 새 메모리 블록이 생성됨
small_chunk = large_tensor.narrow(0, 0, 1).contiguous()
del large_tensor # 이제 큰 메모리는 GC 대상이 됨
4. 결론 및 요약: 언제 contiguous()를 써야 하는가?
결론적으로 contiguous()는 "논리적 데이터 순서와 물리적 저장 순서를 일치시키는 작업"입니다. 2026년 현재 PyTorch의 내부 최적화가 많이 진행되어 자동으로 처리해주는 경우(예: reshape())도 있지만, 메모리 효율성과 하드웨어 가속을 100% 활용해야 하는 실무 환경에서는 개발자가 이를 명시적으로 제어하는 것이 성능 최적화의 핵심입니다.
다음 세 가지 상황을 기억하십시오:
transpose(),permute()후에view()를 호출해야 할 때- 직접 작성한 외부 라이브러리(C++/CUDA)로 텐서를 넘길 때
- 대규모 반복 연산 전, 메모리 접근 효율(Locality)을 높이고 싶을 때
5. 참고 문헌 및 출처
- PyTorch Internals: The Layout of Tensors (https://pytorch.org/docs/stable/tensor_attributes.html)
- NVIDIA Parallel Forall Blog: "Everything you need to know about Strides and Contiguity"
- "Deep Learning Programming with PyTorch" by Soumith Chintala
- PyTorch GitHub Discussion: Why is contiguous() necessary for view?