MiniGPT4Qwen-14B:极少量可训练参数的双语多模态大模型DeepSpeed流水线并行的踩填坑历程

代码库: github.com/Coobiw/Mini...

已加入MiniGPT4Qwen-14B-Chat模型的双卡DeepSpeed流水线并行训练,后续的推理(命令行demo+ gradio WebUI demo),以及14B模型的checkpoint和train log(流水线并行14B模型的权重和日志)。如果有帮助,可以考虑star一下,马上200个了!有相关问题和建议也可以github上直接提issue,会比知乎戳我更快!

训练的完整代码:

github.com/Coobiw/Mini...

阅读后可能的收获

本文的主题是极少可训练参数的大模型的流水线并行(模型并行的一种)训练,阅读后,你可能可以收获:

  • 对于GPipe流水线并行有初步的认识和理解
  • 对于DeepSpeed流水线并行有详细的了解和实战参考(踩坑填坑踩坑填坑,笔者看了不少DeepSpeed流水线并行的源码。。。还在DeepSpeed repo来了一波issue的自问自答hhh)
  • 对于多模态大语言模型的Pipeline有着清晰的认识
  • 对于大语言模型,尤其是通义千问的各个层次和组件有着进一步的认识(因为流水线并行需要非常了解层次结构和输入输出)

动机

继之前MiniGPT4Qwen的博客和github repo取得了不错的反响后,我不禁开始思考:如何在有限的资源下,进一步挑战自己,扩展这个项目的边界呢? 对于这个问题,无非是模型和数据上的升级和scale up,数据上的scale up对于我们来说比较难产生新的知识,所以我选择了scale up模型,将LLM从7B提升到14B。这其中就会产生许多的技术问题(本质原因还是硬件不行,只有RTX3090的24GB显存。。。)当然,量化LLM到4bit、8bit也是很好的解决方案,但总感觉有些无聊,且有稍许明显的对话能力下降。

MiniGPT4Qwen-14B主要是接入了更强大的Qwen-14B-Chat的大语言模型,由于大语言模型从7B增大到了14B,14B的模型,使用16bits的fp16或bf16,至少需要28GB的显存,再加上视觉部分,约需要30GB显存,这至少需要在V100、A6000、A100、A800上才能放得下。对于我这样的个人用户,只能使用RTX3090,24GB的显存甚至让我没办法完全放得下整个模型,更何况还需要进行训练。因此,我选择使用流水线并行,它可以认为是一种更高效的模型并行方法,将模型按层的粒度进行拆分,然后按一定策略分配到不同的GPU上。

框架选择与相关技术博客调研

为什么采用DeepSpeed?现有的比较好的流水线并行框架主要是torch原生、DeepSpeed和英伟达家的Megatron LM。torch原生版本接口比较low-level,需要更多的手撸操作,尤其是device的分配,具体可以参考良睦路程序员的博客《给llama实现流水线并行》;而Megatron的话我并不熟悉,所以采用了相对熟悉的DeepSpeed框架的流水线并行实现。

大佬刘聪NLP写过一篇《大模型流水线并行(Pipeline)实战》的文章,也是采用的DeepSpeed框架。我与他的不同主要在于以下几点:

  1. 该博客主要针对LLM,由于加入了图像模态,MLLM相对来说更加复杂,MLLM的DeepSpeed流水线并行暂无好的中文blog

  2. 该博客的流水线并行是在A100上对LLM进行全参微调,而本文的MiniGPT4Qwen-14B仅仅微调了一个ViT+Qformer与LLM中间连接的linear层,微调的参数量极少

  3. 极其少量参数微调的流水线并行存在较多的实现上的坑(因为几乎99%的参数均freeze住了),如:仅GPU0上有requires_grad=True的参数,其他GPU均没有,导致报错

  4. 本文对于DeepSpeed的一些细节和源码有比较深层次的讨论

流水线并行的理论部分

要应用一个技术,首先需要大概知道它是在做什么,解决什么问题,我参考了BLOOM的博客,其中有对Pipeline Parallel有一个比较简单易懂的介绍,这里就主要将他的介绍进行翻译和一定程度的自我理解加工了。感兴趣的朋友可以去看一下原博客或者其中文翻译版本。

