专栏名称: 自动驾驶之心
自动驾驶开发者社区,关注计算机视觉、多维感知融合、部署落地、定位规控、领域方案等,坚持为领域输出最前沿的技术方向!
目录
相关文章推荐
幸福东台  ·  22:00至第二天8:00,禁止! ·  19 小时前  
安全学习那些事儿  ·  国家标准《互联网金融个人网络消费信贷 ... ·  2 天前  
安全学习那些事儿  ·  国家标准《互联网金融个人网络消费信贷 ... ·  2 天前  
51好读  ›  专栏  ›  自动驾驶之心

多模态模型(VLM)部署方法抛砖引玉

自动驾驶之心  · 公众号  ·  · 2024-09-30 07:30

正文

作者 | oldpan  编辑 | oldpan博客

点击下方 卡片 ,关注“ 自动驾驶之心 ”公众号

戳我-> 领取 自动驾驶近15个 方向 学习 路线

>> 点击进入→ 自动驾驶之心 模型部署 技术交流群

本文只做学术分享,如有侵权,联系删文


去年年初LLM刚起步的时候,大模型的部署方案还不是很成熟,如今仅仅过了一年多,LLM部署方案已经遍地都是了。

而多模态模型相比大语言模型来说,发展的还没有很“特别”成熟,不过由于两者结构很相似,LLMs的经验还是可以很好地利用到VLMs中。

本篇文章中提到的多模态指的是视觉多模态,即VLM(Vision Language Models)。

以下用一张图展示下简单多模态模型的运行流程:

  • Text Embeddings即文本输入,就是常见LLM中的输入;
  • 而Multomode projector则是多模态模型额外一个模态的输入,这里指的是视觉输入信息,当然是转换维度之后的;

将这个 转换维度之后 的视觉特征和Text Embeddings执行concat操作合并起来,输入decoder中(例如llama)就完成推理流程了;

Multomode projector负责将原始的图像特征转换下维度,输出转换后的图像特征;所以有个中文叫投射层,这个名字有点抽象,理解就行,其实就是个mlp层,转换下视觉特征的维度

多模态运作流程 from https://huggingface.co/blog/zh/vlms

引入VLM

VLM就是 视觉encoder加上语言decoder ,这么说可能有点抽象,我们先简单回顾下transformer的基本结构:

from huggingface

由编码器和解码器组成,最开始的transformer主要是处理机器翻译任务,结构是encoder+decoder。除了这个结构,还有一些其他结构适用于各种任务,比如

  • Encoder-only models:适用于需要理解输入的任务,例如句子分类和命名实体识别。
  • Decoder-only models:适用于生成性任务,如文本生成。
  • Encoder-decoder models or sequence-to-sequence models:适用于需要输入的生成性任务,例如翻译或摘要。

我们常见的llama属于Decoder-only models,只有decoder层;bert属于encoder-only;T5属于encoder-decoder。llama不多说了,BERT 是一种仅包含编码器的模型,它通过学习双向的上下文来更好地理解语言的深层含义。

BERT 通常用于理解任务,如情感分析、命名实体识别、问题回答等。T5 模型包括编码器和解码器,适用于同时需要理解和生成语言的任务。T5 被设计来处理各种“文本到文本”的任务,比如机器翻译、文摘生成、文本分类等。这种结构允许模型首先通过编码器理解输入文本,然后通过解码器生成相应的输出文本。基础知识就过到这里,我们先说enc-dec结构。

Encoder-Decoder (Enc-Dec)

注意,Enc-Dec模型 需要区别于多模态模型 ,目前TensorRT-LLM官方支持的enc-dec模型如下:

  • T5 [1]
  • T5v1.1 [2] and Flan-T5 [3]
  • mT5 [4]
  • BART [5]
  • mBART [6]
  • FairSeq NMT [7]
  • ByT5 [8]

第一个就是T5也就是上述提到的编码器+解码器结构。需要注意这些模型都是基于Transformer架构的自然语言处理(NLP)模型,区别于多模态模型,这些模型主要是处理文本,算是单模态。

VLM or multimodal

而多模态除了本文,就带上了图像:

VILA架构和训练流程

