Skip to content

Commit 162ee99

Browse files
authored
VLM: fix EmbeddedInput shape handling, error recovery, and parallel loading (#79)
* VLM: fix tokenCount for 2D tensors, replace precondition with throw - EmbeddedInput.tokenCount: handle 2D [seq_len, hidden_dim] vs 3D [batch, seq_len, hidden_dim] - scatterMerge: replace precondition(float16) with guard/throw for bfloat16 compatibility * Address review: remove premature maxTiles flag, centralize embedding shape handling - Remove --max-tiles CLI option (tiling model not implemented yet) - Move seq_len extraction into EmbeddedInput.seqLen(of:) so scatterMerge and other call sites use one canonical shape resolution path - tokenCount is now a stored property computed once at init * Simplify EmbeddedInput: assume 3D [batch, seq_len, hidden_dim] layout All current VLM models produce 3D embeddings. Drop the 2D fallback and the seqLen helper -- tokenCount is just shape[1]. * EmbeddedInput: validate exactly 3D shape at init, throw on mismatch * Fix test: EmbeddedInput.init is now throwing * Fix test: mark embeddedInputBasics as throws
1 parent d5a78c8 commit 162ee99

4 files changed

Lines changed: 18 additions & 14 deletions

File tree

swift/Sources/CoreAILanguageModels/InferenceEngines/CoreAISequentialVLMEngine.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ public final class CoreAISequentialVLMEngine: MultimodalInferenceEngine, @unchec
385385

386386
CLILogger.log("VLM encodeImage complete: \(tokenCount) embedding tokens")
387387

388-
return EmbeddedInput(
388+
return try EmbeddedInput(
389389
embeddings: projectedEmbeddings,
390390
embeddingPositions: placeholderRange
391391
)
@@ -560,8 +560,8 @@ public final class CoreAISequentialVLMEngine: MultimodalInferenceEngine, @unchec
560560
+ "expected \(imageTokenCount) from config. Check prompt template.")
561561
}
562562

563-
let seqLen = textEmbeddings.shape.count >= 2 ? textEmbeddings.shape[1] : 0
564-
let imgSeqLen = imageEmbeddings.shape.count >= 2 ? imageEmbeddings.shape[1] : 0
563+
let seqLen = textEmbeddings.shape[1]
564+
let imgSeqLen = imageEmbeddings.shape[1]
565565
guard imgSeqLen >= imageTokenCount else {
566566
throw InferenceRuntimeError.invalidArgument(
567567
"scatterMerge: image embeddings have \(imgSeqLen) tokens, need \(imageTokenCount)")
@@ -574,10 +574,10 @@ public final class CoreAISequentialVLMEngine: MultimodalInferenceEngine, @unchec
574574
}
575575

576576
// Copy image embeddings into placeholder positions.
577-
precondition(
578-
imageEmbeddings.scalarType == .float16,
579-
"scatterMerge only supports float16 embeddings; got \(imageEmbeddings.scalarType)"
580-
)
577+
guard imageEmbeddings.scalarType == .float16 else {
578+
throw InferenceRuntimeError.invalidInputType(
579+
"scatterMerge only supports float16 embeddings; got \(imageEmbeddings.scalarType)")
580+
}
581581
imageEmbeddings.view(as: Float16.self).withUnsafePointer { imgPtr, _, _ in
582582
var mutableView = merged.mutableView(as: Float16.self)
583583
mutableView.withUnsafeMutablePointer { mergedPtr, _, _ in

swift/Sources/CoreAILanguageModels/InferenceEngines/EmbeddedInput.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,25 @@ import Foundation
1212
/// language model. The engine performs scatter-merge: replacing placeholder
1313
/// token positions with these embeddings before the first forward pass.
1414
public struct EmbeddedInput: Sendable {
15-
/// The embedding tensor, typically shape [1, seq_len, hidden_dim].
15+
/// The embedding tensor, shape [batch, seq_len, hidden_dim].
1616
/// Scalar type matches the LLM's expected input (float16, bFloat16, etc.).
1717
public let embeddings: NDArray
1818

1919
/// Positions in the token sequence where embeddings replace placeholders.
2020
public let embeddingPositions: Range<Int>
2121

22-
public init(embeddings: NDArray, embeddingPositions: Range<Int>) {
22+
public init(embeddings: NDArray, embeddingPositions: Range<Int>) throws {
23+
guard embeddings.shape.count == 3 else {
24+
throw InferenceRuntimeError.invalidArgument(
25+
"EmbeddedInput requires 3D embeddings [batch, seq_len, hidden_dim], "
26+
+ "got shape with \(embeddings.shape.count) dimensions")
27+
}
2328
self.embeddings = embeddings
2429
self.embeddingPositions = embeddingPositions
2530
}
2631

2732
/// Number of embedding tokens (seq_len dimension).
28-
public var tokenCount: Int {
29-
embeddings.shape.count >= 2 ? embeddings.shape[1] : 0
30-
}
33+
public var tokenCount: Int { embeddings.shape[1] }
3134

3235
// TODO: Multi-turn support — allow multiple image regions per input,
3336
// persistent across generate() calls (keep in KV cache on reset).

swift/Sources/Tools/llm-runner/LLMRunnerMain.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@ struct LLMRunner: AsyncParsableCommand, Sendable {
371371
)
372372
let vlmConfig = VLMModelConfig(base: baseConfig, visionConfig: visionConfig)
373373

374+
// Sequential to avoid runtime errors with concurrent model preparation.
374375
let visionModel = try await PreparedModel.prepare(at: visionURL)
375376
let embedModel = try await PreparedModel.prepare(at: embedURL)
376377
let llmModel = try await PreparedModel.prepare(at: mainURL)

swift/Tests/LanguageModelsTests/VLMProtocolTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ import CoreAI
1616
struct MultimodalTypeTests {
1717
#if canImport(CoreAI)
1818
@Test("EmbeddedInput wraps NDArray with positions")
19-
func embeddedInputBasics() {
19+
func embeddedInputBasics() throws {
2020
let embeddings = NDArray(
2121
shape: [1, 256, 2048],
2222
scalarType: .float16
2323
)
24-
let input = EmbeddedInput(
24+
let input = try EmbeddedInput(
2525
embeddings: embeddings,
2626
embeddingPositions: 5..<261
2727
)

0 commit comments

Comments
 (0)