Skip to content

feat(models): add InternVL (internvl_chat) VLM architecture - #37

Merged
inureyes merged 1 commit into
mainfrom
feature/internvl-vlm
May 20, 2026
Merged

inureyes merged 1 commit into
mainfrom
feature/internvl-vlm

Conversation

@inureyes

Copy link
Copy Markdown
Member

Summary

Adds end-to-end support for the InternVL VLM family (internvl_chat, e.g. internvl3-1b) so it loads and runs in both text-only and image modes, wired into the real CLI and server VLM dispatch rather than left as standalone modules.

Architecture

Verified against the real config.json and safetensors tensor names, which override the "reuse the InternLM backbone" assumption:

  • Language model is Qwen2, not InternLM. text_config.model_type == "qwen2" (hidden 896, 24 layers, 14 heads, 2 KV heads, QKV bias, untied embeddings, rope_theta 1e6), so the existing Qwen2 backbone (models::Qwen2Model) is reused rather than reimplementing a transformer. The bogus top-level llm_config (vocab 151936, 32B-template dims) is ignored in favor of the real text_config (vocab 151674).
  • InternViT vision tower (vision_model.*): a standard SDPA encoder with fused QKV (+bias), per-layer layer-scale (ls1/ls2), LayerNorm, a Conv2d patch embed, a CLS token, and a learned position embedding. qk_normalization is false for InternVL3 so q/k norms are skipped. select_layer=-1 returns the last hidden state and the CLS token is stripped before the connector. Every tile is exactly 448x448, so the patch grid matches the trained position-embed grid and interpolation is the identity.
  • Connector mlp1.*: pixel_shuffle(downsample_ratio=0.5) reshapes (B, 1024, 1024) into (B, 256, 4096), then LayerNorm -> Linear(4096->896) -> GELU -> Linear(896->896), yielding 256 image tokens per tile in the Qwen2 hidden size.
  • Dynamic-tiling processor: closest-aspect-ratio tiling (min 1, max 12 tiles, plus a thumbnail when split), ImageNet normalization, channels-first output.
  • Image-token merge replaces <IMG_CONTEXT> (151667, resolved from the tokenizer) positions with the connector output through the shared merge_llava path.

Mixed precision: the top-level quantization block makes the common VLM weight loader treat the checkpoint as quantized and skip its bf16->f16 conversion, keeping the 4-bit LLM and connector scales/biases as bf16 for quantized_matmul. The InternViT tower and connector LayerNorm are plain bf16 (which JIT-crash on M5), so the loader explicitly converts every remaining bf16 tensor to f16 except .scales/.biases, mirroring the standard Apple Silicon non-quantized-weight policy.

RoPE: standard 1D RoPE (not Qwen2-VL MRoPE). The config's rope_scaling dynamic-NTK factor is parsed but not applied in the rope path; for the 1B at short context this is a no-op and is noted as a known limitation.

Integration

  • models/detection.rs: model_type == "internvl_chat" -> ModelType::InternVLChatVLM.
  • models/mod.rs, model_metadata.rs: new InternVLChatVLM ModelType plus its Vlm metadata descriptor so the CLI and server both route it as a VLM.
  • loaded_model.rs: LoadedModel::InternVLChatVLM variant with LanguageModel delegation.
  • loaded_model_capabilities.rs, multimodal/vlm_runtime.rs: VlmRuntimeRef::InternVL runtime arm with tile-count-driven <img> + <IMG_CONTEXT>*N + </img> token expansion.
  • loading/vlm_internvl.rs: load_internvl_vlm() (config parse, mixed-precision weight handling, Qwen2 + InternViT + connector + processor construction, token-id/EOS resolution).
  • New modules: vision/encoders/internvl.rs, vision/internvl.rs, vision/processors/internvl.rs, multimodal/internvl_prompt.rs.
  • distributed/tensor_parallel/inference.rs: maps InternVL onto the llama-family planner placeholder (TP is already refused for VLM-kind models upstream).

