Apache MXNet - Composants système

Ici, les composants système d'Apache MXNet sont expliqués en détail. Tout d'abord, nous étudierons le moteur d'exécution dans MXNet.

Moteur d'exécution

Le moteur d'exécution d'Apache MXNet est très polyvalent. Nous pouvons l'utiliser pour l'apprentissage en profondeur ainsi que pour tout problème spécifique à un domaine: exécuter un tas de fonctions suivant leurs dépendances. Il est conçu de telle manière que les fonctions avec dépendances sont sérialisées alors que les fonctions sans dépendances peuvent être exécutées en parallèle.

Interface principale

L'API ci-dessous est l'interface principale du moteur d'exécution d'Apache MXNet -

virtual void PushSync(Fn exec_fun, Context exec_ctx,
std::vector<VarHandle> const& const_vars,
std::vector<VarHandle> const& mutate_vars) = 0;

L'API ci-dessus a les éléments suivants -

  • exec_fun - L'API d'interface principale de MXNet nous permet de pousser la fonction nommée exec_fun, ainsi que ses informations de contexte et ses dépendances, vers le moteur d'exécution.

  • exec_ctx - Les informations de contexte dans lesquelles la fonction exec_fun mentionnée ci-dessus doit être exécutée.

  • const_vars - Ce sont les variables à partir desquelles la fonction lit.

  • mutate_vars - Ce sont les variables qui doivent être modifiées.

Le moteur d'exécution offre à son utilisateur la garantie que l'exécution de deux fonctions quelconques qui modifient une variable commune est sérialisée dans leur ordre de transmission.

Fonction

Voici le type de fonction du moteur d'exécution d'Apache MXNet -

using Fn = std::function<void(RunContext)>;

Dans la fonction ci-dessus, RunContextcontient les informations d'exécution. Les informations d'exécution doivent être déterminées par le moteur d'exécution. La syntaxe deRunContext est comme suit -

struct RunContext {
   // stream pointer which could be safely cast to
   // cudaStream_t* type
   void *stream;
};

Vous trouverez ci-dessous quelques points importants sur les fonctions du moteur d'exécution -

  • Toutes les fonctions sont exécutées par les threads internes du moteur d'exécution de MXNet.

  • Il n'est pas bon de pousser le blocage de la fonction vers le moteur d'exécution car avec cela la fonction occupera le thread d'exécution et réduira également le débit total.

Pour cela, MXNet fournit une autre fonction asynchrone comme suit -

using Callback = std::function<void()>;
using AsyncFn = std::function<void(RunContext, Callback)>;
  • Dans ce AsyncFn fonction, nous pouvons passer la partie lourde de nos threads, mais le moteur d'exécution ne considère pas la fonction comme terminée tant que nous n'appelons pas la fonction callback fonction.

Le contexte

Dans Context, nous pouvons spécifier le contexte de la fonction à exécuter dans. Cela comprend généralement les éléments suivants -

  • Si la fonction doit être exécutée sur un CPU ou un GPU.

  • Si nous spécifions GPU dans le contexte, quel GPU utiliser.

  • Il existe une énorme différence entre Context et RunContext. Le contexte a le type d'appareil et l'ID d'appareil, tandis que RunContext a les informations qui ne peuvent être décidées que pendant l'exécution.

VarHandle

