Skip to content

feat(embeddings): pooling, masks, Embedding kind and /v1/embeddings - #1408

Merged
inureyes merged 1 commit into
mainfrom
feature/issue-1353-embeddings-foundation
Aug 25, 2026
Merged

inureyes merged 1 commit into
mainfrom
feature/issue-1353-embeddings-foundation

Conversation

@inureyes

Copy link
Copy Markdown
Member

Summary

Adds the shared embedding foundation from #1353 (epic #1348): the padding-aware attention-mask builders, the src/embeddings/ module with the EmbeddingModel trait, pooling, normalization, length limits and batch tokenization, ModelKind::Embedding with layout-based detection, subfolder safetensors loading, the dedicated embedding worker thread, POST /v1/embeddings, the server flags, mlxcel embed and docs/embeddings.md. No family forward pass lands here; every recognized family reports not yet supported from the dispatcher until its own port merges, and the route, batching and pooling are exercised end to end through a test-only stub.

What changed

  • src/lib/mlxcel-core/src/utils.rs: create_bidirectional_padding_mask, create_causal_padding_mask, create_bidirectional_window_mask (f32 0 / -inf built with where_cond, fully blocked rows keep their diagonal, // Used by: comments) and the array_to_vec_f32 readback helper; tests in utils_embedding_mask_tests.rs, including the f16 / bf16 cast and softmax finiteness check.
  • src/lib/mlxcel-core/src/weights.rs: load_weights_from_dir_with_subfolders reads the top-level shards and prefixes every <dir>/*/ *.safetensors tensor with <folder>., skipping consolidated.safetensors and adapter_model.safetensors.
  • src/embeddings/: model.rs (trait, EmbeddingBatch, EmbeddingOutput, ImageInput), pooling.rs (cls / mean / max / lasttoken, PoolingConfig::read for both key styles with load errors for weightedmean, mean_sqrt_len_tokens, include_prompt: false and combined flags, resolve_pooling_mode with the MLXCEL_EMBEDDING_POOLING override, normalize_l2, truncate_dimensions), limits.rs (max_length derivation per the issue rule, pad token, vocab size), tokenize.rs (right-padded encode_batch / encode_pairs, truncation that keeps a trailing special token, strip_padding_and_truncation for tokenizer.json files that bake in a fixed pad), loader.rs (load_embedding_model, the per-family dispatcher, load_embedding_weights with the text bf16 rule, quantization_params), engine.rs (length-sorted micro-batching, normalization, dimensions, multi-vector rows, readback) and the test-only stub.rs.
  • src/model_metadata.rs, src/models/mod.rs, src/models/detection.rs, src/loading/mod.rs: ModelKind::Embedding, thirteen ModelType variants under the Embedding family in mlxcel arch, is_embedding_checkpoint (encoder-only model_type, embedding architectures[0] including the flag-gated Gemma3TextModel and Ministral3Model, modules.json Pooling entry, 1_Pooling/config.json; ForSequenceClassification and a LogitScore-only modules.json never qualify; a pooling layout on an unmapped model_type is an error rather than a misroute), and the load_model bail pointing at /v1/embeddings.
  • src/server/embedding_model.rs, src/server/embedding_worker.rs: EmbeddingModelProvider, EmbeddingError { QueueFull, Timeout, InvalidInput, Internal }, and EmbeddingWorker with the audio worker's bounded SyncSender, reply timeout and catch_unwind boundary; model, tokenizer and arrays live on the worker thread.
  • src/server/routes/embeddings.rs, src/server/types/embeddings.rs, src/server/app.rs, src/server/routes/models.rs, src/server/state.rs: POST /v1/embeddings and /embeddings with string, list, token-id, token-id list and typed-part inputs, encoding_format: base64 (little-endian f32, shape sibling for multi-vector output), dimensions, instruction, usage.prompt_tokens, the 400 / 501 / 503 / 504 mapping, image decoding through the shared media limits, the embedding_model slot and the extra /v1/models entry.
  • src/server/config.rs, src/server/cli_input.rs, src/server/startup.rs, src/bin/mlx_server.rs, src/main.rs, src/commands/serve.rs: --embedding-model (LLAMA_ARG_EMBEDDING_MODEL, MLXCEL_EMBEDDING_MODEL), --embedding-batch-size, --embedding-max-length, --embedding-queue-depth, --embedding-request-timeout-secs; -m <embedding checkpoint> serves the route while the chat worker logs and stays unloaded, -m <chat> --embedding-model <embed> serves both, both embedding sources together is a startup error, an explicit --embedding-model load failure is fatal.
  • src/commands/embed.rs: mlxcel embed -m <path> -p ... [--image ...] [--instruction] [--dimensions] [--json] printing one vector per line and the cosine (MaxSim for multi-vector) matrix.
  • docs/embeddings.md (new), docs/supported-models.md (Embedding models section with the empty family table), docs/environment-variables.md, docs/README.md, README.md, CONTRIBUTING.md.

Real-checkpoint verification

  • sentence-transformers/all-MiniLM-L6-v2: detected as Bert via model_type: bert and 1_Pooling/config.json; pooling config parses to mean; max_length derives to 256 from sentence_bert_config.json; load_weights_from_dir_with_subfolders reads the top-level shard (embeddings.word_embeddings.weight [30522, 384]) and prefixes a synthetic 2_Dense/model.safetensors as 2_Dense.linear.weight; the batch tokenizer right-pads with [PAD] (id 0), keeps [CLS] / [SEP], is no longer padded to the fixed 128 baked into tokenizer.json, and keeps [SEP] under truncation; load_model bails with the /v1/embeddings message.
  • Qwen/Qwen3-Embedding-0.6B: detected as Qwen3Embedding via modules.json / 1_Pooling with pooling_mode_lasttoken; max_length derives to the 8192 cap; pad token resolves to <|endoftext|> (151643); every encoded row ends with <|endoftext|> and truncation keeps it; load_model bails.
  • Detection sweep over the locally present checkpoints (intfloat/multilingual-e5-small, BAAI/bge-m3, nomic-ai/modernbert-embed-base, google/siglip-base-patch16-224, mlx-community/embeddinggemma-300m-4bit, Qwen/Qwen3-VL-Embedding-2B, LiquidAI/LFM2.5-Embedding-350M, nvidia/Nemotron-3-Embed-1B-BF16, nvidia/llama-nemotron-embed-1b-v2, nvidia/llama-nemotron-embed-vl-1b-v2, vidore/ColSmolVLM-Instruct-256M-base, vidore/colqwen2.5-base) resolves each to its family, and the rerankers (Qwen/Qwen3-VL-Reranker-2B, mlx-community/Qwen3-Reranker-0.6B-4bit, cross-encoder/ms-marco-MiniLM-L6-v2, BAAI/bge-reranker-v2-m3, Alibaba-NLP/gte-reranker-modernbert-base) never route to an embedding variant. All of these gates soft-skip when the checkpoint is absent.
  • mlxcel-server -m all-MiniLM-L6-v2 end to end: the chat worker logs the /v1/embeddings rejection and stays unloaded, /v1/models lists the checkpoint, /v1/embeddings and /embeddings answer the structured 501, a malformed body answers 400; -m all-MiniLM-L6-v2 --embedding-model Qwen3-Embedding-0.6B fails at startup with the two-embedding-models message; -m Qwen3-Reranker-0.6B-4bit --embedding-model all-MiniLM-L6-v2 (and the MLXCEL_EMBEDDING_MODEL alias) fails at startup naming the flag and the unported family; mlxcel embed and mlxcel generate on both checkpoints report the expected errors.
  • The forward pass is exercised by the test-only stub (mean-pooled one-hot token embeddings): the route tests cover string, list-across-micro-batches, token-id, base64 round-trip, dimensions, empty input, 501, model mismatch, usage counting, image rejection and multi-vector output.

Test plan

  • cargo test --profile test-fast --features cuda --lib -- server::embedding_worker server::routes::embeddings models::detection_tests model_metadata_tests models::tests embeddings:: server::cli_input_tests server::startup_tests (115 passed)
  • cargo test --profile test-fast --features cuda --lib -- embeddings::real_checkpoint_tests (8 passed with both checkpoints present)
  • cargo test --profile test-fast --features cuda -p mlxcel-core --lib embedding_mask_tests (5 passed)
  • cargo test --profile test-fast --features cuda --bin mlxcel -- embed serve_tests (3 passed)
  • cargo check --profile test-fast --features cuda --all-targets
  • cargo clippy --profile test-fast --features cuda --lib --bins --tests -- -D warnings
  • cargo fmt --all -- --check
  • Workspace test run on the Metal CI runner

Closes #1353

mlxcel had no embedding path: no pooling, no padding-only attention mask, no model kind that loads a checkpoint without an lm_head, and no /v1/embeddings route. This adds the shared foundation every embedding family builds on, so the family ports in epic #1348 only have to register a constructor.

mlxcel-core gains create_bidirectional_padding_mask, create_causal_padding_mask and create_bidirectional_window_mask next to create_causal_mask (f32 0/-inf, built with where_cond so they survive the f16 cast without NaN, fully padded rows keep their diagonal), an array_to_vec_f32 readback helper, and load_weights_from_dir_with_subfolders, which reads the top-level shards and prefixes every subfolder tensor with <folder>. so a sentence-transformers 2_Dense module is not dropped.

The new src/embeddings module holds the EmbeddingModel trait with EmbeddingBatch and EmbeddingOutput, the four pooling modes with the 1_Pooling/config.json reader for both key styles (unsupported modes are load errors), L2 normalization and dimensions truncation, max_length derivation, pad token and vocab resolution, right-padded batch tokenization that keeps a trailing special token under truncation and strips any padding baked into tokenizer.json, the load_embedding_model dispatcher with a not-yet-supported arm per family, and the engine that sorts texts by length, micro-batches them, runs the forward pass, normalizes and reads back plain vectors.

ModelKind::Embedding and thirteen ModelType variants are registered; get_model_type recognizes an embedding checkpoint before the model_type match by encoder-only model_type, embedding architectures[0] (including the flag-gated Gemma3TextModel and Ministral3Model), a modules.json Pooling entry or a 1_Pooling/config.json, and never for ForSequenceClassification or a LogitScore-only modules.json. load_model rejects the kind with a message pointing at /v1/embeddings.

The server gets an EmbeddingModelProvider seam, a dedicated EmbeddingWorker thread with the audio worker's bounded queue, reply timeout and panic boundary, the POST /v1/embeddings and /embeddings routes with base64, dimensions, token-id input, multi-vector output and the 400/501/503/504 mapping, the embedding entry in /v1/models, the --embedding-model, --embedding-batch-size, --embedding-max-length, --embedding-queue-depth and --embedding-request-timeout-secs flags on both binaries (with the LLAMA_ARG_EMBEDDING_MODEL and MLXCEL_EMBEDDING_MODEL aliases), and startup wiring that serves an -m embedding checkpoint on the route while chat stays unloaded and rejects two embedding models. mlxcel embed is the offline validation tool, and docs/embeddings.md documents the surface.

Validated with the unit suites under embeddings::, server::embedding_worker, server::routes::embeddings and models::detection_tests, the mlxcel-core mask tests, clippy and fmt, and real-checkpoint gates on sentence-transformers/all-MiniLM-L6-v2 and Qwen/Qwen3-Embedding-0.6B (detection, pooling config, max_length, subfolder weights, batch tokenization, generation-loader rejection) plus a detection sweep over the locally present embedding and reranker checkpoints. Server startup was exercised end to end with the MiniLM checkpoint in -m and with the two-embedding-models and explicit --embedding-model error paths.

Refs #1353
@inureyes inureyes added status:review Under review type:enhancement New features, capabilities, or significant additions priority:high High priority area:docs User and developer documentation area:core mlxcel-core: MLX FFI, primitives, KV cache, layers area:inference Generation, sampling, decoding (incl. speculative, DRY) area:cli Command-line interface / CLI flags modeltype:text Text-only language model status:done Completed and removed status:review Under review labels Aug 25, 2026
@inureyes
inureyes merged commit b6f7785 into main Aug 25, 2026
11 checks passed
@inureyes
inureyes deleted the feature/issue-1353-embeddings-foundation branch August 25, 2026 17:27
inureyes added a commit that referenced this pull request Aug 25, 2026
PR #1408 (feat(embeddings): pooling, masks, Embedding kind and /v1/embeddings, squash-merged as b6f7785) was merged without its technical report. This PR adds the bilingual report pair `TECHNICAL_REPORTS/1408-embeddings-foundation-20260825.en.md` and `.ko.md`, dated by the PR's merge date, covering the `src/embeddings/` subsystem, the padding-aware mask builders and subfolder safetensors loading in `mlxcel-core`, `ModelKind::Embedding` detection, the embedding worker thread, `POST /v1/embeddings`, the `--embedding-*` server flags, `mlxcel embed`, the verification evidence recorded on the PR, and the follow-up items left for the epic #1348 family ports. No code changes are included.

Refs #1353
@inureyes inureyes self-assigned this Aug 27, 2026
be-free-snowboy pushed a commit to freestylejs/snowbrain-mlxcel that referenced this pull request Oct 27, 2026
…ablup#1408)

## Summary

Adds the shared embedding foundation from lablup#1353 (epic lablup#1348): the padding-aware attention-mask builders, the `src/embeddings/` module with the `EmbeddingModel` trait, pooling, normalization, length limits and batch tokenization, `ModelKind::Embedding` with layout-based detection, subfolder safetensors loading, the dedicated embedding worker thread, `POST /v1/embeddings`, the server flags, `mlxcel embed` and `docs/embeddings.md`. No family forward pass lands here; every recognized family reports `not yet supported` from the dispatcher until its own port merges, and the route, batching and pooling are exercised end to end through a test-only stub.

## What changed

- `src/lib/mlxcel-core/src/utils.rs`: `create_bidirectional_padding_mask`, `create_causal_padding_mask`, `create_bidirectional_window_mask` (f32 `0 / -inf` built with `where_cond`, fully blocked rows keep their diagonal, `// Used by:` comments) and the `array_to_vec_f32` readback helper; tests in `utils_embedding_mask_tests.rs`, including the f16 / bf16 cast and softmax finiteness check.
- `src/lib/mlxcel-core/src/weights.rs`: `load_weights_from_dir_with_subfolders` reads the top-level shards and prefixes every `<dir>/*/ *.safetensors` tensor with `<folder>.`, skipping `consolidated.safetensors` and `adapter_model.safetensors`.
- `src/embeddings/`: `model.rs` (trait, `EmbeddingBatch`, `EmbeddingOutput`, `ImageInput`), `pooling.rs` (`cls` / `mean` / `max` / `lasttoken`, `PoolingConfig::read` for both key styles with load errors for `weightedmean`, `mean_sqrt_len_tokens`, `include_prompt: false` and combined flags, `resolve_pooling_mode` with the `MLXCEL_EMBEDDING_POOLING` override, `normalize_l2`, `truncate_dimensions`), `limits.rs` (`max_length` derivation per the issue rule, pad token, vocab size), `tokenize.rs` (right-padded `encode_batch` / `encode_pairs`, truncation that keeps a trailing special token, `strip_padding_and_truncation` for `tokenizer.json` files that bake in a fixed pad), `loader.rs` (`load_embedding_model`, the per-family dispatcher, `load_embedding_weights` with the text bf16 rule, `quantization_params`), `engine.rs` (length-sorted micro-batching, normalization, `dimensions`, multi-vector rows, readback) and the test-only `stub.rs`.
- `src/model_metadata.rs`, `src/models/mod.rs`, `src/models/detection.rs`, `src/loading/mod.rs`: `ModelKind::Embedding`, thirteen `ModelType` variants under the `Embedding` family in `mlxcel arch`, `is_embedding_checkpoint` (encoder-only `model_type`, embedding `architectures[0]` including the flag-gated `Gemma3TextModel` and `Ministral3Model`, `modules.json` Pooling entry, `1_Pooling/config.json`; `ForSequenceClassification` and a LogitScore-only `modules.json` never qualify; a pooling layout on an unmapped `model_type` is an error rather than a misroute), and the `load_model` bail pointing at `/v1/embeddings`.
- `src/server/embedding_model.rs`, `src/server/embedding_worker.rs`: `EmbeddingModelProvider`, `EmbeddingError { QueueFull, Timeout, InvalidInput, Internal }`, and `EmbeddingWorker` with the audio worker's bounded `SyncSender`, reply timeout and `catch_unwind` boundary; model, tokenizer and arrays live on the worker thread.
- `src/server/routes/embeddings.rs`, `src/server/types/embeddings.rs`, `src/server/app.rs`, `src/server/routes/models.rs`, `src/server/state.rs`: `POST /v1/embeddings` and `/embeddings` with string, list, token-id, token-id list and typed-part inputs, `encoding_format: base64` (little-endian f32, `shape` sibling for multi-vector output), `dimensions`, `instruction`, `usage.prompt_tokens`, the `400 / 501 / 503 / 504` mapping, image decoding through the shared media limits, the `embedding_model` slot and the extra `/v1/models` entry.
- `src/server/config.rs`, `src/server/cli_input.rs`, `src/server/startup.rs`, `src/bin/mlx_server.rs`, `src/main.rs`, `src/commands/serve.rs`: `--embedding-model` (`LLAMA_ARG_EMBEDDING_MODEL`, `MLXCEL_EMBEDDING_MODEL`), `--embedding-batch-size`, `--embedding-max-length`, `--embedding-queue-depth`, `--embedding-request-timeout-secs`; `-m <embedding checkpoint>` serves the route while the chat worker logs and stays unloaded, `-m <chat> --embedding-model <embed>` serves both, both embedding sources together is a startup error, an explicit `--embedding-model` load failure is fatal.
- `src/commands/embed.rs`: `mlxcel embed -m <path> -p ... [--image ...] [--instruction] [--dimensions] [--json]` printing one vector per line and the cosine (MaxSim for multi-vector) matrix.
- `docs/embeddings.md` (new), `docs/supported-models.md` (Embedding models section with the empty family table), `docs/environment-variables.md`, `docs/README.md`, `README.md`, `CONTRIBUTING.md`.

## Real-checkpoint verification

- `sentence-transformers/all-MiniLM-L6-v2`: detected as `Bert` via `model_type: bert` and `1_Pooling/config.json`; pooling config parses to `mean`; `max_length` derives to 256 from `sentence_bert_config.json`; `load_weights_from_dir_with_subfolders` reads the top-level shard (`embeddings.word_embeddings.weight` `[30522, 384]`) and prefixes a synthetic `2_Dense/model.safetensors` as `2_Dense.linear.weight`; the batch tokenizer right-pads with `[PAD]` (id 0), keeps `[CLS]` / `[SEP]`, is no longer padded to the fixed 128 baked into `tokenizer.json`, and keeps `[SEP]` under truncation; `load_model` bails with the `/v1/embeddings` message.
- `Qwen/Qwen3-Embedding-0.6B`: detected as `Qwen3Embedding` via `modules.json` / `1_Pooling` with `pooling_mode_lasttoken`; `max_length` derives to the 8192 cap; pad token resolves to `<|endoftext|>` (151643); every encoded row ends with `<|endoftext|>` and truncation keeps it; `load_model` bails.
- Detection sweep over the locally present checkpoints (`intfloat/multilingual-e5-small`, `BAAI/bge-m3`, `nomic-ai/modernbert-embed-base`, `google/siglip-base-patch16-224`, `mlx-community/embeddinggemma-300m-4bit`, `Qwen/Qwen3-VL-Embedding-2B`, `LiquidAI/LFM2.5-Embedding-350M`, `nvidia/Nemotron-3-Embed-1B-BF16`, `nvidia/llama-nemotron-embed-1b-v2`, `nvidia/llama-nemotron-embed-vl-1b-v2`, `vidore/ColSmolVLM-Instruct-256M-base`, `vidore/colqwen2.5-base`) resolves each to its family, and the rerankers (`Qwen/Qwen3-VL-Reranker-2B`, `mlx-community/Qwen3-Reranker-0.6B-4bit`, `cross-encoder/ms-marco-MiniLM-L6-v2`, `BAAI/bge-reranker-v2-m3`, `Alibaba-NLP/gte-reranker-modernbert-base`) never route to an embedding variant. All of these gates soft-skip when the checkpoint is absent.
- `mlxcel-server -m all-MiniLM-L6-v2` end to end: the chat worker logs the `/v1/embeddings` rejection and stays unloaded, `/v1/models` lists the checkpoint, `/v1/embeddings` and `/embeddings` answer the structured 501, a malformed body answers 400; `-m all-MiniLM-L6-v2 --embedding-model Qwen3-Embedding-0.6B` fails at startup with the two-embedding-models message; `-m Qwen3-Reranker-0.6B-4bit --embedding-model all-MiniLM-L6-v2` (and the `MLXCEL_EMBEDDING_MODEL` alias) fails at startup naming the flag and the unported family; `mlxcel embed` and `mlxcel generate` on both checkpoints report the expected errors.
- The forward pass is exercised by the test-only stub (mean-pooled one-hot token embeddings): the route tests cover string, list-across-micro-batches, token-id, base64 round-trip, `dimensions`, empty input, 501, model mismatch, usage counting, image rejection and multi-vector output.

## Test plan

- [x] `cargo test --profile test-fast --features cuda --lib -- server::embedding_worker server::routes::embeddings models::detection_tests model_metadata_tests models::tests embeddings:: server::cli_input_tests server::startup_tests` (115 passed)
- [x] `cargo test --profile test-fast --features cuda --lib -- embeddings::real_checkpoint_tests` (8 passed with both checkpoints present)
- [x] `cargo test --profile test-fast --features cuda -p mlxcel-core --lib embedding_mask_tests` (5 passed)
- [x] `cargo test --profile test-fast --features cuda --bin mlxcel -- embed serve_tests` (3 passed)
- [x] `cargo check --profile test-fast --features cuda --all-targets`
- [x] `cargo clippy --profile test-fast --features cuda --lib --bins --tests -- -D warnings`
- [x] `cargo fmt --all -- --check`
- [ ] Workspace test run on the Metal CI runner

Closes lablup#1353
be-free-snowboy pushed a commit to freestylejs/snowbrain-mlxcel that referenced this pull request Oct 27, 2026
PR lablup#1408 (feat(embeddings): pooling, masks, Embedding kind and /v1/embeddings, squash-merged as b6f7785) was merged without its technical report. This PR adds the bilingual report pair `TECHNICAL_REPORTS/1408-embeddings-foundation-20260825.en.md` and `.ko.md`, dated by the PR's merge date, covering the `src/embeddings/` subsystem, the padding-aware mask builders and subfolder safetensors loading in `mlxcel-core`, `ModelKind::Embedding` detection, the embedding worker thread, `POST /v1/embeddings`, the `--embedding-*` server flags, `mlxcel embed`, the verification evidence recorded on the PR, and the follow-up items left for the epic lablup#1348 family ports. No code changes are included.

Refs lablup#1353
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:cli Command-line interface / CLI flags area:core mlxcel-core: MLX FFI, primitives, KV cache, layers area:docs User and developer documentation area:inference Generation, sampling, decoding (incl. speculative, DRY) modeltype:text Text-only language model priority:high High priority status:done Completed type:enhancement New features, capabilities, or significant additions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(embeddings): pooling, bidirectional masks, Embedding model kind and /v1/embeddings

1 participant