Постукивание руками, обмен вводными заметками о развитии оператора Ascend

Эта статья предоставлена ​​Джеффом Дином из « Вводных заметок по разработке операторов Ascend » сообщества Huawei Cloud .

Базовые концепты

Что такое Асценд С?

Ascend C — это язык программирования, разработанный CANN для сценариев разработки операторов. Он изначально поддерживает стандартные спецификации C и C++, чтобы наилучшим образом соответствовать привычкам разработки пользователей; он значительно повышает производительность операторов за счет таких ключевых технологий, как абстракция многоуровневого интерфейса, автоматические параллельные вычисления и двойная отладка.Эффективность разработки помогает разработчикам ИИ завершить разработку операторов, а также настройку и развертывание модели с низкими затратами.

Преимущества использования Ascend C для разработки собственных операторов

  • Примитивное программирование на C/C++ для максимального соответствия привычкам пользователей в области разработки.
  • Модель программирования скрывает различия в аппаратном обеспечении, а парадигма программирования повышает эффективность разработки.
  • Многоуровневая инкапсуляция API, от простого до гибкого, с учетом простоты использования и эффективности.
  • Двойная отладка: сторона ЦП имитирует поведение стороны NPU, что позволяет оптимизировать отладку на стороне ЦП.

Вычислительная архитектура Ascend CANN

Веб-сайт, посвященный CANN: https://www.hiascend.com/software/cann

AI Core — это вычислительное ядро ​​карты NPU. Внутри NPU имеется несколько ядер AI. Каждое ядро ​​AI эквивалентно ядру многоядерного процессора.

SIMD

SIMD, который представляет собой вычисление нескольких данных с помощью одной инструкции, одна инструкция может обрабатывать несколько данных: API программирования Ascend C - это в основном API векторных вычислений и API матричных операций, а API вычислений - это стиль SIMD.

Параллелизм данных SPMD и оптимизированный параллелизм в параллельных вычислениях

Принцип параллелизма данных SPMD

  • Запустить набор процессов, запускающих одну и ту же программу
  • Разделите данные, подлежащие обработке, и распределите разделенные данные на разные части для обработки.
  • Каждый процесс выполняет три задачи T1, T2 и T3 над своими фрагментами данных.

Принцип параллельности конвейеров

  • Запустить набор процессов
  • Сегментируйте данные
  • Каждый процесс обрабатывает все срезы данных и выполняет только одну задачу над срезами входных данных.

Модель и парадигма программирования Ascend C

Абстракция архитектуры параллельных вычислений

Операторы, разработанные с использованием языка программирования Ascend C, работают на AI Core, который является вычислительным ядром процессора Ascend AI. Внутри процессора AI
имеется несколько ядер AI. Ядро AI включает в себя вычислительные блоки, блоки хранения, обработки агрегаты и т. д. основные компоненты

Вычислительный блок включает в себя три основных вычислительных ресурса.

  1. Скалярный вычислительный блок: выполняет скалярные вычисления, такие как вычисление адреса и управление циклом, а также отправляет векторные вычисления, матричные вычисления, полукруги данных и инструкции синхронизации соответствующим блокам для выполнения.
  2. Вычислительный блок куба: отвечает за выполнение матричных операций.
  3. Блок векторных вычислений: отвечает за выполнение векторных вычислений.

Блок передачи отвечает за передачу данных между глобальной памятью и локальной памятью, включая блок передачи MTE (Memory Transfer Engine, блок передачи данных), MTE3 (блок передачи данных).

Единицей хранения является внутренняя память AI Core, называемая локальной памятью. Соответственно, внешняя память AI Core называется глобальной памятью.

Асинхронный поток команд

Блок скалярных вычислений считывает последовательность команд и отправляет инструкции векторного вычисления, матричного вычисления и передачи данных в очередь команд соответствующего блока.Блок векторного вычисления, блок матричного вычисления и блок передачи данных выполняют полученные инструкции асинхронно и в параллельно.

поток сигнала синхронизации

Между инструкциями могут быть зависимости.Чтобы гарантировать, что инструкции между различными очередями инструкций выполняются в соответствии с правильным логическим соотношением, скалярный вычислительный блок также выдает инструкции синхронизации соответствующим блокам.

