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
+161
View File
@@ -0,0 +1,161 @@
import Metal
// Multimodal inference pipeline for 12B
// Handles audio/image processing and integration with text model
public final class MultimodalInference {
public let model: MultimodalModel
public let engine: MarkBaseEngine
// Temporary buffers for multimodal embeddings
private let audioEmbedBuffer: MTLBuffer
private let visionEmbedBuffer: MTLBuffer
public init(model: MultimodalModel) throws {
self.model = model
self.engine = model.engine
let device = engine.device
let hiddenSize = model.textModel.hiddenSize
audioEmbedBuffer = device.makeBuffer(length: 1024 * hiddenSize * 4)!
visionEmbedBuffer = device.makeBuffer(length: 1024 * hiddenSize * 4)!
}
// Complete multimodal inference pipeline
public func generate(
textTokens: [Int],
audioFeatures: [[Float]]? = nil,
imagePatches: [Float]? = nil,
numImagePatches: Int = 0,
precomputedVisionEmbedding: MTLBuffer? = nil,
maxTokens: Int = 50
) throws -> [Int] {
print("\n═══════════════════════════════════════")
print(" Multimodal Inference Pipeline")
print("═══════════════════════════════════════\n")
var fullTokens = textTokens
let hiddenSize = model.textModel.hiddenSize
var audioTokenCount = 0
var imageTokenCount = 0
// Step 1: Process audio
if let audio = audioFeatures {
print("Step 1: Processing audio...")
print(" Audio frames: \(audio.count)")
fullTokens.append(model.boaTokenId)
audioTokenCount = audio.count
for _ in 0..<audioTokenCount {
fullTokens.append(model.audioTokenId)
}
fullTokens.append(model.eoaTokenId)
if let tower = model.audioTower {
let flatFeatures = audio.flatMap { $0 }
let inputBuffer = engine.device.makeBuffer(
bytes: flatFeatures,
length: flatFeatures.count * MemoryLayout<Float>.stride
)!
try tower.forward(
inputBuffer: inputBuffer,
seqLen: audioTokenCount,
outputBuffer: audioEmbedBuffer
)
print(" ✓ Audio towers forward done")
}
}
// Step 2: Process image
if let precomputed = precomputedVisionEmbedding {
// Pre-computed pooled embedding single IMAGE token
print("Step 2: Using precomputed vision embedding")
fullTokens.append(model.boiTokenId)
fullTokens.append(model.imageTokenId)
fullTokens.append(model.eoiTokenId)
imageTokenCount = 1
// Copy the pooled embedding into visionEmbedBuffer
let cmdBuf = engine.commandQueue.makeCommandBuffer()!
let blit = cmdBuf.makeBlitCommandEncoder()!
blit.copy(from: precomputed, sourceOffset: 0,
to: visionEmbedBuffer, destinationOffset: 0,
size: min(precomputed.length, visionEmbedBuffer.length))
blit.endEncoding()
cmdBuf.commit()
cmdBuf.waitUntilCompleted()
} else if let patches = imagePatches, numImagePatches > 0 {
print("Step 2: Processing image...")
print(" Image patches: \(numImagePatches)")
fullTokens.append(model.boiTokenId)
imageTokenCount = numImagePatches
for _ in 0..<imageTokenCount {
fullTokens.append(model.imageTokenId)
}
fullTokens.append(model.eoiTokenId)
let inputBuffer = engine.device.makeBuffer(
bytes: patches,
length: patches.count * MemoryLayout<Float>.stride
)!
if let tower = model.visionTowerFull {
try tower.forward(patchEmbeddings: inputBuffer, numPatches: imageTokenCount, outputBuffer: visionEmbedBuffer)
} else if let tower = model.visionTower {
try tower.forward(patchEmbeddings: inputBuffer, numPatches: imageTokenCount, outputBuffer: visionEmbedBuffer)
}
print(" ✓ Vision tower forward done")
}
// Step 3: Pre-fill prompt with injection
print("\nStep 3: Pre-filling \(fullTokens.count) tokens...")
var generated = fullTokens
var audioIdx = 0
var imageIdx = 0
for pos in 0..<fullTokens.count {
let tokenId = fullTokens[pos]
if tokenId == model.audioTokenId, audioIdx < audioTokenCount {
let offset = audioIdx * hiddenSize * MemoryLayout<Float>.stride
_ = try model.textModel.forwardFromHidden(hiddenBuffer: audioEmbedBuffer, offset: offset, position: pos)
audioIdx += 1
} else if tokenId == model.imageTokenId, imageIdx < imageTokenCount {
let offset = imageIdx * hiddenSize * MemoryLayout<Float>.stride
_ = try model.textModel.forwardFromHidden(hiddenBuffer: visionEmbedBuffer, offset: offset, position: pos)
imageIdx += 1
} else {
_ = try model.textModel.forward(tokenId: tokenId, position: pos)
}
}
// Step 4: Auto-regressive generation
print("Step 4: Generating \(maxTokens) tokens...")
let sampler = Sampler()
for _ in 0..<maxTokens {
let logits = try model.textModel.forward(
tokenId: generated.last ?? 0,
position: generated.count - 1
)
// Use sampler with unused token filtering
let nextToken = sampler.sample(
logits: logits,
temperature: 0.7,
topK: 50,
topP: 0.95,
filterUnusedTokens: true
)
generated.append(nextToken)
}
let newTokens = generated.count - fullTokens.count
print(" ✓ Generated \(newTokens) tokens")
return generated
}
}