GTC 2014において、Lars Nyland、Dale Southard、Alex Fit-Floreaという3人の NVIDIAの技術者が、浮動小数点演算に関する誤解を解き、精度の高い計算を行うための手法を説明するという発表を行った。
Top500に使われるHPL(High Performance Linpack)の計算をCPUとGPUにやらせて、その中間結果を詳細に比較すると、値が微妙に異なっている。このため、GPUの浮動小数点演算の精度が低く、誤差が出ているという認識を持っている人がいる。
しかし、IEEE 754規格では、計算結果は無限精度で計算した値を、数値表現が持つビット数(10進の場合の有効桁数)に丸めた答えを出すと規定されており、IEEE 754規格に準拠していれば、CPUでもGPUでも加減乗除の計算の答えは完全に一致していなければならない。
それでは、なぜHPLなどの計算結果が一致しないのか? どうしたら一致、あるいは違いを小さくすることができるのかを説明するのが、この発表である。
浮動小数点演算とは
まず、浮動小数点演算を行う場合の数値の表現形式は、図1のようになっている。単精度の場合は全体が32bitで、符号のsが1ビット、指数eが8ビット、数値を表すfが23ビットである。倍精度の場合は全長が64ビットで、sは1ビットであるが、eは11ビット、fは52ビットに拡張されている。
どれだけの範囲の数を表せるかを10進数で表現すると、単精度は-3.4e38~+3.4e38、倍精度は-1.79e308~+1.79e308となる。倍精度はfの有効ビット数が長いだけでなく、表現できる数の範囲も大きく広がっている。
4÷3のような計算の真の答えは1.3333333…と無限に続いてしまう。このため、小数点以下2桁に丸めると1.33となり0.003333…の誤差が生じる。これと同様に、IEEE 754の計算も有効数字fでぴったりと表せない数となった場合は、図2に示すように、有効数字で表せる数に丸める必要がでる。
10進の場合の有理数は有限の桁数で正確に表現できるが、無理数や循環小数は有限の桁数で打ち切って表現すると誤差がでてしまう。これと同様に、2進数でも有限のビット数で、無理数や循環小数を表現すると誤差がでてしまう。
0.001は1/1000で、10進では有理数であるが、2進16ビットでは655/216、あるいは656/216が近い数であるが、きっかり0.001を表現することはできない。余談であるが、0.1%の利率の場合の利息を2進の浮動小数点演算で計算すると、このような誤差で利息の額が違ってしまうことがあるので、銀行などでのお金の計算には2進の浮動小数点計算は使われない。
なお、図2の丸めはいちばん近い表現が可能な数に丸めるRN(Round to Nearest)であるが、絶対値で切り捨てを行うRZ(Round to Zero)、切り上げ(Round Up)、切り下げ(Round Down)などもIEEE 754 規格には規定されている。
当初のIEEE 754規格では加減乗除の4つの演算しか規定されていなかったが、2008年の改定で、a×b+cを計算する積和演算(Floating Multiply Add:FMA)が追加された。乗算と加算を順に行えばよいように思うかもしれないが、計算精度の点で重要な違いがある。
図3のようにa×bを計算すると積のビット数は2倍の長さになる。しかし、通常の乗算では、元のfのビット数に納まるように、楕円で囲んだ部分は丸められて2段目のp=RN(a×b)のようになってしまう。ここで、a×bを丸めなしで計算し、そこからpを引いて丸めを行うと、ゼロでない値が残る。これは積を長いビット数で保持して加算を行うため、より精度の高い計算を行っているからである。
一方、a×bを丸めてからpを引くと、答えは0になってしまう。
GPUは精度の高いFMA演算器を持っているが、CPUはFMA演算器をもっていないものもあり、これはCPUの演算結果とGPUの演算結果の違いの原因の1つとなっている。
(中編に続く)