Вычисление потока данных

Блок перемещения DMA перемещает данные в локальную память, блок вычислений вектора/куба завершает вычисление данных и записывает структуру вычислений обратно в локальную память, а блок вывода DMA перемещает обработанные данные обратно в глобальную память.

Введение в модель программирования SPMD

Операторское программирование Ascend C - это программирование SPMD. Обрабатываемые данные разделяются и распределяются параллельно по нескольким вычислительным ядрам. Несколько ядер AI используют один и тот же код инструкции. Единственная разница между запущенными экземплярами на каждом ядре заключается в том, что Block_idx различен. Подобно процессу, block_idx — это идентификатор процесса, который однозначно идентифицирует процесс. Используйте функцию GetBlockIdx() для получения идентификатора в программировании.

изображение.png

Написание и вызов функций ядра

Функция ядра — это вход на сторону устройства оператора Acend C. Ascend C позволяет пользователям использовать функции ядра (синтаксическое расширение функций C/C++) для управления выполняемым кодом на стороне устройства. Пользователи могут писать операторную логику в функциях ядра, например, настраивать классы операторов и их функции-члены для реализации алгоритма. Все функции саб. Функция ядра — это мост между стороной хоста и стороной устройства
изображение.png
. Функция ядра — это код, который выполняется непосредственно на стороне устройства. В функции ядра необходимо указать операции доступа к данным и вычисления, которые необходимо выполнить для кода, выполняемого на одном ядре.Модель программирования SPMD позволяет нескольким ядрам выполнять одну и ту же вычислительную задачу параллельно при вызове функции ядра.

Используйте квалификаторы типа функции

Помимо определения функции ядра в соответствии с объявлением функции C/C++, к функции ядра необходимо добавить дополнительные квалификаторы типа функции, включая __global__ и __aicore__.

Используйте квалификатор типа функции __global__, чтобы определить, что это основная функция, которую можно вызвать с помощью <<<...>>>; используйте квалификатор типа функции __aicore__, чтобы определить, что функция выполняется на стороне устройства AI Core

__gloabl__ __aircore__ void kernel_name(список аргументов);

изображение.png

Используйте квалификаторы типа переменной

Для удобства: единый тип входных переменных указателя определен как __gm__uint8_t*

Пользователи могут единообразно использовать указатели типа uint8_t и преобразовывать их в фактические типы указателей при использовании; они также могут напрямую передавать фактический тип указателя.

изображение.png

правила или предложения

  1. Функции ядра должны иметь тип возврата void.
  2. Поддерживаются только входные параметры, которые являются типами указателей или встроенными типами данных C/C++ (примитивные типы данных), например: half* s0, Flat* s1, int32_t c
  3. Предоставляет инкапсулированный макрос GM_ADDR, чтобы избежать длинных списков параметров функции.
#define GM_ADDR __gm__ unit8_t* __restrict__

Вызов функции ядра

Оператор вызова функции ядра является расширением оператора вызова функции C/C++.

Общие методы вызова функций C/C++ следующие:

имя_функции (список аргументов);

Функция ядра использует синтаксическую форму внутреннего вызывающего объекта <<<…>>> для указания конфигурации выполнения функции ядра:

имя_ядра<<<blockDim, l2ctrl, поток>>>(список аргументов);

Примечание: вызывающую программу ядра можно вызвать только при компиляции в режиме NPU. Этот символ не может быть распознан при компиляции в режиме CPU.

blocakdim предусматривает, что функция ядра будет выполняться на нескольких ядрах. Каждому ядру, выполняющему функцию ядра, будет присвоен логический идентификатор, который представлен встроенной переменной block_idx. Номер начинается с 0 и может быть определен для разных логических Поведение можно получить, используя функцию GetBlockIDX() в реализации оператора.

l2ctl, зарезервированная функция, отображает значение nullptr, для которого установлено фиксированное значение.

Поток: тип — aclrtStream.Поток — это очередь задач.Приложение использует поток для управления параллелизмом задач.

Используйте вызывающую программу ядра <<<…>>>> для вызова функции ядра:

