Repository navigation
feat(embeddings): pooling, masks, Embedding kind and /v1/embeddings - #1408
Merged
Merged
Conversation
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
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
9 tasks done
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the shared embedding foundation from #1353 (epic #1348): the padding-aware attention-mask builders, the
src/embeddings/module with theEmbeddingModeltrait, pooling, normalization, length limits and batch tokenization,ModelKind::Embeddingwith layout-based detection, subfolder safetensors loading, the dedicated embedding worker thread,POST /v1/embeddings, the server flags,mlxcel embedanddocs/embeddings.md. No family forward pass lands here; every recognized family reportsnot yet supportedfrom 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(f320 / -infbuilt withwhere_cond, fully blocked rows keep their diagonal,// Used by:comments) and thearray_to_vec_f32readback helper; tests inutils_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_subfoldersreads the top-level shards and prefixes every<dir>/*/ *.safetensorstensor with<folder>., skippingconsolidated.safetensorsandadapter_model.safetensors.src/embeddings/:model.rs(trait,EmbeddingBatch,EmbeddingOutput,ImageInput),pooling.rs(cls/mean/max/lasttoken,PoolingConfig::readfor both key styles with load errors forweightedmean,mean_sqrt_len_tokens,include_prompt: falseand combined flags,resolve_pooling_modewith theMLXCEL_EMBEDDING_POOLINGoverride,normalize_l2,truncate_dimensions),limits.rs(max_lengthderivation per the issue rule, pad token, vocab size),tokenize.rs(right-paddedencode_batch/encode_pairs, truncation that keeps a trailing special token,strip_padding_and_truncationfortokenizer.jsonfiles that bake in a fixed pad),loader.rs(load_embedding_model, the per-family dispatcher,load_embedding_weightswith the text bf16 rule,quantization_params),engine.rs(length-sorted micro-batching, normalization,dimensions, multi-vector rows, readback) and the test-onlystub.rs.src/model_metadata.rs,src/models/mod.rs,src/models/detection.rs,src/loading/mod.rs:ModelKind::Embedding, thirteenModelTypevariants under theEmbeddingfamily inmlxcel arch,is_embedding_checkpoint(encoder-onlymodel_type, embeddingarchitectures[0]including the flag-gatedGemma3TextModelandMinistral3Model,modules.jsonPooling entry,1_Pooling/config.json;ForSequenceClassificationand a LogitScore-onlymodules.jsonnever qualify; a pooling layout on an unmappedmodel_typeis an error rather than a misroute), and theload_modelbail pointing at/v1/embeddings.src/server/embedding_model.rs,src/server/embedding_worker.rs:EmbeddingModelProvider,EmbeddingError { QueueFull, Timeout, InvalidInput, Internal }, andEmbeddingWorkerwith the audio worker's boundedSyncSender, reply timeout andcatch_unwindboundary; 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/embeddingsand/embeddingswith string, list, token-id, token-id list and typed-part inputs,encoding_format: base64(little-endian f32,shapesibling for multi-vector output),dimensions,instruction,usage.prompt_tokens, the400 / 501 / 503 / 504mapping, image decoding through the shared media limits, theembedding_modelslot and the extra/v1/modelsentry.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-modelload 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 asBertviamodel_type: bertand1_Pooling/config.json; pooling config parses tomean;max_lengthderives to 256 fromsentence_bert_config.json;load_weights_from_dir_with_subfoldersreads the top-level shard (embeddings.word_embeddings.weight[30522, 384]) and prefixes a synthetic2_Dense/model.safetensorsas2_Dense.linear.weight; the batch tokenizer right-pads with[PAD](id 0), keeps[CLS]/[SEP], is no longer padded to the fixed 128 baked intotokenizer.json, and keeps[SEP]under truncation;load_modelbails with the/v1/embeddingsmessage.Qwen/Qwen3-Embedding-0.6B: detected asQwen3Embeddingviamodules.json/1_Poolingwithpooling_mode_lasttoken;max_lengthderives to the 8192 cap; pad token resolves to<|endoftext|>(151643); every encoded row ends with<|endoftext|>and truncation keeps it;load_modelbails.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-v2end to end: the chat worker logs the/v1/embeddingsrejection and stays unloaded,/v1/modelslists the checkpoint,/v1/embeddingsand/embeddingsanswer the structured 501, a malformed body answers 400;-m all-MiniLM-L6-v2 --embedding-model Qwen3-Embedding-0.6Bfails at startup with the two-embedding-models message;-m Qwen3-Reranker-0.6B-4bit --embedding-model all-MiniLM-L6-v2(and theMLXCEL_EMBEDDING_MODELalias) fails at startup naming the flag and the unported family;mlxcel embedandmlxcel generateon both checkpoints report the expected errors.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-targetscargo clippy --profile test-fast --features cuda --lib --bins --tests -- -D warningscargo fmt --all -- --checkCloses #1353