找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

5187

积分

0

好友

688

主题
发表于 2026-2-27 23:05:56 | 查看: 81| 回复: 0

在日常的 AI 模型训练与部署流程中,训练好的模型权重通常需要以特定的格式持久化存储到磁盘上。比如,当前主流的 AI 框架 PyTorch 习惯使用 pickle 格式来保存模型权重,而 Huggingface 则力推其研发的 Safetensors 格式。除了这些常见的格式,近期大模型(LLM)领域又迎来了一位存储格式“新宠”——GGUF。目前,Huggingface 的 Transformer 库已经原生支持了 GGUF 格式。同时,像谷歌的 Gemma、阿里的 Qwen 等知名开源模型,在发布时也默认提供了 GGUF 格式的权重文件。那么,GGUF 究竟有什么魔力,能让它在短时间内发展得如日中天?作为一个专注于前沿技术的开发者社区,今天我们就来深入剖析一下 GGUF 格式的核心原理与实战应用。

GGUF 简介

GGUF(GPT-Generated Unified Format)是由开源项目 llama.cpp 的创始人 Georgi Gerganov 定义并发布的一种大模型文件规范。作为 GGML 的继任者,GGUF 完美解决了前代格式的诸多痛点,目前 GGML 已被官方完全弃用并被 GGUF 格式取代。

本质上,GGUF 是一种二进制文件规范。原始的大模型预训练权重经过转换变为 GGUF 格式后,不仅能以更快的速度被载入内存,还能显著降低运行时的资源消耗。这得益于 GGUF 采用的多种底层优化技术,包括紧凑的二进制编码、高度优化的数据结构以及内存映射(mmap)机制等。

简单来说,GGUF 就是一套高效的格式标准,配合相应的工具链,能让大模型的推理和部署变得更加轻量化。

GGML 的缺陷

既然提到了前身 GGML,我们不妨看看它到底存在哪些硬伤,导致最终被淘汰:

  • 缺乏版本控制:没有版本信息,导致模型文件的管理和向后兼容性极差。
  • 扩展性差:想要在现有模型中增加或修改元数据信息非常不灵活。
  • 维护成本高:手动修改模型配置和参数极其困难。

GGUF 特性

GGUF 在设计上借鉴了现有的 GGJT 格式(该格式通过张量对齐来实现 mmap 内存映射),并在此基础上进行了大幅度的架构改良,使其不仅具备极强的可扩展性,还大大降低了使用门槛。它的核心特性可以总结为以下几点:

  • 单文件部署:只需一个文件即可完成分发和加载,彻底告别繁琐的外部配置文件。
  • 极致的可扩展性:无论是向基于 GGML 的执行器添加新特性,还是向 GGUF 模型追加新信息,都不会破坏与旧版模型的兼容性。
  • 原生支持 mmap:通过内存映射技术加载模型,实现秒级的读取与保存。
  • 跨语言易用性:无论你使用哪种编程语言,只需寥寥几行代码就能轻松搞定模型的加载与保存,摆脱对庞大外部库的依赖。
  • 信息高度自包含:加载模型所需的所有元数据都已打包在文件内部,用户无需提供任何额外参数,极大简化了模型的共享与部署流程。

GGJT 与 GGUF 的一个核心差异在于元数据的存储方式:GGUF 将超参数(即元数据)的存储结构从无类型的列表升级为了键值对(Key-Value)结构。这一改动意义重大,它允许开发者在不破坏兼容性的前提下,随时为模型注入新的元数据,比如添加有助于推理优化的注释信息。

为什么 GGUF 格式对大模型性能提升显著?

