본문으로 건너뛰기

[vLLM] GGUF: llama.cpp 양자화 포맷 지원

들어가며

GGUF(GGML Unified Format)는 llama.cpp에서 사용하는 양자화 포맷이다. HuggingFace에서 GGUF 파일을 직접 다운로드하여 vLLM으로 서빙할 수 있으며, Q2_K부터 Q8_0, IQ4_XS까지 다양한 양자화 타입을 지원한다. 이번 글에서는 vLLM이 GGUF를 어떤 구조로 지원하는지 살펴본다.

공식 문서

vLLM 공식 문서: GGUF Quantization

핵심 구조/코드 분석

양자화 타입 분류

vLLM은 GGUF의 양자화 타입을 네 가지 카테고리로 분류한다:

STANDARD_QUANT_TYPES = {
    WeightType.Q4_0, WeightType.Q4_1, WeightType.Q5_0,
    WeightType.Q5_1, WeightType.Q8_0, WeightType.Q8_1,
}
KQUANT_TYPES = {
    WeightType.Q2_K, WeightType.Q3_K, WeightType.Q4_K,
    WeightType.Q5_K, WeightType.Q6_K,
}
IMATRIX_QUANT_TYPES = {
    WeightType.IQ1_M, WeightType.IQ1_S, WeightType.IQ2_XXS,
    WeightType.IQ2_XS, WeightType.IQ2_S, WeightType.IQ3_XXS,
    WeightType.IQ3_S, WeightType.IQ4_XS, WeightType.IQ4_NL,
}

이 분류에 따라 어떤 커널을 사용할지가 결정된다:

  • MMVQ (Matrix-Matrix Vector Quantized): 소규모 배치(batch <= 2~16)에서 사용. 표준+K-양자화+I-Matrix 모두 지원.
  • MMQ (Matrix-Matrix Quantized): 대규모 배치에서 사용. 표준+K-양자화만 지원.
  • Dequant 폴백: MMQ 미지원 타입은 역양자화 후 FP16 GEMM으로 처리.

커널 선택 전략

_fused_mul_mat_gguf에서 입력 크기에 따라 최적의 커널을 선택한다:

def _fused_mul_mat_gguf(x, qweight, qweight_type):
    if qweight_type in IMATRIX_QUANT_TYPES:
        mmvq_safe = 8 if qweight.shape[0] > 5120 else 16
    else:
        mmvq_safe = 2 if qweight.shape[0] > 5120 else 6

    if qweight_type in UNQUANTIZED_TYPES:
        return x @ qweight.T
    if x.shape[0] <= mmvq_safe and qweight_type in MMVQ_QUANT_TYPES:
        y = ops.ggml_mul_mat_vec_a8(qweight, x, qweight_type, qweight.shape[0])
    elif qweight_type in MMQ_QUANT_TYPES:
        y = ops.ggml_mul_mat_a8(qweight, x, qweight_type, qweight.shape[0])
    elif qweight_type in DEQUANT_TYPES:
        block_size, type_size = gguf.GGML_QUANT_SIZES[qweight_type]
        shape = (qweight.shape[0], qweight.shape[1] // type_size * block_size)
        weight = ops.ggml_dequantize(qweight, qweight_type, *shape, x.dtype)
        y = x @ weight.T
    return y

가중치 크기(5120 기준)에 따라 MMVQ 임계값을 동적으로 조정하는 것이 눈에 띈다. 큰 행렬일수록 MMVQ 대신 MMQ가 유리하기 때문이다.

MoE 전용 GGUF 커널

GGUF MoE는 독자적인 _fused_moe_gguf 커스텀 연산을 사용한다:

def _fused_moe_gguf(x, w1, w2, topk_weights, topk_ids,
                     qweight_type, qweight_type2, activation):
    if (qweight_type2 in MMQ_QUANT_TYPES
        and qweight_type in MMQ_QUANT_TYPES
        and x.shape[0] > 64):
        # 대규모 배치: ggml_moe_a8 사용
        sorted_token_ids, expert_ids, num_tokens_post_padded = \
            moe_align_block_size(topk_ids, BLOCK_SIZE, E)
        out = ops.ggml_moe_a8(x, w1, sorted_token_ids, ...)
    elif qweight_type2 in MMVQ_QUANT_TYPES:
        # 소규모 배치: ggml_moe_a8_vec 사용
        out = ops.ggml_moe_a8_vec(x, w1, topk_ids, ...)

배치 크기 64를 기준으로 MMQ/MMVQ를 선택하며, 둘 다 사용할 수 없는 경우 전문가별 순차 처리로 폴백한다.

GGUFUninitializedParameter

GGUF 가중치는 양자화 타입에 따라 shape이 달라지므로, UninitializedParameter를 상속한 특수 파라미터를 사용한다:

class GGUFUninitializedParameter(UninitializedParameter):
    cls_to_become = Parameter
    data_container: list[torch.Tensor]

가중치 로딩 시점에서 실제 데이터를 data_container에 수집한 뒤, 패딩된 텐서로 병합하여 CUDA Graph 호환성을 확보한다.

왜 이 설계인가

  1. llama.cpp 생태계 활용: GGUF는 커뮤니티에서 가장 널리 사용되는 양자화 포맷이다. TheBloke 등의 제공자가 대부분의 모델을 GGUF로 제공하므로, 이를 지원하면 사용 가능한 모델 풀이 크게 넓어진다.

  2. 3단계 커널 폴백: MMVQ -> MMQ -> Dequant로 이어지는 폴백 체인은 모든 양자화 타입에서 동작을 보장하면서도, 가능한 최적의 커널을 사용하도록 한다.

  3. Blackwell 주의: SM100(Blackwell)에서는 bf16 정밀도 이슈가 있어 fp16만 지원하도록 경고를 출력한다. GGUF 역양자화 커널이 내부적으로 fp16을 사용하기 때문이다.

참고 자료

댓글

관련 포스트

vLLM 의 다른글