개요
35~37편은 계속 모델을 줄이는 이야기였다. 숫자를 몇 비트에 담을지, 가중치를 어떻게 깎을지, 어디만 학습시킬지.
그런데 줄이는 데는 목적이 있었다. 어딘가에 올리려고 줄인 거다. 그럼 그 어딘가가 어디인지, 거기서 뭐가 기다리고 있는지를 봐야 한다. 이번 편이 그 이야기다.
솔직히 이 파트는 처음부터 끝까지 새로 배우는 내용이었다. 나는 서버 개발을 하다가 왔고, 지금까지 내가 만든 건 전부 서버 어딘가에서 돌아가는 것이었다. 모델이 폰이나 로봇이나 공장 기계 안으로 들어간다는 상황 자체를 생각해본 적이 없었다..
이번 글이 답할 질문 왜 굳이 기기 안에서 모델을 돌리려고 하고, 왜 내가 만든 모델이 그 기기에 그대로 안 올라가는가.
교육 과정 자료는 대외비라 원본 자료는 싣지 않는다.
1. 왜 굳이 기기 안에서 — 클라우드가 멈추면 서비스도 멈춘다
온디바이스 AI(On-device AI) 라는 말부터 풀고 가자. 모델을 서버에 두고 인터넷으로 물어보는 게 아니라, 모델을 기기 안에 직접 넣어서 그 기기가 스스로 계산하게 하는 것이다.
클라우드 서비스가 몇 시간 멈춰서 전 세계 서비스가 같이 멈춘 사건들이 말해준다…
모든 지능이 클라우드에 얹혀 있으면 위험하다.
어찌보면 당연한 말 이지만, 서버 개발자로서 이건 좀 뜨끔한 문장이었다. 나는 늘 “서버가 살아있다”는 가정 위에서 코드를 짰다. Ducktopia 때 Gateway가 죽으면 게임 전체가 멈추는 걸 보면서도, 그게 구조의 문제라고만 생각했지 계산하는 위치의 문제라고는 생각하지 않았다.
기기 안에서 돌리면 뭐가 달라지나
| 기준 | 클라우드에서 계산 | 기기 안에서 계산 |
|---|---|---|
| 응답 속도 | 네트워크 왕복 시간이 항상 붙는다 | 왕복이 없다. 바로 답이 나온다 |
| 네트워크 | 끊기면 서비스도 끊긴다 | 끊겨도 돈다 |
| 비용 | 요청 수만큼 서버·통신 비용 | 기기 값만 내면 끝 |
| 개인정보 | 데이터가 밖으로 나간다 | 기기 밖으로 안 나간다 |
| 성능 한계 | 서버를 늘리면 된다 | 기기 성능이 곧 천장이다 |
자료에 나온 산업 현장 사례가 이걸 잘 보여줬다. 공장에서 불량품을 잡아내는 카메라 시스템인데, 요구사항이 이랬다.
- 밀가루·먼지가 날리는 환경이라 팬이 없는(fanless) 하드웨어여야 한다
- 낮은 성능의 하드웨어에서 돌아야 한다
- 생산 속도에 따라갈 수 있는 실시간 추론이어야 한다
여기서 “클라우드로 사진 보내고 답 받아오면 되잖아” 는 성립하지 않는다. 컨베이어 벨트는 네트워크 응답을 기다려주지 않는다.
온디바이스는 성능이 좋아서 고르는 게 아니다. 거기서밖에 못 돌리니까 고르는 것이다.
2. 메모리 벽 — 계산보다 옮기는 게 비싸다
이번 편에서 제일 인상 깊었던 게 이거다.
나는 계산이 느려서 컴퓨터가 느린 거라고 막연히 생각했다. 그런데 실제로 발목을 잡는 건 계산이 아니라 숫자를 가져오는 일이었다.
칩 안에서 덧셈 한 번 하는 데 드는 전기와, 칩 밖 메모리에서 숫자 하나 읽어오는 데 드는 전기를 나란히 놓으면 이렇게 된다.
로그 눈금이라 막대 길이만 보면 감이 안 오는데, 숫자를 그대로 읽으면 이렇다. 8비트 정수 덧셈 한 번이 0.03, 칩 밖 메모리 읽기 한 번이 640. 대략 2만 배다.
즉 연산기를 아무리 빠르게 만들어도, 그 연산기에 먹일 숫자를 밖에서 계속 실어 나르는 한 전체 속도는 거기서 막힌다. 이걸 메모리 벽(memory wall), 전력 관점에서는 파워 월(power wall) 이라고 부른다.
더 쉬운 비유로
요리를 못 해서 늦는 게 아니다. 재료가 매번 창고에 있어서 늦는 것이다.
칼질은 1초면 끝난다. 그런데 양파 하나 꺼내러 창고까지 다녀오는 데 5분이 걸린다면, 칼을 아무리 좋은 걸로 바꿔도 요리 시간은 안 줄어든다. 줄이려면 재료를 도마 옆에 미리 갖다 놔야 한다.
칩 설계도 똑같은 결론으로 간다.
- 자주 쓰는 숫자는 칩 안 메모리에 붙잡아 둔다 (도마 옆에 두기)
- 한 번 가져온 숫자로 최대한 많은 계산을 한 뒤에 버린다 (한 번 갈 때 왕창 들고 오기)
- 메모리를 아예 연산기 근처로 끌어다 붙인다 (창고를 부엌 옆으로 옮기기)
그리고 여기서 35~36편이 다시 등장한다. 양자화가 왜 그렇게 중요했는지가 여기서 설명된다. 32비트를 8비트로 줄이면 계산이 4배 빨라져서 좋은 게 아니라, 실어 나를 짐이 4분의 1이 돼서 좋은 거였다. 메모리 벽에 부딪히는 횟수 자체가 줄어든다.
경량화는 모델 크기를 줄이는 작업이 아니라, 메모리와 연산기 사이 왕복을 줄이는 작업이었다!
3. 다섯 글자 — CPU, GPU, NPU, TPU, MCU
메모리 벽 때문에 “범용 칩으로 어떻게든 해보자” 가 안 통하게 됐다. 그래서 한 가지 계산만 죽어라 잘하는 칩을 따로 만드는 쪽으로 흐름이 넘어갔다. 이걸 도메인 특화 가속기라고 부른다.
이름이 비슷해서 헷갈렸는데, 축 하나로 세워놓으니 정리가 됐다.
| 이름 | 풀어쓰면 | 잘하는 일 | 주로 있는 곳 |
|---|---|---|---|
| CPU | 중앙처리장치 | 복잡한 순서·분기 처리 | 안 들어간 데가 없다 |
| GPU | 그래픽처리장치 | 같은 계산을 수천 개씩 동시에 | PC, 서버, 게임기 |
| NPU | 신경망처리장치 | 신경망 연산 전용 회로 | 폰, 차, 가전 |
| TPU | 텐서처리장치 | 행렬 곱 전용 | 구글 서버, 엣지 보드 |
| MCU | 마이크로컨트롤러 | 아주 작은 일을 아주 적은 전기로 | 센서, 가전 부품 |
더 쉬운 비유로
CPU는 뭐든 다 만드는 요리사고, GPU는 같은 반찬을 수천 접시 찍어내는 조리 라인이고, NPU와 TPU는 계란말이 하나만 만드는 기계다.
계란말이 기계는 다른 걸 하나도 못 한다. 대신 계란말이는 요리사보다 빠르고 전기도 훨씬 덜 먹는다. 신경망은 결국 행렬 곱을 미친 듯이 반복하는 일이라, 그 반복만 전담하는 회로를 박아두면 이득이 크다.
MCU만 이 줄 위에 놓기가 애매했다. 얘는 성능 축이 아니라 전력과 크기 축에 있는 친구다. 밀리와트급으로 돌아가는 손톱만 한 컴퓨터라, 센서 바로 옆에 붙여놓고 아주 작은 모델을 돌린다.
실제로 파는 물건들
개념만 보면 붕 뜨는데, 자료가 실물 보드를 같이 보여줘서 감이 잡혔다.
| 분류 | 예시 | 특징 |
|---|---|---|
| USB 스틱형 | Google Coral, Intel NCS2 | 기존 PC에 꽂아서 가속만 얹는다 |
| 소형 컴퓨터형 | NVIDIA Jetson 계열 | 리눅스가 도는 작은 컴퓨터 + GPU |
| 모바일 SoC | 폰에 들어가는 통합 칩 | CPU·GPU·NPU가 한 칩 안에 다 있다 |
SoC(System on Chip) 는 말 그대로 칩 하나 안에 시스템을 다 넣은 것이다. 예전엔 CPU 따로, 그래픽 따로, 통신 따로 기판에 꽂았는데 그러면 크고 전기를 많이 먹는다. 그래서 하나로 합쳤다. 요즘 폰 칩은 거의 다 이 구조고, 그 안에 NPU가 같이 들어있다.
우리가 매일 들고 다니는 폰 안에 이미 신경망 전용 회로가 박혀 있었다는 게 제일 신기했다!
4. 올리는 게 왜 어렵나 — 프레임워크 종속과 최적화 병목
여기부터가 진짜 문제다.
학습은 보통 PyTorch나 TensorFlow로 한다. 그런데 기기에 올릴 때는 그 프레임워크를 그대로 못 쓴다. 가속기마다 자기 전용 실행기(런타임) 가 따로 있기 때문이다.
| 런타임 | 만든 곳 | 돌아가는 자리 |
|---|---|---|
| LiteRT (옛 TensorFlow Lite) | 구글 | 안드로이드, 엣지 TPU |
| Core ML | 애플 | iPhone, Mac |
| TensorRT | NVIDIA | NVIDIA GPU, Jetson |
| OpenVINO | 인텔 | 인텔 CPU·GPU·VPU |
| SNPE | 퀄컴 | 스냅드래곤 DSP·NPU |
| NNAPI | 구글 | 안드로이드 (지금은 중단) |
여기서 두 가지 문제가 생긴다.
문제 1 — 프레임워크 종속성
TensorFlow로 짠 모델은 LiteRT로 나가기 편하고, PyTorch로 짠 모델은 TensorRT 쪽으로 나가기 편하다. 모델을 어떤 도구로 만들었느냐가 나중에 어디에 올릴 수 있느냐를 결정해버린다.
문제 2 — 최적화 병목
이게 더 고약하다. 변환은 성공했는데 안 빨라지는 경우다.
가속기는 자기가 아는 연산만 전담 회로로 처리한다. 모델 안에 그 회로가 모르는 연산이 하나 끼어 있으면, 그 부분만 CPU로 되돌려서 계산한다. 그런데 아까 봤듯이 왔다 갔다 하는 게 제일 비싸다. 결과적으로 이렇게 된다.
- 변환은 됐다
- 실행도 된다
- 그런데 기대한 만큼 안 빨라진다
- 어디가 발목인지 로그만 봐서는 안 보인다
자료에서는 이걸 두고 개발 시간이 비용을 초과한다고 표현했다. 모델을 만드는 시간보다 올리고 튜닝하는 시간이 더 든다는 뜻이다.
모델이 좋다고 서비스가 되는 게 아니라, 그 기기 위에서 좋아야 서비스가 된다.
5. ONNX — 다들 이 파일로 말하자
프레임워크가 N개, 런타임이 M개면 변환기는 N×M개가 필요하다. 3개와 4개만 잡아도 벌써 12개다. 하나 늘어날 때마다 곱하기로 늘어난다.
그래서 가운데에 공용 규격을 하나 세웠다. 그게 ONNX(Open Neural Network Exchange) 다. “이 형식으로 저장해두면 아무 데서나 읽을 수 있게 하자” 는 약속이다.
가운데를 하나 두면 변환기가 N+M개로 줄어든다. 3+4=7개. 런타임이 하나 늘어도 ONNX에서 그쪽으로 나가는 변환기 하나만 만들면 된다.
더 쉬운 비유로
나라마다 통역사를 다 붙일 것인가, 다 같이 영어를 쓸 것인가.
10개 나라 사람이 모이면 통역사는 45쌍이 필요하다. 근데 다들 영어를 한 다리 거쳐서 말하기로 하면 통역사는 10명이면 된다. ONNX가 그 영어 자리다.
실제로 쓰는 흐름은 이렇다.
import torch
model.eval()
dummy = torch.randn(1, 3, 224, 224) # 입력 모양만 알려주는 가짜 입력
torch.onnx.export(
model, dummy, "model.onnx",
input_names=["input"], output_names=["output"],
opset_version=17, # ONNX 연산자 규격 버전
)
-
model.eval()— 학습 모드를 끈다. 이걸 빼먹으면 배치 정규화·드롭아웃이 학습 때 동작으로 저장된다 -
dummy— ONNX는 모델을 한 번 실제로 돌려보면서 연산 순서를 기록한다. 그래서 가짜 입력이 필요하다 -
opset_version— 연산자 규격의 버전. 여기가 자주 사고 나는 자리다
만능은 아니다
당연히 단점도 있다.
- 모르는 연산자가 있으면 거기서 멈춘다. 직접 짠 커스텀 연산이 있으면 그대로는 안 나간다
-
opset버전이 안 맞으면 못 읽는다. 내보낸 쪽은 최신인데 읽는 쪽 런타임이 옛날이면 실패한다 - 변환됐다고 가속되는 건 아니다. 4번에서 본 병목이 그대로 남는다
- 한 번 실제로 돌려보며 기록하는 방식이라, 입력 값에 따라 경로가 갈리는 모델은 그중 한 갈래만 저장된다
ONNX는 말이 통하게 해주는 규격이지, 빨라지게 해주는 규격이 아니다.
정리
- 온디바이스 AI는 모델을 서버가 아니라 기기 안에 넣고 그 기기가 직접 계산하게 하는 것이다. 속도·네트워크·비용·개인정보 때문에 고르지만, 실은 거기서밖에 못 돌려서 고르는 경우가 많다
- 발목을 잡는 건 계산이 아니라 데이터를 옮기는 일이다. 칩 밖 메모리 읽기 한 번이 8비트 덧셈 2만 번어치 전기를 쓴다. 이게 메모리 벽이다
- 그래서 양자화가 중요했다. 비트를 줄이면 실어 나를 짐이 줄어든다
- 범용 칩으로 안 되니 전용 칩으로 갔다. CPU → GPU → NPU·TPU 순으로 할 수 있는 일은 줄고 전기당 성능은 오른다. MCU는 성능이 아니라 전력과 크기 축에 있다
- 가속기마다 전용 런타임이 따로 있다. LiteRT, Core ML, TensorRT, OpenVINO, SNPE… 그래서 프레임워크 종속성과 변환은 됐는데 안 빨라지는 병목이 생긴다
- ONNX는 그 사이에 낀 공용 규격이다. 변환기를 N×M에서 N+M으로 줄여준다. 다만 모르는 연산자·버전 불일치·가속 실패는 그대로 남는다
네 편을 한 줄로
35편에서 숫자를 담는 법을, 36편에서 모델을 깎는 법을, 37편에서 줄인 채로 튜닝하는 법을 봤다. 그리고 이번 38편에서, 그렇게 만든 모델이 결국 어디로 가는지를 봤다.
세 편 내내 “왜 이렇게까지 줄여야 하지” 싶었는데, 답이 여기 있었다.
줄이는 이유는 작아서 좋아서가 아니라, 옮기는 게 비싸서였다.
양자화도, 프루닝도, LoRA도 결국 메모리와 연산기 사이를 덜 왕복하려는 방법들이었다. 목적지를 보고 나니까 앞의 세 편이 다시 읽혔다.
참고 자료
- Computing’s Energy Problem (and what we can do about it) — M. Horowitz, ISSCC 2014
- ONNX 공식 문서
- torch.onnx — PyTorch 공식 문서
- LiteRT 공식 문서 (구 TensorFlow Lite)
- NNAPI Migration Guide — Android NDK
한줄 평
계산보다 옮기는 게 2만 배 비싸다는 한 줄에 앞의 세 편이 전부 설명돼버려서, 이번엔 좀 빠르게 이해할 수 있었다.