2026년 8월 5일

  • inference
  • on-device
  • evaluation
  • grammar

실패담: 사용자 노트북에서 LLM을 돌리는 방법

한줄 요약

문서를 서버로 보내지 않으려고 사용자 노트북에서 모델을 직접 돌렸더니, 사무용 노트북에서 문서 한 건 평가에 23분이 걸렸습니다. 그래픽카드가 없는 CPU에서는 모델을 키우는 쪽도 줄이는 쪽도 막혀서, 작은 모델을 쓰고 JSON을 없애고 문법으로 형식을 강제하는 가설로 갔습니다. 구현하다 여섯 번 막혔고 원인은 전부 저희 코드였습니다. 10배 처리 속도를 높인 것은 쓰지도 않는 확률 분포를 매 토큰 받아오던 옵션 한 줄이었습니다.

최근 진행하던 R&D 중 문서를 서버로 보내지 않고 저희 자체 AI 모델로 처리할 것을 요구받았습니다. 저희 기업 사용자들이 다루는 파일이 이력서, 면접 기록, 회의록, 계약서라서 애초에 외부 전송을 꺼립니다. 그래서 모델을 사용자 PC에서 직접 돌리기로 했고, 그 대가로 하드웨어를 통제할 권한을 잃었습니다. 그 하드웨어의 상당수는 그래픽카드가 없는 사무용 노트북이어서 참 난감했습니다. 근데 뭐 어쩌겠습니까, AI의 민주화를 위해서는 하드웨어 제약을 뚫어야 하지요.

환경문서 1건 평가
개발 머신 (GPU)수 초
사무용 노트북 (CPU)23분

개발 머신에서 수 초에 끝나는 일이 사무용 노트북에서는 23분입니다. 이 글은 그 23분을 줄이려고 세운 가설과, 그 가설을 구현하다 막힌 자리를 순서대로 적은 기록입니다.

GPU가 없으면 CPU에서 돌려야 합니다

첫 제약은 그래픽카드가 없다는 것이고, 그래서 토큰 생성이 전부 CPU에서 돕니다. CPU에서 decode는 메모리 대역폭이 지배합니다. 토큰 하나를 뱉을 때마다 모델 가중치 전체를 메모리에서 다시 읽어와야 하니, 노트북 대역폭 대략 40GB/s에 4비트 4B 모델 약 2.5GB를 넣으면 이론 상한이 초당 16토큰으로 나오고, 실측은 그 절반도 안 됩니다.

두 번째 제약은 RAM입니다. 대상 노트북 상당수가 8GB이고 그 안에서 운영체제와 저희 앱과 모델이 같이 살아야 하니, 4비트로 양자화해도 올릴 수 있는 크기가 먼저 정해집니다. 온디바이스에서는 아키텍처와 RAM과 양자화가 후보를 잘라낸 다음에야 품질을 비교할 수 있습니다.

아키텍처도 같은 자리에서 걸립니다. 예를 들어 최근 세대 모델은 레이어 75%가 선형 어텐션이라 긴 문서에서 메모리 비용이 낮은데, 이 방식은 커지는 캐시를 고정 크기 순환 상태로 대체하기 때문에 요청 하나를 짧은 입력으로 처리하는 조건에서는 표준 방식보다 오히려 더 메모리에 묶입니다. 저희 워크로드가 정확히 그 조건입니다. 서버에서 이득인 설계가 사용자 노트북에서 그대로 이득이 되지는 않습니다.

키우는 쪽과 줄이는 쪽이 같이 막힙니다

모델을 키우면 품질은 올라갈 수 있는데, 위의 천장이 크기에 비례해서 내려가고 8GB 안에 들어가지 못하면 아예 실행이 안 됩니다. 반대로 줄이면 실행은 되지만 속도 자체가 나쁩니다. 23분은 이미 줄인 상태에서 나온 숫자입니다.

크다고 품질이 항상 좋은 것도 아닙니다. 벤치마크에서 크기 격차가 뚜렷하게 벌어지는 항목은 컨텍스트를 많이 쓰는 작업인데, RAM 때문에 애초에 그 구간까지 갈 수 없습니다. 저희가 실제로 돌리는 것은 짧은 입력 한 건이라, 큰 모델의 강점이 나타나는 구간과 저희 사용 구간이 겹치지 않습니다.

가설: 작은 모델, JSON 제거, 문법, 사고 모드 끄기

그래서 세운 가설은 네 줄입니다. 작은 모델을 쓰고, 출력에서 JSON을 없애고, 형식은 GBNF 문법으로 강제하고, 사고 모드는 끕니다. 네 항목의 이유가 같은데, CPU에서 토큰 하나가 비싸니 낭비되는 토큰을 줄이고 남은 토큰이 틀릴 여지를 없애는 것입니다.

JSON을 없앤 이유

기존에는 모델에게 {"name": "...", "email": "..."} 형태를 직접 만들게 시켰습니다. 이 방식은 출력의 상당 부분이 구두점으로 나가고, 한 글자만 틀려도 객체 전체가 무효가 되고, 값이 없는 칸에서 모델이 "모르겠다"를 말할 수 없어 값을 지어냅니다. 세 번째는 채용 판단에 쓰이는 값입니다. 게다가 프롬프트에는 각 칸이 무엇을 뜻하는지 설명이 없어서, 모델은 채울 내용의 의미를 모른 채 JSON 모양만 맞추고 있었습니다.

제 첫 해결안은 자유 텍스트로 받아서 저희가 파싱하자는 것이었고, 이것도 틀렸습니다. 그러면 취약성이 파서로 자리만 옮기고, 옮겨간 자리에서는 실패가 조용해집니다.

(저희가 컨텍스트에 직접 씀)   이름:
(모델이 디코딩)               박서연
(저희가 씀)                   전화번호:
(모델이 디코딩)               N/A

라벨을 저희가 캐시에 직접 써 넣으니 모델은 라벨을 틀릴 수 없습니다. 문서는 최초 1회만 처리하고 그 상태가 캐시에 남으니, 필드가 늘어도 라벨 몇 토큰씩만 더 붙습니다. 부재 토큰을 합법으로 허용해 둔 덕에 모델은 없는 값을 정직하게 없다고 말할 수 있고, 필드 하나가 실패해도 나머지는 살아남고, 채워지는 대로 화면에 하나씩 나옵니다.

추출·평가·생성·템플릿 네 경로에 같은 구조를 적용하고 JSON 생성 함수를 지웠습니다.

문법이 하는 일

GBNF는 모델이 다음에 쓸 수 있는 토큰을 제한하는 규칙입니다. 프롬프트로 "숫자만 쓰세요"라고 부탁하면 확률이 그쪽으로 기우는 정도에 그치지만, 문법은 샘플러 계층에서 위반 토큰의 확률을 0으로 눌러 위반 자체를 불가능하게 만듭니다.

flowchart TD
  A["모델 forward pass<br/>어휘 전체에 logit"] --> B["문법 마스킹<br/>위반 토큰 확률을 0으로"]
  B --> C["샘플링"]
  C --> D["선택된 토큰으로<br/>문법 상태 전진"]
  D --> A

그래서 문법이 허용해도 모델이 확률을 주지 않으면 그 토큰은 나오지 않고, 문법이 막으면 모델이 아무리 원해도 낼 수 없습니다. 뒤에서 이야기할 막힌 자리 하나가 정확히 이 두 번째 경우입니다.

(여담: 이번에 새로 배운 어원도 한 줄 끄적여 봅니다. GBNFGGML BNF이고 BNF는 Backus-Naur Form인데, 원래 이름은 Backus Normal Form이었습니다. 1964년 Donald Knuth가 학회지에 편지를 보내 이것은 수학적 normal form이 아니며 Naur의 기여가 이름에 들어가야 한다고 지적했고, 그 제안이 받아들여졌습니다. 1967년에는 P.Z. Ingerman이 산스크리트 문법을 형식 규칙으로 기술한 파니니에게 공을 돌려 Pāṇini-Backus Form으로 부르자고 제안했고, 위키백과는 지금도 이 별칭을 병기합니다. GGMLGGGeorgi Gerganov입니다.)