BLOOM原博客(英文):huggingface.co/blog/bloom-...

官方中文翻译版:huggingface.co/blog/zh/blo...

一句话解释流水线并行:模型在多个 GPU 上垂直 (即按层) 拆分,只有一个或多个模型层放置在单个 GPU 上。每个 GPU 并行处理流水线的不同阶段(只对于一条数据的forward来说,在GPU上是串行的,每个GPU负责该条数据forward的不同阶段),并处理 batch 的一部分数据。

类似下图将一个8层的模型均分到两张卡上:

现在,当数据从第 0 层传到第 3层,这就跟单 GPU 上的普通前向传播一样。但是当数据需要从第 3 层传到第 4 层时,它需要从 GPU0 传输到 GPU1,这会跨GPU进行数据传输,引入通信开销。然后第 4 到第 7 层又像普通模型一样,当第 7 层完成时,我们通常需要将数据发送回标签所在的第 0 层 (或者将标签发送到最后一层)。现在可以计算损失,然后使用优化器来进行更新参数了。

如果只是简单地串行,那么GPU会存在大量的闲置、等待时间,导致资源的浪费。类似上图的上半部分。下半部分为知名论文GPipe,就是来解决上述存在的资源闲置问题,将一个batch,切分成许多的micro_batch,然后进行流水线并行。对于同一个micro_batch来说,是在device0-3间串行的(如图中蓝框部分),而对不同micro_batch,在同一个device0上,运行了不同micro_batch的前向的同一阶段(如图中红框部分,device0就是第一阶段)。

数据并行 + 流水线并行(DP + PP)

这里重要的是要了解 DP rank 0 是看不见 GPU2 的, DP rank 1 是看不到 GPU3 的。对于 DP 而言,只有 GPU 0 和 1,并向它们馈送数据。GPU0 使用 PP 来"秘密地" 将它的一些负载卸载到 GPU2。同样地, GPU1 也会得到 GPU3 的帮助。

由于每个维度至少需要 2 个 GPU,因此这儿至少需要 4 个 GPU。

值得注意的是,ZERO系列均属于DP,而PP+DP的话,ZERO2和ZERO3是禁用的,最高只能开到ZERO-1

MiniGPT4Qwen14B的模型分析与层次拆分

沿用MiniGPT4的结构,MiniGPT4Qwen14B仅将语言模型替换成Qwen-14B-Chat,且只训练视觉端到LLM的线性投影层。为了进行流水线并行,我们需要对模型的进行层级别的拆分

LLM可以拆分成三个部分:Word Embedding、Transformer和最后映射回词表索引的LM Head,而Transformer可以非常容易地拆分成一个个Transformer Block,LM Head单独为一层即可。

至于Word Embedding,如果是仅有语言模态的LLM,直接单独一层即可,加入图像模态后,可以进一步与图像的编码器进行合并,如图中红框所示的Multimodal Tokenizer。我们知道,LLM Transformer处理的输入的基本单位是token,也就是说图像、文本都需要变成token输入进Transformer,图像是通过Patch Embedding + ViT + Q-former进行tokenize,而文本则是通过一个大量语料预训练好的BBPE分词器来进行tokenize。二者得到的特征embedding进行拼接,构成了LLM Transformer的输入(input_embeddings)。

接下来进行代码部分,我们现在有一个放在CPU上的model------ minigpt4qwen ,我们要抽出其中的组件,构成流水线并行的一个个layer。

Multimodal Tokenizer PipeLayer

首先,我们做最简单的,抽出Word Embedding层,得到我们文本的Tokenizer,写成一个nn.Module即可

python 复制代码
class EmbeddingPipeLayer(nn.Module):
    def __init__(self, model: Minigpt4Qwen):
        super().__init__()
        self.word_embeddings = model.llm_model.transformer.wte
        # enable_input_require_grads(self.word_embeddings)

    def forward(self, ipt):
        llm_tokens = ipt.long()
        return self.word_embeddings(llm_tokens)

然后是ViT + Q-Former + 线性投影层,构成视觉的Tokenizer,也非常简单直接:

python 复制代码
class VisionPipe(nn.Module):
    def __init__(self, model: Minigpt4Qwen):
        super().__init__()
        self.visual_encoder = model.visual_encoder
        self.ln_vision = model.ln_vision
        self.Qformer, self.query_tokens = model.Qformer, model.query_tokens

        self.maybe_autocast = model.maybe_autocast()
        self.enable_autocast = model.enable_autocast

        self.llm_proj = model.llm_proj

    def forward(self,ipt):
        image = ipt
        with (self.maybe_autocast if self.enable_autocast else contextlib.nullcontext()):
            image_embeds = self.visual_encoder(image)
            image_embeds = self.ln_vision(image_embeds)

        image_atts = torch.ones(image_embeds.size()[:-1], dtype=torch.long).to(image_embeds.device)

        bs = image.size(0)

        query_tokens = self.query_tokens.expand(image_embeds.shape[0], -1, -1)
        query_output = self.Qformer.bert(
            query_embeds=query_tokens,
            encoder_hidden_states=image_embeds,
            encoder_attention_mask=image_atts,
            return_dict=True,
        )

        inputs_llm = self.llm_proj(query_output.last_hidden_state[:,:query_tokens.size(1),:])

        return inputs_llm

在得到视觉和文本的Tokenizer之后,即可得到多模态的Tokenizer。需要注意的是,我们这里需要构建LLM Transformer的输出形式,我们知道,Transformer Block具有同质化的特性,即:输入和输出的形式完全相同,所以我们在这里构建好,后续的构建Transformer Block的输出形式就非常方便了。

对于LLM Transformer Block来说,需要以下几个部分的输入:

  • input_embeds: 即输入的tokens的特征向量
  • attention_mask:next token prediction的关键,MLLM会多一些可见的视觉tokens
  • targets:计算next token prediction损失的目标,也就是QA中的Answer(Question和视觉tokens的target index = ignore_index(也就是-100),不参与计算loss)
  • rotary_pos_emb_list:ROPE旋转位置编码,每一层Transformer Block的输入tokens在计算QKV时都需要加入位置编码,引入位置信息
  • position_ids:直接的位置信息,比如['I','love','you']对应的位置就是[0,1,2]
  • output_shape:即输出的张量尺寸

所以在TokenizerPipeLayer里除了进行视觉和文本的token化,拼接得到input_embeds,还需要准备其他的Transformer Block需要的输入内容。

p.s.:这里可以暂时不care为什么 attention_mask , rotary_pos_embed_list 等内容都要requires_grad设为True,以及所有的输入都要转换成torch.Tensor的形式,这和DeepSpeed流水线并行的相关要求(协议)有关。

python 复制代码
class TokenizerPipeLayer(nn.Module):
    def __init__(self, model:Minigpt4Qwen):
        super().__init__()
        self.replace_image_token_id = model.replace_image_token_id

        self.visionpipe = VisionPipe(model)
        self.wtepipe = EmbeddingPipeLayer(model)

        self.drop = model.llm_model.transformer.drop

        self.config = model.llm_model.transformer.config
        self.use_dynamic_ntk = model.llm_model.transformer.use_dynamic_ntk
        self.llm_training = model.llm_model.transformer.training

        # rope + ntk
        self.rotary_emb = model.llm_model.transformer.rotary_emb

        # rope+ntk related func
        self.get_ntk_alpha = model.llm_model.transformer.get_ntk_alpha
        # self.get_head_mask = model.llm_model.transformer.get_head_mask

    def forward(self,ipt):
        image, llm_tokens, targets, attention_mask = ipt
        inputs_llm = self.visionpipe(image)

        device = inputs_llm.device

        replace_image_idxs = torch.where(llm_tokens == self.replace_image_token_id)
        inputs_embeds = self.wtepipe(llm_tokens) # B, L, C
        _,_,channels = inputs_embeds.shape

        inputs_embeds = inputs_embeds.clone()
        inputs_embeds[replace_image_idxs[0],replace_image_idxs[1]] = inputs_llm.view(-1,channels).to(inputs_embeds.dtype)

        # rope + ntk
        # get rotary_pos_emb_list
        input_shape = inputs_embeds.size()[:-1]
        position_ids = torch.arange(
                0,
                input_shape[-1],
                dtype=torch.long,
                device=device,
            )
        position_ids = position_ids.unsqueeze(0).view(-1, input_shape[-1])

        kv_seq_len = inputs_embeds.size()[1]
        if self.llm_training or not self.use_dynamic_ntk:
            ntk_alpha_list = [1.0]
        else:
            ntk_alpha_list = []
            ntk_alpha = self.get_ntk_alpha(kv_seq_len)
            ntk_alpha_list.append(ntk_alpha)
        self.rotary_emb._ntk_alpha_cached_list = ntk_alpha_list
        ntk_alpha = ntk_alpha_list[0]
        rotary_pos_emb_list = self.rotary_emb(kv_seq_len, ntk_alpha=ntk_alpha)
        rotary_pos_emb_list = torch.stack(rotary_pos_emb_list,dim=0)
        # print(rotary_pos_emb_list);exit(0)

        inputs_embeds = self.drop(inputs_embeds)
        output_shape = input_shape + (inputs_embeds.size(-1),)
        output_shape = torch.tensor(output_shape,device="cuda")

        batch_size = inputs_embeds.shape[0]
        if attention_mask is not None:
            if batch_size <= 0:
                raise ValueError("batch_size has to be defined and > 0")
            attention_mask = attention_mask.view(batch_size, -1)
            attention_mask = attention_mask[:, None, None, :]
            attention_mask = attention_mask.to(dtype=self.wtepipe.word_embeddings.weight.dtype)
            attention_mask = (1.0 - attention_mask) * torch.finfo(self.wtepipe.word_embeddings.weight.dtype).min

        rotary_pos_emb_list.requires_grad_(True)
        attention_mask.requires_grad_(True)

        return inputs_embeds, attention_mask, targets, rotary_pos_emb_list, position_ids, output_shape

QwenBlockPipeLayer

由于TokenizerPipeLayer中准备好了Transformer Block所需的所有内容,这部分就非常好写啦,一个forward就搞定~

python 复制代码
class QwenBlockPipeLayer(torch.nn.Module):
    def __init__(self, model: Minigpt4Qwen, layer_idx):
        super().__init__()
        self.layer = model.llm_model.transformer.h[layer_idx]
        self.layer_idx = layer_idx

    def forward(self, ipt):
        inputs_embeds, attention_mask, targets, rotary_pos_emb_list, position_ids, output_shape = ipt
        # print("grad: ", inputs_embeds.requires_grad)
        inputs_embeds = self.layer(inputs_embeds, rotary_pos_emb_list=[[rotary_pos_emb_list[0],rotary_pos_emb_list[1]]],
                    attention_mask=attention_mask,
                    head_mask=None)[0]
        return inputs_embeds, attention_mask, targets, rotary_pos_emb_list, position_ids, output_shape

LM Head PipeLayer

首先,在所有Transformer Block计算完毕后,需要计算最后一个Norm层,来归一化输出

python 复制代码
class FLNPipeLayer(torch.nn.Module):
    def __init__(self, model: Minigpt4Qwen):
        super().__init__()
        self.final_layernorm = model.llm_model.transformer.ln_f

    def forward(self, ipt):
        inputs_embeds, attention_mask, targets, rotary_pos_emb_list, position_ids, output_shape = ipt
        inputs_embeds = self.final_layernorm(inputs_embeds)
        inputs_embeds = inputs_embeds.view(list(output_shape)).contiguous()
        # print(inputs_embeds)
        return inputs_embeds, targets

然后,直接经过一个LM Head层即可:

python 复制代码
class LMPipeLayer(torch.nn.Module):
    def __init__(self, model: Minigpt4Qwen):
        super().__init__()
        self.lm_head = model.llm_model.lm_head

    def forward(self, ipt):
        hidden_states, labels = ipt
        logits = self.lm_head(hidden_states)
        # print(logits)
        return logits, labels

LossPipeLayer

