モバイル&ワイヤレスブロードバンドでインターネットへ

gwaw.jp
 
GWAW.JP / TENSORFLOW.JS / BACKEND BENCHMARK
TENSORFLOW.JS EXPERIMENT

TensorFlow.js バックエンド
速度比較
CPU / WebGL / WebGPU

演算の種類とサイズを変えながら3バックエンドの実行時間を計測。 「小さいモデルはCPUが速い」の理由と、GPUが逆転する境界を実機で示します。

CPU WebGL WebGPU
01

概要

TensorFlow.js は同じコードで CPUWebGLWebGPU の3つのバックエンドを切り替えられます。 しかし「GPU の方が速い」という直感は、演算の種類やデータサイズによっては 正反対の結果をもたらすことがあります。

この記事では Dense・LSTM・Conv2D・Activation など演算の種類を変えながら 3バックエンドの実行時間を計測し、どの演算でどのバックエンドを選ぶべきかを データで示します。

02

動作環境・注意事項

Chrome 113+✔ 3バックエンド全て計測可能(推奨)
Edge 113+✔ 3バックエンド全て計測可能
Firefox△ WebGPU 非対応。CPU・WebGL のみ計測
Safari 18+△ WebGPU 実験的対応。CPU・WebGL推奨
モバイル△ Android Chrome は動作可。デバイスにより結果が大きく異なります
計測精度△ バックグラウンドの負荷・熱による変動があります。傾向を見てください
⚠️ WebGPU 非対応環境では WebGPU の列はスキップされ CPU・WebGL のみ計測されます。 それでも「CPU vs WebGL」の比較として有効な情報が得られます。

03

ベンチマーク・コンソール

▶ TFJS BACKEND BENCHMARK TensorFlow.js 初期化中...
BACKENDS
初期化中...
FASTEST OVERALL
CPU WIN COUNT
演算種別
WEBGL WIN COUNT
演算種別
WEBGPU WIN COUNT
演算種別
MAX SPEEDUP

EXECUTION TIME BY OPERATION (ms) — LOWER IS BETTER

DENSE LAYER — SIZE SWEEP

SPEEDUP vs CPU (×)

OPERATION SIZE CPU (ms) WebGL (ms) WebGPU (ms) FASTEST
RUN ALL を押して計測を開始してください
04

動作原理:なぜバックエンドで速さが変わるか

3バックエンドの違いは「どこで・どうやってテンソル演算を実行するか」です。 データの転送コストと並列度のバランスが、速さを決定します。

CPU BACKEND

転送ゼロ・逐次処理

JavaScriptエンジン上で直接実行。GPUへのデータ転送がないため、小さい演算・少ないデータ量では最速。並列度は低いため大規模演算では遅い。

WEBGL BACKEND

テクスチャ経由のGPU

テンソルをテクスチャとしてGPUに転送し、GLSL シェーダで計算。転送のオーバーヘッドがあるが、中〜大規模の行列演算では並列処理が効く。

WEBGPU BACKEND

Storage Buffer直接アクセス

テクスチャ不要でStorage BufferにデータをGPUと共有。WebGLより転送効率が高く、大規模・バッチ処理で最も有利。ただし小規模では転送コストが勝る。

⚖️ 転送コスト vs 並列効果
GPU バックエンドを使う場合、CPU↔GPU のデータ転送が必ず発生します。 演算量が少ないと「転送コスト > 並列効果」となり CPUより遅くなります。 演算量が増えると「並列効果 > 転送コスト」に逆転します。この境界がデバイスごとに異なり、 実測してみないとわからない点がベンチマークの価値です。
05

コードのポイント解説

TensorFlow.js でバックエンドを切り替えながら計測する実装のポイントを解説します。

バックエンドの切替と初期化
// バックエンドを切り替えるたびに setBackend → ready を呼ぶ
async function switchBackend(name) {
  // WebGPU は navigator.gpu で事前チェック
  if (name === 'webgpu' && !navigator.gpu) {
    return false;
  }
  try {
    await tf.setBackend(name);
    await tf.ready();
    // 実際に切り替わったか確認(フォールバック検出)
    return tf.getBackend() === name;
  } catch (e) {
    return false;
  }
}

setBackend() だけでは実際に切り替わらない場合があります。 tf.getBackend() で確認するのが確実です。

ウォームアップ込みの計測関数
async function measure(fn, warmupRuns, measureRuns) {
  // ウォームアップ: JIT・シェーダコンパイルを済ませる
  for (let i = 0; i < warmupRuns; i++) {
    const t = fn();
    await t.data();   // GPU 完了を待つ
    t.dispose();
  }

  // 本計測
  const times = [];
  for (let i = 0; i < measureRuns; i++) {
    const t0 = performance.now();
    const result = fn();
    await result.data();  // GPU 同期(これがないと未完了のまま時刻を取る)
    const t1 = performance.now();
    result.dispose();
    times.push(t1 - t0);
  }

  // 最小値(最速スコア)と中央値を返す
  times.sort((a, b) => a - b);
  return {
    min:    times[0],
    median: times[Math.floor(times.length / 2)],
  };
}

ハイライトの await result.data() が計測精度の核心です。 TensorFlow.js の演算は非同期でGPUにオフロードされるため、 data() でデータを取り出すまで実際の完了時刻が確定しません。 このウェイトがないと、全バックエンドがほぼ同じ(= 非常に小さい)時間を返してしまいます。