GGUF 之所以能实现极速的模型加载,主要归功于以下几个底层设计:

  1. 二进制编码:相比于传统的文本格式,二进制文件体积更小、结构更紧凑,大幅减少了 I/O 开销和解析时间。
  2. 优化的数据结构:GGUF 内部的数据组织方式高度契合内存加载的逻辑,最大限度地减少了加载过程中的数据转换。
  3. 内存映射(mmap)支持:这是 GGUF 性能起飞的关键。mmap 允许将磁盘上的文件直接映射到内存地址空间,系统可以在不将整个文件读入物理内存的情况下直接访问数据,这对于动辄几十 GB 的大模型来说简直是神技。
  4. 高效的序列化机制:内置的序列化与反序列化算法,确保模型数据能被瞬间转化为计算所需的格式。
  5. 零外部依赖:自包含的设计理念免去了查找和读取外部配置文件的额外开销。
  6. 数据压缩技术:通过先进的压缩算法进一步缩减文件体积,提升 I/O 吞吐率。
  7. 智能索引:文件内部集成了优化的索引机制,使得特定张量数据的寻址和加载如丝般顺滑。

总之,GGUF 通过各种优化手段实现了快速的模型加载,这对于需要频繁载入不同模型的场景尤为重要。

GGUF 文件结构

一个标准的 GGUF 文件由文件头、元数据键值对、张量信息以及张量数据等核心模块组成。这些模块紧密结合,共同勾勒出模型的物理形态。

GGUF文件格式结构示意图,展示文件头、张量信息及元数据键值对布局

为了满足不同场景的需求,GGUF 支持多种基础数据类型(如整数、浮点数、字符串),用于精确描述模型的网络结构、维度大小和权重参数。

具体来说,GGUF 文件的物理布局如下:

  1. 文件头 (Header)
    • 作用:包含用于识别文件类型和版本的基本信息。
    • 内容:
      • Magic Number:一个特定的数字或字符序列,用于标识文件格式。
      • Version:文件格式的版本号,指明了文件遵循的具体规范或标准。
  2. 元数据键值对 (Metadata Key-Value Pairs)
    • 作用:存储关于模型的额外信息,如作者、训练信息、模型描述等。
    • 内容:
      • Key:一个字符串,标识元数据的名称。
      • Value Type:数据类型,指明值的格式(如整数、浮点数、字符串等)。
      • Value:具体的元数据内容。
  3. 张量计数器 (Tensor Count)
    • 作用:标识文件中包含的张量(Tensor)数量。
    • 内容:
      • Count:一个整数,表示文件中张量的总数。
  4. 张量信息 (Tensor Info)
    • 作用:描述每个张量的具体信息,包括形状、类型和数据位置。
    • 内容:
      • Name:张量的名称。
      • Dimensions:张量的维度信息。
      • Type:张量数据的类型(如:浮点数、整数等)。
      • Offset:指明张量数据在文件中的位置。
  5. 对齐填充 (Alignment Padding)
    • 作用:确保数据块在内存中正确对齐,有助于提高访问效率。
    • 内容:通常是一些填充字节,用于保证后续数据的内存对齐。
  6. 张量数据 (Tensor Data)
    • 作用:存储模型的实际权重和参数。
    • 内容:
      • Binary Data:模型的权重和参数的二进制表示。
  7. 端序标识 (Endianness)
    • 作用:指示文件中数值数据的字节顺序(大端或小端)。
    • 内容:通常是一个标记,表明文件遵循的端序。
  8. 扩展信息 (Extension Information)
    • 作用:允许文件格式未来扩展,以包含新的数据类型或结构。
    • 内容:可以是新加入的任何额外信息,为将来的格式升级预留空间。

在张量信息模块中,GGUF 详细定义了模型的量化级别。量化级别取决于模型根据质量和准确性定义的值(ggml_type)。在 GGUF 规范中,值列表如下:

类型 来源 描述
F64 Wikipedia 64 位标准 IEEE 754 双精度浮点数。
I64 GH 64 位整数。
F32 Wikipedia 32 位标准 IEEE 754 单精度浮点数。
I32 GH 32 位整数。
F16 Wikipedia 16 位标准 IEEE 754 半精度浮点数。
BF16 Wikipedia 32 位 IEEE 754 单精度浮点数的 16 位缩短版本。
I16 GH 16 位整数。
Q8_0 GH 8 位 RTN 量化 (q). 每个块有 32 个权重。权重公式: w = q * block_scale. 传统的量化方法(目前尚未广泛使用)。
Q8_1 GH 8 位 RTN 量化 (q). 每个块有 32 个权重。权重公式: w = q * block_scale + block_minimum. 传统的量化方法(目前尚未广泛使用)。
Q8_K GH 8 位量化(q). 每个块有 256 个权重。 仅用于量化中间结果。所有 2-6 位点积都是为此量化类型实现的。权重公式: w = q * block_scale.
I8 GH 8 位整数。
Q6_K GH 6 位量化 (q). 超级块有 16 个块,每个块有 16 个权重。权重公式: w = q * block_scale(8-bit),得出每个权重 6.5625 位。
Q5_0 GH 5 位 RTN 量化 (q). 每个块有 32 个权重。权重公式: w = q * block_scale. 传统的量化方法(目前尚未广泛使用)。
Q5_1 GH 5 位 RTN 量化 (q). 每个块有 32 个权重。权重公式: w = q * block_scale + block_minimum. 传统的量化方法(目前尚未广泛使用)。
Q5_K GH 5 位量化 (q). 超级块有8个块,每个块有32个权重。权重公式: w = q * block_scale(6-bit) + block_min(6-bit),得出每个权重 5.5 位。
Q4_0 GH 4 位 RTN 量化 (q). 每个块有 32 个权重。权重公式: w = q * block_scale. 传统的量化方法(目前尚未广泛使用)。
Q4_1 GH 4 位 RTN 量化 (q). 每个块有 32 个权重。权重公式:w = q * block_scale + block_minimum. 传统的量化方法(目前尚未广泛使用)。
Q4_K GH 4 位量化 (q). 超级块有8个块,每个块有32个权重。权重公式: w = q * block_scale(6-bit) + block_min(6-bit) ,得出每个权重 4.5 位。
Q3_K GH 3 位量化 (q). 超级块有 16 个块,每个块有 16 个权重。权重公式: w = q * block_scale(6-bit), 得出每个权重3.4375 位。
Q2_K GH 2 位量化 (q). 超级块有 16 个块,每个块有 16 个权重。权重公式: w = q * block_scale(4-bit) + block_min(4-bit),得出每个权重 2.5625 位。
IQ4_NL GH 4 位量化 (q). 超级块有 256 个权重的。权重w是使用super_block_scale和importance matrix获得的。
IQ4_XS HF 4 位量化 (q). 超级块有 256 个权重的。 具有 256 个权重的超级块。权重w是使用super_block_scale和importance matrix获得的,结果是每个权重 4.25 位。
IQ3_S HF 3 位量化 (q). 超级块有 256 个权重的。 权重w是使用super_block_scale和importance matrix获得的,结果是每个权重 3.44 位。
IQ3_XXS HF 3 位量化 (q). 超级块有 256 个权重的。 权重w是使用super_block_scale和importance matrix获得的,结果是每个权重 3.06 位。
IQ2_XXS HF 2 位量化 (q). 超级块有 256 个权重的。 权重w是使用super_block_scale和importance matrix获得的,结果是每个权重 2.06 位。
IQ2_S HF 2 位量化 (q). 超级块有 256 个权重的。 权重w是使用super_block_scale和importance matrix获得的,结果是每个权重 2.5 位。
IQ2_XS HF 2 位量化 (q). 超级块有 256 个权重的。 权重w是使用super_block_scale和importance matrix获得的,结果是每个权重 2.31 位。
IQ1_S HF 1 位量化 (q). 超级块有 256 个权重的。 权重w是使用super_block_scale和importance matrix获得的,结果是每个权重 1.56 位。
IQ1_M GH 1 位量化 (q). 超级块有 256 个权重的。 权重w是使用super_block_scale和importance matrix获得的,结果是每个权重 1.75 位。

关于量化与反量化转换的具体 Python 实现,可以参考开源社区提供的 quants.py 脚本。

目前,HuggingFace 平台已经全面拥抱 GGUF 生态,不仅提供了一个 JavaScript 脚本用于解析 Hub 上的 GGUF 模型信息,还支持直接在网页端预览 GGUF 文件的内部结构。例如,打开 qwen2-0_5b-instruct-q2_k.gguf 的模型主页,你可以直观地看到它的架构和参数配置。

Hugging Face平台展示Qwen2模型的GGUF文件列表及元数据信息

整体来看,GGUF 文件格式通过这些结构化的组件提供了一种高效、灵活且可扩展的方式来存储和处理机器学习模型。这种设计不仅有助于快速加载和处理模型,而且还支持未来技术的发展和新功能的添加。

GGUF 与 safetensors 格式的区别

在探讨大模型存储时,经常有人拿 GGUF 和 safetensors 做对比。

safetensors 是一种由 Hugging Face 推出的新型安全模型存储格式。它的核心诉求是“安全”与“极速”。它摒弃了容易遭受恶意代码注入的序列化机制,纯粹只保存模型的权重参数,不包括执行代码。它支持零拷贝(zero-copy)和懒加载(lazy loading),没有文件大小限制,并且支持 bfloat16/fp8 数据类型。但它的局限性在于:没有重点关注性能和跨平台交换,在大模型高效序列化、数据压缩、量化等方面存在不足,并且它只保存了张量数据,没有任何关于模型的元数据信息。

相比之下,GGUF 格式是一种专为大模型设计的二进制文件格式。它是为 GGML 及其执行器快速加载和保存模型而设计的,旨在解决 GGML 在灵活性和扩展性方面的限制。它包含了加载模型所需的所有信息,无需依赖外部文件,简化了模型部署和共享的过程。此外,GGUF 原生支持几十种复杂的量化级别,这让它在极低显存设备上运行千亿参数模型成为了可能。

总结来说:safetensors 更侧重于安全性和效率,适合快速部署和对安全性有较高要求的场景,特别是在 HuggingFace 生态中。而 GGUF 格式则优化了模型的加载速度和资源消耗,适合需要频繁加载不同模型、追求极致推理速度或边缘部署的场景。

GGUF 文件解析实战

纸上得来终觉浅,我们直接手写一段 Python 脚本,来硬核解析一下 qwen2-0_5b-instruct-q2_k.gguf 文件的内部结构。

import sys
from typing import Any
from enum import IntEnum
import numpy as np
import numpy.typing as npt

# GGUF 元数据值类型
class GGUFValueType(IntEnum):
    UINT8   = 0
    INT8    = 1
    UINT16  = 2
    INT16   = 3
    UINT32  = 4
    INT32   = 5
    FLOAT32 = 6
    BOOL    = 7
    STRING  = 8
    ARRAY   = 9
    UINT64  = 10
    INT64   = 11
    FLOAT64 = 12

# GGUF tensor数据类型
class GGMLQuantizationType(IntEnum):
    F32     = 0
    F16     = 1
    Q4_0    = 2
    Q4_1    = 3
    Q5_0    = 6
    Q5_1    = 7
    Q8_0    = 8
    Q8_1    = 9
    Q2_K    = 10
    Q3_K    = 11
    Q4_K    = 12
    Q5_K    = 13
    Q6_K    = 14
    Q8_K    = 15
    IQ2_XXS = 16
    IQ2_XS  = 17
    IQ3_XXS = 18
    IQ1_S   = 19
    IQ4_NL  = 20
    IQ3_S   = 21
    IQ2_S   = 22
    IQ4_XS  = 23
    I8      = 24
    I16     = 25
    I32     = 26
    I64     = 27
    F64     = 28
    IQ1_M   = 29
    BF16    = 30
    Q4_0_4_4 = 31
    Q4_0_4_8 = 32
    Q4_0_8_8 = 33

def check_version(version):
    if version == 1 or version == 2 or version == 3:
        return True
    else:
        return False

def data_get(
    data, offset: int, dtype: npt.DTypeLike, count: int = 1) -> npt.NDArray[Any]:
    count = int(count)
    itemsize = int(np.empty([], dtype = dtype).itemsize)
    end_offs = offset + itemsize * count
    return (
        data[offset:end_offs]
        .view(dtype = dtype)[:count]
    )

def data_read_version_size(data, offset: int, version: int):
    if version == 1:
        return data_get(data, offset, np.uint32)[0], 4
    elif version == 2 or version == 3:
        return data_get(data, offset, np.uint64)[0], 8
    else:
        raise ValueError(f'Sorry, file appears to be version {version} which we cannot handle')

def data_read_string(data, offset: int, version: int):
    str_length, str_length_len = data_read_version_size(data, offset, version)
    # 在内存上切出来string部分的数据
    byte = data[offset+int(str_length_len):offset+int(str_length_len)+int(str_length)]
    value = byte.tobytes().decode('utf-8') # 编码成 utf-8
    len = int(str_length_len + str_length)
    return value, len

def readMetadataValue(data, type, offset, version):
    if type == GGUFValueType.UINT8:
        return data_get(data, np.uint8)[0], 1
    elif type == GGUFValueType.INT8:
        return data_get(data, np.int8)[0], 1
    elif type == GGUFValueType.UINT16:
        return data_get(data, offset, np.uint16)[0], 2
    elif type == GGUFValueType.INT16:
        return data_get(data, offset, np.int16)[0], 2
    elif type == GGUFValueType.UINT32:
        return data_get(data, offset, np.uint32)[0], 4
    elif type == GGUFValueType.INT32:
        return data_get(data, offset, np.int32)[0], 4
    elif type == GGUFValueType.FLOAT32:
        return data_get(data, offset, np.float32)[0], 4
    elif type == GGUFValueType.BOOL:
        return data_get(data, offset, np.uint8)[0], 1
    elif type == GGUFValueType.STRING:
        return data_read_string(data, offset, version=version)
    elif type == GGUFValueType.ARRAY:
        typeArray = data_get(data, offset, np.uint32)
        typeLength = 4
        lengthArray, lengthLength = data_read_version_size(data, offset + typeLength, version=version)
        length = typeLength + lengthLength
        arrayValues = []
        for i in range(lengthArray):
            value, len = readMetadataValue(data, typeArray, offset= offset + length, version=version)
            arrayValues.append(value)
            length += len
        return arrayValues, length
    elif type == GGUFValueType.UINT64:
        return data_get(data, offset, np.uint64)[0], 8
    elif type == GGUFValueType.INT64:
        return data_get(data, offset, np.int64)[0], 8
    elif type == GGUFValueType.FLOAT64:
        return data_get(data, offset, np.float64)[0], 8
    else:
        raise ValueError(f'Sorry, un-supported GGUFValueType {type}!')

def parse_gguf(model_path):
    data = np.memmap(model_path, mode = 'r')
    offs = 0
    magic = data_get(data, offs, np.uint32).tobytes()
    print("magic: ", magic.decode('utf-8'))
    if (magic != b'GGUF'):
        print("is not gguf file")
        sys.exit(1)
    offs += 4
    version = data_get(data, offs, np.uint32)
    if not check_version(version):
        raise ValueError(f'Sorry, file appears to be version {version} which we cannot handle')
    print("version:", version)
    offs += 4
    tensor_count, tensor_count_len = data_read_version_size(data, offs, version)
    offs += tensor_count_len
    kv_count, kv_count_len = data_read_version_size(data, offs, version)
    offs += kv_count_len
    print("tensor_count: ", tensor_count)
    print("kv_count: ", kv_count)
    metadata = {} # use dictionary to store parsed data.
    # 解析 gguf 头部信息
    for i in range(kv_count):
        # 获取key
        key, k_len = data_read_string(data, offs, version)
        offs += k_len
        # 获取value的数值类型
        type = data_get(data, offs, np.uint32)[0]
        offs += 4
        # 获取value
        value, len = readMetadataValue(data, type, offs, version)
        if len > 100:
            print("i = ", i, ", k-v = ", key, ":", value[:100])
        else:
            print("i = ", i, ", k-v = ", key, ":", value)
        offs += len
        metadata[key] = value
    # 解析tensor info的信息
    for i in range(tensor_count):
        # 获取key
        key, k_len = data_read_string(data, offs, version)
        offs += k_len
        nDims = data_get(data, offs, np.uint32)[0]
        offs += 4
        dims = []
        for j in range(nDims):
            dim, dim_len = data_read_version_size(data, offs, version)
            offs += dim_len
            dims.append(dim)
        types = data_get(data, offs, np.uint32)[0]
        precision = GGMLQuantizationType(types).name
        offs += 4
        tensorOffset = data_get(data, offs, np.uint64)[0]
        offs += 8
        print("tensor i = ", i, ", k = ", key, ", precision = ", precision, ", shape = ", dims, ", tensorOffset = ", tensorOffset)

