제 6 장

공개 모델의 로컬 실행

보안 담당자가 봇이 도는 것을 보다가 한 가지를 물었다. 사내 규정 원문이 매 요청마다 외부 API로 나가고 있는데 그래도 되느냐는 것이었다. 교육비 규정 정도야 그렇다 쳐도, 총무팀이 넘기겠다는 인사·복리후생 문서까지 같은 통로로 나간다고 생각하니 개발팀도 대답이 나오지 않았다.

그래서 개발팀은 모델 파일을 내려받아 사내에서 직접 돌리는 쪽을 따져 봤다. 그렇게 하면 문서와 질문이 밖으로 나가지 않고, 가중치와 추론 설정도 직접 고정할 수 있다. 대신 모델을 메모리에 올리고, 요청을 처리하고, 장애를 복구하는 일이 운영자의 몫이 된다. 개발팀은 작은 공개 모델 Qwen3-0.6B를 실제로 내려받아 한울연구소 문서에 네 가지를 물어봤다. 모델이 문서에 있는 답을 못 찾은 것도 실행 기록에 그대로 남겼다.

공개 가중치와 오픈소스

‘공개 모델’에서 공개된 것내려받으면 손에 들어오는 것✓가중치 (추론에 필요한 숫자)✓토크나이저 · 설정 파일✓채팅 템플릿따로 확인해야 하는 것?학습 데이터 · 정제 과정?학습 코드 · 재현 설정?사용 조건 (LICENSE)라이선스는 네 곳에 따로 있다. 프로그램의 LICENSE 하나로 전체를 판단하지 않는다추론 라이브러리모델 저장소데이터셋변환된 가중치

가중치를 내려받을 수 있다는 것은 추론에 필요한 숫자에 접근할 수 있다는 뜻이다. 학습 데이터, 정제 과정, 학습 코드는 따로다. 그래서 이 책에서는 내려받을 수 있는 가중치를 ‘공개 가중치 모델’이라고 부르고 사용 권한은 라이선스로 따로 본다. 개발팀이 고른 Qwen/Qwen3-0.6B는 고정한 리비전의 LICENSE가 Apache License 2.0이다. 제삼자가 변환한 파일을 쓸 때는 원본과 변환본의 출처·조건을 다시 기록한다. 이 리비전의 LICENSE

공개 가중치는 선택권을 늘리지만 운영 비용을 없애지는 않는다. API 청구 대신 장비, 실행 시간, 저장 공간, 배포와 유지보수 비용이 든다. 호출당 가격이 없다고 비용을 0으로 적지 않는다.

모델 카드와 실행 묶음

모델 카드는 후보를 빠르게 이해하는 출발점이다. 작업, 입력 형태, 사용 예, 제한 사항을 읽고 봇이 지켜야 할 요구사항과 맞춰 본다. 벤치마크 점수는 후보를 좁히는 참고일 뿐, 한국어 사내 문서의 금액·시행일·권한을 제대로 다룬다는 보증은 아니다. ‘다국어’나 ‘도구 사용’이라고 적혀 있다고 그 요구사항을 통과한 것으로 치지 않는다. 항목마다 검사할 입력을 따로 준비한다.

확인할 항목확인 이유이번에 고른 값
작업 유형생성·임베딩·분류 모델을 구별한다인과 언어 모델의 텍스트 생성
대화 입력 형식역할과 메시지 경계를 맞춘다저장소의 채팅 템플릿
파일과 리비전다음 실행에서 같은 자료를 쓴다전체 커밋 식별자 고정
로딩 자료형메모리와 연산 조건을 정한다CPU의 float32
실행 엔진모델 구조와 파일을 해석한다Transformers와 PyTorch
업무 적합성프로젝트 요구사항을 확인한다가상 문서 질문 4개를 실제 실행

개발팀은 다운로드와 단일 요청 추론을 직접 돌려 입력과 출력을 눈으로 보려고 작은 Qwen3 모델을 골랐다. 모델 카드가 안내하는 비사고 모드로 돌린다. 모델을 바꾸면 같은 이름의 옵션이 있는지부터 다시 보고 기본 모델인지 대화용으로 조정된 모델인지도 구별한다. 이때 보는 것은 이름의 접미사가 아니라 카드의 작업 설명과 입력 예제다. Qwen3-0.6B 모델 카드

