Apache MXNet - Componenti di sistema

Qui, i componenti di sistema in Apache MXNet sono spiegati in dettaglio. Innanzitutto, studieremo il motore di esecuzione in MXNet.

Motore di esecuzione

Il motore di esecuzione di Apache MXNet è molto versatile. Possiamo usarlo per il deep learning e per qualsiasi problema specifico del dominio: eseguire un gruppo di funzioni seguendo le loro dipendenze. È progettato in modo tale che le funzioni con dipendenze siano serializzate mentre le funzioni senza dipendenze possono essere eseguite in parallelo.

Interfaccia principale

L'API fornita di seguito è l'interfaccia principale per il motore di esecuzione di 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 di cui sopra ha quanto segue:

  • exec_fun - L'API dell'interfaccia principale di MXNet ci consente di inviare la funzione denominata exec_fun, insieme alle sue informazioni di contesto e dipendenze, al motore di esecuzione.

  • exec_ctx - Le informazioni di contesto in cui deve essere eseguita la suddetta funzione exec_fun.

  • const_vars - Queste sono le variabili da cui legge la funzione.

  • mutate_vars - Queste sono le variabili che devono essere modificate.

Il motore di esecuzione fornisce all'utente la garanzia che l'esecuzione di due funzioni qualsiasi che modificano una variabile comune sia serializzata nel loro ordine push.

Funzione

Di seguito è riportato il tipo di funzione del motore di esecuzione di Apache MXNet:

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

Nella funzione sopra, RunContextcontiene le informazioni di runtime. Le informazioni di runtime dovrebbero essere determinate dal motore di esecuzione. La sintassi diRunContext è il seguente -

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

Di seguito vengono forniti alcuni punti importanti sulle funzioni del motore di esecuzione:

  • Tutte le funzioni vengono eseguite dai thread interni del motore di esecuzione di MXNet.

  • Non è bene spingere il blocco della funzione al motore di esecuzione perché con ciò la funzione occuperà il thread di esecuzione e ridurrà anche il throughput totale.

Per questo MXNet fornisce un'altra funzione asincrona come segue -

using Callback = std::function<void()>;
using AsyncFn = std::function<void(RunContext, Callback)>;
  • In questo AsyncFn possiamo passare la parte pesante dei nostri thread, ma il motore di esecuzione non considera la funzione finita finché non chiamiamo il file callback funzione.

Contesto

Nel Context, possiamo specificare il contesto della funzione da eseguire all'interno. Questo di solito include quanto segue:

  • Se la funzione deve essere eseguita su una CPU o una GPU.

  • Se specifichiamo GPU nel contesto, quale GPU utilizzare.

  • C'è un'enorme differenza tra Context e RunContext. Il contesto ha il tipo di dispositivo e l'ID del dispositivo, mentre RunContext ha le informazioni che possono essere decise solo durante il runtime.

VarHandle

VarHandle, utilizzato per specificare le dipendenze delle funzioni, è come un token (fornito in particolare dal motore di esecuzione) che possiamo utilizzare per rappresentare le risorse esterne che la funzione può modificare o utilizzare.

Ma sorge la domanda, perché dobbiamo usare VarHandle? È perché il motore Apache MXNet è progettato per essere disaccoppiato da altri moduli MXNet.

Di seguito sono riportati alcuni punti importanti su VarHandle:

  • È leggero, quindi creare, eliminare o copiare una variabile comporta costi operativi ridotti.

  • Dobbiamo specificare le variabili immutabili, ovvero le variabili che verranno utilizzate nel file const_vars.

  • Dobbiamo specificare le variabili mutabili, ovvero le variabili che verranno modificate nel file mutate_vars.

  • La regola utilizzata dal motore di esecuzione per risolvere le dipendenze tra le funzioni è che l'esecuzione di due funzioni qualsiasi quando una di esse modifica almeno una variabile comune viene serializzata nel loro ordine push.

  • Per creare una nuova variabile, possiamo usare il NewVar() API.

  • Per eliminare una variabile, possiamo usare il PushDelete API.

Facci capire il suo funzionamento con un semplice esempio:

Supponiamo di avere due funzioni, ovvero F1 e F2, e che entrambe mutino la variabile, ovvero V2. In tal caso, è garantito che F2 venga eseguito dopo F1 se F2 viene premuto dopo F1. D'altra parte, se F1 e F2 utilizzano entrambi V2, il loro effettivo ordine di esecuzione potrebbe essere casuale.

Spingi e aspetta

Push e wait sono due API più utili del motore di esecuzione.

Di seguito sono riportate due importanti caratteristiche di Push API:

  • Tutte le API push sono asincrone, il che significa che la chiamata API ritorna immediatamente indipendentemente dal fatto che la funzione inviata sia terminata o meno.

  • L'API push non è thread-safe, il che significa che solo un thread deve effettuare chiamate API del motore alla volta.

Ora, se parliamo di Wait API, i seguenti punti lo rappresentano:

  • Se un utente desidera attendere il completamento di una funzione specifica, deve includere una funzione di callback nella chiusura. Una volta inclusa, chiama la funzione alla fine della funzione.

  • D'altra parte, se un utente vuole aspettare che tutte le funzioni che coinvolgono una certa variabile finiscano, dovrebbe usare WaitForVar(var) API.

  • Se qualcuno desidera attendere il completamento di tutte le funzioni inviate, utilizzare il WaitForAll () API.

  • Utilizzato per specificare le dipendenze delle funzioni, è come un token.

