code-daemon-relation-v1

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

What makes it different: typed relation extraction is usually handed to a large generative model, token by token. This does it as a single classification, which makes it cheap enough to sweep a whole corpus β€” ~2 900 pairs/s on a laptop RTX 5060 β€” and its head reads the two marked entities directly instead of a whole-passage summary, so the relation is scored between them and in order. Multilingual: the XLM-R backbone reads prose and code comments in many languages.

The weights under this id were replaced on 2026-10-12. Same architecture, same inputs and outputs, same tokenizer; the backbone is now the MIT-licensed hotchpotch/mMiniLMv2-L12-H384, and macro-F1 on the held-out set moved from 0.57 to 0.60. Engines built for the previous weights are gone from this repository.

The four classes

Wrap the entities in [E1]…[/E1] and [E2]…[/E2] in their own context.

idx label meaning
0 NO_RELATION they co-occur but are unrelated β€” abstain
1 semantically_similar_to same purpose or goal, different mechanism
2 supersedes_or_conflicts one replaces, supersedes or contradicts the other
3 depends_on one requires or configures the other

The taxonomy is coarse on purpose: an earlier 8-way split (replaced_by vs contradicts, depends_on vs configured_by, …) drew boundaries the context could not support.

Decision rule: accept when argmax != NO_RELATION and 1 βˆ’ softmax[NO_RELATION] β‰₯ tau. Gating on "is there any relation" holds up better than gating on the winning class, whose probability sags when a real relation splits between two plausible types.

Numbers

Same 691 held-out pairs, same 4-way mapping, same machine. The textbook alternative with no training data is a zero-shot NLI model that scores each class as a hypothesis:

model params macro-F1 "has a relation" F1 P R pairs/s
this model 117M 0.598 0.798 0.786 0.811 351
mDeBERTa-v3-xnli, zero-shot 279M 0.194 0.699 0.538 0.997 61
always NO_RELATION β€” 0.158 β€” β€” β€” β€”
  • The NLI model says "related" to almost everything (recall 0.997 at precision 0.538); as an edge weight for a graph that is not a usable signal.
  • 5.8Γ— faster on the same runtime, structurally: zero-shot pays one forward pass per class, this pays one per pair. Speeds above are onnxruntime CUDA FP32 at batch 16 Γ— seq 256; the shipped TensorRT engine is faster again.
  • The trade: three relation types plus abstain, nothing finer, and macro-F1 0.60 β€” the classes are imbalanced and some boundaries are genuinely ambiguous (per class on the same set: NO_RELATION 0.76, semantically_similar_to 0.64, supersedes_or_conflicts 0.47, depends_on 0.52). It is built for a high-precision operating point: spot-check the edges at the tau you choose.

Speed by lane

Batch 16 Γ— seq 256, laptop Intel Core Ultra 9 275HX + RTX 5060:

lane pairs/s
TensorRT FP16, RTX 5060 2 942
OpenVINO FP16, Intel iGPU 111
OpenVINO FP16, CPU 44
Apple M4, Neural Engine (seq 64 / 128 / 256) 1 909 / 829 / 275

Inside a real index, with wide batches, the daemon sustains ~2 160 pairs/s and creates 1 000–1 350 relations/s; at that width tokenisation, not the GPU, is the limit β€” give it threads.

How to use it

  • One passage, both entities marked in place. The model reads context; two bare names carry little signal. Window the passage so both markers survive truncation β€” a pair whose second entity falls outside the window cannot be answered.
  • Three length buckets at batch 16: s seq 64, m 128, l 256. Attention is quadratic in length, so route short passages to short engines (on the iGPU, s is 4.6Γ— l). Padding is masked: a pair gives the same logits from any bucket that fits it.
  • tau β‰ˆ 0.7 is a reasonable start; raise it for fewer, surer edges.
import numpy as np, onnxruntime as ort
from transformers import AutoTokenizer

LABELS = ["NO_RELATION", "semantically_similar_to", "supersedes_or_conflicts", "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.")
# -> ('supersedes_or_conflicts', 0.7x)

Text goes in as an (empty query, marked passage) pair.

Not suited to: fine-grained relation ontologies; finding the entities (it classifies pairs you already have); entities more than a 256-token window apart.

How it was made

  • Backbone: hotchpotch/mMiniLMv2-L12-H384 β€” Microsoft's multilingual MiniLM v2 (12 layers Γ— 384, distilled from XLM-RoBERTa Large), a plain masked-language-model encoder with no task-specific stage in front of it β€” with four marker tokens added to its vocabulary.
  • Head: a 4-class layer over the hidden states at the [E1] and [E2] positions, concatenated β€” not over [CLS]. Direction comes free: swap the markers and the input genuinely changes.
  • Training: sequence-level distillation from an LLM teacher on relation tuples, each grounded in its passage, with a class-balanced (focal) objective; NO_RELATION is a trained class, not an inferred one.
  • FP16 engines, not INT8: this architecture's activation outliers make INT8 calibration lossy.

Files

file what it is
model.onnx + model.onnx.data FP32 ONNX β€” the build source and the standalone path
model.safetensors, config.json the same weights: encoder as roberta.*, head as classifier.weight [4, 768] + classifier.bias (see below); the Apple (MLX) build is prepared from this pair
tokenizer.json, tokenizer_config.json, sentencepiece.bpe.model XLM-R tokenizer with the four marker tokens
code-daemon-relation-v1-{s,m,l}_{win_x64,linux_x64}_trt11.3_sm_{75,80,86,89,90,120}.engine TensorRT FP16 per bucket Γ— OS Γ— GPU; sm_90 (H100/H200) Linux only. The set is filled per architecture as each is built β€” the file list is the truth
code-daemon-relation-v1-{s,m,l}_ov2026.4_{cpu,igpu}_fp16_b16_s{64,128,256}.{xml,bin} OpenVINO 2026.4 FP16, Intel CPU / iGPU
coreml_ane/embed.mlpackage/ Core ML for the Apple Neural Engine, functions b16 s64 / s128 / s256; load with cpuAndNeuralEngine

⚠ The head is not the stock transformers one β€” AutoModelForSequenceClassification would initialise its own and ignore this. Load the encoder with AutoModel, mean-pool the last hidden state over the tokens equal to each marker id (config.json β†’ ultracode.entity_marker_ids), concatenate the two and apply the classifier; that matches the ONNX to 3e-6.

License & attribution

Released under the MIT license.

Backbone: hotchpotch/mMiniLMv2-L12-H384 (MIT), Microsoft's Multilingual-MiniLM-L12-H384 distilled from XLM-RoBERTa Large. Used by the Code-Daemon code assistant, though nothing about the model is specific to it.

Downloads last month
107
Safetensors
Model size
0.1B params
Tensor type
F32
Β·
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support

Model tree for faxenoff/code-daemon-relation-v1

Quantized
(2)
this model