summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ant-build.yaml63
-rw-r--r--docker/Dockerfile22
-rw-r--r--htroot/IndexImportZim_p.html53
-rw-r--r--htroot/env/templates/submenuIndexImport.template1
-rw-r--r--ivy.xml2
-rw-r--r--source/net/yacy/ai/OllamaClient.java26
-rw-r--r--source/net/yacy/ai/OpenAIClient.java128
-rw-r--r--source/net/yacy/ai/llama3/ChatFormat.java130
-rw-r--r--source/net/yacy/ai/llama3/Context.java61
-rw-r--r--source/net/yacy/ai/llama3/Llama.java427
-rw-r--r--source/net/yacy/ai/llama3/Llama3.java155
-rw-r--r--source/net/yacy/ai/llama3/Model/Arch.java31
-rw-r--r--source/net/yacy/ai/llama3/Model/GGMLTensorEntry.java97
-rw-r--r--source/net/yacy/ai/llama3/Model/GGMLType.java99
-rw-r--r--source/net/yacy/ai/llama3/Model/GGUF.java645
-rw-r--r--source/net/yacy/ai/llama3/Model/ModelLoader.java333
-rw-r--r--source/net/yacy/ai/llama3/Model/Pair.java59
-rw-r--r--source/net/yacy/ai/llama3/Model/Tokenizer.java295
-rw-r--r--source/net/yacy/ai/llama3/Model/Vocabulary.java65
-rw-r--r--source/net/yacy/ai/llama3/Sampler.java175
-rw-r--r--source/net/yacy/ai/llama3/Tensor/ArrayFloatTensor.java177
-rw-r--r--source/net/yacy/ai/llama3/Tensor/BF16FloatTensor.java74
-rw-r--r--source/net/yacy/ai/llama3/Tensor/DirectBufferFloatTensor.java111
-rw-r--r--source/net/yacy/ai/llama3/Tensor/F16FloatTensor.java75
-rw-r--r--source/net/yacy/ai/llama3/Tensor/FloatTensor.java286
-rw-r--r--source/net/yacy/ai/llama3/Tensor/Q4_0FloatTensor.java219
-rw-r--r--source/net/yacy/ai/llama3/Tensor/Q8_0FloatTensor.java134
-rw-r--r--source/net/yacy/cora/document/id/MultiProtocolURL.java1
-rw-r--r--source/net/yacy/cora/util/Html2Image.java3
-rw-r--r--source/net/yacy/gui/framework/Browser.java2
-rw-r--r--source/net/yacy/htroot/IndexImportZim_p.java85
-rw-r--r--source/net/yacy/http/servlets/RAGProxyServlet.java6
-rw-r--r--source/net/yacy/kelondro/logging/ThreadDump.java1
-rw-r--r--source/net/yacy/kelondro/util/FileUtils.java1
-rw-r--r--source/net/yacy/server/http/HTTPDProxyHandler.java1
35 files changed, 3981 insertions, 62 deletions
diff --git a/.github/workflows/ant-build.yaml b/.github/workflows/ant-build.yaml
new file mode 100644
index 000000000..35cb574c9
--- /dev/null
+++ b/.github/workflows/ant-build.yaml
@@ -0,0 +1,63 @@
+name: YaCy CI/CD Pipeline
+
+on:
+ push:
+ branches: [ "master" ]
+ pull_request:
+ branches: [ "master" ]
+ workflow_dispatch:
+
+jobs:
+ build-and-release:
+ runs-on: ubuntu-latest
+ env:
+ RELEASE_DIR: RELEASE
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Required for git version info in build
+
+ - name: Set up JDK 11
+ uses: actions/setup-java@v3
+ with:
+ java-version: '11'
+ distribution: 'temurin'
+
+ - name: Install build dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -yq ant git wkhtmltopdf imagemagick xvfb ghostscript
+
+ - name: Configure runtime settings
+ run: |
+ sed -i "/adminAccountBase64MD5=/c\adminAccountBase64MD5=MD5:8cffbc0d66567a0987a4aba1ec46d63c" defaults/yacy.init
+ sed -i "/adminAccountForLocalhost=/c\adminAccountForLocalhost=false" defaults/yacy.init
+ sed -i "/server.https=false/c\server.https=true" defaults/yacy.init
+
+ - name: Build with Ant
+ run: ant clean all dist
+
+ - name: Verify tarball creation
+ run: |
+ ls -la ${{ env.RELEASE_DIR }}/
+ tar -ztvf ${{ env.RELEASE_DIR }}/yacy_*.tar.gz | head
+
+ - name: Upload build artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: yacy-release
+ path: ${{ env.RELEASE_DIR }}/yacy_*.tar.gz
+ retention-days: 5
+
+ - name: Create GitHub Release (on tag)
+ if: startsWith(github.ref, 'refs/tags/v')
+ uses: softprops/action-gh-release@v1
+ with:
+ files: ${{ env.RELEASE_DIR }}/yacy_*.tar.gz
+ body: |
+ YaCy Search Server Release
+ Built from ${{ github.sha }}
+ Version: ${{ github.ref_name }}
+ draft: false
+ prerelease: false
diff --git a/docker/Dockerfile b/docker/Dockerfile
index ca201b1f7..3967b0728 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -6,17 +6,14 @@
# run with
# docker run -d --name yacy -p 8090:8090 -p 8443:8443 -v yacy_data:/opt/yacy_search_server/DATA --log-opt max-size=200m --log-opt max-file=2 yacy/yacy_search_server:latest
+
## builder image
-FROM eclipse-temurin:21-jdk AS builder
-RUN apt-get update && apt-get install -y --no-install-recommends \
- ant \
- git \
- curl \
- && rm -rf /var/lib/apt/lists/*
+FROM eclipse-temurin:24-jdk-noble AS builder
+RUN apt-get update && apt-get install -y --no-install-recommends ant git curl && rm -rf /var/lib/apt/lists/*
+# compile YaCy
WORKDIR /opt
COPY . /opt/yacy_search_server/
-
RUN ant compile -f /opt/yacy_search_server/build.xml
# Set initial admin password: "yacy" (encoded with custom yacy md5 function net.yacy.cora.order.Digest.encodeMD5Hex())
@@ -26,16 +23,11 @@ RUN sed -i "/adminAccountBase64MD5=/c\adminAccountBase64MD5=MD5:8cffbc0d66567a09
sed -i "/server.https=false/c\server.https=true" /opt/yacy_search_server/defaults/yacy.init
## build final image
-FROM eclipse-temurin:21-jre AS app
-
-RUN apt-get update && apt-get install -y --no-install-recommends \
- wkhtmltopdf \
- xvfb \
- ghostscript \
- && rm -rf /var/lib/apt/lists/*
-
+FROM eclipse-temurin:24-jdk-noble AS app
+RUN apt-get update && apt-get install -y --no-install-recommends wkhtmltopdf xvfb ghostscript && rm -rf /var/lib/apt/lists/*
LABEL maintainer="Michael Peter Christen <mc@yacy.net>"
+# copy YaCy to app image
RUN adduser --system --group --no-create-home --disabled-password yacy
WORKDIR /opt
COPY --chown=yacy:yacy --from=builder /opt/yacy_search_server /opt/yacy_search_server
diff --git a/htroot/IndexImportZim_p.html b/htroot/IndexImportZim_p.html
new file mode 100644
index 000000000..68d74762e
--- /dev/null
+++ b/htroot/IndexImportZim_p.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>YaCy '#[clientname]#': ZIM File Import</title>
+ #%env/templates/metas.template%#
+ #(import)#::<meta http-equiv="REFRESH" content="10;url=IndexImportZim_p.html" />
+ <!-- the url= removes http get parameters on refresh, preventing restart of import -->
+ #(/import)#
+ </head>
+ <body id="IndexImportZim">
+ #%env/templates/header.template%#
+ #%env/templates/submenuIndexImport.template%#
+ <h2>ZIM File Import</h2>
+
+ #(import)#
+ <p>No import thread is running, you can start a new thread here</p>
+ <form action="IndexImportZim_p.html" method="post" enctype="multipart/form-data" accept-charset="UTF-8">
+ <!-- no post method here, we don't want to transmit the whole file, only the path-->
+ <fieldset>
+ <legend>Zim File Selection: select a '.zim' file</legend>
+ <p>
+ You can download ZIM files for example here
+ <a href="https://download.kiwix.org/zim/" target="_blank">Kiwix ZIM Archive</a>.
+ </p>
+ <dl>
+ <dt class="TableCellDark"><label for="file">File:</label></dt>
+ <dd><input name="file" id="file" type="file" value="" size="75" /></dd>
+ <dt></dt>
+ <dd><input name="submit" class="btn btn-primary" type="submit" value="Import ZIM File" /></dd>
+ </dl>
+ </fieldset>
+ </form>
+
+ <br />
+ ::
+ <form>
+ <fieldset><legend>Import Process</legend>
+ <dl>
+ <dt>Thread:</dt><dd>#[thread]#</dd>
+ <dt>ZIM File:</dt><dd>#[zimfile]#</dd>
+ <dt>Processed:</dt><dd>#[count]# Entries</dd>
+ <dt>Speed:</dt><dd>#[speed]# pages per second</dd>
+ <dt>Running Time:</dt><dd>#[runningHours]# hours, #[runningMinutes]# minutes</dd>
+ <dt>Remaining Time:</dt><dd>#[remainingHours]# hours, #[remainingMinutes]# minutes</dd>
+ </dl>
+ </fieldset>
+ <input name="abort" type="submit" class="btn btn-danger" value="Stop"/>
+ </form>
+ #(/import)#
+
+ #%env/templates/footer.template%#
+ </body>
+</html> \ No newline at end of file
diff --git a/htroot/env/templates/submenuIndexImport.template b/htroot/env/templates/submenuIndexImport.template
index 39a1029a3..ccbc49412 100644
--- a/htroot/env/templates/submenuIndexImport.template
+++ b/htroot/env/templates/submenuIndexImport.template
@@ -14,6 +14,7 @@
<li><a href="Load_RSS_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">RSS Feed Importer</a></li>
<li><a href="IndexImportOAIPMH_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">OAI-PMH Importer</a></li>
<li><a href="IndexImportWarc_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">Warc Importer</a></li>
+ <li><a href="IndexImportZim_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">Zim Importer</a></li>
<li><a href="IndexImportJsonList_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">JsonList Importer</a></li>
</ul>
</div>
diff --git a/ivy.xml b/ivy.xml
index e3a3aa2d6..2ba8dbdc5 100644
--- a/ivy.xml
+++ b/ivy.xml
@@ -15,7 +15,7 @@
<dependency org="com.drewnoakes" name="metadata-extractor" rev="2.19.0" />
<dependency org="com.fasterxml.jackson.core" name="jackson-databind" rev="2.18.2"/>
<dependency org="com.github.ben-manes.caffeine" name="caffeine" rev="3.1.8"/>
- <dependency org="com.google.guava" name="guava" rev="33.3.1-jre" conf="compile->master"/>
+ <dependency org="com.google.guava" name="guava" rev="33.4.8-jre" conf="compile->master"/>
<dependency org="com.google.guava" name="failureaccess" rev="1.0.2" />
<dependency org="com.ibm.icu" name="icu4j" rev="76.1"/>
<dependency org="com.github.mwiede" name="jsch" rev="0.2.21" />
diff --git a/source/net/yacy/ai/OllamaClient.java b/source/net/yacy/ai/OllamaClient.java
index 9eaba920b..53aa66742 100644
--- a/source/net/yacy/ai/OllamaClient.java
+++ b/source/net/yacy/ai/OllamaClient.java
@@ -32,15 +32,17 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
-public class OllamaClient {
+public class OllamaClient extends OpenAIClient {
public static String OLLAMA_API_HOST = "http://localhost:11434";
- private final String hoststub;
-
public OllamaClient(final String hoststub) {
- this.hoststub = hoststub;
+ super(hoststub);
}
+
+ public String getHoststub() {
+ return this.hoststub;
+ }
public LinkedHashMap<String, Long> listOllamaModels() {
final LinkedHashMap<String, Long> sortedMap = new LinkedHashMap<>();
@@ -95,7 +97,7 @@ public class OllamaClient {
return false;
}
}
-
+
public static void main(final String[] args) {
final OllamaClient oc = new OllamaClient(OLLAMA_API_HOST);
@@ -103,7 +105,7 @@ public class OllamaClient {
System.out.println(models.toString());
// check if model exists
- final String model = "phi3:3.8b";
+ final String model = "qwen2.5:0.5b";
if (oc.ollamaModelExists(model))
System.out.println("model " + model + " exists");
else
@@ -111,7 +113,17 @@ public class OllamaClient {
// pull a model
final boolean success = oc.pullOllamaModel(model);
- System.out.println("pulled model + " + model + ": " + success);
+ System.out.println("pulled model: " + model + ": " + success);
+
+ String response;
+ try {
+ response = oc.chat(model, "You are a helpful assistant.", "What is the capital of France?", 1000);
+ System.out.println("Chat response: " + response);
+ } catch (IOException e) {
+
+ e.printStackTrace();
+ }
}
+
}
diff --git a/source/net/yacy/ai/OpenAIClient.java b/source/net/yacy/ai/OpenAIClient.java
index f068eadd2..cee112495 100644
--- a/source/net/yacy/ai/OpenAIClient.java
+++ b/source/net/yacy/ai/OpenAIClient.java
@@ -28,6 +28,9 @@ import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
import org.json.JSONArray;
import org.json.JSONException;
@@ -38,7 +41,7 @@ public class OpenAIClient {
private static String[] STOPTOKENS = new String[]{"[/INST]", "<|im_end|>", "<|end_of_turn|>", "<|eot_id|>", "<|end_header_id|>", "<EOS_TOKEN>", "</s>", "<|end|>"};
- private final String hoststub;
+ protected final String hoststub;
public OpenAIClient(final String hoststub) {
this.hoststub = hoststub;
@@ -93,27 +96,57 @@ public class OpenAIClient {
throw new IOException("Request failed with response code " + responseCode);
}
}
+
+ public static class Context extends JSONArray {
+ public Context(String systemPrompt) throws JSONException {
+ super();
+ final JSONObject systemPromptObject = new JSONObject(true);
+ systemPromptObject.put("role", "system");
+ systemPromptObject.put("content", systemPrompt);
+ this.put(systemPromptObject);
+ }
+ public void addDialog(String user, String assistant) throws JSONException {
+ final JSONObject userPromptObject = new JSONObject(true);
+ userPromptObject.put("role", "user");
+ userPromptObject.put("content", user);
+ this.put(userPromptObject);
+ final JSONObject assistantPromptObject = new JSONObject(true);
+ assistantPromptObject.put("role", "assistant");
+ assistantPromptObject.put("content", assistant);
+ this.put(assistantPromptObject);
+ }
+ public void addPrompt(String userPrompt) throws JSONException {
+ final JSONObject userPromptObject = new JSONObject(true);
+ userPromptObject.put("role", "user");
+ userPromptObject.put("content", userPrompt);
+ this.put(userPromptObject);
+ }
+ }
// OpenAI chat client, works with llama.cpp and Ollama
- public String chat(final String model, final String prompt, final int max_tokens) throws IOException {
+ public String chat(final String model, final Context context, JSONObject schema, final int max_tokens) throws IOException {
final JSONObject data = new JSONObject();
- final JSONArray messages = new JSONArray();
- final JSONObject systemPrompt = new JSONObject(true);
- final JSONObject userPrompt = new JSONObject(true);
- messages.put(systemPrompt);
- messages.put(userPrompt);
+
try {
- systemPrompt.put("role", "system");
- systemPrompt.put("content", "Make short answers.");
- userPrompt.put("role", "user");
- userPrompt.put("content", prompt);
data.put("model", model);
data.put("temperature", 0.1);
data.put("max_tokens", max_tokens);
- data.put("messages", messages);
+ data.put("messages", context);
data.put("stop", new JSONArray(STOPTOKENS));
data.put("stream", false);
+
+ if (schema != null) {
+ System.out.println(schema.toString());
+ JSONObject json_schema = new JSONObject(true);
+ json_schema.put("strict", true);
+ json_schema.put("schema", schema);
+ JSONObject response_format = new JSONObject();
+ response_format.put("type", "json_schema");
+ response_format.put("json_schema", json_schema);
+ data.put("response_format", response_format);
+ }
+
final String response = sendPostRequest(this.hoststub + "/v1/chat/completions", data);
final JSONObject responseObject = new JSONObject(response);
final JSONArray choices = responseObject.getJSONArray("choices");
@@ -125,28 +158,63 @@ public class OpenAIClient {
throw new IOException(e.getMessage());
}
}
-
- public static String[] stringsFromChat(final String answer) {
- final int p = answer.indexOf('[');
- final int q = answer.indexOf(']');
- if (p < 0 || q < 0 || q < p) return new String[0];
+
+ public String chat(final String model, final String systemPrompt, final String userPrompt, final int max_tokens) throws IOException {
try {
- final JSONArray a = new JSONArray(answer.substring(p, q + 1));
- final String[] arr = new String[a.length()];
- for (int i = 0; i < a.length(); i++) arr[i] = a.getString(i);
- return arr;
- } catch (final JSONException e) {
- return new String[0];
+ Context context = new Context(systemPrompt);
+ context.addPrompt(userPrompt);
+ return chat(model, context, null, max_tokens);
+ } catch (JSONException e) {
+ throw new IOException(e.getMessage());
}
}
+
+ public static String[] stringsFromChat(String chatanswer) throws JSONException {
+ JSONArray ja = new JSONArray(chatanswer);
+ List<String> list = new ArrayList<>();
+ // parse the JSON array and extract strings
+ for (int i = 0; i < ja.length(); i++) {
+ Object item = ja.get(i);
+ if (item instanceof String) {
+ list.add((String) item);
+ } else if (item instanceof JSONObject) {
+ JSONObject jo = (JSONObject) item;
+ String answer = jo.optString("answer", null);
+ if (answer != null) {
+ list.add(answer);
+ } else {
+ // take any string value from the object
+ for (String key : jo.keySet()) {
+ Object value = jo.optString(key, null);
+ if (value != null && value instanceof String) {
+ list.add((String) value);
+ break; // take the first string found
+ }
+ }
+ }
+ }
+ }
+ // convert the list to an array
+ String[] result = new String[list.size()];
+ return list.toArray(result);
+ }
+
+ public final static JSONObject listSchema = new JSONObject(Map.of(
+ "title", "Answer List",
+ "type", "array",
+ "properties", Map.of(
+ "answer", Map.of("type", "string")
+ ),
+ "required", List.of("answer")
+ ));
public static void main(final String[] args) {
- final String model = "phi3:3.8b";
+ final String model = "qwen2.5:0.5b";
final OpenAIClient oaic = new OpenAIClient(OllamaClient.OLLAMA_API_HOST);
// make chat completion with model
String question = "Who invented the wheel?";
try {
- final String answer = oaic.chat(model, question, 80);
+ final String answer = oaic.chat(model, "Make short answers.", question, 200);
System.out.println(answer);
} catch (final IOException e) {
e.printStackTrace();
@@ -155,9 +223,13 @@ public class OpenAIClient {
// try the json parser from chat results
question = "Make a list of four names from Star Wars movies. Use a JSON Array.";
try {
- final String[] a = stringsFromChat(oaic.chat(model, question, 80));
- for (final String s: a) System.out.println(s);
- } catch (final IOException e) {
+ Context context = new Context("Make short answers");
+ context.addPrompt(question);
+ final String[] a = stringsFromChat(oaic.chat(model, context, listSchema, 1000));
+ for (String s : a) {
+ System.out.println(s);
+ }
+ } catch (final IOException | JSONException e) {
e.printStackTrace();
}
}
diff --git a/source/net/yacy/ai/llama3/ChatFormat.java b/source/net/yacy/ai/llama3/ChatFormat.java
new file mode 100644
index 000000000..871b83d05
--- /dev/null
+++ b/source/net/yacy/ai/llama3/ChatFormat.java
@@ -0,0 +1,130 @@
+/**
+ * ChatFormat.java
+
+ * This file was extracted from the llama3/qwen2 projects
+ * https://github.com/mukel/llama3.java
+ * https://github.com/mukel/qwen2.svm.java
+ *
+ * License: MIT License
+ *
+ * Copyright (c) 2024 Andrej Karpathy (for llama2.c)
+ * Copyright (c) 2024 Alfonso² Peterssen (for llama3/qwen2)
+ * Copyright (c) 2023 Georgi Gerganov et al. (for llama.cpp)
+ * Copyright (c) 2025 Michael Peter Christen for modifications:
+ * The code was modified to fit the YaCy AI project:
+ * - back-port to Java 11 (removal of Vector API operations and record types)
+ * - removal of interactive mode and system.out printing
+ * - separation of the classes in the single java and refactoring
+ * - run-time performance optimizations for dot product computation of quantized values
+ * - joining of llama3/qwen2 into one code base; multi-arch options
+ * - alignment with code from https://github.com/ggml-org/llama.cpp/
+ */
+
+package net.yacy.ai.llama3;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import net.yacy.ai.llama3.Model.Tokenizer;
+
+/**
+ * Utility tailored for Llama 3 instruct prompt format.
+ */
+public class ChatFormat {
+
+ final Tokenizer tokenizer;
+ final int beginOfText;
+ final int endHeader;
+ final int startHeader;
+ final int endOfTurn;
+ final int endOfText;
+ final int endOfMessage;
+ final Set<Integer> stopTokens;
+
+ public ChatFormat(Tokenizer tokenizer) {
+ this.tokenizer = tokenizer;
+ Map<String, Integer> specialTokens = this.tokenizer.getSpecialTokens();
+ this.beginOfText = specialTokens.get("<|begin_of_text|>");
+ this.startHeader = specialTokens.get("<|start_header_id|>");
+ this.endHeader = specialTokens.get("<|end_header_id|>");
+ this.endOfTurn = specialTokens.get("<|eot_id|>");
+ this.endOfText = specialTokens.get("<|end_of_text|>");
+ this.endOfMessage = specialTokens.getOrDefault("<|eom_id|>", -1); // only in 3.1
+ this.stopTokens = Set.of(endOfText, endOfTurn);
+ }
+
+ public Tokenizer getTokenizer() {
+ return tokenizer;
+ }
+
+ public Set<Integer> getStopTokens() {
+ return stopTokens;
+ }
+
+ public List<Integer> encodeHeader(ChatFormat.Message message) {
+ List<Integer> tokens = new ArrayList<>();
+ tokens.add(startHeader);
+ tokens.addAll(this.tokenizer.encodeAsList(message.role().name()));
+ tokens.add(endHeader);
+ tokens.addAll(this.tokenizer.encodeAsList("\n"));
+ return tokens;
+ }
+
+ public List<Integer> encodeMessage(ChatFormat.Message message) {
+ List<Integer> tokens = this.encodeHeader(message);
+ tokens.addAll(this.tokenizer.encodeAsList(message.content().strip()));
+ tokens.add(endOfTurn);
+ return tokens;
+ }
+
+ public List<Integer> encodeDialogPrompt(boolean appendAssistantTurn, List<ChatFormat.Message> dialog) {
+ List<Integer> tokens = new ArrayList<>();
+ tokens.add(beginOfText);
+ for (ChatFormat.Message message : dialog) {
+ tokens.addAll(this.encodeMessage(message));
+ }
+ if (appendAssistantTurn) {
+ // Add the start of an assistant message for the model to complete.
+ tokens.addAll(this.encodeHeader(new ChatFormat.Message(ChatFormat.Role.ASSISTANT, "")));
+ }
+ return tokens;
+ }
+
+ public static final class Message {
+ private final ChatFormat.Role role;
+ private final String content;
+
+ public Message(ChatFormat.Role role, String content) {
+ this.role = role;
+ this.content = content;
+ }
+
+ public ChatFormat.Role role() {
+ return role;
+ }
+
+ public String content() {
+ return content;
+ }
+
+ }
+
+ public static final class Role {
+ private final String name;
+
+ public static final Role SYSTEM = new Role("system");
+ public static final Role USER = new Role("user");
+ public static final Role ASSISTANT = new Role("assistant");
+
+ public Role(String name) {
+ this.name = name;
+ }
+
+ public String name() {
+ return name;
+ }
+
+ }
+} \ No newline at end of file
diff --git a/source/net/yacy/ai/llama3/Context.java b/source/net/yacy/ai/llama3/Context.java
new file mode 100644
index 000000000..b4cfbe1b2
--- /dev/null
+++ b/source/net/yacy/ai/llama3/Context.java
@@ -0,0 +1,61 @@
+/**
+ * Context
+ * Copyright 2025 by Michael Peter Christen
+ * First released 25.05.2025 at https://yacy.net
+ *
+ ** This class was not part of the original llama3 implementation,
+ ** but added later by the author to support different architectures.
+ ** It therefore does not inherit the llama3 copyright.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program in the file lgpl21.txt
+ * If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.yacy.ai.llama3;
+
+
+public final class Context {
+
+ public final String prompt;
+ public final String systemPrompt;
+ public final float temp;
+ public final float topp;
+ public final long seed;
+ public final int maxTokens;
+
+ public static final int DEFAULT_MAX_TOKENS = 512;
+
+ /**
+ * Create a context.
+ *
+ * @param prompt the prompt to use for the model
+ * @param systemPrompt a system prompt to use for the model
+ * @param temp temperature for sampling, must be >= 0; 0 means greedy sampling (deterministic)
+ * @param topp if 0 <= topp <= 1, use top-p (nucleus) sampling; otherwise, use categorical sampling
+ * @param seed random seed for sampling; use 0 for a random seed
+ * @param maxTokens maximum number of tokens to generate; use 0 for no limit (default is 512)
+ */
+ public Context(String prompt, String systemPrompt,
+ float temp, float topp, long seed, int maxTokens) {
+ assert(0 <= temp): "temperature must be positive";
+ assert(0 <= topp && topp <= 1): "top-p must be between 0 and 1";
+
+ this.prompt = prompt;
+ this.systemPrompt = systemPrompt;
+ this.temp = temp;
+ this.topp = topp;
+ this.seed = seed;
+ this.maxTokens = maxTokens;
+ }
+} \ No newline at end of file
diff --git a/source/net/yacy/ai/llama3/Llama.java b/source/net/yacy/ai/llama3/Llama.java
new file mode 100644
index 000000000..e6d04bffd
--- /dev/null
+++ b/source/net/yacy/ai/llama3/Llama.java
@@ -0,0 +1,427 @@
+/**
+ * Llama.java
+
+ * This file was extracted from the llama3/qwen2 projects
+ * https://github.com/mukel/llama3.java
+ * https://github.com/mukel/qwen2.svm.java
+ *
+ * License: MIT License
+ *
+ * Copyright (c) 2024 Andrej Karpathy (for llama2.c)
+ * Copyright (c) 2024 Alfonso² Peterssen (for llama3/qwen2)
+ * Copyright (c) 2023 Georgi Gerganov et al. (for llama.cpp)
+ * Copyright (c) 2025 Michael Peter Christen for modifications:
+ * The code was modified to fit the YaCy AI project:
+ * - back-port to Java 11 (removal of Vector API operations and record types)
+ * - removal of interactive mode and system.out printing
+ * - separation of the classes in the single java and refactoring
+ * - run-time performance optimizations for dot product computation of quantized values
+ * - joining of llama3/qwen2 into one code base; multi-arch options
+ * - alignment with code from https://github.com/ggml-org/llama.cpp/
+ */
+
+package net.yacy.ai.llama3;
+
+import java.nio.FloatBuffer;
+import java.util.*;
+import java.util.function.IntConsumer;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import net.yacy.ai.llama3.Model.Arch;
+import net.yacy.ai.llama3.Model.Tokenizer;
+import net.yacy.ai.llama3.Tensor.ArrayFloatTensor;
+import net.yacy.ai.llama3.Tensor.FloatTensor;
+
+public final class Llama {
+
+ private final Configuration configuration;
+ private final Tokenizer tokenizer;
+ private final Weights weights;
+
+ public Llama(Configuration configuration, Tokenizer tokenizer, Weights weights) {
+ this.configuration = configuration;
+ this.tokenizer = tokenizer;
+ this.weights = weights;
+ }
+
+ public Configuration configuration() {
+ return configuration;
+ }
+
+ public Tokenizer tokenizer() {
+ return tokenizer;
+ }
+
+ public Weights weights() {
+ return weights;
+ }
+
+ public State createNewState(int batchsize) {
+ State state = new State(configuration(), batchsize);
+ state.latestToken = tokenizer().getSpecialTokens().get("<|begin_of_text|>");
+ return state;
+ }
+
+ public static final class Configuration {
+ public final Arch arch; // architecture
+ public final int dim; // transformer dimension
+ public final int hiddenDim; // for ffn layers
+ public final int numberOfLayers; // number of layers
+ public final int numberOfHeads; // number of query heads
+ public final int numberOfKeyValueHeads; // number of key/value heads (can be < query heads because of multiquery)
+ public final int vocabularySize; // vocabulary size, usually 256 (byte-level)
+ public final int contextLength; // max sequence length
+ public final boolean sharedWeights;
+ public final float rmsNormEps;
+ public final float ropeTheta;
+ public final int headSize;
+
+ public Configuration(Arch arch, int dim, int hiddenDim, int numberOfLayers, int numberOfHeads, int numberOfKeyValueHeads,
+ int vocabularySize, int contextLength, boolean sharedWeights, float rmsNormEps, float ropeTheta) {
+ this.arch = arch;
+ this.dim = dim;
+ this.hiddenDim = hiddenDim;
+ this.numberOfLayers = numberOfLayers;
+ this.numberOfHeads = numberOfHeads;
+ this.numberOfKeyValueHeads = numberOfKeyValueHeads;
+ this.vocabularySize = vocabularySize;
+ this.contextLength = contextLength;
+ this.sharedWeights = sharedWeights;
+ this.rmsNormEps = rmsNormEps;
+ this.ropeTheta = ropeTheta;
+ this.headSize = dim / numberOfHeads;
+ }
+
+ public Configuration withContextLength(int newContextLength) {
+ if (newContextLength < 0) {
+ return this; // no change
+ }
+ return new Configuration(this.arch, this.dim, this.hiddenDim, this.numberOfLayers, this.numberOfHeads,
+ this.numberOfKeyValueHeads, this.vocabularySize, newContextLength,
+ this.sharedWeights, this.rmsNormEps, this.ropeTheta);
+ }
+
+ }
+
+ public static final class Weights {
+ // token embedding table
+ public final FloatTensor token_embedding_table; // (vocab_size, dim)
+ // weights for rmsnorms
+ public final FloatBuffer[] rms_att_weight; // (layer, dim) rmsnorm weights
+ // weights for matmuls
+ public final FloatTensor[] wq; // (layer, n_heads * head_size)
+ public final FloatTensor[] wk; // (layer, n_kv_heads, head_size)
+ public final FloatTensor[] wv; // (layer, n_kv_heads * head_size)
+ public final FloatTensor[] wo; // (layer, n_heads * head_size, dim)
+ public final FloatTensor[] q_bias; // (layer, dim)
+ public final FloatTensor[] k_bias; // (layer, kv_dim)
+ public final FloatTensor[] v_bias; // (layer, kv_dim)
+ public final FloatBuffer[] rms_ffn_weight; // (layer, dim)
+ // weights for ffn
+ public final FloatTensor[] w1; // (layer, hidden_dim, dim)
+ public final FloatTensor[] w2; // (layer, dim, hidden_dim)
+ public final FloatTensor[] w3; // (layer, hidden_dim, dim)
+ // public final rmsnorm
+ public final FloatBuffer rms_final_weight; // (dim,)
+ // freq_cis for RoPE relatively positional embeddings
+ public final FloatBuffer freq_cis_real; // (seq_len, head_size/2)
+ public final FloatBuffer freq_cis_imag; // (seq_len, head_size/2)
+ // (optional) classifier weights for the logits, on the last layer
+ public final FloatTensor wcls; // (vocab_size, dim)
+
+ public Weights(FloatTensor token_embedding_table, FloatBuffer[] rms_att_weight, FloatTensor[] wq,
+ FloatTensor[] wk, FloatTensor[] wv,
+ FloatTensor[] q_bias, FloatTensor[] k_bias, FloatTensor[] v_bias,
+ FloatTensor[] wo, FloatBuffer[] rms_ffn_weight,
+ FloatTensor[] w1, FloatTensor[] w2, FloatTensor[] w3, FloatBuffer rms_final_weight,
+ FloatBuffer freq_cis_real, FloatBuffer freq_cis_imag, FloatTensor wcls) {
+ this.token_embedding_table = token_embedding_table;
+ this.rms_att_weight = rms_att_weight;
+ this.wq = wq;
+ this.wk = wk;
+ this.wv = wv;
+ this.q_bias = q_bias;
+ this.k_bias = k_bias;
+ this.v_bias = v_bias;
+ this.wo = wo;
+ this.rms_ffn_weight = rms_ffn_weight;
+ this.w1 = w1;
+ this.w2 = w2;
+ this.w3 = w3;
+ this.rms_final_weight = rms_final_weight;
+ this.freq_cis_real = freq_cis_real;
+ this.freq_cis_imag = freq_cis_imag;
+ this.wcls = wcls;
+ }
+
+ }
+
+ public static final class State {
+
+ // current wave of activations
+ public final int batchsize;
+ public final FloatTensor[] x; // activation at current time stamp (dim,)
+ public final FloatTensor[] xb; // same, but inside a residual branch (dim,)
+ public final FloatTensor[] xb2; // an additional buffer just for convenience (dim,)
+ public final FloatTensor[] hb; // buffer for hidden dimension in the ffn (hidden_dim,)
+ public final FloatTensor[] hb2; // buffer for hidden dimension in the ffn (hidden_dim,)
+ public final FloatTensor[] q; // query (dim,)
+ public final FloatTensor[] k; // key (dim,)
+ public final FloatTensor[] v; // value (dim,)
+ public final FloatTensor[] att; // buffer for scores/attention values (n_heads, seq_len)
+ public final FloatTensor logits; // output logits
+
+ // kv cache
+ public final FloatTensor[] keyCache; // (n_layer, seq_len, kv_dim)
+ public final FloatTensor[] valueCache; // (n_layer, seq_len, kv_dim)
+
+ /** last index in previous block */
+ int idxPrevBlock;
+
+ public int latestToken;
+
+ State(Configuration config, int batchsize) {
+ this.batchsize = batchsize;
+ this.x = allocate(batchsize, config.dim);
+ this.xb = allocate(batchsize, config.dim);
+ this.xb2 = allocate(batchsize, config.dim);
+ this.hb = allocate(batchsize, config.hiddenDim);
+ this.hb2 = allocate(batchsize, config.hiddenDim);
+ this.q = allocate(batchsize, config.dim);
+ this.k = allocate(batchsize, config.dim);
+ this.v = allocate(batchsize, config.dim);
+ this.att = allocate(batchsize, config.numberOfHeads, config.contextLength);
+ idxPrevBlock = -1;
+
+ this.logits = ArrayFloatTensor.allocate(config.vocabularySize);
+ int kvDim = (config.dim * config.numberOfKeyValueHeads) / config.numberOfHeads;
+ this.keyCache = Stream.generate(() -> ArrayFloatTensor.allocate(config.contextLength, kvDim)).limit(config.numberOfLayers).toArray(FloatTensor[]::new);
+ this.valueCache = Stream.generate(() -> ArrayFloatTensor.allocate(config.contextLength, kvDim)).limit(config.numberOfLayers).toArray(FloatTensor[]::new);
+ }
+
+ private static FloatTensor[] allocate(int numTokens, int... dims) {
+ return IntStream.range(0, numTokens)
+ .mapToObj(i -> ArrayFloatTensor.allocate(dims))
+ .toArray(FloatTensor[]::new);
+ }
+
+ }
+
+ static void rmsnorm(FloatTensor out, FloatTensor x, FloatBuffer weight, int size, float rmsNormEps) {
+ // calculate sum of squares
+ float ss = x.reduce(0, size, 0f, (acc, xi) -> acc + xi * xi);
+ ss /= size;
+ ss += rmsNormEps;
+ ss = (float) (1.0 / Math.sqrt(ss));
+ // normalize and scale
+ for (int i = 0; i < size; ++i) {
+ out.setFloat(i, weight.get(i) * (ss * x.getFloat(i)));
+ }
+ }
+
+ static FloatTensor forward(Llama model, State state, int[] tokens, int position, boolean computeLogits) {
+ // a few convenience variables
+ Configuration config = model.configuration();
+ Weights weights = model.weights();
+ int dim = config.dim;
+ int headSize = config.headSize;
+ int kvDim = (config.dim * config.numberOfKeyValueHeads) / config.numberOfHeads;
+ int kvMul = config.numberOfHeads / config.numberOfKeyValueHeads; // integer multiplier of the kv sharing in multiquery
+ float sqrtHeadSize = (float) Math.sqrt(headSize);
+ final int nTokens = tokens.length;
+
+ // copy the token embedding into x
+ FloatTensor.parallelFor(0, nTokens, t ->
+ weights.token_embedding_table.copyTo(tokens[t] * dim, state.x[t], 0, dim)
+ );
+
+ // forward all the layers
+ for (int l = 0; l < config.numberOfLayers; l++) {
+
+ // attention rmsnorm
+ final int curLayer = l;
+ FloatTensor.parallelFor(0, nTokens, t ->
+ rmsnorm(state.xb[t], state.x[t], weights.rms_att_weight[curLayer], dim, config.rmsNormEps)
+ );
+
+ // qkv matmuls for this position
+ weights.wq[l].matmul(nTokens, state.xb, state.q, dim, dim);
+ weights.wk[l].matmul(nTokens, state.xb, state.k, kvDim, dim);
+ weights.wv[l].matmul(nTokens, state.xb, state.v, kvDim, dim);
+
+ // RoPE relative positional encoding: complex-valued rotate q and k in each head
+ FloatTensor.parallelFor(0, nTokens, t -> {
+ for (int i = 0; i < dim; i += 2) {
+ int head_dim = i % headSize;
+ float fcr = weights.freq_cis_real.get((position + t) * (headSize / 2) + (head_dim / 2));
+ float fci = weights.freq_cis_imag.get((position + t) * (headSize / 2) + (head_dim / 2));
+ int rotn = i < kvDim ? 2 : 1; // how many vectors? 2 = q & k, 1 = q only
+ for (int vi = 0; vi < rotn; vi++) {
+ FloatTensor vec = vi == 0 ? state.q[t] : state.k[t]; // the vector to rotate (query or key)
+ float v0 = vec.getFloat(i);
+ float v1 = vec.getFloat(i + 1);
+ vec.setFloat(i, v0 * fcr - v1 * fci);
+ vec.setFloat(i + 1, v0 * fci + v1 * fcr);
+ }
+ }
+ });
+
+ // save key,value at this time step (position) to our kv cache
+ FloatTensor.parallelFor(0, nTokens, t -> {
+ state.k[t].copyTo(0, state.keyCache[curLayer], (position + t) * kvDim, kvDim);
+ state.v[t].copyTo(0, state.valueCache[curLayer], (position + t) * kvDim, kvDim);
+ });
+
+ // If the logits are not required, the attention and FFN of the last layer can be skipped entirely.
+ if (!computeLogits && curLayer == config.numberOfLayers - 1) {
+ state.idxPrevBlock = nTokens - 1;
+ return null;
+ }
+
+ // multihead attention. iterate over all heads
+ FloatTensor.parallelForLong(0, (long) nTokens * (long) config.numberOfHeads, ht -> {
+ int token = (int) (ht / config.numberOfHeads);
+ int h = (int) (ht % config.numberOfHeads);
+ int qOffset = h * headSize;
+ int attOffset = h * config.contextLength;
+
+ for (int t = 0; t <= position + token; t++) {
+ int keyCacheOffset = t * kvDim + (h / kvMul) * headSize;
+ float score = state.q[token].dot(qOffset, state.keyCache[curLayer], keyCacheOffset, headSize);
+ score /= sqrtHeadSize;
+ state.att[token].setFloat(attOffset + t, score);
+ }
+
+ state.att[token].softmaxInPlace(attOffset, position + token + 1);
+
+ int xbOffset = h * headSize;
+ state.xb[token].fillInPlace(xbOffset, headSize, 0f);
+
+ for (int t = 0; t <= position + token; t++) {
+ int vOffset = t * kvDim + (h / kvMul) * headSize;
+ float a = state.att[token].getFloat(attOffset + t);
+ state.xb[token].saxpyInPlace(xbOffset, state.valueCache[curLayer], vOffset, headSize, a);
+ }
+ });
+
+ // final matmul to get the output of the attention
+ weights.wo[l].matmul(nTokens, state.xb, state.xb2, dim, dim);
+
+ // residual connection back into x
+ FloatTensor.parallelFor(0, nTokens, t -> {
+ state.x[t].addInPlace(state.xb2[t]);
+ });
+
+ // ffn rmsnorm
+ FloatTensor.parallelFor(0, nTokens, t -> {
+ rmsnorm(state.xb[t], state.x[t], weights.rms_ffn_weight[curLayer], dim, config.rmsNormEps);
+ });
+
+ // Now for FFN in PyTorch we have: self.w2(F.silu(self.w1(x)) * self.w3(x))
+ weights.w1[l].matmul(nTokens, state.xb, state.hb, config.hiddenDim, dim);
+ weights.w3[l].matmul(nTokens, state.xb, state.hb2, config.hiddenDim, dim);
+
+ // SwiGLU non-linearity
+ FloatTensor.parallelFor(0, nTokens, t -> {
+ state.hb[t].mapInPlace(value -> value / (float) (1.0 + Math.exp(-value)));
+ });
+
+ // elementwise multiply with w3(x)
+ FloatTensor.parallelFor(0, nTokens, t -> {
+ state.hb[t].multiplyInPlace(state.hb2[t]);
+ });
+
+ // final matmul to get the output of the ffn
+ weights.w2[l].matmul(nTokens, state.hb, state.xb, dim, config.hiddenDim);
+
+ // residual connection
+ FloatTensor.parallelFor(0, nTokens, t -> {
+ state.x[t].addInPlace(state.xb[t]);
+ });
+ }
+
+ // final rmsnorm
+ FloatTensor.parallelFor(0, nTokens, t -> {
+ rmsnorm(state.x[t], state.x[t], weights.rms_final_weight, dim, config.rmsNormEps);
+ });
+
+ // classifier into logits
+ weights.wcls.matmul(state.x[nTokens - 1], state.logits, config.vocabularySize, dim);
+ state.idxPrevBlock = nTokens - 1;
+
+ return state.logits;
+ }
+
+ /**
+ * LLM generation entry point, ingest prompt tokens and generates new tokens.
+ *
+ * <p>
+ * All prompt tokens are ingested first, then inference starts, until a stop token is found.
+ * The returned tokens only include generated/inferred tokens.
+ *
+ * @param model model to run inference (including weights, configuration, tokenizer ...)
+ * @param state state of the model e.g. key/value caches ... this is mutated by this call
+ * @param startPosition start prompt ingestion + inference at this position in the context e.g. useful if state was kept across calls (chained generation). 0 implies run with no previous context.
+ * @param promptTokens prompt tokens to ingest, all the prompt tokens will be ingested, given there's enough capacity left in the context
+ * @param stopTokens set of tokens that abort generation during inference, stop tokens do not affect prompt ingestion
+ * @param maxTokens maximum number of tokens (can go up to {@link Configuration#contextLength context length}
+ * if this value is negative or greater than {@link Configuration#contextLength context length}
+ * @param sampler {@link Sampler strategy} used to select tokens
+ * @param echo debugging flag, prints ALL, prompt and inferred tokens, to {@link System#err stderr}
+ * @param onTokenGenerated callback, if non-null, it's called every time a token is inferred e.g. it's not called when ingesting prompt tokens
+ * @return list of generated/inferred tokens, including the stop token, if any e.g. does not include any token from the prompt
+ */
+ public static List<Integer> generateTokens(Llama model, State state, int startPosition, List<Integer> promptTokens,
+ Set<Integer> stopTokens, int maxTokens, Sampler sampler,
+ IntConsumer onTokenGenerated) {
+ //long startNanos = System.nanoTime();
+ //long startGen = 0;
+ if (maxTokens < 0 || model.configuration().contextLength < maxTokens) {
+ maxTokens = model.configuration().contextLength;
+ }
+ List<Integer> generatedTokens = new ArrayList<>(maxTokens);
+ int token = state.latestToken; // BOS?
+ int nextToken;
+ int promptIndex = 0;
+ for (int position = startPosition; position < maxTokens; ++position) {
+ if (promptIndex < promptTokens.size()) {
+ final int nTokens = Math.min(maxTokens - position, Math.min(promptTokens.size() - promptIndex, state.batchsize));
+ final int[] tokens = new int[nTokens];
+ for (int i = 0; i < nTokens; i++) {
+ tokens[i] = promptTokens.get(promptIndex + i);
+ //System.out.print(Tokenizer.replaceControlCharacters(model.tokenizer().decode(List.of(tokens[i]))));
+
+ }
+ //System.out.format("position=%d, promptIdx=%d, promptSize=%d, tokens=%s%n", position, promptIndex, promptTokens.size(), Arrays.toString(tokens));
+
+ boolean computeLogits = promptIndex + nTokens >= promptTokens.size();
+ forward(model, state, tokens, position, computeLogits);
+ position += nTokens - 1;
+ promptIndex += nTokens;
+ if (promptIndex < promptTokens.size()) {
+ continue;
+ }
+ //startGen = System.nanoTime();
+ } else {
+ forward(model, state, new int[]{token}, position, true);
+ }
+ nextToken = sampler.sampleToken(state.logits);
+ // System.out.print(Tokenizer.replaceControlCharacters(model.tokenizer().decode(List.of(nextToken))));
+ generatedTokens.add(nextToken);
+ if (onTokenGenerated != null) {
+ onTokenGenerated.accept(nextToken);
+ }
+ if (stopTokens.contains(nextToken)) {
+ break;
+ }
+ state.latestToken = token = nextToken;
+ }
+
+ //long elapsedNanos = System.nanoTime() - startNanos;
+ //long promptNanos = startGen - startNanos;
+ //long genNanos = elapsedNanos - startGen + startNanos;
+ // System.out.printf(startPosition + promptIndex + generatedTokens.size(), model.configuration().contextLength, promptTokens.size() / (promptNanos / 1_000_000_000.0), promptTokens.size(), generatedTokens.size() / (genNanos / 1_000_000_000.0), generatedTokens.size());
+ return generatedTokens;
+ }
+} \ No newline at end of file
diff --git a/source/net/yacy/ai/llama3/Llama3.java b/source/net/yacy/ai/llama3/Llama3.java
new file mode 100644
index 000000000..8d1ab166f
--- /dev/null
+++ b/source/net/yacy/ai/llama3/Llama3.java
@@ -0,0 +1,155 @@
+/**
+ * Llama3.java
+
+ * This file was extracted from the llama3/qwen2 projects
+ * https://github.com/mukel/llama3.java
+ * https://github.com/mukel/qwen2.svm.java
+ *
+ * License: MIT License
+ *
+ * Copyright (c) 2024 Andrej Karpathy (for llama2.c)
+ * Copyright (c) 2024 Alfonso² Peterssen (for llama3/qwen2)
+ * Copyright (c) 2023 Georgi Gerganov et al. (for llama.cpp)
+ * Copyright (c) 2025 Michael Peter Christen for modifications:
+ * The code was modified to fit the YaCy AI project:
+ * - back-port to Java 11 (removal of Vector API operations and record types)
+ * - removal of interactive mode and system.out printing
+ * - separation of the classes in the single java and refactoring
+ * - run-time performance optimizations for dot product computation of quantized values
+ * - joining of llama3/qwen2 into one code base; multi-arch options
+ * - alignment with code from https://github.com/ggml-org/llama.cpp/
+ */
+
+package net.yacy.ai.llama3;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+import java.util.Set;
+import java.util.function.IntConsumer;
+
+import net.yacy.ai.llama3.Model.ModelLoader;
+
+public class Llama3 {
+
+ // Batch-size used in prompt evaluation.
+ private static final int BATCH_SIZE = Integer.getInteger("llama.BatchSize", 16);
+
+
+ static void runInteractive(Llama model, Sampler sampler, Context options) {
+ Llama.State state = null;
+ List<Integer> conversationTokens = new ArrayList<>();
+ ChatFormat chatFormat = new ChatFormat(model.tokenizer());
+ conversationTokens.add(chatFormat.beginOfText);
+ if (options.systemPrompt != null) {
+ conversationTokens.addAll(chatFormat.encodeMessage(new ChatFormat.Message(ChatFormat.Role.SYSTEM, options.systemPrompt)));
+ }
+ int startPosition = 0;
+ @SuppressWarnings("resource")
+ Scanner in = new Scanner(System.in);
+ while (true) {
+ System.out.print("> ");
+ System.out.flush();
+ String userText = in.nextLine();
+ if (state == null) {
+ state = model.createNewState(BATCH_SIZE);
+ }
+ conversationTokens.addAll(chatFormat.encodeMessage(new ChatFormat.Message(ChatFormat.Role.USER, userText)));
+ conversationTokens.addAll(chatFormat.encodeHeader(new ChatFormat.Message(ChatFormat.Role.ASSISTANT, "")));
+ Set<Integer> stopTokens = chatFormat.getStopTokens();
+ List<Integer> responseTokens = Llama.generateTokens(model, state, startPosition, conversationTokens.subList(startPosition, conversationTokens.size()), stopTokens, options.maxTokens, sampler, token -> {
+ if (!model.tokenizer().isSpecialToken(token)) {
+ System.out.print(model.tokenizer().decode(List.of(token)));
+ }
+ });
+ // Include stop token in the prompt history, but not in the response displayed to the user.
+ conversationTokens.addAll(responseTokens);
+ startPosition = conversationTokens.size();
+ Integer stopToken = null;
+ if (!responseTokens.isEmpty() && stopTokens.contains(responseTokens.get(responseTokens.size()-1))) {
+ stopToken = responseTokens.get(responseTokens.size()-1);
+ responseTokens.remove(responseTokens.size()-1);
+ }
+ //System.out.println(model.tokenizer().decode(responseTokens));
+ if (stopToken == null) {
+ System.out.println("Ran out of context length...");
+ break;
+ }
+ }
+ }
+
+ public static List<Integer> runInstructOnce(Llama model, Sampler sampler, Context options, IntConsumer onTokenGenerated) {
+ Llama.State state = model.createNewState(BATCH_SIZE);
+ ChatFormat chatFormat = new ChatFormat(model.tokenizer());
+
+ List<Integer> promptTokens = new ArrayList<>();
+ promptTokens.add(chatFormat.beginOfText);
+ if (options.systemPrompt != null) {
+ promptTokens.addAll(chatFormat.encodeMessage(new ChatFormat.Message(ChatFormat.Role.SYSTEM, options.systemPrompt)));
+ }
+ //System.out.println("Context after System Prompt: " + toString(model, promptTokens));
+ promptTokens.addAll(chatFormat.encodeMessage(new ChatFormat.Message(ChatFormat.Role.USER, options.prompt)));
+ //System.out.println("Context after User Prompt: " + toString(model, promptTokens));
+ promptTokens.addAll(chatFormat.encodeHeader(new ChatFormat.Message(ChatFormat.Role.ASSISTANT, "")));
+ //System.out.println("Context after Assitant Prompt: " + toString(model, promptTokens));
+
+ Set<Integer> stopTokens = chatFormat.getStopTokens();
+ List<Integer> responseTokens = Llama.generateTokens(model, state, 0, promptTokens, stopTokens, options.maxTokens, sampler, onTokenGenerated);
+
+ // remove stop token at the end of the response, if present
+ if (!responseTokens.isEmpty() && stopTokens.contains(responseTokens.get(responseTokens.size()-1))) {
+ responseTokens.remove(responseTokens.size()-1);
+ }
+ //System.out.println(model.tokenizer().decode(responseTokens));
+ return responseTokens;
+ }
+
+ public static String toString(Llama model, List<Integer> tokens) {
+ return model.tokenizer().decode(tokens);
+ }
+
+ public static void main(String[] args) throws IOException {
+ // model download paths:
+ // https://huggingface.co/mukel/Llama-3.2-1B-Instruct-GGUF/resolve/main/Llama-3.2-1B-Instruct-Q4_0.gguf
+ // https://huggingface.co/mukel/Llama-3.2-1B-Instruct-GGUF/resolve/main/Llama-3.2-1B-Instruct-Q8_0.gguf
+ // https://huggingface.co/mukel/Llama-3.2-3B-Instruct-GGUF/resolve/main/Llama-3.2-3B-Instruct-Q4_0.gguf
+
+ // performance on iMac x86:
+ // semeru 11 : 1.40 T/s
+ // semeru 21 : 1.13 T/s
+ // GraalVM 21: 2.27 T/s
+ // openjdk 21: 3.02 T/s; 3.2 with VarHandle
+
+ Path modelPath = Path.of("/Users/admin/git/yacy_search_server", "DATA", "LLMS", "Llama-3.2-1B-Instruct-Q4_0.gguf"); // 26.7 T/s/M4 orig jdk 21; 24.9 T/s/M4 Temurin 24; 25.8 T/s/M4 jdk 21; 22.2 T/s/M4 GraalVM21; 9.2 T/s/M4 Semeru 11; 9.2 T/s/M1 Ultra jdk 11
+ //Path modelPath = Path.of("/Users/admin/git/yacy_search_server", "DATA", "LLMS", "Llama-3.2-1B-Instruct-Q8_0.gguf"); // 10.7 T/s/M4 orig jdk 21; 21.2 T/s/M4 Temurin 24; 22 T/s/M4 jdk 21; 18 T/s/M4 GraalVM21; 5.8 T/s/M4 Semeru 11; 6.7 T/s/M1 Ultra jdk 11
+ //Path modelPath = Path.of("/Users/admin/git/yacy_search_server", "DATA", "LLMS", "Llama-3.2-3B-Instruct-Q4_0.gguf"); // 9.8 T/s/M4 orig jdk 21; 9.6 T/s/M4 Temurin 24; 9.3 T/s/M4 jdk 21; 8.0 T/s/M4 GraalVM21; 3.2 T/s/M4 Semeru 11; 3.8 T/s/M1 Ultra jdk 11
+ //Path modelPath = Path.of("/Users/admin/git/yacy_search_server", "DATA", "LLMS", "Llama-3.2-3B-Instruct-Q8_0.gguf"); // 7.2 T/s/M4 jdk 24
+ //Path modelPath = Path.of("/Users/admin/git/yacy_search_server", "DATA", "LLMS", "Meta-Llama-3-8B-Instruct-Q4_0.gguf"); // 3.6 T/s/M4 jdk 24;
+ //Path modelPath = Path.of("/Users/admin/git/yacy_search_server", "DATA", "LLMS", "OLMo-2-0425-1B-Instruct-Q4_0.gguf");
+ Context options = new Context("Write a Java program which computes the first 42 prime numbers.", "Be a very good programmer.", 0.0f, 0.95f, 0, 1024);
+ Llama model = ModelLoader.loadModel(modelPath, 1024, true);
+ // get time
+ long startTime = System.currentTimeMillis();
+ Sampler sampler = Sampler.selectSampler(model.configuration().vocabularySize, options.temp, options.topp, options.seed);
+ List<Integer> resultToken = runInstructOnce(model, sampler, options, token -> {
+ if (!model.tokenizer().isSpecialToken(token)) {
+ System.out.print(model.tokenizer().decode(List.of(token)));
+ }
+ });
+ long endTime = System.currentTimeMillis();
+ System.out.println("\nToken: " + resultToken.size() + ", " + ((double) resultToken.size()) * 1000.0d / ((double) (endTime - startTime)) + " Tokens per second");
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/source/net/yacy/ai/llama3/Model/Arch.java b/source/net/yacy/ai/llama3/Model/Arch.java
new file mode 100644
index 000000000..816980c63
--- /dev/null
+++ b/source/net/yacy/ai/llama3/Model/Arch.java
@@ -0,0 +1,31 @@
+/**
+ * Arch
+ * Copyright 2025 by Michael Peter Christen
+ * First released 25.05.2025 at https://yacy.net
+ *
+ ** This class was not part of the original llama3 implementation,
+ ** but added later by the author to support different architectures.
+ ** It therefore does not inherit the llama3 copyright.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program in the file lgpl21.txt
+ * If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.yacy.ai.llama3.Model;
+
+
+// architecture of the model, see https://github.com/ggml-org/llama.cpp/blob/master/src/llama-arch.cpp
+public enum Arch {
+ LLM_ARCH_LLAMA, LLM_ARCH_QWEN2, LLM_ARCH_QWEN3, LLM_ARCH_OLMO2
+}
diff --git a/source/net/yacy/ai/llama3/Model/GGMLTensorEntry.java b/source/net/yacy/ai/llama3/Model/GGMLTensorEntry.java
new file mode 100644
index 000000000..3b2f19e7b
--- /dev/null
+++ b/source/net/yacy/ai/llama3/Model/GGMLTensorEntry.java
@@ -0,0 +1,97 @@
+/**
+ * GGMLTensorEntry.java
+
+ * This file was extracted from the llama3/qwen2 projects
+ * https://github.com/mukel/llama3.java
+ * https://github.com/mukel/qwen2.svm.java
+ *
+ * License: MIT License
+ *
+ * Copyright (c) 2024 Andrej Karpathy (for llama2.c)
+ * Copyright (c) 2024 Alfonso² Peterssen (for llama3/qwen2)
+ * Copyright (c) 2023 Georgi Gerganov et al. (for llama.cpp)
+ * Copyright (c) 2025 Michael Peter Christen for modifications:
+ * The code was modified to fit the YaCy AI project:
+ * - back-port to Java 11 (removal of Vector API operations and record types)
+ * - removal of interactive mode and system.out printing
+ * - separation of the classes in the single java and refactoring
+ * - run-time performance optimizations for dot product computation of quantized values
+ * - joining of llama3/qwen2 into one code base; multi-arch options
+ * - alignment with code from https://github.com/ggml-org/llama.cpp/
+ */
+
+package net.yacy.ai.llama3.Model;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.util.Arrays;
+import java.util.Objects;
+
+import net.yacy.ai.llama3.Tensor.FloatTensor;
+import net.yacy.ai.llama3.Tensor.Q4_0FloatTensor;
+import net.yacy.ai.llama3.Tensor.Q8_0FloatTensor;
+
+public final class GGMLTensorEntry {
+
+ private final ByteBuffer buffer;
+ private final String name;
+ private final GGMLType ggmlType;
+ private final int[] shape;
+
+
+ public GGMLTensorEntry(String name, GGMLType ggmlType, int[] shape, ByteBuffer buffer) throws IOException {
+ assert buffer.isDirect() : "Buffer must be a direct ByteBuffer";
+ this.buffer = buffer;
+ buffer.order(ByteOrder.nativeOrder()); // Set correct byte order
+ this.name = Objects.requireNonNull(name);
+ this.ggmlType = Objects.requireNonNull(ggmlType);
+ this.shape = Arrays.copyOf(Objects.requireNonNull(shape), shape.length);
+ }
+
+ public float getFloat(int index) {
+ return buffer.getFloat(index * Float.BYTES);
+ }
+
+ public String name() {
+ return name;
+ }
+
+ public GGMLType ggmlType() {
+ return ggmlType;
+ }
+
+ public int[] shape() {
+ return Arrays.copyOf(shape, shape.length);
+ }
+
+ public FloatBuffer toFloatBuffer() {
+ switch (ggmlType) {
+ case F32: return buffer.order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer();
+ default: throw new UnsupportedOperationException("Conversion to " + ggmlType);
+ }
+ }
+
+ public FloatTensor loadQuantized() {
+ FloatTensor tensor = null;
+ switch (ggmlType) {
+ //case F32: return new F32FloatTensor(FloatTensor.numberOfElements(entry.shape()), entry.memorySegment());
+ case Q8_0: tensor = new Q8_0FloatTensor(FloatTensor.numberOfElements(this.shape()), this.buffer); break;
+ case Q4_0: tensor = new Q4_0FloatTensor(FloatTensor.numberOfElements(this.shape()), this.buffer); break;
+ default: throw new UnsupportedOperationException("Quantization format " + ggmlType);
+ }
+ return tensor;
+
+ // create a new ArrayFloatTensor(final float[] values)
+ //float[] values = new float[tensor.size()];
+ // copy the values from the tensor to the float array
+ //for (int i = 0; i < values.length; i++) {
+ /// values[i] = tensor.getFloat(i);
+ //}
+ //return new DirectBufferFloatTensor(values);
+ //return new F16FloatTensor(values);
+ //return new BF16FloatTensor(values);
+ //return new ArrayFloatTensor(values);
+ }
+} \ No newline at end of file
diff --git a/source/net/yacy/ai/llama3/Model/GGMLType.java b/source/net/yacy/ai/llama3/Model/GGMLType.java
new file mode 100644
index 000000000..d0fe6c427
--- /dev/null
+++ b/source/net/yacy/ai/llama3/Model/GGMLType.java
@@ -0,0 +1,99 @@
+/**
+ * GGMLType.java
+
+ * This file was extracted from the llama3/qwen2 projects
+ * https://github.com/mukel/llama3.java
+ * https://github.com/mukel/qwen2.svm.java
+ *
+ * License: MIT License
+ *
+ * Copyright (c) 2024 Andrej Karpathy (for llama2.c)
+ * Copyright (c) 2024 Alfonso² Peterssen (for llama3/qwen2)
+ * Copyright (c) 2023 Georgi Gerganov et al. (for llama.cpp)
+ * Copyright (c) 2025 Michael Peter Christen for modifications:
+ * The code was modified to fit the YaCy AI project:
+ * - back-port to Java 11 (removal of Vector API operations and record types)
+ * - removal of interactive mode and system.out printing
+ * - separation of the classes in the single java and refactoring
+ * - run-time performance optimizations for dot product computation of quantized values
+ * - joining of llama3/qwen2 into one code base; multi-arch options
+ * - alignment with code from https://github.com/ggml-org/llama.cpp/
+ */
+
+package net.yacy.ai.llama3.Model;
+
+public enum GGMLType {
+ F32(Float.BYTES),
+ F16(GGMLType.FLOAT16_BYTES),
+ Q4_0(GGMLType.FLOAT16_BYTES + 16 * Byte.BYTES, 32),
+ Q4_1(2 * GGMLType.FLOAT16_BYTES + 16 * Byte.BYTES, 32),
+ UNSUPPORTED_Q4_2(Integer.MAX_VALUE), // support has been removed
+ UNSUPPORTED_Q4_3(Integer.MAX_VALUE), // support has been removed
+ Q5_0(Integer.MAX_VALUE),
+ Q5_1(Integer.MAX_VALUE),
+ Q8_0(GGMLType.FLOAT16_BYTES + 32 * Byte.BYTES, 32),
+ Q8_1(32 * Byte.BYTES + 2 * Float.BYTES, 32),
+ // k-quantizations
+ Q2_K(Integer.MAX_VALUE),
+ Q3_K(Integer.MAX_VALUE),
+ Q4_K(2 * GGMLType.FLOAT16_BYTES + ((GGMLType.QK_K / 16) / 8 * 6) + GGMLType.QK_K / 2, GGMLType.QK_K),
+ Q5_K(2 * GGMLType.FLOAT16_BYTES + ((GGMLType.QK_K / 16) / 8 * 6) + GGMLType.QK_K / 8 + GGMLType.QK_K / 2, GGMLType.QK_K),
+ Q6_K(GGMLType.QK_K / 2 + GGMLType.QK_K / 4 + GGMLType.QK_K / 16 + GGMLType.FLOAT16_BYTES, GGMLType.QK_K),
+ Q8_K(Integer.MAX_VALUE),
+
+ IQ2_XXS(Integer.MAX_VALUE),
+ IQ2_XS(Integer.MAX_VALUE),
+ IQ3_XXS(Integer.MAX_VALUE),
+ IQ1_S(Integer.MAX_VALUE),
+ IQ4_NL(Integer.MAX_VALUE),
+ IQ3_S(Integer.MAX_VALUE),
+ IQ2_S(Integer.MAX_VALUE),
+ IQ4_XS(Integer.MAX_VALUE),
+
+ I8(Byte.BYTES),
+ I16(Short.BYTES),
+ I32(Integer.BYTES),
+ I64(Long.BYTES),
+ F64(Double.BYTES),
+ IQ1_M(Integer.MAX_VALUE),
+ BF16(GGMLType.BFLOAT16_BYTES),
+ Q4_0_4_4(GGMLType.FLOAT16_BYTES + 16 * Byte.BYTES, 32),
+ Q4_0_4_8(GGMLType.FLOAT16_BYTES + 16 * Byte.BYTES, 32),
+ Q4_0_8_8(GGMLType.FLOAT16_BYTES + 16 * Byte.BYTES, 32),
+ TQ1_0(Integer.MAX_VALUE),
+ TQ2_0(Integer.MAX_VALUE);
+
+ public static final int BFLOAT16_BYTES = 2;
+ public static final int FLOAT16_BYTES = 2;
+ private static final GGMLType[] VALUES = values();
+ public final int typeSize;
+ public final int blockSize;
+
+ public final static GGMLType fromId(int id) {
+ return VALUES[id];
+ }
+
+ GGMLType(int typeSize) {
+ this(typeSize, 1);
+ }
+
+ public long byteSizeFor(int numberOfElements) {
+ long t = numberOfElements * (long) this.typeSize;
+ assert t % this.blockSize == 0;
+ return Math.toIntExact(t / this.blockSize);
+ }
+
+ public static final int QK_K = 256; // or 64?
+
+ GGMLType(int typeSize, int blockSize) {
+ assert blockSize > 0;
+ assert typeSize > 0;
+ assert isPowerOf2(blockSize);
+ this.typeSize = typeSize;
+ this.blockSize = blockSize;
+ }
+
+ private final static boolean isPowerOf2(int n) {
+ return n > 0 && (n & (n - 1)) == 0;
+ }
+}
diff --git a/source/net/yacy/ai/llama3/Model/GGUF.java b/source/net/yacy/ai/llama3/Model/GGUF.java
new file mode 100644
index 000000000..6a4fd7209
--- /dev/null
+++ b/source/net/yacy/ai/llama3/Model/GGUF.java
@@ -0,0 +1,645 @@
+/**
+ * GGUF.java
+
+ * This file was extracted from the llama3/qwen2 projects
+ * https://github.com/mukel/llama3.java
+ * https://github.com/mukel/qwen2.svm.java
+ *
+ * License: MIT License
+ *
+ * Copyright (c) 2024 Andrej Karpathy (for llama2.c)
+ * Copyright (c) 2024 Alfonso² Peterssen (for llama3/qwen2)
+ * Copyright (c) 2023 Georgi Gerganov et al. (for llama.cpp)
+ * Copyright (c) 2025 Michael Peter Christen for modifications:
+ * The code was modified to fit the YaCy AI project:
+ * - back-port to Java 11 (removal of Vector API operations and record types)
+ * - removal of interactive mode and system.out printing
+ * - separation of the classes in the single java and refactoring
+ * - run-time performance optimizations for dot product computation of quantized values
+ * - joining of llama3/qwen2 into one code base; multi-arch options
+ * - alignment with code from https://github.com/ggml-org/llama.cpp/
+ */
+
+package net.yacy.ai.llama3.Model;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import net.yacy.ai.llama3.Tensor.FloatTensor;
+
+/*
+ * GGUF File Reader. For specification see https://github.com/ggml-org/ggml/blob/master/docs/gguf.md
+ */
+public final class GGUF {
+ private static final int GGUF_MAGIC = 0x46554747;
+ private static final int DEFAULT_ALIGNMENT = 32; // must be a power of 2
+ private static final List<Integer> SUPPORTED_GGUF_VERSIONS = List.of(2, 3);
+ private int magic;
+ private int version;
+ private int tensorCount; // uint64_t
+ private int alignment;
+ private int metadata_kv_count; // uint64_t
+ private Map<String, Object> metadata;
+ private Map<String, GGUFTensorInfo> tensorInfos;
+ private long tensorDataOffset;
+ private Map<String, GGMLTensorEntry> tensorEntries;
+
+ public Map<String, GGUFTensorInfo> getTensorInfos() {
+ return tensorInfos;
+ }
+
+ public long getTensorDataOffset() {
+ return tensorDataOffset;
+ }
+
+ public Map<String, Object> getMetadata() {
+ return metadata;
+ }
+
+ private final ByteBuffer BB_1 = ByteBuffer.allocate(Byte.BYTES).order(ByteOrder.LITTLE_ENDIAN);
+ private final ByteBuffer BB_2 = ByteBuffer.allocate(Short.BYTES).order(ByteOrder.LITTLE_ENDIAN);
+ private final ByteBuffer BB_4 = ByteBuffer.allocate(Integer.BYTES).order(ByteOrder.LITTLE_ENDIAN);
+ private final ByteBuffer BB_8 = ByteBuffer.allocate(Long.BYTES).order(ByteOrder.LITTLE_ENDIAN);
+
+ public Map<String, GGMLTensorEntry> getTensorEntries() {
+ return tensorEntries;
+ }
+
+ public static GGUF loadModel(Path modelPath) throws IOException {
+ try (FileChannel fileChannel = FileChannel.open(modelPath)) {
+ GGUF gguf = new GGUF();
+ gguf.loadModelImpl(fileChannel);
+ return gguf;
+ }
+ }
+
+ enum MetadataValueType {
+ // The value is a 8-bit unsigned integer.
+ UINT8(1),
+ // The value is a 8-bit signed integer.
+ INT8(1),
+ // The value is a 16-bit unsigned little-endian integer.
+ UINT16(2),
+ // The value is a 16-bit signed little-endian integer.
+ INT16(2),
+ // The value is a 32-bit unsigned little-endian integer.
+ UINT32(4),
+ // The value is a 32-bit signed little-endian integer.
+ INT32(4),
+ // The value is a 32-bit IEEE754 floating point number.
+ FLOAT32(4),
+ // The value is a boolean.
+ // 1-byte value where 0 is false and 1 is true.
+ // Anything else is invalid, and should be treated as either the model being invalid or the reader being buggy.
+ BOOL(1),
+ // The value is a UTF-8 non-null-terminated string, with length prepended.
+ STRING(-8),
+ // The value is an array of other values, with the length and type prepended.
+ // Arrays can be nested, and the length of the array is the number of elements in the array, not the number of bytes.
+ ARRAY(-8),
+ // The value is a 64-bit unsigned little-endian integer.
+ UINT64(8),
+ // The value is a 64-bit signed little-endian integer.
+ INT64(8),
+ // The value is a 64-bit IEEE754 floating point number.
+ FLOAT64(8);
+ private final int byteSize;
+
+ MetadataValueType(int byteSize) {
+ this.byteSize = byteSize;
+ }
+
+ private static final MetadataValueType[] VALUES = values();
+
+ public static MetadataValueType fromIndex(int index) {
+ return VALUES[index];
+ }
+
+ public int byteSize() {
+ return byteSize;
+ }
+ }
+
+ private void loadModelImpl(FileChannel fileChannel) throws IOException {
+ // The header of the file.
+ readHeader(fileChannel); // gguf_header_t header;
+ // Tensor infos, which can be used to locate the tensor data.
+ // gguf_tensor_info_t tensor_infos[header.tensor_count];
+ this.tensorInfos = new HashMap<>(tensorCount);
+ for (int i = 0; i < tensorCount; ++i) {
+ GGUF.GGUFTensorInfo ti = readTensorInfo(fileChannel);
+ assert !tensorInfos.containsKey(ti.name);
+ tensorInfos.put(ti.name, ti);
+ }
+ // Padding to the nearest multiple of `ALIGNMENT`.
+ // uint8_t _padding[ALIGNMENT - (sizeof(header + tensor_infos) % ALIGNMENT)];
+ //long _padding = -fileChannel.position() & (ALIGNMENT - 1);
+ long _padding = getAlignment() - (fileChannel.position() % getAlignment());
+ fileChannel.position(fileChannel.position() + _padding);
+ this.tensorDataOffset = fileChannel.position();
+ }
+ /*
+ public static Map<String, GGMLTensorEntry> loadTensors(FileChannel fileChannel, long tensorDataOffset, Map<String, GGUFTensorInfo> tensorInfos) throws IOException {
+ // Map the whole remaining file region into memory
+ MappedByteBuffer mappedBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, tensorDataOffset, fileChannel.size() - tensorDataOffset);
+ mappedBuffer.order(ByteOrder.nativeOrder());
+
+ Map<String, GGMLTensorEntry> tensorEntries = new HashMap<>(tensorInfos.size());
+
+ for (Map.Entry<String, GGUFTensorInfo> entry : tensorInfos.entrySet()) {
+ GGUFTensorInfo ti = entry.getValue();
+ int numberOfElements = FloatTensor.numberOfElements(ti.dimensions());
+ int sizeInBytes = Math.toIntExact(ti.ggmlType().byteSizeFor(numberOfElements));
+ int offset = Math.toIntExact(ti.offset()); // assumes offset is within Integer range
+
+ // Create a slice of the mapped buffer for this tensor
+ MappedByteBuffer buffer = (MappedByteBuffer) mappedBuffer.duplicate();
+ buffer.position(offset);
+ buffer.limit(offset + sizeInBytes);
+
+ // copy this into a DirectByteBuffer (tests show that this is not faster than using the MappedByteBuffer directly)
+ //ByteBuffer directBuffer = ByteBuffer.allocateDirect(sizeInBytes).order(ByteOrder.nativeOrder());
+ //directBuffer.put(buffer);
+ //directBuffer.flip();
+ //tensorEntries.put(ti.name(), new GGMLTensorEntry(ti.name(), ti.ggmlType(), ti.dimensions(), directBuffer));
+
+ // Create a slice of the mapped buffer for this tensor
+ MappedByteBuffer tensorBuffer = (MappedByteBuffer) buffer.slice().order(ByteOrder.nativeOrder());
+ tensorEntries.put(ti.name(), new GGMLTensorEntry(ti.name(), ti.ggmlType(), ti.dimensions(), tensorBuffer));
+ }
+
+ return tensorEntries;
+ }
+*/
+
+ static Map<String, GGMLTensorEntry> loadTensors(FileChannel fileChannel, long tensorDataOffset,
+ Map<String, GGUFTensorInfo> tensorInfos) throws IOException {
+
+ final long totalDataSize = fileChannel.size() - tensorDataOffset;
+ Map<String, GGMLTensorEntry> tensorEntries = new HashMap<>(tensorInfos.size());
+
+ // Fast path: if the entire tensor data fits in one mapped buffer
+ if (totalDataSize <= Integer.MAX_VALUE) {
+ MappedByteBuffer fullBuffer = (MappedByteBuffer) fileChannel.map(
+ FileChannel.MapMode.READ_ONLY,
+ tensorDataOffset,
+ totalDataSize
+ ).order(ByteOrder.nativeOrder());
+
+ for (Map.Entry<String, GGUFTensorInfo> entry : tensorInfos.entrySet()) {
+ GGUFTensorInfo ti = entry.getValue();
+ long offset = ti.offset();
+ int sizeInBytes = Math.toIntExact(ti.ggmlType().byteSizeFor(
+ FloatTensor.numberOfElements(ti.dimensions())));
+
+ MappedByteBuffer tensorBuffer = (MappedByteBuffer) fullBuffer.duplicate();
+ tensorBuffer.position((int)offset);
+ tensorBuffer.limit((int)offset + sizeInBytes);
+ tensorBuffer = (MappedByteBuffer) tensorBuffer.slice().order(ByteOrder.nativeOrder());
+
+ tensorEntries.put(ti.name(), new GGMLTensorEntry(
+ ti.name(), ti.ggmlType(), ti.dimensions(), tensorBuffer));
+ }
+ return tensorEntries;
+ }
+
+ // Slow path: only for large files > 2GB:
+ // Models can be very large, so we cannot map the entire tensor data at once.
+ // Instead, we will map segments of the tensor data as needed.
+
+ // First pass: collect all tensor boundaries and sort them
+ List<Long> boundaries = new ArrayList<>();
+ for (GGUFTensorInfo ti : tensorInfos.values()) {
+ long start = ti.offset();
+ long end = start + ti.ggmlType().byteSizeFor(FloatTensor.numberOfElements(ti.dimensions()));
+ boundaries.add(start);
+ boundaries.add(end);
+ }
+ boundaries = boundaries.stream()
+ .distinct()
+ .sorted()
+ .collect(Collectors.toList());
+
+ // Second pass: create memory mappings for each segment between boundaries
+ List<MappedSegment> mappings = new ArrayList<>();
+ long currentPos = tensorDataOffset;
+
+ for (long boundary : boundaries) {
+ if (boundary <= currentPos) continue;
+
+ long mappingSize = boundary - currentPos;
+ // Split into chunks no larger than Integer.MAX_VALUE
+ while (mappingSize > 0) {
+ long chunkSize = Math.min(mappingSize, Integer.MAX_VALUE);
+ MappedByteBuffer buffer = (MappedByteBuffer) fileChannel.map(
+ FileChannel.MapMode.READ_ONLY,
+ currentPos,
+ chunkSize
+ ).order(ByteOrder.nativeOrder());
+ mappings.add(new MappedSegment(currentPos, buffer));
+ currentPos += chunkSize;
+ mappingSize -= chunkSize;
+ }
+ }
+
+ // Handle any remaining data after last boundary
+ if (currentPos < fileChannel.size()) {
+ long remaining = fileChannel.size() - currentPos;
+ while (remaining > 0) {
+ long chunkSize = Math.min(remaining, Integer.MAX_VALUE);
+ MappedByteBuffer buffer = (MappedByteBuffer) fileChannel.map(
+ FileChannel.MapMode.READ_ONLY,
+ currentPos,
+ chunkSize
+ ).order(ByteOrder.nativeOrder());
+ mappings.add(new MappedSegment(currentPos, buffer));
+ currentPos += chunkSize;
+ remaining -= chunkSize;
+ }
+ }
+
+ // Third pass: create tensor views
+ for (Map.Entry<String, GGUFTensorInfo> entry : tensorInfos.entrySet()) {
+ GGUFTensorInfo ti = entry.getValue();
+ String name = ti.name();
+ long tensorOffset = ti.offset() + tensorDataOffset;
+ long tensorSize = ti.ggmlType().byteSizeFor(FloatTensor.numberOfElements(ti.dimensions()));
+ long tensorEnd = tensorOffset + tensorSize;
+
+ // Find all segments that overlap with this tensor
+ List<MappedSegment> overlappingSegments = mappings.stream()
+ .filter(seg -> seg.startOffset <= tensorEnd &&
+ seg.startOffset + seg.buffer.capacity() > tensorOffset)
+ .sorted(Comparator.comparingLong(seg -> seg.startOffset))
+ .collect(Collectors.toList());
+
+ if (overlappingSegments.isEmpty()) {
+ throw new IOException("Tensor " + name + " not found in any mapped segment");
+ }
+
+ if (overlappingSegments.size() == 1) {
+ // Simple case - tensor fits entirely in one segment
+ MappedSegment segment = overlappingSegments.get(0);
+ int bufferOffset = (int)(tensorOffset - segment.startOffset);
+ int sizeInBytes = (int)tensorSize;
+
+ MappedByteBuffer tensorBuffer = (MappedByteBuffer) segment.buffer.duplicate();
+ tensorBuffer.position(bufferOffset);
+ tensorBuffer.limit(bufferOffset + sizeInBytes);
+ tensorBuffer = (MappedByteBuffer) tensorBuffer.slice().order(ByteOrder.nativeOrder());
+
+ tensorEntries.put(name, new GGMLTensorEntry(
+ name, ti.ggmlType(), ti.dimensions(), tensorBuffer));
+ } else {
+ // Complex case - tensor spans multiple segments
+ ByteBuffer combinedBuffer = ByteBuffer.allocateDirect((int)tensorSize)
+ .order(ByteOrder.nativeOrder());
+
+ long remainingBytes = tensorSize;
+ long currentTensorPos = tensorOffset;
+
+ for (MappedSegment segment : overlappingSegments) {
+ long segmentEnd = segment.startOffset + segment.buffer.capacity();
+ long copyStart = Math.max(currentTensorPos, segment.startOffset);
+ long copyEnd = Math.min(tensorEnd, segmentEnd);
+ int bytesToCopy = (int)(copyEnd - copyStart);
+
+ int srcPos = (int)(copyStart - segment.startOffset);
+ segment.buffer.position(srcPos);
+
+ byte[] temp = new byte[bytesToCopy];
+ segment.buffer.get(temp);
+ combinedBuffer.put(temp);
+
+ remainingBytes -= bytesToCopy;
+ currentTensorPos += bytesToCopy;
+
+ if (remainingBytes <= 0) break;
+ }
+
+ combinedBuffer.flip();
+ tensorEntries.put(name, new GGMLTensorEntry(
+ name, ti.ggmlType(), ti.dimensions(), combinedBuffer));
+ }
+ }
+
+ return tensorEntries;
+ }
+
+ // Helper class to track mapped segments
+ private static class MappedSegment {
+ final long startOffset;
+ final MappedByteBuffer buffer;
+
+ MappedSegment(long startOffset, MappedByteBuffer buffer) {
+ this.startOffset = startOffset;
+ this.buffer = buffer;
+ }
+ }
+
+ public static final class GGUFTensorInfo {
+ private final String name;
+ private final int[] dimensions;
+ private final GGMLType ggmlType;
+ private final long offset;
+
+ public GGUFTensorInfo(String name, int[] dimensions, GGMLType ggmlType, long offset) {
+ this.name = name;
+ this.dimensions = dimensions != null ? dimensions.clone() : null;
+ this.ggmlType = ggmlType;
+ this.offset = offset;
+ }
+
+ public String name() {
+ return name;
+ }
+
+ public int[] dimensions() {
+ return dimensions != null ? dimensions.clone() : null;
+ }
+
+ public GGMLType ggmlType() {
+ return ggmlType;
+ }
+
+ public long offset() {
+ return offset;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ GGUFTensorInfo that = (GGUFTensorInfo) o;
+ return offset == that.offset &&
+ Objects.equals(name, that.name) &&
+ Arrays.equals(dimensions, that.dimensions) &&
+ Objects.equals(ggmlType, that.ggmlType);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Objects.hash(name, ggmlType, offset);
+ result = 31 * result + Arrays.hashCode(dimensions);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "GGUFTensorInfo[" +
+ "name=" + name +
+ ", dimensions=" + Arrays.toString(dimensions) +
+ ", ggmlType=" + ggmlType +
+ ", offset=" + offset +
+ ']';
+ }
+ }
+
+ private GGMLType readGGMLType(FileChannel fileChannel) throws IOException {
+ int ggmlTypeId = readInt(fileChannel); // ggml_type type;
+ return GGMLType.fromId(ggmlTypeId);
+ }
+
+ private GGUF.GGUFTensorInfo readTensorInfo(FileChannel fileChannel) throws IOException {
+ // The name of the tensor. It is a standard GGUF string, with the caveat that
+ // it must be at most 64 bytes long.
+ String name = readString(fileChannel); // gguf_string_t name;
+ assert name.length() <= 64;
+ // The number of dimensions in the tensor.
+ // Currently at most 4, but this may change in the future.
+ int n_dimensions = readInt(fileChannel); // uint32_t n_dimensions;
+ assert n_dimensions <= 4;
+ // The dimensions of the tensor.
+ int[] dimensions = new int[n_dimensions]; // uint64_t dimensions[n_dimensions];
+ for (int i = 0; i < n_dimensions; ++i) {
+ dimensions[i] = Math.toIntExact(readLong(fileChannel));
+ }
+ // The type of the tensor.
+ GGMLType ggmlType = readGGMLType(fileChannel); // ggml_type type;
+ // The offset of the tensor's data in this file in bytes.
+ // This offset is relative to `tensor_data`, not to the start
+ // of the file, to make it easier for writers to write the file.
+ // Readers should consider exposing this offset relative to the
+ // file to make it easier to read the data.
+ // Must be a multiple of `ALIGNMENT`.
+ long offset = readLong(fileChannel); // uint64_t offset;
+ assert offset % getAlignment() == 0;
+ return new GGUF.GGUFTensorInfo(name, dimensions, ggmlType, offset);
+ }
+
+ private String readString(FileChannel fileChannel) throws IOException {
+ // A string in GGUF.
+ // The length of the string, in bytes.
+ int len = Math.toIntExact(readLong(fileChannel)); // uint64_t len;
+ // The string as a UTF-8 non-null-terminated string.
+ byte[] bytes = new byte[len]; // char string[len];
+ int bytesRead = fileChannel.read(ByteBuffer.wrap(bytes));
+ assert len == bytesRead;
+ return new String(bytes, StandardCharsets.UTF_8);
+ }
+
+ private Pair<String, Object> readKeyValuePair(FileChannel fileChannel) throws IOException {
+ // The key of the metadata. It is a standard GGUF string, with the following caveats:
+ // - It must be a valid ASCII string.
+ // - It must be a hierarchical key, where each segment is `lower_snake_case` and separated by a `.`.
+ // - It must be at most 2^16-1/65535 bytes long.
+ // Any keys that do not follow these rules are invalid.
+ String key = readString(fileChannel); // gguf_string_t key;
+ assert key.length() < (1 << 16);
+ assert key.codePoints().allMatch(cp -> ('a' <= cp && cp <= 'z') || ('0' <= cp && cp <= '9') || cp == '_' || cp == '.');
+ Object value = readMetadataValue(fileChannel);
+ return new Pair<>(key, value);
+ }
+
+ private Object readMetadataValue(FileChannel fileChannel) throws IOException {
+ // The type of the value.
+ // Must be one of the `gguf_metadata_value_type` values.
+ MetadataValueType value_type = readMetadataValueType(fileChannel); // gguf_metadata_value_type value_type;
+ // The value.
+ return readMetadataValueOfType(value_type, fileChannel); // gguf_metadata_value_t value;
+ }
+
+ // compare this with https://github.com/ggml-org/llama.cpp/blob/master/gguf-py/gguf/gguf_reader.py#L132
+ void readHeader(FileChannel fileChannel) throws IOException {
+ // Magic number to announce that this is a GGUF file.
+ // Must be `GGUF` at the byte level: `0x47` `0x47` `0x55` `0x46`.
+ // Your executor might do little-endian byte order, so it might be
+ // check for 0x46554747 and letting the endianness cancel out.
+ // Consider being *very* explicit about the byte order here.
+ this.magic = readInt(fileChannel); // uint32_t magic;
+ if (magic != GGUF_MAGIC) {
+ throw new IllegalArgumentException("unsupported header.magic " + magic);
+ }
+ // The version of the format implemented.
+ // Must be `3` for version described in this spec.
+ //
+ // This version should only be increased for structural changes to the format.
+ // Changes that do not affect the structure of the file should instead update the metadata
+ // to signify the change.
+ this.version = readInt(fileChannel); // uint32_t version;
+ if (!SUPPORTED_GGUF_VERSIONS.contains(version)) {
+ throw new IllegalArgumentException("unsupported header.version " + version);
+ }
+ // The number of tensors in the file.
+ // This is explicit, instead of being included in the metadata, to ensure it is always present
+ // for loading the tensors.
+ // https://github.com/ggml-org/llama.cpp/blob/master/gguf-py/gguf/gguf_reader.py#L165
+ this.tensorCount = Math.toIntExact(readLong(fileChannel)); // uint64_t tensor_count;
+ // The number of metadata key-value pairs.
+ this.metadata_kv_count = Math.toIntExact(readLong(fileChannel)); // uint64_t metadata_kv_count;
+ // The metadata key-value pairs.
+ // gguf_metadata_kv_t metadata_kv[metadata_kv_count];
+ this.metadata = new HashMap<>(metadata_kv_count);
+ for (int i = 0; i < metadata_kv_count; ++i) {
+ Pair<String, Object> keyValue = readKeyValuePair(fileChannel);
+ assert !metadata.containsKey(keyValue.first());
+ metadata.put(keyValue.first(), keyValue.second());
+ }
+ }
+
+ private Object readArray(FileChannel fileChannel) throws IOException {
+ // Any value type is valid, including arrays.
+ MetadataValueType value_type = readMetadataValueType(fileChannel); // gguf_metadata_value_type type;
+ // Number of elements, not bytes
+ int len = Math.toIntExact(readLong(fileChannel)); // uint64_t len;
+ // The array of values.
+ // gguf_metadata_value_t array[len];
+ switch (value_type) {
+ case UINT8:
+ case INT8: {
+ byte[] bytes = new byte[len];
+ for (int i = 0; i < len; ++i) {
+ bytes[i] = readByte(fileChannel);
+ }
+ return bytes;
+ }
+ case UINT16:
+ case INT16: {
+ short[] shorts = new short[len];
+ for (int i = 0; i < len; ++i) {
+ shorts[i] = readShort(fileChannel);
+ }
+ return shorts;
+ }
+ case UINT32:
+ case INT32: {
+ int[] ints = new int[len];
+ for (int i = 0; i < len; ++i) {
+ ints[i] = readInt(fileChannel);
+ }
+ return ints;
+ }
+ case FLOAT32: {
+ float[] floats = new float[len];
+ for (int i = 0; i < len; ++i) {
+ floats[i] = readFloat(fileChannel);
+ }
+ return floats;
+ }
+ case BOOL: {
+ boolean[] booleans = new boolean[len];
+ for (int i = 0; i < len; ++i) {
+ booleans[i] = readBoolean(fileChannel);
+ }
+ return booleans;
+ }
+ case STRING: {
+ String[] strings = new String[len];
+ for (int i = 0; i < len; ++i) {
+ strings[i] = readString(fileChannel);
+ }
+ return strings;
+ }
+ case ARRAY: {
+ Object[] arrays = new Object[len];
+ for (int i = 0; i < len; ++i) {
+ arrays[i] = readArray(fileChannel);
+ }
+ return arrays;
+ }
+ default: throw new UnsupportedOperationException("read array of " + value_type);
+ }
+ }
+
+ private Object readMetadataValueOfType(MetadataValueType valueType, FileChannel fileChannel) throws IOException {
+ switch (valueType) {
+ case UINT8:
+ case INT8: return readByte(fileChannel);
+ case UINT16:
+ case INT16: return readShort(fileChannel);
+ case UINT32:
+ case INT32: return readInt(fileChannel);
+ case FLOAT32: return readFloat(fileChannel);
+ case UINT64:
+ case INT64: return readLong(fileChannel);
+ case FLOAT64: return readDouble(fileChannel);
+ case BOOL: return readBoolean(fileChannel);
+ case STRING: return readString(fileChannel);
+ case ARRAY: return readArray(fileChannel);
+ default: throw new AssertionError();
+ }
+ }
+
+ private byte readByte(FileChannel fileChannel) throws IOException {
+ int bytesRead = fileChannel.read(BB_1);
+ assert bytesRead == 1;
+ return BB_1.clear().get(0);
+ }
+
+ private boolean readBoolean(FileChannel fileChannel) throws IOException {
+ return readByte(fileChannel) != 0;
+ }
+
+ private short readShort(FileChannel fileChannel) throws IOException {
+ int bytesRead = fileChannel.read(BB_2);
+ assert bytesRead == 2;
+ return BB_2.clear().getShort(0);
+ }
+
+ private int readInt(FileChannel fileChannel) throws IOException {
+ int bytesRead = fileChannel.read(BB_4);
+ assert bytesRead == 4;
+ return BB_4.clear().getInt(0);
+ }
+
+ private long readLong(FileChannel fileChannel) throws IOException {
+ int bytesRead = fileChannel.read(BB_8);
+ assert bytesRead == 8;
+ return BB_8.clear().getLong(0);
+ }
+
+ private float readFloat(FileChannel fileChannel) throws IOException {
+ return Float.intBitsToFloat(readInt(fileChannel));
+ }
+
+ private double readDouble(FileChannel fileChannel) throws IOException {
+ return Double.longBitsToDouble(readLong(fileChannel));
+ }
+
+ private MetadataValueType readMetadataValueType(FileChannel fileChannel) throws IOException {
+ int index = readInt(fileChannel);
+ return MetadataValueType.fromIndex(index);
+ }
+
+ public int getAlignment() {
+ if (alignment != 0) {
+ return alignment;
+ }
+ alignment = (int) metadata.getOrDefault("general.alignment", DEFAULT_ALIGNMENT);
+ assert Integer.bitCount(alignment) == 1 : "alignment must be a power of two";
+ return alignment;
+ }
+} \ No newline at end of file
diff --git a/source/net/yacy/ai/llama3/Model/ModelLoader.java b/source/net/yacy/ai/llama3/Model/ModelLoader.java
new file mode 100644
index 000000000..4cd7ce40b
--- /dev/null
+++ b/source/net/yacy/ai/llama3/Model/ModelLoader.java
@@ -0,0 +1,333 @@
+/**
+ * ModelLoader.java
+
+ * This file was extracted from the llama3/qwen2 projects
+ * https://github.com/mukel/llama3.java
+ * https://github.com/mukel/qwen2.svm.java
+ *
+ * License: MIT License
+ *
+ * Copyright (c) 2024 Andrej Karpathy (for llama2.c)
+ * Copyright (c) 2024 Alfonso² Peterssen (for llama3/qwen2)
+ * Copyright (c) 2023 Georgi Gerganov et al. (for llama.cpp)
+ * Copyright (c) 2025 Michael Peter Christen for modifications:
+ * The code was modified to fit the YaCy AI project:
+ * - back-port to Java 11 (removal of Vector API operations and record types)
+ * - removal of interactive mode and system.out printing
+ * - separation of the classes in the single java and refactoring
+ * - run-time performance optimizations for dot product computation of quantized values
+ * - joining of llama3/qwen2 into one code base; multi-arch options
+ * - alignment with code from https://github.com/ggml-org/llama.cpp/
+ */
+
+package net.yacy.ai.llama3.Model;
+
+import java.io.IOException;
+import java.nio.FloatBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.function.IntFunction;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import net.yacy.ai.llama3.Llama;
+import net.yacy.ai.llama3.Tensor.FloatTensor;
+
+public final class ModelLoader {
+ private static final String TOKENIZER_LLAMA_3_MODEL = "gpt2";
+ //private static final String TOKENIZER_QWEN2_7B_MODEL = "gpt2";
+
+ private static final String LLAMA_3_PATTERN = "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}{1,3}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+";
+ private final static String QWEN2_PATTERN = "(?:'[sS]|'[tT]|'[rR][eE]|'[vV][eE]|'[mM]|'[lL][lL]|'[dD])|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+";
+
+ private static Vocabulary loadVocabulary(Map<String, Object> metadata) {
+ String model = (String) metadata.get("tokenizer.ggml.model");
+ if (!TOKENIZER_LLAMA_3_MODEL.equals(model)) {
+ throw new IllegalArgumentException("expected " + TOKENIZER_LLAMA_3_MODEL + " but found " + model);
+ }
+ String[] tokens = (String[]) metadata.get("tokenizer.ggml.tokens");
+ float[] scores = (float[]) metadata.get("tokenizer.ggml.scores");
+ return new Vocabulary(tokens, scores);
+ }
+
+ public static Llama loadModel(Path ggufPath, int contextLength, boolean loadWeights) throws IOException {
+ String name = ggufPath.getFileName().toString();
+ if (name.contains("Llama")) return loadModelLlama3(ggufPath, contextLength, loadWeights);
+ //if (name.contains("OLMo")) return loadModelLlama3(ggufPath, contextLength, loadWeights);
+ if (name.contains("Qwen3")) return loadModelQwen3(ggufPath, contextLength);
+ //if (name.contains("Qwen2")) return loadModelQwen2(ggufPath, contextLength);
+ throw new IOException("model type unknown");
+ }
+
+ private static Llama loadModelLlama3(Path ggufPath, int contextLength, boolean loadWeights) throws IOException {
+ GGUF gguf = GGUF.loadModel(ggufPath);
+ FileChannel fileChannel = FileChannel.open(ggufPath, StandardOpenOption.READ);
+ Map<String, Object> metadata = gguf.getMetadata();
+ Vocabulary vocabulary = loadVocabulary(metadata);
+ Tokenizer tokenizer = createLlama3Tokenizer(metadata, vocabulary);
+
+ Llama.Configuration config = new Llama.Configuration(
+ Arch.LLM_ARCH_LLAMA,
+ (int) metadata.get("llama.embedding_length"),
+ (int) metadata.get("llama.feed_forward_length"),
+ (int) metadata.get("llama.block_count"),
+ (int) metadata.get("llama.attention.head_count"),
+
+ metadata.containsKey("llama.attention.head_count_kv")
+ ? (int) metadata.get("llama.attention.head_count_kv")
+ : (int) metadata.get("llama.attention.head_count"),
+
+ vocabulary.size(),
+ (int) metadata.get("llama.context_length"),
+ false,
+ (float) metadata.getOrDefault("llama.attention.layer_norm_rms_epsilon", 1e-5f),
+ (float) metadata.getOrDefault("llama.rope.freq_base", 10000f)
+ ).withContextLength(contextLength);
+
+ Llama.Weights weights = null;
+ if (loadWeights) {
+ Map<String, GGMLTensorEntry> tensorEntries = GGUF.loadTensors(fileChannel, gguf.getTensorDataOffset(), gguf.getTensorInfos());
+ weights = loadWeightsLlama3(tensorEntries, config);
+ }
+ return new Llama(config, tokenizer, weights);
+ }
+
+ private static Llama.Weights loadWeightsLlama3(Map<String, GGMLTensorEntry> tensorEntries, Llama.Configuration config) {
+ boolean ropeScaling = tensorEntries.containsKey("rope_freqs");
+ float scaleFactor = 8;
+ float loFreqFactor = 1;
+ float hiFreqFactor = 3;
+ int oldContextLength = 8192;
+ Pair<float[], float[]> ropeFreqs = precomputeFreqsCis4Llama3(config.contextLength, config.headSize, config.ropeTheta,
+ ropeScaling, scaleFactor, loFreqFactor, hiFreqFactor, oldContextLength);
+ float[] ropeFreqsReal = ropeFreqs.first();
+ float[] ropeFreqsImag = ropeFreqs.second();
+
+ GGMLTensorEntry tokenEmbeddings = tensorEntries.get("token_embd.weight");
+ Llama.Weights qw = new Llama.Weights(
+ tokenEmbeddings.loadQuantized(),
+ loadArrayOfFloatBuffer(config.numberOfLayers, i -> tensorEntries.get("blk." + i + ".attn_norm.weight")),
+ loadArrayOfQuantized(config.numberOfLayers, i -> tensorEntries.get("blk." + i + ".attn_q.weight")),
+ loadArrayOfQuantized(config.numberOfLayers, i -> tensorEntries.get("blk." + i + ".attn_k.weight")),
+ loadArrayOfQuantized(config.numberOfLayers, i -> tensorEntries.get("blk." + i + ".attn_v.weight")),
+ null, null, null,
+ loadArrayOfQuantized(config.numberOfLayers, i -> tensorEntries.get("blk." + i + ".attn_output.weight")),
+ loadArrayOfFloatBuffer(config.numberOfLayers, i -> tensorEntries.get("blk." + i + ".ffn_norm.weight")),
+ loadArrayOfQuantized(config.numberOfLayers, i -> tensorEntries.get("blk." + i + ".ffn_gate.weight")), // w1
+ loadArrayOfQuantized(config.numberOfLayers, i -> tensorEntries.get("blk." + i + ".ffn_down.weight")), // w2
+ loadArrayOfQuantized(config.numberOfLayers, i -> tensorEntries.get("blk." + i + ".ffn_up.weight")), // w3
+ tensorEntries.get("output_norm.weight").toFloatBuffer(),
+ FloatBuffer.wrap(ropeFreqsReal),
+ FloatBuffer.wrap(ropeFreqsImag),
+ // If "output.weight" is not present then the embedding weights are tied/shared with the decoder.
+ // This is commonly referred as "tie word embeddings".
+ tensorEntries.getOrDefault("output.weight", tokenEmbeddings).loadQuantized()
+ );
+
+ return qw;
+ }
+
+ private static Llama loadModelQwen3(Path ggufPath, int contextLength) throws IOException {
+ // see https://github.com/ggml-org/llama.cpp/pull/12501/files
+ GGUF gguf = GGUF.loadModel(ggufPath);
+ Map<String, Object> metadata = gguf.getMetadata();
+
+ Vocabulary vocabulary = loadVocabulary(metadata);
+ Tokenizer tokenizer = createQwen2Tokenizer(metadata, vocabulary);
+
+ int modelContextLength = (int) metadata.get("qwen2.context_length");
+ if (contextLength < 0 || modelContextLength < contextLength) {
+ contextLength = modelContextLength;
+ }
+
+ Llama.Configuration config = new Llama.Configuration(
+ Arch.LLM_ARCH_QWEN3,
+ (int) metadata.get("qwen2.embedding_length"),
+ (int) metadata.get("qwen2.feed_forward_length"),
+ (int) metadata.get("qwen2.block_count"),
+ (int) metadata.get("qwen2.attention.head_count"),
+
+ metadata.containsKey("qwen2.attention.head_count_kv")
+ ? (int) metadata.get("qwen2.attention.head_count_kv")
+ : (int) metadata.get("qwen2.attention.head_count"),
+
+ vocabulary.size(),
+ contextLength,
+ false,
+ (float) metadata.get("qwen2.attention.layer_norm_rms_epsilon"),
+ (float) metadata.get("qwen2.rope.freq_base")
+ );
+
+ Map<String, GGMLTensorEntry> tensorEntries = gguf.getTensorEntries();
+
+ Pair<float[], float[]> ropeFreqs = precomputeFreqsCis4Qwen2(config.contextLength, config.headSize, config.ropeTheta);
+ float[] ropeFreqsReal = ropeFreqs.first();
+ float[] ropeFreqsImag = ropeFreqs.second();
+
+
+ FloatTensor tokenEmbeddingTable = tensorEntries.get("token_embd.weight").loadQuantized();
+ Llama.Weights qw = new Llama.Weights(
+ tokenEmbeddingTable,
+ loadArrayOfFloatBuffer(config.numberOfLayers, i -> tensorEntries.get("blk." + i + ".attn_norm.weight")),
+ loadArrayOfQuantized(config.numberOfLayers, i -> tensorEntries.get("blk." + i + ".attn_q.weight")),
+ loadArrayOfQuantized(config.numberOfLayers, i -> tensorEntries.get("blk." + i + ".attn_k.weight")),
+ loadArrayOfQuantized(config.numberOfLayers, i -> tensorEntries.get("blk." + i + ".attn_v.weight")),
+
+ loadArrayOfQuantized(config.numberOfLayers, i -> tensorEntries.get("blk." + i + ".attn_q.bias")),
+ loadArrayOfQuantized(config.numberOfLayers, i -> tensorEntries.get("blk." + i + ".attn_k.bias")),
+ loadArrayOfQuantized(config.numberOfLayers, i -> tensorEntries.get("blk." + i + ".attn_v.bias")),
+
+ loadArrayOfQuantized(config.numberOfLayers, i -> tensorEntries.get("blk." + i + ".attn_output.weight")),
+ loadArrayOfFloatBuffer(config.numberOfLayers, i -> tensorEntries.get("blk." + i + ".ffn_norm.weight")),
+ loadArrayOfQuantized(config.numberOfLayers, i -> tensorEntries.get("blk." + i + ".ffn_gate.weight")), // w1
+ loadArrayOfQuantized(config.numberOfLayers, i -> tensorEntries.get("blk." + i + ".ffn_down.weight")), // w2
+ loadArrayOfQuantized(config.numberOfLayers, i -> tensorEntries.get("blk." + i + ".ffn_up.weight")), // w3
+ tensorEntries.get("output_norm.weight").toFloatBuffer(),
+ FloatBuffer.wrap(ropeFreqsReal),
+ FloatBuffer.wrap(ropeFreqsImag),
+ tensorEntries.containsKey("output.weight")
+ ? tensorEntries.get("output.weight").loadQuantized()
+ : tokenEmbeddingTable // weights are shared
+ );
+
+ return new Llama(config, tokenizer, qw);
+ }
+
+ private static Tokenizer createLlama3Tokenizer(Map<String, Object> metadata, Vocabulary vocabulary) {
+ String[] mergeLines = (String[]) metadata.get("tokenizer.ggml.merges");
+ List<Pair<Integer, Integer>> merges = Arrays.stream(mergeLines)
+ .map(line -> line.split(" "))
+ .map(parts ->
+ new Pair<>(
+ vocabulary.getIndex(parts[0]).orElseThrow(),
+ vocabulary.getIndex(parts[1]).orElseThrow())
+ ).collect(Collectors.toList());
+
+ int allTokens = vocabulary.size();
+ int baseTokens = 128000; // assume all tokens after the base ones are special.
+ //int reservedSpecialTokens = allTokens - baseTokens;
+ List<String> specialTokensList = Arrays.stream(vocabulary.tokens(), baseTokens, allTokens).collect(Collectors.toList());
+
+ assert specialTokensList.stream().allMatch(token -> vocabulary.getIndex(token).isPresent());
+
+ Map<String, Integer> specialTokens =
+ IntStream.range(0, specialTokensList.size())
+ .boxed()
+ .collect(Collectors.toMap(
+ i -> specialTokensList.get(i),
+ i -> baseTokens + i)
+ );
+
+ return new Tokenizer(vocabulary, merges, LLAMA_3_PATTERN, specialTokens, null);
+ }
+
+ private static Tokenizer createQwen2Tokenizer(Map<String, Object> metadata, Vocabulary vocabulary) {
+ int[] tokenTypes = (int[]) metadata.get("tokenizer.ggml.token_type");
+ String[] mergeLines = (String[]) metadata.get("tokenizer.ggml.merges");
+ List<Pair<Integer, Integer>> merges = Arrays.stream(mergeLines)
+ .map(line -> line.split(" "))
+ .map(parts ->
+ new Pair<>(
+ vocabulary.getIndex(parts[0]).orElseThrow(),
+ vocabulary.getIndex(parts[1]).orElseThrow())
+ )
+ .collect(Collectors.toList());
+
+ int allTokens = vocabulary.size();
+ int baseTokens = vocabulary.getIndex("<|endoftext|>").orElseThrow(); // assume all tokens after the base ones are special.
+ //int reservedSpecialTokens = allTokens - baseTokens;
+ List<String> specialTokensList = Arrays.stream(vocabulary.tokens(), baseTokens, allTokens).collect(Collectors.toList());
+
+ assert specialTokensList.stream().allMatch(token -> vocabulary.getIndex(token).isPresent());
+
+ Map<String, Integer> specialTokens =
+ IntStream.range(0, specialTokensList.size())
+ .boxed()
+ .collect(Collectors.toMap(
+ i -> specialTokensList.get(i),
+ i -> baseTokens + i)
+ );
+
+ return new Tokenizer(vocabulary, merges, QWEN2_PATTERN, specialTokens, tokenTypes);
+ }
+
+
+ private static FloatTensor[] loadArrayOfQuantized(int size, IntFunction<GGMLTensorEntry> getTensorEntry) {
+ FloatTensor[] array = new FloatTensor[size];
+ for (int i = 0; i < size; i++) {
+ array[i] = getTensorEntry.apply(i).loadQuantized();
+ }
+ return array;
+ }
+
+ private static FloatBuffer[] loadArrayOfFloatBuffer(int size, IntFunction<GGMLTensorEntry> getTensorEntry) {
+ FloatBuffer[] array = new FloatBuffer[size];
+ for (int i = 0; i < size; i++) {
+ array[i] = getTensorEntry.apply(i).toFloatBuffer();
+ }
+ return array;
+ }
+
+ // RoPE
+
+ // for LLama3
+ private static Pair<float[], float[]> precomputeFreqsCis4Llama3(
+ int contextLength, int headSize, double theta,
+ boolean ropeScaling, float scaleFactor,
+ float loFreqFactor, float hiFreqFactor, float oldContextLength) {
+ assert headSize % 2 == 0;
+ float[] cr = new float[contextLength * (headSize / 2)];
+ float[] ci = new float[contextLength * (headSize / 2)];
+ int n = 0;
+ for (int pos = 0; pos < contextLength; ++pos) {
+ for (int i = 0; i < headSize; i += 2) {
+ float freq = (float) (1.0 / Math.pow(theta, i / (double) headSize));
+ if (ropeScaling) {
+ // Llama 3.1 scaling
+ float loFreqWavelen = oldContextLength / loFreqFactor;
+ float hiFreqWavelen = oldContextLength / hiFreqFactor;
+ float wavelen = (float) (2.0 * Math.PI / freq);
+ if (wavelen < hiFreqWavelen) {
+ //freq = freq;
+ } else if (wavelen > loFreqWavelen) {
+ freq = freq / scaleFactor;
+ } else {
+ float smooth = (oldContextLength / wavelen - loFreqFactor) / (hiFreqFactor - loFreqFactor);
+ freq = (1.0f - smooth) * freq / scaleFactor + smooth * freq;
+ }
+ }
+ float val = pos * freq;
+ cr[n] = (float) Math.cos(val);
+ ci[n] = (float) Math.sin(val);
+ n++;
+ }
+ }
+ assert contextLength * (headSize / 2) == n;
+ return new Pair<>(cr, ci);
+ }
+
+ // for Qwen3
+ private static Pair<float[], float[]> precomputeFreqsCis4Qwen2(
+ int contextLength, int headSize, double theta) {
+ assert headSize % 2 == 0;
+ float[] cr = new float[contextLength * (headSize / 2)];
+ float[] ci = new float[contextLength * (headSize / 2)];
+ int n = 0;
+ for (int pos = 0; pos < contextLength; ++pos) {
+ for (int i = 0; i < headSize; i += 2) {
+ float freq = (float) (1.0 / Math.pow(theta, i / (double) headSize));
+ float val = pos * freq;
+ cr[n] = (float) Math.cos(val);
+ ci[n] = (float) Math.sin(val);
+ n++;
+ }
+ }
+ assert contextLength * (headSize / 2) == n;
+ return new Pair<>(cr, ci);
+ }
+
+} \ No newline at end of file
diff --git a/source/net/yacy/ai/llama3/Model/Pair.java b/source/net/yacy/ai/llama3/Model/Pair.java
new file mode 100644
index 000000000..528994cc8
--- /dev/null
+++ b/source/net/yacy/ai/llama3/Model/Pair.java
@@ -0,0 +1,59 @@
+/**
+ * Pair.java
+
+ * This file was extracted from the llama3/qwen2 projects
+ * https://github.com/mukel/llama3.java
+ * https://github.com/mukel/qwen2.svm.java
+ *
+ * License: MIT License
+ *
+ * Copyright (c) 2024 Andrej Karpathy (for llama2.c)
+ * Copyright (c) 2024 Alfonso² Peterssen (for llama3/qwen2)
+ * Copyright (c) 2023 Georgi Gerganov et al. (for llama.cpp)
+ * Copyright (c) 2025 Michael Peter Christen for modifications:
+ * The code was modified to fit the YaCy AI project:
+ * - back-port to Java 11 (removal of Vector API operations and record types)
+ * - removal of interactive mode and system.out printing
+ * - separation of the classes in the single java and refactoring
+ * - run-time performance optimizations for dot product computation of quantized values
+ * - joining of llama3/qwen2 into one code base; multi-arch options
+ * - alignment with code from https://github.com/ggml-org/llama.cpp/
+ */
+
+package net.yacy.ai.llama3.Model;
+
+import java.util.Objects;
+
+
+public final class Pair<F, S> {
+ public final F first;
+ public final S second;
+
+ public Pair(F first, S second) {
+ this.first = first;
+ this.second = second;
+ }
+
+ public F first() { return first; }
+ public S second() { return second; }
+
+ // Optional factory method (avoids 'new' keyword)
+ public static <F, S> Pair<F, S> of(F first, S second) {
+ return new Pair<>(first, second);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Pair)) return false;
+ Pair<?, ?> pair = (Pair<?, ?>) o;
+ return Objects.equals(first, pair.first) &&
+ Objects.equals(second, pair.second);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(first, second);
+ }
+
+} \ No newline at end of file
diff --git a/source/net/yacy/ai/llama3/Model/Tokenizer.java b/source/net/yacy/ai/llama3/Model/Tokenizer.java
new file mode 100644
index 000000000..8b3a118a1
--- /dev/null
+++ b/source/net/yacy/ai/llama3/Model/Tokenizer.java
@@ -0,0 +1,295 @@
+/**
+ * Tokenizer.java
+
+ * This file was extracted from the llama3/qwen2 projects
+ * https://github.com/mukel/llama3.java
+ * https://github.com/mukel/qwen2.svm.java
+ *
+ * License: MIT License
+ *
+ * Copyright (c) 2024 Andrej Karpathy (for llama2.c)
+ * Copyright (c) 2024 Alfonso² Peterssen (for llama3/qwen2)
+ * Copyright (c) 2023 Georgi Gerganov et al. (for llama.cpp)
+ * Copyright (c) 2025 Michael Peter Christen for modifications:
+ * The code was modified to fit the YaCy AI project:
+ * - back-port to Java 11 (removal of Vector API operations and record types)
+ * - removal of interactive mode and system.out printing
+ * - separation of the classes in the single java and refactoring
+ * - run-time performance optimizations for dot product computation of quantized values
+ * - joining of llama3/qwen2 into one code base; multi-arch options
+ * - alignment with code from https://github.com/ggml-org/llama.cpp/
+ */
+
+package net.yacy.ai.llama3.Model;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Byte Pair Encoding tokenizer.
+ * <p>
+ * Based on <a href="https://github.com/karpathy/minbpe">minbpe</a>, algorithmically follows along the
+ * <a href="https://github.com/openai/gpt-2/blob/master/src/encoder.py">GPT 2 tokenizer</a>
+ */
+public class Tokenizer {
+ private final Pattern compiledPattern;
+ private final Vocabulary vocabulary;
+ private final Map<Pair<Integer, Integer>, Integer> merges;
+ private final Map<String, Integer> specialTokens;
+ private final int[] tokenTypes;
+
+ public String regexPattern() {
+ if (compiledPattern == null) {
+ return null;
+ }
+ return compiledPattern.pattern();
+ }
+
+ public Map<String, Integer> getSpecialTokens() {
+ return specialTokens;
+ }
+
+ public boolean isSpecialToken(int tokenIndex) {
+ return specialTokens.containsValue(tokenIndex);
+ }
+
+ public int getTokenType(int tokenIndex) {
+ return tokenTypes[tokenIndex];
+ }
+
+ public Tokenizer(Vocabulary vocabulary, List<Pair<Integer, Integer>> merges, String regexPattern, Map<String, Integer> specialTokens, int[] tokenTypes) {
+ this.vocabulary = vocabulary;
+ this.compiledPattern = regexPattern != null ? Pattern.compile(regexPattern) : null;
+ this.specialTokens = new HashMap<>(specialTokens);
+ this.merges = new HashMap<>();
+ this.tokenTypes = tokenTypes;
+ for (Pair<Integer, Integer> pair : merges) {
+ int firstIndex = pair.first();
+ int secondIndex = pair.second();
+ int mergeIndex = vocabulary.getIndex(vocabulary.get(firstIndex) + vocabulary.get(secondIndex)).orElseThrow();
+ this.merges.put(pair, mergeIndex);
+ }
+ }
+
+ private int[] encodeImpl(String text) {
+ return encode(text, Set.of()).stream().mapToInt(i -> i).toArray();
+ }
+
+ /**
+ * Unlike {@link #encodeOrdinary(String)}, this function handles special tokens.
+ * allowed_special: can be "all"|"none"|"none_raise" or a custom set of special tokens
+ * if none_raise, then an error is raised if any special token is encountered in text
+ * this is the default tiktoken behavior right now as well
+ * any other behavior is either annoying, or a major footgun.
+ */
+ List<Integer> encode(String text, Set<String> allowedSpecial) {
+ // decode the user desire w.r.t. handling of special tokens
+ Set<String> special = allowedSpecial;
+ assert getSpecialTokens().keySet().containsAll(special);
+ if (special.isEmpty()) {
+ // shortcut: if no special tokens, just use the ordinary encoding
+ return encodeOrdinary(text);
+ }
+
+ // otherwise, we have to be careful with potential special tokens in text
+ // we handle special tokens by splitting the text
+ // based on the occurrence of any exact match with any of the special tokens
+ // we can use re.split for this. note that surrounding the pattern with ()
+ // makes it into a capturing group, so the special tokens will be included
+ String specialPattern = special
+ .stream()
+ .map(Pattern::quote)
+ .collect(Collectors.joining("|", "(", ")"));
+
+ String[] specialChunks = text.split(specialPattern);
+ // now all the special characters are separated from the rest of the text
+ // all chunks of text are encoded separately, then results are joined
+ List<Integer> ids = new ArrayList<>();
+ for (String part : specialChunks) {
+ if (special.contains(part)) {
+ // this is a special token, encode it separately as a special case
+ ids.add(getSpecialTokens().get(part));
+ } else {
+ // this is an ordinary sequence, encode it normally
+ ids.addAll(encodeOrdinary(part));
+ }
+ }
+ return ids;
+ }
+
+ private static List<String> findAll(Pattern pattern, String text) {
+ List<String> allMatches = new ArrayList<>();
+ Matcher matcher = pattern.matcher(text);
+ while (matcher.find()) {
+ allMatches.add(matcher.group());
+ }
+ return allMatches;
+ }
+
+ /**
+ * Encoding that ignores any special tokens.
+ */
+ public List<Integer> encodeOrdinary(String text) {
+ // split text into chunks of text by categories defined in regex pattern
+ List<String> textChunks = findAll(compiledPattern, text);
+ // all chunks of text are encoded separately, then results are joined
+ List<Integer> ids = new ArrayList<>();
+ for (String chunk : textChunks) {
+ List<Integer> chunkIds = encodeChunk(chunk);
+ ids.addAll(chunkIds);
+ }
+ return ids;
+ }
+
+ private Map<Pair<Integer, Integer>, Integer> getStats(List<Integer> ids) {
+ Map<Pair<Integer, Integer>, Integer> map = new HashMap<>();
+ for (int i = 0; i + 1 < ids.size(); i++) {
+ Pair<Integer, Integer> key = new Pair<>(ids.get(i), ids.get(i + 1));
+ map.put(key, map.getOrDefault(key, 0) + 1);
+ }
+ return map;
+ }
+
+ private List<Integer> encodeChunk(String chunk) {
+ // return the token ids
+ // let's begin. first, convert all bytes to integers in range 0..255
+ List<Integer> ids = new ArrayList<>();
+ for (int b : chunk.toCharArray()) {
+ int tokenIndex = this.vocabulary.getIndex(String.valueOf((char) b)).orElseThrow();
+ ids.add(tokenIndex);
+ }
+
+ while (ids.size() >= 2) {
+ // find the pair with the lowest merge index
+ Map<Pair<Integer, Integer>, Integer> stats = getStats(ids);
+ Pair<Integer, Integer> pair = stats.keySet().stream().min(Comparator.comparingInt(key -> this.merges.getOrDefault(key, Integer.MAX_VALUE))).orElseThrow();
+ // subtle: if there are no more merges available, the key will
+ // result in an inf for every single pair, and the min will be
+ // just the first pair in the list, arbitrarily
+ // we can detect this terminating case by a membership check
+ if (!this.merges.containsKey(pair)) {
+ break; // nothing else can be merged anymore
+ }
+ // otherwise let's merge the best pair (lowest merge index)
+ int idx = this.merges.get(pair);
+ ids = merge(ids, pair, idx);
+ }
+ return ids;
+ }
+
+ private static List<Integer> merge(List<Integer> ids, Pair<Integer, Integer> pair, int idx) {
+ List<Integer> newids = new ArrayList<>();
+ int i = 0;
+ while (i < ids.size()) {
+ // if not at the very last position AND the pair matches, replace it
+ if (ids.get(i).equals(pair.first()) && i < ids.size() - 1 && ids.get(i + 1).equals(pair.second())) {
+ newids.add(idx);
+ i += 2;
+ } else {
+ newids.add(ids.get(i));
+ i += 1;
+ }
+ }
+ return newids;
+ }
+
+ public String decodeImpl(List<Integer> tokens) {
+ StringBuilder sb = new StringBuilder();
+ for (int token : tokens) {
+ String tokenString = vocabulary.get(token);
+ sb.append(tokenString);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Returns list of utf-8 byte and a corresponding list of unicode strings.
+ * The reversible bpe codes work on unicode strings.
+ * This means you need a large # of unicode characters in your vocab if you want to avoid UNKs.
+ * When you're at something like a 10B token dataset you end up needing around 5K for decent coverage.
+ * This is a significant percentage of your normal, say, 32K bpe vocab.
+ * To avoid that, we want lookup tables between utf-8 bytes and unicode strings.
+ * And avoids mapping to whitespace/control characters the bpe code barfs on.
+ */
+ private static Map<Integer, Integer> bytesToUnicode() {
+ List<Integer> bs = new ArrayList<>();
+ IntStream.rangeClosed('!', '~').forEach(bs::add);
+ IntStream.rangeClosed('¡', '¬').forEach(bs::add);
+ IntStream.rangeClosed('®', 'ÿ').forEach(bs::add);
+
+ List<Integer> cs = new ArrayList<>(bs);
+ int n = 0;
+ for (int b = 0; b < 256; ++b) {
+ if (!bs.contains(b)) {
+ bs.add(b);
+ cs.add(256 + n);
+ n += 1;
+ }
+ }
+
+ // return dict(zip(bs, cs))
+ return IntStream.range(0, bs.size())
+ .boxed()
+ .collect(Collectors.toMap(bs::get, cs::get));
+ }
+
+ static final Map<Integer, Integer> BYTE_ENCODER = bytesToUnicode();
+ static final Map<Integer, Integer> BYTE_DECODER = BYTE_ENCODER.entrySet()
+ .stream()
+ .collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey));
+
+ public int[] encode(String text) {
+ StringBuilder sb = new StringBuilder();
+ byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
+ for (byte b : bytes) {
+ sb.appendCodePoint(BYTE_ENCODER.get(Byte.toUnsignedInt(b)));
+ }
+ return encodeImpl(sb.toString());
+ }
+
+ /*
+ public static String replaceControlCharacters(int[] codePoints) {
+ // we don't want to print control characters
+ // which distort the output (e.g. \n or much worse)
+ // https://stackoverflow.com/questions/4324790/removing-control-characters-from-a-string-in-python/19016117#19016117
+ // http://www.unicode.org/reports/tr44/#GC_Values_Table\
+ StringBuilder chars = new StringBuilder();
+ for (int cp : codePoints) {
+ if (Character.getType(cp) == Character.CONTROL && cp != '\n') {
+ chars.append("\\u").append(HexFormat.of().toHexDigits(cp, 4)); // escape
+ } else {
+ chars.appendCodePoint(cp); // this character is ok
+ }
+ }
+ return chars.toString();
+ }
+
+ public static String replaceControlCharacters(String str) {
+ return replaceControlCharacters(str.codePoints().toArray());
+ }
+ */
+
+ public List<Integer> encodeAsList(String text) {
+ return Arrays.stream(encode(text)).boxed().collect(Collectors.toList());
+ }
+
+ public String decode(List<Integer> tokens) {
+ String decoded = decodeImpl(tokens);
+ int[] decodedBytesAsInts = decoded.codePoints().map(BYTE_DECODER::get).toArray();
+ byte[] rawBytes = new byte[decodedBytesAsInts.length];
+ for (int i = 0; i < decoded.length(); i++) {
+ rawBytes[i] = (byte) decodedBytesAsInts[i];
+ }
+ return new String(rawBytes, StandardCharsets.UTF_8);
+ }
+}
diff --git a/source/net/yacy/ai/llama3/Model/Vocabulary.java b/source/net/yacy/ai/llama3/Model/Vocabulary.java
new file mode 100644
index 000000000..f2ca7e4dd
--- /dev/null
+++ b/source/net/yacy/ai/llama3/Model/Vocabulary.java
@@ -0,0 +1,65 @@
+/**
+ * Vocabulary.java
+
+ * This file was extracted from the llama3/qwen2 projects
+ * https://github.com/mukel/llama3.java
+ * https://github.com/mukel/qwen2.svm.java
+ *
+ * License: MIT License
+ *
+ * Copyright (c) 2024 Andrej Karpathy (for llama2.c)
+ * Copyright (c) 2024 Alfonso² Peterssen (for llama3/qwen2)
+ * Copyright (c) 2023 Georgi Gerganov et al. (for llama.cpp)
+ * Copyright (c) 2025 Michael Peter Christen for modifications:
+ * The code was modified to fit the YaCy AI project:
+ * - back-port to Java 11 (removal of Vector API operations and record types)
+ * - removal of interactive mode and system.out printing
+ * - separation of the classes in the single java and refactoring
+ * - run-time performance optimizations for dot product computation of quantized values
+ * - joining of llama3/qwen2 into one code base; multi-arch options
+ * - alignment with code from https://github.com/ggml-org/llama.cpp/
+ */
+
+package net.yacy.ai.llama3.Model;
+
+import java.util.*;
+import java.util.stream.*;
+
+public final class Vocabulary {
+ private final String[] tokens;
+ private final float[] scores;
+ private final Map<String, Integer> tokenToIndex;
+
+ // Primary constructor
+ public Vocabulary(String[] tokens, float[] scores, Map<String, Integer> tokenToIndex) {
+ this.tokens = tokens == null ? null : Arrays.copyOf(tokens, tokens.length);
+ this.scores = scores == null ? null : Arrays.copyOf(scores, scores.length);
+ this.tokenToIndex = tokenToIndex == null ? null : new HashMap<>(tokenToIndex);
+ }
+
+ // Secondary constructor
+ public Vocabulary(String[] vocabulary, float[] scores) {
+ this(vocabulary, scores,
+ IntStream.range(0, vocabulary.length)
+ .boxed()
+ .collect(Collectors.toMap(i -> vocabulary[i], i -> i))
+ );
+ }
+
+ public String[] tokens() {
+ return Arrays.copyOf(tokens, tokens.length);
+ }
+
+ public String get(int tokenIndex) {
+ return tokens[tokenIndex];
+ }
+
+ public OptionalInt getIndex(String token) {
+ Integer value = tokenToIndex.get(token);
+ return value != null ? OptionalInt.of(value) : OptionalInt.empty();
+ }
+
+ public int size() {
+ return tokens.length;
+ }
+} \ No newline at end of file
diff --git a/source/net/yacy/ai/llama3/Sampler.java b/source/net/yacy/ai/llama3/Sampler.java
new file mode 100644
index 000000000..c94876cab
--- /dev/null
+++ b/source/net/yacy/ai/llama3/Sampler.java
@@ -0,0 +1,175 @@
+/**
+ * Sampler.java
+
+ * This file was extracted from the llama3/qwen2 projects
+ * https://github.com/mukel/llama3.java
+ * https://github.com/mukel/qwen2.svm.java
+ *
+ * License: MIT License
+ *
+ * Copyright (c) 2024 Andrej Karpathy (for llama2.c)
+ * Copyright (c) 2024 Alfonso² Peterssen (for llama3/qwen2)
+ * Copyright (c) 2023 Georgi Gerganov et al. (for llama.cpp)
+ * Copyright (c) 2025 Michael Peter Christen for modifications:
+ * The code was modified to fit the YaCy AI project:
+ * - back-port to Java 11 (removal of Vector API operations and record types)
+ * - removal of interactive mode and system.out printing
+ * - separation of the classes in the single java and refactoring
+ * - run-time performance optimizations for dot product computation of quantized values
+ * - joining of llama3/qwen2 into one code base; multi-arch options
+ * - alignment with code from https://github.com/ggml-org/llama.cpp/
+ */
+
+package net.yacy.ai.llama3;
+
+import java.util.Comparator;
+import java.util.Random;
+
+import net.yacy.ai.llama3.Tensor.FloatTensor;
+
+@FunctionalInterface
+interface Sampler {
+ int sampleToken(FloatTensor logits);
+
+ Sampler ARGMAX = FloatTensor::argmax;
+
+ static Sampler selectSampler(int vocabularySize, float temperature, float topp, long rngSeed) {
+ Sampler sampler;
+ if (temperature == 0.0f) {
+ // greedy argmax sampling: take the token with the highest probability
+ sampler = Sampler.ARGMAX;
+ } else {
+ // we sample from this distribution to get the next token
+ // RandomGeneratorFactory.getDefault().create(rngSeed); requires additonal native-image configuration.
+ Random rng = new Random(rngSeed);
+ Sampler innerSampler;
+ if (topp <= 0 || topp >= 1) {
+ // simply sample from the predicted probability distribution
+ innerSampler = new CategoricalSampler(rng);
+ } else {
+ // top-p (nucleus) sampling, clamping the least likely tokens to zero
+ innerSampler = new ToppSampler(vocabularySize, topp, rng);
+ }
+ sampler = logits -> {
+ // apply the temperature to the logits
+ logits.divideInPlace(0, logits.size(), temperature);
+ // apply softmax to the logits to get the probabilities for next token
+ logits.softmaxInPlace(0, logits.size());
+ return innerSampler.sampleToken(logits);
+ };
+ }
+ return sampler;
+ }
+
+ static class CategoricalSampler implements Sampler {
+
+ final Random rng;
+
+ public CategoricalSampler(Random rng) {
+ this.rng = rng;
+ }
+
+ @Override
+ public int sampleToken(FloatTensor logits) {
+ // sample index from probabilities (they must sum to 1!)
+ float random0to1 = rng.nextFloat();
+ float cdf = 0.0f;
+ for (int i = 0; i < logits.size(); i++) {
+ cdf += logits.getFloat(i);
+ if (random0to1 < cdf) {
+ return i;
+ }
+ }
+ return logits.size() - 1; // in case of rounding errors
+ }
+ }
+
+ static class ToppSampler implements Sampler {
+
+ final int[] indices;
+ final float topp;
+ final Random rng;
+
+ public ToppSampler(int maxNumberOfElements, float topp, Random rng) {
+ this.indices = new int[maxNumberOfElements];
+ this.topp = topp;
+ this.rng = rng;
+ }
+
+ static void swap(int[] array, int from, int to) {
+ int tmp = array[from];
+ array[from] = array[to];
+ array[to] = tmp;
+ }
+
+ static void siftDown(int[] array, int from, int n, Comparator<Integer> comparator) {
+ int prev = from, next;
+ while ((next = 2 * prev + 1) < n) {
+ int r = 2 * prev + 2;
+ if (r < n && comparator.compare(array[r], array[next]) < 0) {
+ next = r;
+ }
+ if (comparator.compare(array[next], array[prev]) < 0) {
+ swap(array, prev, next);
+ prev = next;
+ } else {
+ break;
+ }
+ }
+ }
+
+ @Override
+ public int sampleToken(FloatTensor logits) {
+ // top-p sampling (or "nucleus sampling") samples from the smallest set of
+ // tokens that exceed probability topp. This way we never sample tokens that
+ // have very low probabilities and are less likely to go "off the rails".
+ Comparator<Integer> comparator = Comparator.comparingDouble(logits::getFloat).reversed();
+
+ int n = logits.size();
+ int head = 0;
+ int tail = n - 1;
+ // values smaller than (1 - topp) / (n - 1) cannot be part of the result
+ // so for efficiency we crop these out as candidates before sorting
+ float cutoff = (1.0f - topp) / (n - 1);
+ for (int i = 0; i < indices.length; i++) {
+ if (logits.getFloat(i) >= cutoff) {
+ indices[head++] = i;
+ } else {
+ indices[tail--] = i;
+ }
+ }
+
+ int n0 = head;
+ // build heap O(n0)
+ for (int i = n0 / 2 - 1; i >= 0; --i) {
+ siftDown(indices, i, n0, comparator);
+ }
+
+ // truncate the list where cumulative probability of the largest k elements exceeds topp
+ // O(k lg n0)
+ float cumulativeProb = 0.0f;
+ int lastIndex = 0;
+ for (int i = n0 - 1; i >= 0; i--) {
+ swap(indices, 0, i);
+ cumulativeProb += logits.getFloat(indices[i]);
+ if (cumulativeProb > topp) {
+ lastIndex = i;
+ break; // we've exceeded topp by including lastIndex
+ }
+ siftDown(indices, 0, i - 1, comparator);
+ }
+
+ // sample from the truncated list
+ float r = rng.nextFloat() * cumulativeProb;
+ float cdf = 0.0f;
+ for (int i = n0 - 1; i >= lastIndex; i--) {
+ cdf += logits.getFloat(indices[i]);
+ if (r < cdf) {
+ return indices[i];
+ }
+ }
+
+ return indices[lastIndex]; // in case of rounding errors
+ }
+ }
+}
diff --git a/source/net/yacy/ai/llama3/Tensor/ArrayFloatTensor.java b/source/net/yacy/ai/llama3/Tensor/ArrayFloatTensor.java
new file mode 100644
index 000000000..9e0155eba
--- /dev/null
+++ b/source/net/yacy/ai/llama3/Tensor/ArrayFloatTensor.java
@@ -0,0 +1,177 @@
+/**
+ * ArrayFloatTensor.java
+
+ * This file was extracted from the llama3/qwen2 projects
+ * https://github.com/mukel/llama3.java
+ * https://github.com/mukel/qwen2.svm.java
+ *
+ * License: MIT License
+ *
+ * Copyright (c) 2024 Andrej Karpathy (for llama2.c)
+ * Copyright (c) 2024 Alfonso² Peterssen (for llama3/qwen2)
+ * Copyright (c) 2023 Georgi Gerganov et al. (for llama.cpp)
+ * Copyright (c) 2025 Michael Peter Christen for modifications:
+ * The code was modified to fit the YaCy AI project:
+ * - back-port to Java 11 (removal of Vector API operations and record types)
+ * - removal of interactive mode and system.out printing
+ * - separation of the classes in the single java and refactoring
+ * - run-time performance optimizations for dot product computation of quantized values
+ * - joining of llama3/qwen2 into one code base; multi-arch options
+ * - alignment with code from https://github.com/ggml-org/llama.cpp/
+ */
+
+package net.yacy.ai.llama3.Tensor;
+
+import java.util.Arrays;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+
+import net.yacy.ai.llama3.Model.GGMLType;
+
+public final class ArrayFloatTensor extends FloatTensor {
+
+ public final float[] values;
+ private static final VarHandle FLOAT_ARRAY_HANDLE;
+
+ static {
+ try {
+ FLOAT_ARRAY_HANDLE = MethodHandles.arrayElementVarHandle(float[].class);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to get VarHandle for float[]", e);
+ }
+ }
+
+ public ArrayFloatTensor(final float[] values) {
+ this.values = values;
+ }
+
+ public static FloatTensor allocate(final int... dims) {
+ int numberOfElements = FloatTensor.numberOfElements(dims);
+ return new ArrayFloatTensor(new float[numberOfElements]);
+ }
+
+ @Override
+ public final int size() {
+ return this.values.length;
+ }
+
+ @Override
+ public final float getFloat(final int index) {
+ return (float) FLOAT_ARRAY_HANDLE.get(this.values, index);
+ }
+
+ @Override
+ public final void setFloat(final int index, final float value) {
+ FLOAT_ARRAY_HANDLE.set(this.values, index, value);
+ }
+
+ @Override
+ public GGMLType type() {
+ return GGMLType.F32;
+ }
+
+ @Override
+ public final FloatTensor fillInPlace(final int thisOffset, final int size, final float value) {
+ Arrays.fill(this.values, thisOffset, thisOffset + size, value);
+ return this;
+ }
+
+ @Override
+ public final FloatTensor mapInPlace(final int thisOffset, final int size, MapFunction mapFunction) {
+ int endIndex = thisOffset + size;
+ for (int i = thisOffset; i < endIndex; ++i) {
+ this.values[i] = mapFunction.apply(this.values[i]);
+ }
+ return this;
+ }
+
+ @Override
+ public final void copyTo(final int thisOffset, final FloatTensor that, final int thatOffset, final int size) {
+ final int delta = thisOffset - thatOffset;
+ if (that instanceof ArrayFloatTensor) {
+ final ArrayFloatTensor aft = (ArrayFloatTensor) that;
+ int endOffset = thatOffset + size;
+ for (int i = thatOffset; i < endOffset; ++i) {
+ FLOAT_ARRAY_HANDLE.set(aft.values, i, (float) FLOAT_ARRAY_HANDLE.get(this.values, i + delta));
+ }
+ } else {
+ int endOffset = thatOffset + size;
+ for (int i = thatOffset; i < endOffset; ++i) {
+ that.setFloat(i, (float) FLOAT_ARRAY_HANDLE.get(this.values, i + delta));
+ }
+ }
+ }
+
+ @Override
+ public final float dot(final int thisOffset, final FloatTensor that, final int thatOffset, final int size) {
+ float result = 0f;
+ if (that instanceof ArrayFloatTensor) {
+ final ArrayFloatTensor aft = (ArrayFloatTensor) that;
+ final float[] a = this.values;
+ final float[] b = aft.values;
+
+ for (int i = 0; i < size; i++) {
+ final float valA = (float) FLOAT_ARRAY_HANDLE.get(a, thisOffset + i);
+ final float valB = (float) FLOAT_ARRAY_HANDLE.get(b, thatOffset + i);
+ result += valA * valB;
+ }
+ } else {
+ final float[] a = this.values;
+ for (int i = 0; i < size; i++) {
+ final float valA = (float) FLOAT_ARRAY_HANDLE.get(a, thisOffset + i);
+ result += valA * that.getFloat(thatOffset + i);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public final void matmul(final FloatTensor that, final FloatTensor out, final int dim0, final int dim1) {
+ if (that instanceof ArrayFloatTensor) {
+ parallelFor(0, dim0, i -> ((ArrayFloatTensor) out).values[i] = this.dot(i * dim1, that, 0, dim1));
+ } else {
+ parallelFor(0, dim0, i -> out.setFloat(i, this.dot(i * dim1, that, 0, dim1)));
+ }
+ }
+
+ @Override
+ public final void matmul(final int context, final FloatTensor[] that, final FloatTensor[] out, final int dim0, final int dim1) {
+ if (that.length != out.length) {
+ throw new IllegalArgumentException(String.format("that.len=%d, out.len=%d", that.length, out.length));
+ }
+ parallelForLong(0, dim0 * context, ti -> {
+ int idxArr = (int) (ti / dim0);
+ int i = (int) (ti % dim0);
+ out[idxArr].setFloat(i, this.dot(i * dim1, that[idxArr], 0, dim1));
+ });
+ }
+
+ @Override
+ public final FloatTensor saxpyInPlace(final int thisOffset, final FloatTensor that, final int thatOffset, final int size, final float a) {
+ if (that instanceof Q4_0FloatTensor) {
+ Q4_0FloatTensor qft = (Q4_0FloatTensor) that;
+ final float[] decodedBlock = Q4_0FloatTensor.scratchBuffer.get();
+ int remaining = size;
+ int i = 0;
+
+ while (remaining > 0) {
+ int chunkSize = Math.min(remaining, GGMLType.Q4_0.blockSize);
+ qft.getFloatArray(thatOffset + i, decodedBlock, 0, chunkSize);
+ for (int j = 0; j < chunkSize; ++j) {
+ int dstIdx = thisOffset + i + j;
+ this.setFloat(dstIdx, a * decodedBlock[j] + this.getFloat(dstIdx));
+ }
+ i += chunkSize;
+ remaining -= chunkSize;
+ }
+ } else {
+ for (int i = 0; i < size; ++i) {
+ int idx = thisOffset + i;
+ this.setFloat(idx, a * that.getFloat(thatOffset + i) + this.getFloat(idx));
+ }
+ }
+
+ return this;
+ }
+
+} \ No newline at end of file
diff --git a/source/net/yacy/ai/llama3/Tensor/BF16FloatTensor.java b/source/net/yacy/ai/llama3/Tensor/BF16FloatTensor.java
new file mode 100644
index 000000000..0de5ac525
--- /dev/null
+++ b/source/net/yacy/ai/llama3/Tensor/BF16FloatTensor.java
@@ -0,0 +1,74 @@
+/**
+ * BF16FloatTensor.java
+
+ * This file was extracted from the llama3/qwen2 projects
+ * https://github.com/mukel/llama3.java
+ * https://github.com/mukel/qwen2.svm.java
+ *
+ * License: MIT License
+ *
+ * Copyright (c) 2024 Andrej Karpathy (for llama2.c)
+ * Copyright (c) 2024 Alfonso² Peterssen (for llama3/qwen2)
+ * Copyright (c) 2023 Georgi Gerganov et al. (for llama.cpp)
+ * Copyright (c) 2025 Michael Peter Christen for modifications:
+ * The code was modified to fit the YaCy AI project:
+ * - back-port to Java 11 (removal of Vector API operations and record types)
+ * - removal of interactive mode and system.out printing
+ * - separation of the classes in the single java and refactoring
+ * - run-time performance optimizations for dot product computation of quantized values
+ * - joining of llama3/qwen2 into one code base; multi-arch options
+ * - alignment with code from https://github.com/ggml-org/llama.cpp/
+ */
+
+package net.yacy.ai.llama3.Tensor;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+import net.yacy.ai.llama3.Model.GGMLType;
+
+public final class BF16FloatTensor extends FloatTensor {
+
+ final int size;
+ final ByteBuffer buffer;
+
+ public BF16FloatTensor(int size, ByteBuffer buffer) {
+ if (buffer.remaining() < size * GGMLType.BFLOAT16_BYTES) {
+ throw new IllegalArgumentException("Buffer too small");
+ }
+ this.size = size;
+ this.buffer = buffer.duplicate().order(ByteOrder.nativeOrder());
+ }
+
+ public BF16FloatTensor(final float[] values) {
+ this.size = values.length;
+ this.buffer = ByteBuffer.allocateDirect(size * GGMLType.BFLOAT16_BYTES).order(ByteOrder.nativeOrder());
+ for (int i = 0; i < size; i++) {
+ setFloat(i, values[i]);
+ }
+ }
+
+ @Override
+ public int size() {
+ return size;
+ }
+
+ @Override
+ public GGMLType type() {
+ return GGMLType.BF16;
+ }
+
+ @Override
+ public final void setFloat(int index, float value) {
+ assert 0 <= index && index < size;
+ int hBits = Float.floatToIntBits(value) >>> 16; // convert float to bfloat16
+ buffer.putShort(index * GGMLType.BFLOAT16_BYTES, (short) hBits);
+ }
+
+ @Override
+ public final float getFloat(int index) {
+ assert 0 <= index && index < size;
+ return Float.intBitsToFloat(buffer.getShort(index * GGMLType.BFLOAT16_BYTES) << 16);
+ }
+
+}
diff --git a/source/net/yacy/ai/llama3/Tensor/DirectBufferFloatTensor.java b/source/net/yacy/ai/llama3/Tensor/DirectBufferFloatTensor.java
new file mode 100644
index 000000000..64c615fec
--- /dev/null
+++ b/source/net/yacy/ai/llama3/Tensor/DirectBufferFloatTensor.java
@@ -0,0 +1,111 @@
+/**
+ * DirectBufferFloatTensor
+ * Copyright 2025 by Michael Peter Christen
+ * First released 19.06.2025 at https://yacy.net
+ *
+ ** This class was not part of the original llama3 implementation,
+ ** but added later by the author to support different architectures.
+ ** It therefore does not inherit the llama3 copyright.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program in the file lgpl21.txt
+ * If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.yacy.ai.llama3.Tensor;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+
+import net.yacy.ai.llama3.Model.GGMLType;
+
+public class DirectBufferFloatTensor extends FloatTensor {
+
+ final FloatBuffer floatBuffer;
+
+ public DirectBufferFloatTensor(ByteBuffer byteBuffer) {
+ if (byteBuffer.isDirect()) {
+ this.floatBuffer = byteBuffer.asFloatBuffer();
+ } else {
+ int capacityBytes = byteBuffer.remaining();
+ ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(capacityBytes).order(byteBuffer.order());
+ directByteBuffer.put(byteBuffer.duplicate());
+ directByteBuffer.flip();
+ this.floatBuffer = directByteBuffer.asFloatBuffer();
+ }
+ }
+
+ public DirectBufferFloatTensor(final float[] values) {
+ int capacityBytes = values.length * Float.BYTES;
+ ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(capacityBytes).order(ByteOrder.nativeOrder());
+ this.floatBuffer = directByteBuffer.asFloatBuffer();
+ this.floatBuffer.put(values);
+ }
+
+ public static FloatTensor allocate(final int... dims) {
+ int numberOfElements = FloatTensor.numberOfElements(dims);
+ int bytesNeeded = numberOfElements * Float.BYTES;
+ ByteBuffer buffer = ByteBuffer.allocateDirect(bytesNeeded).order(ByteOrder.nativeOrder());
+ return new DirectBufferFloatTensor(buffer);
+ }
+
+ @Override
+ public final int size() {
+ return this.floatBuffer.capacity();
+ }
+
+ @Override
+ public final float getFloat(final int index) {
+ return this.floatBuffer.get(index);
+ }
+
+ @Override
+ public final void setFloat(final int index, final float value) {
+ this.floatBuffer.put(index, value);
+ }
+
+ @Override
+ public final GGMLType type() {
+ return GGMLType.F32;
+ }
+
+ @Override
+ public final float dot(final int thisOffset, final FloatTensor that, final int thatOffset, final int size) {
+ float result = 0f;
+ for (int j = 0; j < size; j++) {
+ result += this.floatBuffer.get(thisOffset + j) * that.getFloat(thatOffset + j);
+ }
+ return result;
+ }
+
+ @Override
+ public final FloatTensor fillInPlace(final int thisOffset, final int size, final float value) {
+ int end = thisOffset + size;
+ for (int i = thisOffset; i < end; i++) {
+ floatBuffer.put(i, value);
+ }
+ return this;
+ }
+
+ @Override
+ public final FloatTensor mapInPlace(final int thisOffset, final int size, MapFunction mapFunction) {
+ int end = thisOffset + size;
+ for (int i = thisOffset; i < end; i++) {
+ float current = floatBuffer.get(i);
+ floatBuffer.put(i, mapFunction.apply(current));
+ }
+ return this;
+ }
+
+} \ No newline at end of file
diff --git a/source/net/yacy/ai/llama3/Tensor/F16FloatTensor.java b/source/net/yacy/ai/llama3/Tensor/F16FloatTensor.java
new file mode 100644
index 000000000..d41fdbc87
--- /dev/null
+++ b/source/net/yacy/ai/llama3/Tensor/F16FloatTensor.java
@@ -0,0 +1,75 @@
+/**
+ * F16FloatTensor.java
+
+ * This file was extracted from the llama3/qwen2 projects
+ * https://github.com/mukel/llama3.java
+ * https://github.com/mukel/qwen2.svm.java
+ *
+ * License: MIT License
+ *
+ * Copyright (c) 2024 Andrej Karpathy (for llama2.c)
+ * Copyright (c) 2024 Alfonso² Peterssen (for llama3/qwen2)
+ * Copyright (c) 2023 Georgi Gerganov et al. (for llama.cpp)
+ * Copyright (c) 2025 Michael Peter Christen for modifications:
+ * The code was modified to fit the YaCy AI project:
+ * - back-port to Java 11 (removal of Vector API operations and record types)
+ * - removal of interactive mode and system.out printing
+ * - separation of the classes in the single java and refactoring
+ * - run-time performance optimizations for dot product computation of quantized values
+ * - joining of llama3/qwen2 into one code base; multi-arch options
+ * - alignment with code from https://github.com/ggml-org/llama.cpp/
+ */
+
+
+package net.yacy.ai.llama3.Tensor;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+import net.yacy.ai.llama3.Model.GGMLType;
+
+public final class F16FloatTensor extends FloatTensor {
+
+ final int size;
+ final ByteBuffer buffer;
+
+ public F16FloatTensor(int size, ByteBuffer buffer) {
+ if (buffer.remaining() < size * GGMLType.FLOAT16_BYTES) {
+ throw new IllegalArgumentException("Buffer too small");
+ }
+ this.size = size;
+ this.buffer = buffer.duplicate().order(ByteOrder.nativeOrder());
+ }
+
+ public F16FloatTensor(final float[] values) {
+ this.size = values.length;
+ this.buffer = ByteBuffer.allocateDirect(size * GGMLType.FLOAT16_BYTES).order(ByteOrder.nativeOrder());
+ for (int i = 0; i < size; i++) {
+ setFloat(i, values[i]);
+ }
+ }
+
+ @Override
+ public int size() {
+ return size;
+ }
+
+ @Override
+ public GGMLType type() {
+ return GGMLType.F16;
+ }
+
+ @Override
+ public final void setFloat(int index, float value) {
+ assert 0 <= index && index < size;
+ short hBits = floatToFloat16(value);
+ buffer.putShort(index * GGMLType.FLOAT16_BYTES, hBits);
+ }
+
+ @Override
+ public final float getFloat(int index) {
+ assert 0 <= index && index < size;
+ return float16ToFloat(buffer.getShort(index * GGMLType.FLOAT16_BYTES));
+ }
+
+}
diff --git a/source/net/yacy/ai/llama3/Tensor/FloatTensor.java b/source/net/yacy/ai/llama3/Tensor/FloatTensor.java
new file mode 100644
index 000000000..4457b10d0
--- /dev/null
+++ b/source/net/yacy/ai/llama3/Tensor/FloatTensor.java
@@ -0,0 +1,286 @@
+/**
+ * FloatTensor.java
+
+ * This file was extracted from the llama3/qwen2 projects
+ * https://github.com/mukel/llama3.java
+ * https://github.com/mukel/qwen2.svm.java
+ *
+ * License: MIT License
+ *
+ * Copyright (c) 2024 Andrej Karpathy (for llama2.c)
+ * Copyright (c) 2024 Alfonso² Peterssen (for llama3/qwen2)
+ * Copyright (c) 2023 Georgi Gerganov et al. (for llama.cpp)
+ * Copyright (c) 2025 Michael Peter Christen for modifications:
+ * The code was modified to fit the YaCy AI project:
+ * - back-port to Java 11 (removal of Vector API operations and record types)
+ * - removal of interactive mode and system.out printing
+ * - separation of the classes in the single java and refactoring
+ * - run-time performance optimizations for dot product computation of quantized values
+ * - joining of llama3/qwen2 into one code base; multi-arch options
+ * - alignment with code from https://github.com/ggml-org/llama.cpp/
+ */
+
+package net.yacy.ai.llama3.Tensor;
+
+import java.util.Arrays;
+import java.util.function.IntConsumer;
+import java.util.function.LongConsumer;
+import java.util.stream.IntStream;
+import java.util.stream.LongStream;
+
+import net.yacy.ai.llama3.Model.GGMLType;
+
+/**
+ * Over-simplified, shapeless, float tensor.
+ * <p>
+ * Not a strict tensor, but rather just a sequence of floats, not required to be backed by memory
+ * e.g. can represent a sequence of quantized floats.
+ */
+public abstract class FloatTensor {
+
+ /**
+ * Converts a 16-bit float (half-precision) to a 32-bit float (single-precision).
+ *
+ * @param h the half-precision float as a short
+ * @return the single-precision float
+ */
+ public final static float float16ToFloat(short h) {
+
+ final int hBits = h & 0xFFFF; // treat as unsigned
+ final int sign = (hBits >>> 15) & 0x00000001;
+
+ int exp = (hBits >>> 10) & 0x0000001F;
+ int mant = hBits & 0x000003FF;
+ int fBits;
+
+ if (exp == 0) {
+ if (mant == 0) {
+ // zero
+ fBits = sign << 31;
+ } else {
+ // subnormal
+ while ((mant & 0x00000400) == 0) {
+ mant <<= 1;
+ exp -= 1;
+ }
+ exp += 1;
+ mant &= ~0x00000400;
+ fBits = (sign << 31) | ((exp + 127 - 15) << 23) | (mant << 13);
+ }
+ } else if (exp == 31) {
+ // Inf/NaN
+ fBits = (sign << 31) | 0x7F800000 | (mant << 13);
+ } else {
+ // normalized number
+ fBits = (sign << 31) | ((exp + 127 - 15) << 23) | (mant << 13);
+ }
+
+ return Float.intBitsToFloat(fBits);
+ }
+
+ /**
+ * Converts a 32-bit float (single-precision) to a 16-bit float (half-precision).
+ *
+ * @param f the single-precision float
+ * @return the half-precision float as a short
+ */
+ public final static short floatToFloat16(final float f) {
+ final int fBits = Float.floatToIntBits(f);
+ final int sign = (fBits >>> 31) & 0x00000001;
+ final int exp = (fBits >>> 23) & 0x000000FF;
+ final int mant = fBits & 0x007FFFFF;
+
+ short hBits;
+
+ if (exp == 0xFF) {
+ // Inf/NaN
+ hBits = (short) ((sign << 15) | 0x7C00 | (mant >>> 13));
+ } else if (exp < 112) {
+ // subnormal or zero
+ hBits = (short) (sign << 15);
+ } else if (exp > 143) {
+ // overflow to Inf
+ hBits = (short) ((sign << 15) | 0x7C00);
+ } else {
+ // normalized number
+ hBits = (short) ((sign << 15) | ((exp - 112) << 10) | (mant >>> 13));
+ }
+
+ return hBits;
+ }
+
+ public static void parallelFor(final int startInclusive, final int endExclusive, final IntConsumer action) {
+ if (startInclusive == 0 && endExclusive == 1) {
+ action.accept(0);
+ return;
+ }
+ IntStream.range(startInclusive, endExclusive).parallel().forEach(action);
+ }
+
+ public static void parallelForLong(final long startInclusive, final long endExclusive, final LongConsumer action) {
+ if (startInclusive == 0 && endExclusive == 1) {
+ action.accept(0);
+ return;
+ }
+ LongStream.range(startInclusive, endExclusive).parallel().forEach(action);
+ }
+
+ public abstract int size();
+
+ public abstract float getFloat(final int index);
+
+ public abstract void setFloat(final int index, final float value);
+
+ abstract GGMLType type();
+
+ public static int numberOfElements(final int... dimensions) {
+ assert Arrays.stream(dimensions).allMatch(i -> i > 0);
+ return Arrays.stream(dimensions).reduce(Math::multiplyExact).orElseThrow();
+ }
+
+ public static float scalarDot(final FloatTensor thiz, final int thisOffset, final FloatTensor that, final int thatOffset, final int size) {
+ float result = 0f;
+ for (int j = 0; j < size; j++) {
+ result += thiz.getFloat(thisOffset + j) * that.getFloat(thatOffset + j);
+ }
+ return result;
+ }
+
+ public float dot(final int thisOffset, final FloatTensor that, final int thatOffset, final int size) {
+ return scalarDot(this, thisOffset, that, thatOffset, size);
+ }
+
+ public void matmul(final FloatTensor that, final FloatTensor out, final int dim0, final int dim1) {
+ parallelFor(0, dim0, i -> out.setFloat(i, dot(i * dim1, that, 0, dim1)));
+ }
+
+ public void matmul(final int context, final FloatTensor[] that, final FloatTensor[] out, final int dim0, final int dim1) {
+ if (that.length != out.length) {
+ throw new IllegalArgumentException(String.format("that.len=%d, out.len=%d", that.length, out.length));
+ }
+ parallelForLong(0, dim0 * context, ti -> {
+ int idxArr = (int) (ti / dim0);
+ int i = (int) (ti % dim0);
+ out[idxArr].setFloat(i, dot(i * dim1, that[idxArr], 0, dim1));
+ });
+ }
+
+ @FunctionalInterface
+ public interface AggregateFunction {
+ float apply(float acc, float value);
+ }
+
+ public float reduce(final int thisOffset, final int size, final float seed, final AggregateFunction reduce) {
+ float result = seed;
+ for (int i = 0; i < size; ++i) {
+ result = reduce.apply(result, getFloat(thisOffset + i));
+ }
+ return result;
+ }
+
+ private float sum(final int thisOffset, final int size) {
+ return reduce(thisOffset, size, 0f, Float::sum);
+ }
+
+ private float max(final int thisOffset, final int size) {
+ return reduce(thisOffset, size, Float.NEGATIVE_INFINITY, Float::max);
+ }
+
+ public void copyTo(final int thisOffset, final FloatTensor that, final int thatOffset, final int size) {
+ int endOffset = thatOffset + size;
+ for (int i = thatOffset; i < endOffset; ++i) {
+ that.setFloat(i, this.getFloat(i - thatOffset + thisOffset));
+ }
+ }
+
+ private int argmax(final int thisOffset, final int size) {
+ assert size > 0;
+ int maxIndex = thisOffset;
+ float maxValue = this.getFloat(maxIndex);
+ int endIndex = thisOffset + size;
+ for (int i = thisOffset; i < endIndex; ++i) {
+ float f = this.getFloat(i);
+ if (f > maxValue) {
+ maxValue = f;
+ maxIndex = i;
+ }
+ }
+ return maxIndex;
+ }
+
+ public int argmax() {
+ return argmax(0, size());
+ }
+
+ @FunctionalInterface
+ public interface MapFunction {
+ float apply(float value);
+ }
+
+ @FunctionalInterface
+ public interface MapWithIndexFunction {
+ float apply(float value, int index);
+ }
+
+ public FloatTensor mapInPlace(final int thisOffset, final int size, MapFunction mapFunction) {
+ int endIndex = thisOffset + size;
+ for (int i = thisOffset; i < endIndex; ++i) {
+ setFloat(i, mapFunction.apply(getFloat(i)));
+ }
+ return this;
+ }
+
+ public final FloatTensor mapInPlace(final MapFunction mapFunction) {
+ return mapInPlace(0, size(), mapFunction);
+ }
+
+ public final FloatTensor mapWithIndexInPlace(final int thisOffset, final int size, final FloatTensor.MapWithIndexFunction mapWithIndexFunction) {
+ int endOffset = thisOffset + size;
+ for (int i = thisOffset; i < endOffset; ++i) {
+ setFloat(i, mapWithIndexFunction.apply(getFloat(i), i));
+ }
+ return this;
+ }
+
+ private final FloatTensor addInPlace(final int thisOffset, final FloatTensor that, final int thatOffset, int size) {
+ return mapWithIndexInPlace(thisOffset, size, (value, index) -> value + that.getFloat(index - thisOffset + thatOffset));
+ }
+
+ public final FloatTensor addInPlace(final FloatTensor that) {
+ return addInPlace(0, that, 0, size());
+ }
+
+ private final FloatTensor multiplyInPlace(final int thisOffset, final FloatTensor that, final int thatOffset, final int size) {
+ return mapWithIndexInPlace(thisOffset, size, (value, index) -> value * that.getFloat(index - thisOffset + thatOffset));
+ }
+
+ public final FloatTensor multiplyInPlace(final FloatTensor that) {
+ return multiplyInPlace(0, that, 0, size());
+ }
+
+ public final FloatTensor divideInPlace(final int thisOffset, final int size, final float value) {
+ return mapInPlace(thisOffset, size, f -> f / value);
+ }
+
+ public FloatTensor fillInPlace(final int thisOffset, final int size, final float value) {
+ return mapInPlace(thisOffset, size, unused -> value);
+ }
+
+ public final FloatTensor softmaxInPlace(final int thisOffset, final int size) {
+ // find max value (for numerical stability)
+ float maxVal = max(thisOffset, size);
+ // exp and sum
+ mapInPlace(thisOffset, size, f -> (float) Math.exp(f - maxVal));
+ float sum = sum(thisOffset, size);
+ // normalize
+ return divideInPlace(thisOffset, size, sum);
+ }
+
+ public FloatTensor saxpyInPlace(final int thisOffset, final FloatTensor that, final int thatOffset, final int size, final float a) {
+ // this[thatOffset ... thatOffset + size) = a * that[thatOffset ... thatOffset + size) + this[thisOffset ... thisOffset + size)
+ for (int i = 0; i < size; ++i) {
+ this.setFloat(thisOffset + i, a * that.getFloat(thatOffset + i) + this.getFloat(thisOffset + i));
+ }
+ return this;
+ }
+} \ No newline at end of file
diff --git a/source/net/yacy/ai/llama3/Tensor/Q4_0FloatTensor.java b/source/net/yacy/ai/llama3/Tensor/Q4_0FloatTensor.java
new file mode 100644
index 000000000..b4e60379a
--- /dev/null
+++ b/source/net/yacy/ai/llama3/Tensor/Q4_0FloatTensor.java
@@ -0,0 +1,219 @@
+/**
+ * Q4_0FloatTensor.java
+
+ * This file was extracted from the llama3/qwen2 projects
+ * https://github.com/mukel/llama3.java
+ * https://github.com/mukel/qwen2.svm.java
+ *
+ * License: MIT License
+ *
+ * Copyright (c) 2024 Andrej Karpathy (for llama2.c)
+ * Copyright (c) 2024 Alfonso² Peterssen (for llama3/qwen2)
+ * Copyright (c) 2023 Georgi Gerganov et al. (for llama.cpp)
+ * Copyright (c) 2025 Michael Peter Christen for modifications:
+ * The code was modified to fit the YaCy AI project:
+ * - back-port to Java 11 (removal of Vector API operations and record types)
+ * - removal of interactive mode and system.out printing
+ * - separation of the classes in the single java and refactoring
+ * - run-time performance optimizations for dot product computation of quantized values
+ * - joining of llama3/qwen2 into one code base; multi-arch options
+ * - alignment with code from https://github.com/ggml-org/llama.cpp/
+ */
+
+package net.yacy.ai.llama3.Tensor;
+
+import java.nio.ByteBuffer;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+
+import net.yacy.ai.llama3.Model.GGMLType;
+
+public final class Q4_0FloatTensor extends FloatTensor {
+
+ private final int size;
+ private final ByteBuffer buffer;
+
+ public Q4_0FloatTensor(final int size, final ByteBuffer buffer) {
+ this.size = size;
+ this.buffer = buffer;
+ }
+
+ private static final VarHandle FLOAT_ARRAY_HANDLE;
+
+ static {
+ try {
+ FLOAT_ARRAY_HANDLE = MethodHandles.arrayElementVarHandle(float[].class);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to get VarHandle for float[]", e);
+ }
+ }
+
+ @Override
+ public final int size() {
+ return size;
+ }
+
+ @Override
+ public final void setFloat(final int index, final float value) {
+ throw new UnsupportedOperationException("setFloat");
+ }
+
+ @Override
+ public final GGMLType type() {
+ return GGMLType.Q4_0;
+ }
+
+ private final static int LOG2_QUANT_BLOCK_SIZE = Integer.numberOfTrailingZeros(GGMLType.Q4_0.blockSize); // only if QUANT_BLOCK_SIZE == 2^LOG2_QUANT_BLOCK_SIZE
+ private final static int QUANT_HALF_BLOCK = GGMLType.Q4_0.blockSize / 2; // 16
+ private final static int QUANT_FLOAT16_BYTES = GGMLType.FLOAT16_BYTES; // 2
+
+ @Override
+ public final float getFloat(final int index) {
+ assert 0 <= index && index < size;
+
+ int blockIndex = index >>> LOG2_QUANT_BLOCK_SIZE; // index / QUANT_BLOCK_SIZE;
+ int blockOffset = blockIndex * GGMLType.Q4_0.typeSize;
+ final long offset = blockOffset;
+ float scale = FloatTensor.float16ToFloat(buffer.getShort((int) offset));
+ final int modIndex = index & (GGMLType.Q4_0.blockSize - 1); //index % QUANT_BLOCK_SIZE;
+ final boolean isLow = modIndex < QUANT_HALF_BLOCK;
+ final int adjustedIndex = modIndex - (isLow ? 0 : QUANT_HALF_BLOCK);
+ final int dataIndex = blockOffset + QUANT_FLOAT16_BYTES + adjustedIndex;
+ final int packed = (buffer.get((int) (long) dataIndex)) & 0xFF;
+ final int nibble = isLow ? (packed & 0x0F) : ((packed >>> 4) & 0x0F);
+ //final float quant = nibble - 8;
+ return (nibble - 8) * scale;
+ }
+
+ public final void getFloatArray(final int index, final float[] out, final int outOffset, final int length) {
+ int inPos = index;
+ int outPos = outOffset;
+ final int end = index + length;
+
+ while (inPos < end) {
+ final int blockIndex = inPos >>> LOG2_QUANT_BLOCK_SIZE;
+ final int blockOffset = blockIndex * GGMLType.Q4_0.typeSize;
+ float scale = FloatTensor.float16ToFloat(buffer.getShort((int) (long) blockOffset));
+
+ final int blockStart = blockIndex * GGMLType.Q4_0.blockSize;
+ final int blockEnd = Math.min(blockStart + GGMLType.Q4_0.blockSize, end);
+
+ // Process two values at a time
+ int i = inPos;
+ while (i + 1 < blockEnd) {
+ int blockMod0 = i & (GGMLType.Q4_0.blockSize - 1);
+ int blockMod1 = blockMod0 + 1;
+
+ int byteOffset0 = blockOffset + QUANT_FLOAT16_BYTES + (blockMod0 < QUANT_HALF_BLOCK ? blockMod0 : blockMod0 - QUANT_HALF_BLOCK);
+ int byteOffset1 = blockOffset + QUANT_FLOAT16_BYTES + (blockMod1 < QUANT_HALF_BLOCK ? blockMod1 : blockMod1 - QUANT_HALF_BLOCK);
+ final long offset = byteOffset0;
+
+ int packed0 = buffer.get((int) offset) & 0xFF;
+ final long offset1 = byteOffset1;
+ int packed1 = (byteOffset1 == byteOffset0) ? packed0 : (buffer.get((int) offset1) & 0xFF);
+
+ // Decode first value from packed0
+ int nibble0 = (blockMod0 < QUANT_HALF_BLOCK) ? (packed0 & 0x0F) : ((packed0 >>> 4) & 0x0F);
+ out[outPos++] = (nibble0 - 8) * scale;
+
+ // Decode second value from packed1
+ int nibble1 = (blockMod1 < QUANT_HALF_BLOCK) ? (packed1 & 0x0F) : ((packed1 >>> 4) & 0x0F);
+ out[outPos++] = (nibble1 - 8) * scale;
+
+ i += 2;
+ }
+
+ // Handle last element if length is odd
+ if (i < blockEnd) {
+ int blockMod = i & (GGMLType.Q4_0.blockSize - 1);
+ int byteOffset = blockOffset + QUANT_FLOAT16_BYTES + (blockMod < QUANT_HALF_BLOCK ? blockMod : blockMod - QUANT_HALF_BLOCK);
+ final long offset = byteOffset;
+ int packed = buffer.get((int) offset) & 0xFF;
+ int nibble = (blockMod < QUANT_HALF_BLOCK) ? (packed & 0x0F) : ((packed >>> 4) & 0x0F);
+ out[outPos++] = (nibble - 8) * scale;
+ i++;
+ }
+
+ int copied = blockEnd - inPos;
+ inPos += copied;
+ }
+ }
+
+ public static final ThreadLocal<float[]> scratchBuffer = ThreadLocal.withInitial(() -> new float[GGMLType.Q4_0.blockSize]);
+
+ @Override
+ public void copyTo(final int thisOffset, final FloatTensor that, final int thatOffset, final int size) {
+ final float[] decoded = scratchBuffer.get();
+ int remaining = size;
+ int srcIndex = thisOffset;
+ int dstIndex = thatOffset;
+
+ // Decode and copy in QUANT_BLOCK_SIZE chunks
+ while (remaining >= GGMLType.Q4_0.blockSize) {
+ getFloatArray(srcIndex, decoded, 0, GGMLType.Q4_0.blockSize);
+ for (int i = 0; i < GGMLType.Q4_0.blockSize; i++) {
+ that.setFloat(dstIndex + i, (float) FLOAT_ARRAY_HANDLE.get(decoded, i));
+ }
+ srcIndex += GGMLType.Q4_0.blockSize;
+ dstIndex += GGMLType.Q4_0.blockSize;
+ remaining -= GGMLType.Q4_0.blockSize;
+ }
+
+ // Decode and copy any leftover elements one by one
+ if (remaining > 0) {
+ getFloatArray(srcIndex, decoded, 0, remaining);
+ for (int i = 0; i < remaining; i++) {
+ that.setFloat(dstIndex + i, (float) FLOAT_ARRAY_HANDLE.get(decoded, i));
+ }
+ }
+ }
+
+ /**
+ * dot product which has the getFloat method inlined in such a way that it processes full blocks at once.
+ * This gains a > 2.5 times token/s performance increase compared to the generic dot-getFloat implementation.
+ */
+ public final float dot(final int thisOffset, final FloatTensor that, final int thatOffset, final int size) {
+ float result = 0.0f;
+ int index = 0;
+ final int blockLimit = size - (size % GGMLType.Q4_0.blockSize);
+
+ // Process full blocks, one block are 32 elements, 16 quantized values and 1 scale
+ while (index < blockLimit) {
+
+ // Get this block
+ final int thisBlockIndex = (thisOffset + index) >>> LOG2_QUANT_BLOCK_SIZE; // (thisOffset + index) / QUANT_BLOCK_SIZE;
+ final int thisBlockOffset = thisBlockIndex * GGMLType.Q4_0.typeSize;
+ final float thisScale = FloatTensor.float16ToFloat(buffer.getShort((int) (long) thisBlockOffset));
+
+ // Process block: read all quantized values from this block at once
+ final int quantOffset = thisBlockOffset + QUANT_FLOAT16_BYTES;
+ float blockResult = 0.0f;
+ final int thatIndex = thatOffset + index;
+ if (that instanceof ArrayFloatTensor) {
+ final ArrayFloatTensor thatArray = (ArrayFloatTensor) that;
+ final float[] b = thatArray.values;
+ for (int i = 0; i < QUANT_HALF_BLOCK; ++i) {
+ final byte packed = buffer.get(quantOffset + i);
+ final float valB0 = (float) FLOAT_ARRAY_HANDLE.get(b, thatIndex + i);
+ final float valB1 = (float) FLOAT_ARRAY_HANDLE.get(b, thatIndex + i + QUANT_HALF_BLOCK);
+ blockResult += ((packed & 0x0F) - 8) * valB0 + (((packed >>> 4) & 0x0F) - 8) * valB1;
+ }
+ } else {
+ for (int i = 0; i < QUANT_HALF_BLOCK; ++i) {
+ final byte packed = buffer.get(quantOffset + i);
+ blockResult += ((packed & 0x0F) - 8) * that.getFloat(thatIndex + i) + (((packed >>> 4) & 0x0F) - 8) * that.getFloat(thatIndex + i + QUANT_HALF_BLOCK);
+ }
+ }
+ result += blockResult * thisScale;
+ index += GGMLType.Q4_0.blockSize;
+ }
+
+ // Process remaining elements
+ for (; index < size; index++) {
+ result += this.getFloat(thisOffset + index) * that.getFloat(thatOffset + index);
+ }
+
+ return (float) result;
+ }
+
+} \ No newline at end of file
diff --git a/source/net/yacy/ai/llama3/Tensor/Q8_0FloatTensor.java b/source/net/yacy/ai/llama3/Tensor/Q8_0FloatTensor.java
new file mode 100644
index 000000000..5e84b86fb
--- /dev/null
+++ b/source/net/yacy/ai/llama3/Tensor/Q8_0FloatTensor.java
@@ -0,0 +1,134 @@
+/**
+ * Q8_0FloatTensor.java
+
+ * This file was extracted from the llama3/qwen2 projects
+ * https://github.com/mukel/llama3.java
+ * https://github.com/mukel/qwen2.svm.java
+ *
+ * License: MIT License
+ *
+ * Copyright (c) 2024 Andrej Karpathy (for llama2.c)
+ * Copyright (c) 2024 Alfonso² Peterssen (for llama3/qwen2)
+ * Copyright (c) 2023 Georgi Gerganov et al. (for llama.cpp)
+ * Copyright (c) 2025 Michael Peter Christen for modifications:
+ * The code was modified to fit the YaCy AI project:
+ * - back-port to Java 11 (removal of Vector API operations and record types)
+ * - removal of interactive mode and system.out printing
+ * - separation of the classes in the single java and refactoring
+ * - run-time performance optimizations for dot product computation of quantized values
+ * - joining of llama3/qwen2 into one code base; multi-arch options
+ * - alignment with code from https://github.com/ggml-org/llama.cpp/
+ */
+
+package net.yacy.ai.llama3.Tensor;
+
+import java.nio.ByteBuffer;
+
+import net.yacy.ai.llama3.Model.GGMLType;
+
+public final class Q8_0FloatTensor extends FloatTensor {
+
+ final int size;
+ final ByteBuffer buffer;
+
+
+ public Q8_0FloatTensor(final int size, final ByteBuffer buffer) {
+ this.size = size;
+ this.buffer = buffer;
+ }
+
+ @Override
+ public int size() {
+ return size;
+ }
+
+ @Override
+ public void setFloat(final int index, final float value) {
+ throw new UnsupportedOperationException("setFloat");
+ }
+
+ @Override
+ public GGMLType type() {
+ return GGMLType.Q8_0;
+ }
+
+ @Override
+ public final float getFloat(final int index) {
+ assert 0 <= index && index < size;
+ int blockIndex = index / GGMLType.Q8_0.blockSize;
+ int withinBlockIndex = index % GGMLType.Q8_0.blockSize;
+ int blockOffset = blockIndex * GGMLType.Q8_0.typeSize;
+ byte quant = buffer.get((int) (long) (blockOffset + GGMLType.FLOAT16_BYTES + withinBlockIndex));
+ final long offset = blockOffset;
+ float scale = FloatTensor.float16ToFloat(buffer.getShort((int) offset));
+ return quant * scale;
+ }
+
+ @Override
+ public float dot(final int thisOffset, final FloatTensor that, final int thatOffset, final int size) {
+ assert 0 <= thisOffset && thisOffset + size <= this.size;
+ assert 0 <= thatOffset && thatOffset + size <= that.size();
+
+ float result = 0f;
+
+ // Calculate first and last block indices
+ int firstBlock = thisOffset / GGMLType.Q8_0.blockSize;
+ int lastBlock = (thisOffset + size - 1) / GGMLType.Q8_0.blockSize;
+
+ for (int block = firstBlock; block <= lastBlock; block++) {
+ // Calculate block boundaries and overlaps
+ int blockStart = block * GGMLType.Q8_0.blockSize;
+ int blockEnd = blockStart + GGMLType.Q8_0.blockSize;
+ int start = Math.max(thisOffset, blockStart);
+ int end = Math.min(thisOffset + size, blockEnd);
+ int length = end - start;
+
+ // Get common scale factor for this block
+ int blockOffset = block * GGMLType.Q8_0.typeSize;
+ final long offset = blockOffset;
+ float thisScale = FloatTensor.float16ToFloat(buffer.getShort((int) offset));
+
+ // Compute sum of products for this block
+ float blockSum = 0f;
+ int withinBlockStart = start % GGMLType.Q8_0.blockSize;
+
+ int memOffset = blockOffset + GGMLType.FLOAT16_BYTES + withinBlockStart;
+ int thatoffset = thatOffset + (start - thisOffset);
+ if (that instanceof ArrayFloatTensor) {
+ ArrayFloatTensor thatArray = (ArrayFloatTensor) that;
+ for (int i = 0; i < length; i++) {
+ blockSum += buffer.get((int) (long) memOffset++) * thatArray.getFloat(thatoffset++);
+ }
+ } else {
+ for (int i = 0; i < length; i++) {
+ blockSum += buffer.get((int) (long) memOffset++) * that.getFloat(thatoffset++);
+ }
+ }
+
+ // Apply scale to the entire block sum
+ result += thisScale * blockSum;
+ }
+
+ return result;
+ }
+
+ @Override
+ public void copyTo(final int thisOffset, final FloatTensor that, final int thatOffset, final int size) {
+ assert 0 <= thisOffset && thisOffset + size <= this.size;
+ assert 0 <= thatOffset && thatOffset + size <= that.size();
+
+ final int endOffset = thatOffset + size;
+
+ for (int i = thatOffset; i < endOffset; ++i) {
+ int index = i - thatOffset + thisOffset;
+ int blockIndex = index / GGMLType.Q8_0.blockSize;
+ int withinBlockIndex = index % GGMLType.Q8_0.blockSize;
+ int blockOffset = blockIndex * GGMLType.Q8_0.typeSize;
+
+ byte quant = buffer.get((int) (long) (blockOffset + GGMLType.FLOAT16_BYTES + withinBlockIndex));
+ final long offset = blockOffset;
+ float scale = FloatTensor.float16ToFloat(buffer.getShort((int) offset));
+ that.setFloat(i, quant * scale);
+ }
+ }
+}
diff --git a/source/net/yacy/cora/document/id/MultiProtocolURL.java b/source/net/yacy/cora/document/id/MultiProtocolURL.java
index 653d65eba..0ff750b09 100644
--- a/source/net/yacy/cora/document/id/MultiProtocolURL.java
+++ b/source/net/yacy/cora/document/id/MultiProtocolURL.java
@@ -2660,7 +2660,6 @@ public class MultiProtocolURL implements Serializable, Comparable<MultiProtocolU
return splitpattern.split(normalizedURL.toLowerCase()); // word components of the url
}
- @SuppressWarnings("deprecation")
public static void main(final String[] args) {
final String[][] test = new String[][]{
new String[]{null, "file://y:/"},
diff --git a/source/net/yacy/cora/util/Html2Image.java b/source/net/yacy/cora/util/Html2Image.java
index 10b9afb52..83efe559b 100644
--- a/source/net/yacy/cora/util/Html2Image.java
+++ b/source/net/yacy/cora/util/Html2Image.java
@@ -141,7 +141,6 @@ public class Html2Image {
private static boolean wkhtmltopdfAvailableInPath() {
boolean available = false;
try {
- @SuppressWarnings("deprecation")
final Process p = Runtime.getRuntime().exec(WKHTMLTOPDF_COMMAND + " -V");
available = p.waitFor(2, TimeUnit.SECONDS) && p.exitValue() == 0;
} catch (final IOException e) {
@@ -184,7 +183,6 @@ public class Html2Image {
boolean available = false;
if(!OS.isWindows) { // on MS Windows convert is a system tool to convert volumes from FAT to NTFS
try {
- @SuppressWarnings("deprecation")
final Process p = Runtime.getRuntime().exec(CONVERT_COMMAND + " -version");
available = p.waitFor(2, TimeUnit.SECONDS) && p.exitValue() == 0;
} catch (final IOException e) {
@@ -296,7 +294,6 @@ public class Html2Image {
* @throws IOException when an unexpected error occurred
*/
private static boolean execWkhtmlToPdf(final String proxy, final File destination, final String commandline, final long maxSeconds) throws IOException {
- @SuppressWarnings("deprecation")
final Process p = Runtime.getRuntime().exec(commandline);
try {
diff --git a/source/net/yacy/gui/framework/Browser.java b/source/net/yacy/gui/framework/Browser.java
index 80d566127..fc396c741 100644
--- a/source/net/yacy/gui/framework/Browser.java
+++ b/source/net/yacy/gui/framework/Browser.java
@@ -136,7 +136,6 @@ public class Browser {
* It is part of the LSB (Linux Standard Base) and therefore included in all recent Linux Distributions supporting it
* (see https://www.linuxbase.org/navigator/browse/cmd_single.php?cmd=list-by-name&Section=ABI&Cname=xdg-open) */
final String cmd = "xdg-open " + url;
- @SuppressWarnings("deprecation")
final Process p = Runtime.getRuntime().exec(cmd);
p.waitFor();
if (p.exitValue() != 0) {
@@ -153,7 +152,6 @@ public class Browser {
cmd = "rundll32 url.dll,FileProtocolHandler \"" + url + "\"";
}
//cmd = "cmd.exe /c start javascript:document.location='" + url + "'";
- @SuppressWarnings("deprecation")
final Process p = Runtime.getRuntime().exec(cmd);
p.waitFor();
if (p.exitValue() != 0) {
diff --git a/source/net/yacy/htroot/IndexImportZim_p.java b/source/net/yacy/htroot/IndexImportZim_p.java
new file mode 100644
index 000000000..8517121d5
--- /dev/null
+++ b/source/net/yacy/htroot/IndexImportZim_p.java
@@ -0,0 +1,85 @@
+// IndexImportZim_p.java
+// -------------------------
+// (c) 2025 by Michael Peter Christen, mc@yacy.net
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package net.yacy.htroot;
+
+import java.io.File;
+import java.io.IOException;
+
+import net.yacy.cora.protocol.RequestHeader;
+import net.yacy.document.importer.ZimImporter;
+import net.yacy.server.serverObjects;
+import net.yacy.server.serverSwitch;
+
+public class IndexImportZim_p {
+
+ public static serverObjects respond(@SuppressWarnings("unused") final RequestHeader request, final serverObjects post, @SuppressWarnings("unused") final serverSwitch env) {
+
+ // read multipart data from post request
+
+ final serverObjects prop = new serverObjects();
+
+ if (ZimImporter.job != null && ZimImporter.job.isAlive()) {
+ // one import is running, no option to insert anything
+ prop.put("import", 1);
+ prop.put("import_thread", "running");
+ prop.put("import_warcfile", ZimImporter.job.source());
+ prop.put("import_count", ZimImporter.job.count());
+ prop.put("import_speed", ZimImporter.job.speed());
+ prop.put("import_runningHours", (ZimImporter.job.runningTime() / 60) / 60);
+ prop.put("import_runningMinutes", (ZimImporter.job.runningTime() / 60) % 60);
+ prop.put("import_remainingHours", (ZimImporter.job.remainingTime() / 60) / 60);
+ prop.put("import_remainingMinutes", (ZimImporter.job.remainingTime() / 60) % 60);
+ if (post != null && post.containsKey("abort")) {
+ ZimImporter.job.quit();
+ }
+ } else {
+ prop.put("import", 0);
+ if (post != null) {
+ if (post.containsKey("file")) {
+ final String filename = post.get("file");
+ if (filename != null && filename.length() > 0) {
+ final File sourcefile = new File(filename);
+ if (sourcefile.exists()) {
+ try {
+ final ZimImporter zi = new ZimImporter(sourcefile.getAbsolutePath());
+ zi.start();
+ prop.put("import_thread", "started");
+ } catch (final IOException ex) {
+ prop.put("import_thread", "Error: file not found [" + filename + "]");
+ }
+ prop.put("import", 1);
+ prop.put("import_zimfile", filename);
+ } else {
+ prop.put("import_zimfile", "");
+ prop.put("import_thread", "Error: file not found [" + filename + "]");
+ }
+ }
+
+ prop.put("import_count", 0);
+ prop.put("import_speed", 0);
+ prop.put("import_runningHours", 0);
+ prop.put("import_runningMinutes", 0);
+ prop.put("import_remainingHours", 0);
+ prop.put("import_remainingMinutes", 0);
+ }
+ }
+ }
+ return prop;
+ }
+}
diff --git a/source/net/yacy/http/servlets/RAGProxyServlet.java b/source/net/yacy/http/servlets/RAGProxyServlet.java
index e7de7d61d..166a69aea 100644
--- a/source/net/yacy/http/servlets/RAGProxyServlet.java
+++ b/source/net/yacy/http/servlets/RAGProxyServlet.java
@@ -211,10 +211,12 @@ public class RAGProxyServlet extends HttpServlet {
String question = "Make a list of a maximum of four search words for the following question; use a JSON Array: " + prompt;
try {
OpenAIClient oaic = new OpenAIClient(LLM_API_HOST);
- String[] a = OpenAIClient.stringsFromChat(oaic.chat(model, question, 80));
+ OpenAIClient.Context context = new OpenAIClient.Context(LLM_SYSTEM_PREFIX);
+ context.addPrompt(question);
+ String[] a = OpenAIClient.stringsFromChat(oaic.chat(model, context, OpenAIClient.listSchema, 80));
for (String s: a) query.append(s).append(' ');
return query.toString().trim();
- } catch (IOException e) {
+ } catch (IOException | JSONException e) {
e.printStackTrace();
return "";
}
diff --git a/source/net/yacy/kelondro/logging/ThreadDump.java b/source/net/yacy/kelondro/logging/ThreadDump.java
index 00a4eb8dc..a4d842d02 100644
--- a/source/net/yacy/kelondro/logging/ThreadDump.java
+++ b/source/net/yacy/kelondro/logging/ThreadDump.java
@@ -236,7 +236,6 @@ public class ThreadDump extends HashMap<ThreadDump.StackTrace, List<String>> imp
while (tracename.length() < 20) tracename = tracename + "_";
tracename = "[" + tracename + "] ";
}
- @SuppressWarnings("deprecation")
final String threadtitle = tracename + "Thread= " + thread.getName() + " " + (thread.isDaemon()?"daemon":"") + " id=" + thread.getId() + " " + thread.getState().toString();
String className;
boolean cutcore = true;
diff --git a/source/net/yacy/kelondro/util/FileUtils.java b/source/net/yacy/kelondro/util/FileUtils.java
index 408ee459d..372793e27 100644
--- a/source/net/yacy/kelondro/util/FileUtils.java
+++ b/source/net/yacy/kelondro/util/FileUtils.java
@@ -982,7 +982,6 @@ public final class FileUtils {
// deleting files on windows sometimes does not work with java
try {
final String command = "cmd /C del /F /Q \"" + p + "\"";
- @SuppressWarnings("deprecation")
final Process r = Runtime.getRuntime().exec(command);
if ( r == null ) {
ConcurrentLog.severe("FileUtils", "cannot execute command: " + command);
diff --git a/source/net/yacy/server/http/HTTPDProxyHandler.java b/source/net/yacy/server/http/HTTPDProxyHandler.java
index e300ec9e2..3f01a7ead 100644
--- a/source/net/yacy/server/http/HTTPDProxyHandler.java
+++ b/source/net/yacy/server/http/HTTPDProxyHandler.java
@@ -90,7 +90,6 @@ import net.yacy.repository.Blacklist.BlacklistType;
import net.yacy.search.Switchboard;
import net.yacy.server.serverObjects;
-@SuppressWarnings("deprecation")
public final class HTTPDProxyHandler {