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
+89
View File
@@ -0,0 +1,89 @@
import Metal
/// Buffer pool for reusing MTLBuffers to reduce allocation overhead
///
/// Metal buffer allocation can be expensive, especially when done frequently
/// during inference. This pool caches and reuses buffers of common sizes.
public final class BufferPool: @unchecked Sendable {
private let device: MTLDevice
private var availableBuffers: [Int: [MTLBuffer]] = [:] // size -> [buffers]
private let lock = NSLock()
/// Statistics
public private(set) var totalAllocations: Int = 0
public private(set) var totalReuses: Int = 0
public private(set) var peakBufferCount: Int = 0
public init(device: MTLDevice) {
self.device = device
}
/// Acquire a buffer of the specified size
/// Returns a reusable buffer if available, otherwise creates a new one
public func acquire(length: Int) -> MTLBuffer {
lock.lock()
defer { lock.unlock() }
// Round up to nearest 256 bytes for alignment
let alignedLength = (length + 255) & ~255
// Check for available buffer
if var buffers = availableBuffers[alignedLength], !buffers.isEmpty {
let buffer = buffers.removeLast()
availableBuffers[alignedLength] = buffers
totalReuses += 1
return buffer
}
// Create new buffer
totalAllocations += 1
guard let buffer = device.makeBuffer(
length: alignedLength,
options: .storageModeShared
) else {
fatalError("Failed to allocate Metal buffer of size \(alignedLength)")
}
// Track peak
let currentCount = totalAllocations - totalReuses
if currentCount > peakBufferCount {
peakBufferCount = currentCount
}
return buffer
}
/// Release a buffer back to the pool for reuse
public func release(_ buffer: MTLBuffer) {
lock.lock()
defer { lock.unlock() }
let length = buffer.length
if availableBuffers[length] == nil {
availableBuffers[length] = []
}
availableBuffers[length]?.append(buffer)
}
/// Clear all cached buffers (useful for memory pressure situations)
public func clear() {
lock.lock()
defer { lock.unlock() }
availableBuffers.removeAll()
}
/// Get pool statistics
public var stats: String {
lock.lock()
defer { lock.unlock() }
let totalBuffers = availableBuffers.values.reduce(0) { $0 + $1.count }
return """
BufferPool Stats:
Allocations: \(totalAllocations)
Reuses: \(totalReuses)
Available: \(totalBuffers)
Peak: \(peakBufferCount)
Hit Rate: \(totalAllocations > 0 ? String(format: "%.1f%%", Float(totalReuses) / Float(totalAllocations) * 100) : "0%")
"""
}
}