저장소 식별자만 적어 두면 나중에 실행 기록을 펴 보는 사람은 어느 시점의 파일을 썼는지 알 수 없다. 브랜치의 최신 파일이 바뀌면 같은 코드가 다른 템플릿과 설정을 읽는다. 실행 기록에는 전체 커밋 식별자를 남긴다. Hub 다운로드 안내

model: Qwen/Qwen3-0.6B
revision: c1899de289a04d12100db370d81485cdf75e47ca
Python: 3.14.6
transformers: 5.16.1
torch: 2.14.0
safetensors: 0.8.0
huggingface-hub: 1.30.0
device: cpu
dtype: float32
CPU threads: 4

토크나이저, 설정, 가중치는 한 묶음이다. 다른 모델의 토크나이저를 섞으면 같은 문장이 다른 숫자열이 되어 결과가 나빠지거나 실행이 실패한다. 다운로드와 추론은 서로 다른 단계로 나뉜다. 오프라인으로 돌리면 캐시에서만 모델을 찾고, 캐시가 불완전하면 그대로 오류를 낸다. 모델 저장소에 딸려 온 임의 Python 코드는 실행하지 않도록 막아 둔다. 별도 코드를 요구하는 후보라면 옵션을 켜는 대신 그 구현과 의존성을 검토해 묶음에 넣는다.

로컬 추론의 흐름

내려받은 가중치는 따로 캐시에 두고 저장소에는 넣지 않는다. 첫 실행에는 다운로드 시간이 섞이니 캐시가 준비된 실행과 나눠 기록한다. 질문마다 모델을 다시 올리는 CLI와 모델을 유지한 채 여러 요청을 처리하는 프로세스도 시간을 따로 잰다.

메시지에서 결과 상태까지messages (system · user)role과 content. 모델은 이걸 직접 읽지 않는다apply_chat_template역할·경계를 그 모델의 특수 토큰으로 · enable_thinking=False토큰 ID 텐서입력 길이 = sizemodel.generatemax_new_tokens · do_sample=False출력 = 입력 토큰 + 새 토큰size 뒤만 잘라 낸다. 전부 디코딩하면 문서까지 답처럼 보인다decode → 종료 토큰 확인EOS면 ok · 한도 도달이면 incomplete · 빈 텍스트면 unexpected_output

애플리케이션은 role과 content가 있는 메시지를 만들지만 모델이 처리하는 것은 토큰의 숫자열이다. 채팅 템플릿이 역할과 메시지 경계를 그 모델의 특수 토큰으로 바꿔 준다. user:와 assistant:를 손으로 붙이는 방식이 학습 형식과 같다고 볼 수 없다. 템플릿 적용과 토큰화를 한 번에 시키면 특수 토큰이 두 번 들어가는 실수가 준다. 그리고 답이 시작될 자리를 템플릿이 표시하게 둔다. 템플릿은 입력 형식이지 문서 속 악성 지시를 막는 권한 장치가 아니다. Transformers 채팅 템플릿 안내

생성 결과에는 입력 토큰도 들어 있어서 전체를 디코딩하면 문서와 질문까지 답변처럼 보인다. 입력 길이 뒤의 새 토큰만 잘라 디코딩한다. 새로 만들 토큰 수에 상한을 두고, 입력 길이와 출력 예약량의 합이 예산 4,096토큰(모델 설정의 최대값이 더 작으면 그 값)을 넘는지 생성 전에 검사한다. 샘플링을 끄면 이번 실험의 생성 경로가 고정된다. 모델 카드가 품질을 위해 권하는 설정은 따로 있다. Transformers 시작 안내

종료 토큰과 상태

