В архиве из утечки Nvidia есть файлы типа nvasm.exe, который делает cubin файлы из так сказать нативного ассемблера и даже файлы такого вида как nvasm_internal.exe, то есть для внутреннего использования, но nvasm, а тем более nvasm_internal нет в официальном ToolKit от Nvidia. Так что эта вещь получается очень интересная чтобы ее использовать. Даже если ничего не знать про синтаксис, то можно готовый файл с разрешением '.cubin' сначала дизассемблировать с помощью nvdisasm.exe, а потом уже заново скомпилировать nvasm'ом в сравнении с ptx псевдоассемблером, который на порядок выше по стеку и дает намного меньше возможностей в плане контроля используемой постоянной памяти, регистров и так далее, а также использовать команды GPU, не доступные при написании кода ядра на ptx.
Cubin файл представляет из себя elf формат исполняемых файлов под linux, который немного изменен, а точнее расширен. Внутри находятся главный заголовок, подзаголовки, секции кода для GPU, инициализированных и не инициализированных данных, а также специальная информационная секция и даже секции отладки и перемещений именно для GPU кода. Естественно это код для GPU, а не CPU, причем для каждого ядра (функции) создаются свои отдельные секции инициализированных данных, кода и информации о конкретном ядре. В главном заголовке можно найти версию Toolkit'a, который использовался для компиляции кода ядер GPU и номер архитектуры видеокарты для которой собран этот cubin файл, а также режим адресации памяти (32 или 64 бита). Исходник на нативном ассемблере имеет расширение '.S', но конечно если хочется, то можно использовать любое другое, например - '.asm' Первые две строчки каждого исходника выглядят примерно так : .headerflags @"EF_CUDA_TEXMODE_UNIFIED EF_CUDA_SM90 EF_CUDA_VIRTUAL_SM(EF_CUDA_SM90) EF_CUDA_64BIT_ADDRESS" - здесь указываются флаги для компиляции, означающие номер архитектуры и режим адресации памяти 32- или 64- бита. В настоящий момент самые новые карточки (sm_90 и соответственно с большим номером архитектуры) поддерживают только 64-битный режим, потому если программа для CPU 32-битная, то она просто не будет работать с этими видеокартами. Кстати текущая версия Toolkit, тоже поддерживает только режим 64 бита, то есть Nvidia вообще отказалась от 32 бит, не удивительно ведь начиная с серии GT 16xx, все карточки имеют объем оперативной памяти 4 Гигабайт или более. Таким образом флаг EF_CUDA_64BIT_ADDRESS всегда должен присутствовать. .elftype @"ET_EXEC" - это тип файла. Помимо этого есть еще 2 типа, но нужно использовать именно такой.
А теперь немного о памяти. В отличие от обычных CPU, где есть только 3 вида памяти, не считая регистров - это глобальная, локальная (которая берется из стека) и кэш память нескольких уровней (она есть, но недоступна при написании программ на обычном ассемблере) в GPU помимо "глобальной" (global) и "локальной" (local), еще имеется "постоянная" (constant), "разделяемая" (shared) и "текстурная" (texture) памяти. С "глобальной" и "локальной" GPU памятью все более менее ясно, потому что это тоже самое что и в CPU, а вот "разделяемая" память это очень интересная вещь. Фактически она представляет из себя управляемый кэш первого уровня (аналогична уровню L1, если сравнивать с уровнями кэшей CPU)!!! То есть можно прямо в коде ядра указывать какие байты из регистров процессора и по какому смещению писать в кэш и читать из него. Еще один тип памяти - "постоянная". Из нее можно только читать. Здесь имеются в виду права доступа из функции ядра, так как тут описывается только ассемблер. Справедливости ради надо сказать, что в нее конечно можно и записывать со стороны хоста (host), то есть кода основной программы, который выполняется на CPU. Но это как говорится уже совсем другая история . В "текстурную" память так же как и в "постоянную" данные можно только записывать, но естественно она отличается от "постоянной", поэтому и имеет другое название. По сути эти 2 типа памяти совпадают правами доступа, а все остальное у них различное (назначение, управление ее при программировании и так далее). Пару слов о дальнейших обозначениях. В тексте ассемблерного кода можно использовать комментарии которые такие же как например и в языке C: // строка с комментарием /* многострочный комментарий */ 1 - число в десятичной системе 0x12 - число в шестнадцатеричной системе Имена переменных могут содержать заглавные (A - Z) и строчные (a - z) латинские буквы, числа (0 - 9) и знак подчеркивания (_). выравнивания (align) могут иметь значения 1, 2, 4, 8 или 16. Пример для не инициализированной глобальной памяти (общая для всего cubin файла): .section .nv.global,"aw",@nobits /* заголовок секции глобальных данных .nv.global - тип секции aw - флаги доступа, означающие соответственно чтение ( a ) и запись ( w ) */ .align 16 // выравнивание в памяти в байтах .type first_variable_name,@object // first_variable_name - имя переменной задаваемая разработчиком .size first_variable_name,(Label2-Label1) /* размер это переменной в байтах, тут интересно, что разница в скобках обозначает размер в байтах между двумя метками Label1 и Label2 (см. чуть ниже), равное 16 байт с учетом указанного выравнивания */ Label1: .zero 16 /* указывается, что при запуске всей программы на CPU, то есть только при первом запуске ядра, но не при каждом вызове функции ядра (например если ядро запускается в цикле или более одного раза) эта переменная заполняется нулями, но конечно эту строчку можно и не использовать */ Label2: .align 8 // выравнивание в памяти в байтах .type second_variable_name,@object // имя переменной задаваемая разработчиком .size variable_name,8 // размер это переменной в байтах И так далее... Пример для инициализированной глобальной памяти (общая для всего cubin файла): .section .nv.global.init,"aw",@progbits /* заголовок секции глобальных данных .nv.global.init - тип секции aw - флаги доступа, означающие соответственно чтение (a) и запись (w) */ .align 4 // выравнивание в памяти в байтах .type table_name,@object // table_name - имя переменной задаваемая разработчиком .size table_name,4 // размер это переменной в байтах .byte 0,0x1,2,3 // значения 4 байтов переменной table_name - 1, 2, 3 и 4 соответственно Как и в примере выше можно использовать большее одной переменной. Пример для разделяемой памяти (своя отдельная для каждого ядра): .section .nv.shared.Kernel_Name,"aw",@nobits /* заголовок секции глобальных данных .nv.shared. - тип секции Kernel_Name - имя функции ядра в которой будет использоваться разделяемая память. Это делается потому, что в cubin файле может быть больше одной функции ядра и разные ядра могут различные размера кэша или же конкретное ядро вообще может не использовать разделяемую память aw - флаги доступа, означающие соответственно чтение (a) и запись (w)*/ .align 1 // выравнивание в памяти в байтах .zero 512 /* указываем сколько байт кэша мы будем использовать, а не то, что будет заполнение нулями !!! Так как данные кэша определены только на время запуска блока потоков, то инициализировать байты кэша не имеет смысла */ Для разделяемой памяти можно использовать только одну секцию на ядро !!!
Пару слов о существующих типах переменных : .byte - размер 1 байт .short - размер 2 байта .word - размер 4 байта .dword - размер 8 байт При этом данные могут быть и не одиночными переменными, а массивами. Например .byte 1,2,3,4 - это массив из 4 элементов каждый размером 1 байт, элементы массива разделяются запятой Массив даже может состоять из элементов различного размера как в следующем примере: .byte 1,2,3,4 .word 5,6,7,8 Каждая строка начитается с типа элементов этой строки. В конце запятая не ставится. Но при этом не надо забывать про выравнивание, где в данном случае оно 4, а не 1 (.align 4). Определяется по большему размеру элемента. Чтение или запись данных в любой тип памяти по не выровненному адресу всегда приводит к аварийном завершению (исключению) при выполнении кода функции ядра поэтому никогда не стоит забывать про этот факт (ОЧЕНЬ СЕРЬЕЗНАЯ ШТУКА)!!!!! Когда пишется код на ассемблере для обычного CPU с использованием регистров общего назначения, то об этом не задумываешься. Кстати, для векторных регистров CPU (SSE, SSE2) есть нечто подобное, но там уже самому можно выбивать между MOVDQA (перемещение выровненных данных) и MOVDQU (перемещение не выровненных данных), а для GPU такого нет. Пример для "постоянной" памяти (своя для каждого ядра) : .section .nv.constant0.Kernel_Name,"a",@progbits /* заголовок секции постоянных данных .nv.constant0. - тип секции. Нулевой банк постоянной памяти. Используется для передачи параметров в конкретную функцию ядра и дополнительно служебной информации. Всегда присутствует в тексте исходника, без этого никак. Kernel_Name - имя функции ядра. a - флаг доступа, означающий чтение */ .align 8 // выравнивание в памяти в байтах. Если в функцию ядра будут передаваться только 4 - байтные параметры, то тогда здесь достаточно указать всего лишь значение 4. Если размер хотя бы одного из параметров 8 байт , то обязательно нужно установить значение 8. .zero 328 /* размер памяти в байтах , ее размер вычисляется так (приведу как пример только для 32-битной модели памяти, для 64-битной будет по аналогии) - всегда = 320 байта + еще общий размер всех параметров в байтах, передаваемых в функцию ядра. К примеру если передается только один параметр размером 4 байта, то получится 324 (320 + 4). Если 2 параметра и каждый по четыре байта (320 + 4 + 4) или один параметр длиной 8 байт (320 + 8), то уже 328. Начальные 320 байта содержат служебную информацию для ядра и ее можно прочитать, но только с этого уровня нативного ассемблера. Ни в псевдоассемблере PTX , ни тем более в языке высокого уровня С этого сделать нельзя!!! При 64-битной модели памяти почти все тоже самое за исключением того, что некоторые данные служебной информации становятся уже 64-битные, поэтому так сказать базовое число будет уже не 320, а больше */ Первый банк постоянной памяти (.nv.constant1) не используется и зарезервирован. Данная секция всегда отсутствует. .section .nv.constant2.Kernel_Name,"a",@progbits /* заголовок секции постоянных данных .nv.constant2. - тип секции. Второй банк постоянной памяти. Используется для числовых констант, используемых в функции ядра, поэтому у каждого ядра эта секция также будет своей. Kernel_Name - имя функции ядра. a - флаг доступа, означающий чтение */ .align 4 // выравнивание в памяти в байтах. .word 0x12345678 , 0x12345678 // непосредственно данные банка памяти Здесь указывать размер секции не требуется, он автоматически определяется по размеру всех данных с учетом выравнивая, что очень удобно. .section .nv.constant3,"a",@progbits /* заголовок секции постоянных данных .nv.constant3. - тип секции. Третий банк постоянной памяти. Общий для всех ядер в cubin файле. Как раз в этот банк памяти и копируются данные из программы выполняемой на CPU. a - флаг доступа, означающий чтение */ .align 4 // выравнивание в памяти в байтах .type Var_Name,@object // Var_Name - имя переменной .size Var_Name,4 // размер переменной Var_Name .short 1,2 /* Значение переменной. Также вместо это строчки может быть к примеру ".zero 4", которая означает, что переменная не инициализирована, а точнее неявно инициализирована нулями , но конечно сразу одновременно двух строк быть не должно либо ".zero 4", либо ''.short 1,2 " */
Пример "информационной" секции ( одна общая для всего cubin файла ) : .section .nv.info,"",@"SHT_CUDA_INFO" // права доступа здесь не указываются SHT_CUDA_INFO // флаги для секции .nv.info // тип секции Эта секция имеет несколько записей, каждая из которых начинается со следующей структуры : .align 4 - выравнивание на 4 байта 2 байта - тип записи 2 байта - размер записи Далее идут данные текущей записи размером "размер записи" байт. Полный пример одной из записей : // Часть одинаковая для всех записей .align 4 // выравнивание на 4 байта .short 0x1204 /* тип записи. В данном случае это минимальный размер стека, то есть размер локальной памяти выделяемой в стеке на каждый поток */ .short 8 // размер записи // Часть одинаковая для всех записей .align 4 // выравнивание на 4 байта .word index@(kernel_name) // kernel_name - имя функции ядра .word 0x00000010 // размер стека в байтах на каждый поток .align 4 // выравнивание на 4 байта .short 0x1104 // тип записи. В данном случае это размер кадра .short 8 // размер записи .align 4 // выравнивание на 4 байта .word index@(kernel_name) // kernel_name - имя функции ядра .word 0x00000010 // размер кадра в байтах на каждый поток .align 4 // выравнивание на 4 байта .short 0x2304 // тип записи. В данном случае это максимальный размер стека .short 8 // размер записи .align 4 // выравнивание на 4 байта .word index@(kernel_name) // kernel_name - имя функции ядра .word 0x00000000 // максимальный размер стека в байтах на каждый поток
Также в секции .nv.info бывает еще такая запись : .align 4 // выравнивание на 4 байта .short 0x2f04 // тип записи. В данном случае это число регистров в функции ядра .short 8 // размер записи .align 4 // выравнивание на 4 байта .word index@(kernel_name) // kernel_name - имя функции ядра .word 64 // число регистров в функции ядра на каждый поток Вообще в секции "инфо" могут быть разные типы записей и их разное количество в зависимости от того какие команды GPU используются в функции ядра. А даже если содержимое функции совпадает, то число и тип записей может отличаться при разных версиях ToolKit'а. В коде ядра размер локальной памяти в стеке берется из секции .nv.info, а смещение на начало стека выполняется командой MOV. Как пример полная команда выглядит следующим образом: MOV R1, c[0x0][0x20]; Здесь происходит загрузка значения по смещения 32 из банка 0 (ноль) постоянной памяти в обычный регистр общего назначения. В конце каждой команды ставится точка с запятой. Регистры нумеруются с нуля (R0, R1 и так далее... ). Если например в секции "инфо" указано, что используется 32 регистра, то это означает задействование в функции ядра регистров с номерами от R0 до R31 включительно. Пример именованной секции "инфо" (для каждого ядра отдельная) : .section .nv.info.kernel_name,"",@"SHT_CUDA_INFO" /* права доступа здесь не указываются .nv.info - тип секции kernel_name - имя функции ядра SHT_CUDA_INFO - флаги для секции */ Полный пример одной из записей: // Часть одинаковая для всех записей .align 4 // выравнивание на 4 байта .short 0x3704 // тип записи. В данном случае это версия CUDA API // Часть одинаковая для всех записей .short 4 // размер записи .word 0x00000075 /* это число деленное на 10 дает старшую версия, а остаток от деления на 10 младшую версия CUDA API. В данном примере получается, что версия равна "11.7" */ .align 4 // выравнивание на 4 байта .short 0x0a04 /* тип записи. В данном случае это размер служебной информации в байтах и суммарный размер всех параметров ядра в байтах */ .short 8 // размер записи .align 4 // выравнивание на 4 байта .word index@(kernel_name) // kernel_name - имя функции ядра .short 0x0140 // размер служебной информации в байтах .short 12 /* суммарный размер всех параметров ядра в байтах. Например если всего 3 параметра и каждый размером 4 байта */ .align 4 // выравнивание на 4 байта .short 0x1903 // тип записи. В данном случае это суммарный размер всех параметров ядра в байтах .short 16 // Например если всего 3 параметра и каждый размером 8 байт .align 4 // выравнивание на 4 байта .short 0x1404 // тип записи. В данном случае это информация о параметре ядра .short 12 // размер записи .word 0x00000000 // Индекс символьной таблицы .short 0x0000 // номер параметра ( первый параметр имеет номер ноль .short 0x0000 // смещение начала параметра в байтах, считая от нулевого смещения .byte 0x00, 0xf0, 0x11, 0x00 /* Последние 14 бит указывают на размер в байтах данного параметра. В данном примере это равно 4 байта */ .align 4 // выравнивание на 4 байта .short 0x1b03 /* тип записи. В данном случае это максимальное число регистров для данной архитектуры видеокарты */ .short 0x00ff /* в данном примере равно 255. Такое значение присутствует с архитектуры sm32 до самой новой sm90 */ .align 4 // выравнивание на 4 байта .short 0x1c04 /* тип записи. В данном случае это список смещений на все инструкции выхода из функции ядра (EXIT) в коде, отсчитывается от первой команды, которая имеет смещение ноль */ .short 8 // Общий размер таблицы смещений, размер каждого смещения 4 байта .word 0x000060c0 // Первое смещение .word 0x000060d0 // Второе смещение // И так далее... .align 4 // выравнивание на 4 байта .short 0x1d04 /* тип записи. В данном случае это список смещений на все инструкции загрузки номера текущего блока из специального регистра в обычный (S2R) в коде, отсчитывается от первой команды, которая имеет смещение ноль. Например полностью команда может выглядеть как S2R R1, SR_CTAID.X ;, где R1 - обычный регистр с номером один */ .short 8 // Общий размер таблицы смещений, размер каждого смещения 4 байта .word 0x000060c0 // Первое смещение .word 0x000060d0 // Второе смещение // И так далее...
Пример заголовка секции "кода" (для каждого ядра естественно создается отдельная секция) : .section .text.kernel_name,"ax",@progbits /* .text. - тип секции кода для исполнения на GPU. kernel_name - имя функции ядра. ax - флаги доступа, означающий соответственно чтение (a) и исполнение (x) */ .sectioninfo @"SHI_REGISTERS=64" /* информация о секции SHI_REGISTERS - число регистров используемых данной функцией ядра. В данном примере равно 64, то есть как я уже объяснял ранее в предыдущем сообщении от R0 до R63 включительно */ .align 32 /* выравнивание секции в памяти в байтах. В данном случае равно 32 (для архитектур sm50 - sm62), но может быть и другое значение. К примеру 64 (для архитектур sm30 - sm37) или 128 (для архитектур sm70 - sm90), это зависит от архитектуры видеокарты. Кстати, смещение в нулевом банке постоянной памяти на начало стека для локальной памяти тоже зависит от архитектуры и может иметь значение 32 (для архитектур sm50 - sm62), 40 (для архитектур sm70 - sm90) или 68 (для архитектур sm30 - sm37). */ .global kernel_name // kernel_name - имя функции ядра .type kernel_name,@function // тип объекта - функция .size kernel_name,(begin_ - end_) // размер секции кода в байтах .other kernel_name,@"STO_CUDA_ENTRY STV_DEFAULT" // флаги для кода ядра begin_: ............... //код ядра между 2 метками начала и конца команд end_: Интересный факт, что GPU от NVidia это 32-разрядный процессор, хотя адресация памяти может быть как 32-битной так и 64-битной. Поэтому если бы они сделали процессор 64-разрядным, то скорость некоторых алгоритмов на GPU увеличилась бы сразу в 2 раза без всяких дополнительных ухищрений. Каждая команда GPU в отличие от CPU имеет размер 8 байт (для архитектур sm30 - sm62) и 16 байт (для архитектур sm70 - sm90). Это очень удобно. Даже если не известна кодировка какой-то конкретной команды, то всегда известны смещения по которым начинаются предыдущая и следующая команды процессора. Есть еще один интересный факт. Для архитектур sm30 - sm37 на каждые 7 команд добавляется еще одна инструкция для "логического контроля", которая не выполняет вычислений из кода функции ядра, но дополнительно занимает 8 байт. Сначала идет эта инструкция, а потом сразу за ней подряд 7 обычных команд и это циклически повторяется на протяжении всей секции кода. Таким образом общий размер всего кода увеличивается на 14 процентов. Для архитектур sm50 - sm62 на каждые 3 команды добавляется еще одна инструкция для "логического контроля". Сначала идет эта инструкция, а потом сразу за ней подряд 3 обычных команды и это циклически повторяется на протяжении всей секции кода. Таким образом общий размер всего кода увеличивается на 33 процента.
Leopard, вы в курсе, что в редакторе сообщений есть теги [соde=язык] и [/соdе]? А то в ваших постах не разберёшь, где текст, а где исходник. Ведь в отформатированном виде намного лучше инфа воспринимается: Код (C++): .global kernel_name // kernel_name - имя функции ядра .type kernel_name,@function // тип объекта - функция .size kernel_name,(begin_ - end_) // размер секции кода в байтах .other kernel_name,@"STO_CUDA_ENTRY STV_DEFAULT" // флаги для кода ядра begin_ : ............... //код ядра между 2 метками начала и конца команд end_ :
Знаю , но это же не исходный код на С и даже не обычный ассемблер , тут ведь совсем другой синтаксис .
Их графические карты постоянно перегреваются и тормозят. Они даже не тестировают свои продукты перед выпуском. Все, что они делают, это копируют идеи других компаний и продают их за непомерные деньги. Серьезно, зачем нужны эти супер-дорогие RTX-карты с рейтрейсингом, если игрокам на самом деле необходима нормальная оптимизация и стабильная производительность? Nvidia просто выжимает из нас деньги, не предоставляя достойного качества. Мне просто омерзительно, как они относятся к своим клиентам. Я предпочитаю использовать продукты AMD, где как минимум есть хоть какая-то забота о потребителях.