Skip to content

Commit a7b9ea2

Browse files
committed
package-fatjars: keep only the target's own CPU natives in each all-backends jar
Each llama-<v>-all-<os>-<arch> fat jar was built as a copy of the default fat jar plus the target's backend trees, so it carried the CPU natives of all nine platforms: ~71.5 MB of 341.5 MB in all-windows-x86-64 (per-tree sizes from run 36606543343). The introducing commit (aff68f5) gives no reason for that; it was how the jar was assembled, and the README only described it. Every CPU tree except the jar's exact OS+arch is now removed before the backends are added. That includes Windows/x86 and Windows/aarch64 in all-windows-x86-64, so a 32-bit JVM on 64-bit Windows needs the default fat jar, which is unchanged and still carries every platform. Trees are the upper-case directories below net/ladenthin/llama/ (OSInfo folder names; Java packages are lower-case), matched by exact path component: a Linux* prefix would also hit Linux-Android, and a pattern without the directory's trailing slash matches LlamaModel.class. New fail-loud checks: the filter matched something; the own CPU library is present and byte-identical to the default jar's; no other <OS>/<ARCH> tree is left; the .class count equals the default jar's. README: the all-backends jars no longer claim CPU natives for every platform. CLAUDE.md: records the filter, the decision, and that the "natives missing from a jar" incidents in the history (7ad8066, 40d564c) concern the default jar's collection, not this step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AytmJF9faEiQEVt6eetQS2
1 parent 5f836c5 commit a7b9ea2

3 files changed

Lines changed: 74 additions & 13 deletions

File tree

‎.github/package-fatjars.sh‎

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,25 @@
99
#
1010
# For every OS/arch that has GPU classifier jars, the default jar-with-dependencies
1111
# uber jar (library classes + Java runtime deps + default CPU natives for every
12-
# platform) is copied and each backend's native tree is added under a backend
13-
# subdirectory (net/ladenthin/llama/<OS>/<ARCH>/<backend>/), together with a
14-
# jllama-backends.txt manifest listing the backends in priority order. LlamaLoader
15-
# reads that manifest at runtime and loads the first backend whose library loads,
16-
# falling back to the default CPU library (see LlamaLoader.BACKEND_MANIFEST_FILE).
12+
# platform) is copied, the CPU natives of every OTHER platform are removed, and each
13+
# backend's native tree is added under a backend subdirectory
14+
# (net/ladenthin/llama/<OS>/<ARCH>/<backend>/), together with a jllama-backends.txt
15+
# manifest listing the backends in priority order. LlamaLoader reads that manifest at
16+
# runtime and loads the first backend whose library loads, falling back to the
17+
# target's CPU library (see LlamaLoader.BACKEND_MANIFEST_FILE).
18+
#
19+
# The foreign CPU trees are dead weight in a jar named for one OS/arch (~71 MB of
20+
# 342 MB in all-windows-x86-64). Only the exact OS+arch of the jar name is kept, so
21+
# all-windows-x86-64 also loses Windows/x86 and Windows/aarch64: a 32-bit JVM finds
22+
# no natives there. The default fat jar (section 7) still carries every platform.
1723
#
1824
# Fail-loud invariants (a broken invariant must red the pipeline, never skip):
1925
# * every <classifier> in llama/pom.xml must be parseable/explicitly excluded,
2026
# * the pom classifier set and the on-disk classifier-jar set must match exactly,
2127
# * every expected backend library must end up inside the combined jar,
22-
# * the Main-Class of the combined jar must survive the zip update.
28+
# * the Main-Class of the combined jar must survive the zip update,
29+
# * a combined jar keeps its own CPU library (byte-identical), carries no other CPU
30+
# tree, had at least one removed, and keeps every .class of the default fat jar.
2331
#
2432
# Usage: package-fatjars.sh <jars-dir> <out-dir> [pom]
2533
# jars-dir directory holding the `llama-jars` artifact (llama/target/*.jar)
@@ -62,6 +70,18 @@ is_excluded() {
6270
return 1
6371
}
6472

