v2: Initial clean branch with unit tests + CI/CD pipeline
CI / build (push) Waiting to run
CI / unit-tests (push) Blocked by required conditions
CI / lint (push) Blocked by required conditions

- Started from ac75faa (initial E4B-MarkBase integration)
- Kept Sources/ (all engine code) + Package.swift + .gitignore
- Removed all ad-hoc tests, documentation, scripts, Python files
- Added Tests/00_Unit/ (MathTest, TokenizerTest, SamplerTest)
- Added .gitea/workflows/ci.yaml (build + unit tests + lint)
- Added Scripts/check_resources.sh (memory-aware test runner)
- Added Tests/Manifest.json (resource requirements for all tests)
- Focus: 4-bit quantized models only
This commit is contained in:
MarkBase Admin
2026-07-05 13:29:25 +08:00
commit 8a66b9086a
90 changed files with 22252 additions and 0 deletions
+240
View File
@@ -0,0 +1,240 @@
import Foundation
import MarkBase
//
// Complete API Router Implementation
//
/// API endpoint handler
public final class APIRouter: @unchecked Sendable {
private let modelManager: ModelManager
private let metricsCollector: MetricsCollector
private let concurrencyController: DynamicConcurrencyController
private let requestQueue: RequestQueue
public init(
modelManager: ModelManager,
metricsCollector: MetricsCollector = .shared,
concurrencyController: DynamicConcurrencyController
) {
self.modelManager = modelManager
self.metricsCollector = metricsCollector
self.concurrencyController = concurrencyController
self.requestQueue = RequestQueue(controller: concurrencyController)
}
//
// Health & Info Endpoints
//
/// GET /health
public func handleHealth() async -> [String: Any] {
let currentModel = await modelManager.getCurrentModel()
return [
"status": "healthy",
"model": currentModel?.name ?? "none",
"model_id": currentModel?.id ?? "",
"loaded": currentModel?.loaded ?? false,
"version": "1.0.0"
]
}
/// GET /v1/models
public func handleListModels() async -> [String: Any] {
let models = await modelManager.listModels()
return [
"object": "list",
"data": models.map { model in
[
"id": model.id,
"object": "model",
"created": Int(Date().timeIntervalSince1970),
"owned_by": "markbase",
"loaded": model.loaded
]
}
]
}
//
// Model Management Endpoints
//
/// POST /v1/models/load
public func handleLoadModel(modelId: String) async throws -> [String: Any] {
try await modelManager.loadModel(id: modelId)
return [
"status": "success",
"message": "Model loaded: \(modelId)"
]
}
/// POST /v1/models/unload
public func handleUnloadModel() async -> [String: Any] {
await modelManager.unloadModel()
return [
"status": "success",
"message": "Model unloaded"
]
}
/// POST /v1/models/switch
public func handleSwitchModel(modelId: String) async throws -> [String: Any] {
try await modelManager.switchModel(to: modelId)
return [
"status": "success",
"message": "Model switched to: \(modelId)"
]
}
//
// Chat Completions Endpoint
//
/// POST /v1/chat/completions
public func handleChatCompletion(
messages: [ChatMessage],
config: GenerationConfig
) async throws -> ChatCompletionResponse {
try await requestQueue.execute { [weak self] in
guard let self = self else {
throw ModelError.noModelLoaded
}
let generator = try await self.modelManager.getGenerator()
let tokenizer = try await self.modelManager.getTokenizer()
let _ = try await self.modelManager.getModel()
// Build prompt
let prompt = self.buildChatPrompt(messages: messages, tokenizer: tokenizer)
// Generate
let startTime = Date()
let response = try generator.generateComplete(prompt: prompt, config: config)
let duration = Date().timeIntervalSince(startTime)
// Record metrics
let tokens = tokenizer.encode(text: response).count
let resolvedModelId = (await modelManager.getCurrentModel())?.id ?? "unknown"
self.metricsCollector.recordRequest(duration: duration, tokens: tokens, model: resolvedModelId)
return ChatCompletionResponse(
id: self.generateId("chatcmpl"),
object: "chat.completion",
created: Int(Date().timeIntervalSince1970),
model: resolvedModelId,
choices: [
Choice(
index: 0,
message: ChatMessage(role: "assistant", content: response),
finish_reason: "stop"
)
],
usage: Usage(
promptTokens: tokenizer.encode(text: prompt).count,
completionTokens: tokens,
totalTokens: tokenizer.encode(text: prompt + response).count
)
)
}
}
//
// Embeddings Endpoint
//
/// POST /v1/embeddings
public func handleEmbeddings(
input: EmbeddingsRequest.InputType
) async throws -> EmbeddingsResponse {
try await requestQueue.execute { [weak self] in
guard let self = self else {
throw ModelError.noModelLoaded
}
let tokenizer = try await self.modelManager.getTokenizer()
var embeddings: [EmbeddingData] = []
var totalTokens = 0
switch input {
case .string(let text):
let embedding = try await self.generateEmbedding(text: text)
let tokens = tokenizer.encode(text: text).count
totalTokens += tokens
embeddings.append(EmbeddingData(index: 0, embedding: embedding))
case .strings(let texts):
for (index, text) in texts.enumerated() {
let embedding = try await self.generateEmbedding(text: text)
let tokens = tokenizer.encode(text: text).count
totalTokens += tokens
embeddings.append(EmbeddingData(index: index, embedding: embedding))
}
case .tokens(let tokens):
let text = tokenizer.decode(tokens: tokens)
let embedding = try await self.generateEmbedding(text: text)
totalTokens += tokens.count
embeddings.append(EmbeddingData(index: 0, embedding: embedding))
case .tokensList(let tokensList):
for (index, tokens) in tokensList.enumerated() {
let text = tokenizer.decode(tokens: tokens)
let embedding = try await self.generateEmbedding(text: text)
totalTokens += tokens.count
embeddings.append(EmbeddingData(index: index, embedding: embedding))
}
}
return EmbeddingsResponse(
data: embeddings,
model: (await self.modelManager.getCurrentModel())?.id ?? "unknown",
usage: EmbeddingUsage(
prompt_tokens: totalTokens,
total_tokens: totalTokens
)
)
}
}
//
// Private Helpers
//
private func buildChatPrompt(messages: [ChatMessage], tokenizer: Tokenizer) -> String {
var prompt = ""
for message in messages {
let role = message.role == "assistant" ? "model" : message.role
prompt += "<|turn>\(role)\n\(message.content ?? "")<turn|>\n"
}
prompt += "<|turn>model\n"
return prompt
}
private func generateEmbedding(text: String) async throws -> [Float] {
let tokenizer = try await modelManager.getTokenizer()
let model = try await modelManager.getModel()
let tokens = tokenizer.encode(text: text)
var lastHidden: [Float] = []
for (position, tokenId) in tokens.enumerated() {
lastHidden = try model.forward(tokenId: tokenId, position: position)
}
return lastHidden
}
private func generateId(_ prefix: String) -> String {
let uuid = UUID().uuidString.replacingOccurrences(of: "-", with: "")
return "\(prefix)-\(uuid.prefix(29))"
}
}
+31
View File
@@ -0,0 +1,31 @@
import MarkBase
/// Entry point for MarkBase API server
/// Usage: swift run G12BServer [model_dir] [port] [model_id] [--benchmark]
@main
public struct ServerMain {
public static func main() async throws {
let args = CommandLine.arguments
// Check for benchmark mode
if args.contains("--benchmark") {
let modelDir = args.count > 2 ? args[1] : "./model"
let modelName = args.count > 3 ? args[2] : "markbase"
var benchmark = PerformanceBenchmark(modelDir: modelDir, modelName: modelName)
try await benchmark.run()
return
}
let modelDir = args.count > 1 ? args[1] : "./model"
let port = args.count > 2 ? Int(args[2]) ?? 8080 : 8080
let modelId = args.count > 3 ? args[3] : "markbase-12b"
print("\n╔═════════════════════════════════════════════╗")
print("║ MarkBase API Server ║")
print("╚═════════════════════════════════════════════╝\n")
let server = try MarkBaseServer(modelDir: modelDir, modelId: modelId)
try await server.start(port: port)
}
}
+373
View File
@@ -0,0 +1,373 @@
import Foundation
import MarkBase
//
// MarkBase CLI - Command Line Interface
//
/// CLI command
public enum CLICommand {
case serve(ServeOptions)
case chat(ChatOptions)
case list(ListOptions)
case load(LoadOptions)
case unload(UnloadOptions)
case switchModel(SwitchOptions)
case download(DownloadOptions)
case search(SearchOptions)
case benchmark(BenchmarkOptions)
case help
}
/// CLI options
public struct ServeOptions {
public var modelDir: String
public var port: Int
public var host: String
public var maxConcurrency: Int
public init(
modelDir: String = "./model",
port: Int = 8080,
host: String = "127.0.0.1",
maxConcurrency: Int = 4
) {
self.modelDir = modelDir
self.port = port
self.host = host
self.maxConcurrency = maxConcurrency
}
}
public struct ChatOptions {
public var modelDir: String
public var prompt: String
public var maxTokens: Int
public var temperature: Float
public var stream: Bool
public init(
modelDir: String = "./model",
prompt: String = "",
maxTokens: Int = 100,
temperature: Float = 0.7,
stream: Bool = true
) {
self.modelDir = modelDir
self.prompt = prompt
self.maxTokens = maxTokens
self.temperature = temperature
self.stream = stream
}
}
public struct ListOptions {
public var modelsDir: String
public init(modelsDir: String = "./models") {
self.modelsDir = modelsDir
}
}
public struct LoadOptions {
public var modelId: String
public var modelDir: String
public init(modelId: String, modelDir: String = "./models") {
self.modelId = modelId
self.modelDir = modelDir
}
}
public struct UnloadOptions {
public var modelId: String?
public init(modelId: String? = nil) {
self.modelId = modelId
}
}
public struct SwitchOptions {
public var modelId: String
public init(modelId: String) {
self.modelId = modelId
}
}
public struct DownloadOptions {
public var repoId: String
public var outputDir: String
public init(repoId: String, outputDir: String = "./models") {
self.repoId = repoId
self.outputDir = outputDir
}
}
public struct SearchOptions {
public var query: String
public var limit: Int
public var gguf: Bool
public var safetensors: Bool
public init(
query: String = "",
limit: Int = 20,
gguf: Bool = false,
safetensors: Bool = false
) {
self.query = query
self.limit = limit
self.gguf = gguf
self.safetensors = safetensors
}
}
public struct BenchmarkOptions {
public var modelDir: String
public var modelName: String
public var numPrompts: Int
public init(
modelDir: String = "./model",
modelName: String = "markbase",
numPrompts: Int = 10
) {
self.modelDir = modelDir
self.modelName = modelName
self.numPrompts = numPrompts
}
}
/// CLI parser
public final class CLIParser {
public static func parse(arguments: [String]) -> CLICommand {
let args = Array(arguments.dropFirst()) // Skip program name
guard !args.isEmpty else {
return .help
}
let command = args[0]
switch command {
case "serve":
return parseServe(args: Array(args.dropFirst()))
case "chat":
return parseChat(args: Array(args.dropFirst()))
case "list":
return .list(ListOptions())
case "load":
return parseLoad(args: Array(args.dropFirst()))
case "unload":
return .unload(UnloadOptions())
case "switch":
return parseSwitch(args: Array(args.dropFirst()))
case "download":
return parseDownload(args: Array(args.dropFirst()))
case "search":
return parseSearch(args: Array(args.dropFirst()))
case "benchmark":
return parseBenchmark(args: Array(args.dropFirst()))
case "help", "--help", "-h":
return .help
default:
print("Unknown command: \(command)")
return .help
}
}
private static func parseServe(args: [String]) -> CLICommand {
var options = ServeOptions()
var i = 0
while i < args.count {
switch args[i] {
case "--model", "-m":
if i + 1 < args.count { options.modelDir = args[i + 1]; i += 2 }
else { i += 1 }
case "--port", "-p":
if i + 1 < args.count, let port = Int(args[i + 1]) { options.port = port; i += 2 }
else { i += 1 }
case "--host":
if i + 1 < args.count { options.host = args[i + 1]; i += 2 }
else { i += 1 }
case "--concurrency", "-c":
if i + 1 < args.count, let concurrency = Int(args[i + 1]) { options.maxConcurrency = concurrency; i += 2 }
else { i += 1 }
default:
i += 1
}
}
return .serve(options)
}
private static func parseChat(args: [String]) -> CLICommand {
var options = ChatOptions()
var i = 0
while i < args.count {
switch args[i] {
case "--model", "-m":
if i + 1 < args.count { options.modelDir = args[i + 1]; i += 2 }
else { i += 1 }
case "--prompt", "-p":
if i + 1 < args.count { options.prompt = args[i + 1]; i += 2 }
else { i += 1 }
case "--max-tokens", "-n":
if i + 1 < args.count, let n = Int(args[i + 1]) { options.maxTokens = n; i += 2 }
else { i += 1 }
case "--temperature", "-t":
if i + 1 < args.count, let t = Float(args[i + 1]) { options.temperature = t; i += 2 }
else { i += 1 }
case "--stream", "-s":
options.stream = true; i += 1
case "--no-stream":
options.stream = false; i += 1
default:
i += 1
}
}
return .chat(options)
}
private static func parseLoad(args: [String]) -> CLICommand {
var modelId = ""
var modelDir = "./models"
var i = 0
while i < args.count {
switch args[i] {
case "--model", "-m":
if i + 1 < args.count { modelId = args[i + 1]; i += 2 }
else { i += 1 }
case "--dir", "-d":
if i + 1 < args.count { modelDir = args[i + 1]; i += 2 }
else { i += 1 }
default:
if modelId.isEmpty { modelId = args[i] }
i += 1
}
}
return .load(LoadOptions(modelId: modelId, modelDir: modelDir))
}
private static func parseSwitch(args: [String]) -> CLICommand {
var modelId = ""
var i = 0
while i < args.count {
switch args[i] {
case "--model", "-m":
if i + 1 < args.count { modelId = args[i + 1]; i += 2 }
else { i += 1 }
default:
if modelId.isEmpty { modelId = args[i] }
i += 1
}
}
return .switchModel(SwitchOptions(modelId: modelId))
}
private static func parseDownload(args: [String]) -> CLICommand {
var repoId = ""
var outputDir = "./models"
var i = 0
while i < args.count {
switch args[i] {
case "--output", "-o":
if i + 1 < args.count { outputDir = args[i + 1]; i += 2 }
else { i += 1 }
default:
if repoId.isEmpty { repoId = args[i] }
i += 1
}
}
return .download(DownloadOptions(repoId: repoId, outputDir: outputDir))
}
private static func parseSearch(args: [String]) -> CLICommand {
var options = SearchOptions()
var i = 0
while i < args.count {
switch args[i] {
case "--query", "-q":
if i + 1 < args.count { options.query = args[i + 1]; i += 2 }
else { i += 1 }
case "--limit", "-l":
if i + 1 < args.count, let limit = Int(args[i + 1]) { options.limit = limit; i += 2 }
else { i += 1 }
case "--gguf":
options.gguf = true; i += 1
case "--safetensors":
options.safetensors = true; i += 1
default:
if options.query.isEmpty { options.query = args[i] }
i += 1
}
}
return .search(options)
}
private static func parseBenchmark(args: [String]) -> CLICommand {
var options = BenchmarkOptions()
var i = 0
while i < args.count {
switch args[i] {
case "--model", "-m":
if i + 1 < args.count { options.modelDir = args[i + 1]; i += 2 }
else { i += 1 }
case "--name":
if i + 1 < args.count { options.modelName = args[i + 1]; i += 2 }
else { i += 1 }
case "--prompts", "-n":
if i + 1 < args.count, let n = Int(args[i + 1]) { options.numPrompts = n; i += 2 }
else { i += 1 }
default:
i += 1
}
}
return .benchmark(options)
}
}
/// CLI help message
public func printHelp() {
print("""
MarkBase CLI - Command Line Interface
Usage: markbase <command> [options]
Commands:
serve Start API server
chat Interactive chat
list List available models
load Load a model
unload Unload current model
switch Switch to a different model
download Download model from HuggingFace
search Search models on HuggingFace
benchmark Run performance benchmark
help Show this help message
Examples:
markbase serve --model ./model --port 8080
markbase chat --model ./model --prompt "Hello!"
markbase download mlx-community/gemma-4-e4b-it-4bit
markbase search "gemma-4" --gguf
markbase benchmark --model ./model --prompts 10
Use "markbase <command> --help" for more information about a command.
""")
}
@@ -0,0 +1,106 @@
import Foundation
import os
//
// Concurrent Request Handling with Dynamic Concurrency
//
/// Request queue for concurrent processing with dynamic concurrency
public actor RequestQueue {
private let controller: DynamicConcurrencyController
private var semaphore: AsyncSemaphore
public init(controller: DynamicConcurrencyController) {
self.controller = controller
self.semaphore = AsyncSemaphore(value: 4)
}
public func initialize() async {
let maxConcurrency = await controller.getCurrentMax()
semaphore = AsyncSemaphore(value: maxConcurrency)
}
/// Execute request with concurrency limit
public func execute<T: Sendable>(_ operation: @escaping @Sendable () async throws -> T) async throws -> T {
await semaphore.wait()
defer { semaphore.signal() }
return try await operation()
}
/// Update semaphore when concurrency changes
public func updateSemaphore(newMax: Int) {
semaphore = AsyncSemaphore(value: newMax)
}
}
/// Async semaphore for concurrency control
public final class AsyncSemaphore: @unchecked Sendable {
private var value: Int
private let lock = OSAllocatedUnfairLock()
private var waiters: [CheckedContinuation<Void, Never>] = []
public init(value: Int) {
self.value = value
}
public func wait() async {
let shouldWait = lock.withLock {
if value > 0 {
value -= 1
return false
}
return true
}
guard shouldWait else { return }
return await withCheckedContinuation { continuation in
lock.withLock {
waiters.append(continuation)
}
}
}
public func signal() {
lock.withLock {
if !waiters.isEmpty {
let waiter = waiters.removeFirst()
waiter.resume()
} else {
value += 1
}
}
}
}
/// Batch request processor with dynamic concurrency
public actor BatchProcessor {
private let requestQueue: RequestQueue
public init(controller: DynamicConcurrencyController) {
self.requestQueue = RequestQueue(controller: controller)
}
/// Process multiple requests concurrently
public func processBatch<T: Sendable>(
_ requests: [@Sendable () async throws -> T]
) async throws -> [T] {
try await withThrowingTaskGroup(of: (Int, T).self) { group in
var results: [T?] = Array(repeating: nil, count: requests.count)
for (index, request) in requests.enumerated() {
let req = request
group.addTask {
let result = try await self.requestQueue.execute(req)
return (index, result)
}
}
for try await (index, result) in group {
results[index] = result
}
return results.compactMap { $0 }
}
}
}
@@ -0,0 +1,139 @@
import Foundation
//
// Cross-Device Client
//
/// Cross-device client for making requests to other nodes
public final class CrossDeviceClient: @unchecked Sendable {
private let session: URLSession
private let loadBalancer: LoadBalancer
private let timeout: TimeInterval
public init(
loadBalancer: LoadBalancer,
timeout: TimeInterval = 30
) {
self.loadBalancer = loadBalancer
self.timeout = timeout
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = timeout
self.session = URLSession(configuration: config)
}
/// Send request to cluster
public func sendToCluster(
endpoint: String,
method: String = "POST",
body: Data? = nil
) async throws -> CrossDeviceResponse {
guard let node = loadBalancer.getNextNode() else {
throw CrossDeviceError.noHealthyNodes
}
return try await sendToNode(
node: node,
endpoint: endpoint,
method: method,
body: body
)
}
/// Send request to specific node
public func sendToNode(
node: DeviceNode,
endpoint: String,
method: String = "POST",
body: Data? = nil
) async throws -> CrossDeviceResponse {
let url = URL(string: "\(node.baseURL)\(endpoint)")!
var request = URLRequest(url: url)
request.httpMethod = method
request.httpBody = body
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.timeoutInterval = timeout
let startTime = Date()
do {
let (data, response) = try await session.data(for: request)
let latency = Date().timeIntervalSince(startTime)
guard let httpResponse = response as? HTTPURLResponse else {
throw CrossDeviceError.invalidResponse
}
// Update node status
loadBalancer.updateNodeStatus(
id: node.id,
status: httpResponse.statusCode < 500 ? .healthy : .degraded,
load: nil
)
return CrossDeviceResponse(
requestId: UUID().uuidString,
statusCode: httpResponse.statusCode,
body: data,
latency: latency,
nodeId: node.id
)
} catch {
let latency = Date().timeIntervalSince(startTime)
// Update node status
loadBalancer.updateNodeStatus(id: node.id, status: .unhealthy)
throw CrossDeviceError.requestFailed(error)
}
}
/// Broadcast request to all healthy nodes
public func broadcastToAll(
endpoint: String,
method: String = "POST",
body: Data? = nil
) async throws -> [CrossDeviceResponse] {
let nodes = loadBalancer.getNodes().filter { $0.status == .healthy }
return try await withThrowingTaskGroup(of: CrossDeviceResponse.self) { group in
for node in nodes {
group.addTask {
try await self.sendToNode(
node: node,
endpoint: endpoint,
method: method,
body: body
)
}
}
var responses: [CrossDeviceResponse] = []
for try await response in group {
responses.append(response)
}
return responses
}
}
}
/// Cross-device errors
public enum CrossDeviceError: Error, LocalizedError {
case noHealthyNodes
case invalidResponse
case requestFailed(Error)
case timeout
public var errorDescription: String? {
switch self {
case .noHealthyNodes:
return "No healthy nodes available"
case .invalidResponse:
return "Invalid response from node"
case .requestFailed(let error):
return "Request failed: \(error.localizedDescription)"
case .timeout:
return "Request timed out"
}
}
}
@@ -0,0 +1,109 @@
import Foundation
//
// Cross-Device Communication Protocol
//
/// Device node in the cluster
public struct DeviceNode: Codable, Sendable {
public let id: String
public let host: String
public let port: Int
public let capabilities: DeviceCapabilities
public var status: DeviceStatus
public var load: Double // 0.0 to 1.0
public init(
id: String,
host: String,
port: Int,
capabilities: DeviceCapabilities,
status: DeviceStatus = .healthy,
load: Double = 0.0
) {
self.id = id
self.host = host
self.port = port
self.capabilities = capabilities
self.status = status
self.load = load
}
public var baseURL: String {
"http://\(host):\(port)"
}
}
/// Device capabilities
public struct DeviceCapabilities: Codable, Sendable {
public let maxConcurrency: Int
public let supportedModels: [String]
public let hasGPU: Bool
public let memoryGB: Int
public init(
maxConcurrency: Int = 4,
supportedModels: [String] = [],
hasGPU: Bool = true,
memoryGB: Int = 16
) {
self.maxConcurrency = maxConcurrency
self.supportedModels = supportedModels
self.hasGPU = hasGPU
self.memoryGB = memoryGB
}
}
/// Device status
public enum DeviceStatus: String, Codable, Sendable {
case healthy
case degraded
case unhealthy
case offline
}
/// Cross-device request
public struct CrossDeviceRequest: Codable, Sendable {
public let id: String
public let endpoint: String
public let method: String
public let body: Data?
public let timeout: TimeInterval
public init(
id: String = UUID().uuidString,
endpoint: String,
method: String = "POST",
body: Data? = nil,
timeout: TimeInterval = 30
) {
self.id = id
self.endpoint = endpoint
self.method = method
self.body = body
self.timeout = timeout
}
}
/// Cross-device response
public struct CrossDeviceResponse: Codable, Sendable {
public let requestId: String
public let statusCode: Int
public let body: Data?
public let latency: TimeInterval
public let nodeId: String
public init(
requestId: String,
statusCode: Int,
body: Data? = nil,
latency: TimeInterval,
nodeId: String
) {
self.requestId = requestId
self.statusCode = statusCode
self.body = body
self.latency = latency
self.nodeId = nodeId
}
}
@@ -0,0 +1,110 @@
import Foundation
//
// Dynamic Concurrency Controller
// Automatically adjusts concurrency based on system resources
//
/// Memory statistics
public struct MemoryStats {
public let total: UInt64
public let available: UInt64
public let used: UInt64
public let percentage: Double
public init(total: UInt64, available: UInt64, used: UInt64) {
self.total = total
self.available = available
self.used = used
self.percentage = total > 0 ? Double(used) / Double(total) : 0
}
}
/// Dynamic concurrency controller
public actor DynamicConcurrencyController {
private var currentMax: Int
private let minConcurrency: Int
private let maxConcurrency: Int
private let modelMemoryEstimate: UInt64
/// Concurrency adjustment event
public struct ConcurrencyEvent {
public let timestamp: Date
public let oldMax: Int
public let newMax: Int
public let reason: String
}
/// Event handler
public var onConcurrencyChange: ((ConcurrencyEvent) -> Void)?
public init(
initialConcurrency: Int = 4,
minConcurrency: Int = 1,
maxConcurrency: Int = 16,
modelMemoryEstimate: UInt64 = 9 * 1024 * 1024 * 1024 // 9GB for 12B
) {
self.currentMax = initialConcurrency
self.minConcurrency = minConcurrency
self.maxConcurrency = maxConcurrency
self.modelMemoryEstimate = modelMemoryEstimate
}
/// Get current max concurrency
public func getCurrentMax() -> Int {
return currentMax
}
/// Adjust concurrency based on current memory
@discardableResult
public func adjust() -> ConcurrencyEvent? {
let oldMax = currentMax
// Simplified: use available memory estimation
// In production, you would use actual memory stats
let recommendedConcurrency = 4 // Default for now
let newMax = max(minConcurrency, min(maxConcurrency, recommendedConcurrency))
if newMax != oldMax {
let event = ConcurrencyEvent(
timestamp: Date(),
oldMax: oldMax,
newMax: newMax,
reason: "Automatic adjustment"
)
currentMax = newMax
onConcurrencyChange?(event)
return event
}
return nil
}
/// Get recommended concurrency for a given model size
public static func recommendConcurrency(
modelMemoryBytes: UInt64,
totalMemory: UInt64,
reservedMemory: UInt64 = 2 * 1024 * 1024 * 1024 // 2GB reserved
) -> Int {
let availableMemory = totalMemory - reservedMemory
let concurrency = Int(availableMemory / modelMemoryBytes)
return max(1, concurrency)
}
}
/// Memory monitor using system information
public actor MemoryMonitor {
public init() {}
/// Get current memory statistics (simplified)
public func getStats() -> MemoryStats {
// Simplified implementation
return MemoryStats(
total: 48 * 1024 * 1024 * 1024, // 48GB
available: 32 * 1024 * 1024 * 1024, // 32GB
used: 16 * 1024 * 1024 * 1024 // 16GB
)
}
}
+112
View File
@@ -0,0 +1,112 @@
import Foundation
//
// Embeddings API Models
// OpenAI Compatible Format
//
/// Embeddings request
public struct EmbeddingsRequest: Codable {
public let model: String
public let input: InputType
public let encoding_format: String?
public enum InputType: Codable, Sendable {
case string(String)
case strings([String])
case tokens([Int])
case tokensList([[Int]])
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let string = try? container.decode(String.self) {
self = .string(string)
} else if let strings = try? container.decode([String].self) {
self = .strings(strings)
} else if let tokens = try? container.decode([Int].self) {
self = .tokens(tokens)
} else if let tokensList = try? container.decode([[Int]].self) {
self = .tokensList(tokensList)
} else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Invalid input type"
)
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let string):
try container.encode(string)
case .strings(let strings):
try container.encode(strings)
case .tokens(let tokens):
try container.encode(tokens)
case .tokensList(let tokensList):
try container.encode(tokensList)
}
}
}
public init(
model: String,
input: InputType,
encoding_format: String? = nil
) {
self.model = model
self.input = input
self.encoding_format = encoding_format
}
}
/// Embeddings response
public struct EmbeddingsResponse: Codable, Sendable {
public let object: String
public let data: [EmbeddingData]
public let model: String
public let usage: EmbeddingUsage
public init(
object: String = "list",
data: [EmbeddingData],
model: String,
usage: EmbeddingUsage
) {
self.object = object
self.data = data
self.model = model
self.usage = usage
}
}
/// Single embedding data
public struct EmbeddingData: Codable, Sendable {
public let object: String
public let index: Int
public let embedding: [Float]
public init(
object: String = "embedding",
index: Int,
embedding: [Float]
) {
self.object = object
self.index = index
self.embedding = embedding
}
}
/// Embedding usage
public struct EmbeddingUsage: Codable, Sendable {
public let prompt_tokens: Int
public let total_tokens: Int
public init(prompt_tokens: Int, total_tokens: Int) {
self.prompt_tokens = prompt_tokens
self.total_tokens = total_tokens
}
}
+310
View File
@@ -0,0 +1,310 @@
import Foundation
//
// Unified Error Handling for MarkBase
//
/// MarkBase error types
public enum MarkBaseError: Error, LocalizedError {
case modelNotFound(String)
case invalidRequest(String)
case modelLoadingFailed(String)
case inferenceFailed(String)
case tokenLimitExceeded(current: Int, max: Int)
case invalidParameter(parameter: String, message: String)
case internalError(String)
case multimodalNotSupported
case imageProcessingFailed
case audioProcessingFailed
public var errorDescription: String? {
switch self {
case .modelNotFound(let path):
return "Model not found at: \(path)"
case .invalidRequest(let message):
return "Invalid request: \(message)"
case .modelLoadingFailed(let detail):
return "Failed to load model: \(detail)"
case .inferenceFailed(let detail):
return "Inference failed: \(detail)"
case .tokenLimitExceeded(let current, let max):
return "Token limit exceeded: \(current) > \(max)"
case .invalidParameter(let parameter, let message):
return "Invalid parameter '\(parameter)': \(message)"
case .internalError(let detail):
return "Internal error: \(detail)"
case .multimodalNotSupported:
return "Multimodal inference not supported for this model"
case .imageProcessingFailed:
return "Image processing failed"
case .audioProcessingFailed:
return "Audio processing failed"
}
}
/// HTTP status code for this error
public var httpStatus: Int {
switch self {
case .modelNotFound:
return 404
case .invalidRequest, .invalidParameter:
return 400
case .modelLoadingFailed, .internalError:
return 500
case .inferenceFailed:
return 500
case .tokenLimitExceeded:
return 400
case .multimodalNotSupported:
return 400
case .imageProcessingFailed, .audioProcessingFailed:
return 500
}
}
/// OpenAI-compatible error type
public var errorType: String {
switch self {
case .modelNotFound:
return "model_not_found"
case .invalidRequest, .invalidParameter, .tokenLimitExceeded:
return "invalid_request_error"
case .modelLoadingFailed, .internalError:
return "server_error"
case .inferenceFailed:
return "server_error"
case .multimodalNotSupported:
return "invalid_request_error"
case .imageProcessingFailed, .audioProcessingFailed:
return "server_error"
}
}
/// Convert to OpenAI error response format
public func toErrorResponse(param: String? = nil) -> ErrorResponse {
ErrorResponse(
error: toErrorDetail(param: param)
)
}
/// Convert to ErrorDetail
public func toErrorDetail(param: String? = nil) -> ErrorDetail {
ErrorDetail(
message: localizedDescription,
type: errorType,
code: httpStatus,
param: param
)
}
}
/// OpenAI-compatible error response
public struct ErrorResponse: Codable {
public let error: ErrorDetail
public init(error: ErrorDetail) {
self.error = error
}
/// Create error response from MarkBaseError
public static func from(_ error: MarkBaseError) -> ErrorResponse {
ErrorResponse(error: error.toErrorDetail())
}
}
public struct ErrorDetail: Codable {
public let message: String
public let type: String
public let code: Int
public let param: String?
public init(message: String, type: String, code: Int, param: String? = nil) {
self.message = message
self.type = type
self.code = code
self.param = param
}
}
/// Validation helpers
public enum Validator {
/// Validate model path exists
public static func validateModelPath(_ path: String) throws {
guard FileManager.default.fileExists(atPath: path) else {
throw MarkBaseError.modelNotFound(path)
}
// Check for required files (support both single-file and sharded safetensors)
let hasSingleFile = FileManager.default.fileExists(atPath: (path as NSString).appendingPathComponent("model.safetensors"))
let hasIndexFile = FileManager.default.fileExists(atPath: (path as NSString).appendingPathComponent("model.safetensors.index.json"))
guard hasSingleFile || hasIndexFile else {
throw MarkBaseError.modelLoadingFailed("Missing required file: model.safetensors or model.safetensors.index.json")
}
guard FileManager.default.fileExists(atPath: (path as NSString).appendingPathComponent("config.json")) else {
throw MarkBaseError.modelLoadingFailed("Missing required file: config.json")
}
}
/// Validate generation parameters
public static func validateGenerationParams(
maxTokens: Int?,
temperature: Float?,
topP: Float?,
topK: Int?
) throws {
if let maxTokens = maxTokens {
guard maxTokens > 0 && maxTokens <= 4096 else {
throw MarkBaseError.invalidParameter(
parameter: "max_tokens",
message: "Must be between 1 and 4096"
)
}
}
if let temperature = temperature {
guard temperature >= 0.0 && temperature <= 2.0 else {
throw MarkBaseError.invalidParameter(
parameter: "temperature",
message: "Must be between 0.0 and 2.0"
)
}
}
if let topP = topP {
guard topP >= 0.0 && topP <= 1.0 else {
throw MarkBaseError.invalidParameter(
parameter: "top_p",
message: "Must be between 0.0 and 1.0"
)
}
}
if let topK = topK {
guard topK > 0 else {
throw MarkBaseError.invalidParameter(
parameter: "top_k",
message: "Must be greater than 0"
)
}
}
}
/// Validate prompt
public static func validatePrompt(_ prompt: String, maxLength: Int = 4096) throws {
guard !prompt.isEmpty else {
throw MarkBaseError.invalidRequest("Prompt cannot be empty")
}
guard prompt.count <= maxLength else {
throw MarkBaseError.invalidParameter(
parameter: "prompt",
message: "Prompt too long (max \(maxLength) characters)"
)
}
}
/// Validate messages
public static func validateMessages(_ messages: [ChatMessage]) throws {
guard !messages.isEmpty else {
throw MarkBaseError.invalidRequest("Messages array cannot be empty")
}
for (index, message) in messages.enumerated() {
guard let content = message.content, !content.isEmpty else {
throw MarkBaseError.invalidParameter(
parameter: "messages[\(index)].content",
message: "Content cannot be empty"
)
}
guard ["system", "user", "assistant"].contains(message.role) else {
throw MarkBaseError.invalidParameter(
parameter: "messages[\(index)].role",
message: "Role must be 'system', 'user', or 'assistant'"
)
}
}
}
/// Validate multimodal messages
public static func validateMultimodalMessages(_ messages: [MultimodalMessage]) throws {
guard !messages.isEmpty else {
throw MarkBaseError.invalidRequest("Messages array cannot be empty")
}
for (index, message) in messages.enumerated() {
guard !message.content.isEmpty else {
throw MarkBaseError.invalidParameter(
parameter: "messages[\(index)].content",
message: "Content array cannot be empty"
)
}
// Validate content parts
for (partIndex, part) in message.content.enumerated() {
switch part {
case .text(let text):
guard !text.isEmpty else {
throw MarkBaseError.invalidParameter(
parameter: "messages[\(index)].content[\(partIndex)]",
message: "Text content cannot be empty"
)
}
case .imageUrl(let imageUrl):
guard !imageUrl.url.isEmpty else {
throw MarkBaseError.invalidParameter(
parameter: "messages[\(index)].content[\(partIndex)].image_url.url",
message: "Image URL cannot be empty"
)
}
case .audioUrl(let audioUrl):
guard !audioUrl.url.isEmpty else {
throw MarkBaseError.invalidParameter(
parameter: "messages[\(index)].content[\(partIndex)].audio_url.url",
message: "Audio URL cannot be empty"
)
}
case .videoUrl(let videoUrl):
guard !videoUrl.url.isEmpty else {
throw MarkBaseError.invalidParameter(
parameter: "messages[\(index)].content[\(partIndex)].video_url.url",
message: "Video URL cannot be empty"
)
}
}
}
// Validate role
guard ["system", "user", "assistant"].contains(message.role) else {
throw MarkBaseError.invalidParameter(
parameter: "messages[\(index)].role",
message: "Role must be 'system', 'user', or 'assistant'"
)
}
}
}
}
/// Result type with error handling
public enum Result<T> {
case success(T)
case failure(MarkBaseError)
public func get() throws -> T {
switch self {
case .success(let value):
return value
case .failure(let error):
throw error
}
}
public func map<U>(_ transform: (T) -> U) -> Result<U> {
switch self {
case .success(let value):
return .success(transform(value))
case .failure(let error):
return .failure(error)
}
}
}
@@ -0,0 +1,116 @@
import Foundation
//
// Function Calling API Models
// OpenAI Compatible Format
//
/// Tool definition
public struct Tool: Codable {
public let type: String
public let function: FunctionDefinition
public init(type: String = "function", function: FunctionDefinition) {
self.type = type
self.function = function
}
}
/// Function definition
public struct FunctionDefinition: Codable {
public let name: String
public let description: String?
public let parameters: FunctionParameters?
public init(
name: String,
description: String? = nil,
parameters: FunctionParameters? = nil
) {
self.name = name
self.description = description
self.parameters = parameters
}
}
/// Function parameters (JSON Schema)
public struct FunctionParameters: Codable {
public let type: String
public let properties: [String: PropertySchema]?
public let required: [String]?
public init(
type: String = "object",
properties: [String: PropertySchema]? = nil,
required: [String]? = nil
) {
self.type = type
self.properties = properties
self.required = required
}
}
/// Property schema
public struct PropertySchema: Codable {
public let type: String
public let description: String?
public let `enum`: [String]?
public init(
type: String,
description: String? = nil,
enum: [String]? = nil
) {
self.type = type
self.description = description
self.enum = `enum`
}
}
/// Tool call in response
public struct ToolCall: Codable, Sendable {
public let id: String
public let type: String
public let function: FunctionCall
public init(
id: String = "call_\(UUID().uuidString.replacingOccurrences(of: "-", with: "").prefix(9))",
type: String = "function",
function: FunctionCall
) {
self.id = id
self.type = type
self.function = function
}
}
/// Function call
public struct FunctionCall: Codable, Sendable {
public let name: String
public let arguments: String
public init(name: String, arguments: String) {
self.name = name
self.arguments = arguments
}
}
/// Tool message
public struct ToolMessage: Codable {
public let role: String
public let content: String?
public let tool_call_id: String?
public let name: String?
public init(
role: String = "tool",
content: String?,
tool_call_id: String?,
name: String?
) {
self.role = role
self.content = content
self.tool_call_id = tool_call_id
self.name = name
}
}
+116
View File
@@ -0,0 +1,116 @@
import Foundation
//
// JSON Schema Response API Models
// OpenAI Compatible Format
//
/// Response format specification
public enum ResponseFormat: Codable {
case text
case jsonObject
case jsonSchema(JSONSchemaDefinition)
private enum CodingKeys: String, CodingKey {
case type
case jsonSchema = "json_schema"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
switch type {
case "text":
self = .text
case "json_object":
self = .jsonObject
case "json_schema":
let schema = try container.decode(JSONSchemaDefinition.self, forKey: .jsonSchema)
self = .jsonSchema(schema)
default:
throw DecodingError.dataCorruptedError(
forKey: .type,
in: container,
debugDescription: "Unknown response format type: \(type)"
)
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .text:
try container.encode("text", forKey: .type)
case .jsonObject:
try container.encode("json_object", forKey: .type)
case .jsonSchema(let schema):
try container.encode("json_schema", forKey: .type)
try container.encode(schema, forKey: .jsonSchema)
}
}
}
/// JSON Schema definition
public struct JSONSchemaDefinition: Codable {
public let name: String
public let description: String?
public let schema: JSONSchema
public let strict: Bool?
public init(
name: String,
description: String? = nil,
schema: JSONSchema,
strict: Bool? = nil
) {
self.name = name
self.description = description
self.schema = schema
self.strict = strict
}
}
/// JSON Schema
public struct JSONSchema: Codable {
public let type: String
public let properties: [String: SchemaProperty]?
public let required: [String]?
public let additionalProperties: Bool?
public init(
type: String,
properties: [String: SchemaProperty]? = nil,
required: [String]? = nil,
additionalProperties: Bool? = false
) {
self.type = type
self.properties = properties
self.required = required
self.additionalProperties = additionalProperties
}
}
/// Schema property
public struct SchemaProperty: Codable {
public let type: String
public let description: String?
public let `enum`: [String]?
public let properties: [String: SchemaProperty]?
public let required: [String]?
public init(
type: String,
description: String? = nil,
enum: [String]? = nil,
properties: [String: SchemaProperty]? = nil,
required: [String]? = nil
) {
self.type = type
self.description = description
self.enum = `enum`
self.properties = properties
self.required = required
}
}
+92
View File
@@ -0,0 +1,92 @@
import Foundation
//
// Load Balancer for Cross-Device Communication
//
/// Load balancing strategies
public enum LoadBalancingStrategy: Sendable {
case roundRobin
case leastLoaded
case random
case geographic
}
/// Load balancer
public final class LoadBalancer: @unchecked Sendable {
private var nodes: [DeviceNode] = []
private var currentIndex: Int = 0
private let strategy: LoadBalancingStrategy
private let lock = NSLock()
public init(strategy: LoadBalancingStrategy = .roundRobin) {
self.strategy = strategy
}
/// Add a node
public func addNode(_ node: DeviceNode) {
lock.lock()
defer { lock.unlock() }
nodes.append(node)
}
/// Remove a node
public func removeNode(id: String) {
lock.lock()
defer { lock.unlock() }
nodes.removeAll { $0.id == id }
}
/// Get next node based on strategy
public func getNextNode() -> DeviceNode? {
lock.lock()
defer { lock.unlock() }
let healthyNodes = nodes.filter { $0.status == .healthy }
guard !healthyNodes.isEmpty else { return nil }
switch strategy {
case .roundRobin:
let node = healthyNodes[currentIndex % healthyNodes.count]
currentIndex += 1
return node
case .leastLoaded:
return healthyNodes.min { $0.load < $1.load }
case .random:
return healthyNodes.randomElement()
case .geographic:
// Simplified: return node with lowest latency
return healthyNodes.first
}
}
/// Update node status
public func updateNodeStatus(id: String, status: DeviceStatus, load: Double? = nil) {
lock.lock()
defer { lock.unlock() }
if let index = nodes.firstIndex(where: { $0.id == id }) {
var node = nodes[index]
node.status = status
if let load = load {
node.load = load
}
nodes[index] = node
}
}
/// Get all nodes
public func getNodes() -> [DeviceNode] {
lock.lock()
defer { lock.unlock() }
return nodes
}
/// Get healthy nodes count
public var healthyNodesCount: Int {
nodes.filter { $0.status == .healthy }.count
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,176 @@
import Foundation
//
// Model Downloader - Download models from HuggingFace
//
/// Download progress
public struct DownloadProgress: Sendable {
public let current: Int64
public let total: Int64
public let percentage: Double
public let speed: Double // bytes per second
public let eta: TimeInterval // seconds
public init(current: Int64, total: Int64, speed: Double = 0) {
self.current = current
self.total = total
self.percentage = total > 0 ? Double(current) / Double(total) : 0
self.speed = speed
self.eta = speed > 0 ? Double(total - current) / speed : 0
}
}
/// Model downloader
public final class ModelDownloader: @unchecked Sendable {
public static let shared = ModelDownloader()
private let session: URLSession
private var progressHandler: ((DownloadProgress) -> Void)?
private init() {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 300
config.timeoutIntervalForResource = 3600
self.session = URLSession(configuration: config)
}
/// Set progress handler
public func onProgress(_ handler: @escaping (DownloadProgress) -> Void) {
self.progressHandler = handler
}
/// Download model from HuggingFace
public func downloadModel(
repoId: String,
to destinationDir: String,
revision: String = "main"
) async throws {
let destinationURL = URL(fileURLWithPath: destinationDir)
// Create destination directory
try FileManager.default.createDirectory(
at: destinationURL,
withIntermediateDirectories: true
)
// List files in repo
let files = try await listRepoFiles(repoId: repoId, revision: revision)
// Download each file
for file in files {
let fileURL = destinationURL.appendingPathComponent(file)
// Skip if already exists
if FileManager.default.fileExists(atPath: fileURL.path) {
print("Skipping \(file) (already exists)")
continue
}
print("Downloading \(file)...")
try await downloadFile(
repoId: repoId,
file: file,
revision: revision,
to: fileURL
)
}
print("✓ Model downloaded to \(destinationDir)")
}
/// List files in repo
private func listRepoFiles(repoId: String, revision: String) async throws -> [String] {
let url = URL(string: "https://huggingface.co/api/models/\(repoId)/tree/\(revision)")!
let (data, _) = try await session.data(from: url)
struct FileInfo: Codable {
let path: String
let type: String
}
let files = try JSONDecoder().decode([FileInfo].self, from: data)
return files.filter { $0.type == "file" }.map { $0.path }
}
/// Download single file
private func downloadFile(
repoId: String,
file: String,
revision: String,
to destination: URL
) async throws {
let downloadURL = URL(
string: "https://huggingface.co/\(repoId)/resolve/\(revision)/\(file)"
)!
let (tempURL, response) = try await session.download(from: downloadURL)
// Get total size
let total = response.expectedContentLength
// Move to destination
try FileManager.default.moveItem(at: tempURL, to: destination)
}
/// Download with progress
private func downloadWithProgress(
from url: URL,
to destination: URL
) async throws {
var request = URLRequest(url: url)
request.timeoutInterval = 300
let (bytes, response) = try await session.bytes(for: request)
let total = response.expectedContentLength
var current: Int64 = 0
var startTime = Date()
// Create file
let fileHandle = try FileHandle(forWritingTo: destination)
defer { try? fileHandle.close() }
for try await byte in bytes {
try fileHandle.write(contentsOf: [byte])
current += 1
// Update progress every 1MB
if current % (1024 * 1024) == 0 {
let elapsed = Date().timeIntervalSince(startTime)
let speed = elapsed > 0 ? Double(current) / elapsed : 0
let progress = DownloadProgress(
current: current,
total: total,
speed: speed
)
progressHandler?(progress)
}
}
}
}
/// Model repository
public struct ModelRepository {
public let id: String
public let name: String
public let description: String
public let downloads: Int
public let likes: Int
public init(
id: String,
name: String,
description: String,
downloads: Int,
likes: Int
) {
self.id = id
self.name = name
self.description = description
self.downloads = downloads
self.likes = likes
}
}
+211
View File
@@ -0,0 +1,211 @@
import Foundation
//
// Model Finder - Search and Select Models
//
/// Model search result
public struct ModelSearchResult: Codable, Sendable {
public let id: String
public let modelId: String
public let author: String
public let sha: String?
public let lastModified: String?
public let isPrivate: Bool
public let disabled: Bool
public let gated: String?
public let pipelineTag: String?
public let tags: [String]
public let downloads: Int
public let likes: Int
public let libraryName: String?
public let createdAt: String?
public var displayName: String {
modelId.split(separator: "/").last.map(String.init) ?? modelId
}
public var isGGUF: Bool {
tags.contains { $0.lowercased().contains("gguf") }
}
public var isSafetensors: Bool {
tags.contains { $0.lowercased().contains("safetensors") }
}
}
/// Model search filters
public struct ModelFilters {
public var search: String?
public var author: String?
public var library: String?
public var task: String?
public var tags: [String]?
public var sort: String?
public var direction: Int? // -1 for descending, 1 for ascending
public var limit: Int?
public init(
search: String? = nil,
author: String? = nil,
library: String? = nil,
task: String? = nil,
tags: [String]? = nil,
sort: String? = nil,
direction: Int? = nil,
limit: Int? = nil
) {
self.search = search
self.author = author
self.library = library
self.task = task
self.tags = tags
self.sort = sort
self.direction = direction
self.limit = limit
}
/// Convert to query parameters
public func toQueryItems() -> [URLQueryItem] {
var items: [URLQueryItem] = []
if let search = search {
items.append(URLQueryItem(name: "search", value: search))
}
if let author = author {
items.append(URLQueryItem(name: "author", value: author))
}
if let library = library {
items.append(URLQueryItem(name: "filter", value: library))
}
if let task = task {
items.append(URLQueryItem(name: "pipeline_tag", value: task))
}
if let tags = tags {
for tag in tags {
items.append(URLQueryItem(name: "filter", value: tag))
}
}
if let sort = sort {
items.append(URLQueryItem(name: "sort", value: sort))
}
if let direction = direction {
items.append(URLQueryItem(name: "direction", value: "\(direction)"))
}
if let limit = limit {
items.append(URLQueryItem(name: "limit", value: "\(limit)"))
}
return items
}
}
/// Model finder
public final class ModelFinder {
public nonisolated(unsafe) static let shared = ModelFinder()
private let session: URLSession
private init() {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
self.session = URLSession(configuration: config)
}
/// Search models on HuggingFace
public func searchModels(filters: ModelFilters) async throws -> [ModelSearchResult] {
var components = URLComponents(string: "https://huggingface.co/api/models")!
components.queryItems = filters.toQueryItems()
guard let url = components.url else {
throw ModelFinderError.invalidURL
}
let (data, _) = try await session.data(from: url)
let models = try JSONDecoder().decode([ModelSearchResult].self, from: data)
return models
}
/// Search by name
public func searchByName(_ name: String, limit: Int = 20) async throws -> [ModelSearchResult] {
let filters = ModelFilters(
search: name,
sort: "downloads",
direction: -1,
limit: limit
)
return try await searchModels(filters: filters)
}
/// Search GGUF models
public func searchGGUF(
name: String? = nil,
limit: Int = 20,
sortBy: String = "downloads"
) async throws -> [ModelSearchResult] {
let filters = ModelFilters(
search: name,
tags: ["gguf"],
sort: sortBy,
direction: -1,
limit: limit
)
return try await searchModels(filters: filters)
}
/// Search Safetensors models
public func searchSafetensors(
name: String? = nil,
limit: Int = 20,
sortBy: String = "downloads"
) async throws -> [ModelSearchResult] {
let filters = ModelFilters(
search: name,
tags: ["safetensors"],
sort: sortBy,
direction: -1,
limit: limit
)
return try await searchModels(filters: filters)
}
/// Get model details
public func getModelDetails(repoId: String) async throws -> ModelSearchResult {
let url = URL(string: "https://huggingface.co/api/models/\(repoId)")!
let (data, _) = try await session.data(from: url)
return try JSONDecoder().decode(ModelSearchResult.self, from: data)
}
/// List model files
public func listModelFiles(repoId: String, revision: String = "main") async throws -> [String] {
let url = URL(string: "https://huggingface.co/api/models/\(repoId)/tree/\(revision)")!
let (data, _) = try await session.data(from: url)
struct FileInfo: Codable {
let path: String
let type: String
}
let files = try JSONDecoder().decode([FileInfo].self, from: data)
return files.filter { $0.type == "file" }.map { $0.path }
}
}
/// Model finder errors
public enum ModelFinderError: Error, LocalizedError {
case invalidURL
case requestFailed(String)
case decodeFailed(String)
public var errorDescription: String? {
switch self {
case .invalidURL:
return "Invalid URL"
case .requestFailed(let detail):
return "Request failed: \(detail)"
case .decodeFailed(let detail):
return "Failed to decode response: \(detail)"
}
}
}
+181
View File
@@ -0,0 +1,181 @@
import Foundation
import MarkBase
//
// Model Manager - Runtime Model Switching
//
/// Model information
public struct ModelInfo: Codable, Sendable {
public let id: String
public let path: String
public let name: String
public var loaded: Bool
public let parameters: [String: String]?
public init(
id: String,
path: String,
name: String,
loaded: Bool = false,
parameters: [String: String]? = nil
) {
self.id = id
self.path = path
self.name = name
self.loaded = loaded
self.parameters = parameters
}
}
/// Model errors
public enum ModelError: Error, LocalizedError {
case modelNotFound(String)
case noModelLoaded
case loadFailed(String)
public var errorDescription: String? {
switch self {
case .modelNotFound(let id):
return "Model not found: \(id)"
case .noModelLoaded:
return "No model is currently loaded"
case .loadFailed(let detail):
return "Failed to load model: \(detail)"
}
}
}
/// Model manager for runtime model switching
public actor ModelManager {
private var models: [String: ModelInfo] = [:]
private var currentModelId: String?
// Active model instances
private var engine: MarkBaseEngine?
private var model: E4BModel?
private var tokenizer: Tokenizer?
private var generator: StreamingGenerator?
private var sampler: Sampler?
public init() {}
/// Register a model
public func register(id: String, path: String, name: String) {
models[id] = ModelInfo(id: id, path: path, name: name, loaded: false)
}
/// Get list of registered models
public func listModels() -> [ModelInfo] {
return Array(models.values)
}
/// Get current model info
public func getCurrentModel() -> ModelInfo? {
guard let id = currentModelId else { return nil }
return models[id]
}
/// Load a model
public func loadModel(id: String) async throws {
guard let modelInfo = models[id] else {
throw ModelError.modelNotFound(id)
}
// Validate path
try Validator.validateModelPath(modelInfo.path)
// Create engine
let newEngine = try MarkBaseEngine(autoCompile: true)
// Load model
let newModel = try E4BModel(
modelDir: modelInfo.path,
engine: newEngine,
maxContextLength: 512
)
// Load tokenizer
let newTokenizer = try TokenizerFactory.load(modelDir: modelInfo.path)
// Create generator
let newGenerator = StreamingGenerator(
model: newModel,
tokenizer: newTokenizer,
engine: newEngine
)
// Update active instances
engine = newEngine
model = newModel
tokenizer = newTokenizer
generator = newGenerator
sampler = Sampler()
currentModelId = id
models[id]?.loaded = true
}
/// Unload current model
public func unloadModel() {
guard let id = currentModelId else { return }
engine = nil
model = nil
tokenizer = nil
generator = nil
sampler = nil
currentModelId = nil
models[id]?.loaded = false
}
/// Switch to a different model
public func switchModel(to id: String) async throws {
// Unload current model if loaded
if currentModelId != nil {
unloadModel()
}
// Load new model
try await loadModel(id: id)
}
/// Get active engine
public func getEngine() throws -> MarkBaseEngine {
guard let engine = engine else {
throw ModelError.noModelLoaded
}
return engine
}
/// Get active model
public func getModel() throws -> E4BModel {
guard let model = model else {
throw ModelError.noModelLoaded
}
return model
}
/// Get active tokenizer
public func getTokenizer() throws -> Tokenizer {
guard let tokenizer = tokenizer else {
throw ModelError.noModelLoaded
}
return tokenizer
}
/// Get active generator
public func getGenerator() throws -> StreamingGenerator {
guard let generator = generator else {
throw ModelError.noModelLoaded
}
return generator
}
/// Get active sampler
public func getSampler() throws -> Sampler {
guard let sampler = sampler else {
throw ModelError.noModelLoaded
}
return sampler
}
}
+109
View File
@@ -0,0 +1,109 @@
import Foundation
import MarkBase
//
// Model API Models
//
/// Chat completion request (for testing)
public struct ChatCompletionRequest: Codable {
public let model: String
public let messages: [ChatMessage]
public let max_tokens: Int?
public let temperature: Float?
public let stream: Bool?
public func toGenerationConfig() -> GenerationConfig {
GenerationConfig(
maxTokens: max_tokens ?? 100,
temperature: temperature ?? 1.0
)
}
}
/// Multimodal chat completion request (for testing)
public struct MultimodalChatCompletionRequest: Codable {
public let model: String
public let messages: [MultimodalMessage]
public let max_tokens: Int?
public let stream: Bool?
public func toGenerationConfig() -> GenerationConfig {
GenerationConfig(maxTokens: max_tokens ?? 100)
}
}
/// Model capabilities
public struct ModelCapabilities: Codable, Sendable {
public let text: Bool
public let vision: Bool
public let audio: Bool
public let embeddings: Bool
public let streaming: Bool
public init(
text: Bool = true,
vision: Bool = true,
audio: Bool = true,
embeddings: Bool = true,
streaming: Bool = true
) {
self.text = text
self.vision = vision
self.audio = audio
self.embeddings = embeddings
self.streaming = streaming
}
}
/// Model parameters
public struct ModelParameters: Codable, Sendable {
public let context_length: Int
public let num_hidden_layers: Int
public let hidden_size: Int
public let vocab_size: Int
public let num_attention_heads: Int
public let num_kv_heads: Int
public init(
context_length: Int,
num_hidden_layers: Int,
hidden_size: Int,
vocab_size: Int,
num_attention_heads: Int,
num_kv_heads: Int
) {
self.context_length = context_length
self.num_hidden_layers = num_hidden_layers
self.hidden_size = hidden_size
self.vocab_size = vocab_size
self.num_attention_heads = num_attention_heads
self.num_kv_heads = num_kv_heads
}
}
/// Model details response
public struct ModelDetails: Codable, Sendable {
public let id: String
public let object: String
public let created: Int
public let owned_by: String
public let capabilities: ModelCapabilities
public let parameters: ModelParameters
public init(
id: String,
object: String = "model",
created: Int = Int(Date().timeIntervalSince1970),
owned_by: String = "markbase",
capabilities: ModelCapabilities,
parameters: ModelParameters
) {
self.id = id
self.object = object
self.created = created
self.owned_by = owned_by
self.capabilities = capabilities
self.parameters = parameters
}
}
+267
View File
@@ -0,0 +1,267 @@
import Foundation
//
// Multimodal API Models
// OpenAI Compatible Format
//
/// Multimodal message content part
public enum ContentPart: Codable {
case text(String)
case imageUrl(ImageUrl)
case audioUrl(AudioUrl)
case videoUrl(VideoUrl)
public struct ImageUrl: Codable {
public let url: String
public init(url: String) {
self.url = url
}
}
public struct AudioUrl: Codable {
public let url: String
public init(url: String) {
self.url = url
}
}
public struct VideoUrl: Codable {
public let url: String
public init(url: String) {
self.url = url
}
}
private enum CodingKeys: String, CodingKey {
case type
case text
case imageUrl = "image_url"
case audioUrl = "audio_url"
case videoUrl = "video_url"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
switch type {
case "text":
let text = try container.decode(String.self, forKey: .text)
self = .text(text)
case "image_url":
let imageUrl = try container.decode(ImageUrl.self, forKey: .imageUrl)
self = .imageUrl(imageUrl)
case "audio_url":
let audioUrl = try container.decode(AudioUrl.self, forKey: .audioUrl)
self = .audioUrl(audioUrl)
case "video_url":
let videoUrl = try container.decode(VideoUrl.self, forKey: .videoUrl)
self = .videoUrl(videoUrl)
default:
throw DecodingError.dataCorruptedError(
forKey: .type,
in: container,
debugDescription: "Unknown content type: \(type)"
)
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .text(let text):
try container.encode("text", forKey: .type)
try container.encode(text, forKey: .text)
case .imageUrl(let imageUrl):
try container.encode("image_url", forKey: .type)
try container.encode(imageUrl, forKey: .imageUrl)
case .audioUrl(let audioUrl):
try container.encode("audio_url", forKey: .type)
try container.encode(audioUrl, forKey: .audioUrl)
case .videoUrl(let videoUrl):
try container.encode("video_url", forKey: .type)
try container.encode(videoUrl, forKey: .videoUrl)
}
}
}
/// Multimodal message
public struct MultimodalMessage: Codable {
public let role: String
public let content: [ContentPart]
public init(role: String, content: [ContentPart]) {
self.role = role
self.content = content
}
/// Extract text content
public var textContent: String {
content.compactMap { part -> String? in
if case .text(let text) = part { return text }
return nil
}.joined(separator: "\n")
}
/// Extract image URLs
public var imageUrls: [ContentPart.ImageUrl] {
content.compactMap { part -> ContentPart.ImageUrl? in
if case .imageUrl(let url) = part { return url }
return nil
}
}
/// Extract audio URLs
public var audioUrls: [ContentPart.AudioUrl] {
content.compactMap { part -> ContentPart.AudioUrl? in
if case .audioUrl(let url) = part { return url }
return nil
}
}
/// Extract video URLs
public var videoUrls: [ContentPart.VideoUrl] {
content.compactMap { part -> ContentPart.VideoUrl? in
if case .videoUrl(let url) = part { return url }
return nil
}
}
}
/// Multimodal chat completion request
public struct MultimodalChatRequest: Codable {
public let messages: [MultimodalMessage]
public let max_tokens: Int?
public let temperature: Float?
public let top_p: Float?
public let top_k: Int?
public let stream: Bool?
public let tools: [Tool]?
public let response_format: ResponseFormat?
public init(
messages: [MultimodalMessage],
max_tokens: Int? = nil,
temperature: Float? = nil,
top_p: Float? = nil,
top_k: Int? = nil,
stream: Bool? = nil,
tools: [Tool]? = nil,
response_format: ResponseFormat? = nil
) {
self.messages = messages
self.max_tokens = max_tokens
self.temperature = temperature
self.top_p = top_p
self.top_k = top_k
self.stream = stream
self.tools = tools
self.response_format = response_format
}
}
//
// Image/Audio Processing Helpers
//
public enum MediaProcessor {
/// Parse data URI to base64 and mime type
public static func parseDataURI(_ uri: String) throws -> (mimeType: String, data: Data) {
guard uri.hasPrefix("data:") else {
throw MarkBaseError.invalidParameter(
parameter: "url",
message: "Expected data URI format"
)
}
let parts = uri.split(separator: ",", maxSplits: 1)
guard parts.count == 2 else {
throw MarkBaseError.invalidParameter(
parameter: "url",
message: "Invalid data URI format"
)
}
let header = String(parts[0])
let base64 = String(parts[1])
// Extract mime type
let mimeType = header.dropFirst(5).split(separator: ";").first.map(String.init) ?? "application/octet-stream"
// Decode base64
guard let data = Data(base64Encoded: base64) else {
throw MarkBaseError.invalidParameter(
parameter: "url",
message: "Invalid base64 data"
)
}
return (mimeType, data)
}
/// Load image from URL or data URI
public static func loadImage(from url: String) throws -> Data {
if url.hasPrefix("data:") {
let (_, data) = try parseDataURI(url)
return data
} else if url.hasPrefix("http://") || url.hasPrefix("https://") {
throw MarkBaseError.invalidParameter(
parameter: "url",
message: "HTTP URLs not yet supported, use base64 data URI"
)
} else {
// Local file path
let filePath = url
guard FileManager.default.fileExists(atPath: filePath) else {
throw MarkBaseError.invalidParameter(
parameter: "url",
message: "File not found: \(filePath)"
)
}
return try Data(contentsOf: URL(fileURLWithPath: filePath))
}
}
/// Load audio from URL or data URI
public static func loadAudio(from url: String) throws -> Data {
if url.hasPrefix("data:") {
let (_, data) = try parseDataURI(url)
return data
} else {
throw MarkBaseError.invalidParameter(
parameter: "url",
message: "Only base64 data URI supported for audio"
)
}
}
/// Load video from file path or data URI
public static func loadVideo(from url: String) throws -> URL {
if url.hasPrefix("data:") {
let (mimeType, data) = try parseDataURI(url)
let ext: String
if mimeType.contains("mp4") { ext = "mp4" }
else if mimeType.contains("quicktime") { ext = "mov" }
else { ext = "mp4" }
let tempURL = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension(ext)
try data.write(to: tempURL)
return tempURL
} else {
let fileURL = URL(fileURLWithPath: url)
guard FileManager.default.fileExists(atPath: fileURL.path) else {
throw MarkBaseError.invalidParameter(
parameter: "url",
message: "Video file not found: \(url)"
)
}
return fileURL
}
}
}
@@ -0,0 +1,206 @@
import Foundation
import MarkBase
//
// Performance Benchmarking Tool
//
public struct PerformanceBenchmark {
private let modelDir: String
private let modelName: String
public init(modelDir: String, modelName: String = "markbase") {
self.modelDir = modelDir
self.modelName = modelName
}
/// Run all benchmarks
public mutating func run() async throws {
print("""
╔══════════════════════════════════════╗
║ Performance Benchmark ║
║ Model: \(modelName)
╚══════════════════════════════════════╝
""")
try await benchmarkModelLoading()
try await benchmarkTokenGeneration()
try await benchmarkTokenizer()
try await benchmarkBufferPool()
print("\n✅ All benchmarks completed!")
}
//
// Model Loading Benchmark
//
private mutating func benchmarkModelLoading() async throws {
print("\n📊 Model Loading Benchmark")
print(String(repeating: "", count: 40))
let start = Date()
let engine = try MarkBaseEngine(autoCompile: true)
let loadEngineTime = Date().timeIntervalSince(start)
print(" Engine initialization: \(String(format: "%.3f", loadEngineTime))s")
let start2 = Date()
let model = try E4BModel(modelDir: modelDir, engine: engine, maxContextLength: 512)
let loadModelTime = Date().timeIntervalSince(start2)
print(" Model loading: \(String(format: "%.3f", loadModelTime))s")
print(" Total: \(String(format: "%.3f", loadEngineTime + loadModelTime))s")
print(" Layers: \(model.numHiddenLayers)")
print(" Vocab: \(model.vocabSize)")
print(" Hidden: \(model.hiddenSize)")
}
//
// Token Generation Benchmark
//
private mutating func benchmarkTokenGeneration() async throws {
print("\n📊 Token Generation Benchmark")
print(String(repeating: "", count: 40))
let engine = try MarkBaseEngine(autoCompile: true)
let model = try E4BModel(modelDir: modelDir, engine: engine, maxContextLength: 512)
let tokenizer = try TokenizerFactory.load(modelDir: modelDir)
let generator = StreamingGenerator(model: model, tokenizer: tokenizer, engine: engine)
let sampler = Sampler()
// Test different temperatures
print("\nTesting with different temperatures:")
for temp in [Float(0.0), Float(0.7), Float(1.0)] {
let prompt = "Hello, how are you?"
let config = GenerationConfig(maxTokens: 20, temperature: temp)
print("\n Temperature \(temp):")
let response = try generator.generateComplete(prompt: prompt, config: config)
print(" Generated: \"\(response)\"")
}
// Now run benchmark with temperature=0.7
let prompt = "Hello, how are you?"
let config = GenerationConfig(maxTokens: 20, temperature: Float(0.7))
// Benchmark
let numRuns = 3
var totalTokens = 0
var totalTime: TimeInterval = 0
for i in 1...numRuns {
let start = Date()
let response = try generator.generateComplete(prompt: prompt, config: config)
let elapsed = Date().timeIntervalSince(start)
let tokens = tokenizer.encode(text: response).count
totalTokens += tokens
totalTime += elapsed
print(" Run \(i): \(tokens) tokens in \(String(format: "%.3f", elapsed))s (\(String(format: "%.1f", Double(tokens) / elapsed)) tok/s)")
if i == 1 {
print(" Generated text: \"\(response)\"")
}
}
let avgTokens = totalTokens / numRuns
let avgTime = totalTime / Double(numRuns)
let avgSpeed = Double(avgTokens) / avgTime
print("\n Average: \(avgTokens) tokens in \(String(format: "%.3f", avgTime))s")
print(" Speed: \(String(format: "%.1f", avgSpeed)) tok/s")
}
//
// Tokenizer Benchmark
//
private func benchmarkTokenizer() throws {
print("\n📊 Tokenizer Benchmark")
print(String(repeating: "", count: 40))
let tokenizer = try TokenizerFactory.load(modelDir: modelDir)
let testTexts = [
"Hello, world!",
"The quick brown fox jumps over the lazy dog.",
"Swift is a powerful and intuitive programming language for macOS, iOS, watchOS, and tvOS.",
"Artificial intelligence is transforming the world in unprecedented ways."
]
// Encode benchmark
let numIterations = 100
var totalEncodeTime: TimeInterval = 0
for text in testTexts {
let start = Date()
for _ in 0..<numIterations {
_ = tokenizer.encode(text: text)
}
let elapsed = Date().timeIntervalSince(start)
totalEncodeTime += elapsed
let tokens = tokenizer.encode(text: text).count
print(" Encode: \"\(text.prefix(30))...\" -> \(tokens) tokens in \(String(format: "%.3f", elapsed / Double(numIterations) * 1000))ms")
}
// Decode benchmark
var totalDecodeTime: TimeInterval = 0
for text in testTexts {
let tokens = tokenizer.encode(text: text)
let start = Date()
for _ in 0..<numIterations {
_ = tokenizer.decode(tokens: tokens)
}
let elapsed = Date().timeIntervalSince(start)
totalDecodeTime += elapsed
print(" Decode: \(tokens.count) tokens -> \"\(text.prefix(30))...\" in \(String(format: "%.3f", elapsed / Double(numIterations) * 1000))ms")
}
print("\n Total encode time: \(String(format: "%.3f", totalEncodeTime))s")
print(" Total decode time: \(String(format: "%.3f", totalDecodeTime))s")
}
//
// Buffer Pool Benchmark
//
private func benchmarkBufferPool() throws {
print("\n📊 Buffer Pool Benchmark")
print(String(repeating: "", count: 40))
let engine = try MarkBaseEngine()
let pool = engine.bufferPool
let bufferSize = 1024
let numIterations = 1000
// Without pool
let start1 = Date()
for _ in 0..<numIterations {
let _ = try engine.makeBuffer(length: bufferSize)
}
let withoutPoolTime = Date().timeIntervalSince(start1)
print(" Without pool: \(String(format: "%.3f", withoutPoolTime))s (\(String(format: "%.0f", Double(numIterations) / withoutPoolTime)) allocs/s)")
// With pool
let start2 = Date()
for _ in 0..<numIterations {
let buf = engine.acquireBuffer(length: bufferSize)
engine.releaseBuffer(buf)
}
let withPoolTime = Date().timeIntervalSince(start2)
print(" With pool: \(String(format: "%.3f", withPoolTime))s (\(String(format: "%.0f", Double(numIterations) / withPoolTime)) allocs/s)")
let speedup = withoutPoolTime / withPoolTime
print("\n Speedup: \(String(format: "%.1f", speedup))x")
print(" Pool stats:")
print(" \(pool.stats)")
}
}
@@ -0,0 +1,152 @@
import Foundation
//
// Performance Metrics API Models
// Prometheus Compatible Format
//
/// Performance metrics collector
public final class MetricsCollector {
public nonisolated(unsafe) static let shared = MetricsCollector()
private var counters: [String: CounterMetric] = [:]
private var histograms: [String: MutableHistogram] = [:]
private let lock = NSLock()
private init() {}
/// Record a request
public func recordRequest(duration: TimeInterval, tokens: Int, model: String) {
lock.lock()
defer { lock.unlock() }
// Request count
incrementCounter(name: "markbase_requests_total", labels: ["model": model])
// Request duration
observeHistogram(name: "markbase_request_duration_seconds", value: duration, labels: ["model": model])
// Tokens processed
incrementCounter(name: "markbase_tokens_total", labels: ["model": model, "type": "output"], value: Double(tokens))
}
/// Record a token generation
public func recordToken(tokens: Int, duration: TimeInterval, model: String) {
lock.lock()
defer { lock.unlock() }
let tokensPerSecond = Double(tokens) / duration
observeHistogram(name: "markbase_tokens_per_second", value: tokensPerSecond, labels: ["model": model])
}
/// Record an error
public func recordError(type: String, model: String) {
lock.lock()
defer { lock.unlock() }
incrementCounter(name: "markbase_errors_total", labels: ["model": model, "type": type])
}
/// Get Prometheus format metrics
public func getPrometheusMetrics() -> String {
lock.lock()
defer { lock.unlock() }
var output = ""
for (name, counter) in counters {
output += counter.toPrometheus(name: name)
}
for (name, histogram) in histograms {
output += histogram.toPrometheus(name: name)
}
return output
}
//
// Private helpers
//
private func incrementCounter(name: String, labels: [String: String], value: Double = 1.0) {
if counters[name] == nil {
counters[name] = CounterMetric()
}
counters[name]?.increment(by: value, labels: labels)
}
private func observeHistogram(name: String, value: Double, labels: [String: String]) {
if histograms[name] == nil {
histograms[name] = MutableHistogram()
}
histograms[name]?.observe(value, labels: labels)
}
}
//
// Metric types
//
protocol Metric {
func toPrometheus(name: String) -> String
}
struct CounterMetric: Metric {
var values: [[String: String]: Double] = [:]
mutating func increment(by value: Double, labels: [String: String]) {
values[labels, default: 0] += value
}
func toPrometheus(name: String) -> String {
var output = "# TYPE \(name) counter\n"
for (labels, value) in values {
let labelsStr = labels.map { "\($0.key)=\"\($0.value)\"" }.joined(separator: ",")
output += "\(name){\(labelsStr)} \(value)\n"
}
return output + "\n"
}
}
struct HistogramMetric: Metric {
var counts: [[String: String]: Int] = [:]
var sums: [[String: String]: Double] = [:]
mutating func observe(_ value: Double, labels: [String: String]) {
counts[labels, default: 0] += 1
sums[labels, default: 0] += value
}
func toPrometheus(name: String) -> String {
var output = "# TYPE \(name) histogram\n"
for (labels, count) in counts {
let sum = sums[labels] ?? 0
let labelsStr = labels.map { "\($0.key)=\"\($0.value)\"" }.joined(separator: ",")
output += "\(name)_count{\(labelsStr)} \(count)\n"
output += "\(name)_sum{\(labelsStr)} \(sum)\n"
}
return output + "\n"
}
}
// Wrapper for mutable histogram
final class MutableHistogram {
var histogram: HistogramMetric
init() {
self.histogram = HistogramMetric()
}
func observe(_ value: Double, labels: [String: String]) {
histogram.observe(value, labels: labels)
}
func toPrometheus(name: String) -> String {
histogram.toPrometheus(name: name)
}
}
@@ -0,0 +1,101 @@
import Foundation
import MarkBase
import Metal
import RDMAKit
//
// RDMA Distribution Service Pipeline Parallelism
//
public actor RDMADistributionService {
public enum Role: Sendable {
case primary(splitLayer: Int)
case secondary
}
public struct Config: Sendable {
public let role: Role
public let peerAddress: String?
public let peerPort: UInt16
public init(role: Role, peerAddress: String? = nil, peerPort: UInt16 = 0) {
self.role = role
self.peerAddress = peerAddress
self.peerPort = peerPort
}
}
// RDMA state
private var context: RDMAContext?
private var pd: RDMAProtectionDomain?
private var sendCQ: RDMACompletionQueue?
private var recvCQ: RDMACompletionQueue?
private var qp: RDMAQueuePair?
// Registered GPU buffers
private var registeredHiddenState: RegisteredMemory?
public let config: Config
public let discovery: RDMADiscovery
public init(config: Config) {
self.config = config
self.discovery = RDMADiscovery()
}
/// Initialize RDMA device and resources. Returns false if no RDMA device is available.
@discardableResult
public func initialize() throws -> Bool {
let devices = discovery.listDevices()
guard let device = devices.first else {
print(" RDMA: No devices found — running in local-only mode")
return false
}
let tbInfo = device.isThunderbolt ? " (Thunderbolt)" : ""
print(" RDMA: Found device '\(device.name)'\(tbInfo)")
context = try discovery.openDevice(device)
let attrs = try context!.queryDeviceAttributes()
print(" RDMA: Firmware \(attrs.fwVer), max MR size \(attrs.maxMRSize) bytes")
pd = try RDMAProtectionDomain(context: context!)
sendCQ = try RDMACompletionQueue(context: context!, cqe: 128)
recvCQ = try RDMACompletionQueue(context: context!, cqe: 128)
qp = try RDMAQueuePair(pd: pd!, sendCQ: sendCQ!, recvCQ: recvCQ!)
try qp!.modifyToInit()
print(" RDMA: QP initialized in RC mode")
if case .primary = config.role {
try qp!.modifyToRTR(destQPN: 0)
try qp!.modifyToRTS()
}
print(" RDMA: Ready")
return true
}
/// Register a Metal buffer for remote RDMA access
public func registerBuffer(_ buffer: MTLBuffer) throws -> RegisteredMemory? {
guard let pd else { return nil }
let reg = try pd.registerMTLBuffer(
UnsafeMutableRawPointer(buffer.contents()),
length: buffer.length
)
print(" RDMA: Registered buffer \(buffer.length) bytes (lkey=\(reg.lkey), rkey=\(reg.rkey))")
return reg
}
/// Register the model's hidden state buffer for pipeline-split transfer
public func registerHiddenState(_ model: E4BModel) throws {
let buf = model.temps.io
let reg = try registerBuffer(buf)
registeredHiddenState = reg
print(" RDMA: Hidden state buffer registered (\(buf.length) bytes)")
}
/// Split point for pipeline parallelism
public var splitLayer: Int? {
if case .primary(let layer) = config.role { return layer }
return nil
}
}
+150
View File
@@ -0,0 +1,150 @@
import Foundation
//
// Server-Sent Events (SSE) Support
//
/// SSE Event structure
public struct SSEEvent {
public let id: String?
public let event: String?
public let data: String
public let retry: Int?
public init(id: String? = nil, event: String? = nil, data: String, retry: Int? = nil) {
self.id = id
self.event = event
self.data = data
self.retry = retry
}
/// Format as SSE string
public func format() -> String {
var output = ""
if let id = id {
output += "id: \(id)\n"
}
if let event = event {
output += "event: \(event)\n"
}
if let retry = retry {
output += "retry: \(retry)\n"
}
// Handle multi-line data
let lines = data.split(separator: "\n")
for line in lines {
output += "data: \(line)\n"
}
output += "\n"
return output
}
}
/// SSE Stream Generator
public final class SSEStream {
private var buffer = ""
public init() {}
/// Add event to stream
public func add(event: SSEEvent) -> String {
let formatted = event.format()
buffer += formatted
return formatted
}
/// Create chat completion chunk
public static func chatChunk(
id: String,
model: String,
content: String? = nil,
role: String? = nil,
finishReason: String? = nil
) -> SSEEvent {
var delta: [String: String] = [:]
if let role = role {
delta["role"] = role
}
if let content = content {
delta["content"] = content
}
let chunk: [String: Any] = [
"id": id,
"object": "chat.completion.chunk",
"created": Int(Date().timeIntervalSince1970),
"model": model,
"choices": [
[
"index": 0,
"delta": delta,
"finish_reason": finishReason as Any
]
]
]
guard let jsonData = try? JSONSerialization.data(withJSONObject: chunk),
let jsonString = String(data: jsonData, encoding: .utf8) else {
return SSEEvent(data: "")
}
return SSEEvent(data: jsonString)
}
/// Create text completion chunk
public static func textChunk(
id: String,
model: String,
text: String,
finishReason: String? = nil
) -> SSEEvent {
let chunk: [String: Any] = [
"id": id,
"object": "text_completion",
"created": Int(Date().timeIntervalSince1970),
"model": model,
"choices": [
[
"index": 0,
"text": text,
"finish_reason": finishReason as Any
]
]
]
guard let jsonData = try? JSONSerialization.data(withJSONObject: chunk),
let jsonString = String(data: jsonData, encoding: .utf8) else {
return SSEEvent(data: "")
}
return SSEEvent(data: jsonString)
}
/// Create [DONE] event
public static func done() -> SSEEvent {
SSEEvent(data: "[DONE]")
}
/// Create error event
public static func error(message: String) -> SSEEvent {
let error: [String: Any] = [
"error": [
"message": message,
"type": "invalid_request_error",
"code": nil
]
]
guard let jsonData = try? JSONSerialization.data(withJSONObject: error),
let jsonString = String(data: jsonData, encoding: .utf8) else {
return SSEEvent(data: "")
}
return SSEEvent(event: "error", data: jsonString)
}
}
@@ -0,0 +1,314 @@
import Foundation
import MarkBase
import Hummingbird
struct SimpleServerApp {
static func main() async throws {
//
let args = CommandLine.arguments
let modelName = args.count > 1 ? args[1] : "E4B-MarkBase"
let port = args.count > 2 ? Int(args[2]) ?? 8080 : 8080
let modelPath = NSString(string: "~/MarkBaseEngine/models/\(modelName)").expandingTildeInPath
print("═══════════════════════════════════════════════════════════════════")
print(" MarkBaseEngine Server")
print("═══════════════════════════════════════════════════════════════════")
print(" Model: \(modelName)")
print(" Port: \(port)")
print(" Path: \(modelPath)")
print("")
let engine = try MarkBaseEngine(autoCompile: true)
let model = try E4BModel(modelDir: modelPath, engine: engine, maxContextLength: 512)
let tokenizer = try TokenizerFactory.load(modelDir: modelPath)
let generator = StreamingGenerator(model: model, tokenizer: tokenizer, engine: engine)
print("✓ E4B loaded (\(model.numHiddenLayers) layers)")
let router = Router()
let layers = model.numHiddenLayers
@Sendable func helpJSON() -> String {
return """
{
"server": {
"name": "MarkBaseEngine",
"version": "1.0.0",
"model": "E4B (\(layers) layers)",
"framework": "Hummingbird 2.x + Metal GPU",
"platform": "Apple Silicon",
"base_url": "http://localhost:8080"
},
"endpoints": [
{
"method": "GET",
"path": "/",
"summary": "API help and documentation",
"content_type": "application/json",
"curl": "curl http://localhost:8080/"
},
{
"method": "GET",
"path": "/help",
"summary": "API help and documentation (alias)",
"content_type": "application/json",
"curl": "curl http://localhost:8080/help"
},
{
"method": "GET",
"path": "/health",
"summary": "Health check - returns server status and model information",
"responses": {
"200": {
"description": "Server is healthy",
"body": "OK"
}
},
"curl": "curl http://localhost:8080/health"
},
{
"method": "GET",
"path": "/v1/models",
"summary": "List available models (OpenAI-compatible)",
"responses": {
"200": {
"description": "Model list",
"body": {
"id": "e4b",
"object": "model",
"owned_by": "markbase"
}
}
},
"curl": "curl http://localhost:8080/v1/models"
},
{
"method": "POST",
"path": "/v1/chat/completions",
"summary": "Chat completion (OpenAI-compatible API)",
"description": "Generate chat responses using the E4B model. Supports text-only input via messages array.",
"request": {
"body": {
"messages": [
{"role": "user", "content": "Hello"}
],
"max_tokens": 100,
"temperature": 0.7,
"top_p": 0.95,
"top_k": 40,
"stream": false
}
},
"responses": {
"200": {
"description": "Successful completion",
"body": {
"id": "chatcmpl-xxx",
"object": "chat.completion",
"created": 1234567890,
"model": "e4b",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello! How can I help you?"
},
"finish_reason": "stop"
}
]
}
},
"400": {
"description": "Invalid request - missing or malformed messages field"
}
},
"parameters": [
{"name": "messages", "type": "array", "required": true, "description": "Array of message objects with 'role' (system/user/assistant/tool) and 'content' (string). Supports 'tool_calls' in assistant messages and 'tool_call_id'/'name' in tool messages."},
{"name": "max_tokens", "type": "integer", "required": false, "default": 100, "description": "Maximum number of tokens to generate (1-4096)"},
{"name": "temperature", "type": "float", "required": false, "default": 0.7, "description": "Sampling temperature (0.0-2.0)"},
{"name": "top_p", "type": "float", "required": false, "description": "Nucleus sampling threshold (0.0-1.0)"},
{"name": "top_k", "type": "integer", "required": false, "description": "Top-k sampling count"},
{"name": "tools", "type": "array", "required": false, "description": "Array of tool definitions (OpenAI format) for function calling. Each tool has 'type' (function) and 'function' with 'name', 'description', 'parameters'."},
{"name": "stream", "type": "boolean", "required": false, "default": false, "description": "Enable streaming response (not yet implemented)"}
],
"curl": "curl -X POST http://localhost:8080/v1/chat/completions -H 'Content-Type: application/json' -d '{\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}],\"max_tokens\":100}'",
"examples": [
{
"title": "Text completion",
"curl": "curl -X POST http://localhost:8080/v1/chat/completions -H 'Content-Type: application/json' -d '{\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}],\"max_tokens\":100}'"
},
{
"title": "Function calling",
"curl": "curl -X POST http://localhost:8080/v1/chat/completions -H 'Content-Type: application/json' -d '{\"messages\":[{\"role\":\"user\",\"content\":\"Search for cats\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"find_file\",\"description\":\"Search for files\",\"parameters\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\"}},\"required\":[\"query\"]}}}],\"max_tokens\":200}'"
}
]
}
],
"schema": {
"content_type": "application/json",
"error_format": {
"error": {
"message": "Error description",
"type": "error_type",
"code": 400
}
},
"error_codes": [
{"code": 400, "type": "invalid_request_error", "description": "Invalid request parameters"},
{"code": 404, "type": "not_found_error", "description": "Resource not found"},
{"code": 500, "type": "server_error", "description": "Internal server error"}
]
},
"documentation": {
"api_spec": "docs/API_SPEC.md",
"api_reference": "docs/API.md",
"deployment": "docs/DEPLOYMENT.md",
"performance": "docs/PERFORMANCE.md"
},
"notes": [
"All responses are in JSON format",
"Text generation only (multimodal not yet supported via API)",
"E4B model with 42 layers, ~4B parameters",
"For multimodal (vision/audio) support, use the MarkBase Swift library directly",
"Streaming support is planned but not yet implemented",
"Function calling uses native Gemma 4 special tokens",
"Messages can include tool_calls (assistant) and tool responses (tool role) for multi-turn function calling"
]
}
"""
}
@Sendable func healthResponse() -> String {
return "{\"status\":\"healthy\",\"model\":\"e4b\",\"layers\":\(layers)}"
}
router.get("/") { _, _ in
return helpJSON()
}
router.get("/help") { _, _ in
return helpJSON()
}
router.get("/health") { _, _ in
return healthResponse()
}
router.get("/v1/models") { _, _ in
return "{\"id\":\"e4b\",\"object\":\"model\",\"owned_by\":\"markbase\"}"
}
router.post("/v1/chat/completions") { 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 messages = json["messages"] as? [[String: Any]] else {
return "{\"error\":\"invalid request\",\"type\":\"invalid_request_error\",\"code\":400}"
}
let maxTokens = (json["max_tokens"] as? Int) ?? 100
let temperature = Float(json["temperature"] as? Double ?? 0.7)
let topK = json["top_k"] as? Int
let topP = (json["top_p"] as? Double).map { Float($0) }
let tools = json["tools"] as? [[String: Any]]
let prompt = Gemma4Format.buildChatPrompt(messages: messages, tools: tools)
let promptTokens = tokenizer.encode(text: prompt)
let config = GenerationConfig(
maxTokens: maxTokens,
temperature: temperature,
topK: topK,
topP: topP
)
let generatedTokens = try generator.generateTokens(promptTokens: promptTokens, config: config)
let id = UUID().uuidString
let ts = Int(Date().timeIntervalSince1970)
// Check for tool calls (token ID 48 = <|tool_call>)
if generatedTokens.contains(48) {
var results: [ToolCallResult] = []
var i = 0
while i < generatedTokens.count {
if generatedTokens[i] == 48 {
i += 1
var callTokens: [Int] = []
while i < generatedTokens.count && generatedTokens[i] != 49 {
callTokens.append(generatedTokens[i])
i += 1
}
if i < generatedTokens.count && generatedTokens[i] == 49 {
i += 1
}
let callText = tokenizer.decode(tokens: callTokens)
if let colonIdx = callText.firstIndex(of: ":"),
let braceIdx = callText.firstIndex(of: "{"),
let endBrace = callText.lastIndex(of: "}") {
let name = String(callText[callText.index(after: colonIdx)..<braceIdx]).trimmingCharacters(in: .whitespaces)
let rawArgs = String(callText[callText.index(after: braceIdx)..<endBrace])
let jsonArgs = Gemma4Format.gemma4ArgsToJSON(rawArgs)
results.append(ToolCallResult(
id: "call_\(UUID().uuidString.replacingOccurrences(of: "-", with: "").prefix(16))",
function: ToolCallFunction(name: name, arguments: jsonArgs)
))
}
} else {
i += 1
}
}
let encoder = JSONEncoder()
let callsData = try encoder.encode(results)
let callsStr = String(data: callsData, encoding: .utf8) ?? "[]"
return """
{"id":"chatcmpl-\(id)","object":"chat.completion","created":\(ts),"model":"e4b","choices":[{"index":0,"message":{"role":"assistant","content":null,"tool_calls":\(callsStr)},"finish_reason":"tool_calls"}]}
"""
} else {
let response = tokenizer.decode(tokens: generatedTokens)
let trimmed = response.trimmingCharacters(in: .whitespacesAndNewlines)
let escaped = trimmed
.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "\"", with: "\\\"")
.replacingOccurrences(of: "\n", with: "\\n")
.replacingOccurrences(of: "\r", with: "\\r")
.replacingOccurrences(of: "\t", with: "\\t")
return """
{"id":"chatcmpl-\(id)","object":"chat.completion","created":\(ts),"model":"e4b","choices":[{"index":0,"message":{"role":"assistant","content":"\(escaped)"},"finish_reason":"stop"}]}
"""
}
}
let app = Application(
router: router,
configuration: .init(address: .hostname("0.0.0.0", port: port))
)
print("Server starting on port \(port)...")
print("Endpoints:")
print(" GET / - API help")
print(" GET /help - API help")
print(" GET /health - Health check")
print(" GET /v1/models - Model list")
print(" POST /v1/chat/completions - Chat completion")
print("")
print("Model: \(modelName)")
if modelName.contains("E4B") {
print(" ⚠️ E4B is multimodal (Vision + Audio + Text)")
print(" For text-only, use: 12B-it-MLX-8bit or 31B-it-8bit")
} else {
print(" ✓ LLM for text generation")
}
print("")
try await app.run()
}
}
+408
View File
@@ -0,0 +1,408 @@
import Accelerate
import AVFoundation
import CoreVideo
import Foundation
//
// Video Processor Extract frames + audio from video files
//
public struct VideoFrame {
public let index: Int
public let timestamp: CMTime
public let pixelBuffer: CVPixelBuffer
}
public struct VideoData {
public let frames: [VideoFrame]
public let audioSamples: [Float]
public let sampleRate: Int
public let duration: CMTime
public let naturalSize: CGSize
public let estimatedFrameRate: Float
}
public enum VideoProcessor {
public struct Config {
public let maxFrames: Int
public let targetFPS: Float
public let audioSampleRate: Int
public let sceneThreshold: Double // 01; 0 = fixed FPS, >0 = scene detection
public init(maxFrames: Int = 64, targetFPS: Float = 2.0,
audioSampleRate: Int = 16000, sceneThreshold: Double = 0.15) {
self.maxFrames = maxFrames
self.targetFPS = targetFPS
self.audioSampleRate = audioSampleRate
self.sceneThreshold = sceneThreshold
}
}
/// Compute luminance histogram difference between two pixel buffers (01, higher = more different).
public static func sceneDiff(_ a: CVPixelBuffer, _ b: CVPixelBuffer) -> Double {
CVPixelBufferLockBaseAddress(a, .readOnly)
CVPixelBufferLockBaseAddress(b, .readOnly)
defer {
CVPixelBufferUnlockBaseAddress(a, .readOnly)
CVPixelBufferUnlockBaseAddress(b, .readOnly)
}
let w = min(CVPixelBufferGetWidth(a), CVPixelBufferGetWidth(b))
let h = min(CVPixelBufferGetHeight(a), CVPixelBufferGetHeight(b))
let rowA = CVPixelBufferGetBytesPerRow(a)
let rowB = CVPixelBufferGetBytesPerRow(b)
guard let baseA = CVPixelBufferGetBaseAddress(a),
let baseB = CVPixelBufferGetBaseAddress(b) else { return 0 }
let ptrA = baseA.assumingMemoryBound(to: UInt8.self)
let ptrB = baseB.assumingMemoryBound(to: UInt8.self)
var histA = [Double](repeating: 0, count: 256)
var histB = [Double](repeating: 0, count: 256)
let total = w * h
for y in 0..<h {
for x in 0..<w {
// Luminance green channel (BGRA index 1)
histA[Int(ptrA[y * rowA + x * 4 + 1])] += 1
histB[Int(ptrB[y * rowB + x * 4 + 1])] += 1
}
}
// Normalise and compute intersection
var diff: Double = 0
for i in 0..<256 {
let normA = histA[i] / Double(total)
let normB = histB[i] / Double(total)
diff += abs(normA - normB)
}
return diff / 2 // 0..1
}
public static func process(url: URL, config: Config = Config()) async throws -> VideoData {
let asset = AVURLAsset(url: url)
let duration = try await asset.load(.duration)
let videoTracks = try await asset.loadTracks(withMediaType: .video)
let audioTracks = try await asset.loadTracks(withMediaType: .audio)
guard let videoTrack = videoTracks.first else {
throw MarkBaseError.invalidParameter(parameter: "url", message: "No video track found")
}
let naturalSize = try await videoTrack.load(.naturalSize)
let nominalFrameRate = try await videoTrack.load(.nominalFrameRate)
// Read audio samples concurrently
let audioSamples: [Float]
if let audioTrack = audioTracks.first {
audioSamples = try readAudioTrack(audioTrack, sampleRate: config.audioSampleRate)
} else {
audioSamples = []
}
// Read video frames (scene detection or fixed FPS)
let frames: [VideoFrame]
if config.sceneThreshold > 0 {
frames = try await readVideoTrackSceneDetect(
videoTrack,
duration: duration,
threshold: config.sceneThreshold,
maxFrames: config.maxFrames
)
print(" Scene detection: \(frames.count) keyframes")
} else {
frames = try await readVideoTrack(
videoTrack,
duration: duration,
nominalFrameRate: nominalFrameRate,
targetFPS: config.targetFPS,
maxFrames: config.maxFrames
)
}
return VideoData(
frames: frames,
audioSamples: audioSamples,
sampleRate: config.audioSampleRate,
duration: duration,
naturalSize: naturalSize,
estimatedFrameRate: nominalFrameRate
)
}
// Video reading
private static func readVideoTrack(
_ track: AVAssetTrack,
duration: CMTime,
nominalFrameRate: Float,
targetFPS: Float,
maxFrames: Int
) async throws -> [VideoFrame] {
let reader = try AVAssetReader(asset: track.asset!)
let formatDescriptions = try await track.load(.formatDescriptions)
let pixelFormat: OSType
if let firstDesc = formatDescriptions.first {
pixelFormat = CMFormatDescriptionGetMediaSubType(firstDesc)
} else {
pixelFormat = kCVPixelFormatType_32BGRA
}
let settings: [String: Any] = [
kCVPixelBufferPixelFormatTypeKey as String: pixelFormat,
kCVPixelBufferMetalCompatibilityKey as String: true,
]
let output = AVAssetReaderTrackOutput(track: track, outputSettings: settings)
reader.add(output)
reader.startReading()
defer {
if reader.status == .reading {
reader.cancelReading()
}
}
let stepSeconds = CMTime(value: CMTimeValue(1.0 / targetFPS), timescale: CMTimeScale(targetFPS * 100))
.seconds
var frames: [VideoFrame] = []
var lastSampleTime: Double = -stepSeconds
while reader.status == .reading, frames.count < maxFrames {
guard let sampleBuffer = output.copyNextSampleBuffer() else { break }
let presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
let timeSeconds = presentationTime.seconds
if timeSeconds - lastSampleTime >= stepSeconds {
if let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
frames.append(VideoFrame(
index: frames.count,
timestamp: presentationTime,
pixelBuffer: pixelBuffer
))
}
lastSampleTime = timeSeconds
}
}
return frames
}
// Scene-detection reading
private static func readVideoTrackSceneDetect(
_ track: AVAssetTrack,
duration: CMTime,
threshold: Double,
maxFrames: Int
) async throws -> [VideoFrame] {
let reader = try AVAssetReader(asset: track.asset!)
let formatDescriptions = try await track.load(.formatDescriptions)
let pixelFormat: OSType
if let firstDesc = formatDescriptions.first {
pixelFormat = CMFormatDescriptionGetMediaSubType(firstDesc)
} else {
pixelFormat = kCVPixelFormatType_32BGRA
}
let settings: [String: Any] = [
kCVPixelBufferPixelFormatTypeKey as String: pixelFormat,
kCVPixelBufferMetalCompatibilityKey as String: true,
]
let output = AVAssetReaderTrackOutput(track: track, outputSettings: settings)
reader.add(output)
reader.startReading()
defer {
if reader.status == .reading { reader.cancelReading() }
}
var frames: [VideoFrame] = []
var prevBuffer: CVPixelBuffer?
while reader.status == .reading, frames.count < maxFrames {
guard let sampleBuffer = output.copyNextSampleBuffer() else { break }
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { continue }
if let prev = prevBuffer {
let diff = sceneDiff(prev, pixelBuffer)
if diff >= threshold || frames.isEmpty {
frames.append(VideoFrame(
index: frames.count,
timestamp: CMSampleBufferGetPresentationTimeStamp(sampleBuffer),
pixelBuffer: pixelBuffer
))
}
} else {
frames.append(VideoFrame(
index: frames.count,
timestamp: CMSampleBufferGetPresentationTimeStamp(sampleBuffer),
pixelBuffer: pixelBuffer
))
}
prevBuffer = pixelBuffer
}
return frames
}
// Audio reading
private static func readAudioTrack(_ track: AVAssetTrack, sampleRate: Int) throws -> [Float] {
let reader = try AVAssetReader(asset: track.asset!)
let settings: [String: Any] = [
AVFormatIDKey: kAudioFormatLinearPCM,
AVLinearPCMIsFloatKey: true,
AVLinearPCMBitDepthKey: 32,
AVNumberOfChannelsKey: 1,
AVSampleRateKey: sampleRate,
]
let output = AVAssetReaderTrackOutput(track: track, outputSettings: settings)
reader.add(output)
reader.startReading()
defer {
if reader.status == .reading {
reader.cancelReading()
}
}
var samples: [Float] = []
while reader.status == .reading {
guard let sampleBuffer = output.copyNextSampleBuffer() else { break }
guard let blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer) else { continue }
let length = CMBlockBufferGetDataLength(blockBuffer)
var data = [Float](repeating: 0, count: length / MemoryLayout<Float>.stride)
CMBlockBufferCopyDataBytes(blockBuffer, atOffset: 0, dataLength: length, destination: &data)
samples.append(contentsOf: data)
}
return samples
}
// Pixel buffer float array
public static func pixelBufferToFloats(_ pixelBuffer: CVPixelBuffer) -> [Float] {
CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) }
let width = CVPixelBufferGetWidth(pixelBuffer)
let height = CVPixelBufferGetHeight(pixelBuffer)
let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
guard let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer) else { return [] }
var floats = [Float](repeating: 0, count: width * height * 3) // RGB
let pixelBuffer = baseAddress.assumingMemoryBound(to: UInt8.self)
for y in 0..<height {
for x in 0..<width {
let offset = y * bytesPerRow + x * 4
let b = Float(pixelBuffer[offset]) / 255.0
let g = Float(pixelBuffer[offset + 1]) / 255.0
let r = Float(pixelBuffer[offset + 2]) / 255.0
let pixelOffset = (y * width + x) * 3
floats[pixelOffset] = r
floats[pixelOffset + 1] = g
floats[pixelOffset + 2] = b
}
}
return floats
}
/// Resize pixel buffer to target size using vImage
public static func resizePixelBuffer(
_ pixelBuffer: CVPixelBuffer,
targetWidth: Int,
targetHeight: Int
) -> CVPixelBuffer? {
let srcWidth = CVPixelBufferGetWidth(pixelBuffer)
let srcHeight = CVPixelBufferGetHeight(pixelBuffer)
var srcBuffer = vImage_Buffer()
CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
srcBuffer.data = CVPixelBufferGetBaseAddress(pixelBuffer)
srcBuffer.width = vImagePixelCount(srcWidth)
srcBuffer.height = vImagePixelCount(srcHeight)
srcBuffer.rowBytes = CVPixelBufferGetBytesPerRow(pixelBuffer)
var destPixelBuffer: CVPixelBuffer?
let attrs: [String: Any] = [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA,
kCVPixelBufferMetalCompatibilityKey as String: true,
]
CVPixelBufferCreate(
kCFAllocatorDefault,
targetWidth, targetHeight,
kCVPixelFormatType_32BGRA,
attrs as CFDictionary,
&destPixelBuffer
)
guard let dest = destPixelBuffer else {
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
return nil
}
CVPixelBufferLockBaseAddress(dest, [])
var destBuffer = vImage_Buffer()
destBuffer.data = CVPixelBufferGetBaseAddress(dest)
destBuffer.width = vImagePixelCount(targetWidth)
destBuffer.height = vImagePixelCount(targetHeight)
destBuffer.rowBytes = CVPixelBufferGetBytesPerRow(dest)
let scale = vImageScale_ARGB8888(&srcBuffer, &destBuffer, nil, vImage_Flags(kvImageNoFlags))
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
CVPixelBufferUnlockBaseAddress(dest, [])
return scale == kvImageNoError ? dest : nil
}
/// Frame to patch embeddings (simple 16×16 grid)
public static func frameToPatchEmbeddings(
_ pixelBuffer: CVPixelBuffer,
patchSize: Int = 16
) -> (embeddings: [Float], numPatches: Int, patchDim: Int) {
let rgb = pixelBufferToFloats(pixelBuffer)
let width = CVPixelBufferGetWidth(pixelBuffer)
let height = CVPixelBufferGetHeight(pixelBuffer)
let numPatchesH = height / patchSize
let numPatchesW = width / patchSize
let numPatches = numPatchesH * numPatchesW
let patchDim = patchSize * patchSize * 3
var embeddings = [Float](repeating: 0, count: numPatches * patchDim)
for ph in 0..<numPatchesH {
for pw in 0..<numPatchesW {
let patchIdx = ph * numPatchesW + pw
for py in 0..<patchSize {
for px in 0..<patchSize {
let srcY = ph * patchSize + py
let srcX = pw * patchSize + px
let srcIdx = (srcY * width + srcX) * 3
let dstIdx = (patchIdx * patchDim) + (py * patchSize + px) * 3
if srcIdx + 2 < rgb.count, dstIdx + 2 < embeddings.count {
embeddings[dstIdx] = rgb[srcIdx]
embeddings[dstIdx + 1] = rgb[srcIdx + 1]
embeddings[dstIdx + 2] = rgb[srcIdx + 2]
}
}
}
}
}
return (embeddings, numPatches, patchDim)
}
}
+12
View File
@@ -0,0 +1,12 @@
import Foundation
// Entry point avoids @main conflict with top-level code
Task {
do {
try await SimpleServerApp.main()
} catch {
print("Server error: \(error)")
exit(1)
}
}
dispatchMain()