HelloWorld<<<8, nullptr, поток>>>(fooDevice));

Для параметра blockDim установлено значение 8, что означает, что функция ядра HelloWorld вызывается на 8 ядрах. Каждое ядро ​​будет выполнять основную функцию независимо и параллельно. Поток можно создать с помощью aclrtCreateStream. Его функция заключается в явном создании списка аргументов aclrtStream. установлен входной параметр cooDevice.

Вызов функции ядра асинхронный, после завершения вызова функции ядра управление сразу возвращается на сторону хоста.

API, который заставляет программу на стороне хоста ждать выполнения всех основных функций (блокирует приложение до тех пор, пока все задачи в указанном потоке не будут завершены, интерфейс синхронизации) — aclrtSynchronizeStream.

aclError aclrtSynchronizeStream (поток aclrtStream);

Введение в API программирования

Операторы Ascend C программируются с использованием стандартного синтаксиса C++ и набора API-интерфейсов библиотеки классов.

API вычислений: API скалярных вычислений, API векторных вычислений, API матричных вычислений, соответственно реализуют вызов блока скалярных вычислений, блока векторных вычислений, блока вычислений куба.

API передачи данных: для выполнения вычислений на основе данных локальной памяти данные необходимо сначала перенести из Gloabl Memory в локальную память, затем использовать интерфейс расчета для завершения расчета и, наконец, перенести из локальной памяти в Gloabl Memory. Например, интерфейс DataCopy

API управления памятью: используется для распределения и управления памятью, например интерфейсы AllocTensor и FreeTensor.

API синхронизации задач: обеспечивает связь и синхронизацию между задачами, такими как интерфейсы EnQue и DeQue. Различные инструкции выполняются асинхронно и параллельно.Чтобы гарантировать, что инструкции между различными очередями инструкций выполняются в соответствии с правильной логической связью, синхронные инструкции необходимо отправлять различным компонентам.

Базовыми типами данных, используемыми API Ascend C для вычислений, являются Tensor: GlobalTensor и LocalTensor.

Определение API уровня 4

Определение API уровня 4: API разделены на четыре уровня в зависимости от сценариев использования пользователя.

API уровня 3, перегрузка операторов, поддерживает +, -,*,/,=,|,&,^,>,<,>-,<= для реализации простого выражения вычисления, аналогичного dst=src1+src2

API непрерывного расчета уровня 2, аналогичный Add(dst,src1,src2,count), вычисляет непрерывные данные COUNT исходного операнда и непрерывно записывает целевой операнд для решения проблемы вычисления данных непрерывного счета одномерного тензора.

API расчета срезов уровня 1 для решения проблем расчета срезов в многомерных данных (в разработке)

Вычислительный API с богатым набором функций уровня 0, вычислительный API, который может в полной мере использовать преимущества аппаратного обеспечения. Эта функция может полностью использовать мощные инструкции чипа серии CANN и поддерживать операции повторения, повторения и операции MASK для каждого операнда. Вызов аналогичен: Add(dst,src1,src2,repeatTimes,repeatParams);

изображение.png

Введение в парадигму конвейерного программирования

Парадигма программирования Ascend C разделяет внутреннюю программу обработки оператора на несколько конвейерных задач (Stage), использует тензор в качестве носителя данных, использует очередь (Queue) для связи и синхронизации между задачами, а также использует модуль управления памятью (Pipe) для управления память связи между задачами.

  • Фиксированные шаги для быстрого программирования разработки.
  • Ярлыки разработки для платформы единого кода
  • Опыт разработки, обобщенный пользователями
  • Идеи программирования для конкретных сценариев
  • Опыт разработки индивидуальных методологий

Модель абстрактного программирования «Параллельные вычисления TPIPE»

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

Основные элементы парадигмы параллельного программирования Ascend C

  • Набор параллельных вычислительных задач
  • Связь и синхронизация между задачами через очереди
  • Программисты могут автономно планировать параллельные вычислительные задачи и ресурсы.
Типичная вычислительная парадигма
  • Базовая парадигма векторного программирования: вычислительные задачи делятся на CopyIn, Compute, CopyOut.
  • Базовая парадигма матричного программирования: вычислительные задачи делятся на CopyIn, Compute, Aggregate, CopyOut.
  • Парадигма сложного векторного/матричного программирования, реализующая сложный поток расчетных данных путем объединения Out/ln вектора/матрицы

