
MicrosoftのMage-VLをAWSで動かして、NVIDIAのVSSに載せようとした
はじめに
Xの「MicrosoftがMage-VL 4Bを出した」というポストを見て、気になったので触ってみました。コーデックが持つ「どこが動いたか」という情報を使って、画面の中で動きのあった部分を重点的に読むVLM(Vision Language Model)です。
NVIDIA VSS(Video Search and Summarization)はVLMを差し替えられる作りになっているので、Mage-VLを載せられないか検証してみました。
Mage-VLは1つのモデルの中に、従来どおり静止画を読むframesバックエンドと、コーデック情報を使うcodecバックエンドの両方を持っています。以下ではこの名前で呼び分けます。
先に結論を書くと、こうなりました。
- Mage-VLは、同じトークン数でframesの8倍のフレームを走査する設計です(論文にcanvasを8Nフレームから構成すると明記されています)。これは実測でも成立していて、フレーム数の設定がn=32のとき、framesが32フレームで4,608トークンのところ、codecは256フレームを走査して同じ4,608トークンでした
- ではcodecのほうが速いのかというと、どこまでを計測範囲に含めるかで答えが変わります。prefillだけを測るとcodecのほうが2〜5割遅く、その手前の前処理も含めるとキャッシュがある状態でcodecのほうが1.6〜2.4倍速く、生成の終わりまで含めるとほぼ差がありません
- 動画内の小さい文字(スコアボード)の読み取りは、トークン数を揃えるとcodecの正答率が高くなりました(framesの4.8%に対してcodecは33.3%)。ただしトークン数の制限を外して高解像度のまま読ませたframesは6回中6回正解でした(codec側の同条件は未測定です)
- 話題になっていた「見たい瞬間をプロンプトで指定できる」は、公開実装では確認できませんでした。Mage-VLには動画を流しながら「今しゃべるべきか」をセグメントごとに判定するゲートの仕組みがありますが、プロンプトを5種類変えても、ゲートが開くタイミングは変わりませんでした(確率が小数4桁まで一致)。プロンプトで変わるのは、開いたあとに何を言うかだけです
- VSSは、モデル名に
cosmosが含まれるかどうかで、VLMに送る内容を変えます。Mage-VLの名前では元動画は届かず、JPEG 30枚が届きます - VSSからMage-VLに推論させるところまでは、今回はやっていません。Mage-VLをAPIサーバーとして配信できるのは現状、モデルカードが案内する個人フォーク版SGLangだけで、そこは次回に回しました。この記事で確認したのは、その手前の「VSSがVLMに何を送っているか」までです。推論フェーズでの検証については次回行います。
公式ドキュメントはこちらです。手順自体はここを見れば足りるので、この記事は自分が詰まったところと、測定の結果に絞ります。
Mage-VLとは何か
普通のVLMでは、動画を1秒1枚などで静止画に切り出して、画像として読ませます。これは無駄が多いです。30秒の防犯カメラ映像で動いているのが端の1人だけでも、毎フレーム画面全体をトークンにしてGPUに流します。
Mage-VLはH.264/HEVCの圧縮の仕組みを利用します。動画コーデックはもともと「前のフレームから変わった場所」にだけデータ量を割く設計です。VLMは画像を16×16ピクセルの小さなタイル(パッチ)に区切って読みます。Mage-VLはコーデックの動きベクトルと残差を読んで、実際に動きのあった場所のパッチだけをモデルに入れます。
なおデコード自体はします。ffmpegとcv-preinferを通してから、動きベクトルと残差でパッチを選ぶという流れです。圧縮データのまま推論するわけではありません。
構成はMage-ViTとQwen3-4B-Instruct-2507を2層MLPでつないだものです。Mage-ViTは、コーデック情報を扱うViT(Codec-ViT)をスクラッチから学習したものです。「しゃべるべきか判定するゲート」も同梱されていて、モデルカードではSystem 1として中核機能の扱いになっています。
なおリリース直後のモデルなので、以下の数値は特定リビジョンでの結果として読んでください。私が検証したのはこれです。
| 対象 | バージョン |
|---|---|
| Mage-VL | microsoft/Mage-VL @ 5c78cab61938e73859b63724d9bf5cb88c477eaa |
| 論文 | arXiv:2607.24904(2026-07-27公開) |
| VSS | v3.2.1 |
| transformers | 5.14.1 |
実際、モデルカードは検証中の2日間で2回編集されていました。
環境と方針
| 検証内容 | インスタンス |
|---|---|
| 環境構築・ゲート・元解像度のOCR | g6e.xlarge(L40S×1) |
| 速度測定・OCRの追試 | g6e.12xlarge(L40S×4のうち空いている1枚をCUDA_VISIBLE_DEVICESで指定) |
| VSSの検証 | g6e.12xlarge(L40S×4) |
使い分けの理由は、Mage-VL単体の検証はL40S 1枚で足りる一方、VSSはL40S構成だとLLMとVLMを別々のGPUに載せる必要があり、GPUが複数あるインスタンスが要るためです。
- 東京リージョン / AMI: Deep Learning Base OSS Nvidia Driver GPU AMI (Ubuntu 24.04)
- ドライバ: 595.71.05 / Python 3.12.3
- 推論は全て
attn_implementation=sdpa(flash-attnは入れていません)
どのインスタンスもGPUは同じL40Sなので、GPUで動く処理(prefillと生成)の時間はインスタンスが違っても比較できます。一方、前処理はCPUで動き、vCPU数はインスタンスごとに違うので、前処理の時間はインスタンスを跨いで比較しません。どの数字をどのインスタンスで取ったかは、上の表の対応どおりです。
ポイント1: 公式手順どおりで詰まった
モデルカードには2026-07-29に追加された節があって、こう書かれています。
so image and frame-sampled video inference need no script download and no extra package beyond
transformers
「transformers以外に追加パッケージは不要」とのことです。そのとおりに試したら、パッケージ不足のエラーが5回続き、6回目でインストール自体が失敗しました。
| 回 | 入れたもの | 止まった箇所 |
|---|---|---|
| 1 | transformersのみ | ModuleNotFoundError: No module named 'torch' |
| 2 | +torch | Qwen2VLImageProcessor requires the Torchvision library / requires the PIL library |
| 3 | +torchvision, pillow | ValueError: Using a device_map ... requires accelerate |
| 4 | +accelerate | cv2を要求される |
| 5 | +opencv-python | mamba_ssmを要求される |
| 6 | pip install mamba-ssm |
失敗(ビルド環境でtorchが見えない) |
ModuleNotFoundError: No module named 'torch'
[end of output]
note: This error originates from a subprocess, and is likely not a problem with pip.
ERROR: Failed to build 'mamba-ssm' when getting requirements to build wheel
mamba-ssmのsetup.pyがtorchを要求するのに、pipのビルド分離でtorchが見えないという、よくあるパターンです。--no-build-isolationを使うか、prebuilt wheelを直接指定する必要があります。
なぜこうなるのか
transformersのcheck_importsが、リモートコードの外部importを静的に走査するためです。Mage-VLはモデリング・処理・ストリーミングゲートを1つのリポジトリに詰めているので、画像推論しか使わなくてもcv2(動画処理)とmamba_ssm(ゲート)が要求されます。
なお、このcheck_importsの要求は、一度モデルを動かしたマシンでは再現しません。リモートコードが~/.cache/huggingface/modules/にキャッシュされるためで、このキャッシュはvenvの外にあり、venvを作り直しても消えません。その状態では表の4〜6回目(cv2とmamba_ssmの要求)が起きず、実行時に本当に使うtransformers・torch・torchvision・pillow・accelerateの5つで動きます。
動く組み合わせ
pip既定で入るtorchは2.13で、mamba-ssmもflash-attnもprebuilt wheelがありません。両方ソースビルドになります。
私はGitHubのrequirements.txtの下限に合わせて、torch 2.9系にしました。
pip install --index-url https://download.pytorch.org/whl/cu130 torch==2.9.1 torchvision==0.24.1
pip install "https://github.com/state-spaces/mamba/releases/download/v2.3.2.post1/mamba_ssm-2.3.2.post1%2Bcu13torch2.9cxx11abiTRUE-cp312-cp312-linux_x86_64.whl"
この記事の測定はsdpaで通したのでflash-attnは使っていませんが、inference_streaming.pyの既定がflash-attnなので、入れる場合のprebuilt wheelの提供状況も調べました(執筆時点、cu13+Python 3.12+x86_64、安定版リリースのみ)。
| パッケージ | wheelがあるtorch |
|---|---|
| mamba-ssm v2.3.2.post1 | 2.9 / 2.10 |
| flash-attn v2.8.3 | 2.9 / 2.10 |
| flash-attn v2.8.3.post1 | 2.9のみ |
flash-attnは.post1だけ見ると2.9しかないように見えますが、v2.8.3のアセットには2.10もあります。torch 2.10でも両方揃います。
そのほかに詰まったところも挙げておきます。
inference_streaming.pyはHugging Faceのリポジトリになく、GitHubのmage_vl/だけにあります。Hugging Faceのresolve URLは404でEntry not foundを返しますが、curl -Oは-fを付けないとこのエラー本文をそのままファイルに書き出すので気づきにくいです(15バイトのEntry not foundが入ったファイルができます)- 同スクリプトの
--attn_implの既定がflash_attention_2です。flash-attnはモデルカードのinstall一覧にないので、既定のままでは動きません。--attn_impl sdpaが必要でした decordもモデルカードのinstall一覧にありません- VSS側ではDockerのバージョンも引っかかりました。DLAMIに入っていたのは29.6.2で、VSSの要件は
28.3.3+ and earlier than 29.5.0です。29.4.3にダウングレードしてからapt-mark holdしました
Hugging FaceのモデルカードとGitHubのrequirements.txtで記述が食い違っているので、両方見たほうがいいというのが実感です。
ポイント2: 速度は「どこまで測るか」で結論が変わる
知りたかったのは「同じ動画を読ませたとき、codecはframesより処理が速いのか」です。
公式リポジトリに入っているのは推論スクリプト2本だけで、framesとcodecを比較する手順はありません。比較は自分で組みました。先に書いておくと、この測定は3回やり直しています。条件を揃え損ねたり、同じマシンで動いていたVSSの影響で測定に狂いが生じたりしたためで、何につまずいたかは該当箇所で触れます。以下の数値はすべて最終ランのものです。
時間は3つの段階に分けて測っています。動画をデコードしてモデルへの入力を作る「前処理」、入力全体をモデルに読み込ませて最初のトークンを出す直前までの「prefill」、回答を1トークンずつ出していく「生成」です。
比較の条件
論文には、framesとcodec(論文表記ではframe-Nとtc-N)の比較条件がこう書かれています(強調は筆者によるもの)。
both paradigms share identical vision architectures (16×16 patch size, max 150K pixels per input canvas). Consequently, frame-N and tc-N subject the vision backbone to the same nominal token workload, but tc-N adaptively concentrates representation capacity onto motion-salient regions across a significantly wider temporal horizon.
両方を150Kピクセルに制限すれば視覚トークン数が同等になる、という設計です。ただし論文のframe-NはMage-VL自身のframesバックエンドではなく、別モデルのQwen3-VL-4Bです。Mage-VL自身のframesを同じトークン数で走らせた数字は論文にないので、以下は論文の設定を移植した自前の測定です。
ここで1つ注意点があります。processorのmax_pixels引数はcodecバックエンド専用で、framesパスには渡りません。framesはimage_processorの既定(max_pixels: 4000000)で処理されるため、960×540の動画は32の倍数への丸めで960×544になるだけで、実質無制限です。そこでframes側はQwen2-VLと同じ規則を再現してcv2.resizeしました。公式のパイプラインを通した数字ではありません。
実際、最初のランではこのことに気づかず、frames側を元解像度のまま回していました。すると「codecはトークンを71.8%削減し、prefillが3.42倍速い」というもっともらしい数字が出ます。これは誤りで、frames側を3.54倍の解像度で回していたことの反映でした。元解像度のframes(16,320トークン)のprefillは2.121秒、512×288に揃えると(4,608トークン)0.519秒で、比4.09倍がトークン比3.54倍とおおむね対応します。
また、当初はVSSを起動したままベンチを回していて、GPU0をVSSのLLMが37GB占有していたため、device_map="auto"がモデルをGPU4枚に分散させていました。この状態では生成が3割遅く出ます(n=32のframesで7.28秒。単一GPUなら5.21秒)。空きGPUをCUDA_VISIBLE_DEVICESで指定して測り直したのが以下の数値です。ベンチマークの前にnvidia-smiで同居プロセスを確認するべきでした。
測定結果
両バックエンドを150Kピクセルに揃えて測りました。960×544をsmart_resizeで512×288(147,456ピクセル)に落とすと、1フレーム144視覚トークンになります。
各条件3回実行した中央値です。
| n | バックエンド | 走査フレーム | 視覚トークン | prefill | VRAM |
|---|---|---|---|---|---|
| 8 | frames | 8 | 1,152 | 0.1080s | 9.889GB |
| 8 | codec | 64 | 1,152 | 0.1299s | 9.921GB |
| 16 | frames | 16 | 2,304 | 0.2248s | 10.284GB |
| 16 | codec | 128 | 2,880 | 0.3375s | 10.499GB |
| 32 | frames | 32 | 4,608 | 0.5015s | 11.074GB |
| 32 | codec | 256 | 4,608 | 0.6326s | 11.082GB |
| 64 | frames | 64 | 9,216 | 1.1577s | 12.650GB |
| 64 | codec | 512 | 9,792 | 1.5660s | 12.877GB |
n=8とn=32では視覚トークン数もパッチ数も厳密に一致し、VRAMも0.32%と0.08%しか違いませんでした。論文の「same nominal token workload」は、この2点では実測でも成立しています。一方、トークン数がずれたn=16とn=64では、VRAMも+2.09%/+1.79%ずれています。
表の「走査フレーム」列は実測ではなく、この式から計算して埋めた値です。num_sampled_frames = (target_canvas // images_per_group) * group_sizeで8nになる仕様です。
canvasの枚数は要求どおりにならないことがあります。n=8とn=32は一致しましたが、n=16は20枚(トークンが+25%)、n=64は68枚(+6.25%)でした。走査するフレーム数自体は8nのままで、その中の適応グルーピングの結果としてcanvas数が変わっているようです。
なぜprefillが重いのか
視覚トークンが同数なのにcodecのprefillが重いのは、入力全長が違うからでした。
| n=32 | 視覚トークン | 入力全長 | 差(非視覚) |
|---|---|---|---|
| frames | 4,608 | 4,909 | 301 |
| codec | 4,608 | 6,527 | 1,919 |
codecはcanvasごとにタイムスタンプをプロンプトに埋め込むので、テキスト側が1,600トークンほど増えます。入力全長の比は1.33〜1.60倍で、prefill比の1.20〜1.50倍とおおむね対応します(n=8だけは全長+43%に対しprefillが+20%とずれます)。視覚トークンを揃えたつもりが、プロンプト全体では揃っていなかったわけです。ただし、テキストだけを水増しした対照実験はしていないので、これで全部説明できるとまでは言えません。
どこまでを計測範囲に含めるか
prefillだけを見ると「codecはむしろ1.2〜1.5倍重い」ように見えます。しかし同じ測定には前処理と生成の時間も入っています。n=32で、前処理・prefill・生成の3つに分けて並べるとこうなります(生成は128トークン固定)。
| n=32 | 前処理 | prefill | generate | 合計 |
|---|---|---|---|---|
| frames | 0.867s | 0.502s | 5.212s | 6.580s |
| codec(キャッシュあり) | 0.095s | 0.633s | 5.267s | 5.994s |
| codec(初回) | 1.974s | 0.633s | 5.267s | 7.874s |
どこまでを計測範囲に含めるかで、結論が変わります。
| 見る範囲 | n=32での結果 | 全n(8/16/32/64) |
|---|---|---|
| prefillだけ | codecが1.26倍重い | codecが1.20〜1.50倍重い |
| 前処理+prefill・キャッシュあり | codecが1.88倍速い | codecが1.62〜2.39倍速い |
| 前処理+prefill・初回 | codecが1.90倍遅い | codecが1.56〜2.91倍遅い(n=16/32/64) |
| 3つ全部・キャッシュあり | codecが1.10倍速い | codecが1.04〜1.10倍速い |
| 3つ全部・初回 | framesが1.20倍速い | framesが1.20〜1.24倍速い(n=16/32/64) |
生成を含めると差がほぼ消えます。128トークンの生成に5秒以上かかり、そこが支配的になるからです。「1.88倍速い」は前処理とprefillだけを見た数字なので、生成が長い処理ではその差はほぼ埋もれてしまいます。恩恵があるのは、生成が短い使い方の場合です。
なお「初回」の2行にn=8が無いのは、計測手順の都合でn=8だけ初回の値を取れていないためです。
なお、論文が出している「3.5倍速い」という数字は、この表のどの行とも別物です。
On NExT-QA [168], Mage-VL (tc8) cuts the evaluation wall-clock time from 1460s down to 415s (3.5× speedup) while achieving higher accuracy (80.8% vs. 79.8%).
これはNExT-QAというベンチマークを1本通したときの合計時間の比で、比較相手はQwen3-VL-4B(32フレーム)、GPUは8×B200です。論文は時間を段階別に分けた数字を出していないので、この記事の表と突き合わせることはできません。
この表は、行によって数字の確からしさが違います。
一番確かなのはprefillの行です。両バックエンドとも同じGPUで同じ条件の計算をしていて、差が出る理由も入力全長で説明がつきます。
前処理込みの2行は条件に依存します。frames側の前処理は、cap.set(CAP_PROP_POS_FRAMES)でフレームを1枚ずつ読み出す自作の素朴な実装です。さらにキャッシュはcodec側にしかないため、「キャッシュのあるcodec」対「キャッシュのないframes」の比較になっています(通常利用ではcodecのキャッシュは~/.cache/huggingface/online_codec/へ永続化されます。この測定では一時ディレクトリを指定しました)。生成込みの行も、128トークン固定という設定に依存します。
まとめると、この測定から確かに言えるのは「同じトークン数ならcodecのprefillは2〜5割重い」と「その代わり走査する時間範囲は8倍」の2つです。前処理や合計時間の優劣は、実装・キャッシュ・生成の長さの条件次第で変わります。
品質はどうなるのか
同じトークン数で8倍のフレームを走査できるとして、答えの質はどうなるのか。
サンプル動画は30秒のBBC Sportのサッカー中継クリップです。画面に出るスコアボードを読ませてみました。正解はENG 1 2 ARG FTで、元動画のフレームを目視して確定したものです。
プロンプトはこれです。
Read the scoreboard graphic in this video. Reply with the exact text shown on the scoreboard, nothing else.
「出力に2を含むか」で正誤を判定し、greedy 1回とサンプリング20回(temperature=0.7 / top_p=0.9 / seed 0-19)の計21回で測りました。
| 条件 | 視覚トークン | 走査フレーム | 正答率 |
|---|---|---|---|
| frames n=32 | 4,608 | 32 | 1/21 = 4.8% |
| codec n=32 | 4,608 | 256 | 7/21 = 33.3% |
| frames n=64 | 9,216 | 64 | 0/21 = 0.0% |
| codec n=64 | 9,792 | 512 | 3/21 = 14.3% |
| frames(元解像度・別ラン) | 16,320 | 32 | 6/6 = 100% |
トークン数が少ないn=8とn=16も測っていますが、こちらは6サンプルしか回しておらず、差は出ませんでした。
| 条件 | 視覚トークン | 走査フレーム | 正答率 |
|---|---|---|---|
| frames n=8 | 1,152 | 8 | 1/6 |
| codec n=8 | 1,152 | 64 | 1/6 |
| frames n=16 | 2,304 | 16 | 1/6 |
| codec n=16 | 2,880 | 128 | 1/6 |
プロンプトを変えると結果が変わります。 同じ実験でもう1種類、What is the score of the football match shown, and which two teams are playing?という聞き方でも、各条件でgreedyを1回ずつ取っていました。
| 条件 | score_exact(上の表) | score_qa |
|---|---|---|
| frames n=32 | × | × |
| codec n=32 | × | ◯ England 1, Argentina 2 |
| frames n=64 | × | ◯ eng 1 arg 2 |
| codec n=64 | ◯ | ◯ England 1, Argentina 2 |
score_exactで0/21だったframes n=64が、聞き方を変えると正答しています。この課題の正答率は、プロンプトの表現にかなり左右されるということです。サンプリングまで回したのはscore_exactだけで、score_qaは各1回のgreedyしかありませんが、少なくとも「frames n=64では読めない」とまでは言えないことが分かります。
Fisherの正確検定(両側)を行いました。
| 比較 | p値 |
|---|---|
| n=32のcodec対frames(トークン厳密一致) | p=0.0448 |
| n=64のcodec対frames | p=0.2317 |
| codecのn=32対n=64 | p=0.2772 |
| framesのn=32対n=64 | p=1.0000 |
有意水準5%を下回ったのはn=32だけです。
読み取れることを整理します。
トークン数を揃えた条件では、codecの正答率が高いです。トークン数が厳密に一致するn=32で、4.8%対33.3%(p=0.0448)でした。n=64もcodecのほうが高い(0%対14.3%)のですが、こちらはcodec側のトークンが6.25%多い条件で、しかもp=0.2317なので統計的な差は見られません。
一方、トークン数を3.5倍に増やしてよいなら、この課題では解像度に振ったほうが良い結果でした。元解像度のframes(16,320トークン)は6/6で、greedyの出力もENG 1 2 ARG FTと完全一致しています。ただしこれは6試行だけの別ランで、codec側の同条件も測っていないので、上の4行やcodecとの直接比較はできません。
512フレームまで広げると読み間違いが増えました
codec自身も、走査を256フレームから512フレームに広げると、正答率が33.3%から14.3%に落ちています(ただしp=0.2772なので、統計的な差があるとは言えません)。
それとは別に、codec n=64ではアルゼンチン側のスコアを誤る出力が5/21ありました。正解ENG 1 2 ARG FTの2を1や0と読み違えるもので、内訳はARG 1系が4件(ENG 1 ARG 1 FT / ENGLAND 1 ARGENTINA 1 / ENG 1 ARG 1 ×2)、ENG 1 ARG 0 FTが1件です。codec n=32は0/21、framesも両条件とも0/21でした。codec n=64の5/21とcodec n=32の0/21を比べるとp=0.0478です。
最後に、この節の数字が主張できる範囲を書いておきます。すべて動画1本・プロンプト1種での結果で、プロンプトを変えると順位が入れ替わるのは上で見たとおりです。検定は正答率の4本に読み間違いの1本を加えた計5本なので、Bonferroni補正をかけると(α=0.05÷5=0.01)どのp値も有意ではなくなります。判定基準も「出力に2を含むか」なので、ENG 2 ARG 1のような誤答も正解に数えてしまいます。言えるのは「この動画・このプロンプトではcodecのほうが読めた」までで、一般的な能力差の主張には足りません。
ポイント3: プロンプトはゲートの発火タイミングを変えない
Xのポストには、「見たい瞬間をプロンプトで指定できる。たとえば電車が来たとき、ゴールが決まったとき」という趣旨のことが書かれていました。これが本当なら使い道が広いので、確かめました。
ゲートはセグメントごとに「今しゃべるべきか」を確率で出し、閾値(0.5)を超えたら発話する仕組みです。同じ動画・同じ4セグメント(8秒刻みで、30秒の動画なので最後だけ6秒)に対してプロンプトだけを5種類変えて、この確率が変わるかを比べました。条件はvideo_backend=codec / num_frames=16 / cur_fps=2 / segment_sec=8.0 / gate_threshold=0.5です。試したのは動画1本・この単一条件だけで、framesバックエンドや他の閾値では確認していません。また、ハッシュと確率を取り出す必要があったため、公式スクリプトそのままではなく自作スクリプトで走らせています。
| プロンプト | 0.0-8.0s | 8.0-16.0s | 16.0-24.0s | 24.0-30.0s |
|---|---|---|---|---|
| 公式のハードコード文 | 0.2167 | 0.5635 | 0.7360 | 0.3069 |
| Tell me only when there is a goal. | 0.2167 | 0.5635 | 0.7360 | 0.3069 |
| Tell me only when the train arrives. | 0.2167 | 0.5635 | 0.7360 | 0.3069 |
| Alert me the moment the crowd stands up and celebrates. | 0.2167 | 0.5635 | 0.7360 | 0.3069 |
| Ignore the video. Reply with the word banana. | 0.2167 | 0.5635 | 0.7360 | 0.3069 |
ゲートの確率は、5種類のプロンプトすべてで小数4桁まで一致しました。ゲートに渡っている映像側の入力データ3種をハッシュで比べても、全プロンプトで同一でした。
一方でinput_len(テキスト込みのトークン長)はプロンプトごとに違います。1本目のセグメントでは5,544〜5,549でした。つまりプロンプトは確かにモデルに届いているのに、ゲートの判断には一切影響していません。
開いたあとの出力は、プロンプトごとに変わります。同じ8.0-16.0sのセグメントの生成結果です。
| プロンプト | 生成 |
|---|---|
| 公式 | The video features a live sports broadcast from BBC Sport, ... |
| goal(ゴールの時だけ教えて) | 0 |
| train(電車が来た時だけ教えて) | [] |
| crowd(観客が沸いた瞬間を教えて) | 0.0 |
| banana(動画を無視してbananaと言え) | banana |
「ゴールの時だけ教えて」と指示しても、ゲートは指示と無関係に同じタイミングで開きます。開いても言うことがないからか、0を返します。
コード側の根拠もはっきりしています。inference_streaming.pyはUSER_PROMPTがハードコードで--prompt引数がなく、gate_inputs()はpixel_values / image_grid_thw / patch_positionsの3キーしか渡していません。modeling_mage_vl.pyのstreammind_gate_forward_segments()にもテキストを受け取る口がありません。
そして論文にはこう書かれています。
A text query may be inserted at any time, whereas proactive responses are controlled by the cognition gate.
つまり、プロンプトで変えられるのは「何をしゃべるか」までで、「いつしゃべるか」はゲートが決めるという設計でした。
誤解が生まれた経路も想像がつきます。モデルカードにはa text query can be injected at any timeとだけ書かれていて、ゲートとの役割分担には触れていません。ここだけ読むと「プロンプトで瞬間を指定できる」と受け取れます。
なお公式のinference_streaming.py自体は、モデルカードの掲載例とほぼ同じ結果を再現できました。
| セグメント | 実測 | モデルカード掲載 |
|---|---|---|
| 0.0-8.0s | silence 0.22 | silence 0.19 |
| 8.0-16.0s | response 0.56 | response 0.55 |
| 16.0-24.0s | response 0.74 | response 0.73 |
| 24.0-30.0s | silence 0.31 | silence 0.31 |
ポイント4: VSSに載せる
VSSにはVLMを差し替える口があります。同じトークン数で8倍のフレームを見られるなら、VSSの動画理解を今より少ないトークン(=計算コスト)で済ませられるかもしれない、というのが最初の期待でした。
g6e.12xlarge(L40S×4)に、公式のL40S Dedicated GPU構成でデプロイしました。
deploy/docker/scripts/dev-profile.sh up -p base -H L40S \
--llm-device-id 0 --vlm-device-id 1 \
--external-ip <インスタンスのパブリックIP>
VSSがVLMに何を送っているかを確かめた
VSSが実際にVLMへ何を送っているのかを確認したくて、OpenAI互換のダミーサーバーを立てて、VLM_ENDPOINT_URLをそこに向けました。リクエストを保存するだけのプロキシです。
export VLM_ENDPOINT_URL="http://<ホストのプライベートIP>:39000"
deploy/docker/scripts/dev-profile.sh up -p base -H L40S \
--llm-device-id 0 --use-remote-vlm --vlm '<モデル名>' \
--external-ip <パブリックIP>
動画全体への要約リクエストを、--vlmに渡すモデル名だけを変えた2構成でキャプチャしてみると、送られる内容が変わりました。
┌─ モデル名に cosmos を含む
│ → video_url に元動画のmp4をbase64で丸ごと
元動画 ──> VSS ──> 分岐 ────┤ + media_io_kwargs で「30フレームに間引け」と指示
(720f/24fps) │ = コーデック情報がそのまま届く
│
└─ 含まない(microsoft/Mage-VL 等)
→ image_url × 30(JPEG)
= デコード済み、コーデック情報は消失
| 項目 | nvidia/cosmos-reason2-8b |
microsoft/Mage-VL |
|---|---|---|
| content parts | video_url × 1 |
image_url × 30 |
| メディア | video/mp4 3,914,911バイト | JPEG 30枚(合計5,423,858バイト) |
| ffprobe | 720フレーム / 24fps / 30.0秒 / h264 | — |
media_io_kwargs |
{"video":{"num_frames":30}} |
なし |
mm_processor_kwargs |
{"size":{...}} |
なし |
cosmosの名前のときに送られたmp4は3,914,911バイトで、元ファイルとバイト数が一致しています。ffprobeの結果も元動画と同じです(ハッシュ照合まではしていません)。つまり元動画をそのままbase64で送り、「30フレームにサンプリングしろ」という指示(media_io_kwargs)だけを添えています。フレーム抽出をサーバー側に委譲しているわけです。
一方、Mage-VLの名前だとJPEG 30枚に切り替わります。コーデック情報はここで消えます。動きベクトルと残差を読むというMage-VLの仕組みは、静止画のJPEGには使えないからです。
この分岐の理由
分岐の実装はこうです(services/agent/src/vss_agents/tools/video_understanding.py)。
return _is_remote_vlm(vlm_mode) and (
_is_cosmos_model(model_name) or (enable_audio and _is_omni_audio_model(model_name))
)
_is_cosmos_model()はモデル名にcosmosが含まれるかを見るだけです。ただdocstringに理由が書いてありました(OpenAI API compatibilityの強調は筆者によるもの、Remote Cosmos modelsは原文の太字)。
- Remote Cosmos models — avoids the per-prompt image count limit that rejects >5 individual
image_urlentries. The NIM handles frame sampling server-side viamedia_io_kwargs.
Non-cosmos remote VLMs (Qwen, GPT-4o, etc.) stay on theimage_urlframe-sampling path for OpenAI API compatibility.
Cosmos NIMが画像5枚超を弾くのを回避するための分岐です。非CosmosはOpenAI API互換性のためにimage_urlを使います。意図的な互換性判断で、理由もコードに書かれています。
なお、この挙動には前提条件があります。
- この分岐に入るのは
VLM_MODE=remoteのときだけです。baseプロファイルの.envの既定はlocal_sharedなので、既定のままでは上の送信は起きません num_frames=30はbaseプロファイルのconfig.ymlにあるmax_frames: 30/max_fps: 2から来ています(num_frames = min(動画秒数×max_fps, max_frames)という式です)- JPEGの枚数30は、
frame_selectが計算したrange(0, 719, 24)から来ています。num_framesの30とは別々の計算で、この動画(30秒・24fps・720フレーム)ではたまたま両方30になっています
VSSのコードにはRT-VLMという別経路もあり、そちらは10枚のJPEGをmp4に再エンコードして送る実装になっています。ただしbaseプロファイルで起動するのはvideo_understandingのほうで、RT-VLMのコンテナは動きません。VSSのコードを読むときは、どちらの経路が実際に動いているかを先に確認するのが安全です。
コストと後片付け
| リソース | 単価(東京・On-Demand) | 実績 |
|---|---|---|
| g6e.xlarge(L40S×1) | $2.699/h | 約5時間 ≈ $13.5 |
| g6e.8xlarge(L40S×1・32vCPU) | $6.56775/h | 約1.5時間 ≈ $10(VSS用に最初に選んだ分。2GPU必須と分かり作り直し) |
| g6e.12xlarge(L40S×4) | $15.21742/h | 約5時間 ≈ $76 |
| EBS gp3 | $0.096/GB-月 | 300GBから1TBに拡張。約1日で$3前後 |
累計で$100〜110でした。当初は$50〜90を想定していたのですが、VSSのL40S構成が2GPU必須だったためインスタンスを作り直し、測り直しもしたので超過しました。
VSSの最小要件は18コア/128GB RAM/1TB SSDです(下記ドキュメント)。GPU以外もそれなりに要ります。
後片付けは、インスタンスのterminateと、ボリュームが残っていないことの確認までやります。stopだけではEBSの課金が続きます(1TBで約$96/月)。
aws ec2 terminate-instances --instance-ids <instance-id>
aws ec2 describe-volumes --filters "Name=status,Values=available" # 残ったボリュームの確認
おわりに
Mage-VLをVSSに繋いで実際に推論させるところまでは、今回はやっていません。
モデルカードは「Online inference」として、個人フォーク版SGLang(kcz358/sglangのfeat/mage-vlブランチ)でOpenAI互換サーバーを立てる手順を案内しています。ただしvLLMは未対応、SGLang本家も対応issueがopenのままで、本家の推論サーバーでは動きません。フォーク版のビルドと、VSSが送るJPEG 30枚をMage-VLで受ける検証は、別の記事で試してみたいと思います。








