GGUF에는 가중치 외에 무엇이 들어 있고, 아직 무엇이 빠져 있나?
(nobodywho.ooo)- GGUF는 llama.cpp가 쓰는 언어 모델 파일 형식으로, 실행에 필요한 메타데이터를 단일 파일에 담아 모델 배포와 로딩을 단순하게 만듦
- 채팅 템플릿은 Jinja2 스크립트로 대화 형식, 도구 호출, 멀티미디어 메시지 인코딩을 처리하지만 구현체별 동작 차이가 있음
- GGUF는 종료 토큰 같은 특수 토큰과 권장 샘플러 설정을 담을 수 있고, 최근에는 샘플러 체인 순서도 명시 가능해짐
- 아직 도구 호출 형식은 모델마다 달라 추론 엔진별 하드코딩이 필요하며, 문법 기반 파서 생성이 표준 개선 후보로 남아 있음
think_token, 프로젝션 모델 번들링, 기능 플래그가 부족해 생각 구간 분리, 멀티모달 구성, 지원 기능 감지가 여전히 어려움
GGUF가 담는 것
- GGUF는 llama.cpp가 언어 모델에 사용하는 파일 형식
- GGUF의 핵심 장점은 모델 실행에 필요한 여러 구성요소를 단일 파일에 담는다는 데 있음
- Hugging Face의 일반적인 safetensors 저장소는 필요한 JSON 파일이 여러 곳에 흩어져 있음
- 일반적인 Ollama 모델은 레이어 JSON, Go 템플릿 등을 포함한 OCI 형태
- GGUF는 이런 부가 정보를 한 파일에 넣어 모델을 더 쉽게 다루게 해줌
채팅 템플릿
- 대화형 언어 모델은 특정 형식의 토큰 시퀀스로 학습되며, 이 형식은 대화 구조처럼 보임
- Gemma4 형식 예시는 다음과 같음
<|turn>user
Hi there!<turn|>
<|turn>model
Hi there, how can I help you today?<turn|>
- LFM2 형식 템플릿 예시는 다음과 같음
<s>
<|im_start|>user Hi there!<|im_end|>
<|im_start|>assistant Hi there, how can I help you today?<|im_end|>
- 실제 템플릿은 추론 블록, 도구 설명, 도구 호출과 응답, 이미지·오디오·비디오 같은 멀티미디어 메시지 인코딩까지 포함하면서 훨씬 복잡해짐
- 이런 처리는 채팅 템플릿이 맡으며, Jinja2 템플릿 언어로 작성된 스크립트
- 예시로 Gemma 4에 포함된 chat template가 있음
- GGUF 메타데이터에서는 기본 채팅 템플릿이
tokenizer.chat_template키 아래 저장됨
- 모델은 여러 개의 채팅 템플릿을 가질 수 있음
- 도구 호출을 지원하는 템플릿과 지원하지 않는 템플릿이 따로 있을 수 있음
- 대부분의 모델은 단일 거대 채팅 템플릿을 제공하고, 도구가 지정된 경우에만 도구 호출 관련 처리를 함
- 일부 모델에서는 도구 전용 채팅 템플릿을 별도로 찾아야 함
- Jinja2는 루프, 조건문, 할당, 리스트, 딕셔너리 등을 가진 프로그래밍 언어에 가까움
- 대화형 LLM 애플리케이션은 새 메시지가 추가될 때마다 Gemma가 제공하는 약 250줄짜리 Jinja 스크립트 같은 프로그램을 실행할 인터프리터를 포함해야 함
- 구현체별 Jinja 처리 방식도 서로 다름
- Hugging Face transformers는 Python의 기존 jinja2 라이브러리를 사용함
- llama.cpp의
llama-server와llama-cli는 자체 Jinja 구현을 사용함 libllamaAPI에 노출된 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, 프로젝션 모델 번들링, 기능 플래그 같은 개선 여지가 남아 있음
Hacker News 의견들
-
프로젝션 모델이 별도 파일로 나뉘게 된 건 아쉽고, 나도 단일 파일 안에 들어가는 편을 원했음
왜 그렇게 됐는지는 정확히 모르겠지만, GGUF를 설계할 때 염두에 둔 단일 파일 철학과는 꽤 어긋남
둘을 합치는 일을 누군가 이끌어주길 바라며, 이번에는 내가 흐름에서 너무 벗어나 있는 것 같음 :-)- 지금 MTP 지원이 개발 중인 걸 보면, 그 논의 중에 Mmproj처럼 MTP 모델을 메인 GGUF에서 분리하자는 아이디어가 오간 듯했지만 거부됐음
그 결정은 마음에 듦. 그렇다면 Mmproj 파일도 GGUF 안에 포함하는 데 열려 있을 가능성이 있다고 보는 게 무리도 아니라고 생각함
떠오르는 유일한 문제는 어떤 형식을 넣을지임. BF16, F16 등 선택지가 있음
- 지금 MTP 지원이 개발 중인 걸 보면, 그 논의 중에 Mmproj처럼 MTP 모델을 메인 GGUF에서 분리하자는 아이디어가 오간 듯했지만 거부됐음
-
GGML과 GGUF는 오픈소스 머신러닝/AI 생태계에 매우 중요했음
llama.cpp, whisper.cpp, stable-diffusion.cpp 같은 프로젝트는 다양한 플랫폼과 하드웨어 백엔드에서 대체로 바로 잘 동작함- llama.cpp는 Meta 쪽 산물이긴 하고 Meta는 정말 싫어하지만, 다른 것들에 비해 가장 쉽다는 건 인정함
컴파일하고 모델을 넣고 실행하면 됨. 그러면 웹 UI와 API까지 얻을 수 있음
- llama.cpp는 Meta 쪽 산물이긴 하고 Meta는 정말 싫어하지만, 다른 것들에 비해 가장 쉽다는 건 인정함
-
> <|turn>user Hi there!<|turn>model Hi there, how can I help you today
세상에, XML보다도 가독성이 낮은 형식을 만들어냈음- 사람이 읽으라고 만든 형식이 아님. 실제로 들여다볼 일도 거의 없음
이 형식은 실제 내용과 혼동되지 않도록 설계됐고, 그 내용은 인터넷에서 온 아무 텍스트나 될 수 있음
그러려면 어디에서도 쓰이지 않는 형식을 써야 함 - 맞음. 메모리 사용 효율 측면에서는 최적이 아닌 형식처럼 보임
- 사람이 읽으라고 만든 형식이 아님. 실제로 들여다볼 일도 거의 없음
-
현재 가장 크게 빠진 것은 모델 아키텍처를 현재 빌드에 하드코딩하지 않고 정의하는 방법이라고 봄
완전히 지원되는 모델과 1:1 성능 동등성을 가질 필요는 없음
출시 첫날부터 벤더가 검증한 제대로 된 지원이 있느냐가 모델을 훌륭하다고 느끼게 하느냐 끔찍하다고 느끼게 하느냐를 가름함. 최근 Gemma와 Qwen 출시가 그 예임
해결책은 잘 모르겠지만, 모델 그래프를 설명하는 DSL을 작성해 GGUF에 넣는 방식이 있을 수 있음
다른 대안은 공식 모델 릴리스의 PyTorch 모듈을 읽어서 어떻게든 GGML 연산으로 변환하는 것임- GGUF 명세에 계산 그래프를 포함할 공간을 일부러 남겨뒀고, 누군가 이어받길 바랐음
첫 버전에 넣고 싶었지만, 당시에는 최소 기능 명세를 내고 구현되게 하는 쪽을 우선했음
지금도 보고 싶지만, 현재 GGML IR 상태를 아주 잘 아는 추진자가 필요함 - 계산 그래프를 ONNX처럼 가중치 파일 안에 임베드할 수 있을 것 같음
그런 다음 공통 매개변수를 받는 공통 인터페이스를 노출하고, 추가 커스텀 매개변수는 Wayland처럼 확장으로 둘 수 있음
그러면 LLaMa 같은 트랜스포머 계열뿐 아니라 RWKV 같은 순환 신경망 계열, 멀티모달 모델 등도 지원할 수 있음
실제 구현은 잘 모르겠지만 멋진 아이디어로 들림. 다만 계산 그래프가 모델 파일에 박혀 있으면, 가중치를 바꿀 필요 없는 아키텍처 개선이나 최적화가 기존 파일에는 변환 없이는 적용되지 않을까 걱정됨
- GGUF 명세에 계산 그래프를 포함할 공간을 일부러 남겨뒀고, 누군가 이어받길 바랐음
-
> GGUF의 정말 깔끔한 점은 파일 하나라는 것이다. Hugging Face의 일반적인 safetensors 저장소와 비교하면, 필요한 JSON 파일들이 여기저기 흩어져 있다 [...]
흥미롭게도 내게 AI 모델은 “항상” 단일 파일이었음. 로컬 이미지 생성 쪽에서는 그게 표준이었기 때문임
safetensors 파일도 내부에 온갖 것을 넣을 수 있어서, 이를 위해 꼭 GGUF가 필요한 건 아님
다만 현대 모델의 텍스트 인코더는 그 자체로 수 기가바이트짜리 언어 모델이라, 누구도 모든 체크포인트에 중복 복사본을 넣지는 않음- 단일 파일 배포는 내가 의도적으로 세운 설계 목표였음
대부분의 이미지 모델은 단일 파일이었거나 지금도 그렇지만, LLM의 safetensors는 적어도 당시에는 그렇지 않았고, 구조적 수준에서 이를 강제하고 싶었음
또한 실행기, 예를 들어 llama.cpp에 JSON 리더를 요구하고 싶지 않았는데, ST 방식은 그게 필요했을 것임
더 큰 문제는 기억이 맞다면 당시 ST가 GGML의 새 양자화 형식을 지원할 수 없었고, 자체 파일 형식이 있으면 ST로는 얻기 어려운 유연성을 확보할 수 있었다는 점임 - “로컬 이미지 생성 쪽에서는 AI 모델이 항상 단일 파일이었다”는 말은 그 영역에서도 말이 안 됨
아키텍처를 가중치로 실제 실행하려면 단일 가중치 파일만 쓰는 게 아니라 여러 인코더와 디코더 등이 필요함
쓰는 도구가 그걸 숨겨줄 수는 있지만, 표면 아래에는 여전히 존재함
- 단일 파일 배포는 내가 의도적으로 세운 설계 목표였음
-
libllamaAPI에 노출된, 몇 가지 채팅 형식을 C++에 직접 하드코딩한 다소 이상한llama_chat_apply_template말인데, 데스크톱 기반 추론 앱을 FLTK[0]로 만지작거리는 입장에서는 이게 llama.cpp가 쓰는 실제 Jinja2 템플릿 파서를 사용했으면 좋겠음
아니면 그런 일을 해주는 다른 C 함수가 있었으면 함. 제대로 파싱하려면, 예를 들어 도구 호출 여부를 템플릿이 알 수 있도록 여러 데이터를 넘길 수 있어야 하는 것 같음
지금은 이 임시스러운 함수를 쓰고 있지만, 결국 Jinja2 인터프리터를 직접 쓰거나 llama.cpp 코드에서 가져와 붙이게 될 것 같음
그래도 GGUF의 올인원 접근은 매우 편리함. 프로젝션 모델이 별도 파일인 건 이상하게 느껴진다는 데 동의함
비전 지원 모델을 처음 받았을 때 적당해 보이는 GGUF만 내려받았는데, llama.cpp가 모델을 처리할 수 없다고 해서 한참 뒤에 추가 파일이 필요하다는 걸 깨달았음
그때 든 생각이 말 그대로 “GGUF는 전부 담는 형식 아니었나?”였음 :-P
[0] https://i.imgur.com/GiTBE1j.png -
나는 항상 Hugging Face 저장소와 비슷한 safetensors + 메타데이터 파일 형식을 써왔음
큰 불편은 전혀 아니지만, GGUF가 더 조밀한 형식과 좋은 지원을 갖춘 건 괜찮아 보임 -
GGUF에 아직 없는 것들을 보면서 오히려 GGUF를 더 배웠음
도구 호출 형식은 정말 자연스럽고, LLM에서 에이전트로 넘어가는 이정표가 될 것 같음 -
최근 TheBloke의 7B Mistral을 받아서 시험해보려 했고, 4070을 가지고 있음
- Mistral을 좋아하지만 그 모델은 최고는 아님
Gemma 4 e4b를 한번 써보는 게 좋겠음. Mistral 7B와 비슷한 크기이고 4070에서 잘 돌아갈 것임
“E4B”라는 이름은 살짝 오해를 부를 수 있음 - 7B Mistral은 꽤 오래됐음
12GB 4070에서는 Qwen 3.5 9B q4km나 Qwen 3.6 35B를 돌릴 수 있음. 후자가 훨씬 똑똑하지만 메모리 오프로딩 때문에 훨씬 느림
둘 다 LM Studio에서 써보면, 정말 놀랄 만큼 능력이 좋음 - 2070에서도 정말 빠르게 잘 동작하는 걸 확인했음
TheBloke를 좋아해서, 아직도 모델을 만들어줬으면 좋겠음
- Mistral을 좋아하지만 그 모델은 최고는 아님