samisuteria commited on
Commit
7262b6a
·
verified ·
1 Parent(s): 068a086

Rewrite model card as a reference card

Browse files
Files changed (1) hide show
  1. README.md +85 -160
README.md CHANGED
@@ -1,6 +1,7 @@
1
  ---
2
  license: apache-2.0
3
  base_model: google/siglip2-base-patch16-256
 
4
  tags:
5
  - coreml
6
  - siglip2
@@ -9,137 +10,86 @@ tags:
9
  - image-text
10
  - embeddings
11
  - vision
 
12
  ---
13
 
14
- # SigLIP2 base-patch16-256 — CoreML (fp16 + int8-embedding text variant)
15
 
16
- CoreML `.mlpackage` conversion of
17
- [`google/siglip2-base-patch16-256`](https://huggingface.co/google/siglip2-base-patch16-256)
18
- for on-device (macOS/Apple Silicon) text/image embedding — built for
19
- [Alarian](https://alarian.ai)'s semantic frame search.
20
 
21
- **Provenance**: converted directly from the Hugging Face `transformers`
22
- reference implementation and `coremltools`, by nodevorg. Not affiliated with
23
- Google. The conversion script is public:
24
- [`tools/siglip2-coreml/convert.py`](https://github.com/nodevorg/studio) (in
25
- the `nodevorg/studio` repo, kept uncommitted pending review — ask if you want
26
- a copy). This project intentionally never looked at or referenced any
27
- third-party GPLv3 conversion script; it was written from the HF
28
- `transformers` SigLIP2 modeling code and `coremltools` docs only.
29
 
30
- **License**: apache-2.0, inherited from the base model. Same terms apply
31
- here — free for commercial and closed-source use.
32
 
33
- ## What's in this repo
 
 
 
 
 
 
 
34
 
35
- | file | what it is |
36
- |---|---|
37
- | `ImageEncoder.mlpackage.zip` | image tower → 768-dim embedding |
38
- | `TextEncoder.int8emb.mlpackage.zip` | **recommended** — text tower → 768-dim embedding; token-embedding table int8, everything else fp16 (36% smaller, parity ≥0.9996) |
39
- | `TextEncoder.mlpackage.zip` | text tower → 768-dim embedding, full fp16 (use only if you need max precision and don't care about download size) |
40
- | `tokenizer.json` | Gemma BPE tokenizer (256k vocab), verbatim from the source repo |
41
- | `tokenizer_config.json` | tokenizer config (padding_side=right, do_lower_case=true), verbatim |
42
- | `special_tokens_map.json` | special tokens, verbatim |
43
-
44
- **For app downloads, use `TextEncoder.int8emb.mlpackage.zip`.** Same inputs/
45
- outputs as `TextEncoder.mlpackage` (see below) — it's a drop-in replacement,
46
- 323 MiB instead of 506 MiB, with only the 256k-row token-embedding table
47
- quantized to int8 (linear_symmetric, per-channel/per-token-row scale);
48
- attention and MLP weights stay fp16. Measured parity cost: worst-case cosine
49
- similarity 0.9996 vs the fp16 model's 0.999999 — both pass the >0.99 gate by
50
- a wide margin, and text↔image rankings are unaffected (see the parity
51
- section below).
52
-
53
- ### A note on architecture
54
-
55
- `google/siglip2-base-patch16-256`'s own `config.json` self-describes as
56
- `model_type: siglip` (not `siglip2`) — at this fixed-256-resolution, non-NaFlex
57
- tier, the SigLIP2-trained weights are architecturally identical to SigLIP v1
58
- (`transformers.SiglipModel`); only the weights and the tokenizer (Gemma BPE,
59
- 256k vocab, vs. SigLIP v1's 32k SentencePiece) differ. `AutoModel.from_pretrained`
60
- resolves this checkpoint to `SiglipModel`, and that's what was converted.
61
-
62
- ## Inputs / outputs
63
-
64
- ### ImageEncoder.mlpackage
65
-
66
- - **Input**: `pixel_values` — a CoreML `ImageType`, RGB, 256×256. The exact
67
- SigLIP preprocessing (`SiglipImageProcessor` defaults for this checkpoint:
68
- `rescale_factor=1/255`, `image_mean=[.5,.5,.5]`, `image_std=[.5,.5,.5]`) is
69
- **baked into the model** as `scale=1/127.5, bias=[-1,-1,-1]` — i.e. the
70
- model itself maps raw RGB `[0,255]` to `[-1, 1]`. Just resize/squash your
71
- image to 256×256 and hand it the raw RGB pixels; no separate normalization
72
- step needed on the caller side. (Aspect-ratio squash, not center-crop — the
73
- model saw squashed images in training.)
74
- - **Output**: `image_embedding` — 768-dim float16, **already L2-normalized**.
75
-
76
- ### TextEncoder.mlpackage
77
-
78
- - **Input**: `input_ids` — 1×64 **int32** token ids. No `attention_mask`
79
- input: SigLIP's text tower trains with unmasked bidirectional attention
80
- over the full fixed-length 64-token sequence and pools the hidden state at
81
- the *last* sequence position (`SiglipTextModel.forward`, not an EOS-aware
82
- pool) — passing a mask would deviate from training. Tokenize with the
83
- included `tokenizer.json`/`tokenizer_config.json` using
84
- `padding="max_length", max_length=64`, and **lowercase the input text**
85
- (`do_lower_case=True` in the tokenizer config; the tokenizer will also do
86
- this itself if you feed it through its normalizer, but be aware if you
87
- hand-roll token ids).
88
- - **Output**: `text_embedding` — 768-dim float16, **already L2-normalized**.
89
-
90
- `TextEncoder.int8emb.mlpackage` has the exact same input/output names,
91
- shapes, and dtypes — only the internal token-embedding weight is quantized,
92
- which is invisible from the outside.
93
-
94
- Both encoders normalize their pooled output inside the graph, so cosine
95
- similarity between an image and text embedding is just a dot product.
96
 
97
- ## Conversion details
98
 
99
- - `minimum_deployment_target`: macOS 15
100
- - `compute_precision`: `ct.precision.FLOAT16`
101
- - `convert_to`: `mlprogram`
102
- - Traced via `torch.jit.trace` on thin wrapper modules around
103
- `SiglipModel.vision_model` / `.text_model` (not the full contrastive model —
104
- each `.mlpackage` is just the one branch).
105
- - Pinned toolchain (see the sha256 table below for exact artifact hashes):
106
- `torch==2.7.0`, `transformers==5.14.1`, `coremltools==9.0`, `numpy==2.3.5`.
107
- **Gotcha**: `numpy>=2.4.0` breaks `coremltools==9.0`'s PyTorch frontend with
108
- `TypeError: only 0-dimensional arrays can be converted to Python scalars`
109
- on any model using `torch.nn.MultiheadAttention` (SigLIP's vision pooling
110
- head hits this) — confirmed via
111
- [apple/coremltools#2633](https://github.com/apple/coremltools/issues/2633);
112
- pin `numpy<2.4.0` until a coremltools release ships the fix from
113
- [#2632](https://github.com/apple/coremltools/pull/2632).
114
-
115
- ### int8-embedding quantization (`TextEncoder.int8emb.mlpackage`)
116
-
117
- Built by post-training-quantizing the token-embedding weight of the already-
118
- converted fp16 `TextEncoder.mlpackage`, via `coremltools.optimize.coreml`:
119
- `OpLinearQuantizerConfig(mode="linear_symmetric", dtype="int8",
120
- granularity="per_channel")` applied to *only* the
121
- `text_model.embeddings.token_embedding` weight (256000×768) by op name —
122
- `OptimizationConfig(global_config=None, op_name_configs={...})` so every
123
- other weight (attention, MLP) is left untouched at fp16. This is a `gather`
124
- weight: each forward pass reads exactly one row per token, so quantization
125
- error doesn't compound the way it would through a matmul chain, which is why
126
- it tolerates int8 so well.
127
-
128
- A whole-model int8 pass (`global_config` instead of a single `op_name_config`)
129
- was also built and measured for comparison: smaller (≈270 MB unzipped vs
130
- ≈352 MB for embedding-only) but with visibly worse worst-case parity (0.9964
131
- vs 0.9996 cosine). Embedding-only was shipped since it's the clearly-better
132
- size/parity trade-off — the extra ~30% file size buys back a large chunk of
133
- the precision the whole-model pass gives up, for a component (search
134
- ranking) where score margins matter.
135
-
136
- ## Parity (PyTorch fp32 reference vs. converted CoreML, cosine similarity)
137
-
138
- 4 test images (solid red / green / blue squares + a synthetic sunset
139
- gradient) × 4 test strings, L2-normalized before comparison. Threshold: > 0.99
140
- per item.
141
-
142
- | item | cosine(pytorch, coreml) |
 
 
 
 
 
 
 
 
 
 
 
 
143
  |---|---|
144
  | image: solid_red | 0.9999988 |
145
  | image: solid_green | 0.9999992 |
@@ -150,53 +100,28 @@ per item.
150
  | text: "a solid blue color" | 0.9999995 |
151
  | text: "a green field" | 0.9999996 |
152
 
153
- **Worst per-item cosine similarity (fp16 text encoder): 0.9999987** — well
154
- above the 0.99 threshold.
155
 
156
- Text↔image ranking also matches exactly between PyTorch and CoreML across
157
- all 4×4 pairs (e.g. "a red square" ranks `solid_red` highest in both
158
- backends; "a photo of a sunset over the ocean" ranks `gradient_sky` highest
159
- in both).
160
 
161
- ### int8-embedding text encoder parity
162
-
163
- Same fixture (4 images × 4 strings), same gate (>0.99 per item, unchanged
164
- rankings), reference is still PyTorch fp32:
165
-
166
- | text | cosine(pytorch, int8emb coreml) |
167
  |---|---|
168
  | "a red square" | 0.99978 |
169
  | "a photo of a sunset over the ocean" | 0.99976 |
170
  | "a solid blue color" | 0.99962 |
171
  | "a green field" | 0.99975 |
172
 
173
- **Worst per-item cosine similarity (int8-embedding text encoder): 0.99962**
174
- — passes the 0.99 gate with a wide margin. Text↔image rankings are identical
175
- to both the PyTorch reference and the fp16 CoreML model across all 4 queries
176
- (each color string still top-matches its own solid-color image; the sunset
177
- string still top-matches the gradient).
178
 
179
- ## sha256
180
 
181
- | file | sha256 |
182
- |---|---|
183
- | `ImageEncoder.mlpackage.zip` | `406938456c8f8e91f01632cb74131fdfb993064d8d0bc8f6fbbd142709f1ba66` |
184
- | `TextEncoder.int8emb.mlpackage.zip` | `11471872101a6ca88dd51ed9de668b728ae9d09c0f853aeac8a6cc96f1c109d6` |
185
- | `TextEncoder.mlpackage.zip` | `4f31f38a1729a0044f0ffc95a940dc1a6a2fdcd5fcf78a03cbb662bada6cbafb` |
186
- | `tokenizer.json` | `cb9140fae3ac5122c972d37adf83e1248471a38147ad76f8215c8872c6fd8322` |
187
- | `tokenizer_config.json` | `14afe629fe4959b9e0d51e1852b8d9f7ad074f90a1a7125a4fcdd17f06e78fc8` |
188
- | `special_tokens_map.json` | `baec30ea10906f16adb8c18af7a34023002c1746542612b8b41c9f09e1351351` |
189
 
190
- ## File sizes
 
191
 
192
- | file | size |
193
- |---|---|
194
- | `ImageEncoder.mlpackage.zip` | 163 MiB |
195
- | `TextEncoder.int8emb.mlpackage.zip` | 323 MiB (**36% smaller** than the fp16 text encoder) |
196
- | `TextEncoder.mlpackage.zip` | 506 MiB |
197
- | `tokenizer.json` | 33 MiB |
198
-
199
- The text encoder is large mostly because of the Gemma tokenizer's 256k-entry
200
- vocabulary embedding table (256000 × 768 × 2 bytes ≈ 375 MiB alone, fp16;
201
- ≈188 MiB at int8, which is most of where the int8-embedding variant's size
202
- savings come from).
 
1
  ---
2
  license: apache-2.0
3
  base_model: google/siglip2-base-patch16-256
4
+ pipeline_tag: feature-extraction
5
  tags:
6
  - coreml
7
  - siglip2
 
10
  - image-text
11
  - embeddings
12
  - vision
13
+ - apple-silicon
14
  ---
15
 
16
+ # SigLIP2 base-patch16-256 — Core ML
17
 
18
+ Core ML (`.mlpackage`) conversion of [`google/siglip2-base-patch16-256`](https://huggingface.co/google/siglip2-base-patch16-256) for on-device text and image embedding on Apple platforms. The image and text towers ship as separate models; both produce 768-dimensional, L2-normalized embeddings in a shared space, so image↔text similarity is a single dot product.
 
 
 
19
 
20
+ Converted and maintained by [nodevorg](https://huggingface.co/nodevorg) (used in production by [Alarian](https://alarian.ai) for semantic frame search). Not affiliated with Google.
 
 
 
 
 
 
 
21
 
22
+ ## Files
 
23
 
24
+ | file | size | sha256 |
25
+ |---|---|---|
26
+ | `ImageEncoder.mlpackage.zip` | 163 MiB | `406938456c8f8e91f01632cb74131fdfb993064d8d0bc8f6fbbd142709f1ba66` |
27
+ | `TextEncoder.int8emb.mlpackage.zip` — **recommended** | 323 MiB | `11471872101a6ca88dd51ed9de668b728ae9d09c0f853aeac8a6cc96f1c109d6` |
28
+ | `TextEncoder.mlpackage.zip` | 506 MiB | `4f31f38a1729a0044f0ffc95a940dc1a6a2fdcd5fcf78a03cbb662bada6cbafb` |
29
+ | `tokenizer.json` | 33 MiB | `cb9140fae3ac5122c972d37adf83e1248471a38147ad76f8215c8872c6fd8322` |
30
+ | `tokenizer_config.json` | — | `14afe629fe4959b9e0d51e1852b8d9f7ad074f90a1a7125a4fcdd17f06e78fc8` |
31
+ | `special_tokens_map.json` | — | `baec30ea10906f16adb8c18af7a34023002c1746542612b8b41c9f09e1351351` |
32
 
33
+ `TextEncoder.int8emb` is a drop-in replacement for the fp16 `TextEncoder` — identical input/output names, shapes, and dtypes — with the token-embedding table quantized to int8 (36% smaller download, worst-case parity 0.9996; details below). Use the full-fp16 text encoder only if you need maximum precision.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
+ The tokenizer files are copied verbatim from the source checkpoint (Gemma BPE, 256k vocabulary).
36
 
37
+ ## Model interface
38
+
39
+ ### ImageEncoder
40
+
41
+ - **Input** — `pixel_values`: Core ML `ImageType`, RGB, 256×256. Preprocessing is baked into the model (`scale=1/127.5`, `bias=[-1,-1,-1]`, matching the checkpoint's `SiglipImageProcessor` configuration): resize to 256×256 and pass raw `[0, 255]` RGB — no caller-side normalization. Resize with an aspect-ratio squash rather than a center crop; that matches training.
42
+ - **Output** — `image_embedding`: 768-dim float16, L2-normalized.
43
+
44
+ ### TextEncoder (both variants)
45
+
46
+ - **Input** — `input_ids`: 1×64 int32 token ids. Tokenize with the included tokenizer using `padding="max_length"`, `max_length=64`, lowercased input (`do_lower_case=true`). There is deliberately **no `attention_mask` input**: SigLIP's text tower trains with unmasked bidirectional attention over the fixed 64-token sequence and pools the last sequence position, so a mask would deviate from training.
47
+ - **Output** — `text_embedding`: 768-dim float16, L2-normalized.
48
+
49
+ Because both outputs are normalized in-graph, cosine similarity is `dot(image_embedding, text_embedding)`.
50
+
51
+ ## Usage (Swift)
52
+
53
+ ```swift
54
+ import CoreML
55
+
56
+ let image = try MLModel(contentsOf: compiledImageEncoderURL)
57
+ let text = try MLModel(contentsOf: compiledTextEncoderURL)
58
+
59
+ // Image: 256×256 RGB CVPixelBuffer, raw [0, 255] values
60
+ let imageOut = try image.prediction(
61
+ from: MLDictionaryFeatureProvider(dictionary: ["pixel_values": pixelBuffer]))
62
+ let imageEmbedding = imageOut.featureValue(for: "image_embedding")!.multiArrayValue!
63
+
64
+ // Text: 64 int32 token ids (e.g. via swift-transformers' Tokenizers module,
65
+ // loading the tokenizer.json from this repo), lowercased, padded to length 64
66
+ let ids = try MLMultiArray(shape: [1, 64], dataType: .int32) // fill with token ids
67
+ let textOut = try text.prediction(
68
+ from: MLDictionaryFeatureProvider(dictionary: ["input_ids": ids]))
69
+ let textEmbedding = textOut.featureValue(for: "text_embedding")!.multiArrayValue!
70
+
71
+ // Similarity = dot product (embeddings are already L2-normalized)
72
+ ```
73
+
74
+ ## Architecture note
75
+
76
+ The source checkpoint's `config.json` declares `model_type: siglip`, not `siglip2`. At this fixed-resolution, non-NaFlex tier, the SigLIP2-trained weights are architecturally identical to SigLIP v1 — `transformers.AutoModel` resolves the checkpoint to `SiglipModel`, and that is what was converted. Relative to SigLIP v1, only the weights and the tokenizer (Gemma BPE 256k, vs. 32k SentencePiece) differ.
77
+
78
+ Most of the text encoder's size is the 256k-row token-embedding table (256000 × 768 × 2 bytes ≈ 375 MiB at fp16), which is why quantizing that single tensor recovers most of the possible size savings.
79
+
80
+ ## Quantized text encoder
81
+
82
+ `TextEncoder.int8emb.mlpackage` is produced by post-training quantization of the converted fp16 model via `coremltools.optimize.coreml`, applying `OpLinearQuantizerConfig(mode="linear_symmetric", dtype="int8", granularity="per_channel")` to **only** the `text_model.embeddings.token_embedding` weight (op-name-targeted); attention and MLP weights remain fp16. The embedding table is a gather weight — each forward pass reads one row per token, so quantization error does not compound through a matmul chain.
83
+
84
+ A whole-model int8 variant was also evaluated: ≈270 MiB unzipped (vs. ≈352 MiB for embedding-only) but with worst-case parity of 0.9964 vs. 0.9996. The embedding-only variant was published as the better size/precision trade-off for retrieval workloads, where score margins matter.
85
+
86
+ ## Parity vs. PyTorch reference
87
+
88
+ Cosine similarity between the PyTorch fp32 reference (`transformers`) and the converted Core ML models, on a fixture of 4 images (solid red/green/blue, synthetic sunset gradient) × 4 texts. Acceptance gate: > 0.99 per item, with unchanged text↔image rankings.
89
+
90
+ ### fp16 encoders
91
+
92
+ | item | cosine |
93
  |---|---|
94
  | image: solid_red | 0.9999988 |
95
  | image: solid_green | 0.9999992 |
 
100
  | text: "a solid blue color" | 0.9999995 |
101
  | text: "a green field" | 0.9999996 |
102
 
103
+ Worst case **0.9999987**. Text↔image rankings match the PyTorch reference exactly across all 4×4 pairs.
 
104
 
105
+ ### int8-embedding text encoder
 
 
 
106
 
107
+ | text | cosine |
 
 
 
 
 
108
  |---|---|
109
  | "a red square" | 0.99978 |
110
  | "a photo of a sunset over the ocean" | 0.99976 |
111
  | "a solid blue color" | 0.99962 |
112
  | "a green field" | 0.99975 |
113
 
114
+ Worst case **0.99962**; rankings identical to both the PyTorch reference and the fp16 model.
 
 
 
 
115
 
116
+ ## Conversion details
117
 
118
+ Converted from the Hugging Face `transformers` reference implementation. Each tower was traced independently (`torch.jit.trace` on thin wrappers around `SiglipModel.vision_model` / `.text_model`) and converted with:
 
 
 
 
 
 
 
119
 
120
+ - `convert_to="mlprogram"`, `compute_precision=FLOAT16`
121
+ - `minimum_deployment_target`: macOS 15
122
 
123
+ Pinned toolchain: `torch==2.7.0`, `transformers==5.14.1`, `coremltools==9.0`, `numpy==2.3.5`. Note that `numpy>=2.4.0` breaks the `coremltools==9.0` PyTorch frontend for models using `torch.nn.MultiheadAttention` (SigLIP's vision pooling head) — see [apple/coremltools#2633](https://github.com/apple/coremltools/issues/2633); pin `numpy<2.4.0` until the fix ships in a release.
124
+
125
+ ## License
126
+
127
+ Apache-2.0, inherited from the base model. Free for commercial and closed-source use.