¡Puro producto seco! Un artículo ofrece todos los puntos de conocimiento para comenzar con la programación de Ascend C.

Este artículo es compartido por Huawei Cloud Community " Tutorial de introducción a la programación de Ascend C ", autor: Ascend CANN.

El 6 de mayo de 2023, en la Cumbre de Desarrolladores de IA de Ascend, Huawei lanzó oficialmente el lenguaje de programación Ascend C para escenarios de desarrollo de operadores. Ascend C admite de forma nativa las especificaciones de programación C/C++. A través de tecnologías como la abstracción de interfaz multicapa, el paradigma de programación paralela y la depuración gemela, mejora en gran medida la eficiencia del desarrollo de los operadores y ayuda a los desarrolladores de IA a completar el desarrollo de operadores y el ajuste e implementación de modelos a bajo costo. costo. .

Base de hardware y software de Ascend AI

Al igual que los operadores desarrollados por CUDA se ejecutan en la GPU, los operadores desarrollados en base a Ascend C pueden ejecutarse en el procesador Ascend AI (NPU para abreviar) a través de la arquitectura informática heterogénea CANN (Compute Architecture for Neural Networks). CANN es una pila de software que habilita el procesador Ascend AI y, a través de la cooptimización de software y hardware, puede aprovechar al máximo la poderosa potencia informática del procesador Ascend AI. Como se puede ver claramente en el diagrama de arquitectura a continuación, los operadores desarrollados utilizando el lenguaje de programación Ascend C son compilados por un compilador, programados en tiempo de ejecución y finalmente ejecutados en el procesador Ascend AI.

cke_150.png

Sabemos que la computación de propósito general es lo que a menudo escribimos y ejecutamos en la CPU. Es buena en control lógico y computación en serie. En comparación con la computación de propósito general, la computación con IA es mejor en computación paralela y puede admitir computación a gran escala. tareas intensivas. . Como se muestra en la figura de la izquierda a continuación, para realizar una multiplicación de matrices, se requieren tres capas de bucles for para los cálculos de la CPU, mientras que la figura de la derecha usa la unidad de cálculo vectorial en el procesador Ascend AI, solo se requieren dos capas de bucles for y el código de cálculo mínimo puede calcular simultáneamente múltiples La multiplicación y suma de datos está un paso más cerca. Si usa la unidad informática Cube, solo necesita una declaración para completar el cálculo de una multiplicación de matrices. Esto es lo que llamamos SIMD (Instrucción única Múltiples datos). Por lo tanto, solemos utilizar procesadores de IA para realizar cálculos paralelos masivos.

cke_151.png

La NPU no puede ejecutarse de forma independiente y necesita trabajar con la CPU. Puede considerarse como un coprocesador de la CPU. La CPU es responsable de ejecutar todo el sistema operativo, administrar diversos recursos y realizar controles lógicos complejos, mientras que la NPU es la principal responsable. para tareas de computación paralela. En la arquitectura informática heterogénea basada en CPU + NPU, la NPU y la CPU están conectadas entre sí a través del bus PCIe para trabajar juntas. La ubicación de la CPU se llama host (host) y la ubicación de la NPU se llama dispositivo. (dispositivo) El diagrama esquemático es el siguiente:

cke_152.png

Aquí hay una introducción detallada al procesador Ascend AI. Los procesadores Ascend AI vienen en diferentes modelos y formas de productos, desde módulos y tarjetas aceleradoras hasta servidores y clústeres. La parte central del procesador Ascend AI es el AI Core. Hay múltiples AI Cores, que son los núcleos informáticos acelerados por la red neuronal. Cada AI Core es equivalente a cada núcleo en la CPU multinúcleo que normalmente entendemos. Utiliza el lenguaje de programación Ascend C. El operador desarrollado se ejecuta en AI Core, porque la aceleración del cálculo de la red neuronal central proviene de la potencia informática del AI Core.

La abstracción de la arquitectura informática paralela dentro de AI Core se muestra en la siguiente figura:

cke_153.pngEl núcleo abstracto de esta arquitectura informática paralela incluye varios componentes grandes. Hay una memoria global fuera del núcleo AI, que es compartida por varios núcleos AI. Hay una memoria local dentro del núcleo AI. Porque es cerca de la unidad de computación, su ancho de banda será muy alto y la capacidad relativa será muy pequeña, generalmente de cientos de K a 1 M. Los componentes principales dentro de AI Core tienen tres unidades informáticas: una unidad informática escalar, una unidad informática vectorial y una unidad informática matricial. También hay una unidad de manejo DMA, que se encarga de mover datos entre la Memoria Global y la Memoria Local.

El proceso de computación paralela asíncrona dentro del AI Core: la unidad de computación escalar lee la secuencia de instrucciones y transmite las instrucciones de computación vectorial, computación matricial y manejo de datos a la cola de instrucciones de la unidad correspondiente, y la unidad de computación vectorial, matriz. La unidad de computación y la unidad de manejo de datos se ejecutan de forma asincrónica y en paralelo. Las instrucciones recibidas. Este proceso puede hacer referencia al flujo de instrucciones que se muestra con la flecha azul en la figura anterior. Puede haber dependencias entre diferentes instrucciones. Para garantizar que las instrucciones entre diferentes colas de instrucciones se ejecuten de acuerdo con la relación lógica correcta, la unidad informática Scalar también emitirá instrucciones de sincronización a las unidades correspondientes. El proceso de sincronización entre unidades puede referirse al flujo de señal de sincronización que se muestra con la flecha naranja en la figura anterior.

