自動運転モデル BEVFormer-tiny の推論結果。上段が予測、下段が正解(GT)、右が鳥瞰図(BEV)。可視化の詳しい読み方は前回記事:「自動運転モデル BEVFormer を NVIDIA H200 で動かしてみた」を参照。
前回(H200編)は自動運転モデル BEVFormer(ECCV 2022)をクラウドの H200×2 でフル学習し、論文値を再現するところまでを扱った。本稿はその続きである。
TL;DR (結論)
- 自動運転モデル BEVFormer(ECCV 2022)を最新の NVIDIA Blackwell B200 に載せても、学習は素直には速くならなかった。原因は中核の Deformable Attention が Tensor Core に乗らない疎な gather + 双線形補間だからで、Tensor Core 稼働率は約 8% で頭打ち、しかも GPU 世代に依らず一定だった。
- ならばとカーネル自体に手を入れ、value を FP8 / FP4 で読むように CUDA カーネルをチューニングした。速度が得られたのは帯域で頭打ちになるコンシューマ?級 GPU(DGX Spark GB10)だけで、HBM3e 8TB/s の B200 では速度向上は消えた。
- 結局、低精度化の真価は速度ではなく メモリ効率化 だった。value を FP8 で 1/4、FP4 で 1/8 に縮められることが、より大きな batch・長い時系列・高解像度を同じメモリで回す余地を生む。副産物として、公開研究では前例の見当たらない成果もいくつか得られた。
1. なぜ B200 なのか
BEVFormer は車載 6 カメラの映像から Bird's-Eye-View(俯瞰)表現を構築し 3D 物体検出を行う Transformer 系モデルである。公式の学習スクリプトは 2022 年当時のデータセンタ GPU(V100 / A100 世代)を 8 枚束ねる分散学習を前提にしている。これを 2026 年の最新 GPU に載せ替えれば速くなるはずだ、というのが素朴な期待だった。とりわけ Blackwell 世代の B200(sm_100、HBM3e)は Tensor Core が強力で、LLM 学習を速める AMP(自動混合精度)も効くはずである。
BEVFormer が開発された V100 世代から、前回の H200、今回の B200 までを並べると、スペックの伸びは一目瞭然である(値はいずれも NVIDIA データシートの dense、非 sparse)。
| V100(開発時 / 2017) | H200(前回 / Hopper) | B200(今回 / Blackwell) | |
|---|---|---|---|
| アーキ(SM) | Volta (sm_70) | Hopper (sm_90) | Blackwell (sm_100) |
| メモリ | 32 GB HBM2 | 141 GB HBM3e | 180 GB HBM3e |
| メモリ帯域 | 0.9 TB/s | 4.8 TB/s | 8 TB/s |
| Tensor Core(FP16 dense) | 125 TFLOPS | 約 990 TFLOPS | 約 2,250 TFLOPS |
| FP8 / FP4 | なし / なし | FP8 / なし | FP8 / FP4 |
| BEVFormer 公式学習 | 8×V100 で 24 epoch | — | — |
開発時の V100 から見れば、B200 はメモリ帯域で約 9 倍、Tensor Core スループット(FP16)で約 18 倍に達する。それでも、後で見るように BEVFormer-tiny の学習は素直には速くならなかった。 なぜか——それが前半(1〜4 章)の主題である。ちなみに最下段の FP4 対応は B200 世代で初めて入った precision で、これは後半(5 章以降)のカーネルチューニングの話につながる伏線でもある。
そこで出発点の問いはこうなる。「BEVFormer-tiny を B200 に載せると、学習は大きく速くなるのか?」
2. 環境構築と、再現用イメージの配布
第2弾の宿題だった再現用 Docker イメージをここで配布する。docker pull nabe2030/bevformer:blackwell-pt2.8 だけで、PyTorch 2.8 + CUDA 12.8 + Blackwell 上に 2022 年の BEVFormer コードベースが立ち上がる。
SimpleDDP — mmcv 1.x を PyTorch 2.x で動かす
最大の障害は、BEVFormer が依存する mmcv 1.x の MMDistributedDataParallel が PyTorch 2.x で動かないことだった。mmcv 1.x は PyTorch の DDP の内部(private)API に直接触れており、それらが PyTorch 1.11 → 2.0 → 2.8 の過程で順次削除されたためである。
解決は、標準の DistributedDataParallel を継承し、train_step / val_step の中で mmcv 自身の scatter_kwargs で DataContainer を展開してから self.module.train_step を直接呼ぶ、というものだ。勾配の集約(AllReduce)は DDP が仕込む backward hook が担うので、forward() を通す必要はない。private API を一切触らないため、PyTorch 2.x / 3.x のバージョン差に強い。差分はおよそ 14 行で収まった。
from torch.nn.parallel.distributed import DistributedDataParallel, _find_tensors # noqa
from .scatter_gather import scatter_kwargs
class MMDistributedDataParallel(DistributedDataParallel):
def train_step(self, *inputs, **kwargs):
if self.device_ids:
inputs, kwargs = scatter_kwargs(inputs, kwargs, self.device_ids, dim=self.dim)
return self.module.train_step(*inputs[0], **kwargs[0])
return self.module.train_step(*inputs, **kwargs)
def val_step(self, *inputs, **kwargs):
if self.device_ids:
inputs, kwargs = scatter_kwargs(inputs, kwargs, self.device_ids, dim=self.dim)
return self.module.val_step(*inputs[0], **kwargs[0])
return self.module.val_step(*inputs, **kwargs)
ここに至るまでに3回設計のやり直しをした。最初の設計v1(forward を差し替える案)は PyTorch 2.x の _pre_forward が dict 引数を展開して TypeErrorでNG、次の設計v2(forward を迂回する案)は DataContainer の展開が抜けて 'DataContainer' is not subscriptableでNG、そして現行設計v3でようやく通った。この修正は BEVFormer だけでなく、mmcv 1.x に依存する BEVDet / UniAD / StreamPETR / Sparse4D などにも同じように効くはずである。
ハマりどころ(失敗の記録)
- ベースイメージに
nvidia/cudaを直接使うと Pod が restart loop に入った。RunPod のrunpod/pytorchベースの起動スクリプトを継承する必要があり、そこからFROMする形が正解だった。 - 別系統の実験タグ(
blackwell-runpod-v6)を GraphQL で直接指定して Pod を起動したところ、2 回続けて boot stall(runtime が上がらないまま)になり、クラウド課金を無駄に消費した。正解は公式タグblackwell-pt2.8を template 経由で起動することだった。RunPod は container のログや image pull の進捗が API から取れず、runtime=null 以外の手がかりがないため、この種の詰まりは診断が難しい。同一症状 2 回で停止する運用ルールを追加して無駄を抑えた。
3. 実測 — B200 vs H200
学習は、データを小さな塊(バッチ)に分けて 1 塊ずつ処理し、そのたびに重みを 1 回更新する。この 1 回分をイテレーションと呼ぶ。以下では 1 イテレーションにかかる時間で速さを比べる。計測は wall-clock 時間の中央値(p50)で行う(mini batch、samples_per_gpu=1)。平均値は warmup の外れ値に汚染されるため、定常状態の代表値として p50 を採る。
イテレーション時間
| Run | 精度 | B200×2 p50 | H200×2 p50 |
|---|---|---|---|
| A | FP32 | 0.3810 s | 0.4085 s |
| B | FP16 (AMP ON) | 0.4100 s | 0.3700 s |
4 つの数値はいずれも 0.37〜0.41 秒の狭い帯に収まり、差は最大でも 1 割に満たない。注目すべきは、FP16(AMP)にすると B200 では FP32 より むしろ遅くなっている(0.3810 → 0.4100 秒)ことである。これは、AMP が入れるキャストとロススケールの overhead が、BEVFormer-tiny の hot path(Tensor Core を使わない Deformable Attention)では取り返せないためだ。native の FP32 がもともと速い B200 では、その取り返せない overhead がそのまま数 % の遅さとして表に出る。逆に FP32 が相対的に遅い H200 では AMP が僅かにプラスへ働く。もっともこの符号の向きは batch サイズで入れ替わる程度の小ささで、有意差とは言えない(詳細は次節)。要するに、FP32↔FP16 の切り替えも H200→B200 の世代更新も、このモデルの速度をほとんど動かさない。
一方、loss の収束は両 GPU でほぼ完全に一致した(23.3 → 18.1、grad_mean 約 41.6)。ここが重要な確認点になる。データ並列学習では各 GPU が自分の担当データで勾配を計算し、DDP がそれを AllReduce で平均して全 GPU の重み更新を揃える。もしこの集約が壊れていれば、各 GPU は実質バラバラに学習して収束曲線が単一 GPU の基準からずれるはずである。それがぴたりと一致したということは、forward() を経由しない自作の SimpleDDP v3 でも勾配の集約が正しく発火している、という裏取りになる。
AMP の効果
図: B200 / H200・mini / full の全条件で、AMP の A/B 比(FP32 p50 ÷ FP16 p50)は 1.0 の ±10% 以内に収まり、符号すら構成で入れ替わる。
AMP(自動混合精度)は、計算の一部を FP16 など低精度に落として速度とメモリを稼ぐ標準的な手法で、LLM では学習を速める効果がある。自動運転モデルでもこのAMPが効くかどうか?を測定した。AMP の A/B 比を条件ごとに並べると、0.93×(B200 mini)、1.02×(B200 full)、1.10×(H200 mini)となった。いずれも 1.0 の ±10% 以内で、符号は構成によって入れ替わる。つまり BEVFormer-tiny にとって AMP の効果は認められず、有意差のない範囲である。 小バッチでは方向性(符号)が出たように見えたが、フルデータではその方向性は頑健ではなかった、と付言しておく。
Tensor Core 稼働率
図: Tensor Core カーネルが占める時間は B200 で 8.08%、H200 で 8.49%。ほぼ同一で、GPU 世代に依らない。
torch.profiler でカーネル時間を集計すると、Tensor Core カーネルの占有は B200×1 で 8.08%(94.6 ms / 1170.4 ms)、H200×2 で 8.49%(125.1 ms / 1473.0 ms)だった。両者ともおよそ 8% で、GPU 世代に依らずほぼ一定である。
図: Tensor Core を使っているのは ResNet backbone の conv(cutlass tensorop)だけ。Deformable Attention は使っていない。
カーネル時間を分解すると、Tensor Core に乗っているのは ResNet backbone の畳み込み(cutlass3x_sm100_tensorop)だけで、BEVFormer の中核である Deformable Attention(ms_deformable_im2col / col2im)は Tensor Core を使っていない。GPU 時間を実際に支配していたのは optimizer step、BatchNorm、memcpy / elementwise といった非 Tensor Core カーネルである。なお H200×2 では、GPU 間で勾配を配り合う NCCL AllReduce が全体の 23% を占めた。これは 2 枚構成で 1 GPU あたり 1 サンプルという設定のためである。1 イテレーションの計算量が小さいのに、GPU 間の勾配のやり取りは毎回フルに走るので、その割合だけが相対的に大きく見えてしまう。バッチを増やせばこの比率は下がる。Tensor Core 比を 2 枚ではなく単一 GPU で測ったのも、この多 GPU 特有の水増しを避けるためである。
4. なぜ速くならないのか
Deformable Attention の計算は、各 query について特徴マップ上の少数のサンプリング点を選び、各点で双線形補間により特徴を取り出し(疎な gather + 補間)、attention weight で加重和を取る、というものである。これはメモリを不規則に読み集める演算であって、Tensor Core が加速する dense な行列積ではない。だから Tensor Core が使われない。
興味深いのは、これが Deformable Attention の効率の裏返しである点だ。全位置を密に見るのではなく数点だけを疎にサンプリングする設計こそが計算を軽くしている。その「疎」こそが「密」専用ユニットである Tensor Core と構造的に相容れない理由になっている。AMP(FP16 化)もメモリ帯域を半減させるので原理的には少し得なはずだが、FP32↔FP16 のキャストやロススケールの overhead とほぼ相殺し、正味の効果は誤差の範囲に収まる。
ここで自然に次の問いが立つ。「では、カーネル自体を FP8 / FP4 に書き直したら速くなるのか?」
5. 当初の期待 — 速くなると思っていた
着手時の仮説は 3 つあった。(1) dequant と補間のループを FP8/FP4 で書き直す。(2) それで Tensor Core を使わせる。(3) 結果として数倍速くする。
いつも通り、手を動かす前に NVIDIA 公式・GitHub・コミュニティを数回検索した。ここで (2) と (3) が実装前に崩れた。Deformable Attention の実体は、不規則な gather と補間、そして散乱書き込みである。Tensor Core が要求する dense な行列積の形に整形する道筋は、現状では無い(疎行列積化は将来の別テーマとする)。疎な attention を Tensor Core に載せた前例は Flash Attention にあるが、あれは規則的な疎で、deformable の不規則な gather をどこまで密に畳めるかは別問題である。
残ったのは (1) だけだった。value の読み込みを低精度化する、純粋なメモリ削減である。これが速度に効くかどうかは規模しだいで、事前には分からない。だから「速くなるかは未検証」と正直に据えたまま、実装に入った。
図: value テンソルだけを FP8/FP4 に落とし、補間と累算は FP32 のまま残す。
6. 設計前のサーベイ — 何を棄却したか
-
FP4 の dequant は scalar か vector か。 1 スレッドが 1 nibble を復号する scalar 方式は分岐と復号命令が増える。2 値を同時復号する vector 方式(
__nv_cvt_fp4x2_to_halfraw2系)は LLM の GEMM 量子化で確立していたが、これを deformable の gather + 補間に適用した前例はサーベイの範囲で見当たらなかった。 -
FP8/FP4 の atomicAdd は提供されていない。
cuda_fp8.h/cuda_fp4.hを grep しても atomicAdd は 0 件で(対してcuda_fp16.h/cuda_bf16.hにはある)、CAS ループ自作も 32bit 最小単位に FP4 で 8 値・FP8 で 4 値が pack される非現実な構造になる。backward で勾配を低精度に直接書き込む道はなく、FP32 の staging buffer に集約するしかないことが、設計を固める前にコード根拠で確定した。 - backward の atomic は順序非決定。 mmcv も DCNv2 も同じ入力位置へ複数スレッドが atomicAdd する構造で、DCNv2 の公式 README も「backward は再入不可、gradcheck には倍精度が要る」と明記している。量子化 backward で数値ズレが出やすい点はあらかじめ織り込んだ。
- 最前線の attention 量子化はすべて dense。 SageBwd / Attn-QAT / FPSAttention などはいずれも Transformer の Q×Kᵀ と Softmax の量子化であり、gather + 補間 + 散乱 atomic の量子化前例は見当たらなかった。
7. 実装と検証の反復(失敗込み)
低精度化するのは value テンソルだけで、それ以外は FP32 のまま残す。この「不変項」を最初に固めた。
| 要素 | 精度 | 理由 |
|---|---|---|
| value / 入力特徴 | FP8 e4m3 / FP4 e2m1 | 低精度化の対象。GMEM から読む最大のデータ |
| sampling location / offset | FP32 | 座標は精度が効く |
| attention weight / mask | FP32 | ∈ [0,1] の少数値、データ量も小さい |
| 双線形補間の累算 | FP32 | 誤差を value の量子化だけに局在させる |
| 出力 | FP32 | 後段の matmul / 損失計算へ |
加えて、量子化スケール(FP8 は max/448、FP4 は max/6)は補間が線形なのでカーネル内では掛けず、出力に一度だけ mul_(scale) で戻す。また mmcv の AT_DISPATCH_FLOATING_TYPES_AND_HALF は float / double / half しか展開しないため、dispatcher には手を入れず、独立した cpp_extension として別に書く方針にした。
図: Spark sm_121 の forward speedup。large ほど効き、FP4 vector が FP8 を上回る。
1-A. FP8 forward — 素直に効いた
value を FP8 の e4m3 形式でロードする。FP8 は 1 バイト(8 ビット)の浮動小数点で、FP32 の 1/4 の容量しかない。e4m3 はその 8 ビットの内訳を表す表記で、符号 1 ビット+指数 4 ビット(e4)+仮数 3 ビット(m3)を意味する。指数を厚めに取るほど表現できる値の幅(ダイナミックレンジ)が広くなり、活性値のように桁の開きがある量に向く(もう一方の e5m2 は範囲がさらに広い代わりに精度が粗い)。この読み方は後で出てくる FP4 の e2m1(指数 2・仮数 1 ビットの 4 ビット型)も同じである。
実装では、FP8 の value を補間の直前に static_cast<float> で FP32 に戻し、補間と累算は FP32 で行う。公平を期すため、value 型だけを FP32 に差し替えた byte-for-byte の双子カーネルを baseline にした。
- 数値精度: 正規化絶対誤差 2.55%(判定 < 5%)
- 速度(Spark sm_121、vs FP32 双子): tiny 0.77× / base 2.05× / large 2.16×(値は FP32 の実行時間 ÷ FP8 の実行時間。1 より大きいほど FP8 が速い)
正規化絶対誤差の定義は次の通り。要素ごとの差の絶対値を平均し、参照値(FP32 双子カーネルの出力)の絶対値の平均で割る。
\text{正規化絶対誤差} = \frac{\operatorname{mean}\left(\left|y_{\mathrm{FP8}} - y_{\mathrm{FP32}}\right|\right)}{\operatorname{mean}\left(\left|y_{\mathrm{FP32}}\right|\right)}
要素ごとの相対誤差にしないのは、出力に 0 近傍の要素が混ざると分母が潰れて誤差が発散して見えるためで、全体を平均で正規化するこの形が量子化誤差の評価では扱いやすい。以降の FP4 や backward の誤差もすべて同じ定義で測っている(backward では $y$ を各勾配テンソルに読み替える)。
理論上限は 4 バイト → 1 バイトの 4× だが、sampling location・weight・出力を FP32 に残すため実効バイト比は約 2.66× で、実測の約 2× はその範囲に収まる。tiny で 1 を割る(遅くなる)のは、value が小さくキャッシュに収まり、カーネル起動と mul_ の overhead が勝つためだ。大きいワークロードほど効くのは、帯域で頭打ちになっている証拠である。
1-B. FP4 scalar forward — FP8 に負けた
FP4 e2m1(1 バイトに 2 値 pack、uint8 storage + 自作 quantize/pack)を「1 スレッド = 1 チャネル」で素朴に書いたところ、FP8 に負けた。
- 数値精度: 13.88%(FP8 の約 5.4 倍悪いが、判定 < 20% は合格)
- 速度: base で FP8 の 0.45×、large でも FP8 の 0.65×(いずれも FP8 に負け)。FP32 比では tiny 0.60× / base 0.92× / large 1.40×
敗因は命令数で説明できる。1 バイトに 2 値あるのに 1 スレッドが片方の nibble しか使わず、復号が 1 要素あたり 3〜4 命令(FP8 は 1 命令)、さらに偶奇 nibble の選択で warp 内分岐が生じる。FP8 に対するメモリ半減の利得を、増えた命令数が食い潰した。負けはしたが、なぜ負けたかを命令数で説明できたことが次につながった。
1-B'. FP4 vector decode — FP8 を上回った
scalar 版の敗因(1 値ずつ・分岐あり)を vector decode で潰した。1 スレッド = 2 チャネルにし、__nv_cvt_fp4x2_to_halfraw2 系で 2 つの nibble を分岐なしに同時復号する。スレッド数は半減し、復号は 1 チャネルあたり約 1.5 命令に下がる。
- scalar 版と bit-exact 一致(意味は不変、精度も同じ 13.88%)
- 速度(Spark sm_121、vs FP32): tiny 0.63× / base 1.69× / large 2.35×
- FP8 比: large で 1.10×(FP4 vector が FP8 を上回る)。large の 2.35× は FP8 の 2.16× を超える
large でだけ勝つのも帯域の話で筋が通る。value がキャッシュ(Spark GB10 でおよそ 40 MB と見積もっている。正確な値は NVIDIA 公開仕様で未確認)を超えて HBM で頭打ちになる large では、メモリ半減が支配的になり、復号命令は HBM 待ちの陰に隠れる。キャッシュに収まる base / tiny では FP8 の命令数の少なさが勝つ。vector FP4 を deformable の gather + 双線形補間に適用して FP8 を上回った例は、公開研究では見当たらなかった。 dense attention の FP4 化(FlashAttention-4 等)はあるが、それは行列積であって本件の不規則 gather とは別物である。
1-C. backward の量子化 — atomicAdd の型制約をどう超えたか
forward が通っても、学習には backward が要る。backward は各サンプリング点から入力へ勾配を散らす(scatter)ため、複数スレッドが同じ入力位置に書き込む atomicAdd が必須になる。ところが前述の通り FP8/FP4 への atomicAdd は存在しない。そこで、value は FP8/FP4 でロードして都度復号するが、勾配は FP32 の staging buffer に float の HW atomicAdd で集約する(NVIDIA Transformer Engine の FP8 backward と同じ思想)。
FP8 backward は最初の 4 回、通らなかった。バグは段階的に正体を現した。
| 回 | 症状 | 原因 | 修正 |
|---|---|---|---|
| 1 | grad_sloc / grad_aw が 88〜90× ズレ | block 256 で複数の (b,q,m) が混在し、共有メモリ reduce が誤集約 | block = channels(mmcv の dispatcher 仕様に準拠) |
| 2 | 同じズレ | 共有メモリ reduce を疑う | reduce を bypass して検証 |
| 3 | 同じズレ | 検証 ref(autograd)を疑う | mmcv 純正 FP32 backward を移植して ref に切替 |
| 4 | ref は正しい、bug は FP8 path 内 | 比 88〜90 が 1/scale と正確一致 | wrapper で grad_sloc / grad_aw に scale を復元(2 行) |
最終的に、FP8 backward は output 2.55% / grad_value 2.56% / grad_sampling_loc 2.50% / grad_attn_weight 2.00% で mmcv 純正 backward と一致した。最大の教訓は、「mmcv カーネルを FP8 化する」研究なら検証 ref も最初から mmcv 純正 backward にしておくべきだった、という点である。そうすれば 4 回のうち 2 回の迷走(autograd との規約差を疑った回)は避けられた。
この 3 つの教訓(load 名を .py と分離・最初から scale 復元・mmcv を ref に)を組み込んで書いた FP4 backward は、一発で通った。output 13.88% / grad_value 13.86% / grad_sampling_loc 11.72% / grad_attn_weight 9.81%(tol 20%)。forward の量子化誤差がそのまま勾配へ伝播し、破綻なく一致している。deformable backward(gather + scatter)を量子化して mmcv 純正と一致させた例も、公開研究では見当たらなかった。
ただし backward の速度は出なかった。forward + backward の実時間で、Spark FP8 は 0.63〜0.71×、B200 FP8 は 0.67〜0.78× と、いずれも FP32 より遅い。FP32 staging buffer の毎回のゼロ埋め、毎回の quantize、勾配計算と atomic 書き込みが支配的で、value のメモリ削減の利得を上回るためである。forward がメモリで頭打ちだったのに対し、backward はむしろ compute 寄りになる。
DCN への横展開 — 構造同型を実測で裏取り
前回の base profile で見えていた重要な事実がある。BEVFormer-base(ResNet-101-DCN backbone)ではカーネル時間の内訳が DCN 52.49% / MSDA 10.46% / Tensor Core 6.71% で、最大の柱は MSDA ではなく DCN(Deformable Convolution v2)だった。
DCN v2 のカーネルを読むと MSDA と構造同型である(両者とも同系統の移植が源流)。
| 項目 | MSDA | DCN v2 | 対応 |
|---|---|---|---|
| 双線形補間 helper | ms_deform_attn_im2col_bilinear |
dmcn_im2col_bilinear |
ロジック一致 |
| forward の thread 割当 | 1 出力要素 | 1 出力要素 | 同型 |
| 重み | attention weight (∈[0,1], Σ=1) | mask (∈[0,1], sigmoid) | 同型 |
| backward の atomicAdd | 多数 | 1 箇所(grad_im のみ) | DCN の方が単純 |
| dispatcher | AT_DISPATCH_FLOATING_TYPES_AND_HALF |
同じ | 同じ壁 |
ここから「MSDA の FP8 forward は DCN にそのまま転用できる」と予測が立つ。実測すると、DCN forward FP8(B200)は正規化絶対誤差 2.63% で mmcv 純正 FP32 DCN と一致した。これは MSDA 1-A の 2.55% をほぼ再現しており、構造同型の予測が裏取りされた。これで base profile の DCN 52% を低精度で攻める道が技術的に開通した。ただし DCN の速度面の結論は MSDA と同じで、B200 上の DCN は全 config が L2(約 126 MB)に収まり、達成帯域は最大でも 251 GB/s(HBM 実効ピークの約 3%)で、帯域は飽和していない。DCN は不規則 gather のレイテンシに律速される演算で、B200 では value を縮めても速度は出ない。なお DCN backward の低精度化は atomicAdd が 1 箇所だけで MSDA より易しく、設計上は転用可能と確認済みだが、実装・実測は未了である。本稿で「実測した」と言えるのは DCN forward FP8 までとする。
8. 4 象限で見る結論
図: forward / backward × コンシューマ(Spark)/ データセンタ(B200)。低精度が速度に効くのは帯域で頭打ちのコンシューマ GPU の forward だけ。
| Spark GB10 (sm_121, 帯域で頭打ち) | B200 (sm_100, HBM3e 8 TB/s) | |
|---|---|---|
| forward FP8 | large 2.16×(速い) | large で FP32 = FP8(効果消失) |
| forward FP4 vector | large 2.35×(FP8 超え) | large 0.92×(むしろ負ける) |
| backward FP8 / FP4 | FP32 より遅い | FP32 より遅い |
B200 で速度が消える理由はこうだ。HBM3e の 8 TB/s が速すぎて、large の 208 MiB の value ですら帯域を飽和させない。帯域で頭打ちになっていない以上、型を 4 バイト → 1 バイトに縮めてもカーネル時間は変わらず、FP4 の復号 overhead だけが表に出て逆に遅くなる。加えて Tensor Core はコンシューマ Blackwell sm_121 では封じられており、そもそも deformable は不規則 gather なので Tensor Core を使えない。
図: value を FP8 で 1/4、FP4 で 1/8。速度ではなくメモリ容量が低精度化の効きどころ。
だから低精度化の本当の価値は速度ではなくメモリ効率化にある。value を FP8 にすれば FP32 の 1/4、FP4 なら 1/8。これは 4〜8 倍大きい batch・長い時系列キュー・高解像度を同じメモリで回せることを意味する。前回の「最新 GPU でも速くならない」と、本稿の「低精度化しても(B200 では)速くならない」は根が同じである。Deformable Attention は compute でも bandwidth でもなく、不規則メモリアクセスのレイテンシに律速される演算だからだ。
なお副産物として、deformable backward の量子化は前例の見当たらない領域で、FP8 で 2.0〜2.6%、FP4 で 9.8〜13.9% の誤差で mmcv 純正 backward を再現できた。速度に効かないとしても、QAT 互換の学習が理論的に可能だと示せたこと自体には価値がある(LLM の FP8 学習も、まず可能だと示すのが最初の一歩だった)。
9. 新規性の開示
誇張を避けるため、前例調査の結果をそのまま開示する。
公開研究では前例が見当たらなかった(= 新規の可能性がある):
- vector FP4 を deformable の gather + 双線形補間に適用して FP8 を上回ったこと
- deformable backward(gather + scatter)の量子化を、FP32 staging で mmcv 純正と一致させたこと
- 同一カーネルを sm_100(B200)と sm_121(DGX Spark)の 2 アーキテクチャで比較したこと
前例があり、本研究の独自性ではない部分:
- ms_deform_attn を速くすること単体(TensorRT の FP16/INT8 プラグイン等の前例がある)
- dense attention の FP4 化(FlashAttention-4 等。ただし行列積で別系統)
断り書き: 「前例なし」は、あくまで公開研究の範囲での話である。NVIDIA や自動運転各社の社内に同等の取り組みが存在する可能性は十分にあり、それは外からは確認しようがない。
10. まとめ — 測ったことと、あえて測らなかったこと
GPU 投資への含意を、前回の続きとして実務的に整理する。
- deformable 系の低精度化に速度を期待するなら、GPU を選ぶ。帯域で頭打ちになるコンシューマ級(GB10 など)では FP8/FP4 が効く(large で 2.1〜2.4×)。HBM3e のデータセンタ級(B200)では効かない。
- メモリを減らしたいなら GPU を問わず効く。value FP8 = 1/4、FP4 = 1/8。容量制約で batch や解像度を上げられない場面でこそ出番がある。
- backward(= 学習)の低精度化は、現状では速度の足を引っ張る。staging buffer のコストが効くので、メモリ削減目的に限るのが現実的である。
- 一般則として、「モデル + カーネル + GPU」を一体で評価する。スペック表の比較だけでは ROI を見誤る。
最後に、あえて測らなかったことも明記する。FP4 backward × B200 の速度は測っていない。理由は 5 点ある。(1) 速度の主張は FP8 の 4 象限で完結している。(2) backward の速度は value の bit 数に依存しない要因(ゼロ埋め・atomic・再復号)が支配し、B200 では差がさらに薄まる。(3) Spark で FP4 と FP8 が同オーダー(FP4 が僅かに速い)、B200 で FP8 が negative なので、FP4 も同オーダーの negative と構造的に予測できる。(4) FP4 を測る価値は速度でなく correctness にあり、それは Spark の gradcheck で実証済みである。(5) コスト最適化。つまり「測れなかった」ではなく「測る必要がないと判断した」。判断根拠を隠さず開示することで、片手落ちと見られないようにする。
関連研究・引用・ライセンス
- BEVFormer (ECCV 2022): https://arxiv.org/abs/2203.17270
- Deformable ConvNets v2 (Zhu et al., CVPR 2019): https://arxiv.org/abs/1811.11168
- OpenMMLab / mmcv: https://github.com/open-mmlab/mmcv
- Flash Attention (Dao et al. 2022): https://arxiv.org/abs/2205.14135
- NVIDIA Transformer Engine(FP8 backward の思想の出典): https://github.com/NVIDIA/TransformerEngine
- 前回 README(第3弾 Part I の公開版): https://github.com/nabe2030/bevformer-blackwell
- 本稿のカーネル(第4弾): https://github.com/nabe2030/deformable-attn-blackwell
関連 issue(mmcv 1.x と PyTorch 2.x の互換性): pytorch/pytorch #47050、open-mmlab/mmcv #636 ほか。
ライセンス: 両リポジトリとも Apache 2.0。mmcv / BEVFormer 由来の派生部分は各上流の Apache 2.0 に従う。
本研究の調査・実装・debug・docs 作成に Anthropic Claude Code を使用した。図は別途生成。