最后,我们还需要计算next token prediction的损失函数,返回一个最终的损失,工程实现上就是一个seq_length维度上的移位操作 + CrossEntropyLoss计算即可

python 复制代码
class LossPipeLayer(torch.nn.Module):
    def __init__(self, model: Minigpt4Qwen):
        super().__init__()

    def forward(self, ipt):
        logits, labels = ipt
        # print(logits.size());print(labels.size());exit(0)

        shift_logits = logits[..., :-1, :].contiguous()
        shift_labels = labels[..., 1:].contiguous()

        bs = shift_labels.size(0)
        loss_fct = nn.CrossEntropyLoss()
        loss = loss_fct(shift_logits.reshape(-1, shift_logits.size(-1)), shift_labels.reshape(-1))
        # print(loss)
        return loss, bs

至此,我们将MiniGPT4Qwen14B模型拆分成了一个TokenizerPipeLayer、数个QwenBlockPipeLayer、一个FLNPipeLayer、一个LMPipeLayer以及最后计算next token prediction损失的LossPipeLayer。 方便后续的流水线并行进行层级别的分配。

DeepSpeed流水线并行

上面我们获得了拆分后的layers,接下来我们需要按照计算的顺序将他们排序组合起来,得到一个有序的List,来获得我们的流水线模型,同时也需要利用DeepSpeed定义的一些接口和规则、协议,来完善整个流水线并行的训练流程。

PipelineModule的使用

首先,我们需要获得一个按照forward计算顺序排序的layer list,每个layer都采用deepspeed的LayerSpec类进行封装

from deepspeed.pipe import LayerSpec

LayerSpec用法:

args_1: 指定该层的nn.Module class

args_2~args_n: 输入该class的__init__函数的参数

python 复制代码
def get_model(model):
    layers = [LayerSpec(TokenizerPipeLayer,model=model),
            *[LayerSpec(QwenBlockPipeLayer, model=model, layer_idx=idx) for idx in
                range(model.llm_model.transformer.config.num_hidden_layers)],
            LayerSpec(FLNPipeLayer, model=model),
            LayerSpec(LMPipeLayer, model=model),
            LayerSpec(LossPipeLayer, model=model),
            LayerSpec(IndentityPipeLayer,model=model)]
    return layers

得到这样的layers list后,就可以利用DeepSpeed的PipelineModule来得到整个模型

python 复制代码
model = PipelineModule(
            layers=get_model(model),
            num_stages=args.num_stages,
            partition_method='uniform'
         )

用法:

  • layers:一个按forward计算顺序排序的List[LayerSpec]类型的输入

  • num_stages:一整个模型会被并行在几个GPU上

  • partition_method:如何将layers划分到这些gpus上(分配策略)

    • uniform:layers按顺序尽可能均匀地(在层数上) 分摊到各卡

    • parameters:按requires_grad为True的参数量进行划分

      • 只有requires_grad为True的参数会被计数
      • 然后按可训练的参数量,均匀地分摊到各卡
      • 适合全参训练的情景
    • 指定type:指定的type放在1卡,其他放在0卡

最后,使用deepspeed.initialize得到DeepSpeedEngine

python 复制代码
engine, _, _, _ = deepspeed.initialize(
                        model=model,
                        config=OmegaConf.to_container(ds_cfg),
                        model_parameters=[p for p in model.parameters() if p.requires_grad],
                    )

极少可训练参数带来的挑战

由于MiniGPT4Qwen14B仅训练一个从视觉端投射到LLM的线性投影层,可训练参数仅有3~4M,99%以上的参数都是freeze住的,这涉及到一个问题:

linear层只在GPU0上,GPU1上没有可训练参数,导致报错:

ValueError: optimizer got an empty parameter list

AttributeError: 'DeepSpeedCPUAdam' object has no attribute 'ds_opt_adam'

我在DeepSpeed的repo里提了issue:github.com/microsoft/D...

这个坑需要从两个方面去填掉:

  1. 修改PipelineModule的partition_method:选择"uniform"而非"parameters"