M5 embedding-prefill fixes

InternVL is the first VLM short enough to exercise the single-pass prefill path on the M5 Neural Accelerator, which surfaced two embedding-prefill bugs fixed here:

  • Skip NA-tile prefill padding for embedding requests. NA prefill alignment padded the token-id vector and built a matching padded causal mask, but VLM image requests run through forward_with_embeddings with pre-merged embeddings that are not padded, so a 293-token image prompt met a 320-wide mask and aborted with a broadcast-shape error. Alignment is now skipped whenever a sequence carries pre-merged embeddings; text-only prefill is unchanged.
  • Route VLM embedding prefill to full prefill, not chunked. Prompts longer than prefill_chunk_size (default 512) were sent to the chunked path, which re-triggered the same broadcast abort and is incoherent for embedding-based prefill (the whole embedding sequence is consumed while the prefill offset advances one chunk, corrupting cache bookkeeping). A multi-tile image (e.g. a 2:1 photo at 768+ image tokens) trivially exceeds 512 and hit this path; the chunked route is now gated on the absence of pre-merged embeddings, mirroring the batched-prefill dispatch that already forces VLMs onto full prefill.

Testing

  • cargo fmt --all -- --check — clean.
  • cargo clippy --features metal,accelerate --all-targets -- -D warnings — clean.
  • 13 new/related unit tests pass: pixel_shuffle shape collapse (1024x1024 -> 256x4096), dynamic-tiling processor (single-tile square, multi-tile + thumbnail, ImageNet normalization, per-image tile counts), prompt-token insertion/expansion, and the two VLM prefill routing guards (exercised without M5 hardware).
  • Model-gated tests/internvl_parity.rs covers detection, processor shapes, and finite text-only and image-conditioned logits of the correct vocab dimension (runs when the model is present).

Add end-to-end support for the InternVL VLM family so `internvl3-1b` loads and runs in both text-only and image modes, wired into the real CLI and server VLM dispatch.

Architecture (verified against the real config.json and safetensors tensor names, which override the "reuse the InternLM backbone" assumption):

- The language model is Qwen2, not InternLM. `text_config.model_type == "qwen2"` (hidden 896, 24 layers, 14 heads, 2 KV heads, QKV bias, untied embeddings, rope_theta 1e6), so we reuse mlxcel's existing Qwen2 backbone (`models::Qwen2Model`) rather than reimplementing a transformer. The bogus top-level `llm_config` (vocab 151936, 32B-template dims) is ignored in favor of the real `text_config` (vocab 151674).
- The InternViT vision tower (`vision_model.*`) is a standard SDPA encoder with fused QKV (+bias), per-layer layer-scale (ls1/ls2), LayerNorm, a Conv2d patch embed, a CLS token, and a learned position embedding. `qk_normalization` is false for InternVL3 so the q/k norms are skipped. `select_layer=-1` returns the last hidden state and the CLS token is stripped before the connector. Every tile is exactly 448x448, so the patch grid matches the trained position-embed grid and interpolation is the identity.
- The connector `mlp1.*` applies pixel_shuffle(downsample_ratio=0.5) to reshape (B, 1024, 1024) into (B, 256, 4096), then LayerNorm -> Linear(4096->896) -> GELU -> Linear(896->896), yielding 256 image tokens per tile in the Qwen2 hidden size.
- The dynamic-tiling processor picks the closest aspect ratio (min 1, max 12 tiles, plus a thumbnail when split), applies ImageNet normalization, and emits channels-first tensors.
- Image-token merge replaces `<IMG_CONTEXT>` (151667, resolved from the tokenizer) positions with the connector output through the shared `merge_llava` path.

Mixed precision: the top-level `quantization` block makes the common VLM weight loader treat the checkpoint as quantized and skip its bf16->f16 conversion, which correctly keeps the 4-bit LLM and connector scales/biases as bf16 for `quantized_matmul`. The InternViT tower and the connector LayerNorm are plain bf16, which JIT-crash on M5, so the loader explicitly converts every remaining bf16 tensor to f16 except `.scales`/`.biases`, mirroring the standard Apple Silicon non-quantized-weight policy.

