Files
MarkBase Admin ac75faa0cc
CI / build-and-test (push) Has been cancelled
Initial commit: E4B-MarkBase model integration with passing tests
- E4B-MarkBase model (42 layers, 4.4GB) loaded successfully
- All Phase 1-6 tests passed (model loading, forward pass, vision/audio towers, token generation, performance)
- All stress tests passed (5/5 in 127.6s)
  - Concurrent inference
  - Memory stress (67.5 tok/s, 0 NaN)
  - Continuous generation
  - Batch processing
  - Long-running stability
- Swift Metal inference engine with multimodal support
2026-06-23 18:12:35 +08:00

244 lines
8.4 KiB
Swift

import Metal
// Batch generation extension for E4BModel
// Goal: Generate multiple tokens in one pass (reduce kernel dispatches)
extension E4BModel {
/// Batch forward pass - process multiple tokens at once
/// Reduces kernel dispatches from 854*N → 854 (for N tokens)
/// Expected improvement: ~8x for batch generation
public func forwardBatch(tokenIds: [Int], positions: [Int]) throws -> [[Float]] {
guard tokenIds.count == positions.count else {
throw NSError(domain: "Batch", code: -1,
userInfo: [NSLocalizedDescriptionKey: "tokenIds and positions must have same count"])
}
let batchSize = tokenIds.count
if batchSize == 0 { return [] }
if batchSize == 1 {
return [try forwardOptimized(tokenId: tokenIds[0], position: positions[0])]
}
let cmdBuf = engine.commandQueue.makeCommandBuffer()!
let device = engine.device
let batchInputBuffer = device.makeBuffer(
length: batchSize * hiddenSize * MemoryLayout<Float>.stride
)!
let batchOutputBuffer = device.makeBuffer(
length: batchSize * vocabSize * MemoryLayout<Float>.stride
)!
// Process embeddings in batch
var batchEmbeddings: [[Float]] = []
for i in 0..<batchSize {
let embedding = try dequantizeEmbedding(tokenId: tokenIds[i])
batchEmbeddings.append(embedding)
}
// Flatten embeddings for batch processing
var flatEmbeddings: [Float] = []
for emb in batchEmbeddings {
flatEmbeddings.append(contentsOf: emb)
}
let inputPtr = batchInputBuffer.contents().assumingMemoryBound(to: Float.self)
for i in 0..<flatEmbeddings.count {
inputPtr[i] = flatEmbeddings[i]
}
// Batch layer processing (simplified - sequential for now)
// TODO: True batch layer processing with shared weights
for i in 0..<batchSize {
let offset = i * hiddenSize
let singleInput = device.makeBuffer(
bytesNoCopy: inputPtr + offset,
length: hiddenSize * 4,
options: MTLResourceOptions.storageModeShared
)!
// Process through layers (using shared command buffer)
try processLayersBatch(
input: singleInput,
position: positions[i],
cmdBuf: cmdBuf,
outputOffset: i * vocabSize
)
}
// Commit and wait
cmdBuf.commit()
cmdBuf.waitUntilCompleted()
// Read batch outputs
let outputPtr = batchOutputBuffer.contents().assumingMemoryBound(to: Float.self)
var results: [[Float]] = []
for i in 0..<batchSize {
let offset = i * vocabSize
let logits = Array(UnsafeBufferPointer<Float>(
start: outputPtr + offset,
count: vocabSize
))
results.append(logits)
}
return results
}
/// Helper: Dequantize embedding for a single token
private func dequantizeEmbedding(tokenId: Int) throws -> [Float] {
let device = engine.device
let tempBuffer = device.makeBuffer(length: hiddenSize * 4)!
let cmdBuf = engine.commandQueue.makeCommandBuffer()!
try dequantizeRowOptimized(
weight: embedWeight,
tokenId: tokenId,
output: tempBuffer,
cmdBuf: cmdBuf
)
if embedScale != 1.0 {
try scaleBufferOptimized(tempBuffer, scale: embedScale, count: hiddenSize, cmdBuf: cmdBuf)
}
cmdBuf.commit()
cmdBuf.waitUntilCompleted()
let ptr = tempBuffer.contents().assumingMemoryBound(to: Float.self)
return Array(UnsafeBufferPointer(start: ptr, count: hiddenSize))
}
/// Helper: Process layers for batch generation
private func processLayersBatch(
input: MTLBuffer,
position: Int,
cmdBuf: MTLCommandBuffer,
outputOffset: Int
) throws {
// For now, use existing layer forward (batching would require kernel modification)
// This still saves embedding/lm_head dispatches
let h = input
// Process through all layers
for layerIdx in 0..<numHiddenLayers {
let isOwner = layerIdx < firstKVShared
let cacheIdx = isOwner ? layerIdx : (kvSourceMap[layerIdx] ?? (layerIdx - numKvShared))
let cache = kvCaches[cacheIdx]
let plOffset = perLayerInputSize > 0 ?
layerIdx * perLayerInputSize * MemoryLayout<Float>.stride : 0
try layers[layerIdx].forwardOptimized(
input: h,
position: position,
kvCache: cache,
shouldStoreKV: isOwner,
temps: temps,
engine: engine,
cmdBuf: cmdBuf,
perLayerInput: perLayerEmbedBuffer,
perLayerInputOffset: plOffset
)
}
// Final norm
var lmInput = h
if let fn = finalNorm {
try rmsNormOptimized(input: h, weight: fn, output: temps.ns,
count: hiddenSize, cmdBuf: cmdBuf)
lmInput = temps.ns
}
// LM head (batched output)
// Note: This would need special handling for true batching
try quantizedMatmulOptimized(
input: lmInput,
weights: embedWeight,
output: logitsBuffer,
cmdBuf: cmdBuf
)
// Logits scaling
if embedWeight.groupSize == 32 && embedWeight.inDim == hiddenSize {
let logitsScale = Float(30.0 / 116.23 / sqrt(Float(hiddenSize)))
try scaleBufferOptimized(logitsBuffer, scale: logitsScale, count: vocabSize, cmdBuf: cmdBuf)
}
// Softcapping
if let cap = finalLogitSoftcapping {
try applyLogitSoftcappingOptimized(
buffer: logitsBuffer,
cap: cap,
count: vocabSize,
cmdBuf: cmdBuf
)
}
}
/// Batch generation - generate N tokens with batch processing
public func generateBatch(startToken: Int, numTokens: Int) throws -> [Int] {
var tokens: [Int] = [startToken]
var allLogits: [[Float]] = []
// Generate in batches of 8 (optimal for most GPUs)
let batchSize = 8
while tokens.count < numTokens {
let remaining = numTokens - tokens.count
let currentBatchSize = min(batchSize, remaining)
// Prepare batch inputs
var batchTokens: [Int] = []
var batchPositions: [Int] = []
for i in 0..<currentBatchSize {
let pos = tokens.count - 1 + i
if i == 0 {
batchTokens.append(tokens.last!)
} else {
// Use predicted token from previous batch
if let prevLogits = allLogits.last {
let nextToken = argmax(prevLogits)
batchTokens.append(nextToken)
} else {
batchTokens.append(tokens.last!)
}
}
batchPositions.append(pos)
}
// Batch forward
let batchLogits = try forwardBatch(
tokenIds: batchTokens,
positions: batchPositions
)
// Select next tokens
for logits in batchLogits {
let nextToken = argmax(logits)
tokens.append(nextToken)
allLogits.append(logits)
if tokens.count >= numTokens { break }
}
}
return tokens
}
/// Helper: Argmax for token selection
private func argmax(_ logits: [Float]) -> Int {
var maxIdx = 0
var maxVal = logits[0]
for i in 1..<logits.count {
if logits[i] > maxVal {
maxVal = logits[i]
maxIdx = i
}
}
return maxIdx
}
}