在我的阅读源码和实验后发现,"parameters"的划分方式是根据可训练也就是requires_grad为True的参数来计算的,由于这里只有不到1%的可训练参数,导致所有的层都在一张GPU上,其他GPU上均没有层分配

  1. 在最后加入1M的占位参数,不参与任何运算

细心的朋友在上面的get_model函数注意到了一个没见过的层:IndentityPipeLayer,它就是个占位层,实现如下:

python 复制代码
class IndentityPipeLayer(nn.Module):
    def __init__(self, model: Minigpt4Qwen):
        super().__init__()
        self.occupy = nn.Linear(1000,1000,bias=False)
        nn.init.constant_(self.occupy.weight,0.)
    
    def forward(self,ipt):
        loss, bs = ipt
        # zero_in = torch.zeros((bs,self.occupy.in_features),device='cuda')
        # return loss + 0. * self.occupy(zero_in).sum()
        return loss

这样就使得GPU1上也有1M的可训练参数:

python 复制代码
GPU0 Trainable Params: 3937280
GPU1 Trainable Params: 1000000

跨GPU传递数据的踩坑

在从GPU0到GPU1传递数据时有两个坑:

  1. type上:只允许传递torch.Tensor类型的数据

  2. requires_grad上:阅读源码发现有一些detach()操作等,导致很可能会报错,说有Tensor没有grad_fn和梯度之类的

这些问题的解决都在TokenizerPipeLayer上,比如:将rotary_pos_emb_list、output_shape参数换成torch.Tensor类型;将rotary_pos_emb_list、attention_mask的requires_grad设为True等等,都是报错之后去填坑的解决措施 (哭死,太难debug了啊啊啊啊)

DataLoader的设计

首先,DeepSpeed的DataLoader的每一个iter的数据需要是一个二元祖形式(Tuple[Tensor,Tensor]),0号位是要传递的数据,1号位是最后计算损失的labels

这里labels其实在我们的实现上并没有用到,因为我们自己实现了一个LossPipeLayer,还有一种方式是给PipelineModule类输入一个参数loss_fn,这样就会拿PipelineModule运算到最后的输出,和这个1号位的labels按照loss_fn计算损失,这种实现我没有去尝试。

如果loss_fn是None,那么这个1号位的labels其实是用不到的。总之,不管你用不用,在设计DataLoader的collate_fn,或者设计DataLoader的Dataset的时候,都需要按这个协议,返回一个Tuple[Tensor,Tensor]的(inputs, labels)二元祖