가설을 구현하다 여섯 번 막혔습니다

가설은 종이 위에서만 그럴듯했기 때문에, 실제로 저희 제품이 다루는 문서로 확인해야 했습니다. 이력서와 면접 기록을 넣고 이름·연락처·경력 같은 칸을 채운 다음 점수를 매기는 설정을 만들어서, 네 가지 결정을 한꺼번에 켜고 돌렸습니다. 이때 최근 세대 소형 모델도 후보로 같이 올렸는데, 라이선스가 Apache 2.0이고 4비트 배포본이 이미 나와 있어서 그대로 받아 쓸 수 있었고, 텍스트와 이미지를 한 모델로 처리하니 비전 모델을 따로 싣지 않아도 되겠다는 계산이었습니다.

돌려 보니 여섯 번 막혔고, 그중 남겨 둘 만한 것은 네 가지입니다.

첫 번째는 줄바꿈 하나였습니다. 사고 모드를 끄려면 모델에게 "생각은 건너뛰고 바로 답하라"는 신호를 정해진 형식으로 넣어 줘야 하는데, 저희가 넣은 신호가 배포본 문서의 형식과 줄바꿈 한 개만큼 달랐습니다. 모델은 그것을 신호로 알아보지 못했고, 점수 칸이 전부 비어서 돌아왔습니다. 원인은 저희가 만든 템플릿이었습니다.

두 번째는 모델이 "다 썼습니다"라고 말할 길이 없던 것입니다. 문법은 다음에 올 수 있는 토큰을 제한하는데, 저희가 쓴 규칙에는 대답을 끝내는 토큰으로 가는 경로가 빠져 있었습니다. 그래서 모델은 할 말을 다 하고도 멈추지 못하고 공백만 계속 찍었고, 문서 한 건이 토큰 예산을 다 태울 때까지 1393초가 걸렸습니다. 맨 앞에 적은 23분이 바로 이 숫자입니다.

세 번째는 채점표가 거짓말을 한 것입니다. 정수를 받는 칸에 자릿수 제한을 걸어 두지 않았더니, 문서에 값이 없을 때 기존 세대 모델은 숫자를 끝없이 뽑아냈고 그 답은 형식 오류로 걸러져 집계에서 "정답" 쪽에 붙었습니다. 새 세대 모델은 그럴듯한 숫자 하나를 냈고 그 답은 "환각"으로 집계됐습니다. 같은 실패인데 표에서는 새 모델만 나쁜 모델로 보였습니다.

네 번째는 속도입니다. 저희는 토큰 하나를 고를 때마다 어휘 전체에 대한 확률표를 통째로 받아오고 있었고, 실제로 쓰는 것은 고른 토큰 하나의 확률뿐이었습니다. 옵션 한 줄을 지우니 평가가 CPU에서 4분 이상에서 82초로, 추출이 GPU에서 242초에서 113초로 줄었습니다. 새 세대가 "2.1배 느리다"고 적어 둔 격차도 상당 부분 이 버그였습니다.

네 번의 원인은 전부 저희 코드에 있었습니다. 이메일·전화번호처럼 형식이 까다로운 칸을 문법으로 더 조이려다 규칙 자체를 깨뜨린 일까지 겪고 나서 규칙을 하나 정했습니다. 문법에는 어떤 종류의 토큰이 올 수 있는지만 맡기고, 언제 멈추고 어디서 자르고 어떻게 맞출지는 저희 코드에서 처리합니다.

판정을 하네스로 옮겼습니다

네 번 막히고 나서 분명해진 것은, 결과를 눈으로 훑어보는 방식으로는 계속 속는다는 사실이었습니다. 그래서 문서를 넣고 나온 답을 미리 만들어 둔 정답과 자동으로 대조하는 도구를 만들고, 새로운 실패를 만날 때마다 검사를 하나씩 붙였습니다. Apache 2.0으로 공개해 뒀습니다: redrob-labs/redrob-eval.

이 도구로 같은 계열의 작은 모델과 한 단계 큰 모델을 기존 세대와 최근 세대 양쪽에서 같이 재봤습니다. 숫자는 정답과 맞은 칸의 개수입니다.