Operatori

L'operatore in Apache MXNet è una classe che contiene la logica di calcolo effettiva, nonché informazioni ausiliarie e aiuta il sistema a eseguire l'ottimizzazione.

Interfaccia operatore

Forward è l'interfaccia operatore principale la cui sintassi è la seguente:

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 struttura di OpContext, definito in Forward() è come segue:

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

Il OpContextdescrive lo stato dell'operatore (sia in fase di treno che di test), su quale dispositivo deve funzionare l'operatore e anche le risorse richieste. altre due utili API del motore di esecuzione.

Dall'alto Forward interfaccia principale, possiamo comprendere le risorse richieste come segue:

  • in_data e out_data rappresentano i tensori di ingresso e di uscita.

  • req denota come il risultato del calcolo viene scritto nel file out_data.

Il OpReqType può essere definito come -

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

Come Forward , possiamo opzionalmente implementare l'operatore Backward interfaccia come segue -

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);

Vari compiti

Operator l'interfaccia consente agli utenti di eseguire le seguenti attività:

  • L'utente può specificare gli aggiornamenti sul posto e può ridurre i costi di allocazione della memoria

  • Per renderlo più pulito, l'utente può nascondere alcuni argomenti interni a Python.

  • L'utente può definire la relazione tra i tensori e i tensori di uscita.

  • Per eseguire il calcolo, l'utente può acquisire ulteriore spazio temporaneo dal sistema.

Proprietà operatore

Poiché sappiamo che nella rete neurale convoluzionale (CNN), una convoluzione ha diverse implementazioni. Per ottenere le migliori prestazioni da loro, potremmo voler passare tra quelle diverse convoluzioni.

Questo è il motivo per cui Apache MXNet separa l'interfaccia semantica dell'operatore dall'interfaccia di implementazione. Questa separazione avviene sotto forma diOperatorProperty classe che consiste di quanto segue -

InferShape - L'interfaccia InferShape ha due scopi come indicato di seguito:

  • Il primo scopo è indicare al sistema la dimensione di ciascun tensore di input e output in modo che lo spazio possa essere allocato prima Forward e Backward chiamata.

  • Il secondo scopo è eseguire un controllo delle dimensioni per assicurarsi che non ci siano errori prima di eseguire.

La sintassi è data di seguito:

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

Request Resource- E se il tuo sistema fosse in grado di gestire l'area di lavoro di calcolo per operazioni come cudnnConvolutionForward? Il tuo sistema può eseguire ottimizzazioni come il riutilizzo dello spazio e molte altre. Qui, MXNet raggiunge facilmente questo obiettivo con l'aiuto delle seguenti due interfacce:

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

Ma cosa succede se il file ForwardResource e BackwardResourcerestituire array non vuoti? In tal caso, il sistema offre le risorse corrispondenti tramitectx parametro in Forward e Backward interfaccia di Operator.

Backward dependency - Apache MXNet ha le seguenti due diverse firme di operatore per gestire la dipendenza all'indietro -

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);

Qui, i due punti importanti da notare:

  • Out_data in FullyConnectedForward non viene utilizzato da FullyConnectedBackward e

  • PoolingBackward richiede tutti gli argomenti di PoolingForward.

Ecco perché per FullyConnectedForward, il out_datatensore una volta consumato potrebbe essere liberato in modo sicuro perché la funzione di ritorno non ne avrà bisogno. Con l'aiuto di questo sistema è riuscito a raccogliere alcuni tensori come spazzatura il prima possibile.

In place Option- Apache MXNet fornisce un'altra interfaccia agli utenti per risparmiare il costo dell'allocazione della memoria. L'interfaccia è appropriata per operazioni basate sugli elementi in cui sia i tensori di input che quelli di output hanno la stessa forma.

Di seguito è riportata la sintassi per specificare l'aggiornamento sul posto:

Esempio per la creazione di un operatore

Con l'aiuto di OperatorProperty possiamo creare un operatore. A tale scopo, seguire i passaggi indicati di seguito:

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]} }
}

Passo 1

Create Operator

Per prima cosa implementa la seguente interfaccia in OperatorProperty:

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

L'esempio è fornito di seguito:

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

Passo 2

Parameterize Operator

Se si intende implementare un operatore di convoluzione, è obbligatorio conoscere la dimensione del kernel, la dimensione del passo, la dimensione del padding e così via. Perché, perché questi parametri dovrebbero essere passati all'operatore prima di chiamare qualsiasiForward o backward interfaccia.

Per questo, dobbiamo definire un file ConvolutionParam struttura come sotto -

#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;
};

Ora, dobbiamo inserire questo ConvolutionOpProperty e passarlo all'operatore come segue:

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_;
};

Passaggio 3

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

Infine, dobbiamo registrare la classe della proprietà dell'operatore e la classe del parametro su MXNet. Può essere fatto con l'aiuto delle seguenti macro:

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

Nella macro precedente, il primo argomento è la stringa del nome e il secondo è il nome della classe di proprietà.