El proceso básico del procesamiento de datos interno de AI Core: la unidad de importación DMA transfiere los datos a la memoria local, la unidad de cálculo Vector/Cubo completa los datos y escribe los resultados del cálculo nuevamente en la memoria local, y la unidad de transferencia DMA transfiere los datos. los datos procesados ​​de regreso a la Memoria Global. Este proceso puede referirse al flujo de datos que se muestra con la flecha roja en la figura anterior.

Fundamentos del modelo de programación Ascend C

Paradigma de programación de Ascend C

El paradigma de programación de Ascend C es un paradigma de programación canalizada: el programa de procesamiento en el núcleo del operador se divide en múltiples tareas de canalización, y la comunicación y sincronización entre tareas se completa a través de la cola (Queue) y el módulo de administración de memoria unificada (Pipe). Gestionar la memoria de comunicación entre tareas. El paradigma de programación de tuberías aplica el método de computación paralela de tuberías.

cke_154.png

Si n = 3, es decir, los datos a procesar se dividen en 3 partes, el diagrama esquemático de la tarea de canalización que se ejecuta en la figura anterior es el siguiente: en la figura de operación, se puede ver que para la misma parte de datos, los datos entre el procesamiento Stage1, Stage2 y Stage3 tienen dependencias y requieren procesamiento en serie; diferentes segmentos de datos pueden tener múltiples tareas procesadas en paralelo al mismo tiempo, logrando así el propósito del paralelismo de tareas y mejorando el rendimiento.

cke_155.png

Ascend C ha diseñado diferentes tareas de canalización para la programación de vectores y cubos, respectivamente. Los desarrolladores solo necesitan completar la implementación del código de las tareas básicas. La sincronización de instrucciones subyacentes y la programación paralela se implementan mediante el marco Ascend C, y los desarrolladores no necesitan prestar atención.

paradigma de programación vectorial

El paradigma de programación vectorial divide el proceso de implementación de operadores en tres tareas básicas: CopyIn, Compute y CopyOut. CopyIn es responsable de la operación de entrada, Compute es responsable de la operación de cálculo de vectores y CopyOut es responsable de la operación de salida.

cke_156.png

Solo necesitamos completar la implementación del código de las tareas básicas de acuerdo con el paradigma de programación, y la sincronización de instrucciones subyacentes y la programación paralela se implementan mediante el marco Ascend C.

¿Cómo completa Ascend C la comunicación y sincronización de datos entre diferentes tareas? Aquí, Ascend C proporciona la API de administración de colas Queue, principalmente dos API de operación de colas EnQue, DeQue y abstracción lógica de memoria.

La posición lógica (QuePosition) utilizada en la programación vectorial se define de la siguiente manera:

  • La ubicación de almacenamiento de los datos importados: VECIN;
  • Calcular la posición de la variable intermedia: VECCALC;
  • La ubicación de almacenamiento de los datos exportados: VECOUT.

Como se puede ver desde el frente, la programación vectorial se divide principalmente en tres tareas: CopyIn, Compute y CopyOut. Después de que los datos de entrada se mueven de la memoria global a la memoria local en la tarea CopyIn, se debe usar EnQue para colocar el Tensor local en la cola VECIN; la tarea Compute espera a que el Tensor local en la cola VECIN sea retirado de la cola antes de completar el cálculo vectorial. Una vez completado el cálculo, use EnQue para El resultado del cálculo LocalTensor se coloca en la cola de VECOUT; la tarea CopyOut espera a que el LocalTensor en la cola de VECOUT salga de la cola y luego lo copia en la memoria global. De esta forma, la cola Queue completa la comunicación de datos y la sincronización entre las tres tareas. El proceso específico y el diagrama de flujo son los siguientes:

  • Etapa 1: tarea Copiar entrada.

Utilice la interfaz DataCopy para copiar datos de GlobalTensor a LocalTensor.

Utilice la interfaz EnQue para colocar el LocalTensor en la cola VECIN.

  • Etapa 2: tarea de cálculo.

Obtenga LocalTensor de VECIN usando la interfaz DeQue.

Los cálculos vectoriales se realizan utilizando la interfaz Ascend C.

Utilice la interfaz EnQue para colocar el resultado del cálculo LocalTensor en la cola de VECOUT.

  • Etapa 3: tarea CopyOut.

Utilice la interfaz DeQue para eliminar LocalTensor de la cola de VECOUT.

Utilice la interfaz DataCopy para copiar LocalTensor a GlobalTensor.

cke_157.png

De esta manera, nuestro código de implementación del kernel es muy claro. Primero inicialice la memoria y la cola, y luego realice las tres etapas de CopyIn, Compute y CopyOut a través del paradigma de programación.

Programación paralela SPMD - multinúcleo

Al presentar anteriormente el procesador Ascend AI, se introdujo que hay múltiples núcleos AI, entonces, ¿cómo podemos aprovechar al máximo los múltiples núcleos AI? Entre los métodos de computación paralela más utilizados, existe un método paralelo de datos SPMD (programa único, datos múltiples), que en pocas palabras consiste en dividir los datos en partes y cada parte de los datos pasa por un proceso completo de procesamiento de datos. Esto se puede combinar con el multinúcleo del procesador Ascend AI. Dividimos los datos en varias partes y cada parte de los datos se procesa en un núcleo. De esta manera, cada parte de los datos se procesa en paralelo y Se procesan todos los datos. Ascend C es programación SPMD (programa único y datos múltiples). Múltiples núcleos AI comparten el mismo código de instrucción. La única diferencia entre las instancias en ejecución en cada núcleo es que el block_idx (variable incorporada) es diferente, por lo que podemos usar block_idx para distinguir diferentes núcleos, siempre que la dirección de datos en la Memoria Global esté segmentada y compensada, cada núcleo puede procesar su parte correspondiente de los datos.

