Analice el rendimiento de tf.data con TF Profiler

Visión general

Esta guía asume la familiaridad con TensorFlow Profiler y tf.data . Su objetivo es proporcionar instrucciones paso a paso con ejemplos para ayudar a los usuarios a diagnosticar y solucionar problemas de rendimiento de canalización de entrada.

Para comenzar, recopila un perfil de tu trabajo de TensorFlow. Las instrucciones sobre cómo hacerlo están disponibles para CPU/GPU y Cloud TPU .

TensorFlow Trace Viewer

El flujo de trabajo de análisis que se detalla a continuación se centra en la herramienta de visor de seguimiento en Profiler. Esta herramienta muestra una línea de tiempo que muestra la duración de las operaciones ejecutadas por su programa TensorFlow y le permite identificar qué operaciones tardan más en ejecutarse. Para obtener más información sobre el visor de seguimiento, consulte esta sección de la guía TF Profiler. En general, los eventos de tf.data aparecerán en la línea de tiempo de la CPU del host.

Flujo de trabajo de análisis

Siga el flujo de trabajo a continuación. Si tiene comentarios para ayudarnos a mejorarlo, cree un problema de github con la etiqueta "comp: data".

1. ¿Su tubería tf.data produce datos lo suficientemente rápido?

Comience por determinar si la canalización de entrada es el cuello de botella de su programa TensorFlow.

Para hacerlo, busque IteratorGetNext::DoCompute ops en el visor de seguimiento. En general, espera verlos al comienzo de un paso. Estos segmentos representan el tiempo que le toma a su tubería de entrada generar un lote de elementos cuando se solicita. Si está utilizando keras o iterando sobre su conjunto de datos en una tf.function ., estos deben encontrarse en los subprocesos tf_data_iterator_get_next .

Tenga en cuenta que si usa una estrategia de distribución , es posible que vea eventos IteratorGetNextAsOptional::DoCompute en lugar de IteratorGetNext::DoCompute (a partir de TF 2.3).

image

Si las llamadas regresan rápidamente (<= 50 us), significa que sus datos están disponibles cuando se solicitan. La tubería de entrada no es su cuello de botella; consulte la guía de Profiler para obtener sugerencias de análisis de rendimiento más genéricas.

image

Si las llamadas regresan lentamente, tf.data no puede mantenerse al día con las solicitudes del consumidor. Continúe con la siguiente sección.

2. ¿Está precargando datos?

La mejor práctica para el rendimiento de la canalización de entrada es insertar una transformación tf.data.Dataset.prefetch al final de su canalización tf.data . Esta transformación superpone el cálculo de preprocesamiento de la canalización de entrada con el siguiente paso del cálculo del modelo y es necesaria para un rendimiento óptimo de la canalización de entrada al entrenar su modelo. Si está precargando datos, debería ver un segmento Iterator::Prefetch en el mismo subproceso que la IteratorGetNext::DoCompute .

image

Si no tiene una prefetch al final de su canalización , debe agregar una. Para obtener más información sobre las recomendaciones de rendimiento de tf.data , consulte la guía de rendimiento de tf.data .

Si ya está obteniendo datos previamente y la canalización de entrada sigue siendo su cuello de botella, continúe con la siguiente sección para analizar más a fondo el rendimiento.

3. ¿Está alcanzando una alta utilización de la CPU?

tf.data logra un alto rendimiento al intentar hacer el mejor uso posible de los recursos disponibles. En general, incluso cuando ejecuta su modelo en un acelerador como GPU o TPU, las canalizaciones de tf.data se ejecutan en la CPU. Puede verificar su uso con herramientas como sar y htop , o en la consola de monitoreo en la nube si está ejecutando GCP.

Si su utilización es baja, esto sugiere que es posible que su canal de entrada no esté aprovechando al máximo la CPU del host. Debe consultar la guía de rendimiento de tf.data para conocer las mejores prácticas. Si ha aplicado las mejores prácticas y la utilización y el rendimiento siguen siendo bajos, continúe con el análisis de cuellos de botella a continuación.

Si su utilización se acerca al límite de recursos , para mejorar aún más el rendimiento, debe mejorar la eficiencia de su canalización de entrada (por ejemplo, evitando el cálculo innecesario) o descargar el cálculo.

