Open12

qwen2-vl の VisonEncoder を自前 C++ ライブラリで動かしたいメモ

syoyosyoyo

model weight 構成

https://huggingface.co/Qwen/Qwen2.5-VL-3B-Instruct/blob/main/model.safetensors.index.json

VisionEncoder 部分は visual の prefix が付く

PatchEmbed

    "visual.patch_embed.proj.weight": "model-00001-of-00002.safetensors"

Attention

    "visual.blocks.0.attn.proj.bias": "model-00001-of-00002.safetensors",
    "visual.blocks.0.attn.proj.weight": "model-00001-of-00002.safetensors",
    "visual.blocks.0.attn.qkv.bias": "model-00001-of-00002.safetensors",
    "visual.blocks.0.attn.qkv.weight": "model-00001-of-00002.safetensors",
    "visual.blocks.0.mlp.down_proj.bias": "model-00001-of-00002.safetensors",
    "visual.blocks.0.mlp.down_proj.weight": "model-00001-of-00002.safetensors",
    "visual.blocks.0.mlp.gate_proj.bias": "model-00001-of-00002.safetensors",
    "visual.blocks.0.mlp.gate_proj.weight": "model-00001-of-00002.safetensors",

これも基本普通の Attention だが, FlashAttention 利用を考慮してか, qkv が一つの tensor になっているのが少しややこしい感じ. block 数(depth)は 32.

activation function は quick-GELU
https://github.com/huggingface/transformers/blob/86d7564611d21731fc004b4e79e472d48c4b0fec/src/transformers/models/qwen2_vl/configuration_qwen2_vl.py#L34

これは config.jsonvision_config には記載されておらず, ソースコードを見ないとわからないのでややこしい

https://huggingface.co/Qwen/Qwen2-VL-2B-Instruct/blob/main/config.json

(transformer のコードを見ないとわからない)

Merger(MLP)

    "visual.merger.ln_q.weight": "model-00001-of-00002.safetensors",
    "visual.merger.mlp.0.bias": "model-00001-of-00002.safetensors",
    "visual.merger.mlp.0.weight": "model-00001-of-00002.safetensors",
    "visual.merger.mlp.2.bias": "model-00001-of-00002.safetensors",
    "visual.merger.mlp.2.weight": "model-00001-of-00002.safetensors",

ふつう. activation function は素の GELU(faster-GELU や quick-GELU などではない)
ちなみに mlp.1 がないのはここに GELU activation function が入っているから
(pytorch では named tensor ではない場合, op/function の定義順に 0, 1, 2, ... と番号を割り振っていく)

syoyosyoyo

Model 構成

  Qwen2VLForConditionalGeneration(
    (visual): Qwen2VisionTransformerPretrainedModel(
      (patch_embed): PatchEmbed(
        (proj): Conv3d(3, 1280, kernel_size=(2, 14, 14), stride=(2, 14, 14), bias=False)
      )
      (rotary_pos_emb): VisionRotaryEmbedding()
      (blocks): ModuleList(
        (0-31): 32 x Qwen2VLVisionBlock(
          (norm1): LayerNorm((1280,), eps=1e-06, elementwise_affine=True)
          (norm2): LayerNorm((1280,), eps=1e-06, elementwise_affine=True)
          (attn): VisionSdpaAttention(
            (qkv): Linear(in_features=1280, out_features=3840, bias=True)
            (proj): Linear(in_features=1280, out_features=1280, bias=True)
          )
          (mlp): VisionMlp(
            (fc1): Linear(in_features=1280, out_features=5120, bias=True)
            (act): QuickGELUActivation()
            (fc2): Linear(in_features=5120, out_features=1280, bias=True)
          )
        )
      )
      (merger): PatchMerger(
        (ln_q): LayerNorm((1280,), eps=1e-06, elementwise_affine=True)
        (mlp): Sequential(
          (0): Linear(in_features=5120, out_features=5120, bias=True)
          (1): GELU(approximate='none')
          (2): Linear(in_features=5120, out_features=1536, bias=True)
        )
      )
    )

syoyosyoyo

Conv3D

動画は隣接2フレーム, 画像は同じものを使い2フレームにするで処理しているので, 実質 2 x Conv2d

syoyosyoyo

PatchEmbed

中身としては Conv3D で畳み込む(project)だけ.
RGB(3 channel) 画像の patch を 1280 次元の特徴量にする

syoyosyoyo

Qwen2VLVisionBlock

概ね通常の Transformer. qkv の weight が一つになっているのがちょっとやっかいなくらいか.
また MLP の activation function は QuickGELU

syoyosyoyo

PatchMerger

これもよくある MLP か.
activation function は通常の GELU(sigmoid)

syoyosyoyo

Conv3D, Conv2D は vol2col, im2col で matmul に落とし込めるので, 基本 matmul があればあとは Python コードを参考に C++ で書き直しすれば良いと思われる.

syoyosyoyo

nn.Linear

これも matmul で対応可能
weight を転置しておいてメモリアクセス改善の手はある

syoyosyoyo

Qwen2RMSNorm

中身は T5LayerNorm と同じとあるが..

class Qwen2RMSNorm(nn.Module):
    def __init__(self, hidden_size, eps=1e-6):
        """
        Qwen2RMSNorm is equivalent to T5LayerNorm
        """
        super().__init__()
        self.weight = nn.Parameter(torch.ones(hidden_size))
        self.variance_epsilon = eps

    def forward(self, hidden_states):
        input_dtype = hidden_states.dtype
        hidden_states = hidden_states.to(torch.float32)
        variance = hidden_states.pow(2).mean(-1, keepdim=True)
        hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
        return self.weight * hidden_states.to(input_dtype)

    def extra_repr(self):
        return f"{tuple(self.weight.shape)}, eps={self.variance_epsilon}"

variance の計算を mean(sqr(hidden_states)) だけとしている(平均 0 として sqr を計算している)

もうちょっと C っぽく書くとこんなかんか

y = weight *  x / sqrt(ave(x^2) + var_eps)
syoyosyoyo

各モジュールは

 from transformers.models.qwen2_5_vl.modeling_qwen2_5_vl import Qwen2RMSNorm

のような感じで import できるので, これでダミーデータ流すなどして C++ 実装と動作確認する