Rewrite model card as a reference card
Browse files
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 —
|
| 15 |
|
| 16 |
-
|
| 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 |
-
|
| 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 |
-
|
| 31 |
-
here — free for commercial and closed-source use.
|
| 32 |
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
|
| 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 |
-
|
| 98 |
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
##
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
**
|
| 154 |
-
above the 0.99 threshold.
|
| 155 |
|
| 156 |
-
|
| 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 |
-
|
| 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 |
-
**
|
| 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 |
-
##
|
| 180 |
|
| 181 |
-
|
| 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 |
-
|
|
|
|
| 191 |
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|