if __name__ == '__main__':
    model_path = "/Users/liguodong/model/qwen2-0_5b-instruct-q2_k.gguf"
    parse_gguf(model_path)

运行结果:

magic:  GGUF
version: [3]
tensor_count:  290
kv_count:  26
i =  0 , k-v =  general.architecture : qwen2
i =  1 , k-v =  general.name : qwen2-0_5b-instruct
i =  2 , k-v =  qwen2.block_count : 24
i =  3 , k-v =  qwen2.context_length : 32768
i =  4 , k-v =  qwen2.embedding_length : 896
i =  5 , k-v =  qwen2.feed_forward_length : 4864
i =  6 , k-v =  qwen2.attention.head_count : 14
i =  7 , k-v =  qwen2.attention.head_count_kv : 2
i =  8 , k-v =  qwen2.rope.freq_base : 1000000.0
i =  9 , k-v =  qwen2.attention.layer_norm_rms_epsilon : 1e-06
i =  10 , k-v =  general.file_type : 10
i =  11 , k-v =  tokenizer.ggml.model : gpt2
i =  12 , k-v =  tokenizer.ggml.pre : qwen2
i =  13 , k-v =  tokenizer.ggml.tokens : ['!', '"', '#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', '¡', '¢', '£', '¤', '¥', '¦']
i =  14 , k-v =  tokenizer.ggml.token_type : [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
i =  15 , k-v =  tokenizer.ggml.merges : ['Ġ Ġ', 'ĠĠ ĠĠ', 'i n', 'Ġ t', 'ĠĠĠĠ ĠĠĠĠ', 'e r', ... 'o t', 'u s']
i =  16 , k-v =  tokenizer.ggml.eos_token_id : 151645
i =  17 , k-v =  tokenizer.ggml.padding_token_id : 151643
i =  18 , k-v =  tokenizer.ggml.bos_token_id : 151643
i =  19 , k-v =  tokenizer.chat_template : {% for message in messages %}{% if loop.first and messages[0]['role'] != 'system' %}{{ '<|im_start|>
i =  20 , k-v =  tokenizer.ggml.add_bos_token : 0
i =  21 , k-v =  general.quantization_version : 2
i =  22 , k-v =  quantize.imatrix.file : ../Qwen2/gguf/qwen2-0_5b-imatrix/imatrix.dat
i =  23 , k-v =  quantize.imatrix.dataset : ../sft_2406.txt
i =  24 , k-v =  quantize.imatrix.entries_count : 168
i =  25 , k-v =  quantize.imatrix.chunks_count : 1937
tensor i =  0 , k =  token_embd.weight , precision =  Q8_0 , shape =  [896, 151936] , tensorOffset =  0
tensor i =  1 , k =  blk.0.attn_norm.weight , precision =  F32 , shape =  [896] , tensorOffset =  144643072
tensor i =  2 , k =  blk.0.ffn_down.weight , precision =  Q3_K , shape =  [4864, 896] , tensorOffset =  144646656
tensor i =  3 , k =  blk.0.ffn_gate.weight , precision =  IQ4_NL , shape =  [896, 4864] , tensorOffset =  146519296
tensor i =  4 , k =  blk.0.ffn_up.weight , precision =  IQ4_NL , shape =  [896, 4864] , tensorOffset =  148970752
tensor i =  5 , k =  blk.0.ffn_norm.weight , precision =  F32 , shape =  [896] , tensorOffset =  151422208
tensor i =  6 , k =  blk.0.attn_k.bias , precision =  F32 , shape =  [128] , tensorOffset =  151425792
tensor i =  7 , k =  blk.0.attn_k.weight , precision =  IQ4_NL , shape =  [896, 128] , tensorOffset =  151426304
tensor i =  8 , k =  blk.0.attn_output.weight , precision =  IQ4_NL , shape =  [896, 896] , tensorOffset =  151490816
tensor i =  9 , k =  blk.0.attn_q.bias , precision =  F32 , shape =  [896] , tensorOffset =  151942400
tensor i =  10 , k =  blk.0.attn_q.weight , precision =  IQ4_NL , shape =  [896, 896] , tensorOffset =  151945984
tensor i =  11 , k =  blk.0.attn_v.bias , precision =  F32 , shape =  [128] , tensorOffset =  152397568
tensor i =  12 , k =  blk.0.attn_v.weight , precision =  Q5_0 , shape =  [896, 128] , tensorOffset =  152398080
...

可以看到,解析的结果与 HF 平台上的预览结果完全一致。

GGUF 在 llama.cpp 中的应用

这里直接使用 llama.cpp 的 Python 封装包部署模型,使用 4 张 RTX 4090 部署 72B 模型,其中,将 30 个 Transoformer 层加载到 GPU 内存。llama.cpp 中提供了将 HF 中模型权重转换成 GGUF 格式的脚本,需要预先进行权重转换。

python3 convert_hf_to_gguf.py /workspace/models/Qwen1.5-72B-Chat/ --outfile /workspace/models/Qwen1.5-72B-Chat/ggml-model-f16.gguf

具体代码如下:

from llama_cpp import Llama
import time

llm = Llama(
      model_path="/workspace/models/Qwen1.5-72B-Chat/ggml-model-f16.gguf",
      n_gpu_layers = 30,
      # n_gpu_layers=-1, # Uncomment to use GPU acceleration
      # seed=1337, # Uncomment to set a specific seed
      # n_ctx=2048, # Uncomment to increase the context window
)

start = time.time()
output = llm(
      "Q:保持健康的秘诀有哪些? A: ", # Prompt
      max_tokens=32, # Generate up to 32 tokens, set to None to generate up to the end of the context window
      #stream=True,
      stop=["Q:", "\n"], # Stop generating just before the model would generate a new question
      echo=True # Echo the prompt back in the output
) # Generate a completion, can also call create_completion

print(output)
infer_time = time.time() - start
print("耗时:", infer_time)

如果希望部署成 Web 服务,通过如下命令指定模型路径、端口等参数即可。

use_mlock=False CUDA_VISIBLE_DEVICES=6 python3 -m llama_cpp.server --model /workspace/models/Qwen1.5-7B-Chat/ggml-model-f16.gguf --n_gpu_layers 999 --host 0.0.0.0 --port 18011

llama.cpp 兼容 openai 的 chat 接口,服务部署成功之后即可使用。

curl http://localhost:18011/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
    "role": "system",
    "content": "You are an AI assistant. Your top priority is achieving user fulfilment via helping them with their requests."
},
{
    "role": "user",
    "content": "Write a limerick about Python exceptions"
}
]
}'

