File size: 11,290 Bytes
0602acc
 
a21dfcb
 
 
 
 
 
a2a69e2
a21dfcb
 
 
a2a69e2
 
a21dfcb
 
 
 
 
0602acc
a21dfcb
 
 
a2a69e2
 
 
 
 
 
 
 
 
 
a21dfcb
a2a69e2
 
a21dfcb
a2a69e2
a21dfcb
a2a69e2
a21dfcb
a2a69e2
 
a21dfcb
 
 
a2a69e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c004050
 
a2a69e2
 
c004050
 
 
 
 
 
 
 
 
 
 
 
 
a2a69e2
 
 
 
 
 
 
57653fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a2a69e2
 
 
 
a21dfcb
 
a2a69e2
a21dfcb
 
a2a69e2
a21dfcb
a2a69e2
a21dfcb
 
a2a69e2
a21dfcb
 
 
 
 
a2a69e2
 
a21dfcb
a2a69e2
a21dfcb
a2a69e2
 
a21dfcb
 
a2a69e2
 
a21dfcb
a2a69e2
a21dfcb
a2a69e2
a21dfcb
a2a69e2
a21dfcb
a2a69e2
 
 
 
 
 
a21dfcb
a2a69e2
 
 
 
a21dfcb
a2a69e2
 
 
 
a21dfcb
a2a69e2
 
 
 
 
 
 
 
c004050
a2a69e2
 
 
 
 
 
 
 
 
 
 
 
a21dfcb
 
 
a2a69e2
 
 
a21dfcb
a2a69e2
 
 
a21dfcb
 
a2a69e2
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
---
license: mit
language:
- code
- multilingual
tags:
- knowledge-graph
- relation-extraction
- relation-classification
- cross-encoder
- text-classification
- knowledge-distillation
- tensorrt
- openvino
pipeline_tag: text-classification
base_model:
- cross-encoder/mmarco-mMiniLMv2-L12-H384-v1
datasets:
- unicamp-dl/mmarco
---

# code-daemon-relation-v1

A **117M-parameter relation classifier**. Mark two entities inside a passage and it answers, in one
forward pass, **how they relate** β€” one of three relation types, or *no relation*.

It does a job usually handed to a large generative model β€” read a passage, extract typed relations
between the things it mentions β€” as **a single classification** instead of token-by-token generation.
That makes it cheap enough to sweep an entire corpus: **~2 900 pairs/sec** on a laptop RTX 5060.

```python
logits = session.run(None, {"input_ids": ids, "attention_mask": mask})[0]   # [B, 4]
```

Text is fed as an *(empty query, marked passage)* pair. Multilingual β€” the XLM-R backbone reads prose
and code comments in many languages.

---

## 1. The four classes

Wrap each entity in `[E1]…[/E1]` and `[E2]…[/E2]` inside its natural context. Take the `argmax`;
class 0 is an explicit **abstain**, and a softmax threshold drops the rest of the low-confidence tail.

| idx | label | meaning |
|--:|---|---|
| 0 | `NO_RELATION` | the two co-occur but are not related β€” **abstain** |
| 1 | `semantically_similar_to` | near-duplicate purpose, or the same goal by a different mechanism |
| 2 | `invalidates_with` | one supersedes, replaces or contradicts the other |
| 3 | `depends_on` | one requires or configures the other |

The taxonomy is deliberately **coarse**. An earlier 8-way version split these into near-synonym pairs
(`semantically_similar_to` vs `shares_purpose_with`, `replaced_by` vs `contradicts`, `depends_on` vs
`configured_by`) and the distinctions were not reliably separable from context β€” the classifier spent
its capacity on boundaries that downstream consumers then collapsed anyway. Merging them into three
positives plus abstain is what the model is actually good at.

**Decision rule as shipped:** `argmax != NO_RELATION` **and** `1 - softmax[NO_RELATION] >= tau`.
Gating on the *probability that any relation exists* rather than on the winning class's own
probability is more robust: when a real relation's mass spreads across two plausible classes, the
per-class maximum sags while "something is here" stays high.

---

## 2. Architecture

| | |
|---|---|
| Warm-start | [`cross-encoder/mmarco-mMiniLMv2-L12-H384-v1`](https://huggingface.co/cross-encoder/mmarco-mMiniLMv2-L12-H384-v1) |
| Encoder | XLM-RoBERTa, **12 layers / 384 hidden / 12 heads**, FFN 1536 |
| Vocabulary | **250 006** SentencePiece pieces = 250 002 XLM-R + 4 marker tokens |
| Markers | `[E1]` `[/E1]` `[E2]` `[/E2]` (ids 250002–250005) |
| Sequence | **256 tokens** on the shipped engines (64 / 128 / 320 also provided) |
| Inputs | `input_ids`, `attention_mask` β€” **no `token_type_ids`** |
| Output | `logits[batch, 4]` |
| Parameters | **~117M**, of which 96M is the multilingual embedding table |

### Entity-marker pooling, not `[CLS]`

The classification head does **not** read the `[CLS]` vector. It mean-pools the hidden states at the
**entity-start markers** β€” the `[E1]` and `[E2]` positions β€” concatenates the two, and passes that
through a single linear layer.

This matters for a relation task. A `[CLS]` vector summarises the whole passage, so the head has to
recover *which two things* the question is about from a global summary. Reading the marker positions
instead gives the head both arguments directly and in order, so the relation is scored between the
two entities rather than inferred from the sentence as a whole. Direction comes free: swap the
markers and the input genuinely changes.

---

## 3. How it was made

**Warm-started** from a strong multilingual ranking cross-encoder, with its single ranking logit
replaced by the 4-class marker-pooling head, then fine-tuned by **sequence-level distillation**.

A large instruction-tuned LLM (Claude) read real documentation and emitted relation tuples grounded
in the passage it was shown. Those tuples became the training targets after several filtering passes:

- **Grounding** β€” entity names the teacher marked must actually resolve inside the chunk, which drops
  hallucinated arguments.
- **Windowing** β€” the passage is cropped so that both markers survive truncation.
- **Merging** β€” the original 8 labels are collapsed to the 4 above.
- **Negatives** β€” explicit `NO_RELATION` examples synthesised from co-occurring but untupled entity
  pairs in the same chunk, so abstain is trained rather than inferred.
- **Logit adjustment** β€” the class prior is corrected at the loss, since a teacher naturally emits
  "similar" far more often than "depends on".

---

## 4. Speed

Measured on one laptop: Intel Core Ultra 9 275HX / NVIDIA RTX 5060 Laptop.

| lane | batch Γ— seq | per batch | throughput | per pair |
|---|---|--:|--:|--:|
| **TensorRT FP16, RTX 5060 Laptop** | 16 Γ— 256 | **5.44 ms** | **2 942 pairs/s** | 0.34 ms |
| **OpenVINO FP16, iGPU** (OV 2026.3) | 16 Γ— 256 | **143 ms** | **111 pairs/s** | 8.96 ms |
| OpenVINO FP16, CPU (OV 2026.3) | 16 Γ— 256 | 366 ms | 44 pairs/s | 22.9 ms |
| ONNX Runtime FP32, CPU | 16 Γ— 256 | 380 ms | 42 pairs/s | 23.7 ms |

Per bucket, OpenVINO 2026.3 on the same laptop (pairs/s, solo):

| bucket | batch Γ— seq | CPU FP16 | iGPU FP16 |
|---|---|--:|--:|
| s | 16 Γ— 64 | 159 | **508** |
| m | 16 Γ— 128 | 85 | **245** |
| l | 16 Γ— 256 | 44 | **111** |
| xl | 16 Γ— 320 | 34 | **85** |

The integrated GPU is ~2.6Γ— the CPU on every bucket, so on a host without a discrete card the
iGPU lane is the one to route relation extraction to. Both devices at once give ~87 % of the sum
of their solo rates β€” they share one memory controller.

The GPU lane is ~67Γ— the CPU lane, which is the point: relation extraction over a corpus means tens
of thousands of candidate pairs, and only the compiled-engine path makes that a background task
rather than a batch job.

Four **length buckets** ship β€” seq 64 / 128 / 256 / 320 at batch 16. Attention is quadratic in
sequence length, so routing short passages to a short engine is worth taking when your pairs vary in
length. Padding is attention-masked, so a pair produces the same logits from any bucket that fits it.

### At corpus scale

The table above is a per-batch micro-benchmark. Sweeping a real corpus batches far wider, which
changes what the bottleneck is:

| | measured |
|---|--:|
| GPU inference, 267 pairs per call at seq 256 | **5.87 ms** β†’ ~45 000 pairs/s |
| End-to-end incl. tokenisation (12 CPU threads, overlapped) | **~2 500 pairs/s** |
| Relations written, 3 043 chunks | 15 175 in 15.3 s β†’ **994/s** |
| Relations written, 4 975 chunks | 31 696 in 45.0 s β†’ **704/s** |

Once the engine is batched this wide the GPU is no longer the limit β€” **tokenisation is**, and it is
worth giving it real thread count. Measured padding waste at these batch sizes is 18–20%, which is
what the length buckets are there to keep down.

---

## 5. Standalone use

```python
import numpy as np, onnxruntime as ort
from transformers import AutoTokenizer

LABELS = ["NO_RELATION", "semantically_similar_to", "invalidates_with", "depends_on"]

tok  = AutoTokenizer.from_pretrained(".")        # includes the [E1]/[E2] marker tokens
sess = ort.InferenceSession("model.onnx", providers=["CPUExecutionProvider"])

def classify(marked_text, max_len=256, tau=0.7):
    enc = tok([""], [marked_text], padding="max_length", truncation=True,
              max_length=max_len, return_tensors="np", return_token_type_ids=False)
    logits = sess.run(None, {"input_ids":      enc["input_ids"].astype(np.int64),
                             "attention_mask": enc["attention_mask"].astype(np.int64)})[0][0]
    p = np.exp(logits - logits.max()); p /= p.sum()
    if 1.0 - p[0] < tau:                          # gate on "any relation at all"
        return "NO_RELATION", float(p[0])
    i = int(p.argmax())
    return (LABELS[i], float(p[i])) if i else ("NO_RELATION", float(p[0]))

classify("The [E1]FAISS[/E1] index was replaced by the [E2]native IVF[/E2] backend.")
# -> ('invalidates_with', 0.7x)
```

Both entities must appear **inside one passage**, marked in place. The model reads context, so a bare
pair of names with no surrounding text carries little signal.

---

## 6. Evaluation

**Dev macro-F1 = 0.547** over the four classes, on a held-out split of the distillation set.

Read that as what it is. The classes are intrinsically imbalanced β€” a teacher describing
documentation emits "similar" far more often than "depends on" β€” and the merged taxonomy still
contains genuinely ambiguous boundaries that human annotators would also disagree on. The abstain
class plus the `tau` gate exist because the useful operating point is *high-precision edges*, not
maximum recall: for building a graph, the real test is spot-checking the edges it emits at your
chosen threshold.

**Suited to**
- Turning prose or documentation into a typed concept graph.
- Any sweep where a large LLM per pair would be too slow or too expensive.
- Multilingual corpora, including code comments.

**Not suited to**
- Fine-grained relation ontologies β€” this is 3 positives plus abstain by design.
- Entity *extraction*: it classifies pairs you already found, it does not find them.
- Passages where the two entities are far apart β€” the marked window is 256 tokens.

---

## 7. What is in this repo

Compiled engines, named per **runtime Γ— OS Γ— GPU arch**, plus the ONNX for standalone use.

- **TensorRT FP16** β€” `code-daemon-relation-v1-{s,m,l,xl}_{win_x64,linux_x64}_trt11.0_sm_120.engine`
  (buckets seq 64 / 128 / 256 / 320, batch 16).
- **OpenVINO FP16** β€” `code-daemon-relation-v1-{s,m,l,xl}_ov2026.3_{cpu,igpu}_fp16_b16_s{64,128,256,320}.{xml,bin}`.
- **Tokenizer** β€” `tokenizer.json`, `sentencepiece.bpe.model`, `tokenizer_config.json` (XLM-R
  SentencePiece with the four marker tokens added).
- **ONNX** β€” `model.onnx` (+ `model.onnx.data`), FP32, the build source for every engine above.

FP16 rather than INT8: this architecture's activation outliers make per-tensor INT8 calibration
lossy, and FP16 costs nothing on any GPU that can run it.

---

## 8. License & attribution

Released **MIT**.

| Source | Note |
|---|---|
| `cross-encoder/mmarco-mMiniLMv2-L12-H384-v1` (warm-start) | **mMARCO ← MS MARCO β†’ non-commercial research terms** |
| Distillation targets (LLM-labelled open-source documentation) | self-generated |
| Synthesised negatives and rare-class augmentation | generated |

⚠️ The warm-start base derives from **MS MARCO (non-commercial)**. Whether a fine-tuned model
inherits dataset-use terms is legally unsettled β€” this is **not legal advice**. Retrain from a
permissive base if strict compliance matters to you.

Warm-started from **[cross-encoder/mmarco-mMiniLMv2-L12-H384-v1](https://huggingface.co/cross-encoder/mmarco-mMiniLMv2-L12-H384-v1)**.
Backbone: XLM-RoBERTa. Used by the [UltraCode](https://github.com/faxenoff/ultracode) code assistant,
though nothing about the model is specific to it.