cke_158.png

Cuando se llama a un operador, todos los núcleos informáticos ejecutan el mismo código de implementación y los parámetros de entrada de la función de entrada también son los mismos. La dirección de datos procesada en cada núcleo debe obtenerse agregando el desplazamiento de block_idx*BLOCK_LENGTH (longitud de datos procesada por cada bloque) a la dirección inicial. De esta manera, se logra la segmentación de datos para la computación paralela de múltiples núcleos.

class KernelAdd { 

public: 

__aicore__ inline KernelAdd() {} 

__aicore__ inline void Init(GM_ADDR x, GM_ADDR y, GM_ADDR z) 

{ 

// obtiene el índice inicial para el núcleo actual, núcleo paralelo 

GM_ADDR xGmOffset = x + BLOCK_LENGTH * GetBlockIdx(); 

GM_ADDR yGmOffset = y + BLOCK_LENGTH * GetBlockIdx(); 

GM_ADDR zGmOffset = z + BLOCK_LENGTH * GetBlockIdx(); 

xGm.SetGlobalBuffer((__gm__ half*)xGmOffset, BLOCK_LENGTH); 

yGm.SetGlobalBuffer((__gm__ half*)yGmOffset, BLOCK_LENGTH); 

zGm.SetGlobalBuffer((__gm__ half*)zGmOffset, BLOCK_LENGTH); 

…… 

} 

…… 

}

Introducción a la API de Ascend C

En toda la implementación del kernel, el código central es Add(zLocal, xLocal, yLocal, TILE_LENGTH); el cálculo de la suma de todos los datos se completa a través de una interfaz API proporcionada por Ascend C, sí, lo leíste bien, esta interfaz se completa calcula .

A continuación, presentaremos la API proporcionada por Ascend C. Los operadores de Ascend C utilizan la sintaxis estándar de C++ y un conjunto de API de biblioteca de clases para la programación. Las API de biblioteca de clases incluyen principalmente los siguientes tipos. Puede elegir la API adecuada según sus necesidades en la implementación de la función del kernel:

cke_159.png

  • Las API de computación, incluidas la API de computación escalar, la API de computación vectorial y la API de computación matricial, implementan las funciones de llamar a unidades de computación escalar, unidades de computación vectorial y unidades de computación cúbica para realizar cálculos.
  • API de transferencia de datos, la API de cálculo anterior se calcula en función de los datos de la memoria local, por lo que los datos deben transferirse primero de la memoria global a la memoria local, luego usar la interfaz de cálculo para completar el cálculo y finalmente pasar de la memoria local a la memoria global. . La interfaz que realiza el proceso de transferencia se denomina interfaz de transferencia de datos, como la interfaz DataCopy.
  • API de administración de memoria, utilizada para asignar y administrar memoria, como las interfaces AllocTensor y FreeTensor.
  • API de sincronización de tareas para completar la comunicación y sincronización entre tareas, como las interfaces EnQue y DeQue.

Los operandos de cálculo de la API de Ascend C son tipos de tensor: GlobalTensor y LocalTensor.

Después de presentar los tipos de API de Ascend C, expliquemos por qué todos los números se pueden calcular con una interfaz Agregar. El modelo de programación original de Ascend C se basa en la arquitectura SIMD (Instrucción única de datos múltiples). Una sola instrucción puede completar múltiples operaciones de datos y, al mismo tiempo, algunas funciones avanzadas de las instrucciones están encapsuladas dentro de la API.

Proceso básico de ejecución del operador

Como se mencionó anteriormente, en la arquitectura informática heterogénea, la NPU y la CPU trabajan juntas. En el modelo de programación Ascend C, necesitamos implementar el código en el lado de la NPU y el código en el lado de la CPU. El código del lado de la NPU generalmente se llama código de implementación del kernel, y el código del lado de la CPU generalmente se llama código de implementación del host. Un código completo de Ascend C generalmente incluye el código de implementación del lado del host y el código de implementación del lado del kernel. El proceso básico de ejecución del operador Ascend C es el siguiente:

  1. Inicialice el dispositivo del dispositivo;
  2. Crear un dispositivo de enlace de contexto;
  3. Asignar memoria del Host e inicializar datos;
  4. Asigne memoria del dispositivo y copie datos del host al dispositivo;
  5. Utilice el símbolo de llamada del kernel <<<>>> para llamar a la función del kernel para completar la operación especificada;
  6. Copie el resultado de la operación en el Dispositivo nuevamente al Host;
  7. Liberar el recurso solicitado.

Introducción a la función kernel

En el proceso anterior, el paso más importante es llamar a la función del núcleo para realizar tareas informáticas paralelas. La función del kernel (función del kernel) es el punto de entrada para la implementación del operador Ascend C en el lado del dispositivo. En la función del núcleo, las operaciones de cálculo y acceso a datos que se realizarán deben especificarse para el código ejecutado en el núcleo de IA.

externo "C" __global__ __aicore__ void add_custom(__gm__ uint8_t* x, __gm__ uint8_t* y, __gm__ uint8_t* z);