세대소형중형
Qwen328 / 4534 / 45
Qwen3.529 / 4528 / 45

예상은 같은 계열이면 클수록 점수가 오른다는 것이었는데, 최근 세대에서는 짧은 문서 기준으로 작은 쪽이 큰 쪽에 밀리지 않았고, 기존 세대에서는 큰 쪽이 여섯 칸 앞섰습니다. 한 사이즈만 재고 결론을 냈다면 세대 비교를 반대로 적었을 것입니다.

그래도 최근 세대는 채택하지 않았습니다. 경력 개월 수가 문서에 없을 때 0을 반환하는 실패가 모델을 키워도 그대로 남았고, 한국어는 글자 하나가 차지하는 토큰이 최악 기준 2에서 3으로 늘었습니다. 토큰이 늘면 같은 문서를 읽는 데 시간이 더 드는데, 저희는 토큰 하나가 비싼 쪽에서 싸우는 중이었습니다.

지금 배포하는 구성

그래서 세대는 Qwen3에 남기고, 크기는 사용자 기계에 맞춰 고르기로 했습니다.

사용자 환경모델평가 1건
8GB, GPU 없음Qwen3-1.7B Q4_K_M88초
16GB, GPUQwen3-4B Q4_K_M7초

맨 앞에서 23분이 걸리던 사무용 노트북이 88초가 됐습니다. 88초는 사용자가 화면을 보며 기다리기에는 길고 뒤에서 돌려 두기에는 충분한 시간이라, 평가는 사용자가 다른 일을 하는 동안 백그라운드로 처리하고 끝나면 알립니다. GPU가 있는 머신은 7초라 그냥 기다려도 됩니다. 이 12.6배가 제품에서 기다려도 되는 기능과 기다릴 수 없는 기능을 나누는 선이 됐고, 대화 기능은 어느 쪽 기계에서도 88초를 감당할 수 없어서 같은 구성으로 묶지 않았습니다.

따라하지 않으셨으면 하는 것들

  • 비교 표를 읽기 전에, 실패가 성공으로 집계되는 경로가 있는지 먼저 찾습니다.
  • 모델은 한 사이즈만 보고 판정하지 않고, 같은 계열의 두 사이즈를 같이 잽니다.
  • 문법은 형식을 강제하는 용도로만 쓰고, 언제 멈출지는 코드에서 정합니다.
  • 토큰 예산을 글자 수로 계산하지 않습니다. 영어만 보면 통과하고 한국어·인도어에서 깨집니다.
  • 무언가 실패했을 때 조용히 다른 경로로 넘어가게 두지 않습니다. 시작할 때 검사하던 항목만 제대로 잡혔습니다.

하나로 줄이면, "모델이 나쁘다"는 결과가 나오면 먼저 자기 코드를 의심합니다.

업스트림에 올려야 합니다

엔진은 llama.cpp입니다. 막힌 자리 중 일부는 저희 규칙 버그였고, 일부는 엔진 쪽에 남은 문제입니다. 후자는 저희만 쓰는 포크에 감춰 두는 것보다 이슈와 PR로 올리는 쪽이 맞다고 보고 그렇게 정리하고 있습니다.

아직 안 끝난 것

짧은 입력 한 건 조건만 재봤고, 규모를 올린 조건은 아직입니다. 큰 모델의 강점이 나온다고 알려진 구간이라, 여기를 안 재고는 "작은 모델로 충분하다"를 일반화할 수 없습니다.

정확도 측정도 영어 합성 문서뿐입니다. 인도 시장이 본진이고 한국어가 다음인데, 한국어 글자당 토큰이 3이라는 것만 재고 한국어 문서로 추출 정확도를 본 적은 없습니다. 점수 표본도 한 번씩만 돌려 봐서 분산을 모릅니다.

일곱 번째로 틀릴 자리가 여기라고 보고 있습니다.

참고 자료

외부 사실은 위 출처를 확인해 적었고, 자체 수치는 실측이며 표본이 작고 대부분 단일 측정입니다.