API нейронных сетей

Android Neural Networks API (NNAPI) — это API Android C, предназначенный для выполнения ресурсоемких операций машинного обучения на устройствах Android. NNAPI предназначен для обеспечения базового уровня функциональности для платформ машинного обучения более высокого уровня, таких как TensorFlow Lite и Caffe2, которые создают и обучают нейронные сети. API доступен на всех устройствах Android под управлением Android 8.1 (уровень API 27) или выше.

NNAPI поддерживает логический вывод, применяя данные с устройств Android к предварительно обученным моделям, определенным разработчиком. Примеры вывода включают классификацию изображений, прогнозирование поведения пользователя и выбор подходящих ответов на поисковый запрос.

Инференция на устройстве имеет множество преимуществ:

  • Задержка : вам не нужно отправлять запрос через сетевое соединение и ждать ответа. Например, это может быть критично для видеоприложений, обрабатывающих последовательные кадры, поступающие с камеры.
  • Доступность : приложение работает даже вне зоны покрытия сети.
  • Скорость : новое оборудование, предназначенное для обработки нейронных сетей, обеспечивает значительно более быстрые вычисления, чем один только процессор общего назначения.
  • Конфиденциальность : данные не покидают устройство Android.
  • Стоимость : ферма серверов не требуется, если все вычисления выполняются на устройстве Android.

Есть также компромиссы, о которых следует помнить разработчику:

  • Использование системы . Оценка нейронных сетей требует большого количества вычислений, что может увеличить расход заряда батареи. Вам следует рассмотреть возможность мониторинга состояния батареи, если это беспокоит ваше приложение, особенно при длительных вычислениях.
  • Размер приложения : обратите внимание на размер ваших моделей. Модели могут занимать несколько мегабайт места. Если объединение больших моделей в APK-файл может оказать чрезмерное влияние на ваших пользователей, вы можете рассмотреть возможность загрузки моделей после установки приложения, использования моделей меньшего размера или выполнения вычислений в облаке. NNAPI не предоставляет возможности запуска моделей в облаке.

См. пример API нейронных сетей Android , чтобы увидеть один пример использования NNAPI.

Понимание среды выполнения API нейронных сетей

NNAPI предназначен для вызова библиотеками, платформами и инструментами машинного обучения, которые позволяют разработчикам обучать свои модели за пределами устройства и развертывать их на устройствах Android. Приложения обычно не используют NNAPI напрямую, а вместо этого используют платформы машинного обучения более высокого уровня. Эти платформы, в свою очередь, могут использовать NNAPI для выполнения аппаратно-ускоренных операций вывода на поддерживаемых устройствах.

В зависимости от требований приложения и аппаратных возможностей устройства Android среда выполнения нейронной сети Android может эффективно распределять вычислительную нагрузку между доступными процессорами устройства, включая выделенное оборудование нейронной сети, графические процессоры (GPU) и процессоры цифровых сигналов (DSP). ).

Для устройств Android, у которых отсутствует специализированный драйвер поставщика, среда выполнения NNAPI выполняет запросы на ЦП.

На рисунке 1 показана системная архитектура высокого уровня для NNAPI.

Рис. 1. Архитектура системы для API нейронных сетей Android.

Модель программирования API нейронных сетей

Чтобы выполнять вычисления с использованием NNAPI, сначала необходимо построить ориентированный граф, определяющий выполняемые вычисления. Этот граф вычислений в сочетании с входными данными (например, весами и смещениями, переданными из среды машинного обучения) формирует модель для оценки времени выполнения NNAPI.

NNAPI использует четыре основные абстракции:

  • Модель : граф вычислений математических операций и постоянных значений, полученных в процессе обучения. Эти операции специфичны для нейронных сетей. Они включают в себя двумерную (2D) свертку , логистическую ( сигмовидную ) активацию, выпрямленную линейную активацию (ReLU) и многое другое. Создание модели — синхронная операция. После успешного создания его можно повторно использовать в потоках и компиляциях. В NNAPI модель представлена ​​как экземпляр ANeuralNetworksModel .
  • Компиляция : представляет конфигурацию для компиляции модели NNAPI в код нижнего уровня. Создание компиляции — синхронная операция. После успешного создания его можно повторно использовать в разных потоках и выполнениях. В NNAPI каждая компиляция представлена ​​как экземпляр ANeuralNetworksCompilation .
  • Память : представляет общую память, файлы, отображаемые в памяти, и аналогичные буферы памяти. Использование буфера памяти позволяет среде выполнения NNAPI более эффективно передавать данные драйверам. Приложение обычно создает один буфер общей памяти, который содержит все тензоры, необходимые для определения модели. Вы также можете использовать буферы памяти для хранения входных и выходных данных экземпляра выполнения. В NNAPI каждый буфер памяти представлен как экземпляр ANeuralNetworksMemory .
  • Выполнение : интерфейс для применения модели NNAPI к набору входных данных и сбора результатов. Выполнение может выполняться синхронно или асинхронно.

    При асинхронном выполнении несколько потоков могут ожидать одного и того же выполнения. Когда это выполнение завершается, все потоки освобождаются.

    В NNAPI каждое выполнение представлено как экземпляр ANeuralNetworksExecution .

На рис. 2 показан основной процесс программирования.

Рисунок 2. Процесс программирования для Android Neural Networks API

Остальная часть этого раздела описывает шаги по настройке модели NNAPI для выполнения вычислений, компиляции модели и выполнения скомпилированной модели.

Предоставить доступ к данным обучения

Ваши обученные данные о весах и смещениях, скорее всего, хранятся в файле. Чтобы предоставить среде выполнения NNAPI эффективный доступ к этим данным, создайте экземпляр ANeuralNetworksMemory , вызвав функцию ANeuralNetworksMemory_createFromFd() и передав файловый дескриптор открытого файла данных. Вы также указываете флаги защиты памяти и смещение, где начинается область общей памяти в файле.

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

Хотя в этом примере мы используем только один экземпляр ANeuralNetworksMemory для всех наших весов, можно использовать более одного экземпляра ANeuralNetworksMemory для нескольких файлов.

Используйте собственные аппаратные буферы

Вы можете использовать собственные аппаратные буферы для входных и выходных данных модели, а также постоянных значений операндов. В некоторых случаях ускоритель NNAPI может получать доступ к объектам AHardwareBuffer без необходимости копирования данных драйвером. AHardwareBuffer имеет множество различных конфигураций, и не каждый ускоритель NNAPI может поддерживать все эти конфигурации. Из-за этого ограничения обратитесь к ограничениям, перечисленным в справочной документации ANeuralNetworksMemory_createFromAHardwareBuffer , и заранее протестируйте на целевых устройствах, чтобы убедиться, что компиляции и выполнения, использующие AHardwareBuffer , ведут себя должным образом, используя назначение устройств для указания ускорителя.

Чтобы разрешить среде выполнения NNAPI доступ к объекту AHardwareBuffer , создайте экземпляр ANeuralNetworksMemory , вызвав функцию ANeuralNetworksMemory_createFromAHardwareBuffer и передав объект AHardwareBuffer , как показано в следующем примере кода:

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

Когда NNAPI больше не требуется доступ к объекту AHardwareBuffer , освободите соответствующий экземпляр ANeuralNetworksMemory :

ANeuralNetworksMemory_free(mem2);

Примечание:

  • Вы можете использовать AHardwareBuffer только для всего буфера; вы не можете использовать его с параметром ARect .
  • Среда выполнения NNAPI не очищает буфер. Прежде чем планировать выполнение, вам необходимо убедиться, что входной и выходной буферы доступны.
  • Дескрипторы файлов ограждения синхронизации не поддерживаются.
  • Для AHardwareBuffer с форматами и битами использования, зависящими от поставщика, реализация поставщика должна определить, кто отвечает за очистку кэша: клиент или драйвер.

Модель

Модель — это фундаментальная единица вычислений в NNAPI. Каждая модель определяется одним или несколькими операндами и операциями.

Операнды

Операнды — это объекты данных, используемые при определении графа. К ним относятся входные и выходные данные модели, промежуточные узлы, содержащие данные, передаваемые от одной операции к другой, и константы, передаваемые в эти операции.

В модели NNAPI можно добавлять два типа операндов: скаляры и тензоры .

Скаляр представляет одно значение. NNAPI поддерживает скалярные значения в логическом формате, 16-битном формате с плавающей запятой, 32-битном формате с плавающей запятой, 32-битном целочисленном формате и 32-битном целочисленном формате без знака.

Большинство операций в NNAPI используют тензоры. Тензоры — это n-мерные массивы. NNAPI поддерживает тензоры с 16-битными числами с плавающей запятой, 32-битными числами с плавающей запятой, 8-битными квантованными , 16-битными квантованными, 32-битными целыми числами и 8-битными логическими значениями.

Например, на рисунке 3 представлена ​​модель с двумя операциями: сложением и последующим умножением. Модель принимает входной тензор и создает один выходной тензор.

Рисунок 3. Пример операндов модели NNAPI

Модель выше имеет семь операндов. Эти операнды неявно идентифицируются по индексу порядка, в котором они добавляются в модель. Первый добавленный операнд имеет индекс 0, второй — индекс 1 и так далее. Операнды 1, 2, 3 и 5 являются постоянными операндами.

Порядок добавления операндов не имеет значения. Например, выходной операнд модели может быть добавлен первым. Важная часть — использовать правильное значение индекса при обращении к операнду.

Операнды имеют типы. Они указываются при добавлении в модель.

Операнд не может использоваться как вход и выход модели одновременно.

Каждый операнд должен быть либо входным параметром модели, либо константой, либо выходным операндом ровно одной операции.

Дополнительную информацию об использовании операндов см. в разделе «Подробнее об операндах» .

Операции

Операция определяет вычисления, которые необходимо выполнить. Каждая операция состоит из следующих элементов:

  • тип операции (например, сложение, умножение, свертка),
  • список индексов операндов, которые операция использует для ввода, и
  • список индексов операндов, которые операция использует для вывода.

Порядок в этих списках имеет значение; ожидаемые входные и выходные данные для каждого типа операции см. в справочнике по API NNAPI .

Перед добавлением операции необходимо добавить в модель операнды, которые операция использует или создает.

Порядок добавления операций не имеет значения. NNAPI опирается на зависимости, установленные графом вычислений операндов и операций, для определения порядка выполнения операций.

Операции, которые поддерживает NNAPI, приведены в таблице ниже:

Категория Операции
Поэлементные математические операции
Тензорные манипуляции
Операции с изображениями
Операции поиска
Операции нормализации
Операции свертки
Объединение операций
Операции активации
Прочие операции

Известная проблема на уровне API 28: при передаче тензоров ANEURALNETWORKS_TENSOR_QUANT8_ASYMM в операцию ANEURALNETWORKS_PAD , которая доступна на Android 9 (уровень API 28) и более поздних версиях, выходные данные NNAPI могут не соответствовать выводам платформ машинного обучения более высокого уровня, таких как TensorFlow Lite. . Вместо этого вам следует передать только ANEURALNETWORKS_TENSOR_FLOAT32 . Проблема решена в Android 10 (уровень API 29) и выше.

Сборка моделей

В следующем примере мы создаем модель с двумя операциями, показанную на рисунке 3 .

Чтобы построить модель, выполните следующие действия:

  1. Вызовите функцию ANeuralNetworksModel_create() , чтобы определить пустую модель.

    ANeuralNetworksModel* model = NULL;
    ANeuralNetworksModel_create(&model);
    
  2. Добавьте операнды в свою модель, вызвав ANeuralNetworks_addOperand() . Их типы данных определяются с использованием структуры данных 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. Для операндов, которые имеют постоянные значения, такие как веса и смещения, которые ваше приложение получает в процессе обучения, используйте функции ANeuralNetworksModel_setOperandValue() и ANeuralNetworksModel_setOperandValueFromMemory() .

    В следующем примере мы устанавливаем постоянные значения из файла обучающих данных, соответствующих буферу памяти, который мы создали в разделе «Предоставление доступа к обучающим данным» .

    // 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. Для каждой операции в ориентированном графе, которую вы хотите вычислить, добавьте операцию в свою модель, вызвав функцию ANeuralNetworksModel_addOperation() .

    В качестве параметров этого вызова ваше приложение должно предоставить:

    • тип операции
    • количество входных значений
    • массив индексов для входных операндов
    • количество выходных значений
    • массив индексов для выходных операндов

    Обратите внимание, что операнд не может использоваться одновременно для ввода и вывода одной и той же операции.

    // 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. Определите, какие операнды модель должна рассматривать как входные и выходные данные, вызвав функцию 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. При необходимости укажите, разрешено ли вычислять ANEURALNETWORKS_TENSOR_FLOAT32 с таким же низким диапазоном или точностью, как у 16-битного формата с плавающей запятой IEEE 754, вызвав ANeuralNetworksModel_relaxComputationFloat32toFloat16() .

  7. Вызовите ANeuralNetworksModel_finish() , чтобы завершить определение вашей модели. Если ошибок нет, эта функция возвращает код результата ANEURALNETWORKS_NO_ERROR .

    ANeuralNetworksModel_finish(model);
    

Создав модель, вы можете компилировать ее любое количество раз и выполнять каждую компиляцию любое количество раз.

Поток управления

Чтобы включить поток управления в модель NNAPI, выполните следующие действия:

  1. Создайте соответствующие подграфы выполнения (подграфы then и else для оператора IF , подграфы condition и body для цикла WHILE ) как автономные модели ANeuralNetworksModel* :

    ANeuralNetworksModel* thenModel = makeThenModel();
    ANeuralNetworksModel* elseModel = makeElseModel();
    
  2. Создайте операнды, которые ссылаются на эти модели в модели, содержащей поток управления:

    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. Добавьте операцию потока управления:

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

Сборник

Шаг компиляции определяет, на каких процессорах будет выполняться ваша модель, и запрашивает соответствующие драйверы подготовиться к ее выполнению. Это может включать в себя генерацию машинного кода, специфичного для процессоров, на которых будет работать ваша модель.

