Qwen2-VL を llama.cpp で動かしたいメモ
とりまここを参考に, 足りない op(Conv3d とか?) や関数を地道に llama.cpp(C++)で実装すればいけそうであるが...
動画については ffmpeg あたりで連番画像にデコードさせておくのを想定でよさそ?
- Conv3D
- patch embed
- RoPE(Qwen2VL はちょっと特殊?)
- Dynamic resolution(model 内で画像リサイズ?)
あたりの pytorch(python)実装を C++ 移植すればいけるか?
Naive dynamic resolution
Patch n' Pack: NaViT, a Vision Transformer for any Aspect Ratio and Resolution
patch_size については 14x14 pixel. これを隣接する 2x2 でまとめて MLP で処理して 1 つの token に圧縮する(実質 28x28 pixels が 1 token)
2x2 tokens を 1 つに圧縮するのは, Visual tokens の数を減らすため.
Multimodal Rotary Position Embedding (M-RoPE)
RoPE を temporal, width, height に分解する.
text input の場合は位置(width のみ利用かな?)で 1D RoPE
image の場合は width, height 利用
video の場合はさらに temporal 利用
テキストは対角で扱うなどしてテキストと画像(と video)の共存を可能とする.
元のアイデアは
参照
Unified Image and Video Understanding
画像と動画をミックスして扱えるように,
video は 2 frames/秒で抽出して, Conv3D で depth 2 で処理
画像は同じフレームとして処理(つまり depth 2 の Conv3D で処理するのは変わらない)
長い動画については, 解像度を調整して, 最大 token 数が 16384 までになるようにしている
Conv3D
3 channels(RGB) の画像(動画)を, 2(depth) x 14 x 14 kernel で畳み込む.
stride は kernel size と同じ.
padding 0, dilation 1 なので
つまり 14x14x2 の画素 patch が一つになる.
bias はなし
output_dim(embed_dim) は 1152
rorate_half . 論文の式だと
[q0, q1, q2, q3, ...] -> [q1, -q0, q3, -q2]
になるのが期待だが, 実際は
[-q2, -q3, q0, q1]
となる. 半分ずらし + 入力の後ろ半分は negate して前方にもってくる.
なんか他のところで multiply とかしているのでこうなっているっぽい.
ggml で custom op
conv3d は deth 2(2 枚の画像フレームを convolve)なので, 2 つの conv2d で表現できる
あとは vol2col 相当を実装して matmul で処理でもいいかも
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) になるようにして分割している.
Vision encoder 側の Attention の MLP では, activation は QuickGELU を使っている
Qwen2VLRotaryEmbedding
multimodal rope 用と通常 rope(LLM part)で同じ class を使い回ししているためわかりずらい.
mrope の場合はこの結果に対して独自処理を適用(apply_multimodal_rotary_pos_emb)
また Vision encoder 側の rope は VisionRotaryEmbedding + apply_rotary_pos_emb_vision を利用
VisionRotaryEmbedding
Vision encoder 側の RotaryEmbedding. フツーな感じ. dim は head_dim // 2
call での seq_len は max_grid_size
PatchEmbed
(patch_embed): PatchEmbed(
(proj): Conv3d(3, 1280, kernel_size=(2, 14, 14), stride=(2, 14, 14), bias=False)
)
Conv3d があるが, depth 2 なので Conv2d を二回適用に分解することは容易.
VisionModel(VisionEncoder) の処理フロー
- PatchEmbed
- Position Embedding(RoPE)
- VisionBlock(Attention + MLP)
- PatchMerger
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" },
},