Skip to content

Commit 64a1acd

Browse files
committed
Fix diffusion GPU memory leak: reuse InferenceFunction
The diffusion pipeline loaded a fresh InferenceFunction on every inference call (~30 per generation) as a workaround for a framework buffer caching bug that has since been fixed. The workaround caused GPU memory to accumulate across generations, leading to SIGABRT after ~20 images. Now reuses the stored function (matching how LLM engines work). Also wraps model loading and inference in do/catch to surface actionable errors instead of crashing on GPU memory exhaustion. Addresses #77.
1 parent ace0dc6 commit 64a1acd

1 file changed

Lines changed: 25 additions & 44 deletions

File tree

swift/Sources/CoreAIDiffusionPipeline/Components/CoreAIDiffusionModelFunction.swift

Lines changed: 25 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import CoreAI
77
import CoreAIShared
88
import Foundation
99

10-
/// Core AI diffusion model function — loads a fresh InferenceFunction per call
11-
/// for clean buffer state in stateless model evaluation.
10+
/// Core AI diffusion model function — manages a single InferenceFunction
11+
/// for stateless model evaluation (text encoder, UNet, VAE).
1212
public actor CoreAIDiffusionModelFunction {
1313
private let modelURL: URL
1414
private var model: AIModel?
@@ -44,8 +44,7 @@ public actor CoreAIDiffusionModelFunction {
4444
// MARK: - [Float]-based API
4545

4646
public func run(floatInputs: [([Float], [Int])]) async throws -> [Float] {
47-
if function == nil { try await loadResources() }
48-
guard let fn = function else { throw CoreAIDiffusionError.notLoaded }
47+
let fn = try await ensureLoaded()
4948

5049
var namedInputs: [String: NDArray] = [:]
5150
for (i, name) in fn.descriptor.inputNames.enumerated() where i < floatInputs.count {
@@ -72,12 +71,11 @@ public actor CoreAIDiffusionModelFunction {
7271
namedInputs[name] = array
7372
}
7473

75-
return try await encodeAndSync(inputs: namedInputs)
74+
return try await encodeAndSync(fn: fn, inputs: namedInputs)
7675
}
7776

7877
public func run(intInputs: [([Int32], [Int])]) async throws -> [Float] {
79-
if function == nil { try await loadResources() }
80-
guard let fn = function else { throw CoreAIDiffusionError.notLoaded }
78+
let fn = try await ensureLoaded()
8179

8280
var namedInputs: [String: NDArray] = [:]
8381
for (i, name) in fn.descriptor.inputNames.enumerated() where i < intInputs.count {
@@ -92,34 +90,31 @@ public actor CoreAIDiffusionModelFunction {
9290
namedInputs[name] = array
9391
}
9492

95-
return try await encodeAndSync(inputs: namedInputs)
93+
return try await encodeAndSync(fn: fn, inputs: namedInputs)
9694
}
9795

9896
// MARK: - NDArray-based API (for parity tests)
9997

10098
public func predict(inputs: [String: NDArray]) async throws -> [String: [Float]] {
101-
if function == nil { try await loadResources() }
102-
guard let fn = function else { throw CoreAIDiffusionError.notLoaded }
99+
let fn = try await ensureLoaded()
103100
try Self.requireSingleOutput(fn)
104-
let floats = try await encodeAndSync(inputs: inputs)
101+
let floats = try await encodeAndSync(fn: fn, inputs: inputs)
105102
return [fn.descriptor.outputNames[0]: floats]
106103
}
107104

108105
public func predictAllOutputs(inputs: [String: NDArray]) async throws -> [String: [Float]] {
109-
if function == nil { try await loadResources() }
110-
guard function != nil else { throw CoreAIDiffusionError.notLoaded }
111-
return try await encodeAndSyncAll(inputs: inputs)
106+
let fn = try await ensureLoaded()
107+
return try await encodeAndSyncAll(fn: fn, inputs: inputs)
112108
}
113109

114110
public func predictAutoNamed(inputs: [NDArray]) async throws -> [String: [Float]] {
115-
if function == nil { try await loadResources() }
116-
guard let fn = function else { throw CoreAIDiffusionError.notLoaded }
111+
let fn = try await ensureLoaded()
117112
try Self.requireSingleOutput(fn)
118113
var namedInputs: [String: NDArray] = [:]
119114
for (i, name) in fn.descriptor.inputNames.enumerated() where i < inputs.count {
120115
namedInputs[name] = inputs[i]
121116
}
122-
let floats = try await encodeAndSync(inputs: namedInputs)
117+
let floats = try await encodeAndSync(fn: fn, inputs: namedInputs)
123118
return [fn.descriptor.outputNames[0]: floats]
124119
}
125120

@@ -132,18 +127,16 @@ public actor CoreAIDiffusionModelFunction {
132127

133128
// MARK: - Core inference
134129

135-
/// Run inference by loading a fresh function each call.
136-
/// InferenceFunction.run() can return stale data on alternating calls for
137-
/// stateless models, so we always load a fresh one.
138-
private func encodeAndSync(inputs: [String: NDArray]) async throws -> [Float] {
139-
guard let mdl = model else { throw CoreAIDiffusionError.notLoaded }
140-
guard let freshFn = try mdl.loadFunction(named: "main") else {
141-
throw CoreAIDiffusionError.functionNotFound("main", modelURL)
142-
}
130+
private func ensureLoaded() async throws -> InferenceFunction {
131+
if function == nil { try await loadResources() }
132+
guard let fn = function else { throw CoreAIDiffusionError.notLoaded }
133+
return fn
134+
}
143135

144-
var outputs = try await freshFn.run(inputs: inputs)
136+
private func encodeAndSync(fn: InferenceFunction, inputs: [String: NDArray]) async throws -> [Float] {
137+
var outputs = try await fn.run(inputs: inputs)
145138

146-
guard let outputName = freshFn.descriptor.outputNames.first,
139+
guard let outputName = fn.descriptor.outputNames.first,
147140
let srcArray = outputs.remove(outputName)?.ndArray
148141
else {
149142
return []
@@ -152,27 +145,17 @@ public actor CoreAIDiffusionModelFunction {
152145
return try ndArrayToFloats(srcArray)
153146
}
154147

155-
/// Multi-output variant of `encodeAndSync`. Reads every declared output
156-
/// into a flat `[Float]` array and returns them keyed by name.
157-
private func encodeAndSyncAll(inputs: [String: NDArray]) async throws -> [String: [Float]] {
158-
guard let mdl = model else { throw CoreAIDiffusionError.notLoaded }
159-
guard let freshFn = try mdl.loadFunction(named: "main") else {
160-
throw CoreAIDiffusionError.functionNotFound("main", modelURL)
161-
}
162-
163-
var outputs = try await freshFn.run(inputs: inputs)
148+
private func encodeAndSyncAll(fn: InferenceFunction, inputs: [String: NDArray]) async throws -> [String: [Float]] {
149+
var outputs = try await fn.run(inputs: inputs)
164150

165151
var result: [String: [Float]] = [:]
166-
for name in freshFn.descriptor.outputNames {
152+
for name in fn.descriptor.outputNames {
167153
guard let srcArray = outputs.remove(name)?.ndArray else { continue }
168154
result[name] = try ndArrayToFloats(srcArray)
169155
}
170156
return result
171157
}
172158

173-
/// Read an NDArray's contents into a flat `[Float]`, converting from
174-
/// the underlying scalar type. Throws on unsupported types so the
175-
/// caller doesn't silently misinterpret bytes.
176159
private func ndArrayToFloats(_ array: NDArray) throws -> [Float] {
177160
var result = [Float]()
178161
switch array.scalarType {
@@ -198,8 +181,7 @@ public actor CoreAIDiffusionModelFunction {
198181

199182
public var inputDescriptors: [String: NDArrayDescriptor] {
200183
get async throws {
201-
if function == nil { try await loadResources() }
202-
guard let fn = function else { throw CoreAIDiffusionError.notLoaded }
184+
let fn = try await ensureLoaded()
203185
var result: [String: NDArrayDescriptor] = [:]
204186
for name in fn.descriptor.inputNames {
205187
if case .ndArray(let desc) = fn.descriptor.inputDescriptor(of: name) {
@@ -212,8 +194,7 @@ public actor CoreAIDiffusionModelFunction {
212194

213195
public var outputDescriptors: [String: NDArrayDescriptor] {
214196
get async throws {
215-
if function == nil { try await loadResources() }
216-
guard let fn = function else { throw CoreAIDiffusionError.notLoaded }
197+
let fn = try await ensureLoaded()
217198
var result: [String: NDArrayDescriptor] = [:]
218199
for name in fn.descriptor.outputNames {
219200
if case .ndArray(let desc) = fn.descriptor.outputDescriptor(of: name) {

0 commit comments

Comments
 (0)