생성된 문자열이 있다고 바로 ok가 아니다. 마지막 토큰이 모델 설정의 EOS(종료 토큰)에 속하면 ok로 분류한다. 종료 토큰 없이 한도에 닿았으면 incomplete로 분류하고 부분 답변을 비운다. 정상 종료인데 빈 텍스트면 unexpected_output이다. 로컬 호출에는 제공자의 요청 ID가 없어 그 자리는 비어 있고, 사용량은 이 토크나이저로 계산한 입력·생성 토큰 수다. 청구서에 찍히는 수치는 제공자가 자기 기준으로 센다. 다운로드 오류와 메모리 부족은 예외로 올라온다. 서비스로 붙일 때는 모델 로드 실패와 개별 요청 실패를 나눠 준비 상태와 오류 응답에 반영한다.

실행 도구

방식선택할 때의 목적확인할 것
Transformers 직접 로딩Python에서 입력·토큰·자료형을 관찰한다장치 배치, 메모리, 생성 결과 처리
Ollama로컬 모델을 별도 서버의 API로 호출한다모델 식별, 스트림 종료, 서버 생명주기
LM Studio로컬 모델 실험과 개발용 API 연결을 함께 본다모델 로드 상태, 엔드포인트와 지원 기능

Ollama의 채팅 API는 messages를 받고 done으로 완료를 표현한다. 기본이 스트리밍인지 확인하고 클라이언트를 짠다. LM Studio도 개발자용 API가 있다. 두 제품은 설치하지 않았고, 성능을 견준 것이 아니라 실행 방식을 고를 때 무엇을 보는지만 적었다. ‘API 호환’이라고 적혀 있어도 요청 필드, 지원 모델, 도구 호출, 구조화 출력, 스트림 이벤트, 사용량 집계를 각각 본다. strict JSON Schema 검증이 평문 생성 함수에 그대로 따라오지는 않는다. 로컬 엔진의 제약 생성 기능을 쓰거나 별도 파싱·검증 경로를 만든다. Ollama 채팅 API, LM Studio 개발자 문서

메모리와 양자화

파라미터 596,049,920개의 가중치 크기float322.22 GiB 실측16비트1.11 GiB 계산값4비트0.28 GiB 계산값프로세스 메모리 = 가중치 + 그 밖의 것들 (측정하지 않았다)가중치토크나이저 · 라이브러리중간 연산생성 캐시로딩 임시 복사본입력이 길어지고 동시 요청이 늘면 오른쪽 칸들이 커진다. “0.6B니까 2.22GiB면 된다”로 장비를 고르면 부족하다

파일을 저장할 공간이 넉넉해도 추론할 메모리가 모자라면 모델이 올라오지 못한다. 낮은 정밀도로 저장된 파일도 로딩하면서 다른 자료형으로 바뀔 수 있다. 개발팀은 CPU에 float32로 올렸다. 실제로 올라온 객체의 파라미터가 596,049,920개이고 하나에 4바이트씩이니, 파라미터 텐서를 다 더하면 2,384,199,680바이트가 된다. 양자화는 수치를 더 적은 비트로 근사해 저장 공간과 메모리를 줄일 가능성이 있지만, 장치와 커널의 지원, 지연, 품질 변화는 같은 평가 사례로 따로 재야 한다. 이 기록은 양자화하지 않은 float32라 그림의 16비트·4비트 크기는 계산값이다. 양자화 방식 안내

메모리 부족은 어느 단계인지부터 본다. 로딩 중이면 가중치와 임시 복사본, 긴 질문에서만 실패하면 입력 길이와 생성 캐시, 요청이 겹칠 때 실패하면 동시 실행 수다. 지연도 입력 처리와 순차 생성으로 나눈다. 생성 토큰 수를 전체 시간으로 나눈 값에는 토큰화와 입력 처리가 섞여 있어서 순수 디코딩 처리량보다 낮게 나온다. 어댑터에 Apple 장치용 mps 선택지가 있지만 이번에는 CPU에서만 돌렸다.

실제 문서로 돌려 본 기록

환경은 macOS 26.6.2 arm64, CPU 4스레드, float32, 샘플링 없음, 새 토큰 상한 128이다. 워밍업과 병렬 요청은 없었고 캐시에서 모델을 올리는 데 약 3.53초가 걸렸다. 문서와 질문은 모두 가상 자료다.

