Las cargas de trabajo que procesan grandes cantidades de datos, especialmente las que se ejecutan en la nube, a menudo utilizan un servicio de almacenamiento de objetos (S3, Google Cloud Storage, Azure Blob Storage, etc.) como fuente de datos.
Los servicios de almacenamiento de objetos pueden almacenar y servir grandes cantidades de datos, pero obtener el mejor rendimiento puede requerir adaptar su carga de trabajo a cómo se comportan los almacenes de objetos remotos. Esta publicación es para usuarios de RAPIDS que desean leer o escribir datos en el almacenamiento de objetos lo más rápido posible para que IO no bloquee su carga de trabajo.
Parte de su conocimiento sobre cómo se comportan los sistemas de archivos locales se traduce en almacenes de objetos remotos, pero son fundamentalmente diferentes. Probablemente la mayor diferencia entre los dos, al menos para las cargas de trabajo de análisis de datos, es que las operaciones de lectura y escritura en el almacenamiento de objetos tienen latencia más alta y más variable. Cada servicio de almacenamiento tiene su propio conjunto de mejores prácticas y pautas de rendimiento (AWS, Azure). Aquí, weizll dar algunas pautas generales que se centran en las cargas de trabajo de análisis de datos.
Ubicación
Colocar sus nodos de cómputo cerca del servicio de almacenamiento (idealmente, en la misma región de la nube) le dará la red más rápida y confiable entre las máquinas que ejecutan su carga de trabajo y las máquinas que sirven los datos. Y, al final del día, la transferencia estará limitada por la velocidad de la luz, por lo que minimizar la distancia física no duele.
Formato de archivo
“Los formatos de archivo Cloud-native” se han desarrollado para funcionar bien con el almacenamiento de objetos. Estos formatos de archivo generalmente proporcionan un acceso rápido y fácil a los metadatos (que incluye información de alto nivel como los nombres de columna o los tipos de datos, e información de nivel inferior como dónde se encuentran los subconjuntos de datos específicos del archivo). Parquet Apache, Zarr, y GeoTIFF Optimizado en la Nube son algunos ejemplos de formatos de archivo nativos de la nube para varios tipos de datos.

Porque los servicios de almacenamiento de objetos generalmente admiten solicitudes de rango, clientes (como cuDF) puede leer los metadatos y luego descargar solo los datos que realmente necesitas. Por ejemplo, cuDF puede leer solo unas pocas columnas de un archivo de parquet con muchas columnas, o un cliente de Zarr puede leer un solo fragmento de una gran matriz n-dimensional. Estas lecturas se realizan en solo unas pocas solicitudes HTTP, y sin necesidad de descargar un montón de datos extraños que simplemente se filtran.
Tamaño del archivo
Debido a que cada operación de lectura requiere (al menos) una petición HTTP, weedd prefiere amortizar la sobrecarga de cada petición HTTP sobre un número razonablemente grande de bytes. Si controla el proceso de escritura de datos, querrá asegurarse de que los archivos sean lo suficientemente grandes como para que sus tareas de procesamiento posteriores obtengan un buen rendimiento. El valor óptimo depende de su carga de trabajo, pero en algún lugar de las docenas a cientos de MB es común para los archivos de parquet (ver más abajo para algunos ejemplos específicos).
Dicho esto, deberá tener cuidado con la forma en que el tamaño del archivo interactúa con la siguiente herramienta de nuestro kit: concurrencia.
Concurrencia
El uso de concurrencia para descargar múltiples blobs (o múltiples piezas de un solo blob) al mismo tiempo es esencial para obtener un buen rendimiento de un servicio de almacenamiento remoto. Ya que es un remoto servicio, su proceso va a pasar algún tiempo (quizás mucho tiempo) esperando no hacer nada. Esta espera abarca el tiempo entre el momento en que se envía la solicitud HTTP y la respuesta recibida. Durante este tiempo, esperamos a que la red lleve la solicitud, el servicio de almacenamiento la procese y envíe la respuesta, y la red lleve la respuesta (posiblemente grande). Mientras que partes de esa escala de ciclo de solicitud/respuesta con la cantidad de datos involucrados, otras partes son solo gastos generales fijos.
Los servicios de almacenamiento de objetos están diseñados para manejar muchas solicitudes concurrentes. Podemos combinar eso con el hecho de que cada solicitud implica algún tiempo esperando no hacer nada, para hacer muchas solicitudes concurrentes para aumentar nuestro rendimiento general. En Python, esto se haría típicamente usando un piscina de hilo:
pool = concurrent.futures.ThreadPoolExecutor() futures = pool. map (request_chunk, chunks) |
O con asincio:
tasks = [request_chunk_async(chunk) for chunk in chunks] await asyncio.gather( * tasks) |
Podemos tener muchas lecturas esperando sin hacer nada al mismo tiempo, lo que mejora nuestro rendimiento. Debido a que cada hilo/tarea no está haciendo nada en su mayoría, está bien tener más hilos/tareas de las que su máquina tiene núcleos. Dadas las suficientes solicitudes concurrentes, eventualmente saturará su servicio de almacenamiento, que tiene algunas solicitudes por segundo y objetivos de ancho de banda que intenta cumplir. Pero esos objetivos son altos; por lo general, necesitará muchas máquinas para saturar el servicio de almacenamiento y debería lograr un rendimiento muy alto.
Bibliotecas
Todo lo anterior se aplica esencialmente a cualquier biblioteca que haga IO remoto desde un servicio de almacenamiento de objetos. En el contexto de RAPIDS, NVIDIA KviKIO es notable porque
- Automáticamente divide las solicitudes grandes en varias más pequeñas y realiza esas solicitudes simultáneamente.
- Puede leer eficientemente en la memoria del host o del dispositivo, especialmente si almacenamiento Directo de GPU está habilitado.
- Es rápido.
Como se mencionó en el Anuncio de lanzamiento de RADIDS 24.12, KvikIO puede lograr un rendimiento impresionante al leer desde S3. Echemos un vistazo a algunos puntos de referencia para ver cómo funciona.
Puntos de referencia
Cuando lee un archivo, KvikIO divide esa lectura en lecturas más pequeñas de kvikio.defaults.task_size
bytes. Hace esas solicitudes de lectura en paralelo usando un grupo de hilos con kvikio.defaults.num_threads
trabajadores. Estos pueden controlarse utilizando las variables de entorno KVIKIO_TASK_SIZE
y KVIKIO_NTHREADS
, o a través de Python con:
with kvikio.defaults.set_num_threads(num_threads), kvikio.defaults.set_task_size(size): ... |
Ver Configuración de tiempo de ejecución para más.
Este gráfico muestra el rendimiento, en megabits por segundo, de leer una gota de 1 GB S3 a g4dn instancia EC2 en la misma región para varios tamaños de la agrupación de hilos (más alto es mejor).

