Skip to content

feat(server): align b10621 embedding and reranking mode and pooling - #1483

Merged
inureyes merged 1 commit into
mainfrom
feature/issue-1452-embedding-rerank-mode
Aug 28, 2026
Merged

inureyes merged 1 commit into
mainfrom
feature/issue-1452-embedding-rerank-mode

Conversation

@inureyes

Copy link
Copy Markdown
Member

Summary

Lands issue #1452's embedding and reranking surface: the four b10621 mode and shaping options, the two rerank response envelopes, the aligned request errors, and a resolved-capability report on /props and /v1/models. Six of the ten manifest entries this issue owns flip to supported; the four option entries stay deferred with their divergences recorded, so this PR does not close the issue.

What changed

  • src/cli/embedding_compat_args.rs (new): --embedding / --embeddings, --rerank / --reranking, --pooling and --embd-normalize, defined once and flattened into both server binaries. LLAMA_ARG_EMBEDDINGS and LLAMA_ARG_RERANKING are read through b10621's own truthy set (on, enabled, true, 1) rather than clap's boolish parser, which both accepts more and errors outside its vocabulary; LLAMA_ARG_POOLING binds through clap because it takes a value.
  • src/embeddings/normalize.rs (new): b10621's whole --embd-normalize domain with upstream's arithmetic, including its norm = sum > 0 ? 1/sum : 0 rule so a zero vector normalizes to zeros rather than to a NaN the route would refuse with a 500. 2 delegates to the existing L2 kernel, so the default path is numerically unchanged.
  • src/embeddings/pooling.rs: a process-wide override cell for --pooling, installed at startup before any checkpoint loads, because every family resolves its mode inside its own constructor and carries no server context. Precedence is flag, then MLXCEL_EMBEDDING_POOLING, then 1_Pooling/config.json, then the family default.
  • src/embeddings/engine.rs: EmbedOptions carries the normalization, and post-processing applies it (and re-applies it after a dimensions truncation, which breaks whatever invariant the first pass established).
  • src/embeddings/model.rs and nine families: a pooling() accessor returning the mode that was really resolved, so /props reports what happened rather than the family default.
  • src/server/config.rs: EmbeddingServingMode, the restriction half of the two mode flags, and the resolved embd_normalize.
  • src/server/startup.rs: installs the pooling override, then refuses a mode flag that resolved no worker able to serve it.
  • src/server/routes/mod.rs: generation routes answer 501 naming the mode flag, even when a chat model is loaded next to the side model.
  • src/server/types/rerank.rs, routes/rerank.rs: object: "list" on the Jina envelope, b10621's TEI array for a texts request, return_text / text, and upstream's wording for the three shape errors. A body carrying both list spellings drops the redundant one and stays on the Jina envelope, because serde would otherwise reject it as a duplicate field where upstream reads one and ignores the other.
  • src/server/routes/embeddings.rs: the per-request embd_normalize, and "input" or "content" must be provided for a body carrying neither.
  • src/server/routes/props.rs, models.rs: the resolved capability block and the per-entry capabilities array.
  • src/commands/embed.rs: --pooling and --embd-normalize on the offline path too, so it resolves identically to the server it documents itself against.
  • src/server/routes/health.rs: a server in one of the two modes reports on the worker the mode selected. It has no chat worker to be "loaded", so /health answered 503 {"status": "loading model"} for the life of the process, which would have made the flag unusable behind a container probe. Scoped to the flags: a server whose -m is an embedding checkpoint but that was started without them is unchanged, so GET /health's recorded divergences and its manifest entry stay feat(server): align props, slots, metrics, health, and slot persistence #1440's.

Why the four option entries are still deferred

Each is implemented and tested; each has one observable difference recorded on its manifest entry.

  • --embedding and --rerank: mlxcel additionally requires a checkpoint that can serve the mode, and fails at startup when none resolves. b10621 restricts whatever -m loaded and embeds or scores with it, and mlxcel has no path that produces embeddings from a causal chat checkpoint. --reranking also leaves /v1/embeddings answering upstream, where a mlxcel server in rerank mode has no embedding worker.
  • --pooling: none is refused rather than served, because every embedding family pools inside its forward pass and there is no unpooled hidden state left to return; rank selects the reranking worker rather than becoming a pooling type.
  • --embd-normalize: with the flag unset mlxcel follows the checkpoint's own normalize flag, which is euclidean for every checkpoint that does not set it and none for one that sets it to false. b10621 always defaults to 2, because GGUF carries no such metadata.

One observation worth recording

