KFMによるインタレ解除は、AvisynthCUDAFiltersで実装されているのだけど、CUDAを使って計算量の多い処理をGPUである程度の速さでに実行できるようになっている。
ただ、RTX4080とかを使っても爆速とはならず、RTX2070から乗り換えた時に思ったよりは速くならないなあという印象だった。
そこで、KFMをもっと高速化できないかなあということで、これまでに新しめのCUDAでビルドできるようにして、動かない個所を修正したりしてきた。
ただ、KFMは本当にたくさんのフィルタの集合体で、GPUフィルタなのもあってこれをすべてちゃんと理解するのは難しい、というかほぼ無理…という感じ。(そもそもインタレ解除のアルゴリズム自体には全然詳しくないし)
それでも別に全体を理解しないと高速化はできないというわけではなくて、純粋にプログラミング的に高速化することは可能だと思ったので、いろいろ調べてみた。
まず、NVIDIAのプロファイラである、Nsight Systemsを使って、KFM動作中のGPUの関数のうち、どれがどのくらい時間かかっているのか、様子を確認してみる。
こうしてみると、kl_searchというCUDAのkernel(関数)が計算時間の大半を占めていることがわかる。なので、普通にいえばまずはここを高速化するのが効果的なのでまずは目指すべき、ということになる。ただ、kl_searchはおそらく動きベクトル探索を行う関数で、正直中身はさっぱりわからんので、高速化といってもどうしたらいいやら…。
ということでもっと簡単に理解できて確実に効果がでそうなあたりを探してみることにした。
1. ビルドしなおす
単純に新しいCUDAの最適化に期待して、コンパイルしなおして高速化できないか、というもの。
RTX4080では高速化したが、GTX1080では逆に遅くなった。謎である。
2. 新命令(__reduce_add_sync)の活用
kl_searchの一部を確認すると、総和演算(いわゆるreduce add)を行っているところがある。
総和をとるというのはよく行う計算のわりに、GPUで高速に行おうとするとちょっと厄介な計算になる。"reduction 最適化"とかで検索すると、すぐにGPUコードの闇を感じることができる黒魔術のようなコードがいっぱい出てくるぐらいには厄介である。まあだいたいはsharedメモリを使ってどうのこうのとか、shuffle命令を使ってどうのこうのとかいうのに落ち着く。
ところが、Ampere以降のGPUには、この厄介なreductionを32個分(1 warp内)をすっきり一命令で高速に実行してくれる命令が追加されている(__reduce_add_sync)ので、これを使ってみた。KFMではkl_searchのほかにも多くの個所でこのreductionが使用されているため、多少効果があった。
3. GPU使用効率の向上
次に、プロファイル結果を見ていくと、kl_searchが重い、という以外にもわかる問題点として、kl_search以外は非常に短時間で終わるCUDAの関数(kernel)が大量に実行されている、という点である。
短時間でおわるなら問題なさそうなものだけど、実際にはこれが大量にあるとわりと問題になる。
例えば、下のような同じ関数が9回連続で呼ばれているところを見てみる。
横方向が時間経過を表していて、青い部分が関数が実際に実行されている時間になっている。
これを見ると、実はkernelの実行とkernelの実行の間に結構な隙間があって、kernelを起動するための時間が、実際の計算時間と同じぐらい(だいたい1.5usぐらい)だけ、それぞれ挟まってしまっている。
また、各kernelの計算量の目安であるスレッド数と実行時間を見比べると、計算量がある程度以上減っても、実行時間が減らないことがわかる。
番号 | スレッド数 (≒計算量) | 実行時間 (us) | 起動時間 (us) |
1 | 412160 | 4.192 | 1.485 |
2 | 110592 | 1.696 | 1.579 |
3 | 87040 | 1.472 | 1.431 |
4 | 12288 | 1.280 | 2.106 |
5 | 4096 | 1.568 | 1.650 |
6 | 3072 | 1.407 | 1.550 |
7 | 1024 | 1.152 | 1.250 |
8 | 1024 | 1.376 | 1.500 |
9 | 1024 | 1.344 | 1.250 |
このとこからわかるようにGPUの特性として、
- kernel切り替えに一定の時間を必要とする
- 少量の計算でも一定の時間を必要とする
というのがあるので、あまり計算量の少ない関数を大量に実行してしまうと、GPUを効率よく稼働させられないことがわかる。
今回は、これに対する古典的な高速化手法である
- 複数のkernelをひとつにまとめる
- まとめられない場合はstreamとよばれるタスクキューを複数作成し、並列に実行する
の2つを実行した。
このあたりの高速化はひとつひとつはたいして速度向上に繋がらないけれど、たくさんのものに対して行うことで、確実に効果を出していくことができるものになっている。
3-1. 複数のkernelをひとつにまとめる
計算量の少ない連続するkernelをひとつのkernelにまとめたり、計算対象ごとに複数回呼び出している関数を一括して実行するようにしたりして、とにかくkernelをまとめていく。
これによって、
- kernel呼び出し回数の削減
- 計算量増大による効率化
によって高速化できる。
たとえば、もともと
だったのが、kernelをまとめたあとは
のようになって、同じ処理を表す緑の範囲が205usから121usに短縮できたし、範囲内のkernel(青いブロック)の数もかなり減らせていることが確認できる。
まあ、まとめた後もまだまだ短時間で終了する関数ばかりだけど、これでも多少は効率化できた。正直、このぐらいではあまり効果がないのだけど、実際には、この箇所だけではなくて、こんな感じのkernelの統合を10か所以上で行って、少しずつ効果積み上げていった。
3-2. streamを使った高速化
GPUではkernelの切り替えに一定の時間がかかるというのは前に書いた通りだけど、kernelを複数並列に実行しておけば、あるkernelの切り替え中に他のkernelが実行できて高速、というのがこのstreamを使った高速化。
基本的に画像にはY,U,Vの3成分があって、フィルタ処理はこの3成分に対して並列に実行できるものが多いので、CUDAのタスクキューであるstreamを3つ使って3並列で実行できるようにしてみた。
たとえば、もともと
だったのがstreamを使うと
こんな感じで、同じ処理を表す緑の範囲が147usから94usに短縮できた。たしかにkernelを3並列で実行して、kernel切り替えの隙間を他のkernelで埋めることができ、同じ処理の実行時間が削減で来ていることがわかる。
4. kl_searchの一部の高速化
とはいえ、こうした高速化を積み上げてもやはり少しづつしか処理時間を削れず、限界はある…ということで、やはり一番遅いkl_searchを高速化したい。
kl_searchで一番重い関数は、dev_calc_sadで、いわゆる差の絶対値和を求める計算になっている。すでに__sadを使ってこれを高速に計算してる(内部命令ではvabsdiff)のだけど、まあとにかく実行回数が多い。実際、kl_searchで実行された命令をチェックしてみると5番目ぐらいにVABSDIFFが来ている。
そこで、対象が8bitの場合、Ampere以降のGPUにはこれを4つ同時に実行してくれる命令(vabsdiff4)が追加された(というか復活した)ので、これを使ってみた。
VABSDIFFがなくなって、代わりにVABSDIFF4が使われているけどかなり数は減っていて、計算負荷が下がったのがわかる。
さすがに重い関数だけあって効果は大きく、kl_searchの所要時間を大きく短縮することができた。
比較
環境CPU | i9 12900K | i7 11700K |
Core | 8P+8E/24T | 8C/16T |
RAM | DDR4-3600 | DDR4-3600 |
GPU | RTX4080 | GTX1080 |
GPUドライバ | 551.23 | 546.33 |
OS | Win11 x64 | Win11 x64 |
Amatsukaze 0.9.6.3/0.9.7.0エンコード: NVEnc 7.50 デフォルト (H.264)
デコード: デフォルト (CPU)
フィルタ設定: KFM(VFR)+SMDegrain+DecombUCF+Deblock
入力ファイル: ささこい #01 (1440x1080, 約30分)
Amatsukaze 0.9.6.3 (オリジナル @ RTX4080)
Amatsukaze 0.9.7.0 (高速化後 @ RTX4080)
高速化詳細
GTX1080の結果も追加して、高速化の内訳はこんな感じ。
エンコードもデコードもボトルネックにならないような、高速化効果が出やすい条件を選んでいるのもあって、RTX4080ではそこそこの高速化になっている。原理的にはAmpere以降のGPUでそれなりに高速化するはず。
一番効果が大きいのが、「3-1. kernelの統合」というのも面白い。ひとつひとつは大したことがないけど、たくさん統合したので目に見える効果になったと思う。
一方、GTX1080については残念ながら少ししか高速化できていない。そもそもビルドしなおすと遅くなるのが謎である…。まあ最終的にはオリジナルより少しだけど高速化しているようでよかった(正直遅くなったときはどうしようかと思った)
ということで、中身のアルゴリズムはあまり理解していないけれど、プログラミング的な工夫である程度の高速化ができた。
AvisynthCUDAFiltersのビルドに挑戦し始めてからかなり時間が経ってしまったけど、目的だった高速化はひとまず達成できてよかった。
ダウンロード>>