<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Zane Neave</title>
    <description>The latest articles on DEV Community by Zane Neave (@xquantize).</description>
    <link>https://dev.to/xquantize</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4172186%2F9e4b50fc-4c8d-4d2a-8771-34b855e06397.jpg</url>
      <title>DEV Community: Zane Neave</title>
      <link>https://dev.to/xquantize</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/xquantize"/>
    <language>en</language>
    <item>
      <title>Sentence embeddings in JAX, after Transformers v5 dropped it</title>
      <dc:creator>Zane Neave</dc:creator>
      <pubDate>Thu, 08 Oct 2026 23:05:00 +0000</pubDate>
      <link>https://dev.to/xquantize/sentence-embeddings-in-jax-after-transformers-v5-dropped-it-137k</link>
      <guid>https://dev.to/xquantize/sentence-embeddings-in-jax-after-transformers-v5-dropped-it-137k</guid>
      <description>&lt;p&gt;&lt;a href="https://huggingface.co/docs/transformers/en/index" rel="noopener noreferrer"&gt;Hugging Face Transformers&lt;/a&gt; v5 removed its TensorFlow and JAX code to focus on PyTorch. If you used FlaxBertModel or FlaxAutoModel to compute sentence embeddings in JAX, those classes are gone.&lt;/p&gt;

&lt;p&gt;This post shows how to compute sentence embeddings in JAX with &lt;a href="https://github.com/xquantize/eqx-zoo" rel="noopener noreferrer"&gt;eqx-zoo&lt;/a&gt;, an open-source library that loads Hugging Face checkpoints as plain &lt;a href="https://github.com/patrick-kidger/equinox" rel="noopener noreferrer"&gt;Equinox &lt;/a&gt; modules. The embeddings match &lt;a href="https://www.sbert.net/" rel="noopener noreferrer"&gt;sentence-transformers&lt;/a&gt; to within float32 rounding, and the post ends with how that's verified.&lt;/p&gt;

&lt;p&gt;We'll cover English and multilingual models, a small semantic search, and &lt;a href="https://huggingface.co/Qwen/Qwen3-Embedding-0.6B" rel="noopener noreferrer"&gt;Qwen3-Embedding&lt;/a&gt;, a modern embedding model built on a language model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;eqx-zoo tokenizers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/xquantize/eqx-zoo" rel="noopener noreferrer"&gt;eqx-zoo&lt;/a&gt; brings in JAX and &lt;a href="https://github.com/patrick-kidger/equinox" rel="noopener noreferrer"&gt;Equinox&lt;/a&gt;; tokenizers is Hugging Face's fast tokenizer library, which we'll use to turn text into token ids. PyTorch isn't needed: &lt;a href="https://github.com/xquantize/eqx-zoo" rel="noopener noreferrer"&gt;eqx-zoo&lt;/a&gt; reads the checkpoint's safetensors files directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your first embeddings
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2" rel="noopener noreferrer"&gt;all-MiniLM-L6-v2&lt;/a&gt; is a small, fast English model that's a common default for semantic search:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;jax&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;jax.numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;jnp&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tokenizers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Tokenizer&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;eqx_zoo&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Encoder&lt;/span&gt;

&lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sentence-transformers/all-MiniLM-L6-v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Tokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enable_padding&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Encoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;sentences&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The cat sits on the mat.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A feline rests on a rug.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stock markets fell today.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sentences&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jnp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jnp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attention_mask&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The embeddings have unit length, so their dot products are cosine similarities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="mf"&gt;0.9999997&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;0.55840427&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.05399836&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.55840427&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0000001&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;0.06003189&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.05399836&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.06003189&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.99999946&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two cat sentences score 0.56 with each other, and about 0.05 with the stock-market one, even though the only word they share is "on".&lt;/p&gt;

&lt;p&gt;Two things to notice, &lt;code&gt;model.embed&lt;/code&gt; works on one sentence, and &lt;code&gt;jax.vmap&lt;/code&gt; maps it over the batch. And &lt;code&gt;mask&lt;/code&gt; marks which tokens are real: the shorter sentences are padded to the longest, and padding is excluded from the embedding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Each checkpoint brings its own recipe
&lt;/h3&gt;

&lt;p&gt;An embedding model is more than its transformer. &lt;a href="https://www.sbert.net/" rel="noopener noreferrer"&gt;sentence-transformers&lt;/a&gt; checkpoints also say how to turn per-token outputs into one vector, in a &lt;code&gt;modules.json&lt;/code&gt; file and a pooling config. &lt;a href="https://github.com/xquantize/eqx-zoo" rel="noopener noreferrer"&gt;eqx-zoo's&lt;/a&gt; &lt;code&gt;from_pretrained&lt;/code&gt; reads those, so embed follows each checkpoint's own recipe:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Checkpoint&lt;/th&gt;
&lt;th&gt;Pooling&lt;/th&gt;
&lt;th&gt;Normalised&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2" rel="noopener noreferrer"&gt;all-MiniLM-L6-v2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Mean over real tokens&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/BAAI/bge-small-en-v1.5" rel="noopener noreferrer"&gt;bge-small-en-v1.5&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The first token, &lt;code&gt;[CLS]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/Qwen/Qwen3-Embedding-0.6B" rel="noopener noreferrer"&gt;Qwen3-Embedding-0.6B&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The last real token&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This matters because the wrong pooling gives embeddings that look plausible but aren't the ones the model was trained to produce. If a checkpoint asks for something &lt;a href="https://github.com/xquantize/eqx-zoo" rel="noopener noreferrer"&gt;eqx-zoo&lt;/a&gt; doesn't implement yet, such as another pooling mode or an extra projection layer, loading fails with a clear &lt;code&gt;NotImplementedError&lt;/code&gt; instead of quietly computing something different.&lt;/p&gt;

&lt;p&gt;You can see what was read with &lt;code&gt;model.pooling&lt;/code&gt; and &lt;code&gt;model.normalize&lt;/code&gt;, and use &lt;code&gt;model(ids, mask)&lt;/code&gt; to get the per-token hidden states instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  A tiny semantic search
&lt;/h3&gt;

&lt;p&gt;Models are ordinary JAX pytrees, so the usual transformations apply. Here's a search over a handful of documents, with the embedding step JIT-compiled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;equinox&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;eqx&lt;/span&gt;

&lt;span class="nd"&gt;@eqx.filter_jit&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;embed_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jnp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jnp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attention_mask&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;embed_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The lighthouse keeper wrote in the logbook every night.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Interest rates rose for the third month in a row.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The ferry to the island leaves at nine.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A new recipe for lemon cake.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;doc_embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;When does the boat depart?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc_embeddings&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;jnp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;argsort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.533  The ferry to the island leaves at nine.
0.207  The lighthouse keeper wrote in the logbook every night.
0.065  A new recipe for lemon cake.
0.052  Interest rates rose for the third month in a row.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ferry timetable comes out on top, though the only word it shares with the query is "the".&lt;/p&gt;

&lt;p&gt;&lt;code&gt;eqx.filter_jit&lt;/code&gt; compiles the function once per input shape. With &lt;code&gt;enable_padding()&lt;/code&gt;, each batch is padded to its own longest sentence, so a new length means a new compilation. For steady throughput, pad to a fixed length instead, for example &lt;code&gt;tokenizer.enable_padding(length=128)&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multilingual embeddings
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://huggingface.co/intfloat/multilingual-e5-base" rel="noopener noreferrer"&gt;multilingual-e5&lt;/a&gt; models cover about 100 languages and use the same &lt;code&gt;Encoder&lt;/code&gt; API. Swapping the repository is the only code change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;intfloat/multilingual-e5-base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Tokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enable_padding&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Encoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;texts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query: Where is the lighthouse?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;passage: Le phare se trouve au bout du port.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;passage: Die Zinsen sind im dritten Monat gestiegen.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tokenize and embed them exactly as before. Against the English query, the French lighthouse passage scores 0.741, and the German one about interest rates 0.670.&lt;/p&gt;

