Qu’est-ce que tfcompile ?
tfcompile
est un outil autonome qui compile à l'avance (AOT) les graphiques TensorFlow en code exécutable. Cela peut réduire la taille totale des binaires et également éviter certaines surcharges d'exécution. Un cas d'utilisation typique de tfcompile
consiste à compiler un graphique d'inférence en code exécutable pour les appareils mobiles.
Le graphique TensorFlow est normalement exécuté par le runtime TensorFlow. Cela entraîne une certaine surcharge d'exécution pour l'exécution de chaque nœud du graphique. Cela conduit également à une taille binaire totale plus grande, puisque le code du runtime TensorFlow doit être disponible, en plus du graphique lui-même. Le code exécutable produit par tfcompile
n'utilise pas le runtime TensorFlow et n'a que des dépendances sur les noyaux réellement utilisés dans le calcul.
Le compilateur est construit sur le framework XLA. Le code reliant TensorFlow au framework XLA réside sous tensorflow/compiler .
Que fait tfcompile ?
tfcompile
prend un sous-graphe, identifié par les concepts TensorFlow de flux et de récupérations, et génère une fonction qui implémente ce sous-graphe. Les feeds
sont les arguments d'entrée de la fonction et les fetches
sont les arguments de sortie de la fonction. Toutes les entrées doivent être entièrement spécifiées par les flux ; le sous-graphe élagué résultant ne peut pas contenir de nœuds Placeholder ou Variable. Il est courant de spécifier tous les espaces réservés et variables en tant que flux, ce qui garantit que le sous-graphe résultant ne contient plus ces nœuds. La fonction générée est conditionnée sous la forme d'une cc_library
, avec un fichier d'en-tête exportant la signature de la fonction et un fichier objet contenant l'implémentation. L'utilisateur écrit du code pour appeler la fonction générée, le cas échéant.
Utiliser tfcompile
Cette section détaille les étapes de haut niveau pour générer un binaire exécutable avec tfcompile
à partir d'un sous-graphe TensorFlow. Les étapes sont les suivantes :
- Étape 1 : Configurer le sous-graphe à compiler
- Étape 2 : utilisez la macro de construction
tf_library
pour compiler le sous-graphe - Étape 3 : Écrivez du code pour appeler le sous-graphe
- Étape 4 : Créer le binaire final
Étape 1 : Configurer le sous-graphe à compiler
Identifiez les flux et les récupérations qui correspondent aux arguments d'entrée et de sortie de la fonction générée. Configurez ensuite les feeds
et fetches
dans un proto tensorflow.tf2xla.Config
.
# Each feed is a positional input argument for the generated function. The order
# of each entry matches the order of each input argument. Here “x_hold” and “y_hold”
# refer to the names of placeholder nodes defined in the graph.
feed {
id { node_name: "x_hold" }
shape {
dim { size: 2 }
dim { size: 3 }
}
}
feed {
id { node_name: "y_hold" }
shape {
dim { size: 3 }
dim { size: 2 }
}
}
# Each fetch is a positional output argument for the generated function. The order
# of each entry matches the order of each output argument. Here “x_y_prod”
# refers to the name of a matmul node defined in the graph.
fetch {
id { node_name: "x_y_prod" }
}
Étape 2 : utilisez la macro de construction tf_library pour compiler le sous-graphe
Cette étape convertit le graphique en cc_library
à l'aide de la macro de construction tf_library
. La cc_library
se compose d'un fichier objet contenant le code généré à partir du graphique, ainsi que d'un fichier d'en-tête qui donne accès au code généré. tf_library
utilise tfcompile
pour compiler le graphique TensorFlow en code exécutable.
load("//tensorflow/compiler/aot:tfcompile.bzl", "tf_library")
# Use the tf_library macro to compile your graph into executable code.
tf_library(
# name is used to generate the following underlying build rules:
# <name> : cc_library packaging the generated header and object files
# <name>_test : cc_test containing a simple test and benchmark
# <name>_benchmark : cc_binary containing a stand-alone benchmark with minimal deps;
# can be run on a mobile device
name = "test_graph_tfmatmul",
# cpp_class specifies the name of the generated C++ class, with namespaces allowed.
# The class will be generated in the given namespace(s), or if no namespaces are
# given, within the global namespace.
cpp_class = "foo::bar::MatMulComp",
# graph is the input GraphDef proto, by default expected in binary format. To
# use the text format instead, just use the ‘.pbtxt’ suffix. A subgraph will be
# created from this input graph, with feeds as inputs and fetches as outputs.
# No Placeholder or Variable ops may exist in this subgraph.
graph = "test_graph_tfmatmul.pb",
# config is the input Config proto, by default expected in binary format. To
# use the text format instead, use the ‘.pbtxt’ suffix. This is where the
# feeds and fetches were specified above, in the previous step.
config = "test_graph_tfmatmul.config.pbtxt",
)
Pour générer le proto GraphDef (test_graph_tfmatmul.pb) pour cet exemple, exécutez make_test_graphs.py et spécifiez l'emplacement de sortie avec l'indicateur --out_dir.
Les graphiques typiques contiennent Variables
représentant les poids appris via la formation, mais tfcompile
ne peut pas compiler un sous-graphe contenant Variables
. L'outil freeze_graph.py convertit les variables en constantes, en utilisant les valeurs stockées dans un fichier de point de contrôle. Pour plus de commodité, la macro tf_library
prend en charge l'argument freeze_checkpoint
, qui exécute l'outil. Pour plus d'exemples, voir tensorflow/compiler/aot/tests/BUILD .
Les constantes qui apparaissent dans le sous-graphe compilé sont compilées directement dans le code généré. Pour transmettre les constantes dans la fonction générée, plutôt que de les compiler, transmettez-les simplement sous forme de flux.
Pour plus de détails sur la macro de construction tf_library
, consultez tfcompile.bzl .
Pour plus de détails sur l'outil tfcompile
sous-jacent, consultez tfcompile_main.cc .
Étape 3 : Écrivez du code pour appeler le sous-graphe
Cette étape utilise le fichier d'en-tête ( test_graph_tfmatmul.h
) généré par la macro de construction tf_library
à l'étape précédente pour appeler le code généré. Le fichier d'en-tête se trouve dans le répertoire bazel-bin
correspondant au package de build et est nommé en fonction de l'attribut name défini sur la macro de build tf_library
. Par exemple, l'en-tête généré pour test_graph_tfmatmul
serait test_graph_tfmatmul.h
. Vous trouverez ci-dessous une version abrégée de ce qui est généré. Le fichier généré, dans bazel-bin
, contient des commentaires utiles supplémentaires.
namespace foo {
namespace bar {
// MatMulComp represents a computation previously specified in a
// TensorFlow graph, now compiled into executable code.
class MatMulComp {
public:
// AllocMode controls the buffer allocation mode.
enum class AllocMode {
ARGS_RESULTS_AND_TEMPS, // Allocate arg, result and temp buffers
RESULTS_AND_TEMPS_ONLY, // Only allocate result and temp buffers
};
MatMulComp(AllocMode mode = AllocMode::ARGS_RESULTS_AND_TEMPS);
~MatMulComp();
// Runs the computation, with inputs read from arg buffers, and outputs
// written to result buffers. Returns true on success and false on failure.
bool Run();
// Arg methods for managing input buffers. Buffers are in row-major order.
// There is a set of methods for each positional argument.
void** args();
void set_arg0_data(float* data);
float* arg0_data();
float& arg0(size_t dim0, size_t dim1);
void set_arg1_data(float* data);
float* arg1_data();
float& arg1(size_t dim0, size_t dim1);
// Result methods for managing output buffers. Buffers are in row-major order.
// Must only be called after a successful Run call. There is a set of methods
// for each positional result.
void** results();
float* result0_data();
float& result0(size_t dim0, size_t dim1);
};
} // end namespace bar
} // end namespace foo
La classe C++ générée est appelée MatMulComp
dans l'espace de noms foo::bar
, car il s'agissait de la cpp_class
spécifiée dans la macro tf_library
. Toutes les classes générées ont une API similaire, la seule différence étant les méthodes permettant de gérer les tampons d'arguments et de résultats. Ces méthodes diffèrent en fonction du nombre et des types de tampons, qui ont été spécifiés par les arguments feed
et fetch
de la macro tf_library
.
Il existe trois types de tampons gérés au sein de la classe générée : args
représentant les entrées, results
représentant les sorties et temps
représentant les tampons temporaires utilisés en interne pour effectuer le calcul. Par défaut, chaque instance de la classe générée alloue et gère tous ces tampons pour vous. L'argument constructeur AllocMode
peut être utilisé pour modifier ce comportement. Tous les tampons sont alignés sur des limites de 64 octets.
La classe C++ générée n'est qu'un wrapper autour du code de bas niveau généré par XLA.
Exemple d'appel de la fonction générée basée sur tfcompile_test.cc
:
#define EIGEN_USE_THREADS
#define EIGEN_USE_CUSTOM_THREAD_POOL
#include <iostream>
#include "third_party/eigen3/unsupported/Eigen/CXX11/Tensor"
#include "tensorflow/compiler/aot/tests/test_graph_tfmatmul.h" // generated
int main(int argc, char** argv) {
Eigen::ThreadPool tp(2); // Size the thread pool as appropriate.
Eigen::ThreadPoolDevice device(&tp, tp.NumThreads());
foo::bar::MatMulComp matmul;
matmul.set_thread_pool(&device);
// Set up args and run the computation.
const float args[12] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
std::copy(args + 0, args + 6, matmul.arg0_data());
std::copy(args + 6, args + 12, matmul.arg1_data());
matmul.Run();
// Check result
if (matmul.result0(0, 0) == 58) {
std::cout << "Success" << std::endl;
} else {
std::cout << "Failed. Expected value 58 at 0,0. Got:"
<< matmul.result0(0, 0) << std::endl;
}
return 0;
}
Étape 4 : Créer le binaire final
Cette étape combine la bibliothèque générée par tf_library
à l'étape 2 et le code écrit à l'étape 3 pour créer un binaire final. Vous trouverez ci-dessous un exemple de fichier bazel
BUILD.
# Example of linking your binary
# Also see //tensorflow/compiler/aot/tests/BUILD
load("//tensorflow/compiler/aot:tfcompile.bzl", "tf_library")
# The same tf_library call from step 2 above.
tf_library(
name = "test_graph_tfmatmul",
...
)
# The executable code generated by tf_library can then be linked into your code.
cc_binary(
name = "my_binary",
srcs = [
"my_code.cc", # include test_graph_tfmatmul.h to access the generated header
],
deps = [
":test_graph_tfmatmul", # link in the generated object file
"//third_party/eigen3",
],
linkopts = [
"-lpthread",
]
)