изображение.png

Запуск задач

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

В рамках функции ядра параллельная обработка данных может быть реализована с помощью конвейерных задач для повышения производительности.

Например, функции одноядерного процессора можно разделить на три задачи конвейера: Этап 1, Этап 2 и Этап 3. Каждая задача ориентирована на обработку срезов данных. Заголовки между этапами выражают зависимость между данными. Например, после того, как этап 1 обрабатывает Progress1, этап 2 может обрабатывать Progress1.

изображение.png

Если в Progres n=3, обрабатываемые данные делятся на 3 среза. Для одного и того же фрагмента данных обработка между Stage1, Stage2 и Stage3 имеет зависимости и должна обрабатываться последовательно; разные срезы данных одновременно Это может быть несколько этапов задач конвейера, которые обрабатываются параллельно, тем самым обеспечивая параллелизм задач и повышая производительность.

изображение.png

Межзадачная связь и синхронизация

Менеджер по передаче и синхронизации данных

Queue используется в Ascend C для передачи данных и синхронизации между задачами. Queue предоставляет базовые API, такие как EnQue и DeQue.

Когда Queue управляет различными уровнями физической памяти в NPU, она использует абстрактную логическую позицию (QuePosition) для выражения каждого уровня хранения (Storage Scope), заменяя концепцию встроенного физического хранилища, и разработчикам не нужно об этом знать. аппаратной архитектуры.

Типы очередей (логические местоположения) в векторном программировании включают: VECIN, VECOUT.

носитель данных

Ascend C использует GlobalTensor и LocalTensor в качестве основных операционных единиц данных.Это объект, напрямую вызываемый различными API-интерфейсами инструкций, а также являющийся носителем данных.

Векторное программирование межзадачной коммуникации и задач

Логическая позиция (QuePosition) в векторном программировании: место хранения внесенных данных: VECIN и место хранения вынесенных данных: VECOUT.

Векторное программирование в основном делится на три задачи: CopyIn, Compute и CopyOut.

  • После передачи входных данных из GlobalTensor в LocalTensor в задаче CopyIn вам необходимо использовать EnQue, чтобы поместить LocalTensor в очередь VECIN.
  • Задача Compute ожидает, пока LocalTensor в очереди VECIN будет исключен из очереди, прежде чем она сможет выполнить векторные вычисления. После завершения расчета используйте EnQue, чтобы поместить результат вычисления LocalTensor в очередь VECOUT.
  • Задача CopyOut ожидает удаления Localtensor из очереди VECOUT, а затем копирует его в GlobalTensor.

Этап 1. Задача CopyIn

Используйте интерфейс DataCopy, чтобы скопировать GlobalTensor в LocalTensor.

Используйте EnQue, чтобы поместить LocalTensor в очередь VECIN

Этап 2: Вычислительная задача

Используйте DeQue для получения LocalTensor от VECIN

Выполните векторные вычисления с помощью командного API Ascend C:Добавить.

Используйте EnQue, чтобы поместить результат LocalTensor в очередь VECOUT.

Этап 3: Задача копирования

Используйте интерфейс DeQue, чтобы получить LocalTensor из очереди VECOUT.

Используйте интерфейс DataCopy для копирования LocalTensor в GlobalTensor.

изображение.png

Управление памятью

Память, используемая для передачи данных задачи, единообразно управляется модулем управления памятью Pipe.

В качестве встроенного менеджера памяти Pipe предоставляет функцию инициализации памяти очереди через интерфейс InitBuffer. Разработчики могут выделять память для указанной очереди через этот интерфейс.

После инициализации памяти очереди очереди, когда требуется память, выделите память для LocalTensor, вызвав AllocTensor. Когда созданный LocalTensor завершит соответствующие вычисления и больше не будет нуждаться в использовании, вызовите FreeTensor для повторного использования памяти LocalTensor.

изображение.png

Управление временной переменной памятью

