Na wstępie krótka powtórka z lekcji informatyki. Jak wiadomo, każdy procesor, również graficzny, działa na podstawie modelu binarnego. Oznacza to, że wszystkie operacje są wykonywane na danych reprezentowanych przez sekwencje zer i jedynek, pełniących funkcję stanów logicznych. Takie sekwencje są przechowywane w komórkach pamięci, zwanych rejestrami, o rozmiarze wyrażonym liczbą bitów, czyli „miejsc”, które mogą przyjąć jeden z dwóch wspomnianych stanów.
Za standard reprezentacji binarnej i działania na liczbach zmiennoprzecinkowych od lat przyjmuje się format IEEE-754. Do wyrażenia liczby zmiennoprzecinkowej pojedynczej precyzji wymagane są 32 bity. Jeden przechowuje znak, osiem – wykładnik (część całkowitą), a pozostałe 23 – mantysę (część ułamkową). To właśnie z anglojęzycznego określenia liczby zmiennoprzecinkowej, floating-point, a także liczby wykorzystywanych bitów powstał często stosowany akronim FP32, określający taką formę reprezentacji. Co jednak najistotniejsze, zapis ten wykorzystywany jest powszechnie przez jednostki wykonawcze współczesnych kart graficznych, niezależnie od tego, czy chodzi o CUDA Nvidii czy może Stream Processors AMD.
Nie było tak, wbrew pozorom, zawsze. W zamierzchłych czasach, kiedy inżynierowie dysponowali ograniczoną przestrzenią na budowę układu za sprawą ówczesnych procesów litograficznych, zdolność kart graficznych do operowania w FP32 stanowiła twardy orzech do zgryzienia. Za ciekawostkę może posłużyć to, że wadliwa implementacja techniki wielu celów renderowania (ang. Multiple Render Targets) na 32 bitach była jedną z bezpośrednich przyczyn kiepskiej wydajności GeForce'ów z serii FX w środowisku DirectX 9. Co więcej, dopiero wraz z DirectX 9.0c oraz Shader Model 3.0 wprowadzono 32-bitową zmiennoprzecinkową reprezentację oświetlenia. Wcześniej obowiązywał model 8-bitowy, całkowity, który ograniczał kontrast do 255 : 1. Mało tego, precyzje niższe od FP32 były często stosowane jeszcze w grach na PlayStation 3. Jednostki cieniowania układu graficznego RSX, a więc również pecetowego G70, nie mogą bowiem pracować na liczbach zmiennoprzecinkowych o rozmiarze większym niż 16 bitów – choć kolor w pamięci kodowano na 32 bitach. Jaki z tego wniosek, zapytacie? Zmniejszenie precyzji zapisu w grafice komputerowej nie jest niczym odkrywczym; jest raczej rozwiązaniem sprzed lat, które postęp techniczny pozwolił zastąpić czymś wyraźnie lepszym.
„Słynne” GeForce'y FX poległy właśnie na obsłudze FP32. Ich właściciele w wybranych grach, np. Half-Life 2, musieli wymuszać połowiczną precyzję
Jakiś czas temu temat niższych precyzji powrócił. Inżynierowie wydedukowali, że zastosowanie 32-bitowej jednostki obliczeniowej pozwala wykonać symultanicznie dwie operacje 16-bitowe (FP16). Technicznie rzecz biorąc, względem FP32 oznacza to ograniczenie reprezentacji binarnej do znaku, 5-bitowego wykładnika oraz 10-bitowej mantysy, przez co topnieje porcja przetwarzanych danych, ale też dokładność obliczeń. Tym sposobem działy marketingu mogły przemnożyć przez dwa teoretyczną liczbę operacji zmiennoprzecinkowych wykonywanych w ciągu sekundy, wyrażaną we flopach. Jako pierwszy nad FP16 rozpływał się Mark Cerny z Sony, główny architekt PlayStation 4, gdy firma wprowadzała udoskonalony model Pro. Przedstawiciel japońskiej korporacji oszacował tak moc urządzenia na 8,4 teraflopa, choć wynikało to oczywiście z działań dostawcy układu graficznego, firmy AMD, która wdrożyła przetwarzanie połowicznej precyzji do użytej przez Sony architektury Polaris. Jak wiadomo, rozwiązanie to trafiło także do najnowszej rodziny Vega i do dziś jest szumnie promowane. Tymczasem niewielu zdaje sobie sprawę, że pomysłu na powrót do FP16 wcale nie zapoczątkowało AMD, ale konkurencyjna Nvidia, w opartym na architekturze Maxwell mobilnym układzie Tegra X1.
Demo technologiczne UE4 zostało uruchomione na Tegrze X1 m.in. dzięki połowicznej precyzji, ale jego jakość nie dorastała do pięt jakości pierwotnego wydania
Pamiętacie jeszcze Elemental Demo, oparte na silniku Unreal Engine 4? Pierwotnie uruchomiono je na karcie GeForce GTX 680. Wspomniano przy tym o konieczności zapewnienia około 2,5 teraflopa mocy obliczeniowej. Później jednak pojawiła się kompilacja tej pokazówki dla Tegry X1, której moc szacowana była na mniej więcej 0,5 teraflopa. Jednym z głównych założeń portu okazało się przeniesienie kodu na FP16, co oczywiście przełożyło się na znaczącą utratę jakości obrazu, m.in. dynamicznego oświetlenia globalnego.
Porównanie 16-bitowej mapy cieni z 32-bitową odpowiedniczką nie pozostawia złudzeń
W tym miejscu podam następny, nawet doskonalszy, przykład: technikę rozbieżnego mapowania cieni (ang. Variance Shadow Mapping – VSM), którą zaczęto stosować mniej więcej w czasach pierwszych kart graficznych zgodnych z DirectX 10. W mapach cieni w przededniu wprowadzenia VSM zamiast przetwarzania 32-bitowego wykorzystywano 16-bitowe. Wystarczy rzucić okiem na porównawcze zrzuty ekranu, by zdać sobie sprawę, jak negatywnie wpływało to na jakość generowanego obrazu. Ogólnie rzecz biorąc, obniżenie precyzji pozwala na wzrost szybkości renderowania kosztem jakości grafiki. Wiadomo, że czasem może być to rozsądny kompromis, umożliwiający chociażby przeportowanie jakiejś „dużej” produkcji na mobilną konsolkę Nintendo Switch, ale rynek urządzeń stacjonarnych rządzi się innymi prawami, a tym bardziej nie należy zestawiać osiągów w FP32 z osiągami w FP16.
Na razie najbardziej znanym przykładem wykorzystania obliczeń FP16 jest nowa część serii Wolfenstein, która wykorzystuje połowiczną precyzję do realizacji efektów postprodukcyjnych
No dobrze, ale dlaczego tak się w ogóle dzieje? Jak już zdążyłem wspomnieć, większa liczba bitów pozwala zapisywać większe wartości liczbowe, a przy tym zapewnia mniejsze różnice pomiędzy nimi w górnych partiach skali. Największym wykładnikiem w FP16 jest liczba dziesiętna 15, w FP32 zaś – 127. Jeśliby w obydwu systemach miał być realizowany gradient, zredukowana precyzja rodziłaby obawy związane ze zjawiskiem schodkowania, ze względu na zbyt duże różnice w natężeniu światła pomiędzy sąsiadującymi odcieniami. To przypadek dość skrajny, ale zarazem bardzo wymowny. Bezinwazyjnie precyzję ciąć można w tych zastosowaniach, które faktycznie zadowalają się 16-bitowym zapisem. I tylko wtedy. Są to przeważnie proste efekty postprodukcji, chociażby bloom.
Zobacz ranking najpopularniejszych kart graficznych w grudniu i styczniu
Cóż, teraz przynajmniej nie powinno być już wątpliwości, że nie istnieje coś takiego jak bezstratne przeniesienie kodu z FP32 na FP16. Entuzjastyczne teorie o imponującym przyroście wydajności wydają się zatem formułowane nieco na wyrost. Oczywiście, twórcy gier mogą podejść do optymalizacji bardziej kreatywnie, mianowicie skorzystać z precyzji połowicznej tylko tam, gdzie nie wiąże się to z nazbyt widocznymi cięciami (patrz przykład powyżej). Tutaj jednak upada mit o słupkach rosnących o dwie długości. Można natomiast odciążyć podsystem pamięci i jednostki renderujące, by osiągnąć wyższą rozdzielczość obrazu, ale powtórzę raz jeszcze: kosztem szczegółowości.
Ujmując to wszystko nieco kolokwialnie, można powiedzieć, że w programowaniu nie ma żadnych magicznych sztuczek, a mniejsza porcja danych siłą rzeczy przenosi mniej informacji.
Tak czy inaczej, to czas zweryfikuje, co precyzja połowiczna w istocie oznacza dla graczy. Ja mimo wszystko radzę zachować powściągliwość, do czego, mam nadzieję, udało mi się skłonić niektórych nazbyt optymistycznych Czytelników.