diff --git a/.github/buildcheck/nativedeps.py b/.github/buildcheck/nativedeps.py index 90075d1ed..abd80f267 100755 --- a/.github/buildcheck/nativedeps.py +++ b/.github/buildcheck/nativedeps.py @@ -35,7 +35,9 @@ and the directories in GLIBC_CEILING, built in a manylinux image or against its sysroot, may not reference a glibc symbol version above the floor they promise, nor a libstdc++ one (GLIBCXX_*, CXXABI_*) above LIBSTDCXX_CEILING. Android libraries must also have every LOAD segment 16 KB -aligned. +aligned, and on s390x every typeinfo name (_ZTS*) of the static symbol table must sit at the even +address the ABI promises (TYPEINFO_NAME_ALIGNMENT; a jllama library without any is not examined, +which fails too). That every listed build arrived is merge-native-artifacts.sh's check. Exit codes: 0 clean, 1 violation, 2 nothing found to check. @@ -262,6 +264,72 @@ def elf_exports(data): return out +SHT_SYMTAB = 2 +SHN_LORESERVE = 0xFF00 + + +def elf_symbols(data): + """(name, address) of every defined symbol of the static symbol table (.symtab), whatever its + binding: the export list and -fvisibility=hidden make almost every symbol of a shipped library + local there. A stripped library has no .symtab, and the list is empty.""" + is64, end, sections = _elf_sections(data) + out = [] + for sh_type, sh_offset, sh_size, sh_link, _ in sections: + if sh_type != SHT_SYMTAB: + continue + strtab = sections[sh_link][1] + entry = 24 if is64 else 16 + for off in range(sh_offset, sh_offset + sh_size, entry): + if is64: + st_name, _, _, st_shndx, st_value = struct.unpack_from(end + "IBBHQ", data, off) + else: + st_name, st_value, _, _, _, st_shndx = struct.unpack_from(end + "IIIBBH", data, off) + if st_name == 0 or st_shndx == 0 or st_shndx >= SHN_LORESERVE: + continue + out.append((_cstring(data, strtab + st_name), st_value)) + return out + + +# The s390x ELF ABI gives every global symbol an even address, and GCC relies on it: it loads an +# address with LARL, whose R_390_PC32DBL relocation counts in halfwords. clang emits typeinfo name +# strings (_ZTS*) with the alignment of char (ItaniumCXXABI.cpp, GetAddrOfTypeName), and the +# s390x build mixes clang's objects with GCC's -- gcc-toolset-14's libstdc++_nonshared.a, which +# loads _ZTSSt19_Sp_make_shared_tag with LARL. Whether a link then fails ("misaligned symbol") +# depends on where the copy it keeps lands: green on main, red in the next PR's jllama_test link +# (#577). .github/manylinux/s390x-typeinfo-names.ld (SUBALIGN(2) over every .rodata._ZTS* +# section) and -fdata-sections (which gives every typeinfo name such a section, the ones of +# classes with a key function included) make it hold for all of them; this check is what notices +# when they stop being in effect, in every build rather than in the one whose layout comes out odd. +TYPEINFO_NAME_ALIGNMENT = {"s390x": 2} + + +def typeinfo_name_alignment(rel): + """The alignment the ABI of the directory of `rel` (...////) + requires for typeinfo names, or None.""" + parts = rel.split("/") + return TYPEINFO_NAME_ALIGNMENT.get(parts[-3]) if len(parts) >= 4 else None + + +def typeinfo_name_violations(rel, symbols, alignment, required=True): + """Typeinfo names (_ZTS*) among `symbols` ((name, address) pairs) at an address that is not a + multiple of `alignment`. A library that must carry them (`required`) and carries none was not + examined -- stripped, or its table unreadable -- which is never a pass.""" + names = [(n, v) for n, v in symbols if n.startswith("_ZTS")] + if not names: + if not required: + return [] + return [f"{rel}: no typeinfo name (_ZTS*) in its symbol table ({len(symbols)} symbols) -- stripped, or the " + f"table could not be read, so their {alignment}-byte alignment cannot be checked"] + odd = [n for n, v in names if v % alignment] + if not odd: + return [] + shown = ", ".join(odd[:8]) + (f", ... ({len(odd)} in all)" if len(odd) > 8 else "") + return [f"{rel}: {len(odd)} of {len(names)} typeinfo names (_ZTS*) are not {alignment}-byte aligned: {shown} -- " + f"GCC code (gcc-toolset's libstdc++_nonshared.a) loads them with LARL, so a link that keeps such a " + f"copy fails with 'misaligned symbol'; .github/manylinux/s390x-typeinfo-names.ld or -fdata-sections " + f"(s390x-toolchain.cmake) is not in effect"] + + def elf_load_alignments(data): """p_align of every PT_LOAD program header.""" if data[:4] != b"\x7fELF": @@ -611,6 +679,7 @@ def main(argv): rel = os.path.relpath(path, root).replace(os.sep, "/") siblings = {n for n in os.listdir(os.path.dirname(path)) if n != os.path.basename(path)} alignments, runpath, glibc, glibcxx, cxxabi, note = (), None, None, None, None, "" + typeinfo = [] try: if path.endswith(".so"): with open(path, "rb") as f: @@ -620,6 +689,10 @@ def main(argv): glibcxx, cxxabi = info["glibcxx"], info["cxxabi"] if "/Linux-Android/" in "/" + rel: alignments = elf_load_alignments(data) + symbol_alignment = typeinfo_name_alignment(rel) + if symbol_alignment: + typeinfo = typeinfo_name_violations(rel, elf_symbols(data), symbol_alignment, + required=os.path.basename(path) in JLLAMA_LIBRARIES) note = (f" [runpath={runpath}]" if runpath else "") + "".join( f" [{glibc_name(v, family)}]" for family, v in (("GLIBC", glibc), ("GLIBCXX", glibcxx), ("CXXABI", cxxabi)) if v) @@ -630,6 +703,7 @@ def main(argv): continue print(f"{rel}: {' '.join(deps)}{note}") failures += violations(rel, deps, alignments, siblings, runpath, glibc, glibcxx, cxxabi) + failures += typeinfo if os.path.basename(path) in JLLAMA_LIBRARIES: try: names = exports(path) diff --git a/.github/buildcheck/tests/test_nativedeps.py b/.github/buildcheck/tests/test_nativedeps.py index 05991af3d..5ccf33b18 100644 --- a/.github/buildcheck/tests/test_nativedeps.py +++ b/.github/buildcheck/tests/test_nativedeps.py @@ -12,14 +12,15 @@ from buildcheck import nativedeps -def elf64(needed, runpath=None, glibc=(), exports=(), undefined=()): +def elf64(needed, runpath=None, glibc=(), exports=(), undefined=(), symtab=()): """A minimal little-endian ELF64 with a .dynstr and a .dynamic naming `needed` (and a DT_RUNPATH when given), plus a .gnu.version_r requiring the `glibc` symbol versions, plus a - .dynsym defining `exports` (global) and referencing `undefined`.""" + .dynsym defining `exports` (global) and referencing `undefined`, plus a .symtab defining the + `symtab` (name, address) pairs as local objects.""" strtab = b"\0" offsets = {} for name in (list(needed) + ([runpath] if runpath is not None else []) + (["libc.so.6"] if glibc else []) - + list(glibc) + list(exports) + list(undefined)): + + list(glibc) + list(exports) + list(undefined) + [n for n, _ in symtab]): offsets[name] = len(strtab) strtab += name.encode() + b"\0" dynamic = b"".join(struct.pack(" + * + * SPDX-License-Identifier: MIT OR Apache-2.0 + */ + +/* + * Augments GNU ld's default script for the s390x cross build (s390x-toolchain.cmake passes it with + * -T; INSERT keeps the default script in force): every typeinfo name string (_ZTS*) lands 2-byte + * aligned. + * + * The s390x ABI guarantees every global symbol an even address, and GCC relies on it: it loads a + * symbol's address with LARL, whose R_390_PC32DBL relocation counts in halfwords. clang emits the + * typeinfo name strings with the alignment of char instead (ItaniumCXXABI.cpp, GetAddrOfTypeName, + * unchanged on LLVM main in 2026-10), and as COMDAT, so the copy the link keeps is the first one -- + * clang's, from our objects. gcc-toolset-14's libstdc++_nonshared.a, which the toolset's libstdc++.so + * script adds to every link, is GCC code: fs_path80.o's _M_get_deleter loads + * _ZTSSt19_Sp_make_shared_tag with LARL, and the link fails with "misaligned symbol ... for + * relocation R_390_PC32DBL" whenever the sizes of the strings before it add up to an odd number. A + * layout lottery: green on main, red on the next change (PR #577, the jllama_test link). + * + * SUBALIGN(2) sets the alignment of every input section of this output section to 2. Only typeinfo + * name strings are matched; they are char arrays read byte-wise, and 2 is all the ABI promises + * for them (GCC emits them with 2). INSERT BEFORE .rodata puts the statement ahead of the default + * .rodata pattern (.rodata.*), so it is the first to match these sections. -fdata-sections (in the + * toolchain) gives every typeinfo name such a section, those of classes with a key function and of + * internal types included, and verify-native-deps.py fails a library with one at an odd address. + */ +SECTIONS +{ + .rodata.jllama_typeinfo_names : SUBALIGN(2) { *(.rodata._ZTS*) } +} +INSERT BEFORE .rodata; diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 04741885e..f9c6c24cd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -424,14 +424,14 @@ jobs: java-version-file: .java-version - name: Install parent + core net.ladenthin:llama (classes; the test JVM loads the downloaded native library via lib.path) uses: ./.github/actions/build-core - - name: Build, test and document llama-ffm (the model-backed test runs on the cached draft model) + - name: Build, test and document llama-ffm (the model-backed tests run on the cached draft model and the cached Qwen3-TTS pair) run: > mvn -B --no-transfer-progress -f llama-ffm/pom.xml verify -Dgpg.skip=true -Dnet.ladenthin.llama.ffm.model=models/${DRAFT_MODEL_NAME} -Dnet.ladenthin.llama.lib.path=${{ github.workspace }}/llama/src/main/natives/net/ladenthin/llama/Linux/x86_64/cpu - name: Require the native-backed tests to have run (a self-skip would hide a broken binding) run: | - for test in FfmLlamaModelIntegrationTest JllamaNativeTest; do + for test in FfmLlamaModelIntegrationTest FfmTextToSpeechIntegrationTest JllamaNativeTest; do report="llama-ffm/target/surefire-reports/TEST-net.ladenthin.llama.ffm.$test.xml" test -f "$report" || { echo "::error::no report for $test"; exit 1; } if grep -q 'skipped="[1-9]' "$report" || grep -q 'tests="0"' "$report"; then @@ -947,8 +947,10 @@ jobs: - name: Run C++ unit tests under qemu-s390x (big-endian gate) run: ctest --test-dir llama/build --no-tests=error --output-on-failure # The same check `package` runs over the merged tree, here so a lost floor fails in this job: - # GLIBC_2.28, GLIBCXX_3.4.25, CXXABI_1.3.11 and the exact dependency list. - - name: Check the dependencies and the glibc floor + # GLIBC_2.28, GLIBCXX_3.4.25, CXXABI_1.3.11, the exact dependency list, and every typeinfo name + # at an even address (s390x-typeinfo-names.ld + -fdata-sections; the link alone fails only when + # GCC code happens to reference an odd one). + - name: Check the dependencies, the glibc floor and the typeinfo name alignment run: python3 .github/verify-native-deps.py llama/src/main/natives/net/ladenthin/llama/ - name: Upload artifacts uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d6a4b73f..01270b2e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,30 @@ from version 5.0.0 onward. Pre-fork releases (`1.x`–`4.2.0`) were authored by ## [Unreleased] ### Changed +- **llama.cpp b11901** (from b11887, 14 upstream commits, tagged). Nothing new to call from Java; + what changes is robustness. Input nested deeper than upstream's new limits is refused instead of + overflowing the native stack, which ended the JVM (#31153): a JSON schema (`response_format`, tool + parameters) nested more than 128 levels deep -- or, with `additionalProperties`, a property name of + 128 characters or more -- is an invalid request (`InvalidRequestException`); a chat template nested + more than 128 levels deep, or with macro calls nested that deep, is an error of its own; model output + the chat parser would have to follow more than 64 levels deep (a tool call's arguments) fails the + request with a `LlamaException`. Long + tokens of a GGUF no longer recurse in the tokenizer or the grammar sampler, which upstream also + measures as faster grammar sampling. Metal no longer overflows the output offset of very large + matrix multiplications (#30072), the SYCL module bounds its reorder buffer to 32 MiB (#31062), + DeepSeek-V4.1 review fixes (#31137), WebUI layout work (#31030, #31119). No patch changed. - **`bernardladenthin/actions` pinned at v1.1.0.** v1.1.0 adds `java/audit-publication`, which `post-release.yml` uses (see Added); nothing changes for the actions already in use. ### Fixed +- **`TextToSpeech.synthesize()` works more than once per instance.** Every call after the first failed + with `prompt processing failed` (llama.cpp: "inconsistent sequence positions"): the Qwen3-TTS + pipeline numbers each prompt from position 0 and leaves the KV cache to the caller, and the engine + never cleared it, so the previous synthesis was still there. It now clears the sequence before each + synthesis, as upstream's server does (`prompt_clear` before `set_input`); `FfmTextToSpeech` and the + C function `jllama_tts_synthesize` share the fix. Unseen until now because the integration test + loaded a fresh instance for its one synthesis; `TtsIntegrationTest` now shares one instance and + synthesizes on it repeatedly. - **`cpu-linux-s390x` loads on glibc 2.28, like the other Linux CPU jars** (RHEL 8, Ubuntu 20.04, Debian 10 and later, z13 or newer). The 6.0.0 jar was cross-compiled against the ubuntu runner's glibc 2.39 sysroot and needed `GLIBC_2.38`, `GLIBCXX_3.4.32` and `CXXABI_1.3.13`, so @@ -21,7 +41,14 @@ from version 5.0.0 onward. Pre-fork releases (`1.x`–`4.2.0`) were authored by cross-compiles with clang against the manylinux_2_28 s390x image unpacked as a sysroot (`.github/manylinux/manylinux_2_28-s390x-sysroot`, `s390x-toolchain.cmake`: glibc 2.28, gcc-toolset-14's libstdc++ treatment); the library references `GLIBC_2.27`, `GLIBCXX_3.4.25` and - `CXXABI_1.3.11`, and its C++ suite runs under qemu against that runtime. + `CXXABI_1.3.11`, and its C++ suite runs under qemu against that runtime. Mixing clang's objects with + the toolset's GCC-built `libstdc++_nonshared.a` needs one linker script + (`.github/manylinux/s390x-typeinfo-names.ld`): clang aligns typeinfo name strings like `char`, + GCC loads them with `LARL`, which needs an even address, and the link failed whenever the layout put + the one it kept on an odd one (`misaligned symbol _ZTSSt19_Sp_make_shared_tag`). `-fdata-sections` + puts every typeinfo name within the script's reach, and `verify-native-deps.py` fails an s390x + library with a typeinfo name at an odd address -- in every build, not only in the one whose layout + happens to come out odd. - **The Android AAR poms no longer carry `checker-qual`.** The 6.0.0 `llama-android` and `llama-android-opencl` poms listed `org.checkerframework:checker-qual:4.3.1` at `compile` scope, which the core keeps `provided`, so it was in every Android consumer's dependency graph. Not a crash @@ -31,6 +58,16 @@ from version 5.0.0 onward. Pre-fork releases (`1.x`–`4.2.0`) were authored by An app that used checker-qual's annotations itself through the AAR has to declare it. ### Added +- **`TextToSpeech.supportsSpeakerReference()`** (and `FfmTextToSpeech.supportsSpeakerReference()`, + C API `jllama_tts_supports_speaker_reference()`): whether the loaded pair clones a voice from a + speaker reference clip, llama.cpp's `mtmd_helper_cap::audio_gen_spk_ref` (b11901, #31183) -- + Qwen3-TTS and Pocket TTS do, Soprano does not. A pair that does not now refuses a reference clip + with a `LlamaException` naming it, instead of the generic "TTS input rejected" it failed with. +- **Capabilities in `getModelMeta()`**: `ModelMeta.supportsChat()` (false only for a + text-to-speech-only model), `supportsAudioGeneration()` (speech output, the embedded server's + `POST /tts`) and `supportsAudioStream()` (realtime transcription, `/v1/realtime`), from a new + `capabilities` object of the meta JSON (`chat`, `audio_gen`, `audio_stream`) that llama.cpp's + server derives from the loaded projector. Absent, and therefore `false`, for a vocab-only load. - **The post-release check runs by itself** (`.github/workflows/post-release.yml`, after the Publish run of a `v*` tag, or by hand with a tag). It audits the published release from the outside: - through bernardladenthin/actions' new `java/audit-publication`: the run and its jobs, the files diff --git a/CLAUDE.md b/CLAUDE.md index acc107860..f8d837ecd 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: **b11887** +Current llama.cpp pinned version: **b11901** ## Natives jars: library jars and GPU module jars (`.github/natives.csv`) @@ -103,7 +103,7 @@ it or is checked against it: | `.github/merge-native-artifacts.sh` | reads the list: every listed `natives-*` artifact present, no other, each holding its library (or module) and nothing outside its directory, each with a `jllama-build.txt` naming a tag, the provenance keys and the row's classifier, next to a `jllama-sbom.json` naming the same classifier, a module directory holding nothing but its module, the `jllama-*.txt` lists and the BOM, no path claimed twice; writes `jllama-extras.txt` (sibling files loaded before the library, e.g. OpenVINO's `OpenCL.dll` on Windows -- never the files a build's own `jllama-files.txt` names, which are only extracted; each of those must exist) | | `.github/package-smoke-sets.sh` | reads the list through `check-natives.py smoke-targets` / `smoke-set`: fails when a natives jar of a set was not built; also writes the glibc-floor set (`floor-set`) | | the glibc-floor set | **derived**: the library rows whose directory promises a glibc floor (`nativedeps.GLIBC_CEILING`: the three Linux CPU jars) — `check-natives.py floor-set` prints them; `package` uploads them with the classes jar and its dependencies as `llama-glibc-floor`, and `smoke-glibc-floor` loads each on RHEL 8.10 (glibc 2.28, GCC 8's libstdc++) on its own architecture, s390x under qemu (`smoke-glibc-floor.sh`, `NativeLoadSmoke.java` in the UBI 8 OpenJDK 21 runtime image); `check-natives.py` holds the job's matrix rows to the set | -| `.github/verify-native-deps.py` | every library of a directory: exact dependency allowlist per library directory (`cpu`/`metal`) plus the files next to it; **the export set of every `jllama` library** (`JNI_OnLoad`, `JNI_OnUnload`, `Java_*`, `jllama_*` and nothing else -- `cmake/jllama-exports.map` on ELF and `cmake/jllama-exports-macos.txt` on macOS keep the statically linked llama/ggml/mtmd/BoringSSL symbols local, Windows exports only `dllexport`; before the lists the Linux library exported 14,220 symbols, 47 of them JNI), and for the Android OpenCL module (bionic + `libOpenCL.so` + `libggml-base.so`); denylist for the other GPU modules, each of which must import `ggml-base` and, as ELF, reach it only through run path `$ORIGIN` (an Android library carries no run path -- bionic's search path is the app's native-library directory -- and only a build-tree one fails there); the manylinux_2_28 builds (both Linux CPU jars, CUDA) held to `GLIBC_2.28`; every Android library 16 KB page-aligned (Google Play). Runs in `package` and on the staged AAR libraries | +| `.github/verify-native-deps.py` | every library of a directory: exact dependency allowlist per library directory (`cpu`/`metal`) plus the files next to it; **the export set of every `jllama` library** (`JNI_OnLoad`, `JNI_OnUnload`, `Java_*`, `jllama_*` and nothing else -- `cmake/jllama-exports.map` on ELF and `cmake/jllama-exports-macos.txt` on macOS keep the statically linked llama/ggml/mtmd/BoringSSL symbols local, Windows exports only `dllexport`; before the lists the Linux library exported 14,220 symbols, 47 of them JNI), and for the Android OpenCL module (bionic + `libOpenCL.so` + `libggml-base.so`); denylist for the other GPU modules, each of which must import `ggml-base` and, as ELF, reach it only through run path `$ORIGIN` (an Android library carries no run path -- bionic's search path is the app's native-library directory -- and only a build-tree one fails there); the manylinux_2_28 builds (both Linux CPU jars, CUDA) held to `GLIBC_2.28`; every Android library 16 KB page-aligned (Google Play); every typeinfo name (`_ZTS*`) of the s390x library at an even address (see "Linux s390x"). Runs in `package` and on the staged AAR libraries | | `.github/smoke-natives-jars.sh` (`package` job) | loads the real jars: classes + all 23 natives jars at once, on the classpath **and** the module path; it passes `net.ladenthin.llama.backend` naming **every** module of the runner's platform, opt-in ones included, and the loader line must then name each -- so a module jar the loader skipped fails here, and this is also the only place in CI that loads an opt-in module at all | | `.github/smoke-lib.sh` / `smoke-test-natives.ps1` | the model-backed smoke rows expect every module of the set **except** the opt-in ones (`optin` column), because the loader deliberately leaves those out -- it is the same list, read with one more condition | @@ -215,7 +215,7 @@ measurements behind the Windows step are in `docs/handover/local-agent-report-b1 a `llama-common`/`cpp-httplib` that came out shared (one more file to ship). On the static platforms (macOS, s390x) all of it stays inside `libjllama`. **Every ggml default llama.cpp's own CMakeLists changes before its `add_subdirectory(ggml)` is repeated** -- `GGML_LLAMAFILE_DEFAULT` - and `GGML_CUDA_GRAPHS_DEFAULT` at b11887 (unchanged since b11639) -- because adding ggml first skips that block; re-check with + and `GGML_CUDA_GRAPHS_DEFAULT` at b11901 (unchanged since b11639) -- because adding ggml first skips that block; re-check with `grep -n _DEFAULT CMakeLists.txt` at the new tag on every bump. The second one was missing once: ggml's own default for CUDA graphs is OFF, and a CUDA module built without it lost two thirds of its token generation (265.6 -> 88.8 tg128 on an RTX 3070, prompt processing -20%; Vulkan, which @@ -368,7 +368,7 @@ rejects anything else loudly. | `natives.py` | `check-natives.py` (`code-style`) | everything that names a natives jar agrees with `natives.csv` (see "Natives jars") | | `models.py` | `check-natives.py` | every `*_MODEL_NAME` of publish.yml's `env:` is a filename of `models.csv` | | `patches.py` | `check-patches.py` (`code-style`) | every hunk of `llama/patches/*.patch` declares exactly the lines it carries -- the half `git apply --check` does not see (see "Local llama.cpp source patches"); and `docs/patches.md` is what `check-patches.py --render` generates from the patch headers (a stale page fails) | -| `nativedeps.py` | `verify-native-deps.py` (`package`, `package-android-aar`) | dependency allowlists, 16 KB Android alignment | +| `nativedeps.py` | `verify-native-deps.py` (`package`, `package-android-aar`, `build-linux-s390x`) | dependency allowlists, 16 KB Android alignment, every s390x typeinfo name at an even address | | `hipoffload.py` | `verify-hip-offload-compressed.py` (the two ROCm jobs) | no uncompressed GPU code bundle | | `aarpoms.py` | `verify-aar-poms.py` (`package-android-aar`, on the poms in mavenLocal) | both AAR poms carry exactly the core's compile-scope dependencies, at its versions and at `compile` (plus the CPU AAR, `type aar`, in the OpenCL one) | | `postrelease.py` | `post-release-check.py` (`post-release.yml`, after the release) | the published natives jars against `natives.csv`, their provenance with `patches.sha256` recomputed from the tag, the source archive's stamp and reproducible pack, the AAR invariants and poms, the cache-free tag build (docs/post-release-check.md, checks 3, 8, 9, 13, 14) | @@ -1229,7 +1229,7 @@ needs no extra step here, `build-webui` re-reads the tag and rebuilds the matchi ships no UI): ```bash # needs node/npm + network for the asset build; the embed step is plain cmake -P -git clone --depth 1 --branch b11887 https://github.com/ggml-org/llama.cpp /tmp/lc +git clone --depth 1 --branch b11901 https://github.com/ggml-org/llama.cpp /tmp/lc ( 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 \ @@ -1269,7 +1269,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 (`JLLAMA_GIT_TAG b11887`), the +Because `sccache` is **content-addressed** and llama.cpp is pinned (`JLLAMA_GIT_TAG b11901`), 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 @@ -1856,7 +1856,12 @@ engine owns semantic-token sampling via a `common_sampler`, feeding each sampled backbone's hidden state into `step_gen()` and receiving the next hidden state back — the same pattern upstream's own `tools/tts/tts.cpp` `main()` uses) → `get_output()` for raw PCM, which the engine encodes to WAV itself via `tts_wav.hpp` (not upstream's own WAV writer) so that already-tested code -stays in the loop. +stays in the loop. **The engine clears sequence 0 of the KV cache before `set_input()`**: the Qwen3-TTS +pipeline numbers every prompt from position 0 and leaves the cache to the caller (upstream's server +calls `prompt_clear()` right before `set_input()`; `tools/tts/tts.cpp` synthesizes once per process and +never needs to). Without it every synthesis after the first on one `TextToSpeech` failed with "prompt +processing failed" -- unseen until `TtsIntegrationTest` shared one instance across its tests; +`synthesizesAgainOnTheSameInstance` pins it. **Why this replaced OuteTTS, not extended it.** Upstream #26254 ("mtmd: support Qwen3-TTS") deleted the entire OuteTTS implementation from `tools/tts/tts.cpp` (it shrank from ~1450 to 205 lines) and @@ -2300,7 +2305,7 @@ If the local check passes (`BUILD SUCCESS`), the `mvn package` job in **Java layer** (`src/main/java/net/ladenthin/llama/`): - `LlamaModel` — Main API class (AutoCloseable). Wraps native context for inference, embeddings, re-ranking, and tokenization. -- `TextToSpeech` — Separate AutoCloseable native type for speech synthesis over llama.cpp's upstream Qwen3-TTS pipeline (a backbone text GGUF + an mmproj GGUF bundling the speaker encoder, code predictor, and code2wav decoder); `synthesize(text)` returns a 24 kHz mono 16-bit WAV byte stream, with overloads for a cloned-voice speaker-reference clip and language. Native orchestration in `tts_engine.{h,cpp}` drives upstream's `mtmd_helper::gen_audio` streaming API directly (see "Qwen3-TTS via `mtmd_helper::gen_audio`" below) — there is nothing extracted or hand-copied from llama.cpp source; the in-memory WAV writer is `tts_wav.hpp`. +- `TextToSpeech` — Separate AutoCloseable native type for speech synthesis over llama.cpp's upstream Qwen3-TTS pipeline (a backbone text GGUF + an mmproj GGUF bundling the speaker encoder, code predictor, and code2wav decoder); `synthesize(text)` returns a 24 kHz mono 16-bit WAV byte stream, with overloads for a cloned-voice speaker-reference clip and language; `supportsSpeakerReference()` says whether the pair clones voices (llama.cpp's `mtmd_helper_cap::audio_gen_spk_ref`, read once at load), and the engine refuses a reference clip by name for a pair that does not. Native orchestration in `tts_engine.{h,cpp}` drives upstream's `mtmd_helper::gen_audio` streaming API directly (see "Qwen3-TTS via `mtmd_helper::gen_audio`" below) — there is nothing extracted or hand-copied from llama.cpp source; the in-memory WAV writer is `tts_wav.hpp`. - `ModelParameters` / `InferenceParameters` — Builder-pattern parameter classes. Every wire name they can emit is an enum constant carrying the contract it must satisfy (`args.ModelOption` + `args.ModelFlag` for argv, `parameters.RequestField` for the request body), and the base classes accept nothing else — see @@ -2316,7 +2321,7 @@ If the local check passes (`BUILD SUCCESS`), the `mvn package` job in - The `server` package is a dedicated top layer in the ArchUnit `layeredArchitecture` rule (the only layer allowed to access the root `Api`); `noInternalJdkImports` carries an explicit exception for the supported `com.sun.net.httpserver` (the exported `jdk.httpserver` module, which `module-info.java` `requires`). See README "OpenAI-compatible HTTP server". **Native layer** (`src/main/cpp/`): -- `jllama.cpp` — JNI implementation bridging Java calls to llama.cpp. 36 native methods (32 `LlamaModel` + 3 `TextToSpeech` + 1 `LlamaQuantizer`) plus `JNI_OnLoad`/`JNI_OnUnload`; since the C API (see "The C API" below) every one of them is a shell over `jllama_api.cpp` / `jllama_engine.cpp`, where the model logic lives. +- `jllama.cpp` — JNI implementation bridging Java calls to llama.cpp. 37 native methods (32 `LlamaModel` + 4 `TextToSpeech` + 1 `LlamaQuantizer`) plus `JNI_OnLoad`/`JNI_OnUnload`; since the C API (see "The C API" below) every one of them is a shell over `jllama_api.cpp` / `jllama_engine.cpp`, where the model logic lives. - `utils.hpp` — Helper utilities (format helpers, argv stripping, token-piece serialisation). - `json_helpers.hpp` — Pure JSON transformation helpers (no JNI, no llama state). Independently unit-testable. - `jni_helpers.hpp` — JNI bridge helpers (handle management + server orchestration). Includes `json_helpers.hpp`. @@ -2442,8 +2447,8 @@ Functions with `_impl` suffix are called directly from `jllama.cpp`. An exception that escapes a native method and unwinds across the JNI boundary is **undefined behaviour and aborts the JVM** on most implementations. **Every `Java_*` entry point must therefore -convert anything that escapes into a Java exception**, and there are 46 of them across four TUs — -`jllama.cpp` (36), `native_server.cpp` (5), `rpc_bridge.cpp` (4), `train_engine.cpp` (1). +convert anything that escapes into a Java exception**, and there are 47 of them across four TUs — +`jllama.cpp` (37), `native_server.cpp` (5), `rpc_bridge.cpp` (4), `train_engine.cpp` (1). The mechanism is `jni_guard_impl(env, exception_class, [&]() -> Ret { … })` (`jni_helpers.hpp`, Layer A). It is **additive**: an entry point that already converts `std::exception` itself keeps @@ -2587,6 +2592,29 @@ through binfmt_misc without arguments (with `-L` the six `RpcComm` death tests f `JLLAMA_S390X_SYSROOT` in the environment still finds it. The job then runs `verify-native-deps.py` on its own tree, and `smoke-glibc-floor` loads the jar on RHEL 8.10 s390x under qemu. +**Mixing clang's objects with GCC's needs `s390x-typeinfo-names.ld`** (next to the toolchain, passed as +`-Wl,-T` with `INSERT`, so it only augments ld's default script). The s390x ABI promises every global +symbol an even address and GCC relies on it, loading addresses with `LARL` (`R_390_PC32DBL`, counted in +halfwords); clang emits typeinfo name strings (`_ZTS*`, COMDAT) with the alignment of `char` +(`GetAddrOfTypeName` in `ItaniumCXXABI.cpp`, unchanged on LLVM main in 2026-10), and the copy a link +keeps is clang's, from our objects. gcc-toolset-14's `libstdc++_nonshared.a` is GCC code +(`fs_path80.o`'s `_M_get_deleter` loads `_ZTSSt19_Sp_make_shared_tag`), so the link failed with +`misaligned symbol ... for relocation R_390_PC32DBL` whenever the strings before it added up to an odd +size: green on main, red in the next PR's `jllama_test` link (#577). The script puts every +`.rodata._ZTS*` into one output section with `SUBALIGN(2)`. Measured with Ubuntu's s390x cross GCC +standing in for the toolset (a GCC archive whose `_M_get_deleter` uses `LARL`, clang objects in front +of it, the real toolchain file over a sysroot in the RHEL layout): the link fails without the script +and succeeds with it, for an executable, a PIE and a shared object, and the program runs under qemu. +**The script reaches only names in a `.rodata._ZTS*` section of their own**, and clang gives one only +to COMDAT names; those of classes with a key function and of internal types (lambdas) sat in plain +`.rodata`, so the toolchain also passes **`-fdata-sections`**. **And the link is not the test:** it +fails only where GCC code happens to reference an odd name, so `verify-native-deps.py` reads the +static symbol table of the s390x `libjllama.so` and fails any `_ZTS*` at an odd address -- by every +binding, because the export map makes all of them local (`TYPEINFO_NAME_ALIGNMENT` in +`nativedeps.py`; a library without any typeinfo name fails too, since it would pass vacuously). It +runs in `build-linux-s390x` after the build. Measured on two local builds of the library with the +stand-in sysroot: script alone, 444 of 1833 typeinfo names odd; script plus `-fdata-sections`, 0. + Until 2026-10-25 the job used the runner's `g++-s390x-linux-gnu` (GCC 13) with its glibc 2.39 sysroot: the 6.0.0 jar referenced `GLIBC_2.38` (`__isoc23_strtol` & co., `fmod`), `GLIBCXX_3.4.32` and `CXXABI_1.3.13`, and failed to load on RHEL 8/9 and Ubuntu 22.04 while every job was green -- @@ -2745,24 +2773,24 @@ ctest --test-dir build --output-on-failure -R "ResultsToJson" | `src/test/cpp/test_common_log_callback.cpp` | 7 | **The runnable guard for `patches/0014`**: `common_log_set_callback()` on a private `common_log_init()` instance (never `common_log_main()`, so the process-wide logger the other tests print through is untouched) — delivery of level + bare text, no prefix/timestamp even when both are on (what `common_init()` does), clearing stops delivery, a swap drains queued entries to the *previous* sink (the property behind `LlamaModel.setLogger(format, null)` being a synchronous flush), a `--log-file` keeps being written alongside the sink, every `ggml_log_level` passes through unchanged, and the sink is not gated on upstream's `common_log_set_stdo()` console switch (b11612: entries reach the callback with stdout/stderr output off). The Java half (`LlamaLoggerTest`, model-free) proves the JNI trampoline on top of it. | | `src/test/cpp/test_jni_helpers.cpp` | 77 | All functions in `jni_helpers.hpp` using a zero-filled `JNINativeInterface_` mock (incl. the `utf8_to_jstring_impl` byte-array string path: emoji byte-preservation, truncated-UTF-8 replace-not-throw), and `jni_utf8.hpp` (six: an ASCII message stays on `ThrowNew`, a 4-byte character or an invalid byte reaches the exception constructor as the original bytes through `String(byte[], "UTF-8")`, the same for `new_string_utf8_impl` and for the JNI exception boundary; and seven: an exception already pending is left in place on both paths, as `jni_guard_impl` does). Seven of them pin `jni_guard_impl` — the JNI exception boundary every `Java_*` entry point runs inside — including the `catch (...)` arm that is the only backstop for a non-`std::exception` type, and its two refusals (never `ThrowNew` over a pending Java exception, never with a null class). | | `src/test/cpp/test_tts_wav.cpp` | 2 | The in-memory WAV writer `pcm_to_wav16_bytes` in `tts_wav.hpp` (WAV header/payload + little-endian clamping) — our own code, not upstream. The Qwen3-TTS pipeline it pairs with (`mtmd_helper::gen_audio`) is entirely upstream-owned (no project-side DSP to unit-test here). The load path is additionally covered by `test_tts_params.cpp` (3 tests over `tts_params.hpp`'s `build_tts_params`, plus 2 pinning the upstream `-1` default it depends on), which pins the CPU-thread resolution whose absence used to crash the JVM on every platform — see the `TODO.md` entry for the mechanism. End-to-end coverage is `TtsIntegrationTest`, which is model-gated. | -| `src/test/cpp/test_tts_params.cpp` | 13 | The **three** builders every hand-assembled `common_params` goes through: `build_tts_params` (`tts_params.hpp`), `build_train_params` (`train_params.hpp`) and the shared `jllama::resolve_cpu_params` (`cpu_params.hpp`). Each builder is guarded separately on purpose — testing the resolver alone does **not** cover its call sites, because `train_engine.cpp` is compiled into `jllama` only, never into `jllama_test`, and `LlamaTrainerIntegrationTest` is gated on `net.ladenthin.llama.train.model`, which no CI job sets. Without these the JVM-abort bug could regress in the trainer on every platform, unseen. | +| `src/test/cpp/test_tts_params.cpp` | 16 | The **three** builders every hand-assembled `common_params` goes through: `build_tts_params` (`tts_params.hpp`), `build_train_params` (`train_params.hpp`) and the shared `jllama::resolve_cpu_params` (`cpu_params.hpp`). Each builder is guarded separately on purpose — testing the resolver alone does **not** cover its call sites, because `train_engine.cpp` is compiled into `jllama` only, never into `jllama_test`, and `LlamaTrainerIntegrationTest` is gated on `net.ladenthin.llama.train.model`, which no CI job sets. Without these the JVM-abort bug could regress in the trainer on every platform, unseen. Three more pin `speaker_reference_refusal` (`tts_params.hpp`), the rule `engine_synthesize` applies before it touches the model: a pair without a voice encoder (`mtmd_helper_cap::audio_gen_spk_ref` false, b11901) refuses a speaker reference by name, no reference or a pair that clones voices goes on. | | `src/test/cpp/test_prefetch.cpp` | 5 | **The runnable guard for `patches/0017`** (the x86 prefetch helper). The patch fixes two defects in the four `_mm_prefetch` calls of `ggml_vec_dot_q4_0_q8_0`'s SSSE3-without-AVX branch, and only one of them is a build error. The **type** half is caught by the compiler, but only by a recent one: plain clang on a `*-windows-msvc` target gets MSVC's `char const *` declaration (its own casting macro sits behind `#ifndef _MSC_VER`), and the resulting `-Wincompatible-pointer-types` is an **error by default from clang 22 on** -- measured on the real file at `-msse4.2`: 4 warnings on 20.1.8, 4 errors on 22.1.3 and 23.1.3, and 0/0 on all three once patched (`-msse4.2 -mavx` compiles either way, taking the AVX branch). So `GGML_CPU_ALL_VARIANTS=ON` stops building with plain clang on Windows once the compiler is new enough -- via the **`sse42`** variant, the only one that reaches `#elif defined(__SSSE3__)` (the `x64` baseline has no SSSE3 and takes the scalar branch; everything richer has AVX). Nothing in a test can cover that half. The **arithmetic** half is what this file guards, and it is silent: `&x[ib] + sizeof(block_q4_0)` adds `sizeof()` *elements*, so with the sizes `ggml-common.h` pins by `static_assert` (18 and 34) the prefetch targets +324 and +1156 bytes instead of +18 and +34, and the `+ 2 *` pair +648 instead of +36 -- wrong on **every** platform, including the ones where the call compiles. `ggml_prefetch_at()` casts to `const char *` first, which is what makes the offset byte-wise. **The case that makes the file worth having:** writing the cast around the whole expression, `(const char *)(&x[ib] + sizeof(block_q4_0))`, silences the compiler and keeps the wrong address -- that form builds and fails here. Verified both ways: 5/5 green against the patched header, 5/5 red against a deliberately element-wise one. Includes two internal headers (`ggml-common.h` for the block types, `prefetch.h` for the helper) via the `${llama.cpp_SOURCE_DIR}/ggml/src` and `.../ggml/src/ggml-cpu/arch/x86` include dirs added for it. **Measured, so the upstream report does not overclaim:** the wrong prefetch distance costs no measurable throughput (Ryzen 7 5800H forced onto the `sse42` kernel, 0.6B Q4_0: pp512 101.3 vs 100.9, tg128 37.3 vs 37.0, inside each other's error bars) -- it is a correctness and portability fix, not a performance one. | | `src/test/cpp/test_kolibri1.cpp` | 3 | **The guard for upstream's Kolibri-1 support** (b11709, #30673; carried as `patches/0016` until the b11760 bump, see `docs/history/dropped-llama-patches.md`). Writes tiny random `kolibri1` GGUFs with the public `gguf` API in the format upstream's converter writes (`conversion/kolibri1.py` over `AfmoeModel`: gating function 2, pre-tokenizer `qwen2`, `expert_weights_norm`, an explicit `output.weight`, the per-layer sliding-window pattern), loads them through the real library on the CPU and compares every logit -- one batch, and token by token through the iSWA KV cache -- with an independent double-precision reference written from Aleph Alpha's vLLM implementation, not from llama.cpp. The layer pattern puts a full-attention (NoPE) layer between sliding ones, the sequence is longer than the window, and the expert biases are large enough that DeepSeek-V3's router would pick other experts (one test asserts that, so the comparison cannot pass vacuously); the second comparison turns `norm_topk_prob` on. A red comparison means upstream computes something else than Aleph Alpha's reference (router, window boundary, positional encoding) and is a finding, not a test to adjust. The two format rows of the community dialects the patch loaded (gating 5 + pre `kolibri1` + tied output; the rejection of gating 1) went with the patch: upstream reads the keys above as required, knows no pre-tokenizer `kolibri1`, and passes an unknown gating value on to `build_moe_ffn` instead of refusing it -- those GGUFs are reconverted, not shimmed (README, "Kolibri-1"). | | `src/test/cpp/test_model_flags.cpp` | 4 | **The contract between the Java CLI-flag registries and llama.cpp's server argument parser.** CMake reads `ModelFlag.java` + `ModelOption.java` (`cmake/extract-java-wire-names.cmake` → a generated header of `{name, contract}` pairs), and this file asserts every `SERVER_PARSER` name is in `common_params_parser_init(params, LLAMA_EXAMPLE_SERVER).options`. It exists because **no Java test can catch this class**: `ModelFlagTest`/`ModelParametersExtendedTest` pin the *string mapping* (`hasKey("--mlock")`), never that llama.cpp still accepts the string, so they stay green forever while the flag is dead — and `common_params_parse` treats an unregistered option as a hard error, so the affected builder method makes the model **unloadable**, not merely ineffective. **A grep over `arg.cpp` is not a substitute**: `--grp-attn-n`/`-w` are present there at every pinned tag but `set_examples()`-scoped to `LLAMA_EXAMPLE_COMPLETION`/`PASSKEY`, so the server parser rejects them exactly like a deleted flag — only the real option table sees that. `--vocab-only` is the one exemption, and it declares itself `CliContract.PROJECT_PSEUDO` on its own constant rather than appearing in a list inside this file; the test asserts such a name is **still unknown** to the parser (an exemption upstream later registers would be hiding a real check) and that the exempt set is non-empty. | | `src/test/cpp/test_wire_contracts.cpp` | 6 | **The same contract for the two quieter surfaces.** `RequestField` against `server_schema::make_llama_cmpl_schema(...)` (5 tests) and `TrainingField` against `jllama_train::config_keys()` (1 test). Both receivers *silently ignore* an unknown key — the schema skips it, `train_engine.cpp` reads with `j.value(key, default)` and falls back — so a dead field produces no error anywhere and every string-mapping test keeps passing. `OAI_LAYER`-declared keys (consumed by `oaicompat_*_params_parse` before the schema) are exempt from the schema check, and are checked **both** ways: still unknown to the schema (the inverted check), and read by at least one upstream reader-shaped site (the configure-time sweep — this is what caught `chat_template`, a key a public builder wrote and nothing read). See [`docs/history/parameter-wire-surface.md`](docs/history/parameter-wire-surface.md). | | `src/test/cpp/test_rpc.cpp` | 28 | **The runnable guard for `patches/0015` and for `rpc_support.hpp`.** The device-selection rules (pure, literal inputs: `--rpc` endpoints accumulate across repeated options, an explicit `--device`/`-dev` is never overridden, stale RPC devices are replaced by the local GPUs chosen the way llama.cpp's default does — duplicates by device id dropped, iGPUs only without a discrete GPU, `none` on a CPU-only host). Then the **real** ggml-rpc client and server over loopback, no model: a `mul_mat` graph computed on the RPC backend equals the local CPU result; `ggml_backend_rpc_stop_server()` ends the server, frees the port and disconnects a client that is still connected; an unreachable or malformed endpoint is `nullptr`/`std::invalid_argument` instead of an abort; a registered server that went away is re-checked on the next registration; its device reports 0/0 memory instead of aborting; and a stale server is kept out of a later load that did not ask for it. The loopback server picks a free port from a range so parallel jobs do not collide, and serves the **CPU** by name — a served GPU that lacks an op aborts the server (see the RPC section). Also: server devices chosen by name (case-insensitive, deduplicated), an unknown name listing the available devices, and a registered RPC device refused; the mmproj device pinned the way clip would choose it minus the stale server; and `exclude_stale_devices()`, the params-level guard for TTS and the trainer. | | `src/test/cpp/test_rpc_comm.cpp` | 6 | **`patches/0015`'s COMM_INIT and stop contract over real TCP, protocol v8.** Each scenario runs the C API's RPC server in a fresh subprocess with a deadline, so a regression fails instead of hanging the runner: a loopback server opens no wildcard peer listener and a network bind stays an explicit opt-in; a stop with no peer closes both listeners and lets the port be bound again; a stop interrupts a stalled capability handshake of rank 0 and of rank 1; and a successful peer setup still works and stops. No model, GPU or external network. | -| `src/test/cpp/test_c_api.cpp` | 52 | **The C API (`jllama_api.h`, layer B) without a JVM.** Writes a tiny random two-layer llama GGUF with the GPT-2 byte tokenizer (the public `gguf` API, like `test_kolibri1.cpp`), loads it through `jllama_model_load` and drives the shipped contract: version and build info, null-argument refusal with a message, a missing model failing to load, the load-progress callback (reported, and a `false` return aborts the load), a chat completion that is an OpenAI `chat.completion` object, an invalid request answered as the caller's fault (`JLLAMA_STATUS_INVALID_REQUEST`), streaming until `stop` with the task released afterwards ("Task not found" on the next receive), a released task gone, and the log callback seeing the server's lines with a null set flushing synchronously. Hermetic, so it runs in every `C++ Tests` job without `restore-models`; the output of a random model is noise, the contract is what is asserted. Two things the fixture does on purpose: the output rows of every non-ASCII and special token are scaled to almost nothing, because a byte above 0x7F is an incomplete UTF-8 piece the server holds back rather than streams (a random model that emits only those sends no partial chunk at all), and the task id is checked `>= 0` -- the server numbers its tasks from zero. Then every other `LlamaModel` operation in turn (the meta, raw and OAI completions, raw streaming, the streamed OAI completion (`text_completion` chunks with one id, probabilities, `usage` on the last), the template, tokenize/detokenize, metrics, slots, LoRA, configure, the refusals of a model loaded without `--embedding`/`--reranking`/FIM tokens, the grammar conversion, a Q8_0 quantisation that loads again, several prompts in one request and several completions of one (`"n"` > 1) refused on the blocking, raw and streamed paths while a token array stays one prompt, and every OpenAI answer and chunk carrying a `chatcmpl-` id and the model name, one id per stream), then the other engines: the embedded server in router mode and attached to the model (which still answers after the server stopped), the RPC server over loopback, text to speech refusing missing models, and a fine-tuning run over the random F32 model that writes a GGUF -- which is why `tts_engine.cpp`, `train_engine.cpp` and `native_server_engine.cpp` are in `jllama_test`'s source list. Four model-free cases (`CApiStatic`) run before any load. `CApiSlotFiles` loads the random model with `--slot-save-path`: a saved slot file lands under that directory by its name and restores, and a name with a directory part, `..`, an absolute path or nothing is `JLLAMA_STATUS_INVALID_REQUEST` with nothing written (red before F251: the name was the path, relative to the working directory); a model without the directory refuses save and restore with `JLLAMA_STATUS_ERROR`. Four `CApiRpc` cases pin `jllama_rpc_server_stop()` around the bind and the next serve: a stop issued before ggml binds the socket -- from the serving thread through the test seam `jllama::rpc::set_serve_hook_for_testing()` (`rpc_support.hpp`, not exported), or from another thread while the serve is held in that seam, where the stop must not return before the serve has -- ends the serve instead of being lost (patches/0015 clears ggml's stop flag at the bind; red before F241), a stop while nothing serves does not stop the next serve, and a stop does not reach a serve started while it waits for the one it is for (the stop seam `set_stop_hook_for_testing()` starts the next serve once the stop is recorded; red before F271: the stop waited 30 s for the newcomer and ended it too). Two `CApiFim` cases load the same random model with FIM tokens (`write_tiny_llama_gguf(prefix, true)`): a raw completion with `input_prefix`/`input_suffix`, blocking and streamed, evaluates exactly the prompt `jllama_infill` builds from the same fields, and one of the two fields alone is refused naming the other (red before F246: the completion path completed the empty prompt and dropped both). Two more `CApi` cases pin `messages` on the raw paths: blocking and streamed, the request evaluates exactly the tokens of a completion of the prompt `jllama_apply_template` renders from them (an empty `prompt` beside them counts as none), and a non-empty `prompt` or the infill fields beside them are refused (red before F247: the field was ignored). Two more: the chat fields the raw paths cannot apply beside `messages` (`response_format`, `tools`, `tool_choice`, `parallel_tool_calls`, `top_logprobs`, `thinking_budget_tokens`) are refused by name on both raw paths, and `CApiTemplateStops` loads the random model with an end-of-generation byte of its own (`write_tiny_llama_gguf(prefix, false, 4)`) and a template upstream adds the stop string `` to, which a grammar forces: the raw paths stop there as the chat path does, also beside a `stop` of the request (red before F248: dropped, `stop_type` `eos` and the string in `content`). And `CApiReasoningBudget` loads it with a template that opens a `` block in its generation prompt: a raw completion with `messages` and `reasoning_budget_tokens` 1 completes the forced `` and then the chat path's answer, blocking and streamed, while an output-format grammar beside it still runs (the generation prompt the budget is primed with is left out there); a template without thinking tags leaves the request unchanged (red before F249: the tags were dropped and the budget did nothing). A third case loads the random model with ``, `` and `` as control tokens (`write_tiny_llama_gguf(prefix, false, 4, special)`): the forced end tag reads as text in the raw content, blocking and streamed, and a request's own `preserved_tokens` stays in force beside the template's (red before F250: the session's preserved tokens were not taken over, and the special end tag was an empty piece). A fourth, without `messages`, names the tags in the request with a logit bias that makes the model write the start tag: a budget of zero forces the end tag after each one, and a `reasoning_budget_message` before it (red before F252: without the message the budget forced nothing). Four cases pin the raw multimodal prompt: `CApiVision` loads the random model with a random llava-style vision projector the shared fixture writes too (`write_tiny_mmproj_gguf()`, `--mmproj`) and finds the server's `media_marker` in the model meta; a raw completion whose `prompt` is upstream's `{"prompt_string": "......", "multimodal_data": [""]}` evaluates the text tokens plus the image's four patch embeddings on the blocking, streamed and OpenAI paths (red before F258: refused as if the model had no projector), while markers that do not match the images, data that is not base64, no data and an image at the very end (an abort of the process in `update_slots`) are refused, and the same object on the model without a projector is the caller's fault. `CApiVocabOnly` loads the random model with `--vocab-only`: the meta, tokenize and detokenize calls answer, and a parameterized case per other C function on a model (22 instances: every completion, chat and stream path, the template, embeddings, rerank, infill, metrics, slots, LoRA, configure, systemone, the attached server) is `JLLAMA_STATUS_ERROR` with "`jllama_` is not available in vocab-only mode" (red before F266: a `SIGSEGV` in `llama_vocab::token_bos()` or a wait that never ended). | +| `src/test/cpp/test_c_api.cpp` | 54 | **The C API (`jllama_api.h`, layer B) without a JVM.** Writes a tiny random two-layer llama GGUF with the GPT-2 byte tokenizer (the public `gguf` API, like `test_kolibri1.cpp`), loads it through `jllama_model_load` and drives the shipped contract: version and build info, null-argument refusal with a message, a missing model failing to load, the load-progress callback (reported, and a `false` return aborts the load), a chat completion that is an OpenAI `chat.completion` object, an invalid request answered as the caller's fault (`JLLAMA_STATUS_INVALID_REQUEST`), streaming until `stop` with the task released afterwards ("Task not found" on the next receive), a released task gone, and the log callback seeing the server's lines with a null set flushing synchronously. Hermetic, so it runs in every `C++ Tests` job without `restore-models`; the output of a random model is noise, the contract is what is asserted. Two things the fixture does on purpose: the output rows of every non-ASCII and special token are scaled to almost nothing, because a byte above 0x7F is an incomplete UTF-8 piece the server holds back rather than streams (a random model that emits only those sends no partial chunk at all), and the task id is checked `>= 0` -- the server numbers its tasks from zero. Then every other `LlamaModel` operation in turn (the meta, raw and OAI completions, raw streaming, the streamed OAI completion (`text_completion` chunks with one id, probabilities, `usage` on the last), the template, tokenize/detokenize, metrics, slots, LoRA, configure, the refusals of a model loaded without `--embedding`/`--reranking`/FIM tokens, the grammar conversion, a Q8_0 quantisation that loads again, several prompts in one request and several completions of one (`"n"` > 1) refused on the blocking, raw and streamed paths while a token array stays one prompt, and every OpenAI answer and chunk carrying a `chatcmpl-` id and the model name, one id per stream), then the other engines: the embedded server in router mode and attached to the model (which still answers after the server stopped), the RPC server over loopback, text to speech refusing missing models, and a fine-tuning run over the random F32 model that writes a GGUF -- which is why `tts_engine.cpp`, `train_engine.cpp` and `native_server_engine.cpp` are in `jllama_test`'s source list. Four model-free cases (`CApiStatic`) run before any load. `CApiSlotFiles` loads the random model with `--slot-save-path`: a saved slot file lands under that directory by its name and restores, and a name with a directory part, `..`, an absolute path or nothing is `JLLAMA_STATUS_INVALID_REQUEST` with nothing written (red before F251: the name was the path, relative to the working directory); a model without the directory refuses save and restore with `JLLAMA_STATUS_ERROR`. Four `CApiRpc` cases pin `jllama_rpc_server_stop()` around the bind and the next serve: a stop issued before ggml binds the socket -- from the serving thread through the test seam `jllama::rpc::set_serve_hook_for_testing()` (`rpc_support.hpp`, not exported), or from another thread while the serve is held in that seam, where the stop must not return before the serve has -- ends the serve instead of being lost (patches/0015 clears ggml's stop flag at the bind; red before F241), a stop while nothing serves does not stop the next serve, and a stop does not reach a serve started while it waits for the one it is for (the stop seam `set_stop_hook_for_testing()` starts the next serve once the stop is recorded; red before F271: the stop waited 30 s for the newcomer and ended it too). Two `CApiFim` cases load the same random model with FIM tokens (`write_tiny_llama_gguf(prefix, true)`): a raw completion with `input_prefix`/`input_suffix`, blocking and streamed, evaluates exactly the prompt `jllama_infill` builds from the same fields, and one of the two fields alone is refused naming the other (red before F246: the completion path completed the empty prompt and dropped both). Two more `CApi` cases pin `messages` on the raw paths: blocking and streamed, the request evaluates exactly the tokens of a completion of the prompt `jllama_apply_template` renders from them (an empty `prompt` beside them counts as none), and a non-empty `prompt` or the infill fields beside them are refused (red before F247: the field was ignored). Two more: the chat fields the raw paths cannot apply beside `messages` (`response_format`, `tools`, `tool_choice`, `parallel_tool_calls`, `top_logprobs`, `thinking_budget_tokens`) are refused by name on both raw paths, and `CApiTemplateStops` loads the random model with an end-of-generation byte of its own (`write_tiny_llama_gguf(prefix, false, 4)`) and a template upstream adds the stop string `` to, which a grammar forces: the raw paths stop there as the chat path does, also beside a `stop` of the request (red before F248: dropped, `stop_type` `eos` and the string in `content`). And `CApiReasoningBudget` loads it with a template that opens a `` block in its generation prompt: a raw completion with `messages` and `reasoning_budget_tokens` 1 completes the forced `` and then the chat path's answer, blocking and streamed, while an output-format grammar beside it still runs (the generation prompt the budget is primed with is left out there); a template without thinking tags leaves the request unchanged (red before F249: the tags were dropped and the budget did nothing). A third case loads the random model with ``, `` and `` as control tokens (`write_tiny_llama_gguf(prefix, false, 4, special)`): the forced end tag reads as text in the raw content, blocking and streamed, and a request's own `preserved_tokens` stays in force beside the template's (red before F250: the session's preserved tokens were not taken over, and the special end tag was an empty piece). A fourth, without `messages`, names the tags in the request with a logit bias that makes the model write the start tag: a budget of zero forces the end tag after each one, and a `reasoning_budget_message` before it (red before F252: without the message the budget forced nothing). Four cases pin the raw multimodal prompt: `CApiVision` loads the random model with a random llava-style vision projector the shared fixture writes too (`write_tiny_mmproj_gguf()`, `--mmproj`) and finds the server's `media_marker` in the model meta; a raw completion whose `prompt` is upstream's `{"prompt_string": "......", "multimodal_data": [""]}` evaluates the text tokens plus the image's four patch embeddings on the blocking, streamed and OpenAI paths (red before F258: refused as if the model had no projector), while markers that do not match the images, data that is not base64, no data and an image at the very end (an abort of the process in `update_slots`) are refused, and the same object on the model without a projector is the caller's fault. `CApiVocabOnly` loads the random model with `--vocab-only`: the meta, tokenize and detokenize calls answer, and a parameterized case per other C function on a model (22 instances: every completion, chat and stream path, the template, embeddings, rerank, infill, metrics, slots, LoRA, configure, systemone, the attached server) is `JLLAMA_STATUS_ERROR` with "`jllama_` is not available in vocab-only mode" (red before F266: a `SIGSEGV` in `llama_vocab::token_bos()` or a wait that never ended). Two pin the `capabilities` object of the model meta (b11901, from upstream's server meta): the random model chats and neither generates nor streams audio, with the vision projector loaded as well (`CApiVision`), while a vocab-only load claims none; and `CApiTts` refuses `jllama_tts_supports_speaker_reference()` with a null handle or out-parameter. | | `src/test/cpp/test_native_server_shutdown.cpp` | 3 | **The runnable guard for the shutdown-handler guard in `patches/0006`/`0007`.** Runs the real `llama_server()` in router mode over an empty `--models-dir` on an ephemeral loopback port (no model, no worker) and stops it the way `native_server.cpp` does. Pins a clean stop (exit code 0), that `llama_server_request_shutdown()` **after** the server returned is a no-op (the deterministic form of the CI `SIGSEGV` — it crashed every run before the fix), and that requests hammered from another thread while the server tears down are safe (5 rounds). Compiled on non-Android only, like `server.cpp` itself. | | `src/test/cpp/test_gallocr_leafs.cpp` | 1 | **The runnable guard for `patches/0020`.** Two graphs with the same two nodes in the same order (`n1 = add(x, x)`, `n2 = mul(w, n1)`) and only the leafs moved: one expands `n1` before `n2` and meets the input first, the other expands only `n2`, whose first source is the weight -- the shape of the llama.cpp case, where every node and node source still matches its slot, so the old check passed it. Reserved with the first, the second must get an allocation for the input inside the allocator's buffer and compute the right product on it. No model, every platform. **Verified both ways:** with the fix removed from the source tree the test dies with SIGSEGV (rc 139); a first version that swapped the operands of one `add` passed without the fix, because a swapped *node source* was already caught -- a guard has to move only the leafs. | | `src/test/cpp/test_lora_cache.cpp` | 5 | **The runnable guard for `patches/0021`.** Writes a random llama model (the shared `tiny_llama_gguf.hpp`, also used by `test_c_api.cpp`) and a random LoRA adapter on K, V and the FFN output of every layer, loads them with one slot and `--cache-ram 0`, and answers the same prompt greedily. After `jllama_set_lora_adapters` (scale 1 -> 0 and back) and after a request with its own adapters, the warm slot must answer exactly like an emptied one. One case guards the others against passing vacuously: the adapter changes the answer, and a repeated request reuses the cache (`timings.cache_n > 0`; not `tokens_cached`, which counts every token the slot holds and is positive after any request). After each adapter change the next request must take nothing from the cache (`timings.cache_n == 0`), so those three cases are red without the patch whether or not the random model happens to answer differently. **Verified both ways:** without the patch 3 of 4 red (the warm slot answered from the old scales), with it 4 of 4 green in three runs; no model, every platform. A second fixture (`LoraCacheFreshSlot`: two slots, `--slot-save-path`) saves slot 0 (by its file name inside that directory) after a request and restores it into the never-used slot 1, whose request must take all of the prompt but at most two tokens from it and answer like slot 0 -- red before the `init()` hunk (`Expected: (prompt_n) <= (2), actual: 77 vs 2`), green with it (F240). | -**Current total: 673 tests** (`TEST`/`TEST_F`/`TEST_P` definitions; ctest counts 694 cases, because +**Current total: 678 tests** (`TEST`/`TEST_F`/`TEST_P` definitions; ctest counts 699 cases, because the parameterized `CApiVocabOnly` case expands to 22 instances). #### Upstream source location (in CMake build tree) -llama.cpp is fetched via CMake FetchContent, pinned to `JLLAMA_GIT_TAG b11887`. +llama.cpp is fetched via CMake FetchContent, pinned to `JLLAMA_GIT_TAG b11901`. **GoogleTest** is a separate `BUILD_TESTING`-only FetchContent (`GIT_TAG v1.18.0`), used solely by the `jllama_test` C++ unit-test binary — not by the shipped library, and not coupled to the @@ -3229,26 +3257,31 @@ the "Java 22+ over the C API" item of the delivery roadmap (TODO.md item 10), de Maven running below JDK 22, selecting a registered JDK 22+ from `~/.m2/toolchains.xml`. - **CI**: `test-java-llama-ffm` (`needs: [crosscompile-linux-x86_64, verify-model-cache]`) downloads the Linux x86-64 natives, restores the models, builds the core, runs `mvn -f llama-ffm/pom.xml - verify` with `lib.path` and the draft model, and fails when `FfmLlamaModelIntegrationTest` skipped - itself (a self-skip would hide a broken binding). It gates both publish jobs; the release gate check + verify` with `lib.path` and the draft model, and fails when `FfmLlamaModelIntegrationTest`, + `FfmTextToSpeechIntegrationTest` or `JllamaNativeTest` skipped itself (a self-skip would hide a broken + binding). It gates both publish jobs; the release gate check demands it. The bytecode gate (`java/verify-bytecode`) keeps scanning `llama/target` only: the module's classes are major 66 and are not part of the smoke sets. - **Tests**: `JllamaNativeTest` (model-free: `apiVersion` equals the pom version surefire passes, `buildInfo` starts with `LlamaCppVersion`, a grammar conversion, a missing model's message, the log callback reached from the worker and flushed on `null`, an `Error` thrown by the log callback losing that line and nothing else, RPC device names, and three `takeStrings` cases: a populated array read - in order and freed with its count, an empty non-null array still freed, a null array not freed), + in order and freed with its count, an empty non-null array still freed, a null array not freed, + and `jllama_tts_supports_speaker_reference` refusing a null handle through its bool slot), `FfmNativeServerTest` and `FfmTextToSpeechTest` (no native library, fake `Backend`s: `close()` waits for an `isRunning()` / `synthesize` call inside the native call -- the lifecycle lock, a read/write - lock for TTS --, a call after `close()` does not touch the handle, a second `close()` has no - effect), `FfmRpcServerTest` (loopback, CPU: a stop while nothing serves does not stop the next serve, + lock for TTS --, a call after `close()` does not touch the handle -- `supportsSpeakerReference()` + included --, a second `close()` has no effect), `FfmRpcServerTest` (loopback, CPU: a stop while nothing serves does not stop the next serve, a stop ends it) and `FfmLlamaModelIntegrationTest` (the draft model, or `-Dnet.ladenthin.llama.ffm.model`: progress callback, meta, chat completion, streaming to `stop` and an early close, raw completion and its stream, the OAI completion stream ending with `usage`, tokenize/detokenize round trip, template, metrics, LoRA and configure, slot erase, the save refused without `--slot-save-path` and a slot file named inside that directory (a path refused), the embedding refusal, invalid requests, the attached server answering `/health` and the model outliving it, an `Error` thrown by the progress callback aborting the load instead of the JVM, a - closed model refusing calls). Locally the C++ test fixture's twin, a + closed model refusing calls, the meta's `capabilities`) and `FfmTextToSpeechIntegrationTest` (CI's + Qwen3-TTS pair, or `-Dnet.ladenthin.llama.ffm.tts.model` / `.tts.mmproj`: the speaker-reference + capability and a synthesis with a voice cloned from the core's `sample.wav`; the CI job fails when + it skipped itself, like the other two). Locally the C++ test fixture's twin, a tiny random llama GGUF written with `gguf-py`, serves as the model where no download is possible. - **`maven-versions.poms`** of `.github/actions-config.json` names `llama-ffm/pom.xml` (the siblings read this repository's POM list from there). diff --git a/README.md b/README.md index 20ce89b77..f1adc43bd 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,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 b11887](https://img.shields.io/badge/llama.cpp-%23b11887-informational)](https://github.com/ggml-org/llama.cpp/releases/tag/b11887) +[![llama.cpp b11901](https://img.shields.io/badge/llama.cpp-%23b11901-informational)](https://github.com/ggml-org/llama.cpp/releases/tag/b11901) [![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.1%20%40NullMarked-25A162)](https://jspecify.dev) @@ -756,6 +756,9 @@ try (LlamaModel model = new LlamaModel(modelParams)) { ``` `LlamaModel.supportsVision()` / `supportsAudio()` report which modalities the loaded projector enables. +`getModelMeta()` adds what llama.cpp derives from it: `supportsChat()` (false only for a +text-to-speech-only model), `supportsAudioGeneration()` (speech output, the embedded server's +`POST /tts`) and `supportsAudioStream()` (realtime transcription, `/v1/realtime`). ### Tool Calling @@ -913,7 +916,9 @@ try (TextToSpeech tts = new TextToSpeech( Add `(modelPath, mmprojPath, gpuLayers, threads)` to offload to the GPU, or `synthesize(text, maxFrames, topK, seed)` for explicit sampling, or the full `synthesize(text, speakerReferenceAudioPath, language, maxFrames, topK, seed)` overload for -voice cloning from a reference clip. As with `LlamaModel`, native memory is not GC-managed — use +voice cloning from a reference clip. `supportsSpeakerReference()` says whether the loaded pair can +clone a voice (Qwen3-TTS and Pocket TTS can; a pair that cannot refuses a reference clip with a +`LlamaException`). As with `LlamaModel`, native memory is not GC-managed — use try-with-resources or call `close()`. This replaces the project's earlier two-model OuteTTS + WavTokenizer pipeline, which upstream diff --git a/docs/c-api.md b/docs/c-api.md index 810ac1c54..970316be6 100644 --- a/docs/c-api.md +++ b/docs/c-api.md @@ -35,7 +35,7 @@ Every operation of `LlamaModel` and `LlamaQuantizer` is in it (the roadmap's "B. | `jllama_last_error()` | the message of the last failed call on this thread (thread-local) | | `jllama_string_free()`, `jllama_floats_free()`, `jllama_tokens_free()` | release what the library returned through an out-parameter | | `jllama_model_load()` / `jllama_model_free()` | a model from llama-server's command line (`-m`, `-c`, `-ngl`, ... and `--vocab-only`, which loads the tokenizer only: such a model answers the meta, tokenize and detokenize calls and refuses every other with `JLLAMA_STATUS_ERROR`), with an optional load-progress callback | -| `jllama_model_meta()` | the model's metadata object (vocab, context, modalities, the `media_marker` a raw multimodal prompt places, chat template, the GGUF key/value map) | +| `jllama_model_meta()` | the model's metadata object (vocab, context, modalities, the `capabilities` `chat` / `audio_gen` / `audio_stream` llama.cpp derives from the loaded projector, the `media_marker` a raw multimodal prompt places, chat template, the GGUF key/value map) | | `jllama_chat_completions()` | one OpenAI chat completion, blocking | | `jllama_chat_completions_stream_request()` / `_receive()` / `jllama_task_release()` | a streamed one: a task id, then one `{"data": , "stop": }` envelope per call until `stop` | | `jllama_chat_completions_request()` | a streamed chat completion whose results are llama.cpp's raw result objects (received with `jllama_completions_stream_receive`) | @@ -56,7 +56,7 @@ Every operation of `LlamaModel` and `LlamaQuantizer` is in it (the roadmap's "B. | `jllama_set_log_callback()` | every log line of llama.cpp, ggml and the server to a callback; null restores the console after a synchronous flush | | `jllama_server_start()`, `jllama_server_start_attached()`, `jllama_server_stop()`, `jllama_server_is_running()`, `jllama_server_set_worker_command()` | the embedded llama-server (`NativeServer`): the full upstream server from an argv, WebUI included, or the HTTP frontend attached to a loaded model; router mode's worker command. Not on Android (server.cpp does not build there) | | `jllama_rpc_server_serve()`, `jllama_rpc_server_stop()`, `jllama_rpc_server_listening()`, `jllama_rpc_server_devices()`, `jllama_strings_free()` | the RPC server (`RpcServer`): this process's devices served to llama.cpp RPC clients, one per process | -| `jllama_tts_load()`, `jllama_tts_synthesize()`, `jllama_tts_free()`, `jllama_bytes_free()` | text to speech (`TextToSpeech`): a Qwen3-TTS backbone and mmproj, a WAV per call | +| `jllama_tts_load()`, `jllama_tts_synthesize()`, `jllama_tts_supports_speaker_reference()`, `jllama_tts_free()`, `jllama_bytes_free()` | text to speech (`TextToSpeech`): a Qwen3-TTS backbone and mmproj, a WAV per call; whether the pair clones a voice from a speaker reference (a pair that does not refuses one) | | `jllama_finetune()` | fine-tuning (`LlamaTrainer`): one training run from its JSON configuration | That is every native entry point of the Java API: `LlamaModel`, `LlamaQuantizer`, `NativeServer`, diff --git a/docs/history/llama-cpp-breaking-changes.md b/docs/history/llama-cpp-breaking-changes.md index cd9f6e935..fd704f94b 100644 --- a/docs/history/llama-cpp-breaking-changes.md +++ b/docs/history/llama-cpp-breaking-changes.md @@ -919,3 +919,5 @@ Used during `llama.cpp` version bumps: when upgrading, scan this file from the r | b11827–b11881 | patches | **`0006` and `0007` rewritten, the other ten apply unchanged** (the fail-loud applier on a fresh tree at `fa3645feb`; `check-patches.py`: 12 patches, 94 hunks, 0 problems). Six patch-target files are in the range: `common/arg.cpp` (`0001`, `0015`; the HF-lock change is away from both), `ggml/src/ggml-rpc/transport.cpp` (`0015`; #30399 adds an RDMA bounds check, and RDMA is off here), `tools/server/server-context.{cpp,h}` (`0002`, `0003`, `0021`; #30864 and #30994 touch the rerank template and the realtime route, the `server_state` enum moved out of the header), `tools/server/server-models.cpp` (`0008`; `server_child` moved out, `update_args` unchanged) and `tools/server/server.cpp` (`0001`, `0006`, `0007`), where #31113 broke `0006` and `0007`. **`0006`:** the embedded mode clears upstream's new `server_run_opts::install_signal_handlers` instead of guarding the `sigaction` block, and the `llama_server_shutdown_handler_reset` moved into the two role functions that install a handler, declared after the last local the handler captures -- `models_routes` in the router (declared after `ctx_http` there, so a reset next to `ctx_http` would have run after `models_routes` was destroyed) and `ctx_http` in the instance; the forwarded-argv parse, the mutex and `llama_server_request_shutdown()` are unchanged. **`0007`:** upstream now has the route table as a function of its own, so the patch's `llama_server_register_common_routes()` and its removal block are gone (4 hunks to 2) and `llama_server_attach()` registers `server_register_routes()` plus `server_register_stream_routes()` with the instance handlers, exactly what `server_run_instance()` registers; the verbatim-copy invariant of the b10519 row no longer exists. Guards: `NativeServerShutdown` (3, the real `llama_server()` in router mode), `CApiServer.TheEmbeddedServerStartsInRouterModeAndStops` and `CApi.TheAttachedServerServesTheModelAndLeavesItUsable` green. Drop-checks re-read at `fa3645feb`: `0001` -- `argv = utf8.ptrs.data();` still at `common/arg.cpp:1296`, no `common_params_parse_main`; `0002` -- `params_base.load_progress_callback = load_progress_callback;` unguarded at `server-context.cpp:1286`; `0003` -- no `slot_prompt_similarity` in `server-context.h`; `0008` -- no `LLAMA_SERVER_WORKER_CMD` upstream; `0014` -- `common/log.h` names only `common_log_default_callback`; `0015` -- `GGML_ABORT("Failed to connect to %s\n"` at `ggml-rpc.cpp:650`; `0017` -- the four `_mm_prefetch(&x[ib] + sizeof(...))` calls at `quants.c:846/847/864/865`; `0018` -- `free(buffer->context);` at `amx.cpp:46`; `0020` -- `ggml_gallocr_needs_realloc()` compares the leaf count only; `0021` -- the `else` branch at `server-context.cpp:1942` still takes `params_base.lora_adapters` without the clear. | | b11881–b11883 | `src/llama-grammar.{cpp,h}`, `src/llama-quant.cpp` | **None needed.** Two commits: a recursion limit in the grammar parser (#29755) and the 4D-tensor/expert detection fix of the quantizer (#30820); no header the project includes changed, all twelve patches apply unchanged (`git apply` on `5c965fd8c`), the commit count is 11883. | | b11883–b11887 | `include/llama.h`, `src/llama-sampler.cpp`, `tools/server/server-schema.cpp`, `src/models/qwen4exp.cpp`, `src/llama-arch.{cpp,h}` | **None needed in the code; a contract moved.** #31035 adds `LLAMA_DRY_MAX_SEQ_BREAKERS` (64) to `llama.h`: the request schema drops duplicate `dry_sequence_breakers` and refuses more than 64 distinct ones (an invalid request), the sampler stops at 64 with a warning. Pinned by `test_server.cpp` (`DrySequenceBreakers_DuplicatesDropped`, `DrySequenceBreakers_DistinctLimit`), noted in `withDrySequenceBreakers`' Javadoc. #31028 merges Qwen4-exp tensors on conversion (new tensor names); #30977/#31157 touch upstream CI and tests only. All twelve patches apply unchanged (`git apply` on `6050b467c`), the commit count is 11887. | +| b11887–b11901 | `common/{common.{cpp,h},json-schema-to-grammar.{cpp,h},json-schema.cpp,peg-parser.{cpp,h},jinja/{parser,runtime,value}.cpp}`, `src/llama-grammar.cpp`, `src/llama-vocab.cpp`, `tools/mtmd/mtmd-helper.{cpp,h}`, `tools/server/server-context.cpp`, `tools/server/server-models.cpp`, `src/llama-ext.h`, `src/llama-arch.cpp`, `src/llama-model-saver.{cpp,h}`, `src/llama-kv-cache-dsv4.{cpp,h}`, `src/models/{deepseek4,deepseek41,dflash}.cpp`, `ggml/src/{ggml-cuda/ggml-cuda.cu,ggml-sycl/ggml-sycl.cpp,ggml-metal/kernels/mul_mm.metal,ggml-webgpu/ggml-webgpu.cpp,ggml-cpu/arch/arm/quants.c}` (14 commits, `5320c87ff` = b11901, 2026-10-25 .. 2026-10-26, tagged) | **None needed.** #31153 bounds every recursion over input upstream walks with `COMMON_RECURSIVE_LIMIT` (128, new in `common.h`): the JSON-schema converter and the schema walks (a `$ref` chain counts too; a property name of 128 characters or more is refused where `additionalProperties` makes the converter build a trie of the names), Jinja templates and values (`JINJA_RECURSIVE_LIMIT` 128, macro calls included) and `common.cpp`'s glob matcher -- each an `std::invalid_argument` / `std::runtime_error` where the native stack used to overflow and take the JVM with it; on our request paths these are request-parse errors and stay `InvalidRequestException`. The PEG chat-output parser throws `common_peg_recursion_error` (an `std::invalid_argument`, new in `peg-parser.h`) when a rule is entered more than 64 levels deep -- in the parse of the **model's output**, so it surfaces from `task_result_state::update_chat_msg` on the reading thread and reaches Java as `LlamaException` (`run_guarded` maps everything but `jllama_invalid_request` to `JLLAMA_STATUS_ERROR`). The grammar's candidate rejection, the SPM resegmentation and the vocab trie no longer recurse per character (a long GGUF token cannot overflow the stack; upstream measures faster grammar sampling, same masks), and the rule-name escaping drops `std::regex` (`gbnf_format_rule_name`, now public). #31183 adds `mtmd_helper_get_capabilities()` (`mtmd_helper_cap`: chat, vision, audio, video, audio_stream, text_gen, audio_gen, audio_gen_spk_ref) and deprecates `mtmd_helper_model_can_chat` / `mtmd_helper_support_video`; the server takes its capabilities from it with the same values, and the project calls neither deprecated function (syntax-only compile of `jllama.cpp`, `jllama_api.cpp`, `jllama_engine.cpp`, `native_server_engine.cpp`, `tts_engine.cpp` against the patched tree with `-Wdeprecated-declarations`: no diagnostic). #30880 adds C++ architecture queries to `llama-ext.h` (not included here). #31137 is DeepSeek-V4.1 review cleanup (no GGUF format change), #30072 stops a Metal output-offset overflow in large matrix multiplications, #31062 bounds the SYCL reorder buffer to 32 MiB, #30463 / #31180 are CUDA (no VLA) and ARM warning fixes, #30548 adds tests only, #31030 / #31119 are WebUI (no new npm package, `package-lock.json` unchanged), #31162 / #31174 / #31175 upstream CI. The root `CMakeLists.txt` (`_DEFAULT`s), the ggml CMake files, `vendor/`, `licenses/` and the release workflow (OpenVINO, CCCL, ROCm, SYCL pins) are unchanged; the commit count is 11901. | +| b11887–b11901 | patches | **All 12 apply unchanged** (the fail-loud applier on a fresh tree at `5320c87ff`; `check-patches.py`: 12 patches, 94 hunks, 0 problems). Three patch-target files are in the range: `tools/server/server-context.cpp` (`0002`, `0003`, `0021`; #31183 caches the capabilities in a member, away from all three), `tools/server/server-models.cpp` (`0008`; one comment in `update_caps`) and `tools/mtmd/mtmd-cli.cpp` (`0001`'s flip; the capability change is away from it). Drop-checks re-read at `5320c87ff`: `0001` -- `argv = utf8.ptrs.data();` at `common/arg.cpp:1296`, no `common_params_parse_main`; `0002` -- `params_base.load_progress_callback = load_progress_callback;` unguarded at `server-context.cpp:1287`; `0003` -- no `slot_prompt_similarity` in `server-context.h`; `0008` -- no `LLAMA_SERVER_WORKER_CMD` upstream; `0021` -- the `else` branch at `server-context.cpp:1945` takes `params_base.lora_adapters` without the clear. The files of `0014`, `0015`, `0017`, `0018` and `0020` are not in the range, so their answers of the b11827–b11881 row stand. | diff --git a/llama-ffm/README.md b/llama-ffm/README.md index f46c03fce..8501be575 100644 --- a/llama-ffm/README.md +++ b/llama-ffm/README.md @@ -86,12 +86,15 @@ it selects a registered JDK 22+ from `~/.m2/toolchains.xml`. CI needs only the s ```bash mvn -pl llama -am -DskipTests install # the core first mvn -f llama-ffm/pom.xml verify # the model-free tests + the model-backed ones -# the model-backed test takes the draft model of .github/models.csv, or: -mvn -f llama-ffm/pom.xml verify -Dnet.ladenthin.llama.ffm.model=/path/to/model.gguf +# the model-backed tests take the draft model and the Qwen3-TTS pair of .github/models.csv, or: +mvn -f llama-ffm/pom.xml verify -Dnet.ladenthin.llama.ffm.model=/path/to/model.gguf \ + -Dnet.ladenthin.llama.ffm.tts.model=/path/to/backbone.gguf \ + -Dnet.ladenthin.llama.ffm.tts.mmproj=/path/to/mmproj.gguf ``` `FfmNativeServerTest` and `FfmTextToSpeechTest` run over fake `Backend` implementations and need -no native library. `JllamaNativeTest` and the model-backed `FfmLlamaModelIntegrationTest` skip -themselves without a native build (`llama/src/main/natives`, or +no native library. `JllamaNativeTest` and the model-backed `FfmLlamaModelIntegrationTest` and +`FfmTextToSpeechIntegrationTest` (the speaker-reference capability and a cloned-voice synthesis) +skip themselves without a native build (`llama/src/main/natives`, or `-Dnet.ladenthin.llama.lib.path=`); `FfmRpcServerTest` serves the CPU over loopback and needs one as well. diff --git a/llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmTextToSpeech.java b/llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmTextToSpeech.java index 51ac5dbe2..76172bdee 100644 --- a/llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmTextToSpeech.java +++ b/llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmTextToSpeech.java @@ -4,6 +4,8 @@ package net.ladenthin.llama.ffm; import static java.lang.foreign.ValueLayout.ADDRESS; +import static java.lang.foreign.ValueLayout.JAVA_BOOLEAN; +import static net.ladenthin.llama.ffm.JllamaNative.booleanSlot; import static net.ladenthin.llama.ffm.JllamaNative.cString; import static net.ladenthin.llama.ffm.JllamaNative.invoke; import static net.ladenthin.llama.ffm.JllamaNative.longSlot; @@ -22,7 +24,7 @@ */ public final class FfmTextToSpeech implements AutoCloseable { - /** The two native calls on a loaded pair's handle; a seam so the locking is testable without it. */ + /** The native calls on a loaded pair's handle; a seam so the locking is testable without it. */ interface Backend { /** @@ -40,6 +42,14 @@ interface Backend { byte[] synthesize(MemorySegment handle, String text, String speakerReferencePath, String language, int maxFrames, int topK, int seed); + /** + * Whether the pair on {@code handle} clones a voice from a speaker reference. + * + * @param handle the loaded pair + * @return {@code true} if a speaker reference is accepted + */ + boolean supportsSpeakerReference(MemorySegment handle); + /** * Frees {@code handle}, the engine with it. * @@ -63,6 +73,15 @@ public byte[] synthesize(MemorySegment handle, String text, String speakerRefere } } + @Override + public boolean supportsSpeakerReference(MemorySegment handle) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment supported = booleanSlot(arena); + JllamaNative.call(JllamaNative.TTS_SUPPORTS_SPEAKER_REFERENCE, handle, supported); + return supported.get(JAVA_BOOLEAN, 0); + } + } + @Override public void free(MemorySegment handle) { invoke(JllamaNative.TTS_FREE, handle); @@ -120,11 +139,32 @@ public static FfmTextToSpeech load(String modelPath, String mmprojPath, int gpuL } } + /** + * Whether this pair clones a voice from a speaker reference, i.e. whether {@link #synthesize} + * accepts a {@code speakerReferencePath} ({@code mtmd_helper_cap::audio_gen_spk_ref}: Qwen3-TTS + * and Pocket TTS do, Soprano does not). + * + * @return {@code true} if a speaker reference is accepted + * @throws IllegalStateException when the pair is closed + */ + public boolean supportsSpeakerReference() { + lifecycleLock.readLock().lock(); + try { + if (closed) { + throw new IllegalStateException("the model is closed"); + } + return backend.supportsSpeakerReference(handle); + } finally { + lifecycleLock.readLock().unlock(); + } + } + /** * Synthesises text. * * @param text the text to speak - * @param speakerReferencePath a WAV to clone the voice from, or {@code null} + * @param speakerReferencePath a WAV to clone the voice from, or {@code null}; refused with a + * {@link LlamaException} when {@link #supportsSpeakerReference()} is {@code false} * @param language the language ({@code "english"}, {@code "chinese"}, ...), or {@code null} * @param maxFrames the bound on the generated frames * @param topK the sampling top-k diff --git a/llama-ffm/src/main/java/net/ladenthin/llama/ffm/JllamaNative.java b/llama-ffm/src/main/java/net/ladenthin/llama/ffm/JllamaNative.java index a7490f17c..17a62cd2f 100644 --- a/llama-ffm/src/main/java/net/ladenthin/llama/ffm/JllamaNative.java +++ b/llama-ffm/src/main/java/net/ladenthin/llama/ffm/JllamaNative.java @@ -144,6 +144,8 @@ final class JllamaNative { static final MethodHandle TTS_SYNTHESIZE = downcall( "jllama_tts_synthesize", json(ADDRESS, ADDRESS, ADDRESS, ADDRESS, JAVA_INT, JAVA_INT, JAVA_INT, ADDRESS, ADDRESS)); + static final MethodHandle TTS_SUPPORTS_SPEAKER_REFERENCE = + downcall("jllama_tts_supports_speaker_reference", json(ADDRESS, ADDRESS)); static final MethodHandle TTS_FREE = downcall("jllama_tts_free", FunctionDescriptor.ofVoid(ADDRESS)); private JllamaNative() {} @@ -344,4 +346,9 @@ static MemorySegment pointerSlot(Arena arena) { static MemorySegment longSlot(Arena arena) { return arena.allocate(JAVA_LONG); } + + /** An out-slot for a C {@code bool}. */ + static MemorySegment booleanSlot(Arena arena) { + return arena.allocate(JAVA_BOOLEAN); + } } diff --git a/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmLlamaModelIntegrationTest.java b/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmLlamaModelIntegrationTest.java index 7b67ce700..8be124b43 100644 --- a/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmLlamaModelIntegrationTest.java +++ b/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmLlamaModelIntegrationTest.java @@ -76,6 +76,11 @@ void metaDescribesTheModel() throws IOException { JsonNode meta = JSON.readTree(model.meta()); assertThat(meta.path("n_vocab").asInt(), greaterThan(0)); assertTrue(meta.has("metadata")); + // a model without a projector chats and neither generates nor streams audio (b11901) + JsonNode capabilities = meta.path("capabilities"); + assertTrue(capabilities.path("chat").asBoolean(false), capabilities.toString()); + assertFalse(capabilities.path("audio_gen").asBoolean(true), capabilities.toString()); + assertFalse(capabilities.path("audio_stream").asBoolean(true), capabilities.toString()); } @Test diff --git a/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmTestSupport.java b/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmTestSupport.java index 6ec8ddb43..0d44a2ff5 100644 --- a/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmTestSupport.java +++ b/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmTestSupport.java @@ -16,6 +16,19 @@ final class FfmTestSupport { static final String DEFAULT_MODEL = "models/AMD-Llama-135m-code.Q2_K.gguf"; + /** The TTS backbone the TTS integration test loads; CI's Qwen3-TTS backbone by default. */ + static final String TTS_MODEL_PROPERTY = "net.ladenthin.llama.ffm.tts.model"; + + static final String DEFAULT_TTS_MODEL = "models/Qwen3-TTS-12Hz-1.7B-Base-Q4_K_M.gguf"; + + /** The TTS mmproj the TTS integration test loads; CI's Qwen3-TTS mmproj by default. */ + static final String TTS_MMPROJ_PROPERTY = "net.ladenthin.llama.ffm.tts.mmproj"; + + static final String DEFAULT_TTS_MMPROJ = "models/mmproj-Qwen3-TTS-12Hz-1.7B-Base-Q8_0.gguf"; + + /** The speaker reference clip committed with the core's tests (2.6 s, 48 kHz stereo WAV). */ + static final String SPEAKER_REFERENCE = "llama/src/test/resources/audios/sample.wav"; + private FfmTestSupport() {} /** @@ -36,7 +49,17 @@ static void assumeNativesLoadable() { * the resolution the core's {@code TestConstants} does. */ static Path modelPath() { - String configured = System.getProperty(MODEL_PROPERTY, DEFAULT_MODEL); + return resolve(System.getProperty(MODEL_PROPERTY, DEFAULT_MODEL)); + } + + /** + * {@code configured} as an existing path: absolute or module-relative first, then relative to + * the reactor root; unchanged when neither exists. + * + * @param configured the path as configured + * @return the path to use + */ + static Path resolve(String configured) { Path candidate = Paths.get(configured); if (candidate.isAbsolute() || Files.exists(candidate)) { return candidate; @@ -45,6 +68,21 @@ static Path modelPath() { return Files.exists(fromParent) ? fromParent : candidate; } + /** + * Skips the test when libjllama is not loadable or the file the property names (or its default) + * is absent. + * + * @param property the system property naming the file + * @param defaultPath the path used when the property is unset + * @return the resolved, existing path + */ + static Path assumeFile(String property, String defaultPath) { + assumeNativesLoadable(); + Path file = resolve(System.getProperty(property, defaultPath)); + Assumptions.assumeTrue(Files.exists(file), "file not found: " + file + " (-D" + property + ")"); + return file; + } + /** Skips the test when the model is absent. */ static Path assumeModel() { assumeNativesLoadable(); diff --git a/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmTextToSpeechIntegrationTest.java b/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmTextToSpeechIntegrationTest.java new file mode 100644 index 000000000..06ba1a4c9 --- /dev/null +++ b/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmTextToSpeechIntegrationTest.java @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2026 Bernard Ladenthin +// +// SPDX-License-Identifier: MIT +package net.ladenthin.llama.ffm; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * {@link FfmTextToSpeech} over a real Qwen3-TTS pair (CI's backbone and mmproj, {@code + * -Dnet.ladenthin.llama.ffm.tts.model} / {@code .tts.mmproj} for others): the speaker-reference + * capability and a synthesis with a voice cloned from the clip the core's tests commit. Self-skips + * without the natives or the models, which CI does not allow. + */ +class FfmTextToSpeechIntegrationTest { + + /** Canonical RIFF/WAVE header size in bytes (16-bit PCM, no extra chunks). */ + private static final int WAV_HEADER_BYTES = 44; + + /** About four seconds at Qwen3-TTS's 12 frames per second: enough to prove the clip was used. */ + private static final int MAX_FRAMES = 48; + + private static FfmTextToSpeech tts; + + @BeforeAll + static void load() { + Path model = FfmTestSupport.assumeFile(FfmTestSupport.TTS_MODEL_PROPERTY, FfmTestSupport.DEFAULT_TTS_MODEL); + Path mmproj = + FfmTestSupport.assumeFile(FfmTestSupport.TTS_MMPROJ_PROPERTY, FfmTestSupport.DEFAULT_TTS_MMPROJ); + tts = FfmTextToSpeech.load(model.toString(), mmproj.toString(), 0, 0); + } + + @AfterAll + static void free() { + if (tts != null) { + tts.close(); + } + } + + @Test + void qwen3TtsClonesAVoiceFromASpeakerReference() { + // mtmd_helper_cap::audio_gen_spk_ref through jllama_tts_supports_speaker_reference's bool slot + assertTrue(tts.supportsSpeakerReference()); + } + + @Test + void synthesisWithASpeakerReferenceReturnsAWav() { + Path reference = FfmTestSupport.resolve(FfmTestSupport.SPEAKER_REFERENCE); + byte[] wav = tts.synthesize("hello from llama", reference.toString(), "english", MAX_FRAMES, 4, 0); + + assertThat(wav.length, greaterThan(WAV_HEADER_BYTES)); + assertEquals("RIFF", new String(wav, 0, 4, StandardCharsets.US_ASCII)); + assertEquals("WAVE", new String(wav, 8, 4, StandardCharsets.US_ASCII)); + } +} diff --git a/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmTextToSpeechTest.java b/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmTextToSpeechTest.java index 02fa1f95d..fccf9117d 100644 --- a/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmTextToSpeechTest.java +++ b/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmTextToSpeechTest.java @@ -39,6 +39,7 @@ private static final class GatedBackend implements FfmTextToSpeech.Backend { final List events = new CopyOnWriteArrayList<>(); final CountDownLatch insideSynthesize = new CountDownLatch(1); final CountDownLatch releaseSynthesize = new CountDownLatch(1); + volatile boolean speakerReference; @Override public byte[] synthesize(MemorySegment handle, String text, String speakerReferencePath, String language, @@ -54,6 +55,12 @@ public byte[] synthesize(MemorySegment handle, String text, String speakerRefere return WAV; } + @Override + public boolean supportsSpeakerReference(MemorySegment handle) { + events.add("supportsSpeakerReference"); + return speakerReference; + } + @Override public void free(MemorySegment handle) { events.add("free (deletes the engine)"); @@ -114,6 +121,27 @@ void synthesizeReturnsTheBackendsWav() { assertThat(tts.synthesize("hi", null, null, 10, 1, 1), is(WAV)); } + @Test + void supportsSpeakerReferenceReturnsTheBackendsAnswer() { + GatedBackend backend = new GatedBackend(); + FfmTextToSpeech tts = new FfmTextToSpeech(MemorySegment.NULL, backend); + + assertThat(tts.supportsSpeakerReference(), is(false)); + backend.speakerReference = true; + assertThat(tts.supportsSpeakerReference(), is(true)); + } + + @Test + void supportsSpeakerReferenceAfterCloseIsRefusedWithoutTouchingTheHandle() { + GatedBackend backend = new GatedBackend(); + FfmTextToSpeech tts = new FfmTextToSpeech(MemorySegment.NULL, backend); + + tts.close(); + + assertThrows(IllegalStateException.class, tts::supportsSpeakerReference); + assertThat(backend.events, contains("free (deletes the engine)")); + } + @Test void synthesizeWithoutTextIsRefused() { FfmTextToSpeech tts = new FfmTextToSpeech(MemorySegment.NULL, new GatedBackend()); diff --git a/llama-ffm/src/test/java/net/ladenthin/llama/ffm/JllamaNativeTest.java b/llama-ffm/src/test/java/net/ladenthin/llama/ffm/JllamaNativeTest.java index 1c80d89ce..f377c4484 100644 --- a/llama-ffm/src/test/java/net/ladenthin/llama/ffm/JllamaNativeTest.java +++ b/llama-ffm/src/test/java/net/ladenthin/llama/ffm/JllamaNativeTest.java @@ -165,4 +165,19 @@ void rpcServerDevicesResolveTheCpuAndRefuseAnUnknownName() { LlamaException e = assertThrows(LlamaException.class, () -> FfmRpcServer.devices("NoSuchDevice9")); assertThat(e.getMessage(), containsString("CPU")); } + + /** + * The speaker-reference query resolves and reaches the library without a TTS model: a null + * handle is the caller's mistake, refused before the out-slot is written. The answer for a real + * pair is FfmTextToSpeechIntegrationTest's. + */ + @Test + void ttsSupportsSpeakerReferenceRefusesANullHandle() { + try (Arena arena = Arena.ofConfined()) { + MemorySegment supported = JllamaNative.booleanSlot(arena); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> JllamaNative.call(JllamaNative.TTS_SUPPORTS_SPEAKER_REFERENCE, MemorySegment.NULL, supported)); + assertThat(e.getMessage(), containsString("TextToSpeech handle is null")); + } + } } diff --git a/llama/CMakeLists.txt b/llama/CMakeLists.txt index c2778f3d5..188f66767 100644 --- a/llama/CMakeLists.txt +++ b/llama/CMakeLists.txt @@ -356,8 +356,8 @@ endif() # hash, every other consumer keeps the number, and the pin check after FetchContent_MakeAvailable # asserts that the fetched commit's count IS the number. Clear it once the tag exists; # `llama-next-version.sh` says when, and `llama-next-version.sh --resolve b` prints the hash. -set(JLLAMA_GIT_TAG b11887) -set(JLLAMA_GIT_COMMIT "6050b467c07a95c7b87debd4ddac2f8ba8d9b807") # the commit the tag b11887 names; kept as the pin (a tag name can move) +set(JLLAMA_GIT_TAG b11901) +set(JLLAMA_GIT_COMMIT "5320c87ff0d994845b44ce7976f10055ec697447") # the commit the tag b11901 names; kept as the pin (a tag name can move) if(NOT JLLAMA_GIT_TAG MATCHES "^b[0-9]+$") message(FATAL_ERROR "JLLAMA_GIT_TAG must be a llama.cpp release number b, got '${JLLAMA_GIT_TAG}'") endif() diff --git a/llama/legal/natives/NOTICE.md b/llama/legal/natives/NOTICE.md index 2d003d9f4..47f83c76c 100644 --- a/llama/legal/natives/NOTICE.md +++ b/llama/legal/natives/NOTICE.md @@ -22,7 +22,7 @@ texts are not needed here. | Component | Version | Compiled into | SPDX | Texts | Source | |---|---|---|---|---|---| -| llama.cpp (ggml / llama / mtmd / llama-common / tools/server / CPU+Metal+GPU kernels / WebUI own code) | b11887 6050b467c07a95c7b87debd4ddac2f8ba8d9b807 (texts unchanged since fa3645feb) | all 23 jars + both AARs | MIT | `llama.cpp` | https://github.com/ggml-org/llama.cpp/blob/fa3645feb5f47cbb30df5e6d031c22543d43d240/LICENSE | +| llama.cpp (ggml / llama / mtmd / llama-common / tools/server / CPU+Metal+GPU kernels / WebUI own code) | b11901 5320c87ff0d994845b44ce7976f10055ec697447 (texts unchanged since fa3645feb) | all 23 jars + both AARs | MIT | `llama.cpp` | https://github.com/ggml-org/llama.cpp/blob/fa3645feb5f47cbb30df5e6d031c22543d43d240/LICENSE | | nlohmann/json | 3.12.0 + vendor/nlohmann/json-deep-nesting.patch | library jars | MIT | `nlohmann-json` | https://github.com/ggml-org/llama.cpp/blob/fa3645feb5f47cbb30df5e6d031c22543d43d240/licenses/LICENSE-jsonhpp | | cpp-httplib | v0.62.0 | library jars | MIT | `cpp-httplib` | https://github.com/ggml-org/llama.cpp/blob/fa3645feb5f47cbb30df5e6d031c22543d43d240/vendor/cpp-httplib/LICENSE | | BoringSSL (ssl + crypto; static; OPENSSL_NO_ASM) | 0.20261020.0 = d3bf4795ad5b79d646cddf2fec687da5b9b502db | library jars except cpu-android-* and cpu-linux-s390x | Apache-2.0 | `boringssl` | https://boringssl.googlesource.com/boringssl/+/refs/tags/0.20261020.0/LICENSE | @@ -47,7 +47,7 @@ texts are not needed here. | WebUI vendored big-integer | in tree | library jars (embedded ui.cpp) | Unlicense | `webui-big-integer` | tools/ui/src/lib/vendors/big-integer/LICENSE | | WebUI vendored decimal.js | in tree | library jars (embedded ui.cpp) | MIT | `webui-decimal.js` | tools/ui/src/lib/vendors/decimal.js/LICENCE.md | | WebUI vendored nerdamer-prime | in tree | library jars (embedded ui.cpp) | MIT | `webui-nerdamer-prime` | tools/ui/src/lib/vendors/nerdamer-prime/LICENSE | -| WebUI npm bundle (JS + CSS incl. KaTeX fonts + workbox) | tools/ui/package-lock.json at b11887 | library jars (every library build job downloads webui-generated) (upper bound (tree-shaking / server-only deps)) | 337 pkgs: MIT 271, ISC 43, BSD-3-Clause 9, Apache-2.0 5, BSD-2-Clause 2, MPL-2.0 OR Apache-2.0 1, Unlicense 1, 5 without field (MIT files); 19 MIT packages ship no license file, the MIT text follows them | `webui-npm` | webui-npm/packages.csv | +| WebUI npm bundle (JS + CSS incl. KaTeX fonts + workbox) | tools/ui/package-lock.json at b11901 | library jars (every library build job downloads webui-generated) (upper bound (tree-shaking / server-only deps)) | 337 pkgs: MIT 271, ISC 43, BSD-3-Clause 9, Apache-2.0 5, BSD-2-Clause 2, MPL-2.0 OR Apache-2.0 1, Unlicense 1, 5 without field (MIT files); 19 MIT packages ship no license file, the MIT text follows them | `webui-npm` | webui-npm/packages.csv | | WebUI third-party logos (huggingface.ico / exa.ico / context7.png / github-*.png) | - | library jars (yes) | trademarks (no license file) | `(not shipped)` | tools/ui/static | | CCCL (CUB / Thrust / libcu++ headers) | v3.4.3 | cuda13-linux-x86-64 + cuda13-windows-x86-64 | Apache-2.0 WITH LLVM-exception AND Apache-2.0 AND BSD-3-Clause (CUB) | `cccl` | https://github.com/NVIDIA/cccl/blob/v3.4.3/LICENSE | | CUDA Toolkit headers / libdevice | CUDA 13.4 | cuda13 modules (yes) | LicenseRef-NVIDIA-CUDA-EULA | `(not shipped)` | - | diff --git a/llama/spotbugs-exclude.xml b/llama/spotbugs-exclude.xml index 7fbc3714b..fcf8ef71e 100644 --- a/llama/spotbugs-exclude.xml +++ b/llama/spotbugs-exclude.xml @@ -691,8 +691,8 @@ SPDX-License-Identifier: MIT `long handle` to the native OuteTTS engine. A generated toString would emit just that pointer value (no meaningful state), exactly the IMC_IMMATURE_CLASS_NO_TOSTRING / java/default-tostring case suppressed for LlamaModelBackend above, so toString is intentionally omitted. And - synthesize() guards the closed handle with a fixed, descriptive precondition message - ("TextToSpeech is closed") — the value is closed-or-not by definition at that throw, with no + requireOpen() (behind synthesize() and supportsSpeakerReference()) guards the closed handle + with a fixed, descriptive precondition message ("TextToSpeech is closed") — the value is closed-or-not by definition at that throw, with no extra state worth adding (same rationale as the server request-parser WEM suppressions above). --> @@ -702,7 +702,7 @@ SPDX-License-Identifier: MIT - +