개요

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. 올리는 게 왜 어렵나 — 프레임워크 종속과 최적화 병목

여기부터가 진짜 문제다.

학습은 보통 PyTorchTensorFlow로 한다. 그런데 기기에 올릴 때는 그 프레임워크를 그대로 못 쓴다. 가속기마다 자기 전용 실행기(런타임) 가 따로 있기 때문이다.

런타임 만든 곳 돌아가는 자리
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) 다. “이 형식으로 저장해두면 아무 데서나 읽을 수 있게 하자” 는 약속이다.

ONNX 허브 구조

가운데를 하나 두면 변환기가 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 연산자 규격 버전
)
  1. model.eval() — 학습 모드를 끈다. 이걸 빼먹으면 배치 정규화·드롭아웃이 학습 때 동작으로 저장된다
  2. dummy — ONNX는 모델을 한 번 실제로 돌려보면서 연산 순서를 기록한다. 그래서 가짜 입력이 필요하다
  3. 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도 결국 메모리와 연산기 사이를 덜 왕복하려는 방법들이었다. 목적지를 보고 나니까 앞의 세 편이 다시 읽혔다.


참고 자료

한줄 평

계산보다 옮기는 게 2만 배 비싸다는 한 줄에 앞의 세 편이 전부 설명돼버려서, 이번엔 좀 빠르게 이해할 수 있었다.