Программирование NVidia 8800: вести с веба
Опуская обычные охи "ну почему этому не учат на ВМК" - все в наших руках и я думаю, что через пару лет такие курсы будут и у нас, тема вкусная - хочу обратить внимание на слайды и транскрипты лекций, доступные вот тут. Читают приглашенные лекторы из NVidia, поэтому основное внимание уделено, сюрприз, NVidia 8800. Курс включает в себя лабы, которые сделаны очень интересно: есть готовая рыба, делающая подготовительную работу (I/O, печать результаты) и студент должен только написать несколько десятков-сотен строк изучаемой функциональности. Что, конечно, экономит кучу непроизводительного времени (смотреть тут)
Я успел посмотреть не все, из того что посмотрел - многое нравится, а многое
не нравится:
Не нравится то, что танцы идут от предыдущего поколения видеокарт. Если говорить о программировании вообще (а все примеры - про это), а не про видеоигры, то надо забывать про шейдеры и текстурные буферы. Ведь студентов учат, которые, я надеюсь, всего этого ужаса не нюхали.
Нравится, как практику, подробный разбор аппаратуры. Вылезает множество подробностей, которые нигде не опубликованы.
Вот о подробностях - ниже.
Самые интересные презентации (из того, что прочитал): The CUDA Hardware Model и CUDA Performance. Там раскрываются многие интересные подробности:
- Впервые опубликован размер Register File у мультипроцессора: 32 килобайта. Какой от этого толк прикладному программисту поговорим ниже.
- Опубликована группировка оборудования:
- Streaming Multiprocessor (SM) содержит 8 Streaming Processors (SP), делающих простые операции (FMUL, FMAD и т.п.) и два Super Function Units (SFU), делающих сложные операции (RCP, RSQ и т.п.).
- SP выполняет инструкцию за цикл.
- SFU выполняет инструкцию за 4 цикла.
- Два SM, один текстурный юнит и кэши для текстур, инструкций и констант объединяются в один Texture Processing Cluster (TPC).
- Явно написано, что псевдоассемблер (ptx) транслируется в реальный код реального оборудования на рантайме.
- Явно написано, что компилятор делает reordering для load/store (и из текстур и из глобальной памяти) если есть достаточное количество независимых операций. В результате прячется memory latency.
- Описано, что и как делать, чтобы избегать конфликтов при параллельном доступе к памяти
Теперь давайте делать выводы.
Формальные гигафлопсы
Если считать по-честному, а не MAD-ами, то 8800GTX имеет такой теоретический предел:- 128 SP, каждый делающий операцию за такт на 1.35Ghz тактовой = 172.8 GFLOP/s
- Про SFU я понял плохо. С одной стороны, в презентации явно написано про одну операцию на 4 такта. С другой, речь может идти об операции thread warp (32 thread), тогда одну условную операцию одного thread мы делаем опять за один такт. Предполагая второй вариант, быстродействие на SFU равно 32 SFU * 1.35 = 43.2 GFLOP/s
- Итого получается 216 GFLOP/s при подсчете по честному. Для сравнения, свежеобъявленный Quad Core Intel X5355 имеет теоретическую производительность в 2.66 Ghz * 4 core * 8 инструкций на такт (SSE3, 2xFPU = 85.12 GFLOP/s. Но зато умеет 64-битные операции с теоретической производительностью в 42.6 гигафлопа
Размер файла регистров
Подсчет теоретических попугаев особого смысла не имеет, а вот опубликованный размер Register File имеет непосредственное влияние на практическую производительность. Выбор количества threads per block рекомендуют делать с учетом следующих ограничений:- кратность 32
- не более 512
- чем больше - тем лучше (эта рекомендация - неявная, явно рекомендуют 192 или 256 или больше...)
Очевидно, что наибольшая производительность будет достигнута при полной утилизации быстрой памяти: shared memory (общей на мультипроцессор) и register file (регистры - приватные для thread). При этом:
- мультипроцессор исполняет одновременно целое количество блоков
- максимальное количество threads на мультипроцессор - не более 768
Однако представим, что thread потребляет 12 регистров (по 4 байта) т.е. 48 байт регистрового файла. В этой ситуации максимальное количество тредов на SM равно 32768/48 = 682. Соответственно, запустить более чем два блока - не получится и 8 килобайт регистровой памяти будет не использовано.
Для 12 регистров на thread оптимальным размером блока будет 320, тогда недоиспользование регистрового файла будет всего 2 килобайта.
Посмотреть количество используемых регистров (а так же shared memory и локальной shared memory у thread) можно, если компилятору дать ключик -keep. В этом случае в каталоге компиляции останется файлик с расширением .cubin, где для секции описывающей kernel все будет указано:
name = Sum_h
lmem = 0
smem = 28
reg = 13
bar = 0
bincode {...
};
};