모델 운영

MLOPS / 50번째 글

휴대폰에 모델을 올릴 때 실제로 부딪히는 것들

계산상으로는 초당 30토큰인데 1분 뒤에는 14토큰이 됩니다. 발열과 배터리, 메모리 상한, 첫 토큰 지연, 기기 파편화까지 모바일에서만 나타나는 제약을 정리합니다.

PALDYN Team12 MIN READ

지난 글에서 기기 추론의 속도와 메모리를 손으로 계산하는 법을 봤다. 그 계산은 맞다. 다만 휴대폰에서는 계산대로 안 간다.

이유는 휴대폰이 컴퓨터라기보다 열을 못 버리는 얇은 판이기 때문이다. 서버에는 팬이 있고 전원이 무한하지만 휴대폰에는 둘 다 없다. 여기서 나오는 제약들은 코드로 우회할 수 있는 종류가 아니라 설계에 처음부터 반영해야 하는 종류다.

지속 처리량은 첫 30초의 절반이다

벤치마크에서 초당 32토큰이 나왔다고 하자. 그 숫자는 대개 처음 몇 십 초를 잰 값이다. 같은 부하를 계속 주면 칩 온도가 올라가고, 일정 지점에서 시스템이 클럭을 낮춘다. 앱이 할 수 있는 것은 없다 — OS가 기기를 지키기 위해 내리는 결정이다.

휴대폰의 속도는 1분쯤 뒤에 절반이 된다

그래서 온디바이스 기능을 설계할 때 봐야 할 숫자는 최고 속도가 아니라 평평해진 뒤의 속도다. 초당 14토큰에서도 쓸 만한 기능이면 넣어도 되고, 32토큰이어야 쓸 만한 기능이면 그건 기기에 둘 자리가 아니다.

이 특성이 기능의 모양도 정한다. 짧게 쓰고 쉬는 패턴 — 문장 자동완성, 알림 요약, 분류 — 은 열이 쌓일 틈이 없어 거의 항상 최고 속도로 돈다. 반대로 긴 문서를 통째로 요약하거나 여러 단계를 이어 도는 에이전트처럼 몇 분씩 이어지는 작업은 뒤로 갈수록 느려지고, 사용자는 "쓸수록 느려지는 앱"으로 기억한다.

대량 처리가 꼭 필요하면 조건을 붙이는 편이 낫다. 사진 수천 장에 태그를 다는 것 같은 일은 충전 중이고 화면이 꺼져 있을 때 하도록 미루면, 열도 배터리도 사용자 눈에 안 띈다. 두 플랫폼 모두 이런 조건부 백그라운드 작업을 위한 장치를 제공한다.

배터리는 사용자가 세는 유일한 비용이다

서버에서 비용은 청구서로 오지만 기기에서는 배터리 잔량으로 온다. 그리고 사용자는 어느 앱이 배터리를 먹었는지 설정 화면에서 정확히 볼 수 있다.

지속적인 추론은 기기와 유닛에 따라 다르지만 대체로 몇 와트를 먹는다. 동영상을 재생하는 것과 비슷하거나 그보다 무거운 수준이라고 보면 감이 맞는다. 짧은 요청 몇 번은 아무 문제가 없지만, 백그라운드에서 조용히 도는 기능을 넣으면 순위표 상단에 올라가고 그다음은 삭제다.

여기서 정할 것은 하나다. 이 기능이 배터리를 쓸 만한 가치가 있다고 사용자가 동의했는가. 사용자가 버튼을 눌러 시작한 작업이면 대개 괜찮고, 사용자가 모르는 사이에 도는 작업이면 거의 항상 문제가 된다.

어느 유닛에서 돌릴지가 절반을 정한다

같은 모델이라도 어느 연산 유닛에 올리느냐에 따라 속도와 전력이 몇 배씩 갈린다.

유닛 강점 대가
CPU 어디서나 돈다, 구현이 가장 단순하다 느리고 와트당 성능이 가장 나쁘다
GPU 넓게 지원되고 프리필이 빠르다 UI·게임과 자원을 다투고 열이 난다
NPU 와트당 성능이 압도적이다 벤더마다 도구가 다르고 모델 변환이 필요하다

NPU가 답처럼 보이지만 여기가 파편화의 진앙이다. 애플 쪽은 그나마 하나로 정리되어 있고, 안드로이드 쪽은 칩 제조사마다 자기 런타임과 자기 모델 포맷을 갖고 있다. 어떤 연산은 NPU에서 안 돌아 자동으로 CPU로 떨어지는데, 이 경우 데이터가 유닛 사이를 오가느라 CPU만 쓰는 것보다 느려지기도 한다.

현실적인 해법은 대개 이렇다. 이식성 있는 경로(CPU·GPU)를 기본으로 깔아 모든 기기에서 동작하게 만들고, 사용자 수가 많은 상위 몇 종 칩에만 NPU 경로를 추가한다. 그리고 어느 경로로 돌았는지를 반드시 기록한다 — 안 남기면 "왜 어떤 사용자만 느린가"를 영영 못 푼다.

메모리 상한을 넘으면 앱이 사라진다

기기에서 메모리 부족은 예외가 아니라 종료다. iOS는 앱마다 쓸 수 있는 몫에 상한을 두고 넘으면 즉시 죽이며, 안드로이드는 경고를 먼저 보내지만 여유가 급하면 마찬가지다.

모델을 올려 둔 앱은 이 상한에 상시로 가까이 있다. 그래서 세 가지를 미리 정해 둬야 한다.

  • 백그라운드로 갈 때 놓을 것인가. 놓으면 다시 돌아왔을 때 로드 시간을 또 낸다. 놓지 않으면 백그라운드에서 종료될 확률이 크게 오른다. 대개는 놓는 쪽이 낫고, 대신 돌아왔을 때의 로드를 눈에 안 띄게 만든다.
  • 문맥 길이의 상한. KV 캐시는 대화가 길어질수록 자라므로 상한이 없으면 언젠가 반드시 넘는다. 오래된 대화를 요약해 접는 처리를 처음부터 넣는다.
  • 저사양 기기에서의 동작. 램이 4GB인 기기에서는 아예 켜지 말고 서버로 보내는 것이 정답인 경우가 많다. 억지로 켜면 그 기기에서만 앱이 자주 죽고, 리뷰에는 기기 이름이 안 적힌다.

첫 글자까지의 침묵을 줄인다

체감 속도를 정하는 것은 초당 토큰 수보다 첫 글자가 나오기까지의 시간이다. 그리고 기기에서는 이 시간의 대부분이 계산이 아니다.

첫 글자까지의 침묵은 대부분 계산이 아니다

줄이는 방법은 셋인데 전부 "미리 해 두기"다. 모델 파일을 앱이 한가할 때 미리 메모리에 올려 두고, 파일 전체를 복사하는 대신 매핑해서 필요한 부분만 읽게 하고, 매번 똑같이 들어가는 프롬프트 앞부분은 계산 결과를 남겨 두었다가 재사용한다. 셋을 다 하면 4초짜리 침묵이 1초 아래로 내려간다.

그리고 첫 토큰이 나오는 즉시 화면에 흘리기 시작한다. 같은 총 시간이라도 글자가 흐르기 시작한 뒤의 기다림은 사용자가 훨씬 잘 견딘다.

모델을 어떻게 배달할 것인가

모델 파일은 앱 스토어의 크기 제한과 사용자의 저장 공간을 동시에 건드린다. 앱에 통째로 넣으면 설치 크기가 몇 배가 되고, 받아 오게 하면 첫 실행에 기다림이 생긴다. 대개는 받아 오는 쪽을 택하되 다음을 함께 준비해야 한다.

다운로드 중에도 앱이 서버 경로로 동작할 것, 저장 공간이 모자랄 때 사용자에게 무엇을 지우라고 할지 정할 것, 기기 등급에 따라 다른 크기의 모델을 내려 줄 것. 마지막 항목이 특히 실효가 크다 — 상위 기기에는 3B를, 중저가 기기에는 1B를 주고, 그 아래는 서버로 보내는 식이다.

그리고 기기의 모델은 사용자가 앱을 업데이트해야 바뀐다. 몇 달 전 버전이 계속 돌고 있다는 전제로 서버 쪽 인터페이스를 짜고, 문제가 생겼을 때 원격 설정 하나로 기기 경로를 끄고 서버로 되돌릴 스위치를 처음부터 넣어 둔다.

결국 하이브리드가 된다

여기까지의 제약을 모아 보면 결론은 하나로 모인다. 순수한 온디바이스 앱은 드물고, 잘 도는 것은 대개 기기를 기본값으로 두고 조건이 나쁠 때 서버로 넘기는 구성이다. 넘기는 조건은 기기가 목록에 없을 때, 램이 모자랄 때, 저전력 모드일 때, 요청이 기기가 감당할 길이를 넘을 때, 기기 답의 검증이 실패했을 때다.

이렇게 두면 좋은 기기의 사용자는 즉각적이고 사적인 경험을 얻고, 그렇지 않은 사용자도 느릴 뿐 못 쓰지는 않는다. 어느 요청을 어느 쪽으로 보낼지 정하는 이 판단은 기기 안에서만 쓰는 기법이 아니라 모델 여러 개를 함께 운영할 때 늘 필요한 기법이고, 뒤에서 따로 다룬다.


읽어주셔서 감사합니다. 😊

LATEST

모델 운영의 최신 글

모델 운영2026.09.04

KV 캐시 양자화 — 가중치보다 이쪽이 먼저 넘친다

긴 문맥에서 GPU 메모리를 실제로 잡아먹는 것은 가중치가 아니라 KV 캐시입니다. 캐시 크기를 계산하는 법, K와 V를 다르게 다뤄야 하는 이유, 어디까지 줄여도 되는지를 정리합니다.

16 MIN
모델 운영2026.09.04

양자화 보정 — 데이터 128개가 모델 품질을 정한다

양자화에서 스케일을 정하는 절차가 보정입니다. 무엇을 재는지, 데이터를 어디서 몇 개 뽑아야 하는지, 자르는 지점을 어떻게 고르는지, 그리고 잘못된 보정이 어떤 모양으로 드러나는지를 정리합니다.

21 MIN
모델 운영2026.09.03

과제 특화 증류 — 큰 모델의 답을 작은 모델에 옮긴다

범용 성능이 아니라 우리 과제 하나만 잘하는 작은 모델을 만드는 방법입니다. 교사에게 무엇을 받아야 하는지, 데이터를 어떻게 모으고 거르는지, 손익 분기가 어디인지를 정리합니다.

15 MIN