专栏名称: 吃果冻不吐果冻皮
专注于AI工程化(LLM、MLOps、LLMOps、RAG、Agent)落地。
目录
相关文章推荐
中国证券报  ·  新纪录!90.2亿人次! ·  11 小时前  
卓奇365食品网  ·  昆明发布春季校园食品安全风险提示 ·  20 小时前  
卓奇365食品网  ·  昆明发布春季校园食品安全风险提示 ·  20 小时前  
中国证券报  ·  分歧出现!700亿资金有变 ·  3 天前  
上海证券报  ·  如何破招商内卷?临港出招 ·  3 天前  
中国证券报  ·  今天,A股两大主线走强 ·  3 天前  
51好读  ›  专栏  ›  吃果冻不吐果冻皮

一文搞懂大模型文件存储格式新宠GGUF

吃果冻不吐果冻皮  · 公众号  ·  · 2024-10-08 11:45

正文

【点击】 加入大模型技术交流群

在日常AI模型训练过程中,训练好的模型权重通常需要以一种格式存储在磁盘中。比如:目前最流行的AI框架 PyTorch 使用 pickle 格式存储模型权重文件,还有 Huggingface 提出的 Safetensors 格式。之前在 一文详解模型权重存储新格式 Safetensors 中讲述Safetensors,本文大介绍大模型文件存储格式新宠GGUF,目前 Huggingface Transformers 已经支持了GGUF格式,同时,像谷歌的Gemma、阿里的Qwen等模型默认已经提供了GGUF格式文件,可见其发展势头如日中天。

GGUF 简介

GGUF(GPT-Generated Unified Format)是由 Georgi Gerganov(著名开源项目llama.cpp的创始人)定义发布的一种大模型文件格式。GGUF 继承自其前身 GGML,但 GGML 格式有一些缺点,已被完全弃用并被 GGUF 格式取代。GGUF 是一种二进制格式文件的规范,原始的大模型预训练结果经过转换后变成 GGUF 格式可以更快地被载入使用,也会消耗更低的资源。原因在于 GGUF 采用了多种技术来保存大模型预训练结果,包括采用紧凑的二进制编码格式、优化的数据结构、内存映射等。

综上所述,GGUF 可以理解为一种格式定义,采用相应的工具将原始模型预训练结果转换成GGUF之后可以更加高效的使用。

GGML 的缺陷

GGUF 继承自其前身 GGML,而 GGML 有以下缺点:

  • 无版本信息,导致无法管理和向后兼容
  • 增加或者修改信息非常不灵活
  • 手动修改模型信息很困难

GGUF 特性

GGUF 是一种基于现有 GGJT 的格式(这种格式对张量进行对齐,以便能够使用内存映射(mmap)),但对该格式进行了一些更改,使其更具可扩展性且更易于使用。GGUF 具有如下特性:

  • 单文件部署:它们可以轻松分发和加载,并且不需要任何外部文件来获取附加信息。
  • 可扩展性:可以将新特征添加到基于 GGML 的执行器中/可以将新信息添加到 GGUF 模型中,而不会破坏与现有模型的兼容性。
  • mmap 兼容性:可以使用 mmap 加载模型,以实现快速地加载和保存。
  • 易于使用:无论使用何种语言,都可以使用少量代码轻松加载和保存模型,无需外部库。
  • 信息完整:加载模型所需的所有信息都包含在模型文件中,用户不需要提供任何额外的信息。这大大简化了模型部署和共享的流程。

GGJT 和 GGUF 之间的主要区别在于:超参数(现称为元数据)使用键值结构,而不是非类型化的值列表。这允许在不破坏与现有模型的兼容性的情况下添加新的元数据,这使得可以添加对推理或识别模型有用的附加信息来注释模型。

为什么GGUF格式对大模型文件性能很好

GGUF文件格式能够更快载入模型的原因主要归结于以下几个关键特性:

  1. 二进制格式 :GGUF作为一种二进制格式,相较于文本格式的文件,可以更快地被读取和解析。二进制文件通常更紧凑,减少了读取和解析时所需的I/O操作和处理时间。
  2. 优化的数据结构 :GGUF可能采用了特别优化的数据结构,这些结构为快速访问和加载模型数据提供了支持。例如,数据可能按照内存加载的需要进行组织,以减少加载时的处理。
  3. 内存映射(mmap)兼容性 :GGUF支持内存映射(mmap),这允许直接从磁盘映射数据到内存地址空间,从而加快了数据的加载速度。这样,数据可以在不实际加载整个文件的情况下被访问,特别是对于大模型非常有效。
  4. 高效的序列化和反序列化 :GGUF使用高效的序列化和反序列化方法,这意味着模型数据可以快速转换为可用的格式。
  5. 少量的依赖和外部引用 :如果GGUF格式设计为自包含,即所有需要的信息都存储在单个文件中,这将减少解析和加载模型时所需的外部文件查找和读取操作。
  6. 数据压缩 :GGUF格式采用了有效的数据压缩技术,减少了文件大小,从而加速了读取过程。
  7. 优化的索引和访问机制 :文件中数据的索引和访问机制经过优化,使得查找和加载所需的特定数据片段更加迅速。

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