실행 상태와 업무 판정은 다르다 · CPU float32 · 새 토큰 128사례실행 상태업무 판정호출 시간 (초, 2회)L01교육비 한도와 신청 순서✓ok✕답이 있는데 유보1.19 / 0.98L02한도 문장 그대로 옮기기✓ok△내용 ✓ · 형식 ✕2.07 / 2.04L03출장 숙박비 한도 (문서에 없음)✓ok✓유보가 정답1.09 / 1.09L04문서 없이 교육비 질문✓ok✓유보가 정답0.58 / 0.58실행 성공 4/4 · 업무 성공 2/4. 빨리 틀린 L01의 0.98초는 채택 근거가 아니다
사례기대 행동실제 결과
L01 교육비 한도와 신청 순서금액·분기 단위·승인·포털 신청 전달“문서에서 확인할 수 없다”고 답함
L02 한도 문장 그대로 옮기기한도 문장만 출력금액 문장은 정확하지만 안내 문구와 따옴표를 덧붙임
L03 출장 숙박비 한도문서에 없는 금액을 만들지 않음“확인할 수 없다”고 답함
L04 문서 없이 교육비 질문근거 부족으로 유보“확인할 수 없다”고 답함

네 호출 모두 종료 토큰을 만들어 상태가 ok였다. 그런데 개발팀이 L01에 넣은 문서를 다시 열어 보니 분기당 30만 원, 구매 전 팀장 승인, 지원 포털 신청이 정작 거기 다 있었다. 단서는 L02였다. 같은 문서에서 한도 문장을 그대로 옮기라고 하자 금액과 단위가 든 문장이 나왔으니, L01의 실패가 문서가 전달되지 않아서라고 볼 수는 없다. 그래서 다음에는 복합 질문을 나눠 보고 지시문도 바꿔 가며 다시 물어본다. L03·L04의 유보는 바람직하지만 언제나 유보하는 모델도 그 두 사례는 통과한다. 답변 가능 사례와 불가능 사례는 따로 봐야 한다. L02도 금액이 맞다는 이유로 ‘문장만’이라는 형식 요구를 통과한 것으로 치지 않는다.

개발팀은 같은 네 사례를 한 번 더 돌려 봤는데 결과가 그대로였다. 두 번씩 돌렸어도 물어본 것은 네 가지라 표본은 여덟이 아니라 넷이다. 권한·문서 리비전 충돌·도구·구조화 출력은 이 네 질문으로 확인하지 못한다.

호스팅 API와의 비교

질문만 같게 보내는 것으로는 부족하다. 문서, 지시문, 대화 이력, 출력 요구, 채점 기준을 맞추고 모델별 채팅 템플릿이 만든 실제 전송 형태도 기록한다. 같은 출력 토큰 상한이라도 한국어는 담기는 분량이 줄어든다. 비용은 API가 입력·출력·캐시 사용량에 단가를 곱한 값이고, 로컬은 장비 비용 배분과 가동 시간, 운영 비용이다. 둘 다 성공한 업무 한 건당 비용과 대기 시간으로 비교한다. L01처럼 빨리 나왔어도 답이 틀렸으면 짧은 시간을 채택 근거로 삼지 못한다. 이 기록에는 외부 API의 실제 호출이 없으므로 우열이나 절감률은 없다. 모의 전송 테스트는 API 처리 경로의 증거고 로컬 기록은 특정 모델의 실제 출력이라, 둘을 한 품질 표에 섞지 않는다.

개발팀은 규정 원문을 한 줄도 밖으로 내보내지 않고 사내 장비에서 답을 받아 냈다. 보안 담당자가 그래도 되느냐고 물었을 때 내놓을 것이 없었는데 이제는 그 실행 기록이 있다. 다만 사내에서 돌린 쪽만 재 봤으니 외부 API를 같은 조건으로 불러 견주는 일이 남았고, 무엇을 쓸지는 그다음에 고른다.

모델 호출을 애플리케이션으로 만들기6 / 21