Skip to content

Fold the VLM engine into CoreAISequentialEngine - #388

Open
stikves wants to merge 11 commits into
apple:mainfrom
stikves:sukru/fold-vlm-engine
Open

stikves wants to merge 11 commits into
apple:mainfrom
stikves:sukru/fold-vlm-engine

Conversation

@stikves

@stikves stikves commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

CoreAISequentialVLMEngine duplicated the sequential engine's decode loop, session state and input handling. Vision-language bundles now run on CoreAISequentialEngine, which gains an optional VisionFrontEnd (image and video encoding) and an EmbeddingsInputHandler (token embedding plus image-embedding merge). The VLM class is deleted; a deprecated typealias keeps existing callers compiling.

Changes

Behavior changes

  • A separate-embedding VLM bundle rejects engine variants other than coreai-sequential (the variant was ignored before).
  • is CoreAISequentialVLMEngine matches every sequential engine; use acceptsMultimodalInput.
  • The engine's config is ModelConfig; vision settings are in visionConfig.
  • reset(to:) on the VLM path cancels in-flight generation on partial resets too, and applies the sliding-window and hybrid-state checks.
  • Image prefill on a used session embeds only the new tokens (the old class re-embedded from its cursor, which shifted positions).
  • Embeddings decoders with non-float16 logits fail at load.

Verification

  • Greedy text and saved logits byte-identical to main on smollm2, qwen3-1.7b and phi-4-mini (sequential; pipelined text identical).
  • Muse Glimmer 30B VLM byte-identical on text, long text, image, image logits, full logits and 4-frame video; VLMSessionParityTests 5/5.
  • Perf vs main (M2 Max, 3 interleaved pairs): qwen3-1.7b decode −0.8%, prefill +3.3% at 512 tokens and −2.8% at 4,096, within run-to-run noise. Muse Glimmer 30B VLM image decode 12.42 vs 12.30 tok/s (5 interleaved pairs, 256 tokens), also within noise.

InputLayout uses the output named "logits", else the first output.
processTokenBatch takes a LogitsRequest. Decode and prefill copy the last row;
all-position paths copy every row.
Logits are read back as Float16, so a bfloat16 logits output now fails at load.
Embeds tokens and scatter-merges image embeddings on prefill. Not wired into an engine yet.
Verbatim move of the VLM engine's image and video encoding; VLMModelConfig gets its own file.
SequentialDecodeLoop owns the token sequence, stop handling and sampling for both iterators; GenerationSequence keeps its public name.
Optional VisionFrontEnd and EmbeddingsInputHandler, image prefill on the session, prefix reuse cut at the first image, 2-4 states (apple#276).
makeVLMEngine builds the sequential engine and rejects variant overrides other than coreai-sequential.
A deprecated typealias and init keep existing callers compiling. is CoreAISequentialVLMEngine now matches every sequential engine; use acceptsMultimodalInput.
@stikves
stikves marked this pull request as ready for review October 30, 2026 21:53
@stikves
stikves requested a review from a team as a code owner October 30, 2026 21:53
let (commonPrefix, _) = sessionState.history.resolve(input: input)
let plan = Self.prefixResetPlan(
commonPrefix: commonPrefix,
commonPrefix: Self.reusablePrefix(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently a text-only request whose tokens contain image placeholders silently loses the image

This is important since this whole refactor is to make multimodal reuse the text path

}

extension MultimodalInferenceEngine {
public var acceptsMultimodalInput: Bool { true }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flag being true makes a text only static shape engine claim image support, the risk is that for such a flag (capability flag), a default true is risky. This means that conformer who never thought about it now claims that. Let's not have a default, or a conservative false

languageModelURL: URL,
options: EngineOptions
) async throws -> any InferenceEngine {
_ = try resolveVLMVariant(override: options.variant)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variant checks one token_merge branch but not the other

So --inference-engine-variant coreai-sequential on a Gemma 4 VLM quietly builds a StaticShapeEngine, while the wrong variant on Qwen throws. Let's keep the switch visionConfig.tokenMerge check the variant for both branches

let strategy = engine.selectPrefillStrategy(newTokenCount: allTokens.count)
switch strategy {
case .chunked(let chunkSize):
allLogits = try await engine.processChunkedPromptAllLogits(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Batched forced continuation ignored prefix cache?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants