ThinEmbed: compressed query encoders
The query side of an embedding model, compressed into a small file that runs in the browser on WebGPU. The document index stays exactly as it is; only the query encoder changes. Every number below compares a compressed client with the full-precision model on the same queries, against the model's own fp32 index.
Size, quality and speed in the browser
harrier-0.6b on SciFact, test split, 300 queries; nDCG@10 against the model's own fp32 index. One idle laptop, Intel integrated GPU, headless Chrome; latency per query; peak memory of all Chrome processes. The ThinEmbed rows are the released files.
| query encoder | file | nDCG@10 | p50 | p95 | load | peak memory |
|---|---|---|---|---|---|---|
| harrier-0.6b, full precision (reference, not run in the browser) | 1 143 MiB (fp16) | 0.7559 | – | – | – | – |
| Q3_K, llama.cpp format, wllama on WebGPU | 235.1 MiB | 0.7481 | 444 ms | 754 ms | 4.2 s | 2 080 MiB |
| IQ2_M, llama.cpp format, wllama on WebGPU | 199.3 MiB | 0.7387 | 508 ms | 1 012 ms | 5.2 s | 1 983 MiB |
| Q2_K, llama.cpp format, wllama on WebGPU | 192.4 MiB | 0.7413 | 405 ms | 680 ms | 4.7 s | 2 124 MiB |
| Q2_K, llama.cpp format, wllama on the CPU (WebAssembly, 8 threads) | 192.4 MiB | 0.7425 | 1 669 ms | 3 363 ms | 4.4 s | 1 228 MiB |
| ThinEmbed, 2.10 bits per weight, WebGPU | 119.5 MiB | 0.7375 | 103 ms | 113 ms | 2.4 s | 1 592 MiB |
| ThinEmbed, 1.83 bits per weight, WebGPU | 105.3 MiB | 0.7109 | 102 ms | 112 ms | 2.5 s | 1 501 MiB |
The Q3_K and Q2_K files are our GPTQ exports onto llama.cpp's formats; llama.cpp's own quantiser reaches the same quality at that size. One device, one model, one corpus: discrete GPUs and phones are not measured. Source: vq_results.md §2.
The smallest files against other approaches
harrier-0.6b on SciFact, test split, 300 queries, fp32 0.7559. Native: llama.cpp on the CPU. Browser: our WebGPU runtime. Simulation: torch simulation of the quantised weights.
| query encoder | file | nDCG@10 | measured |
|---|---|---|---|
| Q3_K, our GPTQ export, generic English calibration (released) | 235.1 MiB | 0.7475 | native |
| IQ2_M, llama.cpp quantiser, domain imatrix | 199.3 MiB | 0.7418 | native |
| Q2_K, our GPTQ export, SciFact-corpus calibration | 192.4 MiB | 0.7440 | native |
| IQ2_XS, llama.cpp's smallest format, domain imatrix | 177.5 MiB | 0.7097 | native |
| BitNet-embedding-270m, ternary, trained by Microsoft (a different model with its own index) | 140 MiB | 0.7350 | native |
| ThinEmbed, 2.10 bits per weight (released) | 119.5 MiB | 0.7375 | browser; simulation 0.7380 |
| ThinEmbed, 1.83 bits per weight (released) | 105.3 MiB | 0.7109 | browser = simulation |
| ThinEmbed, 1.58 bits per weight (not released) | ~91 MiB | 0.7074 | simulation; ±0.03 across rotation seeds |
Paired against BitNet-270m at 2.12 bits per weight: SciFact +0.007 [−0.014; +0.028] and NFCorpus −0.005 [−0.020; +0.009], both indistinguishable; ArguAna +0.044 [+0.023; +0.065]. Sources: vq_results.md §1, model card of ThinEmbed-harrier-0.6b.
Czech: WebFAQ-cs
Qwen3-Embedding-0.6B on WebFAQ-cs (PaDaS-Lab/webfaq-retrieval, CC BY 4.0): 71 529 passages, 7 231 human-judged test questions; nDCG@10 against the model's own fp32 index.
| query encoder | file | nDCG@10 | cosine to fp32 | top-10 overlap | p50 |
|---|---|---|---|---|---|
| Qwen3-Embedding-0.6B, full precision (reference) | – | 0.7308 | – | – | – |
| ThinEmbed, 3.57 bits per weight (2-d codebooks) and the full token table (released) | 237.0 MiB | 0.7072 | 0.946 | 0.755 | 112 ms |
ThinEmbed row: torch simulation of the file on the full test split. In the browser (Intel integrated GPU) the file reproduces the simulation on the first 50 questions to the fourth decimal (0.6393 against 0.6393); p95 130 ms, load 2.5 s. Calibration on 3 000 WebFAQ-cs training questions, one seed. The released scalar Czech files are measured on a Czech supreme-court index instead (see the released files). Source: model card of ThinEmbed-Qwen3-Embedding-0.6B-vq.
Released files
Each file is a query encoder for exactly one document encoder and its settings; the index does not change. Quality in % of the model's own fp32 nDCG@10, test splits unless stated.
| repository | base model · licence | files | measured |
|---|---|---|---|
| ThinEmbed-harrier-0.6b | microsoft/harrier-oss-v1-0.6b · MIT | Q3_K, generic English calibration, 235.1 MiB | SciFact 98.9 · NFCorpus 99.3 · ArguAna 101.3 · SciDocs 99.2 % |
| ThinEmbed-harrier-0.6b | MIT | Q2_K, SciDocs synthetic queries, 192.4 MiB | SciDocs 94.5 % |
| ThinEmbed-harrier-0.6b | MIT | Q2_K, generic English calibration, 192.4 MiB | SciFact 96.4 · SciDocs 90.1 % |
| ThinEmbed-harrier-0.6b-vq | MIT | 2.10 / 1.83 bits per weight, SciDocs files 127.0 / 112.8 MiB, SciFact files 119.5 / 105.3 MiB | SciDocs 94.7 / 91.3 % · SciFact 97.6 / 94.0 % (browser) |
| ThinEmbed-harrier-0.6b-vq | MIT | prompt-K/V variants, 2.10 / 1.83 bits per weight, SciDocs 129.1 / 114.9 MiB, SciFact 121.6 / 107.3 MiB | SciDocs 96.6 / 93.8 % · SciFact, all 1 109 queries, 98.1 / 95.8 % (with the stored prompt keys and values) |
| ThinEmbed-Qwen3-Embedding-0.6B-vq | Qwen/Qwen3-Embedding-0.6B · Apache-2.0 | 3.57 bits per weight, Czech, 237.0 MiB | WebFAQ-cs 96.8 % (browser-verified) |
| ThinEmbed-Qwen3-Embedding-0.6B | Apache-2.0 | Q4_K_M / Q5_K_M, English calibration, 339.9 / 385.4 MiB | SciFact 100.0 / 99.8 · NFCorpus 99.4 / 100.1 · ArguAna 100.1 / 100.1 · SciDocs 99.3 / 100.1 % |
| ThinEmbed-Qwen3-Embedding-0.6B | Apache-2.0 | Q4_K_M / Q5_K_M, Czech calibration, 339.9 / 385.4 MiB | Czech supreme-court index 98.7 / 99.5 % |
| ThinEmbed-bge-m3 | BAAI/bge-m3 · MIT | Q4_K_M / Q3_K, Czech calibration, 354.6 / 320.6 MiB | Czech supreme-court index 99.2 / 98.3 % · SciFact 99.6 / 100.0 % |
The Czech supreme-court index: 55 071 segments, 1 137 synthetic queries; the data are not public. Each repository's model card carries the compatibility, the recipe and the sha256 of every file. Sources: the model cards in docs/release of the repository.
How it was measured
Test split and the model's own fp32 index, in native llama.cpp or in the browser. Every run is pre-registered with a prediction and a kill rule, comparisons change one thing at a time, and differences are paired bootstrap intervals over queries (10 000 draws), read against the variance of the calibration draw (about 0.01 nDCG@10) and between machines (±0.006).
- At about 235 MiB, llama.cpp's own quantiser with an imatrix reaches the same quality as our GPTQ export. The scalar files are no better than this free baseline.
- Storing the query prompt's keys and values in full precision (2 MB) and calibrating with them in place added about 0.010 nDCG@10 at 1.8 bits per weight on SciFact and on the Czech legal index (three seeds), and nothing at 2.1 or 1.6 bits. These files are released as the prompt-K/V variants.
- Giving the most-used neurons a larger codebook did not help: −0.0085 cosine to fp32 against a matched random split, nDCG@10 within noise (three seeds).
- Qwen3-Embedding-0.6B at 2.1 bits per weight on Czech is unstable: 65.6 % of fp32 on one machine and 80.6 % on another from the same configuration and seed. At 3.1 bits the two agree to 0.002.
Quickstart
In the browser with the npm package @moravian/thinembed-vq. Needs WebGPU with shader-f16 (Chrome, Edge); there is no CPU fallback.
import { loadVqwClient } from 'https://moravian.ai/vqweb/vqweb.js'; // or: npm install @moravian/thinembed-vq
const client = await loadVqwClient(
'https://huggingface.co/moravianai/ThinEmbed-Qwen3-Embedding-0.6B-vq/resolve/main/qwen3-0.6b-vq3.5d2-webfaq_q-vocfull.vqw');
const q = await client.embed('Kolik stojí parkování v centru Prahy?'); // Float32Array(1024), L2-normalised
// score = dot(q, d) against your Qwen/Qwen3-Embedding-0.6B document vectors
Measuring a client on your own index with the PyPI package thinembed; the document vectors you already have are never re-encoded:
pip install "thinembed[torch]"
eq import --name mycorpus --docs corpus.jsonl --doc-emb index.f32 --doc-ids ids.json --teacher qwen3-0.6b --language cs
eq synth --dataset mycorpus --lang Czech --n-per-doc 2 --as-calib # synthetic queries + calibration text
eq cache --dataset mycorpus --teacher qwen3-0.6b # fp32 query vectors of the unchanged model
eq compile --model model-f16.gguf --types Q4_K_M Q3_K --calib data/calib/mycorpus_synth_q.txt --table q4_0
eq eval --dataset mycorpus --teacher qwen3-0.6b --clients models/*.gguf --bin llama-embedding --rule
eq compare results/perq/mycorpus_<a>.npz results/perq/mycorpus_<b>.npz # paired bootstrap, same queries
Links
- Code, technical report and verification recipe: github.com/rosecky/embedding-quantization-public (Apache-2.0)
- Technical report: technical_report.md · vector-quantised results: vq_results.md
- npm: @moravian/thinembed-vq · package source and README
- PyPI: thinembed · package README
- Released files: huggingface.co/moravianai
- Browser demo: moravian.ai/demo
Code Apache-2.0. Client files under their base model's licence (MIT or Apache-2.0). Demo corpora SciDocs (BEIR, CC BY 4.0) and WebFAQ-cs (CC BY 4.0).