// // LLMInferenceEngineWrapper.m // mnn-llm // Modified by 游薪渝(揽清) on 2025/7/7. // Created by wangzhaode on 2023/12/14. // /** * LLMInferenceEngineWrapper - A high-level Objective-C wrapper for MNN LLM inference engine * * This class provides a convenient interface for integrating MNN's Large Language Model * inference capabilities into iOS applications. It handles model loading, configuration, * text processing, and streaming output with proper memory management and error handling. * * Key Features: * - Asynchronous model loading with completion callbacks * - Streaming text generation with real-time output * - Configurable inference parameters through JSON * - Memory-mapped model loading for efficiency * - Chat history management and conversation context * - Benchmarking capabilities for performance testing * * Usage Examples: * * 1. Basic Model Loading and Inference: * ```objc * LLMInferenceEngineWrapper *engine = [[LLMInferenceEngineWrapper alloc] * initWithModelPath:@"/path/to/model" * completion:^(BOOL success) { * if (success) { * NSLog(@"Model loaded successfully"); * } * }]; * * [engine processInput:@"Hello, how are you?" * withOutput:^(NSString *output) { * NSLog(@"AI Response: %@", output); * }]; * ``` * * 2. Configuration with Custom Parameters: * ```objc * NSString *config = @"{\"temperature\":0.7,\"max_tokens\":100}"; * [engine setConfigWithJSONString:config]; * ``` * * 3. Chat History Management: * ```objc * NSArray *chatHistory = @[ * @{@"user": @"What is AI?"}, * @{@"assistant": @"AI stands for Artificial Intelligence..."} * ]; * [engine addPromptsFromArray:chatHistory]; * ``` * * Architecture: * - Built on top of MNN's C++ LLM inference engine * - Uses smart pointers for automatic memory management * - Implements custom stream buffer for real-time text output * - Supports both bundled and external model loading */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // #include "MNN/expr/ExecutorScope.hpp" // Removed - file not found #import #import #import #if defined(__has_include) #if __has_include() #include #else namespace MNN { namespace Express { enum DimensionType { NHWC }; struct DummyVariable { struct Info { std::vector dim; }; Info info; template T* writeMap() { return nullptr; } template T* writeMap() const { return nullptr; } Info* getInfo() { return &info; } const Info* getInfo() const { return &info; } }; class VARP { public: DummyVariable storage; DummyVariable* operator->() { return &storage; } const DummyVariable* operator->() const { return &storage; } DummyVariable* get() { return &storage; } const DummyVariable* get() const { return &storage; } void reset() {} explicit operator bool() const { return false; } }; inline VARP _Input(std::initializer_list, DimensionType, int) { return VARP(); } } } template int halide_type_of() { return 0; } #endif #endif #if __has_include() #import #elif __has_include() #import #define UIImage NSImage #endif #import "LLMInferenceEngineWrapper.h" #if defined(__has_include) && __has_include() #include using namespace MNN::Transformer; #else namespace MNN { namespace Transformer { struct PromptImagePart; struct PromptAudioPart; struct MultimodalPrompt; class Llm { public: static Llm* createLLM(const std::string& config_path); virtual void set_config(const std::string& config) = 0; virtual void load() = 0; virtual void response(const std::string& input_str, std::ostream* os = nullptr, const char* end_with = nullptr) = 0; virtual void response(const std::vector>& history, std::ostream* os = nullptr, const char* end_with = nullptr, int max_new_tokens = 999999) = 0; virtual void response(const MultimodalPrompt& prompt, std::ostream* os = nullptr, const char* end_with = nullptr, int max_new_tokens = 999999) = 0; virtual void response(const std::vector& tokens, std::ostream* os = nullptr, const char* end_with = nullptr, int max_new_tokens = 999999) = 0; virtual void reset() = 0; virtual bool stoped() = 0; virtual int generate(int max_token_number = 0) = 0; virtual void generateWavform() = 0; virtual void setWavformCallback(std::function callback) = 0; struct LlmContext { int prompt_len; int gen_seq_len; int64_t prefill_us; int64_t decode_us; }; virtual LlmContext* getContext() = 0; virtual ~Llm() = default; }; struct PromptImagePart { MNN::Express::VARP image_data; int width = 0; int height = 0; }; struct PromptAudioPart { std::string file_path; MNN::Express::VARP waveform; }; struct MultimodalPrompt { std::string prompt_template; std::map images; std::map audios; }; } } using namespace MNN::Transformer; #endif using ChatMessage = std::pair; // MARK: - Benchmark Progress Info Implementation @implementation BenchmarkProgressInfo - (instancetype)init { self = [super init]; if (self) { _progress = 0; _statusMessage = @""; _progressType = BenchmarkProgressTypeUnknown; _currentIteration = 0; _totalIterations = 0; _nPrompt = 0; _nGenerate = 0; _runTimeSeconds = 0.0f; _prefillTimeSeconds = 0.0f; _decodeTimeSeconds = 0.0f; _prefillSpeed = 0.0f; _decodeSpeed = 0.0f; } return self; } @end // MARK: - Benchmark Result Implementation @implementation BenchmarkResult - (instancetype)init { self = [super init]; if (self) { _success = NO; _errorMessage = nil; _prefillTimesUs = @[]; _decodeTimesUs = @[]; _sampleTimesUs = @[]; _promptTokens = 0; _generateTokens = 0; _repeatCount = 0; _kvCacheEnabled = NO; } return self; } @end /** * C++ Benchmark result structure */ struct BenchmarkResultCpp { bool success; std::string error_message; std::vector prefill_times_us; std::vector decode_times_us; std::vector sample_times_us; int prompt_tokens; int generate_tokens; int repeat_count; bool kv_cache_enabled; }; /** * C++ Benchmark progress info structure */ struct BenchmarkProgressInfoCpp { int progress; std::string statusMessage; int progressType; int currentIteration; int totalIterations; int nPrompt; int nGenerate; float runTimeSeconds; float prefillTimeSeconds; float decodeTimeSeconds; float prefillSpeed; float decodeSpeed; BenchmarkProgressInfoCpp() : progress(0), statusMessage(""), progressType(0), currentIteration(0), totalIterations(0), nPrompt(0), nGenerate(0), runTimeSeconds(0.0f), prefillTimeSeconds(0.0f), decodeTimeSeconds(0.0f), prefillSpeed(0.0f), decodeSpeed(0.0f) {} }; // MARK: - C++ Benchmark Implementation /** * C++ Benchmark callback structure */ struct BenchmarkCallback { std::function onProgress; std::function onError; std::function onIterationComplete; }; /** * Enhanced LlmStreamBuffer with improved performance and error handling */ class OptimizedLlmStreamBuffer : public std::streambuf { public: using CallBack = std::function; OptimizedLlmStreamBuffer(CallBack callback) : callback_(callback) { buffer_.reserve(1024); // Pre-allocate buffer for better performance } ~OptimizedLlmStreamBuffer() { flushBuffer(); } protected: virtual std::streamsize xsputn(const char* s, std::streamsize n) override { if (!callback_ || n <= 0) { return n; } try { buffer_.append(s, n); const size_t BUFFER_THRESHOLD = 64; bool shouldFlush = buffer_.size() >= BUFFER_THRESHOLD; if (!shouldFlush && n > 0) { shouldFlush = checkForFlushTriggers(s, n); } if (shouldFlush) { flushBuffer(); } return n; } catch (const std::exception& e) { NSLog(@"Error in stream buffer: %s", e.what()); return -1; } } private: void flushBuffer() { if (callback_ && !buffer_.empty()) { callback_(buffer_.c_str(), buffer_.size()); buffer_.clear(); } } bool checkForFlushTriggers(const char* s, std::streamsize n) { // Check ASCII punctuation char lastChar = s[n-1]; if (lastChar == '\n' || lastChar == '\r' || lastChar == '\t' || lastChar == '.' || lastChar == ',' || lastChar == ';' || lastChar == ':' || lastChar == '!' || lastChar == '?') { return true; } // Check Unicode punctuation return checkUnicodePunctuation(); } bool checkUnicodePunctuation() { if (buffer_.size() >= 3) { const char* bufferEnd = buffer_.c_str() + buffer_.size() - 3; // Chinese punctuation marks (3-byte UTF-8) static const std::vector chinesePunctuation = { "\xE3\x80\x82", // 。 "\xEF\xBC\x8C", // , "\xEF\xBC\x9B", // ; "\xEF\xBC\x9A", // : "\xEF\xBC\x81", // ! "\xEF\xBC\x9F", // ? "\xE2\x80\xA6", // … }; for (const auto& punct : chinesePunctuation) { if (memcmp(bufferEnd, punct.c_str(), 3) == 0) { return true; } } } // Check 2-byte punctuation if (buffer_.size() >= 2) { const char* bufferEnd = buffer_.c_str() + buffer_.size() - 2; if (memcmp(bufferEnd, "\xE2\x80\x93", 2) == 0 || // – memcmp(bufferEnd, "\xE2\x80\x94", 2) == 0) { // — return true; } } return false; } CallBack callback_ = nullptr; std::string buffer_; // Buffer for accumulating output }; static std::vector ExtractImagePlaceholders(const std::string& prompt) { std::vector keys; try { std::regex img_regex("([^<]+)"); auto begin = std::sregex_iterator(prompt.begin(), prompt.end(), img_regex); auto end = std::sregex_iterator(); for (auto it = begin; it != end; ++it) { if ((*it).size() > 1) { keys.push_back((*it)[1].str()); } } } catch (const std::exception& e) { NSLog(@"Regex error while extracting image placeholders: %s", e.what()); } return keys; } static void RemoveImagePlaceholder(std::string& prompt, const std::string& key) { if (key.empty()) { return; } const std::string tag = "" + key + ""; size_t pos = 0; while ((pos = prompt.find(tag, pos)) != std::string::npos) { prompt.erase(pos, tag.size()); } } static std::vector ExtractAudioPlaceholders(const std::string& prompt) { std::vector keys; try { std::regex audio_regex(""); auto begin = std::sregex_iterator(prompt.begin(), prompt.end(), audio_regex); auto end = std::sregex_iterator(); for (auto it = begin; it != end; ++it) { if ((*it).size() > 1) { keys.push_back((*it)[1].str()); } } } catch (const std::exception& e) { NSLog(@"Regex error while extracting audio placeholders: %s", e.what()); } return keys; } static void RemoveAudioPlaceholder(std::string& prompt, const std::string& key) { if (key.empty()) { return; } const std::string tag = ""; size_t pos = 0; while ((pos = prompt.find(tag, pos)) != std::string::npos) { prompt.erase(pos, tag.size()); } } static std::vector ExtractVideoPlaceholders(const std::string& prompt) { std::vector keys; try { std::regex video_regex(""); auto begin = std::sregex_iterator(prompt.begin(), prompt.end(), video_regex); auto end = std::sregex_iterator(); for (auto it = begin; it != end; ++it) { if ((*it).size() > 1) { keys.push_back((*it)[1].str()); } } } catch (const std::exception& e) { NSLog(@"Regex error while extracting video placeholders: %s", e.what()); } return keys; } static void ReplaceVideoPlaceholder(std::string& prompt, const std::string& key, const std::string& replacement) { if (key.empty()) { return; } const std::string tag = ""; size_t pos = 0; while ((pos = prompt.find(tag, pos)) != std::string::npos) { prompt.replace(pos, tag.size(), replacement); pos += replacement.size(); } } static UIImage *LoadUIImageFromPath(const std::string& path) { NSString *nsPath = [NSString stringWithUTF8String:path.c_str()]; if (!nsPath || nsPath.length == 0) { return nil; } if (![[NSFileManager defaultManager] fileExistsAtPath:nsPath]) { return nil; } #if TARGET_OS_IPHONE return [UIImage imageWithContentsOfFile:nsPath]; #else return [[NSImage alloc] initWithContentsOfFile:nsPath]; #endif } static NSArray *ExtractFramesFromVideoAtPath(NSString *videoPath, NSInteger maxFrames) { if (!videoPath) { return nil; } NSURL *url = [NSURL fileURLWithPath:videoPath]; AVURLAsset *asset = [AVURLAsset assetWithURL:url]; if (!asset) { return nil; } NSError *error = nil; if ([asset tracksWithMediaType:AVMediaTypeVideo].count == 0) { return nil; } AVAssetImageGenerator *generator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset]; generator.appliesPreferredTrackTransform = YES; generator.requestedTimeToleranceAfter = CMTimeMake(1, 30); generator.requestedTimeToleranceBefore = CMTimeMake(1, 30); CMTime durationTime = asset.duration; Float64 duration = CMTimeGetSeconds(durationTime); if (!isfinite(duration) || duration <= 0) { return nil; } NSInteger frameCount = MAX(1, MIN(maxFrames, (NSInteger)ceil(duration))); NSMutableArray *frames = [NSMutableArray arrayWithCapacity:frameCount]; for (NSInteger index = 0; index < frameCount; index++) { Float64 seconds = MIN(duration - 0.001, (duration / frameCount) * index); CMTime time = CMTimeMakeWithSeconds(seconds, durationTime.timescale == 0 ? 600 : durationTime.timescale); CGImageRef cgImage = [generator copyCGImageAtTime:time actualTime:NULL error:&error]; if (cgImage) { #if TARGET_OS_IPHONE UIImage *image = [UIImage imageWithCGImage:cgImage]; #else NSSize size = NSMakeSize(CGImageGetWidth(cgImage), CGImageGetHeight(cgImage)); UIImage *image = [[NSImage alloc] initWithCGImage:cgImage size:size]; #endif [frames addObject:image]; CGImageRelease(cgImage); } } return frames.count > 0 ? frames : nil; } static std::vector ExtractVideoFramesToFilePaths(NSString *videoPath, NSInteger maxFrames) { std::vector filePaths; if (!videoPath) { return filePaths; } NSURL *url = [NSURL fileURLWithPath:videoPath]; AVURLAsset *asset = [AVURLAsset assetWithURL:url]; if (!asset) { return filePaths; } NSError *error = nil; NSArray *tracks = [asset tracksWithMediaType:AVMediaTypeVideo]; if (tracks.count == 0) { return filePaths; } CMTime durationTime = asset.duration; Float64 duration = CMTimeGetSeconds(durationTime); if (!isfinite(duration) || duration <= 0) { return filePaths; } NSInteger frameCount = MAX(1, MIN(maxFrames, (NSInteger)ceil(duration))); AVAssetImageGenerator *generator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset]; generator.appliesPreferredTrackTransform = YES; generator.requestedTimeToleranceAfter = CMTimeMake(1, 30); generator.requestedTimeToleranceBefore = CMTimeMake(1, 30); NSString *tempDir = NSTemporaryDirectory(); NSString *baseName = [[videoPath lastPathComponent] stringByDeletingPathExtension]; if (baseName.length == 0) { baseName = @"video"; } for (NSInteger index = 0; index < frameCount; index++) { Float64 seconds = MIN(duration - 0.001, (duration / frameCount) * index); CMTime time = CMTimeMakeWithSeconds(seconds, durationTime.timescale == 0 ? 600 : durationTime.timescale); CGImageRef cgImage = [generator copyCGImageAtTime:time actualTime:NULL error:&error]; if (!cgImage) { if (error) { NSLog(@"[LegacyVideo] Failed to extract frame %ld: %@", (long)index, error.localizedDescription); } continue; } #if TARGET_OS_IPHONE UIImage *image = [UIImage imageWithCGImage:cgImage]; NSData *data = UIImageJPEGRepresentation(image, 0.9); #else NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithCGImage:cgImage]; NSData *data = [rep representationUsingType:NSBitmapImageFileTypeJPEG properties:@{NSImageCompressionFactor: @0.9}]; #endif CGImageRelease(cgImage); if (!data) { continue; } NSString *uuid = [[NSUUID UUID] UUIDString]; NSString *fileName = [NSString stringWithFormat:@"%@_frame_%ld_%@.jpg", baseName, (long)index, uuid]; NSString *filePath = [tempDir stringByAppendingPathComponent:fileName]; if ([data writeToFile:filePath atomically:YES]) { filePaths.push_back([filePath UTF8String]); } else { NSLog(@"[LegacyVideo] Failed to write frame to %@", filePath); } } return filePaths; } static std::string ProcessLegacyVideoPlaceholders(const std::string& prompt, int maxFrames) { if (prompt.find("