- GGUF는 llama.cpp가 쓰는 언어 모델 파일 형식으로, 실행에 필요한 메타데이터를 단일 파일에 담아 모델 배포와 로딩을 단순하게 만듦
- 채팅 템플릿은 Jinja2 스크립트로 대화 형식, 도구 호출, 멀티미디어 메시지 인코딩을 처리하지만 구현체별 동작 차이가 있음
- GGUF는 종료 토큰 같은 특수 토큰과 권장 샘플러 설정을 담을 수 있고, 최근에는 샘플러 체인 순서도 명시 가능해짐
- 아직 도구 호출 형식은 모델마다 달라 추론 엔진별 하드코딩이 필요하며, 문법 기반 파서 생성이 표준 개선 후보로 남아 있음
- think_token, 프로젝션 모델 번들링, 기능 플래그가 부족해 생각 구간 분리, 멀티모달 구성, 지원 기능 감지가 여전히 어려움
GGUF가 담는 것
- GGUF는 llama.cpp가 언어 모델에 사용하는 파일 형식
- GGUF의 핵심 장점은 모델 실행에 필요한 여러 구성요소를 단일 파일에 담는다는 데 있음
- GGUF는 이런 부가 정보를 한 파일에 넣어 모델을 더 쉽게 다루게 해줌
채팅 템플릿
- 대화형 언어 모델은 특정 형식의 토큰 시퀀스로 학습되며, 이 형식은 대화 구조처럼 보임
- Gemma4 형식 예시는 다음과 같음
<|turn>user
Hi there!<turn|>
<|turn>model
Hi there, how can I help you today?<turn|>
<s>
<|im_start|>user Hi there!<|im_end|>
<|im_start|>assistant Hi there, how can I help you today?<|im_end|>
- 실제 템플릿은 추론 블록, 도구 설명, 도구 호출과 응답, 이미지·오디오·비디오 같은 멀티미디어 메시지 인코딩까지 포함하면서 훨씬 복잡해짐
- 이런 처리는 채팅 템플릿이 맡으며, Jinja2 템플릿 언어로 작성된 스크립트
- 모델은 여러 개의 채팅 템플릿을 가질 수 있음
- 도구 호출을 지원하는 템플릿과 지원하지 않는 템플릿이 따로 있을 수 있음
- 대부분의 모델은 단일 거대 채팅 템플릿을 제공하고, 도구가 지정된 경우에만 도구 호출 관련 처리를 함
- 일부 모델에서는 도구 전용 채팅 템플릿을 별도로 찾아야 함
- Jinja2는 루프, 조건문, 할당, 리스트, 딕셔너리 등을 가진 프로그래밍 언어에 가까움
- 대화형 LLM 애플리케이션은 새 메시지가 추가될 때마다 Gemma가 제공하는 약 250줄짜리 Jinja 스크립트 같은 프로그램을 실행할 인터프리터를 포함해야 함
- 구현체별 Jinja 처리 방식도 서로 다름
- Hugging Face transformers는 Python의 기존 jinja2 라이브러리를 사용함
- llama.cpp의 llama-server와 llama-cli는 자체 Jinja 구현을 사용함
- libllama API에 노출된 llama_chat_apply_template는 소수의 채팅 형식을 C++에 직접 하드코딩한 옛 방식
- NobodyWho는 Jinja 원작자가 Rust로 다시 구현한 minijinja를 사용함
- 이는 llama.cpp가 한때 사용했던 미니멀 Jinja 라이브러리 minja와는 다름
- Jinja 구현체 사이에는 상당한 성능 차이가 있음
- 로컬 LLM 애플리케이션에서 채팅 템플릿 처리는 성능 병목이 아니므로 큰 논쟁거리는 아님
특수 토큰
- 언어 모델은 입력된 토큰 시퀀스에 대해 다음 토큰을 계속 출력할 수 있으므로, 생성을 멈출 방법이 필요함
- 일반적인 해법은 종료 토큰을 두고, 모델이 이 토큰을 내보내면 추론 엔진이 생성을 멈추는 방식
- 종료 토큰은 특수 토큰의 한 예
- 특수 토큰은 일반적으로 토큰화된 문자 이상의 의미를 가짐
- 보통 사용자에게 보여주지 않아야 하지만, 텍스트 표현을 갖는 경우가 많아 표시 자체는 가능함
- Gemma4의 일부 특수 토큰 예시는 다음과 같음
- 1 / <eos>: 시퀀스 종료이며, 모델이 생성을 멈추기 위해 출력함
- 2 / <bos>: 시퀀스 시작이며, 입력 앞에 붙음
- 46 / <|tool_call>: 도구 호출의 시작을 표시함
- 47 / <tool_call|>: 도구 호출의 끝을 표시함
- 105 / <|turn>: 대화 턴의 시작을 표시함
- 106 / <turn|>: 대화 턴의 끝을 표시함
샘플러 설정과 순서
- 언어 모델은 다음 토큰 확률 분포를 출력하며, 이 분포에서 토큰을 고르는 과정을 샘플링이라고 함
- 가장 단순한 방식은 가중치가 적용된 분포에서 무작위로 선택하는 것
- 실제로는 구체적 토큰을 선택하기 전에 확률 분포에 변환을 적용하면 더 나은 결과를 얻을 수 있음
- 연구소가 새 모델을 배포할 때 특정 권장 샘플러 설정을 함께 제공하는 경우가 많음
- 사용자가 더 나은 응답을 얻기 위해 Markdown 파일 등에서 값을 복사해 붙여넣는 일도 잦음
- NobodyWho는 사용자의 수동 복사를 줄이기 위해 Hugging Face 페이지에 선별 모델을 올리고, 자체 형식으로 권장 샘플러 설정을 묶어 제공했음
- 동작은 했지만, 모델이 유용해지려면 NobodyWho 쪽 변환이 필요했음
- GGUF 형식에 최근 추가된 기능으로 샘플러 체인을 모델 파일 안에 직접 명시할 수 있게 됨
- 이로 인해 NobodyWho의 자체 형식은 불필요해졌고, 이는 원하던 결과였음
- llm-sampling 웹앱에서는 서로 다른 샘플러 단계의 역할을 빠르게 확인할 수 있음
- 개별 단계를 드래그 앤드 드롭하면, 샘플링 단계의 순서가 최종 분포에 큰 차이를 만들 수 있음
- Ollama 이미지의 JSON 파일이나 Hugging Face의 generation_config.json을 포함한 많은 샘플러 설정 형식은 샘플링 단계의 순서를 명시할 방법이 없음
- GGUF 표준은 general.sampling.sequence 필드로 샘플링 순서를 지정할 수 있음
- 여전히 많은 GGUF 모델은 이 필드를 생략하고 llama.cpp의 기본 동작이라는 암묵적 순서에 의존함
아직 빠진 것들
- 좋은 추론 엔진은 다양한 언어 모델에 대해 통합 인터페이스를 제공하려 함
- GGUF 메타데이터의 부가 정보를 파싱하고 활용하면 모델별 코드 경로를 많이 줄일 수 있음
-
도구 호출 형식
- 거의 모든 추론 엔진은 서로 다른 도구 호출 형식을 파싱하기 위한 하드코딩 경로를 갖고 있음
- Qwen3의 도구 호출 형식 예시는 다음과 같음
<tool_call>{"name": "get_weather", "arguments": {"location": "Copenhagen"}}</tool_call>
- Qwen3.5의 도구 호출 형식 예시는 다음과 같음
<tool_call>
<function=get_weather>
<parameter=city>
Copenhagen
</parameter>
</function>
</tool_call>
- Gemma4의 도구 호출 형식 예시는 다음과 같음
<|tool_call>call:get_weather{city:<|"|>Copenhagen<|"|>}<tool_call|>
- 새 모델이 나오면 여러 추론 엔진이 각자 파서를 구현해야 하는 상태
- 모델 파일에 문법(grammar)이 포함되고, 그 문법에서 파서를 도출할 수 있다면 GGUF 표준에 훌륭한 추가가 될 수 있음
- NobodyWho는 전달된 특정 도구에 맞춰 제약 문법을 생성하는 단계를 추가로 수행함
- 이를 통해 도구 호출의 타입 안전성을 보장할 수 있음
- 특히 1B 이하의 작은 모델이 정수가 필요한 곳에 float을 넘기는 식으로 실수할 수 있어 유용함
- 일반 도구 호출 파서를 만들 수 있는 문법이 있더라도, NobodyWho는 전달된 구체적 도구별 문법을 생성하는 함수는 계속 구현해야 함
- 특정 도구에 맞는 구체 문법을 만들고 거기서 파서를 도출할 수 있는 메타 문법 형식은 흥미로운 문제로 남아 있음
-
Think 토큰
- 빠진 항목 중 가장 쉽게 추가할 수 있는 부분
- 업스트림 Hugging Face 저장소는 think_token 필드를 포함하기 시작했음
- think_token은 생성된 출력의 생각 구간을 분리하는 데 매우 유용함
- 생각 구간은 일반적으로 제거하거나 본문 출력과 다르게 렌더링해야 함
- 다운스트림 GGUF 변환본은 이 필드를 보통 포함하지 않음
- 그 결과 GGUF 기반 추론 엔진은 특정 모델 계열별 코드를 따로 쓰지 않고는 생각 스트림을 본문 출력에서 분리할 수 없음
- 표준 GGUF 변환 파이프라인에 think_token을 추가하면 이 문제가 해결됨
-
프로젝션 모델
- 이미지와 오디오를 텍스트가 아니라 LLM이 네이티브로 볼 수 있게 하는 멀티모달 LLM 상호작용에는 비텍스트 입력 처리를 위한 추가 모델이 필요함
- 이 추가 모델은 프로젝션 모델로 불림
- 현재 관례는 두 개의 GGUF 파일을 전달하는 방식
- 하나는 주 언어 모델용 GGUF
- 다른 하나는 이미지와 오디오 처리를 위한 더 작은 모델
- 이 방식은 GGUF의 단일 파일 편의성을 깨뜨림
- 단일 GGUF 파일이 주 파일 안에 프로젝션 모델의 가중치와 설정을 함께 묶을 수 있다면 큰 개선이 됨
- 프로젝션 모델은 종종 약 1GB 크기
- 사용하지 않을 때는 이 오버헤드를 피하고 싶을 만큼 큼
- 프로젝션 가중치가 포함된 GGUF와 포함되지 않은 GGUF 두 가지 변형을 제공하는 방식은 합리적임
- 이렇게 하면 다운로드할 URL 하나, 디스크에 캐시할 파일 하나만 관리하는 상태로 돌아갈 수 있음
-
지원 기능 목록
- 모델마다 지원하는 기능이 다르며, GGUF 파일만 보고 실제 지원 기능을 쉽게 감지하기 어려움
- 일부 모델은 이미지 입력을 지원하고 일부는 지원하지 않음
- 현재 가장 나은 처리는 프로젝션 모델이 전달되면 이미지 지원이 있다고 가정하는 방식
- 일부 모델은 네이티브 도구 호출을 지원하고 일부는 지원하지 않음
- 현재 가장 나은 처리는 채팅 템플릿에서 도구 JSON 스키마 목록을 렌더링하려는 부분이 있는지 문자열 부분 일치로 확인하는 방식
- 이는 명백히 임시방편
- 일부 모델은 생각 블록을 출력하고 일부는 출력하지 않음
- 생각 태그가 보통 GGUF 메타데이터에 없기 때문에, 모델에서 생각 블록을 기대해도 되는지 확인할 좋은 방법이 불분명함
- GGUF 커뮤니티가 모델 파일에 기능 플래그를 추가하면 모델 비의존 추론 라이브러리가 더 일관된 오류 메시지와 경고를 제공할 수 있음
- 예를 들어 네이티브 도구 호출을 지원하지 않는 모델에 도구 호출을 시도할 때 더 적절한 안내가 가능해짐
결론
- GGUF는 모델을 올바르게 실행하는 데 필요한 부가 정보를 단일 파일로 담아, 모델별 코드 경로를 많이 추가하지 않아도 되게 함
- GGUF는 개방적이고 확장 가능한 형식이며, 강한 커뮤니티를 갖고 있음
- 표준을 함께 강화하면 좋은 개발자 경험을 유지하면서도 애플리케이션에서 모델을 쉽게 교체할 수 있음
- GGUF 메타데이터는 이미 유용한 부분이 많지만, 도구 호출 문법, think_token, 프로젝션 모델 번들링, 기능 플래그 같은 개선 여지가 남아 있음