From d0d15877ab56d21f56009eea79b4649ffb67a774 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 07:22:18 +0000 Subject: [PATCH 1/3] fix(ci): restore clang-format and REUSE compliance on test_model_split.cpp The file added in #421 went in without either, so main is currently red on two gates that the PR run reported but the merge did not wait for: * clang-format (pinned 22.1.8) rejected the `(size_t) 1` cast spacing in the n_devices loops -- the repo style is `(size_t)1`. * REUSE found no copyright/licensing information. Every other file under src/test/cpp carries the SPDX header pair; this one did not. The patch file from the same change is fine -- REUSE.toml's `llama/patches/**` glob covers it, because a unified diff cannot carry an inline header without corrupting the diff. Both are mine and neither was caught locally, because clang-format and REUSE run only in CI. Verified after the fix with the same tools CI uses: `clang-format --dry-run --Werror` over src/main/cpp and src/test/cpp is silent, and `reuse lint` reports 455/455 files with both copyright and license information. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH --- llama/src/test/cpp/test_model_split.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/llama/src/test/cpp/test_model_split.cpp b/llama/src/test/cpp/test_model_split.cpp index 5395af0d..18042b3f 100644 --- a/llama/src/test/cpp/test_model_split.cpp +++ b/llama/src/test/cpp/test_model_split.cpp @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2026 Bernard Ladenthin +// +// SPDX-License-Identifier: MIT +// // Runnable guard for patches/0012 (the layer-split fix in llama.cpp's src/llama-model.cpp). // // The patch also ships an upstream test (tests/test-model-split.cpp), but a FetchContent @@ -26,7 +30,7 @@ void expect_every_layer_maps_into_range(const std::vector &splits, int n_ int idx = -1; ASSERT_NO_THROW(idx = llama_model_splits_select_device(splits, il, n_layers)) << "layer " << il; EXPECT_GE(idx, 0) << "layer " << il; - EXPECT_LT((size_t) idx, splits.size()) << "layer " << il; + EXPECT_LT((size_t)idx, splits.size()) << "layer " << il; } } @@ -52,7 +56,7 @@ TEST(LlamaModelSplits, NormalizeSingleDeviceTakesEverything) { // currentAllocatedSize has grown past its recommendedMaxWorkingSetSize -- makes the sum of the // weights zero. Dividing by it put a NaN in every split point. TEST(LlamaModelSplits, ZeroSumDoesNotProduceNaNSplitPoints) { - for (size_t n_devices : {(size_t) 1, (size_t) 2, (size_t) 4}) { + for (size_t n_devices : {(size_t)1, (size_t)2, (size_t)4}) { std::vector splits(n_devices, 0.0f); llama_model_splits_normalize(splits); @@ -68,7 +72,7 @@ TEST(LlamaModelSplits, ZeroSumDoesNotProduceNaNSplitPoints) { // load_tensors() indexed one past the last device -- an std::out_of_range whose libc++ what() is // the bare string "vector". This is the assertion that would have failed before the fix. TEST(LlamaModelSplits, ZeroSumStillMapsEveryLayerToARealDevice) { - for (size_t n_devices : {(size_t) 1, (size_t) 2, (size_t) 4}) { + for (size_t n_devices : {(size_t)1, (size_t)2, (size_t)4}) { std::vector splits(n_devices, 0.0f); llama_model_splits_normalize(splits); expect_every_layer_maps_into_range(splits, 32); From b49dbb872426fb0a451b5a401a32236338b3fd63 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 07:22:35 +0000 Subject: [PATCH 2/3] feat!: upgrade llama.cpp from b10850 to b10870 No project-source change. The four pin sites move together (llama/CMakeLists.txt GIT_TAG, README badge + link, CLAUDE.md, LlamaCppVersion.LLAMA_CPP_VERSION), so NativeLibraryLoadSmokeTest's pin-vs-linked-binary assertion stays satisfied. The headline diff number is misleading: common/chat.cpp loses 2524 lines, but that is a pure internal reorganisation splitting the per-model chat parsers into a new common/parsers/ directory. common/chat.h -- which jllama.cpp includes directly and which is #2 on the priority review list -- is byte-identical in the range, so nothing the project compiles against moved. The patch intersection was NOT empty, and 0012 was the patch at risk: the range touches src/llama-model.cpp and tests/CMakeLists.txt, both of which 0012 modifies, one week after it landed. The check CLAUDE.md mandates was run by hand first, because the fail-loud applier detects "does not apply" but never "upstream already fixed this": b10870:src/llama-model.cpp still carries the bare `splits[i] /= split_sum` with no zero-sum guard, so the patch stays rather than being dropped. Upstream's own change to that file (#28160, resolving lazy-mode AUTO to OFF where mmap is unsupported) sits ~60 lines above the patched region and is unrelated. Two behaviour changes that a header diff cannot see, both reaching every entry point that parses argv: * --mmproj-device now defaults to --device instead of auto-selecting, so a caller setting --device but not -mmdev now pins the multimodal projector to the same device. * the draft model inherits the global device list the same way, and common_speculative_init only overwrites result.devices when the spec list is non-empty (it previously assigned unconditionally), plus forces LLAMA_SPLIT_MODE_LAYER for a single-device draft. That is the path LlamaModelTest#testSpeculativeDecoding drives -- the same test that was red on macOS before patches/0012, so a failure there now needs attributing carefully between the two. ggml.h gains precision enum values and two functions, and deprecates ggml_mul_mat_set_prec / ggml_flash_attn_ext_set_prec; the project source has zero references to any of it. Verified: applier clean on a fresh build dir with all nine patch hashes stamped at head 1945e092 (= b10870); server contract byte-identical in all three mechanical dimensions (request fields, set_hard_limits bounds, response keys in both emit forms); ctest 527/527; mvn clean verify green (1742 tests, 0 failures, 0 errors); bytecode gate clean (616 classes / 5 jars, 0 over major 52). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH --- CLAUDE.md | 8 ++++---- README.md | 2 +- docs/history/llama-cpp-breaking-changes.md | 2 ++ llama/CMakeLists.txt | 2 +- .../java/net/ladenthin/llama/value/LlamaCppVersion.java | 8 ++++---- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index fab124e7..31990405 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Java bindings for [llama.cpp](https://github.com/ggerganov/llama.cpp) via JNI, providing a high-level API for LLM inference in Java. The Java layer communicates with a native C++ library through JNI. -Current llama.cpp pinned version: **b10850** +Current llama.cpp pinned version: **b10870** ## Upgrading CUDA Version @@ -490,7 +490,7 @@ needs no extra step here, `build-webui` re-reads the tag and rebuilds the matchi ships no UI): ```bash # needs node/npm + network; embed.cpp is plain C++17 (no npm) -git clone --depth 1 --branch b10850 https://github.com/ggml-org/llama.cpp /tmp/lc +git clone --depth 1 --branch b10870 https://github.com/ggml-org/llama.cpp /tmp/lc ( cd /tmp/lc/tools/ui && npm ci && npm run build \ && ( cd dist && find . -type f -not -path './_gzip/*' \ | while read -r f; do mkdir -p "_gzip/$(dirname "$f")"; gzip -9 -c "$f" > "_gzip/$f"; done ) \ @@ -530,7 +530,7 @@ cache lives in **Depot Cache** over sccache's **WebDAV** backend: - `SCCACHE_WEBDAV_TOKEN: ${{ secrets.DEPOT_TOKEN }}` — a Depot **organization** token, stored as the repo secret **`DEPOT_TOKEN`**. -Because `sccache` is **content-addressed** and llama.cpp is pinned (`GIT_TAG b10850`), the +Because `sccache` is **content-addressed** and llama.cpp is pinned (`GIT_TAG b10870`), the ~280 upstream object files are byte-identical every run, so a warm cache recompiles only the *changed* files. Depot's cache is **shared across all branches** (unlike GitHub's per-branch `actions/cache`), so every branch builds incrementally; a `b` version bump @@ -1453,7 +1453,7 @@ ctest --test-dir build --output-on-failure -R "ResultsToJson" #### Upstream source location (in CMake build tree) -llama.cpp is fetched via CMake FetchContent, pinned to `GIT_TAG b10850`. +llama.cpp is fetched via CMake FetchContent, pinned to `GIT_TAG b10870`. **GoogleTest** is a separate `BUILD_TESTING`-only FetchContent (`GIT_TAG v1.17.0`), used solely by the `jllama_test` C++ unit-test binary — not by the shipped library, and not coupled to the diff --git a/README.md b/README.md index f344da82..4a296595 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ **Build:** ![Java 8+](https://img.shields.io/badge/Java-8%2B-informational) ![Platform](https://img.shields.io/badge/Platform-Linux%20%7C%20macOS%20%7C%20Windows%20%7C%20Android-lightgrey) -[![llama.cpp b10850](https://img.shields.io/badge/llama.cpp-%23b10850-informational)](https://github.com/ggml-org/llama.cpp/releases/tag/b10850) +[![llama.cpp b10870](https://img.shields.io/badge/llama.cpp-%23b10870-informational)](https://github.com/ggml-org/llama.cpp/releases/tag/b10870) [![JPMS](https://img.shields.io/badge/JPMS-modular%20JAR-25A162)](https://openjdk.org/projects/jigsaw/) ![JUnit](https://img.shields.io/badge/tested%20with-JUnit6-25A162) [![JSpecify](https://img.shields.io/badge/JSpecify-1.0.0%20%40NullMarked-25A162)](https://jspecify.dev) diff --git a/docs/history/llama-cpp-breaking-changes.md b/docs/history/llama-cpp-breaking-changes.md index a76f656d..83656e00 100644 --- a/docs/history/llama-cpp-breaking-changes.md +++ b/docs/history/llama-cpp-breaking-changes.md @@ -698,3 +698,5 @@ Used during `llama.cpp` version bumps: when upgrading, scan this file from the r | **macOS-15 failure — cause isolated, 2026-09-06** | `Java Tests macOS 15 arm64 (Metal)` vs `(no Metal)`, run **33994480652** (#910, dispatch, pin b10819) | **It is a Metal bug.** The first dispatch after the `-DGGML_METAL=OFF` fix turns the two macOS-15 jobs into a clean single-variable comparison, and they disagree: same runner, same OS, same `-DGGML_NATIVE=OFF`, identical **1742 tests / 11 skipped** on both sides, differing only in `GGML_METAL`. With Metal: **2 errors** — `MemoryManagementTest` 1 of 10 and `LlamaModelTest` 1 of 60, both `llama_model_load: error loading model: vector`, unchanged from #897. Without Metal: **0 errors**, those same classes 10/10 and 60/60, and **zero** `MTL0`/`ggml_metal` lines in the log where the old build had 44 — so the flag fix demonstrably took effect. The standing conclusion "the no-Metal job fails identically, so this is not a Metal problem" was an artifact of the no-op flag and is now retired. **What this does NOT establish:** `Java Tests macOS 14 arm64 (Metal)` is green, but that job differs from the red one in **two** variables at once — OS version *and* `GGML_NATIVE` (macos-14 builds host-native, macos-15 passes `-DGGML_NATIVE=OFF`) — so it cannot narrow "Metal" to "Metal on macOS 15" or to "Metal plus a portable build". Only the macos-15 pair isolates anything. **What it buys.** The bisect window stays b10618 → b10797 in tags, but the *candidate set* collapses from every upstream change to the Metal backend alone (`ggml/src/ggml-metal/**`) — precisely the directory every bump review in this table waved through as "GPU backend, safe to skip". That rule is sound for compile/link breaks and blind to runtime ones; this is the first entry where it cost something. **Still open:** which Metal commit, and whether upstream has since fixed it. The rest of run #910 was green — 51 success, 1 failure, 12 skipped (all downstream of the red job), including `Run PIT mutation tests: success`, which is the first CI confirmation of the PIT gate since the SLF4J classpath fix. **Both open questions are answered by the root-cause row at the end of this table**: the commit is `8c0b9cd04` (#27701), and no — upstream has not fixed it (the bare `splits[i] /= split_sum` is still there at b10850), so the fix is carried locally as `patches/0012`. | | b10819–b10850 | `common/arg.cpp` (**additive**: `--log-jsonl` / `--no-log-jsonl`), `common/log.{cpp,h}` (**additive**: `common_log_set_jsonl`, plus a `level_str` helper and a `json.h` include), `common/jinja/{caps,runtime}.cpp`, `tools/server/server-models.{cpp,h}` (**159 lines rewritten**; `request_stop(name)` added to the header), `tools/server/CMakeLists.txt` + `README.md` + `tests/unit/test_router.py`. **`include/`, `ggml/include/` and `tools/mtmd/` are untouched in the range.** | **No project-source change.** The header moves are additive: `common_log_set_jsonl` is new API nothing here calls, and the two new `common_arg` entries are CLI-only. The 159 changed lines in `server-models.cpp` are the router's instance lifecycle, not its argv rendering. **The patch intersection was NOT empty this time** — `common/arg.cpp` (patch `0001`) and `tools/server/server-models.cpp` (patch `0008`) are both in the range, so the applier had to prove the contexts survived rather than a disjoint file list implying it: a fresh `rm -rf build && cmake -B build -DBUILD_TESTING=ON` applied all eight clean and stamped them at head `f114f91f9ed6792cf402437e3874adad98902744`. Server contract re-checked mechanically and byte-identical (request-field set, `set_hard_limits` bounds, response keys in both emit forms). **Chunking, with the figures recorded rather than a verdict asserted:** the full diff is **466 KB over 31 commits**; excluding the paths this table's earlier rows excluded (`ggml/src`, `tools/ui`, `docs`, `.github`, `conversion`) it is still **171 KB**, over the runbook's 100 KiB threshold — but that figure is misleading, because the remainder is dominated by `scripts/ui-assets.cmake`, `tests/test-backend-ops.cpp`, a new model architecture (`src/models/spark2-5.cpp` + its Jinja template) and `gguf-py/`, none of which the project links against and none of which the old exclusion list names. The **review surface proper** — `common/`, `include/`, `tools/server/`, `tools/mtmd/`, `ggml/include/`, top-level `CMakeLists.txt` — is **27 KB / 10 files / +181 / −98**, comfortably inside the threshold. Recorded both ways so the call is auditable. **Metal moved in this range too** (`ggml-metal-device.m` +7/−2 and a new 147-line `ggml-metal-tuning.cpp`), which matters only because of the macOS-15 finding in the row below: this bump neither targets nor is known to fix it. | | **macOS-15 failure — root cause + fix, 2026-09-08** | `src/llama-model.cpp` (upstream) ← `ggml/src/ggml-metal/ggml-metal-device.m` `8c0b9cd04` ([#27701](https://github.com/ggml-org/llama.cpp/pull/27701)); carried as `llama/patches/0012` | **Found by reading, not by bisecting — the bisect was prepared and then not needed.** The chain, each link checked against the source rather than inferred: **(1)** `8c0b9cd04` "metal : fix memory query under low-memory conditions" lies inside the b10618→b10797 window and rewrote `ggml_metal_device_get_memory` to `*free = *total > cur ? *total - cur : 0`. **(2)** Both the green and the red run log `current allocated size is greater than the recommended max working set size`, i.e. `cur > total` — so that condition is the **precondition, not the discriminator**; it held on both sides of the regression. **(3)** Before the clamp, `*total - cur` **underflowed** to a huge `size_t`, which normalised harmlessly; after it, the device reports exactly `free == 0`. **(4)** `load_tensors`' `if (free == 0 && total == 0)` host-memory fallback does not fire, because `total` is `recommendedMaxWorkingSetSize` and is non-zero. **(5)** `splits[0] = 0` → `split_sum = 0` → `splits[i] /= split_sum` = **NaN**. **(6)** `std::upper_bound(…, NaN)` — every comparison false — returns the end iterator, so `layer_gpu == 1`. **(7)** `devices.at(1)` on a one-element vector throws `std::out_of_range`, whose libc++ `what()` is the bare string `"vector"`; `llama.cpp`'s `catch (const std::exception & err)` prints it verbatim. **(8)** The **discriminator** is `const int act_gpu_layers = devices.empty() ? 0 : …`: without a GPU backend `devices` is empty, every layer returns early on `cpu_dev`, and the `.at()` line is unreachable — which is exactly why only the Metal job failed. This accounts for every observation the two investigation rows above collected: Metal-only; only on *repeat* loads in a long-lived JVM (that is merely how `currentAllocatedSize` grew past the recommended set size, not a precondition of its own); `what() == "vector"`; ~30 ms in, right after the vocab warnings; and the absence of a `hyperparameters:`/`vocabulary:` prefix, since upstream's own rethrows would have added one. **The fix** is `patches/0012`: `llama_model_splits_normalize()` falls back to an even split when the weights sum to zero, and `llama_model_splits_select_device()` bounds-checks the lookup and throws a message naming the function, the layer, the device index and the split points. Both are lifted out of `load_tensors` into free functions **for testability** — the failing state needs a real over-committed GPU and cannot be arranged through any API — with an upstream `tests/test-model-split.cpp` and, because a FetchContent subproject sets `LLAMA_BUILD_TESTS=OFF`, a project-side runnable guard `src/test/cpp/test_model_split.cpp` that links the same two functions into `jllama_test` on every platform. **A second trigger, found while writing the fix up and verified against the unfixed library:** `--tensor-split` is parsed with `std::stof` and never range-checked, so `-ts 1,-1` makes the weights cancel, `split_sum` is 0 again, the split points become `[inf, -nan]`, and every layer maps one past the last device — on CUDA, Vulkan or ROCm as much as on Metal, in a fresh process with no memory pressure. The macOS failure is therefore one *instance* of a general defect, not a Metal edge case, which is what settles the question of upstream-submittability. **Two lessons worth carrying.** The `ggml/src/**` "safe to skip" rule in the review list is sound for compile/link breaks and blind to runtime ones; this is the first entry where it cost something, and the cost was ~180 builds of bisect window. And a `catch (…) { log(err.what()); }` over a library that throws `std::out_of_range` is a **diagnostic dead end** on libc++, which reports it as `"vector"` and nothing else — the second half of `0012` exists for that reason alone, and is why the message a future occurrence produces will name its own cause. | +| b10850–b10870 | `common/chat.cpp` (**−2524 lines**: every model-specific chat parser split out into a new `common/parsers/` directory — 19 new files, wired in via `common/parsers/sources.cmake` + `common/CMakeLists.txt`), `common/arg.cpp` (**behaviour change, not a signature change** — see below), `common/speculative.cpp` (**behaviour change** — see below), `ggml/include/ggml.h` (**additive + one deprecation**: `ggml_prec` gains `GGML_PREC_UNDEFINED`/`BF16`/`F16`/`Q8`/`Q4`, `GGML_PREC_DEFAULT` kept as a same-value alias marked deprecated; two new `GGML_API` functions `ggml_prec_set_acc` / `ggml_prec_set_src`, and **`ggml_mul_mat_set_prec` + `ggml_flash_attn_ext_set_prec` are now `GGML_DEPRECATED`**), `tools/server/server-context.cpp` (checkpoint eviction), `tools/mtmd/clip.cpp` + two model files, `src/llama-model.cpp` (**#28160**, lazy-mode AUTO), `tests/CMakeLists.txt`. **`common/chat.h` is byte-identical in the range** | **No project-source change.** The headline number is misleading: `common/chat.cpp` losing 2524 lines is a pure **internal reorganisation** — `common/chat.h`, which `jllama.cpp` includes directly and which is #2 on the priority review list, does not change at all, so nothing the project compiles against moved. The `ggml.h` change is additive plus deprecations that break nothing: `GGML_PREC_DEFAULT` keeps its value, and the two newly-deprecated functions still exist. The project source was grepped for all of it — `GGML_PREC`, `ggml_prec_set_*`, `ggml_mul_mat_set_prec`, `ggml_flash_attn_ext_set_prec` across `src/main/cpp/**` and `src/test/cpp/**` — with **zero** references, so none of it can reach us. (A deprecation is worth naming anyway: it is the shape that becomes a removal two bumps later, and a removal is the one change that breaks a build with no diff hunk to notice.) Server contract re-checked mechanically and **byte-identical in all three dimensions** (request-field set, `set_hard_limits` bounds, response keys in both emit forms). **Two behaviour changes that a header diff cannot see, and both reach every entry point that parses argv** (`NativeServer` in both modes, `LlamaModel`'s own parameter parse): **(1)** `--mmproj-device` now **defaults to `--device`** instead of auto-selecting (`common_params_parse` assigns `params.mmproj_device = params.devices.front()` when `mmproj_use_gpu` is set and `-mmdev` was not) — a caller that sets `--device` but not `-mmdev` now pins the multimodal projector to the same device rather than letting it choose, which is the surface `MultimodalIntegrationTest` and every vision user exercises. **(2)** the **draft model inherits the global device list** the same way, and `common_speculative_init` now only overwrites `result.devices` **when the spec device list is non-empty** (it previously assigned unconditionally), plus forces `LLAMA_SPLIT_MODE_LAYER` when the draft is pinned to exactly one device. That is the speculative-decoding path `LlamaModelTest#testSpeculativeDecoding` drives — the same test that was red on macOS before `patches/0012`, so a failure there after this bump needs to be attributed carefully between the two. **Chunking, with the figures recorded rather than a verdict asserted:** the full diff is **393 KB over 20 commits**, over the runbook's 100 KiB threshold; the **review surface proper** (`common/`, `include/`, `tools/server/`, `tools/mtmd/`, `ggml/include/`, top-level `CMakeLists.txt`) is **28 files, +2686 / −2480**, but ~2500 of those lines on each side are the one mechanical parser move, so the material change is a few dozen lines. Bumped straight rather than chunked on that basis, with the raw numbers here so the call is auditable. | +| b10850–b10870 | patches + upstream verification | **The intersection was NOT empty, and `0012` was the patch at risk.** The range touches `src/llama-model.cpp` and `tests/CMakeLists.txt` — both files `patches/0012` modifies, one week after that patch landed — plus `common/arg.cpp` (`0001`) and `tools/server/server-context.cpp` (`0002`/`0003`/`0010`). So this bump could not be waved through on a disjoint file list. **The `0012`-specific check `CLAUDE.md` mandates was run by hand first**, because the fail-loud applier detects "does not apply" but never "upstream already fixed this": `git show b10870:src/llama-model.cpp | grep -A3 split_sum` still shows the bare `splits[i] /= split_sum` with **no zero-sum guard**, so upstream has not adopted the fix and the patch stays rather than being dropped. Upstream's own change to that file (#28160, resolving `LLAMA_LAZY_MODE_AUTO` to `OFF` on devices without mmap support) sits ~60 lines above the patched region and is unrelated. Then the applier was run for real: fresh `rm -rf llama/build && cmake -B build -DBUILD_TESTING=ON`, configure clean, stamp written at head `1945e092030f8668ff93382799502d01490e564d` (= `b10870`), **all nine hashes recorded**. | diff --git a/llama/CMakeLists.txt b/llama/CMakeLists.txt index 0b107226..3609f725 100644 --- a/llama/CMakeLists.txt +++ b/llama/CMakeLists.txt @@ -173,7 +173,7 @@ set(LLAMA_BUILD_APP OFF CACHE BOOL "" FORCE) FetchContent_Declare( llama.cpp GIT_REPOSITORY https://github.com/ggerganov/llama.cpp.git - GIT_TAG b10850 + GIT_TAG b10870 PATCH_COMMAND ${CMAKE_COMMAND} -DPATCH_DIR=${CMAKE_CURRENT_SOURCE_DIR}/patches -DLLAMA_SRC= diff --git a/llama/src/main/java/net/ladenthin/llama/value/LlamaCppVersion.java b/llama/src/main/java/net/ladenthin/llama/value/LlamaCppVersion.java index 4bebe2f4..17b07180 100644 --- a/llama/src/main/java/net/ladenthin/llama/value/LlamaCppVersion.java +++ b/llama/src/main/java/net/ladenthin/llama/value/LlamaCppVersion.java @@ -10,13 +10,13 @@ * library was compiled against, exposed as a compile-time constant so callers can render a badge or * emit a startup log line without loading the native library. * - *