bge-small-en-v1.5 returns the same vector for --pooling cls and --pooling last, to 1.3e-5 relative. That is the checkpoint, not the pooling: all-MiniLM-L6-v2 separates the two by 0.90 through the identical code path, the pool() kernel's last-token index is unit-tested against right padding, left padding and an all-padding row, and the coincidence reproduces on two unrelated prompts and through the offline mlxcel embed path. A BERT trained with CLS pooling collapses its [SEP] representation onto [CLS]. The real-checkpoint test asserts mean against each single-position mode on both checkpoints, and cls against last only where the two positions are known to differ, so it still fails if the override stops reaching the forward pass.

Test plan

  • cargo test --profile test-fast --features metal,accelerate --lib -- embeddings::normalize embeddings::pooling cli::embedding_compat server::routes::embedding_rerank_mode server::routes::embeddings server::routes::rerank server::routes::props server::routes::models server::routes::health server::startup server::llama_compat: 185 passed.
  • cargo test --test embedding_pooling_normalize_real --profile test-fast --features metal,accelerate: every pooling and normalization path over all-MiniLM-L6-v2 (mean pooling), bge-small-en-v1.5 (cls pooling) and the ms-marco-MiniLM-L6-v2 cross-encoder.
  • python3 scripts/ci/check_llama_compat_manifest.py and bash scripts/ci/check_llama_compat_manifest_test.sh
  • cargo fmt --all -- --check and cargo clippy --lib --tests --features metal,accelerate -- -D warnings
  • Live server, mlxcel-server against the two real checkpoints:
    • --embeddings --pooling cls --embd-normalize 1 over all-MiniLM-L6-v2: /props reported {"generation": false, "serving_mode": "--embeddings", "embedding": {"pooling": "cls", "embd_normalize": 1, ...}}, /v1/models labelled the entry ["embedding"], both embedding shapes served, and /v1/chat/completions and /completion answered 501 naming the flag.
    • --reranking over ms-marco-MiniLM-L6-v2: documents answered {"model", "object": "list", "results": [{"index", "relevance_score", "document"}], "usage"} and texts answered [{"index", "score", "text"}] on the same query, with the panda passage first at 0.9985 against 0.0000149 for the distractor; generation and /v1/embeddings answered 501.
    • --pooling rank selected the same reranking mode.
    • --pooling none, --embd-normalize -2, --embeddings on a reranker checkpoint and --reranking on an embedding checkpoint each failed at startup with their diagnostic.
    • LLAMA_ARG_EMBEDDINGS=1 LLAMA_ARG_POOLING=mean alone started the server and logged the pooling override, so the runtime environment bridge works without any command line.

Refs #1452

b10621 controls its embedding and reranking surface with four options mlxcel accepted none of, and answers two rerank envelopes where mlxcel answered one. This lands the option surface, the envelopes and the resolved-capability report of issue #1452.

`--embedding` / `--embeddings` and `--rerank` / `--reranking` are accepted on both server binaries, with `LLAMA_ARG_EMBEDDINGS` and `LLAMA_ARG_RERANKING` read through b10621's own truthy set rather than clap's wider boolish parser. They do the two things upstream's do: generation routes answer 501 naming the flag, and the matching side-model routes serve. They also do one thing upstream never needs, because mlxcel runs a dedicated worker rather than reusing one set of weights: a mode that resolves no worker fails at startup naming the flag, instead of booting a server whose only route answers 501 forever.

`--pooling` maps `mean`, `cls` and `last` onto mlxcel's kernels and outranks both the checkpoint's `1_Pooling/config.json` and `MLXCEL_EMBEDDING_POOLING`. The other two values are not pooling kernels: `rank` is how b10621 selects its reranking path, so it is a synonym for `--reranking`, and `none` asks for one vector per token, which no mlxcel embedding family can return because each pools inside its own forward pass. `none` is refused at startup with that reason.

`--embd-normalize` implements b10621's whole domain in `src/embeddings/normalize.rs`: `-1` none, `0` the max-absolute rescale into the signed int16 range, `1` taxicab, `2` euclidean, and any value above 2 that p-norm. Upstream's `norm = sum > 0 ? 1/sum : 0` rule is reproduced, so a zero vector normalizes to zeros rather than to a NaN the route would then refuse with a 500. `2` delegates to the L2 kernel every embedding response already used, so the default path returns exactly the numbers it returned before. The value is readable per request as `embd_normalize`, as upstream reads it, and `mlxcel embed` takes the same two flags so the offline and server surfaces resolve identically.

The rerank envelope now follows the request. A `documents` list answers the Jina shape with b10621's top-level `"object": "list"`, which mlxcel omitted; a `texts` list answers b10621's TEI shape instead, a bare array of `{index, score}` with `text` echoed under `return_text`. The Cohere `return_documents` / `document` spelling keeps working, a body carrying both list spellings stays on the Jina envelope, and the shape errors on both families use upstream's own wording.

