AMD APP SDK: Compute Abstraction Layer (CAL)

    AMD APP В первой части я рассказал о технологии AMD Intermediate Language (IL). В этой статье, как можно догадаться из заголовка, речь пойдет о второй составляющей: AMD Compute Abstraction Layer (CAL). Эти две технологии являются нераздельными друг от друга: невозможно использовать одну, не используя другую. Поэтому для понимания дальнейшего рекомендую ознакомиться с первой частью.

    Я постараюсь осветить основные аспекты работы на верхнем уровне с AMD GPU, опишу ограничения данной технологии и возможные проблемы при работе с ней. Кому интересно, прошу под кат.

    Вместо введения


    Когда я только начинал разбираться с программированием под AMD GPU, меня спросили, что я для этого использую. «ATI CAL» — ответил я. «Да, ATI действительно CAL» — было мне в ответ.
    В общем, как произносится аббревиатура CAL, я не знаю, но произношу через «О», чтоб не смущать людей.

    Для краткости обозначим программу, описанную в первой части ядром (kernel). Под ядром я буду подразумевать как исходный текст программы, так и скомпилированный, бинарный код, загружаемый на GPU. Я не буду приводить полный текст какой-либо программы, работающей с GPU через AMD CAL, а просто расскажу про основные моменты работы:
    • инициализация драйвера
    • получение информации о всех поддерживаемых GPU
    • выделение и копирование памяти
    • компиляция и загрузка ядра на GPU
    • запуск ядра на исполнение
    • синхронизация работы с CPU

    Для начала работы нам понадобятся два заголовочных файла из AMD APP SDK:
    • cal.h — описывает основные функции драйвера, функции имеют префикс «cal» (библиотека aticalrt.dll)
    • calcl.h — описывает основные функции компилятора текстового ядра в бинарный код, функции имеют префикс «calcl» (библиотека aticalcl.dll)

    Как вы видите, в отличие от Nvidia CUDA, у которой есть Run-time API и Driver API, для AMD доступно только Driver API. Поэтому для работы вашего приложения не забудьте слинковаться с соответствующими библиотеками.

    Большинство вызываемых функций возвращает значение типа CALresult. Всего доступно 11 кодов возврата. Самый важный для нас — код CAL_RESULT_OK, равный 0 (свидетельствует об успешном завершении вызова).

    Итак, поехали.

    Инициализация драйвера


    Правило №1: перед началом работы с GPU вымыйте руки инициализируйте работу драйвера следующим вызовом:
    CALresult result = calInit();
    

    Правило №2: после работы с GPU не забываем смыть за собой завершить работу корректно. Это делается следующим вызовом:
    CALresult result = calShutdown();
    

    Эти два вызова всегда должны идти в паре. Их (таких пар вызовов) может быть несколько в программе, но никогда не работайте с GPU вне этих вызовов: такое поведение может повлечь за собой hardware exception.

    Получение информации о GPU


    Узнаем количество поддерживаемых GPU (их может быть меньше, чем общее число AMD GPU в системе):
    unsigned int deviceCount = 0;
    CALresult result = calDeviceGetCount( &deviceCount );
    

    В данной статье я буду указывать, где используется идентификатор GPU, но «работать» я буду с GPU под идентификатором 0. В общем случае этот идентификатор принимает значения от 0 до (deviceCount-1).

    Узнаем информацию о GPU:
    unsigned int deviceId = 0;  // идентификатор GPU
    
    CALdeviceinfo deviceInfo;
    CALresult result = calDeviceGetInfo( &deviceInfo, deviceId );
    
    CALdeviceattribs deviceAttribs;
    deviceAttribs.struct_size = sizeof( deviceAttribs );
    CALresult result = calDeviceGetAttribs( &deviceAttribs, deviceId );
    

    Самое важное в структуре CALdeviceinfo — идентификатор чипа GPU. Он именуется здесь Device Kernel ISA:
    typedef struct CALdeviceinfoRec {
        CALtarget  target;              /**< Device Kernel ISA  */
        CALuint    maxResource1DWidth;  /**< Maximum resource 1D width */
        CALuint    maxResource2DWidth;  /**< Maximum resource 2D width */
        CALuint    maxResource2DHeight; /**< Maximum resource 2D height */
    } CALdeviceinfo;
    

    Остальные поля структуры определяют максимальный размер текстурной памяти по двум координатам, которую можно выделить на данном GPU.

    Намного более интересна структура CALdeviceattribs, отвечающая за атрибуты GPU (я приведу только несколько полей структуры):
    typedef struct CALdeviceattribsRec {
        CALtarget  target;              /**< Asic identifier (тот же Device Kernel ISA) */
        CALuint    localRAM;            /**< Размер локальной GPU RAM в мегабайтах */
        CALuint    wavefrontSize;       /**< Размер warp'a (количество одновременно запускаемых потоков одним потоковым процессором) */
        CALuint    numberOfSIMD;        /**< Количество мультипроцессоров */
        CALboolean computeShader;       /**< Поддерживается ли Compute Shader */
        CALuint    pitch_alignment;     /**< Требуемая величина выравнивания размера памяти при вызове calCreateRes */
        /* Другие поля */
    } CALdeviceattribs;
    

    Правило №3: поле CALdeviceattribs.pitch_alignment измеряется в элементах памяти, а не в байтах. Элементом памяти является 1-, 2- или 4-х компонентный вектор 8-, 16- или 32-битных регистров.

    А теперь давайте внимательно посмотрим, какие же значания может принимать поле CALdeviceinfo.target (оно же CALdeviceattribs.target):
    /** Device Kernel ISA */
    typedef enum CALtargetEnum {
        CAL_TARGET_600,                /**< R600 GPU ISA */     
        CAL_TARGET_610,                /**< RV610 GPU ISA */    
        CAL_TARGET_630,                /**< RV630 GPU ISA */
        CAL_TARGET_670,                /**< RV670 GPU ISA */
        CAL_TARGET_7XX,                /**< R700 class GPU ISA */
        CAL_TARGET_770,                /**< RV770 GPU ISA */
        CAL_TARGET_710,                /**< RV710 GPU ISA */
        CAL_TARGET_730,                /**< RV730 GPU ISA */
        CAL_TARGET_CYPRESS,            /**< CYPRESS GPU ISA */
        CAL_TARGET_JUNIPER,            /**< JUNIPER GPU ISA */
        CAL_TARGET_REDWOOD,            /**< REDWOOD GPU ISA */
        CAL_TARGET_CEDAR,               /**< CEDAR GPU ISA */
        CAL_TARGET_RESERVED0,
        CAL_TARGET_RESERVED1,           
        CAL_TARGET_WRESTLER,            /**< WRESTLER GPU ISA */
        CAL_TARGET_CAYMAN,              /**< CAYMAN GPU ISA */
        CAL_TARGET_RESERVED2,            
        CAL_TARGET_BARTS,               /**< BARTS GPU ISA */
    } CALtarget;
    

    Оказывается, это поле обозначает чип, на котором построен GPU. Таким образом, узнать точно, как называется GPU в миру (к примеру, Radeon HD 3850) с помощью AMD CAL нельзя в принципе! Вот такая вот удобная технология… Зато интересно было наблюдать, что, к примеру, Radeon HD 5750 и Radeon HD 6750 являются на самом деле одной и той же видеокартой! Они лишь немного отличаются по частоте работы памяти (в пределах нескольких процентов).

    Еще одно замечание: в этом списке нет Evergreen GPU, о котором я упоминал в первой части. Мое предположение — GPU семейства Evergreen начинаются с чипа Cypress (CAL_TARGET_CYPRESS). Все, что до этого, — предыдущее поколение без поддержки новых плюшек (циклический сдвиг, поддержка флагов операций и 64-битные операции).

    Для дальнейшей работы нам надо создать дескриптор устройства (device), с помощью которого мы будем взаимодействовать с GPU:
    unsigned int deviceId = 0;  // идентификатор GPU
    
    CALdevice device;
    CALresult result = calDeviceOpen( &device, deviceId );
    
    CALcontext context;
    result = calCtxCreate( &context, device );
    

    Контекст необходим для работы в пределах вашего приложения с данным GPU. Вся работа с GPU производится при помощи этого контекста. Как только вы удаляете контекст, все выделенные ресурсы считаются освобожденными, а все незавершенные задачи на GPU в принудительном порядке завершаются.

    Не забываем про парные вызовы после окончания работы с устройством:
    calCtxDestroy( context );
    calDeviceClose( device );
    

    Вызовы должны идти именно в этом порядке, иначе получим hardware exception.

    Итак, мы создали устройство и контекст для него, теперь можно приступить к

    Выделение памяти


    Для работы с памятью надо выделить ресурс. Согласно документации, ресурс может располагаться в локальной памяти (local memory = stream processor memory) и удаленной памяти (remote memory = system memory). Насколько я понял, remote memory является ничем иным, как оперативной памятью, тогда как local memory — память самой GPU.

    Зачем нужна remote memory, если есть local memory? Во-первых, она нужна для разделения одной и той же памяти между несколькими GPU. То есть, remote memory можно выделить один раз и работать с ней из нескольких GPU. Во-вторых, не все GPU поддерживают прямое обращение к своей памяти (см. ниже «Получение прямого доступа к памяти»).

    CALresource     resource;
    
    unsigned int    memoryWidth;
    unsigned int    memoryHight;
    CALformat       memoryFormat;
    unsigned int    flags;
    
    // Выделение ресурса в удаленной памяти
    
    // 1D память
    CALresult result = calResAllocRemote1D( &resource, &device, 1, memoryWidth, memoryFormat, flags );
    /* Так как удаленную память можно разделять между несколькими GPU, то второй параметр - массив указателей на соответствующие дескрипторы, третий параметр - количество элементов в этом массиве (1 в нашем случае) */
    
    // 2D память
    CALresult result = calResAllocRemote2D( &resource, &device, 1, memoryWidth, memoryHeight, memoryFormat, flags );
    
    // Выделение ресурса в локальной памяти
    
    // 1D память
    CALresult result = calResAllocLocal1D( &resource, device, memoryWidth, memoryFormat, flags );
    /* Обратите внимание, что здесь уже принимается только один дескриптор, а не массив указателей на дескрипторы */
    
    // 2D память
    CALresult result = calResAllocLocal2D( &resource, device, memoryWidth, memoryHeight, memoryFormat, flags );
    

    Ширина и высота выделяемого ресурса измеряется в элементах памяти.
    Сам же элемент памяти описывается параметром memoryFormat:
    // Список длинный, я приведу только несколько записей с авторскими комментариями
    /** Data format representation */
    typedef enum CALformatEnum {
        CAL_FORMAT_UNORM_INT8_1,        /**< 1 component, normalized unsigned 8-bit integer value per component */
        CAL_FORMAT_UNORM_INT8_4,        /**< 4 component, normalized unsigned 8-bit integer value per component */
        CAL_FORMAT_UNORM_INT32_1,       /**< 1 component, normalized unsigned 32-bit integer value per component */
        CAL_FORMAT_UNORM_INT32_4,       /**< 4 component, normalized unsigned 32-bit integer value per component */
        CAL_FORMAT_SNORM_INT8_1,        /**< 1 component, normalized signed 8-bit integer value per component */
        CAL_FORMAT_SNORM_INT8_4,        /**< 4 component, normalized signed 8-bit integer value per component */
        CAL_FORMAT_SNORM_INT32_1,       /**< 1 component, normalized signed 32-bit integer value per component */
        CAL_FORMAT_SNORM_INT32_4,       /**< 4 component, normalized signed 32-bit integer value per component */
    
        CAL_FORMAT_UNSIGNED_INT8_1,     /**< 1 component, unnormalized unsigned 8-bit integer value per component */
        CAL_FORMAT_UNSIGNED_INT8_4,     /**< 4 component, unnormalized unsigned 8-bit integer value per component */
        CAL_FORMAT_SIGNED_INT8_1,       /**< 1 component, unnormalized signed 8-bit integer value per component */
        CAL_FORMAT_SIGNED_INT8_4,       /**< 4 component, unnormalized signed 8-bit integer value per component */
        CAL_FORMAT_UNSIGNED_INT32_1,    /**< 1 component, unnormalized unsigned 32-bit integer value per component */
        CAL_FORMAT_UNSIGNED_INT32_4,    /**< 4 component, unnormalized unsigned 32-bit integer value per component */
        CAL_FORMAT_SIGNED_INT32_1,      /**< 1 component, unnormalized signed 32-bit integer value per component */
        CAL_FORMAT_SIGNED_INT32_4,      /**< 4 component, unnormalized signed 32-bit integer value per component */
    
        CAL_FORMAT_UNORM_SHORT_565,     /**< 3 component, normalized 5-6-5 RGB image. */
        CAL_FORMAT_UNORM_SHORT_555,     /**< 4 component, normalized x-5-5-5 xRGB image */
        CAL_FORMAT_UNORM_INT10_3,       /**< 4 component, normalized x-10-10-10 xRGB */
        CAL_FORMAT_FLOAT32_1,           /**< A 1 component, 32-bit float value per component */
        CAL_FORMAT_FLOAT32_4,           /**< A 4 component, 32-bit float value per component */
        CAL_FORMAT_FLOAT64_1,           /**< A 1 component, 64-bit float value per component */
        CAL_FORMAT_FLOAT64_2,           /**< A 2 component, 64-bit float value per component */
    } CALformat;
    

    Жаль, что 64-битные операции на старых видеокартах (не Evergreen) можно выполнять только с данными типа float…

    Правило №4: формат элемента описывает лишь способ, которым GPU будет трактовать данные, лежащие в этом элементе. Физически элемент всегда занимает 16 байт памяти.

    Это можно понять, если вспомнить, что в первой части мы описали ресурс следующим образом:
    dcl_resource_id(0)_type(2d,unnorm)_fmtx(uint)_fmty(uint)_fmtz(uint)_fmtw(uint)
    

    И согласно спецификации языка AMD IL, значения fmtx-fmtw являются обязательными. То есть следующий код (вроде так можно было бы описать текстуру с элементами типа 1-компонентный вектор) является неверным:
    dcl_resource_id(0)_type(2d,unnorm)_fmtx(uint)
    

    Правило №5: соблюдайте типы, которые вы объявляет в ядре и при выделении ресурса. При их несовпадении вы не сможете привязать ресурс к ядру.

    Правило №6: для константной памяти тип элемента всегда должен быть типа float.

    Почему так сделано, мне не ясно, так как все равно можно загружать из константной памяти целочисленные значения (что мы и делаем в примере).

    Еще пара слов о флагах, которые необходимы при выделении памяти:
    /** CAL resource allocation flags **/
    typedef enum CALresallocflagsEnum {
        CAL_RESALLOC_GLOBAL_BUFFER  = 1, /**< used for global import/export buffer */
        CAL_RESALLOC_CACHEABLE      = 2, /**< cacheable memory? */
    } CALresallocflags;
    

    Со вторым флагом я никогда не работал, когда он дает преимущество, я не знаю. Да и судя по знаку вопроса в комментарии самих авторов, они тоже не в курсе (smile).
    А вот первый флаг нужен для выделения глобального буфера(«g[]»).

    Теперь применим теорию на практике. Помня о примере, описанном в предыдущей статье, зададимся еще и параметрами запуска ядра:
    unsigned int blocks = 4;    // запускаем 4 блока
    unsigned int threads = 64;  // по 64 потока в каждом
    
    // Выделение ресурса для cb0
    CALresource constantResource;
    CALresult result = calResAllocLocal1D( &constantResource, device, 1, CAL_FORMAT_FLOAT32_4, 0 );
    
    // Выделение ресурса для i0
    CALresource textureResource;
    result = calResAllocLocal2D( &textureResource, device, threads, blocks, CAL_FORMAT_UNSIGNED_INT32_4, 0 );
    
    // Выделение ресурса для g[]
    CALresource globalResource;
    result = calResAllocLocal1D( &globalResource, device, threads * blocks, CAL_FORMAT_UNSIGNED_INT32_4, CAL_RESALLOC_GLOBAL_BUFFER );
    

    После того, как ресурсы уже не понадобятся, их надо будет освободить:
    calResFree( constantResource );
    calResFree( textureResource );
    calResFree( globalResource );
    

    Копирование памяти


    Получение прямого доступа к памяти


    Если GPU поддерживает mapping своей памяти (отображение адресов своей памяти в адресное пространство процесса), то мы можем получить указатель на эту память, а дальше работать с ней, как и с любой другой памятью:
    unsigned int pitch;
    unsigned char* mappedPointer;
    
    CALresult result = calResMap( (CALvoid**)&mappedPointer, &pitch, resource, 0 );
    // последний параметр является флагами, но согласно документации, они не используются
    

    А после того, как мы закончим работу с памятью, необходимо освободить указатель:
    CALresult result = calResUnmap( resource );
    

    Правило №7: всегда помните, что при работе с памятью GPU нужно учитывать выравнивание. Это выравнивание характеризуется переменной pitch.

    Правило №8: pitch измеряется в элементах, а не в байтах.

    Зачем нужно знать про это выравнивание? Дело в том, что в отличие от оперативной памяти, память GPU не всегда является непрерывной областью. Особенно это проявляется при работе с текстурами. Поясню сказанное на примере: если вы хотите работать с текстурой 100х100 элементов, а функция calResMap() вернула значение pitch равное 200, то это значит, что на самом деле GPU будет работать с текстурой 200х100, просто в каждой строке текстуры будут учитываться только первые 100 элементов.

    Копирование в память GPU с учетом значения pitch можно организовать следующим образом:
    unsigned int pitch;
    unsigned char* mappedPointer;
    unsigned char* dataBuffer;
    
    CALresult result = calResMap( (CALvoid**)&mappedPointer, &pitch, resource, 0 );
    
    unsigned int width;
    unsigned int height;
    unsigned int elementSize = 16;
    
    if( pitch > width )
    {
        for( uint index = 0; index < height; ++index )
        {
            memcpy( mappedPointer  + index * pitch * elementSize,
                    dataBuffer     + index * width * elementSize,
                    width * elementSize );
        }
    }
    else
    {
        memcpy( mappedPointer, dataBuffer, width * height * elementSize );
    }
    

    Естественно, данные в dataBuffer должны быть подготовлены с учетом типа элемента. Но при этом помните, что элемент всегда имеет размер 16 байт.
    То есть для элемента формата CAL_FORMAT_UNSIGNED_INT16_2 его побайтовое представление в памяти будет следующим:
    // w - word, 16 бит
    // wi.j - i-й word, j-й байт
    // x - мусор
    
    [ w0.0 | w0.1 | x | x ][ w1.0 | w1.1 | x | x ][ x | x | x | x ][ x | x |  x | x ]
    

    Копирование данных между ресурсами


    Копирование данных производится не напрямую между ресурсами, а между их отображенными на контекст значениями. Операция копирования является асинхронной, поэтому, чтобы узнать о завершении операции копирования, используется системный объект типа CALevent:
    CALresource inputResource;
    CALresource outputResource;
    
    CALmem  inputResourceMem;
    CALmem  outputResourceMem;
    
    // Отображаем ресурсы на контекст
    CALresult result = calCtxGetMem( &inputResourceMem, context, inputResource );
    result = calCtxGetMem( &outputResourceMem, context, outputResource );
    
    // Копируем данные
    CALevent syncEvent;
    result = calMemCopy( &syncEvent, context, inputResourceMem, outputResourceMem, 0 );
    // последний параметр является флагами, но согласно документации, они не используются
    
    // Ждем завершения операции копирования
    while( calCtxIsEventDone( context, syncEvent ) == CAL_RESULT_PENDING );
    

    Компиляция и загрузка ядра на GPU


    «Смерть Кощея в игле, игла в яйце, яйцо в утке, утка в зайце, заяц в сундуке...»

    Процесс загрузки ядра на GPU можно описать следующим образом: исходник (txt) компилируется в объектник (object), затем один или несколько объектников линкуются в образ (image), который после этого загружается в модуль GPU (module), а уже из модуля можно получить указатель на точку входа ядра (по этому указателю мы сможем запустить ядро на исполнение).

    А теперь, как это реализуется:
    const char* kernel; // здесь должен лежать исходный текст ядра
    
    // Узнаем, под какую GPU компилировать
    unsigned int  deviceId = 0;  // идентификатор GPU
    CALdeviceinfo deviceInfo;
    
    CALresult result = calDeviceGetInfo( &deviceInfo, deviceId );
    
    // Компилируем исходник
    CALobject obj;
    result = calclCompile( &obj, CAL_LANGUAGE_IL, kernel, deviceInfo.target );
    
    // Линкуем объектник в образ
    CALimage image;
    result = calclLink( &image, &obj, 1 );
    // второй параметр - список объектников, третий - количество объектников
    
    // Объектник нам уже не понадобится, освобождаем его
    result = calclFreeObject( obj );
    
    // Загружаем образ в модуль
    CALmodule module;
    result = calModuleLoad( &module, context, image );
    
    // Получаем точку входа в ядро
    CALfunc function;
    result = calModuleGetEntry( &function, context, module, "main" );    
    

    Правило №9: точка входа в ядро всегда одна, так как там только одна функция после линковки — функция «main».

    То есть в отличие от Nvidia CUDA, в ядре AMD CAL может быть только одна глобальная функция «main».

    Как вы могли заметить, компилятор может обработать только исходник, написанный на языке IL.

    Загрузка образа в модуль объясняется тем, что образ нужно подгрузить в выделенный контекст GPU. Следовательно, описанный процесс компиляции нужно делать для каждой GPU (за исключением случая, когда 2 одинаковые GPU: достаточно один раз скомпилировать и слинковать, но все равно придется загрузить образ в модуль для каждой карточки).

    Хочу обратить внимание на возможность линковки нескольких объектников. Может, кому-нибудь эта возможность пригодится. На мой взгляд ее можно применить в случае различной реализации одной и той же подфункции: эти реализации можно вынести в различные объектники, так как в AMD IL нет директив препроцессора наподобие #ifdef.

    После того, как завершится выполнение ядра на GPU, надо будет освободить соответствующие ресурсы:
    CALresult result = calclFreeImage( image );
    result = calModuleUnload( context, module );
    

    Запуск ядра на исполнение


    Установка параметров запуска ядра


    Итак, у нас есть выделенные ресурсы, заполненная память и скомпилированное ядро. Остается только привязать ресурсы к нашему конкретному ядру и запустить его. Для этого нужно получить из ядра его параметры запуска, а ресурс отобразить на контекст.
    const char* memoryName; // здесь должно быть имя, для которого мы хотим получить дескриптор
    
    // Получаем параметр ядра по имени
    CALname kernelParameter;
    CALresult result = calModuleGetName( &kernelParameter, context, module, memoryName );
    
    // Отображаем ресурс на контекст
    CALmem resourceMem;
    result = calCtxGetMem( &resourceMem, context, resource );
    
    // Устанавливаем ресурс в качестве источника для параметра ядра
    result = calCtxSetMem( context, kernelParameter, resourceMem );
    

    И теперь делаем это в рамках нашего примера:
    CALname kernelParameter;
    CALmem  resourceMem;
    
    // Привязываем константную память к ядру
    CALresult result = calModuleGetName( &kernelParameter, context, module, "cb0" );
    result = calCtxGetMem( &resourceMem, context, constantResource );
    result = calCtxSetMem( context, kernelParameter, resourceMem );
    
    // Привязываем текстурную память к ядру
    result = calModuleGetName( &kernelParameter, context, module, "i0" );
    result = calCtxGetMem( &resourceMem, context, textureResource );
    result = calCtxSetMem( context, kernelParameter, resourceMem );
    
    // Привязываем глобальную память к ядру
    result = calModuleGetName( &kernelParameter, context, module, "g[]" );
    result = calCtxGetMem( &resourceMem, context, globalResource );
    result = calCtxSetMem( context, kernelParameter, resourceMem );
    

    После того, как завершится выполнение ядра на GPU, надо будет отвязать ресурсы от ядра. Это можно сделать так:
    CALname kernelParameter;
    
    // Отвязываем константную память от ядра
    CALresult result = calModuleGetName( &kernelParameter, context, module, "cb0" );
    result = calCtxSetMem( context, kernelParameter, 0 );
    
    // Отвязываем текстурную память от ядра
    result = calModuleGetName( &kernelParameter, context, module, "i0" );
    result = calCtxSetMem( context, kernelParameter, 0 );
    
    // Отвязываем глобальную память от ядра
    result = calModuleGetName( &kernelParameter, context, module, "g[]" );
    result = calCtxSetMem( context, kernelParameter, 0 );
    

    Теперь ядро знает, откуда брать данные. Остается дело за малым:

    Запуск ядра


    Как вы помните, в первой части я упоминал шейдеры PS и CS. Узнать, поддерживается ли последний, можно из атрибутов GPU (см. выше).

    Запуск PS:
    unsigned int blocks = 4;    // запускаем 4 блока
    unsigned int threads = 64;  // по 64 потока в каждом
    
    CALdomain domain;
    domain.x = 0;
    domain.y = 0;
    domain.width = threads;
    domain.height = blocks;
    
    CALevent syncEvent;
    CALresult result = calCtxRunProgram( &syncEvent, context, function, &domain );
    
    while( calCtxIsEventDone( context, syncEvent ) == CAL_RESULT_PENDING );
    

    Здесь function — та точка входа в ядро, что мы получили на этапе загрузки ядра на GPU (см. выше «Компиляция и загрузка ядра на GPU»).

    Правило №10: PS не знает внутри себя величину threads, ее нужно передавать через память (в нашем примере это сделано через константную память).

    Запуск CS:
    unsigned int blocks = 4;    // запускаем 4 блока
    unsigned int threads = 64;  // по 64 потока в каждом
    
    CALprogramGrid  programGrid;
    programGrid.func             = function;
    programGrid.flags            = 0;
    programGrid.gridBlock.width  = threads;
    programGrid.gridBlock.height = 1;
    programGrid.gridBlock.depth  = 1;
    programGrid.gridSize.width   = blocks;
    programGrid.gridSize.height  = 1;
    programGrid.gridSize.depth   = 1;
    
    CALevent syncEvent;
    CALresult result = calCtxRunProgramGrid( &syncEvent, context, &programGrid );
    
    while( calCtxIsEventDone( context, syncEvent ) == CAL_RESULT_PENDING );
    

    Правило №11: величина threads должна соответствовать величине, пробитой в исходном коде ядра. Ядро будет запущено в любом случае, однако можно либо выйти за пределы памяти (запуск меньшего количества потоков, чем было объявлено в ядре), либо не все входные данные будут обработаны (запуск большего количества потоков, чем было объявлено в ядре).

    Done! Ядро запущено, и если все прошло успешно, то в выходной памяти («g[]») лежат обработанные данные. Осталось их только скопировать наружу (см. выше раздел «Копирование памяти»).

    Полезные функции


    Осталось только упомянуть некоторые функции, которые могут пригодиться в быту.
    CALresult result;
    
    // Получение статуса отдельного устройства
    CALdevicestatus status;
    result = calDeviceGetStatus( &status, device );
    
    // Принудительное завершение всех выполняемых на GPU задач
    result = calCtxFlush( context );
    
    // Получение информации о функции из ядра (после компиляции)
    CALfunc     function;
    CALfuncInfo functionInfo;
    result = calModuleGetFuncInfo( &functionInfo, context, module, function );
    /* Хоть эта функция и выглядит привлекательной, она не работала в моем случае (я пытался узнать максимальное количество потоков, с которым может быть запущено ядро) */
    
    // Получить описание последней произошедшей ошибки из библиотеки aticalrt.dll
    const char* errorString = calGetErrorString();
    
    // Получить описание последней произошедшей ошибки из библиотеки aticalcl.dll (компилятор)
    const char* errorString = calclGetErrorString();
    

    Межпоточая синхронизация


    В отличие от Nvidia CUDA, вам не надо производить дополнительные действия с контекстом, если вы работаете с GPU из разных потоков. Но все же есть некоторые ограничения.

    Правило №12: все функции компилятора CAL не являются потоко-безопасными. В пределах одного приложения только один поток может работать с компилятором в один момент времени.

    Правило №13: все функции основной библиотеки CAL, работающие с конкретным контекстом/дескриптором устройства (context/device) являются потоко-безопасными. Все остальные функции не являются потоко-безопасными.

    Правило №14: только один поток приложения в один момент времени может работать с конкретным контекстом.

    Заключение


    Я попытался наиболее доступно описать технологии AMD CAL и AMD IL, чтобы любой желающий мог почти с нуля написать простое приложение для AMD GPU. Главное, всегда помнить одно золотое правило: RTFM!

    Надеюсь, вам было интересно читать.

    Ссылки для ознакомления


    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 8

      +1
      Какие задачи можно решать с помощью AMD GPU? Как-то не хочется въезжать в такое, когда не знаешь где можно применить данную технологию.
        +1
        Да в общем-то все те же самые, что решаются при помощи Nvidia CUDA или OpenCL: цифровая обработка сигнала, сортировка, подбор пароля по хешу и т.д. Главное условие: одна и та же операция над большим количеством данных (Single Instruction Multiple Data, SIMD) в один момент времени. (я продублировал комментарий, потому что промахнулся в первый раз)
        +1
        Да в общем-то все те же самые, что решаются при помощи Nvidia CUDA или OpenCL: цифровая обработка сигнала, сортировка, подбор пароля по хешу и т.д. Главное условие: одна и та же операция над большим количеством данных (Single Instruction Multiple Data, SIMD) в один момент времени.
          0
          Есть ли какие-либо преимущества использования CAL/IL перед OpenCL (скажем 1.0/1.1)?
            0
            Я ж написал в первой части в разделе «Отличие от OpenCL».
            Что касается скорости работы, у меня ядро на CAL/IL работало на 15% быстрее, чем на OpenCL. Возможно, новое SDK работает лучше, я не проверял.
              0
              Там скорее говорится о недостатках реализации OpenCL, а не о достоинствах CAL/IL.
              Мой вопрос скорее звучит так: есть ли какие либо полезные фишки в IL, которых нет даже в спецификации OpenCL'я?
                0
                Нет, таких фишек нет (вроде как). Все, что можно реализовать на OpenCL, реализуется на IL, и наоборот. Вот только будет ли это работать на OpenCL — большой вопрос.
                Основная фишка IL — больший контроль над происходящим и понимание, почему одно работает, а другое нет.
                  0
                  Хотя вроде для Evergreen GPU заявлены специальные multimedia operations (из разряда упаковка-распаковка данных). Не знаю, есть ли аналог в OpenCL.

          Only users with full accounts can post comments. Log in, please.