RoPE: standard 1D RoPE is used (not Qwen2-VL MRoPE). The config's `rope_scaling` dynamic-NTK factor is parsed but not applied in the rope path; for the 1B at short context this is a no-op and is noted as a known limitation.

Integration: detection maps `model_type == "internvl_chat"` to the new `ModelType::InternVLChatVLM`, which carries a Vlm metadata descriptor so both the CLI and the server route it as a VLM. A `LoadedModel::InternVLChatVLM` variant delegates the language model, a `VlmRuntimeRef::InternVL` runtime arm drives tile-count-based `<img> + <IMG_CONTEXT>*N + </img>` token expansion, and `load_internvl_vlm()` parses the config, handles the mixed-precision weights, and constructs the Qwen2 backbone, InternViT tower, connector, and processor. New modules live under `vision/encoders/internvl.rs`, `vision/internvl.rs`, `vision/processors/internvl.rs`, and `multimodal/internvl_prompt.rs`. Tensor-parallel inference maps InternVL onto the llama-family planner placeholder (TP is already refused for VLM-kind models upstream of that point).

This change also fixes embedding-based VLM prefill on the M5 Neural Accelerator, which InternVL is the first VLM short enough to exercise on the single-pass prefill path:

- Skip NA-tile prefill padding for embedding requests. The NA prefill alignment padded the token-id vector and built a matching padded causal mask, but VLM image requests run through `forward_with_embeddings` with pre-merged embeddings that are not padded, so a 293-token image prompt met a 320-wide mask and aborted with a broadcast-shape error. NA-tile alignment is now skipped whenever a sequence carries pre-merged embeddings; the backbone then builds a causal mask sized to the embeddings, matching the CLI generate path. Token-id (text-only) prefill is unchanged.
- Route VLM embedding prefill to full prefill instead of chunked. Prompts longer than `prefill_chunk_size` (default 512) were sent to the chunked path, which re-triggered the same broadcast abort and is fundamentally incoherent for embedding-based prefill (the whole embedding sequence is consumed while the prefill offset advances by only one chunk, corrupting cache bookkeeping). A multi-tile image (e.g. a 2:1 photo at 768+ image tokens) trivially exceeds 512 and hit this latent path; the chunked route is now gated on the sequence having no pre-merged embeddings, mirroring the batched-prefill dispatch which already forces VLMs onto full prefill.

Tests: unit tests cover the pixel_shuffle shape collapse (1024x1024 -> 256x4096), the dynamic-tiling processor (single-tile square, multi-tile plus thumbnail, ImageNet normalization, per-image tile counts), prompt-token insertion and expansion, and the two VLM prefill routing guards (both conditioned on the absence of pre-merged embeddings, exercised without requiring M5 hardware). A model-gated `tests/internvl_parity.rs` covers detection, processor shapes, and finite text-only and image-conditioned logits of the correct vocab dimension.
@inureyes inureyes added type:enhancement New features, capabilities, or significant additions area:models Model architectures, weights, loading, metadata area:inference Generation, sampling, decoding (incl. speculative, DRY) labels May 20, 2026
@inureyes
inureyes merged commit e6429b4 into main May 20, 2026
3 checks passed
@inureyes
inureyes deleted the feature/internvl-vlm branch May 20, 2026 14:09
inureyes added a commit that referenced this pull request May 23, 2026
Bump version to 0.0.30. Highlights: unified pre-load memory estimator with the new mlxcel inspect subcommand and --estimate-memory preflight (#64, #65, #66, #67), Molmo v1 and InternVL VLM architectures (#41, #37), Gemma 3n / Phi-3.5 / Gemma / Jamba decode performance work (#42, #43, #44, #60, #62), and server --ctx-size as a shared parallel context budget (#57).
@inureyes inureyes added this to the 0.1 milestone Jun 21, 2026
sjswerdloff added a commit to sjswerdloff/mlxcel that referenced this pull request Jul 11, 2026
…blup#37 (not fixed here)

From Xander's blind-spot pass, triaged three-way with Violet (independent
verdicts converged at source):

