- Published on
PyTorch→GGUF 변환 실패 - 양자화·텐서명 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서빙용 로컬 LLM 파이프라인에서 PyTorch 체크포인트를 GGUF로 변환하려다 실패하는 케이스는 생각보다 패턴이 뚜렷합니다. 특히 llama.cpp 계열 툴체인에서 많이 겪는 문제는 크게 두 갈래입니다.
- 양자화(quantization) 단계에서의 실패:
q4_k_m같은 스킴 적용 중 텐서 dtype, shape, 메모리 정렬, 지원 연산 불일치로 중단 - 텐서명(tensor name) 불일치: 모델이 기대하는 키 이름과 실제 체크포인트
state_dict키가 다르거나, 레이어 인덱싱 규칙이 달라 매핑이 깨짐
이 글은 “왜 실패하는지”를 로그에서 빠르게 분류하고, 변환 전에 PyTorch 체크포인트를 정리하거나 변환 스크립트에 매핑 규칙을 추가해 끝까지 GGUF를 뽑아내는 방법을 다룹니다.
참고로 로컬 LLM이 메모리에서 터지는 문제는 변환 이슈와는 별개지만, 변환 후 실행 단계에서 OOM이 자주 이어집니다. 실행 메모리 최적화는 별도 글인 Transformers 로컬 LLM OOM 해결 - 4bit+오프로딩도 함께 보시면 흐름이 매끈합니다.
변환 파이프라인을 먼저 고정하기
PyTorch→GGUF는 보통 아래 흐름 중 하나입니다.
PyTorch(.bin/.pt/.safetensors)→ (필요 시)HF Transformers형태로 정규화HF모델을llama.cpp변환 스크립트로GGUF생성- 생성된
GGUF를quantize바이너리로 양자화(Q4_K_M,Q5_K_M,Q8_0등)
문제는 2, 3 단계에서 각각 다른 유형의 에러가 터진다는 점입니다. 그래서 로그를 보고 어느 단계에서 죽는지부터 분리해야 합니다.
- 변환 스크립트에서 죽으면: 텐서명, shape, config 불일치 가능성이 큼
quantize에서 죽으면: dtype, 지원 스킴, 특정 텐서 예외 처리 문제 가능성이 큼
버전 불일치가 만드는 “가짜” 오류
llama.cpp는 GGUF 포맷과 텐서명 규칙이 빠르게 변합니다. 즉, 같은 모델이라도 llama.cpp 커밋 시점에 따라
- 요구하는 키 이름
- RoPE 스케일링 메타데이터
- GQA 관련 헤더 필드
등이 달라져 “텐서명 불일치”처럼 보이는 오류가 발생할 수 있습니다.
가장 먼저 할 일은 llama.cpp를 최신으로 맞추거나, 최소한 모델이 만들어진 시점과 비슷한 커밋으로 맞추는 것입니다.
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
git pull
make -j
증상 1: 텐서명 불일치로 변환이 중단될 때
대표적인 로그 패턴은 다음과 비슷합니다.
KeyError: ...형태로 특정 텐서를 못 찾음unexpected key(s) in state_dict또는missing key(s)- 특정 블록만 반복적으로 누락
이 문제는 대부분 체크포인트가 표준 HF 네이밍 규칙과 다르거나, 학습 프레임워크가 키에 접두사를 붙였기 때문입니다.
1) 가장 흔한 원인: 접두사(prefix) 문제
예를 들어 학습 시 DataParallel 또는 래퍼가 붙으면 키가 module.로 시작합니다.
- 기대:
model.layers.0.self_attn.q_proj.weight - 실제:
module.model.layers.0.self_attn.q_proj.weight
이 경우 변환 전에 prefix를 제거해 저장하면 끝납니다.
import torch
src = "pytorch_model.bin"
dst = "pytorch_model.stripped.bin"
sd = torch.load(src, map_location="cpu")
if "state_dict" in sd:
sd = sd["state_dict"]
new_sd = {}
for k, v in sd.items():
nk = k
if nk.startswith("module."):
nk = nk[len("module."):]
new_sd[nk] = v
torch.save(new_sd, dst)
print("saved:", dst, "keys:", len(new_sd))
2) 모델 아키텍처 차이: 레이어 명명 규칙이 다름
LLaMA 계열이라고 해도 구현체에 따라 다음이 달라질 수 있습니다.
self_attn.q_projvsattention.wqmlp.gate_projvsfeed_forward.w1layernorm위치 및 이름
이건 단순 prefix 제거로 해결이 안 되고, 키 리네이밍 규칙이 필요합니다. 가장 안전한 방식은 “현재 키 목록”과 “기대 키 목록”을 비교해 매핑 테이블을 만드는 것입니다.
키 목록 덤프해서 차이 확인
import torch
from collections import Counter
sd = torch.load("pytorch_model.stripped.bin", map_location="cpu")
keys = list(sd.keys())
print("num_keys:", len(keys))
print("sample:")
for k in keys[:30]:
print(" ", k)
# 상위 prefix 통계
prefixes = [".".join(k.split(".")[:3]) for k in keys]
print("top prefixes:")
for p, c in Counter(prefixes).most_common(20):
print(p, c)
이 결과를 바탕으로 변환 스크립트에서 요구하는 키와 맞추거나, 변환 전에 아예 state_dict를 재작성합니다.
예시: attention 네이밍을 LLaMA 스타일로 치환
아래 코드는 예시입니다. 실제로는 모델별로 매핑이 달라서, replace 규칙을 최소 단위로 쪼개 검증하는 게 좋습니다.
import torch
sd = torch.load("pytorch_model.stripped.bin", map_location="cpu")
rules = [
("attention.wq.", "self_attn.q_proj."),
("attention.wk.", "self_attn.k_proj."),
("attention.wv.", "self_attn.v_proj."),
("attention.wo.", "self_attn.o_proj."),
("feed_forward.w1.", "mlp.gate_proj."),
("feed_forward.w2.", "mlp.down_proj."),
("feed_forward.w3.", "mlp.up_proj."),
]
new_sd = {}
for k, v in sd.items():
nk = k
for a, b in rules:
nk = nk.replace(a, b)
new_sd[nk] = v
torch.save(new_sd, "pytorch_model.renamed.bin")
print("done")
3) config.json 불일치: 변환기가 메타를 못 맞춤
GGUF는 텐서뿐 아니라 모델 메타데이터도 중요합니다. 특히 아래 값이 실제 가중치와 불일치하면, 변환 중 또는 로딩 중 문제가 납니다.
num_hidden_layershidden_sizenum_attention_headsnum_key_value_heads(GQA)rope_theta, RoPE scaling 관련 필드
체크포인트만 있고 config.json이 부정확하면, 최소한 위 값은 가중치 shape에서 역추정해 맞춰야 합니다.
증상 2: 양자화 단계에서 실패할 때
GGUF는 만들어졌는데 quantize 단계에서 죽는 경우가 있습니다. 이때는 텐서명보다는 다음을 의심합니다.
- 특정 텐서가 양자화 대상이 아니어야 하는데 포함됨
- 지원하지 않는 dtype 또는 비정상 shape
- 너무 오래된
quantize바이너리 사용
1) 양자화 스킴과 모델 타입 매칭
모든 모델이 모든 양자화 스킴에서 잘 동작하는 건 아닙니다. 특히 MoE, 비표준 블록, 또는 특수 레이어가 들어간 모델은
Q4_K_M에서 실패하지만Q8_0에서는 성공- 혹은
Q5_K_M에서만 성공
같은 편차가 생깁니다.
실전에서는 아래 순서로 “안전하게” 좁혀가는 게 좋습니다.
- 먼저
Q8_0로 성공 여부 확인 - 성공하면
Q6_K또는Q5_K_M로 다운 - 마지막에
Q4_K_M같은 공격적인 스킴 시도
# 예시: fp16 GGUF를 Q8_0로
./quantize model.fp16.gguf model.q8_0.gguf q8_0
# 성공 후 더 줄이기
./quantize model.fp16.gguf model.q5_k_m.gguf q5_k_m
2) quantize가 특정 텐서에서 죽을 때: 예외 텐서 분리
일부 텐서는 양자화하면 안 되거나(예: 스케일/바이어스 계열), 특정 방식으로만 처리해야 합니다. 최신 llama.cpp는 이런 예외를 많이 내장했지만, 모델이 비표준이면 여전히 실패합니다.
이때 접근은 두 가지입니다.
llama.cpp최신화로 예외 규칙을 얻는다- 변환 단계에서 텐서를 “표준 이름”으로 맞춰 예외 규칙에 걸리게 한다
즉, 양자화 실패가 사실상 “텐서명 불일치의 변형”인 경우도 많습니다.
3) dtype 문제: bf16, fp32 혼재
PyTorch 체크포인트가 bf16과 fp32가 섞여 있거나, 일부 텐서가 fp32로 남아 있으면 변환 또는 양자화에서 예상치 못한 경로를 탈 수 있습니다.
변환 전, 전체를 fp16 또는 bf16으로 통일하는 것이 안전합니다.
import torch
sd = torch.load("pytorch_model.renamed.bin", map_location="cpu")
def to_fp16(t):
if not torch.is_floating_point(t):
return t
return t.to(dtype=torch.float16)
new_sd = {k: to_fp16(v) for k, v in sd.items()}
torch.save(new_sd, "pytorch_model.fp16.bin")
print("converted to fp16")
변환 성공률을 올리는 체크리스트
여기부터는 “한 번에 성공”을 위한 실전 체크리스트입니다.
1) 변환 전 점검
- 체크포인트가
state_dict단일 딕셔너리인지 확인 - 키에
module.같은 prefix가 붙었는지 확인 config.json이 실제 모델과 일치하는지 확인- float 텐서 dtype이 섞였는지 확인
2) 변환 중 점검
- 실패 로그에서 “없는 키”가 어떤 규칙으로 반복되는지 본다
- 특정 블록(예:
layers.0,layers.1)만 누락이면 인덱싱 규칙 차이를 의심 - attention/MLP 네이밍이 완전히 다르면 매핑 테이블이 필요
3) 양자화 단계 점검
- 먼저
Q8_0로 성공시키고, 그 다음 더 작은 스킴을 시도 - 실패 시
llama.cpp최신화 후 재시도 - 그래도 실패하면 텐서명이 표준 예외 규칙에 걸리는지(예: norm, embed, output) 확인
재현 가능한 디버깅 루틴(권장)
변환 실패는 “한 번에 다 고치려다” 더 꼬입니다. 아래처럼 단계별로 산출물을 남기면 원인 추적이 빨라집니다.
pytorch_model.bin원본 보관- prefix 제거 후
pytorch_model.stripped.bin - 키 매핑 후
pytorch_model.renamed.bin - dtype 통일 후
pytorch_model.fp16.bin GGUF(fp16)생성Q8_0양자화 성공- 목표 스킴으로 다운그레이드
이 루틴은 장애 대응 관점에서도 유용합니다. 원인 추적을 단계적으로 쪼개는 방식은 인프라 문제에서도 동일하게 통합니다. 예를 들어 장애 분석 흐름을 잡는 감각은 systemd 서비스가 계속 재시작될 때 원인 추적법 같은 글의 접근과도 유사합니다.
FAQ: 자주 묻는 함정
GGUF는 만들었는데 로딩 시 크래시가 납니다
대개 메타데이터 불일치(헤더 필드) 또는 토크나이저 파일 불일치입니다.
tokenizer.model또는tokenizer.json이 모델과 맞는지bos/eos토큰 ID가 맞는지rope_scaling관련 설정이 런타임과 맞는지
를 확인하세요.
같은 모델인데 어떤 사람은 되는데 나는 안 됩니다
대부분 아래 둘 중 하나입니다.
llama.cpp커밋 차이- 체크포인트가 미묘하게 다른 변형(예: merged, lora merged, vocab 변경)
버전을 고정하고, 체크포인트 해시를 비교해 동일 파일인지부터 확인하는 게 빠릅니다.
마무리
PyTorch→GGUF 변환 실패는 겉보기엔 복잡하지만, 실제로는
- 텐서명 매핑 문제
- 메타데이터(config) 불일치
- 양자화 스킴과 텐서 예외 처리 문제
세 축으로 정리됩니다. 핵심은 로그를 보고 어느 축인지 빠르게 분류한 뒤, 체크포인트를 “표준 형태”로 정리해 변환기가 기대하는 입력으로 맞추는 것입니다.
다음 액션으로는
Q8_0로 먼저 성공시키기- 키 덤프 후 prefix 제거 및 규칙 기반 리네이밍
- float dtype 통일
이 3가지만 적용해도 성공률이 크게 올라갑니다.