Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ All notable changes to models are documented here.

## [Unreleased]

### Added

- Added an embedding equivalence gate to `models-bench`, run with
`embedding-equivalence --model <artifact.gguf>`. It tests that Models produces
the same vectors as llama.cpp, for eight pinned probes over the same model
bytes, exiting non-zero when they diverge. The reference vectors are
committed, so it runs in seconds and needs no local llama.cpp build.

Agreement is gated at 0.999 cosine, where a correct run measures 0.99950 and
mean pooling in place of last-token measures 0.66156. Vector length is gated
separately at 1e-3: cosine is scale-invariant, so a runtime that skips L2
normalization agrees with a normalized reference at exactly 1.0.

## [0.3.0] - 2026-08-05

### Added
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ Implemented functionality includes:
- byte-level BPE and Llama SentencePiece tokenizers
- grouped-query attention, RoPE, SwiGLU, KV caching, and autoregressive decode
- greedy, temperature, top-k, top-p, and repetition-penalty sampling
- tool calling across Qwen, Hermes, Llama 3, Gemma 4, and MiniCPM5 formats
- in-JVM text embeddings with last-token and mean pooling, tested to produce the
same vectors as llama.cpp
- plain Java, LangChain4j, Spring AI, and Spring Boot integrations
- Apple Foundation Models on supported Apple Silicon Macs
- framework-neutral guarded RAG
Expand Down
67 changes: 67 additions & 0 deletions benchmark-results/embedding/qwen3-embedding-0.6b-q8_0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"workload" : "oracle-equivalence-v1",
"model" : "Qwen3-Embedding-0.6B GGUF Q8_0",
"backend" : "pure-java",
"artifactSha256" : "06507c7b42688469c4e7298b0a1e16deff06caf291cf0a5b278c308249c3e439",
"artifactSizeBytes" : 639150592,
"probeSetSha256" : "a25952a4cb80e57c099291dda9b44ad2b713c0688c386015a1e0000bf6fbf8b9",
"probes" : 8,
"embeddingDimension" : 1024,
"pooling" : "last-token",
"normalized" : true,
"oracleBackend" : "llama.cpp",
"oracleVersion" : "6ea215d17",
"minimumOracleCosine" : 0.9995014497617521,
"meanOracleCosine" : 0.999646999958326,
"maxComponentDelta" : 0.005263041704893112,
"maxNormDeviation" : 2.73471778555745E-9,
"minimumOracleCosineFloor" : 0.999,
"maxNormDeviationFloor" : 0.001,
"normalizationHeld" : true,
"qualified" : true,
"perProbe" : [ {
"probe" : "The cat sat on the mat.",
"cosine" : 0.9996153359277914,
"embedMillis" : 771.153635
}, {
"probe" : "How do I reset my password?",
"cosine" : 0.9995014497617521,
"embedMillis" : 657.486327
}, {
"probe" : "Quantum chromodynamics describes the strong interaction.",
"cosine" : 0.9996144549341472,
"embedMillis" : 739.567641
}, {
"probe" : "a",
"cosine" : 0.9998283165348626,
"embedMillis" : 134.151371
}, {
"probe" : "The quick brown fox jumps over the lazy dog near the riverbank at dawn while birds sing overhead.",
"cosine" : 0.9997038948483886,
"embedMillis" : 1581.679878
}, {
"probe" : "Database backups are retained for 30 days.",
"cosine" : 0.9996728657361779,
"embedMillis" : 766.542997
}, {
"probe" : "def add(a, b): return a + b",
"cosine" : 0.999726416583442,
"embedMillis" : 765.948487
}, {
"probe" : "¿Dónde está la biblioteca?",
"cosine" : 0.9995132653400448,
"embedMillis" : 687.01172
} ],
"environment" : {
"host" : "rockhopper",
"osName" : "Linux",
"osVersion" : "7.0.11-76070011-generic",
"architecture" : "amd64",
"cpuModel" : "Intel(R) Core(TM) i9-14900HX",
"processors" : 32,
"physicalMemoryBytes" : 101073932288,
"javaVersion" : "25.0.3",
"javaVendor" : "Azul Systems, Inc.",
"vmName" : "OpenJDK 64-Bit Server VM"
}
}
1 change: 1 addition & 0 deletions docs/content/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* xref:spring-boot.adoc[Spring Boot]