&lt;p&gt;Two details come from the &lt;a href="https://huggingface.co/intfloat/multilingual-e5-base" rel="noopener noreferrer"&gt;model card&lt;/a&gt;, and both matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every input starts with&lt;/strong&gt; &lt;code&gt;query:&lt;/code&gt; &lt;strong&gt;or&lt;/strong&gt; &lt;code&gt;passage:&lt;/code&gt;. That's how the model was trained, and leaving the prefixes out degrades results. For similarity between texts of the same kind, the card recommends &lt;code&gt;query:&lt;/code&gt; for both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scores sit high, mostly around 0.7 to 1.0&lt;/strong&gt;, because of how the model was trained. What matters is the order of the scores, not their absolute values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The base model is an &lt;a href="https://huggingface.co/docs/transformers/en/model_doc/xlm-roberta" rel="noopener noreferrer"&gt;XLM-RoBERTa&lt;/a&gt;, and &lt;a href="https://huggingface.co/intfloat/multilingual-e5-small" rel="noopener noreferrer"&gt;multilingual-e5-small&lt;/a&gt; is a &lt;a href="https://huggingface.co/docs/transformers/en/model_doc/bert" rel="noopener noreferrer"&gt;BERT&lt;/a&gt; with a multilingual vocabulary; &lt;a href="https://github.com/xquantize/eqx-zoo" rel="noopener noreferrer"&gt;eqx-zoo&lt;/a&gt; supports both architectures, so you don't need to know which is which.&lt;/p&gt;

&lt;h3&gt;
  
  
  Qwen3-Embedding: a language model as an embedder
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/Qwen/Qwen3-Embedding-0.6B" rel="noopener noreferrer"&gt;Qwen3-Embedding&lt;/a&gt; works differently from &lt;a href="https://huggingface.co/docs/transformers/en/model_doc/bert" rel="noopener noreferrer"&gt;BERT-style models&lt;/a&gt;. It's a decoder, like a chat model, with causal attention: each token only sees the tokens before it. So the embedding is the hidden state of the &lt;strong&gt;last&lt;/strong&gt; token, the only one that has seen the whole input. &lt;a href="https://github.com/xquantize/eqx-zoo" rel="noopener noreferrer"&gt;eqx-zoo&lt;/a&gt; loads it with &lt;code&gt;DecoderEmbedder&lt;/code&gt;, which has the same &lt;code&gt;embed&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;eqx_zoo&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DecoderEmbedder&lt;/span&gt;

&lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Qwen/Qwen3-Embedding-0.6B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Tokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enable_padding&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DecoderEmbedder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Instruct: Given a web search query, retrieve relevant passages that answer the query&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Query:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;texts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What is the capital of France?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Paris is the capital of France.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cats sleep a lot.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jnp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jnp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attention_mask&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;passage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unrelated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;passage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;unrelated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 0.736 0.117
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As with e5, there's a convention to follow: &lt;strong&gt;queries get an instruction prompt, and documents don't&lt;/strong&gt;. The prompt comes from the checkpoint's &lt;a href="https://www.sbert.net/" rel="noopener noreferrer"&gt;sentence-transformers&lt;/a&gt; configuration, and you can change the task description to suit your search.&lt;/p&gt;

&lt;p&gt;The tokenizer also appends an end-of-text token to every input, and that's the token whose hidden state becomes the embedding. The standalone &lt;code&gt;tokenizers&lt;/code&gt; library adds it automatically, just as &lt;a href="https://www.sbert.net/" rel="noopener noreferrer"&gt;sentence-transformers&lt;/a&gt; does.&lt;/p&gt;

&lt;h3&gt;
  
  
  How we know it's right
&lt;/h3&gt;

