diff --git a/Package.swift b/Package.swift index acaa49bd652..d6f6c32cee8 100644 --- a/Package.swift +++ b/Package.swift @@ -48,6 +48,7 @@ let products = deliverables([ "frameworks": [ "Accelerate", "CoreML", + "CoreVideo", ], "libraries": [ "sqlite3", diff --git a/backends/apple/coreml/CMakeLists.txt b/backends/apple/coreml/CMakeLists.txt index a362658564a..2b557ffba36 100644 --- a/backends/apple/coreml/CMakeLists.txt +++ b/backends/apple/coreml/CMakeLists.txt @@ -9,6 +9,7 @@ if(APPLE) find_library(FOUNDATION_FRAMEWORK Foundation REQUIRED) find_library(ACCELERATE_FRAMEWORK Accelerate REQUIRED) find_library(COREML_FRAMEWORK CoreML REQUIRED) + find_library(COREVIDEO_FRAMEWORK CoreVideo REQUIRED) find_library(SQLITE_LIBRARY sqlite3 REQUIRED) endif() @@ -257,8 +258,9 @@ if(APPLE) endif() target_link_libraries( - coremldelegate PRIVATE ${ACCELERATE_FRAMEWORK} ${COREML_FRAMEWORK} - ${FOUNDATION_FRAMEWORK} ${SQLITE_LIBRARY} + coremldelegate + PRIVATE ${ACCELERATE_FRAMEWORK} ${COREML_FRAMEWORK} ${COREVIDEO_FRAMEWORK} + ${FOUNDATION_FRAMEWORK} ${SQLITE_LIBRARY} ) if(EXECUTORCH_BUILD_SHARED) set_target_properties( diff --git a/backends/apple/coreml/runtime/delegate/ETCoreMLModel.h b/backends/apple/coreml/runtime/delegate/ETCoreMLModel.h index e72f2369d23..735928b2dd6 100644 --- a/backends/apple/coreml/runtime/delegate/ETCoreMLModel.h +++ b/backends/apple/coreml/runtime/delegate/ETCoreMLModel.h @@ -6,6 +6,7 @@ // Please refer to the license found in the LICENSE file in the root directory of the source tree. #import +#import #import NS_ASSUME_NONNULL_BEGIN @@ -83,6 +84,24 @@ __attribute__((objc_subclassing_restricted)) - (BOOL)prewarmAndReturnError:(NSError* __autoreleasing*)error; +/// Binds an IOSurface-backed `kCVPixelFormatType_OneComponent16Half` pixel buffer to this model, so that an input or +/// output whose data pointer is the buffer's base address is passed to Core ML as the pixel buffer: Core ML reads or +/// writes it in place at the buffer's row stride instead of copying it. Must be called before the first prediction. +/// +/// @param pixelBuffer The pixel buffer, retained for the model's lifetime. +/// @param error On failure, error is filled with the failure information. +/// @retval `YES` if the buffer was bound, `NO` if it is not an IOSurface-backed fp16 buffer, the OS is older than +/// macOS 12 / iOS 16, locking fails, or the buffer has no base address. +- (BOOL)bindPixelBuffer:(CVPixelBufferRef)pixelBuffer error:(NSError* __autoreleasing*)error; + +/// Sets `multiArray` to an array backed by the pixel buffer bound at `arg`'s data pointer, or to `nil` if no buffer is +/// bound there. While `arg`'s shape stays the same, every call returns the same array. +/// +/// @retval `NO` if the bound pixel buffer doesn't have `arg`'s shape and data type. +- (BOOL)getPixelBufferMultiArray:(MLMultiArray* _Nullable __autoreleasing* _Nonnull)multiArray + forArg:(const executorchcoreml::MultiArray&)arg + error:(NSError* __autoreleasing*)error; + @end NS_ASSUME_NONNULL_END diff --git a/backends/apple/coreml/runtime/delegate/ETCoreMLModel.mm b/backends/apple/coreml/runtime/delegate/ETCoreMLModel.mm index 206714768b1..994c3b9de1c 100644 --- a/backends/apple/coreml/runtime/delegate/ETCoreMLModel.mm +++ b/backends/apple/coreml/runtime/delegate/ETCoreMLModel.mm @@ -172,8 +172,20 @@ void reset_state_for_feature_name(NSString *feature_name, MLState *state) { } +namespace { +/// A pixel buffer bound to the model, with the array Core ML reads or writes it through. +struct BoundPixelBuffer { + void *baseAddress = nullptr; + id pixelBuffer = nil; // CVPixelBufferRef + MLMultiArray *multiArray = nil; + std::vector multiArrayShape; +}; +} // namespace + #pragma mark - ETCoreMLModel -@interface ETCoreMLModel () +@interface ETCoreMLModel () { + std::vector _boundPixelBuffers; +} @property (strong, readonly, nonatomic) NSCache *cache; @property (copy, readonly, nonatomic) NSDictionary *inputConstraintsByName; @@ -318,8 +330,24 @@ - (nullable instancetype)initWithCompiledModelURL:(NSURL *)compiledModelURL const auto& layout = arg.layout(); auto dataType = to_ml_multiarray_data_type(layout.dataType()); + MLMultiArray *pixelBufferArray = nil; + if (![self getPixelBufferMultiArray:&pixelBufferArray forArg:arg error:error]) { + return nil; + } + MLMultiArray *multiArrayArg = nil; - if (dataType == constraint.dataType) { + if (pixelBufferArray) { + if (pixelBufferArray.dataType != constraint.dataType) { + ETCoreMLLogErrorAndSetNSError(error, + ETCoreMLErrorInternalError, + "The pixel buffer bound to arg '%@' does not match the model feature's data type.", + argName); + return nil; + } + multiArrayArg = pixelBufferArray; + // Core ML reads and writes the pixel buffer at its own row stride, not the arg's strides. + lCopyData = NO; + } else if (dataType == constraint.dataType) { // We can use the same data storage. multiArrayArg = [[MLMultiArray alloc] initWithDataPointer:arg.data() shape:to_array(layout.shape()) @@ -430,4 +458,83 @@ - (BOOL)prewarmAndReturnError:(NSError* __autoreleasing*)error { return result; } +- (BOOL)bindPixelBuffer:(CVPixelBufferRef)pixelBuffer error:(NSError * __autoreleasing *)error { + BOOL bindable = NO; + if (@available(macOS 12.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)) { + bindable = CVPixelBufferGetIOSurface(pixelBuffer) != NULL && + CVPixelBufferGetPixelFormatType(pixelBuffer) == kCVPixelFormatType_OneComponent16Half; + } + if (!bindable) { + ETCoreMLLogErrorAndSetNSError(error, + ETCoreMLErrorInternalError, + "Only an IOSurface-backed fp16 pixel buffer can be bound to a model, on macOS 12 / iOS 16 or later."); + return NO; + } + + const CVReturn lockStatus = CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); + if (lockStatus != kCVReturnSuccess) { + ETCoreMLLogErrorAndSetNSError(error, + ETCoreMLErrorInternalError, + "Failed to lock the pixel buffer for binding (status=%d).", + static_cast(lockStatus)); + return NO; + } + void *baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer); + CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); + if (baseAddress == nullptr) { + ETCoreMLLogErrorAndSetNSError(error, + ETCoreMLErrorInternalError, + "The pixel buffer has no base address and cannot be bound."); + return NO; + } + + BoundPixelBuffer bound; + bound.baseAddress = baseAddress; + bound.pixelBuffer = (__bridge id)pixelBuffer; + for (auto& existing : _boundPixelBuffers) { + if (existing.baseAddress == bound.baseAddress) { + existing = std::move(bound); + return YES; + } + } + _boundPixelBuffers.push_back(std::move(bound)); + return YES; +} + +- (BOOL)getPixelBufferMultiArray:(MLMultiArray * _Nullable __autoreleasing *)multiArray + forArg:(const executorchcoreml::MultiArray&)arg + error:(NSError * __autoreleasing *)error { + *multiArray = nil; + for (auto& bound : _boundPixelBuffers) { + if (bound.baseAddress != arg.data()) { + continue; + } + + // Wrapping the buffer costs Core ML a setup per array, so the array is made once per shape. + const auto& shape = arg.layout().shape(); + if (bound.multiArray == nil || bound.multiArrayShape != shape) { + bound.multiArray = nil; + bound.multiArrayShape = shape; + CVPixelBufferRef buffer = (__bridge CVPixelBufferRef)bound.pixelBuffer; + if (@available(macOS 12.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)) { + if (!shape.empty() && shape.back() == CVPixelBufferGetWidth(buffer) && + std::accumulate(shape.cbegin(), shape.cend() - 1, size_t(1), std::multiplies{}) == CVPixelBufferGetHeight(buffer)) { + bound.multiArray = [[MLMultiArray alloc] initWithPixelBuffer:buffer shape:to_array(shape)]; + } + } + } + if (bound.multiArray == nil || arg.layout().dataType() != MultiArray::DataType::Float16) { + ETCoreMLLogErrorAndSetNSError(error, + ETCoreMLErrorInternalError, + "The pixel buffer bound at an arg's data pointer doesn't match the arg's shape and data type."); + return NO; + } + + *multiArray = bound.multiArray; + return YES; + } + + return YES; +} + @end diff --git a/backends/apple/coreml/runtime/delegate/ETCoreMLModelManager.mm b/backends/apple/coreml/runtime/delegate/ETCoreMLModelManager.mm index 6eb848ec290..7a09f9938f6 100644 --- a/backends/apple/coreml/runtime/delegate/ETCoreMLModelManager.mm +++ b/backends/apple/coreml/runtime/delegate/ETCoreMLModelManager.mm @@ -154,7 +154,24 @@ void set_outputs(NSArray *outputs, NSArray *mode } } -void copy(MLMultiArray *src, executorchcoreml::MultiArray& dst) { +BOOL copy(ETCoreMLModel *model, + MLMultiArray *src, + executorchcoreml::MultiArray& dst, + NSError * __autoreleasing *error) { + // When Core ML didn't write into a bound pixel buffer itself, the copy has to follow the + // buffer's row stride rather than dst's strides. + MLMultiArray *pixel_buffer_dst = nil; + if (![model getPixelBufferMultiArray:&pixel_buffer_dst forArg:dst error:error]) { + return NO; + } + if (pixel_buffer_dst) { + // Core ML returns the output backing itself when it wrote into it. + if (src != pixel_buffer_dst) { + ::copy(src, pixel_buffer_dst); + } + return YES; + } + void (^copy_data)(void *) = ^(void *bytes) { if (bytes == dst.data()) { return; @@ -174,15 +191,23 @@ void copy(MLMultiArray *src, executorchcoreml::MultiArray& dst) { } else { copy_data(src.dataPointer); } + + return YES; } -void set_outputs(std::vector& outputs, - NSArray *model_outputs) { +BOOL set_outputs(ETCoreMLModel *model, + std::vector& outputs, + NSArray *model_outputs, + NSError * __autoreleasing *error) { NSEnumerator *enumerator = [model_outputs objectEnumerator]; for (auto& output : outputs) { MLMultiArray *model_output = [enumerator nextObject]; - ::copy(model_output, output); + if (!::copy(model, model_output, output, error)) { + return NO; + } } + + return YES; } NSData * _Nullable get_file_data(const inmemoryfs::InMemoryFileSystem *inMemoryFS, @@ -1176,8 +1201,7 @@ - (BOOL)executeModelWithHandle:(ModelHandle *)handle outputArgs[i].resize(new_size); argsVec[model.orderedInputNames.count + i].resize(new_size); } - ::set_outputs(outputArgs, modelOutputs); - result = YES; + result = ::set_outputs(model, outputArgs, modelOutputs, &localError); } } } diff --git a/backends/apple/coreml/runtime/test/pixel_buffer_binding_test.mm b/backends/apple/coreml/runtime/test/pixel_buffer_binding_test.mm new file mode 100644 index 00000000000..0bcda325e53 --- /dev/null +++ b/backends/apple/coreml/runtime/test/pixel_buffer_binding_test.mm @@ -0,0 +1,444 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using executorchcoreml::BackendDelegate; +using executorchcoreml::Buffer; +using executorchcoreml::ModelLoggingOptions; +using executorchcoreml::MultiArray; + +@interface ETCoreMLModelManager (PixelBufferBindingTest) +- (id)executorWithHandle:(ModelHandle *)handle; +@end + +namespace { + +constexpr size_t kHeight = 8; +constexpr size_t kWidth = 16; +constexpr std::array kShape = {1, 1, kHeight, kWidth}; + +struct PixelBufferDeleter { + void operator()(CVPixelBufferRef pixelBuffer) const { CVPixelBufferRelease(pixelBuffer); } +}; +using PixelBufferPtr = std::unique_ptr, PixelBufferDeleter>; + +PixelBufferPtr makePixelBuffer(OSType format, bool ioSurfaceBacked = true) { + NSDictionary *attributes = ioSurfaceBacked ? @{(id)kCVPixelBufferIOSurfacePropertiesKey: @{}} : @{}; + CVPixelBufferRef pixelBuffer = nullptr; + CVPixelBufferCreate(kCFAllocatorDefault, kWidth, kHeight, format, (__bridge CFDictionaryRef)attributes, &pixelBuffer); + return PixelBufferPtr(pixelBuffer); +} + +void *baseAddress(CVPixelBufferRef pixelBuffer) { + CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); + void *address = CVPixelBufferGetBaseAddress(pixelBuffer); + CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); + return address; +} + +_Float16 *row(CVPixelBufferRef pixelBuffer, size_t y) { + auto *base = static_cast(CVPixelBufferGetBaseAddress(pixelBuffer)); + return reinterpret_cast<_Float16 *>(base + y * CVPixelBufferGetBytesPerRow(pixelBuffer)); +} + +MultiArray makeArg(void *data, std::vector shape, MultiArray::DataType dataType = MultiArray::DataType::Float16) { + std::vector strides(shape.size()); + ssize_t stride = 1; + for (size_t i = shape.size(); i > 0; --i) { + strides[i - 1] = stride; + stride *= shape[i - 1]; + } + return MultiArray(data, MultiArray::MemoryLayout(dataType, std::move(shape), std::move(strides))); +} + +template +MultiArray makeArg(void *data, const std::array& shape, MultiArray::DataType dataType = MultiArray::DataType::Float16) { + return makeArg(data, std::vector(shape.begin(), shape.end()), dataType); +} + +float inputValue(size_t x, size_t y, int frame) { + return float((y * kWidth + x + frame) % 64) / 8; +} + +std::string toString(NSString *string) { + const char *utf8 = string.UTF8String; + return utf8 != nullptr ? utf8 : ""; +} + +/// Returns an ML program that computes `z = x + x` for an fp16 or fp32 `x` of shape `kShape`. +std::string makeModelSpec(bool useFloat16 = true) { + namespace spec = CoreML::Specification; + namespace mil = CoreML::Specification::MILSpec; + auto setTensorType = [useFloat16](mil::ValueType *type) { + auto *tensorType = type->mutable_tensortype(); + tensorType->set_datatype(useFloat16 ? mil::FLOAT16 : mil::FLOAT32); + tensorType->set_rank(kShape.size()); + for (size_t size : kShape) { + tensorType->add_dimensions()->mutable_constant()->set_size(size); + } + }; + auto setFeature = [useFloat16](spec::FeatureDescription *feature, const char *name) { + feature->set_name(name); + auto *arrayType = feature->mutable_type()->mutable_multiarraytype(); + arrayType->set_datatype(useFloat16 ? spec::ArrayFeatureType::FLOAT16 : spec::ArrayFeatureType::FLOAT32); + for (size_t size : kShape) { + arrayType->add_shape(size); + } + }; + + spec::Model model; + // Core ML 6 is the first version with fp16 model inputs and outputs. + model.set_specificationversion(7); + setFeature(model.mutable_description()->add_input(), "x"); + setFeature(model.mutable_description()->add_output(), "z"); + auto *program = model.mutable_mlprogram(); + program->set_version(1); + auto& function = (*program->mutable_functions())["main"]; + function.set_opset("CoreML6"); + auto *input = function.add_inputs(); + input->set_name("x"); + setTensorType(input->mutable_type()); + auto& block = (*function.mutable_block_specializations())["CoreML6"]; + block.add_outputs("z"); + auto *add = block.add_operations(); + add->set_type("add"); + (*add->mutable_inputs())["x"].add_arguments()->set_name("x"); + (*add->mutable_inputs())["y"].add_arguments()->set_name("x"); + auto *output = add->add_outputs(); + output->set_name("z"); + setTensorType(output->mutable_type()); + return model.SerializeAsString(); +} + +void writeJSON(NSDictionary *object, NSURL *url) { + [[NSJSONSerialization dataWithJSONObject:object options:0 error:nil] writeToURL:url atomically:YES]; +} + +/// Writes the model package and its metadata to `directory` and returns them in the delegate's AOT format. +std::string makeModelBlob(NSURL *directory, bool useFloat16 = true) { + NSURL *packageURL = [directory URLByAppendingPathComponent:@"model.mlpackage"]; + NSURL *specDirectoryURL = [packageURL URLByAppendingPathComponent:@"Data/com.apple.CoreML"]; + [NSFileManager.defaultManager createDirectoryAtURL:specDirectoryURL + withIntermediateDirectories:YES + attributes:nil + error:nil]; + const std::string spec = makeModelSpec(useFloat16); + NSURL *specURL = [specDirectoryURL URLByAppendingPathComponent:@"model.mlmodel"]; + if (specURL == nil) { + return ""; + } + [[NSData dataWithBytes:spec.data() length:spec.size()] writeToURL:specURL atomically:YES]; + NSString *specItem = NSUUID.UUID.UUIDString; + writeJSON(@{ + @"fileFormatVersion": @"1.0.0", + @"itemInfoEntries": @{ + specItem: @{ + @"author": @"com.apple.CoreML", + @"description": @"CoreML Model Specification", + @"name": @"model.mlmodel", + @"path": @"com.apple.CoreML/model.mlmodel", + }, + }, + @"rootModelIdentifier": specItem, + }, [packageURL URLByAppendingPathComponent:@"Manifest.json"]); + writeJSON(@{ + @"identifier": NSUUID.UUID.UUIDString, + @"inputNames": @[@"x"], + @"outputNames": @[@"z"], + }, [directory URLByAppendingPathComponent:@"metadata.json"]); + + std::error_code error; + auto fileSystem = inmemoryfs::InMemoryFileSystem::make_from_directory(toString(directory.path), + inmemoryfs::InMemoryFileSystem::FileLoadOption::Malloc, + error); + std::ostringstream stream; + if (fileSystem == nullptr || !inmemoryfs::serialize(*fileSystem, {}, 1, stream, error)) { + return ""; + } + return stream.str(); +} + +class PixelBufferBindingTest : public ::testing::Test { +protected: + static void SetUpTestSuite() { + executorch::runtime::runtime_init(); + } + + void SetUp() override { + directory_ = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:NSUUID.UUID.UUIDString]]; + blob_ = makeModelBlob(directory_); + ASSERT_FALSE(blob_.empty()); + delegate_ = BackendDelegate::make(BackendDelegate::Config()); + } + + void TearDown() override { + delegate_.reset(); + [NSFileManager.defaultManager removeItemAtURL:directory_ error:nil]; + } + + BackendDelegate::Handle *load() { + // In the iOS simulator, Core ML reads pixel buffer inputs as zeros unless it runs on the CPU. + const std::string computeUnits = toString(ETCoreMLStrings.cpuComputeUnitName); + const std::unordered_map specs = { + {toString(ETCoreMLStrings.computeUnitsKeyName), Buffer(computeUnits.data(), computeUnits.size())}, + }; + return delegate_->init(Buffer(blob_.data(), blob_.size()), specs); + } + + NSURL *directory_ = nil; + std::string blob_; + std::shared_ptr delegate_; +}; + +TEST_F(PixelBufferBindingTest, BindsOnlyIOSurfaceBackedFp16PixelBuffers) { + ETCoreMLModel *model = (__bridge ETCoreMLModel *)load(); + ASSERT_NE(model, nil); + + EXPECT_FALSE([model bindPixelBuffer:makePixelBuffer(kCVPixelFormatType_OneComponent16Half, false).get() error:nil]); + EXPECT_FALSE([model bindPixelBuffer:makePixelBuffer(kCVPixelFormatType_OneComponent32Float).get() error:nil]); + EXPECT_TRUE([model bindPixelBuffer:makePixelBuffer(kCVPixelFormatType_OneComponent16Half).get() error:nil]); +} + +TEST_F(PixelBufferBindingTest, ReusesTheBoundBufferArrayWhileTheArgShapeStaysTheSame) { + ETCoreMLModel *model = (__bridge ETCoreMLModel *)load(); + ASSERT_NE(model, nil); + PixelBufferPtr pixelBuffer = makePixelBuffer(kCVPixelFormatType_OneComponent16Half); + ASSERT_TRUE([model bindPixelBuffer:pixelBuffer.get() error:nil]); + void *data = baseAddress(pixelBuffer.get()); + + MLMultiArray *array = nil; + ASSERT_TRUE([model getPixelBufferMultiArray:&array forArg:makeArg(data, kShape) error:nil]); + ASSERT_NE(array, nil); + EXPECT_EQ(array.pixelBuffer, pixelBuffer.get()); + + MLMultiArray *sameShapeArray = nil; + ASSERT_TRUE([model getPixelBufferMultiArray:&sameShapeArray forArg:makeArg(data, kShape) error:nil]); + EXPECT_EQ(sameShapeArray, array); + + MLMultiArray *otherShapeArray = nil; + ASSERT_TRUE([model getPixelBufferMultiArray:&otherShapeArray forArg:makeArg(data, {kHeight, kWidth}) error:nil]); + ASSERT_NE(otherShapeArray, nil); + EXPECT_NE(otherShapeArray, array); + EXPECT_EQ(otherShapeArray.pixelBuffer, pixelBuffer.get()); +} + +TEST_F(PixelBufferBindingTest, FindsNoArrayForAnUnboundDataPointer) { + ETCoreMLModel *model = (__bridge ETCoreMLModel *)load(); + ASSERT_NE(model, nil); + ASSERT_TRUE([model bindPixelBuffer:makePixelBuffer(kCVPixelFormatType_OneComponent16Half).get() error:nil]); + + std::vector<_Float16> data(kHeight * kWidth); + MLMultiArray *array = nil; + EXPECT_TRUE([model getPixelBufferMultiArray:&array forArg:makeArg(data.data(), kShape) error:nil]); + EXPECT_EQ(array, nil); +} + +TEST_F(PixelBufferBindingTest, RejectsAnArgThatDoesntMatchTheBoundBuffer) { + ETCoreMLModel *model = (__bridge ETCoreMLModel *)load(); + ASSERT_NE(model, nil); + PixelBufferPtr pixelBuffer = makePixelBuffer(kCVPixelFormatType_OneComponent16Half); + ASSERT_TRUE([model bindPixelBuffer:pixelBuffer.get() error:nil]); + void *data = baseAddress(pixelBuffer.get()); + + MLMultiArray *array = nil; + NSError *error = nil; + EXPECT_FALSE([model getPixelBufferMultiArray:&array forArg:makeArg(data, {1, 1, kHeight, 2 * kWidth}) error:&error]); + EXPECT_NE(error, nil); + error = nil; + EXPECT_FALSE([model getPixelBufferMultiArray:&array forArg:makeArg(data, {1, 2, kHeight, kWidth}) error:&error]); + EXPECT_NE(error, nil); + error = nil; + EXPECT_FALSE([model getPixelBufferMultiArray:&array + forArg:makeArg(data, kShape, MultiArray::DataType::Float32) + error:&error]); + EXPECT_NE(error, nil); +} + +TEST_F(PixelBufferBindingTest, RejectsBoundBuffersWithMismatchedModelFeatureTypes) { + blob_ = makeModelBlob(directory_, /*useFloat16=*/false); + ASSERT_FALSE(blob_.empty()); + ETCoreMLModel *model = (__bridge ETCoreMLModel *)load(); + ASSERT_NE(model, nil); + PixelBufferPtr pixelBuffer = makePixelBuffer(kCVPixelFormatType_OneComponent16Half); + ASSERT_NE(pixelBuffer.get(), nullptr); + ASSERT_TRUE([model bindPixelBuffer:pixelBuffer.get() error:nil]); + const std::vector args = {makeArg(baseAddress(pixelBuffer.get()), kShape)}; + + NSError *error = nil; + EXPECT_EQ([model prepareInputs:args error:&error], nil); + EXPECT_NE(error, nil); + error = nil; + EXPECT_EQ([model prepareOutputBackings:args error:&error], nil); + EXPECT_NE(error, nil); +} + +TEST_F(PixelBufferBindingTest, ConvertsUnboundBuffersWithMismatchedModelFeatureTypes) { + blob_ = makeModelBlob(directory_, /*useFloat16=*/false); + ASSERT_FALSE(blob_.empty()); + ETCoreMLModel *model = (__bridge ETCoreMLModel *)load(); + ASSERT_NE(model, nil); + std::vector<_Float16> data(kHeight * kWidth, 2); + const std::vector args = {makeArg(data.data(), kShape)}; + + NSError *error = nil; + NSArray *inputs = [model prepareInputs:args error:&error]; + ASSERT_NE(inputs, nil); + EXPECT_EQ(error, nil); + EXPECT_EQ(inputs.firstObject.dataType, MLMultiArrayDataTypeFloat32); + EXPECT_EQ([inputs.firstObject[0] floatValue], 2.0f); + NSArray *outputs = [model prepareOutputBackings:args error:&error]; + ASSERT_NE(outputs, nil); + EXPECT_EQ(error, nil); + EXPECT_EQ(outputs.firstObject.dataType, MLMultiArrayDataTypeFloat32); +} + +TEST_F(PixelBufferBindingTest, CopiesIntoBoundOutputWhenCoreMLIgnoresBackings) { + // A model manager's executor can withhold output backings. Core ML then allocates the output itself, and the + // result reaches the bound buffer only through the fallback copy. + NSURL *assetsURL = [directory_ URLByAppendingPathComponent:@"assets"]; + NSURL *trashURL = [directory_ URLByAppendingPathComponent:@"trash"]; + NSURL *databaseURL = [directory_ URLByAppendingPathComponent:@"assets.db"]; + ASSERT_TRUE([NSFileManager.defaultManager createDirectoryAtURL:assetsURL + withIntermediateDirectories:YES + attributes:nil + error:nil]); + ASSERT_TRUE([NSFileManager.defaultManager createDirectoryAtURL:trashURL + withIntermediateDirectories:YES + attributes:nil + error:nil]); + ETCoreMLAssetManager *assetManager = [[ETCoreMLAssetManager alloc] initWithDatabaseURL:databaseURL + assetsDirectoryURL:assetsURL + trashDirectoryURL:trashURL + maxAssetsSizeInBytes:2 * 1024 * 1024 + error:nil]; + ASSERT_NE(assetManager, nil); + ETCoreMLModelManager *manager = [[ETCoreMLModelManager alloc] initWithAssetManager:assetManager]; + MLModelConfiguration *configuration = [[MLModelConfiguration alloc] init]; + configuration.computeUnits = MLComputeUnitsCPUOnly; + NSData *data = [NSData dataWithBytesNoCopy:blob_.data() length:blob_.size() freeWhenDone:NO]; + ModelHandle *handle = [manager loadModelFromAOTData:data configuration:configuration error:nil]; + ASSERT_NE(handle, nullptr); + id executor = [manager executorWithHandle:handle]; + ASSERT_NE(executor, nil); + executor.ignoreOutputBackings = YES; + ETCoreMLModel *model = [manager modelWithHandle:handle]; + ASSERT_NE(model, nil); + + PixelBufferPtr output = makePixelBuffer(kCVPixelFormatType_OneComponent16Half); + ASSERT_TRUE([model bindPixelBuffer:output.get() error:nil]); + // Padded rows make a copy that ignores the row stride fail the checks below. + const size_t rowLength = CVPixelBufferGetBytesPerRow(output.get()) / sizeof(_Float16); + ASSERT_GT(rowLength, kWidth); + // The model doubles values in [0, 8), so its output never holds this. + constexpr _Float16 kUnwritten = -1; + std::vector<_Float16> input(kHeight * kWidth); + + for (int frame = 0; frame < 3; ++frame) { + for (size_t y = 0; y < kHeight; ++y) { + for (size_t x = 0; x < kWidth; ++x) { + input[y * kWidth + x] = inputValue(x, y, frame); + } + } + CVPixelBufferLockBaseAddress(output.get(), 0); + for (size_t y = 0; y < kHeight; ++y) { + for (size_t x = 0; x < rowLength; ++x) { + row(output.get(), y)[x] = kUnwritten; + } + } + CVPixelBufferUnlockBaseAddress(output.get(), 0); + + std::vector args = { + makeArg(input.data(), kShape), + makeArg(baseAddress(output.get()), kShape), + }; + NSError *error = nil; + ASSERT_TRUE([manager executeModelWithHandle:handle + argsVec:args + loggingOptions:ModelLoggingOptions() + eventLogger:nullptr + error:&error]) + << "frame " << frame; + + CVPixelBufferLockBaseAddress(output.get(), kCVPixelBufferLock_ReadOnly); + for (size_t y = 0; y < kHeight; ++y) { + for (size_t x = 0; x < rowLength; ++x) { + const float expected = x < kWidth ? 2 * inputValue(x, y, frame) : float(kUnwritten); + EXPECT_EQ(float(row(output.get(), y)[x]), expected) + << "frame " << frame << " at (" << x << ", " << y << ")"; + } + } + CVPixelBufferUnlockBaseAddress(output.get(), kCVPixelBufferLock_ReadOnly); + } + EXPECT_TRUE(executor.ignoreOutputBackings); + [manager unloadModelWithHandle:handle]; +} + +TEST_F(PixelBufferBindingTest, PredictsInPlaceThroughBoundBuffers) { + BackendDelegate::Handle *handle = load(); + ETCoreMLModel *model = (__bridge ETCoreMLModel *)handle; + ASSERT_NE(model, nil); + PixelBufferPtr input = makePixelBuffer(kCVPixelFormatType_OneComponent16Half); + PixelBufferPtr output = makePixelBuffer(kCVPixelFormatType_OneComponent16Half); + ASSERT_TRUE([model bindPixelBuffer:input.get() error:nil]); + ASSERT_TRUE([model bindPixelBuffer:output.get() error:nil]); + // Padded rows make a read or write that ignores the row stride fail the checks below. + ASSERT_GT(CVPixelBufferGetBytesPerRow(input.get()), kWidth * sizeof(_Float16)); + ASSERT_GT(CVPixelBufferGetBytesPerRow(output.get()), kWidth * sizeof(_Float16)); + + for (int frame = 0; frame < 3; ++frame) { + CVPixelBufferLockBaseAddress(input.get(), 0); + for (size_t y = 0; y < kHeight; ++y) { + for (size_t x = 0; x < kWidth; ++x) { + row(input.get(), y)[x] = inputValue(x, y, frame); + } + } + CVPixelBufferUnlockBaseAddress(input.get(), 0); + + std::vector args = { + makeArg(baseAddress(input.get()), kShape), + makeArg(baseAddress(output.get()), kShape), + }; + std::error_code error; + ASSERT_TRUE(delegate_->execute(handle, args, ModelLoggingOptions(), nullptr, error)) << "frame " << frame; + + CVPixelBufferLockBaseAddress(output.get(), kCVPixelBufferLock_ReadOnly); + for (size_t y = 0; y < kHeight; ++y) { + for (size_t x = 0; x < kWidth; ++x) { + EXPECT_EQ(float(row(output.get(), y)[x]), 2 * inputValue(x, y, frame)) + << "frame " << frame << " at (" << x << ", " << y << ")"; + } + } + CVPixelBufferUnlockBaseAddress(output.get(), kCVPixelBufferLock_ReadOnly); + } +} + +} // namespace diff --git a/backends/apple/coreml/runtime/workspace/executorchcoreml.xcodeproj/project.pbxproj b/backends/apple/coreml/runtime/workspace/executorchcoreml.xcodeproj/project.pbxproj index 45bca191f7b..8d75ffe50f7 100644 --- a/backends/apple/coreml/runtime/workspace/executorchcoreml.xcodeproj/project.pbxproj +++ b/backends/apple/coreml/runtime/workspace/executorchcoreml.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 191820CBF09461CE46719A45 /* CoreVideo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C6025C1089BE3E722FF1F4D /* CoreVideo.framework */; }; 8307EB8A2C9262060011AE6D /* state_coreml_all.pte in Resources */ = {isa = PBXBuildFile; fileRef = 8307EB892C9262060011AE6D /* state_coreml_all.pte */; }; 838CA6872CD1965700462190 /* add_mul_compiled_coreml_all.pte in Resources */ = {isa = PBXBuildFile; fileRef = 838CA6862CD1965700462190 /* add_mul_compiled_coreml_all.pte */; }; 83BB78A02C65DA7300274ED7 /* ETCoreMLModelDebugInfo.mm in Sources */ = {isa = PBXBuildFile; fileRef = 83BB789F2C65DA7300274ED7 /* ETCoreMLModelDebugInfo.mm */; }; @@ -124,6 +125,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0C6025C1089BE3E722FF1F4D /* CoreVideo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreVideo.framework; path = System/Library/Frameworks/CoreVideo.framework; sourceTree = SDKROOT; }; 8307EB892C9262060011AE6D /* state_coreml_all.pte */ = {isa = PBXFileReference; lastKnownFileType = file; name = state_coreml_all.pte; path = ../test/models/state_coreml_all.pte; sourceTree = ""; }; 838CA6862CD1965700462190 /* add_mul_compiled_coreml_all.pte */ = {isa = PBXFileReference; lastKnownFileType = file; name = add_mul_compiled_coreml_all.pte; path = ../test/models/add_mul_compiled_coreml_all.pte; sourceTree = ""; }; 83BB789E2C65DA7300274ED7 /* ETCoreMLModelDebugInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ETCoreMLModelDebugInfo.h; path = ../sdk/ETCoreMLModelDebugInfo.h; sourceTree = ""; }; @@ -329,6 +331,7 @@ F24817E52BC655E100E80D98 /* libexecutorch_core.a in Frameworks */, C94D510E2ABDF86800AF47FD /* libsqlite3.tbd in Frameworks */, C94D50D92ABD7B2400AF47FD /* CoreML.framework in Frameworks */, + 191820CBF09461CE46719A45 /* CoreVideo.framework in Frameworks */, C99883862B95AD7D000953A3 /* libprotobuf-lite.a in Frameworks */, C99883882B964413000953A3 /* libexecutorch.a in Frameworks */, ); @@ -538,6 +541,7 @@ C9EA3FE52B73EF6300B7D7BD /* Accelerate.framework */, C9EA3FDE2B73EEA000B7D7BD /* libsqlite3.tbd */, C9EA3DB22B71A2B200B7D7BD /* CoreML.framework */, + 0C6025C1089BE3E722FF1F4D /* CoreVideo.framework */, C99883872B964413000953A3 /* libexecutorch.a */, ); name = Frameworks; diff --git a/backends/apple/coreml/targets.bzl b/backends/apple/coreml/targets.bzl index e2a0f25789f..370b6b7ea49 100644 --- a/backends/apple/coreml/targets.bzl +++ b/backends/apple/coreml/targets.bzl @@ -234,6 +234,7 @@ def define_common_targets(is_fbcode = False): fbobjc_frameworks = [ "Accelerate", "CoreML", + "CoreVideo", "Foundation", ], include_directories = [ @@ -310,6 +311,39 @@ def define_common_targets(is_fbcode = False): ], ) + runtime.cxx_test( + name = "pixel_buffer_binding_test", + srcs = [ + "runtime/test/pixel_buffer_binding_test.mm", + ], + headers = glob([ + "runtime/delegate/*.h", + "runtime/inmemoryfs/*.hpp", + "runtime/kvstore/*.hpp", + ]), + compiler_flags = [ + "-fobjc-arc", + ], + fbobjc_frameworks = [ + "CoreML.framework", + "CoreVideo.framework", + "Foundation.framework", + ], + include_directories = [ + "runtime/delegate", + "runtime/inmemoryfs", + "runtime/kvstore", + ], + platforms = [APPLE], + deps = [ + ":coreml", + ":proto", + "//executorch/runtime/backend:backend_options", + "//executorch/runtime/core:core", + "//executorch/runtime/platform:platform", + ], + ) + # Header-only library for CoreML backend options runtime.cxx_library( name = "coreml_backend_options", diff --git a/examples/apple/coreml/executor_runner/coreml_executor_runner.xcodeproj/project.pbxproj b/examples/apple/coreml/executor_runner/coreml_executor_runner.xcodeproj/project.pbxproj index 5e6ff53a580..82d92f943a1 100644 --- a/examples/apple/coreml/executor_runner/coreml_executor_runner.xcodeproj/project.pbxproj +++ b/examples/apple/coreml/executor_runner/coreml_executor_runner.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 38626BB42B225A560059413D /* libflatccrt.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 38626BB32B225A560059413D /* libflatccrt.a */; }; 38626BB52B225A890059413D /* libetdump.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 38626BAF2B21C98F0059413D /* libetdump.a */; }; + 4A18A25D03085CB9ABDE5D75 /* CoreVideo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A580E08A3E468CDA6A1FA9B /* CoreVideo.framework */; }; 879121DA2D91DDBA001E6C66 /* libcoreml_inmemoryfs.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 879121D82D91DDBA001E6C66 /* libcoreml_inmemoryfs.a */; }; 879121DB2D91DDBA001E6C66 /* libcoreml_util.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 879121D92D91DDBA001E6C66 /* libcoreml_util.a */; }; C94D51592ACF4BFC00AF47FD /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = C94D51582ACF4BFC00AF47FD /* main.mm */; }; @@ -36,6 +37,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 2A580E08A3E468CDA6A1FA9B /* CoreVideo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreVideo.framework; path = System/Library/Frameworks/CoreVideo.framework; sourceTree = SDKROOT; }; 38626BAF2B21C98F0059413D /* libetdump.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libetdump.a; path = libraries/libetdump.a; sourceTree = ""; }; 38626BB32B225A560059413D /* libflatccrt.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libflatccrt.a; path = "../../../../third-party/flatcc/lib/libflatccrt.a"; sourceTree = ""; }; 879121D82D91DDBA001E6C66 /* libcoreml_inmemoryfs.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libcoreml_inmemoryfs.a; path = "libraries/libcoreml_inmemoryfs.a"; sourceTree = ""; }; @@ -68,6 +70,7 @@ C988D69D2B998CDE00979CF6 /* libprotobuf-lite.a in Frameworks */, C97BFFA62BC0C1F200F55BAC /* libportable_ops_lib.a in Frameworks */, C94D51642ACFCBC500AF47FD /* CoreML.framework in Frameworks */, + 4A18A25D03085CB9ABDE5D75 /* CoreVideo.framework in Frameworks */, C97BFFA42BC0C17300F55BAC /* libportable_kernels.a in Frameworks */, C94D51622ACFCBBA00AF47FD /* libsqlite3.tbd in Frameworks */, C94D515E2ACFCBA000AF47FD /* libexecutorch.a in Frameworks */, @@ -104,6 +107,7 @@ 38626BAF2B21C98F0059413D /* libetdump.a */, C94D51652ACFCBCB00AF47FD /* Accelerate.framework */, C94D51632ACFCBC500AF47FD /* CoreML.framework */, + 2A580E08A3E468CDA6A1FA9B /* CoreVideo.framework */, C94D515C2ACFCBA000AF47FD /* libexecutorch.a */, C94D51612ACFCBBA00AF47FD /* libsqlite3.tbd */, C94D51672ACFCC7100AF47FD /* libcoremldelegate.a */, diff --git a/runtime/backend/backend_init_context.h b/runtime/backend/backend_init_context.h index 0f649b3733f..4c1a5ca2bfb 100644 --- a/runtime/backend/backend_init_context.h +++ b/runtime/backend/backend_init_context.h @@ -25,6 +25,9 @@ namespace executorch { namespace ET_RUNTIME_NAMESPACE { + +class MethodMeta; + /** * BackendInitContext will be used to inject runtime info for to initialize * delegate. @@ -36,7 +39,8 @@ class BackendInitContext final { EventTracer* event_tracer = nullptr, const char* method_name = nullptr, const NamedDataMap* named_data_map = nullptr, - Span runtime_specs = {}) + Span runtime_specs = {}, + const MethodMeta* method_meta = nullptr) : runtime_allocator_(runtime_allocator), #ifdef ET_EVENT_TRACER_ENABLED event_tracer_(event_tracer), @@ -45,7 +49,8 @@ class BackendInitContext final { #endif method_name_(method_name), named_data_map_(named_data_map), - runtime_specs_(runtime_specs) { + runtime_specs_(runtime_specs), + method_meta_(method_meta) { } /** Get the runtime allocator passed from Method. It's the same runtime @@ -94,6 +99,16 @@ class BackendInitContext final { return runtime_specs_; } + /** + * Returns metadata for the enclosing method, or nullptr if not supplied. + * The runtime provides a borrowed view valid during backend initialization. + * Other callers must keep the view and its backing program alive for the + * lifetime of this context. + */ + const MethodMeta* method_meta() const { + return method_meta_; + } + /** * Get a runtime spec value by key and type. * @@ -135,6 +150,7 @@ class BackendInitContext final { const char* method_name_ = nullptr; const NamedDataMap* named_data_map_ = nullptr; Span runtime_specs_; + const MethodMeta* method_meta_ = nullptr; }; } // namespace ET_RUNTIME_NAMESPACE diff --git a/runtime/backend/test/backend_init_context_test.cpp b/runtime/backend/test/backend_init_context_test.cpp index 8d74b9361fc..25e6c5f055e 100644 --- a/runtime/backend/test/backend_init_context_test.cpp +++ b/runtime/backend/test/backend_init_context_test.cpp @@ -8,7 +8,12 @@ #include #include +#include #include +#include + +#include +#include #include @@ -17,6 +22,7 @@ using executorch::runtime::BackendInitContext; using executorch::runtime::BackendOption; using executorch::runtime::BackendOptions; using executorch::runtime::Error; +using executorch::runtime::MethodMeta; using executorch::runtime::Span; class BackendInitContextTest : public ::testing::Test { @@ -32,6 +38,32 @@ TEST_F(BackendInitContextTest, DefaultConstructorNoRuntimeSpecs) { auto specs = context.runtime_specs(); EXPECT_EQ(specs.size(), 0); + EXPECT_EQ(context.method_meta(), nullptr); +} + +TEST_F(BackendInitContextTest, ConstructorPreservesMethodMeta) { + namespace fb = executorch_flatbuffer; + flatbuffers::FlatBufferBuilder builder; + const auto plan = fb::CreateExecutionPlan( + builder, + builder.CreateString("forward"), + /*container_meta_type=*/0, + builder.CreateVector(std::vector>{}), + builder.CreateVector(std::vector{}), + builder.CreateVector(std::vector{}), + /*chains=*/0, + /*operators=*/0, + builder.CreateVector( + std::vector>{})); + builder.Finish(plan); + const auto meta = MethodMeta::from_serialized_execution_plan( + builder.GetBufferPointer(), builder.GetSize()); + ASSERT_TRUE(meta.ok()); + + BackendInitContext context( + nullptr, nullptr, "forward", nullptr, {}, &meta.get()); + + EXPECT_EQ(context.method_meta(), &meta.get()); } // Test constructor with runtime specs diff --git a/runtime/backend/test/targets.bzl b/runtime/backend/test/targets.bzl index 5ce89cba9d0..0859cdddc86 100644 --- a/runtime/backend/test/targets.bzl +++ b/runtime/backend/test/targets.bzl @@ -31,6 +31,8 @@ def define_common_targets(): deps = [ "//executorch/runtime/core:core", "//executorch/runtime/backend:interface", + "//executorch/runtime/executor:program", + "//executorch/schema:program", ], ) diff --git a/runtime/executor/method.cpp b/runtime/executor/method.cpp index 34aca8e3856..b95078af095 100644 --- a/runtime/executor/method.cpp +++ b/runtime/executor/method.cpp @@ -1022,6 +1022,7 @@ Error Method::init( // makes it safe for errors to return without updating any state. n_delegate_ = 0; + const auto method_meta = this->method_meta(); for (size_t i = 0; i < n_delegate; ++i) { const auto& delegate = *delegates->Get(i); @@ -1038,7 +1039,8 @@ Error Method::init( /*event_tracer=*/event_tracer_, /*method_name=*/serialization_plan_->name()->c_str(), /*named_data_map=*/named_data_map, - /*runtime_specs=*/delegate_runtime_specs); + /*runtime_specs=*/delegate_runtime_specs, + /*method_meta=*/&method_meta); Error err = BackendDelegate::Init( delegate, program_, backend_init_context, &delegates_[i]); if (err != Error::Ok) { diff --git a/runtime/executor/test/backend_integration_test.cpp b/runtime/executor/test/backend_integration_test.cpp index e395b23bf2d..594f7070120 100644 --- a/runtime/executor/test/backend_integration_test.cpp +++ b/runtime/executor/test/backend_integration_test.cpp @@ -610,6 +610,34 @@ TEST_P(BackendIntegrationTest, GetMethodNameDuringInitSuccess) { ASSERT_EQ(program.error(), Error::Ok); } +TEST_P(BackendIntegrationTest, MethodMetaPassedToBackendInitContext) { + Result loader = FileDataLoader::from(program_path()); + ASSERT_EQ(loader.error(), Error::Ok); + Result program = Program::load(&loader.get()); + ASSERT_EQ(program.error(), Error::Ok); + + bool init_called = false; + StubBackend::singleton().install_init( + [&](FreeableBuffer* processed, + ET_UNUSED ArrayRef compile_specs, + BackendInitContext& context) -> Result { + init_called = true; + const auto* meta = context.method_meta(); + EXPECT_NE(meta, nullptr); + if (meta != nullptr) { + EXPECT_STREQ(meta->name(), "forward"); + EXPECT_TRUE(meta->uses_backend(StubBackend::kName)); + } + processed->Free(); + return nullptr; + }); + + ManagedMemoryManager mmm(kDefaultNonConstMemBytes, kDefaultRuntimeMemBytes); + Result method = program->load_method("forward", &mmm.get()); + EXPECT_TRUE(method.ok()); + EXPECT_TRUE(init_called); +} + TEST_P(BackendIntegrationTest, GetMethodNameDuringExecuteSuccess) { Result loader = FileDataLoader::from(program_path()); ASSERT_EQ(loader.error(), Error::Ok); diff --git a/schema/targets.bzl b/schema/targets.bzl index 0528f54536d..7f8d66f7a42 100644 --- a/schema/targets.bzl +++ b/schema/targets.bzl @@ -80,9 +80,10 @@ def define_common_targets(): "//executorch/runtime/executor/...", "//executorch/backends/native/extension/module/...", # Tests have a set up which uses raw flatbuffer. - # TODO will refactor these setup steps into + # TODO will refactor these setup steps into # testing utils in runtime/executor/... path "//executorch/backends/xnnpack/test/...", + "//executorch/runtime/backend/test/...", ], exported_headers = { OUTPUT_PROGRAM_HEADER: ":{}[{}]".format(PROGRAM_GEN_RULE_NAME, OUTPUT_PROGRAM_HEADER),