VarHandle, utilisé pour spécifier les dépendances des fonctions, est comme un jeton (notamment fourni par le moteur d'exécution) que nous pouvons utiliser pour représenter les ressources externes que la fonction peut modifier ou utiliser.

Mais la question se pose, pourquoi devons-nous utiliser VarHandle? C'est parce que le moteur Apache MXNet est conçu pour être découplé des autres modules MXNet.

Voici quelques points importants sur VarHandle -

  • Il est léger, donc créer, supprimer ou copier une variable entraîne peu de frais d'exploitation.

  • Nous devons spécifier les variables immuables c'est-à-dire les variables qui seront utilisées dans le const_vars.

  • Nous devons spécifier les variables mutables c'est-à-dire les variables qui seront modifiées dans le mutate_vars.

  • La règle utilisée par le moteur d'exécution pour résoudre les dépendances entre les fonctions est que l'exécution de deux fonctions quelconques lorsque l'une d'elles modifie au moins une variable commune est sérialisée dans leur ordre de transmission.

  • Pour créer une nouvelle variable, nous pouvons utiliser le NewVar() API.

  • Pour supprimer une variable, nous pouvons utiliser le PushDelete API.

Comprenons son fonctionnement avec un exemple simple -

Supposons que nous ayons deux fonctions à savoir F1 et F2 et qu'elles mutent toutes les deux la variable à savoir V2. Dans ce cas, F2 est garanti pour être exécuté après F1 si F2 est poussé après F1. De l'autre côté, si F1 et F2 utilisent tous les deux V2, leur ordre d'exécution réel pourrait être aléatoire.

Poussez et attendez

Push et wait sont deux API plus utiles du moteur d'exécution.

Voici deux caractéristiques importantes de Push API:

  • Toutes les API Push sont asynchrones, ce qui signifie que l'appel d'API retourne immédiatement, que la fonction poussée soit terminée ou non.

  • L'API Push n'est pas thread-safe, ce qui signifie qu'un seul thread doit effectuer des appels d'API du moteur à la fois.

Maintenant, si nous parlons d'API Wait, les points suivants le représentent -

  • Si un utilisateur veut attendre qu'une fonction spécifique soit terminée, il / elle doit inclure une fonction de rappel dans la fermeture. Une fois inclus, appelez la fonction à la fin de la fonction.

  • D'un autre côté, si un utilisateur veut attendre que toutes les fonctions impliquant une certaine variable se terminent, il / elle doit utiliser WaitForVar(var) API.

  • Si quelqu'un veut attendre que toutes les fonctions poussées soient terminées, utilisez le WaitForAll () API.

  • Utilisé pour spécifier les dépendances des fonctions, c'est comme un jeton.

Les opérateurs

Operator dans Apache MXNet est une classe qui contient la logique de calcul réelle ainsi que des informations auxiliaires et aide le système à effectuer l'optimisation.

Interface opérateur

Forward est l'interface opérateur principale dont la syntaxe est la suivante:

virtual void Forward(const OpContext &ctx,
const std::vector<TBlob> &in_data,
const std::vector<OpReqType> &req,
const std::vector<TBlob> &out_data,
const std::vector<TBlob> &aux_states) = 0;

La structure de OpContext, défini dans Forward() est comme suit:

struct OpContext {
   int is_train;
   RunContext run_ctx;
   std::vector<Resource> requested;
}

le OpContextdécrit l'état de l'opérateur (que ce soit en train ou en phase de test), sur quel appareil l'opérateur doit être utilisé ainsi que les ressources demandées. deux API plus utiles du moteur d'exécution.

De ce qui précède Forward interface principale, nous pouvons comprendre les ressources demandées comme suit -

  • in_data et out_data représentent les tenseurs d'entrée et de sortie.

  • req indique comment le résultat du calcul est écrit dans le out_data.

le OpReqType peut être défini comme -

enum OpReqType {
   kNullOp,
   kWriteTo,
   kWriteInplace,
   kAddTo
};

Comme Forward opérateur, nous pouvons éventuellement implémenter le Backward interface comme suit -

virtual void Backward(const OpContext &ctx,
const std::vector<TBlob> &out_grad,
const std::vector<TBlob> &in_data,
const std::vector<TBlob> &out_data,
const std::vector<OpReqType> &req,
const std::vector<TBlob> &in_grad,
const std::vector<TBlob> &aux_states);

Diverses tâches

Operator l'interface permet aux utilisateurs d'effectuer les tâches suivantes -

  • L'utilisateur peut spécifier des mises à jour sur place et réduire le coût d'allocation de mémoire

  • Afin de le rendre plus propre, l'utilisateur peut masquer certains arguments internes de Python.

  • L'utilisateur peut définir la relation entre les tenseurs et les tenseurs de sortie.

  • Pour effectuer le calcul, l'utilisateur peut acquérir un espace temporaire supplémentaire du système.

Propriété de l'opérateur

Comme nous sommes conscients que dans le réseau de neurones convolutionnels (CNN), une convolution a plusieurs implémentations. Pour obtenir les meilleures performances de leur part, nous pourrions vouloir basculer entre ces différentes convolutions.

C'est la raison pour laquelle Apache MXNet sépare l'interface sémantique de l'opérateur de l'interface d'implémentation. Cette séparation se fait sous la forme deOperatorProperty classe qui se compose des éléments suivants -

InferShape - L'interface InferShape a deux objectifs comme indiqué ci-dessous:

  • Le premier objectif est d'indiquer au système la taille de chaque tenseur d'entrée et de sortie afin que l'espace puisse être alloué avant Forward et Backward appel.

  • Le deuxième objectif est d'effectuer une vérification de la taille pour s'assurer qu'il n'y a pas d'erreur avant de démarrer.

La syntaxe est donnée ci-dessous -

virtual bool InferShape(mxnet::ShapeVector *in_shape,
mxnet::ShapeVector *out_shape,
mxnet::ShapeVector *aux_shape) const = 0;

Request Resource- Et si votre système pouvait gérer l'espace de travail de calcul pour des opérations telles que cudnnConvolutionForward? Votre système peut effectuer des optimisations telles que la réutilisation de l'espace et bien d'autres. Ici, MXNet y parvient facilement en suivant deux interfaces -

virtual std::vector<ResourceRequest> ForwardResource(
   const mxnet::ShapeVector &in_shape) const;
virtual std::vector<ResourceRequest> BackwardResource(
   const mxnet::ShapeVector &in_shape) const;

Mais, que faire si le ForwardResource et BackwardResourcerenvoyer des tableaux non vides? Dans ce cas, le système offre les ressources correspondantes viactx paramètre dans le Forward et Backward interface de Operator.

Backward dependency - Apache MXNet utilise deux signatures d'opérateurs différentes pour gérer la dépendance vers l'arrière -

void FullyConnectedForward(TBlob weight, TBlob in_data, TBlob out_data);
void FullyConnectedBackward(TBlob weight, TBlob in_data, TBlob out_grad, TBlob in_grad);
void PoolingForward(TBlob in_data, TBlob out_data);
void PoolingBackward(TBlob in_data, TBlob out_data, TBlob out_grad, TBlob in_grad);

Ici, les deux points importants à noter -

  • Le out_data dans FullyConnectedForward n'est pas utilisé par FullyConnectedBackward, et

  • PoolingBackward requiert tous les arguments de PoolingForward.

C'est pourquoi pour FullyConnectedForward, la out_datatenseur une fois consommé pourrait être libéré en toute sécurité car la fonction arrière n'en aura pas besoin. Avec l'aide de ce système, j'ai pu collecter des tenseurs le plus tôt possible.

In place Option- Apache MXNet fournit une autre interface aux utilisateurs pour économiser le coût d'allocation de mémoire. L'interface convient aux opérations élémentaires dans lesquelles les tenseurs d'entrée et de sortie ont la même forme.

Voici la syntaxe pour spécifier la mise à jour sur place -

Exemple de création d'un opérateur

Avec l'aide d'OperatorProperty, nous pouvons créer un opérateur. Pour ce faire, suivez les étapes ci-dessous -

virtual std::vector<std::pair<int, void*>> ElewiseOpProperty::ForwardInplaceOption(
   const std::vector<int> &in_data,
   const std::vector<void*> &out_data) 
const {
   return { {in_data[0], out_data[0]} };
}
virtual std::vector<std::pair<int, void*>> ElewiseOpProperty::BackwardInplaceOption(
   const std::vector<int> &out_grad,
   const std::vector<int> &in_data,
   const std::vector<int> &out_data,
   const std::vector<void*> &in_grad) 
const {
   return { {out_grad[0], in_grad[0]} }
}

Étape 1

Create Operator

Commencez par implémenter l'interface suivante dans OperatorProperty:

virtual Operator* CreateOperator(Context ctx) const = 0;

L'exemple est donné ci-dessous -

class ConvolutionOp {
   public:
      void Forward( ... ) { ... }
      void Backward( ... ) { ... }
};
class ConvolutionOpProperty : public OperatorProperty {
   public:
      Operator* CreateOperator(Context ctx) const {
         return new ConvolutionOp;
      }
};

Étape 2

Parameterize Operator

Si vous allez implémenter un opérateur de convolution, il est obligatoire de connaître la taille du noyau, la taille de la foulée, la taille du rembourrage, etc. Pourquoi, car ces paramètres doivent être passés à l'opérateur avant d'appelerForward ou backward interface.

Pour cela, nous devons définir un ConvolutionParam structure comme ci-dessous -

#include <dmlc/parameter.h>
struct ConvolutionParam : public dmlc::Parameter<ConvolutionParam> {
   mxnet::TShape kernel, stride, pad;
   uint32_t num_filter, num_group, workspace;
   bool no_bias;
};

Maintenant, nous devons mettre ça dans ConvolutionOpProperty et passez-le à l'opérateur comme suit -

class ConvolutionOp {
   public:
      ConvolutionOp(ConvolutionParam p): param_(p) {}
      void Forward( ... ) { ... }
      void Backward( ... ) { ... }
   private:
      ConvolutionParam param_;
};
class ConvolutionOpProperty : public OperatorProperty {
   public:
      void Init(const vector<pair<string, string>& kwargs) {
         // initialize param_ using kwargs
      }
      Operator* CreateOperator(Context ctx) const {
         return new ConvolutionOp(param_);
      }
   private:
      ConvolutionParam param_;
};

Étape 3

Register the Operator Property Class and the Parameter Class to Apache MXNet

Enfin, nous devons enregistrer la classe de propriété de l'opérateur et la classe de paramètre dans MXNet. Cela peut être fait à l'aide des macros suivantes -

DMLC_REGISTER_PARAMETER(ConvolutionParam);
MXNET_REGISTER_OP_PROPERTY(Convolution, ConvolutionOpProperty);

Dans la macro ci-dessus, le premier argument est la chaîne de nom et le second est le nom de la classe de propriété.