kvikio.RemoteFile.read
para diversos valores de kvikio.defaults.num
_threads y un tamaño de tarea de 16 MiB. El rendimiento aumenta a medida que agregamos más hilos y paralelizamos las lecturas, hasta cierto punto.Menos hilos (menos de cuatro) logran un rendimiento más bajo y tardan más en leer el archivo. Más subprocesos (64, 128, 256) logran un mayor rendimiento al paralelizar las solicitudes al servicio de almacenamiento, que las sirve en paralelo. Hay rendimientos decrecientes e incluso negativos a medida que alcanzamos los límites del servicio de almacenamiento, la red u otros cuellos de botella en nuestro sistema.
Con IO remoto, cada hilo pasa un tiempo inactivo relativamente largo esperando la respuesta, por lo que un mayor número de hilos (en relación con su número de núcleos) podría ser apropiado para su carga de trabajo. Vemos que el rendimiento es más alto entre 64 y 128 hilos en este caso.
Como se muestra en la siguiente figura, el tamaño de la tarea también afecta al rendimiento máximo.

kvikio.RemoteFile.read
. El eje horizontal muestra el rendimiento para varios tamaños de tarea, mientras que el eje vertical muestra varios recuentos de roscas.Siempre que el tamaño de la tarea no sea demasiado pequeño (alrededor o por debajo de 4 MiB) o demasiado grande (alrededor o por encima de 128 MiB), entonces obtenemos alrededor de 10 Gbps de rendimiento. Con un tamaño de tarea demasiado pequeño, la sobrecarga de hacer muchas solicitudes HTTP reduce el rendimiento. Con un tamaño de tarea demasiado grande, no obtenemos suficiente concurrencia para maximizar el rendimiento.
KvikIO logra un mayor rendimiento en esta carga de trabajo en comparación con boto3, el SDK de AWS para Python, incluso cuando boto3 se usa en un grupo de subprocesos para ejecutar solicitudes simultáneamente.

Como una carga de trabajo un poco más realista, aunque todavía solo una centrada únicamente en IO, comparamos el rendimiento leyendo un lote de 360 archivos de parquet, cada uno de aproximadamente 128 MB. Esto se ejecutó en un AWS g4dn.12xlarge
instancia, que tiene 4 T4 NVIDIA GPU y 48 vCPU.

Con KvikIO habilitado, los cuatro procesos de trabajo de Dask pueden alcanzar colectivamente casi 20 Gbps de rendimiento de S3 a este único nodo.
Conclusión
A medida que RAPIDS acelera otras partes de su carga de trabajo, IO puede convertirse en un cuello de botella. Si está utilizando el almacenamiento de objetos y está cansado de esperar a que se carguen sus datos, pruebe algunas de las recomendaciones de esta publicación. Háganos saber cómo funcionan las cosas con KvikIO en GitHub. También puede unirse a más de 3.500 miembros de la comunidad RAPIDS Slack para hablar sobre el procesamiento de datos acelerado por GPU.
Recursos relacionados
- Contenedores NGC: Operador de GPU NVIDIA
- Contenedores NGC: NVIDIA Kata Manager para Kubernetes
- SDK: IO Magnum
- SDK: Almacenamiento GPUDirect
- SDK: OS DRIVE
- Webinar: Escritorio Virtual en la Era de la IA
NVIDIA Blog. T. A. Traducido al español