Чтобы скомпилировать модель, выполните следующие действия:

  1. Вызовите функцию ANeuralNetworksCompilation_create() , чтобы создать новый экземпляр компиляции.

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

    При желании вы можете использовать назначение устройств , чтобы явно выбрать, на каких устройствах выполнять выполнение.

  2. При желании вы можете повлиять на то, как время выполнения будет зависеть от использования энергии аккумулятора и скорости выполнения. Вы можете сделать это, вызвав ANeuralNetworksCompilation_setPreference() .

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

    Предпочтения, которые вы можете указать, включают в себя:

    • ANEURALNETWORKS_PREFER_LOW_POWER : предпочитаете выполнение таким образом, чтобы минимизировать разряд батареи. Это желательно для часто выполняемых компиляций.
    • ANEURALNETWORKS_PREFER_FAST_SINGLE_ANSWER : предпочитать возвращать один ответ как можно быстрее, даже если это приводит к большему энергопотреблению. Это значение по умолчанию.
    • ANEURALNETWORKS_PREFER_SUSTAINED_SPEED : предпочитает максимизировать пропускную способность последовательных кадров, например, при обработке последовательных кадров, поступающих с камеры.
  3. При желании вы можете настроить кэширование компиляции, вызвав ANeuralNetworksCompilation_setCaching .

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

    Используйте getCodeCacheDir() для cacheDir . Указанный token должен быть уникальным для каждой модели в приложении.

  4. Завершите определение компиляции, вызвав ANeuralNetworksCompilation_finish() . Если ошибок нет, эта функция возвращает код результата ANEURALNETWORKS_NO_ERROR .

    ANeuralNetworksCompilation_finish(compilation);
    

Обнаружение и назначение устройств

На устройствах Android под управлением Android 10 (уровень API 29) и выше NNAPI предоставляет функции, которые позволяют библиотекам и приложениям платформы машинного обучения получать информацию о доступных устройствах и указывать устройства, которые будут использоваться для выполнения. Предоставление информации о доступных устройствах позволяет приложениям получать точную версию драйверов, найденных на устройстве, чтобы избежать известных несовместимостей. Предоставляя приложениям возможность указывать, какие устройства должны выполнять различные разделы модели, приложения можно оптимизировать для устройства Android, на котором они развернуты.

Обнаружение устройств

Используйте ANeuralNetworks_getDeviceCount чтобы получить количество доступных устройств. Для каждого устройства используйте ANeuralNetworks_getDevice , чтобы установить для экземпляра ANeuralNetworksDevice ссылку на это устройство.

Получив ссылку на устройство, вы можете получить дополнительную информацию об этом устройстве, используя следующие функции:

Назначение устройства

Используйте ANeuralNetworksModel_getSupportedOperationsForDevices чтобы определить, какие операции модели можно запускать на определенных устройствах.

Чтобы контролировать, какие ускорители использовать для выполнения, вызовите ANeuralNetworksCompilation_createForDevices вместо ANeuralNetworksCompilation_create . Используйте полученный объект ANeuralNetworksCompilation как обычно. Функция возвращает ошибку, если предоставленная модель содержит операции, которые не поддерживаются выбранными устройствами.

Если указано несколько устройств, среда выполнения отвечает за распределение работы между устройствами.

Подобно другим устройствам, реализация ЦП NNAPI представлена ANeuralNetworksDevice с именем nnapi-reference и типом ANEURALNETWORKS_DEVICE_TYPE_CPU . При вызове ANeuralNetworksCompilation_createForDevices реализация ЦП не используется для обработки случаев сбоя при компиляции и выполнении модели.

В обязанности приложения входит разделение модели на подмодели, которые могут работать на указанных устройствах. Приложения, которым не требуется выполнять секционирование вручную, должны продолжать вызывать более простой метод ANeuralNetworksCompilation_create , чтобы использовать все доступные устройства (включая ЦП) для ускорения модели. Если модель не может быть полностью поддержана устройствами, указанными вами с помощью ANeuralNetworksCompilation_createForDevices , возвращается ANEURALNETWORKS_BAD_DATA .

Разделение модели

Если для модели доступно несколько устройств, среда выполнения NNAPI распределяет работу между устройствами. Например, если ANeuralNetworksCompilation_createForDevices было предоставлено более одного устройства, все указанные будут учитываться при распределении работы. Обратите внимание: если устройство ЦП отсутствует в списке, выполнение ЦП будет отключено. При использовании ANeuralNetworksCompilation_create будут учитываться все доступные устройства, включая ЦП.

Распределение осуществляется путем выбора из списка доступных устройств для каждой операции в модели устройства, поддерживающего операцию и декларирующего лучшую производительность, т. е. самое быстрое время выполнения или самое низкое энергопотребление, в зависимости от заданного предпочтения выполнения. клиентом. Этот алгоритм разделения не учитывает возможную неэффективность, вызванную вводом-выводом между различными процессорами, поэтому при указании нескольких процессоров (либо явно при использовании ANeuralNetworksCompilation_createForDevices , либо неявно при использовании ANeuralNetworksCompilation_create ) важно профилировать полученное приложение.

Чтобы понять, как ваша модель была разделена NNAPI, проверьте журналы Android на наличие сообщения (на уровне INFO с тегом ExecutionPlan ):

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

op-name — это описательное имя операции в графе, а device-index — это индекс устройства-кандидата в списке устройств. Этот список является входными данными, предоставляемыми ANeuralNetworksCompilation_createForDevices или, если используется ANeuralNetworksCompilation_createForDevices , списком устройств, возвращаемым при переборе всех устройств с использованием ANeuralNetworks_getDeviceCount и ANeuralNetworks_getDevice .

Сообщение (на уровне INFO с тегом ExecutionPlan ):

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

Это сообщение указывает на то, что весь граф был ускорен на устройстве device-name .

Исполнение

На этапе выполнения модель применяется к набору входных данных, а выходные данные вычислений сохраняются в одном или нескольких пользовательских буферах или областях памяти, выделенных вашим приложением.

Чтобы выполнить скомпилированную модель, выполните следующие действия:

  1. Вызовите функцию ANeuralNetworksExecution_create() , чтобы создать новый экземпляр выполнения.

    // Run the compiled model against a set of inputs
    ANeuralNetworksExecution* run1 = NULL;
    ANeuralNetworksExecution_create(compilation, &run1);
    
  2. Укажите, где ваше приложение считывает входные значения для вычислений. Ваше приложение может считывать входные значения либо из пользовательского буфера, либо из выделенного пространства памяти, вызывая ANeuralNetworksExecution_setInput() или 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. Укажите, куда ваше приложение записывает выходные значения. Ваше приложение может записывать выходные значения либо в пользовательский буфер, либо в выделенное пространство памяти, вызывая ANeuralNetworksExecution_setOutput() или ANeuralNetworksExecution_setOutputFromMemory() соответственно.

    // Set the output
    float32 myOutput[3][4];
    ANeuralNetworksExecution_setOutput(run1, 0, NULL, myOutput, sizeof(myOutput));
    
  4. Запланируйте запуск выполнения, вызвав функцию ANeuralNetworksExecution_startCompute() . Если ошибок нет, эта функция возвращает код результата ANEURALNETWORKS_NO_ERROR .

    // Starts the work. The work proceeds asynchronously
    ANeuralNetworksEvent* run1_end = NULL;
    ANeuralNetworksExecution_startCompute(run1, &run1_end);
    
  5. Вызовите функцию ANeuralNetworksEvent_wait() , чтобы дождаться завершения выполнения. Если выполнение прошло успешно, эта функция возвращает код результата ANEURALNETWORKS_NO_ERROR . Ожидание может выполняться в другом потоке, отличном от того, который запускает выполнение.

    // 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. При желании вы можете применить другой набор входных данных к скомпилированной модели, используя тот же экземпляр компиляции для создания нового экземпляра 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);
    

Синхронное выполнение

Асинхронное выполнение тратит время на создание и синхронизацию потоков. Более того, задержка может сильно варьироваться: самые длинные задержки достигают 500 микросекунд между моментом уведомления или пробуждения потока и моментом его окончательной привязки к ядру ЦП.

Чтобы уменьшить задержку, вы можете вместо этого указать приложению выполнить синхронный вызов вывода во время выполнения. Этот вызов вернется только после завершения вывода, а не после его запуска. Вместо вызова ANeuralNetworksExecution_startCompute для асинхронного вызова среды выполнения приложение вызывает ANeuralNetworksExecution_compute для синхронного вызова среды выполнения. Вызов ANeuralNetworksExecution_compute не принимает ANeuralNetworksEvent и не связан с вызовом ANeuralNetworksEvent_wait .

Пакетные казни

На устройствах Android под управлением Android 10 (уровень API 29) и выше NNAPI поддерживает пакетное выполнение через объект ANeuralNetworksBurst . Пакетные исполнения — это последовательность выполнения одной и той же компиляции, которые происходят в быстрой последовательности, например, при работе с кадрами, снятыми с камеры, или с последовательными аудиосэмплами. Использование объектов ANeuralNetworksBurst может привести к более быстрому выполнению, поскольку они указывают ускорителям, что ресурсы могут повторно использоваться между выполнениями и что ускорители должны оставаться в состоянии высокой производительности на протяжении всего пакета.

ANeuralNetworksBurst вносит лишь небольшое изменение в обычный путь выполнения. Вы создаете пакетный объект с помощью ANeuralNetworksBurst_create , как показано в следующем фрагменте кода:

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

Пакетные исполнения являются синхронными. Однако вместо использования ANeuralNetworksExecution_compute для выполнения каждого вывода вы соединяете различные объекты ANeuralNetworksExecution с одним и тем же ANeuralNetworksBurst в вызовах функции 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
// ...

Освободите объект ANeuralNetworksBurst с помощью ANeuralNetworksBurst_free , когда он больше не нужен.

// Cleanup
ANeuralNetworksBurst_free(burst);

Асинхронные очереди команд и изолированное выполнение

В Android 11 и более поздних версиях NNAPI поддерживает дополнительный способ планирования асинхронного выполнения с помощью метода ANeuralNetworksExecution_startComputeWithDependencies() . При использовании этого метода выполнение ожидает сигнала обо всех зависимых событиях, прежде чем начать оценку. Как только выполнение завершено и выходные данные готовы к использованию, возвращается сигнал о возвращенном событии.

В зависимости от того, какие устройства обрабатывают выполнение, событие может поддерживаться барьером синхронизации . Вы должны вызвать ANeuralNetworksEvent_wait() чтобы дождаться события и восстановить ресурсы, использованные при выполнении. Вы можете импортировать границы синхронизации в объект события с помощью ANeuralNetworksEvent_createFromSyncFenceFd() и экспортировать границы синхронизации из объекта события с помощью ANeuralNetworksEvent_getSyncFenceFd() .

Выходные данные с динамическим размером

Для поддержки моделей, в которых размер выходных данных зависит от входных данных — то есть, где размер не может быть определен во время выполнения модели — используйте ANeuralNetworksExecution_getOutputOperandRank и ANeuralNetworksExecution_getOutputOperandDimensions .

В следующем примере кода показано, как это сделать:

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

Очистка

Шаг очистки обеспечивает освобождение внутренних ресурсов, используемых для вычислений.

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

Управление ошибками и резервное использование ЦП

Если во время разделения возникает ошибка, если драйвер не может скомпилировать (часть модели) или если драйвер не может выполнить скомпилированную модель (часть модели), NNAPI может вернуться к своей собственной реализации ЦП той или более операций.

Если клиент NNAPI содержит оптимизированные версии операции (как, например, TFLite), может быть полезно отключить резервный процессор ЦП и обрабатывать сбои с помощью оптимизированной реализации операции клиента.

В Android 10, если компиляция выполняется с использованием ANeuralNetworksCompilation_createForDevices , резервное использование ЦП будет отключено.

В Android P выполнение NNAPI возвращается к ЦП, если выполнение драйвера завершается сбоем. Это также верно для Android 10, когда используется ANeuralNetworksCompilation_create а не ANeuralNetworksCompilation_createForDevices .

Первое выполнение возвращается к этому единственному разделу, и если это по-прежнему не удается, оно повторяет всю модель на ЦП.

Если секционирование или компиляция не удались, вся модель будет опробована на ЦП.

Бывают случаи, когда некоторые операции не поддерживаются процессором, и в таких ситуациях компиляция или выполнение завершаются сбоем, а не откатываются назад.

Даже после отключения резервного использования ЦП в модели все еще могут быть операции, запланированные на ЦП. Если ЦП находится в списке процессоров, предоставленном ANeuralNetworksCompilation_createForDevices , и является либо единственным процессором, поддерживающим эти операции, либо процессором, который заявляет о наилучшей производительности для этих операций, он будет выбран в качестве основного (без резервного) исполнителя.

Чтобы гарантировать отсутствие выполнения ЦП, используйте ANeuralNetworksCompilation_createForDevices , исключив nnapi-reference из списка устройств. Начиная с Android P, можно отключить резервный вариант во время выполнения сборок DEBUG, установив для свойства debug.nn.partition значение 2.

Домены памяти

В Android 11 и более поздних версиях NNAPI поддерживает домены памяти, которые предоставляют интерфейсы распределителя для непрозрачных воспоминаний. Это позволяет приложениям передавать собственную память устройства при выполнении, так что NNAPI не копирует и не преобразовывает данные без необходимости при выполнении последовательных исполнений на одном и том же драйвере.

Функция домена памяти предназначена для тензоров, которые в основном являются внутренними по отношению к драйверу и не требуют частого доступа к стороне клиента. Примеры таких тензоров включают тензоры состояния в моделях последовательностей. Для тензоров, которым требуется частый доступ к ЦП на стороне клиента, вместо этого используйте пулы общей памяти.

Чтобы выделить непрозрачную память, выполните следующие действия:

  1. Вызовите функцию ANeuralNetworksMemoryDesc_create() , чтобы создать новый дескриптор памяти:

    // Create a memory descriptor
    ANeuralNetworksMemoryDesc* desc;
    ANeuralNetworksMemoryDesc_create(&desc);
    
  2. Укажите все предполагаемые роли ввода и вывода, вызвав ANeuralNetworksMemoryDesc_addInputRole() и 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. При необходимости укажите размеры памяти, вызвав ANeuralNetworksMemoryDesc_setDimensions() .

    // Specify the memory dimensions
    uint32_t dims[] = {3, 4};
    ANeuralNetworksMemoryDesc_setDimensions(desc, 2, dims);
    
  4. Завершите определение дескриптора, вызвав ANeuralNetworksMemoryDesc_finish() .

    ANeuralNetworksMemoryDesc_finish(desc);
    
  5. Выделите столько памяти, сколько вам нужно, передав дескриптор в ANeuralNetworksMemory_createFromDesc() .

    // Allocate two opaque memories with the descriptor
    ANeuralNetworksMemory* opaqueMem;
    ANeuralNetworksMemory_createFromDesc(desc, &opaqueMem);
    
  6. Освободите дескриптор памяти, когда он вам больше не нужен.

    ANeuralNetworksMemoryDesc_free(desc);
    