Puede mejorar la eficiencia de su canalización de entrada evitando cálculos innecesarios en tf.data . Una forma de hacer esto es insertar una transformación tf.data.Dataset.cache después de un trabajo de computación intensivo si sus datos caben en la memoria; esto reduce el cálculo a costa de un mayor uso de la memoria. Además, deshabilitar el paralelismo dentro de la operación en tf.data tiene el potencial de aumentar la eficiencia en > 10 %, y se puede hacer configurando la siguiente opción en su canal de entrada:

dataset = ...
options = tf.data.Options()
options.experimental_threading.max_intra_op_parallelism = 1
dataset = dataset.with_options(options)

4. Análisis de cuellos de botella

La siguiente sección explica cómo leer eventos tf.data en el visor de seguimiento para comprender dónde está el cuello de botella y las posibles estrategias de mitigación.

Comprender los eventos tf.data en el generador de perfiles

Cada evento tf.data en Profiler tiene el nombre Iterator::<Dataset> , donde <Dataset> es el nombre del origen o la transformación del conjunto de datos. Cada evento también tiene el nombre largo Iterator::<Dataset_1>::...::<Dataset_n> , que puede ver haciendo clic en el evento tf.data . En el nombre largo, <Dataset_n> coincide con <Dataset> del nombre (abreviado), y los otros conjuntos de datos en el nombre largo representan transformaciones posteriores.

image

Por ejemplo, la captura de pantalla anterior se generó a partir del siguiente código:

dataset = tf.data.Dataset.range(10)
dataset = dataset.map(lambda x: x)
dataset = dataset.repeat(2)
dataset = dataset.batch(5)

Aquí, el evento Iterator:: Iterator::Map tiene el nombre largo Iterator::BatchV2::FiniteRepeat::Map . Tenga en cuenta que el nombre de los conjuntos de datos puede diferir ligeramente de la API de Python (por ejemplo, FiniteRepeat en lugar de Repeat), pero debe ser lo suficientemente intuitivo para analizar.

Transformaciones síncronas y asíncronas

Para las transformaciones sincrónicas tf.data (como Batch y Map ), verá eventos de transformaciones anteriores en el mismo subproceso. En el ejemplo anterior, dado que todas las transformaciones utilizadas son sincrónicas, todos los eventos aparecen en el mismo hilo.

Para las transformaciones asincrónicas (como Prefetch , ParallelMap , ParallelInterleave y MapAndBatch ), los eventos de las transformaciones ascendentes estarán en un subproceso diferente. En tales casos, el "nombre largo" puede ayudarlo a identificar a qué transformación en una canalización corresponde un evento.

image

Por ejemplo, la captura de pantalla anterior se generó a partir del siguiente código:

dataset = tf.data.Dataset.range(10)
dataset = dataset.map(lambda x: x)
dataset = dataset.repeat(2)
dataset = dataset.batch(5)
dataset = dataset.prefetch(1)

Aquí, los eventos Iterator Iterator::Prefetch están en los subprocesos tf_data_iterator_get_next . Dado que Prefetch es asíncrono, sus eventos de entrada ( BatchV2 ) estarán en un subproceso diferente y se pueden ubicar buscando el nombre largo Iterator::Prefetch::BatchV2 . En este caso, están en el subproceso tf_data_iterator_resource . Por su nombre largo, puede deducir que BatchV2 está aguas arriba de Prefetch . Además, el parent_id del evento BatchV2 coincidirá con el ID del evento Prefetch .

Identificando el cuello de botella

En general, para identificar el cuello de botella en la canalización de entrada, recorra la canalización de entrada desde la transformación más externa hasta el origen. A partir de la transformación final en su canalización, recurra a transformaciones ascendentes hasta que encuentre una transformación lenta o llegue a un conjunto de datos de origen, como TFRecord . En el ejemplo anterior, comenzaría desde Prefetch , luego caminaría río arriba hasta BatchV2 , FiniteRepeat , Map y finalmente Range .

En general, una transformación lenta corresponde a aquella cuyos eventos son largos, pero cuyos eventos de entrada son cortos. Algunos ejemplos siguen a continuación.

Tenga en cuenta que la transformación final (más externa) en la mayoría de las canalizaciones de entrada de host es el evento Iterator::Model . El tiempo de ejecución de tf.data introduce automáticamente la transformación del modelo y se usa para instrumentar y ajustar automáticamente el rendimiento de la canalización de entrada.

