项目文件夹

文件
2026-07-13 13:33:03 +08:00

2496 行
93 KiB
Plaintext

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
//
// 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 <iostream>
#include <string>
#include <unistd.h>
#include <sys/stat.h>
#include <filesystem>
#include <functional>
#include <atomic>
#include <mutex>
#include <thread>
#include <chrono>
#include <fstream>
#include <iomanip>
#include <regex>
#include <unordered_set>
#include <map>
#include <initializer_list>
#include <vector>
#include <utility>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
// #include "MNN/expr/ExecutorScope.hpp" // Removed - file not found
#import <TargetConditionals.h>
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
#if defined(__has_include)
#if __has_include(<MNN/expr/Expr.hpp>)
#include <MNN/expr/Expr.hpp>
#else
namespace MNN {
namespace Express {
enum DimensionType { NHWC };
struct DummyVariable {
struct Info { std::vector<int> dim; };
Info info;
template<typename T>
T* writeMap() { return nullptr; }
template<typename T>
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<int>, DimensionType, int) { return VARP(); }
}
}
template<typename T>
int halide_type_of() { return 0; }
#endif
#endif
#if __has_include(<UIKit/UIKit.h>)
#import <UIKit/UIKit.h>
#elif __has_include(<AppKit/AppKit.h>)
#import <AppKit/AppKit.h>
#define UIImage NSImage
#endif
#import "LLMInferenceEngineWrapper.h"
#if defined(__has_include) && __has_include(<MNN/llm/llm.hpp>)
#include <MNN/llm/llm.hpp>
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<std::pair<std::string, std::string>>& 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<int>& 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<bool(const float*, size_t, bool)> 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<std::string, PromptImagePart> images;
std::map<std::string, PromptAudioPart> audios;
};
}
}
using namespace MNN::Transformer;
#endif
using ChatMessage = std::pair<std::string, std::string>;
// 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<int64_t> prefill_times_us;
std::vector<int64_t> decode_times_us;
std::vector<int64_t> 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<void(const BenchmarkProgressInfoCpp& progressInfo)> onProgress;
std::function<void(const std::string& error)> onError;
std::function<void(const std::string& detailed_stats)> onIterationComplete;
};
/**
* Enhanced LlmStreamBuffer with improved performance and error handling
*/
class OptimizedLlmStreamBuffer : public std::streambuf {
public:
using CallBack = std::function<void(const char* str, size_t len)>;
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<std::string> 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<std::string> ExtractImagePlaceholders(const std::string& prompt) {
std::vector<std::string> keys;
try {
std::regex img_regex("<img>([^<]+)</img>");
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 = "<img>" + key + "</img>";
size_t pos = 0;
while ((pos = prompt.find(tag, pos)) != std::string::npos) {
prompt.erase(pos, tag.size());
}
}
static std::vector<std::string> ExtractAudioPlaceholders(const std::string& prompt) {
std::vector<std::string> keys;
try {
std::regex audio_regex("<audio>([^<]+)</audio>");
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 = "<audio>" + key + "</audio>";
size_t pos = 0;
while ((pos = prompt.find(tag, pos)) != std::string::npos) {
prompt.erase(pos, tag.size());
}
}
static std::vector<std::string> ExtractVideoPlaceholders(const std::string& prompt) {
std::vector<std::string> keys;
try {
std::regex video_regex("<video>([^<]+)</video>");
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 = "<video>" + key + "</video>";
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<UIImage *> *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<UIImage *> *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<std::string> ExtractVideoFramesToFilePaths(NSString *videoPath, NSInteger maxFrames) {
std::vector<std::string> filePaths;
if (!videoPath) {
return filePaths;
}
NSURL *url = [NSURL fileURLWithPath:videoPath];
AVURLAsset *asset = [AVURLAsset assetWithURL:url];
if (!asset) {
return filePaths;
}
NSError *error = nil;
NSArray<AVAssetTrack *> *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("<video>") == std::string::npos) {
return prompt;
}
std::string processed = prompt;
auto videoKeys = ExtractVideoPlaceholders(prompt);
std::unordered_set<std::string> handled;
for (const auto& videoPath : videoKeys) {
if (handled.count(videoPath) > 0) {
continue;
}
handled.insert(videoPath);
NSString *nsVideoPath = [NSString stringWithUTF8String:videoPath.c_str()];
if (!nsVideoPath || ![[NSFileManager defaultManager] fileExistsAtPath:nsVideoPath]) {
NSLog(@"[LegacyVideo] Video file not found for placeholder %@", nsVideoPath ?: @"(null)");
ReplaceVideoPlaceholder(processed, videoPath, "");
continue;
}
auto framePaths = ExtractVideoFramesToFilePaths(nsVideoPath, maxFrames);
if (framePaths.empty()) {
NSLog(@"[LegacyVideo] No frames extracted for %@", nsVideoPath);
ReplaceVideoPlaceholder(processed, videoPath, "");
continue;
}
std::string replacement;
for (const auto& framePath : framePaths) {
replacement += "<img>" + framePath + "</img>";
}
NSLog(@"[LegacyVideo] Replaced %@ with %lu frame placeholders", nsVideoPath, (unsigned long)framePaths.size());
ReplaceVideoPlaceholder(processed, videoPath, replacement);
}
return processed;
}
@interface LLMInferenceEngineWrapper ()
- (MNN::Express::VARP)convertUIImageToVARP:(UIImage *)image;
@end
@implementation LLMInferenceEngineWrapper {
std::shared_ptr<MNN::Transformer::Llm> _llm;
std::vector<ChatMessage> _history;
std::mutex _historyMutex;
std::atomic<bool> _isProcessing;
std::atomic<bool> _isBenchmarkRunning;
std::atomic<bool> _shouldStopBenchmark;
std::atomic<bool> _shouldStopInference;
std::atomic<int> _videoMaxFrames;
std::atomic<bool> _enableAudioOutput;
NSString *_talkerSpeaker;
BOOL (^_audioWaveformCallback)(const float *, size_t, BOOL);
NSString *_modelPath;
}
/**
* Initializes the LLM inference engine with a model path
*
* This method asynchronously loads the LLM model from the specified path
* and calls the completion handler on the main queue when finished.
*
* @param modelPath The file system path to the model directory
* @param completion Completion handler called with success/failure status
* @return Initialized instance of LLMInferenceEngineWrapper
*/
- (instancetype)initWithModelPath:(NSString *)modelPath completion:(CompletionHandler)completion {
self = [super init];
if (self) {
_modelPath = [modelPath copy];
_isProcessing = false;
_isBenchmarkRunning = false;
_shouldStopBenchmark = false;
_shouldStopInference = false;
_videoMaxFrames = 8;
_enableAudioOutput = false;
_talkerSpeaker = @"default";
_audioWaveformCallback = nil;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
// Resolve the model path using the new path resolution logic
NSString *resolvedPath = [self resolveModelPath:modelPath];
BOOL success = [self loadModelFromPath:resolvedPath];
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) {
completion(success);
}
});
});
}
return self;
}
/**
* Utility function to remove a directory and all its contents
*
* @param path The directory path to remove
* @return true if successful, false otherwise
*/
bool removeDirectorySafely(const std::string& path) {
@try {
NSString *pathStr = [NSString stringWithUTF8String:path.c_str()];
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:pathStr]) {
NSError *error = nil;
BOOL success = [fileManager removeItemAtPath:pathStr error:&error];
if (!success && error) {
NSLog(@"Error removing directory %s: %@", path.c_str(), error.localizedDescription);
return false;
}
return success;
}
return true;
} @catch (NSException *exception) {
NSLog(@"Exception removing directory %s: %@", path.c_str(), exception.reason);
return false;
}
}
/**
* Resolves model path for new ModelIndex.json structure
*
* @param modelPath The original model path from ModelInfo
* @return Resolved absolute path to the model directory
*/
- (NSString *)resolveModelPath:(NSString *)modelPath {
// Check if this is a new ModelIndex.json structure path
if ([modelPath hasPrefix:@"LocalModel/"]) {
NSString *bundlePath = [[NSBundle mainBundle] resourcePath];
return [bundlePath stringByAppendingPathComponent:modelPath];
}
// Check if this is a bundle_root path (legacy flattened structure)
if ([modelPath hasPrefix:@"bundle_root/"]) {
NSString *bundlePath = [[NSBundle mainBundle] resourcePath];
return bundlePath;
}
// For absolute paths or other formats, return as-is
return modelPath;
}
/**
* Validates model path and configuration
*
* @param modelPath The path to validate
* @return YES if path is valid and contains required files
*/
- (BOOL)validateModelPath:(NSString *)modelPath {
if (!modelPath || modelPath.length == 0) {
NSLog(@"Error: Model path is nil or empty");
return NO;
}
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isDirectory;
if (![fileManager fileExistsAtPath:modelPath isDirectory:&isDirectory] || !isDirectory) {
NSLog(@"Error: Model path does not exist or is not a directory: %@", modelPath);
return NO;
}
NSString *configPath = [modelPath stringByAppendingPathComponent:@"config.json"];
if (![fileManager fileExistsAtPath:configPath]) {
NSLog(@"Error: config.json not found at path: %@", configPath);
return NO;
}
return YES;
}
/**
* Loads the LLM model from the application bundle
*
* This method is used for testing with models bundled within the app.
* It sets up the model with default configuration and temporary directory.
*
* @return YES if model loading succeeds, NO otherwise
*/
- (BOOL)loadModel {
@try {
if (_llm) {
NSLog(@"Warning: Model already loaded");
return YES;
}
NSString *bundleDirectory = [[NSBundle mainBundle] bundlePath];
std::string model_dir = [bundleDirectory UTF8String];
std::string config_path = model_dir + "/config.json";
_llm.reset(MNN::Transformer::Llm::createLLM(config_path));
if (!_llm) {
NSLog(@"Error: Failed to create LLM from bundle");
return NO;
}
NSString *tempDirectory = NSTemporaryDirectory();
std::string configStr = "{\"tmp_path\":\"" + std::string([tempDirectory UTF8String]) + "\", \"use_mmap\":true}";
_llm->set_config(configStr);
_llm->load();
NSLog(@"Model loaded successfully from bundle");
return YES;
}
@catch (NSException *exception) {
NSLog(@"Exception during model loading: %@", exception.reason);
return NO;
}
}
/**
* Loads the LLM model from a specified file system path
*
* This method handles the complete model loading process including:
* - Path validation and error checking
* - Reading model configuration from config.json
* - Setting up temporary directories for model operations
* - Configuring memory mapping settings
* - Loading the model into memory with proper error handling
*
* @param modelPath The file system path to the model directory
* @return YES if model loading succeeds, NO otherwise
*/
- (BOOL)loadModelFromPath:(NSString *)modelPath {
@try {
if (_llm) {
NSLog(@"Warning: Model already loaded");
return YES;
}
if (![self validateModelPath:modelPath]) {
return NO;
}
std::string config_path = std::string([modelPath UTF8String]) + "/config.json";
// Read and parse configuration with error handling
NSError *error = nil;
NSData *configData = [NSData dataWithContentsOfFile:[NSString stringWithUTF8String:config_path.c_str()]];
if (!configData) {
NSLog(@"Error: Failed to read config file at %s", config_path.c_str());
return NO;
}
NSDictionary *configDict = [NSJSONSerialization JSONObjectWithData:configData options:0 error:&error];
if (error) {
NSLog(@"Error parsing config JSON: %@", error.localizedDescription);
return NO;
}
// Get memory mapping setting with default fallback
BOOL useMmap = configDict[@"use_mmap"] == nil ? YES : [configDict[@"use_mmap"] boolValue];
// Create LLM instance with error checking
_llm.reset(MNN::Transformer::Llm::createLLM(config_path));
if (!_llm) {
NSLog(@"Error: Failed to create LLM instance from config: %s", config_path.c_str());
return NO;
}
// Setup temporary directory with improved error handling
// Use iOS system temporary directory instead of model path (which is read-only in Bundle)
NSString *tempDir = NSTemporaryDirectory();
NSString *modelName = [[modelPath lastPathComponent] stringByDeletingPathExtension];
NSString *tempDirPath = [tempDir stringByAppendingPathComponent:[NSString stringWithFormat:@"MNN_%@_temp", modelName]];
std::string temp_directory_path = [tempDirPath UTF8String];
// Clean up existing temp directory
if (!removeDirectorySafely(temp_directory_path)) {
NSLog(@"Warning: Failed to remove existing temp directory, continuing...");
}
// Create new temp directory in system temp location
if (mkdir(temp_directory_path.c_str(), 0755) != 0 && errno != EEXIST) {
NSLog(@"Error: Failed to create temp directory: %s, errno: %d", temp_directory_path.c_str(), errno);
return NO;
}
NSLog(@"Created temp directory at: %s", temp_directory_path.c_str());
// Configure LLM with proper error handling
bool useMmapCpp = (useMmap == YES);
std::string configStr = "{\"tmp_path\":\"" + temp_directory_path + "\", \"use_mmap\":" + (useMmapCpp ? "true" : "false") + "}";
_llm->set_config(configStr);
_llm->load();
NSLog(@"Model loaded successfully from path: %@", modelPath);
return YES;
}
@catch (NSException *exception) {
NSLog(@"Exception during model loading: %@", exception.reason);
_llm.reset();
return NO;
}
}
/**
* Sets the configuration for the LLM engine using a JSON string
*
* This method allows runtime configuration of various LLM parameters
* such as temperature, max tokens, sampling methods, etc.
*
* @param jsonStr JSON string containing configuration parameters
*/
- (void)setConfigWithJSONString:(NSString *)jsonStr {
if (!_llm) {
NSLog(@"Error: LLM not initialized, cannot set configuration");
return;
}
if (!jsonStr || jsonStr.length == 0) {
NSLog(@"Error: JSON string is nil or empty");
return;
}
@try {
NSLog(@"[AudioWrapper] setConfigWithJSONString length=%lu content=%@", (unsigned long)[jsonStr length], jsonStr);
// Validate JSON format
NSError *error = nil;
NSData *jsonData = [jsonStr dataUsingEncoding:NSUTF8StringEncoding];
[NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
if (error) {
NSLog(@"Error: Invalid JSON configuration: %@", error.localizedDescription);
return;
}
const char *cString = [jsonStr UTF8String];
std::string stdString(cString);
_llm->set_config(stdString);
NSLog(@"Configuration updated successfully");
}
@catch (NSException *exception) {
NSLog(@"Exception while setting configuration: %@", exception.reason);
}
}
/**
* Set thinking mode for the LLM engine
*
* @param enabled Whether to enable thinking mode
*/
- (void)setThinkingModeEnabled:(BOOL)enabled {
if (!_llm) {
NSLog(@"Warning: LLM engine not initialized, cannot set thinking mode");
return;
}
try {
std::string configJson = R"({
"jinja": {
"context": {
"enable_thinking":)" + std::string(enabled ? "true" : "false") + R"(
}
}
})";
_llm->set_config(configJson);
NSLog(@"Thinking mode %@", enabled ? @"enabled" : @"disabled");
} catch (const std::exception& e) {
NSLog(@"Error setting thinking mode: %s", e.what());
} catch (...) {
NSLog(@"Unknown error occurred while setting thinking mode");
}
}
/**
* Processes user input and generates streaming LLM response with enhanced error handling
*
* This method handles the main inference process by:
* - Validating input parameters and model state
* - Setting up streaming output callback with error handling
* - Adding user input to chat history thread-safely
* - Executing LLM inference with streaming output
* - Handling special commands like benchmarking
*
* @param input The user's input text to process
* @param output Callback block that receives streaming output chunks
*/
- (void)processInput:(NSString *)input withOutput:(OutputHandler)output {
[self processInput:input withOutput:output showPerformance:NO];
}
/**
* Processes user input and generates streaming LLM response with optional performance output
*
* @param input The user's input text to process
* @param output Callback block that receives streaming output chunks
* @param showPerformance Whether to output performance statistics after response completion
*/
- (void)processInput:(NSString *)input withOutput:(OutputHandler)output showPerformance:(BOOL)showPerformance {
if (!_llm) {
if (output) {
output(@"Error: Model not loaded. Please initialize the model first.");
}
return;
}
if (!input || input.length == 0) {
if (output) {
output(@"Error: Input text is empty.");
}
return;
} else {
std::string legacyPrompt = [input UTF8String];
legacyPrompt = ProcessLegacyVideoPlaceholders(legacyPrompt, std::max(1, _videoMaxFrames.load()));
input = [NSString stringWithUTF8String:legacyPrompt.c_str()];
}
if (_isProcessing.load()) {
if (output) {
output(@"Error: Another inference is already in progress.");
}
return;
}
// Get initial context state BEFORE inference starts
auto* context = _llm->getContext();
int initial_prompt_len = 0;
int initial_decode_len = 0;
int64_t initial_prefill_time = 0;
int64_t initial_decode_time = 0;
if (context && showPerformance) {
initial_prompt_len = context->prompt_len;
initial_decode_len = context->gen_seq_len;
initial_prefill_time = context->prefill_us;
initial_decode_time = context->decode_us;
}
_isProcessing = true;
// Store reference for block execution
LLMInferenceEngineWrapper *blockSelf = self;
// Use high priority queue for better responsiveness
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
// Check if object is still valid before proceeding
if (!blockSelf || !blockSelf->_llm) {
NSLog(@"LLMInferenceEngineWrapper was deallocated or model unloaded during inference");
return;
}
@try {
auto inference_start_time = std::chrono::high_resolution_clock::now();
OptimizedLlmStreamBuffer::CallBack callback = [output](const char* str, size_t len) {
if (output && str && len > 0) {
@autoreleasepool {
NSString *nsOutput = [[NSString alloc] initWithBytes:str
length:len
encoding:NSUTF8StringEncoding];
if (nsOutput) {
dispatch_async(dispatch_get_main_queue(), ^{
output(nsOutput);
});
}
}
}
};
OptimizedLlmStreamBuffer streambuf(callback);
std::ostream os(&streambuf);
// Thread-safe history management
{
std::lock_guard<std::mutex> lock(blockSelf->_historyMutex);
blockSelf->_history.emplace_back(ChatMessage("user", [input UTF8String]));
}
std::string inputStr = [input UTF8String];
#ifdef DEBUG
if (inputStr == "benchmark") {
[blockSelf performBenchmarkWithOutput:&os];
} else {
#else
{
#endif
// Reset stop flag before starting inference
blockSelf->_shouldStopInference = false;
// Execute inference with enhanced stopped status checking
@try {
// Debug information for prompt
std::string prompt_debug = "";
for (const auto& msg : blockSelf->_history) {
prompt_debug += msg.first + ": " + msg.second + "\n";
}
NSLog(@"submitNative prompt_string_for_debug:\n%s\nmax_new_tokens_: %d", prompt_debug.c_str(), 999999);
// dump_config may not be available in all builds of MNN; guard the call
#if defined(MNN_LLM_HAS_DUMP_CONFIG)
if (blockSelf->_llm) {
auto cfg = blockSelf->_llm->dump_config();
NSLog(@"[AudioWrapper] dump_config before inference (text): %s", cfg.c_str());
}
#else
NSLog(@"[AudioWrapper] dump_config not available in this build (text)");
#endif
NSLog(@"[AudioWrapper] Inference start (text): enable_audio_output=%@, talker_speaker=%@", blockSelf->_enableAudioOutput.load() ? @"YES" : @"NO", blockSelf->_talkerSpeaker ?: @"(nil)");
// Start inference with initial response processing
blockSelf->_llm->response(blockSelf->_history, &os, "<eop>", 1);
int current_size = 1;
int max_new_tokens = 999999;
// Continue generation with precise token-by-token control
while (!blockSelf->_shouldStopInference.load() &&
!blockSelf->_llm->stoped() &&
current_size < max_new_tokens) {
// Generate single token for maximum control
blockSelf->_llm->generate(1);
current_size++;
// Small delay to allow UI updates and stop signal processing
// std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
if (!blockSelf->_shouldStopInference.load() && blockSelf->_enableAudioOutput.load()) {
NSLog(@"[AudioWrapper] Text generation completed, generating waveform (enable_audio_output=YES)");
blockSelf->_llm->generateWavform();
NSLog(@"[AudioWrapper] generateWavform() called");
} else {
NSLog(@"[AudioWrapper] Skipping waveform generation: stop_requested=%@, enable_audio_output=%@",
blockSelf->_shouldStopInference.load() ? @"YES" : @"NO",
blockSelf->_enableAudioOutput.load() ? @"YES" : @"NO");
}
// Send appropriate end signal based on stop reason
if (output) {
dispatch_async(dispatch_get_main_queue(), ^{
if (blockSelf->_shouldStopInference.load()) {
output(@"<stoped>");
} else {
output(@"<eop>");
}
});
}
NSLog(@"Inference completed. Generated tokens: %d, Stopped by user: %s, Model stopped: %s",
current_size,
blockSelf->_shouldStopInference.load() ? "YES" : "NO",
blockSelf->_llm->stoped() ? "YES" : "NO");
} @catch (NSException *exception) {
NSLog(@"Exception during response generation: %@", exception.reason);
// Send end signal even on error to unlock UI
if (output) {
dispatch_async(dispatch_get_main_queue(), ^{
output(@"<eop>");
});
}
}
// Calculate performance metrics if requested
if (showPerformance && context) {
auto inference_end_time = std::chrono::high_resolution_clock::now();
auto total_inference_time = std::chrono::duration_cast<std::chrono::milliseconds>(
inference_end_time - inference_start_time
);
int prompt_len = 0;
int decode_len = 0;
int64_t prefill_time = 0;
int64_t decode_time = 0;
prompt_len += context->prompt_len;
decode_len += context->gen_seq_len;
prefill_time += context->prefill_us;
decode_time += context->decode_us;
// Convert microseconds to seconds
float prefill_s = static_cast<float>(prefill_time) / 1e6f;
float decode_s = static_cast<float>(decode_time) / 1e6f;
// Calculate speeds (tokens per second)
float prefill_speed = (prefill_s > 0.001f) ?
static_cast<float>(prompt_len) / prefill_s : 0.0f;
float decode_speed = (decode_s > 0.001f) ?
static_cast<float>(decode_len) / decode_s : 0.0f;
// Format performance results in 2-line format
std::ostringstream performance_output;
performance_output << "\n\nPrefill: " << std::fixed << std::setprecision(2) << prefill_s << "s, "
<< prompt_len << " tokens, " << std::setprecision(2) << prefill_speed << " tokens/s\n"
<< "Decode: " << std::fixed << std::setprecision(2) << decode_s << "s, "
<< decode_len << " tokens, " << std::setprecision(2) << decode_speed << " tokens/s\n";
// Output performance results on main queue
std::string perf_str = performance_output.str();
if (output) {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *perfOutput = [NSString stringWithUTF8String:perf_str.c_str()];
if (perfOutput) {
output(perfOutput);
}
});
}
}
}
}
@catch (NSException *exception) {
NSLog(@"Exception during inference: %@", exception.reason);
if (output) {
dispatch_async(dispatch_get_main_queue(), ^{
output([NSString stringWithFormat:@"Error: Inference failed - %@", exception.reason]);
});
}
}
@finally {
blockSelf->_isProcessing = false;
}
});
}
- (void)processMultimodalInput:(NSString *)promptTemplate
images:(NSDictionary<NSString *, UIImage *> *)images
withOutput:(OutputHandler)output
showPerformance:(BOOL)showPerformance {
if (!_llm) {
if (output) {
output(@"Error: Model not loaded. Please initialize the model first.");
}
return;
}
if (!promptTemplate || promptTemplate.length == 0) {
if (output) {
output(@"Error: Prompt template is empty.");
}
// return;
}
if (_isProcessing.load()) {
if (output) {
output(@"Error: Another inference is already in progress.");
}
return;
}
auto* context = _llm->getContext();
_isProcessing = true;
LLMInferenceEngineWrapper *blockSelf = self;
NSDictionary<NSString *, UIImage *> *imageDict = images ?: @{};
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
if (!blockSelf || !blockSelf->_llm) {
return;
}
@try {
OptimizedLlmStreamBuffer::CallBack callback = [output](const char* str, size_t len) {
if (output && str && len > 0) {
@autoreleasepool {
NSString *nsOutput = [[NSString alloc] initWithBytes:str
length:len
encoding:NSUTF8StringEncoding];
if (nsOutput) {
dispatch_async(dispatch_get_main_queue(), ^{
output(nsOutput);
});
}
}
}
};
OptimizedLlmStreamBuffer streambuf(callback);
std::ostream os(&streambuf);
std::string original_prompt = promptTemplate ? [promptTemplate UTF8String] : "";
std::string sanitized_prompt = original_prompt;
MNN::Transformer::MultimodalPrompt multimodal_input;
std::vector<MNN::Express::VARP> retainedImageVars;
retainedImageVars.reserve(imageDict.count);
auto addImageForKey = [&](const std::string& key, UIImage *image, bool removeOnFailure) {
if (!image) {
if (removeOnFailure) {
RemoveImagePlaceholder(sanitized_prompt, key);
}
return false;
}
auto varp = [blockSelf convertUIImageToVARP:image];
if (!varp.get()) {
if (removeOnFailure) {
RemoveImagePlaceholder(sanitized_prompt, key);
}
NSLog(@"Warning: Failed to convert image for key %@", [NSString stringWithUTF8String:key.c_str()]);
return false;
}
retainedImageVars.push_back(varp);
MNN::Transformer::PromptImagePart imagePart;
imagePart.image_data = varp;
auto info = varp->getInfo();
if (info && info->dim.size() >= 3) {
imagePart.height = static_cast<int>(info->dim[1]);
imagePart.width = static_cast<int>(info->dim[2]);
} else {
CGFloat scale = image.scale;
if (scale <= 0) { scale = 0.6; }
imagePart.width = static_cast<int>(image.size.width * scale);
imagePart.height = static_cast<int>(image.size.height * scale);
}
multimodal_input.images[key] = imagePart;
return true;
};
auto placeholderKeys = ExtractImagePlaceholders(original_prompt);
std::unordered_set<std::string> processedKeys;
for (const auto& key : placeholderKeys) {
if (processedKeys.count(key) > 0) {
continue;
}
processedKeys.insert(key);
NSString *nsKey = [NSString stringWithUTF8String:key.c_str()];
UIImage *image = imageDict[nsKey];
if (!image) {
image = LoadUIImageFromPath(key);
if (!image) {
NSLog(@"[Multimodal] Image not found for key %@", nsKey ?: @"(null)");
}
}
if (!addImageForKey(key, image, true)) {
NSLog(@"Warning: Missing image for placeholder %@", nsKey ?: @"(null)");
} else {
NSLog(@"[Multimodal] Attached image placeholder %@", nsKey ?: @"(null)");
}
}
auto videoKeys = ExtractVideoPlaceholders(original_prompt);
int autoImageIndex = 0;
for (const auto& videoPath : videoKeys) {
NSString *nsVideoPath = [NSString stringWithUTF8String:videoPath.c_str()];
if (!nsVideoPath) {
ReplaceVideoPlaceholder(sanitized_prompt, videoPath, "");
continue;
}
int maxFrames = std::max(1, _videoMaxFrames.load());
NSArray<UIImage *> *frames = ExtractFramesFromVideoAtPath(nsVideoPath, maxFrames);
if (!frames || frames.count == 0) {
NSLog(@"Warning: Failed to extract frames for video %@", nsVideoPath);
ReplaceVideoPlaceholder(sanitized_prompt, videoPath, "");
continue;
}
NSLog(@"[Multimodal] Extracted %lu frames for %@", (unsigned long)frames.count, nsVideoPath);
std::string replacement;
for (UIImage *frame in frames) {
std::string autoKey = "video_auto_" + std::to_string(autoImageIndex++);
if (addImageForKey(autoKey, frame, false)) {
replacement += "<img>" + autoKey + "</img>";
NSLog(@"[Multimodal] Added video frame placeholder %s", autoKey.c_str());
}
}
ReplaceVideoPlaceholder(sanitized_prompt, videoPath, replacement);
}
auto audioKeys = ExtractAudioPlaceholders(original_prompt);
std::unordered_set<std::string> processedAudioKeys;
for (const auto& key : audioKeys) {
if (key.empty() || processedAudioKeys.count(key) > 0) {
continue;
}
processedAudioKeys.insert(key);
NSString *nsPath = [NSString stringWithUTF8String:key.c_str()];
if (![[NSFileManager defaultManager] fileExistsAtPath:nsPath]) {
NSLog(@"Warning: Audio file not found for placeholder %@", nsPath);
RemoveAudioPlaceholder(sanitized_prompt, key);
continue;
}
NSLog(@"[Multimodal] Attached audio placeholder %@", nsPath);
MNN::Transformer::PromptAudioPart audioPart;
audioPart.file_path = key;
multimodal_input.audios[key] = audioPart;
}
if (sanitized_prompt.empty() && multimodal_input.images.empty() && multimodal_input.audios.empty()) {
sanitized_prompt = " ";
}
multimodal_input.prompt_template = sanitized_prompt;
{
std::lock_guard<std::mutex> lock(blockSelf->_historyMutex);
blockSelf->_history.emplace_back(ChatMessage("user", sanitized_prompt));
}
blockSelf->_shouldStopInference = false;
bool useMultimodal = !multimodal_input.images.empty();
// dump_config may not be available in all builds of MNN; guard the call
#if defined(MNN_LLM_HAS_DUMP_CONFIG)
if (blockSelf->_llm) {
auto cfg = blockSelf->_llm->dump_config();
NSLog(@"[AudioWrapper] dump_config before inference (multimodal): %s", cfg.c_str());
}
#else
NSLog(@"[AudioWrapper] dump_config not available in this build (multimodal)");
#endif
NSLog(@"[AudioWrapper] Inference start (multimodal): enable_audio_output=%@, talker_speaker=%@", blockSelf->_enableAudioOutput.load() ? @"YES" : @"NO", blockSelf->_talkerSpeaker ?: @"(nil)");
if (useMultimodal) {
blockSelf->_llm->response(multimodal_input, &os, "<eop>", 1);
} else {
blockSelf->_llm->response(blockSelf->_history, &os, "<eop>", 1);
}
int current_size = 1;
const int max_new_tokens = 999999;
while (!blockSelf->_shouldStopInference.load() &&
!blockSelf->_llm->stoped() &&
current_size < max_new_tokens) {
blockSelf->_llm->generate(1);
current_size++;
}
if (!blockSelf->_shouldStopInference.load() && blockSelf->_enableAudioOutput.load()) {
NSLog(@"[AudioWrapper] Multimodal text generation completed, generating waveform (enable_audio_output=YES)");
blockSelf->_llm->generateWavform();
NSLog(@"[AudioWrapper] generateWavform() called");
} else {
NSLog(@"[AudioWrapper] Skipping waveform generation: stop_requested=%@, enable_audio_output=%@",
blockSelf->_shouldStopInference.load() ? @"YES" : @"NO",
blockSelf->_enableAudioOutput.load() ? @"YES" : @"NO");
}
if (output) {
dispatch_async(dispatch_get_main_queue(), ^{
if (blockSelf->_shouldStopInference.load()) {
output(@"<stoped>");
} else {
output(@"<eop>");
}
});
}
if (showPerformance && context) {
auto inference_end_time = std::chrono::high_resolution_clock::now();
(void)inference_end_time;
int prompt_len = context->prompt_len;
int decode_len = context->gen_seq_len;
int64_t prefill_time = context->prefill_us;
int64_t decode_time = context->decode_us;
float prefill_s = static_cast<float>(prefill_time) / 1e6f;
float decode_s = static_cast<float>(decode_time) / 1e6f;
float prefill_speed = (prefill_s > 0.001f) ?
static_cast<float>(prompt_len) / prefill_s : 0.0f;
float decode_speed = (decode_s > 0.001f) ?
static_cast<float>(decode_len) / decode_s : 0.0f;
std::ostringstream performance_output;
performance_output << "\n\nPrefill: " << std::fixed << std::setprecision(2) << prefill_s << "s, "
<< prompt_len << " tokens, " << std::setprecision(2) << prefill_speed << " tokens/s\n"
<< "Decode: " << std::fixed << std::setprecision(2) << decode_s << "s, "
<< decode_len << " tokens, " << std::setprecision(2) << decode_speed << " tokens/s\n";
std::string perf_str = performance_output.str();
if (output) {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *perfOutput = [NSString stringWithUTF8String:perf_str.c_str()];
if (perfOutput) {
output(perfOutput);
}
});
}
}
}
@catch (NSException *exception) {
NSLog(@"Exception during multimodal inference: %@", exception.reason);
if (output) {
dispatch_async(dispatch_get_main_queue(), ^{
output([NSString stringWithFormat:@"Error: Inference failed - %@", exception.reason]);
});
}
}
@finally {
blockSelf->_isProcessing = false;
}
});
}
- (void)setVideoMaxFrames:(NSInteger)frames {
int clamped = (int)std::max(1, std::min(64, (int)frames));
_videoMaxFrames.store(clamped);
NSLog(@"Updated video max frames to %d", clamped);
}
- (void)setEnableAudioOutput:(BOOL)enable {
NSLog(@"[AudioWrapper] setEnableAudioOutput: %@", enable ? @"YES" : @"NO");
_enableAudioOutput = enable;
if (_llm) {
// Update config with enable_audio_output
NSString *configStr = [NSString stringWithFormat:@"{\"enable_audio_output\":%s}", enable ? "true" : "false"];
std::string stdConfig = [configStr UTF8String];
NSLog(@"[AudioWrapper] Updating LLM config: %s", stdConfig.c_str());
_llm->set_config(stdConfig);
} else {
NSLog(@"[AudioWrapper] LLM not initialized, config will be applied when model loads");
}
}
- (void)setTalkerSpeaker:(NSString *)speaker {
NSLog(@"[AudioWrapper] setTalkerSpeaker: %@", speaker ?: @"(nil)");
_talkerSpeaker = [speaker copy];
if (_llm) {
// Update config with talker_speaker
NSString *escapedSpeaker = [speaker stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
NSString *configStr = [NSString stringWithFormat:@"{\"talker_speaker\":\"%@\"}", escapedSpeaker];
std::string stdConfig = [configStr UTF8String];
NSLog(@"[AudioWrapper] Updating LLM config: %s", stdConfig.c_str());
_llm->set_config(stdConfig);
} else {
NSLog(@"[AudioWrapper] LLM not initialized, config will be applied when model loads");
}
}
- (void)setAudioWaveformCallback:(BOOL (^)(const float *, size_t, BOOL))callback {
NSLog(@"[AudioWrapper] setAudioWaveformCallback: callback=%@, llm=%@", callback ? @"YES" : @"NO", _llm ? @"YES" : @"NO");
_audioWaveformCallback = [callback copy];
if (_llm && callback) {
NSLog(@"[AudioWrapper] Registering waveform callback with LLM");
static std::atomic<int> s_waveCallbackCount{0};
_llm->setWavformCallback([self](const float *ptr, size_t size, bool last_chunk) -> bool {
int cbId = ++s_waveCallbackCount;
if (!self->_enableAudioOutput.load()) {
NSLog(@"[AudioWrapper] Waveform callback #%d: audio output disabled, skipping (size=%zu)", cbId, size);
return false;
}
if (self->_shouldStopInference.load()) {
NSLog(@"[AudioWrapper] Waveform callback #%d: inference stopped, skipping (size=%zu)", cbId, size);
return false;
}
if (self->_audioWaveformCallback) {
// Check if data is valid
if (ptr == nullptr || size == 0) {
NSLog(@"[AudioWrapper] Waveform callback #%d: invalid data (ptr=%p, size=%zu)", cbId, ptr, size);
return false;
}
// Log first few samples for debugging
if (size > 0) {
float firstSample = ptr[0];
bool hasNaN = false;
bool hasInf = false;
bool hasValid = false;
// Check first 10 samples for data quality
for (size_t i = 0; i < std::min(size, (size_t)10); i++) {
if (std::isnan(ptr[i])) hasNaN = true;
if (std::isinf(ptr[i])) hasInf = true;
if (std::isfinite(ptr[i]) && std::abs(ptr[i]) > 0.0001f) hasValid = true;
}
NSLog(@"[AudioWrapper] Waveform callback #%d: forwarding to Swift (size=%zu, last_chunk=%s, first_sample=%.6f, hasNaN=%s, hasInf=%s, hasValid=%s)",
cbId, size, last_chunk ? "YES" : "NO", firstSample,
hasNaN ? "YES" : "NO", hasInf ? "YES" : "NO", hasValid ? "YES" : "NO");
}
BOOL shouldStop = self->_audioWaveformCallback(ptr, size, last_chunk ? YES : NO);
if (last_chunk) {
NSLog(@"[AudioWrapper] Waveform callback tail at #%d", cbId);
}
return shouldStop;
}
NSLog(@"[AudioWrapper] Waveform callback #%d: no Swift callback registered (size=%zu)", cbId, size);
return false;
});
NSLog(@"[AudioWrapper] Waveform callback registered successfully");
} else if (_llm && !callback) {
// Clear callback
NSLog(@"[AudioWrapper] Clearing waveform callback");
_llm->setWavformCallback(nullptr);
} else {
NSLog(@"[AudioWrapper] Cannot register callback: llm=%@, callback=%@", _llm ? @"YES" : @"NO", callback ? @"YES" : @"NO");
}
}
- (MNN::Express::VARP)convertUIImageToVARP:(UIImage *)image {
if (!image) {
return MNN::Express::VARP();
}
#if TARGET_OS_IPHONE
CGImageRef cgImage = image.CGImage;
#else
NSRect proposedRect = NSMakeRect(0, 0, image.size.width, image.size.height);
CGImageRef cgImage = [image CGImageForProposedRect:&proposedRect context:nil hints:nil];
#endif
if (!cgImage) {
return MNN::Express::VARP();
}
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
NSLog(@"[Multimodal] convertUIImageToVARP source size = %zux%zu", width, height);
if (width == 0 || height == 0) {
return MNN::Express::VARP();
}
size_t bytesPerRow = width * 4;
std::vector<uint8_t> rawData(height * bytesPerRow);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(
rawData.data(),
width,
height,
8,
bytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big
);
CGColorSpaceRelease(colorSpace);
if (!context) {
return MNN::Express::VARP();
}
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
CGContextRelease(context);
#if DEBUG
if (!rawData.empty()) {
uint8_t r = rawData[0];
uint8_t g = rawData[1];
uint8_t b = rawData[2];
uint8_t a = rawData.size() > 3 ? rawData[3] : 255;
NSLog(@"[Multimodal] Raw pixel RGBA = (%u,%u,%u,%u)", r, g, b, a);
}
#endif
auto varp = MNN::Express::_Input(
{1, static_cast<int>(height), static_cast<int>(width), 3},
MNN::Express::NHWC,
halide_type_of<float>()
);
auto ptr = varp->writeMap<float>();
if (!ptr) {
return MNN::Express::VARP();
}
for (size_t y = 0; y < height; ++y) {
for (size_t x = 0; x < width; ++x) {
size_t byteIndex = y * bytesPerRow + x * 4;
size_t pixelIndex = (y * width + x) * 3;
ptr[pixelIndex + 0] = rawData[byteIndex + 0] / 255.0f;
ptr[pixelIndex + 1] = rawData[byteIndex + 1] / 255.0f;
ptr[pixelIndex + 2] = rawData[byteIndex + 2] / 255.0f;
}
}
#if DEBUG
NSLog(@"[Multimodal] VARP first pixel floats (R,G,B)=(%.4f, %.4f, %.4f) for size %zux%zu",
ptr[0], ptr[1], ptr[2], width, height);
#endif
return varp;
}
/**
* Performs benchmark testing with enhanced error handling and reporting
*
* @param os Output stream for benchmark results
*/
- (void)performBenchmarkWithOutput:(std::ostream *)os {
@try {
std::string model_dir = [[[NSBundle mainBundle] bundlePath] UTF8String];
std::string prompt_file = model_dir + "/bench.txt";
std::ifstream prompt_fs(prompt_file);
if (!prompt_fs.is_open()) {
*os << "Error: Could not open benchmark file at " << prompt_file << std::endl;
return;
}
std::vector<std::string> prompts;
std::string prompt;
while (std::getline(prompt_fs, prompt)) {
if (prompt.empty() || prompt.substr(0, 1) == "#") {
continue;
}
// Process escape sequences
std::string::size_type pos = 0;
while ((pos = prompt.find("\\n", pos)) != std::string::npos) {
prompt.replace(pos, 2, "\n");
pos += 1;
}
prompts.push_back(prompt);
}
if (prompts.empty()) {
*os << "Error: No valid prompts found in benchmark file" << std::endl;
return;
}
// Performance metrics
int prompt_len = 0;
int decode_len = 0;
int64_t prefill_time = 0;
int64_t decode_time = 0;
auto context = _llm->getContext();
auto start_time = std::chrono::high_resolution_clock::now();
for (const auto& p : prompts) {
_llm->response(p, os, "\n");
prompt_len += context->prompt_len;
decode_len += context->gen_seq_len;
prefill_time += context->prefill_us;
decode_time += context->decode_us;
}
auto end_time = std::chrono::high_resolution_clock::now();
auto total_time = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
float prefill_s = prefill_time / 1e6;
float decode_s = decode_time / 1e6;
*os << "\n#################################\n"
<< "Benchmark Results:\n"
<< "Total prompts processed: " << prompts.size() << "\n"
<< "Total time: " << total_time.count() << " ms\n"
<< "Prompt tokens: " << prompt_len << "\n"
<< "Decode tokens: " << decode_len << "\n"
<< "Prefill time: " << std::fixed << std::setprecision(2) << prefill_s << " s\n"
<< "Decode time: " << std::fixed << std::setprecision(2) << decode_s << " s\n"
<< "Prefill speed: " << std::fixed << std::setprecision(2)
<< (prefill_s > 0 ? prompt_len / prefill_s : 0) << " tok/s\n"
<< "Decode speed: " << std::fixed << std::setprecision(2)
<< (decode_s > 0 ? decode_len / decode_s : 0) << " tok/s\n"
<< "#################################\n";
*os << "<eop>";
}
@catch (NSException *exception) {
*os << "Error during benchmark: " << [exception.reason UTF8String] << std::endl;
}
}
/**
* Enhanced deallocation with proper cleanup and timeout
*/
- (void)dealloc {
NSLog(@"LLMInferenceEngineWrapper deallocating...");
// Actively cancel all operations first
[self cancelInference];
// Wait for any ongoing processing to complete with timeout
int timeout = 100; // 1 second timeout (100 * 10ms)
while ((_isProcessing.load() || _isBenchmarkRunning.load()) && timeout > 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
timeout--;
}
if (timeout <= 0) {
NSLog(@"Warning: Dealloc timeout, forcing cleanup");
_isProcessing = false;
_isBenchmarkRunning = false;
}
{
std::lock_guard<std::mutex> lock(_historyMutex);
_history.clear();
}
_llm.reset();
NSLog(@"LLMInferenceEngineWrapper deallocation complete");
#if !__has_feature(objc_arc)
[super dealloc];
#endif
}
/**
* Enhanced chat history initialization with thread safety
*
* @param chatHistory Vector of strings representing alternating user/assistant messages
*/
- (void)init:(const std::vector<std::string>&)chatHistory {
std::lock_guard<std::mutex> lock(_historyMutex);
_history.clear();
_history.emplace_back("system", "You are a helpful assistant.");
for (size_t i = 0; i < chatHistory.size(); ++i) {
_history.emplace_back(i % 2 == 0 ? "user" : "assistant", chatHistory[i]);
}
NSLog(@"Chat history initialized with %zu messages", chatHistory.size());
}
/**
* Enhanced method for adding chat prompts from array with validation
*
* @param array NSArray containing NSDictionary objects with chat messages
*/
- (void)addPromptsFromArray:(NSArray<NSDictionary *> *)array {
if (!array || array.count == 0) {
NSLog(@"Warning: Empty or nil chat history array provided");
return;
}
std::lock_guard<std::mutex> lock(_historyMutex);
_history.clear();
for (NSDictionary *dict in array) {
if ([dict isKindOfClass:[NSDictionary class]]) {
[self addPromptsFromDictionary:dict];
} else {
NSLog(@"Warning: Invalid dictionary in chat history array");
}
}
NSLog(@"Added prompts from array with %lu items", (unsigned long)array.count);
}
/**
* Enhanced method for adding prompts from dictionary with validation
*
* @param dictionary NSDictionary containing role-message key-value pairs
*/
- (void)addPromptsFromDictionary:(NSDictionary *)dictionary {
if (!dictionary || dictionary.count == 0) {
return;
}
for (NSString *key in dictionary) {
NSString *value = dictionary[key];
if (![key isKindOfClass:[NSString class]] || ![value isKindOfClass:[NSString class]]) {
NSLog(@"Warning: Invalid key-value pair in chat dictionary");
continue;
}
std::string keyString = [key UTF8String];
std::string valueString = [value UTF8String];
_history.emplace_back(ChatMessage(keyString, valueString));
}
}
/**
* Check if model is ready for inference
*
* @return YES if model is loaded and ready
*/
- (BOOL)isModelReady {
return _llm != nullptr && !_isProcessing.load();
}
/**
* Get current processing status
*
* @return YES if currently processing an inference request
*/
- (BOOL)isProcessing {
return _isProcessing.load();
}
/**
* Cancel ongoing inference (if supported)
*/
- (void)cancelInference {
NSLog(@"Cancelling inference...");
// Set all stop flags to true
_shouldStopInference = true;
_shouldStopBenchmark = true;
// Force set processing states to false for immediate cleanup
_isProcessing = false;
_isBenchmarkRunning = false;
NSLog(@"Inference cancellation completed - all flags set");
}
/**
* Get chat history count
*
* @return Number of messages in chat history
*/
- (NSUInteger)getChatHistoryCount {
std::lock_guard<std::mutex> lock(_historyMutex);
return _history.size();
}
/**
* Clear chat history
*/
- (void)clearChatHistory {
std::lock_guard<std::mutex> lock(_historyMutex);
_history.clear();
NSLog(@"Chat history cleared");
}
/**
* Initialize benchmark result structure
*/
- (BenchmarkResultCpp)initializeBenchmarkResult:(int)nPrompt nGenerate:(int)nGenerate nRepeat:(int)nRepeat kvCache:(bool)kvCache {
BenchmarkResultCpp result;
result.prompt_tokens = nPrompt;
result.generate_tokens = nGenerate;
result.repeat_count = nRepeat;
result.kv_cache_enabled = kvCache;
result.success = false;
return result;
}
/**
* Initialize LLM for benchmark and verify it's ready
*/
- (BOOL)initializeLlmForBenchmark:(BenchmarkResultCpp&)result callback:(const BenchmarkCallback&)callback {
if (!_llm) {
result.error_message = "LLM object is not initialized";
if (callback.onError) callback.onError(result.error_message);
return NO;
}
// Verify LLM context is valid before proceeding
auto context = _llm->getContext();
if (!context) {
result.error_message = "LLM context is not valid - model may not be properly loaded";
if (callback.onError) callback.onError(result.error_message);
return NO;
}
// Clear chat history for clean benchmark
[self clearChatHistory];
// Re-verify context after reset
context = _llm->getContext();
if (!context) {
result.error_message = "LLM context became invalid after reset";
if (callback.onError) callback.onError(result.error_message);
return NO;
}
return YES;
}
/**
* Report benchmark progress
*/
- (void)reportBenchmarkProgress:(int)iteration nRepeat:(int)nRepeat nPrompt:(int)nPrompt nGenerate:(int)nGenerate callback:(const BenchmarkCallback&)callback {
if (callback.onProgress) {
BenchmarkProgressInfoCpp progressInfo;
if (iteration == 0) {
progressInfo.progress = 0;
progressInfo.statusMessage = "Warming up...";
progressInfo.progressType = 2; // BenchmarkProgressTypeWarmingUp
} else {
progressInfo.progress = (iteration * 100) / nRepeat;
progressInfo.statusMessage = "Running test " + std::to_string(iteration) + "/" + std::to_string(nRepeat) +
" (prompt=" + std::to_string(nPrompt) + ", generate=" + std::to_string(nGenerate) + ")";
progressInfo.progressType = 3; // BenchmarkProgressTypeRunningTest
}
// Set structured data
progressInfo.currentIteration = iteration;
progressInfo.totalIterations = nRepeat;
progressInfo.nPrompt = nPrompt;
progressInfo.nGenerate = nGenerate;
callback.onProgress(progressInfo);
}
}
/**
* Run KV cache test iteration
*/
- (BOOL)runKvCacheTest:(int)iteration nPrompt:(int)nPrompt nGenerate:(int)nGenerate
startTime:(std::chrono::high_resolution_clock::time_point)start_time
result:(BenchmarkResultCpp&)result callback:(const BenchmarkCallback&)callback {
const int tok = 16;
std::vector<int> tokens(nPrompt, tok);
// Validate token vector
if (tokens.empty() || nPrompt <= 0) {
result.error_message = "Invalid token configuration for kv-cache test";
if (callback.onError) callback.onError(result.error_message);
return NO;
}
_llm->response(tokens, nullptr, nullptr, nGenerate);
// Re-get context after response to ensure it's still valid
auto context = _llm->getContext();
if (!context) {
result.error_message = "Context became invalid after response in kv-cache test " + std::to_string(iteration);
if (callback.onError) callback.onError(result.error_message);
return NO;
}
if (iteration > 0) { // Exclude the first performance value
auto end_time = std::chrono::high_resolution_clock::now();
[self processBenchmarkResults:context->prefill_us decodeTime:context->decode_us
startTime:start_time endTime:end_time iteration:iteration
nPrompt:nPrompt nGenerate:nGenerate result:result
callback:callback isKvCache:true];
}
return YES;
}
/**
* Run llama-bench test iteration (without kv cache)
*/
- (BOOL)runLlamaBenchTest:(int)iteration nPrompt:(int)nPrompt nGenerate:(int)nGenerate
startTime:(std::chrono::high_resolution_clock::time_point)start_time
result:(BenchmarkResultCpp&)result callback:(const BenchmarkCallback&)callback {
const int tok = 500;
int64_t prefill_us = 0;
int64_t decode_us = 0;
std::vector<int> tokens(nPrompt, tok);
std::vector<int> tokens1(1, tok);
// Validate token vectors
if ((nPrompt > 0 && tokens.empty()) || tokens1.empty()) {
result.error_message = "Invalid token configuration for llama-bench test " + std::to_string(iteration);
if (callback.onError) callback.onError(result.error_message);
return NO;
}
NSLog(@"runLlamaBenchTest nPrompt:%d, nGenerate:%d", nPrompt, nGenerate);
if (nPrompt > 0) {
NSLog(@"runLlamaBenchTest prefill begin");
_llm->response(tokens, nullptr, nullptr, 1);
NSLog(@"runLlamaBenchTest prefill end");
auto context = _llm->getContext();
if (!context) {
result.error_message = "Context became invalid after prefill response in llama-bench test " + std::to_string(iteration);
if (callback.onError) callback.onError(result.error_message);
return NO;
}
prefill_us = context->prefill_us;
}
if (nGenerate > 0) {
NSLog(@"runLlamaBenchTest generate begin");
_llm->response(tokens1, nullptr, nullptr, nGenerate);
NSLog(@"runLlamaBenchTest generate end");
auto context = _llm->getContext();
if (!context) {
result.error_message = "Context became invalid after decode response in llama-bench test " + std::to_string(iteration);
if (callback.onError) callback.onError(result.error_message);
return NO;
}
decode_us = context->decode_us;
}
if (iteration > 0) { // Exclude the first performance value
auto end_time = std::chrono::high_resolution_clock::now();
[self processBenchmarkResults:prefill_us decodeTime:decode_us
startTime:start_time endTime:end_time iteration:iteration
nPrompt:nPrompt nGenerate:nGenerate result:result
callback:callback isKvCache:false];
result.sample_times_us.push_back(prefill_us + decode_us);
result.decode_times_us.push_back(decode_us);
result.prefill_times_us.push_back(prefill_us);
}
return YES;
}
/**
* Process and report benchmark results
*/
- (void)processBenchmarkResults:(int64_t)prefillTime decodeTime:(int64_t)decodeTime
startTime:(std::chrono::high_resolution_clock::time_point)start_time
endTime:(std::chrono::high_resolution_clock::time_point)end_time
iteration:(int)iteration nPrompt:(int)nPrompt nGenerate:(int)nGenerate
result:(BenchmarkResultCpp&)result callback:(const BenchmarkCallback&)callback
isKvCache:(bool)isKvCache {
auto runTime = std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time).count();
if (isKvCache) {
result.prefill_times_us.push_back(prefillTime);
result.decode_times_us.push_back(decodeTime);
}
// Convert times to seconds
float runTimeSeconds = runTime / 1000000.0f;
float prefillTimeSeconds = prefillTime / 1000000.0f;
float decodeTimeSeconds = decodeTime / 1000000.0f;
// Calculate speeds (tokens per second)
float prefillSpeed = (prefillTime > 0 && nPrompt > 0) ? ((float)nPrompt / prefillTimeSeconds) : 0.0f;
float decodeSpeed = (decodeTime > 0 && nGenerate > 0) ? ((float)nGenerate / decodeTimeSeconds) : 0.0f;
// Report detailed results with structured data
BenchmarkProgressInfoCpp detailedInfo;
detailedInfo.progress = (iteration * 100) / result.repeat_count;
detailedInfo.progressType = 3; // BenchmarkProgressTypeRunningTest
detailedInfo.currentIteration = iteration;
detailedInfo.totalIterations = result.repeat_count;
detailedInfo.nPrompt = nPrompt;
detailedInfo.nGenerate = nGenerate;
detailedInfo.runTimeSeconds = runTimeSeconds;
detailedInfo.prefillTimeSeconds = prefillTimeSeconds;
detailedInfo.decodeTimeSeconds = decodeTimeSeconds;
detailedInfo.prefillSpeed = prefillSpeed;
detailedInfo.decodeSpeed = decodeSpeed;
// Format detailed message
char detailedMsg[1024];
snprintf(detailedMsg, sizeof(detailedMsg),
"BenchmarkService: Native Progress [%dp+%dg] (%d%%): Running test %d/%d (prompt=%d, generate=%d) runTime:%.3fs, prefillTime:%.3fs, decodeTime:%.3fs, prefillSpeed:%.2f tok/s, decodeSpeed:%.2f tok/s",
nPrompt, nGenerate, detailedInfo.progress, iteration, result.repeat_count, nPrompt, nGenerate,
runTimeSeconds, prefillTimeSeconds, decodeTimeSeconds, prefillSpeed, decodeSpeed);
detailedInfo.statusMessage = std::string(detailedMsg);
NSLog(@"%s", detailedMsg);
if (callback.onProgress) {
callback.onProgress(detailedInfo);
}
if (callback.onIterationComplete) {
callback.onIterationComplete(std::string(detailedMsg));
}
}
/**
* Core benchmark implementation
*/
- (BenchmarkResultCpp)runBenchmarkCore:(int)backend threads:(int)threads useMmap:(bool)useMmap power:(int)power
precision:(int)precision memory:(int)memory dynamicOption:(int)dynamicOption
nPrompt:(int)nPrompt nGenerate:(int)nGenerate nRepeat:(int)nRepeat
kvCache:(bool)kvCache callback:(const BenchmarkCallback&)callback {
NSLog(@"BENCHMARK: runBenchmark() STARTED!");
NSLog(@"BENCHMARK: Parameters - nPrompt=%d, nGenerate=%d, nRepeat=%d, kvCache=%s",
nPrompt, nGenerate, nRepeat, kvCache ? "true" : "false");
// Initialize result structure
NSLog(@"BENCHMARK: Initializing benchmark result structure");
BenchmarkResultCpp result = [self initializeBenchmarkResult:nPrompt nGenerate:nGenerate nRepeat:nRepeat kvCache:kvCache];
// Initialize LLM for benchmark
NSLog(@"BENCHMARK: About to initialize LLM for benchmark");
if (![self initializeLlmForBenchmark:result callback:callback]) {
NSLog(@"BENCHMARK: initializeLlmForBenchmark FAILED!");
return result;
}
NSLog(@"BENCHMARK: initializeLlmForBenchmark SUCCESS - entering benchmark loop");
// Run benchmark iterations
NSLog(@"BENCHMARK: Starting benchmark loop for %d iterations", nRepeat + 1);
for (int i = 0; i < nRepeat + 1; ++i) {
if (_shouldStopBenchmark.load()) {
result.error_message = "Benchmark stopped by user";
if (callback.onError) callback.onError(result.error_message);
return result;
}
NSLog(@"BENCHMARK: Starting iteration %d/%d", i, nRepeat);
auto start_time = std::chrono::high_resolution_clock::now();
// Report progress
NSLog(@"BENCHMARK: Reporting progress for iteration %d", i);
[self reportBenchmarkProgress:i nRepeat:nRepeat nPrompt:nPrompt nGenerate:nGenerate callback:callback];
// Run the actual test
BOOL success;
if (kvCache) {
success = [self runKvCacheTest:i nPrompt:nPrompt nGenerate:nGenerate startTime:start_time result:result callback:callback];
} else {
success = [self runLlamaBenchTest:i nPrompt:nPrompt nGenerate:nGenerate startTime:start_time result:result callback:callback];
}
if (!success) {
return result;
}
}
// Report completion
if (callback.onProgress) {
BenchmarkProgressInfoCpp completionInfo;
completionInfo.progress = 100;
completionInfo.statusMessage = "Benchmark completed!";
completionInfo.progressType = 5; // BenchmarkProgressTypeCompleted
callback.onProgress(completionInfo);
}
result.success = true;
return result;
}
/**
* Convert C++ BenchmarkProgressInfoCpp to Objective-C BenchmarkProgressInfo
*/
- (BenchmarkProgressInfo *)convertProgressInfo:(const BenchmarkProgressInfoCpp&)cppInfo {
BenchmarkProgressInfo *objcInfo = [[BenchmarkProgressInfo alloc] init];
objcInfo.progress = cppInfo.progress;
objcInfo.statusMessage = [NSString stringWithUTF8String:cppInfo.statusMessage.c_str()];
objcInfo.progressType = (BenchmarkProgressType)cppInfo.progressType;
objcInfo.currentIteration = cppInfo.currentIteration;
objcInfo.totalIterations = cppInfo.totalIterations;
objcInfo.nPrompt = cppInfo.nPrompt;
objcInfo.nGenerate = cppInfo.nGenerate;
objcInfo.runTimeSeconds = cppInfo.runTimeSeconds;
objcInfo.prefillTimeSeconds = cppInfo.prefillTimeSeconds;
objcInfo.decodeTimeSeconds = cppInfo.decodeTimeSeconds;
objcInfo.prefillSpeed = cppInfo.prefillSpeed;
objcInfo.decodeSpeed = cppInfo.decodeSpeed;
return objcInfo;
}
/**
* Convert C++ BenchmarkResultCpp to Objective-C BenchmarkResult
*/
- (BenchmarkResult *)convertBenchmarkResult:(const BenchmarkResultCpp&)cppResult {
BenchmarkResult *objcResult = [[BenchmarkResult alloc] init];
objcResult.success = cppResult.success;
if (!cppResult.error_message.empty()) {
objcResult.errorMessage = [NSString stringWithUTF8String:cppResult.error_message.c_str()];
}
// Convert timing arrays
NSMutableArray<NSNumber *> *prefillTimes = [[NSMutableArray alloc] init];
for (int64_t time : cppResult.prefill_times_us) {
[prefillTimes addObject:@(time)];
}
objcResult.prefillTimesUs = [prefillTimes copy];
NSMutableArray<NSNumber *> *decodeTimes = [[NSMutableArray alloc] init];
for (int64_t time : cppResult.decode_times_us) {
[decodeTimes addObject:@(time)];
}
objcResult.decodeTimesUs = [decodeTimes copy];
NSMutableArray<NSNumber *> *sampleTimes = [[NSMutableArray alloc] init];
for (int64_t time : cppResult.sample_times_us) {
[sampleTimes addObject:@(time)];
}
objcResult.sampleTimesUs = [sampleTimes copy];
objcResult.promptTokens = cppResult.prompt_tokens;
objcResult.generateTokens = cppResult.generate_tokens;
objcResult.repeatCount = cppResult.repeat_count;
objcResult.kvCacheEnabled = cppResult.kv_cache_enabled;
return objcResult;
}
// MARK: - Public Benchmark Methods
/**
* Run official benchmark following llm_bench.cpp approach
*/
- (void)runOfficialBenchmarkWithBackend:(NSInteger)backend
threads:(NSInteger)threads
useMmap:(BOOL)useMmap
power:(NSInteger)power
precision:(NSInteger)precision
memory:(NSInteger)memory
dynamicOption:(NSInteger)dynamicOption
nPrompt:(NSInteger)nPrompt
nGenerate:(NSInteger)nGenerate
nRepeat:(NSInteger)nRepeat
kvCache:(BOOL)kvCache
progressCallback:(BenchmarkProgressCallback _Nullable)progressCallback
errorCallback:(BenchmarkErrorCallback _Nullable)errorCallback
iterationCompleteCallback:(BenchmarkIterationCompleteCallback _Nullable)iterationCompleteCallback
completeCallback:(BenchmarkCompleteCallback _Nullable)completeCallback {
if (_isBenchmarkRunning.load()) {
if (errorCallback) {
errorCallback(@"Benchmark is already running");
}
return;
}
if (!_llm) {
if (errorCallback) {
errorCallback(@"Model is not initialized");
}
return;
}
_isBenchmarkRunning = true;
_shouldStopBenchmark = false;
// Run benchmark in background thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@try {
// Create C++ callback structure
BenchmarkCallback cppCallback;
cppCallback.onProgress = [progressCallback, self](const BenchmarkProgressInfoCpp& progressInfo) {
if (progressCallback) {
BenchmarkProgressInfo *objcProgressInfo = [self convertProgressInfo:progressInfo];
dispatch_async(dispatch_get_main_queue(), ^{
progressCallback(objcProgressInfo);
});
}
};
cppCallback.onError = [errorCallback](const std::string& error) {
if (errorCallback) {
NSString *errorStr = [NSString stringWithUTF8String:error.c_str()];
dispatch_async(dispatch_get_main_queue(), ^{
errorCallback(errorStr);
});
}
};
cppCallback.onIterationComplete = [iterationCompleteCallback](const std::string& detailed_stats) {
if (iterationCompleteCallback) {
NSString *statsStr = [NSString stringWithUTF8String:detailed_stats.c_str()];
dispatch_async(dispatch_get_main_queue(), ^{
iterationCompleteCallback(statsStr);
});
}
};
// Run the actual benchmark
BenchmarkResultCpp cppResult = [self runBenchmarkCore:(int)backend
threads:(int)threads
useMmap:(bool)useMmap
power:(int)power
precision:(int)precision
memory:(int)memory
dynamicOption:(int)dynamicOption
nPrompt:(int)nPrompt
nGenerate:(int)nGenerate
nRepeat:(int)nRepeat
kvCache:(bool)kvCache
callback:cppCallback];
// Convert result and call completion callback
BenchmarkResult *objcResult = [self convertBenchmarkResult:cppResult];
dispatch_async(dispatch_get_main_queue(), ^{
if (completeCallback) {
completeCallback(objcResult);
}
});
}
@catch (NSException *exception) {
NSLog(@"Exception during benchmark: %@", exception.reason);
if (errorCallback) {
dispatch_async(dispatch_get_main_queue(), ^{
errorCallback([NSString stringWithFormat:@"Benchmark failed: %@", exception.reason]);
});
}
}
@finally {
self->_isBenchmarkRunning = false;
}
});
}
/**
* Stop running benchmark
*/
- (void)stopBenchmark {
_shouldStopBenchmark = true;
NSLog(@"Benchmark stop requested");
}
/**
* Check if benchmark is currently running
*/
- (BOOL)isBenchmarkRunning {
return _isBenchmarkRunning.load();
}
/**
* Process multiple prompts in a single batch and return their responses.
* This method executes each prompt sequentially using the underlying LLM engine,
* collects generated output without streaming to UI, and returns them in order.
*
* @param prompts Array of NSString prompts
* @param completion Completion block called on main thread with responses
*/
- (void)processBatchPrompts:(NSArray<NSString *> *)prompts
completion:(void (^)(NSArray<NSString *> *responses))completion {
if (!_llm) {
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(@[]);
});
}
return;
}
if (!prompts || prompts.count == 0) {
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(@[]);
});
}
return;
}
// Prevent concurrent inference while batch is running
if (_isProcessing.load()) {
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(@[]);
});
}
return;
}
_isProcessing = true;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSMutableArray<NSString *> *results = [[NSMutableArray alloc] initWithCapacity:prompts.count];
@try {
for (NSString *prompt in prompts) {
if (!prompt || prompt.length == 0) {
[results addObject:@""]; // Keep position
continue;
}
// Prepare stream buffer to accumulate output
std::string accumulator;
OptimizedLlmStreamBuffer::CallBack cb = [&accumulator](const char* str, size_t len) {
if (str && len > 0) {
accumulator.append(str, len);
}
};
OptimizedLlmStreamBuffer streambuf(cb);
std::ostream os(&streambuf);
// Reset stop flag
self->_shouldStopInference = false;
// Clear and set history for this prompt
{
std::lock_guard<std::mutex> lock(self->_historyMutex);
self->_history.clear();
self->_history.emplace_back(ChatMessage("user", [prompt UTF8String]));
}
// Perform response generation similar to streaming method but without UI callbacks
@try {
self->_llm->response(self->_history, &os, "<eop>", 1);
int current_size = 1;
const int max_new_tokens = 999999;
while (!self->_shouldStopInference.load() && !self->_llm->stoped() && current_size < max_new_tokens) {
self->_llm->generate(1);
current_size++;
}
} @catch (NSException *e) {
NSLog(@"Exception during batch response generation: %@", e.reason);
}
// Convert accumulated C++ string to NSString
NSString *nsOut = [NSString stringWithUTF8String:accumulator.c_str()];
if (!nsOut) {
nsOut = @"";
}
// Strip trailing <eop> marker if present
if ([nsOut hasSuffix:@"<eop>"]) {
nsOut = [nsOut stringByReplacingOccurrencesOfString:@"<eop>" withString:@"" options:0 range:NSMakeRange(nsOut.length - 5, 5)];
}
[results addObject:nsOut];
// Reset context/history between prompts
{
std::lock_guard<std::mutex> lock(self->_historyMutex);
self->_history.clear();
}
}
} @catch (NSException *exception) {
NSLog(@"Batch processing exception: %@", exception.reason);
}
@finally {
self->_isProcessing = false;
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion([results copy]);
});
}
}
});
}
@end