Lo anterior es un ejemplo de una declaración de función del kernel, "C" externa significa que la función del kernel se compila y vincula de acuerdo con el protocolo de compilación y vinculación similar a C, el calificador de tipo de función __global__ indica que es una función del kernel y el El calificador de tipo de función __aicore__ indica que La función del kernel se ejecuta en AI Core en el lado del dispositivo. El calificador de tipo de variable __gm__ en la lista de parámetros indica que la variable de puntero apunta a una dirección de memoria en algún lugar de la Memoria Global. Tenga en cuenta que los parámetros de entrada aquí solo pueden admitir punteros o tipos de datos integrados de C/C++. El tipo de puntero utilizado en el ejemplo es uint8_t, que debe convertirse a un tipo de puntero real en su uso posterior.

La función del núcleo en el modelo de programación de Ascend C se llama mediante el símbolo de llamada del núcleo <<<...>>>, de la siguiente manera:

kernel_name<<<blockDim, l2ctrl, stream>>>(lista de argumentos);

kernel_name es el nombre de la función del kernel mencionada anteriormente, y la lista de argumentos es el parámetro de entrada de la función del kernel. En el medio de <<<>>>, hay 3 parámetros:

  • blockDim, especifica que la función del kernel se ejecutará en varios núcleos, podemos configurarlo en 1 primero;
  • l2ctrl, mantiene los parámetros, configúrelo temporalmente en un valor fijo nullptr, no necesitamos prestar atención;
  • flujo, creado usando aclrtCreateStream, usado para programación multiproceso.

Explicación del desarrollo de muestra

Estructura de código de muestra

|-- CMakeLists.txt //Compilar archivos de proyecto 

|-- cmake //Compilar archivos de proyecto 

|-- data_utils.h //Funciones de lectura y escritura de datos 

|-- input //Almacena el directorio de datos de entrada generado por el script 

|-- Leakyrelu_custom.cpp //Implementación del kernel del operador 

|-- Leakyrelu_custom.py //Archivo de script de generación de datos reales de entrada y datos reales 

|-- Leakyrelu_custom_tiling.h //Función de ordenamiento en mosaico del lado del host 

|-- main.cpp //Función principal , código de llamada lateral del host, incluida la llamada al dominio de la CPU y al dominio npu 

|-- salida // El directorio que almacena los datos de salida de la operación del operador y los datos de referencia 

|-- readme.md // Descripción del comando de ejecución 

|-- run.sh // Ejecutar guión

documento principal

Archivo de script de generación de datos de entrada y datos reales: KERNEL_NAME.py.

Escriba scripts que generen datos de entrada y datos reales sobre el terreno en función de la entrada y salida del operador.

Este ejemplo genera datos fp16 de tamaño 8 * 200 * 1024:

...... 

def gen_golden_data_simple(): 

total_length_imm = 8 * 200 * 1024tile_num_imm 

= 8 

// Generar archivo de contenedor de labranza 

total_length = np.array(total_length_imm, dtype=np.uint32) 

Tile_num = np.array(tile_num_imm, dtype =np. uint32) 

escalar = np.array(0.1, dtype=np.float32) 

mosaico = (total_length, Tile_num, escalar) 

mosaico_data = b''.join(x.tobytes() para x en mosaico) 

con os.fdopen (os. open('./input/tiling.bin', WRITE_FILE_FLAGS, PEN_FILE_MODES_640), 'wb') as f: 

f.write(tiling_data) 

// Generar datos de entrada 

input_x = np.random.uniform(-100, 100 , [8, 200, 1024]).astype(np.float16) 

// Genera datos dorados, la función es la misma que LeakyRelu 

golden = np.where(input_x > 0, input_x, input_x * scalar).astype(np. flotador16)

input_x.tofile("./input/input_x.bin") 

golden.tofile("./output/golden.bin")

Compilar archivo de proyecto: CMakeLists.txt

Se utiliza para compilar el operador Ascend C que se ejecuta en el lado de la CPU o en el lado de la NPU. Preste atención principalmente a si todos los archivos fuente en CMakeLists.txt están enumerados.

La aplicación que llama al operador: main.cpp

Principalmente aplicación de memoria, copia de datos, lectura y escritura de archivos y otras operaciones, y finalmente llamar al operador, la introducción de la API relevante es la siguiente:

1. La interfaz de inicialización de AscendCL aclInit se usa para inicializar la interfaz de tiempo de ejecución AscendCL y es la primera interfaz llamada por el programa; aclrtCreateContext y aclrtCreateStream se usan para crear Context y Stream, principalmente para la gestión de recursos relacionados con subprocesos.

2.Interfaz aclrtMallocHost, utilizada para solicitar memoria en el Host:

aclError  aclrtMallocHost(void **hostPtr, size_t tamaño)

Esta función es similar a malloc en lenguaje C. Se utiliza para solicitar un determinado byte de memoria en el Host, donde hostPtr es un puntero a la memoria asignada y size es el tamaño de memoria solicitado. Si necesita liberar esta memoria , Utilice la versión de interfaz aclrtFreeHost, que corresponde a la función gratuita en lenguaje C.

3.Interfaz aclrtMalloc, utilizada para solicitar memoria en el dispositivo:

aclError  aclrtMalloc(void **devPtr, size_t size, política aclrtMemMallocPolicy )  