73+
# Native CPU trees are the upper-case DIRECTORIES below net/ladenthin/llama/ (OSInfo
74+
# folder names: Linux, Linux-Android, Mac, Windows, ...); Java packages there are
75+
# lower-case. (Top-level class files such as LlamaModel.class are upper-case too, so
76+
# every match must require the trailing slash of a directory.) Prints <OS>/<ARCH>, sorted.
77+
native_trees() {
78+
unzip -Z1 "$1" | sed -n -E 's#^net/ladenthin/llama/([A-Z][^/]*/[^/]+)/.*#\1#p' | sort -u
79+
}
80+
81+
class_count() {
82+
unzip -Z1 "$1" | grep -c '\.class$'
83+
}
84+
6585
backend_priority_index() {
6686
local backend="$1" i
6787
for i in "${!BACKEND_PRIORITY[@]}"; do
@@ -194,6 +214,16 @@ for target in $(printf '%s\n' "${!TARGET_BACKENDS[@]}" | sort); do
194214

195215
out_jar="$OUT_DIR/llama-$VERSION-all-$target-jar-with-dependencies.jar"
196216
cp "$BASE_FAT_JAR" "$out_jar"
217+
# Drop every CPU tree but this target's (see the header). Matched by exact path
218+
# components, never by prefix: a Linux* glob would also hit Linux-Android.
219+
foreign_entries="$WORK_DIR/foreign-$target.txt"
220+
unzip -Z1 "$out_jar" \
221+
| awk -v keep="$resource_dir/" -v own_os="net/ladenthin/llama/$os_folder/" \
222+
'/^net\/ladenthin\/llama\/[A-Z][^\/]*\// && index($0, keep) != 1 && $0 != own_os' \
223+
> "$foreign_entries"
224+
[ -s "$foreign_entries" ] \
225+
|| fail "$out_jar: no foreign CPU tree to remove — did the OSInfo folder names change?"
226+
zip -q -d -nw "$out_jar" -@ < "$foreign_entries" || fail "$out_jar: removing foreign CPU trees failed"
197227
(cd "$staging" && zip -q -ur "$out_jar" net)
198228

199229
# --- Verify the combined jar -----------------------------------------------------
@@ -206,6 +236,13 @@ for target in $(printf '%s\n' "${!TARGET_BACKENDS[@]}" | sort); do
206236
|| fail "$out_jar: missing $resource_dir/jllama-backends.txt"
207237
unzip -p "$out_jar" META-INF/MANIFEST.MF | grep -qF "Main-Class: $MAIN_CLASS" \
208238
|| fail "$out_jar: Main-Class $MAIN_CLASS did not survive the zip update"
239+
unzip -p "$out_jar" "$resource_dir/$main_lib" | cmp -s - <(unzip -p "$BASE_FAT_JAR" "$resource_dir/$main_lib") \
240+
|| fail "$out_jar: CPU fallback $resource_dir/$main_lib is missing or differs from the default fat jar's"
241+
out_trees="$(native_trees "$out_jar")"
242+
[ "$out_trees" = "$os_folder/$arch_folder" ] \
243+
|| fail "$out_jar: expected only the CPU tree $os_folder/$arch_folder, found: $(echo "$out_trees" | tr '\n' ' ')"
244+
[ "$(class_count "$out_jar")" -eq "$(class_count "$BASE_FAT_JAR")" ] \
245+
|| fail "$out_jar: .class count differs from the default fat jar — the CPU-tree filter removed classes"
209246
sample_backend="${ordered_backends[0]}"
210247
unzip -p "$out_jar" "$resource_dir/$sample_backend/$main_lib" \
211248
| cmp -s - "$staging/$resource_dir/$sample_backend/$main_lib" \

‎CLAUDE.md‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,12 +470,33 @@ Mechanism (three pieces):
470470
backends missing from its priority table, missing native trees, or zip-update corruption
471471
(entry list, `Main-Class`, sample byte-compare). Excluded by design: `msvc-windows`
472472
(redundant CPU variant) and `opencl-android-aarch64` (no `java -jar` on Android). For each
473-
OS/arch it copies the default fat jar and adds every backend's native tree under
473+
OS/arch it copies the default fat jar, **removes the CPU natives of every other platform**,
474+
and adds every backend's native tree under
474475
`net/ladenthin/llama/<OS>/<ARCH>/<backend>/` plus a **`jllama-backends.txt`** manifest
475476
(backends in priority order `cuda13 rocm sycl-fp16 sycl-fp32 sycl vulkan opencl openvino`;
476477
extra tokens per line list sibling files such as openvino-windows' bundled `OpenCL.dll`).
477478
**A new classifier fails this script until it is consciously ranked/excluded** — that is the
478479
no-silent-gaps guarantee.
480+
481+
**Only the jar's own OS+arch CPU tree is kept.** Originally each combined jar was the
482+
default fat jar plus backends, i.e. it carried the CPU natives of all nine platforms — ~71.5 MB
483+
of dead weight in `all-windows-x86-64` (341.5 MB → ~270 MB, computed from the per-tree sizes of
484+
run 36606543343). That was how the jar was built, not a requirement: the introducing commit
485+
(`aff68f5e`) gives no reason, and the README merely described it. The rule is deliberately the
486+
strict one, so `all-windows-x86-64` also loses `Windows/x86` and `Windows/aarch64` — a 32-bit JVM
487+
on 64-bit Windows needs the default fat jar, which is untouched and still runs everywhere. The
488+
own tree must survive: it is `LlamaLoader`'s fallback when no backend loads. The "natives missing
489+
from a jar" incidents in the history are a different layer — the **default** jar's collection
490+
(`package` once lacked `needs:` on three build jobs, `7ad8066a`; `merge-native-artifacts.sh`
491+
guards the glob since `40d564c0`), which this filter sits downstream of.
492+
Trees are found as the **upper-case directories** below `net/ladenthin/llama/` (OSInfo folder
493+
names; Java packages are lower-case) and matched by exact path component — a `Linux*` prefix
494+
would also hit `Linux-Android`, and a pattern without the trailing `/` of a directory matches
495+
upper-case class files such as `LlamaModel.class`. Fail-loud checks: before the removal, that
496+
the filter matched anything at all; after the zip update, that the own CPU library is present
497+
and byte-identical to the default jar's, that **no other `<OS>/<ARCH>` tree is left**, and that
498+
the `.class` count equals the default jar's (the check that caught the missing-slash variant
499+
during development).
479500
2. **`LlamaLoader` backend selection** — when (and only when) the manifest resource exists,
480501
the loader tries each backend subdirectory in order: extract into a per-backend temp subdir
481502
(`jllama-backend-<name>/`; backends share file names), load manifest extras first, then the

‎README.md‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -281,12 +281,15 @@ java -jar llama-<version>-all-linux-x86-64-jar-with-dependencies.jar -m model.gg
281281
| `llama-<version>-jar-with-dependencies.jar` | none (CPU only, incl. macOS Metal) | — |
282282

283283
Each all-backends jar contains the library classes, all Java runtime
284-
dependencies, the default CPU natives for **every** platform, and every GPU
285-
backend for its named OS/arch. At startup the loader tries the bundled backends
286-
in priority order (CUDA → ROCm → SYCL → Vulkan → OpenCL → OpenVINO) and uses
287-
the **first one whose native library loads**; if none loads — e.g. no GPU
288-
driver/toolkit installed — it falls back to the CPU natives, so the jar starts
289-
everywhere. The usual GPU policy applies: vendor runtimes are **not** bundled
284+
dependencies, the CPU natives for **its named OS/arch only**, and every GPU
285+
backend for that OS/arch — pick the jar that matches your platform. (A 32-bit
286+
JVM on 64-bit Windows, for example, needs the default
287+
`llama-<version>-jar-with-dependencies.jar` from the table above, which carries
288+
the CPU natives of every platform.) At startup the loader tries the bundled
289+
backends in priority order (CUDA → ROCm → SYCL → Vulkan → OpenCL → OpenVINO)
290+
and uses the **first one whose native library loads**; if none loads — e.g. no
291+
GPU driver/toolkit installed — it falls back to the CPU natives, so the jar
292+
starts on every host of its OS/arch, with or without a GPU. The usual GPU policy applies: vendor runtimes are **not** bundled
290293
(see the classifier table above for what each backend needs on the host).
291294
Force a specific backend with `-Dnet.ladenthin.llama.backend=<name>`
292295
(e.g. `vulkan`; fails loud instead of falling back) or force CPU with

0 commit comments

Comments
 (0)