Skip to content

Commit 33e76d3

Browse files
committed
feat(model/gemma4): neutral vision weight-name canonicalisation (vision loader 1/n)
First slice of the gemma4 vision-tower port (#8): the neutral front of metal's vision_load.go — canonicalise vision weight names off a checkpoint's tensors (strip the multimodal wrapper prefixes via the existing profile.TrimWeightWrapperPrefix, then the vision_tower./vision_model. tower prefix; multi_modal_projector./ embed_vision. keep their prefix) + detect tower-vs-projector-only packs (via the existing model.WeightAny). Reuses what's there rather than re-porting it. The device build of the SigLIP tower from these weights is the next piece, backend-side in pkg/native — same split as the text path (neutral assemble -> native build). pkg/model green. Co-Authored-By: Virgil <virgil@lethean.io>
1 parent 5e89f30 commit 33e76d3

2 files changed

Lines changed: 135 additions & 0 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// SPDX-Licence-Identifier: EUPL-1.2
2+
3+
package gemma4
4+
5+
import (
6+
core "dappco.re/go"
7+
"dappco.re/go/mlx/pkg/model"
8+
"dappco.re/go/mlx/pkg/safetensors"
9+
"dappco.re/go/mlx/profile"
10+
)
11+
12+
// vision_weights.go is the neutral front of the gemma4 vision-tower loader: it canonicalises the vision
13+
// weight names off a checkpoint's tensors (strip the multimodal wrapper prefixes, then the
14+
// vision_tower./vision_model. tower prefix) and detects whether a pack carries a full SigLIP tower or
15+
// only the projector. The DEVICE build of the tower from these weights lives backend-side (pkg/native),
16+
// the same split as the text path (neutral assemble → native device-build). Lifted from
17+
// pkg/metal/model/gemma4/vision_load.go, reusing profile.TrimWeightWrapperPrefix + model.WeightAny
18+
// rather than re-porting them.
19+
20+
// canonicalGemma4VisionWeightName strips the wrapper prefixes then the vision-tower prefix, returning the
21+
// canonical vision-weight name and whether the tensor is a vision weight at all. multi_modal_projector.
22+
// and embed_vision. weights keep their prefix — the projector reads them by full name.
23+
func canonicalGemma4VisionWeightName(name string) (string, bool) {
24+
trimmed := name
25+
for {
26+
next, changed := profile.TrimWeightWrapperPrefix("gemma4", trimmed)
27+
if !changed {
28+
break
29+
}
30+
trimmed = next
31+
}
32+
for _, prefix := range []string{"vision_tower.", "vision_model."} {
33+
if core.HasPrefix(trimmed, prefix) {
34+
return core.TrimPrefix(trimmed, prefix), true
35+
}
36+
}
37+
for _, prefix := range []string{"multi_modal_projector.", "embed_vision."} {
38+
if core.HasPrefix(trimmed, prefix) {
39+
return trimmed, true
40+
}
41+
}
42+
return "", false
43+
}
44+
45+
// SanitizeVisionWeights returns the vision weights from a checkpoint's tensors, keyed by their canonical
46+
// names — the input the device-side vision build consumes.
47+
func SanitizeVisionWeights(raw map[string]safetensors.Tensor) map[string]safetensors.Tensor {
48+
vision := make(map[string]safetensors.Tensor)
49+
for name, t := range raw {
50+
if canonical, ok := canonicalGemma4VisionWeightName(name); ok {
51+
vision[canonical] = t
52+
}
53+
}
54+
return vision
55+
}
56+
57+
// HasVisionTowerWeights reports whether the pack carries a full SigLIP vision tower (a patch embedder),
58+
// vs only the multimodal projector.
59+
func HasVisionTowerWeights(weights map[string]safetensors.Tensor) bool {
60+
_, ok := model.WeightAny(weights,
61+
"patch_embedder.input_proj.weight",
62+
"patch_embedder.input_proj.linear.weight",
63+
"embeddings.patch_embedding.weight",
64+
"patch_embedding.weight",
65+
)
66+
return ok
67+
}
68+
69+
// HasVisionProjectionWeights reports whether the pack carries the vision-to-text multimodal projector.
70+
func HasVisionProjectionWeights(weights map[string]safetensors.Tensor) bool {
71+
_, ok := model.WeightAny(weights,
72+
"embed_vision.embedding_projection.weight",
73+
"embed_vision.embedding_projection.linear.weight",
74+
"multi_modal_projector.embedding_projection.weight",
75+
"multi_modal_projector.embedding_projection.linear.weight",
76+
"multi_modal_projector.proj.weight",
77+
"multi_modal_projector.weight",
78+
)
79+
return ok
80+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// SPDX-Licence-Identifier: EUPL-1.2
2+
3+
package gemma4
4+
5+
import (
6+
"testing"
7+
8+
"dappco.re/go/mlx/pkg/safetensors"
9+
)
10+
11+
// TestCanonicalGemma4VisionWeightName pins the canonicalisation: the vision_tower./vision_model. prefix
12+
// is stripped, the projector prefixes are kept, and a text weight is rejected.
13+
func TestCanonicalGemma4VisionWeightName(t *testing.T) {
14+
cases := []struct {
15+
in, want string
16+
ok bool
17+
}{
18+
{"vision_tower.encoder.layers.0.self_attn.q_proj.weight", "encoder.layers.0.self_attn.q_proj.weight", true},
19+
{"vision_model.embeddings.patch_embedding.weight", "embeddings.patch_embedding.weight", true},
20+
{"multi_modal_projector.proj.weight", "multi_modal_projector.proj.weight", true},
21+
{"embed_vision.embedding_projection.weight", "embed_vision.embedding_projection.weight", true},
22+
{"model.layers.0.self_attn.q_proj.weight", "", false},
23+
}
24+
for _, c := range cases {
25+
got, ok := canonicalGemma4VisionWeightName(c.in)
26+
if got != c.want || ok != c.ok {
27+
t.Fatalf("canonicalGemma4VisionWeightName(%q) = (%q,%v), want (%q,%v)", c.in, got, ok, c.want, c.ok)
28+
}
29+
}
30+
}
31+
32+
// TestSanitizeAndDetectVisionWeights pins the gather + tower/projector detection over a tiny tensor set.
33+
func TestSanitizeAndDetectVisionWeights(t *testing.T) {
34+
raw := map[string]safetensors.Tensor{
35+
"vision_tower.embeddings.patch_embedding.weight": {Dtype: "BF16", Shape: []int{8, 4}},
36+
"multi_modal_projector.proj.weight": {Dtype: "BF16", Shape: []int{4, 8}},
37+
"model.layers.0.self_attn.q_proj.weight": {Dtype: "BF16", Shape: []int{4, 4}}, // text — dropped
38+
}
39+
vision := SanitizeVisionWeights(raw)
40+
if _, ok := vision["embeddings.patch_embedding.weight"]; !ok {
41+
t.Fatal("tower weight should be present under its canonical name")
42+
}
43+
if _, ok := vision["multi_modal_projector.proj.weight"]; !ok {
44+
t.Fatal("projector weight should keep its prefix")
45+
}
46+
if len(vision) != 2 {
47+
t.Fatalf("the text weight should be dropped: got %d vision weights", len(vision))
48+
}
49+
if !HasVisionTowerWeights(vision) {
50+
t.Fatal("should detect a full vision tower")
51+
}
52+
if !HasVisionProjectionWeights(vision) {
53+
t.Fatal("should detect the multimodal projector")
54+
}
55+
}

0 commit comments

Comments
 (0)