fullseye

GPU の損益分岐とパイプライン(第 5 便、2026-11-02〜03)

device="auto" が「GPU が在れば常に GPU」から、入力の大きさ・op・空き VRAM を見て CPU / GPU を選ぶように なった根拠の実測。規則の表は fullseye/data/DEVICE_RULES.json(生成 tools/gen_device_rules.py、生ログ docs/perf/logs/crossover/*.jsonl から決定的に作る。regen_all の鎖に入っている)。

要点

測定条件

1. 単独 op の損益分岐(代表 33 op、一辺 64〜4096 × 枚数 1〜100)

「一辺 X から GPU」= その寸法以上で GPU が CPU より揺れの幅(10 %)を超えて速い。括弧は、いったん GPU が 勝った後に再び CPU が勝った寸法。角括弧は打ち切り・停止・未測定の点(CPU の状態/GPU の状態)。 体積は一辺 32〜512、ncc_locate は画像の一辺。体積の 16 / 100 枚は格子に無い。体積 512³ × 4 枚は vol_median が VRAM を溢れて返らなかった(番犬で停止)ので、他の体積 op も測っていない。

族 op 1 枚 4 枚 16 枚 100 枚 4096² 1 枚 CPU / GPU(ms)
point threshold GPU 勝たず GPU 勝たず GPU 勝たず GPU 勝たず 138.52 / 186.56
point invert GPU 勝たず GPU 勝たず GPU 勝たず GPU 勝たず 158.87 / 193.73
point gamma 一辺 256 から GPU 一辺 128 から GPU(256 で再び CPU) 一辺 64 から GPU(1024 で再び CPU) 一辺 64 から GPU(256 で再び CPU) 283.19 / 231.71
point sigmoid 一辺 512 から GPU 一辺 512 から GPU 一辺 64 から GPU(128,256,512 で再び CPU) 一辺 512 から GPU 502.61 / 127.07
point scale_clip 一辺 128 から GPU(256 で再び CPU) 一辺 512 から GPU 一辺 64 から GPU(128,256 で再び CPU) 一辺 512 から GPU 327.75 / 111.02
separable gaussian 一辺 512 から GPU 一辺 512 から GPU 一辺 64 から GPU 一辺 64 から GPU[4096:ok/aborted] 569.75 / 131.04
separable mean_box 一辺 1024 から GPU 一辺 1024 から GPU 一辺 1024 から GPU 一辺 1024 から GPU 1114 / 196.87
separable sobel_mag 一辺 512 から GPU 一辺 512 から GPU 一辺 128 から GPU 一辺 64 から GPU 1625 / 82.22
separable dog 一辺 256 から GPU 一辺 128 から GPU 一辺 64 から GPU 一辺 64 から GPU 1215 / 87.79
separable unsharp 一辺 512 から GPU 一辺 512 から GPU 一辺 64 から GPU(128 で再び CPU) 一辺 64 から GPU 596.77 / 80.40
nonseparable median 一辺 64 から GPU 一辺 64 から GPU 一辺 64 から GPU[4096:cutoff/skipped_vram] 一辺 64 から GPU[2048:cutoff/aborted] 8564 / 192.10
nonseparable percentile 一辺 64 から GPU 一辺 64 から GPU(4096 で再び CPU) 一辺 64 から GPU(2048 で再び CPU)[4096:cutoff/skipped_vram] 一辺 64 から GPU[1024:ok/aborted; 2048:cutoff/skipped_vram] 9231 / 196.10
nonseparable bilateral 一辺 256 から GPU 一辺 64 から GPU 一辺 64 から GPU[2048:cutoff/ok; 4096:cutoff/ok] 一辺 64 から GPU[1024:cutoff/ok; 2048:cutoff/ok] 14788 / 121.03
nonseparable std_filter 一辺 256 から GPU 一辺 128 から GPU 一辺 64 から GPU 一辺 64 から GPU 1073 / 94.52
nonseparable laplace 一辺 512 から GPU 一辺 256 から GPU 一辺 128 から GPU 一辺 64 から GPU 679.18 / 110.41
morphology min_filter 一辺 128 から GPU 一辺 64 から GPU 一辺 64 から GPU 一辺 64 から GPU 799.83 / 107.64
morphology gopen 一辺 128 から GPU 一辺 64 から GPU 一辺 64 から GPU 一辺 64 から GPU 975.92 / 110.79
morphology tophat 一辺 128 から GPU 一辺 64 から GPU 一辺 64 から GPU 一辺 64 から GPU 1004 / 120.16
morphology erosion_circle 一辺 1024 から GPU 一辺 128 から GPU(256,512 で再び CPU) 一辺 64 から GPU 一辺 64 から GPU 257.55 / 107.45
morphology reg_dilate 一辺 256 から GPU 一辺 64 から GPU(256 で再び CPU) 一辺 64 から GPU 一辺 64 から GPU 270.73 / 90.35
frequency lowpass 一辺 64 から GPU 一辺 64 から GPU 一辺 64 から GPU 一辺 64 から GPU 2414 / 91.57
frequency highpass 一辺 64 から GPU 一辺 64 から GPU 一辺 64 から GPU 一辺 64 から GPU 2411 / 89.28
iterative sk_tv 一辺 256 から GPU 一辺 256 から GPU[4096:cutoff/ok] 一辺 128 から GPU[2048:cutoff/ok; 4096:cutoff/ok] 一辺 64 から GPU[1024:cutoff/ok; 2048:cutoff/ok] 47330 / 202.82
iterative fill_holes GPU 勝たず GPU 勝たず GPU 勝たず 一辺 64 から GPU(512,1024 で再び CPU)[2048:ok/aborted] 1053 / —
iterative canny 一辺 256 から GPU 一辺 128 から GPU 一辺 64 から GPU 一辺 64 から GPU 1944 / 98.07
iterative equalize 一辺 128 から GPU 一辺 64 から GPU 一辺 64 から GPU 一辺 64 から GPU 555.09 / 107.98
iterative otsu 一辺 512 から GPU 一辺 512 から GPU 一辺 512 から GPU 一辺 512 から GPU 423.59 / 118.84
volume vol_gaussian 一辺 32 から GPU 一辺 32 から GPU GPU 未測定 GPU 未測定 — / —
volume vol_median 一辺 32 から GPU 一辺 32 から GPU[512:cutoff/aborted] GPU 未測定 GPU 未測定 — / —
volume vol_erode 一辺 32 から GPU 一辺 32 から GPU GPU 未測定 GPU 未測定 — / —
volume vol_threshold 一辺 32 から GPU(64 で再び CPU) 一辺 64 から GPU GPU 未測定 GPU 未測定 — / —
volume vol_opening_ball 一辺 32 から GPU 一辺 32 から GPU GPU 未測定 GPU 未測定 — / —
match ncc_locate 一辺 64 から GPU 一辺 64 から GPU 一辺 64 から GPU[4096:cutoff/ok] 一辺 64 から GPU[2048:cutoff/ok] 7827 / 159.13

初回起動(別プロセス 10 回の中央値)

import_api_s は import api 全体(torch の import を含む)。context_s は CUDA の文脈を作る時間、 first_op_s は最初の op(kernel のコンパイル・cuDNN の選択を含む)、second_op_s は同じ op の 2 回目。 規則の表は常駐後(2 回目以降)の値で作った。初回の固定費(約 0.45 s)はプロセスに 1 回なので閾値には 入れず、before/after の「初回起動」の行で効き目を別に測った。

項目 中央値 回数
import_api_s 3061 ms 10
is_available_s 22.16 ms 10
context_s 129.36 ms 10
first_op_s 325.61 ms 10
second_op_s 25.78 ms 10
first_other_op_s 56.28 ms 10

2. パイプライン(4 本 × 全 GPU / 混在)

1 枚(ms、CPU / GPU。GPU は転送込み)

パイプライン 変種 128 256 512 1024 2048 4096
scratch_surface all 0.86 / 3.44 2.33 / 2.26 15.64 / 6.71 99.94 / 14.97 863.93 / 64.81 2331 / 196.27
scratch_surface mixed 2.48 / 5.57 6.63 / 10.87 29.72 / 27.74 191.81 / 150.10 868.45 / 690.96 4611 / 2301
edge_dimension all 5.64 / 32.67 20.49 / 52.37 108.09 / 138.39 428.89 / 210.13 1712 / 443.13 7695 / 4923
edge_dimension mixed 5.93 / 75.46 24.46 / 109.71 163.39 / 174.89 591.09 / 441.04 2123 / 1415 8405 / 4233
shading_blob all 1.92 / 1.72 6.06 / 1.58 54.29 / 2.96 227.26 / 7.64 1009 / 35.80 5027 / 104.11
shading_blob mixed 1.89 / 1.65 5.57 / 2.03 31.63 / 7.65 131.30 / 24.67 582.97 / 97.50 2459 / 351.09
interleaved_worst all 4.65 / 1.22 17.25 / 1.60 85.56 / 3.33 352.58 / 8.80 1569 / 32.60 6952 / 114.33
interleaved_worst mixed 8.47 / 4.67 30.24 / 11.55 139.36 / 46.45 684.95 / 218.86 2818 / 808.82 11874 / 3206

16 枚(ms、CPU / GPU。GPU は転送込み)

パイプライン 変種 128 256 512 1024 2048 4096
scratch_surface all 10.78 / 2.71 38.57 / 20.96 230.18 / 53.58 1684 / 189.73 13334 / 885.26 未測定
scratch_surface mixed 37.63 / 40.11 99.80 / 116.95 508.11 / 469.63 3293 / 2407 17744 / 12917 未測定
edge_dimension all 85.63 / 34.07 329.96 / 67.87 1514 / 281.25 6755 / 935.16 27882 / 7209 未測定
edge_dimension mixed 141.35 / 99.42 510.14 / 357.92 2025 / 1109 8547 / 4965 ok/aborted 未測定
shading_blob all 48.97 / 2.87 138.81 / 6.22 759.66 / 24.44 3601 / 101.83 15207 / 432.64 未測定
shading_blob mixed 22.54 / 6.08 82.16 / 19.02 486.15 / 103.09 2190 / 344.18 9505 / 1434 未測定
interleaved_worst all 68.08 / 2.74 271.55 / 13.34 1297 / 52.70 5936 / 118.40 25350 / 455.63 未測定
interleaved_worst mixed 133.14 / 47.93 514.26 / 148.75 2263 / 758.50 10611 / 3314 44077 / 13184 未測定

内訳(1 枚、一辺 2048、ms。GPU 側の中央値の回の内訳)

パイプライン 変種 GPU 区間 GPU 段 CPU 段 往路 回/ms 復路 回/ms CPU 段 ms kernel ms 計 ms
scratch_surface all 1 5 0 1 / 25.80 1 / 28.65 0.00 10.36 64.81
scratch_surface mixed 2 5 1 2 / 62.05 2 / 44.72 558.28 25.90 690.96
edge_dimension all 1 5 0 1 / 24.70 1 / 23.30 0.00 395.14 443.13
edge_dimension mixed 2 4 1 2 / 26.72 2 / 57.71 746.49 583.94 1415
shading_blob all 1 5 0 1 / 14.18 1 / 11.02 0.00 10.61 35.80
shading_blob mixed 2 4 1 2 / 29.53 2 / 22.60 35.82 9.55 97.50
interleaved_worst all 1 5 0 1 / 14.06 1 / 10.27 0.00 8.28 32.60
interleaved_worst mixed 4 5 3 4 / 52.22 4 / 43.93 705.21 7.46 808.82

測り直し(1 枚、同じプロセスで CPU と GPU を交互に 15 回ずつ、中央値 ms)

パイプライン 変種 一辺 元の CPU / GPU 測り直し CPU / GPU
edge_dimension all 128 5.64 / 32.67 4.71 / 14.01
edge_dimension all 256 20.49 / 52.37 18.46 / 27.98
edge_dimension mixed 128 5.93 / 75.46 5.74 / 19.03
edge_dimension mixed 256 24.46 / 109.71 21.58 / 30.76
interleaved_worst all 128 4.65 / 1.22 3.89 / 1.45
interleaved_worst all 256 17.25 / 1.60 15.03 / 1.85
interleaved_worst mixed 128 8.47 / 4.67 6.52 / 4.97
interleaved_worst mixed 256 30.24 / 11.55 22.82 / 11.46
scratch_surface all 128 0.86 / 3.44 0.74 / 1.07
scratch_surface all 256 2.33 / 2.26 2.24 / 1.14
scratch_surface mixed 128 2.48 / 5.57 2.12 / 2.75
scratch_surface mixed 256 6.63 / 10.87 5.62 / 4.87
shading_blob all 128 1.92 / 1.72 1.60 / 1.66
shading_blob all 256 6.06 / 1.58 5.54 / 1.76
shading_blob mixed 128 1.89 / 1.65 1.43 / 1.66
shading_blob mixed 256 5.57 / 2.03 4.52 / 2.24

3. 規則と採否

規則(device_policy.route)

  1. 単独 op: op ごとの閾値(画素数)未満なら CPU。閾値は「掃引の全つまみ(a = 0.2 / 0.5 / 0.9)・全寸法 (一辺 128〜2048)で、CPU が揺れの幅を超えて速かった最後の寸法の直後」。2048 まで CPU が勝ち続けた op は 格子の一辺 4096 で同じ判定をする。さらに before/after の 1 回目で CPU が遅かった寸法は GPU に戻す。
  2. 枚数 n > 1: 族ごとの枚数の表で閾値を下げる(GPU 寄りにしか動かない)。
  3. 2 段以上の鎖: GPU 経路を持つ全段が単独で CPU に残る大きさなら CPU(min_stage)。測った鎖の全点で 遅くならないときだけ採用する(表の pipeline.adopted、不採用なら負けた点を violations に残す)。
  4. 判断できないとき(表が無い・壊れている・表に無い GPU 経路の op・大きさが判らない)は GPU(規則の前の挙動)。

族ごとの閾値(出荷する表):

族 op 数 CPU に残す上限の一辺(op 数)
frequency 2 ≤128: 2
geometry 3 ≤256: 2, ≤512: 1
iterative 7 0(常に GPU): 1, ≤128: 3, ≤2048: 2, ≤256: 1
morphology 31 0(常に GPU): 17, ≤127: 1, ≤128: 6, ≤256: 4, ≤361: 1, ≤512: 2
nonseparable 15 0(常に GPU): 10, ≤128: 3, ≤256: 2
point 10 ≤1024: 1, ≤128: 2, ≤256: 2, ≤512: 5
separable 22 ≤128: 8, ≤256: 9, ≤361: 1, ≤512: 4

経緯(失敗も残す)

before / after

before = 規則の前(device="cuda" = CUDA が在れば常に GPU)、after = device="auto"。規則が CPU に回す 点だけを測る(GPU に送る点は before と同じ道なので差が出ない)。一辺は掃引の寸法と間の寸法 (90, 128, 181, 256, 362, 512, 724, 1024, 1448, 2048)、つまみ 0.5 / 0.9。初回起動は別プロセスで gaussian 256² × 1 / 20 回、threshold 1024² × 20 回、fill_holes 1024² × 5 回を 3 回ずつ。

before_after.jsonl

before_after_2.jsonl

採用の門(tests/test_device_rules.py): 出荷する表で CPU に回る op の点の集合が 2 回目のログと一致し (測り切っている)、揺れの幅を超えて遅い点が 0、合計で after < before。

4. 許可リストの道(gpu_shadow)

fullseye/data/gpu_auto_allow.json(機械的 GPU 化の掃引、docs/perf/gpu_auto_sweep_2026-11-02.md)の 701 行(単独 ops 521 / 常駐 resident_ops 141 / 鎖 pipeline_ops 39)を表の shadow に写した。 倍率は実寸(画像 2048²、体積 256³、信号 2²⁰ など)で測った値なので、auto は測った大きさ以上の入力 でだけこの道を選ぶ(min_elements、未満は below_measured_size で CPU)。測った形が判らない 79 行は auto では選ばない(明示の cuda では使う)。常駐・鎖の枠は、隣の段も許可リストで GPU に載せたまま 続くときだけ使う(単独や孤立した段は chain_only で CPU)。

抜き取り(単独 ops の登録 op、入力 image、名前順に等間隔 12 件)。before = CPU(規則の前、手書き経路の 無い op は CPU)、after = auto:

op 一辺 before(CPU)ms after(auto)ms 倍率 after の経路
affine_trans_image 2048 271.34 15.68 17.30 allow_single
affine_trans_image 1024 66.84 66.88 1.00 cpu(below_measured_size)
aug_barrel 2048 307.20 23.40 13.13 allow_single
aug_barrel 1024 74.61 73.73 1.01 cpu(below_measured_size)
clahe 2048 343.41 36.65 9.37 allow_single
clahe 1024 89.21 89.67 0.99 cpu(below_measured_size)
edges_sub_pix 2048 1552 124.33 12.48 allow_single
edges_sub_pix 1024 196.42 196.20 1.00 cpu(below_measured_size)
gabor 2048 427.39 36.55 11.69 allow_single
gabor 1024 95.95 96.80 0.99 cpu(below_measured_size)
hx_gen_lowpass 2048 62.93 33.71 1.87 allow_single
hx_gen_lowpass 1024 12.56 12.23 1.03 cpu(below_measured_size)
it_bit_lshift 2048 124.41 37.71 3.30 allow_single
it_bit_lshift 1024 26.01 27.01 0.96 cpu(below_measured_size)
iv_wiener_deconv_spatial 2048 620.35 23.06 26.90 allow_single
iv_wiener_deconv_spatial 1024 130.82 130.07 1.01 cpu(below_measured_size)
median_rect 2048 1839 22.89 80.34 allow_single
median_rect 1024 457.88 456.25 1.00 cpu(below_measured_size)
quantize_lloyd_max 2048 1005 52.61 19.10 allow_single
quantize_lloyd_max 1024 248.15 247.55 1.00 cpu(below_measured_size)
sk_adjust_log 2048 104.53 38.19 2.74 allow_single
sk_adjust_log 1024 24.05 24.17 1.00 cpu(below_measured_size)
tf_gradient_domain_reintegrate 2048 592.97 23.85 24.86 allow_single
tf_gradient_domain_reintegrate 1024 121.01 113.41 1.07 cpu(below_measured_size)

hx_gen_lowpass(1.9 倍)・sk_adjust_log(2.7 倍)・it_bit_lshift(3.3 倍)は許可リストの 5 倍に 届いていない(掃引と別の環境・別の時刻。どれも CPU より速いので after < before は保たれる)。

5. VRAM の閾値

auto は GPU を選ぶ前に空き VRAM(torch.cuda.mem_get_info、無ければ CuPy の memGetInfo、どちらも 装置全体の空き = 他のプロセスの使用分を引いた値)を見て、FULLSEYE_GPU_MIN_FREE_MB 未満なら CPU (vram_low)。問い合わせられなければ判定しない。値は 1 秒覚える。0 で判定を切る。明示の cuda と FULLSEYE_DEVICE はこの判定より優先する。

既定 2048 MiB の根拠(実測、docs/perf/logs/crossover/ctx_mem.jsonl と格子の peak_mib):

6. 経路の選択は 1 か所(gpu_route.route)

gpu_route.route(op_name, context="single" | "pipeline") -> Route | None。apply / run_pipeline / explain_device / MCP はこの関数だけを呼び、GPU の台帳(手書き経路の表・許可リスト・native の実測)を 読むのもこのモジュールだけ(tests/test_device_select.py が他のファイルの直読みを拒む)。

7. 開示(AI と人が選ぶために)

測れなかったもの・残り