---
name: awembed
description: >-
Train an embedding model that knows your corpus. Your agents search your code, your docs, your tickets with an embedding model that has never seen any of it. A general-purpose embedder is right about two thirds of the time on a corpus it was not trained on. You can do better with a model thirteen times smaller, in an afternoon, on one GPU — and prove it on a split the model never saw.
---
# awembed — train an embedding model that knows your corpus
Your agents search your code, your docs, your tickets with an embedding model that has
never seen any of it. A general-purpose embedder is right about two thirds of the time
on a corpus it was not trained on. You can do better with a model thirteen times smaller,
in an afternoon, on one GPU — and prove it on a split the model never saw.
Measured on one codebase: the distilled 0.6B student retrieved the right directory on the
first try 80% of the time; the 7.85B teacher it learned from managed 66%; the same 0.6B
model before training, 62%. The student ships as 1.06 GB of int8 at 0.999 fidelity.
## Why a small model wins here
The student is trained on two signals at once. The **teacher's margins** — how far it
separates the right answer from plausible wrong ones — is the distillation half. The
**corpus's own labels** — which document actually answers which question — is the
in-domain half, and it is the one the teacher never had. For retrieval over something
you own, in-domain supervision is worth more than parameters.
## Install
```bash
pip install awembed # student side (torch + transformers)
pip install "awembed[teacher]" # also the teacher server
```
## The recipe, stage by stage
Every stage writes a sidecar with what it measured and refuses to hand on an artifact
that fails its gate. Run them in order against one output root.
```bash
# 0. Corpus, from per-directory READINGS of your repo: one JSON per line,
# {"rel": "
", "reading": "", "prompt_version": "v1"}.
# Any LLM over `ls` + a few file heads produces them (~1,500 chars each is plenty).
# Output: one row per question -> right directory -> K hard negatives (plausible
# and wrong), split by DIRECTORY so eval never sees a directory training saw.
# Your own question -> target pairs go in via --ground-truth (JSONL {"query","target"}).
awembed corpus --readings dir_readings.jsonl --out corpus.jsonl
# 1. Prove the teacher loads and answers on THIS machine before you rent anything.
awembed probe # exit 0 ok / 1 failed / 2 could not judge
# 2. Capture: score every row with the teacher (starts the teacher server for you).
awembed capture --corpus corpus.jsonl --seed 1 --out artifacts/
# 3. Distill the student. Gate: the loss must fall (last-10 mean < first-10 mean).
awembed distill --corpus corpus.jsonl --seed 1 --out artifacts/
# 4. Weight-only int8 export. Gate: >= 0.98 mean cosine to the fp32 student.
awembed quantize --corpus corpus.jsonl --seed 1 --out artifacts/
# 5. Teacher vs untrained baseline vs student vs int8 on the held-out directories.
awembed eval --corpus corpus.jsonl --seed 1 --out artifacts/
# All four artifact stages, stopping at the first gate that refuses:
awembed run --corpus corpus.jsonl --seed 1 --out artifacts/
```
## Things that cost a run each, so you do not have to pay for them
- **Probe first.** The teacher's remote code needs `einops`, `datasets` and
`accelerate`; without them it dies at load, after you have paid for the machine.
- **The teacher's backbone loads fp32 whatever dtype you ask for** — 27.9 GB for a
14.6 GB checkpoint. The teacher module casts explicitly; if you serve it another way,
cast after load or a 24 GB card will OOM in fp16 *and* bf16.
- **Two environments.** The default teacher pins an older `transformers` than the
student needs (its cache API broke on the first forward under 4.55+). Run
`teacher`/`probe` in their own venv and point `capture` at it:
`NV_EMBED_PYTHON=/path/to/venv/bin/python awembed capture ...`
- **Activations, not weights, are what OOM the trainer** — batch 16 with 3 negatives is
80 sequences per step with grad. Gradient checkpointing is on by default on CUDA.
- **Dynamic int8 wrecks Qwen3-family MLPs** (cosine 0.87). The quantize stage uses
weight-only per-channel int8 (0.999) for that reason; do not "optimise" it back.
## Using what you trained
`artifacts/student/` is an ordinary Hugging Face directory. Serve it with vLLM
(`--task embed`), Text Embeddings Inference, or `transformers` directly. Two rules:
1. **Queries carry the instruction prefix the student was trained with; documents do
not.** The prefix is in `train_sidecar.json`. Omit it on queries and the vector
spaces stop matching.
2. **It is a new vector space.** Index into a fresh collection. A 1024-dim vector into a
768-dim collection is a write error; a mixed collection scores garbage silently.
Then point the family at it: `awgraph` (set its code-embed URL, model and query
prefix), `awm` (agent memory), `awfind` (ranked answers), `awrecurse` (documents beyond
a context window), and any `awdk` agent that uses those tools.
## Licences
Your student carries its base model's licence (the default student,
Qwen3-Embedding-0.6B, is Apache-2.0). The default teacher, NV-Embed-v2, is
CC-BY-NC-4.0: use it as a signal, keep its weights and its captured targets out of
anything you ship. The sidecars record which teacher revision supervised the run.