/* Copyright 2019 The TensorFlow Authors. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ #import "tensorflow/lite/delegates/gpu/metal_delegate.h" #import #include #include #include #include #include #include #include #include #include #include "absl/container/flat_hash_set.h" #include "absl/types/span.h" #include "tensorflow/lite/builtin_ops.h" #include "tensorflow/lite/context_util.h" #include "tensorflow/lite/core/c/common.h" #include "tensorflow/lite/delegates/gpu/common/convert.h" #include "tensorflow/lite/delegates/gpu/common/gpu_info.h" #include "tensorflow/lite/delegates/gpu/common/model.h" #include "tensorflow/lite/delegates/gpu/common/model_builder.h" #include "tensorflow/lite/delegates/gpu/common/model_transformer.h" #include "tensorflow/lite/delegates/gpu/common/precision.h" #include "tensorflow/lite/delegates/gpu/common/quantization_util.h" #include "tensorflow/lite/delegates/gpu/common/shape.h" #include "tensorflow/lite/delegates/gpu/common/status.h" #include "tensorflow/lite/delegates/gpu/common/types.h" #include "tensorflow/lite/delegates/gpu/metal/buffer_convert.h" #include "tensorflow/lite/delegates/gpu/metal/common.h" #include "tensorflow/lite/delegates/gpu/metal/inference_context.h" #include "tensorflow/lite/delegates/gpu/metal/metal_spatial_tensor.h" #include "tensorflow/lite/kernels/kernel_util.h" #include "tensorflow/lite/minimal_logging.h" namespace tflite { namespace gpu { namespace metal { namespace { // Multi-thread safe alarm clock for preventing GPU sleeping. It spawns lightweight compute tasks // until no inference is performing on a device. It's reduces the CPU-to-CPU inference latency. // The class is used only for kAggressive wait type. class GpuAlarmClock { public: explicit GpuAlarmClock(id command_queue) { auto device = [command_queue device]; std::lock_guard lock(alarms_mutex_); if (!alarms_) alarms_ = new std::map, GpuAlarmClockInternal*>(); auto it = alarms_->find(device); if (it == alarms_->end()) { internal_ = new GpuAlarmClockInternal(command_queue); (*alarms_)[device] = internal_; } else { internal_ = it->second; internal_->total_alarms_++; } } ~GpuAlarmClock() { std::lock_guard lock(alarms_mutex_); if (--internal_->total_alarms_ > 0) return; Stop(); delete internal_; // Remove the alarm from the container to free-up device handle. for (auto it = alarms_->begin(); it != alarms_->end(); ++it) { if (it->second == internal_) { alarms_->erase(it); break; } } if (alarms_->empty()) { delete alarms_; alarms_ = nullptr; } } void Start() { if (started_) return; started_ = true; internal_->active_alarms_++; } void Stop() { if (!started_) return; started_ = false; internal_->active_alarms_--; } private: class GpuAlarmClockInternal { public: id stub_program_; id stub_buffer_; explicit GpuAlarmClockInternal(id command_queue) { command_queue_ = command_queue; device_ = [command_queue_ device]; total_alarms_ = 1; id program; // TODO(impjdi): Properly handle returned status. CreateComputeProgram(device_, "kernel void ComputeFunction(device int* output_buffer [[buffer(0)]]) { " "output_buffer[0] = 0; }", "ComputeFunction", {}, &program) .IgnoreError(); stub_program_ = program; stub_buffer_ = [device_ newBufferWithLength:sizeof(int) * 4 options:MTLResourceHazardTrackingModeUntracked]; alarm_thread_ = std::thread([this]() { id prev_command_buffer; while (!release_thread_) { @autoreleasepool { if (active_alarms_ == total_alarms_) { id command_buffer = [command_queue_ commandBuffer]; id encoder = [command_buffer computeCommandEncoder]; [encoder setComputePipelineState:stub_program_]; [encoder setBuffer:stub_buffer_ offset:0 atIndex:0]; [encoder dispatchThreadgroups:MTLSizeMake(1, 1, 1) threadsPerThreadgroup:MTLSizeMake(1, 1, 1)]; [encoder endEncoding]; [command_buffer commit]; if (prev_command_buffer != nil) [prev_command_buffer waitUntilScheduled]; prev_command_buffer = command_buffer; } else { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } } }); } ~GpuAlarmClockInternal() { release_thread_ = true; alarm_thread_.join(); } private: friend class GpuAlarmClock; std::atomic active_alarms_; std::thread alarm_thread_; id command_queue_; id device_; volatile bool release_thread_ = false; int total_alarms_ = 0; }; static std::map, GpuAlarmClockInternal*>* alarms_; std::mutex alarms_mutex_; GpuAlarmClockInternal* internal_; bool started_ = false; }; std::map, GpuAlarmClock::GpuAlarmClockInternal*>* GpuAlarmClock::alarms_ = nullptr; // Forward declaration. TfLiteStatus DelegatePrepare(TfLiteContext* context, TfLiteDelegate* delegate); class Delegate { struct ValueRef { BHWC shape; int64_t tensor_id; }; public: explicit Delegate(const TFLGpuDelegateOptions* options) { if (options) { options_ = *options; } else { options_ = TFLGpuDelegateOptionsDefault(); } metal_device_ = MTLCreateSystemDefaultDevice(); command_queue_ = [metal_device_ newCommandQueue]; if (options_.wait_type == TFLGpuDelegateWaitType::TFLGpuDelegateWaitTypeAggressive) { gpu_alarm_clock_ = std::unique_ptr(new GpuAlarmClock(command_queue_)); const std::string code = R"( kernel void ComputeFunction(device int* output_buffer [[buffer(0)]], constant int& value [[buffer(1)]]) { output_buffer[0] = value; } )"; id signal_program; // TODO(impjdi): Properly handle returned status. CreateComputeProgram(metal_device_, code, "ComputeFunction", {}, &signal_program) .IgnoreError(); signal_program_ = signal_program; signal_buffer_ = [metal_device_ newBufferWithLength:sizeof(int) * 4 options:MTLResourceStorageModeShared | MTLResourceHazardTrackingModeUntracked]; } } absl::Status BindBufferToTensor(id buffer, int tensor_index) { // The tensor index is expected to be an input or output tensor of the interpreter. // For quantized model, the buffer should be linked with their dequantized counterpart. if (quant_conversion_map_.find(tensor_index) != quant_conversion_map_.end()) { tensor_index = quant_conversion_map_[tensor_index]; // remove [dequantized tensor ID] -> [quantized tensor ID] mapping, to prevent extra // dequant/quant on in/outputs. quant_conversion_map_.erase(tensor_index); } for (auto& input : graph_inputs_) { if (input.tensor_id == tensor_index) { if (in_out_tensors_[input.id]->GetBufferHandle() != buffer) { RETURN_IF_ERROR(in_out_tensors_[input.id]->SetBufferHandle(buffer)); RETURN_IF_ERROR(inference_context_.SetTensor(input.id, in_out_tensors_[input.id].get())); } input.set_externally = true; return absl::OkStatus(); } } for (auto& output : graph_outputs_) { if (output.tensor_id == tensor_index) { if (in_out_tensors_[output.id]->GetBufferHandle() != buffer) { RETURN_IF_ERROR(in_out_tensors_[output.id]->SetBufferHandle(buffer)); RETURN_IF_ERROR( inference_context_.SetTensor(output.id, in_out_tensors_[output.id].get())); } output.set_externally = true; return absl::OkStatus(); } } return absl::NotFoundError("Couldn't find tensor: " + std::to_string(tensor_index)); } void SetCommandBuffer(id command_buffer) { external_command_buffer_ = command_buffer; } // This directs the runtime to allocate memory for input/output temporary // tensors that require dequantization/quantization. absl::Status GetRequiredTemporaries(TfLiteContext* context, TfLiteNode* node, TfLiteIntArray** temporaries_array_ptr) { if (quant_conversion_map_.empty()) return absl::OkStatus(); std::vector temporary_tensor_ids; for (auto index : input_tensor_ids_) { if (quant_conversion_map_.find(index) != quant_conversion_map_.end()) { temporary_tensor_ids.push_back(index); } } for (auto index : output_tensor_ids_) { if (quant_conversion_map_.find(index) != quant_conversion_map_.end()) { temporary_tensor_ids.push_back(index); } } TfLiteIntArrayFree(*temporaries_array_ptr); *temporaries_array_ptr = TfLiteIntArrayCreate(temporary_tensor_ids.size()); for (int i = 0; i < temporary_tensor_ids.size(); ++i) { (*temporaries_array_ptr)->data[i] = temporary_tensor_ids[i]; } return absl::OkStatus(); } absl::Status Prepare(TfLiteContext* context, const TfLiteDelegateParams* delegate_params) { // Extract TFLite delegate execution plan from the context and convert it into GraphFloat32. GraphFloat32 graph; quant_conversion_map_.clear(); if (options_.enable_quantization) { RETURN_IF_ERROR(BuildFinalModel(context, delegate_params, &graph, &quant_conversion_map_)); } else { RETURN_IF_ERROR(BuildFinalModel(context, delegate_params, &graph)); } // TODO(impjdi): Remove code duplication. auto values = graph.values(); auto find_value = [&](int tensor_index) -> Value* { for (auto value : values) { if (value->tensor.ref == tensor_index) return value; } return nullptr; }; tensors_.reserve(values.back()->id + 1); for (const auto* value : values) { if (tensors_.size() <= value->id) tensors_.resize(value->id + 1); tensors_[value->id] = { value->tensor.shape, // .shape value->tensor.ref, // .tensor_id }; } // Prepare graph inputs. // // Note that graph.inputs() cannot be used directly, as the notion of graph input has a // different meaning in public API and GPU-internal API. for (int tensor_index : TfLiteIntArrayView(delegate_params->input_tensors)) { auto* tensor = &context->tensors[tensor_index]; if (IsConstantTensor(tensor)) continue; // For quantized models, actual inputs of GPU graph are float tensors, so the 8-bit inputs // to the delegate kernel need to be dequantized berfore feeding to the GPU graph. if (options_.enable_quantization) { auto it = quant_conversion_map_.find(tensor_index); if (it != quant_conversion_map_.end()) { tensor_index = it->second; tensor = &context->tensors[tensor_index]; } } const auto* input = find_value(tensor_index); if (!input || tensor->type != TfLiteType::kTfLiteFloat32) { return absl::NotFoundError("Input tensor is not found in the graph."); } inputs_.push_back(input->id); input_tensor_ids_.push_back(tensor_index); tensor->buffer_handle = input->id; tensor->delegate = &delegate_; } // Prepare graph outputs. // // Note that graph.outputs() cannot be used directly, as the notion of graph output has a // different meaning in public API and GPU-internal API. for (int tensor_index : TfLiteIntArrayView(delegate_params->output_tensors)) { auto* tensor = &context->tensors[tensor_index]; if (IsConstantTensor(tensor)) continue; // For quantized models, actual outputs of GPU graph are float tensors, so they should be // quantized to be the 8-bit outputs of delegate. if (options_.enable_quantization) { auto it = quant_conversion_map_.find(tensor_index); if (it != quant_conversion_map_.end()) { tensor_index = quant_conversion_map_[tensor_index]; tensor = &context->tensors[tensor_index]; } } const auto* output = find_value(tensor_index); if (!output || tensor->type != TfLiteType::kTfLiteFloat32) { return absl::NotFoundError("Output tensor is not found in the graph."); } outputs_.push_back(output->id); output_tensor_ids_.push_back(tensor_index); tensor->buffer_handle = output->id; tensor->delegate = &delegate_; } auto utf8_name = [[metal_device_ name] UTF8String]; const std::string device_name = utf8_name != nil ? utf8_name : ""; GpuInfo gpu_info; GetGpuInfoFromDeviceDescription(device_name, GpuApi::kMetal, &gpu_info); size_t storage_type_size; CalculationsPrecision precision; if (options_.allow_precision_loss) { storage_type_size = sizeof(HalfBits); if (gpu_info.IsRoundToNearestSupported()) { precision = CalculationsPrecision::F16; } else { precision = CalculationsPrecision::F32_F16; } } else { storage_type_size = sizeof(float); precision = CalculationsPrecision::F32; } CreateGpuModelInfo create_info; create_info.precision = precision; create_info.storage_type = GetFastestStorageType(gpu_info); create_info.hints.Add(ModelHints::kAllowSpecialKernels); const DataType external_data_type = DeduceDataTypeFromPrecision(create_info.precision); const TensorStorageType external_storage_type = TensorStorageType::BUFFER; for (auto& value : graph.inputs()) { Layout layout = value->tensor.shape.b == 1 ? Layout::HWC : Layout::BHWC; create_info.external_mutable_tensors[value->id] = TensorDescriptor{external_data_type, external_storage_type, layout}; } for (auto& value : graph.outputs()) { Layout layout = value->tensor.shape.b == 1 ? Layout::HWC : Layout::BHWC; create_info.external_mutable_tensors[value->id] = TensorDescriptor{external_data_type, external_storage_type, layout}; } // TODO(impjdi): Merge logic with above. // Pre-allocate input and output metal buffers std::vector<::tflite::gpu::ValueId> input_ids; input_ids.reserve(inputs_.size()); std::map<::tflite::gpu::ValueId, BHWC> input_dimensions; graph_inputs_.reserve(inputs_.size()); for (const ValueId input : inputs_) { const auto& input_tensor = tensors_[input]; const auto tensor_id = input_tensor.tensor_id; input_ids.push_back(input); input_dimensions[input] = input_tensor.shape; graph_inputs_.push_back({ input, // .id tensor_id, // .tensor_id input_tensor.shape, // .shape false, // .set_externally }); // Create BHWC F32 buffer int bhwc_f32_length = static_cast(sizeof(float) * input_tensor.shape.DimensionsProduct()); in_out_bhwc_f32_buffers_[input] = [metal_device_ newBufferWithLength:bhwc_f32_length options:MTLResourceStorageModeShared]; // Create shared Metal spatial tensor with storage type BUFFER int bphwc4_length = static_cast(storage_type_size * GetElementsSizeForPHWC4(input_tensor.shape)); id bphwc4_buffer = [metal_device_ newBufferWithLength:bphwc4_length options:MTLResourceStorageModeShared]; MetalSpatialTensor metal_tensor; TensorDescriptor descriptor_with_shape = create_info.external_mutable_tensors[input]; descriptor_with_shape.SetBHWCShape(input_tensor.shape); RETURN_IF_ERROR( CreateTensorSharedBuffer(bphwc4_buffer, descriptor_with_shape, &metal_tensor)); in_out_tensors_[input] = std::make_unique(std::move(metal_tensor)); } std::vector<::tflite::gpu::ValueId> output_ids; output_ids.reserve(outputs_.size()); graph_outputs_.reserve(outputs_.size()); for (const ValueId output : outputs_) { const auto& output_tensor = tensors_[output]; const auto tensor_id = output_tensor.tensor_id; output_ids.push_back(output); graph_outputs_.push_back({ output, // .id tensor_id, // .tensor_id output_tensor.shape, // .shape false, // .set_externally }); // Create BHWC F32 buffer int bhwc_length = static_cast(sizeof(float) * output_tensor.shape.DimensionsProduct()); in_out_bhwc_f32_buffers_[output] = [metal_device_ newBufferWithLength:bhwc_length options:MTLResourceStorageModeShared]; // Create shared Metal spatial tensor with storage type BUFFER int bphwc4_length = static_cast(storage_type_size * GetElementsSizeForPHWC4(output_tensor.shape)); id bphwc4_buffer = [metal_device_ newBufferWithLength:bphwc4_length options:MTLResourceStorageModeShared]; MetalSpatialTensor metal_tensor; TensorDescriptor descriptor_with_shape = create_info.external_mutable_tensors[output]; descriptor_with_shape.SetBHWCShape(output_tensor.shape); RETURN_IF_ERROR( CreateTensorSharedBuffer(bphwc4_buffer, descriptor_with_shape, &metal_tensor)); in_out_tensors_[output] = std::make_unique(std::move(metal_tensor)); } // allocate converter bhwc->bphwc4 converter_to_BPHWC4_ = [[TFLBufferConvert alloc] initWithDevice:metal_device_ isFloat16:options_.allow_precision_loss convertToPBHWC4:true]; if (converter_to_BPHWC4_ == nil) { return absl::InternalError("Error initialization of input buffer converter"); } // allocate converter bphwc4->bhwc converter_from_BPHWC4_ = [[TFLBufferConvert alloc] initWithDevice:metal_device_ isFloat16:options_.allow_precision_loss convertToPBHWC4:false]; if (converter_from_BPHWC4_ == nil) { return absl::InternalError("Error initialization of output buffer converter"); } RETURN_IF_ERROR( inference_context_.InitFromGraphWithTransforms(create_info, &graph, metal_device_)); for (auto& external_tensor : in_out_tensors_) { RETURN_IF_ERROR( inference_context_.SetTensor(external_tensor.first, external_tensor.second.get())); } return absl::OkStatus(); } absl::Status Invoke(TfLiteContext* context) { if (options_.wait_type == TFLGpuDelegateWaitType::TFLGpuDelegateWaitTypeAggressive) gpu_alarm_clock_->Stop(); // We need only synchronization so volatile works better than atomic which reads from global // memory each time. __block volatile bool buffer_completed = false; id command_buffer = external_command_buffer_; if (external_command_buffer_ == nil) { command_buffer = [command_queue_ commandBuffer]; } const bool flush = external_command_buffer_ == nil && (options_.wait_type == TFLGpuDelegateWaitType::TFLGpuDelegateWaitTypeActive || options_.wait_type == TFLGpuDelegateWaitType::TFLGpuDelegateWaitTypeAggressive); const int flush_period = 8; const bool is_quantized_model = !quant_conversion_map_.empty(); if (is_quantized_model) { RETURN_IF_ERROR(DequantizeInputs(context, input_tensor_ids_, quant_conversion_map_)); } // CPU HWC input data conversion to PHWC4 and fill the GPU buffer for (const auto& input : graph_inputs_) { if (input.set_externally) { continue; } // A user provides data on CPU memory for this buffer - need to copy to MTLBuffer TfLiteTensor* tensor = &context->tensors[input.tensor_id]; void* gpu_ptr = [in_out_bhwc_f32_buffers_[input.id] contents]; std::memcpy(gpu_ptr, tensor->data.f, input.shape.DimensionsProduct() * sizeof(float)); id input_encoder = [command_buffer computeCommandEncoder]; [converter_to_BPHWC4_ convertWithEncoder:input_encoder shape:input.shape sourceBuffer:in_out_bhwc_f32_buffers_[input.id] convertedBuffer:in_out_tensors_[input.id]->GetBufferHandle()]; [input_encoder endEncoding]; } @autoreleasepool { if (flush) { [command_buffer commit]; inference_context_.EncodeWithCommandQueue(command_queue_, flush_period); command_buffer = [command_queue_ commandBuffer]; } else { inference_context_.EncodeWithCommandBuffer(command_buffer); } } for (const auto& output : graph_outputs_) { if (output.set_externally) { continue; } id output_encoder = [command_buffer computeCommandEncoder]; [converter_from_BPHWC4_ convertWithEncoder:output_encoder shape:output.shape sourceBuffer:in_out_tensors_[output.id]->GetBufferHandle() convertedBuffer:in_out_bhwc_f32_buffers_[output.id]]; [output_encoder endEncoding]; } if (external_command_buffer_ == nil) { if (options_.wait_type == TFLGpuDelegateWaitType::TFLGpuDelegateWaitTypeActive) { [command_buffer addCompletedHandler:^(id) { buffer_completed = true; }]; } [command_buffer commit]; if (options_.wait_type == TFLGpuDelegateWaitType::TFLGpuDelegateWaitTypeActive) { while (!buffer_completed) { // Busy wait. Use local variable. Volatile uses RAM access all the time. for (volatile int i = 0; i < 100; i++) { } } } else if (options_.wait_type == TFLGpuDelegateWaitType::TFLGpuDelegateWaitTypePassive) { // passive wait: this thread sleeps until GPU finishes. [command_buffer waitUntilCompleted]; } else if (options_.wait_type == TFLGpuDelegateWaitType::TFLGpuDelegateWaitTypeAggressive) { id signal_cb = [command_queue_ commandBuffer]; id signal_encoder = [signal_cb computeCommandEncoder]; [signal_encoder setComputePipelineState:signal_program_]; [signal_encoder setBuffer:signal_buffer_ offset:0 atIndex:0]; signal_value_++; [signal_encoder setBytes:&signal_value_ length:sizeof(int) atIndex:1]; [signal_encoder dispatchThreadgroups:MTLSizeMake(1, 1, 1) threadsPerThreadgroup:MTLSizeMake(1, 1, 1)]; [signal_encoder endEncoding]; [signal_cb commit]; gpu_alarm_clock_->Start(); const int* signal_ptr = reinterpret_cast([signal_buffer_ contents]); while (signal_ptr[0] != signal_value_) { // Busy wait. Spinning with local variable to avoid RAM pressure. for (volatile int i = 0; i < 100; i++) { } } } } else { // External command buffer must be set before every invoke call. external_command_buffer_ = nil; // External command buffer is assigned so all output buffers are controlled by a user. for (const auto& output : graph_outputs_) { if (!output.set_externally) { return absl::InternalError( "External command encoder is used, but not all output buffers are bound."); } } return absl::OkStatus(); } // Retrieve data from GPU and convert from PHWC4 to HWC. for (const auto& output : graph_outputs_) { if (output.set_externally) continue; // A user retrieves data on CPU memory for this buffer - need to copy from MTLBuffer. TfLiteTensor* tensor = context->tensors + output.tensor_id; const void* gpu_ptr = [in_out_bhwc_f32_buffers_[output.id] contents]; std::memcpy(tensor->data.f, gpu_ptr, output.shape.DimensionsProduct() * sizeof(float)); } if (is_quantized_model) { RETURN_IF_ERROR(QuantizeOutputs(context, output_tensor_ids_, quant_conversion_map_)); } return absl::OkStatus(); } const TFLGpuDelegateOptions options() const { return options_; } TfLiteDelegate* tflite_delegate() { return &delegate_; } private: TfLiteDelegate delegate_ = { reinterpret_cast(this), // .data_ DelegatePrepare, // .Prepare nullptr, // .CopyFromBufferHandle nullptr, // .CopyToBufferHandle nullptr, // .FreeBufferHandle kTfLiteDelegateFlagsNone, // .flags }; TFLGpuDelegateOptions options_; id metal_device_; std::vector tensors_; // indexed by ValueId std::vector inputs_; std::vector outputs_; std::vector input_tensor_ids_; std::vector output_tensor_ids_; // Whenever quantized inference is enabled, this maps the tensor index of each // originally quantized (8-bit) tensor to its float version added in // model_builder - and vice versa. absl::flat_hash_map quant_conversion_map_; InferenceContext inference_context_; // Metal bhwc f32 input and output buffers for better conversion performance from cpu tensors // We will memcpy cpu<->gpu and use metal for other conversions(layout changes, for example) std::map> in_out_bhwc_f32_buffers_; // input and output tensors can be set externally with help of // TFLGpuDelegateBindMetalBufferToTensor std::map> in_out_tensors_; TFLBufferConvert* converter_to_BPHWC4_ = nil; TFLBufferConvert* converter_from_BPHWC4_ = nil; struct BufferDescriptor { ValueId id; int64_t tensor_id; BHWC shape; bool set_externally; // a user fills/retrieves data on this MTLBuffer buffer }; std::vector graph_inputs_; std::vector graph_outputs_; id external_command_buffer_ = nil; id command_queue_; std::unique_ptr gpu_alarm_clock_; id signal_program_; id signal_buffer_; int signal_value_ = 0; }; Delegate* GetMetalDelegate(TfLiteNode* node) { return reinterpret_cast(node->user_data); } Delegate* GetMetalDelegate(TfLiteDelegate* delegate) { return reinterpret_cast(delegate->data_); } TfLiteStatus DelegatePrepare(TfLiteContext* context, TfLiteDelegate* delegate) { const TfLiteRegistration kRegistration = { // .init [](TfLiteContext* context, const char* buffer, size_t) -> void* { const auto* params = reinterpret_cast(buffer); auto* metal_delegate = GetMetalDelegate(params->delegate); // Everything below should happen in prepare function call, but TFLite for whatever reason // forbids that. const auto status = metal_delegate->Prepare(context, params); if (status.ok()) return metal_delegate; TF_LITE_KERNEL_LOG(context, "TfLiteMetalDelegate Prepare: %s", std::string(status.message()).c_str()); return nullptr; }, // .free [](TfLiteContext*, void* buffer) -> void {}, // .prepare [](TfLiteContext* context, TfLiteNode* node) -> TfLiteStatus { if (!node->user_data) { return kTfLiteError; } auto* gpu_delegate_kernel = GetMetalDelegate(node); const auto status = gpu_delegate_kernel->GetRequiredTemporaries(context, node, &node->temporaries); if (!status.ok()) { TF_LITE_KERNEL_LOG(context, "TfLiteMetalDelegate Prepare: %s", std::string(status.message()).c_str()); return kTfLiteError; } return node->user_data ? kTfLiteOk : kTfLiteError; }, // .invoke [](TfLiteContext* context, TfLiteNode* node) -> TfLiteStatus { const auto status = GetMetalDelegate(node)->Invoke(context); if (status.ok()) return kTfLiteOk; TF_LITE_KERNEL_LOG(context, "TfLiteMetalDelegate Invoke: %s", std::string(status.message()).c_str()); return kTfLiteError; }, nullptr, // .profiling_string 0, // .builtin_code "TfLiteMetalDelegate", // .custom_name 1, // .version }; TfLiteIntArray* ops_to_replace = GetOpsToReplace(context, GetMetalDelegate(delegate)->options().enable_quantization); const auto status = context->ReplaceNodeSubsetsWithDelegateKernels(context, kRegistration, ops_to_replace, delegate); TfLiteIntArrayFree(ops_to_replace); return status; } } // namespace } // namespace metal } // namespace gpu } // namespace tflite TfLiteDelegate* TFLGpuDelegateCreate(const TFLGpuDelegateOptions* options) { TFLITE_LOG_PROD_ONCE(tflite::TFLITE_LOG_INFO, "Created TensorFlow Lite delegate for Metal."); auto* metal_delegate = new ::tflite::gpu::metal::Delegate(options); return metal_delegate ? metal_delegate->tflite_delegate() : nullptr; } void TFLGpuDelegateDelete(TfLiteDelegate* delegate) { delete ::tflite::gpu::metal::GetMetalDelegate(delegate); } bool TFLGpuDelegateBindMetalBufferToTensor(TfLiteDelegate* delegate, int tensor_index, id buffer) { auto* metal_delegate = ::tflite::gpu::metal::GetMetalDelegate(delegate); return metal_delegate && metal_delegate->BindBufferToTensor(buffer, tensor_index).ok(); } // Note: This function is not exposed in `metal_delegate.h`, but it's exposed in // `metal_delegate_internal.h`. bool TFLGpuDelegateSetCommandBuffer(TfLiteDelegate* delegate, id command_buffer) { auto* metal_delegate = ::tflite::gpu::metal::GetMetalDelegate(delegate); if (!metal_delegate) return false; metal_delegate->SetCommandBuffer(command_buffer); return true; } TFLGpuDelegateOptions TFLGpuDelegateOptionsDefault() { TFLGpuDelegateOptions options = { .allow_precision_loss = false, .wait_type = TFLGpuDelegateWaitType::TFLGpuDelegateWaitTypePassive, .enable_quantization = true, }; return options; }