`/props` gains a `capabilities` block reporting what was resolved rather than what was passed: whether generation is on, which mode flag restricted it, and each side model's id plus the pooling and `embd_normalize` really in force. Reporting the resolved pooling needed a new `EmbeddingModel::pooling` accessor, overridden by the nine families that store it. `/v1/models` labels each entry with `completion`, `embedding` or `rerank`, omitted when empty so a plain generation deployment keeps the OpenAI object shape.

A server in one of the two modes also reports healthy once its worker is up. It has no chat worker to be "loaded", so `/health` answered 503 for the life of the process, which would have made the flag unusable behind a container probe. The change is scoped to the flags, so a server whose `-m` is an embedding checkpoint but that was started without them is unchanged and `GET /health`'s recorded divergences stay #1440's.

Validation: 185 unit and route tests, plus `tests/embedding_pooling_normalize_real.rs` over `all-MiniLM-L6-v2` (mean), `bge-small-en-v1.5` (cls) and the `ms-marco-MiniLM-L6-v2` cross-encoder, and a live server across both modes. Six manifest entries flip to `supported`; the four option entries stay `deferred` with their divergences recorded, so #1452 stays open and owns them.

Refs #1452
@inureyes inureyes added status:review Under review type:bug Bug fixes, error corrections, or issue resolutions priority:high High priority area:models Model architectures, weights, loading, metadata area:inference Generation, sampling, decoding (incl. speculative, DRY) labels Aug 28, 2026
@inureyes
inureyes merged commit 7c9cb65 into main Aug 28, 2026
12 checks passed
@inureyes inureyes self-assigned this Aug 31, 2026
@inureyes inureyes added status:done Completed and removed status:review Under review labels Sep 6, 2026
@inureyes
inureyes deleted the feature/issue-1452-embedding-rerank-mode branch October 6, 2026 12:12
be-free-snowboy pushed a commit to freestylejs/snowbrain-mlxcel that referenced this pull request Oct 27, 2026
…ablup#1483)

## Summary

Lands issue lablup#1452's embedding and reranking surface: the four b10621 mode and shaping options, the two rerank response envelopes, the aligned request errors, and a resolved-capability report on `/props` and `/v1/models`. Six of the ten manifest entries this issue owns flip to `supported`; the four option entries stay `deferred` with their divergences recorded, so this PR does not close the issue.

## What changed

- `src/cli/embedding_compat_args.rs` (new): `--embedding` / `--embeddings`, `--rerank` / `--reranking`, `--pooling` and `--embd-normalize`, defined once and flattened into both server binaries. `LLAMA_ARG_EMBEDDINGS` and `LLAMA_ARG_RERANKING` are read through b10621's own truthy set (`on`, `enabled`, `true`, `1`) rather than clap's boolish parser, which both accepts more and errors outside its vocabulary; `LLAMA_ARG_POOLING` binds through clap because it takes a value.
- `src/embeddings/normalize.rs` (new): b10621's whole `--embd-normalize` domain with upstream's arithmetic, including its `norm = sum > 0 ? 1/sum : 0` rule so a zero vector normalizes to zeros rather than to a NaN the route would refuse with a 500. `2` delegates to the existing L2 kernel, so the default path is numerically unchanged.
- `src/embeddings/pooling.rs`: a process-wide override cell for `--pooling`, installed at startup before any checkpoint loads, because every family resolves its mode inside its own constructor and carries no server context. Precedence is flag, then `MLXCEL_EMBEDDING_POOLING`, then `1_Pooling/config.json`, then the family default.
- `src/embeddings/engine.rs`: `EmbedOptions` carries the normalization, and post-processing applies it (and re-applies it after a `dimensions` truncation, which breaks whatever invariant the first pass established).
- `src/embeddings/model.rs` and nine families: a `pooling()` accessor returning the mode that was really resolved, so `/props` reports what happened rather than the family default.
- `src/server/config.rs`: `EmbeddingServingMode`, the restriction half of the two mode flags, and the resolved `embd_normalize`.
- `src/server/startup.rs`: installs the pooling override, then refuses a mode flag that resolved no worker able to serve it.
- `src/server/routes/mod.rs`: generation routes answer 501 naming the mode flag, even when a chat model is loaded next to the side model.
- `src/server/types/rerank.rs`, `routes/rerank.rs`: `object: "list"` on the Jina envelope, b10621's TEI array for a `texts` request, `return_text` / `text`, and upstream's wording for the three shape errors. A body carrying both list spellings drops the redundant one and stays on the Jina envelope, because serde would otherwise reject it as a duplicate field where upstream reads one and ignores the other.
- `src/server/routes/embeddings.rs`: the per-request `embd_normalize`, and `"input" or "content" must be provided` for a body carrying neither.
- `src/server/routes/props.rs`, `models.rs`: the resolved capability block and the per-entry `capabilities` array.
- `src/commands/embed.rs`: `--pooling` and `--embd-normalize` on the offline path too, so it resolves identically to the server it documents itself against.
- `src/server/routes/health.rs`: a server in one of the two modes reports on the worker the mode selected. It has no chat worker to be "loaded", so `/health` answered `503 {"status": "loading model"}` for the life of the process, which would have made the flag unusable behind a container probe. Scoped to the flags: a server whose `-m` is an embedding checkpoint but that was started without them is unchanged, so `GET /health`'s recorded divergences and its manifest entry stay lablup#1440's.

## Why the four option entries are still `deferred`

Each is implemented and tested; each has one observable difference recorded on its manifest entry.

- `--embedding` and `--rerank`: mlxcel additionally requires a checkpoint that can serve the mode, and fails at startup when none resolves. b10621 restricts whatever `-m` loaded and embeds or scores with it, and mlxcel has no path that produces embeddings from a causal chat checkpoint. `--reranking` also leaves `/v1/embeddings` answering upstream, where a mlxcel server in rerank mode has no embedding worker.
- `--pooling`: `none` is refused rather than served, because every embedding family pools inside its forward pass and there is no unpooled hidden state left to return; `rank` selects the reranking worker rather than becoming a pooling type.
- `--embd-normalize`: with the flag unset mlxcel follows the checkpoint's own `normalize` flag, which is euclidean for every checkpoint that does not set it and none for one that sets it to `false`. b10621 always defaults to `2`, because GGUF carries no such metadata.

## One observation worth recording

`bge-small-en-v1.5` returns the same vector for `--pooling cls` and `--pooling last`, to 1.3e-5 relative. That is the checkpoint, not the pooling: `all-MiniLM-L6-v2` separates the two by 0.90 through the identical code path, the `pool()` kernel's last-token index is unit-tested against right padding, left padding and an all-padding row, and the coincidence reproduces on two unrelated prompts and through the offline `mlxcel embed` path. A BERT trained with CLS pooling collapses its `[SEP]` representation onto `[CLS]`. The real-checkpoint test asserts `mean` against each single-position mode on both checkpoints, and `cls` against `last` only where the two positions are known to differ, so it still fails if the override stops reaching the forward pass.

## Test plan

- [x] `cargo test --profile test-fast --features metal,accelerate --lib -- embeddings::normalize embeddings::pooling cli::embedding_compat server::routes::embedding_rerank_mode server::routes::embeddings server::routes::rerank server::routes::props server::routes::models server::routes::health server::startup server::llama_compat`: 185 passed.
- [x] `cargo test --test embedding_pooling_normalize_real --profile test-fast --features metal,accelerate`: every pooling and normalization path over `all-MiniLM-L6-v2` (mean pooling), `bge-small-en-v1.5` (cls pooling) and the `ms-marco-MiniLM-L6-v2` cross-encoder.
- [x] `python3 scripts/ci/check_llama_compat_manifest.py` and `bash scripts/ci/check_llama_compat_manifest_test.sh`
- [x] `cargo fmt --all -- --check` and `cargo clippy --lib --tests --features metal,accelerate -- -D warnings`
- [x] Live server, `mlxcel-server` against the two real checkpoints:
  - `--embeddings --pooling cls --embd-normalize 1` over `all-MiniLM-L6-v2`: `/props` reported `{"generation": false, "serving_mode": "--embeddings", "embedding": {"pooling": "cls", "embd_normalize": 1, ...}}`, `/v1/models` labelled the entry `["embedding"]`, both embedding shapes served, and `/v1/chat/completions` and `/completion` answered 501 naming the flag.
  - `--reranking` over `ms-marco-MiniLM-L6-v2`: `documents` answered `{"model", "object": "list", "results": [{"index", "relevance_score", "document"}], "usage"}` and `texts` answered `[{"index", "score", "text"}]` on the same query, with the panda passage first at 0.9985 against 0.0000149 for the distractor; generation and `/v1/embeddings` answered 501.
  - `--pooling rank` selected the same reranking mode.
  - `--pooling none`, `--embd-normalize -2`, `--embeddings` on a reranker checkpoint and `--reranking` on an embedding checkpoint each failed at startup with their diagnostic.
  - `LLAMA_ARG_EMBEDDINGS=1 LLAMA_ARG_POOLING=mean` alone started the server and logged the pooling override, so the runtime environment bridge works without any command line.

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

Labels

area:inference Generation, sampling, decoding (incl. speculative, DRY) area:models Model architectures, weights, loading, metadata priority:high High priority status:done Completed type:bug Bug fixes, error corrections, or issue resolutions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant