Skip to content

feat(models): add MiniCPM-V 4.6 VLM support - #82

Merged
inureyes merged 1 commit into
mainfrom
feat/81-minicpmv4-6-vlm
May 24, 2026
Merged

inureyes merged 1 commit into
mainfrom
feat/81-minicpmv4-6-vlm

Conversation

@inureyes

Copy link
Copy Markdown
Member

Closes #81

Adds MiniCPM-V 4.6, a vision-language model that pairs a Qwen 3.5 text backbone (standard 1D RoPE) with a two-stage visual compression pipeline. Ported from the upstream mlx-vlm MiniCPM-V 4.6 model (PR #1212).

Architecture

The vision pipeline compresses tokens in two stages: a VitMerger doing cross-attention spatial 2x2 pooling inserted after ViT layer 6, followed by a pixel-shuffle Merger MLP. For a 448x448 image this produces 64 visual tokens, matching query_num.

New files

  • src/vision/encoders/minicpmv4_6.rs: VitMerger, MergerBlock, Merger, and MiniCPMV46VisionModel with per-layer ViT access for mid-tower injection.
  • src/vision/minicpmv4_6_vl.rs: MiniCPMV46VLModel wrapping Qwen35Model.

Wiring

  • src/models/mod.rs: register the MiniCPMV46VLM variant, display name, metadata() arm, and ALL_MODEL_TYPES entry (so it appears in mlxcel list).
  • src/models/detection.rs: map the minicpmv4_6 model-type string to the variant.
  • src/loading/vlm_special.rs (+ vlm.rs, loading/mod.rs): MiniCPM-V 4.6 weight remap.
  • src/vision/encoders/minicpmo.rs: expose per-layer ViT access for reuse.
  • loaded_model.rs, loaded_model_capabilities.rs, model_metadata.rs, multimodal/vlm_runtime.rs, distributed/tensor_parallel/inference.rs: capabilities, metadata, runtime, and tensor-parallel routing.

Verification

  • cargo fmt --check clean.
  • cargo clippy --features metal,accelerate --all-targets -- -D warnings clean.
  • 17 unit tests pass (vision encoder config + merger reducing a 32x32 grid to 64 tokens, the weight remap, and the model-type registry integrity tests).
  • mlxcel list shows MiniCPM-V 4.6 (SigLIP + VitMerger + Merger + Qwen3.5 text).

Add MiniCPM-V 4.6, a vision-language model that pairs a Qwen 3.5 text backbone (standard 1D RoPE) with a two-stage visual compression pipeline. Ported from the upstream mlx-vlm MiniCPM-V 4.6 model (PR #1212).

The vision pipeline compresses tokens in two stages: a VitMerger doing cross-attention spatial 2x2 pooling inserted after ViT layer 6, followed by a pixel-shuffle Merger MLP. For a 448x448 image this produces 64 visual tokens, matching `query_num`.

New files:

- `src/vision/encoders/minicpmv4_6.rs`: VitMerger, MergerBlock, Merger, and MiniCPMV46VisionModel with per-layer ViT access for mid-tower injection.
- `src/vision/minicpmv4_6_vl.rs`: MiniCPMV46VLModel wrapping Qwen35Model.

Wiring:

- `src/models/mod.rs`: register the MiniCPMV46VLM variant, its display name, and the `metadata()` arm, and add it to `ALL_MODEL_TYPES` so it appears in `mlxcel list`.
- `src/models/detection.rs`: map the `minicpmv4_6` model-type string to `ModelType::MiniCPMV46VLM`.
- `src/loading/vlm_special.rs`: add the MiniCPM-V 4.6 weight remap, wired in through `src/loading/vlm.rs` and `src/loading/mod.rs`.
- `src/vision/encoders/minicpmo.rs`: expose forward_embeddings / layer / post_layernorm / num_layers for per-layer ViT access by the 4.6 path.
- `src/loaded_model.rs`, `src/loaded_model_capabilities.rs`, `src/model_metadata.rs`, `src/multimodal/vlm_runtime.rs`, `src/distributed/tensor_parallel/inference.rs`: route the new model through capabilities, metadata, the multimodal runtime, and tensor-parallel inference.

Verified with cargo fmt, cargo clippy (metal,accelerate, all-targets, -D warnings), 17 unit tests (vision encoder config and merger token-reduction, the weight remap, and the model-type registry integrity tests), and `mlxcel list` showing the new model.
@inureyes inureyes added type:enhancement New features, capabilities, or significant additions priority:medium Medium priority area:models Model architectures, weights, loading, metadata labels May 24, 2026
@inureyes
inureyes merged commit 25d6384 into main May 24, 2026
4 checks passed
@inureyes
inureyes deleted the feat/81-minicpmv4-6-vlm branch May 24, 2026 14:45
@inureyes inureyes self-assigned this May 25, 2026
inureyes added a commit that referenced this pull request May 26, 2026
Bump version to 0.0.31. Highlights: MiniCPM-V 4.6 and RT-DETRv2 model support plus an Anthropic-style /v1/messages API endpoint (#82, #80, #74); fixes for missing or null chat-message content, Gemma 3n VLM per_layer_inputs, Qwen3.5 MTP verify attention, and batched quantized KV cache mask offsets (#91, #86, #78, #76); and a pinned 1.93.1 Rust toolchain (#87, #90).
@inureyes inureyes added this to the 0.1 milestone Jun 21, 2026
sjswerdloff added a commit to sjswerdloff/mlxcel that referenced this pull request Jun 25, 2026
)

Add per-layer m3_idx_k cache to KVCache for MiniMax-M3's MSA indexer
projection (index_k_proj). The indexer scores key BLOCKS for top-k
selection; in chunked-prefill / multi-turn it must see the FULL cached
prefix idx_k, not just the current chunk's projection. Current code
(pre-lablup#80) projects from x only, so top-k selection is blind to cached
positions — the architectural gap behind the cycle-65 latent comment.

API additions on KVCache:
- m3_idx_k_update_and_fetch(new_idx_k) -> full cached idx_k
  Concatenates the new chunk to prior cached state, advances
  m3_idx_offset, returns an independently-owned slice of the full cache.
- m3_idx_offset() — accessor for the indexer's accumulated length.
- has_m3_idx_k_state() — defensive check for whether the indexer cache
  has been written.

Fields added to KVCache struct (initialized to None / 0 in both new()
constructors at lines 514 and 562; default state for non-M3 models):
- m3_idx_k: Option<UniquePtr<MlxArray>>
- m3_idx_offset: i32

5 unit tests in cache.rs covering: initial-empty state, single-chunk
write, two-chunk concat along seq axis, three-chunk accumulation
(precursor to task lablup#82's four-chunk gate), independence from main K/V
offset. All pass.

Wired into M3's forward() by task lablup#81; until then the methods exist but
are unused by mlxcel-M3.

KVCache rationale (cf. struct doc): the model-specific fields are
documented as opt-in state with Option<> types, consistent with the
Turbo/INT8 sidecar pattern. Avoids per-model branching in the trait at
the cost of a small per-cache memory footprint when unused. Per-request
scoping (KVCache is request-scoped) inherits naturally.
sjswerdloff added a commit to sjswerdloff/mlxcel that referenced this pull request Jun 25, 2026
The architectural completion: SparseAttention::forward() now correctly
handles chunked-prefill / cached cases.

Key changes:
- Compute kv_len = cache_offset + l BEFORE update_and_fetch, so the
  asymmetric block counts are derived from the FULL cached length, not
  just the current chunk's length.
- num_query_blocks vs num_key_blocks throughout: Q padded to
  num_query_blocks*block_size, K/V padded to num_key_blocks*block_size,
  reshape sites updated accordingly.
- cache.m3_idx_k_update_and_fetch(&idx_k) (from lablup#80) swaps the
  current-chunk-only idx_k for the FULL cached idx_k. This closes the
  architectural gap behind the cycle-65 latent comment: top-k block
  selection now scores ALL num_key_blocks blocks, including the cached
  prefix that pre-lablup#81 was invisible to the indexer.
- New apply_causal_block_mask_asymmetric: per-query-block max attendable
  absolute key block = floor((cache_offset + (i+1)*block_size - 1) /
  block_size). Over-permissive at block level; sparse_sdpa's unified mask
  trims within-block future positions.
- New ensure_local_block_score_asymmetric: forces top-k to include the
  cached-prefix's last few absolute blocks (the query's local
  neighbourhood), which sit BEFORE the current chunk in absolute coords.
- Dispatch dense when num_key_blocks <= top_k (top-k saturates → no
  sparsity benefit, dense is at least as good).
- selected has shape [b, num_kv_heads, num_query_blocks, top_k] with
  ABSOLUTE key block indices in [0, num_key_blocks). sparse_sdpa's
  asymmetric path consumes them correctly.

Symmetric apply_causal_block_mask + ensure_local_block_score marked
#[allow(dead_code)] for regression value; the asymmetric forms subsume
them at cache_offset=0. Deletion planned after lablup#78-lablup#83 lands and the
asymmetric forms have lived without regressions.

Cycle 65's "Unreachable under default prefill_chunk_size=512, hence
latent until now" comment in cycle 65's code is now obsolete — the
cached-prefill path is exercised on every forward with non-empty cache.

14 mask tests pass (7 symmetric regression + 7 asymmetric). Integration
test for the actual four-chunk forward-attention path is task lablup#82; this
commit wires the path, the test in lablup#82 verifies correctness against
dense single-shot.
sjswerdloff added a commit to sjswerdloff/mlxcel that referenced this pull request Jun 25, 2026
Proves the asymmetric MSA path produces correct output across cached
prefill — the cycle-65 latent comment turned into a live gate.

Three test variants on a synthetic SparseAttention layer (small dims:
num_heads=4, num_kv_heads=2, head_dim=4, block_size=2, top_k=2,
sparse_local_block=1, index_dim=4) with deterministic weights and input:

1. test_msa_four_chunk_chunked_vs_single_shot_no_crash
   The minimum gate: four forwards with growing KVCache complete cleanly,
   per-chunk output shapes match expected, final cache.offset and
   m3_idx_offset both equal total tokens (lockstep main + indexer cache).

2. test_msa_four_chunk_matches_single_shot_within_tolerance
   The correctness gate (Stuart's "no-crash + dense-differential within
   fp tol" bar). Four-chunk path (with growing cache) vs single-shot
   path (no cache) on the same 24-token input. Compare via relative L2:
   chunked_concat against single_out. Tolerance 5% — fp accumulation
   order differs across paths; selection determinism keeps the residual
   numerical-only, not structural.

3. test_msa_four_chunk_determinism_bit_exact
   Cycle 64 lesson: rule out MLX nondeterminism before declaring fp
   issues real. Run the chunked path twice, assert L2 diff < 1e-6.

l_chunk=6 (3 blocks of 2 per chunk) chosen so num_key_blocks > top_k at
chunk 1, making MSA fire on every chunk in both paths — neither path
falls into the dense dispatch.

3 passing tests; cycle 65's existing 14 MSA mask tests still pass; total
17 MSA tests in minimax_m3.rs.

The cycle-65 latent comment is no longer latent — task lablup#82 is the
regression gate for any future MSA path modification.
@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 MiniCPM-V 4.6, a vision-language model that pairs a Qwen 3.5 text backbone (standard 1D RoPE) with a two-stage visual compression pipeline. Ported from the upstream mlx-vlm MiniCPM-V 4.6 model (PR lablup#1212).

The vision pipeline compresses tokens in two stages: a VitMerger doing cross-attention spatial 2x2 pooling inserted after ViT layer 6, followed by a pixel-shuffle Merger MLP. For a 448x448 image this produces 64 visual tokens, matching `query_num`.

New files:

- `src/vision/encoders/minicpmv4_6.rs`: VitMerger, MergerBlock, Merger, and MiniCPMV46VisionModel with per-layer ViT access for mid-tower injection.
- `src/vision/minicpmv4_6_vl.rs`: MiniCPMV46VLModel wrapping Qwen35Model.

Wiring:

- `src/models/mod.rs`: register the MiniCPMV46VLM variant, its display name, and the `metadata()` arm, and add it to `ALL_MODEL_TYPES` so it appears in `mlxcel list`.
- `src/models/detection.rs`: map the `minicpmv4_6` model-type string to `ModelType::MiniCPMV46VLM`.
- `src/loading/vlm_special.rs`: add the MiniCPM-V 4.6 weight remap, wired in through `src/loading/vlm.rs` and `src/loading/mod.rs`.
- `src/vision/encoders/minicpmo.rs`: expose forward_embeddings / layer / post_layernorm / num_layers for per-layer ViT access by the 4.6 path.
- `src/loaded_model.rs`, `src/loaded_model_capabilities.rs`, `src/model_metadata.rs`, `src/multimodal/vlm_runtime.rs`, `src/distributed/tensor_parallel/inference.rs`: route the new model through capabilities, metadata, the multimodal runtime, and tensor-parallel inference.

Verified with cargo fmt, cargo clippy (metal,accelerate, all-targets, -D warnings), 17 unit tests (vision encoder config and merger token-reduction, the weight remap, and the model-type registry integrity tests), and `mlxcel list` showing the new model.
be-free-snowboy pushed a commit to freestylejs/snowbrain-mlxcel that referenced this pull request Oct 27, 2026
Bump version to 0.0.31. Highlights: MiniCPM-V 4.6 and RT-DETRv2 model support plus an Anthropic-style /v1/messages API endpoint (lablup#82, lablup#80, lablup#74); fixes for missing or null chat-message content, Gemma 3n VLM per_layer_inputs, Qwen3.5 MTP verify attention, and batched quantized KV cache mask offsets (lablup#91, lablup#86, lablup#78, lablup#76); and a pinned 1.93.1 Rust toolchain (lablup#87, lablup#90).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:models Model architectures, weights, loading, metadata priority:medium Medium priority status:done Completed type:enhancement New features, capabilities, or significant additions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(models): add MiniCPM-V 4.6 VLM support

1 participant