Se usó la API de Cloud Translation para traducir esta página.
Switch to English

Analice el rendimiento de tf.data con TF Profiler

Visión general

En esta guía, se asume que está familiarizado 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 la canalización de entrada.

Para comenzar, recopile un perfil de su 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 visualización de trazas 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 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 que nos ayuden a mejorarlo, cree un problema de github con la etiqueta "comp: data".

1. ¿Su canalización tf.data produce datos con la suficiente rapidez?

Comience por determinar si la canalización de entrada es el cuello de botella para 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 tarda su canalización de entrada en producir un lote de elementos cuando se solicita. Si está usando keras o iterando sobre su conjunto de datos en una función tf.function , estos deben encontrarse en tf_data_iterator_get_next threads.

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

image

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

image

Si las llamadas regresan lentamente, tf.data no puede seguir el ritmo de las solicitudes del consumidor. Continúe con la siguiente sección.

2. ¿Está obteniendo datos previamente?

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 la canalización tf.data . Esta transformación se superpone al cálculo previo del procesamiento 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á obteniendo datos previamente, debería ver un segmento Iterator::Prefetch en el mismo hilo 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 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 se ejecuta su modelo en un acelerador como una GPU o TPU, las canalizaciones 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 su canalización de entrada puede no estar 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 cuello 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 canal de entrada (por ejemplo, evitando cálculos innecesarios) 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 hacerlo es insertando una transformación tf.data.Dataset.cache después de un trabajo de cálculo intensivo si sus datos caben en la memoria; esto reduce la computación a costa de un mayor uso de memoria. Además, deshabilitar el paralelismo tf.data 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 los eventos tf.data en el visor de seguimiento para comprender dónde está el cuello de botella y las posibles estrategias de mitigación.

Comprensión de los eventos tf.data en Profiler

Cada evento tf.data en Profiler tiene el nombre Iterator::<Dataset> , donde <Dataset> es el nombre de la fuente o 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 (corto), 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 Iterator::Map evento tiene el nombre largo Iterator::BatchV2::FiniteRepeat::Map . Tenga en cuenta que el nombre del conjunto 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 sincrónicas y asincrónicas

Para las transformaciones tf.data síncronas (como Batch y Map ), verá eventos de transformaciones ascendentes en el mismo hilo. 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 tf_data_iterator_get_next Iterator::Prefetch están en los hilos tf_data_iterator_get_next . Dado que Prefetch es asincrónico, sus eventos de entrada ( BatchV2 ) estarán en un hilo diferente y se pueden ubicar buscando el nombre largo Iterator::Prefetch::BatchV2 . En este caso, están en el hilo tf_data_iterator_resource . Por su nombre largo, puede deducir que BatchV2 es anterior a Prefetch . Además, la parent_id de la BatchV2 evento coincidir con el ID de la Prefetch evento.

Identificar el cuello de botella

En general, para identificar el cuello de botella en su canalización de entrada, recorra la canalización de entrada desde la transformación más externa hasta la fuente. 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 una cuyos eventos son largos, pero cuyos eventos de entrada son cortos. A continuación se muestran algunos ejemplos.

Tenga en cuenta que la transformación final (más externa) en la mayoría de las canalizaciones de entrada del host es el evento Iterator::Model . El tiempo de ejecución tf.data introduce la transformación del modelo automáticamente y se utiliza para instrumentar y tf.data 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 IteratorGetNextOp::DoCompute 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 Iterator::Map son largos, pero (2) sus eventos de entrada ( Iterator::FlatMap ) regresan rápidamente. Esto sugiere que la transformación secuencial del mapa 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. Observamos aquí que (1) los eventos Iterator::ParallelMap son largos, pero (2) sus eventos de entrada Iterator::FlatMap (que están en un hilo diferente, ya que ParallelMap es asincrónico) 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 ha identificado 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.experimental.AUTOTUNE . Si el determinismo no es importante para su programa, puede mejorar aún más el rendimiento estableciendo el indicador deterministic=False en tf.data.Dataset.interleave 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.experimental.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 ha identificado 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 encajan 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 canalización de entrada se ve inicialmente como 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 un "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.experimental.AUTOTUNE)
dataset = dataset.prefetch(tf.data.experimental.AUTOTUNE)

Recursos adicionales