v2: add embedding tests, multilingual embedding support
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.markbase.12b</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/Users/accusys/MarkBaseEngine/.build/arm64-apple-macosx/release/MarkBaseServer</string>
|
||||||
|
<string>gemma-4-12b-it-4bit</string>
|
||||||
|
<string>8081</string>
|
||||||
|
</array>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>/Users/accusys/MarkBaseEngine/logs/12b.log</string>
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>/Users/accusys/MarkBaseEngine/logs/12b.log</string>
|
||||||
|
<key>WorkingDirectory</key>
|
||||||
|
<string>/Users/accusys/MarkBaseEngine</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.markbase.26b</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/Users/accusys/MarkBaseEngine/.build/arm64-apple-macosx/release/MarkBaseServer</string>
|
||||||
|
<string>gemma-4-26b-standard</string>
|
||||||
|
<string>8082</string>
|
||||||
|
</array>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>/Users/accusys/MarkBaseEngine/logs/26b.log</string>
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>/Users/accusys/MarkBaseEngine/logs/26b.log</string>
|
||||||
|
<key>WorkingDirectory</key>
|
||||||
|
<string>/Users/accusys/MarkBaseEngine</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.markbase.31b</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/Users/accusys/MarkBaseEngine/.build/arm64-apple-macosx/release/MarkBaseServer</string>
|
||||||
|
<string>gemma-4-31b-it-4bit</string>
|
||||||
|
<string>8083</string>
|
||||||
|
</array>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>/Users/accusys/MarkBaseEngine/logs/31b.log</string>
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>/Users/accusys/MarkBaseEngine/logs/31b.log</string>
|
||||||
|
<key>WorkingDirectory</key>
|
||||||
|
<string>/Users/accusys/MarkBaseEngine</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.markbase.e4b</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/Users/accusys/MarkBaseEngine/.build/arm64-apple-macosx/release/MarkBaseServer</string>
|
||||||
|
<string>E4B-MarkBase</string>
|
||||||
|
<string>8080</string>
|
||||||
|
</array>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>/Users/accusys/MarkBaseEngine/logs/e4b.log</string>
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>/Users/accusys/MarkBaseEngine/logs/e4b.log</string>
|
||||||
|
<key>WorkingDirectory</key>
|
||||||
|
<string>/Users/accusys/MarkBaseEngine</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.markbase.embedding</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/Users/accusys/MarkBaseEngine/.build/arm64-apple-macosx/release/MarkBaseServer</string>
|
||||||
|
<string>E4B-MarkBase</string>
|
||||||
|
<string>8084</string>
|
||||||
|
</array>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>/Users/accusys/MarkBaseEngine/logs/embedding.log</string>
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>/Users/accusys/MarkBaseEngine/logs/embedding.log</string>
|
||||||
|
<key>WorkingDirectory</key>
|
||||||
|
<string>/Users/accusys/MarkBaseEngine</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
Executable
+53
@@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Setup MarkBase model servers as launchd services
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
LAUNCH_AGENTS="$HOME/Library/LaunchAgents"
|
||||||
|
LOG_DIR="/Users/accusys/MarkBaseEngine/logs"
|
||||||
|
|
||||||
|
echo "Setting up MarkBase model servers..."
|
||||||
|
|
||||||
|
# Create logs directory
|
||||||
|
mkdir -p "$LOG_DIR"
|
||||||
|
|
||||||
|
# List of services to install
|
||||||
|
SERVICES=(
|
||||||
|
"com.markbase.e4b.plist"
|
||||||
|
"com.markbase.12b.plist"
|
||||||
|
"com.markbase.26b.plist"
|
||||||
|
"com.markbase.31b.plist"
|
||||||
|
"com.markbase.embedding.plist"
|
||||||
|
)
|
||||||
|
|
||||||
|
for plist in "${SERVICES[@]}"; do
|
||||||
|
src="$SCRIPT_DIR/$plist"
|
||||||
|
dst="$LAUNCH_AGENTS/$plist"
|
||||||
|
|
||||||
|
if [ ! -f "$src" ]; then
|
||||||
|
echo " SKIP: $plist not found"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy to LaunchAgents
|
||||||
|
cp "$src" "$dst"
|
||||||
|
echo " Installed: $plist"
|
||||||
|
|
||||||
|
# Load the service (skip if already loaded)
|
||||||
|
label="${plist%.plist}"
|
||||||
|
if launchctl list | grep -q "$label"; then
|
||||||
|
echo " Reloading: $label"
|
||||||
|
launchctl unload "$dst" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
launchctl load "$dst"
|
||||||
|
echo " Loaded: $label"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Done! Services:"
|
||||||
|
echo " E4B-MarkBase → http://localhost:8080"
|
||||||
|
echo " gemma-4-12b-it-4bit → http://localhost:8081"
|
||||||
|
echo " gemma-4-26b → http://localhost:8082"
|
||||||
|
echo " gemma-4-31b → http://localhost:8083"
|
||||||
|
echo " Embedding (E4B) → http://localhost:8084"
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// EmbeddingGemma model configuration
|
||||||
|
public struct EmbeddingGemmaConfig: Codable {
|
||||||
|
public let hiddenSize: Int
|
||||||
|
public let numHiddenLayers: Int
|
||||||
|
public let vocabSize: Int
|
||||||
|
public let numAttentionHeads: Int
|
||||||
|
public let numKeyValueHeads: Int
|
||||||
|
public let headDim: Int
|
||||||
|
public let intermediateSize: Int
|
||||||
|
public let maxPositionEmbeddings: Int
|
||||||
|
public let slidingWindow: Int
|
||||||
|
public let rmsNormEps: Float
|
||||||
|
public let ropeTheta: Float
|
||||||
|
public let useBidirectionalAttention: Bool
|
||||||
|
public let layerTypes: [String]
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case hiddenSize = "hidden_size"
|
||||||
|
case numHiddenLayers = "num_hidden_layers"
|
||||||
|
case vocabSize = "vocab_size"
|
||||||
|
case numAttentionHeads = "num_attention_heads"
|
||||||
|
case numKeyValueHeads = "num_key_value_heads"
|
||||||
|
case headDim = "head_dim"
|
||||||
|
case intermediateSize = "intermediate_size"
|
||||||
|
case maxPositionEmbeddings = "max_position_embeddings"
|
||||||
|
case slidingWindow = "sliding_window"
|
||||||
|
case rmsNormEps = "rms_norm_eps"
|
||||||
|
case ropeTheta = "rope_theta"
|
||||||
|
case useBidirectionalAttention = "use_bidirectional_attention"
|
||||||
|
case layerTypes = "layer_types"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -976,10 +976,8 @@ func quantizedMatmulExpert(engine: MarkBaseEngine, cmdBuf: MTLCommandBuffer,
|
|||||||
gate: MoEExpertGroup, up: MoEExpertGroup, down: MoEExpertGroup,
|
gate: MoEExpertGroup, up: MoEExpertGroup, down: MoEExpertGroup,
|
||||||
accum: MTLBuffer) throws -> Bool {
|
accum: MTLBuffer) throws -> Bool {
|
||||||
guard let pso = try? engine.pipeline(named: "moe_mega_kernel") else { return false }
|
guard let pso = try? engine.pipeline(named: "moe_mega_kernel") else { return false }
|
||||||
// Mega kernel supports only 4-bit router with groupSize=64 experts
|
|
||||||
guard router.bits == 4 else { return false }
|
guard router.bits == 4 else { return false }
|
||||||
let expertGroupSize = gate.expertInDim / gate.numGroups
|
let expertGroupSize = gate.expertInDim / gate.numGroups
|
||||||
guard expertGroupSize == 64 else { return false }
|
|
||||||
let enc = cmdBuf.makeComputeCommandEncoder()!
|
let enc = cmdBuf.makeComputeCommandEncoder()!
|
||||||
enc.setComputePipelineState(pso)
|
enc.setComputePipelineState(pso)
|
||||||
enc.setBuffer(input, offset: 0, index: 0)
|
enc.setBuffer(input, offset: 0, index: 0)
|
||||||
@@ -1011,6 +1009,8 @@ func quantizedMatmulExpert(engine: MarkBaseEngine, cmdBuf: MTLCommandBuffer,
|
|||||||
enc.setBytes(&rScale, length: MemoryLayout<Float>.size, index: 17)
|
enc.setBytes(&rScale, length: MemoryLayout<Float>.size, index: 17)
|
||||||
var topK = UInt32(topK)
|
var topK = UInt32(topK)
|
||||||
enc.setBytes(&topK, length: MemoryLayout<UInt32>.size, index: 18)
|
enc.setBytes(&topK, length: MemoryLayout<UInt32>.size, index: 18)
|
||||||
|
var groupSize = UInt32(expertGroupSize)
|
||||||
|
enc.setBytes(&groupSize, length: MemoryLayout<UInt32>.size, index: 19)
|
||||||
|
|
||||||
let count = Int(max(hiddenSize, moeIntermediate))
|
let count = Int(max(hiddenSize, moeIntermediate))
|
||||||
let logitStorage = Int(numExperts) + Int(topK) + Int(topK)
|
let logitStorage = Int(numExperts) + Int(topK) + Int(topK)
|
||||||
|
|||||||
@@ -815,13 +815,14 @@ kernel void moe_mega_kernel(
|
|||||||
constant uint &numExperts [[buffer(16)]],
|
constant uint &numExperts [[buffer(16)]],
|
||||||
constant float &routerScale [[buffer(17)]],
|
constant float &routerScale [[buffer(17)]],
|
||||||
constant uint &topK [[buffer(18)]],
|
constant uint &topK [[buffer(18)]],
|
||||||
|
constant uint &groupSize [[buffer(19)]],
|
||||||
threadgroup float *shared_space [[threadgroup(0)]],
|
threadgroup float *shared_space [[threadgroup(0)]],
|
||||||
uint gid [[thread_position_in_grid]],
|
uint gid [[thread_position_in_grid]],
|
||||||
uint tid [[thread_position_in_threadgroup]],
|
uint tid [[thread_position_in_threadgroup]],
|
||||||
uint tgSize [[threads_per_threadgroup]]
|
uint tgSize [[threads_per_threadgroup]]
|
||||||
) {
|
) {
|
||||||
uint numGroupsIn = hiddenSize / 64;
|
uint numGroupsIn = hiddenSize / groupSize;
|
||||||
uint numGroupsOut = moeIntermediate / 64;
|
uint numGroupsOut = moeIntermediate / groupSize;
|
||||||
uint packedPerIn = hiddenSize / 8;
|
uint packedPerIn = hiddenSize / 8;
|
||||||
uint packedPerOut = moeIntermediate / 8;
|
uint packedPerOut = moeIntermediate / 8;
|
||||||
|
|
||||||
@@ -841,10 +842,10 @@ kernel void moe_mega_kernel(
|
|||||||
for (uint g = 0; g < numGroupsIn; g++) {
|
for (uint g = 0; g < numGroupsIn; g++) {
|
||||||
float scale = s_router[tid * numGroupsIn + g];
|
float scale = s_router[tid * numGroupsIn + g];
|
||||||
float bias = b_router[tid * numGroupsIn + g];
|
float bias = b_router[tid * numGroupsIn + g];
|
||||||
uint wBase = tid * packedPerIn + g * 8;
|
uint wBase = tid * packedPerIn + g * (groupSize / 8);
|
||||||
uint xBase = g * 64;
|
uint xBase = g * groupSize;
|
||||||
|
|
||||||
for (uint p = 0; p < 8; p += 4) {
|
for (uint p = 0; p < groupSize / 8; p += 4) {
|
||||||
device uint4 *rPtr = (device uint4*)(&w_router[wBase + p]);
|
device uint4 *rPtr = (device uint4*)(&w_router[wBase + p]);
|
||||||
uint4 packed = *rPtr;
|
uint4 packed = *rPtr;
|
||||||
|
|
||||||
@@ -971,10 +972,10 @@ kernel void moe_mega_kernel(
|
|||||||
float uScale = s_up[sUpBase + gid * numGroupsIn + g];
|
float uScale = s_up[sUpBase + gid * numGroupsIn + g];
|
||||||
float uBias = b_up[sUpBase + gid * numGroupsIn + g];
|
float uBias = b_up[sUpBase + gid * numGroupsIn + g];
|
||||||
|
|
||||||
uint wb = gid * packedPerIn + g * 8;
|
uint wb = gid * packedPerIn + g * (groupSize / 8);
|
||||||
uint xBase = g * 64;
|
uint xBase = g * groupSize;
|
||||||
|
|
||||||
for (uint p = 0; p < 8; p += 4) {
|
for (uint p = 0; p < groupSize / 8; p += 4) {
|
||||||
device uint4 *gPtr = (device uint4*)(&w_gate[wGateBase + wb + p]);
|
device uint4 *gPtr = (device uint4*)(&w_gate[wGateBase + wb + p]);
|
||||||
device uint4 *uPtr = (device uint4*)(&w_up[wUpBase + wb + p]);
|
device uint4 *uPtr = (device uint4*)(&w_up[wUpBase + wb + p]);
|
||||||
uint4 gP = *gPtr;
|
uint4 gP = *gPtr;
|
||||||
@@ -1047,10 +1048,10 @@ kernel void moe_mega_kernel(
|
|||||||
float scale = s_down[wDownBase + gid * numGroupsOut + g];
|
float scale = s_down[wDownBase + gid * numGroupsOut + g];
|
||||||
float bias = b_down[wDownBase + gid * numGroupsOut + g];
|
float bias = b_down[wDownBase + gid * numGroupsOut + g];
|
||||||
|
|
||||||
uint wb = gid * packedPerOut + g * 8;
|
uint wb = gid * packedPerOut + g * (groupSize / 8);
|
||||||
uint iBase = g * 64;
|
uint iBase = g * groupSize;
|
||||||
|
|
||||||
for (uint p = 0; p < 8; p += 4) {
|
for (uint p = 0; p < groupSize / 8; p += 4) {
|
||||||
device uint4 *wPtr = (device uint4*)(&w_down[wDownBase + wb + p]);
|
device uint4 *wPtr = (device uint4*)(&w_down[wDownBase + wb + p]);
|
||||||
uint4 packed = *wPtr;
|
uint4 packed = *wPtr;
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ struct SimpleServerApp {
|
|||||||
let model = try E4BModel(modelDir: modelPath, engine: engine, maxContextLength: 512)
|
let model = try E4BModel(modelDir: modelPath, engine: engine, maxContextLength: 512)
|
||||||
let tokenizer = try TokenizerFactory.load(modelDir: modelPath)
|
let tokenizer = try TokenizerFactory.load(modelDir: modelPath)
|
||||||
let generator = StreamingGenerator(model: model, tokenizer: tokenizer, engine: engine)
|
let generator = StreamingGenerator(model: model, tokenizer: tokenizer, engine: engine)
|
||||||
|
let embeddingModel = try TextEmbeddingModel(modelDir: modelPath, engine: engine, config: TextEmbeddingConfig())
|
||||||
|
|
||||||
print("✓ E4B loaded (\(model.numHiddenLayers) layers)")
|
print("✓ E4B loaded (\(model.numHiddenLayers) layers)")
|
||||||
|
|
||||||
@@ -175,7 +176,8 @@ struct SimpleServerApp {
|
|||||||
"For multimodal (vision/audio) support, use the MarkBase Swift library directly",
|
"For multimodal (vision/audio) support, use the MarkBase Swift library directly",
|
||||||
"Streaming support is planned but not yet implemented",
|
"Streaming support is planned but not yet implemented",
|
||||||
"Function calling uses native Gemma 4 special tokens",
|
"Function calling uses native Gemma 4 special tokens",
|
||||||
"Messages can include tool_calls (assistant) and tool responses (tool role) for multi-turn function calling"
|
"Messages can include tool_calls (assistant) and tool responses (tool role) for multi-turn function calling",
|
||||||
|
"Text embeddings available via /v1/embeddings endpoint (OpenAI-compatible)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@@ -287,6 +289,73 @@ struct SimpleServerApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
router.post("/v1/embeddings") { request, _ in
|
||||||
|
let buffer = try await request.body.collect(upTo: .max)
|
||||||
|
let data = Data(buffer: buffer)
|
||||||
|
|
||||||
|
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||||
|
let input = json["input"] else {
|
||||||
|
return "{\"error\":\"invalid request\",\"type\":\"invalid_request_error\",\"code\":400,\"message\":\"missing 'input' field\"}"
|
||||||
|
}
|
||||||
|
|
||||||
|
let modelId = (json["model"] as? String) ?? "e4b"
|
||||||
|
let encodingFormat = (json["encoding_format"] as? String) ?? "float"
|
||||||
|
|
||||||
|
let inputs: [String]
|
||||||
|
if let str = input as? String {
|
||||||
|
inputs = [str]
|
||||||
|
} else if let arr = input as? [String] {
|
||||||
|
inputs = arr
|
||||||
|
} else {
|
||||||
|
return "{\"error\":\"invalid request\",\"type\":\"invalid_request_error\",\"code\":400,\"message\":\"'input' must be string or array of strings\"}"
|
||||||
|
}
|
||||||
|
|
||||||
|
var embeddings: [[String: Any]] = []
|
||||||
|
for (i, text) in inputs.enumerated() {
|
||||||
|
let t0 = Date()
|
||||||
|
let embedding = try embeddingModel.embed(text: text)
|
||||||
|
let duration = Date().timeIntervalSince(t0)
|
||||||
|
|
||||||
|
let embeddingData: [String: Any]
|
||||||
|
if encodingFormat == "base64" {
|
||||||
|
let base64 = embedding.withUnsafeBytes { Data($0).base64EncodedString() }
|
||||||
|
embeddingData = [
|
||||||
|
"object": "embedding",
|
||||||
|
"index": i,
|
||||||
|
"embedding": base64,
|
||||||
|
"usage_ms": Int(duration * 1000)
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
embeddingData = [
|
||||||
|
"object": "embedding",
|
||||||
|
"index": i,
|
||||||
|
"embedding": embedding,
|
||||||
|
"usage_ms": Int(duration * 1000)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
embeddings.append(embeddingData)
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = UUID().uuidString
|
||||||
|
let ts = Int(Date().timeIntervalSince1970)
|
||||||
|
let totalTokens = inputs.reduce(0) { $0 + tokenizer.encode(text: $1).count }
|
||||||
|
|
||||||
|
let response: [String: Any] = [
|
||||||
|
"id": id,
|
||||||
|
"object": "list",
|
||||||
|
"created": ts,
|
||||||
|
"model": modelId,
|
||||||
|
"data": embeddings,
|
||||||
|
"usage": [
|
||||||
|
"prompt_tokens": totalTokens,
|
||||||
|
"total_tokens": totalTokens
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
let jsonData = try JSONSerialization.data(withJSONObject: response)
|
||||||
|
return String(data: jsonData, encoding: .utf8) ?? "{}"
|
||||||
|
}
|
||||||
|
|
||||||
let app = Application(
|
let app = Application(
|
||||||
router: router,
|
router: router,
|
||||||
configuration: .init(address: .hostname("0.0.0.0", port: port))
|
configuration: .init(address: .hostname("0.0.0.0", port: port))
|
||||||
@@ -299,6 +368,7 @@ struct SimpleServerApp {
|
|||||||
print(" GET /health - Health check")
|
print(" GET /health - Health check")
|
||||||
print(" GET /v1/models - Model list")
|
print(" GET /v1/models - Model list")
|
||||||
print(" POST /v1/chat/completions - Chat completion")
|
print(" POST /v1/chat/completions - Chat completion")
|
||||||
|
print(" POST /v1/embeddings - Text embeddings")
|
||||||
print("")
|
print("")
|
||||||
print("Model: \(modelName)")
|
print("Model: \(modelName)")
|
||||||
if modelName.contains("E4B") {
|
if modelName.contains("E4B") {
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import MarkBase
|
||||||
|
|
||||||
|
final class EmbeddingTest: XCTestCase {
|
||||||
|
var engine: MarkBaseEngine!
|
||||||
|
var model: E4BModel!
|
||||||
|
var embeddingModel: TextEmbeddingModel!
|
||||||
|
let modelDir = "/Users/accusys/MarkBaseEngine/models/E4B-MarkBase"
|
||||||
|
|
||||||
|
override func setUp() {
|
||||||
|
super.setUp()
|
||||||
|
guard FileManager.default.fileExists(atPath: modelDir + "/model.safetensors") else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
engine = try? MarkBaseEngine(autoCompile: true)
|
||||||
|
model = try? E4BModel(modelDir: modelDir, engine: engine, maxContextLength: 512)
|
||||||
|
embeddingModel = try? TextEmbeddingModel(modelDir: modelDir, engine: engine, config: TextEmbeddingConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEmbeddingDimension() throws {
|
||||||
|
try XCTSkipIf(embeddingModel == nil, "Model not found")
|
||||||
|
let embedding = try embeddingModel.embed(text: "Hello world")
|
||||||
|
XCTAssertEqual(embedding.count, 2560, "Embedding dimension should be 2560")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEmbeddingNormalized() throws {
|
||||||
|
try XCTSkipIf(embeddingModel == nil, "Model not found")
|
||||||
|
let embedding = try embeddingModel.embed(text: "Test text")
|
||||||
|
let norm = sqrt(embedding.reduce(0) { $0 + $1 * $1 })
|
||||||
|
XCTAssertEqual(norm, 1.0, accuracy: 0.001, "Embedding should be L2 normalized")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSimilarSentences() throws {
|
||||||
|
try XCTSkipIf(embeddingModel == nil, "Model not found")
|
||||||
|
let e1 = try embeddingModel.embed(text: "The cat is sitting on the mat")
|
||||||
|
let e2 = try embeddingModel.embed(text: "A cat rests on a rug")
|
||||||
|
let e3 = try embeddingModel.embed(text: "The stock market crashed today")
|
||||||
|
|
||||||
|
let sim12 = cosineSimilarity(e1, e2)
|
||||||
|
let sim13 = cosineSimilarity(e1, e3)
|
||||||
|
print("Similar(cat, cat): \(sim12)")
|
||||||
|
print("Similar(cat, stock): \(sim13)")
|
||||||
|
XCTAssertGreaterThan(sim12, sim13, "Similar sentences should have higher cosine similarity")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDifferentLengths() throws {
|
||||||
|
try XCTSkipIf(embeddingModel == nil, "Model not found")
|
||||||
|
let e1 = try embeddingModel.embed(text: "Hi")
|
||||||
|
let e2 = try embeddingModel.embed(text: "This is a much longer sentence with many words")
|
||||||
|
XCTAssertEqual(e1.count, e2.count, "Embeddings should have same dimension regardless of input length")
|
||||||
|
XCTAssertEqual(e1.count, 2560)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEmptyInput() throws {
|
||||||
|
try XCTSkipIf(embeddingModel == nil, "Model not found")
|
||||||
|
let embedding = try embeddingModel.embed(text: "")
|
||||||
|
XCTAssertEqual(embedding.count, 0, "Empty input should return empty embedding")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBatchEmbedding() throws {
|
||||||
|
try XCTSkipIf(embeddingModel == nil, "Model not found")
|
||||||
|
let texts = ["Hello", "World", "Test"]
|
||||||
|
let embeddings = try embeddingModel.embedBatch(texts: texts)
|
||||||
|
XCTAssertEqual(embeddings.count, 3)
|
||||||
|
for embedding in embeddings {
|
||||||
|
XCTAssertEqual(embedding.count, 2560)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cosineSimilarity(_ a: [Float], _ b: [Float]) -> Float {
|
||||||
|
guard a.count == b.count, !a.isEmpty else { return 0 }
|
||||||
|
var dot: Float = 0, normA: Float = 0, normB: Float = 0
|
||||||
|
for i in 0..<a.count {
|
||||||
|
dot += a[i] * b[i]
|
||||||
|
normA += a[i] * a[i]
|
||||||
|
normB += b[i] * b[i]
|
||||||
|
}
|
||||||
|
return dot / (sqrt(normA) * sqrt(normB))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import MarkBase
|
||||||
|
|
||||||
|
final class MultilangEmbeddingTest: XCTestCase {
|
||||||
|
var engine: MarkBaseEngine!
|
||||||
|
var embeddingModel: TextEmbeddingModel!
|
||||||
|
let modelDir = "/Users/accusys/MarkBaseEngine/models/E4B-MarkBase"
|
||||||
|
|
||||||
|
override func setUp() {
|
||||||
|
super.setUp()
|
||||||
|
guard FileManager.default.fileExists(atPath: modelDir + "/model.safetensors") else { return }
|
||||||
|
engine = try? MarkBaseEngine(autoCompile: true)
|
||||||
|
embeddingModel = try? TextEmbeddingModel(modelDir: modelDir, engine: engine, config: TextEmbeddingConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMultilangEmbeddings() throws {
|
||||||
|
try XCTSkipIf(embeddingModel == nil, "Model not found")
|
||||||
|
|
||||||
|
let texts: [String: String] = [
|
||||||
|
"en": "The weather is beautiful today",
|
||||||
|
"zh": "今天天氣很好",
|
||||||
|
"ja": "今日は天気がいいです",
|
||||||
|
"ko": "오늘 날씨가 좋습니다",
|
||||||
|
"es": "El clima está hermoso hoy",
|
||||||
|
"fr": "Il fait beau aujourd'hui",
|
||||||
|
"de": "Das Wetter ist heute schön",
|
||||||
|
"ru": "Сегодня прекрасная погода",
|
||||||
|
"ar": "الطقس جميل اليوم",
|
||||||
|
"hi": "आज मौसम बहुत सुंदर है"
|
||||||
|
]
|
||||||
|
|
||||||
|
var embeddings: [String: [Float]] = [:]
|
||||||
|
for (lang, text) in texts {
|
||||||
|
let emb = try embeddingModel.embed(text: text)
|
||||||
|
XCTAssertEqual(emb.count, 2560, "\(lang) embedding dimension")
|
||||||
|
let norm = sqrt(emb.reduce(0) { $0 + $1 * $1 })
|
||||||
|
XCTAssertEqual(norm, 1.0, accuracy: 0.001, "\(lang) embedding normalized")
|
||||||
|
embeddings[lang] = emb
|
||||||
|
print("\(lang): OK (norm=\(String(format: "%.4f", norm)))")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-language similarity (en-zh should be higher than en-ru for weather context)
|
||||||
|
let enEmb = embeddings["en"]!
|
||||||
|
let zhEmb = embeddings["zh"]!
|
||||||
|
let jaEmb = embeddings["ja"]!
|
||||||
|
|
||||||
|
let simEnZh = cosineSimilarity(enEmb, zhEmb)
|
||||||
|
let simEnJa = cosineSimilarity(enEmb, jaEmb)
|
||||||
|
print("EN-ZH similarity: \(String(format: "%.4f", simEnZh))")
|
||||||
|
print("EN-JA similarity: \(String(format: "%.4f", simEnJa))")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCrossLingualSemanticSimilarity() throws {
|
||||||
|
try XCTSkipIf(embeddingModel == nil, "Model not found")
|
||||||
|
|
||||||
|
// Same meaning, different languages
|
||||||
|
let enCat = try embeddingModel.embed(text: "The cat is sleeping")
|
||||||
|
let zhCat = try embeddingModel.embed(text: "貓在睡覺")
|
||||||
|
let enDog = try embeddingModel.embed(text: "The dog is running")
|
||||||
|
|
||||||
|
let simSame = cosineSimilarity(enCat, zhCat)
|
||||||
|
let simDiff = cosineSimilarity(enCat, enDog)
|
||||||
|
print("EN(cat) - ZH(cat): \(String(format: "%.4f", simSame))")
|
||||||
|
print("EN(cat) - EN(dog): \(String(format: "%.4f", simDiff))")
|
||||||
|
print("Cross-lingual > Same-lingual different topic: \(simSame > simDiff)")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cosineSimilarity(_ a: [Float], _ b: [Float]) -> Float {
|
||||||
|
guard a.count == b.count, !a.isEmpty else { return 0 }
|
||||||
|
var dot: Float = 0, normA: Float = 0, normB: Float = 0
|
||||||
|
for i in 0..<a.count {
|
||||||
|
dot += a[i] * b[i]
|
||||||
|
normA += a[i] * a[i]
|
||||||
|
normB += b[i] * b[i]
|
||||||
|
}
|
||||||
|
return dot / (sqrt(normA) * sqrt(normB))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import Foundation
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
@testable import MarkBase
|
||||||
|
|
||||||
|
final class Timing26BTest: XCTestCase {
|
||||||
|
let modelDir = "/Users/accusys/MarkBaseEngine/models/gemma-4-26b-standard"
|
||||||
|
var engine: MarkBaseEngine!
|
||||||
|
var model: E4BModel!
|
||||||
|
let maxCtx = 128
|
||||||
|
|
||||||
|
override func setUp() {
|
||||||
|
super.setUp()
|
||||||
|
guard FileManager.default.fileExists(atPath: modelDir + "/model.safetensors") else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let t0 = Date()
|
||||||
|
engine = try? MarkBaseEngine(autoCompile: true)
|
||||||
|
model = try? E4BModel(modelDir: modelDir, engine: engine, maxContextLength: maxCtx)
|
||||||
|
let loadTime = Date().timeIntervalSince(t0)
|
||||||
|
print("Total init: \(String(format: "%.1f", loadTime))s")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testForwardTiming() throws {
|
||||||
|
try XCTSkipIf(model == nil, "26B model not found")
|
||||||
|
for i in 0..<5 {
|
||||||
|
let t = Date()
|
||||||
|
_ = try model.forward(tokenId: 100 + i, position: i)
|
||||||
|
let ft = Date().timeIntervalSince(t)
|
||||||
|
print("Forward \(i+1): \(String(format: "%.3f", ft))s")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user