&lt;p&gt;A port that loads and runs isn't necessarily correct: a missing bias or the wrong pooling still produces vectors that look reasonable. So every supported checkpoint is checked against the reference implementations on every pull request:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Layer by layer&lt;/strong&gt;: each layer's output matches &lt;a href="https://huggingface.co/docs/transformers/en/index" rel="noopener noreferrer"&gt;Hugging Face Transformers&lt;/a&gt; in float32.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;End to end&lt;/strong&gt;: embeddings match &lt;a href="https://www.sbert.net/" rel="noopener noreferrer"&gt;sentence-transformers&lt;/a&gt; itself, on a padded batch of sentences of different lengths. For the examples in this post, the largest difference is 1.7e-7 for &lt;a href="https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2" rel="noopener noreferrer"&gt;all-MiniLM-L6-v2&lt;/a&gt;, and 4.7e-7 for &lt;a href="https://huggingface.co/Qwen/Qwen3-Embedding-0.6B" rel="noopener noreferrer"&gt;Qwen3-Embedding&lt;/a&gt; with its query prompt, which is float32 rounding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In bfloat16&lt;/strong&gt;: the error against float32 must be within 2x the reference library's own bf16 error. Measured ratios are 0.84 to 1.51 for encoders and up to 1.38 for &lt;a href="https://huggingface.co/Qwen/Qwen3-Embedding-0.6B" rel="noopener noreferrer"&gt;Qwen3-Embedding&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block by block&lt;/strong&gt;: precision-sensitive pieces such as LayerNorm are tested directly in bf16, because a subtle bug there barely moves a whole model's output but is off by thousands of rounding steps at the block level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiny random models&lt;/strong&gt;: small randomly initialised versions of each architecture cover code paths no single checkpoint exercises, and run in seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The verified checkpoints so far are &lt;a href="https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2" rel="noopener noreferrer"&gt;all-MiniLM-L6-v2&lt;/a&gt;, &lt;a href="https://huggingface.co/BAAI/bge-small-en-v1.5" rel="noopener noreferrer"&gt;bge-small-en-v1.5&lt;/a&gt;, &lt;a href="https://huggingface.co/intfloat/multilingual-e5-small" rel="noopener noreferrer"&gt;multilingual-e5-small&lt;/a&gt;, &lt;a href="https://huggingface.co/intfloat/multilingual-e5-base" rel="noopener noreferrer"&gt;multilingual-e5-base&lt;/a&gt; and &lt;a href="https://huggingface.co/Qwen/Qwen3-Embedding-0.6B" rel="noopener noreferrer"&gt;Qwen3-Embedding-0.6B&lt;/a&gt;. They're collected on the Hugging Face Hub in Verified in &lt;a href="https://github.com/xquantize/eqx-zoo" rel="noopener noreferrer"&gt;eqx-zoo&lt;/a&gt;. Other checkpoints with the same architectures (&lt;a href="https://huggingface.co/docs/transformers/en/model_doc/bert" rel="noopener noreferrer"&gt;BERT&lt;/a&gt;, &lt;a href="https://huggingface.co/docs/transformers/en/model_doc/roberta" rel="noopener noreferrer"&gt;RoBERTa&lt;/a&gt;, &lt;a href="https://huggingface.co/docs/transformers/en/model_doc/xlm-roberta" rel="noopener noreferrer"&gt;XLM-RoBERTa&lt;/a&gt;, &lt;a href="https://huggingface.co/collections/Qwen/qwen3" rel="noopener noreferrer"&gt;Qwen3&lt;/a&gt;) load the same way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limitations, and trying it
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/xquantize/eqx-zoo" rel="noopener noreferrer"&gt;eqx-zoo&lt;/a&gt; is a young project, so a few honest caveats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The test suite runs on CPU&lt;/strong&gt;. JAX runs the same code on GPUs and TPUs, but the numerical checks haven't been run there systematically yet. GPU and TPU reports are very welcome, and the repository has an issue template for them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedding models are limited to the architectures above&lt;/strong&gt;. MPNet-based models such as all-mpnet-base-v2, and checkpoints that need custom code, aren't supported yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokenization is up to you&lt;/strong&gt;. &lt;a href="https://github.com/xquantize/eqx-zoo" rel="noopener noreferrer"&gt;eqx-zoo&lt;/a&gt; takes token ids, so you choose how to tokenize, truncate and pad. The tokenizers library, as used here, matches what &lt;a href="https://www.sbert.net/" rel="noopener noreferrer"&gt;sentence-transformers&lt;/a&gt; does.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To try it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;eqx-zoo tokenizers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code is on GitHub at &lt;a href="https://github.com/xquantize/eqx-zoo" rel="noopener noreferrer"&gt;xquantize/eqx-zoo&lt;/a&gt;, along with &lt;a href="https://huggingface.co/docs/transformers/en/model_doc/llama" rel="noopener noreferrer"&gt;Llama&lt;/a&gt;, &lt;a href="https://huggingface.co/Qwen" rel="noopener noreferrer"&gt;Qwen&lt;/a&gt; and &lt;a href="https://huggingface.co/docs/transformers/en/model_doc/qwen3_moe" rel="noopener noreferrer"&gt;Qwen3-MoE&lt;/a&gt; language models verified the same way. Issues and pull requests are welcome, from bug reports to new checkpoints.&lt;/p&gt;

&lt;p&gt;Which embedding model would you most like to use from JAX? Let me know in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tutorial</category>
      <category>opensource</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
