Skip to content

gpu-vision, vision, serve: MiniCPM-V 4.7 images end to end, a tile capped at 70 tokens - #147

Merged
midagedev merged 19 commits into
mainfrom
mcv033
Oct 14, 2026
Merged

midagedev merged 19 commits into
mainfrom
mcv033

Conversation

@midagedev

@midagedev midagedev commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

What and why

MiniCPM-V 4.7 images for 0.3.3. The tower runs end to end on the card. It is held against mainline 358a420e's nodes, and a tile group is encoded and prompted as a canvas. The serve seat caps a tile at 70 tokens. A larger tile is refused by the tower's existing buffer check.

  • gpu-vision: the minicpmv4_7 tower arm, vis_attn_group and vis_mean_groups (held by gate_minicpmv_tower against their host rules), the vit_merger and the 2x2 merger.
  • tools/ref, refset: the minicpmv4_7 oracle and family. The family refuses another mmproj, oracle build or rope order by name. gate-gpu-minicpm-text runs the e2e's self-consistency clauses on MiniCPM's file.
  • vision, gpu, serve: Prepared is Grid or Tiles. The rope follows the file's section order (zyxt). A grid encode refuses a tile group by name.
  • serve: ImageNumber, PlaceholderCount and ImageLast are HTTP 400. A build without the vision feature refuses --mmproj by name.

Code shape

  • No second copy.
  • Model code holds only architecture facts.
  • Dead code is gone.
  • No silent failure.
  • Comments state what is true now.
  • No relaxed gate. vis_attn_group and vis_mean_groups are new rows in tools/ref/ptx-shapes.tsv, pinned at 0 spill.
  • fmt and clippy clean; lint 41 = ratchet.

Duplication found or removed: none.

What I ran

  • Rebased on 27b4ce3. Three WIP commits were squashed into one, and the tree equals the pre-squash tree minus a stale handoff file.
  • Mac static (full, no base): fmt-check 0, lint 41 = ratchet 41, combos 42 shapes 0 red, check-arch/comments/defaults/levers/loads/ptx-names/records/rustflags/unsafe/waits 0. check-recipes was red on the remote only (one git range self-test). tools/check-recipes.sh on the same tree here: check-recipes: ok.
  • GPU, workstation (A6000 and 3090, one job at a time), on 4aeb8ca: the fixture tier of just affected (159 items) ran 156 green, and the 3 that lost the ssh session reran green (gate-gpu-qwen35moe-attn, gate-gpu-routed-legs, gate-ds41-bind). The real tier of the vision path, all rc 0: gate-refset (198 passed), gate-vision, gate-gpu-vision-lib, gate-gpu-minicpmv-tower, gate-gpu-minicpm-text, gate-gpu-vision, gate-gpu-qwen3vl-tower, gate-gpu-qwen35moe-media, gate-gpu-qwen35-media, gate-gpu-qwen3-serve, gate-gpu-ds41-media. e3ff6dd (the test helper only): gate-refset 198 passed, Mac static fmt 0 and lint 41.
  • Not run: a ptx-scan pair against the base. The two new vis_ entries are pinned in tools/ref/ptx-shapes.tsv at 0 spill.

Prepared by the bloomery assistant on behalf of @midagedev.

🤖 Generated with Claude Code

midagedev and others added 16 commits October 14, 2026 08:38
…vel merge-order rows, the nearest-bucket position rows, the vit_merger and the 2x2 MLP merger, host twins

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…roups of rows) and vis_mean_groups (each group's mean in mainline's sum order), held by gate_minicpmv_tower against their host rules

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he group mean in the chain's bf16, both kernels wired, the tower's clauses on the real mmproj (card figures, the vit_merger and the final merger teacher-forced against their f32 twins, three images, reuse), the card figure owned by one equality, and gate-gpu-vision-lib for gpu-vision's lib tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… tree by the one build script, dump_mtmd's minicpmv4_7 canvas seat with per-tile taps, the minicpmv47 profile row, and the minicpmv4_7 refset family that refuses another mmproj, oracle build or rope order by name

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e's nodes — every tap of three images' seven tiles fed mainline's pixels, our rows permuted to theirs, the separator rows bit for bit, and tile_embd, the tile's rows then its suffix rows