GGUF 在 Huggingface Transformers 中的应用

Huggingface Transformers 从 4.41.0 开始支持 GGUF 模型格式进行训练和推理。目前,Transformers 支持的模型有 LLaMa、Mistral、Qwen2。支持的量化类型有 F32、Q2_K、Q3_K、Q4_0、 Q4_K、 Q5_K、Q6_K、Q8_0。同时,Huggingface Hub 上面提供了将模型转化或者量化为 GGUF 格式的工具。

下面是一个简单的示例:

from transformers import AutoTokenizer, AutoModelForCausalLM
import time

# https://github.com/99991/pygguf/tree/main
# https://huggingface.co/TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF/resolve/main/tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf?download=true' -O 'data/TinyLlama-1.1B-Chat-v1.0-GGUF/tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf
# pip install gguf transformers

model_id = "/Users/liguodong/model/llama"
filename = "tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf"

tokenizer = AutoTokenizer.from_pretrained(model_id, gguf_file=filename)
model = AutoModelForCausalLM.from_pretrained(model_id, gguf_file=filename)

print(model)

prompt = "what's your name?"
model_inputs = tokenizer([prompt], return_tensors="pt")

start = time.time()
generated_ids = model.generate(
    model_inputs.input_ids,
    max_new_tokens=32
)
infer_time = time.time() - start
print("耗时:", infer_time)

