1// 2// ETCoreMLModelProfiler.mm 3// 4// Copyright © 2024 Apple Inc. All rights reserved. 5// 6// Please refer to the license found in the LICENSE file in the root directory of the source tree. 7 8#import "ETCoreMLModelProfiler.h" 9 10#import "ETCoreMLAsset.h" 11#import "ETCoreMLModel.h" 12#import "ETCoreMLLogging.h" 13#import "ETCoreMLModelStructurePath.h" 14#import "ETCoreMLOperationProfilingInfo.h" 15#import "ETCoreMLPair.h" 16#import "ETCoreMLStrings.h" 17#import <mach/mach_time.h> 18#import <math.h> 19#import "program_path.h" 20 21namespace { 22using namespace executorchcoreml::modelstructure; 23 24#if MODEL_PROFILING_IS_AVAILABLE 25 26API_AVAILABLE(macos(14.4), ios(17.4), tvos(17.4), watchos(10.4)) 27MLComputePlan *_Nullable get_compute_plan_of_model_at_url(NSURL *model_url, 28 MLModelConfiguration *configuration, 29 NSError* __autoreleasing *error) { 30 __block NSError *local_error = nil; 31 __block MLComputePlan *result = nil; 32 dispatch_semaphore_t sema = dispatch_semaphore_create(0); 33 [MLComputePlan loadContentsOfURL:model_url configuration:configuration completionHandler:^(MLComputePlan * _Nullable compute_plan, 34 NSError * _Nullable compute_plan_error) { 35 result = compute_plan; 36 local_error = compute_plan_error; 37 dispatch_semaphore_signal(sema); 38 }]; 39 40 long status = dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * 60 * NSEC_PER_SEC))); 41 if (status != 0) { 42 ETCoreMLLogUnderlyingErrorAndSetNSError(error, 43 ETCoreMLErrorCompilationFailed, 44 local_error, 45 "%@: Failed to get compute plan of model with name=%@.", 46 NSStringFromClass(ETCoreMLModelProfiler.class), 47 model_url.lastPathComponent); 48 return nil; 49 } 50 51 return result; 52} 53 54API_AVAILABLE(macos(14.4), ios(17.4), tvos(17.4), watchos(10.4)) 55void visit_program_operation(MLModelStructureProgramBlock *block, 56 const Path& block_path, 57 BOOL (^handler)(MLModelStructureProgramOperation *operation, ETCoreMLModelStructurePath *path)) { 58 for (MLModelStructureProgramOperation *operation in block.operations) { 59 Path operation_path = block_path; 60 operation_path.append_component(Path::Program::Operation(operation.outputs.firstObject.name.UTF8String)); 61 if (!handler(operation, [[ETCoreMLModelStructurePath alloc] initWithUnderlyingValue:operation_path])) { 62 return; 63 } 64 65 for (size_t i = 0; i < operation.blocks.count; ++i) { 66 Path nested_block_path = operation_path; 67 nested_block_path.append_component(Path::Program::Block(i)); 68 visit_program_operation(operation.blocks[i], nested_block_path,handler); 69 } 70 } 71} 72 73API_AVAILABLE(macos(14.4), ios(17.4), tvos(17.4), watchos(10.4)) 74void visit_program_operation(MLModelStructure *modelStructure, BOOL (^handler)(MLModelStructureProgramOperation *operation, ETCoreMLModelStructurePath *path)) { 75 using namespace executorchcoreml::modelstructure; 76 [modelStructure.program.functions enumerateKeysAndObjectsUsingBlock:^(NSString *function_name, 77 MLModelStructureProgramFunction *function, 78 BOOL * _Nonnull __unused stop) { 79 Path path; 80 path.append_component(Path::Program()); 81 path.append_component(Path::Program::Function(function_name.UTF8String)); 82 path.append_component(Path::Program::Block(-1)); 83 visit_program_operation(function.block, path, handler); 84 }]; 85} 86 87API_AVAILABLE(macos(14.4), ios(17.4), tvos(17.4), watchos(10.4)) 88ETCoreMLComputeUnits to_compute_unit(id<MLComputeDeviceProtocol> compute_device) { 89 if ([compute_device isKindOfClass:MLCPUComputeDevice.class]) { 90 return ETCoreMLComputeUnitCPU; 91 } else if ([compute_device isKindOfClass:MLGPUComputeDevice.class]) { 92 return ETCoreMLComputeUnitGPU; 93 } else if ([compute_device isKindOfClass:MLNeuralEngineComputeDevice.class]) { 94 return ETCoreMLComputeUnitNeuralEngine; 95 } else { 96 return ETCoreMLComputeUnitUnknown; 97 } 98} 99 100API_AVAILABLE(macos(14.4), ios(17.4), tvos(17.4), watchos(10.4)) 101ETCoreMLComputeUnits to_compute_units(NSArray<id<MLComputeDeviceProtocol>> *compute_devices) { 102 ETCoreMLComputeUnits units = ETCoreMLComputeUnitUnknown; 103 for (id<MLComputeDeviceProtocol> compute_device in compute_devices) { 104 units |= to_compute_unit(compute_device); 105 } 106 107 return units; 108} 109 110API_AVAILABLE(macos(14.4), ios(17.4), tvos(17.4), watchos(10.4)) 111ETCoreMLOperationProfilingInfo *get_profiling_info(MLComputePlanDeviceUsage *device_usage, 112 MLModelStructureProgramOperation *operation, 113 uint64_t op_execution_start_time, 114 uint64_t op_execution_end_time, 115 double estimatedCost) { 116 NSMutableArray<NSString *> *outputNames = [[NSMutableArray alloc] initWithCapacity:operation.outputs.count]; 117 for (MLModelStructureProgramNamedValueType *output in operation.outputs) { 118 [outputNames addObject:output.name]; 119 } 120 121 ETCoreMLComputeUnits preferred_compute_unit = to_compute_unit(device_usage.preferredComputeDevice); 122 ETCoreMLComputeUnits supported_compute_units = to_compute_units(device_usage.supportedComputeDevices); 123 ETCoreMLOperationProfilingInfo *info = [[ETCoreMLOperationProfilingInfo alloc] initWithPreferredComputeUnit:preferred_compute_unit 124 supportedComputeUnits:supported_compute_units 125 estimatedExecutionStartTime:op_execution_start_time 126 estimatedExecutionEndTime:op_execution_end_time 127 estimatedCost:estimatedCost 128 outputNames:outputNames 129 operatorName:operation.operatorName]; 130 return info; 131} 132 133API_AVAILABLE(macos(14.4), ios(17.4), tvos(17.4), watchos(10.4)) 134NSDictionary<ETCoreMLModelStructurePath *, ETCoreMLOperationProfilingInfo *> * 135prepare_profiling_infos(NSArray<MLModelStructureProgramOperation *> *operations, 136 NSDictionary<NSValue *, ETCoreMLModelStructurePath *> *operation_to_path_map, 137 MLComputePlan *compute_plan) { 138 NSMutableDictionary<ETCoreMLModelStructurePath *, ETCoreMLOperationProfilingInfo *> *result = [NSMutableDictionary dictionaryWithCapacity:operation_to_path_map.count]; 139 for (MLModelStructureProgramOperation *operation in operations) { 140 MLComputePlanCost *estimated_cost = [compute_plan estimatedCostOfMLProgramOperation:operation]; 141 if (!estimated_cost || std::isnan(estimated_cost.weight)) { 142 continue; 143 } 144 145 NSValue *key = [NSValue valueWithPointer:(const void*)operation]; 146 ETCoreMLModelStructurePath *path = operation_to_path_map[key]; 147 MLComputePlanDeviceUsage *device_usage = [compute_plan computeDeviceUsageForMLProgramOperation:operation]; 148 if (path && device_usage) { 149 ETCoreMLOperationProfilingInfo *profiling_info = get_profiling_info(device_usage, 150 operation, 151 0, 152 0, 153 estimated_cost.weight); 154 result[path] = profiling_info; 155 } 156 } 157 158 return result; 159} 160 161API_AVAILABLE(macos(14.4), ios(17.4), tvos(17.4), watchos(10.4)) 162NSDictionary<ETCoreMLModelStructurePath *, ETCoreMLOperationProfilingInfo *> * 163get_profiling_infos_for_paths(NSSet<ETCoreMLModelStructurePath *> *paths, 164 NSArray<MLModelStructureProgramOperation *> *topologically_sorted_operations, 165 NSDictionary<NSValue *, ETCoreMLModelStructurePath *> *operation_to_path_map, 166 NSDictionary<ETCoreMLModelStructurePath *, ETCoreMLOperationProfilingInfo *> *profiling_infos, 167 uint64_t model_execution_start_time, 168 uint64_t model_execution_end_time) { 169 NSMutableDictionary<ETCoreMLModelStructurePath *, ETCoreMLOperationProfilingInfo *> *result = [NSMutableDictionary dictionaryWithCapacity:paths.count]; 170 uint64_t op_execution_start_time = model_execution_start_time; 171 uint64_t op_execution_end_time = model_execution_start_time; 172 // `model_execution_end_time` >= `model_execution_start_time`. 173 uint64_t model_execution_duration = model_execution_end_time - model_execution_start_time; 174 for (MLModelStructureProgramOperation *operation in topologically_sorted_operations) { 175 NSValue *key = [NSValue valueWithPointer:(const void*)operation]; 176 ETCoreMLModelStructurePath *path = operation_to_path_map[key]; 177 ETCoreMLOperationProfilingInfo *profiling_info = profiling_infos[path]; 178 if (!profiling_info) { 179 continue; 180 } 181 182 op_execution_end_time = op_execution_start_time + static_cast<uint64_t>(static_cast<double>(model_execution_duration) * profiling_info.estimatedCost); 183 if (path && [paths containsObject:path]) { 184 ETCoreMLOperationProfilingInfo *profiling_info_new = [[ETCoreMLOperationProfilingInfo alloc] initWithPreferredComputeUnit:profiling_info.preferredComputeUnit 185 supportedComputeUnits:profiling_info.supportedComputeUnits 186 estimatedExecutionStartTime:op_execution_start_time 187 estimatedExecutionEndTime:op_execution_end_time 188 estimatedCost:profiling_info.estimatedCost 189 outputNames:profiling_info.outputNames 190 operatorName:profiling_info.operatorName 191 metadata:profiling_info.metadata]; 192 result[path] = profiling_info_new; 193 } 194 op_execution_start_time = op_execution_end_time; 195 } 196 197 return result; 198} 199 200API_AVAILABLE(macos(14.4), ios(17.4), tvos(17.4), watchos(10.4)) 201BOOL is_const_operation(MLModelStructureProgramOperation *operation) { 202 return [operation.operatorName isEqualToString:@"const"]; 203} 204 205void set_model_outputs(id<MLFeatureProvider> output_features, 206 NSOrderedSet<NSString *> *output_names, 207 NSArray<MLMultiArray *> *_Nullable __autoreleasing *_Nonnull model_outputs) { 208 NSMutableArray<MLMultiArray *> *values = [NSMutableArray arrayWithCapacity:output_names.count]; 209 for (NSString *output_name in output_names) { 210 MLFeatureValue *feature_value = [output_features featureValueForName:output_name]; 211 NSCAssert(feature_value.multiArrayValue != nil, @"%@: Expected a multiarray value for output name=%@.", 212 NSStringFromClass(ETCoreMLModelProfiler.class), 213 output_name); 214 [values addObject:feature_value.multiArrayValue]; 215 } 216 217 *model_outputs = values; 218} 219 220#endif 221 222} 223 224@interface ETCoreMLModelProfiler () 225/// The model. 226@property (readonly, strong, nonatomic) ETCoreMLModel *model; 227/// The model output names. 228@property (readonly, copy, nonatomic) NSOrderedSet<NSString *> *outputNames; 229#if MODEL_PROFILING_IS_AVAILABLE 230/// The compute plan. 231@property (readonly, strong, nonatomic) MLComputePlan *computePlan API_AVAILABLE(macos(14.4), ios(17.4), tvos(17.4), watchos(10.4)); 232/// The topologically sorted operations. 233@property (readonly, copy, nonatomic) NSArray<MLModelStructureProgramOperation *> *topologicallySortedOperations API_AVAILABLE(macos(14.4), ios(17.4), tvos(17.4), watchos(10.4)); 234#endif 235/// The mapping from operation to it's path in the model structure. 236@property (readonly, strong, nonatomic) NSDictionary<NSValue *, ETCoreMLModelStructurePath *> *operationToPathMap; 237/// The profiling infos for all the operations. 238@property (readonly, copy, nonatomic) NSDictionary<ETCoreMLModelStructurePath *, ETCoreMLOperationProfilingInfo *> *profilingInfos; 239 240@end 241 242@implementation ETCoreMLModelProfiler 243 244- (nullable instancetype)initWithModel:(ETCoreMLModel *)model 245 configuration:(MLModelConfiguration *)configuration 246 error:(NSError * __autoreleasing *)error { 247#if MODEL_PROFILING_IS_AVAILABLE 248 if (@available(macOS 14.4, iOS 17.4, tvOS 17.4, watchOS 10.4, *)) { 249 NSURL *compiledModelURL = model.asset.contentURL; 250 MLComputePlan *computePlan = get_compute_plan_of_model_at_url(compiledModelURL, 251 configuration, 252 error); 253 if (!computePlan) { 254 return nil; 255 } 256 257 __block NSMutableArray<ETCoreMLModelStructurePath *> *operationPaths = [NSMutableArray array]; 258 __block NSMutableDictionary<NSValue *, ETCoreMLModelStructurePath *> *operationToPathMap = [NSMutableDictionary dictionary]; 259 __block NSMutableArray<MLModelStructureProgramOperation *> *topologicallySortedOperations = [NSMutableArray new]; 260 visit_program_operation(computePlan.modelStructure, ^BOOL(MLModelStructureProgramOperation *operation, ETCoreMLModelStructurePath *operationPath) { 261 if (is_const_operation(operation)) { 262 return YES; 263 } 264 265 [topologicallySortedOperations addObject:operation]; 266 NSValue *key = [NSValue valueWithPointer:(const void*)operation]; 267 operationToPathMap[key] = operationPath; 268 [operationPaths addObject:operationPath]; 269 return YES; 270 }); 271 272 NSDictionary<ETCoreMLModelStructurePath *, ETCoreMLOperationProfilingInfo *> *profilingInfos = prepare_profiling_infos(topologicallySortedOperations, 273 operationToPathMap, 274 computePlan); 275 276 self = [super init]; 277 if (self) { 278 _model = model; 279 _computePlan = computePlan; 280 _operationToPathMap = operationToPathMap; 281 _topologicallySortedOperations = topologicallySortedOperations; 282 _operationPaths = operationPaths; 283 _profilingInfos = profilingInfos; 284 } 285 286 return self; 287 } 288#endif 289 ETCoreMLLogErrorAndSetNSError(error, 290 ETCoreMLErrorModelProfilingNotSupported, 291 "%@: Model profiling is only available for macOS >= 14.4, iOS >= 17.4, tvOS >= 17.4 and watchOS >= 10.4.", 292 NSStringFromClass(self.class)); 293 return nil; 294} 295 296- (nullable ETCoreMLModelProfilingResult *)profilingInfoForOperationsAtPaths:(NSArray<ETCoreMLModelStructurePath *> *)paths 297 options:(MLPredictionOptions *)options 298 inputs:(id<MLFeatureProvider>)inputs 299 modelOutputs:(NSArray<MLMultiArray *> *_Nullable __autoreleasing *_Nonnull)modelOutputs 300 error:(NSError* __autoreleasing *)error { 301#if MODEL_PROFILING_IS_AVAILABLE 302 uint64_t modelExecutionStartTime = mach_absolute_time(); 303 id<MLFeatureProvider> outputFeatures = [self.model predictionFromFeatures:inputs options:options error:error]; 304 uint64_t modelExecutionEndTime = mach_absolute_time(); 305 if (!modelOutputs) { 306 return nil; 307 } 308 309 if (@available(macOS 14.4, iOS 17.4, tvOS 17.4, watchOS 10.4, *)) { 310 ETCoreMLModelProfilingResult *profilingInfos = get_profiling_infos_for_paths([NSSet setWithArray:paths], 311 self.topologicallySortedOperations, 312 self.operationToPathMap, 313 self.profilingInfos, 314 modelExecutionStartTime, 315 modelExecutionEndTime); 316 317 318 319 if (outputFeatures) { 320 set_model_outputs(outputFeatures, self.outputNames, modelOutputs); 321 } 322 323 return profilingInfos; 324 } 325#endif 326 return nil; 327} 328 329- (nullable ETCoreMLModelProfilingResult *)profilingInfoForOperationsAtPaths:(MLPredictionOptions *)options 330 inputs:(id<MLFeatureProvider>)inputs 331 modelOutputs:(NSArray<MLMultiArray *> *_Nullable __autoreleasing *_Nonnull)modelOutputs 332 error:(NSError* __autoreleasing *)error { 333#if MODEL_PROFILING_IS_AVAILABLE 334 if (@available(macOS 14.4, iOS 17.4, tvOS 17.4, watchOS 10.4, *)) { 335 __block NSMutableArray<ETCoreMLModelStructurePath *> *paths = [NSMutableArray array]; 336 visit_program_operation(self.computePlan.modelStructure, ^BOOL(MLModelStructureProgramOperation *operation, ETCoreMLModelStructurePath *path) { 337 if (!is_const_operation(operation)) { 338 [paths addObject:path]; 339 } 340 return YES; 341 }); 342 343 return [self profilingInfoForOperationsAtPaths:paths 344 options:options 345 inputs:inputs 346 modelOutputs:modelOutputs 347 error:error]; 348 } 349#endif 350 return nil; 351} 352 353@end 354