Cómo añadir un paso de compilación personalizada con NSIGHT para CUDA
Problema común: he instalado NSIGHT y he creado un nuevo proyecto con un par de ficheros .cu que quiero compilar con CUDA pero no encuentro la manera de hacerlo.
La solución es fácil, lo que hay que hacer es indicarle que se debe compilar con un paso de compilación personalizada. El problema es que en la lista no aparece este paso si no lo activamos en el proyecto. Para eso hay que ir a Proyecto/Personalizaciones de compilación.
Ahí debemos marcar la casilla de la versión de CUDA que queramos activar, en mi caso CUDA 3.2 (.targets, .props). Siguiente paso es clic derecho en el fichero .cu y Propiedades. Ahí marcamos en General/Tipo de elemento la opción CUDA C/C++.
Con esto indicamos que ese fichero se compilará con CUDA 3.2, además ahora en las propiedades del proyecto aparecerá un desplegable CUDA C/C++ con algunas opciones avanzadas que es mejor no tocar.
Comunicación entre QThread con slot y signal
Es habitual crear un hilo QThread que haga un proceso en segundo plano y vaya avisando de su estado. En mi caso tengo un QTextEdit que me sirve de log para escribir cualquier cosa. El problema es que QT tiene mecanismos de seguridad que impiden a un QThread comunicarse con un QObject creado en otro hilo, como es la interfaz.
Para eso se aprovecha el sistema de signals y slots que permiten la comunicación.
Emisor: clase QThread
Hay que definir una signal en la clase emisora (que será un hilo QThread), el código queda así:
class FileAccess : public QObject, public QThread{
Q_OBJECT
public:
FileAccess(Logic *myLogic){}
~FileAccess(){}
void run(){
emit writeState("Running thread");
}
signals:
void writeState(QString q);
};
Como se puede apreciar, hemos creado nuestra propia señal de nombre writeState que recibe una QString y se emite nada más entrar en la ejecución del hilo.
Conexión con el receptor
Esta señal debe conectarse con el receptor, en mi caso un QTextEdit, para eso se utiliza el método connect():
fa = new FileAccess(); //nuevo hilo QObject::connect(fa, SIGNAL(writeState(QString)), myTextEdit, SLOT(append(QString)), Qt::QueuedConnection); fa->start(); //lanzar hilo fa->wait(); //esperar a que el hilo termine o hacer nuestras tareas
Destaco la segunda línea: conectamos la señal writeState del objeto fa con el slot append del objeto myTextEdit.
Evitar que se haga un salto de línea en Fortran
Desde tierras muy lejanas dejo aquí esta información sobre cómo evitar que un WRITE (VWRITE) haga un salto de línea y poder concatenar en una misma línea varios formatos de Fortran (Fortran Format).
Basta con poner un $ como último carácter del formato Fortran:
*CFOPEN,'elements','txt',' '
*VWRITE,e1(1,1), e2(1,1), e3(1,1), e4(1,1), e5(1,1),[...]
(19(' ',F10.0),$)
*VWRITE, e20(1,1)
(1(' ',F10.0),' ')
Empieza el proyecto BRUMA
Llega un momento en la vida de todo desarrollador en que se da cuenta de que todo lo que había hecho hasta ese momento no sirve para nada. El proyecto ha terminado, ¡viva el proyecto!
Esta vez probablemente no va a haber desarrollo en CUDA, por lo que dejaré de aprender esta tecnología. Aunque lo hecho hasta ahora me servirá para un futuro, no he llegado nunca a ahondar y profundizar en este tipo de programación tanto como me gustaría. Quién sabe si más adelante me serán útiles estos conocimientos, dejaremos pasar el tiempo a ver qué pasa.
Este nuevo proyecto, codename BRUMA (BReast Ultrasound and Magnetic resonance Alignment) va a permitir deformar una resonancia magnética según los datos que se reciben de una cámara TOF. La deformación de la resonancia magnética la podrá ver el médico en el momento de la operación y se le mostrará en la posición y orientación en la que maneje un ecógrafo.
BRUMA tiene tres grandes retos:
- Conseguir deformar una textura 3D de forma que los niveles de gris se interpolen correctamente y tener así una deformación de la resonancia acorde con la deformación a la que está sometida en realidad
- Captación del mapa de profunidad que viene de la cámara TOF y registro del mapa con la resonancia para su posterior deformación
- Tracking del ecógrafo y muestra de la resonancia en la posición y orientación del mismo
Y estas van a ser las tareas a las que me voy a dedicar ahora. Cambio y corto.
Crear una ventana de carga en un hilo y comunicarla con un Widget de QT
Desde hace tiempo tengo un botón que lanza un proceso externo (concretamente ANSYS) y normalmente tarda varios minutos. Esto provoca que la interfaz se congele hasta que el proceso termina, con la consiguiente falta de información, bloqueo de ventanas…
Crear un hilo con QT
Como parece que ahora la aplicación tiene que servir para otras personas, era hora de hacer eso bien y tener una pantallita típica de “Running…” con su botón de Abort y todo esto sin bloquear la interfaz, claro. Para eso tenemos que usar hilos y QT tiene sus propios hilos: QThread.
La creación de una clase que sea hilo simplemente debe heredar de QThread, aquí hay una muestra básica:
class MiHilo: public QThread{
private:
//mis variables
public:
void run(){ //método que se ejecutará al hacer start
//hacer cosas
}
};
int main(){
MiHilo th;
th.start(); //lanza el hilo
th.wait(); //espera a que el hilo termine
}
Aunque lo mejor es leerse la documentación de QThread para ver todos los métodos.
Mostrar una ventana de carga
Ahora lo interesante es tener una ventanita que indique que el hilo está haciendo cosas y que bloquee la ejecución del programa principal hasta que se cierre esa ventana, ya sea por acción del usuario o porque el proceso lanzado ha terminado. Para sacar una ventana modal (bloqueante) lo más sencillo es lo siguiente:
th.start();
loadingBox.setText("Process is running...");
loadingBox.setWindowTitle("Please wait");
loadingBox.setWindowModality(Qt::ApplicationModal);
loadingBox.setStandardButtons(QMessageBox::Abort);
loadingBox.setIcon(QMessageBox::Warning);
int r=loadingBox.exec();
th.wait();
Esta ventana tiene además un botón de Abort con el que podemos hacer lo que queramos al mirar el valor de la variable r. La ventana, al ser modal, no permite que el programa principal ejecute el wait() hasta que se haya cerrado, en caso contrario, el programa principal seguiría su ejecución de forma independiente.
Cerrar la ventana con un evento
Siguiente paso: que la ventana se cierre cuando el proceso termine. Aunque pueda parecer sencillo, es más complicado de lo que parece puesto que QT impide enviar eventos directamente entre hilos distintos. Por lo tanto, la clase MiHilo no podrá mandar un close() directamente a loadingBox. Según parece se puede montar un sistema con signals y slots que permitan mandar una señal de cierre, pero no lo he conseguido…
Lo que sí he conseguido es enviar un evento desde un hilo distinto al de la aplicación principal mediante postEvent. Esta función permite mandar un evento a la cola de eventos del objeto receptor definido, entonces, al finalizar la ejecución del programa que está lanzandose en el hilo, se pasa lo siguiente:
QApplication::postEvent(&padre->loadingBox,new QCloseEvent()); //padre es la clase que contiene el objeto loadingBox y que el hilo debe conocer
Y, aunque hay un new, el evento se borra automáticamente al procesarse, si lo borramos después con un delete tendremos errores difíciles de entender.
Todo esto nos ha permitido lanzar un proceso en un hilo, mostrar una ventana modal que interrumpa el programa principal pero sin bloquear la interfaz y cerrarla al terminar la ejecución del proceso.
Mezclando BOOST, uBLAS y CLAPACK para resolver sistemas de ecuaciones
Si bien anteriormente me entusiasmaron Newmat y MTL4, ninguna de ellas era capaz de resolver lo que necesitaba. La primera no resuelve sistemas y la segunda tardaba muchsímimo y hasta petaba en grandes sistemas (22k ecuaciones). Pero seguí investigando para utilizar las famosas librerías de resolución LAPACK o suery su versión para C: CLAPACK. Sin embargo, esta biblioteca no permite el almacenamiento eficiente de matrices dispersas/simétricas y para eso existe otra biblioteca llamada BLAS y, concretamente, la que viene ya incluida en BOOST: uBLAS.
Este post relata cómo, utilizando el almacenamiento de matrices de uBLAS y un binding sobre BOOST de CLAPACK y el propio CLAPACK, se resuelven sistemas de ecuaciones extremadamente grandes en poco tiempo.
Lo primero es tener instalado y preparado BOOST, con esto ya tenemos uBLAS a punto. Seguidamente podemos probar que esté correcto con un programita simple como este:
#include "stdafx.h"
#include <boost/numeric/ublas/matrix_sparse.hpp>
#include <boost/numeric/ublas/lu.hpp>
#include <boost/numeric/ublas/io.hpp>
int main(){
using namespace boost::numeric::ublas;
mapped_matrix<double> m (5, 5, 3 * 3);
for (unsigned i = 0; i < m.size1 (); ++ i)
for (unsigned j = 0; j < m.size2 (); ++ j)
m (i, j) = 5 * i + j*5.2;
m(3,3)=10;
std::cout << m << std::endl;
matrix<double> inv(5,5);
permutation_matrix<std::size_t> pm(m.size1());
lu_factorize(m,pm);
inv.assign(identity_matrix<double>(m.size1()));
lu_substitute(m,pm,inv);
std::cout << inv << std::endl;
system("PAUSE");
return 0;
}
Lo que hace es rellenar una matriz y luego invertirla. Es la forma más rápida de invertir una matriz utilizando uBLAS que he encontrado.
Matrices dispersas
Existen tres maneras de guardar matrices dispersas con uBLAS, yo he utilizado la compressed_matrix, me pareció la más eficiente. Aquí un ejemplo:
boost::numeric::ublas::compressed_matrix<float> miMatrizDispersa(ancho,alto);
El acceso para lectura y escritura es con miMatrizDispersa(i,j), todo muy fácil, rápido y sencillo.
Añadir el binding de LAPACK
Por sí misma, la biblioteca uBLAS no resuelve sistemas, podría utilizarse para invertir una matriz y multiplicarla por un vector, resolviendo así el sistema, pero esto es extremadamente ineficiente y es muy buena idea utilizar CLAPACK. Pero para ello tenemos que añadir el binding uBLAS-LAPACK. Esto es muy sencillo:
- Colocarse en el directorio donde tengamos BOOST, típicamente: C:/Archivos de programa/boost/boost_1_44/boost/numeric/
- Crear la carpeta bindings/
- Utilizar un cliente de SubVersion para bajar todo el árbol de:
svn co http://svn.boost.org/svn/boost/sandbox/numeric_bindings-v1/boost/numeric/bindings
- Colocarse en el directorio: C:/Archivos de programa/boost/boost_1_44/libs/numeric/
- Crear la carpeta bindings/
- Utilizar un cliente de SubVersion para bajar todo el árbol de:
svn co http://svn.boost.org/svn/boost/sandbox/numeric_bindings-v1/libs/numeric/bindings
Tendremos ya los bindings instalados y fácilmente comprobable. Pero antes…
Instalar y vincular CLAPACK
Debemos bajar CLAPACK para Visual Studio y colocarlo donde queramos, por ejemplo en C:/CLAPACK-3.1.1-VisualStudio. Importante bajarse las cosas ya compiladas a ser posible. Seguidamente añadimos en Visual Studio los directorios include y lib/Win32 como directorios adicionales de inclusión y biblioteca respectivamente.
Luego en el proyecto tenemos que añadir las bibliotecas adicionales: clapackd.lib y BLASd.lib en el vinculador.
Resolver un sistema de ecuaciones
Nos falta una cosa importante para que toda esta mezcla funcione, hay que añadir lo siguiente al principio del fichero donde se incluyan las cosas importantes:
#define BOOST_NUMERIC_BINDINGS_USE_CLAPACK //¡muy importante para que todo funcione! #include <boost/numeric/bindings/traits/ublas_matrix.hpp> #include <boost/numeric/bindings/lapack/gesv.hpp> #include <boost/numeric/ublas/matrix_sparse.hpp> #include <boost/numeric/ublas/vector.hpp> #include <boost/numeric/ublas/io.hpp>
Con este define y los includes que necesitemos ya podemos rellenar la matriz y resolver así:
ublas::matrix<float, ublas::column_major> A(ancho,alto); //importante el column_major para compatibilidad con LAPACK ublas::matrix<float, ublas::column_major> b(alto,1); //ponemos una matriz alto x 1 para hacerlo más sencillo, pero seguramente con un vector //rellenar A y b [...] bindings::lapack::gesv(A,b); //la solución se encontrará en b
Así hemos conseguido finalmente resolver el sistema Ax=b y sorprenderá la velocidad con que lo hace CLAPACK, el cuello de botella de aquí es la inserción de datos en una matriz dispersa.
Resolución de sistemas de ecuaciones con MTL4
Si bien hablé en otro post sobre la biblioteca Newmat, he seguido buscando alternativas pues Newmat no maneja matrices dispersas. He encontrado tres bibliotecas interesantes: SparseLib++, CSparse y MTL4.
De ellas decidí probar MTL4 para guardar una matriz de 1.400 millones de elementos y resolver un sistema también muy grande. He tenido bastantes problemillas al usarlo con Visual Studio 2005, pero al final parece que funciona.
Primeramente, MTL2 lo he descartado por obsoleto y he tirado directamente a MTL4, bajando todo el código del servidor subversion que proporcionan. Las instrucciones están muy bien detalladas y básicamente vienen a decir:
- Descarga e instala Boost C++
- Descarga MTL4 del subversion (con TortoiseSVN para Windows)
- Añadir los directorios de inclusión adicionales en Visual Studio:
- El de boost, típicamente: C:\Archivos de programa\boost\boost_1_44
- El de MTL4, típicamente: C:\mtl4
- Incluir las librerías necesarias cuando vayamos a usar MTL
#include <boost/numeric/mtl/mtl.hpp> #include <boost/numeric/itl/itl.hpp>
Y ahora dejo unas muestras de código para hacer matrices, invertirlas…
Matrices densas
dense2D<float> A(ancho, alto); A[0][4]=55; //ponemos a 55 el valor de la posición 0,4 float v=A[0][4]; //sacamos el valor 0,4 para guardarlo en v std::cout << "A is \n" << A; //imprimir una matriz es muy fácil
Matrices dispersas
Éstas son algo distintas en su manejo, no se pueden rellenar de la misma manera, necesitan un inserter.
compressed2D<double> B(ancho, alto);
if(true){
matrix::inserter<compressed2D<double>> ins(B); //crear un inserter sobre B
ins[0][4] << 55; //ponemos a 55 el valor de la posición 0,4
}
float v=B[0][4]; //la extracción es igual
std::cout << "B is \n" << B;
Nótese el if(true), eso lo he hecho porque la creación del inserter debe ser dentro de un bloque. Esto es así porque hasta que el inserter no se destruya, la matriz no podrá ser accesible y dará una excepción al intentar hacer B[0][4] o cualquier otro acceso. Se puede meter dentro de un bucle o función para que el inserter se destruya una vez hecho su trabajo.
Resolver un sistema
MTL sólo permite resolver sistemas con matrices densas, una pena. Se deben seguir estos pasos:
dense_vector<float> solucion(tam); dense2D<float> A(tam,tam); dense_vector<float> b(tam); solucion=lu_solve(A,b);
Obviamente, teniendo en cuenta que todos los tamaños permitan la resolución del sistema.
Newmat, buena biblioteca para el manejo de matrices en C++
Puede que no sea la mejor, pero he encontrado una gran biblioteca para el manejo de matrices en C++. Todo ha venido porque hasta ahora estaba usando matrices simples 2D y nos ha surgido el problema de que uno de los modelos FEM tenía tantos nodos que una de las matrices que teníamos que usar para simular una deformación llegaba a ocupar 8GB en memoria, lo cual, obviamente, mata la aplicación.
Me habían mencionado la biblioteca LAPACK, pero no he visto que permita almacenar matrices dispersas o simétricas (como es mi caso), aunque sus funciones sí que son capaces de manejar este tipo de matrices, asumo que ignorando aquellas posiciones simétricas o algo así.
He seguido buscando y me he topado con Newmat:
Esta biblioteca C++ está orientada a científicos e ingenieros que necesitan manipular una serie de tipos de matrices usando operaciones estándar. Se ha hecho énfasis en el tipo de operaciones necesarias y cálculos estadísticos como mínimos cuadrados, resolución de sistemas de ecuaciones lineales y eigenvalores.
Por lo que he bajado el código y he empezado a trastear con ello, ha compilado perfectamente en Visual Studio 2008. Crear un nuevo proyecto y añadir los .cpp y .h que se indican en estas instrucciones. Me ha sorprendido que todo funcionara a la primera, en estos casos siempre te falta tal o cual dependencia…
Además, se ejecuta un ejemplo de uso, lo que viene muy bien para comprobar el funcionamiento y ver algo de código siempre es la mejor manera de conocer una herramienta. Hasta me he atrevido a hacer una matriz simétrica e invertirla para ver el funcionamiento, no puede ser más fácil:
SymmetricMatrix SM(2);
SM.Row(1) << 11;
SM.Row(2) << -1 << 22;
SM=SM.i();
cout << "MI MATRIZ PREFERIDA ("<<SM.Storage()<<")\n\n"<<setw(10) << setprecision(5) <<SM<<endl;
El funcionamiento está claro, se crea una matriz simétrica de 2×2 y se rellena. Al ser simétrica, en la fila 1 basta con rellenar el primer valor. Además, con SM.Storage() se puede ver cuántas posiciones en memoria se han reservado, y lo hace optimizando la memoria puesto que para la matriz simétrica va a dar 3 y no 4. Si la imprimiéramos la matriz tendría este aspecto:
11 -1 -1 22
Al invertirla e imprimirla con el cout el resultado es:
0.09129 0.00415 0.00415 0.04564
¡Fácil y sencillo! Y ahora a darse una vuelta por la documentación y ver todo lo que esta biblioteca permite hacer.
NSIGHT para depurar CUDA en GPU
NVIDIA ha publicado (por fin) una herramienta que permitirá depurar CUDA en tiempo real. La depuración de CUDA sobre GPU es muy delicada y no es posible ver el estado de las variables, colocar puntos de ruptura o conocer cómo está funcionando el kernel lanzado.
Pero ahora NVIDIA lanza NSIGHT, una herramienta con la que podremos hacer todas estas tareas y además se integra con Visual Studio sin problemas. Sin embargo tiene algunas peculiaridades que he sufrido.
Sólo funciona con Visual Studio 2008 SP1, aunque dicen que pronto será compatible con VS2010, veremos si vale la pena este nuevo entorno, pero tiene muy buena pinta.
Es necesario (a menos que tengas una configuración de dos GPU en SLI), tener dos máquinas, una de ellas con VS2008 SP1 y NSIGHT instalado y otra de ellas con NSIGHT Monitor instalado y GPU G92 o superior, la segunda será la máquina donde se lanzarán las ejecuciones.
Ambas máquinas deben tener Windows Vista/7 (bye bye XP) y, por supuesto, estar conectadas entre sí. Es MUY RECOMENDABLE actualizar los controladores gráficos de NVIDIA a la última versión.
Estoy en estos momentos intentando lanzar mi primera depuración, en otro post haré un breve tutorial de instalación y manejo.
¿Por qué un unsigned char no sirve para un bucle de 256 iteraciones?
Necesito iterar sobre una matriz de 256x256x256 elementos, y para ello tengo 3 bucles anidados, de 0 a 256, lo típico para recorrerlo todo. Muchos pensareis, para los índices puede utilizarse un unsigned char ya que sus valores están entre 0 y 255, perfecto, llega justo: ¡pues no!
Y lo peor es que lo tenía en un bucle de un kernel CUDA y lo que hace es petar la gráfica y Windows, obligándote a reiniciar la máquina…
La explicación es sencilla, el bucle es tal que así:
for(unsigned char i=0; i<256; i++)
Por lo tanto la variable i empieza en 0, 1, 2… 255. Y ahí debería ser la última iteración, pero, y aquí viene lo interesante, cuando se haga i++ la variable sufre un overflow y vuelve a cero. Con lo que el bucle vuelve a empezar y nunca termina.
Así pues, niños, los unsigned char sirven únicamente para bucles de hasta 255 iteraciones, para todo lo demás: unsigned short.
