66import CoreAI
77import CoreAIShared
88import Foundation
9+ import Synchronization
910
1011/// Static-shape inference engine using Core AI models.
1112public final class StaticShapeEngine : InferenceEngine , @unchecked Sendable {
@@ -47,6 +48,10 @@ public final class StaticShapeEngine: InferenceEngine, @unchecked Sendable {
4748 // Number of tokens already processed in the current sequence.
4849 private var processedTokenCount : Int = 0
4950
51+ // Track in-flight generation for drain/cancel
52+ private let generating = Mutex ( false )
53+ private let cancelRequested = Mutex ( false )
54+
5055 // MARK: - Initialization
5156
5257 public init ( configuration: ModelConfig , preparedModel: PreparedModel ) async throws {
@@ -531,7 +536,39 @@ public final class StaticShapeEngine: InferenceEngine, @unchecked Sendable {
531536
532537 // MARK: - Lifecycle
533538
539+ public var isBusy : Bool { generating. withLock { $0 } }
540+
541+ public func cancel( ) async {
542+ cancelRequested. withLock { $0 = true }
543+ var attempts = 0
544+ while generating. withLock ( { $0 } ) {
545+ attempts += 1
546+ if attempts > Self . drainTimeoutIterations {
547+ CLILogger . log (
548+ " WARNING: StaticShape engine cancel() drain timeout after \( Self . drainTimeoutIterations) iterations " ,
549+ component: " StaticShape "
550+ )
551+ break
552+ }
553+ await Task . yield ( )
554+ }
555+ cancelRequested. withLock { $0 = false }
556+ }
557+
558+ /// Wait for any in-flight generate() iteration to finish.
559+ private func drain( ) {
560+ var attempts = 0
561+ while generating. withLock ( { $0 } ) {
562+ attempts += 1
563+ if attempts > Self . drainTimeoutIterations {
564+ fatalError ( " StaticShape engine drain() timeout — generation stuck? " )
565+ }
566+ Thread . sleep ( forTimeInterval: 0.001 )
567+ }
568+ }
569+
534570 public func reset( ) {
571+ drain ( )
535572 let resetSpan = InstrumentsProfiler . beginReset ( engine: " StaticShape " )
536573 processedTokenCount = 0
537574 resetSpan. end ( )
@@ -595,6 +632,8 @@ extension StaticShapeEngine.GenerationSequence {
595632
596633 private var inputTokens : [ StaticShapeEngine . TokenId ]
597634 private var step : Int = 0
635+ private var didAcquireLock : Bool = false
636+ private var finished : Bool = false
598637
599638 init (
600639 engine: StaticShapeEngine ,
@@ -620,15 +659,29 @@ extension StaticShapeEngine.GenerationSequence {
620659 }
621660
622661 public mutating func next( ) async throws -> InferenceOutput ? {
662+ if finished { return nil }
663+
664+ if !didAcquireLock {
665+ engine. generating. withLock { $0 = true }
666+ didAcquireLock = true
667+ }
668+
623669 guard step < maxTokens else {
624670 // Natural exhaustion. Don't clobber a reason a decoder set (e.g. `.eos`).
625671 stopReasonStore. setIfUnset ( . maxTokens)
672+ finishAndRelease ( )
626673 return nil
627674 }
628675
629676 do {
630677 try Task . checkCancellation ( )
631678
679+ if engine. cancelRequested. withLock ( { $0 } ) {
680+ stopReasonStore. set ( . cancelled)
681+ finishAndRelease ( )
682+ return nil
683+ }
684+
632685 // When forced, we still need the forward pass (for logits + KV cache update)
633686 // but skip the sampler — the next token is predetermined.
634687 let ( logits, sampledToken) = try await engine. inference (
@@ -647,11 +700,22 @@ extension StaticShapeEngine.GenerationSequence {
647700 )
648701 } catch is CancellationError {
649702 stopReasonStore. set ( . cancelled)
703+ finishAndRelease ( )
650704 throw CancellationError ( )
651705 } catch {
652706 stopReasonStore. set ( . error)
707+ finishAndRelease ( )
653708 throw error
654709 }
655710 }
711+
712+ private mutating func finishAndRelease( ) {
713+ guard !finished else { return }
714+ finished = true
715+ if didAcquireLock {
716+ engine. generating. withLock { $0 = false }
717+ didAcquireLock = false
718+ }
719+ }
656720 }
657721}
0 commit comments