各演算のベンチマーク定義
const OPS = [
  {
    name: 'Dense (MatMul)',
    fn: (sz) => () => {
      const x = tf.randomNormal([sz.batch, sz.in]);
      const w = tf.randomNormal([sz.in, sz.out]);
      return tf.matMul(x, w);
    },
  },
  {
    name: 'LSTM Cell',
    fn: (sz) => () => {
      const model = tf.sequential();
      model.add(tf.layers.lstm({ units: sz.units, inputShape: [sz.seq, sz.feat] }));
      const x = tf.randomNormal([sz.batch, sz.seq, sz.feat]);
      return model.predict(x);
    },
  },
  {
    name: 'Conv2D',
    fn: (sz) => () => {
      const x = tf.randomNormal([sz.batch, sz.h, sz.w, sz.ch]);
      const f = tf.randomNormal([3, 3, sz.ch, sz.filters]);
      return tf.conv2d(x, f, 1, 'same');
    },
  },
  {
    name: 'Softmax',
    fn: (sz) => () => tf.softmax(tf.randomNormal([sz.batch, sz.classes])),
  },
];

各演算を関数として定義しておき、バックエンドを切り替えながら同じ関数を呼ぶ設計にすることで、 公平な比較が可能になります。演算ごとに「サイズパラメータ」を注入できる構造です。

メモリリークを防ぐ dispose 管理
// tf.tidy() でスコープ内のテンソルを自動解放
const result = tf.tidy(() => {
  const x = tf.randomNormal([batch, n]);
  const w = tf.randomNormal([n, n]);
  return tf.matMul(x, w);  // x, w は tidy 終了時に自動 dispose
});

// result だけ手動 dispose(tidy の外に出ているため)
await result.data();
result.dispose();

// テンソル数の確認(ベンチマーク中はこれで監視する)
console.log(tf.memory().numTensors);

ベンチマークは同じ演算を何十回も繰り返すため、テンソルの dispose() 忘れが メモリリークとなり後の計測を汚染します。tf.tidy() で スコープ管理するか、明示的に dispose() するのを徹底してください。

06

考察:バックエンド選択の指針

ベンチマーク結果から導かれる、実用的なバックエンド選択の考え方を整理します。

🏆 CPU が有利なケース
小さいモデル・少ないバッチサイズ・LSTM / RNN 系。 特にシーケンス長が短い LSTM は CPU が最速になることが多いです。 転送コストに対して演算量が少ない「軽量推論」は CPU を選ぶ理由が十分あります。
🚀 WebGPU が有利なケース
大きい行列積(Dense)・Conv2D・大バッチ推論。 入力サイズが大きくなるほど WebGPU の並列性が転送コストを上回ります。 画像系モデル(MobileNet 等)の推論や、バッチサイズ 32 以上の学習では WebGPU が明確に有利になる傾向があります。
📊 WebGL の立ち位置
WebGPU が使えない環境(Firefox・一部のSafari)での GPU バックエンドとして有効。 中規模の演算では WebGPU に劣るものの、CPU よりは速いケースが多く、 対応環境の広さが最大の強みです。

以上をまとめると、実践的な選択指針は以下の通りです。

ユースケース 推奨バックエンド 理由
LSTM / RNN 推論(小) CPU 転送コストが演算コストを上回るため
Dense 推論(大) WebGPU 行列積の並列性が最大限に活かせる
Conv2D / 画像系 WebGPU 大規模並列演算でGPUが圧倒的
Softmax / 活性化関数単体 CPU 要素ごとの演算は並列化の恩恵が少ない
バッチ学習(中〜大) WebGPU → WebGL 実測して判断。自動切替ロジックが有効
対応環境を広くしたい WebGL Firefox 含め広い環境で GPU 演算が可能
💡 自動選択の実装例
実際のアプリでは「モデルサイズとバッチサイズに応じて自動切替」が最善です。
07

実機計測レポート — iPad mini A17 Pro

このベンチマークを iPad mini(A17 Pro チップ)/ Safari + WebGPU 環境で実行した 実測値を掲載します。モバイル最高クラスのSoCでも、演算の種類によって 最速バックエンドが大きく異なることが確認できます。

演算 サイズ CPU (ms) WebGL (ms) WebGPU (ms) 最速
Dense (MatMul) batch=8 in=256 out=256 5.00 8.00 3.00 WebGPU
LSTM Cell batch=1 seq=20 feat=16 units=32 2.00 98.00 12.00 CPU
Conv2D batch=4 32×32 ch=8 f=16 4.00 9.00 2.00 WebGPU
Softmax batch=16 classes=256 1.00 8.00 1.00 CPU
Layer Norm batch=8 dim=256 1.00 8.00 1.00 WebGPU
🔍 注目:LSTM Cell の WebGL が 98ms
CPU の 2ms に対して WebGL は 98ms(約50倍)という結果は、 LSTMのゲート演算がテクスチャ経由のGPU処理と根本的に相性が悪いことを示しています。 WebGPU は 12ms まで改善していますが、それでも CPU の 6 倍遅い。
🚀 Dense は N=64 付近で逆転
Dense サイズスイープのグラフでは、N=64 付近で CPU と WebGPU が逆転しています。 WebGL は N=32 から既に CPU より遅く、小規模ではWebGLが最も不利。 A17 Pro という最高クラスのモバイル GPU でも、小さい行列では CPU が勝つという事実は、 アーキテクチャの問題ではなく アルゴリズムの逐次性と転送コストの問題です。
📱 モバイル環境での特徴
デスクトップ GPU と比較すると A17 Pro の WebGPU は Dense で 3ms・Conv2D で 2ms と 非常に優秀ですが、WebGL の LSTM 98ms のような極端な遅さも同時に見られます。 モバイルでは バックエンドの選択ミスがデスクトップより大きなペナルティになるため、 演算種別ごとの適切な選択がより重要です。

『TensorFlow.js バックエンド速度比較 CPU / WebGL / WebGPU』を公開しました。