Path: blob/master/site/es-419/guide/function.ipynb
25115 views
Copyright 2020 The TensorFlow Authors.
Mejor rendimiento con tf.function
En TensorFlow 2, la ejecución eager está activada de forma predeterminada. La interfaz de usuario es intuitiva y flexible (ejecutar operaciones puntuales es mucho más fácil y rápido), pero esto puede ir en detrimento del rendimiento y la capacidad de implementación.
Puede usar tf.function
para crear grafos a partir de sus programas. Se trata de una herramienta de transformación que crea grafos de flujo de datos independientes de Python a partir de su código Python. Esto le ayudará a crear modelos portátiles y de alto rendimiento, y es necesario usar SavedModel
.
Esta guía le ayudará a conceptualizar cómo funciona tf.function
por dentro, para que pueda usarlo con eficacia.
Las principales conclusiones y recomendaciones son:
Depure en modo eager, luego decore con
@tf.function
.No dependa de los efectos secundarios de Python, como la mutación de objetos o la anexión de listas.
tf.function
funciona mejor con las ops de TensorFlow; las llamadas de NumPy y Python se convierten en constantes.
Preparación
Defina una función ayudante para demostrar los tipos de errores que puede encontrar:
Conceptos básicos
Uso
Una Function
que usted defina (por ejemplo, aplicando el decorador @tf.function
) es igual que una operación central de TensorFlow: Puede ejecutarse de forma eager; puede calcular gradientes; etc.
Puede usar Function
s dentro de otras Function
s.
Function
s puede ser más rápido que el código eager, especialmente para grafos con muchas ops pequeñas. Pero para grafos con unas pocas ops costosas (como las convoluciones), es posible que no se vea mucho aumento de la velocidad.
Trazado
Esta sección expone cómo Función
funciona por dentro, incluyendo detalles de implementación que pueden cambiar en el futuro. De todos modos, en cuanto entienda por qué y cuándo se produce el trazado, ¡le resultará mucho más fácil usar tf.function
con eficacia!
¿Qué es el "trazado"?
Una Function
ejecuta su programa en un Graph de TensorFlow. Sin embargo, un tf.Graph
no puede representar todas las cosas que usted escribiría en un programa TensorFlow eager. Por ejemplo, Python admite el polimorfismo, pero tf.Graph
requiere que sus entradas tengan un tipo de datos y una dimensión especificados. O puede realizar tareas secundarias como leer argumentos de la línea de comandos, lanzar un error o trabajar con un objeto Python más complejo; ninguna de estas cosas puede ejecutarse en un tf.Graph
.
Function
cubre este vacío al separar su código en dos etapas:
En la primera etapa, llamada "trazado",
Function
crea un nuevotf.Graph
. El código Python se ejecuta normalmente, pero todas las operaciones TensorFlow (como sumar dos Tensores) están aplazadas: son capturadas por eltf.Graph
y no se ejecutan.En la segunda etapa, se ejecuta un
tf.Graph
que contiene todo lo que se aplazó en la primera etapa. Esta etapa es mucho más rápida que la etapa de trazado.
Dependiendo de sus entradas, Function
no siempre ejecutará la primera etapa cuando sea llamada. Vea más abajo las "Reglas de trazado" si quiere entender mejor cómo toma esa determinación. TensorFlow tiene un alto rendimiento porque se salta la primera etapa y sólo ejecuta la segunda.
Cuando Function
sí decide trazar, la etapa de trazado es seguida inmediatamente por la segunda etapa, por lo que llamar a Function
tanto crea como ejecuta el tf.Graph
. Más adelante verá cómo puede ejecutar sólo la etapa de trazado con get_concrete_function
.
Cuando se pasan argumentos de distinto tipo a una Function
, se ejecutan ambas etapas:
Tenga en cuenta que si llama repetidamente a una Function
con el mismo tipo de argumento, TensorFlow se saltará la etapa de trazado y reutilizará un grafo trazado previamente, ya que el grafo generado sería idéntico.
Puede usar pretty_printed_concrete_signatures()
para ver todos los trazados disponibles:
Hasta ahora, ha visto que tf.function
crea una capa de envío dinámica y en caché sobre la lógica de trazado del grafo de TensorFlow. Para ser más específico sobre la terminología:
Un
tf.Graph
es la representación en bruto, agnóstica al lenguaje y portátil de un cálculo TensorFlow.Una
ConcreteFunction
encapsula untf.Graph
.Una
Function
administra una caché deConcreteFunction
s y elige la adecuada para sus entradas.tf.function
encapsula una función Python, devolviendo un objetoFunction
.El trazado crea un
tf.Graph
y lo encapsula en unaConcreteFunction
, también conocida como un trazo.
Reglas de trazado
Cuando se llama, una Function
hace coincidir los argumentos de llamada con ConcreteFunction
s existentes usando tf.types.experimental.TraceType
de cada argumento. Si se encuentra una ConcreteFunction
que coincida, se le envía la llamada. Si no se encuentra ninguna coincidencia, se traza una nueva ConcreteFunction
.
Si se encuentran varias coincidencias, se selecciona la firma más específica. La coincidencia se realiza mediante subtipado, de forma muy parecida a las llamadas a funciones normales en C++ o Java, por ejemplo. Por ejemplo, TensorShape([1, 2])
es un subtipo de TensorShape([None, None])
y, por tanto, una llamada a tf.function con TensorShape([1, 2])
puede ser enviada a la ConcreteFunction
producida con TensorShape([None, None])
pero si también existe una ConcreteFunction
con TensorShape([1, None])
entonces se le dará prioridad ya que es más específica.
El TraceType
se determina a partir de los argumentos de entrada como sigue:
Para
Tensor
, el tipo está parametrizado por losdtype
yshape
delTensor
; las formas clasificadas son un subtipo de las formas no clasificadas; las dimensiones fijas son un subtipo de las dimensiones desconocidasPara
Variable
, el tipo es similar aTensor
, pero también incluye un ID de recurso único de la variable, necesario para integrar correctamente las dependencias de control.Para los valores primitivos de Python, el tipo corresponde al propio valor. Por ejemplo, el
TraceType
del valor3
esLiteralTraceType<3>
, noint
.Para los contenedores ordenados de Python como
list
ytuple
, etc., el tipo está parametrizado por los tipos de sus elementos; por ejemplo, el tipo de[1, 2]
esListTraceType<LiteralTraceType<1>, LiteralTraceType<2>>
y el tipo para[2, 1]
esListTraceType<LiteralTraceType<2>, LiteralTraceType<1>>
que es diferente.Para mapeos en Python como
dict
, el tipo es también un mapeo de las mismas claves pero a los tipos de valores en lugar de a los valores reales. Por ejemplo, el tipo de{1: 2, 3: 4}
, esMappingTraceType<<KeyValue<1, LiteralTraceType<2>>>, <KeyValue<3, LiteralTraceType<4>>>>
. Sin embargo, a diferencia de los contenedores ordenados,{1: 2, 3: 4}
y{3: 4, 1: 2}
tienen tipos equivalentes.Para los objetos Python que implementan el método
__tf_tracing_type__
, el tipo es cualquier cosa que devuelva ese métodoPara cualquier otro objeto Python, el tipo es un
TraceType
genérico, su precedencia de coincidencia es:Primero comprueba si el objeto es el mismo que se usó en el trazo anterior (usando python
id()
ois
). Tenga en cuenta que esto seguirá coincidiendo si el objeto ha cambiado, por lo que si usa objetos python como argumentostf.function
es mejor usar los inmutables.A continuación comprueba si el objeto es igual al objeto usado en el trazo anterior (usando python
==
).
Tenga en cuenta que este procedimiento sólo conserva una referencia débil (weakref) al objeto y, por lo tanto, sólo funciona mientras el objeto esté en el ámbito de aplicación/no se haya eliminado).
Nota: TraceType
se basa en los parámetros de entrada Function
por lo que los cambios en las variables globales y libres por sí solos no crearán un nuevo trazo. Consulte esta sección para conocer las prácticas recomendadas al tratar con variables globales y libres de Python.
Control del retrazado
El retrazado, que es cuando su Function
crea más de un trazo, ayuda a asegurar que TensorFlow genera grafos correctos para cada conjunto de entradas. Sin embargo, ¡el trazado es una operación costosa! Si su Function
retraza un nuevo grafo para cada llamada, se dará cuenta de que su código se ejecuta más lentamente que si no usara tf.function
.
Para controlar el comportamiento del trazado, puede usar las siguientes técnicas:
Pasar una input_signature
fija a tf.function
Usar dimensiones desconocidas para mayor flexibilidad
Ya que TensorFlow hace coincidir los tensores basándose en su forma, usar una dimensión None
como comodín permitirá a las Function
s reutilizar trazos para entradas de tamaño variable. La entrada de tamaño variable puede ocurrir si tiene secuencias de diferente longitud, o imágenes de diferentes tamaños para cada lote (Vea los tutoriales Transformer y Deep Dream por ejemplo).
Pasar tensores en lugar de literales python
Los argumentos de Python se usan a menudo para controlar los hiperparámetros y la construcción de grafos; por ejemplo, num_layers=10
o training=True
o nonlinearity='relu'
. Por lo tanto, si el argumento Python cambia, es lógico que tenga que retrazar el grafo.
Sin embargo, podría darse el caso de que no se esté usando un argumento de Python para controlar la construcción del grafo. En estos casos, un cambio en el valor de Python puede desencadenar un retrazado innecesario. Tomemos, por ejemplo, este bucle de entrenamiento, que AutoGraph desenrollará dinámicamente. A pesar de las múltiples trazas, el grafo generado es en realidad idéntico, por lo que el retrazado es innecesario.
Si necesita forzar el retrazado, cree una nueva Function
. Se garantiza que los objetos Function
separados no comparten trazos.
Usar el protocolo de trazado
Siempre que sea posible, debería preferir en su lugar convertir el tipo Python en un tf.experimental.ExtensionType
. Además, el TraceType
de un ExtensionType
es el tf.TypeSpec
asociado a él. Por lo tanto, si es necesario, puede simplemente anular el tf.TypeSpec
predeterminado para tomar el control del Tracing Protocol
de un ExtensionType
. Para más detalles, consulte la sección Personalizar el TypeSpec de ExtensionType en la guía Tipos de extensión.
De lo contrario, para tener un control directo sobre cuándo Function
debe retrazar con respecto a un tipo particular de Python, puede implementar usted mismo el Tracing Protocol
para ello.
Obtener funciones concretas
Cada vez que se traza una función, se crea una nueva función concreta. Puede obtener directamente una función concreta, usando get_concrete_function
.
Al imprimir una ConcreteFunction
se muestra un resumen de sus argumentos de entrada (con tipos) y su tipo de salida.
También puede recuperar directamente la firma de una función concreta.
Usar un trazo concreto con tipos incompatibles arrojará un error
Puede que haya notado que los argumentos Python reciben un tratamiento especial en la firma de entrada de una función concreta. Antes de TensorFlow 2.3, los argumentos Python eran simplemente eliminados de la firma de la función concreta. A partir de TensorFlow 2.3, los argumentos Python permanecen en la firma, pero están restringidos a tomar el valor fijado durante el trazado.
Obtener grafos
Cada función concreta es un contenedor invocable que envuelve un tf.Graph
. Aunque recuperar el objeto tf.Graph
real no es algo que necesite hacer normalmente, puede hacerlo fácilmente desde cualquier función concreta.
Depuración
En general, depurar código es más fácil en modo eager que dentro de tf.function
. Debe asegurarse de que su código se ejecuta sin errores en modo eager antes de decorar con tf.function
. Para ayudar en el proceso de depuración, puede llamar tf.config.run_functions_eagerly(True)
para deshabilitar y volver a habilitar globalmente tf.function
.
A continuación le ofrecemos algunos consejos para el seguimiento de los problemas que sólo aparecen dentro de tf.function
:
Las llamadas simples
print
de Python sólo se ejecutan durante el trazado, lo que le ayuda a realizar un seguimiento de cuándo se (re)traza su función.Las llamadas
tf.print
se ejecutarán cada vez, y pueden ayudarle a realizar un seguimiento de los valores intermedios durante la ejecución.tf.debugging.enable_check_numerics
es una forma sencilla de descubrir dónde se crean los NaN y los Inf.pdb
(el depurador de Python) puede ayudarle a entender lo que ocurre durante el trazado. (Advertencia:pdb
le llevará al código fuente transformado por AutoGraph).
Transformaciones de AutoGraph
AutoGraph es una librería que está activada de forma predeterminada en tf.function
, y transforma un subgrupo de código eager de Python en ops de TensorFlow compatibles con grafos. Esto incluye flujos de control como if
, for
, while
.
Las ops de TensorFlow como tf.cond
y tf.while_loop
siguen funcionando, pero el flujo de control suele ser más fácil de escribir y entender cuando se escribe en Python.
Si le interesa, puede inspeccionar el código que genera el autograph.
Condicionales
AutoGraph convertirá algunas sentencias if <condition>
en las llamadas tf.cond
equivalentes. Esta sustitución se realiza si <condition>
es un Tensor. En caso contrario, la sentencia if
se ejecuta como un condicional de Python.
Un condicional Python se ejecuta durante el trazado, por lo que se añadirá exactamente una derivación del condicional al grafo. Sin AutoGraph, este grafo trazado sería incapaz de tomar la bifurcación alternativa si existe un flujo de control dependiente de los datos.
tf.cond
traza y añade ambas bifurcaciones del condicional al grafo, seleccionando dinámicamente una derivación en el momento de la ejecución. El trazado puede tener efectos secundarios no deseados; revise efectos del trazado de AutoGraph si desea más información.
Si desea saber más sobre las restricciones de las sentencias if convertidas con AutoGraph, consulte la documentación de referencia.
Bucles
AutoGraph convertirá algunas sentencias for
y while
en las operaciones de bucle equivalentes de TensorFlow, como tf.while_loop
. Si no se convierte, el bucle for
o while
se ejecuta como un bucle Python.
Esta sustitución se realiza en las siguientes situaciones:
for x in y
: siy
es un Tensor, convierta atf.while_loop
. En el caso especial de quey
sea untf.data.Dataset
, se genera una combinación de opstf.data.Dataset
.while <condición>
: si<condition>
es un tensor, convierta atf.while_loop
.
Un bucle Python se ejecuta durante el trazado, añadiendo ops adicionales al tf.Graph
por cada iteración del bucle.
Un bucle TensorFlow traza el cuerpo del bucle y selecciona dinámicamente cuántas iteraciones debe ejecutar en el momento de la ejecución. El cuerpo del bucle sólo aparece una vez en el tf.Graph
generado.
Si desea saber más sobre las restricciones de las sentencias for
y while
convertidas con AutoGraph, consulte la documentación de referencia.
Bucle sobre datos Python
Un error común es hacer un bucle sobre los datos de Python/NumPy dentro de una tf.function
. Este bucle se ejecutará durante el proceso de trazado, añadiendo una copia de su modelo al tf.Graph
por cada iteración del bucle.
Si desea encapsular todo el bucle de entrenamiento en tf.function
, la forma más segura de hacerlo es encapsular sus datos como un tf.data.Dataset
para que AutoGraph desenrede dinámicamente el bucle de entrenamiento.
Cuando encapsule datos Python/NumPy en un conjunto de datos, preste atención a tf.data.Dataset.from_generator
versus tf.data.Dataset.from_tensor_slices
. El primero conservará los datos en Python y los recuperará mediante tf.py_function
, lo que puede tener implicaciones en el rendimiento, mientras que el segundo agrupará una copia de los datos como un gran nodo tf.constant()
en el grafo, lo que puede tener implicaciones en la memoria.
La lectura de datos de archivos a través de TFRecordDataset
, CsvDataset
, etc. es la forma más eficaz de consumir datos, ya que entonces el propio TensorFlow puede administrar la carga asíncrona y la preextracción de datos, sin tener que involucrar a Python. Si desea obtener más información, consulte el tf.data
: Construir canalizaciones de entrada de TensorFlow.
Valores acumulados en un bucle
Un patrón común es acumular valores intermedios de un bucle. Normalmente, esto se consigue añadiendo a una lista Python o añadiendo entradas a un diccionario Python. Sin embargo, como estos son efectos secundarios de Python, no funcionarán como se espera en un bucle desenredado dinámicamente. Use tf.TensorArray
para acumular resultados de un bucle desenredado dinámicamente.
Limitaciones
Function
de TensorFlow tiene algunas limitaciones por diseño que debe conocer al convertir una función Python en una Function
.
Efectos secundarios de la ejecución de Python
Los efectos secundarios, como imprimir, anexar a listas y mutar globales, pueden comportarse de forma inesperada dentro de una Function
, a veces ejecutándose dos veces o ninguna. Sólo ocurren la primera vez que se llama a una Function
con un conjunto de entradas. Después, se vuelve a ejecutar el tf. Graph
trazado, sin ejecutar el código Python.
La regla general es evitar depender de los efectos secundarios de Python en su lógica y sólo usarlos para depurar sus trazados. De lo contrario, la mejor forma de asegurarse de que su código será ejecutado por el runtime de TensorFlow con cada llamada son las APIs de TensorFlow como tf.data
, tf.print
, tf.summary
, tf.Variable.assign
, y tf.TensorArray
.
Si desea ejecutar código Python durante cada invocación de una Function
, tf.py_function
es una solución de salida. El inconveniente de tf.py_function
es que no es portable ni especialmente eficiente, no puede guardarse con SavedModel y no funciona bien en configuraciones distribuidas (multi-GPU, TPU). Además, dado que tf.py_function
tiene que ser integrada en el grafo, convierte todas las entradas/salidas en tensores.
Cambio de variables globales y libres de Python
Cambiar las variables globales y libres cuenta como un efecto secundario de Python, por lo que sólo ocurre durante el trazado.
A veces, los comportamientos inesperados son muy difíciles de notar. En el ejemplo siguiente, se pretende que el counter
garantice el incremento de una variable. Sin embargo, debido a que es un entero python y no un objeto TensorFlow, su valor es capturado durante el primer trazado. Cuando se usa tf.function
, assign_add
se graba incondicionalmente en el grafo subyacente. Por lo tanto, v
aumentará en 1, cada vez que se llame a la tf.function
. Este problema es común entre los usuarios que intentan migrar su código Tensorflow en modo Graph a Tensorflow 2 usando decoradores tf.function
, cuando los efectos secundarios de Python (counter
en el ejemplo) se usan para determinar qué ops ejecutar (assign_add
en el ejemplo). Normalmente, los usuarios se dan cuenta de esto sólo después de ver resultados numéricos sospechosos, o un rendimiento significativamente inferior al esperado (por ejemplo, si la operación vigilada es muy costosa).
Una solución para conseguir el comportamiento esperado es usar tf.init_scope
para elevar las operaciones fuera del grafo de funciones. Esto garantiza que el incremento de la variable sólo se realice una vez durante el tiempo de trazado. Debe tenerse en cuenta que init_scope
tiene otros efectos secundarios, incluyendo el flujo de control despejado y la cinta de gradiente. A veces el uso de init_scope
puede llegar a ser demasiado complejo para administrarlo de forma realista.
En resumen, como regla general, debe evitar mutar objetos python, como enteros, o contenedores, como listas, que se encuentren fuera de la Function
. En su lugar, use argumentos y objetos TF. Por ejemplo, la sección "Valores acumuladors en un bucle" tiene un ejemplo de cómo implementar operaciones tipo lista.
Puede, en algunos casos, capturar y manipular el estado si es una tf.Variable
. Así es como se actualizan las ponderaciones de los modelos Keras con llamadas repetidas a la misma ConcreteFunction
.
Usar iteradores y generadores de Python
Muchas funciones de Python, como los generadores y los iteradores, dependen de que el runtime de Python lleve el control de su estado. En general, aunque estos constructos funcionan como se espera en modo eager, son ejemplos de efectos secundarios de Python y, por tanto, sólo se producen durante el trazado.
Al igual que TensorFlow tiene un tf.TensorArray
especializado para los constructos de lista, tiene un tf.data.Iterator
especializado para los constructos de iteración. Consulte la sección sobre Transformaciones AutoGraph para una descripción general. Además, la API tf.data
puede ayudar a implementar patrones generadores:
Todas las salidas de una función tf.deben ser valores retornados
Con la excepción de tf.Variable
s, una función tf.debe retornar todas sus salidas. Intentar acceder directamente a cualquier tensor desde una función sin pasar por los valores de retorno provoca "fugas".
Por ejemplo, la función siguiente "fuga" el tensor a
a través de la x
global de Python:
Esto es verdadero incluso si también se retorna el valor filtrado:
Normalmente, este tipo de fugas se producen cuando se usan sentencias o estructuras de datos de Python. Además de filtrar tensores inaccesibles, tales declaraciones también son probablemente erróneas porque cuentan como efectos secundarios de Python, y no se garantiza que se ejecuten en cada llamada de función.
Entre las formas habituales de fugas de tensores locales también se incluye mutar una colección externa de Python, o un objeto:
Las tf.functions recursivas no son compatibles
Function
s recursivas no son compatibles y podrían provocar bucles infinitos. Por ejemplo,
Incluso si una Function
recursiva parece funcionar, la función python será trazada varias veces y podría tener consecuencias para el rendimiento. Por ejemplo,
Problemas conocidos
Si su Function
no se evalúa correctamente, el error puede explicarse debido a estos problemas conocidos que está previsto solucionar en el futuro.
Depender de variables globales y libres de Python
Function
crea una nueva ConcreteFunction
cuando se llama con un nuevo valor de un argumento Python. Sin embargo, no lo hace para el cierre de Python, ni para sus globales o no locales de esa Function
. Si su valor cambia entre llamadas a la Function
, la Function
seguirá usando los valores que tenían cuando fue trazada. Esto es diferente de como trabajan las funciones normales de Python.
Por este motivo, debe seguir un estilo de programación funcional que use argumentos en lugar de cierre sobre nombres externos.
Otra forma de actualizar un valor global, es convertirlo en una tf.Variable
y usar en su lugar el método Variable.assign
.
Depender de objetos Python
Es permitido pasar objetos Python personalizados como argumentos a tf.function
, pero tiene ciertas limitaciones.
Para obtener la máxima cobertura de funciones, considere la posibilidad de transformar los objetos en Tipos de extensión antes de pasarlos a tf.function
. También puede usar primitivas de Python y estructuras compatibles con tf.nest
.
Sin embargo, como se explica en las reglas de trazado, cuando la clase personalizada de Python no ofrece un TraceType
personalizado, tf.function
se ve obligada a usar la igualdad basada en instancias, lo que significa que no creará un nuevo trazado cuando le pase el mismo objeto con atributos modificados.
Usar la misma Function
para evaluar la instancia modificada del modelo tendrá errores, ya que sigue teniendo el mismo TraceType basado en instancias que el modelo original.
Por este motivo, se recomienda que escriba su Function
para evitar depender de los atributos mutables de los objetos o que implemente el Protocolo de trazado para que los objetos informen a su Function
sobre dichos atributos.
Si no es posible, una solución consiste en crear nuevas Function
cada vez que modifique su objeto para forzar el retrazado:
Como el retrazado puede resultar costoso, puede usar tf.Variable
s como atributos de objeto, que pueden mutarse (¡ojo! pero no cambiarse) para conseguir un efecto similar sin necesidad de retrazado.
Crear tf.Variables
Function
sólo admite tf.Variable
s únicos creados una vez en la primera llamada y reutilizados en las siguientes llamadas a la función. El siguiente fragmento de código crearía una nueva tf.Variable
en cada llamada a la función, lo que resultaría en una excepción ValueError
.
Ejemplo:
Un patrón común utilizado para solucionar esta limitación es comenzar con un valor None de Python y, a continuación, crear condicionalmente la tf.Variable
si el valor es None:
Usar con múltiples optimizadores Keras
Puede encontrarse con el error ValueError: tf.function only supports singleton tf.Variables created on the first call.
al usar más de un optimizador Keras con una tf.function
. Este error se produce porque los optimizadores crean internamente tf.Variables
cuando aplican gradientes por primera vez.
Si necesita cambiar el optimizador durante el entrenamiento, una solución es crear una nueva Function
para cada optimizador, llamando directamente a la ConcreteFunction
.
Usar múltiples modelos Keras
También puede encontrarse con el error ValueError: tf.function only supports singleton tf.Variables created on the first call.
al pasar diferentes instancias del modelo a la misma Function
.
Este error se produce porque los modelos Keras (que no tienen definida su forma de entrada) y las capas Keras crean tf.Variables
s cuando son llamadas por primera vez. Puede que esté intentando inicializar esas variables dentro de una Function
, que ya ha sido llamada. Para evitar este error, intente llamar a model.build(input_shape)
para inicializar todas las ponderaciones antes de entrenar el modelo.
Lecturas adicionales
Si desea más información sobre cómo exportar y cargar una Function
, consulte la guía de SavedModel. Si desea más información sobre las optimizaciones de grafos que se realizan tras el trazado, consulte la guía de Grappler. Si desea saber cómo optimizar su canalización de datos y perfilar su modelo, consulte la guía de Profiler.