Skip to content

Commit df0f513

Browse files
authored
Merge branch 'main' into sukru/state-handlers
2 parents 7331d82 + 49becc6 commit df0f513

18 files changed

Lines changed: 307 additions & 26 deletions

File tree

python/src/coreai_models/diffusion/pipeline.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,6 @@ def _load_hf_pipeline(model_id: str, pipeline_type: str, model_dtype: torch.dtyp
162162
from diffusers import Flux2KleinPipeline
163163

164164
hf_pipe = Flux2KleinPipeline.from_pretrained(model_id, torch_dtype=model_dtype)
165-
# Text encoder needs float32 for token embedding precision
166-
hf_pipe.text_encoder = hf_pipe.text_encoder.float()
167165
return hf_pipe
168166

169167
if pipeline_type == "sd3":

swift/Sources/CoreAIDiffusionPipeline/Components/CoreAIDenoiser.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public final class CoreAIDenoiser: Sendable {
3131
let batchSize = latents.shape[0]
3232

3333
var timestepArray = NDArray(shape: [batchSize], scalarType: .float32)
34-
var timestepView = timestepArray.mutableView(as: Float.self)
34+
let timestepView = timestepArray.mutableView(as: Float.self)
3535
timestepView.withUnsafeMutablePointer { ptr, _, _ in
3636
for i in 0..<batchSize { ptr[i] = timestep }
3737
}
@@ -58,7 +58,7 @@ public final class CoreAIDenoiser: Sendable {
5858

5959
let shape = latents.shape
6060
var result = NDArray(shape: shape, scalarType: .float32)
61-
var resultView = result.mutableView(as: Float.self)
61+
let resultView = result.mutableView(as: Float.self)
6262
resultView.withUnsafeMutablePointer { ptr, _, _ in
6363
for i in 0..<floats.count { ptr[i] = floats[i] }
6464
}

swift/Sources/CoreAIDiffusionPipeline/Components/CoreAIDiffusionModelFunction.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,13 @@ public actor CoreAIDiffusionModelFunction {
5555
switch resolved.scalarType {
5656
#if !((os(macOS) || targetEnvironment(macCatalyst)) && arch(x86_64))
5757
case .float16:
58-
var view = array.mutableView(as: Float16.self)
58+
let view = array.mutableView(as: Float16.self)
5959
view.withUnsafeMutablePointer { ptr, _, _ in
6060
for j in 0..<data.count { ptr[j] = Float16(data[j]) }
6161
}
6262
#endif
6363
case .float32:
64-
var view = array.mutableView(as: Float.self)
64+
let view = array.mutableView(as: Float.self)
6565
view.withUnsafeMutablePointer { ptr, _, _ in
6666
for j in 0..<data.count { ptr[j] = data[j] }
6767
}
@@ -83,7 +83,7 @@ public actor CoreAIDiffusionModelFunction {
8383
guard case .ndArray(let nd) = fn.descriptor.inputDescriptor(of: name) else { continue }
8484
let resolved = nd.resolvingDynamicDimensions(shape)
8585
var array = NDArray(descriptor: resolved)
86-
var view = array.mutableView(as: Int32.self)
86+
let view = array.mutableView(as: Int32.self)
8787
view.withUnsafeMutablePointer { ptr, _, _ in
8888
for j in 0..<data.count { ptr[j] = data[j] }
8989
}

swift/Sources/CoreAIDiffusionPipeline/Components/CoreAILatentCodec.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public final class CoreAILatentDecoder: Sendable {
4444
let outW = shape[3] * 8
4545
let outShape = [1, 3, outH, outW]
4646
var result = NDArray(shape: outShape, scalarType: .float32)
47-
var resultView = result.mutableView(as: Float.self)
47+
let resultView = result.mutableView(as: Float.self)
4848
resultView.withUnsafeMutablePointer { ptr, _, _ in
4949
for i in 0..<outputFloats.count { ptr[i] = outputFloats[i] }
5050
}
@@ -107,7 +107,7 @@ public final class CoreAILatentEncoder: Sendable {
107107
// Scale and return as NDArray
108108
let outShape = [1, 4, height / 8, width / 8]
109109
var result = NDArray(shape: outShape, scalarType: .float32)
110-
var resultView = result.mutableView(as: Float.self)
110+
let resultView = result.mutableView(as: Float.self)
111111
resultView.withUnsafeMutablePointer { ptr, _, _ in
112112
for i in 0..<outputFloats.count { ptr[i] = outputFloats[i] * scaleFactor }
113113
}

swift/Sources/CoreAIDiffusionPipeline/Pipelines/Flux2Pipeline.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ public struct Flux2Pipeline: DiffusionPipeline {
274274
let image = try DiffusionUtilities.pixelsToCGImage(pixels, height: outputHeight, width: outputWidth)
275275

276276
var latentsND = NDArray(shape: latentShape, scalarType: .float32)
277-
var latentsView = latentsND.mutableView(as: Float.self)
277+
let latentsView = latentsND.mutableView(as: Float.self)
278278
latentsView.withUnsafeMutablePointer { ptr, _, _ in
279279
for i in 0..<noise.count { ptr[i] = noise[i] }
280280
}

swift/Sources/CoreAIDiffusionPipeline/Pipelines/SD3Pipeline.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ public struct SD3Pipeline: DiffusionPipeline {
160160
pixels, height: imageSize, width: imageSize)
161161

162162
var latentsND = NDArray(shape: latentShape, scalarType: .float32)
163-
var latentsView = latentsND.mutableView(as: Float.self)
163+
let latentsView = latentsND.mutableView(as: Float.self)
164164
latentsView.withUnsafeMutablePointer { ptr, _, _ in
165165
for i in 0..<latents.count { ptr[i] = latents[i] }
166166
}
@@ -226,7 +226,7 @@ public struct SD3Pipeline: DiffusionPipeline {
226226
}
227227

228228
var inputArray = NDArray(shape: [1, Self.clipSeqLen], scalarType: .int32)
229-
var view = inputArray.mutableView(as: Int32.self)
229+
let view = inputArray.mutableView(as: Int32.self)
230230
view.withUnsafeMutablePointer { ptr, _, _ in
231231
for i in 0..<ids.count { ptr[i] = ids[i] }
232232
}

swift/Sources/CoreAIDiffusionPipeline/Pipelines/StableDiffusionPipeline.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ public struct StableDiffusionPipeline: DiffusionPipeline {
161161

162162
// Wrap latents back to NDArray for GenerationResult
163163
var latentsND = NDArray(shape: latentShape, scalarType: .float32)
164-
var latentsView = latentsND.mutableView(as: Float.self)
164+
let latentsView = latentsND.mutableView(as: Float.self)
165165
latentsView.withUnsafeMutablePointer { ptr, _, _ in
166166
for i in 0..<latents.count { ptr[i] = latents[i] }
167167
}

swift/Sources/CoreAILanguageModels/GuidedGeneration/ConstrainedGenerationSession.swift

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import Tokenizers
1717
/// Each session is tied to a specific JSON schema and vocabulary. It tracks
1818
/// the generation state and produces token masks that enforce schema compliance.
1919
public struct ConstrainedGenerationSession: ~Copyable {
20+
static let maxRollbackTokens = 64
21+
2022
private let tokenizerInfo: TokenizerInfo
2123
private let compiler: GrammarCompiler
2224
private let compiledGrammar: CompiledGrammar
@@ -93,7 +95,7 @@ public struct ConstrainedGenerationSession: ~Copyable {
9395
self.tokenizerInfo = tokenizerInfo
9496
self.compiler = GrammarCompiler(tokenizerInfo: tokenizerInfo)
9597
self.compiledGrammar = try compiler.compileJSONSchema(jsonSchema)
96-
self.matcher = GrammarMatcher(compiledGrammar: compiledGrammar)
98+
self.matcher = GrammarMatcher(compiledGrammar: compiledGrammar, maxRollbackTokens: Self.maxRollbackTokens)
9799
self.vocabularySize = tokenizerInfo.vocabularySize
98100
self.bitmaskSize = (vocabularySize + 31) / 32
99101
self.bitmaskBuffer = Array(repeating: 0, count: bitmaskSize)
@@ -193,6 +195,50 @@ public struct ConstrainedGenerationSession: ~Copyable {
193195
matcher.reset()
194196
allTokensBlocked = false
195197
}
198+
199+
/// Rollback the grammar state by N tokens. Returns false if rollback failed
200+
/// (e.g., exceeds maxRollbackTokens budget).
201+
@discardableResult
202+
public mutating func rollback(_ numTokens: Int = 1) -> Bool {
203+
guard numTokens >= 0 else { return false }
204+
return matcher.rollback(numTokens)
205+
}
206+
207+
/// Find the longest deterministic string from the current grammar state.
208+
/// Does not change the matcher state. Returns nil if no jump-forward is possible.
209+
public func findJumpForwardString() -> String? {
210+
matcher.findJumpForwardString()
211+
}
212+
213+
/// Result of filling a bitmask for the next token.
214+
public enum BitmaskResult: Equatable {
215+
/// Grammar is terminated or all tokens are blocked — generation should stop.
216+
case terminated
217+
/// All tokens are allowed — no mask needed, generate unconstrained.
218+
case unconstrained
219+
/// Bitmask was written — apply it to constrain sampling.
220+
case constrained
221+
}
222+
223+
/// Fill the bitmask directly into a caller-provided buffer (e.g., a GPU-visible MTLBuffer).
224+
///
225+
/// The caller must ensure the pointer has room for at least `(vocabularySize + 31) / 32`
226+
/// Int32 words.
227+
public mutating func fillBitmask(into pointer: UnsafeMutablePointer<Int32>) -> BitmaskResult {
228+
if isTerminated { return .terminated }
229+
230+
let needsApplication = matcher.fillNextTokenBitmask(pointer)
231+
if !needsApplication {
232+
// xgrammar signals all tokens are allowed — no mask needed
233+
return .unconstrained
234+
}
235+
// Check for all-zeros (no tokens allowed — grammar done)
236+
for i in 0..<bitmaskSize {
237+
if pointer[i] != 0 { return .constrained }
238+
}
239+
allTokensBlocked = true
240+
return .terminated
241+
}
196242
}
197243

198244
// MARK: - Float16 Masking

swift/Sources/CoreAILanguageModels/GuidedGeneration/XGrammarWrapper.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,29 @@ public final class GrammarMatcher {
136136
return xgrammar_matcher_is_terminated(handle)
137137
}
138138

139+
public var isCompleted: Bool {
140+
return xgrammar_matcher_is_completed(handle)
141+
}
142+
139143
public func reset() {
140144
xgrammar_matcher_reset(handle)
141145
}
146+
147+
@discardableResult
148+
public func rollback(_ numTokens: Int = 1) -> Bool {
149+
xgrammar_matcher_rollback(handle, Int32(numTokens))
150+
}
151+
152+
/// Returns the longest deterministic string from the current grammar state,
153+
/// or nil if no jump-forward is possible. Does not change matcher state.
154+
public func findJumpForwardString() -> String? {
155+
guard let cStr = xgrammar_matcher_find_jump_forward_string(handle) else {
156+
return nil
157+
}
158+
let result = String(cString: cStr)
159+
free(UnsafeMutablePointer(mutating: cStr))
160+
return result.isEmpty ? nil : result
161+
}
142162
}
143163

144164
// MARK: - Errors

swift/Sources/CoreAILanguageModels/InferenceEngines/CoreAISequentialEngine.swift

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,85 @@ public final class CoreAISequentialEngine: InferenceEngine, @unchecked Sendable
474474
CLILogger.log("CoreAI clean engine cleanup complete")
475475
cleanupSpan.end()
476476
}
477+
478+
// MARK: - KV Cache (dynamic growth)
479+
480+
private func ensureKVCapacity(forContextLength needed: Int) throws {
481+
guard needed > currentKVCapacity else { return }
482+
guard needed <= config.maxContextLength else {
483+
throw InferenceRuntimeError.invalidState(
484+
"Context length \(needed) exceeds maximum \(config.maxContextLength)")
485+
}
486+
487+
var newCapacity = currentKVCapacity
488+
while newCapacity < needed { newCapacity *= 2 }
489+
newCapacity = min(newCapacity, config.maxContextLength)
490+
491+
let resolvedKeyDesc = keyCacheDescriptor.resolvingDynamicDimensions(
492+
keyCacheDescriptor.shape.map { $0 < 0 ? newCapacity : $0 })
493+
let resolvedValueDesc = valueCacheDescriptor.resolvingDynamicDimensions(
494+
valueCacheDescriptor.shape.map { $0 < 0 ? newCapacity : $0 })
495+
496+
var newKeyCache = NDArray(descriptor: resolvedKeyDesc)
497+
var newValueCache = NDArray(descriptor: resolvedValueDesc)
498+
_ = newKeyCache.mutableRawView()
499+
_ = newValueCache.mutableRawView()
500+
501+
try Self.copyCache(from: keyCache, to: &newKeyCache)
502+
try Self.copyCache(from: valueCache, to: &newValueCache)
503+
504+
CLILogger.log("KV cache grew: \(currentKVCapacity)\(newCapacity)")
505+
keyCache = newKeyCache
506+
valueCache = newValueCache
507+
currentKVCapacity = newCapacity
508+
}
509+
510+
private static func copyCache(from source: NDArray, to destination: inout NDArray) throws {
511+
let srcShape = source.shape
512+
let dstShape = destination.shape
513+
guard let headDim = srcShape.last else {
514+
throw InferenceRuntimeError.invalidState("KV cache has empty shape — cannot copy")
515+
}
516+
let seqDim = KVCacheFactory.detectSequenceDim(shape: srcShape)
517+
518+
// Number of independent blocks before the sequence dimension (L * B * H or B * H)
519+
let numBlocks = srcShape[..<seqDim].reduce(1, *)
520+
let oldSeqLen = srcShape[seqDim]
521+
let copySize = oldSeqLen * headDim
522+
523+
// Strides in elements for the sequence block
524+
let srcBlockStride = srcShape[seqDim...].reduce(1, *) // S_old * D
525+
let dstBlockStride = dstShape[seqDim...].reduce(1, *) // S_new * D
526+
527+
source.view(as: LogitsScalarType.self).withUnsafePointer { srcPtr, _, _ in
528+
let dstView = destination.mutableView(as: LogitsScalarType.self)
529+
dstView.withUnsafeMutablePointer { dstPtr, _, _ in
530+
for block in 0..<numBlocks {
531+
let srcOff = block * srcBlockStride
532+
let dstOff = block * dstBlockStride
533+
dstPtr.advanced(by: dstOff).update(
534+
from: srcPtr.advanced(by: srcOff), count: copySize)
535+
}
536+
}
537+
}
538+
}
539+
540+
// MARK: - Helpers
541+
542+
private func zeroFill(_ array: inout NDArray) {
543+
let count = array.shape.reduce(1, *)
544+
let view = array.mutableView(as: LogitsScalarType.self)
545+
// Inlined constant write — under -Onone, fillNDArray's
546+
// `(Int) -> LogitsScalarType` closure is invoked per element (no inlining),
547+
// which made zeroing the KV cache (~14.7M elements for a 32K-context
548+
// Qwen3) take ~6 seconds per `reset()`. Direct loop keeps this in
549+
// the few-ms range even unoptimized; under -O it lowers to memset.
550+
view.withUnsafeMutablePointer { ptr, _, _ in
551+
for i in 0..<count {
552+
ptr[i] = 0
553+
}
554+
}
555+
}
477556
}
478557

479558
extension CoreAISequentialEngine {

0 commit comments

Comments
 (0)