---
title: "im2col에서 반복된 입력은 어디에 저장될까"
author: "브레이브브라우저쓰자"
published_at: "2026-11-01T01:58:04.946Z"
updated_at: "2026-10-06T03:35:25.661Z"
tags: ["im2col"]
canonical_url: "https://ariser.io/@writer_a9817d5a8de0/im2col%EC%97%90%EC%84%9C-%EB%B0%98%EB%B3%B5%EB%90%9C-%EC%9E%85%EB%A0%A5%EC%9D%80-%EC%96%B4%EB%94%94%EC%97%90-%EC%A0%80%EC%9E%A5%EB%90%A0%EA%B9%8C"
ai_assisted: true
---

# im2col에서 반복된 입력은 어디에 저장될까

이 글은 AI가 활용됐습니다.

[원문](https://ariser.io/@writer_a9817d5a8de0/im2col%EC%97%90%EC%84%9C-%EB%B0%98%EB%B3%B5%EB%90%9C-%EC%9E%85%EB%A0%A5%EC%9D%80-%EC%96%B4%EB%94%94%EC%97%90-%EC%A0%80%EC%9E%A5%EB%90%A0%EA%B9%8C)

convolution의 작은 window를 한 row로 펴면 행렬 곱으로 계산할 수 있다. 하지만 식이 같은 답을 준다고 해서 입력을 읽는 모양까지 같아지는 것은 아니다. im2col은 convolution을 GEMM에 맞게 표현하는 변환이면서, 겹치는 window의 데이터를 반복해 나타내는 선택이다. 수학적 변환과 실제 buffer를 만드는 일을 나누어 보면 비용의 출처가 드러난다.

## 출력 위치마다 필요한 window를 한 row로 만든다

여기서는 deep learning에서 흔히 convolution이라고 부르는, kernel을 뒤집지 않는 cross-correlation을 사용한다. 입력을 4×4, channel을 하나, kernel을 2×2, stride를 1, padding을 0으로 정하자. 출력은 3×3이다. row와 column을 모두 0부터 세고 출력 좌표를 p,q로 적으면 대응 입력은 `X[p+r,q+s]`다. r,s는 kernel 안의 좌표다.

im2col 행렬 P의 row는 출력 위치 `3p+q`, column은 kernel 위치 `2r+s`로 정한다. 각 값은 `P[3p+q,2r+s]=X[p+r,q+s]`다. 이름은 im2col이지만 이런 row 배치는 한 가지 표기 관례다. row·column을 뒤집어 적어도 kernel 벡터와 곱하는 방향을 함께 맞추면 같은 결과다.

입력에 1부터 16까지 row 순서로 값을 넣으면 앞의 두 window는 다음과 같다.

```text
X = [[ 1,  2,  3,  4],
     [ 5,  6,  7,  8],
     [ 9, 10, 11, 12],
     [13, 14, 15, 16]]

P의 첫 row   = [1, 2, 5, 6]
P의 두 번째 row = [2, 3, 6, 7]
```

kernel을 같은 r,s 순서로 편 [1,2,3,4]와 곱하면 첫 출력은 1+4+15+24=44, 두 번째는 2+6+18+28=54다. 전체 P는 9×4이고 결과는 9×1이다. 이를 3×3으로 되돌리면 출력은 `[[44,54,64],[84,94,104],[124,134,144]]`다. 직접 window를 따라 계산한 결과와 대응 원소가 모두 같다.

## 원래 16개였던 값이 36개 자리에 등장한다

P에는 9개의 window마다 네 원소가 있어 논리적 원소가 36개다. 원래 입력의 16개에 비하면 2.25배다. 입력의 값 6은 서로 겹치는 네 window에 등장한다. 각각 다른 출력 원소의 계산에 필요하지만 같은 input에서 온 값이다.

이를 DRAM에 새 buffer로 전부 쓰는 구현이라면 그 36개 원소를 저장하는 공간과 write가 추가된다. 그러나 2.25배라는 수를 외부 memory traffic의 정확한 증가율로 부를 수는 없다. cache와 local 재사용, buffer가 놓이는 위치, 생성과 소비가 겹치는 방식이 traffic을 달리한다. buffer를 쓴 횟수와 원본을 읽은 횟수도 같은 숫자가 아니다.

필터가 여러 개이면 P를 여러 출력 channel에 재사용할 수 있다. 그러면 변환 비용을 더 많은 GEMM 작업이 나누어 부담한다. 반대로 출력 channel이 적거나 작은 작업을 자주 실행하면 변환과 호출 비용이 계산 시간에서 큰 몫을 차지할 수 있다. 동일한 im2col이라는 이름 아래에서 채산성이 달라지는 이유다.

## stride와 padding은 주소 계약을 바꾼다

가로세로 stride가 t, padding이 h인 경우에는 입력 주소가 `X[p×t+r−h,q×t+s−h]`다. 범위 밖은 이번 zero-padding 규칙에서 0으로 만든다. channel이 여러 개라면 kernel의 r,s뿐 아니라 channel c도 P의 column index에 포함하고, weight도 같은 순서로 펼쳐야 한다.

앞의 입력에서 stride만 2로 바꾸면 겹치지 않는 네 window가 생긴다. P는 4×4가 되고 출력은 `[[44,64],[124,144]]`다. logical 원소 수가 이번에는 16이라 입력 원소 수와 같다. 그렇다고 모든 stride와 kernel 조합에서 중복이 사라진다는 일반 규칙은 아니다.

stride=1로 돌아가 padding을 1로 정하면 출력은 5×5다. 맨 왼쪽 위 window는 [0,0,0,1]이고 첫 결과는 4다. 범위 밖 주소를 실제로 load한 뒤 값을 0으로 바꾸는 동작과, 범위를 먼저 검사해 load 자체를 하지 않는 동작은 다르다. zero-padding의 수학적 정의는 유효하지 않은 memory 접근을 허용하지 않는다.

## 큰 행렬을 실제로 저장하지 않아도 된다

한 번에 출력 세 위치의 window만 생성하면 이 예에서 12개 원소의 임시 묶음으로 순차 처리할 수 있다. P 전체를 외부 memory에 materialize할 필요는 없다. GEMM에 들어가는 순서를 유지하며 작은 tile을 생성하거나, hardware가 소비할 때 필요한 원소를 바로 공급하는 방식도 가능하다. 이 경우에도 logical 중복은 있지만 모든 복사본이 동시에 저장되는 것은 아니다.

Gemmini 논문은 host CPU가 im2col을 수행하는 구성과 accelerator의 optional block이 on-the-fly로 수행하는 구성을 구분한다. 변환의 수학은 같아도 어디서 실행하는지가 host의 부담과 전체 실행 경로를 바꾼다. 그 논문의 특정 성능 수치를 이번 작은 입력의 개선율로 옮기지 않는다. [Gemmini 논문, 4쪽 IV.B절](https://arxiv.org/pdf/1911.09925)

im2col을 쓴다고 하면 먼저 index와 kernel 순서가 같은 계산을 만드는지 확인한다. 그다음 어느 크기의 window 묶음을 어디서 생성하고 어디에 보관하는지 묻는다. 이 두 질문을 분리하면 행렬 곱의 편리함을 얻으면서, 겹치는 입력 때문에 생긴 비용이 실제 어느 memory와 처리기에 남는지도 설명할 수 있다.