{@link #LLAMA_CPP_VERSION} is a pure-Java string ({@code "b10850"}) that mirrors the + *

{@link #LLAMA_CPP_VERSION} is a pure-Java string ({@code "b10870"}) that mirrors the * {@code GIT_TAG} in {@code llama/CMakeLists.txt}. It is available even when {@code libjllama} is * absent (pure-Java checkout, before {@code System.load}), which is what makes it suitable for a * lightweight version badge in Android or other UIs.

* *

For the authoritative value that is baked into the native binary — the build number - * plus the resolved upstream commit, e.g. {@code "b10850-"} — call + * plus the resolved upstream commit, e.g. {@code "b10870-"} — call * {@link net.ladenthin.llama.LlamaModel#getLlamaCppBuildInfo()} instead; that reads llama.cpp's own * {@code build-info} through JNI and therefore cannot drift from the compiled library (but requires * the native library to be loaded).

@@ -24,14 +24,14 @@ public final class LlamaCppVersion { /** - * The pinned llama.cpp release tag this library was built against, e.g. {@code "b10850"}. + * The pinned llama.cpp release tag this library was built against, e.g. {@code "b10870"}. * *

Kept in lockstep with {@code GIT_TAG} in {@code llama/CMakeLists.txt} — see the * "Upgrading/Downgrading llama.cpp Version" checklist in {@code CLAUDE.md}. This is the * compile-time pin; use {@link net.ladenthin.llama.LlamaModel#getLlamaCppBuildInfo()} for the * value actually linked into the native binary.

*/ - public static final String LLAMA_CPP_VERSION = "b10850"; + public static final String LLAMA_CPP_VERSION = "b10870"; // Constants holder — not instantiable. private LlamaCppVersion() {} From b1db2c275250befbd9686ead84c6dbf3c5534fb3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 08:51:49 +0000 Subject: [PATCH 3/3] fix(ci): rebuild the WebUI embed step on upstream's ui-assets.cmake The `Build WebUI assets (shared)` job has been failing since #420 with cc1plus: fatal error: embed.cpp: No such file or directory and every native build job declares `needs: build-webui`, so the whole matrix -- including `Java Tests macOS 15 arm64 (Metal)` -- was `skipped` behind it. That is why run 34170576764 (#913, main at b10850) produced no macOS result, and why patches/0012 has still never executed on Metal. Not caused by the b10870 bump: `tools/ui/` is byte-identical between b10850 and b10870 and `embed.cpp` exists at neither tag. Upstream c457e3bf7 ("ui : embed assets directly with CMake", #28445) deleted the host tool and replaced it with scripts/ui-assets.cmake plus ui.cpp.in / ui.h.in templates -- present at b10819, gone at b10850. The step now invokes that script directly. Priority 1 of its provisioning order is "pre-built assets in /dist", which is exactly what the npm step above produces, so BUILD_UI=OFF and HF_ENABLED=OFF keep it from running a second npm build or reaching Hugging Face. LLAMA_UI_GZIP is upstream's own knob and replaces the hand-rolled gzip loop. The sparse checkout gains `scripts` alongside `tools/ui`, and generation goes to RUNNER_TEMP so only ui.cpp/ui.h are copied into the artifact, as before. The completeness guard had to change too, and this is the part worth reading. A bare `grep LLAMA_UI_HAS_ASSETS` -- inherited from the old step -- silently PASSES the failure case now: ui.h.in emits "/* #undef LLAMA_UI_HAS_ASSETS */" for an empty table, so the token is present either way. embed.cpp emitted no such line, which is why the naive grep used to be sufficient. Verified, not assumed: with dist/index.html removed the old guard exited 0 and reported "real WebUI embedded" while ui.cpp shrank from 3862 to 694 bytes. The guard now asserts the ACTIVE #define plus a non-zero count parsed from `std::array`. Verified by running the step itself, extracted verbatim from publish.yml and executed in a simulated CI layout against a real b10870 tools/ui + scripts checkout: good path exits 0 with "8 assets embedded"; the empty-dist path exits 1 with "empty asset table (assets=0)". The generated ui.cpp/ui.h compile standalone and expose the same interface as src/main/cpp/webui_stub/ui.h (same struct, same three functions), so they stay a drop-in for the stub. Docs corrected in the same commit: CLAUDE.md's WebUI section and local recipe described the deleted embed.cpp, and the stub's header comment named it as the generator. webui_stub/ui.h is outside the clang-format CI glob (*.cpp/*.hpp only), so its pre-existing formatting is left untouched. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01AnNYn8W1xuVxVJtyL34GyH --- .github/workflows/publish.yml | 61 ++++++++++++++++++++---------- CLAUDE.md | 32 +++++++++++----- llama/src/main/cpp/webui_stub/ui.h | 4 +- 3 files changed, 65 insertions(+), 32 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7e0af9cd..03df5cf0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -549,7 +549,12 @@ jobs: repository: ggml-org/llama.cpp ref: ${{ steps.tag.outputs.tag }} path: llamacpp-ui - sparse-checkout: tools/ui + # tools/ui holds the npm project AND the ui.cpp.in / ui.h.in templates; + # scripts/ holds ui-assets.cmake, which consumes them. Both are needed + # since upstream #28445 replaced the embed.cpp host tool. + sparse-checkout: | + tools/ui + scripts sparse-checkout-cone-mode: true - uses: actions/setup-node@v7 with: @@ -565,30 +570,46 @@ jobs: npm ci --ignore-scripts npm run build test -f dist/index.html - - name: Embed assets into ui.cpp / ui.h (gzip parity with upstream) - working-directory: llamacpp-ui/tools/ui + - name: Embed assets into ui.cpp / ui.h (upstream scripts/ui-assets.cmake) shell: bash run: | set -euo pipefail - # gzip every asset into dist/_gzip/ so llama-ui-embed embeds the - # compressed bytes (LLAMA_UI_GZIP parity); embed auto-detects _gzip. - ( cd dist && find . -type f -not -path './_gzip/*' | while read -r f; do - mkdir -p "_gzip/$(dirname "$f")" - gzip -9 -c "$f" > "_gzip/$f" - done ) - # llama-ui-embed is a self-contained C++17 host tool (no npm) — build + run it. - g++ -O2 -std=c++17 -o llama-ui-embed embed.cpp - mkdir -p "$GITHUB_WORKSPACE/llama/webui-generated" - ./llama-ui-embed \ - "$GITHUB_WORKSPACE/llama/webui-generated/ui.cpp" \ - "$GITHUB_WORKSPACE/llama/webui-generated/ui.h" \ - dist + # Upstream #28445 ("ui : embed assets directly with CMake") deleted the + # tools/ui/embed.cpp host tool this step used to compile, and replaced it + # with scripts/ui-assets.cmake -- a plain `cmake -P` script, no npm and no + # host executable. Priority 1 of its provisioning order is "pre-built + # assets in /dist", which is exactly what the npm step + # above produced, so BUILD_UI and HF_ENABLED stay OFF: no second npm run + # and no Hugging Face download happen here. LLAMA_UI_GZIP is upstream's + # own knob and replaces the hand-rolled gzip loop this step used to do. + GEN="${RUNNER_TEMP}/ui-assets" + OUT="${GITHUB_WORKSPACE}/llama/webui-generated" + mkdir -p "$GEN" "$OUT" + cmake \ + "-DUI_SOURCE_DIR=${GITHUB_WORKSPACE}/llamacpp-ui/tools/ui" \ + "-DUI_BINARY_DIR=${GEN}" \ + "-DLLAMA_SOURCE_DIR=${GITHUB_WORKSPACE}/llamacpp-ui" \ + -DBUILD_UI=OFF \ + -DHF_ENABLED=OFF \ + -DLLAMA_UI_GZIP=ON \ + -P "${GITHUB_WORKSPACE}/llamacpp-ui/scripts/ui-assets.cmake" + # The script also drops a ui-gzip/ working tree next to the generated + # sources; copy only the two files the artifact is defined to carry. + cp "$GEN/ui.cpp" "$GEN/ui.h" "$OUT/" echo "=== generated WebUI assets ===" - ls -la "$GITHUB_WORKSPACE/llama/webui-generated" - if grep -q LLAMA_UI_HAS_ASSETS "$GITHUB_WORKSPACE/llama/webui-generated/ui.h"; then - echo "LLAMA_UI_HAS_ASSETS: present (real WebUI embedded)" + ls -la "$OUT" + # Guard against a silently empty WebUI. A bare `grep LLAMA_UI_HAS_ASSETS` + # does NOT work here and would pass the failure case: upstream's ui.h.in + # emits "/* #undef LLAMA_UI_HAS_ASSETS */" when the table is empty, so the + # token is present either way. (The old embed.cpp emitted no such line at + # all, which is why the naive grep used to be sufficient.) Assert the ACTIVE + # #define and a non-zero asset count instead -- verified against both paths. + N=$(sed -n 's/.*std::array.*/\1/p' "$OUT/ui.h" | head -1) + if grep -qE '^[[:space:]]*#define[[:space:]]+LLAMA_UI_HAS_ASSETS' "$OUT/ui.h" \ + && [ -n "$N" ] && [ "$N" -gt 0 ]; then + echo "LLAMA_UI_HAS_ASSETS: present, $N assets embedded" else - echo "ERROR: embed produced an empty asset table" >&2 + echo "ERROR: ui-assets.cmake produced an empty asset table (assets=${N:-unknown})" >&2 exit 1 fi - name: Upload WebUI artifact diff --git a/CLAUDE.md b/CLAUDE.md index 31990405..c1a7ae35 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -470,9 +470,21 @@ Pipeline (`.github/workflows/publish.yml`): pinned `b` tag from `llama/CMakeLists.txt`'s `GIT_TAG`, sparse-checks-out `ggml-org/llama.cpp@` `tools/ui`, runs the upstream Svelte build (`npm ci && npm run build`), gzips `dist/` into `dist/_gzip/` (LLAMA_UI_GZIP - parity), builds the self-contained `llama-ui-embed` host tool (plain C++17, **no - npm**) and runs it to produce the platform-independent **`webui-generated/ui.cpp` - + `ui.h`**, uploaded as the `webui-generated` artifact. + parity), then runs upstream's own **`scripts/ui-assets.cmake`** (a plain `cmake -P` + script, **no npm and no host executable**) to produce the platform-independent + **`webui-generated/ui.cpp` + `ui.h`**, uploaded as the `webui-generated` artifact. + Upstream **#28445** deleted the `tools/ui/embed.cpp` host tool this job used to + compile and replaced it with that script plus `ui.cpp.in`/`ui.h.in` templates; the + job passes `BUILD_UI=OFF HF_ENABLED=OFF` so the script takes its priority-1 path + ("pre-built assets in `/dist`") over the tree npm just built — no + second npm run, no Hugging Face download. `LLAMA_UI_GZIP` is upstream's own knob + and replaced the job's hand-rolled gzip loop. The sparse checkout therefore needs + **`scripts` as well as `tools/ui`**. **The completeness guard cannot be a bare + `grep LLAMA_UI_HAS_ASSETS`**: `ui.h.in` emits `/* #undef LLAMA_UI_HAS_ASSETS */` + for an empty table, so that token is present either way and the check passes the + failure case — the old `embed.cpp` emitted no such line, which is why the naive + grep used to work. The job asserts the **active** `#define` plus a non-zero count + parsed out of `std::array`. 2. **Every native build job** (`needs: [startgate, build-webui]`) downloads that artifact into `webui-generated/` before building. npm never runs in the dockcross cross-compilers (which have no node) or per-platform. @@ -489,14 +501,14 @@ needs no extra step here, `build-webui` re-reads the tag and rebuilds the matchi **Building the WebUI locally** (optional — a plain `cmake` build uses the stub and ships no UI): ```bash -# needs node/npm + network; embed.cpp is plain C++17 (no npm) +# needs node/npm + network for the asset build; the embed step is plain cmake -P git clone --depth 1 --branch b10870 https://github.com/ggml-org/llama.cpp /tmp/lc -( cd /tmp/lc/tools/ui && npm ci && npm run build \ - && ( cd dist && find . -type f -not -path './_gzip/*' \ - | while read -r f; do mkdir -p "_gzip/$(dirname "$f")"; gzip -9 -c "$f" > "_gzip/$f"; done ) \ - && g++ -O2 -std=c++17 -o /tmp/llama-ui-embed embed.cpp ) -mkdir -p webui-generated -/tmp/llama-ui-embed webui-generated/ui.cpp webui-generated/ui.h /tmp/lc/tools/ui/dist +( cd /tmp/lc/tools/ui && npm ci && npm run build ) +mkdir -p webui-generated /tmp/ui-gen +cmake -DUI_SOURCE_DIR=/tmp/lc/tools/ui -DUI_BINARY_DIR=/tmp/ui-gen \ + -DLLAMA_SOURCE_DIR=/tmp/lc -DBUILD_UI=OFF -DHF_ENABLED=OFF -DLLAMA_UI_GZIP=ON \ + -P /tmp/lc/scripts/ui-assets.cmake +cp /tmp/ui-gen/ui.cpp /tmp/ui-gen/ui.h webui-generated/ cmake -B build && cmake --build build --target jllama # now embeds the real UI ``` `webui-generated/` is git-ignored. diff --git a/llama/src/main/cpp/webui_stub/ui.h b/llama/src/main/cpp/webui_stub/ui.h index feb15889..a2589a28 100644 --- a/llama/src/main/cpp/webui_stub/ui.h +++ b/llama/src/main/cpp/webui_stub/ui.h @@ -6,7 +6,7 @@ // ui.h — minimal stand-in for the WebUI asset interface that llama.cpp's // tools/ui (CMake target "llama-ui") normally GENERATES into ui.h / ui.cpp at -// build time via the llama-ui-embed host tool. +// build time via scripts/ui-assets.cmake (before upstream #28445: the embed.cpp host tool). // // The upstream HTTP transport (tools/server/server-http.cpp) does // #include "ui.h" @@ -14,7 +14,7 @@ // llama_ui_use_gzip(). We compile server-http.cpp directly into libjllama but do // NOT ship the Svelte WebUI assets (building them needs npm, or a prebuilt-asset // download from Hugging Face) — so we provide the exact "empty asset table" -// interface that embed.cpp emits for its n_assets == 0 branch: the struct plus +// interface upstream's ui.h.in emits for its n_assets == 0 branch: the struct plus // the three functions, returning nothing. // // LLAMA_UI_HAS_ASSETS is intentionally left UNDEFINED. Every static-asset-serving