DeepSpeed源码如下:(还有我在DeepSpeed上自问自答的issue:github.com/microsoft/D...

可以看到,第一个stage是看0号位的数据,最后一个stage需要1号位的label

按照这个要求,可以参考的collate_fn代码如下:

python 复制代码
def collate_fn_minigpt4qwen(batch,preprocess_func):
    image_list, conversation_list = [], []

    for sample in batch:
        image_list.append(sample["image"])
        conversation_list.append(sample["conversations"])

    new_batch = \
        {
            "image": torch.stack(image_list, dim=0),
            "conversations": conversation_list,
        }
    data_dict = preprocess_func(new_batch['conversations'])

    return ((new_batch['image'], data_dict['input_ids'],data_dict['labels'],data_dict['attention_mask']),
                data_dict['labels']
        ) # Tuple[Tuple[Tensor], Tensor]

然后,流水线并行的DataLoader有其独特的Sampler设计,再放一遍DP+PP的图,可以看到图中GPU0和GPU2共享一个rank,他们的Dataloader得到的数据应该是完全一致的,而GPU1和GPU3是一致的,所以,Sampler和DataLoader的代码如下:

python 复制代码
g = torch.Generator()
sampler = torch.utils.data.distributed.DistributedSampler(
                datasets['train'],
                num_replicas=engine.dp_world_size, # 按上图例子,这里应该是2 而不是4
                rank=engine.mpu.get_data_parallel_rank(), # 值在[0,1]中,只有这2个值
                shuffle=False
            )

train_dataloader = DataLoader(datasets['train'],
                        shuffle=False,
                        drop_last=True,
                        batch_size=ds_cfg.train_micro_batch_size_per_gpu,
                        generator=g,
                        sampler=sampler,
                        collate_fn=collate_fn_minigpt4qwen_func,
                    )

为了防止iteration到最后,需要回头开头,还需要:

python 复制代码
train_dataloader = deepspeed.utils.RepeatingLoader(train_dataloader)

为了防止每个epoch的batches数据完全一致,还需要在每个epoch开头对sampler进行set_epoch操作:

python 复制代码
for epoch in range(cfg.run_cfg.max_epoch):
    sampler.set_epoch(epoch)

    train_iter = iter(train_dataloader)
    for cur_step in range(num_update_steps_per_epoch):
        step = cur_step + epoch * num_update_steps_per_epoch
        with (torch.cuda.amp.autocast(dtype=model_dtype,cache_enabled=False) if model_dtype != torch.float32 else contextlib.nullcontext()):
            loss = engine.train_batch(data_iter=train_iter)

        print("step = {}, loss = {}".format(step, loss.item()))

示例、总结与展望

先放两个14B模型的对话示例吧:

可以看到,引入14B的Qwen-LLM模型作为底座之后,模型的对话性能好了很多,更会遵循指令,更会"说人话"啦。

总而言之,MiniGPT4Qwen14B对语言模型进行了Scale Up,采用Qwen-14B-Chat模型作为底座,以获得更好的对话体验。值得一提的是,为了能在3090上训练14B~15B的模型(不进行量化操作),MiniGPT4Qwen14B选择采用DeepSpeed的流水线并行技术。

然而,在训练过程中,我发现将ViT+Q-former仅通过一个linear层对齐到14B的LLM上的难度,比对齐到7B的LLM上的难度要高许多。 在MiniGPT4Qwen中我仅用10个甚至5个epoch就可以完成比较好的对齐,而在MiniGPT4Qwen14B的模型上,我使用了20个epoch才差强人意地对齐上。

这也比较符合直觉,因为14B的LLM的特征维度更高,语义更丰富,更抽象,Vision端确实更加难以对齐。MiniGPT4Qwen14B使用BLIP2的ViT和Qformer,以及冻结的LLM,在不经过任何额外预训练的情况下,仅使用了18.8K的数据,微调3~4M的参数就想得到一个良好的模态对齐,显然是比较困难的。要想获得更好的MLLM,需要更多的预训练对齐数据、指令微调数据,微调更多的Vision端参数以保证对齐(同时还要尽可能不让视觉的特征抽取能力有所损失,所以ViT一般冻住)。当然,也需要更好的"ViT" (不一定是Vision Transformer,但要更好更全面的视觉特征),现阶段的视觉编码器发展很大程度限制了MLLM的进步,LVM或许必须要来了。

相关推荐
数维学长9869 小时前
【Manus资料合集】激活码内测渠道+《Manus Al:Agent应用的ChatGPT时刻》(附资源)
人工智能·chatgpt
beolus12 小时前
DeepSeek-R1-Distill-Qwen-1.5B基于MindIE推理实践
llm
ππ记录14 小时前
Manus全球首个通用Agent,Manus AI:Agent应用的ChatGPT时刻
人工智能·chatgpt·manus详细介绍·manus介绍·manus详细应用·manus教程·manus详情介绍
Loocor15 小时前
DB2LLM | 传统数据库链接大语言模型的最小化原型
llm
隔窗听雨眠16 小时前
DeepSeek 与 ChatGPT的主要区别
人工智能·chatgpt·deepseek
MoonOut16 小时前
LLM · RL | Plan4MC:使用有向无环图 high-level planning + 基于 RL 执行 low-level policy
llm
青梅主码19 小时前
DeskTime最新研究显示:ChatGPT 成全球职场 AI 工具之王,印度使用率高达 92%!
chatgpt
walker_sunxy19 小时前
AI 漫画生成器:从收集热点事件到生成漫画
llm
我码玄黄19 小时前
大模型时代,为什么模型都是多少B?
人工智能·llm
X204620 小时前
我的新开源Markify!专为 LLM 优化的开源文档解析神器,轻松破解 PDF 难题!融合MinerU和markitdown!
人工智能·llm