Si su trabajo utiliza una estrategia de distribución , el visor de seguimiento contendrá eventos adicionales que corresponden a la canalización de entrada del dispositivo. La transformación más externa de la canalización del dispositivo (anidada en IteratorGetNextOp::DoCompute o IteratorGetNextAsOptionalOp::DoCompute ) será un evento Iterator::Prefetch con un evento Iterator::Generator ascendente. Puede encontrar la canalización de host correspondiente buscando eventos Iterator::Model .

Ejemplo 1

image

La captura de pantalla anterior se genera a partir de la siguiente canalización de entrada:

dataset = tf.data.TFRecordDataset(filename)
dataset = dataset.map(parse_record)
dataset = dataset.batch(32)
dataset = dataset.repeat()

En la captura de pantalla, observe que (1) los eventos de Iterator::Map son largos, pero (2) sus eventos de entrada ( Iterator::FlatMap ) regresan rápidamente. Esto sugiere que la transformación de mapa secuencial es el cuello de botella.

Tenga en cuenta que en la captura de pantalla, el evento InstantiatedCapturedFunction::Run corresponde al tiempo que lleva ejecutar la función de mapa.

Ejemplo 2

image

La captura de pantalla anterior se genera a partir de la siguiente canalización de entrada:

dataset = tf.data.TFRecordDataset(filename)
dataset = dataset.map(parse_record, num_parallel_calls=2)
dataset = dataset.batch(32)
dataset = dataset.repeat()

Este ejemplo es similar al anterior, pero usa ParallelMap en lugar de Map. Notamos aquí que (1) los eventos Iterator::ParallelMap son largos, pero (2) sus eventos de entrada Iterator::FlatMap (que están en un subproceso diferente, ya que ParallelMap es asíncrono) son cortos. Esto sugiere que la transformación ParallelMap es el cuello de botella.

Abordar el cuello de botella

Conjuntos de datos de origen

Si identificó una fuente de conjunto de datos como el cuello de botella, como la lectura de archivos TFRecord, puede mejorar el rendimiento paralelizando la extracción de datos. Para hacerlo, asegúrese de que sus datos estén fragmentados en varios archivos y use tf.data.Dataset.interleave con el parámetro num_parallel_calls establecido en tf.data.AUTOTUNE . Si el determinismo no es importante para su programa, puede mejorar aún más el rendimiento configurando el indicador deterministic=False en tf.data.Dataset.interleave a partir de TF 2.2. Por ejemplo, si está leyendo de TFRecords, puede hacer lo siguiente:

dataset = tf.data.Dataset.from_tensor_slices(filenames)
dataset = dataset.interleave(tf.data.TFRecordDataset,
  num_parallel_calls=tf.data.AUTOTUNE,
  deterministic=False)

Tenga en cuenta que los archivos fragmentados deben ser razonablemente grandes para amortizar la sobrecarga de abrir un archivo. Para obtener más detalles sobre la extracción de datos en paralelo, consulte esta sección de la guía de rendimiento de tf.data .

Conjuntos de datos de transformación

Si identificó una transformación tf.data intermedia como el cuello de botella, puede solucionarlo paralelizando la transformación o almacenando en caché el cálculo si sus datos caben en la memoria y es apropiado. Algunas transformaciones como Map tienen contrapartes paralelas; la guía de rendimiento de tf.data demuestra cómo paralelizarlos. Otras transformaciones, como Filter , Unbatch y Batch son inherentemente secuenciales; puede paralelizarlos introduciendo "paralelismo externo". Por ejemplo, supongamos que su canal de entrada inicialmente se parece a lo siguiente, con Batch como cuello de botella:

filenames = tf.data.Dataset.list_files(file_path, shuffle=is_training)
dataset = filenames_to_dataset(filenames)
dataset = dataset.batch(batch_size)

Puede introducir el "paralelismo externo" ejecutando varias copias de la canalización de entrada sobre entradas fragmentadas y combinando los resultados:

filenames = tf.data.Dataset.list_files(file_path, shuffle=is_training)

def make_dataset(shard_index):
  filenames = filenames.shard(NUM_SHARDS, shard_index)
  dataset = filenames_to_dataset(filenames)
  Return dataset.batch(batch_size)

indices = tf.data.Dataset.range(NUM_SHARDS)
dataset = indices.interleave(make_dataset,
                             num_parallel_calls=tf.data.AUTOTUNE)
dataset = dataset.prefetch(tf.data.AUTOTUNE)

Recursos adicionales