-
Notifications
You must be signed in to change notification settings - Fork 55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
libai 配置系统设计文档 #12
Comments
补充fairseq的配置方法
|
Configuration System 方案讨论配置系统的确定需要同时考虑模型构建,数据读取以及训练流程等构建,下面是一些配置系统的使用方式以及讨论。 基本的配置方式当前决定采用 yacs-based 的配置系统 + 命令行参数共同构建,其中使用 python dict 来替代 yaml 有以下几点考虑:
整体的配置有下面三个好处:
一个简单的例子一个简单的配置文件如下 # examples_cfg.py
from ._base_.models.bert import model
from ._base_.default_train import train
# User can customize these arguments in model
model.embedding.num_embeddings = 12345
model.blocks.hidden_size = 321
model.blocks.num_layers = 3
# User can customize these arguments in train
train.eval_period = 2000 用户可以通过 python3 train_net.py --config-file configs/examples_cfg.py train.eval_period=1000 读取这个配置文件,同时在命令行对 配置系统构建完之后,会保存为如下 model:
_target_: libai.models.Bert
blocks: {_target_: libai.models.gpt.GPT.make_default_blocks, hidden_size: 321, num_layers: 3}
embedding: {_target_: libai.layers.VocabEmbedding, embedding_dim: 756, num_embeddings: 12345}
train:
amp: {enabled: false}
checkpointer: {max_to_keep: 100, period: 5000}
device: cuda
eval_period: 1000
init_checkpoint: ''
log_period: 20
max_iter: 5
output_dir: ./output 如果用户希望复现结果,可以使用下面的命令 python3 train_net.py --config-file output/config.yaml LayCall 的优势上面是一个基本的 config 使用,除此之外,detectron2 还新增了一个 LazyCall 的方案,其可以进行递归实例化,主要的模式是使用字典来描述 class/function 的调用关系,字典中包含的 基于 LazyCall 的机制,一个非常的复杂的 Mask R-CNN 可以通过下面的配置文件进行构建 model = L(GeneralizedRCNN)(
backbone=L(FPN)(
bottom_up=L(ResNet)(
stem=L(BasicStem)(in_channels=3, out_channels=64, norm="FrozenBN"),
stages=L(ResNet.make_default_stages)(
depth=50,
stride_in_1x1=True,
norm="FrozenBN",
),
out_features=["res2", "res3", "res4", "res5"],
),
in_features="${.bottom_up.out_features}",
out_channels=256,
top_block=L(LastLevelMaxPool)(),
),
proposal_generator=L(RPN)(
in_features=["p2", "p3", "p4", "p5", "p6"],
head=L(StandardRPNHead)(in_channels=256, num_anchors=3),
anchor_generator=L(DefaultAnchorGenerator)(
sizes=[[32], [64], [128], [256], [512]],
aspect_ratios=[0.5, 1.0, 2.0],
strides=[4, 8, 16, 32, 64],
offset=0.0,
),
anchor_matcher=L(Matcher)(
thresholds=[0.3, 0.7], labels=[0, -1, 1], allow_low_quality_matches=True
),
box2box_transform=L(Box2BoxTransform)(weights=[1.0, 1.0, 1.0, 1.0]),
batch_size_per_image=256,
positive_fraction=0.5,
pre_nms_topk=(2000, 1000),
post_nms_topk=(1000, 1000),
nms_thresh=0.7,
),
roi_heads=L(StandardROIHeads)(
num_classes=80,
batch_size_per_image=512,
positive_fraction=0.25,
proposal_matcher=L(Matcher)(
thresholds=[0.5], labels=[0, 1], allow_low_quality_matches=False
),
box_in_features=["p2", "p3", "p4", "p5"],
box_pooler=L(ROIPooler)(
output_size=7,
scales=(1.0 / 4, 1.0 / 8, 1.0 / 16, 1.0 / 32),
sampling_ratio=0,
pooler_type="ROIAlignV2",
),
box_head=L(FastRCNNConvFCHead)(
input_shape=ShapeSpec(channels=256, height=7, width=7),
conv_dims=[],
fc_dims=[1024, 1024],
),
box_predictor=L(FastRCNNOutputLayers)(
input_shape=ShapeSpec(channels=1024),
test_score_thresh=0.05,
box2box_transform=L(Box2BoxTransform)(weights=(10, 10, 5, 5)),
num_classes="${..num_classes}",
),
mask_in_features=["p2", "p3", "p4", "p5"],
mask_pooler=L(ROIPooler)(
output_size=14,
scales=(1.0 / 4, 1.0 / 8, 1.0 / 16, 1.0 / 32),
sampling_ratio=0,
pooler_type="ROIAlignV2",
),
mask_head=L(MaskRCNNConvUpsampleHead)(
input_shape=ShapeSpec(channels=256, width=14, height=14),
num_classes="${..num_classes}",
conv_dims=[256, 256, 256, 256, 256],
),
),
pixel_mean=[103.530, 116.280, 123.675],
pixel_std=[1.0, 1.0, 1.0],
input_format="BGR",
) 如此复杂的模型都可以利用 LazyCall 来构建,利用他来构建 Bert 当然也很容易 from libai.config import LazyCall as L
from libai.models import Bert
from libai.layers import VocabEmbedding, TransformerLayer
model = L(Bert)(
embedding=L(VocabEmbedding)(num_embeddings=500, embedding_dim=756),
blocks=L(Bert.make_default_blocks)(
num_layers=12, layer_class=TransformerLayer, hidden_size=123
),
add_pooler=False,
) 不过这样的构建方式依赖于模型的整体代码,所以下面讨论一下模型构建的一些问题。 模型构建对于一些比较小的 submodule,因为其本身就是其他更大组件的 components,比如 Linear,Layernorm 等等,所以这种小 module 直接定义参数就好,可以和配置系统解耦,因为其本身的参数也比较少,另外也方便别人来借鉴我们的 submodule,增加我们的 credits。 一些比较大的 module 或者是 models 的构建一般有下面三种方式
class Bert1(nn.Module):
def __init__(self, embedding, blocks, add_pooler=True,) -> None:
super().__init__()
self.embedding = embedding
self.add_pooler = add_pooler
for i, block in enumerate(blocks):
name = f"layer_{i}"
self.add_module(name, block)
@staticmethod
def make_default_blocks(num_layers, layer_class=None, **kwargs):
if layer_class is None:
layer_class = TransformerLayer
layers = []
for i in range(num_layers):
kwargs["layer_idx"] = i
layers.append(layer_class(**kwargs))
return layers
class Bert2(nn.Module):
def __init__(self, num_vocab, hidden_size, blocks, add_pooler=True,) -> None:
super().__init__()
self.embedding = VocabEmbedding(num_vocab, hidden_size)
self.add_pooler = add_pooler
for i, block in enumerate(blocks):
name = f"layer_{i}"
self.add_module(name, block)
class Bert3(nn.Module):
def __init__(self, cfg, blocks) -> None:
super().__init__()
self.num_vocab = cfg.num_vocab
self.hidden_size = cfg.hidden_size
self.add_pooler = cfg.add_pooler
self.embedding = VocabEmbedding(self.num_vocab, self.hidden_size)
for i, block in enumerate(blocks):
name = f"layer_{i}"
self.add_module(name, block) 至于其他的变种,比如通过一个类方法对参数进行解析等等,都是类似的,如果大家还有其他的参考也可以在下面补充。 下面是三种方法的一个比较:
下面列一下不同的 model 传参方式的配置系统
其他部分除了模型的定义之后,其他部分都是类似的,也更简单,比如数据集的构建,可以通过下面的方式 train_loader = L(build_train_loader)(
dataset=L(get_dataset_name)(names="bert_chinese")
) Q&A 和讨论Q: 这里是使用instantiate函数将所有东西实例化吗,能不能让cfg包含所有参数,然后创建模型或dataloader的时候通过cfg.xxx来访问那个参数? Q: dict下的配置和parser下的配置是什么关系? Q: 怎么添加默认参数?bert模型可能有base、large这样的区分,配置默认参数,让用户可以使用“bert-base”这样的方式获取它的全部配置,无须改动其他参数。 # bert_base.py
from libai.config import LazyCall as L
from libai.models import Bert
from libai.layers import VocabEmbedding, TransformerLayer
model = L(Bert)(
embedding=L(VocabEmbedding)(num_embeddings=500, embedding_dim=756),
blocks=L(Bert.make_default_blocks)(
num_layers=12, layer_class=TransformerLayer, hidden_size=123
),
add_pooler=False,
arg_1=1,
arg_2=2,
arg_3=3,
arg_4=4,
)
# bert_large.py
from .bert_base import model
model.embedding.embedding_dim = 678
model.arg_1 = 1000
# bert_small.py
from .bert_base import model
model.arg_4 = 10 Q: 在上一个问题的情况下,如何支持参数重写,例如在微调阶段,可能会使用不同的dropout,怎么适应这种需求? # pretrain_bert.py
model = L(Bert)(
embedding=L(VocabEmbedding)(num_embeddings=500, embedding_dim=756),
blocks=L(Bert.make_default_blocks)(
num_layers=12, layer_class=TransformerLayer, hidden_size=123
),
add_pooler=False,
arg_1=1,
arg_2=2,
arg_3=3,
arg_4=4,
dropout_prob=0.1,
)
# finetune_bert.py
from .pretrain_bert import model
model.dropout_prob = 0.5 |
模型构建部分,我不太赞成第一种,首先bert、gpt这样的模型,它的backstone很固定,都是transformer layer,可能attention等其他细节会发生变化,但可以用户重新搭建一个模型,然后换另一个名字。方法2、3有各自的优缺点,可以结合一下,既可以方便创建类,也可以清楚模型所涉及的参数: class Bert(nn.Module):
def __init__(self, num_vocab, hidden_size, blocks, add_pooler=True,) -> None:
super().__init__()
self.embedding = VocabEmbedding(num_vocab, hidden_size)
self.add_pooler = add_pooler
for i, block in enumerate(blocks):
name = f"layer_{i}"
self.add_module(name, block)
@classmethod
def build_model(cls, cfg):
return Bert(cfg.num_vocab, cgf.hidden_size) 另外,我们需要以LazyCall来实例化模型吗,我感觉这个需求也没有。我认为的配置文件需求如下:
你们看看这个需求有什么疑问,先把需求定下来,再看解决方案。 |
需求一开始就写好了,你可以滑上去看,目前的解决方案也可以解决这些需求 我觉得讨论模型的实例化方式就可以了 |
我觉得不用因为 nlp 里面不这样做就去否定这个做法,这个库本身的定位也不是只做 nlp 的内容,我们应该去讨论哪种方式更好,不用受限在 nlp 的领域 |
模型的参数不需要通过 add_args 进行添加,因为现在的配置系统本身已经没有 python argparser,只需要在 init 里面定义好参数即可,最终的参数隔离是通过不同的模型的配置文件进行隔离的,比如 # bert_base.py
bert_cfg = dict(num_vocab=500, hidden_size=756, add_pooler=False, num_layers=100) # 这里的值可以理解为给定的默认值
# gpt_base.py
gpt_cfg = dict(num_vocab=1000, hidden_size=1000) 最终 dataloader 的配置,distributed 配置,model 的配置,train 的配置都在不同的配置文件中进行隔离,使用的时候通过 import 这些文件,然后可以任意修改,在训练开始前进行序列化 |
个人感觉还是不合适,nlp的backstone少,不会出现cv里面框架和backstone排列组合的情形。这个框架最开始的用户还是做nlp预训练的,得迎合他们的习惯。通过LazyCall方法生成模型,会增加学习框架的难度。采用传参加初始化,或build_model这样的方法,比较简单,用户可以轻松上手。 另外,采用dict的方式,和dataclass有什么区别?我看到了好几个库都使用dataclass,这是python的新特性,类似于命名元组,可以设置类型,可以添加默认值,可以有help说明。但目前不清楚dict和dataclass的优劣,可能新更新的库都用dataclass?可以考虑采用这种方法。 现在卡在这里不太好,建议先确定一种简单的,易于实现的,让项目推动下去。即使是args,也可以继续做。之后在性能调试,或者使用时发现缺点,到时候改也行。最好今天确定下来,然后按照一个方案做下去。让框架早日处于可调试,可使用状态。 |
|
目前看来第一种方法是最灵活的, |
我们今天把模型定义的方式讨论好就可以推进后续的工作,后续并不依赖整个配置系统确定才能写代码,所以大家可以都发表一下各自的意见,目前应该就是我列的这三种方式,如果有同学知道其他的方式还可以补充 |
我有个糟糕的预感, 可能第一种写法, 在阅读代码的时候会最后变成 层层嵌套的那种方式. 要看一个东西可能得跳好几个地方. |
class Bert(nn.Module):
def __init__(self, num_vocab, hidden_size, blocks, add_pooler=True)
super().__init__()
self.embedding = VocabEmbedding(num_vocab, hidden_size)
self.add_pooler = add_pooler
for i, block in enumerate(blocks):
name = f"layer_{i}"
self.add_module(name, block)
@classmethod
def build_model(cls, cfg):
return Bert(cfg.num_vocab, cgf.hidden_size, cfg.num_layers)
model = Bert.build_model(cfg) 这种方式怎么样,把方法2和3结合起来了。 |
这个我记得星宇在code review里面提过 这样确实会简洁一点 |
如果其他人没有意见,那就采用这种方式吧 |
配置系统目前的配置系统基于下面4个特点进行构建:
两种配置方式最终选择基于 LazyConfig 来构建 libai 项目,提供两种风格的配置文件,一种是注册+解析的方式,一种是 LazyCall 的方式。注册+解析是为了兼容之前一些项目的构建习惯,LazyCall 则是另外一种全新的配置系统思路,比注册+解析更灵活。 注册+解析是之前比较常用的配置方式,通过装饰器将可能会用到的对象注册好,随后便可以通过名字去获取对应的对象,在实例化阶段将参数传入。这种方式的一个问题在于额外引入的注册机制并不够方便,只有注册过的对象才能获取,而我们显然不能对所有代码中的对象都进行注册,一些其他库的对象也无法注册;另外一个问题在于不同用户需要使用对方注册的内容时也不够方便。 LazyCall 是另外一种类型的配置方式,不需要利用注册系统,利用 python 配置文件的特性将需要的模块直接 import,利用延后初始化的特性直接传参,在实例化之前可以进行任意参数的修改,最终在需要的时候进行实例化。 配置文件语法上述两种方式都是采用了 python 文件来构建,因为序列化和反序列化的问题,没有采用命令行传参。另外考虑到 yaml 文件不够灵活,而 python 不仅语法更熟悉,而且可以内置一些计算,更复杂的嵌套和数据类型。 最终生成的配置文件会保存成一个 yaml 文件,如果要复现实验结果,只需要将 不同类型的参数可以放在不同的地方实现参数隔离,比如 # data.py
data = dict(
# Pad the vocab size to be divisible by this value
# This is added for computational efficiency reasons.
make_vocab_size_divisible_by=128,
data_path=[
"/workspace/idea_model/idea_bert/output_data/loss_compara_content_sentence"
],
split="949,50,1",
vocab_file="/workspace/idea_model/idea_bert/bert-base-chinese-vocab.txt",
...
)
# bert.py
model = dict(
model_name="BertModel",
model_cfg=dict(
vocab_size=30522,
hidden_size=768,
hidden_layers=24,
num_attention_heads=12,
...
)
)
# gpt.py
model = dict(
model_name="GPT_2",
model_cfg=dict(
vocab_size=30522,
hidden_size=768,
...
)
)
# bert_large.py
from bert.py import model
model.model_cfg.hidden_size = 1537 通过这种方式,需要使用哪个模型作为模板可以直接 import 对应的模型配置文件,然后再做对应的修改,如果是一个全新的模型,也可以根据模型本身的参数完全重写一套,保证配置文件中的参数名字和模型定义的参数名字一致就可以了。 要查看内置模型需要的参数也可以直接访问对应模型的配置文件,不需要到模型定义的代码处进行查看。最后序列化会只保存使用到的配置文件参数。 模型的配置文件举例下面分别用上述提到的两种配置方式对 bert 模型构建进行举例说明。 采用注册+解析的方式# configs/model/bert.py
# registry + arguments parse
model = dict(
model_name="BertModel",
model_cfg=dict(
vocab_size=30522,
hidden_size=768,
hidden_layers=24,
num_attention_heads=12,
intermediate_size=4096,
hidden_dropout_prob=0.1,
attention_probs_dropout_prob=0.1,
max_position_embeddings=512,
num_tokentypes=2,
add_pooling_layer=True,
initializer_range=0.02,
layernorm_eps=1e-5,
bias_gelu_fusion=True,
bias_dropout_fusion=True,
scale_mask_softmax_fusion=False,
apply_query_key_layer_scaling=True,
),
)
# modify default arguments
model.model_cfg.vocab_size = 3000 序列化后的 config.yaml 文件,支持反序列化之后进行构建。 # config.yaml
model:
model_name: BertModel
model_cfg: {add_pooling_layer: true, apply_query_key_layer_scaling: true, attention_probs_dropout_prob: 0.1, bias_dropout_fusion: true, bias_gelu_fusion: true, hidden_dropout_prob: 0.1, hidden_layers: 24, hidden_size: 768, initializer_range: 0.02, intermediate_size: 4096, layernorm_eps: 1.0e-05, max_position_embeddings: 512, num_attention_heads: 12, num_tokentypes: 2, scale_mask_softmax_fusion: false, vocab_size: 30522} LazyCall# configs/model/bert.py
# LazyCall
from libai.config import LazyCall
from libai.models import BertModel
cfg = dict(
vocab_size=30522,
hidden_size=768,
hidden_layers=24,
num_attention_heads=12,
intermediate_size=4096,
hidden_dropout_prob=0.1,
attention_probs_dropout_prob=0.1,
max_position_embeddings=512,
num_tokentypes=2,
add_pooling_layer=True,
initializer_range=0.02,
layernorm_eps=1e-5,
bias_gelu_fusion=True,
bias_dropout_fusion=True,
scale_mask_softmax_fusion=False,
apply_query_key_layer_scaling=True,
)
model = LazyCall(BertModel)(cfg=cfg)
# modify default arguments
model.cfg.vocab_size = 3000 序列化之后的配置文件。 model:
_target_: libai.models.BertModel
cfg: {add_pooling_layer: true, apply_query_key_layer_scaling: true, attention_probs_dropout_prob: 0.1, bias_dropout_fusion: true, bias_gelu_fusion: true, hidden_dropout_prob: 0.1, hidden_layers: 24, hidden_size: 768, initializer_range: 0.02, intermediate_size: 4096, layernorm_eps: 1.0e-05, max_position_embeddings: 512, num_attention_heads: 12, num_tokentypes: 2, scale_mask_softmax_fusion: false, vocab_size: 30522} 上面两种方法最终获得的保存结果是类似的,唯一的区别是 LazyCall 可以更好的定位到使用的模型是 总结有了标准的注册+解析的配置文件机制,最后再讲一下为什么我们还想额外引入 LazyCall 的机制:
整体调用流程介绍
|
数据集相关配置文件要求:比较灵活地支持多数据集训练和多数据集测试 我觉得 data 的配置文件不应该每一种 dataset 一个,每一种 dataset 之间的区别并不大 dataloader 相关的配置不应该放到 data 的配置文件里面,因为所有的 dataset 共享一个 dataloader, data 的配置应该和数据集本身的处理相关 data 配置里面应该包含的内容:
|
BlendableDataset没有区分文本和图像,是通用的 |
主要的问题是在 cv 里面,还是如果两个 dataset 的 classes 不一样,则需要 reindex 类别和下标,所以现在我们决定先只支持类别一致的 dataset 进行融合 |
配置系统主要是为模型结构的定义和训练超参提供配置参数,好的配置系统可以让模型的定义以及训练流程更清晰,也可以让用户一眼能够了解到不同模型配置差异以及训练差异在哪里,而且还可以让模型的复现变得更加容易。
一个好的配置系统我认为有下面4个特点:
调研了市面上比较常见的配置系统,下面是一个总结
megatron
使用 python argparser 进行定义
问题是定义的参数无法序列化,不好对比两次训练的超参差别,在代码中通过
get_args()
直接穿透到内层获得参数,同时参数的修改没有保护机制,非常容易误修改参数;huggingface
使用了 python class 进行定义
huggingface 的配置系统非常繁琐,阅读代码的时候很不方便,整个配置被分散在了不同的 python 文件中,貌似可以保存配置,但是不确定能不能读取配置进行再次训练,也不了解新增配置参数是否方便;
detectron2 & ColossalAI & mmdet
他们的配置系统类似于传统的 yacs-based 配置系统,不过都是利用 dict 的方式进行定义
这一类配置系统和传统的 yaml 类型的配置系统相比,优势就是非常灵活,利用了 python 语法可以方便的增加和修改字典,也可以在配置系统中写一些简单的算术或者是简单函数,同时也可以非侵入式增加配置参数。
整体配置的定义非常清晰,网络定义,dataloader 以及 训练流程可以完全由配置系统构成,也支持序列化和反序列化,应该能够支持用户任意组合内置模块进行训练而不需要额外写代码,只需要完成配置即可。
The text was updated successfully, but these errors were encountered: