作者: Hongyu Chiu
创建日期: 2024/05/14
最后修改日期: 2024/05/14
描述: 使用float8量化训练一个简单的Transformer模型。
随着Transformer模型中参数数量的不断增加,训练和推理变得非常消耗内存和计算资源。因此,引入了8位浮点数(FP8),在几乎没有精度下降的情况下提供了比16位浮点数更好的性能。
具体来说,FP8有两种不同类型:E4M3和E5M2,在训练的不同部分有不同的用途。
通常,E4M3在前向传播过程中最好使用,因为激活值和权重需要更高的精度。然而,在反向传播过程中,由于梯度对精度损失的不敏感,E5M2被利用,但需要更高的动态范围。
值得注意的是,FP8推理部署大大简化,因为推理和训练使用相同的数据类型。这与使用32位或16位浮点数训练的网络进行INT8推理形成对比,这需要后训练量化(PTQ)校准,甚至量化感知训练(QAT)以维持模型精度。
在这个例子中,我们将构建一个简单的Transformer模型,并使用FP16和FP8精度对其进行训练。你将观察到,精度不会因为较低的精度而降低。
注意:你需要一块支持FP8张量核心的不错GPU才能获得预期的性能提升。
我们将使用KerasNLP库简化模型实现。此外,使用混合精度训练来减少训练时间。
注意:对TensorFlow的依赖仅在于数据处理。
!pip install -q --upgrade keras-nlp
!pip install -q --upgrade keras # 升级到Keras 3.
import os
os.environ["KERAS_BACKEND"] = "jax"
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
import re
import keras
import keras_nlp
import tensorflow as tf
keras.config.set_dtype_policy("mixed_bfloat16")
定义一些超参数。
EPOCHS = 3
BATCH_SIZE = 32
VOCABULARY_SIZE = 20000
MAX_SEQUENCE_LENGTH = 200
MODEL_KWARGS = dict(
vocabulary_size=VOCABULARY_SIZE,
max_sequence_length=MAX_SEQUENCE_LENGTH,
hidden_dim=32, # 每个token的隐藏层大小
num_heads=2, # 注意力头的数量
intermediate_dim=32, # 前馈网络中的中间层大小
dropout=0.1, # 丢弃率
)
首先,让我们下载IMDB数据集并解压缩。
!mkdir -p datasets
!wget http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz -q -O datasets/aclImdb_v1.tar.gz
!mkdir -p datasets/aclImdb
!tar -xzf datasets/aclImdb_v1.tar.gz -C datasets
!rm -rf datasets/aclImdb/train/unsup
我们将使用keras.utils.text_dataset_from_directory
工具从文本文件生成标记的tf.data.Dataset
数据集。
train_ds = keras.utils.text_dataset_from_directory(
"datasets/aclImdb/train",
batch_size=BATCH_SIZE,
validation_split=0.2,
subset="training",
seed=42,
)
val_ds = keras.utils.text_dataset_from_directory(
"datasets/aclImdb/train",
batch_size=BATCH_SIZE,
validation_split=0.2,
subset="validation",
seed=42,
)
test_ds = keras.utils.text_dataset_from_directory(
"datasets/aclImdb/test", batch_size=BATCH_SIZE
)
找到25000个属于2个类的文件。
使用20000个文件进行训练。
找到25000个属于2个类的文件。
使用5000个文件进行验证。
找到25000个属于2个类的文件。
我们现在将文本转换为小写字母。
train_ds = train_ds.map(lambda x, y: (tf.strings.lower(x), y))
val_ds = val_ds.map(lambda x, y: (tf.strings.lower(x), y))
test_ds = test_ds.map(lambda x, y: (tf.strings.lower(x), y))
让我们打印一些样本。
for text_batch, label_batch in train_ds.take(1):
for i in range(3):
print(f"文本: {text_batch.numpy()[i]}")
print(f"标签: {label_batch.numpy()[i]}")
文本:b'"pandemonium"是一部恐怖电影的恶搞,给人感觉比搞笑更愚蠢。相信我,当我告诉你我喜欢喜剧时,尤其是喜剧恶搞。"空中大灌篮"、"赤裸裸的枪"三部曲、"火焰马鞍"、"高焦虑"和"太空大炮"是我最喜欢的恶搞特定类型的喜剧。"pandemonium"并不在那些电影之中。这部电影中的大部分场景让我坐在那里目瞪口呆,因为这部电影并不是那么搞笑。电影中有几处让人发笑的地方,但当你观看一部喜剧时,你期待更多的笑声,而这部电影只是略有几次。天哪,"尖叫"比这部电影笑得还多,而那部影片更像是一部恐怖片。这是多么离奇的事情呢?<br /><br />*1/2(满分四分)'
标签:0
文本:b'david mamet是一位非常有趣且非常不平等的导演。他的第一部电影《游戏之家》是我最喜欢的那一部,它设定了一系列角色在复杂情境中生活视角的变化,观众的视角也随之变化。<br /><br />所以《凶杀案》也是如此,从标题就试图将观众的思维引向常规的犯罪 drama。主要角色是两名警察,一个犹太人和一个爱尔兰人,他们处理一个种族关系紧张的地区。一个古老的犹太商人的谋杀,证明他是以色列独立战争的古老退伍军人,触发了犹太侦探心中的犹太身份。<br /><br />这就是电影缺陷最明显的地方。觉醒的过程戏剧化且难以置信,犹太激进分子的团体呈现歌剧化,而侦探最终走向最终暴力对抗的方式显得悲惨。电影的结尾聪明地具有mamet的风格,但从人类情感的角度来看让人失望。<br /><br />joe mantegna和william macy的表演都很出色,但故事的缺陷过于明显,无法轻易弥补。'
标签:0
文本:b'关于纽约消防员在有史以来最严重恐怖袭击中的生活的伟大纪录片……仅这一原因就足以让它成为必看收藏品。我感到震惊的不仅是袭击,还有一些消防员的"高脂饮食"和外貌。我认为很多医生会同意,在他们的身体状况下,一些消防员在背负超过60磅装备时,是无法到达79层的。话虽如此,我现在对消防员有了更大的尊重,我意识到成为一名消防员是一份改变生活的工作。法国有制作优秀纪录片的历史,而这就是一部伟大的纪录片.....'
标签:1
我们将使用keras_nlp.tokenizers.WordPieceTokenizer
层来切分文本。keras_nlp.tokenizers.WordPieceTokenizer
接受一个WordPiece词汇并具备切分文本及重新组合令牌序列的功能。
在定义切分器之前,我们首先需要在我们拥有的数据集上训练它。WordPiece切分算法是一种子词切分算法;在语料库上训练它可以为我们提供一个子词词汇表。子词切分器是词切分器(词切分器需要非常大的词汇量以良好覆盖输入词)与字符切分器(字符并不能像词那样真正编码意义)之间的一种妥协。幸运的是,KerasNLP让在语料库上训练WordPiece变得非常简单,使用的是keras_nlp.tokenizers.compute_word_piece_vocabulary
工具。
def train_word_piece(ds, vocab_size, reserved_tokens):
word_piece_ds = ds.unbatch().map(lambda x, y: x)
vocab = keras_nlp.tokenizers.compute_word_piece_vocabulary(
word_piece_ds.batch(1000).prefetch(2),
vocabulary_size=vocab_size,
reserved_tokens=reserved_tokens,
)
return vocab
每个词汇中都有几个特殊的保留令牌。我们有两个这样的令牌:
"[PAD]"
- 填充令牌。当输入序列的长度小于最大序列长度时,填充令牌会附加到输入序列长度后面。"[UNK]"
- 未知令牌。reserved_tokens = ["[PAD]", "[UNK]"]
train_sentences = [element[0] for element in train_ds]
vocab = train_word_piece(train_ds, VOCABULARY_SIZE, reserved_tokens)
让我们看看一些令牌!
print("令牌: ", vocab[100:110])
令牌: ['à', 'á', 'â', 'ã', 'ä', 'å', 'æ', 'ç', 'è', 'é']
现在,让我们定义切分器。我们将根据上述训练的词汇配置切分器。我们将定义一个最大序列长度,以便所有序列在长度小于指定序列长度时都填充到相同的长度。否则,序列将被截断。
tokenizer = keras_nlp.tokenizers.WordPieceTokenizer(
vocabulary=vocab,
lowercase=False,
sequence_length=MAX_SEQUENCE_LENGTH,
)
让我们尝试对数据集中一个样本进行标记化!为了验证文本是否已正确标记化,我们还可以将标记列表反标记化为原始文本。
input_sentence_ex = train_ds.take(1).get_single_element()[0][0]
input_tokens_ex = tokenizer(input_sentence_ex)
print("句子: ", input_sentence_ex)
print("标记: ", input_tokens_ex)
print("反标记化后的文本: ", tokenizer.detokenize(input_tokens_ex))
句子: tf.Tensor(b'great movie - especially the music - etta james - "at last". this speaks volumes when you have finally found that special someone.', shape=(), dtype=string)
标记:
[ 218 150 14 393 137 356 14 4917 2941 719 14 3
164 370 3 15 145 2705 11670 186 155 160 557 391
146 452 416 15 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0]
反标记化后的文本: tf.Tensor(b'great movie - especially the music - etta james - " at last " . this speaks volumes when you have finally found that special someone . [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD]', shape=(), dtype=string)
接下来,我们将格式化我们的数据集,以便可以供模型使用。我们需要对文本进行标记化。
def format_dataset(sentence, label):
sentence = tokenizer(sentence)
return ({"input_ids": sentence}, label)
def make_dataset(dataset):
dataset = dataset.map(format_dataset, num_parallel_calls=tf.data.AUTOTUNE)
return dataset.shuffle(512).prefetch(tf.data.AUTOTUNE).cache()
train_ds = make_dataset(train_ds)
val_ds = make_dataset(val_ds)
test_ds = make_dataset(test_ds)
让我们建立一个简单的 Transformer 模型。我们将使用 KerasNLP 库中的 TokenAndPositionEmbedding
和 TransformerDecoder
。TokenAndPositionEmbedding
表示句子中的单词及其顺序,而 TransformerDecoder
为输入序列的每个时间步输出一个向量。在这里,我们对所有时间步取平均,并在其上使用前馈神经网络来对文本进行分类。
def build_model(
vocabulary_size=20000,
max_sequence_length=200,
hidden_dim=32,
num_heads=2,
intermediate_dim=32,
dropout=0.1,
):
token_id_input = keras.layers.Input(shape=(None,), dtype="int32", name="input_ids")
x = keras_nlp.layers.TokenAndPositionEmbedding(
vocabulary_size=vocabulary_size,
sequence_length=max_sequence_length,
embedding_dim=hidden_dim,
)(token_id_input)
x = keras.layers.Dropout(rate=dropout)(x)
x = keras_nlp.layers.TransformerDecoder(
intermediate_dim=intermediate_dim,
num_heads=num_heads,
dropout=dropout,
)(x)
x = keras.layers.GlobalAveragePooling1D()(x)
x = keras.layers.Dropout(dropout)(x)
x = keras.layers.Dense(intermediate_dim, activation="relu")(x)
x = keras.layers.Dropout(dropout)(x)
outputs = keras.layers.Dense(1, activation="sigmoid")(x)
return keras.Model(inputs=token_id_input, outputs=outputs)
首先,我们使用混合精度("mixed_bfloat16"
)训练和评估模型。之后,我们将结果与 FP8 训练/推理进行比较。
model = build_model(**MODEL_KWARGS)
model.summary()
model.compile(
optimizer="adam",
loss="binary_crossentropy",
metrics=["accuracy"],
)
history = model.fit(train_ds, epochs=EPOCHS, validation_data=val_ds)
result = model.evaluate(test_ds)
print(f"准确率(mixed_bfloat16): {result[1]:.2%}")
模型: "functional_1"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ 层 (类型) ┃ 输出形状 ┃ 参数 # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ input_ids (输入层) │ (无, 无) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ token_and_position_embedding │ (无, 无, 32) │ 646,400 │ │ (TokenAndPositionEmbedding) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dropout (丢弃层) │ (无, 无, 32) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ transformer_decoder │ (无, 无, 32) │ 6,464 │ │ (TransformerDecoder) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ global_average_pooling1d │ (无, 32) │ 0 │ │ (全局平均池化1D) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dropout_2 (丢弃层) │ (无, 32) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense (全连接层) │ (无, 32) │ 1,056 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dropout_3 (丢弃层) │ (无, 32) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_1 (Dense) │ (None, 1) │ 33 │ └─────────────────────────────────┴────────────────────────┴───────────────┘
总参数: 653,953 (2.49 MB)
可训练参数: 653,953 (2.49 MB)
不可训练参数: 0 (0.00 B)
准确率 (mixed_bfloat16): 75.56%
我们可以通过一行API启用FP8训练/推理:
model.quantize("float8")
。
model = build_model(**MODEL_KWARGS)
model.quantize("float8")
要检查FP8训练是否发生,我们可以打印一些与FP8训练相关的变量:
*_scale
: 将输入、权重和梯度的分布移入FP8可表示范围的缩放因子。默认为1.0
*_amax_history
: 用于缩放因子计算的amax历史窗口。默认为0.0
,长度为1024。pattern = r"(transformer).+(multi_head).+(query).+(scale|amax_history)"
for v in model.trainable_variables:
if re.findall(pattern, v.path):
print(v.path)
print(keras.ops.convert_to_numpy(v.value))
FP8层的dtype策略也已被修改。
for layer in model._flatten_layers(recursive=True):
if "float8" in str(layer.dtype_policy):
print(f"{layer.name}: {layer.dtype_policy}")
feedforward_output_dense: <QuantizedFloat8DTypePolicy "float8_from_mixed_bfloat16">
feedforward_intermediate_dense: <QuantizedFloat8DTypePolicy "float8_from_mixed_bfloat16">
attention_output: <QuantizedFloat8DTypePolicy "float8_from_mixed_bfloat16">
value: <QuantizedFloat8DTypePolicy "float8_from_mixed_bfloat16">
key: <QuantizedFloat8DTypePolicy "float8_from_mixed_bfloat16">
query: <QuantizedFloat8DTypePolicy "float8_from_mixed_bfloat16">
dense_2: <QuantizedFloat8DTypePolicy "float8_from_mixed_bfloat16">
dense_3: <QuantizedFloat8DTypePolicy "float8_from_mixed_bfloat16">
让我们训练模型并查看结果。我们可以验证用FP8训练时准确率并没有下降,并且包含FP8信息的变量在拟合后发生了变化。
model.compile(
optimizer="adam",
loss="binary_crossentropy",
metrics=["accuracy"],
)
history = model.fit(train_ds, epochs=EPOCHS, validation_data=val_ds)
result = model.evaluate(test_ds)
print(f"准确率 (float8): {result[1]:.2%}")
for v in model.trainable_variables:
if re.findall(pattern, v.path):
print(v.path)
print(keras.ops.convert_to_numpy(v.value))
准确率 (float8): 74.16%