GGUF文件结构

一个GGUF文件包括文件头、元数据键值对和张量信息等。这些组成部分共同定义了模型的结构和行为。具体如下所示:

同时,GGUF支持多种数据类型,如整数、浮点数和字符串等。这些数据类型用于定义模型的不同方面,如结构、大小和参数。

GGUF文件具体的组成信息如下所示:

  1. 文件头 (Header)
  • 作用 :包含用于识别文件类型和版本的基本信息。
  • 内容
    • Magic Number :一个特定的数字或字符序列,用于标识文件格式。
    • Version :文件格式的版本号,指明了文件遵循的具体规范或标准。
  1. 元数据key-value对 (Metadata Key-Value Pairs)
  • 作用 :存储关于模型的额外信息,如作者、训练信息、模型描述等。
  • 内容
    • Key :一个字符串,标识元数据的名称。
    • Value Type :数据类型,指明值的格式(如整数、浮点数、字符串等)。
    • Value :具体的元数据内容。
  1. 张量计数器 (Tensor Count)
  • 作用 :标识文件中包含的张量(Tensor)数量。
  • 内容
    • Count :一个整数,表示文件中张量的总数。
  1. 张量信息 (Tensor Info)
  • 作用 :描述每个张量的具体信息,包括形状、类型和数据位置。
  • 内容
    • Name :张量的名称。
    • Dimensions :张量的维度信息。
    • Type :张量数据的类型(如:浮点数、整数等)。
    • Offset :指明张量数据在文件中的位置。
  1. 对齐填充 (Alignment Padding)
  • 作用 :确保数据块在内存中正确对齐,有助于提高访问效率。
  • 内容
    • 通常是一些填充字节,用于保证后续数据的内存对齐。
  1. 张量数据 (Tensor Data)
  • 作用 :存储模型的实际权重和参数。
  • 内容
    • Binary Data :模型的权重和参数的二进制表示。
  1. 端序标识 (Endianness)
  • 作用 :指示文件中数值数据的字节顺序(大端或小端)。
  • 内容
    • 通常是一个标记,表明文件遵循的端序。
  1. 扩展信息 (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 格式提供了支持。同时,HuggingFace 开发了一个JavaScript脚本可以用来解析 HuggingFace Hub 上 GGUF 格式的模型的信息。并且可以直接在 HF平台上对GGUF的元数据进行预览 ,包括模型的架构、具体参数等。比如:qwen2-0_5b-instruct-q2_k.gguf 模型的详细信息如下所示。

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

GGUF 与 safetensors 格式的区别

safetensors是一种由Hugging Face推出的新型的安全的模型存储格式。它特别关注模型的安全性和隐私保护,同时保证了加载速度。safetensors文件仅包含模型的权重参数,不包括执行代码,这有助于减少模型文件的大小并提高加载速度。此外,safetensors支持零拷贝(zero-copy)和懒加载(lazy loading),没有文件大小限制,并且支持bfloat16/fp8数据类型。但safetensors没有重点关注性能和跨平台交换。在大模型高效序列化、数据压缩、量化等方面存在不足,并且它只保存了张量数据,没有任何关于模型的元数据信息。

而gguf格式是一种针对大模型的二进制文件格式。专为GGML及其执行器快速加载和保存模型而设计。它是GGML格式的替代者,旨在解决GGML在灵活性和扩展性方面的限制。它包含加载模型所需的所有信息,无需依赖外部文件,这简化了模型部署和共享的过程,同时有助于跨平台操作。此外,GGUF还支持量化技术,可以降低模型的资源消耗,并且设计为可扩展的,以便在不破坏兼容性的情况下添加新信息。

总的来说,safetensors更侧重于安全性和效率,适合快速部署和对安全性有较高要求的场景,特别是在HuggingFace生态中。而gguf格式则是一种为大模型设计的二进制文件格式,优化了模型的加载速度和资源消耗,适合需要频繁加载不同模型的场景。

GGUF 文件解析

通过以下脚本解析上面的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






请到「今天看啥」查看全文


推荐文章
中国证券报  ·  新纪录!90.2亿人次!
11 小时前
卓奇365食品网  ·  昆明发布春季校园食品安全风险提示
20 小时前
卓奇365食品网  ·  昆明发布春季校园食品安全风险提示
20 小时前
中国证券报  ·  分歧出现!700亿资金有变
3 天前
上海证券报  ·  如何破招商内卷?临港出招
3 天前
中国证券报  ·  今天,A股两大主线走强
3 天前
影视大魔王  ·  原来他没有变成僵尸,而是成了鬼怪。
8 年前
销售与市场  ·  品牌传播要用大白话
7 年前