extensión del lenguaje C
La interfaz de programación de CUDA es un conjunto de extensiones del lenguaje C, la principal es la biblioteca Runtime, que se divide en tres componentes: componente host, componente de dispositivo y componente público.
Componente de host: se ejecuta en el host y proporciona funciones para controlar y acceder a uno o más dispositivos informáticos.
Componentes del dispositivo: los dispositivos ejecutan y proporcionan funciones específicas del dispositivo.
Componentes comunes: un subconjunto de la biblioteca estándar de C que proporciona tipos de vectores integrados y compatibilidad con codificaciones tanto de host como de dispositivo.
hacer una pregunta
- Definición de función y declaración de calificación del tipo de declaración, ¿cómo se llaman las funciones del host y el dispositivo (GPU)?
- ¿Cómo definir la ubicación de la memoria y el tamaño de la GPU?
- ¿Cómo se ejecuta una función del kernel en el dispositivo?
- ¿Cómo llamar a los indicadores de bloques e hilos de GPU?
- ¿Cómo compilar código CUDA?
declaración de calificación del tipo de función
__device__ // 在设备上执行,仅可从设备上调用
__global__ // 限定一个函数作为kernel的存在,在设备上执行,仅可从主机上调用
__host__ // 在主机上执行,仅可从主机上调用
/*
__host__ 可以与 __device__ 限定词的组合,支持主机和设备双向编译
*/
/*
限定:
__device__ 和 __global__ 不支持递归、内部不能声明静态变量、不能有自变量的一个变量数字
__device__ 不能拿到函数地址,另外,函数指向 __global__ 是支持的
__global__ 和 __host__ 不能一起使用
__global__ 函数必须要void的返回类型,调用到__global__函数必须指定它的执行配置
__global__ 函数的调用是同步的,意味着设备执行完成前返回
__global__函数参数目前是通过共享内存到设备的,并且被限制在256 个字节
*/
declaración de calificación de tipo variable
__device__
__device__
Un calificador declara una variable que reside en el dispositivo.
Como máximo, se define otro calificador de tipo en los siguientes tres elementos, que se
pueden __device__
usar juntos para especificar con más detalle a qué espacio de memoria pertenece la variable.
- residen en el espacio de memoria global
- tiene una vida útil de la aplicación
- Accesible desde todos los subprocesos dentro del grid y desde el host a través de la biblioteca en tiempo de ejecución
__constant__
__constant__
calificador, __device__
usado aleatoriamente, declara una variable
- residen en un espacio de memoria constante
- tiene una vida útil de la aplicación
- son accesibles desde todos los subprocesos dentro del grid y desde el host a través de la biblioteca en tiempo de ejecución
__shared__
__shared__
El calificador, __device__
usado con select, declara una variable:
- reside en el espacio de memoria compartida del bloque de subprocesos
- tiene una vida útil de un bloque
- Solo se puede acceder a todos los hilos dentro del bloque.
Las variables compartidas entre subprocesos tienen coherencia secuencial completa.
Solo __syncthreads()
se garantiza que la función en ejecución será visible en las escrituras de otros subprocesos
. A menos que la variable se defina como volátil, el compilador es libre de optimizar la memoria compartida tan pronto como se alcance el estado anterior de lectura y escritura. en
Al declarar una variable en la memoria compartida como una matriz externa, por ejemplo, extern_shared_float shared[];
el tamaño de la matriz se determina en el momento del envío. Todas las variables declaradas de esta manera comienzan en la misma dirección en la memoria,
por lo que la ubicación de las variables en la matriz debe offset
controlarse explícitamente mediante (desplazamiento).
Por ejemplo, si desea definir
short array0[128];
float array1[64];
int array2[256];
En la memoria compartida asignada dinámicamente, puede definir una matriz de la siguiente manera
extern __shared__ short array[];
__device__ void func() //__device__or__global__function
{
Short* array0 = (short*)array;
float* array1 = (float*)&array0[128];
int* array2 = (int*)&array1[64];
}
limitado
Estos calificadores no permiten que los miembros struct
y union
los parámetros formales y las variables locales de la función se ejecuten en el host.
__shared__
y __constant__
las variables implican almacenamiento estático
__device__,__shared__
y __constant__
las variables no se pueden extern
definir con la palabra clave para uso externo
__device__
y __constant__
las variables solo están permitidas en el alcance del archivo
__constant__
Las variables no se pueden asignar desde el dispositivo, solo se pueden asignar desde el host a través de la función de tiempo de ejecución del host
__shared__
Las variables no se pueden inicializar como parte de su declaración.
Un argumento declarado en el código del dispositivo sin ningún calificador generalmente reside en un registro.
Aunque en algunos casos el compilador puede optar por colocarlo en la memoria local.
Por lo general, en este caso, una estructura o matriz grande consumirá mucho espacio de registro y el compilador no puede estar seguro de que la matriz esté indexada con un número constante.
ptx
La inspección del código ensamblador (obtenido al compilar con - ptx
la -keep
opción o) indicará si la variable se colocó en la memoria local durante la primera fase de compilación, por lo que se declarará usando el .local
mnemónico o se accederá usando ld.local
el mnemónico yst.local
Si no es así, incluso si descubren que consume demasiado espacio de registro para la arquitectura de destino, las fases de compilación posteriores aún pueden tomar otras decisiones.
Se puede generar --ptxas-option=-v
un informe de uso de memoria local (lmem) usando .
Soporte para códigos de puntero ejecutados en el dispositivo, cuando el compilador puede resolver si apuntan al espacio de memoria global o al espacio de memoria local.
De lo contrario, estarán restringidos para que apunten únicamente a la memoria direccionada o al espacio declarado en la memoria global.
Libere un puntero al código en la memoria global o compartida que se ejecuta en el host, o al código de la memoria del host que se ejecuta en el dispositivo
El resultado es un comportamiento indefinido, que provoca un error de fragmento o la finalización de la aplicación.
La dirección solo se puede obtener a través de la variable __device__,__shared__
o en el código del dispositivo__constant__
donde __device__
o __constant__
la dirección de una variable sólo se puede cudaGetSymbolAddress()
obtener mediante
ejecutar configuración
Todas __global__
las llamadas a funciones deben especificar una configuración de ejecución.
Las configuraciones de ejecución definen las dimensiones de cuadrícula y bloque de las funciones que normalmente se ejecutan en el dispositivo, y de manera similar stream
.
<<< Dg, Db, Ns,S>>>
Se especifica insertando entre el nombre de la función y los argumentos encerrados entre paréntesis.
Dg
es el tipo dim3
y especifica la dimensión y el tamaño del ráster, de modo que Dg.x * Dg.y
sea igual al número de bloques que se envían;
Db
es tipo dim3
y especifica las dimensiones y tamaño de cada bloque, de manera que Db.x * Db.y * Db.z
sea igual al número de subprocesos por bloque;
Ns
es de tipo size_t y especifica el número de bytes en la memoria compartida que se asigna dinámicamente para cada bloque además de la memoria asignada estáticamente; esta memoria asignada dinámicamente es utilizada por cualquier variable declarada como una matriz externa, Ns es un parámetro opcional que se establece de forma predeterminada a 0.
S
es de tipo cudaStream_t y especifica la secuencia asociada; S
es un número opcional cuyo valor predeterminado es 0
Como ejemplo, la función se declara como
__global__ void Func(float* parameter);
debe llamarse así:
Func<<< Dg, Db, Ns >>>(parameter);
Los parámetros de función que realizan la configuración se evalúan antes de ser llamados y pasados al dispositivo a través de la memoria compartida.
Si Dg o Db es mayor que el máximo permitido por el dispositivo, o el valor de Ns es mayor que menos (memoria asignada estáticamente en la memoria compartida, parámetros de función y configuración de ejecución)), la función no se llamará
variables incorporadas
/*
gridDim
这个变量是类型dim3 (参见4.3.1.2 部分)并且包含栅格的维数。
blockIdx
这变量是类型uint3 (参见4.3.1.1 部分)并且包含栅格之内的块索引。
blockDim
这变量是类型dim3 (参见4.3.1.2 部分)并且包含在块的维数。
threadIdx
这变量是类型uint3 (参见4.3.1.1 部分)并且包含块之内的线程索引。
限定
内置变量不允许取得任何地址。
不允许赋值到任何内置变量。
*/
Compilación NVCC
nvcc
es la abreviatura de controlador de compilación para el proceso de compilación de código CUDA: proporciona opciones de línea de comandos simples y familiares y las ejecuta invocando herramientas que implementan una colección de diferentes fases de compilación.
nvcc
El flujo de trabajo básico consiste en separar el código del dispositivo del código del host y compilar el código del dispositivo en un formato binario u cubin
objeto.
Salida de código host generado, ya sea como código C enviado para compilación usando otras herramientas o como código objeto que invoca directamente al compilador host durante la etapa de compilación final.
Las aplicaciones pueden simplemente ignorar el código de host generado y usar CUDA
el objeto controlador API
cargado en el dispositivo cubin
o vincular el código de host generado.
El código incluye objetos como una matriz de datos inicializada globalmente cubin
y contiene un modificador que implementa la sintaxis de configuración e ingresa el CUDA Runtime
código de inicio necesario para cargar y enviar cada uno de los compilados Kernel
.
La sintaxis que el compilador procesa CUDA
en la parte frontal del archivo fuente cumple totalmente conC++
El código de host es totalmente compatible C++
. Pero los códigos de dispositivo sólo admiten un subconjunto C++
de C
;
No se admiten C++
funciones en bloques básicos , como: o declaraciones de variablesclasses, inheritance
Como resultado del uso C++
de la sintaxis, void
un puntero (por ejemplo, malloc()
devolviendo) typecast
no se puede asignar a non-void
un puntero sin usarlo.
A continuación se presentan las dos detecciones del compilador de NVCC.
__noinline__
De forma predeterminada, __device__
las funciones siempre están en línea.
__noinline__
La función se puede utilizar como sugerencia para una función no en línea.
La función en sí debe colocarse en el archivo de llamada y el compilador no puede garantizar que __noinline__
los calificadores de funciones con parámetros de puntero y funciones con listas de parámetros grandes funcionen correctamente.
#pragma unroll
De forma predeterminada, el compilador desenrolla pequeños bucles para recuentos de viajes conocidos.
#pragma unroll
Puede detectar y controlar cualquier bucle desenrollado.
Debe colocarse antes de este bucle y solo afecta a este bucle. Al mismo tiempo, puede especificar cuántas veces se puede expandir el bucle mediante un parámetro.
Por ejemplo:
#pragma unroll 5
For (int i = 0; i < n; ++i)
El bucle se desenrollará 5 veces. Asegúrese de que la acción de expansión no afecte la corrección del programa.
Si #pragma unroll
no hay ningún valor adjunto, el bucle se desenrolla completamente cuando el recuento de viajes es constante; de lo contrario, no se desenrollará
Componentes de tiempo de ejecución públicos
Los componentes comunes de Runtime pueden ser utilizados tanto por funciones del host como del dispositivo
内置矢量类型
char1, uchar1, char2, uchar2, char3, uchar3, char4, uchar4, short1, ushort1,
short2, ushort2, short3, ushort3, short4, ushort4, int1, uint1, int2, uint2, int3,
uint3, int4, uint4, long1, ulong1, long2, ulong2, long3, ulong3, long4, ulong4,
float1, float2, float3, float4
Estos tipos de vectores se derivan de los tipos básicos de punto flotante y entero.
Son estructuras y se puede acceder a los componentes 1.º, 2.º, 3.º y 4.º a través de los campos x, y, z,
y w
respectivamente
Todos toman una make_<type name>
función constructora del formato.
Ejemplo: crear un vector de tipo int2 make_int2(int x, int y);
por asignación(x, y)
int2
dim3 类型
Este tipo es uint3
un tipo de vector entero para especificar dimensiones basadas en
Al definir una dim3
variable de tipo, todos los componentes restantes no especificados se inicializan para1
数学函数
Table B-1
Las funciones matemáticas se pueden utilizar como funciones de host y dispositivo, y los límites de error de cada función se han probado intensamente, pero no se garantiza que sean absolutamente correctos.
La suma y la multiplicación cumplen con IEEE, por lo que tienen un error máximo de 0,5 ulp. Por lo general, se combinan en una instrucción de suma múltiple (FMAD)
Recomendamos usar rintf() en lugar de roundf() al calcular números de punto flotante a enteros.
Porque roundf() asigna 8 secuencias de instrucciones mientras que rintf() asigna solo una instrucción
truncf(), ceilf() y Floorf() también asignan solo una instrucción
La biblioteca de tiempo de ejecución CUDA también admite números enteros min() y max(), que también asignan una instrucción.
Table B-2
La función solo se puede utilizar como función del dispositivo.
Sus límites de error son específicos de la GPU.
Aunque estas funciones tienen menor precisión, son mucho más rápidas que algunas de las funciones de la Tabla B-1; tienen el mismo prefijo
__fadd_rz(x,y)
Calcule los parámetros de punto flotante xey con redondeo a cero y el parámetro de punto flotante xx__fmul_rz(x,y)
con redondeo a cerox 和 y y y producto
La división regular de punto flotante __fdividef(x,y)
tiene la misma precisión, pero para 2 126 < y < 2 128 2^{126} < y < 2^{128}2126<y<2128
__fdividef(x,y)
El resultado es cero y la división normal puede obtener el resultado correcto. De manera similar, para 2 126 < y < 2 128 2^{126}< y <2^{128}2126<y<2128 sixxx es infinito
__fdividef(x,y)
El resultado es NaN
(el resultado es infinito multiplicado por cero), y la división regular devuelve infinito
__[u]mul24(x,y)
Calcula el producto de los 24 bits menos significativos de los argumentos enteros xey y da los 32 bits menos significativos del resultado. xxx 和 y y Se ignoran los 8 bits más significativos de y .
__[u]mulhi(x,y)
Calcular el parámetro entero xxx 和 y y y y da los 32 bits más significativos del resultado de 64 bits
__[u]mul64hi(x,y)
Calcular el parámetro entero de 64 bits xxx和 y y y y da los 64 bits más significativos del resultado de 128 bits
__saturate(x)
si xxx es menor que 0, devuelve 0; sixxDevuelve 1 si x es mayor que 1; si x está entre [0, 1], devuelvexxX
__[u]sad(x,y,z)(Sum of Absolute Difference )
Encuentra el parámetro entero zzz y parámetro enteroxxx 和 y y la suma de los valores absolutos de las diferencias y
__clz(x)
Calcula el parámetro entero de 32 bits xxceros a la izquierda de x
__clzll(x)
Calcular el parámetro entero de 64 bits xxceros a la izquierda de x
__ffs(x)
Devuelve la posición del primer bit del parámetro entero x. La posición de bit menos significativa es 1 si x es 0
__ffs()
devuelve 0. Esto es lo mismo que la función ffs de Linux.
__ffsll(x)
Devuelve el parámetro entero de 64 bits xxLa posición del primer bit de x es 1. La posición de bit menos significativa es 1 si x es 0
__ffsll()
devuelve 0. Esto es lo mismo que la función ffsll de Linux.
时间函数
clock_t clock();
El valor de retorno del contador incrementa cada ciclo de reloj.
Muestre este contador al kernel
principio y al final y tome la diferencia de las dos muestras.
Y registra los resultados obtenidos al ejecutar el hilo completamente a través del dispositivo por ciclo de reloj por hilo.
en lugar del número de ciclos de reloj realmente gastados por el dispositivo que ejecuta las instrucciones del hilo
El primer número es mayor que el segundo porque los hilos se cortan en segmentos de tiempo.
纹理类型
CUDA
Admite un subconjunto de renderizado de texturas por hardware mediante GPU
el uso de memoria de texturas para gráficos
La lectura de datos a través de la memoria de textura tiene muchas ventajas de rendimiento sobre la memoria global
La memoria de textura se lee texture fetches
a través de una función del dispositivo llamadakernel
Texture fetch
El primer parámetro de especifica un texture referece
objeto llamado
Texture reference
Define qué parte de la memoria de textura esfetch
Antes de que pueda kernel
usarse, runtime
las funciones del host deben vincularlo a alguna región de memoria.
Algunos texture reference
pueden estar vinculados bajo la misma textura o en la memoria de un mapa de texturas.
Texture reference
tiene algunas propiedades
Uno de ellos es que puede especificar si la textura utiliza una matriz unidimensional direccionada a través de una coordenada de textura.
O especifique si la textura utiliza una matriz bidimensional direccionada por dos coordenadas de textura.
Los elementos de la matriz se denominan simplemente texels
(elementos de textura)
Otro atributo es fetch
definir los tipos de datos de entrada y salida para la textura.
Texture Reference 声明
Algunos texture reference
de los atributos son fijos, texture reference
se especifican al declarar el archivo . Una variable se declara texture reference
en el ámbito del archivo como un tipo.texture
Texrure<Type, Dim, ReadMode> texRef;
en
Type
El tipo de datos especificado se devuelve cuando se selecciona la textura.Dim
Especificatexture reference
la dimensión de , que es igual a 1 o 2;Dim
es un argumento opcional cuyo valor predeterminado es 1;ReadMode
igual acudaReadModeNormalizedFloat
ocudaReadModeElementType
; si escudaReadModeNormalizedFloat
yType
es un tipo entero16-bit
o8-bit
, el valor devuelto en realidad se tratará como un tipo de punto flotante, alunsigned
cual se asigna[0.0,1.0]
el tipo entero ; por ejemplo, un valor con un texto de 8 bits sin signo leído como 0xff ; si es así , no se realizará ninguna conversión; es un argumento opcional cuyo valor predeterminado essigned
[-1.0,1.0]
1
cudaReadModeElementType
ReadMode
cudaReadModeElementType
Runtime Texture Reference 属性
Algunas texture reference
de las propiedades no son fijas, pueden cambiarse mediante el tiempo de ejecución del host.
Pueden especificar si las coordenadas de textura son normalized
, el modo de direccionamiento y el filtrado de textura.
De forma predeterminada, las texturas están [0,N)
referenciadas mediante coordenadas de punto flotante, N
que son el tamaño de la textura en el espacio con respecto a las coordenadas.
Por ejemplo: una 64x32
textura de tamaño tiene un rango de coordenadas en el eje x [0,63]
y el eje y.[0,31]
Normalized
La textura [0.0,1.0)
está referenciada por coordenadas en lugar de[0,N)
Por lo tanto, la misma 64x32
textura apuntará al normalized
eje x [0.0,1.0)
y al eje y del[0.0,1.0)
Normalized
Las coordenadas de textura son inherentemente adecuadas para algunas aplicaciones, por ejemplo, las coordenadas de textura son independientes del tamaño de la textura.
El modo de direccionamiento define qué sucede cuando las coordenadas de textura se salen de los límites.
Cuando se utilizan unnormalized
coordenadas de textura, cuando las coordenadas de textura están fuera de rango [0, N)
, el valor menor que 0 se establece en 0 y el valor mayor que N se establece en N-1
Cuando se utilizan normalized
coordenadas de textura de , el rango de coordenadas de textura se limita a [0.0,1.0)
. Para las coordenadas de textura de , normalized
también se especifica warp
el direccionamiento
warp
El direccionamiento se usa generalmente cuando la textura contiene una señal periódica que solo actúa en partes fraccionarias de las coordenadas de la textura.
Por ejemplo, 1,25 se tratará como 0,25 y -1,25 se tratará como 0,75.
El filtrado de textura lineal solo se puede utilizar si la textura está configurada para devolver datos de punto flotante
texel
Realiza una interpolación de baja precisión en los texels adyacentes alrededor de los cuales se leerán las direcciones de recuperación de texturas.
y texel
devuelve el valor de recuperación de textura interpolado en función de las coordenadas de textura en las que
La interpolación simple se realiza en texturas 1D, bilinear
la interpolación se realiza en texturas 2D
线性内存纹理操作对比CUDA 数组
Una textura se puede asignar en memoria lineal o en una matriz CUDA, las texturas se asignan en memoria lineal:
- Sólo cuando la dimensión es 1;
- No se admite el filtrado de texturas;
- Sólo se puede utilizar direccionamiento de coordenadas de textura no normalizadas;
- No se pueden admitir diferentes modos de direccionamiento: el acceso a texturas fuera de rango devuelve 0;
Componente de tiempo de ejecución del dispositivo
Los componentes del dispositivo runtime
solo se pueden utilizar en funciones del dispositivo.
同步函数:void __syncthreads();
Sincroniza todos los hilos dentro de un bloque. Una vez que todos los subprocesos hayan llegado a este punto, reanude la ejecución normal.
__syncthreads()
Generalmente se usa para coordinar la comunicación de subprocesos entre el mismo bloque.
Cuando algunos subprocesos dentro de un bloque acceden a la misma memoria compartida o global
potencialmente peligroso para algunos accesos a la memoria read-after-write, write-after-read
, owrite-after-write
Estos peligros de datos se pueden evitar sincronizando el acceso entre subprocesos.
__syncthreads()
Se permite la colocación en código condicional, pero solo si todo el bloque de subprocesos tiene la misma condición en todo momento; de lo contrario, la ejecución del código puede bloquearse o causar efectos secundarios no deseados.
类型转换函数
El sufijo de las siguientes funciones especifica el modo de redondeo IEEE-754
rn
es encontrar el número par más cercano
rz
esta cerca de cero
ru
se redondea hacia arriba (al infinito positivo)
rd
se redondea hacia abajo (al infinito negativo)
int __float2int_[rn,rz,ru,rd](float);
Convierte un argumento de punto flotante en un número entero usando el modo de redondeo especificado
Unsignde int __float2unit_[rn,rz,ru,zd](float);
Convierte un argumento de punto flotante en un entero sin signo usando el modo de redondeo especificado
float __int2float_[rn,rz,ru,rd](int);
Convierte un argumento entero en un número de coma flotante usando el modo de redondeo especificado
float __int2float_[rn,rz,ru,rd](unsigned int);
Convierte un argumento entero sin signo en un número de coma flotante usando el modo de redondeo especificado
Type Casting 函数
float __int_as_float(int);
Realiza una conversión de tipo de un flotante en un argumento entero, dejando el valor sin cambios
Por ejemplo, __int_as_float(0xC0000000)
igual a-2
int __float_as_int(float);
Una conversión de tipo entero realizada en un argumento de punto flotante, dejando el valor sin cambios
Por ejemplo, __float_as_int (1.0f)
igual a0x3f800000
纹理函数
tex1Dfetch()
Operación de textura de la memoria del dispositivo: se accede a las texturas en la memoria del dispositivo a través de funciones
template<class Type>
Type tex1Dfetch(
texture<Type, 1, cudaReadModeElementType> texRef,
int x);
float tex1Dfetch(
texture<unsigned char, 1, cudaReadModeNormalizedFloat> texRef,
int x);
float tex1Dfetch(
texture<signed char, 1, cudaReadModeNormalizedFloat> texRef,
int x);
float tex1Dfetch(
texture<unsigned short, 1, cudaReadModeNormalizedFloat> texRef,
int x);
float tex1Dfetch(
texture<signed short, 1, cudaReadModeNormalizedFloat> texRef,
int x);
Estas funciones recuperan una región x
en la memoria lineal vinculada mediante coordenadas de texturatexture reference texRef
Para tipos de números enteros, no se permiten el filtrado de texturas ni los modos de direccionamiento seleccionados. Para estas funciones, puede ser necesario actualizar los números enteros a 32-bit
flotantes.
La siguiente función demuestra 2-
el 4-
soporte para tuplas.
float4 tex1Dfetch(
texture<uchar4, 1, cudaReadModeNormalizedFloat> texRef,
int x);
x
Selecciona texture reference texRef
la región en la memoria lineal vinculada por coordenadas de textura
- Operación de textura de matriz CUDA: acceso desde la textura en la matriz CUDA a través de la función tex1D() o tex2D()
template<class Type, enum cudaTextureReadMode readMode>
Type tex1D(texture<Type, 1, readMode> texRef, float x);
template<class Type, enum cudaTextureReadMode readMode>
Type tex2D(texture<Type, 2, readMode> texRef, float x, float y);
Estas funciones seleccionan la región CUDA
en la matriz unida por las coordenadas de textura xeytexture reference texRef
Texture reference
Los atributos de tiempo de compilación (fijo) y tiempo de ejecución (variable) determinan cómo se interpretan las coordenadas, qué procesamiento se producirá cuando se recupera la textura y el valor devuelto por la recuperación de la textura.
función atómica
Las funciones atómicas solo están disponibles en dispositivos con Compute Compatibility 1.1
/*
1. atomicAdd()
从全局内存中读取地址为address 的32-bit 字old,计算(old + val),将结果返回全
局内存中的同一地址。这三个操作由一个原子操作执行。函数返回old
*/
int atomicAdd(int* address, int val);
unsigned int atomicAdd(unsigned int* address, unsigned int val);
/*
2. atomicSub()
从全局内存中读取地址为address 的32-bit 字old,计算(old - val),将结果返回全
局内存中的同一地址。这三个操作由一个原子操作执行。函数返回old
*/
int atomicSub(int* address, int val);
unsigned int atomicSub(unsigned int* address, unsigned int val);
/*
3 atomicExch()
从全局内存中读取地址为address 的32-bit 字old,存储val 返回全局内存中的同一地址。
这二个操作由一个原子操作执行。函数返回old。
*/
int atomicExch(int* address, int val);
unsigned int atomicExch(unsigned int* address, unsigned int val);
float atomicExch(float* address, float val);
/*
4 atomicMin()
从全局内存中读取地址为address 的32-bit 字old,计算old 和val 的最小值,将结果返
回全局内存中的同一地址。这三个操作由一个原子操作执行。函数返回old。
*/
int atomicMin(int* address, int val);
unsigned int atomicMin(unsigned int* address,
unsigned int val);
/*
5 atomicMax()
从全局内存中读取地址为address 的32-bit 字old,计算old 和val 的最大值,将结果返
回全局内存中的同一地址。这三个操作由一个原子操作执行。函数返回old
*/
int atomicMax(int* address, int val);
unsigned int atomicMax(unsigned int* address, unsigned int val);
/*
6 atomicInc()
从全局内存中读取地址为address 的32-bit 字old,计算((old >= val) ? 0 :
(old+1)),将结果返回全局内存中的同一地址。这三个操作由一个原子操作执行。函数返回old
*/
unsigned int atomicInc(unsigned int* address, unsigned int val);
/*
7 atomicDec()
从全局内存中读取地址为address的32-bit 字old,计算(((old ==0)| (old > val)) ?
val : (old-1)), 将结果返回全局内存中的同一地址。这三个操作由一个原子操作执行。
函数返回old。
*/
unsigned int atomicDec(unsigned int* address, unsigned int val);
/*
8 atomicCAS()
从全局内存中读取地址为address 的32-bit 字old,计算(old == compare ? val :
old),将结果返回全局内存中的同一地址。这三个操作由一个原子操作执行。函数返回old
(比较和置换)
*/
int atomicCAS(int* address, int compare, int val);
unsigned int atomicCAS(unsignedint*address, unsigned int compare, unsigned int val);
/*
9. atomicAnd()
从全局内存中读取地址为address 的32-bit 字old,计算(old & val),将结果返回全
局内存中的同一地址。这三个操作由一个原子操作执行。函数返回old。
*/
int atomicAnd(int* address, int val);
unsigned int atomicAnd(unsigned int* address, unsigned int val);
/* 10.
atomicOr()
从全局内存中读取地址为address 的32-bit 字old,计算(old | val),将结果返回全
局内存中的同一地址。这三个操作由一个原子操作执行。函数返回old。
*/
int atomicOr(int* address, int val);
unsigned int atomicOr(unsigned int* address, unsigned int val);
/*
11 atomicXor()
从全局内存中读取地址为address 的32-bit 字old,计算(old ^ val),将结果返回全
局内存中的同一地址。这三个操作由一个原子操作执行。函数返回old
*/
int atomicXor(int* address, int val);
unsigned int atomicXor(unsigned int* address, unsigned int val);
32-bit
Una función atómica realiza una operación atómica de lectura, modificación y escritura en una palabra en la memoria global.
Por ejemplo, atomicAdd()
lea una palabra en la misma dirección en la memoria global 32-bit
, agréguele un número entero y escriba el resultado nuevamente en la misma dirección.
El llamado "atómico" es para garantizar que la operación no interfiera con otros subprocesos. Ningún otro hilo puede acceder a esta dirección hasta que se complete la operación.
Las operaciones atómicas sólo están disponibles 32-bit
en enteros con y sin signo.
Componentes de tiempo de ejecución del host
Los componentes del host Runtime
solo pueden ser utilizados por funciones del host
Proporciona funciones para manejar los siguientes problemas:
- gestión de dispositivos
- Gestión del contexto
- gestión de la memoria
- Gestión del módulo de codificación
- control ejecutivo
- Gestión de referencias de textura.
- Interoperabilidad de OpenGL y Direct3D
Se compone de dos API
:
Un controlador API
de llamadas de bajo nivelCUDA
API
API
Una llamada de alto nivel que se ejecuta por encima del CUDA runtime API
controlador CUDA
.API
API
Estos API
son mutuamente excluyentes: una aplicación debe elegir uno de ellos para utilizarlo.
CUDA runtime
context
Facilita la gestión de códigos de dispositivos al proporcionar inicialización, gestión y gestión de módulos inherentes.
Nvcc
El código de host generado C
se basa en CUDA runtime
, por lo que las aplicaciones que se conectan a este código deben usarCUDA runtime API
Por el contrario, CUDA
un controlador API
requiere más código, lo que dificulta la programación y la depuración, pero proporciona un mejor control y es
independiente del lenguaje ya que solo trata con cubin
objetos .
Especialmente con la configuración y el inicio CUDA
del controlador , es más difícil porque la configuración y los parámetros de ejecución deben especificar llamadas a funciones extrínsecas, reemplazando la sintaxis de configuración de ejecución.API
Kernel
kernel
Asimismo, la emulación de dispositivos no funciona con CUDA
controladores.API
CUDA
El controlador se proporciona API
a través de cuda
una biblioteca dinámica, todos sus puntos de entrada tienen el prefijocu
CUDA runtime API
Proporcionado a través de cudart
una biblioteca dinámica, todos sus puntos de entrada tienen el prefijocuda
concepto publico
equipo
Ambos API
proporcionan funciones para enumerar los dispositivos disponibles en el sistema, consultar sus atributos y seleccionar uno de ellos para ejecutar.kernel
Algunos subprocesos de host pueden ejecutar código de dispositivo en el mismo dispositivo, pero por diseño, un subproceso de host solo puede ejecutar código de dispositivo en un dispositivo.
Por lo tanto, los subprocesos de múltiples hosts deben ejecutar el código del dispositivo en múltiples dispositivos.
runtime
Además, cualquier archivo fuente creado en un subproceso del host a través de CUDA
no puede ser utilizado por otros subprocesos del host.
Memoria
La memoria del dispositivo se puede asignar en memoria lineal o como CUDA
una matriz
La memoria lineal del dispositivo utiliza 32-bit
espacios de direcciones, de modo que las entidades asignadas por separado puedan referirse entre sí mediante punteros.
Por ejemplo, en una estructura de árbol binario, CUDA
las matrices son diseños de memoria opaca optimizados para la recuperación de texturas. Están compuestos por elementos unidimensionales o bidimensionales, cada uno con 1, 2 o 4 componentes, cada uno de los cuales puede estar firmado o no firmado 8- bit, entero de 16 bits o 32 bits, punto flotante de 16 bits (solo compatible con el controlador CUDA
) o punto flotante de 32 bits.API
CUDA
Las matrices sólo kernel
se pueden leer a través de texturas.
Tanto la memoria lineal como CUDA
las matrices se pueden leer y escribir mediante la función de copia de memoria del host.
A diferencia de la memoria del host malloc()
asignada por funciones , el host también proporciona funciones que pueden asignar y liberar memoria del host.pageable
runtime
page-locked
Si la memoria del host se asigna comopage-locked
La ventaja de usar page-locked
memoria es que el ancho de banda entre la memoria del host y la memoria del dispositivo será muy alto.
Sin embargo, asignar demasiada page-locked
memoria reducirá la cantidad de memoria física disponible para el sistema, lo que reducirá el rendimiento general del sistema.
OpenGL Interoperability
Los objetos de búfer OpenGL se pueden asignar al espacio de direcciones CUDA, lo que permite a CUDA leer datos escritos por OpenGL.
O habilite CUDA para escribir datos consumidos por OpenGL
Direct3D Interoperability
Los buffers de vértices de Direct3D 9.0 se pueden asignar al espacio de direcciones CUDA, lo que permite a CUDA leer datos escritos por Direct3D.
O habilite CUDA para escribir datos consumidos por Direct3D
Un contexto CUDA solo puede usar un dispositivo Direct3D cada vez, al incluir la función de inicio/fin para llamar
El contexto CUDA y el dispositivo Direct3D deben estar integrados en la misma GPU
Esto se puede garantizar consultando el adaptador Direct3D asociado con el dispositivo CUDA.
Los dispositivos Direct3D deben crearse con el indicador D3DCREATE_HARDWARE_VERTEXPROCESSING
CUDA aún no admite:
- Versiones distintas a Direct3D 9.0
- Objetos Direct3D distintos de los buffers de vértices
- cudaD3D9GetDevice() y cuD3D9GetDevice() también pueden garantizar que el dispositivo Direct3D y el contexto CUDA se establezcan en diferentes dispositivos, como el equilibrio de carga de Direct3D y la sobreinteroperabilidad de CUDA.
Ejecución concurrente asincrónica
Para facilitar la ejecución simultánea entre el host y el dispositivo, algunas funciones de ejecución son asincrónicas: el control se devuelve a la aplicación hasta que el dispositivo haya completado la tarea solicitada.
- El kernel se inicia mediante la función __global__ o cuGridLaunch() y cuGridLaunchAsync()
- Las funciones que realizan copia de memoria requieren el sufijo Async
- Funciones que realizan una copia de memoria de dispositivo a dispositivo
- función para configurar la memoria
Algunos dispositivos también pueden realizar copias simultáneas entre la memoria del host con página bloqueada y la memoria del dispositivo.
La aplicación puede consultar si esta función está disponible a través de cuDeviceGetAttribute() junto con CU_DEVICE_ATTRIBUTE_GPU_OVERLAP
Actualmente, esta función solo admite la copia de memoria y se asigna mediante cudaMallocPitch() o cuMemAllocPitch(), excluyendo matrices CUDA o matrices 2D.
Las aplicaciones gestionan la simultaneidad a través de transmisiones. Una secuencia es una secuencia de operaciones ejecutadas en orden. Por otro lado, diferentes flujos pueden seguir ejecuciones paralelas o fuera de orden.
Una secuencia se define creando un objeto de secuencia y especificando sus parámetros de secuencia en la secuencia de inicio del kernel y la copia de memoria del host al dispositivo.
Solo si se han completado todas las operaciones: la operación incluida debe ser parte del flujo y ninguna operación posterior se completará hasta que se complete
Una secuencia con cero parámetros solo se inicia, por ejemplo: cualquier inicio del kernel, configuración de memoria o copia de memoria.
cudaStreamQuery() y cuStreamQuery() proporcionan un método para que la aplicación consulte si todas las operaciones en una secuencia se han completado
cudaStreamSynchronize() y cuStreamSynchronize() proporcionan un método para forzar a la aplicación a esperar hasta que se completen todas las operaciones en la secuencia.
Del mismo modo, las aplicaciones cudaThreadSynchronize() y cuThreadSynchronize() pueden forzar el tiempo de ejecución a esperar hasta que se completen todas las tareas del dispositivo.
Para evitar ralentizaciones, estas funciones se utilizan mejor con fines de sincronización, inicios aislados o fallas en la copia de memoria.
El tiempo de ejecución también proporciona un método para monitorear más de cerca el proceso del dispositivo.
En el momento preciso, permita que la aplicación registre eventos de forma asincrónica en cualquier punto del programa y consulte cuándo se registraron estos eventos.
Dos operaciones en diferentes flujos no se pueden ejecutar simultáneamente
Ya sea asignación de memoria de host bloqueada por página, asignación de memoria de dispositivo, configuración de memoria de dispositivo, copia de memoria de dispositivo a dispositivo o registro de eventos entre ellos
La ejecución asincrónica se puede desactivar globalmente estableciendo la variable de entorno CUDA_LAUNCH_BLOCKING en 1, para todas las aplicaciones CUDA en el sistema.
Esta función es sólo para fines de depuración, no para aumentar la confiabilidad de los productos de software.
API de tiempo de ejecución
[inicialización]
No existe una función de inicialización explícita para RuntimeAPI; se inicializa en la primera llamada a la función Runtime
Cabe señalar cuándo llamar a la función Runtime de manera oportuna y cuándo explicar el código de error que ingresó a Runtime desde la primera llamada.
[Gestión de dispositivos]
cudaGetDeviceCount()
y cudaGetDeviceProperties()
proporcionar un método para enumerar estos dispositivos y obtener sus propiedades
int deviceCount;
cudaGetDeviceCount(&deviceCount);
Int device;
for (device = 0; device < deviceCount; ++device) {
cudaDeviceProp deviceProp;
cudaGetDeviceProperties(&deviceProp,device);
}
cudaSetDevice()
Se utiliza para seleccionar el dispositivo asociado con el hilo del host.
cudaSetDevice(device);
Se debe seleccionar un dispositivo antes de que se llame a cualquier función __global__
De lo contrario, el dispositivo 0 se selecciona automáticamente y todas las selecciones de dispositivos posteriores no serán válidas.
[gestión de la memoria]
Llame a funciones para asignar y liberar memoria del dispositivo, acceder a la memoria asignada por variables declaradas arbitrariamente en la memoria global y transferir datos desde la memoria del host a la memoria del dispositivo.
La memoria lineal se asigna mediante cudaMalloc()
o y se libera cudaMallocPitch()
mediantecudaFree()
El siguiente código demuestra la asignación de una matriz de 256 elementos de punto flotante en la memoria lineal.
float* devPtr;
cudaMalloc((void**)&devPtr, 256 * sizeof(float));
Se recomienda asignar matrices 2D cudaMallocPitch()
para garantizar el mejor rendimiento para acceder a direcciones de fila o copiar matrices 2D a otras áreas de la memoria del dispositivo.
El valor devuelto pitch
debe usarse para acceder a los elementos de la matriz.
El siguiente código demuestra la asignación de una matriz 2D de ancho x alto con flotantes y cómo recorrer los elementos de la matriz en el código del dispositivo:
// host code
float* devPtr;
int pitch;
cudaMallocPitch((void**)&devPtr, &pitch,
width*sizeof(float),height);
myKernel<<<100,512>>>(devPtr,pitch);
// device code
__global__ void myKernel(float* devPtr, int pitch)
{
for (int r = 0; r < height; ++r) {
float* row = (float*)((char*)devPtr + r * pitch);
for (int c = 0; c < width; ++c) {
float element = row[c];
}
}
}
Las matrices CUDA se asignan mediante cudaMallocArray()
asignación, mediante cudaFreeArray()
desasignación
cudaMallocArray()
Se requiere una interpretación del formato, cudaCreateChannelDesc()
establecida por
El siguiente código demuestra la asignación de una matriz 2D de ancho x alto con un número de punto flotante de 32 bits:
cudaChannelFormatDescchannelDesc=
cudaCreateChannelDesc<float>();
cudaArray* cuArray;
cudaMallocArray(&cuArray, &channelDesc, width, height);
cudaGetSymbolAddress()
Se utiliza para obtener la dirección de memoria asignada a una variable declarada en la memoria global.
El tamaño de la memoria asignada se cudaGetSymbolSize()
obtiene con
cudaMalloc()
Memoria lineal asignada
cudaMallocPitch()
Memoria lineal asignada, matriz CUDA, memoria asignada de variable global o memoria residente
El siguiente código demuestra cómo copiar una matriz 2D a la matriz CUDA asignada en el ejemplo anterior:
cudaMemcpy2DToArray(cuArray, 0, 0, devPtr, pitch,
width * sizeof(float),height,
cudaMemcpyDeviceToDevice);
El siguiente código demuestra cómo copiar algunas matrices de memoria del host a la memoria del dispositivo:
float data[256];
int size = sizeof(data);
float* devPtr;
cudaMalloc((void**)&devPtr, size);
cudaMemcpy(devPtr, data, size, cudaMemcpyHostToDevice);
El siguiente código demuestra cómo copiar algunas matrices de memoria del host a la memoria residente:
__constant__ float constData[256];
float data[256];
cudaMemcpyToSymbol(constData, data, sizeof(data));
[Gestión de secuencias]
Llame a la función para crear y destruir secuencias y determinar si todas las operaciones en una secuencia se han completado
El siguiente código demuestra que se crean dos secuencias:
cudaStream_t stream[2];
for(inti=0;i<2;++i)
cudaStreamCreate(&stream[i]);
El siguiente código demuestra que cada flujo se define y ejecuta una vez: copia de memoria del host al dispositivo, kernel
inicio, copia de memoria del dispositivo al host.
for (int i = 0; i < 2; ++i)
cudaMemcpyAsync(inputDevPtr+i*size,hostPtr+i*size,size,cudaMemcpyHostToDevice,stream[i]);
for (int i = 0; i < 2; ++i)
myKernel<<<100, 512, 0, stream[i]>>>(outputDevPtr + i * size, inputDevPtr + i * size, size);
for (int i = 0; i < 2; ++i)
cudaMemcpyAsync(hostPtr + i * size, outputDevPtr + i * size, size, cudaMemcpyDeviceToHost, stream[i]);
cudaThreadSynchronize();
Cada flujo copia parte de la matriz de entrada hostPtr
a la matriz de entrada en la memoria del dispositivoinputDevPtr
myKernel()
Procesado en el dispositivo llamando inputDevPtr
y copiando el resultado outputDevPtr
en hostPtr
la misma sección de
El manejo con dos flujos hostPtr
permite superponer una copia de memoria de un flujo a otro. Para cualquier superposición, hostPtr
debe apuntar a page-locked
la memoria del host:
float*hostPtr;
cudaMallocHost((void**)&hostPtr,2*size;
cudaThreadSynchronize()
Se llama al final para garantizar que todas las transmisiones estén completas antes de continuar con el procesamiento.
[Gestión de eventos]
La función de llamada se utiliza para crear, registrar y destruir eventos y puede consultar el tiempo transcurrido entre dos eventos.
El siguiente código demuestra que se crean dos eventos:
cudaEvent_tstart,sto;
cudaEventCreate(&stat);
cudaEventCreate(&stop);
Estos eventos se pueden utilizar para cronometrar el código anterior:
cudaEventRecord(start, 0);
for (int i = 0; i < 2; ++i)
cudaMemcpyAsync(inputDev + i * size, inputHost + i * size, size, cudaMemcpyHostToDevice, stream[i]);
for (int i = 0; i < 2; ++i)
myKernel<<<100, 512, 0, stream[i]>>> (outputDev + i * size, inputDev + i * size, size);
for (int i = 0; i < 2; ++i)
cudaMemcpyAsync(outputHost + i * size, outputDev + i * size, size, cudaMemcpyDeviceToHost, stream[i]);
cudaEventRecord(stop, 0);
cudaEventSynchronize(stop);
float elapsedTime;
cudaEventElapsedTime(&elapsedTime, start, stop);
[Gestión de referencias de textura]
función de llamada para gestionartexture reference
Los tipos de textura son estructuras expuestas definidas por la API de alto nivel y texture Reference
los tipos están definidos por la API de bajo nivel, por ejemplo:
struct textureReference
{
int normalized;
enum cudaTextureFilterMode filterMode;
enum cudaTextureAddressMode addressMode[2];
struct cudaChannelFormatDesc channelDesc;
}
normalized
Especifica si las coordenadas de textura son normalized
;
Si es distinto de cero, todos los elementos de la textura tienen coordenadas de textura [0,1]
en lugar de [0,width-1]
o [0,height-1]
, donde width
y height
son el tamaño de la textura;
filterMode
Especifique el modo de filtro: según las coordenadas de textura de entrada, calcule cómo se devuelve el valor seleccionado por la textura;
modo de filtro es igual cudaFilterModePoint
a o cudaFilterModeLinear
;
Si es así cudaFilterModePoint
, el valor devuelto es igual al más cercano a las coordenadas de textura de entrada texel
;
Si es así , el valor devuelto es igual al resultado de la interpolación lineal de dos (para texturas 1D) o cuatro (para texturas 2D) de cudaFilterModeLinear
las coordenadas de textura de entrada más cercanas ;texel
texel
cudaFilterModeLinear
El valor de retorno debe ser de tipo punto flotante;
addressMode
Especifique el modo de direccionamiento: si controla coordenadas de textura fuera de rango, especifique el modo de direccionamiento de la primera coordenada de textura y el modo de direccionamiento de la segunda coordenada de textura respectivamente;
el modo de direccionamiento es igual a cudaAddressModeClamp
o cudaAddressModeWrap
;
En caso afirmativo cudaAddressModeClamp
, las coordenadas de textura fuera de los límites se sujetan a los límites legales;
En caso afirmativo cudaAddressModeWrap
, las coordenadas de textura que están fuera de los límites se sobrescriben según los límites legales;
cudaAddressModeWrap
Sólo normalized
se admiten coordenadas de textura;
channelDesc
Define el formato del valor de retorno al seleccionar la textura; consulte el siguiente código:
struct cudaChannelFormatDesc {
int x, y, z, w;
enum cudaChannelFormatKind f;
};
en
x,y,z
y w
son el número de bits en cada parte del valor de retorno
f
Sí:
cudaChannelFormatKindSigned
Si estas partes son números enteros con signo,cudaChannelFormatKindUnsigned
Si son enteros sin signo,cudaChannelFormatKindFloat
si son flotadores.
normalized,addressMode,
y filterMode
se puede modificar directamente en el código host.
Sólo se aplican a la matriz CUDA enlazadatexture reference
Antes de que un kernel texture reference
lea la memoria de textura a través de
texture reference
Debe estar vinculado a una textura usando cudaBindTexture()
o .cudaBindTextureToArray()
El siguiente código demuestra cómo vincular texture reference
a devPtr
la memoria lineal señalada por:
utilizar API de bajo nivel
texture<float, 1, cudaReadModeElementType> texRef;
textureReference* texRefPtr;
cudaGetTextureReference(&texRefPtr, “texRef”);
cudaChannelFormatDesc channelDesc = cudaCreateChannelDesc<float>();
cudaBindTexture(0, texRefPtr, devPtr, &channelDesc, size);
Utilice API de alto nivel
texture<float, 1, cudaReadModeElementType> texRef;
cudaBindTexture(0, texRef, devPtr, size);
El siguiente código demuestra cómo vincular a texture reference
a la matriz CUDA cuArray:
Utilice la API de bajo nivel:
texture<float, 2, cudaReadModeElementType> texRef;
textureReference* texRefPtr;
cudaGetTextureReference(&texRefPtr, “texRef”);
cudaChannelFormatDesc channelDesc;
cudaGetChannelDesc(&channelDesc, cuArray);
cudaBindTextureToArray(texRef, cuArray, &channelDesc);
Usando API de alto nivel:
texture<float, 2, cudaReadModeElementType> texRef;
cudaBindTextureToArray(texRef, cuArray);
El formato para vincular una textura a la referencia de textura debe coincidir con los parámetros al declarar la referencia de textura;
De lo contrario, el resultado de la búsqueda de texturas no estará definido.
cudaUnbindTexture()
Se utiliza para desinstalar texture reference
enlaces para .
[Interoperabilidad OpenGL]
Función de llamada para controlar la interoperabilidad de OpenGL
Un objeto de búfer debe registrarse con CUDA antes de que pueda asignarse
uso cudaGLRegisterBufferObject()
:
GLuint bufferObj;
cudaGLRegisterBufferObject(bufferObj);
Después del registro, el objeto del búfer se puede kenrel
leer o escribir utilizando la memoria del dispositivo y la dirección del dispositivo de memoria se cudaGLMapBufferObject()
devuelve mediante:
GLuint bufferObj;
float* devPtr;
cudaGLMapBufferObject((void**)&devPtr, bufferObj);
descargar el mapeo y registro vía, cudaGLUnmapBufferObject()
ycudaGLUnregisterBufferObject()
[Interoperabilidad Direct3D]
Función de llamada para controlar la interoperabilidad de Direct3D
Direct3D
La interoperabilidad debe cudaD3D9Begin()
iniciarse cudaD3D9End()
y finalizarse.
Un objeto de vértice debe registrarse con CUDA antes de poder mapearlo. uso cudaD3D9RegisterVertexBuffer()
:
LPDIRECT3DVERTEXBUFFER9 vertexBuffer;
cudaD3D9RegisterVertexBuffer(vertexBuffer);
Después del registro, el objeto del búfer se puede leer o escribir usando la memoria del dispositivo a través de kenrel, y la dirección del dispositivo de memoria se cudaD3D9MapVertexBuffer()
devuelve mediante:
LPDIRECT3DVERTEXBUFFER9 vertexBuffer;
float* devPtr;
cudaD3D9MapVertexBuffer((void**)&devPtr, vertexBuffer);
descargar el mapeo y registro vía, cudaD3D9UnmapVertexBuffer()
ycudaD3D9UnregisterVertexBuffer()
[Utilice la emulación de dispositivo para depurar]
El entorno de programación no admite ninguna depuración nativa del código que se ejecuta en el dispositivo, pero viene con un modo de emulación de dispositivo para fines de depuración.
Al compilar una aplicación en este modo (usando la deviceemu
opción -), el código del dispositivo se compila para
Permite a los desarrolladores depurar aplicaciones utilizando el soporte de depuración nativo del host.
Las macros del preprocesador __DEVICE_EMULATION__
se definen en este modo.
Todo el código de la aplicación actual, incluidas las bibliotecas utilizadas, debe compilarse, ya sea para la emulación o ejecución del dispositivo.
Los errores de tiempo de ejecución generados por la emulación del dispositivo conectado o el código ejecutado por el dispositivo pueden cudaErrorMixedDeviceExxcution
devolverse mediante .
Cuando se ejecuta una aplicación en modo de emulación de dispositivo, el tiempo de ejecución emula el modelo de programación.
Para cada hilo en el bloque de hilos, runtime
se genera un hilo en el host. Los desarrolladores deben determinar:
La cantidad máxima de subprocesos que el host puede ejecutar por bloque, más un subproceso principal
Hay suficiente memoria disponible para ejecutar todos los subprocesos, cada subproceso requiere 256 KB
una pila
Varias funciones disponibles a través del modo de emulación de dispositivo lo convierten en un conjunto de herramientas de depuración muy eficaz:
Al utilizar el soporte de depuración nativo del host, los desarrolladores pueden utilizar todas las funciones del depurador, como establecer puntos de interrupción e inspección de datos.
Dado que el código del dispositivo se compila para ejecutarse en el host, este código se puede complementar con código que no se puede ejecutar en el dispositivo.
Como printf()
operaciones de entrada y salida a archivos o pantallas (, etc.)
Dado que todos los datos residen en el host, cualquier dato específico del dispositivo o del host se puede leer desde el código del dispositivo o del host;
Del mismo modo, se puede llamar a cualquier dispositivo o función de host desde el dispositivo o el código de host.
Para evitar el uso incorrecto de la sincronización, runtime
se monitorean las condiciones de interbloqueo.
Los desarrolladores deben tener en cuenta que el modo de emulación de dispositivo imita al dispositivo, no lo emula.
Por lo tanto, el modo de emulación de dispositivo es muy útil para encontrar errores algorítmicos, pero algunos errores son difíciles de encontrar.
Cuando varios subprocesos dentro de la cuadrícula acceden a las celdas de memoria al mismo tiempo, los resultados cuando se ejecuta en modo de emulación de dispositivo son bastante diferentes de los de ejecución en el dispositivo, porque los subprocesos se ejecutan secuencialmente en modo de emulación.
Al eliminar la referencia a una referencia a la memoria global en el host o a la memoria del host en el dispositivo, es casi seguro que la ejecución del dispositivo fallará de alguna manera indefinida, mientras que la emulación del dispositivo puede producir resultados correctos.
En la mayoría de los casos, el mismo cálculo de punto flotante no producirá exactamente los mismos resultados cuando se realiza en el dispositivo que en el host a través del modo de emulación del dispositivo.
Por lo general, esto es de esperar: para el mismo cálculo de punto flotante, los diferentes resultados obtenidos se forman mediante diferentes opciones del compilador, sin mencionar diferentes compiladores, diferentes conjuntos de instrucciones o diferentes arquitecturas.
En particular, algunas plataformas host almacenan resultados intermedios de cálculos de punto flotante de precisión simple en registros de precisión extendida, lo que a menudo resulta en una gran diferencia en la precisión en el modo de emulación del dispositivo.
Cuando surgen estas situaciones, los desarrolladores pueden probar lo siguiente, ninguno de los cuales está totalmente garantizado que funcione:
- Declare algunas variables de punto flotante para forzar el almacenamiento de precisión simple debido a la inestabilidad;
- Utilice opciones
gcc
del compilador ;–ffloat-storegcc
- Utilice la opción o
Visual C++
del compilador ;/Op
/fp
- para
Linux
usar_FPU_GETCW()
y_FPU_SETCW()
paraWindows
usar_controlfp()
función para forzar una parte del código a realizar cálculos de punto flotante de precisión simple
unsigned int originalCW;
_FPU_GETCW(originalCW);
unsigned int cw = (originalCW & ~0x300) | 0x000;
_FPU_SETCW(cw);
unsigned int originalCW = _controlfp(0, 0);
_controlfp(_PC_24, _MCW_PC);
Al principio, almacene el valor actual de la palabra de control y fuerce el almacenamiento de la mantisa en 24 bits _FPU_SETCW(originalCW);
o_controlfp(originalCW, 0xfffff);
A diferencia de los dispositivos informáticos, las plataformas host también suelen admitir valores numéricos normalizados.
Esto puede generar resultados significativamente diferentes entre los modos de emulación y ejecución de dispositivo, ya que algunos cálculos pueden producir un resultado finito en un caso y un resultado infinito en el otro.
API del controlador
Los controladores API
se basan en identificadores y son imperativos API
: se hace referencia a la mayoría de los objetos a través de identificadores opacos.
Los objetos disponibles CUDA
se resumen en la Tabla 4-1.
[inicialización]
cuInit()
Se requiere la inicialización de la función antes de llamar a otras funciones .
[Gestión de dispositivos]
Llame a la función para administrar los dispositivos en el sistema actual.
cuDeviceGetCount()
y cuDeviceGet()
se utiliza para enumerar estos dispositivos
int deviceCount;
cuDeviceGetCount(&deviceCount);
int device;
for (int device = 0; device < deviceCount; ++device) {
CUdevice cuDevice;
cuDeviceGet(&cuDevice, device)
int major, minor;
cuDeviceComputeCapability(&major, &minor, cuDevice);
}
[Gestión del contexto]
Llamar a funciones para crear, agrupar y separarCUDA context
A CUDA context
es similar a un CPU
mango
Todos los recursos y acciones realizadas dentro de Compute API se encapsulan en CUDA context
y context
el sistema limpia automáticamente estos recursos cuando se destruyen.
context
Cada uno tiene su propio 32bit
espacio de direcciones independiente , excepto objetos como referencias de módulos y texturas.
Por lo tanto, los valores de diferentes CUDA context
se refieren CUdeviceptr
a diferentes ubicaciones de memoria.
Context
Hay un mecanismo de correspondencia uno a uno en el hilo del host.
Un hilo host solo puede tener un dispositivo a la vezcontext
context
Cuando se crea a cuCtxCreate()
, se convierte en el hilo del host que llama actualmente.
context
Las funciones que operan en CUDA
(la mayoría de las funciones excluyendo la enumeración o context
administración de dispositivos) regresarán CUDA_ERROR_INVALID_CONTEXT
si el hilo actual no es válidocontext
Para facilitar context
la interoperabilidad entre el código con licencia de terceros que se ejecuta en el mismo, el controlador proporciona un contador de uso API
proporcionado por cada cliente definido.context
Por ejemplo, si se cargan tres bibliotecas usando la misma CUDA context
, cada biblioteca debe llamar cuCtxAttach()
para incrementar el contador de uso, y cuando la biblioteca haya terminado context
de usar, llamar para cuCtxDetach()
disminuir el contador de uso.
context
Destruido cuando el contador de uso llega a 0
Para la mayoría de las bibliotecas, la aplicación debe crear una antes de cargar o inicializar la biblioteca CUDA context
;
De esta manera, la aplicación puede crear una biblioteca para su propio uso context
y la biblioteca simplemente opera context
en las tareas que se le han confiado.
[Gestión de módulos]
La función de llamada se utiliza para cargar y descargar el módulo, y obtener el identificador en el módulo, el puntero de la variable o la definición de la función.
Los módulos son paquetes tar que se pueden cargar dinámicamente y que contienen código y datos del dispositivo, como Windows
en , que se exportan DLL
a través denvcc
Los nombres de todos los indicadores, incluidas funciones, variables globales y referencias de textura, se proporcionan en el alcance del módulo para que los módulos escritos por terceros independientes en el CUDA context
mismo
El siguiente código demuestra cómo cargar un módulo y kernel
manejarlo:
CUmodule cuModule;
cuModuleLoad(&cuModule, "myModule.cubin");
CUfunction cuFunction;
cuModuleGetFunction(&cuFunction, cuModule, “myKernel”);
[control de ejecución]
kernel
Función de llamada para gestionar la ejecución de un en el dispositivo.
cuFuncSetBlockShape()
Se utiliza para establecer el número de subprocesos en cada bloque de una función determinada y cómo se asignan los ID de los subprocesos.
cuFuncSetSharedSize()
Especifica el tamaño de la memoria compartida de la función.
cuParam*()
El conjunto de funciones proporciona parámetros para que el kernel kernel
incluya culanuchGrid()
ocuLanuch()
cuFuncSetBlockShape(cuFunction, blockWidth, blockHeight, 1);
int offset = 0; int i;
cuParamSeti(cuFunction, offset, i);
Offset += sizeof(i);
float f;
cuParamSetf(cuFunction, offset, f);
offset += sizeof(f);
char data[32];
cuParamSetv(cuFunction, offset, (void*)data, sizeof(data));
offset += sizeof(data);
cuParamSetSize(cuFunction, offset);
cuFuncSetSharedSize(cuFunction, numElements * sizeof(float));
cuLaunchGrid(cuFuntion, gridWidth, gridHeight);
[gestión de la memoria]
Se llaman funciones para asignar y liberar memoria del dispositivo y para transferir datos entre la memoria del host y del dispositivo.
La memoria lineal es asignada por cuMemAlloc()
o para liberar.cuMemAllocPitch()
cuMemFree()
El siguiente código demuestra la asignación de una matriz de 256 elementos de punto flotante en la memoria lineal:
CUdeviceptr devPtr;
cuMemAlloc(&devPtr, 256 * sizeof(float));
Se recomienda asignar 2D
matrices cuMemMallocPitch()
para garantizar 2D
el mejor rendimiento al acceder a direcciones de fila o copiar matrices a otras áreas de la memoria del dispositivo.
El valor devuelto pitch
debe usarse para acceder a los elementos de la matriz.
El siguiente código demuestra cómo asignar una matriz 2D de ancho x alto con flotantes y cómo recorrer los elementos de la matriz en el código del dispositivo:
// host code
CUdeviceptr devPtr;
int pitch;
cuMemAllocPitch(&devPtr, &pitch, width * sizeof(float), height, 4);
cuModuleGetFunction(&cuFunction,cuModule,"myKernel");
cuFuncSetBlockShape(cuFunction, 512, 1, 1);
cuParamSeti(cuFunction, 0, devPtr);
cuParamSetSize(cuFunction, sizeof(devPtr));
cuLaunchGrid(cuFunction, 100, 1);
// device code
__global__ void myKernel(float* devPtr)
{
for (int r = 0; r < height; ++r) {
float* row = (float*)((char*)devPtr + r * pitch);
for (int c = 0; c < width; ++c) {
float element = row[c];
}
}
}
CUDA
Las matrices son cuArrayCreate()
asignadas por, cuArrayDestroy()
liberadas por
El siguiente código demuestra la asignación de una matriz de ancho x alto con un 32-bit
flotante CUDA
:
CUDA_ARRAY_DESCRIPTOR desc;
desc.Format = CU_AD_T_FLOAT;
desc.NumChannels = 1;
desc.Width = width;
desc.Height = height;
CUarray cuArray;
cuArrayCreate(&cuArray, &desc);
El siguiente código demuestra cómo copiar una matriz 2D a la matriz CUDA asignada en el ejemplo anterior:
CUDA_MEMCPY2D copyParam;
memset(©Param, 0, sizeof(copyParam));
copyParam.dstMemoryType = CU_MEMORYTYPE_ARRAY;
copyParam.dstArray = cuArray;
copyParam.srcMemoryType = CU_MEMORYTYPE_DEVICE;
copyParam.srcDevice = devPtr;
copyParam.srcPitch = pitch;
copyParam.WidthInBytes = width * sizeof(float);
copyParam.Height = height;
cuMemcpy2D(©Param);
El siguiente código demuestra cómo copiar algunas matrices de memoria del host a la memoria del dispositivo:
float data[256];
int size = sizeof(data);
CUdeviceptr devPtr;
cuMemMalloc(&devPtr, size);
cuMemcpyHtoD(devPtr, data, size);
[Gestión de flujo]
La función de llamada se utiliza para crear y destruir secuencias y para determinar si todas las operaciones en una secuencia están completas.
El siguiente código demuestra que se crean dos secuencias:
CUStream stream[2];
for (int i = 0; i < 2; ++i)
cuStreamCreate(&stream[i], 0);
El siguiente código demuestra que cada flujo se define y ejecuta una vez: copia de memoria del host al dispositivo, inicio del kernel, copia de memoria del dispositivo al host
for (int i = 0; i < 2; ++i)
cuMemcpyHtoDAsync(inputDevPtr + i * size, hostPtr + i * size, size, stream[i]);
for (int i = 0; i < 2; ++i) {
cuFuncSetBlockShape(cuFunction, 512, 1, 1);
int offset = 0;
cuParamSeti(cuFunction, offset, outputDevPtr);
offset += sizeof(int);
cuParamSeti(cuFunction, offset, inputDevPtr);
offset += sizeof(int);
cuParamSeti(cuFunction, offset, size);
offset += sizeof(int);
cuParamSetSize(cuFunction, offset);
}
cuLaunchGridAsync(cuFunction, 100, 1, stream[i]
for (int i = 0; i < 2; ++i)
cuMemcpyDtoHAsync(hostPtr + i * size, outputDevPtr + i * size,
size, stream[i]);
cuCtxSynchronize();
Cada flujo copia parte de la matriz de entrada hostPtr
a la matriz de entrada en la memoria del dispositivo , se procesa en el dispositivo inputDevPtr
llamando y copia el resultado en la misma parte de .cuFunction
inputDevPtr
outputDevPtr
hostPtr
El manejo con dos secuencias hostPtr
permite sobrescribir las copias de memoria de una secuencia a la otra.
Para cualquier anulación, hostPtr
debe apuntar a page-locked
la memoria del host en:
float* hostPtr;
cuMemMallocHost((void**)&hostPtr, 2 * size);
cuCtxSynchronize()
Se llama al final para garantizar que todas las transmisiones estén completas antes de continuar con el procesamiento.
[Gestión de eventos]
La función de llamada se utiliza para crear, registrar y destruir eventos y puede consultar el tiempo transcurrido entre dos eventos.
El siguiente código demuestra que se crean dos eventos.
CUEvent start,
stop;cuEventCreate(&start);
cuEventCreate(&stop);
Estos eventos se pueden utilizar para cronometrar el código anterior:
cuEventRecord(start, 0);
for (int i = 0; i < 2; ++i)
cuMemcpyHtoDAsync(inputDevPtr + i * size, hostPtr + i * size, size, stream[i]);
for (int i = 0; i < 2; ++i) {
cuFuncSetBlockShape(cuFunction, 512, 1, 1);
int offset = 0;
cuParamSeti(cuFunction, offset, outputDevPtr);
offset += sizeof(int);
cuParamSeti(cuFunction, offset, inputDevPtr);
offset += sizeof(int);
cuParamSeti(cuFunction, offset, size);
offset += sizeof(int);
cuParamSetSize(cuFunction, offset);
}
cuLaunchGridAsync(cuFunction, 100, 1, stream[i]
for (int i = 0; i < 2; ++i)
cuMemcpyDtoHAsync(hostPtr + i * size, outputDevPtr + i * size, size, stream[i]);
cuEventRecord(stop, 0);
cuEventSynchronize(stop);
float elapsedTime;
cuEventElapsedTime(&elapsedTime, start, stop);
[Gestión de referencias de textura]
función de llamada para gestionartexture reference
Antes de que un pase pueda leer una memoria kernel
de textura texture reference
, texture reference
debe estar vinculada a una textura cuTexRefSetAddress()
o cuTexRefSetArray()
usarse en una textura.
Si un módulo cuModule
contiene algunos texture reference texRef
definidos comotexture<float, 2, cudaReadModeElementType> texRef;
El siguiente código demuestra que texRef
se obtiene un identificador para:
CUtexref cuTexRef;
cuModuleGetTexRef(&cuTexRef, cuModule, “texRef”);
El siguiente código demuestra cómo vincular texture reference
a devPtr
la memoria lineal señalada por:
cuTexRefSetAddress(Null, cuTexRef, devPtr, size);
El siguiente código demuestra cómo vincular a texture reference
a la matriz CUDA cuArray:
cuTexRefSetArray(cuTexRef, cuArrary, CU_TRSA_OVERRIDE_FORMAT);
Configure texture reference
el modo de direccionamiento, el modo de filtro, el formato y otros indicadores para
El formato al que se vincula una textura texture reference
debe coincidir con texture reference
los parámetros al declarar;
De lo contrario, el resultado de la búsqueda de textura no estará definido.
[Interoperabilidad OpenGL]
Función de llamada para controlar OpenGL
la interoperabilidad de
OpenGL
La interoperabilidad debe ser cuGLInit()
inicializada por
Un objeto de búfer debe registrarse con elCUDA
uso cuGLRegisterBufferObject()
:
GLuint bufferObj;
cuGLRegisterBufferObject(bufferObj);
Después del registro, el objeto del búfer se puede leer o escribir usando la memoria del dispositivo a través de kenrel, y la dirección del dispositivo de memoria se cuGLMapBufferObject()
devuelve mediante:
GLuint bufferObj;
CUdeviceptr devPtr;
Int size;
cuGLMapBufferObject(&devPtr, &size, bufferObj);
Desinstale el mapa y regístrese a través de,cuGLUnmapBufferObject()和cuGLUnregisterBufferObject()
[Interoperabilidad Direct3D]
Función de llamada para controlar Direct3D
la interoperabilidad de
Direct3D
La interoperabilidad debe ser cuD3D9Begin()
inicializada y cuD3D9End()
finalizada por
Un objeto de vértice debe registrarse con CUDA
elcuD3D9RegisterVertexBuffer()
LPDIRECT3DVERTEXBUFFER9 vertexBuffer;
cuD3D9RegisterVertexBuffer(vertexBuffer);
Después del registro, el objeto del búfer se puede leer o escribir usando la memoria del dispositivo a través de kenrel, y la dirección del dispositivo de memoria se cuD3D9MapVertexBuffer()
devuelve mediante:
LPDIRECT3DVERTEXBUFFER9 vertexBuffer;
CUdeviceptr devPtr;
Int size;
cuD3D9MapVertexBuffer(&devPtr, &size, vertexBuffer);
Desinstale el mapa y regístrese a través de,cuD3D9UnmapVertexBuffer()和cuD3D9UnregisterVertexBuffer()