Временная переменная память, используемая в процессе программирования, также управляется через Pipe. Временные переменные могут использовать структуру данных TBuf для подачи заявки на пространство для хранения в указанном QuePosition и использовать Get() для выделения выделенного пространства для хранения новому LocalTensor для получения полной длины из TBuf или для получения LocalTensor указанного длина.

LocalTensor<T> Get<T>(); 
LocalTensor<T> Get<T>(uint32_t len);

Примеры интерфейсов Tbuf и Get

// Выделяем память для инициализации TBuf. Длина выделенной памяти — 1024 байта 
TPipe Pipe; 
TBuf<TPosition::VECIN>calcBuf; // Параметр шаблона — это тип VECIN в QuePosition 
uint32_t byteLen = 1024; 
Pipe.InitBuffer(calcBuf,byteLen) ) ; 
//Получаем Tensor из CalcBuf. Размер всей памяти, выделенной Tensor для канала, составляет 1024 байт. LocalTensor 
<int32_t> tempTensor1 = CalcBuf.Get<int32_t>(); 
//Получаем Tensor из CalcBuf. Tensor имеет тип 128 int32_t. элементов.Объем памяти составляет 512 байт. 
LocalTensro<int32_t> tempTensor1 =calcBuf.Get<int32_t>(128);

Пространство памяти, использованное для использования TBuf, может участвовать только в вычислениях и не может выполнять операции постановки и удаления из очереди Queue.

Векторное программирование Ascend C

Операторный анализ

Процесс развития

Анализ оператора: анализируйте математическое выражение, реализацию логики ввода, вывода и вычислений оператора, а также уточняйте интерфейс Ascend, который необходимо вызвать.

Определение функции ядра: определение функции ввода оператора Ascend.

Реализуйте класс операторов в соответствии с парадигмой векторного программирования: выполните внутреннюю реализацию функции ядра.

изображение.png

Принимая оператор ElemWise(ADD) в качестве математической формулы

 

Для простоты задайте для тензоров x, y, z фиксированную форму (8, 2048), тип данных dtype — половину типа, формат типа размещения данных — ND, а имя функции ядра — add_custom.

 

Операторный анализ

изображение.png

Уточнить математическое выражение и логику расчета оператора

Математическое выражение оператора Add:

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

 

Очистить ввод и вывод

Есть два оператора добавления:

Тип входных данных является половинным, а тип выходных данных такой же, как тип входных данных. Входные данные поддерживают фиксированную форму (8, 2048), выходная форма такая же, как входная форма, а тип расположения входных данных — ND.

 

Определить имя и параметры функции ядра

Настройте описание функции ядра, например add_custom. В соответствии с входными и выходными данными определите, что функция ядра имеет три входных параметра x, y, z. x и y — это адреса памяти, вводимые в GlobalMemory, а z — выходной адрес памяти
. в глобальной памяти.

Определить интерфейс, необходимый для реализации оператора

При передаче данных между внутренним и внешним хранилищем используйте интерфейс передачи данных: реализация DataCopy.

Операции сложения, включающие векторные вычисления, реализуются с помощью векторных бинокулярных инструкций: Сложить

Используется LocalTensor, используется управление очередью Queue и такие интерфейсы, как Enque и Deque.

Реализация оператора

Определение функции ядра

В реализации функции ядра add_custom создается экземпляр класса оператора KernelAdd, функция Init() вызывается для завершения инициализации памяти, а функция Process() вызывается для завершения базовой логики.

Примечание. Особых требований к именам классов операторов и функций-членов нет. Разработчики могут выбрать конкретную реализацию функции ядра на основе своих собственных навыков программирования на C/C++.

// реализация функции kenel 
extern "C" __global__ __aicore__ void add_custom(__gm__ uint8_t* x, __gm__ uint8_t* y, __gm__ uint8_t* z) 
{ 
	kernelAdd op; 
	op.Init(x,y,z); 
	оп.Процесс(); 
}

Для вызова функции ядра используйте встроенный макрос __CCE_KT_TEST__ для идентификации <<<…>>>, который будет скомпилирован только в режиме NPU (режим CPU g++ не имеет выражения <<<…>>>) , для функции ядра. При вызове инкапсуляции к инкапсулированной функции можно добавить другую логику. Здесь показан только вызов основной функции.