- REFUTED at source: 'fetch_kvarn8_blocks missing v4 guard' — the guard
  is at cache.rs:1507-1508 since 34df406 and its should_panic passed in
  witnessed runs on both seats. No change.
- FIXED (hygiene, PM-blessed ride-along): downgrade_kvarn8_to_fp16_if_empty
  now resets kvarn_v_bits=8 with the mode — stale width on an Fp16 cache
  was inert but a trap for any future re-mode. Test with named mutation.
- MARKED, NOT FIXED (task lablup#37, Violet PM call): KVCache::nbytes counts no
  kvarn/m3_idx state (~0 for kvarn caches). Store admission unaffected
  (DetachedKVCache::nbytes counts kvarn); the live pool's
  memory_usage_bytes under-reports active kvarn sequences. The fix
  changes what scheduler accounting sees on CURRENT k8v8 production and
  the m3_idx term touches every M3 boot — admission-policy impact wants
  its own sizing. Implementation staged:
  clement-7074f29f/staged_patches/task37_kvcache_nbytes_kvarn_m3idx_20260711.patch.
  KNOWN GAP doc on the method so the next reader finds lablup#37, not a mystery.

cache:: 467/467.

Co-Authored-By: clement-7074f29f <clement-7074f29f@sjstargetedsolutions.co.nz>
sjswerdloff added a commit to sjswerdloff/mlxcel that referenced this pull request Jul 11, 2026
THE FIX (DESIGN_finalize_cap_at_true_length, Violet's design, built at
the junction): pending_finalize_cap: Option<i32> on KVCache — absolute
exclusive position, set by the scheduler pre-forward at padded sites,
CONSUMED (take at entry, one-shot) by update_kvarn8. The cap lands at
the finalize-boundary variable and nowhere else (the K8V4 birth
constraint paying out): a tile finalizes only if it lies entirely below
the cap, so prefill padding stays in the fp16 end-state. Both padding
wrongs die at the root — no tile/offset desync (wrong 1), no Sinkhorn
pollution (wrong 2, asserted bitwise).

DESIGN AMENDMENT (found at source, Violet-confirmed + blessed): the
doc's premise 'the existing dense trim already handles the tail' was
wrong for kvarn — trim() touched zero kvarn fields, so capped padding
would have desynced IN THE TAIL (wrong-1 relocated). trim() now
dispatches KVarN8 to trim_kvarn_fp16_end: tail first, sink only while
no tiles exist (short padded prefills park padding in the SINK — the
cap deliberately does not stop the fp16 sink fill), REFUSE loudly with
zero mutation if quantized tiles would be reached. is_trimmable stays
false (general rewind still unsupported; spec-decode keeps gating).
m3_idx_offset rolls back in lockstep — the idx accumulation received
the padded rows too, and selection reading idx past offset is the
cycle-79 desync class. [FILED, not fixed here: the DENSE trim has the
same m3_idx gap on fp16 M3 caches under padded concurrency — behavior
change on fp16 production, wants its own sizing like lablup#37.]

PREDICATE SHARPENED: padding_trim_would_corrupt now asks per-cache
can_trim_padding(excess) — true exactly when the arm can act (fp16
end-state suffices). The tripwire passes exactly when trimming is
genuinely safe and stays armed for uncapped leaks (defense-in-depth).
Existing empty-cache contract holds (no fp16 end-state → corrupt).

TESTS (cache:: 475/475): six design edges — mid-tile cap, identity,
beyond-end clamp, consume-and-clear, THE padded-roundtrip golden
(stored fields AND fetched window AND offset-reconciliation bitwise vs
the unpadded reference, THROUGH the arm), Sinkhorn purity field-level —
plus refuse-zero-mutation, tail-bounded success with m3 lockstep,
sink-case at the hist-empty boundary, and predicate-mirrors-arm.
Named mutations (consume-clear drop; refuse-check drop) run against
THIS commit next — mutations only ever on committed bases.

Scheduler site placements (4) follow as the wiring commit.

Co-Authored-By: clement-7074f29f <clement-7074f29f@sjstargetedsolutions.co.nz>
sjswerdloff added a commit to sjswerdloff/mlxcel that referenced this pull request Jul 11, 2026
…jections, v_bits echo (design §3.4)

The resolver now returns ResolvedKvCacheConfig { mode, kvarn_v_bits }:
the k8v8/k8v4 split is a width on KVCache::kvarn_v_bits, deliberately
NOT a KVCacheMode variant (the field decision, audited at the surgery
commit). kvarn strings route through a string-level matrix BEFORE
KVCacheMode::from_str — kvarn4 has no mode spelling at all.

The matrix (exactly two pairs construct):
- kvarn8/kvarn8 -> KVarN8 + v_bits 8 (k8v8, production today)
- kvarn8/kvarn4 -> KVarN8 + v_bits 4 (k8v4, THE candidate; gs fixed 32)
- kvarn4 on K   -> REJECTED citing RESULTS_kvarn4_realtile_2026-07-11
  BY NAME (17-21% top-32 flips on real tiles — the operator deserves
  the record, not a shrug)
- kvarn x turbo/int8/fp16 mixes -> REJECTED (unvalidated)
- legacy --kv-cache-mode k8v4 (alias kvarn-k8v4) -> same resolution
  through the same path

Plumb: ResolvedKvCacheConfig -> ServerConfig/StartupConfig ->
WorkerSchedulerConfig -> BatchScheduler::with_kvarn_v_bits ->
apply_kvarn_v_bits (free fn, tested against real caches — the
set_prefill_finalize_caps pattern) applied in BOTH arms of
apply_kv_cache_mode_to. KVCache::set_kvarn_v_bits is the guarded
construction surface: width ∈ {4,8}, non-empty relabel REFUSED
(should_panic pinned) — stored V codes can never be silently relabeled.
generate/chat/bench paths REFUSE k8v4 loudly (no width plumbing there;
running k8v8 under a k8v4 label is the silent-wrong class).

Echo (Violet forward pin): the fail-loud startup artifact now carries
kvarn_v_bits + kvarn_format (k8v8/k8v4/n-a) — boots are
log-distinguishable. Deviation from §3.4's illustrative boot line noted:
bytes_per_token is NOT emitted (needs model geometry the artifact
doesn't hold; the format label carries the distinguishability pin).

MODE-GENERIC-METHOD AUDIT (Violet's systemic observation — kvarn's
parallel fields make every mode-generic KVCache method a blind-spot
candidate; trim() and nbytes() failed identically in one afternoon):
- update():        dispatches update_kvarn8; v_bits consumed (34df406)
- trim():          kvarn tail-bounded arm, refuse-loud (99d7ff6)
- trim_front():    ALLOWLIST Fp16|Int8 — returns 0 for KVarN8, both
                   widths fail SAFE (verified at source THIS commit;
                   the allowlist polarity is the correct pattern)
- nbytes():        kvarn-blind — KNOWN GAP, task lablup#37, patch staged
- is_trimmable/can_trim_prompt_cache/padding_trim_would_corrupt/
  can_trim_padding: kvarn-aware, predicate mirrors the arm (99d7ff6)
- detach (clone_handle/install_detached/pool_backed_handle_clone):
                   v_bits round-trips the donation (34df406)
- DetachedKVCache::nbytes: counts kvarn — store admission correct
- downgrade_kvarn8_to_fp16_if_empty: resets v_bits=8 (4ad2ef3)
- synth_kvarn_state: width-parameterized, layout pinned to update
- fetch (fetch_kvarn8/fetch_kvarn8_blocks/kvarn_qmm_state): 3 loud v4
                   refusals until the read paths land (34df406)
- trim_to (detached): width-agnostic slice, V4 trailing dims pinned

Tests: 9 matrix tests (citation asserted BY NAME; both mix
orientations; requires-kvarn8-K named), 2 into_startup_config
integration pins (the real construction path), 2 apply_kvarn_v_bits
mixed-slice tests, 2 setter-guard tests (empty/same-width accepted,
non-empty relabel should_panic). 12 pre-existing resolver tests
mechanically widened (.mode). Named mutations to run on this committed
base: (1) drop the kvarn4-K citation arm -> citation test red;
(2) drop the mode condition in apply_kvarn_v_bits -> mixed-slice test
red. The config plumb itself is compiler-pinned (missing-field errors
at every initializer — proven during the cut, three initializers named).

cache:: 479/479 (477 + 2 setter guards); CLI selection 39/39;
cargo check clean including --tests.

Co-Authored-By: clement-7074f29f <clement-7074f29f@sjstargetedsolutions.co.nz>
sjswerdloff added a commit to sjswerdloff/mlxcel that referenced this pull request Jul 12, 2026
…(nbytes kvarn-blind, confirmed), finding-3 refuted at source, lablup#36 to Clement

Co-Authored-By: violet-14057653 <violet@sjstargetedsolutions.co.nz>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sjswerdloff added a commit to sjswerdloff/mlxcel that referenced this pull request Jul 12, 2026
… wider / store-admission narrower), Violet server-premise refuted, lablup#29 mechanism found

Co-Authored-By: violet-14057653 <violet@sjstargetedsolutions.co.nz>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@inureyes inureyes self-assigned this Aug 31, 2026
@inureyes inureyes added the status:done Completed label Sep 6, 2026
be-free-snowboy pushed a commit to freestylejs/snowbrain-mlxcel that referenced this pull request Oct 27, 2026
Add end-to-end support for the InternVL VLM family so `internvl3-1b` loads and runs in both text-only and image modes, wired into the real CLI and server VLM dispatch.

Architecture (verified against the real config.json and safetensors tensor names, which override the "reuse the InternLM backbone" assumption):

- The language model is Qwen2, not InternLM. `text_config.model_type == "qwen2"` (hidden 896, 24 layers, 14 heads, 2 KV heads, QKV bias, untied embeddings, rope_theta 1e6), so we reuse mlxcel's existing Qwen2 backbone (`models::Qwen2Model`) rather than reimplementing a transformer. The bogus top-level `llm_config` (vocab 151936, 32B-template dims) is ignored in favor of the real `text_config` (vocab 151674).
- The InternViT vision tower (`vision_model.*`) is a standard SDPA encoder with fused QKV (+bias), per-layer layer-scale (ls1/ls2), LayerNorm, a Conv2d patch embed, a CLS token, and a learned position embedding. `qk_normalization` is false for InternVL3 so the q/k norms are skipped. `select_layer=-1` returns the last hidden state and the CLS token is stripped before the connector. Every tile is exactly 448x448, so the patch grid matches the trained position-embed grid and interpolation is the identity.
- The connector `mlp1.*` applies pixel_shuffle(downsample_ratio=0.5) to reshape (B, 1024, 1024) into (B, 256, 4096), then LayerNorm -> Linear(4096->896) -> GELU -> Linear(896->896), yielding 256 image tokens per tile in the Qwen2 hidden size.
- The dynamic-tiling processor picks the closest aspect ratio (min 1, max 12 tiles, plus a thumbnail when split), applies ImageNet normalization, and emits channels-first tensors.
- Image-token merge replaces `<IMG_CONTEXT>` (151667, resolved from the tokenizer) positions with the connector output through the shared `merge_llava` path.

Mixed precision: the top-level `quantization` block makes the common VLM weight loader treat the checkpoint as quantized and skip its bf16->f16 conversion, which correctly keeps the 4-bit LLM and connector scales/biases as bf16 for `quantized_matmul`. The InternViT tower and the connector LayerNorm are plain bf16, which JIT-crash on M5, so the loader explicitly converts every remaining bf16 tensor to f16 except `.scales`/`.biases`, mirroring the standard Apple Silicon non-quantized-weight policy.

RoPE: standard 1D RoPE is used (not Qwen2-VL MRoPE). The config's `rope_scaling` dynamic-NTK factor is parsed but not applied in the rope path; for the 1B at short context this is a no-op and is noted as a known limitation.

Integration: detection maps `model_type == "internvl_chat"` to the new `ModelType::InternVLChatVLM`, which carries a Vlm metadata descriptor so both the CLI and the server route it as a VLM. A `LoadedModel::InternVLChatVLM` variant delegates the language model, a `VlmRuntimeRef::InternVL` runtime arm drives tile-count-based `<img> + <IMG_CONTEXT>*N + </img>` token expansion, and `load_internvl_vlm()` parses the config, handles the mixed-precision weights, and constructs the Qwen2 backbone, InternViT tower, connector, and processor. New modules live under `vision/encoders/internvl.rs`, `vision/internvl.rs`, `vision/processors/internvl.rs`, and `multimodal/internvl_prompt.rs`. Tensor-parallel inference maps InternVL onto the llama-family planner placeholder (TP is already refused for VLM-kind models upstream of that point).

This change also fixes embedding-based VLM prefill on the M5 Neural Accelerator, which InternVL is the first VLM short enough to exercise on the single-pass prefill path:

- Skip NA-tile prefill padding for embedding requests. The NA prefill alignment padded the token-id vector and built a matching padded causal mask, but VLM image requests run through `forward_with_embeddings` with pre-merged embeddings that are not padded, so a 293-token image prompt met a 320-wide mask and aborted with a broadcast-shape error. NA-tile alignment is now skipped whenever a sequence carries pre-merged embeddings; the backbone then builds a causal mask sized to the embeddings, matching the CLI generate path. Token-id (text-only) prefill is unchanged.
- Route VLM embedding prefill to full prefill instead of chunked. Prompts longer than `prefill_chunk_size` (default 512) were sent to the chunked path, which re-triggered the same broadcast abort and is fundamentally incoherent for embedding-based prefill (the whole embedding sequence is consumed while the prefill offset advances by only one chunk, corrupting cache bookkeeping). A multi-tile image (e.g. a 2:1 photo at 768+ image tokens) trivially exceeds 512 and hit this latent path; the chunked route is now gated on the sequence having no pre-merged embeddings, mirroring the batched-prefill dispatch which already forces VLMs onto full prefill.

Tests: unit tests cover the pixel_shuffle shape collapse (1024x1024 -> 256x4096), the dynamic-tiling processor (single-tile square, multi-tile plus thumbnail, ImageNet normalization, per-image tile counts), prompt-token insertion and expansion, and the two VLM prefill routing guards (both conditioned on the absence of pre-merged embeddings, exercised without requiring M5 hardware). A model-gated `tests/internvl_parity.rs` covers detection, processor shapes, and finite text-only and image-conditioned logits of the correct vocab dimension.
be-free-snowboy pushed a commit to freestylejs/snowbrain-mlxcel that referenced this pull request Oct 27, 2026
Bump version to 0.0.30. Highlights: unified pre-load memory estimator with the new mlxcel inspect subcommand and --estimate-memory preflight (lablup#64, lablup#65, lablup#66, lablup#67), Molmo v1 and InternVL VLM architectures (lablup#41, lablup#37), Gemma 3n / Phi-3.5 / Gemma / Jamba decode performance work (lablup#42, lablup#43, lablup#44, lablup#60, lablup#62), and server --ctx-size as a shared parallel context budget (lablup#57).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:inference Generation, sampling, decoding (incl. speculative, DRY) area:models Model architectures, weights, loading, metadata status:done Completed type:enhancement New features, capabilities, or significant additions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant