gguf的原理和使用——大模型CPU部署系列02

作者: 引线小白-本文永久链接:httpss://www.limoncc.com/post/360dc16c61c8a0a9/
知识共享许可协议: 本博客采用署名-非商业-禁止演绎4.0国际许可证

摘要: 本文意在理清ggfu的原理和使用。若有错误,请大家指正。
关键词: gguf,大模型,模型量化

一、导言

开发者 Georgi Gerganov 基于 Llama 模型手撸的纯 C/C++ 版本,它最大的优势是可以在 CPU上快速地进行推理而不需要 GPU。然后作者将该项目中模型量化的部分提取出来做成了一个用于机器学习张量库:GGML,项目名称中的GG其实就是作者的名字首字母。它与其他张量库最大的不同,就是支持量化模型在CPU中执行推断。从而实现了低资源部署LLM,也就是大家常说的大模型民主化。而它生成的文件格式只存储了张量,为了适应发展提出了gguf格式标准,除了张量,还能存储分词器等其他模型信息。

目前这一格式标准得到了hugging face的支持。你应该容易发现hugging face仓库中经常能看到gguf后缀的文件。

下面通过一个简单例子回顾一下python二进制文件读写的知识,以方便深入理解gguf格式标准。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1、准备文件和数据
import os
file_path = "./data/test.bin"
data = 123

# 2、将数据转换为二进制并写入
content= data.to_bytes(1, 'big')
bin_file = open(file_path, 'wb') #覆盖写入
bin_file.write(content)
print('content',content) # b'{'
bin_file.close()

# 3、打开文件读取二进制文件
bin_file = open(file=file_path,mode='rb')
size = os.path.getsize(file_path) #获得文件大小
content = b''
for i in range(size):
content += bin_file.read(1) #每次输出一个字节
print(content)
bin_file.close()
data = int.from_bytes(data)
print(data) # 123

二、设计一个二进制文件标准

上面是一个简单的例子,但是面临的数据类型是多种多样的,python为我们提供了struct库,它可以将Python对象转换为二进制格式并反之。这个模块提供了一种简单、快速和灵活的方法来处理结构化数据。下面我们来设计一个简单键值对二进制数据格式:键值对 = 长度+键+长度+值,然后不断堆叠。解码二进制的时候我们需要对值做一些约定即可。下面展示一个自己设计的例子:


二进制文件格式设计

按照这一格式设计,生成二进制文件的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import os
import struct

file_path = "./data/test.bin"

# 我们来设计一个二进制文件
# 按照len+key+len+value
bin_kv_data = b''

# 第一部分是title
key1 = "title".encode('utf-8')
value1 = "欢迎来学习大模型量化与CPU部署".encode('utf-8')
bin_kv_data += struct.pack('<H', len(key1)) + key1
bin_kv_data += struct.pack('<H', len(value1)) + value1
# 第二部分是data
key2 = "data".encode('utf-8')
value2 = [1,2,3]
value2 = struct.pack('3i', *value2)
bin_kv_data += struct.pack('<H', len(key2)) + key2
bin_kv_data += struct.pack('<H', len(value2))+value2
# 写入数据
bin_file = open(file_path, 'wb') #覆盖写入
bin_file.write(bin_kv_data)
print('content',bin_kv_data)
bin_file.close()

有这样一套标准信息,我们就很容易读取这个文件

1
2
3
4
5
6
7
8
9
# 然后我们读取这个二进制文件
bin_file = open(file=file_path,mode='rb')
size = os.path.getsize(file_path) #获得文件大小
# 我们先读取前缀,然后读取内容
key_len = struct.unpack('<H', bin_file.read(2))[0]
key = bin_file.read(key_len).decode('utf-8')
value_len = struct.unpack('<H', bin_file.read(2))[0]
value = bin_file.read(value_len).decode('utf-8')
print(f"key:{key}\nvalue:{value}")

读取第二部分,对于list我们需要知道长度这样,才能方便读取。我们目前的设计没有考虑,注意这段代码中的3i,是c语言中的类型,见表:

1
value = struct.unpack('3i', bin_file.read(value_len))

c语言类型
1
2
3
4
5
6
7
8
9
10
11
# 我们先读取前缀,然后读取内容
key_len = struct.unpack('<H', bin_file.read(2))[0]
key = bin_file.read(key_len).decode('utf-8')
value_len = struct.unpack('<H', bin_file.read(2))[0]
value = struct.unpack('3i', bin_file.read(value_len))
print(f"key:{key}\nvalue:{value}")

# 到了文件末尾就关闭文件
if bin_file.tell()==size:
bin_file.close()
...

最后我们做个总结,所谓二进制文件格式设计就是对内容的长度、数据类型、键值对的设计和一套默认规范。有了这套默认规范,这样就方便跨语言分享了。从而降低对框架的依赖。

三、揭开gguf的面纱

3.1、如何生成gguf文件