#ifndef __CCE_KT_TEST__ 
// вызов функции ядра 
void add_custom_do(uint32_t blockDim, void* l2ctrl, void*stream, uint8_t* x, uint8_t* y, uint8_t* z) { 
add_custom 
	<<<blockDim, l2ctrl,stream>>>(x ,у,з); 
}

Реализация класса оператора

Задача CopyIn: переместите входные тензоры xGm и yGm из глобальной памяти в локальную память и сохраните их в xlocal и ylocal соответственно.

Задача вычислений: выполнять операции сложения над xLocal и yLocal, а результаты вычислений сохраняются в zlocal.

Задача CopyOut: перенести выходные данные из zlocal в выходной тензор zGm в глобальной памяти.

Задачи CopyIn.Compute взаимодействуют и синхронизируются через очередь VECIN и inQueueX, inQueueY.

Задачи Compute и CopyOut взаимодействуют и синхронизируются через VECOUT и outQueueZ.

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

изображение.png

Пример кода векторного сложения z=x+y Парадигма конвейерного программирования TPIPE

изображение.png

Реализация класса оператора

Имя класса оператора: KernelAdd.

Функция инициализации Init() и основная функция обработки Process()

Три задачи конвейера: CopyIn(), Compute(), CopyOut().

Значение процесса
изображение.png

Значение BUFFER)NUM шаблона TQue:

Глубина очереди, методы оптимизации двойного буфера

class KernelAdd{ 
public: 
	__aicore__ inline KernelAdd() 
	//Функция инициализации, завершает операции, связанные с инициализацией памяти__aicore__ 
	inline voide Init(__gm__ uint8_t* x, __gm__ uint8_t* y, __gm__ uint8_t* z){} 
	// Основная функция обработки, реализует расчет Sub -logic, вызов закрытых функций-членов CopyIn, Compute, CopyOut для завершения логики оператора 
	__aicore__ inline void Process(){} 

Private: 
	// Перемещение в функцию, завершение обработки этапа CopyIn и вызов функции Process 
	__aicore__ inline void CopyIn(int32_tprocess){ } 
	// Вычисляем функцию, завершаем обработку этапа вычислений и вызываем функцией Process 
	__aicore__ inline void Compute(int32_tprocess){} 
	// Выходим из функции, завершаем обработку фазы CopyOut и вызываться функцией процесса 
	__aicore__ inline void CopyOut(int32_tprocess){ } 

Private: 
	// объект управления памятью канала 
	TPipe Pipe; 
	// Объект управления очередью входных данных, QuePosition — это VECIN 
	TQue<QuePosition:: VECIN, BUFFER_NUM> inQueueX, inQueueY; 
	// Объект управления очередью выходных данных, QuePosition — это VECOUT 
	TQue <QuePosition::VECOUT, BUFFER_NUM> outQueueZ; 
	// Объект, который управляет адресом входных и выходных данных в глобальной памяти, где xGm, yGm — входные данные, а zGm — выходные данные 
	GlobalTensor<half> xGm, yGm,zGm; 
};

Реализация функции Init()

Чтобы использовать многоядерные параллельные вычисления, данные необходимо разделить на части, чтобы получить адрес смещения памяти в глобальной памяти, который фактически необходимо обработать каждому ядру.

Общая длина данных TOTAL_LENGTH составляет 8 * 2048, которая равномерно распределяется для работы на 8 ядрах. Размер данных BLOCK_LENGTH, обрабатываемых на каждом ядре, равен 2048. Block_idx — это логический идентификатор ядра (gm half*)x + GetBlockIdx(). * BLOCK_LENGTH ,  то
есть адрес смещения памяти входных данных ядра с индексом block_idx в глобальной памяти.

Для одноядерной обработки данных вы можете выполнить нарезку данных (Tiling), чтобы разделить данные на блоки 8. После нарезки каждый блок данных снова делится на блоки BUFFER_NUM=2. Двойной буфер можно включить для достижения параллелизма между конвейерами.

