API Neural Networks

L'API Android Neural Networks (NNAPI) è un'API Android C progettata per eseguire operazioni ad alta intensità di calcolo per il machine learning sui dispositivi Android. NNAPI è progettata per fornire un livello di funzionalità di base per framework di machine learning di livello superiore, come TensorFlow Lite e Caffe2, che creano e addestrano reti neurali. L'API è disponibile su tutti i dispositivi Android con Android 8.1 (livello API 27) o versioni successive.

NNAPI supporta l'inferenza applicando i dati dei dispositivi Android a modelli definiti dallo sviluppatore in precedenza. Gli esempi di inferenza includono la classificazione delle immagini, la previsione del comportamento degli utenti e la selezione di risposte appropriate a una query di ricerca.

L'inferenza sul dispositivo offre molti vantaggi:

  • Latenza: non è necessario inviare una richiesta tramite una connessione di rete e attendere una risposta. Questo può essere fondamentale per le applicazioni video che elaborano fotogrammi successivi da una videocamera.
  • Disponibilità: l'applicazione funziona anche al di fuori della copertura di rete.
  • Velocità: il nuovo hardware specifico per l'elaborazione di rete neurale fornisce calcoli significativamente più rapidi rispetto a una CPU per uso generico da sola.
  • Privacy. I dati rimangono sul dispositivo Android.
  • Costo: non è necessaria alcuna server farm quando tutti i calcoli vengono eseguiti sul dispositivo Android.

Esistono anche dei compromessi che uno sviluppatore deve tenere presente:

  • Utilizzo del sistema: la valutazione delle reti neurali comporta un elevato calcolo, che potrebbe aumentare l'utilizzo della batteria. Dovresti considerare di monitorare l'integrità della batteria se questo è un problema per la tua app, soprattutto per i calcoli a lunga esecuzione.
  • Dimensioni applicazione: presta attenzione alle dimensioni dei modelli. I modelli possono occupare diversi megabyte di spazio. Se il raggruppamento di modelli di grandi dimensioni nell'APK potrebbe avere un impatto indebitamente negativo sugli utenti, ti consigliamo di scaricarli dopo l'installazione dell'app, di utilizzare modelli più piccoli o di eseguire i calcoli nel cloud. NNAPI non fornisce funzionalità per l'esecuzione di modelli nel cloud.

Consulta l'esempio di API Android Neural Networks per vedere un esempio di come utilizzare NNAPI.

Informazioni sul runtime dell'API Neural Networks

NNAPI è stata concepita per essere chiamata da librerie, framework e strumenti di machine learning che consentono agli sviluppatori di addestrare i propri modelli fuori dal dispositivo e di eseguirne il deployment sui dispositivi Android. In genere le app non utilizzano direttamente NNAPI, ma utilizzano invece framework di machine learning di livello superiore. Questi framework a loro volta possono utilizzare NNAPI per eseguire operazioni di inferenza con accelerazione hardware sui dispositivi supportati.

In base ai requisiti di un'app e alle funzionalità hardware di un dispositivo Android, il runtime della rete neurale di Android può distribuire in modo efficiente il carico di lavoro di calcolo sui processori disponibili sul dispositivo, inclusi l'hardware di rete neurale dedicato, le GPU (Graphics Processing Unit) e i processori di segnale digitali (DSP).

Per i dispositivi Android privi di un driver del fornitore specializzato, il runtime NNAPI esegue le richieste sulla CPU.

La Figura 1 mostra l'architettura di sistema di alto livello per NNAPI.

Figura 1. Architettura di sistema per l'API Android Neural Networks

Modello di programmazione API Neural Networks

Per eseguire calcoli utilizzando NNAPI, devi prima creare un grafico diretto che definisca i calcoli da eseguire. Questo grafico di calcolo, combinato con i dati di input (ad esempio, ponderazioni e bias trasmessi da un framework di machine learning), forma il modello per la valutazione del runtime NNAPI.

NNAPI utilizza quattro astrazioni principali:

  • Modello: un grafico di calcolo delle operazioni matematiche e dei valori costanti appresi durante un processo di addestramento. Queste operazioni sono specifiche delle reti neurali. Includono convoluzione bidimensionale (2D), attivazione logistica (sigmoid), attivazione lineare rettificata (ReLU) e altro ancora. La creazione di un modello è un'operazione sincrona. Una volta creato correttamente, può essere riutilizzato in tutti i thread e le compilation. In NNAPI, un modello è rappresentato come un'istanza ANeuralNetworksModel.
  • Compilation: rappresenta una configurazione per compilare un modello NNAPI in codice di livello inferiore. La creazione di una compilazione è un'operazione sincrona. Una volta creato correttamente, può essere riutilizzato in più thread ed esecuzioni. In NNAPI, ogni compilazione è rappresentata come un'istanza ANeuralNetworksCompilation.
  • Memoria: rappresenta la memoria condivisa, i file mappati di memoria e buffer di memoria simili. L'utilizzo di un buffer di memoria consente al runtime NNAPI di trasferire i dati ai driver in modo più efficiente. In genere, un'app crea un buffer di memoria condiviso contenente tutti i tensori necessari per definire un modello. Puoi anche utilizzare i buffer di memoria per archiviare gli input e gli output per un'istanza di esecuzione. In NNAPI, ogni buffer di memoria è rappresentato come un'istanza ANeuralNetworksMemory.
  • Esecuzione: interfaccia per l'applicazione di un modello NNAPI a un insieme di input e per la raccolta dei risultati. L'esecuzione può essere eseguita in modo sincrono o asincrono.

    Per l'esecuzione asincrona, più thread possono attendere la stessa esecuzione. Al termine di questa esecuzione, tutti i thread vengono rilasciati.

    In NNAPI, ogni esecuzione è rappresentata come un'istanza ANeuralNetworksExecution.

La figura 2 mostra il flusso di programmazione di base.

Figura 2. Flusso di programmazione per l'API Android Neural Networks

Il resto di questa sezione descrive i passaggi per impostare il modello NNAPI per eseguire il calcolo, compilare il modello ed eseguire il modello compilato.

Fornire accesso ai dati di addestramento

I dati addestrati su ponderazioni e bias sono probabilmente archiviati in un file. Per fornire al runtime NNAPI un accesso efficiente a questi dati, crea un'istanza ANeuralNetworksMemory chiamando la funzione ANeuralNetworksMemory_createFromFd() e passando il descrittore del file del file di dati aperto. Puoi specificare anche i flag di protezione della memoria e un offset in cui inizia la regione della memoria condivisa nel file.

// Create a memory buffer from the file that contains the trained data
ANeuralNetworksMemory* mem1 = NULL;
int fd = open("training_data", O_RDONLY);
ANeuralNetworksMemory_createFromFd(file_size, PROT_READ, fd, 0, &mem1);

Anche se in questo esempio viene utilizzata una sola istanza di ANeuralNetworksMemory per tutte le ponderazioni, è possibile utilizzare più istanze ANeuralNetworksMemory per più file.

Utilizza buffer hardware nativi

Puoi utilizzare buffer hardware nativi per input, output e valori di operandi costanti del modello. In alcuni casi, un acceleratore NNAPI può accedere agli oggetti AHardwareBuffer senza che il driver debba copiare i dati. AHardwareBuffer ha molte configurazioni diverse e non tutti gli acceleratori NNAPI potrebbero supportare tutte queste configurazioni. A causa di questa limitazione, fai riferimento ai vincoli elencati nella documentazione di riferimento di ANeuralNetworksMemory_createFromAHardwareBuffer ed esegui dei test in anticipo sui dispositivi di destinazione per assicurarti che le compilazioni e le esecuzioni che utilizzano AHardwareBuffer si comportino come previsto, utilizzando l'assegnazione del dispositivo per specificare l'acceleratore.

Per consentire al runtime NNAPI di accedere a un oggetto AHardwareBuffer, crea un'istanza ANeuralNetworksMemory chiamando la funzione ANeuralNetworksMemory_createFromAHardwareBuffer e passando l'oggetto AHardwareBuffer, come mostrato nel seguente esempio di codice:

// Configure and create AHardwareBuffer object
AHardwareBuffer_Desc desc = ...
AHardwareBuffer* ahwb = nullptr;
AHardwareBuffer_allocate(&desc, &ahwb);

// Create ANeuralNetworksMemory from AHardwareBuffer
ANeuralNetworksMemory* mem2 = NULL;
ANeuralNetworksMemory_createFromAHardwareBuffer(ahwb, &mem2);

Quando NNAPI non deve più accedere all'oggetto AHardwareBuffer, libera l'istanza ANeuralNetworksMemory corrispondente:

ANeuralNetworksMemory_free(mem2);

Nota :

  • Puoi utilizzare AHardwareBuffer solo per l'intero buffer; non puoi utilizzarlo con un parametro ARect.
  • Il runtime NNAPI non eliminerà il buffer. Prima di pianificare l'esecuzione, devi assicurarti che i buffer di input e di output siano accessibili.
  • Non sono supportati i descrittori dei file del fence di sincronizzazione.
  • Per un oggetto AHardwareBuffer con formati e bit di utilizzo specifici del fornitore, spetta all'implementazione del fornitore determinare se il client o il driver è responsabile dello svuotamento della cache.

Modello

Un modello è l'unità di calcolo fondamentale in NNAPI. Ogni modello è definito da uno o più operandi e operazioni.

Operatori

Gli operandi sono oggetti di dati utilizzati nella definizione del grafico. che includono gli input e gli output del modello, i nodi intermedi che contengono i dati che passano da un'operazione all'altra e le costanti passate a queste operazioni.

Esistono due tipi di operandi che possono essere aggiunti ai modelli NNAPI: scalari e tensori.

Uno scalare rappresenta un singolo valore. NNAPI supporta valori scalari nei formati booleano, in virgola mobile a 16 bit, in virgola mobile a 32 bit, in numeri interi a 32 bit e intero a 32 bit senza segno.

La maggior parte delle operazioni in NNAPI prevede tensori. I tensori sono array n-dimensionali. NNAPI supporta tensori con valori in virgola mobile a 16 bit, in virgola mobile a 32 bit, quantizzati a 8 bit, quantiizzati a 16 bit, numeri interi a 32 bit e valori booleani a 8 bit.

Ad esempio, la figura 3 rappresenta un modello con due operazioni: un'addizione seguita da una moltiplicazione. Il modello prende un tensore di input e produce un tensore di output.

Figura 3. Esempio di operandi per un modello NNAPI

Il modello riportato sopra ha sette operandi. Questi operandi sono identificati implicitamente dall'indice dell'ordine in cui vengono aggiunti al modello. Il primo operando aggiunto ha un indice pari a 0, il secondo è un indice pari a 1 e così via. Gli operandi 1, 2, 3 e 5 sono operandi costanti.

L'ordine in cui aggiungi gli operandi non è importante. Ad esempio, l'operando di output del modello potrebbe essere il primo aggiunto. La parte importante è utilizzare il valore di indice corretto quando fai riferimento a un operando.

Gli operandi hanno un tipo. Questi vengono specificati quando vengono aggiunti al modello.

Un operando non può essere utilizzato sia come input che come output di un modello.

Ogni operando deve essere un input del modello, una costante o l'operando di output di esattamente un'operazione.

Per ulteriori informazioni sull'utilizzo degli operandi, consulta Ulteriori informazioni sugli operandi.

Fasi operative

Un'operazione specifica i calcoli da eseguire. Ogni operazione è costituita dai seguenti elementi:

  • un tipo di operazione (ad es. addizione, moltiplicazione, convoluzione)
  • un elenco di indici degli operandi utilizzati dall'operazione per l'input;
  • un elenco di indici degli operandi utilizzati dall'operazione per l'output.

L'ordine in questi elenchi è importante. Consulta il riferimento dell'API NNAPI per gli input e gli output previsti di ogni tipo di operazione.

Prima di aggiungere l'operazione, devi aggiungere al modello gli operandi utilizzati o prodotti da un'operazione.

L'ordine in cui aggiungi le operazioni non è importante. NNAPI si basa sulle dipendenze stabilite dal grafico di calcolo degli operandi e delle operazioni per determinare l'ordine in cui vengono eseguite le operazioni.

Le operazioni supportate da NNAPI sono riassunte nella tabella seguente:

Categoria Fasi operative
Operazioni matematiche a livello di elemento
Manipolazione dei tensori
Operazioni immagine
Operazioni di ricerca
Operazioni di normalizzazione
Operazioni di convoluzione
Operazioni di pooling
Operazioni di attivazione
Altre operazioni

Problema noto nel livello API 28: quando si passano i tensori ANEURALNETWORKS_TENSOR_QUANT8_ASYMM all'operazione ANEURALNETWORKS_PAD, disponibile su Android 9 (livello API 28) e versioni successive, l'output di NNAPI potrebbe non corrispondere all'output di framework di machine learning di livello superiore, come TensorFlow Lite. Dovresti invece trasmettere solo ANEURALNETWORKS_TENSOR_FLOAT32. Il problema è stato risolto in Android 10 (livello API 29) e versioni successive.

Crea modelli

Nell'esempio seguente, creiamo il modello a due operazioni riportato nella Figura 3.

Per creare il modello:

  1. Richiama la funzione ANeuralNetworksModel_create() per definire un modello vuoto.

    ANeuralNetworksModel* model = NULL;
    ANeuralNetworksModel_create(&model);
    
  2. Aggiungi gli operandi al modello chiamando ANeuralNetworks_addOperand(). I tipi di dati vengono definiti utilizzando la struttura dei dati di ANeuralNetworksOperandType.

    // In our example, all our tensors are matrices of dimension [3][4]
    ANeuralNetworksOperandType tensor3x4Type;
    tensor3x4Type.type = ANEURALNETWORKS_TENSOR_FLOAT32;
    tensor3x4Type.scale = 0.f;    // These fields are used for quantized tensors
    tensor3x4Type.zeroPoint = 0;  // These fields are used for quantized tensors
    tensor3x4Type.dimensionCount = 2;
    uint32_t dims[2] = {3, 4};
    tensor3x4Type.dimensions = dims;

    // We also specify operands that are activation function specifiers ANeuralNetworksOperandType activationType; activationType.type = ANEURALNETWORKS_INT32; activationType.scale = 0.f; activationType.zeroPoint = 0; activationType.dimensionCount = 0; activationType.dimensions = NULL;

    // Now we add the seven operands, in the same order defined in the diagram ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 0 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 1 ANeuralNetworksModel_addOperand(model, &activationType); // operand 2 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 3 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 4 ANeuralNetworksModel_addOperand(model, &activationType); // operand 5 ANeuralNetworksModel_addOperand(model, &tensor3x4Type); // operand 6
  3. Per gli operandi che hanno valori costanti, ad esempio ponderazioni e bias che l'app ottiene da un processo di addestramento, utilizza le funzioni ANeuralNetworksModel_setOperandValue() e ANeuralNetworksModel_setOperandValueFromMemory().

    Nell'esempio seguente, impostiamo i valori costanti dal file di dati di addestramento corrispondenti al buffer di memoria creato nel passaggio Fornisci l'accesso ai dati di addestramento.

    // In our example, operands 1 and 3 are constant tensors whose values were
    // established during the training process
    const int sizeOfTensor = 3 * 4 * 4;    // The formula for size calculation is dim0 * dim1 * elementSize
    ANeuralNetworksModel_setOperandValueFromMemory(model, 1, mem1, 0, sizeOfTensor);
    ANeuralNetworksModel_setOperandValueFromMemory(model, 3, mem1, sizeOfTensor, sizeOfTensor);

    // We set the values of the activation operands, in our example operands 2 and 5 int32_t noneValue = ANEURALNETWORKS_FUSED_NONE; ANeuralNetworksModel_setOperandValue(model, 2, &noneValue, sizeof(noneValue)); ANeuralNetworksModel_setOperandValue(model, 5, &noneValue, sizeof(noneValue));
  4. Per ogni operazione che vuoi calcolare nel grafico diretto, aggiungi l'operazione al modello chiamando la funzione ANeuralNetworksModel_addOperation().

    Come parametri di questa chiamata, la tua app deve fornire:

    • Il tipo di operazione
    • il conteggio dei valori di input
    • l'array degli indici per gli operandi di input
    • il conteggio dei valori di output
    • l'array degli indici per gli operandi di output

    Tieni presente che non è possibile utilizzare un operando sia per l'input sia per l'output della stessa operazione.

    // We have two operations in our example
    // The first consumes operands 1, 0, 2, and produces operand 4
    uint32_t addInputIndexes[3] = {1, 0, 2};
    uint32_t addOutputIndexes[1] = {4};
    ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_ADD, 3, addInputIndexes, 1, addOutputIndexes);

    // The second consumes operands 3, 4, 5, and produces operand 6 uint32_t multInputIndexes[3] = {3, 4, 5}; uint32_t multOutputIndexes[1] = {6}; ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_MUL, 3, multInputIndexes, 1, multOutputIndexes);
  5. Identifica gli operandi che il modello dovrebbe considerare come input e output richiamando la funzione ANeuralNetworksModel_identifyInputsAndOutputs().

    // Our model has one input (0) and one output (6)
    uint32_t modelInputIndexes[1] = {0};
    uint32_t modelOutputIndexes[1] = {6};
    ANeuralNetworksModel_identifyInputsAndOutputs(model, 1, modelInputIndexes, 1 modelOutputIndexes);
    
  6. Facoltativamente, specifica se è consentito calcolare ANEURALNETWORKS_TENSOR_FLOAT32 con un intervallo o con una precisione pari a quelli del formato in virgola mobile a 16 bit IEEE 754 chiamando ANeuralNetworksModel_relaxComputationFloat32toFloat16().

  7. Chiama ANeuralNetworksModel_finish() per finalizzare la definizione del modello. Se non sono presenti errori, questa funzione restituisce un codice risultato di ANEURALNETWORKS_NO_ERROR.

    ANeuralNetworksModel_finish(model);
    

Una volta creato un modello, puoi compilarlo tutte le volte che vuoi ed eseguire ogni compilazione tutte le volte che vuoi.

Flusso di controllo

Per incorporare il flusso di controllo in un modello NNAPI:

  1. Crea i sottografi di esecuzione corrispondenti (sottografi then e else per un'istruzione IF, sottografi condition e body per un loop WHILE) come modelli ANeuralNetworksModel* autonomi:

    ANeuralNetworksModel* thenModel = makeThenModel();
    ANeuralNetworksModel* elseModel = makeElseModel();
    
  2. Crea operandi che facciano riferimento a questi modelli all'interno del modello contenente il flusso di controllo:

    ANeuralNetworksOperandType modelType = {
        .type = ANEURALNETWORKS_MODEL,
    };
    ANeuralNetworksModel_addOperand(model, &modelType);  // kThenOperandIndex
    ANeuralNetworksModel_addOperand(model, &modelType);  // kElseOperandIndex
    ANeuralNetworksModel_setOperandValueFromModel(model, kThenOperandIndex, &thenModel);
    ANeuralNetworksModel_setOperandValueFromModel(model, kElseOperandIndex, &elseModel);
    
  3. Aggiungi l'operazione del flusso di controllo:

    uint32_t inputs[] = {kConditionOperandIndex,
                         kThenOperandIndex,
                         kElseOperandIndex,
                         kInput1, kInput2, kInput3};
    uint32_t outputs[] = {kOutput1, kOutput2};
    ANeuralNetworksModel_addOperation(model, ANEURALNETWORKS_IF,
                                      std::size(inputs), inputs,
                                      std::size(output), outputs);
    

Compilation

Il passaggio di compilazione determina su quali processori verrà eseguito il modello e chiede ai driver corrispondenti di prepararsi per l'esecuzione. Ciò potrebbe includere la generazione di codice macchina specifico per i processori su cui verrà eseguito il modello.

Per compilare un modello, segui questi passaggi:

  1. Chiama la funzione ANeuralNetworksCompilation_create() per creare una nuova istanza di compilazione.

    // Compile the model
    ANeuralNetworksCompilation* compilation;
    ANeuralNetworksCompilation_create(model, &compilation);
    

    Facoltativamente, puoi utilizzare l'assegnazione dei dispositivi per scegliere esplicitamente su quali dispositivi eseguire l'esecuzione.

  2. Facoltativamente, puoi influenzare il rapporto di runtime tra l'utilizzo della batteria e la velocità di esecuzione. Puoi farlo chiamando il numero ANeuralNetworksCompilation_setPreference().

    // Ask to optimize for low power consumption
    ANeuralNetworksCompilation_setPreference(compilation, ANEURALNETWORKS_PREFER_LOW_POWER);
    

    Le preferenze che puoi specificare includono:

    • ANEURALNETWORKS_PREFER_LOW_POWER: Preferisco eseguire l'operazione in modo da ridurre al minimo il consumo della batteria. Questo è utile per le compilazioni eseguite spesso.
    • ANEURALNETWORKS_PREFER_FAST_SINGLE_ANSWER: Preferisci restituire una singola risposta il più rapidamente possibile, anche se ciò causa un maggiore consumo di energia. Questa è l'impostazione predefinita.
    • ANEURALNETWORKS_PREFER_SUSTAINED_SPEED: Preferisci massimizzare la velocità effettiva dei frame successivi, ad esempio durante l'elaborazione di frame successivi provenienti dalla fotocamera.
  3. Facoltativamente, puoi configurare la memorizzazione nella cache della compilazione chiamando ANeuralNetworksCompilation_setCaching.

    // Set up compilation caching
    ANeuralNetworksCompilation_setCaching(compilation, cacheDir, token);
    

    Usa getCodeCacheDir() per cacheDir. Il valore token specificato deve essere univoco per ogni modello all'interno dell'applicazione.

  4. Finalizza la definizione della compilazione chiamando ANeuralNetworksCompilation_finish(). Se non sono presenti errori, questa funzione restituisce un codice risultato di ANEURALNETWORKS_NO_ERROR.

    ANeuralNetworksCompilation_finish(compilation);
    

Rilevamento e assegnazione del dispositivo

Sui dispositivi Android con Android 10 (livello API 29) e versioni successive, NNAPI fornisce funzioni che consentono alle app e alle librerie dei framework di machine learning di ottenere informazioni sui dispositivi disponibili e specificare i dispositivi da utilizzare per l'esecuzione. Se fornisci informazioni sui dispositivi disponibili, le app possono ottenere la versione esatta dei driver trovati su un dispositivo per evitare incompatibilità note. Fornendo alle app la possibilità di specificare quali dispositivi devono eseguire diverse sezioni di un modello, le app possono essere ottimizzate per il dispositivo Android su cui vengono distribuite.

Rilevamento dispositivi

Utilizza ANeuralNetworks_getDeviceCount per ottenere il numero di dispositivi disponibili. Per ogni dispositivo, utilizza ANeuralNetworks_getDevice per impostare un'istanza ANeuralNetworksDevice come riferimento a quel dispositivo.

Una volta ottenuto un riferimento a un dispositivo, puoi trovare ulteriori informazioni su quel dispositivo utilizzando le seguenti funzioni:

Assegnazione dispositivo

Utilizza ANeuralNetworksModel_getSupportedOperationsForDevices per scoprire quali operazioni di un modello possono essere eseguite su dispositivi specifici.

Per controllare quali acceleratori utilizzare per l'esecuzione, chiama ANeuralNetworksCompilation_createForDevices al posto di ANeuralNetworksCompilation_create. Utilizza l'oggetto ANeuralNetworksCompilation risultante, come di consueto. La funzione restituisce un errore se il modello fornito contiene operazioni non supportate dai dispositivi selezionati.

Se vengono specificati più dispositivi, il runtime è responsabile della distribuzione del lavoro su tutti i dispositivi.

Analogamente ad altri dispositivi, l'implementazione della CPU NNAPI è rappresentata da un ANeuralNetworksDevice con il nome nnapi-reference e il tipo ANEURALNETWORKS_DEVICE_TYPE_CPU. Quando chiami ANeuralNetworksCompilation_createForDevices, l'implementazione della CPU non viene utilizzata per gestire i casi di errore per la compilazione e l'esecuzione del modello.

È responsabilità di un'applicazione partizionare un modello in modelli secondari che possono essere eseguiti sui dispositivi specificati. Le applicazioni che non hanno bisogno di eseguire il partizionamento manuale devono continuare a chiamare ANeuralNetworksCompilation_create più facilmente per utilizzare tutti i dispositivi disponibili (compresa la CPU) per accelerare il modello. Se il modello non è stato completamente supportato dai dispositivi specificati utilizzando ANeuralNetworksCompilation_createForDevices, viene restituito ANEURALNETWORKS_BAD_DATA.

Partizionamento del modello

Quando sono disponibili più dispositivi per il modello, il runtime NNAPI distribuisce il lavoro tra i dispositivi. Ad esempio, se a ANeuralNetworksCompilation_createForDevices è stato fornito più di un dispositivo, tutti quelli specificati verranno presi in considerazione al momento dell'allocazione del lavoro. Tieni presente che, se il dispositivo CPU non è nell'elenco, l'esecuzione della CPU sarà disabilitata. Quando utilizzi ANeuralNetworksCompilation_create, verranno presi in considerazione tutti i dispositivi disponibili, inclusa la CPU.

La distribuzione viene effettuata selezionando dall'elenco dei dispositivi disponibili, per ciascuna operazione nel modello, il dispositivo che supporta l'operazione e dichiarando le prestazioni migliori, ovvero il tempo di esecuzione più rapido o il consumo energetico più basso, a seconda della preferenza di esecuzione specificata dal client. Questo algoritmo di partizionamento non tiene conto delle possibili inefficienze causate dall'I/O tra i diversi processori. Di conseguenza, quando specifichi più processori (in modo esplicito quando si utilizza ANeuralNetworksCompilation_createForDevices o in modo implicito con ANeuralNetworksCompilation_create), è importante profilare l'applicazione risultante.

Per capire in che modo il modello è stato partizionato da NNAPI, verifica se nei log Android è presente un messaggio (a livello di informazioni con il tag ExecutionPlan):

ModelBuilder::findBestDeviceForEachOperation(op-name): device-index

op-name è il nome descrittivo dell'operazione nel grafico e device-index è l'indice del dispositivo candidato nell'elenco dei dispositivi. Questo elenco è l'input fornito a ANeuralNetworksCompilation_createForDevices o, se utilizzi ANeuralNetworksCompilation_createForDevices, l'elenco dei dispositivi restituito durante l'iterazione su tutti i dispositivi che utilizzano ANeuralNetworks_getDeviceCount e ANeuralNetworks_getDevice.

Il messaggio (a livello INFO con tag ExecutionPlan):

ModelBuilder::partitionTheWork: only one best device: device-name

Questo messaggio indica che l'intero grafico è stato accelerato sul dispositivo device-name.

Attuazione

Il passaggio di esecuzione applica il modello a un insieme di input e archivia gli output di calcolo in uno o più buffer utente o spazi di memoria allocati dall'app.

Per eseguire un modello compilato, segui questi passaggi:

  1. Chiama la funzione ANeuralNetworksExecution_create() per creare una nuova istanza di esecuzione.

    // Run the compiled model against a set of inputs
    ANeuralNetworksExecution* run1 = NULL;
    ANeuralNetworksExecution_create(compilation, &run1);
    
  2. Specifica dove la tua app legge i valori di input per il calcolo. L'app può leggere i valori di input da un buffer utente o da uno spazio di memoria allocato chiamando rispettivamente ANeuralNetworksExecution_setInput() o ANeuralNetworksExecution_setInputFromMemory().

    // Set the single input to our sample model. Since it is small, we won't use a memory buffer
    float32 myInput[3][4] = { ...the data... };
    ANeuralNetworksExecution_setInput(run1, 0, NULL, myInput, sizeof(myInput));
    
  3. Specifica dove la tua app scrive i valori di output. L'app può scrivere valori di output in un buffer utente o in uno spazio di memoria allocato, chiamando rispettivamente ANeuralNetworksExecution_setOutput() o ANeuralNetworksExecution_setOutputFromMemory().

    // Set the output
    float32 myOutput[3][4];
    ANeuralNetworksExecution_setOutput(run1, 0, NULL, myOutput, sizeof(myOutput));
    
  4. Pianifica l'inizio dell'esecuzione chiamando la funzione ANeuralNetworksExecution_startCompute(). Se non sono presenti errori, questa funzione restituisce un codice risultato di ANEURALNETWORKS_NO_ERROR.

    // Starts the work. The work proceeds asynchronously
    ANeuralNetworksEvent* run1_end = NULL;
    ANeuralNetworksExecution_startCompute(run1, &run1_end);
    
  5. Richiama la funzione ANeuralNetworksEvent_wait() per attendere il completamento dell'esecuzione. Se l'esecuzione è riuscita, questa funzione restituisce un codice risultato di ANEURALNETWORKS_NO_ERROR. L'attesa può essere eseguita su un thread diverso da quello che avvia l'esecuzione.

    // For our example, we have no other work to do and will just wait for the completion
    ANeuralNetworksEvent_wait(run1_end);
    ANeuralNetworksEvent_free(run1_end);
    ANeuralNetworksExecution_free(run1);
    
  6. Se vuoi, puoi applicare un insieme diverso di input al modello compilato utilizzando la stessa istanza di compilazione per creare una nuova istanza ANeuralNetworksExecution.

    // Apply the compiled model to a different set of inputs
    ANeuralNetworksExecution* run2;
    ANeuralNetworksExecution_create(compilation, &run2);
    ANeuralNetworksExecution_setInput(run2, ...);
    ANeuralNetworksExecution_setOutput(run2, ...);
    ANeuralNetworksEvent* run2_end = NULL;
    ANeuralNetworksExecution_startCompute(run2, &run2_end);
    ANeuralNetworksEvent_wait(run2_end);
    ANeuralNetworksEvent_free(run2_end);
    ANeuralNetworksExecution_free(run2);
    

Esecuzione sincrona

L'esecuzione asincrona impiega del tempo per generare e sincronizzare i thread. Inoltre, la latenza può essere estremamente variabile, con i ritardi più lunghi che raggiungono fino a 500 microsecondi tra il momento in cui viene inviata la notifica o la riattivazione di un thread e il momento in cui viene infine associato a un core della CPU.

Per migliorare la latenza, puoi invece indirizzare un'applicazione a effettuare una chiamata di inferenza sincrona al runtime. La chiamata restituirà un risultato solo al completamento di un'inferenza, anziché all'avvio. Invece di chiamare ANeuralNetworksExecution_startCompute per una chiamata di inferenza asincrona al runtime, l'applicazione chiama ANeuralNetworksExecution_compute per effettuare una chiamata sincrona al runtime. Una chiamata al numero ANeuralNetworksExecution_compute non accetta un ANeuralNetworksEvent e non è accoppiata a una chiamata al numero ANeuralNetworksEvent_wait.

Esecuzioni burst

Sui dispositivi Android con Android 10 (livello API 29) e versioni successive, NNAPI supporta le esecuzioni di burst tramite l'oggetto ANeuralNetworksBurst. Le esecuzioni di burst sono una sequenza di esecuzioni della stessa compilazione che si verificano in rapida successione, ad esempio quelle che operano sui frame dell'acquisizione di una videocamera o campioni audio successivi. L'utilizzo di oggetti ANeuralNetworksBurst può comportare esecuzioni più rapide, in quanto indicano agli acceleratori che le risorse possono essere riutilizzate tra le esecuzioni e che gli acceleratori devono rimanere in uno stato di prestazioni elevate per tutta la durata del burst.

ANeuralNetworksBurst introduce solo una piccola modifica nel normale percorso di esecuzione. Puoi creare un oggetto di burst utilizzando ANeuralNetworksBurst_create, come mostrato nel seguente snippet di codice:

// Create burst object to be reused across a sequence of executions
ANeuralNetworksBurst* burst = NULL;
ANeuralNetworksBurst_create(compilation, &burst);

Le esecuzioni di burst sono sincrone. Tuttavia, invece di utilizzare ANeuralNetworksExecution_compute per eseguire ogni inferenza, accoppi i vari oggetti ANeuralNetworksExecution con lo stesso ANeuralNetworksBurst nelle chiamate alla funzione ANeuralNetworksExecution_burstCompute.

// Create and configure first execution object
// ...

// Execute using the burst object
ANeuralNetworksExecution_burstCompute(execution1, burst);

// Use results of first execution and free the execution object
// ...

// Create and configure second execution object
// ...

// Execute using the same burst object
ANeuralNetworksExecution_burstCompute(execution2, burst);

// Use results of second execution and free the execution object
// ...

Libera l'oggetto ANeuralNetworksBurst con ANeuralNetworksBurst_free quando non è più necessario.

// Cleanup
ANeuralNetworksBurst_free(burst);

Code di comandi asincroni ed esecuzione protetta

In Android 11 e versioni successive, NNAPI supporta un ulteriore modo per pianificare l'esecuzione asincrona tramite il metodo ANeuralNetworksExecution_startComputeWithDependencies(). Quando utilizzi questo metodo, l'esecuzione attende la segnalazione di tutti gli eventi dipendenti prima di iniziare la valutazione. Quando l'esecuzione è stata completata e gli output sono pronti per essere utilizzati, l'evento restituito viene segnalato.

A seconda dei dispositivi che gestiscono l'esecuzione, l'evento potrebbe essere supportato da un recinto di sincronizzazione. Devi chiamare ANeuralNetworksEvent_wait() per attendere l'evento e recuperare le risorse utilizzate dall'esecuzione. Puoi importare i confini di sincronizzazione in un oggetto evento utilizzando ANeuralNetworksEvent_createFromSyncFenceFd() ed esportare da un oggetto di evento ANeuralNetworksEvent_getSyncFenceFd().

Output con dimensioni dinamiche

Per supportare modelli in cui la dimensione dell'output dipende dai dati di input, ovvero dove non è possibile determinare la dimensione al momento dell'esecuzione del modello, utilizza ANeuralNetworksExecution_getOutputOperandRank e ANeuralNetworksExecution_getOutputOperandDimensions.

Il seguente esempio di codice mostra come eseguire questa operazione:

// Get the rank of the output
uint32_t myOutputRank = 0;
ANeuralNetworksExecution_getOutputOperandRank(run1, 0, &myOutputRank);

// Get the dimensions of the output
std::vector<uint32_t> myOutputDimensions(myOutputRank);
ANeuralNetworksExecution_getOutputOperandDimensions(run1, 0, myOutputDimensions.data());

Pulizia

Il passaggio di pulizia gestisce la liberazione delle risorse interne utilizzate per il calcolo.

// Cleanup
ANeuralNetworksCompilation_free(compilation);
ANeuralNetworksModel_free(model);
ANeuralNetworksMemory_free(mem1);

Gestione degli errori e riserva della CPU

Se si verifica un errore durante il partizionamento, se un driver non riesce a compilare un modello (parte di un) o se un driver non riesce a eseguire un modello compilato (parte di a), NNAPI potrebbe ricorrere alla propria implementazione della CPU per una o più operazioni.

Se il client NNAPI contiene versioni ottimizzate dell'operazione (ad esempio TFLite), potrebbe essere vantaggioso disabilitare la riserva della CPU e gestire gli errori con l'implementazione delle operazioni ottimizzate del client.

In Android 10, se la compilazione viene eseguita utilizzando ANeuralNetworksCompilation_createForDevices, la CPU di riserva verrà disattivata.

In Android P, l'esecuzione di NNAPI torna sulla CPU se l'esecuzione sul driver non va a buon fine. Questo vale anche su Android 10 quando viene utilizzato ANeuralNetworksCompilation_create anziché ANeuralNetworksCompilation_createForDevices.

La prima esecuzione torna indietro per quella singola partizione e, se l'operazione continua a non riuscire, viene eseguito nuovamente l'intero modello sulla CPU.

Se il partizionamento o la compilazione non va a buon fine, verrà provato l'intero modello con la CPU.

Alcune operazioni non sono supportate sulla CPU, nei casi in cui la compilazione o l'esecuzione non vada a buon fine.

Anche dopo aver disabilitato il fallback della CPU, potrebbero comunque esserci operazioni nel modello pianificate sulla CPU. Se la CPU è nell'elenco dei processori fornito a ANeuralNetworksCompilation_createForDevices e rappresenta l'unico processore che supporta queste operazioni o è il processore che dichiara le migliori prestazioni per queste operazioni, verrà scelto come esecutore principale (non di riserva).

Per assicurarti che non venga eseguita l'esecuzione della CPU, utilizza ANeuralNetworksCompilation_createForDevices escludendo nnapi-reference dall'elenco dei dispositivi. A partire da Android P, è possibile disattivare la funzionalità di riserva al momento dell'esecuzione sulle build DEBUG impostando la proprietà debug.nn.partition su 2.

Domini di memoria

In Android 11 e versioni successive, NNAPI supporta i domini di memoria che forniscono interfacce allocatitrici per le memorie opache. Ciò consente alle applicazioni di passare ricordi nativi del dispositivo tra le esecuzioni, in modo che NNAPI non copi o trasformi i dati inutilmente quando eseguono esecuzioni consecutive sullo stesso driver.

La funzionalità del dominio della memoria è destinata ai tensori che sono per lo più interni al driver e che non richiedono accesso frequente al lato client. Esempi di questi tensori includono i tensori di stato nei modelli di sequenza. Per i tensori che richiedono frequenti accessi alla CPU sul lato client, utilizza invece pool di memoria condivisi.

Per allocare una memoria opaca, segui questi passaggi:

  1. Richiama la funzione ANeuralNetworksMemoryDesc_create() per creare un nuovo descrittore di memoria:

    // Create a memory descriptor
    ANeuralNetworksMemoryDesc* desc;
    ANeuralNetworksMemoryDesc_create(&desc);
    
  2. Specifica tutti i ruoli di input e output previsti chiamando ANeuralNetworksMemoryDesc_addInputRole() e ANeuralNetworksMemoryDesc_addOutputRole().

    // Specify that the memory may be used as the first input and the first output
    // of the compilation
    ANeuralNetworksMemoryDesc_addInputRole(desc, compilation, 0, 1.0f);
    ANeuralNetworksMemoryDesc_addOutputRole(desc, compilation, 0, 1.0f);
    
  3. (Facoltativo) Specifica le dimensioni della memoria chiamando ANeuralNetworksMemoryDesc_setDimensions().

    // Specify the memory dimensions
    uint32_t dims[] = {3, 4};
    ANeuralNetworksMemoryDesc_setDimensions(desc, 2, dims);
    
  4. Finalizza la definizione del descrittore chiamando ANeuralNetworksMemoryDesc_finish().

    ANeuralNetworksMemoryDesc_finish(desc);
    
  5. Assegna tutti i ricordi che ti servono passando il descrittore a ANeuralNetworksMemory_createFromDesc().

    // Allocate two opaque memories with the descriptor
    ANeuralNetworksMemory* opaqueMem;
    ANeuralNetworksMemory_createFromDesc(desc, &opaqueMem);
    
  6. Libera il descrittore di memoria quando non ti serve più.

    ANeuralNetworksMemoryDesc_free(desc);
    

Il client può utilizzare solo l'oggetto ANeuralNetworksMemory creato con ANeuralNetworksExecution_setInputFromMemory() o ANeuralNetworksExecution_setOutputFromMemory() in base ai ruoli specificati nell'oggetto ANeuralNetworksMemoryDesc. Gli argomenti di offset e lunghezza devono essere impostati su 0, per indicare che viene utilizzata l'intera memoria. Il client può anche impostare o estrarre esplicitamente i contenuti della memoria utilizzando ANeuralNetworksMemory_copy().

Puoi creare memorie opache con ruoli di dimensioni o ranking non specificati. In questo caso, la creazione della memoria potrebbe non riuscire con lo stato ANEURALNETWORKS_OP_FAILED se non è supportata dal driver sottostante. Consigliamo al client di implementare la logica di fallback allocando un buffer sufficientemente grande supportato da Ashmem o AHardwareBuffer in modalità BLOB.

Quando NNAPI non ha più bisogno di accedere all'oggetto di memoria opaca, libera l'istanza ANeuralNetworksMemory corrispondente:

ANeuralNetworksMemory_free(opaqueMem);

Misurare il rendimento

Puoi valutare le prestazioni della tua app misurando il tempo di esecuzione o tramite profilazione.

Tempo di esecuzione

Quando vuoi determinare il tempo totale di esecuzione attraverso il runtime, puoi utilizzare l'API di esecuzione sincrona e misurare il tempo impiegato dalla chiamata. Per determinare il tempo totale di esecuzione tramite un livello inferiore dello stack software, puoi utilizzare ANeuralNetworksExecution_setMeasureTiming e ANeuralNetworksExecution_getDuration per ottenere:

  • tempo di esecuzione su un acceleratore (non nel driver, che viene eseguito sul processore host).
  • tempo di esecuzione del conducente, incluso il tempo sull'acceleratore.

Il tempo di esecuzione nel driver esclude l'overhead come quello del runtime stesso e dell'IPC necessario al runtime per comunicare con il driver.

Queste API misurano la durata tra il lavoro inviato e gli eventi di lavoro completato, piuttosto che il tempo che un driver o un acceleratore dedicano all'esecuzione dell'inferenza, potenzialmente interrotta dal cambio di contesto.

Ad esempio, se inizia l'inferenza 1, il driver smette di lavorare per eseguire l'inferenza 2, quindi riprende e completa l'inferenza 1, il tempo di esecuzione per l'inferenza 1 includerà l'ora in cui il lavoro è stato interrotto per eseguire l'inferenza 2.

Queste informazioni di tempistica possono essere utili per un deployment in produzione di un'applicazione al fine di raccogliere dati di telemetria per l'utilizzo offline. Puoi utilizzare i dati di tempistica per modificare l'app in modo da migliorare le prestazioni.

Quando utilizzi questa funzionalità, tieni presente quanto segue:

  • La raccolta delle informazioni sulle tempistiche potrebbe avere un costo in termini di prestazioni.
  • Solo un conducente è in grado di calcolare il tempo trascorso in se stesso o sull'acceleratore, escluso il tempo trascorso nel runtime NNAPI e nell'IPC.
  • Puoi utilizzare queste API solo con un ANeuralNetworksExecution creato con ANeuralNetworksCompilation_createForDevices con numDevices = 1.
  • Non è necessario che il conducente possa inviare informazioni sulle tempistiche.

Profila la tua applicazione con Android Systrace

A partire da Android 10, NNAPI genera automaticamente eventi systrace che puoi utilizzare per profilare la tua applicazione.

L'origine NNAPI include un'utilità parse_systrace per elaborare gli eventi di systrace generati dall'applicazione e generare una visualizzazione tabella che mostra il tempo speso nelle diverse fasi del ciclo di vita del modello (istanza, preparazione, esecuzione di compilazione e terminazione) e i diversi livelli delle applicazioni. I livelli in cui è suddivisa la tua applicazione sono:

  • Application: il codice principale dell'applicazione
  • Runtime: runtime NNAPI
  • IPC: la comunicazione tra il runtime NNAPI e il codice driver
  • Driver: il processo di accelerazione.

Genera i dati dell'analisi di profilazione

Supponendo che tu abbia consultato la struttura ad albero delle origini AOSP all'indirizzo $ANDROID_Build_TOP e utilizzando l'esempio di classificazione delle immagini TFLite come applicazione di destinazione, puoi generare i dati di profilazione NNAPI procedendo nel seguente modo:

  1. Avvia il comando systrace di Android con il seguente comando:
$ANDROID_BUILD_TOP/external/chromium-trace/systrace.py  -o trace.html -a org.tensorflow.lite.examples.classification nnapi hal freq sched idle load binder_driver

Il parametro -o trace.html indica che le tracce verranno scritte in trace.html. Quando profila la tua applicazione, devi sostituire org.tensorflow.lite.examples.classification con il nome di processo specificato nel manifest dell'app.

In questo modo una delle console di shell sarà occupata. Non eseguire il comando in background, poiché attende interattiva la terminazione di un enter.

  1. Dopo l'avvio del raccoglitore systrace, avvia l'app ed esegui il test di benchmark.

Nel nostro caso, puoi avviare l'app Image Classification da Android Studio o direttamente dalla UI dello smartphone di test, se l'app è già stata installata. Per generare alcuni dati NNAPI, devi configurare l'app in modo che utilizzi NNAPI selezionando NNAPI come dispositivo di destinazione nella finestra di dialogo di configurazione dell'app.

  1. Al termine del test, termina il parametro systrace premendo enter sul terminale della console attivo dal passaggio 1.

  2. Esegui l'utilità systrace_parser per generare statistiche cumulative:

$ANDROID_BUILD_TOP/frameworks/ml/nn/tools/systrace_parser/parse_systrace.py --total-times trace.html

Il parser accetta i seguenti parametri: - --total-times: mostra il tempo totale trascorso in un livello, incluso il tempo di attesa per l'esecuzione di una chiamata a un livello sottostante - --print-detail: stampa tutti gli eventi raccolti da systrace - --per-execution: visualizza solo l'esecuzione e le relative sottofasi (come tempi di esecuzione) anziché le statistiche per tutte le fasi - --json: produce l'output in formato JSON

Di seguito è riportato un esempio dell'output:

===========================================================================================================================================
NNAPI timing summary (total time, ms wall-clock)                                                      Execution
                                                           ----------------------------------------------------
              Initialization   Preparation   Compilation           I/O       Compute      Results     Ex. total   Termination        Total
              --------------   -----------   -----------   -----------  ------------  -----------   -----------   -----------   ----------
Application              n/a         19.06       1789.25           n/a           n/a         6.70         21.37           n/a      1831.17*
Runtime                    -         18.60       1787.48          2.93         11.37         0.12         14.42          1.32      1821.81
IPC                     1.77             -       1781.36          0.02          8.86            -          8.88             -      1792.01
Driver                  1.04             -       1779.21           n/a           n/a          n/a          7.70             -      1787.95

Total                   1.77*        19.06*      1789.25*         2.93*        11.74*        6.70*        21.37*         1.32*     1831.17*
===========================================================================================================================================
* This total ignores missing (n/a) values and thus is not necessarily consistent with the rest of the numbers

Il parser potrebbe restituire errori se gli eventi raccolti non rappresentano una traccia completa dell'applicazione. In particolare, potrebbe non funzionare se nella traccia sono presenti eventi systrace generati per contrassegnare la fine di una sezione senza un evento di inizio sezione associato. In genere questo accade se vengono generati alcuni eventi di una sessione di profilazione precedente all'avvio del raccoglitore systrace. In questo caso, dovrai eseguire di nuovo la profilazione.

Aggiungi statistiche per il codice dell'applicazione all'output systrace_parser

L'applicazione parse_systrace si basa sulla funzionalità systrace integrata di Android. Puoi aggiungere tracce per operazioni specifiche nella tua app utilizzando l'API systrace (per Java, per applicazioni native) con nomi di eventi personalizzati.

Per associare gli eventi personalizzati alle fasi del ciclo di vita dell'applicazione, anteponi una delle seguenti stringhe al nome dell'evento:

  • [NN_LA_PI]: evento a livello di applicazione per l'inizializzazione
  • [NN_LA_PP]: evento relativo al livello di applicazione per la preparazione
  • [NN_LA_PC]: evento a livello di applicazione per la compilazione
  • [NN_LA_PE]: evento a livello di applicazione per l'esecuzione

Ecco un esempio di come modificare il codice di esempio di classificazione delle immagini TFLite aggiungendo una sezione runInferenceModel per la fase Execution e il livello Application contenente altre sezioni preprocessBitmap che non verranno considerate nelle tracce NNAPI. La sezione runInferenceModel farà parte degli eventi systrace elaborati dall'analizzatore sintattico systrace nnapi:

Kotlin

/** Runs inference and returns the classification results. */
fun recognizeImage(bitmap: Bitmap): List {
   // This section won’t appear in the NNAPI systrace analysis
   Trace.beginSection("preprocessBitmap")
   convertBitmapToByteBuffer(bitmap)
   Trace.endSection()

   // Run the inference call.
   // Add this method in to NNAPI systrace analysis.
   Trace.beginSection("[NN_LA_PE]runInferenceModel")
   long startTime = SystemClock.uptimeMillis()
   runInference()
   long endTime = SystemClock.uptimeMillis()
   Trace.endSection()
    ...
   return recognitions
}

Java

/** Runs inference and returns the classification results. */
public List recognizeImage(final Bitmap bitmap) {

 // This section won’t appear in the NNAPI systrace analysis
 Trace.beginSection("preprocessBitmap");
 convertBitmapToByteBuffer(bitmap);
 Trace.endSection();

 // Run the inference call.
 // Add this method in to NNAPI systrace analysis.
 Trace.beginSection("[NN_LA_PE]runInferenceModel");
 long startTime = SystemClock.uptimeMillis();
 runInference();
 long endTime = SystemClock.uptimeMillis();
 Trace.endSection();
  ...
 Trace.endSection();
 return recognitions;
}

Qualità del servizio

In Android 11 e versioni successive, NNAPI migliora la qualità del servizio (QoS) consentendo a un'applicazione di indicare le priorità relative dei propri modelli, il tempo massimo previsto per preparare un determinato modello e il tempo massimo previsto per completare un determinato calcolo. Android 11 introduce anche codici risultato NNAPI aggiuntivi che consentono alle applicazioni di comprendere errori come le mancate scadenze di esecuzione.

Imposta la priorità di un carico di lavoro

Per impostare la priorità di un carico di lavoro NNAPI, chiama ANeuralNetworksCompilation_setPriority() prima di chiamare ANeuralNetworksCompilation_finish().

Imposta scadenze

Le applicazioni possono impostare scadenze sia per la compilazione e per l'inferenza del modello.

Scopri di più sugli operandi

La seguente sezione tratta argomenti avanzati sull'utilizzo degli operandi.

Tensori quantizzati

Un tensore quantizzato è un modo compatto per rappresentare un array n-dimensionale di valori in virgola mobile.

NNAPI supporta tensori quantizzati asimmetrici a 8 bit. Per questi tensori, il valore di ogni cella è rappresentato da un numero intero a 8 bit. Associato al tensore ci sono una scala e un valore in punto zero. Questi vengono utilizzati per convertire i numeri interi a 8 bit in valori in virgola mobile rappresentati.

La formula è:

(cellValue - zeroPoint) * scale

dove il valore zeroPoint è un numero intero a 32 bit e la scala è un valore in virgola mobile a 32 bit.

Rispetto ai tensori dei valori in virgola mobile a 32 bit, i tensori quantizzati a 8 bit hanno due vantaggi:

  • L'applicazione è di dimensioni inferiori, poiché i pesi addestrati richiedono un quarto della dimensione dei tensori a 32 bit.
  • I calcoli spesso possono essere eseguiti più velocemente. Ciò è dovuto alla minore quantità di dati che devono essere recuperati dalla memoria e all'efficienza di processori come i DSP nel calcolo dei numeri interi.

Sebbene sia possibile convertire un modello in virgola mobile in uno quantizzato, la nostra esperienza ha dimostrato che l'addestramento diretto di un modello quantizzato consente di ottenere risultati migliori. In effetti, la rete neurale impara a compensare l'aumento della granularità di ogni valore. Per ogni tensore quantizzato, i valori di scala e zeroPoint vengono determinati durante il processo di addestramento.

In NNAPI, definisci i tipi di tensori quantizzati impostando il campo del tipo della struttura di dati di ANeuralNetworksOperandType su ANEURALNETWORKS_TENSOR_QUANT8_ASYMM. Devi specificare anche la scala e il valore zeroPoint del tensore in quella struttura di dati.

Oltre ai tensori quantizzati asimmetrici a 8 bit, NNAPI supporta quanto segue:

Operatori facoltativi

Alcune operazioni, come ANEURALNETWORKS_LSH_PROJECTION, consentono l'utilizzo di operandi facoltativi. Per indicare nel modello che l'operando facoltativo viene omesso, chiama la funzione ANeuralNetworksModel_setOperandValue(), passando NULL per il buffer e 0 per la lunghezza.

Se la decisione sulla presenza o meno dell'operando varia per ogni esecuzione, indichi che l'operando è stato omesso utilizzando le funzioni ANeuralNetworksExecution_setInput() o ANeuralNetworksExecution_setOutput(), passando NULL per il buffer e 0 per la lunghezza.

Tensori di ranking sconosciuto

Android 9 (livello API 28) ha introdotto gli operandi del modello di dimensioni sconosciute ma il ranking noto (il numero di dimensioni). Android 10 (livello API 29) ha introdotto tensori di ranking sconosciuto, come mostrato in ANeuralNetworksOperandType.

Benchmark NNAPI

Il benchmark NNAPI è disponibile su AOSP in platform/test/mlts/benchmark (app di benchmark) e platform/test/mlts/models (modelli e set di dati).

Il benchmark valuta la latenza e l'accuratezza e confronta i driver con lo stesso lavoro svolto utilizzando Tensorflow Lite in esecuzione sulla CPU, per gli stessi modelli e set di dati.

Per utilizzare il benchmark:

  1. Collega un dispositivo Android di destinazione al computer, apri una finestra del terminale e assicurati che il dispositivo sia raggiungibile tramite ADB.

  2. Se è connesso più di un dispositivo Android, esporta la variabile di ambiente ANDROID_SERIAL del dispositivo di destinazione.

  3. Vai alla directory di origine di primo livello di Android.

  4. Esegui questi comandi:

    lunch aosp_arm-userdebug # Or aosp_arm64-userdebug if available
    ./test/mlts/benchmark/build_and_run_benchmark.sh
    

    Alla fine dell'esecuzione di un benchmark, i risultati vengono presentati come una pagina HTML passata a xdg-open.

Log NNAPI

NNAPI genera informazioni diagnostiche utili nei log di sistema. Per analizzare i log, utilizza l'utilità logcat.

Attiva il logging NNAPI dettagliato per fasi o componenti specifici impostando la proprietà debug.nn.vlog (utilizzando adb shell) sul seguente elenco di valori, separati da spazio, due punti o virgola:

  • model: creazione di modelli
  • compilation: generazione del piano di esecuzione del modello e della compilazione
  • execution: esecuzione del modello
  • cpuexe: esecuzione delle operazioni mediante l'implementazione della CPU NNAPI
  • manager: estensioni NNAPI, interfacce disponibili e informazioni correlate alle funzionalità
  • all o 1: tutti gli elementi sopra

Ad esempio, per abilitare il logging dettagliato completo, utilizza il comando adb shell setprop debug.nn.vlog all. Per disabilitare il logging dettagliato, utilizza il comando adb shell setprop debug.nn.vlog '""'.

Una volta abilitato, il logging dettagliato genera voci di log a livello di informazioni con un tag impostato sul nome della fase o del componente.

Oltre ai messaggi controllati con debug.nn.vlog, i componenti dell'API NNAPI forniscono altre voci di log a vari livelli, ciascuna utilizzando un tag di log specifico.

Per ottenere un elenco dei componenti, esegui una ricerca nella struttura di origine utilizzando la seguente espressione:

grep -R 'define LOG_TAG' | awk -F '"' '{print $2}' | sort -u | egrep -v "Sample|FileTag|test"

Al momento questa espressione restituisce i seguenti tag:

  • Costruttore Burst
  • Callback
  • Creazione compilazione
  • CpuExecutor
  • Generatore di esecuzioni
  • ExecutionBurstController
  • ExecutionBurstServer
  • Piano di esecuzione
  • Conducente Fibonacci
  • Graphic Dump
  • Wrapper Indicizzato
  • IonWatcher
  • Gestore
  • Memoria
  • MemoriaUtils
  • MetaModel
  • Informazioni argomento modello
  • Creazione di modelli
  • Reti neurali
  • Operation resolver
  • Fasi operative
  • Utili operativi
  • InformazioniPacchetto
  • TokenHasher
  • Gestore
  • Utili
  • ConvalidaHal
  • Interfaccia con più versioni

Per controllare il livello dei messaggi di log mostrati da logcat, utilizza la variabile di ambiente ANDROID_LOG_TAGS.

Per visualizzare l'insieme completo dei messaggi di log NNAPI e disabilitare altri messaggi, imposta ANDROID_LOG_TAGS come segue:

BurstBuilder:V Callbacks:V CompilationBuilder:V CpuExecutor:V ExecutionBuilder:V ExecutionBurstController:V ExecutionBurstServer:V ExecutionPlan:V FibonacciDriver:V GraphDump:V IndexedShapeWrapper:V IonWatcher:V Manager:V MemoryUtils:V Memory:V MetaModel:V ModelArgumentInfo:V ModelBuilder:V NeuralNetworks:V OperationResolver:V OperationsUtils:V Operations:V PackageInfo:V TokenHasher:V TypeManager:V Utils:V ValidateHal:V VersionedInterfaces:V *:S.

Puoi impostare ANDROID_LOG_TAGS utilizzando il seguente comando:

export ANDROID_LOG_TAGS=$(grep -R 'define LOG_TAG' | awk -F '"' '{ print $2 ":V" }' | sort -u | egrep -v "Sample|FileTag|test" | xargs echo -n; echo ' *:S')

Tieni presente che questo è solo un filtro che si applica a logcat. Devi comunque impostare la proprietà debug.nn.vlog su all per generare informazioni di log dettagliate.