En comparación con la interfaz de la aplicación de memoria en el Host, existe un parámetro de política adicional, que se utiliza para establecer las reglas de asignación de memoria, generalmente configuradas en ACL_MEM_MALLOC_HUGE_FIRST. Después del uso, puede utilizar la interfaz aclrtFree correspondiente para liberar la memoria.

4.Interfaz aclrtMemcpy, utilizada para copiar datos entre el Host y el Dispositivo:

La memoria aplicada anteriormente distingue entre memoria del Host y memoria del Dispositivo, lo que implicará problemas de sincronización de datos. aclrtMemcpy es la interfaz utilizada para la comunicación de datos entre el Host y el Dispositivo:

aclError  aclrtMemcpy(void *dst, size_t destMax, const void *src, size_t count, aclrtMemcpyKind kind)

Donde src apunta a la fuente de datos y dst es la dirección de memoria de destino, destMax es la longitud máxima de memoria de la dirección de memoria de destino, count es el número de bytes copiados, donde aclrtMemcpyKind controla la dirección de copia: ACL_MEMCPY_HOST_TO_HOST, ACL_MEMCPY_HOST_TO_DEVICE, ACL_MEMCPY_DEVICE_TO_HOST y ACL_MEMCPY_DEVICE_TO_DEVICE, como ACL_ MEMCPY_HOST_TO_DEVICE es Los datos del Host se copian al Dispositivo.

5. La función principal es llamar a la función del kernel en el lado de la CPU.

ICPU_RUN_KF(leakyrelu_custom, blockDim, x, y, usrWorkSpace, mosaico);

y el lado de la NPU llama al

Leakyrelu_custom_do(blockDim, nullptr, stream, xDevice, yDevice, workspaceDevice, tilingDevice);

El código completo es el siguiente:

// Este archivo contiene el código de depuración de la CPU y el código npu. Leemos los datos del archivo bin y escribimos el resultado en el archivo. 

#include "data_utils.h" 

#include "leakyrelu_custom_tiling.h" 

#ifndef __CCE_KT_TEST__ 

#include "acl/acl.h" 

extern void Leakyrelu_custom_do(uint32_t coreDim, void* l2ctrl, void* stream, uint8_t* x, uint8_t* y, 

uint8_t * espacio de trabajo, uint8_t* mosaico); 

#else 

#include "tikicpulib.h" 

extern "C" __global__ __aicore__ void fugasyrelu_custom(GM_ADDR x, GM_ADDR y, espacio de trabajo GM_ADDR, mosaico GM_ADDR); 

#endif 

int32_t main(int32_t argc, char* argv[]) 