2048 данных, которые необходимо обработать одному ядру, разделены на 16 блоков, каждый блок имеет TILE_LENGTH = 128 данных. Канал выделяет блоки памяти BUFFER_NUM с размером блока TITLE_LENGTH * sizeof (половина) байтов для inQueueX. Каждый блок памяти может вместить TILE_LENGTH. .=128 данные полутипа

изображение.png

пример кода

constexpr int32_t TOTAL_LENGTH = 8 * 2048; //общая длина данных 
constexpr int32_t USE_CORE_NUM = 8; //количество используемых ядер 
constexpr int32_t BLOCK_LENGTH = TOTAL_LENGTH / USE_CORE_NUM; //вычисленная длина каждого ядра 
constexpr int32_t TILE_NUM = 8; //разбиваем данные на 8 фрагментов 
constexpr int32_t BUFFER_NUM = 2; //количество тензоров для каждой очереди 
constexpr int32_t TILE_LENGTH = BLOCK_LENGTH/TILE_NUM/BUFFER_NUM; //разделяется на 2 части из-за двойного буфера 

__aicore__ inline void Init(GM_ADDR x, GM_ADDR y, GM_ADDR z) 
{ 
	//получаем стартовый индекс для текущего ядра, параллельное ядро 
	​​xGm,SetGlobalBuffer((__gm__ half*)x * BLOCK_LENGTH * GetBlockIdx(), BLOCK_LENGTH); 
	yGm,SetGlobalBuffer((__gm__ half*)y * BLOCK_LENGTH * GetBlockIdx(), BLOCK_LENGTH); 
	zGm,SetGlobalBuffer((__gm__ half*)z * BLOCK_LENGTH * GetBlockIdx(), BLOCK_LENGTH); 
	//выделение памяти в очередь по конвейеру, единицей измерения является байты 
	Pipe.InitBuffer(inQueueX, BUFFER_NUM, TILE_LENGTH * sizeof(half)); 
	Pipe.InitBuffer(inQueueY, BUFFER_NUM, TILE_LENGTH * sizeof(half)); 
	Pipe.InitBuffer(outQueueZ, BUFFER_NUM, TILE_LENGTH * sizeof(half)); 
}

Реализация функции Process()

изображение.png

пример кода

__aicore__ inline void Process() 
{ 
	// количество циклов необходимо удвоить из-за двойного буфера 
	constexpr int32_tloopCount = TILE_NUM * BUFFER_BUM; 
	// стратегия тайлинга, параллельность конвейера 
	for (int32_t i = 0; i <loopCount; i++) { 
		CopyIn(i); 
		Вычислить(я); 
		Копирование (я); 
	} 
} 

__aicore__ inline void CopyIn(int32_t Progress) 
{ 
	// выделяем тензор из памяти очереди 
	LocalTensor<half> xLocal = inQueueX.AllocTensor<half>(); 
	LocalTensor<half> yLocal = inQueueY.AllocTensor<half>(); 
	// копируем тайл Progress_th из глобального тензора в локальный тензор 
	DataCopy(xLocal,xGm[progress * TILE_LENGTH], TILE_LENGTH); 
	DataCopy(xLocal,yGm[прогресс * TILE_LENGTH], TILE_LENGTH); 
	// заносим входные тензоры в очередь VECIN 
	inQueueX.EnQue(xLocal); 
	inQueueY.EnQue(yLocal); 
} 

__aicore__ inline void Compute(int32_t Progress) 
{ 
	//запрашиваем входные тензоры из очереди VECIN 
	LocalTensor<half> xLocal = inQueueX.DeQue<half>(); 
	LocalTensor<half> yLocal = inQueueY.DeQue<half>(); 
	LocalTensor<half> zLocal = outQueueZ.AllocTensor<half>(); 
	// вызов Add instr для вычислений 
	Add(zLocal, xLocal, yLocal, TILE_LENGTH); 
	// поместим выходной тензор в очередь VECOUT 
	outQueueZ.EnQue<half>(zLocal)l 
	// освободим входные тензоры для повторного использования 
	inQueueX.FreeTensor(xLocal); 
	inQueueY.FreeTensor(yLocal); 
} 

