Sentence embeddings from C
The Kwker Runtime (libkwker_rt) runs KwkEncoder's models from any program that can call C. Give it a model folder,
such as a downloaded sentence-transformers model. It reads the tokenizer, the weights and the pooling settings, and turns
texts into embedding vectors: the same vectors sentence-transformers computes in Python.
It loads BERT, RoBERTa, XLM-RoBERTa, CamemBERT, DistilBERT, MPNet, ModernBERT, NomicBERT and EuroBERT models, and the embedding models built on them, such as all-MiniLM-L6-v2, all-mpnet-base-v2, BGE, E5, GTE and nomic-embed-text-v1.5 - multilingual ones too, such as multilingual-e5 and paraphrase-multilingual-MiniLM-L12-v2.
What you need
- The library and its header, as in Run a language model from C.
- A model folder with
config.json,tokenizer.jsonandmodel.safetensors, for example a downloaded Hugging Face snapshot ofsentence-transformers/all-MiniLM-L6-v2.
The program
The program loads the tokenizer and the encoder, turns every text into token ids, embeds them all with one call and prints how similar each text is to the first one (1 is identical in meaning, 0 unrelated).
// Sentence embeddings with the Kwker Runtime from C: load a sentence-transformers model folder (weights and tokenizer),
// embed a few texts and print how similar each one is to the first. No Python, no PyTorch.
//
// rt_embed <model folder> <text> <text> [<text> ...]
//
// The embeddings use the folder's own pooling and normalization, as sentence-transformers does.
//
// Build: cc rt_embed.c -I<kwker include dir> -L<lib dir> -lkwker_rt -lm -o rt_embed
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include "kwker_rt.h"
int main(int argc, char** argv) {
if (argc < 4) {
fprintf(stderr, "usage: %s <model folder> <text> <text> [<text> ...]\n", argv[0]);
return 2;
}
const char* dir = argv[1];
const int64_t n = argc - 2;
kwker_rt_tokenizer* tok = NULL;
kwker_rt_encoder* enc = NULL;
if (kwker_rt_tokenizer_load(dir, &tok) != 0) {
fprintf(stderr, "tokenizer: %s\n", kwker_rt_tokenizer_error());
return 1;
}
if (kwker_rt_encoder_load(dir, "float32", &enc) != 0) {
fprintf(stderr, "encoder: %s\n", kwker_rt_last_error());
return 1;
}
const int64_t hidden = kwker_rt_encoder_hidden(enc);
// Token ids for every text, with the model's start and end tokens; rows padded to the longest one.
int64_t* lens = malloc(sizeof(int64_t) * n);
int64_t longest = 0;
for (int64_t i = 0; i < n; i++) {
kwker_rt_tokenizer_encode(tok, argv[2 + i], -1, 1, NULL, 0, &lens[i]);
if (lens[i] > longest) longest = lens[i];
}
int64_t* ids = calloc((size_t)(n * longest), sizeof(int64_t));
for (int64_t i = 0; i < n; i++) kwker_rt_tokenizer_encode(tok, argv[2 + i], -1, 1, ids + i * longest, longest, &lens[i]);
// One call embeds them all: n rows of hidden floats.
float* emb = malloc(sizeof(float) * (size_t)(n * hidden));
if (kwker_rt_encoder_embed(enc, n, longest, ids, lens, emb, 0) != 0) {
fprintf(stderr, "embed: %s\n", kwker_rt_last_error());
return 1;
}
// Cosine similarity of each text to the first.
for (int64_t i = 1; i < n; i++) {
double dot = 0, a = 0, b = 0;
for (int64_t j = 0; j < hidden; j++) {
dot += (double)emb[j] * emb[i * hidden + j];
a += (double)emb[j] * emb[j];
b += (double)emb[i * hidden + j] * emb[i * hidden + j];
}
printf("%.4f %s\n", dot / sqrt(a * b), argv[2 + i]);
}
kwker_rt_encoder_free(enc);
kwker_rt_tokenizer_free(tok);
free(emb);
free(ids);
free(lens);
return 0;
}
Build it against the library, then run it on a model folder with a few texts:
cc rt_embed.c -I<include dir> -L<library dir> -lkwker_rt -lm -Wl,-rpath,<library dir> -o rt_embed
./rt_embed ~/models/all-MiniLM-L6-v2 "How do I bake bread?" "A recipe for sourdough loaves" "The stock market fell today"
It prints each text's similarity to the first:
0.5845 A recipe for sourdough loaves
0.0645 The stock market fell today
The recipe is close in meaning to the question about bread; the stock market is not.
The calls
| Call | What it does |
|---|---|
kwker_ |
Load a model folder. precision is "float32", "bf16" (faster, slightly less exact) or "int8" (faster still; check the results on your data). |
kwker_ |
Embed n texts given as rows of len token ids; row i holds lens[i] real tokens, the rest is ignored. Writes n rows of hidden floats. threads 0 uses every core. |
kwker_ |
The model's last hidden state instead: n x len rows of hidden floats, rows past a text's length zero. types is NULL or the token type ids. |
kwker_ |
The length of one embedding. |
kwker_ |
Release the encoder. |
kwker_ |
The reason the last encoder call on this thread returned -1. |
The tokenizer calls are listed in Run a language model from C.
Notes
- The pooling follows the folder's sentence-transformers settings (
modules.jsonand1_Pooling/config.json): mean, first token (CLS), max or last token, then normalization when the folder has a Normalize step. A folder withoutmodules.jsongets mean pooling. Other steps, such as an extra Dense layer, are refused bykwker_rt_encoder_embedwith a message. "float32"keeps the checkpoint's weights exactly;"bf16"and"int8"match KwkEncoder's results in Python.- On CPUs with AMX (Intel Xeon 4th generation, Sapphire Rapids, and later)
"bf16"and"int8"run on the AMX units, as KwkEncoder does.KWKER_AMX=0turns that off. - ModernBERT, NomicBERT and EuroBERT models load in
"float32"or"bf16".
Related
- Run a language model from C: the decoder and the tokenizer from C.
- ONNX models: the same encoders inside ONNX Runtime.