1 //
2 // Copyright © 2017-2023 Arm Ltd and Contributors. All rights reserved.
3 // SPDX-License-Identifier: MIT
4 //
5
6 #define LOG_TAG "ArmnnDriver"
7
8 #include "ArmnnPreparedModel.hpp"
9 #include "Utils.hpp"
10
11 #include <armnn/Types.hpp>
12
13 #include <log/log.h>
14 #include <OperationsUtils.h>
15 #include <ValidateHal.h>
16
17 #include <chrono>
18 #include <cinttypes>
19
20 #ifdef ARMNN_ANDROID_S
21 #include <LegacyUtils.h>
22 #endif
23
24 using namespace android;
25
26 namespace
27 {
28 using namespace armnn_driver;
29
NotifyCallbackAndCheck(const::android::sp<V1_0::IExecutionCallback> & callback,V1_0::ErrorStatus errorStatus,std::string callingFunction)30 void NotifyCallbackAndCheck(const ::android::sp<V1_0::IExecutionCallback>& callback, V1_0::ErrorStatus errorStatus,
31 std::string callingFunction)
32 {
33 Return<void> returned = callback->notify(errorStatus);
34 // This check is required, if the callback fails and it isn't checked it will bring down the service
35 if (!returned.isOk())
36 {
37 ALOGE("ArmnnDriver::%s: hidl callback failed to return properly: %s",
38 callingFunction.c_str(), returned.description().c_str());
39 }
40 }
41
ValidateRequestArgument(const V1_0::RequestArgument & requestArg,const armnn::TensorInfo & tensorInfo)42 bool ValidateRequestArgument(const V1_0::RequestArgument& requestArg, const armnn::TensorInfo& tensorInfo)
43 {
44 if (requestArg.dimensions.size() != 0)
45 {
46 if (requestArg.dimensions.size() != tensorInfo.GetNumDimensions())
47 {
48 ALOGE("Mismatched dimensions (request argument: %zu, expected: %u)",
49 requestArg.dimensions.size(), tensorInfo.GetNumDimensions());
50 return false;
51 }
52
53 for (unsigned int d = 0; d < tensorInfo.GetNumDimensions(); ++d)
54 {
55 if (requestArg.dimensions[d] != 0 && requestArg.dimensions[d] != tensorInfo.GetShape()[d])
56 {
57 ALOGE("Mismatched size for dimension %d (request argument: %u, expected %u)",
58 d, requestArg.dimensions[d], tensorInfo.GetShape()[d]);
59 return false;
60 }
61 }
62 }
63
64 return true;
65 }
66
GetTensorForRequestArgument(const V1_0::RequestArgument & requestArg,const armnn::TensorInfo & tensorInfo,const std::vector<::android::nn::RunTimePoolInfo> & requestPools)67 armnn::Tensor GetTensorForRequestArgument(const V1_0::RequestArgument& requestArg,
68 const armnn::TensorInfo& tensorInfo,
69 const std::vector<::android::nn::RunTimePoolInfo>& requestPools)
70 {
71 if (!ValidateRequestArgument(requestArg, tensorInfo))
72 {
73 return armnn::Tensor();
74 }
75
76 return armnn::Tensor(tensorInfo, GetMemoryFromPool(requestArg.location, requestPools));
77 }
78
BuildTensorName(const char * tensorNamePrefix,std::size_t index)79 inline std::string BuildTensorName(const char* tensorNamePrefix, std::size_t index)
80 {
81 return tensorNamePrefix + std::to_string(index);
82 }
83
84 } // anonymous namespace
85
86 using namespace android::hardware;
87
88 namespace armnn_driver
89 {
90 template<typename HalVersion>
91 RequestThread<ArmnnPreparedModel, HalVersion, CallbackContext_1_0>
92 ArmnnPreparedModel<HalVersion>::m_RequestThread;
93
94 template<typename HalVersion>
95 std::unique_ptr<armnn::Threadpool> ArmnnPreparedModel<HalVersion>::m_Threadpool(nullptr);
96
97 template<typename HalVersion>
98 template <typename TensorBindingCollection>
DumpTensorsIfRequired(char const * tensorNamePrefix,const TensorBindingCollection & tensorBindings)99 void ArmnnPreparedModel<HalVersion>::DumpTensorsIfRequired(char const* tensorNamePrefix,
100 const TensorBindingCollection& tensorBindings)
101 {
102 if (!m_RequestInputsAndOutputsDumpDir.empty())
103 {
104 const std::string requestName = std::to_string(m_NetworkId) + "_" + std::to_string(m_RequestCount) + ".dump";
105 for (std::size_t i = 0u; i < tensorBindings.size(); ++i)
106 {
107 DumpTensor(m_RequestInputsAndOutputsDumpDir,
108 requestName,
109 BuildTensorName(tensorNamePrefix, i),
110 tensorBindings[i].second);
111 }
112 }
113 }
114
115 template<typename HalVersion>
ArmnnPreparedModel(armnn::NetworkId networkId,armnn::IRuntime * runtime,const HalModel & model,const std::string & requestInputsAndOutputsDumpDir,const bool gpuProfilingEnabled,const bool asyncModelExecutionEnabled,const unsigned int numberOfThreads,const bool importEnabled,const bool exportEnabled)116 ArmnnPreparedModel<HalVersion>::ArmnnPreparedModel(armnn::NetworkId networkId,
117 armnn::IRuntime* runtime,
118 const HalModel& model,
119 const std::string& requestInputsAndOutputsDumpDir,
120 const bool gpuProfilingEnabled,
121 const bool asyncModelExecutionEnabled,
122 const unsigned int numberOfThreads,
123 const bool importEnabled,
124 const bool exportEnabled)
125 : m_NetworkId(networkId)
126 , m_Runtime(runtime)
127 , m_Model(model)
128 , m_RequestCount(0)
129 , m_RequestInputsAndOutputsDumpDir(requestInputsAndOutputsDumpDir)
130 , m_GpuProfilingEnabled(gpuProfilingEnabled)
131 , m_AsyncModelExecutionEnabled(asyncModelExecutionEnabled)
132 , m_EnableImport(importEnabled)
133 , m_EnableExport(exportEnabled)
134 {
135 // Enable profiling if required.
136 m_Runtime->GetProfiler(m_NetworkId)->EnableProfiling(m_GpuProfilingEnabled);
137
138 if (m_AsyncModelExecutionEnabled)
139 {
140 std::vector<std::shared_ptr<armnn::IWorkingMemHandle>> memHandles;
141 for (unsigned int i=0; i < numberOfThreads; ++i)
142 {
143 memHandles.emplace_back(m_Runtime->CreateWorkingMemHandle(networkId));
144 }
145
146 if (!m_Threadpool)
147 {
148 m_Threadpool = std::make_unique<armnn::Threadpool>(numberOfThreads, runtime, memHandles);
149 }
150 else
151 {
152 m_Threadpool->LoadMemHandles(memHandles);
153 }
154
155 m_WorkingMemHandle = memHandles.back();
156 }
157 }
158
159 template<typename HalVersion>
~ArmnnPreparedModel()160 ArmnnPreparedModel<HalVersion>::~ArmnnPreparedModel()
161 {
162 // Get a hold of the profiler used by this model.
163 std::shared_ptr<armnn::IProfiler> profiler = m_Runtime->GetProfiler(m_NetworkId);
164 if (profiler && m_GpuProfilingEnabled)
165 {
166 // Dump the profiling info to a file if required.
167 DumpJsonProfilingIfRequired(m_GpuProfilingEnabled, m_RequestInputsAndOutputsDumpDir, m_NetworkId,
168 profiler.get());
169 }
170
171 // Unload the network associated with this model.
172 m_Runtime->UnloadNetwork(m_NetworkId);
173
174 // Unload the network memhandles from the threadpool
175 if (m_AsyncModelExecutionEnabled)
176 {
177 m_Threadpool->UnloadMemHandles(m_NetworkId);
178 }
179 }
180
181 template<typename HalVersion>
execute(const V1_0::Request & request,const::android::sp<V1_0::IExecutionCallback> & callback)182 Return<V1_0::ErrorStatus> ArmnnPreparedModel<HalVersion>::execute(
183 const V1_0::Request& request,
184 const ::android::sp<V1_0::IExecutionCallback>& callback)
185 {
186 ALOGV("ArmnnPreparedModel::execute(): %s", GetModelSummary(m_Model).c_str());
187 m_RequestCount++;
188
189 if (callback.get() == nullptr) {
190 ALOGE("ArmnnPreparedModel::execute invalid callback passed");
191 return V1_0::ErrorStatus::INVALID_ARGUMENT;
192 }
193
194 if (!android::nn::validateRequest(request, m_Model))
195 {
196 NotifyCallbackAndCheck(callback, V1_0::ErrorStatus::INVALID_ARGUMENT, "ArmnnPreparedModel::execute");
197 return V1_0::ErrorStatus::INVALID_ARGUMENT;
198 }
199
200 if (!m_RequestInputsAndOutputsDumpDir.empty())
201 {
202 ALOGD("Dumping inputs and outputs for request %" PRIuPTR, reinterpret_cast<std::uintptr_t>(callback.get()));
203 }
204
205 // allocate the tensors on the heap, as they are passed to the request thread
206 auto pInputTensors = std::make_shared<armnn::InputTensors>();
207 auto pOutputTensors = std::make_shared<armnn::OutputTensors>();
208
209 // map the memory pool into shared pointers
210 // use a shared memory pools vector on the heap, as it is passed to the request thread
211 auto pMemPools = std::make_shared<std::vector<android::nn::RunTimePoolInfo>>();
212 #if !defined(ARMNN_ANDROID_S)
213 if (!setRunTimePoolInfosFromHidlMemories(pMemPools.get(), request.pools))
214 #else
215 if (!setRunTimePoolInfosFromCanonicalMemories(pMemPools.get(), uncheckedConvert(request.pools)))
216 #endif
217 {
218 NotifyCallbackAndCheck(callback, V1_0::ErrorStatus::GENERAL_FAILURE, "ArmnnPreparedModel::execute");
219 return V1_0::ErrorStatus::GENERAL_FAILURE;
220 }
221
222 // add the inputs and outputs with their data
223 try
224 {
225 pInputTensors->reserve(request.inputs.size());
226 for (unsigned int i = 0; i < request.inputs.size(); i++)
227 {
228 const auto& inputArg = request.inputs[i];
229 armnn::TensorInfo inputTensorInfo = m_Runtime->GetInputTensorInfo(m_NetworkId, i);
230 // pInputTensors (of type InputTensors) is composed of a vector of ConstTensors.
231 // Therefore, set all TensorInfo isConstant parameters of input Tensors to true.
232 inputTensorInfo.SetConstant();
233 auto result = ValidateRequestArgument<V1_0::ErrorStatus, V1_0::Request>(request,
234 inputTensorInfo,
235 inputArg,
236 "input");
237 if (result != V1_0::ErrorStatus::NONE)
238 {
239 return result;
240 }
241
242 const armnn::Tensor inputTensor = GetTensorForRequestArgument(inputArg, inputTensorInfo, *pMemPools);
243 if (inputTensor.GetMemoryArea() == nullptr)
244 {
245 ALOGE("Cannot execute request. Error converting request input %u to tensor", i);
246 return V1_0::ErrorStatus::GENERAL_FAILURE;
247 }
248
249 pInputTensors->emplace_back(i, inputTensor);
250 }
251
252 pOutputTensors->reserve(request.outputs.size());
253 for (unsigned int i = 0; i < request.outputs.size(); i++)
254 {
255 const auto& outputArg = request.outputs[i];
256 const armnn::TensorInfo outputTensorInfo = m_Runtime->GetOutputTensorInfo(m_NetworkId, i);
257 auto result = ValidateRequestArgument<V1_0::ErrorStatus, V1_0::Request>(request,
258 outputTensorInfo,
259 outputArg,
260 "output");
261
262 if (result != V1_0::ErrorStatus::NONE)
263 {
264 return result;
265 }
266
267 const armnn::Tensor outputTensor = GetTensorForRequestArgument(outputArg, outputTensorInfo, *pMemPools);
268 if (outputTensor.GetMemoryArea() == nullptr)
269 {
270 ALOGE("Cannot execute request. Error converting request output %u to tensor", i);
271 return V1_0::ErrorStatus::GENERAL_FAILURE;
272 }
273
274 pOutputTensors->emplace_back(i, outputTensor);
275 }
276 }
277 catch (armnn::Exception& e)
278 {
279 ALOGW("armnn::Exception caught while preparing for EnqueueWorkload: %s", e.what());
280 NotifyCallbackAndCheck(callback, V1_0::ErrorStatus::GENERAL_FAILURE, "ArmnnPreparedModel::execute");
281 return V1_0::ErrorStatus::GENERAL_FAILURE;
282 }
283 catch (std::exception& e)
284 {
285 ALOGE("std::exception caught while preparing for EnqueueWorkload: %s", e.what());
286 NotifyCallbackAndCheck(callback, V1_0::ErrorStatus::GENERAL_FAILURE, "ArmnnPreparedModel::execute");
287 return V1_0::ErrorStatus::GENERAL_FAILURE;
288 }
289
290 auto cb = [callback](V1_0::ErrorStatus errorStatus, std::string callingFunction)
291 {
292 NotifyCallbackAndCheck(callback, errorStatus, callingFunction);
293 };
294
295 CallbackContext_1_0 armnnCb;
296 armnnCb.callback = cb;
297
298 if (m_AsyncModelExecutionEnabled)
299 {
300 ALOGV("ArmnnPreparedModel::execute(...) before ScheduleGraphForExecution");
301 ScheduleGraphForExecution(pMemPools, pInputTensors, pOutputTensors, armnnCb);
302 ALOGV("ArmnnPreparedModel::execute(...) after ScheduleGraphForExecution");
303 return V1_0::ErrorStatus::NONE;
304 }
305
306 // post the request for asynchronous execution
307 ALOGV("ArmnnPreparedModel::execute(...) before PostMsg");
308 m_RequestThread.PostMsg(this, pMemPools, pInputTensors, pOutputTensors, armnnCb);
309 ALOGV("ArmnnPreparedModel::execute(...) after PostMsg");
310 return V1_0::ErrorStatus::NONE; // successfully queued
311 }
312
313 template<typename HalVersion>
ExecuteGraph(std::shared_ptr<std::vector<::android::nn::RunTimePoolInfo>> & pMemPools,armnn::InputTensors & inputTensors,armnn::OutputTensors & outputTensors,CallbackContext_1_0 cb)314 void ArmnnPreparedModel<HalVersion>::ExecuteGraph(
315 std::shared_ptr<std::vector<::android::nn::RunTimePoolInfo>>& pMemPools,
316 armnn::InputTensors& inputTensors,
317 armnn::OutputTensors& outputTensors,
318 CallbackContext_1_0 cb)
319 {
320 ALOGV("ArmnnPreparedModel::ExecuteGraph(...)");
321 // Capture the graph execution start time.
322 std::chrono::time_point<std::chrono::system_clock> graphExecutionStart = std::chrono::system_clock::now();
323
324 DumpTensorsIfRequired("Input", inputTensors);
325
326 // run it
327 try
328 {
329 armnn::Status status;
330 if (m_AsyncModelExecutionEnabled)
331 {
332 ALOGW("ArmnnPreparedModel::ExecuteGraph m_AsyncModelExecutionEnabled true");
333 status = m_Runtime->Execute(*m_WorkingMemHandle, inputTensors, outputTensors);
334 }
335 else
336 {
337 ALOGW("ArmnnPreparedModel::ExecuteGraph m_AsyncModelExecutionEnabled false");
338 // Create a vector of Input and Output Ids which can be imported. An empty vector means all will be copied.
339 std::vector<armnn::ImportedInputId> importedInputIds;
340 if (m_EnableImport)
341 {
342 importedInputIds = m_Runtime->ImportInputs(m_NetworkId, inputTensors, armnn::MemorySource::Malloc);
343 }
344 std::vector<armnn::ImportedOutputId> importedOutputIds;
345 if (m_EnableExport)
346 {
347 importedOutputIds = m_Runtime->ImportOutputs(m_NetworkId, outputTensors, armnn::MemorySource::Malloc);
348 }
349 status = m_Runtime->EnqueueWorkload(m_NetworkId, inputTensors, outputTensors,
350 importedInputIds, importedOutputIds);
351 }
352 if (status != armnn::Status::Success)
353 {
354 ALOGW("EnqueueWorkload failed");
355 cb.callback(V1_0::ErrorStatus::GENERAL_FAILURE, "ArmnnPreparedModel::ExecuteGraph");
356 return;
357 }
358 }
359 catch (armnn::Exception& e)
360 {
361 ALOGW("armnn::Exception caught from EnqueueWorkload: %s", e.what());
362 cb.callback(V1_0::ErrorStatus::GENERAL_FAILURE, "ArmnnPreparedModel::ExecuteGraph");
363 return;
364 }
365 catch (std::exception& e)
366 {
367 ALOGE("std::exception caught from EnqueueWorkload: %s", e.what());
368 cb.callback(V1_0::ErrorStatus::GENERAL_FAILURE, "ArmnnPreparedModel::ExecuteGraph");
369 return;
370 }
371
372 DumpTensorsIfRequired("Output", outputTensors);
373
374 // Commit output buffers.
375 // Note that we update *all* pools, even if they aren't actually used as outputs -
376 // this is simpler and is what the CpuExecutor does.
377 for (android::nn::RunTimePoolInfo& pool : *pMemPools)
378 {
379 // Type android::nn::RunTimePoolInfo has changed between Android P & Q and Android R, where
380 // update() has been removed and flush() added.
381 #if defined(ARMNN_ANDROID_R) || defined(ARMNN_ANDROID_S) // Use the new Android implementation.
382 pool.flush();
383 #else
384 pool.update();
385 #endif
386 }
387
388 // Log the total time in this call. This is a good number to compare to that printed out by
389 // RuntimeImpl::EnqueueWorkload. The difference should be the execution overhead of the driver.
390 ALOGI("ArmnnPreparedModel::ExecuteGraph Execution time = %lld µs",
391 std::chrono::duration_cast<std::chrono::microseconds>
392 (std::chrono::system_clock::now() - graphExecutionStart).count());
393
394 cb.callback(V1_0::ErrorStatus::NONE, "ExecuteGraph");
395 }
396
397 template<typename HalVersion>
ExecuteWithDummyInputs()398 bool ArmnnPreparedModel<HalVersion>::ExecuteWithDummyInputs()
399 {
400 std::vector<std::vector<char>> storage;
401 armnn::InputTensors inputTensors;
402 for (unsigned int i = 0; i < getMainModel(m_Model).inputIndexes.size(); i++)
403 {
404 armnn::TensorInfo inputTensorInfo = m_Runtime->GetInputTensorInfo(m_NetworkId, i);
405 // pInputTensors (of type InputTensors) is composed of a vector of ConstTensors.
406 // Therefore, set all TensorInfo isConstant parameters of input Tensors to true.
407 inputTensorInfo.SetConstant();
408
409 storage.emplace_back(inputTensorInfo.GetNumBytes());
410 const armnn::ConstTensor inputTensor(inputTensorInfo, storage.back().data());
411
412 inputTensors.emplace_back(i, inputTensor);
413 }
414
415 armnn::OutputTensors outputTensors;
416 for (unsigned int i = 0; i < getMainModel(m_Model).outputIndexes.size(); i++)
417 {
418 const armnn::TensorInfo outputTensorInfo = m_Runtime->GetOutputTensorInfo(m_NetworkId, i);
419 storage.emplace_back(outputTensorInfo.GetNumBytes());
420 const armnn::Tensor outputTensor(outputTensorInfo, storage.back().data());
421
422 outputTensors.emplace_back(i, outputTensor);
423 }
424
425 try
426 {
427 armnn::Status status;
428 if (m_AsyncModelExecutionEnabled)
429 {
430 ALOGW("ArmnnPreparedModel::ExecuteGraph m_AsyncModelExecutionEnabled true");
431 status = m_Runtime->Execute(*m_WorkingMemHandle, inputTensors, outputTensors);
432 }
433 else
434 {
435 ALOGW("ArmnnPreparedModel::ExecuteGraph m_AsyncModelExecutionEnabled false");
436 // Create a vector of Input and Output Ids which can be imported. An empty vector means all will be copied.
437 std::vector<armnn::ImportedInputId> importedInputIds;
438 if (m_EnableImport)
439 {
440 importedInputIds = m_Runtime->ImportInputs(m_NetworkId, inputTensors, armnn::MemorySource::Malloc);
441 }
442 std::vector<armnn::ImportedOutputId> importedOutputIds;
443 if (m_EnableExport)
444 {
445 importedOutputIds = m_Runtime->ImportOutputs(m_NetworkId, outputTensors, armnn::MemorySource::Malloc);
446 }
447 status = m_Runtime->EnqueueWorkload(m_NetworkId, inputTensors, outputTensors,
448 importedInputIds, importedOutputIds);
449 }
450 if (status != armnn::Status::Success)
451 {
452 ALOGW("ExecuteWithDummyInputs: EnqueueWorkload failed");
453 return false;
454 }
455 }
456 catch (armnn::Exception& e)
457 {
458 ALOGW("ExecuteWithDummyInputs: armnn::Exception caught from EnqueueWorkload: %s", e.what());
459 return false;
460 }
461 catch (std::exception& e)
462 {
463 ALOGE("ExecuteWithDummyInputs: std::exception caught from EnqueueWorkload: %s", e.what());
464 return false;
465 }
466 return true;
467 }
468
469 /// Schedule the graph prepared from the request for execution
470 template<typename HalVersion>
471 template<typename CallbackContext>
ScheduleGraphForExecution(std::shared_ptr<std::vector<::android::nn::RunTimePoolInfo>> & pMemPools,std::shared_ptr<armnn::InputTensors> & inputTensors,std::shared_ptr<armnn::OutputTensors> & outputTensors,CallbackContext callbackContext)472 void ArmnnPreparedModel<HalVersion>::ScheduleGraphForExecution(
473 std::shared_ptr<std::vector<::android::nn::RunTimePoolInfo>>& pMemPools,
474 std::shared_ptr<armnn::InputTensors>& inputTensors,
475 std::shared_ptr<armnn::OutputTensors>& outputTensors,
476 CallbackContext callbackContext)
477 {
478 ALOGV("ArmnnPreparedModel::ScheduleGraphForExecution(...)");
479
480 DumpTensorsIfRequired("Input", *inputTensors);
481
482
483 auto tpCb = std::make_shared<
484 ArmnnThreadPoolCallback<CallbackContext_1_0>>(this,
485 pMemPools,
486 inputTensors,
487 outputTensors,
488 callbackContext);
489
490 m_Threadpool->Schedule(m_NetworkId,
491 *tpCb->m_InputTensors,
492 *tpCb->m_OutputTensors,
493 armnn::QosExecPriority::Medium,
494 tpCb);
495 ALOGV("ArmnnPreparedModel::ScheduleGraphForExecution end");
496 }
497
498 template<typename HalVersion>
499 template <typename CallbackContext>
Notify(armnn::Status status,armnn::InferenceTimingPair timeTaken)500 void ArmnnPreparedModel<HalVersion>::ArmnnThreadPoolCallback<CallbackContext>::Notify(
501 armnn::Status status, armnn::InferenceTimingPair timeTaken)
502 {
503 armnn::IgnoreUnused(status, timeTaken);
504 ALOGV("ArmnnPreparedModel::ArmnnThreadPoolCallback_1_2 Notify");
505
506 m_Model->DumpTensorsIfRequired("Output", *m_OutputTensors);
507
508 // Commit output buffers.
509 // Note that we update *all* pools, even if they aren't actually used as outputs -
510 // this is simpler and is what the CpuExecutor does.
511 for (android::nn::RunTimePoolInfo& pool : *m_MemPools)
512 {
513 // Type android::nn::RunTimePoolInfo has changed between Android P & Q and Android R, where
514 // update() has been removed and flush() added.
515 #if defined(ARMNN_ANDROID_R) || defined(ARMNN_ANDROID_S) // Use the new Android implementation.
516 pool.flush();
517 #else
518 pool.update();
519 #endif
520 }
521
522 m_CallbackContext.callback(V1_0::ErrorStatus::NONE, "ArmnnPreparedModel::ArmnnThreadPoolCallback_1_2 Notify");
523 return;
524 }
525
526 ///
527 /// Class template specializations
528 ///
529
530 template class ArmnnPreparedModel<hal_1_0::HalPolicy>;
531 template void ArmnnPreparedModel<hal_1_0::HalPolicy>::ScheduleGraphForExecution<CallbackContext_1_0>(
532 std::shared_ptr<std::vector<::android::nn::RunTimePoolInfo>>& pMemPools,
533 std::shared_ptr<armnn::InputTensors>& inputTensors,
534 std::shared_ptr<armnn::OutputTensors>& outputTensors,
535 CallbackContext_1_0 callbackContext);
536
537 #ifdef ARMNN_ANDROID_NN_V1_1
538 template class ArmnnPreparedModel<hal_1_1::HalPolicy>;
539 #endif
540
541 #ifdef ARMNN_ANDROID_NN_V1_2
542 template class ArmnnPreparedModel<hal_1_1::HalPolicy>;
543 template class ArmnnPreparedModel<hal_1_2::HalPolicy>;
544 #endif
545
546 #ifdef ARMNN_ANDROID_NN_V1_3
547 template class ArmnnPreparedModel<hal_1_1::HalPolicy>;
548 template class ArmnnPreparedModel<hal_1_2::HalPolicy>;
549 template class ArmnnPreparedModel<hal_1_3::HalPolicy>;
550 #endif
551 } // namespace armnn_driver
552