Клиент может использовать созданный объект ANeuralNetworksMemory только с ANeuralNetworksExecution_setInputFromMemory() или ANeuralNetworksExecution_setOutputFromMemory() в соответствии с ролями, указанными в объекте ANeuralNetworksMemoryDesc . Аргументы смещения и длины должны быть установлены в 0, что указывает на то, что используется вся память. Клиент также может явно устанавливать или извлекать содержимое памяти с помощью ANeuralNetworksMemory_copy() .

Вы можете создавать непрозрачные воспоминания с ролями неопределенного размера или ранга. В этом случае создание памяти может завершиться неудачей со статусом ANEURALNETWORKS_OP_FAILED , если оно не поддерживается базовым драйвером. Клиенту рекомендуется реализовать резервную логику, выделив достаточно большой буфер, поддерживаемый Ashmem или AHardwareBuffer в режиме BLOB.

Когда NNAPI больше не требуется доступ к непрозрачному объекту памяти, освободите соответствующий экземпляр ANeuralNetworksMemory :

ANeuralNetworksMemory_free(opaqueMem);

Измерение производительности

Вы можете оценить производительность своего приложения, измеряя время выполнения или профилируя.

Время выполнения

Если вы хотите определить общее время выполнения во время выполнения, вы можете использовать API синхронного выполнения и измерить время, затраченное на вызов. Если вы хотите определить общее время выполнения на более низком уровне программного стека, вы можете использовать ANeuralNetworksExecution_setMeasureTiming и ANeuralNetworksExecution_getDuration , чтобы получить:

  • время выполнения на ускорителе (а не в драйвере, работающем на хост-процессоре).
  • время выполнения в драйвере, включая время на акселераторе.

Время выполнения в драйвере исключает накладные расходы, например, на саму среду выполнения и IPC, необходимые для связи среды выполнения с драйвером.

Эти API измеряют продолжительность между событиями отправки и завершения работы, а не время, которое драйвер или ускоритель тратит на выполнение вывода, возможно, прерываемого переключением контекста.

Например, если начинается вывод 1, затем водитель прекращает работу для выполнения вывода 2, затем возобновляет работу и завершает вывод 1, то во время выполнения вывода 1 будет включено время, когда работа была остановлена ​​для выполнения вывода 2.

Эта информация о времени может быть полезна для производственного развертывания приложения для сбора телеметрии для автономного использования. Вы можете использовать данные синхронизации, чтобы изменить приложение для повышения производительности.

При использовании этой функции имейте в виду следующее:

  • Сбор информации о времени может привести к снижению производительности.
  • Только драйвер способен подсчитать время, проведенное в себе или на ускорителе, не считая времени, проведенного в среде выполнения NNAPI и в IPC.
  • Эти API можно использовать только с ANeuralNetworksExecution , созданным с помощью ANeuralNetworksCompilation_createForDevices с numDevices = 1 .
  • Ни один водитель не обязан иметь возможность сообщать информацию о времени.

Профилируйте свое приложение с помощью Android Systrace

Начиная с Android 10, NNAPI автоматически генерирует события systrace , которые вы можете использовать для профилирования своего приложения.

Источник NNAPI поставляется с утилитой parse_systrace для обработки событий Systrace, сгенерированных вашим приложением, и создания вида таблицы, показывающего время, проведенное на разных этапах жизненного цикла модели (инстанция, подготовка, выполнение компиляции и прекращение) и различные слои применения . Слои, в которых разделено ваше приложение:

  • Application : основной код приложения
  • Runtime : NNAPI Runtime
  • IPC : взаимосвязь между процессом между временем выполнения NNAPI и кодом драйвера
  • Driver : процесс драйвера ускорителя.

Генерировать данные профилирования анализа

Предполагая, что вы проверили дерево источника AOSP в $ android_build_top, и, используя пример классификации Tflite Image в качестве целевого приложения, вы можете генерировать данные профилирования NNAPI со следующими шагами:

  1. Начните Systrace Android со следующей командой:
$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

Параметр -o trace.html указывает, что следы будут записаны в trace.html . При профилировании собственного приложения вам нужно будет заменить org.tensorflow.lite.examples.classification с помощью имени процесса, указанного в манифесте вашего приложения.

Это будет занять одну из вашей консоли оболочки, не запускайте команду в фоновом режиме, так как он интерактивно ожидает завершения enter .

  1. После того, как коллекционер Systrace запустит, запустите приложение и запустите тест на эталон.

В нашем случае вы можете запустить приложение классификации изображений из Android Studio или непосредственно с вашего пользовательского интерфейса Test Phone, если приложение уже установлено. Чтобы сгенерировать некоторые данные NNAPI, вам необходимо настроить приложение для использования NNAPI, выбрав NNAPI в качестве целевого устройства в диалоговом окне «Конфигурация приложения».

  1. Когда тест завершается, прекратите Systrace, нажав enter на консольном терминале, активном с шага 1.

  2. Запустите утилиту systrace_parser Generate Совокупную статистику:

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

Паризер принимает следующие параметры: - --total-times : показывает общее время, проведенное в слое, включая время, потраченное на ожидание выполнения при вызове базового уровня - --print-detail : печатает все события, которые были Собранно из Systrace - --per-execution : печатает только выполнение и его субфазы (как время для выполнения) вместо статистики для всех этапов - --json : производит выход в формате JSON

Пример вывода показан ниже:

===========================================================================================================================================
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

Сигнал может потерпеть неудачу, если собранные события не представляют полного следа приложения. В частности, это может потерпеть неудачу, если в Systrace события, сгенерированные для обозначения конца раздела, присутствуют в трассировке без связанного события начала секции. Обычно это происходит, если при запуске Systrace Collector генерируются некоторые события из предыдущего сеанса профилирования. В этом случае вам придется снова запустить свое профилирование.

Добавить статистику для кода приложения в Systrace_parser

Приложение Parse_systrace основано на встроенной функциональности Android Systrace. Вы можете добавить следы для конкретных операций в вашем приложении, используя Systrace API ( для Java , для нативных приложений ) с помощью пользовательских имен событий.

Чтобы связать свои пользовательские события с этапами жизненного цикла приложения, приготовьте название вашего события с одной из следующих строк:

  • [NN_LA_PI] : событие уровня приложения для инициализации
  • [NN_LA_PP] : событие уровня приложения для подготовки
  • [NN_LA_PC] : событие уровня приложения для компиляции
  • [NN_LA_PE] : событие уровня приложения для выполнения

Вот пример того, как вы можете изменить пример примера классификации изображений TFLITE, добавив раздел runInferenceModel для фазы Execution и слой Application , содержащий другие другие разделы preprocessBitmap , который не будет рассматриваться в трассах NNAPI. Раздел runInferenceModel будет частью событий Systrace, обработанных синестровским анализатором NNAPI:

Котлин

/** 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
}

Ява

/** 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;
}

Качество обслуживания

В Android 11 и выше NNAPI обеспечивает лучшее качество обслуживания (QOS), позволяя приложению указать относительные приоритеты своих моделей, максимальное количество времени, которое, как ожидается, подготовит данную модель, и максимальное количество времени, которое ожидается, завершит данный вычисление. Android 11 также вводит дополнительные коды результатов NNAPI, которые позволяют приложениям понимать сбои, такие как сроки пропущенного выполнения.

Установите приоритет рабочей нагрузки

Чтобы установить приоритет рабочей нагрузки NNAPI, вызовите ANeuralNetworksCompilation_setPriority() до вызова ANeuralNetworksCompilation_finish() .

Установить сроки

Приложения могут устанавливать сроки как для компиляции модели, так и для вывода.

Подробнее о операнде

В следующем разделе рассматриваются расширенные темы об использовании операндов.

Квантовые тензоры

Квантовый тензор-это компактный способ представления N-размерной массивы значений плавающей запятой.

NNAPI поддерживает 8-битные асимметричные квантовые тензоры. Для этих тензоров значение каждой ячейки представлено 8-битным целым числом. С тензором связан масштаб и нулевое значение. Они используются для преобразования 8-битных целых чисел в значения плавающей точки, которые представлены.

Формула:

(cellValue - zeroPoint) * scale

где значение нулевого значения является 32-разрядным целым числом, а шкала-32-разрядное значение плавающей запятой.

По сравнению с тензорами 32-битных значений плавающей запятой, 8-битные квантовые тензоры имеют два преимущества:

  • Ваше приложение меньше, так как обученные веса занимают четверть от 32-битных тензоров.
  • Вычисления часто можно выполнять быстрее. Это связано с меньшим количеством данных, которые необходимо извлечь из памяти и эффективность процессоров, таких как DSP, при выполнении целочисленной математики.

Хотя можно преобразовать модель с плавающей запятой в квантованную, наш опыт показал, что лучшие результаты достигаются путем непосредственного обучения квантовой модели. По сути, нейронная сеть учится компенсировать повышенную гранулярность каждого значения. Для каждого квантового тензора значения масштаба и нулевых значений определяются во время учебного процесса.

В NNAPI вы определяете квантованные типы тензоров, устанавливая поле типа ANeuralNetworksOperandType структуры данных ANEURALNETWORKS_TENSOR_QUANT8_ASYMM . Вы также указываете масштаб и нулевое значение тензора в этой структуре данных.

В дополнение к 8-битным асимметричным квантовым тензорам NNAPI поддерживает следующее:

Дополнительные операнды

Несколько операций, такие как ANEURALNETWORKS_LSH_PROJECTION , принимают дополнительные операнды. Чтобы указать в модели, что необязательный операнд опущен, вызовите функцию ANeuralNetworksModel_setOperandValue() , проходя NULL для буфера и 0 для длины.

Если решение о том, присутствует ли операнд или нет, варьируется для каждого выполнения, вы указываете, что операнд пропущен, используя функции ANeuralNetworksExecution_setInput() или ANeuralNetworksExecution_setOutput() , проходя NULL для буфера и 0 для длины.

Тенсоры неизвестного звания

Android 9 (API -уровень 28) представил модельные операнды неизвестных измерений, но известного ранга (количество измерений). Android 10 (уровень API 29) представил тензоры неизвестного ранга, как показано в AneurnetWorksOperAndType .

NNAPI

Бланка NNAPI доступен на AOSP в platform/test/mlts/benchmark (Benchmark App) и platform/test/mlts/models (модели и наборы данных).

Конфликт оценивает задержку и точность и сравнивает драйверы с той же работой, выполненной с использованием Tensorflow Lite, работающего на процессоре, для тех же моделей и наборов данных.

Чтобы использовать эталон, сделайте следующее:

  1. Подключите целевое устройство Android к компьютеру, откройте окно терминала и убедитесь, что устройство достижимо через ADB.

  2. Если подключено более одного устройства Android, экспортируйте целевое устройство ANDROID_SERIAL Environment Variable.

  3. Перейдите в справочный справочник Android Top Source.

  4. Запустите следующие команды:

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

    В конце эталонного запуска его результаты будут представлены в виде HTML-страницы, переданной в xdg-open .

NNAPI журналы

NNAPI генерирует полезную диагностическую информацию в системных журналах. Чтобы проанализировать журналы, используйте утилиту LogCat .

Включите многословную регистрацию NNAPI для конкретных фаз или компонентов, установив свой свойства debug.nn.vlog (с использованием adb shell ) для следующего списка значений, разделенных пространством, толстой кишкой или запятой:

  • model : построение модели
  • compilation : генерация плана выполнения модели и компиляции
  • execution : выполнение модели
  • cpuexe : выполнение операций с использованием реализации процессора NNAPI
  • manager : расширения NNAPI, доступные интерфейсы и информацию, связанные с возможностями
  • all или 1 : все элементы выше

Например, для включения полного журнала словесного регистрации используйте команду adb shell setprop debug.nn.vlog all . Чтобы отключить регистрацию словеса, используйте команду adb shell setprop debug.nn.vlog '""' .

После включения, журнала Verbose генерирует записи журнала на уровне информации с тегом, установленным на фазу или имени компонента.

Помимо контролируемых сообщений debug.nn.vlog , компоненты API NNAPI предоставляют другие записи журнала на разных уровнях, каждый из которых использует конкретный тег журнала.

Чтобы получить список компонентов, найдите дерево источника, используя следующее выражение:

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

Это выражение в настоящее время возвращает следующие теги:

  • Burstbuilder
  • Обратные вызовы
  • Компиляция строителя
  • CPUEXECUTOR
  • CERSUTIONBUILDER
  • CERSUTIONBURSTCONTROLLER
  • CERSUTIONBURSTSERVER
  • FEECTUTIONPLAN
  • Фибонакцидривер
  • График
  • IndexedShapeWrapper
  • Ionwatcher
  • Менеджер
  • Память
  • Memoryutils
  • Метамодель
  • Modelargumentinfo
  • Модельный задумчик
  • Neuralnetworks
  • Операция
  • Операции
  • Операции
  • PackageInfo
  • Tokenhasher
  • TypeManager
  • Утилиты
  • ValidateHal
  • Версииэнтерфейсы

Чтобы управлять уровнем сообщений журнала, показанного logcat , используйте переменную среды ANDROID_LOG_TAGS .

Чтобы показать полный набор сообщений журнала NNAPI и отключить любые другие, установите ANDROID_LOG_TAGS на следующее:

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.

Вы можете установить ANDROID_LOG_TAGS используя следующую команду:

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

Обратите внимание, что это всего лишь фильтр, который применяется к logcat . Вам по -прежнему нужно установить свойство debug.nn.vlog для all , чтобы генерировать информацию журнала словеса.

,

API API Android Neural Networks (NNAPI) представляет собой API Android C, разработанный для работы вычислительно интенсивных операций для машинного обучения на устройствах Android. NNAPI предназначен для обеспечения базового уровня функциональности для средств машинного обучения более высокого уровня, таких как Tensorflow Lite и Caffe2, которые строят и обучают нейронные сети. API доступен на всех устройствах Android под управлением Android 8.1 (API -уровень 27) или выше.

NNAPI поддерживает вывод, применяя данные от устройств Android к ранее обученным, определенным разработчикам моделям. Примеры вывода включают классификацию изображений, прогнозирование поведения пользователей и выбор соответствующих ответов на поисковый запрос.

Вывод о выводе имеет много преимуществ:

  • Задержка : вам не нужно отправлять запрос через сетевое соединение и ждать ответа. Например, это может иметь решающее значение для видео приложений, которые обрабатывают последовательные кадры, поступающие с камеры.
  • Доступность : приложение работает даже при охвате сети.
  • Скорость : новое оборудование, которое специфично для обработки нейронной сети обеспечивает значительно более быстрые вычисления, чем только процессор общего назначения, в одиночку.
  • Конфиденциальность : данные не покидают устройство Android.
  • Стоимость : Ферма сервера не требуется, когда все вычисления выполняются на устройстве Android.

Есть также компромиссы, которые разработчик должен помнить:

  • Использование системы : оценка нейронных сетей включает в себя много вычислений, которые могут увеличить использование питания батареи. Вам следует рассмотреть возможность мониторинга здоровья батареи, если это проблема для вашего приложения, особенно для длительных вычислений.
  • Размер приложения : обратите внимание на размер ваших моделей. Модели могут занимать несколько мегабайт пространства. Если связать большие модели в вашем APK намеренно повлияет на ваших пользователей, вы можете рассмотреть вопрос о загрузке моделей после установки приложения, с использованием более мелких моделей или запуска ваших вычислений в облаке. NNAPI не предоставляет функциональность для запуска моделей в облаке.

См. Пример API API Android Neural Networks , чтобы увидеть один пример того, как использовать NNAPI.

Понять во время выполнения нейронных сетей API

NNAPI предназначен для того, чтобы называть библиотеками машинного обучения, фреймворками и инструментами, которые позволяют разработчикам обучать свои модели отключение и развернуть их на устройствах Android. Приложения, как правило, не будут использовать NNAPI напрямую, но вместо этого будут использовать структуры машинного обучения более высокого уровня. Эти рамки, в свою очередь, могут использовать NNAPI для выполнения аппаратного ускорения вывода на поддерживаемых устройствах.

Основываясь на требованиях приложения и аппаратных возможностях на устройстве Android, время выполнения нейронной сети Android может эффективно распространять рабочую нагрузку вычислений по доступным процессорам на устройствах, включая специальное оборудование нейронной сети, графические единицы (графические процессоры) и цифровые сигналы (DSP ).

Для устройств Android, в которых отсутствует специализированный драйвер поставщика, время выполнения NNAPI выполняет запросы на процессоре.

На рисунке 1 показана архитектура системы высокого уровня для NNAPI.

Рисунок 1. Архитектура системы для API API Android Neural Networks

Модель программирования Neural Networks API

Чтобы выполнить вычисления, используя NNAPI, вам сначала необходимо построить направленный график, который определяет вычисления для выполнения. Этот график вычислений в сочетании с вашим входным данных (например, веса и смещения, передаваемые из структуры машинного обучения), образует модель для оценки времени выполнения NNAPI.

NNAPI использует четыре основные абстракции:

  • Модель : вычислительный график математических операций и постоянные значения, изученные в рамках учебного процесса. Эти операции специфичны для нейронных сетей. Они включают 2-мерную (2D) свертку , логистическую ( сигмоидную ) активацию, выпрямленную линейную (RELU) активацию и многое другое. Создание модели - это синхронная операция. После успешного создания его можно повторно использовать по потокам и компиляциям. В NNAPI модель представлена ​​в качестве экземпляра ANeuralNetworksModel .
  • Компиляция : представляет конфигурацию для составления модели NNAPI в код нижнего уровня. Создание компиляции - это синхронная операция. После успешного создания его можно повторно использоваться по потокам и казням. В NNAPI каждая компиляция представлена ​​в качестве экземпляра ANeuralNetworksCompilation .
  • Память : представляет общую память, отображенные файлы памяти и аналогичные буферы памяти. Использование буфера памяти позволяет более эффективно передавать данные времени выполнения NNAPI для драйверов. Приложение обычно создает один общий буфер памяти, который содержит каждый тензор, необходимый для определения модели. Вы также можете использовать буферы памяти для хранения входов и выходов для экземпляра выполнения. В NNAPI каждый буфер памяти представлен в качестве экземпляра ANeuralNetworksMemory .
  • Выполнение : интерфейс для применения модели NNAPI к набору входов и для сбора результатов. Выполнение может быть выполнено синхронно или асинхронно.

    Для асинхронного выполнения несколько потоков могут подождать при одном и том же выполнении. Когда это выполнение завершается, все потоки выпускаются.

    В NNAPI каждое исполнение представлено в качестве экземпляра ANeuralNetworksExecution .

На рисунке 2 показан основной поток программирования.

Рисунок 2. Поток программирования для API API Android Neural Networks

Остальная часть этого раздела описывает шаги для настройки вашей модели NNAPI для выполнения вычислений, компиляции модели и выполнения скомпилированной модели.

Предоставьте доступ к данным обучения

Ваши обученные веса и смещения данных, вероятно, хранятся в файле. Чтобы обеспечить время выполнения NNAPI с эффективным доступом к этим данным, создайте экземпляр ANeuralNetworksMemory , вызывая функцию ANeuralNetworksMemory_createFromFd() и пропустив дескриптор файла открытого файла данных. Вы также указываете флаги защиты памяти и смещение, где запускается область общей памяти в файле.

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

Хотя в этом примере мы используем только один экземпляр ANeuralNetworksMemory для всех наших весов, можно использовать более одного экземпляра ANeuralNetworksMemory для нескольких файлов.

Используйте нативные аппаратные буферы

Вы можете использовать собственные аппаратные буферы для входов моделей, выходов и постоянных значений операнда. В некоторых случаях ускоритель NNAPI может получить доступ к объектам AHardwareBuffer без необходимости копировать данные. AHardwareBuffer имеет много разных конфигураций, и не каждый ускоритель NNAPI может поддерживать все эти конфигурации. Из -за этого ограничения обратитесь к ограничениям, перечисленным в ANeuralNetworksMemory_createFromAHardwareBuffer справочную документацию и заранее тестируйте на целевых устройствах, чтобы обеспечить компиляции и выполнения, в которых используется ведущий ведущий AHardwareBuffer , как и ожидалось, используя назначение устройства для определения ускорителя.

Чтобы позволить времени выполнения NNAPI получить доступ к объекту AHardwareBuffer , создайте экземпляр ANeuralNetworksMemory , вызывая функцию ANeuralNetworksMemory_createFromAHardwareBuffer и пропустив в объекте AHardwareBuffer , как показано в следующем примере кода:

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

Когда NNAPI больше не нужно получать доступ к объекту AHardwareBuffer , бесплатны соответствующим экземпляру ANeuralNetworksMemory :

ANeuralNetworksMemory_free(mem2);

Примечание:

  • Вы можете использовать AHardwareBuffer только для всего буфера; Вы не можете использовать его с параметром ARect .
  • Среда выполнения NNAPI не будет промывать буфер. Вы должны убедиться, что входные и выходные буферы доступны перед планированием выполнения.
  • Не существует поддержки для синхронизационных дескрипторов файлов забора.
  • Для AHardwareBuffer с форматами, специфичными для поставщиков и битами использования, она зависит от реализации поставщика, чтобы определить, отвечает ли клиент или драйвер за промывание кэша.

Модель

Модель является фундаментальной единицей вычислений в NNAPI. Каждая модель определяется одним или несколькими операндами и операциями.

Операнды

Операнды - это объекты данных, используемые при определении графика. К ним относятся входы и выходы модели, промежуточные узлы, которые содержат данные, которые текут от одной операции к другой, и константы, которые передаются в эти операции.

Существует два типа операндов, которые можно добавить в модели NNAPI: скаляры и тензоры .

Скаляр представляет единственное значение. NNAPI поддерживает скалярные значения в булевой 16-битной плавающей точке, 32-разрядной плавающей запятой, 32-разрядной целочисленной и без знаковой 32-битной целочисленной форматы.

Большинство операций в NNAPI включают тензоры. Тензоры являются n-мерными массивами. NNAPI поддерживает тензоры с 16-разрядной плавающей запятой, 32-разрядной плавающей запятой, 8-битным квантовым , 16-битным квантовым, 32-битным целочисленным и 8-битным логическим значением.

Например, на рисунке 3 представлена ​​модель с двумя операциями: добавление, за которым следует умножение. Модель берет входной тензор и производит один выходной тензор.

Рисунок 3. Пример операндов для модели NNAPI

Приведенная выше модель имеет семь операндов. Эти операнды неявно идентифицируются по индексу порядка, в котором они добавляются в модель. Первый добавленный операнд имеет индекс 0, второй - индекс 1 и т. Д. Операнды 1, 2, 3 и 5 являются постоянными операндами.

Порядок, в котором вы добавляете операнды, не имеет значения. Например, выходной операнд модели может быть первым добавленным. Важной частью является использование правильного значения индекса при ссылке на операнд.

Операнды имеют типы. Они указаны, когда они добавляются в модель.

Операнд не может быть использован как вход, так и вывод модели.

Каждый операнд должен быть либо входом модели, константой или выходным операндом ровно одной операции.

Для получения дополнительной информации об использовании операндов см. Подробнее о операнде .

Операции

Операция указывает вычисления, которые будут выполнены. Каждая операция состоит из этих элементов:

  • тип операции (например, добавление, умножение, свертка),
  • список индексов операндов, которые операция использует для ввода, и
  • Список индексов операндов, которые операция использует для вывода.

Порядок в этих списках имеет значение; См. Справочник NNAPI API для ожидаемых входов и выходов каждого типа операции.

Вы должны добавить операнды, которые операция потребляет или производит в модель перед добавлением операции.

Порядок, в котором вы добавляете операции, не имеет значения. NNAPI зависит от зависимостей, установленных графиком вычислений операндов и операций, чтобы определить порядок, в котором выполняются операции.

Операции, которые поддерживает NNAPI, суммированы в таблице ниже:

Категория Операции
Элементные математические операции
Манипулирование тензором
Операции изображений
Операции в поисках
Операции нормализации
Строительные операции
Объединение операций
Активационные операции
Прочие операции

Известная проблема на уровне API 28: при передаче ANEURALNETWORKS_TENSOR_QUANT8_ASYMM Tensors в операцию ANEURALNETWORKS_PAD , которая доступна на Android 9 (уровень API 28) и выше, выход из NNAPI может не совпадать с выводом из-за среднего обучения более высокого уровня, такая как LensorFlow Lyte Lyte Lyte Lyte Lyte Lyte Lyte Lyte Lyte Lyt . Вместо этого вы должны передать только ANEURALNETWORKS_TENSOR_FLOAT32 . Проблема решается в Android 10 (API -уровне 29) и выше.

Построить модели

В следующем примере мы создаем модель с двумя операциями, найденную на рисунке 3 .

Чтобы построить модель, следуйте этим шагам:

  1. Вызовите функцию ANeuralNetworksModel_create() , чтобы определить пустую модель.

    ANeuralNetworksModel* model = NULL;
    ANeuralNetworksModel_create(&model);
    
  2. Добавьте операнды в вашу модель, позвонив в ANeuralNetworks_addOperand() . Их типы данных определяются с использованием структуры данных 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. Для операндов, которые имеют постоянные значения, такие как вес и смещения, которые ваше приложение получает из учебного процесса, используйте функции ANeuralNetworksModel_setOperandValue() и ANeuralNetworksModel_setOperandValueFromMemory() .

    В следующем примере мы устанавливаем постоянные значения из файла обучающих данных, соответствующих буферу памяти, который мы создали в предоставлении доступа к учебным данным .

    // 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. Для каждой операции на направленном графике, которую вы хотите вычислить, добавьте операцию в свою модель, вызывая функцию ANeuralNetworksModel_addOperation() .

    В качестве параметров этого вызова, ваше приложение должно предоставить:

    • Тип операции
    • количество входных значений
    • массив индексов для входных операндов
    • количество выходных значений
    • массив индексов для выходных операндов

    Обратите внимание, что операнд не может быть использован как для ввода, так и для вывода одной и той же операции.

    // 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. Определите, какие операнды должны рассматривать модель как входы и выходы, вызывая функцию 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. Необязательно, указать, разрешено ли было рассчитывать ANEURALNETWORKS_TENSOR_FLOAT32 с диапазоном или точностью, как и в 16-битном формате с плавающей точкой IEEE 754, вызывая ANeuralNetworksModel_relaxComputationFloat32toFloat16() .

  7. Позвоните ANeuralNetworksModel_finish() чтобы завершить определение вашей модели. Если ошибок нет, эта функция возвращает код результата ANEURALNETWORKS_NO_ERROR .

    ANeuralNetworksModel_finish(model);
    

После создания модели вы можете скомпилировать ее любое количество раз и выполнить каждую компиляцию в любое количество раз.

Поток управления

Чтобы включить поток управления в модель NNAPI, сделайте следующее:

  1. Построить соответствующие подграфы выполнения ( then и else подграфы для оператора IF , condition и подграфов body для цикла WHILE ) в качестве автономных моделей ANeuralNetworksModel* :

    ANeuralNetworksModel* thenModel = makeThenModel();
    ANeuralNetworksModel* elseModel = makeElseModel();
    
  2. Создайте операнды, которые ссылаются на эти модели в модели, содержащей поток управления:

    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. Добавьте операцию потока управления:

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

Сборник

Шаг компиляции определяет, какие процессоры будут выполнены ваша модель, и просит соответствующие драйверы подготовиться к выполнению. Это может включать в себя генерацию машинного кода, специфичную для процессоров, на которой будет запущена ваша модель.

Чтобы составить модель, выполните следующие действия:

  1. Вызовите функцию ANeuralNetworksCompilation_create() , чтобы создать новый экземпляр компиляции.

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

    При желании вы можете использовать назначение устройства для явного выбора, на какие устройства можно выполнить.

  2. По желанию вы можете повлиять на то, как время выполнения сделки между использованием питания аккумулятора и скоростью выполнения. Вы можете сделать это, позвонив ANeuralNetworksCompilation_setPreference() .

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

    Предпочтения, которые вы можете указать, включают:

    • ANEURALNETWORKS_PREFER_LOW_POWER : предпочитаю выполнять таким образом, чтобы минимизировать утечку батареи. Это желательно для компиляций, которые часто выполняются.
    • ANEURALNETWORKS_PREFER_FAST_SINGLE_ANSWER : предпочитаю вернуть один ответ как можно быстрее, даже если это вызывает больше энергопотребления. Это дефолт.
    • ANEURALNETWORKS_PREFER_SUSTAINED_SPEED : предпочитаю максимизировать пропускную способность последовательных кадров, например, при обработке последовательных кадров, поступающих из камеры.
  3. Вы можете настроить кэширование компиляции, вызывая ANeuralNetworksCompilation_setCaching .

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

    Используйте getCodeCacheDir() для cacheDir . Указанный token должен быть уникальным для каждой модели в приложении.

  4. Завершите определение компиляции, вызывая ANeuralNetworksCompilation_finish() . Если ошибок нет, эта функция возвращает код результата ANEURALNETWORKS_NO_ERROR .

    ANeuralNetworksCompilation_finish(compilation);
    

Обнаружение устройства и назначение

На устройствах Android, работающих на Android 10 (API -уровне 29) и выше, NNAPI предоставляет функции, которые позволяют библиотекам и приложениям для машинного обучения для получения информации о доступных устройствах, и указывают устройства, которые будут использоваться для выполнения. Предоставление информации о доступных устройствах позволяет приложениям получить точную версию драйверов, найденных на устройстве, чтобы избежать известной несовместимости. Предоставляя приложения возможность указать, какие устройства должны выполнять различные разделы модели, приложения могут быть оптимизированы для устройства Android, на котором они развернуты.

Обнаружение устройства

Используйте ANeuralNetworks_getDeviceCount чтобы получить количество доступных устройств. Для каждого устройства используйте ANeuralNetworks_getDevice , чтобы установить экземпляр ANeuralNetworksDevice на ссылку на это устройство.

После того, как у вас есть ссылка на устройство, вы можете узнать дополнительную информацию об этом устройстве, используя следующие функции:

Назначение устройства

Используйте ANeuralNetworksModel_getSupportedOperationsForDevices чтобы выяснить, какие операции модели можно выполнить на определенных устройствах.

Чтобы контролировать, какие ускорители использовать для выполнения, вызовите ANeuralNetworksCompilation_createForDevices вместо ANeuralNetworksCompilation_create . Используйте полученный объект ANeuralNetworksCompilation , как обычно. Функция возвращает ошибку, если предоставленная модель содержит операции, которые не поддерживаются выбранными устройствами.

Если указано несколько устройств, время выполнения отвечает за распространение работы по устройствам.

Подобно другим устройствам, реализация процессора NNAPI представлена ANeuralNetworksDevice с именем nnapi-reference и типом ANEURALNETWORKS_DEVICE_TYPE_CPU . При вызове ANeuralNetworksCompilation_createForDevices реализация ЦП не используется для обработки случаев сбоя для компиляции модели и выполнения.

Приложение несет ответственность за разделение модели на подмодели, которые могут работать на указанных устройствах. Приложения, которые не должны выполнять ручной раздел, должны продолжать вызывать более простую ANeuralNetworksCompilation_create для использования всех доступных устройств (включая ЦП) для ускорения модели. Если модель не может быть полностью поддержана устройствами, которые вы указали, используя ANeuralNetworksCompilation_createForDevices , ANEURALNETWORKS_BAD_DATA возвращается.

Модель разделения

Когда для модели доступно несколько устройств, время выполнения NNAPI распределяет работу по устройствам. Например, если более одного устройства было предоставлено ANeuralNetworksCompilation_createForDevices , все указанные будут рассматриваться при распределении работы. Обратите внимание, что, если устройства процессора не находится в списке, выполнение ЦП будет отключено. При использовании ANeuralNetworksCompilation_create будут приняты все доступные устройства, включая процессор.

Распределение осуществляется путем выбора из списка доступных устройств для каждой из операций в модели, устройства, поддерживающего операцию, и объявления наилучшей производительности, т.е. самое быстрое время выполнения или самое низкое энергопотребление, в зависимости от указанного предпочтения выполнения. клиентом. Этот алгоритм разделения не учитывает возможную неэффективность, вызванную IO между различными процессорами, поэтому при определении нескольких процессоров (либо явно при использовании ANeuralNetworksCompilation_createForDevices , либо неявно, используя ANeuralNetworksCompilation_create ). Важно профилировать получение применения.

Чтобы понять, как ваша модель была разделена NNAPI, проверьте журналы Android для сообщения (на уровне информации с помощью TAG ExecutionPlan ):

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

op-name -это описательное имя операции на графике, а device-index -это индекс устройства кандидата в списке устройств. Этот список является вводом, предоставляемым для ANeuralNetworksCompilation_createForDevices или, если использование ANeuralNetworksCompilation_createForDevices , список устройств, возвращаемых при итерации над всеми устройствами с использованием ANeuralNetworks_getDeviceCount и ANeuralNetworks_getDevice .

Сообщение (на уровне информации с тегом ExecutionPlan ):

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

Это сообщение указывает, что весь график был ускорен на device-name .

Исполнение

Шаг выполнения применяет модель к набору входов и сохраняет вычисления вычислений к одному или нескольким пользовательским буферам или пространствам для памяти, которые выделяло ваше приложение.

Чтобы выполнить скомпилированную модель, выполните следующие действия:

  1. Вызовите функцию ANeuralNetworksExecution_create() , чтобы создать новый экземпляр выполнения.

    // Run the compiled model against a set of inputs
    ANeuralNetworksExecution* run1 = NULL;
    ANeuralNetworksExecution_create(compilation, &run1);
    
  2. Укажите, где ваше приложение считывает входные значения для вычисления. Ваше приложение может считывать значения входных значений либо из пользовательского буфера, либо из выделенного пространства памяти, вызывая ANeuralNetworksExecution_setInput() или 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. Укажите, где ваше приложение записывает выходные значения. Your app can write output values to either a user buffer or an allocated memory space, by calling ANeuralNetworksExecution_setOutput() or ANeuralNetworksExecution_setOutputFromMemory() respectively.

    // Set the output
    float32 myOutput[3][4];
    ANeuralNetworksExecution_setOutput(run1, 0, NULL, myOutput, sizeof(myOutput));
    
  4. Schedule the execution to start, by calling the ANeuralNetworksExecution_startCompute() function. If there are no errors, this function returns a result code of ANEURALNETWORKS_NO_ERROR .

    // Starts the work. The work proceeds asynchronously
    ANeuralNetworksEvent* run1_end = NULL;
    ANeuralNetworksExecution_startCompute(run1, &run1_end);
    
  5. Call the ANeuralNetworksEvent_wait() function to wait for the execution to complete. If the execution was successful, this function returns a result code of ANEURALNETWORKS_NO_ERROR . Waiting can be done on a different thread than the one starting the execution.

    // 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. Optionally, you can apply a different set of inputs to the compiled model by using the same compilation instance to create a new ANeuralNetworksExecution instance.

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

Synchronous execution

Asynchronous execution spends time to spawn and synchronize threads. Furthermore, the latency can be hugely variable, with the longest delays reaching up to 500 microseconds between the time a thread is notified or woken and the time it is eventually bound to a CPU core.

To improve latency, you can instead direct an application to make a synchronous inference call to the runtime. That call will return only once an inference has been completed rather than returning once an inference has been started. Instead of calling ANeuralNetworksExecution_startCompute for an asynchronous inference call to the runtime, the application calls ANeuralNetworksExecution_compute to make a synchronous call to the runtime. A call to ANeuralNetworksExecution_compute does not take an ANeuralNetworksEvent and is not paired with a call to ANeuralNetworksEvent_wait .

Burst executions

On Android devices running Android 10 (API level 29) and higher, the NNAPI supports burst executions through the ANeuralNetworksBurst object. Burst executions are a sequence of executions of the same compilation that occur in rapid succession, such as those operating on frames of a camera capture or successive audio samples. Using ANeuralNetworksBurst objects may result in faster executions, as they indicate to accelerators that resources may be reused between executions and that accelerators should remain in a high-performance state for the duration of the burst.

ANeuralNetworksBurst introduces only a small change in the normal execution path. You create a burst object using ANeuralNetworksBurst_create , as shown in the following code snippet:

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

Burst executions are synchronous. However, instead of using ANeuralNetworksExecution_compute to perform each inference, you pair the various ANeuralNetworksExecution objects with the same ANeuralNetworksBurst in calls to the function 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
// ...

Free the ANeuralNetworksBurst object with ANeuralNetworksBurst_free when it is no longer needed.

// Cleanup
ANeuralNetworksBurst_free(burst);

Asynchronous command queues and fenced execution

In Android 11 and higher, NNAPI supports an additional way to schedule asynchronous execution through the ANeuralNetworksExecution_startComputeWithDependencies() method. When you use this method, the execution waits for all of the depending events to be signaled before starting the evaluation. Once the execution has completed and the outputs are ready to be consumed, the returned event is signaled.

Depending on which devices handle the execution, the event might be backed by a sync fence . You must call ANeuralNetworksEvent_wait() to wait for the event and recuperate the resources that the execution used. You can import sync fences to an event object using ANeuralNetworksEvent_createFromSyncFenceFd() , and you can export sync fences from an event object using ANeuralNetworksEvent_getSyncFenceFd() .

Dynamically sized outputs

To support models where the size of the output depends on the input data—that is, where the size cannot be determined at model execution time—use ANeuralNetworksExecution_getOutputOperandRank and ANeuralNetworksExecution_getOutputOperandDimensions .

The following code sample shows how to do this:

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

Cleanup

The cleanup step handles the freeing of internal resources used for your computation.

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

Error management and CPU fallback

If there is an error during partitioning, if a driver fails to compile a (piece of a) model, or if a driver fails to execute a compiled (piece of a) model, NNAPI might fall back to its own CPU implementation of the one or more operations.

If the NNAPI client contains optimized versions of the operation (as, for example, TFLite) it might be advantageous to disable the CPU fallback and handle the failures with the client's optimized operation implementation.

In Android 10, if compilation is performed using ANeuralNetworksCompilation_createForDevices , then CPU fallback will be disabled.

In Android P, NNAPI execution falls back to the CPU if execution on the driver fails. This is also true on Android 10 when ANeuralNetworksCompilation_create rather than ANeuralNetworksCompilation_createForDevices is used.

First execution falls back for that single partition, and if that still fails, it retries the entire model on the CPU.

If partitioning or compilation fails, the entire model will be tried on CPU.

There are cases where some operations are not supported on CPU, and in such situations compilation or execution will fail rather than falling back.

Even after disabling CPU fallback, there may still be operations in the model that are scheduled on the CPU. If the CPU is in the list of processors supplied to ANeuralNetworksCompilation_createForDevices , and is either the only processor that supports those operations or is the processor that claims best performance for those operations, it will be chosen as a primary (non-fallback) executor.

To ensure there is no CPU execution, use ANeuralNetworksCompilation_createForDevices while excluding the nnapi-reference from the list of devices. Starting in Android P, it is possible to disable fallback at execution time on DEBUG builds by setting the debug.nn.partition property to 2.

Домены памяти

In Android 11 and higher, NNAPI supports memory domains that provide allocator interfaces for opaque memories. This allows applications to pass device-native memories across executions, so that NNAPI does not copy or transform data unnecessarily when performing consecutive executions on the same driver.

The memory domain feature is intended for tensors that are mostly internal to the driver and that don't need frequent access to the client side. Примеры таких тензоров включают тензоры состояния в моделях последовательностей. For tensors that need frequent CPU access on the client side, use shared memory pools instead.

To allocate an opaque memory, perform the following steps:

  1. Call the ANeuralNetworksMemoryDesc_create() function to create a new memory descriptor:

    // Create a memory descriptor
    ANeuralNetworksMemoryDesc* desc;
    ANeuralNetworksMemoryDesc_create(&desc);
    
  2. Specify all of the intended input and output roles by calling ANeuralNetworksMemoryDesc_addInputRole() and 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. Optionally, specify the memory dimensions by calling ANeuralNetworksMemoryDesc_setDimensions() .

    // Specify the memory dimensions
    uint32_t dims[] = {3, 4};
    ANeuralNetworksMemoryDesc_setDimensions(desc, 2, dims);
    
  4. Finalize the descriptor definition by calling ANeuralNetworksMemoryDesc_finish() .

    ANeuralNetworksMemoryDesc_finish(desc);
    
  5. Allocate as many memories as you need by passing the descriptor to ANeuralNetworksMemory_createFromDesc() .

    // Allocate two opaque memories with the descriptor
    ANeuralNetworksMemory* opaqueMem;
    ANeuralNetworksMemory_createFromDesc(desc, &opaqueMem);
    
  6. Free the memory descriptor when you no longer need it.

    ANeuralNetworksMemoryDesc_free(desc);
    

The client may only use the created ANeuralNetworksMemory object with ANeuralNetworksExecution_setInputFromMemory() or ANeuralNetworksExecution_setOutputFromMemory() according to the roles specified in the ANeuralNetworksMemoryDesc object. The offset and length arguments must be set to 0, indicating that the whole memory is used. The client may also explicitly set or extract the contents of the memory by using ANeuralNetworksMemory_copy() .

You can create opaque memories with roles of unspecified dimensions or rank. In that case, the memory creation might fail with the ANEURALNETWORKS_OP_FAILED status if it is not supported by the underlying driver. The client is encouraged to implement fallback logic by allocating a large enough buffer backed by Ashmem or BLOB-mode AHardwareBuffer .

When NNAPI no longer needs to access the opaque memory object, free the corresponding ANeuralNetworksMemory instance:

ANeuralNetworksMemory_free(opaqueMem);

Measure performance

You can evaluate your app's performance by measuring execution time or by profiling.

Execution time

When you want to determine total execution time through the runtime, you can use the synchronous execution API and measure the time taken by the call. When you want to determine total execution time through a lower level of the software stack, you can use ANeuralNetworksExecution_setMeasureTiming and ANeuralNetworksExecution_getDuration to get:

  • execution time on an accelerator (not in the driver, which runs on the host processor).
  • execution time in the driver, including time on the accelerator.

The execution time in the driver excludes overhead such as that of the runtime itself and the IPC needed for the runtime to communicate with the driver.

These APIs measure duration between the work submitted and work completed events, rather than the time a driver or accelerator devotes to performing the inference, possibly interrupted by context switching.

For example, if inference 1 begins, then the driver stops work to perform inference 2, then it resumes and completes inference 1, the execution time for inference 1 will include the time when work was stopped to perform inference 2.

This timing information may be useful for a production deployment of an application to collect telemetry for offline use. You can use the timing data to modify the app for higher performance.

When using this functionality, bear in mind the following:

  • Collecting timing information might have a performance cost.
  • Only a driver is capable of computing the time spent in itself or on the accelerator, excluding time spent in NNAPI runtime and in IPC.
  • You can use these APIs only with an ANeuralNetworksExecution that was created with ANeuralNetworksCompilation_createForDevices with numDevices = 1 .
  • No driver is required to be able to report timing information.

Profile your application with Android Systrace

Starting with Android 10, NNAPI automatically generates systrace events that you can use to profile your application.

The NNAPI Source comes with a parse_systrace utility to process the systrace events generated by your application and generate a table view showing the time spent in the different phases of the model lifecycle (Instantiation, Preparation, Compilation Execution and Termination) and different layers of the applications . The layers in which your application is split are:

  • Application : the main application code
  • Runtime : NNAPI Runtime
  • IPC : The inter process communication between NNAPI Runtime and the Driver code
  • Driver : the accelerator driver process.

Generate the profiling analysys data

Assuming you checked out the AOSP source tree at $ANDROID_BUILD_TOP, and using the TFLite image classification example as target application, you can generate the NNAPI profiling data with the following steps:

  1. Start the Android systrace with the following command:
$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

The -o trace.html parameter indicates that the traces will be written in the trace.html . When profiling own application you will need to replace org.tensorflow.lite.examples.classification with the process name specified in your app manifest.

This will keep one of your shell console busy, don't run the command in background since it is interactively waiting for an enter to terminate.

  1. After the systrace collector is started, start your app and run your benchmark test.

In our case you can start the Image Classification app from Android Studio or directly from your test phone UI if the app has already been installed. To generate some NNAPI data you need to configure the app to use NNAPI by selecting NNAPI as target device in the app configuration dialog.

  1. When the test completes, terminate the systrace by pressing enter on the console terminal active since step 1.

  2. Run the systrace_parser utility generate cumulative statistics:

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

The parser accepts the following parameters: - --total-times : shows the total time spent in a layer including the time spent waiting for execution on a call to an underlying layer - --print-detail : prints all the events that have been collected from systrace - --per-execution : prints only the execution and its subphases (as per-execution times) instead of stats for all phases - --json : produces the output in JSON format

An example of the output is shown below:

===========================================================================================================================================
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

The parser might fail if the collected events do not represent a complete application trace. In particular it might fail if systrace events generated to mark the end of a section are present in the trace without an associated section start event. This usually happens if some events from a previous profiling session are being generated when you start the systrace collector. In this case you would have to run your profiling again.

Add statistics for your application code to systrace_parser output

The parse_systrace application is based on the built-in Android systrace functionality. You can add traces for specific operations in your app using the systrace API ( for Java , for native applications ) with custom event names.

To associate your custom events with phases of the Application lifecycle, prepend your event name with one of the following strings:

  • [NN_LA_PI] : Application level event for Initialization
  • [NN_LA_PP] : Application level event for Preparation
  • [NN_LA_PC] : Application level event for Compilation
  • [NN_LA_PE] : Application level event for Execution

Here is an example of how you can alter the TFLite image classification example code by adding a runInferenceModel section for the Execution phase and the Application layer containing another other sections preprocessBitmap that won't be considered in NNAPI traces. The runInferenceModel section will be part of the systrace events processed by the nnapi systrace parser:

Котлин

/** 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
}

Ява

/** 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;
}

Quality of service

In Android 11 and higher, NNAPI enables better quality of service (QoS) by allowing an application to indicate the relative priorities of its models, the maximum amount of time expected to prepare a given model, and the maximum amount of time expected to complete a given computation. Android 11 also introduces additional NNAPI result codes that enable applications to understand failures such as missed execution deadlines.

Set the priority of a workload

To set the priority of an NNAPI workload, call ANeuralNetworksCompilation_setPriority() prior to calling ANeuralNetworksCompilation_finish() .

Set deadlines

Applications can set deadlines for both model compilation and inference.

More about operands

The following section covers advanced topics about using operands.

Quantized tensors

A quantized tensor is a compact way to represent an n-dimensional array of floating point values.

NNAPI supports 8-bit asymmetric quantized tensors. For these tensors, the value of each cell is represented by an 8-bit integer. Associated with the tensor is a scale and a zero point value. These are used to convert the 8-bit integers into the floating point values that are being represented.

The formula is:

(cellValue - zeroPoint) * scale

where the zeroPoint value is a 32-bit integer and the scale a 32-bit floating point value.

Compared to tensors of 32-bit floating point values, 8-bit quantized tensors have two advantages:

  • Your application is smaller, as the trained weights take a quarter of the size of 32-bit tensors.
  • Computations can often be executed faster. This is due to the smaller amount of data that needs to be fetched from memory and the efficiency of processors such as DSPs in doing integer math.

While it is possible to convert a floating point model to a quantized one, our experience has shown that better results are achieved by training a quantized model directly. In effect, the neural network learns to compensate for the increased granularity of each value. For each quantized tensor, the scale and zeroPoint values are determined during the training process.

In NNAPI, you define quantized tensor types by setting the type field of the ANeuralNetworksOperandType data structure to ANEURALNETWORKS_TENSOR_QUANT8_ASYMM . You also specify the scale and zeroPoint value of the tensor in that data structure.

In addition to 8-bit asymmetric quantized tensors, NNAPI supports the following:

Optional operands

A few operations, like ANEURALNETWORKS_LSH_PROJECTION , take optional operands. To indicate in the model that the optional operand is omitted, call the ANeuralNetworksModel_setOperandValue() function, passing NULL for the buffer and 0 for the length.

If the decision on whether the operand is present or not varies for each execution, you indicate that the operand is omitted by using the ANeuralNetworksExecution_setInput() or ANeuralNetworksExecution_setOutput() functions, passing NULL for the buffer and 0 for the length.

Tensors of unknown rank

Android 9 (API level 28) introduced model operands of unknown dimensions but known rank (the number of dimensions). Android 10 (API level 29) introduced tensors of unknown rank, as shown in ANeuralNetworksOperandType .

NNAPI benchmark

The NNAPI benchmark is available on AOSP in platform/test/mlts/benchmark (benchmark app) and platform/test/mlts/models (models and datasets).

The benchmark evaluates latency and accuracy and compares drivers to the same work done using Tensorflow Lite running on the CPU, for the same models and datasets.

To use the benchmark, do the following:

  1. Connect a target Android device to your computer, open a terminal window, and make sure the device is reachable through adb.

  2. If more than one Android device is connected, export the target device ANDROID_SERIAL environment variable.

  3. Navigate to the Android top-level source directory.

  4. Run the following commands:

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

    At the end of a benchmark run, its results will be presented as an HTML page passed to xdg-open .

NNAPI logs

NNAPI generates useful diagnostic information in the system logs. To analyze the logs, use the logcat utility.

Enable verbose NNAPI logging for specific phases or components by setting the property debug.nn.vlog (using adb shell ) to the following list of values, separated by space, colon, or comma:

  • model : Model building
  • compilation : Generation of the model execution plan and compilation
  • execution : Model execution
  • cpuexe : Execution of operations using the NNAPI CPU implementation
  • manager : NNAPI extensions, available interfaces and capabilities related info
  • all or 1 : All the elements above

For example, to enable full verbose logging use the command adb shell setprop debug.nn.vlog all . To disable verbose logging, use the command adb shell setprop debug.nn.vlog '""' .

Once enabled, verbose logging generates log entries at INFO level with a tag set to the phase or component name.

Beside the debug.nn.vlog controlled messages, NNAPI API components provide other log entries at various levels, each one using a specific log tag.

To get a list of components, search the source tree using the following expression:

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

This expression currently returns the following tags:

  • BurstBuilder
  • Обратные вызовы
  • CompilationBuilder
  • CpuExecutor
  • ExecutionBuilder
  • ExecutionBurstController
  • ExecutionBurstServer
  • ExecutionPlan
  • FibonacciDriver
  • GraphDump
  • IndexedShapeWrapper
  • IonWatcher
  • Менеджер
  • Память
  • MemoryUtils
  • MetaModel
  • ModelArgumentInfo
  • ModelBuilder
  • NeuralNetworks
  • OperationResolver
  • Операции
  • OperationsUtils
  • PackageInfo
  • TokenHasher
  • TypeManager
  • Утилиты
  • ValidateHal
  • VersionedInterfaces

To control the level of log messages shown by logcat , use the environment variable ANDROID_LOG_TAGS .

To show the full set of NNAPI log messages and disable any others, set ANDROID_LOG_TAGS to the following:

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.

You can set ANDROID_LOG_TAGS using the following command:

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

Note that this is just a filter that applies to logcat . You still need to set the property debug.nn.vlog to all to generate verbose log info.

,

The Android Neural Networks API (NNAPI) is an Android C API designed for running computationally intensive operations for machine learning on Android devices. NNAPI is designed to provide a base layer of functionality for higher-level machine learning frameworks, such as TensorFlow Lite and Caffe2, that build and train neural networks. The API is available on all Android devices running Android 8.1 (API level 27) or higher.

NNAPI supports inferencing by applying data from Android devices to previously trained, developer-defined models. Examples of inferencing include classifying images, predicting user behavior, and selecting appropriate responses to a search query.

On-device inferencing has many benefits:

  • Latency : You don't need to send a request over a network connection and wait for a response. For example, this can be critical for video applications that process successive frames coming from a camera.
  • Availability : The application runs even when outside of network coverage.
  • Speed : New hardware that is specific to neural network processing provides significantly faster computation than a general-purpose CPU, alone.
  • Privacy : The data does not leave the Android device.
  • Cost : No server farm is needed when all the computations are performed on the Android device.

There are also trade-offs that a developer should keep in mind:

  • System utilization : Evaluating neural networks involves a lot of computation, which could increase battery power usage. You should consider monitoring the battery health if this is a concern for your app, especially for long-running computations.
  • Application size : Pay attention to the size of your models. Models may take up multiple megabytes of space. If bundling large models in your APK would unduly impact your users, you may want to consider downloading the models after app installation, using smaller models, or running your computations in the cloud. NNAPI does not provide functionality for running models in the cloud.

See the Android Neural Networks API sample to see one example of how to use NNAPI.

Understand the Neural Networks API runtime

NNAPI is meant to be called by machine learning libraries, frameworks, and tools that let developers train their models off-device and deploy them on Android devices. Apps typically would not use NNAPI directly, but would instead use higher-level machine learning frameworks. These frameworks in turn could use NNAPI to perform hardware-accelerated inference operations on supported devices.

Based on an app's requirements and the hardware capabilities on an Android device, Android's neural network runtime can efficiently distribute the computation workload across available on-device processors, including dedicated neural network hardware, graphics processing units (GPUs), and digital signal processors (DSPs ).

For Android devices that lack a specialized vendor driver, the NNAPI runtime executes the requests on the CPU.

Figure 1 shows the high-level system architecture for NNAPI.

Figure 1. System architecture for Android Neural Networks API

Neural Networks API programming model

To perform computations using NNAPI, you first need to construct a directed graph that defines the computations to perform. This computation graph, combined with your input data (for example, the weights and biases passed down from a machine learning framework), forms the model for NNAPI runtime evaluation.

NNAPI uses four main abstractions:

  • Model : A computation graph of mathematical operations and the constant values learned through a training process. These operations are specific to neural networks. They include 2-dimensional (2D) convolution , logistic ( sigmoid ) activation, rectified linear (ReLU) activation, and more. Creating a model is a synchronous operation. Once successfully created, it can be reused across threads and compilations. In NNAPI, a model is represented as an ANeuralNetworksModel instance.
  • Compilation : Represents a configuration for compiling an NNAPI model into lower-level code. Creating a compilation is a synchronous operation. Once successfully created, it can be reused across threads and executions. In NNAPI, each compilation is represented as an ANeuralNetworksCompilation instance.
  • Memory : Represents shared memory, memory mapped files, and similar memory buffers. Using a memory buffer lets the NNAPI runtime transfer data to drivers more efficiently. An app typically creates one shared memory buffer that contains every tensor needed to define a model. You can also use memory buffers to store the inputs and outputs for an execution instance. In NNAPI, each memory buffer is represented as an ANeuralNetworksMemory instance.
  • Execution : Interface for applying an NNAPI model to a set of inputs and to gather the results. Execution can be performed synchronously or asynchronously.

    For asynchronous execution, multiple threads can wait on the same execution. When this execution completes, all threads are released.

    In NNAPI, each execution is represented as an ANeuralNetworksExecution instance.

Figure 2 shows the basic programming flow.

Figure 2. Programming flow for Android Neural Networks API

The rest of this section describes the steps to set up your NNAPI model to perform computation, compile the model, and execute the compiled model.

Provide access to training data

Your trained weights and biases data are likely stored in a file. To provide the NNAPI runtime with efficient access to this data, create an ANeuralNetworksMemory instance by calling the ANeuralNetworksMemory_createFromFd() function and passing in the file descriptor of the opened data file. You also specify memory protection flags and an offset where the shared memory region starts in the 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);

Although in this example we use only one ANeuralNetworksMemory instance for all our weights, it's possible to use more than one ANeuralNetworksMemory instance for multiple files.

Use native hardware buffers

You can use native hardware buffers for model inputs, outputs, and constant operand values. In certain cases, an NNAPI accelerator can access AHardwareBuffer objects without the driver needing to copy the data. AHardwareBuffer has many different configurations, and not every NNAPI accelerator may support all of these configurations. Because of this limitation, refer to the constraints listed in ANeuralNetworksMemory_createFromAHardwareBuffer reference documentation and test ahead of time on target devices to ensure compilations and executions that use AHardwareBuffer behave as expected, using device assignment to specify the accelerator.

To allow the NNAPI runtime to access an AHardwareBuffer object, create an ANeuralNetworksMemory instance by calling the ANeuralNetworksMemory_createFromAHardwareBuffer function and passing in the AHardwareBuffer object, as shown in the following code sample:

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

When NNAPI no longer needs to access the AHardwareBuffer object, free the corresponding ANeuralNetworksMemory instance:

ANeuralNetworksMemory_free(mem2);

Примечание:

  • You can use AHardwareBuffer only for the whole buffer; you cannot use it with an ARect parameter.
  • The NNAPI runtime will not flush the buffer. You need to make sure that the input and output buffers are accessible before scheduling the execution.
  • There is no support for sync fence file descriptors.
  • For an AHardwareBuffer with vendor-specific formats and usage bits, it is up to the vendor implementation to determine whether the client or the driver is responsible for flushing the cache.

Модель

A model is the fundamental unit of computation in NNAPI. Each model is defined by one or more operands and operations.

Operands

Operands are data objects used in defining the graph. These include the inputs and outputs of the model, the intermediate nodes that contain the data that flows from one operation to another, and the constants that are passed to these operations.

There are two types of operands that can be added to NNAPI models: scalars and tensors .

A scalar represents a single value. NNAPI supports scalar values in boolean, 16-bit floating point, 32-bit floating point, 32-bit integer, and unsigned 32-bit integer formats.

Most operations in NNAPI involve tensors. Tensors are n-dimensional arrays. NNAPI supports tensors with 16-bit floating point, 32-bit floating point, 8-bit quantized , 16-bit quantized, 32-bit integer, and 8-bit boolean values.

For example, figure 3 represents a model with two operations: an addition followed by a multiplication. The model takes an input tensor and produces one output tensor.

Figure 3. Example of operands for an NNAPI model

The model above has seven operands. These operands are identified implicitly by the index of the order in which they are added to the model. The first operand added has an index of 0, the second an index of 1, and so on. Operands 1, 2, 3, and 5 are constant operands.

The order in which you add the operands does not matter. For example, the model output operand could be the first one added. The important part is to use the correct index value when referring to an operand.

Operands have types. These are specified when they are added to the model.

An operand cannot be used as both input and output of a model.

Every operand must either be a model input, a constant, or the output operand of exactly one operation.

For additional information on using operands, see More about operands .

Операции

An operation specifies the computations to be performed. Each operation consists of these elements:

  • an operation type (for example, addition, multiplication, convolution),
  • a list of indexes of the operands that the operation uses for input, and
  • a list of indexes of the operands that the operation uses for output.

The order in these lists matters; see the NNAPI API reference for the expected inputs and outputs of each operation type.

You must add the operands that an operation consumes or produces to the model before adding the operation.

The order in which you add operations does not matter. NNAPI relies on the dependencies established by the computation graph of operands and operations to determine the order in which operations are executed.

The operations that NNAPI supports are summarized in the table below:

Категория Операции
Element-wise mathematical operations
Tensor manipulation
Image operations
Lookup operations
Normalization operations
Convolution operations
Pooling operations
Activation operations
Прочие операции

Known issue in API level 28: When passing ANEURALNETWORKS_TENSOR_QUANT8_ASYMM tensors to the ANEURALNETWORKS_PAD operation, which is available on Android 9 (API level 28) and higher, the output from NNAPI may not match output from higher-level machine learning frameworks, such as TensorFlow Lite . You should instead pass only ANEURALNETWORKS_TENSOR_FLOAT32 . The issue is resolved in Android 10 (API level 29) and higher.

Build models

In the following example, we create the two-operation model found in figure 3 .

To build the model, follow these steps:

  1. Call the ANeuralNetworksModel_create() function to define an empty model.

    ANeuralNetworksModel* model = NULL;
    ANeuralNetworksModel_create(&model);
    
  2. Add the operands to your model by calling ANeuralNetworks_addOperand() . Their data types are defined using the ANeuralNetworksOperandType data structure.

    // 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. For operands that have constant values, such as weights and biases that your app obtains from a training process, use the ANeuralNetworksModel_setOperandValue() and ANeuralNetworksModel_setOperandValueFromMemory() functions.

    In the following example, we set constant values from the training data file corresponding to the memory buffer we created in Provide access to training data .

    // 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. For each operation in the directed graph you want to compute, add the operation to your model by calling the ANeuralNetworksModel_addOperation() function.

    As parameters to this call, your app must provide:

    • the operation type
    • the count of input values
    • the array of the indexes for input operands
    • the count of output values
    • the array of the indexes for output operands

    Note that an operand cannot be used for both input and output of the same operation.

    // 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. Identify which operands the model should treat as its inputs and outputs by calling the ANeuralNetworksModel_identifyInputsAndOutputs() function.

    // 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. Optionally, specify whether ANEURALNETWORKS_TENSOR_FLOAT32 is allowed to be calculated with range or precision as low as that of the IEEE 754 16-bit floating-point format by calling ANeuralNetworksModel_relaxComputationFloat32toFloat16() .

  7. Call ANeuralNetworksModel_finish() to finalize the definition of your model. If there are no errors, this function returns a result code of ANEURALNETWORKS_NO_ERROR .

    ANeuralNetworksModel_finish(model);
    

Once you create a model, you can compile it any number of times and execute each compilation any number of times.

Поток управления

To incorporate control flow in an NNAPI model, do the following:

  1. Construct the corresponding execution subgraphs ( then and else subgraphs for an IF statement, condition and body subgraphs for a WHILE loop) as standalone ANeuralNetworksModel* models:

    ANeuralNetworksModel* thenModel = makeThenModel();
    ANeuralNetworksModel* elseModel = makeElseModel();
    
  2. Create operands that reference those models within the model containing the control flow:

    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. Add the control flow operation:

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

Сборник

The compilation step determines on which processors your model will be executed and asks the corresponding drivers to prepare for its execution. This could include the generation of machine code specific to the processors your model will run on.

To compile a model, follow these steps:

  1. Call the ANeuralNetworksCompilation_create() function to create a new compilation instance.

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

    Optionally, you can use device assignment to explicitly choose what devices to execute on.

  2. You can optionally influence how the runtime trades off between battery power usage and execution speed. You can do so by calling ANeuralNetworksCompilation_setPreference() .

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

    The preferences you can specify include:

  3. You can optionally set up compilation caching by calling ANeuralNetworksCompilation_setCaching .

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

    Use getCodeCacheDir() for the cacheDir . The token specified must be unique to each model within the application.

  4. Finalize the compilation definition by calling ANeuralNetworksCompilation_finish() . If there are no errors, this function returns a result code of ANEURALNETWORKS_NO_ERROR .

    ANeuralNetworksCompilation_finish(compilation);
    

Device discovery and assignment

On Android devices running Android 10 (API level 29) and higher, NNAPI provides functions that allow machine learning framework libraries and apps to get information about the devices available and specify devices to be used for execution. Providing information about the available devices allows apps to get the exact version of the drivers found on a device to avoid known incompatibilities. By giving apps the ability to specify which devices are to execute different sections of a model, apps can be optimized for the Android device on which they are deployed.

Device discovery

Use ANeuralNetworks_getDeviceCount to get the number of available devices. For each device, use ANeuralNetworks_getDevice to set an ANeuralNetworksDevice instance to a reference to that device.

Once you have a device reference, you can find out additional information about that device using the following functions:

Device assignment

Use ANeuralNetworksModel_getSupportedOperationsForDevices to discover which operations of a model can be run on specific devices.

To control which accelerators to use for execution, call ANeuralNetworksCompilation_createForDevices in place of ANeuralNetworksCompilation_create . Use the resulting ANeuralNetworksCompilation object, as normal. The function returns an error if the provided model contains operations that are not supported by the selected devices.

If multiple devices are specified, the runtime is responsible for distributing the work across the devices.

Similar to other devices, the NNAPI CPU implementation is represented by an ANeuralNetworksDevice with the name nnapi-reference and the type ANEURALNETWORKS_DEVICE_TYPE_CPU . When calling ANeuralNetworksCompilation_createForDevices , the CPU implementation is not used to handle the failure cases for model compilation and execution.

It is an application's responsibility to partition a model into sub-models that can run on the specified devices. Applications that don't need to do manual partitioning should continue to call the simpler ANeuralNetworksCompilation_create to use all available devices (including the CPU) to accelerate the model. If the model couldn't be fully supported by the devices you specified using ANeuralNetworksCompilation_createForDevices , ANEURALNETWORKS_BAD_DATA is returned.

Model partitioning

When multiple devices are available for the model, the NNAPI runtime distributes the work across the devices. For example, if more than one device was provided to ANeuralNetworksCompilation_createForDevices , all the specified ones will be considered when allocating the work. Note that, if the CPU device is not in the list, CPU execution will be disabled. When using ANeuralNetworksCompilation_create all available devices will be taken into account, including CPU.

The distribution is done by selecting from the list of available devices, for each of the operations in the model, the device supporting the operation and declaring the best performance, ie the fastest execution time or the lowest power consumption, depending on the execution preference specified клиентом. This partitioning algorithm doesn't account for possible inefficiencies caused by the IO between the different processors so, when specifying multiple processors (either explicitly when using ANeuralNetworksCompilation_createForDevices or implicitly by using ANeuralNetworksCompilation_create ) it's important to profile the resulting application.

To understand how your model has been partitioned by NNAPI, check the Android logs for a message (at INFO level with tag ExecutionPlan ):

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

op-name is the descriptive name of the operation in the graph and device-index is the index of the candidate device in the list of devices. This list is the input provided to ANeuralNetworksCompilation_createForDevices or, if using ANeuralNetworksCompilation_createForDevices , the list of devices returned when iterating over all devices using ANeuralNetworks_getDeviceCount and ANeuralNetworks_getDevice .

The message (at INFO level with tag ExecutionPlan ):

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

This message indicates that the whole graph has been accelerated on the device device-name .

Исполнение

The execution step applies the model to a set of inputs and stores the computation outputs to one or more user buffers or memory spaces that your app allocated.

To execute a compiled model, follow these steps:

  1. Call the ANeuralNetworksExecution_create() function to create a new execution instance.

    // Run the compiled model against a set of inputs
    ANeuralNetworksExecution* run1 = NULL;
    ANeuralNetworksExecution_create(compilation, &run1);
    
  2. Specify where your app reads the input values for the computation. Your app can read input values from either a user buffer or an allocated memory space by calling ANeuralNetworksExecution_setInput() or ANeuralNetworksExecution_setInputFromMemory() respectively.

    // 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. Specify where your app writes the output values. Your app can write output values to either a user buffer or an allocated memory space, by calling ANeuralNetworksExecution_setOutput() or ANeuralNetworksExecution_setOutputFromMemory() respectively.

    // Set the output
    float32 myOutput[3][4];
    ANeuralNetworksExecution_setOutput(run1, 0, NULL, myOutput, sizeof(myOutput));
    
  4. Schedule the execution to start, by calling the ANeuralNetworksExecution_startCompute() function. If there are no errors, this function returns a result code of ANEURALNETWORKS_NO_ERROR .

    // Starts the work. The work proceeds asynchronously
    ANeuralNetworksEvent* run1_end = NULL;
    ANeuralNetworksExecution_startCompute(run1, &run1_end);
    
  5. Call the ANeuralNetworksEvent_wait() function to wait for the execution to complete. If the execution was successful, this function returns a result code of ANEURALNETWORKS_NO_ERROR . Waiting can be done on a different thread than the one starting the execution.

    // 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. Optionally, you can apply a different set of inputs to the compiled model by using the same compilation instance to create a new ANeuralNetworksExecution instance.

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

Synchronous execution

Asynchronous execution spends time to spawn and synchronize threads. Furthermore, the latency can be hugely variable, with the longest delays reaching up to 500 microseconds between the time a thread is notified or woken and the time it is eventually bound to a CPU core.

To improve latency, you can instead direct an application to make a synchronous inference call to the runtime. That call will return only once an inference has been completed rather than returning once an inference has been started. Instead of calling ANeuralNetworksExecution_startCompute for an asynchronous inference call to the runtime, the application calls ANeuralNetworksExecution_compute to make a synchronous call to the runtime. A call to ANeuralNetworksExecution_compute does not take an ANeuralNetworksEvent and is not paired with a call to ANeuralNetworksEvent_wait .

Burst executions

On Android devices running Android 10 (API level 29) and higher, the NNAPI supports burst executions through the ANeuralNetworksBurst object. Burst executions are a sequence of executions of the same compilation that occur in rapid succession, such as those operating on frames of a camera capture or successive audio samples. Using ANeuralNetworksBurst objects may result in faster executions, as they indicate to accelerators that resources may be reused between executions and that accelerators should remain in a high-performance state for the duration of the burst.

ANeuralNetworksBurst introduces only a small change in the normal execution path. You create a burst object using ANeuralNetworksBurst_create , as shown in the following code snippet:

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

Burst executions are synchronous. However, instead of using ANeuralNetworksExecution_compute to perform each inference, you pair the various ANeuralNetworksExecution objects with the same ANeuralNetworksBurst in calls to the function 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
// ...

Free the ANeuralNetworksBurst object with ANeuralNetworksBurst_free when it is no longer needed.

// Cleanup
ANeuralNetworksBurst_free(burst);

Asynchronous command queues and fenced execution

In Android 11 and higher, NNAPI supports an additional way to schedule asynchronous execution through the ANeuralNetworksExecution_startComputeWithDependencies() method. When you use this method, the execution waits for all of the depending events to be signaled before starting the evaluation. Once the execution has completed and the outputs are ready to be consumed, the returned event is signaled.

Depending on which devices handle the execution, the event might be backed by a sync fence . You must call ANeuralNetworksEvent_wait() to wait for the event and recuperate the resources that the execution used. You can import sync fences to an event object using ANeuralNetworksEvent_createFromSyncFenceFd() , and you can export sync fences from an event object using ANeuralNetworksEvent_getSyncFenceFd() .

Dynamically sized outputs

To support models where the size of the output depends on the input data—that is, where the size cannot be determined at model execution time—use ANeuralNetworksExecution_getOutputOperandRank and ANeuralNetworksExecution_getOutputOperandDimensions .

The following code sample shows how to do this:

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

Cleanup

The cleanup step handles the freeing of internal resources used for your computation.

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

Error management and CPU fallback

If there is an error during partitioning, if a driver fails to compile a (piece of a) model, or if a driver fails to execute a compiled (piece of a) model, NNAPI might fall back to its own CPU implementation of the one or more operations.

If the NNAPI client contains optimized versions of the operation (as, for example, TFLite) it might be advantageous to disable the CPU fallback and handle the failures with the client's optimized operation implementation.

In Android 10, if compilation is performed using ANeuralNetworksCompilation_createForDevices , then CPU fallback will be disabled.

In Android P, NNAPI execution falls back to the CPU if execution on the driver fails. This is also true on Android 10 when ANeuralNetworksCompilation_create rather than ANeuralNetworksCompilation_createForDevices is used.

First execution falls back for that single partition, and if that still fails, it retries the entire model on the CPU.

If partitioning or compilation fails, the entire model will be tried on CPU.

There are cases where some operations are not supported on CPU, and in such situations compilation or execution will fail rather than falling back.

Even after disabling CPU fallback, there may still be operations in the model that are scheduled on the CPU. If the CPU is in the list of processors supplied to ANeuralNetworksCompilation_createForDevices , and is either the only processor that supports those operations or is the processor that claims best performance for those operations, it will be chosen as a primary (non-fallback) executor.

To ensure there is no CPU execution, use ANeuralNetworksCompilation_createForDevices while excluding the nnapi-reference from the list of devices. Starting in Android P, it is possible to disable fallback at execution time on DEBUG builds by setting the debug.nn.partition property to 2.

Домены памяти

In Android 11 and higher, NNAPI supports memory domains that provide allocator interfaces for opaque memories. This allows applications to pass device-native memories across executions, so that NNAPI does not copy or transform data unnecessarily when performing consecutive executions on the same driver.

The memory domain feature is intended for tensors that are mostly internal to the driver and that don't need frequent access to the client side. Примеры таких тензоров включают тензоры состояния в моделях последовательностей. For tensors that need frequent CPU access on the client side, use shared memory pools instead.

To allocate an opaque memory, perform the following steps:

  1. Call the ANeuralNetworksMemoryDesc_create() function to create a new memory descriptor:

    // Create a memory descriptor
    ANeuralNetworksMemoryDesc* desc;
    ANeuralNetworksMemoryDesc_create(&desc);
    
  2. Specify all of the intended input and output roles by calling ANeuralNetworksMemoryDesc_addInputRole() and 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. Optionally, specify the memory dimensions by calling ANeuralNetworksMemoryDesc_setDimensions() .

    // Specify the memory dimensions
    uint32_t dims[] = {3, 4};
    ANeuralNetworksMemoryDesc_setDimensions(desc, 2, dims);
    
  4. Finalize the descriptor definition by calling ANeuralNetworksMemoryDesc_finish() .

    ANeuralNetworksMemoryDesc_finish(desc);
    
  5. Allocate as many memories as you need by passing the descriptor to ANeuralNetworksMemory_createFromDesc() .

    // Allocate two opaque memories with the descriptor
    ANeuralNetworksMemory* opaqueMem;
    ANeuralNetworksMemory_createFromDesc(desc, &opaqueMem);
    
  6. Free the memory descriptor when you no longer need it.

    ANeuralNetworksMemoryDesc_free(desc);
    

The client may only use the created ANeuralNetworksMemory object with ANeuralNetworksExecution_setInputFromMemory() or ANeuralNetworksExecution_setOutputFromMemory() according to the roles specified in the ANeuralNetworksMemoryDesc object. The offset and length arguments must be set to 0, indicating that the whole memory is used. The client may also explicitly set or extract the contents of the memory by using ANeuralNetworksMemory_copy() .

You can create opaque memories with roles of unspecified dimensions or rank. In that case, the memory creation might fail with the ANEURALNETWORKS_OP_FAILED status if it is not supported by the underlying driver. The client is encouraged to implement fallback logic by allocating a large enough buffer backed by Ashmem or BLOB-mode AHardwareBuffer .

When NNAPI no longer needs to access the opaque memory object, free the corresponding ANeuralNetworksMemory instance:

ANeuralNetworksMemory_free(opaqueMem);

Measure performance

You can evaluate your app's performance by measuring execution time or by profiling.

Execution time

When you want to determine total execution time through the runtime, you can use the synchronous execution API and measure the time taken by the call. When you want to determine total execution time through a lower level of the software stack, you can use ANeuralNetworksExecution_setMeasureTiming and ANeuralNetworksExecution_getDuration to get:

  • execution time on an accelerator (not in the driver, which runs on the host processor).
  • execution time in the driver, including time on the accelerator.

The execution time in the driver excludes overhead such as that of the runtime itself and the IPC needed for the runtime to communicate with the driver.

These APIs measure duration between the work submitted and work completed events, rather than the time a driver or accelerator devotes to performing the inference, possibly interrupted by context switching.

For example, if inference 1 begins, then the driver stops work to perform inference 2, then it resumes and completes inference 1, the execution time for inference 1 will include the time when work was stopped to perform inference 2.

This timing information may be useful for a production deployment of an application to collect telemetry for offline use. You can use the timing data to modify the app for higher performance.

When using this functionality, bear in mind the following:

  • Collecting timing information might have a performance cost.
  • Only a driver is capable of computing the time spent in itself or on the accelerator, excluding time spent in NNAPI runtime and in IPC.
  • You can use these APIs only with an ANeuralNetworksExecution that was created with ANeuralNetworksCompilation_createForDevices with numDevices = 1 .
  • No driver is required to be able to report timing information.

Profile your application with Android Systrace

Starting with Android 10, NNAPI automatically generates systrace events that you can use to profile your application.

The NNAPI Source comes with a parse_systrace utility to process the systrace events generated by your application and generate a table view showing the time spent in the different phases of the model lifecycle (Instantiation, Preparation, Compilation Execution and Termination) and different layers of the applications . The layers in which your application is split are:

  • Application : the main application code
  • Runtime : NNAPI Runtime
  • IPC : The inter process communication between NNAPI Runtime and the Driver code
  • Driver : the accelerator driver process.

Generate the profiling analysys data

Assuming you checked out the AOSP source tree at $ANDROID_BUILD_TOP, and using the TFLite image classification example as target application, you can generate the NNAPI profiling data with the following steps:

  1. Start the Android systrace with the following command:
$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

The -o trace.html parameter indicates that the traces will be written in the trace.html . When profiling own application you will need to replace org.tensorflow.lite.examples.classification with the process name specified in your app manifest.

This will keep one of your shell console busy, don't run the command in background since it is interactively waiting for an enter to terminate.

  1. After the systrace collector is started, start your app and run your benchmark test.

In our case you can start the Image Classification app from Android Studio or directly from your test phone UI if the app has already been installed. To generate some NNAPI data you need to configure the app to use NNAPI by selecting NNAPI as target device in the app configuration dialog.

  1. When the test completes, terminate the systrace by pressing enter on the console terminal active since step 1.

  2. Run the systrace_parser utility generate cumulative statistics:

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

The parser accepts the following parameters: - --total-times : shows the total time spent in a layer including the time spent waiting for execution on a call to an underlying layer - --print-detail : prints all the events that have been collected from systrace - --per-execution : prints only the execution and its subphases (as per-execution times) instead of stats for all phases - --json : produces the output in JSON format

An example of the output is shown below:

===========================================================================================================================================
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

The parser might fail if the collected events do not represent a complete application trace. In particular it might fail if systrace events generated to mark the end of a section are present in the trace without an associated section start event. This usually happens if some events from a previous profiling session are being generated when you start the systrace collector. In this case you would have to run your profiling again.

Add statistics for your application code to systrace_parser output

The parse_systrace application is based on the built-in Android systrace functionality. You can add traces for specific operations in your app using the systrace API ( for Java , for native applications ) with custom event names.

To associate your custom events with phases of the Application lifecycle, prepend your event name with one of the following strings:

  • [NN_LA_PI] : Application level event for Initialization
  • [NN_LA_PP] : Application level event for Preparation
  • [NN_LA_PC] : Application level event for Compilation
  • [NN_LA_PE] : Application level event for Execution

Here is an example of how you can alter the TFLite image classification example code by adding a runInferenceModel section for the Execution phase and the Application layer containing another other sections preprocessBitmap that won't be considered in NNAPI traces. The runInferenceModel section will be part of the systrace events processed by the nnapi systrace parser:

Котлин

/** 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
}

Ява

/** 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;
}

Quality of service

In Android 11 and higher, NNAPI enables better quality of service (QoS) by allowing an application to indicate the relative priorities of its models, the maximum amount of time expected to prepare a given model, and the maximum amount of time expected to complete a given computation. Android 11 also introduces additional NNAPI result codes that enable applications to understand failures such as missed execution deadlines.

Set the priority of a workload

To set the priority of an NNAPI workload, call ANeuralNetworksCompilation_setPriority() prior to calling ANeuralNetworksCompilation_finish() .

Set deadlines

Applications can set deadlines for both model compilation and inference.

More about operands

The following section covers advanced topics about using operands.

Quantized tensors

A quantized tensor is a compact way to represent an n-dimensional array of floating point values.

NNAPI supports 8-bit asymmetric quantized tensors. For these tensors, the value of each cell is represented by an 8-bit integer. Associated with the tensor is a scale and a zero point value. These are used to convert the 8-bit integers into the floating point values that are being represented.

The formula is:

(cellValue - zeroPoint) * scale

where the zeroPoint value is a 32-bit integer and the scale a 32-bit floating point value.

Compared to tensors of 32-bit floating point values, 8-bit quantized tensors have two advantages:

  • Your application is smaller, as the trained weights take a quarter of the size of 32-bit tensors.
  • Computations can often be executed faster. This is due to the smaller amount of data that needs to be fetched from memory and the efficiency of processors such as DSPs in doing integer math.

While it is possible to convert a floating point model to a quantized one, our experience has shown that better results are achieved by training a quantized model directly. In effect, the neural network learns to compensate for the increased granularity of each value. For each quantized tensor, the scale and zeroPoint values are determined during the training process.

In NNAPI, you define quantized tensor types by setting the type field of the ANeuralNetworksOperandType data structure to ANEURALNETWORKS_TENSOR_QUANT8_ASYMM . You also specify the scale and zeroPoint value of the tensor in that data structure.

In addition to 8-bit asymmetric quantized tensors, NNAPI supports the following:

Optional operands

A few operations, like ANEURALNETWORKS_LSH_PROJECTION , take optional operands. To indicate in the model that the optional operand is omitted, call the ANeuralNetworksModel_setOperandValue() function, passing NULL for the buffer and 0 for the length.

If the decision on whether the operand is present or not varies for each execution, you indicate that the operand is omitted by using the ANeuralNetworksExecution_setInput() or ANeuralNetworksExecution_setOutput() functions, passing NULL for the buffer and 0 for the length.

Tensors of unknown rank

Android 9 (API level 28) introduced model operands of unknown dimensions but known rank (the number of dimensions). Android 10 (API level 29) introduced tensors of unknown rank, as shown in ANeuralNetworksOperandType .

NNAPI benchmark

The NNAPI benchmark is available on AOSP in platform/test/mlts/benchmark (benchmark app) and platform/test/mlts/models (models and datasets).

The benchmark evaluates latency and accuracy and compares drivers to the same work done using Tensorflow Lite running on the CPU, for the same models and datasets.

To use the benchmark, do the following:

  1. Connect a target Android device to your computer, open a terminal window, and make sure the device is reachable through adb.

  2. If more than one Android device is connected, export the target device ANDROID_SERIAL environment variable.

  3. Navigate to the Android top-level source directory.

  4. Run the following commands:

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

    At the end of a benchmark run, its results will be presented as an HTML page passed to xdg-open .

NNAPI logs

NNAPI generates useful diagnostic information in the system logs. To analyze the logs, use the logcat utility.

Enable verbose NNAPI logging for specific phases or components by setting the property debug.nn.vlog (using adb shell ) to the following list of values, separated by space, colon, or comma:

  • model : Model building
  • compilation : Generation of the model execution plan and compilation
  • execution : Model execution
  • cpuexe : Execution of operations using the NNAPI CPU implementation
  • manager : NNAPI extensions, available interfaces and capabilities related info
  • all or 1 : All the elements above

For example, to enable full verbose logging use the command adb shell setprop debug.nn.vlog all . To disable verbose logging, use the command adb shell setprop debug.nn.vlog '""' .

Once enabled, verbose logging generates log entries at INFO level with a tag set to the phase or component name.

Beside the debug.nn.vlog controlled messages, NNAPI API components provide other log entries at various levels, each one using a specific log tag.

To get a list of components, search the source tree using the following expression:

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

This expression currently returns the following tags:

  • BurstBuilder
  • Обратные вызовы
  • CompilationBuilder
  • CpuExecutor
  • ExecutionBuilder
  • ExecutionBurstController
  • ExecutionBurstServer
  • ExecutionPlan
  • FibonacciDriver
  • GraphDump
  • IndexedShapeWrapper
  • IonWatcher
  • Менеджер
  • Память
  • MemoryUtils
  • MetaModel
  • ModelArgumentInfo
  • ModelBuilder
  • NeuralNetworks
  • OperationResolver
  • Операции
  • OperationsUtils
  • PackageInfo
  • TokenHasher
  • TypeManager
  • Утилиты
  • ValidateHal
  • VersionedInterfaces

To control the level of log messages shown by logcat , use the environment variable ANDROID_LOG_TAGS .

To show the full set of NNAPI log messages and disable any others, set ANDROID_LOG_TAGS to the following:

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.

You can set ANDROID_LOG_TAGS using the following command:

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

Note that this is just a filter that applies to logcat . You still need to set the property debug.nn.vlog to all to generate verbose log info.