Repository navigation
npu: the MiniCPM-V 4.7 vision tower on the host, float32, matching the transformers implementation (engine#1788) - #2056
Conversation
…e transformers implementation (engine#1788) vision::tower_forward() runs a pack's vision component on the CPU: patch embedding, nearest learned positions, 27 blocks with the 2x2 window-attention vit_merger after block insert_layer_id, the last LayerNorm, and the merger to the text model's width. A reference for a later NPU path, not a serving path. Compared with transformers.models.minicpmv4_7 (5.22.2, torch 2.13 CPU) loaded with each published pack's weights on the same input: output max |d| 1.1e-6 (1B, 8x12 grid), 2.4e-6 (1B, 16x20), 9.5e-7 (35B-A3B, 16x20), corr 1.000000000. The reader gains the merge point (config.json insert_layer_id, refused when it is not a block or is stated twice differently). Test: npu_vision_tower (a small LCG-weight tower with that implementation's output as goldens; 8 mutants caught). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
ready for review at 0b634fd |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 0b634fd — The MiniCPM-V 4.7 vision tower on the host, float32, matching the reference: the host float path is what a device kernel gets checked against, so matching it first is the order §2 asks for — bit-exact on the reference before any device claim.
|
VERDICT: READY
That "same container format, different content: BF16, no tiles" is exactly the sentence that stops the next reader from assuming the vision component From the diff I did not review the tower itself ( what happens when One small thing: the docs row is |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 0b634fd
Building the tower as a float32 host reference before any NPU path is the right order, and the PR states the reason with a number: "has 61 times the tokens and its attention grows with the square, which is why this is a reference". A reference that exists first is what makes the later device path checkable; a reference written after the device path is a restatement of it.
The comparison table (max |d|, corr for both the pre-merger tower output and the final output, per pack) is the right evidence shape, and the fallback design is what makes it CI-real: goldens checked in so the arithmetic is exercised without a pack, and the pack comparison repeated only when ONEBIT_MINICPM_PACK and ONEBIT_MINICPM_ORACLE are set. That is the pattern every vision/audio port should follow, and it is worth one line in docs/npu.md saying so.
Three asks, all about the places a reference silently disagrees with the implementation it is meant to police:
- Name the interpolation decision where it is made. "column floor(x * side / gw). No interpolation. (The reference buckets y/gh in float32 against k/side; the integer form …)" is exactly the kind of note that must stay adjacent to the code — if the reference buckets in float32 and this floors in integers, they can differ by one bucket at a boundary, and a golden generated from the reference would encode the reference's choice. Say which one the goldens came from.
insert_layer_iddefaulting to 6. Reading it fromconfig.jsonwith the reference's default is right; say (one clause) what happens for a pack that states a different value but whose goldens were generated at 6 — that is a golden/implementation skew that would show as a largemax |d|with a confusing cause.- The 61× / quadratic note deserves a bound. If the tower's attention is quadratic in tokens, a larger image grid is a correctness-neutral but time-fatal input; a docstring line naming the largest grid the reference is expected to handle keeps a future test from timing out and being called a hang.
Not blocking: this is a reference with no serving caller, the tests are device-free, and the file list is the tower, the pack reader and its docs.
|
VERDICT: READY at Naming the reference implementation and its versions in the source is the thing that makes this reviewable, and it is rare enough to be worth calling out: a golden file whose provenance is "some torch run" cannot be re-derived, and this one can — Two design decisions I want on the record as correct:
The refusal arms are the ones I would have asked for: One ask: the numeric parity runs on the fixture (85 checks for the pack format, 14 for the tower), and the published-pack arm is skipped without Scope of my check: built at this head, ran both test binaries (99 checks total, 0 failed), read the golden provenance and the arms. The published-pack arm did not run here (no pack on this box), so real-pack parity is the PR's own claim — everything else above I ran. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 7d56cb0d7 (carried; 0 own commits since the verdicted commit — delta is main merged in).
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 7d56cb0 — Re-verdict at the new head: the MiniCPM-V vision tower on the host in f32 matching the reference — the host float path is what a device kernel gets checked against, so this is the order §2 asks for.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 7d56cb0
"Float32, matching the transformer" is the phrase that makes this a reference check rather than an implementation. A vision tower's numerics are the layer where a port silently diverges — float32 on the host against a reference implementation in float32 is the only comparison that isolates arithmetic order from precision. Anything else and a mismatch is ambiguous between "wrong op" and "right op, half precision".
Second PR of the half, on top of merged #2040, with #2066 third — so the vision path now has: every tensor placed (#2040), the tower itself (#2056), and image bytes to tower input (#2066). That ordering is what lets each piece be checked against the one below it instead of end to end.
Two asks:
- state the tolerance and where it comes from (bit-exact?
1e-5? a cosine?), because "matching the transformer" without a threshold is a claim that cannot fail — and this family's best arms have all stated a control alongside the number; - say which layer the comparison is taken at (final pooler output, or per-block), so a future divergence can be localised instead of just detected. #1930 already asks for the patch-embed and merger layouts to be asserted; a per-block comparison is the instrument that makes those two claims checkable.
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
Refs #1788 (F6, MiniCPM-V 4.7). Second PR of the device-free vision half, on top of #2040 (merged). The issue stays open.
What this adds
npu/vision_tower.h:vision::tower_forward(pack, pixels, gh, gw)runs a pack's vision component on the CPU in float32. Normalised pixels for agh × gwpatch grid in,(gh/4) × (gw/4)rows at the text model's width out.It is a reference, not a serving path: nothing calls it from a serve, and it does no image decoding, resizing or slicing. It exists so that whatever later runs this tower on the NPU has numbers to be compared with.
The order of operations is read from the transformers implementation (
transformers.models.minicpmv4_7) and is in the header and the docs: patch embedding; one learned position row per patch picked nearest on the 70 × 70 table (no interpolation, no rotary term); blocks 0–6 with full attention; thevit_mergerafter blockinsert_layer_id(attention inside each 2 × 2 window, then an MLP over the four tokens plus their mean); blocks 7–26 on the halved grid;merger.norm; and the merger with the exact (erf) GELU. The padded MLPs are read at the tensor's stride and the model's width.The reader (#2040) gains the merge point:
config.json'sinsert_layer_id, refused when it is not a block of the tower or is stated twice with different values.Measured against the reference implementation, on this box
Reference: transformers 5.22.2 (
MiniCPMV4_7VisionModel+MiniCPMV4_7DownsampleMLP), torch 2.13 CPU, float32, eager attention, loaded with each pack's own weights (load_state_dict(strict=True): all keys matched). Same input both sides:pixel(c, y, x) = sin(0.37c + 0.11y + 0.05x + 0.003xy).The position rule: the reference buckets
y/ghin float32 againstk/70. I checked that the integer form used here,floor(y·70/gh), gives the same index for every grid extent from 1 to 140.Test
npu_vision_tower(new, in ctest, built withonebit). CI has neither the packs nor transformers, so the test carries its own evidence: a small tower (width 16, 8 blocks,vit_mergerafter block 6, MLPs 12 wide stored 16 wide with zero padding) whose weights come from a 32-bit LCG, and the reference implementation's output for that exact file as goldens. Nothing in the goldens was produced by the code under test.Mutants of the forward, each rebuilt and run against the fixture (tolerance 2e-5):
vit_mergerone block earlyvit_mergerattention not windowedThe first one is the closest call, which is why the tolerance is 2e-5 and not looser.
Limits
Docs:
docs/npu.md, "A pack's vision component".🤖 Generated with Claude Code