__aicore__ inline void CopyOut(int32_t Progress) 
{ 
	//вывод тензора вывода из очереди VECOUT 
	LocalTensor<half> zLocal = outQueueZ.Deque<half>(); 
	// копируем тайл Progress_th из локального тензора в глобальный тензор 
	DataCopy(zGm[progress * TILE_LENGTH), zlocal, TILE_LENGTH); 
	// свободный тензор вывода для повторного использования 
	outQueueZ.freeTensor(zLocal); 
}

двойной буферный механизм

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

Если обрабатываемые данные разделены на части, например Tensor1 и Tensor2.

  • Когда блок векторных вычислений выполняет вычисления на Tensor1, Tensor2 может выполнять задачи CopyIn.
  • Когда блок векторных вычислений выполняет вычисления на Tensor2, Tensor1 может выполнять задачи CopyOut.
  • Когда блок векторных вычислений выполняет CopyOut на Tensor2, Tensor2 может выполнять задачу CopyIn.
    В результате входящая и исходящая передача данных и векторные вычисления распараллеливаются, а проблема простаивающих аппаратных блоков эффективно устраняется.

изображение.png

Вызов оператора Ascend C

Пример HelloWorld

Запустите заголовочные файлы, включенные в режим CPU.

Заголовочные файлы, включенные в режим NPU

Определение функции ядра

Встроенный макрос __CE_KT_TEST__: флаг, позволяющий различать работу логики режима ЦП или режима NPU.

Логика выполнения на стороне хоста: отвечает за применение данных в памяти на стороне хоста, копирование с хоста на устройство, синхронизацию выполнения функций ядра и переработку ресурсов.

Логика выполнения на стороне устройства

Логика режима ЦП выполнения на стороне хоста: используйте инкапсулированный макрос выполнения ICPU_RUN_KF.

в основном включают:

gMAlloc(…): подать заявку на использование памяти в режиме ЦП.

ICPU_RUN_KF: использовать инкапсулированные макросы выполнения.

GmFree: освободить место памяти в режиме процессора

процесс

Инициализация AscendCL—>Запустить приложение управления ресурсами—>Передача данных хоста на устройство—>Выполнить задачи и подождать—>Передача данных устройства на хост—>Запустить выпуск ресурса—>Деинициализация AscendCL

Выполните логику режима NPU на стороне хоста: используйте вызывающую программу ядра <<<…>>>

Важный интерфейс
  • аклинит
  • aclCreateStream
  • aclMallocHost
  • aclMalloc
  • aclMemcpy
  • <<<…>>>>
  • aclrtSynchronizeStream
  • aclrtБесплатно
  • aclrtfreeHost
  • aclrtDestoryStream
  • aclFinalize

АддКастомсампле

Пример кода векторного оператора Ascend C

  1. Исходный файл функции ядра: add_custom.app
  2. Скрипт генерации данных истинного значения: add_custom.py
  3. CmakeLists.txt: удобно для компиляции нескольких исходных файлов.
  4. Вспомогательная функция для чтения и записи файлов данных: data_utils.h
  5. Исходный файл на стороне хоста: main.cpp
  6. Выполните скрипт одним щелчком мыши: run.sh
  7. Организация сценариев cmake, скомпилированных в режиме ЦП и режиме NPU.

Нажмите, чтобы подписаться и узнать о новых технологиях Huawei Cloud как можно скорее~

Автор открытого фреймворка NanUI перешёл на продажу стали, и проект был приостановлен.Список бесплатных бесплатных приложений номер один в Apple App Store занимает порнографическое ПО TypeScript.Он только стал популярным, почему большие парни начинают от него отказываться ? Октябрьский список TIOBE: Наибольший спад в Java, C# приближается к выпуску Java Rust 1.73.0 Мужчину подтолкнула его подруга-ИИ к убийству королевы Англии, и он был приговорен к девяти годам тюремного заключения Qt 6.6 официально выпущен Reuters: RISC-V Технология становится ключом к китайско-американской технологической войне Новое поле битвы RISC-V: Lenovo не контролируется какой-либо отдельной компанией или страной и планирует выпустить ПК на базе Android
{{o.name}}
{{м.имя}}

рекомендация

отmy.oschina.net/u/4526289/blog/10116433
рекомендация