swift-embeddings

repository·main·Indexed 41 days ago

https://github.com/jkrukowski/swift-embeddings

A Swift library for running embedding models locally using MLTensor. It supports a wide variety of architectures, including BERT, ModernBERT, Qwen3, CLIP, RoBERTa, XLM-RoBERTa, NomicBERT, Model2Vec, Word2Vec, and static embeddings.

Tokens
4.8K
Snippets
23
Records
27
Agent score
99%

What's inside swift-embeddings

  1. Manage benchmark baselines

    main

    The benchmarking toolchain allows you to manage performance baselines to track regressions. Use the following commands to list, update, or compare benchmarks against a stored baseline (e.g., the main branch).

    # List available benchmarks
    swift package benchmark list
    
    # Run and store a baseline
    swift package --disable-sandbox benchmark baseline update main
    
    # Compare against a stored baseline
    swift package --disable-sandbox benchmark baseline compare main
  2. Run accuracy tests against Python/transformers reference

    main

    Accuracy tests compare Swift outputs against a Python/transformers reference. These tests are skipped by default unless the UV_PATH environment variable points to a uv binary.

    Important: You must run these tests with the --no-parallel flag. Running multiple model forward passes concurrently can cause crashes in the underlying MLTensor/BNNS matmul operations.

    To run the accuracy tests, use the following command structure:

    • Set PYTORCH_ENABLE_MPS_FALLBACK=1 to allow the PyTorch reference to fall back to CPU for operations unsupported on MPS.
    • Set UV_PATH to your uv binary location.
    • Set CI_DISABLE_NETWORK_MONITOR=1 to disable the network monitor (assuming models are already downloaded).
    PYTORCH_ENABLE_MPS_FALLBACK=1 UV_PATH=$(which uv) CI_DISABLE_NETWORK_MONITOR=1 \
        swift test --no-parallel --filter AccuracyTests
  3. Install swift-embeddings via Swift Package Manager

    main

    To use swift-embeddings in your Swift project, add the package to your Package.swift file.

    1. Add the package dependency:
    dependencies: [
        .package(url: "https://github.com/jkrukowski/swift-embeddings", from: "0.0.16")
    ]
    1. Add the Embeddings product to your target dependencies:
    dependencies: [
        .product(name: "Embeddings", package: "swift-embeddings")
    ]
    dependencies: [
        .package(url: "https://github.com/jkrukowski/swift-embeddings", from: "0.0.16")
    ]
    
    targets: [
        .target(
            name: "YourTarget",
            dependencies: [
                .product(name: "Embeddings", package: "swift-embeddings")
            ]
        )
    ]
  4. Run swift-embeddings benchmarks

    main

    To execute the benchmarks for swift-embeddings, run the following command from the Benchmarks directory. Note that the benchmarking toolchain is maintained as a separate Swift package to keep the main package graph clean.

    On the first run, the tool will download a small static embedding model (minishlab/potion-base-8M) from the Hugging Face Hub and cache it locally. Subsequent runs will use the cached model and will not require network access during measurement.

    swift package --disable-sandbox benchmark
  5. Run performance benchmarks

    main

    The Benchmarks package uses package-benchmark to measure the encoding performance of various model architectures.

    To run all benchmarks, use the --disable-sandbox flag. This is required because the benchmark setup phase downloads the model (which is cached after the first run).

    To see a list of all available benchmarks, use the benchmark list command.

    # Run all benchmarks
    swift package --package-path Benchmarks --disable-sandbox benchmark
    
    # List available benchmarks
    swift package --package-path Benchmarks benchmark list
  6. Load models with specific weight key transforms

    main

    Some models require specific configuration during loading to correctly map weight keys.

    • BERT (google-bert/bert-base-uncased): Requires .googleBert config.
    • ModernBERT (answerdotai/ModernBERT-base): Requires a weight key prefix.
    • RoBERTa (FacebookAI/roberta-base): Requires a weight key prefix.
    • XLM-RoBERTa (FacebookAI/xlm-roberta-base): Requires a weight key prefix.

    Examples

    // BERT with googleBert config
    let modelBundle = try await Bert.loadModelBundle(
        from: "google-bert/bert-base-uncased",
        loadConfig: .googleBert
    )
    
    // ModernBERT with prefix
    let modelBundle = try await Bert.loadModelBundle(
        from: "answerdotai/ModernBERT-base",
        loadConfig: .addWeightKeyPrefix("model.")
    )
    
    // RoBERTa with prefix
    let modelBundle = try await Roberta.loadModelBundle(
        from: "FacebookAI/roberta-base",
        loadConfig: .addWeightKeyPrefix("roberta.")
    )
    let modelBundle = try await Bert.loadModelBundle(
        from: "google-bert/bert-base-uncased",
        loadConfig: .googleBert
    )
  7. Encode text using transformer models

    main

    You can load a model bundle from Hugging Face and encode a single string into an embedding. The resulting embedding can be converted to a Float array using MLTensor utilities.

    import Embeddings
    
    // load model and tokenizer from Hugging Face
    let modelBundle = try await Bert.loadModelBundle(
        from: "sentence-transformers/all-MiniLM-L6-v2"
    )
    
    // encode text
    let encoded = try modelBundle.encode("The cat is black")
    let result = await encoded.cast(to: Float.self).shapedArray(of: Float.self).scalars
    
    // print result
    print(result)
    import Embeddings
    
    let modelBundle = try await Bert.loadModelBundle(
        from: "sentence-transformers/all-MiniLM-L6-v2"
    )
    
    let encoded = try modelBundle.encode("The cat is black")
    let result = await encoded.cast(to: Float.self).shapedArray(of: Float.self).scalars
    
    print(result)
  8. Batch encode multiple texts

    main

    For processing multiple strings at once, use batchEncode. This is useful for calculating distances (like cosine distance) between multiple embeddings.

    import Embeddings
    import MLTensorUtils
    
    let texts = [
        "The cat is black",
        "The dog is black",
        "The cat sleeps well"
    ]
    let modelBundle = try await Bert.loadModelBundle(
        from: "sentence-transformers/all-MiniLM-L6-v2"
    )
    let encoded = try modelBundle.batchEncode(texts)
    let distance = cosineDistance(encoded, encoded)
    let result = await distance.cast(to: Float.self).shapedArray(of: Float.self).scalars
    print(result)
    import Embeddings
    import MLTensorUtils
    
    let texts = [
        "The cat is black",
        "The dog is black",
        "The cat sleeps well"
    ]
    let modelBundle = try await Bert.loadModelBundle(
        from: "sentence-transformers/all-MiniLM-L6-v2"
    )
    let encoded = try modelBundle.batchEncode(texts)
    let distance = cosineDistance(encoded, encoded)
    let result = await distance.cast(to: Float.self).shapedArray(of: Float.self).scalars
    print(result)
  9. Configure EncodeOptions for transformer models

    main

    Transformer bundles (Bert, ModernBert, NomicBert, Qwen3, Roberta, XLMRoberta, Clip) use EncodeOptions to control the encoding process.

    Available Options

    • maxLength: Maximum number of tokens per input sequence.
    • padTokenId: Padding token ID used by batchEncode (nil uses the model's own ID).
    • postProcess: How the token-level output is pooled.
    • computePolicy: The MLComputePolicy for the underlying MLTensor work (e.g., .cpuAndGPU or .cpuOnly).

    Pooling (postProcess)

    • .clsTokenPool(normalize:): Use the first ([CLS]) token.
    • .meanPool(normalize:): Average the (attention-masked) tokens.
    • .lastTokenPool(normalize:): Use the last non-padding token.
    • nil: Return raw token-level output without pooling.

    Note: normalize: true applies L2-normalization to the pooled embedding.

    Example: Customizing Options

    var options = EncodeOptions.bert
    options.postProcess = .meanPool(normalize: true)
    options.computePolicy = .cpuOnly
    let encoded = try modelBundle.encode("The cat is black", options: options)
    var options = EncodeOptions.bert
    options.postProcess = .meanPool(normalize: true)
    options.computePolicy = .cpuOnly
    let encoded = try modelBundle.encode("The cat is black", options: options)
  10. Use the embeddings-cli

    main

    The embeddings-cli allows you to encode text from the command line.

    Command Syntax

    swift run embeddings-cli <subcommand> [--model-id <model-id>] [--model-file <model-file>] [--text <text>] [--max-length <max-length>]

    Subcommands

    • bert: Encode text using BERT model
    • modern-bert: Encode text using ModernBERT model
    • nomic-bert: Encode text using Nomic embedding model
    • qwen3: Encode text using Qwen3 embedding model
    • clip: Encode text using CLIP model
    • model2vec: Encode text using Model2Vec model
    • roberta: Encode text using RoBERTa model
    • static-embeddings: Encode text using Static Embeddings model
    • xlm-roberta: Encode text using XLMRoberta model
    • word2vec: Encode word using Word2Vec model

    Options

    • --model-id <model-id>: ID of the model to use
    • --model-file <model-file>: Path to the model file (only for Word2Vec)
    • --text <text>: Text to encode
    • --max-length <max-length>: Maximum length of the input (not for Word2Vec)
    • -h, --help: Show help information