{ 

size_t tilingSize = sizeof(LeakyReluCustomTilingData); 

size_t usrWorkspaceSize = 4096;

size_t sysWorkspaceSize = 16 * 1024 * 1024; 

uint32_t blockDim = 8; 

#ifdef __CCE_KT_TEST__ //llamada lateral de la CPU 

//aplica memoria para almacenar el espacio de trabajo y cultivar datos 

uint8_t* usrWorkSpace = (uint8_t*)AscendC::GmAlloc(usrWorkspaceSize); 

uint8_ t * mosaico = (uint8_t*)AscendC::GmAlloc(tilingSize); 

ReadFile("./input/tiling.bin", tilingSize, tiling, tilingSize); 

size_t inputByteSize = blockDim * 200 * 1024 * sizeof(uint16_t); // uint16_t representa la mitad 

del tamaño_t outputByteSize = blockDim * 200 * 1024 * sizeof(uint16_t); // uint16_t representa la mitad 

// aplica memoria para almacenar datos de entrada y salida 

uint8_t* x = (uint8_t*)AscendC::GmAlloc(inputByteSize); 

uint8 _t * y = (uint8_t*)AscendC::GmAlloc(inputByteSize); 

//Obtener datos de entrada

ReadFile("./input/input_x.bin", inputByteSize, x, inputByteSize); 

// PrintData(x, 16, printDataType::HALF); 

//在AIV上执行

AscendC::SetKernelMode(KernelMode::AIV_MODE); 

//调用kernel函数

ICPU_RUN_KF(leakyrelu_custom, blockDim, x, y, usrWorkSpace, tiling); // usa esta macro para depurar la CPU 

// PrintData(y, 16, printDataType::HALF); 

WriteFile("./output/output_y.bin", y, outputByteSize); 

AscendC::GmFree((void *)x); 

AscendC::GmFree((void *)y); 

AscendC::GmFree((void *)usrWorkSpace); 

AscendC::GmFree((void *)mosaico); 

#else //NPU侧调用

CHECK_ACL(aclInit(nullptr)); 

contexto aclrtContext; 

int32_t ID del dispositivo = 0; 

CHECK_ACL(aclrtSetDevice(identificador de dispositivo));

CHECK_ACL(aclrtCreateContext(&context, ID de dispositivo)); 

aclrtStream flujo = nullptr; 

CHECK_ACL(aclrtCreateStream(&stream)); 

uint8_t *xHost, *yHost, *tilingHost, *workspaceHost; 

uint8_t *xDispositivo, *yDispositivo, *tilingDevice, *workspaceDevice; 

//申请host上tilling内存并读入tilling数据

CHECK_ACL(aclrtMallocHost((void**)(&tilingHost), tilingSize)); 

ReadFile("./input/tiling.bin", tilingSize, tilingHost, tilingSize); 

//申请host上workspace内存

CHECK_ACL(aclrtMallocHost((void**)(&workspaceHost), tilingSize)); 

size_t inputByteSize = blockDim * 200 * 1024 * sizeof(uint16_t); // uint16_t representa la mitad 

del tamaño_t outputByteSize = blockDim * 200 * 1024 * sizeof(uint16_t); // uint16_t representa la mitad

size_t workspaceByteSize = sysWorkspaceSize + usrWorkspaceSize; 

//申请host和dispositivo上的输入输出内存和dispositivo上的workspace和tilling内存

CHECK_ACL(aclrtMallocHost((void**)(&xHost), inputByteSize)); 

CHECK_ACL(aclrtMallocHost((void**)(&yHost), inputByteSize)); 

CHECK_ACL(aclrtMallocHost((void**)(&workspaceHost), workspaceByteSize)); 

CHECK_ACL(aclrtMalloc((void**)&xDevice, inputByteSize, ACL_MEM_MALLOC_HUGE_FIRST)); 

CHECK_ACL(aclrtMalloc((void**)&yDevice, inputByteSize, ACL_MEM_MALLOC_HUGE_FIRST)); 

CHECK_ACL(aclrtMalloc((void**)&tilingDevice, tilingSize, ACL_MEM_MALLOC_HUGE_FIRST)); 

CHECK_ACL(aclrtMalloc((void**)&workspaceDevice, workspaceByteSize, ACL_MEM_MALLOC_HUGE_FIRST));

ReadFile("./input/input_x.bin", inputByteSize, xHost, inputByteSize); 

// PrintData(xHost, 16, printDataType::HALF); 

//Copiar datos de entrada y datos de cultivo del host al dispositivo 

CHECK_ACL(aclrtMemcpy( xDevice , inputByteSize, xHost, inputByteSize, ACL_MEMCPY_HOST_TO_DEVICE)); 

CHECK_ACL(aclrtMemcpy(tilingDevice, tilingSize, tilingHost, tilingSize, ACL_MEMCPY_HOST_TO_DEVICE)); 

//llamar a la función del kernel 

fugasyrelu_custom_do(blockD im, nullptr, stream, xDevice, yDevice, workspaceDevice, tiling Dispositivo); 

//Esperar a que se complete la función del kernel 

CHECK_ACL(aclrtSynchronizeStream(stream)); 

//Copiar el resultado de la ejecución al host 

CHECK_ACL(aclrtMemcpy(yHost, outputByteSize, yDevice, outputByteSize, ACL_MEMCPY_DEVICE_TO_HOST)); 

// PrintData(yHost, 16, printDataType:: MITAD);

WriteFile("./output/output_y.bin", yHost, outputByteSize); 

//释放资源

CHECK_ACL(aclrtFree(xDevice)); 

CHECK_ACL(aclrtFree(yDispositivo)); 

CHECK_ACL(aclrtFree(dispositivo de espacio de trabajo)); 

CHECK_ACL(aclrtFree(tilingDevice)); 

CHECK_ACL(aclrtFreeHost(xHost)); 

CHECK_ACL(aclrtFreeHost(yHost)); 

CHECK_ACL(aclrtFreeHost(espacio de trabajoHost)); 

CHECK_ACL(aclrtFreeHost(tilingHost)); 

CHECK_ACL(aclrtDestroyStream(flujo)); 

CHECK_ACL(aclrtDestroyContext(contexto)); 

CHECK_ACL(aclrtResetDevice(identificador de dispositivo)); 

CHECK_ACL(aclFinalize()); 

#endif 

devuelve 0; 

}

Compile y ejecute el script run.sh con un solo clic

Compile y ejecute la aplicación.

Ejecute el comando en el lado de la CPU:

bash run.sh fugayrelu_custom ascend910B1 CPU VectorCore

Ejecute el comando en el lado de npu:

bash run.sh fugasyrelu_custom ascend910B1 VectorCore npu

El significado de los parámetros es el siguiente:

bash run.sh <nombre_kernel> <versión_soc> <tipo_núcleo> <modo_ejecución>

<kernel_name> indica el operador que se ejecutará.

<soc_version> indica el modelo del procesador de IA que ejecuta el operador.

<core_type> significa ejecutarse en AI Core o Vector Core, y el valor del parámetro es AiCore/VectorCore.

<run_mode> indica que el operador se ejecuta en modo cpu o npu y el valor del parámetro es cpu/npu.

Implementación del núcleo

definición del prototipo de función

En este ejemplo, el nombre de la función es Leakyrelu_custom, según el análisis de la entrada y salida del operador, se determina que existen dos parámetros xey, donde x es la memoria de entrada e y es la memoria de salida. La definición del prototipo de la función del núcleo es la siguiente:

externo "C" __global__ __aicore__ void fugasyrelu_custom(GM_ADDR x, GM_ADDR y, espacio de trabajo GM_ADDR, mosaico GM_ADDR){ }

Utilice el calificador de tipo de función __global__ para identificar que es una función del núcleo que puede ser llamada por <<<...>>>; utilice el calificador de tipo de función __aicore__ para identificar que la función del núcleo se ejecuta en el AI Core del lado del dispositivo Por conveniencia, la macro GM_ADDR se usa uniformemente para modificar los parámetros de entrada y la definición de la macro GM_ADDR:

#define GM_ADDR __gm__ uint8_t* __restrict__

Obtenga los datos de labranza y llame a las funciones Init y Process de la clase de operador.

La función Init de la clase de operador completa el trabajo relacionado con la inicialización de la memoria y la función Process completa la lógica central de la implementación del operador.

externo "C" __global__ __aicore__ void fugasyrelu_custom(GM_ADDR x, GM_ADDR y, espacio de trabajo GM_ADDR, mosaico GM_ADDR) 

{ 

GET_TILING_DATA(tilingData, mosaico); 

KernelLeakyRelu op; 

op.Init(x, y, tilingData.totalLength, tilingData.tileNum, tilingData.scalar); 

op.Proceso(); 

}

Encapsular la llamada de la función del kernel.

Después de la encapsulación, se obtiene la función Leakyrelu_custom_do, que es conveniente para que la llame el programa principal. #ifndef __CCE_KT_TEST__ indica que esta función contenedora solo se usa al compilar y ejecutar operadores en el lado de la NPU. Al compilar y ejecutar operadores en el lado de la CPU, puede llamar directamente a la función add_custom. Al llamar a la función del kernel, además de pasar los parámetros de entrada y salida x, y y el mosaico de los parámetros relacionados con la segmentación, también debe pasar blockDim (el número de núcleos ejecutados por la función del kernel), l2ctrl (parámetros reservados, establecido en nullptr), flujo (aplicación El flujo que mantiene la secuencia de ejecución de operaciones asincrónicas) para especificar la configuración de ejecución de la función del núcleo.

#ifndef __CCE_KT_TEST__ 

// llamada de la función del kernel 

void Leakyrelu_custom_do(uint32_t blockDim, void* l2ctrl, void* stream, uint8_t* x, uint8_t* y, 

uint8_t* workspace, uint8_t* tiling) 

{ 

Leakyrelu_custom<<<blockDim, l2ctrl, stream> >>(x, y, espacio de trabajo, mosaico); 

} 

#endif

Obtener parámetros de mosaico

Obtenga principalmente los parámetros de mosaico totalLength (longitud total), TileNum (número de divisiones, número de procesamiento de datos de ciclo de un solo núcleo) y escalar (escalar de cálculo de LeakyRelu) de tilingPointer.

#define GET_TILING_DATA(tilingData, tilingPointer) \ 

LeakyReluCustomTilingData tilingData; \ 

INIT_TILING_DATA(LeakyReluCustomTilingData, tilingDataPointer, tilingPointer); \ 

(tilingData).totalLength = tilingDataPointer->totalLength; \ 

(tilingData).tileNum = mosaicoDataPointer->tileNum; \ 

(tilingData).scalar = tilingDataPointer->escalar; 

#endif // LEAKYRELU_CUSTOM_TILING_H

función de inicio

Después de obtener principalmente los datos de mosaico, configure la dirección de gm en el núcleo único e inicialice el búfer.

__aicore__ inline void Init(GM_ADDR x, GM_ADDR y, uint32_t totalLength, uint32_ttileNum, float scalar) 

{ 

ASSERT(GetBlockNum() != 0 && "¡el bloque tenue no puede ser cero!"); 

this->blockLength = longitud total / GetBlockNum(); 

this->tileNum = TileNum; 

this->escalar = static_cast<half>(escalar); 

ASSERT(tileNum != 0 && "¡el número de mosaico no puede ser cero!"); 

this->tileLength = this->blockLength / TileNum / BUFFER_NUM; 

// obtener índice de inicio para el núcleo actual, núcleo paralelo 

xGm.SetGlobalBuffer((__gm__ half*)x + this->blockLength * get_block_idx(), this->blockLength); 

yGm.SetGlobalBuffer((__gm__ half*)y + this->blockLength * get_block_idx(), esto->longituddebloque); 

// canaliza la asignación de memoria a la cola, la unidad es Bytes

pipe.InitBuffer(inQueueX, BUFFER_NUM, this->tileLength * sizeof(half)); 

pipe.InitBuffer(outQueueY, BUFFER_NUM, this->tileLength * sizeof(half)); 

}

Función de proceso

Realiza principalmente tres etapas de CopyIn, Compute y CopyOut.

__aicore__ inline void Process() 

{ 

// el recuento de bucles debe duplicarse, debido al doble búfer 

int32_t loopCount = this->tileNum * BUFFER_NUM; 

// estrategia de mosaico, canalización paralela 

for (int32_t i = 0; i < loopCount; i++) { 

CopyIn(i); 

Calcular (i); 

Copiar(i); 

} 

}

Función copiar entrada

Responsable de copiar datos de la memoria global a la memoria local y agregar datos a la cola

__aicore__ inline void CopyIn(int32_t progreso) 

{ 

// asigna tensor de la memoria de la cola 

LocalTensor<half> xLocal = inQueueX.AllocTensor<half>(); 

// copiar el mosaico Progress_th del tensor global al tensor local 

DataCopy(xLocal, xGm[progress * TileLength], TileLength); 

// coloca los tensores de entrada en la cola VECIN 

inQueueX.EnQue(xLocal); 

}

función de cálculo

Responsable de tomar datos de la cola, realizar cálculos y poner los resultados en la cola.

__aicore__ inline void Compute(int32_t Progress) 

{ 

// deque tensores de entrada de la cola VECIN 

LocalTensor<half> xLocal = inQueueX.DeQue<half>(); 

LocalTensor<mitad> yLocal = outQueueY.AllocTensor<mitad>(); 

// llama a LeakyRelu instr para realizar el cálculo 

LeakyRelu(yLocal, xLocal, scalar,tileLength); 

// coloca el tensor de salida en la cola VECOUT 

outQueueY.EnQue<half>(yLocal); 

// tensores de entrada libres para reutilizar 

inQueueX.FreeTensor(xLocal); 

}

