Open18

Qwen2-VL を llama.cpp で動かしたいメモ

syoyosyoyo
  • Conv3D
  • patch embed
  • RoPE(Qwen2VL はちょっと特殊?)
  • Dynamic resolution(model 内で画像リサイズ?)

あたりの pytorch(python)実装を C++ 移植すればいけるか?

syoyosyoyo

patch_size については 14x14 pixel. これを隣接する 2x2 でまとめて MLP で処理して 1 つの token に圧縮する(実質 28x28 pixels が 1 token)

2x2 tokens を 1 つに圧縮するのは, Visual tokens の数を減らすため.

syoyosyoyo

Multimodal Rotary Position Embedding (M-RoPE)

RoPE を temporal, width, height に分解する.
text input の場合は位置(width のみ利用かな?)で 1D RoPE
image の場合は width, height 利用
video の場合はさらに temporal 利用

テキストは対角で扱うなどしてテキストと画像(と video)の共存を可能とする.
元のアイデアは

https://zenn.dev/syoyo/scraps/83bdc1b7b62883

参照

syoyosyoyo

Unified Image and Video Understanding

画像と動画をミックスして扱えるように,
video は 2 frames/秒で抽出して, Conv3D で depth 2 で処理
画像は同じフレームとして処理(つまり depth 2 の Conv3D で処理するのは変わらない)

長い動画については, 解像度を調整して, 最大 token 数が 16384 までになるようにしている

syoyosyoyo

https://github.com/huggingface/transformers/issues/25199

https://discuss.huggingface.co/t/is-llama-rotary-embedding-implementation-correct/44509

rorate_half . 論文の式だと

[q0, q1, q2, q3, ...] -> [q1, -q0, q3, -q2]

になるのが期待だが, 実際は

[-q2, -q3, q0, q1]

となる. 半分ずらし + 入力の後ろ半分は negate して前方にもってくる.
なんか他のところで multiply とかしているのでこうなっているっぽい.

syoyosyoyo

conv3d は deth 2(2 枚の画像フレームを convolve)なので, 2 つの conv2d で表現できる
あとは vol2col 相当を実装して matmul で処理でもいいかも

syoyosyoyo

Multimodal RoPE パラメータの mrope_section (rope_scale)は [16, 24, 24] になっている.
なぜこの分割なのかは不明.

LLM 側の Attention は nheads 12, hidden_size 1536. したがって head_dim = hidden_size / nheads = 128.

multimodal rope では mrope_section * 2 = [16, 24, 24, 16, 24, 24] で, sum = 128(= head_dim) になるようにして分割している.

https://github.com/huggingface/transformers/blob/f38531619ddff23a510d5f7ccbc257a1bb1a3cb7/src/transformers/models/qwen2_vl/modeling_qwen2_vl.py#L235

syoyosyoyo

Vision encoder 側の Attention の MLP では, activation は QuickGELU を使っている

syoyosyoyo

Qwen2VLRotaryEmbedding

https://github.com/huggingface/transformers/blob/f38531619ddff23a510d5f7ccbc257a1bb1a3cb7/src/transformers/models/qwen2_vl/modeling_qwen2_vl.py#L107

multimodal rope 用と通常 rope(LLM part)で同じ class を使い回ししているためわかりずらい.
mrope の場合はこの結果に対して独自処理を適用(apply_multimodal_rotary_pos_emb)

また Vision encoder 側の rope は VisionRotaryEmbedding + apply_rotary_pos_emb_vision を利用

syoyosyoyo

PatchEmbed

      (patch_embed): PatchEmbed(
        (proj): Conv3d(3, 1280, kernel_size=(2, 14, 14), stride=(2, 14, 14), bias=False)
      )

Conv3d を使っているが, depth 2 なので Conv2d を二回適用に分解することは容易.

syoyosyoyo

VisionModel(VisionEncoder) の処理フロー

  • PatchEmbed
  • Position Embedding(RoPE)
  • VisionBlock(Attention + MLP)
  • PatchMerger
syoyosyoyo

llama.cpp での qwen2 は以下の構成になっているので, qwen2-vl の LLM part の tensor name もこれに合わせる

          LLM_ARCH_QWEN2,
          {
              { LLM_TENSOR_TOKEN_EMBD,      "token_embd" },
              { LLM_TENSOR_OUTPUT_NORM,     "output_norm" },
              { LLM_TENSOR_OUTPUT,          "output" },
              { LLM_TENSOR_ATTN_NORM,       "blk.%d.attn_norm" },
              { LLM_TENSOR_ATTN_Q,          "blk.%d.attn_q" },
              { LLM_TENSOR_ATTN_K,          "blk.%d.attn_k" },
              { LLM_TENSOR_ATTN_V,          "blk.%d.attn_v" },
              { LLM_TENSOR_ATTN_OUT,        "blk.%d.attn_output" },
              { LLM_TENSOR_FFN_NORM,        "blk.%d.ffn_norm" },
              { LLM_TENSOR_FFN_GATE,        "blk.%d.ffn_gate" },
              { LLM_TENSOR_FFN_DOWN,        "blk.%d.ffn_down" },
              { LLM_TENSOR_FFN_UP,          "blk.%d.ffn_up" },
          },