論文概要(Abstract)
Qwen2-VLは、Alibaba Cloudが提案したVision-Language Model(VLM)であり、画像・動画を任意の解像度で処理できるNaive Dynamic Resolutionと、テキスト・画像・動画の位置情報を統一的にモデル化するMultimodal Rotary Position Embedding(M-RoPE)を導入している。2B・7B・72Bの3つのモデルサイズで提供され、72Bモデルは論文Table 2においてDocVQA 96.5%、MathVista 74.1%、TextVQA 99.1%を達成したと報告されている。
この記事は Zenn記事: Gemini 2.0マルチモーダルAPI実践ガイド:画像・音声・動画をPythonで統合処理する の深掘りです。
情報源
- arXiv ID: 2409.12191
- URL: https://arxiv.org/abs/2409.12191
- 著者: Peng Wang et al.(Alibaba Cloud)
- 発表年: 2024
- 分野: cs.CV, cs.CL
背景と動機(Background & Motivation)
GPT-4o、Claude 3.5 Sonnet、Gemini 1.5 Proなどのプロプライエタリモデルがマルチモーダルタスクで高い性能を示す一方、その技術的詳細は公開されていない。オープンソースのVLMでは、入力画像を固定サイズのタイルに分割する手法(InternVL2、LLaVA-OneVisionなど)が主流だが、タイル境界での情報断絶やトークン数の固定化といった課題がある。
著者らは、画像をタイルに分割せず全体をそのまま処理するNaive Dynamic Resolutionと、テキスト・画像・動画を統一的に扱う位置埋め込みM-RoPEの2つの機構により、これらの課題を解決するアプローチを提案している。
主要な貢献(Key Contributions)
- Naive Dynamic Resolution: 画像をタイル分割せず、解像度に応じてトークン数を動的に変化させる機構。28×28から1680×1680までの任意解像度を処理可能
- M-RoPE(Multimodal Rotary Position Embedding): RoPEを temporal・height・widthの3成分に分解し、テキスト・画像・動画の位置情報を統一的にモデル化
- スケーラブルなViT: 72Bモデルでは675Mパラメータのスケールアップ版ViTを使用し、知覚ボトルネックの解消を目指す
- 4.5兆トークン以上の訓練: 3段階の訓練パイプラインにより、文書理解・動画理解・多言語OCRなど幅広いタスクで高い性能を達成
技術的詳細(Technical Details)
アーキテクチャ
Qwen2-VLの全体構成は以下の3段パイプラインである。
1
入力画像/動画 → [Vision Encoder (ViT)] → [MLP Connector] → [LLM Backbone (Qwen2)]
論文Table 1に記載されたモデル構成を以下に示す。
| Model | LLM Params | ViT Params | Hidden Size | LLM Layers | Context Window |
|---|---|---|---|---|---|
| Qwen2-VL-2B | 1.54B | 600M | 1536 | 28 | 32768 |
| Qwen2-VL-7B | 7.07B | 600M | 3584 | 28 | 32768 |
| Qwen2-VL-72B | 72.7B | 675M | 8192 | 80 | 32768 |
2Bモデルと7BモデルはViT 600Mを共有し、72BモデルのみViTを675Mにスケールアップしている。著者らは、現行VLMの性能ボトルネックがViTの知覚能力にあると仮説を立て、ViTのスケールアップによる解消を試みている。
Naive Dynamic Resolution
入力画像の解像度を $r \times c$(height × width)とすると、visual tokenの生成過程は以下の通りである。
\[n_{\text{tokens}} = \frac{r}{28} \times \frac{c}{28}\]具体的には2段階の処理を行う。
- ViTパッチ化: ViT(patch size = 14×14)が画像を $\frac{r}{14} \times \frac{c}{14}$ 個のパッチに分割
- 2D空間プーリング: stride=2の2D平均プーリングで $\frac{r}{28} \times \frac{c}{28}$ 個のvisual tokenに圧縮
最終的に28×28ピクセルが1トークンに対応する。解像度の制約は以下の通りである。
- 最小: 28×28(4トークン保証のためリサイズ)
- 最大: 1680×1680(16384トークン上限)
- リサイズ時はアスペクト比を維持
従来のタイルベース手法との違いは、画像全体を分割せずそのまま処理する点にある。これによりタイル境界での情報断絶を回避できる。異なるサイズの画像はsequence packingによって1回のforward passでバッチ処理が可能である。
動画入力では、最小4フレーム・最大768フレームを抽出し、各フレームを画像と同一のパイプラインで処理する。
M-RoPE(Multimodal Rotary Position Embedding)
通常の1D RoPEがsequence indexの1次元のみで位置を符号化するのに対し、M-RoPEは位置IDを3成分に分解する。
\[\text{position\_id} = (p_{\text{temporal}}, p_{\text{height}}, p_{\text{width}})\]各モダリティでの割り当ては以下の通りである。
| モダリティ | temporal | height | width |
|---|---|---|---|
| テキスト | 順次インクリメント | 同左 | 同左 |
| 画像 | 直前のテキストと同値 | パッチ行位置 | パッチ列位置 |
| 動画 | フレームインデックス | パッチ行位置 | パッチ列位置 |
テキストトークンでは3成分が同一値をとるため、1D RoPEと数学的に等価となり、テキスト専用タスクでの性能劣化がない。
次元配分では、元のRoPEのD次元を3等分して各コンポーネントに $D/3$ 次元を割り当てる。これにより、height・widthの位置インデックスは $0$ から $N/D$($D=2$はViT compression ratio)までの範囲に収まり、1D RoPEの $0$ から $N$ と比較してposition index budgetが実質的に拡張される。
さらに、ViT内部の絶対位置埋め込みも2D RoPEに置き換えることで、訓練時に見ていない解像度への汎化性能を向上させている。
アルゴリズム
Naive Dynamic Resolutionのトークン数計算とM-RoPEの位置ID生成の実装例を以下に示す。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import torch
def calc_visual_tokens(height: int, width: int) -> tuple[int, int]:
"""画像解像度からvisual token数を計算
Args:
height: 画像の高さ(ピクセル)
width: 画像の幅(ピクセル)
Returns:
(token_h, token_w): 高さ方向・幅方向のトークン数
"""
VIT_PATCH_SIZE = 14
POOL_STRIDE = 2
COMPRESSION = VIT_PATCH_SIZE * POOL_STRIDE # 28
token_h = height // COMPRESSION
token_w = width // COMPRESSION
return token_h, token_w
def generate_mrope_ids(
text_len: int,
images: list[tuple[int, int]],
) -> torch.Tensor:
"""テキストと画像列に対するM-RoPE位置IDを生成
Args:
text_len: テキストトークン数
images: 各画像の(token_h, token_w)リスト
Returns:
position_ids: shape (3, total_seq_len)
[temporal, height, width]
"""
ids_t, ids_h, ids_w = [], [], []
pos = 0
for img_h, img_w in images:
# 画像トークンの位置ID
for h in range(img_h):
for w in range(img_w):
ids_t.append(pos) # temporal: 固定
ids_h.append(h) # height: 行位置
ids_w.append(w) # width: 列位置
pos += 1
# テキストトークンの位置ID(3成分同値)
for i in range(text_len):
ids_t.append(pos + i)
ids_h.append(pos + i)
ids_w.append(pos + i)
return torch.tensor([ids_t, ids_h, ids_w], dtype=torch.long)
実装のポイント(Implementation)
- トークン数の事前計算: 画像解像度から
(r//28) * (c//28)でトークン数を算出し、context window(32768トークン)との合計を事前に検証する必要がある。高解像度画像では数千トークンを消費するため、テキストや他画像との合計に注意が必要である - sequence packing: 異なる解像度の画像をパディングなしでバッチ処理できるが、実装時はattention maskの管理が複雑になる。HuggingFace Transformersの
Qwen2VLForConditionalGenerationクラスがこの処理を内部で行う - 動画フレーム数の制御: 768フレーム上限とcontext window 32768トークンの両方の制約を監視するコードを入れる必要がある。1フレームあたりのトークン数は解像度に依存するため、固定値での見積もりは不適切である
- Apache 2.0ライセンス: Qwen2-VLはApache 2.0で公開されており、商用利用が可能である
Production Deployment Guide
AWS実装パターン(コスト最適化重視)
Qwen2-VLはオープンソースモデルであり、セルフホスティングによるAPI構築が可能である。モデルサイズ(2B/7B/72B)とトラフィック量に応じた構成を以下に示す。コスト試算は2026年2月時点のAWS ap-northeast-1(東京)リージョン料金に基づく概算値であり、実際のコストはトラフィックパターンやバースト使用量により変動する。
| 構成 | トラフィック | モデル | インフラ | 月額概算 |
|---|---|---|---|---|
| Small | ~100 req/日 | Qwen2-VL-2B | Lambda + SageMaker Serverless | $80-200 |
| Medium | ~1000 req/日 | Qwen2-VL-7B | ECS Fargate + g5.xlarge | $400-1,000 |
| Large | 10000+ req/日 | Qwen2-VL-72B | EKS + g5.12xlarge Spot | $3,000-8,000 |
コスト削減テクニック:
- Spot Instances: g5系でオンデマンド比で最大70%削減(GPU Spotは割引率が低め)
- Reserved Instances: 1年コミットで最大40%削減
- SageMaker Serverless: アイドル時課金なしで低トラフィック時に有効
- vLLMによる推論最適化: Continuous Batchingで同一GPUのスループットを向上
Terraformインフラコード
Small構成(Serverless: Qwen2-VL-2B)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# SageMaker Serverless Endpoint for Qwen2-VL-2B
resource "aws_iam_role" "sagemaker_execution" {
name = "qwen2vl-sagemaker-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = { Service = "sagemaker.amazonaws.com" }
}]
})
}
resource "aws_iam_role_policy_attachment" "sagemaker_full" {
role = aws_iam_role.sagemaker_execution.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSageMakerFullAccess"
}
resource "aws_sagemaker_model" "qwen2vl_2b" {
name = "qwen2vl-2b-model"
execution_role_arn = aws_iam_role.sagemaker_execution.arn
primary_container {
image = "763104351884.dkr.ecr.ap-northeast-1.amazonaws.com/huggingface-pytorch-tgi-inference:2.1.1-tgi2.0-gpu-py310-cu121-ubuntu22.04"
model_data_url = "s3://${aws_s3_bucket.model.id}/qwen2-vl-2b/model.tar.gz"
environment = {
HF_MODEL_ID = "Qwen/Qwen2-VL-2B-Instruct"
MAX_INPUT_LENGTH = "4096"
MAX_TOTAL_TOKENS = "8192"
}
}
}
# S3 for model artifacts
resource "aws_s3_bucket" "model" {
bucket = "qwen2vl-model-artifacts"
}
resource "aws_s3_bucket_server_side_encryption_configuration" "model" {
bucket = aws_s3_bucket.model.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
}
}
# CloudWatch alarm for cost monitoring
resource "aws_cloudwatch_metric_alarm" "sagemaker_invocations" {
alarm_name = "qwen2vl-high-invocations"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "InvocationsPerInstance"
namespace = "AWS/SageMaker"
period = 3600
statistic = "Sum"
threshold = 500
alarm_actions = [aws_sns_topic.alerts.arn]
}
resource "aws_sns_topic" "alerts" {
name = "qwen2vl-cost-alerts"
}
Large構成(EKS + vLLM: Qwen2-VL-72B)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# EKS Cluster for Qwen2-VL-72B with vLLM
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 20.0"
cluster_name = "qwen2vl-inference"
cluster_version = "1.31"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
# GPU node group with Spot priority
eks_managed_node_groups = {
gpu_spot = {
instance_types = ["g5.12xlarge"] # 4x A10G, 192GB VRAM
capacity_type = "SPOT"
min_size = 1
max_size = 4
desired_size = 1
labels = { "workload" = "inference", "gpu" = "true" }
taints = [{ key = "nvidia.com/gpu", value = "true", effect = "NO_SCHEDULE" }]
}
gpu_ondemand = {
instance_types = ["g5.12xlarge"]
capacity_type = "ON_DEMAND"
min_size = 1
max_size = 1
desired_size = 1 # Baseline capacity
labels = { "workload" = "inference", "gpu" = "true" }
}
}
}
# AWS Budgets alert
resource "aws_budgets_budget" "monthly" {
name = "qwen2vl-monthly-budget"
budget_type = "COST"
limit_amount = "8000"
limit_unit = "USD"
time_unit = "MONTHLY"
notification {
comparison_operator = "GREATER_THAN"
threshold = 80
threshold_type = "PERCENTAGE"
notification_type = "ACTUAL"
subscriber_sns_topic_arns = [aws_sns_topic.alerts.arn]
}
}
運用・監視設定
CloudWatch Logs Insights クエリ
1
2
3
4
5
6
7
8
9
10
11
12
# 推論レイテンシ分析(P95/P99)
fields @timestamp, @message
| filter @message like /inference_time/
| stats avg(inference_time_ms) as avg_ms,
percentile(inference_time_ms, 95) as p95_ms,
percentile(inference_time_ms, 99) as p99_ms
by bin(1h)
# トークン使用量のスパイク検知
fields @timestamp, input_tokens, output_tokens
| stats sum(input_tokens) as total_input, sum(output_tokens) as total_output by bin(1h)
| filter total_input > 100000
X-Ray トレーシング設定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from aws_xray_sdk.core import xray_recorder, patch_all
import boto3
patch_all() # boto3自動計装
@xray_recorder.capture("qwen2vl_inference")
def run_inference(image_bytes: bytes, prompt: str) -> str:
"""Qwen2-VL推論のトレーシング付きラッパー"""
subsegment = xray_recorder.current_subsegment()
subsegment.put_annotation("model", "qwen2vl-7b")
subsegment.put_metadata("input_size", len(image_bytes))
# vLLM API call
result = call_vllm_endpoint(image_bytes, prompt)
subsegment.put_metadata("output_tokens", result.usage.completion_tokens)
return result.text
Cost Explorer日次レポート
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import boto3
from datetime import date, timedelta
def get_daily_cost_report() -> dict:
"""日次コストレポートを取得"""
ce = boto3.client("ce")
today = date.today()
response = ce.get_cost_and_usage(
TimePeriod={
"Start": (today - timedelta(days=1)).isoformat(),
"End": today.isoformat(),
},
Granularity="DAILY",
Metrics=["UnblendedCost"],
Filter={
"Tags": {"Key": "Project", "Values": ["qwen2vl-inference"]}
},
)
cost = float(
response["ResultsByTime"][0]["Total"]["UnblendedCost"]["Amount"]
)
if cost > 300: # $300/日超過でアラート
sns = boto3.client("sns")
sns.publish(
TopicArn="arn:aws:sns:ap-northeast-1:ACCOUNT:qwen2vl-cost-alerts",
Subject="Qwen2-VL Daily Cost Alert",
Message=f"Daily cost: ${cost:.2f}",
)
return {"date": today.isoformat(), "cost_usd": cost}
コスト最適化チェックリスト
アーキテクチャ選択:
- トラフィック量に基づき構成決定(~100: Serverless / ~1000: Fargate / 10000+: EKS)
- モデルサイズ選定(2B/7B/72B)をタスク精度要件で判断
リソース最適化:
- GPU Spot Instances優先(g5系で最大70%削減)
- On-Demand最小台数+Spot追加のハイブリッド構成
- Reserved Instances: 1年コミットで40%削減検討
- Savings Plans: Compute Savings Plansで柔軟な割引
- アイドル時のスケールダウン設定(HPA + Karpenter)
推論コスト削減:
- vLLM Continuous Batching有効化(スループット向上)
- 画像解像度の事前リサイズ(不要な高解像度を回避)
- KVキャッシュ最適化(PagedAttention)
- 軽量モデル(2B/7B)でのプレフィルタリング
監視・アラート:
- AWS Budgets月次予算設定
- CloudWatch推論レイテンシP95/P99アラーム
- Cost Anomaly Detection有効化
- 日次コストレポート自動送信
- GPU使用率モニタリング(低使用率時のスケールダウン)
リソース管理:
- 未使用エンドポイント・モデルの削除
- タグ戦略(Project, Environment, Owner)
- S3モデルアーティファクトのライフサイクルポリシー
- 開発環境の夜間・週末停止
実験結果(Results)
著者らは、画像理解・動画理解・文書理解・多言語理解など広範なベンチマークで評価を行っている。以下に論文Table 2, 3, 4から主要な結果を引用する。
画像理解ベンチマーク(論文Table 2より)
| Model | DocVQA | MathVista | TextVQA | MMBench-EN | MMMU(val) | RealWorldQA |
|---|---|---|---|---|---|---|
| Qwen2-VL-72B | 96.5 | 74.1 | 99.1 | 88.0 | 64.5 | 77.8 |
| GPT-4o | 92.8 | 63.8 | N/A | 83.4 | 69.9 | 75.4 |
| Claude 3.5 Sonnet | 95.2 | 61.6 | N/A | 80.0 | 70.4 | 60.1 |
| Gemini 1.5 Pro | 93.1 | 63.9 | N/A | 73.9 | 65.8 | 70.4 |
| InternVL2-76B | 94.1 | 65.5 | 97.6 | 86.5 | 55.2 | 71.6 |
DocVQA(96.5%)、MathVista(74.1%)、TextVQA(99.1%)、MMBench-EN(88.0%)ではプロプライエタリモデルを含む比較対象中で最高スコアを達成したと報告されている。一方、MMMU(64.5%)ではGPT-4o(69.9%)やClaude 3.5 Sonnet(70.4%)を下回っている。
動画理解ベンチマーク(論文Table 3より)
| Model | MVBench | Video-MME (w/o sub) | Video-MME (w/ sub) |
|---|---|---|---|
| Qwen2-VL-72B | 73.6 | 71.2 | 77.8 |
| GPT-4o | 64.0 | 71.9 | 77.2 |
| Gemini 1.5 Pro | 69.1 | 75.0 | 81.3 |
MVBench(73.6)ではオープンソース最高スコアだが、Video-MME(字幕あり)ではGemini 1.5 Pro(81.3)が上回っている。
モデルサイズ別スケーリング
| Benchmark | 2B | 7B | 72B |
|---|---|---|---|
| DocVQA | 90.1 | 94.5 | 96.5 |
| MathVista | 43.0 | 58.2 | 74.1 |
| Video-MME (w/o sub) | 55.6 | 63.3 | 71.2 |
| OCRBench | 809 | 845 | 877 |
2Bモデルでも DocVQA 90.1%を達成しており、軽量モデルとしての実用性が確認できる。
実運用への応用(Practical Applications)
Qwen2-VLは、Zenn記事で解説されているGemini APIと同様にマルチモーダル処理が可能なオープンソースモデルであり、以下の用途でプロプライエタリAPIの代替候補となる。
- 文書処理パイプライン: DocVQA 96.5%の性能を活かし、請求書・契約書のOCR+構造化処理をセルフホスティングで実現可能。データをクラウドAPIに送信できないケースで有効である
- 動画コンテンツ解析: 最大768フレームの処理に対応しており、約12.8分の動画をフレーム単位で解析可能。監視カメラ映像の異常検知や教育動画のインデキシングに応用できる
- GUIエージェント: ScreenSpot 89.5%の性能が報告されており、RPA(Robotic Process Automation)のスクリーンショット理解に利用できる可能性がある
- コスト面: Apache 2.0ライセンスのため、2Bモデルをエッジデバイスにデプロイすることで、APIコールの従量課金を回避できる
関連研究(Related Work)
- InternVL2(Chen et al., 2024): 固定サイズタイル+サムネイルによる動的解像度処理。76Bモデルで DocVQA 94.1%を達成。Qwen2-VLのNaive Dynamic Resolutionはタイル分割を廃止した点で異なる
- LLaVA-OneVision(Li et al., 2024): 72Bモデルで MathVista 67.5%。Qwen2-VL-72Bの74.1%を下回る
- MiniCPM-V2.6: 軽量VLMだが OCRBench 852で Qwen2-VL-7B(845)と同等水準。ただし他ベンチマークでは差が開く
まとめと今後の展望
Qwen2-VLは、Naive Dynamic ResolutionとM-RoPEにより任意解像度のマルチモーダル入力を統一的に処理するアーキテクチャを提案した。論文の実験では、DocVQA、MathVista、TextVQAなど複数のベンチマークでオープンソースモデル最高性能を達成したと報告されている。Apache 2.0ライセンスで公開されており、2B/7B/72Bのモデルサイズから用途に応じた選択が可能である。
著者らが認めている限界として、768フレーム上限による長時間動画への制約、知識集約型タスク(MMMU)でのプロプライエタリモデルとの差がある。今後は、ViTのさらなるスケールアップやlong-context対応の拡張が研究の方向性として考えられる。
参考文献
- arXiv: https://arxiv.org/abs/2409.12191
- Code: https://github.com/QwenLM/Qwen2-VL
- HuggingFace: https://huggingface.co/Qwen/Qwen2-VL-72B-Instruct
- Related Zenn article: https://zenn.dev/0h_n0/articles/3d32da8cfe0ac1