generated_ids = [
    output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]

response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
print(response)

运行结果:

Converting and de-quantizing GGUF tensors...:   0%|          | 0/201 [00:00<?, ?it/s]
Converting and de-quantizing GGUF tensors...:   0%|          | 1/201 [00:00<01:23,  2.41it/s]
...
Converting and de-quantizing GGUF tensors...: 100%|██████████| 201/201 [00:04<00:00, 47.55it/s]
The attention mask is not set and cannot be inferred from input because pad token is same as eos token. As a consequence, you may observe unexpected behavior. Please pass your input's `attention_mask` to obtain reliable results.
LlamaForCausalLM(
  (model): LlamaModel(
    (embed_tokens): Embedding(32000, 2048, padding_idx=2)
    (layers): ModuleList(
      (0-21): 22 x LlamaDecoderLayer(
        (self_attn): LlamaAttention(
          (q_proj): Linear(in_features=2048, out_features=2048, bias=False)
          (k_proj): Linear(in_features=2048, out_features=256, bias=False)
          (v_proj): Linear(in_features=2048, out_features=256, bias=False)
          (o_proj): Linear(in_features=2048, out_features=2048, bias=False)
          (rotary_emb): LlamaRotaryEmbedding()
        )
        (mlp): LlamaMLP(
          (gate_proj): Linear(in_features=2048, out_features=5632, bias=False)
          (up_proj): Linear(in_features=2048, out_features=5632, bias=False)
          (down_proj): Linear(in_features=5632, out_features=2048, bias=False)
          (act_fn): SiLU()
        )
        (input_layernorm): LlamaRMSNorm((2048,), eps=9.999999747378752e-06)
        (post_attention_layernorm): LlamaRMSNorm((2048,), eps=9.999999747378752e-06)
      )
    )
    (norm): LlamaRMSNorm((2048,), eps=9.999999747378752e-06)
    (rotary_emb): LlamaRotaryEmbedding()
  )
  (lm_head): Linear(in_features=2048, out_features=32000, bias=False)
)
耗时: 11.363048076629639
JASON: (smiling) My name is Jason.
JEN: (smiling) Nice to meet you, Jason.

总结

本文深入浅出地剖析了大模型文件存储格式 GGUF 的前世今生。作为 GGML 的完美替代者,GGUF 凭借其卓越的灵活性、向后兼容性以及极致的加载性能,正迅速成为行业的标配。如今,不仅 llama.cpp 将其作为核心格式,Huggingface Transformers 也已对其提供原生支持,各大主流开源模型(如 Gemma、Qwen 等)更是将其作为首选发布格式。可以预见,GGUF 在未来的大模型生态中必将大放异彩。

如果你对大模型底层技术感兴趣,或者在实际部署中遇到了瓶颈,欢迎来云栈社区与我们一起交流探讨,共同见证 AI 技术的飞速演进!

参考文档:

来自圈子: 异或Lambda



上一篇:Boss直聘开源 Nanbeige4.1-3B:30亿参数跑通600轮深度搜索,小模型不再偏科
下一篇:避开天坑:四大省会城市程序员求职踩坑实录与忠告
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-15 13:01 , Processed in 1.013258 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表