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或许必须要来了。

相关推荐
siliconstorm.ai2 小时前
阿里下场造“机器人”:从通义千问到具身智能,中国AI正走向“实体化”阶段
人工智能·自然语言处理·chatgpt·机器人·云计算
AI大模型3 小时前
利用腾讯混元大模型搭建Cherry Studio自有知识库,打造“智能第二大脑”
程序员·llm·agent
聚客AI4 小时前
系统提示的“消亡”?上下文工程正在重新定义人机交互规则
图像处理·人工智能·pytorch·语言模型·自然语言处理·chatgpt·gpt-3
Brianna Home7 小时前
从“码农”到“导演”:AI结对编程如何重塑软件工程范式
大数据·人工智能·深度学习·自然语言处理·chatgpt
三天哥17 小时前
演示和解读ChatGPT App SDK,以后Android/iOS App不用开发了?
人工智能·ai·chatgpt·aigc·openai·智能体·appsdk
viperrrrrrrrrr720 小时前
GPT系列模型-详解
人工智能·gpt·llm
美人鱼战士爱学习20 小时前
2025 AAAI HLMEA: Unsupervised Entity Alignment Based on Hybrid Language Models
chatgpt·知识图谱
大熊猫侯佩20 小时前
大内密探零零发之 iOS 密探神器 AI 大模型 MCP 服务开发记(下)
llm·ai编程·mcp
大熊猫侯佩20 小时前
大内密探零零发之 iOS 密探神器 AI 大模型 MCP 服务开发记(上)
llm·ai编程·mcp
302AI1 天前
体验升级而非颠覆,API成本直降75%:DeepSeek-V3.2-Exp评测
人工智能·llm·deepseek