VILA [9] 是nvidia推出的一个视觉语言模型,上图很清楚地介绍了其推理和训练流程。我知道的一些多模态模型有(这些模型来自lmdeploy官方github介绍):

  • LLaVA(1.5,1.6) (7B-34B)
  • InternLM-XComposer2 (7B, 4khd-7B)
  • QWen-VL (7B)
  • DeepSeek-VL (7B)
  • InternVL-Chat (v1.1-v1.5)
  • MiniGeminiLlama (7B)
  • CogVLM-Chat (17B)
  • CogVLM2-Chat (19B)
  • MiniCPM-Llama3-V-2_5

这里我只简单介绍下llava,毕竟llava是较早的做多模态的模型,之后很多多模态的架构和llava基本都差不多。剩下的多模态模型大家可以自行查阅,结构基本都类似。

LLAVA

LLaVA(2304) [10] 作为VLMs的代表性工作,号称只需要几个小时的训练,即可让一个LLM转变为VLM:

image

训练方式比较简单,主要操作是将视觉图像视作一种“外语”(相比于之前纯nlp,图像可以当做额外输入的外语),利用vision-encoder和 projection [11] 将“图像翻译成文本信号”,并微调LLM从而可以适应图像任务,我们简单看下其推理代码:

llava的整体运行过程

其中processor就是图像预处理,处理后得到的input为:inputs.pixel_values,其shape是torch.Size([1, 3, 336, 336])。然后进入generate阶段:

image

第一步(stage-1)是执行encoder部分:

  • input_ids(torch.Size([1, 17])) -> input_embeds(torch.Size([1, 17, 4096]))
  • pixel_values(torch.Size([1, 3, 336, 336]))经过视觉模型 输出视觉特征(torch.Size([1, 576, 1024]))
  • 视觉特征经过投影层(mlp)输出最终的图像特征(torch.Size([1, 576, 4096]))

第二步(stage-2)是执行文字embed和图像embed合并过程,也就是合并 inputs_embeds + image_features ,最终得到的inputs_embeds维度为 torch.Size([1, 592, 4096]) ,具体在llava中是 pre_prompt + image + post_prompt 一起输入进来,其中image的特征替换prompt中的标记,对应的token id为32000。

而这个 _merge_input_ids_with_image_features 函数的作用主要是将 image_features inputs_embeds 合并起来:在合并完两者之后,其余部分就和普通的decoder基本一致了。整体运行流程如下:


其他多模态模型拼 输入的方式和llava类似 ,比如 InternVLChat [12] ,输入的prompt是这样的:

image

转换为token_ids如下,其图像特征占位,img_context_token_id是92546:

image

上述prompt中的 之后会被实际的图像特征填上,目前只是占个位子。运行过程中的维度变化:

  • pixel_values.shape   torch.Size([1, 3, 224, 224])  ->  vision model -> torch.Size([1, 64, 2048])
  • input_ids   torch.Size([1, 293])  -> language model embed -> torch.Size([1, 293, 2048])

核心代码如下,需要注意这里是先占位再填(input_embeds[selected]=...)的形式,不是llava中concat的形式,最终实现效果是一样的。

image

最终也是将合并后的embeds送入language-decoder中。

包含cross-attention的多模态

上述介绍的多模态模型都是文本输入和图像输入转换为embeds合并后,输入language decoder中,这个decoder可以是常见的decoder-only models,比如llama。

还有些特殊的多模态模型的decoder部分会有cross-attention结构,比如meta推出的 nougat [13] 。其视觉特征不会和input_ids合并,而是单独输入decoder,在decoder中和文本特征进行cross attention:


这种结构相对稍微复杂一些。

部署方案

部署方案的话,我们参考主流开源(TensorRT-LLM不完全开源)框架来窥探下。虽然一些大厂有自己的LLM部署方案,不过技术其实也多是“借鉴”,大同小异。这几个LLM框架应该是用的比较多的:

  • TensorRT-LLM
  • vLLM
  • lmdeploy

目前这三个框架都支持VLM模型,只不过支持程度不同,我们先看trt-llm。

TensorRT-LLM

trt-llm中多模态部分在Multimodal示例中,如果是跑demo或者实际中使用trt-llm的python session跑的话,直接看官方的example即可:


如果我们更进一步,想要部署在生产环境,也是可以搞的。如果我们想要使用triton inference server部署的话,TensorRT-LLM对多模态模型的支持限于将cv-encoder和decoder分离开,搞成两个模型服务。

即通过ensemble或者tensorrt_llm_bls(python backend)的方式串起来,整体运行流程和在普通模型中是一致的(先输入image和text,然后两者经过tokenizer转换为token id,最终拼接和encoder输出特征图一并传入decoder中)。