Taps within one block of the input are held to K_MARGIN·√N times their controls; the taps after
are held to K_SENS times them, the nudge already carrying the blocks' amplification.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a420e — dump_mtmd's canvas seat gains hidden (result_norm and four position lanes) and chat (llama-server's ids), three cases, and their reader with its refusals

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mv47 profile, gate-gpu-minicpm-text (real-only): the e2e's self-consistency clauses on MiniCPM's file, every ik-set clause skipped by name, and its greedy ids held teacher-forced against mainline 358a420e's; gguf-respell edits a file's own keys in place

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the prompt — image_placeholder(index) and image_marker, one counter through render_chat, check_text refusing the marker

A model that numbers its images (MiniCPM-V's <image_id>N</image_id>) writes the index; the three
models today ignore it and render byte-identical prompts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er gate's and gate-refset's together

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… end to end

Prepared is Grid(Patches) or Tiles(Group), and MiniCPM's prepare returns its
group; the image-id check sits in expand_prompt. The engine side follows the
file's rope section order (zyxt) through SeqRope, ImageShape is Grid or Canvas
with push_canvases and push_images, and the tower gives its separators. The
gpu-gates image readers hold EncoderInput, and grid() refuses a tile group by
name. MediaError::ImageNumber is the model's error; mrope lists
gate-decision-clef and gate-serve.

Co-Authored-By: Grok 4.7 <noreply@x.ai>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
prefill_media keeps the draft's media refusal first, then group_rows and
Body35 with ImageShape::Canvas. A call that mixes a grid and a group is
refused by name. Not compiled. vision_of still accepts only qwen3vl_merger.

Co-Authored-By: Grok 4.7 <noreply@x.ai>
vision_of reads that projector itself. The marks and the <unk> pad come from
the text tokenizer, and the file is held against the text model's width.
The tile bound is not stated, so the tower is not sized and the encoder is
not seated. Not compiled. A qwen projector still goes through open_seat.

Co-Authored-By: Grok 4.7 <noreply@x.ai>
body35 reads the text file's section order by its path, as the load below
it already does, so one arch directory does not import the other. The seat's
tile-bound sentence and its tests import the MiniCPM names. media.rs is the
dispatch for the shared tile group. Not compiled. check-ptx-names still wants
measured rows for vis_attn_group and vis_mean_groups.

Co-Authored-By: Grok 4.7 <noreply@x.ai>
…one shape

span_rows_of already returned NotAGrid for a tile group. The test holds that
refusal and that the tower is not asked. prefill_media's mix check moves to
tile_group_count, the same sentence, so a call of grids and canvas groups is
refused before an encode. Not run. The tile bound, ImageNumber and per-image
replay stay unchosen.

Co-Authored-By: Grok 4.7 <noreply@x.ai>
The vis_ spill check on dc00409d printed both unpinned at spill 0
and jit_local 0. Sixteen entries, fourteen already pinned, no other
violation. Both zeros, so no PIN line.

Co-Authored-By: Grok 4.7 <noreply@x.ai>
ImageNumber is HTTP 400. EmptySpan stays 500. A build without the
vision feature refuses the projector by name and does not seat it.
The one-span test checks the range by index.

A full mac-static of this tree was remote rc 0, lint 41. No PR.

Co-Authored-By: Grok 4.7 <noreply@x.ai>
@github-actions github-actions Bot added gpu The card engine: crates/gpu*, crates/app host Host crates: qdot, threads, engram, engram-lab, runtime, sampler, levers, decision, vision, refset needs-gpu Changes code a card must check, and carries no gate run on a card serve The HTTP server: crates/serve tools Tools, recipes, CI and the toolchain: tools/, justfile, .github/, Cargo labels Oct 13, 2026

@github-actions github-actions Bot left a comment

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.

Verdict: MUST_FIX: Shared sizing, text validation, and reference utilities have new copies.

Must fix

1. Use the existing card sizing owner

crates/gpu-vision/src/minicpmv.rs:95 duplicates crates/vision/src/arch/minicpmv/card.rs:41. The new Lens also repeats card::ScratchLens, and ff_padded repeats card::padded_ff. Kind B (R14). One placement fact has two implementations.
Centralize before the next sizing change.

Delete local ff_padded, weight_bytes_of, Lens and its impl; use:

use vision::arch::minicpmv::card::{
    self, ScratchLens as Lens, padded_ff as ff_padded,
};
pub use vision::arch::minicpmv::card::weights as weight_bytes_of;

pub fn scratch_bytes_of(hp: &Hparams, max_patches: usize) -> u64 {
    card::ScratchLens::of(hp, max_patches).bytes()
}

Rename lens uses: stream → xa, att → attn, rows → out, including self.lens and tests. Allocate xb/h from lens.xb/h.

2. Call the shared projector validation

crates/gpu-gates/src/bin/shared/serve_seats/qwen3.rs:1342 copies the vocabulary, RoPE and PLE checks in crates/gpu-gates/src/vision_seat.rs:388. These checks are common policy. Kind B (R14).
Reuse them before the policy needs another edit.

Remove the unused use vision::MediaModel;. Replace let media = Media::new(...) through the returned Opened with:

let projector = vision_seat::Projector {
    path: path.to_owned(),
    rule,
    card,
    out_dim: hp.out_dim,
    media: Arc::new(Media::new(hp, marks, PAD_TEXT, pad)),
    cap: limits.max(),
    limits,
};
projector.check_text(text, token)?;
let seat = vision_seat::encoder_seat(place, projector.card.total())?;
Ok(vision_seat::Opened {
    path: projector.path,
    seat,
    media: projector.media,
    cap: projector.cap,
    card: projector.card,
    limits: projector.limits,
})

3. Share the prompt manifest parsers

crates/refset/src/arch/minicpmv4_7/prompt.rs:700 copies crates/refset/src/clefvis.rs:1195 exactly. read_chat at line 664 copies crates/refset/src/clefvis.rs:1501, the parser for the same header format. Kind B (R14).
Share before the header format changes.

Make the existing clefvis.rs functions pub(crate). Delete both new copies; replace the ChatLine import with:

use crate::clefvis::{ChatLine, read_chat, unescape};

4. Keep one refusal helper in the test harness

crates/refset/src/arch/minicpmv4_7/prompt_tests.rs:154 and crates/refset/src/arch/minicpmv4_7/vis_tests.rs:140 repeat the same refusal assertion; crates/refset/src/arch/glm5next/vis_tests.rs:279 is a third copy. Kind B (R14).
Share before another family copies it.

Put this in clefvis/testkit.rs; delete the three copies and import testkit::refusal:

pub(crate) fn refusal<T>(r: Result<T, crate::RefError>) -> String {
    match r {
        Ok(_) => panic!("the set was taken"),
        Err(e) => e.to_string(),
    }
}

Non-blocking

⚠ Engine image-prompt gate: crates/gpu-gates/src/bin/shared/serve_seats/qwen3.rs:739 has no comparison against the new prompt oracle. Tower/text gates do not run this image prompt. An equal-area Grid replacing Canvas here survives them. Compare the seat's hidden rows and positions before extending this path.

⚠ Host stage boundaries: crates/gpu-vision/src/minicpmv.rs:326 combines validation, weight packing/upload, scratch allocation and kernel loading. Split those host stages (R12). Split crates/gpu-gates/src/bin/gate_minicpmv_tower.rs:1039 by load, teacher-forced checks, reuse and oracle comparison.

Checked

Duplicates: all four shared-owner defects confirmed.
Complexity: load and tower clauses mix host stages; splitting remains non-blocking.
Silent failure: the base issue reproduced.
Tests: equal-row-count Grid passes span checks; tower/text gates miss the seat path. No hardware run.
Gates: two zero-spill rows added; unsafe-count increase has a dated PIN and reason.
Escape catalog: no override applies.

Notes

The PR body has no complete card gate run, FAIL-first output, or PTX scan evidence. GPU verification remains pending.

Card check: needs-gpu. This pull request changes crates/gpu-gates/Cargo.toml, crates/gpu-gates/src/bin/gate_ds41_media.rs, crates/gpu-gates/src/bin/gate_minicpmv_tower.rs … and carries no gate run on a card (CONTRIBUTING's evidence list); a maintainer adds gpu-verified after one.

🔨 Shingler, bloomery's review bot: approves a clean pull request and requests changes on a must-fix (how it works)

Filed outside this PR: #151

midagedev and others added 2 commits October 14, 2026 09:53
…checks its text through Projector

gpu-vision's minicpmv uses card::{padded_ff, weights, ScratchLens} instead of
its own copies (the lens fields read xa, attn, out; xb and h come from the
lens). open_minicpm builds a vision_seat::Projector and runs check_text and
encoder_seat, so the vocabulary, IMROPE and PLE refusals have one owner.

Co-Authored-By: Grok 4.7 <noreply@x.ai>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… clefvis, and one refusal helper serves the tests

parse_chat is clefvis's `# chat` parser with the set's own location in its
errors; unescape is shared as it was. testkit::refusal replaces the three
per-family copies.

Co-Authored-By: Grok 4.7 <noreply@x.ai>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@midagedev

Copy link
Copy Markdown
Owner Author

The four must-fix items are folded in 976afe6 and 4aeb8ca (head 4aeb8ca).

  1. Card sizing: gpu-vision's minicpmv uses card::{padded_ff, weights, ScratchLens}. The local copies are gone. The lens formulas were compared field by field before the delete.
  2. Text validation: open_minicpm builds a vision_seat::Projector and calls check_text and encoder_seat.
  3. Manifest parsers: unescape is shared. read_chat was not a pure copy (its errors name the set's family), so clefvis now has parse_chat(fields, at), and both families call it with their own location.
  4. testkit::refusal replaces the three copies.

Evidence on 4aeb8ca: full Mac static EXIT 0 (fmt 0, lint 41 = ratchet, 42 combos 0 red, every check-* 0). Workstation, real tier: gate-refset (198 passed), gate-vision, gate-gpu-vision-lib, gate-gpu-minicpm-text, gate-gpu-vision, gate-gpu-qwen3vl-tower, gate-gpu-qwen35moe-media, gate-gpu-qwen35-media, gate-gpu-ds41-media all rc 0. gate-gpu-minicpmv-tower and gate-gpu-qwen3-serve lost the ssh session during the build and will be rerun. The fixture-tier batch is running.

The two non-blocking items (a seat-level oracle comparison, the host stage split) are not in this change.

This comment was written by the bloomery AI assistant on behalf of the maintainer.

@github-actions github-actions Bot left a comment

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.

Since the last review:

  • Card sizing: fixed.
  • Projector validation: fixed.
  • Prompt manifest parsers: fixed.

The earlier findings still open are listed below.

Verdict: MUST_FIX: The new shared refusal helper still has two identical implementations.

Must fix

1. Finish migrating the refusal helper (still open)

crates/refset/src/clefvis/testkit.rs:294 repeats the same match, panic and error rendering in crates/refset/src/arch/qwen_vis_tests.rs:77 and crates/refset/src/arch/qwen35/clefvis/tests.rs:54. Kind B, R14. The refusal contract still has three owners.
Finish the migration before another family copies it.

Keep the generic helper. In both Qwen files, delete the local fn refusal and use crate::RefError; (that import serves only the deleted function). Add:

use crate::clefvis::testkit::refusal;

Non-blocking

⚠ Engine image-prompt gate (still open): crates/gpu-gates/src/bin/shared/serve_seats/qwen3.rs:739 lacks a comparison against the prompt oracle. The tower and text gates do not exercise this call, so replacing Canvas with an equal-area Grid is unguarded there. Add a seat gate comparing hidden rows and canvas positions.

⚠ Host stage boundaries (still open): crates/gpu-vision/src/minicpmv.rs:259 combines validation, packing/upload, scratch allocation and kernel loading. Split these host stages (R12) so each has one job. Split crates/gpu-gates/src/bin/gate_minicpmv_tower.rs:1039 into load, teacher-forced checks, reuse and oracle comparison.

Checked

Duplicates: the 2 new functions were compared with existing owners; sizing and prompt parsing now use shared owners.
Complexity: this increment adds no branches to the host loader or tower gate.
Silent failure: projector validation, chat parsing and prompt unescaping propagate named errors.
Tests: by inspection, changing Scratch::new's xb allocation at line 564 to zero would not fail the changed sizing unit test: it never allocates Scratch. Existing literal card-byte assertions check formula changes. No build or tests run.
Gates: no bands, tolerances, pins or test cases removed in this increment.
Performance: no added launches, copies or per-step allocations in the incremental diff.
Escape catalog: no C1–C4 pattern matches the incremental diff.

Notes

The PR body lacks complete card gate output, FAIL-first evidence and PTX scan evidence. GPU evidence is false.

Card check: needs-gpu. This pull request changes crates/gpu-gates/Cargo.toml, crates/gpu-gates/src/bin/gate_ds41_media.rs, crates/gpu-gates/src/bin/gate_minicpmv_tower.rs … and carries no gate run on a card (CONTRIBUTING's evidence list); a maintainer adds gpu-verified after one.

🔨 Shingler, bloomery's review bot: approves a clean pull request and requests changes on a must-fix (how it works)

qwen_vis_tests and qwen35's clefvis tests drop their own copies of refusal and
import the one in clefvis::testkit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@midagedev

Copy link
Copy Markdown
Owner Author

e3ff6dd finishes the refusal helper: qwen_vis_tests.rs and qwen35/clefvis/tests.rs drop their copies and the RefError import, and take clefvis::testkit::refusal. gate-refset 198 passed on the workstation; Mac static fmt 0, lint 41.

The PR body now lists the card runs on 4aeb8ca: the fixture tier of just affected all green after three ssh-cut reruns, and the eleven real-tier vision-path gates all rc 0.

The two non-blocking items stay out of this change.

This comment was written by the bloomery AI assistant on behalf of the maintainer.

@github-actions github-actions Bot left a comment

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.

Since the last review:

  • Refusal helper: fixed. Both Qwen test modules use the shared owner.

Verdict: CLEAN: The incremental diff replaces duplicate refusal helpers with an equivalent shared implementation; the earlier findings remain non-blocking.

Non-blocking

⚠ Engine image-prompt gate (still open): crates/gpu-gates/src/bin/shared/serve_seats/qwen3.rs:739 constructs canvas spans and passes them into the session, but no gate compares this path against the image-prompt oracle. Existing canvas tests cover row assembly and rotation tables separately. Add a seat gate comparing hidden rows and positions that fails if this call substitutes an equal-area Grid for Canvas.

⚠ Host stage boundaries (still open): crates/gpu-vision/src/minicpmv.rs:259 combines validation, packing/upload, scratch allocation and kernel loading. Split these host stages by job (R12). Split crates/gpu-gates/src/bin/gate_minicpmv_tower.rs:1039 into load, teacher-forced checks, reuse and oracle comparison.

Checked

Duplicates: 0 new items; both deleted helper bodies match clefvis::testkit::refusal.
Complexity: only imports and two deleted match helpers change; no production function changes.
Silent failure: 24 refusal call sites now use the shared helper; unexpected Ok still panics and Err preserves its text.
Tests: no test bodies or assertions changed. Removing clefvis.rs:1117's pad/span guard makes qwen_vis_tests.rs:123 panic through the shared helper, by inspection. No build or tests run.
Gates: no thresholds, pins or test cases changed or removed.
Performance: test-only migration; no engine hot path changes.
Escape catalog: no C1–C4 pattern matches this increment.

Notes

The PR body still lacks the card/driver line and complete gate output required for GPU evidence, FAIL-first evidence and PTX scan evidence. GPU evidence is false.

Card check: needs-gpu. This pull request changes crates/gpu-gates/Cargo.toml, crates/gpu-gates/src/bin/gate_ds41_media.rs, crates/gpu-gates/src/bin/gate_minicpmv_tower.rs … and carries no gate run on a card (CONTRIBUTING's evidence list); a maintainer adds gpu-verified after one.

🔨 Shingler, bloomery's review bot: approves a clean pull request and requests changes on a must-fix (how it works)

@midagedev
midagedev merged commit 02d874b into main Oct 14, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gpu The card engine: crates/gpu*, crates/app host Host crates: qdot, threads, engram, engram-lab, runtime, sampler, levers, decision, vision, refset needs-gpu Changes code a card must check, and carries no gate run on a card serve The HTTP server: crates/serve tools Tools, recipes, CI and the toolchain: tools/, justfile, .github/, Cargo

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant