blob: 212863e1834323609bb2d2c1146bd4332b29e429 [file] [log] [blame]
Michael Butlerf6b2d1a2020-12-19 14:44:35 -08001/*
2 * Copyright (C) 2019 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17#define LOG_TAG "ExecutionBurstController"
18
19#include "ExecutionBurstController.h"
20
21#include <android-base/logging.h>
22
23#include <algorithm>
24#include <cstring>
25#include <limits>
26#include <memory>
27#include <string>
28#include <tuple>
29#include <utility>
30#include <vector>
31
32#include "HalInterfaces.h"
33#include "Tracing.h"
34#include "Utils.h"
35
36namespace android::nn {
37namespace {
38
39using V1_2::FmqRequestDatum;
40using V1_2::FmqResultDatum;
41using V1_2::IBurstCallback;
42using V1_2::IBurstContext;
43using FmqRequestDescriptor = hardware::MQDescriptorSync<FmqRequestDatum>;
44using FmqResultDescriptor = hardware::MQDescriptorSync<FmqResultDatum>;
45
46constexpr V1_2::Timing kNoTiming12 = {std::numeric_limits<uint64_t>::max(),
47 std::numeric_limits<uint64_t>::max()};
48
49class BurstContextDeathHandler : public hardware::hidl_death_recipient {
50 public:
51 using Callback = std::function<void()>;
52
53 BurstContextDeathHandler(const Callback& onDeathCallback) : mOnDeathCallback(onDeathCallback) {
54 CHECK(onDeathCallback != nullptr);
55 }
56
57 void serviceDied(uint64_t /*cookie*/, const wp<hidl::base::V1_0::IBase>& /*who*/) override {
58 LOG(ERROR) << "BurstContextDeathHandler::serviceDied -- service unexpectedly died!";
59 mOnDeathCallback();
60 }
61
62 private:
63 const Callback mOnDeathCallback;
64};
65
66} // anonymous namespace
67
68// serialize a request into a packet
69std::vector<FmqRequestDatum> serialize(const V1_0::Request& request, V1_2::MeasureTiming measure,
70 const std::vector<int32_t>& slots) {
71 // count how many elements need to be sent for a request
72 size_t count = 2 + request.inputs.size() + request.outputs.size() + request.pools.size();
73 for (const auto& input : request.inputs) {
74 count += input.dimensions.size();
75 }
76 for (const auto& output : request.outputs) {
77 count += output.dimensions.size();
78 }
79
80 // create buffer to temporarily store elements
81 std::vector<FmqRequestDatum> data;
82 data.reserve(count);
83
84 // package packetInfo
85 {
86 FmqRequestDatum datum;
87 datum.packetInformation(
88 {/*.packetSize=*/static_cast<uint32_t>(count),
89 /*.numberOfInputOperands=*/static_cast<uint32_t>(request.inputs.size()),
90 /*.numberOfOutputOperands=*/static_cast<uint32_t>(request.outputs.size()),
91 /*.numberOfPools=*/static_cast<uint32_t>(request.pools.size())});
92 data.push_back(datum);
93 }
94
95 // package input data
96 for (const auto& input : request.inputs) {
97 // package operand information
98 FmqRequestDatum datum;
99 datum.inputOperandInformation(
100 {/*.hasNoValue=*/input.hasNoValue,
101 /*.location=*/input.location,
102 /*.numberOfDimensions=*/static_cast<uint32_t>(input.dimensions.size())});
103 data.push_back(datum);
104
105 // package operand dimensions
106 for (uint32_t dimension : input.dimensions) {
107 FmqRequestDatum datum;
108 datum.inputOperandDimensionValue(dimension);
109 data.push_back(datum);
110 }
111 }
112
113 // package output data
114 for (const auto& output : request.outputs) {
115 // package operand information
116 FmqRequestDatum datum;
117 datum.outputOperandInformation(
118 {/*.hasNoValue=*/output.hasNoValue,
119 /*.location=*/output.location,
120 /*.numberOfDimensions=*/static_cast<uint32_t>(output.dimensions.size())});
121 data.push_back(datum);
122
123 // package operand dimensions
124 for (uint32_t dimension : output.dimensions) {
125 FmqRequestDatum datum;
126 datum.outputOperandDimensionValue(dimension);
127 data.push_back(datum);
128 }
129 }
130
131 // package pool identifier
132 for (int32_t slot : slots) {
133 FmqRequestDatum datum;
134 datum.poolIdentifier(slot);
135 data.push_back(datum);
136 }
137
138 // package measureTiming
139 {
140 FmqRequestDatum datum;
141 datum.measureTiming(measure);
142 data.push_back(datum);
143 }
144
145 // return packet
146 return data;
147}
148
149// deserialize a packet into the result
150std::optional<std::tuple<V1_0::ErrorStatus, std::vector<V1_2::OutputShape>, V1_2::Timing>>
151deserialize(const std::vector<FmqResultDatum>& data) {
152 using discriminator = FmqResultDatum::hidl_discriminator;
153
154 std::vector<V1_2::OutputShape> outputShapes;
155 size_t index = 0;
156
157 // validate packet information
158 if (data.size() == 0 || data[index].getDiscriminator() != discriminator::packetInformation) {
159 LOG(ERROR) << "FMQ Result packet ill-formed";
160 return std::nullopt;
161 }
162
163 // unpackage packet information
164 const FmqResultDatum::PacketInformation& packetInfo = data[index].packetInformation();
165 index++;
166 const uint32_t packetSize = packetInfo.packetSize;
167 const V1_0::ErrorStatus errorStatus = packetInfo.errorStatus;
168 const uint32_t numberOfOperands = packetInfo.numberOfOperands;
169
170 // verify packet size
171 if (data.size() != packetSize) {
172 LOG(ERROR) << "FMQ Result packet ill-formed";
173 return std::nullopt;
174 }
175
176 // unpackage operands
177 for (size_t operand = 0; operand < numberOfOperands; ++operand) {
178 // validate operand information
179 if (data[index].getDiscriminator() != discriminator::operandInformation) {
180 LOG(ERROR) << "FMQ Result packet ill-formed";
181 return std::nullopt;
182 }
183
184 // unpackage operand information
185 const FmqResultDatum::OperandInformation& operandInfo = data[index].operandInformation();
186 index++;
187 const bool isSufficient = operandInfo.isSufficient;
188 const uint32_t numberOfDimensions = operandInfo.numberOfDimensions;
189
190 // unpackage operand dimensions
191 std::vector<uint32_t> dimensions;
192 dimensions.reserve(numberOfDimensions);
193 for (size_t i = 0; i < numberOfDimensions; ++i) {
194 // validate dimension
195 if (data[index].getDiscriminator() != discriminator::operandDimensionValue) {
196 LOG(ERROR) << "FMQ Result packet ill-formed";
197 return std::nullopt;
198 }
199
200 // unpackage dimension
201 const uint32_t dimension = data[index].operandDimensionValue();
202 index++;
203
204 // store result
205 dimensions.push_back(dimension);
206 }
207
208 // store result
209 outputShapes.push_back({/*.dimensions=*/dimensions, /*.isSufficient=*/isSufficient});
210 }
211
212 // validate execution timing
213 if (data[index].getDiscriminator() != discriminator::executionTiming) {
214 LOG(ERROR) << "FMQ Result packet ill-formed";
215 return std::nullopt;
216 }
217
218 // unpackage execution timing
219 const V1_2::Timing timing = data[index].executionTiming();
220 index++;
221
222 // validate packet information
223 if (index != packetSize) {
224 LOG(ERROR) << "FMQ Result packet ill-formed";
225 return std::nullopt;
226 }
227
228 // return result
229 return std::make_tuple(errorStatus, std::move(outputShapes), timing);
230}
231
232V1_0::ErrorStatus legacyConvertResultCodeToErrorStatus(int resultCode) {
233 return convertToV1_0(convertResultCodeToErrorStatus(resultCode));
234}
235
236std::pair<std::unique_ptr<ResultChannelReceiver>, const FmqResultDescriptor*>
237ResultChannelReceiver::create(size_t channelLength, std::chrono::microseconds pollingTimeWindow) {
238 std::unique_ptr<FmqResultChannel> fmqResultChannel =
239 std::make_unique<FmqResultChannel>(channelLength, /*confEventFlag=*/true);
240 if (!fmqResultChannel->isValid()) {
241 LOG(ERROR) << "Unable to create ResultChannelReceiver";
242 return {nullptr, nullptr};
243 }
244
245 const FmqResultDescriptor* descriptor = fmqResultChannel->getDesc();
246 return std::make_pair(
247 std::make_unique<ResultChannelReceiver>(std::move(fmqResultChannel), pollingTimeWindow),
248 descriptor);
249}
250
251ResultChannelReceiver::ResultChannelReceiver(std::unique_ptr<FmqResultChannel> fmqResultChannel,
252 std::chrono::microseconds pollingTimeWindow)
253 : mFmqResultChannel(std::move(fmqResultChannel)), kPollingTimeWindow(pollingTimeWindow) {}
254
255std::optional<std::tuple<V1_0::ErrorStatus, std::vector<V1_2::OutputShape>, V1_2::Timing>>
256ResultChannelReceiver::getBlocking() {
257 const auto packet = getPacketBlocking();
258 if (!packet) {
259 return std::nullopt;
260 }
261
262 return deserialize(*packet);
263}
264
265void ResultChannelReceiver::invalidate() {
266 mValid = false;
267
268 // force unblock
269 // ExecutionBurstController waits on a result packet after sending a
270 // request. If the driver containing ExecutionBurstServer crashes, the
271 // controller may be waiting on the futex. This force unblock wakes up any
272 // thread waiting on the futex.
273 // TODO: look for a different/better way to signal/notify the futex to
274 // wake up any thread waiting on it
275 FmqResultDatum datum;
276 datum.packetInformation({/*.packetSize=*/0,
277 /*.errorStatus=*/V1_0::ErrorStatus::GENERAL_FAILURE,
278 /*.numberOfOperands=*/0});
279 mFmqResultChannel->writeBlocking(&datum, 1);
280}
281
282std::optional<std::vector<FmqResultDatum>> ResultChannelReceiver::getPacketBlocking() {
283 if (!mValid) {
284 return std::nullopt;
285 }
286
287 // First spend time polling if results are available in FMQ instead of
288 // waiting on the futex. Polling is more responsive (yielding lower
289 // latencies), but can take up more power, so only poll for a limited period
290 // of time.
291
292 auto& getCurrentTime = std::chrono::high_resolution_clock::now;
293 const auto timeToStopPolling = getCurrentTime() + kPollingTimeWindow;
294
295 while (getCurrentTime() < timeToStopPolling) {
296 // if class is being torn down, immediately return
297 if (!mValid.load(std::memory_order_relaxed)) {
298 return std::nullopt;
299 }
300
301 // Check if data is available. If it is, immediately retrieve it and
302 // return.
303 const size_t available = mFmqResultChannel->availableToRead();
304 if (available > 0) {
305 std::vector<FmqResultDatum> packet(available);
306 const bool success = mFmqResultChannel->read(packet.data(), available);
307 if (!success) {
308 LOG(ERROR) << "Error receiving packet";
309 return std::nullopt;
310 }
311 return std::make_optional(std::move(packet));
312 }
313 }
314
315 // If we get to this point, we either stopped polling because it was taking
316 // too long or polling was not allowed. Instead, perform a blocking call
317 // which uses a futex to save power.
318
319 // wait for result packet and read first element of result packet
320 FmqResultDatum datum;
321 bool success = mFmqResultChannel->readBlocking(&datum, 1);
322
323 // retrieve remaining elements
324 // NOTE: all of the data is already available at this point, so there's no
325 // need to do a blocking wait to wait for more data. This is known because
326 // in FMQ, all writes are published (made available) atomically. Currently,
327 // the producer always publishes the entire packet in one function call, so
328 // if the first element of the packet is available, the remaining elements
329 // are also available.
330 const size_t count = mFmqResultChannel->availableToRead();
331 std::vector<FmqResultDatum> packet(count + 1);
332 std::memcpy(&packet.front(), &datum, sizeof(datum));
333 success &= mFmqResultChannel->read(packet.data() + 1, count);
334
335 if (!mValid) {
336 return std::nullopt;
337 }
338
339 // ensure packet was successfully received
340 if (!success) {
341 LOG(ERROR) << "Error receiving packet";
342 return std::nullopt;
343 }
344
345 return std::make_optional(std::move(packet));
346}
347
348std::pair<std::unique_ptr<RequestChannelSender>, const FmqRequestDescriptor*>
349RequestChannelSender::create(size_t channelLength) {
350 std::unique_ptr<FmqRequestChannel> fmqRequestChannel =
351 std::make_unique<FmqRequestChannel>(channelLength, /*confEventFlag=*/true);
352 if (!fmqRequestChannel->isValid()) {
353 LOG(ERROR) << "Unable to create RequestChannelSender";
354 return {nullptr, nullptr};
355 }
356
357 const FmqRequestDescriptor* descriptor = fmqRequestChannel->getDesc();
358 return std::make_pair(std::make_unique<RequestChannelSender>(std::move(fmqRequestChannel)),
359 descriptor);
360}
361
362RequestChannelSender::RequestChannelSender(std::unique_ptr<FmqRequestChannel> fmqRequestChannel)
363 : mFmqRequestChannel(std::move(fmqRequestChannel)) {}
364
365bool RequestChannelSender::send(const V1_0::Request& request, V1_2::MeasureTiming measure,
366 const std::vector<int32_t>& slots) {
367 const std::vector<FmqRequestDatum> serialized = serialize(request, measure, slots);
368 return sendPacket(serialized);
369}
370
371bool RequestChannelSender::sendPacket(const std::vector<FmqRequestDatum>& packet) {
372 if (!mValid) {
373 return false;
374 }
375
376 if (packet.size() > mFmqRequestChannel->availableToWrite()) {
377 LOG(ERROR)
378 << "RequestChannelSender::sendPacket -- packet size exceeds size available in FMQ";
379 return false;
380 }
381
382 // Always send the packet with "blocking" because this signals the futex and
383 // unblocks the consumer if it is waiting on the futex.
384 return mFmqRequestChannel->writeBlocking(packet.data(), packet.size());
385}
386
387void RequestChannelSender::invalidate() {
388 mValid = false;
389}
390
391hardware::Return<void> ExecutionBurstController::ExecutionBurstCallback::getMemories(
392 const hardware::hidl_vec<int32_t>& slots, getMemories_cb cb) {
393 std::lock_guard<std::mutex> guard(mMutex);
394
395 // get all memories
396 hardware::hidl_vec<hardware::hidl_memory> memories(slots.size());
397 std::transform(slots.begin(), slots.end(), memories.begin(), [this](int32_t slot) {
398 return slot < mMemoryCache.size() ? mMemoryCache[slot] : hardware::hidl_memory{};
399 });
400
401 // ensure all memories are valid
402 if (!std::all_of(memories.begin(), memories.end(),
403 [](const hardware::hidl_memory& memory) { return memory.valid(); })) {
404 cb(V1_0::ErrorStatus::INVALID_ARGUMENT, {});
405 return hardware::Void();
406 }
407
408 // return successful
409 cb(V1_0::ErrorStatus::NONE, std::move(memories));
410 return hardware::Void();
411}
412
413std::vector<int32_t> ExecutionBurstController::ExecutionBurstCallback::getSlots(
414 const hardware::hidl_vec<hardware::hidl_memory>& memories,
415 const std::vector<intptr_t>& keys) {
416 std::lock_guard<std::mutex> guard(mMutex);
417
418 // retrieve (or bind) all slots corresponding to memories
419 std::vector<int32_t> slots;
420 slots.reserve(memories.size());
421 for (size_t i = 0; i < memories.size(); ++i) {
422 slots.push_back(getSlotLocked(memories[i], keys[i]));
423 }
424 return slots;
425}
426
427std::pair<bool, int32_t> ExecutionBurstController::ExecutionBurstCallback::freeMemory(
428 intptr_t key) {
429 std::lock_guard<std::mutex> guard(mMutex);
430
431 auto iter = mMemoryIdToSlot.find(key);
432 if (iter == mMemoryIdToSlot.end()) {
433 return {false, 0};
434 }
435 const int32_t slot = iter->second;
436 mMemoryIdToSlot.erase(key);
437 mMemoryCache[slot] = {};
438 mFreeSlots.push(slot);
439 return {true, slot};
440}
441
442int32_t ExecutionBurstController::ExecutionBurstCallback::getSlotLocked(
443 const hardware::hidl_memory& memory, intptr_t key) {
444 auto iter = mMemoryIdToSlot.find(key);
445 if (iter == mMemoryIdToSlot.end()) {
446 const int32_t slot = allocateSlotLocked();
447 mMemoryIdToSlot[key] = slot;
448 mMemoryCache[slot] = memory;
449 return slot;
450 } else {
451 const int32_t slot = iter->second;
452 return slot;
453 }
454}
455
456int32_t ExecutionBurstController::ExecutionBurstCallback::allocateSlotLocked() {
457 constexpr size_t kMaxNumberOfSlots = std::numeric_limits<int32_t>::max();
458
459 // if there is a free slot, use it
460 if (mFreeSlots.size() > 0) {
461 const int32_t slot = mFreeSlots.top();
462 mFreeSlots.pop();
463 return slot;
464 }
465
466 // otherwise use a slot for the first time
467 CHECK(mMemoryCache.size() < kMaxNumberOfSlots) << "Exceeded maximum number of slots!";
468 const int32_t slot = static_cast<int32_t>(mMemoryCache.size());
469 mMemoryCache.emplace_back();
470
471 return slot;
472}
473
474std::unique_ptr<ExecutionBurstController> ExecutionBurstController::create(
475 const sp<V1_2::IPreparedModel>& preparedModel,
476 std::chrono::microseconds pollingTimeWindow) {
477 // check inputs
478 if (preparedModel == nullptr) {
479 LOG(ERROR) << "ExecutionBurstController::create passed a nullptr";
480 return nullptr;
481 }
482
483 // create callback object
484 sp<ExecutionBurstCallback> callback = new ExecutionBurstCallback();
485
486 // create FMQ objects
487 auto [requestChannelSenderTemp, requestChannelDescriptor] =
488 RequestChannelSender::create(kExecutionBurstChannelLength);
489 auto [resultChannelReceiverTemp, resultChannelDescriptor] =
490 ResultChannelReceiver::create(kExecutionBurstChannelLength, pollingTimeWindow);
491 std::shared_ptr<RequestChannelSender> requestChannelSender =
492 std::move(requestChannelSenderTemp);
493 std::shared_ptr<ResultChannelReceiver> resultChannelReceiver =
494 std::move(resultChannelReceiverTemp);
495
496 // check FMQ objects
497 if (!requestChannelSender || !resultChannelReceiver || !requestChannelDescriptor ||
498 !resultChannelDescriptor) {
499 LOG(ERROR) << "ExecutionBurstController::create failed to create FastMessageQueue";
500 return nullptr;
501 }
502
503 // configure burst
504 V1_0::ErrorStatus errorStatus;
505 sp<IBurstContext> burstContext;
506 const hardware::Return<void> ret = preparedModel->configureExecutionBurst(
507 callback, *requestChannelDescriptor, *resultChannelDescriptor,
508 [&errorStatus, &burstContext](V1_0::ErrorStatus status,
509 const sp<IBurstContext>& context) {
510 errorStatus = status;
511 burstContext = context;
512 });
513
514 // check burst
515 if (!ret.isOk()) {
516 LOG(ERROR) << "IPreparedModel::configureExecutionBurst failed with description "
517 << ret.description();
518 return nullptr;
519 }
520 if (errorStatus != V1_0::ErrorStatus::NONE) {
521 LOG(ERROR) << "IPreparedModel::configureExecutionBurst failed with status "
522 << toString(errorStatus);
523 return nullptr;
524 }
525 if (burstContext == nullptr) {
526 LOG(ERROR) << "IPreparedModel::configureExecutionBurst returned nullptr for burst";
527 return nullptr;
528 }
529
530 // create death handler object
531 BurstContextDeathHandler::Callback onDeathCallback = [requestChannelSender,
532 resultChannelReceiver] {
533 requestChannelSender->invalidate();
534 resultChannelReceiver->invalidate();
535 };
536 const sp<BurstContextDeathHandler> deathHandler = new BurstContextDeathHandler(onDeathCallback);
537
538 // linkToDeath registers a callback that will be invoked on service death to
539 // proactively handle service crashes. If the linkToDeath call fails,
540 // asynchronous calls are susceptible to hangs if the service crashes before
541 // providing the response.
542 const hardware::Return<bool> deathHandlerRet = burstContext->linkToDeath(deathHandler, 0);
543 if (!deathHandlerRet.isOk() || deathHandlerRet != true) {
544 LOG(ERROR) << "ExecutionBurstController::create -- Failed to register a death recipient "
545 "for the IBurstContext object.";
546 return nullptr;
547 }
548
549 // make and return controller
550 return std::make_unique<ExecutionBurstController>(requestChannelSender, resultChannelReceiver,
551 burstContext, callback, deathHandler);
552}
553
554ExecutionBurstController::ExecutionBurstController(
555 const std::shared_ptr<RequestChannelSender>& requestChannelSender,
556 const std::shared_ptr<ResultChannelReceiver>& resultChannelReceiver,
557 const sp<IBurstContext>& burstContext, const sp<ExecutionBurstCallback>& callback,
558 const sp<hardware::hidl_death_recipient>& deathHandler)
559 : mRequestChannelSender(requestChannelSender),
560 mResultChannelReceiver(resultChannelReceiver),
561 mBurstContext(burstContext),
562 mMemoryCache(callback),
563 mDeathHandler(deathHandler) {}
564
565ExecutionBurstController::~ExecutionBurstController() {
566 // It is safe to ignore any errors resulting from this unlinkToDeath call
567 // because the ExecutionBurstController object is already being destroyed
568 // and its underlying IBurstContext object is no longer being used by the NN
569 // runtime.
570 if (mDeathHandler) {
571 mBurstContext->unlinkToDeath(mDeathHandler).isOk();
572 }
573}
574
575static std::tuple<int, std::vector<V1_2::OutputShape>, V1_2::Timing, bool> getExecutionResult(
576 V1_0::ErrorStatus status, std::vector<V1_2::OutputShape> outputShapes, V1_2::Timing timing,
577 bool fallback) {
578 auto [n, checkedOutputShapes, checkedTiming] =
579 getExecutionResult(convertToV1_3(status), std::move(outputShapes), timing);
580 return {n, convertToV1_2(checkedOutputShapes), convertToV1_2(checkedTiming), fallback};
581}
582
583std::tuple<int, std::vector<V1_2::OutputShape>, V1_2::Timing, bool>
584ExecutionBurstController::compute(const V1_0::Request& request, V1_2::MeasureTiming measure,
585 const std::vector<intptr_t>& memoryIds) {
586 // This is the first point when we know an execution is occurring, so begin
587 // to collect systraces. Note that the first point we can begin collecting
588 // systraces in ExecutionBurstServer is when the RequestChannelReceiver
589 // realizes there is data in the FMQ, so ExecutionBurstServer collects
590 // systraces at different points in the code.
591 NNTRACE_FULL(NNTRACE_LAYER_IPC, NNTRACE_PHASE_EXECUTION, "ExecutionBurstController::compute");
592
593 std::lock_guard<std::mutex> guard(mMutex);
594
595 // send request packet
596 const std::vector<int32_t> slots = mMemoryCache->getSlots(request.pools, memoryIds);
597 const bool success = mRequestChannelSender->send(request, measure, slots);
598 if (!success) {
599 LOG(ERROR) << "Error sending FMQ packet";
600 // only use fallback execution path if the packet could not be sent
601 return getExecutionResult(V1_0::ErrorStatus::GENERAL_FAILURE, {}, kNoTiming12,
602 /*fallback=*/true);
603 }
604
605 // get result packet
606 const auto result = mResultChannelReceiver->getBlocking();
607 if (!result) {
608 LOG(ERROR) << "Error retrieving FMQ packet";
609 // only use fallback execution path if the packet could not be sent
610 return getExecutionResult(V1_0::ErrorStatus::GENERAL_FAILURE, {}, kNoTiming12,
611 /*fallback=*/false);
612 }
613
614 // unpack results and return (only use fallback execution path if the
615 // packet could not be sent)
616 auto [status, outputShapes, timing] = std::move(*result);
617 return getExecutionResult(status, std::move(outputShapes), timing, /*fallback=*/false);
618}
619
620void ExecutionBurstController::freeMemory(intptr_t key) {
621 std::lock_guard<std::mutex> guard(mMutex);
622
623 bool valid;
624 int32_t slot;
625 std::tie(valid, slot) = mMemoryCache->freeMemory(key);
626 if (valid) {
627 mBurstContext->freeMemory(slot).isOk();
628 }
629}
630
631} // namespace android::nn