视觉端

视觉端模型转换和普通CV模型一致,可以通过onnx的方式或者直接通过trt-python-api搭建。

以llava举例子,在TensorRT-LLM中,首先把视觉模型(encoder)使用Wrapper类套一层,其中:

  • self.vision_tower 是视觉模型,
  • multi_modal_projector 是投影层,将特征维度转换为和input_embeds相匹配的维度:

转换好之后,模型信息如下:

[I] ==== TensorRT Engine ====
    Name: Unnamed Network 0 | Explicit Batch Engine
    
    ---- 1 Engine Input(s) ----
    {input [dtype=float16, shape=(-1, 3, 336, 336)]}
    
    ---- 1 Engine Output(s) ----
    {output [dtype=float16, shape=(-1, 576, 4096)]}
    
    ---- Memory ----
    Device Memory: 56644608 bytes
    
    ---- 1 Profile(s) (2 Tensor(s) Each) ----
    - Profile: 0
        Tensor: input           (Input), Index: 0 | Shapes: min=(1, 3, 336, 336), opt=(2, 3, 336, 336), max=(4, 3, 336, 336)
        Tensor: output         (Output), Index: 1 | Shape: (-1, 576, 4096)
    
    ---- 398 Layer(s) ----

语言端

decoder端需要额外加入一个合并input_ids 和prompt_table_data的embedding层(这里称为PromptTuningEmbedding),其余的和普通llama一致:

image

通过设置 use_prompt_tuning来确定该decoder是否需要额外的 prompt_tuning 输入:

class LLaMAModel(Module):
    def __init__(self,
                 num_layers,
                 num_heads,
                 ...# 省略参数
                 use_prompt_tuning: bool = False,   # !!!
                 enable_pos_shift=False,
                 dense_context_fmha=False,
                 max_lora_rank=None)
:

        super().__init__()
        self.mapping = mapping
        self.use_prompt_tuning = use_prompt_tuning
        EmbeddingCls = PromptTuningEmbedding if use_prompt_tuning else Embedding

其中PromptTuningEmbedding的forward代码如下,这个使用trt-python-api搭出来的layer主要作用就是将input_ids和视觉特征prompt_embedding_table进行embed并且concat,和上述一开始提到的concat流程大差不差:

# PromptTuningEmbedding
def forward(self, tokens, prompt_embedding_table, tasks, task_vocab_size):
    # do not use '>=' because internally the layer works with floating points
    prompt_tokens_mask = tokens > (self.vocab_size - 1)

    # clip tokens in the [0, vocab_size) range
    normal_tokens = where(prompt_tokens_mask, self.vocab_size - 1, tokens)
    normal_embeddings = embedding(normal_tokens, self.weight.value,
                                  self.tp_size, self.tp_group,
                                  self.sharding_dim, self.tp_rank)

    # put virtual tokens in the [0, max_prompt_vocab_size) range
    prompt_tokens = where(prompt_tokens_mask, tokens - self.vocab_size, 0)

    # add offsets to match the concatenated embedding tables
    tasks = tasks * task_vocab_size

    # tasks: [batch_size, seq_len]
    # prompt_tokens: [batch_size, seq_len]
    prompt_tokens = prompt_tokens + tasks
    prompt_embeddings = embedding(prompt_tokens, prompt_embedding_table)

    # prompt_tokens_mask: [batch_size, seq_len] -> [batch_size, seq_len, 1]
    # combine the correct sources of embedding: normal/prompt
    return where(unsqueeze(prompt_tokens_mask, -1), prompt_embeddings,
                 normal_embeddings)

服务整合

服务整合其实很容易,只不过官方一开始并没有给出示例,只有在最近才在tutorial中给出实际例子:

  • https://github.com/triton-inference-server/tutorials/blob/main/Popular_Models_Guide/Llava1.5/llava_trtllm_guide.md [14]

其实我们可以很早发现 tensorrt_llm/config.pbtxt 中已经包含了这两个输入:

  • prompt_embedding_table
  • prompt_vocab_size

意味着在trt-llm的executor中包装着decoder-engine,可以接受这两个输入。而trt-llm提供的executor,在triton-trt-llm-backend去通过调用这个API实现inflight batching:

image
# inflight_batcher_llm/tensorrt_llm/config.pbtxt






请到「今天看啥」查看全文