지난 MLP와 메모리 장벽에서 우리는 메모리 대역폭이 시스템 성능을 제한하는 ‘Memory Wall’ 현상에 대해 다루었습니다. 그리고 CNN과 지역성에서는 CNN이 순차적인 데이터 처리를 통해 이 문제를 우아하게 해결했는지 살펴보았죠.
2015년 등장한 ResNet (Residual Network)은 딥러닝 역사상 가장 위대한 발명품 중 하나로 꼽힙니다. “층(Layer)이 깊어질수록 학습이 안 된다”는 난제를 Skip Connection Y = F(X) + X이라는 아주 간단한 아이디어로 해결했기 때문입니다. 하지만 소프트웨어 엔지니어들이 ResNet의 우아함에 환호할 때, 하드웨어 아키텍트들은 머리를 감싸 쥐어야 했습니다.
이 간단해 보이는 덧셈(+X) 하나가, 그동안 하드웨어가 지켜오던 순차적 메모리 관리 규칙을 완전히 무너뜨렸기 때문입니다. 이번 글에서는 ResNet이 칩 내부의 메모리 버퍼(Buffer)와 스케줄러(Scheduler)를 어떻게 괴롭히는지 분석해 보겠습니다.
1. 순차적 처리 (Sequentiality)
ResNet 이전의 모델들(AlexNet, VGG)은 아주 단순한 직렬 구조(Chain Structure)였습니다.
이는 하드웨어 입장에서 메모리 관리가 너무나 쉬운 구조입니다.
- Layer 1의 출력을 메모리에 씁니다.
- 그걸 읽어서 Layer 2를 계산합니다.
- Layer 2의 출력이 나오는 순간, Layer 1의 데이터는 덮어써도 됩니다 (Overwrite).
이런 구조는 데이터의 생명주기(Lifetime)가 매우 짧습니다. 즉, 작은 온칩 버퍼(SRAM) 두 개만 가지고 핑퐁(Ping-Pong) 치듯이 데이터를 주고받으면 거대한 모델도 문제없이 돌릴 수 있었습니다.
2. Skip Connection
하지만 ResNet의 Skip Connection (Shortcut)은 이 규칙을 깹니다.
입력 데이터 X는 Conv 연산(복잡한 3 * 3 합성곱 등)을 하기 위해 사용됩니다. 동시에 X는 나중에 결과값과 더해지기 위해 그대로 남아 있어야 합니다. 문제는 Conv(X) 연산이 수행되는 동안(Latency), X를 어디엔가 보관해야 한다는 점입니다.
- Conv(X)가 끝날 때까지 X의 메모리 공간을 해제할 수 없습니다.
- 데이터 X의 Lifetime이 강제로 연장됩니다.
이는 한정된 온칩 메모리(SRAM) 자원을 장시간 점유하게 만들어, 다른 연산들이 사용할 버퍼 공간을 부족하게 만듭니다.
3. 메모리 계층의 딜레마: SRAM vs DRAM
만약 Residual Block 내부의 연산량이 많아서 X를 오랫동안 들고 있어야 하는데, 칩 내부의 SRAM 용량이 부족하다면 어떻게 될까요? Architecture는 어쩔 수 없이 X를 칩 밖의 DRAM으로 쫓아냈다가(Spill), 나중에 다시 가져와야(Fill) 합니다.
- Read X: Conv 연산을 위해 읽음.
- Spill X: X를 나중에 더하기 위해 DRAM에 저장 (SRAM 부족 시).
- Compute F(X): 열심히 합성곱 연산 수행.
- Fill X: 덧셈(F(X)+X)을 위해 DRAM에서 다시 읽어옴.
이 과정에서 불필요한 DRAM 트래픽(대역폭 소모)이 발생하는 것입니다.
4. Streaming Architecture: 동기화(Synchronization)
데이터를 한 번에 처리하지 않고 파이프라인으로 흘려보내는 Streaming Architecture나 FPGA 설계에서는 더 큰 문제가 발생합니다.
- Main Path: Conv -> ReLU -> Conv (연산이 많아서 느림)
- Skip Path: 그냥 wire로 연결
두 데이터가 마지막 덧셈기(Adder)에서 만나야 하는데, 도착 시간이 다릅니다. Skip Path로 온 데이터 X는 Main Path의 연산이 끝날 때까지 기다려야 합니다.
이를 위해 하드웨어에는 데이터를 잠시 가둬두는 FIFO (First-In-First-Out) 버퍼가 추가로 필요합니다. 모델이 깊을수록, 이미지 해상도가 클수록 이 FIFO의 크기는 수 킬로바이트(KB)에서 메가바이트(MB) 단위로 커지며 칩의 면적을 갉아먹습니다.
5. Element-wise Addition
마지막으로, F(X) + X라는 요소별 덧셈(Element-wise Addition) 자체도 문제입니다. 우리는 보통 곱셈(MAC) 비용만 따지지만, 덧셈은 전형적인 Memory-Bound 연산입니다.
- 연산: 덧셈 1회
- 메모리 접근: 읽기 2회(F(X), X), 쓰기 1회(Y)
보시다시피 연산 강도(Arithmetic Intensity)가 매우 낮습니다. ResNet 구조는 주기적으로 이 Memory-Bound 연산을 수행해야 하므로, NPU의 연산 유닛들이 덧셈 데이터가 로딩되기를 기다리며 멈추는(Stall) 현상을 유발합니다.
6. 결론: 유연성(Flexibility)을 위한 비용, 그리고 다음 단계
ResNet의 Skip Connection은 딥러닝의 정확도를 혁명적으로 높여주었지만, 하드웨어 엔지니어에게는 “비순차적 데이터 관리”라는 까다로운 숙제를 안겨주었습니다. 이 문제를 해결하기 위해 현대의 NPU 컴파일러들은 고도의 메모리 할당(Memory Allocation) 알고리즘을 사용하거나, 하드웨어적으로 Skip Connection 전용 압축기를 탑재하기도 합니다.
이것으로 [Category 1. AI & HW Fundamentals] 시리즈를 마칩니다. 우리는 지금까지 12편의 글을 통해 다양한 병목 현상을 마주했습니다.
- MLP: 메모리 대역폭이 부족해서 느렸고 (Memory-Bound),
- MobileNet: 연산기는 놀고 있는데 구조가 복잡해서 느렸으며 (Utilization Issue),
- ResNet: 메모리 관리와 버퍼링 때문에 시스템이 멈칫거렸습니다 (Buffer Management).
그렇다면, 내가 설계한(혹은 분석 중인) NPU가 느리다면 도대체 누구 탓일까요? 연산기일까요, 메모리일까요?
다음 글부터 시작되는 [Category 2. NPU Design & Optimization]에서는 이 복잡한 병목 현상들을 단 한 장의 그래프로 명쾌하게 진단하는 시스템 Architect 최고의 분석 도구, Roofline Model에 대해 알아보겠습니다.