@@ -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
479558extension CoreAISequentialEngine {
0 commit comments