.Retrieval and Grounding
* xref:embeddings.adoc[Embeddings]
* xref:vectors.adoc[Vectors]
* xref:rag.adoc[RAG Validation]

Expand Down
155 changes: 155 additions & 0 deletions docs/content/modules/ROOT/pages/embeddings.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
= Embeddings
:navtitle: Embeddings

Models turns text into vectors in the JVM, from the same GGUF artifacts it
generates from.

An embedding model runs the decoder stack, stops before the vocabulary
projection, reduces the per-position hidden states to one vector, and scales it
to unit length.

== Producing A Backend

`GgufEmbeddingBackend` implements `EmbeddingBackend` over a loaded GGUF model:

[source,kotlin,subs="attributes+"]
----
dependencies {
implementation("com.integrallis:models-api:{models-version}")
implementation("com.integrallis:backend-java:{models-version}")
}
----

[source,java]
----
import com.integrallis.models.api.EmbeddingBackend;
import com.integrallis.models.api.Pooling;
import com.integrallis.models.backend.purejava.GgufEmbeddingBackend;
import com.integrallis.models.backend.purejava.PureJavaBackend;
import java.nio.file.Path;
import java.util.List;

static void embed(Path gguf) {
try (PureJavaBackend backend = PureJavaBackend.load(gguf);
EmbeddingBackend embeddings =
GgufEmbeddingBackend.builder(backend)
.pooling(Pooling.LAST_TOKEN)
.normalize(true)
.build()) {
float[] vector = embeddings.embed("How do I reset my password?");
float[][] batch = embeddings.embedAll(
List.of("Database backups are retained for 30 days.",
"Rotate credentials every 90 days."));
}
}
----

`EmbeddingBackend` lives in `models-api`, so a backend implements it without
inheriting a dependency on Vectors storage.

Resolve the artifact through its marker JAR:

[source,java]
----
import org.modeljars.ModelJarLocator;
import org.modeljars.ModelJarRegistry;

Path gguf = new ModelJarLocator(ModelJarRegistry.fromClasspath())
.requireLocalPath(MODEL);
----

== Pooling

Pooling reduces one hidden state per token to one vector per text. The correct
choice follows from how the model was trained.

`Pooling.LAST_TOKEN`:: The default. Causal decoder-only embedders such as the
Qwen3-Embedding family, where attention is one-directional and only the final
position has seen the whole input.

`Pooling.MEAN`:: Bidirectional encoders, where every position has seen the whole
input.

Mean pooling on a last-token model returns vectors that agree with the correct
ones at 0.66 cosine. Take the pooling the model card specifies.

`normalize(true)` is also the default. Unit vectors let a bare dot product stand
in for cosine similarity.

== Framework Adapters

For LangChain4j, `ModelsEmbeddingModel` adapts an `EmbeddingBackend` to
`EmbeddingModel`:

[source,java]
----
import com.integrallis.models.langchain4j.ModelsEmbeddingModel;

try (var model = new ModelsEmbeddingModel(embeddings)) {
Response<List<Embedding>> response =
model.embedAll(List.of(TextSegment.from("a document")));
}
----

For Spring AI, `ModelsSpringAiEmbeddingModel` adapts the same backend:

[source,java]
----
import com.integrallis.models.spring.ai.ModelsSpringAiEmbeddingModel;

try (var model = new ModelsSpringAiEmbeddingModel(embeddings)) {
float[] vector = model.embed("How do I reset my password?");
}
----

