Программирование NVidia 8800: вести с веба

В University of Illinois at Urbana, куда я чуть было не уехал заниматься геологией 15 лет назад, в настоящее время читается курс ECE 498 AL : Programming Massively Parallel Microprocessors.

Опуская обычные охи "ну почему этому не учат на ВМК" - все в наших руках и я думаю, что через пару лет такие курсы будут и у нас, тема вкусная - хочу обратить внимание на слайды и транскрипты лекций, доступные вот тут. Читают приглашенные лекторы из NVidia, поэтому основное внимание уделено, сюрприз, NVidia 8800. Курс включает в себя лабы, которые сделаны очень интересно: есть готовая рыба, делающая подготовительную работу (I/O, печать результаты) и студент должен только написать несколько десятков-сотен строк изучаемой функциональности. Что, конечно, экономит кучу непроизводительного времени (смотреть тут)

Я успел посмотреть не все, из того что посмотрел - многое нравится, а многое не нравится:
Не нравится то, что танцы идут от предыдущего поколения видеокарт. Если говорить о программировании вообще (а все примеры - про это), а не про видеоигры, то надо забывать про шейдеры и текстурные буферы. Ведь студентов учат, которые, я надеюсь, всего этого ужаса не нюхали.

Нравится, как практику, подробный разбор аппаратуры. Вылезает множество подробностей, которые нигде не опубликованы.

Вот о подробностях - ниже.

Самые интересные презентации (из того, что прочитал): The CUDA Hardware Model и CUDA Performance. Там раскрываются многие интересные подробности:

  1. Впервые опубликован размер Register File у мультипроцессора: 32 килобайта. Какой от этого толк прикладному программисту поговорим ниже.
  2. Опубликована группировка оборудования:
    1. Streaming Multiprocessor (SM) содержит 8 Streaming Processors (SP), делающих простые операции (FMUL, FMAD и т.п.) и два Super Function Units (SFU), делающих сложные операции (RCP, RSQ и т.п.).
    2. SP выполняет инструкцию за цикл.
    3. SFU выполняет инструкцию за 4 цикла.
    4. Два SM, один текстурный юнит и кэши для текстур, инструкций и констант объединяются в один Texture Processing Cluster (TPC).
  3. Явно написано, что псевдоассемблер (ptx) транслируется в реальный код реального оборудования на рантайме.
  4. Явно написано, что компилятор делает reordering для load/store (и из текстур и из глобальной памяти) если есть достаточное количество независимых операций. В результате прячется memory latency.
  5. Описано, что и как делать, чтобы избегать конфликтов при параллельном доступе к памяти

Теперь давайте делать выводы.

Формальные гигафлопсы

Если считать по-честному, а не 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
Казалось бы, делай 256 тредов на блок, будет исполняться три блока на процессор и прекрасно.

Однако представим, что thread потребляет 12 регистров (по 4 байта) т.е. 48 байт регистрового файла. В этой ситуации максимальное количество тредов на SM равно 32768/48 = 682. Соответственно, запустить более чем два блока - не получится и 8 килобайт регистровой памяти будет не использовано.

Для 12 регистров на thread оптимальным размером блока будет 320, тогда недоиспользование регистрового файла будет всего 2 килобайта.

Посмотреть количество используемых регистров (а так же shared memory и локальной shared memory у thread) можно, если компилятору дать ключик -keep. В этом случае в каталоге компиляции останется файлик с расширением .cubin, где для секции описывающей kernel все будет указано:

code  {
        name = Sum_h
        lmem = 0
        smem = 28
        reg = 13
        bar = 0
        bincode  {...
      };
};
В вышеприведенном примере используется 13 регистров, т.е. максимальное количество одновременных threads будет 630. Рекомендуемое количество threads в блоке - 288.