cuBLAS 13 の固定小数点エミュレーション(尾崎スキーム系)を、FP64 を意図的に削られたコンシューマ GPU で実測した記録。測定日 2026-10-04
GeForce の倍精度は仕様上 FP32 の 1/64 に制限されている。一方で INT8 テンソルコアは3桁 TOPS ある。CUDA 13.0 Update 2 で cuBLAS に入った固定小数点エミュレーションは、FP64 の積を整数の列に分解して INT8 テンソルコアで厳密に計算する——つまり潤沢な資源で枯渇した資源を代替する。
公開されている測定値はデータセンター GPU(native FP64 が速い機種)が中心で、そこでの利得は控えめに見える。しかし FP64 を削られたコンシューマ機では前提が逆であり、倍率が大きくなるはずだ。その実測値がほとんど見当たらないので、自分で測った。
cublas64_13.dll と cudart64_13.dll を Python の ctypes から直接叩いている。DLL は読み取りのみで、エンジン側には一切変更を加えていない。| n | native FP64 TFLOPS | emulated, easy TFLOPS | emulated, hard TFLOPS | speedup easy | speedup hard | rel.err native | rel.err emu easy | rel.err emu hard |
|---|---|---|---|---|---|---|---|---|
| 2048 | 1.482 | 5.494 | 4.630 | 3.71× | 3.13× | 1.47e-15 | 0 | 0 |
| 4096 | 1.656 | 9.992 | 7.023 | 6.03× | 4.24× | 2.19e-15 | 0 | 0 |
| 8192 | 1.687 | 13.704 | 9.808 | 8.12× | 5.81× | 2.29e-15 | 1.15e-16 | 0 |
| 16384 | 1.719 | 14.124 | 9.532 | 8.22× | 5.55× | 3.98e-15 | 1.34e-16 | 1.34e-16 |
読み方。native FP64 は行列サイズを変えても 1.48–1.72 TFLOPS で張り付いている。これがレート制限の壁。エミュレーション側は n とともに伸び、大きい行列ほど差が開く。入力の指数レンジが広い «hard» では必要マンティッサビットが増えるため利得は下がるが、それでも 5.8× を保っている。
| n=16384 | 相対誤差 中央値 | 相対誤差 最悪値 |
|---|---|---|
| native FP64 | 3.98e-15 | 4.31e-13 |
| emulated (ADP) | 1.34e-16 | 4.44e-15 |
中央値では 30 倍の差だが、最悪値では 97 倍開く。native FP64 の誤差は n 項の総和にわたる累積なので、打ち消しが起きた要素で大きく悪化する。固定小数点エミュレーションは整数で厳密に積和して最後に一度だけ丸めるため、この種の最悪ケースを構造的に持たない。条件数を2乗する正規方程式のような用途では、この最悪値の差がそのまま効いてくる。
CUBLAS_EMULATION_STRATEGY は performant(エミュレーションが得と判断したときだけ使う)と eager(積極的に使う)を取る。native がここまで遅いカードでは performant でも常にエミュレーションが選ばれるため、差は出なかった。
| CUBLAS_EMULATION_STRATEGY | input | TFLOPS | rel.err (median) |
|---|---|---|---|
performant | easy | 13.634 | 1.17e-16 |
performant | hard | 9.767 | 0 |
eager | easy | 13.688 | 1.17e-16 |
eager | hard | 9.770 | 0 |
既定では ADP(automatic dynamic precision)が入力から必要ビット数を自動決定する。CUBLAS_FIXEDPOINT_EMULATION_MANTISSA_BIT_COUNT で固定すると、速度と精度を直接交換できる。
| mantissa bits | TFLOPS easy | TFLOPS hard | rel.err easy | rel.err hard |
|---|---|---|---|---|
| 8 | 95.397 | 98.099 | 1.30e-04 | 1.18e-03 |
| 12 | 95.285 | 98.607 | 1.30e-04 | 1.18e-03 |
| 16 | 60.850 | 62.757 | 4.88e-07 | 1.14e-05 |
| 20 | 60.933 | 62.440 | 4.88e-07 | 1.14e-05 |
| 24 | 41.920 | 43.062 | 1.77e-09 | 5.63e-08 |
| 28 | 42.599 | 43.193 | 1.77e-09 | 5.63e-08 |
| 32 | 30.146 | 31.104 | 9.12e-12 | 2.34e-10 |
| 40 | 22.732 | 23.328 | 2.98e-14 | 9.89e-13 |
| 48 | 17.793 | 18.308 | 1.61e-16 | 5.59e-15 |
| 53 | 17.775 | 18.367 | 1.61e-16 | 5.59e-15 |
階段状のプラトーが実装を露出させている。要求ビット数を増やしても値が動かない区間が並ぶのは、ビット数が離散的なスライス数に切り上げられているためだ。INT8 のスライスを何枚積むかという内部構造が、外から観測できてしまっている。
25 秒間 DGEMM を連続投入し、その間 nvidia-smi を 200 ms 間隔でサンプリングした。立ち上がりの 3 秒は定常状態から除外。
| mode | sustained TFLOPS | mean board power | GFLOP/s per watt | efficiency vs native | SM clock | peak temp |
|---|---|---|---|---|---|---|
native | 1.716 | 221 W | 7.8 | 1.00× | 2883 MHz | 55 °C |
adp | 14.446 | 567 W | 25.5 | 3.29× | 2195 MHz | 70 °C |
mant48 | 18.516 | 570 W | 32.5 | 4.19× | 2181 MHz | 74 °C |
mant08 | 147.476 | 575 W | 256.4 | 33.08× | 2490 MHz | 76 °C |
native FP64 は電力枠を使い切れない。575 W の枠に対して 221 W しか引かず、SM クロックは 2883 MHz と高いまま張り付く。稼働しているダイ面積が小さすぎて、電力ではなくクロックで律速されているということだ。エミュレーション側は INT8 テンソルコアを動かすため 575 W まで上がり(電力上限に張り付く)、クロックは 2490 MHz に下がる。つまりこの技術は使われずに余っていた電力枠をスループットに変換している。
結果として、FP64 相当の精度を保ったまま電力効率が 4.2 倍になる。消費電力は 2.6 倍に増えるが、仕事量が 10.8 倍になるためだ。
cudaEvent による GPU 時間。warmup 2 回ののち 5 回(掃引では 3 回)測り中央値を採用。FLOP は 2n³。math.fma による two-product で各積の丸め誤差を取り出し、積と誤差の両方を math.fsum で厳密に総和する。これは正しく丸めた内積に一致する。出力行列からランダムに抽出した要素について相対誤差を取った。easy は一様乱数 [-1,1]。hard は正規乱数に 2U(-10,10) を掛けて指数レンジを広げたもの。固定小数点分割の必要ビット数は指数レンジで決まるため、この2本が上下の目安になる。git clone https://github.com/soy-tuber/fp64-emulation-rtx5090
cd fp64-emulation-rtx5090
# CUDA 13.0u2 以降の cublas64_13.dll / cudart64_13.dll があるディレクトリ
set FP64EMU_CUDA_DIR=C:\path\to\cuda13\dlls
python run_all.py # 全モードを掃引し results/*.json を作る
python make_report.py # docs/index.html を生成する
個別に1モードだけ測る場合:
set CUBLAS_EMULATE_DOUBLE_PRECISION=1
python dgemm_bench.py --sizes 16384 --dists easy,hard --label emu