Función Copiar

Responsable de recuperar datos de la cola y copiar datos de la memoria local a la memoria global.

__aicore__ inline void CopyOut(int32_t progreso) 

{ 

// deque tensor de salida de la cola VECOUT 

LocalTensor<half> yLocal = outQueueY.DeQue<half>(); 

// copia el mosaico Progress_th del tensor local al tensor global 

DataCopy(yGm[progress * TileLength], yLocal, TileLength); 

// tensor de salida libre para reutilizar 

outQueueY.FreeTensor(yLocal); 

}

compilar y ejecutar

Ejecutar en el lado de la CPU

Los resultados de la ejecución son los siguientes:

cke_160.png

Se puede ver que el resultado de salida final output_y.bin tiene el mismo valor MD5 que los datos de referencia golden.bin, lo que indica que los resultados del cálculo son los mismos.

Una vez completada la ejecución, los datos de entrada y los datos de mosaico se almacenan en la entrada, los datos de salida y los datos de referencia se almacenan en la salida y el resultado de la ejecución npu_check de cada núcleo está en el directorio npuchk.

También hay un archivo binario ejecutable fugasyrelu_custom_cpu en el directorio actual. Si se informa un error durante la ejecución, puede depurar este archivo ejecutable a través de gdb. Para una depuración específica, consulte el tutorial oficial al final del artículo.

Ejecutar en el lado de la NPU

Hay dos formas de ejecutar en el lado de la NPU: ejecución de simulación y operación a bordo. Los comandos son los mismos, pero las opciones de compilación son diferentes. Podemos ejecutar la simulación CAModel para SIMULATOR modificando la opción de compilación -DASCEND_RUN_MODE. Configúrelo en ONBOARD para correr a bordo.

function compile_and_execute() { 

# Use cmake para compilar operadores del lado de la CPU o del lado de la npu, SIMULADOR o ONBOARD 

mkdir -p build; cd build; \ 

cmake .. \ 

-Dsmoke_testcase=$1 \ 

-DASCEND_PRODUCT_TYPE=$2 \ 

-DASCEND_CORE_TYPE=$3 \ 

-DASC END_RUN_MODE ="SIMULADOR" \ 

-DASCEND_INSTALL_PATH=$ASCEND_HOME_DIR 

VERBOSE=1 cmake --build. --target ${1}_${4} 

... 

}

Referencias

En resumen, para aprender Ascend C, solo necesita comprender la programación en C ++, comprender la comunicación en columnas y el mecanismo de liberación de la aplicación de memoria, y llamar a la interfaz informática y la interfaz de manejo correspondientes para escribir operadores de alto rendimiento que se ejecutan en el procesador Ascend AI.

Para obtener más recursos de aprendizaje de Ascend C, visite el tutorial oficial: Guía de programación de Ascend C (Tutorial oficial)

¡Extra!

cke_158278.jpeg

Huawei celebrará la octava HUAWEI CONNECT 2023 en el Shanghai World Expo Exhibition Hall y el Shanghai World Expo Center del 20 al 22 de septiembre de 2023. Con el tema "acelerar la inteligencia industrial", esta conferencia invita a líderes de opinión, élites empresariales, expertos técnicos, socios, desarrolladores y otros colegas de la industria a discutir cómo acelerar la inteligencia industrial desde los negocios, la industria y la ecología.

Le invitamos sinceramente a visitar el sitio, compartir las oportunidades y desafíos de la inteligenteización, discutir las medidas clave de la inteligenteización y experimentar la innovación y la aplicación de la tecnología inteligente. puede:

  • En más de 100 discursos de apertura, cumbres y foros, colisione con el punto de vista de acelerar la inteligencia industrial
  • Visite el área de exposición de 17.000 metros cuadrados para experimentar de cerca la innovación y la aplicación de la tecnología inteligente en la industria.
  • Reúnase cara a cara con expertos técnicos para conocer las últimas soluciones, herramientas de desarrollo y prácticas.
  • Buscar oportunidades de negocio con clientes y socios.

Gracias por su apoyo y confianza como siempre, y esperamos conocerlo en Shanghai.

Sitio web oficial de la conferencia: https://www.huawei.com/cn/events/huaweiconnect

Bienvenido a seguir la cuenta oficial de "Huawei Cloud Developer Alliance" para obtener la agenda de la conferencia, actividades interesantes y productos secos de vanguardia.

Haga clic para seguir y conocer las nuevas tecnologías de Huawei Cloud por primera vez ~

Anuncio oficial de Microsoft: Visual Studio para Mac retirado El lenguaje de programación creado por el equipo de desarrolladores chino: MoonBit (Moon Rabbit) Padre de LLVM: Mojo no amenazará a Python, el que debería tener miedo debería ser C++ El padre de C++ Bjarne Stroustrup compartió consejo de vida A Linus tampoco le gusta el acrónimo, se lanza lo que TM se llama "GenPD" Rust 1.72.0, y la versión mínima admitida en el futuro es Windows 10. Wenxin dijo que abrirá WordPress a toda la sociedad y lanzará el "100- plan anual" Microsoft no habla de artes marciales y utiliza "ventanas emergentes maliciosas" Invita a los usuarios a desaprobar los lenguajes de programación dinámicos, interpretados, funcionales y de alto nivel de Google: Crumb
{{o.nombre}}
{{m.nombre}}

Supongo que te gusta

Origin my.oschina.net/u/4526289/blog/10106749
Recomendado
Clasificación