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

model weight 構成
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
これは config.json
の vision_config
には記載されておらず, ソースコードを見ないとわからないのでややこしい
(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, ... と番号を割り振っていく)

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)
)
)
)

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

VisionSdpaAttention は Sdpa(Scaled dot product attention) Attention. 要はフツーの Attention

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

VisionRotaryEmbedding
通常の rope.
この時点(VisionEncoder)では m-rope https://zenn.dev/syoyo/scraps/83bdc1b7b62883 関連は特に考慮はなし
(m-rope は LLM part で考慮)

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

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

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

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

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)

各モジュールは
from transformers.models.qwen2_5_vl.modeling_qwen2_5_vl import Qwen2RMSNorm
のような感じで import できるので, これでダミーデータ流すなどして C++ 実装と動作確認する