Both adapters own the backend they wrap and close it. To store the vectors, see
xref:vectors.adoc[Vectors].

== Equivalence Gating

We test that Models produces the same vectors as llama.cpp. `models-bench`
embeds a pinned probe set and compares the result against a pinned llama.cpp
build over the same model bytes:

[source,bash]
----
./gradlew :models-bench:run --args="embedding-equivalence \
--model /path/to/model.gguf \
--report benchmark-results/embedding/model.json"
----

Exit `0` reproduced, `1` not reproduced, `2` usage or integrity problem. It runs
in seconds: the reference vectors are committed, so the gate needs no local
llama.cpp build.

Eight probes cover single token, long input, non-Latin script, code, rare
tokens, and ordinary prose. The gate takes the worst probe.

Two floors, both placed against measurements:

[cols="3,1,1", options="header"]
|===
| Run | Min cosine | Max abs(norm - 1)

| Correct runtime against the reference
| 0.99950
| 2.7e-09

| Mean pooling instead of last-token
| 0.66156
| —

| L2 normalization skipped
| *1.00000*
| ~11

| _floor_
| _0.999_
| _1e-3_
|===

Cosine is scale-invariant, so a runtime that skips L2 normalization agrees with
a normalized reference at exactly 1.0, and vector length is checked separately.

The gate verifies the probe-set and artifact digests before comparing, and
exits `2` when either has moved.
5 changes: 5 additions & 0 deletions docs/content/modules/ROOT/pages/langchain4j.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,17 @@ dependencies {
}
----

`ModelsEmbeddingModel` supplies the `EmbeddingModel`. See
xref:embeddings.adoc[Embeddings] for producing the backend it wraps.

[source,java]
----
interface Assistant {
String answer(String question);
}

var embeddingModel = new ModelsEmbeddingModel(embeddingBackend);

var collection = VectorCollection.builder()
.dimension(embeddingModel.dimension())
.metric(SimilarityFunction.COSINE)
Expand Down
5 changes: 5 additions & 0 deletions docs/content/modules/ROOT/pages/model-support.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ correctness and latency gates on a recorded runtime and host.

A model family name alone is not a production qualification.

Generators are graded on how well they answer. For embedders we test that
Models produces the same vectors as llama.cpp, since retrieval quality is a
published property of the weights. See
xref:embeddings.adoc#equivalence-gating[Equivalence Gating].

== Supported Runtime Surface

The decoder currently recognizes `llama`, `qwen2`, `qwen3`, and `gemma4` GGUF
Expand Down
20 changes: 20 additions & 0 deletions docs/content/modules/ROOT/pages/spring-ai.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,26 @@ application or Spring bean lifecycle owns it. Apple's system model implements
the same contract, but currently emits its complete response as one streaming
element because Apple's API returns a complete response to the bridge.

== Embeddings

`ModelsSpringAiEmbeddingModel` adapts a Models `EmbeddingBackend` to Spring
AI's `EmbeddingModel`.

[source,java]
----
import com.integrallis.models.api.EmbeddingBackend;
import com.integrallis.models.spring.ai.ModelsSpringAiEmbeddingModel;
import org.springframework.ai.embedding.EmbeddingModel;

@Bean(destroyMethod = "close")
EmbeddingModel localEmbeddingModel(EmbeddingBackend backend) {
return new ModelsSpringAiEmbeddingModel(backend);
}
----

This adapter owns the backend it wraps and closes it. See
xref:embeddings.adoc[Embeddings] for producing the backend.

For auto-configuration, named-bean behavior, and combined Models/Vectors setup,
continue with xref:spring-boot.adoc[Spring Boot]. For validation after
retrieval, see xref:rag.adoc[RAG Validation].
2 changes: 1 addition & 1 deletion docs/content/modules/ROOT/pages/spring-boot.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ model.stream(new Prompt("List the retrieved facts."))
----

Streaming has the same ordering and error semantics described in
xref:spring-ai.adoc#_streaming_chat[Spring AI streaming]. Blocking model
xref:spring-ai.adoc#streaming-chat[Spring AI streaming]. Blocking model
inference runs off the Reactor subscriber thread.

== Add Vectors for RAG
Expand Down
15 changes: 15 additions & 0 deletions docs/content/modules/ROOT/pages/testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,21 @@ Set `RAG_MODELS_BACKEND` to `pure-java` or `rust-ffm`; do not put
The controlled inference script is a performance measurement harness. It does
not replace the default-correctness gate in the RAG qualification script.

== Embedding Equivalence

We test that Models produces the same vectors as llama.cpp:

[source,bash]
----
./gradlew :models-bench:run --args="embedding-equivalence \
--model /path/to/model.gguf \
--report benchmark-results/embedding/model.json"
----

Exit `0` reproduced, `1` not reproduced, `2` usage or integrity problem. It
runs in seconds: the llama.cpp reference vectors are committed. See
xref:embeddings.adoc#equivalence-gating[Equivalence Gating] for the floors.

Acceptance policy and reproduction details live in:

* {url-models-github}/blob/main/INFERENCE_BENCHMARKS.md[Controlled inference benchmarks^]
Expand Down
9 changes: 6 additions & 3 deletions docs/content/modules/ROOT/pages/vectors.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ core runtime.

For LangChain4j, `vectors-langchain4j` implements
`EmbeddingStore<TextSegment>` while `models-langchain4j` implements the chat
model. See xref:langchain4j.adoc#_rag_with_vectors[LangChain4j RAG with
model. See xref:langchain4j.adoc#rag-with-vectors[LangChain4j RAG with
Vectors].

For Spring Boot, `vectors-spring-boot-starter` contributes a Spring AI
`VectorStore` and `models-spring-boot-starter` contributes the named local
`ChatModel`. See xref:spring-boot.adoc#_add_vectors_for_rag[Spring Boot with
`ChatModel`. See xref:spring-boot.adoc#add-vectors-for-rag[Spring Boot with
Vectors].

== Direct Embedding Storage
Expand All @@ -30,14 +30,17 @@ dependencies {
}
----

See xref:embeddings.adoc[Embeddings] for producing the `EmbeddingBackend`
these examples take as a parameter.

The backend and collection must use the same vector dimension. The sink owns
and closes the embedding backend; the application retains ownership of the
collection. Batch documents, commit once, and embed the query through the same
backend:

[source,java]
----
import com.integrallis.models.embedding.EmbeddingBackend;
import com.integrallis.models.api.EmbeddingBackend;
import com.integrallis.models.embedding.VectorCollectionEmbeddingSink;
import com.integrallis.vectors.db.SearchRequest;
import com.integrallis.vectors.db.VectorCollection;
Expand Down
8 changes: 8 additions & 0 deletions docs/landing/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,14 @@ <h3 class="card-title">Qualified GGUF runtime</h3>
<h3 class="card-title">Framework adapters</h3>
<p class="card-description">Blocking and streaming APIs for plain Java, LangChain4j, Spring AI, and Spring Boot applications.</p>
</article>
<article class="card reveal">
<h3 class="card-title">In-JVM embeddings</h3>
<p class="card-description">Text to vectors from the same GGUF artifacts, tested to match llama.cpp at 0.999 cosine or better.</p>
</article>
<article class="card reveal">
<h3 class="card-title">Tool calling</h3>
<p class="card-description">Qwen, Hermes, Llama 3, Gemma 4, and MiniCPM5 call formats behind one contract, with argument text carried verbatim.</p>
</article>
<article class="card reveal">
<h3 class="card-title">Guarded RAG</h3>
<p class="card-description">Retrieval abstention, trusted citations, unsupported-claim detection, and deterministic extractive fallback.</p>
Expand Down
Loading
Loading