feat(AI/MNN): 集成 MNN.xcframework + ObjC++ 桥(LLM+SME2,Phase 1-2)
挑战赛考核点要求 Qwen + MNN + SME2 + CPU 端侧推理,MLX(GPU)不满足。
本提交打通原生 MNN 集成的工程层:
- scripts/build-mnn-xcframework.sh:从 alibaba/MNN 源码构建 device+sim arm64
双切片 xcframework,MNN_BUILD_LLM=ON 导出 llm/llm.hpp,MNN_SME2=ON
(KleidiAI 运行时自动路由:A19/iPhone17 走 SME2,A17 回退 NEON)
- MNNLLMBridge.{h,mm}:ObjC++ 封装 MNN Llm 的加载/流式生成,streambuf 按
UTF-8 边界聚合回调,getContext() 取 prefill/decode 算 tok/s;模拟器编为桩
(走 MLX 兜底),SME2 经 sysctl hw.optional.arm.FEAT_SME2 探测
- pbxproj:链接 MNN.xcframework + bridging header
- 二进制 gitignore,由脚本本地生成防历史膨胀
模拟器 BUILD SUCCEEDED(0 error),xcframework 处理 + 桥编译 + 链接通过。
下一步 Phase 3:MNNBackend + AIRuntime 双后端路由。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,3 +2,6 @@
|
|||||||
/Models/
|
/Models/
|
||||||
/build/
|
/build/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# MNN 预编译二进制:由 scripts/build-mnn-xcframework.sh 本地生成,不入库防历史膨胀
|
||||||
|
/Frameworks/MNN.xcframework/
|
||||||
|
|||||||
50
scripts/build-mnn-xcframework.sh
Normal file
50
scripts/build-mnn-xcframework.sh
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# 构建 MNN.xcframework(device arm64 + simulator arm64),含 LLM 引擎 + SME2。
|
||||||
|
# 产物输出到 康康/../Frameworks/MNN.xcframework(被 .gitignore,不入库,防历史膨胀)。
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# MNN_SRC=/path/to/MNN sh scripts/build-mnn-xcframework.sh
|
||||||
|
# 需求:CMake 3.14+、Xcode、约 10-40 分钟。
|
||||||
|
#
|
||||||
|
# 关键 flag:
|
||||||
|
# MNN_BUILD_LLM=ON —— 编入 llm 引擎(并导出 llm/llm.hpp),自动开 MNN_LOW_MEMORY
|
||||||
|
# MNN_SME2=ON —— CMake 默认 ON,A19/iPhone17 运行时经 KleidiAI 自动启用,A17 回退 NEON
|
||||||
|
# MNN_METAL=OFF —— 考核走 CPU+SME2,关 Metal 保持精简
|
||||||
|
# MNN_BUILD_LLM_OMNI —— 如需 VL(图→文)再开,会额外拉 OpenCV/Audio(本脚本默认不开,文本优先)
|
||||||
|
set -e
|
||||||
|
|
||||||
|
MNN_SRC="${MNN_SRC:-/Users/xuhuayong/apps/MNN-src}"
|
||||||
|
OUT_DIR="$(cd "$(dirname "$0")/.." && pwd)/Frameworks"
|
||||||
|
TOOLCHAIN_NEW="${MNN_SRC}/cmake/ios.toolchain.new.cmake"
|
||||||
|
EXTRA="-DMNN_BUILD_LLM=ON -DMNN_METAL=OFF -DMNN_ARM82=true -DMNN_SME2=ON"
|
||||||
|
COMMON="-DCMAKE_BUILD_TYPE=Release -DENABLE_BITCODE=0 -DMNN_AAPL_FMWK=1 -DMNN_SEP_BUILD=0 -DMNN_BUILD_SHARED_LIBS=false -DMNN_USE_THREAD_POOL=OFF"
|
||||||
|
|
||||||
|
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
|
||||||
|
cd "$MNN_SRC"
|
||||||
|
|
||||||
|
# 新版 ios-cmake toolchain(支持 SIMULATORARM64;MNN 自带的旧版只支持 x86_64 模拟器)
|
||||||
|
if [ ! -f "$TOOLCHAIN_NEW" ]; then
|
||||||
|
curl -sL "https://raw.githubusercontent.com/leetal/ios-cmake/master/ios.toolchain.cmake" -o "$TOOLCHAIN_NEW"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# device arm64
|
||||||
|
rm -rf build-dev-arm64 && mkdir build-dev-arm64 && cd build-dev-arm64
|
||||||
|
cmake .. $COMMON $EXTRA -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN_NEW" -DPLATFORM=OS64 -DDEPLOYMENT_TARGET=17.0
|
||||||
|
make MNN -j16
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# simulator arm64
|
||||||
|
rm -rf build-sim-arm64 && mkdir build-sim-arm64 && cd build-sim-arm64
|
||||||
|
cmake .. $COMMON $EXTRA -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN_NEW" -DPLATFORM=SIMULATORARM64 -DDEPLOYMENT_TARGET=17.0
|
||||||
|
make MNN -j16
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# 合成 xcframework
|
||||||
|
rm -rf "$OUT_DIR/MNN.xcframework"
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
xcrun xcodebuild -create-xcframework \
|
||||||
|
-framework build-dev-arm64/MNN.framework \
|
||||||
|
-framework build-sim-arm64/MNN.framework \
|
||||||
|
-output "$OUT_DIR/MNN.xcframework"
|
||||||
|
|
||||||
|
echo "✅ 输出: $OUT_DIR/MNN.xcframework"
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
FEED000000000000DEAD0001 /* MLXLLM in Frameworks */ = {isa = PBXBuildFile; productRef = FEED000000000000DEAD0003 /* MLXLLM */; };
|
FEED000000000000DEAD0001 /* MLXLLM in Frameworks */ = {isa = PBXBuildFile; productRef = FEED000000000000DEAD0003 /* MLXLLM */; };
|
||||||
FEED000000000000DEAD0002 /* MLXLMCommon in Frameworks */ = {isa = PBXBuildFile; productRef = FEED000000000000DEAD0004 /* MLXLMCommon */; };
|
FEED000000000000DEAD0002 /* MLXLMCommon in Frameworks */ = {isa = PBXBuildFile; productRef = FEED000000000000DEAD0004 /* MLXLMCommon */; };
|
||||||
FEED000000000000DEAD0005 /* MLXVLM in Frameworks */ = {isa = PBXBuildFile; productRef = FEED000000000000DEAD0006 /* MLXVLM */; };
|
FEED000000000000DEAD0005 /* MLXVLM in Frameworks */ = {isa = PBXBuildFile; productRef = FEED000000000000DEAD0006 /* MLXVLM */; };
|
||||||
|
FEEDFACE000000000000F002 /* MNN.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = FEEDFACE000000000000F001 /* MNN.xcframework */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -33,6 +34,7 @@
|
|||||||
5E463CF92FC403BB0089145B /* 康康.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "康康.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
5E463CF92FC403BB0089145B /* 康康.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "康康.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
5E463D082FC403BC0089145B /* 康康Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "康康Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
5E463D082FC403BC0089145B /* 康康Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "康康Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
5E463D122FC403BC0089145B /* 康康UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "康康UITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
5E463D122FC403BC0089145B /* 康康UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "康康UITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
FEEDFACE000000000000F001 /* MNN.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MNN.xcframework; path = Frameworks/MNN.xcframework; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
@@ -61,6 +63,7 @@
|
|||||||
FEED000000000000DEAD0001 /* MLXLLM in Frameworks */,
|
FEED000000000000DEAD0001 /* MLXLLM in Frameworks */,
|
||||||
FEED000000000000DEAD0002 /* MLXLMCommon in Frameworks */,
|
FEED000000000000DEAD0002 /* MLXLMCommon in Frameworks */,
|
||||||
FEED000000000000DEAD0005 /* MLXVLM in Frameworks */,
|
FEED000000000000DEAD0005 /* MLXVLM in Frameworks */,
|
||||||
|
FEEDFACE000000000000F002 /* MNN.xcframework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -88,6 +91,7 @@
|
|||||||
5E463D0B2FC403BC0089145B /* 康康Tests */,
|
5E463D0B2FC403BC0089145B /* 康康Tests */,
|
||||||
5E463D152FC403BC0089145B /* 康康UITests */,
|
5E463D152FC403BC0089145B /* 康康UITests */,
|
||||||
5E463CFA2FC403BB0089145B /* Products */,
|
5E463CFA2FC403BB0089145B /* Products */,
|
||||||
|
FEEDFACE000000000000F001 /* MNN.xcframework */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -443,6 +447,8 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = com.xuhuayong.kangkang;
|
PRODUCT_BUNDLE_IDENTIFIER = com.xuhuayong.kangkang;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
|
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/Frameworks";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "康康/康康-Bridging-Header.h";
|
||||||
SDKROOT = auto;
|
SDKROOT = auto;
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
@@ -497,6 +503,8 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = com.xuhuayong.kangkang;
|
PRODUCT_BUNDLE_IDENTIFIER = com.xuhuayong.kangkang;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
|
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/Frameworks";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "康康/康康-Bridging-Header.h";
|
||||||
SDKROOT = auto;
|
SDKROOT = auto;
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
|||||||
55
康康/AI/MNN/MNNLLMBridge.h
Normal file
55
康康/AI/MNN/MNNLLMBridge.h
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
//
|
||||||
|
// MNNLLMBridge.h
|
||||||
|
// 康康
|
||||||
|
//
|
||||||
|
// Objective-C 接口,封装 MNN-LLM(Qwen)的加载与流式推理。
|
||||||
|
// 真实实现在 .mm 中以 ObjC++ 调用 <MNN/llm/llm.hpp>;模拟器下编为可用性返回 NO 的桩
|
||||||
|
// (MNN.framework 仅 device arm64 切片有真实 CPU/SME2 内核,模拟器走 MLX 兜底)。
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/// 末次生成的性能统计(取自 MNN LlmContext)。
|
||||||
|
@interface MNNGenerateStats : NSObject
|
||||||
|
@property (nonatomic, readonly) int promptTokens;
|
||||||
|
@property (nonatomic, readonly) int genTokens;
|
||||||
|
@property (nonatomic, readonly) double prefillMs;
|
||||||
|
@property (nonatomic, readonly) double decodeMs;
|
||||||
|
/// 解码速率 tok/s = genTokens / (decodeMs/1000)。demo 卖点 #6 / Live Activity 用。
|
||||||
|
@property (nonatomic, readonly) double decodeTokensPerSecond;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface MNNLLMBridge : NSObject
|
||||||
|
|
||||||
|
/// 本构建是否含真实 MNN 运行时(device=YES,simulator 桩=NO)。
|
||||||
|
+ (BOOL)isAvailable;
|
||||||
|
/// CPU 是否支持 SME2(运行时探测);A19/iPhone17 YES,A17/iPhone15Pro NO。仅用于 UI 展示加速状态。
|
||||||
|
+ (BOOL)cpuSupportsSME2;
|
||||||
|
|
||||||
|
/// 用 MNN llm 的 config.json 路径加载模型(目录含 llm.mnn / 权重 / tokenizer)。失败返回 nil。
|
||||||
|
- (nullable instancetype)initWithConfigPath:(NSString *)configPath;
|
||||||
|
|
||||||
|
@property (nonatomic, readonly) BOOL isLoaded;
|
||||||
|
|
||||||
|
/// 纯文本流式生成。onToken 每解码出一段文本回调一次(在调用线程,同步阻塞直到生成结束)。
|
||||||
|
/// 返回末次统计。
|
||||||
|
- (MNNGenerateStats *)generateText:(NSString *)prompt
|
||||||
|
maxTokens:(int)maxTokens
|
||||||
|
onToken:(void (^)(NSString *piece))onToken;
|
||||||
|
|
||||||
|
/// 图→文(VL,需 MNN_BUILD_LLM_OMNI 构建)。imagePaths 为本地文件路径。
|
||||||
|
/// 当前文本构建未含 OMNI 时返回 nil 并置 error。
|
||||||
|
- (nullable MNNGenerateStats *)analyzeImages:(NSArray<NSString *> *)imagePaths
|
||||||
|
prompt:(NSString *)prompt
|
||||||
|
maxTokens:(int)maxTokens
|
||||||
|
onToken:(void (^)(NSString *piece))onToken
|
||||||
|
error:(NSError *_Nullable *_Nullable)error;
|
||||||
|
|
||||||
|
/// 请求取消当前生成(best-effort:置标志,后续 token 不再回调)。
|
||||||
|
- (void)cancel;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
178
康康/AI/MNN/MNNLLMBridge.mm
Normal file
178
康康/AI/MNN/MNNLLMBridge.mm
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
//
|
||||||
|
// MNNLLMBridge.mm
|
||||||
|
// 康康
|
||||||
|
//
|
||||||
|
// ObjC++ 实现。device 真机用 <MNN/llm/llm.hpp>;模拟器编为桩(返回不可用,上层回退 MLX)。
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "MNNLLMBridge.h"
|
||||||
|
#include <sys/sysctl.h>
|
||||||
|
|
||||||
|
// MARK: - 性能统计(私有 readwrite 重声明)
|
||||||
|
@interface MNNGenerateStats ()
|
||||||
|
@property (nonatomic, readwrite) int promptTokens;
|
||||||
|
@property (nonatomic, readwrite) int genTokens;
|
||||||
|
@property (nonatomic, readwrite) double prefillMs;
|
||||||
|
@property (nonatomic, readwrite) double decodeMs;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation MNNGenerateStats
|
||||||
|
- (double)decodeTokensPerSecond {
|
||||||
|
return self.decodeMs > 0 ? (self.genTokens / (self.decodeMs / 1000.0)) : 0;
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
|
// MARK: - SME2 / 可用性探测(device + simulator 都可编)
|
||||||
|
|
||||||
|
static BOOL kk_sysctlFlag(const char *name) {
|
||||||
|
int64_t v = 0; size_t sz = sizeof(v);
|
||||||
|
if (sysctlbyname(name, &v, &sz, NULL, 0) != 0) return NO;
|
||||||
|
return v != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if TARGET_OS_SIMULATOR
|
||||||
|
|
||||||
|
// ============ 模拟器桩:无真实 MNN ============
|
||||||
|
@implementation MNNLLMBridge
|
||||||
|
+ (BOOL)isAvailable { return NO; }
|
||||||
|
+ (BOOL)cpuSupportsSME2 { return NO; }
|
||||||
|
- (nullable instancetype)initWithConfigPath:(NSString *)configPath { return nil; }
|
||||||
|
- (BOOL)isLoaded { return NO; }
|
||||||
|
- (MNNGenerateStats *)generateText:(NSString *)prompt maxTokens:(int)maxTokens
|
||||||
|
onToken:(void (^)(NSString *))onToken { return [MNNGenerateStats new]; }
|
||||||
|
- (nullable MNNGenerateStats *)analyzeImages:(NSArray<NSString *> *)imagePaths prompt:(NSString *)prompt
|
||||||
|
maxTokens:(int)maxTokens onToken:(void (^)(NSString *))onToken
|
||||||
|
error:(NSError **)error {
|
||||||
|
if (error) *error = [NSError errorWithDomain:@"MNN" code:-1
|
||||||
|
userInfo:@{NSLocalizedDescriptionKey: @"MNN 在模拟器不可用"}];
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
- (void)cancel {}
|
||||||
|
@end
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
// ============ 真机:真实 MNN-LLM ============
|
||||||
|
#include <MNN/llm/llm.hpp>
|
||||||
|
#include <string>
|
||||||
|
#include <ostream>
|
||||||
|
#include <streambuf>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
using MNN::Transformer::Llm;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
/// 把 MNN 写入 ostream 的解码文本转成 NSString 回调;按 UTF-8 完整边界聚合,避免截断多字节。
|
||||||
|
class TokenStreamBuf : public std::streambuf {
|
||||||
|
public:
|
||||||
|
TokenStreamBuf(void (^onToken)(NSString *), std::atomic<bool> *cancel)
|
||||||
|
: _onToken(onToken), _cancel(cancel) {}
|
||||||
|
void flush() {
|
||||||
|
if (_pending.empty()) return;
|
||||||
|
emitPending(); // 末尾尽力 emit(即便非完整 UTF-8 也交出去)
|
||||||
|
_pending.clear();
|
||||||
|
}
|
||||||
|
protected:
|
||||||
|
std::streamsize xsputn(const char *s, std::streamsize n) override {
|
||||||
|
append(s, (size_t)n);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
int overflow(int c) override {
|
||||||
|
if (c != EOF) { char ch = (char)c; append(&ch, 1); }
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
void append(const char *s, size_t n) {
|
||||||
|
if (_cancel && _cancel->load()) return; // 已取消,吞掉不回调
|
||||||
|
_pending.append(s, n);
|
||||||
|
// 仅当整个 pending 是合法 UTF-8 才 emit(token 通常是完整字/词,边界自然对齐)
|
||||||
|
NSString *str = [[NSString alloc] initWithBytes:_pending.data()
|
||||||
|
length:_pending.size()
|
||||||
|
encoding:NSUTF8StringEncoding];
|
||||||
|
if (str) { if (_onToken) _onToken(str); _pending.clear(); }
|
||||||
|
}
|
||||||
|
void emitPending() {
|
||||||
|
NSString *str = [[NSString alloc] initWithBytes:_pending.data()
|
||||||
|
length:_pending.size()
|
||||||
|
encoding:NSUTF8StringEncoding];
|
||||||
|
if (str && _onToken) _onToken(str);
|
||||||
|
}
|
||||||
|
void (^_onToken)(NSString *);
|
||||||
|
std::atomic<bool> *_cancel;
|
||||||
|
std::string _pending;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
@implementation MNNLLMBridge {
|
||||||
|
Llm *_llm;
|
||||||
|
std::atomic<bool> _cancel;
|
||||||
|
BOOL _loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (BOOL)isAvailable { return YES; }
|
||||||
|
|
||||||
|
+ (BOOL)cpuSupportsSME2 {
|
||||||
|
// Apple 通过 sysctl 暴露 ARM 特性位:FEAT_SME2(A19/iPhone17+)。
|
||||||
|
return kk_sysctlFlag("hw.optional.arm.FEAT_SME2");
|
||||||
|
}
|
||||||
|
|
||||||
|
- (nullable instancetype)initWithConfigPath:(NSString *)configPath {
|
||||||
|
self = [super init];
|
||||||
|
if (!self) return nil;
|
||||||
|
_cancel = false;
|
||||||
|
_llm = Llm::createLLM(std::string(configPath.UTF8String));
|
||||||
|
if (_llm == nullptr) return nil;
|
||||||
|
_loaded = _llm->load();
|
||||||
|
if (!_loaded) { Llm::destroy(_llm); _llm = nullptr; return nil; }
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)dealloc {
|
||||||
|
if (_llm) { Llm::destroy(_llm); _llm = nullptr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)isLoaded { return _loaded; }
|
||||||
|
|
||||||
|
- (void)cancel { _cancel = true; }
|
||||||
|
|
||||||
|
- (MNNGenerateStats *)generateText:(NSString *)prompt
|
||||||
|
maxTokens:(int)maxTokens
|
||||||
|
onToken:(void (^)(NSString *))onToken {
|
||||||
|
_cancel = false;
|
||||||
|
TokenStreamBuf buf(onToken, &_cancel);
|
||||||
|
std::ostream os(&buf);
|
||||||
|
if (_llm) {
|
||||||
|
_llm->response(std::string(prompt.UTF8String), &os, nullptr, maxTokens);
|
||||||
|
}
|
||||||
|
buf.flush();
|
||||||
|
return [self statsFromContext];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (nullable MNNGenerateStats *)analyzeImages:(NSArray<NSString *> *)imagePaths
|
||||||
|
prompt:(NSString *)prompt
|
||||||
|
maxTokens:(int)maxTokens
|
||||||
|
onToken:(void (^)(NSString *))onToken
|
||||||
|
error:(NSError **)error {
|
||||||
|
// VL 需 MNN_BUILD_LLM_OMNI 构建(OpenCV 解码图像)。当前文本构建不含,显式报错。
|
||||||
|
if (error) *error = [NSError errorWithDomain:@"MNN" code:-2
|
||||||
|
userInfo:@{NSLocalizedDescriptionKey: @"当前 MNN 构建未含 VL(OMNI),请用 OMNI 框架"}];
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (MNNGenerateStats *)statsFromContext {
|
||||||
|
MNNGenerateStats *s = [MNNGenerateStats new];
|
||||||
|
if (_llm) {
|
||||||
|
const MNN::Transformer::LlmContext *ctx = _llm->getContext();
|
||||||
|
if (ctx) {
|
||||||
|
s.promptTokens = ctx->prompt_len;
|
||||||
|
s.genTokens = ctx->gen_seq_len;
|
||||||
|
s.prefillMs = ctx->prefill_us / 1000.0;
|
||||||
|
s.decodeMs = ctx->decode_us / 1000.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
#endif
|
||||||
6
康康/康康-Bridging-Header.h
Normal file
6
康康/康康-Bridging-Header.h
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
//
|
||||||
|
// 康康-Bridging-Header.h
|
||||||
|
// 把 Objective-C 接口暴露给 Swift。
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "AI/MNN/MNNLLMBridge.h"
|
||||||
Reference in New Issue
Block a user