下面揭开gguf的面纱,实际上gguf也是这样差不多的规范。我们从一个简单例子开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import numpy as np
from gguf import GGUFWriter

# 写入一下head信息
gguf_writer = GGUFWriter("./data/test.gguf", "test")
gguf_writer.add_block_count(12)
gguf_writer.add_uint32("answer", 42) # Write a 32-bit integer
gguf_writer.add_float32("answer_in_float", 42.0) # Write a 32-bit float
gguf_writer.add_custom_alignment(64)

# 写入矩阵信息
tensor1 = np.ones((32,8), dtype=np.float32) * 100.0
tensor2 = np.ones((64,), dtype=np.float32) * 101.0
gguf_writer.add_tensor("tensor1", tensor1)
gguf_writer.add_tensor("tensor2", tensor2)
# 正式写入
gguf_writer.write_header_to_file()
gguf_writer.write_kv_data_to_file()
gguf_writer.write_tensors_to_file()
# 关闭文件
gguf_writer.close()

然后我们解析它,给个图,可能更加清晰


gguf文件格式

是不是非常简单。gguf按照自己的规范封装了一python库。这就是我们常见的在llama.cpp中的convert.py系列文件用到的一个核心库。

3.2、gguf标准键值对

general

  • general.architecture: string
  • general.quantization_version: uint32
  • general.alignment: uint32
  • general.name: string
  • general.author: string
  • general.description: string
  • general.license: string
  • general.file_type: uint32
  • general.source.url: string
  • general.source.huggingface.repository: string

llm

  • [llm].context_length: uint64
  • [llm].embedding_length: uint64
  • [llm].block_count: uint64
  • [llm].feed_forward_length: uint64
  • [llm].use_parallel_residual: bool
  • [llm].tensor_data_layout: string

attention

  • [llm].attention.head_count: uint64
  • [llm].attention.head_count_kv: uint64
  • [llm].attention.max_alibi_bias: float32
  • [llm].attention.clamp_kqv: float32
  • [llm].attention.layer_norm_epsilon: float32
  • [llm].attention.layer_norm_rms_epsilon: float32
  • [llm].rope.dimension_count: uint64
  • [llm].rope.freq_base: float32
  • [llm].rope.scale_linear: float32

model

  • [model].context_length
  • [model].embedding_length
  • [model].block_count
  • [model]..feed_forward_length
  • [model]..rope.dimension_count
  • [model]..attention.head_count
  • [model]..attention.layer_norm_rms_epsilon
  • [model].rope.scale
  • [model]..attention.head_count_kv
  • [model]..tensor_data_layout

tokenizer

  • tokenizer.ggml.model: string
  • tokenizer.ggml.tokens: array[string]
  • tokenizer.ggml.scores: array[float32]
  • tokenizer.ggml.token_type: array[uint32]
  • tokenizer.ggml.merges: array[string]
  • tokenizer.ggml.added_tokens: array[string]
  • tokenizer.ggml.bos_token_id: uint32
  • tokenizer.ggml.eos_token_id: uint32
  • tokenizer.ggml.unknown_token_id: uint32
  • tokenizer.ggml.separator_token_id: uint32
  • tokenizer.ggml.padding_token_id: uint32
  • tokenizer.huggingface.json: string
3.3、如何读取gguf文件

读取头信息,为什么总有一个<的符号?这个符号的作用主要是为了按照指定格式对齐长度


对齐
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import os
import struct
path = "./data/test.gguf"
model_bin = open(file=path,mode='rb')
size = os.path.getsize(path) #获得文件大小
# 读取第一个,这个确实是一个魔法数,它的二进制是GGUF。
gguf_magic_num = struct.unpack('<I', model_bin.read(4))[0]
gguf_magic = struct.pack("<I", gguf_magic_num)
print(gguf_magic) # b'GGUF'

# 读取第二个,gguf的版本信息
gguf_version = struct.unpack('<I', model_bin.read(4))[0]
print(gguf_version) # 2

# 读取第三个,gguf的张量信息(tesor information)个数
ti_data_count = struct.unpack('<Q', model_bin.read(8))[0]
print(ti_data_count) # 291
# 读取第三个,gguf的kv数据个数信息
kv_data_count = struct.unpack('<Q', model_bin.read(8))[0]
print(kv_data_count) # 20

结果

1
2
3
4
gguf_magic:b'GGUF'
gguf_version:3
ti_data_count:2
kv_data_count:5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# 取第一个kv信息
key1_len = struct.unpack('<Q', model_bin.read(8))[0]
key1_data = model_bin.read(key1_len).decode('utf-8')

value1_type = struct.unpack('<I', model_bin.read(4))[0] # 8对应这string类型
value1_len = struct.unpack('<Q', model_bin.read(8))[0]
value1_data = model_bin.read(value1_len).decode('utf-8') # llama
print(f"{key1_data}:{value1_data}")


# 取第二个kv信息
key2_len = struct.unpack('<Q', model_bin.read(8))[0]
key2_data = model_bin.read(key2_len).decode('utf-8')

value2_type = struct.unpack('<I', model_bin.read(4))[0] # 4对应这UINT32
value2_data = struct.unpack('<I', model_bin.read(4))[0]
print(f"{key2_data}:{value2_data}")


# 取第三个kv信息
key3_len = struct.unpack('<Q', model_bin.read(8))[0]
key3_data = model_bin.read(key3_len).decode('utf-8')

value3_type = struct.unpack('<I', model_bin.read(4))[0] # 4对应着UINT32
value3_data = struct.unpack('<I', model_bin.read(4))[0]
print(f"{key3_data}:{value3_data}")


# 取第四个kv信息
key4_len = struct.unpack('<Q', model_bin.read(8))[0]
key4_data = model_bin.read(key4_len).decode('utf-8')

value4_type = struct.unpack('<I', model_bin.read(4))[0] # 6对应着FLOAT32
value4_data = struct.unpack('<f', model_bin.read(4))[0]
print(f"{key4_data}:{value4_data}")

# 取第五个kv信息
key5_len = struct.unpack('<Q', model_bin.read(8))[0]
key5_data = model_bin.read(key5_len).decode('utf-8')

value5_type = struct.unpack('<I', model_bin.read(4))[0] # 4对应这UINT32
value5_data = struct.unpack('<I', model_bin.read(4))[0]
print(f"{key5_data}:{value5_data}")

# 读取tensor信息
tensor1_name_len = struct.unpack('<Q', model_bin.read(8))[0]
tensor1_name_data = model_bin.read(tensor1_name_len).decode('utf-8')

tensor1_dim = struct.unpack('<I', model_bin.read(4))[0]
tensor1_shape = []
for i in range(tensor1_dim):
tensor1_shape.append(struct.unpack('<Q', model_bin.read(8))[0])
...

tensor1_type = struct.unpack('<I', model_bin.read(4))[0]
tensor1_offset = struct.unpack('<Q', model_bin.read(8))[0]
print(f"{tensor1_name_data}==>dim:{tensor1_dim},type:{tensor1_type},shape:{tensor1_shape},offset:{tensor1_offset}")


tensor2_name_len = struct.unpack('<Q', model_bin.read(8))[0]
tensor2_name_data = model_bin.read(tensor2_name_len).decode('utf-8')
tensor2_dim = struct.unpack('<I', model_bin.read(4))[0]
tensor2_shape = []
for i in range(tensor2_dim):
tensor2_shape.append(struct.unpack('<Q', model_bin.read(8))[0])
...
tensor2_type = struct.unpack('<I', model_bin.read(4))[0]
tensor2_offset = struct.unpack('<Q', model_bin.read(8))[0]
print(f"{tensor2_name_data}==>dim:{tensor2_dim},type:{tensor2_type},shape:{tensor2_shape},offset:{tensor2_offset}")
1
2
3
4
5
6
7
general.architecture:test
test.block_count:12
answer:42
answer_in_float:42.0
general.alignment:64
tensor1==>dim:2,type:0,shape:[8, 32],offset:0
tensor2==>dim:1,type:0,shape:[64],offset:1024

注意当我们开始写入张量信息的时候,gguf标准会对前面的二进制信息作补全操作,就是会加零。这样方便我们读取张量信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 获取当前二进制文件位置
n = model_bin.tell()

# 计算补零数
off_set = ((n + 64 - 1) // 64) * 64 -model_bin.tell()
model_bin.seek(n + off_set)

tensor1_data = struct.unpack(f'<{32*8}f', model_bin.read(4*32))
print(tensor1_data)
len(tensor1_data)


tensor2_data = struct.unpack('<64f', model_bin.read(4*64))
print(tensor2_data)
len(tensor2_data)

if model_bin.tell()==size:
model_bin.close()
...

结果:

1
2
(100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0)
(101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0, 101.0)

如果你对大模型应用有兴趣,欢迎加入AutogenQQ交流群:593623958


版权声明
引线小白创作并维护的柠檬CC博客采用署名-非商业-禁止演绎4.0国际许可证。
本文首发于柠檬CC [ https://www.limoncc.com ] , 版权所有、侵权必究。
本文永久链接httpss://www.limoncc.com/post/360dc16c61c8a0a9/
如果您需要引用本文,请参考:
引线小白. (Oct. 24, 2023). 《gguf的原理和使用——大模型CPU部署系列02》[Blog post]. Retrieved from https://www.limoncc.com/post/360dc16c61c8a0a9
@online{limoncc-360dc16c61c8a0a9,
title={gguf的原理和使用——大模型CPU部署系列02},
author={引线小白},
year={2023},
month={Oct},
date={24},
url={\url{https://www.limoncc.com/post/360dc16c61c8a0a9}},
}

'