- MiniCPM-V-4.7-1B β Core AI
- What it is
- Bundles
- How the VLM rides the text-only engine
- Prompt
- Measured
- Mac M4 Max (macOS 27.0 26A428, coreai-core 1.0.0b2 Python runtime, AOT h16c, 2026-10-13)
- iPhone 18 Pro (iPhone19,2, iOS 27.2 24B5099f, PipelinedBench, device JIT, 2026-10-13)
- iPhone 18 Pro: the same bundles compiled ahead of time (2026-10-13)
- Mac M4 Max: prefill in chunks and a second turn (2026-10-13, not in the upload)
- Gates
- What the Mac and the phone do not separate
- Convert / verify
- Try it
- Lessons
- License
- What it is
MiniCPM-V-4.7-1B β Core AI
π€ mlboydaisuke/MiniCPM-V-4.7-1B-CoreAI Β· Apache-2.0 Β· source openbmb/MiniCPM-V-4.7-1B (revision 984a89a)
MiniCPM-V-4.7-1B on Apple's Core AI: a vision tower and a decoder as two .aimodel graphs, the decoder driven by
Core AI's pipelined GPU engine, converted by the Core AI model zoo's conversion/minicpmv47/
on coreai-torch 0.4.1 / coreai-core 1.0.0b2. On an iPhone 18 Pro (iOS 27.2) the int8 decoder generates 98.0 tokens
per second, measured 2026-10-13 (gate-minicpm-v-4.7-1b-iphone.json).
(generated by scripts/gen-cards at the next CoreAIKit release that ships the 4.7 architecture; until then use the VLChat example on CoreAIKit main, merged as pull request #72 on 2026-10-14)
What it is
OpenBMB's MiniCPM-V-4.7-1B reads an image and a question and answers in text. It follows MiniCPM-V-4.6
(minicpm-v-4.6) in the same family, and the two share their vision code: a 27-layer
SigLIP-type tower (width 1152, patch 14) with a window-attention merger after layer 6 and a 2Γ2 downsample MLP, 64
tokens for a 448Γ448 image, spliced into a Qwen3.5 hybrid text decoder (24 layers: 18 Gated DeltaNet + 6 full
attention, hidden 1024, vocabulary 248,094; the source config.json). In transformers 5.19.0 the 4.7 vision classes
are 4.6's line for line after renaming (lane FEASIBILITY.md Β§r1; "lane" records live under
$ZOO_WORK_ROOT/_minicpmv47/, outside the repository). Three things change for a port:
- Positions are canvas M-RoPE. 4.6 used plain 1-D positions. 4.7 gives every token three planes (T, H, W, interleaved sections [11, 11, 10]): the image sits on a canvas, and the text after it resumes past the canvas. The decoder here derives the planes inside the graph (below).
- The head is untied.
lm_head.weightis its own tensor ([248094, 1024], 0.51 GB in the checkpoint's bf16; laneLANE.mdΒ§1), within max |Ξ| 4.9e-4 of the embedding (row cosine β₯ 0.99998). The embedding used as the head moves P0's logits by up to 0.196 and reorders the top-5 at 2 of 48 positions (laneoracle/parity_text.json), so the bundle carries the checkpoint's own head. - The prompt side. The processor also returns
mm_token_type_ids(not needed here: the decoder reads the image from the ids), generation stops on either 248044 or 248046, and the chat template has anenable_thinkingswitch. This port runs with thinking off.
Bundles
mlboydaisuke/MiniCPM-V-4.7-1B-CoreAI, three folders
under gpu-pipelined/. An answer about an image needs the decoder and one tower. Recommended: the int8hu decoder +
the int8lin tower, 1,942,089,859 bytes together. The fp16 tower is the exact one (per-token cosine to the fp32 tower:
min 0.999957) at 474 MB more.
| folder | what | weights | .aimodel bytes |
main.mlirb sha256 |
|---|---|---|---|---|
minicpmv47_vlm_decode_int8hu/ |
decoder (recommended): .aimodel + metadata.json + tokenizer/ |
linears int8 per block of 32 (body symmetric_with_clipping, the untied head symmetric); embedding, conv and norms fp16 |
1,308,109,419 | ae9609fde02d⦠|
minicpmv47_vision_int8lin/ |
tower (recommended): 448Γ448 β 64 rows, .aimodel + metadata.json |
nn.Linear int8 per block of 16; LayerNorm, conv and embedding fp16; fp16 math |
613,967,602 | 40265d0c06d9β¦ |
minicpmv47_vision_fp16/ |
tower, exact, same layout | fp16 weights and math | 1,087,749,442 | 0b3ece089d7b⦠|
Folder totals: decoder 1,328,120,899 B (its tokenizer/ is the snapshot's tokenizer.json, tokenizer_config.json,
chat_template.jinja and generation_config.json, byte for byte), int8lin tower 613,968,960 B, fp16 tower
1,087,750,729 B (lane r6/STAGE.md, r6/strip_*.json). The int8 block size of the tower is 16 because mlp.fc2
takes 4,304 inputs, which 32 does not divide.
Each .aimodel is the export with its MLIR debug locations removed: coreai-torch 0.4.1 writes the exporting machine's
source paths into main.mlirb, and strip_debug_info (the method of
conversion/d1_omni/strip_ship.py) takes them out. The stripped graphs have
the export's op counts (3,326 / 1,967 / 1,613) and coreai-build inspect summary, their AOT h16c compiles hold the
export's resources.bin byte for byte, and the stripped pair re-ran the end-to-end gate on the Mac with the export's
greedy ids on all four image prompts (lane r6/regate_stripped.json). The timing and iPhone numbers below were measured
on the exports before the strip. No AOT asset is in the upload: the iPhone numbers are the phone's own specialization
of the .aimodel.
How the VLM rides the text-only engine
The engine runs ids + positions β logits over states and knows nothing about images. The image enters as three
static inputs that the host writes once per image and the engine binds on every step
(apps/coreai-pipelined-static-inputs.patch); the Gated DeltaNet
conv and recurrent states ride apps/coreai-pipelined-extra-states.patch.
The compiled main function (gate-minicpm-v-4.7-1b.json descriptor):
| name | shape, type | |
|---|---|---|
| inputs | input_ids |
[1, 1] int32; an image token is id V + k (V = 248,094, k = 0..63, row-major over the 8Γ8 merged grid) |
position_ids |
[1, seq] int32, the ramp 0 .. seq β 1 | |
| static inputs | image_embeds |
[64, 1024] fp16: the tower's rows; zero for a text-only prompt |
rope_shift_start |
[1] int32 | |
rope_shift_amount |
[1] int32 | |
| states | keyCache, valueCache |
[6, 1, 2, β1, 256] fp16 (the 6 full-attention layers; context up to 4,096) |
convState, recState |
[18, 1, 6144, 3], [18, 1, 16, 128, 128] fp16 | |
| output | logits |
[1, 1, 248094] fp16 |
In the graph an id β₯ V reads row id β V of image_embeds; any other id reads the embedding table.
The canvas rule. For one 448Γ448 image the frame is <image>, the 64 image tokens, </image>, and the template's
"\n". With O = the index of <image> (the canvas origin) and the host's start / amount, the graph gives each
position p its planes:
| token | position | (T, H, W) |
|---|---|---|
text before the frame (<image_id>0</image_id> included) |
p < O | (p, p, p) |
<image> (248078) |
p = O | (O, O β 1, O β 1) |
| image row k | p = O + 1 + k | (O, O + k // 8, O + k % 8) |
</image> (248079) |
p = O + 65 | (O, O + 8, O + 8) |
the "\n" after </image> |
O + 65 < p < start | (O, O, O) |
| everything after the frame, generated tokens included | p β₯ start | (p β amount) on all three |
start = the index after </image> and the marker tokens that follow it; amount = start β (O + 9), where 9 =
max(H, W) + 1 for the 8Γ8 grid. On the four image prompts O = 6, 6, 6, 17, start = 73, 73, 73, 84 and amount = 58.
This is transformers' get_rope_index for mrope_mode: canvas: in fp32 torch the rule equals the planes the
original model's rotary received at 433/433 prompt positions and 458/458 generate positions (lane
oracle/parity_vl_torch.json). A text-only prompt sends start = 2Β³β° and amount = 0, every position gets (p, p, p),
and in fp32 torch the decoder then equals the plain Qwen3.5 text decoder bit for bit (same record). The host side is
host_static_inputs in conversion/minicpmv47/minicpmv47_vl_pipelined.py.
Prompt
The processor's expansion, which the decoder's ids follow (P1 of
fixtures-minicpm-v-4.7-1b.json):
<|im_start|>user\n<image_id>0</image_id><image><|image_pad|>Γ64</image>\nWhat is in this image?<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n
- Thinking off:
enable_thinking=Falseends the prompt with<think>\n\n</think>\n\n. Every gate here ran that way. - Stop on 248044 or 248046 (
generation_config.json). The answers here that ended before the 32-token budget ended on<|im_end|>(248046). - The 64
<|image_pad|>(248056) become V + k. The bundle tokenizer's encoding of the expanded prompt equals the processor's ids on all four image prompts (gate-minicpm-v-4.7-1b-e2e.jsontokenizer_ids_equal_processor_ids). - Thumbnail only. The tower is baked for one 448Γ448 image (32Γ32 patches, 64 tokens at the default
downsample_mode16x). Every gate here fed a 448Γ448 image, which the original processor neither slices (at anymax_slice_nums) nor resizes again. For other images the original keeps the aspect ratio (find_best_resize, sides rounded to multiples of 56) and, when the image is larger than 448Γ448 andmax_slice_nums> 1, adds slices (the source card recommends 36 for images); this port squashes the image to 448Γ448 and keeps the thumbnail only. What that costs in answers is not measured here. - Host preprocessing: RGB β Pillow BICUBIC resize to 448Γ448 (aspect ratio not kept) β x / 255 β (x β 0.5) / 0.5 β
CHW
[1, 3, 448, 448], aspreprocessinconversion/minicpmv47/host.py. Against the processor's own pixels: max |Ξ| 5.9e-8 on the four image prompts (laneoracle/host_preprocess.json).
Measured
Two instruments that do not compare with each other: the Mac numbers time one Python-runtime call at a time, the iPhone numbers are the Swift pipelined engine's.
Mac M4 Max (macOS 27.0 26A428, coreai-core 1.0.0b2 Python runtime, AOT h16c, 2026-10-13)
coreai-build compile β¦ --platform macOS --preferred-compute gpu --architecture h16c (--expect-frequent-reshapes
for the decoder), loaded with SpecializationOptions.default(). ms = wall time around one call including the
output read-back, inside a quiet window of the machine-wide GPU lock. The decoder was fed P1's 88-token prompt, then
timed on greedy main calls.
| asset | ms per call, all forms in one process, rotated (run 3) | runs 1 / 2 | cold / warm in the tower gate | record |
|---|---|---|---|---|
decoder int8hu, S=1 main |
7.44 (134.4 tok/s) | 7.89 / 9.04, one process per decoder | β | lane r3/timing_run3.json, r3/timing.json, r3/timing_run2.json |
| decoder fp16 (reference, not uploaded) | 7.42 (134.9 tok/s) | 7.96 / 7.37 | β | same |
| tower int8lin | 43.81 | 44.22 / 43.65 | 921.3 / 44.01 | lane r3/timing_run3.json; gate-minicpm-v-4.7-1b-tower-int8lin.json timing |
| tower fp16 | 43.85 | 44.20 / 43.66 | 86.6 (right after int8lin) / 44.40 | lane r3/timing_run3.json; gate-minicpm-v-4.7-1b-tower-fp16.json timing |
| tower fp16w32 (fp16 storage, fp32 math; not uploaded) | 55.03 | 55.21 / 54.64 | 686.7 / 55.54 | lane r3/timing_run3.json, r3/gate_vision_fp16w32.json |
Separate processes drifted by up to 20 % (in both runs the second process of the window was the slower one); the run-3 column is the number to read. int8hu and fp16 take the same time per call on this runtime (7.44 vs 7.42 ms); how that time splits was not measured.
iPhone 18 Pro (iPhone19,2, iOS 27.2 24B5099f, PipelinedBench, device JIT, 2026-10-13)
PipelinedBench (Swift, Release, Xcode 27.0 RC, the pipelined engine with both patches, the
increased-memory-limit entitlement) loaded the bundles' .aimodel files and the phone specialized them; USB
power, screen state not controlled. The decoder runs in PipelinedBench's GLM-OCR form, which binds image_embeds,
rope_shift_start and rope_shift_amount as static buffers. Speed trial: a random prompt (seed 0) of p = 128
tokens prefilled one token per call (COREAI_CHUNK_THRESHOLD=1), then greedy g = 256, with the image rows zero and
start = 2Β³β° (text only); decode tok/s = (tokens β 1) / time after the opening streamed token, prefill tok/s = p /
time to it. Every row is in gate-minicpm-v-4.7-1b-iphone.json under runs.
| launch | engine ready | prefill tok/s | decode tok/s (trials) | nat P0 / oracle P1 | run |
|---|---|---|---|---|---|
| cold (fresh container) | 6.4 s | 105.7 | 96.0 (94.5, 97.5) | 23/23, 32/32 | run1_cold |
| warm | 0.2 s | 105.7 | 98.0 (98.0, 98.0) | 23/23, 32/32 | run2_warm |
| warm, g = 1024 asked | 0.2 s | 105.5 | 96.9 (one trial) | 23/23, 32/32 | run3_long |
| warm, P1's rows from the fp32 HF tower | 0.2 s | 105.3 | 97.1 (97.1, 97.1) | 23/23, 32/32 | run4_hfrows |
The g = 1024 trial ran without an error; the engine's iOS dynamic-KV guard caps prompt + generated at 1,024 positions, so it decoded 896 tokens by the code path (the app does not print the count).
Towers (PB_VISION, fp16 in and out, P1's 448Γ448 image, median of 9 calls after one cold call; cosine over all
65,536 output values against the Mac's output of the same form on the same fp16 patches):
| tower | .aimodel download |
encode ms, two launches (order reversed) | cos vs the Mac | max |d| | runs |
|---|---|---|---|---|---|
| int8lin | 614 MB | 67.8, 68.5 | 0.999997 | 0.007 | vis_int8lin, vis_int8lin_b |
| fp16 | 1.09 GB | 66.5, 68.6 | 0.999997 | 0.013 | vis_fp16, vis_fp16_b |
The container's opening tower call took 2,214 ms; later launches' cold calls took 82β91 ms with either form.
iPhone 18 Pro: the same bundles compiled ahead of time (2026-10-13)
The decoder and the int8lin tower compiled on the Mac for the phone (coreai-build compile β¦ --platform iOS --preferred-compute gpu --architecture h19p, the decoder with --expect-frequent-reshapes) and run in PipelinedBench
as above, in one device session with the JIT .aimodel run before and after them:
| asset | compiled bytes | load | speed, AOT | JIT .aimodel, same session |
runs |
|---|---|---|---|---|---|
| decoder int8hu, AOT h19p | 2,813,728,487 | engine ready 1.3 s at its initial load (JIT: 6.4 s cold) | 51.4 tok/s decode (51.0, 50.7 on the other two launches); prefill 52.6 | 98.0 / 98.3 tok/s decode, prefill 106.1 / 105.9 | lane r5/logs/dec_aot_run{1,2,3}.pb_run.log, r5/logs/dec_jit_{pre,post}.pb_run.log |
| tower int8lin, AOT h19p | 1,088,140,018 | opening call 249 ms | 67.7 ms (66.4, 69.0) | 66.3 ms | lane r5/logs/vis_aot{,_b}.pb_run.log, r5/logs/vis_jit_pre.pb_run.log |
The AOT decoder answered nat 23/23 and oracle 32/32 on all three launches but decodes at about half the JIT's rate:
its compiled asset holds an fp16 copy of the Linears beside the int8 weights (2.15Γ the bytes of the phone's JIT cache
entry), while the phone's own specialization keeps the decoder in int8. Which bytes the AOT graph reads per token was
not measured. The upload is the JIT .aimodel (lane r5/DEVICE.md, r5/AOT.md).
Mac M4 Max: prefill in chunks and a second turn (2026-10-13, not in the upload)
Measured on the Python runtime (AOT h16c), one quiet window each:
- 16 prompt tokens per call. A two-function export of the int8hu decoder (S=1
main+ S=16prefill,export_decoder.py int8hu --prefill-chunk 16) answers P1βP4 token for token as the one-token-per-call order and the oracle, and brings P1's 88-token prompt from 673.3 ms (88 calls) to 162.7 ms (5 prefill + 8 S=1 calls), 4.14Γ; a prefill call takes 18.40 ms. The last prompt position is not bit-equal to the S=1 order (cos 0.999992β0.999998). Its AOT with--expect-frequent-reshapesis 4,323,182,283 B (an fp16 copy of the Linears per function). Not run on the phone: PipelinedBench's decoder path drives the stock engine one token per call (laner5/chunk.json). - A second question on the same image. The chat template renders the two-turn conversation as turn 1's 88 ids +
its answer + 248046 and
"\n"+ the new turn, so the decoder state after turn 1 can be kept: the second question then takes 20 calls instead of 140, 174.3 ms against 1,318.2 ms, and the answer equals the fp32 oracle's 32 tokens either way (the carried and the re-prefilled last-prompt logits are bit-equal). This is a host / kit feature; the phone was not run (laner5/state.json,oracle/turn2.json).
Gates
The oracle is transformers 5.19.0 in fp32 on the CPU (one thread, eager) at the pinned revision. A knife edge is an
oracle step whose softmax top-2 gap is below 0.1 (conversion/coreai_gate.py's rule), and a free-run passes when it
never leaves the oracle's greedy ids or leaves them at a knife edge. The bars were fixed before the runs.
| gate | what | bar | result | record |
|---|---|---|---|---|
| text, fp32 torch | the overlay's Qwen3.5 decoder vs the oracle, P0's last 48 positions | ordered top-5 equal | 48/48, cos min 0.999999999832, max |Ξ| 2.39e-4 | lane oracle/parity_text.json |
| planes, fp32 torch | the canvas rule vs the planes the oracle's rotary received, P0βP4 + generate | equal at every position | 433/433 + 458/458; red arms (amount + 1, 1-D planes) fail 4/4 | lane oracle/parity_vl_torch.json |
| tower, fp32 torch | the re-authored tower vs the oracle's rows, P1βP4 | per-token cos min β₯ 0.9999, max |Ξ| β€ 1e-3 | min 0.9999999992, max |Ξ| 3.8e-5; the baked export path = the reference path bit for bit | lane oracle/parity_vision.json |
| G1, Mac AOT | int8hu decoder, P0 teacher-forced at positions 19..66 + free-run | argmax equal at every non-knife-edge position; free-run rule | 44/44 (48/48 with the 4 knife edges), logits cos min 0.99458; free-run = the oracle's 23 tokens to <|im_end|> |
gate-minicpm-v-4.7-1b.json G1 |
| G2, Mac AOT | int8hu decoder, P1βP4 free-run on the oracle's tower rows | free-run rule, 4/4 | 4/4, no divergence over 32 / 30 / 2 / 32 tokens; the 1-D-planes red arm red (max |d| at the oracle's top-5, from the frame on: 11.98 vs 0.372) | same, G2, red_arms |
| G3, Mac AOT | towers vs the oracle's rows, P1βP4, per-token cos | int8lin: mean β₯ 0.9995, min β₯ 0.99; the fp16w32 bar: mean β₯ 0.9999, min β₯ 0.999 | int8lin mean β₯ 0.99972, min 0.99411 (P2); fp16 mean β₯ 0.999997, min 0.999957 (meets the fp16w32 bar) | β¦-tower-int8lin.json, β¦-tower-fp16.json |
| G4, Mac AOT | image file β host β tower β int8hu decoder β answer, P1βP4 | free-run rule, 4/4 | 4/4 with the int8lin tower and with the fp16 tower; the answers equal the oracle's token for token | gate-minicpm-v-4.7-1b-e2e.json, β¦-e2e-fp16.json |
| iPhone 18 Pro | int8hu decoder, nat (P0) + oracle (P1) | free-run rule | 23/23 + 32/32 on all 4 launches, with the Mac tower's rows and with the HF rows | gate-minicpm-v-4.7-1b-iphone.json |
| iPhone 18 Pro | towers vs the Mac's output of the same form | cos β₯ 0.999 | 0.999997, both forms, both launches | same |
| staged bytes | the stripped int8lin tower + int8hu decoder, G4 re-run on the Mac | greedy ids = the export's | equal on P1βP4 (and = the oracle's), reset bit-equal | lane r6/regate_stripped.json |
P0's step 8 is a knife edge (gap 0.0553); the Mac and the phone took the oracle's token there. The four answers (G4, either tower), β€ 32 tokens each: P1 "This image shows Earth β specifically, a vivid, close-up view of the planet from space. β¦"; P2 "An aerial black-and-white photo captures a Saturn V rocket standing on its launch pad, surrounded by barren terrain and runway lines under a cloudy sky."; P3 "EXIT"; P4 "Looking at this Earth image, I see a rich variety of colors: β¦". The images are two NASA photographs (public domain) and one drawn with Pillow primitives; the fixture file pins them by sha256 and does not carry them.
What the Mac and the phone do not separate
The Mac's h16c AOT compile and the phone's own specialization both store the int8lin tower as fp16: its compiled
resources.bin is 1,087,609,504 B, the fp16 tower's size (Mac: lane r3/aot_minicpmv47_vision_{int8lin,fp16}.json,
contents different, sha256 261c57c2β¦ vs 152bc955β¦; phone: lane r4/logs/container_caches.json). So the two
towers run at the same speed wherever measured (Mac 43.81 vs 43.85 ms, iPhone 67.8 / 68.5 vs 66.5 / 68.6 ms), and
int8lin buys download size only: 614 MB against 1.09 GB. Its cost is tower accuracy against the oracle (per-token cos
min 0.99411 vs 0.999957, G3), which did not reach the answers (G4 identical with either tower). The decoder keeps its
int8 bytes on the phone (its cache resources.bin is 1,308,616,980 B, the .aimodel 1,308,566,922 B); on the Mac its
AOT with --expect-frequent-reshapes is 2,813,727,608 B (lane r3/FILES.md).
Convert / verify
Environment: the zoo overlay venv (coreai-core 1.0.0b2, coreai-torch 0.4.1, coreai-opt 0.2.1, torch 2.9.0,
transformers 4.57.6) for export, AOT and the gates; the oracle in its own venv (Python 3.12, transformers 5.19.0,
torch 2.14.1, torchvision 0.29.1, Pillow 12.3.0); the snapshot at 984a89a in the Hugging Face cache with
HF_HUB_OFFLINE=1; DEVELOPER_DIR = Xcode 27.0 RC for coreai-build. Every script, in order, with its record:
conversion/minicpmv47/README.md.
PY=../coreai-models/.venv/bin/python # the overlay venv; run from the repository root
<oracle-venv>/bin/python -I conversion/minicpmv47/make_fixtures.py # the three images, 448x448
<oracle-venv>/bin/python -I conversion/minicpmv47/oracle_minicpmv47.py # transformers 5.19.0, fp32, CPU
<oracle-venv>/bin/python -I conversion/minicpmv47/oracle_margins.py # the knife edges
$PY -I -B conversion/minicpmv47/export_decoder.py int8hu
$PY -I -B conversion/minicpmv47/export_vision.py int8lin # and: fp16
$PY -I -B conversion/minicpmv47/aot_compile.py $ZOO_EXPORTS/minicpmv47_vlm_decode_int8hu --reshapes --record <dir>/aot_decoder.json
$PY -I -B conversion/minicpmv47/aot_compile.py $ZOO_EXPORTS/minicpmv47_vision_int8lin --record <dir>/aot_tower.json
$PY -I -B conversion/minicpmv47/gate_decoder.py $ZOO_EXPORTS/minicpmv47_vlm_decode_int8hu --text --vl --red --out <dir>/g1g2.json
$PY -I -B conversion/minicpmv47/gate_vision.py $ZOO_EXPORTS/minicpmv47_vision_int8lin --out <dir>/g3.json
$PY -I -B conversion/minicpmv47/gate_e2e.py $ZOO_EXPORTS/minicpmv47_vision_int8lin $ZOO_EXPORTS/minicpmv47_vlm_decode_int8hu --out <dir>/g4.json
The gates load only the AOT .aimodelc with SpecializationOptions.default(), one bundle per process (the Python
runtime leaks an IOSurface per call; each gate stays under 15 sequences). On the M4 Max an export takes 4β41 s, an AOT
compile 3β37 s, a decoder gate about 20 s and a tower gate about 6 s (lane r3/export_*.json, r3/aot_*.json).
Try it
CoreAIKit: the catalog row minicpm-v-4.7-1b (pinned to revision 51278afd; the int8hu decoder and the int8lin
tower, 1,942 MB) and a MiniCPM-V 4.7 entry for KitVisionModel are on CoreAIKit main (pull
request #72, merged 2026-10-14). The entry renders the processor's frame (<image_id>0</image_id><image>, the 64 pads, </image>
and "\n"), sets the canvas shift (start = the index where the 64 pads begin + 66, amount = 58) and adds the
thinking-off <think>\n\n</think>\n\n after the assistant role; the prompt puts the image before the question, as the
source processor does. On that branch the Mac's vlchat-cli answered P1βP4, and P1 again through the VLChat
example's ask(), with the oracle's tokens (lane r7/mac_gate.json); on the iPhone 18 Pro the VLChat app (Release,
signed with the increased-memory-limit entitlement β without it the tower's specialization ran out of memory once
the decoder was loaded) gave the same five answers byte for byte from the uploaded bundles, decoding at 103.2β107.1
tok/s by the app's own timing (lane r7/device_judge.json, r7/DEVICE.md). The opening question after installing
waited about 21 s for its opening word (engine ready 16.4 s + 4.65 s), the next launch 1.8 s; what the slow launch
builds was not isolated.
Without the kit, the decoder runs in PipelinedBench's GLM-OCR form (above): the host binds image_embeds,
rope_shift_start and rope_shift_amount as static buffers, feeds the ids with the 64 image tokens written as V + k,
and stops on 248044 or 248046.
Lessons
- A last-position logits bar does not see the plane scheme. With 4.6's 1-D positions on 4.7's weights the
last-position top-5 still matched on all four image prompts (cos 0.9981β0.9996 against the oracle's
0.99999999999; lane
oracle/parity_vl_torch.json), so the planes are gated for equality in torch, and on the engine the plane check is the 1-D-planes red arm (11.98 vs 0.372). The amount + 1 arm went red on the fp16 decoder (0.640 vs 0.140, laner3/gate_decoder_fp16.json) but not on int8hu (0.446 vs 0.372,gate-minicpm-v-4.7-1b.jsonred_arms): shifting the frame and the text after it by one changes only their offset from the six tokens before the frame, which int8's own spread covers. - After specialization the int8lin tower is stored as fp16. The Mac AOT and the phone both hold its weights in fp16, so int8 changes the download, not the encode time (above).
devicectl device copy toskips a destination file of the same size whose mtime is newer than the source. A bytes check passes and the old contents stay;touchthe source before re-pushing and compare a pulled md5.
Port notes: knowledge/minicpm-v-4.7-1b-port.md.
License
Source Apache-2.0 (license: apache-2.0 in the source card; the snapshot carries no LICENSE file). The bundles
inherit it, and the Hugging Face repository carries the Apache-2.0 text. The fixture photographs are NASA works in
the public domain; the synthetic image is drawn by make_fixtures.py.
- Downloads last month
- -
Model tree for mlboydaisuke/MiniCPM-V-4.7-1B-CoreAI
Base model
openbmb/MiniCPM-V-4.7-1B