Skip to content

feat: LFM2.5 text-embedding & ColBERT (MLX/XNNPACK) with prompts and multi-vector output#1269

Draft
NorbertKlockiewicz wants to merge 13 commits into
mainfrom
@nk/lfm-embedding-mlx-xnnpack
Draft

feat: LFM2.5 text-embedding & ColBERT (MLX/XNNPACK) with prompts and multi-vector output#1269
NorbertKlockiewicz wants to merge 13 commits into
mainfrom
@nk/lfm-embedding-mlx-xnnpack

Conversation

@NorbertKlockiewicz

@NorbertKlockiewicz NorbertKlockiewicz commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Description

Adds two LFM2.5 retrieval models from Liquid AI and the API needed to use them, through the existing useTextEmbeddings hook — one native runner, one hook, no new public surface beyond optional model-config fields:

  • LFM2.5-Embedding-350M — dense bi-encoder (CLS pooling, dim 1024). Trained with asymmetric query: /document: prompts.
  • LFM2.5-ColBERT-350M — late-interaction retriever (Linear(1024→128) per token). Trained with [Q] /[D] prompts.

Both run on MLX on iOS (physical device) and XNNPACK on Android, quantized (MLX int4, XNNPACK 8da4w).

To support them without breaking the existing API, the model config grew three optional fields and forward became config-driven:

  • prompts — when present, forward requires a role ('query' | 'document') and auto-prepends the matching prompt.
  • multiVector — when true, forward returns a per-token EmbeddingResult (vectors, numTokens, embeddingDim, tokenIds); otherwise it returns a single pooled Float32Array as before.
  • skipListIds — punctuation token ids the consumer excludes from MaxSim scoring.

The library auto-applies the role prompts (the matching query: /[Q] prefix is prepended in forward), but late-interaction scoring (MaxSim) stays the consumer's concern — it runs wherever the vectors are stored. The example app demonstrates one way to score (its own local maxSim), and the ColBERT demo is folded into the unified text-embeddings screen, picking the scorer from the model's config.

Native side: TextEmbeddings::generate returns the raw [numTokens, embeddingDim] matrix as an EmbeddingResult; the TS layer reduces it. The empty BaseEmbeddings base class was removed (TextEmbeddings now extends BaseModel directly), and output-shape validation was extracted into TextEmbeddings::buildResult.

Review order: start with the TS types (types/textEmbeddings.tsForwardFn/ForwardReturn discriminated on the model config), then the module/hook (TextEmbeddingsModule.ts, useTextEmbeddings.ts), then the native TextEmbeddings.cpp/Types.h, then the registry/URLs and the example screen.

Introduces a breaking change?

  • Yes
  • No

forward stays non-breaking: pooled models still return Float32Array. The new return type and role requirement only apply to models that opt in via config.

Type of change

  • Bug fix (latent: existing models now add CLS/SEP special tokens — see Additional notes)
  • New feature (change which adds functionality)
  • Documentation update (improves or adds clarity to existing documentation)
  • Other (chores, tests, code style improvements etc.)

Tested on

  • iOS
  • Android

Testing instructions

  1. Open the text-embeddings example app.
  2. Pick LFM2.5 Embedding (MLX on a physical iOS device, XNNPACK on Android/simulator) and run the example queries — weather → "sunny", match → home-team sentences should rank top.
  3. Pick LFM2.5 ColBERT (late-interaction) — same corpus, scored with MaxSim; ordering should match.
  4. Existing pooled models (MiniLM, MPNet, …) keep working unchanged.

C++ unit tests: TextEmbeddingsTests (incl. new EmbeddingResult metadata / tokenIds assertions) compiles and links under the Android NDK toolchain. The suite is cross-compiled, so it is not executed on the host in this setup.

Related issues

Checklist

  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have updated the documentation accordingly
  • My changes generate no new warnings

Additional notes

MLX requires a physical iOS device — the MLX delegate does not run on the simulator (use XNNPACK there). The two models are hosted on the Software Mansion Hugging Face org; docs are updated for both next and the 0.9.x versioned set.

…xSim

Add the LFM2.5-Embedding-350M and LFM2.5-ColBERT-350M models, served from
HuggingFace (MLX on iOS, XNNPACK on Android / iOS simulator).

Text embeddings are unified into one runner and one hook: the native
TextEmbeddings model returns the raw [numTokens, embeddingDim] matrix
(numTokens === 1 for pooled models, the full sequence for multi-vector /
late-interaction models like ColBERT), plus the input token ids. The TS
layer reduces it — toVector() for the single-vector case, getTokenVectors()
and maxSim() for late interaction.

Models trained with asymmetric query/document prompts (LFM uses query:/
document:, ColBERT uses [Q] /[D] ) carry a "prompts" config; forward then
requires a role argument ('query' | 'document') that auto-prepends the
prompt. The role is type-enforced: required for prompted models, forbidden
for plain ones.

Also: tokenizer post_processor is now applied for text embeddings so the
BOS special token is added (CLS-pooled models depend on it), and the
text-to-image Encoder reads the new EmbeddingResult.

Example app gains a semantic-search screen and a ColBERT late-interaction
search screen demonstrating MaxSim.

Authored with Claude.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@NorbertKlockiewicz NorbertKlockiewicz force-pushed the @nk/lfm-embedding-mlx-xnnpack branch from b1f5bdd to 50e80e1 Compare June 22, 2026 10:46
NorbertKlockiewicz and others added 9 commits June 22, 2026 14:34
- Migrate the segment-anything (SAM) screen to toVector(forward()) — its
  CLIP-text path broke when forward started returning EmbeddingResult.
- Update the C++ TextEmbeddings integration test for the EmbeddingResult
  return type (was still using the old OwningArrayBuffer pointer API).
- Guard the per-token invariant: throw InvalidModelOutput if output rows
  != input token count (pooled numTokens==1 exempt), so skiplist masking
  can't silently misalign if a graph pads/truncates.
- Dedup encode()/encodeWithSpecialTokens() into a shared encodeImpl.
- Drop the redundant Float32Array copy at the JSI boundary; document the
  getTokenVectors view lifetime; remove dead BaseEmbeddings::postprocess.

Authored with Claude.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
forward(text) returns a single pooled Float32Array again for standard
models — restoring the original API, so MiniLM/MPNet/CLIP/SAM consumers
need no migration. The reduction (row 0 of the native [numTokens,
embeddingDim] matrix) happens in the TS module, not at the call site.

Multi-vector (late-interaction) models opt in via a `multiVector: true`
config flag; for those, forward returns the full per-token EmbeddingResult
so MaxSim/skiplist work. Return type is discriminated by the flag, and the
role argument by `prompts` (required when prompted, none when not).

Authored with Claude.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ents

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ments

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@NorbertKlockiewicz NorbertKlockiewicz changed the title @nk/lfm embedding mlx xnnpack feat: LFM2.5 text-embedding & ColBERT (MLX/XNNPACK) with prompts and multi-vector output Jun 23, 2026
@NorbertKlockiewicz NorbertKlockiewicz self-assigned this Jun 23, 2026
@NorbertKlockiewicz NorbertKlockiewicz added the model Issues related to exporting, improving, fixing ML models label Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

model Issues related to exporting, improving, fixing ML models

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant