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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ from version 5.0.0 onward. Pre-fork releases (`1.x`–`4.2.0`) were authored by
## [Unreleased]

### Added
- **The loaded backends and devices are logged at start-up.** `JNI_OnLoad` logs, after loading the CPU
modules next to the library, how many backends ggml registered and which devices they report (reaches
`LlamaModel.setLogger`), and `LlamaLoader` prints how many module files it extracted for a backend:
a module ggml cannot load fails silently (`GGML_LOG_DEBUG` only, no dialog on Windows), and without
these lines a GPU module with a missing runtime was indistinguishable from one that was never there.
- **README: Windows clients under WDAC / AppLocker.** The DLLs are not Authenticode-signed and are
extracted to `%TEMP%`; the troubleshooting section names the way out (`net.ladenthin.llama.lib.path`
pointing at an allow-listed directory) and why the OpenPGP release key cannot sign them.
- **`ModelParameters.enableMetricsEndpoint()` / `enablePropsEndpoint()` / `enableSlotsEndpoint()` /
`disableSlotsEndpoint()`** (`--metrics`, `--props`, `--slots`, `--no-slots`): the HTTP frontend's
endpoint toggles, needed since `NativeServer` attach mode serves the model's own routes (see Fixed).
Expand Down Expand Up @@ -83,6 +91,14 @@ from version 5.0.0 onward. Pre-fork releases (`1.x`–`4.2.0`) were authored by
core's; `check-natives.py` fails when they differ.

### Changed
- **The extracted natives are reused across JVM starts.** `LlamaLoader` used to delete every extracted
file at exit and every `jllama*` entry of the temp directory at the next start, so each JVM copied
the whole backend directory again (18 files for a CPU-variants build, ~1 s with an on-access scanner
on Windows, for every surefire fork too). The per-build extraction directory
(`jllama-backend-<backend>-<key>`) now stays: a later start of the same build compares each file
with the jar byte for byte and writes only what differs; a directory of a build that is not on the
classpath is removed once it is 10 minutes old (a JVM that is still starting touches its directory
first). `net.ladenthin.llama.tmpdir` still chooses the location.
- **The log sink attaches llama.cpp's log worker to the JVM once per thread, not once per line.**
`LlamaModel.setLogger`'s trampoline used to `AttachCurrentThread` + `DetachCurrentThread` for every
log line, i.e. one `java.lang.Thread` object and a pair of JVMTI thread events per line (13 lines of a
Expand Down
21 changes: 20 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,12 @@ of `jllama-files.txt` and named in `jllama-extras.txt` instead, which `LlamaLoad
path **before** `jllama.dll` (otherwise Windows resolves the import from `java.exe`'s directory or
`PATH`, where a foreign llama.cpp install can answer); CMake writes that file itself since 5.2.x,
byte-identical to what `merge-native-artifacts.sh` derives, so a local build is loadable without
the merge step (before, 57 Java tests failed on the load). And sccache is off on that job: upstream
the merge step (before, 57 Java tests failed on the load). `JNI_OnLoad` finds its own directory with
`GetModuleHandleExW` + `GetModuleFileNameW` and hands it to `ggml_backend_load_all_from_path` as
**UTF-8** (a path with an umlaut loaded nothing as ANSI, and no error was logged); a GPU module whose
runtime is missing fails silently (`SEM_FAILCRITICALERRORS`, ggml logs it at `GGML_LOG_DEBUG` only),
which is why `JNI_OnLoad` logs the backend registry and the devices ggml ends up with at INFO, and
`LlamaLoader` says how many module files it extracted. And sccache is off on that job: upstream
adds `-Xclang -fno-pch-timestamp` to every clang TU, which sccache 0.18.0 does not cache (measured:
every TU a miss, build green) -- the job builds cold. 32-bit Windows and Windows arm64 ship no
variants (upstream builds none either).
Expand Down Expand Up @@ -130,6 +135,20 @@ variants (upstream builds none either).
alone, a JVM running another jllama build wrote into the directory of a running one; on Windows
that replaces exactly the modules the first JVM scored and unloaded again (which ones depends on
the CPU) and leaves a mixture of two builds for the next start -- measured with upstream's DLLs.
**And it is reused across starts** (since 5.2.x): `cleanup()` spares the directories of the builds
on the classpath (`extractionDirectoriesOfThisClasspath`, computed before anything is extracted),
nothing is registered for `deleteOnExit` any more, and `extractFile`'s reuse branch -- compare the
file with the jar byte for byte, write only what differs, atomically -- does the rest, so a second
start copies nothing (18 files, ~1 s with an on-access scanner on Windows, on every JVM start
before; the same for every surefire fork). What `cleanup()` removes: the flat files of the pre-5.2.0
layout, and the directory of a build that is **not** on this classpath once it is older than
`STALE_EXTRACTION_AGE` (10 min) -- a start touches its directory's mtime before extracting, so a JVM
that is still starting from a directory is never swept; every delete is best-effort, and a DLL a
running JVM holds is locked on Windows and stays (the modules ggml scored and unloaded are not, and
may go -- that JVM no longer needs them, and the next start of its build re-extracts exactly the
missing ones). Isolating the extraction per process was considered and rejected for the same
reason: a per-build directory shared by every JVM of that build, with content-checked reuse, is
the cache; a per-process one would copy on every start again.
- **RPC is a module too**, so `rpc_bridge.cpp`/`rpc_support.hpp` resolve its four entry points through
`ggml_backend_reg_get_proc_address` (`rpc_proc<>()`, as upstream's `common/arg.cpp` does) in every
build.
Expand Down
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ Every `net.ladenthin.llama.*` system property recognised by the library, deep-sc
| Property | Default | Scope | Consumer | Description |
|---|---|---|---|---|
| `net.ladenthin.llama.lib.path` | unset (falls back to `java.library.path`) | runtime | `LlamaLoader` | Directory containing the native `jllama` shared library. Checked first, before `java.library.path`. Set with `-Dnet.ladenthin.llama.lib.path=/path/to/dir`. |
| `net.ladenthin.llama.tmpdir` | unset (falls back to `java.io.tmpdir`) | runtime | `LlamaLoader` | Custom temporary directory used when extracting the native library from the JAR. |
| `net.ladenthin.llama.tmpdir` | unset (falls back to `java.io.tmpdir`) | runtime | `LlamaLoader` | Directory the natives are extracted into, one subdirectory per backend and build (`jllama-backend-<backend>-<key>`). It is kept across runs: the next start of the same build compares each file with the jar and copies nothing; a directory of a build that is no longer on the classpath is removed by a later start once it is 10 minutes old. Point it elsewhere when the default temp directory is on a slow or policy-restricted volume. |
| `net.ladenthin.llama.osinfo.architecture` | unset (uses `os.arch`) | runtime | `OSInfo` | Override for the architecture string used to locate the bundled library inside the JAR. Useful when `os.arch` reports an unexpected value (e.g. inside dockcross / chrooted environments). |
| `net.ladenthin.llama.backend` | unset (auto: the first backend on the classpath whose library loads) | runtime | `LlamaLoader` | Names one backend directory (e.g. `cuda13`, `vulkan`, `cpu`) to load exclusively — failure is then fatal instead of trying the next backend. See [Choosing the natives jars](#choosing-the-natives-jars). |
| `net.ladenthin.llama.test.ngl` | `43` for the general suite; `0` for `ToolCallingIntegrationTest` | test | Model-backed integration tests | Number of GPU layers used during testing. Pin to `0` on CPU-only hosts: `mvn test -Dnet.ladenthin.llama.test.ngl=0`. The tool test also selects device `none` at zero layers so Metal/CUDA is not initialized. |
Expand Down Expand Up @@ -1573,6 +1573,18 @@ If you still see a crash inside `msvcp140.dll`, it originates in **another** nat
into the same JVM, not in `jllama.dll` -- check the rest of the `hs_err` frame list. Please open an
issue with that file rather than editing your JDK installation.

### Windows: unsigned DLLs on a WDAC / AppLocker-managed client

The native libraries are **not Authenticode-signed** (upstream llama.cpp ships its DLLs unsigned
too), and `LlamaLoader` extracts them into the temp directory on first use. A client whose
application-control policy (WDAC, AppLocker) blocks unsigned DLLs, or any DLL under `%TEMP%`,
refuses the load with `UnsatisfiedLinkError`. The way out is a directory the policy allows: put the
contents of the natives jar's `net/ladenthin/llama/Windows/x86_64/cpu/` directory there and start
the JVM with `-Dnet.ladenthin.llama.lib.path=<that directory>`, which loads from it without
extracting anything. (The project's release-signing key is an OpenPGP key, which signs the jars and
fat jars with detached `.asc` files; Windows does not look at those, so the DLLs would need a
separate code-signing certificate.)

### Contributors: build with Temurin 21, not Oracle JDK 21

`llama/pom.xml` passes `-XDaddTypeAnnotationsToSymbol=true` to `javac` (NullAway's JSpecify mode
Expand Down
Loading
Loading