| --- |
| license: apache-2.0 |
| tags: |
| - executorch |
| - xnnpack |
| - pte |
| - on-device |
| - text-ranking |
| base_model: |
| - Qwen/Qwen3-Reranker-0.6B |
| --- |
| # Qwen3-Reranker-0.6B β ExecuTorch |
|
|
| A reranker that is not a cross-encoder. The other five on this shelf are BERTs with a |
| regression head; this one is a **causal language model asked a yes/no question**, and |
| the score is how much more it would answer "yes" than "no". Same job β read a query and |
| one document together and score the pair β with a different machine underneath. |
|
|
| ``` |
| input_ids (1, 512) int64 the prompt, LEFT-padded |
| attention_mask (1, 512) int64 |
| -> score (1, 1) fp32 log-odds: logit("yes") - logit("no") |
| ``` |
|
|
| - **Source**: [Qwen/Qwen3-Reranker-0.6B](https://huggingface.co/Qwen/Qwen3-Reranker-0.6B) β 595.8M parameters, 28 Qwen3 layers, hidden 1024, 151,669 vocabulary |
| - **License**: apache-2.0 |
| - **Output**: one number per pair. Higher is more relevant; `sigmoid(x)` maps it to 0..1 |
| and does not change the ordering. |
|
|
| ## Variants |
|
|
| | build | file | size (MB) | worst score error vs eager | Mac ms* | backend takes | |
| |---|---|---|---|---|---| |
| | fp32 | `rerank_qwen3_0_6b_xnnpack_fp32.pte` | 2383.7 | **0.0001 logits** | 320.5 | 80.8% | |
| | fp16 | `rerank_qwen3_0_6b_xnnpack_fp16.pte` | 1192.4 | 0.0268 logits | 1188.8 | 70.4% | |
| | **Core ML (fp16, iOS)** | `rerank_qwen3_0_6b_coreml_all.pte` | 1196.2 | 0.0886 logits | **83.4** | 100% | |
|
|
| \*Mac arm64, one 512-token pair, **fastest of five medians of ten** β a reference point for |
| relative cost, not a device number. The host shares its cores with other work and a single |
| median does not survive that; contention only ever adds time, so the fastest repetition is |
| the one that means something. Torch eager fp32, measured the same way, is 320.1 ms. |
| **Core ML is the one to use where it exists**: 3.8x faster than XNNPACK fp32, the whole |
| graph in one delegated subgraph where XNNPACK takes 80.8% across 172. A reranker earns its |
| keep over a list of fifty candidates, so that factor is the whole story. |
| |
| Correlation cannot judge this model β the output is a single number, and the correlation |
| of a one-element vector is undefined. The gate is the score error in the model's own |
| units together with whether the ranking survives. Over six real pairs, **every shipped |
| build reproduces eager's order**, and the narrowest adjacent gap in that ranking is |
| 0.7433 logits. |
| |
| ## Running it |
| |
| **1. Build the prompt.** The model was trained to read one specific frame, and it is not |
| a chat model you can prompt freely: |
| |
| ```python |
| PREFIX = ('<|im_start|>system\nJudge whether the Document meets the requirements ' |
| 'based on the Query and the Instruct provided. Note that the answer can ' |
| 'only be "yes" or "no".<|im_end|>\n<|im_start|>user\n') |
| SUFFIX = "<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n" |
| INSTRUCT = "Given a web search query, retrieve relevant passages that answer the query" |
| |
| text = (f"{PREFIX}<Instruct>: {INSTRUCT}\n<Query>: {query}\n" |
| f"<Document>: {document}{SUFFIX}") |
| ``` |
| |
| `INSTRUCT` is a real input, not decoration: it names the retrieval task, and Qwen |
| reports it is worth a few points to write your own instead of the generic default. |
| |
| **2. Tokenise with LEFT padding to exactly 512.** |
| |
| ```python |
| tokenizer = AutoTokenizer.from_pretrained(repo, padding_side="left") |
| batch = tokenizer([text], padding="max_length", truncation=True, max_length=512, |
| return_tensors="pt") |
| assert batch["attention_mask"][0, -1] == 1 |
| ``` |
| |
| **This is the one that will bite.** The graph reads position β1 unconditionally, which |
| is the last *real* token only when the padding is on the left. Right-padded input scores |
| a pad token and returns a confident-looking number that means nothing β nothing raises. |
| The assertion above is one line and catches it. |
|
|
| **3. Run it once per candidate** and sort by the score, descending. |
|
|
| ## The output projection is folded into one vector |
|
|
| The score needs two of the 151,669 logits, and the difference of two dot products is one |
| dot product against the difference of two rows: |
|
|
| ``` |
| logit_yes - logit_no = h . W[yes] - h . W[no] = h . (W[yes] - W[no]) |
| ``` |
|
|
| So `lm_head` never runs. A 151,669-wide matmul per pair does not happen, and the delegate |
| does not take its own copy of a 621 MB table β which is exactly why this shelf's Whisper |
| decoders come out bigger than their weights. |
|
|
| The two vocabulary ids are read from the repo's own `1_LogitScore/config.json` |
| (**9693** for "yes", **2152** for "no") rather than looked up through the tokenizer, |
| where a leading-space variant is a different token. |
|
|
| **Checked against the reference, at matching precision.** sentence-transformers' |
| `CrossEncoder` scores these pairs at +6.6875 / β6.0625 / β7.2812 and the folded head at |
| +6.7271 / β6.1107 / β7.3159 β a gap of 4.8e-02 that has nothing to do with the fold. |
| The reference loads the checkpoint in its own **bfloat16**; run both arms in fp32 and the |
| folded head matches it to **4.673e-05**, while the reference's bf16 and fp32 arms differ |
| from each other by the full 4.820e-02. |
|
|
| ## It ranks across languages |
|
|
| The six test pairs include a Japanese passage that says what the query asks. It comes out |
| **first of six** at +7.470, above the English document carrying the actual number |
| (+6.727). The shelf's `ms-marco-MiniLM` rerankers are English-only and put it far lower β |
| which is the right answer for them, not a defect. |
|
|
| ## The attention is eager, and the left padding is why that needed checking |
|
|
| `F.scaled_dot_product_attention` does not survive export as one operation. The edge |
| dialect lowers it through `_safe_softmax`, whose guard against a row with no unmasked key |
| at all leaves **eleven operations XNNPACK cannot take, in every attention block** β |
| `scalar_tensor`, `where`, `mul.Scalar`, `logical_not`, `eq`, `full_like`, `any.dim`. Over |
| 28 layers that is 308 operations, each one cutting the subgraph in two. Exporting with |
| `attn_implementation="eager"` removes them: **72.0% to 80.8% delegated**, fp32 from 381.5 |
| to 320.5 ms. |
|
|
| **This is the one model on the shelf where that guard could plausibly have been doing |
| work.** Its prompt is left-padded, so the leading rows of a causal mask have no unmasked |
| key at all β exactly the case the guard exists for. Every other model here is |
| right-padded, where even an all-padding row still sees the real tokens and the question |
| never arises. |
|
|
| So it was measured rather than assumed. With 109 real tokens of 512, sdpa against eager: |
| no `NaN` appears on either arm, and the scored last position reads **54.7696 against |
| 54.7695**. The reason is that the guard fires only on `-inf`, and eager masks with |
| `torch.finfo(dtype).min`, a large finite number β a fully-masked row comes back uniform |
| instead of zeroed. Both answers are defined; they differ only about padding rows, and |
| this graph reads position β1, which is a real token by construction. |
|
|
| ## Not shipped: int8 |
|
|
| `rerank_qwen3_0_6b_xnnpack_int8.pte` is **1063.9 MB** and runs at 265.4 ms β smaller and |
| faster than the fp32 build, and 85.3% of it delegates, the most of any build here. It is |
| withheld on the number that decides. |
|
|
| Dynamic int8 moves a pair score by **0.6336 logits**, against a narrowest adjacent gap of |
| **0.7433** in the ranking it has to preserve. It happens to keep the order on these six |
| pairs, but an error that is 85% of the distance between two neighbours is not a build |
| that ranks reliably β a slightly different candidate list would reorder. |
|
|
| The size was predictable before it was built: the token embedding table is |
| 151,669 x 1024 x 4 = 621 MB of the 2383.7 MB model, a **26.1%** share, and this shelf's |
| rule `int8/fp16 = 0.5 + 1.5 x (table share)` puts the file at 0.891 of fp16. It came out |
| at **0.892** β the same figure as Qwen3-Embedding-0.6B, which shares this backbone. |
|
|
| Going eager (below) made this build 13% faster and took it from 78.1% to 85.3% |
| delegated. It did not move the verdict: speed and quality are separate questions, |
| and what withholds this file is the score error, which barely changed. |
|
|