GemmaEmbed Swift package optimized for Apple Silicon Developer Maralski released GemmaEmbed 0.4.0, a dependency-free Swift package that runs Google's EmbeddingGemma 2 text, image and audio encoders in-process on Apple Silicon using its own Metal kernels, without llama.cpp, MLX or Core ML. The package produces 768-dimensional embeddings that match llama.cpp's to a cosine similarity of 0.99999 for text, 0.99999 for images and 0.99998 for audio in one shared space, loading the official embeddinggemma-2-Q8_0.gguf weights (309,855,456 bytes, 24 layers, 271M parameters) and an optional 554,821,024-byte mmproj projector for image and audio input. It requires macOS 14 or later on Apple Silicon and Swift 6, accepts up to 8192 tokens per text (default 2048), and supports Matryoshka truncation to 512, 256 or 128 dimensions. GemmaEmbed runs Google's EmbeddingGemma 2 text, image and audio encoders in-process on Apple Silicon with its own Metal kernels. It is a Swift package with no dependencies beyond the Apple SDK: no llama.cpp, no MLX, no Core ML. You hand it the official GGUF weight files and get 768-dimensional embeddings that match llama.cpp's to a cosine similarity of 0.99999 for text, 0.99999 for images and 0.99998 for audio, in one shared space. | Model | embeddinggemma-2 GGUF architecture gemma-embedding2 , 24 layers, 271M parameters | | Weight file | embeddinggemma-2-Q8 0.gguf from https://huggingface.co/ggml-org/embeddinggemma-2-GGUF https://huggingface.co/ggml-org/embeddinggemma-2-GGUF | | Size | 309,855,456 bytes | | SHA256 | 2188ac1deca4b77dffefd603c2776a9d76d9d74ec01841392982ebb840b09135 | | Output | 768 dimensions, Matryoshka truncation to 512, 256 or 128 | | Input | up to 8192 tokens per text default limit 2048 , any number of texts per call | | Image and audio encoders optional | mmproj-embeddinggemma-2-Q8 0.gguf from the same repository, 554,821,024 bytes, SHA256 c4a8a52691ecef40618438928bdf9e68379b854e24166f292592353db0aab64f GGUF projectors gemma4v , 16 layers, and gemma4a , 12 Conformer layers; 473M parameters in all | | Image input | JPEG, PNG, HEIC and anything else ImageIO reads, or a CGImage ; any size, scaled to 70 to 1120 vision tokens default cap 256 | | Audio input | WAV parsed by GemmaEmbed , MP3, M4A, AIFF, CAF, FLAC through AVFoundation or PCM samples; any rate and channel count, resampled to 16 kHz mono; 25 rows per second, up to 5 minutes 27 seconds per clip | | Platform | macOS 14 or later on Apple Silicon, Swift 6 | The forward pass is documented in Sources/GemmaEmbed/Model/Architecture.md https://github.com/maralski/gemma-embed-swift/blob/main/Sources/GemmaEmbed/Model/Architecture.md . Add the package to Package.swift : dependencies: .package url: "https://github.com/maralski/gemma-embed-swift.git", from: "0.4.0" , , targets: .target name: "MyApp", dependencies: .product name: "GemmaEmbed", package: "gemma-embed-swift" , In Xcode, choose File Add Package Dependencies and enter the same URL. The weights are not part of the package. Download embeddinggemma-2-Q8 0.gguf and, for images and audio, mmproj-embeddinggemma-2-Q8 0.gguf from the Hugging Face repository above. For a side-by-side comparison with llama.cpp accuracy, speed, features, when to use which , see COMPARISON.md https://github.com/maralski/gemma-embed-swift/blob/main/COMPARISON.md . python import GemmaEmbed let model = try await EmbeddingModel.load from: URL fileURLWithPath: "embeddinggemma-2-Q8 0.gguf" let documents = try await model.embed "Rates were left unchanged.", "A new open-weight model shipped." let query = try await model.embed "central bank decision", options: EmbedOptions task: .query let best = documents.indices.max { model.similarity query, documents $0 < model.similarity query, documents $1 } EmbedOptions selects the model card's task prefix .document , .query , .clustering , .classification , .similarity , .none , the token limit, the output width and whether the vector is L2-normalised. embed batches texts by token count, reports progress after every GPU batch and stops between batches when the surrounding Task is cancelled. Every text is embedded identically whether it travels alone or inside a batch. Load the model with the image encoder and embed files, bytes or CGImage s. The vectors live in the same space as the text embeddings, so a caption query can be scored against pictures and a picture against pictures. js let model = try await EmbeddingModel.load from: textURL, options: LoadOptions imageEncoder: mmprojURL let pictures = try await model.embed images: .url photoURL , .data jpegBytes , .cgImage image let caption = try await model.embed "rolling green vineyard hills", options: EmbedOptions task: .query let score = model.similarity caption, pictures 0 print model.info.imageEncoder?.layerCount // 16 when loaded, nil otherwise ImageEmbedOptions carries the same dimensions and normalise as text, batchSize images per GPU batch, by default packed by patch count and maxImageTokens . The image encoder is a LoadOptions field rather than something added to a loaded model, so a model never changes after load ; loading text alone never opens the mmproj file, and embed images: without it throws imageEncoderNotLoaded . The encoder sees each image resized so that its patch grid gives at most maxImageTokens vision tokens 16 by 16 pixel patches, pooled 3 by 3 , and at least 70. The default cap of 256 is what llama-server applies with its default batch size; llama.cpp's native limit is 1120. The same photo embeds differently at different caps cosine 0.973 between 256 and 1120 on the 1600 by 1600 green hills scene , so a corpus and the queries against it must share one cap. JPEGs decode through a Swift port of stb image's decoder, byte for byte what llama.cpp sees; PNGs match it too. The preprocessing and the vision tower are documented in Sources/GemmaEmbed/Model/Architecture.md https://github.com/maralski/gemma-embed-swift/blob/main/Sources/GemmaEmbed/Model/Architecture.md . One input can mix text, images and audio. The model reads each picture and each clip where it sits among the text, in one forward pass, so a post with its photo and voice note embeds as one document. js let mmproj = URL fileURLWithPath: "mmproj-embeddinggemma-2-Q8 0.gguf" let model = try await EmbeddingModel.load from: textURL, options: LoadOptions imageEncoder: mmproj, audioEncoder: mmproj let post = MultimodalInput .text "Our walk on Saturday:" , .image .url photoURL , .text "and what the guide said:" , .audio .url voiceNoteURL let posts = try await model.embed post, MultimodalInput text: "Quarterly results", images: .data chartPNG let query = try await model.embed "guided hill walk", options: EmbedOptions task: .query let score = model.similarity query, posts 0 The sequence is the one llama.cpp's mtmd builds for EmbeddingGemma 2: bos text <|image image rows